스테이지 데이터 145(챕터 1~10 · 기획서 #1086 §4) = CSV SOT + 임포터 + 월드맵 연결 — Assets/Data/Stages/{stages,chapters,generals,factions}.csv(utf-8-sig) · 메뉴 TKS/Import Stages(StageImporter · 형식 오류면 자산 미변경) → BalanceConfig.chapters·stages · 적 좌표 = formation 규칙(전위·중군·후군·장수 줄 · StageTable.Enemies) · 보스 isBoss = type boss 스테이지만 · 엘리트 = 배율 보정 · 아트 없는 무장·세력 = 도형 · 월드맵 = 챕터·노드 수·보스 노드를 스테이지 표에서(10·10·15·15·15·20·10·15·15·20) · 노드 자리 = anchors slots_N · 3·7장 = 맵 1 임시 · BalanceDefaults 구 4 스테이지 시드 삭제 · 테스트 31/31(신규 4: 145행·보스=5의 배수·병종/아트 id · 자산=CSV·배치가 전장 안·겹침 없음 · 도형 대체 · 월드맵 노드/해금) · console error 0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d2dfa76aac
commit
2d0fc4fab6
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 58f16644f478cba44a713a05b718b341
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5ba406b8d996345478d7ebc6ce5239d9
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
chapter,title,subtitle,years,map,ai_accuracy,ai_aggro,note
|
||||||
|
1,도원결의,황건적의 난,184,ch01,0.5,11,
|
||||||
|
2,반동탁연합,사수관·호로관,189–190,ch02,0.6,11,
|
||||||
|
3,군웅할거,반하·북해·서주,191–194,ch01,0.6,11,맵 신규 전(월드맵 보강) 임시 = 맵 1 재사용
|
||||||
|
4,여포와 서주,우이·소패·하비,195–198,ch03,0.6,11,맵 3(서주) · 우이·회음 장소 추가 예정
|
||||||
|
5,의대조,원술 토벌과 서주,199–200,ch03,0.6,11,맵 3 재사용(권장안) · 허도·망탕산·토산 추가 예정
|
||||||
|
6,관우 천리행,백마·연진·오관,200,ch04,0.6,11,맵 4 · 백마·연진 추가 예정
|
||||||
|
7,고성 재회,여남과 양산,200–201,ch01,0.6,11,맵 신규 전(월드맵 보강) 임시 = 맵 1 재사용
|
||||||
|
8,형주의 객장,서서와 와룡,201–208,ch05,0.6,11,맵 5 · 강하·양양·단계·번성 추가 예정
|
||||||
|
9,장판파,백성과 함께,208,ch06,0.6,11,맵 6 · 신야·백하 추가 예정
|
||||||
|
10,적벽대전,,208,ch07,0.65,12,맵 7(적벽)
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ef30932e4ea39ec4ebf3ab952d296731
|
||||||
|
TextScriptImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
code,name,art,note
|
||||||
|
hgj,황건적,hgj,1장 · 3장 북해 잔당
|
||||||
|
dz,동탁군,dz,2장
|
||||||
|
lb,여포군,dz,4장 · D3 = 동탁군 아트 재사용(베리에이션 lb_ 납품 시 art 교체)
|
||||||
|
ys,원소군,ys,cc 기반 베리에이션(미납품 → 도형)
|
||||||
|
ysu,원술군,ysu,cc 기반 베리에이션(미납품 → 도형)
|
||||||
|
jz,형주군,jz,cc 기반 베리에이션(미납품 → 도형)
|
||||||
|
jzr,형주 반란군,jz,8장 2~5 · 장무·진손 id 접두 jz
|
||||||
|
sj,산적,hgj,7장 2·8 = 황건 아트 재사용(기획서)
|
||||||
|
cc,조조군,cc,
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 471bde1166de62349a709883947c1093
|
||||||
|
TextScriptImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
id,name,hanja,hp_mult,atk_mult,note
|
||||||
|
cc_boss_bianxi,변희,卞喜,,,
|
||||||
|
cc_boss_caiyang,채양,蔡陽,,,
|
||||||
|
cc_boss_caocao,조조,曹操,1.5,1.3,현행 보스 값 승계
|
||||||
|
cc_boss_caohong,조홍,曹洪,,,
|
||||||
|
cc_boss_caoren,조인,曹仁,,,
|
||||||
|
cc_boss_chezhou,차주,車冑,,,
|
||||||
|
cc_boss_chunyudao,순우도,淳于導,,,
|
||||||
|
cc_boss_gaolan,고람,高覽,,,
|
||||||
|
cc_boss_hanfu,한복,韓福,,,
|
||||||
|
cc_boss_kongxiu,공수,孔秀,,,
|
||||||
|
cc_boss_lidian,이전,李典,,,
|
||||||
|
cc_boss_liudai,유대,劉岱,,,
|
||||||
|
cc_boss_lvkuang,여광,呂曠,,,
|
||||||
|
cc_boss_lvxiang,여상,呂翔,,,
|
||||||
|
cc_boss_maojie,모개,毛玠,,,
|
||||||
|
cc_boss_mengtan,맹탄,孟坦,,,
|
||||||
|
cc_boss_qinqi,진기,秦琪,,,
|
||||||
|
cc_boss_wangzhi,왕식,王植,,,
|
||||||
|
cc_boss_wangzhong,왕충,王忠,,,
|
||||||
|
cc_boss_xiahoudun,하후돈,夏侯惇,,,
|
||||||
|
cc_boss_xiahouen,하후은,夏侯恩,,,
|
||||||
|
cc_boss_xiahoujie,하후걸,夏侯傑,,,
|
||||||
|
cc_boss_xuchu,허저,許褚,,,
|
||||||
|
cc_boss_xuhuang,서황,徐晃,,,
|
||||||
|
cc_boss_yanming,안명,晏明,,,
|
||||||
|
cc_boss_yujin,우금,于禁,,,
|
||||||
|
cc_boss_zhanghe,장합,張郃,,,
|
||||||
|
dz_boss_caoxing,조성,曹性,,,
|
||||||
|
dz_boss_chengong,진궁,陳宮,,,
|
||||||
|
dz_boss_gaoshun,고순,高順,,,
|
||||||
|
dz_boss_huaxiong,화웅,華雄,,,
|
||||||
|
dz_boss_huzhen,호진,胡軫,,,
|
||||||
|
dz_boss_lvbu,여포,呂布,1.6,1.5,현행 보스 값 승계
|
||||||
|
dz_boss_zhangliao,장료,張遼,,,
|
||||||
|
hgj_boss_chengyuanzhi,정원지,程遠志,,,
|
||||||
|
hgj_boss_dengmao,등무,鄧茂,,,
|
||||||
|
hgj_boss_guanhai,관해,管亥,,,
|
||||||
|
hgj_boss_peiyuanshao,배원소,裴元紹,,,
|
||||||
|
hgj_boss_zhangbao,장보,張寶,,,
|
||||||
|
hgj_boss_zhangjiao,장각,張角,1.3,1.2,현행 보스 값 승계
|
||||||
|
hgj_boss_zhangliang,장량,張梁,,,
|
||||||
|
jz_boss_caimao,채모,蔡瑁,,,
|
||||||
|
jz_boss_caixun,채훈,蔡壎,,,
|
||||||
|
jz_boss_caizhong,채중,蔡中,,,
|
||||||
|
jz_boss_chensun,진손,陳孫,,,
|
||||||
|
jz_boss_wenpin,문빙,文聘,,,
|
||||||
|
jz_boss_zhangwu,장무,張武,,,
|
||||||
|
jz_boss_zhangyun,장윤,張允,,,
|
||||||
|
ys_boss_quyi,국의,麴義,,,
|
||||||
|
ys_boss_wenchou,문추,文醜,,,
|
||||||
|
ys_boss_yanliang,안량,顏良,,,
|
||||||
|
ysu_boss_jiling,기령,紀靈,,,
|
||||||
|
ysu_boss_xunzheng,순정,荀正,,,
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 65e6abbca07bd2748b6a004d841fb426
|
||||||
|
TextScriptImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
chapter,stage,title,type,place,faction,infantry,spear,cavalry,archer,generals,bg_theme,note
|
||||||
|
1,1,탁현 외곽 — 황건 척후,normal,탁현,hgj,3,0,0,0,,마을 어귀,튜토리얼(조준·이동)
|
||||||
|
1,2,누상촌 약탈대,normal,누상촌,hgj,3,1,0,0,,마을,튜토리얼(기병 관통)
|
||||||
|
1,3,대흥산 길목,normal,대흥산,hgj,3,2,0,0,,산길,튜토리얼(창병 견제)
|
||||||
|
1,4,대흥산 — 등무,elite,대흥산,hgj,3,2,0,1,hgj_boss_dengmao:spear:false,산기슭,
|
||||||
|
1,5,대흥산 전투 — 정원지,boss,대흥산,hgj,4,2,0,1,hgj_boss_chengyuanzhi:infantry:true,산기슭 평지,
|
||||||
|
1,6,청주 포위망,normal,청주,hgj,4,2,0,2,,성 밖 들판,
|
||||||
|
1,7,청주 산골 매복,normal,청주,hgj,3,2,1,2,,협곡,일반(적 기병 첫 등장)
|
||||||
|
1,8,영천 — 불길 속 황건,elite,영천,hgj,3,2,1,2,hgj_boss_zhangbao:archer:false,불탄 들판,
|
||||||
|
1,9,광종 — 황건 본진 전위,elite,광종,hgj,4,2,1,2,hgj_boss_zhangliang:infantry:false,진영 앞 평원,
|
||||||
|
1,10,광종 결전 — 장각,boss,광종,hgj,4,2,1,2,hgj_boss_zhangjiao:infantry:true,황건 본진,
|
||||||
|
2,1,산조 진영 — 동탁군 척후,normal,산조,dz,3,0,2,1,,군영,
|
||||||
|
2,2,사수관 가는 길,normal,사수관 동쪽,dz,3,2,1,1,,산길,
|
||||||
|
2,3,사수관 앞 — 호진,elite,사수관,dz,3,2,1,2,dz_boss_huzhen:cavalry:false,관문 앞 들판,
|
||||||
|
2,4,손견 진영 야습,normal,사수관,dz,3,2,2,1,,밤 군영,
|
||||||
|
2,5,사수관 — 화웅,boss,사수관,dz,4,2,1,2,dz_boss_huaxiong:infantry:true,관문 앞,
|
||||||
|
2,6,사수관 함락 — 잔병,normal,사수관,dz,3,3,1,2,,관문 안쪽,
|
||||||
|
2,7,호로관 가는 길 — 서량 기병,normal,호로관 동쪽,dz,2,2,3,2,,들판,
|
||||||
|
2,8,호로관 앞 — 방열이 쓰러진 자리,normal,호로관,dz,2,2,4,1,,관문 앞,
|
||||||
|
2,9,호로관 측면 — 적토마 돌격대,normal,호로관,dz,2,3,4,1,,관문 측면,
|
||||||
|
2,10,호로관 — 여포,boss,호로관,dz,3,2,3,2,dz_boss_lvbu:cavalry:true,관문 앞,
|
||||||
|
3,1,평원 출진 — 원소군 척후,normal,평원,ys,3,2,1,2,,들판,
|
||||||
|
3,2,반하 들판,normal,반하,ys,3,2,2,1,,강변 들판,
|
||||||
|
3,3,반하 다리 — 문추의 추격,elite,반하,ys,2,2,3,1,ys_boss_wenchou:cavalry:false,다리·강,
|
||||||
|
3,4,반하 — 엄강의 패전,normal,반하,ys,2,2,0,5,,강변,
|
||||||
|
3,5,반하 — 국의,boss,반하,ys,2,2,0,4,ys_boss_quyi:archer:true,강변 진영,
|
||||||
|
3,6,북해 가는 길 — 황건 잔당,normal,북해 서쪽,hgj,4,2,0,2,,들판,
|
||||||
|
3,7,북해 포위망 바깥,normal,북해,hgj,4,2,1,2,,성 밖,
|
||||||
|
3,8,태사자의 돌파로,normal,북해,hgj,4,3,1,2,,성 밖,
|
||||||
|
3,9,북해 성문 — 관해 친위,normal,북해,hgj,5,2,1,2,,성문,
|
||||||
|
3,10,북해 — 관해,boss,북해,hgj,4,2,1,2,hgj_boss_guanhai:infantry:true,성문 앞,
|
||||||
|
3,11,서주 가는 길 — 불타는 마을,normal,서주 북쪽,cc,3,2,2,2,,불탄 마을,
|
||||||
|
3,12,서주 외곽 — 조조 포위진,normal,서주,cc,3,3,2,2,,들판,
|
||||||
|
3,13,서주 남쪽 — 피난민 행렬,normal,서주,cc,3,3,3,2,,들판,
|
||||||
|
3,14,조조 진영 외곽,normal,서주,cc,3,3,2,3,,군영,
|
||||||
|
3,15,서주 — 우금,boss,서주,cc,3,3,2,2,cc_boss_yujin:infantry:true,군영 앞,
|
||||||
|
4,1,우이 들판 — 원술군 선봉,normal,우이,ysu,3,3,1,2,,들판,
|
||||||
|
4,2,회음 강변 — 군량 수송대,normal,회음,ysu,4,2,0,2,,강변,
|
||||||
|
4,3,우이 — 순정,elite,우이,ysu,3,3,1,2,ysu_boss_xunzheng:spear:false,들판,
|
||||||
|
4,4,우이 진영 야습,normal,우이,ysu,3,3,2,2,,밤 군영,
|
||||||
|
4,5,우이 — 기령,boss,우이,ysu,3,3,2,2,ysu_boss_jiling:spear:true,강변 들판,
|
||||||
|
4,6,서주성의 밤 — 조표의 배신,normal,서주성,lb,2,2,4,2,,밤 성안,
|
||||||
|
4,7,소패 — 빼앗긴 말,normal,소패,lb,3,2,3,2,,성 밖,
|
||||||
|
4,8,소패 탈출 — 여포군 추격,normal,소패 서쪽,lb,2,2,4,2,,들판,
|
||||||
|
4,9,소패 구원 — 조성,elite,소패,lb,3,2,2,3,dz_boss_caoxing:archer:false,성 밖,
|
||||||
|
4,10,소패 — 고순,boss,소패,lb,5,2,1,2,dz_boss_gaoshun:infantry:true,성문 앞,
|
||||||
|
4,11,서주성 — 진등의 계책,normal,서주성,lb,3,3,2,2,,성 밖,
|
||||||
|
4,12,하비 외곽 — 진궁,elite,하비,lb,3,2,2,3,dz_boss_chengong:archer:false,들판,
|
||||||
|
4,13,하비 수공,normal,하비,lb,3,3,2,3,,물에 잠긴 성,
|
||||||
|
4,14,하비 성문 — 장료,elite,하비,lb,3,2,3,2,dz_boss_zhangliao:cavalry:false,성문,
|
||||||
|
4,15,하비 — 여포,boss,하비(백문루),lb,3,2,3,3,dz_boss_lvbu:cavalry:true,성안 누각 앞,
|
||||||
|
5,1,허도 근교 — 사냥터의 도적,normal,허도,sj,3,2,2,2,,숲,세력 추정(기획서 미기재 · 도적 = 7장 산적 선례)
|
||||||
|
5,2,허도 출진 — 원술 차단,normal,허도 동쪽,ysu,3,2,2,2,,들판,세력 추정(기획서 미기재 · 원술 차단 출진)
|
||||||
|
5,3,서주 남쪽 — 원술군 선봉,normal,서주 남쪽,ysu,3,3,2,2,,들판,
|
||||||
|
5,4,원술 친위대,normal,서주 남쪽,ysu,3,3,2,3,,들판,
|
||||||
|
5,5,서주 남쪽 — 기령,boss,서주 남쪽,ysu,3,3,2,2,ysu_boss_jiling:spear:true,들판,
|
||||||
|
5,6,원술의 몰락 — 패잔병,normal,수춘 북쪽,ysu,4,3,0,2,,황무지,
|
||||||
|
5,7,서주성 앞 — 차주의 복병,normal,서주성,cc,3,3,2,2,,성문 밖 밤,
|
||||||
|
5,8,서주성 — 차주,elite,서주성,cc,3,3,2,2,cc_boss_chezhou:infantry:false,성문,
|
||||||
|
5,9,서주 외곽 — 조조 토벌군,normal,서주,cc,3,3,2,3,,들판,
|
||||||
|
5,10,서주 — 유대·왕충,boss,서주,cc,3,3,2,2,cc_boss_liudai:spear:true;cc_boss_wangzhong:infantry:true,들판 진영,보스(2인)
|
||||||
|
5,11,소패 야습,normal,소패,cc,3,3,3,2,,밤 군영,
|
||||||
|
5,12,소패 패주로,normal,소패 북쪽,cc,2,3,4,2,,들판,
|
||||||
|
5,13,하비 — 고립된 관우,normal,하비,cc,3,3,3,3,,성 밖,
|
||||||
|
5,14,하비 성 밖 — 하후돈의 유인,elite,하비,cc,3,3,3,2,cc_boss_xiahoudun:cavalry:false,성 밖 들판,
|
||||||
|
5,15,토산 — 서황·허저,boss,토산,cc,3,3,2,3,cc_boss_xuhuang:infantry:true;cc_boss_xuchu:infantry:true,산,보스(2인)
|
||||||
|
6,1,백마 포위망,normal,백마,ys,3,3,2,2,,성 밖 들판,
|
||||||
|
6,2,백마 — 쓰러진 송헌·위속,normal,백마,ys,3,3,2,2,,들판,
|
||||||
|
6,3,백마 들판 — 안량 본진 앞,normal,백마,ys,3,3,3,2,,들판 진영,
|
||||||
|
6,4,백마 — 측면 기병,normal,백마,ys,2,3,4,2,,들판,
|
||||||
|
6,5,백마 — 안량,boss,백마,ys,3,3,3,2,ys_boss_yanliang:spear:true,진영 깃발 아래,
|
||||||
|
6,6,연진 — 원소군 도하,normal,연진,ys,3,3,3,2,,강변 나루,
|
||||||
|
6,7,연진 — 버려진 군량과 말,normal,연진,ys,4,3,3,1,,들판,
|
||||||
|
6,8,연진 — 장료·서황의 패퇴,normal,연진,ys,2,3,4,2,,들판,
|
||||||
|
6,9,연진 강변 — 문추 친위,normal,연진,ys,3,3,4,2,,강변,
|
||||||
|
6,10,연진 — 문추,boss,연진,ys,3,3,4,2,ys_boss_wenchou:cavalry:true,강변 들판,
|
||||||
|
6,11,동령관 — 공수,elite,동령관,cc,4,3,2,2,cc_boss_kongxiu:infantry:false,관문,
|
||||||
|
6,12,낙양 외곽 — 맹탄,elite,낙양,cc,3,3,3,2,cc_boss_mengtan:cavalry:false,성 밖,
|
||||||
|
6,13,낙양 성문 — 한복,elite,낙양,cc,3,3,2,4,cc_boss_hanfu:archer:false,성문,
|
||||||
|
6,14,사수관 — 진국사 매복,normal,사수관(진국사),cc,5,3,1,2,,절간,
|
||||||
|
6,15,사수관 — 변희,boss,사수관,cc,4,3,2,2,cc_boss_bianxi:infantry:true,절 마당,
|
||||||
|
6,16,형양 — 왕식의 연회,normal,형양,cc,4,3,2,3,,밤 성안,
|
||||||
|
6,17,형양 — 불붙은 객관,normal,형양,cc,3,3,2,4,,불타는 성안,
|
||||||
|
6,18,형양 성문 — 왕식,elite,형양,cc,3,3,3,3,cc_boss_wangzhi:archer:false,성문,
|
||||||
|
6,19,활주 나루 — 진기,elite,활주,cc,3,4,2,3,cc_boss_qinqi:spear:false,나루,
|
||||||
|
6,20,황하 나루 — 하후돈,boss,활주,cc,3,3,4,2,cc_boss_xiahoudun:cavalry:true,강변 나루,
|
||||||
|
7,1,황하 남쪽 — 순찰대,normal,고성 북쪽,cc,3,3,3,2,,들판,
|
||||||
|
7,2,와우산 — 주창의 산채,normal,와우산,sj,5,3,0,2,,산채,
|
||||||
|
7,3,고성 — 의심하는 장비,normal,고성,cc,3,3,3,2,,성 밖,
|
||||||
|
7,4,고성 외곽 — 채양 본대,normal,고성,cc,3,3,4,2,,들판,
|
||||||
|
7,5,고성 — 채양,boss,고성,cc,3,3,3,3,cc_boss_caiyang:cavalry:true,성 밖 들판,
|
||||||
|
7,6,하북 경계 — 유비의 탈출,normal,관정장 북쪽,ys,3,3,3,3,,들판,
|
||||||
|
7,7,관정장 — 관평,normal,관정장,ys,3,3,4,2,,마을,
|
||||||
|
7,8,와우산 — 배원소,elite,와우산,sj,4,3,2,2,hgj_boss_peiyuanshao:infantry:false,산채,
|
||||||
|
7,9,양산 — 조조의 추격,elite,양산,cc,3,3,3,3,cc_boss_xuchu:infantry:false;cc_boss_zhanghe:spear:false,산골,
|
||||||
|
7,10,양산 — 고람,boss,양산,cc,3,4,3,3,cc_boss_gaolan:spear:true,산길,
|
||||||
|
8,1,형주 가는 길 — 추격 잔당,normal,완 남쪽,cc,3,3,3,2,,들판,
|
||||||
|
8,2,강하 — 반란군 선봉,normal,강하,jzr,4,3,2,2,,강변,
|
||||||
|
8,3,강하 산채 — 반란군 궁병,normal,강하,jzr,3,2,2,4,,산채,
|
||||||
|
8,4,강하 — 진손,elite,강하,jzr,3,3,3,2,jz_boss_chensun:spear:false,들판,
|
||||||
|
8,5,강하 — 장무,boss,강하,jzr,3,3,3,3,jz_boss_zhangwu:cavalry:true,강변 들판,
|
||||||
|
8,6,양양 연회 — 채모의 복병,elite,양양,jz,4,3,2,3,jz_boss_caimao:archer:false,밤 성안,
|
||||||
|
8,7,단계 — 적로의 도약,normal,단계,jz,3,3,4,2,,개울가,
|
||||||
|
8,8,신야 — 여광,elite,신야,cc,3,3,3,2,cc_boss_lvkuang:cavalry:false,들판,
|
||||||
|
8,9,신야 — 여상,elite,신야,cc,3,3,3,2,cc_boss_lvxiang:cavalry:false,들판,
|
||||||
|
8,10,신야 — 조인 · 팔문금쇄진,boss,신야,cc,4,3,3,2,cc_boss_caoren:infantry:true,진형(바닥 문양),
|
||||||
|
8,11,번성 — 빈 성,normal,번성,cc,3,3,3,3,,성문,
|
||||||
|
8,12,번성 방위 — 조조군 반격,normal,번성,cc,3,4,3,3,,성벽,
|
||||||
|
8,13,융중 가는 길 — 눈보라 속 산적,normal,융중,cc,4,3,2,3,,눈 덮인 산길,
|
||||||
|
8,14,박망파 — 하후돈 선봉,normal,박망파,cc,3,3,4,2,,갈대숲,
|
||||||
|
8,15,박망파 — 하후돈,boss,박망파,cc,3,3,4,2,cc_boss_xiahoudun:cavalry:true;cc_boss_yujin:infantry:false;cc_boss_lidian:spear:false,불타는 갈대숲,우금·이전 = 엘리트 동행(기획서 8장 인물표)
|
||||||
|
9,1,신야 화공 — 조홍,elite,신야,cc,3,3,3,3,cc_boss_caohong:infantry:false,불타는 성안,
|
||||||
|
9,2,백하 — 관우의 수공,normal,백하,cc,4,3,3,2,,강변,
|
||||||
|
9,3,박릉 나루 — 장비 매복,normal,박릉 나루,cc,3,3,3,3,,나루,
|
||||||
|
9,4,번성 철수 — 백성 행렬,normal,번성,cc,3,3,4,2,,강 건너 들판,
|
||||||
|
9,5,양양 성문 — 문빙,boss,양양,jz,3,4,2,3,jz_boss_wenpin:spear:true,성문,
|
||||||
|
9,6,당양 — 호표기의 추격,normal,당양,cc,2,3,6,2,,들판,
|
||||||
|
9,7,장판 — 순우도,elite,장판,cc,3,3,4,2,cc_boss_chunyudao:spear:false,들판,
|
||||||
|
9,8,장판 — 하후은,elite,장판,cc,3,3,4,2,cc_boss_xiahouen:cavalry:false,들판,
|
||||||
|
9,9,장판 — 안명과 하북 4장,elite,장판,cc,2,3,4,2,cc_boss_yanming:spear:false,들판,미배치 = 하북 4장(병사 아트 + 이름표) · 병종 미정(PD 결정 대기)
|
||||||
|
9,10,장판파 — 장합,boss,장판,cc,3,4,4,2,cc_boss_zhanghe:spear:true,들판·언덕,
|
||||||
|
9,11,장판교 — 장비의 일갈,elite,장판교,cc,3,3,4,3,cc_boss_xiahoujie:cavalry:false,다리,
|
||||||
|
9,12,부서진 장판교,normal,장판교,cc,3,3,5,2,,강변,
|
||||||
|
9,13,한진 가는 길 — 조조 철기,normal,한진 서쪽,cc,2,3,5,2,,들판,
|
||||||
|
9,14,한진 — 관우·유기의 수군,normal,한진,cc,3,3,4,3,,나루,
|
||||||
|
9,15,한진 나루 — 조조,boss,한진,cc,3,3,4,3,cc_boss_caocao:infantry:true,나루 언덕,
|
||||||
|
10,1,하구 — 조조군 정찰선,normal,하구,cc,3,2,2,3,,강변 나루,
|
||||||
|
10,2,장강 — 형주 수군 경계선,normal,번구,jz,3,3,0,4,,배 위,
|
||||||
|
10,3,삼강구 — 채훈,elite,삼강구,jz,3,3,0,4,jz_boss_caixun:archer:false,배 위,
|
||||||
|
10,4,삼강구 — 형주 수군 본대,normal,삼강구,jz,4,3,0,4,,배 위,
|
||||||
|
10,5,삼강구 — 채모·장윤,boss,삼강구,jz,3,3,0,4,jz_boss_caimao:archer:true;jz_boss_zhangyun:infantry:true,기함 갑판,보스(2인)
|
||||||
|
10,6,초선차전 — 안개 속 화살비,normal,오림 앞 강,cc,2,2,0,8,,안개 낀 강,
|
||||||
|
10,7,장강 북안 — 감택의 밀서,normal,오림,cc,3,3,3,3,,강 북안,
|
||||||
|
10,8,연환선 — 사슬로 묶인 함대,normal,오림,cc,4,3,0,4,,연환선 갑판,
|
||||||
|
10,9,오림 수채 앞 — 순찰 선단,normal,오림,cc,3,3,2,4,,수채,
|
||||||
|
10,10,연환선단 — 우금·모개,boss,오림,cc,3,3,2,4,cc_boss_yujin:infantry:true;cc_boss_maojie:archer:true,연환선 갑판,보스(2인)
|
||||||
|
10,11,동남풍 — 화선 출격,normal,적벽,cc,3,3,0,5,,강 위,
|
||||||
|
10,12,적벽 — 불타는 연환선,normal,적벽,cc,4,3,2,3,,불타는 갑판,
|
||||||
|
10,13,수채 — 감녕과 채중,elite,오림,cc,4,3,2,3,jz_boss_caizhong:infantry:false,불타는 수채,
|
||||||
|
10,14,수채 본진 — 불길 속 친위대,normal,오림,cc,3,3,4,3,,불타는 수채,
|
||||||
|
10,15,적벽 — 장료,boss,오림,cc,3,3,4,3,dz_boss_zhangliao:cavalry:true,불타는 강변,
|
||||||
|
10,16,오림 — 조운의 매복,elite,오림 서쪽,cc,3,3,4,2,cc_boss_xuhuang:infantry:false;cc_boss_zhanghe:spear:false,숲길,
|
||||||
|
10,17,호로구 — 장비의 매복,elite,호로구,cc,3,3,4,2,cc_boss_xuchu:infantry:false,골짜기,
|
||||||
|
10,18,화용도 — 진흙길,normal,화용도,cc,4,3,3,2,,진흙 습지,
|
||||||
|
10,19,화용도 — 마지막 호위대,normal,화용도,cc,3,3,4,3,,진흙 습지,
|
||||||
|
10,20,화용도 — 조조,boss,화용도,cc,3,3,4,3,cc_boss_caocao:infantry:true,좁은 산길,
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 64046371a1ba18e4d8b2878a72967d2a
|
||||||
|
TextScriptImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,4 +1,3 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace TKS.EditorTools
|
namespace TKS.EditorTools
|
||||||
|
|
@ -7,6 +6,7 @@ namespace TKS.EditorTools
|
||||||
/// BalanceConfig 자산의 최초 1회 시드 값(프로토타입 초안). 이후 수치 조정은 자산(Inspector)에서 한다 — 코드가 SOT 가 아니다.
|
/// BalanceConfig 자산의 최초 1회 시드 값(프로토타입 초안). 이후 수치 조정은 자산(Inspector)에서 한다 — 코드가 SOT 가 아니다.
|
||||||
/// 전장 = 가로 20 × 세로 32 (x −10~10 · y −16~16) · 화면에는 세로 18 만 보여 스크롤한다. 아군은 아래(y −13) · 적은 전위/중군/후군으로 흩어 배치.
|
/// 전장 = 가로 20 × 세로 32 (x −10~10 · y −16~16) · 화면에는 세로 18 만 보여 스크롤한다. 아군은 아래(y −13) · 적은 전위/중군/후군으로 흩어 배치.
|
||||||
/// 아트 id = Resources/Units/<id>/ (아군 = 스켈레탈 세션 리그 프레임 liubei·guanyu·zhangfei · 적 = 일러 세션 납품 이름 <세력>_<병종> · 없으면 도형).
|
/// 아트 id = Resources/Units/<id>/ (아군 = 스켈레탈 세션 리그 프레임 liubei·guanyu·zhangfei · 적 = 일러 세션 납품 이름 <세력>_<병종> · 없으면 도형).
|
||||||
|
/// 챕터·스테이지는 시드하지 않는다 — Assets/Data/Stages/*.csv 가 SOT(StageImporter · 2026-11-05 145 스테이지). 여기는 배치 규칙·장수 배율 출발값만.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class BalanceDefaults
|
public static class BalanceDefaults
|
||||||
{
|
{
|
||||||
|
|
@ -70,52 +70,20 @@ namespace TKS.EditorTools
|
||||||
Hero("황충", UnitType.Archer, 1.0f, 1.25f, "huangzhong"), // 원화 세션 재디자인(2026-11-04 #1081)
|
Hero("황충", UnitType.Archer, 1.0f, 1.25f, "huangzhong"), // 원화 세션 재디자인(2026-11-04 #1081)
|
||||||
};
|
};
|
||||||
|
|
||||||
c.stages = new[]
|
// 적 배치 규칙 = 구 시드 4 스테이지(황건적의 난·호로관·적벽·오장원)의 전위 y −6~−4.5 · 중군 −2~5 · 후군 7~9 · 장수 10~13 · 가로 간격 2.5~3 을 규칙화
|
||||||
|
c.formation = new BalanceConfig.FormationRule
|
||||||
{
|
{
|
||||||
Stage("황건적의 난", "184년", 0.5f, 11f,
|
frontY = -5.5f, middleY = 0.5f, rearY = 7.5f, commandY = 11f,
|
||||||
// 전위
|
colGap = 2.6f, rowGap = 1.8f, maxPerRow = 5, rowStagger = 0.5f,
|
||||||
E("hgj", "황건적 보병", UnitType.Infantry, -3f, -6f),
|
frontShare = 0.4f, rearShare = 0.15f,
|
||||||
E("hgj", "황건적 보병", UnitType.Infantry, 3f, -6f),
|
|
||||||
E("hgj", "황건적 창병", UnitType.Spear, 0f, -4.5f),
|
|
||||||
// 중군
|
|
||||||
E("hgj", "황건적 궁병", UnitType.Archer, -4f, 1f),
|
|
||||||
E("hgj", "황건적 궁병", UnitType.Archer, 4f, 1f),
|
|
||||||
E("hgj", "황건적 보병", UnitType.Infantry, 0f, 2.5f),
|
|
||||||
// 후군
|
|
||||||
E("hgj", "황건적 창병", UnitType.Spear, -2.5f, 8f),
|
|
||||||
Boss("hgj", "zhangjiao", "장각", UnitType.Infantry, 0f, 10f, 1.3f, 1.2f)),
|
|
||||||
Stage("호로관 전투", "190년", 0.6f, 11f,
|
|
||||||
E("dz", "동탁군 창병", UnitType.Spear, -2.5f, -5f),
|
|
||||||
E("dz", "동탁군 창병", UnitType.Spear, 2.5f, -5f),
|
|
||||||
E("dz", "동탁군 기병", UnitType.Cavalry, -6f, -1f),
|
|
||||||
E("dz", "동탁군 기병", UnitType.Cavalry, 6f, -1f),
|
|
||||||
E("dz", "동탁군 보병", UnitType.Infantry, 0f, 1f),
|
|
||||||
E("dz", "동탁군 궁병", UnitType.Archer, -3f, 3f),
|
|
||||||
E("dz", "동탁군 궁병", UnitType.Archer, 3f, 3f),
|
|
||||||
E("dz", "동탁군 기병", UnitType.Cavalry, 0f, 7f),
|
|
||||||
Boss("dz", "lvbu", "여포", UnitType.Cavalry, 0f, 11f, 1.6f, 1.5f)),
|
|
||||||
Stage("적벽대전", "208년", 0.65f, 12f,
|
|
||||||
E("cc", "조조군 보병", UnitType.Infantry, -5f, -6f),
|
|
||||||
E("cc", "조조군 보병", UnitType.Infantry, 0f, -5f),
|
|
||||||
E("cc", "조조군 보병", UnitType.Infantry, 5f, -6f),
|
|
||||||
E("cc", "조조군 기병", UnitType.Cavalry, -3f, 0f),
|
|
||||||
E("cc", "조조군 기병", UnitType.Cavalry, 3f, 0f),
|
|
||||||
E("cc", "조조군 궁병", UnitType.Archer, -4f, 5f),
|
|
||||||
E("cc", "조조군 궁병", UnitType.Archer, 4f, 5f),
|
|
||||||
E("cc", "조조군 창병", UnitType.Spear, 0f, 7f),
|
|
||||||
Boss("cc", "caocao", "조조", UnitType.Infantry, 0f, 12f, 1.5f, 1.3f)),
|
|
||||||
Stage("오장원 전투", "234년", 0.7f, 12f,
|
|
||||||
E("wei", "위군 창병", UnitType.Spear, -3f, -6f),
|
|
||||||
E("wei", "위군 창병", UnitType.Spear, 3f, -6f),
|
|
||||||
E("wei", "위군 보병", UnitType.Infantry, -7f, -2f),
|
|
||||||
E("wei", "위군 보병", UnitType.Infantry, 7f, -2f),
|
|
||||||
E("wei", "위군 기병", UnitType.Cavalry, -2f, 2f),
|
|
||||||
E("wei", "위군 기병", UnitType.Cavalry, 2f, 2f),
|
|
||||||
E("wei", "위군 궁병", UnitType.Archer, -4f, 7f),
|
|
||||||
E("wei", "위군 궁병", UnitType.Archer, 4f, 7f),
|
|
||||||
E("wei", "위군 창병", UnitType.Spear, 0f, 9f),
|
|
||||||
Boss("wei", "simayi", "사마의", UnitType.Infantry, 0f, 13f, 1.6f, 1.3f)),
|
|
||||||
};
|
};
|
||||||
|
// 장수 배율 출발값(밸런스 세션 조정) · 보스 = 구 시드 보스 4인(1.3~1.6 / 1.2~1.5)의 가운데 값 · 장각·여포·조조는 generals.csv 에 구 값 승계
|
||||||
|
c.bossGeneralHpMult = 1.5f;
|
||||||
|
c.bossGeneralAtkMult = 1.3f;
|
||||||
|
c.eliteGeneralHpMult = 1.25f;
|
||||||
|
c.eliteGeneralAtkMult = 1.15f;
|
||||||
|
c.chapters = new BalanceConfig.ChapterDef[0];
|
||||||
|
c.stages = new BalanceConfig.StageDef[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
static BalanceConfig.UnitStats Stats(UnitType t, int hp, int atk, int def, float sling, float range, float radius)
|
static BalanceConfig.UnitStats Stats(UnitType t, int hp, int atk, int def, float sling, float range, float radius)
|
||||||
|
|
@ -128,32 +96,5 @@ namespace TKS.EditorTools
|
||||||
return new BalanceConfig.HeroDef { name = name, type = t, hpMult = hpMult, atkMult = atkMult, art = art };
|
return new BalanceConfig.HeroDef { name = name, type = t, hpMult = hpMult, atkMult = atkMult, art = art };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>적 병사 아트 id = <세력 접두>_<병종> (일러 세션 납품 이름 규칙 · 예 hgj_infantry).</summary>
|
|
||||||
static string ArtKey(UnitType t)
|
|
||||||
{
|
|
||||||
switch (t)
|
|
||||||
{
|
|
||||||
case UnitType.Infantry: return "infantry";
|
|
||||||
case UnitType.Cavalry: return "cavalry";
|
|
||||||
case UnitType.Archer: return "archer";
|
|
||||||
case UnitType.Spear: return "spear";
|
|
||||||
}
|
|
||||||
return "unit";
|
|
||||||
}
|
|
||||||
|
|
||||||
static BalanceConfig.EnemySpawn E(string faction, string name, UnitType t, float x, float y)
|
|
||||||
{
|
|
||||||
return new BalanceConfig.EnemySpawn { name = name, type = t, position = new Vector2(x, y), hpMult = 1f, atkMult = 1f, isBoss = false, art = faction + "_" + ArtKey(t) };
|
|
||||||
}
|
|
||||||
|
|
||||||
static BalanceConfig.EnemySpawn Boss(string faction, string roman, string name, UnitType t, float x, float y, float hpMult, float atkMult)
|
|
||||||
{
|
|
||||||
return new BalanceConfig.EnemySpawn { name = name, type = t, position = new Vector2(x, y), hpMult = hpMult, atkMult = atkMult, isBoss = true, art = faction + "_boss_" + roman };
|
|
||||||
}
|
|
||||||
|
|
||||||
static BalanceConfig.StageDef Stage(string title, string subtitle, float accuracy, float aggro, params BalanceConfig.EnemySpawn[] enemies)
|
|
||||||
{
|
|
||||||
return new BalanceConfig.StageDef { title = title, subtitle = subtitle, aiAccuracy = accuracy, aiAggroRange = aggro, enemies = new List<BalanceConfig.EnemySpawn>(enemies) };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ namespace TKS.EditorTools
|
||||||
if (created || overwrite)
|
if (created || overwrite)
|
||||||
{
|
{
|
||||||
BalanceDefaults.Apply(cfg);
|
BalanceDefaults.Apply(cfg);
|
||||||
|
StageImporter.ImportInto(cfg); // 챕터·스테이지 = Assets/Data/Stages/*.csv
|
||||||
EditorUtility.SetDirty(cfg);
|
EditorUtility.SetDirty(cfg);
|
||||||
}
|
}
|
||||||
return cfg;
|
return cfg;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace TKS.EditorTools
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 스테이지 표(Assets/Data/Stages/*.csv) → BalanceConfig.chapters·stages. 메뉴 TKS/Import Stages 또는
|
||||||
|
/// CLI: Unity.exe -batchmode -nographics -projectPath <proj> -executeMethod TKS.EditorTools.StageImporter.Import -quit
|
||||||
|
/// 형식 오류가 하나라도 있으면 자산을 건드리지 않는다(콘솔 error 로 행 번호 보고).
|
||||||
|
/// </summary>
|
||||||
|
public static class StageImporter
|
||||||
|
{
|
||||||
|
[MenuItem("TKS/Import Stages (스테이지 임포트)")]
|
||||||
|
public static void Import()
|
||||||
|
{
|
||||||
|
var cfg = ProjectSetup.EnsureConfig(false);
|
||||||
|
if (!ImportInto(cfg)) return;
|
||||||
|
AssetDatabase.SaveAssets();
|
||||||
|
Debug.Log("[TKS] 스테이지 임포트 완료 · 챕터 " + cfg.chapters.Length + " · 스테이지 " + cfg.stages.Length + " · " + ProjectSetup.ConfigPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string[] ReadFiles()
|
||||||
|
{
|
||||||
|
var dir = Path.Combine(Application.dataPath, StageTable.Folder);
|
||||||
|
var texts = new string[StageTable.Files.Length];
|
||||||
|
for (int i = 0; i < texts.Length; i++)
|
||||||
|
{
|
||||||
|
var p = Path.Combine(dir, StageTable.Files[i]);
|
||||||
|
texts[i] = File.Exists(p) ? File.ReadAllText(p, Encoding.UTF8) : null;
|
||||||
|
}
|
||||||
|
return texts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool ImportInto(BalanceConfig cfg)
|
||||||
|
{
|
||||||
|
var t = StageTable.Parse(ReadFiles());
|
||||||
|
if (t.errors.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var e in t.errors) Debug.LogError("[TKS] 스테이지 표 오류 · " + e);
|
||||||
|
Debug.LogError("[TKS] 스테이지 임포트 중단(오류 " + t.errors.Count + ") · 자산 변경 없음");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
StageTable.Apply(cfg, t);
|
||||||
|
EditorUtility.SetDirty(cfg);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 42dc6c9bd5b09554d8387821016f247c
|
||||||
|
|
@ -4,6 +4,13 @@ using UnityEngine;
|
||||||
|
|
||||||
namespace TKS
|
namespace TKS
|
||||||
{
|
{
|
||||||
|
public enum StageKind
|
||||||
|
{
|
||||||
|
Normal = 0,
|
||||||
|
Elite = 1, // 이름 있는 무장이 나오는 일반 스테이지(무장 isBoss = false · 배율만 보정)
|
||||||
|
Boss = 2 // 5·10·15·20
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 밸런스·스테이지 데이터의 단일 출처(SOT). 코드에는 수치를 두지 않고 이 자산만 읽는다.
|
/// 밸런스·스테이지 데이터의 단일 출처(SOT). 코드에는 수치를 두지 않고 이 자산만 읽는다.
|
||||||
/// 기본값은 에디터 셋업(ProjectSetup)이 최초 1회 시드한다.
|
/// 기본값은 에디터 셋업(ProjectSetup)이 최초 1회 시드한다.
|
||||||
|
|
@ -58,6 +65,50 @@ namespace TKS
|
||||||
[Tooltip("이 거리 안에 아군이 들어와야 적이 움직인다(넓은 전장에서 부대별 교전)")]
|
[Tooltip("이 거리 안에 아군이 들어와야 적이 움직인다(넓은 전장에서 부대별 교전)")]
|
||||||
public float aiAggroRange = 11f;
|
public float aiAggroRange = 11f;
|
||||||
public List<EnemySpawn> enemies = new List<EnemySpawn>();
|
public List<EnemySpawn> enemies = new List<EnemySpawn>();
|
||||||
|
|
||||||
|
[Header("스테이지 표(Assets/Data/Stages/stages.csv · TKS/Import Stages)")]
|
||||||
|
public int chapter;
|
||||||
|
[Tooltip("챕터 안 번호(1부터)")]
|
||||||
|
public int stage;
|
||||||
|
public StageKind kind;
|
||||||
|
public string place;
|
||||||
|
[Tooltip("factions.csv 세력 코드")]
|
||||||
|
public string faction;
|
||||||
|
public string bgTheme;
|
||||||
|
public string note;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class ChapterDef
|
||||||
|
{
|
||||||
|
public int no;
|
||||||
|
public string title;
|
||||||
|
public string subtitle;
|
||||||
|
public string years;
|
||||||
|
[Tooltip("Resources/ChapterMap/Maps/<map> 맵 그림 · Data/<map>.json 노드 자리(slots_N)")]
|
||||||
|
public string map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>적 배치 규칙(스테이지 표의 병사 수 → 전위·중군·후군 좌표 · 임포트 때 적용).</summary>
|
||||||
|
[Serializable]
|
||||||
|
public class FormationRule
|
||||||
|
{
|
||||||
|
[Tooltip("전위(보병·창병 우선) 첫 줄 y")]
|
||||||
|
public float frontY = -5.5f;
|
||||||
|
[Tooltip("중군(궁병 가운데 · 기병 양 끝) 첫 줄 y")]
|
||||||
|
public float middleY = 0.5f;
|
||||||
|
[Tooltip("후군 경비(창병·기병 우선) 줄 y")]
|
||||||
|
public float rearY = 7.5f;
|
||||||
|
[Tooltip("장수(보스·엘리트) 줄 y · 보스가 가운데")]
|
||||||
|
public float commandY = 11f;
|
||||||
|
public float colGap = 2.6f;
|
||||||
|
[Tooltip("같은 대가 여러 줄일 때 다음 줄 간격(적 쪽으로)")]
|
||||||
|
public float rowGap = 1.8f;
|
||||||
|
public int maxPerRow = 5;
|
||||||
|
[Tooltip("여러 줄일 때 줄마다 좌우로 엇갈리는 양(× colGap)")]
|
||||||
|
public float rowStagger = 0.5f;
|
||||||
|
[Range(0f, 1f)] public float frontShare = 0.4f;
|
||||||
|
[Range(0f, 1f)] public float rearShare = 0.15f;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Header("병종 기본 능력치")]
|
[Header("병종 기본 능력치")]
|
||||||
|
|
@ -119,9 +170,19 @@ namespace TKS
|
||||||
[Header("아군 영웅 (최대 5)")]
|
[Header("아군 영웅 (최대 5)")]
|
||||||
public HeroDef[] heroes = new HeroDef[0];
|
public HeroDef[] heroes = new HeroDef[0];
|
||||||
|
|
||||||
[Header("스테이지")]
|
[Header("스테이지 (Assets/Data/Stages/*.csv → 메뉴 TKS/Import Stages · 직접 고치면 다음 임포트에 덮임)")]
|
||||||
|
public ChapterDef[] chapters = new ChapterDef[0];
|
||||||
public StageDef[] stages = new StageDef[0];
|
public StageDef[] stages = new StageDef[0];
|
||||||
|
|
||||||
|
[Header("스테이지 임포트 규칙 (값을 바꾸면 다시 임포트)")]
|
||||||
|
public FormationRule formation = new FormationRule();
|
||||||
|
[Tooltip("보스 스테이지 장수 배율(generals.csv hp_mult·atk_mult 가 비었을 때) · isBoss 라 bossHpMult 가 더 곱해진다")]
|
||||||
|
public float bossGeneralHpMult = 1.5f;
|
||||||
|
public float bossGeneralAtkMult = 1.3f;
|
||||||
|
[Tooltip("엘리트 장수 배율(isBoss 아님 · generals.csv 가 비었을 때)")]
|
||||||
|
public float eliteGeneralHpMult = 1.25f;
|
||||||
|
public float eliteGeneralAtkMult = 1.15f;
|
||||||
|
|
||||||
public UnitStats GetStats(UnitType t)
|
public UnitStats GetStats(UnitType t)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < unitStats.Length; i++)
|
for (int i = 0; i < unitStats.Length; i++)
|
||||||
|
|
@ -131,6 +192,17 @@ namespace TKS
|
||||||
throw new InvalidOperationException("BalanceConfig 에 " + t + " 능력치가 없습니다.");
|
throw new InvalidOperationException("BalanceConfig 에 " + t + " 능력치가 없습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>챕터 no 의 스테이지 인덱스(stages 순서 = 표 순서).</summary>
|
||||||
|
public List<int> StagesOfChapter(int no)
|
||||||
|
{
|
||||||
|
var res = new List<int>();
|
||||||
|
for (int i = 0; i < stages.Length; i++)
|
||||||
|
{
|
||||||
|
if (stages[i].chapter == no) res.Add(i);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
public float Matchup(UnitType attacker, UnitType defender)
|
public float Matchup(UnitType attacker, UnitType defender)
|
||||||
{
|
{
|
||||||
int idx = (int)attacker * UnitTypeInfo.Count + (int)defender;
|
int idx = (int)attacker * UnitTypeInfo.Count + (int)defender;
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,13 @@ namespace TKS
|
||||||
es.AddComponent<StandaloneInputModule>();
|
es.AddComponent<StandaloneInputModule>();
|
||||||
}
|
}
|
||||||
|
|
||||||
stageText = MakeText("Stage", new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(30f, -28f), new Vector2(760f, 70f), 40, TextAnchor.UpperLeft);
|
stageText = MakeText("Stage", new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(30f, -28f), new Vector2(700f, 70f), 40, TextAnchor.UpperLeft);
|
||||||
|
// 스테이지 표 제목(최대 ~20자)이 오른쪽 위 적 수와 겹치지 않게 칸 안에서 글자 크기를 줄인다
|
||||||
|
stageText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||||
|
stageText.verticalOverflow = VerticalWrapMode.Truncate;
|
||||||
|
stageText.resizeTextForBestFit = true;
|
||||||
|
stageText.resizeTextMinSize = 24;
|
||||||
|
stageText.resizeTextMaxSize = 40;
|
||||||
countText = MakeText("Count", new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-30f, -28f), new Vector2(520f, 70f), 36, TextAnchor.UpperRight);
|
countText = MakeText("Count", new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-30f, -28f), new Vector2(520f, 70f), 36, TextAnchor.UpperRight);
|
||||||
turnText = MakeText("Turn", new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -92f), new Vector2(700f, 70f), 46, TextAnchor.UpperCenter);
|
turnText = MakeText("Turn", new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -92f), new Vector2(700f, 70f), 46, TextAnchor.UpperCenter);
|
||||||
turnText.color = new Color(1f, 0.95f, 0.6f);
|
turnText.color = new Color(1f, 0.95f, 0.6f);
|
||||||
|
|
@ -157,7 +163,9 @@ namespace TKS
|
||||||
void Refresh()
|
void Refresh()
|
||||||
{
|
{
|
||||||
var stage = battle.Stage;
|
var stage = battle.Stage;
|
||||||
stageText.text = "제" + (battle.StageIndex + 1) + "전투 · " + stage.title + (string.IsNullOrEmpty(stage.subtitle) ? "" : " " + stage.subtitle);
|
stageText.text = stage.chapter > 0
|
||||||
|
? stage.chapter + "-" + stage.stage + " · " + stage.title
|
||||||
|
: "제" + (battle.StageIndex + 1) + "전투 · " + stage.title + (string.IsNullOrEmpty(stage.subtitle) ? "" : " " + stage.subtitle);
|
||||||
countText.text = "아군 " + battle.AliveCount(Faction.Player) + " · 적 " + battle.AliveCount(Faction.Enemy);
|
countText.text = "아군 " + battle.AliveCount(Faction.Player) + " · 적 " + battle.AliveCount(Faction.Enemy);
|
||||||
|
|
||||||
switch (battle.Phase)
|
switch (battle.Phase)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,413 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace TKS
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 스테이지 표(Assets/Data/Stages/*.csv · utf-8-sig) 해석 + BalanceConfig 반영. 값의 SOT = CSV(C45) — 코드는 열 이름·형식만 안다.
|
||||||
|
/// stages.csv = chapter,stage,title,type(normal|elite|boss),place,faction,infantry,spear,cavalry,archer,generals(id:병종:보스여부 ; 구분),bg_theme,note
|
||||||
|
/// chapters.csv = chapter,title,subtitle,years,map,ai_accuracy,ai_aggro,note · generals.csv = id,name,hanja,hp_mult,atk_mult,note · factions.csv = code,name,art,note
|
||||||
|
/// 에디터 임포터(TKS/Import Stages)와 데이터 검증 테스트가 같이 쓴다.
|
||||||
|
/// </summary>
|
||||||
|
public static class StageTable
|
||||||
|
{
|
||||||
|
public const string Folder = "Data/Stages"; // Assets/ 아래
|
||||||
|
public static readonly string[] Files = { "stages.csv", "chapters.csv", "generals.csv", "factions.csv" };
|
||||||
|
|
||||||
|
public class GeneralRef
|
||||||
|
{
|
||||||
|
public string id;
|
||||||
|
public UnitType type;
|
||||||
|
public bool boss;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StageRow
|
||||||
|
{
|
||||||
|
public int line;
|
||||||
|
public int chapter;
|
||||||
|
public int stage;
|
||||||
|
public string title;
|
||||||
|
public StageKind kind;
|
||||||
|
public string place;
|
||||||
|
public string faction;
|
||||||
|
public int[] soldiers = new int[UnitTypeInfo.Count]; // UnitType 순서
|
||||||
|
public List<GeneralRef> generals = new List<GeneralRef>();
|
||||||
|
public string bgTheme;
|
||||||
|
public string note;
|
||||||
|
|
||||||
|
public int SoldierCount
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
int n = 0;
|
||||||
|
foreach (var c in soldiers) n += c;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChapterRow
|
||||||
|
{
|
||||||
|
public int no;
|
||||||
|
public string title;
|
||||||
|
public string subtitle;
|
||||||
|
public string years;
|
||||||
|
public string map;
|
||||||
|
public float aiAccuracy;
|
||||||
|
public float aiAggro;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GeneralInfo
|
||||||
|
{
|
||||||
|
public string id;
|
||||||
|
public string name;
|
||||||
|
public float hpMult = float.NaN; // 비면 역할(보스·엘리트) 기본 배율
|
||||||
|
public float atkMult = float.NaN;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FactionInfo
|
||||||
|
{
|
||||||
|
public string code;
|
||||||
|
public string name;
|
||||||
|
public string art;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Tables
|
||||||
|
{
|
||||||
|
public readonly List<StageRow> stages = new List<StageRow>();
|
||||||
|
public readonly List<ChapterRow> chapters = new List<ChapterRow>();
|
||||||
|
public readonly Dictionary<string, GeneralInfo> generals = new Dictionary<string, GeneralInfo>();
|
||||||
|
public readonly Dictionary<string, FactionInfo> factions = new Dictionary<string, FactionInfo>();
|
||||||
|
public readonly List<string> errors = new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 해석 ----------
|
||||||
|
|
||||||
|
/// <summary>texts = Files 순서의 CSV 내용. 형식 오류는 errors 에 모은다(하나라도 있으면 반영하지 않는다).</summary>
|
||||||
|
public static Tables Parse(string[] texts)
|
||||||
|
{
|
||||||
|
var t = new Tables();
|
||||||
|
foreach (var r in ReadCsv(texts[3], "factions.csv", t.errors))
|
||||||
|
{
|
||||||
|
var f = new FactionInfo { code = Get(r, "code"), name = Get(r, "name"), art = Get(r, "art") };
|
||||||
|
if (string.IsNullOrEmpty(f.code) || string.IsNullOrEmpty(f.art)) t.errors.Add("factions.csv 행 " + Get(r, "#") + ": code·art 필수");
|
||||||
|
else t.factions[f.code] = f;
|
||||||
|
}
|
||||||
|
foreach (var r in ReadCsv(texts[2], "generals.csv", t.errors))
|
||||||
|
{
|
||||||
|
var g = new GeneralInfo { id = Get(r, "id"), name = Get(r, "name") };
|
||||||
|
g.hpMult = OptFloat(r, "hp_mult", t.errors, "generals.csv");
|
||||||
|
g.atkMult = OptFloat(r, "atk_mult", t.errors, "generals.csv");
|
||||||
|
if (string.IsNullOrEmpty(g.id) || string.IsNullOrEmpty(g.name)) t.errors.Add("generals.csv 행 " + Get(r, "#") + ": id·name 필수");
|
||||||
|
else t.generals[g.id] = g;
|
||||||
|
}
|
||||||
|
foreach (var r in ReadCsv(texts[1], "chapters.csv", t.errors))
|
||||||
|
{
|
||||||
|
t.chapters.Add(new ChapterRow
|
||||||
|
{
|
||||||
|
no = Int(r, "chapter", t.errors, "chapters.csv"),
|
||||||
|
title = Get(r, "title"),
|
||||||
|
subtitle = Get(r, "subtitle"),
|
||||||
|
years = Get(r, "years"),
|
||||||
|
map = Get(r, "map"),
|
||||||
|
aiAccuracy = Float(r, "ai_accuracy", t.errors, "chapters.csv"),
|
||||||
|
aiAggro = Float(r, "ai_aggro", t.errors, "chapters.csv"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
foreach (var r in ReadCsv(texts[0], "stages.csv", t.errors))
|
||||||
|
{
|
||||||
|
string at = "stages.csv 행 " + Get(r, "#");
|
||||||
|
var s = new StageRow
|
||||||
|
{
|
||||||
|
line = Int(r, "#", t.errors, at),
|
||||||
|
chapter = Int(r, "chapter", t.errors, at),
|
||||||
|
stage = Int(r, "stage", t.errors, at),
|
||||||
|
title = Get(r, "title"),
|
||||||
|
place = Get(r, "place"),
|
||||||
|
faction = Get(r, "faction"),
|
||||||
|
bgTheme = Get(r, "bg_theme"),
|
||||||
|
note = Get(r, "note"),
|
||||||
|
};
|
||||||
|
at += " (" + s.chapter + "-" + s.stage + ")";
|
||||||
|
switch (Get(r, "type"))
|
||||||
|
{
|
||||||
|
case "normal": s.kind = StageKind.Normal; break;
|
||||||
|
case "elite": s.kind = StageKind.Elite; break;
|
||||||
|
case "boss": s.kind = StageKind.Boss; break;
|
||||||
|
default: t.errors.Add(at + ": type = normal|elite|boss (값 「" + Get(r, "type") + "」)"); break;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < UnitTypeInfo.Count; i++)
|
||||||
|
{
|
||||||
|
int n = Int(r, UnitTypeInfo.Key((UnitType)i), t.errors, at);
|
||||||
|
if (n < 0) t.errors.Add(at + ": 병사 수 음수");
|
||||||
|
s.soldiers[i] = Mathf.Max(0, n);
|
||||||
|
}
|
||||||
|
if (!t.factions.ContainsKey(s.faction)) t.errors.Add(at + ": faction 「" + s.faction + "」 이 factions.csv 에 없음");
|
||||||
|
foreach (var part in Get(r, "generals").Split(';'))
|
||||||
|
{
|
||||||
|
var g = part.Trim();
|
||||||
|
if (g.Length == 0) continue;
|
||||||
|
var f = g.Split(':');
|
||||||
|
UnitType ut;
|
||||||
|
if (f.Length != 3 || !UnitTypeInfo.TryParseKey(f[1].Trim(), out ut) || (f[2].Trim() != "true" && f[2].Trim() != "false"))
|
||||||
|
{
|
||||||
|
t.errors.Add(at + ": generals 형식 = id:병종(infantry|spear|cavalry|archer):true|false (값 「" + g + "」)");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var gr = new GeneralRef { id = f[0].Trim(), type = ut, boss = f[2].Trim() == "true" };
|
||||||
|
if (!t.generals.ContainsKey(gr.id)) t.errors.Add(at + ": 무장 「" + gr.id + "」 이 generals.csv 에 없음");
|
||||||
|
if (gr.boss && s.kind != StageKind.Boss) t.errors.Add(at + ": 보스 무장은 type = boss 스테이지에만");
|
||||||
|
s.generals.Add(gr);
|
||||||
|
}
|
||||||
|
t.stages.Add(s);
|
||||||
|
}
|
||||||
|
// 챕터 안 번호 = 1 부터 빈칸 없이 · 챕터는 chapters.csv 에 있어야 한다
|
||||||
|
var seen = new HashSet<long>();
|
||||||
|
foreach (var s in t.stages)
|
||||||
|
{
|
||||||
|
if (!seen.Add(s.chapter * 1000L + s.stage)) t.errors.Add("stages.csv: " + s.chapter + "-" + s.stage + " 중복");
|
||||||
|
if (t.chapters.Find(c => c.no == s.chapter) == null) t.errors.Add("stages.csv: 챕터 " + s.chapter + " 이 chapters.csv 에 없음");
|
||||||
|
}
|
||||||
|
foreach (var c in t.chapters)
|
||||||
|
{
|
||||||
|
int n = t.stages.FindAll(s => s.chapter == c.no).Count;
|
||||||
|
for (int k = 1; k <= n; k++)
|
||||||
|
if (!seen.Contains(c.no * 1000L + k)) t.errors.Add("stages.csv: 챕터 " + c.no + " 의 " + k + "번 스테이지 없음(1부터 빈칸 없이)");
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 반영 ----------
|
||||||
|
|
||||||
|
/// <summary>chapters·stages 를 표 순서(챕터 → 스테이지)로 덮어쓴다. 적 좌표 = cfg.formation 규칙.</summary>
|
||||||
|
public static void Apply(BalanceConfig cfg, Tables t)
|
||||||
|
{
|
||||||
|
var chapters = new List<ChapterRow>(t.chapters);
|
||||||
|
chapters.Sort((a, b) => a.no.CompareTo(b.no));
|
||||||
|
var rows = new List<StageRow>(t.stages);
|
||||||
|
rows.Sort((a, b) => a.chapter != b.chapter ? a.chapter.CompareTo(b.chapter) : a.stage.CompareTo(b.stage));
|
||||||
|
|
||||||
|
cfg.chapters = new BalanceConfig.ChapterDef[chapters.Count];
|
||||||
|
for (int i = 0; i < chapters.Count; i++)
|
||||||
|
{
|
||||||
|
var c = chapters[i];
|
||||||
|
cfg.chapters[i] = new BalanceConfig.ChapterDef { no = c.no, title = c.title, subtitle = c.subtitle, years = c.years, map = c.map };
|
||||||
|
}
|
||||||
|
cfg.stages = new BalanceConfig.StageDef[rows.Count];
|
||||||
|
for (int i = 0; i < rows.Count; i++)
|
||||||
|
{
|
||||||
|
var r = rows[i];
|
||||||
|
var c = chapters.Find(x => x.no == r.chapter);
|
||||||
|
cfg.stages[i] = new BalanceConfig.StageDef
|
||||||
|
{
|
||||||
|
title = r.title,
|
||||||
|
subtitle = string.IsNullOrEmpty(c.years) ? "" : c.years + "년",
|
||||||
|
aiAccuracy = c.aiAccuracy,
|
||||||
|
aiAggroRange = c.aiAggro,
|
||||||
|
enemies = Enemies(cfg, r, t),
|
||||||
|
chapter = r.chapter,
|
||||||
|
stage = r.stage,
|
||||||
|
kind = r.kind,
|
||||||
|
place = r.place,
|
||||||
|
faction = r.faction,
|
||||||
|
bgTheme = r.bgTheme,
|
||||||
|
note = r.note,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 병사 → 후군 경비(창·기·보 우선 · 총수 × rearShare 내림) · 전위(보·창·기 우선 · 총수 × frontShare 올림) · 나머지 = 중군(궁 가운데 → 보·창 → 기병 양 끝).
|
||||||
|
/// 대마다 maxPerRow 넘으면 고르게 여러 줄(적 쪽으로 rowGap · 줄마다 엇갈림). 장수 = commandY 줄(보스 → 엘리트 순 가운데부터).
|
||||||
|
/// </summary>
|
||||||
|
public static List<BalanceConfig.EnemySpawn> Enemies(BalanceConfig cfg, StageRow r, Tables t)
|
||||||
|
{
|
||||||
|
var F = cfg.formation;
|
||||||
|
var fac = t.factions[r.faction];
|
||||||
|
var pool = (int[])r.soldiers.Clone();
|
||||||
|
int total = r.SoldierCount;
|
||||||
|
var rear = Take(pool, Mathf.FloorToInt(total * F.rearShare), UnitType.Spear, UnitType.Cavalry, UnitType.Infantry);
|
||||||
|
var front = Take(pool, Mathf.CeilToInt(total * F.frontShare), UnitType.Infantry, UnitType.Spear, UnitType.Cavalry);
|
||||||
|
var middle = Take(pool, total, UnitType.Archer, UnitType.Infantry, UnitType.Spear, UnitType.Cavalry);
|
||||||
|
|
||||||
|
var res = new List<BalanceConfig.EnemySpawn>();
|
||||||
|
foreach (var band in new[] { new KeyValuePair<float, List<UnitType>>(F.frontY, front), new KeyValuePair<float, List<UnitType>>(F.middleY, middle), new KeyValuePair<float, List<UnitType>>(F.rearY, rear) })
|
||||||
|
{
|
||||||
|
var pos = Block(F, band.Key, band.Value.Count);
|
||||||
|
for (int i = 0; i < band.Value.Count; i++)
|
||||||
|
{
|
||||||
|
var ut = band.Value[i];
|
||||||
|
res.Add(new BalanceConfig.EnemySpawn
|
||||||
|
{
|
||||||
|
name = fac.name + " " + UnitTypeInfo.Korean(ut),
|
||||||
|
type = ut,
|
||||||
|
position = pos[i],
|
||||||
|
hpMult = 1f,
|
||||||
|
atkMult = 1f,
|
||||||
|
isBoss = false,
|
||||||
|
art = fac.art + "_" + UnitTypeInfo.Key(ut),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var gens = new List<GeneralRef>(r.generals);
|
||||||
|
gens.Sort((a, b) => b.boss.CompareTo(a.boss)); // 보스 먼저(가운데)
|
||||||
|
var gpos = Block(F, F.commandY, gens.Count);
|
||||||
|
for (int i = 0; i < gens.Count; i++)
|
||||||
|
{
|
||||||
|
var g = gens[i];
|
||||||
|
var info = t.generals[g.id];
|
||||||
|
res.Add(new BalanceConfig.EnemySpawn
|
||||||
|
{
|
||||||
|
name = info.name,
|
||||||
|
type = g.type,
|
||||||
|
position = gpos[i],
|
||||||
|
hpMult = !float.IsNaN(info.hpMult) ? info.hpMult : g.boss ? cfg.bossGeneralHpMult : cfg.eliteGeneralHpMult,
|
||||||
|
atkMult = !float.IsNaN(info.atkMult) ? info.atkMult : g.boss ? cfg.bossGeneralAtkMult : cfg.eliteGeneralAtkMult,
|
||||||
|
isBoss = g.boss,
|
||||||
|
art = g.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>pool 에서 우선순위 병종 순으로 n 기를 꺼낸다.</summary>
|
||||||
|
static List<UnitType> Take(int[] pool, int n, params UnitType[] order)
|
||||||
|
{
|
||||||
|
var res = new List<UnitType>();
|
||||||
|
foreach (var ut in order)
|
||||||
|
{
|
||||||
|
while (res.Count < n && pool[(int)ut] > 0)
|
||||||
|
{
|
||||||
|
pool[(int)ut]--;
|
||||||
|
res.Add(ut);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>n 자리 = 줄마다 가운데부터 바깥으로(같은 거리면 왼쪽 먼저). 줄 수 = ceil(n / maxPerRow) · 줄마다 고르게.</summary>
|
||||||
|
static List<Vector2> Block(BalanceConfig.FormationRule F, float y0, int n)
|
||||||
|
{
|
||||||
|
var res = new List<Vector2>();
|
||||||
|
if (n <= 0) return res;
|
||||||
|
int per = Mathf.Max(1, F.maxPerRow);
|
||||||
|
int rows = (n + per - 1) / per;
|
||||||
|
int left = n;
|
||||||
|
for (int row = 0; row < rows; row++)
|
||||||
|
{
|
||||||
|
int k = (left + (rows - row) - 1) / (rows - row);
|
||||||
|
left -= k;
|
||||||
|
float shift = rows > 1 ? (row % 2 == 0 ? -0.5f : 0.5f) * F.rowStagger * F.colGap : 0f;
|
||||||
|
var xs = new List<float>();
|
||||||
|
for (int j = 0; j < k; j++) xs.Add((j - (k - 1) * 0.5f) * F.colGap);
|
||||||
|
xs.Sort((a, b) => Mathf.Abs(a) != Mathf.Abs(b) ? Mathf.Abs(a).CompareTo(Mathf.Abs(b)) : a.CompareTo(b));
|
||||||
|
foreach (var x in xs) res.Add(new Vector2(x + shift, y0 + row * F.rowGap));
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- CSV ----------
|
||||||
|
|
||||||
|
/// <summary>머리줄 = 열 이름 · 따옴표("…" · "" = ") 지원 · 빈 줄 건너뜀 · 각 행에 "#" = 파일 줄 번호.</summary>
|
||||||
|
public static List<Dictionary<string, string>> ReadCsv(string text, string file, List<string> errors)
|
||||||
|
{
|
||||||
|
var res = new List<Dictionary<string, string>>();
|
||||||
|
if (string.IsNullOrEmpty(text))
|
||||||
|
{
|
||||||
|
errors.Add(file + ": 비어 있음");
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
if (text[0] == '') text = text.Substring(1);
|
||||||
|
var records = Split(text);
|
||||||
|
if (records.Count == 0) return res;
|
||||||
|
var head = records[0].Value;
|
||||||
|
for (int i = 1; i < records.Count; i++)
|
||||||
|
{
|
||||||
|
var cells = records[i].Value;
|
||||||
|
if (cells.Count == 1 && cells[0].Trim().Length == 0) continue;
|
||||||
|
if (cells.Count != head.Count)
|
||||||
|
{
|
||||||
|
errors.Add(file + " 줄 " + records[i].Key + ": 칸 수 " + cells.Count + " ≠ 머리줄 " + head.Count);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var d = new Dictionary<string, string> { { "#", records[i].Key.ToString(CultureInfo.InvariantCulture) } };
|
||||||
|
for (int c = 0; c < head.Count; c++) d[head[c].Trim()] = cells[c].Trim();
|
||||||
|
res.Add(d);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<KeyValuePair<int, List<string>>> Split(string text)
|
||||||
|
{
|
||||||
|
var rows = new List<KeyValuePair<int, List<string>>>();
|
||||||
|
var cells = new List<string>();
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
bool quoted = false;
|
||||||
|
int line = 1, start = 1;
|
||||||
|
for (int i = 0; i < text.Length; i++)
|
||||||
|
{
|
||||||
|
char ch = text[i];
|
||||||
|
if (quoted)
|
||||||
|
{
|
||||||
|
if (ch == '"' && i + 1 < text.Length && text[i + 1] == '"') { sb.Append('"'); i++; }
|
||||||
|
else if (ch == '"') quoted = false;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (ch == '\n') line++;
|
||||||
|
sb.Append(ch);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch == '"') quoted = true;
|
||||||
|
else if (ch == ',') { cells.Add(sb.ToString()); sb.Length = 0; }
|
||||||
|
else if (ch == '\r') { }
|
||||||
|
else if (ch == '\n')
|
||||||
|
{
|
||||||
|
cells.Add(sb.ToString());
|
||||||
|
sb.Length = 0;
|
||||||
|
rows.Add(new KeyValuePair<int, List<string>>(start, cells));
|
||||||
|
cells = new List<string>();
|
||||||
|
line++;
|
||||||
|
start = line;
|
||||||
|
}
|
||||||
|
else sb.Append(ch);
|
||||||
|
}
|
||||||
|
if (sb.Length > 0 || cells.Count > 0)
|
||||||
|
{
|
||||||
|
cells.Add(sb.ToString());
|
||||||
|
rows.Add(new KeyValuePair<int, List<string>>(start, cells));
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Get(Dictionary<string, string> r, string key)
|
||||||
|
{
|
||||||
|
string v;
|
||||||
|
return r.TryGetValue(key, out v) ? v : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Int(Dictionary<string, string> r, string key, List<string> errors, string at)
|
||||||
|
{
|
||||||
|
int v;
|
||||||
|
if (int.TryParse(Get(r, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out v)) return v;
|
||||||
|
errors.Add(at + ": " + key + " 정수 아님(「" + Get(r, key) + "」)");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static float Float(Dictionary<string, string> r, string key, List<string> errors, string at)
|
||||||
|
{
|
||||||
|
float v;
|
||||||
|
if (float.TryParse(Get(r, key), NumberStyles.Float, CultureInfo.InvariantCulture, out v)) return v;
|
||||||
|
errors.Add(at + " 행 " + Get(r, "#") + ": " + key + " 숫자 아님(「" + Get(r, key) + "」)");
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
static float OptFloat(Dictionary<string, string> r, string key, List<string> errors, string at)
|
||||||
|
{
|
||||||
|
return Get(r, key).Length == 0 ? float.NaN : Float(r, key, errors, at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 39939f824eda1494eadd2b2ab38c602a
|
||||||
|
|
@ -47,5 +47,32 @@ namespace TKS
|
||||||
{
|
{
|
||||||
return t != UnitType.Archer;
|
return t != UnitType.Archer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>데이터·아트 id 용 영문 키(infantry·cavalry·archer·spear) · 적 병사 아트 = <세력>_<키>.</summary>
|
||||||
|
public static string Key(UnitType t)
|
||||||
|
{
|
||||||
|
switch (t)
|
||||||
|
{
|
||||||
|
case UnitType.Infantry: return "infantry";
|
||||||
|
case UnitType.Cavalry: return "cavalry";
|
||||||
|
case UnitType.Archer: return "archer";
|
||||||
|
case UnitType.Spear: return "spear";
|
||||||
|
}
|
||||||
|
return "unit";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryParseKey(string key, out UnitType t)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < Count; i++)
|
||||||
|
{
|
||||||
|
if (Key((UnitType)i) == key)
|
||||||
|
{
|
||||||
|
t = (UnitType)i;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t = UnitType.Infantry;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.UI;
|
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)
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class WorldKitItem
|
public class WorldKitItem
|
||||||
|
|
@ -36,8 +39,7 @@ namespace TKS
|
||||||
public float[] roadX;
|
public float[] roadX;
|
||||||
public float[] roadY;
|
public float[] roadY;
|
||||||
public WorldLandmark[] landmarks;
|
public WorldLandmark[] landmarks;
|
||||||
public int nodes; // 스테이지 수(임시 · 기획 세션이 확정)
|
// world.json 의 nodes·stageLink(구 임시 연결 spec/stage_plan.json) 는 읽지 않는다 — 노드 = 스테이지 표(2026-11-05 145 스테이지)
|
||||||
public int stageLink; // 이 챕터 노드가 쓰는 BalanceConfig.stages 인덱스(임시)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
|
|
@ -56,6 +58,52 @@ namespace TKS
|
||||||
public WorldChapter[] chapters;
|
public WorldChapter[] chapters;
|
||||||
|
|
||||||
public const string Path = "ChapterMap/Data/world";
|
public const string Path = "ChapterMap/Data/world";
|
||||||
|
public const string DataRoot = "ChapterMap/Data/";
|
||||||
|
|
||||||
|
public WorldChapter FindMap(string map)
|
||||||
|
{
|
||||||
|
if (chapters != null)
|
||||||
|
foreach (var c in chapters)
|
||||||
|
if (c.map == map) return c;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Data/<map>.json(원화 세션 anchors) 의 slots_n(맵 px · 첫 칸 = 출발). 없으면 null.</summary>
|
||||||
|
public static Vector2[] AnchorSlots(string map, int n)
|
||||||
|
{
|
||||||
|
var t = Resources.Load<TextAsset>(DataRoot + map);
|
||||||
|
if (t == null || n <= 0) return null;
|
||||||
|
var m = Regex.Match(t.text, "\"slots_" + n + "\"\\s*:\\s*\\[((?:\\s*\\[[^\\]]*\\]\\s*,?)*)\\s*\\]");
|
||||||
|
if (!m.Success) return null;
|
||||||
|
var nums = Regex.Matches(m.Groups[1].Value, @"-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?");
|
||||||
|
if (nums.Count != n * 2) return null;
|
||||||
|
var res = new Vector2[n];
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
res[i] = new Vector2(float.Parse(nums[2 * i].Value, CultureInfo.InvariantCulture), float.Parse(nums[2 * i + 1].Value, CultureInfo.InvariantCulture));
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>점 p 에 가장 가까운 길 위 지점의 호 길이.</summary>
|
||||||
|
public static float ArcOf(WorldChapter c, float[] acc, Vector2 p)
|
||||||
|
{
|
||||||
|
int m = acc.Length;
|
||||||
|
if (m == 1) return 0f;
|
||||||
|
float best = float.MaxValue, res = 0f;
|
||||||
|
for (int i = 1; i < m; i++)
|
||||||
|
{
|
||||||
|
var a = new Vector2(c.roadX[i - 1], c.roadY[i - 1]);
|
||||||
|
var b = new Vector2(c.roadX[i], c.roadY[i]);
|
||||||
|
var ab = b - a;
|
||||||
|
float t = Mathf.Clamp01(Vector2.Dot(p - a, ab) / Mathf.Max(ab.sqrMagnitude, 1e-6f));
|
||||||
|
float d = (a + ab * t - p).sqrMagnitude;
|
||||||
|
if (d < best)
|
||||||
|
{
|
||||||
|
best = d;
|
||||||
|
res = acc[i - 1] + (acc[i] - acc[i - 1]) * t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
public static WorldData Load()
|
public static WorldData Load()
|
||||||
{
|
{
|
||||||
|
|
@ -141,7 +189,8 @@ namespace TKS
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 게임 시작 화면 = 챕터 월드맵(스크롤 · 상하좌우로 끌기). 길 위 깃발 노드를 누르면 스테이지 팝업 → 출전 = 전투 시작 ·
|
/// 게임 시작 화면 = 챕터 월드맵(스크롤 · 상하좌우로 끌기). 길 위 깃발 노드를 누르면 스테이지 팝업 → 출전 = 전투 시작 ·
|
||||||
/// 전투 결과를 탭하면 월드맵으로 돌아와 다음 노드가 열린다. 그림·좌표·부품 배치 = world.json · 전투 데이터 = BalanceConfig(C45).
|
/// 전투 결과를 탭하면 월드맵으로 돌아와 다음 노드가 열린다. 그림·길·부품 배치 = world.json · 노드 자리 = anchors slots_N(없으면 길 위 균등) ·
|
||||||
|
/// 챕터·노드·전투 = BalanceConfig.chapters·stages(스테이지 표 · C45) · 보스 노드 = type boss.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class WorldMapScreen : MonoBehaviour
|
public class WorldMapScreen : MonoBehaviour
|
||||||
{
|
{
|
||||||
|
|
@ -156,6 +205,8 @@ namespace TKS
|
||||||
BattleHUD hud;
|
BattleHUD hud;
|
||||||
SlingshotInput sling;
|
SlingshotInput sling;
|
||||||
WorldData data;
|
WorldData data;
|
||||||
|
BalanceConfig cfg;
|
||||||
|
readonly List<int> nodeStages = new List<int>(); // 현재 챕터 노드 → stages 인덱스
|
||||||
|
|
||||||
GameObject root;
|
GameObject root;
|
||||||
CanvasGroup group;
|
CanvasGroup group;
|
||||||
|
|
@ -183,14 +234,50 @@ namespace TKS
|
||||||
int entryChapter = -1;
|
int entryChapter = -1;
|
||||||
int entryNode = -1;
|
int entryNode = -1;
|
||||||
|
|
||||||
public bool HasData { get { return data != null && data.chapters != null && data.chapters.Length > 0; } }
|
public bool HasData
|
||||||
|
{
|
||||||
|
get { return data != null && data.chapters != null && data.chapters.Length > 0 && cfg != null && cfg.chapters != null && cfg.chapters.Length > 0 && cfg.stages.Length > 0; }
|
||||||
|
}
|
||||||
public bool IsVisible { get { return root != null && root.activeSelf; } }
|
public bool IsVisible { get { return root != null && root.activeSelf; } }
|
||||||
public bool PopupVisible { get { return popup != null && popup.activeSelf; } }
|
public bool PopupVisible { get { return popup != null && popup.activeSelf; } }
|
||||||
public int Chapter { get { return chapter; } }
|
public int Chapter { get { return chapter; } }
|
||||||
public int ChapterCount { get { return HasData ? data.chapters.Length : 0; } }
|
public int ChapterCount { get { return HasData ? cfg.chapters.Length : 0; } }
|
||||||
public WorldData Data { get { return data; } }
|
public WorldData Data { get { return data; } }
|
||||||
public Sprite MapSprite { get { return mapImage != null ? mapImage.sprite : null; } }
|
public Sprite MapSprite { get { return mapImage != null ? mapImage.sprite : null; } }
|
||||||
public int NodeCount { get { return nodeButtons.Count; } }
|
public int NodeCount { get { return nodeButtons.Count; } }
|
||||||
|
public int BossNodeCount { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>챕터(0부터)의 노드 수 = 스테이지 표의 그 챕터 스테이지 수.</summary>
|
||||||
|
public int NodesIn(int ch)
|
||||||
|
{
|
||||||
|
return HasData && ch >= 0 && ch < cfg.chapters.Length ? cfg.StagesOfChapter(cfg.chapters[ch].no).Count : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>챕터(0부터) · 노드(0부터) → BalanceConfig.stages 인덱스(없으면 −1).</summary>
|
||||||
|
public int StageIndexOf(int ch, int node)
|
||||||
|
{
|
||||||
|
if (!HasData || ch < 0 || ch >= cfg.chapters.Length) return -1;
|
||||||
|
var list = cfg.StagesOfChapter(cfg.chapters[ch].no);
|
||||||
|
return node >= 0 && node < list.Count ? list[node] : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>챕터(0부터)의 맵 길(world.json · chapters.csv map 열). 없으면 첫 맵.</summary>
|
||||||
|
public WorldChapter MapOf(int ch)
|
||||||
|
{
|
||||||
|
if (!HasData) return null;
|
||||||
|
var wc = ch >= 0 && ch < cfg.chapters.Length ? data.FindMap(cfg.chapters[ch].map) : null;
|
||||||
|
return wc ?? data.chapters[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>노드 자리(맵 px) = anchors slots_N · 그 N 이 없으면 길 위 균등.</summary>
|
||||||
|
public Vector2[] NodeSlots(int ch, out bool fromAnchors)
|
||||||
|
{
|
||||||
|
int n = NodesIn(ch);
|
||||||
|
var wc = MapOf(ch);
|
||||||
|
var s = wc != null ? WorldData.AnchorSlots(wc.map, n) : null;
|
||||||
|
fromAnchors = s != null;
|
||||||
|
return s ?? (wc != null ? WorldData.Slots(wc, n) : new Vector2[0]);
|
||||||
|
}
|
||||||
|
|
||||||
void Awake()
|
void Awake()
|
||||||
{
|
{
|
||||||
|
|
@ -208,9 +295,10 @@ namespace TKS
|
||||||
hud = h;
|
hud = h;
|
||||||
sling = s;
|
sling = s;
|
||||||
data = WorldData.Load();
|
data = WorldData.Load();
|
||||||
|
cfg = b != null ? b.Config : null;
|
||||||
if (!HasData)
|
if (!HasData)
|
||||||
{
|
{
|
||||||
Debug.LogWarning("[TKS] 월드맵 데이터 없음(" + WorldData.Path + ") → 바로 전투");
|
Debug.LogWarning("[TKS] 월드맵 데이터 없음(" + WorldData.Path + " 또는 BalanceConfig.chapters — TKS/Import Stages) → 바로 전투");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
battle.ResultHandler = OnBattleResult;
|
battle.ResultHandler = OnBattleResult;
|
||||||
|
|
@ -282,34 +370,42 @@ namespace TKS
|
||||||
public void ShowChapter(int ch)
|
public void ShowChapter(int ch)
|
||||||
{
|
{
|
||||||
if (!HasData) return;
|
if (!HasData) return;
|
||||||
chapter = Mathf.Clamp(ch, 0, data.chapters.Length - 1);
|
chapter = Mathf.Clamp(ch, 0, cfg.chapters.Length - 1);
|
||||||
ClosePopup();
|
ClosePopup();
|
||||||
var c = data.chapters[chapter];
|
var def = cfg.chapters[chapter];
|
||||||
|
var c = MapOf(chapter);
|
||||||
foreach (var go in spawned)
|
foreach (var go in spawned)
|
||||||
if (go != null) Destroy(go);
|
if (go != null) Destroy(go);
|
||||||
spawned.Clear();
|
spawned.Clear();
|
||||||
nodeButtons.Clear();
|
nodeButtons.Clear();
|
||||||
cursor = null;
|
cursor = null;
|
||||||
|
nodeStages.Clear();
|
||||||
|
nodeStages.AddRange(cfg.StagesOfChapter(def.no));
|
||||||
|
BossNodeCount = 0;
|
||||||
|
|
||||||
|
if (c.map != def.map) Debug.LogWarning("[TKS] 챕터 " + def.no + " 맵 「" + def.map + "」 이 world.json 에 없음 → " + c.map);
|
||||||
var sp = Resources.Load<Sprite>(MapRoot + c.map);
|
var sp = Resources.Load<Sprite>(MapRoot + c.map);
|
||||||
if (sp == null) Debug.LogWarning("[TKS] 맵 그림 없음: " + MapRoot + c.map);
|
if (sp == null) Debug.LogWarning("[TKS] 맵 그림 없음: " + MapRoot + c.map);
|
||||||
mapImage.sprite = sp;
|
mapImage.sprite = sp;
|
||||||
mapImage.color = sp != null ? Color.white : new Color(0.55f, 0.7f, 0.45f);
|
mapImage.color = sp != null ? Color.white : new Color(0.55f, 0.7f, 0.45f);
|
||||||
bannerText.text = "제" + c.no + "장 " + c.ko;
|
bannerText.text = "제" + def.no + "장 " + def.title;
|
||||||
prevButton.interactable = chapter > 0;
|
prevButton.interactable = chapter > 0;
|
||||||
nextButton.interactable = chapter < data.chapters.Length - 1;
|
nextButton.interactable = chapter < cfg.chapters.Length - 1;
|
||||||
|
|
||||||
var slots = WorldData.Slots(c, c.nodes);
|
bool fromAnchors;
|
||||||
|
var slots = NodeSlots(chapter, out fromAnchors);
|
||||||
var acc = WorldData.Arc(c);
|
var acc = WorldData.Arc(c);
|
||||||
float total = acc[acc.Length - 1];
|
var len = new float[slots.Length];
|
||||||
|
for (int i = 0; i < slots.Length; i++) len[i] = WorldData.ArcOf(c, acc, slots[i]);
|
||||||
int cleared = ChapterUnlocked(chapter) ? WorldProgress.Cleared(chapter) : 0;
|
int cleared = ChapterUnlocked(chapter) ? WorldProgress.Cleared(chapter) : 0;
|
||||||
|
|
||||||
// 길 점(노드 사이)
|
// 길 점(노드 사이 · 노드 자리를 길에 투영한 호 길이 구간)
|
||||||
float spacing = Mathf.Max(8f, data.dotSpacing);
|
float spacing = Mathf.Max(8f, data.dotSpacing);
|
||||||
for (int i = 0; i + 1 < slots.Length; i++)
|
for (int i = 0; i + 1 < slots.Length; i++)
|
||||||
{
|
{
|
||||||
float l0 = total * i / (slots.Length - 1) + 44f;
|
float l0 = Mathf.Min(len[i], len[i + 1]) + 44f;
|
||||||
float l1 = total * (i + 1) / (slots.Length - 1) - 44f;
|
float l1 = Mathf.Max(len[i], len[i + 1]) - 44f;
|
||||||
|
if (l1 <= l0) continue;
|
||||||
int m = Mathf.Max(0, Mathf.FloorToInt((l1 - l0) / spacing));
|
int m = Mathf.Max(0, Mathf.FloorToInt((l1 - l0) / spacing));
|
||||||
for (int j = 0; j <= m; j++)
|
for (int j = 0; j <= m; j++)
|
||||||
{
|
{
|
||||||
|
|
@ -332,7 +428,9 @@ namespace TKS
|
||||||
// 노드
|
// 노드
|
||||||
for (int i = 0; i < slots.Length; i++)
|
for (int i = 0; i < slots.Length; i++)
|
||||||
{
|
{
|
||||||
bool boss = i == slots.Length - 1;
|
var stage = cfg.stages[nodeStages[i]];
|
||||||
|
bool boss = stage.kind == StageKind.Boss;
|
||||||
|
if (boss) BossNodeCount++;
|
||||||
var st = StateOf(chapter, i);
|
var st = StateOf(chapter, i);
|
||||||
string key = (boss ? "flag_boss_" : "flag_") + (st == NodeState.Clear ? "clear" : st == NodeState.Open ? "open" : "lock");
|
string key = (boss ? "flag_boss_" : "flag_") + (st == NodeState.Clear ? "clear" : st == NodeState.Open ? "open" : "lock");
|
||||||
if (boss) Piece("frame", slots[i] + V(data.flagFrame), false);
|
if (boss) Piece("frame", slots[i] + V(data.flagFrame), false);
|
||||||
|
|
@ -346,7 +444,7 @@ namespace TKS
|
||||||
var kit = data.Kit(key);
|
var kit = data.Kit(key);
|
||||||
if (st != NodeState.Lock && kit != null)
|
if (st != NodeState.Lock && kit != null)
|
||||||
{
|
{
|
||||||
var t = Label(img.transform, c.no + "-" + (i + 1), boss ? 38 : (i + 1 >= 10 ? 26 : 32), Paper, new Vector2(kit.numberCenter[0], 1f - kit.numberCenter[1]));
|
var t = Label(img.transform, def.no + "-" + stage.stage, boss ? 38 : (def.no >= 10 || stage.stage >= 10 ? 26 : 32), Paper, new Vector2(kit.numberCenter[0], 1f - kit.numberCenter[1]));
|
||||||
t.raycastTarget = false;
|
t.raycastTarget = false;
|
||||||
}
|
}
|
||||||
if (st == NodeState.Clear)
|
if (st == NodeState.Clear)
|
||||||
|
|
@ -392,8 +490,8 @@ namespace TKS
|
||||||
|
|
||||||
public bool ChapterUnlocked(int ch)
|
public bool ChapterUnlocked(int ch)
|
||||||
{
|
{
|
||||||
if (!HasData || ch < 0 || ch >= data.chapters.Length) return false;
|
if (!HasData || ch < 0 || ch >= cfg.chapters.Length) return false;
|
||||||
return ch == 0 || WorldProgress.Cleared(ch - 1) >= data.chapters[ch - 1].nodes;
|
return ch == 0 || WorldProgress.Cleared(ch - 1) >= NodesIn(ch - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 팝업 ----------
|
// ---------- 팝업 ----------
|
||||||
|
|
@ -407,21 +505,18 @@ namespace TKS
|
||||||
public void OpenPopup(int node)
|
public void OpenPopup(int node)
|
||||||
{
|
{
|
||||||
if (!HasData) return;
|
if (!HasData) return;
|
||||||
var c = data.chapters[chapter];
|
int si = StageIndexOf(chapter, node);
|
||||||
if (node < 0 || node >= c.nodes) return;
|
if (si < 0) return;
|
||||||
popupNode = node;
|
popupNode = node;
|
||||||
var st = StateOf(chapter, node);
|
var st = StateOf(chapter, node);
|
||||||
var stage = StageFor(c);
|
var def = cfg.chapters[chapter];
|
||||||
bool boss = node == c.nodes - 1;
|
var stage = cfg.stages[si];
|
||||||
popupTitle.text = c.no + "-" + (node + 1) + (boss ? " 보스" : "") + "\n" + c.ko;
|
string tag = stage.kind == StageKind.Boss ? " 보스" : stage.kind == StageKind.Elite ? " 엘리트" : "";
|
||||||
|
popupTitle.text = def.no + "-" + stage.stage + tag + "\n제" + def.no + "장 " + def.title;
|
||||||
var body = new System.Text.StringBuilder();
|
var body = new System.Text.StringBuilder();
|
||||||
if (stage != null)
|
|
||||||
{
|
|
||||||
body.Append(stage.title);
|
body.Append(stage.title);
|
||||||
if (!string.IsNullOrEmpty(stage.subtitle)) body.Append(" · ").Append(stage.subtitle);
|
if (!string.IsNullOrEmpty(stage.place)) body.Append("\n<size=26>").Append(stage.place).Append(string.IsNullOrEmpty(stage.bgTheme) ? "" : " · " + stage.bgTheme).Append("</size>");
|
||||||
body.Append("\n\n").Append(EnemySummary(stage));
|
body.Append("\n\n").Append(EnemySummary(stage));
|
||||||
}
|
|
||||||
body.Append("\n\n<size=24>(임시 구성 · 전투 = 스테이지 ").Append(c.stageLink + 1).Append(" · 기획 확정 후 교체)</size>");
|
|
||||||
if (st == NodeState.Lock) body.Append("\n\n잠긴 스테이지 — 앞 스테이지를 먼저 깨세요");
|
if (st == NodeState.Lock) body.Append("\n\n잠긴 스테이지 — 앞 스테이지를 먼저 깨세요");
|
||||||
popupBody.text = body.ToString();
|
popupBody.text = body.ToString();
|
||||||
popupGo.interactable = st != NodeState.Lock;
|
popupGo.interactable = st != NodeState.Lock;
|
||||||
|
|
@ -450,14 +545,16 @@ namespace TKS
|
||||||
battle.StartStage(0);
|
battle.StartStage(0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
chapter = Mathf.Clamp(ch, 0, data.chapters.Length - 1);
|
chapter = Mathf.Clamp(ch, 0, cfg.chapters.Length - 1);
|
||||||
|
int si = StageIndexOf(chapter, node);
|
||||||
|
if (si < 0) return;
|
||||||
entryChapter = chapter;
|
entryChapter = chapter;
|
||||||
entryNode = node;
|
entryNode = node;
|
||||||
ClosePopup();
|
ClosePopup();
|
||||||
root.SetActive(false);
|
root.SetActive(false);
|
||||||
if (hud != null) hud.gameObject.SetActive(true);
|
if (hud != null) hud.gameObject.SetActive(true);
|
||||||
if (sling != null) sling.enabled = true;
|
if (sling != null) sling.enabled = true;
|
||||||
battle.StartStage(data.chapters[chapter].stageLink);
|
battle.StartStage(si);
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnBattleResult(bool win)
|
void OnBattleResult(bool win)
|
||||||
|
|
@ -466,27 +563,24 @@ namespace TKS
|
||||||
if (win && entryNode >= 0)
|
if (win && entryNode >= 0)
|
||||||
{
|
{
|
||||||
WorldProgress.MarkCleared(ch, entryNode);
|
WorldProgress.MarkCleared(ch, entryNode);
|
||||||
// 챕터 보스를 깨면 다음 챕터 맵으로
|
// 챕터 마지막 스테이지를 깨면 다음 챕터 맵으로
|
||||||
if (entryNode == data.chapters[ch].nodes - 1 && ch + 1 < data.chapters.Length) ch++;
|
if (entryNode == NodesIn(ch) - 1 && ch + 1 < cfg.chapters.Length) ch++;
|
||||||
}
|
}
|
||||||
entryChapter = entryNode = -1;
|
entryChapter = entryNode = -1;
|
||||||
Show(ch);
|
Show(ch);
|
||||||
}
|
}
|
||||||
|
|
||||||
BalanceConfig.StageDef StageFor(WorldChapter c)
|
/// <summary>적 수·병종 + 장수(보스 · 엘리트) 이름.</summary>
|
||||||
{
|
|
||||||
if (battle == null || battle.Config == null || battle.Config.stages.Length == 0) return null;
|
|
||||||
return battle.Config.stages[Mathf.Clamp(c.stageLink, 0, battle.Config.stages.Length - 1)];
|
|
||||||
}
|
|
||||||
|
|
||||||
static string EnemySummary(BalanceConfig.StageDef stage)
|
static string EnemySummary(BalanceConfig.StageDef stage)
|
||||||
{
|
{
|
||||||
var counts = new int[UnitTypeInfo.Count];
|
var counts = new int[UnitTypeInfo.Count];
|
||||||
string bossName = null;
|
var bosses = new List<string>();
|
||||||
|
var elites = new List<string>();
|
||||||
foreach (var e in stage.enemies)
|
foreach (var e in stage.enemies)
|
||||||
{
|
{
|
||||||
counts[(int)e.type]++;
|
counts[(int)e.type]++;
|
||||||
if (e.isBoss) bossName = e.name;
|
if (e.isBoss) bosses.Add(e.name);
|
||||||
|
else if (e.art.Contains("_boss_")) elites.Add(e.name);
|
||||||
}
|
}
|
||||||
var sb = new System.Text.StringBuilder("적 " + stage.enemies.Count + "기 · ");
|
var sb = new System.Text.StringBuilder("적 " + stage.enemies.Count + "기 · ");
|
||||||
bool first = true;
|
bool first = true;
|
||||||
|
|
@ -497,7 +591,8 @@ namespace TKS
|
||||||
sb.Append(UnitTypeInfo.Korean((UnitType)t)).Append(" ").Append(counts[t]);
|
sb.Append(UnitTypeInfo.Korean((UnitType)t)).Append(" ").Append(counts[t]);
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
if (bossName != null) sb.Append("\n보스 ").Append(bossName);
|
if (bosses.Count > 0) sb.Append("\n보스 ").Append(string.Join(" · ", bosses.ToArray()));
|
||||||
|
if (elites.Count > 0) sb.Append("\n장수 ").Append(string.Join(" · ", elites.ToArray()));
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.SceneManagement;
|
using UnityEngine.SceneManagement;
|
||||||
|
|
@ -508,13 +509,15 @@ namespace TKS.Tests
|
||||||
Assert.AreEqual(bm.AliveCount(Faction.Enemy), enemyArt, "스테이지 1 황건적은 전원 원화(hgj_*)로 표시");
|
Assert.AreEqual(bm.AliveCount(Faction.Enemy), enemyArt, "스테이지 1 황건적은 전원 원화(hgj_*)로 표시");
|
||||||
Assert.IsTrue(bm.Board.HasArt, "전장 배경 = Resources/Board/" + cfg.boardArt);
|
Assert.IsTrue(bm.Board.HasArt, "전장 배경 = Resources/Board/" + cfg.boardArt);
|
||||||
|
|
||||||
// 원화 2차 납품(2026-11-04): 동탁군 보·창·기·궁 4종 반영 · 여포(dz_boss_lvbu)는 보류 → 도형
|
// 원화 2차 납품(2026-11-04): 동탁군 보·창·기·궁 4종 반영 → 스테이지 표의 첫 동탁군 스테이지(2장)
|
||||||
bm.StartStage(1);
|
int dzStage = System.Array.FindIndex(cfg.stages, s => s.faction == "dz");
|
||||||
|
Assert.GreaterOrEqual(dzStage, 0, "동탁군 스테이지");
|
||||||
|
bm.StartStage(dzStage);
|
||||||
yield return null;
|
yield return null;
|
||||||
foreach (var e in bm.AliveUnits(Faction.Enemy))
|
foreach (var e in bm.AliveUnits(Faction.Enemy))
|
||||||
{
|
{
|
||||||
if (e.IsBoss) continue;
|
if (e.IsBoss) continue;
|
||||||
Assert.IsTrue(e.View.HasArt, "스테이지 2 동탁군 병종 4종은 원화(" + e.ArtId + ")");
|
Assert.IsTrue(e.View.HasArt, "동탁군 병종 4종은 원화(" + e.ArtId + ")");
|
||||||
}
|
}
|
||||||
bm.StartStage(0);
|
bm.StartStage(0);
|
||||||
yield return null;
|
yield return null;
|
||||||
|
|
@ -577,7 +580,7 @@ namespace TKS.Tests
|
||||||
Assert.AreEqual(BattlePhase.Setup, b.Phase, "월드맵 표시 중 = 전투 없음");
|
Assert.AreEqual(BattlePhase.Setup, b.Phase, "월드맵 표시 중 = 전투 없음");
|
||||||
Assert.AreEqual(0, b.Units.Count, "월드맵 표시 중 = 유닛 없음");
|
Assert.AreEqual(0, b.Units.Count, "월드맵 표시 중 = 유닛 없음");
|
||||||
Assert.IsNotNull(wm.MapSprite, "챕터 1 맵 그림");
|
Assert.IsNotNull(wm.MapSprite, "챕터 1 맵 그림");
|
||||||
Assert.AreEqual(wm.Data.chapters[0].nodes, wm.NodeCount, "노드 수 = world.json nodes");
|
Assert.AreEqual(wm.NodesIn(0), wm.NodeCount, "노드 수 = 스테이지 표 1장 스테이지 수");
|
||||||
Assert.AreEqual(NodeState.Open, wm.StateOf(0, 0), "1-1 = 도전 가능");
|
Assert.AreEqual(NodeState.Open, wm.StateOf(0, 0), "1-1 = 도전 가능");
|
||||||
Assert.AreEqual(NodeState.Lock, wm.StateOf(0, 1), "1-2 = 잠김");
|
Assert.AreEqual(NodeState.Lock, wm.StateOf(0, 1), "1-2 = 잠김");
|
||||||
Assert.IsFalse(wm.ChapterUnlocked(1), "2장은 1장 보스를 깨야 열림");
|
Assert.IsFalse(wm.ChapterUnlocked(1), "2장은 1장 보스를 깨야 열림");
|
||||||
|
|
@ -600,7 +603,8 @@ namespace TKS.Tests
|
||||||
yield return null;
|
yield return null;
|
||||||
Assert.IsFalse(wm.IsVisible, "출전 → 월드맵 닫힘");
|
Assert.IsFalse(wm.IsVisible, "출전 → 월드맵 닫힘");
|
||||||
Assert.AreEqual(BattlePhase.PlayerTurn, bm.Phase);
|
Assert.AreEqual(BattlePhase.PlayerTurn, bm.Phase);
|
||||||
var stage = cfg.stages[wm.Data.chapters[0].stageLink];
|
Assert.AreEqual(wm.StageIndexOf(0, 0), bm.StageIndex, "1-1 노드 = 스테이지 표 1-1 전투");
|
||||||
|
var stage = cfg.stages[wm.StageIndexOf(0, 0)];
|
||||||
Assert.AreEqual(stage.enemies.Count, bm.AliveCount(Faction.Enemy), "팝업 노드의 전투가 시작");
|
Assert.AreEqual(stage.enemies.Count, bm.AliveCount(Faction.Enemy), "팝업 노드의 전투가 시작");
|
||||||
|
|
||||||
bm.ForcePhase(BattlePhase.StageClear);
|
bm.ForcePhase(BattlePhase.StageClear);
|
||||||
|
|
@ -634,8 +638,6 @@ namespace TKS.Tests
|
||||||
Assert.AreEqual(d.mapSize[0], sp.rect.width, 0.5f, c.id + " 맵 가로 = world.json mapSize");
|
Assert.AreEqual(d.mapSize[0], sp.rect.width, 0.5f, c.id + " 맵 가로 = world.json mapSize");
|
||||||
Assert.AreEqual(d.mapSize[1], sp.rect.height, 0.5f, c.id + " 맵 세로");
|
Assert.AreEqual(d.mapSize[1], sp.rect.height, 0.5f, c.id + " 맵 세로");
|
||||||
Assert.GreaterOrEqual(c.roadX.Length, 2, c.id + " 길");
|
Assert.GreaterOrEqual(c.roadX.Length, 2, c.id + " 길");
|
||||||
foreach (var s in WorldData.Slots(c, c.nodes))
|
|
||||||
Assert.IsTrue(s.x >= 0f && s.x <= d.mapSize[0] && s.y >= 0f && s.y <= d.mapSize[1], c.id + " 노드가 맵 안");
|
|
||||||
}
|
}
|
||||||
foreach (var k in d.kit)
|
foreach (var k in d.kit)
|
||||||
Assert.IsNotNull(Resources.Load<Sprite>("ChapterMap/UI/" + k.name), "UI 부품 " + k.name);
|
Assert.IsNotNull(Resources.Load<Sprite>("ChapterMap/UI/" + k.name), "UI 부품 " + k.name);
|
||||||
|
|
@ -656,12 +658,15 @@ namespace TKS.Tests
|
||||||
foreach (var p in poses)
|
foreach (var p in poses)
|
||||||
Assert.IsTrue(a.Clips.ContainsKey(v + "_" + p), h.art + " " + v + "_" + p);
|
Assert.IsTrue(a.Clips.ContainsKey(v + "_" + p), h.art + " " + v + "_" + p);
|
||||||
}
|
}
|
||||||
|
// 병사 = 납품 세력(hgj·dz·cc)은 반드시 원화 · 장수(<세력>_boss_<병음>) = 납품된 것만 원화(나머지는 도형 대체 · StageData_MissingArt 테스트)
|
||||||
string[] delivered = { "hgj_", "dz_", "cc_" };
|
string[] delivered = { "hgj_", "dz_", "cc_" };
|
||||||
foreach (var s in cfg.stages)
|
foreach (var s in cfg.stages)
|
||||||
foreach (var e in s.enemies)
|
foreach (var e in s.enemies)
|
||||||
{
|
{
|
||||||
|
bool general = e.art.Contains("_boss_");
|
||||||
bool want = false;
|
bool want = false;
|
||||||
foreach (var f in delivered) want |= e.art.StartsWith(f);
|
foreach (var f in delivered) want |= e.art.StartsWith(f);
|
||||||
|
if (general) want = Resources.Load<TextAsset>("Units/" + e.art + "/meta") != null;
|
||||||
if (!want) continue;
|
if (!want) continue;
|
||||||
var a = UnitArt.Load(e.art);
|
var a = UnitArt.Load(e.art);
|
||||||
Assert.IsNotNull(a, "적 원화 " + e.art);
|
Assert.IsNotNull(a, "적 원화 " + e.art);
|
||||||
|
|
@ -684,5 +689,158 @@ namespace TKS.Tests
|
||||||
Assert.AreEqual("front_idle", u.View.Anim.Current, "아래를 보면 앞모습");
|
Assert.AreEqual("front_idle", u.View.Anim.Current, "아래를 보면 앞모습");
|
||||||
Assert.IsTrue(u.View.Anim.Renderer.flipX, "왼쪽 = 좌우 반전");
|
Assert.IsTrue(u.View.Anim.Renderer.flipX, "왼쪽 = 좌우 반전");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 스테이지 표(Assets/Data/Stages/*.csv · 145 스테이지 · 2026-11-05 #1086) ----------
|
||||||
|
|
||||||
|
static StageTable.Tables ReadStageTable()
|
||||||
|
{
|
||||||
|
var dir = Path.Combine(Application.dataPath, StageTable.Folder);
|
||||||
|
var texts = new string[StageTable.Files.Length];
|
||||||
|
for (int i = 0; i < texts.Length; i++) texts[i] = File.ReadAllText(Path.Combine(dir, StageTable.Files[i]), System.Text.Encoding.UTF8);
|
||||||
|
return StageTable.Parse(texts);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void StageTable_Csv_145Rows_BossEveryFifth_ValidUnitAndArtIds()
|
||||||
|
{
|
||||||
|
var t = ReadStageTable();
|
||||||
|
Assert.IsEmpty(t.errors, "형식 오류(병종 id · 세력 · 무장 id · 번호): " + string.Join(" / ", t.errors.ToArray()));
|
||||||
|
Assert.AreEqual(145, t.stages.Count, "오픈 스펙 = 챕터 1~10 · 145 스테이지(기획서 #1086 · D1 = A)");
|
||||||
|
var artPrefixes = new HashSet<string>();
|
||||||
|
foreach (var f in t.factions.Values) artPrefixes.Add(f.art);
|
||||||
|
foreach (var f in t.factions.Values) artPrefixes.Add(f.code);
|
||||||
|
var idRule = new System.Text.RegularExpressions.Regex("^([a-z]+)_boss_[a-z]+$");
|
||||||
|
foreach (var s in t.stages)
|
||||||
|
{
|
||||||
|
string at = s.chapter + "-" + s.stage;
|
||||||
|
Assert.AreEqual(s.stage % 5 == 0, s.kind == StageKind.Boss, at + " 보스 = 5의 배수 스테이지(기획서 §1 원칙 1)");
|
||||||
|
Assert.Greater(s.SoldierCount, 0, at + " 병사");
|
||||||
|
int bosses = s.generals.FindAll(x => x.boss).Count;
|
||||||
|
if (s.kind == StageKind.Boss) Assert.GreaterOrEqual(bosses, 1, at + " 보스 스테이지에 보스 무장");
|
||||||
|
if (s.kind == StageKind.Elite) Assert.GreaterOrEqual(s.generals.Count, 1, at + " 엘리트 스테이지에 무장");
|
||||||
|
if (s.kind == StageKind.Normal) Assert.AreEqual(0, s.generals.Count, at + " 일반 스테이지 = 무장 없음");
|
||||||
|
foreach (var g in s.generals)
|
||||||
|
{
|
||||||
|
var m = idRule.Match(g.id);
|
||||||
|
Assert.IsTrue(m.Success, at + " 무장 아트 id 규칙 <세력>_boss_<병음>: " + g.id);
|
||||||
|
Assert.IsTrue(artPrefixes.Contains(m.Groups[1].Value), at + " 무장 id 세력 접두: " + g.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void StageTable_ImportedConfig_MatchesCsv_FormationFitsBoard()
|
||||||
|
{
|
||||||
|
var c = Resources.Load<BalanceConfig>("BalanceConfig");
|
||||||
|
var t = ReadStageTable();
|
||||||
|
Assert.AreEqual(t.stages.Count, c.stages.Length, "CSV 를 고쳤으면 TKS/Import Stages 로 다시 임포트");
|
||||||
|
Assert.AreEqual(t.chapters.Count, c.chapters.Length);
|
||||||
|
float halfW = c.boardSize.x / 2f, halfH = c.boardSize.y / 2f;
|
||||||
|
float playerFront = float.MinValue;
|
||||||
|
foreach (var p in c.playerSpawnPoints) playerFront = Mathf.Max(playerFront, p.y);
|
||||||
|
for (int i = 0; i < c.stages.Length; i++)
|
||||||
|
{
|
||||||
|
var s = c.stages[i];
|
||||||
|
var row = t.stages.Find(x => x.chapter == s.chapter && x.stage == s.stage);
|
||||||
|
string at = s.chapter + "-" + s.stage;
|
||||||
|
Assert.IsNotNull(row, at + " CSV 행");
|
||||||
|
Assert.AreEqual(row.kind, s.kind, at);
|
||||||
|
Assert.AreEqual(row.title, s.title, at);
|
||||||
|
Assert.AreEqual(row.SoldierCount + row.generals.Count, s.enemies.Count, at + " 적 수 = 병사 + 무장");
|
||||||
|
var counts = new int[UnitTypeInfo.Count];
|
||||||
|
int bosses = 0;
|
||||||
|
foreach (var e in s.enemies)
|
||||||
|
{
|
||||||
|
if (!e.art.Contains("_boss_")) counts[(int)e.type]++;
|
||||||
|
if (e.isBoss) bosses++;
|
||||||
|
Assert.IsFalse(string.IsNullOrEmpty(e.art), at + " 아트 id");
|
||||||
|
float r = c.GetStats(e.type).radius * (e.isBoss ? c.bossRadiusMult : 1f);
|
||||||
|
Assert.IsTrue(Mathf.Abs(e.position.x) <= halfW - r && e.position.y <= halfH - r, at + " " + e.name + " 전장 안 " + e.position);
|
||||||
|
Assert.Greater(e.position.y, playerFront + 2f, at + " " + e.name + " 아군 출발선보다 앞(적 쪽)");
|
||||||
|
}
|
||||||
|
for (int k = 0; k < UnitTypeInfo.Count; k++) Assert.AreEqual(row.soldiers[k], counts[k], at + " " + UnitTypeInfo.Korean((UnitType)k) + " 수");
|
||||||
|
Assert.AreEqual(row.generals.FindAll(g => g.boss).Count, bosses, at + " 보스 수");
|
||||||
|
for (int a = 0; a < s.enemies.Count; a++)
|
||||||
|
for (int b = a + 1; b < s.enemies.Count; b++)
|
||||||
|
{
|
||||||
|
var ea = s.enemies[a];
|
||||||
|
var eb = s.enemies[b];
|
||||||
|
float ra = c.GetStats(ea.type).radius * (ea.isBoss ? c.bossRadiusMult : 1f);
|
||||||
|
float rb = c.GetStats(eb.type).radius * (eb.isBoss ? c.bossRadiusMult : 1f);
|
||||||
|
Assert.Greater(Vector2.Distance(ea.position, eb.position), ra + rb, at + " " + ea.name + "·" + eb.name + " 겹침");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator StageData_MissingArt_FallsBackToShapes()
|
||||||
|
{
|
||||||
|
// 아트가 아직 없는 무장(보스 신규 25 · 엘리트 25)·세력(원소·원술·형주 베리에이션) = 도형으로 정상 전투
|
||||||
|
int idx = -1;
|
||||||
|
for (int i = 0; i < cfg.stages.Length && idx < 0; i++)
|
||||||
|
foreach (var e in cfg.stages[i].enemies)
|
||||||
|
if (e.isBoss && UnitArt.Load(e.art) == null) { idx = i; break; }
|
||||||
|
Assert.GreaterOrEqual(idx, 0, "아트 없는 보스 스테이지");
|
||||||
|
bm.StartStage(idx);
|
||||||
|
yield return null;
|
||||||
|
Assert.AreEqual(BattlePhase.PlayerTurn, bm.Phase);
|
||||||
|
Assert.AreEqual(cfg.stages[idx].enemies.Count, bm.AliveCount(Faction.Enemy));
|
||||||
|
int shapes = 0;
|
||||||
|
foreach (var e in bm.AliveUnits(Faction.Enemy))
|
||||||
|
{
|
||||||
|
bool has = UnitArt.Load(e.ArtId) != null;
|
||||||
|
Assert.AreEqual(has, e.View.HasArt, e.DisplayName + " 아트 있으면 원화 · 없으면 도형");
|
||||||
|
if (!has) shapes++;
|
||||||
|
}
|
||||||
|
Assert.Greater(shapes, 0, "도형 대체 유닛");
|
||||||
|
bm.StartStage(0);
|
||||||
|
yield return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator WorldMap_ChaptersFromStageTable_AnchorSlotsBossNodesUnlock()
|
||||||
|
{
|
||||||
|
var wm = WorldMapScreen.Instance;
|
||||||
|
Assert.AreEqual(cfg.chapters.Length, wm.ChapterCount, "챕터 = chapters.csv");
|
||||||
|
int total = 0;
|
||||||
|
for (int ch = 0; ch < wm.ChapterCount; ch++)
|
||||||
|
{
|
||||||
|
var def = cfg.chapters[ch];
|
||||||
|
var stages = cfg.StagesOfChapter(def.no);
|
||||||
|
Assert.AreEqual(def.map, wm.MapOf(ch).map, "제" + def.no + "장 맵 = chapters.csv map");
|
||||||
|
Assert.IsNotNull(Resources.Load<Sprite>("ChapterMap/Maps/" + def.map), def.map + " 맵 그림");
|
||||||
|
bool fromAnchors;
|
||||||
|
var slots = wm.NodeSlots(ch, out fromAnchors);
|
||||||
|
Assert.IsTrue(fromAnchors, "제" + def.no + "장 노드 자리 = anchors slots_" + stages.Count);
|
||||||
|
Assert.AreEqual(stages.Count, slots.Length);
|
||||||
|
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 + " 노드가 맵 안");
|
||||||
|
wm.Show(ch);
|
||||||
|
yield return null;
|
||||||
|
Assert.AreEqual(stages.Count, wm.NodeCount, "제" + def.no + "장 노드 수 = 스테이지 수");
|
||||||
|
int bosses = 0;
|
||||||
|
foreach (var si in stages)
|
||||||
|
if (cfg.stages[si].kind == StageKind.Boss) bosses++;
|
||||||
|
Assert.AreEqual(bosses, wm.BossNodeCount, "제" + def.no + "장 보스 노드");
|
||||||
|
total += stages.Count;
|
||||||
|
}
|
||||||
|
Assert.AreEqual(cfg.stages.Length, total, "모든 스테이지가 어느 챕터 노드");
|
||||||
|
|
||||||
|
// 해금 = 앞 스테이지 클리어 · 챕터 마지막 노드를 깨면 다음 챕터 열림 · 마지막 노드 클리어 → 다음 챕터 맵으로
|
||||||
|
Assert.IsFalse(wm.ChapterUnlocked(1));
|
||||||
|
for (int n = 0; n < wm.NodesIn(0) - 1; n++) WorldProgress.MarkCleared(0, n);
|
||||||
|
Assert.AreEqual(NodeState.Open, wm.StateOf(0, wm.NodesIn(0) - 1));
|
||||||
|
Assert.IsFalse(wm.ChapterUnlocked(1), "보스 전 = 2장 잠김");
|
||||||
|
wm.EnterStage(0, wm.NodesIn(0) - 1);
|
||||||
|
yield return null;
|
||||||
|
Assert.AreEqual(wm.StageIndexOf(0, wm.NodesIn(0) - 1), bm.StageIndex);
|
||||||
|
Assert.AreEqual(StageKind.Boss, bm.Stage.kind, "1장 마지막 = 보스");
|
||||||
|
bm.ForcePhase(BattlePhase.StageClear);
|
||||||
|
bm.ContinueAfterResult();
|
||||||
|
yield return null;
|
||||||
|
Assert.IsTrue(wm.ChapterUnlocked(1), "1장 보스 클리어 → 2장 열림");
|
||||||
|
Assert.AreEqual(1, wm.Chapter, "다음 챕터 맵으로");
|
||||||
|
Assert.AreEqual(NodeState.Open, wm.StateOf(1, 0));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,8 @@ Unity.exe -projectPath <proj> -executeMethod TKS.EditorTools.ProjectSetup.AutoPl
|
||||||
|
|
||||||
## 코드 규칙
|
## 코드 규칙
|
||||||
|
|
||||||
- 수치·영웅·스테이지 SOT = `Assets/Resources/BalanceConfig.asset`(Inspector 조정 · 최초 시드만 `Assets/Scripts/Editor/BalanceDefaults.cs`). 코드·테스트에 수치를 직접 적지 않는다(C45).
|
- 수치·영웅 SOT = `Assets/Resources/BalanceConfig.asset`(Inspector 조정 · 최초 시드만 `Assets/Scripts/Editor/BalanceDefaults.cs`). 코드·테스트에 수치를 직접 적지 않는다(C45).
|
||||||
|
- **챕터·스테이지 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종 이상 전부 통과 확인.
|
- 규칙 해석 변경 = `Assets/Scripts/Runtime/ActionResolver.cs`(병종 4종) · `BattleManager.cs`(턴·승패·창병 대기) · `Combat.cs`(피해 공식·경로 질의). 변경 시 `Assets/Tests/PlayMode/BattleFlowTests.cs` 에 테스트 추가 후 22종 이상 전부 통과 확인.
|
||||||
- 콜라이더 질의 전 `Physics2D.SyncTransforms()` · 유닛 제거는 `SetActive(false)` 후 `Destroy` · 자식 렌더러가 있는 표시물은 GameObject 통째로 끈다.
|
- 콜라이더 질의 전 `Physics2D.SyncTransforms()` · 유닛 제거는 `SetActive(false)` 후 `Destroy` · 자식 렌더러가 있는 표시물은 GameObject 통째로 끈다.
|
||||||
- **궁병 규칙(PD 2026-11-04 · #1080)**: 사거리 = `slingMaxDistance`(얇은 원). 조준 방향 사거리 안의 적(가장자리 간격 기준) = **제자리 사격** · 사거리 밖 ~ ×(1 + `archerRangeRatio`)(붉은 도넛) 안의 적 = 들어올 만큼만(최대 `ArcherMoveAllow` = 슬링 × 비율) 다가가 사격 · 더 멀면 **목표점까지 이동만**(#1079). 반격 없음 · 목표점 명중 크리. **근접 전투**(돌진·반격·관통·견제·밀려 닿은 공격)에서 궁병은 근접 유닛 상대 주는 피해 × `archerMeleeDealtRatio`(0.5) · 받는 피해 × `archerMeleeTakenRatio`(1.5) — `Combat.Damage(…, melee)` · 원거리 사격은 패널티 없음.
|
- **궁병 규칙(PD 2026-11-04 · #1080)**: 사거리 = `slingMaxDistance`(얇은 원). 조준 방향 사거리 안의 적(가장자리 간격 기준) = **제자리 사격** · 사거리 밖 ~ ×(1 + `archerRangeRatio`)(붉은 도넛) 안의 적 = 들어올 만큼만(최대 `ArcherMoveAllow` = 슬링 × 비율) 다가가 사격 · 더 멀면 **목표점까지 이동만**(#1079). 반격 없음 · 목표점 명중 크리. **근접 전투**(돌진·반격·관통·견제·밀려 닿은 공격)에서 궁병은 근접 유닛 상대 주는 피해 × `archerMeleeDealtRatio`(0.5) · 받는 피해 × `archerMeleeTakenRatio`(1.5) — `Combat.Damage(…, melee)` · 원거리 사격은 패널티 없음.
|
||||||
|
|
@ -63,7 +64,8 @@ Unity.exe -projectPath <proj> -executeMethod TKS.EditorTools.ProjectSetup.AutoPl
|
||||||
## 구조 (README 와 동일)
|
## 구조 (README 와 동일)
|
||||||
|
|
||||||
- `Assets/Scripts/Runtime/` — `GameBootstrap`(씬 구성) · `BattleManager` · `ActionResolver` · `Combat` · `SlingshotInput`(조작·스크롤·줌) · `RangeIndicator`(사거리 원·궁병 도넛) · `CameraController`(넓은 전장 카메라) · `EnemyAI`(교전 범위) · `BattleHUD`(HUD·미니맵) · `Unit`/`UnitView` · `UnitArt`(아트 로더·`FrameAnimator`) · `Board` · `Fx` · `AutoPlayDriver`
|
- `Assets/Scripts/Runtime/` — `GameBootstrap`(씬 구성) · `BattleManager` · `ActionResolver` · `Combat` · `SlingshotInput`(조작·스크롤·줌) · `RangeIndicator`(사거리 원·궁병 도넛) · `CameraController`(넓은 전장 카메라) · `EnemyAI`(교전 범위) · `BattleHUD`(HUD·미니맵) · `Unit`/`UnitView` · `UnitArt`(아트 로더·`FrameAnimator`) · `Board` · `Fx` · `AutoPlayDriver`
|
||||||
- `Assets/Scripts/Editor/` — `ProjectSetup`(CLI 진입점·씬 생성) · `BalanceDefaults`(최초 시드) · `UnitArtImporter`(아트 임포트 설정)
|
- `Assets/Scripts/Editor/` — `ProjectSetup`(CLI 진입점·씬 생성) · `BalanceDefaults`(최초 시드) · `StageImporter`(스테이지 표 → 자산) · `UnitArtImporter`(아트 임포트 설정)
|
||||||
|
- `Assets/Data/Stages/` — 챕터·스테이지 표 CSV 4장(위 「코드 규칙」) · 해석·배치 = `Runtime/StageTable`
|
||||||
- `Assets/Resources/Units/<art>/` — 유닛 아트 프레임 + meta.json(위 절)
|
- `Assets/Resources/Units/<art>/` — 유닛 아트 프레임 + meta.json(위 절)
|
||||||
- `Assets/Tests/PlayMode/` — `BattleFlowTests`
|
- `Assets/Tests/PlayMode/` — `BattleFlowTests`
|
||||||
- `.claude/skills/` — Unity CLI 에이전트 스킬(`unity skill install claude-code --local` 산출물 · 재설치는 `unity skill refresh`)
|
- `.claude/skills/` — Unity CLI 에이전트 스킬(`unity skill install claude-code --local` 산출물 · 재설치는 `unity skill refresh`)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue