스킬 시스템(PD #1093 세션 B) — skills.csv effect_key 51종 처리기 · 액티브(예약/즉시)·패시브 · HUD 스킬 버튼 · 적 무장 패시브 · 스킬 레벨업·스킬서
- 데이터: SkillInfoDb(= 스킬 DB) 에 effect_key·p1~p3(pMask)·lv_scale·needs_impl 추가 · 임포터 검증(trigger · 키 51종 · p 칸 · skill_levels 연속 · lv_max) · skill_levels = levels(비용 표) - 처리기 Runtime/Skills: SkillKeys(51종 · 레벨 계수 = 값×계수 / 배율은 1 넘는 몫 / 횟수 반올림 / 켜기·치환 고정 · 팀 키) · SkillDef·SkillCatalog · SkillSystem(유닛별 쿨·예약 · 즉시형 시한 효과 이번 아군 턴/이번 적 턴 · 행동 훅 · 호위 분담 · HUD 한 줄) · SkillMods(전투 코드 수치 질의) · SkillRules(상한 = 등급 + ★3 액티브 +1 · ★5 패시브 ×1.5 = evolve.csv · 레벨업 · 스킬서 판매) - 전투 연결: ActionResolver(밀기·튕김·추가 이동 n·추가 이동 공격·충돌 추가 피해·밀며 전진 ×·삼첨도·관통 피해/넉백/반격 없음/빗나가도 복귀/한 번 더 관통·사거리·사선 통과·크리·광역 반경/비율/아군 0·백기습영 이동 후 사격·군령/대장군 공격 횟수) · Combat(상성 하한·공 %·주는/받는 피해·근접 패널티 해제·반격 배율/받는 반격·견제 배율) · BattleManager(편성·적 무장 부착 · 견제 반경/횟수/무제한/회복 · 교전 반응 정지 · 교전 범위 · 즉시형 실행) · EnemyAI·RangeIndicator(슬링·사거리 스킬 반영) - UI: HUD 스킬 버튼(이름·Lv·쿨·사용 가능/불가·패시브) · 탭 = 예약/취소/즉시 · 조준 중 2손가락 탭 · 발동 = 이름 글자 + 플래시 · 스킬 한 줄 · 예약 배지 · 변진 반경 원 · 영웅 상세 스킬 레벨업 팝업 · 거래 상점 스킬서 칸(임시) - 표: economy.csv start_skill_books 10 · skill_book_shop_daily 3 · skill_book_price 300(임시 · PD 확인) - 테스트 73/73(기존 56 + SkillTests 17 · 51종 전부) · console error 0 · 캡처 = DataImporter.CaptureSkills Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d32334786d
commit
85aed38ce5
|
|
@ -30,3 +30,6 @@ shop_slots,6
|
|||
shop_grades,white;green
|
||||
shop_price_mult,5
|
||||
shop_refresh_gem,20
|
||||
start_skill_books,10
|
||||
skill_book_shop_daily,3
|
||||
skill_book_price,300
|
||||
|
|
|
|||
|
|
|
@ -76,3 +76,9 @@ MonoBehaviour:
|
|||
value: 5
|
||||
- key: shop_refresh_gem
|
||||
value: 20
|
||||
- key: start_skill_books
|
||||
value: 10
|
||||
- key: skill_book_shop_daily
|
||||
value: 3
|
||||
- key: skill_book_price
|
||||
value: 300
|
||||
|
|
|
|||
|
|
@ -25,6 +25,13 @@ MonoBehaviour:
|
|||
\uD295\uAE30\uC9C0 \uC54A\uACE0(\uC790\uAE30 \uD295\uAE40 0) \uCD94\uAC00 \uC774\uB3D9\uC774
|
||||
2\uD68C [p1 \uCD94\uAC00 \uC774\uB3D9 \uD69F\uC218 \xB7 p2 \uC790\uAE30 \uD295\uAE40
|
||||
\uBC30\uC728]"
|
||||
effectKey: extra_move_count;self_bounce_mult
|
||||
p1: 2
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 3
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: liubei_p
|
||||
heroId: liubei
|
||||
kind: passive
|
||||
|
|
@ -35,6 +42,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uC544\uAD70\uC774 \uBC00\uB824\uC11C \uD558\uB294 \uACF5\uACA9 \uD53C\uD574
|
||||
+50 %"
|
||||
effectKey: pushed_ally_attack_pct
|
||||
p1: 0.5
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: guanyu_a
|
||||
heroId: guanyu
|
||||
kind: active
|
||||
|
|
@ -45,6 +59,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uB2E4\uC74C \uD589\uB3D9: \uBA85\uC911 \uC2DC \uACAC\uC81C \uBC18\uACBD
|
||||
\xD72 \xB7 \uADF8 \uC801 \uD134 \uB3D9\uC548 \uACAC\uC81C \uD69F\uC218 \uBB34\uC81C\uD55C"
|
||||
effectKey: overwatch_radius_mult;overwatch_unlimited
|
||||
p1: 2
|
||||
p2: 1
|
||||
p3: 0
|
||||
pMask: 3
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: guanyu_p
|
||||
heroId: guanyu
|
||||
kind: passive
|
||||
|
|
@ -55,6 +76,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uBC18\uACA9 \uD53C\uD574 0.5 \u2192 1.0 \uBC30 [p1 \uBC18\uACA9 \uBC30\uC728(\uAE30\uBCF8
|
||||
0.5)]"
|
||||
effectKey: counter_ratio
|
||||
p1: 1
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: zhangfei_a
|
||||
heroId: zhangfei
|
||||
kind: active
|
||||
|
|
@ -67,6 +95,13 @@ MonoBehaviour:
|
|||
\uBC14\uAE65\uC73C\uB85C 0.8 \uBC00\uACE0 \xB7 \uC774\uBC88 \uC801 \uD134 \uADF8
|
||||
\uC801\uB4E4\uC740 \uAD50\uC804 \uBC18\uC751 \uC5C6\uC74C [p1 \uBC00\uAE30
|
||||
\uAC70\uB9AC \xB7 p2 \uBC18\uACBD]"
|
||||
effectKey: aoe_push_fear
|
||||
p1: 0.8
|
||||
p2: 4
|
||||
p3: 0
|
||||
pMask: 3
|
||||
lvScale: 0.1
|
||||
needsImpl: 1
|
||||
- id: zhangfei_p
|
||||
heroId: zhangfei
|
||||
kind: passive
|
||||
|
|
@ -77,6 +112,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uAD00\uD1B5\uC73C\uB85C 2\uAE30 \uC774\uC0C1 \uB9DE\uD788\uBA74 \uD53C\uD574
|
||||
+30 % [p2 \uCD5C\uC18C \uAD00\uD1B5 \uC218 \xB7 \uD0A4 \uADFC\uC0AC]"
|
||||
effectKey: pierce_dmg_mult
|
||||
p1: 1.3
|
||||
p2: 2
|
||||
p3: 0
|
||||
pMask: 3
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: zhaoyun_a
|
||||
heroId: zhaoyun
|
||||
kind: active
|
||||
|
|
@ -87,6 +129,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uB2E4\uC74C \uD589\uB3D9: \uAD00\uD1B5 \uC911 \uBC18\uACA9\uC744 \uBC1B\uC9C0
|
||||
\uC54A\uACE0 \xB7 \uBE57\uB098\uAC00\uB3C4 \uCD9C\uBC1C\uC810 \uBCF5\uADC0"
|
||||
effectKey: pierce_no_counter;pierce_return_on_miss
|
||||
p1: 1
|
||||
p2: 1
|
||||
p3: 0
|
||||
pMask: 3
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: zhaoyun_p
|
||||
heroId: zhaoyun
|
||||
kind: passive
|
||||
|
|
@ -97,6 +146,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uAD00\uD1B5 \uD53C\uD574\uB97C \uC900 \uC801 \uBC00\uAE30 0.35 \u2192
|
||||
0.7 [0.35\u21920.7 = \xD72]"
|
||||
effectKey: pierce_knockback_mult
|
||||
p1: 2
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: huangzhong_a
|
||||
heroId: huangzhong
|
||||
kind: active
|
||||
|
|
@ -107,6 +163,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uB2E4\uC74C \uD589\uB3D9: \uD654\uC0B4\uC774 \uC544\uAD70\uC744 \uD1B5\uACFC(\uC0AC\uC120)\uD558\uACE0
|
||||
\uBA85\uC911 \uC5EC\uBD80\uC640 \uBB34\uAD00\uD558\uAC8C \uD06C\uB9AC"
|
||||
effectKey: arrow_pass_allies;arrow_crit_always
|
||||
p1: 1
|
||||
p2: 1
|
||||
p3: 0
|
||||
pMask: 3
|
||||
lvScale: 0.1
|
||||
needsImpl: 1
|
||||
- id: huangzhong_p
|
||||
heroId: huangzhong
|
||||
kind: passive
|
||||
|
|
@ -117,6 +180,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "HP 50 % \uC774\uD558\uC77C \uB54C \uACF5 +40 % [p1 \uACF5 \uC99D\uAC00
|
||||
\xB7 p2 HP \uC784\uACC4]"
|
||||
effectKey: dmg_pct_hp_below
|
||||
p1: 0.4
|
||||
p2: 0.5
|
||||
p3: 0
|
||||
pMask: 3
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: zhugeliang_a
|
||||
heroId: zhugeliang
|
||||
kind: active
|
||||
|
|
@ -128,6 +198,13 @@ MonoBehaviour:
|
|||
descKo: "\uC989\uC2DC: \uC774\uBC88 \uD134 \uC544\uAD70 \uC804\uC6D0 \uC2AC\uB9C1
|
||||
\uAC70\uB9AC +30 % [p1 \uBE44\uC728 \xB7 p2 \uC808\uB300 \uAC00\uC0B0 \xB7
|
||||
p3 1=\uC544\uAD70 \uC804\uC6D0]"
|
||||
effectKey: sling_dist_pct
|
||||
p1: 0.3
|
||||
p2: 0
|
||||
p3: 1
|
||||
pMask: 7
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: zhugeliang_p
|
||||
heroId: zhugeliang
|
||||
kind: passive
|
||||
|
|
@ -138,6 +215,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uC801 \uAD50\uC804 \uBC94\uC704 \u22121.5(\uC801\uC774 \uB354 \uB2A6\uAC8C
|
||||
\uC6C0\uC9C1\uC778\uB2E4)"
|
||||
effectKey: enemy_aggro_add
|
||||
p1: -1.5
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: zhoucang_a
|
||||
heroId: zhoucang
|
||||
kind: active
|
||||
|
|
@ -147,6 +231,13 @@ MonoBehaviour:
|
|||
cooldown: 3
|
||||
lvMax: 5
|
||||
descKo: "\uB2E4\uC74C \uD589\uB3D9: \uC54C\uAE4C\uAE30 \uBC00\uAE30 \xD72"
|
||||
effectKey: push_mult
|
||||
p1: 2
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: zhoucang_p
|
||||
heroId: zhoucang
|
||||
kind: passive
|
||||
|
|
@ -156,6 +247,13 @@ MonoBehaviour:
|
|||
cooldown: 0
|
||||
lvMax: 5
|
||||
descKo: "\uCD5C\uB300 HP +20 %"
|
||||
effectKey: hp_max_pct
|
||||
p1: 0.2
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: guanping_a
|
||||
heroId: guanping
|
||||
kind: active
|
||||
|
|
@ -168,6 +266,13 @@ MonoBehaviour:
|
|||
\uC544\uAD70\uC774 \uBC1B\uB294 \uD53C\uD574\uC758 50 % \uB97C \uAD00\uD3C9\uC774
|
||||
\uB300\uC2E0 \uBC1B\uC74C [p1 \uB300\uC2E0 \uBC1B\uB294 \uBE44\uC728 \xB7 p2
|
||||
\uBC18\uACBD]"
|
||||
effectKey: guard_share
|
||||
p1: 0.5
|
||||
p2: 1.5
|
||||
p3: 0
|
||||
pMask: 3
|
||||
lvScale: 0.1
|
||||
needsImpl: 1
|
||||
- id: guanping_p
|
||||
heroId: guanping
|
||||
kind: passive
|
||||
|
|
@ -177,6 +282,13 @@ MonoBehaviour:
|
|||
cooldown: 0
|
||||
lvMax: 5
|
||||
descKo: "\uACAC\uC81C \uBC18\uACBD +0.5"
|
||||
effectKey: overwatch_radius_add
|
||||
p1: 0.5
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: xushu_a
|
||||
heroId: xushu
|
||||
kind: active
|
||||
|
|
@ -187,6 +299,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uB2E4\uC74C \uD589\uB3D9: \uAD11\uC5ED \uD53C\uD574\uAC00 \uC544\uAD70\uC5D0\uAC8C
|
||||
0(\uAD11\uC5ED)"
|
||||
effectKey: splash_ally_ratio
|
||||
p1: 0
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 1
|
||||
- id: xushu_p
|
||||
heroId: xushu
|
||||
kind: passive
|
||||
|
|
@ -196,6 +315,13 @@ MonoBehaviour:
|
|||
cooldown: 0
|
||||
lvMax: 5
|
||||
descKo: "\uC0AC\uAC70\uB9AC +0.5"
|
||||
effectKey: range_add
|
||||
p1: 0.5
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: taishici_a
|
||||
heroId: taishici
|
||||
kind: active
|
||||
|
|
@ -205,6 +331,13 @@ MonoBehaviour:
|
|||
cooldown: 3
|
||||
lvMax: 5
|
||||
descKo: "\uB2E4\uC74C \uD589\uB3D9: \uC0AC\uAC70\uB9AC \xD71.5"
|
||||
effectKey: range_mult
|
||||
p1: 1.5
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: taishici_p
|
||||
heroId: taishici
|
||||
kind: passive
|
||||
|
|
@ -215,6 +348,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uAD81\uC218 \uADFC\uC811 \uD328\uB110\uD2F0 \uC5C6\uC74C(\uD53C\uD574
|
||||
100 % \xB7 \uD53C\uACA9 100 %)"
|
||||
effectKey: melee_penalty_off
|
||||
p1: 1
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: caocao_a
|
||||
heroId: caocao
|
||||
kind: active
|
||||
|
|
@ -225,6 +365,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uC989\uC2DC: \uC774\uBC88 \uC801 \uD134 \uC544\uAD70 \uC804\uC6D0 \uBC1B\uB294
|
||||
\uD53C\uD574 \u221230 %"
|
||||
effectKey: team_dmg_taken_pct_turn
|
||||
p1: -0.3
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: caocao_p
|
||||
heroId: caocao
|
||||
kind: passive
|
||||
|
|
@ -235,6 +382,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uCD9C\uC804 5\uC778\uC77C \uB54C \uC544\uAD70 \uC804\uC6D0 \uACF5 +10
|
||||
% [p2 \uCD9C\uC804 \uC778\uC6D0 \uC870\uAC74]"
|
||||
effectKey: team_atk_pct_full
|
||||
p1: 0.1
|
||||
p2: 5
|
||||
p3: 0
|
||||
pMask: 3
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: xiahoudun_a
|
||||
heroId: xiahoudun
|
||||
kind: active
|
||||
|
|
@ -245,6 +399,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uB2E4\uC74C \uD589\uB3D9: \uAD00\uD1B5 \uD53C\uD574 +50 % \xB7 \uBC18\uACA9
|
||||
\uBC1B\uC9C0 \uC54A\uC74C"
|
||||
effectKey: pierce_dmg_mult;pierce_no_counter
|
||||
p1: 1.5
|
||||
p2: 1
|
||||
p3: 0
|
||||
pMask: 3
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: xiahoudun_p
|
||||
heroId: xiahoudun
|
||||
kind: passive
|
||||
|
|
@ -255,6 +416,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "HP 30 % \uC774\uD558\uC77C \uB54C \uACF5 +50 % [p1 \uACF5 \uC99D\uAC00
|
||||
\xB7 p2 HP \uC784\uACC4]"
|
||||
effectKey: dmg_pct_hp_below
|
||||
p1: 0.5
|
||||
p2: 0.3
|
||||
p3: 0
|
||||
pMask: 3
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: xuchu_a
|
||||
heroId: xuchu
|
||||
kind: active
|
||||
|
|
@ -265,6 +433,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uB2E4\uC74C \uD589\uB3D9: \uC790\uAE30 \uD295\uAE40 0 \xB7 \uBC00\uAE30
|
||||
\xD71.5 \xB7 \uCD94\uAC00 \uC774\uB3D9\uC5D0\uC11C \uACF5\uACA9 \uAC00\uB2A5"
|
||||
effectKey: self_bounce_mult;push_mult;extra_move_attack
|
||||
p1: 0
|
||||
p2: 1.5
|
||||
p3: 1
|
||||
pMask: 7
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: xuchu_p
|
||||
heroId: xuchu
|
||||
kind: passive
|
||||
|
|
@ -274,6 +449,13 @@ MonoBehaviour:
|
|||
cooldown: 0
|
||||
lvMax: 5
|
||||
descKo: "\uBC00\uB9AC\uB294 \uAC70\uB9AC \u221250 %"
|
||||
effectKey: knockback_taken_pct
|
||||
p1: -0.5
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: zhangliao_a
|
||||
heroId: zhangliao
|
||||
kind: active
|
||||
|
|
@ -285,6 +467,13 @@ MonoBehaviour:
|
|||
descKo: "\uB2E4\uC74C \uD589\uB3D9: \uBA85\uC911 \uC2DC \uBCF5\uADC0 \uB300\uC2E0
|
||||
\uBAA9\uD45C\uC810\uC5D0\uC11C \uAC19\uC740 \uBC29\uD5A5\uC73C\uB85C \uD55C
|
||||
\uBC88 \uB354 \uAD00\uD1B5 [p1 \uCD94\uAC00 \uAD00\uD1B5 \uD69F\uC218]"
|
||||
effectKey: pierce_repeat
|
||||
p1: 1
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: zhangliao_p
|
||||
heroId: zhangliao
|
||||
kind: passive
|
||||
|
|
@ -295,6 +484,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uAD00\uD1B5\uD55C \uC801 1\uAE30\uB2F9 \uACF5 +5 %(\uCD5C\uB300 +25
|
||||
% \xB7 \uC804\uD22C \uC911 \uB204\uC801) [p1 1\uAE30\uB2F9 \xB7 p2 \uCD5C\uB300]"
|
||||
effectKey: pierce_dmg_pct_per_hit
|
||||
p1: 0.05
|
||||
p2: 0.25
|
||||
p3: 0
|
||||
pMask: 3
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: xuhuang_a
|
||||
heroId: xuhuang
|
||||
kind: active
|
||||
|
|
@ -305,6 +501,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uB2E4\uC74C \uD589\uB3D9: \uC0C1\uC131 \uBD88\uB9AC(\uAE30\uBCD1 0.7)\uB97C
|
||||
1.0 \uC73C\uB85C"
|
||||
effectKey: matchup_floor
|
||||
p1: 1
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: xuhuang_p
|
||||
heroId: xuhuang
|
||||
kind: passive
|
||||
|
|
@ -314,6 +517,13 @@ MonoBehaviour:
|
|||
cooldown: 0
|
||||
lvMax: 5
|
||||
descKo: "\uBC1B\uB294 \uBC18\uACA9 \uD53C\uD574 \u221230 %"
|
||||
effectKey: counter_taken_pct
|
||||
p1: -0.3
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: zhanghe_a
|
||||
heroId: zhanghe
|
||||
kind: active
|
||||
|
|
@ -324,6 +534,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uC989\uC2DC \u2605: \uBC18\uACBD 3 \uC548 \uBE48 \uC9C0\uC810\uC73C\uB85C
|
||||
\uC774\uB3D9(\uD589\uB3D9 \uC18C\uBAA8 \uC5C6\uC74C) [p1 \uBC18\uACBD]"
|
||||
effectKey: teleport
|
||||
p1: 3
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 1
|
||||
- id: zhanghe_p
|
||||
heroId: zhanghe
|
||||
kind: passive
|
||||
|
|
@ -333,6 +550,13 @@ MonoBehaviour:
|
|||
cooldown: 0
|
||||
lvMax: 5
|
||||
descKo: "\uACAC\uC81C \uD53C\uD574 \xD71.5"
|
||||
effectKey: overwatch_dmg_mult
|
||||
p1: 1.5
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: caoren_a
|
||||
heroId: caoren
|
||||
kind: active
|
||||
|
|
@ -344,6 +568,13 @@ MonoBehaviour:
|
|||
descKo: "\uC989\uC2DC \u2605: \uC774\uBC88 \uC801 \uD134 \uBC18\uACBD 2 \uC548
|
||||
\uC544\uAD70 \uC804\uC6D0 \uBC1B\uB294 \uD53C\uD574 \u221240 % [p1 \uBC1B\uB294
|
||||
\uD53C\uD574 \xB7 p2 \uBC18\uACBD]"
|
||||
effectKey: dmg_taken_aura
|
||||
p1: -0.4
|
||||
p2: 2
|
||||
p3: 0
|
||||
pMask: 3
|
||||
lvScale: 0.1
|
||||
needsImpl: 1
|
||||
- id: caoren_p
|
||||
heroId: caoren
|
||||
kind: passive
|
||||
|
|
@ -354,6 +585,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uBC29 +50 % [\uBC29\uC5B4 \uD0A4 \uC5C6\uC74C \xB7 \uD0A4 \uADFC\uC0AC
|
||||
= \uBC29 \xD71.5 \u2192 \uBC1B\uB294 \uD53C\uD574 \xF71.5 \uD658\uC0B0(\uCD94\uC815)]"
|
||||
effectKey: dmg_taken_pct
|
||||
p1: -0.33
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: yujin_a
|
||||
heroId: yujin
|
||||
kind: active
|
||||
|
|
@ -364,6 +602,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uB2E4\uC74C \uD589\uB3D9: \uC54C\uAE4C\uAE30\uB85C \uBC00\uB9B0 \uC544\uAD70\uC758
|
||||
\uACF5\uACA9 2\uD68C"
|
||||
effectKey: pushed_ally_attack_count
|
||||
p1: 2
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: yujin_p
|
||||
heroId: yujin
|
||||
kind: passive
|
||||
|
|
@ -374,6 +619,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uBC00\uB838\uC744 \uB54C \uBC18\uACA9 1.0 \uBC30 [p2 1=\uBC00\uB838\uC744
|
||||
\uB54C\uB9CC \xB7 \uD0A4 \uADFC\uC0AC]"
|
||||
effectKey: counter_ratio
|
||||
p1: 1
|
||||
p2: 1
|
||||
p3: 0
|
||||
pMask: 3
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: sunjian_a
|
||||
heroId: sunjian
|
||||
kind: active
|
||||
|
|
@ -384,6 +636,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uB2E4\uC74C \uD589\uB3D9: \uC54C\uAE4C\uAE30 \uD53C\uD574 +50 % \xB7
|
||||
\uCD94\uAC00 \uC774\uB3D9\uC5D0\uC11C \uACF5\uACA9 \uAC00\uB2A5"
|
||||
effectKey: dmg_pct;extra_move_attack
|
||||
p1: 0.5
|
||||
p2: 1
|
||||
p3: 0
|
||||
pMask: 3
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: sunjian_p
|
||||
heroId: sunjian
|
||||
kind: passive
|
||||
|
|
@ -394,6 +653,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uBCF4\uC2A4\uC5D0\uAC8C \uC8FC\uB294 \uD53C\uD574 +20 % [p2 1=\uBCF4\uC2A4\uB9CC(0=\uBB34\uC7A5
|
||||
\uC804\uCCB4) \xB7 \uD0A4 \uADFC\uC0AC]"
|
||||
effectKey: dmg_vs_general_mult
|
||||
p1: 1.2
|
||||
p2: 1
|
||||
p3: 0
|
||||
pMask: 3
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: zhouyu_a
|
||||
heroId: zhouyu
|
||||
kind: active
|
||||
|
|
@ -404,6 +670,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uB2E4\uC74C \uD589\uB3D9: \uAD11\uC5ED \uBC18\uACBD \xD72 \xB7 \uAD11\uC5ED
|
||||
\uBE44\uC728 1.0(\uAD11\uC5ED)"
|
||||
effectKey: splash_radius_mult;splash_ratio
|
||||
p1: 2
|
||||
p2: 1
|
||||
p3: 0
|
||||
pMask: 3
|
||||
lvScale: 0.1
|
||||
needsImpl: 1
|
||||
- id: zhouyu_p
|
||||
heroId: zhouyu
|
||||
kind: passive
|
||||
|
|
@ -414,6 +687,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uC544\uAD70 \uAD81\uBCD1\xB7\uCC45\uC0AC \uC0AC\uAC70\uB9AC +0.5 [\uB300\uC0C1
|
||||
= \uAD81\uBCD1\xB7\uCC45\uC0AC]"
|
||||
effectKey: team_range_add
|
||||
p1: 0.5
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: huanggai_a
|
||||
heroId: huanggai
|
||||
kind: active
|
||||
|
|
@ -424,6 +704,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uC989\uC2DC \u2605: \uC790\uAE30 HP 30 % \uC18C\uBAA8 \xB7 \uC774\uBC88
|
||||
\uD134 \uACF5 +60 % [p1 \uACF5 \uC99D\uAC00 \xB7 p2 HP \uC18C\uBAA8]"
|
||||
effectKey: hp_cost_atk
|
||||
p1: 0.6
|
||||
p2: 0.3
|
||||
p3: 0
|
||||
pMask: 3
|
||||
lvScale: 0.1
|
||||
needsImpl: 1
|
||||
- id: huanggai_p
|
||||
heroId: huanggai
|
||||
kind: passive
|
||||
|
|
@ -433,6 +720,13 @@ MonoBehaviour:
|
|||
cooldown: 0
|
||||
lvMax: 5
|
||||
descKo: "\uBC1B\uB294 \uD53C\uD574 \u221215 %"
|
||||
effectKey: dmg_taken_pct
|
||||
p1: -0.15
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: ganning_a
|
||||
heroId: ganning
|
||||
kind: active
|
||||
|
|
@ -446,6 +740,13 @@ MonoBehaviour:
|
|||
\uC2DC \uD06C\uB9AC [\uC804\uC6A9 \uD0A4 \uC5C6\uC74C \xB7 \uD0A4 \uADFC\uC0AC
|
||||
= \uC774\uB3D9(p1 0=\uBAA9\uD45C\uC810 \xB7 \uBC18\uACBD \uC81C\uD55C \uC5C6\uC74C)
|
||||
\uD6C4 \uC0AC\uACA9 + \uD06C\uB9AC]"
|
||||
effectKey: teleport;arrow_crit_always
|
||||
p1: 0
|
||||
p2: 1
|
||||
p3: 0
|
||||
pMask: 3
|
||||
lvScale: 0.1
|
||||
needsImpl: 1
|
||||
- id: ganning_p
|
||||
heroId: ganning
|
||||
kind: passive
|
||||
|
|
@ -456,6 +757,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uADFC\uC811 \uBC18\uACA9\uC744 \uBC1B\uC9C0 \uC54A\uC74C [\uADFC\uC811
|
||||
\uBC18\uACA9 \uD55C\uC815 \xB7 \uD0A4 \uADFC\uC0AC]"
|
||||
effectKey: counter_taken_pct
|
||||
p1: -1
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: chengpu_a
|
||||
heroId: chengpu
|
||||
kind: active
|
||||
|
|
@ -466,6 +774,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uB2E4\uC74C \uD589\uB3D9: \uBC00\uBA70 \uC804\uC9C4 \uAC70\uB9AC \xD71.5
|
||||
[\uBC00\uBA70 \uC804\uC9C4 \uAC70\uB9AC \uBC30\uC728 \xB7 \uD0A4 \uADFC\uC0AC]"
|
||||
effectKey: push_mult
|
||||
p1: 1.5
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: chengpu_p
|
||||
heroId: chengpu
|
||||
kind: passive
|
||||
|
|
@ -475,6 +790,13 @@ MonoBehaviour:
|
|||
cooldown: 0
|
||||
lvMax: 5
|
||||
descKo: "\uACAC\uC81C \uD69F\uC218 2\uD68C"
|
||||
effectKey: overwatch_count
|
||||
p1: 2
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: lvbu_a
|
||||
heroId: lvbu
|
||||
kind: active
|
||||
|
|
@ -485,6 +807,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uB2E4\uC74C \uD589\uB3D9: \uAD00\uD1B5 \uD53C\uD574 \xD72 \xB7 \uBC18\uACA9
|
||||
\uBC1B\uC9C0 \uC54A\uC74C"
|
||||
effectKey: pierce_dmg_mult;pierce_no_counter
|
||||
p1: 2
|
||||
p2: 1
|
||||
p3: 0
|
||||
pMask: 3
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: lvbu_p
|
||||
heroId: lvbu
|
||||
kind: passive
|
||||
|
|
@ -495,6 +824,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uC2AC\uB9C1 \uAC70\uB9AC +1.5 [\uC808\uB300 \uAC00\uC0B0 \uD0A4 \uC5C6\uC74C
|
||||
\xB7 p2 \uC808\uB300 \uAC00\uC0B0 \xB7 \uD0A4 \uADFC\uC0AC]"
|
||||
effectKey: sling_dist_pct
|
||||
p1: 0
|
||||
p2: 1.5
|
||||
p3: 0
|
||||
pMask: 7
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: zhangjiao_a
|
||||
heroId: zhangjiao
|
||||
kind: active
|
||||
|
|
@ -506,6 +842,13 @@ MonoBehaviour:
|
|||
descKo: "\uC989\uC2DC \u2605: \uC774\uBC88 \uC801 \uD134 \uC801 \uC804\uC6D0
|
||||
\uAD50\uC804 \uBC18\uC751 \uC5C6\uC74C(\uC548\uAC1C) [\uB300\uC0C1 = \uC801
|
||||
\uC804\uC6D0]"
|
||||
effectKey: enemy_aggro_off_turn
|
||||
p1: 1
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 1
|
||||
- id: zhangjiao_p
|
||||
heroId: zhangjiao
|
||||
kind: passive
|
||||
|
|
@ -515,6 +858,13 @@ MonoBehaviour:
|
|||
cooldown: 0
|
||||
lvMax: 5
|
||||
descKo: "\uC801 \uAD81\uBCD1\uC5D0\uAC8C \uBC1B\uB294 \uD53C\uD574 \u221230 %"
|
||||
effectKey: dmg_taken_from_archer_pct
|
||||
p1: -0.3
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: huaxiong_a
|
||||
heroId: huaxiong
|
||||
kind: active
|
||||
|
|
@ -525,6 +875,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uB2E4\uC74C \uD589\uB3D9: \uC54C\uAE4C\uAE30 \uB300\uC0C1\uC774 \uBB34\uC7A5(\uC601\uC6C5\xB7\uBCF4\uC2A4)\uC774\uBA74
|
||||
\uD53C\uD574 \xD72 [p2 0=\uBB34\uC7A5 \uC804\uCCB4(\uC601\uC6C5\xB7\uBCF4\uC2A4)]"
|
||||
effectKey: dmg_vs_general_mult
|
||||
p1: 2
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 3
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: huaxiong_p
|
||||
heroId: huaxiong
|
||||
kind: passive
|
||||
|
|
@ -534,6 +891,13 @@ MonoBehaviour:
|
|||
cooldown: 0
|
||||
lvMax: 5
|
||||
descKo: "1\uD134\uC9F8 \uACF5 +30 %"
|
||||
effectKey: dmg_pct_turn1
|
||||
p1: 0.3
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: yanliang_a
|
||||
heroId: yanliang
|
||||
kind: active
|
||||
|
|
@ -544,6 +908,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uB2E4\uC74C \uD589\uB3D9: \uBA85\uC911 \uC2DC \uACAC\uC81C \uBC18\uACBD
|
||||
+1.5"
|
||||
effectKey: overwatch_radius_add
|
||||
p1: 1.5
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: yanliang_p
|
||||
heroId: yanliang
|
||||
kind: passive
|
||||
|
|
@ -554,6 +925,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uACAC\uC81C\uB85C \uC801\uC744 \uC4F0\uB7EC\uB728\uB9AC\uBA74 HP 20
|
||||
% \uD68C\uBCF5"
|
||||
effectKey: heal_on_overwatch_kill
|
||||
p1: 0.2
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: wenchou_a
|
||||
heroId: wenchou
|
||||
kind: active
|
||||
|
|
@ -564,6 +942,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uB2E4\uC74C \uD589\uB3D9: \uAD00\uD1B5 \uBC00\uAE30 \xD72(\uACBD\uB85C\uB97C
|
||||
\uBE44\uC6B4\uB2E4)"
|
||||
effectKey: pierce_knockback_mult
|
||||
p1: 2
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: wenchou_p
|
||||
heroId: wenchou
|
||||
kind: passive
|
||||
|
|
@ -574,6 +959,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uC544\uAD70 1\uAE30 \uC804\uC0AC\uB9C8\uB2E4 \uACF5 +20 %(\uC911\uCCA9)
|
||||
[\uC911\uCCA9]"
|
||||
effectKey: on_ally_death_atk
|
||||
p1: 0.2
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: jiling_a
|
||||
heroId: jiling
|
||||
kind: active
|
||||
|
|
@ -585,6 +977,13 @@ MonoBehaviour:
|
|||
descKo: "\uB2E4\uC74C \uD589\uB3D9: \uBC00\uBA70 \uC804\uC9C4 \uB05D\uC5D0 \uB2FF\uC740
|
||||
\uC801 2\uAE30\uAE4C\uC9C0 \uB3D9\uC2DC \uD53C\uD574 [p1 \uB3D9\uC2DC \uD53C\uD574
|
||||
\uCD5C\uB300 \uC218]"
|
||||
effectKey: spear_hit_two
|
||||
p1: 2
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: jiling_p
|
||||
heroId: jiling
|
||||
kind: passive
|
||||
|
|
@ -595,6 +994,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uBC00\uACE0 \uAC04 \uC544\uAD70\uB3C4 \uADF8 \uC790\uB9AC\uC5D0\uC11C
|
||||
\uACF5\uACA9 1\uD68C"
|
||||
effectKey: pushed_ally_attack_on_push
|
||||
p1: 1
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: gaoshun_a
|
||||
heroId: gaoshun
|
||||
kind: active
|
||||
|
|
@ -606,6 +1012,13 @@ MonoBehaviour:
|
|||
descKo: "\uB2E4\uC74C \uD589\uB3D9: \uBC00\uAE30 \xD72 \xB7 \uBC00\uB9B0 \uC801\uC774
|
||||
\uBCBD\xB7\uC720\uB2DB\uC5D0 \uB2FF\uC73C\uBA74 +30 % \uD53C\uD574 [p2 \uBCBD\xB7\uC720\uB2DB
|
||||
\uCDA9\uB3CC \uCD94\uAC00 \uD53C\uD574 \xB7 \uCDA9\uB3CC \uD0A4 \uC5C6\uC74C]"
|
||||
effectKey: push_mult
|
||||
p1: 2
|
||||
p2: 0.3
|
||||
p3: 0
|
||||
pMask: 3
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: gaoshun_p
|
||||
heroId: gaoshun
|
||||
kind: passive
|
||||
|
|
@ -615,6 +1028,13 @@ MonoBehaviour:
|
|||
cooldown: 0
|
||||
lvMax: 5
|
||||
descKo: "\uBC18\uACA9 1.0 \uBC30"
|
||||
effectKey: counter_ratio
|
||||
p1: 1
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: gongsunzan_a
|
||||
heroId: gongsunzan
|
||||
kind: active
|
||||
|
|
@ -625,6 +1045,13 @@ MonoBehaviour:
|
|||
lvMax: 5
|
||||
descKo: "\uB2E4\uC74C \uD589\uB3D9: \uAD00\uD1B5\uC73C\uB85C \uB9DE\uD78C \uC801
|
||||
\uC804\uBD80 \uBC00\uAE30 \xD72"
|
||||
effectKey: pierce_knockback_mult
|
||||
p1: 2
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
- id: gongsunzan_p
|
||||
heroId: gongsunzan
|
||||
kind: passive
|
||||
|
|
@ -634,6 +1061,13 @@ MonoBehaviour:
|
|||
cooldown: 0
|
||||
lvMax: 5
|
||||
descKo: "\uC801 \uAD81\uBCD1\uC5D0\uAC8C \uBC1B\uB294 \uD53C\uD574 \u221230 %"
|
||||
effectKey: dmg_taken_from_archer_pct
|
||||
p1: -0.3
|
||||
p2: 0
|
||||
p3: 0
|
||||
pMask: 1
|
||||
lvScale: 0.1
|
||||
needsImpl: 0
|
||||
levels:
|
||||
- lv: 1
|
||||
books: 0
|
||||
|
|
|
|||
|
|
@ -87,6 +87,23 @@ namespace TKS.EditorTools
|
|||
EditorApplication.EnterPlaymode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 스킬 화면 검증 캡처(GUI 에디터 · 사본 프로젝트 권장 · 세이브 = 메모리 전용 · PD #1093 B):
|
||||
/// Unity.exe -projectPath <copy> -executeMethod TKS.EditorTools.DataImporter.CaptureSkills -skillDir <dir>
|
||||
/// 스킬 레벨업 · 스킬 버튼 · 액티브 발동 · 패시브 표시 PNG 4장 + log · 끝나면 스스로 종료.
|
||||
/// </summary>
|
||||
public static void CaptureSkills()
|
||||
{
|
||||
string dir = GetArg("-skillDir");
|
||||
if (string.IsNullOrEmpty(dir)) dir = Path.Combine(Directory.GetCurrentDirectory(), "Logs", "skill_capture");
|
||||
ImportAll();
|
||||
EditorSceneManager.OpenScene(ProjectSetup.ScenePath, OpenSceneMode.Single);
|
||||
SessionState.SetBool(SaveStore.MemoryFlagKey, true);
|
||||
SessionState.SetString(SkillCaptureDriver.DirKey, dir);
|
||||
Debug.Log("[TKS] 스킬 캡처 시작 → " + dir);
|
||||
EditorApplication.EnterPlaymode();
|
||||
}
|
||||
|
||||
static string GetArg(string name)
|
||||
{
|
||||
var args = System.Environment.GetCommandLineArgs();
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ namespace TKS.OutGame
|
|||
public string[] ShopGrades { get { return values.List("shop_grades"); } }
|
||||
public float ShopPriceMult { get { return values.F("shop_price_mult"); } }
|
||||
public int ShopRefreshGem { get { return values.I("shop_refresh_gem"); } }
|
||||
// 스킬(PD #1093 B) — 신규 유저 스킬서 · 거래 상점 스킬서 임시 판매(획득처 사당 던전 = C 단계 · PD 확인 대상)
|
||||
public int StartSkillBooks { get { return values.I("start_skill_books"); } }
|
||||
public int SkillBookShopDaily { get { return values.I("skill_book_shop_daily"); } }
|
||||
public int SkillBookPrice { get { return values.I("skill_book_price"); } }
|
||||
|
||||
/// <summary>중복 영웅 → 조각(희귀도 1~3).</summary>
|
||||
public int ShardDup(int rarity)
|
||||
|
|
@ -54,6 +58,7 @@ namespace TKS.OutGame
|
|||
"gear_bonus_pct", "power_hp_div", "power_atk_w", "power_def_w",
|
||||
"gold_per_stage", "first_clear_gold_mult", "boss_gear_drop_pct",
|
||||
"shop_slots", "shop_grades", "shop_price_mult", "shop_refresh_gem",
|
||||
"start_skill_books", "skill_book_shop_daily", "skill_book_price",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ using UnityEngine;
|
|||
namespace TKS.OutGame
|
||||
{
|
||||
/// <summary>
|
||||
/// 스킬 표시 정보(skills.csv 이름·desc·쿨 + skill_levels.csv) — A 단계는 영웅 화면 표시만. 효과 처리기·effect_key·p1~p3 는 B 단계(스킬 세션)가 같은 CSV 로 만든다.
|
||||
/// 스킬 표(skills.csv 전 열 + skill_levels.csv) = 스킬 DB. 영웅 화면 표시(A) + 전투 처리기(B · Runtime/Skills 의 SkillCatalog 가 effect_key·p1~p3·lv_scale 을 읽음).
|
||||
/// skill_levels.csv(lv · books · gold) = levels(스킬 레벨 표 · 스킬 레벨업 비용).
|
||||
/// </summary>
|
||||
public class SkillInfoDb : ScriptableObject
|
||||
{
|
||||
|
|
@ -23,6 +24,32 @@ namespace TKS.OutGame
|
|||
public int lvMax;
|
||||
[Tooltip("desc_ko (끝 [ ] 메모 포함 원문)")]
|
||||
public string descKo;
|
||||
[Tooltip("effect_key 원문(효과 여럿 = ; 로 나열 · p1~p3 = 키 순서)")]
|
||||
public string effectKey;
|
||||
public float p1;
|
||||
public float p2;
|
||||
public float p3;
|
||||
[Tooltip("p 칸이 비어 있지 않은가(bit0 = p1 · bit1 = p2 · bit2 = p3)")]
|
||||
public int pMask;
|
||||
[Tooltip("수치 × (1 + lv_scale·(lv−1))")]
|
||||
public float lvScale;
|
||||
[Tooltip("신규 처리기(반경 안 유닛 · HP 소모 · 이동 · 교전 반응 정지)")]
|
||||
public bool needsImpl;
|
||||
|
||||
public string[] EffectKeys
|
||||
{
|
||||
get { return string.IsNullOrEmpty(effectKey) ? new string[0] : effectKey.Split(';'); }
|
||||
}
|
||||
|
||||
public bool HasP(int i)
|
||||
{
|
||||
return (pMask & (1 << i)) != 0;
|
||||
}
|
||||
|
||||
public float P(int i)
|
||||
{
|
||||
return i == 0 ? p1 : i == 1 ? p2 : p3;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
|
|
@ -44,6 +71,24 @@ namespace TKS.OutGame
|
|||
return null;
|
||||
}
|
||||
|
||||
/// <summary>그 레벨이 되는 비용(skill_levels.csv lv 행 · 없으면 null = 최고).</summary>
|
||||
public LevelCost CostTo(int lv)
|
||||
{
|
||||
foreach (var c in levels)
|
||||
if (c.lv == lv) return c;
|
||||
return null;
|
||||
}
|
||||
|
||||
public int MaxLevel
|
||||
{
|
||||
get
|
||||
{
|
||||
int m = 1;
|
||||
foreach (var c in levels) m = Mathf.Max(m, c.lv);
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>표시용 설명 — 끝의 [p1 … ] 개발 메모를 뺀다.</summary>
|
||||
public static string DisplayDesc(Skill s)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ namespace TKS.OutGame
|
|||
{
|
||||
var e = db.Economy;
|
||||
var d = new SaveData { gold = e.StartGold, gem = e.StartGem, stamina = e.StartStamina, staminaAt = SaveStore.Iso(nowUtc), chapterMax = 1 };
|
||||
d.inv.books.skill = e.StartSkillBooks; // 신규 유저 스킬서(PD #1093 B)
|
||||
int gearSlots = GearSlotCount(db);
|
||||
var lineup = new List<string>();
|
||||
foreach (var id in e.StartHeroes)
|
||||
|
|
|
|||
|
|
@ -192,11 +192,13 @@ namespace TKS.OutGame
|
|||
if (h == null || !Save.Owns(id)) continue;
|
||||
var st = StatsOf(id);
|
||||
float g = st.Growth;
|
||||
res.Add(new LineupEntry
|
||||
var entry = new LineupEntry
|
||||
{
|
||||
slot = i, id = h.id, name = h.nameKo, type = h.type, art = h.art,
|
||||
hpMult = h.hpMult * g, atkMult = h.atkMult * g, defMult = h.defMult * g,
|
||||
});
|
||||
};
|
||||
SkillRules.FillLineup(Db, Save, h, entry); // 스킬 id·레벨·★ 패시브 배율(PD #1093 B)
|
||||
res.Add(entry);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ namespace TKS.OutGame
|
|||
{
|
||||
var t = ScriptableObject.CreateInstance<SkillInfoDb>();
|
||||
const string f = "skills.csv";
|
||||
if (Cols(rows, f, e, "id", "hero_id", "kind", "name_ko", "hanja", "trigger", "effect_key", "cooldown", "lv_max", "desc_ko"))
|
||||
if (Cols(rows, f, e, "id", "hero_id", "kind", "name_ko", "hanja", "trigger", "effect_key", "p1", "p2", "p3", "cooldown", "lv_max", "lv_scale", "desc_ko", "needs_impl"))
|
||||
{
|
||||
var seen = new HashSet<string>();
|
||||
foreach (var r in rows)
|
||||
|
|
@ -221,9 +221,39 @@ namespace TKS.OutGame
|
|||
{
|
||||
id = Get(r, "id"), heroId = Get(r, "hero_id"), kind = Get(r, "kind"), nameKo = Get(r, "name_ko"), hanja = Get(r, "hanja"),
|
||||
trigger = Get(r, "trigger"), cooldown = Int(r, "cooldown", f, e), lvMax = Int(r, "lv_max", f, e), descKo = Get(r, "desc_ko"),
|
||||
effectKey = Get(r, "effect_key"), lvScale = Float(r, "lv_scale", f, e),
|
||||
};
|
||||
if (!Unique(seen, s.id, f, r, e)) continue;
|
||||
if (s.kind != "active" && s.kind != "passive") e.Add(f + " 행 " + Get(r, "#") + ": kind 「" + s.kind + "」 ≠ active|passive");
|
||||
string at = f + " 행 " + Get(r, "#") + " " + s.id;
|
||||
if (s.kind != "active" && s.kind != "passive") e.Add(at + ": kind 「" + s.kind + "」 ≠ active|passive");
|
||||
// 스킬 처리기 열(B 단계) — trigger · effect_key 51종 · p 칸 · needs_impl
|
||||
bool active = s.kind == "active";
|
||||
if (active ? (s.trigger != "next_action" && s.trigger != "instant") : s.trigger != "always")
|
||||
e.Add(at + ": trigger 「" + s.trigger + "」 (active = next_action|instant · passive = always)");
|
||||
var ni = Get(r, "needs_impl");
|
||||
if (ni != "0" && ni != "1") e.Add(at + ": needs_impl ≠ 0|1");
|
||||
s.needsImpl = ni == "1";
|
||||
var cols = new[] { "p1", "p2", "p3" };
|
||||
var ps = new float[3];
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var v = Get(r, cols[i]);
|
||||
if (v.Length == 0) continue;
|
||||
if (!float.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out ps[i])) e.Add(at + ": " + cols[i] + " 숫자 아님(「" + v + "」)");
|
||||
else s.pMask |= 1 << i;
|
||||
}
|
||||
s.p1 = ps[0];
|
||||
s.p2 = ps[1];
|
||||
s.p3 = ps[2];
|
||||
var keys = s.EffectKeys;
|
||||
if (keys.Length == 0 || keys.Length > 3) e.Add(at + ": effect_key 수 " + keys.Length + " (1~3)");
|
||||
for (int i = 0; i < keys.Length; i++)
|
||||
{
|
||||
if (!SkillKeys.Known(keys[i])) e.Add(at + ": effect_key 「" + keys[i] + "」 처리기 없음");
|
||||
if (i < 3 && !s.HasP(i) && (keys.Length > 1 || i == 0)) e.Add(at + ": effect_key 「" + keys[i] + "」 의 p" + (i + 1) + " 비었음");
|
||||
}
|
||||
if (active && s.cooldown < 1) e.Add(at + ": 액티브 cooldown < 1");
|
||||
if (s.lvMax < 1) e.Add(at + ": lv_max < 1");
|
||||
t.skills.Add(s);
|
||||
}
|
||||
}
|
||||
|
|
@ -231,6 +261,10 @@ namespace TKS.OutGame
|
|||
if (Cols(lvRows, g, e, "lv", "books", "gold"))
|
||||
foreach (var r in lvRows)
|
||||
t.levels.Add(new SkillInfoDb.LevelCost { lv = Int(r, "lv", g, e), books = Int(r, "books", g, e), gold = Int(r, "gold", g, e) });
|
||||
for (int i = 0; i < t.levels.Count; i++)
|
||||
if (t.levels[i].lv != i + 1) e.Add(g + ": lv 가 1부터 연속이 아님(" + (i + 1) + "번째 = " + t.levels[i].lv + ")");
|
||||
foreach (var s in t.skills)
|
||||
if (s.lvMax > t.levels.Count) e.Add(f + ": " + s.id + " lv_max " + s.lvMax + " > skill_levels.csv 행 " + t.levels.Count);
|
||||
return t;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -129,6 +129,9 @@ namespace TKS.OutGame
|
|||
public string day = "";
|
||||
public List<ShopItem> items = new List<ShopItem>();
|
||||
public int refreshes;
|
||||
[Tooltip("스킬서 임시 판매(PD #1093 B) — 구매한 날(yyyyMMdd) · 그날 구매 수")]
|
||||
public string bookDay = "";
|
||||
public int bookBought;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
|
|
|
|||
|
|
@ -217,8 +217,7 @@ namespace TKS.OutGame
|
|||
float bw = (1032f - 40f) / 3f;
|
||||
UiKit.Btn("Detail", btnRow, "상세정보", 36, UiKit.Dark, UiKit.White, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), Vector2.zero, new Vector2(bw, 96f), ShowStats);
|
||||
UiKit.Btn("Codex", btnRow, "도감", 36, UiKit.Dark, UiKit.White, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(bw, 96f), () => ShowCodex(Root));
|
||||
skillBtn = UiKit.Btn("SkillUp", btnRow, "스킬 레벨업\n<size=22>B 단계(준비 중)</size>", 32, UiKit.Red, UiKit.White, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), Vector2.zero, new Vector2(bw, 96f), null);
|
||||
skillBtn.interactable = false;
|
||||
skillBtn = UiKit.Btn("SkillUp", btnRow, "스킬 레벨업", 32, UiKit.Red, UiKit.White, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), Vector2.zero, new Vector2(bw, 96f), OnSkillUp);
|
||||
}
|
||||
|
||||
void Step(int d)
|
||||
|
|
@ -275,9 +274,9 @@ namespace TKS.OutGame
|
|||
starText.text = h.isSoldier ? UiKit.Stars(h.rarity, 3) : UiKit.Stars(hs.star, db.Evolve.MaxStar);
|
||||
|
||||
var skills = new[] { db.Skills.Get(h.skillActive), db.Skills.Get(h.skillPassive) };
|
||||
int cap = HeroRules.SkillCap(db, hs);
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
int cap = SkillRules.Cap(db, h, hs, i); // 등급 + ★3 액티브 +1(PD #1093 B)
|
||||
var sk = skills[i];
|
||||
if (sk == null)
|
||||
{
|
||||
|
|
@ -290,8 +289,11 @@ namespace TKS.OutGame
|
|||
skillName[i].text = sk.nameKo + (string.IsNullOrEmpty(sk.hanja) ? "" : "(" + sk.hanja + ")") + " · " + (sk.kind == "active" ? "액티브" : "패시브");
|
||||
skillDesc[i].text = SkillInfoDb.DisplayDesc(sk);
|
||||
int slv = hs != null && hs.skillLv != null && hs.skillLv.Length > i ? hs.skillLv[i] : 1;
|
||||
skillChip[i].text = "Lv " + slv + "/" + cap + (sk.kind == "active" ? " · 쿨 " + sk.cooldown + "턴" : "");
|
||||
skillChip[i].text = "Lv " + slv + "/" + cap + (sk.kind == "active" ? " · 쿨 " + sk.cooldown + "턴" : (hs != null && SkillRules.PassiveMult(db, hs.star) > 1f ? " · ★×" + SkillRules.PassiveMult(db, hs.star).ToString("0.##") : ""));
|
||||
}
|
||||
bool canSkill = !h.isSoldier && hs != null && (SkillRules.CanLevelUp(db, S.Save, h, hs, 0) == null || SkillRules.CanLevelUp(db, S.Save, h, hs, 1) == null);
|
||||
UiKit.LabelOf(skillBtn).text = "스킬 레벨업\n<size=22>" + (h.isSoldier ? "병사 = 스킬 없음" : "스킬서 " + S.Save.inv.books.skill) + "</size>";
|
||||
skillBtn.GetComponent<Image>().color = canSkill ? UiKit.Red : UiKit.Disabled;
|
||||
|
||||
int lvCap = HeroRules.LevelCap(db, S.Save);
|
||||
var row = db.Levels.Get(lv.lv);
|
||||
|
|
@ -366,6 +368,43 @@ namespace TKS.OutGame
|
|||
Root.Toast(why ?? h.nameKo + " 승급 → " + UiKit.GradeKo(S.Save.Hero(id).grade) + " 등급 · 새 장비 6칸");
|
||||
}
|
||||
|
||||
/// <summary>스킬 레벨업(PD #1093 B): 액티브·패시브 중 고르기 → 스킬서 + 골드(skill_levels.csv) · 상한 = 등급 + ★3 액티브 +1.</summary>
|
||||
public void OnSkillUp()
|
||||
{
|
||||
var h = S.Def(id);
|
||||
var hs = S.Save.Hero(id);
|
||||
if (h == null || hs == null || h.isSoldier)
|
||||
{
|
||||
Root.Toast("병사 유닛은 스킬이 없습니다");
|
||||
return;
|
||||
}
|
||||
var db = S.Db;
|
||||
var buttons = new List<OutGameRoot.ModalButton>();
|
||||
var body = new System.Text.StringBuilder("보유 스킬서 " + S.Save.inv.books.skill + " · 골드 " + S.Save.gold.ToString("N0") + "\n");
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
int k = i;
|
||||
var sk = SkillRules.Skill(db, h, i);
|
||||
if (sk == null) continue;
|
||||
int lv = SkillRules.Level(hs, i);
|
||||
var cost = db.Skills.CostTo(lv + 1);
|
||||
var why = SkillRules.CanLevelUp(db, S.Save, h, hs, i);
|
||||
body.Append("\n").Append(i == 0 ? "액티브 " : "패시브 ").Append(sk.nameKo).Append(" Lv ").Append(lv).Append(" / ").Append(SkillRules.Cap(db, h, hs, i));
|
||||
if (cost != null && lv < sk.lvMax) body.Append(" → 스킬서 ").Append(cost.books).Append(" · 골드 ").Append(cost.gold.ToString("N0"));
|
||||
if (why != null) body.Append("\n <color=#a03a22>").Append(why).Append("</color>");
|
||||
buttons.Add(new OutGameRoot.ModalButton((i == 0 ? "액티브" : "패시브") + " Lv+1", why == null ? UiKit.Red : UiKit.Disabled, () =>
|
||||
{
|
||||
var r = SkillRules.LevelUp(S.Db, S.Save, h, hs, k);
|
||||
if (r == null) S.Commit();
|
||||
Refresh();
|
||||
if (r == null) OnSkillUp(); // 이어서 올릴 수 있게 다시 연다
|
||||
Root.Toast(r ?? sk.nameKo + " Lv " + SkillRules.Level(hs, k) + "!");
|
||||
}));
|
||||
}
|
||||
buttons.Add(new OutGameRoot.ModalButton("닫기", UiKit.Dark, null));
|
||||
Root.Modal("스킬 레벨업 — " + h.nameKo, body.ToString(), buttons.ToArray());
|
||||
}
|
||||
|
||||
void OnEvolve()
|
||||
{
|
||||
var why = S.Evolve(id);
|
||||
|
|
|
|||
|
|
@ -44,9 +44,24 @@ namespace TKS.OutGame
|
|||
get { return resultRoot != null && resultRoot.activeSelf; }
|
||||
}
|
||||
|
||||
/// <summary>장비 상점 칸 수(스킬서 임시 판매 칸 제외).</summary>
|
||||
public int ShopCardCount
|
||||
{
|
||||
get { return shopGrid.childCount; }
|
||||
get
|
||||
{
|
||||
int n = 0;
|
||||
foreach (Transform c in shopGrid)
|
||||
if (c.name != SkillBookCellName) n++;
|
||||
return n;
|
||||
}
|
||||
}
|
||||
|
||||
public const string SkillBookCellName = "SkillBook";
|
||||
|
||||
/// <summary>스킬서 판매 칸이 있는가(PD #1093 B).</summary>
|
||||
public bool SkillBookCellVisible
|
||||
{
|
||||
get { return shopGrid != null && shopGrid.Find(SkillBookCellName) != null; }
|
||||
}
|
||||
|
||||
public override void Build()
|
||||
|
|
@ -306,6 +321,30 @@ namespace TKS.OutGame
|
|||
});
|
||||
b.interactable = !it.sold;
|
||||
}
|
||||
AddSkillBookCell(d);
|
||||
}
|
||||
|
||||
/// <summary>스킬서 임시 판매(PD #1093 B · 하루 skill_book_shop_daily 개 · 골드 skill_book_price · 정식 획득처 = 사당 던전(C) · PD 확인 대상).</summary>
|
||||
void AddSkillBookCell(SaveData d)
|
||||
{
|
||||
var e = S.Db.Economy;
|
||||
int left = Mathf.Max(0, e.SkillBookShopDaily - SkillRules.BooksBoughtToday(d, S.Now));
|
||||
var cell = UiKit.Cell(shopGrid, SkillBookCellName, left > 0 ? UiKit.Paper : new Color(0.86f, 0.84f, 0.8f), null);
|
||||
UiKit.Border(cell, UiKit.Line, 2f);
|
||||
var strip = UiKit.TopBand("Grade", cell.transform, 0f, 64f);
|
||||
UiKit.Paint(strip, UiKit.Purple);
|
||||
UiKit.FillLabel(strip, "스킬서 · 임시 판매", 28, UiKit.White);
|
||||
UiKit.Label(cell.transform, "스킬서", 40, UiKit.Ink, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -86f), new Vector2(310f, 70f));
|
||||
UiKit.Label(cell.transform, "보유 " + d.inv.books.skill + " · 오늘 " + left + "/" + e.SkillBookShopDaily, 24, UiKit.InkSoft, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -160f), new Vector2(310f, 44f), false);
|
||||
UiKit.Label(cell.transform, "골드 " + e.SkillBookPrice.ToString("N0"), 36, UiKit.Coin, TextAnchor.MiddleCenter, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 120f), new Vector2(310f, 56f));
|
||||
var b = UiKit.Btn("BuyBook", cell.transform, left > 0 ? "구매" : "품절", 34, left > 0 ? UiKit.Red : UiKit.Disabled, UiKit.White, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 20f), new Vector2(260f, 84f), () =>
|
||||
{
|
||||
var why = SkillRules.BuyBook(S.Db, S.Save, S.Now);
|
||||
if (why == null) S.Commit();
|
||||
Root.Toast(why ?? "스킬서 +1 (보유 " + S.Save.inv.books.skill + ")");
|
||||
Refresh();
|
||||
});
|
||||
b.interactable = left > 0;
|
||||
}
|
||||
|
||||
/// <summary>그 장비를 지금 등급 칸에서 기다리는(아직 안 채운) 보유 영웅 수.</summary>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,22 @@ namespace TKS
|
|||
public List<string> Notes = new List<string>();
|
||||
/// <summary>실제로 지나간 경로(시작 포함). 창병 견제 판정용.</summary>
|
||||
public List<Vector2> Path = new List<Vector2>();
|
||||
|
||||
// ---------- 스킬(PD #1093 B) ----------
|
||||
/// <summary>이 행동에 걸린 예약 액티브(next_action) · null = 없음.</summary>
|
||||
public SkillDef SkillUsed;
|
||||
/// <summary>추가 이동 횟수(기본 1 · 인덕 extra_move_count) · 추가 이동에서 공격 가능(extra_move_attack).</summary>
|
||||
public int BonusMoves = 1;
|
||||
public bool BonusAttack;
|
||||
/// <summary>이 행동으로 밀리거나 넉백된 유닛의 이동 구간(청룡언월 「견제 무제한」 판정).</summary>
|
||||
public List<Displacement> Displaced = new List<Displacement>();
|
||||
}
|
||||
|
||||
public struct Displacement
|
||||
{
|
||||
public Unit Unit;
|
||||
public Vector2 From;
|
||||
public Vector2 To;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -38,6 +54,7 @@ namespace TKS
|
|||
/// 첫 피격 = 타겟 + 목표점 명중 시 크리티컬(PD 2026-11-05 #1087 ⑥) · 근접 전투는 피해 50 % · 피격 +50 %(Combat.Damage)
|
||||
/// ④' 책사: 궁병과 같은 사격 + 맞은 유닛 주변 반경 안 다른 유닛 전원(아군 포함)에게 광역 피해(PD 2026-11-05 #1087 ⑤)
|
||||
/// ⑤ 밀린 아군(행동 유닛과 같은 진영)이 상대 유닛과 닿으면 그 자리에서 공격 1회(조작마다 · 자기 행동은 소모하지 않음)
|
||||
/// 스킬(PD #1093 B): 수치는 SkillMods 질의(밀기·튕김·관통·사거리·사선·광역·피해) · 예약 액티브는 Resolve 앞뒤에서 장착·소모(SkillSystem).
|
||||
/// </summary>
|
||||
public class ActionResolver
|
||||
{
|
||||
|
|
@ -61,6 +78,8 @@ namespace TKS
|
|||
if (dist < 0.001f || !u.IsAlive) yield break;
|
||||
Vector2 dir = delta / dist;
|
||||
|
||||
var skills = SkillSystem.Current;
|
||||
if (skills != null) skills.BeginAction(u, moveOnly, o); // 예약 액티브 장착 + 발동 연출(PD #1093 B)
|
||||
// 슬링샷 돌진 중 = 병종 돌진 포즈 1프레임 고정 · 끝나면 대기 (PD 2026-11-05 #1084)
|
||||
if (u.View != null) u.View.SetCharging(true);
|
||||
switch (u.Type)
|
||||
|
|
@ -72,7 +91,7 @@ namespace TKS
|
|||
yield return SpearRoutine(u, origin, dir, dist, target, moveOnly, o);
|
||||
break;
|
||||
case UnitType.Cavalry:
|
||||
yield return CavalryRoutine(u, origin, dir, dist, target, moveOnly, o);
|
||||
yield return CavalryRoutine(u, origin, dir, dist, target, moveOnly, o, true);
|
||||
break;
|
||||
case UnitType.Archer:
|
||||
case UnitType.Strategist:
|
||||
|
|
@ -80,6 +99,7 @@ namespace TKS
|
|||
break;
|
||||
}
|
||||
if (u.View != null) u.View.SetCharging(false);
|
||||
if (skills != null) skills.EndAction(u, o); // 예약 소모 · 쿨 · 견제 강화 이월
|
||||
}
|
||||
|
||||
// ---------- 미리보기(드라이런) ----------
|
||||
|
|
@ -95,36 +115,45 @@ namespace TKS
|
|||
if (dist < 0.001f) return result;
|
||||
Vector2 dir = delta / dist;
|
||||
float hitDist;
|
||||
switch (u.Type)
|
||||
var skills = SkillSystem.Current;
|
||||
bool prev = skills != null && skills.PreviewBegin(u);
|
||||
try
|
||||
{
|
||||
case UnitType.Infantry:
|
||||
case UnitType.Spear:
|
||||
switch (u.Type)
|
||||
{
|
||||
var first = FirstUnitBeforeWall(u, origin, dir, dist, out hitDist);
|
||||
if (first != null && first.Faction != u.Faction) result.Add(first);
|
||||
break;
|
||||
}
|
||||
case UnitType.Cavalry:
|
||||
{
|
||||
float travel = Combat.FreeTravel(u, origin, dir, dist, false, false);
|
||||
travel = StopBeforeUnclearedAllies(u, origin, dir, travel);
|
||||
var dists = new List<float>();
|
||||
result.AddRange(Combat.EnemiesOnPath(u, origin, dir, travel, dists));
|
||||
break;
|
||||
}
|
||||
case UnitType.Archer:
|
||||
case UnitType.Strategist:
|
||||
{
|
||||
var p = PlanShot(u, origin, dir, target);
|
||||
if (!p.Fires) break;
|
||||
if (p.Hit != null && p.Hit.Faction != u.Faction) result.Add(p.Hit);
|
||||
foreach (var s in p.Splash)
|
||||
case UnitType.Infantry:
|
||||
case UnitType.Spear:
|
||||
{
|
||||
if (s.Faction != u.Faction) result.Add(s);
|
||||
var first = FirstUnitBeforeWall(u, origin, dir, dist, out hitDist);
|
||||
if (first != null && first.Faction != u.Faction) result.Add(first);
|
||||
break;
|
||||
}
|
||||
case UnitType.Cavalry:
|
||||
{
|
||||
float travel = Combat.FreeTravel(u, origin, dir, dist, false, false);
|
||||
travel = StopBeforeUnclearedAllies(u, origin, dir, travel);
|
||||
var dists = new List<float>();
|
||||
result.AddRange(Combat.EnemiesOnPath(u, origin, dir, travel, dists));
|
||||
break;
|
||||
}
|
||||
case UnitType.Archer:
|
||||
case UnitType.Strategist:
|
||||
{
|
||||
var p = PlanShot(u, origin, dir, target);
|
||||
if (!p.Fires) break;
|
||||
if (p.Hit != null && p.Hit.Faction != u.Faction) result.Add(p.Hit);
|
||||
foreach (var s in p.Splash)
|
||||
{
|
||||
if (s.Faction != u.Faction) result.Add(s);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (skills != null) skills.PreviewEnd(u, prev);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +164,16 @@ namespace TKS
|
|||
Vector2 delta = target - u.Position;
|
||||
float dist = delta.magnitude;
|
||||
if (dist < 0.001f) return null;
|
||||
return PlanShot(u, u.Position, delta / dist, target);
|
||||
var skills = SkillSystem.Current;
|
||||
bool prev = skills != null && skills.PreviewBegin(u);
|
||||
try
|
||||
{
|
||||
return PlanShot(u, u.Position, delta / dist, target);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (skills != null) skills.PreviewEnd(u, prev);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 공통 질의 ----------
|
||||
|
|
@ -178,9 +216,10 @@ namespace TKS
|
|||
bool enemy = first.Faction != u.Faction;
|
||||
bool isFinal = enemy && Combat.EnemyAtPoint(u, target, cfg.finalHitTolerance) == first;
|
||||
|
||||
int impact = 0;
|
||||
if (enemy)
|
||||
{
|
||||
ImpactExchange(u, first, o); // 부딪힌 순간 피해·반격을 즉시 주고받는다(연출 대기 없음)
|
||||
impact = ImpactExchange(u, first, o); // 부딪힌 순간 피해·반격을 즉시 주고받는다(연출 대기 없음)
|
||||
if (!u.IsAlive) yield break;
|
||||
}
|
||||
else
|
||||
|
|
@ -191,10 +230,15 @@ namespace TKS
|
|||
// 알까기: 부딪힌 순간 양쪽이 동시에 튕긴다 — 상대 = 접촉 방향 × 남은 힘 × collisionPushRatio · 자신 = 반대 방향 × 남은 힘 × collisionSelfBounceRatio
|
||||
Vector2 n = first.Position - contact;
|
||||
n = n.sqrMagnitude > 1e-6f ? n.normalized : dir;
|
||||
yield return Bounce(u, first, n, remaining, o);
|
||||
if (!enemy && first.IsAlive) yield return PushedAllyAttack(first, o);
|
||||
yield return Bounce(u, first, n, remaining, o, enemy ? impact : 0);
|
||||
if (!enemy && first.IsAlive) yield return PushedAllyAttack(first, o, u);
|
||||
|
||||
if (isFinal && u.IsAlive) o.BonusMove = true;
|
||||
if (isFinal && u.IsAlive)
|
||||
{
|
||||
o.BonusMove = true;
|
||||
o.BonusMoves = Mathf.Max(1, SkillMods.Count(u, SkillKeys.ExtraMoveCount, 1)); // 인덕: 추가 이동 n 회
|
||||
o.BonusAttack = SkillMods.Has(u, SkillKeys.ExtraMoveAttack); // 호치·강동의 호랑이: 추가 이동에서 공격
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 창병: 밀며 함께 전진 ----------
|
||||
|
|
@ -231,13 +275,16 @@ namespace TKS
|
|||
o.MovedOnly = true;
|
||||
}
|
||||
|
||||
if (first.IsAlive && remaining > 0.01f)
|
||||
// 밀며 전진 거리 = 남은 거리 × 철척사모(push_mult) × 대상 거구(knockback_taken_pct)
|
||||
float advance = remaining * SkillMods.PushMult(u, true) * SkillMods.KnockbackTakenMult(first, true);
|
||||
if (first.IsAlive && advance > 0.01f)
|
||||
{
|
||||
// 튕기지 않고 앞 유닛을 밀며 함께 전진 · 앞 유닛이 다른 유닛·벽에 막히면 둘 다 멈춤 · 접촉 간격 유지
|
||||
float travel = Combat.FreeTravel(first, first.Position, dir, remaining, true, true, u);
|
||||
float travel = Combat.FreeTravel(first, first.Position, dir, advance, true, true, u);
|
||||
if (travel > 0.01f)
|
||||
{
|
||||
Face(first, dir); // 밀린 유닛은 밀린 방향을 본다(PD #1082)
|
||||
Vector2 firstStart = first.Position;
|
||||
Vector2 firstEnd = first.Position + dir * travel;
|
||||
Vector2 spearEnd = contact + dir * travel;
|
||||
Fx.Run(Fx.MoveAlong(first.transform, first.Position, firstEnd, cfg.moveSpeed, null));
|
||||
|
|
@ -245,11 +292,13 @@ namespace TKS
|
|||
first.Position = firstEnd;
|
||||
u.Position = spearEnd;
|
||||
o.Path.Add(spearEnd);
|
||||
o.Displaced.Add(new Displacement { Unit = first, From = firstStart, To = firstEnd });
|
||||
battle.NotifyChanged();
|
||||
}
|
||||
o.Pushed.Add(first);
|
||||
if (!enemy) yield return PushedAllyAttack(first, o);
|
||||
if (!enemy) yield return PushedAllyAttack(first, o, u);
|
||||
}
|
||||
if (enemy && u.IsAlive) SpearHitMore(u, first, o);
|
||||
|
||||
if (isFinal && u.IsAlive)
|
||||
{
|
||||
|
|
@ -258,19 +307,62 @@ namespace TKS
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>삼첨도(spear_hit_two): 밀며 전진 끝에 창병·밀린 적과 닿은 다른 적까지 합쳐 최대 n 기에 동시 피해(반격 없음).</summary>
|
||||
void SpearHitMore(Unit u, Unit first, ActionOutcome o)
|
||||
{
|
||||
int n = SkillMods.Count(u, SkillKeys.SpearHitTwo, 1);
|
||||
if (n <= 1) return;
|
||||
Physics2D.SyncTransforms();
|
||||
var extra = new List<Unit>();
|
||||
foreach (var near in new[] { u, first })
|
||||
{
|
||||
if (near == null || !near.IsAlive) continue;
|
||||
foreach (var c in Physics2D.OverlapCircleAll(near.Position, near.Radius + Combat.ContactTolerance + 0.5f))
|
||||
{
|
||||
var e = c.GetComponent<Unit>();
|
||||
if (e == null || e == first || e == u || !e.IsAlive || e.Faction == u.Faction || extra.Contains(e)) continue;
|
||||
float gap = Vector2.Distance(near.Position, e.Position) - near.Radius - e.Radius;
|
||||
if (gap <= Combat.ContactTolerance) extra.Add(e);
|
||||
}
|
||||
}
|
||||
extra.Sort((a, b) => Vector2.Distance(u.Position, a.Position).CompareTo(Vector2.Distance(u.Position, b.Position)));
|
||||
for (int i = 0; i < extra.Count && i < n - 1; i++)
|
||||
{
|
||||
var e = extra[i];
|
||||
int dmg = SkillSystem.Deal(e, Combat.Damage(cfg, u, e, false, rng, true));
|
||||
o.Attacked.Add(e);
|
||||
o.DamageDealt += dmg;
|
||||
Fx.Popup(e.Position, dmg.ToString(), Color.white);
|
||||
}
|
||||
battle.NotifyChanged();
|
||||
}
|
||||
|
||||
// ---------- 기병: 관통 ----------
|
||||
|
||||
IEnumerator CavalryRoutine(Unit u, Vector2 origin, Vector2 dir, float dist, Vector2 target, bool moveOnly, ActionOutcome o)
|
||||
/// <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)
|
||||
{
|
||||
float travel = Combat.FreeTravel(u, origin, dir, dist, false, false); // 유닛은 무시 · 벽만
|
||||
travel = StopBeforeUnclearedAllies(u, origin, dir, travel); // 지나칠 수 없는 아군 직전에서 멈춤
|
||||
var dists = new List<float>();
|
||||
var enemies = moveOnly ? new List<Unit>() : Combat.EnemiesOnPath(u, origin, dir, travel, dists);
|
||||
if (exclude != null)
|
||||
{
|
||||
for (int i = enemies.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (!exclude.Contains(enemies[i])) continue;
|
||||
enemies.RemoveAt(i);
|
||||
dists.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
Unit finalHit = moveOnly ? null : Combat.EnemyAtPoint(u, target, cfg.finalHitTolerance);
|
||||
bool willReturn = finalHit != null && enemies.Contains(finalHit);
|
||||
bool hitFinal = first && finalHit != null && enemies.Contains(finalHit);
|
||||
// 단기구주: 빗나가도(관통한 적이 있으면) 출발점 복귀 · 위진소요: 명중 시 복귀 대신 같은 방향 한 번 더 관통
|
||||
int repeat = hitFinal ? SkillMods.Count(u, SkillKeys.PierceRepeat, 0) : 0;
|
||||
bool willReturn = first && repeat <= 0 && (hitFinal || (enemies.Count > 0 && SkillMods.Has(u, SkillKeys.PierceReturnOnMiss)));
|
||||
|
||||
Vector2 end = origin + dir * travel;
|
||||
if (!willReturn) end = BackOffFromEnemies(u, origin, dir, travel);
|
||||
if (!willReturn && repeat <= 0) end = BackOffFromEnemies(u, origin, dir, travel);
|
||||
|
||||
if (enemies.Count == 0)
|
||||
{
|
||||
|
|
@ -279,6 +371,7 @@ namespace TKS
|
|||
yield break;
|
||||
}
|
||||
|
||||
float dmgMult = SkillMods.PierceDmgMult(u, enemies.Count, true); // 만인지적(2기 이상) · 발시담정 · 천하무쌍
|
||||
Face(u, dir);
|
||||
if (u.View != null) u.View.PlayAttack();
|
||||
int idx = 0;
|
||||
|
|
@ -287,7 +380,7 @@ namespace TKS
|
|||
{
|
||||
while (idx < enemies.Count && travelled >= dists[idx] - 0.01f)
|
||||
{
|
||||
PierceHit(u, enemies[idx], dir, o);
|
||||
PierceHit(u, enemies[idx], dir, o, dmgMult);
|
||||
hit.Add(enemies[idx]);
|
||||
idx++;
|
||||
}
|
||||
|
|
@ -298,23 +391,39 @@ namespace TKS
|
|||
{
|
||||
if (dists[idx] <= travel + 0.01f)
|
||||
{
|
||||
PierceHit(u, enemies[idx], dir, o);
|
||||
PierceHit(u, enemies[idx], dir, o, dmgMult);
|
||||
hit.Add(enemies[idx]);
|
||||
}
|
||||
}
|
||||
yield return Fx.Wait(0.15f);
|
||||
|
||||
// 살아남은 근접 적의 반격
|
||||
foreach (var e in hit)
|
||||
// 살아남은 근접 적의 반격(단기구주·발시담정·천하무쌍 = 반격 없음)
|
||||
if (!SkillMods.Has(u, SkillKeys.PierceNoCounter))
|
||||
{
|
||||
if (!u.IsAlive) break;
|
||||
if (!e.IsAlive || !UnitTypeInfo.IsMelee(e.Type)) continue;
|
||||
int c = Combat.CounterDamage(cfg, e, u, rng);
|
||||
u.TakeDamage(c);
|
||||
o.CounterTaken += c;
|
||||
Fx.Popup(u.Position, c.ToString(), new Color(1f, 0.45f, 0.45f));
|
||||
battle.NotifyChanged();
|
||||
yield return Fx.Wait(0.12f);
|
||||
foreach (var e in hit)
|
||||
{
|
||||
if (!u.IsAlive) break;
|
||||
if (!e.IsAlive || !UnitTypeInfo.IsMelee(e.Type)) continue;
|
||||
int c = Combat.CounterDamage(cfg, e, u, rng, true);
|
||||
if (c <= 0) continue;
|
||||
c = SkillSystem.Deal(u, c);
|
||||
o.CounterTaken += c;
|
||||
Fx.Popup(u.Position, c.ToString(), new Color(1f, 0.45f, 0.45f));
|
||||
battle.NotifyChanged();
|
||||
yield return Fx.Wait(0.12f);
|
||||
}
|
||||
}
|
||||
|
||||
if (repeat > 0 && u.IsAlive)
|
||||
{
|
||||
// 위진소요: 목표점에서 같은 방향으로 같은 거리만큼 한 번 더 관통(앞서 관통한 적 제외 · 그 뒤 복귀 없음)
|
||||
for (int i = 0; i < repeat && u.IsAlive; i++)
|
||||
{
|
||||
yield return Fx.Wait(0.1f);
|
||||
Vector2 from = u.Position;
|
||||
yield return CavalryRoutine(u, from, dir, dist, from + dir * dist, false, o, false, hit);
|
||||
}
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (willReturn && u.IsAlive)
|
||||
|
|
@ -325,21 +434,26 @@ namespace TKS
|
|||
}
|
||||
}
|
||||
|
||||
void PierceHit(Unit cav, Unit e, Vector2 dir, ActionOutcome o)
|
||||
void PierceHit(Unit cav, Unit e, Vector2 dir, ActionOutcome o, float dmgMult)
|
||||
{
|
||||
if (!e.IsAlive) return;
|
||||
int dmg = Combat.Damage(cfg, cav, e, false, rng, true);
|
||||
e.TakeDamage(dmg);
|
||||
int dmg = SkillSystem.Deal(e, Combat.Damage(cfg, cav, e, false, rng, true, dmgMult));
|
||||
o.Attacked.Add(e);
|
||||
o.DamageDealt += dmg;
|
||||
Fx.Popup(e.Position, dmg.ToString(), Color.white);
|
||||
// 옆으로 밀어내기
|
||||
var skills = SkillSystem.Current;
|
||||
var cs = skills != null ? skills.Of(cav) : null;
|
||||
if (cs != null) cs.PierceHits++; // 팔백 돌격(전투 중 누적)
|
||||
// 옆으로 밀어내기(상산의 창·연진 돌격·백마의종 = 넉백 배율 · 거구 = 밀리는 거리 −)
|
||||
Vector2 side = new Vector2(-dir.y, dir.x);
|
||||
float sign = Vector2.Dot(e.Position - cav.Position, side) >= 0f ? 1f : -1f;
|
||||
if (e.IsAlive)
|
||||
{
|
||||
Vector2 to = battle.Board.ClampInside(e.Position + side * sign * cfg.cavalryKnockback, e.Radius);
|
||||
float kb = cfg.cavalryKnockback * SkillMods.PierceKnockbackMult(cav, true) * SkillMods.KnockbackTakenMult(e, true);
|
||||
Vector2 from = e.Position;
|
||||
Vector2 to = battle.Board.ClampInside(e.Position + side * sign * kb, e.Radius);
|
||||
Fx.Run(Fx.MoveAlong(e.transform, e.Position, to, cfg.moveSpeed * 0.6f, null));
|
||||
o.Displaced.Add(new Displacement { Unit = e, From = from, To = to });
|
||||
}
|
||||
battle.NotifyChanged();
|
||||
}
|
||||
|
|
@ -413,34 +527,49 @@ namespace TKS
|
|||
public bool Crit;
|
||||
/// <summary>책사: 중심 주변 광역 대상(아군 포함 · 중심·쏜 책사 제외 · 가까운 순).</summary>
|
||||
public readonly List<Unit> Splash = new List<Unit>();
|
||||
/// <summary>책사 광역 반경(화공 배율 반영).</summary>
|
||||
public float SplashRadius;
|
||||
/// <summary>백기습영: 쏘기 전에 목표점 쪽으로 이동하는 거리(0 = 없음).</summary>
|
||||
public float Teleport;
|
||||
public Vector2 Origin;
|
||||
}
|
||||
|
||||
ShotPlan PlanShot(Unit u, Vector2 origin, Vector2 dir, Vector2 target)
|
||||
{
|
||||
var p = new ShotPlan { From = origin, To = origin };
|
||||
var p = new ShotPlan { From = origin, To = origin, Origin = origin, SplashRadius = SkillMods.SplashRadius(u, cfg) };
|
||||
// 백기습영(teleport · 다음 행동): 사거리와 무관하게 목표점까지 이동한 뒤 그 자리에서 사격
|
||||
if (SkillMods.Has(u, SkillKeys.Teleport))
|
||||
{
|
||||
p.Teleport = Combat.FreeTravel(u, origin, dir, Vector2.Distance(origin, target), true, true);
|
||||
origin += dir * p.Teleport;
|
||||
p.From = p.To = origin;
|
||||
}
|
||||
float range = SkillMods.Range(u);
|
||||
// 조준 방향으로 사거리 + 접근 허용 거리 안의 첫 적(타겟 선정 = #1080 그대로). hitDist = 그 적 가장자리에 닿기까지의 이동 거리(= 가장자리 간격).
|
||||
float hitDist;
|
||||
p.Target = Combat.FirstEnemyOnPath(u, origin, dir, u.AttackRange + u.ArcherMoveAllow, out hitDist);
|
||||
p.Target = Combat.FirstEnemyOnPath(u, origin, dir, range + u.ArcherMoveAllow, out hitDist);
|
||||
if (p.Target == null) return p;
|
||||
|
||||
// 사거리 안이면 제자리 · 밖이면 가장자리가 사거리에 들어올 만큼만 다가간다(접근 허용 거리 상한)
|
||||
float need = Mathf.Clamp(hitDist - u.AttackRange, 0f, u.ArcherMoveAllow);
|
||||
float need = Mathf.Clamp(hitDist - range, 0f, u.ArcherMoveAllow);
|
||||
p.Approach = need > 0.01f ? Combat.FreeTravel(u, origin, dir, need, true) : 0f;
|
||||
p.From = origin + dir * p.Approach;
|
||||
p.To = p.From;
|
||||
if (hitDist - p.Approach > u.AttackRange + 0.05f) return p; // 아군·벽에 막혀 사거리에 못 들어감
|
||||
if (hitDist - p.Approach > range + 0.05f) return p; // 아군·벽에 막혀 사거리에 못 들어감
|
||||
|
||||
// 타겟 방향 투사체 · 비행 거리 = 사거리(쏜 유닛 가장자리부터) · 경로 위 첫 유닛이 맞는다(아군 포함 · 관통 없음)
|
||||
// 타겟 방향 투사체 · 비행 거리 = 사거리(쏜 유닛 가장자리부터) · 경로 위 첫 유닛이 맞는다(아군 포함 · 관통 없음 · 백발백중 = 아군 통과)
|
||||
p.Fires = true;
|
||||
Vector2 aim = p.Target.Position - p.From;
|
||||
Vector2 adir = aim.sqrMagnitude > 1e-6f ? aim.normalized : dir;
|
||||
float travel;
|
||||
p.Hit = Combat.FirstUnitOnArrow(u, p.From, adir, u.AttackRange + u.Radius, cfg.arrowRadius, out travel);
|
||||
Faction? pass = SkillMods.Has(u, SkillKeys.ArrowPassAllies) ? u.Faction : (Faction?)null;
|
||||
p.Hit = Combat.FirstUnitOnArrow(u, p.From, adir, range + u.Radius, cfg.arrowRadius, pass, out travel);
|
||||
p.To = p.From + adir * (p.Hit != null ? travel + cfg.arrowRadius : travel);
|
||||
p.Crit = p.Hit == p.Target && Combat.EnemyAtPoint(u, target, cfg.finalHitTolerance) == p.Target;
|
||||
if (SkillMods.Has(u, SkillKeys.ArrowCritAlways)) p.Crit = p.Hit != null && p.Hit.Faction != u.Faction; // 백발백중·백기습영: 명중 여부와 무관하게 크리
|
||||
else p.Crit = p.Hit == p.Target && Combat.EnemyAtPoint(u, target, cfg.finalHitTolerance) == p.Target;
|
||||
if (p.Hit != null && u.Type == UnitType.Strategist)
|
||||
{
|
||||
p.Splash.AddRange(Combat.UnitsInCircle(p.Hit.Position, cfg.strategistSplashRadius, p.Hit, u));
|
||||
p.Splash.AddRange(Combat.UnitsInCircle(p.Hit.Position, p.SplashRadius, p.Hit, u));
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
|
@ -448,10 +577,15 @@ namespace TKS
|
|||
IEnumerator ArcherRoutine(Unit u, Vector2 origin, Vector2 dir, float dist, Vector2 target, bool moveOnly, ActionOutcome o)
|
||||
{
|
||||
var p = moveOnly ? null : PlanShot(u, origin, dir, target);
|
||||
if (p != null && p.Teleport > 0.01f)
|
||||
{
|
||||
yield return Move(u, p.Origin + dir * p.Teleport, o); // 백기습영: 목표점까지 이동 후 사격
|
||||
if (p.Target == null) o.MovedOnly = true;
|
||||
}
|
||||
if (p == null || p.Target == null)
|
||||
{
|
||||
// 공격 가능 범위에 적이 없으면 목표점까지 이동 · 쏘지 않는다 (PD 2026-11-04 2차 지시 · #1087 ⑥ 유지)
|
||||
yield return MoveFree(u, origin, dir, dist, o);
|
||||
if (p == null || p.Teleport <= 0.01f) yield return MoveFree(u, origin, dir, dist, o);
|
||||
yield break;
|
||||
}
|
||||
|
||||
|
|
@ -467,7 +601,7 @@ namespace TKS
|
|||
|
||||
/// <summary>
|
||||
/// 투사체 1발: 첫 피격 유닛에 사격 피해(아군이면 오사 · 상성 적용 · 반격 없음 · 명중 보상 없음 · 밀지 않으므로 규칙 ⑤ 없음)
|
||||
/// → 책사는 그 자리 반경 안 다른 유닛 전원에게 광역(대상별 피해 × 광역 비율 · 같은 편은 × 아군 비율 · 크리 없음).
|
||||
/// → 책사는 그 자리 반경 안 다른 유닛 전원에게 광역(대상별 피해 × 광역 비율 · 같은 편은 × 아군 비율 · 크리 없음 · 화공·팔문 간파 반영).
|
||||
/// </summary>
|
||||
IEnumerator Shoot(Unit u, ShotPlan p, ActionOutcome o)
|
||||
{
|
||||
|
|
@ -486,11 +620,13 @@ namespace TKS
|
|||
ShotDamage(u, p.Hit, Combat.Damage(cfg, u, p.Hit, p.Crit, rng, false), p.Crit, o);
|
||||
if (strategist)
|
||||
{
|
||||
Fx.Burst(center, cfg.strategistSplashRadius, Fx.SplashColor);
|
||||
Fx.Burst(center, p.SplashRadius, Fx.SplashColor);
|
||||
float splashRatio = SkillMods.SplashRatio(u, cfg, true);
|
||||
float allyRatio = SkillMods.SplashAllyRatio(u, cfg, true);
|
||||
foreach (var s in p.Splash)
|
||||
{
|
||||
if (!s.IsAlive) continue;
|
||||
float ratio = cfg.strategistSplashRatio * (s.Faction == u.Faction ? cfg.strategistAllyDamageRatio : 1f);
|
||||
float ratio = splashRatio * (s.Faction == u.Faction ? allyRatio : 1f);
|
||||
int dmg = Combat.SplashDamage(cfg, u, s, ratio, rng);
|
||||
if (dmg > 0) ShotDamage(u, s, dmg, false, o);
|
||||
}
|
||||
|
|
@ -502,7 +638,7 @@ namespace TKS
|
|||
/// <summary>사격 피해 적용 + 집계(상대 = Attacked · 같은 편 = FriendlyHit 오사).</summary>
|
||||
void ShotDamage(Unit a, Unit d, int dmg, bool crit, ActionOutcome o)
|
||||
{
|
||||
d.TakeDamage(dmg);
|
||||
dmg = SkillSystem.Deal(d, dmg);
|
||||
if (d.Faction == a.Faction)
|
||||
{
|
||||
o.FriendlyHit.Add(d);
|
||||
|
|
@ -548,36 +684,55 @@ namespace TKS
|
|||
o.Path.Add(to);
|
||||
}
|
||||
|
||||
/// <summary>부딪힌 순간의 피해 교환(연출 대기 없음): 공격 피해 → 상대가 살아 있으면 반격 피해. 결과는 행동 유닛 요약에 집계.</summary>
|
||||
void ImpactExchange(Unit a, Unit d, ActionOutcome o)
|
||||
/// <summary>부딪힌 순간의 피해 교환(연출 대기 없음): 공격 피해 → 상대가 살아 있으면 반격 피해. 결과는 행동 유닛 요약에 집계. 반환 = 준 피해.</summary>
|
||||
int ImpactExchange(Unit a, Unit d, ActionOutcome o)
|
||||
{
|
||||
if (a.View != null) a.View.PlayAttack();
|
||||
int dmg = Combat.Damage(cfg, a, d, false, rng, true);
|
||||
d.TakeDamage(dmg);
|
||||
int dmg = SkillSystem.Deal(d, Combat.Damage(cfg, a, d, false, rng, true));
|
||||
o.Attacked.Add(d);
|
||||
o.DamageDealt += dmg;
|
||||
Fx.Popup(d.Position, dmg.ToString(), Color.white);
|
||||
if (d.IsAlive && a.IsAlive)
|
||||
{
|
||||
int c = Combat.CounterDamage(cfg, d, a, rng);
|
||||
if (d.View != null) d.View.PlayAttack();
|
||||
a.TakeDamage(c);
|
||||
o.CounterTaken += c;
|
||||
Fx.Popup(a.Position, c.ToString(), new Color(1f, 0.45f, 0.45f));
|
||||
int c = Combat.CounterDamage(cfg, d, a, rng, true); // 부딪혀 밀리는 쪽의 반격(진중 조건)
|
||||
if (c > 0)
|
||||
{
|
||||
if (d.View != null) d.View.PlayAttack();
|
||||
c = SkillSystem.Deal(a, c);
|
||||
o.CounterTaken += c;
|
||||
Fx.Popup(a.Position, c.ToString(), new Color(1f, 0.45f, 0.45f));
|
||||
}
|
||||
}
|
||||
battle.NotifyChanged();
|
||||
return dmg;
|
||||
}
|
||||
|
||||
/// <summary>알까기 양쪽 튕김(동시 시작): 부딪힌 유닛 = n 방향 · 남은 힘 × collisionPushRatio / 행동 유닛 = −n 방향 · 남은 힘 × collisionSelfBounceRatio.</summary>
|
||||
IEnumerator Bounce(Unit striker, Unit struck, Vector2 n, float remaining, ActionOutcome o)
|
||||
/// <summary>
|
||||
/// 알까기 양쪽 튕김(동시 시작): 부딪힌 유닛 = n 방향 · 남은 힘 × collisionPushRatio × 밀기 배율(청룡도 지기·호치·함진영) × 대상 밀림 배율(거구)
|
||||
/// / 행동 유닛 = −n 방향 · 남은 힘 × collisionSelfBounceRatio × 자기 튕김 배율(인덕·호치 = 0). impactDmg = 함진영 충돌 추가 피해 기준.
|
||||
/// </summary>
|
||||
IEnumerator Bounce(Unit striker, Unit struck, Vector2 n, float remaining, ActionOutcome o, int impactDmg)
|
||||
{
|
||||
bool struckDone = true;
|
||||
if (struck != null && struck.IsAlive && remaining * cfg.collisionPushRatio >= 0.01f)
|
||||
float force = struck != null ? remaining * cfg.collisionPushRatio * SkillMods.PushMult(striker, true) * SkillMods.KnockbackTakenMult(struck, true) : 0f;
|
||||
if (struck != null && struck.IsAlive && force >= 0.01f)
|
||||
{
|
||||
struckDone = false;
|
||||
Fx.Run(Track(Push(struck, n, remaining * cfg.collisionPushRatio, striker, o), () => struckDone = true));
|
||||
float bonus = impactDmg > 0 ? SkillMods.PushCollisionBonus(striker) : 0f;
|
||||
System.Action<bool> onEnd = null;
|
||||
if (bonus > 0f)
|
||||
{
|
||||
onEnd = blocked =>
|
||||
{
|
||||
if (!blocked || !struck.IsAlive) return;
|
||||
int extra = SkillSystem.Deal(struck, Mathf.Max(1, Mathf.RoundToInt(impactDmg * bonus)));
|
||||
o.DamageDealt += extra;
|
||||
Fx.Popup(struck.Position, extra + " 충돌", new Color(1f, 0.75f, 0.3f));
|
||||
};
|
||||
}
|
||||
Fx.Run(Track(Push(struck, n, force, striker, o, onEnd), () => struckDone = true));
|
||||
}
|
||||
float self = remaining * cfg.collisionSelfBounceRatio;
|
||||
float self = remaining * cfg.collisionSelfBounceRatio * SkillMods.SelfBounceMult(striker, true);
|
||||
if (self >= 0.01f && striker.IsAlive)
|
||||
{
|
||||
float back = Combat.FreeTravel(striker, striker.Position, -n, self, true, true, struck);
|
||||
|
|
@ -599,41 +754,60 @@ namespace TKS
|
|||
onDone();
|
||||
}
|
||||
|
||||
/// <summary>부딪힌 유닛을 n 방향으로 force 만큼 밀어낸다(다른 유닛·벽에 닿으면 멈춤). 밀어 주는 유닛(pusher)은 막는 것으로 보지 않는다.</summary>
|
||||
IEnumerator Push(Unit target, Vector2 n, float force, Unit pusher, ActionOutcome o)
|
||||
/// <summary>부딪힌 유닛을 n 방향으로 force 만큼 밀어낸다(다른 유닛·벽에 닿으면 멈춤). 밀어 주는 유닛(pusher)은 막는 것으로 보지 않는다. onEnd(true) = 다 못 가고 막힘.</summary>
|
||||
IEnumerator Push(Unit target, Vector2 n, float force, Unit pusher, ActionOutcome o, System.Action<bool> onEnd = null)
|
||||
{
|
||||
if (target == null || !target.IsAlive || force < 0.01f) yield break;
|
||||
float slide = Combat.FreeTravel(target, target.Position, n, force, true, true, pusher);
|
||||
float moved = 0f;
|
||||
if (slide > 0.01f)
|
||||
{
|
||||
Face(target, n); // 밀린 방향을 본다(PD #1082)
|
||||
Vector2 start = target.Position;
|
||||
Vector2 end = battle.Board.ClampInside(target.Position + n * slide, target.Radius);
|
||||
yield return Fx.MoveAlong(target.transform, target.Position, end, cfg.moveSpeed, null);
|
||||
target.Position = end;
|
||||
moved = Vector2.Distance(start, end);
|
||||
o.Displaced.Add(new Displacement { Unit = target, From = start, To = end });
|
||||
}
|
||||
o.Pushed.Add(target);
|
||||
battle.NotifyChanged();
|
||||
if (onEnd != null) onEnd(moved < force - Combat.Epsilon * 2f);
|
||||
}
|
||||
|
||||
/// <summary>규칙 ⑤: 밀린 아군이 상대 유닛과 닿아 있으면 그 자리에서 공격 1회(근접 · 자기 행동 소모 없음).</summary>
|
||||
IEnumerator PushedAllyAttack(Unit ally, ActionOutcome o)
|
||||
/// <summary>
|
||||
/// 규칙 ⑤: 밀린 아군이 상대 유닛과 닿아 있으면 그 자리에서 공격(근접 · 자기 행동 소모 없음) — 기본 1회 ·
|
||||
/// 민 유닛의 군령(pushed_ally_attack_count = n 회) · 대장군(pushed_ally_attack_on_push = +n 회).
|
||||
/// </summary>
|
||||
IEnumerator PushedAllyAttack(Unit ally, ActionOutcome o, Unit pusher)
|
||||
{
|
||||
if (ally == null || !ally.IsAlive) yield break;
|
||||
Unit foe = Combat.TouchingOpponent(ally);
|
||||
if (foe == null) yield break;
|
||||
yield return Fx.Wait(0.1f);
|
||||
Face(ally, foe.Position - ally.Position);
|
||||
yield return PushedAttack(ally, foe, o);
|
||||
if (Combat.TouchingOpponent(ally) == null) yield break;
|
||||
int times = 1;
|
||||
if (pusher != null)
|
||||
{
|
||||
times = Mathf.Max(1, SkillMods.Count(pusher, SkillKeys.PushedAllyAttackCount, 1));
|
||||
times += Mathf.Max(0, SkillMods.Count(pusher, SkillKeys.PushedAllyAttackOnPush, 0, true));
|
||||
}
|
||||
for (int i = 0; i < times; i++)
|
||||
{
|
||||
if (!ally.IsAlive) yield break;
|
||||
Unit foe = Combat.TouchingOpponent(ally);
|
||||
if (foe == null) yield break;
|
||||
yield return Fx.Wait(0.1f);
|
||||
Face(ally, foe.Position - ally.Position);
|
||||
yield return PushedAttack(ally, foe, o);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>밀린 아군의 근접 공격 1회(반격 받음 · 결과 요약에는 집계하지 않고 Notes 에 기록). 원거리 병종도 근접 패널티를 받는다.</summary>
|
||||
/// <summary>밀린 아군의 근접 공격 1회(반격 받음 · 결과 요약에는 집계하지 않고 Notes 에 기록 · 도원의 맹약 = 피해 +%). 원거리 병종도 근접 패널티를 받는다.</summary>
|
||||
IEnumerator PushedAttack(Unit a, Unit d, ActionOutcome o)
|
||||
{
|
||||
if (a.View != null) a.View.PlayAttack();
|
||||
yield return Fx.Lunge(a, d.Position);
|
||||
|
||||
int dmg = Combat.Damage(cfg, a, d, false, rng, true);
|
||||
d.TakeDamage(dmg);
|
||||
float scale = 1f + SkillMods.Sum(a, SkillKeys.PushedAllyAttackPct, true);
|
||||
int dmg = SkillSystem.Deal(d, Combat.Damage(cfg, a, d, false, rng, true, scale));
|
||||
Fx.Popup(d.Position, dmg.ToString(), Color.white);
|
||||
battle.NotifyChanged();
|
||||
yield return Fx.Wait(0.2f);
|
||||
|
|
@ -641,13 +815,16 @@ namespace TKS
|
|||
int counter = 0;
|
||||
if (d.IsAlive && a.IsAlive)
|
||||
{
|
||||
counter = Combat.CounterDamage(cfg, d, a, rng);
|
||||
if (d.View != null) d.View.PlayAttack();
|
||||
yield return Fx.Lunge(d, a.Position);
|
||||
a.TakeDamage(counter);
|
||||
Fx.Popup(a.Position, counter.ToString(), new Color(1f, 0.45f, 0.45f));
|
||||
battle.NotifyChanged();
|
||||
yield return Fx.Wait(0.2f);
|
||||
counter = Combat.CounterDamage(cfg, d, a, rng, false);
|
||||
if (counter > 0)
|
||||
{
|
||||
if (d.View != null) d.View.PlayAttack();
|
||||
yield return Fx.Lunge(d, a.Position);
|
||||
counter = SkillSystem.Deal(a, counter);
|
||||
Fx.Popup(a.Position, counter.ToString(), new Color(1f, 0.45f, 0.45f));
|
||||
battle.NotifyChanged();
|
||||
yield return Fx.Wait(0.2f);
|
||||
}
|
||||
}
|
||||
|
||||
o.Notes.Add("밀린 " + a.DisplayName + " → " + d.DisplayName + " 공격 " + dmg + (counter > 0 ? " · 반격 " + counter : ""));
|
||||
|
|
|
|||
|
|
@ -5,9 +5,16 @@ using UnityEngine.UI;
|
|||
|
||||
namespace TKS
|
||||
{
|
||||
/// <summary>화면 HUD(uGUI · 코드 생성): 스테이지·턴·유닛 수·안내·선택 유닛 정보·미니맵·스킬 아이콘(추후)·결과 오버레이.</summary>
|
||||
/// <summary>화면 HUD(uGUI · 코드 생성): 스테이지·턴·유닛 수·안내·선택 유닛 정보·미니맵·스킬 버튼(PD #1093 B)·스킬 발동 한 줄·결과 오버레이.</summary>
|
||||
public class BattleHUD : MonoBehaviour
|
||||
{
|
||||
static readonly Color SkillReadyColor = new Color(0.72f, 0.2f, 0.14f, 0.94f);
|
||||
static readonly Color SkillReservedColor = new Color(0.96f, 0.76f, 0.22f, 0.97f);
|
||||
static readonly Color SkillOffColor = new Color(0.3f, 0.3f, 0.33f, 0.88f);
|
||||
static readonly Color SkillPassiveOnlyColor = new Color(0.18f, 0.2f, 0.26f, 0.85f);
|
||||
static readonly Color SkillLogColor = new Color(0.6f, 1f, 0.85f);
|
||||
const float SkillLogSeconds = 3f;
|
||||
|
||||
BattleManager battle;
|
||||
Camera uiCam;
|
||||
CameraController camCtrl;
|
||||
|
|
@ -22,6 +29,12 @@ namespace TKS
|
|||
Text resultSub;
|
||||
Unit selected;
|
||||
|
||||
Image skillPanel;
|
||||
Text skillName;
|
||||
Text skillState;
|
||||
Text skillPassive;
|
||||
Text skillLog;
|
||||
|
||||
Image minimapBg;
|
||||
Image minimapView;
|
||||
readonly Dictionary<Unit, Image> minimapDots = new Dictionary<Unit, Image>();
|
||||
|
|
@ -66,12 +79,26 @@ namespace TKS
|
|||
hintText.color = new Color(0.85f, 0.85f, 0.85f);
|
||||
unitText = MakeText("UnitInfo", new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(30f, 110f), new Vector2(760f, 170f), 32, TextAnchor.LowerLeft);
|
||||
|
||||
// 스킬 아이콘(초기 단계 미구현 · 자리만)
|
||||
var skill = MakeImage("SkillIcon", transform, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-36f, 110f), new Vector2(170f, 170f), new Color(0.3f, 0.3f, 0.33f, 0.8f));
|
||||
var skillLabel = MakeText("Label", new Vector2(0f, 0f), new Vector2(1f, 1f), new Vector2(0.5f, 0.5f), Vector2.zero, Vector2.zero, 30, TextAnchor.MiddleCenter);
|
||||
skillLabel.transform.SetParent(skill.transform, false);
|
||||
skillLabel.text = "스킬\n(추후)";
|
||||
skillLabel.color = new Color(0.75f, 0.75f, 0.75f);
|
||||
// 스킬 버튼(PD #1093 B · 유닛 선택 시 · 탭 = 예약/취소 · 즉시 발동) — 이름 · 쿨 · 사용 가능/불가 · 패시브 한 줄
|
||||
skillPanel = MakeImage("SkillButton", transform, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-30f, 110f), new Vector2(270f, 230f), SkillOffColor);
|
||||
skillName = MakeText("Name", new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -14f), new Vector2(256f, 60f), 38, TextAnchor.UpperCenter);
|
||||
skillName.transform.SetParent(skillPanel.transform, false);
|
||||
skillName.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
skillName.resizeTextForBestFit = true;
|
||||
skillName.resizeTextMinSize = 22;
|
||||
skillName.resizeTextMaxSize = 38;
|
||||
skillState = MakeText("State", new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 4f), new Vector2(256f, 80f), 26, TextAnchor.MiddleCenter);
|
||||
skillState.transform.SetParent(skillPanel.transform, false);
|
||||
skillState.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
skillPassive = MakeText("Passive", new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 12f), new Vector2(256f, 40f), 23, TextAnchor.LowerCenter);
|
||||
skillPassive.transform.SetParent(skillPanel.transform, false);
|
||||
skillPassive.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
skillPassive.color = new Color(0.8f, 0.9f, 1f);
|
||||
skillPanel.gameObject.SetActive(false);
|
||||
|
||||
// 스킬 발동 한 줄(액티브·패시브 · 최근 몇 초)
|
||||
skillLog = MakeText("SkillLog", new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(30f, 292f), new Vector2(1020f, 80f), 28, TextAnchor.LowerLeft);
|
||||
skillLog.color = SkillLogColor;
|
||||
|
||||
// 미니맵 (전장 비율 · 탭하면 그 지점으로 카메라 이동)
|
||||
Vector2 bs = battle.Config.boardSize;
|
||||
|
|
@ -136,6 +163,97 @@ namespace TKS
|
|||
selected = u;
|
||||
}
|
||||
|
||||
public bool SkillButtonVisible
|
||||
{
|
||||
get { return skillPanel != null && skillPanel.gameObject.activeSelf; }
|
||||
}
|
||||
|
||||
public string SkillButtonText
|
||||
{
|
||||
get { return skillPanel != null ? skillName.text + " | " + skillState.text + " | " + skillPassive.text : ""; }
|
||||
}
|
||||
|
||||
public string SkillLogText
|
||||
{
|
||||
get { return skillLog != null ? skillLog.text : ""; }
|
||||
}
|
||||
|
||||
/// <summary>화면 점이 스킬 버튼 위면 탭 처리(선택 유닛 액티브 예약·취소·즉시 발동) 후 true — 슬링샷 입력은 건너뛴다.</summary>
|
||||
public bool TrySkillTap(Vector2 screen)
|
||||
{
|
||||
if (!SkillButtonVisible || battle == null) return false;
|
||||
if (!RectTransformUtility.RectangleContainsScreenPoint(skillPanel.rectTransform, screen, uiCam)) return false;
|
||||
PressSkill();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>스킬 버튼 누름(탭 · 테스트 · 캡처 공용).</summary>
|
||||
public string PressSkill()
|
||||
{
|
||||
if (battle == null || battle.Skills == null || selected == null) return null;
|
||||
var msg = battle.Skills.TapActive(selected);
|
||||
if (!string.IsNullOrEmpty(msg)) battle.LastMessage = msg;
|
||||
if (selected.View != null) selected.View.Refresh();
|
||||
return msg;
|
||||
}
|
||||
|
||||
void RefreshSkill()
|
||||
{
|
||||
var sys = battle.Skills;
|
||||
var lines = sys != null ? sys.RecentLog(SkillLogSeconds, 2) : null;
|
||||
skillLog.text = lines != null ? string.Join("\n", lines.ToArray()) : "";
|
||||
|
||||
var s = sys != null && selected != null && selected.IsAlive ? sys.Of(selected) : null;
|
||||
bool show = s != null && (s.Active != null || s.Passive != null);
|
||||
if (skillPanel.gameObject.activeSelf != show) skillPanel.gameObject.SetActive(show);
|
||||
if (!show) return;
|
||||
|
||||
skillPassive.text = s.Passive != null ? "패 · " + s.Passive.NameKo + (s.PassiveMult > 1f ? " ★" : "") : "";
|
||||
if (s.Active == null || selected.Faction != Faction.Player)
|
||||
{
|
||||
skillName.text = s.Active != null ? s.Active.NameKo : "패시브";
|
||||
skillState.text = selected.Faction != Faction.Player ? "적 무장 · 패시브만" : "액티브 없음";
|
||||
skillPanel.color = SkillPassiveOnlyColor;
|
||||
return;
|
||||
}
|
||||
skillName.text = s.Active.NameKo + " Lv" + s.ActiveLv;
|
||||
var st = sys.StateOf(selected);
|
||||
string kind = s.Active.Trigger == SkillTrigger.Instant ? "즉시" : "다음 행동";
|
||||
switch (st)
|
||||
{
|
||||
case SkillSystem.ActiveState.Ready:
|
||||
skillState.text = kind + " · 사용 가능\n탭 = " + (s.Active.Trigger == SkillTrigger.Instant ? "발동" : "예약");
|
||||
skillPanel.color = SkillReadyColor;
|
||||
break;
|
||||
case SkillSystem.ActiveState.Reserved:
|
||||
skillState.text = "예약됨\n다시 탭 = 취소";
|
||||
skillPanel.color = SkillReservedColor;
|
||||
break;
|
||||
case SkillSystem.ActiveState.Targeting:
|
||||
skillState.text = "지점 탭 대기\n다시 탭 = 취소";
|
||||
skillPanel.color = SkillReservedColor;
|
||||
break;
|
||||
case SkillSystem.ActiveState.Cooldown:
|
||||
skillState.text = "쿨 " + s.CooldownLeft(battle.TurnNumber) + "턴 · 사용 불가";
|
||||
skillPanel.color = SkillOffColor;
|
||||
break;
|
||||
case SkillSystem.ActiveState.Acted:
|
||||
skillState.text = kind + " · 행동 완료";
|
||||
skillPanel.color = SkillOffColor;
|
||||
break;
|
||||
case SkillSystem.ActiveState.BonusMove:
|
||||
skillState.text = "추가 이동 중 · 사용 불가";
|
||||
skillPanel.color = SkillOffColor;
|
||||
break;
|
||||
default:
|
||||
skillState.text = kind + " · 쿨 " + s.Active.Cooldown + "턴";
|
||||
skillPanel.color = SkillOffColor;
|
||||
break;
|
||||
}
|
||||
bool dark = st == SkillSystem.ActiveState.Reserved || st == SkillSystem.ActiveState.Targeting;
|
||||
skillName.color = skillState.color = dark ? new Color(0.15f, 0.1f, 0.05f) : Color.white;
|
||||
}
|
||||
|
||||
/// <summary>화면 점이 미니맵 안이면 해당 월드 좌표를 돌려준다.</summary>
|
||||
public bool TryMinimapTap(Vector2 screen, out Vector2 world)
|
||||
{
|
||||
|
|
@ -179,7 +297,11 @@ namespace TKS
|
|||
}
|
||||
messageText.text = battle.LastMessage;
|
||||
|
||||
if (battle.Phase == BattlePhase.PlayerTurn)
|
||||
if (battle.Skills != null && battle.Skills.PendingTeleport != null)
|
||||
{
|
||||
hintText.text = "변진: 반경 안 빈 곳을 탭하면 이동(행동 소모 없음) · 다른 곳 탭 = 취소";
|
||||
}
|
||||
else if (battle.Phase == BattlePhase.PlayerTurn)
|
||||
{
|
||||
hintText.text = battle.OnlyBonusMovesRemain()
|
||||
? "추가 이동: 유닛을 당겨 이동 · 빈 곳을 탭하면 건너뜀"
|
||||
|
|
@ -196,10 +318,10 @@ namespace TKS
|
|||
string side = u.Faction == Faction.Player ? "아군" : "적군";
|
||||
string line1 = side + " · " + u.DisplayName + " · " + UnitTypeInfo.Korean(u.Type) + (u.IsBoss ? " (장수)" : "");
|
||||
string line2 = "HP " + u.Hp + "/" + u.MaxHp + " 공 " + u.Atk + " 방 " + u.Def;
|
||||
string line3 = "슬링 " + u.SlingMaxDistance.ToString("0.#");
|
||||
if (UnitTypeInfo.IsRanged(u.Type)) line3 += " 사거리 " + u.AttackRange.ToString("0.#") + " (접근 +" + u.ArcherMoveAllow.ToString("0.#") + ")";
|
||||
if (u.Type == UnitType.Strategist) line3 += " 광역 " + battle.Config.strategistSplashRadius.ToString("0.#");
|
||||
if (u.Type == UnitType.Spear) line3 += " 견제 반경 " + u.AttackRange.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 (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 = "";
|
||||
if (u.Faction == Faction.Player)
|
||||
{
|
||||
|
|
@ -213,6 +335,7 @@ namespace TKS
|
|||
{
|
||||
unitText.text = "유닛을 선택하세요";
|
||||
}
|
||||
RefreshSkill();
|
||||
|
||||
bool result = battle.Phase == BattlePhase.StageClear || battle.Phase == BattlePhase.Defeat;
|
||||
if (resultOverlay.activeSelf != result) resultOverlay.SetActive(result);
|
||||
|
|
|
|||
|
|
@ -27,6 +27,16 @@ namespace TKS
|
|||
public float hpMult = 1f;
|
||||
public float atkMult = 1f;
|
||||
public float defMult = 1f;
|
||||
|
||||
// 스킬(PD #1093 B · 편성 → SkillRules.FillLineup) — 병사 유닛 = 비어 있음
|
||||
public string skillActive = "";
|
||||
public string skillPassive = "";
|
||||
public int activeLv = 1;
|
||||
public int passiveLv = 1;
|
||||
/// <summary>★5 패시브 배율(evolve.csv skill_unlock passive_x · 그 아래 ★ = 1).</summary>
|
||||
public float passiveMult = 1f;
|
||||
/// <summary>무장(영웅) — 참장 대상.</summary>
|
||||
public bool general;
|
||||
}
|
||||
|
||||
/// <summary>턴 진행·유닛 관리·승패 판정. 아군은 모든 유닛이 행동하면 자동으로 적군 턴이 된다(버튼 없음).</summary>
|
||||
|
|
@ -44,6 +54,8 @@ namespace TKS
|
|||
public int TurnNumber { get; private set; }
|
||||
public System.Random Rng { get; private set; }
|
||||
public ActionResolver Resolver { get; private set; }
|
||||
/// <summary>스킬 상태·시한 효과(PD #1093 B) · 수치 질의 = SkillMods.</summary>
|
||||
public SkillSystem Skills { get; private set; }
|
||||
public string LastMessage = "";
|
||||
|
||||
readonly List<Unit> units = new List<Unit>();
|
||||
|
|
@ -83,12 +95,15 @@ namespace TKS
|
|||
Rng = new System.Random(Environment.TickCount);
|
||||
Fx.Runner = this;
|
||||
Phase = BattlePhase.Setup;
|
||||
Skills = new SkillSystem(this);
|
||||
SkillSystem.Current = Skills;
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (Instance == this) Instance = null;
|
||||
if (Fx.Runner == this) Fx.Runner = null;
|
||||
if (SkillSystem.Current == Skills) SkillSystem.Current = null;
|
||||
}
|
||||
|
||||
public void Configure(BalanceConfig cfg, Board board)
|
||||
|
|
@ -160,6 +175,7 @@ namespace TKS
|
|||
Destroy(units[i].gameObject);
|
||||
}
|
||||
units.Clear();
|
||||
if (Skills != null) Skills.Clear();
|
||||
}
|
||||
|
||||
// ---------- 스테이지 ----------
|
||||
|
|
@ -174,6 +190,7 @@ namespace TKS
|
|||
|
||||
// 아군 = 편성 프리셋(자리 0~2 전열 · 3~4 후열 → playerSpawnPoints) · 방어 배율은 Unit.Init 에 없어 생성 뒤 적용
|
||||
deployed.Clear();
|
||||
SkillCatalog.Invalidate(); // 임포트로 바뀐 스킬 표를 전투마다 다시 읽음
|
||||
var lineup = LineupProvider != null ? LineupProvider() : null;
|
||||
if (lineup != null)
|
||||
{
|
||||
|
|
@ -182,15 +199,18 @@ namespace TKS
|
|||
if (h == null || h.slot < 0 || h.slot >= Config.playerSpawnPoints.Length) continue;
|
||||
var u = SpawnUnit(h.type, Faction.Player, Config.playerSpawnPoints[h.slot], h.name, h.hpMult, h.atkMult, false, h.art);
|
||||
u.Def = Mathf.Max(0, Mathf.RoundToInt(Config.GetStats(h.type).def * h.defMult));
|
||||
Skills.AttachLineup(u, h); // 액티브·패시브(레벨 · ★5) · 산채 체력은 여기서 최대 HP 반영
|
||||
deployed.Add(h);
|
||||
}
|
||||
}
|
||||
foreach (var e in stage.enemies)
|
||||
{
|
||||
SpawnUnit(e.type, Faction.Enemy, e.position, e.name, e.hpMult, e.atkMult, e.isBoss, e.art);
|
||||
var u = SpawnUnit(e.type, Faction.Enemy, e.position, e.name, e.hpMult, e.atkMult, e.isBoss, e.art);
|
||||
Skills.AttachEnemy(u, e); // 적 무장 = 패시브만(액티브 AI 사용은 2차)
|
||||
}
|
||||
|
||||
TurnNumber = 1;
|
||||
Skills.BeginBattle();
|
||||
LastMessage = stage.title + " — 아군 차례";
|
||||
if (StageStarted != null) StageStarted();
|
||||
SetPhase(BattlePhase.PlayerTurn);
|
||||
|
|
@ -235,7 +255,7 @@ namespace TKS
|
|||
public bool AnyEnemyWithinArcherReach(Unit u)
|
||||
{
|
||||
if (u == null) return false;
|
||||
float reach = u.AttackRange + u.ArcherMoveAllow;
|
||||
float reach = SkillMods.Range(u) + u.ArcherMoveAllow;
|
||||
Faction other = u.Faction == Faction.Player ? Faction.Enemy : Faction.Player;
|
||||
foreach (var e in AliveUnits(other))
|
||||
{
|
||||
|
|
@ -256,13 +276,19 @@ namespace TKS
|
|||
|
||||
IEnumerator PlayerActionRoutine(Unit u, Vector2 target)
|
||||
{
|
||||
bool moveOnly = u.BonusMoveAvailable;
|
||||
bool wasBonus = u.BonusMoveAvailable;
|
||||
bool moveOnly = wasBonus && !Skills.BonusCanAttack(u); // 호치·강동의 호랑이 = 추가 이동에서 공격 가능
|
||||
var o = new ActionOutcome();
|
||||
yield return Resolver.Resolve(u, target, moveOnly, o);
|
||||
yield return ApplyOverwatch(u, o.Path);
|
||||
yield return ApplyOverwatch(u, o);
|
||||
|
||||
if (u.IsAlive && o.BonusMove) u.GrantBonusMove();
|
||||
else u.MarkActed();
|
||||
// 추가 이동 = 첫 명중 n 회(인덕 extra_move_count) · 추가 이동 뒤에는 남은 횟수만
|
||||
if (u.IsAlive && Skills.NextBonus(u, wasBonus, o)) u.GrantBonusMove();
|
||||
else
|
||||
{
|
||||
u.MarkActed();
|
||||
Skills.ClearBonus(u);
|
||||
}
|
||||
LastMessage = Describe(u, o, moveOnly);
|
||||
NotifyChanged();
|
||||
|
||||
|
|
@ -305,12 +331,31 @@ namespace TKS
|
|||
if (!OnlyBonusMovesRemain()) return;
|
||||
foreach (var u in AliveUnits(Faction.Player))
|
||||
{
|
||||
if (u.BonusMoveAvailable) u.MarkActed();
|
||||
if (!u.BonusMoveAvailable) continue;
|
||||
u.MarkActed();
|
||||
Skills.ClearBonus(u);
|
||||
}
|
||||
SetPhase(BattlePhase.Resolving);
|
||||
StartCoroutine(EnemyTurnRoutine());
|
||||
}
|
||||
|
||||
/// <summary>즉시형 액티브 연출(장판 일갈 밀기 · 변진 이동 등) — 아군 턴에만 · 끝나면 다시 아군 턴(행동 소모 없음).</summary>
|
||||
public bool RunSkillRoutine(IEnumerator routine)
|
||||
{
|
||||
if (Phase != BattlePhase.PlayerTurn || routine == null) return false;
|
||||
SetPhase(BattlePhase.Resolving);
|
||||
StartCoroutine(SkillRoutine(routine));
|
||||
return true;
|
||||
}
|
||||
|
||||
IEnumerator SkillRoutine(IEnumerator routine)
|
||||
{
|
||||
yield return routine;
|
||||
NotifyChanged();
|
||||
if (CheckBattleEnd()) yield break;
|
||||
SetPhase(BattlePhase.PlayerTurn);
|
||||
}
|
||||
|
||||
// ---------- 적군 턴 ----------
|
||||
|
||||
public Coroutine RunEnemyTurn()
|
||||
|
|
@ -320,6 +365,7 @@ namespace TKS
|
|||
|
||||
IEnumerator EnemyTurnRoutine()
|
||||
{
|
||||
Skills.OnEnemyTurnStart(); // 이번 아군 턴 효과(동남풍·고육계) · 쓰지 않은 예약 정리
|
||||
SetPhase(BattlePhase.EnemyTurn);
|
||||
LastMessage = "적군 차례";
|
||||
NotifyChanged();
|
||||
|
|
@ -328,12 +374,13 @@ namespace TKS
|
|||
|
||||
var enemies = AliveUnits(Faction.Enemy);
|
||||
float accuracy = Stage.aiAccuracy;
|
||||
float aggro = Stage.aiAggroRange;
|
||||
float aggro = Mathf.Max(0f, Stage.aiAggroRange + Skills.AggroAdd(Faction.Enemy)); // 신산 = 적 교전 범위 −
|
||||
int acted = 0;
|
||||
for (int i = 0; i < enemies.Count; i++)
|
||||
{
|
||||
var e = enemies[i];
|
||||
if (!e.IsAlive) continue;
|
||||
if (Skills.Suppressed(e)) continue; // 장판 일갈·태평요술 = 이번 적 턴 교전 반응 없음
|
||||
var players = AliveUnits(Faction.Player);
|
||||
if (players.Count == 0) break;
|
||||
|
||||
|
|
@ -344,7 +391,7 @@ namespace TKS
|
|||
yield return Fx.Wait(0.3f); // 카메라가 따라올 시간
|
||||
var o = new ActionOutcome();
|
||||
yield return Resolver.Resolve(e, target.Value, false, o);
|
||||
yield return ApplyOverwatch(e, o.Path);
|
||||
yield return ApplyOverwatch(e, o);
|
||||
LastMessage = Describe(e, o, false);
|
||||
NotifyChanged();
|
||||
acted++;
|
||||
|
|
@ -354,6 +401,7 @@ namespace TKS
|
|||
if (acted == 0) yield return Fx.Wait(0.3f);
|
||||
|
||||
foreach (var u in AliveUnits(Faction.Player)) u.ResetForNewTurn();
|
||||
Skills.OnPlayerTurnStart(); // 이번 적 턴 효과(호위·철벽·간웅·일갈·안개) · 견제 강화 정리
|
||||
TurnNumber++;
|
||||
LastMessage = TurnNumber + "번째 턴 — 아군 차례" + (acted == 0 ? " (적은 교전 범위 밖에서 대기)" : "");
|
||||
SetPhase(BattlePhase.PlayerTurn);
|
||||
|
|
@ -362,35 +410,79 @@ namespace TKS
|
|||
/// <summary>창병 대기: 상대 유닛이 지나간 경로가 대기 중인 창병 반경을 스치면 피해(창병마다 1회).</summary>
|
||||
public IEnumerator ApplyOverwatch(Unit mover, List<Vector2> path)
|
||||
{
|
||||
if (mover == null || path == null || path.Count < 2) yield break;
|
||||
var o = new ActionOutcome();
|
||||
if (path != null) o.Path.AddRange(path);
|
||||
yield return ApplyOverwatch(mover, o);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 창병 대기 견제(스킬 반영 · PD #1093 B): 반경 = SkillMods.OverwatchRadius(후계·백마의 창·청룡언월) · 지나간 적 1기당 1회(기본) × 견제 횟수(삼대 노신 = n 회 타격) ·
|
||||
/// 「견제 무제한」(청룡언월) = 행동 유닛 경로가 반경을 스친 구간마다 + 그 행동으로 밀리거나 넉백된 상대 유닛도 타격 · 피해 × 교변 · 쓰러뜨리면 하북 명장 회복.
|
||||
/// </summary>
|
||||
public IEnumerator ApplyOverwatch(Unit mover, ActionOutcome o)
|
||||
{
|
||||
if (mover == null || o == null) yield break;
|
||||
var path = o.Path;
|
||||
Faction other = mover.Faction == Faction.Player ? Faction.Enemy : Faction.Player;
|
||||
foreach (var spear in AliveUnits(other))
|
||||
{
|
||||
if (spear.Type != UnitType.Spear || !spear.Overwatch) continue;
|
||||
if (!mover.IsAlive) yield break;
|
||||
bool triggered = false;
|
||||
for (int i = 0; i < path.Count - 1; i++)
|
||||
if (!mover.IsAlive && o.Displaced.Count == 0) yield break;
|
||||
float radius = SkillMods.OverwatchRadius(spear);
|
||||
bool unlimited = SkillMods.OverwatchUnlimited(spear);
|
||||
var victims = new List<Unit>();
|
||||
int passes = 0;
|
||||
for (int i = 0; path != null && i < path.Count - 1; i++)
|
||||
{
|
||||
Vector2 a = path[i];
|
||||
Vector2 b = path[i + 1];
|
||||
if (Vector2.Distance(a, b) < 0.05f) continue;
|
||||
if (Combat.SegmentPointDistance(a, b, spear.Position) <= spear.AttackRange + mover.Radius)
|
||||
if (Combat.SegmentPointDistance(a, b, spear.Position) > radius + mover.Radius) continue;
|
||||
passes++;
|
||||
if (!unlimited) break;
|
||||
}
|
||||
for (int i = 0; i < passes; i++) victims.Add(mover);
|
||||
if (unlimited)
|
||||
{
|
||||
foreach (var d in o.Displaced)
|
||||
{
|
||||
triggered = true;
|
||||
break;
|
||||
if (d.Unit == null || d.Unit == mover || d.Unit.Faction == spear.Faction) continue;
|
||||
if (Vector2.Distance(d.From, d.To) < 0.05f) continue;
|
||||
if (Combat.SegmentPointDistance(d.From, d.To, spear.Position) <= radius + d.Unit.Radius) victims.Add(d.Unit);
|
||||
}
|
||||
}
|
||||
if (victims.Count == 0) continue;
|
||||
int strikes = Mathf.Max(1, SkillMods.Count(spear, SkillKeys.OverwatchCount, 1, true));
|
||||
foreach (var v in victims)
|
||||
{
|
||||
for (int k = 0; k < strikes; k++)
|
||||
{
|
||||
if (!v.IsAlive || !spear.IsAlive) break;
|
||||
int dmg = Combat.OverwatchDamage(Config, spear, v, Rng);
|
||||
if (spear.View != null) spear.View.PlayAttack();
|
||||
yield return Fx.Lunge(spear, v.Position);
|
||||
dmg = SkillSystem.Deal(v, dmg);
|
||||
Fx.Popup(v.Position, dmg + " 견제", new Color(0.6f, 0.85f, 1f));
|
||||
if (!v.IsAlive) HealOnOverwatchKill(spear);
|
||||
NotifyChanged();
|
||||
yield return Fx.Wait(0.2f);
|
||||
}
|
||||
}
|
||||
if (!triggered) continue;
|
||||
int dmg = Combat.OverwatchDamage(Config, spear, mover, Rng);
|
||||
if (spear.View != null) spear.View.PlayAttack();
|
||||
yield return Fx.Lunge(spear, mover.Position);
|
||||
mover.TakeDamage(dmg);
|
||||
Fx.Popup(mover.Position, dmg + " 견제", new Color(0.6f, 0.85f, 1f));
|
||||
NotifyChanged();
|
||||
yield return Fx.Wait(0.2f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>하북 명장: 견제로 적을 쓰러뜨리면 최대 HP × p 회복.</summary>
|
||||
void HealOnOverwatchKill(Unit spear)
|
||||
{
|
||||
float pct = SkillMods.Sum(spear, SkillKeys.HealOnOverwatchKill, true);
|
||||
if (pct <= 0f || !spear.IsAlive) return;
|
||||
int heal = Mathf.Max(1, Mathf.RoundToInt(spear.MaxHp * pct));
|
||||
int before = spear.Hp;
|
||||
spear.Hp = Mathf.Min(spear.MaxHp, spear.Hp + heal);
|
||||
if (spear.View != null) spear.View.Refresh();
|
||||
if (spear.Hp > before) Fx.Popup(spear.Position, "+" + (spear.Hp - before), SkillFx.HealColor);
|
||||
}
|
||||
|
||||
// ---------- 승패 ----------
|
||||
|
||||
public bool CheckBattleEnd()
|
||||
|
|
@ -427,9 +519,15 @@ namespace TKS
|
|||
NotifyChanged();
|
||||
}
|
||||
|
||||
/// <summary>테스트·캡처: 턴 번호 지정(스킬 쿨·선봉 1턴 확인).</summary>
|
||||
public void SetTurnForTest(int turn)
|
||||
{
|
||||
TurnNumber = Mathf.Max(1, turn);
|
||||
}
|
||||
|
||||
static string Describe(Unit u, ActionOutcome o, bool moveOnly)
|
||||
{
|
||||
string who = u.DisplayName + "(" + UnitTypeInfo.Korean(u.Type) + ")";
|
||||
string who = u.DisplayName + "(" + UnitTypeInfo.Korean(u.Type) + ")" + (o.SkillUsed != null ? " 「" + o.SkillUsed.NameKo + "」" : "");
|
||||
string s;
|
||||
if (o.Attacked.Count == 0)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -15,19 +15,22 @@ namespace TKS
|
|||
// ---------- 피해 공식 ----------
|
||||
|
||||
/// <summary>
|
||||
/// 피해 = (공격 × 상성 × 크리 − 방어 × defenseFactor) × 궁병 근접 배율 × 난수.
|
||||
/// 피해 = (공격 × 스킬 공 배율 × 상성(스킬 하한) × 크리 − 방어 × defenseFactor) × 궁병 근접 배율 × scale × 스킬 주는·받는 피해 배율 × 난수.
|
||||
/// melee = 근접 전투(돌진·반격·관통·견제·밀려 닿은 공격). 원거리 병종(궁병·책사)이 근접 유닛(보병·기병·창병)과 근접 전투하면
|
||||
/// 주는 피해 × archerMeleeDealtRatio · 받는 피해 × archerMeleeTakenRatio (PD 2026-11-04 #1080 · 책사 #1087 ⑤ 동일) · 원거리 사격은 패널티 없음.
|
||||
/// 주는 피해 × archerMeleeDealtRatio · 받는 피해 × archerMeleeTakenRatio (PD 2026-11-04 #1080 · 책사 #1087 ⑤ 동일 · 돌파 melee_penalty_off 면 없음) · 원거리 사격은 패널티 없음.
|
||||
/// scale = 호출 쪽 배율(관통 피해 배율 · 밀린 아군 공격 % 등) · 스킬 수치 = SkillMods(PD #1093 B).
|
||||
/// </summary>
|
||||
public static int Damage(BalanceConfig cfg, Unit attacker, Unit defender, bool crit, System.Random rng, bool melee)
|
||||
public static int Damage(BalanceConfig cfg, Unit attacker, Unit defender, bool crit, System.Random rng, bool melee, float scale = 1f)
|
||||
{
|
||||
float mult = cfg.Matchup(attacker.Type, defender.Type);
|
||||
float raw = attacker.Atk * mult * (crit ? cfg.critMultiplier : 1f) - defender.Def * cfg.defenseFactor;
|
||||
float mult = Mathf.Max(cfg.Matchup(attacker.Type, defender.Type), SkillMods.MatchupFloor(attacker, true));
|
||||
float atk = attacker.Atk * SkillMods.AttackMult(attacker, true);
|
||||
float raw = atk * mult * (crit ? cfg.critMultiplier : 1f) - defender.Def * cfg.defenseFactor;
|
||||
if (melee)
|
||||
{
|
||||
if (UnitTypeInfo.IsRanged(attacker.Type) && UnitTypeInfo.IsMelee(defender.Type)) raw *= cfg.archerMeleeDealtRatio;
|
||||
if (UnitTypeInfo.IsRanged(defender.Type) && UnitTypeInfo.IsMelee(attacker.Type)) raw *= cfg.archerMeleeTakenRatio;
|
||||
if (UnitTypeInfo.IsRanged(attacker.Type) && UnitTypeInfo.IsMelee(defender.Type) && !SkillMods.Has(attacker, SkillKeys.MeleePenaltyOff, true)) raw *= cfg.archerMeleeDealtRatio;
|
||||
if (UnitTypeInfo.IsRanged(defender.Type) && UnitTypeInfo.IsMelee(attacker.Type) && !SkillMods.Has(defender, SkillKeys.MeleePenaltyOff, true)) raw *= cfg.archerMeleeTakenRatio;
|
||||
}
|
||||
raw *= scale * SkillMods.DealtMult(attacker, defender, true) * SkillMods.TakenMult(defender, attacker, true);
|
||||
float variance = 1f + ((float)rng.NextDouble() * 2f - 1f) * cfg.damageVariance;
|
||||
return Mathf.Max(1, Mathf.RoundToInt(raw * variance));
|
||||
}
|
||||
|
|
@ -40,16 +43,24 @@ namespace TKS
|
|||
return Mathf.Max(1, Mathf.RoundToInt(baseDmg * ratio));
|
||||
}
|
||||
|
||||
public static int CounterDamage(BalanceConfig cfg, Unit defender, Unit attacker, System.Random rng)
|
||||
/// <summary>
|
||||
/// 반격 피해 = 반격하는 쪽(defender) 피해 × 반격 배율(무성·진중 등 · 기본 counterRatio) × 받는 쪽(attacker) 반격 피해 배율(군율 · 금범적).
|
||||
/// defenderPushed = 반격하는 유닛이 이 교환에서 밀렸다(알까기·밀며 전진·관통 넉백) — 진중(밀렸을 때만) 조건. 0 = 반격 없음.
|
||||
/// </summary>
|
||||
public static int CounterDamage(BalanceConfig cfg, Unit defender, Unit attacker, System.Random rng, bool defenderPushed = false)
|
||||
{
|
||||
float taken = SkillMods.PctProduct(attacker, SkillKeys.CounterTakenPct, true);
|
||||
if (taken <= 0f) return 0;
|
||||
float ratio = SkillMods.CounterRatio(defender, cfg.counterRatio, defenderPushed, true);
|
||||
int baseDmg = Damage(cfg, defender, attacker, false, rng, true);
|
||||
return Mathf.Max(1, Mathf.RoundToInt(baseDmg * cfg.counterRatio));
|
||||
return Mathf.Max(1, Mathf.RoundToInt(baseDmg * ratio * taken));
|
||||
}
|
||||
|
||||
/// <summary>견제 피해 = 근접 피해 × overwatchDamageRatio × 교변(overwatch_dmg_mult).</summary>
|
||||
public static int OverwatchDamage(BalanceConfig cfg, Unit spear, Unit mover, System.Random rng)
|
||||
{
|
||||
int baseDmg = Damage(cfg, spear, mover, false, rng, true);
|
||||
return Mathf.Max(1, Mathf.RoundToInt(baseDmg * cfg.overwatchDamageRatio));
|
||||
return Mathf.Max(1, Mathf.RoundToInt(baseDmg * cfg.overwatchDamageRatio * SkillMods.Product(spear, SkillKeys.OverwatchDmgMult, true)));
|
||||
}
|
||||
|
||||
// ---------- 기하 질의 ----------
|
||||
|
|
@ -98,8 +109,14 @@ namespace TKS
|
|||
/// <summary>
|
||||
/// 화살 투사체(PD 2026-11-05 #1087 ⑥): from 에서 dir 로 반지름 arrowRadius 원을 dist 만큼 날려 처음 닿는 유닛(아군·적군 불문 · 쏜 유닛 제외).
|
||||
/// 벽에 먼저 닿거나 아무도 없으면 null. travel = 화살이 멈춘 거리(유닛·벽 접촉 · 없으면 dist).
|
||||
/// passFaction = 그 진영 유닛은 통과(백발백중 arrow_pass_allies · 기본 null).
|
||||
/// </summary>
|
||||
public static Unit FirstUnitOnArrow(Unit shooter, Vector2 from, Vector2 dir, float dist, float arrowRadius, out float travel)
|
||||
{
|
||||
return FirstUnitOnArrow(shooter, from, dir, dist, arrowRadius, null, out travel);
|
||||
}
|
||||
|
||||
public static Unit FirstUnitOnArrow(Unit shooter, Vector2 from, Vector2 dir, float dist, float arrowRadius, Faction? passFaction, out float travel)
|
||||
{
|
||||
travel = dist;
|
||||
Physics2D.SyncTransforms();
|
||||
|
|
@ -114,6 +131,7 @@ namespace TKS
|
|||
}
|
||||
var u = UnitOf(h.collider);
|
||||
if (u == null || u == shooter || !u.IsAlive) continue;
|
||||
if (passFaction.HasValue && u.Faction == passFaction.Value) continue;
|
||||
travel = h.distance;
|
||||
return u;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,15 +32,16 @@ namespace TKS
|
|||
Vector2 toT = nearest.Position - e.Position;
|
||||
float dist = toT.magnitude;
|
||||
Vector2 dir = dist > 0.001f ? toT / dist : Vector2.up;
|
||||
float sling = SkillMods.SlingMax(e); // 적 무장 패시브(적토마 등 · PD #1093 B) 반영
|
||||
|
||||
Vector2 target;
|
||||
if (dist <= e.SlingMaxDistance)
|
||||
if (dist <= sling)
|
||||
{
|
||||
target = nearest.Position; // 최종 목표점 명중 시도
|
||||
}
|
||||
else
|
||||
{
|
||||
target = e.Position + dir * e.SlingMaxDistance; // 접근(궁병은 사거리 안이면 이동 후 사격)
|
||||
target = e.Position + dir * sling; // 접근(궁병은 사거리 안이면 이동 후 사격)
|
||||
}
|
||||
|
||||
if (rng.NextDouble() > accuracy)
|
||||
|
|
@ -50,11 +51,11 @@ namespace TKS
|
|||
float sn = Mathf.Sin(ang);
|
||||
Vector2 rd = new Vector2(dir.x * cs - dir.y * sn, dir.x * sn + dir.y * cs);
|
||||
float len = (target - e.Position).magnitude * (0.75f + 0.5f * (float)rng.NextDouble());
|
||||
target = e.Position + rd * Mathf.Min(len, e.SlingMaxDistance);
|
||||
target = e.Position + rd * Mathf.Min(len, sling);
|
||||
}
|
||||
|
||||
Vector2 v = target - e.Position;
|
||||
if (v.magnitude > e.SlingMaxDistance) v = v.normalized * e.SlingMaxDistance;
|
||||
if (v.magnitude > sling) v = v.normalized * sling;
|
||||
return board.ClampInside(e.Position + v, e.Radius);
|
||||
}
|
||||
|
||||
|
|
@ -68,7 +69,7 @@ namespace TKS
|
|||
Unit pick = null;
|
||||
int pickTier = int.MaxValue;
|
||||
float pickD = float.MaxValue;
|
||||
float reach = e.AttackRange + e.ArcherMoveAllow;
|
||||
float reach = SkillMods.Range(e) + e.ArcherMoveAllow;
|
||||
for (int i = 0; i < targets.Count; i++)
|
||||
{
|
||||
var t = targets[i];
|
||||
|
|
|
|||
|
|
@ -64,14 +64,14 @@ namespace TKS
|
|||
Hide();
|
||||
return;
|
||||
}
|
||||
float r = u.SlingMaxDistance;
|
||||
float r = SkillMods.SlingMax(u); // 스킬(동남풍·적토마) 반영 · 없으면 슬링샷 최대 거리
|
||||
ring.transform.localScale = new Vector3(r * 2f, r * 2f, 1f);
|
||||
ring.gameObject.SetActive(true);
|
||||
|
||||
bool archer = UnitTypeInfo.IsRanged(u.Type); // 궁병·책사(#1087 ⑤ 같은 사거리 규칙)
|
||||
if (archer)
|
||||
{
|
||||
float ro = r * (1f + ratio);
|
||||
float ro = SkillMods.Range(u) * (1f + ratio); // 사거리 스킬(단기·대도독) 반영 · 없으면 = 슬링 × (1 + 비율)
|
||||
donut.transform.localScale = new Vector3(ro * 2f, ro * 2f, 1f);
|
||||
donut.color = DonutColor;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7daafd22d5ac3a64aa327fffd860c149
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
|
||||
namespace TKS.OutGame
|
||||
{
|
||||
/// <summary>
|
||||
/// 스킬 화면 검증 캡처(에디터 DataImporter.CaptureSkills 가 SessionState 로 출력 폴더를 넘기면 생성 · 세이브 = 메모리 전용 신규 유저 · PD #1093 B).
|
||||
/// ① 스킬 레벨업(영웅 상세 → 팝업 · 액티브 Lv+1 뒤) ② 스킬 버튼(전투 · 관우 선택) ③ 액티브 발동(장비 장판 일갈 · 이름 글자 + 플래시 + 밀기)
|
||||
/// ④ 패시브 표시(조운 관통 → 상산의 창 HUD 한 줄). 캡처를 위해 적 1기씩을 아군 앞에 옮겨 둔다(그 밖은 실제 처리).
|
||||
/// </summary>
|
||||
public class SkillCaptureDriver : MonoBehaviour
|
||||
{
|
||||
public const string DirKey = "TKS_SKILL_CAPTURE_DIR";
|
||||
public string OutputDir;
|
||||
public int Width = 720;
|
||||
public int Height = 1280;
|
||||
|
||||
readonly List<string> log = new List<string>();
|
||||
int shot = 1;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||
static void AutoSpawn()
|
||||
{
|
||||
string dir = UnityEditor.SessionState.GetString(DirKey, "");
|
||||
if (string.IsNullOrEmpty(dir)) return;
|
||||
UnityEditor.SessionState.EraseString(DirKey);
|
||||
var d = new GameObject("SkillCaptureDriver").AddComponent<SkillCaptureDriver>();
|
||||
d.OutputDir = dir;
|
||||
}
|
||||
#endif
|
||||
|
||||
IEnumerator Start()
|
||||
{
|
||||
Directory.CreateDirectory(OutputDir);
|
||||
yield return null;
|
||||
yield return null;
|
||||
var root = OutGameRoot.Instance;
|
||||
var cam = Camera.main;
|
||||
if (root == null || cam == null)
|
||||
{
|
||||
Log("FAIL: OutGameRoot 또는 카메라 없음");
|
||||
Finish(1);
|
||||
yield break;
|
||||
}
|
||||
var s = root.Session;
|
||||
Log("memoryOnly=" + SaveStore.MemoryOnly + " newUser=" + s.IsNewUser + " books=" + s.Save.inv.books.skill + " gold=" + s.Save.gold);
|
||||
|
||||
// ① 스킬 레벨업: 첫 시작 영웅(★3 = 액티브 상한 +1) 액티브 Lv1 → 2
|
||||
var id = s.Db.Economy.StartHeroes[0];
|
||||
var h = s.Def(id);
|
||||
var hs = s.Save.Hero(id);
|
||||
root.ShowHero(id);
|
||||
yield return Wait();
|
||||
Log("skill cap active=" + SkillRules.Cap(s.Db, h, hs, 0) + " passive=" + SkillRules.Cap(s.Db, h, hs, 1) + " star=" + hs.star + " grade=" + hs.grade);
|
||||
Log("levelup active → " + (SkillRules.LevelUp(s.Db, s.Save, h, hs, 0) ?? "ok") + " lv=" + hs.skillLv[0] + " books=" + s.Save.inv.books.skill + " gold=" + s.Save.gold);
|
||||
s.Commit();
|
||||
root.HeroDetail.Refresh();
|
||||
root.HeroDetail.OnSkillUp();
|
||||
yield return Wait();
|
||||
yield return Shot(cam, "skill_levelup_" + id);
|
||||
root.CloseModal();
|
||||
|
||||
// 전투 1-1
|
||||
root.ShowLobby();
|
||||
root.OpenWorldMap();
|
||||
yield return Wait();
|
||||
var wm = root.Map;
|
||||
wm.OpenPopup(0);
|
||||
yield return Wait();
|
||||
wm.ConfirmPopup();
|
||||
var bm = root.Battle;
|
||||
float t0 = Time.realtimeSinceStartup;
|
||||
while (bm.Phase != BattlePhase.PlayerTurn && Time.realtimeSinceStartup - t0 < 10f) yield return null;
|
||||
var byHero = new Dictionary<string, Unit>();
|
||||
foreach (var u in bm.Units)
|
||||
{
|
||||
if (u == null || u.Faction != Faction.Player) continue;
|
||||
var us = bm.Skills.Of(u);
|
||||
Log(" " + u.DisplayName + " " + u.Type + " active=" + (us != null && us.Active != null ? us.Active.Id + " Lv" + us.ActiveLv : "-") + " passive=" + (us != null && us.Passive != null ? us.Passive.Id + " ×" + us.PassiveMult : "-") + " hp" + u.MaxHp + " @" + u.Position);
|
||||
if (us != null) byHero[us.HeroId] = u;
|
||||
}
|
||||
|
||||
// ② 스킬 버튼: 관우 선택(다음 행동형 · 사용 가능)
|
||||
Unit gy;
|
||||
if (byHero.TryGetValue("guanyu", out gy))
|
||||
{
|
||||
bm.Sling.Select(gy);
|
||||
yield return Wait();
|
||||
Log("button: " + bm.Hud.SkillButtonText);
|
||||
yield return Shot(cam, "skill_button_guanyu");
|
||||
}
|
||||
|
||||
// ③ 액티브 발동: 장비 장판 일갈(즉시 · 반경 안 적 밀기 + 이번 적 턴 교전 반응 없음)
|
||||
Unit zf;
|
||||
if (byHero.TryGetValue("zhangfei", out zf))
|
||||
{
|
||||
var foe = Nearest(bm, zf);
|
||||
if (foe != null) foe.Position = zf.Position + new Vector2(0f, 2f);
|
||||
Vector2 before = foe != null ? foe.Position : Vector2.zero;
|
||||
bm.Sling.Select(zf);
|
||||
yield return null;
|
||||
Log("press: " + bm.Hud.PressSkill());
|
||||
yield return new WaitForSecondsRealtime(0.18f);
|
||||
yield return Shot(cam, "active_zhangfei");
|
||||
t0 = Time.realtimeSinceStartup;
|
||||
while (bm.Phase != BattlePhase.PlayerTurn && Time.realtimeSinceStartup - t0 < 10f) yield return null;
|
||||
Log("pushed " + (foe != null ? foe.DisplayName + " " + before + "→" + foe.Position : "-") + " readyTurn=" + bm.Skills.Of(zf).ReadyTurn + " acted=" + zf.ActedThisTurn);
|
||||
}
|
||||
|
||||
// ④ 패시브 표시: 조운 관통(상산의 창 = 넉백 ×) → HUD 한 줄
|
||||
Unit zy;
|
||||
if (byHero.TryGetValue("zhaoyun", out zy))
|
||||
{
|
||||
var foe = Nearest(bm, zy);
|
||||
if (foe != null)
|
||||
{
|
||||
foe.Position = zy.Position + new Vector2(0f, 3.5f);
|
||||
bm.RequestPlayerAction(zy, foe.Position);
|
||||
t0 = Time.realtimeSinceStartup;
|
||||
while (bm.Phase != BattlePhase.PlayerTurn && Time.realtimeSinceStartup - t0 < 10f) yield return null;
|
||||
}
|
||||
bm.Sling.Select(zy);
|
||||
yield return null;
|
||||
Log("log: " + string.Join(" / ", bm.Skills.RecentLog(30f, 8).ToArray()));
|
||||
Log("msg: " + bm.LastMessage);
|
||||
yield return Shot(cam, "passive_zhaoyun");
|
||||
}
|
||||
Finish(0);
|
||||
}
|
||||
|
||||
static Unit Nearest(BattleManager bm, Unit from)
|
||||
{
|
||||
Unit best = null;
|
||||
float bd = float.MaxValue;
|
||||
foreach (var e in bm.AliveUnits(Faction.Enemy))
|
||||
{
|
||||
float d = Vector2.Distance(e.Position, from.Position);
|
||||
if (d < bd)
|
||||
{
|
||||
bd = d;
|
||||
best = e;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
static IEnumerator Wait()
|
||||
{
|
||||
yield return null;
|
||||
yield return new WaitForSecondsRealtime(0.3f);
|
||||
}
|
||||
|
||||
IEnumerator Shot(Camera cam, string name)
|
||||
{
|
||||
string file = Path.Combine(OutputDir, shot.ToString("00") + "_" + name + ".png");
|
||||
shot++;
|
||||
yield return CaptureUtil.Capture(cam, Width, Height, file);
|
||||
Log("shot " + file);
|
||||
}
|
||||
|
||||
void Log(string s)
|
||||
{
|
||||
log.Add(s);
|
||||
Debug.Log("[SkillCapture] " + s);
|
||||
try
|
||||
{
|
||||
File.WriteAllLines(Path.Combine(OutputDir, "capture_log.txt"), log.ToArray());
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
void Finish(int code)
|
||||
{
|
||||
Log("finish code=" + code);
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.EditorApplication.Exit(code);
|
||||
#else
|
||||
Application.Quit(code);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9feab08c83b313f46bd52dea2f5ff769
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
using System.Collections.Generic;
|
||||
using TKS.OutGame;
|
||||
using UnityEngine;
|
||||
|
||||
namespace TKS
|
||||
{
|
||||
public enum SkillTrigger
|
||||
{
|
||||
/// <summary>액티브 · 다음 슬링샷 1회에 붙는다(예약 · 행동 전 다시 탭 = 취소 · 쿨 소모 없음).</summary>
|
||||
NextAction,
|
||||
/// <summary>액티브 · 즉시 발동(행동 소모 없음).</summary>
|
||||
Instant,
|
||||
/// <summary>패시브 · 상시.</summary>
|
||||
Always
|
||||
}
|
||||
|
||||
/// <summary>스킬 효과 1개(effect_key 1개 + 값). 키가 하나뿐인 행은 p2·p3 = 그 키의 조건·보조값.</summary>
|
||||
public class SkillEffect
|
||||
{
|
||||
public string Key;
|
||||
public float P;
|
||||
public float A2;
|
||||
public float A3;
|
||||
public bool HasA2;
|
||||
public bool HasA3;
|
||||
|
||||
public SkillEffect Scaled(float factor)
|
||||
{
|
||||
SkillKeys.Info info;
|
||||
if (!SkillKeys.All.TryGetValue(Key, out info)) return this;
|
||||
return new SkillEffect
|
||||
{
|
||||
Key = Key,
|
||||
P = SkillKeys.Apply(info.Main, P, factor),
|
||||
A2 = info.ScaleA2 && HasA2 ? A2 * factor : A2,
|
||||
A3 = A3,
|
||||
HasA2 = HasA2,
|
||||
HasA3 = HasA3,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>skills.csv 1행의 전투용 정의(SkillInfoDb.Skill → effect_key 를 키 규약대로 풀어 둠).</summary>
|
||||
public class SkillDef
|
||||
{
|
||||
public string Id;
|
||||
public string HeroId;
|
||||
public string NameKo;
|
||||
public bool IsActive;
|
||||
public SkillTrigger Trigger;
|
||||
public int Cooldown;
|
||||
public int LvMax;
|
||||
public float LvScale;
|
||||
public bool NeedsImpl;
|
||||
public readonly List<SkillEffect> Effects = new List<SkillEffect>();
|
||||
|
||||
public static SkillDef From(SkillInfoDb.Skill s)
|
||||
{
|
||||
var d = new SkillDef
|
||||
{
|
||||
Id = s.id,
|
||||
HeroId = s.heroId,
|
||||
NameKo = s.nameKo,
|
||||
IsActive = s.kind == "active",
|
||||
Trigger = s.trigger == "next_action" ? SkillTrigger.NextAction : s.trigger == "instant" ? SkillTrigger.Instant : SkillTrigger.Always,
|
||||
Cooldown = s.cooldown,
|
||||
LvMax = Mathf.Max(1, s.lvMax),
|
||||
LvScale = s.lvScale,
|
||||
NeedsImpl = s.needsImpl,
|
||||
};
|
||||
var keys = s.EffectKeys;
|
||||
for (int i = 0; i < keys.Length; i++)
|
||||
{
|
||||
if (keys.Length > 1) d.Effects.Add(new SkillEffect { Key = keys[i], P = s.P(i) });
|
||||
else d.Effects.Add(new SkillEffect { Key = keys[i], P = s.p1, A2 = s.p2, A3 = s.p3, HasA2 = s.HasP(1), HasA3 = s.HasP(2) });
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
/// <summary>레벨 계수 = 1 + lv_scale·(lv−1) (lv 는 1~lv_max 로 자름).</summary>
|
||||
public float LevelFactor(int lv)
|
||||
{
|
||||
return 1f + LvScale * (Mathf.Clamp(lv, 1, LvMax) - 1);
|
||||
}
|
||||
|
||||
/// <summary>레벨·★ 배율(패시브 ★5 = evolve.csv passive_x)을 적용한 효과 목록.</summary>
|
||||
public List<SkillEffect> ScaledEffects(int lv, float starMult)
|
||||
{
|
||||
float f = LevelFactor(lv) * (IsActive ? 1f : starMult);
|
||||
var list = new List<SkillEffect>();
|
||||
foreach (var e in Effects) list.Add(e.Scaled(f));
|
||||
return list;
|
||||
}
|
||||
|
||||
public SkillEffect First(string key)
|
||||
{
|
||||
foreach (var e in Effects)
|
||||
if (e.Key == key) return e;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>스킬·영웅 표 조회(Resources/OutGame 의 SkillInfoDb · HeroDb = 임포트 자산 · 전투 시작마다 다시 읽음).</summary>
|
||||
public static class SkillCatalog
|
||||
{
|
||||
static Dictionary<string, SkillDef> defs;
|
||||
static SkillInfoDb source;
|
||||
static int sourceCount;
|
||||
|
||||
public static void Invalidate()
|
||||
{
|
||||
defs = null;
|
||||
}
|
||||
|
||||
static void Ensure()
|
||||
{
|
||||
var db = OutGameData.Default;
|
||||
var s = db != null ? db.Skills : null;
|
||||
if (defs != null && s == source && s != null && s.skills.Count == sourceCount) return;
|
||||
defs = new Dictionary<string, SkillDef>();
|
||||
source = s;
|
||||
sourceCount = s != null ? s.skills.Count : 0;
|
||||
if (s == null) return;
|
||||
foreach (var row in s.skills)
|
||||
{
|
||||
if (string.IsNullOrEmpty(row.id) || defs.ContainsKey(row.id)) continue;
|
||||
defs[row.id] = SkillDef.From(row);
|
||||
}
|
||||
}
|
||||
|
||||
public static SkillDef Get(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id)) return null;
|
||||
Ensure();
|
||||
SkillDef d;
|
||||
return defs.TryGetValue(id, out d) ? d : null;
|
||||
}
|
||||
|
||||
public static IEnumerable<SkillDef> All
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return defs.Values;
|
||||
}
|
||||
}
|
||||
|
||||
public static HeroDb.Hero Hero(string id)
|
||||
{
|
||||
var db = OutGameData.Default;
|
||||
return db != null && db.Heroes != null ? db.Heroes.Get(id) : null;
|
||||
}
|
||||
|
||||
/// <summary>적 무장 → heroes.csv 영웅(아트 id 가 같으면 · 없으면 이름이 같은 영웅) · 병사 유닛 제외 · 없으면 null.</summary>
|
||||
public static HeroDb.Hero HeroForEnemy(string art, string name)
|
||||
{
|
||||
var db = OutGameData.Default;
|
||||
if (db == null || db.Heroes == null) return null;
|
||||
HeroDb.Hero byName = null;
|
||||
foreach (var h in db.Heroes.heroes)
|
||||
{
|
||||
if (h.isSoldier) continue;
|
||||
if (!string.IsNullOrEmpty(art) && h.art == art) return h;
|
||||
if (byName == null && !string.IsNullOrEmpty(name) && h.nameKo == name) byName = h;
|
||||
}
|
||||
return byName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 18b39b6c930d65c468093779c77e4777
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace TKS
|
||||
{
|
||||
/// <summary>스킬 수치가 레벨(lv_scale)·★5(패시브 배율)로 커지는 방식.</summary>
|
||||
public enum SkillScale
|
||||
{
|
||||
/// <summary>값 × 계수(비율·가산·거리·반경 · 기획서 §5 「수치 × (1 + lv_scale·(lv−1))」).</summary>
|
||||
Value,
|
||||
/// <summary>배율(×k)은 1 을 넘는 몫만 키운다: 1 + (k − 1) × 계수 (×2 가 Lv5 에 ×2.8 이 되지 않게).</summary>
|
||||
Mult,
|
||||
/// <summary>횟수 = 반올림(값 × 계수).</summary>
|
||||
Count,
|
||||
/// <summary>켜고 끄는 값·치환 값(커지지 않음).</summary>
|
||||
Fixed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// effect_key 51종 처리기 목록(시스템 기획 §5 · skills.csv) — 키 이름 · 레벨 계수 적용 방식 · 같은 편 전원에게 걸리는가.
|
||||
/// 이 표는 「처리기의 뜻」이고 수치는 전부 skills.csv(SkillInfoDb)에서 온다(C45). 표에 없는 키는 임포터가 오류로 막는다.
|
||||
/// p 규약: 키가 여럿이면 p1~p3 = 키 순서 · 키 하나면 p1 = 값 · p2/p3 = 그 키의 조건·보조값(desc_ko [ ] 메모).
|
||||
/// </summary>
|
||||
public static class SkillKeys
|
||||
{
|
||||
public const string SlingDistPct = "sling_dist_pct";
|
||||
public const string PushMult = "push_mult";
|
||||
public const string SelfBounceMult = "self_bounce_mult";
|
||||
public const string ExtraMoveCount = "extra_move_count";
|
||||
public const string ExtraMoveAttack = "extra_move_attack";
|
||||
public const string OverwatchRadiusMult = "overwatch_radius_mult";
|
||||
public const string OverwatchUnlimited = "overwatch_unlimited";
|
||||
public const string OverwatchRadiusAdd = "overwatch_radius_add";
|
||||
public const string OverwatchCount = "overwatch_count";
|
||||
public const string OverwatchDmgMult = "overwatch_dmg_mult";
|
||||
public const string CounterRatio = "counter_ratio";
|
||||
public const string CounterTakenPct = "counter_taken_pct";
|
||||
public const string PierceNoCounter = "pierce_no_counter";
|
||||
public const string PierceReturnOnMiss = "pierce_return_on_miss";
|
||||
public const string PierceKnockbackMult = "pierce_knockback_mult";
|
||||
public const string PierceDmgPctPerHit = "pierce_dmg_pct_per_hit";
|
||||
public const string PierceRepeat = "pierce_repeat";
|
||||
public const string PierceDmgMult = "pierce_dmg_mult";
|
||||
public const string ArrowCritAlways = "arrow_crit_always";
|
||||
public const string ArrowPassAllies = "arrow_pass_allies";
|
||||
public const string RangeMult = "range_mult";
|
||||
public const string RangeAdd = "range_add";
|
||||
public const string MeleePenaltyOff = "melee_penalty_off";
|
||||
public const string SplashRadiusMult = "splash_radius_mult";
|
||||
public const string SplashRatio = "splash_ratio";
|
||||
public const string SplashAllyRatio = "splash_ally_ratio";
|
||||
public const string MatchupFloor = "matchup_floor";
|
||||
public const string DmgPct = "dmg_pct";
|
||||
public const string DmgPctHpBelow = "dmg_pct_hp_below";
|
||||
public const string DmgPctTurn1 = "dmg_pct_turn1";
|
||||
public const string DmgVsGeneralMult = "dmg_vs_general_mult";
|
||||
public const string DmgTakenPct = "dmg_taken_pct";
|
||||
public const string DmgTakenAura = "dmg_taken_aura";
|
||||
public const string DmgTakenFromArcherPct = "dmg_taken_from_archer_pct";
|
||||
public const string PushedAllyAttackPct = "pushed_ally_attack_pct";
|
||||
public const string PushedAllyAttackCount = "pushed_ally_attack_count";
|
||||
public const string KnockbackTakenPct = "knockback_taken_pct";
|
||||
public const string HpMaxPct = "hp_max_pct";
|
||||
public const string HealOnOverwatchKill = "heal_on_overwatch_kill";
|
||||
public const string HpCostAtk = "hp_cost_atk";
|
||||
public const string AoePushFear = "aoe_push_fear";
|
||||
public const string EnemyAggroAdd = "enemy_aggro_add";
|
||||
public const string EnemyAggroOffTurn = "enemy_aggro_off_turn";
|
||||
public const string TeamDmgTakenPctTurn = "team_dmg_taken_pct_turn";
|
||||
public const string GuardShare = "guard_share";
|
||||
public const string Teleport = "teleport";
|
||||
public const string OnAllyDeathAtk = "on_ally_death_atk";
|
||||
public const string TeamAtkPctFull = "team_atk_pct_full";
|
||||
public const string SpearHitTwo = "spear_hit_two";
|
||||
public const string PushedAllyAttackOnPush = "pushed_ally_attack_on_push";
|
||||
public const string TeamRangeAdd = "team_range_add";
|
||||
|
||||
public class Info
|
||||
{
|
||||
public SkillScale Main;
|
||||
/// <summary>p2(키 하나인 행의 보조값)도 같은 계수로 키우는가(절대 가산 · 최대치 · 충돌 추가 피해).</summary>
|
||||
public bool ScaleA2;
|
||||
/// <summary>같은 편 전원에게 걸리는 효과(주인 = 같은 편 · 살아 있을 때).</summary>
|
||||
public bool Team;
|
||||
}
|
||||
|
||||
static Info I(SkillScale m, bool a2 = false, bool team = false)
|
||||
{
|
||||
return new Info { Main = m, ScaleA2 = a2, Team = team };
|
||||
}
|
||||
|
||||
public static readonly Dictionary<string, Info> All = new Dictionary<string, Info>
|
||||
{
|
||||
{ SlingDistPct, I(SkillScale.Value, true) }, // p1 비율 · p2 절대 가산 · p3 1 = 아군 전원
|
||||
{ PushMult, I(SkillScale.Mult, true) }, // p2 = 밀린 적이 벽·유닛에 닿으면 추가 피해 비율(고순)
|
||||
{ SelfBounceMult, I(SkillScale.Value) },
|
||||
{ ExtraMoveCount, I(SkillScale.Count) },
|
||||
{ ExtraMoveAttack, I(SkillScale.Fixed) },
|
||||
{ OverwatchRadiusMult, I(SkillScale.Mult) },
|
||||
{ OverwatchUnlimited, I(SkillScale.Fixed) },
|
||||
{ OverwatchRadiusAdd, I(SkillScale.Value) },
|
||||
{ OverwatchCount, I(SkillScale.Count) },
|
||||
{ OverwatchDmgMult, I(SkillScale.Mult) },
|
||||
{ CounterRatio, I(SkillScale.Value) }, // p2 1 = 밀렸을 때만
|
||||
{ CounterTakenPct, I(SkillScale.Value) },
|
||||
{ PierceNoCounter, I(SkillScale.Fixed) },
|
||||
{ PierceReturnOnMiss, I(SkillScale.Fixed) },
|
||||
{ PierceKnockbackMult, I(SkillScale.Mult) },
|
||||
{ PierceDmgPctPerHit, I(SkillScale.Value, true) }, // p2 = 최대
|
||||
{ PierceRepeat, I(SkillScale.Count) },
|
||||
{ PierceDmgMult, I(SkillScale.Mult) }, // p2 = 최소 관통 수(키 하나일 때)
|
||||
{ ArrowCritAlways, I(SkillScale.Fixed) },
|
||||
{ ArrowPassAllies, I(SkillScale.Fixed) },
|
||||
{ RangeMult, I(SkillScale.Mult) },
|
||||
{ RangeAdd, I(SkillScale.Value) },
|
||||
{ MeleePenaltyOff, I(SkillScale.Fixed) },
|
||||
{ SplashRadiusMult, I(SkillScale.Mult) },
|
||||
{ SplashRatio, I(SkillScale.Value) },
|
||||
{ SplashAllyRatio, I(SkillScale.Value) },
|
||||
{ MatchupFloor, I(SkillScale.Fixed) },
|
||||
{ DmgPct, I(SkillScale.Value) },
|
||||
{ DmgPctHpBelow, I(SkillScale.Value) }, // p2 = HP 임계
|
||||
{ DmgPctTurn1, I(SkillScale.Value) },
|
||||
{ DmgVsGeneralMult, I(SkillScale.Mult) }, // p2 1 = 보스만 · 0 = 무장 전체
|
||||
{ DmgTakenPct, I(SkillScale.Value) },
|
||||
{ DmgTakenAura, I(SkillScale.Value, false, true) }, // p2 = 반경
|
||||
{ DmgTakenFromArcherPct, I(SkillScale.Value) },
|
||||
{ PushedAllyAttackPct, I(SkillScale.Value, false, true) },
|
||||
{ PushedAllyAttackCount, I(SkillScale.Count) },
|
||||
{ KnockbackTakenPct, I(SkillScale.Value) },
|
||||
{ HpMaxPct, I(SkillScale.Value) },
|
||||
{ HealOnOverwatchKill, I(SkillScale.Value) },
|
||||
{ HpCostAtk, I(SkillScale.Value) }, // p2 = HP 소모(커지지 않음)
|
||||
{ AoePushFear, I(SkillScale.Value) }, // p2 = 반경
|
||||
{ EnemyAggroAdd, I(SkillScale.Value) },
|
||||
{ EnemyAggroOffTurn, I(SkillScale.Fixed) },
|
||||
{ TeamDmgTakenPctTurn, I(SkillScale.Value, false, true) },
|
||||
{ GuardShare, I(SkillScale.Value) }, // p2 = 반경
|
||||
{ Teleport, I(SkillScale.Value) }, // p1 = 반경(0 = 슬링 목표점)
|
||||
{ OnAllyDeathAtk, I(SkillScale.Value) },
|
||||
{ TeamAtkPctFull, I(SkillScale.Value, false, true) }, // p2 = 출전 인원 조건
|
||||
{ SpearHitTwo, I(SkillScale.Count) },
|
||||
{ PushedAllyAttackOnPush, I(SkillScale.Count) },
|
||||
{ TeamRangeAdd, I(SkillScale.Value, false, true) }, // 대상 = 궁병·책사
|
||||
};
|
||||
|
||||
public static bool Known(string key)
|
||||
{
|
||||
return key != null && All.ContainsKey(key);
|
||||
}
|
||||
|
||||
/// <summary>레벨·★ 계수 factor 를 적용한 값.</summary>
|
||||
public static float Apply(SkillScale mode, float p, float factor)
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case SkillScale.Value: return p * factor;
|
||||
case SkillScale.Mult: return 1f + (p - 1f) * factor;
|
||||
case SkillScale.Count: return UnityEngine.Mathf.Floor(p * factor + 0.5f);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 65ac45ce344197a4b9bb6dd51d5b8493
|
||||
|
|
@ -0,0 +1,373 @@
|
|||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace TKS
|
||||
{
|
||||
/// <summary>
|
||||
/// 스킬 수치 질의 — ActionResolver · Combat · EnemyAI · BattleManager 가 수치를 읽을 때 부른다(시스템 기획 §5 · PD #1093 B).
|
||||
/// Get(unit, key) = 그 유닛에게 지금 걸린 효과(자기 패시브 · 같은 편 패시브(팀 키) · 장착된 예약 액티브 · 시한 효과).
|
||||
/// 스킬 시스템이 없거나 효과가 없으면 중립값(합 0 · 곱 1)이라 기존 규칙 그대로다.
|
||||
/// note = 실제 적용(피해·밀기) 때만 true → 패시브 발동 HUD 한 줄.
|
||||
/// </summary>
|
||||
public static class SkillMods
|
||||
{
|
||||
static readonly List<SkillHit> Empty = new List<SkillHit>();
|
||||
|
||||
public static List<SkillHit> Get(Unit u, string key)
|
||||
{
|
||||
var sys = SkillSystem.Current;
|
||||
if (sys == null || u == null) return Empty;
|
||||
List<SkillHit> list = null;
|
||||
foreach (var s in sys.Entries)
|
||||
{
|
||||
var src = s.Unit;
|
||||
if (src == null) continue;
|
||||
if (s.PassiveEffects != null && src.IsAlive)
|
||||
{
|
||||
foreach (var e in s.PassiveEffects)
|
||||
{
|
||||
if (e.Key != key || !Applies(src, u, e)) continue;
|
||||
if (list == null) list = new List<SkillHit>();
|
||||
list.Add(new SkillHit { Owner = s, Def = s.Passive, E = e, Passive = true });
|
||||
}
|
||||
}
|
||||
if (s.Armed && src == u && s.ActiveEffects != null)
|
||||
{
|
||||
foreach (var e in s.ActiveEffects)
|
||||
{
|
||||
if (e.Key != key) continue;
|
||||
if (list == null) list = new List<SkillHit>();
|
||||
list.Add(new SkillHit { Owner = s, Def = s.Active, E = e, Passive = false });
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (var t in sys.TimedEffects)
|
||||
{
|
||||
if (t.E.Key != key || !sys.WindowActive(t) || !TimedApplies(t, u)) continue;
|
||||
if (list == null) list = new List<SkillHit>();
|
||||
list.Add(new SkillHit { Owner = t.Owner, Def = t.Def, E = t.E, Passive = false });
|
||||
}
|
||||
return list ?? Empty;
|
||||
}
|
||||
|
||||
static bool IsTeam(SkillEffect e)
|
||||
{
|
||||
SkillKeys.Info info;
|
||||
if (e.Key == SkillKeys.SlingDistPct) return e.HasA3 && e.A3 >= 0.5f; // p3 1 = 아군 전원
|
||||
return SkillKeys.All.TryGetValue(e.Key, out info) && info.Team;
|
||||
}
|
||||
|
||||
static bool Applies(Unit src, Unit u, SkillEffect e)
|
||||
{
|
||||
if (!IsTeam(e)) return src == u;
|
||||
if (src.Faction != u.Faction) return false;
|
||||
if (e.Key == SkillKeys.TeamRangeAdd) return UnitTypeInfo.IsRanged(u.Type);
|
||||
if (e.Key == SkillKeys.DmgTakenAura) return Vector2.Distance(src.Position, u.Position) <= (e.HasA2 ? e.A2 : 0f) + u.Radius;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TimedApplies(SkillSystem.Timed t, Unit u)
|
||||
{
|
||||
if (t.Targets != null) return t.Targets.Contains(u);
|
||||
var src = t.Owner != null ? t.Owner.Unit : null;
|
||||
if (src == null) return false;
|
||||
if (!IsTeam(t.E)) return src == u;
|
||||
if (u.Faction != t.Faction) return false;
|
||||
if (t.E.Key == SkillKeys.DmgTakenAura)
|
||||
return src.IsAlive && Vector2.Distance(src.Position, u.Position) <= (t.E.HasA2 ? t.E.A2 : 0f) + u.Radius;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void Note(SkillHit h, bool note)
|
||||
{
|
||||
if (!note || !h.Passive || SkillSystem.Current == null) return;
|
||||
SkillSystem.Current.NotePassive(h.Owner, h.Def);
|
||||
}
|
||||
|
||||
// ---------- 일반 ----------
|
||||
|
||||
public static float Sum(Unit u, string key, bool note = false)
|
||||
{
|
||||
float s = 0f;
|
||||
foreach (var h in Get(u, key))
|
||||
{
|
||||
s += h.E.P;
|
||||
Note(h, note);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
public static float Product(Unit u, string key, bool note = false)
|
||||
{
|
||||
float p = 1f;
|
||||
foreach (var h in Get(u, key))
|
||||
{
|
||||
p *= h.E.P;
|
||||
Note(h, note);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/// <summary>Π(1 + 값) · 0 아래로 내려가지 않음(받는 피해 −% 류).</summary>
|
||||
public static float PctProduct(Unit u, string key, bool note = false)
|
||||
{
|
||||
float p = 1f;
|
||||
foreach (var h in Get(u, key))
|
||||
{
|
||||
p *= Mathf.Max(0f, 1f + h.E.P);
|
||||
Note(h, note);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
public static bool Has(Unit u, string key, bool note = false)
|
||||
{
|
||||
var l = Get(u, key);
|
||||
foreach (var h in l) Note(h, note);
|
||||
return l.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>횟수 = 걸린 효과 중 최댓값 · 없으면 fallback.</summary>
|
||||
public static int Count(Unit u, string key, int fallback, bool note = false)
|
||||
{
|
||||
var l = Get(u, key);
|
||||
if (l.Count == 0) return fallback;
|
||||
int n = int.MinValue;
|
||||
foreach (var h in l)
|
||||
{
|
||||
n = Mathf.Max(n, Mathf.RoundToInt(h.E.P));
|
||||
Note(h, note);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// ---------- 슬링 거리 · 사거리 · 광역 ----------
|
||||
|
||||
/// <summary>슬링 최대 거리 = 기본 × (1 + 비율 합) + 절대 가산(적토마 p2) — 동남풍(이번 턴 아군 전원) 포함.</summary>
|
||||
public static float SlingMax(Unit u, bool note = false)
|
||||
{
|
||||
if (u == null) return 0f;
|
||||
float pct = 0f;
|
||||
float add = 0f;
|
||||
foreach (var h in Get(u, SkillKeys.SlingDistPct))
|
||||
{
|
||||
pct += h.E.P;
|
||||
if (h.E.HasA2) add += h.E.A2;
|
||||
Note(h, note);
|
||||
}
|
||||
return Mathf.Max(0.1f, u.SlingMaxDistance * (1f + pct) + add);
|
||||
}
|
||||
|
||||
/// <summary>궁병·책사 사거리 = (기본 + range_add + 대도독 team_range_add) × range_mult.</summary>
|
||||
public static float Range(Unit u, bool note = false)
|
||||
{
|
||||
if (u == null) return 0f;
|
||||
float r = u.AttackRange + Sum(u, SkillKeys.RangeAdd, note) + Sum(u, SkillKeys.TeamRangeAdd, note);
|
||||
return Mathf.Max(0f, r * Product(u, SkillKeys.RangeMult, note));
|
||||
}
|
||||
|
||||
public static float SplashRadius(Unit u, BalanceConfig cfg, bool note = false)
|
||||
{
|
||||
return cfg.strategistSplashRadius * Product(u, SkillKeys.SplashRadiusMult, note);
|
||||
}
|
||||
|
||||
/// <summary>광역 비율 = 화공 치환값(여럿이면 큰 값) · 없으면 자산 값.</summary>
|
||||
public static float SplashRatio(Unit u, BalanceConfig cfg, bool note = false)
|
||||
{
|
||||
var l = Get(u, SkillKeys.SplashRatio);
|
||||
if (l.Count == 0) return cfg.strategistSplashRatio;
|
||||
float r = float.MinValue;
|
||||
foreach (var h in l)
|
||||
{
|
||||
r = Mathf.Max(r, h.E.P);
|
||||
Note(h, note);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/// <summary>광역 아군 비율 = 팔문 간파 치환값(여럿이면 작은 값) · 없으면 자산 값.</summary>
|
||||
public static float SplashAllyRatio(Unit u, BalanceConfig cfg, bool note = false)
|
||||
{
|
||||
var l = Get(u, SkillKeys.SplashAllyRatio);
|
||||
if (l.Count == 0) return cfg.strategistAllyDamageRatio;
|
||||
float r = float.MaxValue;
|
||||
foreach (var h in l)
|
||||
{
|
||||
r = Mathf.Min(r, h.E.P);
|
||||
Note(h, note);
|
||||
}
|
||||
return Mathf.Max(0f, r);
|
||||
}
|
||||
|
||||
// ---------- 피해 ----------
|
||||
|
||||
/// <summary>상성 하한(대부 · 0 = 없음).</summary>
|
||||
public static float MatchupFloor(Unit a, bool note = false)
|
||||
{
|
||||
float f = 0f;
|
||||
foreach (var h in Get(a, SkillKeys.MatchupFloor))
|
||||
{
|
||||
f = Mathf.Max(f, h.E.P);
|
||||
Note(h, note);
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
/// <summary>공격력 배율 = 1 + 「공 +%」 합(노익장·맹장 HP 조건 · 선봉 1턴 · 고육계 · 복수 · 패자 · 팔백 돌격).</summary>
|
||||
public static float AttackMult(Unit a, bool note = false)
|
||||
{
|
||||
var sys = SkillSystem.Current;
|
||||
if (sys == null || a == null) return 1f;
|
||||
float pct = 0f;
|
||||
foreach (var h in Get(a, SkillKeys.DmgPctHpBelow))
|
||||
{
|
||||
float th = h.E.HasA2 ? h.E.A2 : 0f;
|
||||
if (a.Hp > a.MaxHp * th) continue;
|
||||
pct += h.E.P;
|
||||
Note(h, note);
|
||||
}
|
||||
if (sys.Battle.TurnNumber == 1) pct += Sum(a, SkillKeys.DmgPctTurn1, note);
|
||||
pct += Sum(a, SkillKeys.HpCostAtk, note);
|
||||
var deaths = Get(a, SkillKeys.OnAllyDeathAtk);
|
||||
if (deaths.Count > 0)
|
||||
{
|
||||
int dead = DeadAllies(a);
|
||||
if (dead > 0)
|
||||
foreach (var h in deaths)
|
||||
{
|
||||
pct += h.E.P * dead;
|
||||
Note(h, note);
|
||||
}
|
||||
}
|
||||
foreach (var h in Get(a, SkillKeys.TeamAtkPctFull))
|
||||
{
|
||||
int need = h.E.HasA2 ? Mathf.RoundToInt(h.E.A2) : 0;
|
||||
if (sys.StartCount(a.Faction) < need) continue;
|
||||
pct += h.E.P;
|
||||
Note(h, note);
|
||||
}
|
||||
foreach (var h in Get(a, SkillKeys.PierceDmgPctPerHit))
|
||||
{
|
||||
var s = sys.Of(a);
|
||||
int hits = s != null ? s.PierceHits : 0;
|
||||
if (hits <= 0) continue;
|
||||
float v = h.E.P * hits;
|
||||
if (h.E.HasA2) v = Mathf.Min(v, h.E.A2);
|
||||
pct += v;
|
||||
Note(h, note);
|
||||
}
|
||||
return Mathf.Max(0f, 1f + pct);
|
||||
}
|
||||
|
||||
static int DeadAllies(Unit a)
|
||||
{
|
||||
var sys = SkillSystem.Current;
|
||||
if (sys == null) return 0;
|
||||
int n = 0;
|
||||
foreach (var u in sys.Battle.Units)
|
||||
if (u != null && u != a && u.Faction == a.Faction && !u.IsAlive) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
/// <summary>주는 피해 배율 = (1 + 알까기 피해 %) × 무장 대상 배율(참장 = 영웅·보스 · 파로 = 보스만).</summary>
|
||||
public static float DealtMult(Unit a, Unit d, bool note = false)
|
||||
{
|
||||
var sys = SkillSystem.Current;
|
||||
float m = 1f + Sum(a, SkillKeys.DmgPct, note);
|
||||
foreach (var h in Get(a, SkillKeys.DmgVsGeneralMult))
|
||||
{
|
||||
bool bossOnly = h.E.HasA2 && h.E.A2 >= 0.5f;
|
||||
bool ok = bossOnly ? d.IsBoss : (sys != null && sys.IsGeneral(d));
|
||||
if (!ok) continue;
|
||||
m *= h.E.P;
|
||||
Note(h, note);
|
||||
}
|
||||
return Mathf.Max(0f, m);
|
||||
}
|
||||
|
||||
/// <summary>받는 피해 배율 = 수성·노장 × 간웅(이번 적 턴) × 철벽(반경) × 적 궁병 피해 감소.</summary>
|
||||
public static float TakenMult(Unit d, Unit a, bool note = false)
|
||||
{
|
||||
float m = PctProduct(d, SkillKeys.DmgTakenPct, note) * PctProduct(d, SkillKeys.TeamDmgTakenPctTurn, note) * PctProduct(d, SkillKeys.DmgTakenAura, note);
|
||||
if (a != null && a.Type == UnitType.Archer) m *= PctProduct(d, SkillKeys.DmgTakenFromArcherPct, note);
|
||||
return m;
|
||||
}
|
||||
|
||||
/// <summary>반격 배율 = 무성·무언의 규율(치환) · 진중(p2 1 = 밀렸을 때만).</summary>
|
||||
public static float CounterRatio(Unit counterer, float baseRatio, bool pushed, bool note = false)
|
||||
{
|
||||
float r = baseRatio;
|
||||
foreach (var h in Get(counterer, SkillKeys.CounterRatio))
|
||||
{
|
||||
if (h.E.HasA2 && h.E.A2 >= 0.5f && !pushed) continue;
|
||||
if (h.E.P <= r) continue;
|
||||
r = h.E.P;
|
||||
Note(h, note);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
// ---------- 밀기 · 튕김 · 관통 ----------
|
||||
|
||||
public static float PushMult(Unit actor, bool note = false)
|
||||
{
|
||||
return Mathf.Max(0f, Product(actor, SkillKeys.PushMult, note));
|
||||
}
|
||||
|
||||
/// <summary>함진영: 밀린 적이 벽·유닛에 닿으면 추가 피해 비율(push_mult 행의 p2).</summary>
|
||||
public static float PushCollisionBonus(Unit actor)
|
||||
{
|
||||
float b = 0f;
|
||||
foreach (var h in Get(actor, SkillKeys.PushMult))
|
||||
if (h.E.HasA2) b += h.E.A2;
|
||||
return b;
|
||||
}
|
||||
|
||||
public static float SelfBounceMult(Unit actor, bool note = false)
|
||||
{
|
||||
return Mathf.Max(0f, Product(actor, SkillKeys.SelfBounceMult, note));
|
||||
}
|
||||
|
||||
/// <summary>밀리는 거리 배율(거구 −50 %).</summary>
|
||||
public static float KnockbackTakenMult(Unit target, bool note = false)
|
||||
{
|
||||
return PctProduct(target, SkillKeys.KnockbackTakenPct, note);
|
||||
}
|
||||
|
||||
public static float PierceKnockbackMult(Unit actor, bool note = false)
|
||||
{
|
||||
return Mathf.Max(0f, Product(actor, SkillKeys.PierceKnockbackMult, note));
|
||||
}
|
||||
|
||||
/// <summary>관통 피해 배율 · 키 하나인 행의 p2 = 최소 관통 수(만인지적 2기 이상).</summary>
|
||||
public static float PierceDmgMult(Unit actor, int pierced, bool note = false)
|
||||
{
|
||||
float m = 1f;
|
||||
foreach (var h in Get(actor, SkillKeys.PierceDmgMult))
|
||||
{
|
||||
if (h.E.HasA2 && pierced < Mathf.RoundToInt(h.E.A2)) continue;
|
||||
m *= h.E.P;
|
||||
Note(h, note);
|
||||
}
|
||||
return Mathf.Max(0f, m);
|
||||
}
|
||||
|
||||
// ---------- 견제 ----------
|
||||
|
||||
/// <summary>견제 반경 = (기본 + 후계 등 패시브 가산 + 이번 대기의 액티브 가산) × 액티브 배율(청룡언월).</summary>
|
||||
public static float OverwatchRadius(Unit spear, bool note = false)
|
||||
{
|
||||
if (spear == null) return 0f;
|
||||
var s = SkillSystem.Current != null ? SkillSystem.Current.Of(spear) : null;
|
||||
float add = Sum(spear, SkillKeys.OverwatchRadiusAdd, note) + (s != null ? s.OwAdd : 0f);
|
||||
float mult = s != null ? s.OwMult : 1f;
|
||||
return Mathf.Max(0f, (spear.AttackRange + add) * mult);
|
||||
}
|
||||
|
||||
public static bool OverwatchUnlimited(Unit spear)
|
||||
{
|
||||
var s = SkillSystem.Current != null ? SkillSystem.Current.Of(spear) : null;
|
||||
return (s != null && s.OwUnlimited) || Has(spear, SkillKeys.OverwatchUnlimited);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7410190a7c798be4baf6e370a7e3461f
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using UnityEngine;
|
||||
|
||||
namespace TKS.OutGame
|
||||
{
|
||||
/// <summary>
|
||||
/// 스킬 성장 규칙(시스템 기획 §3-5·§5 · PD #1093 B): 레벨 1~lv_max · 상한 = 승급 등급(HeroRules.SkillCap) + ★ 보정(evolve.csv skill_unlock 「active_lv+n」 = 액티브 상한 +n) ·
|
||||
/// ★ 패시브 배율(「passive_x1.5」) · 레벨업 비용 = skill_levels.csv(스킬서 + 골드) · 스킬서 임시 판매(economy.csv skill_book_* · 획득처 사당 던전은 C 단계).
|
||||
/// </summary>
|
||||
public static class SkillRules
|
||||
{
|
||||
const string ActiveCapToken = "active_lv+";
|
||||
const string PassiveMultToken = "passive_x";
|
||||
|
||||
/// <summary>★ star 까지 해금된 액티브 상한 보정 합(evolve.csv skill_unlock).</summary>
|
||||
public static int ActiveCapBonus(OutGameData db, int star)
|
||||
{
|
||||
int add = 0;
|
||||
if (db == null || db.Evolve == null) return 0;
|
||||
foreach (var r in db.Evolve.rows)
|
||||
{
|
||||
if (r.star > star || string.IsNullOrEmpty(r.skillUnlock) || !r.skillUnlock.StartsWith(ActiveCapToken)) continue;
|
||||
int n;
|
||||
if (int.TryParse(r.skillUnlock.Substring(ActiveCapToken.Length), NumberStyles.Integer, CultureInfo.InvariantCulture, out n)) add += n;
|
||||
}
|
||||
return add;
|
||||
}
|
||||
|
||||
/// <summary>★ star 까지 해금된 패시브 효과 배율(곱 · 없으면 1).</summary>
|
||||
public static float PassiveMult(OutGameData db, int star)
|
||||
{
|
||||
float m = 1f;
|
||||
if (db == null || db.Evolve == null) return m;
|
||||
foreach (var r in db.Evolve.rows)
|
||||
{
|
||||
if (r.star > star || string.IsNullOrEmpty(r.skillUnlock) || !r.skillUnlock.StartsWith(PassiveMultToken)) continue;
|
||||
float f;
|
||||
if (float.TryParse(r.skillUnlock.Substring(PassiveMultToken.Length), NumberStyles.Float, CultureInfo.InvariantCulture, out f)) m *= f;
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
/// <summary>스킬 i(0 = 액티브 · 1 = 패시브) 레벨 상한 = 등급 순서 + (액티브면 ★ 보정) · 최대 lv_max.</summary>
|
||||
public static int Cap(OutGameData db, HeroDb.Hero h, HeroState s, int i)
|
||||
{
|
||||
var sk = Skill(db, h, i);
|
||||
if (sk == null || s == null) return 1;
|
||||
int cap = HeroRules.SkillCap(db, s) + (i == 0 ? ActiveCapBonus(db, s.star) : 0);
|
||||
return Mathf.Clamp(cap, 1, Mathf.Min(sk.lvMax, db.Skills.MaxLevel));
|
||||
}
|
||||
|
||||
public static SkillInfoDb.Skill Skill(OutGameData db, HeroDb.Hero h, int i)
|
||||
{
|
||||
if (db == null || db.Skills == null || h == null) return null;
|
||||
return db.Skills.Get(i == 0 ? h.skillActive : h.skillPassive);
|
||||
}
|
||||
|
||||
public static int Level(HeroState s, int i)
|
||||
{
|
||||
return s != null && s.skillLv != null && s.skillLv.Length > i ? Mathf.Max(1, s.skillLv[i]) : 1;
|
||||
}
|
||||
|
||||
/// <summary>레벨업 가능하면 null · 아니면 이유.</summary>
|
||||
public static string CanLevelUp(OutGameData db, SaveData d, HeroDb.Hero h, HeroState s, int i)
|
||||
{
|
||||
if (h == null || s == null || h.isSoldier) return "병사 유닛은 스킬 없음";
|
||||
var sk = Skill(db, h, i);
|
||||
if (sk == null) return "스킬 없음";
|
||||
int lv = Level(s, i);
|
||||
int cap = Cap(db, h, s, i);
|
||||
if (lv >= sk.lvMax) return "최고 레벨";
|
||||
if (lv >= cap) return "상한 Lv " + cap + " — 승급" + (i == 0 ? "·★3" : "") + "으로 해금";
|
||||
var c = db.Skills.CostTo(lv + 1);
|
||||
if (c == null) return "비용 표 없음";
|
||||
if (d.inv.books.skill < c.books) return "스킬서 " + d.inv.books.skill + " / " + c.books;
|
||||
if (d.gold < c.gold) return "골드 " + d.gold.ToString("N0") + " / " + c.gold.ToString("N0");
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string LevelUp(OutGameData db, SaveData d, HeroDb.Hero h, HeroState s, int i)
|
||||
{
|
||||
var why = CanLevelUp(db, d, h, s, i);
|
||||
if (why != null) return why;
|
||||
var c = db.Skills.CostTo(Level(s, i) + 1);
|
||||
d.inv.books.skill -= c.books;
|
||||
d.gold -= c.gold;
|
||||
if (s.skillLv == null || s.skillLv.Length < 2) s.skillLv = new[] { 1, 1 };
|
||||
s.skillLv[i] = Level(s, i) + 1;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>출전 칸에 스킬(id · 레벨 = 상한 안 · ★ 패시브 배율 · 무장)을 채운다.</summary>
|
||||
public static void FillLineup(OutGameData db, SaveData d, HeroDb.Hero h, LineupEntry e)
|
||||
{
|
||||
if (h == null || e == null || h.isSoldier) return;
|
||||
var s = d.Hero(h.id);
|
||||
e.general = true;
|
||||
e.skillActive = h.skillActive;
|
||||
e.skillPassive = h.skillPassive;
|
||||
e.activeLv = Mathf.Min(Level(s, 0), Cap(db, h, s, 0));
|
||||
e.passiveLv = Mathf.Min(Level(s, 1), Cap(db, h, s, 1));
|
||||
e.passiveMult = PassiveMult(db, s != null ? s.star : h.rarity);
|
||||
}
|
||||
|
||||
// ---------- 스킬서 임시 판매(거래 > 상점 · PD 확인 대상) ----------
|
||||
|
||||
public static int BooksBoughtToday(SaveData d, DateTime nowUtc)
|
||||
{
|
||||
return d.shop.bookDay == EconomyRules.DayKey(nowUtc) ? d.shop.bookBought : 0;
|
||||
}
|
||||
|
||||
public static string CanBuyBook(OutGameData db, SaveData d, DateTime nowUtc)
|
||||
{
|
||||
var e = db.Economy;
|
||||
if (BooksBoughtToday(d, nowUtc) >= e.SkillBookShopDaily) return "오늘 구매 " + e.SkillBookShopDaily + "회 완료";
|
||||
if (d.gold < e.SkillBookPrice) return "골드 " + d.gold.ToString("N0") + " / " + e.SkillBookPrice.ToString("N0");
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string BuyBook(OutGameData db, SaveData d, DateTime nowUtc)
|
||||
{
|
||||
var why = CanBuyBook(db, d, nowUtc);
|
||||
if (why != null) return why;
|
||||
var day = EconomyRules.DayKey(nowUtc);
|
||||
if (d.shop.bookDay != day)
|
||||
{
|
||||
d.shop.bookDay = day;
|
||||
d.shop.bookBought = 0;
|
||||
}
|
||||
d.shop.bookBought++;
|
||||
d.gold -= db.Economy.SkillBookPrice;
|
||||
d.inv.books.skill++;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ebc0a493bed15e643ab085730236eb9f
|
||||
|
|
@ -0,0 +1,730 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace TKS
|
||||
{
|
||||
/// <summary>유닛 1기의 스킬 상태(액티브 쿨·예약 · 패시브 · 행동 뒤에도 남는 효과).</summary>
|
||||
public class UnitSkills
|
||||
{
|
||||
public Unit Unit;
|
||||
public string HeroId = "";
|
||||
/// <summary>무장(영웅·보스·엘리트 장수) — 참장·파로(dmg_vs_general_mult) 대상 판정.</summary>
|
||||
public bool IsGeneral;
|
||||
|
||||
public SkillDef Active;
|
||||
public int ActiveLv = 1;
|
||||
public List<SkillEffect> ActiveEffects;
|
||||
public SkillDef Passive;
|
||||
public int PassiveLv = 1;
|
||||
public float PassiveMult = 1f;
|
||||
public List<SkillEffect> PassiveEffects;
|
||||
|
||||
/// <summary>액티브를 쓸 수 있는 첫 아군 턴(전투 시작 = 1 · 사용 턴 + 쿨).</summary>
|
||||
public int ReadyTurn = 1;
|
||||
/// <summary>next_action 예약(다음 슬링샷 1회) · 행동 전 다시 탭 = 취소.</summary>
|
||||
public bool Reserved;
|
||||
/// <summary>예약한 효과가 지금 걸려 있다(그 유닛의 행동 처리·조준 미리보기 동안만).</summary>
|
||||
public bool Armed;
|
||||
|
||||
/// <summary>추가 이동: 지금 것 다음에 더 남은 횟수 · 추가 이동에서 공격 가능.</summary>
|
||||
public int BonusMovesLeft;
|
||||
public bool BonusAttack;
|
||||
/// <summary>대기(견제) 중 액티브로 얻은 반경 배율·가산·무제한(그 적 턴까지).</summary>
|
||||
public float OwMult = 1f;
|
||||
public float OwAdd;
|
||||
public bool OwUnlimited;
|
||||
/// <summary>전투 중 관통한 적 누적(장료 팔백 돌격).</summary>
|
||||
public int PierceHits;
|
||||
|
||||
public bool HasActive
|
||||
{
|
||||
get { return Active != null && ActiveEffects != null; }
|
||||
}
|
||||
|
||||
public int CooldownLeft(int turn)
|
||||
{
|
||||
return Mathf.Max(0, ReadyTurn - turn);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>스킬 효과 1건이 어느 유닛·스킬에서 왔는가(SkillMods 질의 결과).</summary>
|
||||
public struct SkillHit
|
||||
{
|
||||
public UnitSkills Owner;
|
||||
public SkillDef Def;
|
||||
public SkillEffect E;
|
||||
public bool Passive;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 전투 1판의 스킬 처리(PD #1093 B · 시스템 기획 §5): 유닛별 스킬 상태 · 즉시형 액티브의 시한 효과(이번 아군 턴 / 이번 적 턴) ·
|
||||
/// 행동 전후 훅(예약 효과 장착·쿨 소모) · 피해 적용(호위 분담) · HUD 한 줄 기록. 수치 질의는 SkillMods.
|
||||
/// </summary>
|
||||
public class SkillSystem
|
||||
{
|
||||
public static SkillSystem Current;
|
||||
|
||||
public class Timed
|
||||
{
|
||||
public UnitSkills Owner;
|
||||
public Faction Faction;
|
||||
public SkillDef Def;
|
||||
public SkillEffect E;
|
||||
/// <summary>true = 이번 적 턴에만 걸림(다음 아군 턴 시작에 사라짐) · false = 이번 아군 턴(적 턴 시작에 사라짐).</summary>
|
||||
public bool EnemyWindow;
|
||||
/// <summary>대상 고정(장판 일갈 = 밀어낸 적) · null = 키 규칙대로.</summary>
|
||||
public HashSet<Unit> Targets;
|
||||
}
|
||||
|
||||
public struct LogLine
|
||||
{
|
||||
public string Text;
|
||||
public float Time;
|
||||
}
|
||||
|
||||
readonly BattleManager battle;
|
||||
readonly Dictionary<Unit, UnitSkills> map = new Dictionary<Unit, UnitSkills>();
|
||||
readonly List<UnitSkills> entries = new List<UnitSkills>();
|
||||
public readonly List<Timed> TimedEffects = new List<Timed>();
|
||||
public readonly List<LogLine> Log = new List<LogLine>();
|
||||
readonly HashSet<string> noted = new HashSet<string>();
|
||||
readonly int[] startCount = new int[2];
|
||||
|
||||
/// <summary>장합 변진: 목표 지점 탭 대기 중인 유닛.</summary>
|
||||
public Unit PendingTeleport { get; private set; }
|
||||
|
||||
public SkillSystem(BattleManager b)
|
||||
{
|
||||
battle = b;
|
||||
}
|
||||
|
||||
public BattleManager Battle
|
||||
{
|
||||
get { return battle; }
|
||||
}
|
||||
|
||||
public IReadOnlyList<UnitSkills> Entries
|
||||
{
|
||||
get { return entries; }
|
||||
}
|
||||
|
||||
// ---------- 부착 ----------
|
||||
|
||||
public UnitSkills Of(Unit u)
|
||||
{
|
||||
if (u == null) return null;
|
||||
UnitSkills s;
|
||||
return map.TryGetValue(u, out s) ? s : null;
|
||||
}
|
||||
|
||||
UnitSkills Ensure(Unit u)
|
||||
{
|
||||
var s = Of(u);
|
||||
if (s != null) return s;
|
||||
s = new UnitSkills { Unit = u };
|
||||
map[u] = s;
|
||||
entries.Add(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
/// <summary>스킬 부착(액티브·패시브 id · 레벨 · ★5 패시브 배율 · 무장 여부). 최대 HP 패시브(hp_max_pct)는 바로 반영.</summary>
|
||||
public UnitSkills Attach(Unit u, string activeId, int activeLv, string passiveId, int passiveLv, float passiveMult, bool general)
|
||||
{
|
||||
if (u == null) return null;
|
||||
var s = Ensure(u);
|
||||
s.IsGeneral = general;
|
||||
s.Active = SkillCatalog.Get(activeId);
|
||||
s.ActiveLv = Mathf.Max(1, activeLv);
|
||||
s.ActiveEffects = s.Active != null ? s.Active.ScaledEffects(s.ActiveLv, 1f) : null;
|
||||
s.Passive = SkillCatalog.Get(passiveId);
|
||||
s.PassiveLv = Mathf.Max(1, passiveLv);
|
||||
s.PassiveMult = passiveMult > 0f ? passiveMult : 1f;
|
||||
s.PassiveEffects = s.Passive != null ? s.Passive.ScaledEffects(s.PassiveLv, s.PassiveMult) : null;
|
||||
if (s.Passive != null) s.HeroId = s.Passive.HeroId;
|
||||
else if (s.Active != null) s.HeroId = s.Active.HeroId;
|
||||
|
||||
float hp = SkillMods.Sum(u, SkillKeys.HpMaxPct);
|
||||
if (Mathf.Abs(hp) > 1e-4f)
|
||||
{
|
||||
u.MaxHp = Mathf.Max(1, Mathf.RoundToInt(u.MaxHp * (1f + hp)));
|
||||
u.Hp = u.MaxHp;
|
||||
if (u.View != null) u.View.Refresh();
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/// <summary>heroes.csv 영웅 id 로 부착(테스트·도구용).</summary>
|
||||
public UnitSkills AttachHero(Unit u, string heroId, int activeLv = 1, int passiveLv = 1, float passiveMult = 1f, bool general = true)
|
||||
{
|
||||
var h = SkillCatalog.Hero(heroId);
|
||||
if (h == null) return null;
|
||||
return Attach(u, h.skillActive, activeLv, h.skillPassive, passiveLv, passiveMult, general && !h.isSoldier);
|
||||
}
|
||||
|
||||
/// <summary>출전 칸(편성 → 스킬 id·레벨·★ 배율이 채워져 있음).</summary>
|
||||
public UnitSkills AttachLineup(Unit u, LineupEntry e)
|
||||
{
|
||||
if (e == null) return null;
|
||||
if (string.IsNullOrEmpty(e.skillActive) && string.IsNullOrEmpty(e.skillPassive))
|
||||
{
|
||||
if (e.general) Ensure(u).IsGeneral = true;
|
||||
return Of(u);
|
||||
}
|
||||
return Attach(u, e.skillActive, e.activeLv, e.skillPassive, e.passiveLv, e.passiveMult, e.general);
|
||||
}
|
||||
|
||||
/// <summary>적 무장(보스·엘리트) = heroes.csv 에 같은 아트·이름 영웅이 있으면 그 패시브만(Lv1 · 액티브 AI 사용은 2차 · PD 확인 대상).</summary>
|
||||
public UnitSkills AttachEnemy(Unit u, BalanceConfig.EnemySpawn e)
|
||||
{
|
||||
if (u == null || e == null || !(e.isBoss || e.isGeneral)) return null;
|
||||
var h = SkillCatalog.HeroForEnemy(e.art, e.name);
|
||||
if (h == null)
|
||||
{
|
||||
var s = Ensure(u);
|
||||
s.IsGeneral = true;
|
||||
return s;
|
||||
}
|
||||
return Attach(u, null, 1, h.skillPassive, 1, 1f, true);
|
||||
}
|
||||
|
||||
public void MarkGeneral(Unit u)
|
||||
{
|
||||
if (u != null) Ensure(u).IsGeneral = true;
|
||||
}
|
||||
|
||||
public bool IsGeneral(Unit u)
|
||||
{
|
||||
if (u == null) return false;
|
||||
if (u.IsBoss) return true;
|
||||
var s = Of(u);
|
||||
return s != null && s.IsGeneral;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
map.Clear();
|
||||
entries.Clear();
|
||||
TimedEffects.Clear();
|
||||
noted.Clear();
|
||||
PendingTeleport = null;
|
||||
startCount[0] = startCount[1] = 0;
|
||||
}
|
||||
|
||||
/// <summary>전투 시작(배치 뒤): 출전 인원 기록(패자 · team_atk_pct_full) · 쿨 = 바로 사용 가능 · 시한 효과 비움.</summary>
|
||||
public void BeginBattle()
|
||||
{
|
||||
Prune();
|
||||
TimedEffects.Clear();
|
||||
noted.Clear();
|
||||
PendingTeleport = null;
|
||||
startCount[0] = battle.AliveCount(Faction.Player);
|
||||
startCount[1] = battle.AliveCount(Faction.Enemy);
|
||||
foreach (var s in entries)
|
||||
{
|
||||
s.ReadyTurn = 1;
|
||||
s.Reserved = false;
|
||||
s.Armed = false;
|
||||
s.BonusMovesLeft = 0;
|
||||
s.BonusAttack = false;
|
||||
s.OwMult = 1f;
|
||||
s.OwAdd = 0f;
|
||||
s.OwUnlimited = false;
|
||||
s.PierceHits = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public int StartCount(Faction f)
|
||||
{
|
||||
return startCount[(int)f];
|
||||
}
|
||||
|
||||
void Prune()
|
||||
{
|
||||
for (int i = entries.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (entries[i].Unit != null) continue;
|
||||
entries.RemoveAt(i);
|
||||
}
|
||||
var dead = new List<Unit>();
|
||||
foreach (var kv in map)
|
||||
if (kv.Key == null) dead.Add(kv.Key);
|
||||
foreach (var k in dead) map.Remove(k);
|
||||
}
|
||||
|
||||
// ---------- 턴 ----------
|
||||
|
||||
/// <summary>적 턴 시작: 이번 아군 턴 효과(동남풍 · 고육계)와 쓰지 않은 예약·추가 이동 정리.</summary>
|
||||
public void OnEnemyTurnStart()
|
||||
{
|
||||
TimedEffects.RemoveAll(t => !t.EnemyWindow);
|
||||
PendingTeleport = null;
|
||||
foreach (var s in entries)
|
||||
{
|
||||
s.Reserved = false;
|
||||
s.Armed = false;
|
||||
s.BonusMovesLeft = 0;
|
||||
s.BonusAttack = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>아군 턴 시작: 이번 적 턴 효과(호위 · 철벽 · 간웅 · 일갈 · 안개)와 아군 견제 강화 정리.</summary>
|
||||
public void OnPlayerTurnStart()
|
||||
{
|
||||
TimedEffects.RemoveAll(t => t.EnemyWindow);
|
||||
foreach (var s in entries)
|
||||
{
|
||||
if (s.Unit == null || s.Unit.Faction != Faction.Player) continue;
|
||||
s.OwMult = 1f;
|
||||
s.OwAdd = 0f;
|
||||
s.OwUnlimited = false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool WindowActive(Timed t)
|
||||
{
|
||||
return !t.EnemyWindow || battle.Phase == BattlePhase.EnemyTurn;
|
||||
}
|
||||
|
||||
// ---------- 액티브: 상태 · 탭 ----------
|
||||
|
||||
public enum ActiveState
|
||||
{
|
||||
None,
|
||||
Ready,
|
||||
Reserved,
|
||||
Cooldown,
|
||||
Acted,
|
||||
BonusMove,
|
||||
NotTurn,
|
||||
Targeting
|
||||
}
|
||||
|
||||
public ActiveState StateOf(Unit u)
|
||||
{
|
||||
var s = Of(u);
|
||||
if (u == null || s == null || !s.HasActive || u.Faction != Faction.Player || !u.IsAlive) return ActiveState.None;
|
||||
if (PendingTeleport == u) return ActiveState.Targeting;
|
||||
if (s.Reserved) return ActiveState.Reserved;
|
||||
if (battle.Phase != BattlePhase.PlayerTurn) return ActiveState.NotTurn;
|
||||
if (s.CooldownLeft(battle.TurnNumber) > 0) return ActiveState.Cooldown;
|
||||
if (s.Active.Trigger == SkillTrigger.NextAction)
|
||||
{
|
||||
if (u.BonusMoveAvailable) return ActiveState.BonusMove;
|
||||
if (!battle.CanAct(u)) return ActiveState.Acted;
|
||||
}
|
||||
return ActiveState.Ready;
|
||||
}
|
||||
|
||||
/// <summary>HUD 스킬 버튼 탭: next_action = 예약/취소 · instant = 즉시 발동(변진 = 목표 지점 탭 대기). 반환 = 안내 문구.</summary>
|
||||
public string TapActive(Unit u)
|
||||
{
|
||||
var s = Of(u);
|
||||
var st = StateOf(u);
|
||||
switch (st)
|
||||
{
|
||||
case ActiveState.None: return null;
|
||||
case ActiveState.Targeting:
|
||||
PendingTeleport = null;
|
||||
return s.Active.NameKo + " 취소";
|
||||
case ActiveState.Reserved:
|
||||
s.Reserved = false;
|
||||
if (u.View != null) u.View.Refresh();
|
||||
return s.Active.NameKo + " 예약 취소(쿨 소모 없음)";
|
||||
case ActiveState.NotTurn: return "아군 턴에만 쓸 수 있습니다";
|
||||
case ActiveState.Cooldown: return s.Active.NameKo + " — 쿨 " + s.CooldownLeft(battle.TurnNumber) + "턴";
|
||||
case ActiveState.Acted: return s.Active.NameKo + " — 이미 행동한 유닛";
|
||||
case ActiveState.BonusMove: return s.Active.NameKo + " — 추가 이동 중에는 예약할 수 없음";
|
||||
}
|
||||
if (s.Active.Trigger == SkillTrigger.NextAction)
|
||||
{
|
||||
s.Reserved = true;
|
||||
if (u.View != null) u.View.Refresh();
|
||||
return s.Active.NameKo + " 예약 — 다음 슬링샷에 발동 · 다시 탭 = 취소";
|
||||
}
|
||||
if (s.Active.First(SkillKeys.Teleport) != null)
|
||||
{
|
||||
PendingTeleport = u;
|
||||
return s.Active.NameKo + " — 반경 " + TeleportRadius(s).ToString("0.#") + " 안 빈 곳을 탭(다른 곳 = 취소)";
|
||||
}
|
||||
battle.RunSkillRoutine(Instant(s));
|
||||
return s.Active.NameKo + " 발동!";
|
||||
}
|
||||
|
||||
// ---------- 행동 훅(ActionResolver.Resolve) ----------
|
||||
|
||||
/// <summary>행동 시작: 예약한 next_action 효과를 장착(이동만인 추가 이동은 제외) · 발동 연출.</summary>
|
||||
public void BeginAction(Unit u, bool moveOnly, ActionOutcome o)
|
||||
{
|
||||
noted.Clear();
|
||||
var s = Of(u);
|
||||
if (s == null || !s.Reserved || !s.HasActive || moveOnly) return;
|
||||
s.Armed = true;
|
||||
o.SkillUsed = s.Active;
|
||||
Announce(s, s.Active);
|
||||
}
|
||||
|
||||
/// <summary>행동 끝: 예약 소모 · 쿨(아군 턴 기준) · 견제 강화 이월.</summary>
|
||||
public void EndAction(Unit u, ActionOutcome o)
|
||||
{
|
||||
var s = Of(u);
|
||||
if (s == null || !s.Armed) return;
|
||||
if (o.Overwatch)
|
||||
{
|
||||
foreach (var e in s.ActiveEffects)
|
||||
{
|
||||
if (e.Key == SkillKeys.OverwatchRadiusMult) s.OwMult *= e.P;
|
||||
else if (e.Key == SkillKeys.OverwatchRadiusAdd) s.OwAdd += e.P;
|
||||
else if (e.Key == SkillKeys.OverwatchUnlimited) s.OwUnlimited = true;
|
||||
}
|
||||
}
|
||||
s.Armed = false;
|
||||
s.Reserved = false;
|
||||
s.ReadyTurn = battle.TurnNumber + s.Active.Cooldown;
|
||||
if (u.View != null) u.View.Refresh();
|
||||
}
|
||||
|
||||
/// <summary>조준 미리보기 동안만 예약 효과를 장착(사거리 · 사선 · 광역 표시). 반환 = 복원값.</summary>
|
||||
public bool PreviewBegin(Unit u)
|
||||
{
|
||||
var s = Of(u);
|
||||
if (s == null) return false;
|
||||
bool prev = s.Armed;
|
||||
if (s.Reserved && !u.BonusMoveAvailable) s.Armed = true;
|
||||
return prev;
|
||||
}
|
||||
|
||||
public void PreviewEnd(Unit u, bool prev)
|
||||
{
|
||||
var s = Of(u);
|
||||
if (s != null) s.Armed = prev;
|
||||
}
|
||||
|
||||
// ---------- 추가 이동 ----------
|
||||
|
||||
public bool BonusCanAttack(Unit u)
|
||||
{
|
||||
var s = Of(u);
|
||||
return s != null && s.BonusAttack;
|
||||
}
|
||||
|
||||
/// <summary>행동 뒤 추가 이동을 (또) 주는가 — 처음 명중 = 추가 이동 n 회(인덕) · 추가 이동 뒤 = 남은 횟수.</summary>
|
||||
public bool NextBonus(Unit u, bool wasBonus, ActionOutcome o)
|
||||
{
|
||||
var s = Of(u);
|
||||
if (!wasBonus)
|
||||
{
|
||||
if (!o.BonusMove) return false;
|
||||
if (s != null)
|
||||
{
|
||||
s.BonusMovesLeft = Mathf.Max(0, o.BonusMoves - 1);
|
||||
s.BonusAttack = o.BonusAttack;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (s != null && s.BonusMovesLeft > 0)
|
||||
{
|
||||
s.BonusMovesLeft--;
|
||||
return true;
|
||||
}
|
||||
ClearBonus(u);
|
||||
return false;
|
||||
}
|
||||
|
||||
public void ClearBonus(Unit u)
|
||||
{
|
||||
var s = Of(u);
|
||||
if (s == null) return;
|
||||
s.BonusMovesLeft = 0;
|
||||
s.BonusAttack = false;
|
||||
}
|
||||
|
||||
// ---------- 적 턴: 교전 반응 · 교전 범위 ----------
|
||||
|
||||
/// <summary>이번 적 턴에 교전 반응이 없는가(장판 일갈 공포 · 태평요술 안개).</summary>
|
||||
public bool Suppressed(Unit e)
|
||||
{
|
||||
if (e == null) return false;
|
||||
foreach (var t in TimedEffects)
|
||||
{
|
||||
if (!WindowActive(t)) continue;
|
||||
if (t.E.Key == SkillKeys.AoePushFear && t.Targets != null && t.Targets.Contains(e)) return true;
|
||||
if (t.E.Key == SkillKeys.EnemyAggroOffTurn && t.Faction != e.Faction) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>상대 진영 패시브(신산)가 actingFaction 의 교전 범위에 더하는 값.</summary>
|
||||
public float AggroAdd(Faction actingFaction)
|
||||
{
|
||||
float add = 0f;
|
||||
foreach (var s in entries)
|
||||
{
|
||||
if (s.Unit == null || !s.Unit.IsAlive || s.Unit.Faction == actingFaction || s.PassiveEffects == null) continue;
|
||||
foreach (var e in s.PassiveEffects)
|
||||
{
|
||||
if (e.Key != SkillKeys.EnemyAggroAdd) continue;
|
||||
add += e.P;
|
||||
NotePassive(s, s.Passive);
|
||||
}
|
||||
}
|
||||
return add;
|
||||
}
|
||||
|
||||
// ---------- 피해 적용(호위) ----------
|
||||
|
||||
/// <summary>피해 적용 — 관평 호위(이번 적 턴 반경 안 같은 편 피해 일부를 관평이 대신) 반영. 반환 = d 가 실제 받은 피해.</summary>
|
||||
public int Hit(Unit d, int dmg)
|
||||
{
|
||||
if (d == null || !d.IsAlive || dmg <= 0) return 0;
|
||||
foreach (var t in TimedEffects)
|
||||
{
|
||||
if (t.E.Key != SkillKeys.GuardShare || !WindowActive(t)) continue;
|
||||
var g = t.Owner.Unit;
|
||||
if (g == null || !g.IsAlive || g == d || g.Faction != d.Faction) continue;
|
||||
float r = t.E.HasA2 ? t.E.A2 : 0f;
|
||||
if (Vector2.Distance(g.Position, d.Position) > r + d.Radius) continue;
|
||||
int share = Mathf.Clamp(Mathf.RoundToInt(dmg * Mathf.Clamp01(t.E.P)), 0, dmg);
|
||||
if (share <= 0) continue;
|
||||
g.TakeDamage(share);
|
||||
Fx.Popup(g.Position, share + " 호위", SkillFx.GuardColor);
|
||||
dmg -= share;
|
||||
break;
|
||||
}
|
||||
d.TakeDamage(dmg);
|
||||
return dmg;
|
||||
}
|
||||
|
||||
/// <summary>스킬 시스템이 없어도 쓰는 피해 적용.</summary>
|
||||
public static int Deal(Unit d, int dmg)
|
||||
{
|
||||
if (Current != null) return Current.Hit(d, dmg);
|
||||
if (d == null) return 0;
|
||||
d.TakeDamage(dmg);
|
||||
return dmg;
|
||||
}
|
||||
|
||||
// ---------- 즉시형 액티브 ----------
|
||||
|
||||
/// <summary>즉시 발동(행동 소모 없음): 효과마다 처리 → 쿨.</summary>
|
||||
public IEnumerator Instant(UnitSkills s)
|
||||
{
|
||||
var u = s.Unit;
|
||||
Announce(s, s.Active);
|
||||
s.ReadyTurn = battle.TurnNumber + s.Active.Cooldown;
|
||||
foreach (var e in s.ActiveEffects)
|
||||
{
|
||||
switch (e.Key)
|
||||
{
|
||||
case SkillKeys.AoePushFear:
|
||||
yield return PushFear(s, e);
|
||||
break;
|
||||
case SkillKeys.HpCostAtk:
|
||||
PayHp(u, e.HasA2 ? e.A2 : 0f);
|
||||
AddTimed(s, e, false, null);
|
||||
break;
|
||||
case SkillKeys.GuardShare:
|
||||
case SkillKeys.TeamDmgTakenPctTurn:
|
||||
case SkillKeys.DmgTakenAura:
|
||||
case SkillKeys.EnemyAggroOffTurn:
|
||||
AddTimed(s, e, true, null);
|
||||
break;
|
||||
case SkillKeys.Teleport:
|
||||
break; // 목표 지점 탭(TryTeleport)
|
||||
default:
|
||||
AddTimed(s, e, false, null); // 동남풍(sling_dist_pct) 등 = 이번 아군 턴
|
||||
break;
|
||||
}
|
||||
}
|
||||
yield return Fx.Wait(0.25f);
|
||||
battle.NotifyChanged();
|
||||
}
|
||||
|
||||
void AddTimed(UnitSkills s, SkillEffect e, bool enemyWindow, HashSet<Unit> targets)
|
||||
{
|
||||
TimedEffects.Add(new Timed { Owner = s, Faction = s.Unit.Faction, Def = s.Active, E = e, EnemyWindow = enemyWindow, Targets = targets });
|
||||
}
|
||||
|
||||
static void PayHp(Unit u, float ratio)
|
||||
{
|
||||
int cost = Mathf.RoundToInt(u.MaxHp * Mathf.Max(0f, ratio));
|
||||
if (cost <= 0) return;
|
||||
u.Hp = Mathf.Max(1, u.Hp - cost); // 스스로 쓰러지지는 않는다
|
||||
if (u.View != null)
|
||||
{
|
||||
u.View.OnHit();
|
||||
u.View.Refresh();
|
||||
}
|
||||
Fx.Popup(u.Position, "HP −" + cost, SkillFx.CostColor);
|
||||
}
|
||||
|
||||
/// <summary>장판 일갈: 반경(p2) 안 상대 전원을 바깥으로 p1 밀고 이번 적 턴 교전 반응 없음.</summary>
|
||||
IEnumerator PushFear(UnitSkills s, SkillEffect e)
|
||||
{
|
||||
var u = s.Unit;
|
||||
float radius = e.HasA2 ? e.A2 : 0f;
|
||||
var targets = new HashSet<Unit>();
|
||||
int running = 0;
|
||||
Faction other = u.Faction == Faction.Player ? Faction.Enemy : Faction.Player;
|
||||
foreach (var t in battle.AliveUnits(other))
|
||||
{
|
||||
if (Vector2.Distance(u.Position, t.Position) > radius + t.Radius) continue;
|
||||
targets.Add(t);
|
||||
Vector2 dir = t.Position - u.Position;
|
||||
dir = dir.sqrMagnitude > 1e-6f ? dir.normalized : Vector2.up;
|
||||
float force = e.P * SkillMods.KnockbackTakenMult(t, true);
|
||||
float slide = Combat.FreeTravel(t, t.Position, dir, force, true, true, u);
|
||||
if (slide <= 0.01f) continue;
|
||||
Vector2 end = battle.Board.ClampInside(t.Position + dir * slide, t.Radius);
|
||||
running++;
|
||||
var unit = t;
|
||||
if (unit.View != null) unit.View.SetFacing(dir);
|
||||
Fx.Run(Track(Fx.MoveAlong(unit.transform, unit.Position, end, battle.Config.moveSpeed, null), () =>
|
||||
{
|
||||
unit.Position = end;
|
||||
running--;
|
||||
}));
|
||||
}
|
||||
Fx.Burst(u.Position, radius, SkillFx.FearColor);
|
||||
while (running > 0) yield return null;
|
||||
var t2 = new Timed { Owner = s, Faction = u.Faction, Def = s.Active, E = e, EnemyWindow = true, Targets = targets };
|
||||
TimedEffects.Add(t2);
|
||||
battle.NotifyChanged();
|
||||
}
|
||||
|
||||
static IEnumerator Track(IEnumerator inner, System.Action onDone)
|
||||
{
|
||||
yield return inner;
|
||||
onDone();
|
||||
}
|
||||
|
||||
// ---------- 변진(장합): 목표 지점 탭 ----------
|
||||
|
||||
public static float TeleportRadius(UnitSkills s)
|
||||
{
|
||||
if (s == null || s.ActiveEffects == null) return 0f;
|
||||
foreach (var e in s.ActiveEffects)
|
||||
if (e.Key == SkillKeys.Teleport) return e.P;
|
||||
return 0f;
|
||||
}
|
||||
|
||||
/// <summary>목표 지점이 반경 안 빈 곳인가(벽·유닛과 겹치지 않음 · 전장 안).</summary>
|
||||
public bool TeleportValid(Unit u, Vector2 w)
|
||||
{
|
||||
var s = Of(u);
|
||||
if (s == null) return false;
|
||||
if (Vector2.Distance(u.Position, w) > TeleportRadius(s) + 1e-3f) return false;
|
||||
if ((battle.Board.ClampInside(w, u.Radius) - w).sqrMagnitude > 1e-4f) return false;
|
||||
Physics2D.SyncTransforms();
|
||||
foreach (var c in Physics2D.OverlapCircleAll(w, u.Radius))
|
||||
{
|
||||
if (c.GetComponent<BoardWall>() != null) return false;
|
||||
var o = c.GetComponent<Unit>();
|
||||
if (o != null && o != u && o.IsAlive) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>변진 대기 중 화면 탭 → 처리했으면 true(유효 = 이동 · 아니면 취소).</summary>
|
||||
public bool TryTeleportTap(Vector2 w, out string message)
|
||||
{
|
||||
message = null;
|
||||
var u = PendingTeleport;
|
||||
if (u == null) return false;
|
||||
PendingTeleport = null;
|
||||
var s = Of(u);
|
||||
if (s == null || !u.IsAlive || battle.Phase != BattlePhase.PlayerTurn) return true;
|
||||
if (!TeleportValid(u, w))
|
||||
{
|
||||
message = s.Active.NameKo + " 취소 — 반경 안 빈 곳이 아님";
|
||||
return true;
|
||||
}
|
||||
battle.RunSkillRoutine(TeleportRoutine(s, w));
|
||||
message = s.Active.NameKo + " 발동!";
|
||||
return true;
|
||||
}
|
||||
|
||||
public IEnumerator TeleportRoutine(UnitSkills s, Vector2 w)
|
||||
{
|
||||
var u = s.Unit;
|
||||
Announce(s, s.Active);
|
||||
s.ReadyTurn = battle.TurnNumber + s.Active.Cooldown;
|
||||
if (u.View != null) u.View.SetFacing(w - u.Position);
|
||||
yield return Fx.MoveAlong(u.transform, u.Position, w, battle.Config.moveSpeed * 2f, null);
|
||||
u.Position = w;
|
||||
battle.NotifyChanged();
|
||||
}
|
||||
|
||||
// ---------- 연출 · HUD 기록 ----------
|
||||
|
||||
/// <summary>액티브 발동 = 유닛 위 스킬 이름 + 플래시 + HUD 한 줄.</summary>
|
||||
public void Announce(UnitSkills s, SkillDef def)
|
||||
{
|
||||
if (s == null || def == null) return;
|
||||
if (s.Unit != null) SkillFx.Announce(s.Unit, def.NameKo);
|
||||
AddLog("액티브 · " + (s.Unit != null ? s.Unit.DisplayName : "") + " 「" + def.NameKo + "」");
|
||||
}
|
||||
|
||||
/// <summary>패시브 발동 HUD 한 줄(같은 행동 안에서는 스킬마다 1번).</summary>
|
||||
public void NotePassive(UnitSkills s, SkillDef def)
|
||||
{
|
||||
if (s == null || def == null || s.Unit == null) return;
|
||||
string key = s.Unit.GetInstanceID() + "/" + def.Id;
|
||||
if (!noted.Add(key)) return;
|
||||
AddLog("패시브 · " + s.Unit.DisplayName + " 「" + def.NameKo + "」");
|
||||
}
|
||||
|
||||
public void AddLog(string text)
|
||||
{
|
||||
Log.Add(new LogLine { Text = text, Time = Time.unscaledTime });
|
||||
if (Log.Count > 8) Log.RemoveAt(0);
|
||||
}
|
||||
|
||||
/// <summary>최근 seconds 초 안의 기록(최신이 마지막).</summary>
|
||||
public List<string> RecentLog(float seconds, int max)
|
||||
{
|
||||
var res = new List<string>();
|
||||
float now = Time.unscaledTime;
|
||||
for (int i = Log.Count - 1; i >= 0 && res.Count < max; i--)
|
||||
if (now - Log[i].Time <= seconds) res.Insert(0, Log[i].Text);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>스킬 연출(이름 글자 · 플래시).</summary>
|
||||
public static class SkillFx
|
||||
{
|
||||
public static readonly Color NameColor = new Color(1f, 0.85f, 0.3f);
|
||||
public static readonly Color FlashColor = new Color(1f, 0.9f, 0.45f, 0.55f);
|
||||
public static readonly Color GuardColor = new Color(0.55f, 0.85f, 1f);
|
||||
public static readonly Color FearColor = new Color(1f, 0.4f, 0.25f, 0.4f);
|
||||
public static readonly Color CostColor = new Color(1f, 0.55f, 0.35f);
|
||||
public static readonly Color HealColor = new Color(0.45f, 1f, 0.5f);
|
||||
|
||||
public static void Announce(Unit u, string name)
|
||||
{
|
||||
Fx.Burst(u.Position, u.Radius * 2.2f, FlashColor);
|
||||
Fx.Run(NameRoutine(u.Position + new Vector2(0f, u.ArtHeight > 0f ? u.ArtHeight * 0.6f : u.Radius + 0.6f), name));
|
||||
}
|
||||
|
||||
static IEnumerator NameRoutine(Vector2 pos, string text)
|
||||
{
|
||||
var tm = TextUtil.CreateWorldText(null, text, 0.55f, NameColor, 21);
|
||||
tm.gameObject.name = "SkillName";
|
||||
var tr = tm.transform;
|
||||
float t = 0f;
|
||||
const float dur = 1.1f;
|
||||
while (t < dur)
|
||||
{
|
||||
t += Time.deltaTime;
|
||||
float k = Mathf.Clamp01(t / dur);
|
||||
tr.position = new Vector3(pos.x, pos.y + 0.4f * k, 0f);
|
||||
var c = NameColor;
|
||||
c.a = k < 0.7f ? 1f : 1f - (k - 0.7f) / 0.3f;
|
||||
tm.color = c;
|
||||
yield return null;
|
||||
}
|
||||
Object.Destroy(tm.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2b67581e954b6cc4ca0ea5d96b1690a4
|
||||
|
|
@ -38,6 +38,7 @@ namespace TKS
|
|||
LineRenderer shotLine; // 궁병·책사 투사체 경로(쏘는 위치 → 첫 피격 지점 · PD 2026-11-05 #1087 ⑥)
|
||||
SpriteRenderer targetRing;
|
||||
SpriteRenderer splashArea; // 책사 광역 원(중심 = 첫 피격 유닛 · #1087 ⑤)
|
||||
SpriteRenderer teleportArea; // 변진(장합) 목표 지점 반경(PD #1093 B)
|
||||
readonly List<Unit> highlighted = new List<Unit>();
|
||||
|
||||
public Unit Selected
|
||||
|
|
@ -89,6 +90,8 @@ namespace TKS
|
|||
targetRing.enabled = false;
|
||||
splashArea = ProceduralSprites.NewSprite(transform, "SplashArea", ProceduralSprites.Circle, SplashAreaColor, 1f, -8);
|
||||
splashArea.gameObject.SetActive(false);
|
||||
teleportArea = ProceduralSprites.NewSprite(transform, "TeleportArea", ProceduralSprites.Circle, new Color(1f, 0.85f, 0.3f, 0.18f), 1f, -8);
|
||||
teleportArea.gameObject.SetActive(false);
|
||||
|
||||
var rgo = new GameObject("RangeIndicator");
|
||||
rgo.transform.SetParent(transform, false);
|
||||
|
|
@ -140,10 +143,41 @@ namespace TKS
|
|||
}
|
||||
if (Input.GetMouseButtonUp(0)) OnRelease(screen);
|
||||
|
||||
// 조준 중(첫 손가락으로 당기는 중) 다른 손가락으로 스킬 버튼 탭 = 예약/취소(PD #1093 B)
|
||||
if (dragging && hud != null && Input.touchCount > 1)
|
||||
{
|
||||
for (int i = 1; i < Input.touchCount; i++)
|
||||
{
|
||||
var t = Input.GetTouch(i);
|
||||
if (t.phase == TouchPhase.Began && hud.TrySkillTap(t.position) && selected != null && previewValid) ShowPreview(selected, currentTarget, null);
|
||||
}
|
||||
}
|
||||
RefreshTeleportArea();
|
||||
|
||||
float wheel = Input.mouseScrollDelta.y;
|
||||
if (Mathf.Abs(wheel) > 0.01f && camCtrl != null) camCtrl.Zoom(1f - wheel * 0.1f);
|
||||
}
|
||||
|
||||
/// <summary>변진 목표 지점 대기 중이면 반경 원 표시.</summary>
|
||||
void RefreshTeleportArea()
|
||||
{
|
||||
var sys = battle.Skills;
|
||||
var u = sys != null ? sys.PendingTeleport : null;
|
||||
bool on = u != null && u.IsAlive;
|
||||
if (on)
|
||||
{
|
||||
float d = SkillSystem.TeleportRadius(sys.Of(u)) * 2f;
|
||||
teleportArea.transform.position = new Vector3(u.Position.x, u.Position.y, 0f);
|
||||
teleportArea.transform.localScale = new Vector3(d, d, 1f);
|
||||
}
|
||||
if (teleportArea.gameObject.activeSelf != on) teleportArea.gameObject.SetActive(on);
|
||||
}
|
||||
|
||||
public bool TeleportAreaVisible
|
||||
{
|
||||
get { return teleportArea != null && teleportArea.gameObject.activeSelf; }
|
||||
}
|
||||
|
||||
void OnPress(Vector2 screen)
|
||||
{
|
||||
lastScreen = screen;
|
||||
|
|
@ -153,6 +187,15 @@ namespace TKS
|
|||
emptyPress = false;
|
||||
previewValid = false;
|
||||
|
||||
// 스킬 버튼(HUD) 탭 = 예약/취소/즉시 발동 · 변진 대기 중 = 목표 지점 탭(PD #1093 B)
|
||||
if (hud != null && hud.TrySkillTap(screen)) return;
|
||||
string tpMsg;
|
||||
if (battle.Skills != null && battle.Skills.TryTeleportTap(ScreenToWorld(screen), out tpMsg))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(tpMsg)) battle.LastMessage = tpMsg;
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 mapWorld;
|
||||
if (hud != null && camCtrl != null && hud.TryMinimapTap(screen, out mapWorld))
|
||||
{
|
||||
|
|
@ -199,7 +242,8 @@ namespace TKS
|
|||
previewValid = false;
|
||||
return;
|
||||
}
|
||||
if (mag > selected.SlingMaxDistance) launch = launch / mag * selected.SlingMaxDistance;
|
||||
float sling = SkillMods.SlingMax(selected); // 동남풍·적토마(PD #1093 B)
|
||||
if (mag > sling) launch = launch / mag * sling;
|
||||
currentTarget = battle.Board.ClampInside(selected.Position + launch, selected.Radius);
|
||||
previewValid = true;
|
||||
ShowPreview(selected, currentTarget, w);
|
||||
|
|
@ -318,7 +362,7 @@ namespace TKS
|
|||
if (friendly) anyFriendly = true;
|
||||
else foes++;
|
||||
}
|
||||
float d = battle.Config.strategistSplashRadius * 2f;
|
||||
float d = shot.SplashRadius * 2f; // 화공 = 반경 ×(PD #1093 B)
|
||||
splashArea.transform.position = new Vector3(shot.Hit.Position.x, shot.Hit.Position.y, 0f);
|
||||
splashArea.transform.localScale = new Vector3(d, d, 1f);
|
||||
splashArea.color = anyFriendly ? SplashAreaFriendlyColor : SplashAreaColor;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ namespace TKS
|
|||
TextMesh mark;
|
||||
TextMesh nameLabel;
|
||||
TextMesh badge;
|
||||
TextMesh skillBadge; // 예약한 액티브(next_action) 이름(PD #1093 B)
|
||||
Color baseColor;
|
||||
Coroutine flash;
|
||||
bool aiming;
|
||||
|
|
@ -160,10 +161,19 @@ namespace TKS
|
|||
badge = TextUtil.CreateWorldText(transform, "추가 이동", 0.22f, new Color(1f, 0.95f, 0.4f), 5);
|
||||
badge.transform.localPosition = new Vector3(0f, topY + 0.24f, 0f);
|
||||
badge.gameObject.SetActive(false);
|
||||
skillBadge = TextUtil.CreateWorldText(transform, "", 0.24f, SkillFx.NameColor, 5);
|
||||
skillBadge.transform.localPosition = new Vector3(0f, topY + 0.5f, 0f);
|
||||
skillBadge.gameObject.SetActive(false);
|
||||
|
||||
Refresh();
|
||||
}
|
||||
|
||||
/// <summary>예약 표시 중인가(테스트·캡처 확인용).</summary>
|
||||
public bool SkillBadgeVisible
|
||||
{
|
||||
get { return skillBadge != null && skillBadge.gameObject.activeSelf; }
|
||||
}
|
||||
|
||||
/// <summary>보는 방향(위 = back · 아래 = front)에 맞는 클립 이름. 그 보기에 없으면 다른 보기 → 대기.</summary>
|
||||
string ClipName(string suffix)
|
||||
{
|
||||
|
|
@ -191,8 +201,18 @@ namespace TKS
|
|||
mark.color = dim ? new Color(1f, 1f, 1f, 0.6f) : Color.white;
|
||||
|
||||
bool ow = unit.Overwatch && unit.Type == UnitType.Spear && unit.IsAlive;
|
||||
if (ow)
|
||||
{
|
||||
float d = SkillMods.OverwatchRadius(unit) * 2f; // 후계·백마의 창·청룡언월 반영(PD #1093 B)
|
||||
overwatchArea.transform.localScale = new Vector3(d, d, 1f);
|
||||
}
|
||||
if (overwatchArea.gameObject.activeSelf != ow) overwatchArea.gameObject.SetActive(ow);
|
||||
badge.gameObject.SetActive(unit.BonusMoveAvailable && unit.IsAlive);
|
||||
|
||||
var sk = SkillSystem.Current != null ? SkillSystem.Current.Of(unit) : null;
|
||||
bool reserved = sk != null && sk.Reserved && sk.Active != null && unit.IsAlive;
|
||||
if (reserved) skillBadge.text = "★" + sk.Active.NameKo;
|
||||
if (skillBadge.gameObject.activeSelf != reserved) skillBadge.gameObject.SetActive(reserved);
|
||||
}
|
||||
|
||||
public void SetSelected(bool on)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: fddad1befff291c4b94bad9d444f79da
|
||||
|
|
@ -45,6 +45,7 @@ Unity.exe -projectPath <proj> -executeMethod TKS.EditorTools.ProjectSetup.AutoPl
|
|||
|
||||
- 전투 수치 SOT = `Assets/Resources/BalanceConfig.asset`(Inspector 조정 · 최초 시드만 `Assets/Scripts/Editor/BalanceDefaults.cs`). 코드·테스트에 수치를 직접 적지 않는다(C45).
|
||||
- **아웃게임 SOT = `Assets/Data/OutGame/*.csv`(12장 · 기획 `공유/기획팀_산출물/삼국지슬링샷/tables/` 사본 · utf-8-sig · 2026-11-05 #1093 A)** — 영웅·병사(`heroes.csv` · 구 `BalanceConfig.heroes[]` 대체) · 레벨·승급·진화·장비·뽑기·보상·재화·신규 유저 초기값(`economy.csv` start_*). 고친 뒤 메뉴 `TKS/Import OutGame Data (아웃게임 데이터 임포트)`(`Editor/DataImporter` · 오류 시 자산 불변) → `Resources/OutGame/` 9 자산. 표를 바꾸면 기획 `tables/` 도 같이.
|
||||
- **스킬(2026-11-05 #1093 B)** — SOT = `Assets/Data/OutGame/skills.csv`(effect_key `;` 복수 · p1~p3 · lv_scale · needs_impl) + `skill_levels.csv` → 임포트 자산 `SkillInfoDb`(effect 열 포함 · 표시 + 전투 공용). 처리기 = `Runtime/Skills/`: `SkillKeys`(51종 · 레벨 계수 방식 · 팀 키 · 표에 없는 키 = 임포트 오류) · `SkillSystem`(유닛별 쿨·예약 · 즉시형 시한 효과 · 행동 훅 · 호위 피해 분담 · HUD 한 줄) · `SkillMods`(전투 코드가 수치 질의) · `SkillRules`(상한 = 등급 + ★3 · ★5 패시브 배율 = evolve.csv skill_unlock · 레벨업 · 스킬서 판매). 전투 코드에 스킬 수치를 직접 쓰지 않고 `SkillMods` 로 읽는다. 테스트 = `Tests/PlayMode/SkillTests`. 캡처 = `-executeMethod TKS.EditorTools.DataImporter.CaptureSkills -skillDir <dir>`.
|
||||
- 아웃게임 코드 = `Assets/Scripts/OutGame/**`(asmref → TKS.Runtime) — `OutGameSession`(표 + 세이브 · 바꾸는 동작은 즉시 저장) · 규칙 `HeroRules`·`EconomyRules`·`GachaRules` · 화면 `UI/`(로비·영웅·편성·거래·결과 패널 · `OutGameRoot` 가 전환). 세이브 = `persistentDataPath/save.json`(테스트·검증 캡처는 `SaveStore.UseMemory` / SessionState `TKS_SAVE_MEMORY` 로 메모리 전용). 출전 = `BattleManager.LineupProvider`(편성 프리셋 → `playerSpawnPoints` 자리 0~2 전열 · 3~4 후열). 화면 캡처 = `Unity.exe -projectPath <사본> -executeMethod TKS.EditorTools.DataImporter.CaptureOutGame -outgameDir <dir>`.
|
||||
- **챕터·스테이지 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종 이상 전부 통과 확인.
|
||||
|
|
@ -70,5 +71,6 @@ Unity.exe -projectPath <proj> -executeMethod TKS.EditorTools.ProjectSetup.AutoPl
|
|||
- `Assets/Scripts/OutGame/` — 아웃게임(세이브·로비·영웅·편성·거래·보상 · 위 「코드 규칙」)
|
||||
- `Assets/Data/Stages/` — 챕터·스테이지 표 CSV 4장(위 「코드 규칙」) · 해석·배치 = `Runtime/StageTable`
|
||||
- `Assets/Resources/Units/<art>/` — 유닛 아트 프레임 + meta.json(위 절)
|
||||
- `Assets/Tests/PlayMode/` — `BattleFlowTests` · `OutGameTests`
|
||||
- `Assets/Scripts/Runtime/Skills/` — 스킬 처리기(위 「코드 규칙」 스킬)
|
||||
- `Assets/Tests/PlayMode/` — `BattleFlowTests` · `OutGameTests` · `SkillTests`
|
||||
- `.claude/skills/` — Unity CLI 에이전트 스킬(`unity skill install claude-code --local` 산출물 · 재설치는 `unity skill refresh`)
|
||||
|
|
|
|||
Loading…
Reference in New Issue