기병 카드 크게·내림 · 병과 심볼 = 검은 선 + 흰 채움 · UI 캡처 화면 지정(-uiOnly)(#1104 · PD 2026-11-09)

- 영웅 목록 카드 기병: 배율 ×0.72 → ×0.85 · 머리 끝 +24 · 전신 상한 390 → 460(말 얼굴이 카드 오른쪽 경계에 닿을 만큼 · PD 「조금 더 키우고 내려도」)
- 병과 심볼 = 검은 선(바깥 테두리 + 안쪽 선) + 그 안쪽 흰 채움(PD 「검은선 안쪽은 흰색」 · 배경 틀 없음 유지) · Tools/class_symbols.py 기본값 변경(--mono = 단색)
- CaptureUI -uiOnly <이름,접두*> = 그 화면만 찍고 바로 종료(PD 「수정된 화면만 체크 · 시간·토큰 낭비 줄여」 · 영웅 목록 1장 실측 15초 · 전체 21장 ≈ 3분) · CLAUDE.md 절차 추가
- 테스트 118/118 · console error 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-11-09 20:10:50 +09:00
parent 89a831cd46
commit d5a3b1376d
10 changed files with 43 additions and 10 deletions

View File

@ -125,6 +125,7 @@ namespace TKS.EditorTools
/// UI 리뉴얼 검증 캡처(GUI 에디터 · 사본 프로젝트 권장 · 세이브 = 메모리 전용 · PD #1100):
/// Unity.exe -projectPath &lt;copy&gt; -executeMethod TKS.EditorTools.DataImporter.CaptureUI -uiDir &lt;dir&gt;
/// 화면 10종 + 팝업(1080×1920) + 1536×2048 2장 PNG + log · 끝나면 스스로 종료.
/// <c>-uiOnly hero_list*,hero_detail</c> = 그 화면만 찍고(정확한 이름 또는 「접두*」) 다 찍으면 바로 종료(PD 2026-11-09 「수정된 화면만 체크」 · 이름 = capture_log 의 shot 파일 이름에서 번호 뺀 것).
/// </summary>
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();
}

View File

@ -89,10 +89,12 @@ namespace TKS.OutGame
const float HeadTop = 50f;
/// <summary>걷는 유닛(보병·창병·궁병·책사) = 머리 끝을 더 내려 발이 카드 아래로 숨게(PD 2026-11-09).</summary>
const float FootDrop = 36f;
/// <summary>말 탄 유닛(크기 표 mounted) = 더 작게 · 말 머리가 카드 안에 보이게(PD 2026-11-09).</summary>
const float MountedScale = 0.72f;
/// <summary>말 탄 유닛(크기 표 mounted) = 걷는 유닛보다 작게 · 말 얼굴이 카드 오른쪽 경계에 닿을 만큼(PD 2026-11-09 0.72 → 「조금 더 크게 · 내려도」).</summary>
const float MountedScale = 0.85f;
/// <summary>말 탄 유닛 머리 끝 내림(카드 px · PD 2026-11-09).</summary>
const float MountedDrop = 24f;
/// <summary>말 탄 유닛 전신(발 ~ 머리 끝) 높이 상한(카드 px) — 크기 표 실측이 튀는 그림(예 여포 size_px 449)도 다른 기병과 비슷하게.</summary>
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<Image>();
}
else

View File

@ -12,7 +12,11 @@ namespace TKS.OutGame
public class UICaptureDriver : MonoBehaviour
{
public const string DirKey = "TKS_UI_CAPTURE_DIR";
/// <summary>찍을 화면만(쉼표 · 정확한 이름 또는 「접두*」 · 비면 전부) — PD 2026-11-09 「수정된 화면만 체크」.</summary>
public const string OnlyKey = "TKS_UI_CAPTURE_ONLY";
public string OutputDir;
string[] only;
readonly HashSet<string> matched = new HashSet<string>();
const int W = 1080, H = 1920, W2 = 1536, H2 = 2048;
readonly List<string> log = new List<string>();
@ -27,6 +31,9 @@ namespace TKS.OutGame
UnityEditor.SessionState.EraseString(DirKey);
var d = new GameObject("UICaptureDriver").AddComponent<UICaptureDriver>();
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);
}
/// <summary>지정 목록에 맞는 토큰(없으면 null).</summary>
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++;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -50,7 +50,7 @@ Unity.exe -projectPath <proj> -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 <dir>`(사본).
- **병종 규칙 개정 + 예측 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 <dir>`.
- 아웃게임 코드 = `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 <dir>`. **UI 수정 검증은 고친 화면만 찍는다**(PD 2026-11-09 「매번 모든 화면 체크 = 시간·토큰 낭비」): `Tools/mirror_verify.py` → `Unity.exe -projectPath E:/Nerdnavis/_tks_verify -force-d3d11 -executeMethod TKS.EditorTools.DataImporter.CaptureUI -uiDir <dir> -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 통째로 끈다.

View File

@ -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 = 단색(채움 한 색 · 안쪽 선 = 뚫린 선).
실행: <ComfyUI venv python> -I -B Tools/class_symbols.py [--src <rgba 폴더>] [--fill #RRGGBB] [--line #RRGGBB] [--out Assets/UI/Symbols] [--preview <png>]
출력 = <out>/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))