From 0b2f56f271c63d9fcde1b3b7bc91caea27e16924 Mon Sep 17 00:00:00 2001 From: swrring Date: Tue, 10 Nov 2026 19:00:18 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9E=A5=EC=88=98=20=EC=8A=A4=ED=82=AC=20144?= =?UTF-8?q?=EA=B0=9C=20=EC=83=88=20=EA=B8=B0=ED=9A=8D=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?(#1112=20=C2=B7=20PD=202026-11-10=20=EC=97=91=EC=85=80=20?= =?UTF-8?q?=E3=80=8C=EC=9E=A5=EC=88=98=20=EB=AA=A9=EB=A1=9D=E3=80=8D)=20?= =?UTF-8?q?=E2=80=94=20=ED=9A=A8=EA=B3=BC=20=ED=91=9C=20skill=5Feffects.cs?= =?UTF-8?q?v(=ED=9A=A8=EA=B3=BC=201=EC=A4=84=20=C2=B7=20=ED=95=9C=EA=B8=80?= =?UTF-8?q?=20=EC=A3=BC=EC=84=9D=20=EC=97=B4)=20=C2=B7=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=2056=EA=B0=9C=20=EC=83=88=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 수치 SOT = Assets/Data/OutGame/skill_effects.csv(skill_id·key·target·cond·dur·p1~p3 + effect_ko·p1_ko~p3_ko 주석) · skills.csv effect_key = 키 목록 · heroes.csv gender(m|f) - 처리기 121종(SkillKeys) · 대상 칸(자신·같은 편·진영·반경·행동 반경) · 조건 칸(치명타·상대 상태 이상·병종·성별·턴·장착 중) · 지속 라운드 - 상태 이상 = 혼란·출혈(이동 거리 비례)·중독(중첩)·화상 + 기절(기믹 행동 불가 재사용) · 정화 · 회복·흡혈·부활·무적·무효화·호위(반격) - 재행동(유비·위연·여몽·손견·곽가·우금·맹획·감녕·고순·장합) · 치명타 확률·피해·무조건 - 이동 변형: 관통(손견·안량·손책 아군 통과) · 핀볼 튕김(손책·동탁) · 복귀(조운·손책·여포·문추·공손찬·순우경 · 하후돈 탭 지점·쓸어 감) · 위치 교환(위연) · 끌어오기(장료) · 아군 통과+탭 방향 밀치기(조조) · 반복(정보) · 난동(동탁) - 특수 사격: 거대 화살 · 직선 관통 · 폭발(화염구·독구름·운석·화살비) · 부채꼴 · 멀티샷 · 도탄 · 회오리 · 목책 · 불 장벽·불꽃·독 지대 · 궁병식 비도 · 광역 회복 - 즉시형: 광역 타격·밀기 · 흡수 · 일제 사격 · 미인계 끌어오기 · 범위 회복·정화 · 재행동 부여 · 변진 - 가상 실행(적 AI) 스킬 상태 사본(TakeState/RestoreState) · 옛 견제 스킬 확장 제거 - 테스트(고친 부분 클래스만): SkillTests 19(새로) · BattleFlowTests 42 · TacticalAITests 7 · GimmickTests 19 · OutGameTests 15 · OutGameCTests 12 전부 통과 · console error 0 Co-Authored-By: Claude Opus 5.5 --- Assets/Data/OutGame/heroes.csv | 176 +- Assets/Data/OutGame/skill_effects.csv | 289 + Assets/Data/OutGame/skill_effects.csv.meta | 7 + Assets/Data/OutGame/skills.csv | 288 +- Assets/Resources/OutGame/HeroDb.asset | 87 + Assets/Resources/OutGame/SkillInfoDb.asset | 5017 ++++++++++++++--- Assets/Scripts/OutGame/Data/HeroDb.cs | 7 + Assets/Scripts/OutGame/Data/SkillInfoDb.cs | 25 + Assets/Scripts/OutGame/OutGameTables.cs | 61 +- Assets/Scripts/Runtime/AI/Sim.cs | 50 +- .../Scripts/Runtime/ActionResolver.Skills.cs | 369 ++ .../Runtime/ActionResolver.Skills.cs.meta | 2 + Assets/Scripts/Runtime/ActionResolver.cs | 705 ++- Assets/Scripts/Runtime/BattleManager.cs | 59 +- Assets/Scripts/Runtime/Combat.cs | 75 +- .../Gimmicks/ActionResolver.Gimmicks.cs | 51 +- .../Runtime/Gimmicks/Combat.Gimmicks.cs | 5 +- .../Scripts/Runtime/Gimmicks/GimmickData.cs | 2 + .../Scripts/Runtime/Gimmicks/GimmickLayer.cs | 13 + Assets/Scripts/Runtime/Skills/SkillDef.cs | 165 +- Assets/Scripts/Runtime/Skills/SkillKeys.cs | 293 +- Assets/Scripts/Runtime/Skills/SkillMods.cs | 412 +- Assets/Scripts/Runtime/Skills/SkillStatus.cs | 103 + .../Runtime/Skills/SkillStatus.cs.meta | 2 + Assets/Scripts/Runtime/Skills/SkillSystem.cs | 1807 +++++- Assets/Scripts/Runtime/SlingshotInput.cs | 9 +- Assets/Scripts/Runtime/Unit.cs | 12 + Assets/Scripts/Runtime/UnitView.cs | 2 +- Assets/Tests/PlayMode/SkillTests.cs | 1429 ++--- CLAUDE.md | 6 +- 30 files changed, 8360 insertions(+), 3168 deletions(-) create mode 100644 Assets/Data/OutGame/skill_effects.csv create mode 100644 Assets/Data/OutGame/skill_effects.csv.meta create mode 100644 Assets/Scripts/Runtime/ActionResolver.Skills.cs create mode 100644 Assets/Scripts/Runtime/ActionResolver.Skills.cs.meta create mode 100644 Assets/Scripts/Runtime/Skills/SkillStatus.cs create mode 100644 Assets/Scripts/Runtime/Skills/SkillStatus.cs.meta diff --git a/Assets/Data/OutGame/heroes.csv b/Assets/Data/OutGame/heroes.csv index f7630039..22cf7c5d 100644 --- a/Assets/Data/OutGame/heroes.csv +++ b/Assets/Data/OutGame/heroes.csv @@ -1,88 +1,88 @@ -id,name_ko,hanja,faction,unit_type,rarity,hp_mult,atk_mult,def_mult,art,skill_active,skill_passive,is_soldier,first_stage -liubei,유비,劉備,shu,infantry,3,1.1,1.0,1.0,liubei,liubei_a,liubei_p,0,1-1 -guanyu,관우,關羽,shu,spear,3,1.1,1.25,1.0,guanyu,guanyu_a,guanyu_p,0,1-1 -zhangfei,장비,張飛,shu,cavalry,3,1.0,1.2,1.0,zhangfei,zhangfei_a,zhangfei_p,0,1-1 -zhaoyun,조운,趙雲,shu,cavalry,3,1.1,1.1,1.0,zhaoyun,zhaoyun_a,zhaoyun_p,0,3-3 -huangzhong,황충,黃忠,shu,archer,3,1.0,1.25,1.0,huangzhong,huangzhong_a,huangzhong_p,0, -zhugeliang,제갈량,諸葛亮,shu,strategist,3,1.15,1.15,1.1,zhugeliang,zhugeliang_a,zhugeliang_p,0,8-13 -zhoucang,주창,周倉,shu,infantry,2,1.05,1.05,1.05,zhoucang,zhoucang_a,zhoucang_p,0,7-2 -guanping,관평,關平,shu,spear,2,1.05,1.05,1.05,guanping,guanping_a,guanping_p,0,7-7 -xushu,서서,徐庶,shu,strategist,3,1.15,1.15,1.1,xushu,xushu_a,xushu_p,0,8-6 -taishici,태사자,太史慈,shu,archer,3,1.15,1.15,1.1,taishici,taishici_a,taishici_p,0,3-8 -machao,마초,馬超,shu,cavalry,3,1.15,1.15,1.1,machao,machao_a,machao_p,0, -weiyan,위연,魏延,shu,infantry,3,1.15,1.15,1.1,weiyan,weiyan_a,weiyan_p,0, -pangtong,방통,龐統,shu,strategist,3,1.15,1.15,1.1,pangtong,pangtong_a,pangtong_p,0, -jiangwei,강유,姜維,shu,spear,3,1.15,1.15,1.1,jiangwei,jiangwei_a,jiangwei_p,0, -liufeng,유봉,劉封,shu,cavalry,2,1.05,1.05,1.05,liufeng,liufeng_a,liufeng_p,0,8-11 -jianyong,간옹,簡雍,shu,strategist,2,1.05,1.05,1.05,jianyong,jianyong_a,jianyong_p,0,3-12 -sunqian,손건,孫乾,shu,strategist,2,1.05,1.05,1.05,sunqian,sunqian_a,sunqian_p,0,3-12 -mizhu,미축,糜竺,shu,archer,2,1.05,1.05,1.05,mizhu,mizhu_a,mizhu_p,0,3-12 -caocao,조조,曹操,wei,infantry,3,1.15,1.15,1.1,cc_boss_caocao,caocao_a,caocao_p,0,1-8 -xiahoudun,하후돈,夏侯惇,wei,cavalry,3,1.15,1.15,1.1,cc_boss_xiahoudun,xiahoudun_a,xiahoudun_p,0,5-14 -xuchu,허저,許褚,wei,infantry,3,1.15,1.15,1.1,cc_boss_xuchu,xuchu_a,xuchu_p,0,5-15 -zhangliao,장료,張遼,wei,cavalry,3,1.15,1.15,1.1,dz_boss_zhangliao,zhangliao_a,zhangliao_p,0,4-14 -xuhuang,서황,徐晃,wei,infantry,3,1.15,1.15,1.1,cc_boss_xuhuang,xuhuang_a,xuhuang_p,0,5-15 -zhanghe,장합,張郃,wei,spear,3,1.15,1.15,1.1,cc_boss_zhanghe,zhanghe_a,zhanghe_p,0,7-9 -caoren,조인,曹仁,wei,infantry,3,1.15,1.15,1.1,cc_boss_caoren,caoren_a,caoren_p,0,8-10 -yujin,우금,于禁,wei,infantry,2,1.05,1.05,1.05,cc_boss_yujin,yujin_a,yujin_p,0,3-15 -simayi,사마의,司馬懿,wei,strategist,3,1.15,1.15,1.1,wei_boss_simayi,simayi_a,simayi_p,0, -xiahouyuan,하후연,夏侯淵,wei,archer,3,1.15,1.15,1.1,xiahouyuan,xiahouyuan_a,xiahouyuan_p,0, -guojia,곽가,郭嘉,wei,strategist,3,1.15,1.15,1.1,guojia,guojia_a,guojia_p,0, -jiaxu,가후,賈詡,wei,strategist,3,1.15,1.15,1.1,jiaxu,jiaxu_a,jiaxu_p,0, -dianwei,전위,典韋,wei,infantry,3,1.15,1.15,1.1,dianwei,dianwei_a,dianwei_p,0, -xunyu,순욱,荀彧,wei,strategist,3,1.15,1.15,1.1,xunyu,xunyu_a,xunyu_p,0, -xiahoumao,하후무,夏侯楙,wei,cavalry,1,1.0,1.0,1.0,xiahoumao,xiahoumao_a,xiahoumao_p,0, -wanglang,왕랑,王朗,wei,strategist,1,1.0,1.0,1.0,wanglang,wanglang_a,wanglang_p,0, -xiahouen,하후은,夏侯恩,wei,cavalry,1,1.0,1.0,1.0,cc_boss_xiahouen,xiahouen_a,xiahouen_p,0,9-8 -sunjian,손견,孫堅,wu,infantry,3,1.15,1.15,1.1,wu_sunjian,sunjian_a,sunjian_p,0,2-3 -zhouyu,주유,周瑜,wu,strategist,3,1.15,1.15,1.1,wu_zhouyu,zhouyu_a,zhouyu_p,0,10-3 -huanggai,황개,黃蓋,wu,infantry,3,1.15,1.15,1.1,wu_huanggai,huanggai_a,huanggai_p,0,10-7 -ganning,감녕,甘寧,wu,archer,3,1.15,1.15,1.1,wu_ganning,ganning_a,ganning_p,0,10-3 -chengpu,정보,程普,wu,spear,2,1.05,1.05,1.05,wu_chengpu,chengpu_a,chengpu_p,0,2-3 -sunce,손책,孫策,wu,cavalry,3,1.15,1.15,1.1,sunce,sunce_a,sunce_p,0, -sunquan,손권,孫權,wu,archer,3,1.15,1.15,1.1,sunquan,sunquan_a,sunquan_p,0, -lusu,노숙,魯肅,wu,strategist,3,1.15,1.15,1.1,lusu,lusu_a,lusu_p,0, -lvmeng,여몽,呂蒙,wu,infantry,3,1.15,1.15,1.1,lvmeng,lvmeng_a,lvmeng_p,0, -luxun,육손,陸遜,wu,strategist,3,1.15,1.15,1.1,luxun,luxun_a,luxun_p,0, -sunshangxiang,손상향,孫尙香,wu,archer,3,1.15,1.15,1.1,sunshangxiang,sunshangxiang_a,sunshangxiang_p,0, -xiaoqiao,소교,小喬,wu,strategist,3,1.15,1.15,1.1,xiaoqiao,xiaoqiao_a,xiaoqiao_p,0, -lingtong,능통,凌統,wu,spear,2,1.05,1.05,1.05,lingtong,lingtong_a,lingtong_p,0, -zhoutai,주태,周泰,wu,infantry,2,1.05,1.05,1.05,zhoutai,zhoutai_a,zhoutai_p,0, -zhugejin,제갈근,諸葛瑾,wu,strategist,2,1.05,1.05,1.05,zhugejin,zhugejin_a,zhugejin_p,0, -dongxi,동습,董襲,wu,spear,1,1.0,1.0,1.0,dongxi,dongxi_a,dongxi_p,0, -lvbu,여포,呂布,other,cavalry,3,1.15,1.15,1.1,dz_boss_lvbu,lvbu_a,lvbu_p,0,2-10 -zhangjiao,장각,張角,other,infantry,2,1.05,1.05,1.05,hgj_boss_zhangjiao,zhangjiao_a,zhangjiao_p,0,1-10 -huaxiong,화웅,華雄,other,infantry,2,1.05,1.05,1.05,dz_boss_huaxiong,huaxiong_a,huaxiong_p,0,2-5 -yanliang,안량,顏良,other,spear,2,1.05,1.05,1.05,ys_boss_yanliang,yanliang_a,yanliang_p,0,6-5 -wenchou,문추,文醜,other,cavalry,2,1.05,1.05,1.05,ys_boss_wenchou,wenchou_a,wenchou_p,0,3-3 -gongsunzan,공손찬,公孫瓚,other,cavalry,2,1.05,1.05,1.05,ys_gongsunzan,gongsunzan_a,gongsunzan_p,0,2-1 -dongzhuo,동탁,董卓,other,infantry,3,1.15,1.15,1.1,dongzhuo,dongzhuo_a,dongzhuo_p,0, -lijue,이각,李傕,other,cavalry,2,1.05,1.05,1.05,lijue,lijue_a,lijue_p,0, -yuanshao,원소,袁紹,other,archer,3,1.15,1.15,1.1,yuanshao,yuanshao_a,yuanshao_p,0, -yuanshu,원술,袁術,other,infantry,2,1.05,1.05,1.05,yuanshu,yuanshu_a,yuanshu_p,0, -diaochan,초선,貂蟬,other,strategist,3,1.15,1.15,1.1,diaochan,diaochan_a,diaochan_p,0, -zhurong,축융,祝融,other,spear,3,1.15,1.15,1.1,zhurong,zhurong_a,zhurong_p,0, -menghuo,맹획,孟獲,other,infantry,3,1.15,1.15,1.1,menghuo,menghuo_a,menghuo_p,0, -gaoshun,고순,高順,other,infantry,2,1.05,1.05,1.05,dz_boss_gaoshun,gaoshun_a,gaoshun_p,0,4-10 -zhanglu,장로,張魯,other,strategist,2,1.05,1.05,1.05,zhanglu,zhanglu_a,zhanglu_p,0, -yanbaihu,엄백호,嚴白虎,other,infantry,1,1.0,1.0,1.0,yanbaihu,yanbaihu_a,yanbaihu_p,0, -hanxuan,한현,韓玄,other,spear,1,1.0,1.0,1.0,hanxuan,hanxuan_a,hanxuan_p,0, -jinxuan,금선,金旋,other,archer,1,1.0,1.0,1.0,jinxuan,jinxuan_a,jinxuan_p,0, -zhaofan,조범,趙範,other,strategist,1,1.0,1.0,1.0,zhaofan,zhaofan_a,zhaofan_p,0, -zhangren,장임,張任,other,archer,1,1.0,1.0,1.0,zhangren,zhangren_a,zhangren_p,0, -chunyuqiong,순우경,淳于瓊,other,cavalry,1,1.0,1.0,1.0,chunyuqiong,chunyuqiong_a,chunyuqiong_p,0, -soldier_infantry_1,정예 보병,步兵,shu,infantry,1,0.85,0.85,0.85,shu_infantry,,,1, -soldier_infantry_2,영웅 보병,步兵,shu,infantry,2,0.95,0.95,0.95,shu_infantry,,,1, -soldier_infantry_3,전설 보병,步兵,shu,infantry,3,1.05,1.05,1.05,shu_infantry,,,1, -soldier_spear_1,정예 창병,槍兵,shu,spear,1,0.85,0.85,0.85,shu_spear,,,1, -soldier_spear_2,영웅 창병,槍兵,shu,spear,2,0.95,0.95,0.95,shu_spear,,,1, -soldier_spear_3,전설 창병,槍兵,shu,spear,3,1.05,1.05,1.05,shu_spear,,,1, -soldier_cavalry_1,정예 기병,騎兵,shu,cavalry,1,0.85,0.85,0.85,shu_cavalry,,,1, -soldier_cavalry_2,영웅 기병,騎兵,shu,cavalry,2,0.95,0.95,0.95,shu_cavalry,,,1, -soldier_cavalry_3,전설 기병,騎兵,shu,cavalry,3,1.05,1.05,1.05,shu_cavalry,,,1, -soldier_archer_1,정예 궁병,弓兵,shu,archer,1,0.85,0.85,0.85,shu_archer,,,1, -soldier_archer_2,영웅 궁병,弓兵,shu,archer,2,0.95,0.95,0.95,shu_archer,,,1, -soldier_archer_3,전설 궁병,弓兵,shu,archer,3,1.05,1.05,1.05,shu_archer,,,1, -soldier_strategist_1,정예 책사,策士,shu,strategist,1,0.85,0.85,0.85,shu_strategist,,,1, -soldier_strategist_2,영웅 책사,策士,shu,strategist,2,0.95,0.95,0.95,shu_strategist,,,1, -soldier_strategist_3,전설 책사,策士,shu,strategist,3,1.05,1.05,1.05,shu_strategist,,,1, +id,name_ko,hanja,faction,unit_type,rarity,hp_mult,atk_mult,def_mult,art,skill_active,skill_passive,is_soldier,first_stage,gender +liubei,유비,劉備,shu,infantry,3,1.1,1.0,1.0,liubei,liubei_a,liubei_p,0,1-1,m +guanyu,관우,關羽,shu,spear,3,1.1,1.25,1.0,guanyu,guanyu_a,guanyu_p,0,1-1,m +zhangfei,장비,張飛,shu,cavalry,3,1.0,1.2,1.0,zhangfei,zhangfei_a,zhangfei_p,0,1-1,m +zhaoyun,조운,趙雲,shu,cavalry,3,1.1,1.1,1.0,zhaoyun,zhaoyun_a,zhaoyun_p,0,3-3,m +huangzhong,황충,黃忠,shu,archer,3,1.0,1.25,1.0,huangzhong,huangzhong_a,huangzhong_p,0,,m +zhugeliang,제갈량,諸葛亮,shu,strategist,3,1.15,1.15,1.1,zhugeliang,zhugeliang_a,zhugeliang_p,0,8-13,m +zhoucang,주창,周倉,shu,infantry,2,1.05,1.05,1.05,zhoucang,zhoucang_a,zhoucang_p,0,7-2,m +guanping,관평,關平,shu,spear,2,1.05,1.05,1.05,guanping,guanping_a,guanping_p,0,7-7,m +xushu,서서,徐庶,shu,strategist,3,1.15,1.15,1.1,xushu,xushu_a,xushu_p,0,8-6,m +taishici,태사자,太史慈,shu,archer,3,1.15,1.15,1.1,taishici,taishici_a,taishici_p,0,3-8,m +machao,마초,馬超,shu,cavalry,3,1.15,1.15,1.1,machao,machao_a,machao_p,0,,m +weiyan,위연,魏延,shu,infantry,3,1.15,1.15,1.1,weiyan,weiyan_a,weiyan_p,0,,m +pangtong,방통,龐統,shu,strategist,3,1.15,1.15,1.1,pangtong,pangtong_a,pangtong_p,0,,m +jiangwei,강유,姜維,shu,spear,3,1.15,1.15,1.1,jiangwei,jiangwei_a,jiangwei_p,0,,m +liufeng,유봉,劉封,shu,cavalry,2,1.05,1.05,1.05,liufeng,liufeng_a,liufeng_p,0,8-11,m +jianyong,간옹,簡雍,shu,strategist,2,1.05,1.05,1.05,jianyong,jianyong_a,jianyong_p,0,3-12,m +sunqian,손건,孫乾,shu,strategist,2,1.05,1.05,1.05,sunqian,sunqian_a,sunqian_p,0,3-12,m +mizhu,미축,糜竺,shu,archer,2,1.05,1.05,1.05,mizhu,mizhu_a,mizhu_p,0,3-12,m +caocao,조조,曹操,wei,infantry,3,1.15,1.15,1.1,cc_boss_caocao,caocao_a,caocao_p,0,1-8,m +xiahoudun,하후돈,夏侯惇,wei,cavalry,3,1.15,1.15,1.1,cc_boss_xiahoudun,xiahoudun_a,xiahoudun_p,0,5-14,m +xuchu,허저,許褚,wei,infantry,3,1.15,1.15,1.1,cc_boss_xuchu,xuchu_a,xuchu_p,0,5-15,m +zhangliao,장료,張遼,wei,cavalry,3,1.15,1.15,1.1,dz_boss_zhangliao,zhangliao_a,zhangliao_p,0,4-14,m +xuhuang,서황,徐晃,wei,infantry,3,1.15,1.15,1.1,cc_boss_xuhuang,xuhuang_a,xuhuang_p,0,5-15,m +zhanghe,장합,張郃,wei,spear,3,1.15,1.15,1.1,cc_boss_zhanghe,zhanghe_a,zhanghe_p,0,7-9,m +caoren,조인,曹仁,wei,infantry,3,1.15,1.15,1.1,cc_boss_caoren,caoren_a,caoren_p,0,8-10,m +yujin,우금,于禁,wei,infantry,2,1.05,1.05,1.05,cc_boss_yujin,yujin_a,yujin_p,0,3-15,m +simayi,사마의,司馬懿,wei,strategist,3,1.15,1.15,1.1,wei_boss_simayi,simayi_a,simayi_p,0,,m +xiahouyuan,하후연,夏侯淵,wei,archer,3,1.15,1.15,1.1,xiahouyuan,xiahouyuan_a,xiahouyuan_p,0,,m +guojia,곽가,郭嘉,wei,strategist,3,1.15,1.15,1.1,guojia,guojia_a,guojia_p,0,,m +jiaxu,가후,賈詡,wei,strategist,3,1.15,1.15,1.1,jiaxu,jiaxu_a,jiaxu_p,0,,m +dianwei,전위,典韋,wei,infantry,3,1.15,1.15,1.1,dianwei,dianwei_a,dianwei_p,0,,m +xunyu,순욱,荀彧,wei,strategist,3,1.15,1.15,1.1,xunyu,xunyu_a,xunyu_p,0,,m +xiahoumao,하후무,夏侯楙,wei,cavalry,1,1.0,1.0,1.0,xiahoumao,xiahoumao_a,xiahoumao_p,0,,m +wanglang,왕랑,王朗,wei,strategist,1,1.0,1.0,1.0,wanglang,wanglang_a,wanglang_p,0,,m +xiahouen,하후은,夏侯恩,wei,cavalry,1,1.0,1.0,1.0,cc_boss_xiahouen,xiahouen_a,xiahouen_p,0,9-8,m +sunjian,손견,孫堅,wu,infantry,3,1.15,1.15,1.1,wu_sunjian,sunjian_a,sunjian_p,0,2-3,m +zhouyu,주유,周瑜,wu,strategist,3,1.15,1.15,1.1,wu_zhouyu,zhouyu_a,zhouyu_p,0,10-3,m +huanggai,황개,黃蓋,wu,infantry,3,1.15,1.15,1.1,wu_huanggai,huanggai_a,huanggai_p,0,10-7,m +ganning,감녕,甘寧,wu,archer,3,1.15,1.15,1.1,wu_ganning,ganning_a,ganning_p,0,10-3,m +chengpu,정보,程普,wu,spear,2,1.05,1.05,1.05,wu_chengpu,chengpu_a,chengpu_p,0,2-3,m +sunce,손책,孫策,wu,cavalry,3,1.15,1.15,1.1,sunce,sunce_a,sunce_p,0,,m +sunquan,손권,孫權,wu,archer,3,1.15,1.15,1.1,sunquan,sunquan_a,sunquan_p,0,,m +lusu,노숙,魯肅,wu,strategist,3,1.15,1.15,1.1,lusu,lusu_a,lusu_p,0,,m +lvmeng,여몽,呂蒙,wu,infantry,3,1.15,1.15,1.1,lvmeng,lvmeng_a,lvmeng_p,0,,m +luxun,육손,陸遜,wu,strategist,3,1.15,1.15,1.1,luxun,luxun_a,luxun_p,0,,m +sunshangxiang,손상향,孫尙香,wu,archer,3,1.15,1.15,1.1,sunshangxiang,sunshangxiang_a,sunshangxiang_p,0,,f +xiaoqiao,소교,小喬,wu,strategist,3,1.15,1.15,1.1,xiaoqiao,xiaoqiao_a,xiaoqiao_p,0,,f +lingtong,능통,凌統,wu,spear,2,1.05,1.05,1.05,lingtong,lingtong_a,lingtong_p,0,,m +zhoutai,주태,周泰,wu,infantry,2,1.05,1.05,1.05,zhoutai,zhoutai_a,zhoutai_p,0,,m +zhugejin,제갈근,諸葛瑾,wu,strategist,2,1.05,1.05,1.05,zhugejin,zhugejin_a,zhugejin_p,0,,m +dongxi,동습,董襲,wu,spear,1,1.0,1.0,1.0,dongxi,dongxi_a,dongxi_p,0,,m +lvbu,여포,呂布,other,cavalry,3,1.15,1.15,1.1,dz_boss_lvbu,lvbu_a,lvbu_p,0,2-10,m +zhangjiao,장각,張角,other,infantry,2,1.05,1.05,1.05,hgj_boss_zhangjiao,zhangjiao_a,zhangjiao_p,0,1-10,m +huaxiong,화웅,華雄,other,infantry,2,1.05,1.05,1.05,dz_boss_huaxiong,huaxiong_a,huaxiong_p,0,2-5,m +yanliang,안량,顏良,other,spear,2,1.05,1.05,1.05,ys_boss_yanliang,yanliang_a,yanliang_p,0,6-5,m +wenchou,문추,文醜,other,cavalry,2,1.05,1.05,1.05,ys_boss_wenchou,wenchou_a,wenchou_p,0,3-3,m +gongsunzan,공손찬,公孫瓚,other,cavalry,2,1.05,1.05,1.05,ys_gongsunzan,gongsunzan_a,gongsunzan_p,0,2-1,m +dongzhuo,동탁,董卓,other,infantry,3,1.15,1.15,1.1,dongzhuo,dongzhuo_a,dongzhuo_p,0,,m +lijue,이각,李傕,other,cavalry,2,1.05,1.05,1.05,lijue,lijue_a,lijue_p,0,,m +yuanshao,원소,袁紹,other,archer,3,1.15,1.15,1.1,yuanshao,yuanshao_a,yuanshao_p,0,,m +yuanshu,원술,袁術,other,infantry,2,1.05,1.05,1.05,yuanshu,yuanshu_a,yuanshu_p,0,,m +diaochan,초선,貂蟬,other,strategist,3,1.15,1.15,1.1,diaochan,diaochan_a,diaochan_p,0,,f +zhurong,축융,祝融,other,spear,3,1.15,1.15,1.1,zhurong,zhurong_a,zhurong_p,0,,f +menghuo,맹획,孟獲,other,infantry,3,1.15,1.15,1.1,menghuo,menghuo_a,menghuo_p,0,,m +gaoshun,고순,高順,other,infantry,2,1.05,1.05,1.05,dz_boss_gaoshun,gaoshun_a,gaoshun_p,0,4-10,m +zhanglu,장로,張魯,other,strategist,2,1.05,1.05,1.05,zhanglu,zhanglu_a,zhanglu_p,0,,m +yanbaihu,엄백호,嚴白虎,other,infantry,1,1.0,1.0,1.0,yanbaihu,yanbaihu_a,yanbaihu_p,0,,m +hanxuan,한현,韓玄,other,spear,1,1.0,1.0,1.0,hanxuan,hanxuan_a,hanxuan_p,0,,m +jinxuan,금선,金旋,other,archer,1,1.0,1.0,1.0,jinxuan,jinxuan_a,jinxuan_p,0,,m +zhaofan,조범,趙範,other,strategist,1,1.0,1.0,1.0,zhaofan,zhaofan_a,zhaofan_p,0,,m +zhangren,장임,張任,other,archer,1,1.0,1.0,1.0,zhangren,zhangren_a,zhangren_p,0,,m +chunyuqiong,순우경,淳于瓊,other,cavalry,1,1.0,1.0,1.0,chunyuqiong,chunyuqiong_a,chunyuqiong_p,0,,m +soldier_infantry_1,정예 보병,步兵,shu,infantry,1,0.85,0.85,0.85,shu_infantry,,,1,,m +soldier_infantry_2,영웅 보병,步兵,shu,infantry,2,0.95,0.95,0.95,shu_infantry,,,1,,m +soldier_infantry_3,전설 보병,步兵,shu,infantry,3,1.05,1.05,1.05,shu_infantry,,,1,,m +soldier_spear_1,정예 창병,槍兵,shu,spear,1,0.85,0.85,0.85,shu_spear,,,1,,m +soldier_spear_2,영웅 창병,槍兵,shu,spear,2,0.95,0.95,0.95,shu_spear,,,1,,m +soldier_spear_3,전설 창병,槍兵,shu,spear,3,1.05,1.05,1.05,shu_spear,,,1,,m +soldier_cavalry_1,정예 기병,騎兵,shu,cavalry,1,0.85,0.85,0.85,shu_cavalry,,,1,,m +soldier_cavalry_2,영웅 기병,騎兵,shu,cavalry,2,0.95,0.95,0.95,shu_cavalry,,,1,,m +soldier_cavalry_3,전설 기병,騎兵,shu,cavalry,3,1.05,1.05,1.05,shu_cavalry,,,1,,m +soldier_archer_1,정예 궁병,弓兵,shu,archer,1,0.85,0.85,0.85,shu_archer,,,1,,m +soldier_archer_2,영웅 궁병,弓兵,shu,archer,2,0.95,0.95,0.95,shu_archer,,,1,,m +soldier_archer_3,전설 궁병,弓兵,shu,archer,3,1.05,1.05,1.05,shu_archer,,,1,,m +soldier_strategist_1,정예 책사,策士,shu,strategist,1,0.85,0.85,0.85,shu_strategist,,,1,,m +soldier_strategist_2,영웅 책사,策士,shu,strategist,2,0.95,0.95,0.95,shu_strategist,,,1,,m +soldier_strategist_3,전설 책사,策士,shu,strategist,3,1.05,1.05,1.05,shu_strategist,,,1,,m diff --git a/Assets/Data/OutGame/skill_effects.csv b/Assets/Data/OutGame/skill_effects.csv new file mode 100644 index 00000000..c9fd17f6 --- /dev/null +++ b/Assets/Data/OutGame/skill_effects.csv @@ -0,0 +1,289 @@ +skill_id,key,target,cond,dur,p1,p2,p3,note,effect_ko,p1_ko,p2_ko,p3_ko +liubei_a,extra_action,,,0,1,,,행동 뒤 재행동 1회,행동 뒤 한 번 더 행동(재행동),재행동 횟수,, +liubei_a,counter_taken_pct,,,1,-1,,,1턴 반격 안 받음,받는 반격 피해 증감 · 1턴 지속,받는 반격 비율(−1 = 반격 안 받음 · −0.5 = 반감),, +liubei_p,atk_pct,camp:shu,,0,0.2,,,,공격력 증가 · 대상: 촉 진영 유닛,공격력 +비율(0.2 = +20 %),, +liubei_p,revive,,,0,1.0,1,0,HP 비율·횟수(전투당)·조건 HP 비율,쓰러지면 부활,부활 HP 비율(1.0 = 100 %),전투당 횟수,맞기 전 HP 가 이 비율 이상일 때만(0 = 조건 없음) +guanyu_a,aoe_strike,,,0,3,2.5,1,반경·공격 배율·밀기,자신 주위 적 전원 공격·밀침,반경,피해 = 공격력 × 배수(0 = 피해 없음),바깥으로 밀기 거리 +guanyu_a,hit_bleed,,,0,2,0.5,1,턴(기본값)·이동 비례·확률,명중한 적 출혈(움직인 거리에 비례해 생명력 감소),출혈 턴,슬링 최대 거리만큼 움직일 때 최대 HP × 비율,확률 +guanyu_p,counter_dmg_pct,camp:shu,,0,0.5,,,,주는 반격 피해 증가 · 대상: 촉 진영 유닛,반격 피해 +비율(0.5 = +50 % · 1.0 = 2배),, +guanyu_p,counter_taken_pct,,,0,-1,,,,받는 반격 피해 증감,받는 반격 비율(−1 = 반격 안 받음 · −0.5 = 반감),, +guanyu_p,kill_stack_atk,,,0,0.1,1.0,,처치당·최대,적 처치마다 공격력 누적(전투 동안),처치 1기당 공격 +비율,최대 누적 비율, +zhangfei_a,aoe_strike,,,0,4,0,1,반경·피해 없음·밀기,자신 주위 적 전원 공격·밀침,반경,피해 = 공격력 × 배수(0 = 피해 없음),바깥으로 밀기 거리 +zhangfei_a,hit_stun,,,0,1,,,,명중한 적 기절(다음 자기 턴 행동 불가),기절 턴,, +zhangfei_p,sling_dist_pct,camp:shu,,0,0.3,,,,이동(슬링) 거리 증가 · 대상: 촉 진영 유닛,이동 거리 +비율(1.0 = 2배),, +zhangfei_p,dash_dmg_pct,,,0,1.0,,,슬링 거리 비례 최대,돌진(슬링) 거리에 비례해 공격력 증가,슬링 최대 거리로 당겼을 때 공격 +비율(최대),, +zhangfei_p,double_hit_stun,,,0,2,1,,연속 피해 수·기절 턴,같은 적에게 연속 행동으로 피해를 주면 기절,연속 피해 횟수,기절 턴, +zhangfei_p,lifesteal,,vs_stunned,0,0.5,,,,입힌 피해만큼 회복(흡혈) · 조건: 상대가 기절일 때,입힌 피해 × 비율 회복,, +zhaoyun_a,sling_dist_pct,,,0,1.0,,,,이동(슬링) 거리 증가,이동 거리 +비율(1.0 = 2배),, +zhaoyun_a,dmg_pct,,,0,0.5,,,,주는 피해 증가,피해 +비율(0.5 = +50 %),, +zhaoyun_p,counter_taken_pct,,,0,-1,,,,받는 반격 피해 증감,받는 반격 비율(−1 = 반격 안 받음 · −0.5 = 반감),, +zhaoyun_p,return_on_hit,,,0,1,0,,복귀 중 피해·쓸어 감,피해를 입히면 출발 위치로 복귀,1 = 복귀하며 경로 위 적에게도 피해,1 = 복귀 경로의 유닛을 함께 쓸어 감, +huangzhong_a,shot_giant,,,0,0.5,5.0,,화살 반지름(기본값)·공격 배율 · 사거리 무관,사거리 무관 거대 화살 — 첫 적에게 큰 피해,화살 반지름,피해 = 공격력 × 배수, +huangzhong_a,hit_stun,,,0,1,,,,명중한 적 기절(다음 자기 턴 행동 불가),기절 턴,, +huangzhong_p,arrow_pass_allies,,,0,1,1,,p2 1 = 장애물도 통과,화살이 아군을 통과,1 = 켬,1 = 장애물도 통과, +huangzhong_p,crit_chance,,,0,0.2,,,,치명타 확률 증가,확률 +(0.2 = +20 %p),, +huangzhong_p,crit_dmg_pct,,,0,0.5,,,,치명타 피해량 증가,치명타 피해 +비율(0.5 = +50 %),, +zhugeliang_a,heal_atk,range,,0,1.0,,,,회복 · 대상: 시전 순간 행동 반경 안 아군,회복 = 공격력 × 배수,, +zhugeliang_a,cleanse,range,,0,1,,,,상태 이상 제거(혼란·출혈·중독·화상·기절) · 대상: 시전 순간 행동 반경 안 아군,1 = 켬,, +zhugeliang_p,counter_taken_pct,camp:shu,,0,-0.5,,,,받는 반격 피해 증감 · 대상: 촉 진영 유닛,받는 반격 비율(−1 = 반격 안 받음 · −0.5 = 반감),, +zhugeliang_p,turn_end_heal_lowest,,,0,3,0.5,,반경·공격 배율,턴 종료 시 주위에서 생명력이 가장 낮은 아군 회복,반경,회복 = 공격력 × 배수, +zhoucang_a,atk_ratio,,,0,1.8,,,,이 공격의 피해 = 공격력의 배수,공격력 배수(1.8 = 180 %),, +zhoucang_a,hit_bleed,,,0,2,0.5,1,,명중한 적 출혈(움직인 거리에 비례해 생명력 감소),출혈 턴,슬링 최대 거리만큼 움직일 때 최대 HP × 비율,확률 +zhoucang_a,push_mult,,,0,2,,,,밀치는 거리 배율,밀기 ×배율(2 = 2배),, +zhoucang_p,atk_pct,,vs_bleed,0,0.5,,,,공격력 증가 · 조건: 상대가 출혈일 때,공격력 +비율(0.2 = +20 %),, +guanping_a,guard_radius_mult,,,2,2,,,,호위·반격 반경 배율 · 2턴 지속,반경 ×배율,, +guanping_a,guard_dr,,,2,-0.5,,,,호위로 대신 받는 피해 감소 · 2턴 지속,받는 비율(−0.5 = 반감),, +guanping_p,counter_dmg_pct,,,0,0.5,,,,주는 반격 피해 증가,반격 피해 +비율(0.5 = +50 % · 1.0 = 2배),, +guanping_p,guard,,,0,2,1.0,,반경·대신 받는 비율,호위 — 반경 안 아군 피해를 대신 받음,호위 반경,대신 받는 비율(1.0 = 전부), +xushu_a,shot_blast,,,0,1.5,2.5,,폭발 반경(기본값 = 책사 광역)·공격 배율,투사체가 첫 적 자리에서 폭발 — 반경 안 적 전원 피해,폭발 반경,피해 = 공격력 × 배수, +xushu_a,hit_burn,,,0,3,0.05,1,,명중한 적 화상(불태우기 · 턴 시작마다 생명력 감소),화상 턴,1회 최대 HP × 비율,확률 +xushu_p,range_mult,,,0,1.5,,,,공격 사거리 배율,사거리 ×배율(1.5 = +50 %),, +xushu_p,splash_ally_ratio,,,0,0,,,,자신의 광역이 아군에게 주는 피해 비율,아군 피해 비율(0 = 아군 피해 없음),, +taishici_a,shot_pierce,,,0,3.0,0,0,공격 배율·아군(0 통과)·밀기 · 장애물 무시,직선 관통 사격 — 경로 위 유닛 전원(장애물 무시),피해 = 공격력 × 배수,아군 처리(0 통과 · 1 피해 · 2 회복),밀기 거리(0 = 없음) +taishici_p,melee_penalty_off,,,0,1,,,,궁병·책사 근접 전투 패널티 없음,1 = 켬,, +taishici_p,atk_pct,,,0,0.5,,,,공격력 증가,공격력 +비율(0.2 = +20 %),, +taishici_p,crit_dmg_pct,,,0,0.5,,,,치명타 피해량 증가,치명타 피해 +비율(0.5 = +50 %),, +machao_a,atk_pct,,,0,0.5,,,,공격력 증가,공격력 +비율(0.2 = +20 %),, +machao_a,crit_always,,,0,1,,,,무조건 치명타,1 = 켬,, +machao_p,lifesteal,,,0,0.2,,,,입힌 피해만큼 회복(흡혈),입힌 피해 × 비율 회복,, +machao_p,dmg_per_hit,,,0,0.25,,,이번 행동 앞서 맞힌 적 1기당,한 행동에서 앞서 맞힌 적 1기당 피해 증가,맞힌 적 1기당 피해 +비율,, +weiyan_a,swap_on_hit,,,0,1,,,명중 대상 ↔ 출발 위치,명중한 대상을 자신의 출발 위치로 보내고 자신은 그 자리로(위치 교환),1 = 켬,, +weiyan_a,extra_action,,,0,1,,,,행동 뒤 한 번 더 행동(재행동),재행동 횟수,, +weiyan_p,matchup_floor,,,0,1.0,,,불리 상성 무시,상성 무시(불리한 상성만 무시 · 유리 상성은 유지),상성 배율 하한(1.0 = 불리 상성 없음),, +weiyan_p,atk_missing_hp,,,0,1.0,1.0,,잃은 HP 비율 × p1 · 최대,잃은 생명력 비율만큼 공격력 증가,잃은 HP 비율 × 계수(1.0 = HP 1 % 감소당 공격 1 %),최대 증가 비율(빈칸 = 제한 없음), +pangtong_a,heal_atk,range,,0,0.5,,,,회복 · 대상: 시전 순간 행동 반경 안 아군,회복 = 공격력 × 배수,, +pangtong_a,hot,range,,3,0.1,,,매 턴 시작 공격 배율,지속 회복 — 매 자기 턴 시작마다 · 대상: 시전 순간 행동 반경 안 아군 · 3턴 지속,회복 = 시전자 공격력 × 배수,, +pangtong_p,dmg_taken_ranged_pct,camp:shu,,0,-0.5,,,,받는 원거리(사격·광역) 피해 증감 · 대상: 촉 진영 유닛,받는 원거리 피해 비율(−0.5 = 반감),, +pangtong_p,turn_end_cleanse,,,0,3,1,,반경·인원,턴 종료 시 주위 아군 상태 이상 제거,반경,인원, +jiangwei_a,heal_self_pct,,,0,0.5,,,,자신 생명력 회복,최대 HP × 비율,, +jiangwei_a,guard_radius_mult,,,3,1.5,,,,호위·반격 반경 배율 · 3턴 지속,반경 ×배율,, +jiangwei_p,counter_dmg_pct,,,0,0.5,,,,주는 반격 피해 증가,반격 피해 +비율(0.5 = +50 % · 1.0 = 2배),, +jiangwei_p,guard_counter,,,0,2,1.0,,반격 범위(기본값 = 관평 호위 2)·대신 받는 비율,호위 반격 — 반경 안 아군 피해를 대신 받고 공격자에게 반격,반격(호위) 반경,대신 받는 비율, +liufeng_a,atk_ratio,,,0,1.85,,,,이 공격의 피해 = 공격력의 배수,공격력 배수(1.8 = 180 %),, +liufeng_a,hit_confuse,,,0,1,0.5,1,턴·이동 배율·확률,명중한 적 혼란(이동 거리 감소),혼란 턴,이동 거리 배율(0.5 = 반감),확률 +liufeng_p,counter_taken_pct,,vs_confused,0,-1,,,,받는 반격 피해 증감 · 조건: 상대가 혼란일 때,받는 반격 비율(−1 = 반격 안 받음 · −0.5 = 반감),, +liufeng_p,hit_stun,,vs_confused,0,1,,,,명중한 적 기절(다음 자기 턴 행동 불가) · 조건: 상대가 혼란일 때,기절 턴,, +jianyong_a,ally_hit_heal,,,0,0.8,,,,자신의 공격이 아군에 맞으면 피해 대신 회복,회복 = 공격력 × 배수,, +jianyong_a,hit_stun,,,0,1,,,,명중한 적 기절(다음 자기 턴 행동 불가),기절 턴,, +jianyong_p,heal_cleanse,,,0,1,,,,자신이 회복시킨 대상의 상태 이상 제거,1 = 켬,, +jianyong_p,on_attacked_stun,,,0,1,3,,기절 턴·재충전 턴,자신을 공격한 대상 기절(재충전),기절 턴,재충전 턴, +sunqian_a,splash_heal,,,0,0.6,,,명중 대상 주위 광역 = 회복(적 포함),책사 광역이 피해 대신 회복(적 포함),회복 = 공격력 × 배수,, +sunqian_p,sling_dist_pct,radius:3,,0,0.3,,,,이동(슬링) 거리 증가 · 대상: 자신 주위 3 안 아군,이동 거리 +비율(1.0 = 2배),, +mizhu_a,sling_dist_pct,range,,1,0.5,,,,이동(슬링) 거리 증가 · 대상: 시전 순간 행동 반경 안 아군 · 1턴 지속,이동 거리 +비율(1.0 = 2배),, +mizhu_a,counter_taken_pct,range,,1,-0.5,,,,받는 반격 피해 증감 · 대상: 시전 순간 행동 반경 안 아군 · 1턴 지속,받는 반격 비율(−1 = 반격 안 받음 · −0.5 = 반감),, +mizhu_p,ally_hit_buff,,,1,0.2,0.2,,이동 %·공격 %,자신의 공격이 아군에 맞으면 버프(피해 없음) · 1턴 지속,이동 거리 +비율,공격력 +비율, +caocao_a,pass_allies,,,0,1,,,,아군을 통과해 돌진,1 = 켬,, +caocao_a,aim_push,,,0,1,,,명중 적을 탭한 방향으로 밀침,명중한 적을 탭한 방향으로 밀침(자신은 밀리지 않음),1 = 켬,, +caocao_p,atk_pct,camp:wei,,0,0.2,,,,공격력 증가 · 대상: 위 진영 유닛,공격력 +비율(0.2 = +20 %),, +caocao_p,matchup_floor,,,0,1.0,,,,상성 무시(불리한 상성만 무시 · 유리 상성은 유지),상성 배율 하한(1.0 = 불리 상성 없음),, +caocao_p,on_ally_death_atk,allies,,0,0.1,,,,아군이 쓰러질 때마다 공격력 증가 · 대상: 같은 편 전원,아군 1기 전사당 공격 +비율,, +xiahoudun_a,sling_dist_pct,,,0,0.5,,,,이동(슬링) 거리 증가,이동 거리 +비율(1.0 = 2배),, +xiahoudun_a,return_aim,,,0,1,,,탭한 곳으로 복귀,탭한 지점으로 복귀(거리 무관),1 = 켬,, +xiahoudun_p,dmg_pct,camp:wei:cavalry,,0,0.5,,,,주는 피해 증가 · 대상: 위 진영 기병 유닛,피해 +비율(0.5 = +50 %),, +xiahoudun_p,return_on_hit,,,0,0,1,,복귀 피해 없음·경로 유닛 쓸어 감,피해를 입히면 출발 위치로 복귀,1 = 복귀하며 경로 위 적에게도 피해,1 = 복귀 경로의 유닛을 함께 쓸어 감, +xiahoudun_p,matchup_floor,,,0,1.0,,,,상성 무시(불리한 상성만 무시 · 유리 상성은 유지),상성 배율 하한(1.0 = 불리 상성 없음),, +xuchu_a,self_bounce_mult,,,0,2,,,,부딪힌 뒤 자신이 튕기는 거리 배율,튕김 ×배율(0 = 튕기지 않음 · 2 = 2배),, +xuchu_a,hit_stun,,,0,1,,,,명중한 적 기절(다음 자기 턴 행동 불가),기절 턴,, +xuchu_p,hp_max_pct,,,0,0.5,,,,최대 생명력 증가,최대 HP +비율,, +xuchu_p,knockback_taken_pct,,,0,-1,,,,밀리는 거리 증감,밀림 비율(−1 = 밀리지 않음),, +xuchu_p,regen_pct,,,0,0.05,,,,자기 턴 시작마다 생명력 회복,최대 HP × 비율 회복,, +zhangliao_a,drag_back,,,0,1,,,명중 적을 출발 위치로 끌고 옴,관통으로 맞힌 적을 자신의 출발 위치로 끌고 옴,1 = 켬,, +zhangliao_a,hit_confuse,,,0,1,0.5,1,,명중한 적 혼란(이동 거리 감소),혼란 턴,이동 거리 배율(0.5 = 반감),확률 +zhangliao_p,atk_pct,,vs_status,0,0.5,,,,공격력 증가 · 조건: 상대가 상태 이상일 때,공격력 +비율(0.2 = +20 %),, +zhangliao_p,counter_taken_pct,,vs_status,0,-1,,,,받는 반격 피해 증감 · 조건: 상대가 상태 이상일 때,받는 반격 비율(−1 = 반격 안 받음 · −0.5 = 반감),, +xuhuang_a,axe_aura,,,3,1.2,0.8,2,도끼 반경(기본값)·공격 배율·튕김 배율,주위를 도는 도끼 — 자신을 친 근접 유닛에게 반격 피해 + 그 유닛이 더 멀리 튕김 · 3턴 지속,도끼 반경,반격 피해 = 공격력 × 배수,공격자 튕김 거리 배율 +xuhuang_p,hit_bleed,,,0,3,0.5,0.3,,명중한 적 출혈(움직인 거리에 비례해 생명력 감소),출혈 턴,슬링 최대 거리만큼 움직일 때 최대 HP × 비율,확률 +xuhuang_p,hit_bleed,,crit,0,3,0.5,1,,명중한 적 출혈(움직인 거리에 비례해 생명력 감소) · 조건: 치명타일 때,출혈 턴,슬링 최대 거리만큼 움직일 때 최대 HP × 비율,확률 +zhanghe_a,teleport,,,0,5,,,,반경 안 원하는 곳으로 이동(대상 무시 · 지점 탭),이동 반경,, +zhanghe_a,extra_action,,,0,1,,,,행동 뒤 한 번 더 행동(재행동),재행동 횟수,, +zhanghe_p,push_mult,,,0,2,,,,밀치는 거리 배율,밀기 ×배율(2 = 2배),, +zhanghe_p,pushed_ally_atk_add,,,0,1.0,,,밀린 아군 공격 + 자신 공격력 × p1,밀린 아군이 적을 치면 자신의 공격력만큼 더함,자신 공격력 × 배수 가산,, +zhanghe_p,self_pushed_mult,,,0,2,,,자신이 밀려 공격 = 피해 × p1,자신이 밀려 적을 친 공격 피해 배율,피해 ×배율,, +caoren_a,dmg_taken_pct,aura,,3,-0.3,,,,받는 피해 증감 · 대상: 자신의 행동 반경 안 아군 · 3턴 지속,받는 피해 비율(−0.3 = 30 % 감소 · +0.15 = 15 % 증가),, +caoren_a,zone_slow,,,3,0.5,,,반경 안 적 이동 비용 +,자신의 행동 반경을 지나는 적의 이동 비용 증가 · 3턴 지속,이동 비용 +비율(0.5 = 1.5배),, +caoren_p,hp_max_pct,camp:wei,,0,0.2,,,,최대 생명력 증가 · 대상: 위 진영 유닛,최대 HP +비율,, +caoren_p,heal_on_action_end,,,0,0.2,,,,행동을 멈출 때마다 생명력 회복,최대 HP × 비율 회복,, +caoren_p,unused_move_dr,,,0,0.5,,,,남은(쓰지 않은) 이동 거리만큼 받는 피해 감소,최대 감소 비율(이동 안 함 = 최대),, +yujin_a,ally_hit_reaction,,,0,1,,,,이 행동으로 맞힌(밀친) 행동 끝난 아군 재행동,1 = 켬,, +yujin_p,move_bank,,,0,2,,,최대 축적 턴,이동하지 않은 턴의 이동력 축적,최대 축적 배수(2 = 이동 거리 최대 2배),, +simayi_a,aoe_drain,,,0,1.5,0.6,,공격 배율·피해 → 아군 회복 비율,행동 반경 안 적 전원 피해 → 준 피해만큼 아군 회복,피해 = 공격력 × 배수,준 피해 합 × 비율을 범위 안 다른 아군 각각 회복, +simayi_p,splash_radius_mult,camp:wei:strategist,,0,1.5,,,,책사 광역 범위 배율 · 대상: 위 진영 책사 유닛,광역 반경 ×배율,, +simayi_p,range_mult,,,0,1.5,,,,공격 사거리 배율,사거리 ×배율(1.5 = +50 %),, +simayi_p,drain_aura,,,0,0.3,,,,행동 반경 안 적이 입은 피해만큼 자신 회복,그 피해 × 비율 회복,, +xiahouyuan_a,shot_fan,,,0,5,3,60,발 수·연속 횟수·부채꼴 각(기본값),부채꼴 연속 사격,한 번에 쏘는 화살 수,연속 횟수,부채꼴 전체 각도(도) +xiahouyuan_p,multishot,,,0,3,30,,발 수·부채꼴 각(기본값),일반 사격이 부채꼴 여러 발(멀티샷),화살 수,부채꼴 전체 각도(도), +guojia_a,dmg_pct,range,,2,0.3,,,,주는 피해 증가 · 대상: 시전 순간 행동 반경 안 아군 · 2턴 지속,피해 +비율(0.5 = +50 %),, +guojia_a,grant_reaction,,,0,1,,,,이미 행동한 다른 아군에게 재행동 부여(없으면 다른 아군이 행동 뒤 재행동),인원,, +guojia_p,kill_in_range_reward,,,0,1,0.3,,쿨 −턴·최대 HP 회복,자신의 행동 반경 안에서 적을 처치한 아군 보상,액티브 쿨 −턴,최대 HP × 비율 회복, +jiaxu_a,shot_blast,,,0,1.5,2.2,,독구름 반경(기본값)·공격 배율,투사체가 첫 적 자리에서 폭발 — 반경 안 적 전원 피해,폭발 반경,피해 = 공격력 × 배수, +jiaxu_a,hit_poison,,,0,5,0.02,1,,명중한 적 중독(턴 시작·행동 끝마다 생명력 감소),중독 턴,1회 최대 HP × 비율(중첩 수만큼 곱),확률 +jiaxu_p,hit_poison,,,0,5,0.02,1,턴(기본값 = 독계),명중한 적 중독(턴 시작·행동 끝마다 생명력 감소),중독 턴,1회 최대 HP × 비율(중첩 수만큼 곱),확률 +jiaxu_p,poison_stack,,,0,5,,,,중독 중첩 허용,최대 중첩,, +dianwei_a,dmg_pct,,,0,1.0,,,,주는 피해 증가,피해 +비율(0.5 = +50 %),, +dianwei_a,push_mult,,,0,2,,,,밀치는 거리 배율,밀기 ×배율(2 = 2배),, +dianwei_a,self_bounce_mult,,,0,0,,,,부딪힌 뒤 자신이 튕기는 거리 배율,튕김 ×배율(0 = 튕기지 않음 · 2 = 2배),, +dianwei_p,dmg_taken_pct,,,0,-0.2,,,,받는 피해 증감,받는 피해 비율(−0.3 = 30 % 감소 · +0.15 = 15 % 증가),, +dianwei_p,gimmick_mult,,,0,2,,,지형 오브젝트 밀기 거리·피해 배율,지형 오브젝트(바위·목책 등) 밀기 거리·그것으로 준 피해 배율,×배율,, +xunyu_a,counter_taken_pct,range,,1,-1,,,,받는 반격 피해 증감 · 대상: 시전 순간 행동 반경 안 아군 · 1턴 지속,받는 반격 비율(−1 = 반격 안 받음 · −0.5 = 반감),, +xunyu_a,atk_pct,range,,1,0.3,,,,공격력 증가 · 대상: 시전 순간 행동 반경 안 아군 · 1턴 지속,공격력 +비율(0.2 = +20 %),, +xunyu_p,ally_hit_heal,,,0,0.5,,,,자신의 공격이 아군에 맞으면 피해 대신 회복,회복 = 공격력 × 배수,, +xunyu_p,ally_hit_buff,,,1,0,0.15,,,자신의 공격이 아군에 맞으면 버프(피해 없음) · 1턴 지속,이동 거리 +비율,공격력 +비율, +xiahoumao_a,dmg_taken_pct,,,1,-0.3,,,,받는 피해 증감 · 1턴 지속,받는 피해 비율(−0.3 = 30 % 감소 · +0.15 = 15 % 증가),, +xiahoumao_p,sling_dist_pct,,,0,0.1,,,,이동(슬링) 거리 증가,이동 거리 +비율(1.0 = 2배),, +wanglang_a,shot_pierce,,,0,1.8,1,0,아군 1 = 아군도 피해,직선 관통 사격 — 경로 위 유닛 전원(장애물 무시),피해 = 공격력 × 배수,아군 처리(0 통과 · 1 피해 · 2 회복),밀기 거리(0 = 없음) +wanglang_a,hit_confuse,,,0,1,0.5,1,,명중한 적 혼란(이동 거리 감소),혼란 턴,이동 거리 배율(0.5 = 반감),확률 +wanglang_p,range_mult,,,0,1.1,,,,공격 사거리 배율,사거리 ×배율(1.5 = +50 %),, +xiahouen_a,first_hit_bonus,,,0,0.5,,,,이 행동에서 처음 맞힌 대상에게 추가 피해,추가 피해 = 공격력 × 비율,, +xiahouen_p,hp_max_pct,,,0,0.1,,,,최대 생명력 증가,최대 HP +비율,, +sunjian_a,atk_pct,,,0,0.5,,,,공격력 증가,공격력 +비율(0.2 = +20 %),, +sunjian_a,pierce_through,,,0,1,,,1 = 아군도 통과,보병·창병도 기병처럼 적을 관통하며 경로 위 적 전원 피해,1 = 아군도 통과 · 0 = 아군 앞 정지,, +sunjian_a,hit_bleed,,,0,2,0.5,1,,명중한 적 출혈(움직인 거리에 비례해 생명력 감소),출혈 턴,슬링 최대 거리만큼 움직일 때 최대 HP × 비율,확률 +sunjian_a,invuln,,,1,1,,,행동 끝 뒤 1턴 피해 무효,무적 — 모든 피해 무효(행동 중이 아닌 동안) · 1턴 지속,1 = 켬,, +sunjian_p,atk_pct,camp:wu,,0,0.2,,,,공격력 증가 · 대상: 오 진영 유닛,공격력 +비율(0.2 = +20 %),, +sunjian_p,kill_reaction,,,0,1,,,턴당,적 처치 시 재행동(턴당 1회),1 = 켬,, +zhouyu_a,shot_blast,,,0,2.5,3.5,,운석 반경(기본값)·공격 배율,투사체가 첫 적 자리에서 폭발 — 반경 안 적 전원 피해,폭발 반경,피해 = 공격력 × 배수, +zhouyu_a,hit_burn,,,0,3,0.05,1,,명중한 적 화상(불태우기 · 턴 시작마다 생명력 감소),화상 턴,1회 최대 HP × 비율,확률 +zhouyu_a,fire_zone,,,0,1.5,3,1.0,불꽃 반경·턴·닿을 때 공격 배율,폭발 자리에 불꽃 지대 — 닿는 적 피해 + 화상,지대 반경,유지 턴,닿을 때 피해 = 공격력 × 배수 +zhouyu_p,skill_hp_cost,,,0,0.05,,,,액티브 스킬 시전 시 생명력 소모,최대 HP × 비율 소모(쓰러지지는 않음),, +zhouyu_p,skill_power_low_hp,,,0,0.1,0.1,,HP p2 잃을 때마다 스킬 위력 +p1,생명력이 낮을수록 스킬 위력 증가,단계당 스킬 위력 +비율,단계 = 잃은 HP 비율(0.1 = 10 %마다), +zhouyu_p,melee_penalty_off,,,0,1,,,,궁병·책사 근접 전투 패널티 없음,1 = 켬,, +zhouyu_p,range_mult,,armed,0,1.5,,,,공격 사거리 배율 · 조건: 액티브 사용 중일 때,사거리 ×배율(1.5 = +50 %),, +huanggai_a,dmg_taken_pct,,,1,-0.3,,,,받는 피해 증감 · 1턴 지속,받는 피해 비율(−0.3 = 30 % 감소 · +0.15 = 15 % 증가),, +huanggai_a,damage_to_atk,,,1,1,,,,이번 턴 받은 피해를 다음 턴 공격력에 더함 · 1턴 지속,1 = 켬,, +huanggai_a,heal_on_kill,,,1,1.0,,,,적 처치 시 생명력 회복 · 1턴 지속,최대 HP × 비율 회복,, +huanggai_p,dmg_taken_pct,,,0,-0.15,,,,받는 피해 증감,받는 피해 비율(−0.3 = 30 % 감소 · +0.15 = 15 % 증가),, +huanggai_p,low_hp_taken,,,0,0.002,0.01,,잃은 HP 1 %당 받는 피해·반격 피해,생명력이 낮을수록 받는 피해·주는 반격 증가,잃은 HP 1 %당 받는 피해 +비율,잃은 HP 1 %당 반격 피해 +비율, +ganning_a,crit_always,,,0,1,,,,무조건 치명타,1 = 켬,, +ganning_a,shot_ricochet,,,0,3,0.5,,도탄 수·공격 배율(아군 포함),명중 대상 주위 가까운 유닛에게 도탄(아군 포함),도탄 대상 수,도탄 피해 = 공격력 × 배수, +ganning_p,counter_taken_pct,,,0,-1,,,,받는 반격 피해 증감,받는 반격 비율(−1 = 반격 안 받음 · −0.5 = 반감),, +ganning_p,ally_hit_warp,,,0,1,,,,자신의 공격이 아군에 맞으면 그 아군 곁으로 이동 + 남은 행동 범위로 재공격,1 = 켬,, +chengpu_a,repeat_action,,,0,1,,,,같은 거리·방향으로 한 번 더 공격,1 = 켬,, +chengpu_p,push_mult,,,0,1.5,,,,밀치는 거리 배율,밀기 ×배율(2 = 2배),, +chengpu_p,dmg_pct,,vs_type:cavalry,0,0.5,,,,주는 피해 증가 · 조건: 상대가 기병일 때,피해 +비율(0.5 = +50 %),, +sunce_a,sling_dist_pct,,,0,1.0,,,,이동(슬링) 거리 증가,이동 거리 +비율(1.0 = 2배),, +sunce_a,pierce_through,,,0,1,,,,보병·창병도 기병처럼 적을 관통하며 경로 위 적 전원 피해,1 = 아군도 통과 · 0 = 아군 앞 정지,, +sunce_a,return_always,,,0,1,,,,공격 후 항상 출발 위치로 복귀,1 = 복귀하며 경로 위 적에게도 피해,, +sunce_a,wall_bounce,,,0,1,,,,전장 가장자리에 닿으면 핀볼처럼 튕겨 남은 거리 소진,1 = 켬,, +sunce_p,sling_dist_pct,camp:wu,,0,0.3,,,,이동(슬링) 거리 증가 · 대상: 오 진영 유닛,이동 거리 +비율(1.0 = 2배),, +sunce_p,counter_taken_pct,,,0,-1,,,,받는 반격 피해 증감,받는 반격 비율(−1 = 반격 안 받음 · −0.5 = 반감),, +sunce_p,return_on_hit,,,0,0,0,,,피해를 입히면 출발 위치로 복귀,1 = 복귀하며 경로 위 적에게도 피해,1 = 복귀 경로의 유닛을 함께 쓸어 감, +sunce_p,dash_dmg_pct,,,0,1.0,,,최대(기본값 = 장비·여포),돌진(슬링) 거리에 비례해 공격력 증가,슬링 최대 거리로 당겼을 때 공격 +비율(최대),, +sunquan_a,shot_blast,,,0,1.5,1.8,,화살비 반경(기본값)·공격 배율,투사체가 첫 적 자리에서 폭발 — 반경 안 적 전원 피해,폭발 반경,피해 = 공격력 × 배수, +sunquan_a,hit_confuse,,,0,1,0.5,1,,명중한 적 혼란(이동 거리 감소),혼란 턴,이동 거리 배율(0.5 = 반감),확률 +sunquan_p,range_mult,camp:wu:archer,,0,1.5,,,,공격 사거리 배율 · 대상: 오 진영 궁병 유닛,사거리 ×배율(1.5 = +50 %),, +sunquan_p,arrow_pass_allies,,,0,1,0,,,화살이 아군을 통과,1 = 켬,1 = 장애물도 통과, +sunquan_p,melee_penalty_off,,,0,1,,,,궁병·책사 근접 전투 패널티 없음,1 = 켬,, +lusu_a,shot_barricade,,,0,3,1.0,,목책 칸 수·밀기,슬링 목표점에 목책 세움 — 그 자리 적 피해·밀침,목책 칸 수,밀기 거리, +lusu_p,atk_per_ally_range,,,0,0.05,,,,행동 반경 안 아군 1기당 공격력 증가,아군 1기당 공격 +비율,, +lusu_p,turn_start_heal_range,,,0,0.1,,,,자기 턴 시작 시 행동 반경 안 아군 회복,최대 HP × 비율 회복,, +lusu_p,death_banner,,,0,5,,,깃발 턴,쓰러진 자리에 깃발 — 패시브 효과 유지,유지 턴,, +lvmeng_a,reaction_on_hit,,,0,1,3,,재행동·최대,이 행동으로 적에게 피해를 주면 재행동(연쇄),피해 시 재행동 수,연쇄 최대 재행동, +lvmeng_a,reaction_on_kill,,,0,1,3,,,이 행동으로 적을 처치하면 재행동(연쇄),처치 시 재행동 수,연쇄 최대 재행동, +lvmeng_p,counter_taken_pct,,,0,-0.3,,,,받는 반격 피해 증감,받는 반격 비율(−1 = 반격 안 받음 · −0.5 = 반감),, +lvmeng_p,reaction_atk_stack,,,0,0.2,,,,이번 턴 재행동할 때마다 공격력 증가(2배씩 · 턴마다 초기화),첫 재행동 공격 +비율(다음은 2배 · 4배),, +luxun_a,shot_firewall,,,0,0.6,3,3.0,장벽 반폭(기본값)·턴·닿을 때 공격 배율,진행 방향 불 장벽(지대) — 닿는 적 피해 + 화상,장벽 반폭,유지 턴,닿을 때 피해 = 공격력 × 배수 +luxun_a,hit_burn,,,0,3,0.05,1,,명중한 적 화상(불태우기 · 턴 시작마다 생명력 감소),화상 턴,1회 최대 HP × 비율,확률 +luxun_p,dmg_pct,camp:wu,vs_status,0,0.5,,,,주는 피해 증가 · 대상: 오 진영 유닛 · 조건: 상대가 상태 이상일 때,피해 +비율(0.5 = +50 %),, +luxun_p,hit_confuse,,,0,1,0.5,1,,명중한 적 혼란(이동 거리 감소),혼란 턴,이동 거리 배율(0.5 = 반감),확률 +sunshangxiang_a,volley,,,0,1.0,1.0,,공격 배율(기본값)·밀기(기본값),사거리 안 적 전원에게 화살 — 피해·밀침(치명타 판정),피해 = 공격력 × 배수,밀기 거리, +sunshangxiang_a,hit_bleed,,crit,0,2,0.5,1,,명중한 적 출혈(움직인 거리에 비례해 생명력 감소) · 조건: 치명타일 때,출혈 턴,슬링 최대 거리만큼 움직일 때 최대 HP × 비율,확률 +sunshangxiang_p,dmg_pct,,vs_male_hero,0,0.5,,,,주는 피해 증가 · 조건: 상대가 남성 영웅일 때,피해 +비율(0.5 = +50 %),, +sunshangxiang_p,melee_penalty_off,,,0,1,,,,궁병·책사 근접 전투 패널티 없음,1 = 켬,, +xiaoqiao_a,heal_atk,range,,0,0.8,,,,회복 · 대상: 시전 순간 행동 반경 안 아군,회복 = 공격력 × 배수,, +xiaoqiao_a,turn_end_heal,,,1,3,0.3,,반경·공격 배율,턴 종료 시 주위 아군 회복 · 1턴 지속,반경,회복 = 공격력 × 배수, +xiaoqiao_p,dmg_taken_pct,,vs_male_hero,0,-0.3,,,,받는 피해 증감 · 조건: 상대가 남성 영웅일 때,받는 피해 비율(−0.3 = 30 % 감소 · +0.15 = 15 % 증가),, +xiaoqiao_p,ally_hit_heal,,,0,0.5,,,,자신의 공격이 아군에 맞으면 피해 대신 회복,회복 = 공격력 × 배수,, +lingtong_a,aoe_strike,,,0,3,1.5,1,,자신 주위 적 전원 공격·밀침,반경,피해 = 공격력 × 배수(0 = 피해 없음),바깥으로 밀기 거리 +lingtong_p,atk_pct,,,0,0.15,,,,공격력 증가,공격력 +비율(0.2 = +20 %),, +lingtong_p,push_mult,,,0,1.3,,,,밀치는 거리 배율,밀기 ×배율(2 = 2배),, +zhoutai_a,shot_pierce,,,0,1.8,1,1.0,검기 · 아군도 피해 · 밀기(기본값),직선 관통 사격 — 경로 위 유닛 전원(장애물 무시),피해 = 공격력 × 배수,아군 처리(0 통과 · 1 피해 · 2 회복),밀기 거리(0 = 없음) +zhoutai_p,hit_confuse,,crit,0,1,0.5,1,,명중한 적 혼란(이동 거리 감소) · 조건: 치명타일 때,혼란 턴,이동 거리 배율(0.5 = 반감),확률 +zhoutai_p,crit_always,,vs_type:spear,0,1,,,,무조건 치명타 · 조건: 상대가 창병일 때,1 = 켬,, +zhugejin_a,shot_whirl,,,0,1.0,3.0,0,회오리 반폭(기본값)·밀기(기본값)·공격 배율(피해 없음),회오리 — 직선 위 적을 진행 방향으로 크게 밀침,회오리 반폭,밀기 거리,피해 = 공격력 × 배수(0 = 없음) +zhugejin_a,hit_confuse,,,0,1,0.5,1,,명중한 적 혼란(이동 거리 감소),혼란 턴,이동 거리 배율(0.5 = 반감),확률 +zhugejin_p,splash_ally_ratio,,,0,0,,,,자신의 광역이 아군에게 주는 피해 비율,아군 피해 비율(0 = 아군 피해 없음),, +dongxi_a,atk_ratio,,,0,1.5,,,,이 공격의 피해 = 공격력의 배수,공격력 배수(1.8 = 180 %),, +dongxi_p,atk_pct,,,0,0.1,,,,공격력 증가,공격력 +비율(0.2 = +20 %),, +lvbu_a,invuln,,,1,1,,,,무적 — 모든 피해 무효(행동 중이 아닌 동안) · 1턴 지속,1 = 켬,, +lvbu_a,counter_dmg_pct,,,1,1.0,,,,주는 반격 피해 증가 · 1턴 지속,반격 피해 +비율(0.5 = +50 % · 1.0 = 2배),, +lvbu_a,sling_dist_pct,,,1,0.5,,,,이동(슬링) 거리 증가 · 1턴 지속,이동 거리 +비율(1.0 = 2배),, +lvbu_a,return_always,,,1,0,,,,공격 후 항상 출발 위치로 복귀 · 1턴 지속,1 = 복귀하며 경로 위 적에게도 피해,, +lvbu_p,atk_pct,camp:other,,0,0.2,,,,공격력 증가 · 대상: 군웅 진영 유닛,공격력 +비율(0.2 = +20 %),, +lvbu_p,counter_taken_pct,,,0,-1,,,,받는 반격 피해 증감,받는 반격 비율(−1 = 반격 안 받음 · −0.5 = 반감),, +lvbu_p,matchup_floor,,,0,1.0,,,,상성 무시(불리한 상성만 무시 · 유리 상성은 유지),상성 배율 하한(1.0 = 불리 상성 없음),, +lvbu_p,dash_dmg_pct,,,0,1.0,,,,돌진(슬링) 거리에 비례해 공격력 증가,슬링 최대 거리로 당겼을 때 공격 +비율(최대),, +zhangjiao_a,invuln,,,1,1,,,,무적 — 모든 피해 무효(행동 중이 아닌 동안) · 1턴 지속,1 = 켬,, +zhangjiao_p,dmg_taken_from_archer_pct,,,0,-0.3,,,,적 궁병에게 받는 피해 증감,받는 피해 비율(−0.3 = 30 % 감소),, +zhangjiao_p,hit_poison,,,0,5,0.02,0.5,턴(기본값)·확률,명중한 적 중독(턴 시작·행동 끝마다 생명력 감소),중독 턴,1회 최대 HP × 비율(중첩 수만큼 곱),확률 +huaxiong_a,dmg_pct,,,0,1.0,,,,주는 피해 증가,피해 +비율(0.5 = +50 %),, +huaxiong_a,counter_taken_pct,,,0,-1,,,,받는 반격 피해 증감,받는 반격 비율(−1 = 반격 안 받음 · −0.5 = 반감),, +huaxiong_p,atk_pct,,turn_le:3,0,0.5,,,,공격력 증가 · 조건: 전투 3턴까지,공격력 +비율(0.2 = +20 %),, +huaxiong_p,counter_taken_pct,,turn_le:3,0,-0.5,,,,받는 반격 피해 증감 · 조건: 전투 3턴까지,받는 반격 비율(−1 = 반격 안 받음 · −0.5 = 반감),, +yanliang_a,pierce_through,,,0,0,,,0 = 아군에 막힘,보병·창병도 기병처럼 적을 관통하며 경로 위 적 전원 피해,1 = 아군도 통과 · 0 = 아군 앞 정지,, +yanliang_a,hit_bleed,,,0,1,0.5,1,,명중한 적 출혈(움직인 거리에 비례해 생명력 감소),출혈 턴,슬링 최대 거리만큼 움직일 때 최대 HP × 비율,확률 +yanliang_p,counter_dmg_pct,,,0,0.5,,,,주는 반격 피해 증가,반격 피해 +비율(0.5 = +50 % · 1.0 = 2배),, +yanliang_p,counter_lifesteal,,,0,0.3,,,,반격 피해만큼 회복,반격 피해 × 비율 회복,, +wenchou_a,dmg_pct,,,0,0.5,,,,주는 피해 증가,피해 +비율(0.5 = +50 %),, +wenchou_a,return_always,,,0,0,,,,공격 후 항상 출발 위치로 복귀,1 = 복귀하며 경로 위 적에게도 피해,, +wenchou_p,heal_on_kill,,,0,0.3,,,,적 처치 시 생명력 회복,최대 HP × 비율 회복,, +gongsunzan_a,sling_dist_pct,,,0,0.5,,,,이동(슬링) 거리 증가,이동 거리 +비율(1.0 = 2배),, +gongsunzan_a,lifesteal,,,0,0.5,,,,입힌 피해만큼 회복(흡혈),입힌 피해 × 비율 회복,, +gongsunzan_a,return_always,,,0,0,,,,공격 후 항상 출발 위치로 복귀,1 = 복귀하며 경로 위 적에게도 피해,, +gongsunzan_p,dmg_pct,,vs_type:archer,0,1.0,,,,주는 피해 증가 · 조건: 상대가 궁병일 때,피해 +비율(0.5 = +50 %),, +gongsunzan_p,heal_on_kill,,,0,0.3,,,,적 처치 시 생명력 회복,최대 HP × 비율 회복,, +dongzhuo_a,rampage,,,0,3,1.5,,돌진 초·밀치기 거리(기본값),일정 시간 직진 난동 — 닿는 유닛 전원 밀침(적 = 피해) · 가장자리 튕김 · 자신은 튕기지 않음,돌진 시간(초 · 이동 속도 × 초 = 거리),옆으로 밀치는 거리, +dongzhuo_p,dmg_taken_pct,camp:other,,0,-0.1,,,,받는 피해 증감 · 대상: 군웅 진영 유닛,받는 피해 비율(−0.3 = 30 % 감소 · +0.15 = 15 % 증가),, +dongzhuo_p,lifesteal,,,0,0.3,0.05,,p2 = 아군 1기 줄 때마다 +,입힌 피해만큼 회복(흡혈),입힌 피해 × 비율 회복,아군 1기 줄 때마다 비율 +, +lijue_a,splash_heal_lowest,,,0,0.5,2,,광역 피해 비율·인접 반경(기본값),광역으로 준 피해만큼 인접한 가장 약한 아군 회복,광역 피해 × 비율 회복,인접 반경, +lijue_p,action_end_sacrifice,,,0,0.1,0.5,2,HP 소모·공격 %·인접 반경(기본값),행동 끝 = 생명력 소모 후 인접 아군 공격력 강화(대상 없으면 발동 안 함),HP 소모 비율(HP 10 % 이하면 소모 없이),인접 아군 공격 +비율(1턴),인접 반경 +yuanshao_a,sling_dist_pct,range,,1,0.5,,,,이동(슬링) 거리 증가 · 대상: 시전 순간 행동 반경 안 아군 · 1턴 지속,이동 거리 +비율(1.0 = 2배),, +yuanshao_a,crit_dmg_pct,range,,1,0.5,,,,치명타 피해량 증가 · 대상: 시전 순간 행동 반경 안 아군 · 1턴 지속,치명타 피해 +비율(0.5 = +50 %),, +yuanshao_a,crit_chance,range,,1,0.2,,,,치명타 확률 증가 · 대상: 시전 순간 행동 반경 안 아군 · 1턴 지속,확률 +(0.2 = +20 %p),, +yuanshao_p,hp_max_pct,camp:other,,0,0.2,,,,최대 생명력 증가 · 대상: 군웅 진영 유닛,최대 HP +비율,, +yuanshao_p,hit_bleed,,crit,0,2,0.5,1,,명중한 적 출혈(움직인 거리에 비례해 생명력 감소) · 조건: 치명타일 때,출혈 턴,슬링 최대 거리만큼 움직일 때 최대 HP × 비율,확률 +yuanshao_p,hit_push,,crit,0,0.3,,,자신 슬링 최대 거리 × p1,명중한 적 밀쳐냄 · 조건: 치명타일 때,밀기 = 자신 슬링 최대 거리 × 비율,, +yuanshu_a,heal_self_pct,,,0,0.6,,,,자신 생명력 회복,최대 HP × 비율,, +yuanshu_a,dmg_taken_pct,,,1,-0.3,,,,받는 피해 증감 · 1턴 지속,받는 피해 비율(−0.3 = 30 % 감소 · +0.15 = 15 % 증가),, +yuanshu_p,dmg_taken_pct,,,0,0.15,,,,받는 피해 증감,받는 피해 비율(−0.3 = 30 % 감소 · +0.15 = 15 % 증가),, +yuanshu_p,atk_pct,radius:3,,0,0.3,,,,공격력 증가 · 대상: 자신 주위 3 안 아군,공격력 +비율(0.2 = +20 %),, +yuanshu_p,on_attacked_confuse,,,0,1,0.5,0.5,,자신을 공격한 대상 혼란(확률),혼란 턴,혼란 이동 배율(0.5 = 반감),확률 +diaochan_a,charm_pull,,,0,1,1,,행동 불가 턴·남성만,행동 반경 안 적을 행동 불가 + 자신 쪽으로 끌어옴,행동 불가 턴,1 = 남성만, +diaochan_p,atk_pct,camp_male:other,,0,0.25,,,,공격력 증가 · 대상: 군웅 진영 남성 유닛,공격력 +비율(0.2 = +20 %),, +diaochan_p,multi_male_stun,,,0,2,0.5,1,남성 수·확률·턴,한 행동으로 남성 유닛 여럿을 맞히면 확률 기절,최소 남성 수,기절 확률,기절 턴 +diaochan_p,dmg_taken_pct,,vs_male,0,-0.3,,,,받는 피해 증감 · 조건: 상대가 남성일 때,받는 피해 비율(−0.3 = 30 % 감소 · +0.15 = 15 % 증가),, +zhurong_a,shot_as_archer,,,0,1,,,,근접 병종이 궁병처럼 원거리 사격,1 = 켬,, +zhurong_a,atk_ratio,,,0,2.2,,,,이 공격의 피해 = 공격력의 배수,공격력 배수(1.8 = 180 %),, +zhurong_a,hit_poison,,,0,5,0.02,1,,명중한 적 중독(턴 시작·행동 끝마다 생명력 감소),중독 턴,1회 최대 HP × 비율(중첩 수만큼 곱),확률 +zhurong_p,lifesteal,,,0,0.2,,,,입힌 피해만큼 회복(흡혈),입힌 피해 × 비율 회복,, +zhurong_p,counter_dmg_pct,,,0,0.5,,,,주는 반격 피해 증가,반격 피해 +비율(0.5 = +50 % · 1.0 = 2배),, +zhurong_p,terrain_immune,,,0,1,,,,지형(늪·고지) 이동 감소 무시,1 = 켬,, +zhurong_p,atk_pct,,vs_female_hero,0,0.5,,,,공격력 증가 · 조건: 상대가 여성 영웅일 때,공격력 +비율(0.2 = +20 %),, +menghuo_a,counter_taken_pct,,,0,-1,,,,받는 반격 피해 증감,받는 반격 비율(−1 = 반격 안 받음 · −0.5 = 반감),, +menghuo_a,self_bounce_mult,,,0,0,,,,부딪힌 뒤 자신이 튕기는 거리 배율,튕김 ×배율(0 = 튕기지 않음 · 2 = 2배),, +menghuo_a,reaction_on_terrain,,,0,1,,,,지형물에 닿거나 이동 감소 지형을 지나면 재행동,재행동 횟수,, +menghuo_p,lifesteal,,,0,0.2,,,,입힌 피해만큼 회복(흡혈),입힌 피해 × 비율 회복,, +menghuo_p,revive,,,0,0.1,7,0.6,HP 비율·횟수·맞기 전 HP 조건,쓰러지면 부활,부활 HP 비율(1.0 = 100 %),전투당 횟수,맞기 전 HP 가 이 비율 이상일 때만(0 = 조건 없음) +gaoshun_a,hit_stun,,,0,1,1,1,p2 1 = 밀린 아군·지형 피해도,명중한 적 기절(다음 자기 턴 행동 불가),기절 턴,1 = 밀린 아군·지형 오브젝트로 준 피해에도,확률(빈칸 = 100 %) +gaoshun_p,push_continue,,,0,1,,,,아군·지형 오브젝트를 밀친 뒤 남은 이동 거리로 재행동,1 = 켬,, +zhanglu_a,shot_single,,,0,1,,,,책사 사격이 광역 없는 직선 투사체,1 = 켬,, +zhanglu_a,atk_ratio,,,0,2.5,,,,이 공격의 피해 = 공격력의 배수,공격력 배수(1.8 = 180 %),, +zhanglu_a,hit_poison,,,0,5,0.02,1,,명중한 적 중독(턴 시작·행동 끝마다 생명력 감소),중독 턴,1회 최대 HP × 비율(중첩 수만큼 곱),확률 +zhanglu_a,poison_zone,,,0,1.0,1,,반경(기본값)·턴,명중 자리에 독 지대 — 닿는 적 중독,지대 반경,유지 턴, +zhanglu_p,zombie,,,0,1,,,되살린 HP,처치한 적이 같은 편 망령으로 되살아나 1번 공격(조작 불가 · 다음 턴 소멸),망령 HP,, +yanbaihu_a,atk_pct,,,0,0.3,,,,공격력 증가,공격력 +비율(0.2 = +20 %),, +yanbaihu_a,crit_chance,,,0,0.15,,,,치명타 확률 증가,확률 +(0.2 = +20 %p),, +yanbaihu_p,hp_max_pct,,,0,0.1,,,,최대 생명력 증가,최대 HP +비율,, +hanxuan_a,counter_taken_pct,,,0,-1,,,,받는 반격 피해 증감,받는 반격 비율(−1 = 반격 안 받음 · −0.5 = 반감),, +hanxuan_a,push_mult,,,0,2,,,,밀치는 거리 배율,밀기 ×배율(2 = 2배),, +hanxuan_p,counter_taken_pct,,,0,-0.2,,,,받는 반격 피해 증감,받는 반격 비율(−1 = 반격 안 받음 · −0.5 = 반감),, +jinxuan_a,crit_chance,,,0,0.3,,,,치명타 확률 증가,확률 +(0.2 = +20 %p),, +jinxuan_a,crit_dmg_pct,,,0,0.5,,,,치명타 피해량 증가,치명타 피해 +비율(0.5 = +50 %),, +jinxuan_p,dmg_taken_from_archer_pct,,,0,-0.15,,,,적 궁병에게 받는 피해 증감,받는 피해 비율(−0.3 = 30 % 감소),, +jinxuan_p,dmg_pct,,vs_type:archer,0,0.15,,,,주는 피해 증가 · 조건: 상대가 궁병일 때,피해 +비율(0.5 = +50 %),, +zhaofan_a,shot_pierce,,,0,1.6,2,0,아군 2 = 회복(ally_hit_heal),직선 관통 사격 — 경로 위 유닛 전원(장애물 무시),피해 = 공격력 × 배수,아군 처리(0 통과 · 1 피해 · 2 회복),밀기 거리(0 = 없음) +zhaofan_a,ally_hit_heal,,,0,0.6,,,,자신의 공격이 아군에 맞으면 피해 대신 회복,회복 = 공격력 × 배수,, +zhaofan_p,negate_hits,,,0,1,,,,받는 피해 무효화(전투 시작부터),무효화 횟수,, +zhangren_a,shot_pierce,,,0,1.5,0,0,,직선 관통 사격 — 경로 위 유닛 전원(장애물 무시),피해 = 공격력 × 배수,아군 처리(0 통과 · 1 피해 · 2 회복),밀기 거리(0 = 없음) +zhangren_p,range_mult,,,0,1.3,,,,공격 사거리 배율,사거리 ×배율(1.5 = +50 %),, +chunyuqiong_a,return_on_hit,,,0,0,0,,,피해를 입히면 출발 위치로 복귀,1 = 복귀하며 경로 위 적에게도 피해,1 = 복귀 경로의 유닛을 함께 쓸어 감, +chunyuqiong_p,atk_missing_hp,,,0,1.0,,,,잃은 생명력 비율만큼 공격력 증가,잃은 HP 비율 × 계수(1.0 = HP 1 % 감소당 공격 1 %),, diff --git a/Assets/Data/OutGame/skill_effects.csv.meta b/Assets/Data/OutGame/skill_effects.csv.meta new file mode 100644 index 00000000..55544b1b --- /dev/null +++ b/Assets/Data/OutGame/skill_effects.csv.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3e88ae866fb008444905b1951fa85a30 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Data/OutGame/skills.csv b/Assets/Data/OutGame/skills.csv index 1e71aa5f..df5e0c64 100644 --- a/Assets/Data/OutGame/skills.csv +++ b/Assets/Data/OutGame/skills.csv @@ -1,145 +1,145 @@ id,hero_id,kind,name_ko,hanja,trigger,effect_key,p1,p2,p3,cooldown,lv_max,lv_scale,desc_ko,needs_impl -liubei_a,liubei,active,인덕,仁德,next_action,extra_move_count;self_bounce_mult,2,0,,3,5,0.1,다음 행동: 알까기 명중 시 자신은 튕기지 않고(자기 튕김 0) 추가 이동이 2회 [p1 추가 이동 횟수 · p2 자기 튕김 배율],0 -liubei_p,liubei,passive,도원의 맹약,,always,pushed_ally_attack_pct,0.5,,,0,5,0.1,아군이 밀려서 하는 공격 피해 +50 %,0 -guanyu_a,guanyu,active,청룡언월,靑龍偃月,next_action,overwatch_radius_mult;overwatch_unlimited,2,1,,3,5,0.1,다음 행동: 명중 시 견제 반경 ×2 · 그 적 턴 동안 견제 횟수 무제한,0 -guanyu_p,guanyu,passive,무성,武聖,always,counter_ratio,1.0,,,0,5,0.1,반격 피해 0.5 → 1.0 배 [p1 반격 배율(기본 0.5)],0 -zhangfei_a,zhangfei,active,장판 일갈,長坂一喝,instant,aoe_push_fear,0.8,4,,4,5,0.1,즉시 ★: 반경 4 안 적 전원을 바깥으로 0.8 밀고 · 이번 적 턴 그 적들은 교전 반응 없음 [p1 밀기 거리 · p2 반경],1 -zhangfei_p,zhangfei,passive,만인지적,萬人之敵,always,pierce_dmg_mult,1.3,2,,0,5,0.1,관통으로 2기 이상 맞히면 피해 +30 % [p2 최소 관통 수 · 키 근사],0 -zhaoyun_a,zhaoyun,active,단기구주,單騎救主,next_action,pierce_no_counter;pierce_return_on_miss,1,1,,3,5,0.1,다음 행동: 관통 중 반격을 받지 않고 · 빗나가도 출발점 복귀,0 -zhaoyun_p,zhaoyun,passive,상산의 창,,always,pierce_knockback_mult,2,,,0,5,0.1,관통 피해를 준 적 밀기 0.35 → 0.7 [0.35→0.7 = ×2],0 -huangzhong_a,huangzhong,active,백발백중,百發百中,next_action,arrow_pass_allies;arrow_crit_always,1,1,,2,5,0.1,다음 행동: 화살이 아군을 통과(사선)하고 명중 여부와 무관하게 크리,1 -huangzhong_p,huangzhong,passive,노익장,老益壯,always,dmg_pct_hp_below,0.4,0.5,,0,5,0.1,HP 50 % 이하일 때 공 +40 % [p1 공 증가 · p2 HP 임계],0 -zhugeliang_a,zhugeliang,active,동남풍,東南風,instant,sling_dist_pct,0.3,0,1,4,5,0.1,즉시: 이번 턴 아군 전원 슬링 거리 +30 % [p1 비율 · p2 절대 가산 · p3 1=아군 전원],0 -zhugeliang_p,zhugeliang,passive,신산,神算,always,enemy_aggro_add,-1.5,,,0,5,0.1,적 교전 범위 −1.5(적이 더 늦게 움직인다),0 -zhoucang_a,zhoucang,active,청룡도 지기,,next_action,push_mult,2,,,3,5,0.1,다음 행동: 알까기 밀기 ×2,0 -zhoucang_p,zhoucang,passive,산채 체력,,always,hp_max_pct,0.2,,,0,5,0.1,최대 HP +20 %,0 -guanping_a,guanping,active,호위,護衛,instant,guard_share,0.5,1.5,,3,5,0.1,즉시 ★: 이번 적 턴 반경 1.5 안 아군이 받는 피해의 50 % 를 관평이 대신 받음 [p1 대신 받는 비율 · p2 반경],1 -guanping_p,guanping,passive,후계,後繼,always,overwatch_radius_add,0.5,,,0,5,0.1,견제 반경 +0.5,0 -xushu_a,xushu,active,팔문 간파,八門看破,next_action,splash_ally_ratio,0,,,3,5,0.1,다음 행동: 광역 피해가 아군에게 0(광역),1 -xushu_p,xushu,passive,단기,單騎,always,range_add,0.5,,,0,5,0.1,사거리 +0.5,0 -taishici_a,taishici,active,신사,神射,next_action,range_mult,1.5,,,3,5,0.1,다음 행동: 사거리 ×1.5,0 -taishici_p,taishici,passive,돌파,突破,always,melee_penalty_off,1,,,0,5,0.1,궁수 근접 패널티 없음(피해 100 % · 피격 100 %),0 -machao_a,machao,active,금마초,錦馬超,next_action,pierce_dmg_mult;pierce_knockback_mult,1.5,2,,3,5,0.1,다음 행동: 관통 피해 +50 % · 관통한 적 밀기 ×2,0 -machao_p,machao,passive,신위,神威,always,pierce_dmg_pct_per_hit,0.08,0.4,,0,5,0.1,관통한 적 1기당 공 +8 %(최대 +40 % · 전투 중 누적) [p1 1기당 · p2 최대],0 -weiyan_a,weiyan,active,자오곡 기책,子午谷奇策,next_action,matchup_floor;dmg_pct,1.0,0.3,,3,5,0.1,다음 행동: 상성 불리(기병 0.7)를 1.0 으로 · 피해 +30 %,0 -weiyan_p,weiyan,passive,반골,反骨,always,dmg_taken_pct,-0.2,0.5,,0,5,0.1,HP 50 % 이하일 때 받는 피해 −20 % [p2 HP 임계(이하일 때)],0 -pangtong_a,pangtong,active,봉추의 계,鳳雛之計,instant,aoe_push_fear,0.5,3,,4,5,0.1,즉시 ★: 반경 3 안 적 전원을 0.5 밀고 이번 적 턴 교전 반응 없음 [p1 밀기 거리 · p2 반경],1 -pangtong_p,pangtong,passive,연환,連環,always,splash_ratio,0.8,,,0,5,0.1,광역 피해 비율 0.6 → 0.8 [광역 비율 절대값(기본 0.6)],0 -jiangwei_a,jiangwei,active,구벌중원,九伐中原,next_action,push_mult;overwatch_count,1.5,2,,3,5,0.1,다음 행동: 밀며 전진 거리 ×1.5 · 명중 시 견제 횟수 2회 [밀며 전진 거리 배율 · 키 근사],0 -jiangwei_p,jiangwei,passive,기린아,麒麟兒,always,overwatch_dmg_mult,1.3,,,0,5,0.1,견제 피해 ×1.3,0 -liufeng_a,liufeng,active,양자의 분투,養子奮鬪,next_action,pierce_dmg_mult,1.4,,,3,5,0.1,다음 행동: 관통 피해 +40 %,0 -liufeng_p,liufeng,passive,상용 수비,上庸守備,always,dmg_taken_pct,-0.1,,,0,5,0.1,받는 피해 −10 %,0 -jianyong_a,jianyong,active,유세,遊說,instant,aoe_push_fear,0,3,,4,5,0.1,즉시 ★: 반경 3 안 적 전원 이번 적 턴 교전 반응 없음 [p1 밀기 0 · p2 반경 · 교전 반응 정지만],1 -jianyong_p,jianyong,passive,말재주,,always,cooldown_add,-1,0,,0,5,0.1,자기 액티브 쿨 −1(최소 1) ★ [p2 0=자기만 · 최소 1 · 신규 키],1 -sunqian_a,sunqian,active,사신,使臣,instant,sling_dist_pct,0.2,0,1,4,5,0.1,즉시: 이번 턴 아군 전원 슬링 거리 +20 % [p3 1=아군 전원],0 -sunqian_p,sunqian,passive,연락,連絡,always,team_range_add,0.3,,,0,5,0.1,아군 궁병·책사 사거리 +0.3 [대상 = 궁병·책사],0 -mizhu_a,mizhu,active,거부의 화살,巨富之矢,next_action,arrow_crit_always,1,,,3,5,0.1,다음 행동: 명중 여부와 무관하게 크리,0 -mizhu_p,mizhu,passive,재력,財力,always,hp_max_pct,0.15,,,0,5,0.1,최대 HP +15 %,0 -caocao_a,caocao,active,난세의 간웅,,instant,team_dmg_taken_pct_turn,-0.3,,,4,5,0.1,즉시: 이번 적 턴 아군 전원 받는 피해 −30 %,0 -caocao_p,caocao,passive,패자,霸者,always,team_atk_pct_full,0.1,5,,0,5,0.1,출전 5인일 때 아군 전원 공 +10 % [p2 출전 인원 조건],0 -xiahoudun_a,xiahoudun,active,발시담정,拔矢啖睛,next_action,pierce_dmg_mult;pierce_no_counter,1.5,1,,3,5,0.1,다음 행동: 관통 피해 +50 % · 반격 받지 않음,0 -xiahoudun_p,xiahoudun,passive,맹장,猛將,always,dmg_pct_hp_below,0.5,0.3,,0,5,0.1,HP 30 % 이하일 때 공 +50 % [p1 공 증가 · p2 HP 임계],0 -xuchu_a,xuchu,active,호치,虎痴,next_action,self_bounce_mult;push_mult;extra_move_attack,0,1.5,1,3,5,0.1,다음 행동: 자기 튕김 0 · 밀기 ×1.5 · 추가 이동에서 공격 가능,0 -xuchu_p,xuchu,passive,거구,巨軀,always,knockback_taken_pct,-0.5,,,0,5,0.1,밀리는 거리 −50 %,0 -zhangliao_a,zhangliao,active,위진소요,威震逍遙,next_action,pierce_repeat,1,,,4,5,0.1,다음 행동: 명중 시 복귀 대신 목표점에서 같은 방향으로 한 번 더 관통 [p1 추가 관통 횟수],0 -zhangliao_p,zhangliao,passive,팔백 돌격,,always,pierce_dmg_pct_per_hit,0.05,0.25,,0,5,0.1,관통한 적 1기당 공 +5 %(최대 +25 % · 전투 중 누적) [p1 1기당 · p2 최대],0 -xuhuang_a,xuhuang,active,대부,大斧,next_action,matchup_floor,1.0,,,3,5,0.1,다음 행동: 상성 불리(기병 0.7)를 1.0 으로,0 -xuhuang_p,xuhuang,passive,군율,軍律,always,counter_taken_pct,-0.3,,,0,5,0.1,받는 반격 피해 −30 %,0 -zhanghe_a,zhanghe,active,변진,變陣,instant,teleport,3,,,3,5,0.1,즉시 ★: 반경 3 안 빈 지점으로 이동(행동 소모 없음) [p1 반경],1 -zhanghe_p,zhanghe,passive,교변,巧變,always,overwatch_dmg_mult,1.5,,,0,5,0.1,견제 피해 ×1.5,0 -caoren_a,caoren,active,철벽,鐵壁,instant,dmg_taken_aura,-0.4,2,,3,5,0.1,즉시 ★: 이번 적 턴 반경 2 안 아군 전원 받는 피해 −40 % [p1 받는 피해 · p2 반경],1 -caoren_p,caoren,passive,수성,守城,always,dmg_taken_pct,-0.33,,,0,5,0.1,방 +50 % [방어 키 없음 · 키 근사 = 방 ×1.5 → 받는 피해 ÷1.5 환산(추정)],0 -yujin_a,yujin,active,군령,軍令,next_action,pushed_ally_attack_count,2,,,3,5,0.1,다음 행동: 알까기로 밀린 아군의 공격 2회,0 -yujin_p,yujin,passive,진중,陣中,always,counter_ratio,1.0,1,,0,5,0.1,밀렸을 때 반격 1.0 배 [p2 1=밀렸을 때만 · 키 근사],0 -simayi_a,simayi,active,은인자중,隱忍自重,instant,team_dmg_taken_pct_turn,-0.4,,,4,5,0.1,즉시: 이번 적 턴 아군 전원 받는 피해 −40 %,0 -simayi_p,simayi,passive,응변,應變,always,range_add;splash_radius_mult,0.5,1.2,,0,5,0.1,사거리 +0.5 · 광역 반경 ×1.2,0 -xiahouyuan_a,xiahouyuan,active,신속,神速,next_action,range_mult;arrow_crit_always,1.5,1,,3,5,0.1,다음 행동: 사거리 ×1.5 · 명중 여부와 무관하게 크리,0 -xiahouyuan_p,xiahouyuan,passive,삼일 오백리,三日五百,always,sling_dist_pct,0.2,,,0,5,0.1,슬링 거리 +20 %,0 -guojia_a,guojia,active,십승십패,十勝十敗,instant,team_dmg_pct_turn,0.25,,,4,5,0.1,즉시 ★: 이번 턴 아군 전원 가하는 피해 +25 % [신규 키],1 -guojia_p,guojia,passive,귀재,鬼才,always,cooldown_add,-1,0,,0,5,0.1,자기 액티브 쿨 −1(최소 1) ★ [p1 쿨 가산 · p2 0=자기만 · 최소 1 · 신규 키],1 -jiaxu_a,jiaxu,active,독계,毒計,next_action,splash_ally_ratio;splash_ratio,0,1.0,,3,5,0.1,다음 행동: 광역 아군 피해 0 · 광역 비율 1.0(광역),1 -jiaxu_p,jiaxu,passive,난세의 독사,,always,dmg_vs_general_mult,1.2,1,,0,5,0.1,보스에게 주는 피해 +20 % [p2 1=보스만],0 -dianwei_a,dianwei,active,쌍철극,雙鐵戟,next_action,dmg_pct;self_bounce_mult,0.6,0,,3,5,0.1,다음 행동: 알까기 피해 +60 % · 자기 튕김 0,0 -dianwei_p,dianwei,passive,고지악래,古之惡來,always,hp_max_pct,0.25,,,0,5,0.1,최대 HP +25 %,0 -xunyu_a,xunyu,active,왕좌지재,王佐之才,instant,counter_ratio,1.0,0,1,4,5,0.1,즉시: 이번 턴 아군 전원 반격 배율 1.0 [p1 반격 배율 · p3 1=아군 전원 · 이번 턴],0 -xunyu_p,xunyu,passive,보급,補給,always,hp_max_pct,0.1,1,,0,5,0.1,아군 전원 최대 HP +10 % [p2 1=아군 전원],0 -xiahoumao_a,xiahoumao,active,부마의 허세,駙馬虛勢,instant,dmg_taken_pct,-0.3,,,3,5,0.1,즉시: 이번 적 턴 자기 받는 피해 −30 % [즉시 · 이번 적 턴 · 자기만],0 -xiahoumao_p,xiahoumao,passive,도주 본능,,always,pierce_return_on_miss,1,,,0,5,0.1,관통이 빗나가도 출발점 복귀,0 -wanglang_a,wanglang,active,설전,舌戰,instant,charm,4,1,,5,5,0.1,즉시 ★: 반경 4 안 가장 가까운 적 무장 1기 — 다음 적 턴 행동 불가 [p1 반경 · p2 턴 · 무장 우선 · 신규 키],1 -wanglang_p,wanglang,passive,노신,老臣,always,hp_max_pct,0.1,,,0,5,0.1,최대 HP +10 %,0 -xiahouen_a,xiahouen,active,청강검,靑釭劍,next_action,pierce_dmg_mult,1.4,,,3,5,0.1,다음 행동: 관통 피해 +40 %,0 -xiahouen_p,xiahouen,passive,보검의 광채,,always,sling_dist_pct,0.1,,,0,5,0.1,슬링 거리 +10 %,0 -sunjian_a,sunjian,active,강동의 호랑이,,next_action,dmg_pct;extra_move_attack,0.5,1,,3,5,0.1,다음 행동: 알까기 피해 +50 % · 추가 이동에서 공격 가능,0 -sunjian_p,sunjian,passive,파로,破虜,always,dmg_vs_general_mult,1.2,1,,0,5,0.1,보스에게 주는 피해 +20 % [p2 1=보스만(0=무장 전체) · 키 근사],0 -zhouyu_a,zhouyu,active,화공,火攻,next_action,splash_radius_mult;splash_ratio,2,1.0,,4,5,0.1,다음 행동: 광역 반경 ×2 · 광역 비율 1.0(광역),1 -zhouyu_p,zhouyu,passive,대도독,大都督,always,team_range_add,0.5,,,0,5,0.1,아군 궁병·책사 사거리 +0.5 [대상 = 궁병·책사],0 -huanggai_a,huanggai,active,고육계,苦肉計,instant,hp_cost_atk,0.6,0.3,,3,5,0.1,즉시 ★: 자기 HP 30 % 소모 · 이번 턴 공 +60 % [p1 공 증가 · p2 HP 소모],1 -huanggai_p,huanggai,passive,노장,老將,always,dmg_taken_pct,-0.15,,,0,5,0.1,받는 피해 −15 %,0 -ganning_a,ganning,active,백기습영,百騎襲營,next_action,teleport;arrow_crit_always,0,1,,3,5,0.1,다음 행동: 사거리 밖이어도 목표점까지 이동 후 사격 · 명중 시 크리 [전용 키 없음 · 키 근사 = 이동(p1 0=목표점 · 반경 제한 없음) 후 사격 + 크리],1 -ganning_p,ganning,passive,금범적,錦帆賊,always,counter_taken_pct,-1.0,,,0,5,0.1,근접 반격을 받지 않음 [근접 반격 한정 · 키 근사],0 -chengpu_a,chengpu,active,철척사모,鐵脊蛇矛,next_action,push_mult,1.5,,,3,5,0.1,다음 행동: 밀며 전진 거리 ×1.5 [밀며 전진 거리 배율 · 키 근사],0 -chengpu_p,chengpu,passive,삼대 노신,,always,overwatch_count,2,,,0,5,0.1,견제 횟수 2회,0 -sunce_a,sunce,active,소패왕,小霸王,next_action,pierce_dmg_mult;pierce_return_on_miss,2,1,,4,5,0.1,다음 행동: 관통 피해 ×2 · 빗나가도 출발점 복귀,0 -sunce_p,sunce,passive,강동의 호걸,,always,counter_taken_pct,-0.5,,,0,5,0.1,받는 반격 피해 −50 %,0 -sunquan_a,sunquan,active,벽안의 군주,碧眼兒,instant,team_dmg_taken_pct_turn;enemy_aggro_add,-0.3,-1,,4,5,0.1,즉시: 이번 적 턴 아군 전원 받는 피해 −30 % · 적 교전 범위 −1,0 -sunquan_p,sunquan,passive,강동의 주인,,always,team_atk_pct_full,0.1,5,,0,5,0.1,출전 5인일 때 아군 전원 공 +10 % [p2 출전 인원 조건],0 -lusu_a,lusu,active,단도부회,單刀赴會,instant,dmg_taken_aura,-0.4,3,,3,5,0.1,즉시 ★: 이번 적 턴 반경 3 안 아군 받는 피해 −40 % [p1 받는 피해 · p2 반경 · 대상 아군],1 -lusu_p,lusu,passive,후덕,厚德,always,splash_ally_ratio,0.5,,,0,5,0.1,광역 아군 피해 비율 0.5,0 -lvmeng_a,lvmeng,active,백의도강,白衣渡江,next_action,extra_move_attack;enemy_aggro_off_turn,1,1,,4,5,0.1,다음 행동: 알까기 명중 시 추가 이동에서 공격 가능 · 맞은 적은 이번 적 턴 교전 반응 없음 [p2 대상 = 맞은 적 1기],0 -lvmeng_p,lvmeng,passive,괄목상대,刮目相對,always,dmg_pct,0.3,5,,0,5,0.1,5턴째부터 공 +30 % [p2 발동 턴(이상)],0 -luxun_a,luxun,active,이릉의 화공,夷陵火攻,next_action,splash_radius_mult;splash_ratio,2,0.9,,4,5,0.1,다음 행동: 광역 반경 ×2 · 광역 비율 0.9(광역),1 -luxun_p,luxun,passive,연영,連營,always,splash_ratio,0.7,,,0,5,0.1,광역 피해 비율 0.6 → 0.7 [광역 비율 절대값],0 -sunshangxiang_a,sunshangxiang,active,궁요희,弓腰姬,next_action,arrow_pass_allies;range_add,1,1,,3,5,0.1,다음 행동: 화살이 아군을 통과(사선) · 사거리 +1,1 -sunshangxiang_p,sunshangxiang,passive,효희,梟姬,always,melee_penalty_off,1,,,0,5,0.1,궁수 근접 패널티 없음,0 -xiaoqiao_a,xiaoqiao,active,금슬,琴瑟,instant,cooldown_add,-1,1,,4,5,0.1,즉시 ★: 아군 전원 액티브 쿨 −1 [p1 쿨 가산 · p2 1=아군 전원 · 신규 키],1 -xiaoqiao_p,xiaoqiao,passive,이교,二喬,always,team_range_add,0.5,,,0,5,0.1,아군 책사 사거리 +0.5 [대상 = 책사],0 -lingtong_a,lingtong,active,유수의 복수,濡須,next_action,push_mult;overwatch_radius_add,1.5,1,,3,5,0.1,다음 행동: 밀며 전진 거리 ×1.5 · 명중 시 견제 반경 +1 [밀며 전진 거리 배율 · 키 근사],0 -lingtong_p,lingtong,passive,용장,勇將,always,counter_taken_pct,-0.3,,,0,5,0.1,받는 반격 피해 −30 %,0 -zhoutai_a,zhoutai,active,열두 상처,十二創,instant,guard_share,0.5,2,,3,5,0.1,즉시 ★: 이번 적 턴 반경 2 안 아군이 받는 피해의 50 % 를 주태가 대신 받음 [p1 대신 받는 비율 · p2 반경],1 -zhoutai_p,zhoutai,passive,철벽 호위,,always,dmg_taken_pct,-0.3,0.5,,0,5,0.1,HP 50 % 이하일 때 받는 피해 −30 % [p2 HP 임계(이하일 때)],0 -zhugejin_a,zhugejin,active,형제의 계,兄弟之計,next_action,splash_ally_ratio,0,,,3,5,0.1,다음 행동: 광역 아군 피해 0(광역),1 -zhugejin_p,zhugejin,passive,온후,溫厚,always,dmg_taken_pct,-0.05,,1,0,5,0.1,아군 전원 받는 피해 −5 % [p3 1=아군 전원],0 -dongxi_a,dongxi,active,유수의 창,濡須之槍,next_action,push_mult,1.3,,,3,5,0.1,다음 행동: 밀며 전진 거리 ×1.3 [밀며 전진 거리 배율 · 키 근사],0 -dongxi_p,dongxi,passive,장사,壯士,always,overwatch_dmg_mult,1.2,,,0,5,0.1,견제 피해 ×1.2,0 -lvbu_a,lvbu,active,천하무쌍,天下無雙,next_action,pierce_dmg_mult;pierce_no_counter,2,1,,4,5,0.1,다음 행동: 관통 피해 ×2 · 반격 받지 않음,0 -lvbu_p,lvbu,passive,적토마,赤兎馬,always,sling_dist_pct,0,1.5,0,0,5,0.1,슬링 거리 +1.5 [절대 가산 키 없음 · p2 절대 가산 · 키 근사],0 -zhangjiao_a,zhangjiao,active,태평요술,太平妖術,instant,enemy_aggro_off_turn,1,,,5,5,0.1,즉시 ★: 이번 적 턴 적 전원 교전 반응 없음(안개) [대상 = 적 전원],1 -zhangjiao_p,zhangjiao,passive,창천이사,蒼天已死,always,dmg_taken_from_archer_pct,-0.3,,,0,5,0.1,적 궁병에게 받는 피해 −30 %,0 -huaxiong_a,huaxiong,active,참장,斬將,next_action,dmg_vs_general_mult,2,0,,3,5,0.1,다음 행동: 알까기 대상이 무장(영웅·보스)이면 피해 ×2 [p2 0=무장 전체(영웅·보스)],0 -huaxiong_p,huaxiong,passive,선봉,先鋒,always,dmg_pct_turn1,0.3,,,0,5,0.1,1턴째 공 +30 %,0 -yanliang_a,yanliang,active,백마의 창,,next_action,overwatch_radius_add,1.5,,,3,5,0.1,다음 행동: 명중 시 견제 반경 +1.5,0 -yanliang_p,yanliang,passive,하북 명장,,always,heal_on_overwatch_kill,0.2,,,0,5,0.1,견제로 적을 쓰러뜨리면 HP 20 % 회복,0 -wenchou_a,wenchou,active,연진 돌격,,next_action,pierce_knockback_mult,2,,,3,5,0.1,다음 행동: 관통 밀기 ×2(경로를 비운다),0 -wenchou_p,wenchou,passive,복수,復讐,always,on_ally_death_atk,0.2,,,0,5,0.1,아군 1기 전사마다 공 +20 %(중첩) [중첩],0 -gongsunzan_a,gongsunzan,active,백마의종,白馬義從,next_action,pierce_knockback_mult,2,,,3,5,0.1,다음 행동: 관통으로 맞힌 적 전부 밀기 ×2,0 -gongsunzan_p,gongsunzan,passive,백마장사,,always,dmg_taken_from_archer_pct,-0.3,,,0,5,0.1,적 궁병에게 받는 피해 −30 %,0 -dongzhuo_a,dongzhuo,active,폭정,暴政,instant,hp_cost_atk,0.8,0.2,,4,5,0.1,즉시 ★: 자기 HP 20 % 소모 · 이번 턴 공 +80 % [p1 공 증가 · p2 HP 소모],1 -dongzhuo_p,dongzhuo,passive,비둔,肥鈍,always,hp_max_pct;knockback_taken_pct,0.4,-0.3,,0,5,0.1,최대 HP +40 % · 밀리는 거리 −30 %,0 -lijue_a,lijue,active,장안 약탈,長安掠奪,next_action,pierce_knockback_mult,2,,,3,5,0.1,다음 행동: 관통 밀기 ×2,0 -lijue_p,lijue,passive,서량 잔당,,always,on_ally_death_atk,0.1,,,0,5,0.1,아군 1기 전사마다 공 +10 %(중첩) [중첩],0 -yuanshao_a,yuanshao,active,사세삼공,四世三公,instant,team_dmg_pct_turn,0.2,,,4,5,0.1,즉시 ★: 이번 턴 아군 전원 가하는 피해 +20 % [신규 키],1 -yuanshao_p,yuanshao,passive,명문,名門,always,dmg_taken_pct,-0.1,,5,0,5,0.1,출전 5인일 때 아군 전원 받는 피해 −10 % [p3 출전 인원 조건 · 대상 아군 전원],0 -yuanshu_a,yuanshu,active,꿀물,蜜水,instant,heal_self_pct,0.3,,,4,5,0.1,즉시 ★: 자기 HP 30 % 회복 [신규 키],1 -yuanshu_p,yuanshu,passive,옥새의 주인,,always,dmg_taken_pct,-0.2,2,,0,5,0.1,보스에게 받는 피해 −20 % [p2 2=보스에게서만],0 -diaochan_a,diaochan,active,연환계,連環計,instant,charm,4,1,,4,5,0.1,즉시 ★: 반경 4 안 가장 가까운 적 무장 1기(없으면 병사)를 매혹 — 다음 적 턴 행동 불가 [p1 반경 · p2 턴 · 대상 무장 우선 · 신규 키],1 -diaochan_p,diaochan,passive,폐월,閉月,always,dmg_taken_pct,-0.25,3,,0,5,0.1,근접 유닛에게 받는 피해 −25 % [p2 3=근접 유닛에게서만],0 -zhurong_a,zhurong,active,비도,飛刀,next_action,spear_hit_two;counter_taken_pct,2,-1,,3,5,0.1,다음 행동: 밀며 전진 끝에 닿은 적 2기 동시 피해 · 반격 받지 않음,0 -zhurong_p,zhurong,passive,남만의 여왕,,always,overwatch_radius_add,0.5,,,0,5,0.1,견제 반경 +0.5,0 -menghuo_a,menghuo,active,칠종칠금,七縱七擒,instant,heal_self_pct;dmg_taken_pct,0.3,-0.3,,4,5,0.1,즉시 ★: 자기 HP 30 % 회복 · 이번 턴 받는 피해 −30 % [p2 이번 턴 · 신규 키],1 -menghuo_p,menghuo,passive,남만왕,南蠻王,always,hp_max_pct,0.3,,,0,5,0.1,최대 HP +30 %,0 -gaoshun_a,gaoshun,active,함진영,陷陣營,next_action,push_mult,2,0.3,,3,5,0.1,다음 행동: 밀기 ×2 · 밀린 적이 벽·유닛에 닿으면 +30 % 피해 [p2 벽·유닛 충돌 추가 피해 · 충돌 키 없음],0 -gaoshun_p,gaoshun,passive,무언의 규율,,always,counter_ratio,1.0,,,0,5,0.1,반격 1.0 배,0 -zhanglu_a,zhanglu,active,오두미도,五斗米道,instant,heal_self_pct;dmg_taken_pct,0.25,-0.2,,4,5,0.1,즉시 ★: 자기 HP 25 % 회복 · 이번 턴 받는 피해 −20 % [p2 이번 턴 · 신규 키],1 -zhanglu_p,zhanglu,passive,한중의 교주,,always,splash_ally_ratio,0.5,,,0,5,0.1,광역 아군 피해 비율 0.5,0 -yanbaihu_a,yanbaihu,active,동오의 덕왕,東吳德王,next_action,push_mult,1.5,,,3,5,0.1,다음 행동: 알까기 밀기 ×1.5,0 -yanbaihu_p,yanbaihu,passive,산적 두목,,always,hp_max_pct,0.1,,,0,5,0.1,최대 HP +10 %,0 -hanxuan_a,hanxuan,active,장사 태수,長沙太守,next_action,overwatch_radius_add,0.5,,,3,5,0.1,다음 행동: 명중 시 견제 반경 +0.5,0 -hanxuan_p,hanxuan,passive,의심,疑心,always,counter_taken_pct,-0.2,,,0,5,0.1,받는 반격 피해 −20 %,0 -jinxuan_a,jinxuan,active,무릉의 화살,武陵之矢,next_action,range_add,0.5,,,3,5,0.1,다음 행동: 사거리 +0.5,0 -jinxuan_p,jinxuan,passive,성주,城主,always,dmg_taken_from_archer_pct,-0.15,,,0,5,0.1,적 궁병에게 받는 피해 −15 %,0 -zhaofan_a,zhaofan,active,계양의 계,桂陽之計,next_action,splash_radius_mult,1.3,,,3,5,0.1,다음 행동: 광역 반경 ×1.3(광역),1 -zhaofan_p,zhaofan,passive,처세,處世,always,dmg_taken_pct,-0.1,3,,0,5,0.1,근접 유닛에게 받는 피해 −10 % [p2 3=근접 유닛에게서만],0 -zhangren_a,zhangren,active,낙봉파의 화살,落鳳坡,next_action,dmg_vs_general_mult,1.5,0,,3,5,0.1,다음 행동: 무장(영웅·보스) 상대 피해 ×1.5 [p2 0=무장 전체],0 -zhangren_p,zhangren,passive,충신,忠臣,always,range_add,0.3,,,0,5,0.1,사거리 +0.3,0 -chunyuqiong_a,chunyuqiong,active,오소 돌격,烏巢突擊,next_action,pierce_knockback_mult,1.5,,,3,5,0.1,다음 행동: 관통 밀기 ×1.5,0 -chunyuqiong_p,chunyuqiong,passive,하북 숙장,宿將,always,dmg_pct_hp_below,0.3,0.3,,0,5,0.1,HP 30 % 이하일 때 공 +30 % [p1 공 증가 · p2 HP 임계],0 +liubei_a,liubei,active,이도류 검법,,next_action,extra_action;counter_taken_pct,,,,3,5,0.1,다음 행동이 2번 행동 가능하며 · 1턴 간 적에게 반격 받지 않음,0 +liubei_p,liubei,passive,도원의 맹약,,always,atk_pct;revive,,,,0,5,0.1,모든 촉 진영 유닛 공격력 + 20% 증가하고 · 자신이 사망할 경우 생명력 100% 상태로 1회 부활 (스테이지당 1회),0 +guanyu_a,guanyu,active,청룡언월,靑龍偃月,instant,aoe_strike;hit_bleed,,,,3,5,0.1,즉시: 청룡언월도를 휘둘러 반경 3m 내 모든 적에게 공격력의 250% 피해를 입히며 1m를 밀쳐내고 출혈 효과를 부여,0 +guanyu_p,guanyu,passive,무신,,always,counter_dmg_pct;counter_taken_pct;kill_stack_atk,,,,0,5,0.1,모든 촉 진영의 반격 피해가 50%증가하며 · 자신은 적에게 반격 받지 않으며 · 자신이 적을 처치할 때마다 공격력 10%씩 씩 증가 (최대 100%),0 +zhangfei_a,zhangfei,active,장판 일갈,長坂一喝,instant,aoe_strike;hit_stun,,,,4,5,0.1,즉시: 반경 4m 범위 내 적 전원을 바깥으로 1m 밀쳐내고 피격 된 적을 1턴간 기절,0 +zhangfei_p,zhangfei,passive,만인지적,萬人之敵,always,sling_dist_pct;dash_dmg_pct;double_hit_stun;lifesteal,,,,0,5,0.1,모든 촉 진영의 이동 거리가 30% 증가하고 · 돌진 거리에 비해 피해가 늘어나고(최대 100% · 슬링샷 거리 기준) 2회 피해를 연속 받은 적을 1턴간 기절 시키고 · 기절한 적을 공격할 경우 피해의 50%만큼 생명력 회복,0 +zhaoyun_a,zhaoyun,active,단기구주,單騎救主,next_action,sling_dist_pct;dmg_pct,,,,3,5,0.1,다음 행동의 이동 거리가 2배로 늘어나고 피해량이 50% 증가,0 +zhaoyun_p,zhaoyun,passive,상산의 창,,always,counter_taken_pct;return_on_hit,,,,0,5,0.1,적에게 반격 받지 않으며 · 거리에 상관 없이 이동 경로 상 적에게 피해를 입힌 경우 항상 제자리로 복귀하고 · 복귀할 때에도 피해를 입힐 수 있음,0 +huangzhong_a,huangzhong,active,백발백중,百發百中,next_action,shot_giant;hit_stun,,,,2,5,0.1,다음 행동 시 공격 거리에 무관하게 지정한 방향으로 거대한 화살을 날려 범위 내 첫 명중 된 적에게 공격력의 500%의 피해를 입히고 · 1턴간 기절 시킴,0 +huangzhong_p,huangzhong,passive,노익장,老益壯,always,arrow_pass_allies;crit_chance;crit_dmg_pct,,,,0,5,0.1,모든 공격이 아군과 장애물을 통과해 공격할 수 있으며 · 자신의 치명타 확률이 20% 증가하고 · 치명타 피해량이 50% 증가,0 +zhugeliang_a,zhugeliang,active,칠성 기원,七星祈願,instant,heal_atk;cleanse,,,,4,5,0.1,즉시: 행동 반경 내 모든 아군의 생명력을 공격력의 100%만큼 회복시키고 · 상태 이상 효과를 제거,0 +zhugeliang_p,zhugeliang,passive,와룡의 가호,臥龍加護,always,counter_taken_pct;turn_end_heal_lowest,,,,0,5,0.1,모든 촉 진영 유닛의 반격 피해를 50% 반감시키고 · 턴 종료 시 자신의 반경 3m 범위의 생명력이 가장 낮은 아군의 생명력을 공격력의 50%만큼 회복,0 +zhoucang_a,zhoucang,active,대도 일섬,大刀一閃,next_action,atk_ratio;hit_bleed;push_mult,,,,3,5,0.1,다음 행동은 180%의 피해를 입히며 · 2턴간 출혈 효과를 유발하며 2배 거리를 밀쳐냄,0 +zhoucang_p,zhoucang,passive,혈흔 추격,血痕追擊,always,atk_pct,,,,0,5,0.1,출혈 된 대상을 공격할 경우 공격력 50% 증가,0 +guanping_a,guanping,active,철벽 호위,鐵壁護衛,instant,guard_radius_mult;guard_dr,,,,3,5,0.1,즉시: 2턴간 호위 범위가 2배로 늘어나며 · 호위 시 받는 피해 50% 감소,0 +guanping_p,guanping,passive,호위,護衛,always,counter_dmg_pct;guard,,,,0,5,0.1,반격 피해가 50% 증가하며 · 항상 자신 주위 2m 범위 내 아군의 피해를 대신 받음,0 +xushu_a,xushu,active,단복의 화계,單福火計,next_action,shot_blast;hit_burn,,,,3,5,0.1,다음 행동 시 지정한 방향으로 거대한 화염구를 날려 범위 내 모든 적에게 공격력의 250%의 피해를 입히고 · 3턴간 불태우기 효과 유발 (불태우기 효과 : 매턴 시작 시 생명력의 5% 피해),0 +xushu_p,xushu,passive,팔문 간파,八門看破,always,range_mult;splash_ally_ratio,,,,0,5,0.1,공격 사거리 50% 증가 및 자신의 광역 공격이 아군에게는 피해를 입히지 않음,0 +taishici_a,taishici,active,관통 화살,,next_action,shot_pierce,,,,3,5,0.1,다음 행동: 경로 상 모든 적을 관통하며 공격력의 300%의 피해를 입힘 (장애물 무시),0 +taishici_p,taishici,passive,신궁,神弓,always,melee_penalty_off;atk_pct;crit_dmg_pct,,,,0,5,0.1,근접 유닛과의 전투 시 패널티가 없으며 · 자신의 공격력과 치명타 피해량이 50%씩 증가,0 +machao_a,machao,active,금마 일격,錦馬一擊,next_action,atk_pct;crit_always,,,,3,5,0.1,다음 행동의 공격력이 50% 증가하고 · 무조건 치명타 피해를 입힘,0 +machao_p,machao,passive,복수의 화신,復讐化身,always,lifesteal;dmg_per_hit,,,,0,5,0.1,입힌 피해의 20%만큼 생명력을 회복하고 · 공격으로 명중 된 적 1기당 피해량 25%씩 증가,0 +weiyan_a,weiyan,active,자오곡 기책,子午谷奇策,next_action,swap_on_hit;extra_action,,,,3,5,0.1,다음 행동 시 명중 된 대상과 위치를 자신의 위치와 교환하고 · 한번 더 행동할 수 있음,0 +weiyan_p,weiyan,passive,반골,反骨,always,matchup_floor;atk_missing_hp,,,,0,5,0.1,상성 효과를 무시하며 · HP가 낮을수록 공격력이 증가 (남은 HP %비율 기준 · 최대 100%),0 +pangtong_a,pangtong,active,봉추의 계,鳳雛之計,instant,heal_atk;hot,,,,4,5,0.1,즉시: 행동 반경 내 모든 아군의 생명력을 공격력의 50%만큼 즉시 회복시키고 · 3턴간 매 턴마다 공격력의 10%씩 생명력을 회복,0 +pangtong_p,pangtong,passive,연환 방비,連環防備,always,dmg_taken_ranged_pct;turn_end_cleanse,,,,0,5,0.1,모든 촉 진영 유닛의 원거리 공격 피해를 50% 반감시키고 · 턴 종료 시 자신의 반경 3m 범위의 아군 1기의 상태이상 효과를 제거,0 +jiangwei_a,jiangwei,active,담대여두,膽大如斗,instant,heal_self_pct;guard_radius_mult,,,,3,5,0.1,즉시: 자신의 생명력을 즉시 50% 회복하고 · 반격 피해 범위를 3턴간 50% 확장,0 +jiangwei_p,jiangwei,passive,기린아의 수호,麒麟兒守護,always,counter_dmg_pct;guard_counter,,,,0,5,0.1,자신의 반격 피해가 50% 증가하고 · 자신의 반격 범위 내 아군이 공격 받을 경우 자신이 피해를 대신 받아 반격 함.,0 +liufeng_a,liufeng,active,양자의 분투,養子奮鬪,next_action,atk_ratio;hit_confuse,,,,3,5,0.1,다음 행동은 185%의 피해를 입히며 · 경로상 피해를 입은 적들에게 1턴간 혼란 효과를 유발시킴.,0 +liufeng_p,liufeng,passive,추격 제압,追擊制壓,always,counter_taken_pct;hit_stun,,,,0,5,0.1,혼란 상태의 적을 공격할 경우 반격 피해를 받지 않고 1턴간 기절 시킴,0 +jianyong_a,jianyong,active,유세,遊說,next_action,ally_hit_heal;hit_stun,,,,4,5,0.1,다음 행동으로 공격 대상이 아군일 경우 공격력의 80%만큼 회복시키고 · 공격 대상이 적일 경우 1턴간 기절 시킴,0 +jianyong_p,jianyong,passive,말재주,,always,heal_cleanse;on_attacked_stun,,,,0,5,0.1,자신의 행동으로 회복 된 대상의 상태이상 효과를 제거할 수 있으며 · 자신을 공격한 대상 1기를 1턴 간 기절시킴(발동 시 3턴 후 1회 충전),0 +sunqian_a,sunqian,active,화친,和親,next_action,splash_heal,,,,4,5,0.1,다음 행동: 다음 공격이 명중 된 대상 주위에 범위 내 모든 유닛(적군 포함)의 생명력을 공격력의 60%만큼 회복,0 +sunqian_p,sunqian,passive,향도,嚮導,always,sling_dist_pct,,,,0,5,0.1,자신 주위 3m 반경 내 아군의 행동 반경을 30% 증가,0 +mizhu_a,mizhu,active,행진가,,instant,sling_dist_pct;counter_taken_pct,,,,3,5,0.1,즉시: 1턴 간 행동 범위 내 아군의 이동력이 50% 상승하며 · 받는 반격 피해가 50% 반감 됨,0 +mizhu_p,mizhu,passive,지원,,always,ally_hit_buff,,,,0,5,0.1,자신의 공격 대상이 아군일 경우 1턴간 이동력과 공격력 20% 증가,0 +caocao_a,caocao,active,난세의 간웅,,next_action,pass_allies;aim_push,,,,4,5,0.1,다음 행동은 아군을 통과하며 · 공격에 명중 된 적이 있을 경우 피해를 입힌 후 밀려나지 않고 원하는 방향으로 밀쳐낼 수 있음 (슬링샷 동작을 한번 더 조작해 유닛을 밀칠 수 있음),0 +caocao_p,caocao,passive,패자,霸者,always,atk_pct;matchup_floor;on_ally_death_atk,,,,0,5,0.1,모든 위 진영 유닛 공격력 +20% 증가하고 · 자신은 상성 효과를 무시하며 · 아군이 사망할 때마다 남은 아군의 공격력이 + 10%씩 증가,0 +xiahoudun_a,xiahoudun,active,맹장,猛將,next_action,sling_dist_pct;return_aim,,,,3,5,0.1,다음 행동은 이동 거리가 50% 증가하고 · 행동 거리에 상관 없이 원하는 방향으로 복귀할 수 있음. (슬링샷 조작으로 복귀),0 +xiahoudun_p,xiahoudun,passive,회군 돌파,回軍突破,always,dmg_pct;return_on_hit;matchup_floor,,,,0,5,0.1,모든 위군 진영의 기병 피해가 50% 증가하고 · 공격 후 복귀 경로에 유닛이 있는 경우 복귀 경로로 적을 밀쳐내며 함께 원래 위치로 복귀(아군 포함)하고 · 자신은 상성 효과를 무시,0 +xuchu_a,xuchu,active,호치,虎痴,next_action,self_bounce_mult;hit_stun,,,,3,5,0.1,다음 행동은 자신의 튕겨지는 거리가 2배로 증가하고 · 자신에게 명중 된 모든 적을 1턴간 기절 시킴,0 +xuchu_p,xuchu,passive,거구,巨軀,always,hp_max_pct;knockback_taken_pct;regen_pct,,,,0,5,0.1,최대 생명력이 50% 증가하고 · 적 공격에 밀쳐지지 않으며 · 매턴 생명력이 5%씩 회복,0 +zhangliao_a,zhangliao,active,요래,遼來,next_action,drag_back;hit_confuse,,,,4,5,0.1,다음 행동으로 경로상 명중 된 적이 있을 경우 피해를 입힌 후 자신이 있던 위치로 끌고 온 후 1턴간 혼란 상태에 빠뜨림(혼란 : 이동력 50% 감소),0 +zhangliao_p,zhangliao,passive,위진소요,威震逍遙,always,atk_pct;counter_taken_pct,,,,0,5,0.1,상태 이상에 걸린 적 공격 시 공격력 50% 증가하고 · 반격 피해를 받지 않음,0 +xuhuang_a,xuhuang,active,대부,大斧,instant,axe_aura,,,,3,5,0.1,즉시: 3턴 간 자신 주위를 맴도는 도끼를 생성해 이동 경로상 도끼 범위에 닿거나 자신을 공격한 대상에게 공격력의 80%의 피해를 추가로 입히고 · 도끼가 회전할 때 자신을 공격한 대상을 2배 멀리 튕겨나게 함,0 +xuhuang_p,xuhuang,passive,혈부,血斧,always,hit_bleed,,,,0,5,0.1,공격 대상에게 30% 확률로 3턴간 지속하는 출혈 효과를 부여(출혈 : 이동 거리의 50%에 비례한 생명력 감소)하고 · 대상에게 치명타 발생 시 100% 확률로 출혈 효과를 부여 (액티브 스킬 - 회전 도끼에도 동일한 효과),0 +zhanghe_a,zhanghe,active,변진,變陣,instant,teleport;extra_action,,,,3,5,0.1,즉시: 반경 5m 내 모든 대상을 무시하고 이동 가능(슬링샷 조작으로 원하는 이동 위치 결정)하고 · 이동 후 재행동 가능,0 +zhanghe_p,zhanghe,passive,교변,巧變,always,push_mult;pushed_ally_atk_add;self_pushed_mult,,,,0,5,0.1,아군이나 적을 미는 거리가 2배 증가하고 · 아군을 밀쳐 적을 공격한 경우 자신의 공격력 만큼 아군의 피해가 증가하고 · 자신이 밀쳐져 적을 공격한 경우에는 2배의 피해를 입힘,0 +caoren_a,caoren,active,철벽,鐵壁,instant,dmg_taken_pct;zone_slow,,,,3,5,0.1,즉시: 3턴간 자신의 행동 반경 내 모든 아군의 받는 피해가 30% 감소되며 · 자신의 행동 반경을 지나가는 적들의 이동소모량이 50% 증가,0 +caoren_p,caoren,passive,수성,守城,always,hp_max_pct;heal_on_action_end;unused_move_dr,,,,0,5,0.1,모든 위군 생명력이 20% 증가하고 · 행동을 멈출 때마다 생명력이 20% 회복되며 · 남은 이동거리의 50%만큼 받는 피해를 차감(이동하지 않았을 경우 · 최대 50%의 피해 차감),0 +yujin_a,yujin,active,군령,軍令,next_action,ally_hit_reaction,,,,3,5,0.1,다음 행동으로 행동 종료한 아군이 명중 된 경우 · 재행동 가능(다수 아군 명중 시 모두 재행동 가능),0 +yujin_p,yujin,passive,진중,陣中,always,move_bank,,,,0,5,0.1,이번턴에 이동하지 않았다면 최대 2턴 거리의 이동력을 축적 가능,0 +simayi_a,simayi,active,고평릉의 변,高平陵之變,instant,aoe_drain,,,,4,5,0.1,즉시: 행동 반경 모든 적에게 공격력의 150% 만큼의 피해를 입히고 · 입힌 피해의 60% 만큼 자신을 제외한 범위 내 모든 아군을 치유,0 +simayi_p,simayi,passive,응시낭고,鷹視狼顧,always,splash_radius_mult;range_mult;drain_aura,,,,0,5,0.1,모든 위군 책사의 광역 피해 범위가 50% 증가하고 · 자신의 사거리가 50% 증가하며 · 행동 반경 내 적이 입은 피해의 30% 만큼 자신의 생명력을 회복,0 +xiahouyuan_a,xiahouyuan,active,전광 연사,電光連射,next_action,shot_fan,,,,3,5,0.1,다음 행동 시 제자리에서 방사형(부채꼴)으로 지정한 방향으로 화살을 5발씩 3회 연속 발동,0 +xiahouyuan_p,xiahouyuan,passive,삼연시,三連矢,always,multishot,,,,0,5,0.1,원거리 공격으로 대상을 타겟한 경우 방사형으로 3발의 화살을 동시에 발사해 경로 상 유닛에게 피해를 입힘(멀티샷),0 +guojia_a,guojia,active,십승십패,十勝十敗,instant,dmg_pct;grant_reaction,,,,4,5,0.1,즉시: 행동 범위 내 아군 전원이 입히는 피해량이 2턴간 30% 증가 하고 · 임의의 아군에게 재행동 기회 1회 부여(이미 행동 종료한 아군에게는 재행동 기회 부여),0 +guojia_p,guojia,passive,귀재,鬼才,always,kill_in_range_reward,,,,0,5,0.1,자신의 행동 범위 내 적을 처치한 아군은 스킬 쿨타임이 1초 감소하고 · 최대 생명력의 30%만큼 회복,0 +jiaxu_a,jiaxu,active,독계,毒計,next_action,shot_blast;hit_poison,,,,3,5,0.1,다음 행동 시 지정한 대상 범위에 독구름을 생성해 공격력의 220%의 피해를 입히고 · 5턴간 중독 상태로 만듬 (중독 : 매턴 시작과 행동 종료 시점 마다 최대 생명력의 2%씩 감소),0 +jiaxu_p,jiaxu,passive,난세의 독사,,always,hit_poison;poison_stack,,,,0,5,0.1,자신의 공격에 당한 적은 항상 중독 상태가 되며 · 중독 효과가 최대 5회까지 중첩될 수 있음,0 +dianwei_a,dianwei,active,쌍철극,雙鐵戟,next_action,dmg_pct;push_mult;self_bounce_mult,,,,3,5,0.1,다음 행동 시 입히는 피해량과 밀리는 거리가 100% 증가하며 · 자신은 튕기지 않음,0 +dianwei_p,dianwei,passive,고지악래,古之惡來,always,dmg_taken_pct;gimmick_mult,,,,0,5,0.1,자신의 받는 피해가 20% 감소하며 · 지형 오브젝트를 밀치는 거리와 지형 오브젝트를 이용한 피해량이 2배로 증가,0 +xunyu_a,xunyu,active,왕좌지재,王佐之才,instant,counter_taken_pct;atk_pct,,,,4,5,0.1,즉시: 행동 범위 내 아군이 1턴간 반격 받지 않게 되며 · 공격력이 30% 증가,0 +xunyu_p,xunyu,passive,보급,補給,always,ally_hit_heal;ally_hit_buff,,,,0,5,0.1,자신의 공격 대상이 아군일 경우 · 공격력의 50%만큼 생명력을 회복시키며 · 1턴간 공격력 15% 증가 버프효과 부여,0 +xiahoumao_a,xiahoumao,active,부마의 허세,駙馬虛勢,instant,dmg_taken_pct,,,,3,5,0.1,즉시: 1턴 간 자신이 받는 피해 -30 %,0 +xiahoumao_p,xiahoumao,passive,도주 본능,,always,sling_dist_pct,,,,0,5,0.1,자신의 이동력 +10%,0 +wanglang_a,wanglang,active,설전,舌戰,next_action,shot_pierce;hit_confuse,,,,5,5,0.1,다음 행동은 직선 범위 내 대상에게 180%의 피해를 입히며 · 1턴간 혼란 효과를 유발(아군이 명중할 경우 아군에 피해),0 +wanglang_p,wanglang,passive,노신,老臣,always,range_mult,,,,0,5,0.1,최대 사거리+10%,0 +xiahouen_a,xiahouen,active,청강검 발도,靑釭劍拔刀,next_action,first_hit_bonus,,,,3,5,0.1,다음 행동으로 최초 명중 된 대상에게 공격력의 50% 피해 추가,0 +xiahouen_p,xiahouen,passive,보검의 광채,,always,hp_max_pct,,,,0,5,0.1,최대 생명력 +10%,0 +sunjian_a,sunjian,active,강동의 호랑이,,next_action,atk_pct;pierce_through;hit_bleed;invuln,,,,3,5,0.1,다음 행동 시 공격력이 50% 증가하고 · 모든 유닛을 통과하며 공격할 수 있으며 경로 상 피격 된 모든 적에게 출혈 피해를 입힘 · 행동 종료 후 1턴간 모든 피해를 무효화함(튕겨질 순 있고 · 반격 피해는 입힐 수 있음),0 +sunjian_p,sunjian,passive,파로,破虜,always,atk_pct;kill_reaction,,,,0,5,0.1,모든 오군 진영 공격력 +20% 증가하고 · 자신의 공격으로 적을 처치할 경우 재행동 기회 1회 부여(턴 당 1회),0 +zhouyu_a,zhouyu,active,적벽의 불비,赤壁火雨,next_action,shot_blast;hit_burn;fire_zone,,,,4,5,0.1,다음 행동 시 명중한 대상 주위에 운석을 소환해 넓은 범위에 공격력의 350%의 피해를 입히고 · 피격 된 대상을 불태우기 상태에 빠뜨리며 · 운석이 낙하한 장소에 불꽃을 생성(닿는 적에게 즉시 공격력의 100%의 피해를 입히고 · 3턴간 유지되는 화상 효과를 입힘),0 +zhouyu_p,zhouyu,passive,대도독,大都督,always,skill_hp_cost;skill_power_low_hp;melee_penalty_off;range_mult,,,,0,5,0.1,스킬 시전 시 생명력이 5% 감소하지만 · 생명이 낮아질 때마다 스킬의 위력이 10%씩 증가하며 · 자신은 근접 유닛과의 전투에 패널티를 받지 않고 · 스킬 사거리가 50% 증가,0 +huanggai_a,huanggai,active,고육계,苦肉計,instant,dmg_taken_pct;damage_to_atk;heal_on_kill,,,,3,5,0.1,즉시: 1턴간 자신의 받는 피해가 30% 감소되며 · 자신이 이번 턴에 받은 피해가 다음 턴의 공격력에 추가되며 · 적을 처치한 경우 생명력 100% 회복,0 +huanggai_p,huanggai,passive,노장,老將,always,dmg_taken_pct;low_hp_taken,,,,0,5,0.1,자신의 받는 피해 -15 % · 생명력이 낮아질수록 받는 피해가 0.2%씩 증가하고 반격 피해가 1%씩 증가,0 +ganning_a,ganning,active,금범 도탄,錦帆跳彈,next_action,crit_always;shot_ricochet,,,,3,5,0.1,다음 행동으로 발사 된 화살은 무조건 치명타로 명중되며 · 피격 된 대상과 가장 가까운 3명의 유닛(아군포함)에게 공격력의 50% 만큼의 도탄 된 화살로 피해를 입힘,0 +ganning_p,ganning,passive,백기습영,百騎襲營,always,counter_taken_pct;ally_hit_warp,,,,0,5,0.1,근접 반격을 받지 않으며 · 아군에게 자신의 공격이 명중 될 경우 아군의 위치로 이동할 수 있고(아군 피해 없음) 남은 행동 범위 내 재공격 가능,0 +chengpu_a,chengpu,active,연환격,連環擊,next_action,repeat_action,,,,3,5,0.1,다음 행동 시 직전 행동 거리와 방향으로 동일하게 한번 더 공격 시도,0 +chengpu_p,chengpu,passive,거마창,拒馬槍,always,push_mult;dmg_pct,,,,0,5,0.1,밀치는 거리가 50% 증가하고 · 상대 병종이 기병일 경우 피해량 50% 증가,0 +sunce_a,sunce,active,소패왕,小霸王,next_action,sling_dist_pct;pierce_through;return_always;wall_bounce,,,,4,5,0.1,다음 행동 시 이동 거리가 2배가 되며 지정한 방향으로 모든 유닛을 관통하며 피해를 입히며 · 멈춘 후 원래 자리로 복귀하며 경로상 모든 적에게 피해를 입힘(화면 가장자리에 닿은 경우 핀볼처럼 튕겨지며 이동 거리를 소진),0 +sunce_p,sunce,passive,강동의 호걸,,always,sling_dist_pct;counter_taken_pct;return_on_hit;dash_dmg_pct,,,,0,5,0.1,모든 오군의 이동 거리가 30% 증가하고 · 자신은 적에게 반격 받지 않으며 · 적에게 피해를 입힌 경우 무조건 출발 위치로 복귀할 수 있고 · 돌진 거리에 비례해 공격력 증가,0 +sunquan_a,sunquan,active,화살비,箭雨,next_action,shot_blast;hit_confuse,,,,4,5,0.1,다음 행동 시 타겟이 된 대상 주위에 화살비를 쏟아 공격력의 180%의 피해를 입히고 · 혼란에 빠뜨려 이동 거리를 50% 반감 시킴,0 +sunquan_p,sunquan,passive,강동의 주인,,always,range_mult;arrow_pass_allies;melee_penalty_off,,,,0,5,0.1,모든 오군의 궁수 사거리가 50% 증가하며 · 자신의 공격이 아군 유닛에게 피해를 주지 않고 관통해 적을 공격할 수 있으며 · 자신은 근접 유닛과의 전투에서 패널티를 받지 않음,0 +lusu_a,lusu,active,녹각진,鹿角陣,next_action,shot_barricade,,,,3,5,0.1,다음 행동으로 지정 된 위치(슬링샷 범위)에 목책을(3열) 생성하고 적이 있는 위치에 목책이 설치 된 경우 피해(필드 오브젝트와 동일)를 주며 튕겨낼 수 있음,0 +lusu_p,lusu,passive,동맹의 깃발,同盟之旗,always,atk_per_ally_range;turn_start_heal_range;death_banner,,,,0,5,0.1,자신의 행동 범위 내 아군 1기당 공격력이 5%씩 증가하고 · 매턴 시작 시 행동 범위 내 아군의 생명력을 10% 회복시킬 수 있으며 · 사망할 경우 쓰러진 장소에 5턴간 효과를 유지하는 깃발을 남겨 자신의 패시브 효과를 전장에 남김,0 +lvmeng_a,lvmeng,active,백의도강,白衣渡江,next_action,reaction_on_hit;reaction_on_kill,,,,4,5,0.1,다음 행동으로 적에게 피해를 입힌 경우 · 한번 더 행동할 수 있으며 적을 처치한 경우 한번 더 행동 가능(최대 3회),0 +lvmeng_p,lvmeng,passive,괄목상대,刮目相對,always,counter_taken_pct;reaction_atk_stack,,,,0,5,0.1,반격 피해를 30% 반감하며 · 매턴 재행동이 발생할 때마다 추가 공격력이 20% 부여되며 · 추가 공격력은 재행동 횟수에 비례해 2배씩 증가(20%→40%→80%) (추가 공격력은 턴마다 초기화),0 +luxun_a,luxun,active,이릉의 화공,夷陵火攻,next_action,shot_firewall;hit_burn,,,,4,5,0.1,다음 행동으로 지정한 방향으로 모든 유닛에게 피해를 입히는 불 장벽을 생성해 3턴간 전장에 유지되며 · 닿는 적에게는 즉시 300%의 피해를 입히고 · 3턴간 화상 효과를 입힘,0 +luxun_p,luxun,passive,백면서생,白面書生,always,dmg_pct;hit_confuse,,,,0,5,0.1,모든 오군이 상태이상 효과에 걸린 적 공격 시 피해량이 50% 증가하며 · 자신의 공격에 피격 된 대상은 1턴간 혼란에 빠뜨림 (혼란 : 이동 속도 50% 감소),0 +sunshangxiang_a,sunshangxiang,active,궁요희,弓腰姬,instant,volley;hit_bleed,,,,3,5,0.1,즉시: 행동 범위 내 모든 적에게 화살을 발사해 피해를 입히고 · 밀쳐내며 치명타 발생 시 2턴간 출혈 효과를 유발함,0 +sunshangxiang_p,sunshangxiang,passive,효희,梟姬,always,dmg_pct;melee_penalty_off,,,,0,5,0.1,남성 영웅에 대한 피해량이 50% 증가하고 · 근접 피해 패널티를 받지 않음.(반격 피해에도 피해량 50% 증가 적용),0 +xiaoqiao_a,xiaoqiao,active,춘풍가,春風歌,instant,heal_atk;turn_end_heal,,,,4,5,0.1,즉시: 행동 범위 내 아군의 생명력을 공격력의 80% 만큼 즉시 회복하고 턴 종료 시 자신의 3m 주위에 있는 아군의 생명력을 공격력의 30% 만큼 회복,0 +xiaoqiao_p,xiaoqiao,passive,경국지색,傾國之色,always,dmg_taken_pct;ally_hit_heal,,,,0,5,0.1,남성 영웅에게 받는 피해가 30% 감소하며 · 자신의 공격 대상이 아군인 경우 공격력의 50% 만큼 생명력을 회복,0 +lingtong_a,lingtong,active,선풍 창술,旋風槍術,instant,aoe_strike,,,,3,5,0.1,즉시: 반경 3m 내 모든 적에게 공격력의 150% 피해를 입히고 1m 밀쳐냄,0 +lingtong_p,lingtong,passive,용장,勇將,always,atk_pct;push_mult,,,,0,5,0.1,자신의 공격력 +15% 증가하고 · 밀치는 거리 +30% 증가,0 +zhoutai_a,zhoutai,active,검기 일섬,劍氣一閃,next_action,shot_pierce,,,,3,5,0.1,다음 행동 시 공격 방향으로 검기를 날려 모든 유닛에게 공격력의 180%의 피해를 입히고 · 밀쳐냄,0 +zhoutai_p,zhoutai,passive,파창,破槍,always,hit_confuse;crit_always,,,,0,5,0.1,치명타 피해를 입힌 적에게 혼란 효과를 부여하고 · 창병에게는 항상 치명타로 피해를 입힘,0 +zhugejin_a,zhugejin,active,선풍계,旋風計,next_action,shot_whirl;hit_confuse,,,,3,5,0.1,다음 행동 시 회오리 바람을 소환해 지정한 방향의 적을 크게 밀쳐내고 낙하한 적에게 1턴간 혼란 효과를 부여,0 +zhugejin_p,zhugejin,passive,온후,溫厚,always,splash_ally_ratio,,,,0,5,0.1,자신의 광역 공격이 아군에게는 피해를 입히지 않음,0 +dongxi_a,dongxi,active,유수의 창,濡須之槍,next_action,atk_ratio,,,,3,5,0.1,다음 행동 시 공격력의 150%의 피해를 입힘,0 +dongxi_p,dongxi,passive,장사,壯士,always,atk_pct,,,,0,5,0.1,자신의 공격력 +10%,0 +lvbu_a,lvbu,active,천하무쌍,天下無雙,instant,invuln;counter_dmg_pct;sling_dist_pct;return_always,,,,4,5,0.1,즉시: 1턴간 모든 공격을 무효화(무적)하고 · 2배의 반격 피해를 입히며 · 이동 거리가 1.5배 증가하고 공격 후 공격 전 제자리로 복귀한다.,0 +lvbu_p,lvbu,passive,적토마 질주,,always,atk_pct;counter_taken_pct;matchup_floor;dash_dmg_pct,,,,0,5,0.1,모든 군영 유닛의 공격력 20% 증가하고 자신은 반격 받지 않으며 · 상성을 무시한 공격이 가능하고 이동 거리에 비례해 공격력 증가한다. (최대 100%),0 +zhangjiao_a,zhangjiao,active,태평요술,太平妖術,instant,invuln,,,,5,5,0.1,즉시: 자신의 모습을 안개에 숨겨 1턴 간 피해를 받지 않음,0 +zhangjiao_p,zhangjiao,passive,창천이사,蒼天已死,always,dmg_taken_from_archer_pct;hit_poison,,,,0,5,0.1,궁병에게 받는 피해가 30 % 감소하며 · 자신의 공격에 명중 된 적에게 50% 확률로 중독 효과를 유발,0 +huaxiong_a,huaxiong,active,참장,斬將,next_action,dmg_pct;counter_taken_pct,,,,3,5,0.1,다음 행동의 피해량이 100% 증가하고 · 자신은 반격 피해를 받지 않는다.,0 +huaxiong_p,huaxiong,passive,선봉,先鋒,always,atk_pct;counter_taken_pct,,,,0,5,0.1,전투 시작 후 3턴 간 자신의 공격력 50% 증가하고 · 반격 피해가 50% 반감,0 +yanliang_a,yanliang,active,백마의 창,,next_action,pierce_through;hit_bleed,,,,3,5,0.1,다음 행동은 적을 관통하며 경로상 모든 적에게 피해를 입히며 피해를 입힌 적에게 1턴간 출혈 효과 부여,0 +yanliang_p,yanliang,passive,하북 명장,,always,counter_dmg_pct;counter_lifesteal,,,,0,5,0.1,반격 피해가 50% 증가하며 · 반격 피해의 30%만큼 생명력 회복,0 +wenchou_a,wenchou,active,연진 돌격,,next_action,dmg_pct;return_always,,,,3,5,0.1,다음 행동의 피해량이 50% 증가하고 · 공격 후 제자리로 복귀한다.,0 +wenchou_p,wenchou,passive,복수,復讐,always,heal_on_kill,,,,0,5,0.1,자신이 적을 쓰러뜨릴 경우 · 생명력 30% 회복,0 +gongsunzan_a,gongsunzan,active,종횡무진,,next_action,sling_dist_pct;lifesteal;return_always,,,,3,5,0.1,다음 행동의 이동력이 50% 증가하고 · 피해의 50%만큼 생명력을 회복하며 공격 후 제자리로 복귀,0 +gongsunzan_p,gongsunzan,passive,백마장사,,always,dmg_pct;heal_on_kill,,,,0,5,0.1,적 궁병에게 입히는 피해가 2배가 되며 · 적을 처치할 경우 생명력 30% 회복,0 +dongzhuo_a,dongzhuo,active,폭정,暴政,next_action,rampage,,,,4,5,0.1,다음 행동이 3초 동안 경로상 모든 유닛을 강하게 튕겨내며 돌진 피해를 입히지만 자신은 튕겨지지 않음(화면 가장자리에 닿을 경우에도 튕겨지며 핀볼처럼 동작),0 +dongzhuo_p,dongzhuo,passive,생명 강탈,,always,dmg_taken_pct;lifesteal,,,,0,5,0.1,모든 군영 유닛의 받는 피해를 10% 감소 시키며 · 자신이 입힌 피해의 30%만큼 생명력을 회복하고 · 아군 유닛이 1기가 줄어들 때마다 회복 효과는 5%씩 증가,0 +lijue_a,lijue,active,약탈,掠奪,next_action,splash_heal_lowest,,,,3,5,0.1,다음 행동으로 입힌 광역 피해의 50% 만큼 자신과 인접한 가장 생명력이 낮은 아군의 생명력을 회복,0 +lijue_p,lijue,passive,혈맹,血盟,always,action_end_sacrifice,,,,0,5,0.1,행동 종료 시 자신의 생명력을 10% 차감한 후 인접한 아군의 공격력을 1턴간 50% 강화 (대상이 없을 경우 발동하지 않고 · 생명 10%이하가 되면 생명력 차감 없이 발동),0 +yuanshao_a,yuanshao,active,사세삼공,四世三公,instant,sling_dist_pct;crit_dmg_pct;crit_chance,,,,4,5,0.1,즉시: 1턴간 행동 범위 내 모든 아군의 이동력과 치명타 피해량이 50% 증가하며 · 치명타 발생 확률이 20% 증가,0 +yuanshao_p,yuanshao,passive,명문의 위세,名門威勢,always,hp_max_pct;hit_bleed;hit_push,,,,0,5,0.1,모든 군영 영웅의 생명력이 20% 증가하고 · 자신의 공격이 치명타로 명중할 경우 대상에게 2턴간 출혈 효과를 유발한 후 최대 이동거리의 30%만큼 밀쳐냄(출혈 피해 추가),0 +yuanshu_a,yuanshu,active,꿀물,蜜水,instant,heal_self_pct;dmg_taken_pct,,,,4,5,0.1,즉시 자신의 생명력을 60% 회복하고 · 1턴간 받는 피해 30% 감소,0 +yuanshu_p,yuanshu,passive,옥새의 주인,,always,dmg_taken_pct;atk_pct;on_attacked_confuse,,,,0,5,0.1,받는 피해가 15% 증가하는 대신 자신 주위 3m 반경 내 아군의 공격력을 30% 증가시키고 자신을 공격한 대상에게 50% 확률로 혼란 효과 부여,0 +diaochan_a,diaochan,active,미인계,,instant,charm_pull,,,,4,5,0.1,즉시: 행동 범위 내 적 남성 유닛을 1턴간 행동 불가 상태에 빠뜨리고 · 즉시 자신의 방향으로 강제 이동 (강제 이동 중 지형지물이나 유닛이 있을 경우 튕겨나가며 막힘),0 +diaochan_p,diaochan,passive,절세미인,,always,atk_pct;multi_male_stun;dmg_taken_pct,,,,0,5,0.1,모든 군영의 남성 유닛의 공격력이 25% 증가하며 · 자신의 공격이 두명 이상의 남성 유닛에게 명중 될 경우 50% 확률로 1턴간 기절하고 · 모든 남성 유닛에게 받는 피해 30% 감소,0 +zhurong_a,zhurong,active,비도,飛刀,next_action,shot_as_archer;atk_ratio;hit_poison,,,,3,5,0.1,다음 행동 시 궁병과 동일하게 공격력의 220%의 원거리 공격을 시전할 수 있으며 · 명중 된 대상에게 중독 효과를 유발 (중독 : 매턴 시작과 행동 종료 시점 마다 최대 생명력의 2%씩 감소),0 +zhurong_p,zhurong,passive,남만의 여왕,,always,lifesteal;counter_dmg_pct;terrain_immune;atk_pct,,,,0,5,0.1,입힌 피해의 20%만큼 생명력을 회복하고 · 반격 피해가 50% 증가하며 지형 효과에 이동력 감소 영향을 받지 않고 여성 영웅을 공격할 때 공격력 50% 증가,0 +menghuo_a,menghuo,active,남만왕,南蠻王,next_action,counter_taken_pct;self_bounce_mult;reaction_on_terrain,,,,4,5,0.1,다음 행동은 반격 피해를 받지 않으며 · 자신은 튕기지 않고 경로상 닿은 유닛만 튕겨낼 수 있고(아군 포함) · 지형물(바위 등)에 닿거나 이동력 감소 지형이 경로상 존재할 경우 한번 더 행동할 수 있음,0 +menghuo_p,menghuo,passive,칠종칠금,七縱七擒,always,lifesteal;revive,,,,0,5,0.1,입힌 피해의 20%만큼 생명력을 회복하며 · 최대 생명이 60% 이상일 경우 사망해도 최대 생명력의 10%를 회복한 상태로 최대 7회까지 부활할 수 있지만 최대 생명력의 60% 이하일 때 사망한 경우 즉사,0 +gaoshun_a,gaoshun,active,함진영,陷陣營,next_action,hit_stun,,,,3,5,0.1,다음 행동으로 피해를 받은 대상은 1턴간 기절하며 · 아군을 밀쳐 적을 공격하거나 맵 오브젝트를 밀쳐 피해를 입힌 경우에도 피격 된 대상을 1턴 간 기절,0 +gaoshun_p,gaoshun,passive,함진 돌파,陷陣突破,always,push_continue,,,,0,5,0.1,아군이나 맵 오브젝트를 밀친 후 남은 이동력만큼 추가 행동 가능,0 +zhanglu_a,zhanglu,active,오두미 독술,五斗米毒術,next_action,shot_single;atk_ratio;hit_poison;poison_zone,,,,4,5,0.1,다음 행동이 직선형 투사체로 전환되며 · 타격 된 적에게 공격력의 250%의 피해를 입히고 중독시키며 · 1턴간 유지되는 소규모 독지대를 형성,0 +zhanglu_p,zhanglu,passive,한중의 교주,,always,zombie,,,,0,5,0.1,자신이 처치한 대상이 1턴간 체력1 상태로 되살아나 적을 공격할 수 있음(1턴 후 강제로 소멸되며 · 조작 불가),0 +yanbaihu_a,yanbaihu,active,동오의 덕왕,東吳德王,next_action,atk_pct;crit_chance,,,,3,5,0.1,다음 행동 시 공격력이 30% 증가하고 · 치명타 확률 15% 상승,0 +yanbaihu_p,yanbaihu,passive,산적 두목,,always,hp_max_pct,,,,0,5,0.1,최대 HP+10% 증가,0 +hanxuan_a,hanxuan,active,장사 태수,長沙太守,next_action,counter_taken_pct;push_mult,,,,3,5,0.1,다음 행동 시 반격 받지 않으며 · 2배 더 먼거리를 밀쳐낸다.,0 +hanxuan_p,hanxuan,passive,의심,疑心,always,counter_taken_pct,,,,0,5,0.1,받는 반격 피해 −20 %,0 +jinxuan_a,jinxuan,active,무릉의 화살,武陵之矢,next_action,crit_chance;crit_dmg_pct,,,,3,5,0.1,다음 행동 시 치명타 확률이 30% · 치명타 피해량이 50% 증가,0 +jinxuan_p,jinxuan,passive,성주,城主,always,dmg_taken_from_archer_pct;dmg_pct,,,,0,5,0.1,적 궁병에게 받는 피해는 15 % 감소하고 · 입히는 피해는 15% 증가,0 +zhaofan_a,zhaofan,active,계양의 계,桂陽之計,next_action,shot_pierce;ally_hit_heal,,,,3,5,0.1,다음 행동의 공격이 직선 형태로 변형되고 · 대상이 아군일 경우 공격력의 60%만큼 생명력을 회복하고 · 공격 대상이 적군일 경우 공격력의 160%만큼 피해를 입힘,0 +zhaofan_p,zhaofan,passive,처세,處世,always,negate_hits,,,,0,5,0.1,전투 시작 후 발생 된 어떤 피해든 1회를 무효화,0 +zhangren_a,zhangren,active,낙봉파의 화살,落鳳坡,next_action,shot_pierce,,,,3,5,0.1,다음 행동의 공격은 아군을 무시하며 직선 범위 내 적을 공격력의 150%의 피해로 공격 가능,0 +zhangren_p,zhangren,passive,매복 사수,埋伏射手,always,range_mult,,,,0,5,0.1,자신의 공격 사거리 30% 증가,0 +chunyuqiong_a,chunyuqiong,active,오소 돌격,烏巢突擊,next_action,return_on_hit,,,,3,5,0.1,다음 행동으로 공격 행동 시 제자리로 복귀 가능,0 +chunyuqiong_p,chunyuqiong,passive,하북 숙장,宿將,always,atk_missing_hp,,,,0,5,0.1,자신의 HP가 1% 감소할 때마다 이하일 때 공격력이 1% 증가,0 diff --git a/Assets/Resources/OutGame/HeroDb.asset b/Assets/Resources/OutGame/HeroDb.asset index 31236380..747dee9c 100644 --- a/Assets/Resources/OutGame/HeroDb.asset +++ b/Assets/Resources/OutGame/HeroDb.asset @@ -27,6 +27,7 @@ MonoBehaviour: skillPassive: liubei_p isSoldier: 0 firstStage: 1-1 + gender: m gear: - grade: green slots: @@ -74,6 +75,7 @@ MonoBehaviour: skillPassive: guanyu_p isSoldier: 0 firstStage: 1-1 + gender: m gear: - grade: green slots: @@ -121,6 +123,7 @@ MonoBehaviour: skillPassive: zhangfei_p isSoldier: 0 firstStage: 1-1 + gender: m gear: - grade: green slots: @@ -168,6 +171,7 @@ MonoBehaviour: skillPassive: zhaoyun_p isSoldier: 0 firstStage: 3-3 + gender: m gear: - grade: green slots: @@ -215,6 +219,7 @@ MonoBehaviour: skillPassive: huangzhong_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -262,6 +267,7 @@ MonoBehaviour: skillPassive: zhugeliang_p isSoldier: 0 firstStage: 8-13 + gender: m gear: - grade: green slots: @@ -309,6 +315,7 @@ MonoBehaviour: skillPassive: zhoucang_p isSoldier: 0 firstStage: 7-2 + gender: m gear: - grade: green slots: @@ -356,6 +363,7 @@ MonoBehaviour: skillPassive: guanping_p isSoldier: 0 firstStage: 7-7 + gender: m gear: - grade: green slots: @@ -403,6 +411,7 @@ MonoBehaviour: skillPassive: xushu_p isSoldier: 0 firstStage: 8-6 + gender: m gear: - grade: green slots: @@ -450,6 +459,7 @@ MonoBehaviour: skillPassive: taishici_p isSoldier: 0 firstStage: 3-8 + gender: m gear: - grade: green slots: @@ -497,6 +507,7 @@ MonoBehaviour: skillPassive: machao_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -544,6 +555,7 @@ MonoBehaviour: skillPassive: weiyan_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -591,6 +603,7 @@ MonoBehaviour: skillPassive: pangtong_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -638,6 +651,7 @@ MonoBehaviour: skillPassive: jiangwei_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -685,6 +699,7 @@ MonoBehaviour: skillPassive: liufeng_p isSoldier: 0 firstStage: 8-11 + gender: m gear: - grade: green slots: @@ -732,6 +747,7 @@ MonoBehaviour: skillPassive: jianyong_p isSoldier: 0 firstStage: 3-12 + gender: m gear: - grade: green slots: @@ -779,6 +795,7 @@ MonoBehaviour: skillPassive: sunqian_p isSoldier: 0 firstStage: 3-12 + gender: m gear: - grade: green slots: @@ -826,6 +843,7 @@ MonoBehaviour: skillPassive: mizhu_p isSoldier: 0 firstStage: 3-12 + gender: m gear: - grade: green slots: @@ -873,6 +891,7 @@ MonoBehaviour: skillPassive: caocao_p isSoldier: 0 firstStage: 1-8 + gender: m gear: - grade: green slots: @@ -920,6 +939,7 @@ MonoBehaviour: skillPassive: xiahoudun_p isSoldier: 0 firstStage: 5-14 + gender: m gear: - grade: green slots: @@ -967,6 +987,7 @@ MonoBehaviour: skillPassive: xuchu_p isSoldier: 0 firstStage: 5-15 + gender: m gear: - grade: green slots: @@ -1014,6 +1035,7 @@ MonoBehaviour: skillPassive: zhangliao_p isSoldier: 0 firstStage: 4-14 + gender: m gear: - grade: green slots: @@ -1061,6 +1083,7 @@ MonoBehaviour: skillPassive: xuhuang_p isSoldier: 0 firstStage: 5-15 + gender: m gear: - grade: green slots: @@ -1108,6 +1131,7 @@ MonoBehaviour: skillPassive: zhanghe_p isSoldier: 0 firstStage: 7-9 + gender: m gear: - grade: green slots: @@ -1155,6 +1179,7 @@ MonoBehaviour: skillPassive: caoren_p isSoldier: 0 firstStage: 8-10 + gender: m gear: - grade: green slots: @@ -1202,6 +1227,7 @@ MonoBehaviour: skillPassive: yujin_p isSoldier: 0 firstStage: 3-15 + gender: m gear: - grade: green slots: @@ -1249,6 +1275,7 @@ MonoBehaviour: skillPassive: simayi_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -1296,6 +1323,7 @@ MonoBehaviour: skillPassive: xiahouyuan_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -1343,6 +1371,7 @@ MonoBehaviour: skillPassive: guojia_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -1390,6 +1419,7 @@ MonoBehaviour: skillPassive: jiaxu_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -1437,6 +1467,7 @@ MonoBehaviour: skillPassive: dianwei_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -1484,6 +1515,7 @@ MonoBehaviour: skillPassive: xunyu_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -1531,6 +1563,7 @@ MonoBehaviour: skillPassive: xiahoumao_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -1578,6 +1611,7 @@ MonoBehaviour: skillPassive: wanglang_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -1625,6 +1659,7 @@ MonoBehaviour: skillPassive: xiahouen_p isSoldier: 0 firstStage: 9-8 + gender: m gear: - grade: green slots: @@ -1672,6 +1707,7 @@ MonoBehaviour: skillPassive: sunjian_p isSoldier: 0 firstStage: 2-3 + gender: m gear: - grade: green slots: @@ -1719,6 +1755,7 @@ MonoBehaviour: skillPassive: zhouyu_p isSoldier: 0 firstStage: 10-3 + gender: m gear: - grade: green slots: @@ -1766,6 +1803,7 @@ MonoBehaviour: skillPassive: huanggai_p isSoldier: 0 firstStage: 10-7 + gender: m gear: - grade: green slots: @@ -1813,6 +1851,7 @@ MonoBehaviour: skillPassive: ganning_p isSoldier: 0 firstStage: 10-3 + gender: m gear: - grade: green slots: @@ -1860,6 +1899,7 @@ MonoBehaviour: skillPassive: chengpu_p isSoldier: 0 firstStage: 2-3 + gender: m gear: - grade: green slots: @@ -1907,6 +1947,7 @@ MonoBehaviour: skillPassive: sunce_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -1954,6 +1995,7 @@ MonoBehaviour: skillPassive: sunquan_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -2001,6 +2043,7 @@ MonoBehaviour: skillPassive: lusu_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -2048,6 +2091,7 @@ MonoBehaviour: skillPassive: lvmeng_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -2095,6 +2139,7 @@ MonoBehaviour: skillPassive: luxun_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -2142,6 +2187,7 @@ MonoBehaviour: skillPassive: sunshangxiang_p isSoldier: 0 firstStage: + gender: f gear: - grade: green slots: @@ -2189,6 +2235,7 @@ MonoBehaviour: skillPassive: xiaoqiao_p isSoldier: 0 firstStage: + gender: f gear: - grade: green slots: @@ -2236,6 +2283,7 @@ MonoBehaviour: skillPassive: lingtong_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -2283,6 +2331,7 @@ MonoBehaviour: skillPassive: zhoutai_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -2330,6 +2379,7 @@ MonoBehaviour: skillPassive: zhugejin_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -2377,6 +2427,7 @@ MonoBehaviour: skillPassive: dongxi_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -2424,6 +2475,7 @@ MonoBehaviour: skillPassive: lvbu_p isSoldier: 0 firstStage: 2-10 + gender: m gear: - grade: green slots: @@ -2471,6 +2523,7 @@ MonoBehaviour: skillPassive: zhangjiao_p isSoldier: 0 firstStage: 1-10 + gender: m gear: - grade: green slots: @@ -2518,6 +2571,7 @@ MonoBehaviour: skillPassive: huaxiong_p isSoldier: 0 firstStage: 2-5 + gender: m gear: - grade: green slots: @@ -2565,6 +2619,7 @@ MonoBehaviour: skillPassive: yanliang_p isSoldier: 0 firstStage: 6-5 + gender: m gear: - grade: green slots: @@ -2612,6 +2667,7 @@ MonoBehaviour: skillPassive: wenchou_p isSoldier: 0 firstStage: 3-3 + gender: m gear: - grade: green slots: @@ -2659,6 +2715,7 @@ MonoBehaviour: skillPassive: gongsunzan_p isSoldier: 0 firstStage: 2-1 + gender: m gear: - grade: green slots: @@ -2706,6 +2763,7 @@ MonoBehaviour: skillPassive: dongzhuo_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -2753,6 +2811,7 @@ MonoBehaviour: skillPassive: lijue_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -2800,6 +2859,7 @@ MonoBehaviour: skillPassive: yuanshao_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -2847,6 +2907,7 @@ MonoBehaviour: skillPassive: yuanshu_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -2894,6 +2955,7 @@ MonoBehaviour: skillPassive: diaochan_p isSoldier: 0 firstStage: + gender: f gear: - grade: green slots: @@ -2941,6 +3003,7 @@ MonoBehaviour: skillPassive: zhurong_p isSoldier: 0 firstStage: + gender: f gear: - grade: green slots: @@ -2988,6 +3051,7 @@ MonoBehaviour: skillPassive: menghuo_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -3035,6 +3099,7 @@ MonoBehaviour: skillPassive: gaoshun_p isSoldier: 0 firstStage: 4-10 + gender: m gear: - grade: green slots: @@ -3082,6 +3147,7 @@ MonoBehaviour: skillPassive: zhanglu_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -3129,6 +3195,7 @@ MonoBehaviour: skillPassive: yanbaihu_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -3176,6 +3243,7 @@ MonoBehaviour: skillPassive: hanxuan_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -3223,6 +3291,7 @@ MonoBehaviour: skillPassive: jinxuan_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -3270,6 +3339,7 @@ MonoBehaviour: skillPassive: zhaofan_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -3317,6 +3387,7 @@ MonoBehaviour: skillPassive: zhangren_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -3364,6 +3435,7 @@ MonoBehaviour: skillPassive: chunyuqiong_p isSoldier: 0 firstStage: + gender: m gear: - grade: green slots: @@ -3411,6 +3483,7 @@ MonoBehaviour: skillPassive: isSoldier: 1 firstStage: + gender: m gear: [] - id: soldier_infantry_2 nameKo: "\uC601\uC6C5 \uBCF4\uBCD1" @@ -3426,6 +3499,7 @@ MonoBehaviour: skillPassive: isSoldier: 1 firstStage: + gender: m gear: [] - id: soldier_infantry_3 nameKo: "\uC804\uC124 \uBCF4\uBCD1" @@ -3441,6 +3515,7 @@ MonoBehaviour: skillPassive: isSoldier: 1 firstStage: + gender: m gear: [] - id: soldier_spear_1 nameKo: "\uC815\uC608 \uCC3D\uBCD1" @@ -3456,6 +3531,7 @@ MonoBehaviour: skillPassive: isSoldier: 1 firstStage: + gender: m gear: [] - id: soldier_spear_2 nameKo: "\uC601\uC6C5 \uCC3D\uBCD1" @@ -3471,6 +3547,7 @@ MonoBehaviour: skillPassive: isSoldier: 1 firstStage: + gender: m gear: [] - id: soldier_spear_3 nameKo: "\uC804\uC124 \uCC3D\uBCD1" @@ -3486,6 +3563,7 @@ MonoBehaviour: skillPassive: isSoldier: 1 firstStage: + gender: m gear: [] - id: soldier_cavalry_1 nameKo: "\uC815\uC608 \uAE30\uBCD1" @@ -3501,6 +3579,7 @@ MonoBehaviour: skillPassive: isSoldier: 1 firstStage: + gender: m gear: [] - id: soldier_cavalry_2 nameKo: "\uC601\uC6C5 \uAE30\uBCD1" @@ -3516,6 +3595,7 @@ MonoBehaviour: skillPassive: isSoldier: 1 firstStage: + gender: m gear: [] - id: soldier_cavalry_3 nameKo: "\uC804\uC124 \uAE30\uBCD1" @@ -3531,6 +3611,7 @@ MonoBehaviour: skillPassive: isSoldier: 1 firstStage: + gender: m gear: [] - id: soldier_archer_1 nameKo: "\uC815\uC608 \uAD81\uBCD1" @@ -3546,6 +3627,7 @@ MonoBehaviour: skillPassive: isSoldier: 1 firstStage: + gender: m gear: [] - id: soldier_archer_2 nameKo: "\uC601\uC6C5 \uAD81\uBCD1" @@ -3561,6 +3643,7 @@ MonoBehaviour: skillPassive: isSoldier: 1 firstStage: + gender: m gear: [] - id: soldier_archer_3 nameKo: "\uC804\uC124 \uAD81\uBCD1" @@ -3576,6 +3659,7 @@ MonoBehaviour: skillPassive: isSoldier: 1 firstStage: + gender: m gear: [] - id: soldier_strategist_1 nameKo: "\uC815\uC608 \uCC45\uC0AC" @@ -3591,6 +3675,7 @@ MonoBehaviour: skillPassive: isSoldier: 1 firstStage: + gender: m gear: [] - id: soldier_strategist_2 nameKo: "\uC601\uC6C5 \uCC45\uC0AC" @@ -3606,6 +3691,7 @@ MonoBehaviour: skillPassive: isSoldier: 1 firstStage: + gender: m gear: [] - id: soldier_strategist_3 nameKo: "\uC804\uC124 \uCC45\uC0AC" @@ -3621,4 +3707,5 @@ MonoBehaviour: skillPassive: isSoldier: 1 firstStage: + gender: m gear: [] diff --git a/Assets/Resources/OutGame/SkillInfoDb.asset b/Assets/Resources/OutGame/SkillInfoDb.asset index a4b7c581..ee988c08 100644 --- a/Assets/Resources/OutGame/SkillInfoDb.asset +++ b/Assets/Resources/OutGame/SkillInfoDb.asset @@ -16,20 +16,18 @@ MonoBehaviour: - id: liubei_a heroId: liubei kind: active - nameKo: "\uC778\uB355" - hanja: "\u4EC1\u5FB7" + nameKo: "\uC774\uB3C4\uB958 \uAC80\uBC95" + hanja: trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uC54C\uAE4C\uAE30 \uBA85\uC911 \uC2DC \uC790\uC2E0\uC740 - \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 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC774 2\uBC88 \uD589\uB3D9 \uAC00\uB2A5\uD558\uBA70 + \xB7 1\uD134 \uAC04 \uC801\uC5D0\uAC8C \uBC18\uACA9 \uBC1B\uC9C0 \uC54A\uC74C" + effectKey: extra_action;counter_taken_pct + p1: 0 p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: liubei_p @@ -40,13 +38,15 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uC544\uAD70\uC774 \uBC00\uB824\uC11C \uD558\uB294 \uACF5\uACA9 \uD53C\uD574 - +50 %" - effectKey: pushed_ally_attack_pct - p1: 0.5 + descKo: "\uBAA8\uB4E0 \uCD09 \uC9C4\uC601 \uC720\uB2DB \uACF5\uACA9\uB825 + 20% + \uC99D\uAC00\uD558\uACE0 \xB7 \uC790\uC2E0\uC774 \uC0AC\uB9DD\uD560 \uACBD\uC6B0 + \uC0DD\uBA85\uB825 100% \uC0C1\uD0DC\uB85C 1\uD68C \uBD80\uD65C (\uC2A4\uD14C\uC774\uC9C0\uB2F9 + 1\uD68C)" + effectKey: atk_pct;revive + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: guanyu_a @@ -54,33 +54,38 @@ MonoBehaviour: kind: active nameKo: "\uCCAD\uB8E1\uC5B8\uC6D4" hanja: "\u9751\u9F8D\u5043\u6708" - trigger: next_action + trigger: instant cooldown: 3 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 + descKo: "\uC989\uC2DC: \uCCAD\uB8E1\uC5B8\uC6D4\uB3C4\uB97C \uD718\uB458\uB7EC + \uBC18\uACBD 3m \uB0B4 \uBAA8\uB4E0 \uC801\uC5D0\uAC8C \uACF5\uACA9\uB825\uC758 + 250% \uD53C\uD574\uB97C \uC785\uD788\uBA70 1m\uB97C \uBC00\uCCD0\uB0B4\uACE0 + \uCD9C\uD608 \uD6A8\uACFC\uB97C \uBD80\uC5EC" + effectKey: aoe_strike;hit_bleed + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: guanyu_p heroId: guanyu kind: passive - nameKo: "\uBB34\uC131" - hanja: "\u6B66\u8056" + nameKo: "\uBB34\uC2E0" + hanja: trigger: always cooldown: 0 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 + descKo: "\uBAA8\uB4E0 \uCD09 \uC9C4\uC601\uC758 \uBC18\uACA9 \uD53C\uD574\uAC00 + 50%\uC99D\uAC00\uD558\uBA70 \xB7 \uC790\uC2E0\uC740 \uC801\uC5D0\uAC8C \uBC18\uACA9 + \uBC1B\uC9C0 \uC54A\uC73C\uBA70 \xB7 \uC790\uC2E0\uC774 \uC801\uC744 \uCC98\uCE58\uD560 + \uB54C\uB9C8\uB2E4 \uACF5\uACA9\uB825 10%\uC529 \uC529 \uC99D\uAC00 (\uCD5C\uB300 + 100%)" + effectKey: counter_dmg_pct;counter_taken_pct;kill_stack_atk + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: zhangfei_a @@ -91,17 +96,16 @@ MonoBehaviour: trigger: instant cooldown: 4 lvMax: 5 - descKo: "\uC989\uC2DC \u2605: \uBC18\uACBD 4 \uC548 \uC801 \uC804\uC6D0\uC744 - \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 + descKo: "\uC989\uC2DC: \uBC18\uACBD 4m \uBC94\uC704 \uB0B4 \uC801 \uC804\uC6D0\uC744 + \uBC14\uAE65\uC73C\uB85C 1m \uBC00\uCCD0\uB0B4\uACE0 \uD53C\uACA9 \uB41C \uC801\uC744 + 1\uD134\uAC04 \uAE30\uC808" + effectKey: aoe_strike;hit_stun + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: zhangfei_p heroId: zhangfei kind: passive @@ -110,13 +114,18 @@ MonoBehaviour: trigger: always cooldown: 0 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 + descKo: "\uBAA8\uB4E0 \uCD09 \uC9C4\uC601\uC758 \uC774\uB3D9 \uAC70\uB9AC\uAC00 + 30% \uC99D\uAC00\uD558\uACE0 \xB7 \uB3CC\uC9C4 \uAC70\uB9AC\uC5D0 \uBE44\uD574 + \uD53C\uD574\uAC00 \uB298\uC5B4\uB098\uACE0(\uCD5C\uB300 100% \xB7 \uC2AC\uB9C1\uC0F7 + \uAC70\uB9AC \uAE30\uC900) 2\uD68C \uD53C\uD574\uB97C \uC5F0\uC18D \uBC1B\uC740 + \uC801\uC744 1\uD134\uAC04 \uAE30\uC808 \uC2DC\uD0A4\uACE0 \xB7 \uAE30\uC808\uD55C + \uC801\uC744 \uACF5\uACA9\uD560 \uACBD\uC6B0 \uD53C\uD574\uC758 50%\uB9CC\uD07C + \uC0DD\uBA85\uB825 \uD68C\uBCF5" + effectKey: sling_dist_pct;dash_dmg_pct;double_hit_stun;lifesteal + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: zhaoyun_a @@ -127,13 +136,13 @@ MonoBehaviour: trigger: next_action cooldown: 3 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 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC758 \uC774\uB3D9 \uAC70\uB9AC\uAC00 2\uBC30\uB85C + \uB298\uC5B4\uB098\uACE0 \uD53C\uD574\uB7C9\uC774 50% \uC99D\uAC00" + effectKey: sling_dist_pct;dmg_pct + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: zhaoyun_p @@ -144,13 +153,16 @@ MonoBehaviour: trigger: always cooldown: 0 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 + descKo: "\uC801\uC5D0\uAC8C \uBC18\uACA9 \uBC1B\uC9C0 \uC54A\uC73C\uBA70 \xB7 + \uAC70\uB9AC\uC5D0 \uC0C1\uAD00 \uC5C6\uC774 \uC774\uB3D9 \uACBD\uB85C \uC0C1 + \uC801\uC5D0\uAC8C \uD53C\uD574\uB97C \uC785\uD78C \uACBD\uC6B0 \uD56D\uC0C1 + \uC81C\uC790\uB9AC\uB85C \uBCF5\uADC0\uD558\uACE0 \xB7 \uBCF5\uADC0\uD560 \uB54C\uC5D0\uB3C4 + \uD53C\uD574\uB97C \uC785\uD790 \uC218 \uC788\uC74C" + effectKey: counter_taken_pct;return_on_hit + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: huangzhong_a @@ -161,15 +173,18 @@ MonoBehaviour: trigger: next_action cooldown: 2 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 + descKo: "\uB2E4\uC74C \uD589\uB3D9 \uC2DC \uACF5\uACA9 \uAC70\uB9AC\uC5D0 \uBB34\uAD00\uD558\uAC8C + \uC9C0\uC815\uD55C \uBC29\uD5A5\uC73C\uB85C \uAC70\uB300\uD55C \uD654\uC0B4\uC744 + \uB0A0\uB824 \uBC94\uC704 \uB0B4 \uCCAB \uBA85\uC911 \uB41C \uC801\uC5D0\uAC8C + \uACF5\uACA9\uB825\uC758 500%\uC758 \uD53C\uD574\uB97C \uC785\uD788\uACE0 \xB7 + 1\uD134\uAC04 \uAE30\uC808 \uC2DC\uD0B4" + effectKey: shot_giant;hit_stun + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: huangzhong_p heroId: huangzhong kind: passive @@ -178,215 +193,234 @@ MonoBehaviour: trigger: always cooldown: 0 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 + descKo: "\uBAA8\uB4E0 \uACF5\uACA9\uC774 \uC544\uAD70\uACFC \uC7A5\uC560\uBB3C\uC744 + \uD1B5\uACFC\uD574 \uACF5\uACA9\uD560 \uC218 \uC788\uC73C\uBA70 \xB7 \uC790\uC2E0\uC758 + \uCE58\uBA85\uD0C0 \uD655\uB960\uC774 20% \uC99D\uAC00\uD558\uACE0 \xB7 \uCE58\uBA85\uD0C0 + \uD53C\uD574\uB7C9\uC774 50% \uC99D\uAC00" + effectKey: arrow_pass_allies;crit_chance;crit_dmg_pct + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: zhugeliang_a heroId: zhugeliang kind: active - nameKo: "\uB3D9\uB0A8\uD48D" - hanja: "\u6771\u5357\u98A8" + nameKo: "\uCE60\uC131 \uAE30\uC6D0" + hanja: "\u4E03\u661F\u7948\u9858" trigger: instant cooldown: 4 lvMax: 5 - 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 + descKo: "\uC989\uC2DC: \uD589\uB3D9 \uBC18\uACBD \uB0B4 \uBAA8\uB4E0 \uC544\uAD70\uC758 + \uC0DD\uBA85\uB825\uC744 \uACF5\uACA9\uB825\uC758 100%\uB9CC\uD07C \uD68C\uBCF5\uC2DC\uD0A4\uACE0 + \xB7 \uC0C1\uD0DC \uC774\uC0C1 \uD6A8\uACFC\uB97C \uC81C\uAC70" + effectKey: heal_atk;cleanse + p1: 0 p2: 0 - p3: 1 - pMask: 7 + p3: 0 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: zhugeliang_p heroId: zhugeliang kind: passive - nameKo: "\uC2E0\uC0B0" - hanja: "\u795E\u7B97" + nameKo: "\uC640\uB8E1\uC758 \uAC00\uD638" + hanja: "\u81E5\u9F8D\u52A0\u8B77" trigger: always cooldown: 0 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 + descKo: "\uBAA8\uB4E0 \uCD09 \uC9C4\uC601 \uC720\uB2DB\uC758 \uBC18\uACA9 \uD53C\uD574\uB97C + 50% \uBC18\uAC10\uC2DC\uD0A4\uACE0 \xB7 \uD134 \uC885\uB8CC \uC2DC \uC790\uC2E0\uC758 + \uBC18\uACBD 3m \uBC94\uC704\uC758 \uC0DD\uBA85\uB825\uC774 \uAC00\uC7A5 \uB0AE\uC740 + \uC544\uAD70\uC758 \uC0DD\uBA85\uB825\uC744 \uACF5\uACA9\uB825\uC758 50%\uB9CC\uD07C + \uD68C\uBCF5" + effectKey: counter_taken_pct;turn_end_heal_lowest + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: zhoucang_a heroId: zhoucang kind: active - nameKo: "\uCCAD\uB8E1\uB3C4 \uC9C0\uAE30" - hanja: + nameKo: "\uB300\uB3C4 \uC77C\uC12C" + hanja: "\u5927\u5200\u4E00\u9583" trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uC54C\uAE4C\uAE30 \uBC00\uAE30 \xD72" - effectKey: push_mult - p1: 2 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC740 180%\uC758 \uD53C\uD574\uB97C \uC785\uD788\uBA70 + \xB7 2\uD134\uAC04 \uCD9C\uD608 \uD6A8\uACFC\uB97C \uC720\uBC1C\uD558\uBA70 + 2\uBC30 \uAC70\uB9AC\uB97C \uBC00\uCCD0\uB0C4" + effectKey: atk_ratio;hit_bleed;push_mult + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: zhoucang_p heroId: zhoucang kind: passive - nameKo: "\uC0B0\uCC44 \uCCB4\uB825" - hanja: + nameKo: "\uD608\uD754 \uCD94\uACA9" + hanja: "\u8840\u75D5\u8FFD\u64CA" trigger: always cooldown: 0 lvMax: 5 - descKo: "\uCD5C\uB300 HP +20 %" - effectKey: hp_max_pct - p1: 0.2 + descKo: "\uCD9C\uD608 \uB41C \uB300\uC0C1\uC744 \uACF5\uACA9\uD560 \uACBD\uC6B0 + \uACF5\uACA9\uB825 50% \uC99D\uAC00" + effectKey: atk_pct + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: guanping_a heroId: guanping kind: active - nameKo: "\uD638\uC704" - hanja: "\u8B77\u885B" + nameKo: "\uCCA0\uBCBD \uD638\uC704" + hanja: "\u9435\u58C1\u8B77\u885B" trigger: instant cooldown: 3 lvMax: 5 - descKo: "\uC989\uC2DC \u2605: \uC774\uBC88 \uC801 \uD134 \uBC18\uACBD 1.5 \uC548 - \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 + descKo: "\uC989\uC2DC: 2\uD134\uAC04 \uD638\uC704 \uBC94\uC704\uAC00 2\uBC30\uB85C + \uB298\uC5B4\uB098\uBA70 \xB7 \uD638\uC704 \uC2DC \uBC1B\uB294 \uD53C\uD574 + 50% \uAC10\uC18C" + effectKey: guard_radius_mult;guard_dr + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: guanping_p heroId: guanping kind: passive - nameKo: "\uD6C4\uACC4" - hanja: "\u5F8C\u7E7C" + nameKo: "\uD638\uC704" + hanja: "\u8B77\u885B" trigger: always cooldown: 0 lvMax: 5 - descKo: "\uACAC\uC81C \uBC18\uACBD +0.5" - effectKey: overwatch_radius_add - p1: 0.5 + descKo: "\uBC18\uACA9 \uD53C\uD574\uAC00 50% \uC99D\uAC00\uD558\uBA70 \xB7 \uD56D\uC0C1 + \uC790\uC2E0 \uC8FC\uC704 2m \uBC94\uC704 \uB0B4 \uC544\uAD70\uC758 \uD53C\uD574\uB97C + \uB300\uC2E0 \uBC1B\uC74C" + effectKey: counter_dmg_pct;guard + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: xushu_a heroId: xushu kind: active - nameKo: "\uD314\uBB38 \uAC04\uD30C" - hanja: "\u516B\u9580\u770B\u7834" + nameKo: "\uB2E8\uBCF5\uC758 \uD654\uACC4" + hanja: "\u55AE\u798F\u706B\u8A08" trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uAD11\uC5ED \uD53C\uD574\uAC00 \uC544\uAD70\uC5D0\uAC8C - 0(\uAD11\uC5ED)" - effectKey: splash_ally_ratio + descKo: "\uB2E4\uC74C \uD589\uB3D9 \uC2DC \uC9C0\uC815\uD55C \uBC29\uD5A5\uC73C\uB85C + \uAC70\uB300\uD55C \uD654\uC5FC\uAD6C\uB97C \uB0A0\uB824 \uBC94\uC704 \uB0B4 + \uBAA8\uB4E0 \uC801\uC5D0\uAC8C \uACF5\uACA9\uB825\uC758 250%\uC758 \uD53C\uD574\uB97C + \uC785\uD788\uACE0 \xB7 3\uD134\uAC04 \uBD88\uD0DC\uC6B0\uAE30 \uD6A8\uACFC + \uC720\uBC1C (\uBD88\uD0DC\uC6B0\uAE30 \uD6A8\uACFC : \uB9E4\uD134 \uC2DC\uC791 + \uC2DC \uC0DD\uBA85\uB825\uC758 5% \uD53C\uD574)" + effectKey: shot_blast;hit_burn p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: xushu_p heroId: xushu kind: passive - nameKo: "\uB2E8\uAE30" - hanja: "\u55AE\u9A0E" + nameKo: "\uD314\uBB38 \uAC04\uD30C" + hanja: "\u516B\u9580\u770B\u7834" trigger: always cooldown: 0 lvMax: 5 - descKo: "\uC0AC\uAC70\uB9AC +0.5" - effectKey: range_add - p1: 0.5 + descKo: "\uACF5\uACA9 \uC0AC\uAC70\uB9AC 50% \uC99D\uAC00 \uBC0F \uC790\uC2E0\uC758 + \uAD11\uC5ED \uACF5\uACA9\uC774 \uC544\uAD70\uC5D0\uAC8C\uB294 \uD53C\uD574\uB97C + \uC785\uD788\uC9C0 \uC54A\uC74C" + effectKey: range_mult;splash_ally_ratio + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: taishici_a heroId: taishici kind: active - nameKo: "\uC2E0\uC0AC" - hanja: "\u795E\u5C04" + nameKo: "\uAD00\uD1B5 \uD654\uC0B4" + hanja: trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uC0AC\uAC70\uB9AC \xD71.5" - effectKey: range_mult - p1: 1.5 + descKo: "\uB2E4\uC74C \uD589\uB3D9: \uACBD\uB85C \uC0C1 \uBAA8\uB4E0 \uC801\uC744 + \uAD00\uD1B5\uD558\uBA70 \uACF5\uACA9\uB825\uC758 300%\uC758 \uD53C\uD574\uB97C + \uC785\uD798 (\uC7A5\uC560\uBB3C \uBB34\uC2DC)" + effectKey: shot_pierce + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: taishici_p heroId: taishici kind: passive - nameKo: "\uB3CC\uD30C" - hanja: "\u7A81\u7834" + nameKo: "\uC2E0\uAD81" + hanja: "\u795E\u5F13" trigger: always cooldown: 0 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 + descKo: "\uADFC\uC811 \uC720\uB2DB\uACFC\uC758 \uC804\uD22C \uC2DC \uD328\uB110\uD2F0\uAC00 + \uC5C6\uC73C\uBA70 \xB7 \uC790\uC2E0\uC758 \uACF5\uACA9\uB825\uACFC \uCE58\uBA85\uD0C0 + \uD53C\uD574\uB7C9\uC774 50%\uC529 \uC99D\uAC00" + effectKey: melee_penalty_off;atk_pct;crit_dmg_pct + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: machao_a heroId: machao kind: active - nameKo: "\uAE08\uB9C8\uCD08" - hanja: "\u9326\u99AC\u8D85" + nameKo: "\uAE08\uB9C8 \uC77C\uACA9" + hanja: "\u9326\u99AC\u4E00\u64CA" trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uAD00\uD1B5 \uD53C\uD574 +50 % \xB7 \uAD00\uD1B5\uD55C - \uC801 \uBC00\uAE30 \xD72" - effectKey: pierce_dmg_mult;pierce_knockback_mult - p1: 1.5 - p2: 2 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC758 \uACF5\uACA9\uB825\uC774 50% \uC99D\uAC00\uD558\uACE0 + \xB7 \uBB34\uC870\uAC74 \uCE58\uBA85\uD0C0 \uD53C\uD574\uB97C \uC785\uD798" + effectKey: atk_pct;crit_always + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: machao_p heroId: machao kind: passive - nameKo: "\uC2E0\uC704" - hanja: "\u795E\u5A01" + nameKo: "\uBCF5\uC218\uC758 \uD654\uC2E0" + hanja: "\u5FA9\u8B90\u5316\u8EAB" trigger: always cooldown: 0 lvMax: 5 - descKo: "\uAD00\uD1B5\uD55C \uC801 1\uAE30\uB2F9 \uACF5 +8 %(\uCD5C\uB300 +40 - % \xB7 \uC804\uD22C \uC911 \uB204\uC801) [p1 1\uAE30\uB2F9 \xB7 p2 \uCD5C\uB300]" - effectKey: pierce_dmg_pct_per_hit - p1: 0.08 - p2: 0.4 + descKo: "\uC785\uD78C \uD53C\uD574\uC758 20%\uB9CC\uD07C \uC0DD\uBA85\uB825\uC744 + \uD68C\uBCF5\uD558\uACE0 \xB7 \uACF5\uACA9\uC73C\uB85C \uBA85\uC911 \uB41C + \uC801 1\uAE30\uB2F9 \uD53C\uD574\uB7C9 25%\uC529 \uC99D\uAC00" + effectKey: lifesteal;dmg_per_hit + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: weiyan_a @@ -397,13 +431,14 @@ MonoBehaviour: trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uC0C1\uC131 \uBD88\uB9AC(\uAE30\uBCD1 0.7)\uB97C - 1.0 \uC73C\uB85C \xB7 \uD53C\uD574 +30 %" - effectKey: matchup_floor;dmg_pct - p1: 1 - p2: 0.3 + descKo: "\uB2E4\uC74C \uD589\uB3D9 \uC2DC \uBA85\uC911 \uB41C \uB300\uC0C1\uACFC + \uC704\uCE58\uB97C \uC790\uC2E0\uC758 \uC704\uCE58\uC640 \uAD50\uD658\uD558\uACE0 + \xB7 \uD55C\uBC88 \uB354 \uD589\uB3D9\uD560 \uC218 \uC788\uC74C" + effectKey: swap_on_hit;extra_action + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: weiyan_p @@ -414,13 +449,14 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "HP 50 % \uC774\uD558\uC77C \uB54C \uBC1B\uB294 \uD53C\uD574 \u221220 - % [p2 HP \uC784\uACC4(\uC774\uD558\uC77C \uB54C)]" - effectKey: dmg_taken_pct - p1: -0.2 - p2: 0.5 + descKo: "\uC0C1\uC131 \uD6A8\uACFC\uB97C \uBB34\uC2DC\uD558\uBA70 \xB7 HP\uAC00 + \uB0AE\uC744\uC218\uB85D \uACF5\uACA9\uB825\uC774 \uC99D\uAC00 (\uB0A8\uC740 + HP %\uBE44\uC728 \uAE30\uC900 \xB7 \uCD5C\uB300 100%)" + effectKey: matchup_floor;atk_missing_hp + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: pangtong_a @@ -431,65 +467,71 @@ MonoBehaviour: trigger: instant cooldown: 4 lvMax: 5 - descKo: "\uC989\uC2DC \u2605: \uBC18\uACBD 3 \uC548 \uC801 \uC804\uC6D0\uC744 - 0.5 \uBC00\uACE0 \uC774\uBC88 \uC801 \uD134 \uAD50\uC804 \uBC18\uC751 \uC5C6\uC74C - [p1 \uBC00\uAE30 \uAC70\uB9AC \xB7 p2 \uBC18\uACBD]" - effectKey: aoe_push_fear - p1: 0.5 - p2: 3 + descKo: "\uC989\uC2DC: \uD589\uB3D9 \uBC18\uACBD \uB0B4 \uBAA8\uB4E0 \uC544\uAD70\uC758 + \uC0DD\uBA85\uB825\uC744 \uACF5\uACA9\uB825\uC758 50%\uB9CC\uD07C \uC989\uC2DC + \uD68C\uBCF5\uC2DC\uD0A4\uACE0 \xB7 3\uD134\uAC04 \uB9E4 \uD134\uB9C8\uB2E4 + \uACF5\uACA9\uB825\uC758 10%\uC529 \uC0DD\uBA85\uB825\uC744 \uD68C\uBCF5" + effectKey: heal_atk;hot + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: pangtong_p heroId: pangtong kind: passive - nameKo: "\uC5F0\uD658" - hanja: "\u9023\u74B0" + nameKo: "\uC5F0\uD658 \uBC29\uBE44" + hanja: "\u9023\u74B0\u9632\u5099" trigger: always cooldown: 0 lvMax: 5 - descKo: "\uAD11\uC5ED \uD53C\uD574 \uBE44\uC728 0.6 \u2192 0.8 [\uAD11\uC5ED - \uBE44\uC728 \uC808\uB300\uAC12(\uAE30\uBCF8 0.6)]" - effectKey: splash_ratio - p1: 0.8 + descKo: "\uBAA8\uB4E0 \uCD09 \uC9C4\uC601 \uC720\uB2DB\uC758 \uC6D0\uAC70\uB9AC + \uACF5\uACA9 \uD53C\uD574\uB97C 50% \uBC18\uAC10\uC2DC\uD0A4\uACE0 \xB7 \uD134 + \uC885\uB8CC \uC2DC \uC790\uC2E0\uC758 \uBC18\uACBD 3m \uBC94\uC704\uC758 \uC544\uAD70 + 1\uAE30\uC758 \uC0C1\uD0DC\uC774\uC0C1 \uD6A8\uACFC\uB97C \uC81C\uAC70" + effectKey: dmg_taken_ranged_pct;turn_end_cleanse + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: jiangwei_a heroId: jiangwei kind: active - nameKo: "\uAD6C\uBC8C\uC911\uC6D0" - hanja: "\u4E5D\u4F10\u4E2D\u539F" - trigger: next_action + nameKo: "\uB2F4\uB300\uC5EC\uB450" + hanja: "\u81BD\u5927\u5982\u6597" + trigger: instant cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uBC00\uBA70 \uC804\uC9C4 \uAC70\uB9AC \xD71.5 - \xB7 \uBA85\uC911 \uC2DC \uACAC\uC81C \uD69F\uC218 2\uD68C [\uBC00\uBA70 \uC804\uC9C4 - \uAC70\uB9AC \uBC30\uC728 \xB7 \uD0A4 \uADFC\uC0AC]" - effectKey: push_mult;overwatch_count - p1: 1.5 - p2: 2 + descKo: "\uC989\uC2DC: \uC790\uC2E0\uC758 \uC0DD\uBA85\uB825\uC744 \uC989\uC2DC + 50% \uD68C\uBCF5\uD558\uACE0 \xB7 \uBC18\uACA9 \uD53C\uD574 \uBC94\uC704\uB97C + 3\uD134\uAC04 50% \uD655\uC7A5" + effectKey: heal_self_pct;guard_radius_mult + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: jiangwei_p heroId: jiangwei kind: passive - nameKo: "\uAE30\uB9B0\uC544" - hanja: "\u9E92\u9E9F\u5152" + nameKo: "\uAE30\uB9B0\uC544\uC758 \uC218\uD638" + hanja: "\u9E92\u9E9F\u5152\u5B88\u8B77" trigger: always cooldown: 0 lvMax: 5 - descKo: "\uACAC\uC81C \uD53C\uD574 \xD71.3" - effectKey: overwatch_dmg_mult - p1: 1.3 + descKo: "\uC790\uC2E0\uC758 \uBC18\uACA9 \uD53C\uD574\uAC00 50% \uC99D\uAC00\uD558\uACE0 + \xB7 \uC790\uC2E0\uC758 \uBC18\uACA9 \uBC94\uC704 \uB0B4 \uC544\uAD70\uC774 + \uACF5\uACA9 \uBC1B\uC744 \uACBD\uC6B0 \uC790\uC2E0\uC774 \uD53C\uD574\uB97C + \uB300\uC2E0 \uBC1B\uC544 \uBC18\uACA9 \uD568." + effectKey: counter_dmg_pct;guard_counter + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: liufeng_a @@ -500,28 +542,32 @@ MonoBehaviour: trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uAD00\uD1B5 \uD53C\uD574 +40 %" - effectKey: pierce_dmg_mult - p1: 1.4 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC740 185%\uC758 \uD53C\uD574\uB97C \uC785\uD788\uBA70 + \xB7 \uACBD\uB85C\uC0C1 \uD53C\uD574\uB97C \uC785\uC740 \uC801\uB4E4\uC5D0\uAC8C + 1\uD134\uAC04 \uD63C\uB780 \uD6A8\uACFC\uB97C \uC720\uBC1C\uC2DC\uD0B4." + effectKey: atk_ratio;hit_confuse + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: liufeng_p heroId: liufeng kind: passive - nameKo: "\uC0C1\uC6A9 \uC218\uBE44" - hanja: "\u4E0A\u5EB8\u5B88\u5099" + nameKo: "\uCD94\uACA9 \uC81C\uC555" + hanja: "\u8FFD\u64CA\u5236\u58D3" trigger: always cooldown: 0 lvMax: 5 - descKo: "\uBC1B\uB294 \uD53C\uD574 \u221210 %" - effectKey: dmg_taken_pct - p1: -0.1 + descKo: "\uD63C\uB780 \uC0C1\uD0DC\uC758 \uC801\uC744 \uACF5\uACA9\uD560 \uACBD\uC6B0 + \uBC18\uACA9 \uD53C\uD574\uB97C \uBC1B\uC9C0 \uC54A\uACE0 1\uD134\uAC04 \uAE30\uC808 + \uC2DC\uD0B4" + effectKey: counter_taken_pct;hit_stun + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: jianyong_a @@ -529,19 +575,20 @@ MonoBehaviour: kind: active nameKo: "\uC720\uC138" hanja: "\u904A\u8AAA" - trigger: instant + trigger: next_action cooldown: 4 lvMax: 5 - descKo: "\uC989\uC2DC \u2605: \uBC18\uACBD 3 \uC548 \uC801 \uC804\uC6D0 \uC774\uBC88 - \uC801 \uD134 \uAD50\uC804 \uBC18\uC751 \uC5C6\uC74C [p1 \uBC00\uAE30 0 \xB7 - p2 \uBC18\uACBD \xB7 \uAD50\uC804 \uBC18\uC751 \uC815\uC9C0\uB9CC]" - effectKey: aoe_push_fear + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC73C\uB85C \uACF5\uACA9 \uB300\uC0C1\uC774 + \uC544\uAD70\uC77C \uACBD\uC6B0 \uACF5\uACA9\uB825\uC758 80%\uB9CC\uD07C \uD68C\uBCF5\uC2DC\uD0A4\uACE0 + \xB7 \uACF5\uACA9 \uB300\uC0C1\uC774 \uC801\uC77C \uACBD\uC6B0 1\uD134\uAC04 + \uAE30\uC808 \uC2DC\uD0B4" + effectKey: ally_hit_heal;hit_stun p1: 0 - p2: 3 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: jianyong_p heroId: jianyong kind: passive @@ -550,80 +597,88 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uC790\uAE30 \uC561\uD2F0\uBE0C \uCFE8 \u22121(\uCD5C\uC18C 1) \u2605 - [p2 0=\uC790\uAE30\uB9CC \xB7 \uCD5C\uC18C 1 \xB7 \uC2E0\uADDC \uD0A4]" - effectKey: cooldown_add - p1: -1 + descKo: "\uC790\uC2E0\uC758 \uD589\uB3D9\uC73C\uB85C \uD68C\uBCF5 \uB41C \uB300\uC0C1\uC758 + \uC0C1\uD0DC\uC774\uC0C1 \uD6A8\uACFC\uB97C \uC81C\uAC70\uD560 \uC218 \uC788\uC73C\uBA70 + \xB7 \uC790\uC2E0\uC744 \uACF5\uACA9\uD55C \uB300\uC0C1 1\uAE30\uB97C 1\uD134 + \uAC04 \uAE30\uC808\uC2DC\uD0B4(\uBC1C\uB3D9 \uC2DC 3\uD134 \uD6C4 1\uD68C + \uCDA9\uC804)" + effectKey: heal_cleanse;on_attacked_stun + p1: 0 p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: sunqian_a heroId: sunqian kind: active - nameKo: "\uC0AC\uC2E0" - hanja: "\u4F7F\u81E3" - trigger: instant + nameKo: "\uD654\uCE5C" + hanja: "\u548C\u89AA" + trigger: next_action cooldown: 4 lvMax: 5 - descKo: "\uC989\uC2DC: \uC774\uBC88 \uD134 \uC544\uAD70 \uC804\uC6D0 \uC2AC\uB9C1 - \uAC70\uB9AC +20 % [p3 1=\uC544\uAD70 \uC804\uC6D0]" - effectKey: sling_dist_pct - p1: 0.2 + descKo: "\uB2E4\uC74C \uD589\uB3D9: \uB2E4\uC74C \uACF5\uACA9\uC774 \uBA85\uC911 + \uB41C \uB300\uC0C1 \uC8FC\uC704\uC5D0 \uBC94\uC704 \uB0B4 \uBAA8\uB4E0 \uC720\uB2DB(\uC801\uAD70 + \uD3EC\uD568)\uC758 \uC0DD\uBA85\uB825\uC744 \uACF5\uACA9\uB825\uC758 60%\uB9CC\uD07C + \uD68C\uBCF5" + effectKey: splash_heal + p1: 0 p2: 0 - p3: 1 - pMask: 7 + p3: 0 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: sunqian_p heroId: sunqian kind: passive - nameKo: "\uC5F0\uB77D" - hanja: "\u9023\u7D61" + nameKo: "\uD5A5\uB3C4" + hanja: "\u56AE\u5C0E" trigger: always cooldown: 0 lvMax: 5 - descKo: "\uC544\uAD70 \uAD81\uBCD1\xB7\uCC45\uC0AC \uC0AC\uAC70\uB9AC +0.3 [\uB300\uC0C1 - = \uAD81\uBCD1\xB7\uCC45\uC0AC]" - effectKey: team_range_add - p1: 0.3 + descKo: "\uC790\uC2E0 \uC8FC\uC704 3m \uBC18\uACBD \uB0B4 \uC544\uAD70\uC758 + \uD589\uB3D9 \uBC18\uACBD\uC744 30% \uC99D\uAC00" + effectKey: sling_dist_pct + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: mizhu_a heroId: mizhu kind: active - nameKo: "\uAC70\uBD80\uC758 \uD654\uC0B4" - hanja: "\u5DE8\u5BCC\u4E4B\u77E2" - trigger: next_action + nameKo: "\uD589\uC9C4\uAC00" + hanja: + trigger: instant cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uBA85\uC911 \uC5EC\uBD80\uC640 \uBB34\uAD00\uD558\uAC8C - \uD06C\uB9AC" - effectKey: arrow_crit_always - p1: 1 + descKo: "\uC989\uC2DC: 1\uD134 \uAC04 \uD589\uB3D9 \uBC94\uC704 \uB0B4 \uC544\uAD70\uC758 + \uC774\uB3D9\uB825\uC774 50% \uC0C1\uC2B9\uD558\uBA70 \xB7 \uBC1B\uB294 \uBC18\uACA9 + \uD53C\uD574\uAC00 50% \uBC18\uAC10 \uB428" + effectKey: sling_dist_pct;counter_taken_pct + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: mizhu_p heroId: mizhu kind: passive - nameKo: "\uC7AC\uB825" - hanja: "\u8CA1\u529B" + nameKo: "\uC9C0\uC6D0" + hanja: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uCD5C\uB300 HP +15 %" - effectKey: hp_max_pct - p1: 0.15 + descKo: "\uC790\uC2E0\uC758 \uACF5\uACA9 \uB300\uC0C1\uC774 \uC544\uAD70\uC77C + \uACBD\uC6B0 1\uD134\uAC04 \uC774\uB3D9\uB825\uACFC \uACF5\uACA9\uB825 20% + \uC99D\uAC00" + effectKey: ally_hit_buff + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: caocao_a @@ -631,16 +686,20 @@ MonoBehaviour: kind: active nameKo: "\uB09C\uC138\uC758 \uAC04\uC6C5" hanja: - trigger: instant + trigger: next_action cooldown: 4 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 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC740 \uC544\uAD70\uC744 \uD1B5\uACFC\uD558\uBA70 + \xB7 \uACF5\uACA9\uC5D0 \uBA85\uC911 \uB41C \uC801\uC774 \uC788\uC744 \uACBD\uC6B0 + \uD53C\uD574\uB97C \uC785\uD78C \uD6C4 \uBC00\uB824\uB098\uC9C0 \uC54A\uACE0 + \uC6D0\uD558\uB294 \uBC29\uD5A5\uC73C\uB85C \uBC00\uCCD0\uB0BC \uC218 \uC788\uC74C + (\uC2AC\uB9C1\uC0F7 \uB3D9\uC791\uC744 \uD55C\uBC88 \uB354 \uC870\uC791\uD574 + \uC720\uB2DB\uC744 \uBC00\uCE60 \uC218 \uC788\uC74C)" + effectKey: pass_allies;aim_push + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: caocao_p @@ -651,47 +710,55 @@ MonoBehaviour: trigger: always cooldown: 0 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 + descKo: "\uBAA8\uB4E0 \uC704 \uC9C4\uC601 \uC720\uB2DB \uACF5\uACA9\uB825 +20% + \uC99D\uAC00\uD558\uACE0 \xB7 \uC790\uC2E0\uC740 \uC0C1\uC131 \uD6A8\uACFC\uB97C + \uBB34\uC2DC\uD558\uBA70 \xB7 \uC544\uAD70\uC774 \uC0AC\uB9DD\uD560 \uB54C\uB9C8\uB2E4 + \uB0A8\uC740 \uC544\uAD70\uC758 \uACF5\uACA9\uB825\uC774 + 10%\uC529 \uC99D\uAC00" + effectKey: atk_pct;matchup_floor;on_ally_death_atk + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: xiahoudun_a heroId: xiahoudun kind: active - nameKo: "\uBC1C\uC2DC\uB2F4\uC815" - hanja: "\u62D4\u77E2\u5556\u775B" + nameKo: "\uB9F9\uC7A5" + hanja: "\u731B\u5C07" trigger: next_action cooldown: 3 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 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC740 \uC774\uB3D9 \uAC70\uB9AC\uAC00 50% + \uC99D\uAC00\uD558\uACE0 \xB7 \uD589\uB3D9 \uAC70\uB9AC\uC5D0 \uC0C1\uAD00 + \uC5C6\uC774 \uC6D0\uD558\uB294 \uBC29\uD5A5\uC73C\uB85C \uBCF5\uADC0\uD560 + \uC218 \uC788\uC74C. (\uC2AC\uB9C1\uC0F7 \uC870\uC791\uC73C\uB85C \uBCF5\uADC0)" + effectKey: sling_dist_pct;return_aim + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: xiahoudun_p heroId: xiahoudun kind: passive - nameKo: "\uB9F9\uC7A5" - hanja: "\u731B\u5C07" + nameKo: "\uD68C\uAD70 \uB3CC\uD30C" + hanja: "\u56DE\u8ECD\u7A81\u7834" trigger: always cooldown: 0 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 + descKo: "\uBAA8\uB4E0 \uC704\uAD70 \uC9C4\uC601\uC758 \uAE30\uBCD1 \uD53C\uD574\uAC00 + 50% \uC99D\uAC00\uD558\uACE0 \xB7 \uACF5\uACA9 \uD6C4 \uBCF5\uADC0 \uACBD\uB85C\uC5D0 + \uC720\uB2DB\uC774 \uC788\uB294 \uACBD\uC6B0 \uBCF5\uADC0 \uACBD\uB85C\uB85C + \uC801\uC744 \uBC00\uCCD0\uB0B4\uBA70 \uD568\uAED8 \uC6D0\uB798 \uC704\uCE58\uB85C + \uBCF5\uADC0(\uC544\uAD70 \uD3EC\uD568)\uD558\uACE0 \xB7 \uC790\uC2E0\uC740 + \uC0C1\uC131 \uD6A8\uACFC\uB97C \uBB34\uC2DC" + effectKey: dmg_pct;return_on_hit;matchup_floor + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: xuchu_a @@ -702,13 +769,14 @@ MonoBehaviour: trigger: next_action cooldown: 3 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 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC740 \uC790\uC2E0\uC758 \uD295\uACA8\uC9C0\uB294 + \uAC70\uB9AC\uAC00 2\uBC30\uB85C \uC99D\uAC00\uD558\uACE0 \xB7 \uC790\uC2E0\uC5D0\uAC8C + \uBA85\uC911 \uB41C \uBAA8\uB4E0 \uC801\uC744 1\uD134\uAC04 \uAE30\uC808 \uC2DC\uD0B4" + effectKey: self_bounce_mult;hit_stun p1: 0 - p2: 1.5 - p3: 1 - pMask: 7 + p2: 0 + p3: 0 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: xuchu_p @@ -719,47 +787,52 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uBC00\uB9AC\uB294 \uAC70\uB9AC \u221250 %" - effectKey: knockback_taken_pct - p1: -0.5 + descKo: "\uCD5C\uB300 \uC0DD\uBA85\uB825\uC774 50% \uC99D\uAC00\uD558\uACE0 \xB7 + \uC801 \uACF5\uACA9\uC5D0 \uBC00\uCCD0\uC9C0\uC9C0 \uC54A\uC73C\uBA70 \xB7 + \uB9E4\uD134 \uC0DD\uBA85\uB825\uC774 5%\uC529 \uD68C\uBCF5" + effectKey: hp_max_pct;knockback_taken_pct;regen_pct + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: zhangliao_a heroId: zhangliao kind: active - nameKo: "\uC704\uC9C4\uC18C\uC694" - hanja: "\u5A01\u9707\u900D\u9059" + nameKo: "\uC694\uB798" + hanja: "\u907C\u4F86" trigger: next_action cooldown: 4 lvMax: 5 - 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 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC73C\uB85C \uACBD\uB85C\uC0C1 \uBA85\uC911 + \uB41C \uC801\uC774 \uC788\uC744 \uACBD\uC6B0 \uD53C\uD574\uB97C \uC785\uD78C + \uD6C4 \uC790\uC2E0\uC774 \uC788\uB358 \uC704\uCE58\uB85C \uB04C\uACE0 \uC628 + \uD6C4 1\uD134\uAC04 \uD63C\uB780 \uC0C1\uD0DC\uC5D0 \uBE60\uB728\uB9BC(\uD63C\uB780 + : \uC774\uB3D9\uB825 50% \uAC10\uC18C)" + effectKey: drag_back;hit_confuse + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: zhangliao_p heroId: zhangliao kind: passive - nameKo: "\uD314\uBC31 \uB3CC\uACA9" - hanja: + nameKo: "\uC704\uC9C4\uC18C\uC694" + hanja: "\u5A01\u9707\u900D\u9059" trigger: always cooldown: 0 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 + descKo: "\uC0C1\uD0DC \uC774\uC0C1\uC5D0 \uAC78\uB9B0 \uC801 \uACF5\uACA9 \uC2DC + \uACF5\uACA9\uB825 50% \uC99D\uAC00\uD558\uACE0 \xB7 \uBC18\uACA9 \uD53C\uD574\uB97C + \uBC1B\uC9C0 \uC54A\uC74C" + effectKey: atk_pct;counter_taken_pct + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: xuhuang_a @@ -767,32 +840,43 @@ MonoBehaviour: kind: active nameKo: "\uB300\uBD80" hanja: "\u5927\u65A7" - trigger: next_action + trigger: instant cooldown: 3 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 + descKo: "\uC989\uC2DC: 3\uD134 \uAC04 \uC790\uC2E0 \uC8FC\uC704\uB97C \uB9F4\uB3C4\uB294 + \uB3C4\uB07C\uB97C \uC0DD\uC131\uD574 \uC774\uB3D9 \uACBD\uB85C\uC0C1 \uB3C4\uB07C + \uBC94\uC704\uC5D0 \uB2FF\uAC70\uB098 \uC790\uC2E0\uC744 \uACF5\uACA9\uD55C + \uB300\uC0C1\uC5D0\uAC8C \uACF5\uACA9\uB825\uC758 80%\uC758 \uD53C\uD574\uB97C + \uCD94\uAC00\uB85C \uC785\uD788\uACE0 \xB7 \uB3C4\uB07C\uAC00 \uD68C\uC804\uD560 + \uB54C \uC790\uC2E0\uC744 \uACF5\uACA9\uD55C \uB300\uC0C1\uC744 2\uBC30 \uBA40\uB9AC + \uD295\uACA8\uB098\uAC8C \uD568" + effectKey: axe_aura + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: xuhuang_p heroId: xuhuang kind: passive - nameKo: "\uAD70\uC728" - hanja: "\u8ECD\u5F8B" + nameKo: "\uD608\uBD80" + hanja: "\u8840\u65A7" trigger: always cooldown: 0 lvMax: 5 - descKo: "\uBC1B\uB294 \uBC18\uACA9 \uD53C\uD574 \u221230 %" - effectKey: counter_taken_pct - p1: -0.3 + descKo: "\uACF5\uACA9 \uB300\uC0C1\uC5D0\uAC8C 30% \uD655\uB960\uB85C 3\uD134\uAC04 + \uC9C0\uC18D\uD558\uB294 \uCD9C\uD608 \uD6A8\uACFC\uB97C \uBD80\uC5EC(\uCD9C\uD608 + : \uC774\uB3D9 \uAC70\uB9AC\uC758 50%\uC5D0 \uBE44\uB840\uD55C \uC0DD\uBA85\uB825 + \uAC10\uC18C)\uD558\uACE0 \xB7 \uB300\uC0C1\uC5D0\uAC8C \uCE58\uBA85\uD0C0 + \uBC1C\uC0DD \uC2DC 100% \uD655\uB960\uB85C \uCD9C\uD608 \uD6A8\uACFC\uB97C + \uBD80\uC5EC (\uC561\uD2F0\uBE0C \uC2A4\uD0AC - \uD68C\uC804 \uB3C4\uB07C\uC5D0\uB3C4 + \uB3D9\uC77C\uD55C \uD6A8\uACFC)" + effectKey: hit_bleed + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: zhanghe_a @@ -803,15 +887,17 @@ MonoBehaviour: trigger: instant cooldown: 3 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 + descKo: "\uC989\uC2DC: \uBC18\uACBD 5m \uB0B4 \uBAA8\uB4E0 \uB300\uC0C1\uC744 + \uBB34\uC2DC\uD558\uACE0 \uC774\uB3D9 \uAC00\uB2A5(\uC2AC\uB9C1\uC0F7 \uC870\uC791\uC73C\uB85C + \uC6D0\uD558\uB294 \uC774\uB3D9 \uC704\uCE58 \uACB0\uC815)\uD558\uACE0 \xB7 + \uC774\uB3D9 \uD6C4 \uC7AC\uD589\uB3D9 \uAC00\uB2A5" + effectKey: teleport;extra_action + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: zhanghe_p heroId: zhanghe kind: passive @@ -820,12 +906,17 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uACAC\uC81C \uD53C\uD574 \xD71.5" - effectKey: overwatch_dmg_mult - p1: 1.5 + descKo: "\uC544\uAD70\uC774\uB098 \uC801\uC744 \uBBF8\uB294 \uAC70\uB9AC\uAC00 + 2\uBC30 \uC99D\uAC00\uD558\uACE0 \xB7 \uC544\uAD70\uC744 \uBC00\uCCD0 \uC801\uC744 + \uACF5\uACA9\uD55C \uACBD\uC6B0 \uC790\uC2E0\uC758 \uACF5\uACA9\uB825 \uB9CC\uD07C + \uC544\uAD70\uC758 \uD53C\uD574\uAC00 \uC99D\uAC00\uD558\uACE0 \xB7 \uC790\uC2E0\uC774 + \uBC00\uCCD0\uC838 \uC801\uC744 \uACF5\uACA9\uD55C \uACBD\uC6B0\uC5D0\uB294 + 2\uBC30\uC758 \uD53C\uD574\uB97C \uC785\uD798" + effectKey: push_mult;pushed_ally_atk_add;self_pushed_mult + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: caoren_a @@ -836,16 +927,18 @@ MonoBehaviour: trigger: instant cooldown: 3 lvMax: 5 - 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 + descKo: "\uC989\uC2DC: 3\uD134\uAC04 \uC790\uC2E0\uC758 \uD589\uB3D9 \uBC18\uACBD + \uB0B4 \uBAA8\uB4E0 \uC544\uAD70\uC758 \uBC1B\uB294 \uD53C\uD574\uAC00 30% + \uAC10\uC18C\uB418\uBA70 \xB7 \uC790\uC2E0\uC758 \uD589\uB3D9 \uBC18\uACBD\uC744 + \uC9C0\uB098\uAC00\uB294 \uC801\uB4E4\uC758 \uC774\uB3D9\uC18C\uBAA8\uB7C9\uC774 + 50% \uC99D\uAC00" + effectKey: dmg_taken_pct;zone_slow + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: caoren_p heroId: caoren kind: passive @@ -854,13 +947,16 @@ MonoBehaviour: trigger: always cooldown: 0 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 + descKo: "\uBAA8\uB4E0 \uC704\uAD70 \uC0DD\uBA85\uB825\uC774 20% \uC99D\uAC00\uD558\uACE0 + \xB7 \uD589\uB3D9\uC744 \uBA48\uCD9C \uB54C\uB9C8\uB2E4 \uC0DD\uBA85\uB825\uC774 + 20% \uD68C\uBCF5\uB418\uBA70 \xB7 \uB0A8\uC740 \uC774\uB3D9\uAC70\uB9AC\uC758 + 50%\uB9CC\uD07C \uBC1B\uB294 \uD53C\uD574\uB97C \uCC28\uAC10(\uC774\uB3D9\uD558\uC9C0 + \uC54A\uC558\uC744 \uACBD\uC6B0 \xB7 \uCD5C\uB300 50%\uC758 \uD53C\uD574 \uCC28\uAC10)" + effectKey: hp_max_pct;heal_on_action_end;unused_move_dr + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: yujin_a @@ -871,13 +967,15 @@ MonoBehaviour: trigger: next_action cooldown: 3 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 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC73C\uB85C \uD589\uB3D9 \uC885\uB8CC\uD55C + \uC544\uAD70\uC774 \uBA85\uC911 \uB41C \uACBD\uC6B0 \xB7 \uC7AC\uD589\uB3D9 + \uAC00\uB2A5(\uB2E4\uC218 \uC544\uAD70 \uBA85\uC911 \uC2DC \uBAA8\uB450 \uC7AC\uD589\uB3D9 + \uAC00\uB2A5)" + effectKey: ally_hit_reaction + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: yujin_p @@ -888,79 +986,90 @@ MonoBehaviour: trigger: always cooldown: 0 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 + descKo: "\uC774\uBC88\uD134\uC5D0 \uC774\uB3D9\uD558\uC9C0 \uC54A\uC558\uB2E4\uBA74 + \uCD5C\uB300 2\uD134 \uAC70\uB9AC\uC758 \uC774\uB3D9\uB825\uC744 \uCD95\uC801 + \uAC00\uB2A5" + effectKey: move_bank + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: simayi_a heroId: simayi kind: active - nameKo: "\uC740\uC778\uC790\uC911" - hanja: "\u96B1\u5FCD\u81EA\u91CD" + nameKo: "\uACE0\uD3C9\uB989\uC758 \uBCC0" + hanja: "\u9AD8\u5E73\u9675\u4E4B\u8B8A" trigger: instant cooldown: 4 lvMax: 5 - descKo: "\uC989\uC2DC: \uC774\uBC88 \uC801 \uD134 \uC544\uAD70 \uC804\uC6D0 \uBC1B\uB294 - \uD53C\uD574 \u221240 %" - effectKey: team_dmg_taken_pct_turn - p1: -0.4 + descKo: "\uC989\uC2DC: \uD589\uB3D9 \uBC18\uACBD \uBAA8\uB4E0 \uC801\uC5D0\uAC8C + \uACF5\uACA9\uB825\uC758 150% \uB9CC\uD07C\uC758 \uD53C\uD574\uB97C \uC785\uD788\uACE0 + \xB7 \uC785\uD78C \uD53C\uD574\uC758 60% \uB9CC\uD07C \uC790\uC2E0\uC744 \uC81C\uC678\uD55C + \uBC94\uC704 \uB0B4 \uBAA8\uB4E0 \uC544\uAD70\uC744 \uCE58\uC720" + effectKey: aoe_drain + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: simayi_p heroId: simayi kind: passive - nameKo: "\uC751\uBCC0" - hanja: "\u61C9\u8B8A" + nameKo: "\uC751\uC2DC\uB0AD\uACE0" + hanja: "\u9DF9\u8996\u72FC\u9867" trigger: always cooldown: 0 lvMax: 5 - descKo: "\uC0AC\uAC70\uB9AC +0.5 \xB7 \uAD11\uC5ED \uBC18\uACBD \xD71.2" - effectKey: range_add;splash_radius_mult - p1: 0.5 - p2: 1.2 + descKo: "\uBAA8\uB4E0 \uC704\uAD70 \uCC45\uC0AC\uC758 \uAD11\uC5ED \uD53C\uD574 + \uBC94\uC704\uAC00 50% \uC99D\uAC00\uD558\uACE0 \xB7 \uC790\uC2E0\uC758 \uC0AC\uAC70\uB9AC\uAC00 + 50% \uC99D\uAC00\uD558\uBA70 \xB7 \uD589\uB3D9 \uBC18\uACBD \uB0B4 \uC801\uC774 + \uC785\uC740 \uD53C\uD574\uC758 30% \uB9CC\uD07C \uC790\uC2E0\uC758 \uC0DD\uBA85\uB825\uC744 + \uD68C\uBCF5" + effectKey: splash_radius_mult;range_mult;drain_aura + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: xiahouyuan_a heroId: xiahouyuan kind: active - nameKo: "\uC2E0\uC18D" - hanja: "\u795E\u901F" + nameKo: "\uC804\uAD11 \uC5F0\uC0AC" + hanja: "\u96FB\u5149\u9023\u5C04" trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uC0AC\uAC70\uB9AC \xD71.5 \xB7 \uBA85\uC911 - \uC5EC\uBD80\uC640 \uBB34\uAD00\uD558\uAC8C \uD06C\uB9AC" - effectKey: range_mult;arrow_crit_always - p1: 1.5 - p2: 1 + descKo: "\uB2E4\uC74C \uD589\uB3D9 \uC2DC \uC81C\uC790\uB9AC\uC5D0\uC11C \uBC29\uC0AC\uD615(\uBD80\uCC44\uAF34)\uC73C\uB85C + \uC9C0\uC815\uD55C \uBC29\uD5A5\uC73C\uB85C \uD654\uC0B4\uC744 5\uBC1C\uC529 + 3\uD68C \uC5F0\uC18D \uBC1C\uB3D9" + effectKey: shot_fan + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: xiahouyuan_p heroId: xiahouyuan kind: passive - nameKo: "\uC0BC\uC77C \uC624\uBC31\uB9AC" - hanja: "\u4E09\u65E5\u4E94\u767E" + nameKo: "\uC0BC\uC5F0\uC2DC" + hanja: "\u4E09\u9023\u77E2" trigger: always cooldown: 0 lvMax: 5 - descKo: "\uC2AC\uB9C1 \uAC70\uB9AC +20 %" - effectKey: sling_dist_pct - p1: 0.2 + descKo: "\uC6D0\uAC70\uB9AC \uACF5\uACA9\uC73C\uB85C \uB300\uC0C1\uC744 \uD0C0\uAC9F\uD55C + \uACBD\uC6B0 \uBC29\uC0AC\uD615\uC73C\uB85C 3\uBC1C\uC758 \uD654\uC0B4\uC744 + \uB3D9\uC2DC\uC5D0 \uBC1C\uC0AC\uD574 \uACBD\uB85C \uC0C1 \uC720\uB2DB\uC5D0\uAC8C + \uD53C\uD574\uB97C \uC785\uD798(\uBA40\uD2F0\uC0F7)" + effectKey: multishot + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: guojia_a @@ -971,15 +1080,18 @@ MonoBehaviour: trigger: instant cooldown: 4 lvMax: 5 - descKo: "\uC989\uC2DC \u2605: \uC774\uBC88 \uD134 \uC544\uAD70 \uC804\uC6D0 \uAC00\uD558\uB294 - \uD53C\uD574 +25 % [\uC2E0\uADDC \uD0A4]" - effectKey: team_dmg_pct_turn - p1: 0.25 + descKo: "\uC989\uC2DC: \uD589\uB3D9 \uBC94\uC704 \uB0B4 \uC544\uAD70 \uC804\uC6D0\uC774 + \uC785\uD788\uB294 \uD53C\uD574\uB7C9\uC774 2\uD134\uAC04 30% \uC99D\uAC00 + \uD558\uACE0 \xB7 \uC784\uC758\uC758 \uC544\uAD70\uC5D0\uAC8C \uC7AC\uD589\uB3D9 + \uAE30\uD68C 1\uD68C \uBD80\uC5EC(\uC774\uBBF8 \uD589\uB3D9 \uC885\uB8CC\uD55C + \uC544\uAD70\uC5D0\uAC8C\uB294 \uC7AC\uD589\uB3D9 \uAE30\uD68C \uBD80\uC5EC)" + effectKey: dmg_pct;grant_reaction + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: guojia_p heroId: guojia kind: passive @@ -988,16 +1100,16 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uC790\uAE30 \uC561\uD2F0\uBE0C \uCFE8 \u22121(\uCD5C\uC18C 1) \u2605 - [p1 \uCFE8 \uAC00\uC0B0 \xB7 p2 0=\uC790\uAE30\uB9CC \xB7 \uCD5C\uC18C 1 \xB7 - \uC2E0\uADDC \uD0A4]" - effectKey: cooldown_add - p1: -1 + descKo: "\uC790\uC2E0\uC758 \uD589\uB3D9 \uBC94\uC704 \uB0B4 \uC801\uC744 \uCC98\uCE58\uD55C + \uC544\uAD70\uC740 \uC2A4\uD0AC \uCFE8\uD0C0\uC784\uC774 1\uCD08 \uAC10\uC18C\uD558\uACE0 + \xB7 \uCD5C\uB300 \uC0DD\uBA85\uB825\uC758 30%\uB9CC\uD07C \uD68C\uBCF5" + effectKey: kill_in_range_reward + p1: 0 p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: jiaxu_a heroId: jiaxu kind: active @@ -1006,15 +1118,18 @@ MonoBehaviour: trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uAD11\uC5ED \uC544\uAD70 \uD53C\uD574 0 - \xB7 \uAD11\uC5ED \uBE44\uC728 1.0(\uAD11\uC5ED)" - effectKey: splash_ally_ratio;splash_ratio + descKo: "\uB2E4\uC74C \uD589\uB3D9 \uC2DC \uC9C0\uC815\uD55C \uB300\uC0C1 \uBC94\uC704\uC5D0 + \uB3C5\uAD6C\uB984\uC744 \uC0DD\uC131\uD574 \uACF5\uACA9\uB825\uC758 220%\uC758 + \uD53C\uD574\uB97C \uC785\uD788\uACE0 \xB7 5\uD134\uAC04 \uC911\uB3C5 \uC0C1\uD0DC\uB85C + \uB9CC\uB4EC (\uC911\uB3C5 : \uB9E4\uD134 \uC2DC\uC791\uACFC \uD589\uB3D9 \uC885\uB8CC + \uC2DC\uC810 \uB9C8\uB2E4 \uCD5C\uB300 \uC0DD\uBA85\uB825\uC758 2%\uC529 \uAC10\uC18C)" + effectKey: shot_blast;hit_poison p1: 0 - p2: 1 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: jiaxu_p heroId: jiaxu kind: passive @@ -1023,12 +1138,14 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uBCF4\uC2A4\uC5D0\uAC8C \uC8FC\uB294 \uD53C\uD574 +20 % [p2 1=\uBCF4\uC2A4\uB9CC]" - effectKey: dmg_vs_general_mult - p1: 1.2 - p2: 1 + descKo: "\uC790\uC2E0\uC758 \uACF5\uACA9\uC5D0 \uB2F9\uD55C \uC801\uC740 \uD56D\uC0C1 + \uC911\uB3C5 \uC0C1\uD0DC\uAC00 \uB418\uBA70 \xB7 \uC911\uB3C5 \uD6A8\uACFC\uAC00 + \uCD5C\uB300 5\uD68C\uAE4C\uC9C0 \uC911\uCCA9\uB420 \uC218 \uC788\uC74C" + effectKey: hit_poison;poison_stack + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: dianwei_a @@ -1039,13 +1156,14 @@ MonoBehaviour: trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uC54C\uAE4C\uAE30 \uD53C\uD574 +60 % \xB7 - \uC790\uAE30 \uD295\uAE40 0" - effectKey: dmg_pct;self_bounce_mult - p1: 0.6 + descKo: "\uB2E4\uC74C \uD589\uB3D9 \uC2DC \uC785\uD788\uB294 \uD53C\uD574\uB7C9\uACFC + \uBC00\uB9AC\uB294 \uAC70\uB9AC\uAC00 100% \uC99D\uAC00\uD558\uBA70 \xB7 \uC790\uC2E0\uC740 + \uD295\uAE30\uC9C0 \uC54A\uC74C" + effectKey: dmg_pct;push_mult;self_bounce_mult + p1: 0 p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: dianwei_p @@ -1056,12 +1174,15 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uCD5C\uB300 HP +25 %" - effectKey: hp_max_pct - p1: 0.25 + descKo: "\uC790\uC2E0\uC758 \uBC1B\uB294 \uD53C\uD574\uAC00 20% \uAC10\uC18C\uD558\uBA70 + \xB7 \uC9C0\uD615 \uC624\uBE0C\uC81D\uD2B8\uB97C \uBC00\uCE58\uB294 \uAC70\uB9AC\uC640 + \uC9C0\uD615 \uC624\uBE0C\uC81D\uD2B8\uB97C \uC774\uC6A9\uD55C \uD53C\uD574\uB7C9\uC774 + 2\uBC30\uB85C \uC99D\uAC00" + effectKey: dmg_taken_pct;gimmick_mult + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: xunyu_a @@ -1072,14 +1193,14 @@ MonoBehaviour: trigger: instant cooldown: 4 lvMax: 5 - descKo: "\uC989\uC2DC: \uC774\uBC88 \uD134 \uC544\uAD70 \uC804\uC6D0 \uBC18\uACA9 - \uBC30\uC728 1.0 [p1 \uBC18\uACA9 \uBC30\uC728 \xB7 p3 1=\uC544\uAD70 \uC804\uC6D0 - \xB7 \uC774\uBC88 \uD134]" - effectKey: counter_ratio - p1: 1 + descKo: "\uC989\uC2DC: \uD589\uB3D9 \uBC94\uC704 \uB0B4 \uC544\uAD70\uC774 1\uD134\uAC04 + \uBC18\uACA9 \uBC1B\uC9C0 \uC54A\uAC8C \uB418\uBA70 \xB7 \uACF5\uACA9\uB825\uC774 + 30% \uC99D\uAC00" + effectKey: counter_taken_pct;atk_pct + p1: 0 p2: 0 - p3: 1 - pMask: 7 + p3: 0 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: xunyu_p @@ -1090,12 +1211,15 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uC544\uAD70 \uC804\uC6D0 \uCD5C\uB300 HP +10 % [p2 1=\uC544\uAD70 \uC804\uC6D0]" - effectKey: hp_max_pct - p1: 0.1 - p2: 1 + descKo: "\uC790\uC2E0\uC758 \uACF5\uACA9 \uB300\uC0C1\uC774 \uC544\uAD70\uC77C + \uACBD\uC6B0 \xB7 \uACF5\uACA9\uB825\uC758 50%\uB9CC\uD07C \uC0DD\uBA85\uB825\uC744 + \uD68C\uBCF5\uC2DC\uD0A4\uBA70 \xB7 1\uD134\uAC04 \uACF5\uACA9\uB825 15% \uC99D\uAC00 + \uBC84\uD504\uD6A8\uACFC \uBD80\uC5EC" + effectKey: ally_hit_heal;ally_hit_buff + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: xiahoumao_a @@ -1106,13 +1230,13 @@ MonoBehaviour: trigger: instant cooldown: 3 lvMax: 5 - descKo: "\uC989\uC2DC: \uC774\uBC88 \uC801 \uD134 \uC790\uAE30 \uBC1B\uB294 \uD53C\uD574 - \u221230 % [\uC989\uC2DC \xB7 \uC774\uBC88 \uC801 \uD134 \xB7 \uC790\uAE30\uB9CC]" + descKo: "\uC989\uC2DC: 1\uD134 \uAC04 \uC790\uC2E0\uC774 \uBC1B\uB294 \uD53C\uD574 + -30 %" effectKey: dmg_taken_pct - p1: -0.3 + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: xiahoumao_p @@ -1123,12 +1247,12 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uAD00\uD1B5\uC774 \uBE57\uB098\uAC00\uB3C4 \uCD9C\uBC1C\uC810 \uBCF5\uADC0" - effectKey: pierce_return_on_miss - p1: 1 + descKo: "\uC790\uC2E0\uC758 \uC774\uB3D9\uB825 +10%" + effectKey: sling_dist_pct + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: wanglang_a @@ -1136,20 +1260,20 @@ MonoBehaviour: kind: active nameKo: "\uC124\uC804" hanja: "\u820C\u6230" - trigger: instant + trigger: next_action cooldown: 5 lvMax: 5 - descKo: "\uC989\uC2DC \u2605: \uBC18\uACBD 4 \uC548 \uAC00\uC7A5 \uAC00\uAE4C\uC6B4 - \uC801 \uBB34\uC7A5 1\uAE30 \u2014 \uB2E4\uC74C \uC801 \uD134 \uD589\uB3D9 - \uBD88\uAC00 [p1 \uBC18\uACBD \xB7 p2 \uD134 \xB7 \uBB34\uC7A5 \uC6B0\uC120 - \xB7 \uC2E0\uADDC \uD0A4]" - effectKey: charm - p1: 4 - p2: 1 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC740 \uC9C1\uC120 \uBC94\uC704 \uB0B4 \uB300\uC0C1\uC5D0\uAC8C + 180%\uC758 \uD53C\uD574\uB97C \uC785\uD788\uBA70 \xB7 1\uD134\uAC04 \uD63C\uB780 + \uD6A8\uACFC\uB97C \uC720\uBC1C(\uC544\uAD70\uC774 \uBA85\uC911\uD560 \uACBD\uC6B0 + \uC544\uAD70\uC5D0 \uD53C\uD574)" + effectKey: shot_pierce;hit_confuse + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: wanglang_p heroId: wanglang kind: passive @@ -1158,28 +1282,29 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uCD5C\uB300 HP +10 %" - effectKey: hp_max_pct - p1: 0.1 + descKo: "\uCD5C\uB300 \uC0AC\uAC70\uB9AC+10%" + effectKey: range_mult + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: xiahouen_a heroId: xiahouen kind: active - nameKo: "\uCCAD\uAC15\uAC80" - hanja: "\u9751\u91ED\u528D" + nameKo: "\uCCAD\uAC15\uAC80 \uBC1C\uB3C4" + hanja: "\u9751\u91ED\u528D\u62D4\u5200" trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uAD00\uD1B5 \uD53C\uD574 +40 %" - effectKey: pierce_dmg_mult - p1: 1.4 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC73C\uB85C \uCD5C\uCD08 \uBA85\uC911 \uB41C + \uB300\uC0C1\uC5D0\uAC8C \uACF5\uACA9\uB825\uC758 50% \uD53C\uD574 \uCD94\uAC00" + effectKey: first_hit_bonus + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: xiahouen_p @@ -1190,12 +1315,12 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uC2AC\uB9C1 \uAC70\uB9AC +10 %" - effectKey: sling_dist_pct - p1: 0.1 + descKo: "\uCD5C\uB300 \uC0DD\uBA85\uB825 +10%" + effectKey: hp_max_pct + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: sunjian_a @@ -1206,13 +1331,18 @@ MonoBehaviour: trigger: next_action cooldown: 3 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 + descKo: "\uB2E4\uC74C \uD589\uB3D9 \uC2DC \uACF5\uACA9\uB825\uC774 50% \uC99D\uAC00\uD558\uACE0 + \xB7 \uBAA8\uB4E0 \uC720\uB2DB\uC744 \uD1B5\uACFC\uD558\uBA70 \uACF5\uACA9\uD560 + \uC218 \uC788\uC73C\uBA70 \uACBD\uB85C \uC0C1 \uD53C\uACA9 \uB41C \uBAA8\uB4E0 + \uC801\uC5D0\uAC8C \uCD9C\uD608 \uD53C\uD574\uB97C \uC785\uD798 \xB7 \uD589\uB3D9 + \uC885\uB8CC \uD6C4 1\uD134\uAC04 \uBAA8\uB4E0 \uD53C\uD574\uB97C \uBB34\uD6A8\uD654\uD568(\uD295\uACA8\uC9C8 + \uC21C \uC788\uACE0 \xB7 \uBC18\uACA9 \uD53C\uD574\uB294 \uC785\uD790 \uC218 + \uC788\uC74C)" + effectKey: atk_pct;pierce_through;hit_bleed;invuln + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: sunjian_p @@ -1223,32 +1353,40 @@ MonoBehaviour: trigger: always cooldown: 0 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 + descKo: "\uBAA8\uB4E0 \uC624\uAD70 \uC9C4\uC601 \uACF5\uACA9\uB825 +20% \uC99D\uAC00\uD558\uACE0 + \xB7 \uC790\uC2E0\uC758 \uACF5\uACA9\uC73C\uB85C \uC801\uC744 \uCC98\uCE58\uD560 + \uACBD\uC6B0 \uC7AC\uD589\uB3D9 \uAE30\uD68C 1\uD68C \uBD80\uC5EC(\uD134 \uB2F9 + 1\uD68C)" + effectKey: atk_pct;kill_reaction + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: zhouyu_a heroId: zhouyu kind: active - nameKo: "\uD654\uACF5" - hanja: "\u706B\u653B" + nameKo: "\uC801\uBCBD\uC758 \uBD88\uBE44" + hanja: "\u8D64\u58C1\u706B\u96E8" trigger: next_action cooldown: 4 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 + descKo: "\uB2E4\uC74C \uD589\uB3D9 \uC2DC \uBA85\uC911\uD55C \uB300\uC0C1 \uC8FC\uC704\uC5D0 + \uC6B4\uC11D\uC744 \uC18C\uD658\uD574 \uB113\uC740 \uBC94\uC704\uC5D0 \uACF5\uACA9\uB825\uC758 + 350%\uC758 \uD53C\uD574\uB97C \uC785\uD788\uACE0 \xB7 \uD53C\uACA9 \uB41C \uB300\uC0C1\uC744 + \uBD88\uD0DC\uC6B0\uAE30 \uC0C1\uD0DC\uC5D0 \uBE60\uB728\uB9AC\uBA70 \xB7 \uC6B4\uC11D\uC774 + \uB099\uD558\uD55C \uC7A5\uC18C\uC5D0 \uBD88\uAF43\uC744 \uC0DD\uC131(\uB2FF\uB294 + \uC801\uC5D0\uAC8C \uC989\uC2DC \uACF5\uACA9\uB825\uC758 100%\uC758 \uD53C\uD574\uB97C + \uC785\uD788\uACE0 \xB7 3\uD134\uAC04 \uC720\uC9C0\uB418\uB294 \uD654\uC0C1 + \uD6A8\uACFC\uB97C \uC785\uD798)" + effectKey: shot_blast;hit_burn;fire_zone + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: zhouyu_p heroId: zhouyu kind: passive @@ -1257,13 +1395,16 @@ MonoBehaviour: trigger: always cooldown: 0 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 + descKo: "\uC2A4\uD0AC \uC2DC\uC804 \uC2DC \uC0DD\uBA85\uB825\uC774 5% \uAC10\uC18C\uD558\uC9C0\uB9CC + \xB7 \uC0DD\uBA85\uC774 \uB0AE\uC544\uC9C8 \uB54C\uB9C8\uB2E4 \uC2A4\uD0AC\uC758 + \uC704\uB825\uC774 10%\uC529 \uC99D\uAC00\uD558\uBA70 \xB7 \uC790\uC2E0\uC740 + \uADFC\uC811 \uC720\uB2DB\uACFC\uC758 \uC804\uD22C\uC5D0 \uD328\uB110\uD2F0\uB97C + \uBC1B\uC9C0 \uC54A\uACE0 \xB7 \uC2A4\uD0AC \uC0AC\uAC70\uB9AC\uAC00 50% \uC99D\uAC00" + effectKey: skill_hp_cost;skill_power_low_hp;melee_penalty_off;range_mult + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: huanggai_a @@ -1274,15 +1415,18 @@ MonoBehaviour: trigger: instant cooldown: 3 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 + descKo: "\uC989\uC2DC: 1\uD134\uAC04 \uC790\uC2E0\uC758 \uBC1B\uB294 \uD53C\uD574\uAC00 + 30% \uAC10\uC18C\uB418\uBA70 \xB7 \uC790\uC2E0\uC774 \uC774\uBC88 \uD134\uC5D0 + \uBC1B\uC740 \uD53C\uD574\uAC00 \uB2E4\uC74C \uD134\uC758 \uACF5\uACA9\uB825\uC5D0 + \uCD94\uAC00\uB418\uBA70 \xB7 \uC801\uC744 \uCC98\uCE58\uD55C \uACBD\uC6B0 + \uC0DD\uBA85\uB825 100% \uD68C\uBCF5" + effectKey: dmg_taken_pct;damage_to_atk;heal_on_kill + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: huanggai_p heroId: huanggai kind: passive @@ -1291,82 +1435,90 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uBC1B\uB294 \uD53C\uD574 \u221215 %" - effectKey: dmg_taken_pct - p1: -0.15 + descKo: "\uC790\uC2E0\uC758 \uBC1B\uB294 \uD53C\uD574 -15 % \xB7 \uC0DD\uBA85\uB825\uC774 + \uB0AE\uC544\uC9C8\uC218\uB85D \uBC1B\uB294 \uD53C\uD574\uAC00 0.2%\uC529 \uC99D\uAC00\uD558\uACE0 + \uBC18\uACA9 \uD53C\uD574\uAC00 1%\uC529 \uC99D\uAC00" + effectKey: dmg_taken_pct;low_hp_taken + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: ganning_a heroId: ganning kind: active - nameKo: "\uBC31\uAE30\uC2B5\uC601" - hanja: "\u767E\u9A0E\u8972\u71DF" + nameKo: "\uAE08\uBC94 \uB3C4\uD0C4" + hanja: "\u9326\u5E06\u8DF3\u5F48" trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uC0AC\uAC70\uB9AC \uBC16\uC774\uC5B4\uB3C4 - \uBAA9\uD45C\uC810\uAE4C\uC9C0 \uC774\uB3D9 \uD6C4 \uC0AC\uACA9 \xB7 \uBA85\uC911 - \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 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC73C\uB85C \uBC1C\uC0AC \uB41C \uD654\uC0B4\uC740 + \uBB34\uC870\uAC74 \uCE58\uBA85\uD0C0\uB85C \uBA85\uC911\uB418\uBA70 \xB7 \uD53C\uACA9 + \uB41C \uB300\uC0C1\uACFC \uAC00\uC7A5 \uAC00\uAE4C\uC6B4 3\uBA85\uC758 \uC720\uB2DB(\uC544\uAD70\uD3EC\uD568)\uC5D0\uAC8C + \uACF5\uACA9\uB825\uC758 50% \uB9CC\uD07C\uC758 \uB3C4\uD0C4 \uB41C \uD654\uC0B4\uB85C + \uD53C\uD574\uB97C \uC785\uD798" + effectKey: crit_always;shot_ricochet p1: 0 - p2: 1 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: ganning_p heroId: ganning kind: passive - nameKo: "\uAE08\uBC94\uC801" - hanja: "\u9326\u5E06\u8CCA" + nameKo: "\uBC31\uAE30\uC2B5\uC601" + hanja: "\u767E\u9A0E\u8972\u71DF" trigger: always cooldown: 0 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 + descKo: "\uADFC\uC811 \uBC18\uACA9\uC744 \uBC1B\uC9C0 \uC54A\uC73C\uBA70 \xB7 + \uC544\uAD70\uC5D0\uAC8C \uC790\uC2E0\uC758 \uACF5\uACA9\uC774 \uBA85\uC911 + \uB420 \uACBD\uC6B0 \uC544\uAD70\uC758 \uC704\uCE58\uB85C \uC774\uB3D9\uD560 + \uC218 \uC788\uACE0(\uC544\uAD70 \uD53C\uD574 \uC5C6\uC74C) \uB0A8\uC740 \uD589\uB3D9 + \uBC94\uC704 \uB0B4 \uC7AC\uACF5\uACA9 \uAC00\uB2A5" + effectKey: counter_taken_pct;ally_hit_warp + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: chengpu_a heroId: chengpu kind: active - nameKo: "\uCCA0\uCC99\uC0AC\uBAA8" - hanja: "\u9435\u810A\u86C7\u77DB" + nameKo: "\uC5F0\uD658\uACA9" + hanja: "\u9023\u74B0\u64CA" trigger: next_action cooldown: 3 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 + descKo: "\uB2E4\uC74C \uD589\uB3D9 \uC2DC \uC9C1\uC804 \uD589\uB3D9 \uAC70\uB9AC\uC640 + \uBC29\uD5A5\uC73C\uB85C \uB3D9\uC77C\uD558\uAC8C \uD55C\uBC88 \uB354 \uACF5\uACA9 + \uC2DC\uB3C4" + effectKey: repeat_action + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: chengpu_p heroId: chengpu kind: passive - nameKo: "\uC0BC\uB300 \uB178\uC2E0" - hanja: + nameKo: "\uAC70\uB9C8\uCC3D" + hanja: "\u62D2\u99AC\u69CD" trigger: always cooldown: 0 lvMax: 5 - descKo: "\uACAC\uC81C \uD69F\uC218 2\uD68C" - effectKey: overwatch_count - p1: 2 + descKo: "\uBC00\uCE58\uB294 \uAC70\uB9AC\uAC00 50% \uC99D\uAC00\uD558\uACE0 \xB7 + \uC0C1\uB300 \uBCD1\uC885\uC774 \uAE30\uBCD1\uC77C \uACBD\uC6B0 \uD53C\uD574\uB7C9 + 50% \uC99D\uAC00" + effectKey: push_mult;dmg_pct + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: sunce_a @@ -1377,13 +1529,18 @@ MonoBehaviour: trigger: next_action cooldown: 4 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uAD00\uD1B5 \uD53C\uD574 \xD72 \xB7 \uBE57\uB098\uAC00\uB3C4 - \uCD9C\uBC1C\uC810 \uBCF5\uADC0" - effectKey: pierce_dmg_mult;pierce_return_on_miss - p1: 2 - p2: 1 + descKo: "\uB2E4\uC74C \uD589\uB3D9 \uC2DC \uC774\uB3D9 \uAC70\uB9AC\uAC00 2\uBC30\uAC00 + \uB418\uBA70 \uC9C0\uC815\uD55C \uBC29\uD5A5\uC73C\uB85C \uBAA8\uB4E0 \uC720\uB2DB\uC744 + \uAD00\uD1B5\uD558\uBA70 \uD53C\uD574\uB97C \uC785\uD788\uBA70 \xB7 \uBA48\uCD98 + \uD6C4 \uC6D0\uB798 \uC790\uB9AC\uB85C \uBCF5\uADC0\uD558\uBA70 \uACBD\uB85C\uC0C1 + \uBAA8\uB4E0 \uC801\uC5D0\uAC8C \uD53C\uD574\uB97C \uC785\uD798(\uD654\uBA74 + \uAC00\uC7A5\uC790\uB9AC\uC5D0 \uB2FF\uC740 \uACBD\uC6B0 \uD540\uBCFC\uCC98\uB7FC + \uD295\uACA8\uC9C0\uBA70 \uC774\uB3D9 \uAC70\uB9AC\uB97C \uC18C\uC9C4)" + effectKey: sling_dist_pct;pierce_through;return_always;wall_bounce + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: sunce_p @@ -1394,29 +1551,36 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uBC1B\uB294 \uBC18\uACA9 \uD53C\uD574 \u221250 %" - effectKey: counter_taken_pct - p1: -0.5 + descKo: "\uBAA8\uB4E0 \uC624\uAD70\uC758 \uC774\uB3D9 \uAC70\uB9AC\uAC00 30% + \uC99D\uAC00\uD558\uACE0 \xB7 \uC790\uC2E0\uC740 \uC801\uC5D0\uAC8C \uBC18\uACA9 + \uBC1B\uC9C0 \uC54A\uC73C\uBA70 \xB7 \uC801\uC5D0\uAC8C \uD53C\uD574\uB97C + \uC785\uD78C \uACBD\uC6B0 \uBB34\uC870\uAC74 \uCD9C\uBC1C \uC704\uCE58\uB85C + \uBCF5\uADC0\uD560 \uC218 \uC788\uACE0 \xB7 \uB3CC\uC9C4 \uAC70\uB9AC\uC5D0 + \uBE44\uB840\uD574 \uACF5\uACA9\uB825 \uC99D\uAC00" + effectKey: sling_dist_pct;counter_taken_pct;return_on_hit;dash_dmg_pct + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: sunquan_a heroId: sunquan kind: active - nameKo: "\uBCBD\uC548\uC758 \uAD70\uC8FC" - hanja: "\u78A7\u773C\u5152" - trigger: instant + nameKo: "\uD654\uC0B4\uBE44" + hanja: "\u7BAD\u96E8" + trigger: next_action cooldown: 4 lvMax: 5 - descKo: "\uC989\uC2DC: \uC774\uBC88 \uC801 \uD134 \uC544\uAD70 \uC804\uC6D0 \uBC1B\uB294 - \uD53C\uD574 \u221230 % \xB7 \uC801 \uAD50\uC804 \uBC94\uC704 \u22121" - effectKey: team_dmg_taken_pct_turn;enemy_aggro_add - p1: -0.3 - p2: -1 + descKo: "\uB2E4\uC74C \uD589\uB3D9 \uC2DC \uD0C0\uAC9F\uC774 \uB41C \uB300\uC0C1 + \uC8FC\uC704\uC5D0 \uD654\uC0B4\uBE44\uB97C \uC3DF\uC544 \uACF5\uACA9\uB825\uC758 + 180%\uC758 \uD53C\uD574\uB97C \uC785\uD788\uACE0 \xB7 \uD63C\uB780\uC5D0 \uBE60\uB728\uB824 + \uC774\uB3D9 \uAC70\uB9AC\uB97C 50% \uBC18\uAC10 \uC2DC\uD0B4" + effectKey: shot_blast;hit_confuse + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: sunquan_p @@ -1427,47 +1591,59 @@ MonoBehaviour: trigger: always cooldown: 0 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 + descKo: "\uBAA8\uB4E0 \uC624\uAD70\uC758 \uAD81\uC218 \uC0AC\uAC70\uB9AC\uAC00 + 50% \uC99D\uAC00\uD558\uBA70 \xB7 \uC790\uC2E0\uC758 \uACF5\uACA9\uC774 \uC544\uAD70 + \uC720\uB2DB\uC5D0\uAC8C \uD53C\uD574\uB97C \uC8FC\uC9C0 \uC54A\uACE0 \uAD00\uD1B5\uD574 + \uC801\uC744 \uACF5\uACA9\uD560 \uC218 \uC788\uC73C\uBA70 \xB7 \uC790\uC2E0\uC740 + \uADFC\uC811 \uC720\uB2DB\uACFC\uC758 \uC804\uD22C\uC5D0\uC11C \uD328\uB110\uD2F0\uB97C + \uBC1B\uC9C0 \uC54A\uC74C" + effectKey: range_mult;arrow_pass_allies;melee_penalty_off + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: lusu_a heroId: lusu kind: active - nameKo: "\uB2E8\uB3C4\uBD80\uD68C" - hanja: "\u55AE\u5200\u8D74\u6703" - trigger: instant + nameKo: "\uB179\uAC01\uC9C4" + hanja: "\u9E7F\u89D2\u9663" + trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uC989\uC2DC \u2605: \uC774\uBC88 \uC801 \uD134 \uBC18\uACBD 3 \uC548 - \uC544\uAD70 \uBC1B\uB294 \uD53C\uD574 \u221240 % [p1 \uBC1B\uB294 \uD53C\uD574 - \xB7 p2 \uBC18\uACBD \xB7 \uB300\uC0C1 \uC544\uAD70]" - effectKey: dmg_taken_aura - p1: -0.4 - p2: 3 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC73C\uB85C \uC9C0\uC815 \uB41C \uC704\uCE58(\uC2AC\uB9C1\uC0F7 + \uBC94\uC704)\uC5D0 \uBAA9\uCC45\uC744(3\uC5F4) \uC0DD\uC131\uD558\uACE0 \uC801\uC774 + \uC788\uB294 \uC704\uCE58\uC5D0 \uBAA9\uCC45\uC774 \uC124\uCE58 \uB41C \uACBD\uC6B0 + \uD53C\uD574(\uD544\uB4DC \uC624\uBE0C\uC81D\uD2B8\uC640 \uB3D9\uC77C)\uB97C + \uC8FC\uBA70 \uD295\uACA8\uB0BC \uC218 \uC788\uC74C" + effectKey: shot_barricade + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: lusu_p heroId: lusu kind: passive - nameKo: "\uD6C4\uB355" - hanja: "\u539A\u5FB7" + nameKo: "\uB3D9\uB9F9\uC758 \uAE43\uBC1C" + hanja: "\u540C\u76DF\u4E4B\u65D7" trigger: always cooldown: 0 lvMax: 5 - descKo: "\uAD11\uC5ED \uC544\uAD70 \uD53C\uD574 \uBE44\uC728 0.5" - effectKey: splash_ally_ratio - p1: 0.5 + descKo: "\uC790\uC2E0\uC758 \uD589\uB3D9 \uBC94\uC704 \uB0B4 \uC544\uAD70 1\uAE30\uB2F9 + \uACF5\uACA9\uB825\uC774 5%\uC529 \uC99D\uAC00\uD558\uACE0 \xB7 \uB9E4\uD134 + \uC2DC\uC791 \uC2DC \uD589\uB3D9 \uBC94\uC704 \uB0B4 \uC544\uAD70\uC758 \uC0DD\uBA85\uB825\uC744 + 10% \uD68C\uBCF5\uC2DC\uD0AC \uC218 \uC788\uC73C\uBA70 \xB7 \uC0AC\uB9DD\uD560 + \uACBD\uC6B0 \uC4F0\uB7EC\uC9C4 \uC7A5\uC18C\uC5D0 5\uD134\uAC04 \uD6A8\uACFC\uB97C + \uC720\uC9C0\uD558\uB294 \uAE43\uBC1C\uC744 \uB0A8\uACA8 \uC790\uC2E0\uC758 + \uD328\uC2DC\uBE0C \uD6A8\uACFC\uB97C \uC804\uC7A5\uC5D0 \uB0A8\uAE40" + effectKey: atk_per_ally_range;turn_start_heal_range;death_banner + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: lvmeng_a @@ -1478,15 +1654,15 @@ MonoBehaviour: trigger: next_action cooldown: 4 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uC54C\uAE4C\uAE30 \uBA85\uC911 \uC2DC \uCD94\uAC00 - \uC774\uB3D9\uC5D0\uC11C \uACF5\uACA9 \uAC00\uB2A5 \xB7 \uB9DE\uC740 \uC801\uC740 - \uC774\uBC88 \uC801 \uD134 \uAD50\uC804 \uBC18\uC751 \uC5C6\uC74C [p2 \uB300\uC0C1 - = \uB9DE\uC740 \uC801 1\uAE30]" - effectKey: extra_move_attack;enemy_aggro_off_turn - p1: 1 - p2: 1 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC73C\uB85C \uC801\uC5D0\uAC8C \uD53C\uD574\uB97C + \uC785\uD78C \uACBD\uC6B0 \xB7 \uD55C\uBC88 \uB354 \uD589\uB3D9\uD560 \uC218 + \uC788\uC73C\uBA70 \uC801\uC744 \uCC98\uCE58\uD55C \uACBD\uC6B0 \uD55C\uBC88 + \uB354 \uD589\uB3D9 \uAC00\uB2A5(\uCD5C\uB300 3\uD68C)" + effectKey: reaction_on_hit;reaction_on_kill + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: lvmeng_p @@ -1497,12 +1673,16 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "5\uD134\uC9F8\uBD80\uD130 \uACF5 +30 % [p2 \uBC1C\uB3D9 \uD134(\uC774\uC0C1)]" - effectKey: dmg_pct - p1: 0.3 - p2: 5 + descKo: "\uBC18\uACA9 \uD53C\uD574\uB97C 30% \uBC18\uAC10\uD558\uBA70 \xB7 \uB9E4\uD134 + \uC7AC\uD589\uB3D9\uC774 \uBC1C\uC0DD\uD560 \uB54C\uB9C8\uB2E4 \uCD94\uAC00 + \uACF5\uACA9\uB825\uC774 20% \uBD80\uC5EC\uB418\uBA70 \xB7 \uCD94\uAC00 \uACF5\uACA9\uB825\uC740 + \uC7AC\uD589\uB3D9 \uD69F\uC218\uC5D0 \uBE44\uB840\uD574 2\uBC30\uC529 \uC99D\uAC00(20%\u219240%\u219280%) + (\uCD94\uAC00 \uACF5\uACA9\uB825\uC740 \uD134\uB9C8\uB2E4 \uCD08\uAE30\uD654)" + effectKey: counter_taken_pct;reaction_atk_stack + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: luxun_a @@ -1513,30 +1693,37 @@ MonoBehaviour: trigger: next_action cooldown: 4 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uAD11\uC5ED \uBC18\uACBD \xD72 \xB7 \uAD11\uC5ED - \uBE44\uC728 0.9(\uAD11\uC5ED)" - effectKey: splash_radius_mult;splash_ratio - p1: 2 - p2: 0.9 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC73C\uB85C \uC9C0\uC815\uD55C \uBC29\uD5A5\uC73C\uB85C + \uBAA8\uB4E0 \uC720\uB2DB\uC5D0\uAC8C \uD53C\uD574\uB97C \uC785\uD788\uB294 + \uBD88 \uC7A5\uBCBD\uC744 \uC0DD\uC131\uD574 3\uD134\uAC04 \uC804\uC7A5\uC5D0 + \uC720\uC9C0\uB418\uBA70 \xB7 \uB2FF\uB294 \uC801\uC5D0\uAC8C\uB294 \uC989\uC2DC + 300%\uC758 \uD53C\uD574\uB97C \uC785\uD788\uACE0 \xB7 3\uD134\uAC04 \uD654\uC0C1 + \uD6A8\uACFC\uB97C \uC785\uD798" + effectKey: shot_firewall;hit_burn + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: luxun_p heroId: luxun kind: passive - nameKo: "\uC5F0\uC601" - hanja: "\u9023\u71DF" + nameKo: "\uBC31\uBA74\uC11C\uC0DD" + hanja: "\u767D\u9762\u66F8\u751F" trigger: always cooldown: 0 lvMax: 5 - descKo: "\uAD11\uC5ED \uD53C\uD574 \uBE44\uC728 0.6 \u2192 0.7 [\uAD11\uC5ED - \uBE44\uC728 \uC808\uB300\uAC12]" - effectKey: splash_ratio - p1: 0.7 + descKo: "\uBAA8\uB4E0 \uC624\uAD70\uC774 \uC0C1\uD0DC\uC774\uC0C1 \uD6A8\uACFC\uC5D0 + \uAC78\uB9B0 \uC801 \uACF5\uACA9 \uC2DC \uD53C\uD574\uB7C9\uC774 50% \uC99D\uAC00\uD558\uBA70 + \xB7 \uC790\uC2E0\uC758 \uACF5\uACA9\uC5D0 \uD53C\uACA9 \uB41C \uB300\uC0C1\uC740 + 1\uD134\uAC04 \uD63C\uB780\uC5D0 \uBE60\uB728\uB9BC (\uD63C\uB780 : \uC774\uB3D9 + \uC18D\uB3C4 50% \uAC10\uC18C)" + effectKey: dmg_pct;hit_confuse + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: sunshangxiang_a @@ -1544,18 +1731,20 @@ MonoBehaviour: kind: active nameKo: "\uAD81\uC694\uD76C" hanja: "\u5F13\u8170\u59EC" - trigger: next_action + trigger: instant cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uD654\uC0B4\uC774 \uC544\uAD70\uC744 \uD1B5\uACFC(\uC0AC\uC120) - \xB7 \uC0AC\uAC70\uB9AC +1" - effectKey: arrow_pass_allies;range_add - p1: 1 - p2: 1 + descKo: "\uC989\uC2DC: \uD589\uB3D9 \uBC94\uC704 \uB0B4 \uBAA8\uB4E0 \uC801\uC5D0\uAC8C + \uD654\uC0B4\uC744 \uBC1C\uC0AC\uD574 \uD53C\uD574\uB97C \uC785\uD788\uACE0 + \xB7 \uBC00\uCCD0\uB0B4\uBA70 \uCE58\uBA85\uD0C0 \uBC1C\uC0DD \uC2DC 2\uD134\uAC04 + \uCD9C\uD608 \uD6A8\uACFC\uB97C \uC720\uBC1C\uD568" + effectKey: volley;hit_bleed + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: sunshangxiang_p heroId: sunshangxiang kind: passive @@ -1564,64 +1753,71 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uAD81\uC218 \uADFC\uC811 \uD328\uB110\uD2F0 \uC5C6\uC74C" - effectKey: melee_penalty_off - p1: 1 + descKo: "\uB0A8\uC131 \uC601\uC6C5\uC5D0 \uB300\uD55C \uD53C\uD574\uB7C9\uC774 + 50% \uC99D\uAC00\uD558\uACE0 \xB7 \uADFC\uC811 \uD53C\uD574 \uD328\uB110\uD2F0\uB97C + \uBC1B\uC9C0 \uC54A\uC74C.(\uBC18\uACA9 \uD53C\uD574\uC5D0\uB3C4 \uD53C\uD574\uB7C9 + 50% \uC99D\uAC00 \uC801\uC6A9)" + effectKey: dmg_pct;melee_penalty_off + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: xiaoqiao_a heroId: xiaoqiao kind: active - nameKo: "\uAE08\uC2AC" - hanja: "\u7434\u745F" + nameKo: "\uCD98\uD48D\uAC00" + hanja: "\u6625\u98A8\u6B4C" trigger: instant cooldown: 4 lvMax: 5 - descKo: "\uC989\uC2DC \u2605: \uC544\uAD70 \uC804\uC6D0 \uC561\uD2F0\uBE0C \uCFE8 - \u22121 [p1 \uCFE8 \uAC00\uC0B0 \xB7 p2 1=\uC544\uAD70 \uC804\uC6D0 \xB7 \uC2E0\uADDC - \uD0A4]" - effectKey: cooldown_add - p1: -1 - p2: 1 + descKo: "\uC989\uC2DC: \uD589\uB3D9 \uBC94\uC704 \uB0B4 \uC544\uAD70\uC758 \uC0DD\uBA85\uB825\uC744 + \uACF5\uACA9\uB825\uC758 80% \uB9CC\uD07C \uC989\uC2DC \uD68C\uBCF5\uD558\uACE0 + \uD134 \uC885\uB8CC \uC2DC \uC790\uC2E0\uC758 3m \uC8FC\uC704\uC5D0 \uC788\uB294 + \uC544\uAD70\uC758 \uC0DD\uBA85\uB825\uC744 \uACF5\uACA9\uB825\uC758 30% \uB9CC\uD07C + \uD68C\uBCF5" + effectKey: heal_atk;turn_end_heal + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: xiaoqiao_p heroId: xiaoqiao kind: passive - nameKo: "\uC774\uAD50" - hanja: "\u4E8C\u55AC" + nameKo: "\uACBD\uAD6D\uC9C0\uC0C9" + hanja: "\u50BE\u570B\u4E4B\u8272" trigger: always cooldown: 0 lvMax: 5 - descKo: "\uC544\uAD70 \uCC45\uC0AC \uC0AC\uAC70\uB9AC +0.5 [\uB300\uC0C1 = \uCC45\uC0AC]" - effectKey: team_range_add - p1: 0.5 + descKo: "\uB0A8\uC131 \uC601\uC6C5\uC5D0\uAC8C \uBC1B\uB294 \uD53C\uD574\uAC00 + 30% \uAC10\uC18C\uD558\uBA70 \xB7 \uC790\uC2E0\uC758 \uACF5\uACA9 \uB300\uC0C1\uC774 + \uC544\uAD70\uC778 \uACBD\uC6B0 \uACF5\uACA9\uB825\uC758 50% \uB9CC\uD07C \uC0DD\uBA85\uB825\uC744 + \uD68C\uBCF5" + effectKey: dmg_taken_pct;ally_hit_heal + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: lingtong_a heroId: lingtong kind: active - nameKo: "\uC720\uC218\uC758 \uBCF5\uC218" - hanja: "\u6FE1\u9808" - trigger: next_action + nameKo: "\uC120\uD48D \uCC3D\uC220" + hanja: "\u65CB\u98A8\u69CD\u8853" + trigger: instant cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uBC00\uBA70 \uC804\uC9C4 \uAC70\uB9AC \xD71.5 - \xB7 \uBA85\uC911 \uC2DC \uACAC\uC81C \uBC18\uACBD +1 [\uBC00\uBA70 \uC804\uC9C4 - \uAC70\uB9AC \uBC30\uC728 \xB7 \uD0A4 \uADFC\uC0AC]" - effectKey: push_mult;overwatch_radius_add - p1: 1.5 - p2: 1 + descKo: "\uC989\uC2DC: \uBC18\uACBD 3m \uB0B4 \uBAA8\uB4E0 \uC801\uC5D0\uAC8C + \uACF5\uACA9\uB825\uC758 150% \uD53C\uD574\uB97C \uC785\uD788\uACE0 1m \uBC00\uCCD0\uB0C4" + effectKey: aoe_strike + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: lingtong_p @@ -1632,66 +1828,70 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uBC1B\uB294 \uBC18\uACA9 \uD53C\uD574 \u221230 %" - effectKey: counter_taken_pct - p1: -0.3 + descKo: "\uC790\uC2E0\uC758 \uACF5\uACA9\uB825 +15% \uC99D\uAC00\uD558\uACE0 + \xB7 \uBC00\uCE58\uB294 \uAC70\uB9AC +30% \uC99D\uAC00" + effectKey: atk_pct;push_mult + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: zhoutai_a heroId: zhoutai kind: active - nameKo: "\uC5F4\uB450 \uC0C1\uCC98" - hanja: "\u5341\u4E8C\u5275" - trigger: instant + nameKo: "\uAC80\uAE30 \uC77C\uC12C" + hanja: "\u528D\u6C23\u4E00\u9583" + trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uC989\uC2DC \u2605: \uC774\uBC88 \uC801 \uD134 \uBC18\uACBD 2 \uC548 - \uC544\uAD70\uC774 \uBC1B\uB294 \uD53C\uD574\uC758 50 % \uB97C \uC8FC\uD0DC\uAC00 - \uB300\uC2E0 \uBC1B\uC74C [p1 \uB300\uC2E0 \uBC1B\uB294 \uBE44\uC728 \xB7 p2 - \uBC18\uACBD]" - effectKey: guard_share - p1: 0.5 - p2: 2 + descKo: "\uB2E4\uC74C \uD589\uB3D9 \uC2DC \uACF5\uACA9 \uBC29\uD5A5\uC73C\uB85C + \uAC80\uAE30\uB97C \uB0A0\uB824 \uBAA8\uB4E0 \uC720\uB2DB\uC5D0\uAC8C \uACF5\uACA9\uB825\uC758 + 180%\uC758 \uD53C\uD574\uB97C \uC785\uD788\uACE0 \xB7 \uBC00\uCCD0\uB0C4" + effectKey: shot_pierce + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: zhoutai_p heroId: zhoutai kind: passive - nameKo: "\uCCA0\uBCBD \uD638\uC704" - hanja: + nameKo: "\uD30C\uCC3D" + hanja: "\u7834\u69CD" trigger: always cooldown: 0 lvMax: 5 - descKo: "HP 50 % \uC774\uD558\uC77C \uB54C \uBC1B\uB294 \uD53C\uD574 \u221230 - % [p2 HP \uC784\uACC4(\uC774\uD558\uC77C \uB54C)]" - effectKey: dmg_taken_pct - p1: -0.3 - p2: 0.5 + descKo: "\uCE58\uBA85\uD0C0 \uD53C\uD574\uB97C \uC785\uD78C \uC801\uC5D0\uAC8C + \uD63C\uB780 \uD6A8\uACFC\uB97C \uBD80\uC5EC\uD558\uACE0 \xB7 \uCC3D\uBCD1\uC5D0\uAC8C\uB294 + \uD56D\uC0C1 \uCE58\uBA85\uD0C0\uB85C \uD53C\uD574\uB97C \uC785\uD798" + effectKey: hit_confuse;crit_always + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: zhugejin_a heroId: zhugejin kind: active - nameKo: "\uD615\uC81C\uC758 \uACC4" - hanja: "\u5144\u5F1F\u4E4B\u8A08" + nameKo: "\uC120\uD48D\uACC4" + hanja: "\u65CB\u98A8\u8A08" trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uAD11\uC5ED \uC544\uAD70 \uD53C\uD574 0(\uAD11\uC5ED)" - effectKey: splash_ally_ratio + descKo: "\uB2E4\uC74C \uD589\uB3D9 \uC2DC \uD68C\uC624\uB9AC \uBC14\uB78C\uC744 + \uC18C\uD658\uD574 \uC9C0\uC815\uD55C \uBC29\uD5A5\uC758 \uC801\uC744 \uD06C\uAC8C + \uBC00\uCCD0\uB0B4\uACE0 \uB099\uD558\uD55C \uC801\uC5D0\uAC8C 1\uD134\uAC04 + \uD63C\uB780 \uD6A8\uACFC\uB97C \uBD80\uC5EC" + effectKey: shot_whirl;hit_confuse p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: zhugejin_p heroId: zhugejin kind: passive @@ -1700,13 +1900,13 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uC544\uAD70 \uC804\uC6D0 \uBC1B\uB294 \uD53C\uD574 \u22125 % [p3 1=\uC544\uAD70 - \uC804\uC6D0]" - effectKey: dmg_taken_pct - p1: -0.05 + descKo: "\uC790\uC2E0\uC758 \uAD11\uC5ED \uACF5\uACA9\uC774 \uC544\uAD70\uC5D0\uAC8C\uB294 + \uD53C\uD574\uB97C \uC785\uD788\uC9C0 \uC54A\uC74C" + effectKey: splash_ally_ratio + p1: 0 p2: 0 - p3: 1 - pMask: 5 + p3: 0 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: dongxi_a @@ -1717,13 +1917,13 @@ MonoBehaviour: trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uBC00\uBA70 \uC804\uC9C4 \uAC70\uB9AC \xD71.3 - [\uBC00\uBA70 \uC804\uC9C4 \uAC70\uB9AC \uBC30\uC728 \xB7 \uD0A4 \uADFC\uC0AC]" - effectKey: push_mult - p1: 1.3 + descKo: "\uB2E4\uC74C \uD589\uB3D9 \uC2DC \uACF5\uACA9\uB825\uC758 150%\uC758 + \uD53C\uD574\uB97C \uC785\uD798" + effectKey: atk_ratio + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: dongxi_p @@ -1734,12 +1934,12 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uACAC\uC81C \uD53C\uD574 \xD71.2" - effectKey: overwatch_dmg_mult - p1: 1.2 + descKo: "\uC790\uC2E0\uC758 \uACF5\uACA9\uB825 +10%" + effectKey: atk_pct + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: lvbu_a @@ -1747,33 +1947,38 @@ MonoBehaviour: kind: active nameKo: "\uCC9C\uD558\uBB34\uC30D" hanja: "\u5929\u4E0B\u7121\u96D9" - trigger: next_action + trigger: instant cooldown: 4 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 + descKo: "\uC989\uC2DC: 1\uD134\uAC04 \uBAA8\uB4E0 \uACF5\uACA9\uC744 \uBB34\uD6A8\uD654(\uBB34\uC801)\uD558\uACE0 + \xB7 2\uBC30\uC758 \uBC18\uACA9 \uD53C\uD574\uB97C \uC785\uD788\uBA70 \xB7 + \uC774\uB3D9 \uAC70\uB9AC\uAC00 1.5\uBC30 \uC99D\uAC00\uD558\uACE0 \uACF5\uACA9 + \uD6C4 \uACF5\uACA9 \uC804 \uC81C\uC790\uB9AC\uB85C \uBCF5\uADC0\uD55C\uB2E4." + effectKey: invuln;counter_dmg_pct;sling_dist_pct;return_always + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: lvbu_p heroId: lvbu kind: passive - nameKo: "\uC801\uD1A0\uB9C8" - hanja: "\u8D64\u514E\u99AC" + nameKo: "\uC801\uD1A0\uB9C8 \uC9C8\uC8FC" + hanja: trigger: always cooldown: 0 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 + descKo: "\uBAA8\uB4E0 \uAD70\uC601 \uC720\uB2DB\uC758 \uACF5\uACA9\uB825 20% + \uC99D\uAC00\uD558\uACE0 \uC790\uC2E0\uC740 \uBC18\uACA9 \uBC1B\uC9C0 \uC54A\uC73C\uBA70 + \xB7 \uC0C1\uC131\uC744 \uBB34\uC2DC\uD55C \uACF5\uACA9\uC774 \uAC00\uB2A5\uD558\uACE0 + \uC774\uB3D9 \uAC70\uB9AC\uC5D0 \uBE44\uB840\uD574 \uACF5\uACA9\uB825 \uC99D\uAC00\uD55C\uB2E4. + (\uCD5C\uB300 100%)" + effectKey: atk_pct;counter_taken_pct;matchup_floor;dash_dmg_pct p1: 0 - p2: 1.5 + p2: 0 p3: 0 - pMask: 7 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: zhangjiao_a @@ -1784,16 +1989,15 @@ MonoBehaviour: trigger: instant cooldown: 5 lvMax: 5 - 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 + descKo: "\uC989\uC2DC: \uC790\uC2E0\uC758 \uBAA8\uC2B5\uC744 \uC548\uAC1C\uC5D0 + \uC228\uACA8 1\uD134 \uAC04 \uD53C\uD574\uB97C \uBC1B\uC9C0 \uC54A\uC74C" + effectKey: invuln + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: zhangjiao_p heroId: zhangjiao kind: passive @@ -1802,12 +2006,14 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uC801 \uAD81\uBCD1\uC5D0\uAC8C \uBC1B\uB294 \uD53C\uD574 \u221230 %" - effectKey: dmg_taken_from_archer_pct - p1: -0.3 + descKo: "\uAD81\uBCD1\uC5D0\uAC8C \uBC1B\uB294 \uD53C\uD574\uAC00 30 % \uAC10\uC18C\uD558\uBA70 + \xB7 \uC790\uC2E0\uC758 \uACF5\uACA9\uC5D0 \uBA85\uC911 \uB41C \uC801\uC5D0\uAC8C + 50% \uD655\uB960\uB85C \uC911\uB3C5 \uD6A8\uACFC\uB97C \uC720\uBC1C" + effectKey: dmg_taken_from_archer_pct;hit_poison + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: huaxiong_a @@ -1818,13 +2024,13 @@ MonoBehaviour: trigger: next_action cooldown: 3 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 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC758 \uD53C\uD574\uB7C9\uC774 100% \uC99D\uAC00\uD558\uACE0 + \xB7 \uC790\uC2E0\uC740 \uBC18\uACA9 \uD53C\uD574\uB97C \uBC1B\uC9C0 \uC54A\uB294\uB2E4." + effectKey: dmg_pct;counter_taken_pct + p1: 0 p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: huaxiong_p @@ -1835,12 +2041,13 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "1\uD134\uC9F8 \uACF5 +30 %" - effectKey: dmg_pct_turn1 - p1: 0.3 + descKo: "\uC804\uD22C \uC2DC\uC791 \uD6C4 3\uD134 \uAC04 \uC790\uC2E0\uC758 \uACF5\uACA9\uB825 + 50% \uC99D\uAC00\uD558\uACE0 \xB7 \uBC18\uACA9 \uD53C\uD574\uAC00 50% \uBC18\uAC10" + effectKey: atk_pct;counter_taken_pct + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: yanliang_a @@ -1851,13 +2058,15 @@ MonoBehaviour: trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uBA85\uC911 \uC2DC \uACAC\uC81C \uBC18\uACBD - +1.5" - effectKey: overwatch_radius_add - p1: 1.5 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC740 \uC801\uC744 \uAD00\uD1B5\uD558\uBA70 + \uACBD\uB85C\uC0C1 \uBAA8\uB4E0 \uC801\uC5D0\uAC8C \uD53C\uD574\uB97C \uC785\uD788\uBA70 + \uD53C\uD574\uB97C \uC785\uD78C \uC801\uC5D0\uAC8C 1\uD134\uAC04 \uCD9C\uD608 + \uD6A8\uACFC \uBD80\uC5EC" + effectKey: pierce_through;hit_bleed + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: yanliang_p @@ -1868,13 +2077,13 @@ MonoBehaviour: trigger: always cooldown: 0 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 + descKo: "\uBC18\uACA9 \uD53C\uD574\uAC00 50% \uC99D\uAC00\uD558\uBA70 \xB7 \uBC18\uACA9 + \uD53C\uD574\uC758 30%\uB9CC\uD07C \uC0DD\uBA85\uB825 \uD68C\uBCF5" + effectKey: counter_dmg_pct;counter_lifesteal + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: wenchou_a @@ -1885,13 +2094,13 @@ MonoBehaviour: trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uAD00\uD1B5 \uBC00\uAE30 \xD72(\uACBD\uB85C\uB97C - \uBE44\uC6B4\uB2E4)" - effectKey: pierce_knockback_mult - p1: 2 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC758 \uD53C\uD574\uB7C9\uC774 50% \uC99D\uAC00\uD558\uACE0 + \xB7 \uACF5\uACA9 \uD6C4 \uC81C\uC790\uB9AC\uB85C \uBCF5\uADC0\uD55C\uB2E4." + effectKey: dmg_pct;return_always + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: wenchou_p @@ -1902,30 +2111,31 @@ MonoBehaviour: trigger: always cooldown: 0 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 + descKo: "\uC790\uC2E0\uC774 \uC801\uC744 \uC4F0\uB7EC\uB728\uB9B4 \uACBD\uC6B0 + \xB7 \uC0DD\uBA85\uB825 30% \uD68C\uBCF5" + effectKey: heal_on_kill + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: gongsunzan_a heroId: gongsunzan kind: active - nameKo: "\uBC31\uB9C8\uC758\uC885" - hanja: "\u767D\u99AC\u7FA9\u5F9E" + nameKo: "\uC885\uD6A1\uBB34\uC9C4" + hanja: trigger: next_action cooldown: 3 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 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC758 \uC774\uB3D9\uB825\uC774 50% \uC99D\uAC00\uD558\uACE0 + \xB7 \uD53C\uD574\uC758 50%\uB9CC\uD07C \uC0DD\uBA85\uB825\uC744 \uD68C\uBCF5\uD558\uBA70 + \uACF5\uACA9 \uD6C4 \uC81C\uC790\uB9AC\uB85C \uBCF5\uADC0" + effectKey: sling_dist_pct;lifesteal;return_always + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: gongsunzan_p @@ -1936,12 +2146,14 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uC801 \uAD81\uBCD1\uC5D0\uAC8C \uBC1B\uB294 \uD53C\uD574 \u221230 %" - effectKey: dmg_taken_from_archer_pct - p1: -0.3 + descKo: "\uC801 \uAD81\uBCD1\uC5D0\uAC8C \uC785\uD788\uB294 \uD53C\uD574\uAC00 + 2\uBC30\uAC00 \uB418\uBA70 \xB7 \uC801\uC744 \uCC98\uCE58\uD560 \uACBD\uC6B0 + \uC0DD\uBA85\uB825 30% \uD68C\uBCF5" + effectKey: dmg_pct;heal_on_kill + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: dongzhuo_a @@ -1949,66 +2161,78 @@ MonoBehaviour: kind: active nameKo: "\uD3ED\uC815" hanja: "\u66B4\u653F" - trigger: instant + trigger: next_action cooldown: 4 lvMax: 5 - descKo: "\uC989\uC2DC \u2605: \uC790\uAE30 HP 20 % \uC18C\uBAA8 \xB7 \uC774\uBC88 - \uD134 \uACF5 +80 % [p1 \uACF5 \uC99D\uAC00 \xB7 p2 HP \uC18C\uBAA8]" - effectKey: hp_cost_atk - p1: 0.8 - p2: 0.2 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC774 3\uCD08 \uB3D9\uC548 \uACBD\uB85C\uC0C1 + \uBAA8\uB4E0 \uC720\uB2DB\uC744 \uAC15\uD558\uAC8C \uD295\uACA8\uB0B4\uBA70 + \uB3CC\uC9C4 \uD53C\uD574\uB97C \uC785\uD788\uC9C0\uB9CC \uC790\uC2E0\uC740 + \uD295\uACA8\uC9C0\uC9C0 \uC54A\uC74C(\uD654\uBA74 \uAC00\uC7A5\uC790\uB9AC\uC5D0 + \uB2FF\uC744 \uACBD\uC6B0\uC5D0\uB3C4 \uD295\uACA8\uC9C0\uBA70 \uD540\uBCFC\uCC98\uB7FC + \uB3D9\uC791)" + effectKey: rampage + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: dongzhuo_p heroId: dongzhuo kind: passive - nameKo: "\uBE44\uB454" - hanja: "\u80A5\u920D" + nameKo: "\uC0DD\uBA85 \uAC15\uD0C8" + hanja: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uCD5C\uB300 HP +40 % \xB7 \uBC00\uB9AC\uB294 \uAC70\uB9AC \u221230 - %" - effectKey: hp_max_pct;knockback_taken_pct - p1: 0.4 - p2: -0.3 + descKo: "\uBAA8\uB4E0 \uAD70\uC601 \uC720\uB2DB\uC758 \uBC1B\uB294 \uD53C\uD574\uB97C + 10% \uAC10\uC18C \uC2DC\uD0A4\uBA70 \xB7 \uC790\uC2E0\uC774 \uC785\uD78C \uD53C\uD574\uC758 + 30%\uB9CC\uD07C \uC0DD\uBA85\uB825\uC744 \uD68C\uBCF5\uD558\uACE0 \xB7 \uC544\uAD70 + \uC720\uB2DB\uC774 1\uAE30\uAC00 \uC904\uC5B4\uB4E4 \uB54C\uB9C8\uB2E4 \uD68C\uBCF5 + \uD6A8\uACFC\uB294 5%\uC529 \uC99D\uAC00" + effectKey: dmg_taken_pct;lifesteal + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: lijue_a heroId: lijue kind: active - nameKo: "\uC7A5\uC548 \uC57D\uD0C8" - hanja: "\u9577\u5B89\u63A0\u596A" + nameKo: "\uC57D\uD0C8" + hanja: "\u63A0\u596A" trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uAD00\uD1B5 \uBC00\uAE30 \xD72" - effectKey: pierce_knockback_mult - p1: 2 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC73C\uB85C \uC785\uD78C \uAD11\uC5ED \uD53C\uD574\uC758 + 50% \uB9CC\uD07C \uC790\uC2E0\uACFC \uC778\uC811\uD55C \uAC00\uC7A5 \uC0DD\uBA85\uB825\uC774 + \uB0AE\uC740 \uC544\uAD70\uC758 \uC0DD\uBA85\uB825\uC744 \uD68C\uBCF5" + effectKey: splash_heal_lowest + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: lijue_p heroId: lijue kind: passive - nameKo: "\uC11C\uB7C9 \uC794\uB2F9" - hanja: + nameKo: "\uD608\uB9F9" + hanja: "\u8840\u76DF" trigger: always cooldown: 0 lvMax: 5 - descKo: "\uC544\uAD70 1\uAE30 \uC804\uC0AC\uB9C8\uB2E4 \uACF5 +10 %(\uC911\uCCA9) - [\uC911\uCCA9]" - effectKey: on_ally_death_atk - p1: 0.1 + descKo: "\uD589\uB3D9 \uC885\uB8CC \uC2DC \uC790\uC2E0\uC758 \uC0DD\uBA85\uB825\uC744 + 10% \uCC28\uAC10\uD55C \uD6C4 \uC778\uC811\uD55C \uC544\uAD70\uC758 \uACF5\uACA9\uB825\uC744 + 1\uD134\uAC04 50% \uAC15\uD654 (\uB300\uC0C1\uC774 \uC5C6\uC744 \uACBD\uC6B0 + \uBC1C\uB3D9\uD558\uC9C0 \uC54A\uACE0 \xB7 \uC0DD\uBA85 10%\uC774\uD558\uAC00 + \uB418\uBA74 \uC0DD\uBA85\uB825 \uCC28\uAC10 \uC5C6\uC774 \uBC1C\uB3D9)" + effectKey: action_end_sacrifice + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: yuanshao_a @@ -2019,31 +2243,35 @@ MonoBehaviour: trigger: instant cooldown: 4 lvMax: 5 - descKo: "\uC989\uC2DC \u2605: \uC774\uBC88 \uD134 \uC544\uAD70 \uC804\uC6D0 \uAC00\uD558\uB294 - \uD53C\uD574 +20 % [\uC2E0\uADDC \uD0A4]" - effectKey: team_dmg_pct_turn - p1: 0.2 + descKo: "\uC989\uC2DC: 1\uD134\uAC04 \uD589\uB3D9 \uBC94\uC704 \uB0B4 \uBAA8\uB4E0 + \uC544\uAD70\uC758 \uC774\uB3D9\uB825\uACFC \uCE58\uBA85\uD0C0 \uD53C\uD574\uB7C9\uC774 + 50% \uC99D\uAC00\uD558\uBA70 \xB7 \uCE58\uBA85\uD0C0 \uBC1C\uC0DD \uD655\uB960\uC774 + 20% \uC99D\uAC00" + effectKey: sling_dist_pct;crit_dmg_pct;crit_chance + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: yuanshao_p heroId: yuanshao kind: passive - nameKo: "\uBA85\uBB38" - hanja: "\u540D\u9580" + nameKo: "\uBA85\uBB38\uC758 \uC704\uC138" + hanja: "\u540D\u9580\u5A01\u52E2" trigger: always cooldown: 0 lvMax: 5 - descKo: "\uCD9C\uC804 5\uC778\uC77C \uB54C \uC544\uAD70 \uC804\uC6D0 \uBC1B\uB294 - \uD53C\uD574 \u221210 % [p3 \uCD9C\uC804 \uC778\uC6D0 \uC870\uAC74 \xB7 \uB300\uC0C1 - \uC544\uAD70 \uC804\uC6D0]" - effectKey: dmg_taken_pct - p1: -0.1 + descKo: "\uBAA8\uB4E0 \uAD70\uC601 \uC601\uC6C5\uC758 \uC0DD\uBA85\uB825\uC774 + 20% \uC99D\uAC00\uD558\uACE0 \xB7 \uC790\uC2E0\uC758 \uACF5\uACA9\uC774 \uCE58\uBA85\uD0C0\uB85C + \uBA85\uC911\uD560 \uACBD\uC6B0 \uB300\uC0C1\uC5D0\uAC8C 2\uD134\uAC04 \uCD9C\uD608 + \uD6A8\uACFC\uB97C \uC720\uBC1C\uD55C \uD6C4 \uCD5C\uB300 \uC774\uB3D9\uAC70\uB9AC\uC758 + 30%\uB9CC\uD07C \uBC00\uCCD0\uB0C4(\uCD9C\uD608 \uD53C\uD574 \uCD94\uAC00)" + effectKey: hp_max_pct;hit_bleed;hit_push + p1: 0 p2: 0 - p3: 5 - pMask: 5 + p3: 0 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: yuanshu_a @@ -2054,15 +2282,15 @@ MonoBehaviour: trigger: instant cooldown: 4 lvMax: 5 - descKo: "\uC989\uC2DC \u2605: \uC790\uAE30 HP 30 % \uD68C\uBCF5 [\uC2E0\uADDC - \uD0A4]" - effectKey: heal_self_pct - p1: 0.3 + descKo: "\uC989\uC2DC \uC790\uC2E0\uC758 \uC0DD\uBA85\uB825\uC744 60% \uD68C\uBCF5\uD558\uACE0 + \xB7 1\uD134\uAC04 \uBC1B\uB294 \uD53C\uD574 30% \uAC10\uC18C" + effectKey: heal_self_pct;dmg_taken_pct + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: yuanshu_p heroId: yuanshu kind: passive @@ -2071,49 +2299,57 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uBCF4\uC2A4\uC5D0\uAC8C \uBC1B\uB294 \uD53C\uD574 \u221220 % [p2 2=\uBCF4\uC2A4\uC5D0\uAC8C\uC11C\uB9CC]" - effectKey: dmg_taken_pct - p1: -0.2 - p2: 2 + descKo: "\uBC1B\uB294 \uD53C\uD574\uAC00 15% \uC99D\uAC00\uD558\uB294 \uB300\uC2E0 + \uC790\uC2E0 \uC8FC\uC704 3m \uBC18\uACBD \uB0B4 \uC544\uAD70\uC758 \uACF5\uACA9\uB825\uC744 + 30% \uC99D\uAC00\uC2DC\uD0A4\uACE0 \uC790\uC2E0\uC744 \uACF5\uACA9\uD55C \uB300\uC0C1\uC5D0\uAC8C + 50% \uD655\uB960\uB85C \uD63C\uB780 \uD6A8\uACFC \uBD80\uC5EC" + effectKey: dmg_taken_pct;atk_pct;on_attacked_confuse + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: diaochan_a heroId: diaochan kind: active - nameKo: "\uC5F0\uD658\uACC4" - hanja: "\u9023\u74B0\u8A08" + nameKo: "\uBBF8\uC778\uACC4" + hanja: trigger: instant cooldown: 4 lvMax: 5 - descKo: "\uC989\uC2DC \u2605: \uBC18\uACBD 4 \uC548 \uAC00\uC7A5 \uAC00\uAE4C\uC6B4 - \uC801 \uBB34\uC7A5 1\uAE30(\uC5C6\uC73C\uBA74 \uBCD1\uC0AC)\uB97C \uB9E4\uD639 - \u2014 \uB2E4\uC74C \uC801 \uD134 \uD589\uB3D9 \uBD88\uAC00 [p1 \uBC18\uACBD - \xB7 p2 \uD134 \xB7 \uB300\uC0C1 \uBB34\uC7A5 \uC6B0\uC120 \xB7 \uC2E0\uADDC - \uD0A4]" - effectKey: charm - p1: 4 - p2: 1 + descKo: "\uC989\uC2DC: \uD589\uB3D9 \uBC94\uC704 \uB0B4 \uC801 \uB0A8\uC131 \uC720\uB2DB\uC744 + 1\uD134\uAC04 \uD589\uB3D9 \uBD88\uAC00 \uC0C1\uD0DC\uC5D0 \uBE60\uB728\uB9AC\uACE0 + \xB7 \uC989\uC2DC \uC790\uC2E0\uC758 \uBC29\uD5A5\uC73C\uB85C \uAC15\uC81C + \uC774\uB3D9 (\uAC15\uC81C \uC774\uB3D9 \uC911 \uC9C0\uD615\uC9C0\uBB3C\uC774\uB098 + \uC720\uB2DB\uC774 \uC788\uC744 \uACBD\uC6B0 \uD295\uACA8\uB098\uAC00\uBA70 + \uB9C9\uD798)" + effectKey: charm_pull + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: diaochan_p heroId: diaochan kind: passive - nameKo: "\uD3D0\uC6D4" - hanja: "\u9589\u6708" + nameKo: "\uC808\uC138\uBBF8\uC778" + hanja: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uADFC\uC811 \uC720\uB2DB\uC5D0\uAC8C \uBC1B\uB294 \uD53C\uD574 \u221225 - % [p2 3=\uADFC\uC811 \uC720\uB2DB\uC5D0\uAC8C\uC11C\uB9CC]" - effectKey: dmg_taken_pct - p1: -0.25 - p2: 3 + descKo: "\uBAA8\uB4E0 \uAD70\uC601\uC758 \uB0A8\uC131 \uC720\uB2DB\uC758 \uACF5\uACA9\uB825\uC774 + 25% \uC99D\uAC00\uD558\uBA70 \xB7 \uC790\uC2E0\uC758 \uACF5\uACA9\uC774 \uB450\uBA85 + \uC774\uC0C1\uC758 \uB0A8\uC131 \uC720\uB2DB\uC5D0\uAC8C \uBA85\uC911 \uB420 + \uACBD\uC6B0 50% \uD655\uB960\uB85C 1\uD134\uAC04 \uAE30\uC808\uD558\uACE0 + \xB7 \uBAA8\uB4E0 \uB0A8\uC131 \uC720\uB2DB\uC5D0\uAC8C \uBC1B\uB294 \uD53C\uD574 + 30% \uAC10\uC18C" + effectKey: atk_pct;multi_male_stun;dmg_taken_pct + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: zhurong_a @@ -2124,13 +2360,17 @@ MonoBehaviour: trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uBC00\uBA70 \uC804\uC9C4 \uB05D\uC5D0 \uB2FF\uC740 - \uC801 2\uAE30 \uB3D9\uC2DC \uD53C\uD574 \xB7 \uBC18\uACA9 \uBC1B\uC9C0 \uC54A\uC74C" - effectKey: spear_hit_two;counter_taken_pct - p1: 2 - p2: -1 + descKo: "\uB2E4\uC74C \uD589\uB3D9 \uC2DC \uAD81\uBCD1\uACFC \uB3D9\uC77C\uD558\uAC8C + \uACF5\uACA9\uB825\uC758 220%\uC758 \uC6D0\uAC70\uB9AC \uACF5\uACA9\uC744 \uC2DC\uC804\uD560 + \uC218 \uC788\uC73C\uBA70 \xB7 \uBA85\uC911 \uB41C \uB300\uC0C1\uC5D0\uAC8C + \uC911\uB3C5 \uD6A8\uACFC\uB97C \uC720\uBC1C (\uC911\uB3C5 : \uB9E4\uD134 + \uC2DC\uC791\uACFC \uD589\uB3D9 \uC885\uB8CC \uC2DC\uC810 \uB9C8\uB2E4 \uCD5C\uB300 + \uC0DD\uBA85\uB825\uC758 2%\uC529 \uAC10\uC18C)" + effectKey: shot_as_archer;atk_ratio;hit_poison + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: zhurong_p @@ -2141,46 +2381,59 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uACAC\uC81C \uBC18\uACBD +0.5" - effectKey: overwatch_radius_add - p1: 0.5 + descKo: "\uC785\uD78C \uD53C\uD574\uC758 20%\uB9CC\uD07C \uC0DD\uBA85\uB825\uC744 + \uD68C\uBCF5\uD558\uACE0 \xB7 \uBC18\uACA9 \uD53C\uD574\uAC00 50% \uC99D\uAC00\uD558\uBA70 + \uC9C0\uD615 \uD6A8\uACFC\uC5D0 \uC774\uB3D9\uB825 \uAC10\uC18C \uC601\uD5A5\uC744 + \uBC1B\uC9C0 \uC54A\uACE0 \uC5EC\uC131 \uC601\uC6C5\uC744 \uACF5\uACA9\uD560 + \uB54C \uACF5\uACA9\uB825 50% \uC99D\uAC00" + effectKey: lifesteal;counter_dmg_pct;terrain_immune;atk_pct + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: menghuo_a heroId: menghuo kind: active - nameKo: "\uCE60\uC885\uCE60\uAE08" - hanja: "\u4E03\u7E31\u4E03\u64D2" - trigger: instant + nameKo: "\uB0A8\uB9CC\uC655" + hanja: "\u5357\u883B\u738B" + trigger: next_action cooldown: 4 lvMax: 5 - descKo: "\uC989\uC2DC \u2605: \uC790\uAE30 HP 30 % \uD68C\uBCF5 \xB7 \uC774\uBC88 - \uD134 \uBC1B\uB294 \uD53C\uD574 \u221230 % [p2 \uC774\uBC88 \uD134 \xB7 \uC2E0\uADDC - \uD0A4]" - effectKey: heal_self_pct;dmg_taken_pct - p1: 0.3 - p2: -0.3 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC740 \uBC18\uACA9 \uD53C\uD574\uB97C \uBC1B\uC9C0 + \uC54A\uC73C\uBA70 \xB7 \uC790\uC2E0\uC740 \uD295\uAE30\uC9C0 \uC54A\uACE0 + \uACBD\uB85C\uC0C1 \uB2FF\uC740 \uC720\uB2DB\uB9CC \uD295\uACA8\uB0BC \uC218 + \uC788\uACE0(\uC544\uAD70 \uD3EC\uD568) \xB7 \uC9C0\uD615\uBB3C(\uBC14\uC704 + \uB4F1)\uC5D0 \uB2FF\uAC70\uB098 \uC774\uB3D9\uB825 \uAC10\uC18C \uC9C0\uD615\uC774 + \uACBD\uB85C\uC0C1 \uC874\uC7AC\uD560 \uACBD\uC6B0 \uD55C\uBC88 \uB354 \uD589\uB3D9\uD560 + \uC218 \uC788\uC74C" + effectKey: counter_taken_pct;self_bounce_mult;reaction_on_terrain + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: menghuo_p heroId: menghuo kind: passive - nameKo: "\uB0A8\uB9CC\uC655" - hanja: "\u5357\u883B\u738B" + nameKo: "\uCE60\uC885\uCE60\uAE08" + hanja: "\u4E03\u7E31\u4E03\u64D2" trigger: always cooldown: 0 lvMax: 5 - descKo: "\uCD5C\uB300 HP +30 %" - effectKey: hp_max_pct - p1: 0.3 + descKo: "\uC785\uD78C \uD53C\uD574\uC758 20%\uB9CC\uD07C \uC0DD\uBA85\uB825\uC744 + \uD68C\uBCF5\uD558\uBA70 \xB7 \uCD5C\uB300 \uC0DD\uBA85\uC774 60% \uC774\uC0C1\uC77C + \uACBD\uC6B0 \uC0AC\uB9DD\uD574\uB3C4 \uCD5C\uB300 \uC0DD\uBA85\uB825\uC758 + 10%\uB97C \uD68C\uBCF5\uD55C \uC0C1\uD0DC\uB85C \uCD5C\uB300 7\uD68C\uAE4C\uC9C0 + \uBD80\uD65C\uD560 \uC218 \uC788\uC9C0\uB9CC \uCD5C\uB300 \uC0DD\uBA85\uB825\uC758 + 60% \uC774\uD558\uC77C \uB54C \uC0AC\uB9DD\uD55C \uACBD\uC6B0 \uC989\uC0AC" + effectKey: lifesteal;revive + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: gaoshun_a @@ -2191,50 +2444,56 @@ MonoBehaviour: trigger: next_action cooldown: 3 lvMax: 5 - 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 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC73C\uB85C \uD53C\uD574\uB97C \uBC1B\uC740 + \uB300\uC0C1\uC740 1\uD134\uAC04 \uAE30\uC808\uD558\uBA70 \xB7 \uC544\uAD70\uC744 + \uBC00\uCCD0 \uC801\uC744 \uACF5\uACA9\uD558\uAC70\uB098 \uB9F5 \uC624\uBE0C\uC81D\uD2B8\uB97C + \uBC00\uCCD0 \uD53C\uD574\uB97C \uC785\uD78C \uACBD\uC6B0\uC5D0\uB3C4 \uD53C\uACA9 + \uB41C \uB300\uC0C1\uC744 1\uD134 \uAC04 \uAE30\uC808" + effectKey: hit_stun + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: gaoshun_p heroId: gaoshun kind: passive - nameKo: "\uBB34\uC5B8\uC758 \uADDC\uC728" - hanja: + nameKo: "\uD568\uC9C4 \uB3CC\uD30C" + hanja: "\u9677\u9663\u7A81\u7834" trigger: always cooldown: 0 lvMax: 5 - descKo: "\uBC18\uACA9 1.0 \uBC30" - effectKey: counter_ratio - p1: 1 + descKo: "\uC544\uAD70\uC774\uB098 \uB9F5 \uC624\uBE0C\uC81D\uD2B8\uB97C \uBC00\uCE5C + \uD6C4 \uB0A8\uC740 \uC774\uB3D9\uB825\uB9CC\uD07C \uCD94\uAC00 \uD589\uB3D9 + \uAC00\uB2A5" + effectKey: push_continue + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: zhanglu_a heroId: zhanglu kind: active - nameKo: "\uC624\uB450\uBBF8\uB3C4" - hanja: "\u4E94\u6597\u7C73\u9053" - trigger: instant + nameKo: "\uC624\uB450\uBBF8 \uB3C5\uC220" + hanja: "\u4E94\u6597\u7C73\u6BD2\u8853" + trigger: next_action cooldown: 4 lvMax: 5 - descKo: "\uC989\uC2DC \u2605: \uC790\uAE30 HP 25 % \uD68C\uBCF5 \xB7 \uC774\uBC88 - \uD134 \uBC1B\uB294 \uD53C\uD574 \u221220 % [p2 \uC774\uBC88 \uD134 \xB7 \uC2E0\uADDC - \uD0A4]" - effectKey: heal_self_pct;dmg_taken_pct - p1: 0.25 - p2: -0.2 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC774 \uC9C1\uC120\uD615 \uD22C\uC0AC\uCCB4\uB85C + \uC804\uD658\uB418\uBA70 \xB7 \uD0C0\uACA9 \uB41C \uC801\uC5D0\uAC8C \uACF5\uACA9\uB825\uC758 + 250%\uC758 \uD53C\uD574\uB97C \uC785\uD788\uACE0 \uC911\uB3C5\uC2DC\uD0A4\uBA70 + \xB7 1\uD134\uAC04 \uC720\uC9C0\uB418\uB294 \uC18C\uADDC\uBAA8 \uB3C5\uC9C0\uB300\uB97C + \uD615\uC131" + effectKey: shot_single;atk_ratio;hit_poison;poison_zone + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: zhanglu_p heroId: zhanglu kind: passive @@ -2243,12 +2502,15 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uAD11\uC5ED \uC544\uAD70 \uD53C\uD574 \uBE44\uC728 0.5" - effectKey: splash_ally_ratio - p1: 0.5 + descKo: "\uC790\uC2E0\uC774 \uCC98\uCE58\uD55C \uB300\uC0C1\uC774 1\uD134\uAC04 + \uCCB4\uB8251 \uC0C1\uD0DC\uB85C \uB418\uC0B4\uC544\uB098 \uC801\uC744 \uACF5\uACA9\uD560 + \uC218 \uC788\uC74C(1\uD134 \uD6C4 \uAC15\uC81C\uB85C \uC18C\uBA78\uB418\uBA70 + \xB7 \uC870\uC791 \uBD88\uAC00)" + effectKey: zombie + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: yanbaihu_a @@ -2259,12 +2521,13 @@ MonoBehaviour: trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uC54C\uAE4C\uAE30 \uBC00\uAE30 \xD71.5" - effectKey: push_mult - p1: 1.5 + descKo: "\uB2E4\uC74C \uD589\uB3D9 \uC2DC \uACF5\uACA9\uB825\uC774 30% \uC99D\uAC00\uD558\uACE0 + \xB7 \uCE58\uBA85\uD0C0 \uD655\uB960 15% \uC0C1\uC2B9" + effectKey: atk_pct;crit_chance + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: yanbaihu_p @@ -2275,12 +2538,12 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uCD5C\uB300 HP +10 %" + descKo: "\uCD5C\uB300 HP+10% \uC99D\uAC00" effectKey: hp_max_pct - p1: 0.1 + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: hanxuan_a @@ -2291,13 +2554,13 @@ MonoBehaviour: trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uBA85\uC911 \uC2DC \uACAC\uC81C \uBC18\uACBD - +0.5" - effectKey: overwatch_radius_add - p1: 0.5 + descKo: "\uB2E4\uC74C \uD589\uB3D9 \uC2DC \uBC18\uACA9 \uBC1B\uC9C0 \uC54A\uC73C\uBA70 + \xB7 2\uBC30 \uB354 \uBA3C\uAC70\uB9AC\uB97C \uBC00\uCCD0\uB0B8\uB2E4." + effectKey: counter_taken_pct;push_mult + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: hanxuan_p @@ -2310,10 +2573,10 @@ MonoBehaviour: lvMax: 5 descKo: "\uBC1B\uB294 \uBC18\uACA9 \uD53C\uD574 \u221220 %" effectKey: counter_taken_pct - p1: -0.2 + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: jinxuan_a @@ -2324,12 +2587,13 @@ MonoBehaviour: trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uC0AC\uAC70\uB9AC +0.5" - effectKey: range_add - p1: 0.5 + descKo: "\uB2E4\uC74C \uD589\uB3D9 \uC2DC \uCE58\uBA85\uD0C0 \uD655\uB960\uC774 + 30% \xB7 \uCE58\uBA85\uD0C0 \uD53C\uD574\uB7C9\uC774 50% \uC99D\uAC00" + effectKey: crit_chance;crit_dmg_pct + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: jinxuan_p @@ -2340,12 +2604,13 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uC801 \uAD81\uBCD1\uC5D0\uAC8C \uBC1B\uB294 \uD53C\uD574 \u221215 %" - effectKey: dmg_taken_from_archer_pct - p1: -0.15 + descKo: "\uC801 \uAD81\uBCD1\uC5D0\uAC8C \uBC1B\uB294 \uD53C\uD574\uB294 15 % + \uAC10\uC18C\uD558\uACE0 \xB7 \uC785\uD788\uB294 \uD53C\uD574\uB294 15% \uC99D\uAC00" + effectKey: dmg_taken_from_archer_pct;dmg_pct + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: zhaofan_a @@ -2356,14 +2621,18 @@ MonoBehaviour: trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uAD11\uC5ED \uBC18\uACBD \xD71.3(\uAD11\uC5ED)" - effectKey: splash_radius_mult - p1: 1.3 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC758 \uACF5\uACA9\uC774 \uC9C1\uC120 \uD615\uD0DC\uB85C + \uBCC0\uD615\uB418\uACE0 \xB7 \uB300\uC0C1\uC774 \uC544\uAD70\uC77C \uACBD\uC6B0 + \uACF5\uACA9\uB825\uC758 60%\uB9CC\uD07C \uC0DD\uBA85\uB825\uC744 \uD68C\uBCF5\uD558\uACE0 + \xB7 \uACF5\uACA9 \uB300\uC0C1\uC774 \uC801\uAD70\uC77C \uACBD\uC6B0 \uACF5\uACA9\uB825\uC758 + 160%\uB9CC\uD07C \uD53C\uD574\uB97C \uC785\uD798" + effectKey: shot_pierce;ally_hit_heal + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 - needsImpl: 1 + needsImpl: 0 - id: zhaofan_p heroId: zhaofan kind: passive @@ -2372,13 +2641,13 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "\uADFC\uC811 \uC720\uB2DB\uC5D0\uAC8C \uBC1B\uB294 \uD53C\uD574 \u221210 - % [p2 3=\uADFC\uC811 \uC720\uB2DB\uC5D0\uAC8C\uC11C\uB9CC]" - effectKey: dmg_taken_pct - p1: -0.1 - p2: 3 + descKo: "\uC804\uD22C \uC2DC\uC791 \uD6C4 \uBC1C\uC0DD \uB41C \uC5B4\uB5A4 \uD53C\uD574\uB4E0 + 1\uD68C\uB97C \uBB34\uD6A8\uD654" + effectKey: negate_hits + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: zhangren_a @@ -2389,29 +2658,30 @@ MonoBehaviour: trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uBB34\uC7A5(\uC601\uC6C5\xB7\uBCF4\uC2A4) - \uC0C1\uB300 \uD53C\uD574 \xD71.5 [p2 0=\uBB34\uC7A5 \uC804\uCCB4]" - effectKey: dmg_vs_general_mult - p1: 1.5 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC758 \uACF5\uACA9\uC740 \uC544\uAD70\uC744 + \uBB34\uC2DC\uD558\uBA70 \uC9C1\uC120 \uBC94\uC704 \uB0B4 \uC801\uC744 \uACF5\uACA9\uB825\uC758 + 150%\uC758 \uD53C\uD574\uB85C \uACF5\uACA9 \uAC00\uB2A5" + effectKey: shot_pierce + p1: 0 p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: zhangren_p heroId: zhangren kind: passive - nameKo: "\uCDA9\uC2E0" - hanja: "\u5FE0\u81E3" + nameKo: "\uB9E4\uBCF5 \uC0AC\uC218" + hanja: "\u57CB\u4F0F\u5C04\u624B" trigger: always cooldown: 0 lvMax: 5 - descKo: "\uC0AC\uAC70\uB9AC +0.3" - effectKey: range_add - p1: 0.3 + descKo: "\uC790\uC2E0\uC758 \uACF5\uACA9 \uC0AC\uAC70\uB9AC 30% \uC99D\uAC00" + effectKey: range_mult + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: chunyuqiong_a @@ -2422,12 +2692,13 @@ MonoBehaviour: trigger: next_action cooldown: 3 lvMax: 5 - descKo: "\uB2E4\uC74C \uD589\uB3D9: \uAD00\uD1B5 \uBC00\uAE30 \xD71.5" - effectKey: pierce_knockback_mult - p1: 1.5 + descKo: "\uB2E4\uC74C \uD589\uB3D9\uC73C\uB85C \uACF5\uACA9 \uD589\uB3D9 \uC2DC + \uC81C\uC790\uB9AC\uB85C \uBCF5\uADC0 \uAC00\uB2A5" + effectKey: return_on_hit + p1: 0 p2: 0 p3: 0 - pMask: 1 + pMask: 0 lvScale: 0.1 needsImpl: 0 - id: chunyuqiong_p @@ -2438,13 +2709,13 @@ MonoBehaviour: trigger: always cooldown: 0 lvMax: 5 - descKo: "HP 30 % \uC774\uD558\uC77C \uB54C \uACF5 +30 % [p1 \uACF5 \uC99D\uAC00 - \xB7 p2 HP \uC784\uACC4]" - effectKey: dmg_pct_hp_below - p1: 0.3 - p2: 0.3 + descKo: "\uC790\uC2E0\uC758 HP\uAC00 1% \uAC10\uC18C\uD560 \uB54C\uB9C8\uB2E4 + \uC774\uD558\uC77C \uB54C \uACF5\uACA9\uB825\uC774 1% \uC99D\uAC00" + effectKey: atk_missing_hp + p1: 0 + p2: 0 p3: 0 - pMask: 3 + pMask: 0 lvScale: 0.1 needsImpl: 0 levels: @@ -2463,3 +2734,2899 @@ MonoBehaviour: - lv: 5 books: 16 gold: 10000 + effects: + - skillId: liubei_a + key: extra_action + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: "\uD589\uB3D9 \uB4A4 \uC7AC\uD589\uB3D9 1\uD68C" + - skillId: liubei_a + key: counter_taken_pct + target: + cond: + dur: 1 + p1: -1 + p2: 0 + p3: 0 + pMask: 1 + note: "1\uD134 \uBC18\uACA9 \uC548 \uBC1B\uC74C" + - skillId: liubei_p + key: atk_pct + target: camp:shu + cond: + dur: 0 + p1: 0.2 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: liubei_p + key: revive + target: + cond: + dur: 0 + p1: 1 + p2: 1 + p3: 0 + pMask: 7 + note: "HP \uBE44\uC728\xB7\uD69F\uC218(\uC804\uD22C\uB2F9)\xB7\uC870\uAC74 HP + \uBE44\uC728" + - skillId: guanyu_a + key: aoe_strike + target: + cond: + dur: 0 + p1: 3 + p2: 2.5 + p3: 1 + pMask: 7 + note: "\uBC18\uACBD\xB7\uACF5\uACA9 \uBC30\uC728\xB7\uBC00\uAE30" + - skillId: guanyu_a + key: hit_bleed + target: + cond: + dur: 0 + p1: 2 + p2: 0.5 + p3: 1 + pMask: 7 + note: "\uD134(\uAE30\uBCF8\uAC12)\xB7\uC774\uB3D9 \uBE44\uB840\xB7\uD655\uB960" + - skillId: guanyu_p + key: counter_dmg_pct + target: camp:shu + cond: + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: guanyu_p + key: counter_taken_pct + target: + cond: + dur: 0 + p1: -1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: guanyu_p + key: kill_stack_atk + target: + cond: + dur: 0 + p1: 0.1 + p2: 1 + p3: 0 + pMask: 3 + note: "\uCC98\uCE58\uB2F9\xB7\uCD5C\uB300" + - skillId: zhangfei_a + key: aoe_strike + target: + cond: + dur: 0 + p1: 4 + p2: 0 + p3: 1 + pMask: 7 + note: "\uBC18\uACBD\xB7\uD53C\uD574 \uC5C6\uC74C\xB7\uBC00\uAE30" + - skillId: zhangfei_a + key: hit_stun + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhangfei_p + key: sling_dist_pct + target: camp:shu + cond: + dur: 0 + p1: 0.3 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhangfei_p + key: dash_dmg_pct + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: "\uC2AC\uB9C1 \uAC70\uB9AC \uBE44\uB840 \uCD5C\uB300" + - skillId: zhangfei_p + key: double_hit_stun + target: + cond: + dur: 0 + p1: 2 + p2: 1 + p3: 0 + pMask: 3 + note: "\uC5F0\uC18D \uD53C\uD574 \uC218\xB7\uAE30\uC808 \uD134" + - skillId: zhangfei_p + key: lifesteal + target: + cond: vs_stunned + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhaoyun_a + key: sling_dist_pct + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhaoyun_a + key: dmg_pct + target: + cond: + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhaoyun_p + key: counter_taken_pct + target: + cond: + dur: 0 + p1: -1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhaoyun_p + key: return_on_hit + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 3 + note: "\uBCF5\uADC0 \uC911 \uD53C\uD574\xB7\uC4F8\uC5B4 \uAC10" + - skillId: huangzhong_a + key: shot_giant + target: + cond: + dur: 0 + p1: 0.5 + p2: 5 + p3: 0 + pMask: 3 + note: "\uD654\uC0B4 \uBC18\uC9C0\uB984(\uAE30\uBCF8\uAC12)\xB7\uACF5\uACA9 \uBC30\uC728 + \xB7 \uC0AC\uAC70\uB9AC \uBB34\uAD00" + - skillId: huangzhong_a + key: hit_stun + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: huangzhong_p + key: arrow_pass_allies + target: + cond: + dur: 0 + p1: 1 + p2: 1 + p3: 0 + pMask: 3 + note: "p2 1 = \uC7A5\uC560\uBB3C\uB3C4 \uD1B5\uACFC" + - skillId: huangzhong_p + key: crit_chance + target: + cond: + dur: 0 + p1: 0.2 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: huangzhong_p + key: crit_dmg_pct + target: + cond: + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhugeliang_a + key: heal_atk + target: range + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhugeliang_a + key: cleanse + target: range + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhugeliang_p + key: counter_taken_pct + target: camp:shu + cond: + dur: 0 + p1: -0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhugeliang_p + key: turn_end_heal_lowest + target: + cond: + dur: 0 + p1: 3 + p2: 0.5 + p3: 0 + pMask: 3 + note: "\uBC18\uACBD\xB7\uACF5\uACA9 \uBC30\uC728" + - skillId: zhoucang_a + key: atk_ratio + target: + cond: + dur: 0 + p1: 1.8 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhoucang_a + key: hit_bleed + target: + cond: + dur: 0 + p1: 2 + p2: 0.5 + p3: 1 + pMask: 7 + note: + - skillId: zhoucang_a + key: push_mult + target: + cond: + dur: 0 + p1: 2 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhoucang_p + key: atk_pct + target: + cond: vs_bleed + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: guanping_a + key: guard_radius_mult + target: + cond: + dur: 2 + p1: 2 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: guanping_a + key: guard_dr + target: + cond: + dur: 2 + p1: -0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: guanping_p + key: counter_dmg_pct + target: + cond: + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: guanping_p + key: guard + target: + cond: + dur: 0 + p1: 2 + p2: 1 + p3: 0 + pMask: 3 + note: "\uBC18\uACBD\xB7\uB300\uC2E0 \uBC1B\uB294 \uBE44\uC728" + - skillId: xushu_a + key: shot_blast + target: + cond: + dur: 0 + p1: 1.5 + p2: 2.5 + p3: 0 + pMask: 3 + note: "\uD3ED\uBC1C \uBC18\uACBD(\uAE30\uBCF8\uAC12 = \uCC45\uC0AC \uAD11\uC5ED)\xB7\uACF5\uACA9 + \uBC30\uC728" + - skillId: xushu_a + key: hit_burn + target: + cond: + dur: 0 + p1: 3 + p2: 0.05 + p3: 1 + pMask: 7 + note: + - skillId: xushu_p + key: range_mult + target: + cond: + dur: 0 + p1: 1.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: xushu_p + key: splash_ally_ratio + target: + cond: + dur: 0 + p1: 0 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: taishici_a + key: shot_pierce + target: + cond: + dur: 0 + p1: 3 + p2: 0 + p3: 0 + pMask: 7 + note: "\uACF5\uACA9 \uBC30\uC728\xB7\uC544\uAD70(0 \uD1B5\uACFC)\xB7\uBC00\uAE30 + \xB7 \uC7A5\uC560\uBB3C \uBB34\uC2DC" + - skillId: taishici_p + key: melee_penalty_off + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: taishici_p + key: atk_pct + target: + cond: + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: taishici_p + key: crit_dmg_pct + target: + cond: + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: machao_a + key: atk_pct + target: + cond: + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: machao_a + key: crit_always + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: machao_p + key: lifesteal + target: + cond: + dur: 0 + p1: 0.2 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: machao_p + key: dmg_per_hit + target: + cond: + dur: 0 + p1: 0.25 + p2: 0 + p3: 0 + pMask: 1 + note: "\uC774\uBC88 \uD589\uB3D9 \uC55E\uC11C \uB9DE\uD78C \uC801 1\uAE30\uB2F9" + - skillId: weiyan_a + key: swap_on_hit + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: "\uBA85\uC911 \uB300\uC0C1 \u2194 \uCD9C\uBC1C \uC704\uCE58" + - skillId: weiyan_a + key: extra_action + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: weiyan_p + key: matchup_floor + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: "\uBD88\uB9AC \uC0C1\uC131 \uBB34\uC2DC" + - skillId: weiyan_p + key: atk_missing_hp + target: + cond: + dur: 0 + p1: 1 + p2: 1 + p3: 0 + pMask: 3 + note: "\uC783\uC740 HP \uBE44\uC728 \xD7 p1 \xB7 \uCD5C\uB300" + - skillId: pangtong_a + key: heal_atk + target: range + cond: + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: pangtong_a + key: hot + target: range + cond: + dur: 3 + p1: 0.1 + p2: 0 + p3: 0 + pMask: 1 + note: "\uB9E4 \uD134 \uC2DC\uC791 \uACF5\uACA9 \uBC30\uC728" + - skillId: pangtong_p + key: dmg_taken_ranged_pct + target: camp:shu + cond: + dur: 0 + p1: -0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: pangtong_p + key: turn_end_cleanse + target: + cond: + dur: 0 + p1: 3 + p2: 1 + p3: 0 + pMask: 3 + note: "\uBC18\uACBD\xB7\uC778\uC6D0" + - skillId: jiangwei_a + key: heal_self_pct + target: + cond: + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: jiangwei_a + key: guard_radius_mult + target: + cond: + dur: 3 + p1: 1.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: jiangwei_p + key: counter_dmg_pct + target: + cond: + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: jiangwei_p + key: guard_counter + target: + cond: + dur: 0 + p1: 2 + p2: 1 + p3: 0 + pMask: 3 + note: "\uBC18\uACA9 \uBC94\uC704(\uAE30\uBCF8\uAC12 = \uAD00\uD3C9 \uD638\uC704 + 2)\xB7\uB300\uC2E0 \uBC1B\uB294 \uBE44\uC728" + - skillId: liufeng_a + key: atk_ratio + target: + cond: + dur: 0 + p1: 1.85 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: liufeng_a + key: hit_confuse + target: + cond: + dur: 0 + p1: 1 + p2: 0.5 + p3: 1 + pMask: 7 + note: "\uD134\xB7\uC774\uB3D9 \uBC30\uC728\xB7\uD655\uB960" + - skillId: liufeng_p + key: counter_taken_pct + target: + cond: vs_confused + dur: 0 + p1: -1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: liufeng_p + key: hit_stun + target: + cond: vs_confused + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: jianyong_a + key: ally_hit_heal + target: + cond: + dur: 0 + p1: 0.8 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: jianyong_a + key: hit_stun + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: jianyong_p + key: heal_cleanse + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: jianyong_p + key: on_attacked_stun + target: + cond: + dur: 0 + p1: 1 + p2: 3 + p3: 0 + pMask: 3 + note: "\uAE30\uC808 \uD134\xB7\uC7AC\uCDA9\uC804 \uD134" + - skillId: sunqian_a + key: splash_heal + target: + cond: + dur: 0 + p1: 0.6 + p2: 0 + p3: 0 + pMask: 1 + note: "\uBA85\uC911 \uB300\uC0C1 \uC8FC\uC704 \uAD11\uC5ED = \uD68C\uBCF5(\uC801 + \uD3EC\uD568)" + - skillId: sunqian_p + key: sling_dist_pct + target: radius:3 + cond: + dur: 0 + p1: 0.3 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: mizhu_a + key: sling_dist_pct + target: range + cond: + dur: 1 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: mizhu_a + key: counter_taken_pct + target: range + cond: + dur: 1 + p1: -0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: mizhu_p + key: ally_hit_buff + target: + cond: + dur: 1 + p1: 0.2 + p2: 0.2 + p3: 0 + pMask: 3 + note: "\uC774\uB3D9 %\xB7\uACF5\uACA9 %" + - skillId: caocao_a + key: pass_allies + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: caocao_a + key: aim_push + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: "\uBA85\uC911 \uC801\uC744 \uD0ED\uD55C \uBC29\uD5A5\uC73C\uB85C \uBC00\uCE68" + - skillId: caocao_p + key: atk_pct + target: camp:wei + cond: + dur: 0 + p1: 0.2 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: caocao_p + key: matchup_floor + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: caocao_p + key: on_ally_death_atk + target: allies + cond: + dur: 0 + p1: 0.1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: xiahoudun_a + key: sling_dist_pct + target: + cond: + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: xiahoudun_a + key: return_aim + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: "\uD0ED\uD55C \uACF3\uC73C\uB85C \uBCF5\uADC0" + - skillId: xiahoudun_p + key: dmg_pct + target: camp:wei:cavalry + cond: + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: xiahoudun_p + key: return_on_hit + target: + cond: + dur: 0 + p1: 0 + p2: 1 + p3: 0 + pMask: 3 + note: "\uBCF5\uADC0 \uD53C\uD574 \uC5C6\uC74C\xB7\uACBD\uB85C \uC720\uB2DB \uC4F8\uC5B4 + \uAC10" + - skillId: xiahoudun_p + key: matchup_floor + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: xuchu_a + key: self_bounce_mult + target: + cond: + dur: 0 + p1: 2 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: xuchu_a + key: hit_stun + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: xuchu_p + key: hp_max_pct + target: + cond: + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: xuchu_p + key: knockback_taken_pct + target: + cond: + dur: 0 + p1: -1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: xuchu_p + key: regen_pct + target: + cond: + dur: 0 + p1: 0.05 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhangliao_a + key: drag_back + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: "\uBA85\uC911 \uC801\uC744 \uCD9C\uBC1C \uC704\uCE58\uB85C \uB04C\uACE0 + \uC634" + - skillId: zhangliao_a + key: hit_confuse + target: + cond: + dur: 0 + p1: 1 + p2: 0.5 + p3: 1 + pMask: 7 + note: + - skillId: zhangliao_p + key: atk_pct + target: + cond: vs_status + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhangliao_p + key: counter_taken_pct + target: + cond: vs_status + dur: 0 + p1: -1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: xuhuang_a + key: axe_aura + target: + cond: + dur: 3 + p1: 1.2 + p2: 0.8 + p3: 2 + pMask: 7 + note: "\uB3C4\uB07C \uBC18\uACBD(\uAE30\uBCF8\uAC12)\xB7\uACF5\uACA9 \uBC30\uC728\xB7\uD295\uAE40 + \uBC30\uC728" + - skillId: xuhuang_p + key: hit_bleed + target: + cond: + dur: 0 + p1: 3 + p2: 0.5 + p3: 0.3 + pMask: 7 + note: + - skillId: xuhuang_p + key: hit_bleed + target: + cond: crit + dur: 0 + p1: 3 + p2: 0.5 + p3: 1 + pMask: 7 + note: + - skillId: zhanghe_a + key: teleport + target: + cond: + dur: 0 + p1: 5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhanghe_a + key: extra_action + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhanghe_p + key: push_mult + target: + cond: + dur: 0 + p1: 2 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhanghe_p + key: pushed_ally_atk_add + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: "\uBC00\uB9B0 \uC544\uAD70 \uACF5\uACA9 + \uC790\uC2E0 \uACF5\uACA9\uB825 + \xD7 p1" + - skillId: zhanghe_p + key: self_pushed_mult + target: + cond: + dur: 0 + p1: 2 + p2: 0 + p3: 0 + pMask: 1 + note: "\uC790\uC2E0\uC774 \uBC00\uB824 \uACF5\uACA9 = \uD53C\uD574 \xD7 p1" + - skillId: caoren_a + key: dmg_taken_pct + target: aura + cond: + dur: 3 + p1: -0.3 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: caoren_a + key: zone_slow + target: + cond: + dur: 3 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: "\uBC18\uACBD \uC548 \uC801 \uC774\uB3D9 \uBE44\uC6A9 +" + - skillId: caoren_p + key: hp_max_pct + target: camp:wei + cond: + dur: 0 + p1: 0.2 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: caoren_p + key: heal_on_action_end + target: + cond: + dur: 0 + p1: 0.2 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: caoren_p + key: unused_move_dr + target: + cond: + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: yujin_a + key: ally_hit_reaction + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: yujin_p + key: move_bank + target: + cond: + dur: 0 + p1: 2 + p2: 0 + p3: 0 + pMask: 1 + note: "\uCD5C\uB300 \uCD95\uC801 \uD134" + - skillId: simayi_a + key: aoe_drain + target: + cond: + dur: 0 + p1: 1.5 + p2: 0.6 + p3: 0 + pMask: 3 + note: "\uACF5\uACA9 \uBC30\uC728\xB7\uD53C\uD574 \u2192 \uC544\uAD70 \uD68C\uBCF5 + \uBE44\uC728" + - skillId: simayi_p + key: splash_radius_mult + target: camp:wei:strategist + cond: + dur: 0 + p1: 1.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: simayi_p + key: range_mult + target: + cond: + dur: 0 + p1: 1.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: simayi_p + key: drain_aura + target: + cond: + dur: 0 + p1: 0.3 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: xiahouyuan_a + key: shot_fan + target: + cond: + dur: 0 + p1: 5 + p2: 3 + p3: 60 + pMask: 7 + note: "\uBC1C \uC218\xB7\uC5F0\uC18D \uD69F\uC218\xB7\uBD80\uCC44\uAF34 \uAC01(\uAE30\uBCF8\uAC12)" + - skillId: xiahouyuan_p + key: multishot + target: + cond: + dur: 0 + p1: 3 + p2: 30 + p3: 0 + pMask: 3 + note: "\uBC1C \uC218\xB7\uBD80\uCC44\uAF34 \uAC01(\uAE30\uBCF8\uAC12)" + - skillId: guojia_a + key: dmg_pct + target: range + cond: + dur: 2 + p1: 0.3 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: guojia_a + key: grant_reaction + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: guojia_p + key: kill_in_range_reward + target: + cond: + dur: 0 + p1: 1 + p2: 0.3 + p3: 0 + pMask: 3 + note: "\uCFE8 \u2212\uD134\xB7\uCD5C\uB300 HP \uD68C\uBCF5" + - skillId: jiaxu_a + key: shot_blast + target: + cond: + dur: 0 + p1: 1.5 + p2: 2.2 + p3: 0 + pMask: 3 + note: "\uB3C5\uAD6C\uB984 \uBC18\uACBD(\uAE30\uBCF8\uAC12)\xB7\uACF5\uACA9 \uBC30\uC728" + - skillId: jiaxu_a + key: hit_poison + target: + cond: + dur: 0 + p1: 5 + p2: 0.02 + p3: 1 + pMask: 7 + note: + - skillId: jiaxu_p + key: hit_poison + target: + cond: + dur: 0 + p1: 5 + p2: 0.02 + p3: 1 + pMask: 7 + note: "\uD134(\uAE30\uBCF8\uAC12 = \uB3C5\uACC4)" + - skillId: jiaxu_p + key: poison_stack + target: + cond: + dur: 0 + p1: 5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: dianwei_a + key: dmg_pct + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: dianwei_a + key: push_mult + target: + cond: + dur: 0 + p1: 2 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: dianwei_a + key: self_bounce_mult + target: + cond: + dur: 0 + p1: 0 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: dianwei_p + key: dmg_taken_pct + target: + cond: + dur: 0 + p1: -0.2 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: dianwei_p + key: gimmick_mult + target: + cond: + dur: 0 + p1: 2 + p2: 0 + p3: 0 + pMask: 1 + note: "\uC9C0\uD615 \uC624\uBE0C\uC81D\uD2B8 \uBC00\uAE30 \uAC70\uB9AC\xB7\uD53C\uD574 + \uBC30\uC728" + - skillId: xunyu_a + key: counter_taken_pct + target: range + cond: + dur: 1 + p1: -1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: xunyu_a + key: atk_pct + target: range + cond: + dur: 1 + p1: 0.3 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: xunyu_p + key: ally_hit_heal + target: + cond: + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: xunyu_p + key: ally_hit_buff + target: + cond: + dur: 1 + p1: 0 + p2: 0.15 + p3: 0 + pMask: 3 + note: + - skillId: xiahoumao_a + key: dmg_taken_pct + target: + cond: + dur: 1 + p1: -0.3 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: xiahoumao_p + key: sling_dist_pct + target: + cond: + dur: 0 + p1: 0.1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: wanglang_a + key: shot_pierce + target: + cond: + dur: 0 + p1: 1.8 + p2: 1 + p3: 0 + pMask: 7 + note: "\uC544\uAD70 1 = \uC544\uAD70\uB3C4 \uD53C\uD574" + - skillId: wanglang_a + key: hit_confuse + target: + cond: + dur: 0 + p1: 1 + p2: 0.5 + p3: 1 + pMask: 7 + note: + - skillId: wanglang_p + key: range_mult + target: + cond: + dur: 0 + p1: 1.1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: xiahouen_a + key: first_hit_bonus + target: + cond: + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: xiahouen_p + key: hp_max_pct + target: + cond: + dur: 0 + p1: 0.1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: sunjian_a + key: atk_pct + target: + cond: + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: sunjian_a + key: pierce_through + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: "1 = \uC544\uAD70\uB3C4 \uD1B5\uACFC" + - skillId: sunjian_a + key: hit_bleed + target: + cond: + dur: 0 + p1: 2 + p2: 0.5 + p3: 1 + pMask: 7 + note: + - skillId: sunjian_a + key: invuln + target: + cond: + dur: 1 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: "\uD589\uB3D9 \uB05D \uB4A4 1\uD134 \uD53C\uD574 \uBB34\uD6A8" + - skillId: sunjian_p + key: atk_pct + target: camp:wu + cond: + dur: 0 + p1: 0.2 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: sunjian_p + key: kill_reaction + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: "\uD134\uB2F9" + - skillId: zhouyu_a + key: shot_blast + target: + cond: + dur: 0 + p1: 2.5 + p2: 3.5 + p3: 0 + pMask: 3 + note: "\uC6B4\uC11D \uBC18\uACBD(\uAE30\uBCF8\uAC12)\xB7\uACF5\uACA9 \uBC30\uC728" + - skillId: zhouyu_a + key: hit_burn + target: + cond: + dur: 0 + p1: 3 + p2: 0.05 + p3: 1 + pMask: 7 + note: + - skillId: zhouyu_a + key: fire_zone + target: + cond: + dur: 0 + p1: 1.5 + p2: 3 + p3: 1 + pMask: 7 + note: "\uBD88\uAF43 \uBC18\uACBD\xB7\uD134\xB7\uB2FF\uC744 \uB54C \uACF5\uACA9 + \uBC30\uC728" + - skillId: zhouyu_p + key: skill_hp_cost + target: + cond: + dur: 0 + p1: 0.05 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhouyu_p + key: skill_power_low_hp + target: + cond: + dur: 0 + p1: 0.1 + p2: 0.1 + p3: 0 + pMask: 3 + note: "HP p2 \uC783\uC744 \uB54C\uB9C8\uB2E4 \uC2A4\uD0AC \uC704\uB825 +p1" + - skillId: zhouyu_p + key: melee_penalty_off + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhouyu_p + key: range_mult + target: + cond: armed + dur: 0 + p1: 1.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: huanggai_a + key: dmg_taken_pct + target: + cond: + dur: 1 + p1: -0.3 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: huanggai_a + key: damage_to_atk + target: + cond: + dur: 1 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: huanggai_a + key: heal_on_kill + target: + cond: + dur: 1 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: huanggai_p + key: dmg_taken_pct + target: + cond: + dur: 0 + p1: -0.15 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: huanggai_p + key: low_hp_taken + target: + cond: + dur: 0 + p1: 0.002 + p2: 0.01 + p3: 0 + pMask: 3 + note: "\uC783\uC740 HP 1 %\uB2F9 \uBC1B\uB294 \uD53C\uD574\xB7\uBC18\uACA9 \uD53C\uD574" + - skillId: ganning_a + key: crit_always + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: ganning_a + key: shot_ricochet + target: + cond: + dur: 0 + p1: 3 + p2: 0.5 + p3: 0 + pMask: 3 + note: "\uB3C4\uD0C4 \uC218\xB7\uACF5\uACA9 \uBC30\uC728(\uC544\uAD70 \uD3EC\uD568)" + - skillId: ganning_p + key: counter_taken_pct + target: + cond: + dur: 0 + p1: -1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: ganning_p + key: ally_hit_warp + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: chengpu_a + key: repeat_action + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: chengpu_p + key: push_mult + target: + cond: + dur: 0 + p1: 1.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: chengpu_p + key: dmg_pct + target: + cond: vs_type:cavalry + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: sunce_a + key: sling_dist_pct + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: sunce_a + key: pierce_through + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: sunce_a + key: return_always + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: sunce_a + key: wall_bounce + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: sunce_p + key: sling_dist_pct + target: camp:wu + cond: + dur: 0 + p1: 0.3 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: sunce_p + key: counter_taken_pct + target: + cond: + dur: 0 + p1: -1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: sunce_p + key: return_on_hit + target: + cond: + dur: 0 + p1: 0 + p2: 0 + p3: 0 + pMask: 3 + note: + - skillId: sunce_p + key: dash_dmg_pct + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: "\uCD5C\uB300(\uAE30\uBCF8\uAC12 = \uC7A5\uBE44\xB7\uC5EC\uD3EC)" + - skillId: sunquan_a + key: shot_blast + target: + cond: + dur: 0 + p1: 1.5 + p2: 1.8 + p3: 0 + pMask: 3 + note: "\uD654\uC0B4\uBE44 \uBC18\uACBD(\uAE30\uBCF8\uAC12)\xB7\uACF5\uACA9 \uBC30\uC728" + - skillId: sunquan_a + key: hit_confuse + target: + cond: + dur: 0 + p1: 1 + p2: 0.5 + p3: 1 + pMask: 7 + note: + - skillId: sunquan_p + key: range_mult + target: camp:wu:archer + cond: + dur: 0 + p1: 1.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: sunquan_p + key: arrow_pass_allies + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 3 + note: + - skillId: sunquan_p + key: melee_penalty_off + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: lusu_a + key: shot_barricade + target: + cond: + dur: 0 + p1: 3 + p2: 1 + p3: 0 + pMask: 3 + note: "\uBAA9\uCC45 \uCE78 \uC218\xB7\uBC00\uAE30" + - skillId: lusu_p + key: atk_per_ally_range + target: + cond: + dur: 0 + p1: 0.05 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: lusu_p + key: turn_start_heal_range + target: + cond: + dur: 0 + p1: 0.1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: lusu_p + key: death_banner + target: + cond: + dur: 0 + p1: 5 + p2: 0 + p3: 0 + pMask: 1 + note: "\uAE43\uBC1C \uD134" + - skillId: lvmeng_a + key: reaction_on_hit + target: + cond: + dur: 0 + p1: 1 + p2: 3 + p3: 0 + pMask: 3 + note: "\uC7AC\uD589\uB3D9\xB7\uCD5C\uB300" + - skillId: lvmeng_a + key: reaction_on_kill + target: + cond: + dur: 0 + p1: 1 + p2: 3 + p3: 0 + pMask: 3 + note: + - skillId: lvmeng_p + key: counter_taken_pct + target: + cond: + dur: 0 + p1: -0.3 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: lvmeng_p + key: reaction_atk_stack + target: + cond: + dur: 0 + p1: 0.2 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: luxun_a + key: shot_firewall + target: + cond: + dur: 0 + p1: 0.6 + p2: 3 + p3: 3 + pMask: 7 + note: "\uC7A5\uBCBD \uBC18\uD3ED(\uAE30\uBCF8\uAC12)\xB7\uD134\xB7\uB2FF\uC744 + \uB54C \uACF5\uACA9 \uBC30\uC728" + - skillId: luxun_a + key: hit_burn + target: + cond: + dur: 0 + p1: 3 + p2: 0.05 + p3: 1 + pMask: 7 + note: + - skillId: luxun_p + key: dmg_pct + target: camp:wu + cond: vs_status + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: luxun_p + key: hit_confuse + target: + cond: + dur: 0 + p1: 1 + p2: 0.5 + p3: 1 + pMask: 7 + note: + - skillId: sunshangxiang_a + key: volley + target: + cond: + dur: 0 + p1: 1 + p2: 1 + p3: 0 + pMask: 3 + note: "\uACF5\uACA9 \uBC30\uC728(\uAE30\uBCF8\uAC12)\xB7\uBC00\uAE30(\uAE30\uBCF8\uAC12)" + - skillId: sunshangxiang_a + key: hit_bleed + target: + cond: crit + dur: 0 + p1: 2 + p2: 0.5 + p3: 1 + pMask: 7 + note: + - skillId: sunshangxiang_p + key: dmg_pct + target: + cond: vs_male_hero + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: sunshangxiang_p + key: melee_penalty_off + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: xiaoqiao_a + key: heal_atk + target: range + cond: + dur: 0 + p1: 0.8 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: xiaoqiao_a + key: turn_end_heal + target: + cond: + dur: 1 + p1: 3 + p2: 0.3 + p3: 0 + pMask: 3 + note: "\uBC18\uACBD\xB7\uACF5\uACA9 \uBC30\uC728" + - skillId: xiaoqiao_p + key: dmg_taken_pct + target: + cond: vs_male_hero + dur: 0 + p1: -0.3 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: xiaoqiao_p + key: ally_hit_heal + target: + cond: + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: lingtong_a + key: aoe_strike + target: + cond: + dur: 0 + p1: 3 + p2: 1.5 + p3: 1 + pMask: 7 + note: + - skillId: lingtong_p + key: atk_pct + target: + cond: + dur: 0 + p1: 0.15 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: lingtong_p + key: push_mult + target: + cond: + dur: 0 + p1: 1.3 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhoutai_a + key: shot_pierce + target: + cond: + dur: 0 + p1: 1.8 + p2: 1 + p3: 1 + pMask: 7 + note: "\uAC80\uAE30 \xB7 \uC544\uAD70\uB3C4 \uD53C\uD574 \xB7 \uBC00\uAE30(\uAE30\uBCF8\uAC12)" + - skillId: zhoutai_p + key: hit_confuse + target: + cond: crit + dur: 0 + p1: 1 + p2: 0.5 + p3: 1 + pMask: 7 + note: + - skillId: zhoutai_p + key: crit_always + target: + cond: vs_type:spear + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhugejin_a + key: shot_whirl + target: + cond: + dur: 0 + p1: 1 + p2: 3 + p3: 0 + pMask: 7 + note: "\uD68C\uC624\uB9AC \uBC18\uD3ED(\uAE30\uBCF8\uAC12)\xB7\uBC00\uAE30(\uAE30\uBCF8\uAC12)\xB7\uACF5\uACA9 + \uBC30\uC728(\uD53C\uD574 \uC5C6\uC74C)" + - skillId: zhugejin_a + key: hit_confuse + target: + cond: + dur: 0 + p1: 1 + p2: 0.5 + p3: 1 + pMask: 7 + note: + - skillId: zhugejin_p + key: splash_ally_ratio + target: + cond: + dur: 0 + p1: 0 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: dongxi_a + key: atk_ratio + target: + cond: + dur: 0 + p1: 1.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: dongxi_p + key: atk_pct + target: + cond: + dur: 0 + p1: 0.1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: lvbu_a + key: invuln + target: + cond: + dur: 1 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: lvbu_a + key: counter_dmg_pct + target: + cond: + dur: 1 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: lvbu_a + key: sling_dist_pct + target: + cond: + dur: 1 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: lvbu_a + key: return_always + target: + cond: + dur: 1 + p1: 0 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: lvbu_p + key: atk_pct + target: camp:other + cond: + dur: 0 + p1: 0.2 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: lvbu_p + key: counter_taken_pct + target: + cond: + dur: 0 + p1: -1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: lvbu_p + key: matchup_floor + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: lvbu_p + key: dash_dmg_pct + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhangjiao_a + key: invuln + target: + cond: + dur: 1 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhangjiao_p + key: dmg_taken_from_archer_pct + target: + cond: + dur: 0 + p1: -0.3 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhangjiao_p + key: hit_poison + target: + cond: + dur: 0 + p1: 5 + p2: 0.02 + p3: 0.5 + pMask: 7 + note: "\uD134(\uAE30\uBCF8\uAC12)\xB7\uD655\uB960" + - skillId: huaxiong_a + key: dmg_pct + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: huaxiong_a + key: counter_taken_pct + target: + cond: + dur: 0 + p1: -1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: huaxiong_p + key: atk_pct + target: + cond: turn_le:3 + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: huaxiong_p + key: counter_taken_pct + target: + cond: turn_le:3 + dur: 0 + p1: -0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: yanliang_a + key: pierce_through + target: + cond: + dur: 0 + p1: 0 + p2: 0 + p3: 0 + pMask: 1 + note: "0 = \uC544\uAD70\uC5D0 \uB9C9\uD798" + - skillId: yanliang_a + key: hit_bleed + target: + cond: + dur: 0 + p1: 1 + p2: 0.5 + p3: 1 + pMask: 7 + note: + - skillId: yanliang_p + key: counter_dmg_pct + target: + cond: + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: yanliang_p + key: counter_lifesteal + target: + cond: + dur: 0 + p1: 0.3 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: wenchou_a + key: dmg_pct + target: + cond: + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: wenchou_a + key: return_always + target: + cond: + dur: 0 + p1: 0 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: wenchou_p + key: heal_on_kill + target: + cond: + dur: 0 + p1: 0.3 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: gongsunzan_a + key: sling_dist_pct + target: + cond: + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: gongsunzan_a + key: lifesteal + target: + cond: + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: gongsunzan_a + key: return_always + target: + cond: + dur: 0 + p1: 0 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: gongsunzan_p + key: dmg_pct + target: + cond: vs_type:archer + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: gongsunzan_p + key: heal_on_kill + target: + cond: + dur: 0 + p1: 0.3 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: dongzhuo_a + key: rampage + target: + cond: + dur: 0 + p1: 3 + p2: 1.5 + p3: 0 + pMask: 3 + note: "\uB3CC\uC9C4 \uCD08\xB7\uBC00\uCE58\uAE30 \uAC70\uB9AC(\uAE30\uBCF8\uAC12)" + - skillId: dongzhuo_p + key: dmg_taken_pct + target: camp:other + cond: + dur: 0 + p1: -0.1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: dongzhuo_p + key: lifesteal + target: + cond: + dur: 0 + p1: 0.3 + p2: 0.05 + p3: 0 + pMask: 3 + note: "p2 = \uC544\uAD70 1\uAE30 \uC904 \uB54C\uB9C8\uB2E4 +" + - skillId: lijue_a + key: splash_heal_lowest + target: + cond: + dur: 0 + p1: 0.5 + p2: 2 + p3: 0 + pMask: 3 + note: "\uAD11\uC5ED \uD53C\uD574 \uBE44\uC728\xB7\uC778\uC811 \uBC18\uACBD(\uAE30\uBCF8\uAC12)" + - skillId: lijue_p + key: action_end_sacrifice + target: + cond: + dur: 0 + p1: 0.1 + p2: 0.5 + p3: 2 + pMask: 7 + note: "HP \uC18C\uBAA8\xB7\uACF5\uACA9 %\xB7\uC778\uC811 \uBC18\uACBD(\uAE30\uBCF8\uAC12)" + - skillId: yuanshao_a + key: sling_dist_pct + target: range + cond: + dur: 1 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: yuanshao_a + key: crit_dmg_pct + target: range + cond: + dur: 1 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: yuanshao_a + key: crit_chance + target: range + cond: + dur: 1 + p1: 0.2 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: yuanshao_p + key: hp_max_pct + target: camp:other + cond: + dur: 0 + p1: 0.2 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: yuanshao_p + key: hit_bleed + target: + cond: crit + dur: 0 + p1: 2 + p2: 0.5 + p3: 1 + pMask: 7 + note: + - skillId: yuanshao_p + key: hit_push + target: + cond: crit + dur: 0 + p1: 0.3 + p2: 0 + p3: 0 + pMask: 1 + note: "\uC790\uC2E0 \uC2AC\uB9C1 \uCD5C\uB300 \uAC70\uB9AC \xD7 p1" + - skillId: yuanshu_a + key: heal_self_pct + target: + cond: + dur: 0 + p1: 0.6 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: yuanshu_a + key: dmg_taken_pct + target: + cond: + dur: 1 + p1: -0.3 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: yuanshu_p + key: dmg_taken_pct + target: + cond: + dur: 0 + p1: 0.15 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: yuanshu_p + key: atk_pct + target: radius:3 + cond: + dur: 0 + p1: 0.3 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: yuanshu_p + key: on_attacked_confuse + target: + cond: + dur: 0 + p1: 1 + p2: 0.5 + p3: 0.5 + pMask: 7 + note: + - skillId: diaochan_a + key: charm_pull + target: + cond: + dur: 0 + p1: 1 + p2: 1 + p3: 0 + pMask: 3 + note: "\uD589\uB3D9 \uBD88\uAC00 \uD134\xB7\uB0A8\uC131\uB9CC" + - skillId: diaochan_p + key: atk_pct + target: camp_male:other + cond: + dur: 0 + p1: 0.25 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: diaochan_p + key: multi_male_stun + target: + cond: + dur: 0 + p1: 2 + p2: 0.5 + p3: 1 + pMask: 7 + note: "\uB0A8\uC131 \uC218\xB7\uD655\uB960\xB7\uD134" + - skillId: diaochan_p + key: dmg_taken_pct + target: + cond: vs_male + dur: 0 + p1: -0.3 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhurong_a + key: shot_as_archer + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhurong_a + key: atk_ratio + target: + cond: + dur: 0 + p1: 2.2 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhurong_a + key: hit_poison + target: + cond: + dur: 0 + p1: 5 + p2: 0.02 + p3: 1 + pMask: 7 + note: + - skillId: zhurong_p + key: lifesteal + target: + cond: + dur: 0 + p1: 0.2 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhurong_p + key: counter_dmg_pct + target: + cond: + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhurong_p + key: terrain_immune + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhurong_p + key: atk_pct + target: + cond: vs_female_hero + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: menghuo_a + key: counter_taken_pct + target: + cond: + dur: 0 + p1: -1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: menghuo_a + key: self_bounce_mult + target: + cond: + dur: 0 + p1: 0 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: menghuo_a + key: reaction_on_terrain + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: menghuo_p + key: lifesteal + target: + cond: + dur: 0 + p1: 0.2 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: menghuo_p + key: revive + target: + cond: + dur: 0 + p1: 0.1 + p2: 7 + p3: 0.6 + pMask: 7 + note: "HP \uBE44\uC728\xB7\uD69F\uC218\xB7\uB9DE\uAE30 \uC804 HP \uC870\uAC74" + - skillId: gaoshun_a + key: hit_stun + target: + cond: + dur: 0 + p1: 1 + p2: 1 + p3: 1 + pMask: 7 + note: "p2 1 = \uBC00\uB9B0 \uC544\uAD70\xB7\uC9C0\uD615 \uD53C\uD574\uB3C4" + - skillId: gaoshun_p + key: push_continue + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhanglu_a + key: shot_single + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhanglu_a + key: atk_ratio + target: + cond: + dur: 0 + p1: 2.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhanglu_a + key: hit_poison + target: + cond: + dur: 0 + p1: 5 + p2: 0.02 + p3: 1 + pMask: 7 + note: + - skillId: zhanglu_a + key: poison_zone + target: + cond: + dur: 0 + p1: 1 + p2: 1 + p3: 0 + pMask: 3 + note: "\uBC18\uACBD(\uAE30\uBCF8\uAC12)\xB7\uD134" + - skillId: zhanglu_p + key: zombie + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: "\uB418\uC0B4\uB9B0 HP" + - skillId: yanbaihu_a + key: atk_pct + target: + cond: + dur: 0 + p1: 0.3 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: yanbaihu_a + key: crit_chance + target: + cond: + dur: 0 + p1: 0.15 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: yanbaihu_p + key: hp_max_pct + target: + cond: + dur: 0 + p1: 0.1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: hanxuan_a + key: counter_taken_pct + target: + cond: + dur: 0 + p1: -1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: hanxuan_a + key: push_mult + target: + cond: + dur: 0 + p1: 2 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: hanxuan_p + key: counter_taken_pct + target: + cond: + dur: 0 + p1: -0.2 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: jinxuan_a + key: crit_chance + target: + cond: + dur: 0 + p1: 0.3 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: jinxuan_a + key: crit_dmg_pct + target: + cond: + dur: 0 + p1: 0.5 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: jinxuan_p + key: dmg_taken_from_archer_pct + target: + cond: + dur: 0 + p1: -0.15 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: jinxuan_p + key: dmg_pct + target: + cond: vs_type:archer + dur: 0 + p1: 0.15 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhaofan_a + key: shot_pierce + target: + cond: + dur: 0 + p1: 1.6 + p2: 2 + p3: 0 + pMask: 7 + note: "\uC544\uAD70 2 = \uD68C\uBCF5(ally_hit_heal)" + - skillId: zhaofan_a + key: ally_hit_heal + target: + cond: + dur: 0 + p1: 0.6 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhaofan_p + key: negate_hits + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: zhangren_a + key: shot_pierce + target: + cond: + dur: 0 + p1: 1.5 + p2: 0 + p3: 0 + pMask: 7 + note: + - skillId: zhangren_p + key: range_mult + target: + cond: + dur: 0 + p1: 1.3 + p2: 0 + p3: 0 + pMask: 1 + note: + - skillId: chunyuqiong_a + key: return_on_hit + target: + cond: + dur: 0 + p1: 0 + p2: 0 + p3: 0 + pMask: 3 + note: + - skillId: chunyuqiong_p + key: atk_missing_hp + target: + cond: + dur: 0 + p1: 1 + p2: 0 + p3: 0 + pMask: 1 + note: diff --git a/Assets/Scripts/OutGame/Data/HeroDb.cs b/Assets/Scripts/OutGame/Data/HeroDb.cs index 0d1f15d2..b5cbf927 100644 --- a/Assets/Scripts/OutGame/Data/HeroDb.cs +++ b/Assets/Scripts/OutGame/Data/HeroDb.cs @@ -31,6 +31,13 @@ namespace TKS.OutGame [Tooltip("병사 유닛 = 승급·진화 없음 · 중복 = 골드")] public bool isSoldier; public string firstStage; + [Tooltip("m|f — 스킬 조건(남성·여성 영웅 · 2026-11-10 #1112)")] + public string gender = "m"; + + public bool Female + { + get { return gender == "f"; } + } public List gear = new List(); } diff --git a/Assets/Scripts/OutGame/Data/SkillInfoDb.cs b/Assets/Scripts/OutGame/Data/SkillInfoDb.cs index 1676307a..802404fb 100644 --- a/Assets/Scripts/OutGame/Data/SkillInfoDb.cs +++ b/Assets/Scripts/OutGame/Data/SkillInfoDb.cs @@ -60,8 +60,33 @@ namespace TKS.OutGame public int gold; } + /// skill_effects.csv 1행 = 스킬 효과 1줄(2026-11-10 #1112 · 키 · 대상 · 조건 · 지속 · p1~p3 · 뜻 = Runtime/Skills/SkillKeys). + [Serializable] + public class Effect + { + public string skillId; + public string key; + [Tooltip("빈칸 = 자신 · allies · camp:<진영>[:<병종>] · camp_male:<진영> · radius: · range · aura")] + public string target; + [Tooltip("crit · vs_status · vs_bleed · vs_confused · vs_stunned · vs_type:<병종> · vs_male · vs_male_hero · vs_female_hero · turn_le: · armed")] + public string cond; + [Tooltip("0 = 상시·장착 중·1회 · n = n 라운드")] + public int dur; + public float p1; + public float p2; + public float p3; + public int pMask; + public string note; + + public bool HasP(int i) + { + return (pMask & (1 << i)) != 0; + } + } + public List skills = new List(); public List levels = new List(); + public List effects = new List(); public Skill Get(string id) { diff --git a/Assets/Scripts/OutGame/OutGameTables.cs b/Assets/Scripts/OutGame/OutGameTables.cs index 84d3aa8b..4da09d1a 100644 --- a/Assets/Scripts/OutGame/OutGameTables.cs +++ b/Assets/Scripts/OutGame/OutGameTables.cs @@ -93,6 +93,7 @@ namespace TKS.OutGame { "heroes.csv", "skills.csv", "hero_levels.csv", "promotion.csv", "evolve.csv", "gear_items.csv", "hero_gear.csv", "skill_levels.csv", "gacha.csv", "gacha_pool.csv", "stage_rewards.csv", "economy.csv", + "skill_effects.csv", // 스킬 효과 1줄 표(2026-11-10 #1112) // C 단계(PD #1093 C) "training.csv", "training_ranks.csv", "patrol.csv", "dev_tree.csv", "treasures.csv", "treasure_sets.csv", "chests.csv", "tower.csv", "dungeons.csv", "arena.csv", }; @@ -128,7 +129,7 @@ namespace TKS.OutGame d.Evolve = ParseEvolve(rows["evolve.csv"], e); d.Levels = ParseLevels(rows["hero_levels.csv"], e); d.Gear = ParseGear(rows["gear_items.csv"], e); - d.Skills = ParseSkills(rows["skills.csv"], rows["skill_levels.csv"], e); + d.Skills = ParseSkills(rows["skills.csv"], rows["skill_levels.csv"], rows["skill_effects.csv"], e); d.Heroes = ParseHeroes(rows["heroes.csv"], rows["hero_gear.csv"], d, e); d.Gacha = ParseGacha(rows["gacha.csv"], rows["gacha_pool.csv"], d, e); d.Rewards = ParseRewards(rows["stage_rewards.csv"], d, e); @@ -231,9 +232,48 @@ namespace TKS.OutGame return t; } - static SkillInfoDb ParseSkills(List> rows, List> lvRows, List e) + static SkillInfoDb ParseSkills(List> rows, List> lvRows, List> fxRows, List e) { var t = ScriptableObject.CreateInstance(); + // 효과 1줄 표(2026-11-10 #1112) — 키 · 대상 · 조건 · 지속 · p1~p3 · 스킬마다 1줄 이상 + const string fx = "skill_effects.csv"; + var fxKeys = new Dictionary>(); + if (Cols(fxRows, fx, e, "skill_id", "key", "target", "cond", "dur", "p1", "p2", "p3", "note")) + { + foreach (var r in fxRows) + { + var x = new SkillInfoDb.Effect + { + skillId = Get(r, "skill_id"), key = Get(r, "key"), target = Get(r, "target"), cond = Get(r, "cond"), note = Get(r, "note"), + dur = Get(r, "dur").Length == 0 ? 0 : Int(r, "dur", fx, e), + }; + string at = fx + " 행 " + Get(r, "#") + " " + x.skillId + "/" + x.key; + if (!SkillKeys.Known(x.key)) e.Add(at + ": 처리기 없는 키"); + var pc = new[] { "p1", "p2", "p3" }; + var pv = new float[3]; + for (int i = 0; i < 3; i++) + { + var v = Get(r, pc[i]); + if (v.Length == 0) continue; + if (!float.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out pv[i])) e.Add(at + ": " + pc[i] + " 숫자 아님(「" + v + "」)"); + else x.pMask |= 1 << i; + } + x.p1 = pv[0]; + x.p2 = pv[1]; + x.p3 = pv[2]; + if (!x.HasP(0)) e.Add(at + ": p1 비었음"); + if (x.dur < 0) e.Add(at + ": dur < 0"); + var probe = new SkillEffect(); + var te = SkillEffect.ParseTarget(x.target, probe); + if (te != null) e.Add(at + ": target " + te); + var ce = SkillEffect.ParseCond(x.cond, probe); + if (ce != null) e.Add(at + ": cond " + ce); + List l; + if (!fxKeys.TryGetValue(x.skillId, out l)) fxKeys[x.skillId] = l = new List(); + if (!l.Contains(x.key)) l.Add(x.key); + t.effects.Add(x); + } + } const string f = "skills.csv"; 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")) { @@ -268,13 +308,11 @@ namespace TKS.OutGame s.p1 = ps[0]; s.p2 = ps[1]; s.p3 = ps[2]; + // effect_key 칸 = 효과 표(skill_effects.csv) 키 목록(;) — 수치는 효과 표에만(2026-11-10 #1112) 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) + " 비었음"); - } + List fk; + if (!fxKeys.TryGetValue(s.id, out fk)) e.Add(at + ": skill_effects.csv 에 효과 줄 없음"); + else if (string.Join(";", keys) != string.Join(";", fk.ToArray())) e.Add(at + ": effect_key 「" + s.effectKey + "」 ≠ 효과 표 키 「" + string.Join(";", fk.ToArray()) + "」"); if (active && s.cooldown < 1) e.Add(at + ": 액티브 cooldown < 1"); if (s.lvMax < 1) e.Add(at + ": lv_max < 1"); t.skills.Add(s); @@ -288,6 +326,8 @@ namespace TKS.OutGame 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); + foreach (var x in t.effects) + if (t.Get(x.skillId) == null) e.Add(fx + ": " + x.skillId + " 이 skills.csv 에 없음"); return t; } @@ -295,7 +335,7 @@ namespace TKS.OutGame { var t = ScriptableObject.CreateInstance(); const string f = "heroes.csv"; - if (!Cols(rows, f, e, "id", "name_ko", "hanja", "faction", "unit_type", "rarity", "hp_mult", "atk_mult", "def_mult", "art", "skill_active", "skill_passive", "is_soldier", "first_stage")) return t; + if (!Cols(rows, f, e, "id", "name_ko", "hanja", "faction", "unit_type", "rarity", "hp_mult", "atk_mult", "def_mult", "art", "skill_active", "skill_passive", "is_soldier", "first_stage", "gender")) return t; var seen = new HashSet(); foreach (var r in rows) { @@ -308,7 +348,10 @@ namespace TKS.OutGame id = Get(r, "id"), nameKo = Get(r, "name_ko"), hanja = Get(r, "hanja"), faction = Get(r, "faction"), type = type, rarity = Int(r, "rarity", f, e), hpMult = Float(r, "hp_mult", f, e), atkMult = Float(r, "atk_mult", f, e), defMult = Float(r, "def_mult", f, e), art = Get(r, "art"), skillActive = Get(r, "skill_active"), skillPassive = Get(r, "skill_passive"), isSoldier = soldier == "1", firstStage = Get(r, "first_stage"), + gender = Get(r, "gender") == "f" ? "f" : "m", }; + var gd = Get(r, "gender"); + if (gd != "m" && gd != "f") e.Add(f + " 행 " + Get(r, "#") + ": gender 「" + gd + "」 ≠ m|f"); if (!Unique(seen, h.id, f, r, e)) continue; if (h.rarity < 1 || h.rarity > 3) e.Add(f + ": " + h.id + " rarity " + h.rarity + " ≠ 1~3"); foreach (var sid in new[] { h.skillActive, h.skillPassive }) diff --git a/Assets/Scripts/Runtime/AI/Sim.cs b/Assets/Scripts/Runtime/AI/Sim.cs index 4905d7e7..fbca5769 100644 --- a/Assets/Scripts/Runtime/AI/Sim.cs +++ b/Assets/Scripts/Runtime/AI/Sim.cs @@ -46,20 +46,11 @@ namespace TKS public bool Acted, Bonus, Overwatch, Collider, Active; } - public class SkillSnap - { - public UnitSkills S; - public int ReadyTurn, BonusMovesLeft, PierceHits; - public bool Reserved, Armed, BonusAttack, OwUnlimited; - public float OwMult, OwAdd; - } - public class Snapshot { public readonly List Units = new List(); - public readonly List Skills = new List(); - public List Timed; - public int LogCount; + /// 스킬 상태 전부(유닛별 값 · 시한 효과 · 상태 이상 · 지대 · 2026-11-10 #1112). + public SkillSystem.State Skills; public GimmickLayer.Snapshot Gimmicks; public string Message; } @@ -77,19 +68,7 @@ namespace TKS }); } var sys = bm.Skills; - if (sys != null) - { - foreach (var e in sys.Entries) - { - s.Skills.Add(new SkillSnap - { - S = e, ReadyTurn = e.ReadyTurn, BonusMovesLeft = e.BonusMovesLeft, PierceHits = e.PierceHits, Reserved = e.Reserved, Armed = e.Armed, - BonusAttack = e.BonusAttack, OwUnlimited = e.OwUnlimited, OwMult = e.OwMult, OwAdd = e.OwAdd - }); - } - s.Timed = new List(sys.TimedEffects); - s.LogCount = sys.Log.Count; - } + if (sys != null) s.Skills = sys.TakeState(); if (GimmickLayer.Current != null) s.Gimmicks = GimmickLayer.Current.Take(); return s; } @@ -112,28 +91,7 @@ namespace TKS if (u.gameObject.activeSelf != x.Active) u.gameObject.SetActive(x.Active); } var sys = bm.Skills; - if (sys != null) - { - foreach (var k in s.Skills) - { - var e = k.S; - e.ReadyTurn = k.ReadyTurn; - e.BonusMovesLeft = k.BonusMovesLeft; - e.PierceHits = k.PierceHits; - e.Reserved = k.Reserved; - e.Armed = k.Armed; - e.BonusAttack = k.BonusAttack; - e.OwUnlimited = k.OwUnlimited; - e.OwMult = k.OwMult; - e.OwAdd = k.OwAdd; - } - if (s.Timed != null) - { - sys.TimedEffects.Clear(); - sys.TimedEffects.AddRange(s.Timed); - } - if (sys.Log.Count > s.LogCount) sys.Log.RemoveRange(s.LogCount, sys.Log.Count - s.LogCount); - } + if (sys != null) sys.RestoreState(s.Skills); if (GimmickLayer.Current != null && s.Gimmicks != null) GimmickLayer.Current.Restore(s.Gimmicks); bm.LastMessage = s.Message; Physics2D.SyncTransforms(); diff --git a/Assets/Scripts/Runtime/ActionResolver.Skills.cs b/Assets/Scripts/Runtime/ActionResolver.Skills.cs new file mode 100644 index 00000000..e96b9538 --- /dev/null +++ b/Assets/Scripts/Runtime/ActionResolver.Skills.cs @@ -0,0 +1,369 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace TKS +{ + /// + /// 스킬 특수 사격(2026-11-10 #1112 · 효과 표 skill_effects.csv · 수치 = 표 p 칸): + /// shot_giant(황충 거대 화살 · 사거리 무관 · 첫 적) · shot_pierce(직선 관통 · 아군 통과/피해/회복 · 밀기) · shot_blast(투사체 → 첫 적 자리 폭발 · 불꽃 지대) · + /// shot_fan(부채꼴 n 발 × 연속) · shot_whirl(회오리 = 직선 안 적 밀기) · shot_barricade(목책 세우기) · shot_firewall(불 장벽 지대) · + /// 일반 사격 덧붙임 = multishot(멀티샷) · shot_ricochet(도탄) · poison_zone(독지대) · 아군 명중 = 회복·버프·이동(ally_hit_*). + /// + public partial class ActionResolver + { + static readonly string[] ShotKeys = + { + SkillKeys.ShotGiant, SkillKeys.ShotPierce, SkillKeys.ShotBlast, SkillKeys.ShotFan, SkillKeys.ShotWhirl, SkillKeys.ShotBarricade, SkillKeys.ShotFirewall + }; + + static bool HasSpecialShot(Unit u) + { + foreach (var k in ShotKeys) + if (SkillMods.Has(u, k)) return true; + return false; + } + + IEnumerator SpecialShot(Unit u, Vector2 origin, Vector2 dir, Vector2 target, ActionOutcome o) + { + o.Fired = true; + Face(u, dir); + if (u.View != null) u.View.PlayAttack(); + if (u.Type == UnitType.Strategist) BattleEvents.Raise(BattleEvents.StrategistShots, u); + SkillEffect e; + if ((e = SkillMods.First(u, SkillKeys.ShotGiant)) != null) yield return GiantArrow(u, dir, e, o); + else if ((e = SkillMods.First(u, SkillKeys.ShotPierce)) != null) yield return PierceShot(u, dir, e, o); + else if ((e = SkillMods.First(u, SkillKeys.ShotBlast)) != null) yield return BlastShot(u, dir, e, o); + else if ((e = SkillMods.First(u, SkillKeys.ShotFan)) != null) yield return FanShot(u, dir, e, o); + else if ((e = SkillMods.First(u, SkillKeys.ShotWhirl)) != null) yield return WhirlShot(u, dir, e, o); + else if ((e = SkillMods.First(u, SkillKeys.ShotBarricade)) != null) yield return Barricade(u, dir, target, e, o); + else if ((e = SkillMods.First(u, SkillKeys.ShotFirewall)) != null) yield return Firewall(u, dir, e, o); + battle.NotifyChanged(); + yield return Fx.Wait(0.2f); + } + + /// 황충 백발백중: 사거리 무관 · 반지름 p1 거대 화살 → 아군·장애물 통과 · 첫 적 = 공격력 × p2. + IEnumerator GiantArrow(Unit u, Vector2 dir, SkillEffect e, ActionOutcome o) + { + Vector2 from = u.Position; + float far = battle.Board != null ? 200f : 60f; + float travel; + GimmickInstance g; + Combat.ArrowIgnoreGimmicks = true; + Unit hit; + try + { + hit = Combat.FirstUnitOnArrow(u, from, dir, far, Mathf.Max(cfg.arrowRadius, e.P), u.Faction, out travel, out g); + } + finally + { + Combat.ArrowIgnoreGimmicks = false; + } + Vector2 to = from + dir * travel; + yield return Fx.Arrow(from, to, new Color(1f, 0.85f, 0.4f)); + if (hit != null) + { + o.ShotHit = hit; + ShotDamage(u, hit, Combat.SkillDamage(cfg, u, hit, e.Get(1, 1f), rng), false, o); + } + } + + /// 직선 관통(태사자·장임·왕랑·조범·주태 검기): 사거리 안 직선 위 유닛 전원(장애물 무시) · 적 = 공격력 × p1 · 아군 = p2(0 통과 · 1 피해 · 2 회복) · p3 = 밀기. + IEnumerator PierceShot(Unit u, Vector2 dir, SkillEffect e, ActionOutcome o) + { + Vector2 from = u.Position; + float range = SkillMods.Range(u) + u.Radius; + float travel = Combat.WallTravel(u, from, dir, range); + var dists = new List(); + var units = Combat.UnitsOnPath(u, from, dir, travel, dists); + yield return Fx.Arrow(from, from + dir * travel, u.Type == UnitType.Strategist ? Fx.StrategistShotColor : new Color(0.75f, 0.95f, 1f)); + int allyMode = Mathf.RoundToInt(e.Get(1)); + var pushed = new List(); + foreach (var t in units) + { + if (!t.IsAlive) continue; + if (t.Faction == u.Faction) + { + if (allyMode == 0) continue; + if (allyMode == 2) + { + AllyHitSpecial(u, t, from, o, true); + continue; + } + } + if (o.ShotHit == null) o.ShotHit = t; + ShotDamage(u, t, Combat.SkillDamage(cfg, u, t, e.P, rng), false, o); + if (t.IsAlive) pushed.Add(t); + } + float push = e.Get(2); + if (push > 0.01f) + { + pushed.Reverse(); // 먼 쪽부터 밀어 서로 막히지 않게 + foreach (var t in pushed) + if (t.IsAlive) yield return Push(t, dir, push * SkillMods.KnockbackTakenMult(t, true), u, o); + } + } + + /// 폭발 사격(서서 화염구 · 가후 독구름 · 주유 운석 · 손권 화살비): 투사체 → 사거리 안 첫 적(아군 너머) 자리 · 반경 p1 안 적 전원 = 공격력 × p2 · 주유 = 불꽃 지대. + IEnumerator BlastShot(Unit u, Vector2 dir, SkillEffect e, ActionOutcome o) + { + Vector2 from = u.Position; + float range = SkillMods.Range(u) + u.Radius; + float travel; + GimmickInstance g; + var hit = Combat.FirstUnitOnArrow(u, from, dir, range, cfg.arrowRadius, u.Faction, out travel, out g); + Vector2 center = hit != null ? hit.Position : from + dir * travel; + yield return Fx.Arrow(from, center, u.Type == UnitType.Strategist ? Fx.StrategistShotColor : Fx.ArrowColor); + float r = e.P; + Fx.Burst(center, r, ExplosionColor); + o.ShotHit = hit; + foreach (var t in Combat.UnitsInCircle(center, r, u, null)) + { + if (!t.IsAlive || t.Faction == u.Faction) continue; + ShotDamage(u, t, Combat.SkillDamage(cfg, u, t, e.Get(1, 1f), rng), false, o); + } + var fz = SkillMods.First(u, SkillKeys.FireZone); + var sys = SkillSystem.Current; + if (fz != null && sys != null && sys.Of(u) != null) + sys.AddZone(sys.Of(u), center, center, fz.P, Mathf.RoundToInt(fz.Get(1, 1f)), fz.Get(2), SkillMods.First(u, SkillKeys.HitBurn), new Color(1f, 0.45f, 0.1f, 0.35f)); + } + + /// 하후연 전광 연사: 부채꼴(p3 도) p1 발 × p2 번 · 화살마다 일반 사격 규칙(첫 유닛 · 아군 오사). + IEnumerator FanShot(Unit u, Vector2 dir, SkillEffect e, ActionOutcome o) + { + int n = Mathf.Max(1, Mathf.RoundToInt(e.P)); + int volleys = Mathf.Max(1, Mathf.RoundToInt(e.Get(1, 1f))); + float spread = e.Get(2, 30f); + for (int v = 0; v < volleys && u.IsAlive; v++) + { + for (int i = 0; i < n; i++) + { + float ang = n > 1 ? -spread * 0.5f + spread * i / (n - 1) : 0f; + FireOne(u, Rotate(dir, ang), o, 1f); + } + battle.NotifyChanged(); + yield return Fx.Wait(0.18f); + } + } + + static Vector2 Rotate(Vector2 v, float deg) + { + float r = deg * Mathf.Deg2Rad; + float c = Mathf.Cos(r), s = Mathf.Sin(r); + return new Vector2(v.x * c - v.y * s, v.x * s + v.y * c); + } + + /// 화살 1발(일반 사격 규칙 · 아군 통과 패시브 반영 · 아군 명중 = 회복류 또는 오사) — 부채꼴 · 멀티샷. + void FireOne(Unit u, Vector2 d, ActionOutcome o, float ratio) + { + float range = SkillMods.Range(u) + u.Radius; + bool obstacles; + var pass = ArrowPass(u, out obstacles); + float travel; + GimmickInstance g; + Combat.ArrowIgnoreGimmicks = obstacles; + Unit hit; + try + { + hit = Combat.FirstUnitOnArrow(u, u.Position, d, range, cfg.arrowRadius, pass, out travel, out g); + } + finally + { + Combat.ArrowIgnoreGimmicks = false; + } + Fx.Run(Fx.Arrow(u.Position, u.Position + d * (hit != null ? travel + cfg.arrowRadius : travel))); + if (hit == null) return; + if (o.ShotHit == null) o.ShotHit = hit; + if (hit.Faction == u.Faction && AllyHitSpecial(u, hit, u.Position, o, true)) return; + ShotDamage(u, hit, ratio >= 0.999f && ratio <= 1.001f ? Combat.Damage(cfg, u, hit, false, rng, false) : Combat.SkillDamage(cfg, u, hit, ratio, rng), false, o); + } + + /// 제갈근 선풍계: 사거리 안 폭 p1 직선 위 적 전원을 진행 방향으로 p2 밀침(먼 쪽부터) · 피해 = 공격력 × p3(0 = 없음) · 명중 상태 이상(혼란). + IEnumerator WhirlShot(Unit u, Vector2 dir, SkillEffect e, ActionOutcome o) + { + Vector2 from = u.Position; + float range = SkillMods.Range(u) + u.Radius; + float travel = Combat.WallTravel(u, from, dir, range); + Vector2 to = from + dir * travel; + yield return Fx.Arrow(from, to, new Color(0.7f, 1f, 0.85f)); + var targets = new List(); + foreach (var t in battle.AliveUnits(u.Faction == Faction.Player ? Faction.Enemy : Faction.Player)) + if (Combat.SegmentPointDistance(from, to, t.Position) <= e.P + t.Radius) targets.Add(t); + targets.Sort((a, b) => Vector2.Distance(from, b.Position).CompareTo(Vector2.Distance(from, a.Position))); + var sys = SkillSystem.Current; + foreach (var t in targets) + { + if (!t.IsAlive) continue; + if (e.Get(2) > 0f) ShotDamage(u, t, Combat.SkillDamage(cfg, u, t, e.Get(2), rng), false, o); + else + { + o.Attacked.Add(t); + if (sys != null) sys.ApplyHitStatuses(u, t, false, HitKind.Skill); + } + if (t.IsAlive) yield return Push(t, dir, e.Get(1, 1f) * SkillMods.KnockbackTakenMult(t, true), u, o); + } + if (o.Attacked.Count == 0) o.MovedOnly = true; + } + + /// 노숙 녹각진: 슬링 목표점(사거리 안)에 진행 방향에 수직으로 목책 p1 칸(칸 지름 p3) · 그 자리 적 = 목책 충돌 피해(기믹 표 자기 피해 %)·p2 밀기 · 아군 = 밀기만. + IEnumerator Barricade(Unit u, Vector2 dir, Vector2 target, SkillEffect e, ActionOutcome o) + { + int n = Mathf.Max(1, Mathf.RoundToInt(e.P)); + float size = Mathf.Max(0.3f, e.Get(2, 1f)); + float range = SkillMods.Range(u) + u.Radius; + Vector2 c = target; + if (Vector2.Distance(u.Position, c) > range) c = u.Position + dir * range; + c = battle.Board.ClampInside(c, size * 0.5f); + Vector2 perp = new Vector2(-dir.y, dir.x); + var layer = GimmickLayer.Current; + var def = GimmickTables.Db != null ? GimmickTables.Db.Get(GimmickIds.Palisade) : null; + float pct = def != null ? Mathf.Max(def.cavSelfPct, def.bouncePct) : 0f; + yield return Fx.Arrow(u.Position, c, Fx.StrategistShotColor); + var sys = SkillSystem.Current; + for (int i = 0; i < n; i++) + { + Vector2 p = c + perp * ((i - (n - 1) * 0.5f) * size); + p = battle.Board.ClampInside(p, size * 0.5f); + foreach (var t in Combat.UnitsInCircle(p, size * 0.5f, null, null)) + { + if (!t.IsAlive) continue; + if (t.Faction != u.Faction && pct > 0f && sys != null) + { + int d = sys.Hit(t, Mathf.Max(1, Mathf.RoundToInt(t.MaxHp * pct)), u, HitKind.Skill); + o.Attacked.Add(t); + o.DamageDealt += d; + Fx.Popup(t.Position, d + " 목책", new Color(1f, 0.75f, 0.4f)); + } + if (t.IsAlive) yield return Push(t, dir, Mathf.Max(e.Get(1, 1f), size), u, o); + } + if (layer != null) layer.Spawn(GimmickIds.Palisade, GimmickShape.Circle(p, size * 0.5f)); + } + o.Notes.Add("목책 " + n + "칸"); + } + + /// 육손 이릉의 화공: 진행 방향 사거리까지 반폭 p1 불 장벽(p2 턴) · 그 안 적 = 공격력 × p3 + 화상 · 장벽 = 닿는 적마다 같은 피해·화상(지대). + IEnumerator Firewall(Unit u, Vector2 dir, SkillEffect e, ActionOutcome o) + { + Vector2 a = u.Position + dir * (u.Radius + e.P); + float range = SkillMods.Range(u) + u.Radius; + Vector2 b = u.Position + dir * Mathf.Max(u.Radius + e.P, Combat.WallTravel(u, u.Position, dir, range)); + yield return Fx.Arrow(u.Position, b, new Color(1f, 0.45f, 0.1f)); + var sys = SkillSystem.Current; + SkillZone z = null; + if (sys != null && sys.Of(u) != null) + z = sys.AddZone(sys.Of(u), a, b, e.P, Mathf.RoundToInt(e.Get(1, 1f)), e.Get(2, 1f), SkillMods.First(u, SkillKeys.HitBurn), new Color(1f, 0.4f, 0.05f, 0.35f)); + foreach (var t in battle.AliveUnits(u.Faction == Faction.Player ? Faction.Enemy : Faction.Player)) + { + if (Combat.SegmentPointDistance(a, b, t.Position) > e.P + t.Radius) continue; + if (z != null) z.Touched.Add(t); + ShotDamage(u, t, Combat.SkillDamage(cfg, u, t, e.Get(2, 1f), rng), false, o); + } + } + + /// 일반 사격 덧붙임: 하후연 삼연시(멀티샷 · 좌우 화살) · 감녕 도탄(맞은 유닛과 가까운 n 기 · 아군 포함) · 장로 독지대. + IEnumerator ExtraArrows(Unit u, ShotPlan p, ActionOutcome o) + { + var ms = SkillMods.First(u, SkillKeys.Multishot, true); + if (ms != null && u.Type == UnitType.Archer) + { + int n = Mathf.Max(1, Mathf.RoundToInt(ms.P)); + float spread = ms.Get(1, 30f); + Vector2 d = (p.To - p.From).sqrMagnitude > 1e-6f ? (p.To - p.From).normalized : Vector2.up; + for (int i = 0; i < n; i++) + { + float ang = n > 1 ? -spread * 0.5f + spread * i / (n - 1) : 0f; + if (Mathf.Abs(ang) < 0.01f) continue; // 가운데 = 본 화살 + FireOne(u, Rotate(d, ang), o, 1f); + } + } + var rc = SkillMods.First(u, SkillKeys.ShotRicochet); + if (rc != null && p.Hit != null) + { + var near = new List(); + foreach (var t in battle.Units) + if (t != null && t.IsAlive && t != u && t != p.Hit) near.Add(t); + Vector2 c = p.Hit.Position; + near.Sort((a, b) => Vector2.Distance(c, a.Position).CompareTo(Vector2.Distance(c, b.Position))); + int cnt = Mathf.Max(0, Mathf.RoundToInt(rc.P)); + for (int i = 0; i < near.Count && i < cnt; i++) + { + var t = near[i]; + Fx.Run(Fx.Arrow(c, t.Position)); + ShotDamage(u, t, Combat.SkillDamage(cfg, u, t, rc.Get(1, 0.5f), rng), false, o); + } + if (cnt > 0) o.Notes.Add("도탄"); + } + var pz = SkillMods.First(u, SkillKeys.PoisonZone); + var sys = SkillSystem.Current; + if (pz != null && sys != null && sys.Of(u) != null && (p.Hit != null || p.HitGimmick != null)) + sys.AddZone(sys.Of(u), p.To, p.To, pz.P, Mathf.RoundToInt(pz.Get(1, 1f)), 0f, SkillMods.First(u, SkillKeys.HitPoison), new Color(0.6f, 0.3f, 0.9f, 0.3f)); + yield return null; + } + + /// + /// 자신의 사격이 아군에 맞음: 감녕 백기습영 = 그 아군 곁으로 이동(피해 없음) + 남은 행동 범위로 재행동 · 순욱·소교·간옹·조범 = 공격력 × p 회복 · + /// 미축·순욱 = 이동·공격 버프(지속 턴). 처리했으면 true(피해 없음) · 효과가 없으면 false(오사). + /// + bool AllyHitSpecial(Unit u, Unit ally, Vector2 from, ActionOutcome o, bool splash = false) + { + var sys = SkillSystem.Current; + if (sys == null || ally == null || ally == u) return false; + bool any = false; + float heal = SkillMods.Sum(u, SkillKeys.AllyHitHeal, true); + if (heal > 0f) + { + sys.Heal(ally, Mathf.RoundToInt(SkillSystem.AtkOf(u) * heal), u); + any = true; + } + foreach (var h in SkillMods.Get(u, SkillKeys.AllyHitBuff)) + { + if (h.Owner == null) continue; + int rounds = Mathf.Max(1, h.E.Dur); + if (h.E.P > 0f) sys.AddBuff(h.Owner, ally, SkillKeys.SlingDistPct, h.E.P, rounds); + if (h.E.Get(1) > 0f) sys.AddBuff(h.Owner, ally, SkillKeys.AtkPct, h.E.Get(1), rounds); + if (h.Passive) sys.NotePassive(h.Owner, h.Def); + any = true; + } + if (!splash && SkillMods.Has(u, SkillKeys.AllyHitWarp, true)) + { + var s = sys.Of(u); + Vector2 d = ally.Position - u.Position; + d = d.sqrMagnitude > 1e-6f ? d.normalized : Vector2.up; + Vector2 spot = battle.Board.ClampInside(ally.Position - d * (u.Radius + ally.Radius + 0.05f), u.Radius); + float used = Vector2.Distance(o.Path.Count > 0 ? o.Path[0] : u.Position, spot); + Vector2 start = u.Position; + if (!Sim.Active) + { + u.transform.position = new Vector3(start.x, start.y, 0f); + Fx.Run(Fx.MoveAlong(u.transform, start, spot, cfg.moveSpeed * 2f, null)); + } + u.Position = spot; + o.Path.Add(spot); + float left = SkillMods.SlingMax(u) - used; + if (s != null && left >= cfg.minDragDistance) + { + s.ReactCredits++; + s.ReactLimit = left; + } + o.Notes.Add(ally.DisplayName + " 곁으로"); + any = true; + } + return any; + } + + /// 이각 약탈: 이번 광역으로 준 피해 × p1 → 자신과 인접(p2)한 가장 HP 비율이 낮은 다른 아군 회복. + void SplashHealLowest(Unit u, int total) + { + var e = SkillMods.First(u, SkillKeys.SplashHealLowest, true); + var sys = SkillSystem.Current; + if (e == null || total <= 0 || sys == null) return; + Unit pick = null; + foreach (var a in battle.AliveUnits(u.Faction)) + { + if (a == u || !SkillMods.Within(u, a, e.Get(1, 2f))) continue; + if (pick == null || (float)a.Hp / a.MaxHp < (float)pick.Hp / pick.MaxHp) pick = a; + } + if (pick != null) sys.Heal(pick, Mathf.RoundToInt(total * e.P), u); + } + } +} diff --git a/Assets/Scripts/Runtime/ActionResolver.Skills.cs.meta b/Assets/Scripts/Runtime/ActionResolver.Skills.cs.meta new file mode 100644 index 00000000..1dafd5d2 --- /dev/null +++ b/Assets/Scripts/Runtime/ActionResolver.Skills.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1f16df9a5a16efb40bd5ff913ee2fa74 \ No newline at end of file diff --git a/Assets/Scripts/Runtime/ActionResolver.cs b/Assets/Scripts/Runtime/ActionResolver.cs index 54186d18..077f6ef7 100644 --- a/Assets/Scripts/Runtime/ActionResolver.cs +++ b/Assets/Scripts/Runtime/ActionResolver.cs @@ -78,7 +78,9 @@ namespace TKS public IEnumerator Resolve(Unit u, Vector2 target, bool moveOnly, ActionOutcome o) { Vector2 origin = u.Position; - target = ReachPoint(u, target); // 늪·고지 이동 비용 = 실제 도달점(근접 병종 · PD #1097) + float slingDist = Vector2.Distance(origin, target); + Vector2 aimed = target; + target = ReachPoint(u, target); // 늪·고지 이동 비용 = 실제 도달점(근접 병종 · PD #1097) · 조인 철벽 = 반경 안 이동 비용 + Vector2 delta = target - origin; float dist = delta.magnitude; o.Path.Add(origin); @@ -87,9 +89,54 @@ namespace TKS GimmickBegin(o); var skills = SkillSystem.Current; - if (skills != null) skills.BeginAction(u, moveOnly, o, dir); // 예약 액티브 장착 + 발동 연출(PD #1093 B) · 이펙트 방향 = 슬링샷 방향 + if (skills != null) + { + skills.BeginAction(u, moveOnly, o, dir, slingDist); // 예약 액티브 장착 + 발동 연출 · 돌진 거리 비율(만인지적·강동의 호걸·적토마) + if (skills.Ctx != null && (aimed - target).sqrMagnitude > 1e-4f) skills.Ctx.HitTerrain = true; // 이동 감소 지형을 지남(맹획 남만왕) + } // 슬링샷 돌진 중 = 병종 돌진 포즈 1프레임 고정 · 끝나면 대기 (PD 2026-11-05 #1084) if (u.View != null) u.View.SetCharging(true); + // 정보 연환격(repeat_action) = 같은 거리·방향으로 한 번 더(멈춘 자리에서) + int runs = !moveOnly && SkillMods.Has(u, SkillKeys.RepeatAction) ? 2 : 1; + for (int k = 0; k < runs && u.IsAlive; k++) + { + if (k > 0) + { + yield return Fx.Wait(0.15f); + origin = u.Position; + target = battle.Board.ClampInside(origin + delta, u.Radius); + Vector2 d2 = target - origin; + dist = d2.magnitude; + if (dist < 0.05f) break; + dir = d2 / dist; + DashBegin(u, dir); + o.Notes.Add("한 번 더"); + } + yield return RoutineFor(u, origin, dir, dist, target, moveOnly, o); + } + yield return GimmickAfter(u, moveOnly, o, !(UnitTypeInfo.IsRanged(u.Type) && o.Fired)); // 빙판 · 지대 스윕 · 강(PD #1097) + if (u.View != null) u.View.SetCharging(false); + if (skills != null) yield return skills.EndAction(u, o); // 출혈·중독 · 지대 · 재행동 · 예약 소모 · 쿨 + } + + /// 병종 루틴 선택 — 장착 스킬이 바꾸는 것: 동탁 폭정(난동) · 손견·안량 관통(보·창도 기병처럼) · 축융 비도(창병이 궁병처럼 사격). + IEnumerator RoutineFor(Unit u, Vector2 origin, Vector2 dir, float dist, Vector2 target, bool moveOnly, ActionOutcome o) + { + if (!moveOnly && SkillMods.Has(u, SkillKeys.Rampage)) + { + yield return RampageRoutine(u, origin, dir, o); + yield break; + } + if (!moveOnly && SkillMods.Has(u, SkillKeys.ShotAsArcher)) + { + yield return ArcherRoutine(u, origin, dir, dist, target, moveOnly, o); + yield break; + } + if (!moveOnly && !UnitTypeInfo.IsRanged(u.Type) && u.Type != UnitType.Cavalry && SkillMods.Has(u, SkillKeys.PierceThrough)) + { + yield return CavalryRoutine(u, origin, dir, dist, target, moveOnly, o); + yield break; + } switch (u.Type) { case UnitType.Infantry: @@ -99,16 +146,13 @@ 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, true); + yield return CavalryRoutine(u, origin, dir, dist, target, moveOnly, o); break; case UnitType.Archer: case UnitType.Strategist: yield return ArcherRoutine(u, origin, dir, dist, target, moveOnly, o); break; } - yield return GimmickAfter(u, moveOnly, o, !(UnitTypeInfo.IsRanged(u.Type) && o.Fired)); // 빙판 · 지대 스윕 · 강(PD #1097) - if (u.View != null) u.View.SetCharging(false); - if (skills != null) skills.EndAction(u, o); // 예약 소모 · 쿨 · 견제 강화 이월 } // ---------- 미리보기(드라이런) ---------- @@ -135,14 +179,15 @@ namespace TKS case UnitType.Infantry: case UnitType.Spear: { - var first = FirstUnitBeforeWall(u, origin, dir, dist, out hitDist); + if (SkillMods.Has(u, SkillKeys.PierceThrough) || SkillMods.Has(u, SkillKeys.Rampage)) goto case UnitType.Cavalry; + var first = SkillMods.Has(u, SkillKeys.PassAllies) ? FirstEnemyBeforeWall(u, origin, dir, dist, out hitDist) : 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); + if (!PierceAllies(u)) travel = StopBeforeUnclearedAllies(u, origin, dir, travel); var dists = new List(); result.AddRange(Combat.EnemiesOnPath(u, origin, dir, travel, dists)); break; @@ -206,6 +251,37 @@ namespace TKS return first; } + /// 조조 난세의 간웅(pass_allies): 아군은 통과 · 경로 위 첫 적(벽이 먼저면 null). + Unit FirstEnemyBeforeWall(Unit u, Vector2 origin, Vector2 dir, float dist, out float hitDist) + { + Unit first = Combat.FirstEnemyOnPath(u, origin, dir, dist, out hitDist); + if (first == null) return null; + float wall = Combat.FreeTravel(u, origin, dir, dist, false, false); + if (hitDist > wall + Combat.Epsilon) + { + hitDist = 0f; + return null; + } + return first; + } + + /// 관통이 아군도 지나는가(손견 강동의 호랑이 · 손책 소패왕 = pierce_through p1 1). + static bool PierceAllies(Unit u) + { + foreach (var h in SkillMods.Get(u, SkillKeys.PierceThrough)) + if (h.E.P >= 0.5f) return true; + return false; + } + + /// 아군 통과 이동(유닛 무시 · 벽·장애물만) → 끝이 유닛과 겹치면 겹치지 않는 곳까지 되짚음. + IEnumerator MoveThrough(Unit u, Vector2 origin, Vector2 dir, float dist, ActionOutcome o) + { + float free = Combat.FreeTravel(u, origin, dir, dist, true, false); + Vector2 end = BackOffFromUnits(u, origin, dir, free, true); + yield return Move(u, end, o); + o.MovedOnly = true; + } + // ---------- 보병: 알까기 충돌 ---------- IEnumerator InfantryRoutine(Unit u, Vector2 origin, Vector2 dir, float dist, Vector2 target, bool moveOnly, ActionOutcome o) @@ -217,7 +293,8 @@ namespace TKS } float hitDist; - Unit first = FirstUnitBeforeWall(u, origin, dir, dist, out hitDist); + bool passAllies = SkillMods.Has(u, SkillKeys.PassAllies); // 조조 난세의 간웅 = 아군 통과 + Unit first = passAllies ? FirstEnemyBeforeWall(u, origin, dir, dist, out hitDist) : FirstUnitBeforeWall(u, origin, dir, dist, out hitDist); float gDist; Vector2 gNormal; var gim = BlockAhead(u, origin, dir, dist, first != null ? hitDist : dist, out gDist, out gNormal); @@ -228,7 +305,8 @@ namespace TKS } if (first == null) { - yield return MoveFree(u, origin, dir, dist, o); + if (passAllies) yield return MoveThrough(u, origin, dir, dist, o); + else yield return MoveFree(u, origin, dir, dist, o); yield break; } @@ -250,20 +328,68 @@ namespace TKS o.MovedOnly = true; } - // 알까기: 부딪힌 순간 양쪽이 동시에 튕긴다 — 상대 = 접촉 방향 × 남은 힘 × collisionPushRatio · 자신 = 반대 방향 × 남은 힘 × collisionSelfBounceRatio Vector2 n = first.Position - contact; n = n.sqrMagnitude > 1e-6f ? n.normalized : dir; - yield return Bounce(u, first, n, remaining, o, enemy ? impact : 0, enemy); // 적과 싸운 뒤 튕김 = 연쇄 1회(PD #1098) + if (SkillMods.Has(u, SkillKeys.SwapOnHit) && first.IsAlive) + { + yield return SwapWith(u, first, origin, o); // 위연 자오곡 기책: 명중 대상 ↔ 출발 위치(튕김 없음) + } + else if (enemy && first.IsAlive && SkillMods.Has(u, SkillKeys.AimPush)) + { + // 조조 난세의 간웅: 자신은 밀리지 않고 탭한 방향으로 명중 적을 밀침(적·자동 = 슬링 방향) + Vector2 aimDir = n; + var sk = SkillSystem.Current; + if (sk != null) + yield return sk.WaitTap(u, SkillSystem.TapKind.Aim, first.Position, 0f, o.SkillUsed != null ? o.SkillUsed.NameKo + " — 밀칠 방향을 탭" : "밀칠 방향을 탭", first.Position + n, + w => { var d = w - first.Position; if (d.sqrMagnitude > 1e-4f) aimDir = d.normalized; }); + float force = Mathf.Max(remaining, cfg.minDragDistance) * cfg.collisionPushRatio * SkillMods.PushMult(u, true) * SkillMods.KnockbackTakenMult(first, true); + if (first.IsAlive) yield return Push(first, aimDir, force, u, o); + } + else + { + // 알까기: 부딪힌 순간 양쪽이 동시에 튕긴다 — 상대 = 접촉 방향 × 남은 힘 × collisionPushRatio · 자신 = 반대 방향 × 남은 힘 × collisionSelfBounceRatio + yield return Bounce(u, first, n, remaining, o, enemy ? impact : 0, enemy); // 적과 싸운 뒤 튕김 = 연쇄 1회(PD #1098) + } if (!enemy && first.IsAlive) yield return PushedAllyAttack(first, o, u); + if (!enemy) PushContinue(u, remaining); // 고순: 아군을 밀친 뒤 남은 이동력으로 재행동 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); // 호치·강동의 호랑이: 추가 이동에서 공격 + o.BonusMoves = 1; + o.BonusAttack = false; } } + /// 고순 함진 돌파(push_continue): 아군·지형 오브젝트를 밀친 뒤 남은 이동 거리만큼 재행동 1회(슬링 상한 = 남은 거리). + void PushContinue(Unit u, float remaining) + { + var sk = SkillSystem.Current; + var s = sk != null ? sk.Of(u) : null; + if (s == null || !u.IsAlive || remaining < cfg.minDragDistance || !SkillMods.Has(u, SkillKeys.PushContinue, true)) return; + s.ReactCredits++; + s.ReactLimit = remaining; + } + + /// 위연: 명중 대상은 행동 유닛의 출발 위치로 · 행동 유닛은 대상 자리로(동시에). + IEnumerator SwapWith(Unit u, Unit t, Vector2 origin, ActionOutcome o) + { + Vector2 tFrom = t.Position; + Vector2 tTo = battle.Board.ClampInside(origin, t.Radius); + Vector2 uTo = battle.Board.ClampInside(tFrom, u.Radius); + bool done = false; + Fx.Run(Track(Fx.MoveAlong(t.transform, tFrom, tTo, cfg.moveSpeed * 1.5f, null), () => done = true)); + Face(u, uTo - u.Position); + yield return Fx.MoveAlong(u.transform, u.Position, uTo, cfg.moveSpeed * 1.5f, null); + while (!done) yield return null; + t.Position = tTo; + u.Position = uTo; + o.Path.Add(uTo); + o.Displaced.Add(new Displacement { Unit = t, From = tFrom, To = tTo }); + o.Notes.Add(t.DisplayName + " 자리 교환"); + battle.NotifyChanged(); + } + // ---------- 창병: 밀치기 + 반작용(PD 2026-11-06 #1098 · 밀며 함께 전진 폐기) ---------- IEnumerator SpearRoutine(Unit u, Vector2 origin, Vector2 dir, float dist, Vector2 target, bool moveOnly, ActionOutcome o) @@ -332,156 +458,299 @@ namespace TKS battle.NotifyChanged(); if (!enemy) yield return PushedAllyAttack(first, o, u); } - if (enemy && u.IsAlive) SpearHitMore(u, first, o); - // 목표점 명중 → 대기(견제) = 제거(PD 2026-11-10) · 견제 스킬(청룡언월·후계·교변 …)은 재설계 결정 대기라 BattleManager.ApplyOverwatch 등은 남겨 둠(대기 유닛이 없어 발동 안 함) - } - - /// 삼첨도(spear_hit_two): 밀며 전진 끝에 창병·밀린 적과 닿은 다른 적까지 합쳐 최대 n 기에 동시 피해(반격 없음). - 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(); - 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(); - 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(); + // 목표점 명중 → 대기(견제) = 제거(PD 2026-11-10) · 새 스킬 기획(2026-11-10 #1112)에도 견제 없음 } // ---------- 기병: 관통(제자리 복귀 없음 · PD 2026-11-10) ---------- - /// false = 위진소요의 추가 관통(다시 반복하지 않음 · exclude = 앞서 관통한 적 제외). - IEnumerator CavalryRoutine(Unit u, Vector2 origin, Vector2 dir, float dist, Vector2 target, bool moveOnly, ActionOutcome o, bool first, List exclude = null) + /// + /// 관통 돌격(기병 · 스킬로 관통하는 보병·창병): 유닛 무시(아군은 지나칠 수 없으면 직전 정지 · 손견·손책 = 아군도 통과) · 경로 위 적 피해·옆 밀기 · + /// 손책 소패왕 = 전장 벽에서 핀볼처럼 튕겨 남은 거리 소진 · 반격 · 장애물 → 장료 요래(끌어오기) → 복귀(조운·손책·여포·문추·공손찬·순우경 · 하후돈 = 탭 지점·쓸어 감). + /// + /// 빙판 미끄러짐 재실행(앞서 관통한 적 제외 · 끌어오기·복귀 없음) · null = 일반 행동. + IEnumerator CavalryRoutine(Unit u, Vector2 origin, Vector2 dir, float dist, Vector2 target, bool moveOnly, ActionOutcome o, List slideExclude = null) { - float travel = Combat.FreeTravel(u, origin, dir, dist, false, false); // 유닛은 무시 · 벽·장애물 기믹만 - travel = StopBeforeUnclearedAllies(u, origin, dir, travel); // 지나칠 수 없는 아군 직전에서 멈춤 - float gDist; - Vector2 gNormal; - var gim = BlockAhead(u, origin, dir, dist, travel + 0.05f, out gDist, out gNormal); // 장애물 = 관통 불가 → 거기서 정지(PD #1097) - var dists = new List(); - var enemies = moveOnly ? new List() : 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 hitFinal = first && finalHit != null && enemies.Contains(finalHit); - // 위진소요: 목표점 명중 시 같은 방향 한 번 더 관통 · 제자리 복귀(옛 목표점 명중 · 단기구주 빗나가도 복귀) = 제거(PD 2026-11-10) - int repeat = hitFinal && gim == null ? SkillMods.Count(u, SkillKeys.PierceRepeat, 0) : 0; - - Vector2 end = origin + dir * travel; - if (repeat <= 0) end = BackOffFromEnemies(u, origin, dir, travel); - - if (enemies.Count == 0) - { - yield return Move(u, end, o); - o.MovedOnly = true; - if (gim != null && u.IsAlive) yield return CavalryHitsGimmick(u, gim, Mathf.Max(0f, dist - travel), gNormal, o); - yield break; - } - - float dmgMult = SkillMods.PierceDmgMult(u, enemies.Count, true); // 만인지적(2기 이상) · 발시담정 · 천하무쌍 - Face(u, dir); - if (u.View != null) u.View.PlayAttack(); - int idx = 0; + bool slide = slideExclude != null; + bool passAll = !moveOnly && PierceAllies(u); + bool pinball = !moveOnly && !slide && SkillMods.Has(u, SkillKeys.WallBounce, true); var hit = new List(); - yield return Fx.MoveAlong(u.transform, origin, end, cfg.moveSpeed, travelled => + Vector2 start = origin; + Vector2 d = dir; + float left = dist; + GimmickInstance gim = null; + Vector2 gNormal = -dir; + float gimLeft = 0f; + Face(u, dir); + for (int seg = 0; seg < 6 && left > 0.05f && u.IsAlive; seg++) { - while (idx < enemies.Count && travelled >= dists[idx] - 0.01f) + float travel = Combat.FreeTravel(u, start, d, left, false, false); // 유닛은 무시 · 벽·장애물 기믹만 + if (!passAll) travel = StopBeforeUnclearedAllies(u, start, d, travel); // 지나칠 수 없는 아군 직전에서 멈춤 + float gDist; + gim = BlockAhead(u, start, d, left, travel + 0.05f, out gDist, out gNormal); // 장애물 = 관통 불가 → 거기서 정지(PD #1097) + var dists = new List(); + var enemies = moveOnly ? new List() : Combat.EnemiesOnPath(u, start, d, travel, dists); + if (slide) + for (int i = enemies.Count - 1; i >= 0; i--) + if (slideExclude.Contains(enemies[i])) + { + enemies.RemoveAt(i); + dists.RemoveAt(i); + } + Vector2 wallN; + bool bounce = false; + if (pinball && gim == null && travel < left - 0.1f) { - PierceHit(u, enemies[idx], dir, o, dmgMult); - hit.Add(enemies[idx]); - idx++; + Combat.BoardWallTravel(u, start, d, left, out wallN); + if (wallN.sqrMagnitude > 0.5f) + { + bounce = true; + gNormal = wallN; + } } - }); - u.Position = end; - o.Path.Add(end); - DashEnd(u); // 관통 돌격 끝 = 돌진 이펙트 소멸(PD 2026-11-10) - for (; idx < enemies.Count; idx++) - { - if (dists[idx] <= travel + 0.01f) + Vector2 end = bounce ? start + d * travel : BackOffFromUnits(u, start, d, travel, passAll); + if (enemies.Count > 0 && u.View != null) u.View.PlayAttack(); + int idx = 0; + var segDir = d; + yield return Fx.MoveAlong(u.transform, start, end, cfg.moveSpeed, travelled => { - PierceHit(u, enemies[idx], dir, o, dmgMult); + while (idx < enemies.Count && travelled >= dists[idx] - 0.01f) + { + PierceHit(u, enemies[idx], segDir, o, 1f); + hit.Add(enemies[idx]); + idx++; + } + }); + u.Position = end; + o.Path.Add(end); + for (; idx < enemies.Count; idx++) + { + if (dists[idx] > travel + 0.01f) continue; + PierceHit(u, enemies[idx], segDir, o, 1f); hit.Add(enemies[idx]); } + gimLeft = Mathf.Max(0f, left - travel); + left -= travel; + if (!bounce) break; + d = Vector2.Reflect(d, gNormal).normalized; // 소패왕: 전장 가장자리에서 튕겨 남은 거리를 마저 달림 + start = end; + o.Notes.Add("튕김"); + DashBegin(u, d); + Face(u, d); } - yield return Fx.Wait(0.15f); + DashEnd(u); // 관통 돌격 끝 = 돌진 이펙트 소멸(PD 2026-11-10) + if (hit.Count == 0) o.MovedOnly = true; + else yield return Fx.Wait(0.15f); - // 살아남은 근접 적의 반격(단기구주·발시담정·천하무쌍 = 반격 없음) - if (!SkillMods.Has(u, SkillKeys.PierceNoCounter)) + // 살아남은 근접 적의 반격(받는 반격 % · −100 % = 없음 → Combat.CounterDamage 가 0) + var countered = new HashSet(); + foreach (var e in hit) { - 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 (!u.IsAlive) break; + if (!e.IsAlive || !UnitTypeInfo.IsMelee(e.Type) || !countered.Add(e)) continue; + int c = Combat.CounterDamage(cfg, e, u, rng, true); + if (c <= 0) continue; + c = SkillSystem.Deal(u, c, e, HitKind.Counter); + 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 (gim != null && u.IsAlive) { - yield return CavalryHitsGimmick(u, gim, Mathf.Max(0f, dist - travel), gNormal, o); + var sk0 = SkillSystem.Current; + if (sk0 != null && sk0.Ctx != null && sk0.Ctx.Actor == u) sk0.Ctx.HitTerrain = true; + yield return CavalryHitsGimmick(u, gim, gimLeft, gNormal, o); yield break; } + if (!u.IsAlive || moveOnly || slide) yield break; - if (repeat > 0 && u.IsAlive) + if (hit.Count > 0 && SkillMods.Has(u, SkillKeys.DragBack, true)) yield return DragBack(u, origin, dir, hit, o); // 장료 요래 + + // 복귀: 항상(return_always) · 피해를 입혔을 때(return_on_hit) · 하후돈 맹장 = 거리와 무관하게 탭한 지점으로 + var ra = SkillMods.First(u, SkillKeys.ReturnAlways, true); + var rh = hit.Count > 0 ? SkillMods.First(u, SkillKeys.ReturnOnHit, true) : null; + bool aim = SkillMods.Has(u, SkillKeys.ReturnAim, true); + if (ra == null && rh == null && !aim) yield break; + bool dmgBack = (ra != null && ra.P >= 0.5f) || (rh != null && rh.P >= 0.5f); + bool sweep = (ra != null && ra.Get(1) >= 0.5f) || (rh != null && rh.Get(1) >= 0.5f); + Vector2 dest = origin; + if (aim) { - // 위진소요: 목표점에서 같은 방향으로 같은 거리만큼 한 번 더 관통(앞서 관통한 적 제외 · 그 뒤 복귀 없음) - for (int i = 0; i < repeat && u.IsAlive; i++) - { - yield return Fx.Wait(0.1f); - Vector2 from = u.Position; - DashBegin(u, dir); // 추가 관통 = 돌진 이펙트 다시 - yield return CavalryRoutine(u, from, dir, dist, from + dir * dist, false, o, false, hit); - } + var sk = SkillSystem.Current; + if (sk != null) + yield return sk.WaitTap(u, SkillSystem.TapKind.Return, u.Position, SkillMods.SlingMax(u), (o.SkillUsed != null ? o.SkillUsed.NameKo : "복귀") + " — 복귀할 곳을 탭", origin, w => dest = w); } + yield return ReturnRoutine(u, dest, dmgBack, sweep, o); + } + + /// 복귀 이동: 유닛 무시 · 복귀 중 피해(조운·손책 소패왕) · 쓸어 감(하후돈 = 경로 위 유닛을 끝까지 함께 밀고 감 · 아군 포함). + IEnumerator ReturnRoutine(Unit u, Vector2 dest, bool dmgBack, bool sweep, ActionOutcome o) + { + yield return Fx.Wait(0.1f); + dest = battle.Board.ClampInside(dest, u.Radius); + Vector2 from = u.Position; + Vector2 rd = dest - from; + float len = rd.magnitude; + if (len < 0.05f) yield break; + Vector2 rdir = rd / len; + float travel = Combat.FreeTravel(u, from, rdir, len, false, false); // 벽·장애물에서 멈춤 + var dists = new List(); + var onPath = Combat.UnitsOnPath(u, from, rdir, travel, dists); + Vector2 end = sweep ? from + rdir * travel : BackOffFromUnits(u, from, rdir, travel, true); + DashBegin(u, rdir); + Face(u, rdir); + int idx = 0; + yield return Fx.MoveAlong(u.transform, from, end, cfg.moveSpeed, t => + { + while (idx < onPath.Count && t >= dists[idx] - 0.01f) + { + var e = onPath[idx++]; + if (dmgBack && e.Faction != u.Faction) PierceHit(u, e, rdir, o, 1f); + } + }); + u.Position = end; + o.Path.Add(end); + o.Returned = true; + DashEnd(u); + if (sweep) + { + // 쓸어 감: 경로 위 유닛을 도착점 앞(복귀 방향)으로 줄지어 민다 + float off = u.Radius; + foreach (var e in onPath) + { + if (e == null || !e.IsAlive) continue; + Vector2 eFrom = e.Position; + off += e.Radius + 0.05f; + Vector2 eTo = battle.Board.ClampInside(end + rdir * off, e.Radius); + off += e.Radius; + Fx.Run(Fx.MoveAlong(e.transform, eFrom, eTo, cfg.moveSpeed, null)); + e.Position = eTo; + o.Displaced.Add(new Displacement { Unit = e, From = eFrom, To = eTo }); + if (!o.Pushed.Contains(e)) o.Pushed.Add(e); + } + if (onPath.Count > 0) o.Notes.Add("복귀하며 쓸어 감"); + } + battle.NotifyChanged(); + } + + /// 장료 요래: 관통으로 피해를 입힌 적을 출발 위치로 끌고 옴(가까운 적부터 출발점 · 뒤는 진행 방향으로 줄지음). + IEnumerator DragBack(Unit u, Vector2 origin, Vector2 dir, List hit, ActionOutcome o) + { + var list = new List(); + foreach (var e in hit) + if (e != null && e.IsAlive && !list.Contains(e)) list.Add(e); + list.Sort((a, b) => Vector2.Distance(origin, a.Position).CompareTo(Vector2.Distance(origin, b.Position))); + float off = 0f; + bool any = false; + foreach (var e in list) + { + Vector2 from = e.Position; + Vector2 to = battle.Board.ClampInside(origin + dir * off, e.Radius); + off += e.Radius * 2f + 0.1f; + Fx.Run(Fx.MoveAlong(e.transform, from, to, cfg.moveSpeed * 1.2f, null)); + e.Position = to; + o.Displaced.Add(new Displacement { Unit = e, From = from, To = to }); + any = true; + } + if (any) + { + o.Notes.Add("끌고 옴"); + yield return Fx.Wait(0.3f); + } + battle.NotifyChanged(); + } + + /// 동탁 폭정: p1 초 동안 이동 속도로 직진 · 전장 가장자리에서 핀볼처럼 튕김 · 닿은 유닛(아군 포함)을 옆으로 p2 강하게 밀침 · 적 = 돌진 피해(반격 받음) · 자신은 튕기지 않음. + IEnumerator RampageRoutine(Unit u, Vector2 origin, Vector2 dir, ActionOutcome o) + { + var e0 = SkillMods.First(u, SkillKeys.Rampage, true); + float total = cfg.moveSpeed * (e0 != null ? e0.P : 1f); + float force = e0 != null ? e0.Get(1, 1f) : 1f; + var touched = new HashSet(); + Vector2 start = origin; + Vector2 d = dir; + float left = total; + Face(u, d); + if (u.View != null) u.View.PlayAttack(); + for (int seg = 0; seg < 8 && left > 0.05f && u.IsAlive; seg++) + { + Vector2 n; + float travel = Combat.BoardWallTravel(u, start, d, left, out n); + float gDist; + Vector2 gN; + var g = BlockAhead(u, start, d, left, travel + 0.05f, out gDist, out gN); + if (g != null) + { + travel = Mathf.Max(0f, gDist - Combat.Epsilon); + n = gN; + } + var dists = new List(); + var units = Combat.UnitsOnPath(u, start, d, travel, dists); + Vector2 end = start + d * travel; + int idx = 0; + var segDir = d; + yield return Fx.MoveAlong(u.transform, start, end, cfg.moveSpeed, t => + { + while (idx < units.Count && t >= dists[idx] - 0.01f) + { + var x = units[idx++]; + if (!x.IsAlive || !touched.Add(x)) continue; + RampageHit(u, x, segDir, force, o); + } + }); + u.Position = end; + o.Path.Add(end); + left -= travel; + if (n.sqrMagnitude < 0.5f || left <= 0.05f) break; + if (g != null) + { + var sk = SkillSystem.Current; + if (sk != null && sk.Ctx != null) sk.Ctx.HitTerrain = true; + } + d = Vector2.Reflect(d, n).normalized; + start = end; + o.Notes.Add("튕김"); + DashBegin(u, d); + Face(u, d); + } + DashEnd(u); + u.Position = BackOffFromUnits(u, origin, d, 0f, true, u.Position); + if (touched.Count == 0) o.MovedOnly = true; + battle.NotifyChanged(); + } + + void RampageHit(Unit u, Unit x, Vector2 dir, float force, ActionOutcome o) + { + if (x.Faction != u.Faction) + { + ImpactExchange(u, x, o); + if (!u.IsAlive) return; + } + if (!x.IsAlive) return; + Vector2 side = new Vector2(-dir.y, dir.x); + float sign = Vector2.Dot(x.Position - u.Position, side) >= 0f ? 1f : -1f; + Vector2 from = x.Position; + Vector2 to = KnockTo(x, side * sign, force * SkillMods.KnockbackTakenMult(x, true)); + Fx.Run(Fx.MoveAlong(x.transform, from, to, cfg.moveSpeed, null)); + x.Position = to; + o.Displaced.Add(new Displacement { Unit = x, From = from, To = to }); + if (!o.Pushed.Contains(x)) o.Pushed.Add(x); } void PierceHit(Unit cav, Unit e, Vector2 dir, ActionOutcome o, float dmgMult) { if (!e.IsAlive) return; - int dmg = SkillSystem.Deal(e, Combat.Damage(cfg, cav, e, false, rng, true, dmgMult)); + int dmg = SkillSystem.Deal(e, Combat.Damage(cfg, cav, e, false, rng, true, dmgMult), cav, HitKind.Attack); 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) { - float kb = cfg.cavalryKnockback * SkillMods.PierceKnockbackMult(cav, true) * SkillMods.KnockbackTakenMult(e, true); + float kb = cfg.cavalryKnockback * SkillMods.KnockbackTakenMult(e, true); Vector2 from = e.Position; Vector2 to = KnockTo(e, side * sign, kb); // 늪 0 · 빙판 × · 장애물·함정에서 멈춤 · 다리 → 강(PD #1097) Fx.Run(Fx.MoveAlong(e.transform, e.Position, to, cfg.moveSpeed * 0.6f, null)); @@ -517,16 +786,18 @@ namespace TKS return t; } - /// 종착점이 적과 겹치면 경로를 되짚어 겹치지 않는 지점까지 물러선다. - Vector2 BackOffFromEnemies(Unit u, Vector2 origin, Vector2 dir, float travel) + /// 종착점이 적(allies = 아군도)과 겹치면 경로를 되짚어 겹치지 않는 지점까지 물러선다 · at = 그 점에서 dir 반대로 되짚음. + Vector2 BackOffFromUnits(Unit u, Vector2 origin, Vector2 dir, float travel, bool allies, Vector2? at = null) { - float t = travel; - for (int i = 0; i < 40 && t > 0f; i++) + Vector2 p0 = at ?? origin + dir * travel; + float maxBack = at.HasValue ? 4f : travel; + for (float b = 0f; b <= maxBack + 1e-3f; b += 0.1f) { - Vector2 p = origin + dir * t; + Vector2 p = p0 - dir * b; bool overlap = false; - foreach (var e in battle.AliveUnits(u.Faction == Faction.Player ? Faction.Enemy : Faction.Player)) + foreach (var e in battle.Units) { + if (e == null || e == u || !e.IsAlive || (!allies && e.Faction == u.Faction)) continue; if (Vector2.Distance(p, e.Position) < u.Radius + e.Radius - 0.01f) { overlap = true; @@ -534,9 +805,8 @@ namespace TKS } } if (!overlap) return p; - t -= 0.1f; } - return origin; + return at ?? origin; } // ---------- 궁병 · 책사: 투사체 사격 (PD 2026-11-05 #1087 ⑤⑥) ---------- @@ -576,13 +846,6 @@ namespace TKS ShotPlan PlanShot(Unit u, Vector2 origin, Vector2 dir, Vector2 target) { 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; @@ -609,16 +872,42 @@ namespace TKS Vector2 aim = p.Target.Position - p.From; Vector2 adir = aim.sqrMagnitude > 1e-6f ? aim.normalized : dir; float 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, out p.HitGimmick); + bool passObstacles; + Faction? pass = ArrowPass(u, out passObstacles); + Combat.ArrowIgnoreGimmicks = passObstacles; + try + { + p.Hit = Combat.FirstUnitOnArrow(u, p.From, adir, range + u.Radius, cfg.arrowRadius, pass, out travel, out p.HitGimmick); + } + finally + { + Combat.ArrowIgnoreGimmicks = false; + } p.To = p.From + adir * (p.Hit != null ? travel + cfg.arrowRadius : travel); - 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 (u.Type == UnitType.Strategist) AimSplash(u, p); + p.Crit = p.Hit == p.Target && Combat.EnemyAtPoint(u, target, cfg.finalHitTolerance) == p.Target; // 목표점 명중 크리(무조건 치명타 스킬 = 피해 계산에서) + if (u.Type == UnitType.Strategist && !SkillMods.Has(u, SkillKeys.ShotSingle)) AimSplash(u, p); // 장로 = 직선 투사체(광역 없음) + } + + /// 화살이 지나는 진영(황충 노익장·손권 강동의 주인 = 아군 통과 · p2 1 = 장애물도 통과). + static Faction? ArrowPass(Unit u, out bool obstacles) + { + obstacles = false; + bool any = false; + foreach (var h in SkillMods.Get(u, SkillKeys.ArrowPassAllies)) + { + any = true; + if (h.E.Get(1) >= 0.5f) obstacles = true; + } + return any ? u.Faction : (Faction?)null; } IEnumerator ArcherRoutine(Unit u, Vector2 origin, Vector2 dir, float dist, Vector2 target, bool moveOnly, ActionOutcome o) { + if (!moveOnly && HasSpecialShot(u)) + { + yield return SpecialShot(u, origin, dir, target, o); // 장착 스킬 특수 사격(ActionResolver.Skills.cs) + yield break; + } var p = moveOnly ? null : PlanShot(u, origin, dir, target); if (p != null && p.Teleport > 0.01f) { @@ -665,42 +954,70 @@ namespace TKS } Vector2 center = p.HasSplash ? p.SplashCenter : p.To; - if (p.Hit != null) ShotDamage(u, p.Hit, Combat.Damage(cfg, u, p.Hit, p.Crit, rng, false), p.Crit, o); + if (p.Hit != null) + { + if (p.Hit.Faction == u.Faction && AllyHitSpecial(u, p.Hit, p.From, o)) + { + // 아군 명중 = 회복·버프(순욱·소교·간옹·미축) · 감녕 = 아군 곁으로 이동 후 재공격 — 피해 없음 + } + else ShotDamage(u, p.Hit, Combat.Damage(cfg, u, p.Hit, p.Crit, rng, false), p.Crit, o); + } else yield return ArrowHitsGimmick(u, p, o); // 덤불·성문 막힘 · 수레 파괴(PD #1097) + yield return ExtraArrows(u, p, o); // 하후연 삼연시(멀티샷) · 감녕 도탄 if (strategist && p.HasSplash) { yield return SplashGimmicks(u, p, o); // 덤불 제거 · 수레 파괴 · 화공 불(PD #1097) 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) + float heal = SkillMods.Sum(u, SkillKeys.SplashHeal, true); + if (heal > 0f) { - if (!s.IsAlive) continue; - 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); + // 손건 화친: 광역 = 피해 대신 회복(적 포함) + int amt = Mathf.RoundToInt(SkillSystem.AtkOf(u) * heal); + var sys = SkillSystem.Current; + foreach (var s in p.Splash) + if (s.IsAlive && sys != null) sys.Heal(s, amt, u); + if (p.Hit != null && p.Hit.IsAlive && p.Hit.Faction == u.Faction && sys != null) sys.Heal(p.Hit, amt, u); + } + else + { + float splashRatio = SkillMods.SplashRatio(u, cfg, true); + float allyRatio = SkillMods.SplashAllyRatio(u, cfg, true); + int splashTotal = 0; + foreach (var s in p.Splash) + { + if (!s.IsAlive) continue; + if (s.Faction == u.Faction && AllyHitSpecial(u, s, p.From, o, true)) continue; // 보급·경국지색 = 광역에 든 아군도 회복 + float ratio = splashRatio * (s.Faction == u.Faction ? allyRatio : 1f); + int dmg = Combat.SplashDamage(cfg, u, s, ratio, rng); + if (dmg <= 0) continue; + int got = ShotDamage(u, s, dmg, false, o); + if (s.Faction != u.Faction) splashTotal += got; + } + SplashHealLowest(u, splashTotal); // 이각 약탈 } } battle.NotifyChanged(); yield return Fx.Wait(0.2f); } - /// 사격 피해 적용 + 집계(상대 = Attacked · 같은 편 = FriendlyHit 오사). - void ShotDamage(Unit a, Unit d, int dmg, bool crit, ActionOutcome o) + /// 사격 피해 적용 + 집계(상대 = Attacked · 같은 편 = FriendlyHit 오사). 반환 = 실제 피해. + int ShotDamage(Unit a, Unit d, int dmg, bool crit, ActionOutcome o, HitKind kind = HitKind.Attack) { - dmg = SkillSystem.Deal(d, dmg); + crit |= Combat.LastCrit; + dmg = SkillSystem.Deal(d, dmg, a, kind); if (d.Faction == a.Faction) { o.FriendlyHit.Add(d); o.FriendlyDamage += dmg; Fx.Popup(d.Position, dmg + " 오사", Fx.FriendlyFireColor); - return; + return dmg; } o.Attacked.Add(d); o.DamageDealt += dmg; o.Crit |= crit; if (crit && a.Type == UnitType.Archer) BattleEvents.Raise(BattleEvents.ArcherCrits, a); // 보물 조건 효과(PD #1093 C) Fx.Popup(d.Position, crit ? dmg + "!" : dmg.ToString(), crit ? new Color(1f, 0.7f, 0.2f) : Color.white); + return dmg; } // ---------- 공통 ---------- @@ -752,17 +1069,20 @@ namespace TKS int ImpactExchange(Unit a, Unit d, ActionOutcome o) { if (a.View != null) a.View.PlayAttack(); - int dmg = SkillSystem.Deal(d, Combat.Damage(cfg, a, d, false, rng, true)); + int raw = Combat.Damage(cfg, a, d, false, rng, true); + bool crit = Combat.LastCrit; + int dmg = SkillSystem.Deal(d, raw, a, HitKind.Attack); o.Attacked.Add(d); o.DamageDealt += dmg; - Fx.Popup(d.Position, dmg.ToString(), Color.white); + o.Crit |= crit; + Fx.Popup(d.Position, crit ? dmg + "!" : dmg.ToString(), crit ? new Color(1f, 0.7f, 0.2f) : Color.white); if (d.IsAlive && a.IsAlive) { - int c = Combat.CounterDamage(cfg, d, a, rng, true); // 부딪혀 밀리는 쪽의 반격(진중 조건) + int c = Combat.CounterDamage(cfg, d, a, rng, true); // 부딪혀 밀리는 쪽의 반격(받는 반격 % · 반격 피해 %) if (c > 0) { if (d.View != null) d.View.PlayAttack(); - c = SkillSystem.Deal(a, c); + c = SkillSystem.Deal(a, c, d, HitKind.Counter); o.CounterTaken += c; Fx.Popup(a.Position, c.ToString(), new Color(1f, 0.45f, 0.45f)); } @@ -783,21 +1103,10 @@ namespace TKS if (struck != null && struck.IsAlive && force >= 0.01f) { struckDone = false; - float bonus = impactDmg > 0 ? SkillMods.PushCollisionBonus(striker) : 0f; - System.Action 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)); + Fx.Run(Track(Push(struck, n, force, striker, o), () => struckDone = true)); } - float self = Reach(striker, striker.Position, -n, remaining * cfg.collisionSelfBounceRatio * SkillMods.SelfBounceMult(striker, true)); + // 자기 튕김 × 자기 배율(허저 ×2 · 전위·맹획 0) × 서황 대부(도끼가 도는 서황을 친 유닛 = ×p3) + float self = Reach(striker, striker.Position, -n, remaining * cfg.collisionSelfBounceRatio * SkillMods.SelfBounceMult(striker, true) * (striker.Faction != (struck != null ? struck.Faction : striker.Faction) ? SkillMods.AxeBounce(struck) : 1f)); if (self >= 0.01f && striker.IsAlive && chain) { // 연쇄(PD #1098): 튕겨나는 길의 첫 유닛(벽·장애물보다 앞) — 적 = 피해·반격 교환 후 다시 튕김 · 아군 = 밀기만(밀린 아군은 규칙 ⑤) @@ -881,31 +1190,31 @@ namespace TKS { if (ally == null || !ally.IsAlive) yield break; 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); - } + Unit foe = Combat.TouchingOpponent(ally); + if (!ally.IsAlive || foe == null) yield break; + yield return Fx.Wait(0.1f); + Face(ally, foe.Position - ally.Position); + yield return PushedAttack(ally, foe, o, pusher); } - /// 밀린 아군의 근접 공격 1회(반격 받음 · 결과 요약에는 집계하지 않고 Notes 에 기록 · 도원의 맹약 = 피해 +%). 원거리 병종도 근접 패널티를 받는다. - IEnumerator PushedAttack(Unit a, Unit d, ActionOutcome o) + /// + /// 밀린 유닛의 근접 공격 1회(반격 받음 · Notes 에 기록) — 장합 교변: 민 장합의 공격력 × p1 을 더함 · 장합 자신이 밀려 친 공격 = × p1 · + /// 고순 함진영: 민 고순의 「명중 기절」이 이 공격에도(간접 명중). 원거리 병종도 근접 패널티를 받는다. + /// + IEnumerator PushedAttack(Unit a, Unit d, ActionOutcome o, Unit pusher = null) { if (a.View != null) a.View.PlayAttack(); yield return Fx.Lunge(a, d.Position); - float scale = 1f + SkillMods.Sum(a, SkillKeys.PushedAllyAttackPct, true); - int dmg = SkillSystem.Deal(d, Combat.Damage(cfg, a, d, false, rng, true, scale)); + float scale = SkillMods.Product(a, SkillKeys.SelfPushedMult, true); + if (pusher != null && pusher != a && pusher.Faction == a.Faction && a.Atk > 0) + { + float add = SkillMods.Sum(pusher, SkillKeys.PushedAllyAtkAdd, true); + if (add > 0f) scale *= 1f + add * SkillSystem.AtkOf(pusher) / Mathf.Max(1f, SkillSystem.AtkOf(a)); + } + int dmg = SkillSystem.Deal(d, Combat.Damage(cfg, a, d, false, rng, true, scale), a, HitKind.Attack); + var sys = SkillSystem.Current; + if (sys != null && pusher != null && pusher != a && pusher.Faction == a.Faction) sys.ApplyHitStatuses(pusher, d, false, HitKind.Indirect); Fx.Popup(d.Position, dmg.ToString(), Color.white); battle.NotifyChanged(); yield return Fx.Wait(0.2f); @@ -918,7 +1227,7 @@ namespace TKS { if (d.View != null) d.View.PlayAttack(); yield return Fx.Lunge(d, a.Position); - counter = SkillSystem.Deal(a, counter); + counter = SkillSystem.Deal(a, counter, d, HitKind.Counter); Fx.Popup(a.Position, counter.ToString(), new Color(1f, 0.45f, 0.45f)); battle.NotifyChanged(); yield return Fx.Wait(0.2f); diff --git a/Assets/Scripts/Runtime/BattleManager.cs b/Assets/Scripts/Runtime/BattleManager.cs index d8cc5b56..83debf4d 100644 --- a/Assets/Scripts/Runtime/BattleManager.cs +++ b/Assets/Scripts/Runtime/BattleManager.cs @@ -254,7 +254,9 @@ namespace TKS 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)); var hd = SkillCatalog.Hero(h.id); - u.Camp = hd != null && hd.faction != null ? hd.faction : ""; // 진영별 이펙트(skill_vfx.csv s_camp) + u.Camp = hd != null && hd.faction != null ? hd.faction : ""; // 진영별 이펙트(skill_vfx.csv s_camp) · 스킬 진영 대상 + u.Female = hd != null && hd.Female; // 스킬 조건 남성·여성(2026-11-10 #1112) + u.HeroId = h.id ?? ""; Skills.AttachLineup(u, h); // 액티브·패시브(레벨 · ★5) · 산채 체력은 여기서 최대 HP 반영 deployed.Add(h); } @@ -349,8 +351,13 @@ namespace TKS yield return Resolver.Resolve(u, target, moveOnly, o); yield return ApplyOverwatch(u, o); - // 추가 이동 = 첫 명중 n 회(인덕 extra_move_count) · 추가 이동 뒤에는 남은 횟수만 + // 추가 이동(보병 목표점 명중) → 끝나면 스킬 재행동(이도류·자오곡·백의도강·파로·군령 …) → 행동 끝 if (u.IsAlive && Skills.NextBonus(u, wasBonus, o)) u.GrantBonusMove(); + else if (u.IsAlive && Skills.TakeReaction(u)) + { + Skills.ClearBonus(u); + u.GrantReAction(); + } else { u.MarkActed(); @@ -439,9 +446,11 @@ namespace TKS foreach (var e in AliveUnits(Faction.Enemy)) e.ResetForNewTurn(); if (Gimmicks != null) { - yield return Gimmicks.TurnStart(Faction.Enemy); // 군기 · 중독 · 불 · 행동 불가(PD #1097) + yield return Gimmicks.TurnStart(Faction.Enemy); // 군기 · 중독 · 불 · 행동 불가(PD #1097) · 스킬 기절 if (CheckBattleEnd()) yield break; } + yield return Skills.TurnBegin(Faction.Enemy); // 스킬 라운드(아군 턴 끝 회복·정화 · 시한 효과 · 중독·화상 · 지대 · 2026-11-10 #1112) + if (CheckBattleEnd()) yield break; yield return Fx.Wait(0.4f); float accuracy = Stage.aiAccuracy; @@ -497,6 +506,7 @@ namespace TKS LastMessage = Describe(e, o, false); NotifyChanged(); acted++; + if (e.IsAlive && Skills.TakeReaction(e)) pending.Add(e); // 적 스킬 재행동 if (CheckBattleEnd()) yield break; yield return Fx.Wait(0.25f); } @@ -513,9 +523,11 @@ namespace TKS TurnNumber++; if (Gimmicks != null) { - yield return Gimmicks.TurnStart(Faction.Player); // 군기 · 불 지속 · 중독 · 불 · 행동 불가(PD #1097) + yield return Gimmicks.TurnStart(Faction.Player); // 군기 · 불 지속 · 중독 · 불 · 행동 불가(PD #1097) · 스킬 기절 if (CheckBattleEnd()) yield break; } + yield return Skills.TurnBegin(Faction.Player); // 스킬 라운드(적 턴 끝 처리 · 시한 효과 · 중독·화상 · 회복 · 지대 · 망령 소멸) + if (CheckBattleEnd()) yield break; LastMessage = TurnNumber + "번째 턴 — 아군 차례" + (acted == 0 ? " (적은 교전 범위 밖에서 대기)" : ""); if (AliveCount(Faction.Player) > 0 && AllPlayersActed()) { @@ -551,8 +563,8 @@ namespace TKS { if (spear.Type != UnitType.Spear || !spear.Overwatch) continue; if (!mover.IsAlive && o.Displaced.Count == 0) yield break; - float radius = SkillMods.OverwatchRadius(spear); - bool unlimited = SkillMods.OverwatchUnlimited(spear); + float radius = spear.AttackRange; // 옛 견제(대기 유닛 없음 · 스킬 확장 제거 2026-11-10 #1112) + bool unlimited = false; var victims = new List(); int passes = 0; for (int i = 0; path != null && i < path.Count - 1; i++) @@ -575,7 +587,7 @@ namespace TKS } } if (victims.Count == 0) continue; - int strikes = Mathf.Max(1, SkillMods.Count(spear, SkillKeys.OverwatchCount, 1, true)); + const int strikes = 1; foreach (var v in victims) { for (int k = 0; k < strikes; k++) @@ -584,9 +596,8 @@ namespace TKS 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); + dmg = SkillSystem.Deal(v, dmg, spear, HitKind.Attack); Fx.Popup(v.Position, dmg + " 견제", new Color(0.6f, 0.85f, 1f)); - if (!v.IsAlive) HealOnOverwatchKill(spear); NotifyChanged(); yield return Fx.Wait(0.2f); } @@ -594,23 +605,29 @@ namespace TKS } } - /// 하북 명장: 견제로 적을 쓰러뜨리면 최대 HP × p 회복. - void HealOnOverwatchKill(Unit spear) + // ---------- 승패 ---------- + + /// 스킬 소환(장로 망령)을 뺀 생존 수 — 승패 판정용. + int RealAlive(Faction f) { - 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); + int n = 0; + foreach (var u in units) + if (u != null && u.IsAlive && u.Faction == f && !SkillSystem.IsSummon(u)) n++; + return n; } - // ---------- 승패 ---------- + /// 유닛 제거(소환 소멸 · 콜라이더 즉시 제거). + public void RemoveUnit(Unit u) + { + if (u == null) return; + units.Remove(u); + u.gameObject.SetActive(false); + Destroy(u.gameObject); + } public bool CheckBattleEnd() { - if (AliveCount(Faction.Enemy) == 0) + if (RealAlive(Faction.Enemy) == 0) { LastMessage = "승리 — 전장의 적을 모두 처치했습니다"; foreach (var p in AliveUnits(Faction.Player)) @@ -620,7 +637,7 @@ namespace TKS SetPhase(BattlePhase.StageClear); return true; } - if (AliveCount(Faction.Player) == 0) + if (RealAlive(Faction.Player) == 0) { LastMessage = "패배 — 아군이 전멸했습니다"; SetPhase(BattlePhase.Defeat); diff --git a/Assets/Scripts/Runtime/Combat.cs b/Assets/Scripts/Runtime/Combat.cs index b2c6330c..02f971ce 100644 --- a/Assets/Scripts/Runtime/Combat.cs +++ b/Assets/Scripts/Runtime/Combat.cs @@ -22,9 +22,30 @@ namespace TKS /// public static int Damage(BalanceConfig cfg, Unit attacker, Unit defender, bool crit, System.Random rng, bool melee, float scale = 1f) { + return DamageCore(cfg, attacker, defender, crit, rng, melee, scale, 1f); + } + + /// 마지막 피해 계산이 치명타였나(명중 상태 이상 「치명타 시」 조건 · SkillSystem.Hit 가 읽음 · 2026-11-10 #1112). + public static bool LastCrit; + + /// 스킬 피해 「공격력의 ratio 배」(원거리 판정 · 반격 없음 · 치명타 판정 있음). + public static int SkillDamage(BalanceConfig cfg, Unit attacker, Unit defender, float ratio, System.Random rng, bool melee = false) + { + if (ratio <= 0f) return 0; + return DamageCore(cfg, attacker, defender, false, rng, melee, 1f, ratio); + } + + /// + /// 피해 = (공격 × 공격 배율(스킬) × atkRatio × 상성(불리 무시 하한) × 치명타 − 방어 × defenseFactor) × 근접 배율 × scale × 주는·받는 피해 배율 × 난수. + /// 치명타 = 호출 쪽 지정(궁병 목표점 명중) 또는 스킬 「무조건 치명타」·「치명타 확률」 판정(가상 실행 = 확률 판정 없음) · 배율 = critMultiplier × (1 + 치명타 피해 %). + /// + static int DamageCore(BalanceConfig cfg, Unit attacker, Unit defender, bool crit, System.Random rng, bool melee, float scale, float atkRatio) + { + if (!crit) crit = SkillMods.CritAlways(attacker, defender) || RollCrit(attacker, defender, rng); + LastCrit = crit; float mult = Mathf.Max(cfg.Matchup(attacker.Type, defender.Type), SkillMods.MatchupFloor(attacker, true)); - float atk = attacker.Atk * SkillMods.AttackMult(attacker, true) * GimmickLayer.AttackMultOf(attacker); // 군기 점령(PD #1097) - float raw = atk * mult * (crit ? SkillMods.CritMult(attacker, cfg) : 1f) - defender.Def * cfg.defenseFactor; // 보물 크리 % 포함(PD #1093 C) + float atk = attacker.Atk * SkillMods.AttackMult(attacker, true, defender) * atkRatio * GimmickLayer.AttackMultOf(attacker); // 군기 점령(PD #1097) + float raw = atk * mult * (crit ? SkillMods.CritMult(attacker, cfg, defender) : 1f) - defender.Def * cfg.defenseFactor; // 보물 크리 % 포함(PD #1093 C) if (melee) { if (UnitTypeInfo.IsRanged(attacker.Type) && UnitTypeInfo.IsMelee(defender.Type) && !SkillMods.Has(attacker, SkillKeys.MeleePenaltyOff, true)) raw *= cfg.archerMeleeDealtRatio; @@ -32,10 +53,17 @@ namespace TKS raw *= GimmickLayer.MeleeTakenMultOf(attacker, defender); // 고지 위 = 아래에서 오는 근접 피해 −(PD #1097) } else raw *= GimmickLayer.RangedTakenMultOf(defender); // 안개 안 = 받는 원거리 피해 ×p1(화살 · 책사 중심·광역 · PD 2026-11-06 개정) - raw *= scale * SkillMods.DealtMult(attacker, defender, true) * SkillMods.TakenMult(defender, attacker, true); + raw *= scale * SkillMods.DealtMult(attacker, defender, true) * SkillMods.TakenMult(defender, attacker, true, !melee); return Mathf.Max(1, Mathf.RoundToInt(raw * Variance(cfg, rng))); // 가상 실행 = 분산 0(PD #1098) } + static bool RollCrit(Unit a, Unit d, System.Random rng) + { + float c = SkillMods.CritChance(a, d); + if (c <= 0f || rng == null || Sim.Active) return false; + return rng.NextDouble() < c; + } + /// 책사 광역 피해(PD 2026-11-05 #1087 ⑤) = 대상별 사격 피해(상성 적용 · 크리 없음) × ratio · ratio ≤ 0 이면 0. public static int SplashDamage(BalanceConfig cfg, Unit attacker, Unit defender, float ratio, System.Random rng) { @@ -50,18 +78,18 @@ namespace TKS /// public static int CounterDamage(BalanceConfig cfg, Unit defender, Unit attacker, System.Random rng, bool defenderPushed = false) { - float taken = SkillMods.PctProduct(attacker, SkillKeys.CounterTakenPct, true); + float taken = SkillMods.CounterTakenMult(attacker, defender, true); // 받는 반격 %(조건 = 반격하는 상대 · 2026-11-10 #1112) if (taken <= 0f) return 0; - float ratio = SkillMods.CounterRatio(defender, cfg.counterRatio, defenderPushed, true); + float ratio = cfg.counterRatio * SkillMods.CounterDmgMult(defender, true); // 반격 피해 +% int baseDmg = Damage(cfg, defender, attacker, false, rng, true); return Mathf.Max(1, Mathf.RoundToInt(baseDmg * ratio * taken)); } - /// 견제 피해 = 근접 피해 × overwatchDamageRatio × 교변(overwatch_dmg_mult). + /// 견제 피해(옛 창병 대기 · 대기 유닛이 없어 쓰이지 않음) = 근접 피해 × overwatchDamageRatio. 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 * SkillMods.Product(spear, SkillKeys.OverwatchDmgMult, true))); + return Mathf.Max(1, Mathf.RoundToInt(baseDmg * cfg.overwatchDamageRatio)); } // ---------- 기하 질의 ---------- @@ -93,6 +121,39 @@ namespace TKS return null; } + /// 경로 위 모든 유닛(아군·적군 · 가까운 순 · 접촉 거리) — 스킬 관통(손견·안량·동탁 · 아군 통과 시). + public static List UnitsOnPath(Unit actor, Vector2 origin, Vector2 dir, float dist, List distances, float radius = -1f) + { + var list = new List(); + distances.Clear(); + Physics2D.SyncTransforms(); + var hits = Physics2D.CircleCastAll(origin, radius > 0f ? radius : actor.Radius * CastRadiusScale, dir, dist); + System.Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance)); + foreach (var h in hits) + { + var u = UnitOf(h.collider); + if (u == null || u == actor || !u.IsAlive || list.Contains(u)) continue; + list.Add(u); + distances.Add(h.distance); + } + return list; + } + + /// 전장 벽까지 나아갈 수 있는 거리와 그 벽의 면 방향(핀볼 튕김 · 손책·동탁). 벽에 안 닿으면 normal = 0. + public static float BoardWallTravel(Unit actor, Vector2 origin, Vector2 dir, float dist, out Vector2 normal) + { + normal = Vector2.zero; + Physics2D.SyncTransforms(); + float best = dist; + foreach (var h in Physics2D.CircleCastAll(origin, actor.Radius * CastRadiusScale, dir, dist)) + { + if (h.collider.GetComponent() == null || h.distance >= best) continue; + best = h.distance; + normal = h.normal; + } + return Mathf.Max(0f, best - Epsilon); + } + /// 경로(원 캐스트) 위 가장 가까운 유닛(아군·적군 불문). hitDist = 닿기까지 이동 거리. 벽은 무시한다(호출 쪽에서 FreeTravel 과 비교). public static Unit FirstUnitOnPath(Unit actor, Vector2 origin, Vector2 dir, float dist, out float hitDist) { diff --git a/Assets/Scripts/Runtime/Gimmicks/ActionResolver.Gimmicks.cs b/Assets/Scripts/Runtime/Gimmicks/ActionResolver.Gimmicks.cs index b46d1268..54647eb9 100644 --- a/Assets/Scripts/Runtime/Gimmicks/ActionResolver.Gimmicks.cs +++ b/Assets/Scripts/Runtime/Gimmicks/ActionResolver.Gimmicks.cs @@ -29,18 +29,49 @@ namespace TKS /// 근접 병종 슬링샷 도달점(늪 ×p1 · 고지 오르기 ×p3) — Resolve·조준 미리보기 공용. 원거리 병종은 접근·이동 단계에서 따로 계산. public Vector2 ReachPoint(Unit u, Vector2 target) { - if (!Gim || u == null || UnitTypeInfo.IsRanged(u.Type)) return target; + if (u == null || UnitTypeInfo.IsRanged(u.Type)) return target; Vector2 d = target - u.Position; float dist = d.magnitude; if (dist < 0.001f) return target; - return u.Position + d / dist * Layer.CostReach(u, u.Position, d / dist, dist); + float reach = SlowZoneReach(u, u.Position, d / dist, dist); // 조인 철벽(상대 반경 안 이동 비용 +) + if (Gim && !SkillMods.Has(u, SkillKeys.TerrainImmune)) reach = Layer.CostReach(u, u.Position, d / dist, reach); // 축융 = 지형 이동 감소 무시 + return u.Position + d / dist * reach; } static float Reach(Unit u, Vector2 origin, Vector2 dir, float budget) { + if (u != null && SkillMods.Has(u, SkillKeys.TerrainImmune)) return budget; return Gim ? Layer.CostReach(u, origin, dir, budget) : budget; } + /// 조인 철벽(zone_slow): 상대 시전자의 행동 반경(기본 슬링 거리) 안을 지나는 구간 = 이동 비용 × (1 + p1). + static float SlowZoneReach(Unit u, Vector2 origin, Vector2 dir, float budget) + { + var sys = SkillSystem.Current; + if (sys == null) return budget; + List> zones = null; + foreach (var t in sys.TimedEffects) + { + if (t.E.Key != SkillKeys.ZoneSlow || t.Faction == u.Faction || t.Owner == null || t.Owner.Unit == null || !t.Owner.Unit.IsAlive) continue; + if (zones == null) zones = new List>(); + zones.Add(new KeyValuePair(t.Owner.Unit, t.E.P)); + } + if (zones == null) return budget; + const float step = 0.05f; + float spent = 0f, moved = 0f; + while (moved < budget + 10f) + { + Vector2 p = origin + dir * (moved + step * 0.5f); + float cost = 1f; + foreach (var z in zones) + if (Vector2.Distance(z.Key.Position, p) <= SkillMods.BaseReach(z.Key)) cost = Mathf.Max(cost, 1f + z.Value); + if (spent + step * cost > budget) return moved + (budget - spent) / cost; + spent += step * cost; + moved += step; + } + return budget; + } + static void NoteForced(Unit u, Vector2 from, Vector2 to) { if (Gim) Layer.NoteForced(u, from, to); @@ -91,18 +122,22 @@ namespace TKS o.MovedOnly = true; float remaining = Mathf.Max(0f, dist - hitDist); var d = g.Def; + var sk = SkillSystem.Current; + if (sk != null && sk.Ctx != null && sk.Ctx.Actor == u) sk.Ctx.HitTerrain = true; // 맹획 남만왕: 지형물에 닿음 = 재행동 + float gm = SkillMods.GimmickMult(u); // 전위 고지악래: 지형 오브젝트 밀기 거리 × bool pusher = u.Type == UnitType.Infantry || u.Type == UnitType.Spear; if (pusher && g.Rolls) { - yield return RollBoulder(u, g, dir, remaining * d.spearPush, o); // 보·창 = 바위를 남은 거리 × spear_push 만큼 굴린다 · 자신은 정지 + yield return RollBoulder(u, g, dir, remaining * d.spearPush * gm, o); // 보·창 = 바위를 남은 거리 × spear_push 만큼 굴린다 · 자신은 정지 + PushContinue(u, remaining); // 고순: 오브젝트를 밀친 뒤 남은 이동력으로 재행동 yield break; } int dmg = d.DamageBy(u.Type); - if (dmg > 0) yield return DamageGimmick(g, dmg, u, o); + if (dmg > 0) yield return DamageGimmick(g, Mathf.RoundToInt(dmg * gm), u, o); if (!u.IsAlive) yield break; if (u.Type == UnitType.Spear && dmg > 0) { - if (d.spearPush > 0.01f && g.BlocksMove) yield return ShoveGimmick(g, dir, d.spearPush, u, o); // 창 = spear_push 만큼 밀고 정지(튕기지 않음) + if (d.spearPush > 0.01f && g.BlocksMove) yield return ShoveGimmick(g, dir, d.spearPush * gm, u, o); // 창 = spear_push 만큼 밀고 정지(튕기지 않음) yield break; } if (d.bouncePct > 0f) o.Notes.Add(u.DisplayName + " 자기 −" + Layer.PctDamage(u, d.bouncePct, d.nameKo)); @@ -159,7 +194,7 @@ namespace TKS foreach (var v in victims) { if (!v.IsAlive) continue; - int dmg = SkillSystem.Deal(v, Combat.GimmickDamage(cfg, by, v, mult, rng)); + int dmg = SkillSystem.Deal(v, Mathf.RoundToInt(Combat.GimmickDamage(cfg, by, v, mult, rng) * SkillMods.GimmickMult(by)), by, HitKind.Gimmick); // 전위 고지악래 × if (v.Faction == by.Faction) { o.FriendlyHit.Add(v); @@ -228,7 +263,7 @@ namespace TKS } if (victim != null && victim.Faction != pusher.Faction) { - int dmg = SkillSystem.Deal(victim, Combat.GimmickDamage(cfg, pusher, victim, g.P(1, 1f), rng)); + int dmg = SkillSystem.Deal(victim, Mathf.RoundToInt(Combat.GimmickDamage(cfg, pusher, victim, g.P(1, 1f), rng) * SkillMods.GimmickMult(pusher)), pusher, HitKind.Gimmick); o.Attacked.Add(victim); o.DamageDealt += dmg; if (!Sim.Active) Fx.Popup(victim.Position, dmg + " 바위", new Color(1f, 0.75f, 0.4f)); @@ -435,7 +470,7 @@ namespace TKS yield return SpearRoutine(u, from, dir, s, target, moveOnly, o); break; case UnitType.Cavalry: - yield return CavalryRoutine(u, from, dir, s, target, moveOnly, o, false, new List(o.Attacked)); + yield return CavalryRoutine(u, from, dir, s, target, moveOnly, o, new List(o.Attacked)); break; default: yield return MoveOrHit(u, from, dir, s, o); diff --git a/Assets/Scripts/Runtime/Gimmicks/Combat.Gimmicks.cs b/Assets/Scripts/Runtime/Gimmicks/Combat.Gimmicks.cs index 040d0cec..06d74bf7 100644 --- a/Assets/Scripts/Runtime/Gimmicks/Combat.Gimmicks.cs +++ b/Assets/Scripts/Runtime/Gimmicks/Combat.Gimmicks.cs @@ -5,6 +5,9 @@ namespace TKS /// 지형 기믹 질의(PD #1097) — 장애물 콜라이더 · 투사체 pass/block/destroy · 안개 · 기믹 피해 공식. public static partial class Combat { + /// true 인 동안 투사체가 장애물 기믹을 통과(황충 노익장 · 스킬 관통 사격 · 호출 쪽이 켜고 끔). + public static bool ArrowIgnoreGimmicks; + /// 전장 가장자리 벽까지만(기믹·유닛 무시) 나아갈 수 있는 거리. public static float WallTravel(Unit actor, Vector2 origin, Vector2 dir, float dist) { @@ -55,7 +58,7 @@ namespace TKS var g = GimmickLayer.InstOf(h.collider); if (g != null) { - if (!g.BlocksMove || g.Def.projectile == GimmickProjectile.Pass) continue; + if (!g.BlocksMove || g.Def.projectile == GimmickProjectile.Pass || ArrowIgnoreGimmicks) continue; // 황충 노익장 = 장애물 통과(2026-11-10 #1112) travel = h.distance; gimmick = g; return null; diff --git a/Assets/Scripts/Runtime/Gimmicks/GimmickData.cs b/Assets/Scripts/Runtime/Gimmicks/GimmickData.cs index 66995cf5..a5acf80e 100644 --- a/Assets/Scripts/Runtime/Gimmicks/GimmickData.cs +++ b/Assets/Scripts/Runtime/Gimmicks/GimmickData.cs @@ -430,5 +430,7 @@ namespace TKS public const string Bridge = "bridge"; /// 폭약 수레 폭발 · 책사 광역(spawn_on_splash)이 만드는 불 지대의 정의. public const string BurningDebris = "burning_debris"; + /// 목책(노숙 녹각진이 세우는 장애물 · 2026-11-10 #1112). + public const string Palisade = "palisade"; } } diff --git a/Assets/Scripts/Runtime/Gimmicks/GimmickLayer.cs b/Assets/Scripts/Runtime/Gimmicks/GimmickLayer.cs index 3f5ae4fd..749b7e7c 100644 --- a/Assets/Scripts/Runtime/Gimmicks/GimmickLayer.cs +++ b/Assets/Scripts/Runtime/Gimmicks/GimmickLayer.cs @@ -635,6 +635,19 @@ namespace TKS RefreshBadge(u); } + /// 스킬 정화(제갈량·방통·간옹 · 2026-11-10 #1112): 중독·행동 불가 해제. 반환 = 지운 것이 있었다. + public bool Cleanse(Unit u) + { + var s = StateOf(u); + if (s == null) return false; + bool any = s.PoisonTurns > 0 || s.Stun > 0 || s.Skip; + s.PoisonTurns = 0; + s.Stun = 0; + s.Skip = false; + RefreshBadge(u); + return any; + } + public void Stun(Unit u, int turns, string why) { if (u == null || turns <= 0) return; diff --git a/Assets/Scripts/Runtime/Skills/SkillDef.cs b/Assets/Scripts/Runtime/Skills/SkillDef.cs index 9de4d714..f85ba033 100644 --- a/Assets/Scripts/Runtime/Skills/SkillDef.cs +++ b/Assets/Scripts/Runtime/Skills/SkillDef.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Globalization; using TKS.OutGame; using UnityEngine; @@ -14,7 +15,43 @@ namespace TKS Always } - /// 스킬 효과 1개(effect_key 1개 + 값). 키가 하나뿐인 행은 p2·p3 = 그 키의 조건·보조값. + /// 효과가 걸리는 대상(skill_effects.csv target 칸). + public enum SkillScope + { + /// 빈칸 = 효과 주인 자신. + Self, + /// allies = 같은 편 전원(자신 포함). + Allies, + /// camp:<진영>[:<병종>] = 같은 편 · 그 진영(heroes.csv faction) · (그 병종). + Camp, + /// camp_male:<진영> = 같은 편 · 그 진영 · 남성. + CampMale, + /// radius:<r> = 자신 주위 r 안 같은 편(자신 제외 · 매번 다시 봄). + Radius, + /// range = 시전 순간 자신 슬링 최대 거리 안 같은 편(자신 포함 · 대상 고정 · 즉시형 지속 효과). + Range, + /// aura = 지금 자신 슬링 최대 거리 안 같은 편(자신 포함 · 매번 다시 봄). + Aura + } + + /// 효과 발동 조건(skill_effects.csv cond 칸) — 상대(공격 대상·공격자)·치명타·턴. + public enum SkillCondKind + { + None, + Crit, + VsStatus, + VsBleed, + VsConfused, + VsStunned, + VsType, + VsMale, + VsMaleHero, + VsFemaleHero, + TurnLe, + Armed + } + + /// 스킬 효과 1줄(skill_effects.csv) — 키 · 대상 · 조건 · 지속 · p1~p3. public class SkillEffect { public string Key; @@ -23,24 +60,101 @@ namespace TKS public float A3; public bool HasA2; public bool HasA3; + public SkillScope Scope; + public string Camp = ""; + public bool HasType; + public UnitType Type; + public float Radius; + public SkillCondKind Cond; + public UnitType CondType; + public int CondN; + /// 0 = 상시·장착 중·1회 · n = n 라운드(시한 효과). + public int Dur; + + public float Get(int i, float fallback = 0f) + { + if (i == 0) return P; + if (i == 1) return HasA2 ? A2 : fallback; + return HasA3 ? A3 : fallback; + } + + public SkillEffect Clone() + { + return (SkillEffect)MemberwiseClone(); + } public SkillEffect Scaled(float factor) { SkillKeys.Info info; - if (!SkillKeys.All.TryGetValue(Key, out info)) return this; - return new SkillEffect + var c = Clone(); + if (!SkillKeys.All.TryGetValue(Key, out info) || info.Mode == SkillScale.Fixed) return c; + if (info.Idx == 0 || info.Both) c.P = SkillKeys.Apply(info.Mode, P, factor); + if ((info.Idx == 1 || info.Both) && HasA2) c.A2 = SkillKeys.Apply(info.Mode, A2, factor); + if (info.Idx == 2 && HasA3) c.A3 = SkillKeys.Apply(info.Mode, A3, factor); + return c; + } + + /// target 칸 해석(오류면 메시지 · 빈칸 = 자신). + public static string ParseTarget(string s, SkillEffect e) + { + e.Scope = SkillScope.Self; + if (string.IsNullOrEmpty(s)) return null; + var parts = s.Split(':'); + switch (parts[0]) { - Key = Key, - P = SkillKeys.Apply(info.Main, P, factor), - A2 = info.ScaleA2 && HasA2 ? A2 * factor : A2, - A3 = A3, - HasA2 = HasA2, - HasA3 = HasA3, - }; + case "allies": e.Scope = SkillScope.Allies; return null; + case "range": e.Scope = SkillScope.Range; return null; + case "aura": e.Scope = SkillScope.Aura; return null; + case "radius": + e.Scope = SkillScope.Radius; + if (parts.Length < 2 || !float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out e.Radius)) return "radius 값 없음"; + return null; + case "camp": + case "camp_male": + e.Scope = parts[0] == "camp" ? SkillScope.Camp : SkillScope.CampMale; + if (parts.Length < 2 || parts[1].Length == 0) return "진영 없음"; + e.Camp = parts[1]; + if (parts.Length >= 3) + { + if (!UnitTypeInfo.TryParseKey(parts[2], out e.Type)) return "병종 「" + parts[2] + "」 모름"; + e.HasType = true; + } + return null; + } + return "대상 「" + s + "」 모름"; + } + + /// cond 칸 해석(오류면 메시지 · 빈칸 = 조건 없음). + public static string ParseCond(string s, SkillEffect e) + { + e.Cond = SkillCondKind.None; + if (string.IsNullOrEmpty(s)) return null; + var parts = s.Split(':'); + switch (parts[0]) + { + case "crit": e.Cond = SkillCondKind.Crit; return null; + case "vs_status": e.Cond = SkillCondKind.VsStatus; return null; + case "vs_bleed": e.Cond = SkillCondKind.VsBleed; return null; + case "vs_confused": e.Cond = SkillCondKind.VsConfused; return null; + case "vs_stunned": e.Cond = SkillCondKind.VsStunned; return null; + case "vs_male": e.Cond = SkillCondKind.VsMale; return null; + case "vs_male_hero": e.Cond = SkillCondKind.VsMaleHero; return null; + case "vs_female_hero": e.Cond = SkillCondKind.VsFemaleHero; return null; + case "armed": e.Cond = SkillCondKind.Armed; return null; + case "vs_type": + e.Cond = SkillCondKind.VsType; + if (parts.Length < 2 || !UnitTypeInfo.TryParseKey(parts[1], out e.CondType)) return "vs_type 병종 모름"; + return null; + case "turn_le": + e.Cond = SkillCondKind.TurnLe; + if (parts.Length < 2 || !int.TryParse(parts[1], out e.CondN)) return "turn_le 턴 없음"; + return null; + } + return "조건 「" + s + "」 모름"; } } - /// skills.csv 1행의 전투용 정의(SkillInfoDb.Skill → effect_key 를 키 규약대로 풀어 둠). + /// skills.csv 1행 + skill_effects.csv 효과 줄의 전투용 정의. public class SkillDef { public string Id; @@ -54,7 +168,7 @@ namespace TKS public bool NeedsImpl; public readonly List Effects = new List(); - public static SkillDef From(SkillInfoDb.Skill s) + public static SkillDef From(SkillInfoDb.Skill s, SkillInfoDb db) { var d = new SkillDef { @@ -68,12 +182,15 @@ namespace TKS 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) }); - } + if (db != null) + foreach (var r in db.effects) + { + if (r.skillId != s.id) continue; + var e = new SkillEffect { Key = r.key, P = r.p1, A2 = r.p2, A3 = r.p3, HasA2 = r.HasP(1), HasA3 = r.HasP(2), Dur = r.dur }; + SkillEffect.ParseTarget(r.target, e); + SkillEffect.ParseCond(r.cond, e); + d.Effects.Add(e); + } return d; } @@ -98,6 +215,11 @@ namespace TKS if (e.Key == key) return e; return null; } + + public bool Has(string key) + { + return First(key) != null; + } } /// 스킬·영웅 표 조회(Resources/OutGame 의 SkillInfoDb · HeroDb = 임포트 자산 · 전투 시작마다 다시 읽음). @@ -116,15 +238,16 @@ namespace TKS { var db = OutGameData.Default; var s = db != null ? db.Skills : null; - if (defs != null && s == source && s != null && s.skills.Count == sourceCount) return; + int n = s != null ? s.skills.Count + s.effects.Count * 1000 : 0; + if (defs != null && s == source && s != null && n == sourceCount) return; defs = new Dictionary(); source = s; - sourceCount = s != null ? s.skills.Count : 0; + sourceCount = n; 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); + defs[row.id] = SkillDef.From(row, s); } } diff --git a/Assets/Scripts/Runtime/Skills/SkillKeys.cs b/Assets/Scripts/Runtime/Skills/SkillKeys.cs index 5dd95e05..92f60421 100644 --- a/Assets/Scripts/Runtime/Skills/SkillKeys.cs +++ b/Assets/Scripts/Runtime/Skills/SkillKeys.cs @@ -5,152 +5,205 @@ namespace TKS /// 스킬 수치가 레벨(lv_scale)·★5(패시브 배율)로 커지는 방식. public enum SkillScale { - /// 값 × 계수(비율·가산·거리·반경 · 기획서 §5 「수치 × (1 + lv_scale·(lv−1))」). + /// 값 × 계수(비율·가산 · 기획서 §5 「수치 × (1 + lv_scale·(lv−1))」). Value, /// 배율(×k)은 1 을 넘는 몫만 키운다: 1 + (k − 1) × 계수 (×2 가 Lv5 에 ×2.8 이 되지 않게). Mult, /// 횟수 = 반올림(값 × 계수). Count, - /// 켜고 끄는 값·치환 값(커지지 않음). + /// 켜고 끄는 값·턴·반경·확률(커지지 않음). Fixed } /// - /// effect_key 55종 처리기 목록(시스템 기획 §5 · skills.csv · 51 + 영웅 52명 확장 4 · PD #1096) — 키 이름 · 레벨 계수 적용 방식 · 같은 편 전원에게 걸리는가. - /// 기령 제외로 pushed_ally_attack_on_push(대장군)는 표에서 쓰지 않지만 처리기는 남긴다. - /// 이 표는 「처리기의 뜻」이고 수치는 전부 skills.csv(SkillInfoDb)에서 온다(C45). 표에 없는 키는 임포터가 오류로 막는다. - /// p 규약: 키가 여럿이면 p1~p3 = 키 순서 · 키 하나면 p1 = 값 · p2/p3 = 그 키의 조건·보조값(desc_ko [ ] 메모). + /// effect_key 처리기 목록(2026-11-10 #1112 · PD 엑셀 「장수 목록」 새 기획 · 표 = skill_effects.csv 효과 1줄 = 키 · 대상 · 조건 · 지속 · p1~p3). + /// 이 표는 「처리기의 뜻 + 레벨 계수가 붙는 칸」이고 수치는 전부 skill_effects.csv(SkillInfoDb)에서 온다(C45). 표에 없는 키 = 임포트 오류. + /// 보물 진영 효과(range_mult · splash_radius_mult · side_crit_pct)도 같은 질의(SkillMods)를 탄다. /// public static class SkillKeys { + // ── 능력치(상시 · 대상·조건 칸) + public const string AtkPct = "atk_pct"; + public const string AtkRatio = "atk_ratio"; + public const string DmgPct = "dmg_pct"; + public const string DmgTakenPct = "dmg_taken_pct"; + public const string DmgTakenRangedPct = "dmg_taken_ranged_pct"; + public const string DmgTakenFromArcherPct = "dmg_taken_from_archer_pct"; + public const string CritChance = "crit_chance"; + public const string CritDmgPct = "crit_dmg_pct"; + public const string CritAlways = "crit_always"; + public const string CounterTakenPct = "counter_taken_pct"; + public const string CounterDmgPct = "counter_dmg_pct"; + public const string CounterLifesteal = "counter_lifesteal"; + public const string Lifesteal = "lifesteal"; + public const string HpMaxPct = "hp_max_pct"; public const string SlingDistPct = "sling_dist_pct"; + public const string RangeMult = "range_mult"; + public const string SplashRadiusMult = "splash_radius_mult"; + public const string SplashAllyRatio = "splash_ally_ratio"; + public const string MeleePenaltyOff = "melee_penalty_off"; + public const string MatchupFloor = "matchup_floor"; + public const string KnockbackTakenPct = "knockback_taken_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 KillStackAtk = "kill_stack_atk"; + public const string AtkMissingHp = "atk_missing_hp"; + public const string AtkPerAllyRange = "atk_per_ally_range"; 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"; - // 영웅 52명 확장(PD #1096 · 기획서 §5 끝) — 신규 4종 - public const string TeamDmgPctTurn = "team_dmg_pct_turn"; - public const string CooldownAdd = "cooldown_add"; - public const string Charm = "charm"; + public const string DashDmgPct = "dash_dmg_pct"; + public const string DmgPerHit = "dmg_per_hit"; + public const string FirstHitBonus = "first_hit_bonus"; + public const string SkillPowerLowHp = "skill_power_low_hp"; + public const string SkillHpCost = "skill_hp_cost"; + public const string LowHpTaken = "low_hp_taken"; + public const string UnusedMoveDr = "unused_move_dr"; + public const string RegenPct = "regen_pct"; + public const string HealOnActionEnd = "heal_on_action_end"; + public const string HealOnKill = "heal_on_kill"; + public const string KillReaction = "kill_reaction"; + public const string ReactionAtkStack = "reaction_atk_stack"; + public const string MoveBank = "move_bank"; + public const string TerrainImmune = "terrain_immune"; + public const string Revive = "revive"; + public const string NegateHits = "negate_hits"; + public const string DrainAura = "drain_aura"; + public const string Guard = "guard"; + public const string GuardCounter = "guard_counter"; + public const string GuardRadiusMult = "guard_radius_mult"; + public const string GuardDr = "guard_dr"; + public const string AxeAura = "axe_aura"; + public const string ZoneSlow = "zone_slow"; + public const string OnAttackedStun = "on_attacked_stun"; + public const string OnAttackedConfuse = "on_attacked_confuse"; + public const string MultiMaleStun = "multi_male_stun"; + public const string TurnEndHeal = "turn_end_heal"; + public const string TurnEndHealLowest = "turn_end_heal_lowest"; + public const string TurnEndCleanse = "turn_end_cleanse"; + public const string TurnStartHealRange = "turn_start_heal_range"; + public const string DeathBanner = "death_banner"; + public const string Hot = "hot"; + public const string HealCleanse = "heal_cleanse"; + public const string AllyHitHeal = "ally_hit_heal"; + public const string AllyHitBuff = "ally_hit_buff"; + public const string AllyHitWarp = "ally_hit_warp"; + public const string AllyHitReaction = "ally_hit_reaction"; + public const string ActionEndSacrifice = "action_end_sacrifice"; + public const string PushContinue = "push_continue"; + public const string PushedAllyAtkAdd = "pushed_ally_atk_add"; + public const string SelfPushedMult = "self_pushed_mult"; + public const string GimmickMult = "gimmick_mult"; + public const string Zombie = "zombie"; + public const string DoubleHitStun = "double_hit_stun"; + public const string KillInRangeReward = "kill_in_range_reward"; + public const string Invuln = "invuln"; + public const string DamageToAtk = "damage_to_atk"; + + // ── 명중 시 상태 이상(공격자 효과 · 조건 칸 · p3 = 확률) + public const string HitStun = "hit_stun"; + public const string HitBleed = "hit_bleed"; + public const string HitPoison = "hit_poison"; + public const string PoisonStack = "poison_stack"; + public const string HitBurn = "hit_burn"; + public const string HitConfuse = "hit_confuse"; + public const string HitPush = "hit_push"; + + // ── 다음 행동(장착 중) 이동·공격 변형 + public const string ExtraAction = "extra_action"; + public const string ReactionOnHit = "reaction_on_hit"; + public const string ReactionOnKill = "reaction_on_kill"; + public const string ReactionOnTerrain = "reaction_on_terrain"; + public const string PassAllies = "pass_allies"; + public const string PierceThrough = "pierce_through"; + public const string SwapOnHit = "swap_on_hit"; + public const string DragBack = "drag_back"; + public const string AimPush = "aim_push"; + public const string ReturnOnHit = "return_on_hit"; + public const string ReturnAlways = "return_always"; + public const string ReturnAim = "return_aim"; + public const string WallBounce = "wall_bounce"; + public const string Rampage = "rampage"; + public const string RepeatAction = "repeat_action"; + + // ── 원거리 특수 사격(장착 중) + public const string ArrowPassAllies = "arrow_pass_allies"; + public const string ShotGiant = "shot_giant"; + public const string ShotPierce = "shot_pierce"; + public const string ShotBlast = "shot_blast"; + public const string ShotFan = "shot_fan"; + public const string Multishot = "multishot"; + public const string ShotRicochet = "shot_ricochet"; + public const string ShotSingle = "shot_single"; + public const string ShotWhirl = "shot_whirl"; + public const string ShotBarricade = "shot_barricade"; + public const string ShotFirewall = "shot_firewall"; + public const string ShotAsArcher = "shot_as_archer"; + public const string FireZone = "fire_zone"; + public const string PoisonZone = "poison_zone"; + public const string SplashHeal = "splash_heal"; + public const string SplashHealLowest = "splash_heal_lowest"; + + // ── 즉시 발동 + public const string AoeStrike = "aoe_strike"; + public const string AoeDrain = "aoe_drain"; + public const string Volley = "volley"; + public const string CharmPull = "charm_pull"; + public const string HealAtk = "heal_atk"; public const string HealSelfPct = "heal_self_pct"; + public const string Cleanse = "cleanse"; + public const string Teleport = "teleport"; + public const string GrantReaction = "grant_reaction"; public class Info { - public SkillScale Main; - /// p2(키 하나인 행의 보조값)도 같은 계수로 키우는가(절대 가산 · 최대치 · 충돌 추가 피해). - public bool ScaleA2; - /// 같은 편 전원에게 걸리는 효과(주인 = 같은 편 · 살아 있을 때). - public bool Team; + public SkillScale Mode; + /// 레벨·★ 계수가 붙는 칸(0 = p1 · 1 = p2 · 2 = p3). + public int Idx; + /// p2 도 같은 계수(이동 % · 공격 % 두 값). + public bool Both; } - static Info I(SkillScale m, bool a2 = false, bool team = false) + static Info I(SkillScale m, int idx = 0, bool both = false) { - return new Info { Main = m, ScaleA2 = a2, Team = team }; + return new Info { Mode = m, Idx = idx, Both = both }; } + static readonly Info Fix = new Info { Mode = SkillScale.Fixed }; + public static readonly Dictionary All = new Dictionary { - { 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) }, // 대상 = 궁병·책사 - { TeamDmgPctTurn, I(SkillScale.Value, false, true) }, // 즉시 = 이번 아군 턴 아군 전원 가하는 피해 % - { CooldownAdd, I(SkillScale.Count) }, // p2 1 = 아군 전원 · 0 = 자기만 · 쿨 최소 1 - { Charm, I(SkillScale.Fixed) }, // p1 반경 · p2 적 턴 수 · 대상 = 무장 우선 - { HealSelfPct, I(SkillScale.Value) }, + { AtkPct, I(SkillScale.Value) }, { AtkRatio, I(SkillScale.Mult) }, { DmgPct, I(SkillScale.Value) }, + { DmgTakenPct, I(SkillScale.Value) }, { DmgTakenRangedPct, I(SkillScale.Value) }, { DmgTakenFromArcherPct, I(SkillScale.Value) }, + { CritChance, I(SkillScale.Value) }, { CritDmgPct, I(SkillScale.Value) }, { CritAlways, Fix }, + { CounterTakenPct, I(SkillScale.Value) }, { CounterDmgPct, I(SkillScale.Value) }, { CounterLifesteal, I(SkillScale.Value) }, + { Lifesteal, I(SkillScale.Value) }, { HpMaxPct, I(SkillScale.Value) }, { SlingDistPct, I(SkillScale.Value) }, + { RangeMult, I(SkillScale.Mult) }, { SplashRadiusMult, I(SkillScale.Mult) }, { SplashAllyRatio, Fix }, + { MeleePenaltyOff, Fix }, { MatchupFloor, Fix }, { KnockbackTakenPct, Fix }, { PushMult, I(SkillScale.Mult) }, + { SelfBounceMult, Fix }, { KillStackAtk, I(SkillScale.Value) }, { AtkMissingHp, I(SkillScale.Value) }, + { AtkPerAllyRange, I(SkillScale.Value) }, { OnAllyDeathAtk, I(SkillScale.Value) }, { DashDmgPct, I(SkillScale.Value) }, + { DmgPerHit, I(SkillScale.Value) }, { FirstHitBonus, I(SkillScale.Value) }, { SkillPowerLowHp, I(SkillScale.Value) }, + { SkillHpCost, Fix }, { LowHpTaken, Fix }, { UnusedMoveDr, I(SkillScale.Value) }, { RegenPct, I(SkillScale.Value) }, + { HealOnActionEnd, I(SkillScale.Value) }, { HealOnKill, I(SkillScale.Value) }, { KillReaction, Fix }, + { ReactionAtkStack, I(SkillScale.Value) }, { MoveBank, Fix }, { TerrainImmune, Fix }, { Revive, Fix }, { NegateHits, Fix }, + { DrainAura, I(SkillScale.Value) }, { Guard, Fix }, { GuardCounter, Fix }, { GuardRadiusMult, I(SkillScale.Mult) }, + { GuardDr, I(SkillScale.Value) }, { AxeAura, I(SkillScale.Value, 1) }, { ZoneSlow, I(SkillScale.Value) }, + { OnAttackedStun, Fix }, { OnAttackedConfuse, Fix }, { MultiMaleStun, Fix }, { TurnEndHeal, I(SkillScale.Value, 1) }, + { TurnEndHealLowest, I(SkillScale.Value, 1) }, { TurnEndCleanse, Fix }, { TurnStartHealRange, I(SkillScale.Value) }, + { DeathBanner, Fix }, { Hot, I(SkillScale.Value) }, { HealCleanse, Fix }, { AllyHitHeal, I(SkillScale.Value) }, + { AllyHitBuff, I(SkillScale.Value, 0, true) }, { AllyHitWarp, Fix }, { AllyHitReaction, Fix }, { ActionEndSacrifice, Fix }, + { PushContinue, Fix }, { PushedAllyAtkAdd, I(SkillScale.Value) }, { SelfPushedMult, I(SkillScale.Mult) }, + { GimmickMult, I(SkillScale.Mult) }, { Zombie, Fix }, { DoubleHitStun, Fix }, { KillInRangeReward, Fix }, + { Invuln, Fix }, { DamageToAtk, Fix }, + { HitStun, Fix }, { HitBleed, I(SkillScale.Value, 1) }, { HitPoison, I(SkillScale.Value, 1) }, { PoisonStack, Fix }, + { HitBurn, I(SkillScale.Value, 1) }, { HitConfuse, Fix }, { HitPush, I(SkillScale.Value) }, + { ExtraAction, Fix }, { ReactionOnHit, Fix }, { ReactionOnKill, Fix }, { ReactionOnTerrain, Fix }, { PassAllies, Fix }, + { PierceThrough, Fix }, { SwapOnHit, Fix }, { DragBack, Fix }, { AimPush, Fix }, { ReturnOnHit, Fix }, { ReturnAlways, Fix }, + { ReturnAim, Fix }, { WallBounce, Fix }, { Rampage, Fix }, { RepeatAction, Fix }, + { ArrowPassAllies, Fix }, { ShotGiant, I(SkillScale.Mult, 1) }, { ShotPierce, I(SkillScale.Mult) }, { ShotBlast, I(SkillScale.Mult, 1) }, + { ShotFan, Fix }, { Multishot, Fix }, { ShotRicochet, I(SkillScale.Value, 1) }, { ShotSingle, Fix }, { ShotWhirl, Fix }, + { ShotBarricade, Fix }, { ShotFirewall, I(SkillScale.Mult, 2) }, { ShotAsArcher, Fix }, { FireZone, Fix }, { PoisonZone, Fix }, + { SplashHeal, I(SkillScale.Value) }, { SplashHealLowest, I(SkillScale.Value) }, + { AoeStrike, I(SkillScale.Mult, 1) }, { AoeDrain, I(SkillScale.Mult) }, { Volley, I(SkillScale.Mult) }, { CharmPull, Fix }, + { HealAtk, I(SkillScale.Value) }, { HealSelfPct, I(SkillScale.Value) }, { Cleanse, Fix }, { Teleport, Fix }, { GrantReaction, Fix }, }; public static bool Known(string key) @@ -164,7 +217,7 @@ namespace TKS switch (mode) { case SkillScale.Value: return p * factor; - case SkillScale.Mult: return 1f + (p - 1f) * factor; + case SkillScale.Mult: return p > 0f ? 1f + (p - 1f) * factor : p; case SkillScale.Count: return UnityEngine.Mathf.Floor(p * factor + 0.5f); } return p; diff --git a/Assets/Scripts/Runtime/Skills/SkillMods.cs b/Assets/Scripts/Runtime/Skills/SkillMods.cs index b62482e2..8bb4ffd0 100644 --- a/Assets/Scripts/Runtime/Skills/SkillMods.cs +++ b/Assets/Scripts/Runtime/Skills/SkillMods.cs @@ -4,16 +4,16 @@ using UnityEngine; namespace TKS { /// - /// 스킬 수치 질의 — ActionResolver · Combat · EnemyAI · BattleManager 가 수치를 읽을 때 부른다(시스템 기획 §5 · PD #1093 B). - /// Get(unit, key) = 그 유닛에게 지금 걸린 효과(자기 패시브 · 같은 편 패시브(팀 키) · 장착된 예약 액티브 · 시한 효과). - /// 스킬 시스템이 없거나 효과가 없으면 중립값(합 0 · 곱 1)이라 기존 규칙 그대로다. - /// note = 실제 적용(피해·밀기) 때만 true → 패시브 발동 HUD 한 줄. + /// 스킬 수치 질의(2026-11-10 #1112 · 효과 표 skill_effects.csv) — ActionResolver · Combat · EnemyAI · BattleManager · SlingshotInput 이 수치를 읽을 때 부른다. + /// Get(unit, key, other, crit) = 그 유닛에게 지금 걸린 효과: 패시브(대상 칸 = 자신 · 같은 편 · 진영 · 반경 · 슬링 범위) · 장착된 예약 액티브 · 시한 효과 · 보물 진영 효과. + /// 조건 칸(치명타 · 상대 상태 이상 · 병종 · 성별 · 턴 · 장착 중)은 other(공격 대상 또는 공격자)·crit 으로 판정한다. 효과가 없으면 중립값(합 0 · 곱 1). + /// note = 실제 적용 때만 true → 패시브 발동 HUD 한 줄. /// public static class SkillMods { static readonly List Empty = new List(); - public static List Get(Unit u, string key) + public static List Get(Unit u, string key, Unit other = null, bool crit = false) { var sys = SkillSystem.Current; if (sys == null || u == null) return Empty; @@ -22,20 +22,21 @@ namespace TKS { var src = s.Unit; if (src == null) continue; - if (s.PassiveEffects != null && src.IsAlive) + if (s.PassiveEffects != null && (src.IsAlive || s.BannerLeft > 0)) { foreach (var e in s.PassiveEffects) { - if (e.Key != key || !Applies(src, u, e)) continue; + if (e.Key != key || !Applies(s, src, u, e, false) || !CondOk(sys, s, e, u, other, crit)) continue; if (list == null) list = new List(); list.Add(new SkillHit { Owner = s, Def = s.Passive, E = e, Passive = true }); } } - if (s.Armed && src == u && s.ActiveEffects != null) + if ((s.Armed || s.Casting) && src == u && s.ActiveEffects != null) { foreach (var e in s.ActiveEffects) { - if (e.Key != key) continue; + if (e.Key != key || e.Key == SkillKeys.Invuln || !CondOk(sys, s, e, u, other, crit)) continue; + if (e.Scope != SkillScope.Self && e.Dur > 0) continue; // 대상이 다른 지속 효과 = 시한 효과로만 if (list == null) list = new List(); list.Add(new SkillHit { Owner = s, Def = s.Active, E = e, Passive = false }); } @@ -43,9 +44,9 @@ namespace TKS } foreach (var t in sys.TimedEffects) { - if (t.E.Key != key || !sys.WindowActive(t) || !TimedApplies(t, u)) continue; + if (t.E.Key != key || !TimedApplies(t, u) || !CondOk(sys, t.Owner, t.E, u, other, crit)) continue; if (list == null) list = new List(); - list.Add(new SkillHit { Owner = t.Owner, Def = t.Def, E = t.E, Passive = false }); + list.Add(new SkillHit { Owner = t.Owner, Def = t.Def, E = t.E, Passive = false, Timed = t }); } // 진영 상시 효과(보물 수집품 crit·range·splash · PD #1093 C) — 주인 없음 · HUD 한 줄 없음 var side = sys.Battle != null ? sys.Battle.SideEffects(u.Faction) : null; @@ -59,34 +60,37 @@ namespace TKS return list ?? Empty; } - /// 보물 전용 크리 피해 % 키(skills.csv 키 아님 · SkillKeys.All 에 넣지 않음 · PD #1093 C). + /// 보물 전용 크리 피해 % 키(skills 표 키 아님 · SkillKeys.All 에 넣지 않음 · PD #1093 C). public const string SideCritPct = "side_crit_pct"; - /// 크리 배율 = 자산 critMultiplier × (1 + 보물 크리 %). - public static float CritMult(Unit a, BalanceConfig cfg) + /// 「행동 반경」 = 주인의 기본 슬링 최대 거리(효과로 늘어난 값이 아님 — 질의가 서로 부르지 않게). + public static float BaseReach(Unit owner) { - return cfg.critMultiplier * (1f + Sum(a, SideCritPct)); + return owner != null ? owner.SlingMaxDistance : 0f; } - static bool IsTeam(SkillEffect e) + public static bool Within(Unit owner, Unit u, float r) { - SkillKeys.Info info; - if (e.Key == SkillKeys.SlingDistPct) return e.HasA3 && e.A3 >= 0.5f; // p3 1 = 아군 전원 - // 영웅 52명 확장(PD #1096): 기존 키의 조건 열로 아군 전원 — 원소 받는 피해(p3 출전 인원) · 순욱 반격(p3 1) · 순욱 최대 HP(p2 1) - if (e.Key == SkillKeys.DmgTakenPct) return e.HasA3 && e.A3 > 0f; - if (e.Key == SkillKeys.CounterRatio) return e.HasA3 && e.A3 >= 0.5f; - if (e.Key == SkillKeys.HpMaxPct) return e.HasA2 && e.A2 >= 0.5f; - if (e.Key == SkillKeys.CooldownAdd) return e.HasA2 && e.A2 >= 0.5f; - return SkillKeys.All.TryGetValue(e.Key, out info) && info.Team; + return owner != null && u != null && Vector2.Distance(owner.Position, u.Position) <= r + u.Radius; } - static bool Applies(Unit src, Unit u, SkillEffect e) + /// 대상 칸 판정(dynamic = 매번 · Range 는 시한 효과에서 고정 대상으로 처리). + public static bool Applies(UnitSkills s, Unit src, Unit u, SkillEffect e, bool timed) { - if (!IsTeam(e)) return src == u; + if (e.Scope == SkillScope.Self) 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; + switch (e.Scope) + { + case SkillScope.Allies: return true; + case SkillScope.Camp: return u.Camp == e.Camp && (!e.HasType || u.Type == e.Type); + case SkillScope.CampMale: return u.Camp == e.Camp && !u.Female; + case SkillScope.Radius: return u != src && Within(src, u, e.Radius); + case SkillScope.Range: + case SkillScope.Aura: + if (!src.IsAlive && s != null && s.BannerLeft > 0) return Vector2.Distance(s.BannerPos, u.Position) <= BaseReach(src) + u.Radius; + return Within(src, u, BaseReach(src)); + } + return false; } static bool TimedApplies(SkillSystem.Timed t, Unit u) @@ -94,11 +98,33 @@ namespace TKS 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; + if (t.E.Scope != SkillScope.Self && !src.IsAlive) return false; + return Applies(t.Owner, src, u, t.E, true); + } + + /// 조건 칸 — self = 효과를 받는 유닛 · other = 상대(공격 대상 또는 공격자). + static bool CondOk(SkillSystem sys, UnitSkills owner, SkillEffect e, Unit self, Unit other, bool crit) + { + switch (e.Cond) + { + case SkillCondKind.None: return true; + case SkillCondKind.Crit: return crit; + case SkillCondKind.TurnLe: return sys.Battle != null && sys.Battle.TurnNumber <= e.CondN; + case SkillCondKind.Armed: return owner != null && (owner.Armed || owner.Casting); + } + if (other == null) return false; + switch (e.Cond) + { + case SkillCondKind.VsStatus: return SkillStatus.HasAny(other); + case SkillCondKind.VsBleed: return SkillStatus.Bleeding(other); + case SkillCondKind.VsConfused: return SkillStatus.Confused(other); + case SkillCondKind.VsStunned: return SkillStatus.Stunned(other); + case SkillCondKind.VsType: return other.Type == e.CondType; + case SkillCondKind.VsMale: return !other.Female; + case SkillCondKind.VsMaleHero: return !other.Female && sys.IsGeneral(other); + case SkillCondKind.VsFemaleHero: return other.Female && sys.IsGeneral(other); + } + return false; } static void Note(SkillHit h, bool note) @@ -109,10 +135,10 @@ namespace TKS // ---------- 일반 ---------- - public static float Sum(Unit u, string key, bool note = false) + public static float Sum(Unit u, string key, bool note = false, Unit other = null, bool crit = false) { float s = 0f; - foreach (var h in Get(u, key)) + foreach (var h in Get(u, key, other, crit)) { s += h.E.P; Note(h, note); @@ -120,10 +146,10 @@ namespace TKS return s; } - public static float Product(Unit u, string key, bool note = false) + public static float Product(Unit u, string key, bool note = false, Unit other = null) { float p = 1f; - foreach (var h in Get(u, key)) + foreach (var h in Get(u, key, other)) { p *= h.E.P; Note(h, note); @@ -132,10 +158,10 @@ namespace TKS } /// Π(1 + 값) · 0 아래로 내려가지 않음(받는 피해 −% 류). - public static float PctProduct(Unit u, string key, bool note = false) + public static float PctProduct(Unit u, string key, bool note = false, Unit other = null) { float p = 1f; - foreach (var h in Get(u, key)) + foreach (var h in Get(u, key, other)) { p *= Mathf.Max(0f, 1f + h.E.P); Note(h, note); @@ -143,49 +169,45 @@ namespace TKS return p; } - public static bool Has(Unit u, string key, bool note = false) + public static bool Has(Unit u, string key, bool note = false, Unit other = null, bool crit = false) { - var l = Get(u, key); + var l = Get(u, key, other, crit); foreach (var h in l) Note(h, note); return l.Count > 0; } - /// 횟수 = 걸린 효과 중 최댓값 · 없으면 fallback. - public static int Count(Unit u, string key, int fallback, bool note = false) + /// 첫 효과(수치 칸을 그대로 쓰는 처리기) · 없으면 null. + public static SkillEffect First(Unit u, string key, 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; + if (l.Count == 0) return null; + Note(l[0], note); + return l[0].E; } // ---------- 슬링 거리 · 사거리 · 광역 ---------- - /// 슬링 최대 거리 = 기본 × (1 + 비율 합) + 절대 가산(적토마 p2) — 동남풍(이번 턴 아군 전원) 포함. + /// + /// 슬링 최대 거리 = 기본 × (1 + 이동 % 합) × (1 + 진중 축적) × 혼란 배율 · 재행동 남은 거리 상한(감녕·고순) — 조준 · 실제 이동 · 적 AI 공용. + /// 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); + float pct = Sum(u, SkillKeys.SlingDistPct, note); + var sys = SkillSystem.Current; + var s = sys != null ? sys.Of(u) : null; + float bank = s != null ? s.Bank : 0f; + float v = u.SlingMaxDistance * Mathf.Max(0.1f, 1f + pct) * (1f + bank) * SkillStatus.ConfuseMult(u); + if (s != null && s.ReactLimit > 0f) v = Mathf.Min(v, s.ReactLimit); + return Mathf.Max(0.1f, v); } - /// 궁병·책사 사거리 = (기본 + range_add + 대도독 team_range_add + 고지 p1(지형 기믹 PD #1097)) × range_mult. + /// 궁병·책사 사거리 = (기본 + 고지 p1(PD #1097)) × range_mult(대상·조건 칸 · 보물) · 근접 병종이 원거리로 쏠 때(축융 비도·주태 검기) = 슬링 거리 기준. 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) + GimmickLayer.RangeBonusOf(u); + float b = UnitTypeInfo.IsRanged(u.Type) ? u.AttackRange : u.SlingMaxDistance; + float r = b + GimmickLayer.RangeBonusOf(u); return Mathf.Max(0f, r * Product(u, SkillKeys.RangeMult, note)); } @@ -194,21 +216,12 @@ namespace TKS return cfg.strategistSplashRadius * Product(u, SkillKeys.SplashRadiusMult, note); } - /// 광역 비율 = 화공 치환값(여럿이면 큰 값) · 없으면 자산 값. 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; + return cfg.strategistSplashRatio; } - /// 광역 아군 비율 = 팔문 간파 치환값(여럿이면 작은 값) · 없으면 자산 값. + /// 광역 아군 비율 = 서서·제갈근 치환값(여럿이면 작은 값) · 없으면 자산 값. public static float SplashAllyRatio(Unit u, BalanceConfig cfg, bool note = false) { var l = Get(u, SkillKeys.SplashAllyRatio); @@ -224,7 +237,7 @@ namespace TKS // ---------- 피해 ---------- - /// 상성 하한(대부 · 0 = 없음). + /// 상성 하한(불리 상성 무시 = 1 · 0 = 없음). public static float MatchupFloor(Unit a, bool note = false) { float f = 0f; @@ -236,21 +249,38 @@ namespace TKS return f; } - /// 공격력 배율 = 1 + 「공 +%」 합(노익장·맹장 HP 조건 · 선봉 1턴 · 고육계 · 복수 · 패자 · 팔백 돌격). - public static float AttackMult(Unit a, bool note = false) + /// + /// 공격력 배율 = (1 + 공 % 합(조건 = 상대 d) + 처치 누적 + 잃은 HP + 범위 안 아군 + 아군 전사 + 돌진 거리 + 재행동 누적 + 고육계 이월) × 「공격력의 X %」(atk_ratio) × 저HP 스킬 위력(주유). + /// + public static float AttackMult(Unit a, bool note = false, Unit d = null) { var sys = SkillSystem.Current; if (sys == null || a == null) return 1f; - float pct = 0f; - foreach (var h in Get(a, SkillKeys.DmgPctHpBelow)) + var s = sys.Of(a); + float pct = Sum(a, SkillKeys.AtkPct, note, d); + foreach (var h in Get(a, SkillKeys.KillStackAtk)) { - float th = h.E.HasA2 ? h.E.A2 : 0f; - if (a.Hp > a.MaxHp * th) continue; - pct += h.E.P; + int kills = s != null ? s.Kills : 0; + if (kills <= 0) continue; + pct += Mathf.Min(h.E.HasA2 ? h.E.A2 : float.MaxValue, h.E.P * kills); + Note(h, note); + } + float missing = a.MaxHp > 0 ? Mathf.Clamp01(1f - (float)a.Hp / a.MaxHp) : 0f; + foreach (var h in Get(a, SkillKeys.AtkMissingHp)) + { + if (missing <= 0f) continue; + pct += Mathf.Min(h.E.HasA2 && h.E.A2 > 0f ? h.E.A2 : float.MaxValue, h.E.P * missing); + Note(h, note); + } + foreach (var h in Get(a, SkillKeys.AtkPerAllyRange)) + { + int n = 0; + foreach (var o in sys.Battle.AliveUnits(a.Faction)) + if (o != a && Within(a, o, BaseReach(a))) n++; + if (n == 0) continue; + pct += h.E.P * n; 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) { @@ -262,166 +292,152 @@ namespace TKS 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); + var ctx = sys.Ctx; + if (ctx != null && ctx.Actor == a && ctx.SlingRatio > 0f) + foreach (var h in Get(a, SkillKeys.DashDmgPct)) + { + pct += h.E.P * ctx.SlingRatio; + Note(h, note); + } + if (s != null && s.ReactsThisTurn > 0) + foreach (var h in Get(a, SkillKeys.ReactionAtkStack)) + { + pct += h.E.P * Mathf.Pow(2f, s.ReactsThisTurn - 1); + Note(h, note); + } + if (s != null && s.AtkFlat > 0f && a.Atk > 0) pct += s.AtkFlat / a.Atk; // 고육계: 지난 턴 받은 피해 → 이번 턴 공격력 + float m = Mathf.Max(0f, 1f + pct); + m *= Product(a, SkillKeys.AtkRatio, note); + if (s != null && (s.Armed || s.Casting)) + foreach (var h in Get(a, SkillKeys.SkillPowerLowHp)) + { + float step = h.E.HasA2 && h.E.A2 > 0f ? h.E.A2 : 0.1f; + int k = Mathf.FloorToInt(missing / step + 1e-4f); + if (k <= 0) continue; + m *= 1f + h.E.P * k; + Note(h, note); + } + return m; } - static int DeadAllies(Unit a) + public 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++; + if (u != null && u != a && u.Faction == a.Faction && !u.IsAlive && !SkillSystem.IsSummon(u)) n++; return n; } - /// 주는 피해 배율 = (1 + 알까기 피해 %) × 무장 대상 배율(참장 = 영웅·보스 · 파로 = 보스만). + /// 주는 피해 배율 = 1 + 피해 % 합(조건 = 상대 d) + 마초 「맞힌 적 1기당」. public static float DealtMult(Unit a, Unit d, bool note = false) { var sys = SkillSystem.Current; - float m = 1f + Sum(a, SkillKeys.TeamDmgPctTurn, note); // 십승십패·사세삼공(이번 턴 아군 전원) - foreach (var h in Get(a, SkillKeys.DmgPct)) - { - // 키 하나인 행의 p2 = 발동 턴(이상 · 여몽 5턴째부터) - if (h.E.HasA2 && h.E.A2 >= 1f && sys != null && sys.Battle.TurnNumber < Mathf.RoundToInt(h.E.A2)) continue; - m += h.E.P; - Note(h, 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); - } + float m = 1f + Sum(a, SkillKeys.DmgPct, note, d); + var ctx = sys != null ? sys.Ctx : null; + if (ctx != null && ctx.Actor == a && ctx.HitCount > 0) + foreach (var h in Get(a, SkillKeys.DmgPerHit)) + { + m += h.E.P * ctx.HitCount; + Note(h, note); + } return Mathf.Max(0f, m); } - /// 받는 피해 배율 = 수성·노장 × 간웅(이번 적 턴) × 철벽(반경) × 적 궁병 피해 감소. - public static float TakenMult(Unit d, Unit a, bool note = false) + /// 받는 피해 배율 = 받는 피해 %(조건 = 공격자 a) × 원거리 받는 피해 × 적 궁병 피해 × 수성(남은 이동 거리) × 노장(잃은 HP). + public static float TakenMult(Unit d, Unit a, bool note = false, bool ranged = false) { - float m = DmgTakenPct(d, a, note) * PctProduct(d, SkillKeys.TeamDmgTakenPctTurn, note) * PctProduct(d, SkillKeys.DmgTakenAura, note); - if (a != null && a.Type == UnitType.Archer) m *= PctProduct(d, SkillKeys.DmgTakenFromArcherPct, note); + float m = PctProduct(d, SkillKeys.DmgTakenPct, note, a); + if (ranged) m *= PctProduct(d, SkillKeys.DmgTakenRangedPct, note, a); + if (a != null && a.Type == UnitType.Archer) m *= PctProduct(d, SkillKeys.DmgTakenFromArcherPct, note, a); + var sys = SkillSystem.Current; + var s = sys != null ? sys.Of(d) : null; + if (s != null && s.UnusedMove > 0f) + foreach (var h in Get(d, SkillKeys.UnusedMoveDr)) + { + m *= Mathf.Max(0f, 1f - h.E.P * s.UnusedMove); + Note(h, note); + } + float lost = d.MaxHp > 0 ? Mathf.Clamp01(1f - (float)d.Hp / d.MaxHp) * 100f : 0f; + if (lost > 0f) + foreach (var h in Get(d, SkillKeys.LowHpTaken)) + { + m *= 1f + h.E.P * lost; + Note(h, note); + } return m; } - /// - /// 받는 피해 % (dmg_taken_pct) — 키 하나인 행의 조건(영웅 52명 확장 · PD #1096): p2 0~1 = HP 그 비율 이하일 때(위연) · p2 2 = 보스에게서만(원술) · - /// p2 3 = 근접 유닛에게서만(초선) · p3 n = 출전 n 인 이상일 때 아군 전원(원소). 조건 열이 없으면 기존 그대로(조인·황개). - /// - static float DmgTakenPct(Unit d, Unit a, bool note) + // ---------- 치명타 ---------- + + /// 크리 배율 = 자산 critMultiplier × (1 + 보물 크리 %) × (1 + 치명타 피해 % 합). + public static float CritMult(Unit a, BalanceConfig cfg, Unit d = null) { - var sys = SkillSystem.Current; - float p = 1f; - foreach (var h in Get(d, SkillKeys.DmgTakenPct)) - { - if (h.E.HasA2 && h.E.A2 > 0f) - { - float c = h.E.A2; - if (c < 1f && d.Hp > d.MaxHp * c) continue; - if (Mathf.Approximately(c, 2f) && (a == null || !a.IsBoss)) continue; - if (Mathf.Approximately(c, 3f) && (a == null || !UnitTypeInfo.IsMelee(a.Type))) continue; - } - if (h.E.HasA3 && h.E.A3 > 0f && (sys == null || sys.StartCount(d.Faction) < Mathf.RoundToInt(h.E.A3))) continue; - p *= Mathf.Max(0f, 1f + h.E.P); - Note(h, note); - } - return p; + return cfg.critMultiplier * (1f + Sum(a, SideCritPct)) * (1f + Sum(a, SkillKeys.CritDmgPct, true, d)); } - /// 반격 배율 = 무성·무언의 규율(치환) · 진중(p2 1 = 밀렸을 때만). - public static float CounterRatio(Unit counterer, float baseRatio, bool pushed, bool note = false) + public static float CritChance(Unit a, Unit d) { - 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; + return Mathf.Clamp01(Sum(a, SkillKeys.CritChance, false, d)); } - // ---------- 밀기 · 튕김 · 관통 ---------- + public static bool CritAlways(Unit a, Unit d) + { + return Has(a, SkillKeys.CritAlways, true, d); + } + + // ---------- 반격 ---------- + + /// 받는 반격 피해 배율(공격한 유닛 a 기준 · 조건 = 반격하는 상대) · 0 = 반격 안 받음. + public static float CounterTakenMult(Unit a, Unit counterer, bool note = false) + { + return PctProduct(a, SkillKeys.CounterTakenPct, note, counterer); + } + + /// 주는 반격 피해 배율 = 1 + 반격 피해 % 합 + 노장(잃은 HP 1 %당 p2). + public static float CounterDmgMult(Unit counterer, bool note = false) + { + float m = 1f + Sum(counterer, SkillKeys.CounterDmgPct, note); + float lost = counterer.MaxHp > 0 ? Mathf.Clamp01(1f - (float)counterer.Hp / counterer.MaxHp) * 100f : 0f; + if (lost > 0f) + foreach (var h in Get(counterer, SkillKeys.LowHpTaken)) + if (h.E.HasA2) m += h.E.A2 * lost; + return Mathf.Max(0f, m); + } + + // ---------- 밀기 · 튕김 ---------- public static float PushMult(Unit actor, bool note = false) { return Mathf.Max(0f, Product(actor, SkillKeys.PushMult, note)); } - /// 함진영: 밀린 적이 벽·유닛에 닿으면 추가 피해 비율(push_mult 행의 p2). - 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)); } - /// 밀리는 거리 배율(거구 −50 %). + /// 밀리는 거리 배율(거구 −100 % = 안 밀림). 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)); - } - - /// 관통 피해 배율 · 키 하나인 행의 p2 = 최소 관통 수(만인지적 2기 이상). - public static float PierceDmgMult(Unit actor, int pierced, bool note = false) + /// 서황 대부: 도끼가 도는 동안 자신을 공격한 유닛은 × p3 멀리 튕긴다. + public static float AxeBounce(Unit struck) { + if (struck == null) return 1f; 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); + foreach (var h in Get(struck, SkillKeys.AxeAura)) + if (h.E.HasA3) m = Mathf.Max(m, h.E.A3); + return m; } - // ---------- 견제 ---------- - - /// 견제 반경 = (기본 + 후계 등 패시브 가산 + 이번 대기의 액티브 가산) × 액티브 배율(청룡언월). - public static float OverwatchRadius(Unit spear, bool note = false) + /// 전위 고지악래: 지형 오브젝트 밀기 거리·그것으로 준 피해 배율. + public static float GimmickMult(Unit u) { - 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); + return u != null ? Mathf.Max(0f, Product(u, SkillKeys.GimmickMult, true)) : 1f; } } } diff --git a/Assets/Scripts/Runtime/Skills/SkillStatus.cs b/Assets/Scripts/Runtime/Skills/SkillStatus.cs new file mode 100644 index 00000000..3994a282 --- /dev/null +++ b/Assets/Scripts/Runtime/Skills/SkillStatus.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace TKS +{ + /// + /// 스킬 상태 이상 1기분(2026-11-10 #1112 · PD 기획 정의): + /// 혼란 = 그 유닛 차례 동안 이동 거리 × 배율(장료·육손·손권 「이동력 50 % 감소」) · 출혈 = 움직인 거리 비례 최대 HP 피해(서황 「이동 거리의 50 %에 비례」) · + /// 중독 = 그 진영 턴 시작 + 자기 행동 끝마다 최대 HP %(가후·축융 · 가후 패시브 = 중첩) · 화상(불태우기) = 그 진영 턴 시작마다 최대 HP %(서서). + /// 기절 = 기존 지형 기믹 「행동 불가」(GimmickLayer.Stun · 다음 자기 턴을 쉼)를 그대로 쓴다. + /// 턴 수 = 그 유닛 진영의 턴 기준(자기 턴 중에 걸리면 다음 턴부터 셈). + /// + public class UnitStatus + { + public int ConfuseTurns; + public float ConfuseMult = 1f; + public int BleedTurns; + public float BleedPct; + public int PoisonTurns; + public float PoisonPct; + public int PoisonStacks; + public int BurnTurns; + public float BurnPct; + public TextMesh Badge; + + public UnitStatus Copy() + { + return (UnitStatus)MemberwiseClone(); + } + + public bool Any + { + get { return ConfuseTurns > 0 || BleedTurns > 0 || PoisonTurns > 0 || BurnTurns > 0; } + } + + public void Clear() + { + ConfuseTurns = BleedTurns = PoisonTurns = BurnTurns = PoisonStacks = 0; + ConfuseMult = 1f; + } + } + + public static class SkillStatus + { + public static readonly Color TextColor = new Color(0.95f, 0.55f, 1f); + + static UnitStatus Of(Unit u) + { + var sys = SkillSystem.Current; + return sys != null ? sys.StatusOf(u) : null; + } + + public static bool Confused(Unit u) + { + var s = Of(u); + return s != null && s.ConfuseTurns > 0; + } + + public static float ConfuseMult(Unit u) + { + var s = Of(u); + return s != null && s.ConfuseTurns > 0 ? Mathf.Clamp(s.ConfuseMult, 0.05f, 1f) : 1f; + } + + public static bool Bleeding(Unit u) + { + var s = Of(u); + return s != null && s.BleedTurns > 0; + } + + public static bool Poisoned(Unit u) + { + var s = Of(u); + if (s != null && s.PoisonTurns > 0) return true; + var g = GimmickLayer.Current != null ? GimmickLayer.Current.StateOf(u) : null; + return g != null && g.PoisonTurns > 0; + } + + public static bool Stunned(Unit u) + { + var g = GimmickLayer.Current != null ? GimmickLayer.Current.StateOf(u) : null; + return g != null && (g.Stun > 0 || g.Skip); + } + + /// 상태 이상 아무것이나(스킬 상태 + 지형 기믹 중독·행동 불가). + public static bool HasAny(Unit u) + { + var s = Of(u); + return (s != null && s.Any) || Poisoned(u) || Stunned(u); + } + + public static string BadgeText(UnitStatus s) + { + if (s == null) return ""; + var parts = new List(); + if (s.ConfuseTurns > 0) parts.Add("혼란" + s.ConfuseTurns); + if (s.BleedTurns > 0) parts.Add("출혈" + s.BleedTurns); + if (s.PoisonTurns > 0) parts.Add("독" + s.PoisonTurns + (s.PoisonStacks > 1 ? "×" + s.PoisonStacks : "")); + if (s.BurnTurns > 0) parts.Add("화상" + s.BurnTurns); + return string.Join(" ", parts.ToArray()); + } + } +} diff --git a/Assets/Scripts/Runtime/Skills/SkillStatus.cs.meta b/Assets/Scripts/Runtime/Skills/SkillStatus.cs.meta new file mode 100644 index 00000000..a353ebe2 --- /dev/null +++ b/Assets/Scripts/Runtime/Skills/SkillStatus.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9893a7a221db90d46a1bf76b1e18e7f0 \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Skills/SkillSystem.cs b/Assets/Scripts/Runtime/Skills/SkillSystem.cs index c3b93368..197bec99 100644 --- a/Assets/Scripts/Runtime/Skills/SkillSystem.cs +++ b/Assets/Scripts/Runtime/Skills/SkillSystem.cs @@ -4,12 +4,12 @@ using UnityEngine; namespace TKS { - /// 유닛 1기의 스킬 상태(액티브 쿨·예약 · 패시브 · 행동 뒤에도 남는 효과). + /// 유닛 1기의 스킬 상태(액티브 쿨·예약 · 패시브 · 행동 뒤에도 남는 값). public class UnitSkills { public Unit Unit; public string HeroId = ""; - /// 무장(영웅·보스·엘리트 장수) — 참장·파로(dmg_vs_general_mult) 대상 판정. + /// 무장(영웅·보스·엘리트 장수) — 남성·여성 「영웅」 조건 판정. public bool IsGeneral; public SkillDef Active; @@ -20,22 +20,46 @@ namespace TKS public float PassiveMult = 1f; public List PassiveEffects; - /// 액티브를 쓸 수 있는 첫 아군 턴(전투 시작 = 1 · 사용 턴 + 쿨). + // ---------- 전투 중 바뀌는 값(가상 실행이 떠 두고 되돌림 · CopyState) ---------- + /// 액티브를 쓸 수 있는 첫 턴(전투 시작 = 1 · 사용 턴 + 쿨). public int ReadyTurn = 1; /// next_action 예약(다음 슬링샷 1회) · 행동 전 다시 탭 = 취소. public bool Reserved; /// 예약한 효과가 지금 걸려 있다(그 유닛의 행동 처리·조준 미리보기 동안만). public bool Armed; - - /// 추가 이동: 지금 것 다음에 더 남은 횟수 · 추가 이동에서 공격 가능. + /// 즉시형 액티브를 시전하는 중(그 효과·조건 armed 를 질의에 보임). + public bool Casting; + /// 보병 목표점 명중 추가 이동(유지 · PD 확인 대상). public int BonusMovesLeft; public bool BonusAttack; - /// 대기(견제) 중 액티브로 얻은 반경 배율·가산·무제한(그 적 턴까지). - public float OwMult = 1f; - public float OwAdd; - public bool OwUnlimited; - /// 전투 중 관통한 적 누적(장료 팔백 돌격). - public int PierceHits; + /// 남은 재행동 · 재행동의 슬링 상한(감녕·고순 남은 거리 · 0 = 없음) · 이번 턴 재행동 수(여몽 패시브). + public int ReactCredits; + public float ReactLimit; + public int ReactsThisTurn; + /// 여몽 백의도강 연쇄(남은 최대 · 피해·처치당 재행동). + public int ChainLeft; + public int ChainHitN; + public int ChainKillN; + public int Kills; + public bool KillReactUsed; + public int RevivesUsed; + public int NegateUsed; + public int AttackedStunCd; + /// 조인 수성: 남은 이동 비율(0~1 · 자기 턴 시작 = 1). + public float UnusedMove = 1f; + /// 우금 진중: 쌓인 추가 이동(배) · 이번 라운드에 행동했나. + public int Bank; + public bool MovedThisRound; + /// 황개 고육계: 창 동안 받은 피해 → 다음 턴 공격력(가산). + public float DmgTakenRound; + public float AtkFlat; + public int AtkFlatRounds; + /// 노숙: 쓰러진 자리 깃발(남은 턴 · 위치). + public int BannerLeft; + public Vector2 BannerPos; + /// 장비 만인지적: 행동 번호 · 대상별 (마지막 행동 번호, 연속 수). + public int ActionSeq; + public Dictionary Streak = new Dictionary(); public bool HasActive { @@ -46,6 +70,37 @@ namespace TKS { return Mathf.Max(0, ReadyTurn - turn); } + + public UnitSkills CopyState() + { + var c = (UnitSkills)MemberwiseClone(); + c.Streak = new Dictionary(Streak); + return c; + } + + public void RestoreState(UnitSkills c) + { + ReadyTurn = c.ReadyTurn; Reserved = c.Reserved; Armed = c.Armed; Casting = c.Casting; + BonusMovesLeft = c.BonusMovesLeft; BonusAttack = c.BonusAttack; + ReactCredits = c.ReactCredits; ReactLimit = c.ReactLimit; ReactsThisTurn = c.ReactsThisTurn; + ChainLeft = c.ChainLeft; ChainHitN = c.ChainHitN; ChainKillN = c.ChainKillN; + Kills = c.Kills; KillReactUsed = c.KillReactUsed; RevivesUsed = c.RevivesUsed; NegateUsed = c.NegateUsed; AttackedStunCd = c.AttackedStunCd; + UnusedMove = c.UnusedMove; Bank = c.Bank; MovedThisRound = c.MovedThisRound; + DmgTakenRound = c.DmgTakenRound; AtkFlat = c.AtkFlat; AtkFlatRounds = c.AtkFlatRounds; + BannerLeft = c.BannerLeft; BannerPos = c.BannerPos; ActionSeq = c.ActionSeq; + Streak = new Dictionary(c.Streak); + } + + public void ResetBattle() + { + ReadyTurn = 1; + Reserved = Armed = Casting = BonusAttack = KillReactUsed = MovedThisRound = false; + BonusMovesLeft = ReactCredits = ReactsThisTurn = ChainLeft = Kills = RevivesUsed = NegateUsed = AttackedStunCd = Bank = 0; + ReactLimit = DmgTakenRound = AtkFlat = 0f; + AtkFlatRounds = BannerLeft = ActionSeq = 0; + UnusedMove = 1f; + Streak.Clear(); + } } /// 스킬 효과 1건이 어느 유닛·스킬에서 왔는가(SkillMods 질의 결과). @@ -55,15 +110,68 @@ namespace TKS public SkillDef Def; public SkillEffect E; public bool Passive; + public SkillSystem.Timed Timed; + } + + /// 피해 종류 — 흡혈·명중 상태 이상·반격·호위가 어떤 피해에 붙는지. + public enum HitKind + { + Attack, + Counter, + Skill, + Dot, + Gimmick, + Indirect, + Guard, + Self + } + + /// 행동 1회의 스킬 맥락(돌진 거리 비율 · 맞힌 수 · 처치 · 지형). + public class ActionCtx + { + public Unit Actor; + public Vector2 Origin; + public float SlingRatio; + public int HitCount; + public bool FirstHitDone; + public int Kills; + public bool DamagedEnemy; + public bool HitTerrain; + public int Seq; + public readonly HashSet MalesHit = new HashSet(); + public readonly List ZombieFrom = new List(); + } + + /// 스킬이 만든 지대(주유 불꽃 · 육손 불 장벽 · 장로 독지대) — 원 또는 선분 두께. 닿는 상대 = 즉시 피해 + 상태 이상(행동마다 1번 · 상대 턴 시작에 안에 있으면 1번). + public class SkillZone + { + public UnitSkills Owner; + public Faction Faction; + public Vector2 A; + public Vector2 B; + public float Radius; + public int RoundsLeft; + public float Ratio; + public string StatusKey; + public SkillEffect Status; + public GameObject View; + public readonly HashSet Touched = new HashSet(); + + public float Distance(Vector2 p) + { + return Combat.SegmentPointDistance(A, B, p); + } } /// - /// 전투 1판의 스킬 처리(PD #1093 B · 시스템 기획 §5): 유닛별 스킬 상태 · 즉시형 액티브의 시한 효과(이번 아군 턴 / 이번 적 턴) · - /// 행동 전후 훅(예약 효과 장착·쿨 소모) · 피해 적용(호위 분담) · HUD 한 줄 기록. 수치 질의는 SkillMods. + /// 전투 1판의 스킬 처리(2026-11-10 #1112 · PD 엑셀 새 기획 · 효과 표 skill_effects.csv): 유닛별 스킬 상태 · 시한 효과(라운드) · 상태 이상 · 지대 · + /// 행동 전후 훅(예약 장착·쿨·재행동·출혈·중독) · 피해 적용(무적·무효화·호위·부활·흡혈·명중 상태 이상·처치 보상) · 회복 · 즉시형 액티브 · 턴 훅 · HUD 한 줄. 수치 질의 = SkillMods. /// public class SkillSystem { public static SkillSystem Current; + /// 테스트·자동 플레이: 행동 중 방향 탭(조조·하후돈)을 기다리지 않고 기본값(슬링 방향 · 제자리)으로. + public static bool AutoTap; public class Timed { @@ -71,12 +179,10 @@ namespace TKS public Faction Faction; public SkillDef Def; public SkillEffect E; - /// true = 이번 적 턴에만 걸림(다음 아군 턴 시작에 사라짐) · false = 이번 아군 턴(적 턴 시작에 사라짐). - public bool EnemyWindow; - /// 대상 고정(장판 일갈 = 밀어낸 적) · null = 키 규칙대로. + /// 대상 고정(range = 시전 순간 범위 안 같은 편 · 버프 받은 아군) · null = 대상 칸 규칙대로. public HashSet Targets; - /// 적 턴 창의 남은 적 턴 수(연환계 매혹 p2 · 기본 1 = 이번 적 턴만). - public int Turns = 1; + /// 남은 라운드(주인 진영 턴 시작마다 1 감소 · 0 = 끝). + public int Rounds = 1; } public struct LogLine @@ -92,12 +198,36 @@ namespace TKS public readonly List Log = new List(); readonly HashSet noted = new HashSet(); readonly int[] startCount = new int[2]; + readonly Dictionary statuses = new Dictionary(); + public readonly List Zones = new List(); + static readonly HashSet summons = new HashSet(); + + /// 지금 행동 중인 유닛의 맥락(BeginAction ~ EndAction). + public ActionCtx Ctx { get; private set; } + + // ---------- 행동 중 탭(조조 밀칠 방향 · 하후돈 복귀 지점) · 장합 변진 목표 ---------- + + public enum TapKind + { + None, + Teleport, + Aim, + Return + } + + public TapKind PendingKind { get; private set; } + public Unit PendingUnit { get; private set; } + public Vector2 PendingCenter { get; private set; } + public float PendingRadius { get; private set; } + Vector2? tapResult; /// 장합 변진: 목표 지점 탭 대기 중인 유닛. - public Unit PendingTeleport { get; private set; } + public Unit PendingTeleport + { + get { return PendingKind == TapKind.Teleport ? PendingUnit : null; } + } - // 스킬 이펙트(SkillVfx · 2026-11-09): 지금 행동 중인 유닛(슬링샷 · 예약 스킬 돌진 · 즉시형) · 이펙트 표 키(스킬 id · basic_<병종>) - // · 돌진에 붙은 이펙트 · 이번 행동에 hit 이펙트를 띄운 대상(대상마다 1번) + // 스킬 이펙트(SkillVfx · 2026-11-09): 지금 행동 중인 유닛 · 이펙트 표 키 · 돌진에 붙은 이펙트 · 이번 행동에 hit 이펙트를 띄운 대상 Unit vfxActor; readonly List vfxKeys = new List(); readonly List travelFx = new List(); @@ -110,11 +240,11 @@ namespace TKS if (!string.IsNullOrEmpty(skillKey)) vfxKeys.Add(skillKey); if (!string.IsNullOrEmpty(basicKey)) vfxKeys.Add(basicKey); if (!travel) return; - if (!string.IsNullOrEmpty(basicKey) && !string.IsNullOrEmpty(SkillVfx.TestTravelKey)) vfxKeys.Add(SkillVfx.TestTravelKey); // 스킬 테스트 씬 시험 키 - foreach (var k in vfxKeys) travelFx.Add(SkillVfx.Play(u, k, SkillVfx.EventTravel, dir)); // 돌진 동안(표 skill_vfx.csv travel) + if (!string.IsNullOrEmpty(basicKey) && !string.IsNullOrEmpty(SkillVfx.TestTravelKey)) vfxKeys.Add(SkillVfx.TestTravelKey); + foreach (var k in vfxKeys) travelFx.Add(SkillVfx.Play(u, k, SkillVfx.EventTravel, dir)); } - /// 슬링샷 직진(돌진) 구간 끝 — 돌진 이펙트(travel)만 멈춤(f_fade 면 그 자리에서 흐려짐) · hit 키는 행동 끝까지 유지(2026-11-10 PD 「이동이 종료될 때 자연스럽게 소멸」). + /// 슬링샷 직진(돌진) 구간 끝 — 돌진 이펙트(travel)만 멈춤. public void DashEnd(Unit u) { if (u == null || vfxActor != u) return; @@ -122,7 +252,7 @@ namespace TKS travelFx.Clear(); } - /// 같은 행동 안의 새 이동 구간(기병 복귀 · 위진소요 추가 관통) — 그 방향으로 돌진 이펙트를 다시 재생(PD 「기병은 복귀할 때 다시 이펙트가 재생」). + /// 같은 행동 안의 새 이동 구간(복귀 · 튕겨 다시 돌진) — 그 방향으로 돌진 이펙트를 다시 재생. public void DashBegin(Unit u, Vector2 dir) { if (u == null || vfxActor != u || dir.sqrMagnitude < 1e-6f) return; @@ -154,6 +284,11 @@ namespace TKS get { return entries; } } + BalanceConfig Cfg + { + get { return battle.Config; } + } + // ---------- 부착 ---------- public UnitSkills Of(Unit u) @@ -173,7 +308,7 @@ namespace TKS return s; } - /// 스킬 부착(액티브·패시브 id · 레벨 · ★5 패시브 배율 · 무장 여부). 최대 HP 패시브(hp_max_pct)는 바로 반영. + /// 스킬 부착(액티브·패시브 id · 레벨 · ★5 패시브 배율 · 무장 여부). 자기 최대 HP 패시브(hp_max_pct 대상 = 자신)는 바로 반영. public UnitSkills Attach(Unit u, string activeId, int activeLv, string passiveId, int passiveLv, float passiveMult, bool general) { if (u == null) return null; @@ -192,7 +327,7 @@ namespace TKS float hp = 0f; if (s.PassiveEffects != null) foreach (var e in s.PassiveEffects) - if (e.Key == SkillKeys.HpMaxPct && !(e.HasA2 && e.A2 >= 0.5f)) hp += e.P; // 아군 전원(p2 1 · 순욱)은 BeginBattle 에서 + if (e.Key == SkillKeys.HpMaxPct && e.Scope == SkillScope.Self) hp += e.P; // 같은 편 대상(진영)은 BeginBattle 에서 ScaleMaxHp(u, hp); return s; } @@ -205,19 +340,24 @@ namespace TKS if (u.View != null) u.View.Refresh(); } - /// 액티브 쿨 = 표 cooldown + 쿨 가산(귀재 = 자기 · p2 1 = 같은 편 전원) · 최소 1. + /// 액티브 쿨 = 표 cooldown · 최소 1. public int CooldownOf(UnitSkills s) { if (s == null || s.Active == null) return 1; - int add = s.Unit != null ? Mathf.RoundToInt(SkillMods.Sum(s.Unit, SkillKeys.CooldownAdd, true)) : 0; - return Mathf.Max(1, s.Active.Cooldown + add); + return Mathf.Max(1, s.Active.Cooldown); } - /// heroes.csv 영웅 id 로 부착(테스트·도구용). + /// heroes.csv 영웅 id 로 부착(테스트·도구용) · 진영·성별도 유닛에 기록. 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; + if (u != null) + { + u.Camp = h.faction ?? ""; + u.Female = h.Female; + u.HeroId = h.id; + } return Attach(u, h.skillActive, activeLv, h.skillPassive, passiveLv, passiveMult, general && !h.isSoldier); } @@ -233,10 +373,7 @@ namespace TKS return Attach(u, e.skillActive, e.activeLv, e.skillPassive, e.passiveLv, e.passiveMult, e.general); } - /// - /// 적 무장(보스·엘리트) = heroes.csv 에 같은 아트·이름 영웅이 있으면 그 패시브(Lv1) · withActive = 액티브도(적 AI 가 다음 행동형만 예약해 씀 · - /// 1부 보스만 = ai_weights.csv boss_active·active_max_chapter · PD #1098 확인 대상). - /// + /// 적 무장(보스·엘리트) = heroes.csv 같은 아트·이름 영웅의 패시브(Lv1) · withActive = 액티브도(적 AI · 1부 보스만 · PD #1098). public UnitSkills AttachEnemy(Unit u, BalanceConfig.EnemySpawn e, bool withActive = false) { if (u == null || e == null || !(e.isBoss || e.isGeneral)) return null; @@ -247,6 +384,9 @@ namespace TKS s.IsGeneral = true; return s; } + u.Camp = h.faction ?? ""; + u.Female = h.Female; + u.HeroId = h.id; return Attach(u, withActive ? h.skillActive : null, 1, h.skillPassive, 1, 1f, true); } @@ -263,47 +403,61 @@ namespace TKS return s != null && s.IsGeneral; } + public static bool IsSummon(Unit u) + { + return u != null && summons.Contains(u); + } + public void Clear() { map.Clear(); entries.Clear(); TimedEffects.Clear(); noted.Clear(); - PendingTeleport = null; + ClearTap(); startCount[0] = startCount[1] = 0; + ClearStatuses(); + ClearZones(); + summons.Clear(); + Ctx = null; VfxEnd(); } - /// 전투 시작(배치 뒤): 출전 인원 기록(패자 · team_atk_pct_full) · 쿨 = 바로 사용 가능 · 시한 효과 비움. + void ClearStatuses() + { + foreach (var kv in statuses) + if (kv.Value.Badge != null) Object.Destroy(kv.Value.Badge.gameObject); + statuses.Clear(); + } + + void ClearZones() + { + foreach (var z in Zones) + if (z.View != null) Object.Destroy(z.View); + Zones.Clear(); + } + + /// 전투 시작(배치 뒤): 출전 인원 기록 · 쿨 = 바로 사용 가능 · 시한 효과·상태 이상 비움 · 같은 편 최대 HP 패시브(진영 대상). public void BeginBattle() { Prune(); TimedEffects.Clear(); noted.Clear(); - PendingTeleport = null; + ClearTap(); + ClearStatuses(); + ClearZones(); + Ctx = null; startCount[0] = battle.AliveCount(Faction.Player); startCount[1] = battle.AliveCount(Faction.Enemy); - // 아군 전원 최대 HP(hp_max_pct p2 1 · 순욱) = 배치가 끝난 뒤 같은 편 전원에게 foreach (var u in battle.Units) { if (u == null || !u.IsAlive) continue; float team = 0f; foreach (var h in SkillMods.Get(u, SkillKeys.HpMaxPct)) - if (h.E.HasA2 && h.E.A2 >= 0.5f) team += h.E.P; + if (h.E.Scope != SkillScope.Self) team += h.E.P; ScaleMaxHp(u, team); } - 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; - } + foreach (var s in entries) s.ResetBattle(); } public int StartCount(Faction f) @@ -324,38 +478,304 @@ namespace TKS foreach (var k in dead) map.Remove(k); } + // ---------- 가상 실행(적 AI 예측)용 상태 사본 ---------- + + public class State + { + public readonly List> Entries = new List>(); + public List Timed; + public readonly Dictionary Statuses = new Dictionary(); + public List Zones; + public int LogCount; + } + + public State TakeState() + { + var st = new State { Timed = new List(TimedEffects), Zones = new List(Zones), LogCount = Log.Count }; + foreach (var e in entries) st.Entries.Add(new KeyValuePair(e, e.CopyState())); + foreach (var kv in statuses) st.Statuses[kv.Key] = kv.Value.Copy(); + return st; + } + + public void RestoreState(State st) + { + if (st == null) return; + foreach (var kv in st.Entries) kv.Key.RestoreState(kv.Value); + TimedEffects.Clear(); + TimedEffects.AddRange(st.Timed); + foreach (var z in Zones) + if (!st.Zones.Contains(z) && z.View != null) Object.Destroy(z.View); + Zones.Clear(); + Zones.AddRange(st.Zones); + foreach (var kv in statuses) + { + UnitStatus old; + if (st.Statuses.TryGetValue(kv.Key, out old)) continue; + kv.Value.Clear(); // 가상 실행 중 새로 생긴 상태 = 비움(배지는 그대로 둠) + } + foreach (var kv in st.Statuses) + { + UnitStatus cur; + if (!statuses.TryGetValue(kv.Key, out cur)) statuses[kv.Key] = kv.Value.Copy(); + else + { + var badge = cur.Badge; + var copy = kv.Value.Copy(); + copy.Badge = badge; + statuses[kv.Key] = copy; + } + } + if (Log.Count > st.LogCount) Log.RemoveRange(st.LogCount, Log.Count - st.LogCount); + Ctx = null; + } + // ---------- 턴 ---------- - /// 적 턴 시작: 이번 아군 턴 효과(동남풍 · 고육계)와 쓰지 않은 예약·추가 이동 정리. + /// 적 턴 시작(동기): 쓰지 않은 예약·추가 이동 정리. public void OnEnemyTurnStart() { - TimedEffects.RemoveAll(t => !t.EnemyWindow); - PendingTeleport = null; + ClearTap(); foreach (var s in entries) { s.Reserved = false; s.Armed = false; + s.Casting = false; s.BonusMovesLeft = 0; s.BonusAttack = false; } } - /// 아군 턴 시작: 이번 적 턴 효과(호위 · 철벽 · 간웅 · 일갈 · 안개)와 아군 견제 강화 정리. + /// 아군 턴 시작(동기) — 남은 것 없음(라운드 처리는 TurnBegin). public void OnPlayerTurnStart() { - TimedEffects.RemoveAll(t => t.EnemyWindow && --t.Turns <= 0); + } + + /// + /// side 진영 턴 시작(코루틴 · BattleManager 가 기믹 턴 시작 뒤에 부름): 상대 진영 턴 끝 처리(턴 종료 회복·정화 · 혼란·출혈 1 감소) → + /// side 주인의 시한 효과 1 감소(고육계 이월) · 턴마다 초기화 · 깃발·소환 정리 → side 유닛 중독·화상 → 재생·지속 회복·턴 시작 회복 → side 지대 1 감소 · 상대 지대 안 유닛. + /// + public IEnumerator TurnBegin(Faction side) + { + bool any = false; + Faction other = Other(side); + any |= TurnEnd(other); + + // 고육계 이월 공격력은 자기 턴 1번 — 다음 자기 턴 시작에 사라짐(새 이월은 아래 시한 효과 만료에서) foreach (var s in entries) { - if (s.Unit == null || s.Unit.Faction != Faction.Player) continue; - s.OwMult = 1f; - s.OwAdd = 0f; - s.OwUnlimited = false; + if (s.Unit == null || s.Unit.Faction != side || s.AtkFlatRounds <= 0) continue; + s.AtkFlat = 0f; + s.AtkFlatRounds = 0; + } + + for (int i = TimedEffects.Count - 1; i >= 0; i--) + { + var t = TimedEffects[i]; + if (t.Faction != side) continue; + if (--t.Rounds > 0) continue; + TimedEffects.RemoveAt(i); + if (t.E.Key == SkillKeys.DamageToAtk && t.Owner != null && t.Owner.DmgTakenRound > 0f) + { + t.Owner.AtkFlat = t.Owner.DmgTakenRound; // 고육계: 받은 피해 → 이번 턴 공격력 + t.Owner.AtkFlatRounds = 1; + t.Owner.DmgTakenRound = 0f; + if (t.Owner.Unit != null) Text(t.Owner.Unit, "공격 +" + Mathf.RoundToInt(t.Owner.AtkFlat), SkillFx.NameColor); + any = true; + } + } + + var gone = new List(); + foreach (var s in entries) + { + var u = s.Unit; + if (u == null || u.Faction != side) continue; + s.KillReactUsed = false; + s.ReactsThisTurn = 0; + s.ReactCredits = 0; + s.ReactLimit = 0f; + s.ChainLeft = 0; + if (s.AttackedStunCd > 0) s.AttackedStunCd--; + if (s.BannerLeft > 0 && !u.IsAlive && --s.BannerLeft == 0) Text(u, "깃발 소멸", SkillFx.CostColor); + if (u.IsAlive) + { + s.UnusedMove = 1f; + var bank = Eff(s, SkillKeys.MoveBank); + if (bank != null && !s.MovedThisRound) s.Bank = Mathf.Min(Mathf.Max(0, Mathf.RoundToInt(bank.P) - 1), s.Bank + 1); + s.MovedThisRound = false; + } + if (summons.Contains(u)) gone.Add(u); + } + foreach (var z in gone) + { + summons.Remove(z); + battle.RemoveUnit(z); + any = true; + } + + // 상태 이상(중독 · 화상) — side 유닛 + foreach (var u in battle.AliveUnits(side)) + { + var st = StatusOf(u); + if (st == null) continue; + if (st.PoisonTurns > 0) + { + PoisonTick(u, st); + st.PoisonTurns--; + if (st.PoisonTurns <= 0) st.PoisonStacks = 0; + any = true; + } + if (u.IsAlive && st.BurnTurns > 0) + { + int d = Mathf.Max(1, Mathf.RoundToInt(u.MaxHp * st.BurnPct)); + d = Hit(u, d, null, HitKind.Dot); + Text(u, "화상 −" + d, new Color(1f, 0.5f, 0.2f)); + st.BurnTurns--; + any = true; + } + RefreshBadge(u); + } + + // 회복: 재생(허저) · 지속 회복(방통) · 턴 시작 범위 회복(노숙 · 깃발) + foreach (var u in battle.AliveUnits(side)) + { + float regen = SkillMods.Sum(u, SkillKeys.RegenPct, true); + if (regen > 0f && Heal(u, Mathf.RoundToInt(u.MaxHp * regen), u) > 0) any = true; + } + foreach (var t in TimedEffects) + { + if (t.E.Key != SkillKeys.Hot || t.Faction != side || t.Owner == null || t.Owner.Unit == null) continue; + int amt = Mathf.RoundToInt(AtkOf(t.Owner.Unit) * t.E.P); + foreach (var u in battle.AliveUnits(side)) + if (t.Targets == null ? u == t.Owner.Unit : t.Targets.Contains(u)) + if (Heal(u, amt, t.Owner.Unit) > 0) any = true; + } + foreach (var s in entries) + { + var src = s.Unit; + if (src == null || src.Faction != side || s.PassiveEffects == null) continue; + if (!src.IsAlive && s.BannerLeft <= 0) continue; + var e = Find(s.PassiveEffects, SkillKeys.TurnStartHealRange); + if (e == null) continue; + Vector2 c = src.IsAlive ? src.Position : s.BannerPos; + foreach (var u in battle.AliveUnits(side)) + if (Vector2.Distance(c, u.Position) <= SkillMods.BaseReach(src) + u.Radius) + if (Heal(u, Mathf.RoundToInt(u.MaxHp * e.P), src) > 0) any = true; + NotePassive(s, s.Passive); + } + + // 지대: side 주인 지대 1 감소 · 상대(side) 유닛이 상대 지대 안에 있으면 닿음 1번 + for (int i = Zones.Count - 1; i >= 0; i--) + { + var z = Zones[i]; + if (z.Faction == side && --z.RoundsLeft <= 0) + { + if (z.View != null) Object.Destroy(z.View); + Zones.RemoveAt(i); + continue; + } + if (z.Faction == side) continue; + z.Touched.Clear(); + foreach (var u in battle.AliveUnits(side)) + if (z.Distance(u.Position) <= z.Radius + u.Radius * 0.5f) + { + ZoneTouch(z, u); + any = true; + } + } + + if (any) + { + battle.NotifyChanged(); + yield return Fx.Wait(0.35f); } } - public bool WindowActive(Timed t) + bool HasTimed(UnitSkills s, string key) { - return !t.EnemyWindow || battle.Phase == BattlePhase.EnemyTurn; + foreach (var t in TimedEffects) + if (t.Owner == s && t.E.Key == key) return true; + return false; + } + + /// side 진영 턴 끝(다음 진영 턴 시작에 부름): 턴 종료 회복·정화(소교·제갈량·방통) · 그 진영 유닛의 혼란·출혈 1 감소. + bool TurnEnd(Faction side) + { + bool any = false; + foreach (var t in TimedEffects) + { + if (t.E.Key != SkillKeys.TurnEndHeal || t.Faction != side || t.Owner == null || t.Owner.Unit == null || !t.Owner.Unit.IsAlive) continue; + var src = t.Owner.Unit; + int amt = Mathf.RoundToInt(AtkOf(src) * t.E.Get(1)); + foreach (var u in battle.AliveUnits(side)) + if (Vector2.Distance(src.Position, u.Position) <= t.E.P + u.Radius && Heal(u, amt, src) > 0) any = true; + } + foreach (var s in entries) + { + var src = s.Unit; + if (src == null || !src.IsAlive || src.Faction != side || s.PassiveEffects == null) continue; + var lowest = Find(s.PassiveEffects, SkillKeys.TurnEndHealLowest); + if (lowest != null) + { + Unit pick = null; + foreach (var u in battle.AliveUnits(side)) + { + if (Vector2.Distance(src.Position, u.Position) > lowest.P + u.Radius || u.Hp >= u.MaxHp) continue; + if (pick == null || (float)u.Hp / u.MaxHp < (float)pick.Hp / pick.MaxHp) pick = u; + } + if (pick != null && Heal(pick, Mathf.RoundToInt(AtkOf(src) * lowest.Get(1)), src) > 0) + { + NotePassive(s, s.Passive); + any = true; + } + } + var cl = Find(s.PassiveEffects, SkillKeys.TurnEndCleanse); + if (cl != null) + { + int n = Mathf.Max(1, Mathf.RoundToInt(cl.Get(1, 1f))); + foreach (var u in battle.AliveUnits(side)) + { + if (n <= 0) break; + if (Vector2.Distance(src.Position, u.Position) > cl.P + u.Radius || !SkillStatus.HasAny(u)) continue; + Cleanse(u); + n--; + NotePassive(s, s.Passive); + any = true; + } + } + } + foreach (var kv in statuses) + { + var u = kv.Key; + if (u == null || u.Faction != side) continue; + var st = kv.Value; + if (st.ConfuseTurns > 0) st.ConfuseTurns--; + if (st.BleedTurns > 0) st.BleedTurns--; + if (st.ConfuseTurns <= 0) st.ConfuseMult = 1f; + RefreshBadge(u); + } + return any; + } + + static Faction Other(Faction f) + { + return f == Faction.Player ? Faction.Enemy : Faction.Player; + } + + static SkillEffect Find(List l, string key) + { + if (l == null) return null; + foreach (var e in l) + if (e.Key == key) return e; + return null; + } + + /// 그 유닛 스킬의 효과(패시브 · 지금 장착/시전 중인 액티브) 첫 줄. + static SkillEffect Eff(UnitSkills s, string key) + { + if (s == null) return null; + var e = Find(s.PassiveEffects, key); + if (e != null) return e; + return (s.Armed || s.Casting) ? Find(s.ActiveEffects, key) : null; } // ---------- 액티브: 상태 · 탭 ---------- @@ -397,7 +817,7 @@ namespace TKS { case ActiveState.None: return null; case ActiveState.Targeting: - PendingTeleport = null; + ClearTap(); return s.Active.NameKo + " 취소"; case ActiveState.Reserved: s.Reserved = false; @@ -416,17 +836,74 @@ namespace TKS } if (s.Active.First(SkillKeys.Teleport) != null) { - PendingTeleport = u; - return s.Active.NameKo + " — 반경 " + TeleportRadius(s).ToString("0.#") + " 안 빈 곳을 탭(다른 곳 = 취소)"; + PendingKind = TapKind.Teleport; + PendingUnit = u; + PendingCenter = u.Position; + PendingRadius = TeleportRadius(s); + return s.Active.NameKo + " — 반경 " + PendingRadius.ToString("0.#") + " 안 빈 곳을 탭(다른 곳 = 취소)"; } battle.RunSkillRoutine(Instant(s)); return s.Active.NameKo + " 발동!"; } + void ClearTap() + { + PendingKind = TapKind.None; + PendingUnit = null; + tapResult = null; + } + + /// 행동 중 방향·지점 탭 대기(조조 난세의 간웅 · 하후돈 맹장) — 적·가상 실행·자동 = 기본값. + public IEnumerator WaitTap(Unit u, TapKind kind, Vector2 center, float radius, string message, Vector2 fallback, System.Action done) + { + if (Sim.Active || AutoTap || u == null || u.Faction != Faction.Player) + { + done(fallback); + yield break; + } + PendingKind = kind; + PendingUnit = u; + PendingCenter = center; + PendingRadius = radius; + tapResult = null; + battle.LastMessage = message; + battle.NotifyChanged(); + while (tapResult == null && PendingKind == kind && u != null && u.IsAlive) yield return null; + Vector2 r = tapResult ?? fallback; + ClearTap(); + done(r); + } + + /// 화면 탭 → 대기 중이면 처리하고 true(변진 = 유효면 이동 · 아니면 취소 / 행동 중 탭 = 결과 지점). + public bool TryTeleportTap(Vector2 w, out string message) + { + message = null; + if (PendingKind == TapKind.Aim || PendingKind == TapKind.Return) + { + Vector2 d = w - PendingCenter; + if (PendingRadius > 0f && d.magnitude > PendingRadius) w = PendingCenter + d.normalized * PendingRadius; + tapResult = w; + return true; + } + var u = PendingTeleport; + if (u == null) return false; + ClearTap(); + 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; + } + // ---------- 행동 훅(ActionResolver.Resolve) ---------- - /// 행동 시작: 예약한 next_action 효과를 장착(이동만인 추가 이동은 제외) · 발동 연출. - public void BeginAction(Unit u, bool moveOnly, ActionOutcome o, Vector2 dir = default(Vector2)) + /// 행동 시작: 예약한 next_action 효과를 장착(이동만인 추가 이동은 제외) · 발동 연출 · 행동 맥락(돌진 거리 비율). + public void BeginAction(Unit u, bool moveOnly, ActionOutcome o, Vector2 dir = default(Vector2), float slingDist = 0f) { noted.Clear(); var s = Of(u); @@ -436,30 +913,119 @@ namespace TKS s.Armed = true; o.SkillUsed = s.Active; Announce(s, s.Active, dir); + var rh = Find(s.ActiveEffects, SkillKeys.ReactionOnHit); + var rk = Find(s.ActiveEffects, SkillKeys.ReactionOnKill); + if (rh != null || rk != null) + { + s.ChainHitN = rh != null ? Mathf.RoundToInt(rh.P) : 0; + s.ChainKillN = rk != null ? Mathf.RoundToInt(rk.P) : 0; + s.ChainLeft = Mathf.Max(rh != null ? Mathf.RoundToInt(rh.Get(1, 1f)) : 0, rk != null ? Mathf.RoundToInt(rk.Get(1, 1f)) : 0); + } } - // 이펙트 = 예약 스킬(스킬 id 행) + 일반 슬링샷 공격(basic_<병종> 행 · 추가 이동 = 이동만이라 없음) + if (s != null) s.ActionSeq++; + Ctx = new ActionCtx { Actor = u, Origin = u.Position, Seq = s != null ? s.ActionSeq : 0 }; + float max = SkillMods.SlingMax(u); + Ctx.SlingRatio = max > 0.01f ? Mathf.Clamp01(slingDist / max) : 0f; + if (s != null) s.ReactLimit = 0f; // 재행동 슬링 상한(감녕·고순)은 조준에서 이미 썼음 — 이 행동이 새로 주면 다시 생김 VfxBegin(u, armed ? s.Active.Id : null, moveOnly ? null : SkillVfx.BasicKey(u.Type), dir, true); } - /// 행동 끝: 예약 소모 · 쿨(아군 턴 기준) · 견제 강화 이월. - public void EndAction(Unit u, ActionOutcome o) + /// + /// 행동 끝: 움직인 유닛 출혈 · 행동한 유닛 중독 · 지대 닿음 · 초선 다중 남성 기절 · 조인·이각·우금 패시브 · + /// (장착) 재행동 · 아군 재행동(우금) · 지형 재행동(맹획) · 지속 효과 시작 · 예약 소모 · 쿨 → 여몽 연쇄 → 장로 망령. + /// + public IEnumerator EndAction(Unit u, ActionOutcome o) { var s = Of(u); + var ctx = Ctx; if (vfxActor == u) VfxEnd(); - if (s == null || !s.Armed) return; - if (o.Overwatch) + + var moved = MovedDistances(u, o); + ApplyBleed(moved); + if (u.IsAlive) + { + var st = StatusOf(u); + if (st != null && st.PoisonTurns > 0) PoisonTick(u, st); // 중독 = 행동 종료 시점에도(가후·축융 정의) + } + ZoneSweep(u, o); + AxeSweep(u, o); + + if (ctx != null && ctx.Actor == u) + { + var mm = SkillMods.First(u, SkillKeys.MultiMaleStun); + if (mm != null && ctx.MalesHit.Count >= Mathf.RoundToInt(mm.P)) + foreach (var m in ctx.MalesHit) + if (m != null && m.IsAlive && Roll(mm.Get(1, 1f))) ApplyStun(m, Mathf.Max(1, Mathf.RoundToInt(mm.Get(2, 1f))), "매혹 기절"); + } + + if (s != null && u.IsAlive) + { + float pathLen = PathLength(o.Path); + s.MovedThisRound = true; + s.Bank = 0; + var heal = Find(s.PassiveEffects, SkillKeys.HealOnActionEnd); + if (heal != null && Heal(u, Mathf.RoundToInt(u.MaxHp * heal.P), u) > 0) NotePassive(s, s.Passive); + if (Find(s.PassiveEffects, SkillKeys.UnusedMoveDr) != null) + s.UnusedMove = Mathf.Clamp01(1f - pathLen / Mathf.Max(0.1f, SkillMods.SlingMax(u))); + Sacrifice(s); + } + + if (s != null && s.Armed) { 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; + if (e.Dur > 0) AddTimed(s, e, e.Scope == SkillScope.Range ? InRange(u) : null, e.Dur); + else if (e.Key == SkillKeys.ExtraAction) s.ReactCredits += Mathf.Max(0, Mathf.RoundToInt(e.P)); + else if (e.Key == SkillKeys.ReactionOnTerrain && ctx != null && ctx.HitTerrain) s.ReactCredits += Mathf.Max(0, Mathf.RoundToInt(e.P)); + else if (e.Key == SkillKeys.AllyHitReaction) + foreach (var p in o.Pushed) + if (p != null && p != u && p.IsAlive && p.Faction == u.Faction && p.ActedThisTurn) + { + p.GrantReAction(); + Text(p, "재행동", SkillFx.NameColor); + } } + s.Armed = false; + s.Reserved = false; + s.ReadyTurn = battle.TurnNumber + CooldownOf(s); + if (u.View != null) u.View.Refresh(); } - s.Armed = false; - s.Reserved = false; - s.ReadyTurn = battle.TurnNumber + CooldownOf(s); - if (u.View != null) u.View.Refresh(); + + // 여몽 백의도강 연쇄: 피해 = +재행동 · 처치 = +재행동(남은 최대까지) + if (s != null && s.ChainLeft > 0 && ctx != null && ctx.Actor == u) + { + int add = (ctx.DamagedEnemy ? s.ChainHitN : 0) + (ctx.Kills > 0 ? s.ChainKillN : 0); + add = Mathf.Min(add, s.ChainLeft); + s.ChainLeft -= add; + s.ReactCredits += add; + if (add == 0) s.ChainLeft = 0; + } + + if (ctx != null && ctx.ZombieFrom.Count > 0 && !Sim.Active) + foreach (var dead in ctx.ZombieFrom) + yield return ZombieRoutine(u, dead); + + Ctx = null; + battle.NotifyChanged(); + } + + /// 재행동을 1회 쓴다(남았으면 true · 이번 턴 재행동 수 +1). + public bool TakeReaction(Unit u) + { + var s = Of(u); + if (s == null || s.ReactCredits <= 0 || !u.IsAlive) return false; + s.ReactCredits--; + s.ReactsThisTurn++; + Text(u, "재행동!", SkillFx.NameColor); + AddLog(u.DisplayName + " 재행동"); + return true; + } + + /// 재행동의 슬링 상한(감녕·고순 남은 거리)은 그 재행동 1회로 끝. + public void ClearReactLimit(Unit u) + { + var s = Of(u); + if (s != null && s.ReactCredits <= 0) s.ReactLimit = 0f; } /// 조준 미리보기 동안만 예약 효과를 장착(사거리 · 사선 · 광역 표시). 반환 = 복원값. @@ -478,7 +1044,75 @@ namespace TKS if (s != null) s.Armed = prev; } - // ---------- 추가 이동 ---------- + static float PathLength(List path) + { + float l = 0f; + for (int i = 0; path != null && i < path.Count - 1; i++) l += Vector2.Distance(path[i], path[i + 1]); + return l; + } + + static Dictionary MovedDistances(Unit actor, ActionOutcome o) + { + var m = new Dictionary(); + if (actor != null) m[actor] = PathLength(o.Path); + foreach (var d in o.Displaced) + { + if (d.Unit == null) continue; + float v; + m.TryGetValue(d.Unit, out v); + m[d.Unit] = v + Vector2.Distance(d.From, d.To); + } + return m; + } + + /// 출혈 = 움직인 거리 / 자기 슬링 최대 × 최대 HP × 비율(서황 정의 「이동 거리의 50 %에 비례」 · PD 확인 대상). + public void ApplyBleed(Dictionary moved) + { + foreach (var kv in moved) + { + var u = kv.Key; + if (u == null || !u.IsAlive || kv.Value < 0.05f) continue; + var st = StatusOf(u); + if (st == null || st.BleedTurns <= 0) continue; + int d = Mathf.Max(1, Mathf.RoundToInt(u.MaxHp * st.BleedPct * kv.Value / Mathf.Max(0.1f, u.SlingMaxDistance))); + d = Hit(u, d, null, HitKind.Dot); + Text(u, "출혈 −" + d, new Color(0.9f, 0.15f, 0.2f)); + } + } + + void PoisonTick(Unit u, UnitStatus st) + { + int d = Mathf.Max(1, Mathf.RoundToInt(u.MaxHp * st.PoisonPct * Mathf.Max(1, st.PoisonStacks))); + d = Hit(u, d, null, HitKind.Dot); + Text(u, "중독 −" + d, new Color(0.7f, 0.4f, 1f)); + } + + /// 이각 패시브: 행동 끝 = HP p1 소모(10 % 이하면 소모 없이) → 인접(p3) 아군 공격 +p2 1턴 · 대상 없으면 발동 안 함. + void Sacrifice(UnitSkills s) + { + var e = Find(s.PassiveEffects, SkillKeys.ActionEndSacrifice); + var u = s.Unit; + if (e == null || u == null || !u.IsAlive) return; + var targets = new HashSet(); + foreach (var a in battle.AliveUnits(u.Faction)) + if (a != u && SkillMods.Within(u, a, e.Get(2, 1.5f))) targets.Add(a); + if (targets.Count == 0) return; + if (u.Hp > u.MaxHp * e.P) PayHp(u, e.P); + var buff = new SkillEffect { Key = SkillKeys.AtkPct, P = e.Get(1), Scope = SkillScope.Range, Dur = 1 }; + TimedEffects.Add(new Timed { Owner = s, Faction = u.Faction, Def = s.Passive, E = buff, Targets = targets, Rounds = 1 }); + foreach (var t in targets) Text(t, "공격 +" + Mathf.RoundToInt(buff.P * 100f) + "%", SkillFx.NameColor); + NotePassive(s, s.Passive); + } + + HashSet InRange(Unit owner) + { + var set = new HashSet(); + foreach (var a in battle.AliveUnits(owner.Faction)) + if (SkillMods.Within(owner, a, SkillMods.BaseReach(owner))) set.Add(a); + return set; + } + + // ---------- 추가 이동(보병 목표점 명중) ---------- public bool BonusCanAttack(Unit u) { @@ -486,7 +1120,7 @@ namespace TKS return s != null && s.BonusAttack; } - /// 행동 뒤 추가 이동을 (또) 주는가 — 처음 명중 = 추가 이동 n 회(인덕) · 추가 이동 뒤 = 남은 횟수. + /// 행동 뒤 추가 이동을 (또) 주는가 — 처음 명중 = 1회 · 추가 이동 뒤 = 남은 횟수. public bool NextBonus(Unit u, bool wasBonus, ActionOutcome o) { var s = Of(u); @@ -517,190 +1151,514 @@ namespace TKS s.BonusAttack = false; } - // ---------- 적 턴: 교전 반응 · 교전 범위 ---------- - - /// 이번 적 턴에 교전 반응이 없는가(장판 일갈 공포 · 태평요술 안개). + /// 이번 적 턴에 교전 반응이 없는가 — 기절은 지형 기믹 행동 불가(GimmickLayer.Skips)가 처리. 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; - if (t.E.Key == SkillKeys.Charm && t.Targets != null && t.Targets.Contains(e)) return true; // 연환계 매혹 - } return false; } - /// 상대 진영 패시브(신산)가 actingFaction 의 교전 범위에 더하는 값. + /// 교전 범위 가산(새 기획에 없음 = 0). 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; + return 0f; } - // ---------- 피해 적용(호위) ---------- + // ---------- 피해 적용 ---------- - /// 피해 적용 — 관평 호위(이번 적 턴 반경 안 같은 편 피해 일부를 관평이 대신) 반영. 반환 = d 가 실제 받은 피해. - public int Hit(Unit d, int dmg) + /// + /// 피해 적용(by = 준 유닛 · kind = 종류): 무적(시한) → 무효화(조범) → 호위(관평·강유 · 강유 = 반격) → 고육계 누적 → 부활(유비·맹획) → HP 감소 → + /// 준 쪽 보상(흡혈 · 사마의 흡수 · 명중 상태 이상 · 장비 연속 기절 · 하후은 첫 명중 추가 피해) → 맞은 쪽 반응(서황 도끼 · 간옹 기절 · 원술 혼란) → 처치 처리. 반환 = 실제 받은 피해. + /// + public int Hit(Unit d, int dmg, Unit by = null, HitKind kind = HitKind.Attack) { if (d == null || !d.IsAlive || dmg <= 0) return 0; + bool crit = (kind == HitKind.Attack || kind == HitKind.Skill || kind == HitKind.Counter) && Combat.LastCrit; if (vfxActor != null && d.Faction != vfxActor.Faction && vfxHit.Add(d)) { - foreach (var k in vfxKeys) SkillVfx.Play(vfxActor, k, SkillVfx.EventHit, Vector2.zero, d); // 행동 중 유닛의 명중 이펙트(대상마다 1번) - HitFeel.OnHit(vfxActor, d); // 타격감 화면 효과(흔들림 · 히트 스톱 · 표 hit_feel.csv) + foreach (var k in vfxKeys) SkillVfx.Play(vfxActor, k, SkillVfx.EventHit, Vector2.zero, d); + HitFeel.OnHit(vfxActor, d); } - foreach (var t in TimedEffects) + var ds = Of(d); + if (kind != HitKind.Self && Invulnerable(d)) { - 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; + Text(d, "무적", SkillFx.GuardColor); + return 0; } + if (kind != HitKind.Self && kind != HitKind.Dot && ds != null && ds.PassiveEffects != null) + { + var ng = Find(ds.PassiveEffects, SkillKeys.NegateHits); + if (ng != null && ds.NegateUsed < Mathf.RoundToInt(ng.P)) + { + ds.NegateUsed++; + Text(d, "피해 무효", SkillFx.GuardColor); + NotePassive(ds, ds.Passive); + return 0; + } + } + if (kind != HitKind.Guard && kind != HitKind.Dot && kind != HitKind.Self) + { + float gr; + SkillEffect ge; + var g = Guarder(d, out gr, out ge); + if (g != null) + { + int share = Mathf.Clamp(Mathf.RoundToInt(dmg * Mathf.Clamp01(ge.Get(1, 1f))), 0, dmg); + if (share > 0) + { + float dr = 1f + SkillMods.Sum(g, SkillKeys.GuardDr); + int took = Hit(g, Mathf.Max(1, Mathf.RoundToInt(share * Mathf.Max(0f, dr))), by, HitKind.Guard); + Fx.Popup(g.Position, took + " 호위", SkillFx.GuardColor); + NotePassive(Of(g), Of(g).Passive); + dmg -= share; + if (ge.Key == SkillKeys.GuardCounter && by != null && by.IsAlive && g.IsAlive && kind == HitKind.Attack) + { + int c = Combat.CounterDamage(Cfg, g, by, battle.Rng); + if (c > 0) + { + c = Hit(by, c, g, HitKind.Counter); + Fx.Popup(by.Position, c + " 호위 반격", new Color(1f, 0.45f, 0.45f)); + } + } + if (dmg <= 0) return 0; + } + } + } + if (ds != null && dmg > 0 && HasTimed(ds, SkillKeys.DamageToAtk)) ds.DmgTakenRound += Mathf.Min(dmg, d.Hp); + + int before = d.Hp; + if (dmg >= d.Hp && TryRevive(d, ds, before)) return before; d.TakeDamage(dmg); - return dmg; + int dealt = before - d.Hp; + if (by != null && by.Faction != d.Faction) OnDealt(by, d, dealt, kind, crit); + if (d.IsAlive && by != null && by.IsAlive && by.Faction != d.Faction && kind == HitKind.Attack) OnAttacked(d, by); + if (!d.IsAlive) OnDeath(d, by); + return dealt; } /// 스킬 시스템이 없어도 쓰는 피해 적용. - public static int Deal(Unit d, int dmg) + public static int Deal(Unit d, int dmg, Unit by = null, HitKind kind = HitKind.Attack) { - if (Current != null) return Current.Hit(d, dmg); + if (Current != null) return Current.Hit(d, dmg, by, kind); if (d == null) return 0; d.TakeDamage(dmg); return dmg; } - // ---------- 즉시형 액티브 ---------- - - /// 즉시 발동(행동 소모 없음): 효과마다 처리 → 쿨. - public IEnumerator Instant(UnitSkills s) + bool Invulnerable(Unit d) { - var u = s.Unit; - Announce(s, s.Active); - VfxBegin(u, s.Active.Id, null, Vector2.zero, false); // 즉시형 피해(장판 일갈 등)도 hit 이펙트 - s.ReadyTurn = battle.TurnNumber + CooldownOf(s); - foreach (var e in s.ActiveEffects) + foreach (var t in TimedEffects) { - switch (e.Key) + if (t.E.Key != SkillKeys.Invuln) continue; + if (t.Targets != null ? t.Targets.Contains(d) : (t.Owner != null && t.Owner.Unit == d)) return true; + } + return false; + } + + /// d 를 대신 맞아 줄 호위(관평 호위 · 강유 반격 범위 · 시한 범위 배율) — 가장 가까운 1기. + Unit Guarder(Unit d, out float radius, out SkillEffect effect) + { + radius = 0f; + effect = null; + Unit best = null; + float bestD = float.MaxValue; + foreach (var s in entries) + { + var g = s.Unit; + if (g == null || g == d || !g.IsAlive || g.Faction != d.Faction || s.PassiveEffects == null) continue; + var e = Find(s.PassiveEffects, SkillKeys.Guard) ?? Find(s.PassiveEffects, SkillKeys.GuardCounter); + if (e == null) continue; + float r = e.P * Mathf.Max(0f, SkillMods.Product(g, SkillKeys.GuardRadiusMult)); + float dist = Vector2.Distance(g.Position, d.Position); + if (dist > r + d.Radius || dist >= bestD) continue; + best = g; + bestD = dist; + radius = r; + effect = e; + } + return best; + } + + bool TryRevive(Unit d, UnitSkills ds, int before) + { + if (ds == null || ds.PassiveEffects == null) return false; + var e = Find(ds.PassiveEffects, SkillKeys.Revive); + if (e == null || ds.RevivesUsed >= Mathf.RoundToInt(e.Get(1, 1f))) return false; + float need = e.Get(2, 0f); + if (need > 0f && before < d.MaxHp * need) return false; // 맹획: 맞기 전 HP 가 조건 미만 = 즉사 + ds.RevivesUsed++; + d.Hp = Mathf.Max(1, Mathf.RoundToInt(d.MaxHp * Mathf.Clamp01(e.P))); + if (d.View != null) + { + d.View.OnHit(); + d.View.Refresh(); + } + Text(d, "부활!", SkillFx.HealColor); + NotePassive(ds, ds.Passive); + AddLog("패시브 · " + d.DisplayName + " 「" + ds.Passive.NameKo + "」 부활"); + return true; + } + + /// 준 쪽: 흡혈(조건 = 상대) · 반격 흡혈 · 사마의 흡수 · 행동 맥락(맞힌 수 · 남성 · 첫 명중) · 명중 상태 이상 · 장비 연속 피해 기절. + void OnDealt(Unit by, Unit d, int dealt, HitKind kind, bool crit) + { + if (dealt <= 0) return; + if (by.IsAlive && (kind == HitKind.Attack || kind == HitKind.Skill || kind == HitKind.Counter)) + { + float ls = 0f; + foreach (var h in SkillMods.Get(by, SkillKeys.Lifesteal, d)) { - case SkillKeys.AoePushFear: - yield return PushFear(s, e); - break; - case SkillKeys.CounterRatio: // 순욱(p3 1 = 아군 전원) — 반격은 적 턴에 일어나므로 이번 적 턴 - AddTimed(s, e, true, null); - break; - case SkillKeys.Charm: - CharmNearest(s, e); - break; - case SkillKeys.HealSelfPct: - Heal(u, Mathf.RoundToInt(u.MaxHp * Mathf.Max(0f, e.P))); - break; - case SkillKeys.CooldownAdd: - ShiftCooldowns(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; + ls += h.E.P + (h.E.HasA2 ? h.E.A2 * SkillMods.DeadAllies(by) : 0f); + if (h.Passive) NotePassive(h.Owner, h.Def); + } + if (kind == HitKind.Counter) ls += SkillMods.Sum(by, SkillKeys.CounterLifesteal, true); + if (ls > 0f) Heal(by, Mathf.RoundToInt(dealt * ls), by); + } + foreach (var s in entries) + { + var sm = s.Unit; + if (sm == null || !sm.IsAlive || sm.Faction == d.Faction || s.PassiveEffects == null) continue; + var dr = Find(s.PassiveEffects, SkillKeys.DrainAura); + if (dr == null || !SkillMods.Within(sm, d, SkillMods.BaseReach(sm))) continue; + if (Heal(sm, Mathf.RoundToInt(dealt * dr.P), sm) > 0) NotePassive(s, s.Passive); + } + var ctx = Ctx; + bool actor = ctx != null && ctx.Actor == by; + if (actor && (kind == HitKind.Attack || kind == HitKind.Skill)) + { + ctx.HitCount++; + ctx.DamagedEnemy = true; + if (!d.Female) ctx.MalesHit.Add(d); + } + if (kind == HitKind.Attack || kind == HitKind.Skill || kind == HitKind.Indirect || kind == HitKind.Gimmick) + ApplyHitStatuses(by, d, crit, kind); + if (actor && kind == HitKind.Attack && !ctx.FirstHitDone) + { + ctx.FirstHitDone = true; + float fb = SkillMods.Sum(by, SkillKeys.FirstHitBonus, true, d); + if (fb > 0f && d.IsAlive) + { + int extra = Mathf.Max(1, Mathf.RoundToInt(AtkOf(by, d) * fb)); + extra = Hit(d, extra, by, HitKind.Skill); + Fx.Popup(d.Position + new Vector2(0f, 0.3f), "+" + extra, SkillFx.NameColor); + } + } + if (actor && kind == HitKind.Attack) + { + var bs = Of(by); + var dh = Eff(bs, SkillKeys.DoubleHitStun); + if (dh != null && d.IsAlive) + { + Vector2Int v; + bs.Streak.TryGetValue(d, out v); + if (v.x != ctx.Seq) + { + v.y = v.x == ctx.Seq - 1 ? v.y + 1 : 1; + v.x = ctx.Seq; + if (v.y >= Mathf.RoundToInt(dh.P)) + { + ApplyStun(d, Mathf.Max(1, Mathf.RoundToInt(dh.Get(1, 1f))), "연속 피해 기절"); + v.y = 0; + NotePassive(bs, bs.Passive); + } + bs.Streak[d] = v; + } } } - yield return Fx.Wait(0.25f); - if (vfxActor == u) VfxEnd(); - battle.NotifyChanged(); } - void AddTimed(UnitSkills s, SkillEffect e, bool enemyWindow, HashSet targets, int turns = 1) + /// 명중 상태 이상(hit_*): 조건(치명타·상대) · 확률 p3 · 간접(밀린 아군·지형) = 고순(hit_stun p2 1)만. + public void ApplyHitStatuses(Unit by, Unit d, bool crit, HitKind kind) { - TimedEffects.Add(new Timed { Owner = s, Faction = s.Unit.Faction, Def = s.Active, E = e, EnemyWindow = enemyWindow, Targets = targets, Turns = Mathf.Max(1, turns) }); + if (by == null || d == null || !d.IsAlive || by.Faction == d.Faction) return; + bool indirect = kind == HitKind.Indirect || kind == HitKind.Gimmick; + foreach (var h in SkillMods.Get(by, SkillKeys.HitStun, d, crit)) + if ((!indirect || h.E.Get(1) >= 0.5f) && Roll(h.E.Get(2, 1f))) + { + ApplyStun(d, Mathf.Max(1, Mathf.RoundToInt(h.E.P)), "기절"); + NoteHit(h); + } + if (indirect) return; + foreach (var h in SkillMods.Get(by, SkillKeys.HitBleed, d, crit)) + if (Roll(h.E.Get(2, 1f))) + { + ApplyBleedStatus(d, Mathf.RoundToInt(h.E.P), h.E.Get(1, 0.5f)); + NoteHit(h); + } + foreach (var h in SkillMods.Get(by, SkillKeys.HitPoison, d, crit)) + if (Roll(h.E.Get(2, 1f))) + { + int max = 1; + foreach (var ps in SkillMods.Get(by, SkillKeys.PoisonStack)) max = Mathf.Max(max, Mathf.RoundToInt(ps.E.P)); + ApplyPoison(d, Mathf.RoundToInt(h.E.P), h.E.Get(1, 0.02f), max); + NoteHit(h); + } + foreach (var h in SkillMods.Get(by, SkillKeys.HitBurn, d, crit)) + if (Roll(h.E.Get(2, 1f))) + { + ApplyBurn(d, Mathf.RoundToInt(h.E.P), h.E.Get(1, 0.05f)); + NoteHit(h); + } + foreach (var h in SkillMods.Get(by, SkillKeys.HitConfuse, d, crit)) + if (Roll(h.E.Get(2, 1f))) + { + ApplyConfuse(d, Mathf.RoundToInt(h.E.P), h.E.Get(1, 0.5f)); + NoteHit(h); + } + foreach (var h in SkillMods.Get(by, SkillKeys.HitPush, d, crit)) + { + Vector2 dir = d.Position - by.Position; + if (dir.sqrMagnitude < 1e-6f) continue; + Shove(d, dir.normalized, h.E.P * by.SlingMaxDistance, by); + NoteHit(h); + } } - /// 연환계(charm): 반경 p1 안 가장 가까운 상대 무장 1기(없으면 병사) → p2 적 턴 동안 행동 불가 · 대상 없으면 쿨만 소모(PD 확인 대상). - void CharmNearest(UnitSkills s, SkillEffect e) + void NoteHit(SkillHit h) { - var u = s.Unit; - Faction other = u.Faction == Faction.Player ? Faction.Enemy : Faction.Player; - Unit pick = null; - bool pickGeneral = false; + if (h.Passive) NotePassive(h.Owner, h.Def); + } + + bool Roll(float chance) + { + if (chance >= 0.999f) return true; + if (chance <= 0f) return false; + if (Sim.Active) return chance >= 0.5f; // 가상 실행 = 결정적 + return battle.Rng.NextDouble() < chance; + } + + /// 맞은 쪽(근접·원거리 공격을 받음 · 살아 있음): 서황 도끼 반격 · 간옹 기절(재충전) · 원술 혼란(확률). + void OnAttacked(Unit d, Unit by) + { + var ds = Of(d); + foreach (var h in SkillMods.Get(d, SkillKeys.AxeAura)) + { + if (!by.IsAlive) break; + if (Vector2.Distance(d.Position, by.Position) > d.Radius + by.Radius + h.E.P) continue; + int ax = Mathf.Max(1, Mathf.RoundToInt(AtkOf(d, by) * h.E.Get(1, 0.8f))); + ax = Hit(by, ax, d, HitKind.Skill); + Fx.Popup(by.Position, ax + " 도끼", SkillFx.NameColor); + break; + } + if (ds == null || ds.PassiveEffects == null || !by.IsAlive) return; + var st = Find(ds.PassiveEffects, SkillKeys.OnAttackedStun); + if (st != null && ds.AttackedStunCd <= 0) + { + ApplyStun(by, Mathf.Max(1, Mathf.RoundToInt(st.P)), "말재주 기절"); + ds.AttackedStunCd = Mathf.Max(0, Mathf.RoundToInt(st.Get(1, 0f))); + NotePassive(ds, ds.Passive); + } + var cf = Find(ds.PassiveEffects, SkillKeys.OnAttackedConfuse); + if (cf != null && Roll(cf.Get(2, 1f))) + { + ApplyConfuse(by, Mathf.Max(1, Mathf.RoundToInt(cf.P)), cf.Get(1, 0.5f)); + NotePassive(ds, ds.Passive); + } + } + + /// 처치: 무신 누적 · 처치 회복(문추·공손찬·고육계) · 파로 재행동(턴당) · 곽가 귀재(범위 안 처치 = 쿨 −·회복) · 한중의 교주(망령) · 노숙 깃발. + void OnDeath(Unit d, Unit by) + { + var dsk = Of(d); + if (dsk != null && dsk.PassiveEffects != null) + { + var bn = Find(dsk.PassiveEffects, SkillKeys.DeathBanner); + if (bn != null) + { + dsk.BannerLeft = Mathf.Max(1, Mathf.RoundToInt(bn.P)); + dsk.BannerPos = d.Position; + Text(d, "깃발", SkillFx.NameColor); + NotePassive(dsk, dsk.Passive); + } + } + if (by == null || by.Faction == d.Faction || summons.Contains(d)) return; + var s = Of(by); + if (s != null) s.Kills++; + var ctx = Ctx; + if (ctx != null && ctx.Actor == by) ctx.Kills++; + if (!by.IsAlive) return; + float hk = SkillMods.Sum(by, SkillKeys.HealOnKill, true); + if (hk > 0f) Heal(by, Mathf.RoundToInt(by.MaxHp * hk), by); + if (s != null && !s.KillReactUsed && Eff(s, SkillKeys.KillReaction) != null) + { + s.KillReactUsed = true; + s.ReactCredits++; + NotePassive(s, s.Passive); + } + foreach (var g in entries) + { + var gu = g.Unit; + if (gu == null || !gu.IsAlive || gu.Faction != by.Faction || g.PassiveEffects == null) continue; + var rw = Find(g.PassiveEffects, SkillKeys.KillInRangeReward); + if (rw == null || Vector2.Distance(gu.Position, d.Position) > SkillMods.BaseReach(gu) + d.Radius) continue; + if (s != null && s.HasActive) s.ReadyTurn = Mathf.Max(battle.TurnNumber, s.ReadyTurn - Mathf.RoundToInt(rw.P)); + Heal(by, Mathf.RoundToInt(by.MaxHp * rw.Get(1, 0f)), gu); + NotePassive(g, g.Passive); + } + if (ctx != null && ctx.Actor == by && Eff(s, SkillKeys.Zombie) != null && !Sim.Active) ctx.ZombieFrom.Add(d); + } + + /// 장로 한중의 교주: 처치한 적이 체력 1 로 같은 편이 되어 가장 가까운 상대를 1번 공격(조작 불가) · 그 진영 다음 턴 시작에 사라짐. + IEnumerator ZombieRoutine(Unit owner, Unit dead) + { + if (owner == null || dead == null || battle.Board == null) yield break; + var s = Of(owner); + var e = Eff(s, SkillKeys.Zombie); + var z = battle.SpawnUnit(dead.Type, owner.Faction, dead.Position, "망령 " + dead.DisplayName, 1f, 1f, false, dead.ArtId); + z.Atk = dead.Atk; + z.Hp = Mathf.Max(1, Mathf.RoundToInt(e != null ? e.P : 1f)); + summons.Add(z); + if (z.View != null) z.View.Refresh(); + Text(z, "망령", SkillFx.NameColor); + if (s != null) NotePassive(s, s.Passive); + Unit foe = null; float best = float.MaxValue; - foreach (var t in battle.AliveUnits(other)) + foreach (var t in battle.AliveUnits(Other(owner.Faction))) { - float d = Vector2.Distance(u.Position, t.Position); - if (d > e.P + t.Radius) continue; - bool g = IsGeneral(t); - if (pick == null || (g && !pickGeneral) || (g == pickGeneral && d < best)) + float dd = Vector2.Distance(z.Position, t.Position); + if (dd < best) { - pick = t; - pickGeneral = g; - best = d; + best = dd; + foe = t; } } - Fx.Burst(u.Position, e.P, SkillFx.FearColor); - if (pick == null) - { - AddLog("연환계 대상 없음 — 쿨만 소모"); - return; - } - AddTimed(s, e, true, new HashSet { pick }, e.HasA2 ? Mathf.RoundToInt(e.A2) : 1); - Fx.Popup(pick.Position, "매혹", SkillFx.FearColor); + if (foe == null) yield break; + yield return Fx.Wait(0.2f); + var prevCtx = Ctx; + Vector2 aim = foe.Position - z.Position; + float max = SkillMods.SlingMax(z); + if (aim.magnitude > max) aim = aim.normalized * max; + var o = new ActionOutcome(); + yield return battle.Resolver.Resolve(z, battle.Board.ClampInside(z.Position + aim, z.Radius), false, o); + z.MarkActed(); + Ctx = prevCtx; } - static void Heal(Unit u, int amount) + // ---------- 상태 이상 ---------- + + public UnitStatus StatusOf(Unit u, bool create = false) { - if (u == null || !u.IsAlive || amount <= 0) return; + if (u == null) return null; + UnitStatus s; + if (statuses.TryGetValue(u, out s)) return s; + if (!create) return null; + s = new UnitStatus(); + statuses[u] = s; + return s; + } + + /// 자기 진영 턴 중에 걸리면 그 턴은 세지 않는다(다음 자기 턴부터). + int TurnsFor(Unit u, int turns) + { + bool ownTurn = (u.Faction == Faction.Player && (battle.Phase == BattlePhase.PlayerTurn || (battle.Phase == BattlePhase.Resolving && Ctx != null && Ctx.Actor != null && Ctx.Actor.Faction == Faction.Player))) + || (u.Faction == Faction.Enemy && battle.Phase == BattlePhase.EnemyTurn); + return turns + (ownTurn ? 1 : 0); + } + + public void ApplyStun(Unit u, int turns, string why) + { + if (u == null || !u.IsAlive || turns <= 0) return; + var g = GimmickLayer.Current; + if (g != null) g.Stun(u, turns, why); + } + + public void ApplyConfuse(Unit u, int turns, float mult) + { + if (u == null || !u.IsAlive || turns <= 0) return; + var s = StatusOf(u, true); + s.ConfuseTurns = Mathf.Max(s.ConfuseTurns, TurnsFor(u, turns)); + s.ConfuseMult = Mathf.Min(s.ConfuseTurns > 0 && s.ConfuseMult < 1f ? s.ConfuseMult : 1f, mult); + Text(u, "혼란", SkillStatus.TextColor); + RefreshBadge(u); + } + + public void ApplyBleedStatus(Unit u, int turns, float pct) + { + if (u == null || !u.IsAlive || turns <= 0) return; + var s = StatusOf(u, true); + s.BleedTurns = Mathf.Max(s.BleedTurns, TurnsFor(u, turns)); + s.BleedPct = Mathf.Max(s.BleedPct, pct); + Text(u, "출혈", new Color(0.9f, 0.15f, 0.2f)); + RefreshBadge(u); + } + + public void ApplyPoison(Unit u, int turns, float pct, int maxStacks) + { + if (u == null || !u.IsAlive || turns <= 0) return; + var s = StatusOf(u, true); + s.PoisonStacks = s.PoisonTurns > 0 ? Mathf.Min(Mathf.Max(1, maxStacks), s.PoisonStacks + 1) : 1; + s.PoisonTurns = Mathf.Max(s.PoisonTurns, turns); + s.PoisonPct = Mathf.Max(s.PoisonPct, pct); + Text(u, s.PoisonStacks > 1 ? "중독 ×" + s.PoisonStacks : "중독", new Color(0.7f, 0.4f, 1f)); + RefreshBadge(u); + } + + public void ApplyBurn(Unit u, int turns, float pct) + { + if (u == null || !u.IsAlive || turns <= 0) return; + var s = StatusOf(u, true); + s.BurnTurns = Mathf.Max(s.BurnTurns, turns); + s.BurnPct = Mathf.Max(s.BurnPct, pct); + Text(u, "화상", new Color(1f, 0.5f, 0.2f)); + RefreshBadge(u); + } + + /// 상태 이상 제거 = 스킬 상태(혼란·출혈·중독·화상) + 지형 기믹 중독·행동 불가. + public void Cleanse(Unit u) + { + if (u == null) return; + var s = StatusOf(u); + bool any = s != null && s.Any; + if (s != null) s.Clear(); + if (GimmickLayer.Current != null) any |= GimmickLayer.Current.Cleanse(u); + if (any) Text(u, "정화", SkillFx.HealColor); + RefreshBadge(u); + } + + void RefreshBadge(Unit u) + { + if (Sim.Active || u == null) return; + var s = StatusOf(u); + if (s == null) return; + string t = SkillStatus.BadgeText(s); + if (s.Badge == null) + { + if (t.Length == 0) return; + s.Badge = TextUtil.CreateWorldText(u.transform, t, 0.24f, SkillStatus.TextColor, 6); + s.Badge.gameObject.name = "SkillStatus"; + float top = u.View != null && u.View.HasArt ? -u.Radius * 0.55f + u.ArtHeight + 0.12f : u.Radius + 0.22f; + s.Badge.transform.localPosition = new Vector3(0f, top + 1.05f, 0f); + } + s.Badge.text = t; + s.Badge.gameObject.SetActive(t.Length > 0 && u.IsAlive); + } + + // ---------- 회복 ---------- + + /// 공격력(효과 반영) — 「공격력의 X %」 회복·추가 피해 기준. + public static float AtkOf(Unit u, Unit vs = null) + { + return u != null ? u.Atk * SkillMods.AttackMult(u, false, vs) : 0f; + } + + /// 회복(최대 HP 넘지 않음) · 간옹 패시브 = 회복한 대상 정화. 반환 = 실제 회복량. + public int Heal(Unit u, int amount, Unit healer = null) + { + if (u == null || !u.IsAlive || amount <= 0) return 0; int before = u.Hp; u.Hp = Mathf.Min(u.MaxHp, u.Hp + amount); - if (u.View != null) u.View.Refresh(); - if (u.Hp > before) Fx.Popup(u.Position, "+" + (u.Hp - before), SkillFx.HealColor); + if (u.View != null && !Sim.Active) u.View.Refresh(); + int got = u.Hp - before; + if (got > 0) Fx.Popup(u.Position, "+" + got, SkillFx.HealColor); + if (healer != null && healer != u && SkillMods.Has(healer, SkillKeys.HealCleanse, true) && SkillStatus.HasAny(u)) Cleanse(u); + return got; } - /// 금슬(cooldown_add 즉시 · p2 1 = 아군 전원): 같은 편 다른 유닛의 남은 액티브 쿨을 p1 만큼(이번 턴 아래로는 안 내려감 · 시전자 제외). - void ShiftCooldowns(UnitSkills s, SkillEffect e) - { - if (!(e.HasA2 && e.A2 >= 0.5f)) return; - int add = Mathf.RoundToInt(e.P); - foreach (var o in entries) - { - if (o == s || o.Unit == null || !o.Unit.IsAlive || o.Unit.Faction != s.Unit.Faction || !o.HasActive) continue; - o.ReadyTurn = Mathf.Max(battle.TurnNumber, o.ReadyTurn + add); - if (o.Unit.View != null) o.Unit.View.Refresh(); - } - } - - static void PayHp(Unit u, float ratio) + 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) + if (u.View != null && !Sim.Active) { u.View.OnHit(); u.View.Refresh(); @@ -708,44 +1666,362 @@ namespace TKS Fx.Popup(u.Position, "HP −" + cost, SkillFx.CostColor); } - /// 장판 일갈: 반경(p2) 안 상대 전원을 바깥으로 p1 밀고 이번 적 턴 교전 반응 없음. - IEnumerator PushFear(UnitSkills s, SkillEffect e) + // ---------- 밀기(스킬) ---------- + + /// 스킬 밀기(원소 치명타 밀침 · 즉시형 광역 밀기): 벽·유닛·장애물에서 멈춤 · 움직인 거리 = 출혈. + public float Shove(Unit t, Vector2 dir, float dist, Unit by) + { + if (t == null || !t.IsAlive || dist < 0.01f) return 0f; + float force = dist * SkillMods.KnockbackTakenMult(t, true); + float slide = Combat.FreeTravel(t, t.Position, dir, force, true, true, by); + if (slide <= 0.01f) return 0f; + Vector2 start = t.Position; + Vector2 end = battle.Board.ClampInside(start + dir * slide, t.Radius); + if (t.View != null && !Sim.Active) t.View.SetFacing(dir); + t.Position = end; + if (!Sim.Active) + { + t.transform.position = new Vector3(start.x, start.y, 0f); + Fx.Run(Fx.MoveAlong(t.transform, start, end, Cfg.moveSpeed, null)); + t.Position = end; // 논리 위치는 바로(콜라이더 질의) · 그림은 따라감 + } + float moved = Vector2.Distance(start, end); + ApplyBleed(new Dictionary { { t, moved } }); + return moved; + } + + // ---------- 지대(주유 불꽃 · 육손 불 장벽 · 장로 독지대) ---------- + + public SkillZone AddZone(UnitSkills owner, Vector2 a, Vector2 b, float radius, int rounds, float ratio, SkillEffect status, Color color) + { + var z = new SkillZone { Owner = owner, Faction = owner.Unit.Faction, A = a, B = b, Radius = Mathf.Max(0.2f, radius), RoundsLeft = Mathf.Max(1, rounds), Ratio = ratio, Status = status, StatusKey = status != null ? status.Key : null }; + if (!Sim.Active) + { + var root = new GameObject("SkillZone"); + Vector2 mid = (a + b) * 0.5f; + root.transform.position = new Vector3(mid.x, mid.y, 0f); + float len = Vector2.Distance(a, b); + if (len > 0.05f) + { + var bar = ProceduralSprites.NewSprite(root.transform, "Body", ProceduralSprites.Square, color, 1f, -12); + bar.transform.localScale = new Vector3(len, z.Radius * 2f, 1f); + Vector2 d = b - a; + bar.transform.rotation = Quaternion.Euler(0f, 0f, Mathf.Atan2(d.y, d.x) * Mathf.Rad2Deg); + } + foreach (var p in new[] { a, b }) + { + var cap = ProceduralSprites.NewSprite(root.transform, "Cap", ProceduralSprites.Circle, color, z.Radius * 2f, -12); + cap.transform.position = new Vector3(p.x, p.y, 0f); + } + z.View = root; + } + Zones.Add(z); + return z; + } + + /// 지대 닿음: 상대 = 주인 공격력 × 배율 피해 + 지대 상태 이상(행동마다 · 턴마다 1번). + void ZoneTouch(SkillZone z, Unit u) + { + if (u == null || !u.IsAlive || u.Faction == z.Faction || !z.Touched.Add(u)) return; + var src = z.Owner != null ? z.Owner.Unit : null; + if (z.Ratio > 0f && src != null) + { + int d = Mathf.Max(1, Mathf.RoundToInt(Combat.SkillDamage(Cfg, src, u, z.Ratio, battle.Rng))); + d = Hit(u, d, src, HitKind.Dot); + Text(u, (z.StatusKey == SkillKeys.HitPoison ? "독지대 −" : "불꽃 −") + d, new Color(1f, 0.55f, 0.25f)); + } + if (z.Status != null && u.IsAlive) + { + if (z.StatusKey == SkillKeys.HitBurn) ApplyBurn(u, Mathf.RoundToInt(z.Status.P), z.Status.Get(1, 0.05f)); + else if (z.StatusKey == SkillKeys.HitPoison) ApplyPoison(u, Mathf.RoundToInt(z.Status.P), z.Status.Get(1, 0.02f), 1); + } + } + + /// 이 행동에서 움직인 상대 유닛의 경로가 지대와 겹치면 닿음(행동 유닛 경로 + 밀림·넉백 구간). + void ZoneSweep(Unit actor, ActionOutcome o) + { + if (Zones.Count == 0) return; + foreach (var z in Zones) z.Touched.Clear(); + var segs = new List>(); + for (int i = 0; actor != null && i < o.Path.Count - 1; i++) segs.Add(new KeyValuePair(actor, new Vector4(o.Path[i].x, o.Path[i].y, o.Path[i + 1].x, o.Path[i + 1].y))); + foreach (var d in o.Displaced) + if (d.Unit != null) segs.Add(new KeyValuePair(d.Unit, new Vector4(d.From.x, d.From.y, d.To.x, d.To.y))); + foreach (var z in Zones) + foreach (var kv in segs) + { + var u = kv.Key; + if (u == null || !u.IsAlive || u.Faction == z.Faction) continue; + var s = kv.Value; + if (SegSegDistance(new Vector2(s.x, s.y), new Vector2(s.z, s.w), z.A, z.B) <= z.Radius + u.Radius * 0.5f) ZoneTouch(z, u); + } + } + + /// 서황 대부: 도끼가 도는 동안 자신의 이동 경로 도끼 반경(p1)에 닿은 상대 = 공격력 × p2 추가 피해(행동마다 1번). + void AxeSweep(Unit u, ActionOutcome o) + { + if (u == null || !u.IsAlive || o.Path.Count < 2) return; + var axe = SkillMods.First(u, SkillKeys.AxeAura); + if (axe == null) return; + foreach (var e in battle.AliveUnits(Other(u.Faction))) + { + bool near = false; + for (int i = 0; i < o.Path.Count - 1 && !near; i++) + near = Combat.SegmentPointDistance(o.Path[i], o.Path[i + 1], e.Position) <= u.Radius + e.Radius + axe.P; + if (!near) continue; + int d = Mathf.Max(1, Mathf.RoundToInt(AtkOf(u, e) * axe.Get(1, 0.8f))); + d = Hit(e, d, u, HitKind.Skill); + Fx.Popup(e.Position, d + " 도끼", SkillFx.NameColor); + } + } + + public static float SegSegDistance(Vector2 p1, Vector2 q1, Vector2 p2, Vector2 q2) + { + if (SegmentsIntersect(p1, q1, p2, q2)) return 0f; + return Mathf.Min(Mathf.Min(Combat.SegmentPointDistance(p1, q1, p2), Combat.SegmentPointDistance(p1, q1, q2)), + Mathf.Min(Combat.SegmentPointDistance(p2, q2, p1), Combat.SegmentPointDistance(p2, q2, q1))); + } + + static bool SegmentsIntersect(Vector2 a, Vector2 b, Vector2 c, Vector2 d) + { + float d1 = Cross(d - c, a - c), d2 = Cross(d - c, b - c), d3 = Cross(b - a, c - a), d4 = Cross(b - a, d - a); + return ((d1 > 0f && d2 < 0f) || (d1 < 0f && d2 > 0f)) && ((d3 > 0f && d4 < 0f) || (d3 < 0f && d4 > 0f)); + } + + static float Cross(Vector2 a, Vector2 b) + { + return a.x * b.y - a.y * b.x; + } + + // ---------- 즉시형 액티브 ---------- + + /// 즉시 발동(행동 소모 없음): 시전 중 효과 장착 → 처리기 → 지속 효과(dur) → 쿨. + public IEnumerator Instant(UnitSkills s) { var u = s.Unit; - float radius = e.HasA2 ? e.A2 : 0f; - var targets = new HashSet(); - int running = 0; - Faction other = u.Faction == Faction.Player ? Faction.Enemy : Faction.Player; - foreach (var t in battle.AliveUnits(other)) + Announce(s, s.Active); + VfxBegin(u, s.Active.Id, null, Vector2.zero, false); + s.ReadyTurn = battle.TurnNumber + CooldownOf(s); + s.Casting = true; + var prevCtx = Ctx; + Ctx = new ActionCtx { Actor = u, Origin = u.Position, Seq = ++s.ActionSeq }; + var moved = new Dictionary(); + foreach (var e in s.ActiveEffects) { - 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), () => + if (e.Dur > 0) { - unit.Position = end; - running--; - })); + AddTimed(s, e, e.Scope == SkillScope.Range ? InRange(u) : null, e.Dur); + continue; + } + switch (e.Key) + { + case SkillKeys.AoeStrike: + yield return AoeStrike(s, e, moved); + break; + case SkillKeys.AoeDrain: + AoeDrain(s, e); + break; + case SkillKeys.Volley: + yield return Volley(s, e, moved); + break; + case SkillKeys.CharmPull: + yield return CharmPull(s, e, moved); + break; + case SkillKeys.HealAtk: + { + int amt = Mathf.RoundToInt(AtkOf(u) * e.P); + foreach (var t in e.Scope == SkillScope.Self ? new HashSet { u } : InRange(u)) Heal(t, amt, u); + break; + } + case SkillKeys.Cleanse: + foreach (var t in e.Scope == SkillScope.Self ? new HashSet { u } : InRange(u)) Cleanse(t); + break; + case SkillKeys.HealSelfPct: + Heal(u, Mathf.RoundToInt(u.MaxHp * Mathf.Max(0f, e.P)), u); + break; + case SkillKeys.GrantReaction: + GrantReaction(u, Mathf.Max(1, Mathf.RoundToInt(e.P))); + break; + } } - 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); + ApplyBleed(moved); + s.Casting = false; + Ctx = prevCtx; + yield return Fx.Wait(0.25f); + if (vfxActor == u) VfxEnd(); battle.NotifyChanged(); } - static IEnumerator Track(IEnumerator inner, System.Action onDone) + /// 한 유닛에게 거는 시한 버프(미축 지원 · 순욱 보급 = 아군 명중 시 이동·공격 %). + public void AddBuff(UnitSkills owner, Unit target, string key, float value, int rounds) { - yield return inner; - onDone(); + if (owner == null || target == null || owner.Unit == null) return; + var e = new SkillEffect { Key = key, P = value, Scope = SkillScope.Range, Dur = rounds }; + TimedEffects.Add(new Timed { Owner = owner, Faction = owner.Unit.Faction, Def = owner.Passive ?? owner.Active, E = e, Targets = new HashSet { target }, Rounds = Mathf.Max(1, rounds) }); + Text(target, key == SkillKeys.AtkPct ? "공격 +" + Mathf.RoundToInt(value * 100f) + "%" : "이동 +" + Mathf.RoundToInt(value * 100f) + "%", SkillFx.NameColor); + } + + void AddTimed(UnitSkills s, SkillEffect e, HashSet targets, int rounds) + { + TimedEffects.Add(new Timed { Owner = s, Faction = s.Unit.Faction, Def = s.Active, E = e, Targets = targets, Rounds = Mathf.Max(1, rounds) }); + } + + /// 관우 청룡언월 · 장비 장판 일갈 · 능통: 반경 p1 안 상대 전원 = 공격력 × p2 피해(0 = 없음) → 바깥으로 p3 밀기 · 명중 상태 이상(피해 없어도). + IEnumerator AoeStrike(UnitSkills s, SkillEffect e, Dictionary moved) + { + var u = s.Unit; + float radius = e.P; + var targets = new List(); + foreach (var t in battle.AliveUnits(Other(u.Faction))) + if (Vector2.Distance(u.Position, t.Position) <= radius + t.Radius) targets.Add(t); + Fx.Burst(u.Position, radius, SkillFx.FearColor); + if (u.View != null && !Sim.Active) u.View.PlayAttack(); + foreach (var t in targets) + { + bool crit = false; + if (e.Get(1) > 0f) + { + int d = Combat.SkillDamage(Cfg, u, t, e.Get(1), battle.Rng); + crit = Combat.LastCrit; + d = Hit(t, d, u, HitKind.Skill); + Fx.Popup(t.Position, crit ? d + "!" : d.ToString(), Color.white); + } + else ApplyHitStatuses(u, t, false, HitKind.Skill); + } + yield return Fx.Wait(0.15f); + float push = e.Get(2); + if (push > 0.01f) + { + targets.Sort((a, b) => Vector2.Distance(u.Position, b.Position).CompareTo(Vector2.Distance(u.Position, a.Position))); // 바깥부터 + foreach (var t in targets) + { + if (!t.IsAlive) continue; + Vector2 dir = t.Position - u.Position; + dir = dir.sqrMagnitude > 1e-6f ? dir.normalized : Vector2.up; + float m = ShoveRaw(t, dir, push * SkillMods.KnockbackTakenMult(t, true), u); + if (m > 0f) AddMoved(moved, t, m); + } + yield return Fx.Wait(0.2f); + } + battle.NotifyChanged(); + } + + static void AddMoved(Dictionary m, Unit u, float d) + { + float v; + m.TryGetValue(u, out v); + m[u] = v + d; + } + + /// 밀기(출혈은 호출 쪽이 모아서) — 반환 = 움직인 거리. + float ShoveRaw(Unit t, Vector2 dir, float force, Unit by) + { + float slide = Combat.FreeTravel(t, t.Position, dir, force, true, true, by); + if (slide <= 0.01f) return 0f; + Vector2 start = t.Position; + Vector2 end = battle.Board.ClampInside(start + dir * slide, t.Radius); + if (t.View != null && !Sim.Active) t.View.SetFacing(dir); + t.Position = end; + if (!Sim.Active) + { + t.transform.position = new Vector3(start.x, start.y, 0f); + Fx.Run(Fx.MoveAlong(t.transform, start, end, Cfg.moveSpeed, null)); + t.Position = end; + } + return Vector2.Distance(start, end); + } + + /// 사마의: 행동 반경 안 상대 전원 = 공격력 × p1 → 준 피해 합 × p2 를 반경 안 다른 아군 각각 회복. + void AoeDrain(UnitSkills s, SkillEffect e) + { + var u = s.Unit; + float r = SkillMods.BaseReach(u); + int total = 0; + Fx.Burst(u.Position, r, Fx.SplashColor); + foreach (var t in battle.AliveUnits(Other(u.Faction))) + { + if (!SkillMods.Within(u, t, r)) continue; + int d = Hit(t, Combat.SkillDamage(Cfg, u, t, e.P, battle.Rng), u, HitKind.Skill); + Fx.Popup(t.Position, d.ToString(), Color.white); + total += d; + } + int amt = Mathf.RoundToInt(total * e.Get(1)); + foreach (var a in battle.AliveUnits(u.Faction)) + if (a != u && SkillMods.Within(u, a, r)) Heal(a, amt, u); + } + + /// 손상향 궁요희: 사거리 안 상대 전원에게 화살(공격력 × p1 · 치명타 판정) → 바깥으로 p2 밀기 · 치명타 = 출혈(조건 칸). + IEnumerator Volley(UnitSkills s, SkillEffect e, Dictionary moved) + { + var u = s.Unit; + float range = SkillMods.Range(u); + var targets = new List(); + foreach (var t in battle.AliveUnits(Other(u.Faction))) + if (Vector2.Distance(u.Position, t.Position) - t.Radius - u.Radius <= range) targets.Add(t); + if (u.View != null && !Sim.Active) u.View.PlayAttack(); + foreach (var t in targets) Fx.Run(Fx.Arrow(u.Position, t.Position)); + yield return Fx.Wait(0.2f); + foreach (var t in targets) + { + if (!t.IsAlive) continue; + int d = Combat.SkillDamage(Cfg, u, t, e.P, battle.Rng); + bool crit = Combat.LastCrit; + d = Hit(t, d, u, HitKind.Skill); + Fx.Popup(t.Position, crit ? d + "!" : d.ToString(), crit ? new Color(1f, 0.7f, 0.2f) : Color.white); + if (t.IsAlive && e.Get(1) > 0.01f) + { + Vector2 dir = (t.Position - u.Position).normalized; + float m = ShoveRaw(t, dir, e.Get(1) * SkillMods.KnockbackTakenMult(t, true), u); + if (m > 0f) AddMoved(moved, t, m); + } + } + yield return Fx.Wait(0.15f); + } + + /// 초선 미인계: 행동 반경 안 상대 남성(p2 1) = p1 턴 행동 불가 + 자신 쪽으로 끌려옴(유닛·장애물에 막힘). + IEnumerator CharmPull(UnitSkills s, SkillEffect e, Dictionary moved) + { + var u = s.Unit; + float r = SkillMods.BaseReach(u); + bool maleOnly = e.Get(1, 1f) >= 0.5f; + var targets = new List(); + foreach (var t in battle.AliveUnits(Other(u.Faction))) + if (SkillMods.Within(u, t, r) && (!maleOnly || !t.Female)) targets.Add(t); + targets.Sort((a, b) => Vector2.Distance(u.Position, a.Position).CompareTo(Vector2.Distance(u.Position, b.Position))); + Fx.Burst(u.Position, r, SkillFx.FearColor); + if (targets.Count == 0) AddLog(s.Active.NameKo + " 대상 없음 — 쿨만 소모"); + foreach (var t in targets) + { + ApplyStun(t, Mathf.Max(1, Mathf.RoundToInt(e.P)), "행동 불가"); + Vector2 to = u.Position - t.Position; + float want = to.magnitude - u.Radius - t.Radius - 0.05f; + if (want <= 0.05f) continue; + float m = ShoveRaw(t, to.normalized, want, null); + if (m > 0f) AddMoved(moved, t, m); + } + yield return Fx.Wait(0.25f); + } + + /// 곽가 십승십패: 이미 행동한 다른 아군 n 기에 재행동(없으면 임의의 다른 아군이 행동 뒤 재행동 1회). + void GrantReaction(Unit u, int n) + { + var acted = new List(); + var others = new List(); + foreach (var a in battle.AliveUnits(u.Faction)) + { + if (a == u) continue; + others.Add(a); + if (a.ActedThisTurn) acted.Add(a); + } + for (int i = 0; i < n; i++) + { + var pool = acted.Count > 0 ? acted : others; + if (pool.Count == 0) return; + var pick = pool[battle.Rng.Next(pool.Count)]; + pool.Remove(pick); + if (pick.ActedThisTurn) pick.GrantReAction(); + else Of(pick).ReactCredits++; + Text(pick, "재행동", SkillFx.NameColor); + } } // ---------- 변진(장합): 목표 지점 탭 ---------- @@ -768,32 +2044,14 @@ namespace TKS Physics2D.SyncTransforms(); foreach (var c in Physics2D.OverlapCircleAll(w, u.Radius)) { - if (c.GetComponent() != null) return false; + if (c.GetComponent() != null || GimmickLayer.BlocksMove(c)) return false; var o = c.GetComponent(); if (o != null && o != u && o.IsAlive) return false; } return true; } - /// 변진 대기 중 화면 탭 → 처리했으면 true(유효 = 이동 · 아니면 취소). - 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; - } - + /// 변진: 대상 무시하고 그 지점으로 → 이미 행동했으면 재행동(extra_action). public IEnumerator TeleportRoutine(UnitSkills s, Vector2 w) { var u = s.Unit; @@ -802,19 +2060,33 @@ namespace TKS 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; + if (s.Active.Has(SkillKeys.ExtraAction) && u.ActedThisTurn) + { + u.GrantReAction(); + Text(u, "재행동", SkillFx.NameColor); + } battle.NotifyChanged(); } // ---------- 연출 · HUD 기록 ---------- - /// 액티브 발동 = 유닛 위 스킬 이름 + 플래시 + HUD 한 줄. + /// 액티브 발동 = 유닛 위 스킬 이름 + 플래시 + HUD 한 줄 · 주유 패시브 = 시전 HP 소모. public void Announce(UnitSkills s, SkillDef def, Vector2 dir = default(Vector2)) { if (s == null || def == null) return; - if (def.IsActive) BattleEvents.Raise(BattleEvents.ActiveUses, s.Unit); // 보물 조건 효과 카운터(PD #1093 C) + if (def.IsActive) BattleEvents.Raise(BattleEvents.ActiveUses, s.Unit); if (s.Unit != null) SkillFx.Announce(s.Unit, def.NameKo); - if (s.Unit != null && def.IsActive) SkillVfx.Play(s.Unit, def.Id, SkillVfx.EventCast, dir); // 발동 이펙트(표 skill_vfx.csv · 행이 없으면 없음) + if (s.Unit != null && def.IsActive) SkillVfx.Play(s.Unit, def.Id, SkillVfx.EventCast, dir); AddLog("액티브 · " + (s.Unit != null ? s.Unit.DisplayName : "") + " 「" + def.NameKo + "」"); + if (def.IsActive && s.Unit != null) + { + var cost = Find(s.PassiveEffects, SkillKeys.SkillHpCost); + if (cost != null) + { + PayHp(s.Unit, cost.P); + NotePassive(s, s.Passive); + } + } } /// 패시브 발동 HUD 한 줄(같은 행동 안에서는 스킬마다 1번). @@ -828,10 +2100,17 @@ namespace TKS public void AddLog(string text) { + if (Sim.Active) return; Log.Add(new LogLine { Text = text, Time = Time.unscaledTime }); if (Log.Count > 8) Log.RemoveAt(0); } + static void Text(Unit u, string text, Color c) + { + if (Sim.Active || u == null) return; + Fx.Popup(u.Position + new Vector2(0f, 0.55f), text, c); + } + /// 최근 seconds 초 안의 기록(최신이 마지막). public List RecentLog(float seconds, int max) { diff --git a/Assets/Scripts/Runtime/SlingshotInput.cs b/Assets/Scripts/Runtime/SlingshotInput.cs index 23020ac5..703941c0 100644 --- a/Assets/Scripts/Runtime/SlingshotInput.cs +++ b/Assets/Scripts/Runtime/SlingshotInput.cs @@ -195,12 +195,13 @@ namespace TKS void RefreshTeleportArea() { var sys = battle.Skills; - var u = sys != null ? sys.PendingTeleport : null; - bool on = u != null && u.IsAlive; + var u = sys != null ? sys.PendingUnit : null; + bool on = u != null && u.IsAlive && sys.PendingRadius > 0f; // 변진 목표 · 맹장 복귀 지점(조조 방향 탭 = 반경 없음) if (on) { - float d = SkillSystem.TeleportRadius(sys.Of(u)) * 2f; - teleportArea.transform.position = new Vector3(u.Position.x, u.Position.y, 0f); + float d = sys.PendingRadius * 2f; + Vector2 c = sys.PendingKind == SkillSystem.TapKind.Teleport ? u.Position : sys.PendingCenter; + teleportArea.transform.position = new Vector3(c.x, c.y, 0f); teleportArea.transform.localScale = new Vector3(d, d, 1f); } if (teleportArea.gameObject.activeSelf != on) teleportArea.gameObject.SetActive(on); diff --git a/Assets/Scripts/Runtime/Unit.cs b/Assets/Scripts/Runtime/Unit.cs index ac9b9c75..37b6ffd6 100644 --- a/Assets/Scripts/Runtime/Unit.cs +++ b/Assets/Scripts/Runtime/Unit.cs @@ -93,6 +93,10 @@ namespace TKS /// 진영(heroes.csv faction = shu · wei · wu · other) — 출전 영웅만 · 그 밖(적 병사 등) = 빈칸(이펙트 표에서는 other 로 봄 · 2026-11-09 진영별 돌진 이펙트). public string Camp = ""; + /// 여성 영웅(heroes.csv gender f) — 스킬 조건 남성·여성(2026-11-10 #1112) · 그 밖(적 병사 포함) = 남성. + public bool Female; + /// heroes.csv 영웅 id(출전 영웅 · 영웅과 연결된 적 무장) · 그 밖 = 빈칸. + public string HeroId = ""; public void TakeDamage(int dmg) { @@ -135,6 +139,14 @@ namespace TKS if (View != null) View.Refresh(); } + /// 재행동(스킬) — 다시 일반 행동 1회(공격 가능). + public void GrantReAction() + { + ActedThisTurn = false; + BonusMoveAvailable = false; + if (View != null) View.Refresh(); + } + public void GrantBonusMove() { ActedThisTurn = false; diff --git a/Assets/Scripts/Runtime/UnitView.cs b/Assets/Scripts/Runtime/UnitView.cs index f3bc9269..108a877e 100644 --- a/Assets/Scripts/Runtime/UnitView.cs +++ b/Assets/Scripts/Runtime/UnitView.cs @@ -251,7 +251,7 @@ namespace TKS bool ow = unit.Overwatch && unit.Type == UnitType.Spear && unit.IsAlive; if (ow) { - float d = SkillMods.OverwatchRadius(unit) * 2f; // 후계·백마의 창·청룡언월 반영(PD #1093 B) + float d = unit.AttackRange * 2f; // 옛 견제 반경(대기 유닛 없음 · 2026-11-10 #1112 스킬 확장 제거) overwatchArea.transform.localScale = new Vector3(d, d, 1f); } if (overwatchArea.gameObject.activeSelf != ow) overwatchArea.gameObject.SetActive(ow); diff --git a/Assets/Tests/PlayMode/SkillTests.cs b/Assets/Tests/PlayMode/SkillTests.cs index e09ecfa3..5277dade 100644 --- a/Assets/Tests/PlayMode/SkillTests.cs +++ b/Assets/Tests/PlayMode/SkillTests.cs @@ -1,7 +1,5 @@ -using System; using System.Collections; using System.Collections.Generic; -using System.IO; using NUnit.Framework; using TKS.OutGame; using UnityEngine; @@ -11,15 +9,13 @@ using UnityEngine.TestTools; namespace TKS.Tests { /// - /// 스킬 시스템(PD #1093 B · 시스템 기획 §5·§3-5) — effect_key 51종 처리기(수치·조건·레벨 계수) · 쿨·예약·취소 · 즉시형 · ★3/★5 · 적 무장 패시브 · 스킬 레벨업·스킬서. - /// 기대값은 전부 skills.csv(SkillCatalog)·BalanceConfig·표에서 계산한다(C45 · 테스트에 수치 상수 없음 · 스킬 id 만 지정). - /// 결정 피해 비교 = 같은 시드의 난수로 Combat.Damage 를 부른다. + /// 스킬(2026-11-10 #1112 · PD 엑셀 「장수 목록」 새 기획 · 효과 표 skill_effects.csv) — 효과 묶음별 동작: + /// 표 무결 · 진영 버프·부활 · 즉시 광역(피해·밀기·출혈 / 피해 없는 기절) · 재행동·반격 면역 · 무조건 치명타·흡혈 · 중독 중첩 · 혼란 · 출혈 · 무적 · + /// 호위 · 기병 복귀 · 거대 화살 · 화염구(화상) · 회복·정화 · 위치 교환 · 아군 통과·탭 밀치기 · 보병 관통 · 가상 실행 복원. + /// 기대값은 효과 표 수치(SkillCatalog · Eff)와 자산 값에서 읽는다(C45 · 테스트에 수치 상수 없음 · 스킬 id 만 지정). /// public class SkillTests { - const int Seed = 7; - static readonly DateTime T0 = new DateTime(2026, 11, 5, 3, 0, 0, DateTimeKind.Utc); - BattleManager bm; BalanceConfig cfg; @@ -37,12 +33,14 @@ namespace TKS.Tests if (wm != null && wm.HasData) wm.EnterStage(0, 0); yield return null; cfg = bm.Config; + SkillSystem.AutoTap = true; Time.timeScale = 20f; } [UnityTearDown] public IEnumerator TearDown() { + SkillSystem.AutoTap = false; Time.timeScale = 1f; yield return null; } @@ -58,1197 +56,408 @@ namespace TKS.Tests // ---------- 도우미 ---------- - static OutGameData Db - { - get { return OutGameData.Default; } - } - - static SkillEffect Eff(string skillId, string key, int lv = 1, float star = 1f) + static SkillEffect Eff(string skillId, string key) { var d = SkillCatalog.Get(skillId); Assert.IsNotNull(d, skillId); - foreach (var e in d.ScaledEffects(lv, star)) + foreach (var e in d.ScaledEffects(1, 1f)) if (e.Key == key) return e; Assert.Fail(skillId + " 에 " + key + " 없음"); return null; } - static int Cd(string skillId) + Unit Spawn(UnitType t, Faction f, float x, float y, string name) { - return SkillCatalog.Get(skillId).Cooldown; + return bm.SpawnUnit(t, f, new Vector2(x, y), name, 1f, 1f, false); } - Unit Spawn(UnitType t, Faction f, float x, float y, string name, bool boss = false) + UnitSkills Give(Unit u, string heroId) { - return bm.SpawnUnit(t, f, new Vector2(x, y), name, 1f, 1f, boss); - } - - UnitSkills Give(Unit u, string heroId, int alv = 1, int plv = 1, float pm = 1f) - { - var s = bm.Skills.AttachHero(u, heroId, alv, plv, pm); + var s = bm.Skills.AttachHero(u, heroId); Assert.IsNotNull(s, heroId); return s; } - void Reserve(Unit u) + void Begin() + { + bm.Skills.BeginBattle(); + } + + IEnumerator Act(Unit u, Vector2 target, ActionOutcome o) + { + yield return bm.ResolveForTest(u, target, false, o); + } + + IEnumerator ActArmed(Unit u, Vector2 target, ActionOutcome o) { bm.Skills.Of(u).Reserved = true; + yield return bm.ResolveForTest(u, target, false, o); } - void Arm(Unit u, bool on) - { - bm.Skills.Of(u).Armed = on; - } - - IEnumerator Act(Unit u, Vector2 target, ActionOutcome o, bool moveOnly = false) - { - yield return bm.ResolveForTest(u, target, moveOnly, o); - } - - static float CastContact(Unit mover, Unit other) - { - return mover.Radius * Combat.CastRadiusScale + other.Radius; - } - - static float Gap(Unit a, Unit b) - { - return Vector2.Distance(a.Position, b.Position) - a.Radius - b.Radius; - } - - int Dmg(Unit a, Unit d, bool melee = true, float scale = 1f) - { - return Combat.Damage(cfg, a, d, false, new System.Random(Seed), melee, scale); - } - - /// 피해 공식 기대값(같은 시드 난수) = (공 × 상성 − 방 × 계수) × 배율 × 분산. - int Expect(float atk, float matchup, float def, float scale) - { - var r = new System.Random(Seed); - float v = 1f + ((float)r.NextDouble() * 2f - 1f) * cfg.damageVariance; - return Mathf.Max(1, Mathf.RoundToInt((atk * matchup - def * cfg.defenseFactor) * scale * v)); - } - - IEnumerator WaitPhase(BattlePhase p, float sec = 10f) - { - float t0 = Time.realtimeSinceStartup; - while (bm.Phase != p && Time.realtimeSinceStartup - t0 < sec) yield return null; - Assert.AreEqual(p, bm.Phase); - } - - // ---------- 표 · 임포터 · 레벨 계수 ---------- - - [Test] - public void Import_EffectKeys_All51_Handled_PColumns_Parsed() - { - var keys = new HashSet(); - foreach (var s in Db.Skills.skills) - foreach (var k in s.EffectKeys) - { - Assert.IsTrue(SkillKeys.Known(k), s.id + " " + k); - keys.Add(k); - } - // 영웅 56명 확장(PD #1096): 표 키 ⊆ 처리기 55종 · 표에서 안 쓰는 처리기 = 대장군(기령 제외)뿐 - var unused = new List(); - foreach (var k in SkillKeys.All.Keys) - if (!keys.Contains(k)) unused.Add(k); - CollectionAssert.AreEquivalent(new[] { SkillKeys.PushedAllyAttackOnPush }, unused, "표에서 안 쓰는 처리기"); - foreach (var k in new[] { SkillKeys.TeamDmgPctTurn, SkillKeys.CooldownAdd, SkillKeys.Charm, SkillKeys.HealSelfPct }) Assert.IsTrue(keys.Contains(k), "확장 키 " + k); - - // 자산 = CSV(effect_key · p1~p3 · lv_scale · needs_impl) - var dir = Path.Combine(Application.dataPath, OutGameTables.Folder); - var texts = new string[OutGameTables.Files.Length]; - for (int i = 0; i < texts.Length; i++) texts[i] = File.ReadAllText(Path.Combine(dir, OutGameTables.Files[i]), System.Text.Encoding.UTF8); - var r = OutGameTables.Parse(texts); - try - { - Assert.AreEqual(0, r.errors.Count, string.Join("\n", r.errors.ToArray())); - foreach (var s in r.data.Skills.skills) - { - var a = Db.Skills.Get(s.id); - Assert.AreEqual(s.effectKey, a.effectKey, s.id); - Assert.AreEqual(s.pMask, a.pMask, s.id); - Assert.AreEqual(s.p1, a.p1, 1e-5f, s.id); - Assert.AreEqual(s.p2, a.p2, 1e-5f, s.id); - Assert.AreEqual(s.p3, a.p3, 1e-5f, s.id); - Assert.AreEqual(s.lvScale, a.lvScale, 1e-5f, s.id); - Assert.AreEqual(s.needsImpl, a.needsImpl, s.id); - var d = SkillCatalog.Get(s.id); - Assert.AreEqual(s.EffectKeys.Length, d.Effects.Count, s.id); - if (s.EffectKeys.Length > 1) - for (int i = 0; i < d.Effects.Count; i++) Assert.AreEqual(s.P(i), d.Effects[i].P, 1e-5f, s.id + " p" + (i + 1) + " = 키 순서"); - } - } - finally - { - OutGameTables.Destroy(r); - } - - // 처리기 없는 키 = 임포트 오류 - int si = Array.IndexOf(OutGameTables.Files, "skills.csv"); - var broken = (string[])texts.Clone(); - broken[si] = broken[si].Replace("counter_ratio", "counter_ratio_x"); - var rb = OutGameTables.Parse(broken); - try - { - Assert.IsTrue(rb.errors.Exists(e => e.Contains("처리기 없음")), "모르는 effect_key → 오류"); - } - finally - { - OutGameTables.Destroy(rb); - } - } - - [Test] - public void LevelScale_Value_Mult_Count_Fixed_AndStar5Passive() - { - var cr = SkillCatalog.Get("guanyu_p"); - float raw = cr.Effects[0].P; - Assert.AreEqual(raw * (1f + cr.LvScale * 2f), Eff("guanyu_p", SkillKeys.CounterRatio, 3).P, 1e-4f, "값 × (1 + lv_scale·(lv−1))"); - float star = SkillRules.PassiveMult(Db, Db.Evolve.MaxStar); - Assert.Greater(star, 1f, "★5 패시브 배율(evolve.csv)"); - Assert.AreEqual(raw * star, Eff("guanyu_p", SkillKeys.CounterRatio, 1, star).P, 1e-4f, "★5 = 패시브 ×배율"); - - var pm = SkillCatalog.Get("zhoucang_a"); - float praw = pm.Effects[0].P; - Assert.AreEqual(1f + (praw - 1f) * (1f + pm.LvScale * 4f), Eff("zhoucang_a", SkillKeys.PushMult, 5).P, 1e-4f, "배율 = 1 을 넘는 몫만 키움"); - Assert.AreEqual(praw, Eff("zhoucang_a", SkillKeys.PushMult, 1, star).P, 1e-4f, "액티브에는 ★5 패시브 배율 없음"); - - var em = SkillCatalog.Get("liubei_a"); - float craw = em.First(SkillKeys.ExtraMoveCount).P; - Assert.AreEqual(Mathf.Floor(craw * (1f + em.LvScale * 4f) + 0.5f), Eff("liubei_a", SkillKeys.ExtraMoveCount, 5).P, 1e-4f, "횟수 = 반올림"); - Assert.AreEqual(SkillCatalog.Get("guanyu_a").First(SkillKeys.OverwatchUnlimited).P, Eff("guanyu_a", SkillKeys.OverwatchUnlimited, 5).P, 1e-4f, "켜기 값은 그대로"); - - var lb = SkillCatalog.Get("lvbu_p").Effects[0]; - var lb3 = Eff("lvbu_p", SkillKeys.SlingDistPct, 3); - Assert.AreEqual(lb.A2 * (1f + SkillCatalog.Get("lvbu_p").LvScale * 2f), lb3.A2, 1e-4f, "절대 가산(p2)도 레벨 계수"); - var hg = SkillCatalog.Get("huanggai_a").Effects[0]; - Assert.AreEqual(hg.A2, Eff("huanggai_a", SkillKeys.HpCostAtk, 5).A2, 1e-4f, "HP 소모(p2)는 커지지 않음"); - } - - // ---------- 보병 ---------- + // ---------- 표 ---------- [UnityTest] - public IEnumerator Infantry_Rende_NoSelfBounce_ExtraMoves_CooldownFromTable() + public IEnumerator Data_EveryHeroSkillHasEffects() { yield return Fresh(); - var inf = Spawn(UnitType.Infantry, Faction.Player, 0f, -3f, "유비"); - Give(inf, "liubei"); - Reserve(inf); - var foe = Spawn(UnitType.Spear, Faction.Enemy, 0f, 0f, "적창병"); - float contactY = -3f + (3f - CastContact(inf, foe)); - var o = new ActionOutcome(); - yield return Act(inf, foe.Position, o); - Assert.AreEqual("liubei_a", o.SkillUsed != null ? o.SkillUsed.Id : null, "예약 = 다음 행동에 발동"); - Assert.AreEqual(contactY, inf.Position.y, 0.1f, "self_bounce_mult 0 = 자신은 튕기지 않음"); - Assert.IsTrue(o.BonusMove); - int n = Mathf.RoundToInt(Eff("liubei_a", SkillKeys.ExtraMoveCount).P); - Assert.AreEqual(n, o.BonusMoves, "extra_move_count = 추가 이동 n 회"); - var s = bm.Skills.Of(inf); - Assert.IsFalse(s.Reserved); - Assert.AreEqual(bm.TurnNumber + Cd("liubei_a"), s.ReadyTurn, "쿨 = 사용 턴 + cooldown"); - // 추가 이동 n 회: 첫 명중 → 1 회 부여 + 남은 n−1 - Assert.IsTrue(bm.Skills.NextBonus(inf, false, o)); - for (int i = 1; i < n; i++) Assert.IsTrue(bm.Skills.NextBonus(inf, true, new ActionOutcome()), "남은 추가 이동 " + i); - Assert.IsFalse(bm.Skills.NextBonus(inf, true, new ActionOutcome()), "n 회 뒤 끝"); + var db = OutGameData.Default; + Assert.IsNotNull(db); + int skills = 0; + foreach (var h in db.Heroes.heroes) + { + if (h.isSoldier) continue; + foreach (var sid in new[] { h.skillActive, h.skillPassive }) + { + var d = SkillCatalog.Get(sid); + Assert.IsNotNull(d, sid); + Assert.Greater(d.Effects.Count, 0, sid + " 효과 줄"); + foreach (var e in d.Effects) Assert.IsTrue(SkillKeys.Known(e.Key), sid + " " + e.Key); + skills++; + } + } + Assert.AreEqual(db.Skills.skills.Count, skills); + Assert.IsTrue(db.Heroes.Get("diaochan").Female); + Assert.IsFalse(db.Heroes.Get("lvbu").Female); + } + + // ---------- 패시브: 진영 공격 · 부활 ---------- + + [UnityTest] + public IEnumerator Passive_CampAttack_And_Revive() + { + yield return Fresh(); + var liu = Spawn(UnitType.Infantry, Faction.Player, 0f, -3f, "유비"); + Give(liu, "liubei"); + var shu = Spawn(UnitType.Spear, Faction.Player, 2f, -3f, "촉"); + Give(shu, "guanyu"); + var wei = Spawn(UnitType.Spear, Faction.Player, 4f, -3f, "위"); + Give(wei, "zhanghe"); + Begin(); + float camp = Eff("liubei_p", SkillKeys.AtkPct).P; + Assert.AreEqual(1f + camp, SkillMods.AttackMult(liu), 1e-4f, "촉 진영 공격 +"); + Assert.AreEqual(1f, SkillMods.AttackMult(wei), 1e-4f, "위 진영은 안 받음"); + + var rv = Eff("liubei_p", SkillKeys.Revive); + int got = bm.Skills.Hit(liu, liu.Hp + 50, null, HitKind.Dot); + Assert.IsTrue(liu.IsAlive, "부활"); + Assert.AreEqual(Mathf.RoundToInt(liu.MaxHp * rv.P), liu.Hp); + Assert.Greater(got, 0); + bm.Skills.Hit(liu, liu.Hp + 50, null, HitKind.Dot); + Assert.IsFalse(liu.IsAlive, "부활은 전투당 " + rv.Get(1) + "회"); + } + + // ---------- 즉시 광역 ---------- + + [UnityTest] + public IEnumerator Instant_AoeStrike_DamagePushBleed() + { + yield return Fresh(); + var gy = Spawn(UnitType.Spear, Faction.Player, 0f, 0f, "관우"); + var s = Give(gy, "guanyu"); + var aoe = Eff("guanyu_a", SkillKeys.AoeStrike); + var near = Spawn(UnitType.Infantry, Faction.Enemy, aoe.P * 0.5f, 0f, "근처"); + var far = Spawn(UnitType.Infantry, Faction.Enemy, aoe.P + 3f, 0f, "멀리"); + Begin(); + int hp0 = near.Hp, farHp = far.Hp; + float d0 = Vector2.Distance(gy.Position, near.Position); + yield return bm.Skills.Instant(s); + yield return new WaitForSeconds(0.3f); + Assert.Less(near.Hp, hp0, "반경 안 피해"); + Assert.AreEqual(farHp, far.Hp, "반경 밖 무사"); + Assert.Greater(Vector2.Distance(gy.Position, near.Position), d0 + 0.05f, "바깥으로 밀림"); + Assert.IsTrue(SkillStatus.Bleeding(near), "출혈"); + Assert.AreEqual(1 + s.Active.Cooldown, s.ReadyTurn, "쿨"); } [UnityTest] - public IEnumerator Infantry_PushMult_KnockbackTaken_DmgPct_ExtraMoveAttack() + public IEnumerator Instant_AoeStun_NoDamage() { yield return Fresh(); - // 청룡도 지기(push_mult): 밀리는 거리 = 남은 힘 × collisionPushRatio × p - var a = Spawn(UnitType.Infantry, Faction.Player, 0f, -3f, "주창"); - Give(a, "zhoucang"); - Reserve(a); + var zf = Spawn(UnitType.Cavalry, Faction.Player, 0f, 0f, "장비"); + var s = Give(zf, "zhangfei"); + var aoe = Eff("zhangfei_a", SkillKeys.AoeStrike); + var foe = Spawn(UnitType.Infantry, Faction.Enemy, aoe.P * 0.5f, 0f, "적"); + Begin(); + int hp0 = foe.Hp; + yield return bm.Skills.Instant(s); + Assert.AreEqual(hp0, foe.Hp, "피해 없음(p2 0)"); + Assert.IsTrue(SkillStatus.Stunned(foe), "기절"); + } + + // ---------- 다음 행동: 재행동 · 반격 면역 ---------- + + [UnityTest] + public IEnumerator NextAction_ExtraAction_CounterImmune() + { + yield return Fresh(); + var liu = Spawn(UnitType.Infantry, Faction.Player, 0f, -3f, "유비"); + var s = Give(liu, "liubei"); var foe = Spawn(UnitType.Spear, Faction.Enemy, 0f, 0f, "적"); - float remaining = CastContact(a, foe); + Begin(); var o = new ActionOutcome(); - yield return Act(a, foe.Position, o); - Assert.AreEqual(remaining * cfg.collisionPushRatio * Eff("zhoucang_a", SkillKeys.PushMult).P, foe.Position.y, 0.12f, "push_mult"); - - // 거구(knockback_taken_pct): 부딪힌 쪽이 밀리는 거리 × (1 + p) - yield return Fresh(); - var b = Spawn(UnitType.Infantry, Faction.Player, 0f, -3f, "보병"); - var xu = Spawn(UnitType.Spear, Faction.Enemy, 0f, 0f, "허저"); - Give(xu, "xuchu"); - float rem2 = CastContact(b, xu); - var o2 = new ActionOutcome(); - yield return Act(b, xu.Position, o2); - Assert.AreEqual(rem2 * cfg.collisionPushRatio * (1f + Eff("xuchu_p", SkillKeys.KnockbackTakenPct).P), xu.Position.y, 0.12f, "knockback_taken_pct"); - - // 강동의 호랑이(dmg_pct · extra_move_attack): 피해 × (1 + p) · 명중 시 추가 이동에서 공격 가능 - yield return Fresh(); - var sj = Spawn(UnitType.Infantry, Faction.Player, 0f, -3f, "손견"); - var plain = Spawn(UnitType.Infantry, Faction.Player, 6f, -3f, "보병"); - var d = Spawn(UnitType.Spear, Faction.Enemy, 0f, 0f, "적"); - Give(sj, "sunjian"); - Arm(sj, true); - Assert.AreEqual(Expect(plain.Atk, cfg.Matchup(UnitType.Infantry, UnitType.Spear), d.Def, 1f + Eff("sunjian_a", SkillKeys.DmgPct).P), Dmg(sj, d), 1, "dmg_pct"); - Arm(sj, false); - Assert.AreEqual(Dmg(plain, d), Dmg(sj, d), "예약 효과는 행동 중에만"); - Reserve(sj); - var o3 = new ActionOutcome(); - yield return Act(sj, d.Position, o3); - Assert.IsTrue(o3.BonusMove && o3.BonusAttack, "extra_move_attack"); - Assert.IsTrue(bm.Skills.NextBonus(sj, false, o3)); - Assert.IsTrue(bm.Skills.BonusCanAttack(sj), "추가 이동 = 공격 가능"); + yield return ActArmed(liu, new Vector2(0f, 1f), o); + Assert.AreEqual(0, o.CounterTaken, "반격 안 받음"); + Assert.AreEqual(Mathf.RoundToInt(Eff("liubei_a", SkillKeys.ExtraAction).P), s.ReactCredits, "재행동"); + Assert.IsTrue(bm.Skills.TakeReaction(liu)); + Assert.AreEqual(0f, SkillMods.CounterTakenMult(liu, foe), 1e-4f, "1턴 반격 면역은 재행동에도"); } + // ---------- 치명타 · 흡혈 ---------- + [UnityTest] - public IEnumerator Infantry_Yujin_PushedAllyAttacksTwice_Liubei_PushedAttackBonus_Gaoshun_Collision() + public IEnumerator CritAlways_And_Lifesteal() { yield return Fresh(); - // 군령(pushed_ally_attack_count): 알까기로 밀린 아군이 적과 닿으면 n 회 공격 - var a = Spawn(UnitType.Infantry, Faction.Player, -3f, -2f, "우금"); - var b = Spawn(UnitType.Spear, Faction.Player, 0f, -2f, "창병B"); - Give(a, "yujin"); - Reserve(a); - float pushX = CastContact(a, b); - var foe = Spawn(UnitType.Infantry, Faction.Enemy, pushX, -2f + b.Radius + 0.45f + 0.05f, "적"); - foe.MaxHp = foe.Hp = 10000; - var o = new ActionOutcome(); - yield return Act(a, new Vector2(0f, -2f), o); - Assert.AreEqual(Mathf.RoundToInt(Eff("yujin_a", SkillKeys.PushedAllyAttackCount).P), o.Notes.Count, "pushed_ally_attack_count"); - - // 도원의 맹약(pushed_ally_attack_pct · 같은 편 전원): 밀린 아군 공격 피해 × (1 + p) - yield return Fresh(); - var lb = Spawn(UnitType.Infantry, Faction.Player, -6f, -8f, "유비"); - Give(lb, "liubei"); - var ally = Spawn(UnitType.Infantry, Faction.Player, 0f, -2f, "보병"); - var en = Spawn(UnitType.Infantry, Faction.Enemy, 6f, 6f, "적"); - float p = Eff("liubei_p", SkillKeys.PushedAllyAttackPct).P; - Assert.AreEqual(p, SkillMods.Sum(ally, SkillKeys.PushedAllyAttackPct), 1e-5f, "같은 편 = 적용"); - Assert.AreEqual(0f, SkillMods.Sum(en, SkillKeys.PushedAllyAttackPct), 1e-5f, "상대 = 미적용"); - - // 함진영(push_mult p2): 밀린 적이 벽·유닛에 막히면 알까기 피해 × p2 추가 - yield return Fresh(); - var gs = Spawn(UnitType.Infantry, Faction.Player, 0f, -3f, "고순"); - Give(gs, "gaoshun"); - Reserve(gs); - var f1 = Spawn(UnitType.Spear, Faction.Enemy, 0f, 0f, "적1"); - var f2 = Spawn(UnitType.Spear, Faction.Enemy, 0f, f1.Radius * 2f + 0.01f, "적2"); - f1.MaxHp = f1.Hp = 10000; - var o2 = new ActionOutcome(); - yield return Act(gs, f1.Position, o2); - float bonus = Eff("gaoshun_a", SkillKeys.PushMult).A2; - int total = f1.MaxHp - f1.Hp; - Assert.AreEqual(total, o2.DamageDealt, "충돌 추가 피해도 집계"); - int impact = Mathf.RoundToInt(total / (1f + bonus)); - Assert.AreEqual(Mathf.Max(1, Mathf.RoundToInt(impact * bonus)), total - impact, 2, "막혀서 충돌 = 알까기 피해 × p2"); - } - - // ---------- 기병 ---------- - - [UnityTest] - public IEnumerator Cavalry_NoCounter_ReturnOnMiss_Knockback_Repeat_PerHit_MinPierce() - { - yield return Fresh(); - // 천하무쌍(pierce_dmg_mult · pierce_no_counter) - var lv = Spawn(UnitType.Cavalry, Faction.Player, 0f, -4f, "여포"); - Give(lv, "lvbu"); - Reserve(lv); - var e1 = Spawn(UnitType.Spear, Faction.Enemy, 0f, -1.5f, "적창병"); - e1.MaxHp = e1.Hp = 10000; - var o = new ActionOutcome(); - yield return Act(lv, new Vector2(0f, 1.5f), o); - Assert.Less(e1.Hp, e1.MaxHp); - Assert.AreEqual(0, o.CounterTaken, "pierce_no_counter = 살아남은 근접 적도 반격 없음"); - Arm(lv, true); - Assert.AreEqual(Eff("lvbu_a", SkillKeys.PierceDmgMult).P, SkillMods.PierceDmgMult(lv, 1), 1e-5f, "pierce_dmg_mult"); - Arm(lv, false); - - // 단기구주(pierce_return_on_miss) + 상산의 창(pierce_knockback_mult) - yield return Fresh(); - var zy = Spawn(UnitType.Cavalry, Faction.Player, 0f, -4f, "조운"); - Give(zy, "zhaoyun"); - Reserve(zy); - var e2 = Spawn(UnitType.Infantry, Faction.Enemy, 0.3f, -1.5f, "적"); - e2.MaxHp = e2.Hp = 10000; - var o2 = new ActionOutcome(); - yield return Act(zy, new Vector2(0f, 2f), o2); - Assert.IsFalse(o2.Returned, "기병 복귀 제거(PD 2026-11-10) = 단기구주 「빗나가도 출발점 복귀」 발동 안 함(스킬 재설계 PD 결정 대기)"); - Assert.Greater(zy.Position.y, e2.Position.y, "적을 꿰뚫고 지나가 그 너머에 남음"); - Assert.AreEqual(0, o2.CounterTaken, "단기구주 = 반격 없음"); - var disp = o2.Displaced.Find(x => x.Unit == e2); - Assert.IsNotNull(disp.Unit); - Assert.AreEqual(cfg.cavalryKnockback * Eff("zhaoyun_p", SkillKeys.PierceKnockbackMult).P, Vector2.Distance(disp.From, disp.To), 0.05f, "pierce_knockback_mult"); - - // 위진소요(pierce_repeat) + 팔백 돌격(pierce_dmg_pct_per_hit) - yield return Fresh(); - var zl = Spawn(UnitType.Cavalry, Faction.Player, 0f, -7f, "장료"); - Give(zl, "zhangliao"); - Reserve(zl); - var t1 = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -4f, "적1"); - var t2 = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -1.2f, "적2"); - t1.MaxHp = t1.Hp = t2.MaxHp = t2.Hp = 10000; - var o3 = new ActionOutcome(); - yield return Act(zl, t1.Position, o3); - Assert.Less(t2.Hp, t2.MaxHp, "명중 → 같은 방향 한 번 더 관통"); - Assert.IsFalse(o3.Returned, "추가 관통 · 복귀 없음"); - Assert.AreEqual(t1.MaxHp - t1.Hp, o3.DamageDealt - (t2.MaxHp - t2.Hp), "앞서 관통한 적은 다시 맞지 않음"); - var ps = Eff("zhangliao_p", SkillKeys.PierceDmgPctPerHit); - int hits = bm.Skills.Of(zl).PierceHits; - Assert.AreEqual(2, hits, "관통한 적 누적"); - Assert.AreEqual(1f + Mathf.Min(ps.P * hits, ps.A2), SkillMods.AttackMult(zl), 1e-4f, "pierce_dmg_pct_per_hit(최대 p2)"); - - // 만인지적(pierce_dmg_mult p2 = 최소 관통 수) - yield return Fresh(); - var zf = Spawn(UnitType.Cavalry, Faction.Player, 0f, -4f, "장비"); - Give(zf, "zhangfei"); - var mp = Eff("zhangfei_p", SkillKeys.PierceDmgMult); - int need = Mathf.RoundToInt(mp.A2); - Assert.AreEqual(1f, SkillMods.PierceDmgMult(zf, need - 1), 1e-5f, "최소 관통 수 미만"); - Assert.AreEqual(mp.P, SkillMods.PierceDmgMult(zf, need), 1e-5f, "최소 관통 수 이상"); - } - - // ---------- 창병 · 견제 ---------- - - int StrikeHi(Unit spear, Unit mover) - { - int one = Combat.OverwatchDamage(cfg, spear, mover, new System.Random(Seed)); - return Mathf.CeilToInt(one / (1f - cfg.damageVariance) * (1f + cfg.damageVariance)) + 1; - } - - int StrikeLo(Unit spear, Unit mover) - { - int one = Combat.OverwatchDamage(cfg, spear, mover, new System.Random(Seed)); - return Mathf.FloorToInt(one / (1f + cfg.damageVariance) * (1f - cfg.damageVariance)) - 1; - } - - [UnityTest] - [Ignore("창병 견제 제거(PD 2026-11-10) — 대기 유닛이 생기지 않아 견제 스킬(청룡언월·후계·삼대 노신·교변·하북 명장 …)이 발동하지 않음 · 스킬 재설계 PD 결정 대기")] - public IEnumerator Spear_OverwatchSkills_Radius_Unlimited_Count_DmgMult_HealOnKill() - { - yield return Fresh(); - // 청룡언월(overwatch_radius_mult · overwatch_unlimited): 명중 → 대기 반경 × p · 반경을 스친 구간마다 + 밀린 적도 - var gy = Spawn(UnitType.Spear, Faction.Player, 0f, -3f, "관우"); - Give(gy, "guanyu"); - Reserve(gy); + var mc = Spawn(UnitType.Cavalry, Faction.Player, 0f, -3f, "마초"); + var s = Give(mc, "machao"); var foe = Spawn(UnitType.Infantry, Faction.Enemy, 0f, 0f, "적"); - var o = new ActionOutcome(); - yield return Act(gy, foe.Position, o); - Assert.IsTrue(o.Overwatch && gy.Overwatch); - float r = SkillMods.OverwatchRadius(gy); - Assert.AreEqual(gy.AttackRange * Eff("guanyu_a", SkillKeys.OverwatchRadiusMult).P, r, 1e-4f, "overwatch_radius_mult"); - Assert.IsTrue(SkillMods.OverwatchUnlimited(gy)); - var mover = Spawn(UnitType.Infantry, Faction.Enemy, gy.Position.x + r + 3f, gy.Position.y, "이동 적"); - mover.MaxHp = mover.Hp = 10000; - var path = new ActionOutcome(); - Vector2 c = gy.Position; - path.Path.AddRange(new[] { c + new Vector2(r + 2f, 0.3f), c + new Vector2(0.5f, 0.3f), c + new Vector2(r + 2f, 0.6f), c + new Vector2(0.5f, 0.6f) }); - yield return bm.ApplyOverwatch(mover, path); - int drop = mover.MaxHp - mover.Hp; - Assert.GreaterOrEqual(drop, StrikeLo(gy, mover) * 2, "반경을 스친 구간 2 번 = 견제 2 회(무제한)"); - // 밀린 적(Displaced)도 견제 - var pushedFoe = Spawn(UnitType.Infantry, Faction.Enemy, c.x - r - 3f, c.y, "밀린 적"); - pushedFoe.MaxHp = pushedFoe.Hp = 10000; - var o2 = new ActionOutcome(); - o2.Path.Add(mover.Position); - o2.Displaced.Add(new Displacement { Unit = pushedFoe, From = c + new Vector2(-r - 1f, 0f), To = c + new Vector2(-0.5f, 0f) }); - yield return bm.ApplyOverwatch(mover, o2); - Assert.Less(pushedFoe.Hp, pushedFoe.MaxHp, "무제한 = 밀려서 반경을 지난 적도 견제"); + Begin(); + s.Armed = true; + Combat.Damage(cfg, mc, foe, false, new System.Random(1), true); + Assert.IsTrue(Combat.LastCrit, "무조건 치명타"); + s.Armed = false; + mc.Hp = mc.MaxHp / 2; + int before = mc.Hp; + int dealt = bm.Skills.Hit(foe, 40, mc, HitKind.Attack); + Assert.AreEqual(Mathf.RoundToInt(dealt * Eff("machao_p", SkillKeys.Lifesteal).P), mc.Hp - before, "흡혈"); + } - // 후계(overwatch_radius_add) · 삼대 노신(overwatch_count) · 교변(overwatch_dmg_mult) + // ---------- 상태 이상 ---------- + + [UnityTest] + public IEnumerator Poison_Stacks_And_Ticks() + { yield return Fresh(); - var gp = Spawn(UnitType.Spear, Faction.Player, -6f, -6f, "관평"); - Give(gp, "guanping"); - gp.SetOverwatch(true); - Assert.AreEqual(gp.AttackRange + Eff("guanping_p", SkillKeys.OverwatchRadiusAdd).P, SkillMods.OverwatchRadius(gp), 1e-4f, "overwatch_radius_add"); - - var cp = Spawn(UnitType.Spear, Faction.Player, 4f, -6f, "정보"); - Give(cp, "chengpu"); - cp.SetOverwatch(true); - var m2 = Spawn(UnitType.Infantry, Faction.Enemy, 4f, 2f, "적"); - m2.MaxHp = m2.Hp = 10000; - var po = new ActionOutcome(); - po.Path.AddRange(new[] { new Vector2(3f, -3f), new Vector2(3f, -8f) }); - yield return bm.ApplyOverwatch(m2, po); - int strikes = Mathf.RoundToInt(Eff("chengpu_p", SkillKeys.OverwatchCount).P); - Assert.GreaterOrEqual(m2.MaxHp - m2.Hp, StrikeLo(cp, m2) * strikes, "overwatch_count = 지나간 적 1기당 n 회"); - Assert.LessOrEqual(m2.MaxHp - m2.Hp, StrikeHi(cp, m2) * strikes); - - var zh = Spawn(UnitType.Spear, Faction.Player, 8f, 8f, "장합"); - Give(zh, "zhanghe"); - var plainSpear = Spawn(UnitType.Spear, Faction.Player, -8f, 8f, "창병"); - int baseOw = Combat.OverwatchDamage(cfg, plainSpear, m2, new System.Random(Seed)); - int zhOw = Combat.OverwatchDamage(cfg, zh, m2, new System.Random(Seed)); - Assert.AreEqual(baseOw * Eff("zhanghe_p", SkillKeys.OverwatchDmgMult).P, zhOw, 1.01f, "overwatch_dmg_mult"); - - // 하북 명장(heal_on_overwatch_kill): 견제로 쓰러뜨리면 최대 HP × p 회복 - yield return Fresh(); - var yl = Spawn(UnitType.Spear, Faction.Player, 0f, -3f, "안량"); - Give(yl, "yanliang"); - yl.SetOverwatch(true); - yl.Hp = yl.MaxHp / 2; - int hp0 = yl.Hp; - var weak = Spawn(UnitType.Infantry, Faction.Enemy, 0f, 1f, "적"); - weak.Hp = 1; - var ko = new ActionOutcome(); - ko.Path.AddRange(new[] { new Vector2(-1f, -1.5f), new Vector2(1f, -1.5f) }); - yield return bm.ApplyOverwatch(weak, ko); - Assert.IsFalse(weak.IsAlive); - Assert.AreEqual(Mathf.Min(yl.MaxHp, hp0 + Mathf.Max(1, Mathf.RoundToInt(yl.MaxHp * Eff("yanliang_p", SkillKeys.HealOnOverwatchKill).P))), yl.Hp, "heal_on_overwatch_kill"); + var jx = Spawn(UnitType.Strategist, Faction.Player, 0f, -3f, "가후"); + Give(jx, "jiaxu"); + var foe = Spawn(UnitType.Infantry, Faction.Enemy, 0f, 3f, "적"); + Begin(); + bm.Skills.ApplyHitStatuses(jx, foe, false, HitKind.Attack); + bm.Skills.ApplyHitStatuses(jx, foe, false, HitKind.Attack); + var st = bm.Skills.StatusOf(foe); + Assert.AreEqual(2, st.PoisonStacks, "중첩"); + var hp = Eff("jiaxu_p", SkillKeys.HitPoison); + int before = foe.Hp; + yield return bm.Skills.TurnBegin(Faction.Enemy); + Assert.AreEqual(Mathf.Max(1, Mathf.RoundToInt(foe.MaxHp * hp.Get(1) * 2)), before - foe.Hp, "턴 시작 중독 × 중첩"); } [UnityTest] - public IEnumerator Spear_HitTwo_OnPushAllyAttacks_ChengpuAdvance() + public IEnumerator Confuse_HalvesSling_ThenExpires() { yield return Fresh(); - // 비도(spear_hit_two · 축융 · 기령은 영웅 풀 제외 PD #1096): 밀며 전진 끝에 닿은 적까지 최대 n 기 동시 피해 - var jl = Spawn(UnitType.Spear, Faction.Player, 0f, -4f, "축융"); - Give(jl, "zhurong"); - Reserve(jl); - var f1 = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -1f, "적1"); - var f2 = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -1f + f1.Radius * 2f + 0.03f, "적2"); - f1.MaxHp = f1.Hp = f2.MaxHp = f2.Hp = 10000; - var o = new ActionOutcome(); - yield return Act(jl, new Vector2(0f, -1f), o); - Assert.Less(f1.Hp, f1.MaxHp); - Assert.Less(f2.Hp, f2.MaxHp, "spear_hit_two = 닿은 둘째 적도 피해"); - Assert.Contains(f2, o.Attacked); - - // 대장군(pushed_ally_attack_on_push)은 기령 제외로 표에서 쓰지 않음(처리기만 유지 · PD #1096) - - // 철척사모(push_mult · 창병): 밀며 전진 거리 × p - yield return Fresh(); - var ch = Spawn(UnitType.Spear, Faction.Player, 0f, -3f, "정보"); - Give(ch, "chengpu"); - Reserve(ch); - var t = Spawn(UnitType.Infantry, Faction.Enemy, 0f, 0f, "적"); - t.MaxHp = t.Hp = 10000; - float rem = CastContact(ch, t); - var o3 = new ActionOutcome(); - yield return Act(ch, t.Position, o3); - Assert.AreEqual(rem * cfg.spearPushRatio * Eff("chengpu_a", SkillKeys.PushMult).P, t.Position.y, 0.12f, "push_mult(창병 밀치기 = 남은 거리 × spearPushRatio × p · PD 2026-11-10)"); - } - - // ---------- 궁병 · 책사 ---------- - - [UnityTest] - public IEnumerator Ranged_Range_TeamRange_MeleePenaltyOff_PassAllies_CritAlways() - { - yield return Fresh(); - var ts = Spawn(UnitType.Archer, Faction.Player, -6f, -8f, "태사자"); - Give(ts, "taishici"); - Arm(ts, true); - Assert.AreEqual(ts.AttackRange * Eff("taishici_a", SkillKeys.RangeMult).P, SkillMods.Range(ts), 1e-4f, "range_mult"); - Arm(ts, false); - var xs = Spawn(UnitType.Strategist, Faction.Player, -3f, -8f, "서서"); - Give(xs, "xushu"); - Assert.AreEqual(xs.AttackRange + Eff("xushu_p", SkillKeys.RangeAdd).P, SkillMods.Range(xs), 1e-4f, "range_add"); - - var zy = Spawn(UnitType.Strategist, Faction.Player, 0f, -8f, "주유"); - Give(zy, "zhouyu"); - var archer = Spawn(UnitType.Archer, Faction.Player, 3f, -8f, "궁병"); - var inf = Spawn(UnitType.Infantry, Faction.Player, 6f, -8f, "보병"); - var enemyArcher = Spawn(UnitType.Archer, Faction.Enemy, 6f, 8f, "적궁병"); - float tr = Eff("zhouyu_p", SkillKeys.TeamRangeAdd).P; - Assert.AreEqual(archer.AttackRange + tr, SkillMods.Range(archer), 1e-4f, "team_range_add = 아군 궁병"); - Assert.AreEqual(0f, SkillMods.Sum(inf, SkillKeys.TeamRangeAdd), 1e-5f, "보병 제외"); - Assert.AreEqual(enemyArcher.AttackRange, SkillMods.Range(enemyArcher), 1e-4f, "상대 제외"); - - // 돌파(melee_penalty_off): 근접 전투 패널티 없음(주는·받는 피해) - var foe = Spawn(UnitType.Infantry, Faction.Enemy, 8f, 8f, "적보병"); - float m = cfg.Matchup(UnitType.Archer, UnitType.Infantry); - Assert.AreEqual(Expect(ts.Atk, m, foe.Def, 1f), Dmg(ts, foe, true), 1, "주는 근접 피해 패널티 없음"); - Assert.AreEqual(Expect(archer.Atk, m, foe.Def, cfg.archerMeleeDealtRatio), Dmg(archer, foe, true), 1, "일반 궁병은 패널티"); - float m2 = cfg.Matchup(UnitType.Infantry, UnitType.Archer); - Assert.AreEqual(Expect(foe.Atk, m2, ts.Def, 1f), Dmg(foe, ts, true), 1, "받는 근접 피해 패널티 없음"); - - // 백발백중(arrow_pass_allies · arrow_crit_always): 사선의 아군 통과 · 목표점 명중 없이도 크리 - yield return Fresh(); - var hz = Spawn(UnitType.Archer, Faction.Player, 0f, -4f, "황충"); - Give(hz, "huangzhong"); - Reserve(hz); - var block = Spawn(UnitType.Infantry, Faction.Player, 0f, -2.6f, "아군"); - var target = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -0.6f, "적"); - target.MaxHp = target.Hp = 10000; - var o = new ActionOutcome(); - yield return Act(hz, new Vector2(0f, -3.2f), o); - Assert.AreEqual(block.MaxHp, block.Hp, "arrow_pass_allies = 사선의 아군 무피해"); - Assert.Less(target.Hp, target.MaxHp); - Assert.IsTrue(o.Crit, "arrow_crit_always = 목표점 명중 없이 크리"); + var foe = Spawn(UnitType.Infantry, Faction.Enemy, 0f, 3f, "적"); + Begin(); + var c = Eff("zhangliao_a", SkillKeys.HitConfuse); + float base0 = SkillMods.SlingMax(foe); + bm.Skills.ApplyConfuse(foe, Mathf.RoundToInt(c.P), c.Get(1)); + Assert.AreEqual(base0 * c.Get(1), SkillMods.SlingMax(foe), 1e-3f, "혼란 = 이동 × 배율"); + yield return bm.Skills.TurnBegin(Faction.Player); // 적 턴 끝 + Assert.AreEqual(base0, SkillMods.SlingMax(foe), 1e-3f, "적 턴 1번 뒤 해제"); } [UnityTest] - public IEnumerator Strategist_SplashRadiusRatio_AllyZero_Ganning_Teleport() + public IEnumerator Bleed_DamageByMovedDistance() { yield return Fresh(); - // 화공(splash_radius_mult · splash_ratio) - var zy = Spawn(UnitType.Strategist, Faction.Player, 0f, -4f, "주유"); - Give(zy, "zhouyu"); - Reserve(zy); - var c = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -1f, "중심"); - float mult = Eff("zhouyu_a", SkillKeys.SplashRadiusMult).P; - float far = cfg.strategistSplashRadius * (1f + mult) * 0.5f + 0.45f; // 기본 반경 밖 · 화공 반경 안 - var side = Spawn(UnitType.Infantry, Faction.Enemy, far, -1f, "곁"); - var plan = bm.Resolver.PreviewShot(zy, c.Position, false); - Assert.IsNotNull(plan); - Assert.AreEqual(cfg.strategistSplashRadius * mult, plan.SplashRadius, 1e-4f, "splash_radius_mult"); - Assert.Contains(side, plan.Splash, "커진 광역에 들어옴"); - Arm(zy, true); - Assert.AreEqual(Eff("zhouyu_a", SkillKeys.SplashRatio).P, SkillMods.SplashRatio(zy, cfg), 1e-4f, "splash_ratio"); - Arm(zy, false); - Assert.AreEqual(cfg.strategistSplashRatio, SkillMods.SplashRatio(zy, cfg), 1e-4f, "예약 없으면 자산 값"); - - // 팔문 간파(splash_ally_ratio): 광역의 아군 피해 = 0 - yield return Fresh(); - var xs = Spawn(UnitType.Strategist, Faction.Player, 0f, -4f, "서서"); - Give(xs, "xushu"); - Reserve(xs); - var en = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -0.6f, "적"); - en.MaxHp = en.Hp = 10000; - var ally = Spawn(UnitType.Infantry, Faction.Player, 1.0f, -0.6f, "아군"); - var o = new ActionOutcome(); - yield return Act(xs, en.Position, o); - Assert.Less(en.Hp, en.MaxHp); - Assert.AreEqual(ally.MaxHp, ally.Hp, "splash_ally_ratio = 0"); - - // 백기습영(teleport · arrow_crit_always): 사거리 밖이어도 목표점까지 이동 후 사격 · 크리 - yield return Fresh(); - var gn = Spawn(UnitType.Archer, Faction.Player, 0f, -9f, "감녕"); - Give(gn, "ganning"); - var far2 = Spawn(UnitType.Infantry, Faction.Enemy, 0f, 0.5f, "먼 적"); - far2.MaxHp = far2.Hp = 10000; - Assert.Greater(Gap(gn, far2), SkillMods.Range(gn) + gn.ArcherMoveAllow, "출발점에서는 사거리 밖"); - Reserve(gn); - Vector2 to = gn.Position + Vector2.up * SkillMods.SlingMax(gn); - var o2 = new ActionOutcome(); - yield return Act(gn, to, o2); - Assert.Greater(gn.Position.y, -9f + SkillMods.SlingMax(gn) - 0.3f, "목표점까지 이동"); - Assert.IsTrue(o2.Fired && far2.Hp < far2.MaxHp, "이동 후 사격"); - Assert.IsTrue(o2.Crit, "명중 시 크리"); + var foe = Spawn(UnitType.Infantry, Faction.Enemy, 0f, 3f, "적"); + Begin(); + var b = Eff("xuhuang_p", SkillKeys.HitBleed); + bm.Skills.ApplyBleedStatus(foe, Mathf.RoundToInt(b.P), b.Get(1)); + int before = foe.Hp; + float moved = foe.SlingMaxDistance * 0.5f; + bm.Skills.ApplyBleed(new Dictionary { { foe, moved } }); + Assert.AreEqual(Mathf.Max(1, Mathf.RoundToInt(foe.MaxHp * b.Get(1) * 0.5f)), before - foe.Hp, "출혈 = 거리 비례"); } - // ---------- 피해 · 반격 · 공격력 ---------- + // ---------- 무적 · 호위 ---------- [UnityTest] - public IEnumerator Damage_MatchupFloor_HpBelow_Turn1_General_Taken_Archer_Counter_Death_Full_HpMax() + public IEnumerator Invuln_Instant_BlocksDamageForRound() { yield return Fresh(); - var cav = Spawn(UnitType.Cavalry, Faction.Enemy, 6f, 8f, "적기병"); - var xh = Spawn(UnitType.Infantry, Faction.Player, -6f, -8f, "서황"); - var plain = Spawn(UnitType.Infantry, Faction.Player, -3f, -8f, "보병"); - Give(xh, "xuhuang"); - Arm(xh, true); - float floor = Eff("xuhuang_a", SkillKeys.MatchupFloor).P; - Assert.AreEqual(Expect(xh.Atk, Mathf.Max(cfg.Matchup(UnitType.Infantry, UnitType.Cavalry), floor), cav.Def, 1f), Dmg(xh, cav), 1, "matchup_floor"); - Arm(xh, false); - Assert.AreEqual(Dmg(plain, cav), Dmg(xh, cav)); - - // 노익장(dmg_pct_hp_below): HP 임계 이하 공 +p - var hz = Spawn(UnitType.Archer, Faction.Player, 0f, -8f, "황충"); - Give(hz, "huangzhong"); - var hb = Eff("huangzhong_p", SkillKeys.DmgPctHpBelow); - Assert.AreEqual(1f, SkillMods.AttackMult(hz), 1e-5f, "HP 가득 = 없음"); - hz.Hp = Mathf.FloorToInt(hz.MaxHp * hb.A2); - Assert.AreEqual(1f + hb.P, SkillMods.AttackMult(hz), 1e-5f, "dmg_pct_hp_below"); - - // 선봉(dmg_pct_turn1) - var hx = Spawn(UnitType.Infantry, Faction.Player, 3f, -8f, "화웅"); - Give(hx, "huaxiong"); - bm.SetTurnForTest(1); - Assert.AreEqual(1f + Eff("huaxiong_p", SkillKeys.DmgPctTurn1).P, SkillMods.AttackMult(hx), 1e-5f, "dmg_pct_turn1"); - bm.SetTurnForTest(2); - Assert.AreEqual(1f, SkillMods.AttackMult(hx), 1e-5f, "2턴 = 없음"); - - // 파로(dmg_vs_general_mult p2 1 = 보스만) · 참장(p2 0 = 무장 전체) - var sj = Spawn(UnitType.Infantry, Faction.Player, 6f, -8f, "손견"); - Give(sj, "sunjian"); - var boss = Spawn(UnitType.Infantry, Faction.Enemy, 0f, 8f, "보스", true); - var gen = Spawn(UnitType.Infantry, Faction.Enemy, 3f, 8f, "장수"); - bm.Skills.MarkGeneral(gen); - float im = cfg.Matchup(UnitType.Infantry, UnitType.Infantry); - Assert.AreEqual(Expect(sj.Atk, im, boss.Def, Eff("sunjian_p", SkillKeys.DmgVsGeneralMult).P), Dmg(sj, boss), 1, "보스 = 배율"); - Assert.AreEqual(Expect(sj.Atk, im, gen.Def, 1f), Dmg(sj, gen), 1, "보스만 = 일반 장수 제외"); - Arm(hx, true); - Assert.AreEqual(Expect(hx.Atk, im, gen.Def, Eff("huaxiong_a", SkillKeys.DmgVsGeneralMult).P), Dmg(hx, gen), 1, "참장 = 무장 전체"); - Arm(hx, false); - - // 수성(dmg_taken_pct) · 창천이사(dmg_taken_from_archer_pct) - var cr = Spawn(UnitType.Infantry, Faction.Player, -6f, -5f, "조인"); - Give(cr, "caoren"); - Assert.AreEqual(Expect(gen.Atk, im, cr.Def, 1f + Eff("caoren_p", SkillKeys.DmgTakenPct).P), Dmg(gen, cr), 1, "dmg_taken_pct"); - var zj = Spawn(UnitType.Infantry, Faction.Player, -3f, -5f, "장각"); - Give(zj, "zhangjiao"); - var ea = Spawn(UnitType.Archer, Faction.Enemy, -3f, 8f, "적궁병"); - float am = cfg.Matchup(UnitType.Archer, UnitType.Infantry); - Assert.AreEqual(Expect(ea.Atk, am, zj.Def, 1f + Eff("zhangjiao_p", SkillKeys.DmgTakenFromArcherPct).P), Dmg(ea, zj, false), 1, "적 궁병 피해 −"); - Assert.AreEqual(Expect(gen.Atk, im, zj.Def, 1f), Dmg(gen, zj), 1, "궁병 아님 = 그대로"); - - // 무성(counter_ratio) · 진중(p2 1 = 밀렸을 때만) · 군율(counter_taken_pct) · 금범적(−1 = 반격 없음) - var gy = Spawn(UnitType.Spear, Faction.Player, 0f, -5f, "관우"); - Give(gy, "guanyu"); - int baseGy = Combat.Damage(cfg, gy, gen, false, new System.Random(Seed), true); - Assert.AreEqual(Mathf.Max(1, Mathf.RoundToInt(baseGy * Eff("guanyu_p", SkillKeys.CounterRatio).P)), Combat.CounterDamage(cfg, gy, gen, new System.Random(Seed)), "counter_ratio"); - var yj = Spawn(UnitType.Infantry, Faction.Player, 3f, -5f, "우금"); - Give(yj, "yujin"); - int baseYj = Combat.Damage(cfg, yj, gen, false, new System.Random(Seed), true); - Assert.AreEqual(Mathf.Max(1, Mathf.RoundToInt(baseYj * cfg.counterRatio)), Combat.CounterDamage(cfg, yj, gen, new System.Random(Seed), false), "안 밀림 = 기본 반격"); - Assert.AreEqual(Mathf.Max(1, Mathf.RoundToInt(baseYj * Eff("yujin_p", SkillKeys.CounterRatio).P)), Combat.CounterDamage(cfg, yj, gen, new System.Random(Seed), true), "밀렸을 때 = 반격 배율"); - int baseC = Combat.Damage(cfg, gen, xh, false, new System.Random(Seed), true); - Assert.AreEqual(Mathf.Max(1, Mathf.RoundToInt(baseC * cfg.counterRatio * (1f + Eff("xuhuang_p", SkillKeys.CounterTakenPct).P))), Combat.CounterDamage(cfg, gen, xh, new System.Random(Seed)), "counter_taken_pct"); - var gn = Spawn(UnitType.Archer, Faction.Player, 6f, -5f, "감녕"); - Give(gn, "ganning"); - Assert.AreEqual(0, Combat.CounterDamage(cfg, gen, gn, new System.Random(Seed)), "금범적 = 근접 반격 없음"); - - // 복수(on_ally_death_atk) · 패자(team_atk_pct_full · 출전 인원 조건) - yield return Fresh(); - var wc = Spawn(UnitType.Cavalry, Faction.Player, 0f, -8f, "문추"); - Give(wc, "wenchou"); - var dead = Spawn(UnitType.Infantry, Faction.Player, 3f, -8f, "전사자"); - Assert.AreEqual(1f, SkillMods.AttackMult(wc), 1e-5f); - dead.TakeDamage(dead.Hp); - Assert.AreEqual(1f + Eff("wenchou_p", SkillKeys.OnAllyDeathAtk).P, SkillMods.AttackMult(wc), 1e-5f, "on_ally_death_atk"); - - yield return Fresh(); - var cc = Spawn(UnitType.Infantry, Faction.Player, -6f, -8f, "조조"); - Give(cc, "caocao"); - var fe = Eff("caocao_p", SkillKeys.TeamAtkPctFull); - int needN = Mathf.RoundToInt(fe.A2); - var mates = new List(); - for (int i = 1; i < needN; i++) mates.Add(Spawn(UnitType.Infantry, Faction.Player, -6f + i * 2.5f, -8f, "아군" + i)); - bm.Skills.BeginBattle(); - Assert.AreEqual(1f + fe.P, SkillMods.AttackMult(mates[0]), 1e-5f, "출전 n 인 = 아군 전원 공 +p"); - mates[0].gameObject.SetActive(false); - bm.ClearUnits(); - yield return null; - var cc2 = Spawn(UnitType.Infantry, Faction.Player, -6f, -8f, "조조"); - Give(cc2, "caocao"); - var mate = Spawn(UnitType.Infantry, Faction.Player, 0f, -8f, "아군"); - bm.Skills.BeginBattle(); - Assert.AreEqual(1f, SkillMods.AttackMult(mate), 1e-5f, "인원 미달 = 없음"); - - // 산채 체력(hp_max_pct) - var zc = Spawn(UnitType.Infantry, Faction.Player, 6f, -8f, "주창"); - int baseHp = zc.MaxHp; - Give(zc, "zhoucang"); - Assert.AreEqual(Mathf.RoundToInt(baseHp * (1f + Eff("zhoucang_p", SkillKeys.HpMaxPct).P)), zc.MaxHp, "hp_max_pct"); - Assert.AreEqual(zc.MaxHp, zc.Hp); + var zj = Spawn(UnitType.Infantry, Faction.Player, 0f, -3f, "장각"); + var s = Give(zj, "zhangjiao"); + Begin(); + yield return bm.Skills.Instant(s); + int hp = zj.Hp; + Assert.AreEqual(0, bm.Skills.Hit(zj, 30, null, HitKind.Attack)); + Assert.AreEqual(hp, zj.Hp, "무적"); + yield return bm.Skills.TurnBegin(Faction.Player); + Assert.Greater(bm.Skills.Hit(zj, 30, null, HitKind.Attack), 0, "다음 자기 턴 시작에 끝"); } - // ---------- 즉시형(needs_impl 처리기 포함) ---------- - [UnityTest] - public IEnumerator Instant_PushFear_Fog_Guard_Aura_TeamTaken_HpCost_SlingTeam_Aggro() + public IEnumerator Guard_TakesAllyDamage() { - yield return Fresh(); - // 장판 일갈(aoe_push_fear): 반경 p2 안 적을 바깥으로 p1 · 이번 적 턴 교전 반응 없음 - var zf = Spawn(UnitType.Cavalry, Faction.Player, 0f, -3f, "장비"); - Give(zf, "zhangfei"); - var pf = Eff("zhangfei_a", SkillKeys.AoePushFear); - var near = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -3f + 1.5f, "가까운 적"); - var farE = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -3f + pf.A2 + 3f, "먼 적"); - float y0 = near.Position.y; - bm.Sling.Select(zf); - Assert.IsNotNull(bm.Hud.PressSkill()); - yield return WaitPhase(BattlePhase.PlayerTurn); - Assert.AreEqual(y0 + pf.P, near.Position.y, 0.08f, "바깥으로 p1 밀기"); - Assert.AreEqual(bm.TurnNumber + Cd("zhangfei_a"), bm.Skills.Of(zf).ReadyTurn, "즉시형도 쿨"); - Assert.IsFalse(zf.ActedThisTurn, "행동 소모 없음"); - Assert.IsFalse(bm.Skills.Suppressed(near), "아군 턴에는 해당 없음"); - bm.ForcePhase(BattlePhase.EnemyTurn); - Assert.IsTrue(bm.Skills.Suppressed(near), "이번 적 턴 교전 반응 없음"); - Assert.IsFalse(bm.Skills.Suppressed(farE), "반경 밖 적은 정상"); - bm.ForcePhase(BattlePhase.PlayerTurn); - - // 태평요술(enemy_aggro_off_turn): 이번 적 턴 적 전원 - var zj = Spawn(UnitType.Infantry, Faction.Player, 4f, -6f, "장각"); - Give(zj, "zhangjiao"); - bm.Sling.Select(zj); - bm.Hud.PressSkill(); - yield return WaitPhase(BattlePhase.PlayerTurn); - bm.ForcePhase(BattlePhase.EnemyTurn); - Assert.IsTrue(bm.Skills.Suppressed(farE), "안개 = 적 전원"); - bm.ForcePhase(BattlePhase.PlayerTurn); - // 적 턴을 실제로 돌리면 아무도 움직이지 않고 · 끝나면 효과가 사라진다 - zf.MarkActed(); - zj.MarkActed(); - Vector2 fp = farE.Position; - yield return bm.RunEnemyTurn(); - Assert.AreEqual(fp, farE.Position, "교전 반응 없음 = 행동 안 함"); - Assert.IsFalse(bm.Skills.Suppressed(farE), "다음 아군 턴 = 해제"); - - // 호위(guard_share) · 철벽(dmg_taken_aura) · 간웅(team_dmg_taken_pct_turn) yield return Fresh(); var gp = Spawn(UnitType.Spear, Faction.Player, 0f, -3f, "관평"); Give(gp, "guanping"); - var gs = Eff("guanping_a", SkillKeys.GuardShare); - var mate = Spawn(UnitType.Infantry, Faction.Player, gs.A2 * 0.8f, -3f, "아군"); - mate.MaxHp = mate.Hp = gp.MaxHp = gp.Hp = 10000; - Spawn(UnitType.Infantry, Faction.Enemy, 0f, 9f, "먼 적"); // 적이 없으면 즉시 발동 뒤 승리 판정 - bm.Sling.Select(gp); - bm.Hud.PressSkill(); - yield return WaitPhase(BattlePhase.PlayerTurn); - int share = Mathf.RoundToInt(100 * Mathf.Clamp01(gs.P)); - Assert.AreEqual(100, SkillSystem.Deal(mate, 100), "아군 턴 = 분담 없음"); - bm.ForcePhase(BattlePhase.EnemyTurn); - int mh = mate.Hp, gh = gp.Hp; - Assert.AreEqual(100 - share, SkillSystem.Deal(mate, 100), "guard_share = 받는 피해 일부를 관평이"); - Assert.AreEqual(gh - share, gp.Hp); - Assert.AreEqual(mh - (100 - share), mate.Hp); - bm.ForcePhase(BattlePhase.PlayerTurn); + var g = Eff("guanping_p", SkillKeys.Guard); + var ally = Spawn(UnitType.Archer, Faction.Player, g.P * 0.5f, -3f, "궁병"); + var far = Spawn(UnitType.Archer, Faction.Player, g.P + 4f, -3f, "먼 궁병"); + Begin(); + int a0 = ally.Hp, g0 = gp.Hp, f0 = far.Hp; + bm.Skills.Hit(ally, 40, null, HitKind.Attack); + Assert.AreEqual(a0 - Mathf.RoundToInt(40 * (1f - Mathf.Clamp01(g.Get(1)))), ally.Hp, "호위 비율만큼 대신"); + Assert.Less(gp.Hp, g0, "관평이 받음"); + bm.Skills.Hit(far, 40, null, HitKind.Attack); + Assert.AreEqual(f0 - 40, far.Hp, "반경 밖 = 그대로"); + } + // ---------- 기병 복귀 · 보병 관통 · 교환 · 아군 통과 ---------- + + [UnityTest] + public IEnumerator Cavalry_ReturnsToOriginOnHit() + { yield return Fresh(); - var cr = Spawn(UnitType.Infantry, Faction.Player, 0f, -3f, "조인"); - Give(cr, "caoren"); - var au = Eff("caoren_a", SkillKeys.DmgTakenAura); - var inR = Spawn(UnitType.Infantry, Faction.Player, au.A2 * 0.8f, -3f, "곁"); - var outR = Spawn(UnitType.Infantry, Faction.Player, au.A2 + 3f, -3f, "먼 아군"); - var hitter = Spawn(UnitType.Infantry, Faction.Enemy, 0f, 8f, "적"); - bm.Sling.Select(cr); - bm.Hud.PressSkill(); - yield return WaitPhase(BattlePhase.PlayerTurn); - Assert.AreEqual(1f, SkillMods.TakenMult(inR, hitter), 1e-5f, "아군 턴 = 아직"); - bm.ForcePhase(BattlePhase.EnemyTurn); - Assert.AreEqual(1f + au.P, SkillMods.TakenMult(inR, hitter), 1e-5f, "dmg_taken_aura 반경 안"); - Assert.AreEqual(1f, SkillMods.TakenMult(outR, hitter), 1e-5f, "반경 밖"); - bm.ForcePhase(BattlePhase.PlayerTurn); + var zy = Spawn(UnitType.Cavalry, Faction.Player, 0f, -4f, "조운"); + Give(zy, "zhaoyun"); + var foe = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -1f, "적"); + Begin(); + Vector2 origin = zy.Position; + var o = new ActionOutcome(); + yield return Act(zy, origin + Vector2.up * zy.SlingMaxDistance * 0.9f, o); + Assert.Contains(foe, o.Attacked); + Assert.IsTrue(o.Returned, "복귀"); + Assert.Less(Vector2.Distance(zy.Position, origin), 0.3f, "출발 위치"); + Assert.AreEqual(0, o.CounterTaken, "반격 안 받음"); + } - var cc = Spawn(UnitType.Infantry, Faction.Player, -6f, -8f, "조조"); + [UnityTest] + public IEnumerator Infantry_PierceThrough_HitsAllOnPath() + { + yield return Fresh(); + var sj = Spawn(UnitType.Infantry, Faction.Player, 0f, -4f, "손견"); + Give(sj, "sunjian"); + var a = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -2.5f, "적1"); + var b = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -1f, "적2"); + Begin(); + var o = new ActionOutcome(); + yield return ActArmed(sj, sj.Position + Vector2.up * sj.SlingMaxDistance * 0.95f, o); + Assert.Contains(a, o.Attacked); + Assert.Contains(b, o.Attacked); + Assert.IsTrue(SkillStatus.Bleeding(a) || !a.IsAlive, "출혈"); + Assert.AreEqual(0, bm.Skills.Hit(sj, 30, null, HitKind.Attack), "행동 뒤 1턴 무적"); + } + + [UnityTest] + public IEnumerator Swap_OnHit_TargetToOrigin() + { + yield return Fresh(); + var wy = Spawn(UnitType.Infantry, Faction.Player, 0f, -4f, "위연"); + var s = Give(wy, "weiyan"); + var foe = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -1f, "적"); + Begin(); + Vector2 origin = wy.Position; + Vector2 foe0 = foe.Position; + var o = new ActionOutcome(); + yield return ActArmed(wy, foe0, o); + Assert.Less(Vector2.Distance(foe.Position, origin), 0.05f, "적 → 출발 위치"); + Assert.Less(Vector2.Distance(wy.Position, foe0), 0.05f, "위연 → 적 자리"); + Assert.AreEqual(Mathf.RoundToInt(Eff("weiyan_a", SkillKeys.ExtraAction).P), s.ReactCredits, "재행동"); + } + + [UnityTest] + public IEnumerator PassAllies_ThenAimPush_NoSelfBounce() + { + yield return Fresh(); + var cc = Spawn(UnitType.Infantry, Faction.Player, 0f, -5f, "조조"); Give(cc, "caocao"); - bm.Sling.Select(cc); - bm.Hud.PressSkill(); - yield return WaitPhase(BattlePhase.PlayerTurn); - bm.ForcePhase(BattlePhase.EnemyTurn); - Assert.AreEqual(1f + Eff("caocao_a", SkillKeys.TeamDmgTakenPctTurn).P, SkillMods.TakenMult(outR, hitter), 1e-5f, "team_dmg_taken_pct_turn"); - bm.ForcePhase(BattlePhase.PlayerTurn); + var ally = Spawn(UnitType.Archer, Faction.Player, 0f, -3.2f, "아군"); + var foe = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -1.4f, "적"); + Begin(); + Vector2 foe0 = foe.Position; + var o = new ActionOutcome(); + yield return ActArmed(cc, cc.Position + Vector2.up * cc.SlingMaxDistance, o); + Assert.Contains(foe, o.Attacked, "아군 통과 → 적 명중"); + Assert.IsFalse(o.Pushed.Contains(ally), "아군은 밀지 않음"); + Assert.Greater(foe.Position.y, foe0.y + 0.05f, "자동 탭 = 슬링 방향으로 밀침"); + } - // 고육계(hp_cost_atk): HP p2 소모 · 이번 턴 공 +p1 · 적 턴 시작에 끝 - yield return Fresh(); - var hg = Spawn(UnitType.Infantry, Faction.Player, 0f, -3f, "황개"); - Give(hg, "huanggai"); - var hc = Eff("huanggai_a", SkillKeys.HpCostAtk); - Spawn(UnitType.Infantry, Faction.Enemy, 0f, 9f, "먼 적"); - int hp0 = hg.Hp; - bm.Sling.Select(hg); - bm.Hud.PressSkill(); - yield return WaitPhase(BattlePhase.PlayerTurn); - Assert.AreEqual(Mathf.Max(1, hp0 - Mathf.RoundToInt(hg.MaxHp * hc.A2)), hg.Hp, "HP 소모"); - Assert.AreEqual(1f + hc.P, SkillMods.AttackMult(hg), 1e-5f, "이번 턴 공 +"); - bm.Skills.OnEnemyTurnStart(); - Assert.AreEqual(1f, SkillMods.AttackMult(hg), 1e-5f, "적 턴 시작 = 끝"); + // ---------- 원거리 특수 사격 ---------- - // 동남풍(sling_dist_pct · p3 1 = 아군 전원 · 이번 턴) · 적토마(p2 절대 가산) · 신산(enemy_aggro_add) + [UnityTest] + public IEnumerator GiantArrow_IgnoresRange_Stuns() + { yield return Fresh(); - var zg = Spawn(UnitType.Strategist, Faction.Player, 0f, -6f, "제갈량"); - Give(zg, "zhugeliang"); - var lb = Spawn(UnitType.Cavalry, Faction.Player, 4f, -6f, "여포"); - Give(lb, "lvbu"); - var foot = Spawn(UnitType.Infantry, Faction.Player, -4f, -6f, "보병"); - var en = Spawn(UnitType.Infantry, Faction.Enemy, 0f, 8f, "적"); - var lbp = Eff("lvbu_p", SkillKeys.SlingDistPct); - Assert.AreEqual(lb.SlingMaxDistance * (1f + lbp.P) + lbp.A2, SkillMods.SlingMax(lb), 1e-4f, "적토마 = 절대 가산"); - Assert.AreEqual(Eff("zhugeliang_p", SkillKeys.EnemyAggroAdd).P, bm.Skills.AggroAdd(Faction.Enemy), 1e-5f, "신산 = 적 교전 범위 +p"); - Assert.AreEqual(0f, bm.Skills.AggroAdd(Faction.Player), 1e-5f); - bm.Sling.Select(zg); - bm.Hud.PressSkill(); - yield return WaitPhase(BattlePhase.PlayerTurn); - var dn = Eff("zhugeliang_a", SkillKeys.SlingDistPct); - Assert.AreEqual(foot.SlingMaxDistance * (1f + dn.P) + dn.A2, SkillMods.SlingMax(foot), 1e-4f, "동남풍 = 아군 전원"); - Assert.AreEqual(en.SlingMaxDistance, SkillMods.SlingMax(en), 1e-4f, "상대 제외"); - bm.Skills.OnEnemyTurnStart(); - Assert.AreEqual(foot.SlingMaxDistance, SkillMods.SlingMax(foot), 1e-4f, "이번 턴만"); + var hz = Spawn(UnitType.Archer, Faction.Player, 0f, -10f, "황충"); + Give(hz, "huangzhong"); + var ally = Spawn(UnitType.Infantry, Faction.Player, 0f, -7f, "아군"); + var foe = Spawn(UnitType.Infantry, Faction.Enemy, 0f, hz.Position.y + SkillMods.Range(hz) * 2f + 2f, "먼 적"); + Begin(); + int ally0 = ally.Hp, foe0 = foe.Hp; + var o = new ActionOutcome(); + yield return ActArmed(hz, hz.Position + Vector2.up * 2f, o); + Assert.AreEqual(ally0, ally.Hp, "아군 통과"); + Assert.Less(foe.Hp, foe0, "사거리 밖 적 명중"); + Assert.IsTrue(SkillStatus.Stunned(foe) || !foe.IsAlive, "기절"); } [UnityTest] - public IEnumerator Instant_Teleport_TapInsideRadius_NoActionUsed_OutsideCancels() + public IEnumerator Fireball_Blast_Burns() { yield return Fresh(); - var zh = Spawn(UnitType.Spear, Faction.Player, 0f, -5f, "장합"); - Give(zh, "zhanghe"); - Spawn(UnitType.Infantry, Faction.Enemy, 0f, 8f, "적"); - float r = Eff("zhanghe_a", SkillKeys.Teleport).P; - bm.Sling.Select(zh); - bm.Hud.PressSkill(); - Assert.AreEqual(zh, bm.Skills.PendingTeleport, "변진 = 지점 탭 대기"); - yield return null; - Assert.IsTrue(bm.Sling.TeleportAreaVisible, "반경 원 표시"); - string msg; - Assert.IsTrue(bm.Skills.TryTeleportTap(zh.Position + new Vector2(r + 1f, 0f), out msg)); - Assert.IsNull(bm.Skills.PendingTeleport); - Assert.AreEqual(new Vector2(0f, -5f), zh.Position, "반경 밖 = 취소"); - Assert.AreEqual(SkillSystem.ActiveState.Ready, bm.Skills.StateOf(zh), "취소 = 쿨 소모 없음"); - bm.Hud.PressSkill(); - Vector2 dest = zh.Position + new Vector2(r * 0.7f, 0f); - Assert.IsTrue(bm.Skills.TryTeleportTap(dest, out msg)); - yield return WaitPhase(BattlePhase.PlayerTurn); - Assert.Less(Vector2.Distance(dest, zh.Position), 0.01f, "teleport = 반경 안 빈 곳으로 이동"); - Assert.IsFalse(zh.ActedThisTurn, "행동 소모 없음"); - Assert.AreEqual(bm.TurnNumber + Cd("zhanghe_a"), bm.Skills.Of(zh).ReadyTurn); + var xs = Spawn(UnitType.Strategist, Faction.Player, 0f, -4f, "서서"); + Give(xs, "xushu"); + var bl = Eff("xushu_a", SkillKeys.ShotBlast); + var a = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -1f, "적1"); + var b = Spawn(UnitType.Infantry, Faction.Enemy, bl.P * 0.6f, -1f, "적2"); + Begin(); + int a0 = a.Hp, b0 = b.Hp; + var o = new ActionOutcome(); + yield return ActArmed(xs, xs.Position + Vector2.up * 2f, o); + Assert.Less(a.Hp, a0); + Assert.Less(b.Hp, b0, "폭발 반경"); + Assert.Greater(bm.Skills.StatusOf(b).BurnTurns, 0, "화상"); } - // ---------- 쿨 · 예약 · 취소 · HUD ---------- + // ---------- 회복 · 정화 ---------- [UnityTest] - public IEnumerator Active_Reserve_Cancel_UseCooldown_HudButton_PassiveLog() + public IEnumerator Instant_HealAndCleanse_InRange() { yield return Fresh(); - var gy = Spawn(UnitType.Spear, Faction.Player, 0f, -3f, "관우"); - Give(gy, "guanyu"); - var other = Spawn(UnitType.Infantry, Faction.Player, -6f, -8f, "보병"); - var foe = Spawn(UnitType.Infantry, Faction.Enemy, 0f, 0f, "적"); - foe.MaxHp = foe.Hp = 10000; - bm.Sling.Select(gy); - yield return null; - Assert.IsTrue(bm.Hud.SkillButtonVisible, "유닛 선택 → 스킬 버튼"); - StringAssert.Contains(SkillCatalog.Get("guanyu_a").NameKo, bm.Hud.SkillButtonText, "이름"); - Assert.AreEqual(SkillSystem.ActiveState.Ready, bm.Skills.StateOf(gy), "전투 시작 = 사용 가능"); - - bm.Hud.PressSkill(); - Assert.IsTrue(bm.Skills.Of(gy).Reserved, "탭 = 예약"); - yield return null; - Assert.IsTrue(gy.View.SkillBadgeVisible, "예약 표시"); - bm.Hud.PressSkill(); - Assert.IsFalse(bm.Skills.Of(gy).Reserved, "다시 탭 = 취소"); - Assert.AreEqual(1, bm.Skills.Of(gy).ReadyTurn, "취소 = 쿨 소모 없음"); - - // 예약 후 쓰지 않고 적 턴 = 예약만 사라짐 - bm.Hud.PressSkill(); - bm.Skills.OnEnemyTurnStart(); - Assert.IsFalse(bm.Skills.Of(gy).Reserved); - Assert.AreEqual(1, bm.Skills.Of(gy).ReadyTurn); - - // 예약 → 실제 행동(RequestPlayerAction) → 쿨 - bm.Sling.Select(gy); - bm.Hud.PressSkill(); - Assert.IsTrue(bm.RequestPlayerAction(gy, foe.Position)); - yield return WaitPhase(BattlePhase.PlayerTurn); - int cd = Cd("guanyu_a"); - Assert.AreEqual(1 + cd, bm.Skills.Of(gy).ReadyTurn, "사용한 턴 + 쿨"); - StringAssert.Contains(SkillCatalog.Get("guanyu_a").NameKo, bm.LastMessage, "행동 문구에 스킬 이름"); - StringAssert.Contains("액티브", string.Join(" ", bm.Skills.RecentLog(30f, 8).ToArray()), "HUD 액티브 한 줄"); - gy.ResetForNewTurn(); - bm.SetTurnForTest(cd); - Assert.AreEqual(SkillSystem.ActiveState.Cooldown, bm.Skills.StateOf(gy), "쿨 중 = 사용 불가"); - Assert.AreEqual(1, bm.Skills.Of(gy).CooldownLeft(bm.TurnNumber)); - bm.Hud.PressSkill(); - Assert.IsFalse(bm.Skills.Of(gy).Reserved, "쿨 중 탭 = 예약 안 됨"); - bm.SetTurnForTest(1 + cd); - Assert.AreEqual(SkillSystem.ActiveState.Ready, bm.Skills.StateOf(gy), "N 턴 뒤 재사용"); - - // 행동 끝난 유닛 = next_action 예약 불가 · 적 유닛 = 버튼은 패시브만 - gy.MarkActed(); - Assert.AreEqual(SkillSystem.ActiveState.Acted, bm.Skills.StateOf(gy)); - var eb = Spawn(UnitType.Infantry, Faction.Enemy, 6f, 6f, "적 무장"); - Give(eb, "caoren"); - Assert.AreEqual(SkillSystem.ActiveState.None, bm.Skills.StateOf(eb), "적 = 액티브 사용 없음"); - - // 패시브 발동 = HUD 한 줄 - Combat.Damage(cfg, foe, eb, false, new System.Random(Seed), true); - StringAssert.Contains(SkillCatalog.Get("caoren_p").NameKo, bm.Skills.Log[bm.Skills.Log.Count - 1].Text, "패시브 발동 한 줄"); - StringAssert.Contains("패시브", bm.Skills.Log[bm.Skills.Log.Count - 1].Text); - bm.Sling.Select(eb); - yield return null; - StringAssert.Contains("패시브", bm.Hud.SkillButtonText); + var zg = Spawn(UnitType.Strategist, Faction.Player, 0f, -3f, "제갈량"); + var s = Give(zg, "zhugeliang"); + var ally = Spawn(UnitType.Infantry, Faction.Player, 1.5f, -3f, "아군"); + var far = Spawn(UnitType.Infantry, Faction.Player, zg.SlingMaxDistance + 4f, -3f, "먼 아군"); + Begin(); + ally.Hp = ally.MaxHp / 3; + far.Hp = far.MaxHp / 3; + var burn = Eff("xushu_a", SkillKeys.HitBurn); + bm.Skills.ApplyBurn(ally, Mathf.RoundToInt(burn.P), burn.Get(1)); + int a0 = ally.Hp, f0 = far.Hp; + yield return bm.Skills.Instant(s); + Assert.Greater(ally.Hp, a0, "범위 안 회복"); + Assert.AreEqual(f0, far.Hp, "범위 밖"); + Assert.IsFalse(SkillStatus.HasAny(ally), "정화"); } - // ---------- ★3 · ★5 · 편성 · 적 무장 ---------- + // ---------- 가상 실행(적 AI 예측) 복원 ---------- [UnityTest] - public IEnumerator Star3ActiveCap_Star5PassiveMult_Lineup_EnemyGeneralPassiveOnly() + public IEnumerator Sim_RestoresSkillState() { - var db = Db; - int capStar = 0, multStar = 0; - foreach (var r in db.Evolve.rows) + yield return Fresh(); + var jx = Spawn(UnitType.Strategist, Faction.Enemy, 0f, 3f, "가후"); + Give(jx, "jiaxu"); + var p = Spawn(UnitType.Infantry, Faction.Player, 0f, 0f, "아군"); + Begin(); + var snap = Sim.Take(bm); + Sim.Active = true; + try { - if (capStar == 0 && SkillRules.ActiveCapBonus(db, r.star) > 0) capStar = r.star; - if (multStar == 0 && SkillRules.PassiveMult(db, r.star) > 1f) multStar = r.star; + var o = new ActionOutcome(); + Sim.Run(bm.Resolver.Resolve(jx, p.Position, false, o)); } - Assert.Greater(capStar, 1, "evolve.csv 액티브 상한 해금 ★"); - Assert.Greater(multStar, 1, "evolve.csv 패시브 배율 ★"); - var save = HeroRules.NewSave(db, cfg.playerSpawnPoints.Length, T0); - var h = db.Heroes.Get(db.Economy.StartHeroes[0]); - var hs = save.Hero(h.id); - hs.star = capStar - 1; - int capBefore = SkillRules.Cap(db, h, hs, 0); - hs.star = capStar; - Assert.AreEqual(Mathf.Min(capBefore + SkillRules.ActiveCapBonus(db, capStar), db.Skills.Get(h.skillActive).lvMax), SkillRules.Cap(db, h, hs, 0), "★3 = 액티브 상한 +"); - Assert.AreEqual(HeroRules.SkillCap(db, hs), SkillRules.Cap(db, h, hs, 1), "패시브 상한 = 등급"); - - var e = new LineupEntry(); - hs.star = multStar - 1; - SkillRules.FillLineup(db, save, h, e); - Assert.AreEqual(1f, e.passiveMult, 1e-5f); - hs.star = multStar; - SkillRules.FillLineup(db, save, h, e); - Assert.AreEqual(SkillRules.PassiveMult(db, multStar), e.passiveMult, 1e-5f, "★5 = 패시브 배율"); - Assert.AreEqual(h.skillActive, e.skillActive); - Assert.IsTrue(e.general); - - // 편성 → 전투 유닛에 스킬(시작 영웅 = 액티브·패시브) · ★5 패시브 수치 × - yield return Fresh(); - var u = Spawn(h.type, Faction.Player, 0f, -6f, h.nameKo); - bm.Skills.AttachLineup(u, e); - var s = bm.Skills.Of(u); - Assert.AreEqual(h.skillActive, s.Active.Id); - var raw = SkillCatalog.Get(h.skillPassive).Effects[0]; - SkillKeys.Info info = SkillKeys.All[raw.Key]; - Assert.AreEqual(SkillKeys.Apply(info.Main, raw.P, e.passiveMult), s.PassiveEffects[0].P, 1e-4f, "★5 패시브 효과 적용"); - - // 적 무장(heroes.csv 에 있는 보스·엘리트) = 패시브만 - int idx = -1; - for (int i = 0; i < cfg.stages.Length && idx < 0; i++) - foreach (var en in cfg.stages[i].enemies) - if ((en.isBoss || en.isGeneral) && SkillCatalog.HeroForEnemy(en.art, en.name) != null) - { - idx = i; - break; - } - Assert.GreaterOrEqual(idx, 0, "영웅 표에 있는 적 무장 스테이지"); - bm.StartStage(idx); - yield return null; - int withPassive = 0; - foreach (var x in bm.Units) + finally { - if (x == null || x.Faction != Faction.Enemy) continue; - var es = bm.Skills.Of(x); - if (es == null || es.Passive == null) continue; - withPassive++; - // PD #1098 확정: 적 액티브 = 1부(1~10장) 보스만(적 AI 가 다음 행동형을 예약해 씀) · 엘리트 무장 = 패시브만 - if (x.IsBoss && TacticalAI.BossUsesActive(bm.Stage, new BalanceConfig.EnemySpawn { isBoss = true })) Assert.IsNotNull(es.Active, "1부 보스 = 액티브도"); - else Assert.IsNull(es.Active, "엘리트 무장 = 액티브 없음"); - Assert.AreEqual(SkillSystem.ActiveState.None, bm.Skills.StateOf(x), "적 액티브는 HUD 버튼 상태 없음(AI 전용)"); - Assert.IsTrue(es.IsGeneral); + Sim.Active = false; + Sim.Restore(bm, snap); } - Assert.Greater(withPassive, 0, "적 무장 패시브 부착"); - int playersWithActive = 0; - foreach (var x in bm.Units) - if (x != null && x.Faction == Faction.Player && bm.Skills.Of(x) != null && bm.Skills.Of(x).Active != null) playersWithActive++; - Assert.AreEqual(bm.Deployed.Count, playersWithActive, "출전 영웅 = 액티브 부착"); - } - - // ---------- 스킬 레벨업 · 스킬서 ---------- - - [Test] - public void SkillLevelUp_BooksGold_Cap_Max_BookShopDaily() - { - var db = Db; - var save = HeroRules.NewSave(db, Resources.Load("BalanceConfig").playerSpawnPoints.Length, T0); - Assert.AreEqual(db.Economy.StartSkillBooks, save.inv.books.skill, "신규 유저 스킬서"); - var h = db.Heroes.Get(db.Economy.StartHeroes[0]); - var hs = save.Hero(h.id); - int cap = SkillRules.Cap(db, h, hs, 1); - Assert.AreEqual(1, SkillRules.Level(hs, 1)); - if (cap <= 1) StringAssert.Contains("상한", SkillRules.CanLevelUp(db, save, h, hs, 1), "상한 = 등급"); - - var cost = db.Skills.CostTo(2); - int books0 = save.inv.books.skill, gold0 = save.gold; - hs.grade = db.Promotion.Get(hs.grade).nextGrade; // 승급 → 상한 +1 - Assert.IsNull(SkillRules.LevelUp(db, save, h, hs, 1)); - Assert.AreEqual(2, SkillRules.Level(hs, 1)); - Assert.AreEqual(books0 - cost.books, save.inv.books.skill, "스킬서 소모"); - Assert.AreEqual(gold0 - cost.gold, save.gold, "골드 소모"); - - save.inv.books.skill = 0; - hs.grade = db.Promotion.Get(hs.grade).nextGrade; - StringAssert.Contains("스킬서", SkillRules.CanLevelUp(db, save, h, hs, 1), "스킬서 부족"); - hs.skillLv[0] = db.Skills.Get(h.skillActive).lvMax; - Assert.AreEqual("최고 레벨", SkillRules.CanLevelUp(db, save, h, hs, 0)); - - // 스킬서 임시 판매: 하루 n 개 · 골드 · 다음 날 다시 - var e = db.Economy; - save.gold = e.SkillBookPrice * (e.SkillBookShopDaily + 2); - int g = save.gold; - for (int i = 0; i < e.SkillBookShopDaily; i++) Assert.IsNull(SkillRules.BuyBook(db, save, T0), "구매 " + i); - Assert.IsNotNull(SkillRules.BuyBook(db, save, T0), "하루 한도"); - Assert.AreEqual(e.SkillBookShopDaily, save.inv.books.skill); - Assert.AreEqual(g - e.SkillBookPrice * e.SkillBookShopDaily, save.gold); - Assert.IsNull(SkillRules.BuyBook(db, save, T0.AddDays(1)), "다음 날 다시"); - } - - [UnityTest] - public IEnumerator HeroDetail_SkillUpButton_OpensModal_LevelsUp_ShopHasBookCell() - { - var og = OutGameRoot.Instance; - Assert.IsNotNull(og); - og.ShowLobby(); - var id = og.Session.Db.Economy.StartHeroes[0]; - og.ShowHero(id); - yield return null; - og.HeroDetail.OnSkillUp(); - Assert.IsTrue(og.ModalVisible, "스킬 레벨업 = 고르기 팝업"); - og.CloseModal(); - var h = og.Session.Def(id); - var hs = og.Session.Save.Hero(id); - int lv0 = SkillRules.Level(hs, 0); - if (SkillRules.CanLevelUp(og.Session.Db, og.Session.Save, h, hs, 0) == null) - { - Assert.IsNull(SkillRules.LevelUp(og.Session.Db, og.Session.Save, h, hs, 0)); - og.Session.Commit(); - Assert.AreEqual(lv0 + 1, og.Session.Save.Hero(id).skillLv[0], "세이브 반영"); - } - og.Trade.OpenShop(); - yield return null; - Assert.IsFalse(og.Trade.SkillBookCellVisible, "스킬서 임시 판매 제거 = 사당 던전(PD #1093 C)"); - Assert.AreEqual(og.Session.Db.Economy.ShopSlots, og.Trade.ShopCardCount, "장비 칸 수는 그대로"); - } - - [UnityTest] - public IEnumerator HeroDetail_Redesign_SkillRow_Swipe_Menu() - { - // UI 상업용 재설계 ① 영웅 상세(#1104): 스킬 행 레벨업 = 그 스킬만 · 일러 밀기 80 px 이상 = 영웅 전환 · 메뉴 진화 = 팝업 · 각성 = 준비 중 - var og = OutGameRoot.Instance; - Assert.IsNotNull(og); - og.ShowLobby(); - var id = og.Session.Db.Economy.StartHeroes[0]; - og.ShowHero(id); - yield return null; - var hd = og.HeroDetail; - var h = og.Session.Def(id); - var hs = og.Session.Save.Hero(id); - int a0 = SkillRules.Level(hs, 0), p0 = SkillRules.Level(hs, 1); - bool can = SkillRules.CanLevelUp(og.Session.Db, og.Session.Save, h, hs, 0) == null; - hd.OnSkillRow(0); - Assert.AreEqual(can ? a0 + 1 : a0, SkillRules.Level(og.Session.Save.Hero(id), 0), "액티브 행 레벨업"); - Assert.AreEqual(p0, SkillRules.Level(og.Session.Save.Hero(id), 1), "패시브는 그대로"); - Assert.IsFalse(og.ModalVisible, "행 레벨업 = 팝업 없이"); - hd.Swipe(-20f); - Assert.AreEqual(id, hd.HeroId, "80 px 미만 = 무시"); - if (og.Session.Owned(false).Count > 1) - { - hd.Swipe(-200f); - Assert.AreNotEqual(id, hd.HeroId, "왼쪽 밀기 = 다음 영웅"); - hd.Swipe(200f); - Assert.AreEqual(id, hd.HeroId, "오른쪽 밀기 = 이전 영웅"); - } - hd.OnMenu(1); - Assert.IsTrue(og.ModalVisible, "진화 = 조각 팝업"); - og.CloseModal(); - hd.OnMenu(2); - Assert.IsTrue(og.ModalVisible, "각성 = 준비 중"); - og.CloseModal(); - } - - // ---------- 영웅 56명 확장(PD #1096) — 신규 키 4종 · 기존 키 조건 열 ---------- - - [UnityTest] - public IEnumerator Expansion_NewKeys_TeamDmg_Cooldown_Heal_Charm() - { - // 십승십패(team_dmg_pct_turn · 즉시): 이번 아군 턴 아군 전원 가하는 피해 × (1 + p) · 귀재(cooldown_add 패시브 · 자기) = 쿨 + p(최소 1) - yield return Fresh(); - var gj = Spawn(UnitType.Strategist, Faction.Player, -6f, -8f, "곽가"); - var gs = Give(gj, "guojia"); - var ally = Spawn(UnitType.Infantry, Faction.Player, 0f, -2f, "보병"); - var foe = Spawn(UnitType.Infantry, Faction.Enemy, 6f, 6f, "적"); - float before = SkillMods.DealtMult(ally, foe); - yield return bm.Skills.Instant(gs); - Assert.AreEqual(before * (1f + Eff("guojia_a", SkillKeys.TeamDmgPctTurn).P), SkillMods.DealtMult(ally, foe), 1e-4f, "team_dmg_pct_turn = 아군 전원"); - Assert.AreEqual(1f, SkillMods.DealtMult(foe, ally), 1e-4f, "상대 = 미적용"); - Assert.AreEqual(Mathf.Max(1, Cd("guojia_a") + Mathf.RoundToInt(Eff("guojia_p", SkillKeys.CooldownAdd).P)), gs.ReadyTurn - bm.TurnNumber, "cooldown_add 자기 쿨"); - bm.Skills.OnEnemyTurnStart(); - Assert.AreEqual(before, SkillMods.DealtMult(ally, foe), 1e-4f, "적 턴 시작 = 해제"); - - // 금슬(cooldown_add 즉시 · p2 1 = 아군 전원): 다른 아군의 남은 쿨 + p(이번 턴 아래로는 안 감 · 시전자 제외) - yield return Fresh(); - var xq = Spawn(UnitType.Strategist, Faction.Player, -6f, -8f, "소교"); - var xs = Give(xq, "xiaoqiao"); - var zy = Spawn(UnitType.Cavalry, Faction.Player, 0f, -2f, "조운"); - var zs = Give(zy, "zhaoyun"); - zs.ReadyTurn = bm.TurnNumber + 3; - yield return bm.Skills.Instant(xs); - Assert.AreEqual(bm.TurnNumber + 3 + Mathf.RoundToInt(Eff("xiaoqiao_a", SkillKeys.CooldownAdd).P), zs.ReadyTurn, "아군 쿨 감소"); - Assert.AreEqual(bm.TurnNumber + Cd("xiaoqiao_a"), xs.ReadyTurn, "시전자 쿨 = 표 그대로"); - - // 꿀물(heal_self_pct) - yield return Fresh(); - var ys = Spawn(UnitType.Infantry, Faction.Player, 0f, -4f, "원술"); - var yss = Give(ys, "yuanshu"); - ys.Hp = 1; - yield return bm.Skills.Instant(yss); - Assert.AreEqual(Mathf.Min(ys.MaxHp, 1 + Mathf.RoundToInt(ys.MaxHp * Eff("yuanshu_a", SkillKeys.HealSelfPct).P)), ys.Hp, "heal_self_pct"); - - // 연환계(charm): 반경 p1 안 상대 무장 우선 1기 → p2 적 턴 동안 교전 반응 없음 - yield return Fresh(); - var dc = Spawn(UnitType.Strategist, Faction.Player, 0f, -4f, "초선"); - var dcs = Give(dc, "diaochan"); - var ce = Eff("diaochan_a", SkillKeys.Charm); - var soldier = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -4f + ce.P * 0.4f, "병사"); - var general = Spawn(UnitType.Infantry, Faction.Enemy, 1f, -4f + ce.P * 0.8f, "무장"); - bm.Skills.MarkGeneral(general); - yield return bm.Skills.Instant(dcs); - bm.ForcePhase(BattlePhase.EnemyTurn); - Assert.IsTrue(bm.Skills.Suppressed(general), "무장 우선 매혹"); - Assert.IsFalse(bm.Skills.Suppressed(soldier), "1기만"); - for (int i = 0; i < Mathf.RoundToInt(ce.A2); i++) bm.Skills.OnPlayerTurnStart(); - Assert.IsFalse(bm.Skills.Suppressed(general), "p2 적 턴 뒤 해제"); - } - - [UnityTest] - public IEnumerator Expansion_ConditionColumns_TakenHpTurnCounterTeam() - { - // 받는 피해 조건: 위연(p2 = HP 임계) · 원술(p2 2 = 보스에게서만) · 초선(p2 3 = 근접에게서만) - yield return Fresh(); - var wy = Spawn(UnitType.Infantry, Faction.Player, -4f, -8f, "위연"); - Give(wy, "weiyan"); - var atk = Spawn(UnitType.Infantry, Faction.Enemy, 4f, 8f, "적"); - Assert.AreEqual(1f, SkillMods.TakenMult(wy, atk), 1e-5f, "HP 가득 = 미적용"); - var we = Eff("weiyan_p", SkillKeys.DmgTakenPct); - wy.Hp = Mathf.FloorToInt(wy.MaxHp * we.A2); - Assert.AreEqual(1f + we.P, SkillMods.TakenMult(wy, atk), 1e-5f, "HP 임계 이하 = 적용"); - - yield return Fresh(); - var yu = Spawn(UnitType.Infantry, Faction.Player, -4f, -8f, "원술"); - Give(yu, "yuanshu"); - var boss = Spawn(UnitType.Infantry, Faction.Enemy, 4f, 8f, "보스", true); - var grunt = Spawn(UnitType.Infantry, Faction.Enemy, -4f, 8f, "병사"); - Assert.AreEqual(1f + Eff("yuanshu_p", SkillKeys.DmgTakenPct).P, SkillMods.TakenMult(yu, boss), 1e-5f, "보스에게"); - Assert.AreEqual(1f, SkillMods.TakenMult(yu, grunt), 1e-5f, "병사에게 = 미적용"); - - yield return Fresh(); - var dch = Spawn(UnitType.Strategist, Faction.Player, -4f, -8f, "초선"); - Give(dch, "diaochan"); - var mel = Spawn(UnitType.Cavalry, Faction.Enemy, 4f, 8f, "기병"); - var arc = Spawn(UnitType.Archer, Faction.Enemy, -4f, 8f, "궁병"); - Assert.AreEqual(1f + Eff("diaochan_p", SkillKeys.DmgTakenPct).P, SkillMods.TakenMult(dch, mel), 1e-5f, "근접에게"); - Assert.AreEqual(1f, SkillMods.TakenMult(dch, arc), 1e-5f, "궁병에게 = 미적용"); - - // 원소(p3 = 출전 n 인 이상 · 아군 전원) - var yp = Eff("yuanshao_p", SkillKeys.DmgTakenPct); - int need = Mathf.RoundToInt(yp.A3); - yield return Fresh(); - var ysh = Spawn(UnitType.Archer, Faction.Player, -6f, -8f, "원소"); - Give(ysh, "yuanshao"); - var mates = new List(); - for (int i = 1; i < need; i++) mates.Add(Spawn(UnitType.Infantry, Faction.Player, -6f + i * 2.5f, -10f, "병" + i)); - var en = Spawn(UnitType.Infantry, Faction.Enemy, 0f, 8f, "적"); - bm.Skills.BeginBattle(); - Assert.AreEqual(1f + yp.P, SkillMods.TakenMult(mates[0], en), 1e-5f, "출전 " + need + "인 = 아군 전원"); - Assert.AreEqual(1f, SkillMods.TakenMult(en, mates[0]), 1e-5f, "상대 = 미적용"); - yield return Fresh(); - var en2 = Spawn(UnitType.Infantry, Faction.Enemy, 0f, 8f, "적"); - var solo = Spawn(UnitType.Archer, Faction.Player, -6f, -8f, "원소"); - Give(solo, "yuanshao"); - bm.Skills.BeginBattle(); - Assert.AreEqual(1f, SkillMods.TakenMult(solo, en2), 1e-5f, "출전 인원 미달 = 미적용"); - - // 순욱 패시브(hp_max_pct p2 1 = 아군 전원 · 배치 뒤) · 액티브(counter_ratio p3 1 = 아군 전원 · 이번 적 턴) - yield return Fresh(); - var xy = Spawn(UnitType.Strategist, Faction.Player, -6f, -8f, "순욱"); - var xys = Give(xy, "xunyu"); - var mate = Spawn(UnitType.Infantry, Faction.Player, 0f, -8f, "보병"); - var foe = Spawn(UnitType.Infantry, Faction.Enemy, 0f, 8f, "적"); - int m0 = mate.MaxHp, x0 = xy.MaxHp, f0 = foe.MaxHp; - bm.Skills.BeginBattle(); - float hp = Eff("xunyu_p", SkillKeys.HpMaxPct).P; - Assert.AreEqual(Mathf.RoundToInt(m0 * (1f + hp)), mate.MaxHp, "아군 전원 최대 HP"); - Assert.AreEqual(Mathf.RoundToInt(x0 * (1f + hp)), xy.MaxHp, "자기 포함"); - Assert.AreEqual(f0, foe.MaxHp, "상대 = 미적용"); - yield return bm.Skills.Instant(xys); - bm.ForcePhase(BattlePhase.EnemyTurn); - Assert.AreEqual(Eff("xunyu_a", SkillKeys.CounterRatio).P, SkillMods.CounterRatio(mate, cfg.counterRatio, false), 1e-5f, "아군 전원 반격 배율"); - Assert.AreEqual(cfg.counterRatio, SkillMods.CounterRatio(foe, cfg.counterRatio, false), 1e-5f, "상대 = 기본"); - - // 여몽 패시브(dmg_pct p2 = 발동 턴 이상) - yield return Fresh(); - var lm = Spawn(UnitType.Infantry, Faction.Player, -4f, -8f, "여몽"); - Give(lm, "lvmeng"); - var tg = Spawn(UnitType.Infantry, Faction.Enemy, 4f, 8f, "적"); - var lp = Eff("lvmeng_p", SkillKeys.DmgPct); - bm.SetTurnForTest(Mathf.RoundToInt(lp.A2) - 1); - Assert.AreEqual(1f, SkillMods.DealtMult(lm, tg), 1e-5f, "발동 턴 전"); - bm.SetTurnForTest(Mathf.RoundToInt(lp.A2)); - Assert.AreEqual(1f + lp.P, SkillMods.DealtMult(lm, tg), 1e-5f, "발동 턴부터"); + Assert.IsFalse(SkillStatus.HasAny(p), "가상 실행 중 걸린 중독이 남지 않음"); + Assert.AreEqual(p.MaxHp, p.Hp); } } } diff --git a/CLAUDE.md b/CLAUDE.md index 8e7ca8a7..e2bb5216 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,9 +45,9 @@ Unity.exe -projectPath -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 `. +- **스킬(2026-11-10 #1112 전면 개편 · PD 엑셀 「장수 목록」 새 기획 144개)** — 수치 SOT = **`Assets/Data/OutGame/skill_effects.csv`(효과 1줄 = `skill_id · key · target · cond · dur · p1~p3` + 한글 주석 열 `effect_ko · p1_ko~p3_ko` = 밸런싱용 · 게임은 안 읽음)** · `skills.csv` = 이름·발동(next_action|instant|always)·쿨·설명 · `effect_key` = 효과 키 목록(p 칸 비움) · `heroes.csv gender`(m|f = 남성·여성 조건). 원본 = 기획 `tables/`(생성기 `_tables_t1.py` SKILL_EFFECTS · KEY_DOC 주석 사전 · 이름·설명 = `영웅_리스트_스킬.md` · 실행 뒤 `economy.csv`·`gacha.csv` 는 git 되돌림 = 생성기보다 손으로 늘린 판이 최신). 처리기 = `Runtime/Skills/`: `SkillKeys`(121종 · 레벨 계수 칸) · `SkillMods`(대상 칸 자신/allies/camp/radius/range/aura · 조건 칸 crit/vs_*/turn_le/armed · 공격·피해·치명타·반격·슬링 질의) · `SkillSystem`(쿨·예약 · 시한 효과 = 라운드(주인 진영 턴 시작마다 −1) · 피해 적용 `Hit(d, dmg, by, HitKind)` = 무적→무효화→호위→부활→흡혈·명중 상태 이상→처치 보상 · 재행동 `ReactCredits` · 즉시형 · `TurnBegin(side)` = 상대 턴 끝 회복·정화 + 중독·화상·회복·지대 · 행동 중 탭 `WaitTap`(조조·하후돈 · 테스트 `AutoTap`) · 지대 `SkillZone`) · `SkillStatus`(혼란·출혈·중독·화상 · 기절 = 기믹 `GimmickLayer.Stun` 재사용) · 특수 이동 = `ActionResolver`(관통·핀볼·복귀·교환·끌어오기·난동·반복) · 특수 사격 = `ActionResolver.Skills.cs`. 가상 실행 = `SkillSystem.TakeState/RestoreState`(Sim). 「1 m」 = 전장 1 칸 · 「행동 반경」 = 기본 슬링 거리(PD 확인 대상). 테스트 = `Tests/PlayMode/SkillTests`(효과 묶음 19). 캡처 = `-executeMethod TKS.EditorTools.DataImporter.CaptureSkills -skillDir `. - **아웃게임 2차(2026-11-05~06 #1093 C)** — 표 10장 추가(`training` · `training_ranks` · `patrol` · `dev_tree` · `treasures` · `treasure_sets` · `chests`(+ iron/buy 열) · `tower` · `dungeons`(+ reward_amount) · `arena`(+ 상점 키)) + `economy.csv` C 키 24 → 임포트 자산 18(A 9 + C 9 · `OutGameTables.C.cs` 검증). 코드 = `OutGame/Domestic/`(`TrainingRules` 훈련 · `PatrolRules` 순찰 · `DevRules` 개발 효과 키 → 적용 지점 · `DomesticScreen`) · `OutGame/Treasure/`(`TreasureRules` 보유·세트·강화·조건 카운터·상자 · `TreasureScreen`) · `OutGame/Arena/`(`ArenaRules` 탑·던전·투기장·적장 · `ArenaScreen`). 스탯 = §3-2 × (1 + 훈련(병종·항목)) × (1 + 보물(항목)) = `HeroRules.Compute(…, save)` · 보물 crit/range/splash = `BattleManager.SideEffectsProvider` → `SkillMods.Get` 진영 효과(`side_crit_pct` · `range_mult` · `splash_radius_mult`) · 조건 효과 카운터 = `Runtime/BattleEvents.Raise`(전투 코드 1줄씩 · 아군만) · 연무장 전투 = `BattleManager.StartMode(BattleMode)`(적 편성 · 제한 턴 · 결과 탭 = `onExit`) → `OutGameSession.OnModeEnd`. 테스트 = `Tests/PlayMode/OutGameCTests`. 캡처 = `-executeMethod TKS.EditorTools.DataImporter.CaptureC -cDir `(사본 · 10장). -- **영웅 56명(PD #1096 · 2026-11-06)** — heroes 71행(영웅 56 + 병사 15) · skills 112 · hero_gear 1344 · 신규 effect_key 4(`team_dmg_pct_turn` · `cooldown_add` · `charm` · `heal_self_pct`) + 기존 키 조건 열(받는 피해 p2 = HP 임계/2 보스/3 근접 · p3 = 출전 인원 아군 전원 · 최대 HP·반격 아군 전원 · 피해 % 발동 턴) = `SkillMods`·`SkillSystem` · 대장군 `pushed_ally_attack_on_push` 는 표에서 미사용(처리기 유지). +- **영웅 72명(PD #1108 · 2026-11-10)** — heroes 87행(영웅 72 + 병사 15) · skills 144 · skill_effects 288 · 옛 키 체계(51+4종 · 견제·관통 배율 등)는 #1112 에서 폐기. - **지형 기믹 레이어(PD #1097 · 2026-11-06 세션 D)** — SOT = `Assets/Data/Gimmicks/{gimmicks,stage_gimmicks,tutorial_tips}.csv`(기획 `tables/` 사본 · utf-8-sig) → 메뉴 `TKS/Import Gimmicks (기믹 임포트)`(`Editor/GimmickImporter` · 오류 시 자산 불변 · `ai_weights.csv` 도 함께) → `Resources/Gimmicks/GimmickDb`·`StageGimmickTable` · `Resources/AI/AiWeights`. 전투 시작 = `GimmickLayer.Build(stage)`(장-번호 배치 · 배경 위 · 유닛 아래 · 장애물 = 콜라이더 + `GimmickBody` · 지대 = 모양 스윕 · 강 = 늪처럼 통과하는 지대(강 안 ×p3 · 다리 구간 = 효과 없음 · 강가·다리에서 밀려 강물에 들면 빠짐 · PD 2026-11-06 개정) · 겹친 유닛은 빈 자리로). 전투 코드는 질의만: `Combat.FreeTravel`(장애물 = 벽 · 숨은 함정 정지) · `FirstUnitOnArrow`(pass/block/destroy) · `Combat.Damage`(군기 공 · 고지 근접 피격 · 안개 원거리 피격 ×p1 · 책사 광역이 그 반경 안개를 걷음) · `SkillMods.Range`(고지 사거리) · `BattleManager` 턴 시작 `TurnStart`(군기·중독·불·행동 불가). 충돌 반응·폭발·굴림·미끄러짐·지대 = `Gimmicks/ActionResolver.Gimmicks.cs`(partial). 배치 도구 = `TKS/Gimmick Painter (기믹 배치)`. 테스트 = `Tests/PlayMode/GimmickTests` · 캡처 = `-executeMethod TKS.EditorTools.GimmickImporter.Capture -gimmickDir `(사본). - **병종 규칙 개정 + 예측 AI(PD #1098 · 2026-11-06)** — 보병 = 적과 싸운 뒤 튕겨나는 길의 다른 적과 연쇄 교환 1회(`ActionOutcome.Chains`) · 창병 = 상대만 밀리고 자신은 × `spearRecoilRatio` 반대로(밀며 전진 폐기). 적 턴 = `Runtime/AI/TacticalAI`(후보 방위 × 파워 3 × 액티브 → `Sim` 가상 실행(연출 없음 · 분산 0 · 상태 복원) → `ai_weights.csv` 점수 · 서포터 → 돌격 → 사격 · 1기마다 재평가) · 가중치 자산이 없으면 `EnemyAI.Decide`(기존 조준) · 적 액티브 = 1부 보스만(`boss_active`). 가상 실행 중(`Sim.Active`)에는 `Fx`·`UnitView`·`BattleEvents`·HUD 갱신이 꺼진다 — 새 연출 코드도 `Sim.Active` 면 건너뛸 것. 조준 미리보기도 같은 가상 실행(`GimmickPreview` · 숨은 함정 모름). 테스트 = `Tests/PlayMode/TacticalAITests`. - 아웃게임 코드 = `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 `. **UI 수정 검증은 고친 화면만 찍는다**(PD 2026-11-09 「매번 모든 화면 체크 = 시간·토큰 낭비」): `Tools/mirror_verify.py` → `Unity.exe -projectPath E:/Nerdnavis/_tks_verify -force-d3d11 -executeMethod TKS.EditorTools.DataImporter.CaptureUI -uiDir -uiOnly hero_list,hero_detail*`(이름 = 정확히 또는 「접두*」 · 다 찍으면 바로 종료 · 1장 ≈ 15초 · 전체 21장 ≈ 3분 · 이름 목록 = `UICaptureDriver` 의 Shot 이름). @@ -84,7 +84,7 @@ UI 를 새로 만들거나 고칠 때 **반드시** 이 구조를 따른다(어 - **IB(쿠로네코) 이펙트 시험(현재 기본 공격 · PD 2026-11-10 「내가 원한 비주얼은 이런게 아니야(이노센트베인 영상) → 우선 IB 이펙트를 그대로 써서 확인 · 나중에 구조에 맞게 새로 그림(화풍 통일)」)**: IB 슬링샷 발사 `UnitVisualEffectControl.ShowBoosterFX()` 와 같은 구성 — `ef_booster`(앞 빛 물결 2장 + 가시 · 0.2초 반복 맥동 · 유닛 위 · 이동 끝에 바로 지움 = IB Breaking 때 Kill) · `ef_speed_tail`(꼬리 0.6초 + 화살표 입자 · 유닛 아래 · 끝나면 꼬리가 줄며 사라짐 — `SkillVfx.Stop` 이 트레일 길이만큼 기다림) · `ef_booster_dust`(출발 먼지 1번) · 타격 `ef_normal_hit`. 원본 = `Assets/VFX/IB/`(쿠로네코 `E:\NerdNavis\Kuroneko\KuronekoClient` 에서 `Tools/wl_vfx_import.py copy … --wl <쿠로네코> --dest Assets/VFX/IB --skip-scripts` · GUID 유지 · 무수정) · 사본 = `Resources/Fx/TKS_Fx_IB_*`(`Tools/make_ib_fx_copies.cs` — IB 전용 스크립트 부품 제거 · 정렬 레이어 Default · 방향 있는 것은 감싸는 루트 + 안쪽 Z −90°(IB 정면 +Y → 이 게임 +X) · 소리 이벤트 받는 곳 `IbFxAnimEvents`(아무것도 안 함 · 없으면 재생마다 콘솔 오류)). 셰이더 = `Fingar/Additive`(더하기 섞기 · 빌트인 그대로). 2D 돌진 3차 행은 표에 `#` 주석(되돌리기). **진영색(PD 「진영 색을 입히고」)** = `TKS_Fx_IB_Booster_<진영>`(스프라이트 색의 색상만 진영색 · 채도·밝기 유지) · `TKS_Fx_IB_SpeedTail_<진영>`(꼬리 그림 `Assets/VFX/IB/_TKS/Tail_Basic02_<진영>.png` = `Tools/make_ib_camp_textures.py` · 재질 같은 폴더 · 화살표 입자 색) — 둘 다 `make_ib_fx_copies.cs` 가 `Tools/dash2d_camps.csv` 로 만듦 · 원색 사본 `TKS_Fx_IB_Booster` · `SpeedTail` 은 비교용으로 남음(표에서 안 씀). **사라지는 시점(PD 「멈춘 이후 뒤늦게 이펙트가 사라짐 · 위치 안 맞음」)** = 속도 꼬리 `f_fade` 0.12 — 멈춘 자리에서 꼬리째 흐려짐(`SkillVfxFade` 가 트레일 색 그러데이션 불투명도도 낮춤 · 전에는 0.6초 꼬리 머리가 기병 돌격 끝자리에 남아 복귀 중에도 보였음). **타격** = 현재 없음(PD 2026-11-10 「적이 피격되었을 때 히트 이펙트는 제거 · 어떤 히트 이펙트를 쓸지 알려줄게」 · IB NormalHit · BounceHit 행은 표에 `#` 주석). **기준점 `e_anchor` = dash**(빛 물결 · 꼬리 · PD 「복귀할 때 이펙트가 캐릭터보다 위쪽에 생겨 안 보임 → 유닛 바닥에」) = 위·옆으로 갈 때 몸 가운데 · 아래로 갈수록 발 쪽(곧장 아래 = 발). **흙먼지** = WL 창 돌진 흙먼지 `TKS_Fx_SpearRushDust`(0.15초마다 발밑 · PD 「복귀할 때 흙먼지가 너무 세서 뭉개짐 → WL 흙먼지로」 · IB 출발 먼지 행은 `#` 주석). - **방향 결정 UI = IB 이식(PD 2026-11-10 「IB 와 같이 슬링샷 조작 시 방향 결정 UI 등을 그대로 이식」)**: `TKS.IbAimArrow`(IB `GameTouchArrow` 동작 · 프리팹 `Resources/Fx/TKS_Fx_IB_GameArrow` = 원본 `Assets/VFX/IB/AssetResources/Game/GameArrow` 사본) — 누르면 유닛 둘레 고리 + 퍼지는 고리 3개(3초 · 1초 간격) · 당기면 하늘색 화살표가 쏠 방향으로 돌고 당긴 세기만큼 길어짐(0.5~1.5 · 최소 당김 → 슬링샷 최대 거리) · 화살 끝이 전장 밖이면 안으로 · 놓으면 숨김. 전에 쓰던 조준선 · 당김선은 끔(`SlingshotInput.ShowPreview` · IbAimArrow 가 없을 때만 예전 선). 목표 고리 · 강조 · 궁병 화살 경로선 · 기믹 예상 궤적은 그대로. 화살표 색 = IB 공격 종류 3(하늘색) 하나. - **여포 크기(PD 2026-11-10 「왜 여포는 다른 기병보다 작지?」)**: 말 탄 유닛은 머리 끝 상한(BalanceConfig.mountedArtMaxTop 1.3 × 표준 키)에 맞춰 줄어드는데, `dz_boss_lvbu` 의 몸 높이(`meta.json` bodyHeightPx · `art_size.csv` body_px)가 머리 깃털 끝까지 819 px 라 몸 전체가 더 줄었음 → 왕관 끝 ~ 발 718 px(front_idle 실측 · 깃털 제외 · 다른 기병의 투구 끝 기준과 같은 규칙)로 고침 → 배율 0.2381 → 0.2716(+14 %). 원본 백업 = 세션 scratchpad. **다른 PC 에서 그림을 다시 export 하면 819 로 되돌아갈 수 있음**(export 규칙 = 불투명 폭 30 % 행 — 깃털이 넓어서 안 걸러짐). -- **근접 3병종 특징 = 한 줄 정의(PD 2026-11-10 직접 지시)**: **보병** = 적을 밀치고 튕겨나며 튕기면서 다른 적을 추가로 공격(알까기 + 연쇄 · #1098 그대로 · 목표점 명중 추가 이동 1회는 유지 = PD 확인 대상) · **창병** = 적을 크게 밀치지만 자신은 적게 밀림(`spearPushRatio` 1.5 > 보병 `collisionPushRatio` 1.0 · 반작용 남은 거리 × 0.3 < 보병 자기 튕김 0.5) · **창병 대기(견제) 제거**(「유저가 인지하기 어려움」 · 목표점 명중해도 `SetOverwatch` 안 함 · HUD 「견제 반경·대기 중」 문구 삭제) · **기병** = 적을 관통해 이동 · **목표점 명중 시 제자리 복귀 제거**(「유저가 인지하기 어려움」 · `PreviewCavalryReturn` · 조준 때 복귀 영역 `ShowReturnZones` 삭제 · AI 가중치 `cav_return`·`cav_stranded` 삭제). 견제·복귀 스킬(청룡언월 · 후계 · 구벌중원 · 기린아 · 교변 · 삼대 노신 · 유수의 복수 · 장사 · 백마의 창 · 하북 명장 · 남만의 여왕 · 장사 태수 · 단기구주 · 도주 본능 · 소패왕) = 발동 안 함 · **재설계 PD 결정 대기** → 그때까지 `BattleManager.ApplyOverwatch` · `Unit.Overwatch` · `SkillMods.Overwatch*` 는 남겨 둠(대기 유닛이 없어 실행 안 됨) · `SkillTests.Spear_OverwatchSkills_*` = [Ignore]. +- **근접 3병종 특징 = 한 줄 정의(PD 2026-11-10 직접 지시)**: **보병** = 적을 밀치고 튕겨나며 튕기면서 다른 적을 추가로 공격(알까기 + 연쇄 · #1098 그대로 · 목표점 명중 추가 이동 1회는 유지 = PD 확인 대상) · **창병** = 적을 크게 밀치지만 자신은 적게 밀림(`spearPushRatio` 1.5 > 보병 `collisionPushRatio` 1.0 · 반작용 남은 거리 × 0.3 < 보병 자기 튕김 0.5) · **창병 대기(견제) 제거**(「유저가 인지하기 어려움」 · 목표점 명중해도 `SetOverwatch` 안 함 · HUD 「견제 반경·대기 중」 문구 삭제) · **기병** = 적을 관통해 이동 · **목표점 명중 시 제자리 복귀 제거**(「유저가 인지하기 어려움」 · `PreviewCavalryReturn` · 조준 때 복귀 영역 `ShowReturnZones` 삭제 · AI 가중치 `cav_return`·`cav_stranded` 삭제). 견제·복귀 스킬 = #1112 PD 새 기획으로 전부 재설계(견제 없음 · 복귀는 스킬 효과 `return_on_hit`·`return_always`·`return_aim` 으로만) · `SkillMods.Overwatch*` 삭제 · `BattleManager.ApplyOverwatch` · `Unit.Overwatch` 는 스킬 확장 없이 남음(대기 유닛이 없어 실행 안 됨). - **IB 크기 · 캐릭터 연출(PD 2026-11-10 「IB 와 화살표 크기도 다르고 선택된 캐릭터 주위 연출도 누락」)**: 값 표 `Resources/Skills/ib_ui.csv`(`TKS.IbUi`) — IB 유닛 반지름 0.496(ExportCharacter size 8 × 0.062) 기준 배율 = 이 게임 유닛 반지름 / 0.496 → 고리 지름 = 반지름 × 5.4 · 화살표 세기 s = IB 월드 당김 × 0.6(0.5~1.5) · IB 취소 거리 0.5. **`TKS.IbUnitMarks`** = 선택한 행동 가능 아군 발밑 도는 원(`TKS_Fx_IB_TargetCircle` · IB ShowUserTurnMark) + 몸 둘레 빛(`TKS_Fx_IB_AttackReady`) · 조준 중(누름 ~ 놓음) 모든 유닛 속성 방울(`TKS_Fx_IB_Attribute_<속성>` · IB SetActiveBarrierVisualEffect · 진영 → 속성 = 표 element_*: 위 물 · 촉 나무 · 오 불 · 군·진영 없음 어둠). - **타격감(PD 2026-11-10 「화면 흔들림 등도 IB 를 참고해서」)**: `TKS.HitFeel` + 표 `Resources/Skills/hit_feel.csv`(행동 중 유닛이 상대에게 처음 피해를 준 순간 · `SkillSystem.Hit` · 흔들림 = `CameraController.Shake`(추적 위치는 그대로 두고 흔들림만 더함) · 히트 스톱 칸 있음(현재 0)). **IB 실측**: 흔들림 함수 6종은 스킬·궁극기·기믹 이펙트 애니메이션 이벤트로만 불리고 본문이 디컴파일에서 비어 있어 값 없음 · 일반 타격·벽 튕김엔 흔들림 없음 · 히트 스톱 없음 → 표 값(hit 0.15초 · 키 10 % / 기병 0.18초 · 13 %)은 추정. - **2D 돌진(PD 2026-11-10 「기본 공격(돌진) 이펙트가 너무 3D 느낌 → 2D 처럼」 · MSFX 혜성 행은 표에 `#` 주석으로 남김 = 되돌리기)**: **3차(현재 · PD 2026-11-10 「검기는 이전 형태가 더 나음 · 대쉬 이펙트는 캐릭터 몸을 벗어나지 않도록 · 첨부 같은 느낌으로 몸을 뒤덮는 연출을 스킬용 시험」)** = 초승달 `Head` = 1차 그림 `dash2d_crescent`(검정 외곽선 · 진영색 곱) · 대쉬 층(꼬리 0.04초 · 바람 줄기 · 알갱이 · 흙먼지) = 유닛을 따라 움직이는 Local 공간 · 몸 기준점 반경 0.45 키 안(실측 그림 경계 좌우 ±0.56 · 아래 −0.60 ~ 위 +0.59 키) · 바람 줄기 `Lines` · 알갱이 `Dots` = 유닛 위 반투명(표 `s_over` = `Head;Lines;Dots` · 유닛 아래면 몸에 다 가려 안 보임 · PD 「몸의 대쉬 효과가 왜 안 보이나」) · 꼬리 · 흙먼지 = 유닛 아래 · **기병 복귀 = 돌아서서 감**(`UnitView.TurnAround` · 복귀 방향으로 보기 + 곧장 위·아래면 좌우 반전 · 도착 뒤에도 마지막 이동 방향 PD #1082) · **몸 뒤덮는 기운(시험)** = `TKS_Fx_Aura2D_<진영>`(혜성 기운 `aura_comet_<진영>` 겹쳐 깜빡임 · 불꽃 혀 `aura_wisp` · 반짝임 = 유닛 아래 · 옅은 막 `Veil` · 앞 테 `Rim`(2차 검기 그림) = 유닛 위) · 표 키 `test_aura` · 테스트 씬 「기운 시험」 스위치(`SkillVfx.TestTravelKey` · 켜면 슬링샷 공격 돌진에 함께 · 실제 전투 = 늘 빈칸). 2차 = 외곽선 없음 · 그림 `Assets/VFX/TKS2D/`(만들기 = `python -I -X utf8 Tools/make_dash2d_textures.py` · 진영색 원본 = `Tools/dash2d_camps.csv`) → 프리팹 `Resources/Fx/TKS_Fx_Dash2D_`(검기 초승달 `Head` = 진영별 그림 `dash2d_slash_<진영>` · 유닛 위 · 반투명 꼬리 `Trail`(앞 0.7 → 0) · 흰 바람 줄기 `Lines` · 알갱이 `Dots` = 유닛 아래 · 수명 동안 투명해짐) + `TKS_Fx_Dust2D`(부드러운 반투명 흙먼지 · 모든 진영) · 다시 만들기 = `unity command … script eval --file Tools/build_dash2d.cs`(값을 바꾸면 이 파일과 프리팹을 함께). 1차(검정 외곽선 셀 그림)의 `dash2d_crescent.png` · `dash2d_highlight.png` 는 안 씀. 표 `b_flat` = 1 이면 화면 안에서 진행 방향으로만 돌림(기울기·팩 루트 회전 무시 · `SkillVfx.FlatRotation`) · 크기 단위 = 유닛 키 1(`f_scale` 1) · 끝 소멸 = `f_fade` 0.12(`SkillVfxFade` 가 스프라이트 불투명도도 낮춤).