챕터 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>
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 7.4 MiB After Width: | Height: | Size: 7.2 MiB |
|
Before Width: | Height: | Size: 8.0 MiB After Width: | Height: | Size: 7.6 MiB |
|
Before Width: | Height: | Size: 7.5 MiB After Width: | Height: | Size: 7.7 MiB |
|
Before Width: | Height: | Size: 8.1 MiB After Width: | Height: | Size: 6.9 MiB |
|
Before Width: | Height: | Size: 7.3 MiB After Width: | Height: | Size: 7.1 MiB |
|
Before Width: | Height: | Size: 7.9 MiB After Width: | Height: | Size: 7.7 MiB |
|
Before Width: | Height: | Size: 7.4 MiB After Width: | Height: | Size: 7.9 MiB |
|
Before Width: | Height: | Size: 7.6 MiB After Width: | Height: | Size: 6.9 MiB |
|
Before Width: | Height: | Size: 7.6 MiB After Width: | Height: | Size: 7.0 MiB |
|
Before Width: | Height: | Size: 7.2 MiB After Width: | Height: | Size: 7.0 MiB |
|
|
@ -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/<map>.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/<map>.json(원화 세션 anchors) 의 slots_n(맵 px · 첫 칸 = 출발). 없으면 null.</summary>
|
/// <summary>Data/<map>.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/<맵>.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/<맵>.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/<맵>.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]);
|
||||||
|
|
|
||||||
|
|
@ -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 + " 노드가 맵 안");
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||