diff --git a/.gitignore b/.gitignore index ca3fa542..3d527200 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Compile output build/ *.o +zh_CN.qm # IDE & LSP .kdev4/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 45f49348..1b252f9f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ find_package(Qt6 REQUIRED COMPONENTS Network Multimedia QuickControls2 + LinguistTools ) find_package(OpenSSL) @@ -56,6 +57,10 @@ add_custom_command( ) qt_add_executable(FreeKill) +qt_add_translations(FreeKill + TS_FILES lang/zh_CN.ts + QM_FILES_OUTPUT_VARIABLE zh_CN.qm +) if (${CMAKE_SYSTEM_NAME} MATCHES "Emscripten") file(GLOB_RECURSE FK_RESOURCE_FILES diff --git a/android/copy_assets.sh b/android/copy_assets.sh index 91f5e059..124182f5 100755 --- a/android/copy_assets.sh +++ b/android/copy_assets.sh @@ -11,6 +11,7 @@ if [ ! -e assets/res ]; then mkdir -p assets/res fi +cp -r ../audio assets/res cp -r ../fonts assets/res cp -r ../image assets/res cp -r ../lua assets/res @@ -19,6 +20,7 @@ cp -r ../qml assets/res cp -r ../server assets/res rm assets/res/server/users.db cp ../LICENSE assets/res +cp ../zh_CN.qm assets/res # Due to Qt Android's bug, we need make sure every directory has a subfile (not subdir) function fixDir() { diff --git a/audio/card/common/armor.mp3 b/audio/card/common/armor.mp3 new file mode 100644 index 00000000..e4d7bfdb Binary files /dev/null and b/audio/card/common/armor.mp3 differ diff --git a/audio/card/common/horse.mp3 b/audio/card/common/horse.mp3 new file mode 100644 index 00000000..813a60b8 Binary files /dev/null and b/audio/card/common/horse.mp3 differ diff --git a/audio/card/common/weapon.mp3 b/audio/card/common/weapon.mp3 new file mode 100644 index 00000000..056fcc7f Binary files /dev/null and b/audio/card/common/weapon.mp3 differ diff --git a/audio/card/female/amazing_grace.mp3 b/audio/card/female/amazing_grace.mp3 new file mode 100644 index 00000000..9b33beb4 Binary files /dev/null and b/audio/card/female/amazing_grace.mp3 differ diff --git a/audio/card/female/archery_attack.mp3 b/audio/card/female/archery_attack.mp3 new file mode 100644 index 00000000..ce2c8639 Binary files /dev/null and b/audio/card/female/archery_attack.mp3 differ diff --git a/audio/card/female/collateral.mp3 b/audio/card/female/collateral.mp3 new file mode 100644 index 00000000..51814ac5 Binary files /dev/null and b/audio/card/female/collateral.mp3 differ diff --git a/audio/card/female/dismantlement.mp3 b/audio/card/female/dismantlement.mp3 new file mode 100644 index 00000000..5796adf1 Binary files /dev/null and b/audio/card/female/dismantlement.mp3 differ diff --git a/audio/card/female/duel.mp3 b/audio/card/female/duel.mp3 new file mode 100644 index 00000000..1606fad1 Binary files /dev/null and b/audio/card/female/duel.mp3 differ diff --git a/audio/card/female/ex_nihilo.mp3 b/audio/card/female/ex_nihilo.mp3 new file mode 100644 index 00000000..1480dcd7 Binary files /dev/null and b/audio/card/female/ex_nihilo.mp3 differ diff --git a/audio/card/female/god_salvation.mp3 b/audio/card/female/god_salvation.mp3 new file mode 100644 index 00000000..fdafeba0 Binary files /dev/null and b/audio/card/female/god_salvation.mp3 differ diff --git a/audio/card/female/indulgence.mp3 b/audio/card/female/indulgence.mp3 new file mode 100644 index 00000000..afaed649 Binary files /dev/null and b/audio/card/female/indulgence.mp3 differ diff --git a/audio/card/female/jink.mp3 b/audio/card/female/jink.mp3 new file mode 100644 index 00000000..21a550dc Binary files /dev/null and b/audio/card/female/jink.mp3 differ diff --git a/audio/card/female/lightning.mp3 b/audio/card/female/lightning.mp3 new file mode 100644 index 00000000..dc111c19 Binary files /dev/null and b/audio/card/female/lightning.mp3 differ diff --git a/audio/card/female/nullification.mp3 b/audio/card/female/nullification.mp3 new file mode 100644 index 00000000..4e4edad6 Binary files /dev/null and b/audio/card/female/nullification.mp3 differ diff --git a/audio/card/female/peach.mp3 b/audio/card/female/peach.mp3 new file mode 100644 index 00000000..2f8ebdd4 Binary files /dev/null and b/audio/card/female/peach.mp3 differ diff --git a/audio/card/female/savage_assault.mp3 b/audio/card/female/savage_assault.mp3 new file mode 100644 index 00000000..5fe21f41 Binary files /dev/null and b/audio/card/female/savage_assault.mp3 differ diff --git a/audio/card/female/slash.mp3 b/audio/card/female/slash.mp3 new file mode 100644 index 00000000..25c3c231 Binary files /dev/null and b/audio/card/female/slash.mp3 differ diff --git a/audio/card/female/snatch.mp3 b/audio/card/female/snatch.mp3 new file mode 100644 index 00000000..09e6659a Binary files /dev/null and b/audio/card/female/snatch.mp3 differ diff --git a/audio/card/male/amazing_grace.mp3 b/audio/card/male/amazing_grace.mp3 new file mode 100644 index 00000000..43deea7e Binary files /dev/null and b/audio/card/male/amazing_grace.mp3 differ diff --git a/audio/card/male/archery_attack.mp3 b/audio/card/male/archery_attack.mp3 new file mode 100644 index 00000000..f4811659 Binary files /dev/null and b/audio/card/male/archery_attack.mp3 differ diff --git a/audio/card/male/collateral.mp3 b/audio/card/male/collateral.mp3 new file mode 100644 index 00000000..db2ad753 Binary files /dev/null and b/audio/card/male/collateral.mp3 differ diff --git a/audio/card/male/dismantlement.mp3 b/audio/card/male/dismantlement.mp3 new file mode 100644 index 00000000..6c734b82 Binary files /dev/null and b/audio/card/male/dismantlement.mp3 differ diff --git a/audio/card/male/duel.mp3 b/audio/card/male/duel.mp3 new file mode 100644 index 00000000..775cfb39 Binary files /dev/null and b/audio/card/male/duel.mp3 differ diff --git a/audio/card/male/ex_nihilo.mp3 b/audio/card/male/ex_nihilo.mp3 new file mode 100644 index 00000000..c0857e86 Binary files /dev/null and b/audio/card/male/ex_nihilo.mp3 differ diff --git a/audio/card/male/god_salvation.mp3 b/audio/card/male/god_salvation.mp3 new file mode 100644 index 00000000..780c7537 Binary files /dev/null and b/audio/card/male/god_salvation.mp3 differ diff --git a/audio/card/male/indulgence.mp3 b/audio/card/male/indulgence.mp3 new file mode 100644 index 00000000..3c3b43ca Binary files /dev/null and b/audio/card/male/indulgence.mp3 differ diff --git a/audio/card/male/jink.mp3 b/audio/card/male/jink.mp3 new file mode 100644 index 00000000..f8b1be7d Binary files /dev/null and b/audio/card/male/jink.mp3 differ diff --git a/audio/card/male/lightning.mp3 b/audio/card/male/lightning.mp3 new file mode 100644 index 00000000..91f36e45 Binary files /dev/null and b/audio/card/male/lightning.mp3 differ diff --git a/audio/card/male/nullification.mp3 b/audio/card/male/nullification.mp3 new file mode 100644 index 00000000..a30553df Binary files /dev/null and b/audio/card/male/nullification.mp3 differ diff --git a/audio/card/male/peach.mp3 b/audio/card/male/peach.mp3 new file mode 100644 index 00000000..2f8ebdd4 Binary files /dev/null and b/audio/card/male/peach.mp3 differ diff --git a/audio/card/male/savage_assault.mp3 b/audio/card/male/savage_assault.mp3 new file mode 100644 index 00000000..ae7c3c79 Binary files /dev/null and b/audio/card/male/savage_assault.mp3 differ diff --git a/audio/card/male/slash.mp3 b/audio/card/male/slash.mp3 new file mode 100644 index 00000000..8787c334 Binary files /dev/null and b/audio/card/male/slash.mp3 differ diff --git a/audio/card/male/snatch.mp3 b/audio/card/male/snatch.mp3 new file mode 100644 index 00000000..ab3339e9 Binary files /dev/null and b/audio/card/male/snatch.mp3 differ diff --git a/audio/death/caocao.mp3 b/audio/death/caocao.mp3 new file mode 100644 index 00000000..9854192b Binary files /dev/null and b/audio/death/caocao.mp3 differ diff --git a/audio/death/daqiao.mp3 b/audio/death/daqiao.mp3 new file mode 100644 index 00000000..c55c9d01 Binary files /dev/null and b/audio/death/daqiao.mp3 differ diff --git a/audio/death/diaochan.mp3 b/audio/death/diaochan.mp3 new file mode 100644 index 00000000..36d88522 Binary files /dev/null and b/audio/death/diaochan.mp3 differ diff --git a/audio/death/ganning.mp3 b/audio/death/ganning.mp3 new file mode 100644 index 00000000..8904b2a2 Binary files /dev/null and b/audio/death/ganning.mp3 differ diff --git a/audio/death/guanyu.mp3 b/audio/death/guanyu.mp3 new file mode 100644 index 00000000..a6ae047d Binary files /dev/null and b/audio/death/guanyu.mp3 differ diff --git a/audio/death/guojia.mp3 b/audio/death/guojia.mp3 new file mode 100644 index 00000000..3200ae8c Binary files /dev/null and b/audio/death/guojia.mp3 differ diff --git a/audio/death/huanggai.mp3 b/audio/death/huanggai.mp3 new file mode 100644 index 00000000..0a03bcdc Binary files /dev/null and b/audio/death/huanggai.mp3 differ diff --git a/audio/death/huangyueying.mp3 b/audio/death/huangyueying.mp3 new file mode 100644 index 00000000..6d6656f5 Binary files /dev/null and b/audio/death/huangyueying.mp3 differ diff --git a/audio/death/huatuo.mp3 b/audio/death/huatuo.mp3 new file mode 100644 index 00000000..61578619 Binary files /dev/null and b/audio/death/huatuo.mp3 differ diff --git a/audio/death/liubei.mp3 b/audio/death/liubei.mp3 new file mode 100644 index 00000000..8451479a Binary files /dev/null and b/audio/death/liubei.mp3 differ diff --git a/audio/death/luxun.mp3 b/audio/death/luxun.mp3 new file mode 100644 index 00000000..8ee55884 Binary files /dev/null and b/audio/death/luxun.mp3 differ diff --git a/audio/death/lvbu.mp3 b/audio/death/lvbu.mp3 new file mode 100644 index 00000000..6876280f Binary files /dev/null and b/audio/death/lvbu.mp3 differ diff --git a/audio/death/lvmeng.mp3 b/audio/death/lvmeng.mp3 new file mode 100644 index 00000000..d8de1e68 Binary files /dev/null and b/audio/death/lvmeng.mp3 differ diff --git a/audio/death/machao.mp3 b/audio/death/machao.mp3 new file mode 100644 index 00000000..5c42197d Binary files /dev/null and b/audio/death/machao.mp3 differ diff --git a/audio/death/simayi.mp3 b/audio/death/simayi.mp3 new file mode 100644 index 00000000..ffeda187 Binary files /dev/null and b/audio/death/simayi.mp3 differ diff --git a/audio/death/sunquan.mp3 b/audio/death/sunquan.mp3 new file mode 100644 index 00000000..8f846a70 Binary files /dev/null and b/audio/death/sunquan.mp3 differ diff --git a/audio/death/sunshangxiang.mp3 b/audio/death/sunshangxiang.mp3 new file mode 100644 index 00000000..b1ad9515 Binary files /dev/null and b/audio/death/sunshangxiang.mp3 differ diff --git a/audio/death/xiahoudun.mp3 b/audio/death/xiahoudun.mp3 new file mode 100644 index 00000000..3625b5f2 Binary files /dev/null and b/audio/death/xiahoudun.mp3 differ diff --git a/audio/death/xuchu.mp3 b/audio/death/xuchu.mp3 new file mode 100644 index 00000000..7b2264e9 Binary files /dev/null and b/audio/death/xuchu.mp3 differ diff --git a/audio/death/zhangfei.mp3 b/audio/death/zhangfei.mp3 new file mode 100644 index 00000000..ab6a3629 Binary files /dev/null and b/audio/death/zhangfei.mp3 differ diff --git a/audio/death/zhangliao.mp3 b/audio/death/zhangliao.mp3 new file mode 100644 index 00000000..d6e8f979 Binary files /dev/null and b/audio/death/zhangliao.mp3 differ diff --git a/audio/death/zhaoyun.mp3 b/audio/death/zhaoyun.mp3 new file mode 100644 index 00000000..bd21595f Binary files /dev/null and b/audio/death/zhaoyun.mp3 differ diff --git a/audio/death/zhenji.mp3 b/audio/death/zhenji.mp3 new file mode 100644 index 00000000..e2dc7f02 Binary files /dev/null and b/audio/death/zhenji.mp3 differ diff --git a/audio/death/zhouyu.mp3 b/audio/death/zhouyu.mp3 new file mode 100644 index 00000000..e0488fcd Binary files /dev/null and b/audio/death/zhouyu.mp3 differ diff --git a/audio/death/zhugeliang.mp3 b/audio/death/zhugeliang.mp3 new file mode 100644 index 00000000..651a0306 Binary files /dev/null and b/audio/death/zhugeliang.mp3 differ diff --git a/audio/skill/biyue1.mp3 b/audio/skill/biyue1.mp3 new file mode 100644 index 00000000..2e1de0df Binary files /dev/null and b/audio/skill/biyue1.mp3 differ diff --git a/audio/skill/biyue2.mp3 b/audio/skill/biyue2.mp3 new file mode 100644 index 00000000..1276509a Binary files /dev/null and b/audio/skill/biyue2.mp3 differ diff --git a/audio/skill/fanjian1.mp3 b/audio/skill/fanjian1.mp3 new file mode 100644 index 00000000..178d2c9e Binary files /dev/null and b/audio/skill/fanjian1.mp3 differ diff --git a/audio/skill/fanjian2.mp3 b/audio/skill/fanjian2.mp3 new file mode 100644 index 00000000..72109667 Binary files /dev/null and b/audio/skill/fanjian2.mp3 differ diff --git a/audio/skill/fankui1.mp3 b/audio/skill/fankui1.mp3 new file mode 100644 index 00000000..57ab5c1f Binary files /dev/null and b/audio/skill/fankui1.mp3 differ diff --git a/audio/skill/fankui2.mp3 b/audio/skill/fankui2.mp3 new file mode 100644 index 00000000..2c9c2ffc Binary files /dev/null and b/audio/skill/fankui2.mp3 differ diff --git a/audio/skill/ganglie1.mp3 b/audio/skill/ganglie1.mp3 new file mode 100644 index 00000000..d6648d0e Binary files /dev/null and b/audio/skill/ganglie1.mp3 differ diff --git a/audio/skill/ganglie2.mp3 b/audio/skill/ganglie2.mp3 new file mode 100644 index 00000000..d914dd95 Binary files /dev/null and b/audio/skill/ganglie2.mp3 differ diff --git a/audio/skill/guanxing1.mp3 b/audio/skill/guanxing1.mp3 new file mode 100644 index 00000000..7f0dd2cf Binary files /dev/null and b/audio/skill/guanxing1.mp3 differ diff --git a/audio/skill/guanxing2.mp3 b/audio/skill/guanxing2.mp3 new file mode 100644 index 00000000..f3f46e79 Binary files /dev/null and b/audio/skill/guanxing2.mp3 differ diff --git a/audio/skill/guicai1.mp3 b/audio/skill/guicai1.mp3 new file mode 100644 index 00000000..131b2c54 Binary files /dev/null and b/audio/skill/guicai1.mp3 differ diff --git a/audio/skill/guicai2.mp3 b/audio/skill/guicai2.mp3 new file mode 100644 index 00000000..f07ec7f2 Binary files /dev/null and b/audio/skill/guicai2.mp3 differ diff --git a/audio/skill/guose1.mp3 b/audio/skill/guose1.mp3 new file mode 100644 index 00000000..a42ba695 Binary files /dev/null and b/audio/skill/guose1.mp3 differ diff --git a/audio/skill/guose2.mp3 b/audio/skill/guose2.mp3 new file mode 100644 index 00000000..64ee97c3 Binary files /dev/null and b/audio/skill/guose2.mp3 differ diff --git a/audio/skill/jianxiong1.mp3 b/audio/skill/jianxiong1.mp3 new file mode 100644 index 00000000..c064465e Binary files /dev/null and b/audio/skill/jianxiong1.mp3 differ diff --git a/audio/skill/jianxiong2.mp3 b/audio/skill/jianxiong2.mp3 new file mode 100644 index 00000000..f0300193 Binary files /dev/null and b/audio/skill/jianxiong2.mp3 differ diff --git a/audio/skill/jieyin1.mp3 b/audio/skill/jieyin1.mp3 new file mode 100644 index 00000000..62c05b3b Binary files /dev/null and b/audio/skill/jieyin1.mp3 differ diff --git a/audio/skill/jieyin2.mp3 b/audio/skill/jieyin2.mp3 new file mode 100644 index 00000000..c5c9ea2c Binary files /dev/null and b/audio/skill/jieyin2.mp3 differ diff --git a/audio/skill/jijiu1.mp3 b/audio/skill/jijiu1.mp3 new file mode 100644 index 00000000..b067464a Binary files /dev/null and b/audio/skill/jijiu1.mp3 differ diff --git a/audio/skill/jijiu2.mp3 b/audio/skill/jijiu2.mp3 new file mode 100644 index 00000000..8092f0de Binary files /dev/null and b/audio/skill/jijiu2.mp3 differ diff --git a/audio/skill/jizhi1.mp3 b/audio/skill/jizhi1.mp3 new file mode 100644 index 00000000..f2a88699 Binary files /dev/null and b/audio/skill/jizhi1.mp3 differ diff --git a/audio/skill/jizhi2.mp3 b/audio/skill/jizhi2.mp3 new file mode 100644 index 00000000..16d0bb93 Binary files /dev/null and b/audio/skill/jizhi2.mp3 differ diff --git a/audio/skill/keji1.mp3 b/audio/skill/keji1.mp3 new file mode 100644 index 00000000..8385a49a Binary files /dev/null and b/audio/skill/keji1.mp3 differ diff --git a/audio/skill/keji2.mp3 b/audio/skill/keji2.mp3 new file mode 100644 index 00000000..59ef0d59 Binary files /dev/null and b/audio/skill/keji2.mp3 differ diff --git a/audio/skill/kongcheng1.mp3 b/audio/skill/kongcheng1.mp3 new file mode 100644 index 00000000..39a46ee7 Binary files /dev/null and b/audio/skill/kongcheng1.mp3 differ diff --git a/audio/skill/kongcheng2.mp3 b/audio/skill/kongcheng2.mp3 new file mode 100644 index 00000000..22ccc597 Binary files /dev/null and b/audio/skill/kongcheng2.mp3 differ diff --git a/audio/skill/kurou1.mp3 b/audio/skill/kurou1.mp3 new file mode 100644 index 00000000..fea20594 Binary files /dev/null and b/audio/skill/kurou1.mp3 differ diff --git a/audio/skill/kurou2.mp3 b/audio/skill/kurou2.mp3 new file mode 100644 index 00000000..d09be0ab Binary files /dev/null and b/audio/skill/kurou2.mp3 differ diff --git a/audio/skill/lianying1.mp3 b/audio/skill/lianying1.mp3 new file mode 100644 index 00000000..b3a3821d Binary files /dev/null and b/audio/skill/lianying1.mp3 differ diff --git a/audio/skill/lianying2.mp3 b/audio/skill/lianying2.mp3 new file mode 100644 index 00000000..cefcfb46 Binary files /dev/null and b/audio/skill/lianying2.mp3 differ diff --git a/audio/skill/lijian1.mp3 b/audio/skill/lijian1.mp3 new file mode 100644 index 00000000..572049b1 Binary files /dev/null and b/audio/skill/lijian1.mp3 differ diff --git a/audio/skill/lijian2.mp3 b/audio/skill/lijian2.mp3 new file mode 100644 index 00000000..1787d8a8 Binary files /dev/null and b/audio/skill/lijian2.mp3 differ diff --git a/audio/skill/liuli1.mp3 b/audio/skill/liuli1.mp3 new file mode 100644 index 00000000..c0c91ace Binary files /dev/null and b/audio/skill/liuli1.mp3 differ diff --git a/audio/skill/liuli2.mp3 b/audio/skill/liuli2.mp3 new file mode 100644 index 00000000..2fd49c38 Binary files /dev/null and b/audio/skill/liuli2.mp3 differ diff --git a/audio/skill/longdan1.mp3 b/audio/skill/longdan1.mp3 new file mode 100644 index 00000000..fd484688 Binary files /dev/null and b/audio/skill/longdan1.mp3 differ diff --git a/audio/skill/longdan2.mp3 b/audio/skill/longdan2.mp3 new file mode 100644 index 00000000..b83e96e5 Binary files /dev/null and b/audio/skill/longdan2.mp3 differ diff --git a/audio/skill/luoshen1.mp3 b/audio/skill/luoshen1.mp3 new file mode 100644 index 00000000..927def4f Binary files /dev/null and b/audio/skill/luoshen1.mp3 differ diff --git a/audio/skill/luoshen2.mp3 b/audio/skill/luoshen2.mp3 new file mode 100644 index 00000000..aee0268f Binary files /dev/null and b/audio/skill/luoshen2.mp3 differ diff --git a/audio/skill/luoyi1.mp3 b/audio/skill/luoyi1.mp3 new file mode 100644 index 00000000..cdad92c5 Binary files /dev/null and b/audio/skill/luoyi1.mp3 differ diff --git a/audio/skill/luoyi2.mp3 b/audio/skill/luoyi2.mp3 new file mode 100644 index 00000000..e32aa76f Binary files /dev/null and b/audio/skill/luoyi2.mp3 differ diff --git a/audio/skill/paoxiao1.mp3 b/audio/skill/paoxiao1.mp3 new file mode 100644 index 00000000..6482afa2 Binary files /dev/null and b/audio/skill/paoxiao1.mp3 differ diff --git a/audio/skill/paoxiao2.mp3 b/audio/skill/paoxiao2.mp3 new file mode 100644 index 00000000..36423478 Binary files /dev/null and b/audio/skill/paoxiao2.mp3 differ diff --git a/audio/skill/qianxun1.mp3 b/audio/skill/qianxun1.mp3 new file mode 100644 index 00000000..5537ba5e Binary files /dev/null and b/audio/skill/qianxun1.mp3 differ diff --git a/audio/skill/qianxun2.mp3 b/audio/skill/qianxun2.mp3 new file mode 100644 index 00000000..15742323 Binary files /dev/null and b/audio/skill/qianxun2.mp3 differ diff --git a/audio/skill/qingguo1.mp3 b/audio/skill/qingguo1.mp3 new file mode 100644 index 00000000..e47f1486 Binary files /dev/null and b/audio/skill/qingguo1.mp3 differ diff --git a/audio/skill/qingguo2.mp3 b/audio/skill/qingguo2.mp3 new file mode 100644 index 00000000..a6e701e6 Binary files /dev/null and b/audio/skill/qingguo2.mp3 differ diff --git a/audio/skill/qingnang1.mp3 b/audio/skill/qingnang1.mp3 new file mode 100644 index 00000000..b0a128a5 Binary files /dev/null and b/audio/skill/qingnang1.mp3 differ diff --git a/audio/skill/qingnang2.mp3 b/audio/skill/qingnang2.mp3 new file mode 100644 index 00000000..482b5ca0 Binary files /dev/null and b/audio/skill/qingnang2.mp3 differ diff --git a/audio/skill/qixi1.mp3 b/audio/skill/qixi1.mp3 new file mode 100644 index 00000000..fc39c093 Binary files /dev/null and b/audio/skill/qixi1.mp3 differ diff --git a/audio/skill/qixi2.mp3 b/audio/skill/qixi2.mp3 new file mode 100644 index 00000000..bc5f2c92 Binary files /dev/null and b/audio/skill/qixi2.mp3 differ diff --git a/audio/skill/rende1.mp3 b/audio/skill/rende1.mp3 new file mode 100644 index 00000000..18571d1c Binary files /dev/null and b/audio/skill/rende1.mp3 differ diff --git a/audio/skill/rende2.mp3 b/audio/skill/rende2.mp3 new file mode 100644 index 00000000..25402f33 Binary files /dev/null and b/audio/skill/rende2.mp3 differ diff --git a/audio/skill/tiandu1.mp3 b/audio/skill/tiandu1.mp3 new file mode 100644 index 00000000..f911ec0c Binary files /dev/null and b/audio/skill/tiandu1.mp3 differ diff --git a/audio/skill/tiandu2.mp3 b/audio/skill/tiandu2.mp3 new file mode 100644 index 00000000..52481666 Binary files /dev/null and b/audio/skill/tiandu2.mp3 differ diff --git a/audio/skill/tieqi1.mp3 b/audio/skill/tieqi1.mp3 new file mode 100644 index 00000000..8db166c4 Binary files /dev/null and b/audio/skill/tieqi1.mp3 differ diff --git a/audio/skill/tieqi2.mp3 b/audio/skill/tieqi2.mp3 new file mode 100644 index 00000000..81514190 Binary files /dev/null and b/audio/skill/tieqi2.mp3 differ diff --git a/audio/skill/tuxi1.mp3 b/audio/skill/tuxi1.mp3 new file mode 100644 index 00000000..74d41b3a Binary files /dev/null and b/audio/skill/tuxi1.mp3 differ diff --git a/audio/skill/tuxi2.mp3 b/audio/skill/tuxi2.mp3 new file mode 100644 index 00000000..fe142e16 Binary files /dev/null and b/audio/skill/tuxi2.mp3 differ diff --git a/audio/skill/wusheng1.mp3 b/audio/skill/wusheng1.mp3 new file mode 100644 index 00000000..e8f8f130 Binary files /dev/null and b/audio/skill/wusheng1.mp3 differ diff --git a/audio/skill/wusheng2.mp3 b/audio/skill/wusheng2.mp3 new file mode 100644 index 00000000..c33029d0 Binary files /dev/null and b/audio/skill/wusheng2.mp3 differ diff --git a/audio/skill/wushuang1.mp3 b/audio/skill/wushuang1.mp3 new file mode 100644 index 00000000..6c601b43 Binary files /dev/null and b/audio/skill/wushuang1.mp3 differ diff --git a/audio/skill/wushuang2.mp3 b/audio/skill/wushuang2.mp3 new file mode 100644 index 00000000..b2c6dc47 Binary files /dev/null and b/audio/skill/wushuang2.mp3 differ diff --git a/audio/skill/xiaoji1.mp3 b/audio/skill/xiaoji1.mp3 new file mode 100644 index 00000000..811f22eb Binary files /dev/null and b/audio/skill/xiaoji1.mp3 differ diff --git a/audio/skill/xiaoji2.mp3 b/audio/skill/xiaoji2.mp3 new file mode 100644 index 00000000..d28ea595 Binary files /dev/null and b/audio/skill/xiaoji2.mp3 differ diff --git a/audio/skill/yiji1.mp3 b/audio/skill/yiji1.mp3 new file mode 100644 index 00000000..d8e96816 Binary files /dev/null and b/audio/skill/yiji1.mp3 differ diff --git a/audio/skill/yiji2.mp3 b/audio/skill/yiji2.mp3 new file mode 100644 index 00000000..0ac76a58 Binary files /dev/null and b/audio/skill/yiji2.mp3 differ diff --git a/audio/skill/yingzi1.mp3 b/audio/skill/yingzi1.mp3 new file mode 100644 index 00000000..def3c7c5 Binary files /dev/null and b/audio/skill/yingzi1.mp3 differ diff --git a/audio/skill/yingzi2.mp3 b/audio/skill/yingzi2.mp3 new file mode 100644 index 00000000..b545002c Binary files /dev/null and b/audio/skill/yingzi2.mp3 differ diff --git a/audio/skill/zhiheng1.mp3 b/audio/skill/zhiheng1.mp3 new file mode 100644 index 00000000..bfdca368 Binary files /dev/null and b/audio/skill/zhiheng1.mp3 differ diff --git a/audio/skill/zhiheng2.mp3 b/audio/skill/zhiheng2.mp3 new file mode 100644 index 00000000..775dd4ad Binary files /dev/null and b/audio/skill/zhiheng2.mp3 differ diff --git a/audio/system/bgm.mp3 b/audio/system/bgm.mp3 new file mode 100644 index 00000000..7aae1590 Binary files /dev/null and b/audio/system/bgm.mp3 differ diff --git a/audio/system/fire_damage.mp3 b/audio/system/fire_damage.mp3 new file mode 100644 index 00000000..4292a020 Binary files /dev/null and b/audio/system/fire_damage.mp3 differ diff --git a/audio/system/fire_damage2.mp3 b/audio/system/fire_damage2.mp3 new file mode 100644 index 00000000..8f23b7d4 Binary files /dev/null and b/audio/system/fire_damage2.mp3 differ diff --git a/audio/system/losehp.mp3 b/audio/system/losehp.mp3 new file mode 100644 index 00000000..7df149a1 Binary files /dev/null and b/audio/system/losehp.mp3 differ diff --git a/audio/system/normal_damage.mp3 b/audio/system/normal_damage.mp3 new file mode 100644 index 00000000..1076f13b Binary files /dev/null and b/audio/system/normal_damage.mp3 differ diff --git a/audio/system/normal_damage2.mp3 b/audio/system/normal_damage2.mp3 new file mode 100644 index 00000000..c12a560f Binary files /dev/null and b/audio/system/normal_damage2.mp3 differ diff --git a/audio/system/thunder_damage.mp3 b/audio/system/thunder_damage.mp3 new file mode 100644 index 00000000..2dc952e3 Binary files /dev/null and b/audio/system/thunder_damage.mp3 differ diff --git a/audio/system/thunder_damage2.mp3 b/audio/system/thunder_damage2.mp3 new file mode 100644 index 00000000..769795a5 Binary files /dev/null and b/audio/system/thunder_damage2.mp3 differ diff --git a/doc/dev/gamelogic.md b/doc/dev/gamelogic.md index a8e8aa93..8c8363b0 100644 --- a/doc/dev/gamelogic.md +++ b/doc/dev/gamelogic.md @@ -14,6 +14,110 @@ ___ ## 触发技 +在lua/fk_ex.lua中有对触发技的描述: + +```lua +---@alias TrigFunc fun(self: TriggerSkill, event: Event, target: ServerPlayer, player: ServerPlayer):boolean +---@class TriggerSkillSpec: SkillSpec +---@field global boolean +---@field events Event | Event[] +---@field refresh_events Event | Event[] +---@field priority number | table +---@field on_trigger TrigFunc +---@field can_trigger TrigFunc +---@field on_cost TrigFunc +---@field on_use TrigFunc +---@field on_refresh TrigFunc +``` + +具体的`fk.CreateTriggerSkill`函数接受一个类型为如上所述的TriggerSkillSpec形式的表。这个表中的属性一共有一下这些: + +- 所有技能通用的`name`、`anim_type`、`mute`。其中name为必需项。 +- global: 是否是全局技能。 +- events: 技能的所有触发时机 +- can_trigger: 技能能否被触发 +- on_trigger: 技能触发时具体的行为 +- on_cost: 技能如何执行消耗 +- on_use: 技能被发动后,具体的生效内容 +- priority: 技能的优先级。在同一时机有多个技能能够被触发时,先触发优先级高的。 + +refresh等一系列函数与前面同理,下面会对其展开细说。 + +首先先来看看触发技究竟是如何被触发的:(以下代码详见room.lua和gamelogic.lua,这里只是简单说明一下) + +1. 某处调用`logic:trigger(event, player, data)` +2. 开始调用GameLogic:trigger,首先从所有符合该时机的技能中选出那个技能列表。这里说明一下,所有的触发技都保存在GameLogic的`skill_table`表中,这个表的键是相应的触发时机,值则是技能列表。每当GameLogic被创建时,首先会将全局触发技都加入到表中;然后,在游戏中每当有角色获得了一个触发技,就将这个技能加入到表中直到游戏结束。 +3. 若调用trigger函数时对target参数传入了nil,表示这是一个通用型时机,没有特定的承担者,比如fk.GameStart时机。这时候会对技能进行can_trigger检测并直接触发。 +4. 若target不是nil,那么将对整个Room中所有玩家进行遍历。在这个遍历过程中,对每个玩家分别判断其能否触发这个技能,若能的话就进行on_trigger的内容,中间的优先级和选择发动哪个技能暂且不说明,可以在代码中查看到。 +5. 若on_trigger函数返回了true,那么就说明这个时机被中断了,此时trigger函数返回,否则就这样一直遍历完所有玩家为止。 + +这就是整个触发技的流程了,可见只涉及了can_trigger和on_trigger函数,并没有on_cost和on_use环节。熟悉太阳神三国杀Lua的朋友知道触发技的发动时机难以定义,因为没有很好的办法知道究竟在哪个时候才算是“发动”了技能。为了解决这个问题,FreeKill引入了on_cost和on_use这两个函数。 + +这部分相关的代码位于core/skill_type/trigger.lua中。来看看这些函数的默认值: + +```lua +function TriggerSkill:triggerable(event, target, player, data) + return target and (target == player) + and (self.global or (target:isAlive() and target:hasSkill(self))) +end + +function TriggerSkill:trigger(event, target, player, data) + return self:doCost(event, target, player, data) +end +``` + +这就是can_trigger和on_trigger的默认值了。can_trigger默认情况下判断遍历到的角色就是承担者角色,并且这个角色要拥有本技能才行。这种判断适用于绝大多数情况,比如英姿等技能。而on_trigger则是调用了TriggerSkill:doCost函数了。doCost函数并不是fk_ex.lua中的on_cost,而是triggerSkill中的一个特别的函数,其内容如下: + +```lua +function TriggerSkill:doCost(event, target, player, data) + local ret = self:cost(event, target, player, data) + if ret then + local room = player.room + if not self.mute then + room:broadcastSkillInvoke(self.name) + end + room:notifySkillInvoked(player, self.name) + player:addSkillUseHistory(self.name) + ret = self:use(event, target, player, data) + return ret + end +end +``` + +这个函数首先调用self:cost(即on_cost),判断是否返回了true。(返回true的话意味着玩家已经完成了消耗,技能被正式发动了)如果返回true的话,那么就认为技能发动了,这时会添加技能发动记录、播放配音等行为,然后正式执行self:use(即on_use)。这就是触发技完整的从触发到使用的过程。 + +现在以鬼才为例:(packages/standard/init.lua) + +```lua +local guicai = fk.CreateTriggerSkill{ + name = "guicai", + anim_type = "control", + events = {fk.AskForRetrial}, + can_trigger = function(self, event, target, player, data) + return player:hasSkill(self.name) and not player:isKongcheng() + end, + on_cost = function(self, event, target, player, data) + local room = player.room + local prompt = "#guicai-ask::" .. target.id + local card = room:askForResponse(player, self.name, ".|.|.|hand", prompt, true) + if card ~= nil then + self.cost_data = card + return true + end + end, + on_use = function(self, event, target, player, data) + local room = player.room + room:retrial(self.cost_data, player, data, self.name) + end, +} +``` + +首先name和anim_type啥的不多说。技能的时机是AskForRetrial,这也就是询问改判的时机。由于鬼才的触发条件是只要自己有手牌就能触发,无需判定者是自己,因此这里没有用默认的can_trigger。on_trigger函数采用默认方案,直接只执行doCost。在on_cost环节,玩家需要选择是否打出一张手牌。如果确实打出牌了,那么就返回true,并把打出的牌保存到self.cost_data中。(self是这个技能本身,注意技能的本质其实就是一张表,因此可以像这样指定一个新的键值也是没问题的)在on_use,也就是技能的生效部分,才会正式执行改判这一动作。 + +on_trigger在非常多情况下仅仅只是简单的执行一下doCost而已,但对于有些技能则不然,比如遗计,它能在一次伤害事件中执行许多次,每受一点伤害就能发动一次,因此这种情况下需要自己对on_trigger中的内容手动编写一下。 + +在有些时候,只是想在特定的时机执行一些代码,而不想进行询问和发动技能流程时,可以使用on_refresh执行。在refresh的情况下,代码仅仅只是执行了一次,不会做出发动技能之类的动作、 + ___ ## 移动牌 diff --git a/doc/dev/ui.md b/doc/dev/ui.md index e3f7c4a9..b4821bfb 100644 --- a/doc/dev/ui.md +++ b/doc/dev/ui.md @@ -27,3 +27,65 @@ ___ ## config Config.qml存储一些客户端需要用到的设置或者即将发送的数据,(TODO) + +--- + +## Room和RoomLogic + +这部分是整个UI体系中最复杂的一部分,其中尤以手牌区的操作为甚。下面来整理一下与出牌相关的UI逻辑。 + +首先要指明一个常用函数: + +```cpp + Q_INVOKABLE QString callLuaFunction(const QString &func_name, + QVariantList params); +``` + +该函数声明位于qmlbackend.h中,第一个参数是函数名,必须是lua的全局函数,第二个列表是参数列表。lua一侧应当返回字符串/数字/布尔值,然后再在这里转成QString并返回qml中。这就是qml调用lua函数的核心。 + +然后来说说Room。Room中一共有4种状态,分别是: + +- notactive: 平常的不活跃状态。在此期间牌都是暗的,不能操作。 +- playing: 出牌阶段主动出牌状态。 +- responding: 需要选择响应使用/打出的状态。 +- replying: 需要操作对话框以回应服务器的状态。 + +notactive和replying不是本次的重点,重点在于playing和responding中关于手牌区的操作。 + +先看Room.qml中关于切换到这两个状态后的动作是什么: + +```js +Transition { + from: "*"; to: "playing" + ScriptAction { + script: { + dashboard.enableCards(); + dashboard.enableSkills(); + progress.visible = true; + okCancel.visible = true; + endPhaseButton.visible = true; + respond_play = false; + } + } +}, + +Transition { + from: "*"; to: "responding" + ScriptAction { + script: { + dashboard.enableCards(responding_card); + dashboard.enableSkills(responding_card); + progress.visible = true; + okCancel.visible = true; + } + } +}, +``` + +其中,涉及到的值得注意的函数是enableCards和enableSkills,这里只关心前者。 + +这两个函数的定义都是在Dashboard.qml中。其中,enableCards的内容大致是:如果状态是playing(即不提供参数),那么就判断每个card的CanUseCard,如果通过的话就点亮;如果是responding状态,那么就需要卡牌符合当前respond的pattern(即Room.qml中的responding_card属性)。 + +当一张牌被点击之后,牌的selected属性会变为true。经过一系列信号槽传输后,最终会触发RoomLogic.js中的enableTargets函数。类似的,在选中一个photo后,会触发Logic.updateSelectedTargets。 + +这两个函数的内容基本上大同小异,都是对能否选择某个目标、能否按下确定键进行判断,判断的依据也都是lua函数的内容。 diff --git a/image/anim/jink/0.png b/image/anim/jink/0.png new file mode 100644 index 00000000..bafb7760 Binary files /dev/null and b/image/anim/jink/0.png differ diff --git a/image/anim/jink/1.png b/image/anim/jink/1.png new file mode 100644 index 00000000..ada2c387 Binary files /dev/null and b/image/anim/jink/1.png differ diff --git a/image/anim/jink/10.png b/image/anim/jink/10.png new file mode 100644 index 00000000..7a9048b3 Binary files /dev/null and b/image/anim/jink/10.png differ diff --git a/image/anim/jink/11.png b/image/anim/jink/11.png new file mode 100644 index 00000000..c442245f Binary files /dev/null and b/image/anim/jink/11.png differ diff --git a/image/anim/jink/12.png b/image/anim/jink/12.png new file mode 100644 index 00000000..64c4637f Binary files /dev/null and b/image/anim/jink/12.png differ diff --git a/image/anim/jink/13.png b/image/anim/jink/13.png new file mode 100644 index 00000000..07bf9db1 Binary files /dev/null and b/image/anim/jink/13.png differ diff --git a/image/anim/jink/14.png b/image/anim/jink/14.png new file mode 100644 index 00000000..3cbc3af7 Binary files /dev/null and b/image/anim/jink/14.png differ diff --git a/image/anim/jink/15.png b/image/anim/jink/15.png new file mode 100644 index 00000000..3cbc3af7 Binary files /dev/null and b/image/anim/jink/15.png differ diff --git a/image/anim/jink/16.png b/image/anim/jink/16.png new file mode 100644 index 00000000..c6e5a01e Binary files /dev/null and b/image/anim/jink/16.png differ diff --git a/image/anim/jink/17.png b/image/anim/jink/17.png new file mode 100644 index 00000000..c6e5a01e Binary files /dev/null and b/image/anim/jink/17.png differ diff --git a/image/anim/jink/18.png b/image/anim/jink/18.png new file mode 100644 index 00000000..3cbc3af7 Binary files /dev/null and b/image/anim/jink/18.png differ diff --git a/image/anim/jink/19.png b/image/anim/jink/19.png new file mode 100644 index 00000000..3cbc3af7 Binary files /dev/null and b/image/anim/jink/19.png differ diff --git a/image/anim/jink/2.png b/image/anim/jink/2.png new file mode 100644 index 00000000..3049ab05 Binary files /dev/null and b/image/anim/jink/2.png differ diff --git a/image/anim/jink/20.png b/image/anim/jink/20.png new file mode 100644 index 00000000..3cbc3af7 Binary files /dev/null and b/image/anim/jink/20.png differ diff --git a/image/anim/jink/21.png b/image/anim/jink/21.png new file mode 100644 index 00000000..d59dc694 Binary files /dev/null and b/image/anim/jink/21.png differ diff --git a/image/anim/jink/22.png b/image/anim/jink/22.png new file mode 100644 index 00000000..03cde037 Binary files /dev/null and b/image/anim/jink/22.png differ diff --git a/image/anim/jink/3.png b/image/anim/jink/3.png new file mode 100644 index 00000000..fc953854 Binary files /dev/null and b/image/anim/jink/3.png differ diff --git a/image/anim/jink/4.png b/image/anim/jink/4.png new file mode 100644 index 00000000..c39153fb Binary files /dev/null and b/image/anim/jink/4.png differ diff --git a/image/anim/jink/5.png b/image/anim/jink/5.png new file mode 100644 index 00000000..9db5c932 Binary files /dev/null and b/image/anim/jink/5.png differ diff --git a/image/anim/jink/6.png b/image/anim/jink/6.png new file mode 100644 index 00000000..8da2c080 Binary files /dev/null and b/image/anim/jink/6.png differ diff --git a/image/anim/jink/7.png b/image/anim/jink/7.png new file mode 100644 index 00000000..a00fbbd6 Binary files /dev/null and b/image/anim/jink/7.png differ diff --git a/image/anim/jink/8.png b/image/anim/jink/8.png new file mode 100644 index 00000000..d8f5ed1d Binary files /dev/null and b/image/anim/jink/8.png differ diff --git a/image/anim/jink/9.png b/image/anim/jink/9.png new file mode 100644 index 00000000..095c51b2 Binary files /dev/null and b/image/anim/jink/9.png differ diff --git a/image/anim/judgebad/0.png b/image/anim/judgebad/0.png new file mode 100644 index 00000000..d3e2bb49 Binary files /dev/null and b/image/anim/judgebad/0.png differ diff --git a/image/anim/judgebad/1.png b/image/anim/judgebad/1.png new file mode 100644 index 00000000..e47e3c9c Binary files /dev/null and b/image/anim/judgebad/1.png differ diff --git a/image/anim/judgebad/10.png b/image/anim/judgebad/10.png new file mode 100644 index 00000000..fd33b23d Binary files /dev/null and b/image/anim/judgebad/10.png differ diff --git a/image/anim/judgebad/11.png b/image/anim/judgebad/11.png new file mode 100644 index 00000000..fd33b23d Binary files /dev/null and b/image/anim/judgebad/11.png differ diff --git a/image/anim/judgebad/12.png b/image/anim/judgebad/12.png new file mode 100644 index 00000000..fd33b23d Binary files /dev/null and b/image/anim/judgebad/12.png differ diff --git a/image/anim/judgebad/13.png b/image/anim/judgebad/13.png new file mode 100644 index 00000000..fd33b23d Binary files /dev/null and b/image/anim/judgebad/13.png differ diff --git a/image/anim/judgebad/14.png b/image/anim/judgebad/14.png new file mode 100644 index 00000000..d3782324 Binary files /dev/null and b/image/anim/judgebad/14.png differ diff --git a/image/anim/judgebad/15.png b/image/anim/judgebad/15.png new file mode 100644 index 00000000..2f04b574 Binary files /dev/null and b/image/anim/judgebad/15.png differ diff --git a/image/anim/judgebad/16.png b/image/anim/judgebad/16.png new file mode 100644 index 00000000..2dc92fa0 Binary files /dev/null and b/image/anim/judgebad/16.png differ diff --git a/image/anim/judgebad/17.png b/image/anim/judgebad/17.png new file mode 100644 index 00000000..b5f770ff Binary files /dev/null and b/image/anim/judgebad/17.png differ diff --git a/image/anim/judgebad/18.png b/image/anim/judgebad/18.png new file mode 100644 index 00000000..2cf6f476 Binary files /dev/null and b/image/anim/judgebad/18.png differ diff --git a/image/anim/judgebad/2.png b/image/anim/judgebad/2.png new file mode 100644 index 00000000..9e0c67c9 Binary files /dev/null and b/image/anim/judgebad/2.png differ diff --git a/image/anim/judgebad/3.png b/image/anim/judgebad/3.png new file mode 100644 index 00000000..47a7f831 Binary files /dev/null and b/image/anim/judgebad/3.png differ diff --git a/image/anim/judgebad/4.png b/image/anim/judgebad/4.png new file mode 100644 index 00000000..0dcbffd3 Binary files /dev/null and b/image/anim/judgebad/4.png differ diff --git a/image/anim/judgebad/5.png b/image/anim/judgebad/5.png new file mode 100644 index 00000000..8c7ee1c8 Binary files /dev/null and b/image/anim/judgebad/5.png differ diff --git a/image/anim/judgebad/6.png b/image/anim/judgebad/6.png new file mode 100644 index 00000000..ccbc5b48 Binary files /dev/null and b/image/anim/judgebad/6.png differ diff --git a/image/anim/judgebad/7.png b/image/anim/judgebad/7.png new file mode 100644 index 00000000..fd33b23d Binary files /dev/null and b/image/anim/judgebad/7.png differ diff --git a/image/anim/judgebad/8.png b/image/anim/judgebad/8.png new file mode 100644 index 00000000..fd33b23d Binary files /dev/null and b/image/anim/judgebad/8.png differ diff --git a/image/anim/judgebad/9.png b/image/anim/judgebad/9.png new file mode 100644 index 00000000..fd33b23d Binary files /dev/null and b/image/anim/judgebad/9.png differ diff --git a/image/anim/judgegood/0.png b/image/anim/judgegood/0.png new file mode 100644 index 00000000..c9686934 Binary files /dev/null and b/image/anim/judgegood/0.png differ diff --git a/image/anim/judgegood/1.png b/image/anim/judgegood/1.png new file mode 100644 index 00000000..9a064b0f Binary files /dev/null and b/image/anim/judgegood/1.png differ diff --git a/image/anim/judgegood/10.png b/image/anim/judgegood/10.png new file mode 100644 index 00000000..6335c202 Binary files /dev/null and b/image/anim/judgegood/10.png differ diff --git a/image/anim/judgegood/11.png b/image/anim/judgegood/11.png new file mode 100644 index 00000000..5000493b Binary files /dev/null and b/image/anim/judgegood/11.png differ diff --git a/image/anim/judgegood/12.png b/image/anim/judgegood/12.png new file mode 100644 index 00000000..a40fbb6b Binary files /dev/null and b/image/anim/judgegood/12.png differ diff --git a/image/anim/judgegood/13.png b/image/anim/judgegood/13.png new file mode 100644 index 00000000..2dc73ac6 Binary files /dev/null and b/image/anim/judgegood/13.png differ diff --git a/image/anim/judgegood/14.png b/image/anim/judgegood/14.png new file mode 100644 index 00000000..9f465b1d Binary files /dev/null and b/image/anim/judgegood/14.png differ diff --git a/image/anim/judgegood/15.png b/image/anim/judgegood/15.png new file mode 100644 index 00000000..8621f063 Binary files /dev/null and b/image/anim/judgegood/15.png differ diff --git a/image/anim/judgegood/16.png b/image/anim/judgegood/16.png new file mode 100644 index 00000000..0afc5cc2 Binary files /dev/null and b/image/anim/judgegood/16.png differ diff --git a/image/anim/judgegood/2.png b/image/anim/judgegood/2.png new file mode 100644 index 00000000..7af67b93 Binary files /dev/null and b/image/anim/judgegood/2.png differ diff --git a/image/anim/judgegood/3.png b/image/anim/judgegood/3.png new file mode 100644 index 00000000..c50687b1 Binary files /dev/null and b/image/anim/judgegood/3.png differ diff --git a/image/anim/judgegood/4.png b/image/anim/judgegood/4.png new file mode 100644 index 00000000..d41a1163 Binary files /dev/null and b/image/anim/judgegood/4.png differ diff --git a/image/anim/judgegood/5.png b/image/anim/judgegood/5.png new file mode 100644 index 00000000..fe476049 Binary files /dev/null and b/image/anim/judgegood/5.png differ diff --git a/image/anim/judgegood/6.png b/image/anim/judgegood/6.png new file mode 100644 index 00000000..5000493b Binary files /dev/null and b/image/anim/judgegood/6.png differ diff --git a/image/anim/judgegood/7.png b/image/anim/judgegood/7.png new file mode 100644 index 00000000..5000493b Binary files /dev/null and b/image/anim/judgegood/7.png differ diff --git a/image/anim/judgegood/8.png b/image/anim/judgegood/8.png new file mode 100644 index 00000000..5000493b Binary files /dev/null and b/image/anim/judgegood/8.png differ diff --git a/image/anim/judgegood/9.png b/image/anim/judgegood/9.png new file mode 100644 index 00000000..6335c202 Binary files /dev/null and b/image/anim/judgegood/9.png differ diff --git a/image/anim/peach/0.png b/image/anim/peach/0.png new file mode 100644 index 00000000..005e11d9 Binary files /dev/null and b/image/anim/peach/0.png differ diff --git a/image/anim/peach/1.png b/image/anim/peach/1.png new file mode 100644 index 00000000..7f9777ef Binary files /dev/null and b/image/anim/peach/1.png differ diff --git a/image/anim/peach/10.png b/image/anim/peach/10.png new file mode 100644 index 00000000..3df6c4c4 Binary files /dev/null and b/image/anim/peach/10.png differ diff --git a/image/anim/peach/11.png b/image/anim/peach/11.png new file mode 100644 index 00000000..cd0151ef Binary files /dev/null and b/image/anim/peach/11.png differ diff --git a/image/anim/peach/12.png b/image/anim/peach/12.png new file mode 100644 index 00000000..0a5f826c Binary files /dev/null and b/image/anim/peach/12.png differ diff --git a/image/anim/peach/13.png b/image/anim/peach/13.png new file mode 100644 index 00000000..b67e9156 Binary files /dev/null and b/image/anim/peach/13.png differ diff --git a/image/anim/peach/14.png b/image/anim/peach/14.png new file mode 100644 index 00000000..89fb1497 Binary files /dev/null and b/image/anim/peach/14.png differ diff --git a/image/anim/peach/15.png b/image/anim/peach/15.png new file mode 100644 index 00000000..0c4d4202 Binary files /dev/null and b/image/anim/peach/15.png differ diff --git a/image/anim/peach/16.png b/image/anim/peach/16.png new file mode 100644 index 00000000..0d0044d5 Binary files /dev/null and b/image/anim/peach/16.png differ diff --git a/image/anim/peach/2.png b/image/anim/peach/2.png new file mode 100644 index 00000000..a35cc9fa Binary files /dev/null and b/image/anim/peach/2.png differ diff --git a/image/anim/peach/3.png b/image/anim/peach/3.png new file mode 100644 index 00000000..a418a5e2 Binary files /dev/null and b/image/anim/peach/3.png differ diff --git a/image/anim/peach/4.png b/image/anim/peach/4.png new file mode 100644 index 00000000..dcb0e7e9 Binary files /dev/null and b/image/anim/peach/4.png differ diff --git a/image/anim/peach/5.png b/image/anim/peach/5.png new file mode 100644 index 00000000..96a81bb1 Binary files /dev/null and b/image/anim/peach/5.png differ diff --git a/image/anim/peach/6.png b/image/anim/peach/6.png new file mode 100644 index 00000000..64028ba0 Binary files /dev/null and b/image/anim/peach/6.png differ diff --git a/image/anim/peach/7.png b/image/anim/peach/7.png new file mode 100644 index 00000000..259c8285 Binary files /dev/null and b/image/anim/peach/7.png differ diff --git a/image/anim/peach/8.png b/image/anim/peach/8.png new file mode 100644 index 00000000..7b918694 Binary files /dev/null and b/image/anim/peach/8.png differ diff --git a/image/anim/peach/9.png b/image/anim/peach/9.png new file mode 100644 index 00000000..da7c4107 Binary files /dev/null and b/image/anim/peach/9.png differ diff --git a/image/anim/skillInvoke/control/0.png b/image/anim/skillInvoke/control/0.png new file mode 100644 index 00000000..e4aa2ada Binary files /dev/null and b/image/anim/skillInvoke/control/0.png differ diff --git a/image/anim/skillInvoke/control/1.png b/image/anim/skillInvoke/control/1.png new file mode 100644 index 00000000..216f103b Binary files /dev/null and b/image/anim/skillInvoke/control/1.png differ diff --git a/image/anim/skillInvoke/control/10.png b/image/anim/skillInvoke/control/10.png new file mode 100644 index 00000000..4c4cb219 Binary files /dev/null and b/image/anim/skillInvoke/control/10.png differ diff --git a/image/anim/skillInvoke/control/11.png b/image/anim/skillInvoke/control/11.png new file mode 100644 index 00000000..f69bb672 Binary files /dev/null and b/image/anim/skillInvoke/control/11.png differ diff --git a/image/anim/skillInvoke/control/12.png b/image/anim/skillInvoke/control/12.png new file mode 100644 index 00000000..7955ff9e Binary files /dev/null and b/image/anim/skillInvoke/control/12.png differ diff --git a/image/anim/skillInvoke/control/13.png b/image/anim/skillInvoke/control/13.png new file mode 100644 index 00000000..9cc479f1 Binary files /dev/null and b/image/anim/skillInvoke/control/13.png differ diff --git a/image/anim/skillInvoke/control/14.png b/image/anim/skillInvoke/control/14.png new file mode 100644 index 00000000..061477cb Binary files /dev/null and b/image/anim/skillInvoke/control/14.png differ diff --git a/image/anim/skillInvoke/control/2.png b/image/anim/skillInvoke/control/2.png new file mode 100644 index 00000000..1bb3b1d3 Binary files /dev/null and b/image/anim/skillInvoke/control/2.png differ diff --git a/image/anim/skillInvoke/control/3.png b/image/anim/skillInvoke/control/3.png new file mode 100644 index 00000000..c7ea51a2 Binary files /dev/null and b/image/anim/skillInvoke/control/3.png differ diff --git a/image/anim/skillInvoke/control/4.png b/image/anim/skillInvoke/control/4.png new file mode 100644 index 00000000..e3324a16 Binary files /dev/null and b/image/anim/skillInvoke/control/4.png differ diff --git a/image/anim/skillInvoke/control/5.png b/image/anim/skillInvoke/control/5.png new file mode 100644 index 00000000..2cc76b67 Binary files /dev/null and b/image/anim/skillInvoke/control/5.png differ diff --git a/image/anim/skillInvoke/control/6.png b/image/anim/skillInvoke/control/6.png new file mode 100644 index 00000000..d3d9a6f5 Binary files /dev/null and b/image/anim/skillInvoke/control/6.png differ diff --git a/image/anim/skillInvoke/control/7.png b/image/anim/skillInvoke/control/7.png new file mode 100644 index 00000000..ce7e971b Binary files /dev/null and b/image/anim/skillInvoke/control/7.png differ diff --git a/image/anim/skillInvoke/control/8.png b/image/anim/skillInvoke/control/8.png new file mode 100644 index 00000000..039ae044 Binary files /dev/null and b/image/anim/skillInvoke/control/8.png differ diff --git a/image/anim/skillInvoke/control/9.png b/image/anim/skillInvoke/control/9.png new file mode 100644 index 00000000..c1c9bc71 Binary files /dev/null and b/image/anim/skillInvoke/control/9.png differ diff --git a/image/anim/skillInvoke/defensive/0.png b/image/anim/skillInvoke/defensive/0.png new file mode 100644 index 00000000..417a4204 Binary files /dev/null and b/image/anim/skillInvoke/defensive/0.png differ diff --git a/image/anim/skillInvoke/defensive/1.png b/image/anim/skillInvoke/defensive/1.png new file mode 100644 index 00000000..c8f6b9e5 Binary files /dev/null and b/image/anim/skillInvoke/defensive/1.png differ diff --git a/image/anim/skillInvoke/defensive/10.png b/image/anim/skillInvoke/defensive/10.png new file mode 100644 index 00000000..bd2f7857 Binary files /dev/null and b/image/anim/skillInvoke/defensive/10.png differ diff --git a/image/anim/skillInvoke/defensive/11.png b/image/anim/skillInvoke/defensive/11.png new file mode 100644 index 00000000..6321231c Binary files /dev/null and b/image/anim/skillInvoke/defensive/11.png differ diff --git a/image/anim/skillInvoke/defensive/12.png b/image/anim/skillInvoke/defensive/12.png new file mode 100644 index 00000000..cf2d7b68 Binary files /dev/null and b/image/anim/skillInvoke/defensive/12.png differ diff --git a/image/anim/skillInvoke/defensive/2.png b/image/anim/skillInvoke/defensive/2.png new file mode 100644 index 00000000..69fa1e72 Binary files /dev/null and b/image/anim/skillInvoke/defensive/2.png differ diff --git a/image/anim/skillInvoke/defensive/3.png b/image/anim/skillInvoke/defensive/3.png new file mode 100644 index 00000000..0e4cb030 Binary files /dev/null and b/image/anim/skillInvoke/defensive/3.png differ diff --git a/image/anim/skillInvoke/defensive/4.png b/image/anim/skillInvoke/defensive/4.png new file mode 100644 index 00000000..0767b8c9 Binary files /dev/null and b/image/anim/skillInvoke/defensive/4.png differ diff --git a/image/anim/skillInvoke/defensive/5.png b/image/anim/skillInvoke/defensive/5.png new file mode 100644 index 00000000..fbd498fe Binary files /dev/null and b/image/anim/skillInvoke/defensive/5.png differ diff --git a/image/anim/skillInvoke/defensive/6.png b/image/anim/skillInvoke/defensive/6.png new file mode 100644 index 00000000..e9d59876 Binary files /dev/null and b/image/anim/skillInvoke/defensive/6.png differ diff --git a/image/anim/skillInvoke/defensive/7.png b/image/anim/skillInvoke/defensive/7.png new file mode 100644 index 00000000..0f0fe118 Binary files /dev/null and b/image/anim/skillInvoke/defensive/7.png differ diff --git a/image/anim/skillInvoke/defensive/8.png b/image/anim/skillInvoke/defensive/8.png new file mode 100644 index 00000000..8a22a46f Binary files /dev/null and b/image/anim/skillInvoke/defensive/8.png differ diff --git a/image/anim/skillInvoke/defensive/9.png b/image/anim/skillInvoke/defensive/9.png new file mode 100644 index 00000000..0a64b5ac Binary files /dev/null and b/image/anim/skillInvoke/defensive/9.png differ diff --git a/image/anim/skillInvoke/drawcard/0.png b/image/anim/skillInvoke/drawcard/0.png new file mode 100644 index 00000000..45c09880 Binary files /dev/null and b/image/anim/skillInvoke/drawcard/0.png differ diff --git a/image/anim/skillInvoke/drawcard/1.png b/image/anim/skillInvoke/drawcard/1.png new file mode 100644 index 00000000..65bb0fab Binary files /dev/null and b/image/anim/skillInvoke/drawcard/1.png differ diff --git a/image/anim/skillInvoke/drawcard/10.png b/image/anim/skillInvoke/drawcard/10.png new file mode 100644 index 00000000..b2844eb7 Binary files /dev/null and b/image/anim/skillInvoke/drawcard/10.png differ diff --git a/image/anim/skillInvoke/drawcard/11.png b/image/anim/skillInvoke/drawcard/11.png new file mode 100644 index 00000000..815ddc1f Binary files /dev/null and b/image/anim/skillInvoke/drawcard/11.png differ diff --git a/image/anim/skillInvoke/drawcard/12.png b/image/anim/skillInvoke/drawcard/12.png new file mode 100644 index 00000000..1139f5e3 Binary files /dev/null and b/image/anim/skillInvoke/drawcard/12.png differ diff --git a/image/anim/skillInvoke/drawcard/2.png b/image/anim/skillInvoke/drawcard/2.png new file mode 100644 index 00000000..cda1ff4a Binary files /dev/null and b/image/anim/skillInvoke/drawcard/2.png differ diff --git a/image/anim/skillInvoke/drawcard/3.png b/image/anim/skillInvoke/drawcard/3.png new file mode 100644 index 00000000..d283a501 Binary files /dev/null and b/image/anim/skillInvoke/drawcard/3.png differ diff --git a/image/anim/skillInvoke/drawcard/4.png b/image/anim/skillInvoke/drawcard/4.png new file mode 100644 index 00000000..e668cd49 Binary files /dev/null and b/image/anim/skillInvoke/drawcard/4.png differ diff --git a/image/anim/skillInvoke/drawcard/5.png b/image/anim/skillInvoke/drawcard/5.png new file mode 100644 index 00000000..9f6277f5 Binary files /dev/null and b/image/anim/skillInvoke/drawcard/5.png differ diff --git a/image/anim/skillInvoke/drawcard/6.png b/image/anim/skillInvoke/drawcard/6.png new file mode 100644 index 00000000..9ecc9d25 Binary files /dev/null and b/image/anim/skillInvoke/drawcard/6.png differ diff --git a/image/anim/skillInvoke/drawcard/7.png b/image/anim/skillInvoke/drawcard/7.png new file mode 100644 index 00000000..d8b34447 Binary files /dev/null and b/image/anim/skillInvoke/drawcard/7.png differ diff --git a/image/anim/skillInvoke/drawcard/8.png b/image/anim/skillInvoke/drawcard/8.png new file mode 100644 index 00000000..508107fa Binary files /dev/null and b/image/anim/skillInvoke/drawcard/8.png differ diff --git a/image/anim/skillInvoke/drawcard/9.png b/image/anim/skillInvoke/drawcard/9.png new file mode 100644 index 00000000..587fd977 Binary files /dev/null and b/image/anim/skillInvoke/drawcard/9.png differ diff --git a/image/anim/skillInvoke/masochism/0.png b/image/anim/skillInvoke/masochism/0.png new file mode 100644 index 00000000..39f2ce8d Binary files /dev/null and b/image/anim/skillInvoke/masochism/0.png differ diff --git a/image/anim/skillInvoke/masochism/1.png b/image/anim/skillInvoke/masochism/1.png new file mode 100644 index 00000000..c552c95b Binary files /dev/null and b/image/anim/skillInvoke/masochism/1.png differ diff --git a/image/anim/skillInvoke/masochism/10.png b/image/anim/skillInvoke/masochism/10.png new file mode 100644 index 00000000..debb291e Binary files /dev/null and b/image/anim/skillInvoke/masochism/10.png differ diff --git a/image/anim/skillInvoke/masochism/11.png b/image/anim/skillInvoke/masochism/11.png new file mode 100644 index 00000000..f68bc0dc Binary files /dev/null and b/image/anim/skillInvoke/masochism/11.png differ diff --git a/image/anim/skillInvoke/masochism/12.png b/image/anim/skillInvoke/masochism/12.png new file mode 100644 index 00000000..ffe24b87 Binary files /dev/null and b/image/anim/skillInvoke/masochism/12.png differ diff --git a/image/anim/skillInvoke/masochism/2.png b/image/anim/skillInvoke/masochism/2.png new file mode 100644 index 00000000..c1f555bb Binary files /dev/null and b/image/anim/skillInvoke/masochism/2.png differ diff --git a/image/anim/skillInvoke/masochism/3.png b/image/anim/skillInvoke/masochism/3.png new file mode 100644 index 00000000..94f3e81b Binary files /dev/null and b/image/anim/skillInvoke/masochism/3.png differ diff --git a/image/anim/skillInvoke/masochism/4.png b/image/anim/skillInvoke/masochism/4.png new file mode 100644 index 00000000..9a4403be Binary files /dev/null and b/image/anim/skillInvoke/masochism/4.png differ diff --git a/image/anim/skillInvoke/masochism/5.png b/image/anim/skillInvoke/masochism/5.png new file mode 100644 index 00000000..908beed1 Binary files /dev/null and b/image/anim/skillInvoke/masochism/5.png differ diff --git a/image/anim/skillInvoke/masochism/6.png b/image/anim/skillInvoke/masochism/6.png new file mode 100644 index 00000000..45a90d95 Binary files /dev/null and b/image/anim/skillInvoke/masochism/6.png differ diff --git a/image/anim/skillInvoke/masochism/7.png b/image/anim/skillInvoke/masochism/7.png new file mode 100644 index 00000000..2ecebf71 Binary files /dev/null and b/image/anim/skillInvoke/masochism/7.png differ diff --git a/image/anim/skillInvoke/masochism/8.png b/image/anim/skillInvoke/masochism/8.png new file mode 100644 index 00000000..8f87642d Binary files /dev/null and b/image/anim/skillInvoke/masochism/8.png differ diff --git a/image/anim/skillInvoke/masochism/9.png b/image/anim/skillInvoke/masochism/9.png new file mode 100644 index 00000000..4f41c56d Binary files /dev/null and b/image/anim/skillInvoke/masochism/9.png differ diff --git a/image/anim/skillInvoke/negative/0.png b/image/anim/skillInvoke/negative/0.png new file mode 100644 index 00000000..1d9f2971 Binary files /dev/null and b/image/anim/skillInvoke/negative/0.png differ diff --git a/image/anim/skillInvoke/negative/1.png b/image/anim/skillInvoke/negative/1.png new file mode 100644 index 00000000..7f2b95ba Binary files /dev/null and b/image/anim/skillInvoke/negative/1.png differ diff --git a/image/anim/skillInvoke/negative/10.png b/image/anim/skillInvoke/negative/10.png new file mode 100644 index 00000000..adfdd062 Binary files /dev/null and b/image/anim/skillInvoke/negative/10.png differ diff --git a/image/anim/skillInvoke/negative/11.png b/image/anim/skillInvoke/negative/11.png new file mode 100644 index 00000000..fd9eec11 Binary files /dev/null and b/image/anim/skillInvoke/negative/11.png differ diff --git a/image/anim/skillInvoke/negative/12.png b/image/anim/skillInvoke/negative/12.png new file mode 100644 index 00000000..0a69095c Binary files /dev/null and b/image/anim/skillInvoke/negative/12.png differ diff --git a/image/anim/skillInvoke/negative/2.png b/image/anim/skillInvoke/negative/2.png new file mode 100644 index 00000000..70166eef Binary files /dev/null and b/image/anim/skillInvoke/negative/2.png differ diff --git a/image/anim/skillInvoke/negative/3.png b/image/anim/skillInvoke/negative/3.png new file mode 100644 index 00000000..fa3bd67d Binary files /dev/null and b/image/anim/skillInvoke/negative/3.png differ diff --git a/image/anim/skillInvoke/negative/4.png b/image/anim/skillInvoke/negative/4.png new file mode 100644 index 00000000..312fb687 Binary files /dev/null and b/image/anim/skillInvoke/negative/4.png differ diff --git a/image/anim/skillInvoke/negative/5.png b/image/anim/skillInvoke/negative/5.png new file mode 100644 index 00000000..446b1df1 Binary files /dev/null and b/image/anim/skillInvoke/negative/5.png differ diff --git a/image/anim/skillInvoke/negative/6.png b/image/anim/skillInvoke/negative/6.png new file mode 100644 index 00000000..ce1b194d Binary files /dev/null and b/image/anim/skillInvoke/negative/6.png differ diff --git a/image/anim/skillInvoke/negative/7.png b/image/anim/skillInvoke/negative/7.png new file mode 100644 index 00000000..b276ca1a Binary files /dev/null and b/image/anim/skillInvoke/negative/7.png differ diff --git a/image/anim/skillInvoke/negative/8.png b/image/anim/skillInvoke/negative/8.png new file mode 100644 index 00000000..26dc12ef Binary files /dev/null and b/image/anim/skillInvoke/negative/8.png differ diff --git a/image/anim/skillInvoke/negative/9.png b/image/anim/skillInvoke/negative/9.png new file mode 100644 index 00000000..30bdc063 Binary files /dev/null and b/image/anim/skillInvoke/negative/9.png differ diff --git a/image/anim/skillInvoke/offensive/0.png b/image/anim/skillInvoke/offensive/0.png new file mode 100644 index 00000000..343b99a5 Binary files /dev/null and b/image/anim/skillInvoke/offensive/0.png differ diff --git a/image/anim/skillInvoke/offensive/1.png b/image/anim/skillInvoke/offensive/1.png new file mode 100644 index 00000000..e90b2231 Binary files /dev/null and b/image/anim/skillInvoke/offensive/1.png differ diff --git a/image/anim/skillInvoke/offensive/10.png b/image/anim/skillInvoke/offensive/10.png new file mode 100644 index 00000000..26cf15e3 Binary files /dev/null and b/image/anim/skillInvoke/offensive/10.png differ diff --git a/image/anim/skillInvoke/offensive/11.png b/image/anim/skillInvoke/offensive/11.png new file mode 100644 index 00000000..b1ae100b Binary files /dev/null and b/image/anim/skillInvoke/offensive/11.png differ diff --git a/image/anim/skillInvoke/offensive/12.png b/image/anim/skillInvoke/offensive/12.png new file mode 100644 index 00000000..b1ae100b Binary files /dev/null and b/image/anim/skillInvoke/offensive/12.png differ diff --git a/image/anim/skillInvoke/offensive/2.png b/image/anim/skillInvoke/offensive/2.png new file mode 100644 index 00000000..d72c8165 Binary files /dev/null and b/image/anim/skillInvoke/offensive/2.png differ diff --git a/image/anim/skillInvoke/offensive/3.png b/image/anim/skillInvoke/offensive/3.png new file mode 100644 index 00000000..8dae743e Binary files /dev/null and b/image/anim/skillInvoke/offensive/3.png differ diff --git a/image/anim/skillInvoke/offensive/4.png b/image/anim/skillInvoke/offensive/4.png new file mode 100644 index 00000000..3807d4d7 Binary files /dev/null and b/image/anim/skillInvoke/offensive/4.png differ diff --git a/image/anim/skillInvoke/offensive/5.png b/image/anim/skillInvoke/offensive/5.png new file mode 100644 index 00000000..b45f61c6 Binary files /dev/null and b/image/anim/skillInvoke/offensive/5.png differ diff --git a/image/anim/skillInvoke/offensive/6.png b/image/anim/skillInvoke/offensive/6.png new file mode 100644 index 00000000..e9b0a7aa Binary files /dev/null and b/image/anim/skillInvoke/offensive/6.png differ diff --git a/image/anim/skillInvoke/offensive/7.png b/image/anim/skillInvoke/offensive/7.png new file mode 100644 index 00000000..80a22d44 Binary files /dev/null and b/image/anim/skillInvoke/offensive/7.png differ diff --git a/image/anim/skillInvoke/offensive/8.png b/image/anim/skillInvoke/offensive/8.png new file mode 100644 index 00000000..5b199f59 Binary files /dev/null and b/image/anim/skillInvoke/offensive/8.png differ diff --git a/image/anim/skillInvoke/offensive/9.png b/image/anim/skillInvoke/offensive/9.png new file mode 100644 index 00000000..fecfca5a Binary files /dev/null and b/image/anim/skillInvoke/offensive/9.png differ diff --git a/image/anim/skillInvoke/special/0.png b/image/anim/skillInvoke/special/0.png new file mode 100644 index 00000000..b9dee948 Binary files /dev/null and b/image/anim/skillInvoke/special/0.png differ diff --git a/image/anim/skillInvoke/special/1.png b/image/anim/skillInvoke/special/1.png new file mode 100644 index 00000000..ef61acb5 Binary files /dev/null and b/image/anim/skillInvoke/special/1.png differ diff --git a/image/anim/skillInvoke/special/10.png b/image/anim/skillInvoke/special/10.png new file mode 100644 index 00000000..a43e809b Binary files /dev/null and b/image/anim/skillInvoke/special/10.png differ diff --git a/image/anim/skillInvoke/special/11.png b/image/anim/skillInvoke/special/11.png new file mode 100644 index 00000000..2b194790 Binary files /dev/null and b/image/anim/skillInvoke/special/11.png differ diff --git a/image/anim/skillInvoke/special/12.png b/image/anim/skillInvoke/special/12.png new file mode 100644 index 00000000..f65525dc Binary files /dev/null and b/image/anim/skillInvoke/special/12.png differ diff --git a/image/anim/skillInvoke/special/13.png b/image/anim/skillInvoke/special/13.png new file mode 100644 index 00000000..7a008869 Binary files /dev/null and b/image/anim/skillInvoke/special/13.png differ diff --git a/image/anim/skillInvoke/special/14.png b/image/anim/skillInvoke/special/14.png new file mode 100644 index 00000000..3d5ffe10 Binary files /dev/null and b/image/anim/skillInvoke/special/14.png differ diff --git a/image/anim/skillInvoke/special/15.png b/image/anim/skillInvoke/special/15.png new file mode 100644 index 00000000..00d35aa4 Binary files /dev/null and b/image/anim/skillInvoke/special/15.png differ diff --git a/image/anim/skillInvoke/special/16.png b/image/anim/skillInvoke/special/16.png new file mode 100644 index 00000000..e2f75846 Binary files /dev/null and b/image/anim/skillInvoke/special/16.png differ diff --git a/image/anim/skillInvoke/special/2.png b/image/anim/skillInvoke/special/2.png new file mode 100644 index 00000000..a76b1702 Binary files /dev/null and b/image/anim/skillInvoke/special/2.png differ diff --git a/image/anim/skillInvoke/special/3.png b/image/anim/skillInvoke/special/3.png new file mode 100644 index 00000000..2749dbe9 Binary files /dev/null and b/image/anim/skillInvoke/special/3.png differ diff --git a/image/anim/skillInvoke/special/4.png b/image/anim/skillInvoke/special/4.png new file mode 100644 index 00000000..dea53d76 Binary files /dev/null and b/image/anim/skillInvoke/special/4.png differ diff --git a/image/anim/skillInvoke/special/5.png b/image/anim/skillInvoke/special/5.png new file mode 100644 index 00000000..93bb6f0f Binary files /dev/null and b/image/anim/skillInvoke/special/5.png differ diff --git a/image/anim/skillInvoke/special/6.png b/image/anim/skillInvoke/special/6.png new file mode 100644 index 00000000..5b1337cd Binary files /dev/null and b/image/anim/skillInvoke/special/6.png differ diff --git a/image/anim/skillInvoke/special/7.png b/image/anim/skillInvoke/special/7.png new file mode 100644 index 00000000..b41c44f5 Binary files /dev/null and b/image/anim/skillInvoke/special/7.png differ diff --git a/image/anim/skillInvoke/special/8.png b/image/anim/skillInvoke/special/8.png new file mode 100644 index 00000000..625cc801 Binary files /dev/null and b/image/anim/skillInvoke/special/8.png differ diff --git a/image/anim/skillInvoke/special/9.png b/image/anim/skillInvoke/special/9.png new file mode 100644 index 00000000..7057b2f5 Binary files /dev/null and b/image/anim/skillInvoke/special/9.png differ diff --git a/image/anim/skillInvoke/support/0.png b/image/anim/skillInvoke/support/0.png new file mode 100644 index 00000000..2006a9e9 Binary files /dev/null and b/image/anim/skillInvoke/support/0.png differ diff --git a/image/anim/skillInvoke/support/1.png b/image/anim/skillInvoke/support/1.png new file mode 100644 index 00000000..802cdc68 Binary files /dev/null and b/image/anim/skillInvoke/support/1.png differ diff --git a/image/anim/skillInvoke/support/10.png b/image/anim/skillInvoke/support/10.png new file mode 100644 index 00000000..689ad660 Binary files /dev/null and b/image/anim/skillInvoke/support/10.png differ diff --git a/image/anim/skillInvoke/support/11.png b/image/anim/skillInvoke/support/11.png new file mode 100644 index 00000000..9465c0ed Binary files /dev/null and b/image/anim/skillInvoke/support/11.png differ diff --git a/image/anim/skillInvoke/support/12.png b/image/anim/skillInvoke/support/12.png new file mode 100644 index 00000000..8f031d09 Binary files /dev/null and b/image/anim/skillInvoke/support/12.png differ diff --git a/image/anim/skillInvoke/support/13.png b/image/anim/skillInvoke/support/13.png new file mode 100644 index 00000000..566613f3 Binary files /dev/null and b/image/anim/skillInvoke/support/13.png differ diff --git a/image/anim/skillInvoke/support/14.png b/image/anim/skillInvoke/support/14.png new file mode 100644 index 00000000..fb2287a9 Binary files /dev/null and b/image/anim/skillInvoke/support/14.png differ diff --git a/image/anim/skillInvoke/support/2.png b/image/anim/skillInvoke/support/2.png new file mode 100644 index 00000000..c6e84044 Binary files /dev/null and b/image/anim/skillInvoke/support/2.png differ diff --git a/image/anim/skillInvoke/support/3.png b/image/anim/skillInvoke/support/3.png new file mode 100644 index 00000000..34806539 Binary files /dev/null and b/image/anim/skillInvoke/support/3.png differ diff --git a/image/anim/skillInvoke/support/4.png b/image/anim/skillInvoke/support/4.png new file mode 100644 index 00000000..e76dce33 Binary files /dev/null and b/image/anim/skillInvoke/support/4.png differ diff --git a/image/anim/skillInvoke/support/5.png b/image/anim/skillInvoke/support/5.png new file mode 100644 index 00000000..522bfbe6 Binary files /dev/null and b/image/anim/skillInvoke/support/5.png differ diff --git a/image/anim/skillInvoke/support/6.png b/image/anim/skillInvoke/support/6.png new file mode 100644 index 00000000..0ae39304 Binary files /dev/null and b/image/anim/skillInvoke/support/6.png differ diff --git a/image/anim/skillInvoke/support/7.png b/image/anim/skillInvoke/support/7.png new file mode 100644 index 00000000..b3d7fa7a Binary files /dev/null and b/image/anim/skillInvoke/support/7.png differ diff --git a/image/anim/skillInvoke/support/8.png b/image/anim/skillInvoke/support/8.png new file mode 100644 index 00000000..c2cc1372 Binary files /dev/null and b/image/anim/skillInvoke/support/8.png differ diff --git a/image/anim/skillInvoke/support/9.png b/image/anim/skillInvoke/support/9.png new file mode 100644 index 00000000..6ead5d44 Binary files /dev/null and b/image/anim/skillInvoke/support/9.png differ diff --git a/image/anim/slash/0.png b/image/anim/slash/0.png new file mode 100644 index 00000000..0caa1c3f Binary files /dev/null and b/image/anim/slash/0.png differ diff --git a/image/anim/slash/1.png b/image/anim/slash/1.png new file mode 100644 index 00000000..febb6239 Binary files /dev/null and b/image/anim/slash/1.png differ diff --git a/image/anim/slash/10.png b/image/anim/slash/10.png new file mode 100644 index 00000000..8eb07366 Binary files /dev/null and b/image/anim/slash/10.png differ diff --git a/image/anim/slash/11.png b/image/anim/slash/11.png new file mode 100644 index 00000000..e7329d97 Binary files /dev/null and b/image/anim/slash/11.png differ diff --git a/image/anim/slash/12.png b/image/anim/slash/12.png new file mode 100644 index 00000000..a11cd3f5 Binary files /dev/null and b/image/anim/slash/12.png differ diff --git a/image/anim/slash/13.png b/image/anim/slash/13.png new file mode 100644 index 00000000..1ae51ba9 Binary files /dev/null and b/image/anim/slash/13.png differ diff --git a/image/anim/slash/14.png b/image/anim/slash/14.png new file mode 100644 index 00000000..75e559ea Binary files /dev/null and b/image/anim/slash/14.png differ diff --git a/image/anim/slash/15.png b/image/anim/slash/15.png new file mode 100644 index 00000000..c367a04f Binary files /dev/null and b/image/anim/slash/15.png differ diff --git a/image/anim/slash/16.png b/image/anim/slash/16.png new file mode 100644 index 00000000..96c5dd1f Binary files /dev/null and b/image/anim/slash/16.png differ diff --git a/image/anim/slash/17.png b/image/anim/slash/17.png new file mode 100644 index 00000000..96c5dd1f Binary files /dev/null and b/image/anim/slash/17.png differ diff --git a/image/anim/slash/18.png b/image/anim/slash/18.png new file mode 100644 index 00000000..96c5dd1f Binary files /dev/null and b/image/anim/slash/18.png differ diff --git a/image/anim/slash/19.png b/image/anim/slash/19.png new file mode 100644 index 00000000..96c5dd1f Binary files /dev/null and b/image/anim/slash/19.png differ diff --git a/image/anim/slash/2.png b/image/anim/slash/2.png new file mode 100644 index 00000000..febb6239 Binary files /dev/null and b/image/anim/slash/2.png differ diff --git a/image/anim/slash/20.png b/image/anim/slash/20.png new file mode 100644 index 00000000..96c5dd1f Binary files /dev/null and b/image/anim/slash/20.png differ diff --git a/image/anim/slash/21.png b/image/anim/slash/21.png new file mode 100644 index 00000000..96c5dd1f Binary files /dev/null and b/image/anim/slash/21.png differ diff --git a/image/anim/slash/22.png b/image/anim/slash/22.png new file mode 100644 index 00000000..854e1cb8 Binary files /dev/null and b/image/anim/slash/22.png differ diff --git a/image/anim/slash/23.png b/image/anim/slash/23.png new file mode 100644 index 00000000..44877036 Binary files /dev/null and b/image/anim/slash/23.png differ diff --git a/image/anim/slash/3.png b/image/anim/slash/3.png new file mode 100644 index 00000000..2eadc2bd Binary files /dev/null and b/image/anim/slash/3.png differ diff --git a/image/anim/slash/4.png b/image/anim/slash/4.png new file mode 100644 index 00000000..07061a3e Binary files /dev/null and b/image/anim/slash/4.png differ diff --git a/image/anim/slash/5.png b/image/anim/slash/5.png new file mode 100644 index 00000000..d5eb272f Binary files /dev/null and b/image/anim/slash/5.png differ diff --git a/image/anim/slash/6.png b/image/anim/slash/6.png new file mode 100644 index 00000000..36c5f5e9 Binary files /dev/null and b/image/anim/slash/6.png differ diff --git a/image/anim/slash/7.png b/image/anim/slash/7.png new file mode 100644 index 00000000..7b62a0ba Binary files /dev/null and b/image/anim/slash/7.png differ diff --git a/image/anim/slash/8.png b/image/anim/slash/8.png new file mode 100644 index 00000000..b987346e Binary files /dev/null and b/image/anim/slash/8.png differ diff --git a/image/anim/slash/9.png b/image/anim/slash/9.png new file mode 100644 index 00000000..c3db9deb Binary files /dev/null and b/image/anim/slash/9.png differ diff --git a/lang/zh_CN.ts b/lang/zh_CN.ts new file mode 100644 index 00000000..9d1361fc --- /dev/null +++ b/lang/zh_CN.ts @@ -0,0 +1,71 @@ + + + ClientSocket + + Connection was refused or timeout + 连接被拒绝或超时 + + + Remote host close this connection + 远程主机已关闭此次连接 + + + Host not found + 未找到主机 + + + Socket access error + 套接字访问错误 + + + Unknown error + 未知错误 + + + Connection failed, error code = %1 + reason: %2 + 连接失败,错误码 = %1 + 原因:%2 + + + + + Init + + Username + 用户名 + + + Password + 密码 + + + Join Server + 加入服务器 + + + Console start + 单机启动 + + + + + Logic + + MD5 check failed! + MD5检测失败!请与服务端保持一致后再登入 + + + others logged in with this name + 已经有人用这个名字登入了 + + + invalid user name + 用户名不合法,只能含有英数字和汉字 + + + username or password error + 用户名或密码错误 + + + diff --git a/lua/client/client.lua b/lua/client/client.lua index 04bc50a3..28d2ca15 100644 --- a/lua/client/client.lua +++ b/lua/client/client.lua @@ -44,6 +44,22 @@ function Client:getPlayerById(id) return nil end +---@param cardId integer | card +---@return CardArea +function Client:getCardArea(cardId) + if type(cardId) ~= "number" then + assert(cardId and cardId:isInstanceOf(Card)) + cardId = cardId:getEffectiveId() + end + if table.contains(Self.player_cards[Player.Hand], cardId) then + return Card.PlayerHand + end + if table.contains(Self.player_cards[Player.Equip], cardId) then + return Card.PlayerEquip + end + error("Client:getCardArea can only judge cards in your hand or equip area") +end + function Client:moveCards(moves) for _, move in ipairs(moves) do if move.from and move.fromArea then @@ -383,9 +399,35 @@ fk.client_callback["AddCardUseHistory"] = function(jsonData) Self:addCardUseHistory(data[1], data[2]) end -fk.client_callback["ResetCardUseHistory"] = function(jsonData) - if jsonData == "" then jsonData = nil end - Self:resetCardUseHistory(jsonData) +fk.client_callback["SetCardUseHistory"] = function(jsonData) + local data = json.decode(jsonData) + Self:setCardUseHistory(data[1], data[2], data[3]) +end + +fk.client_callback["AddSkillUseHistory"] = function(jsonData) + local data = json.decode(jsonData) + Self:addSkillUseHistory(data[1], data[2]) +end + +fk.client_callback["SetSkillUseHistory"] = function(jsonData) + local data = json.decode(jsonData) + Self:setSkillUseHistory(data[1], data[2], data[3]) +end + +fk.client_callback["AddVirtualEquip"] = function(jsonData) + local data = json.decode(jsonData) + local cname = data.name + local player = ClientInstance:getPlayerById(data.player) + local subcards = data.subcards + local c = Fk:cloneCard(cname) + c:addSubcards(subcards) + player:addVirtualEquip(c) +end + +fk.client_callback["RemoveVirtualEquip"] = function(jsonData) + local data = json.decode(jsonData) + local player = ClientInstance:getPlayerById(data.player) + player:removeVirtualEquip(data.id) end -- Create ClientInstance (used by Lua) diff --git a/lua/client/client_util.lua b/lua/client/client_util.lua index f3b13231..5d35eeec 100644 --- a/lua/client/client_util.lua +++ b/lua/client/client_util.lua @@ -90,7 +90,18 @@ function CanUseCard(card, player) if type(card) == "number" then c = Fk:getCardById(card) else - error() + local data = json.decode(card) + local skill = Fk.skills[data.skill] + local selected_cards = data.subcards + if skill:isInstanceOf(ViewAsSkill) then + c = skill:viewAs(selected_cards) + if not c then + return "false" + end + else + -- ActiveSkill should return true here + return "true" + end end local ret = c.skill:canUse(ClientInstance:getPlayerById(player)) @@ -188,6 +199,18 @@ function ActiveCanUse(skill_name) ret = skill:canUse(Self) elseif skill:isInstanceOf(ViewAsSkill) then ret = skill:enabledAtPlay(Self) + if ret then + local exp = Exppattern:Parse(skill.pattern) + local cnames = {} + for _, m in ipairs(exp.matchers) do + if m.name then table.insertTable(cnames, m.name) end + end + for _, n in ipairs(cnames) do + local c = Fk:cloneCard(n) + ret = c.skill:canUse(Self) + if ret then break end + end + end end end return json.encode(ret) @@ -251,9 +274,29 @@ function CanViewAs(skill_name, card_ids) return json.encode(ret) end +-- card_name may be id, name of card, or json string function CardFitPattern(card_name, pattern) local exp = Exppattern:Parse(pattern) - local ret = exp:matchExp(card_name) + local c + local ret = false + if type(card_name) == "number" then + c = Fk:getCardById(card_name) + ret = exp:match(c) + elseif string.sub(card_name, 1, 1) == "{" then + local data = json.decode(card_name) + local skill = Fk.skills[data.skill] + local selected_cards = data.subcards + if skill:isInstanceOf(ViewAsSkill) then + c = skill:viewAs(selected_cards) + if c then + ret = exp:match(c) + end + else + return "true" + end + else + ret = exp:matchExp(card_name) + end return json.encode(ret) end @@ -267,6 +310,24 @@ function SkillFitPattern(skill_name, pattern) return json.encode(ret) end +function SkillCanResponse(skill_name) + local skill = Fk.skills[skill_name] + local ret = false + if skill and skill:isInstanceOf(ViewAsSkill) then + ret = skill:enabledAtResponse(Self) + end + return json.encode(ret) +end + +function GetVirtualEquip(player, cid) + local c = ClientInstance:getPlayerById(player):getVirualEquip(cid) + if not c then return "null" end + return json.encode{ + name = c.name, + cid = c.subcards[1], + } +end + Fk:loadTranslationTable{ -- Lobby ["Room List"] = "房间列表", @@ -333,6 +394,9 @@ Fk:loadTranslationTable{ ["#AskForGeneral"] = "请选择 1 名武将", ["#AskForSkillInvoke"] = "你想发动技能“%1”吗?", ["#AskForChoice"] = "%1:请选择", + ["#choose-trigger"] = "请选择一项技能发动", + ["trigger"] = "选择技能", + ["Please arrange cards"] = "请拖拽移动卡牌", [" thinking..."] = " 思考中...", ["AskForGeneral"] = "选择武将", @@ -348,7 +412,10 @@ Fk:loadTranslationTable{ ["#AskForUseActiveSkill"] = "请使用技能 %1", ["#AskForUseCard"] = "请使用卡牌 %1", ["#AskForResponseCard"] = "请打出卡牌 %1", - ["#AskForNullification"] = "无懈", + ["#AskForNullification"] = "是否为目标为 %dest 的 %arg 使用无懈可击?", + ["#AskForNullificationWithoutTo"] = "是否对 %src 使用的 %arg 使用无懈可击?", + + ["#AskForDiscard"] = "请弃置 %arg 张牌,最少 %arg2 张", ["Trust"] = "托管", ["Sort Cards"] = "牌序", @@ -449,4 +516,7 @@ Fk:loadTranslationTable{ ["#EnterDying"] = "%from 进入了濒死阶段", ["#KillPlayer"] = "%from [%arg] 阵亡,凶手是 %to", ["#KillPlayerWithNoKiller"] = "%from [%arg] 阵亡,无伤害来源", + + -- misc + ["#GuanxingResult"] = "%from 的观星结果为 %arg 上 %arg2 下", } diff --git a/lua/core/card.lua b/lua/core/card.lua index f29e094e..b4e1a958 100644 --- a/lua/core/card.lua +++ b/lua/core/card.lua @@ -145,6 +145,10 @@ function Card:clearSubcards() updateColorAndNumber(self) end +function Card:matchPattern(pattern) + return Exppattern:Parse(pattern):match(self) +end + function Card:getSuitString() local suit = self.suit if suit == Card.Spade then diff --git a/lua/core/engine.lua b/lua/core/engine.lua index c2e00002..127a09fa 100644 --- a/lua/core/engine.lua +++ b/lua/core/engine.lua @@ -109,6 +109,10 @@ function Engine:addSkill(skill) table.insert(t[skill.class], skill) end end + + for _, s in ipairs(skill.related_skills) do + self:addSkill(s) + end end ---@param skills Skill[] diff --git a/lua/core/exppattern.lua b/lua/core/exppattern.lua index 533e9738..07cec557 100644 --- a/lua/core/exppattern.lua +++ b/lua/core/exppattern.lua @@ -19,10 +19,10 @@ ---@class Matcher ---@field name string[] ---@field number integer[] ----@field suit integer[] +---@field suit string[] ---@field place string[] ---@field generalName string[] ----@field cardType integer[] +---@field cardType string[] ---@field id integer[] local numbertable = { @@ -39,6 +39,11 @@ local suittable = { [Card.Diamond] = "diamond", } +local placetable = { + [Card.PlayerHand] = "hand", + [Card.PlayerEquip] = "equip", +} + local typetable = { [Card.TypeBasic] = "basic", [Card.TypeTrick] = "trick", @@ -64,7 +69,12 @@ local function matchCard(matcher, card) return false end - -- TODO: place + if matcher.place and not table.contains( + matcher.place, + placetable[Fk:currentRoom():getCardArea(card.id)] + ) then + return false + end -- TODO: generalName if matcher.cardType and not table.contains(matcher.cardType, typetable[card.type]) then @@ -142,38 +152,29 @@ local function parseMatcher(str) if n then table.insertIfNeed(ret.number, n) else - if string.find(n, "~") then - local start, _end = table.unpack(n:split("~")) + if string.find(num, "~") then + local s, e = table.unpack(num:split("~")) + local start = tonumber(s) + if not start then + start = numbertable[s] + end + local _end = tonumber(e) + if not _end then + _end = numbertable[e] + end + for i = start, _end do - table.insertIfNeed(ret.number, n) + table.insertIfNeed(ret.number, i) end end end end end - if not table.contains(t[3], ".") then - ret.suit = {} - for _, num in ipairs(t[3]) do - local n = suittable[num] - if n then - table.insertIfNeed(ret.suit, n) - end - end - end - + ret.suit = not table.contains(t[3], ".") and t[3] or nil ret.place = not table.contains(t[4], ".") and t[4] or nil ret.generalName = not table.contains(t[5], ".") and t[5] or nil - - if not table.contains(t[6], ".") then - ret.cardType = {} - for _, num in ipairs(t[6]) do - local n = typetable[num] - if n then - table.insertIfNeed(ret.cardType, n) - end - end - end + ret.cardType = not table.contains(t[6], ".") and t[6] or nil if not table.contains(t[7], ".") then ret.id = {} diff --git a/lua/core/player.lua b/lua/core/player.lua index c5977a81..14d48b30 100644 --- a/lua/core/player.lua +++ b/lua/core/player.lua @@ -5,6 +5,7 @@ ---@field kingdom string ---@field role string ---@field general string +---@field gender integer ---@field handcard_num integer ---@field seat integer ---@field next Player @@ -20,8 +21,10 @@ ---@field tag table ---@field mark table ---@field player_cards table +---@field virtual_equips Card[] ---@field special_cards table ----@field cardUsedHistory table +---@field cardUsedHistory table +---@field skillUsedHistory table ---@field fixedDistance table local Player = class("Player") @@ -44,6 +47,11 @@ Player.Equip = 2 Player.Judge = 3 Player.Special = 4 +Player.HistoryPhase = 1 +Player.HistoryTurn = 2 +Player.HistoryRound = 3 +Player.HistoryGame = 4 + function Player:initialize() self.id = 114514 self.hp = 0 @@ -51,6 +59,7 @@ function Player:initialize() self.kingdom = "qun" self.role = "" self.general = "" + self.gender = General.Male self.seat = 0 self.next = nil self.phase = Player.PhaseNone @@ -70,9 +79,11 @@ function Player:initialize() [Player.Equip] = {}, [Player.Judge] = {}, } + self.virtual_equips = {} self.special_cards = {} self.cardUsedHistory = {} + self.skillUsedHistory = {} self.fixedDistance = {} end @@ -182,6 +193,47 @@ function Player:removeCards(playerArea, cardIds, specialName) end end +-- virtual delayed trick can use these functions too + +---@param card Card +function Player:addVirtualEquip(card) + assert(card and card:isInstanceOf(Card) and card:isVirtual()) + table.insertIfNeed(self.virtual_equips, card) +end + +---@param cid integer +function Player:removeVirtualEquip(cid) + for _, c in ipairs(self.virtual_equips) do + for _, id in ipairs(c.subcards) do + if id == cid then + table.removeOne(self.virtual_equips, c) + return c + end + end + end +end + +---@param cid integer +function Player:getVirualEquip(cid) + for _, c in ipairs(self.virtual_equips) do + for _, id in ipairs(c.subcards) do + if id == cid then + return c + end + end + end +end + +function Player:hasDelayedTrick(card_name) + for _, id in ipairs(self:getCardIds(Player.Judge)) do + local c = self:getVirualEquip(id) + if not c then c = Fk:getCardById(id) end + if c.name == card_name then + return true + end + end +end + ---@param playerAreas PlayerCardArea ---@param specialName string ---@return integer[] @@ -272,6 +324,7 @@ function Player:distanceTo(other) local status_skills = Fk:currentRoom().status_skills[DistanceSkill] or {} for _, skill in ipairs(status_skills) do local correct = skill:getCorrect(self, other) + if correct == nil then correct = 0 end ret = ret + correct end @@ -297,23 +350,79 @@ function Player:inMyAttackRange(other) end function Player:addCardUseHistory(cardName, num) + num = num or 1 assert(type(num) == "number" and num ~= 0) - self.cardUsedHistory[cardName] = self.cardUsedHistory[cardName] or 0 - self.cardUsedHistory[cardName] = self.cardUsedHistory[cardName] + num + self.cardUsedHistory[cardName] = self.cardUsedHistory[cardName] or {0, 0, 0, 0} + local t = self.cardUsedHistory[cardName] + for i, _ in ipairs(t) do + t[i] = t[i] + num + end end -function Player:resetCardUseHistory(cardName) - if not cardName then +function Player:setCardUseHistory(cardName, num, scope) + if cardName == "" and num == nil and scope == nil then self.cardUsedHistory = {} + return end + + num = num or 0 + if cardName == "" then + for _, v in pairs(self.cardUsedHistory) do + v[scope] = num + end + return + end + if self.cardUsedHistory[cardName] then - self.cardUsedHistory[cardName] = 0 + self.cardUsedHistory[cardName][scope] = num end end -function Player:usedTimes(cardName) - return self.cardUsedHistory[cardName] or 0 +function Player:addSkillUseHistory(skill_name, num) + num = num or 1 + assert(type(num) == "number" and num ~= 0) + + self.skillUsedHistory[skill_name] = self.skillUsedHistory[skill_name] or {0, 0, 0, 0} + local t = self.skillUsedHistory[skill_name] + for i, _ in ipairs(t) do + t[i] = t[i] + num + end +end + +function Player:setSkillUseHistory(skill_name, num, scope) + if skill_name == "" and num == nil and scope == nil then + self.skillUsedHistory = {} + return + end + + num = num or 0 + if skill_name == "" then + for _, v in pairs(self.skillUsedHistory) do + v[scope] = num + end + return + end + + if self.skillUsedHistory[skill_name] then + self.skillUsedHistory[skill_name][scope] = num + end +end + +function Player:usedCardTimes(cardName, scope) + if not self.cardUsedHistory[cardName] then + return 0 + end + scope = scope or Player.HistoryTurn + return self.cardUsedHistory[cardName][scope] +end + +function Player:usedSkillTimes(cardName, scope) + if not self.skillUsedHistory[cardName] then + return 0 + end + scope = scope or Player.HistoryTurn + return self.skillUsedHistory[cardName][scope] end function Player:isKongcheng() @@ -390,12 +499,12 @@ function Player:addSkill(skill, source_skill) for _, s in ipairs(toget) do if not self:hasSkill(s) then table.insert(ret, s) - if skill:isInstanceOf(TriggerSkill) and RoomInstance then - room.logic:addTriggerSkill(skill) + if s:isInstanceOf(TriggerSkill) and RoomInstance then + room.logic:addTriggerSkill(s) end - if table.contains(StatusSkills, skill.class) then + if s:isInstanceOf(StatusSkill) then room.status_skills[skill.class] = room.status_skills[skill.class] or {} - table.insertIfNeed(room.status_skills[skill.class], skill) + table.insertIfNeed(room.status_skills[skill.class], s) end end end diff --git a/lua/core/skill.lua b/lua/core/skill.lua index 7b32172d..861cd970 100644 --- a/lua/core/skill.lua +++ b/lua/core/skill.lua @@ -2,6 +2,8 @@ ---@field name string ---@field frequency Frequency ---@field visible boolean +---@field mute boolean +---@field anim_type string ---@field related_skills Skill[] local Skill = class("Skill") @@ -18,7 +20,13 @@ function Skill:initialize(name, frequency) self.name = name self.frequency = frequency self.visible = true + self.mute = false + self.anim_type = "" self.related_skills = {} + + if string.sub(name, 1, 1) == "#" then + self.visible = false + end end ---@param skill Skill diff --git a/lua/core/skill_type/active_skill.lua b/lua/core/skill_type/active.lua similarity index 89% rename from lua/core/skill_type/active_skill.lua rename to lua/core/skill_type/active.lua index 795ce3f1..8236ccc9 100644 --- a/lua/core/skill_type/active_skill.lua +++ b/lua/core/skill_type/active.lua @@ -1,10 +1,8 @@ ---- ActiveSkill is a skill type like SkillCard+ViewAsSkill in QSanguosha ---- ----@class ActiveSkill : Skill -local ActiveSkill = Skill:subclass("ActiveSkill") +---@class ActiveSkill : UsableSkill +local ActiveSkill = UsableSkill:subclass("ActiveSkill") function ActiveSkill:initialize(name) - Skill.initialize(self, name, Skill.NotFrequent) + UsableSkill.initialize(self, name, Skill.NotFrequent) end --------- diff --git a/lua/core/skill_type/attack_range.lua b/lua/core/skill_type/attack_range.lua index 50624a7f..395b429b 100644 --- a/lua/core/skill_type/attack_range.lua +++ b/lua/core/skill_type/attack_range.lua @@ -1,12 +1,5 @@ ----@class AttackRangeSkill : Skill ----@field global boolean -local AttackRangeSkill = Skill:subclass("AttackRangeSkill") - -function AttackRangeSkill:initialize(name) - Skill.initialize(self, name, Skill.NotFrequent) - - self.global = false -end +---@class AttackRangeSkill : StatusSkill +local AttackRangeSkill = StatusSkill:subclass("AttackRangeSkill") ---@param from Player ---@param to Player diff --git a/lua/core/skill_type/distance.lua b/lua/core/skill_type/distance.lua index b6b82a3e..694f8066 100644 --- a/lua/core/skill_type/distance.lua +++ b/lua/core/skill_type/distance.lua @@ -1,12 +1,5 @@ ----@class DistanceSkill : Skill ----@field global boolean -local DistanceSkill = Skill:subclass("DistanceSkill") - -function DistanceSkill:initialize(name) - Skill.initialize(self, name, Skill.NotFrequent) - - self.global = false -end +---@class DistanceSkill : StatusSkill +local DistanceSkill = StatusSkill:subclass("DistanceSkill") ---@param from Player ---@param to Player diff --git a/lua/core/skill_type/max_cards.lua b/lua/core/skill_type/max_cards.lua index 738497c0..a31a264a 100644 --- a/lua/core/skill_type/max_cards.lua +++ b/lua/core/skill_type/max_cards.lua @@ -1,12 +1,5 @@ ----@class MaxCardsSkill : Skill ----@field global boolean -local MaxCardsSkill = Skill:subclass("MaxCardsSkill") - -function MaxCardsSkill:initialize(name) - Skill.initialize(self, name, Skill.NotFrequent) - - self.global = false -end +---@class MaxCardsSkill : StatusSkill +local MaxCardsSkill = StatusSkill:subclass("MaxCardsSkill") ---@param from Player ---@param to Player diff --git a/lua/core/skill_type/prohibit.lua b/lua/core/skill_type/prohibit.lua index e4610f9d..765c90db 100644 --- a/lua/core/skill_type/prohibit.lua +++ b/lua/core/skill_type/prohibit.lua @@ -1,12 +1,5 @@ ----@class ProhibitSkill : Skill ----@field global boolean -local ProhibitSkill = Skill:subclass("ProhibitSkill") - -function ProhibitSkill:initialize(name) - Skill.initialize(self, name, Skill.NotFrequent) - - self.global = false -end +---@class ProhibitSkill : StatusSkill +local ProhibitSkill = StatusSkill:subclass("ProhibitSkill") ---@param from Player ---@param to Player diff --git a/lua/core/skill_type/status_skill.lua b/lua/core/skill_type/status_skill.lua new file mode 100644 index 00000000..869c48f8 --- /dev/null +++ b/lua/core/skill_type/status_skill.lua @@ -0,0 +1,11 @@ +---@class StatusSkill : Skill +---@field global boolean +local StatusSkill = Skill:subclass("StatusSkill") + +function StatusSkill:initialize(name, frequency) + frequency = frequency or Skill.NotFrequent + Skill.initialize(self, name, frequency) + self.global = false +end + +return StatusSkill diff --git a/lua/core/skill_type/target_mod.lua b/lua/core/skill_type/target_mod.lua index e69de29b..b1daea2a 100644 --- a/lua/core/skill_type/target_mod.lua +++ b/lua/core/skill_type/target_mod.lua @@ -0,0 +1,22 @@ +---@class TargetModSkill : StatusSkill +local TargetModSkill = StatusSkill:subclass("TargetModSkill") + +---@param player Player +---@param card_skill ActiveSkill +function TargetModSkill:getResidueNum(player, card_skill, scope) + return 0 +end + +---@param player Player +---@param card_skill ActiveSkill +function TargetModSkill:getDistanceLimit(player, card_skill) + return 0 +end + +---@param player Player +---@param card_skill ActiveSkill +function TargetModSkill:getExtraTargetNum(player, card_skill) + return 0 +end + +return TargetModSkill diff --git a/lua/core/skill_type/trigger.lua b/lua/core/skill_type/trigger.lua index 21419590..30d90d9f 100644 --- a/lua/core/skill_type/trigger.lua +++ b/lua/core/skill_type/trigger.lua @@ -1,12 +1,12 @@ ----@class TriggerSkill : Skill +---@class TriggerSkill : UsableSkill ---@field global boolean ---@field events Event[] ---@field refresh_events Event[] ---@field priority_table table -local TriggerSkill = Skill:subclass("TriggerSkill") +local TriggerSkill = UsableSkill:subclass("TriggerSkill") function TriggerSkill:initialize(name, frequency) - Skill.initialize(self, name, frequency) + UsableSkill.initialize(self, name, frequency) self.global = false self.events = {} @@ -57,7 +57,11 @@ function TriggerSkill:doCost(event, target, player, data) local ret = self:cost(event, target, player, data) if ret then local room = player.room + if not self.mute then + room:broadcastSkillInvoke(self.name) + end room:notifySkillInvoked(player, self.name) + player:addSkillUseHistory(self.name) ret = self:use(event, target, player, data) return ret end diff --git a/lua/core/skill_type/usable_skill.lua b/lua/core/skill_type/usable_skill.lua new file mode 100644 index 00000000..875b5105 --- /dev/null +++ b/lua/core/skill_type/usable_skill.lua @@ -0,0 +1,56 @@ +---@class UsableSkill : Skill +---@field target_num integer|integer[] +---@field max_use_time integer[] +---@field distance_limit integer +local UsableSkill = Skill:subclass("UsableSkill") + +function UsableSkill:initialize(name, frequency) + frequency = frequency or Skill.NotFrequent + Skill.initialize(self, name, frequency) + + self.target_num = 9999 + self.max_use_time = {9999, 9999, 9999, 9999} + self.distance_limit = 9999 +end + +---@param player Player +function UsableSkill:getMinTargetNum(player) + local ret = type(self.target_num) == "table" and self.target_num[1] or self.target_num + return ret +end + +function UsableSkill:getMaxTargetNum(player) + local ret = type(self.target_num) == "table" and self.target_num[2] or self.target_num + local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or {} + for _, skill in ipairs(status_skills) do + local correct = skill:getExtraTargetNum(player, self) + if correct == nil then correct = 0 end + ret = ret + correct + end + return ret +end + +function UsableSkill:getMaxUseTime(player, scope) + scope = scope or Player.HistoryTurn + local ret = self.max_use_time[scope] + local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or {} + for _, skill in ipairs(status_skills) do + local correct = skill:getResidueNum(player, self, scope) + if correct == nil then correct = 0 end + ret = ret + correct + end + return ret +end + +function UsableSkill:getDistanceLimit(player) + local ret = self.distance_limit + local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or {} + for _, skill in ipairs(status_skills) do + local correct = skill:getDistanceLimit(player, self) + if correct == nil then correct = 0 end + ret = ret + correct + end + return ret +end + +return UsableSkill diff --git a/lua/core/skill_type/view_as.lua b/lua/core/skill_type/view_as.lua index bbaa3e7c..aa787f70 100644 --- a/lua/core/skill_type/view_as.lua +++ b/lua/core/skill_type/view_as.lua @@ -1,9 +1,9 @@ ----@class ViewAsSkill +---@class ViewAsSkill : UsableSkill ---@field pattern string @ cards that can be viewAs'ed by this skill -local ViewAsSkill = Skill:subclass("ViewAsSkill") +local ViewAsSkill = UsableSkill:subclass("ViewAsSkill") function ViewAsSkill:initialize(name) - Skill.initialize(self, name, Skill.NotFrequent) + UsableSkill.initialize(self, name, Skill.NotFrequent) self.pattern = "" end diff --git a/lua/fk_ex.lua b/lua/fk_ex.lua index 06f9deed..de1c85fc 100644 --- a/lua/fk_ex.lua +++ b/lua/fk_ex.lua @@ -3,18 +3,13 @@ dofile "lua/server/event.lua" dofile "lua/server/system_enum.lua" TriggerSkill = require "core.skill_type.trigger" -ActiveSkill = require "core.skill_type.active_skill" +ActiveSkill = require "core.skill_type.active" ViewAsSkill = require "core.skill_type.view_as" DistanceSkill = require "core.skill_type.distance" ProhibitSkill = require "core.skill_type.prohibit" AttackRangeSkill = require "core.skill_type.attack_range" MaxCardsSkill = require "core.skill_type.max_cards" -StatusSkills = { - DistanceSkill, - ProhibitSkill, - AttackRangeSkill, - MaxCardsSkill, -} +TargetModSkill = require "core.skill_type.target_mod" BasicCard = require "core.card_type.basic" local Trick = require "core.card_type.trick" @@ -22,10 +17,16 @@ TrickCard, DelayedTrickCard = table.unpack(Trick) local Equip = require "core.card_type.equip" _, Weapon, Armor, DefensiveRide, OffensiveRide, Treasure = table.unpack(Equip) ----@class SkillSpec: Skill +---@class UsableSkillSpec: UsableSkill +---@field max_phase_use_time integer +---@field max_turn_use_time integer +---@field max_round_use_time integer +---@field max_game_use_time integer + +---@class StatusSkillSpec: StatusSkill ---@alias TrigFunc fun(self: TriggerSkill, event: Event, target: ServerPlayer, player: ServerPlayer):boolean ----@class TriggerSkillSpec: SkillSpec +---@class TriggerSkillSpec: UsableSkillSpec ---@field global boolean ---@field events Event | Event[] ---@field refresh_events Event | Event[] @@ -46,6 +47,16 @@ function fk.CreateTriggerSkill(spec) local frequency = spec.frequency or Skill.NotFrequent local skill = TriggerSkill:new(spec.name, frequency) + skill.mute = spec.mute + skill.anim_type = spec.anim_type + skill.target_num = spec.target_num or skill.target_num + skill.max_use_time = { + spec.max_phase_use_time or 9999, + spec.max_turn_use_time or 9999, + spec.max_round_use_time or 9999, + spec.max_game_use_time or 9999, + } + skill.distance_limit = spec.distance_limit or skill.distance_limit if type(spec.events) == "number" then table.insert(skill.events, spec.events) @@ -99,7 +110,7 @@ function fk.CreateTriggerSkill(spec) return skill end ----@class ActiveSkillSpec: SkillSpec +---@class ActiveSkillSpec: UsableSkillSpec ---@field can_use fun(self: ActiveSkill, player: Player): boolean ---@field card_filter fun(self: ActiveSkill, to_select: integer, selected: integer[], selected_targets: integer[]): boolean ---@field target_filter fun(self: ActiveSkill, to_select: integer, selected: integer[], selected_cards: integer[]): boolean @@ -113,6 +124,17 @@ end function fk.CreateActiveSkill(spec) assert(type(spec.name) == "string") local skill = ActiveSkill:new(spec.name) + skill.mute = spec.mute + skill.anim_type = spec.anim_type + skill.target_num = spec.target_num or skill.target_num + skill.max_use_time = { + spec.max_phase_use_time or 9999, + spec.max_turn_use_time or 9999, + spec.max_round_use_time or 9999, + spec.max_game_use_time or 9999, + } + skill.distance_limit = spec.distance_limit or skill.distance_limit + if spec.can_use then skill.canUse = spec.can_use end if spec.card_filter then skill.cardFilter = spec.card_filter end if spec.target_filter then skill.targetFilter = spec.target_filter end @@ -123,7 +145,7 @@ function fk.CreateActiveSkill(spec) return skill end ----@class ViewAsSkillSpec: SkillSpec +---@class ViewAsSkillSpec: UsableSkillSpec ---@field card_filter fun(self: ViewAsSkill, to_select: integer, selected: integer[]): boolean ---@field view_as fun(self: ViewAsSkill, cards: integer[]) ---@field pattern string @@ -137,6 +159,17 @@ function fk.CreateViewAsSkill(spec) assert(type(spec.view_as) == "function") local skill = ViewAsSkill:new(spec.name) + skill.mute = spec.mute + skill.anim_type = spec.anim_type + skill.target_num = spec.target_num or skill.target_num + skill.max_use_time = { + spec.max_phase_use_time or 9999, + spec.max_turn_use_time or 9999, + spec.max_round_use_time or 9999, + spec.max_game_use_time or 9999, + } + skill.distance_limit = spec.distance_limit or skill.distance_limit + skill.viewAs = spec.view_as if spec.card_filter then skill.cardFilter = spec.card_filter @@ -154,9 +187,8 @@ function fk.CreateViewAsSkill(spec) return skill end ----@class DistanceSpec: SkillSpec +---@class DistanceSpec: StatusSkillSpec ---@field correct_func fun(self: DistanceSkill, from: Player, to: Player) ----@field global boolean ---@param spec DistanceSpec ---@return DistanceSkill @@ -173,9 +205,8 @@ function fk.CreateDistanceSkill(spec) return skill end ----@class ProhibitSpec: SkillSpec +---@class ProhibitSpec: StatusSkillSpec ---@field is_prohibited fun(self: ProhibitSkill, from: Player, to: Player, card: Card) ----@field global boolean ---@param spec ProhibitSpec ---@return ProhibitSkill @@ -192,9 +223,8 @@ function fk.CreateProhibitSkill(spec) return skill end ----@class AttackRangeSpec: SkillSpec +---@class AttackRangeSpec: StatusSkillSpec ---@field correct_func fun(self: AttackRangeSkill, from: Player, to: Player) ----@field global boolean ---@param spec AttackRangeSpec ---@return AttackRangeSkill @@ -211,10 +241,9 @@ function fk.CreateAttackRangeSkill(spec) return skill end ----@class MaxCardsSpec: SkillSpec +---@class MaxCardsSpec: StatusSkillSpec ---@field correct_func fun(self: MaxCardsSkill, player: Player) ---@field fixed_func fun(self: MaxCardsSkill, from: Player) ----@field global boolean ---@param spec MaxCardsSpec ---@return MaxCardsSkill @@ -236,6 +265,33 @@ function fk.CreateMaxCardsSkill(spec) return skill end +---@class TargetModSpec: StatusSkillSpec +---@field residue_func fun(self: TargetModSkill, player: Player, skill: ActiveSkill, scope: integer) +---@field distance_limit_func fun(self: TargetModSkill, player: Player, skill: ActiveSkill) +---@field extra_target_func fun(self: TargetModSkill, player: Player, skill: ActiveSkill) + +---@param spec TargetModSpec +---@return TargetModSkill +function fk.CreateTargetModSkill(spec) + assert(type(spec.name) == "string") + + local skill = TargetModSkill:new(spec.name) + if spec.residue_func then + skill.getResidueNum = spec.residue_func + end + if spec.distance_limit_func then + skill.getDistanceLimit = spec.distance_limit_func + end + if spec.extra_target_func then + skill.getExtraTargetNum = spec.extra_target_func + end + if spec.global then + skill.global = spec.global + end + + return skill +end + ---@class CardSpec: Card ---@field skill Skill diff --git a/lua/freekill.lua b/lua/freekill.lua index 2754f077..f470d281 100644 --- a/lua/freekill.lua +++ b/lua/freekill.lua @@ -22,6 +22,8 @@ General = require "core.general" Card = require "core.card" Exppattern = require "core.exppattern" Skill = require "core.skill" +UsableSkill = require "core.skill_type.usable_skill" +StatusSkill = require "core.skill_type.status_skill" Player = require "core.player" -- load packages diff --git a/lua/server/gamelogic.lua b/lua/server/gamelogic.lua index 1c616c24..c7551efd 100644 --- a/lua/server/gamelogic.lua +++ b/lua/server/gamelogic.lua @@ -60,7 +60,9 @@ function GameLogic:chooseGenerals() local function setPlayerGeneral(player, general) if Fk.generals[general] == nil then return end player.general = general + player.gender = Fk.generals[general].gender self.room:notifyProperty(player, player, "general") + self.room:notifyProperty(player, player, "gender") end local lord = room:getLord() local lord_general = nil @@ -222,21 +224,8 @@ function GameLogic:trigger(event, target, data) self.event_stack:push({event, target, data}) if target == nil then - for _, skill in ipairs(skills_to_refresh) do - if skill:canRefresh(event, target, player, data) then - skill:refresh(event, target, player, data) - end - end - - for _, skill in ipairs(skills) do - if skill:triggerable(event, target, player, data) then - broken = skill:trigger(event, target, player, data) - if broken then break end - end - end - - self.event_stack:pop() - return broken + target = room.current + player = target end repeat do @@ -280,7 +269,7 @@ function GameLogic:trigger(event, target, data) end while #skill_names > 0 do - local skill_name = room:askForChoice(player, skill_names, "trigger") + local skill_name = room:askForChoice(player, skill_names, "trigger", "#choose-trigger") local skill = triggerables[table.indexOf(skill_names, skill_name)] broken = skill:trigger(event, target, player, data) if broken then break end diff --git a/lua/server/room.lua b/lua/server/room.lua index 1b730b22..a84be31c 100644 --- a/lua/server/room.lua +++ b/lua/server/room.lua @@ -130,6 +130,7 @@ end ---@param id integer ---@return ServerPlayer function Room:getPlayerById(id) + if not id then return nil end assert(type(id) == "number") for _, p in ipairs(self.players) do @@ -181,6 +182,11 @@ function Room:getAlivePlayers(sortBySeat) if sortBySeat == nil or sortBySeat then local current = self.current local temp = current.next + + -- did not arrange seat, use default + if temp == nil then + return { table.unpack(self.players) } + end local ret = {current} while temp ~= current do if not temp.dead then @@ -226,19 +232,6 @@ function Room:getLord() return nil end ----@param expect ServerPlayer ----@return ServerPlayer[] -function Room:getOtherPlayers(expect, include_dead) - local ret - if include_dead then - ret = {table.unpack(self.players)} - else - ret = {table.unpack(self.alive_players)} - end - table.removeOne(ret, expect) - return ret -end - ---@param num integer ---@param from string ---@return integer[] @@ -495,23 +488,71 @@ function Room:setEmotion(player, name) }) end +function Room:setCardEmotion(cid, name) + self:doAnimate("Emotion", { + player = cid, + emotion = name, + is_card = true, + }) +end + function Room:sendLogEvent(type, data, players) players = players or self.players data.type = type self:doBroadcastNotify("LogEvent", json.encode(data), players) end +---@param skill_name string +---@param index integer +function Room:broadcastSkillInvoke(skill_name, index) + index = index or -1 + self:sendLogEvent("PlaySkillSound", { + name = skill_name, + i = index + }) +end + +---@param skill_name string +---@param index integer +function Room:broadcastPlaySound(path) + self:sendLogEvent("PlaySound", { + name = path, + }) +end + ---@param player ServerPlayer ---@param skill_name string ----@param skill_type nil +---@param skill_type string function Room:notifySkillInvoked(player, skill_name, skill_type) + if not skill_type then + local skill = Fk.skills[skill_name] + if not skill then skill_type = "" end + skill_type = skill.anim_type + end self:sendLog{ type = "#InvokeSkill", from = player.id, arg = skill_name, } - -- TODO: notifySkill animation + self:doAnimate("InvokeSkill", { + name = skill_name, + player = player.id, + skill_type = skill_type, + }) +end + +---@param source integer +---@param targets integer[] +function Room:doIndicate(source, targets) + local target_group = {} + for _, id in ipairs(targets) do + table.insert(target_group, { id }) + end + self:doAnimate("Indicate", { + from = source, + to = target_group, + }) end ------------------------------------------------------------------------ @@ -547,7 +588,8 @@ function Room:askForUseActiveSkill(player, skill_name, prompt, cancelable, extra local targets = data.targets local card_data = json.decode(card) local selected_cards = card_data.subcards - skill:onEffect(room, { + self:doIndicate(player.id, targets) + skill:onEffect(self, { from = player.id, cards = selected_cards, tos = targets, @@ -576,7 +618,8 @@ function Room:askForDiscard(player, minNum, maxNum, includeEquip, skillName) include_equip = includeEquip, reason = skillName } - local _, ret = self:askForUseActiveSkill(player, "discard_skill", "", true, data) + local prompt = "#AskForDiscard:::" .. maxNum .. ":" .. minNum + local _, ret = self:askForUseActiveSkill(player, "discard_skill", prompt, true, data) if ret then toDiscard = ret.cards else @@ -593,13 +636,14 @@ function Room:askForDiscard(player, minNum, maxNum, includeEquip, skillName) end ---@param player ServerPlayer ----@param targets ServerPlayer[] +---@param targets integer[] ---@param minNum integer ---@param maxNum integer +---@param prompt string ---@return integer[] -function Room:askForChoosePlayers(player, targets, minNum, maxNum, skillName) - if minNum < 1 then - return nil +function Room:askForChoosePlayers(player, targets, minNum, maxNum, prompt, skillName) + if maxNum < 1 then + return {} end local data = { @@ -608,7 +652,7 @@ function Room:askForChoosePlayers(player, targets, minNum, maxNum, skillName) min_num = minNum, reason = skillName } - local _, ret = self:askForUseActiveSkill(player, "choose_players_skill", "", true, data) + local _, ret = self:askForUseActiveSkill(player, "choose_players_skill", prompt or "", true, data) if ret then return ret.targets else @@ -669,12 +713,13 @@ end ---@param player ServerPlayer ---@param choices string[] ---@param skill_name string -function Room:askForChoice(player, choices, skill_name, data) +function Room:askForChoice(player, choices, skill_name, prompt, data) if #choices == 1 then return choices[1] end local command = "AskForChoice" + prompt = prompt or "" self:notifyMoveFocus(player, skill_name) local result = self:doRequest(player, command, json.encode{ - choices, skill_name + choices, skill_name, prompt }) if result == "" then result = choices[1] end return result @@ -692,6 +737,44 @@ function Room:askForSkillInvoke(player, skill_name, data) return invoked end +-- TODO: guanxing type +function Room:askForGuanxing(player, cards) + if #cards == 1 then + table.insert(self.draw_pile, 1, cards[1]) + return + end + local command = "AskForGuanxing" + self:notifyMoveFocus(player, command) + local data = { + cards = cards, + } + + local result = self:doRequest(player, command, json.encode(data)) + local top, bottom + if result ~= "" then + local d = json.decode(result) + top = d[1] + bottom = d[2] + else + top = cards + bottom = {} + end + + for i = #top, 1, -1 do + table.insert(self.draw_pile, 1, top[i]) + end + for _, id in ipairs(bottom) do + table.insert(self.draw_pile, id) + end + + self:sendLog{ + type = "#GuanxingResult", + from = player.id, + arg = #top, + arg2 = #bottom, + } +end + ---@param player ServerPlayer ---@param data string ---@return CardUseStruct @@ -704,7 +787,12 @@ function Room:handleUseCardReply(player, data) local skill = Fk.skills[card_data.skill] local selected_cards = card_data.subcards if skill:isInstanceOf(ActiveSkill) then + if not skill.mute then + self:broadcastSkillInvoke(skill.name) + end self:notifySkillInvoked(player, skill.name) + player:addSkillUseHistory(skill.name) + self:doIndicate(player.id, targets) skill:onEffect(self, { from = player.id, cards = selected_cards, @@ -714,13 +802,20 @@ function Room:handleUseCardReply(player, data) elseif skill:isInstanceOf(ViewAsSkill) then local c = skill:viewAs(selected_cards) if c then + if not skill.mute then + self:broadcastSkillInvoke(skill.name) + end self:notifySkillInvoked(player, skill.name) + player:addSkillUseHistory(skill.name) local use = {} ---@type CardUseStruct use.from = player.id use.tos = {} for _, target in ipairs(targets) do table.insert(use.tos, { target }) end + if #use.tos == 0 then + use.tos = nil + end use.card = c return use end @@ -753,7 +848,7 @@ function Room:askForUseCard(player, card_name, pattern, prompt, cancelable, extr cancelable = cancelable or false extra_data = extra_data or {} pattern = pattern or card_name - prompt = prompt or "#AskForUseCard" + prompt = prompt or "" local data = {card_name, pattern, prompt, cancelable, extra_data} local result = self:doRequest(player, command, json.encode(data)) @@ -774,7 +869,7 @@ function Room:askForResponse(player, card_name, pattern, prompt, cancelable, ext cancelable = cancelable or false extra_data = extra_data or {} pattern = pattern or card_name - prompt = prompt or "#AskForResponseCard" + prompt = prompt or "" local data = {card_name, pattern, prompt, cancelable, extra_data} local result = self:doRequest(player, command, json.encode(data)) @@ -796,7 +891,7 @@ function Room:askForNullification(players, card_name, pattern, prompt, cancelabl card_name = card_name or "nullification" cancelable = cancelable or false extra_data = extra_data or {} - prompt = prompt or "#AskForUseCard" + prompt = prompt or "" pattern = pattern or card_name self:notifyMoveFocus(self.alive_players, card_name) @@ -821,6 +916,24 @@ local sendCardEmotionAndLog = function(room, cardUseEvent) local from = cardUseEvent.from local card = cardUseEvent.card room:setEmotion(room:getPlayerById(from), card.name) + + local soundName + if card.type == Card.TypeEquip then + local subTypeStr + if card.sub_type == Card.SubtypeDefensiveRide or card.sub_type == Card.SubtypeOffensiveRide then + subTypeStr = "horse" + elseif card.sub_type == Card.SubtypeWeapon then + subTypeStr = "weapon" + else + subTypeStr = "armor" + end + + soundName = "common/" .. subTypeStr + else + soundName = (room:getPlayerById(from).gender == General.Male and "male/" or "female/") .. card.name + end + room:broadcastPlaySound("./audio/card/" .. soundName) + room:doAnimate("Indicate", { from = from, to = cardUseEvent.tos or {}, @@ -1131,6 +1244,10 @@ function Room:useCard(cardUseEvent) end if not findSameCard then + if cardUseEvent.card:isVirtual() then + self:getPlayerById(target):addVirtualEquip(cardUseEvent.card) + end + self:moveCards({ ids = realCardIds, to = target, @@ -1253,7 +1370,11 @@ function Room:doCardEffect(cardEffectEvent) table.contains(cardEffectEvent.unoffsetableList or {}, cardEffectEvent.to) ) then local to = self:getPlayerById(cardEffectEvent.to) - local use = self:askForUseCard(to, "jink") + local prompt = "" + if cardEffectEvent.from then + prompt = "#slash-jink:" .. cardEffectEvent.from .. "::" .. 1 + end + local use = self:askForUseCard(to, "jink", nil, prompt) if use then use.toCard = cardEffectEvent.card use.responseToEvent = cardEffectEvent @@ -1271,7 +1392,13 @@ function Room:doCardEffect(cardEffectEvent) end end - local use = self:askForNullification(players) + local prompt = "" + if cardEffectEvent.to then + prompt = "#AskForNullification::" .. cardEffectEvent.to .. ":" .. cardEffectEvent.card.name + elseif cardEffectEvent.from then + prompt = "#AskForNullificationWithoutTo:" .. cardEffectEvent.from .. "::" .. cardEffectEvent.card.name + end + local use = self:askForNullification(players, nil, nil, prompt) if use then use.toCard = cardEffectEvent.card use.responseToEvent = cardEffectEvent @@ -1324,6 +1451,8 @@ function Room:responseCard(cardResponseEvent) }) self:setEmotion(self:getPlayerById(from), card.name) + local soundName = (self:getPlayerById(from).gender == General.Male and "male/" or "female/") .. card.name + self:broadcastPlaySound("./audio/card/" .. soundName) for _, event in ipairs({ fk.PreCardRespond, fk.CardResponding, fk.CardRespondFinished }) do self.logic:trigger(event, self:getPlayerById(cardResponseEvent.from), cardResponseEvent) @@ -1449,14 +1578,14 @@ end function Room:obtainCard(player, cid, unhide, reason) if type(cid) ~= "number" then assert(cid and cid:isInstanceOf(Card)) - cid = cid:isVirtual() and {cid.id} or cid.subcards + cid = cid:isVirtual() and cid.subcards or {cid.id} else cid = {cid} end if #cid == 0 then return end self:moveCards({ ids = cid, - from = self.owner_map[cid], + from = self.owner_map[cid[1]], to = player, toArea = Card.PlayerHand, moveReason = reason or fk.ReasonJustMove, @@ -1496,11 +1625,11 @@ function Room:moveCardTo(card, to_place, target, reason, skill_name, special_nam special_name = special_name or "" local ids = {} if card[1] ~= nil then - for i, cd in ipairs(card) do - ids[i] = cd.id + for _, cd in ipairs(card) do + table.insertTable(ids, self:getSubcardsByRule(card)) end else - ids[1] = card.id + ids = self:getSubcardsByRule(card) end local to @@ -1579,6 +1708,7 @@ function Room:changeHp(player, num, reason, skillName, damageStruct) self:sendLogEvent("Damage", { to = player.id, damageType = damage_nature_table[damageStruct.damageType], + damageNum = damageStruct.damage, }) elseif reason == "loseHp" then self:sendLog{ @@ -1586,6 +1716,7 @@ function Room:changeHp(player, num, reason, skillName, damageStruct) from = player.id, arg = 0 - num, } + self:sendLogEvent("LoseHP", {}) elseif reason == "recover" then self:sendLog{ type = "#HealHP", @@ -1910,12 +2041,69 @@ function Room:judge(data) card = {data.card.id}, } + if data.pattern then + self:delay(400); + self:setCardEmotion(data.card.id, data.card:matchPattern(data.pattern) and "judgegood" or "judgebad") + self:delay(900); + end + self.logic:trigger(fk.FinishJudge, who, data) if self:getCardArea(data.card.id) == Card.Processing then self:moveCardTo(data.card, Card.DiscardPile, nil, fk.ReasonPutIntoDiscardPile) end end +---@param card Card +---@param player ServerPlayer +---@param judge JudgeStruct +---@param skillName string +---@param exchange boolean +function Room:retrial(card, player, judge, skillName, exchange) + if not card then return end + local triggerResponded = self.owner_map[card:getEffectiveId()] == player + local isHandcard = (triggerResponded and self:getCardArea(card:getEffectiveId()) == Card.PlayerHand) + + local oldJudge = judge.card + judge.card = Fk:getCardById(card:getEffectiveId()) + local rebyre = judge.retrial_by_response + judge.retrial_by_response = player + + local resp = {} ---@type CardResponseEvent + resp.from = player.id + resp.card = card + + if triggerResponded then + self.logic:trigger(fk.PreCardRespond, player, resp) + end + + local move1 = {} ---@type CardsMoveInfo + move1.ids = { card:getEffectiveId() } + move1.from = player.id + move1.toArea = Card.Processing + move1.moveReason = fk.ReasonResonpse + move1.skillName = skillName + + local move2 = {} ---@type CardsMoveInfo + move2.ids = { oldJudge:getEffectiveId() } + move2.toArea = exchange and Card.PlayerHand or Card.DiscardPile + move2.moveReason = fk.ReasonJustMove + move2.to = exchange and player.id or nil + + self:sendLog{ + type = "#ChangedJudge", + from = player.id, + to = { judge.who.id }, + card = { card:getEffectiveId() }, + arg = skillName, + } + + self:moveCards(move1, move2) + + if triggerResponded then + self.logic:trigger(fk.CardRespondFinished, player, resp) + end +end + ---@param card_ids integer[] ---@param skillName string ---@param who ServerPlayer diff --git a/lua/server/serverplayer.lua b/lua/server/serverplayer.lua index 31a1050a..1040635b 100644 --- a/lua/server/serverplayer.lua +++ b/lua/server/serverplayer.lua @@ -88,8 +88,7 @@ function ServerPlayer:marshal(player) local room = self.room room:notifyProperty(player, self, "maxHp") room:notifyProperty(player, self, "hp") - -- TODO - --room:notifyProperty(player, self, "gender") + room:notifyProperty(player, self, "gender") if self.kingdom ~= Fk.generals[self.general].kingdom then room:notifyProperty(player, self, "kingdom") @@ -409,14 +408,42 @@ function ServerPlayer:throwAllCards(flag) end end +function ServerPlayer:addVirtualEquip(card) + Player.addVirtualEquip(self, card) + self.room:doBroadcastNotify("AddVirtualEquip", json.encode{ + player = self.id, + name = card.name, + subcards = card.subcards, + }) +end + +function ServerPlayer:removeVirtualEquip(cid) + local ret = Player.removeVirtualEquip(self, cid) + self.room:doBroadcastNotify("RemoveVirtualEquip", json.encode{ + player = self.id, + id = cid, + }) + return ret +end + function ServerPlayer:addCardUseHistory(cardName, num) Player.addCardUseHistory(self, cardName, num) self:doNotify("AddCardUseHistory", json.encode{cardName, num}) end -function ServerPlayer:resetCardUseHistory(cardName) - Player.resetCardUseHistory(self, cardName) - self:doNotify("ResetCardUseHistory", cardName or "") +function ServerPlayer:setCardUseHistory(cardName, num, scope) + Player.setCardUseHistory(self, cardName, num, scope) + self:doNotify("SetCardUseHistory", json.encode{cardName, num, scope}) +end + +function ServerPlayer:addSkillUseHistory(cardName, num) + Player.addSkillUseHistory(self, cardName, num) + self:doNotify("AddSkillUseHistory", json.encode{cardName, num}) +end + +function ServerPlayer:setSkillUseHistory(cardName, num, scope) + Player.setSkillUseHistory(self, cardName, num, scope) + self:doNotify("SetSkillUseHistory", json.encode{cardName, num, scope}) end return ServerPlayer diff --git a/lua/server/system_enum.lua b/lua/server/system_enum.lua index 9b755e36..a1374518 100644 --- a/lua/server/system_enum.lua +++ b/lua/server/system_enum.lua @@ -15,7 +15,7 @@ ---@alias CardEffectEvent { from: integer, to: integer, subTargets: integer[]|null, tos: TargetGroup, card: Card, toCard: Card|null, responseToEvent: CardUseStruct|null, nullifiedTargets: interger[]|null, extraUse: boolean|null, disresponsiveList: integer[]|null, unoffsetableList: integer[]|null, addtionalDamage: integer|null, customFrom: integer|null, cardsResponded: Card[]|null, disresponsive: boolean|null, unoffsetable: boolean|null } ---@alias SkillEffectEvent { from: integer, tos: integer[], cards: integer[] } ----@alias JudgeStruct { who: ServerPlayer, card: Card, reason: string } +---@alias JudgeStruct { who: ServerPlayer, card: Card, reason: string, pattern: string } ---@alias CardResponseEvent { from: integer, card: Card, responseToEvent: CardEffectEvent|null, skipDrop: boolean|null, customFrom: integer|null } ---@alias CardMoveReason integer diff --git a/packages/standard/aux_skills.lua b/packages/standard/aux_skills.lua index c80a6e54..0589a4a8 100644 --- a/packages/standard/aux_skills.lua +++ b/packages/standard/aux_skills.lua @@ -5,6 +5,10 @@ local discardSkill = fk.CreateActiveSkill{ return false end + if not self.include_equip then + return ClientInstance:getCardArea(to_select) ~= Player.Equip + end + return true end, feasible = function(self, _, selected) @@ -19,7 +23,7 @@ local choosePlayersSkill = fk.CreateActiveSkill{ end, target_filter = function(self, to_select, selected) if #selected < self.num then - return table.contains(self.player_ids, to_select) + return table.contains(self.targets, to_select) end end, feasible = function(self, selected) diff --git a/packages/standard/game_rule.lua b/packages/standard/game_rule.lua index 0184043f..ee96946b 100644 --- a/packages/standard/game_rule.lua +++ b/packages/standard/game_rule.lua @@ -56,11 +56,9 @@ GameRule = fk.CreateTriggerSkill{ return false end - if target == nil then - if event == fk.GameStart then - fk.qInfo("Game started") - RoomInstance.tag["FirstRound"] = true - end + if event == fk.GameStart then + fk.qInfo("Game started") + RoomInstance.tag["FirstRound"] = true return false end @@ -96,6 +94,13 @@ GameRule = fk.CreateTriggerSkill{ player:setFlag("Global_FirstRound") end + if player.seat == 1 then + for _, p in ipairs(room.players) do + p:setCardUseHistory("", 0, Player.HistoryRound) + p:setSkillUseHistory("", 0, Player.HistoryRound) + end + end + room:sendLog{ type = "$AppendSeparator" } player:addMark("Global_TurnCount") @@ -120,7 +125,11 @@ GameRule = fk.CreateTriggerSkill{ [Player.Judge] = function() local cards = player:getCardIds(Player.Judge) for i = #cards, 1, -1 do - local card = Fk:getCardById(cards[i]) + local card + card = player:removeVirtualEquip(cards[i]) + if not card then + card = Fk:getCardById(cards[i]) + end room:moveCardTo(card, Card.Processing, nil, fk.ReasonPut, self.name) ---@type CardEffectEvent @@ -171,11 +180,16 @@ GameRule = fk.CreateTriggerSkill{ end, [fk.EventPhaseEnd] = function() if player.phase == Player.Play then - player:resetCardUseHistory() + player:setCardUseHistory("", 0, Player.HistoryPhase) + player:setSkillUseHistory("", 0, Player.HistoryPhase) end end, [fk.EventPhaseChanging] = function() -- TODO: copy but dont copy all + if data.to == Player.NotActive then + player:setCardUseHistory("", 0, Player.HistoryTurn) + player:setSkillUseHistory("", 0, Player.HistoryTurn) + end end, [fk.AskForPeaches] = function() local savers = room:getAlivePlayers() diff --git a/packages/standard/init.lua b/packages/standard/init.lua index cd0904a2..be749926 100644 --- a/packages/standard/init.lua +++ b/packages/standard/init.lua @@ -19,6 +19,7 @@ Fk:loadTranslationTable{ local jianxiong = fk.CreateTriggerSkill{ name = "jianxiong", + anim_type = "masochism", events = {fk.Damaged}, can_trigger = function(self, event, target, player, data) local room = target.room @@ -40,8 +41,30 @@ Fk:loadTranslationTable{ ["jianxiong"] = "奸雄", } +local guicai = fk.CreateTriggerSkill{ + name = "guicai", + anim_type = "control", + events = {fk.AskForRetrial}, + can_trigger = function(self, event, target, player, data) + return player:hasSkill(self.name) and not player:isKongcheng() + end, + on_cost = function(self, event, target, player, data) + local room = player.room + local prompt = "#guicai-ask::" .. target.id + local card = room:askForResponse(player, self.name, ".|.|.|hand", prompt, true) + if card ~= nil then + self.cost_data = card + return true + end + end, + on_use = function(self, event, target, player, data) + local room = player.room + room:retrial(self.cost_data, player, data, self.name) + end, +} local fankui = fk.CreateTriggerSkill{ name = "fankui", + anim_type = "masochism", events = {fk.Damaged}, frequency = Skill.NotFrequent, can_trigger = function(self, event, target, player, data) @@ -50,6 +73,7 @@ local fankui = fk.CreateTriggerSkill{ return from ~= nil and target == player and target:hasSkill(self.name) and + (not from:isNude()) and not target.dead end, on_use = function(self, event, target, player, data) @@ -60,14 +84,18 @@ local fankui = fk.CreateTriggerSkill{ end } local simayi = General:new(extension, "simayi", "wei", 3) +simayi:addSkill(guicai) simayi:addSkill(fankui) Fk:loadTranslationTable{ ["simayi"] = "司马懿", + ["guicai"] = "鬼才", + ["#guicai-ask"] = "是否发动“鬼才”,打出一张手牌修改 %dest 的判定?", ["fankui"] = "反馈", } local ganglie = fk.CreateTriggerSkill{ name = "ganglie", + anim_type = "masochism", events = {fk.Damaged}, can_trigger = function(self, event, target, player, data) local room = target.room @@ -82,6 +110,7 @@ local ganglie = fk.CreateTriggerSkill{ local judge = { who = from, reason = self.name, + pattern = ".|.|spade,club,diamond", } room:judge(judge) if judge.card.suit ~= Card.Heart then @@ -104,33 +133,247 @@ Fk:loadTranslationTable{ ["ganglie"] = "刚烈", } +local tuxi = fk.CreateTriggerSkill{ + name = "tuxi", + anim_type = "control", + events = {fk.EventPhaseStart}, + can_trigger = function(self, event, target, player, data) + local ret = (target == player and player:hasSkill(self.name) and player.phase == Player.Draw) + if ret then + local room = player.room + for _, p in ipairs(room:getOtherPlayers(player)) do + if not p:isKongcheng() then + return true + end + end + end + end, + on_cost = function(self, event, target, player, data) + local room = player.room + local other = room:getOtherPlayers(player) + local targets = {} + for _, p in ipairs(other) do + if not p:isKongcheng() then + table.insert(targets, p.id) + end + end + + local result = room:askForChoosePlayers(player, targets, 1, 2, "#tuxi-ask", self.name) + if #result > 0 then + self.cost_data = result + return true + end + end, + on_use = function(self, event, target, player, data) + local room = player.room + for _, id in ipairs(self.cost_data) do + local p = room:getPlayerById(id) + local c = room:askForCardChosen(player, p, "h", self.name) + room:obtainCard(player.id, c, false) + end + return true + end, +} local zhangliao = General:new(extension, "zhangliao", "wei", 4) +zhangliao:addSkill(tuxi) Fk:loadTranslationTable{ ["zhangliao"] = "张辽", + ["tuxi"] = "突袭", + ["#tuxi-ask"] = "是否发动“突袭”,改为获得1-2名角色各一张牌?", } +local luoyi = fk.CreateTriggerSkill{ + name = "luoyi", + anim_type = "offensive", + events = {fk.DrawNCards}, + can_trigger = function(self, event, target, player, data) + return target == player and player:hasSkill(self.name) and data.n > 0 + end, + on_use = function(self, event, target, player, data) + data.n = data.n - 1 + end, + + refresh_events = {fk.DamageCaused}, + can_refresh = function(self, event, target, player, data) + if not (target == player and player:hasSkill(self.name) and + player:usedSkillTimes(self.name) > 0) then + return + end + + local c = data.card + return c and c.name == "slash" or c.name == "duel" + end, + on_refresh = function(self, event, target, player, data) + local room = player.room + room:broadcastSkillInvoke(self.name) + room:notifySkillInvoked(player, self.name) + data.damage = data.damage + 1 + end, +} local xuchu = General:new(extension, "xuchu", "wei", 4) +xuchu:addSkill(luoyi) Fk:loadTranslationTable{ ["xuchu"] = "许褚", + ["luoyi"] = "裸衣", } +local tiandu = fk.CreateTriggerSkill{ + name = "tiandu", + events = {fk.FinishJudge}, + on_use = function(self, event, target, player, data) + local room = player.room + room:obtainCard(player.id, data.card) + end, +} +local yiji = fk.CreateTriggerSkill{ + name = "yiji", + anim_type = "masochism", + events = {fk.Damaged}, + on_trigger = function(self, event, target, player, data) + self.cancel_cost = false + for i = 1, data.damage do + if self.cancel_cost then break end + self:doCost(event, target, player, data) + end + end, + on_cost = function(self, event, target, player, data) + local room = player.room + if room:askForSkillInvoke(player, self.name, data) then + return true + end + self.cancel_cost = true + end, + on_use = function(self, event, target, player, data) + -- TODO: yiji logic + player:drawCards(2) + end, +} local guojia = General:new(extension, "guojia", "wei", 3) +guojia:addSkill(tiandu) +guojia:addSkill(yiji) Fk:loadTranslationTable{ ["guojia"] = "郭嘉", + ["tiandu"] = "天妒", + ["yiji"] = "遗计", } +local luoshen = fk.CreateTriggerSkill{ + name = "luoshen", + anim_type = "drawcard", + events = {fk.EventPhaseStart}, + can_trigger = function(self, event, target, player, data) + return target == player and player:hasSkill(self.name) and + player.phase == Player.Start + end, + on_use = function(self, event, target, player, data) + local room = player.room + while true do + local judge = { + who = player, + reason = self.name, + pattern = ".|A~K|spade,club", + } + room:judge(judge) + if judge.card.color ~= Card.Black then + break + end + + if not room:askForSkillInvoke(player, self.name) then + break + end + end + end, + + refresh_events = {fk.FinishJudge}, + can_refresh = function(self, event, target, player, data) + return target == player and player:hasSkill(self.name) and + data.reason == self.name and data.card.color == Card.Black + end, + on_refresh = function(self, event, target, player, data) + local room = player.room + room:obtainCard(player.id, data.card) + end, +} +local qingguo = fk.CreateViewAsSkill{ + name = "qingguo", + anim_type = "defensive", + pattern = "jink", + card_filter = function(self, to_select, selected) + if #selected == 1 then return false end + return Fk:getCardById(to_select).color == Card.Black + and ClientInstance:getCardArea(to_select) ~= Player.Equip + end, + view_as = function(self, cards) + if #cards ~= 1 then + return nil + end + local c = Fk:cloneCard("jink") + c:addSubcard(cards[1]) + return c + end, +} local zhenji = General:new(extension, "zhenji", "wei", 3, 3, General.Female) +zhenji:addSkill(luoshen) +zhenji:addSkill(qingguo) Fk:loadTranslationTable{ ["zhenji"] = "甄姬", + ["luoshen"] = "洛神", + ["qingguo"] = "倾国", } +local rendetrig = fk.CreateTriggerSkill{ + name = "#rendetrig", + mute = true, + refresh_events = {fk.EventPhaseStart}, + can_refresh = function(self, event, target, player, data) + return target == player and player:hasSkill(self.name) and player.phase == Player.NotActive + end, + on_refresh = function(self, event, target, player, data) + local room = player.room + room:setPlayerMark(player, "_rende_cards", 0) + end, +} +local rende = fk.CreateActiveSkill{ + name = "rende", + anim_type = "support", + card_filter = function(self, to_select, selected) + return ClientInstance:getCardArea(to_select) ~= Card.PlayerEquip + end, + target_filter = function(self, to_select, selected) + return #selected == 0 and to_select ~= Self.id + end, + feasible = function(self, targets, cards) + return #targets == 1 and #cards > 0 + end, + on_effect = function(self, room, effect) + local target = room:getPlayerById(effect.tos[1]) + local player = room:getPlayerById(effect.from) + local cards = effect.cards + local marks = player:getMark("_rende_cards") + local dummy = Fk:cloneCard'slash' + dummy:addSubcards(cards) + room:obtainCard(target.id, dummy, false, fk.ReasonGive) + room:addPlayerMark(player, "_rende_cards", #cards) + if marks < 2 and marks + #cards >= 2 and player:isWounded() then + room:recover{ + who = player.id, + num = 1, + skillName = self.name + } + end + end, +} +rende:addRelatedSkill(rendetrig) local liubei = General:new(extension, "liubei", "shu", 4) +liubei:addSkill(rende) Fk:loadTranslationTable{ ["liubei"] = "刘备", + ["rende"] = "仁德", } local wusheng = fk.CreateViewAsSkill{ name = "wusheng", + anim_type = "offensive", pattern = "slash", card_filter = function(self, to_select, selected) if #selected == 1 then return false end @@ -152,11 +395,78 @@ Fk:loadTranslationTable{ ["wusheng"] = "武圣", } +local paoxiaoAudio = fk.CreateTriggerSkill{ + name = "#paoxiaoAudio", + refresh_events = {fk.CardUsing}, + can_refresh = function(self, event, target, player, data) + return target == player and player:hasSkill(self.name) and + data.card.name == "slash" and + player:usedCardTimes("slash") > 1 + end, + on_refresh = function(self, event, target, player, data) + player.room:broadcastSkillInvoke("paoxiao") + player.room:doAnimate("InvokeSkill", { + name = "paoxiao", + player = player.id, + skill_type = "offensive", + }) + end, +} +local paoxiao = fk.CreateTargetModSkill{ + name = "paoxiao", + residue_func = function(self, player, skill, scope) + if player:hasSkill(self.name) and skill.name == "slash_skill" + and scope == Player.HistoryPhase then + return 999 + end + end, +} +paoxiao:addRelatedSkill(paoxiaoAudio) local zhangfei = General:new(extension, "zhangfei", "shu", 4) +zhangfei:addSkill(paoxiao) Fk:loadTranslationTable{ ["zhangfei"] = "张飞", + ["paoxiao"] = "咆哮", } +local guanxing = fk.CreateTriggerSkill{ + name = "guanxing", + anim_type = "control", + events = {fk.EventPhaseStart}, + can_trigger = function(self, event, target, player, data) + return target == player and player:hasSkill(self.name) and + player.phase == Player.Start + end, + on_use = function(self, event, target, player, data) + local room = player.room + room:askForGuanxing(player, room:getNCards(math.min(5, #room.alive_players))) + end, +} +local kongchengAudio = fk.CreateTriggerSkill{ + name = "#kongchengAudio", + refresh_events = {fk.AfterCardsMove}, + can_refresh = function(self, event, target, player, data) + if not player:hasSkill(self.name) then return end + if not player:isKongcheng() then return end + for _, move in ipairs(data) do + if move.from == player.id then + for _, info in ipairs(move.moveInfo) do + if info.fromArea == Card.PlayerHand then + return true + end + end + end + end + end, + on_refresh = function(self, event, target, player, data) + player.room:broadcastSkillInvoke("kongcheng") + player.room:doAnimate("InvokeSkill", { + name = "kongcheng", + player = player.id, + skill_type = "defensive", + }) + end, +} local kongcheng = fk.CreateProhibitSkill{ name = "kongcheng", is_prohibited = function(self, from, to, card) @@ -165,10 +475,13 @@ local kongcheng = fk.CreateProhibitSkill{ end end, } +kongcheng:addRelatedSkill(kongchengAudio) local zhugeliang = General:new(extension, "zhugeliang", "shu", 3) +zhugeliang:addSkill(guanxing) zhugeliang:addSkill(kongcheng) Fk:loadTranslationTable{ ["zhugeliang"] = "诸葛亮", + ["guanxing"] = "观星", ["kongcheng"] = "空城", } @@ -210,15 +523,39 @@ local mashu = fk.CreateDistanceSkill{ end end, } +local tieqi = fk.CreateTriggerSkill{ + name = "tieqi", + anim_type = "offensive", + events = {fk.TargetSpecified}, + can_trigger = function(self, event, target, player, data) + return target == player and player:hasSkill(self.name) and + data.card.name == "slash" + end, + on_use = function(self, event, target, player, data) + local room = player.room + local judge = { + who = player, + reason = self.name, + pattern = ".|.|heart,diamond", + } + room:judge(judge) + if judge.card.color == Card.Red then + data.disresponsive = true + end + end, +} local machao = General:new(extension, "machao", "shu", 4) machao:addSkill(mashu) +machao:addSkill(tieqi) Fk:loadTranslationTable{ ["machao"] = "马超", ["mashu"] = "马术", + ["tieqi"] = "铁骑", } local jizhi = fk.CreateTriggerSkill{ name = "jizhi", + anim_type = "drawcard", events = {fk.CardUsing}, can_trigger = function(self, event, target, player, data) return target == player and player:hasSkill(self.name) and @@ -229,15 +566,31 @@ local jizhi = fk.CreateTriggerSkill{ player:drawCards(1, self.name) end, } +local qicai = fk.CreateTargetModSkill{ + name = "qicai", + distance_limit_func = function(self, player, skill) + local card_name = string.sub(skill.name, 1, -7) -- assuming all card skill is named with name_skill + local card = Fk:cloneCard(card_name) + if player:hasSkill(self.name) and card.type == Card.TypeTrick then + return 999 + end + end, +} local huangyueying = General:new(extension, "huangyueying", "shu", 3, 3, General.Female) huangyueying:addSkill(jizhi) +huangyueying:addSkill(qicai) Fk:loadTranslationTable{ ["huangyueying"] = "黄月英", ["jizhi"] = "集智", + ["qicai"] = "奇才", } local zhiheng = fk.CreateActiveSkill{ name = "zhiheng", + anim_type = "drawcard", + can_use = function(self, player) + return player:usedSkillTimes(self.name) == 0 + end, feasible = function(self, selected, selected_cards) return #selected == 0 and #selected_cards > 0 end, @@ -256,6 +609,7 @@ Fk:loadTranslationTable{ local qixi = fk.CreateViewAsSkill{ name = "qixi", + anim_type = "control", pattern = "dismantlement", card_filter = function(self, to_select, selected) if #selected == 1 then return false end @@ -279,11 +633,12 @@ Fk:loadTranslationTable{ local keji = fk.CreateTriggerSkill{ name = "keji", + anim_type = "defensive", events = {fk.EventPhaseChanging}, can_trigger = function(self, event, target, player, data) return target == player and player:hasSkill(self.name) and data.to == Player.Discard and - player:usedTimes("slash") < 1 and + player:usedCardTimes("slash") < 1 and player:getMark("_keji_played_slash") == 0 end, on_use = function(self, event, target, player, data) @@ -319,6 +674,7 @@ Fk:loadTranslationTable{ local kurou = fk.CreateActiveSkill{ name = "kurou", + anim_type = "drawcard", card_filter = function(self, to_select, selected, selected_targets) return false end, @@ -339,21 +695,105 @@ Fk:loadTranslationTable{ local yingzi = fk.CreateTriggerSkill{ name = "yingzi", + anim_type = "drawcard", events = {fk.DrawNCards}, on_use = function(self, event, target, player, data) data.n = data.n + 1 end, } +local fanjian = fk.CreateActiveSkill{ + name = "fanjian", + can_use = function(self, player) + return player:usedSkillTimes(self.name) < 1 and not player:isKongcheng() + end, + card_filter = function() return false end, + target_filter = function(self, to_select, selected) + return #selected == 0 and to_select ~= Self.id + end, + feasible = function(self, selected) + return #selected == 1 + end, + on_effect = function(self, room, effect) + local player = room:getPlayerById(effect.from) + local target = room:getPlayerById(effect.tos[1]) + local choice = room:askForChoice(target, {"spade", "heart", "club", "diamond"}, self.name) + local card = room:askForCardChosen(target, player, 'h', self.name) + room:obtainCard(target.id, card, true) + if Fk:getCardById(card):getSuitString() ~= choice then + room:damage{ + from = player.id, + to = target.id, + damage = 1, + skillName = self.name, + } + end + end, +} local zhouyu = General:new(extension, "zhouyu", "wu", 3) zhouyu:addSkill(yingzi) +zhouyu:addSkill(fanjian) Fk:loadTranslationTable{ ["zhouyu"] = "周瑜", ["yingzi"] = "英姿", + ["fanjian"] = "反间", } +local guose = fk.CreateViewAsSkill{ + name = "guose", + anim_type = "control", + pattern = "indulgence", + card_filter = function(self, to_select, selected) + if #selected == 1 then return false end + return Fk:getCardById(to_select).suit == Card.Diamond + end, + view_as = function(self, cards) + if #cards ~= 1 then + return nil + end + local c = Fk:cloneCard("indulgence") + c:addSubcard(cards[1]) + return c + end, +} +local liuli = fk.CreateTriggerSkill{ + name = "liuli", + anim_type = "defensive", + events = {fk.TargetConfirming}, + can_trigger = function(self, event, target, player, data) + local ret = target == player and player:hasSkill(self.name) and + data.card.name == "slash" + if ret then + self.target_list = {} + local room = player.room + for _, p in ipairs(room:getOtherPlayers(player)) do + if p.id ~= data.from and player:inMyAttackRange(p) then + table.insert(self.target_list, p.id) + end + end + return #self.target_list > 0 + end + end, + on_cost = function(self, event, target, player, data) + local room = player.room + local p = room:askForChoosePlayers(player, self.target_list, 1, 1, prompt, self.name) + if #p > 0 then + self.cost_data = p[1] + return true + end + end, + on_use = function(self, event, target, player, data) + local room = player.room + room:doIndicate(player.id, { self.cost_data }) + data.to = self.cost_data -- TODO + end, +} local daqiao = General:new(extension, "daqiao", "wu", 3, 3, General.Female) +daqiao:addSkill(guose) +daqiao:addSkill(liuli) Fk:loadTranslationTable{ ["daqiao"] = "大乔", + ["guose"] = "国色", + ["liuli"] = "流离", } local qianxun = fk.CreateProhibitSkill{ @@ -364,23 +804,79 @@ local qianxun = fk.CreateProhibitSkill{ end end, } +local lianying = fk.CreateTriggerSkill{ + name = "lianying", + anim_type = "drawcard", + events = {fk.AfterCardsMove}, + can_trigger = function(self, event, target, player, data) + if not player:hasSkill(self.name) then return end + if not player:isKongcheng() then return end + for _, move in ipairs(data) do + if move.from == player.id then + for _, info in ipairs(move.moveInfo) do + if info.fromArea == Card.PlayerHand then + return true + end + end + end + end + end, + on_use = function(self, event, target, player, data) + player:drawCards(1, self.name) + end, +} local luxun = General:new(extension, "luxun", "wu", 3) luxun:addSkill(qianxun) +luxun:addSkill(lianying) Fk:loadTranslationTable{ ["luxun"] = "陆逊", ["qianxun"] = "谦逊", + ["lianying"] = "连营", } +local xiaoji = fk.CreateTriggerSkill{ + name = "xiaoji", + anim_type = "drawcard", + events = {fk.AfterCardsMove}, + can_trigger = function(self, event, target, player, data) + if not player:hasSkill(self.name) then return end + self.trigger_times = 0 + for _, move in ipairs(data) do + if move.from == player.id then + for _, info in ipairs(move.moveInfo) do + if info.fromArea == Card.PlayerEquip then + self.trigger_times = self.trigger_times + 1 + end + end + end + end + return self.trigger_times > 0 + end, + on_trigger = function(self, event, target, player, data) + local ret + for i = 1, self.trigger_times do + ret = self:doCost(event, target, player, data) + if ret then return ret end + end + end, + on_use = function(self, event, target, player, data) + player:drawCards(2, self.name) + end, +} local jieyin = fk.CreateActiveSkill{ name = "jieyin", + anim_type = "support", + can_use = function(self, player) + return player:usedSkillTimes(self.name) == 0 + end, card_filter = function(self, to_select, selected) - return #selected < 2 + return #selected < 2 and ClientInstance:getCardArea(to_select) ~= Player.Equip end, target_filter = function(self, to_select, selected) local target = Fk:currentRoom():getPlayerById(to_select) local name = target.general return target:isWounded() and - Fk.generals[name].gender == General.Male + target.gender == General.Male and #selected < 1 end, feasible = function(self, selected, selected_cards) @@ -406,15 +902,70 @@ local jieyin = fk.CreateActiveSkill{ end } local sunshangxiang = General:new(extension, "sunshangxiang", "wu", 3, 3, General.Female) +sunshangxiang:addSkill(xiaoji) sunshangxiang:addSkill(jieyin) Fk:loadTranslationTable{ ["sunshangxiang"] = "孙尚香", + ["xiaoji"] = "枭姬", ["jieyin"] = "结姻", } +local qingnang = fk.CreateActiveSkill{ + name = "qingnang", + anim_type = "support", + can_use = function(self, player) + return player:usedSkillTimes(self.name) == 0 + end, + card_filter = function(self, to_select, selected, targets) + return #selected == 0 and ClientInstance:getCardArea(to_select) ~= Player.Equip + end, + target_filter = function(self, to_select, selected, cards) + return #selected == 0 and Fk:currentRoom():getPlayerById(to_select):isWounded() + end, + feasible = function(self, targets, cards) + return #targets == 1 and #cards == 1 + end, + on_effect = function(self, room, effect) + local from = room:getPlayerById(effect.from) + room:throwCard(effect.cards, self.name, from) + room:recover({ + who = effect.tos[1], + num = 1, + recoverBy = effect.from, + skillName = self.name + }) + end, +} +local jijiu = fk.CreateViewAsSkill{ + name = "jijiu", + anim_type = "support", + pattern = "peach", + card_filter = function(self, to_select, selected) + if #selected == 1 then return false end + return Fk:getCardById(to_select).color == Card.Red + end, + view_as = function(self, cards) + if #cards ~= 1 then + return nil + end + local c = Fk:cloneCard("peach") + c:addSubcard(cards[1]) + return c + end, + enabled_at_play = function(self, player) + return false + end, + enabled_at_response = function(self, player) + return player.phase == Player.NotActive + end, +} local huatuo = General:new(extension, "huatuo", "qun", 3) +huatuo:addSkill(qingnang) +huatuo:addSkill(jijiu) Fk:loadTranslationTable{ ["huatuo"] = "华佗", + ["qingnang"] = "青囊", + ["jijiu"] = "急救", } local lvbu = General:new(extension, "lvbu", "qun", 4) @@ -422,9 +973,23 @@ Fk:loadTranslationTable{ ["lvbu"] = "吕布", } +local biyue = fk.CreateTriggerSkill{ + name = "biyue", + anim_type = "drawcard", + events = {fk.EventPhaseStart}, + can_trigger = function(self, event, target, player, data) + return target == player and player:hasSkill(self.name) + and player.phase == Player.Finish + end, + on_use = function(self, event, target, player, data) + player:drawCards(1) + end, +} local diaochan = General:new(extension, "diaochan", "qun", 3, 3, General.Female) +diaochan:addSkill(biyue) Fk:loadTranslationTable{ ["diaochan"] = "貂蝉", + ["biyue"] = "闭月", } return extension diff --git a/packages/standard_cards/init.lua b/packages/standard_cards/init.lua index fa6492d0..c4d98e31 100644 --- a/packages/standard_cards/init.lua +++ b/packages/standard_cards/init.lua @@ -7,19 +7,19 @@ Fk:loadTranslationTable{ local slashSkill = fk.CreateActiveSkill{ name = "slash_skill", + max_phase_use_time = 1, + target_num = 1, can_use = function(self, player) - -- TODO: tmd skill - return player:usedTimes("slash") < 1 + return player:usedCardTimes("slash", Player.HistoryPhase) < self:getMaxUseTime(Self, Player.HistoryPhase) end, target_filter = function(self, to_select, selected) - if #selected == 0 then + if #selected < self:getMaxTargetNum(Self) then local player = Fk:currentRoom():getPlayerById(to_select) return Self ~= player and Self:inMyAttackRange(player) end end, feasible = function(self, selected) - -- TODO: tmd - return #selected == 1 + return #selected >= self:getMinTargetNum() end, on_effect = function(self, room, effect) local to = effect.to @@ -43,6 +43,7 @@ local slash = fk.CreateBasicCard{ } Fk:loadTranslationTable{ ["slash"] = "杀", + ["#slash-jink"] = "%src 对你使用了杀,请使用 %arg 张闪", } extension:addCards({ @@ -166,17 +167,19 @@ extension:addCards({ local dismantlementSkill = fk.CreateActiveSkill{ name = "dismantlement_skill", + target_num = 1, target_filter = function(self, to_select, selected) - if #selected == 0 then + if #selected < self:getMaxTargetNum() then local player = Fk:currentRoom():getPlayerById(to_select) return Self ~= player and not player:isAllNude() end end, feasible = function(self, selected) - return #selected == 1 + return #selected >= self:getMinTargetNum() end, on_effect = function(self, room, effect) local to = room:getPlayerById(effect.to) + if to:isAllNude() then return end local from = room:getPlayerById(effect.from) local cid = room:askForCardChosen( from, @@ -212,10 +215,11 @@ extension:addCards({ local snatchSkill = fk.CreateActiveSkill{ name = "snatch_skill", + distance_limit = 1, target_filter = function(self, to_select, selected) if #selected == 0 then local player = Fk:currentRoom():getPlayerById(to_select) - return Self ~= player and Self:distanceTo(player) <= 1 + return Self ~= player and Self:distanceTo(player) <= self:getDistanceLimit(Self) and not player:isAllNude() end end, @@ -587,14 +591,7 @@ extension:addCards({ local lightningSkill = fk.CreateActiveSkill{ name = "lightning_skill", can_use = function(self, player) - local judge = player:getCardIds(Player.Judge) - for _, id in ipairs(judge) do - local cd = Fk:getCardById(id) - if cd.name == "lightning" then - return false - end - end - return true + return not Self:hasDelayedTrick("lightning") end, on_use = function(self, room, use) if not use.tos or #TargetGroup:getRealTargets(use.tos) == 0 then @@ -606,6 +603,7 @@ local lightningSkill = fk.CreateActiveSkill{ local judge = { who = to, reason = "lightning", + pattern = ".|2~9|spade", } room:judge(judge) local result = judge.card @@ -659,14 +657,7 @@ local indulgenceSkill = fk.CreateActiveSkill{ if #selected == 0 then local player = Fk:currentRoom():getPlayerById(to_select) if Self ~= player then - local judge = player:getCardIds(Player.Judge) - for _, id in ipairs(judge) do - local cd = Fk:getCardById(id) - if cd.name == "indulgence" then - return false - end - end - return true + return not player:hasDelayedTrick("indulgence") end end return false @@ -679,6 +670,7 @@ local indulgenceSkill = fk.CreateActiveSkill{ local judge = { who = to, reason = "indulgence", + pattern = ".|.|spade,club,diamond", } room:judge(judge) local result = judge.card diff --git a/qml/Logic.js b/qml/Logic.js index 40e90f0b..9e081ac1 100644 --- a/qml/Logic.js +++ b/qml/Logic.js @@ -34,7 +34,7 @@ callbacks["NetworkDelayTest"] = function(jsonData) { callbacks["ErrorMsg"] = function(jsonData) { console.log("ERROR: " + jsonData); - toast.show(jsonData, 5000); + toast.show(qsTr(jsonData), 5000); mainWindow.busy = false; } diff --git a/qml/Pages/Common/LogEdit.qml b/qml/Pages/Common/LogEdit.qml index 3cf5f0ca..84e57283 100644 --- a/qml/Pages/Common/LogEdit.qml +++ b/qml/Pages/Common/LogEdit.qml @@ -1,4 +1,5 @@ import QtQuick +import QtQuick.Controls Flickable { id: root @@ -11,6 +12,12 @@ Flickable { contentWidth: textEdit.width contentHeight: textEdit.height clip: true + ScrollBar.vertical: ScrollBar { + parent: root.parent + anchors.top: root.top + anchors.right: root.right + anchors.bottom: root.bottom + } TextEdit { id: textEdit diff --git a/qml/Pages/Init.qml b/qml/Pages/Init.qml index f76da5e2..bdf0f544 100644 --- a/qml/Pages/Init.qml +++ b/qml/Pages/Init.qml @@ -32,7 +32,8 @@ Item { } TextField { id: screenNameEdit - text: "player" + placeholderText: qsTr("Username") + text: "" onTextChanged: { passwordEdit.text = ""; let data = config.savedPassword[server_addr.editText]; @@ -49,12 +50,13 @@ Item { }*/ TextField { id: passwordEdit + placeholderText: qsTr("Password") text: "" echoMode: TextInput.Password passwordCharacter: "*" } Button { - text: "Join Server" + text: qsTr("Join Server") enabled: passwordEdit.text !== "" onClicked: { config.serverAddr = server_addr.editText; @@ -65,7 +67,7 @@ Item { } } Button { - text: "Console start" + text: qsTr("Console start") enabled: passwordEdit.text !== "" onClicked: { config.serverAddr = "127.0.0.1"; diff --git a/qml/Pages/Room.qml b/qml/Pages/Room.qml index 57cd5d88..5e986afb 100644 --- a/qml/Pages/Room.qml +++ b/qml/Pages/Room.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import QtMultimedia import "Common" import "RoomElement" import "RoomLogic.js" as Logic @@ -22,6 +23,7 @@ Item { property alias okButton: okButton property alias cancelButton: cancelButton property alias dynamicCardArea: dynamicCardArea + property alias tableCards: tablePile.cards property var selected_targets: [] property string responding_card @@ -34,6 +36,26 @@ Item { fillMode: Image.PreserveAspectCrop } + MediaPlayer { + id: bgm + source: AppPath + "/audio/system/bgm.mp3" + + // loops: MediaPlayer.Infinite + onPlaybackStateChanged: { + if (playbackState == MediaPlayer.StoppedState && roomScene.isStarted) + play(); + } + audioOutput: AudioOutput {} + } + + onIsStartedChanged: { + if (isStarted) { + bgm.play(); + } else { + // bgm.stop(); + } + } + // tmp Button { text: "quit" @@ -52,15 +74,6 @@ Item { ClientInstance.notifyServer("AddRobot", "[]"); } } - Button { - text: "test" - onClicked: dashboard.expandPile("_equip"); - } - Button { - text: "test2" - x: 60 - onClicked: dashboard.retractPile("_equip"); - } states: [ State { name: "notactive" }, // Normal status @@ -79,11 +92,13 @@ Item { okCancel.visible = false; endPhaseButton.visible = false; respond_play = false; + extra_data = {}; if (dashboard.pending_skill !== "") dashboard.stopPending(); dashboard.disableAllCards(); dashboard.disableSkills(); + dashboard.retractAllPiles(); selected_targets = []; if (popupBox.item != null) { @@ -127,6 +142,9 @@ Item { dashboard.disableSkills(); progress.visible = true; respond_play = false; + roomScene.okCancel.visible = false; + roomScene.okButton.enabled = false; + roomScene.cancelButton.enabled = false; } } } @@ -192,7 +210,7 @@ Item { width: parent.width * 0.6 height: 150 x: parent.width * 0.2 - y: parent.height * 0.6 + 20 + y: parent.height * 0.6 + 10 } } diff --git a/qml/Pages/RoomElement/CardItem.qml b/qml/Pages/RoomElement/CardItem.qml index 69962778..6b46da29 100644 --- a/qml/Pages/RoomElement/CardItem.qml +++ b/qml/Pages/RoomElement/CardItem.qml @@ -24,7 +24,7 @@ Item { property string subtype: "" property string color: "" // only use when suit is empty property string footnote: "" // footnote, e.g. "A use card to B" - property bool footnoteVisible: true + property bool footnoteVisible: false property bool known: true // if false it only show a card back property bool enabled: true // if false the card will be grey property alias card: cardItem @@ -49,6 +49,7 @@ Item { property bool moveAborted: false property alias goBackAnim: goBackAnimation property int goBackDuration: 500 + property bool busy: false // whether there is a running emotion on the card signal toggleDiscards() signal clicked() diff --git a/qml/Pages/RoomElement/Dashboard.qml b/qml/Pages/RoomElement/Dashboard.qml index 62958d99..034877e4 100644 --- a/qml/Pages/RoomElement/Dashboard.qml +++ b/qml/Pages/RoomElement/Dashboard.qml @@ -83,6 +83,8 @@ RowLayout { data.x = parentPos.x; data.y = parentPos.y; let card = component.createObject(roomScene, data); + card.footnoteVisible = true; + card.footnote = Backend.translate("$Equip"); handcardAreaItem.add(card); }) handcardAreaItem.updateCardPosition(); @@ -110,14 +112,33 @@ RowLayout { } } + function retractAllPiles() { + for (let key in expanded_piles) { + retractPile(key); + } + } + // If cname is set, we are responding card. function enableCards(cname) { if (cname) { let ids = [], cards = handcardAreaItem.cards; for (let i = 0; i < cards.length; i++) { - if (JSON.parse(Backend.callLuaFunction("CardFitPattern", [cards[i].name, cname]))) + if (JSON.parse(Backend.callLuaFunction("CardFitPattern", [cards[i].cid, cname]))) ids.push(cards[i].cid); } + cards = selfPhoto.equipArea.getAllCards(); + cards.forEach(c => { + if (JSON.parse(Backend.callLuaFunction( + "CardFitPattern", + [c.cid, cname] + ))) { + ids.push(c.cid); + if (!expanded_piles["_equip"]) { + expandPile("_equip"); + } + } + }); + handcardAreaItem.enableCards(ids); return; } @@ -178,6 +199,19 @@ RowLayout { ))) enabled_cards.push(card.cid); }); + + let cards = selfPhoto.equipArea.getAllCards(); + cards.forEach(c => { + if (JSON.parse(Backend.callLuaFunction( + "ActiveCardFilter", + [pending_skill, c.cid, pendings, targets] + ))) { + enabled_cards.push(c.cid); + if (!expanded_piles["_equip"]) { + expandPile("_equip"); + } + } + }) handcardAreaItem.enableCards(enabled_cards); if (JSON.parse(Backend.callLuaFunction( @@ -204,10 +238,6 @@ RowLayout { item.enabled = item.pressed; } - // TODO: expand pile - - // TODO: equipment - updatePending(); } @@ -221,9 +251,7 @@ RowLayout { pending_skill = ""; pending_card = -1; - // TODO: expand pile - - // TODO: equipment + retractAllPiles(); pendings = []; handcardAreaItem.adjustCards(); @@ -241,9 +269,12 @@ RowLayout { function enableSkills(cname) { if (cname) { + // if cname is presented, we are responding use or play. for (let i = 0; i < skillButtons.count; i++) { let item = skillButtons.itemAt(i); - item.enabled = JSON.parse(Backend.callLuaFunction("SkillFitPattern", [item.orig, cname])); + let fitpattern = JSON.parse(Backend.callLuaFunction("SkillFitPattern", [item.orig, cname])); + let canresp = JSON.parse(Backend.callLuaFunction("SkillCanResponse", [item.orig])); + item.enabled = fitpattern && canresp; } return; } diff --git a/qml/Pages/RoomElement/GeneralCardItem.qml b/qml/Pages/RoomElement/GeneralCardItem.qml index 42764202..25f44844 100644 --- a/qml/Pages/RoomElement/GeneralCardItem.qml +++ b/qml/Pages/RoomElement/GeneralCardItem.qml @@ -59,7 +59,7 @@ CardItem { y: lineCount > 6 ? 30 : 34 text: Backend.translate(name) color: "white" - font.family: fontLiSu.name + font.family: fontLibian.name font.pixelSize: 18 lineHeight: Math.max(1.4 - lineCount / 10, 0.6) style: Text.Outline diff --git a/qml/Pages/RoomElement/GuanxingBox.qml b/qml/Pages/RoomElement/GuanxingBox.qml new file mode 100644 index 00000000..f6e93919 --- /dev/null +++ b/qml/Pages/RoomElement/GuanxingBox.qml @@ -0,0 +1,132 @@ +import QtQuick +import QtQuick.Layouts +import ".." + +GraphicsBox { + id: root + property var cards: [] + property var result: [] + property var areaCapacities: [] + property var areaNames: [] + property int padding: 25 + + title.text: Backend.translate("Please arrange cards") + width: body.width + padding * 2 + height: title.height + body.height + padding * 2 + + ColumnLayout { + id: body + x: padding + y: parent.height - padding - height + spacing: 20 + + Repeater { + id: areaRepeater + model: areaCapacities + + Row { + spacing: 5 + + property int areaCapacity: modelData + property string areaName: index < areaNames.length ? qsTr(areaNames[index]) : "" + + Repeater { + id: cardRepeater + model: areaCapacity + + Rectangle { + color: "#1D1E19" + width: 93 + height: 130 + + Text { + anchors.centerIn: parent + text: areaName + color: "#59574D" + width: parent.width * 0.8 + wrapMode: Text.WordWrap + } + } + } + property alias cardRepeater: cardRepeater + } + } + + MetroButton { + Layout.alignment: Qt.AlignHCenter + text: Backend.translate("OK") + width: 120 + height: 35 + + onClicked: close(); + } + } + + Repeater { + id: cardItem + model: cards + + CardItem { + x: index + y: -1 + cid: modelData.cid + name: modelData.name + suit: modelData.suit + number: modelData.number + draggable: true + onReleased: arrangeCards(); + } + } + + function arrangeCards() { + result = new Array(areaCapacities.length); + let i; + for (i = 0; i < result.length; i++) + result[i] = []; + + let card, j, area, cards, stay; + for (i = 0; i < cardItem.count; i++) { + card = cardItem.itemAt(i); + + stay = true; + for (j = areaRepeater.count - 1; j >= 0; j--) { + area = areaRepeater.itemAt(j); + cards = result[j]; + if (cards.length < areaCapacities[j] && card.y >= area.y) { + cards.push(card); + stay = false; + break; + } + } + + if (stay) + result[0].push(card); + } + + for(i = 0; i < result.length; i++) + result[i].sort((a, b) => a.x - b.x); + + let box, pos, pile; + for (j = 0; j < areaRepeater.count; j++) { + pile = areaRepeater.itemAt(j); + for (i = 0; i < result[j].length; i++) { + box = pile.cardRepeater.itemAt(i); + pos = mapFromItem(pile, box.x, box.y); + card = result[j][i]; + card.origX = pos.x; + card.origY = pos.y; + card.goBack(true); + } + } + } + + function getResult() { + let ret = []; + result.forEach(t => { + let t2 = []; + t.forEach(v => t2.push(v.cid)); + ret.push(t2); + }); + return ret; + } +} diff --git a/qml/Pages/RoomElement/InvisibleCardArea.qml b/qml/Pages/RoomElement/InvisibleCardArea.qml index 6795cee4..a0a26215 100644 --- a/qml/Pages/RoomElement/InvisibleCardArea.qml +++ b/qml/Pages/RoomElement/InvisibleCardArea.qml @@ -67,9 +67,12 @@ Item { card.y -= card.height / 2; items.push(card); if (checkExisting) { - //@to-do: remove it from cards - cards.splice(i, 1); - i--; + for (let j = 0; j < length; j++) { + if (cards[j].cid == card.cid) { + cards.splice(j, 1); + break; + } + } } } } diff --git a/qml/Pages/RoomElement/PhotoElement/DelayedTrickArea.qml b/qml/Pages/RoomElement/PhotoElement/DelayedTrickArea.qml index 8f63ec71..9a999391 100644 --- a/qml/Pages/RoomElement/PhotoElement/DelayedTrickArea.qml +++ b/qml/Pages/RoomElement/PhotoElement/DelayedTrickArea.qml @@ -33,11 +33,18 @@ Item { function add(inputs) { area.add(inputs); - if (inputs instanceof Array) { - cards.append(...inputs); - } else { - cards.append(inputs); + if (!(inputs instanceof Array)) { + inputs = [inputs]; } + inputs.forEach(card => { + let v = JSON.parse(Backend.callLuaFunction("GetVirtualEquip", [parent.playerid, card.cid])); + console.log(JSON.stringify(v)); + if (v !== null) { + cards.append(v); + } else { + cards.append(card); + } + }); } function remove(outputs) diff --git a/qml/Pages/RoomElement/PhotoElement/EquipArea.qml b/qml/Pages/RoomElement/PhotoElement/EquipArea.qml index 3fd9e3b1..5cf0a21c 100644 --- a/qml/Pages/RoomElement/PhotoElement/EquipArea.qml +++ b/qml/Pages/RoomElement/PhotoElement/EquipArea.qml @@ -20,6 +20,7 @@ Column { InvisibleCardArea { id: area + anchors.centerIn: parent checkExisting: true } diff --git a/qml/Pages/RoomElement/PixmapAnimation.qml b/qml/Pages/RoomElement/PixmapAnimation.qml index b4b44545..672742cf 100644 --- a/qml/Pages/RoomElement/PixmapAnimation.qml +++ b/qml/Pages/RoomElement/PixmapAnimation.qml @@ -8,6 +8,7 @@ Item { property int loadedFrameCount: 0 property bool autoStart: false property bool loop: false + property bool keepAtStop: false signal loaded() signal started() @@ -38,8 +39,10 @@ Item { } onLoaded: { - if (autoStart) + if (autoStart) { + root.started(); timer.start(); + } } Timer { @@ -48,7 +51,9 @@ Item { repeat: true onTriggered: { if (currentFrame >= fileModel) { - frames.itemAt(fileModel - 1).visible = false; + if (!keepAtStop) { + frames.itemAt(fileModel - 1).visible = false; + } if (loop) { currentFrame = 0; } else { @@ -69,9 +74,11 @@ Item { function start() { if (loadedFrameCount == fileModel) { + root.started(); timer.start(); } else { - root.loaded.connect(function(){ + root.loaded.connect(() => { + root.started(); timer.start(); }); } diff --git a/qml/Pages/RoomElement/SkillInvokeAnimation.qml b/qml/Pages/RoomElement/SkillInvokeAnimation.qml new file mode 100644 index 00000000..af7a6c1b --- /dev/null +++ b/qml/Pages/RoomElement/SkillInvokeAnimation.qml @@ -0,0 +1,75 @@ +import QtQuick + +Item { + id: root + property string skill_type + property string skill_name + signal finished() + + PixmapAnimation { + id: typeAnim + anchors.centerIn: parent + source: "skillInvoke/" + skill_type + keepAtStop: true + } + + Text { + id: bigSkillName + anchors.centerIn: parent + anchors.horizontalCenterOffset: 100 + text: skill_name + font.pixelSize: Math.max(24, 48 - (text.length - 2) * 6) + font.family: fontLi2.name + style: Text.Outline + color: "white" + opacity: 0 + } + + ParallelAnimation { + id: textAnim + PropertyAnimation { + target: bigSkillName + property: "opacity" + to: 1 + easing.type: Easing.InQuart + duration: 200 + } + + PropertyAnimation { + target: bigSkillName + property: "anchors.horizontalCenterOffset" + to: 0 + easing.type: Easing.InQuad + duration: 240 + } + + onFinished: { + pauseAnim.start(); + } + } + + SequentialAnimation { + id: pauseAnim + + PauseAnimation { + duration: 1200 + } + + PropertyAnimation { + target: root + property: "opacity" + to: 0 + duration: 200 + easing.type: Easing.OutQuart + } + onFinished: { + root.visible = false; + root.finished(); + } + } + + Component.onCompleted: { + typeAnim.start(); + textAnim.start(); + } +} diff --git a/qml/Pages/RoomElement/TablePile.qml b/qml/Pages/RoomElement/TablePile.qml index be335fc1..94af1f52 100644 --- a/qml/Pages/RoomElement/TablePile.qml +++ b/qml/Pages/RoomElement/TablePile.qml @@ -26,21 +26,28 @@ Item { if (toVanish) { for (i = 0; i < discardedCards.length; i++) { card = discardedCards[i]; + if (card.busy) { + discardedCards.splice(i, 1); + continue; + } card.origOpacity = 0; card.goBack(true); card.destroyOnStop() } - cards.splice(0, discardedCards.length); + cards = cards.filter((c) => discardedCards.indexOf(c) === -1); updateCardPosition(true); - discardedCards = new Array(cards.length); - for (i = 0; i < cards.length; i++) - discardedCards[i] = cards[i]; - toVanish = false + discardedCards = []; + for (i = 0; i < cards.length; i++) { + if (cards[i].busy) + continue; + discardedCards.push(cards[i]); + } + toVanish = false; } else { for (i = 0; i < discardedCards.length; i++) { - discardedCards[i].selectable = false + discardedCards[i].selectable = false; } toVanish = true } @@ -52,8 +59,12 @@ Item { area.add(inputs); // if (!inputs instanceof Array) for (let i = 0; i < inputs.length; i++) { - inputs[i].footnoteVisible = true - inputs[i].selectable = true + let c = inputs[i]; + c.footnoteVisible = true; + c.selectable = true; + c.height = c.height * 0.8; + c.width = c.width * 0.8; + c.rotation = (Math.random() - 0.5) * 5; } } @@ -62,6 +73,14 @@ Item { let i, j; let result = area.remove(outputs); + for (let i = 0; i < result.length; i++) { + let c = result[i]; + c.footnoteVisible = false; + c.selectable = false; + c.height = c.height / 0.8; + c.width = c.width / 0.8; + c.rotation = 0; + } let vanished = []; if (result.length < outputs.length) { for (i = 0; i < outputs.length; i++) { diff --git a/qml/Pages/RoomLogic.js b/qml/Pages/RoomLogic.js index f346f596..463566d3 100644 --- a/qml/Pages/RoomLogic.js +++ b/qml/Pages/RoomLogic.js @@ -129,6 +129,15 @@ function getPhotoOrDashboard(id) { return photo; } +function getPhotoOrSelf(id) { + let photo = getPhoto(id); + if (!photo) { + if (id === Self.id) + return dashboard.self; + } + return photo; +} + function getAreaItem(area, id) { if (area === Card.DrawPile) { return drawPile; @@ -169,7 +178,7 @@ function moveCards(moves) { } } -function setEmotion(id, emotion) { +function setEmotion(id, emotion, isCardId) { let path; if (OS === "Win") { // Windows: file:///C:/xxx/xxxx @@ -189,18 +198,38 @@ function setEmotion(id, emotion) { if (component.status !== Component.Ready) return; - let photo = getPhoto(id); - if (!photo) { - if (id === dashboardModel.id) { - photo = dashboard.self; - } else { - return null; + let photo; + if (isCardId === true) { + roomScene.tableCards.forEach((v) => { + if (v.cid === id) { + photo = v; + return; + } + }) + if (!photo) + return; + } else { + photo = getPhoto(id); + if (!photo) { + if (id === dashboardModel.id) { + photo = dashboard.self; + } else { + return null; + } } } let animation = component.createObject(photo, {source: emotion}); animation.anchors.centerIn = photo; - animation.finished.connect(() => animation.destroy()); + if (isCardId) { + animation.started.connect(() => photo.busy = true); + animation.finished.connect(() => { + photo.busy = false; + animation.destroy() + }); + } else { + animation.finished.connect(() => animation.destroy()); + } animation.start(); } @@ -264,7 +293,14 @@ callbacks["AddPlayer"] = function(jsonData) { function enableTargets(card) { // card: int | { skill: string, subcards: int[] } if (roomScene.respond_play) { let candidate = (!isNaN(card) && card !== -1) || typeof(card) === "string"; - okButton.enabled = candidate; + if (candidate) { + okButton.enabled = JSON.parse(Backend.callLuaFunction( + "CardFitPattern", + [card, roomScene.responding_card] + )); + } else { + okButton.enabled = false; + } return; } @@ -298,6 +334,14 @@ function enableTargets(card) { // card: int | { skill: string, subcards: int[] } okButton.enabled = JSON.parse(Backend.callLuaFunction( "CardFeasible", [card, selected_targets] )); + if (okButton.enabled && roomScene.state === "responding") { + okButton.enabled = JSON.parse(Backend.callLuaFunction( + "CardFitPattern", + [card, roomScene.responding_card] + )); + } else if (okButton.enabled && roomScene.state == "playing") { + okButton.enabled = JSON.parse(Backend.callLuaFunction("CanUseCard", [card, Self.id])); + } if (okButton.enabled) { if (roomScene.extra_data instanceof Object) { let must = roomScene.extra_data.must_targets; @@ -345,6 +389,14 @@ function updateSelectedTargets(playerid, selected) { okButton.enabled = JSON.parse(Backend.callLuaFunction( "CardFeasible", [card, selected_targets] )); + if (okButton.enabled && roomScene.state === "responding") { + okButton.enabled = JSON.parse(Backend.callLuaFunction( + "CardFitPattern", + [card, roomScene.responding_card] + )); + } else if (okButton.enabled && roomScene.state == "playing") { + okButton.enabled = JSON.parse(Backend.callLuaFunction("CanUseCard", [card, Self.id])); + } if (okButton.enabled) { if (roomScene.extra_data instanceof Object) { let must = roomScene.extra_data.must_targets; @@ -524,14 +576,39 @@ callbacks["AskForSkillInvoke"] = function(jsonData) { roomScene.cancelButton.enabled = true; } +callbacks["AskForGuanxing"] = function(jsonData) { + let data = JSON.parse(jsonData); + let cards = []; + + roomScene.state = "replying"; + roomScene.popupBox.source = "RoomElement/GuanxingBox.qml"; + data.cards.forEach(id => { + let d = Backend.callLuaFunction("GetCardData", [id]); + cards.push(JSON.parse(d)); + }); + let box = roomScene.popupBox.item; + box.areaCapacities = [cards.length, cards.length]; + box.areaNames = ["Top", "Bottom"]; + box.cards = cards; + box.arrangeCards(); + box.accepted.connect(() => { + replyToServer(JSON.stringify(box.getResult())); + }); +} + callbacks["AskForChoice"] = function(jsonData) { // jsonData: [ string[] choices, string skill ] // TODO: multiple choices, e.g. benxi_ol let data = JSON.parse(jsonData); let choices = data[0]; let skill_name = data[1]; - roomScene.promptText = Backend.translate("#AskForChoice") - .arg(Backend.translate(jsonData));; + let prompt = data[2]; + if (prompt === "") { + roomScene.promptText = Backend.translate("#AskForChoice") + .arg(Backend.translate(skill_name)); + } else { + roomScene.promptText = processPrompt(prompt); + } roomScene.state = "replying"; roomScene.popupBox.source = "RoomElement/ChoiceBox.qml"; let box = roomScene.popupBox.item; @@ -619,6 +696,19 @@ callbacks["AddSkill"] = function(jsonData) { } } +// prompt: 'string::::' +function processPrompt(prompt) { + let data = prompt.split(":"); + let raw = Backend.translate(data[0]); + let src = parseInt(data[1]); + let dest = parseInt(data[2]); + if (raw.match("%src")) raw = raw.replace("%src", Backend.translate(getPhotoOrSelf(src).general)); + if (raw.match("%dest")) raw = raw.replace("%dest", Backend.translate(getPhotoOrSelf(dest).general)); + if (raw.match("%arg")) raw = raw.replace("%arg", Backend.translate(data[3])); + if (raw.match("%arg2")) raw = raw.replace("%arg2", Backend.translate(data[4])); + return raw; +} + callbacks["AskForUseActiveSkill"] = function(jsonData) { // jsonData: string skill_name, string prompt let data = JSON.parse(jsonData); @@ -628,8 +718,9 @@ callbacks["AskForUseActiveSkill"] = function(jsonData) { if (prompt === "") { roomScene.promptText = Backend.translate("#AskForUseActiveSkill") .arg(Backend.translate(skill_name)); + } else { + roomScene.promptText = processPrompt(prompt); } - // TODO: process prompt roomScene.respond_play = false; roomScene.state = "responding"; @@ -656,8 +747,12 @@ callbacks["AskForUseCard"] = function(jsonData) { roomScene.extra_data = extra_data; } - roomScene.promptText = Backend.translate(prompt) - .arg(Backend.translate(cardname)); + if (prompt === "") { + roomScene.promptText = Backend.translate("#AskForUseCard") + .arg(Backend.translate(cardname)); + } else { + roomScene.promptText = processPrompt(prompt); + } roomScene.responding_card = pattern; roomScene.respond_play = false; roomScene.state = "responding"; @@ -672,8 +767,12 @@ callbacks["AskForResponseCard"] = function(jsonData) { let pattern = data[1]; let prompt = data[2]; - roomScene.promptText = Backend.translate(prompt) - .arg(Backend.translate(cardname)); + if (prompt === "") { + roomScene.promptText = Backend.translate("#AskForResponseCard") + .arg(Backend.translate(cardname)); + } else { + roomScene.promptText = processPrompt(prompt); + } roomScene.responding_card = pattern; roomScene.respond_play = true; roomScene.state = "responding"; @@ -698,12 +797,35 @@ callbacks["Animate"] = function(jsonData) { }) break; case "Emotion": - setEmotion(data.player, data.emotion); + setEmotion(data.player, data.emotion, data.is_card); break; case "LightBox": break; case "SuperLightBox": break; + case "InvokeSkill": { + let id = data.player; + let component = Qt.createComponent("RoomElement/SkillInvokeAnimation.qml"); + if (component.status !== Component.Ready) + return; + + let photo = getPhoto(id); + if (!photo) { + if (id === dashboardModel.id) { + photo = dashboard.self; + } else { + return null; + } + } + + let animation = component.createObject(photo, { + skill_name: Backend.translate(data.name), + skill_type: (data.skill_type ? data.skill_type : "special"), + }); + animation.anchors.centerIn = photo; + animation.finished.connect(() => animation.destroy()); + break; + } default: break; } @@ -713,10 +835,32 @@ callbacks["LogEvent"] = function(jsonData) { // jsonData: [Object object] let data = JSON.parse(jsonData); switch (data.type) { - case "Damage": + case "Damage": { let item = getPhotoOrDashboard(data.to); setEmotion(data.to, "damage"); item.tremble(); + Backend.playSound("./audio/system/" + data.damageType + (data.damageNum > 1 ? "2" : "")); + break; + } + case "LoseHP": { + Backend.playSound("./audio/system/losehp"); + break; + } + case "PlaySkillSound": { + Backend.playSound("./audio/skill/" + data.name, data.i); + break; + } + case "PlaySound": { + Backend.playSound(data.name); + break; + } + case "Death": { + let item = getPhoto(data.to); + if (data.to === dashboardModel.id) { + item = dashboard.self; + } + Backend.playSound("./audio/death/" + item.general); + } default: break; } @@ -727,4 +871,5 @@ callbacks["GameOver"] = function(jsonData) { roomScene.popupBox.source = "RoomElement/GameOverBox.qml"; let box = roomScene.popupBox.item; box.winner = jsonData; + roomScene.isStarted = false; } diff --git a/qml/main.qml b/qml/main.qml index dddb64e4..afb4b62f 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -30,7 +30,6 @@ Item { fillMode: Image.PreserveAspectCrop } - FontLoader { id: fontLiSu; source: AppPath + "/fonts/simli.ttf" } FontLoader { id: fontLibian; source: AppPath + "/fonts/FZLBGBK.ttf" } FontLoader { id: fontLi2; source: AppPath + "/fonts/FZLE.ttf" } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6fdd8a9f..badeba01 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -29,6 +29,14 @@ set(freekill_HEADERS ) set(FKP_LIB fkparse) +set(QT_LIB + Qt6::Qml + Qt6::Gui + Qt6::Widgets + Qt6::Network + Qt6::Multimedia + Qt6::QuickControls2 +) if (WIN32) set(LUA_LIB ${PROJECT_SOURCE_DIR}/lib/win/lua54.dll) set(SQLITE3_LIB ${PROJECT_SOURCE_DIR}/lib/win/sqlite3.dll) @@ -41,6 +49,7 @@ elseif (ANDROID) QT_ANDROID_PACKAGE_SOURCE_DIR ${PROJECT_SOURCE_DIR}/android QT_ANDROID_EXTRA_LIBS "${LUA_LIB};${SQLITE3_LIB};${CRYPTO_LIB}" ) + list(REMOVE_ITEM QT_LIB Qt6::QuickControls2) elseif (${CMAKE_SYSTEM_NAME} MATCHES "Emscripten") # WASM list(REMOVE_ITEM freekill_SRCS @@ -72,10 +81,5 @@ target_link_libraries(FreeKill PRIVATE ${CRYPTO_LIB} ${READLINE_LIB} ${FKP_LIB} - Qt6::Qml - Qt6::Gui - Qt6::Widgets - Qt6::Network - Qt6::Multimedia - Qt6::QuickControls2 + ${QT_LIB} ) diff --git a/src/core/util.cpp b/src/core/util.cpp index a4e76ddc..cb00ac24 100644 --- a/src/core/util.cpp +++ b/src/core/util.cpp @@ -116,7 +116,7 @@ QJsonObject SelectFromDatabase(sqlite3 *db, const QString &sql) { QString SelectFromDb(sqlite3 *db, const QString &sql) { QJsonObject obj = SelectFromDatabase(db, sql); - return QJsonDocument(obj).toJson(); + return QJsonDocument(obj).toJson(QJsonDocument::Compact); } void ExecSQL(sqlite3 *db, const QString &sql) { @@ -202,3 +202,12 @@ QString calcFileMD5() { return ret.toHex(); } +QByteArray JsonArray2Bytes(const QJsonArray &arr) { + auto doc = QJsonDocument(arr); + return doc.toJson(QJsonDocument::Compact); +} + +QJsonDocument String2Json(const QString &str) { + auto bytes = str.toUtf8(); + return QJsonDocument::fromJson(bytes); +} diff --git a/src/core/util.h b/src/core/util.h index 767497b3..281f4bc5 100644 --- a/src/core/util.h +++ b/src/core/util.h @@ -18,5 +18,7 @@ RSA *InitServerRSA(); #endif QString calcFileMD5(); +QByteArray JsonArray2Bytes(const QJsonArray &arr); +QJsonDocument String2Json(const QString &str); #endif // _GLOBAL_H diff --git a/src/main.cpp b/src/main.cpp index c54da763..c9e1d8be 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,7 +14,9 @@ #include #include #include +#ifndef Q_OS_ANDROID #include +#endif #if defined(Q_OS_ANDROID) || defined(Q_OS_WASM) static bool copyPath(const QString &srcFilePath, const QString &tgtFilePath) @@ -49,22 +51,22 @@ static bool copyPath(const QString &srcFilePath, const QString &tgtFilePath) void fkMsgHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { fprintf(stderr, "\r[%s] ", QTime::currentTime().toString("hh:mm:ss").toLatin1().constData()); auto localMsg = msg.toUtf8(); - auto threadName = QThread::currentThread()->objectName().toLatin1().constData(); + auto threadName = QThread::currentThread()->objectName().toLatin1(); switch (type) { case QtDebugMsg: - fprintf(stderr, "[%s/DEBUG] %s\n", threadName, localMsg.constData()); + fprintf(stderr, "[%s/DEBUG] %s\n", threadName.constData(), localMsg.constData()); break; case QtInfoMsg: - fprintf(stderr, "[%s/INFO] %s\n", threadName, localMsg.constData()); + fprintf(stderr, "[%s/INFO] %s\n", threadName.constData(), localMsg.constData()); break; case QtWarningMsg: - fprintf(stderr, "[%s/WARNING] %s\n", threadName, localMsg.constData()); + fprintf(stderr, "[%s/WARNING] %s\n", threadName.constData(), localMsg.constData()); break; case QtCriticalMsg: - fprintf(stderr, "[%s/CRITICAL] %s\n", threadName, localMsg.constData()); + fprintf(stderr, "[%s/CRITICAL] %s\n", threadName.constData(), localMsg.constData()); break; case QtFatalMsg: - fprintf(stderr, "[%s/FATAL] %s\n", threadName, localMsg.constData()); + fprintf(stderr, "[%s/FATAL] %s\n", threadName.constData(), localMsg.constData()); break; } } @@ -119,6 +121,10 @@ int main(int argc, char *argv[]) ((QApplication *)app)->setWindowIcon(QIcon("image/icon.png")); #endif + QTranslator translator; + Q_UNUSED(translator.load("zh_CN.qm")); + QCoreApplication::installTranslator(&translator); + #define SHOW_SPLASH_MSG(msg) \ splash.showMessage(msg, Qt::AlignHCenter | Qt::AlignBottom); @@ -138,7 +144,9 @@ int main(int argc, char *argv[]) SHOW_SPLASH_MSG("Loading qml files..."); QQmlApplicationEngine *engine = new QQmlApplicationEngine; +#ifndef Q_OS_ANDROID QQuickStyle::setStyle("Material"); +#endif QmlBackend backend; backend.setEngine(engine); diff --git a/src/network/client_socket.cpp b/src/network/client_socket.cpp index 57fbee8a..6008f835 100644 --- a/src/network/client_socket.cpp +++ b/src/network/client_socket.cpp @@ -97,7 +97,7 @@ void ClientSocket::raiseError(QAbstractSocket::SocketError socket_error) reason = tr("Socket access error"); break; case QAbstractSocket::NetworkError: return; // this error is ignored ... - default: reason = tr("Unknow error"); break; + default: reason = tr("Unknown error"); break; } emit error_message(tr("Connection failed, error code = %1\n reason: %2") diff --git a/src/network/router.cpp b/src/network/router.cpp index 11f7d306..e34187d9 100644 --- a/src/network/router.cpp +++ b/src/network/router.cpp @@ -80,7 +80,7 @@ void Router::request(int type, const QString& command, body << jsonData; body << timeout; - emit messageReady(QJsonDocument(body).toJson(QJsonDocument::Compact)); + emit messageReady(JsonArray2Bytes(body)); #endif } @@ -92,7 +92,7 @@ void Router::reply(int type, const QString& command, const QString& jsonData) body << command; body << jsonData; - emit messageReady(QJsonDocument(body).toJson(QJsonDocument::Compact)); + emit messageReady(JsonArray2Bytes(body)); } void Router::notify(int type, const QString& command, const QString& jsonData) @@ -103,7 +103,7 @@ void Router::notify(int type, const QString& command, const QString& jsonData) body << command; body << jsonData; - emit messageReady(QJsonDocument(body).toJson(QJsonDocument::Compact)); + emit messageReady(JsonArray2Bytes(body)); } int Router::getTimeout() const @@ -128,10 +128,14 @@ void Router::cancelRequest() QString Router::waitForReply(int timeout) { + QString ret; #ifndef Q_OS_WASM replyReadySemaphore.tryAcquire(1, timeout * 1000); - return m_reply; + replyMutex.lock(); + ret = m_reply; + replyMutex.unlock(); #endif + return ret; } void Router::abortRequest() @@ -155,7 +159,7 @@ void Router::handlePacket(const QByteArray& rawPacket) static QMap lobby_actions; if (lobby_actions.size() <= 0) { lobby_actions["UpdateAvatar"] = [](ServerPlayer *sender, const QString &jsonData){ - auto arr = QJsonDocument::fromJson(jsonData.toUtf8()).array(); + auto arr = String2Json(jsonData).array(); auto avatar = arr[0].toString(); static QRegularExpression nameExp("[\\000-\\057\\072-\\100\\133-\\140\\173-\\177]"); if (!nameExp.match(avatar).hasMatch()) { @@ -167,7 +171,7 @@ void Router::handlePacket(const QByteArray& rawPacket) } }; lobby_actions["UpdatePassword"] = [](ServerPlayer *sender, const QString &jsonData){ - auto arr = QJsonDocument::fromJson(jsonData.toUtf8()).array(); + auto arr = String2Json(jsonData).array(); auto oldpw = arr[0].toString(); auto newpw = arr[1].toString(); auto sql_find = QString("SELECT password, salt FROM userinfo WHERE id=%1;") @@ -191,13 +195,13 @@ void Router::handlePacket(const QByteArray& rawPacket) sender->doNotify("UpdatePassword", passed ? "1" : "0"); }; lobby_actions["CreateRoom"] = [](ServerPlayer *sender, const QString &jsonData){ - auto arr = QJsonDocument::fromJson(jsonData.toUtf8()).array(); + auto arr = String2Json(jsonData).array(); auto name = arr[0].toString(); auto capacity = arr[1].toInt(); ServerInstance->createRoom(sender, name, capacity); }; lobby_actions["EnterRoom"] = [](ServerPlayer *sender, const QString &jsonData){ - auto arr = QJsonDocument::fromJson(jsonData.toUtf8()).array(); + auto arr = String2Json(jsonData).array(); auto roomId = arr[0].toInt(); ServerInstance->findRoom(roomId)->addPlayer(sender); }; diff --git a/src/server/room.cpp b/src/server/room.cpp index 53cccf17..82bad244 100644 --- a/src/server/room.cpp +++ b/src/server/room.cpp @@ -109,7 +109,7 @@ void Room::setOwner(ServerPlayer *owner) this->owner = owner; QJsonArray jsonData; jsonData << owner->getId(); - doBroadcastNotify(players, "RoomOwner", QJsonDocument(jsonData).toJson()); + doBroadcastNotify(players, "RoomOwner", JsonArray2Bytes(jsonData)); } void Room::addPlayer(ServerPlayer *player) @@ -131,7 +131,7 @@ void Room::addPlayer(ServerPlayer *player) jsonData << player->getId(); jsonData << player->getScreenName(); jsonData << player->getAvatar(); - doBroadcastNotify(getPlayers(), "AddPlayer", QJsonDocument(jsonData).toJson()); + doBroadcastNotify(getPlayers(), "AddPlayer", JsonArray2Bytes(jsonData)); } players.append(player); @@ -143,20 +143,20 @@ void Room::addPlayer(ServerPlayer *player) jsonData = QJsonArray(); jsonData << this->capacity; jsonData << this->timeout; - player->doNotify("EnterRoom", QJsonDocument(jsonData).toJson()); + player->doNotify("EnterRoom", JsonArray2Bytes(jsonData)); foreach (ServerPlayer *p, getOtherPlayers(player)) { jsonData = QJsonArray(); jsonData << p->getId(); jsonData << p->getScreenName(); jsonData << p->getAvatar(); - player->doNotify("AddPlayer", QJsonDocument(jsonData).toJson()); + player->doNotify("AddPlayer", JsonArray2Bytes(jsonData)); } if (this->owner != nullptr) { jsonData = QJsonArray(); jsonData << this->owner->getId(); - player->doNotify("RoomOwner", QJsonDocument(jsonData).toJson()); + player->doNotify("RoomOwner", JsonArray2Bytes(jsonData)); } if (isFull() && !gameStarted) @@ -182,12 +182,14 @@ void Room::addRobot(ServerPlayer *player) void Room::removePlayer(ServerPlayer *player) { if (!gameStarted) { - players.removeOne(player); + if (players.contains(player)) { + players.removeOne(player); + } emit playerRemoved(player); QJsonArray jsonData; jsonData << player->getId(); - doBroadcastNotify(getPlayers(), "RemovePlayer", QJsonDocument(jsonData).toJson()); + doBroadcastNotify(getPlayers(), "RemovePlayer", JsonArray2Bytes(jsonData)); if (isLobby()) return; } else { @@ -266,7 +268,7 @@ void Room::doBroadcastNotify(const QList targets, } void Room::chat(ServerPlayer *sender, const QString &jsonData) { - auto doc = QJsonDocument::fromJson(jsonData.toUtf8()).object(); + auto doc = String2Json(jsonData).object(); auto type = doc["type"].toInt(); doc["type"] = sender->getId(); if (type == 1) { @@ -306,6 +308,10 @@ void Room::pushRequest(const QString &req) { request_queue_mutex.unlock(); } +bool Room::hasRequest() const { + return !request_queue.isEmpty(); +} + void Room::run() { gameStarted = true; diff --git a/src/server/room.h b/src/server/room.h index 0426413b..dd014d52 100644 --- a/src/server/room.h +++ b/src/server/room.h @@ -56,6 +56,7 @@ public: QString fetchRequest(); void pushRequest(const QString &req); + bool hasRequest() const; signals: void abandoned(); diff --git a/src/server/server.cpp b/src/server/server.cpp index 5f2e5f87..a2d2c349 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -109,10 +109,11 @@ void Server::updateRoomList() obj << room->getCapacity(); // capacity arr << obj; } + auto jsonData = JsonArray2Bytes(arr); lobby()->doBroadcastNotify( lobby()->getPlayers(), "UpdateRoomList", - QJsonDocument(arr).toJson() + QString(jsonData) ); } @@ -135,7 +136,7 @@ void Server::processNewConnection(ClientSocket* client) body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT); body << "NetworkDelayTest"; body << public_key; - client->send(QJsonDocument(body).toJson(QJsonDocument::Compact)); + client->send(JsonArray2Bytes(body)); // Note: the client should send a setup string next connect(client, &ClientSocket::message_got, this, &Server::processRequest); client->timerSignup.start(30000); @@ -159,7 +160,7 @@ void Server::processRequest(const QByteArray& msg) ) valid = false; else - valid = (QJsonDocument::fromJson(doc[3].toString().toUtf8()).array().size() == 3); + valid = (String2Json(doc[3].toString()).array().size() == 3); } if (!valid) { @@ -169,21 +170,20 @@ void Server::processRequest(const QByteArray& msg) body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT); body << "ErrorMsg"; body << "INVALID SETUP STRING"; - client->send(QJsonDocument(body).toJson(QJsonDocument::Compact)); + client->send(JsonArray2Bytes(body)); client->disconnectFromHost(); return; } - QJsonArray arr = QJsonDocument::fromJson(doc[3].toString().toUtf8()).array(); + QJsonArray arr = String2Json(doc[3].toString()).array(); if (md5 != arr[2].toString()) { - qWarning() << "MD5 check failed!"; QJsonArray body; body << -2; body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT); body << "ErrorMsg"; body << "MD5 check failed!"; - client->send(QJsonDocument(body).toJson(QJsonDocument::Compact)); + client->send(JsonArray2Bytes(body)); client->disconnectFromHost(); return; } @@ -273,7 +273,7 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString& name, co arr << player->getId(); arr << player->getScreenName(); arr << player->getAvatar(); - player->doNotify("Setup", QJsonDocument(arr).toJson()); + player->doNotify("Setup", JsonArray2Bytes(arr)); lobby()->addPlayer(player); } else { @@ -283,7 +283,7 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString& name, co body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT); body << "ErrorMsg"; body << error_msg; - client->send(QJsonDocument(body).toJson(QJsonDocument::Compact)); + client->send(JsonArray2Bytes(body)); client->disconnectFromHost(); return; } diff --git a/src/server/shell.cpp b/src/server/shell.cpp index 0edddae6..47e37ab0 100644 --- a/src/server/shell.cpp +++ b/src/server/shell.cpp @@ -14,35 +14,12 @@ static void sigintHandler(int) { rl_redisplay(); } -const char *Shell::ColoredText(const char *input, Color color, TextType type) { - QString str(input); - str.append("\e[0m"); - QString header = "\e["; - switch (type) { - case NoType: - header.append("0"); - break; - case Bold: - header.append("1"); - break; - case UnderLine: - header.append("4"); - break; - } - header.append(";"); - header.append(QString::number(30 + color)); - header.append("m"); - header.append(str); - auto bytes = header.toUtf8(); - return bytes.constData(); -} - void Shell::helpCommand(QStringList &) { qInfo("Frequently used commands:"); - qInfo("%s: Display this help message.", ColoredText("help", Blue)); - qInfo("%s: Shut down the server.", ColoredText("quit", Blue)); - qInfo("%s: List all online players.", ColoredText("lsplayer", Blue)); - qInfo("%s: List all running rooms.", ColoredText("lsroom", Blue)); + qInfo("%s: Display this help message.", "help"); + qInfo("%s: Shut down the server.", "quit"); + qInfo("%s: List all online players.", "lsplayer"); + qInfo("%s: List all running rooms.", "lsroom"); qInfo("For more commands, check the documentation."); } @@ -51,7 +28,7 @@ void Shell::lspCommand(QStringList &) { qInfo("No online player."); return; } - qInfo("Current %d online player(s) are:", ServerInstance->players.size()); + qInfo("Current %lld online player(s) are:", ServerInstance->players.size()); foreach (auto player, ServerInstance->players) { qInfo() << player->getId() << "," << player->getScreenName(); } @@ -62,7 +39,7 @@ void Shell::lsrCommand(QStringList &) { qInfo("No running room."); return; } - qInfo("Current %d running rooms are:", ServerInstance->rooms.size()); + qInfo("Current %lld running rooms are:", ServerInstance->rooms.size()); foreach (auto room, ServerInstance->rooms) { qInfo() << room->getId() << "," << room->getName(); } diff --git a/src/server/shell.h b/src/server/shell.h index 1bcb1c3e..fa966604 100644 --- a/src/server/shell.h +++ b/src/server/shell.h @@ -20,7 +20,6 @@ public: Bold, UnderLine }; - static const char *ColoredText(const char *input, Color color, TextType type = NoType); protected: virtual void run(); diff --git a/src/swig/naturalvar.i b/src/swig/naturalvar.i index 8bd216aa..a55ec40a 100644 --- a/src/swig/naturalvar.i +++ b/src/swig/naturalvar.i @@ -27,7 +27,16 @@ SWIG_arg ++; %{ $1 = lua_tostring(L, $input); %} %typemap(out) QString -%{ lua_pushstring(L, $1.toUtf8()); SWIG_arg++; %} +%{ + if ($1.isEmpty()) { + lua_pushstring(L, ""); + } else if ($1 == "__notready") { + lua_pushstring(L, "__notready"); + } else { + lua_pushstring(L, $1.toUtf8()); + } + SWIG_arg++; +%} // const QString & %typemap(arginit) QString const & @@ -40,7 +49,16 @@ SWIG_arg ++; %} %typemap(out) QString const & -%{ lua_pushstring(L, $1.toUtf8()); SWIG_arg++; %} +%{ + if ($1.isEmpty()) { + lua_pushstring(L, ""); + } else if ($1 == "__notready") { + lua_pushstring(L, "__notready"); + } else { + lua_pushstring(L, $1.toUtf8()); + } + SWIG_arg++; +%} // QStringList %naturalvar QStringList; diff --git a/src/swig/server.i b/src/swig/server.i index 763b1d40..579df26a 100644 --- a/src/swig/server.i +++ b/src/swig/server.i @@ -52,6 +52,7 @@ public: LuaFunction startGame; QString fetchRequest(); + bool hasRequest() const; }; %{ diff --git a/src/ui/qmlbackend.cpp b/src/ui/qmlbackend.cpp index 2ba194b6..11af88fa 100644 --- a/src/ui/qmlbackend.cpp +++ b/src/ui/qmlbackend.cpp @@ -1,4 +1,8 @@ #include "qmlbackend.h" +#include +#include +#include +#include #ifndef Q_OS_WASM #include "server.h" #endif @@ -55,7 +59,7 @@ void QmlBackend::joinServer(QString address) { if (ClientInstance != nullptr) return; Client *client = new Client(this); - connect(client, &Client::error_message, [this, client](const QString &msg){ + connect(client, &Client::error_message, this, [=](const QString &msg){ client->deleteLater(); emit notifyUI("ErrorMsg", msg); emit notifyUI("BackToStart", "[]"); @@ -285,3 +289,38 @@ QString QmlBackend::calcFileMD5() { return ::calcFileMD5(); } +void QmlBackend::playSound(const QString &name, int index) { + QString fname(name); + if (index == -1) { + int i = 1; + while (true) { + if (!QFile::exists(name + QString::number(i) + ".mp3")) { + i--; + break; + } + i++; + } + + index = i == 0 ? 0 : (QRandomGenerator::global()->generate()) % i + 1; + } + if (index != 0) + fname = fname + QString::number(index) + ".mp3"; + else + fname = fname + ".mp3"; + + if (!QFile::exists(fname)) return; + + auto player = new QMediaPlayer; + auto output = new QAudioOutput; + player->setAudioOutput(output); + player->setSource(QUrl::fromLocalFile(fname)); + output->setVolume(50); // TODO: volume config + connect(player, &QMediaPlayer::playbackStateChanged, this, [=](){ + if (player->playbackState() == QMediaPlayer::StoppedState) { + player->deleteLater(); + output->deleteLater(); + } + }); + player->play(); +} + diff --git a/src/ui/qmlbackend.h b/src/ui/qmlbackend.h index 01e07ad9..79a7bd2e 100644 --- a/src/ui/qmlbackend.h +++ b/src/ui/qmlbackend.h @@ -43,6 +43,7 @@ public: Q_INVOKABLE void parseFkp(const QString &filename); Q_INVOKABLE QString calcFileMD5(); + Q_INVOKABLE void playSound(const QString &name, int index = 0); signals: void notifyUI(const QString &command, const QString &jsonData);