챕터 1~10 기획 확정판 월드맵 반영(PD #1091 ③) — 챕터맵 보강 세션 납품 worldmap 10장(QA PASS · md5 20/20) → Resources/ChapterMap/Maps·Data/ch01~ch10(파일 번호 = 기획 장 · 가안 맵 대체) · world.json 길·장소 갱신 · Tools/apply_delivery.py worldmap kind(md5 검증·복사·world.json 항목 교체 · 같은 맵 뒤 납품이 있으면 앞 것 .superseded.json · --kind 필터 · QA 「PASS(…)」 허용) · chapters.csv map = 1:1(3·7장 임시 맵 1 해소) · 노드 = Data/<맵>.json stages[].at(번호로 맞춤 · 없으면 slots_N → 길 균등) · 테스트 41/41(신규: 10장 노드 수·보스 = 5의 배수·지명·길 위·깃발 겹침 없음) · console error 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-11-05 18:56:15 +09:00
parent ce8d45de8f
commit 05cf37a6b5
26 changed files with 20479 additions and 14068 deletions

View File

@ -1,11 +1,11 @@
chapter,title,subtitle,years,map,ai_accuracy,ai_aggro,note chapter,title,subtitle,years,map,ai_accuracy,ai_aggro,note
1,도원결의,황건적의 난,184,ch01,0.5,11, 1,도원결의,황건적의 난,184,ch01,0.5,11,기획 확정판 월드맵(챕터맵 보강 세션 · QA PASS · 납품 인박스 worldmap_ch01 · 노드 = Data/ch01.json stages[].at · PD #1091 ③ 2026-11-05)
2,반동탁연합,사수관·호로관,189–190,ch02,0.6,11, 2,반동탁연합,사수관·호로관,189–190,ch02,0.6,11,기획 확정판 월드맵(챕터맵 보강 세션 · QA PASS · 납품 인박스 worldmap_ch02 · 노드 = Data/ch02.json stages[].at · PD #1091 ③ 2026-11-05)
3,군웅할거,반하·북해·서주,191–194,ch01,0.6,11,맵 신규 전(월드맵 보강) 임시 = 맵 1 재사용 3,군웅할거,반하·북해·서주,191–194,ch03,0.6,11,기획 확정판 월드맵(챕터맵 보강 세션 · QA PASS · 납품 인박스 worldmap_ch03 · 노드 = Data/ch03.json stages[].at · PD #1091 ③ 2026-11-05)
4,여포와 서주,우이·소패·하비,195–198,ch03,0.6,11,맵 3(서주) · 우이·회음 장소 추가 예정 4,여포와 서주,우이·소패·하비,195–198,ch04,0.6,11,기획 확정판 월드맵(챕터맵 보강 세션 · QA PASS · 납품 인박스 worldmap_ch04 · 노드 = Data/ch04.json stages[].at · PD #1091 ③ 2026-11-05)
5,의대조,원술 토벌과 서주,199–200,ch03,0.6,11,맵 3 재사용(권장안) · 허도·망탕산·토산 추가 예정 5,의대조,원술 토벌과 서주,199–200,ch05,0.6,11,기획 확정판 월드맵(챕터맵 보강 세션 · QA PASS · 납품 인박스 worldmap_ch05 · 노드 = Data/ch05.json stages[].at · PD #1091 ③ 2026-11-05)
6,관우 천리행,백마·연진·오관,200,ch04,0.6,11,맵 4 · 백마·연진 추가 예정 6,관우 천리행,백마·연진·오관,200,ch06,0.6,11,기획 확정판 월드맵(챕터맵 보강 세션 · QA PASS · 납품 인박스 worldmap_ch06 · 노드 = Data/ch06.json stages[].at · PD #1091 ③ 2026-11-05)
7,고성 재회,여남과 양산,200–201,ch01,0.6,11,맵 신규 전(월드맵 보강) 임시 = 맵 1 재사용 7,고성 재회,여남과 양산,200–201,ch07,0.6,11,기획 확정판 월드맵(챕터맵 보강 세션 · QA PASS · 납품 인박스 worldmap_ch07 · 노드 = Data/ch07.json stages[].at · PD #1091 ③ 2026-11-05)
8,형주의 객장,서서와 와룡,201–208,ch05,0.6,11,맵 5 · 강하·양양·단계·번성 추가 예정 8,형주의 객장,서서와 와룡,201–208,ch08,0.6,11,기획 확정판 월드맵(챕터맵 보강 세션 · QA PASS · 납품 인박스 worldmap_ch08 · 노드 = Data/ch08.json stages[].at · PD #1091 ③ 2026-11-05)
9,장판파,백성과 함께,208,ch06,0.6,11,맵 6 · 신야·백하 추가 예정 9,장판파,백성과 함께,208,ch09,0.6,11,기획 확정판 월드맵(챕터맵 보강 세션 · QA PASS · 납품 인박스 worldmap_ch09 · 노드 = Data/ch09.json stages[].at · PD #1091 ③ 2026-11-05)
10,적벽대전,,208,ch07,0.65,12,맵 7(적벽) 10,적벽대전,,208,ch10,0.65,12,기획 확정판 월드맵(챕터맵 보강 세션 · QA PASS · 납품 인박스 worldmap_ch10 · 노드 = Data/ch10.json stages[].at · PD #1091 ③ 2026-11-05)

1 chapter title subtitle years map ai_accuracy ai_aggro note
2 1 도원결의 황건적의 난 184 ch01 0.5 11 기획 확정판 월드맵(챕터맵 보강 세션 · QA PASS · 납품 인박스 worldmap_ch01 · 노드 = Data/ch01.json stages[].at · PD #1091 ③ 2026-11-05)
3 2 반동탁연합 사수관·호로관 189–190 ch02 0.6 11 기획 확정판 월드맵(챕터맵 보강 세션 · QA PASS · 납품 인박스 worldmap_ch02 · 노드 = Data/ch02.json stages[].at · PD #1091 ③ 2026-11-05)
4 3 군웅할거 반하·북해·서주 191–194 ch01 ch03 0.6 11 맵 신규 전(월드맵 보강) 임시 = 맵 1 재사용 기획 확정판 월드맵(챕터맵 보강 세션 · QA PASS · 납품 인박스 worldmap_ch03 · 노드 = Data/ch03.json stages[].at · PD #1091 ③ 2026-11-05)
5 4 여포와 서주 우이·소패·하비 195–198 ch03 ch04 0.6 11 맵 3(서주) · 우이·회음 장소 추가 예정 기획 확정판 월드맵(챕터맵 보강 세션 · QA PASS · 납품 인박스 worldmap_ch04 · 노드 = Data/ch04.json stages[].at · PD #1091 ③ 2026-11-05)
6 5 의대조 원술 토벌과 서주 199–200 ch03 ch05 0.6 11 맵 3 재사용(권장안) · 허도·망탕산·토산 추가 예정 기획 확정판 월드맵(챕터맵 보강 세션 · QA PASS · 납품 인박스 worldmap_ch05 · 노드 = Data/ch05.json stages[].at · PD #1091 ③ 2026-11-05)
7 6 관우 천리행 백마·연진·오관 200 ch04 ch06 0.6 11 맵 4 · 백마·연진 추가 예정 기획 확정판 월드맵(챕터맵 보강 세션 · QA PASS · 납품 인박스 worldmap_ch06 · 노드 = Data/ch06.json stages[].at · PD #1091 ③ 2026-11-05)
8 7 고성 재회 여남과 양산 200–201 ch01 ch07 0.6 11 맵 신규 전(월드맵 보강) 임시 = 맵 1 재사용 기획 확정판 월드맵(챕터맵 보강 세션 · QA PASS · 납품 인박스 worldmap_ch07 · 노드 = Data/ch07.json stages[].at · PD #1091 ③ 2026-11-05)
9 8 형주의 객장 서서와 와룡 201–208 ch05 ch08 0.6 11 맵 5 · 강하·양양·단계·번성 추가 예정 기획 확정판 월드맵(챕터맵 보강 세션 · QA PASS · 납품 인박스 worldmap_ch08 · 노드 = Data/ch08.json stages[].at · PD #1091 ③ 2026-11-05)
10 9 장판파 백성과 함께 208 ch06 ch09 0.6 11 맵 6 · 신야·백하 추가 예정 기획 확정판 월드맵(챕터맵 보강 세션 · QA PASS · 납품 인박스 worldmap_ch09 · 노드 = Data/ch09.json stages[].at · PD #1091 ③ 2026-11-05)
11 10 적벽대전 208 ch07 ch10 0.65 12 맵 7(적벽) 기획 확정판 월드맵(챕터맵 보강 세션 · QA PASS · 납품 인박스 worldmap_ch10 · 노드 = Data/ch10.json stages[].at · PD #1091 ③ 2026-11-05)

View File

@ -147,42 +147,42 @@ MonoBehaviour:
title: "\uAD70\uC6C5\uD560\uAC70" title: "\uAD70\uC6C5\uD560\uAC70"
subtitle: "\uBC18\uD558\xB7\uBD81\uD574\xB7\uC11C\uC8FC" subtitle: "\uBC18\uD558\xB7\uBD81\uD574\xB7\uC11C\uC8FC"
years: "191\u2013194" years: "191\u2013194"
map: ch01 map: ch03
- no: 4 - no: 4
title: "\uC5EC\uD3EC\uC640 \uC11C\uC8FC" title: "\uC5EC\uD3EC\uC640 \uC11C\uC8FC"
subtitle: "\uC6B0\uC774\xB7\uC18C\uD328\xB7\uD558\uBE44" subtitle: "\uC6B0\uC774\xB7\uC18C\uD328\xB7\uD558\uBE44"
years: "195\u2013198" years: "195\u2013198"
map: ch03 map: ch04
- no: 5 - no: 5
title: "\uC758\uB300\uC870" title: "\uC758\uB300\uC870"
subtitle: "\uC6D0\uC220 \uD1A0\uBC8C\uACFC \uC11C\uC8FC" subtitle: "\uC6D0\uC220 \uD1A0\uBC8C\uACFC \uC11C\uC8FC"
years: "199\u2013200" years: "199\u2013200"
map: ch03 map: ch05
- no: 6 - no: 6
title: "\uAD00\uC6B0 \uCC9C\uB9AC\uD589" title: "\uAD00\uC6B0 \uCC9C\uB9AC\uD589"
subtitle: "\uBC31\uB9C8\xB7\uC5F0\uC9C4\xB7\uC624\uAD00" subtitle: "\uBC31\uB9C8\xB7\uC5F0\uC9C4\xB7\uC624\uAD00"
years: 200 years: 200
map: ch04 map: ch06
- no: 7 - no: 7
title: "\uACE0\uC131 \uC7AC\uD68C" title: "\uACE0\uC131 \uC7AC\uD68C"
subtitle: "\uC5EC\uB0A8\uACFC \uC591\uC0B0" subtitle: "\uC5EC\uB0A8\uACFC \uC591\uC0B0"
years: "200\u2013201" years: "200\u2013201"
map: ch01 map: ch07
- no: 8 - no: 8
title: "\uD615\uC8FC\uC758 \uAC1D\uC7A5" title: "\uD615\uC8FC\uC758 \uAC1D\uC7A5"
subtitle: "\uC11C\uC11C\uC640 \uC640\uB8E1" subtitle: "\uC11C\uC11C\uC640 \uC640\uB8E1"
years: "201\u2013208" years: "201\u2013208"
map: ch05 map: ch08
- no: 9 - no: 9
title: "\uC7A5\uD310\uD30C" title: "\uC7A5\uD310\uD30C"
subtitle: "\uBC31\uC131\uACFC \uD568\uAED8" subtitle: "\uBC31\uC131\uACFC \uD568\uAED8"
years: 208 years: 208
map: ch06 map: ch09
- no: 10 - no: 10
title: "\uC801\uBCBD\uB300\uC804" title: "\uC801\uBCBD\uB300\uC804"
subtitle: subtitle:
years: 208 years: 208
map: ch07 map: ch10
stages: stages:
- title: "\uD0C1\uD604 \uC678\uACFD \u2014 \uD669\uAC74 \uCC99\uD6C4" - title: "\uD0C1\uD604 \uC678\uACFD \u2014 \uD669\uAC74 \uCC99\uD6C4"
subtitle: "184\uB144" subtitle: "184\uB144"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 MiB

After

Width:  |  Height:  |  Size: 7.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 MiB

After

Width:  |  Height:  |  Size: 7.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 MiB

After

Width:  |  Height:  |  Size: 7.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 MiB

After

Width:  |  Height:  |  Size: 6.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 MiB

After

Width:  |  Height:  |  Size: 7.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 MiB

After

Width:  |  Height:  |  Size: 7.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 MiB

After

Width:  |  Height:  |  Size: 7.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 MiB

After

Width:  |  Height:  |  Size: 6.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 MiB

After

Width:  |  Height:  |  Size: 7.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 MiB

After

Width:  |  Height:  |  Size: 7.0 MiB

View File

@ -8,7 +8,7 @@ using UnityEngine.UI;
namespace TKS namespace TKS
{ {
// ---------- 데이터(Resources/ChapterMap/Data/world.json · 원화 세션 chap_install.py 생성 · 좌표 = 맵 px · 왼쪽 위 원점 · y 아래로) ---------- // ---------- 데이터(Resources/ChapterMap/Data/world.json · 원화 세션 chap_install.py 생성 · 좌표 = 맵 px · 왼쪽 위 원점 · y 아래로) ----------
// 맵 그림·길 = world.json(맵 id 별) · 노드 자리 = Data/<맵>.json 의 slots_N · 게임 챕터·스테이지·노드 수 = BalanceConfig.chapters·stages(스테이지 표 CSV) // 맵 그림·길 = world.json(맵 id 별) · 노드 자리 = Data/<맵>.json 의 stages[].at(기획 확정판 · 없으면 slots_N) · 게임 챕터·스테이지·노드 수 = BalanceConfig.chapters·stages(스테이지 표 CSV)
[Serializable] [Serializable]
public class WorldKitItem public class WorldKitItem
@ -68,6 +68,23 @@ namespace TKS
return null; return null;
} }
/// <summary>Data/&lt;map&gt;.json 의 stages(기획 확정판 맵 · 스테이지 번호·보스·지명·노드 좌표 at). 없으면 null.</summary>
public static AnchorStage[] AnchorStages(string map)
{
var t = Resources.Load<TextAsset>(DataRoot + map);
if (t == null) return null;
try
{
var f = JsonUtility.FromJson<AnchorFile>(t.text);
return f != null && f.stages != null && f.stages.Length > 0 ? f.stages : null;
}
catch (Exception e)
{
Debug.LogWarning("[TKS] " + DataRoot + map + " stages 읽기 실패: " + e.Message);
return null;
}
}
/// <summary>Data/&lt;map&gt;.json(원화 세션 anchors) 의 slots_n(맵 px · 첫 칸 = 출발). 없으면 null.</summary> /// <summary>Data/&lt;map&gt;.json(원화 세션 anchors) 의 slots_n(맵 px · 첫 칸 = 출발). 없으면 null.</summary>
public static Vector2[] AnchorSlots(string map, int n) public static Vector2[] AnchorSlots(string map, int n)
{ {
@ -163,8 +180,28 @@ namespace TKS
} }
} }
/// <summary>Data/&lt;맵&gt;.json 의 stages 한 칸(챕터맵 보강 세션 기획 확정판 · 기획서 §4 번호·유형·지명 + 노드 좌표).</summary>
[Serializable]
public class AnchorStage
{
public int stage;
public bool boss;
public string place;
public string title;
public float[] at; // 맵 px(왼쪽 위 원점 · y 아래로)
}
[Serializable]
public class AnchorFile
{
public AnchorStage[] stages;
}
public enum NodeState { Lock, Open, Clear } public enum NodeState { Lock, Open, Clear }
/// <summary>노드 자리 출처: 기획 확정판 stages[].at · 가안 slots_N · 길 위 균등(대체).</summary>
public enum NodeSource { Stages, Slots, Road }
/// <summary>임시 진행도(실행 중 메모리 · 저장 없음). 챕터별 깬 노드 수.</summary> /// <summary>임시 진행도(실행 중 메모리 · 저장 없음). 챕터별 깬 노드 수.</summary>
public static class WorldProgress public static class WorldProgress
{ {
@ -189,7 +226,7 @@ namespace TKS
/// <summary> /// <summary>
/// 게임 시작 화면 = 챕터 월드맵(스크롤 · 상하좌우로 끌기). 길 위 깃발 노드를 누르면 스테이지 팝업 → 출전 = 전투 시작 · /// 게임 시작 화면 = 챕터 월드맵(스크롤 · 상하좌우로 끌기). 길 위 깃발 노드를 누르면 스테이지 팝업 → 출전 = 전투 시작 ·
/// 전투 결과를 탭하면 월드맵으로 돌아와 다음 노드가 열린다. 그림·길·부품 배치 = world.json · 노드 자리 = anchors slots_N(없으면 길 위 균등) · /// 전투 결과를 탭하면 월드맵으로 돌아와 다음 노드가 열린다. 그림·길·부품 배치 = world.json · 노드 자리 = Data/&lt;맵&gt;.json stages[].at(없으면 slots_N → 길 위 균등) ·
/// 챕터·노드·전투 = BalanceConfig.chapters·stages(스테이지 표 · C45) · 보스 노드 = type boss. /// 챕터·노드·전투 = BalanceConfig.chapters·stages(스테이지 표 · C45) · 보스 노드 = type boss.
/// </summary> /// </summary>
public class WorldMapScreen : MonoBehaviour public class WorldMapScreen : MonoBehaviour
@ -269,13 +306,36 @@ namespace TKS
return wc ?? data.chapters[0]; return wc ?? data.chapters[0];
} }
/// <summary>노드 자리(맵 px) = anchors slots_N · 그 N 이 없으면 길 위 균등.</summary> /// <summary>
public Vector2[] NodeSlots(int ch, out bool fromAnchors) /// 노드 자리(맵 px) = Data/&lt;맵&gt;.json stages[].at(스테이지 번호로 맞춤 · 기획 확정판 맵) → 없거나 번호가 빠지면 slots_N → 그것도 없으면 길 위 균등.
/// </summary>
public Vector2[] NodeSlots(int ch, out NodeSource source)
{ {
int n = NodesIn(ch); var list = HasData && ch >= 0 && ch < cfg.chapters.Length ? cfg.StagesOfChapter(cfg.chapters[ch].no) : new List<int>();
int n = list.Count;
var wc = MapOf(ch); var wc = MapOf(ch);
var plan = wc != null ? WorldData.AnchorStages(wc.map) : null;
if (plan != null)
{
var res = new Vector2[n];
int found = 0;
for (int i = 0; i < n; i++)
foreach (var p in plan)
if (p.stage == cfg.stages[list[i]].stage && p.at != null && p.at.Length >= 2)
{
res[i] = new Vector2(p.at[0], p.at[1]);
found++;
break;
}
if (found == n)
{
source = NodeSource.Stages;
return res;
}
Debug.LogWarning("[TKS] " + wc.map + " stages[].at 이 스테이지 표와 안 맞음(" + found + "/" + n + ") → slots_N");
}
var s = wc != null ? WorldData.AnchorSlots(wc.map, n) : null; var s = wc != null ? WorldData.AnchorSlots(wc.map, n) : null;
fromAnchors = s != null; source = s != null ? NodeSource.Slots : NodeSource.Road;
return s ?? (wc != null ? WorldData.Slots(wc, n) : new Vector2[0]); return s ?? (wc != null ? WorldData.Slots(wc, n) : new Vector2[0]);
} }
@ -392,8 +452,8 @@ namespace TKS
prevButton.interactable = chapter > 0; prevButton.interactable = chapter > 0;
nextButton.interactable = chapter < cfg.chapters.Length - 1; nextButton.interactable = chapter < cfg.chapters.Length - 1;
bool fromAnchors; NodeSource source;
var slots = NodeSlots(chapter, out fromAnchors); var slots = NodeSlots(chapter, out source);
var acc = WorldData.Arc(c); var acc = WorldData.Arc(c);
var len = new float[slots.Length]; var len = new float[slots.Length];
for (int i = 0; i < slots.Length; i++) len[i] = WorldData.ArcOf(c, acc, slots[i]); for (int i = 0; i < slots.Length; i++) len[i] = WorldData.ArcOf(c, acc, slots[i]);

View File

@ -1062,6 +1062,57 @@ namespace TKS.Tests
yield return null; yield return null;
} }
/// <summary>점 p 와 길(world.json 맵 항목의 꺾은선) 사이 최단 거리(맵 px).</summary>
static float DistanceToRoad(WorldChapter c, Vector2 p)
{
float best = float.MaxValue;
for (int i = 1; i < Mathf.Min(c.roadX.Length, c.roadY.Length); i++)
{
var a = new Vector2(c.roadX[i - 1], c.roadY[i - 1]);
var ab = new Vector2(c.roadX[i], c.roadY[i]) - a;
float t = Mathf.Clamp01(Vector2.Dot(p - a, ab) / Mathf.Max(ab.sqrMagnitude, 1e-6f));
best = Mathf.Min(best, Vector2.Distance(a + ab * t, p));
}
return best;
}
[Test]
public void WorldMap_PlanMaps_TenChapters_NodesOnRoad_BossEveryFifth()
{
// PD #1091 ③(2026-11-05): 챕터맵 보강 세션 기획 확정판 10장 · 노드 = Data/<맵>.json stages[].at
var c = Resources.Load<BalanceConfig>("BalanceConfig");
var d = WorldData.Load();
var nodeKit = d.Kit("flag_open");
Assert.IsNotNull(nodeKit, "노드 부품 크기(kit flag_open)");
float onRoad = d.dotSpacing * 0.5f; // 노드 중심이 길 점 간격의 절반 안 = 길 위(납품 실측 ≤ 0.1 px)
foreach (var def in c.chapters)
{
string at = "제" + def.no + "장(" + def.map + ")";
var wc = d.FindMap(def.map);
Assert.IsNotNull(wc, at + " world.json 길");
var plan = WorldData.AnchorStages(def.map);
Assert.IsNotNull(plan, at + " Data/" + def.map + ".json stages");
var list = c.StagesOfChapter(def.no);
Assert.AreEqual(list.Count, plan.Length, at + " 노드 수 = 스테이지 표");
Vector2? prev = null;
for (int i = 0; i < list.Count; i++)
{
var s = c.stages[list[i]];
var p = System.Array.Find(plan, x => x.stage == s.stage);
Assert.IsNotNull(p, at + " " + s.stage + "번 노드");
Assert.AreEqual(s.kind == StageKind.Boss, p.boss, at + " " + s.stage + " 보스 표시 = 스테이지 표 type");
Assert.AreEqual(s.stage % 5 == 0, p.boss, at + " " + s.stage + " 보스 = 5의 배수");
Assert.AreEqual(s.place, p.place, at + " " + s.stage + " 지명");
var pos = new Vector2(p.at[0], p.at[1]);
Assert.IsTrue(pos.x >= 0f && pos.x <= d.mapSize[0] && pos.y >= 0f && pos.y <= d.mapSize[1], at + " " + s.stage + " 맵 안 " + pos);
Assert.LessOrEqual(DistanceToRoad(wc, pos), onRoad, at + " " + s.stage + " 노드가 길 위 " + pos);
if (prev.HasValue)
Assert.Greater(Vector2.Distance(prev.Value, pos), nodeKit.refSize[0], at + " " + s.stage + " 앞 노드와 깃발 겹침");
prev = pos;
}
}
}
[UnityTest] [UnityTest]
public IEnumerator WorldMap_ChaptersFromStageTable_AnchorSlotsBossNodesUnlock() public IEnumerator WorldMap_ChaptersFromStageTable_AnchorSlotsBossNodesUnlock()
{ {
@ -1074,9 +1125,9 @@ namespace TKS.Tests
var stages = cfg.StagesOfChapter(def.no); var stages = cfg.StagesOfChapter(def.no);
Assert.AreEqual(def.map, wm.MapOf(ch).map, "제" + def.no + "장 맵 = chapters.csv map"); Assert.AreEqual(def.map, wm.MapOf(ch).map, "제" + def.no + "장 맵 = chapters.csv map");
Assert.IsNotNull(Resources.Load<Sprite>("ChapterMap/Maps/" + def.map), def.map + " 맵 그림"); Assert.IsNotNull(Resources.Load<Sprite>("ChapterMap/Maps/" + def.map), def.map + " 맵 그림");
bool fromAnchors; NodeSource source;
var slots = wm.NodeSlots(ch, out fromAnchors); var slots = wm.NodeSlots(ch, out source);
Assert.IsTrue(fromAnchors, "제" + def.no + "장 노드 자리 = anchors slots_" + stages.Count); Assert.AreEqual(NodeSource.Stages, source, "제" + def.no + "장 노드 자리 = 기획 확정판 맵 stages[].at");
Assert.AreEqual(stages.Count, slots.Length); Assert.AreEqual(stages.Count, slots.Length);
foreach (var p in slots) foreach (var p in slots)
Assert.IsTrue(p.x >= 0f && p.x <= wm.Data.mapSize[0] && p.y >= 0f && p.y <= wm.Data.mapSize[1], def.map + " 노드가 맵 안"); Assert.IsTrue(p.x >= 0f && p.x <= wm.Data.mapSize[0] && p.y >= 0f && p.y <= wm.Data.mapSize[1], def.map + " 노드가 맵 안");

View File

@ -16,11 +16,19 @@ PD 2026-11-05: 「매번 수신하지 않아도, 요청한 작업이 완전히
"arts": ["dz_infantry", "dz_spear", …], "arts": ["dz_infantry", "dz_spear", …],
"clips": ["front_idle", "front_ready", "front_action"], # back_* 가 있으면 함께 "clips": ["front_idle", "front_ready", "front_action"], # back_* 가 있으면 함께
"md5_list": "C:/ArtRnD/samguk_game/poses/md5.txt" # 선택 · <root> 기준 상대 경로 · md5sum 형식 "md5_list": "C:/ArtRnD/samguk_game/poses/md5.txt" # 선택 · <root> 기준 상대 경로 · md5sum 형식
# kind == "worldmap" (챕터 월드맵 기획 확정판 · 챕터맵 보강 세션 · 2026-11-05)
"root": "C:/ArtRnD/samguk_chapmap/plan", # 선택(없으면 이 기본값) · maps[].map·anchors 의 기준
"maps": [{"chapter": 3, "id": "ch03", "ko": "군웅할거", "nodes": 15,
"map": "maps/ch03.png", "map_md5": "<md5>", "anchors": "anchors/ch03.json", "anchors_md5": "<md5>"}]
} }
worldmap = 맵 PNG → Assets/Resources/ChapterMap/Maps/<id>.png · 좌표 json → Data/<id>.json(게임 노드 = stages[].at) ·
Data/world.json 의 같은 id 항목(길 roadX·roadY · 장소)을 좌표 json 으로 교체. 게임 챕터 → 맵 연결 = Assets/Data/Stages/chapters.csv map 열.
같은 맵 id 를 더 늦게 다시 납품한 json(_r2 등)이 미반영으로 있으면 앞의 것은 반영하지 않고 .superseded.json 으로 바꾼다.
실행: <ComfyUI venv python> -I -B -X utf8 Tools/apply_delivery.py list 실행: <ComfyUI venv python> -I -B -X utf8 Tools/apply_delivery.py list
<ComfyUI venv python> -I -B -X utf8 Tools/apply_delivery.py apply <json 경로> (반영 뒤 .applied.json 으로 이름 바꿈) <ComfyUI venv python> -I -B -X utf8 Tools/apply_delivery.py apply <json 경로> (반영 뒤 .applied.json 으로 이름 바꿈)
<ComfyUI venv python> -I -B -X utf8 Tools/apply_delivery.py apply --all (미반영 전부) <ComfyUI venv python> -I -B -X utf8 Tools/apply_delivery.py apply --all (미반영 전부)
<ComfyUI venv python> -I -B -X utf8 Tools/apply_delivery.py apply --all --kind worldmap (그 kind 만)
반영 뒤 Unity: 메뉴 재임포트 → 비동기 테스트 → 커밋 (끝에 출력). 반영 뒤 Unity: 메뉴 재임포트 → 비동기 테스트 → 커밋 (끝에 출력).
""" """
import argparse import argparse
@ -36,6 +44,8 @@ INBOX = r"C:\ArtRnD\samguk_game\deliveries"
EXPORTER = r"C:\Ai\NerdNavisAi\.claude\skills\skeletal-anim-pipeline\files\scripts\rig_frames_export.py" EXPORTER = r"C:\Ai\NerdNavisAi\.claude\skills\skeletal-anim-pipeline\files\scripts\rig_frames_export.py"
UNITS = os.path.join(PROJECT, "Assets", "Resources", "Units") UNITS = os.path.join(PROJECT, "Assets", "Resources", "Units")
BOARD = os.path.join(PROJECT, "Assets", "Resources", "Board", "battlefield_01.png") BOARD = os.path.join(PROJECT, "Assets", "Resources", "Board", "battlefield_01.png")
CHAPMAP = os.path.join(PROJECT, "Assets", "Resources", "ChapterMap")
WORLDMAP_ROOT = r"C:\ArtRnD\samguk_chapmap\plan"
def md5_of(path): def md5_of(path):
@ -48,7 +58,23 @@ def md5_of(path):
def pending(): def pending():
os.makedirs(INBOX, exist_ok=True) os.makedirs(INBOX, exist_ok=True)
return sorted(f for f in os.listdir(INBOX) if f.endswith(".json") and not f.endswith(".applied.json")) return sorted(f for f in os.listdir(INBOX)
if f.endswith(".json") and not f.endswith(".applied.json") and not f.endswith(".superseded.json"))
def superseded(files):
"""worldmap 납품 중 같은 맵 id 를 모두 더 늦은 미반영 납품이 덮는 것(파일명 = 시각순)."""
maps = {}
for f in files:
d = json.load(open(os.path.join(INBOX, f), encoding="utf-8"))
if d.get("kind") == "worldmap":
maps[f] = {m["id"] for m in d.get("maps", [])}
out = set()
for f, ids in maps.items():
later = set().union(*[v for g, v in maps.items() if g > f] or [set()])
if ids and ids <= later:
out.add(f)
return out
def check_md5_list(root, md5_list): def check_md5_list(root, md5_list):
@ -100,23 +126,66 @@ def apply_poses(d):
print(r.stdout.strip().splitlines()[-1]) print(r.stdout.strip().splitlines()[-1])
def apply_worldmap(d):
root = d.get("root") or WORLDMAP_ROOT
world_path = os.path.join(CHAPMAP, "Data", "world.json")
world = json.load(open(world_path, encoding="utf-8"))
spec = os.path.join(root, "spec", "chapters.json")
lm_scale = json.load(open(spec, encoding="utf-8")).get("lm_scale", 1.0) if os.path.exists(spec) else 1.0
for m in d["maps"]:
mid = m["id"]
pairs = [(m["map"], m.get("map_md5"), os.path.join(CHAPMAP, "Maps", mid + ".png")),
(m["anchors"], m.get("anchors_md5"), os.path.join(CHAPMAP, "Data", mid + ".json"))]
for rel, digest, dst in pairs:
src = os.path.join(root, rel.replace("/", os.sep))
if digest and md5_of(src) != digest.lower():
sys.exit("worldmap md5 불일치: " + src)
for rel, digest, dst in pairs:
shutil.copyfile(os.path.join(root, rel.replace("/", os.sep)), dst)
if digest and md5_of(dst) != digest.lower():
sys.exit("복사 뒤 md5 불일치: " + dst)
an = json.load(open(pairs[1][2], encoding="utf-8"))
# 장소 이름판 자리 = 장소 아래(chap_install.py 와 같은 식) · showLabels 가 꺼져 있으면 쓰지 않음
lms = [{"ko": L["ko"], "x": L["at"][0], "y": round(L["at"][1] + 120 * L.get("s", 1.0) * lm_scale, 1), "role": L.get("role", "")}
for L in an.get("landmarks", []) if L.get("ko")]
entry = {"id": mid, "no": m.get("chapter", an.get("no", 0)), "ko": m.get("ko", an.get("ko", "")), "years": "", "map": mid,
"roadX": [p[0] for p in an["road"]], "roadY": [p[1] for p in an["road"]], "landmarks": lms}
chapters = [c for c in world["chapters"] if c["id"] != mid] + [entry]
world["chapters"] = sorted(chapters, key=lambda c: (c["no"], c["id"]))
print("worldmap", mid, "->", pairs[0][2], "· 노드", len(an.get("stages", [])), "· md5 OK")
world["_doc"] = ("게임이 읽는 챕터 맵 데이터 · 맵별 길·장소 = Data/<id>.json(좌표 json) 에서 옮김 · 기획 확정판 맵은 "
"Tools/apply_delivery.py worldmap 이 갱신(원화 세션 가안 chap_install.py 를 다시 돌리면 가안으로 덮이니 쓰지 않는다) · "
"좌표 = 맵 px(왼쪽 위 원점 · y 아래로) · nodes·stageLink 는 게임이 읽지 않음(노드 = 스테이지 표 + Data/<id>.json stages[].at)")
open(world_path, "w", encoding="utf-8").write(json.dumps(world, ensure_ascii=False, indent=1))
def cmd_list(a): def cmd_list(a):
files = pending() files = pending()
old = superseded(files)
print("미반영 납품:", len(files)) print("미반영 납품:", len(files))
for f in files: for f in files:
d = json.load(open(os.path.join(INBOX, f), encoding="utf-8")) d = json.load(open(os.path.join(INBOX, f), encoding="utf-8"))
print(" ", f, "|", d.get("kind"), d.get("batch"), "|", d.get("qa"), "|", d.get("session", "")) print(" ", f, "|", d.get("kind"), d.get("batch"), "|", d.get("qa"), "|", d.get("session", ""), "| (뒤 납품으로 대체됨)" if f in old else "")
def cmd_apply(a): def cmd_apply(a):
targets = pending() if a.all else [os.path.basename(a.json)] files = pending()
targets = files if a.all else [os.path.basename(a.json)]
if a.kind:
targets = [f for f in targets if json.load(open(os.path.join(INBOX, f), encoding="utf-8")).get("kind") == a.kind]
if not targets: if not targets:
print("반영할 납품 없음") print("반영할 납품 없음")
return return
old = superseded(files)
for f in targets: for f in targets:
p = os.path.join(INBOX, f) p = os.path.join(INBOX, f)
d = json.load(open(p, encoding="utf-8")) d = json.load(open(p, encoding="utf-8"))
if d.get("qa", "PASS") != "PASS": if f in old:
print("대체됨(같은 맵의 뒤 납품이 있음) → .superseded.json:", f)
os.replace(p, p[:-5] + ".superseded.json")
continue
# QA 문구 뒤에 설명이 붙은 경우(「PASS(…)」) 도 통과로 본다
if not str(d.get("qa", "PASS")).startswith("PASS"):
print("건너뜀(QA 미통과):", f) print("건너뜀(QA 미통과):", f)
continue continue
print("==", f, d.get("kind"), d.get("batch")) print("==", f, d.get("kind"), d.get("batch"))
@ -124,11 +193,13 @@ def cmd_apply(a):
apply_bg(d) apply_bg(d)
elif d["kind"] == "poses": elif d["kind"] == "poses":
apply_poses(d) apply_poses(d)
elif d["kind"] == "worldmap":
apply_worldmap(d)
else: else:
sys.exit("모르는 kind: " + str(d.get("kind"))) sys.exit("모르는 kind: " + str(d.get("kind")))
os.replace(p, p[:-5] + ".applied.json") os.replace(p, p[:-5] + ".applied.json")
print("\n다음(Unity · PD 에디터 Play 중이면 먼저 editor.playmode stop):") print("\n다음(Unity · PD 에디터 Play 중이면 먼저 editor.playmode stop):")
print(" unity command --project-path . editor invoke_menu --path 'TKS/Reimport Unit Art (유닛 아트 재임포트)' --result-only") print(" unity command --project-path . editor invoke_menu --path 'TKS/Reimport Unit Art (유닛 아트 재임포트)' --result-only (Units·Board·ChapterMap)")
print(" unity command --project-path . test run --mode playmode --async-tests --timeout 600 --result-only ; unity command --project-path . test status --result-only") print(" unity command --project-path . test run --mode playmode --async-tests --timeout 600 --result-only ; unity command --project-path . test status --result-only")
print(" git add -A && git commit (끝줄 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>)") print(" git add -A && git commit (끝줄 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>)")
@ -141,6 +212,7 @@ if __name__ == "__main__":
ap = sp.add_parser("apply") ap = sp.add_parser("apply")
ap.add_argument("json", nargs="?", default="") ap.add_argument("json", nargs="?", default="")
ap.add_argument("--all", action="store_true") ap.add_argument("--all", action="store_true")
ap.add_argument("--kind", default="", help="이 kind 만(bg · poses · worldmap)")
ap.set_defaults(fn=cmd_apply) ap.set_defaults(fn=cmd_apply)
a = p.parse_args() a = p.parse_args()
if a.cmd == "apply" and not a.all and not a.json: if a.cmd == "apply" and not a.all and not a.json: