근접 3병종 특징 재정의(#1111 · PD 2026-11-10) — 창병 견제 제거 · 크게 밀치고 적게 밀림 / 기병 복귀 제거 · 관통 이동 / 보병 = 밀치고 튕기며 연쇄 공격

- 창병: BalanceConfig.spearPushRatio 1.5 신설(보병 1.0) · spearRecoilRatio 0.5 → 0.2 · 목표점 명중 → 대기(견제) 삭제 · HUD 견제 반경·대기 중 문구 삭제
- 기병: 목표점 명중 제자리 복귀 삭제(위진소요 추가 관통 유지) · PreviewCavalryReturn · 조준 복귀 영역(ShowReturnZones) 삭제 · 결과 문구 정리
- 적 AI: ai_weights cav_return · cav_stranded 삭제(기획 tables 원본과 같음) · 관통 점수 = 적중 수 × 10
- 보병: #1098 알까기 + 연쇄 그대로(목표점 명중 추가 이동 1회 유지 = PD 확인 대상)
- 튜토리얼 1-3 이름 「창병 밀치기」 · 팁 3행 새 정의 · CLAUDE.md 충돌 규칙 · 근접 3병종 줄
- 견제·복귀 스킬 15개 = 발동 안 함 · 재설계 PD 결정 대기 → ApplyOverwatch 등 내부는 남겨 둠 · SkillTests.Spear_OverwatchSkills_* [Ignore]
- 확인(고친 부분만): 재컴파일·재임포트 error 0 · 테스트 13 통과 · 1 건너뜀

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-11-10 17:25:47 +09:00
parent d48b33728a
commit b383e355c8
19 changed files with 75 additions and 178 deletions

View File

@ -2,13 +2,11 @@
dmg_player,1.0,플레이어 유닛에게 준 피해 1당 dmg_player,1.0,플레이어 유닛에게 준 피해 1당
kill_base,50,처치 1기당 기본 kill_base,50,처치 1기당 기본
kill_hp_ratio,30,처치한 유닛의 행동 전 HP 비율 × 값 kill_hp_ratio,30,처치한 유닛의 행동 전 HP 비율 × 값
dmg_own,-0.8,자기·아군(적 측) 피해 1당(반격·오사·견제·기믹 포함) dmg_own,-0.8,자기·아군(적 측) 피해 1당(반격·오사·기믹 포함)
gimmick_dest_bonus,30,밀림·튕김 목적지 기믹(함정·독늪·불·강·폭약 반경)에 플레이어 유닛이 들어가면 피해 환산 + 값(자기 편이면 −) gimmick_dest_bonus,30,밀림·튕김 목적지 기믹(함정·독늪·불·강·폭약 반경)에 플레이어 유닛이 들어가면 피해 환산 + 값(자기 편이면 −)
chain,20,보병 연쇄 교환 1건당 chain,20,보병 연쇄 교환 1건당
support_reach,15,창병이 민 아군(적 측)이 다음 턴 닿을 수 있는 플레이어 유닛 수 × 값 support_reach,15,창병이 민 아군(적 측)이 다음 턴 닿을 수 있는 플레이어 유닛 수 × 값
pierce_hit,10,기병 관통 적중 1기당 pierce_hit,10,기병 관통 적중 1기당
cav_return,15,기병 복귀 성공
cav_stranded,-20,기병 관통 후 복귀 실패(적진 잔류)
archer_cover,15,궁병 종료 위치가 목책 뒤(플레이어 근접 유닛과의 직선에 목책) archer_cover,15,궁병 종료 위치가 목책 뒤(플레이어 근접 유닛과의 직선에 목책)
high_ground,10,궁·책 종료 위치가 고지 high_ground,10,궁·책 종료 위치가 고지
cav_line,-15,궁·책 종료 위치가 플레이어 기병 관통선 위 cav_line,-15,궁·책 종료 위치가 플레이어 기병 관통선 위

1 key value desc_ko
2 dmg_player 1.0 플레이어 유닛에게 준 피해 1당
3 kill_base 50 처치 1기당 기본
4 kill_hp_ratio 30 처치한 유닛의 행동 전 HP 비율 × 값
5 dmg_own -0.8 자기·아군(적 측) 피해 1당(반격·오사·견제·기믹 포함) 자기·아군(적 측) 피해 1당(반격·오사·기믹 포함)
6 gimmick_dest_bonus 30 밀림·튕김 목적지 기믹(함정·독늪·불·강·폭약 반경)에 플레이어 유닛이 들어가면 피해 환산 + 값(자기 편이면 −)
7 chain 20 보병 연쇄 교환 1건당
8 support_reach 15 창병이 민 아군(적 측)이 다음 턴 닿을 수 있는 플레이어 유닛 수 × 값
9 pierce_hit 10 기병 관통 적중 1기당
cav_return 15 기병 복귀 성공
cav_stranded -20 기병 관통 후 복귀 실패(적진 잔류)
10 archer_cover 15 궁병 종료 위치가 목책 뒤(플레이어 근접 유닛과의 직선에 목책)
11 high_ground 10 궁·책 종료 위치가 고지
12 cav_line -15 궁·책 종료 위치가 플레이어 기병 관통선 위

View File

@ -1,4 +1,4 @@
stage,tip_ko stage,tip_ko
1-1,보병은 튕겨내고 창병은 밀쳐내고 물러난다 · 기병은 꿰뚫고 돌아온다 · 궁병은 목책 너머를 쏜다 · 책사는 뭉친 적을 태우고 덤불을 치운다 1-1,보병은 밀치고 튕겨나며 다른 적까지 친다 · 창병은 크게 밀쳐내고 조금만 밀린다 · 기병은 적을 꿰뚫고 지나간다 · 궁병은 목책 너머를 쏜다 · 책사는 뭉친 적을 태우고 덤불을 치운다
1-2,보병은 튕겨내고 창병은 밀쳐내고 물러난다 · 기병은 꿰뚫고 돌아온다 · 궁병은 목책 너머를 쏜다 · 책사는 뭉친 적을 태우고 덤불을 치운다 1-2,보병은 밀치고 튕겨나며 다른 적까지 친다 · 창병은 크게 밀쳐내고 조금만 밀린다 · 기병은 적을 꿰뚫고 지나간다 · 궁병은 목책 너머를 쏜다 · 책사는 뭉친 적을 태우고 덤불을 치운다
1-3,보병은 튕겨내고 창병은 밀쳐내고 물러난다 · 기병은 꿰뚫고 돌아온다 · 궁병은 목책 너머를 쏜다 · 책사는 뭉친 적을 태우고 덤불을 치운다 1-3,보병은 밀치고 튕겨나며 다른 적까지 친다 · 창병은 크게 밀쳐내고 조금만 밀린다 · 기병은 적을 꿰뚫고 지나간다 · 궁병은 목책 너머를 쏜다 · 책사는 뭉친 적을 태우고 덤불을 치운다

1 stage tip_ko
2 1-1 보병은 튕겨내고 창병은 밀쳐내고 물러난다 · 기병은 꿰뚫고 돌아온다 · 궁병은 목책 너머를 쏜다 · 책사는 뭉친 적을 태우고 덤불을 치운다 보병은 밀치고 튕겨나며 다른 적까지 친다 · 창병은 크게 밀쳐내고 조금만 밀린다 · 기병은 적을 꿰뚫고 지나간다 · 궁병은 목책 너머를 쏜다 · 책사는 뭉친 적을 태우고 덤불을 치운다
3 1-2 보병은 튕겨내고 창병은 밀쳐내고 물러난다 · 기병은 꿰뚫고 돌아온다 · 궁병은 목책 너머를 쏜다 · 책사는 뭉친 적을 태우고 덤불을 치운다 보병은 밀치고 튕겨나며 다른 적까지 친다 · 창병은 크게 밀쳐내고 조금만 밀린다 · 기병은 적을 꿰뚫고 지나간다 · 궁병은 목책 너머를 쏜다 · 책사는 뭉친 적을 태우고 덤불을 치운다
4 1-3 보병은 튕겨내고 창병은 밀쳐내고 물러난다 · 기병은 꿰뚫고 돌아온다 · 궁병은 목책 너머를 쏜다 · 책사는 뭉친 적을 태우고 덤불을 치운다 보병은 밀치고 튕겨나며 다른 적까지 친다 · 창병은 크게 밀쳐내고 조금만 밀린다 · 기병은 적을 꿰뚫고 지나간다 · 궁병은 목책 너머를 쏜다 · 책사는 뭉친 적을 태우고 덤불을 치운다

View File

@ -1,7 +1,7 @@
chapter,stage,title,type,place,faction,infantry,spear,cavalry,archer,strategist,generals,bg_theme,note chapter,stage,title,type,place,faction,infantry,spear,cavalry,archer,strategist,generals,bg_theme,note
1,1,탁현 외곽 — 황건 척후,normal,탁현,hgj,3,0,0,0,0,,마을 어귀,튜토리얼(조준·이동) 1,1,탁현 외곽 — 황건 척후,normal,탁현,hgj,3,0,0,0,0,,마을 어귀,튜토리얼(조준·이동)
1,2,누상촌 약탈대,normal,누상촌,hgj,3,1,0,0,0,,마을,튜토리얼(기병 관통) 1,2,누상촌 약탈대,normal,누상촌,hgj,3,1,0,0,0,,마을,튜토리얼(기병 관통)
1,3,대흥산 길목,normal,대흥산,hgj,3,2,0,0,0,,산길,튜토리얼(창병 견제) 1,3,대흥산 길목,normal,대흥산,hgj,3,2,0,0,0,,산길,튜토리얼(창병 밀치기)
1,4,대흥산 — 등무,elite,대흥산,hgj,3,2,0,1,0,hgj_boss_dengmao:spear:false,산기슭, 1,4,대흥산 — 등무,elite,대흥산,hgj,3,2,0,1,0,hgj_boss_dengmao:spear:false,산기슭,
1,5,대흥산 전투 — 정원지,boss,대흥산,hgj,4,2,0,1,0,hgj_boss_chengyuanzhi:infantry:true,산기슭 평지, 1,5,대흥산 전투 — 정원지,boss,대흥산,hgj,4,2,0,1,0,hgj_boss_chengyuanzhi:infantry:true,산기슭 평지,
1,6,청주 포위망,normal,청주,hgj,4,2,0,2,0,,성 밖 들판, 1,6,청주 포위망,normal,청주,hgj,4,2,0,2,0,,성 밖 들판,

1 chapter stage title type place faction infantry spear cavalry archer strategist generals bg_theme note
2 1 1 탁현 외곽 — 황건 척후 normal 탁현 hgj 3 0 0 0 0 마을 어귀 튜토리얼(조준·이동)
3 1 2 누상촌 약탈대 normal 누상촌 hgj 3 1 0 0 0 마을 튜토리얼(기병 관통)
4 1 3 대흥산 길목 normal 대흥산 hgj 3 2 0 0 0 산길 튜토리얼(창병 견제) 튜토리얼(창병 밀치기)
5 1 4 대흥산 — 등무 elite 대흥산 hgj 3 2 0 1 0 hgj_boss_dengmao:spear:false 산기슭
6 1 5 대흥산 전투 — 정원지 boss 대흥산 hgj 4 2 0 1 0 hgj_boss_chengyuanzhi:infantry:true 산기슭 평지
7 1 6 청주 포위망 normal 청주 hgj 4 2 0 2 0 성 밖 들판

View File

@ -26,7 +26,7 @@ MonoBehaviour:
\xD7 \uAC12" \xD7 \uAC12"
- key: dmg_own - key: dmg_own
value: -0.8 value: -0.8
desc: "\uC790\uAE30\xB7\uC544\uAD70(\uC801 \uCE21) \uD53C\uD574 1\uB2F9(\uBC18\uACA9\xB7\uC624\uC0AC\xB7\uACAC\uC81C\xB7\uAE30\uBBF9 desc: "\uC790\uAE30\xB7\uC544\uAD70(\uC801 \uCE21) \uD53C\uD574 1\uB2F9(\uBC18\uACA9\xB7\uC624\uC0AC\xB7\uAE30\uBBF9
\uD3EC\uD568)" \uD3EC\uD568)"
- key: gimmick_dest_bonus - key: gimmick_dest_bonus
value: 30 value: 30
@ -44,13 +44,6 @@ MonoBehaviour:
- key: pierce_hit - key: pierce_hit
value: 10 value: 10
desc: "\uAE30\uBCD1 \uAD00\uD1B5 \uC801\uC911 1\uAE30\uB2F9" desc: "\uAE30\uBCD1 \uAD00\uD1B5 \uC801\uC911 1\uAE30\uB2F9"
- key: cav_return
value: 15
desc: "\uAE30\uBCD1 \uBCF5\uADC0 \uC131\uACF5"
- key: cav_stranded
value: -20
desc: "\uAE30\uBCD1 \uAD00\uD1B5 \uD6C4 \uBCF5\uADC0 \uC2E4\uD328(\uC801\uC9C4
\uC794\uB958)"
- key: archer_cover - key: archer_cover
value: 15 value: 15
desc: "\uAD81\uBCD1 \uC885\uB8CC \uC704\uCE58\uAC00 \uBAA9\uCC45 \uB4A4(\uD50C\uB808\uC774\uC5B4 desc: "\uAD81\uBCD1 \uC885\uB8CC \uC704\uCE58\uAC00 \uBAA9\uCC45 \uB4A4(\uD50C\uB808\uC774\uC5B4

View File

@ -91,6 +91,8 @@ MonoBehaviour:
strategistAllyDamageRatio: 1 strategistAllyDamageRatio: 1
collisionPushRatio: 1 collisionPushRatio: 1
collisionSelfBounceRatio: 0.5 collisionSelfBounceRatio: 0.5
spearPushRatio: 1.5
spearRecoilRatio: 0.2
unitArtHeight: 1.5 unitArtHeight: 1.5
mountedArtMaxTop: 1.3 mountedArtMaxTop: 1.3
boardArt: battlefield_01 boardArt: battlefield_01
@ -290,7 +292,7 @@ MonoBehaviour:
place: "\uB300\uD765\uC0B0" place: "\uB300\uD765\uC0B0"
faction: hgj faction: hgj
bgTheme: "\uC0B0\uAE38" bgTheme: "\uC0B0\uAE38"
note: "\uD29C\uD1A0\uB9AC\uC5BC(\uCC3D\uBCD1 \uACAC\uC81C)" note: "\uD29C\uD1A0\uB9AC\uC5BC(\uCC3D\uBCD1 \uBC00\uCE58\uAE30)"
- title: "\uB300\uD765\uC0B0 \u2014 \uB4F1\uBB34" - title: "\uB300\uD765\uC0B0 \u2014 \uB4F1\uBB34"
subtitle: "184\uB144" subtitle: "184\uB144"
aiAccuracy: 0.5 aiAccuracy: 0.5

View File

@ -1044,7 +1044,7 @@
{ {
"stage": 3, "stage": 3,
"id": "1-3", "id": "1-3",
"type": "튜토리얼(창병 견제)", "type": "튜토리얼(창병 밀치기)",
"boss": false, "boss": false,
"place": "대흥산", "place": "대흥산",
"title": "대흥산 길목", "title": "대흥산 길목",

View File

@ -7752,21 +7752,27 @@ MonoBehaviour:
- stage: 1-1 - stage: 1-1
chapter: 1 chapter: 1
stageNo: 1 stageNo: 1
text: "\uBCF4\uBCD1\uC740 \uD295\uACA8\uB0B4\uACE0 \uCC3D\uBCD1\uC740 \uBC00\uCCD0\uB0B4\uACE0 text: "\uBCF4\uBCD1\uC740 \uBC00\uCE58\uACE0 \uD295\uACA8\uB098\uBA70 \uB2E4\uB978
\uBB3C\uB7EC\uB09C\uB2E4 \xB7 \uAE30\uBCD1\uC740 \uAFF0\uB6AB\uACE0 \uB3CC\uC544\uC628\uB2E4 \uC801\uAE4C\uC9C0 \uCE5C\uB2E4 \xB7 \uCC3D\uBCD1\uC740 \uD06C\uAC8C \uBC00\uCCD0\uB0B4\uACE0
\xB7 \uAD81\uBCD1\uC740 \uBAA9\uCC45 \uB108\uBA38\uB97C \uC3DC\uB2E4 \xB7 \uCC45\uC0AC\uB294 \uC870\uAE08\uB9CC \uBC00\uB9B0\uB2E4 \xB7 \uAE30\uBCD1\uC740 \uC801\uC744
\uBB49\uCE5C \uC801\uC744 \uD0DC\uC6B0\uACE0 \uB364\uBD88\uC744 \uCE58\uC6B4\uB2E4" \uAFF0\uB6AB\uACE0 \uC9C0\uB098\uAC04\uB2E4 \xB7 \uAD81\uBCD1\uC740 \uBAA9\uCC45
\uB108\uBA38\uB97C \uC3DC\uB2E4 \xB7 \uCC45\uC0AC\uB294 \uBB49\uCE5C \uC801\uC744
\uD0DC\uC6B0\uACE0 \uB364\uBD88\uC744 \uCE58\uC6B4\uB2E4"
- stage: 1-2 - stage: 1-2
chapter: 1 chapter: 1
stageNo: 2 stageNo: 2
text: "\uBCF4\uBCD1\uC740 \uD295\uACA8\uB0B4\uACE0 \uCC3D\uBCD1\uC740 \uBC00\uCCD0\uB0B4\uACE0 text: "\uBCF4\uBCD1\uC740 \uBC00\uCE58\uACE0 \uD295\uACA8\uB098\uBA70 \uB2E4\uB978
\uBB3C\uB7EC\uB09C\uB2E4 \xB7 \uAE30\uBCD1\uC740 \uAFF0\uB6AB\uACE0 \uB3CC\uC544\uC628\uB2E4 \uC801\uAE4C\uC9C0 \uCE5C\uB2E4 \xB7 \uCC3D\uBCD1\uC740 \uD06C\uAC8C \uBC00\uCCD0\uB0B4\uACE0
\xB7 \uAD81\uBCD1\uC740 \uBAA9\uCC45 \uB108\uBA38\uB97C \uC3DC\uB2E4 \xB7 \uCC45\uC0AC\uB294 \uC870\uAE08\uB9CC \uBC00\uB9B0\uB2E4 \xB7 \uAE30\uBCD1\uC740 \uC801\uC744
\uBB49\uCE5C \uC801\uC744 \uD0DC\uC6B0\uACE0 \uB364\uBD88\uC744 \uCE58\uC6B4\uB2E4" \uAFF0\uB6AB\uACE0 \uC9C0\uB098\uAC04\uB2E4 \xB7 \uAD81\uBCD1\uC740 \uBAA9\uCC45
\uB108\uBA38\uB97C \uC3DC\uB2E4 \xB7 \uCC45\uC0AC\uB294 \uBB49\uCE5C \uC801\uC744
\uD0DC\uC6B0\uACE0 \uB364\uBD88\uC744 \uCE58\uC6B4\uB2E4"
- stage: 1-3 - stage: 1-3
chapter: 1 chapter: 1
stageNo: 3 stageNo: 3
text: "\uBCF4\uBCD1\uC740 \uD295\uACA8\uB0B4\uACE0 \uCC3D\uBCD1\uC740 \uBC00\uCCD0\uB0B4\uACE0 text: "\uBCF4\uBCD1\uC740 \uBC00\uCE58\uACE0 \uD295\uACA8\uB098\uBA70 \uB2E4\uB978
\uBB3C\uB7EC\uB09C\uB2E4 \xB7 \uAE30\uBCD1\uC740 \uAFF0\uB6AB\uACE0 \uB3CC\uC544\uC628\uB2E4 \uC801\uAE4C\uC9C0 \uCE5C\uB2E4 \xB7 \uCC3D\uBCD1\uC740 \uD06C\uAC8C \uBC00\uCCD0\uB0B4\uACE0
\xB7 \uAD81\uBCD1\uC740 \uBAA9\uCC45 \uB108\uBA38\uB97C \uC3DC\uB2E4 \xB7 \uCC45\uC0AC\uB294 \uC870\uAE08\uB9CC \uBC00\uB9B0\uB2E4 \xB7 \uAE30\uBCD1\uC740 \uC801\uC744
\uBB49\uCE5C \uC801\uC744 \uD0DC\uC6B0\uACE0 \uB364\uBD88\uC744 \uCE58\uC6B4\uB2E4" \uAFF0\uB6AB\uACE0 \uC9C0\uB098\uAC04\uB2E4 \xB7 \uAD81\uBCD1\uC740 \uBAA9\uCC45
\uB108\uBA38\uB97C \uC3DC\uB2E4 \xB7 \uCC45\uC0AC\uB294 \uBB49\uCE5C \uC801\uC744
\uD0DC\uC6B0\uACE0 \uB364\uBD88\uC744 \uCE58\uC6B4\uB2E4"

View File

@ -54,7 +54,8 @@ namespace TKS.EditorTools
c.strategistAllyDamageRatio = 1f; // 광역 아군 피해 = 적과 동일(PD 확인 대상) c.strategistAllyDamageRatio = 1f; // 광역 아군 피해 = 적과 동일(PD 확인 대상)
c.collisionPushRatio = 1f; // PD 2026-11-04 #1080: 보병 알까기 밀어내기 = 남은 거리 그대로 c.collisionPushRatio = 1f; // PD 2026-11-04 #1080: 보병 알까기 밀어내기 = 남은 거리 그대로
c.collisionSelfBounceRatio = 0.5f; // PD 2026-11-04 #1081: 보병 자신도 반대로 튕김(남은 거리의 절반 · 손맛 조정 대상) c.collisionSelfBounceRatio = 0.5f; // PD 2026-11-04 #1081: 보병 자신도 반대로 튕김(남은 거리의 절반 · 손맛 조정 대상)
c.spearRecoilRatio = 0.5f; // PD 2026-11-06 #1098: 창병 밀치기 반작용(밀린 거리의 절반 · 함께 전진 폐기) c.spearPushRatio = 1.5f; // PD 2026-11-10: 창병 = 적을 크게 밀침(남은 거리 × 1.5 · 보병 1.0)
c.spearRecoilRatio = 0.2f; // PD 2026-11-10: 창병 자신은 적게 밀림(밀린 거리 × 0.2 = 남은 거리 × 0.3 · 보병 자기 튕김 0.5) · 옛 0.5(#1098)
c.unitArtHeight = 1.5f; c.unitArtHeight = 1.5f;
c.mountedArtMaxTop = 1.3f; // 기마 머리 끝 상한 = 표준 키 × 1.3(PD 2026-11-09) c.mountedArtMaxTop = 1.3f; // 기마 머리 끝 상한 = 표준 키 × 1.3(PD 2026-11-09)
c.boardArt = "battlefield_01"; // 원화 세션 1차 납품(2026-11-04) · Resources/Board/ c.boardArt = "battlefield_01"; // 원화 세션 1차 납품(2026-11-04) · Resources/Board/

View File

@ -17,7 +17,7 @@ namespace TKS
/// <summary>코드가 읽는 키(표에 없으면 임포트 오류).</summary> /// <summary>코드가 읽는 키(표에 없으면 임포트 오류).</summary>
public static readonly string[] Keys = public static readonly string[] Keys =
{ {
"dmg_player", "kill_base", "kill_hp_ratio", "dmg_own", "gimmick_dest_bonus", "chain", "support_reach", "pierce_hit", "cav_return", "cav_stranded", "dmg_player", "kill_base", "kill_hp_ratio", "dmg_own", "gimmick_dest_bonus", "chain", "support_reach", "pierce_hit",
"archer_cover", "high_ground", "cav_line", "splash_hit", "route_open", "zone_end", "banner", "skill_waste", "approach", "boss_accuracy", "mode_accuracy", "archer_cover", "high_ground", "cav_line", "splash_hit", "route_open", "zone_end", "banner", "skill_waste", "approach", "boss_accuracy", "mode_accuracy",
"noise_power", "top_n", "dirs", "dirs_many", "many_units", "power_1", "power_2", "power_3", "budget_ms", "boss_active", "active_max_chapter" "noise_power", "top_n", "dirs", "dirs_many", "many_units", "power_1", "power_2", "power_3", "budget_ms", "boss_active", "active_max_chapter"
}; };

View File

@ -312,8 +312,8 @@ namespace TKS
} }
if (hit.Count > 0) if (hit.Count > 0)
{ {
s += w["pierce_hit"] * hit.Count + (o.Returned ? w["cav_return"] : w["cav_stranded"]); s += w["pierce_hit"] * hit.Count; // 복귀 성공·실패 점수 = 기병 복귀 제거로 삭제(PD 2026-11-10)
parts.Add("관통 " + hit.Count + (o.Returned ? " · 복귀" : " · 잔류")); parts.Add("관통 " + hit.Count);
} }
} }

View File

@ -50,9 +50,10 @@ namespace TKS
/// 슬링샷 벡터(시작점 → 목표점)를 병종 규칙대로 이동·공격으로 해석한다 (PD 2026-11-04 #1080 충돌 규칙). /// 슬링샷 벡터(시작점 → 목표점)를 병종 규칙대로 이동·공격으로 해석한다 (PD 2026-11-04 #1080 충돌 규칙).
/// ① 보병: 경로 위 첫 유닛(아군·적군)에 부딪히면 알까기처럼 그 유닛을 남은 힘만큼 밀어내고 자신은 반대로 튕김 · 적이면 공격(반격 받음) · 목표점 명중 시 추가 이동 1회 · /// ① 보병: 경로 위 첫 유닛(아군·적군)에 부딪히면 알까기처럼 그 유닛을 남은 힘만큼 밀어내고 자신은 반대로 튕김 · 적이면 공격(반격 받음) · 목표점 명중 시 추가 이동 1회 ·
/// 적과 싸운 뒤 튕겨나는 도중 다른 적과 부딪히면 그 적과도 피해·반격 교환 + 다시 튕김(연쇄 1회 · 아군이면 밀기만 · PD 2026-11-06 #1098) /// 적과 싸운 뒤 튕겨나는 도중 다른 적과 부딪히면 그 적과도 피해·반격 교환 + 다시 튕김(연쇄 1회 · 아군이면 밀기만 · PD 2026-11-06 #1098)
/// ② 기병: 유닛을 무시하고 관통 · 경로 위 적은 모두 피해(옆으로 밀림) · 아군은 지나칠 거리가 안 나오면 그 아군 직전에 멈춤 · 목표점 명중 시 제자리 복귀 /// ② 기병 = 적을 관통해 이동: 유닛을 무시하고 관통 · 경로 위 적은 모두 피해(옆으로 밀림) · 아군은 지나칠 거리가 안 나오면 그 아군 직전에 멈춤 ·
/// ③ 창병: 첫 유닛에 부딪히면 그 유닛을 남은 거리만큼 밀치고 자신은 그 × spearRecoilRatio 만큼 반대로 밀림(반작용 · 함께 전진 폐기 · PD #1098) · /// 제자리 복귀 없음(PD 2026-11-10 「유저가 인지하기 어려움 → 제거 · 기병 특징 = 적을 관통해 이동」)
/// 적이면 피해(반격 받음) · 아군이면 밀기만 · 목표점 명중 시 대기(견제) /// ③ 창병 = 적을 크게 밀치고 자신은 적게 밀림: 첫 유닛에 부딪히면 그 유닛을 남은 거리 × spearPushRatio 만큼 밀치고 자신은 그 × spearRecoilRatio 만큼 반대로 밀림 ·
/// 적이면 피해(반격 받음) · 아군이면 밀기만 · 대기(견제) 없음(PD 2026-11-10 「유저가 인지하기 어려움 → 제거」)
/// ④ 궁병: 사거리 = 슬링샷 최대 거리 · 조준 방향 사거리 안의 적은 제자리에서 사격 · 사거리 밖 ~ ×(1+비율) 안이면 들어올 만큼만 다가가 사격 · /// ④ 궁병: 사거리 = 슬링샷 최대 거리 · 조준 방향 사거리 안의 적은 제자리에서 사격 · 사거리 밖 ~ ×(1+비율) 안이면 들어올 만큼만 다가가 사격 ·
/// 더 멀면 목표점까지 이동(쏘지 않음) · 사격 = 타겟 방향 화살 투사체 → 경로 위 첫 유닛(아군 포함)이 맞음 · 관통 없음 · 반격 없음 · /// 더 멀면 목표점까지 이동(쏘지 않음) · 사격 = 타겟 방향 화살 투사체 → 경로 위 첫 유닛(아군 포함)이 맞음 · 관통 없음 · 반격 없음 ·
/// 첫 피격 = 타겟 + 목표점 명중 시 크리티컬(PD 2026-11-05 #1087 ⑥) · 근접 전투는 피해 50 % · 피격 +50 %(Combat.Damage) /// 첫 피격 = 타겟 + 목표점 명중 시 크리티컬(PD 2026-11-05 #1087 ⑥) · 근접 전투는 피해 50 % · 피격 +50 %(Combat.Damage)
@ -294,7 +295,6 @@ namespace TKS
DashEnd(u); // 슬링샷 직진 끝 = 돌진 이펙트 소멸(밀기·반작용은 반응 · PD 2026-11-10) DashEnd(u); // 슬링샷 직진 끝 = 돌진 이펙트 소멸(밀기·반작용은 반응 · PD 2026-11-10)
float remaining = Mathf.Max(0f, dist - hitDist); float remaining = Mathf.Max(0f, dist - hitDist);
bool enemy = first.Faction != u.Faction; bool enemy = first.Faction != u.Faction;
bool isFinal = enemy && Combat.EnemyAtPoint(u, target, cfg.finalHitTolerance) == first;
if (enemy) if (enemy)
{ {
@ -306,8 +306,9 @@ namespace TKS
o.MovedOnly = true; o.MovedOnly = true;
} }
// 밀치기 거리 = 남은 거리 × 철척사모(push_mult) × 대상 거구(knockback_taken_pct) · 상대만 밀리고 창병은 그 × spearRecoilRatio 만큼 반대로(반작용 · PD #1098) // 밀치기 거리 = 남은 거리 × spearPushRatio(보병보다 크게 · PD 2026-11-10) × 철척사모(push_mult) × 대상 거구(knockback_taken_pct) ·
float push = remaining * SkillMods.PushMult(u, true) * SkillMods.KnockbackTakenMult(first, true); // 상대만 밀리고 창병은 그 × spearRecoilRatio 만큼 반대로(반작용 · 보병 자기 튕김보다 작게 · PD #1098 · 2026-11-10)
float push = remaining * cfg.spearPushRatio * SkillMods.PushMult(u, true) * SkillMods.KnockbackTakenMult(first, true);
if (first.IsAlive && push > 0.01f) if (first.IsAlive && push > 0.01f)
{ {
bool pushedDone = false; bool pushedDone = false;
@ -332,12 +333,7 @@ namespace TKS
if (!enemy) yield return PushedAllyAttack(first, o, u); if (!enemy) yield return PushedAllyAttack(first, o, u);
} }
if (enemy && u.IsAlive) SpearHitMore(u, first, o); if (enemy && u.IsAlive) SpearHitMore(u, first, o);
// 목표점 명중 → 대기(견제) = 제거(PD 2026-11-10) · 견제 스킬(청룡언월·후계·교변 …)은 재설계 결정 대기라 BattleManager.ApplyOverwatch 등은 남겨 둠(대기 유닛이 없어 발동 안 함)
if (isFinal && u.IsAlive)
{
u.SetOverwatch(true);
o.Overwatch = true;
}
} }
/// <summary>삼첨도(spear_hit_two): 밀며 전진 끝에 창병·밀린 적과 닿은 다른 적까지 합쳐 최대 n 기에 동시 피해(반격 없음).</summary> /// <summary>삼첨도(spear_hit_two): 밀며 전진 끝에 창병·밀린 적과 닿은 다른 적까지 합쳐 최대 n 기에 동시 피해(반격 없음).</summary>
@ -370,44 +366,16 @@ namespace TKS
battle.NotifyChanged(); battle.NotifyChanged();
} }
// ---------- 기병: 관통 ---------- // ---------- 기병: 관통(제자리 복귀 없음 · PD 2026-11-10) ----------
/// <summary> /// <param name="first">false = 위진소요의 추가 관통(다시 반복하지 않음 · exclude = 앞서 관통한 적 제외).</param>
/// 기병 조준 미리보기(PD 2026-11-10 「기병도 궁수와 같이 슬링샷 조작 시 공격 후 복귀 발동 지점을 알 수 있도록」) — 이 조준으로 놓으면
/// 공격 뒤 출발점으로 돌아오는가(CavalryRoutine 의 willReturn 과 같은 조건 · 첫 관통) · returnFrom = 복귀가 시작되는 자리(관통 끝).
/// 예약 중인 액티브의 관통 효과(위진소요 · 단기구주)는 발동 전이라 SkillMods 에 이미 걸린 것만 본다.
/// </summary>
public bool PreviewCavalryReturn(Unit u, Vector2 aim, out Vector2 returnFrom)
{
returnFrom = u != null ? u.Position : Vector2.zero;
if (u == null || u.Type != UnitType.Cavalry || u.BonusMoveAvailable) return false;
Vector2 origin = u.Position;
Vector2 target = ReachPoint(u, aim);
Vector2 delta = target - origin;
float dist = delta.magnitude;
if (dist < 0.001f) return false;
Vector2 dir = delta / dist;
float travel = Combat.FreeTravel(u, origin, dir, dist, false, false);
travel = StopBeforeUnclearedAllies(u, origin, dir, travel);
float gDist;
Vector2 gNormal;
var gim = BlockAhead(u, origin, dir, dist, travel + 0.05f, out gDist, out gNormal);
var enemies = Combat.EnemiesOnPath(u, origin, dir, travel, new List<float>());
Unit finalHit = Combat.EnemyAtPoint(u, target, cfg.finalHitTolerance);
bool hitFinal = finalHit != null && enemies.Contains(finalHit);
int repeat = hitFinal && gim == null ? SkillMods.Count(u, SkillKeys.PierceRepeat, 0) : 0;
returnFrom = origin + dir * travel;
return gim == null && repeat <= 0 && (hitFinal || (enemies.Count > 0 && SkillMods.Has(u, SkillKeys.PierceReturnOnMiss)));
}
/// <param name="first">false = 위진소요의 추가 관통(다시 반복·복귀하지 않음 · exclude = 앞서 관통한 적 제외).</param>
IEnumerator CavalryRoutine(Unit u, Vector2 origin, Vector2 dir, float dist, Vector2 target, bool moveOnly, ActionOutcome o, bool first, List<Unit> exclude = null) IEnumerator CavalryRoutine(Unit u, Vector2 origin, Vector2 dir, float dist, Vector2 target, bool moveOnly, ActionOutcome o, bool first, List<Unit> exclude = null)
{ {
float travel = Combat.FreeTravel(u, origin, dir, dist, false, false); // 유닛은 무시 · 벽·장애물 기믹만 float travel = Combat.FreeTravel(u, origin, dir, dist, false, false); // 유닛은 무시 · 벽·장애물 기믹만
travel = StopBeforeUnclearedAllies(u, origin, dir, travel); // 지나칠 수 없는 아군 직전에서 멈춤 travel = StopBeforeUnclearedAllies(u, origin, dir, travel); // 지나칠 수 없는 아군 직전에서 멈춤
float gDist; float gDist;
Vector2 gNormal; Vector2 gNormal;
var gim = BlockAhead(u, origin, dir, dist, travel + 0.05f, out gDist, out gNormal); // 장애물 = 관통 불가 → 거기서 정지 · 복귀 없음(PD #1097) var gim = BlockAhead(u, origin, dir, dist, travel + 0.05f, out gDist, out gNormal); // 장애물 = 관통 불가 → 거기서 정지(PD #1097)
var dists = new List<float>(); var dists = new List<float>();
var enemies = moveOnly ? new List<Unit>() : Combat.EnemiesOnPath(u, origin, dir, travel, dists); var enemies = moveOnly ? new List<Unit>() : Combat.EnemiesOnPath(u, origin, dir, travel, dists);
if (exclude != null) if (exclude != null)
@ -421,12 +389,11 @@ namespace TKS
} }
Unit finalHit = moveOnly ? null : Combat.EnemyAtPoint(u, target, cfg.finalHitTolerance); Unit finalHit = moveOnly ? null : Combat.EnemyAtPoint(u, target, cfg.finalHitTolerance);
bool hitFinal = first && finalHit != null && enemies.Contains(finalHit); bool hitFinal = first && finalHit != null && enemies.Contains(finalHit);
// 단기구주: 빗나가도(관통한 적이 있으면) 출발점 복귀 · 위진소요: 명중 시 복귀 대신 같은 방향 한 번 더 관통 // 위진소요: 목표점 명중 시 같은 방향 한 번 더 관통 · 제자리 복귀(옛 목표점 명중 · 단기구주 빗나가도 복귀) = 제거(PD 2026-11-10)
int repeat = hitFinal && gim == null ? SkillMods.Count(u, SkillKeys.PierceRepeat, 0) : 0; int repeat = hitFinal && gim == null ? SkillMods.Count(u, SkillKeys.PierceRepeat, 0) : 0;
bool willReturn = gim == null && first && repeat <= 0 && (hitFinal || (enemies.Count > 0 && SkillMods.Has(u, SkillKeys.PierceReturnOnMiss)));
Vector2 end = origin + dir * travel; Vector2 end = origin + dir * travel;
if (!willReturn && repeat <= 0) end = BackOffFromEnemies(u, origin, dir, travel); if (repeat <= 0) end = BackOffFromEnemies(u, origin, dir, travel);
if (enemies.Count == 0) if (enemies.Count == 0)
{ {
@ -452,7 +419,7 @@ namespace TKS
}); });
u.Position = end; u.Position = end;
o.Path.Add(end); o.Path.Add(end);
DashEnd(u); // 관통 돌격 끝 = 돌진 이펙트 소멸(복귀 때 다시 재생 · PD 2026-11-10) DashEnd(u); // 관통 돌격 끝 = 돌진 이펙트 소멸(PD 2026-11-10)
for (; idx < enemies.Count; idx++) for (; idx < enemies.Count; idx++)
{ {
if (dists[idx] <= travel + 0.01f) if (dists[idx] <= travel + 0.01f)
@ -496,17 +463,6 @@ namespace TKS
DashBegin(u, dir); // 추가 관통 = 돌진 이펙트 다시 DashBegin(u, dir); // 추가 관통 = 돌진 이펙트 다시
yield return CavalryRoutine(u, from, dir, dist, from + dir * dist, false, o, false, hit); yield return CavalryRoutine(u, from, dir, dist, from + dir * dist, false, o, false, hit);
} }
yield break;
}
if (willReturn && u.IsAlive)
{
yield return Fx.Wait(0.1f);
DashBegin(u, origin - u.Position); // 복귀 = 복귀 방향으로 돌진 이펙트 다시 재생 → 도착하면 소멸(PD 2026-11-10)
if (u.View != null) u.View.TurnAround(origin - u.Position); // 복귀 = 돌아서서 감(그림 좌우 반전 · PD 2026-11-10 · 도착 뒤에도 마지막 이동 방향 PD #1082)
yield return Move(u, origin, o, false);
DashEnd(u);
o.Returned = true;
} }
} }

View File

@ -27,7 +27,7 @@ namespace TKS
public int def; public int def;
[Tooltip("슬링샷으로 지정할 수 있는 최대 거리(월드 단위)")] [Tooltip("슬링샷으로 지정할 수 있는 최대 거리(월드 단위)")]
public float slingMaxDistance; public float slingMaxDistance;
[Tooltip("창병 대기(견제) 반경 · 궁병은 쓰지 않음(사거리 = slingMaxDistance)")] [Tooltip("옛 창병 대기(견제) 반경 · 견제 제거(PD 2026-11-10)로 지금은 쓰지 않음 · 궁병도 쓰지 않음(사거리 = slingMaxDistance)")]
public float attackRange; public float attackRange;
public float radius; public float radius;
} }
@ -146,8 +146,10 @@ namespace TKS
public float collisionPushRatio = 1f; public float collisionPushRatio = 1f;
[Tooltip("보병 알까기: 부딪힌 보병 자신이 반대 방향으로 튕겨나는 거리 = 충돌 시 남은 슬링샷 거리 × 비율 (PD #1081 「자신도 튕겨나야」)")] [Tooltip("보병 알까기: 부딪힌 보병 자신이 반대 방향으로 튕겨나는 거리 = 충돌 시 남은 슬링샷 거리 × 비율 (PD #1081 「자신도 튕겨나야」)")]
public float collisionSelfBounceRatio = 0.5f; public float collisionSelfBounceRatio = 0.5f;
[Tooltip("창병 밀치기 반작용(PD 2026-11-06 #1098): 상대가 밀리는 거리(남은 거리 × 밀기 배율) × 비율만큼 창병이 반대로 밀린다(벽·유닛에 닿으면 정지 · 피해 없음) · 「밀며 함께 전진」 폐기")] [Tooltip("창병 밀치기(PD 2026-11-10 「창병 = 적을 크게 밀치지만 자신은 적게 밀린다」): 상대가 밀리는 거리 = 충돌 시 남은 슬링샷 거리 × 비율 × 밀기 배율(철척사모) · 보병(collisionPushRatio)보다 크게")]
public float spearRecoilRatio = 0.5f; public float spearPushRatio = 1.5f;
[Tooltip("창병 밀치기 반작용(PD 2026-11-06 #1098 · 2026-11-10 「자신은 적게 밀린다」): 상대가 밀린 거리 × 비율만큼 창병이 반대로 밀린다(벽·유닛에 닿으면 정지 · 피해 없음) · 보병 자기 튕김(collisionSelfBounceRatio)보다 작게")]
public float spearRecoilRatio = 0.2f;
[Header("표시")] [Header("표시")]
[Tooltip("유닛 아트(스프라이트) 세로 크기 · 월드 단위 · 장수는 반지름 배율만큼 커진다")] [Tooltip("유닛 아트(스프라이트) 세로 크기 · 월드 단위 · 장수는 반지름 배율만큼 커진다")]

View File

@ -329,7 +329,6 @@ namespace TKS
defText.text = u.Def.ToString(); defText.text = u.Def.ToString();
string extra = "슬링 " + SkillMods.SlingMax(u).ToString("0.#"); string extra = "슬링 " + SkillMods.SlingMax(u).ToString("0.#");
if (UnitTypeInfo.IsRanged(u.Type)) extra = "사거리 " + SkillMods.Range(u).ToString("0.#"); if (UnitTypeInfo.IsRanged(u.Type)) extra = "사거리 " + SkillMods.Range(u).ToString("0.#");
else if (u.Type == UnitType.Spear) extra = "견제 " + SkillMods.OverwatchRadius(u).ToString("0.#");
if (u.Type == UnitType.Strategist) extra = "광역 " + SkillMods.SplashRadius(u, battle.Config).ToString("0.#"); if (u.Type == UnitType.Strategist) extra = "광역 " + SkillMods.SplashRadius(u, battle.Config).ToString("0.#");
extraText.text = extra; extraText.text = extra;
string state = u.Faction == Faction.Player ? "아군" : "적군"; string state = u.Faction == Faction.Player ? "아군" : "적군";
@ -338,7 +337,6 @@ namespace TKS
if (u.BonusMoveAvailable) state = "추가 이동"; if (u.BonusMoveAvailable) state = "추가 이동";
else if (u.ActedThisTurn) state = "행동 완료"; else if (u.ActedThisTurn) state = "행동 완료";
} }
if (u.Overwatch) state += " · 대기 중";
stateText.text = state; stateText.text = state;
} }
@ -582,14 +580,12 @@ namespace TKS
string line3 = "슬링 " + SkillMods.SlingMax(u).ToString("0.#"); string line3 = "슬링 " + SkillMods.SlingMax(u).ToString("0.#");
if (UnitTypeInfo.IsRanged(u.Type)) line3 += " 사거리 " + SkillMods.Range(u).ToString("0.#") + " (접근 +" + u.ArcherMoveAllow.ToString("0.#") + ")"; if (UnitTypeInfo.IsRanged(u.Type)) line3 += " 사거리 " + SkillMods.Range(u).ToString("0.#") + " (접근 +" + u.ArcherMoveAllow.ToString("0.#") + ")";
if (u.Type == UnitType.Strategist) line3 += " 광역 " + SkillMods.SplashRadius(u, battle.Config).ToString("0.#"); if (u.Type == UnitType.Strategist) line3 += " 광역 " + SkillMods.SplashRadius(u, battle.Config).ToString("0.#");
if (u.Type == UnitType.Spear) line3 += " 견제 반경 " + SkillMods.OverwatchRadius(u).ToString("0.#");
string state = ""; string state = "";
if (u.Faction == Faction.Player) if (u.Faction == Faction.Player)
{ {
if (u.BonusMoveAvailable) state = " [추가 이동]"; if (u.BonusMoveAvailable) state = " [추가 이동]";
else if (u.ActedThisTurn) state = " [행동 완료]"; else if (u.ActedThisTurn) state = " [행동 완료]";
} }
if (u.Overwatch) state += " [대기 중]";
unitText.text = line1 + "\n" + line2 + "\n" + line3 + state; unitText.text = line1 + "\n" + line2 + "\n" + line3 + state;
} }
else else

View File

@ -687,8 +687,6 @@ namespace TKS
} }
foreach (var n in o.Notes) s += " · " + n; foreach (var n in o.Notes) s += " · " + n;
if (o.BonusMove) s += " · 추가 이동!"; if (o.BonusMove) s += " · 추가 이동!";
if (o.Returned) s += " · 제자리 복귀";
if (o.Overwatch) s += " · 대기(견제) 발동";
return s; return s;
} }
} }

View File

@ -57,24 +57,6 @@ namespace TKS
{ {
get { return marks; } get { return marks; }
} }
/// <summary>
/// 기병 복귀 영역(PD 2026-11-10 「기병도 궁수와 같이 공격 후 복귀 발동 지점을 알 수 있도록」 → 「궁병처럼 채워진 영역 형태로」):
/// 슬링샷 목표점을 이 원 안에 놓으면 공격 뒤 출발점으로 돌아온다 = 적마다 (유닛 원 반지름 + 최종 명중 허용 거리) 원 ·
/// 그 적을 노렸을 때 복귀 조건(ActionResolver.PreviewCavalryReturn)이 참이고 슬링샷 거리 안인 적만 · 지금 노리는 원은 진하게.
/// 색 = IB 하늘색(궁병 사거리 도넛과 같은 채운 원 · 바닥 층).
/// </summary>
static readonly Color ReturnZoneColor = new Color(0.468f, 0.866f, 0.897f, 0.3f);
static readonly Color ReturnZoneAimColor = new Color(0.468f, 0.866f, 0.897f, 0.55f);
/// <summary>유닛 위(11 · 속성 방울 12 아래) — 원 크기가 적 발판과 거의 같아 바닥 층이면 적 그림에 가려 안 보임(2026-11-10 실측).</summary>
const int ReturnZoneOrder = 11;
readonly List<SpriteRenderer> returnZones = new List<SpriteRenderer>();
int returnZoneCount;
public int ReturnZoneCount
{
get { return returnZoneCount; }
}
public bool AimArrowVisible public bool AimArrowVisible
{ {
get { return ibArrow != null && ibArrow.ArrowVisible; } get { return ibArrow != null && ibArrow.ArrowVisible; }
@ -144,34 +126,6 @@ namespace TKS
marks = ago.AddComponent<IbUnitMarks>(); marks = ago.AddComponent<IbUnitMarks>();
} }
/// <summary>기병 복귀 영역 갱신 — u = 조준 중(또는 선택된) 아군 기병 · aim = 지금 목표점(없으면 null) · 그 밖 = 모두 숨김.</summary>
void ShowReturnZones(Unit u, Vector2? aim)
{
int n = 0;
if (u != null && u.IsAlive && u.Type == UnitType.Cavalry && u.Faction == Faction.Player && !u.BonusMoveAvailable && battle.CanAct(u))
{
float tol = battle.Config.finalHitTolerance;
float sling = SkillMods.SlingMax(u);
foreach (var e in battle.AliveUnits(Faction.Enemy))
{
float r = e.Radius + tol;
if (Vector2.Distance(e.Position, u.Position) - r > sling) continue;
Vector2 from;
if (!battle.Resolver.PreviewCavalryReturn(u, e.Position, out from)) continue;
if (n >= returnZones.Count)
returnZones.Add(ProceduralSprites.NewSprite(transform, "ReturnZone", ProceduralSprites.Circle, ReturnZoneColor, 1f, ReturnZoneOrder));
var z = returnZones[n++];
z.transform.position = new Vector3(e.Position.x, e.Position.y, 0f);
z.transform.localScale = new Vector3(r * 2f, r * 2f, 1f);
z.color = aim.HasValue && Vector2.Distance(aim.Value, e.Position) <= r ? ReturnZoneAimColor : ReturnZoneColor;
if (!z.gameObject.activeSelf) z.gameObject.SetActive(true);
}
}
for (int i = n; i < returnZones.Count; i++)
if (returnZones[i].gameObject.activeSelf) returnZones[i].gameObject.SetActive(false);
returnZoneCount = n;
}
LineRenderer MakeLine(string name, float width, Color color, int order) LineRenderer MakeLine(string name, float width, Color color, int order)
{ {
var go = new GameObject(name); var go = new GameObject(name);
@ -377,7 +331,6 @@ namespace TKS
range.Show(u); range.Show(u);
if (u != null && UnitTypeInfo.IsRanged(u.Type)) range.SetArcherReachable(battle.AnyEnemyWithinArcherReach(u)); if (u != null && UnitTypeInfo.IsRanged(u.Type)) range.SetArcherReachable(battle.AnyEnemyWithinArcherReach(u));
} }
ShowReturnZones(u, null); // 기병 = 궁병 사거리 도넛처럼 선택하면 복귀 영역
if (marks != null) marks.ShowTurn(u != null && u.Faction == Faction.Player && battle.CanAct(u) ? u : null); // IB ShowUserTurnMark if (marks != null) marks.ShowTurn(u != null && u.Faction == Faction.Player && battle.CanAct(u) ? u : null); // IB ShowUserTurnMark
} }
@ -413,8 +366,6 @@ namespace TKS
targetRing.color = finalHit != null ? TargetHitColor : TargetFreeColor; targetRing.color = finalHit != null ? TargetHitColor : TargetFreeColor;
targetRing.transform.localScale = finalHit != null ? new Vector3(0.9f, 0.9f, 1f) : new Vector3(0.7f, 0.7f, 1f); targetRing.transform.localScale = finalHit != null ? new Vector3(0.9f, 0.9f, 1f) : new Vector3(0.7f, 0.7f, 1f);
ShowReturnZones(u, aim); // 기병 복귀 영역(지금 노리는 원 진하게)
ClearHighlights(); ClearHighlights();
var shot = battle.Resolver.PreviewShot(u, aim, moveOnly); var shot = battle.Resolver.PreviewShot(u, aim, moveOnly);
var gl = battle.Gimmicks; var gl = battle.Gimmicks;

View File

@ -168,7 +168,7 @@ namespace TKS.Tests
// ---------- 기병 ---------- // ---------- 기병 ----------
[UnityTest] [UnityTest]
public IEnumerator Cavalry_PiercesAllOnPath_AndReturnsOnFinalHit() public IEnumerator Cavalry_PiercesAllOnPath_NoReturnOnFinalHit()
{ {
yield return Fresh(); yield return Fresh();
var cav = bm.SpawnUnit(UnitType.Cavalry, Faction.Player, new Vector2(0f, -4f), "기병"); var cav = bm.SpawnUnit(UnitType.Cavalry, Faction.Player, new Vector2(0f, -4f), "기병");
@ -178,8 +178,9 @@ namespace TKS.Tests
yield return bm.ResolveForTest(cav, e2.Position, false, o); yield return bm.ResolveForTest(cav, e2.Position, false, o);
Assert.Less(e1.Hp, e1.MaxHp, "경로 위 첫 적 관통 피해"); Assert.Less(e1.Hp, e1.MaxHp, "경로 위 첫 적 관통 피해");
Assert.Less(e2.Hp, e2.MaxHp, "경로 위 둘째 적 관통 피해"); Assert.Less(e2.Hp, e2.MaxHp, "경로 위 둘째 적 관통 피해");
Assert.IsTrue(o.Returned, "최종 목표점 명중 → 제자리 복귀"); Assert.IsFalse(o.Returned, "목표점 명중해도 제자리 복귀 없음(PD 2026-11-10 제거)");
Assert.Less(Vector2.Distance(cav.Position, new Vector2(0f, -4f)), 0.05f); Assert.Greater(cav.Position.y, e1.Position.y, "첫 적을 꿰뚫고 지나가 그 너머에 남음");
Assert.GreaterOrEqual(Vector2.Distance(cav.Position, e2.Position), cav.Radius + e2.Radius - 0.05f, "적과 겹치지 않음");
} }
[UnityTest] [UnityTest]
@ -577,7 +578,7 @@ namespace TKS.Tests
// ---------- 창병 ---------- // ---------- 창병 ----------
[UnityTest] [UnityTest]
public IEnumerator Spear_PlowsEnemyForward_OverwatchOnFinalHit_DamagesMovingEnemy() public IEnumerator Spear_PushesEnemyFar_RecoilsLittle_NoOverwatch()
{ {
yield return Fresh(); yield return Fresh();
var spear = bm.SpawnUnit(UnitType.Spear, Faction.Player, new Vector2(0f, -3f), "창병"); var spear = bm.SpawnUnit(UnitType.Spear, Faction.Player, new Vector2(0f, -3f), "창병");
@ -587,21 +588,11 @@ namespace TKS.Tests
Assert.Contains(foe, o.Attacked); Assert.Contains(foe, o.Attacked);
Assert.Less(foe.Hp, foe.MaxHp, "부딪힌 적에게 피해"); Assert.Less(foe.Hp, foe.MaxHp, "부딪힌 적에게 피해");
Assert.Contains(foe, o.Pushed, "부딪힌 상대를 밀친다"); Assert.Contains(foe, o.Pushed, "부딪힌 상대를 밀친다");
Assert.Greater(foe.Position.y, 0.5f, "적이 남은 거리만큼 앞으로 밀림");
float remaining = CastContact(spear, foe); // 당긴 3 − 접촉까지(3 − 접촉 거리) float remaining = CastContact(spear, foe); // 당긴 3 − 접촉까지(3 − 접촉 거리)
Assert.AreEqual(-CastContact(spear, foe) - remaining * cfg.spearRecoilRatio, spear.Position.y, 0.1f, "창병 = 밀친 거리 × spearRecoilRatio 만큼 반대로(PD #1098 · 함께 전진 폐기)"); float push = remaining * cfg.spearPushRatio; // PD 2026-11-10 「적을 크게 밀친다」
Assert.IsTrue(o.Overwatch && spear.Overwatch, "최종 목표점 명중 → 대기(견제)"); Assert.AreEqual(push, foe.Position.y, 0.1f, "적 = 남은 거리 × spearPushRatio 만큼 앞으로 밀림");
Assert.AreEqual(-CastContact(spear, foe) - push * cfg.spearRecoilRatio, spear.Position.y, 0.1f, "창병 = 밀친 거리 × spearRecoilRatio 만큼만 반대로(PD 2026-11-10 「자신은 적게 밀린다」)");
int hp = foe.Hp; Assert.IsFalse(o.Overwatch || spear.Overwatch, "목표점 명중해도 대기(견제) 없음(PD 2026-11-10 제거)");
var path = new List<Vector2> { foe.Position, foe.Position + new Vector2(1.5f, 0f) };
yield return bm.ApplyOverwatch(foe, path);
Assert.Less(foe.Hp, hp, "반경 안에서 움직인 적에게 피해");
// 반경 밖에서만 움직인 적은 피해 없음
var far = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(5f, 6f), "먼적");
int farHp = far.Hp;
yield return bm.ApplyOverwatch(far, new List<Vector2> { far.Position, far.Position + new Vector2(-1f, 0f) });
Assert.AreEqual(farHp, far.Hp);
} }
[UnityTest] [UnityTest]
@ -620,7 +611,7 @@ namespace TKS.Tests
Assert.LessOrEqual(Gap(ally, foe), Combat.ContactTolerance + 0.05f, "밀린 아군이 적과 닿음"); Assert.LessOrEqual(Gap(ally, foe), Combat.ContactTolerance + 0.05f, "밀린 아군이 적과 닿음");
float contactY = -2f - CastContact(spear, ally); float contactY = -2f - CastContact(spear, ally);
float push = 4f - (contactY + 4f); // 남은 거리 = 당긴 4 − 접촉까지 float push = 4f - (contactY + 4f); // 남은 거리 = 당긴 4 − 접촉까지
Assert.AreEqual(contactY - push * cfg.spearRecoilRatio, spear.Position.y, 0.1f, "창병 반작용(PD #1098) = 밀친 거리 × 비율만큼 뒤로"); Assert.AreEqual(contactY - push * cfg.spearPushRatio * cfg.spearRecoilRatio, spear.Position.y, 0.1f, "창병 반작용 = 밀친 거리(남은 거리 × spearPushRatio) × spearRecoilRatio 만큼 뒤로(PD #1098 · 2026-11-10)");
Assert.Less(foe.Hp, foe.MaxHp, "밀린 아군이 적을 공격(규칙 ⑤)"); Assert.Less(foe.Hp, foe.MaxHp, "밀린 아군이 적을 공격(규칙 ⑤)");
Assert.AreEqual(spear.MaxHp, spear.Hp, "창병은 피해 없음"); Assert.AreEqual(spear.MaxHp, spear.Hp, "창병은 피해 없음");
Assert.AreEqual(1, o.Notes.Count); Assert.AreEqual(1, o.Notes.Count);

View File

@ -625,7 +625,7 @@ namespace TKS.Tests
} }
[UnityTest] [UnityTest]
public IEnumerator Rule_SpearPushesTarget_RecoilsHalf_NoAdvanceTogether() public IEnumerator Rule_SpearPushesTargetFar_RecoilsLittle_NoOverwatch()
{ {
yield return Fresh(); yield return Fresh();
var spear = bm.SpawnUnit(UnitType.Spear, Faction.Player, new Vector2(0f, -3f), "창병"); var spear = bm.SpawnUnit(UnitType.Spear, Faction.Player, new Vector2(0f, -3f), "창병");
@ -634,11 +634,13 @@ namespace TKS.Tests
float cc = spear.Radius * Combat.CastRadiusScale + foe.Radius; float cc = spear.Radius * Combat.CastRadiusScale + foe.Radius;
var o = new ActionOutcome(); var o = new ActionOutcome();
yield return Act(spear, foe.Position, o); yield return Act(spear, foe.Position, o);
float push = cc; // 남은 거리 = 당긴 3 − 접촉까지(3 − cc) float push = cc * cfg.spearPushRatio; // 남은 거리(당긴 3 − 접촉까지 = cc) × 창병 밀치기 배율(PD 2026-11-10 「크게 밀친다」)
Assert.AreEqual(push, foe.Position.y, 0.08f, "상대 = 남은 거리만큼 밀림"); Assert.AreEqual(push, foe.Position.y, 0.08f, "상대 = 남은 거리 × spearPushRatio 만큼 밀림");
Assert.AreEqual(-cc - push * cfg.spearRecoilRatio, spear.Position.y, 0.08f, "창병 = 그 × spearRecoilRatio 만큼 반대로(함께 전진 없음)"); Assert.AreEqual(-cc - push * cfg.spearRecoilRatio, spear.Position.y, 0.08f, "창병 = 그 × spearRecoilRatio 만큼 반대로(함께 전진 없음)");
Assert.Greater(cfg.spearPushRatio, cfg.collisionPushRatio, "창병은 보병보다 크게 민다(PD 2026-11-10)");
Assert.Less(cfg.spearPushRatio * cfg.spearRecoilRatio, cfg.collisionSelfBounceRatio, "창병 자신은 보병 자기 튕김보다 적게 밀린다(PD 2026-11-10)");
Assert.Less(foe.Hp, foe.MaxHp, "접촉 순간 공격"); Assert.Less(foe.Hp, foe.MaxHp, "접촉 순간 공격");
Assert.IsTrue(o.Overwatch, "명중 = 견제 유지"); Assert.IsFalse(o.Overwatch || spear.Overwatch, "목표점 명중해도 대기(견제) 없음(PD 2026-11-10 제거)");
} }
} }
} }

View File

@ -366,8 +366,8 @@ namespace TKS.Tests
e2.MaxHp = e2.Hp = 10000; e2.MaxHp = e2.Hp = 10000;
var o2 = new ActionOutcome(); var o2 = new ActionOutcome();
yield return Act(zy, new Vector2(0f, 2f), o2); yield return Act(zy, new Vector2(0f, 2f), o2);
Assert.IsTrue(o2.Returned, "빗나가도 출발점 복귀"); Assert.IsFalse(o2.Returned, "기병 복귀 제거(PD 2026-11-10) = 단기구주 「빗나가도 출발점 복귀」 발동 안 함(스킬 재설계 PD 결정 대기)");
Assert.Less(Vector2.Distance(zy.Position, new Vector2(0f, -4f)), 0.05f); Assert.Greater(zy.Position.y, e2.Position.y, "적을 꿰뚫고 지나가 그 너머에 남음");
Assert.AreEqual(0, o2.CounterTaken, "단기구주 = 반격 없음"); Assert.AreEqual(0, o2.CounterTaken, "단기구주 = 반격 없음");
var disp = o2.Displaced.Find(x => x.Unit == e2); var disp = o2.Displaced.Find(x => x.Unit == e2);
Assert.IsNotNull(disp.Unit); Assert.IsNotNull(disp.Unit);
@ -384,7 +384,7 @@ namespace TKS.Tests
var o3 = new ActionOutcome(); var o3 = new ActionOutcome();
yield return Act(zl, t1.Position, o3); yield return Act(zl, t1.Position, o3);
Assert.Less(t2.Hp, t2.MaxHp, "명중 → 같은 방향 한 번 더 관통"); Assert.Less(t2.Hp, t2.MaxHp, "명중 → 같은 방향 한 번 더 관통");
Assert.IsFalse(o3.Returned, "복귀 대신 추가 관통"); Assert.IsFalse(o3.Returned, "추가 관통 · 복귀 없음");
Assert.AreEqual(t1.MaxHp - t1.Hp, o3.DamageDealt - (t2.MaxHp - t2.Hp), "앞서 관통한 적은 다시 맞지 않음"); Assert.AreEqual(t1.MaxHp - t1.Hp, o3.DamageDealt - (t2.MaxHp - t2.Hp), "앞서 관통한 적은 다시 맞지 않음");
var ps = Eff("zhangliao_p", SkillKeys.PierceDmgPctPerHit); var ps = Eff("zhangliao_p", SkillKeys.PierceDmgPctPerHit);
int hits = bm.Skills.Of(zl).PierceHits; int hits = bm.Skills.Of(zl).PierceHits;
@ -416,6 +416,7 @@ namespace TKS.Tests
} }
[UnityTest] [UnityTest]
[Ignore("창병 견제 제거(PD 2026-11-10) — 대기 유닛이 생기지 않아 견제 스킬(청룡언월·후계·삼대 노신·교변·하북 명장 …)이 발동하지 않음 · 스킬 재설계 PD 결정 대기")]
public IEnumerator Spear_OverwatchSkills_Radius_Unlimited_Count_DmgMult_HealOnKill() public IEnumerator Spear_OverwatchSkills_Radius_Unlimited_Count_DmgMult_HealOnKill()
{ {
yield return Fresh(); yield return Fresh();
@ -518,7 +519,7 @@ namespace TKS.Tests
float rem = CastContact(ch, t); float rem = CastContact(ch, t);
var o3 = new ActionOutcome(); var o3 = new ActionOutcome();
yield return Act(ch, t.Position, o3); yield return Act(ch, t.Position, o3);
Assert.AreEqual(rem * Eff("chengpu_a", SkillKeys.PushMult).P, t.Position.y, 0.12f, "push_mult(창병 = 밀며 전진 거리)"); Assert.AreEqual(rem * cfg.spearPushRatio * Eff("chengpu_a", SkillKeys.PushMult).P, t.Position.y, 0.12f, "push_mult(창병 밀치기 = 남은 거리 × spearPushRatio × p · PD 2026-11-10)");
} }
// ---------- 궁병 · 책사 ---------- // ---------- 궁병 · 책사 ----------

View File

@ -55,7 +55,7 @@ Unity.exe -projectPath <proj> -executeMethod TKS.EditorTools.ProjectSetup.AutoPl
- 규칙 해석 변경 = `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)` · 원거리 사격은 패널티 없음.
- **충돌 규칙(PD 2026-11-04 · #1080)** — `ActionResolver`: 보병 = 경로 첫 유닛(아군·적군)에 부딪히면 **알까기**(등속 접근 → 부딪힌 순간 피해·반격을 즉시 교환(`ImpactExchange`) → **양쪽이 동시에 튕김**: 상대는 접촉 방향으로 남은 거리 × `collisionPushRatio` · 자신은 반대 방향으로 남은 거리 × `collisionSelfBounceRatio` · PD #1081) · 기병 = 유닛 무시 관통(적 피해 · 아군은 지나칠 거리가 안 나오면 **그 아군 직전 정지** `StopBeforeUnclearedAllies`) · 창병 = 첫 유닛을 **밀며 함께 전진**(접촉 간격 유지 · 앞 유닛이 막히면 둘 다 정지 · 적이면 피해 · 아군은 밀기만) · **규칙 ⑤** = 밀린 같은 진영 유닛이 상대와 닿으면(`Combat.TouchingOpponent` · 간격 ≤ 0.12) 그 자리에서 근접 공격 1회(조작마다 · 자기 행동 소모 없음 · `ActionOutcome.Notes`). 적 AI 도 같은 코드(대칭). `UnitStats.attackRange` 는 창병 견제 반경만. - **충돌 규칙(PD 2026-11-04 · #1080)** — `ActionResolver`: 보병 = 경로 첫 유닛(아군·적군)에 부딪히면 **알까기**(등속 접근 → 부딪힌 순간 피해·반격을 즉시 교환(`ImpactExchange`) → **양쪽이 동시에 튕김**: 상대는 접촉 방향으로 남은 거리 × `collisionPushRatio` · 자신은 반대 방향으로 남은 거리 × `collisionSelfBounceRatio` · PD #1081) · 기병 = 유닛 무시 관통(적 피해 · 아군은 지나칠 거리가 안 나오면 **그 아군 직전 정지** `StopBeforeUnclearedAllies`) · 창병 = 첫 유닛을 **크게 밀치고 자신은 적게 밀림**(남은 거리 × `spearPushRatio` 1.5 · 반작용 = 그 × `spearRecoilRatio` 0.2 · 적이면 피해 · 아군은 밀기만 · PD #1098 · 2026-11-10) · **규칙 ⑤** = 밀린 같은 진영 유닛이 상대와 닿으면(`Combat.TouchingOpponent` · 간격 ≤ 0.12) 그 자리에서 근접 공격 1회(조작마다 · 자기 행동 소모 없음 · `ActionOutcome.Notes`). 적 AI 도 같은 코드(대칭). `UnitStats.attackRange` = 옛 창병 견제 반경(견제 제거로 미사용).
- **사거리 표시(PD 2026-11-04)**: 유닛 탭(아군·적군) → `RangeIndicator`(슬링샷 최대 거리 = 얇은 반투명 원 · 궁병 = 슬링샷 범위 ~ ×(1+비율) 채워진 붉은 도넛 · 범위 안 적이 없으면 회색). - **사거리 표시(PD 2026-11-04)**: 유닛 탭(아군·적군) → `RangeIndicator`(슬링샷 최대 거리 = 얇은 반투명 원 · 궁병 = 슬링샷 범위 ~ ×(1+비율) 채워진 붉은 도넛 · 범위 안 적이 없으면 회색).
- 보고는 실측·추정·미확인을 분리한다(C23 · C5). 커밋은 의도 변경 파일만. - 보고는 실측·추정·미확인을 분리한다(C23 · C5). 커밋은 의도 변경 파일만.
@ -84,7 +84,7 @@ UI 를 새로 만들거나 고칠 때 **반드시** 이 구조를 따른다(어
- **IB(쿠로네코) 이펙트 시험(현재 기본 공격 · PD 2026-11-10 「내가 원한 비주얼은 이런게 아니야(이노센트베인 영상) → 우선 IB 이펙트를 그대로 써서 확인 · 나중에 구조에 맞게 새로 그림(화풍 통일)」)**: IB 슬링샷 발사 `UnitVisualEffectControl.ShowBoosterFX()` 와 같은 구성 — `ef_booster`(앞 빛 물결 2장 + 가시 · 0.2초 반복 맥동 · 유닛 위 · 이동 끝에 바로 지움 = IB Breaking 때 Kill) · `ef_speed_tail`(꼬리 0.6초 + 화살표 입자 · 유닛 아래 · 끝나면 꼬리가 줄며 사라짐 — `SkillVfx.Stop` 이 트레일 길이만큼 기다림) · `ef_booster_dust`(출발 먼지 1번) · 타격 `ef_normal_hit`. 원본 = `Assets/VFX/IB/`(쿠로네코 `E:\NerdNavis\Kuroneko\KuronekoClient` 에서 `Tools/wl_vfx_import.py copy … --wl <쿠로네코> --dest Assets/VFX/IB --skip-scripts` · GUID 유지 · 무수정) · 사본 = `Resources/Fx/TKS_Fx_IB_*`(`Tools/make_ib_fx_copies.cs` — IB 전용 스크립트 부품 제거 · 정렬 레이어 Default · 방향 있는 것은 감싸는 루트 + 안쪽 Z −90°(IB 정면 +Y → 이 게임 +X) · 소리 이벤트 받는 곳 `IbFxAnimEvents`(아무것도 안 함 · 없으면 재생마다 콘솔 오류)). 셰이더 = `Fingar/Additive`(더하기 섞기 · 빌트인 그대로). 2D 돌진 3차 행은 표에 `#` 주석(되돌리기). **진영색(PD 「진영 색을 입히고」)** = `TKS_Fx_IB_Booster_<진영>`(스프라이트 색의 색상만 진영색 · 채도·밝기 유지) · `TKS_Fx_IB_SpeedTail_<진영>`(꼬리 그림 `Assets/VFX/IB/_TKS/Tail_Basic02_<진영>.png` = `Tools/make_ib_camp_textures.py` · 재질 같은 폴더 · 화살표 입자 색) — 둘 다 `make_ib_fx_copies.cs` 가 `Tools/dash2d_camps.csv` 로 만듦 · 원색 사본 `TKS_Fx_IB_Booster` · `SpeedTail` 은 비교용으로 남음(표에서 안 씀). **사라지는 시점(PD 「멈춘 이후 뒤늦게 이펙트가 사라짐 · 위치 안 맞음」)** = 속도 꼬리 `f_fade` 0.12 — 멈춘 자리에서 꼬리째 흐려짐(`SkillVfxFade` 가 트레일 색 그러데이션 불투명도도 낮춤 · 전에는 0.6초 꼬리 머리가 기병 돌격 끝자리에 남아 복귀 중에도 보였음). **타격** = 현재 없음(PD 2026-11-10 「적이 피격되었을 때 히트 이펙트는 제거 · 어떤 히트 이펙트를 쓸지 알려줄게」 · IB NormalHit · BounceHit 행은 표에 `#` 주석). **기준점 `e_anchor` = dash**(빛 물결 · 꼬리 · PD 「복귀할 때 이펙트가 캐릭터보다 위쪽에 생겨 안 보임 → 유닛 바닥에」) = 위·옆으로 갈 때 몸 가운데 · 아래로 갈수록 발 쪽(곧장 아래 = 발). **흙먼지** = WL 창 돌진 흙먼지 `TKS_Fx_SpearRushDust`(0.15초마다 발밑 · PD 「복귀할 때 흙먼지가 너무 세서 뭉개짐 → WL 흙먼지로」 · IB 출발 먼지 행은 `#` 주석). - **IB(쿠로네코) 이펙트 시험(현재 기본 공격 · PD 2026-11-10 「내가 원한 비주얼은 이런게 아니야(이노센트베인 영상) → 우선 IB 이펙트를 그대로 써서 확인 · 나중에 구조에 맞게 새로 그림(화풍 통일)」)**: IB 슬링샷 발사 `UnitVisualEffectControl.ShowBoosterFX()` 와 같은 구성 — `ef_booster`(앞 빛 물결 2장 + 가시 · 0.2초 반복 맥동 · 유닛 위 · 이동 끝에 바로 지움 = IB Breaking 때 Kill) · `ef_speed_tail`(꼬리 0.6초 + 화살표 입자 · 유닛 아래 · 끝나면 꼬리가 줄며 사라짐 — `SkillVfx.Stop` 이 트레일 길이만큼 기다림) · `ef_booster_dust`(출발 먼지 1번) · 타격 `ef_normal_hit`. 원본 = `Assets/VFX/IB/`(쿠로네코 `E:\NerdNavis\Kuroneko\KuronekoClient` 에서 `Tools/wl_vfx_import.py copy … --wl <쿠로네코> --dest Assets/VFX/IB --skip-scripts` · GUID 유지 · 무수정) · 사본 = `Resources/Fx/TKS_Fx_IB_*`(`Tools/make_ib_fx_copies.cs` — IB 전용 스크립트 부품 제거 · 정렬 레이어 Default · 방향 있는 것은 감싸는 루트 + 안쪽 Z −90°(IB 정면 +Y → 이 게임 +X) · 소리 이벤트 받는 곳 `IbFxAnimEvents`(아무것도 안 함 · 없으면 재생마다 콘솔 오류)). 셰이더 = `Fingar/Additive`(더하기 섞기 · 빌트인 그대로). 2D 돌진 3차 행은 표에 `#` 주석(되돌리기). **진영색(PD 「진영 색을 입히고」)** = `TKS_Fx_IB_Booster_<진영>`(스프라이트 색의 색상만 진영색 · 채도·밝기 유지) · `TKS_Fx_IB_SpeedTail_<진영>`(꼬리 그림 `Assets/VFX/IB/_TKS/Tail_Basic02_<진영>.png` = `Tools/make_ib_camp_textures.py` · 재질 같은 폴더 · 화살표 입자 색) — 둘 다 `make_ib_fx_copies.cs` 가 `Tools/dash2d_camps.csv` 로 만듦 · 원색 사본 `TKS_Fx_IB_Booster` · `SpeedTail` 은 비교용으로 남음(표에서 안 씀). **사라지는 시점(PD 「멈춘 이후 뒤늦게 이펙트가 사라짐 · 위치 안 맞음」)** = 속도 꼬리 `f_fade` 0.12 — 멈춘 자리에서 꼬리째 흐려짐(`SkillVfxFade` 가 트레일 색 그러데이션 불투명도도 낮춤 · 전에는 0.6초 꼬리 머리가 기병 돌격 끝자리에 남아 복귀 중에도 보였음). **타격** = 현재 없음(PD 2026-11-10 「적이 피격되었을 때 히트 이펙트는 제거 · 어떤 히트 이펙트를 쓸지 알려줄게」 · IB NormalHit · BounceHit 행은 표에 `#` 주석). **기준점 `e_anchor` = dash**(빛 물결 · 꼬리 · PD 「복귀할 때 이펙트가 캐릭터보다 위쪽에 생겨 안 보임 → 유닛 바닥에」) = 위·옆으로 갈 때 몸 가운데 · 아래로 갈수록 발 쪽(곧장 아래 = 발). **흙먼지** = WL 창 돌진 흙먼지 `TKS_Fx_SpearRushDust`(0.15초마다 발밑 · PD 「복귀할 때 흙먼지가 너무 세서 뭉개짐 → WL 흙먼지로」 · IB 출발 먼지 행은 `#` 주석).
- **방향 결정 UI = IB 이식(PD 2026-11-10 「IB 와 같이 슬링샷 조작 시 방향 결정 UI 등을 그대로 이식」)**: `TKS.IbAimArrow`(IB `GameTouchArrow` 동작 · 프리팹 `Resources/Fx/TKS_Fx_IB_GameArrow` = 원본 `Assets/VFX/IB/AssetResources/Game/GameArrow` 사본) — 누르면 유닛 둘레 고리 + 퍼지는 고리 3개(3초 · 1초 간격) · 당기면 하늘색 화살표가 쏠 방향으로 돌고 당긴 세기만큼 길어짐(0.5~1.5 · 최소 당김 → 슬링샷 최대 거리) · 화살 끝이 전장 밖이면 안으로 · 놓으면 숨김. 전에 쓰던 조준선 · 당김선은 끔(`SlingshotInput.ShowPreview` · IbAimArrow 가 없을 때만 예전 선). 목표 고리 · 강조 · 궁병 화살 경로선 · 기믹 예상 궤적은 그대로. 화살표 색 = IB 공격 종류 3(하늘색) 하나. - **방향 결정 UI = IB 이식(PD 2026-11-10 「IB 와 같이 슬링샷 조작 시 방향 결정 UI 등을 그대로 이식」)**: `TKS.IbAimArrow`(IB `GameTouchArrow` 동작 · 프리팹 `Resources/Fx/TKS_Fx_IB_GameArrow` = 원본 `Assets/VFX/IB/AssetResources/Game/GameArrow` 사본) — 누르면 유닛 둘레 고리 + 퍼지는 고리 3개(3초 · 1초 간격) · 당기면 하늘색 화살표가 쏠 방향으로 돌고 당긴 세기만큼 길어짐(0.5~1.5 · 최소 당김 → 슬링샷 최대 거리) · 화살 끝이 전장 밖이면 안으로 · 놓으면 숨김. 전에 쓰던 조준선 · 당김선은 끔(`SlingshotInput.ShowPreview` · IbAimArrow 가 없을 때만 예전 선). 목표 고리 · 강조 · 궁병 화살 경로선 · 기믹 예상 궤적은 그대로. 화살표 색 = IB 공격 종류 3(하늘색) 하나.
- **여포 크기(PD 2026-11-10 「왜 여포는 다른 기병보다 작지?」)**: 말 탄 유닛은 머리 끝 상한(BalanceConfig.mountedArtMaxTop 1.3 × 표준 키)에 맞춰 줄어드는데, `dz_boss_lvbu` 의 몸 높이(`meta.json` bodyHeightPx · `art_size.csv` body_px)가 머리 깃털 끝까지 819 px 라 몸 전체가 더 줄었음 → 왕관 끝 ~ 발 718 px(front_idle 실측 · 깃털 제외 · 다른 기병의 투구 끝 기준과 같은 규칙)로 고침 → 배율 0.2381 → 0.2716(+14 %). 원본 백업 = 세션 scratchpad. **다른 PC 에서 그림을 다시 export 하면 819 로 되돌아갈 수 있음**(export 규칙 = 불투명 폭 30 % 행 — 깃털이 넓어서 안 걸러짐). - **여포 크기(PD 2026-11-10 「왜 여포는 다른 기병보다 작지?」)**: 말 탄 유닛은 머리 끝 상한(BalanceConfig.mountedArtMaxTop 1.3 × 표준 키)에 맞춰 줄어드는데, `dz_boss_lvbu` 의 몸 높이(`meta.json` bodyHeightPx · `art_size.csv` body_px)가 머리 깃털 끝까지 819 px 라 몸 전체가 더 줄었음 → 왕관 끝 ~ 발 718 px(front_idle 실측 · 깃털 제외 · 다른 기병의 투구 끝 기준과 같은 규칙)로 고침 → 배율 0.2381 → 0.2716(+14 %). 원본 백업 = 세션 scratchpad. **다른 PC 에서 그림을 다시 export 하면 819 로 되돌아갈 수 있음**(export 규칙 = 불투명 폭 30 % 행 — 깃털이 넓어서 안 걸러짐).
- **기병 복귀 영역(PD 2026-11-10 「기병도 궁수와 같이 … 복귀 발동 지점」 → 「궁병처럼 채워진 영역 형태로」)**: 아군 기병을 선택·조준하면 적마다 (유닛 반지름 + 최종 명중 허용 거리) 하늘색 채운 원 = 목표점을 이 안에 놓으면 공격 뒤 복귀(`ActionResolver.PreviewCavalryReturn` 이 참 · 슬링샷 거리 안인 적만 · 지금 노리는 원 진하게 · 유닛 위 정렬 11 — 바닥이면 적 그림에 가려 안 보임 · `SlingshotInput.ShowReturnZones`). 예약 액티브(위진소요 · 단기구주)는 발동 전이라 판정에 안 들어감. - **근접 3병종 특징 = 한 줄 정의(PD 2026-11-10 직접 지시)**: **보병** = 적을 밀치고 튕겨나며 튕기면서 다른 적을 추가로 공격(알까기 + 연쇄 · #1098 그대로 · 목표점 명중 추가 이동 1회는 유지 = PD 확인 대상) · **창병** = 적을 크게 밀치지만 자신은 적게 밀림(`spearPushRatio` 1.5 > 보병 `collisionPushRatio` 1.0 · 반작용 남은 거리 × 0.3 < 보병 자기 튕김 0.5) · **창병 대기(견제) 제거**(「유저가 인지하기 어려움」 · 목표점 명중해도 `SetOverwatch` 안 함 · HUD 「견제 반경·대기 중」 문구 삭제) · **기병** = 적을 관통해 이동 · **목표점 명중 시 제자리 복귀 제거**(「유저가 인지하기 어려움」 · `PreviewCavalryReturn` · 조준 때 복귀 영역 `ShowReturnZones` 삭제 · AI 가중치 `cav_return`·`cav_stranded` 삭제). 견제·복귀 스킬(청룡언월 · 후계 · 구벌중원 · 기린아 · 교변 · 삼대 노신 · 유수의 복수 · 장사 · 백마의 창 · 하북 명장 · 남만의 여왕 · 장사 태수 · 단기구주 · 도주 본능 · 소패왕) = 발동 안 함 · **재설계 PD 결정 대기** → 그때까지 `BattleManager.ApplyOverwatch` · `Unit.Overwatch` · `SkillMods.Overwatch*` 는 남겨 둠(대기 유닛이 없어 실행 안 됨) · `SkillTests.Spear_OverwatchSkills_*` = [Ignore].
- **IB 크기 · 캐릭터 연출(PD 2026-11-10 「IB 와 화살표 크기도 다르고 선택된 캐릭터 주위 연출도 누락」)**: 값 표 `Resources/Skills/ib_ui.csv`(`TKS.IbUi`) — IB 유닛 반지름 0.496(ExportCharacter size 8 × 0.062) 기준 배율 = 이 게임 유닛 반지름 / 0.496 → 고리 지름 = 반지름 × 5.4 · 화살표 세기 s = IB 월드 당김 × 0.6(0.5~1.5) · IB 취소 거리 0.5. **`TKS.IbUnitMarks`** = 선택한 행동 가능 아군 발밑 도는 원(`TKS_Fx_IB_TargetCircle` · IB ShowUserTurnMark) + 몸 둘레 빛(`TKS_Fx_IB_AttackReady`) · 조준 중(누름 ~ 놓음) 모든 유닛 속성 방울(`TKS_Fx_IB_Attribute_<속성>` · IB SetActiveBarrierVisualEffect · 진영 → 속성 = 표 element_*: 위 물 · 촉 나무 · 오 불 · 군·진영 없음 어둠). - **IB 크기 · 캐릭터 연출(PD 2026-11-10 「IB 와 화살표 크기도 다르고 선택된 캐릭터 주위 연출도 누락」)**: 값 표 `Resources/Skills/ib_ui.csv`(`TKS.IbUi`) — IB 유닛 반지름 0.496(ExportCharacter size 8 × 0.062) 기준 배율 = 이 게임 유닛 반지름 / 0.496 → 고리 지름 = 반지름 × 5.4 · 화살표 세기 s = IB 월드 당김 × 0.6(0.5~1.5) · IB 취소 거리 0.5. **`TKS.IbUnitMarks`** = 선택한 행동 가능 아군 발밑 도는 원(`TKS_Fx_IB_TargetCircle` · IB ShowUserTurnMark) + 몸 둘레 빛(`TKS_Fx_IB_AttackReady`) · 조준 중(누름 ~ 놓음) 모든 유닛 속성 방울(`TKS_Fx_IB_Attribute_<속성>` · IB SetActiveBarrierVisualEffect · 진영 → 속성 = 표 element_*: 위 물 · 촉 나무 · 오 불 · 군·진영 없음 어둠).
- **타격감(PD 2026-11-10 「화면 흔들림 등도 IB 를 참고해서」)**: `TKS.HitFeel` + 표 `Resources/Skills/hit_feel.csv`(행동 중 유닛이 상대에게 처음 피해를 준 순간 · `SkillSystem.Hit` · 흔들림 = `CameraController.Shake`(추적 위치는 그대로 두고 흔들림만 더함) · 히트 스톱 칸 있음(현재 0)). **IB 실측**: 흔들림 함수 6종은 스킬·궁극기·기믹 이펙트 애니메이션 이벤트로만 불리고 본문이 디컴파일에서 비어 있어 값 없음 · 일반 타격·벽 튕김엔 흔들림 없음 · 히트 스톱 없음 → 표 값(hit 0.15초 · 키 10 % / 기병 0.18초 · 13 %)은 추정. - **타격감(PD 2026-11-10 「화면 흔들림 등도 IB 를 참고해서」)**: `TKS.HitFeel` + 표 `Resources/Skills/hit_feel.csv`(행동 중 유닛이 상대에게 처음 피해를 준 순간 · `SkillSystem.Hit` · 흔들림 = `CameraController.Shake`(추적 위치는 그대로 두고 흔들림만 더함) · 히트 스톱 칸 있음(현재 0)). **IB 실측**: 흔들림 함수 6종은 스킬·궁극기·기믹 이펙트 애니메이션 이벤트로만 불리고 본문이 디컴파일에서 비어 있어 값 없음 · 일반 타격·벽 튕김엔 흔들림 없음 · 히트 스톱 없음 → 표 값(hit 0.15초 · 키 10 % / 기병 0.18초 · 13 %)은 추정.
- **2D 돌진(PD 2026-11-10 「기본 공격(돌진) 이펙트가 너무 3D 느낌 → 2D 처럼」 · MSFX 혜성 행은 표에 `#` 주석으로 남김 = 되돌리기)**: **3차(현재 · PD 2026-11-10 「검기는 이전 형태가 더 나음 · 대쉬 이펙트는 캐릭터 몸을 벗어나지 않도록 · 첨부 같은 느낌으로 몸을 뒤덮는 연출을 스킬용 시험」)** = 초승달 `Head` = 1차 그림 `dash2d_crescent`(검정 외곽선 · 진영색 곱) · 대쉬 층(꼬리 0.04초 · 바람 줄기 · 알갱이 · 흙먼지) = 유닛을 따라 움직이는 Local 공간 · 몸 기준점 반경 0.45 키 안(실측 그림 경계 좌우 ±0.56 · 아래 −0.60 ~ 위 +0.59 키) · 바람 줄기 `Lines` · 알갱이 `Dots` = 유닛 위 반투명(표 `s_over` = `Head;Lines;Dots` · 유닛 아래면 몸에 다 가려 안 보임 · PD 「몸의 대쉬 효과가 왜 안 보이나」) · 꼬리 · 흙먼지 = 유닛 아래 · **기병 복귀 = 돌아서서 감**(`UnitView.TurnAround` · 복귀 방향으로 보기 + 곧장 위·아래면 좌우 반전 · 도착 뒤에도 마지막 이동 방향 PD #1082) · **몸 뒤덮는 기운(시험)** = `TKS_Fx_Aura2D_<진영>`(혜성 기운 `aura_comet_<진영>` 겹쳐 깜빡임 · 불꽃 혀 `aura_wisp` · 반짝임 = 유닛 아래 · 옅은 막 `Veil` · 앞 테 `Rim`(2차 검기 그림) = 유닛 위) · 표 키 `test_aura` · 테스트 씬 「기운 시험」 스위치(`SkillVfx.TestTravelKey` · 켜면 슬링샷 공격 돌진에 함께 · 실제 전투 = 늘 빈칸). 2차 = 외곽선 없음 · 그림 `Assets/VFX/TKS2D/`(만들기 = `python -I -X utf8 Tools/make_dash2d_textures.py` · 진영색 원본 = `Tools/dash2d_camps.csv`) → 프리팹 `Resources/Fx/TKS_Fx_Dash2D_<wei|shu|wu|other>`(검기 초승달 `Head` = 진영별 그림 `dash2d_slash_<진영>` · 유닛 위 · 반투명 꼬리 `Trail`(앞 0.7 → 0) · 흰 바람 줄기 `Lines` · 알갱이 `Dots` = 유닛 아래 · 수명 동안 투명해짐) + `TKS_Fx_Dust2D`(부드러운 반투명 흙먼지 · 모든 진영) · 다시 만들기 = `unity command … script eval --file Tools/build_dash2d.cs`(값을 바꾸면 이 파일과 프리팹을 함께). 1차(검정 외곽선 셀 그림)의 `dash2d_crescent.png` · `dash2d_highlight.png` 는 안 씀. 표 `b_flat` = 1 이면 화면 안에서 진행 방향으로만 돌림(기울기·팩 루트 회전 무시 · `SkillVfx.FlatRotation`) · 크기 단위 = 유닛 키 1(`f_scale` 1) · 끝 소멸 = `f_fade` 0.12(`SkillVfxFade` 가 스프라이트 불투명도도 낮춤). - **2D 돌진(PD 2026-11-10 「기본 공격(돌진) 이펙트가 너무 3D 느낌 → 2D 처럼」 · MSFX 혜성 행은 표에 `#` 주석으로 남김 = 되돌리기)**: **3차(현재 · PD 2026-11-10 「검기는 이전 형태가 더 나음 · 대쉬 이펙트는 캐릭터 몸을 벗어나지 않도록 · 첨부 같은 느낌으로 몸을 뒤덮는 연출을 스킬용 시험」)** = 초승달 `Head` = 1차 그림 `dash2d_crescent`(검정 외곽선 · 진영색 곱) · 대쉬 층(꼬리 0.04초 · 바람 줄기 · 알갱이 · 흙먼지) = 유닛을 따라 움직이는 Local 공간 · 몸 기준점 반경 0.45 키 안(실측 그림 경계 좌우 ±0.56 · 아래 −0.60 ~ 위 +0.59 키) · 바람 줄기 `Lines` · 알갱이 `Dots` = 유닛 위 반투명(표 `s_over` = `Head;Lines;Dots` · 유닛 아래면 몸에 다 가려 안 보임 · PD 「몸의 대쉬 효과가 왜 안 보이나」) · 꼬리 · 흙먼지 = 유닛 아래 · **기병 복귀 = 돌아서서 감**(`UnitView.TurnAround` · 복귀 방향으로 보기 + 곧장 위·아래면 좌우 반전 · 도착 뒤에도 마지막 이동 방향 PD #1082) · **몸 뒤덮는 기운(시험)** = `TKS_Fx_Aura2D_<진영>`(혜성 기운 `aura_comet_<진영>` 겹쳐 깜빡임 · 불꽃 혀 `aura_wisp` · 반짝임 = 유닛 아래 · 옅은 막 `Veil` · 앞 테 `Rim`(2차 검기 그림) = 유닛 위) · 표 키 `test_aura` · 테스트 씬 「기운 시험」 스위치(`SkillVfx.TestTravelKey` · 켜면 슬링샷 공격 돌진에 함께 · 실제 전투 = 늘 빈칸). 2차 = 외곽선 없음 · 그림 `Assets/VFX/TKS2D/`(만들기 = `python -I -X utf8 Tools/make_dash2d_textures.py` · 진영색 원본 = `Tools/dash2d_camps.csv`) → 프리팹 `Resources/Fx/TKS_Fx_Dash2D_<wei|shu|wu|other>`(검기 초승달 `Head` = 진영별 그림 `dash2d_slash_<진영>` · 유닛 위 · 반투명 꼬리 `Trail`(앞 0.7 → 0) · 흰 바람 줄기 `Lines` · 알갱이 `Dots` = 유닛 아래 · 수명 동안 투명해짐) + `TKS_Fx_Dust2D`(부드러운 반투명 흙먼지 · 모든 진영) · 다시 만들기 = `unity command … script eval --file Tools/build_dash2d.cs`(값을 바꾸면 이 파일과 프리팹을 함께). 1차(검정 외곽선 셀 그림)의 `dash2d_crescent.png` · `dash2d_highlight.png` 는 안 씀. 표 `b_flat` = 1 이면 화면 안에서 진행 방향으로만 돌림(기울기·팩 루트 회전 무시 · `SkillVfx.FlatRotation`) · 크기 단위 = 유닛 키 1(`f_scale` 1) · 끝 소멸 = `f_fade` 0.12(`SkillVfxFade` 가 스프라이트 불투명도도 낮춤).