diff --git a/Assets/Scripts/Editor/DataImporter.cs b/Assets/Scripts/Editor/DataImporter.cs index 6790e675..e326dd40 100644 --- a/Assets/Scripts/Editor/DataImporter.cs +++ b/Assets/Scripts/Editor/DataImporter.cs @@ -125,6 +125,7 @@ namespace TKS.EditorTools /// UI 리뉴얼 검증 캡처(GUI 에디터 · 사본 프로젝트 권장 · 세이브 = 메모리 전용 · PD #1100): /// Unity.exe -projectPath <copy> -executeMethod TKS.EditorTools.DataImporter.CaptureUI -uiDir <dir> /// 화면 10종 + 팝업(1080×1920) + 1536×2048 2장 PNG + log · 끝나면 스스로 종료. + /// -uiOnly hero_list*,hero_detail = 그 화면만 찍고(정확한 이름 또는 「접두*」) 다 찍으면 바로 종료(PD 2026-11-09 「수정된 화면만 체크」 · 이름 = capture_log 의 shot 파일 이름에서 번호 뺀 것). /// public static void CaptureUI() { @@ -135,6 +136,7 @@ namespace TKS.EditorTools EditorSceneManager.OpenScene(ProjectSetup.ScenePath, OpenSceneMode.Single); SessionState.SetBool(SaveStore.MemoryFlagKey, true); SessionState.SetString(UICaptureDriver.DirKey, dir); + SessionState.SetString(UICaptureDriver.OnlyKey, GetArg("-uiOnly") ?? ""); Debug.Log("[TKS] UI 캡처 시작 → " + dir); EditorApplication.EnterPlaymode(); } diff --git a/Assets/Scripts/OutGame/UI/HeroScreens.cs b/Assets/Scripts/OutGame/UI/HeroScreens.cs index 95d7ca63..892f8801 100644 --- a/Assets/Scripts/OutGame/UI/HeroScreens.cs +++ b/Assets/Scripts/OutGame/UI/HeroScreens.cs @@ -89,10 +89,12 @@ namespace TKS.OutGame const float HeadTop = 50f; /// 걷는 유닛(보병·창병·궁병·책사) = 머리 끝을 더 내려 발이 카드 아래로 숨게(PD 2026-11-09). const float FootDrop = 36f; - /// 말 탄 유닛(크기 표 mounted) = 더 작게 · 말 머리가 카드 안에 보이게(PD 2026-11-09). - const float MountedScale = 0.72f; + /// 말 탄 유닛(크기 표 mounted) = 걷는 유닛보다 작게 · 말 얼굴이 카드 오른쪽 경계에 닿을 만큼(PD 2026-11-09 0.72 → 「조금 더 크게 · 내려도」). + const float MountedScale = 0.85f; + /// 말 탄 유닛 머리 끝 내림(카드 px · PD 2026-11-09). + const float MountedDrop = 24f; /// 말 탄 유닛 전신(발 ~ 머리 끝) 높이 상한(카드 px) — 크기 표 실측이 튀는 그림(예 여포 size_px 449)도 다른 기병과 비슷하게. - const float MountedMaxBody = 390f; + const float MountedMaxBody = 460f; public static Image Card(Transform content, OutGameSession s, HeroDb.Hero h, bool deployed, System.Action onTap) { @@ -127,7 +129,7 @@ namespace TKS.OutGame bool mounted = a.Mounted; float k = UnitPx / size * (mounted ? MountedScale : 1f); if (mounted) k = Mathf.Min(k, MountedMaxBody / body); - float top = HeadTop + (mounted ? 0f : FootDrop); + float top = HeadTop + (mounted ? MountedDrop : FootDrop); art = UiKit.Rect("Character", b, new Vector2(0.5f, 1f), new Vector2(0.5f, headY), new Vector2(0f, -top), sp.rect.size * k).gameObject.AddComponent(); } else diff --git a/Assets/Scripts/OutGame/UICaptureDriver.cs b/Assets/Scripts/OutGame/UICaptureDriver.cs index ef9d8625..aea5858c 100644 --- a/Assets/Scripts/OutGame/UICaptureDriver.cs +++ b/Assets/Scripts/OutGame/UICaptureDriver.cs @@ -12,7 +12,11 @@ namespace TKS.OutGame public class UICaptureDriver : MonoBehaviour { public const string DirKey = "TKS_UI_CAPTURE_DIR"; + /// 찍을 화면만(쉼표 · 정확한 이름 또는 「접두*」 · 비면 전부) — PD 2026-11-09 「수정된 화면만 체크」. + public const string OnlyKey = "TKS_UI_CAPTURE_ONLY"; public string OutputDir; + string[] only; + readonly HashSet matched = new HashSet(); const int W = 1080, H = 1920, W2 = 1536, H2 = 2048; readonly List log = new List(); @@ -27,6 +31,9 @@ namespace TKS.OutGame UnityEditor.SessionState.EraseString(DirKey); var d = new GameObject("UICaptureDriver").AddComponent(); d.OutputDir = dir; + string only = UnityEditor.SessionState.GetString(OnlyKey, ""); + UnityEditor.SessionState.EraseString(OnlyKey); + if (!string.IsNullOrEmpty(only)) d.only = only.Split(new[] { ',' }, System.StringSplitOptions.RemoveEmptyEntries); } #endif @@ -170,8 +177,30 @@ namespace TKS.OutGame yield return new WaitForSecondsRealtime(0.3f); } + /// 지정 목록에 맞는 토큰(없으면 null). + string Match(string name) + { + foreach (var t in only) + { + string k = t.Trim(); + if (k.EndsWith("*") ? name.StartsWith(k.Substring(0, k.Length - 1)) : name == k) return k; + } + return null; + } + IEnumerator Shot(Camera cam, string name, int w, int h) { + if (only != null) + { + string k = Match(name); + if (k == null) + { + // 지정 화면을 모두 찍었으면 나머지 흐름(월드맵 · 전투 등)은 건너뛰고 끝낸다 + if (matched.Count >= only.Length) Finish(0); + yield break; + } + matched.Add(k); + } yield return Wait(); string file = Path.Combine(OutputDir, shot.ToString("00") + "_" + name + ".png"); shot++; diff --git a/Assets/UI/Symbols/cls_sym_archer.png b/Assets/UI/Symbols/cls_sym_archer.png index 24a2850e..745fe695 100644 Binary files a/Assets/UI/Symbols/cls_sym_archer.png and b/Assets/UI/Symbols/cls_sym_archer.png differ diff --git a/Assets/UI/Symbols/cls_sym_cavalry.png b/Assets/UI/Symbols/cls_sym_cavalry.png index 4bdfa2a6..099e01b8 100644 Binary files a/Assets/UI/Symbols/cls_sym_cavalry.png and b/Assets/UI/Symbols/cls_sym_cavalry.png differ diff --git a/Assets/UI/Symbols/cls_sym_infantry.png b/Assets/UI/Symbols/cls_sym_infantry.png index 3a60fc20..29ef27b8 100644 Binary files a/Assets/UI/Symbols/cls_sym_infantry.png and b/Assets/UI/Symbols/cls_sym_infantry.png differ diff --git a/Assets/UI/Symbols/cls_sym_spear.png b/Assets/UI/Symbols/cls_sym_spear.png index 9f09f44d..d9d7aaa6 100644 Binary files a/Assets/UI/Symbols/cls_sym_spear.png and b/Assets/UI/Symbols/cls_sym_spear.png differ diff --git a/Assets/UI/Symbols/cls_sym_strategist.png b/Assets/UI/Symbols/cls_sym_strategist.png index 384ab06c..0d791a93 100644 Binary files a/Assets/UI/Symbols/cls_sym_strategist.png and b/Assets/UI/Symbols/cls_sym_strategist.png differ diff --git a/CLAUDE.md b/CLAUDE.md index 0666ef51..82ce8eb8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,7 +50,7 @@ Unity.exe -projectPath -executeMethod TKS.EditorTools.ProjectSetup.AutoPl - **영웅 56명(PD #1096 · 2026-11-06)** — heroes 71행(영웅 56 + 병사 15) · skills 112 · hero_gear 1344 · 신규 effect_key 4(`team_dmg_pct_turn` · `cooldown_add` · `charm` · `heal_self_pct`) + 기존 키 조건 열(받는 피해 p2 = HP 임계/2 보스/3 근접 · p3 = 출전 인원 아군 전원 · 최대 HP·반격 아군 전원 · 피해 % 발동 턴) = `SkillMods`·`SkillSystem` · 대장군 `pushed_ally_attack_on_push` 는 표에서 미사용(처리기 유지). - **지형 기믹 레이어(PD #1097 · 2026-11-06 세션 D)** — SOT = `Assets/Data/Gimmicks/{gimmicks,stage_gimmicks,tutorial_tips}.csv`(기획 `tables/` 사본 · utf-8-sig) → 메뉴 `TKS/Import Gimmicks (기믹 임포트)`(`Editor/GimmickImporter` · 오류 시 자산 불변 · `ai_weights.csv` 도 함께) → `Resources/Gimmicks/GimmickDb`·`StageGimmickTable` · `Resources/AI/AiWeights`. 전투 시작 = `GimmickLayer.Build(stage)`(장-번호 배치 · 배경 위 · 유닛 아래 · 장애물 = 콜라이더 + `GimmickBody` · 지대 = 모양 스윕 · 강 = 늪처럼 통과하는 지대(강 안 ×p3 · 다리 구간 = 효과 없음 · 강가·다리에서 밀려 강물에 들면 빠짐 · PD 2026-11-06 개정) · 겹친 유닛은 빈 자리로). 전투 코드는 질의만: `Combat.FreeTravel`(장애물 = 벽 · 숨은 함정 정지) · `FirstUnitOnArrow`(pass/block/destroy) · `Combat.Damage`(군기 공 · 고지 근접 피격 · 안개 원거리 피격 ×p1 · 책사 광역이 그 반경 안개를 걷음) · `SkillMods.Range`(고지 사거리) · `BattleManager` 턴 시작 `TurnStart`(군기·중독·불·행동 불가). 충돌 반응·폭발·굴림·미끄러짐·지대 = `Gimmicks/ActionResolver.Gimmicks.cs`(partial). 배치 도구 = `TKS/Gimmick Painter (기믹 배치)`. 테스트 = `Tests/PlayMode/GimmickTests` · 캡처 = `-executeMethod TKS.EditorTools.GimmickImporter.Capture -gimmickDir `(사본). - **병종 규칙 개정 + 예측 AI(PD #1098 · 2026-11-06)** — 보병 = 적과 싸운 뒤 튕겨나는 길의 다른 적과 연쇄 교환 1회(`ActionOutcome.Chains`) · 창병 = 상대만 밀리고 자신은 × `spearRecoilRatio` 반대로(밀며 전진 폐기). 적 턴 = `Runtime/AI/TacticalAI`(후보 방위 × 파워 3 × 액티브 → `Sim` 가상 실행(연출 없음 · 분산 0 · 상태 복원) → `ai_weights.csv` 점수 · 서포터 → 돌격 → 사격 · 1기마다 재평가) · 가중치 자산이 없으면 `EnemyAI.Decide`(기존 조준) · 적 액티브 = 1부 보스만(`boss_active`). 가상 실행 중(`Sim.Active`)에는 `Fx`·`UnitView`·`BattleEvents`·HUD 갱신이 꺼진다 — 새 연출 코드도 `Sim.Active` 면 건너뛸 것. 조준 미리보기도 같은 가상 실행(`GimmickPreview` · 숨은 함정 모름). 테스트 = `Tests/PlayMode/TacticalAITests`. -- 아웃게임 코드 = `Assets/Scripts/OutGame/**`(asmref → TKS.Runtime) — `OutGameSession`(표 + 세이브 · 바꾸는 동작은 즉시 저장) · 규칙 `HeroRules`·`EconomyRules`·`GachaRules` · 화면 `UI/`(로비·영웅·편성·거래·결과 패널 · `OutGameRoot` 가 전환). 세이브 = `persistentDataPath/save.json`(테스트·검증 캡처는 `SaveStore.UseMemory` / SessionState `TKS_SAVE_MEMORY` 로 메모리 전용). 출전 = `BattleManager.LineupProvider`(편성 프리셋 → `playerSpawnPoints` 자리 0~2 전열 · 3~4 후열). 화면 캡처 = `Unity.exe -projectPath <사본> -executeMethod TKS.EditorTools.DataImporter.CaptureOutGame -outgameDir `. +- 아웃게임 코드 = `Assets/Scripts/OutGame/**`(asmref → TKS.Runtime) — `OutGameSession`(표 + 세이브 · 바꾸는 동작은 즉시 저장) · 규칙 `HeroRules`·`EconomyRules`·`GachaRules` · 화면 `UI/`(로비·영웅·편성·거래·결과 패널 · `OutGameRoot` 가 전환). 세이브 = `persistentDataPath/save.json`(테스트·검증 캡처는 `SaveStore.UseMemory` / SessionState `TKS_SAVE_MEMORY` 로 메모리 전용). 출전 = `BattleManager.LineupProvider`(편성 프리셋 → `playerSpawnPoints` 자리 0~2 전열 · 3~4 후열). 화면 캡처 = `Unity.exe -projectPath <사본> -executeMethod TKS.EditorTools.DataImporter.CaptureOutGame -outgameDir `. **UI 수정 검증은 고친 화면만 찍는다**(PD 2026-11-09 「매번 모든 화면 체크 = 시간·토큰 낭비」): `Tools/mirror_verify.py` → `Unity.exe -projectPath E:/Nerdnavis/_tks_verify -force-d3d11 -executeMethod TKS.EditorTools.DataImporter.CaptureUI -uiDir -uiOnly hero_list,hero_detail*`(이름 = 정확히 또는 「접두*」 · 다 찍으면 바로 종료 · 1장 ≈ 15초 · 전체 21장 ≈ 3분 · 이름 목록 = `UICaptureDriver` 의 Shot 이름). - **챕터·스테이지 SOT = `Assets/Data/Stages/*.csv`(utf-8-sig · 2026-11-05 145 스테이지 · 기획서 #1086 §4 전사)** — `stages.csv`(chapter·stage·title·type normal|elite|boss·place·faction·병종 4 수·generals `id:병종:true|false`·bg_theme·note) · `chapters.csv`(장 제목·연도·**월드맵 맵 id**·AI 출발값) · `generals.csv`(무장 id → 이름·배율 덮어쓰기) · `factions.csv`(세력 코드 → 이름·병사 아트 접두). 고친 뒤 메뉴 `TKS/Import Stages (스테이지 임포트)`(`StageImporter` · 형식 오류면 자산 미변경) → `BalanceConfig.chapters·stages`. 적 좌표 = `BalanceConfig.formation` 규칙(전위·중군·후군·장수 줄 · `StageTable.Enemies`) · 장수 배율 = `generals.csv` 가 비면 `boss/eliteGeneral*Mult`. 월드맵 노드 = 스테이지 표(챕터별 수 · 보스 = type boss) · 노드 자리 = `ChapterMap/Data/<맵>.json` `slots_N`(없으면 길 위 균등). - 규칙 해석 변경 = `Assets/Scripts/Runtime/ActionResolver.cs`(병종 4종) · `BattleManager.cs`(턴·승패·창병 대기) · `Combat.cs`(피해 공식·경로 질의). 변경 시 `Assets/Tests/PlayMode/BattleFlowTests.cs` 에 테스트 추가 후 22종 이상 전부 통과 확인. - 콜라이더 질의 전 `Physics2D.SyncTransforms()` · 유닛 제거는 `SetActive(false)` 후 `Destroy` · 자식 렌더러가 있는 표시물은 GameObject 통째로 끈다. diff --git a/Tools/class_symbols.py b/Tools/class_symbols.py index 0eb75e2a..1ceb4329 100644 --- a/Tools/class_symbols.py +++ b/Tools/class_symbols.py @@ -2,7 +2,7 @@ 새로 그리지 않는다 — 확정 납품 메달 아이콘(Assets/UI/Samguk/class/cls_<병과>.png)을 만들 때 쓴 투명 바탕 사물 그림 (아트 R&D `C:/ArtRnD/samguk_ui/_work/class_symbol/rgba/cls_<병과>_s21A_rgba.png` · picked_auto.json = seed 21)의 투명도 = 형태. -기본 = 단색(PD 2026-11-09 「배경 프레임 없이 단색」 · 채움 한 색 · 원본의 진한 안쪽 선 = 뚫린 선) · --outline = 테두리 + 안쪽 선 색(구 방식). +기본 = 검은 선(바깥 테두리 + 원본의 진한 안쪽 선) + 그 안쪽 흰 채움(PD 2026-11-09 「배경 프레임 없이 · 검은 선 안쪽은 흰색」) · --mono = 단색(채움 한 색 · 안쪽 선 = 뚫린 선). 실행: -I -B Tools/class_symbols.py [--src ] [--fill #RRGGBB] [--line #RRGGBB] [--out Assets/UI/Symbols] [--preview ] 출력 = /cls_sym_<병과>.png(256 정사각 · 투명) · --preview = 어두운 원 위 5종 확인 그림. """ @@ -63,17 +63,17 @@ def main(): ap = argparse.ArgumentParser() ap.add_argument("--src", default=SRC) ap.add_argument("--seed", default="s21") - ap.add_argument("--fill", default="#F2D49B") - ap.add_argument("--line", default="#2A2230") + ap.add_argument("--fill", default="#FFFFFF") + ap.add_argument("--line", default="#1A1A22") ap.add_argument("--out", default="Assets/UI/Symbols") ap.add_argument("--preview", default="") - ap.add_argument("--outline", action="store_true", help="테두리 + 진한 안쪽 선(구 방식) · 기본 = 단색") + ap.add_argument("--mono", action="store_true", help="단색(채움 한 색 · 안쪽 선 = 뚫린 선 · 테두리 없음) · 기본 = 검은 선 + 흰 채움") a = ap.parse_args() out = os.path.join(ROOT, a.out) os.makedirs(out, exist_ok=True) tiles = [] for t in TYPES: - g = symbol(os.path.join(a.src, "cls_%s_%sA_rgba.png" % (t, a.seed)), hexrgb(a.fill), hexrgb(a.line), mono=not a.outline) + g = symbol(os.path.join(a.src, "cls_%s_%sA_rgba.png" % (t, a.seed)), hexrgb(a.fill), hexrgb(a.line), mono=a.mono) g.save(os.path.join(out, "cls_sym_%s.png" % t)) tile = Image.new("RGBA", (300, 300), (0, 0, 0, 0)) tile.alpha_composite(g.resize((200, 200), Image.LANCZOS), (50, 50))