diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml new file mode 100644 index 00000000..5db967a3 --- /dev/null +++ b/.github/workflows/sphinx.yml @@ -0,0 +1,19 @@ +name: Deploy Sphinx documentation to Pages + +# Runs on pushes targeting the default branch +on: + push: + branches: [master] + +jobs: + pages: + runs-on: ubuntu-20.04 + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + permissions: + pages: write + id-token: write + steps: + - id: deployment + uses: sphinx-notes/pages@v3 diff --git a/doc/dev/compile.md b/doc/dev/compile.md deleted file mode 100644 index 0f0e69b3..00000000 --- a/doc/dev/compile.md +++ /dev/null @@ -1,134 +0,0 @@ -# 编译 FreeKill - -> [dev](./index.md) > 编译 - -___ - -## 全平台通用步骤 - -FreeKill采用最新的Qt进行构建,因此需要先安装Qt6的开发环境。 - -无论是Win还是Linux,都建议用[Qt官方的下载器](https://download.qt.io/official_releases/online_installers/)进行安装。当然了,在一些软件更新很频繁的Linux发行版里面,可能已经能从包管理器安装Qt6,对此后文细说。这个环节介绍用Qt安装器安装的步骤。 - -Qt安装的流程不赘述。为了编译FreeKill,至少需要安装以下的组件: -- Qt 6: MinGW 11.2.0 64-bit (不支持MSVC) -- Qt 6: Qt5 Compat -- Qt 6: Shader Tools (为了使用GraphicalEffects) -- Qt 6: Multimedia -- QtCreator(这个是安装器强制要你安装的) -- CMake、Ninja -- OpenSSL 1.1.1 - -接下来根据平台的不同,步骤也稍有区别。 - -___ - -## Windows - -从网络上下载swig、flex、bison。swig在其官网可以下载,flex和bison可在[github](https://github.com/lexxmark/winflexbison/releases/)或者SourceForge下载。 - -全都下载完成之后,将含有swig.exe、win_flex.exe、win_bison.exe的文件夹全部都设置到Path环境变量里面去。 - -接下来使用QtCreator打开项目,然后尝试编译。 - -这时遇到cmake报错:OpenSSL:Crypto not found. 这是因为我们还没有告诉编译器OpenSSL的位置,点左侧“项目”,查看构建选项,在CMake的Initial Configuration中,点击添加按钮,新增String型环境变量OPENSSL_ROOT_DIR,将其值设为跟Qt一同安装的OpenSSL的位置(如C:/Qt/Tools/OpenSSL/Win_x64)。然后点下方的Re-configure with Initial Parameters,这样就能正常编译了。 - -运行的话,在Qt Creator的项目选项->运行中,先将工作目录改为项目所在的目录(git仓库的目录)。然后先将编译好了的FreeKill.exe放到项目目录中,在目录下打开CMD,执行windeployqt FreeKill.exe。调整目录下的dll文件直到能运行起来为止,之后就可以在Qt Creator中正常运行和调试了。 - -___ - -## Linux - -通过包管理器安装一些额外软件包方可编译。 - -Debian一家子: - -```sh -$ sudo apt install liblua5.4-dev libsqlite3-dev libssl-dev swig flex bison -``` - -Arch Linux: - -```sh -$ sudo pacman -Sy lua sqlite swig openssl flex bison -``` - -然后使用配置好的QtCreator环境即可编译。 - -如果你不想用Qt安装器的话,可以用包管理器安装依赖,下面仅举例Arch: - -```sh -$ sudo pacman -S qt6-base qt6-declarative qt6-5compat qt6-multimedia -$ sudo pacman -S cmake lua sqlite swig openssl swig flex bison -``` - -然后可以用命令行编译: - -```sh -$ mkdir build && cd build -$ cmake .. -$ make -j8 -``` - -___ - -## Linux服务器 - -一般来说Linux服务器的包管理器都没新到提供Qt6下载,这个时候想编译服务端的话,需要在尽可能安装完Qt5环境的情况下,对FreeKill的Qt版本降一下等级。 - -首先将根目录和src下面的两个CMakeLists.txt的Qt6都改成Qt5,然后试图进行编译。 - -编译器会报告大概不超过10处错误,将它们修改成Qt5可以接受的形式就行了。 - -___ - -## MacOS - -大致与Windows类似,但尚且缺少确切的方案。 - -___ - -## 编译安卓版 - -用Qt安装器装好Android库,然后配置一下android-sdk就能编译了。 - -(Qt 6.4的刘海屏bug,手动往QActivity.java的onCreate函数追加如下代码即可实现完全全屏。这里做个笔记方便复制粘贴,等Qt修了再说) - -```java -getWindow().addFlags(LayoutParams.FLAG_FULLSCREEN); -if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { - getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); -} -if (Build.VERSION.SDK_INT > 28) { - WindowManager.LayoutParams lp = getWindow().getAttributes(); - lp.layoutInDisplayCutoutMode = LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; - getWindow().setAttributes(lp); -} -``` - -___ - -## WASM下编译 - -WASM大概就是能在浏览器中跑C++。编译用Qt Creator即可。 - -### 1. 条件与局限性 - -如果程序运行在网页上的话,那么理应只有客户端,然后提供网页的服务器上自然也运行着一个后端服务器。所以说在编译时应该舍弃掉服务端相关的代码。因此依赖库就不再需要sqlite3。 - -总之是编译个纯客户端的FK。 - -### 2. 编译OpenSSL - -进入OpenSSL的src目录,然后 - - $ ./config -no-asm -no-engine -no-dso - $ emmake make -j8 build_generated libssl.a libcrypto.a - -编译Lua的话直接emmake make就行了,总之库已经传到仓库了。 - -### 3. 部署资源文件 - -由于CMake中`file(GLOB_RECURSE)`所带来的缺陷,每当资源文件变动时,需要手动更新。 - -把构建目录中的.rcc目录删掉然后重新执行CMake->make即可。每次编译资源文件总要消耗相当多的时间。 diff --git a/doc/dev/index.md b/doc/dev/index.md deleted file mode 100644 index 829007fb..00000000 --- a/doc/dev/index.md +++ /dev/null @@ -1,15 +0,0 @@ -# FreeKill 开发文档 - -> dev - -___ - -FreeKill采用Qt框架提供底层支持,在上层使用lua语言开发。在UI方面使用的是Qt Quick。 - -- [编译](./compile.md) -- [通信](./protocol.md) -- [游戏逻辑](./gamelogic.md) -- [数据库](./database.md) -- [UI](./ui.md) -- [包管理](./package.md) -- [AI](./ai.md) diff --git a/doc/diy/01-env.md b/doc/diy/01-env.md deleted file mode 100644 index 7129c121..00000000 --- a/doc/diy/01-env.md +++ /dev/null @@ -1,227 +0,0 @@ -# Fk DIY - 环境搭建 - -> [diy](./index.md) > 环境搭建 - -* [DIY总览](#diy总览) -* [环境搭建](#环境搭建) - * [Fk](#fk) - * [代码编辑器](#代码编辑器) - * [git](#git) - * [安装git](#安装git) -* [新增mod](#新增mod) -* [发布mod](#发布mod) - * [将终端切换为Git Bash](#将终端切换为git-bash) - * [配置ssh key](#配置ssh-key) - * [新建git仓库](#新建git仓库) - * [让他人安装并游玩你的mod](#让他人安装并游玩你的mod) - * [更新mod](#更新mod) - -___ - -## DIY总览 - -正如[项目README](../../README.md)所言,FreeKill“试图打造一个最适合diy玩家游玩的民间三国杀”。即便是最开始游戏功能尚未完善,FreeKill也已经具备了对DIY的支持。所有拓展包都列在packages/文件夹下,感兴趣者可以自行查看。 - -欲为FreeKill进行DIY,需要使用的编程语言为Lua。若您对Lua语言完全不熟悉,推荐去[菜鸟教程](https://www.runoob.com/lua/lua-tutorial.html)速通一遍基本语法。剩下的就基本是在实践中慢慢领会了。 - -FreeKill本体中自带有标准包和标准卡牌包,可作为DIY时候的例子。事实上,其他DIY包也是像这样子组织的。 - -接下来讲述如何配置环境。 - -___ - -## 环境搭建 - -### Fk - -Fk是游戏本身,也是拓展包运行的平台。事实上这份文档应该与Fk一同发布的,如果您正在阅读这份文档,那么您理应已经接收到了Fk本身。 - -### 代码编辑器 - -代码编辑器任选一种即可,但一定要确保以下几点: - -- 至少要是一款**代码**编辑器,要有语法高亮功能 -- 需要有EmmyLua插件的支持 -- 需要默认UTF-8格式保存代码文件 - -> EmmyLua是一种特别的Lua注释方式,可以为本来弱类型的Lua语言提供类型支持,这对于像FreeKill这种稍有规模的Lua项目是十分必要的。目前能提供开箱即用的EmmyLua插件编辑器主要有IntelliJ IDEA和Visual Studio Code。EmmyLua也能以LSP的方式运行,因此支持LSP的编辑器(这种就多了,比如vim, sublime)也能符合条件。 - -编辑器的具体安装以及插件配置不在此赘述。 - -> 出于易用性和免费的考虑,推荐用VSCode进行拓展。下文将以VSCode为编辑器进行进一步说明。 - -### git - -git就不必多介绍了吧,这里说说为什么需要配置git。这是因为在Fk中,拓展包拥有在线安装/在线更新的功能,这种功能都是依托于git进行的,因此如果你打算将自己的拓展包发布出去的话,就需要将其创建git仓库,并托管到git托管网站去。 - -> 考虑到国内绝大部分人的访问速度,综合国内几家git托管平台,建议使用gitee。 - -大多数人可能从未用过git,并且git上手的门槛并不低,因此以下会对涉及git的操作进行详尽的解说。 - -#### 安装git - -前往[官网](https://git-scm.com/download/win)下载git,下载64-bit Git for Windows Setup。这样应该会为您下载一个exe安装包。 - -考虑到官网的下载链接实际上指向github,而且可能连官网的都进不去,所以也考虑[从清华源下载Git](https://mirrors.tuna.tsinghua.edu.cn/github-release/git-for-windows/git/)。 - -欲验证安装是否完成,可以按下Win+R -> cmd弹出命令行窗口,输入git命令,如果出来一长串英文说明安装成功了。 - -___ - -## 新增mod - -这只是新增mod的一个例子。当然了,以后有啥要做的实例也会继续用这个拓展包的。 - -首先前往packages下,新建名为fk_study的文件夹。 - -再在fk_study下新建init.lua文件,写入以下内容: - -```lua -local extension = Package("fk_study") - -Fk:loadTranslationTable{ - ["fk_study"] = "fk学习包", -} - -return { extension } -``` - -保存退出,打开Fk,进武将一览。你现在应该能在武将一览里面看到“fk学习包”了,但也仅此而已了,毕竟这还只是个空壳包而已。 - -至此我们已经创建了最为简单的mod。mod的文件结构如下: - - fk_study - └── init.lua - -___ - -## 发布mod - -一种最常见的发布mod方式是把mod打包成zip,发到公共平台上供玩家下载。这种办法虽然可行,但并不是fk推荐的做法。 - -> 以下介绍的其实就是新建仓库并推送到gitee的办法,熟悉git者请跳过。 - -下面着重介绍用git发布mod的办法。使用git进行发布的话,就可以让用户体验在线安装、在线更新等便捷之处。 - -以下假设你使用vscode进行代码编辑。你是先用vscode打开了整个FreeKill文件夹,再在其中新建文件夹和文件、然后进行编辑的。 - -菜单栏 -> 终端 -> 新建终端。我们接下来的工作都在终端中完成。 - -### 将终端切换为Git Bash - -启动终端后,终端的内容大概是: - -```plain -Mincrosoft Windows 10 [版本号啥的] -xxxxxxxx 保留所有权利。 - -C:\FreeKill> -``` - -这个是Windows自带的cmd,我们不使用这个,而是去用git bash。此时终端上面应该有这么一条: - -```plain -问题 输出 调试控制台 _终端_ cmd + v 分屏 删除 - 注意这个加号 -``` - -这时候点击加号右边那个下拉箭头,选择"Git Bash"。这样就成功的切换到了git bash中,终端看起来应该像这样: - -```plain -xxx@xxxxx MINGW64 /c/FreeKill -$ -``` - -### 配置ssh key - -你应该已经注册好了自己的gitee账号。首先在Git bash中输入这些命令(#号后面的是命令注释,不用照搬;命令开头的\$符号是模拟shell的界面,不要输入进去): - -```sh -$ cd ~/.ssh -$ ssh-keygen -t rsa -C "你注册用的邮箱地址" # 换成自己真正的邮箱 - # 出来一堆东西,一路点回车就是了 -$ cat id_rsa.pub - # 出来一堆乱七八糟的东西:ssh-rsa <一大堆乱七八糟的内容> <你的邮箱> -$ cd - -``` - -在cat id_rsa.pub中,出来的那一堆以ssh-rsa的输出,就是这里要用到的“公钥”。然后在gitee中: - -1. 点右上角你的头像,点账号设置 -2. 点左侧栏中 安全设置 - SSH公钥 -3. 此时弹出公钥添加界面,标题任选,下面公钥那一栏中,将刚刚生成的公钥复制粘贴上去 -4. 点确定 - -这样就配置好了ssh公钥。进行验证,在bash中使用命令: - -```sh -$ ssh -T git@gitee.com -Hi xxxx! You've successfully authenticated, but GITEE.COM does not provide shell access. -``` - -输出像Hi xxx!这样的信息,就说明配置成功了。否则需要进一步检查自己的操作,上网查一下吧。 - -### 新建git仓库 - -现在终端的工作目录应该还是FreeKill根目录,我们先切换到mod的目录去,然后再在shell中进行一系列操作。 - -```sh -$ cd packages/fk_study -$ git init # 创建新的空仓库 -$ git add . # 将文件夹中所有的文件都加入暂存区 -$ git commit -m "init" # 提交目前所有的文件,这样文件就正式存在于仓库里面了 -作者身份未知 -*** 请告诉我您是谁。 -运行 - git config --global user.email "you@example.com" - git config --global user.name "Your Name" - -来设置您账号的缺省身份标识。如果仅在本仓库设置身份标识,则省略 --global 参数。 -``` - -看来我们初次安装Git,Git还不知道我们的身份呢,不过git已经告诉了配置所需的命令了。运行前一条命令告知自己的名字,运行后一条命令告知自己的邮箱。如此就OK了,然后再commit一次。 - -然后在gitee中也新建一个仓库,取名为fk_study。接下来回到终端里面: - -```sh -$ git remote add origin git@gitee.com:xxx/fk_study # 其中这个xxx是你的用户名 -$ git push -u origin master -``` - -OK了,刷新你新建的那个仓库的页面,可以看到里面已经有init.lua了。此时距离发布mod只有最后一步,那就是把仓库设置为开源。请自行在gitee中设置吧。 - -### 让他人安装并游玩你的mod - -注意到Fk初始界面里面的“管理拓展包”了不?这个就是让你安装、删除、更新拓展包用的。在那个页面里面有个输入框,在浏览器中复制仓库的地址(比如https://gitee.com/xxx/fk_study/ ),粘贴到输入框,然后单击“从URL安装”即可安装拓展包了。 - -### 更新mod - -现在mod要发生更新了,更新内容为一个武将。先在init.lua中新增武将吧。 - -```lua -local study_sunce = General(extension, "study_sunce", "wu", 4) -Fk:loadTranslationTable{ - ["study_sunce"] = "孙伯符", -} -``` - -保存,此时注意vscode左侧栏变成了: - - v fk_study - └── init.lua M - -init.lua后面出现了“M”,并且文件名字也变成了黄色,这表示这个文件已经被修改过了,接下来我们把修改文件提交到仓库中: - -```sh -$ git add . # 将当前目录下的文件暂存 -$ git commit -m "add general sunce" # 提交更改,提交说明为add general sunce -$ git push # “推”到远端,也就是把本地的更新传给远端 -``` - -不喜欢用命令行的话,也可以用vscode自带的git支持完成这些操作,这里就不赘述了。做完git push后,实际上就已经完成更新了,可以让大伙点点更新按钮来更新你的新版本了。 - -___ - -以上介绍了大致的创建mod以及更新的流程。至于资源文件组织等等杂七杂八的问题,请参考已有的例子拓展包。 - -下一篇: [fk技能类型总览](./02-skilltype.md) diff --git a/doc/diy/02-skilltype.md b/doc/diy/02-skilltype.md deleted file mode 100644 index 4d4cad90..00000000 --- a/doc/diy/02-skilltype.md +++ /dev/null @@ -1,45 +0,0 @@ -# fk技能类型总览 - -> [diy](./index.md) > fk技能类型总览 - -___ - -fk的目的是便于三国杀的DIY,而三国杀DIY的核心就是制作各种技能了。 - -fk的技能分为两大类,这两大类又各自细分为更小的分类: - -(关于这部分的源码详见lua/core/skill.lua和lua/core/skill_type下的所有文件) - -* 可使用类技能(UsableSkill) - * 触发技(TriggerSkill):在满足一定条件时,能够通过被动触发发挥效果的技能 - * 主动技(ActiveSkill):玩家主动发动的技能 - * 视为技(ViewAsSkill):将一张牌当做另一张牌的技能 -* 状态技(StatusSkill) - * 距离技(DistanceSkill):影响距离计算的技能 - * 攻击范围技(DistanceSkill):影响攻击范围计算的技能 - * 手牌上限技(MaxCardsSkill):影响手牌上限计算的技能 - * 禁止技(ProhibitSkill):禁止成为卡牌目标的技能 - * 卡牌增强技(TargetModSkill):影响卡牌使用次数上限、目标上限、距离限制等等的技能 - * 锁定视为技(FilterSkill):让一张牌强制视为另一张牌的技能 - -其中,触发技的逻辑最为复杂,但是[已经在这里分析过了](../dev/gamelogic.md),故不再赘述。 - -主动技和状态技应该不算难,先按下不表。视为技与神杀有所区别,区别如下: - -在神杀中,视为技是否可响应是专门写在enabled_at_response的,fk则不然,看倾国的代码: - -```lua -local qingguo = fk.CreateViewAsSkill{ - name = "qingguo", - anim_type = "defensive", - pattern = "jink", - card_filter = function(self, to_select, selected) - -- ... - end, - view_as = function(self, cards) - -- ... - end, -} -``` - -可见并没有编写跟响应时候有关的函数,也没有声明出牌阶段不可用。其中的奥妙就在于pattern中,视为技可以转化的卡牌都应该写在pattern里面,Fk会根据pattern的内容判断技能出牌阶段是否可用、是否能够响应等。 diff --git a/doc/diy/03-events.md b/doc/diy/03-events.md deleted file mode 100644 index 0d28d1ce..00000000 --- a/doc/diy/03-events.md +++ /dev/null @@ -1,9 +0,0 @@ -# fk中的游戏事件 - -在进行DIY时,需要对三国杀的规则有一定了解;在编写技能时,也要熟悉游戏提供的各种事件,他的触发方式、触发时机、相关数据。必须要知道这些才能写出正确的代码。 - -- [与游戏流程相关的事件](./event/gameflow.md) -- [与体力值相关的事件](./event/hp.md) -- [与卡牌使用有关的事件](./event/usecard.md) -- [与移动牌有关的事件](./event/movecard.md) -- [杂项](./event/misc.md) diff --git a/doc/diy/event/gameflow.md b/doc/diy/event/gameflow.md deleted file mode 100644 index 985a3af6..00000000 --- a/doc/diy/event/gameflow.md +++ /dev/null @@ -1,33 +0,0 @@ -# 与游戏流程有关的事件 - -先来看游戏流程本身。以下节选自lua/server/gamelogic.lua - -```lua -function GameLogic:action() - self:trigger(fk.GameStart) - local room = self.room - - for _, p in ipairs(room.alive_players) do - self:trigger(fk.DrawInitialCards, p, { num = 4 }) - end - - local function checkNoHuman() - -- 如果房里已经没有人类玩家了就结束游戏 - end - - while true do - self:trigger(fk.TurnStart, room.current) - if room.game_finished then break end - room.current = room.current:getNextAlive() - if checkNoHuman() then - room:gameOver("") - end - end -end -``` - -以上这段代码,述说的就是整个游戏流程的核心。首先开始游戏、摸初始手牌,然后按照座位顺序每人依次执行回合直到游戏结束。 - -___ - -TODO diff --git a/doc/diy/event/hp.md b/doc/diy/event/hp.md deleted file mode 100644 index 3bd7708e..00000000 --- a/doc/diy/event/hp.md +++ /dev/null @@ -1,11 +0,0 @@ -# 与体力值相关的事件 - -___ - -## 伤害 - -## 失去体力/体力上限 - -## 回复体力 - -## 濒死和死亡 diff --git a/doc/diy/event/misc.md b/doc/diy/event/misc.md deleted file mode 100644 index e69de29b..00000000 diff --git a/doc/diy/event/movecard.md b/doc/diy/event/movecard.md deleted file mode 100644 index e69de29b..00000000 diff --git a/doc/diy/event/usecard.md b/doc/diy/event/usecard.md deleted file mode 100644 index e69de29b..00000000 diff --git a/doc/diy/index.md b/doc/diy/index.md deleted file mode 100644 index e992b84d..00000000 --- a/doc/diy/index.md +++ /dev/null @@ -1,16 +0,0 @@ -# 如何用FreeKill实现diy武将 - -> diy - -___ - -以下是一系列文档,旨在向从未接触过FreeKill(以后简称为Fk)的DIYer介绍Fk的DIY接口,以及如何打包、发布。 - -本系列文档针对对神杀Lua有基础的读者编写。 - -由于对于Win系统而言,fk仅仅支持Win 10及以上的64位系统,因此本文档假设您正使用Windows 10作为操作系统,且使用着64位的处理器。 - -文档中蓝色字均为超链接。 - -1. [环境搭建](./01-env.md) -2. [fk技能类型总览](./02-skilltype.md) diff --git a/doc/index.md b/doc/index.md deleted file mode 100644 index 50906e52..00000000 --- a/doc/index.md +++ /dev/null @@ -1,6 +0,0 @@ -# FreeKill 文档 - -___ - -- [开发者文档](./dev/index.md) -- [DIY玩家文档](./diy/index.md) diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..ed880990 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/api/client.rst b/docs/api/client.rst new file mode 100644 index 00000000..5cd51a1b --- /dev/null +++ b/docs/api/client.rst @@ -0,0 +1,2 @@ +Client +============ diff --git a/docs/api/core.rst b/docs/api/core.rst new file mode 100644 index 00000000..07d64d1a --- /dev/null +++ b/docs/api/core.rst @@ -0,0 +1,15 @@ +Core +======== + +.. toctree:: + :maxdepth: 1 + :caption: API + + core/engine.rst + core/card.rst + core/general.rst + core/package.rst + core/skill.rst + core/game_mode.rst + core/player.rst + diff --git a/docs/api/core/card.rst b/docs/api/core/card.rst new file mode 100644 index 00000000..35299e1e --- /dev/null +++ b/docs/api/core/card.rst @@ -0,0 +1,4 @@ +Card +============== + +.. lua:autoclass:: Card diff --git a/docs/api/core/engine.rst b/docs/api/core/engine.rst new file mode 100644 index 00000000..bc1420ab --- /dev/null +++ b/docs/api/core/engine.rst @@ -0,0 +1,4 @@ +Engine +============== + +.. lua:autoclass:: Engine diff --git a/docs/api/core/game_mode.rst b/docs/api/core/game_mode.rst new file mode 100644 index 00000000..abb1ac4c --- /dev/null +++ b/docs/api/core/game_mode.rst @@ -0,0 +1,5 @@ +GameMode +============== + +.. lua:autoclass:: GameMode + diff --git a/docs/api/core/general.rst b/docs/api/core/general.rst new file mode 100644 index 00000000..cd739381 --- /dev/null +++ b/docs/api/core/general.rst @@ -0,0 +1,5 @@ +General +============== + +.. lua:autoclass:: General + diff --git a/docs/api/core/package.rst b/docs/api/core/package.rst new file mode 100644 index 00000000..273ff3dd --- /dev/null +++ b/docs/api/core/package.rst @@ -0,0 +1,24 @@ +Package +============== + +.. lua:autoclass:: Package + +详细信息 +~~~~~~~~~~~~~~ + +.. _extension name: + +``extensionName`` 指的是这个Package所属的mod的名称。 + +一般来说,一个mod(即packages/下面的一个文件夹)只含有一个拓展包,典型的例子就是fk自带的几个拓展包。FreeKill在寻找武将的图片、配音等素材的时候,就会根据这个mod的名字去寻找。 + +在大多数情况下,Package的名字和mod的名字都是一致的(默认情况下也是如此),但有时候一个mod可能会含有好几个拓展包,比如神话再临mod里面就含有不少拓展包,这时候就要手动把extensionName设为mod的名字。以下是定义风包的代码: + +.. highlight:: lua + +:: + + local extension = Package:new("wind") + extension.extensionName = "shzl" + +这段代码定义了名为wind的拓展包,但是他所属的mod文件夹名是shzl,所以需要手动指定。 diff --git a/docs/api/core/player.rst b/docs/api/core/player.rst new file mode 100644 index 00000000..996b7114 --- /dev/null +++ b/docs/api/core/player.rst @@ -0,0 +1,5 @@ +Player +============== + +.. lua:autoclass:: Player + diff --git a/docs/api/core/skill.rst b/docs/api/core/skill.rst new file mode 100644 index 00000000..8ef04e3d --- /dev/null +++ b/docs/api/core/skill.rst @@ -0,0 +1,4 @@ +Skill +============== + +.. lua:autoclass:: Skill diff --git a/docs/api/index.rst b/docs/api/index.rst new file mode 100644 index 00000000..69326107 --- /dev/null +++ b/docs/api/index.rst @@ -0,0 +1,9 @@ +API文档 +============ + +.. toctree:: + :maxdepth: 1 + + core.rst + server.rst + client.rst diff --git a/docs/api/server.rst b/docs/api/server.rst new file mode 100644 index 00000000..fc65c98e --- /dev/null +++ b/docs/api/server.rst @@ -0,0 +1,2 @@ +Server +============ diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..3262ac09 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,80 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'FreeKill' +copyright = '2023, Notify' +author = 'Notify' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.mathjax', + 'sphinx.ext.ifconfig', + 'sphinx.ext.viewcode', + 'sphinx.ext.githubpages', + 'sphinxcontrib.luadomain', + 'sphinx_lua', +] + +lua_source_path = [ + "../lua", + "../packages", +] + +lua_source_encoding = 'utf8' +lua_source_comment_prefix = '---' +lua_source_use_emmy_lua_syntax = True +lua_source_private_prefix = '_' + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'zh_CN' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] diff --git a/doc/dev/ai.md b/docs/dev/ai.rst similarity index 73% rename from doc/dev/ai.md rename to docs/dev/ai.rst index ac318c8f..17c21695 100644 --- a/doc/dev/ai.md +++ b/docs/dev/ai.rst @@ -1,19 +1,18 @@ -# FreeKill 的 AI 系统 +FreeKill 的 AI 系统 +=================== -> [dev](./index.md) > AI - -___ - -## 概述 +概述 +---- 备选算法: -- MCTS -- 神杀算法 +- MCTS +- 神杀算法 -___ +-------------- -## MCTS实现 +MCTS实现 +-------- 实现该算法的最大难点在于如何模拟。 @@ -25,12 +24,17 @@ ___ 1. 首先Room初始化的时候也初始化一个AI用的Room 2. Room内要能够录像,记录所有的request结果和random生成的值。为此可能要自定义一个random函数对自带的math.random进行封装。 -3. 在录像的时候,AI Room也跟着录像的内容进行更新。AI Room本质上也就是一个Room而已,或者可以是Room的子类,反正他的内容就是用这个方式和真Room即时同步的。 +3. 在录像的时候,AI Room也跟着录像的内容进行更新。AI + Room本质上也就是一个Room而已,或者可以是Room的子类,反正他的内容就是用这个方式和真Room即时同步的。 4. 在AI即将处理问题的时候,首先获得所有可行选项。根据算法,需要对某个节点进行randomplay。 5. randomplay的话如果直接用AI Room,那么回溯的时候如何回到先前的状态呢? - 1. 考虑新建一个新的AI Room,然后重放录像以达到开始状态。这样每次randomplay之前都要先回复一下状态,而随着录像的加长这个过程也可能变长,导致AI越来越慢 - 2. 考虑真Room的所有字段全部复制给AI Room一份。但有个问题在于如何把程序控制流和栈也跳转到一样的地方。所以这个是很难实现的。 + + 1. 考虑新建一个新的AI + Room,然后重放录像以达到开始状态。这样每次randomplay之前都要先回复一下状态,而随着录像的加长这个过程也可能变长,导致AI越来越慢 + 2. 考虑真Room的所有字段全部复制给AI + Room一份。但有个问题在于如何把程序控制流和栈也跳转到一样的地方。所以这个是很难实现的。 3. 所以考虑用方案1。为了缓解太慢的情况,可以把1和2结合起来。约定好在某个时间点(比如GameLogic:action中的那个死循环执行)就与Room交换数据,然后这时候复盘录像的起始时间点修改。这样的话为了从randomplay恢复状态,就有必要将此时交换的数据额外保存一份。为了能让Logic平安跑到那个时间点,从人凑齐直到那个时间点的录像也要保存一份。 + 6. 解决了模拟和回溯的问题的话,就可以考虑实现该算法了。 那么为了模拟,首先得实现一个RandomAI才行。 diff --git a/docs/dev/compile.rst b/docs/dev/compile.rst new file mode 100644 index 00000000..9b97fc7f --- /dev/null +++ b/docs/dev/compile.rst @@ -0,0 +1,146 @@ +编译 FreeKill +============= + +全平台通用步骤 +-------------- + +FreeKill采用最新的Qt进行构建,因此需要先安装Qt6的开发环境。 + +无论是Win还是Linux,都建议用\ `Qt官方的下载器 `__\ 进行安装。当然了,在一些软件更新很频繁的Linux发行版里面,可能已经能从包管理器安装Qt6,对此后文细说。这个环节介绍用Qt安装器安装的步骤。 + +Qt安装的流程不赘述。为了编译FreeKill,至少需要安装以下的组件: - Qt 6: +MinGW 11.2.0 64-bit (不支持MSVC) - Qt 6: Qt5 Compat - Qt 6: Shader +Tools (为了使用GraphicalEffects) - Qt 6: Multimedia - +QtCreator(这个是安装器强制要你安装的) - CMake、Ninja - OpenSSL 1.1.1 + +接下来根据平台的不同,步骤也稍有区别。 + +-------------- + +Windows +------- + +从网络上下载swig、flex、bison。swig在其官网可以下载,flex和bison可在\ `github `__\ 或者SourceForge下载。 + +全都下载完成之后,将含有swig.exe、win_flex.exe、win_bison.exe的文件夹全部都设置到Path环境变量里面去。 + +接下来使用QtCreator打开项目,然后尝试编译。 + +这时遇到cmake报错:OpenSSL:Crypto not found. +这是因为我们还没有告诉编译器OpenSSL的位置,点左侧“项目”,查看构建选项,在CMake的Initial +Configuration中,点击添加按钮,新增String型环境变量OPENSSL_ROOT_DIR,将其值设为跟Qt一同安装的OpenSSL的位置(如C:/Qt/Tools/OpenSSL/Win_x64)。然后点下方的Re-configure +with Initial Parameters,这样就能正常编译了。 + +运行的话,在Qt +Creator的项目选项->运行中,先将工作目录改为项目所在的目录(git仓库的目录)。然后先将编译好了的FreeKill.exe放到项目目录中,在目录下打开CMD,执行windeployqt +FreeKill.exe。调整目录下的dll文件直到能运行起来为止,之后就可以在Qt +Creator中正常运行和调试了。 + +-------------- + +Linux +----- + +通过包管理器安装一些额外软件包方可编译。 + +Debian一家子: + +.. code:: sh + + $ sudo apt install liblua5.4-dev libsqlite3-dev libssl-dev swig flex bison + +Arch Linux: + +.. code:: sh + + $ sudo pacman -Sy lua sqlite swig openssl flex bison + +然后使用配置好的QtCreator环境即可编译。 + +如果你不想用Qt安装器的话,可以用包管理器安装依赖,下面仅举例Arch: + +.. code:: sh + + $ sudo pacman -S qt6-base qt6-declarative qt6-5compat qt6-multimedia + $ sudo pacman -S cmake lua sqlite swig openssl swig flex bison + +然后可以用命令行编译: + +.. code:: sh + + $ mkdir build && cd build + $ cmake .. + $ make -j8 + +-------------- + +Linux服务器 +----------- + +一般来说Linux服务器的包管理器都没新到提供Qt6下载,这个时候想编译服务端的话,需要在尽可能安装完Qt5环境的情况下,对FreeKill的Qt版本降一下等级。 + +首先将根目录和src下面的两个CMakeLists.txt的Qt6都改成Qt5,然后试图进行编译。 + +编译器会报告大概不超过10处错误,将它们修改成Qt5可以接受的形式就行了。 + +-------------- + +MacOS +----- + +大致与Windows类似,但尚且缺少确切的方案。 + +-------------- + +编译安卓版 +---------- + +用Qt安装器装好Android库,然后配置一下android-sdk就能编译了。 + +(Qt +6.4的刘海屏bug,手动往QActivity.java的onCreate函数追加如下代码即可实现完全全屏。这里做个笔记方便复制粘贴,等Qt修了再说) + +.. code:: java + + getWindow().addFlags(LayoutParams.FLAG_FULLSCREEN); + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { + getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + } + if (Build.VERSION.SDK_INT > 28) { + WindowManager.LayoutParams lp = getWindow().getAttributes(); + lp.layoutInDisplayCutoutMode = LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + getWindow().setAttributes(lp); + } + +-------------- + +WASM下编译 +---------- + +WASM大概就是能在浏览器中跑C++。编译用Qt Creator即可。 + +1. 条件与局限性 +~~~~~~~~~~~~~~~ + +如果程序运行在网页上的话,那么理应只有客户端,然后提供网页的服务器上自然也运行着一个后端服务器。所以说在编译时应该舍弃掉服务端相关的代码。因此依赖库就不再需要sqlite3。 + +总之是编译个纯客户端的FK。 + +2. 编译OpenSSL +~~~~~~~~~~~~~~ + +进入OpenSSL的src目录,然后 + +:: + + $ ./config -no-asm -no-engine -no-dso + $ emmake make -j8 build_generated libssl.a libcrypto.a + +编译Lua的话直接emmake make就行了,总之库已经传到仓库了。 + +3. 部署资源文件 +~~~~~~~~~~~~~~~ + +由于CMake中\ ``file(GLOB_RECURSE)``\ 所带来的缺陷,每当资源文件变动时,需要手动更新。 + +把构建目录中的.rcc目录删掉然后重新执行CMake->make即可。每次编译资源文件总要消耗相当多的时间。 diff --git a/doc/dev/database.md b/docs/dev/database.rst similarity index 52% rename from doc/dev/database.md rename to docs/dev/database.rst index 310b16dc..4cfab213 100644 --- a/doc/dev/database.md +++ b/docs/dev/database.rst @@ -1,15 +1,14 @@ -# FreeKill 的数据库 - -> [dev](./index.md) > 数据库 - -___ +FreeKill 的数据库 +================= FreeKill 使用 sqlite3 数据库。 关于数据库的组织,详见server/init.sql。单纯存个用户名和密码而已 -## 服务端用来管理用户的数据库 +服务端用来管理用户的数据库 +-------------------------- 保存用户名与密码而已。 -## 包管理用的数据库 +包管理用的数据库 +---------------- diff --git a/doc/dev/gameevent.md b/docs/dev/gameevent.rst similarity index 64% rename from doc/dev/gameevent.md rename to docs/dev/gameevent.rst index b4ad91af..d3223acc 100644 --- a/doc/dev/gameevent.md +++ b/docs/dev/gameevent.rst @@ -1,28 +1,29 @@ -# Fk的游戏事件 +Fk的游戏事件 +============ -___ - -在Fk中,“事件”指的大约是像`room:judge`, `room:damage`之类的操作。这些操作一般和某个游戏术语挂钩(“判定”、“伤害”),然后其中包含着一系列操作,比如伤害事件包含了与伤害事件有关的各种触发时机、以及扣减等实际的动作等。 +在Fk中,“事件”指的大约是像\ ``room:judge``, +``room:damage``\ 之类的操作。这些操作一般和某个游戏术语挂钩(“判定”、“伤害”),然后其中包含着一系列操作,比如伤害事件包含了与伤害事件有关的各种触发时机、以及扣减等实际的动作等。 之所以要把事件单独挑出来聊聊,是因为有以下几点需求: -* 事件要能够被半路中止。 -* 对于被中止的事件,要能判断它能不能中止事件栈中的更低层事件。 -* 对于被中止的事件,需要做“垃圾回收”(例如将处于处理区的相关卡牌移动到弃牌堆等等) +- 事件要能够被半路中止。 +- 对于被中止的事件,要能判断它能不能中止事件栈中的更低层事件。 +- 对于被中止的事件,需要做“垃圾回收”(例如将处于处理区的相关卡牌移动到弃牌堆等等) -___ +-------------- -## 对于如何实现的构想 +对于如何实现的构想 +------------------ (施工完成后再来修改这一节) 首先是如何实现事件类。初步构想一下,应该有以下的属性/方法: -* 事件名(也可以是枚举值) -* 事件数据(一个表,内含所有要用到的数据) -* 事件id(在一局游戏中唯一标记某个事件) -* 事件的函数体,也就是具体要做的事情 -* 事件被中止或者正常结束时,用来清理现场的函数 +- 事件名(也可以是枚举值) +- 事件数据(一个表,内含所有要用到的数据) +- 事件id(在一局游戏中唯一标记某个事件) +- 事件的函数体,也就是具体要做的事情 +- 事件被中止或者正常结束时,用来清理现场的函数 问题来了,既然事件本质上还是个函数体,那要怎么才能中止呢? @@ -30,104 +31,113 @@ ___ 事件必然会发生嵌套。所以对此更要慎重考虑。 -现在只是设想!假设以`room:judge`入手: +现在只是设想!假设以\ ``room:judge``\ 入手: -```lua -function Room:judge(judgeStruct) - local judgeEvent = GameEvent:new(GameEvent.Judge, judgeStruct) - judgeEvent:exec() -end -``` +.. code:: lua + + function Room:judge(judgeStruct) + local judgeEvent = GameEvent:new(GameEvent.Judge, judgeStruct) + judgeEvent:exec() + end 总之可能是这样的吧。exec()就是实际执行事件,可能如下: -```lua -function GameEvent:exec() - local event_f = self.event_f - local co = coroutine.create(event_f) - while true do - local yield_result = coroutine.resume(co) - if yield_result == "__handleRequest" then - -- 正常yield掉,如此层层yield最后到Room的主循环,然后进requestLoop协程处理事务 - -- 最后返回这里继续resume - coroutine.yield(yield_result) - else - -- 事件被中止,考虑做点什么 - break - end - end -end -``` +.. code:: lua + + function GameEvent:exec() + local event_f = self.event_f + local co = coroutine.create(event_f) + while true do + local yield_result = coroutine.resume(co) + if yield_result == "__handleRequest" then + -- 正常yield掉,如此层层yield最后到Room的主循环,然后进requestLoop协程处理事务 + -- 最后返回这里继续resume + coroutine.yield(yield_result) + else + -- 事件被中止,考虑做点什么 + break + end + end + end 现在考虑嵌套的情况,event1和event2嵌套,也就是event1的体里面又创建了event2并exec,此时的协程调用关系如下: - RoomLogic -> event1 -> event2 | RequestLoop +:: + + RoomLogic -> event1 -> event2 | RequestLoop 此时event2中调用了某个耗时的函数,比如room:delay或者各种request,这时候就触发了yield。然后在上面的函数中就获取了yield返回值,然后判断是正常yield后,就进一步yield,此时协程到了event1中。event1继续yield,于是到了Room主协程,主协程其实也调用了exec,所以被yield切回到真正的主线程,然后执行requestloop的协程。乍一看似乎没问题,除了这个跳跃链有够长的。 -这种开销看起来应该不大吧,而且在AI Random Play这种非常需要性能的场所也根本不会发生这种类型的yield(画饼),总之先不考虑这块。 +这种开销看起来应该不大吧,而且在AI Random +Play这种非常需要性能的场所也根本不会发生这种类型的yield(画饼),总之先不考虑这块。 -如果事件被中止(按目前的实现来说就是在特定的时机return true了),那么事件的本体也应该调用yield(这就如同在目前--v0.0.1实现中,那些函数常见的遇到true就直接返回了一样),这样相当于从函数返回了。这种yield就会进入那个else分支,进行这个事件类的有关清扫工作。 +如果事件被中止(按目前的实现来说就是在特定的时机return +true了),那么事件的本体也应该调用yield(这就如同在目前–v0.0.1实现中,那些函数常见的遇到true就直接返回了一样),这样相当于从函数返回了。这种yield就会进入那个else分支,进行这个事件类的有关清扫工作。 -___ +-------------- -## 构想2 类似无懈可击的事件 +构想2 类似无懈可击的事件 +------------------------ 考虑一类特殊的事件:“取消其他事件的事件”。它和普通事件一样,能被中止之类的,而它的作用在于取消掉其他事件。 接着上面的else分支继续考虑: -```lua - else - local cancelEvent = GameEvent:new(GameEvent.CancelEvent, self) - local ret = cancelEvent:exec() - if ret then break end -``` +.. code:: lua + + else + local cancelEvent = GameEvent:new(GameEvent.CancelEvent, self) + local ret = cancelEvent:exec() + if ret then break end 似乎也没什么非常特殊的内容啊。 exec()的返回值哪里来?这好像真的是个问题呢。可以考虑返回布尔值表示事件是否中止了?或者更详细的,返回一个状态码,毕竟本质上身为协程自然能有协程该有的种种状态。这里只是初步考虑而已,就考虑前者好了。 -___ +-------------- -## 落实 - 手杀皇甫嵩 +落实 - 手杀皇甫嵩 +----------------- -手杀皇甫嵩是重构整个事件体系的罪魁祸首(雾)。其技能为:若blah blah,你可以终止本次判定,然后blah blah。 +手杀皇甫嵩是重构整个事件体系的罪魁祸首(雾)。其技能为:若blah +blah,你可以终止本次判定,然后blah blah。 而终止本次判定是目前的体系做不到的。 考虑如下技能片段: -```lua - on_effect = function(xxx) - local judge = {} - room:judge(judge) - if judge.card.number > 5 then xxx end - end -``` +.. code:: lua + + on_effect = function(xxx) + local judge = {} + room:judge(judge) + if judge.card.number > 5 then xxx end + end 皇甫嵩能终止判定,就算他在fk.Judge时机返回true算了。前文已经考虑过judge了,他创建了新event并执行之。而今judge事件遭到打断,room:judge可能可以返回一个返回值来告诉玩家已经被中断之类的。但是Luaer,特别是像我这样的Luaer,懒得考虑事件的合法性之类的,而既然judge已经被终止,那么judge.card就不应该被使用才行。 -为此可以为judge表添加__index元方法,当对key="card"进行取值时,就直接yield掉,除此之外的就rawget。 +为此可以为judge表添加\__index元方法,当对key=“card”进行取值时,就直接yield掉,除此之外的就rawget。 还有更复杂的情况呢。当皇甫嵩判乐的时候,如果是黑桃,那么他发动技能终止了判定,然后像个无事人一样出牌呢。乐都还贴在他头上。考察一下Fk里面的乐是怎么写的,哦,原来是on_effect的末尾才移走啊,那没事了。也就是说,如果对judge.card的非法访问使得事件被中止了,那么照这个逻辑,乐是下不来的,符合手刹了这下。 -___ +-------------- -## 考虑 事件为何中止? +考虑 事件为何中止? +------------------- 事件是协程,因此协程中止的方法就是事件中止的方法。有这两种: -* yield, 落实到Fk就是触发技的各种返回true -* error, 这不就是我经常发生的事情吗 +- yield, 落实到Fk就是触发技的各种返回true +- error, 这不就是我经常发生的事情吗 前面也提到过发生yield的时候会有cancelEvent产生,方便玩家反悔中止这次事件,但因为error而中断事件是无法恢复的。试图resume一个报错的协程的话,他会立刻因为error而自动yield。这个可以在exec函数里面多加考虑,如果resume函数返回了true和特定值,那就是正常情况。否则就是报错,输出错误信息并返回。 那前文那个judge.card怎么办呢?这种严格来说得算在error的范畴,因为不是人为中止本次effect的。但是error的话势必要输出到屏幕,而我个人聚德直接拿judge.card算是合法行为。这种情况或许可以定一个约定好的特殊错误信息,在处理错误的时候如果是这个错误的话就不输出。 -___ +-------------- -## 考虑 有哪些事件 +考虑 有哪些事件 +--------------- 在最开始的时候,“依赖关系”这个现象的存在使得触发技多了个on_cost(消耗),但是现在on_cost已经成为界定skill是否发动了的标准。而在skill的effect环节,依然存在着一环扣一环的关系,比如前面举的room:judge例子。 @@ -138,23 +148,27 @@ ___ 总之,事件不止room.lua里面那些。就拿前面的考虑来说,由于要中断on_effect,所以on_effect肯定会算成一个事件,可能叫SkillEffect事件吧。 再考虑万恶之源武将——老朱然,直接结束你的回合。(他只要回合内造成了伤害就能结束回合,但没说在谁的回合造成了伤害)所以进行回合也理应算是个事件。 -___ +-------------- -## 考虑 老朱然 +考虑 老朱然 +----------- 对于老朱然这种人而言,他想要杀掉的是回合事件,而能发动这个技能的时候,事件栈想必已经很深了,稍微模拟一下这个情景:老朱然杀界徐盛并打掉他一滴血,此时事件栈大概如下(还没正式设计各种事件,所以可能不妥): -* 伤害事件 - room:damage - 询问技能:是否发动胆守,点确定 -* 技能生效事件 - activeskill:onEffect - 【杀】的effect -* 使用牌事件 - room:useCard - 出杀 -* 进行阶段事件 - ? - 在出牌阶段 -* 回合事件 - ? - 在回合 +- 伤害事件 - room:damage - 询问技能:是否发动胆守,点确定 +- 技能生效事件 - activeskill:onEffect - 【杀】的effect +- 使用牌事件 - room:useCard - 出杀 +- 进行阶段事件 - ? - 在出牌阶段 +- 回合事件 - ? - 在回合 我们的限制条件:无法获得room:damage的返回值,或者说根本没想去获得,其他同理。 coroutine.yield的功能也只有挂起协程并让相应的resume调用返回而已,那么该怎么办呢?由于以上种种限制的存在(主要还是想把Luaer惯着),我们不能对杀的onEffect下手,其他函数都是核心函数,改改也无妨咯。 -还是结合情景考虑吧。胆守点了确定,此时最直接的感受应该是return true。但是return true的意思是防止伤害(都已经是“造成伤害后”了,怎么防止哦,return true也不会有人管你的),所以这里要另辟蹊径。考虑直接yield:此时会处于DamageEvent的exec()中,也就是处于room:damage中,他在处理中止信息。正常的中止的话,会使用break跳出循环;那么如果我访问调用栈,直接让他一路yield到我们想要的那个事件,如同yield到requestLoop那样呢? +还是结合情景考虑吧。胆守点了确定,此时最直接的感受应该是return +true。但是return +true的意思是防止伤害(都已经是“造成伤害后”了,怎么防止哦,return +true也不会有人管你的),所以这里要另辟蹊径。考虑直接yield:此时会处于DamageEvent的exec()中,也就是处于room:damage中,他在处理中止信息。正常的中止的话,会使用break跳出循环;那么如果我访问调用栈,直接让他一路yield到我们想要的那个事件,如同yield到requestLoop那样呢? 没错,访问事件栈确实是个解决办法的可能方案。(这时候用id指示事件的重要性就出来了,可以传一个id表示事件,不过话说回来传那个事件本身也没有任何关系就是了咯)如果yield函数返回了一个GameEvent类的实例,那么就在处理环节将其和self进行比较,如果不同,就继续yield,直到退到相应的事件中。 @@ -162,9 +176,10 @@ coroutine.yield的功能也只有挂起协程并让相应的resume调用返回 总之这不考虑如何防止这种直接结束回合了,毕竟这种不断yield的方式无法用事件进行描述。 -___ +-------------- -## 考虑 内存泄漏的应对 +考虑 内存泄漏的应对 +------------------- 首先声明,Lua没有内存泄漏。但是如果有些东西用户不想要,但是又不告诉lua的话,lua就会觉得用户想要,然后一直保存着它,这在某种意义上也相当于内存泄漏了。拿实例来说,如果事件被中止了,那么在很多情况下确实就不需要了,但Lua会认为协程是挂起的,用户可能想要恢复,于是一直保存着。 diff --git a/doc/dev/gamelogic.md b/docs/dev/gamelogic.rst similarity index 56% rename from doc/dev/gamelogic.md rename to docs/dev/gamelogic.rst index 19f57339..5d950c0a 100644 --- a/doc/dev/gamelogic.md +++ b/docs/dev/gamelogic.rst @@ -1,52 +1,52 @@ -# 游戏逻辑 +游戏逻辑 +======== -> [dev](./index.md) > 游戏逻辑 - -___ - -## 概述 +概述 +---- FreeKill的游戏相关处理逻辑完全使用lua实现。在服务端上,每个Room都有自己的lua_State,并且只会在Room线程启动后才会去调用lua函数进行游戏逻辑处理。 本文档将简要介绍几个最为复杂的逻辑实现。 -___ +-------------- -## 触发技 +触发技 +------ 在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 -``` +.. code:: lua -具体的`fk.CreateTriggerSkill`函数接受一个类型为如上所述的TriggerSkillSpec形式的表。这个表中的属性一共有一下这些: + ---@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 -- 所有技能通用的`name`、`anim_type`、`mute`。其中name为必需项。 -- global: 是否是全局技能。 -- events: 技能的所有触发时机 -- can_trigger: 技能能否被触发 -- on_trigger: 技能触发时具体的行为 -- on_cost: 技能如何执行消耗 -- on_use: 技能被发动后,具体的生效内容 -- priority: 技能的优先级。在同一时机有多个技能能够被触发时,先触发优先级高的。 +具体的\ ``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被创建时,首先会将全局触发技都加入到表中;然后,在游戏中每当有角色获得了一个触发技,就将这个技能加入到表中直到游戏结束。 +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函数返回,否则就这样一直遍历完所有玩家为止。 @@ -55,62 +55,62 @@ refresh等一系列函数与前面同理,下面会对其展开细说。 这部分相关的代码位于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 +.. code:: lua -function TriggerSkill:trigger(event, target, player, data) - return self:doCost(event, target, player, data) -end -``` + 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 -``` +.. code:: 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, -} -``` +.. code:: 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,也就是技能的生效部分,才会正式执行改判这一动作。 @@ -118,25 +118,26 @@ on_trigger在非常多情况下仅仅只是简单的执行一下doCost而已, 在有些时候,只是想在特定的时机执行一些代码,而不想进行询问和发动技能流程时,可以使用on_refresh执行。在refresh的情况下,代码仅仅只是执行了一次,不会做出发动技能之类的动作、 -___ +-------------- -## 移动牌 +移动牌 +------ -移动牌的核心函数是`Room:moveCards(...)`。这是个变长参数函数,根据Emmy注解可知所有的参数都应该是CardsMoveInfo类型。CardsMoveInfo在[system_enum.lua](../../lua/server/system_enum.lua)里面有类型注解,来看看: +移动牌的核心函数是\ ``Room:moveCards(...)``\ 。这是个变长参数函数,根据Emmy注解可知所有的参数都应该是CardsMoveInfo类型。CardsMoveInfo在\ `system_enum.lua <../../lua/server/system_enum.lua>`__\ 里面有类型注解,来看看: -```lua ----@class CardsMoveInfo ----@field ids integer[] ----@field from integer|null ----@field to integer|null ----@field toArea CardArea ----@field moveReason CardMoveReason ----@field proposer integer ----@field skillName string|null ----@field moveVisible boolean|null ----@field specialName string|null ----@field specialVisible boolean|null -``` +.. code:: lua + + ---@class CardsMoveInfo + ---@field ids integer[] + ---@field from integer|null + ---@field to integer|null + ---@field toArea CardArea + ---@field moveReason CardMoveReason + ---@field proposer integer + ---@field skillName string|null + ---@field moveVisible boolean|null + ---@field specialName string|null + ---@field specialVisible boolean|null moveCards函数的第一步是将参数中所有的moveInfo都转化为CardsMoveStruct。CardsMoveStruct与CardsMoveInfo几乎没有区别,除了它将每一张牌都单独划分出了一个moveinfo之外。这么做是为了在同时移动来源不同的牌的时候,让牌能该明牌明牌,该暗牌暗牌。 @@ -146,10 +147,11 @@ moveCards函数的第一步是将参数中所有的moveInfo都转化为CardsMove 然后,对所有的CardsMoveStruct进行遍历,根据move.from和move.fromArea获取这张牌的id实际所在的数组,然后将这个id移动到目标数组中。如此就在服务端的数据层面移动了一张牌。移牌OK后,Room会更新这张牌的位置信息,然后视情况更新这张牌的锁定视为技信息。如果是装备牌的话,那么就做一些跟装备技能有关的事情。 -___ +-------------- -## 使用牌 +使用牌 +------ 使用一张牌应该是全游戏最复杂而又最常见的一种事件了。说他复杂,其实也是被狗卡各种乱七八糟的技能和规则搞得很复杂的。 -使用牌的核心函数是`Room:useCard`,接收的参数是CardUseStruct。不行太复杂了,过一阵子再来看吧。 +使用牌的核心函数是\ ``Room:useCard``\ ,接收的参数是CardUseStruct。不行太复杂了,过一阵子再来看吧。 diff --git a/docs/dev/index.rst b/docs/dev/index.rst new file mode 100644 index 00000000..edb2ac51 --- /dev/null +++ b/docs/dev/index.rst @@ -0,0 +1,16 @@ +Dev文档 +============ + +.. toctree:: + :maxdepth: 1 + + ai.rst + compile.rst + database.rst + gameevent.rst + gamelogic.rst + package.rst + protocol.rst + scenario.rst + todo.rst + ui.rst diff --git a/doc/dev/package.md b/docs/dev/package.rst similarity index 80% rename from doc/dev/package.md rename to docs/dev/package.rst index 6ae87739..94371e8c 100644 --- a/doc/dev/package.md +++ b/docs/dev/package.rst @@ -1,30 +1,32 @@ -# FreeKill 的包管理策略 - -> [dev](./index.md) > 包管理 - -___ +FreeKill 的包管理策略 +===================== FreeKill使用git进行包管理,具体而言是使用libgit2库进行管理。 -## 包的组织 +包的组织 +-------- -所有拓展包都位于packages/目录下。其中,standard(标包)、standard_cards(标包卡牌)和manuvering_cards(军争卡牌, TODO )属于基础拓展,其直接处于FreeKill的项目仓库之下。其他所有的拓展均处于项目之外。 +所有拓展包都位于packages/目录下。其中,standard(标包)、standard_cards(标包卡牌)和manuvering_cards(军争卡牌, +TODO +)属于基础拓展,其直接处于FreeKill的项目仓库之下。其他所有的拓展均处于项目之外。 每个拓展包都是一个单独的文件夹,内含代码文件(init.lua,以及诸如其他lua文件和fkp文件等等)和音图等资源文件。关于具体如何组织各种文件,请参照已有的拓展包。 -## 包的管理 +包的管理 +-------- 包管理使用git,以下使用类似git命令行的方式解说。 首先,packages中除了基本的三个包之外,其他的包都要从仓库中排除掉。这方面由一个.gitignore文件控制。 -然后,在packages目录下,有一个名为packages.db的文件统领所有拓展包。这是个sqlite数据库,结构详见[数据库](./database.md)。 +然后,在packages目录下,有一个名为packages.db的文件统领所有拓展包。这是个sqlite数据库,结构详见\ :doc:`./database`\ 。 下面从连接过程中简要分析这个文件的作用: 1. 当一个客户端尝试对服务端发起连接请求的时候,首先它们之间会先比较MD5值。 2. 如果MD5通过则无事发生,否则服务端会把自己的packages.db中的关键信息发送给客户端。 -3. 客户端根据文件内容检查自己的拓展包。如果那个文件夹存在,那么就git fetch -> git checkout \。 +3. 客户端根据文件内容检查自己的拓展包。如果那个文件夹存在,那么就git + fetch -> git checkout 。 4. 如果文件夹不存在,那么先git clone,然后再checkout。 5. 做完这些后,客户端再次发起请求。若仍不通过,则向用户通知错误信息。 @@ -32,20 +34,24 @@ FreeKill使用git进行包管理,具体而言是使用libgit2库进行管理 有时候客户端会包含服务端所没有的拓展包,这时候比起直接删除之,更加明智的选择是将其标记为禁用。将拓展包文件夹的名字设为xxx.disabled即可将拓展包标记为禁用的拓展包。禁用的拓展包不会被游戏加载,也不会被MD5检测计入。 -## 包的托管 +包的托管 +-------- 一般来说都是推荐将项目放在github上面的,但由于FreeKill暂且不考虑国际化且必须照顾广大玩家的体验,因此将拓展包托管到github可能不是一个明智的选择。推荐将拓展包托管到gitee平台,或者其他的好办法也行。 总之有一点要注意的是,packages.db中的url需要是国内访问比较方便的网站才行。 -## 包的部署 +包的部署 +-------- 此处不讨论具体如何编写代码,单论在这个管理框架下如何进行开发。 -一般来说,在对一个仓库进行开发时,由于目前各托管平台都用SSH Key认证而非用户名密码,因此仓库的URL通常为git@gitxxx.com:xxxx/xxxx.git。这样的URL有一个问题就在于只有认证过的用户可以clone,而非所有人。 +一般来说,在对一个仓库进行开发时,由于目前各托管平台都用SSH +Key认证而非用户名密码,因此仓库的URL通常为git@gitxxx.com:xxxx/xxxx.git。这样的URL有一个问题就在于只有认证过的用户可以clone,而非所有人。 因此在部署的时候,一定要保证所有url都是https://xxxx。这一点FreeKill是不会进行检测的。 -## 包的下载与更新(TODO) +包的下载与更新(TODO) +---------------------- 客户端使用GUI,服务端使用Fk shell或者直接编辑packages.db。 diff --git a/doc/dev/protocol.md b/docs/dev/protocol.rst similarity index 68% rename from doc/dev/protocol.md rename to docs/dev/protocol.rst index 72604777..64359625 100644 --- a/doc/dev/protocol.md +++ b/docs/dev/protocol.rst @@ -1,49 +1,49 @@ -# FreeKill 的通信 +FreeKill 的通信 +=============== -> [dev](./index.md) > 通信 - -___ - -## 概述 +概述 +---- FreeKill使用UTF-8文本进行通信。基本的通信格式为JSON数组: -`[requestId, packetType, command, jsonData]` +``[requestId, packetType, command, jsonData]`` 其中: -- requestId用来在request型通信使用,用来确保收到的回复和发出的请求相对应。 -- packetType用来确定这条消息的类型以及发送的目的地。 -- command用来表示消息的类型。使用首字母大写的驼峰式命名,因为下划线命名会造成额外的网络开销。 -- jsonData保存着这个消息的额外信息,必须是一个JSON数组。数组中的具体内容详见源码及注释。 +- requestId用来在request型通信使用,用来确保收到的回复和发出的请求相对应。 +- packetType用来确定这条消息的类型以及发送的目的地。 +- command用来表示消息的类型。使用首字母大写的驼峰式命名,因为下划线命名会造成额外的网络开销。 +- jsonData保存着这个消息的额外信息,必须是一个JSON数组。数组中的具体内容详见源码及注释。 FreeKill通信有三大类型:请求(Request)、回复(Reply)和通知(Notification)。 -___ +-------------- -## 从连接上到进入大厅 +从连接上到进入大厅 +------------------ 想要启动服务器,需要通过命令行终端: -```sh -$ ./FreeKill -s -``` +.. code:: sh -``是服务器运行的端口号,如果不带任何参数则启动GUI界面,在GUI界面里面只能加入服务器或者单机游戏。 + $ ./FreeKill -s + +````\ 是服务器运行的端口号,如果不带任何参数则启动GUI界面,在GUI界面里面只能加入服务器或者单机游戏。 服务器以TCP方式监听。在默认情况下(比如单机启动),服务器的端口号是9527。 每当任何一个客户端连接上了之后,游戏会先进行以下流程: -1. 检查IP是否被封禁。 // TODO: 数据库 +1. 检查IP是否被封禁。 // TODO: 数据库 2. 服务端将RSA公钥发给客户端,然后检查客户端的延迟是否小于30秒。 3. 在网络检测环节,若客户端网速达标的话,客户端应该会发回一个字符串。这个字符串保存着用户的用户名和RSA公钥加密后的密码,服务端检查这个字符串是否合法。如果合法,检查密码是否正确。 4. 上述检查都通过后,重连(TODO:) -5. 不要重连的话,服务端便为新连接新建一个`ServerPlayer`对象,并将其添加到大厅中。 +5. 不要重连的话,服务端便为新连接新建一个\ ``ServerPlayer``\ 对象,并将其添加到大厅中。 -___ +-------------- -## 大厅和房间 +大厅和房间 +---------- 大厅(Lobby)是一个比较特殊的房间。除了大厅之外,所有的房间都被作为游戏房间对待。 @@ -58,24 +58,27 @@ ___ 1. 只要有玩家进入,就刷新一次房间列表。 2. 只要玩家变动,就更新大厅内人数(TODO:) -> 因为上述特点都是通过信号槽实现的,通过阅读代码不易发现,故记录之。 +.. -___ + 因为上述特点都是通过信号槽实现的,通过阅读代码不易发现,故记录之。 -## 对游戏内交互的实例分析 +-------------- + +对游戏内交互的实例分析 +---------------------- 下面围绕着askForSkillInvoke对游戏内的交互进行简析,其他交互也是一样的原理。 -```lua -function Room:askForSkillInvoke(player, skill_name, data) - local command = "AskForSkillInvoke" - self:notifyMoveFocus(player, skill_name) - local invoked = false - local result = self:doRequest(player, command, skill_name) - if result ~= "" then invoked = true end - return invoked -end -``` +.. code:: lua + + function Room:askForSkillInvoke(player, skill_name, data) + local command = "AskForSkillInvoke" + self:notifyMoveFocus(player, skill_name) + local invoked = false + local result = self:doRequest(player, command, skill_name) + if result ~= "" then invoked = true end + return invoked + end 在这期间,一共涉及两步走: @@ -84,29 +87,29 @@ end 首先看第一步:通知。这里涉及的函数是doNotify。(调查notifyMoveFocus的代码即可知道) -调查`ServerPlayer:doNotify`发现: +调查\ ``ServerPlayer:doNotify``\ 发现: -```lua - self.serverplayer:doNotify(command, jsonData) -``` +.. code:: lua + + self.serverplayer:doNotify(command, jsonData) 这里的self.serverplayer,其实指的是C++中的ServerPlayer实例,因此这一行代码实际上调用的是C++中的ServerPlayer::doNotify。调查C++中对应的函数,发现实际上调用了Router::notify,调查Router::notify,发现发送了一个信号量,调查Router::setSocket发现这个信号量连接到了ClientSocket::send。调查ClientSocket::send后发现: -```cpp -void ClientSocket::send(const QByteArray &msg) -{ - if (msg.length() >= 1024) { - auto comp = qCompress(msg); - auto _msg = "Compressed" + comp.toBase64() + "\n"; - socket->write(_msg); - socket->flush(); - } - socket->write(msg); - if (!msg.endsWith("\n")) - socket->write("\n"); - socket->flush(); -} -``` +.. code:: cpp + + void ClientSocket::send(const QByteArray &msg) + { + if (msg.length() >= 1024) { + auto comp = qCompress(msg); + auto _msg = "Compressed" + comp.toBase64() + "\n"; + socket->write(_msg); + socket->flush(); + } + socket->write(msg); + if (!msg.endsWith("\n")) + socket->write("\n"); + socket->flush(); + } 核心在于socket->write,这里其实就调用了QTcpSocket::write,正式向网络中发送数据。从前面的分析也慢慢可以发现,发送的其实就是json字符串。 @@ -116,55 +119,56 @@ void ClientSocket::send(const QByteArray &msg) 其中有这样的一段: -```cpp - if (type & TYPE_NOTIFICATION) { - if (type & DEST_CLIENT) { - ClientInstance->callLua(command, jsonData); - } -``` +.. code:: cpp + + if (type & TYPE_NOTIFICATION) { + if (type & DEST_CLIENT) { + ClientInstance->callLua(command, jsonData); + } 调用了ClientInstance::callLua函数,这个函数不做详细追究,只要知道他调用了这个lua函数即可: -```lua - self.client.callback = function(_self, command, jsonData) - local cb = fk.client_callback[command] - if (type(cb) == "function") then - cb(jsonData) - else - self:notifyUI(command, jsonData); - end - end -``` +.. code:: lua + + self.client.callback = function(_self, command, jsonData) + local cb = fk.client_callback[command] + if (type(cb) == "function") then + cb(jsonData) + else + self:notifyUI(command, jsonData); + end + end 至此,我们已经可以基本得出结论:Client在接收到信息时就根据信息的command类型调用相应的函数,若无则直接调用qml中的函数。 -接下来聊聊doRequest。和前面类似,doRequest最终也是向玩家发送了一个JSON字符串,但是然后它就进入了等待回复的状态。在此期间,可以使用waitForReply函数尝试获取对方的reply,若无则得到默认结果__notready,然后在Lua侧进行进一步处理。 +接下来聊聊doRequest。和前面类似,doRequest最终也是向玩家发送了一个JSON字符串,但是然后它就进入了等待回复的状态。在此期间,可以使用waitForReply函数尝试获取对方的reply,若无则得到默认结果\__notready,然后在Lua侧进行进一步处理。 客户在收到request类型的消息后,可以用reply对服务端进行答复。reply本身也是JSON字符串,服务端在handlePacket环节发觉这个是reply后,就知道自己已经收到回复了。这时用waitForReply即可得到正确的回复结果。 在Lua侧,对waitForReply其实有所封装: -```lua - while true do - result = player.serverplayer:waitForReply(0) - if result ~= "__notready" then - return result - end - local rest = timeout * 1000 - (os.getms() - start) / 1000 - if timeout and rest <= 0 then - return "" - end - coroutine.yield(rest) - end -``` +.. code:: lua + + while true do + result = player.serverplayer:waitForReply(0) + if result ~= "__notready" then + return result + end + local rest = timeout * 1000 - (os.getms() - start) / 1000 + if timeout and rest <= 0 then + return "" + end + coroutine.yield(rest) + end 这里就是一个死循环,不断的试图读取玩家的回复,直到超时为止。因为waitForReply指定的等待时间为0,所以会立刻返回(这也是为什么waitForReply在读取reply时需要加锁的原因,因为读取操作很频繁),此时若lua发现玩家并未给出答复,就会调用coroutine.yield切换到其他线程去做点别的事情(比如处理旁观请求,调用QThread::msleep睡眠一阵子等等),别的协程办完事情后再次切换回这个协程(yield函数返回),然后开启新一轮循环,如此往复直到等待时间耗尽或者收到了回复。 -___ +-------------- -## 对掉线的处理 +对掉线的处理 +------------ -因为每个连接都对应着一个`new ClientSocket`和`new ServerPlayer`,所以对于掉线的处理要慎重,处理不当会导致内存泄漏以及各种奇怪的错误。 +因为每个连接都对应着一个\ ``new ClientSocket``\ 和\ ``new ServerPlayer``\ ,所以对于掉线的处理要慎重,处理不当会导致内存泄漏以及各种奇怪的错误。 一般来说掉线有以下几种情况: @@ -173,25 +177,26 @@ ___ 3. 在未开始游戏的房间里面掉线。 4. 在已开始游戏的房间里掉线。 -首先对所有的这些情况,都应该把ClientSocket释放掉。这部分代码写在[server_socket.cpp](../../src/network/server_socket.cpp)里面。 +首先对所有的这些情况,都应该把ClientSocket释放掉。这部分代码写在\ `server_socket.cpp <../../src/network/server_socket.cpp>`__\ 里面。 对于2、3两种情况,都算是在游戏开始之前的房间中掉线。这种情况下直接从房间中删除这个玩家,并告诉其他玩家一声,然后从服务器玩家列表中也删除那名玩家。但对于情况3,因为从普通房间删除玩家的话,那名玩家会自动进入大厅,所以需要大厅再删除一次玩家。 对于情况4,因为游戏已经开始,所以不能直接删除玩家,需要把玩家的状态设为“离线”并继续游戏。在游戏结束后,若玩家仍未重连,则按情况2、3处理。 -> Note: 这部分处理见于ServerPlayer类的析构函数。 + Note: 这部分处理见于ServerPlayer类的析构函数。 -___ +-------------- -## 断线重连 +断线重连 +-------- 根据用户id找到掉线的那位玩家,将玩家的状态设置为“在线”,并将房间的状态都发送给他即可。 -但是为了[UI不出错](./ui.md#mainStack),依然需要对重连的玩家走一遍进大厅的流程。 +但是为了UI不出错,依然需要对重连的玩家走一遍进大厅的流程。 重连的流程应为: -1. 总之先新建`ServerPlayer`并加到大厅 +1. 总之先新建\ ``ServerPlayer``\ 并加到大厅 2. 在默认的处理流程中,此时会提醒玩家“已经有同名玩家加入”,然后断掉连接。 3. 在这时可以改成:如果这个已经在线的玩家是Offline状态,那么就继续,否则断开。 4. pass之后,走一遍流程,把玩家加到大厅里面先。 @@ -204,10 +209,10 @@ ___ 直接从UI着手: -1. 首先EnterRoom消息,需要**人数**和**操作时长**。 -2. 既然需要人数了,那么就需要**所有玩家**。 +1. 首先EnterRoom消息,需要\ **人数**\ 和\ **操作时长**\ 。 +2. 既然需要人数了,那么就需要\ **所有玩家**\ 。 3. 此外还需要让玩家知道牌堆、弃牌堆、轮数之类的。 -4. 玩家的信息就更多了,武将、身份、血量、id... +4. 玩家的信息就更多了,武将、身份、血量、id… 所以Lua要在某时候让出一段时间,处理重连等其他内容,可能还会处理一下AI。 @@ -215,28 +220,29 @@ ___ 会阻塞住lua代码的函数有: -- ServerPlayer:waitForReplay() -- Room:delay() +- ServerPlayer:waitForReplay() +- Room:delay() 在这里让出主线程,然后调度函数查找目前的请求列表。事实上,整个Room的游戏主流程就是一个协程: -```lua --- room.lua:53 -local co_func = function() - self:run() -end -local co = coroutine.create(co_func) -while not self.game_finished do - local ret, err_msg = coroutine.resume(co) - ... -end -``` +.. code:: lua + + -- room.lua:53 + local co_func = function() + self:run() + end + local co = coroutine.create(co_func) + while not self.game_finished do + local ret, err_msg = coroutine.resume(co) + ... + end 如果在游戏流程中调用yield的话,那么这里的resume会返回true,然后可以带有额外的返回值。不过只要返回true就好了,这时候lua就可以做一些简单的任务。而这个简单的任务其实也可以另外写个协程解决。 -___ +-------------- -## 旁观(TODO) +旁观(TODO) +------------ 因为房间不允许加入比玩家上限的玩家,可以考虑在房间里新建一个列表存储旁观中的玩家。但是这样或许会让某些处理(如掉线)变得复杂化。 diff --git a/doc/dev/scenario.md b/docs/dev/scenario.rst similarity index 77% rename from doc/dev/scenario.md rename to docs/dev/scenario.rst index badfd943..67b59dfc 100644 --- a/doc/dev/scenario.md +++ b/docs/dev/scenario.rst @@ -1,6 +1,5 @@ -# 关于扩展FreeKill玩法的思考 - -___ +关于扩展FreeKill玩法的思考 +========================== 要扩展玩法,大概就这些: @@ -14,14 +13,16 @@ ___ 3. 加载GameRule后根据模式加载特殊规则 4. 开始玩 -___ +-------------- -## 拓展新规 +拓展新规 +-------- 首先就是如何覆盖老规则,这个可以通过设置一个特殊tag -___ +-------------- -## 拓展logic +拓展logic +--------- 从GameLogic继承然后重写有关函数就行 diff --git a/doc/dev/todo.md b/docs/dev/todo.rst similarity index 77% rename from doc/dev/todo.md rename to docs/dev/todo.rst index 60e6c77e..54efe6a3 100644 --- a/doc/dev/todo.md +++ b/docs/dev/todo.rst @@ -1,29 +1,32 @@ -# TODO list - -___ +TODO list +========= 本文档用来记载一些可能会需要实现但暂且无暇的想法。留待日后再做或者同伴帮忙做了。 -## 服务端的包验证 +服务端的包验证 +-------------- 当客户端连接到服务端遇到MD5失败时,考虑: 1. 服务端除了告知失败之外还告知客户端自己的打包属性,即自己启用了哪些包,包的URL和版本等。 2. 客户端根据信息禁用掉不需要的包,下载没有的包,更新启用的包,将需要的包都切换到服务器提供的版本。 -___ +-------------- -## UI主题可拓展 +UI主题可拓展 +------------ 考虑影响一下skin-bank.js,使其根据某个config的不同,将相应的值设为不同的路径。然后确保所有图片资源(关于页面和logo除外)都通过skin-bank.js访问所需图片。 由于所有拓展包都是只能通过init.lua访问,考虑为QmlBackend提供相应的Lua接口使其能够注册新的配置方案。配置文件本身的组织考虑JSON。 -考虑在skin-bank.js中加入更多信息,例如各个组件的x, y, width, height等。因为影响到的都是Image所以设置这些应该是够了。 +考虑在skin-bank.js中加入更多信息,例如各个组件的x, y, width, +height等。因为影响到的都是Image所以设置这些应该是够了。 -___ +-------------- -## 对话框可拓展 +对话框可拓展 +------------ 考虑劫营,严教等含有特殊交互框的例子。 @@ -33,10 +36,12 @@ ___ skin-bank.js的话依然可以用相对位置进行加载,这个理论上应该是不会被影响到。 -___ +-------------- -## 代码简洁化 +代码简洁化 +---------- -目前FK的lua代码中仍有不少地方的代码重用度不高,典型的例子是fk_ex.lua。考虑在这里用local function将重复代码合并一下。 +目前FK的lua代码中仍有不少地方的代码重用度不高,典型的例子是fk_ex.lua。考虑在这里用local +function将重复代码合并一下。 -还有视为技/触发技/主动技这些能够被“发动”的技能(即继承于UsableSkill的基本),它们的技能生效环节都有很多重复,比如播放声音动画和具体生效等。考虑在某处用一个函数总结一下,至于具体生效部分可以包在一个函数里面`function() skill:onEffect(room, effect) end`然后作为参数传递到useskill函数中。如果后面要做SkillUsed之类的时机的话,这方面就更加重要了。 +还有视为技/触发技/主动技这些能够被“发动”的技能(即继承于UsableSkill的基本),它们的技能生效环节都有很多重复,比如播放声音动画和具体生效等。考虑在某处用一个函数总结一下,至于具体生效部分可以包在一个函数里面\ ``function() skill:onEffect(room, effect) end``\ 然后作为参数传递到useskill函数中。如果后面要做SkillUsed之类的时机的话,这方面就更加重要了。 diff --git a/doc/dev/ui.md b/docs/dev/ui.rst similarity index 50% rename from doc/dev/ui.md rename to docs/dev/ui.rst index b4821bfb..4bb23120 100644 --- a/doc/dev/ui.md +++ b/docs/dev/ui.rst @@ -1,86 +1,88 @@ -# FreeKill 的UI +FreeKill 的UI +============= -> [dev](./index.md) > UI +概述 +---- -___ +FreeKill的UI系统使用Qt +Quick开发。UI依赖\ `QmlBackend <../../src/ui/qmlbackend.h>`__\ 调用需要的C++函数。关于这方面也可参考\ `main.cpp <../../src/main.cpp>`__\ 。 -## 概述 - -FreeKill的UI系统使用Qt Quick开发。UI依赖[QmlBackend](../../src/ui/qmlbackend.h)调用需要的C++函数。关于这方面也可参考[main.cpp](../../src/main.cpp)。 - -> Note: 我感觉QmlBackend这种实现方式很尴尬。 + Note: 我感觉QmlBackend这种实现方式很尴尬。 整体UI采用StackView进行页面切换之类的。 -___ +-------------- -## mainStack +mainStack +--------- -mainStack定义于[main.qml](../../qml/main.qml)中。它以堆栈的形式保存着所有的页面,页面在栈中的顺序需要像这样排布: +mainStack定义于\ `main.qml <../../qml/main.qml>`__\ 中。它以堆栈的形式保存着所有的页面,页面在栈中的顺序需要像这样排布: -- (栈底)登录界面,Init.qml -- 大厅,Lobby.qml -- 别的什么页面 +- (栈底)登录界面,Init.qml +- 大厅,Lobby.qml +- 别的什么页面 -___ +-------------- -## config +config +------ Config.qml存储一些客户端需要用到的设置或者即将发送的数据,(TODO) ---- +-------------- -## Room和RoomLogic +Room和RoomLogic +--------------- 这部分是整个UI体系中最复杂的一部分,其中尤以手牌区的操作为甚。下面来整理一下与出牌相关的UI逻辑。 首先要指明一个常用函数: -```cpp - Q_INVOKABLE QString callLuaFunction(const QString &func_name, - QVariantList params); -``` +.. code:: 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: 平常的不活跃状态。在此期间牌都是暗的,不能操作。 +- 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; - } - } -}, +.. code:: js -Transition { - from: "*"; to: "responding" - ScriptAction { - script: { - dashboard.enableCards(responding_card); - dashboard.enableSkills(responding_card); - progress.visible = true; - okCancel.visible = true; - } - } -}, -``` + 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,这里只关心前者。 diff --git a/docs/diy/01-env.rst b/docs/diy/01-env.rst new file mode 100644 index 00000000..30864c2c --- /dev/null +++ b/docs/diy/01-env.rst @@ -0,0 +1,239 @@ +Fk DIY - 环境搭建 +================= + +DIY总览 +------- + +正如项目README所言,FreeKill“试图打造一个最适合diy玩家游玩的民间三国杀”。即便是最开始游戏功能尚未完善,FreeKill也已经具备了对DIY的支持。所有拓展包都列在packages/文件夹下,感兴趣者可以自行查看。 + +欲为FreeKill进行DIY,需要使用的编程语言为Lua。若您对Lua语言完全不熟悉,推荐去\ `菜鸟教程 `__\ 速通一遍基本语法。剩下的就基本是在实践中慢慢领会了。 + +FreeKill本体中自带有标准包和标准卡牌包,可作为DIY时候的例子。事实上,其他DIY包也是像这样子组织的。 + +接下来讲述如何配置环境。 + +-------------- + +环境搭建 +-------- + +Fk +~~ + +Fk是游戏本身,也是拓展包运行的平台。事实上这份文档应该与Fk一同发布的,如果您正在阅读这份文档,那么您理应已经接收到了Fk本身。 + +代码编辑器 +~~~~~~~~~~ + +代码编辑器任选一种即可,但一定要确保以下几点: + +- 至少要是一款\ **代码**\ 编辑器,要有语法高亮功能 +- 需要有EmmyLua插件的支持 +- 需要默认UTF-8格式保存代码文件 + +.. + + EmmyLua是一种特别的Lua注释方式,可以为本来弱类型的Lua语言提供类型支持,这对于像FreeKill这种稍有规模的Lua项目是十分必要的。目前能提供开箱即用的EmmyLua插件编辑器主要有IntelliJ + IDEA和Visual Studio + Code。EmmyLua也能以LSP的方式运行,因此支持LSP的编辑器(这种就多了,比如vim, + sublime)也能符合条件。 + +编辑器的具体安装以及插件配置不在此赘述。 + + 出于易用性和免费的考虑,推荐用VSCode进行拓展。下文将以VSCode为编辑器进行进一步说明。 + +git +~~~ + +git就不必多介绍了吧,这里说说为什么需要配置git。这是因为在Fk中,拓展包拥有在线安装/在线更新的功能,这种功能都是依托于git进行的,因此如果你打算将自己的拓展包发布出去的话,就需要将其创建git仓库,并托管到git托管网站去。 + + 考虑到国内绝大部分人的访问速度,综合国内几家git托管平台,建议使用gitee。 + +大多数人可能从未用过git,并且git上手的门槛并不低,因此以下会对涉及git的操作进行详尽的解说。 + +安装git +^^^^^^^ + +前往\ `官网 `__\ 下载git,下载64-bit +Git for Windows Setup。这样应该会为您下载一个exe安装包。 + +考虑到官网的下载链接实际上指向github,而且可能连官网的都进不去,所以也考虑\ `从清华源下载Git `__\ 。 + +欲验证安装是否完成,可以按下Win+R -> +cmd弹出命令行窗口,输入git命令,如果出来一长串英文说明安装成功了。 + +-------------- + +新增mod +------- + +这只是新增mod的一个例子。当然了,以后有啥要做的实例也会继续用这个拓展包的。 + +首先前往packages下,新建名为fk_study的文件夹。 + +再在fk_study下新建init.lua文件,写入以下内容: + +.. code:: lua + + local extension = Package("fk_study") + + Fk:loadTranslationTable{ + ["fk_study"] = "fk学习包", + } + + return { extension } + +保存退出,打开Fk,进武将一览。你现在应该能在武将一览里面看到“fk学习包”了,但也仅此而已了,毕竟这还只是个空壳包而已。 + +至此我们已经创建了最为简单的mod。mod的文件结构如下: + +:: + + fk_study + └── init.lua + +-------------- + +发布mod +------- + +一种最常见的发布mod方式是把mod打包成zip,发到公共平台上供玩家下载。这种办法虽然可行,但并不是fk推荐的做法。 + + 以下介绍的其实就是新建仓库并推送到gitee的办法,熟悉git者请跳过。 + +下面着重介绍用git发布mod的办法。使用git进行发布的话,就可以让用户体验在线安装、在线更新等便捷之处。 + +以下假设你使用vscode进行代码编辑。你是先用vscode打开了整个FreeKill文件夹,再在其中新建文件夹和文件、然后进行编辑的。 + +菜单栏 -> 终端 -> 新建终端。我们接下来的工作都在终端中完成。 + +将终端切换为Git Bash +~~~~~~~~~~~~~~~~~~~~ + +启动终端后,终端的内容大概是: + +.. code:: + + Mincrosoft Windows 10 [版本号啥的] + xxxxxxxx 保留所有权利。 + + C:\FreeKill> + +这个是Windows自带的cmd,我们不使用这个,而是去用git +bash。此时终端上面应该有这么一条: + +.. code:: + + 问题 输出 调试控制台 _终端_ cmd + v 分屏 删除 + 注意这个加号 + +这时候点击加号右边那个下拉箭头,选择”Git Bash”。这样就成功的切换到了git +bash中,终端看起来应该像这样: + +.. code:: + + xxx@xxxxx MINGW64 /c/FreeKill + $ + +配置ssh key +~~~~~~~~~~~ + +你应该已经注册好了自己的gitee账号。首先在Git +bash中输入这些命令(#号后面的是命令注释,不用照搬;命令开头的$符号是模拟shell的界面,不要输入进去): + +.. code:: bash + + $ cd ~/.ssh + $ ssh-keygen -t rsa -C "你注册用的邮箱地址" # 换成自己真正的邮箱 + # 出来一堆东西,一路点回车就是了 + $ cat id_rsa.pub + # 出来一堆乱七八糟的东西:ssh-rsa <一大堆乱七八糟的内容> <你的邮箱> + $ cd - + +在cat +id_rsa.pub中,出来的那一堆以ssh-rsa的输出,就是这里要用到的“公钥”。然后在gitee中: + +1. 点右上角你的头像,点账号设置 +2. 点左侧栏中 安全设置 - SSH公钥 +3. 此时弹出公钥添加界面,标题任选,下面公钥那一栏中,将刚刚生成的公钥复制粘贴上去 +4. 点确定 + +这样就配置好了ssh公钥。进行验证,在bash中使用命令: + +:: + + $ ssh -T git@gitee.com + Hi xxxx! You've successfully authenticated, but GITEE.COM does not provide shell access. + +输出像Hi +xxx!这样的信息,就说明配置成功了。否则需要进一步检查自己的操作,上网查一下吧。 + +新建git仓库 +~~~~~~~~~~~ + +现在终端的工作目录应该还是FreeKill根目录,我们先切换到mod的目录去,然后再在shell中进行一系列操作。 + +.. code:: sh + + $ cd packages/fk_study + $ git init # 创建新的空仓库 + $ git add . # 将文件夹中所有的文件都加入暂存区 + $ git commit -m "init" # 提交目前所有的文件,这样文件就正式存在于仓库里面了 + 作者身份未知 + *** 请告诉我您是谁。 + 运行 + git config --global user.email "you@example.com" + git config --global user.name "Your Name" + + 来设置您账号的缺省身份标识。如果仅在本仓库设置身份标识,则省略 --global 参数。 + +看来我们初次安装Git,Git还不知道我们的身份呢,不过git已经告诉了配置所需的命令了。运行前一条命令告知自己的名字,运行后一条命令告知自己的邮箱。如此就OK了,然后再commit一次。 + +然后在gitee中也新建一个仓库,取名为fk_study。接下来回到终端里面: + +.. code:: sh + + $ git remote add origin git@gitee.com:xxx/fk_study # 其中这个xxx是你的用户名 + $ git push -u origin master + +OK了,刷新你新建的那个仓库的页面,可以看到里面已经有init.lua了。此时距离发布mod只有最后一步,那就是把仓库设置为开源。请自行在gitee中设置吧。 + +让他人安装并游玩你的mod +~~~~~~~~~~~~~~~~~~~~~~~ + +注意到Fk初始界面里面的“管理拓展包”了不?这个就是让你安装、删除、更新拓展包用的。在那个页面里面有个输入框,在浏览器中复制仓库的地址(比如https://gitee.com/xxx/fk_study/ +),粘贴到输入框,然后单击“从URL安装”即可安装拓展包了。 + +更新mod +~~~~~~~ + +现在mod要发生更新了,更新内容为一个武将。先在init.lua中新增武将吧。 + +.. code:: lua + + local study_sunce = General(extension, "study_sunce", "wu", 4) + Fk:loadTranslationTable{ + ["study_sunce"] = "孙伯符", + } + +保存,此时注意vscode左侧栏变成了: + +:: + + v fk_study + └── init.lua M + +init.lua后面出现了“M”,并且文件名字也变成了黄色,这表示这个文件已经被修改过了,接下来我们把修改文件提交到仓库中: + +.. code:: sh + + $ git add . # 将当前目录下的文件暂存 + $ git commit -m "add general sunce" # 提交更改,提交说明为add general sunce + $ git push # “推”到远端,也就是把本地的更新传给远端 + +不喜欢用命令行的话,也可以用vscode自带的git支持完成这些操作,这里就不赘述了。做完git +push后,实际上就已经完成更新了,可以让大伙点点更新按钮来更新你的新版本了。 + +-------------- + +以上介绍了大致的创建mod以及更新的流程。至于资源文件组织等等杂七杂八的问题,请参考已有的例子拓展包。 diff --git a/docs/diy/02-skilltype.rst b/docs/diy/02-skilltype.rst new file mode 100644 index 00000000..f1a4e2bd --- /dev/null +++ b/docs/diy/02-skilltype.rst @@ -0,0 +1,45 @@ +fk技能类型总览 +============== + +fk的目的是便于三国杀的DIY,而三国杀DIY的核心就是制作各种技能了。 + +fk的技能分为两大类,这两大类又各自细分为更小的分类: + +(关于这部分的源码详见lua/core/skill.lua和lua/core/skill_type下的所有文件) + +- 可使用类技能(UsableSkill) + + - 触发技(TriggerSkill):在满足一定条件时,能够通过被动触发发挥效果的技能 + - 主动技(ActiveSkill):玩家主动发动的技能 + - 视为技(ViewAsSkill):将一张牌当做另一张牌的技能 + +- 状态技(StatusSkill) + + - 距离技(DistanceSkill):影响距离计算的技能 + - 攻击范围技(DistanceSkill):影响攻击范围计算的技能 + - 手牌上限技(MaxCardsSkill):影响手牌上限计算的技能 + - 禁止技(ProhibitSkill):禁止成为卡牌目标的技能 + - 卡牌增强技(TargetModSkill):影响卡牌使用次数上限、目标上限、距离限制等等的技能 + - 锁定视为技(FilterSkill):让一张牌强制视为另一张牌的技能 + +其中,触发技的逻辑最为复杂,但是\ `已经在这里分析过了 <../dev/gamelogic.rst>`__\ ,故不再赘述。 + +主动技和状态技应该不算难,先按下不表。视为技与神杀有所区别,区别如下: + +在神杀中,视为技是否可响应是专门写在enabled_at_response的,fk则不然,看倾国的代码: + +.. code:: lua + + local qingguo = fk.CreateViewAsSkill{ + name = "qingguo", + anim_type = "defensive", + pattern = "jink", + card_filter = function(self, to_select, selected) + -- ... + end, + view_as = function(self, cards) + -- ... + end, + } + +可见并没有编写跟响应时候有关的函数,也没有声明出牌阶段不可用。其中的奥妙就在于pattern中,视为技可以转化的卡牌都应该写在pattern里面,Fk会根据pattern的内容判断技能出牌阶段是否可用、是否能够响应等。 diff --git a/docs/diy/03-events.rst b/docs/diy/03-events.rst new file mode 100644 index 00000000..1307e16e --- /dev/null +++ b/docs/diy/03-events.rst @@ -0,0 +1,14 @@ +fk中的游戏事件 +============== + +在进行DIY时,需要对三国杀的规则有一定了解;在编写技能时,也要熟悉游戏提供的各种事件,他的触发方式、触发时机、相关数据。必须要知道这些才能写出正确的代码。 + +.. toctree:: + :maxdepth: 1 + :caption: 事件列表 + + event/gameflow.rst + event/hp.rst + event/usecard.rst + event/movecard.rst + event/misc.rst diff --git a/docs/diy/03-newgeneral.rst b/docs/diy/03-newgeneral.rst new file mode 100644 index 00000000..89dabad5 --- /dev/null +++ b/docs/diy/03-newgeneral.rst @@ -0,0 +1,199 @@ +创建武将并添加技能 +================== + +欲创建技能,必先有武将;而想要创建武将,先要创建拓展包。拓展包是一个武将的容身之处。 + +之前的fk_study应该都还在吧?我们看看在搭建环境的那一章中,我们所编写的代码: + +.. code:: lua + + local extension = Package("fk_study") + + Fk:loadTranslationTable{ + ["fk_study"] = "fk学习包", + } + + local study_sunce = General(extension, "study_sunce", "wu", 4) + Fk:loadTranslationTable{ + ["study_sunce"] = "孙伯符", + } + + return { extension } + +在这个Lua文件中,我们创建了一个拓展包,并往拓展包中添加了一名武将。 + +-------------- + +创建拓展包 +---------- + +创建拓展包的格式基本是固定的,在Lua文件的第一行写上这样的: + +:: + + local extension = Package("xxxx") + +其中xxxx为拓展包的名字,可以随意填写。然后在Lua的最后一行写上: + +:: + + return { extension } + +这样就能让fk知道这有个拓展包了,于是fk就能读取并将其加载到游戏里面。 + +-------------- + +翻译 +---- + +fk的编程约定之一就是不要在代码中含有中文。需要显示为中文的部分,应该单独写在“翻译表”里面,而在主体代码涉及的字符串应使用英文,或者自定义的变量名。 + +加载翻译表的基本格式为: + +.. code:: lua + + Fk:loadTranslationTable{ + ["源文本"] = "译文", + ...... + } + +像这样就可以插入许多条翻译了。 + +-------------- + +创建武将 +-------- + +创建武将的格式为: + +:: + + local xxx = General(拓展包, 武将名, 势力, 体力值, 体力上限, 性别) + +其中: + +- 拓展包表示武将所在的拓展包,无脑extension完事 +- 武将名是武将的内部名称,不要和别人重复了。如果你在做自己的拓展包的话建议加前缀 +- 势力是武将的势力,目前有这几种:\ ``"wei"``\ ,\ ``"shu"``\ ,\ ``"wu"``\ ,\ ``"qun"``\ ,\ ``"god"``\ ,分别代表魏蜀吴群神 +- 体力值是武将的初始体力值 +- 体力上限是武将的体力上限,可以不写,不写的话默认等于体力值 +- 性别是武将的性别,默认为男性,有以下几种取值可能: + + - ``General.Male``\ :男性 + - ``General.Female``\ :女性 + +-------------- + +为武将添加游戏已有技能 +---------------------- + +fk本身不内置多少技能,但玩家还是可以给武将添加已有的技能,避免重复劳动。 + +比如我们的fk_study包,现在要给白板孙伯符加一个技能“制衡”,那么可以这样写: + +:: + + study_sunce:addSkill("zhiheng") + +这样的一行代码必须在创建武将之后再添加。也就是说,添加之后,Lua文件大概像这样: + +:: + + local study_sunce = General(extension, "study_sunce", "wu", 4) + study_sunce:addSkill("zhiheng") -- 在这里新增 + Fk:loadTranslationTable{ + ["study_sunce"] = "孙伯符", + } + +保存一下,进游戏就能发现多了个技能。 + +.. figure:: https://upload-images.jianshu.io/upload_images/21666547-da0d53b6996941de.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 + :alt: 添加已有技能 + + 添加已有技能 + +-------------- + +为武将制图 +---------- + +此时我们还没有为武将制作图片。没有图片的武将,默认会用貂蝉的剪影作为图片。(其实手刹里面未知武将是周瑜的剪影,不过谁在乎呢) + +总之我们来为武将制图。首先找一张心仪的图片。然后我们要找一个切图的软件,用PS也好,gimp也好,都随意,这里不赘述怎么用软件。 + +fk中,武将的图片应该为250x292分辨率,并且是jpg格式。为了观感舒适,武将的人脸应该位于图片的中上方。 + +.. figure:: https://upload-images.jianshu.io/upload_images/21666547-7b08fd53820d4160.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 + :alt: 使用GIMP切图。我倾向于开5x5参考线,并让人脸位于2行3列的格子里面 + + 使用GIMP切图。我倾向于开5x5参考线,并让人脸位于2行3列的格子里面 + +.. figure:: https://upload-images.jianshu.io/upload_images/21666547-a629150ce8a4eac8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 + :alt: 使用GIMP切图后,将尺寸缩到需要的分辨率 + + 使用GIMP切图后,将尺寸缩到需要的分辨率 + +最后用jpg格式导出图片,图片的名字是武将的内部名称,在这里就是study_sunce。 + +.. figure:: https://upload-images.jianshu.io/upload_images/21666547-7093b57e9cb53118.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 + :alt: 导出JPG + + 导出JPG + +注意了,JPG图片的质量不能拉到100%,不然图片体积会很大,给他人下载你的拓展包带来不便。一般质量为90为好,此时图片大约三四十KB大小。这里图像质量只调了60,这样看起来不至于完全失真,图片的体积也相当较小。 + +至此我们做好了图片,接下来就是把图片放到游戏去。 + +去我们的拓展包文件夹,新建文件夹image,再在里面新建文件夹generals,把图丢进去。这样一来,拓展包的文件结构如下: + +:: + + packages/fk_study + ├── image + │   └── generals + │   └── study_sunce.jpg + └── init.lua + +然后打开游戏就能看到武将的图片了: + +.. figure:: https://upload-images.jianshu.io/upload_images/21666547-faafcd3e899f241b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 + :alt: 效果还不错吧 + + 效果还不错吧 + +-------------- + +为武将制作阵亡语音 +------------------ + +每个武将都有自己的阵亡语音。fk采用mp3格式保存语音。 + +怎么处理mp3音频就不叙述了,可以考虑用audacity这款软件调节mp3的音量、去掉首尾的延迟等等。但是依然需要注意一点——mp3语音的体积不能太大了。为此我的建议是使用格式工厂对mp3文件再进行一次格式转换,将转换后mp3文件的码率设为128kbps,这样一来一句语音差不多就是三四十KB的感觉,而音质却不至于非常模糊。 + +阵亡语音放到拓展包文件夹下的audio/death里面,命名规则是武将的内部名称。如图所示: + +.. figure:: https://upload-images.jianshu.io/upload_images/21666547-1ce5c371b425638e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 + :alt: 阵亡语音的命名,以及存放位置 + + 阵亡语音的命名,以及存放位置 + +-------------- + +更新拓展包 +---------- + +我们也做了这么多了,是时候更新一下了。 + +在我们拓展包文件夹那里右键一下,Git Bash Here,然后: + +.. code:: sh + + $ git add . + $ git commit -m "image and audio for sunce" + $ git push + +至此,就完成了拓展包的更新。其他使用你的拓展包的人此时就能通过fk拓展包管理的“更新拓展包”功能,更新到你所做的这个状态。 + +(我自己在写这一系列的文章的时候,也是确实创建了一个拓展包仓库的。 +https://gitee.com/notify-ctrl/fk_study +如有疑问,可以去查看那个仓库是怎么弄的。) diff --git a/docs/diy/04-newskill.rst b/docs/diy/04-newskill.rst new file mode 100644 index 00000000..2dd35ced --- /dev/null +++ b/docs/diy/04-newskill.rst @@ -0,0 +1,108 @@ +创建新技能 +========== + +fk体量实在太小,只有标包,欲玩到更多技能,还是得自己亲自动手才行。 + +如何快速上手编写技能?答案是——复制粘贴。直接复制已有的技能代码以为己用,并且推敲那段技能代码的原理,无疑是上手的最快方法。 + +本文将实现这样一个技能:“摸牌阶段,你可以多摸4张牌。” + +很明显,这个技能就是一个加强版的英姿。将英姿的代码复制过来,稍微改改即可。接下来和大家细说怎么来复制粘贴。 + +-------------- + +首先,fk所有的技能都是Lua编写的,而本着开源精神也没有对Lua代码进行任何特殊处理,因此你可以直接在fk的release版中找到lua代码。英姿是标包的,因此去packages/standard/init.lua,可以找到英姿的代码: + +.. code:: lua + + 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, + } + +好的,复制过来。注意到标包lua里面,英姿在定义武将周瑜的前面,所以我们也把技能粘贴到武将的前面。完事之后,把技能加给武将。至此我们的lua会像这样: + +.. code:: lua + + 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 study_sunce = General(extension, "study_sunce", "wu", 4) + study_sunce:addSkill("zhiheng") + study_sunce:addSkill(yingzi) + Fk:loadTranslationTable{ + ["study_sunce"] = "孙伯符", + } + +启动游戏试试看,却给我们甩了个报错: + +.. image:: https://upload-images.jianshu.io/upload_images/21666547-b032b4f43ad13b58.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 + +原来是这个复制粘贴的技能和已有的英姿重复了。解法很简单,换个名字就行了,这里改名为“激姿”好了。按照命名习惯,为他起一个内部名称”study_jizi”。然后把所有的yingzi都改成这个名,改名后如下: + +.. code:: lua + + local study_jizi = fk.CreateTriggerSkill{ + name = "study_jizi", + anim_type = "drawcard", + events = {fk.DrawNCards}, + on_use = function(self, event, target, player, data) + data.n = data.n + 1 + end, + } + local study_sunce = General(extension, "study_sunce", "wu", 4) + study_sunce:addSkill("zhiheng") + study_sunce:addSkill(study_jizi) + +重新启动一下游戏,发现正常了,但是只能多摸一张。解法很简单,那句\ ``data.to = data.to + 1``\ 不就是让摸牌数+1吗,那我改成+4就行了,直接把1改成4: + +.. code:: lua + + data.n = data.n + 4 + +还有一件事,我们没给技能加翻译,往翻译表加上: + +.. code:: lua + + ["study_jizi"] = "激姿", + [":study_jizi"] = "摸牌阶段,你可以多摸4张牌。", + +至此完事了。别忘了更新一下git,后面不赘述关于git的事情了。 + +.. figure:: https://upload-images.jianshu.io/upload_images/21666547-f4c76ee91f8c15ae.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 + :alt: 搞定,一摸就是6张,薄纱神郭嘉 + + 搞定,一摸就是6张,薄纱神郭嘉 + +-------------- + +稍微解说一下创建技能的语法 +-------------------------- + +我们再来回头看看刚才复制粘贴的代码。 + +首先可以看出,技能是通过\ ``fk.CreateTriggerSkill``\ 创建的。在这个函数名中,Create意为创建,TriggerSkill则是我们要创建的技能类型——触发技。要创建其他技能也一样,都是通过CreateXXXSkill创建的。 + +然后,对于所有技能,我们都要为其指派一个name,用来标记这个技能的名字。这个技能的名字必须是唯一的,不能和其他任何技能产生冲突,最广泛的避免重名的方法就是给技能加上一些前缀。 + +然后有些技能还指派anim_type。这个其实可有可无,它控制的是技能发动时该播放哪种动效,有以下几种取值: + +- ``special``\ :留空anim_type时候的默认特效。看上去像一条龙的特效,一般用于定位模糊的技能。 +- ``drawcard``\ :看上去像是凤凰展翅的特效,用于主打摸牌的技能。 +- ``control``\ :看上去像草的特效,用于拆牌等控场类技能。 +- ``offensive``\ :看上去像火焰的特效,用于菜刀技能或者直伤等攻击性技能。 +- ``support``\ :看上去像莲花的特效,用于给牌、回血等辅助性技能。 +- ``defensive``\ :看上去像花的特效,用于防御流技能。 +- ``negative``\ :看上去像乌云的特效,用于负面技能。 +- ``masochism``\ :看上去像金色的花的特效,用于卖血类技能。(这个类型取名也是沿用了神杀的恶趣味啊) + +这些特效的图片素材位于image/anim/skillInvoke中。你可以改变技能的anim_type一一查看,或者直接去看素材也行。但是记住一点,这个属性除了控制技能触发的特效之外,和技能本身并没有任何联系,你想指定啥都行。 diff --git a/docs/diy/event/gameflow.rst b/docs/diy/event/gameflow.rst new file mode 100644 index 00000000..66f7a881 --- /dev/null +++ b/docs/diy/event/gameflow.rst @@ -0,0 +1,34 @@ +与游戏流程有关的事件 +==================== + +先来看游戏流程本身。以下节选自lua/server/gamelogic.lua + +.. code:: lua + + function GameLogic:action() + self:trigger(fk.GameStart) + local room = self.room + + for _, p in ipairs(room.alive_players) do + self:trigger(fk.DrawInitialCards, p, { num = 4 }) + end + + local function checkNoHuman() + -- 如果房里已经没有人类玩家了就结束游戏 + end + + while true do + self:trigger(fk.TurnStart, room.current) + if room.game_finished then break end + room.current = room.current:getNextAlive() + if checkNoHuman() then + room:gameOver("") + end + end + end + +以上这段代码,述说的就是整个游戏流程的核心。首先开始游戏、摸初始手牌,然后按照座位顺序每人依次执行回合直到游戏结束。 + +-------------- + +TODO diff --git a/docs/diy/event/hp.rst b/docs/diy/event/hp.rst new file mode 100644 index 00000000..88fd0b4a --- /dev/null +++ b/docs/diy/event/hp.rst @@ -0,0 +1,14 @@ +与体力值相关的事件 +================== + +伤害 +---- + +失去体力/体力上限 +----------------- + +回复体力 +-------- + +濒死和死亡 +---------- diff --git a/docs/diy/event/misc.rst b/docs/diy/event/misc.rst new file mode 100644 index 00000000..74edbb22 --- /dev/null +++ b/docs/diy/event/misc.rst @@ -0,0 +1,2 @@ +杂项事件 +============= diff --git a/docs/diy/event/movecard.rst b/docs/diy/event/movecard.rst new file mode 100644 index 00000000..34d4e4e8 --- /dev/null +++ b/docs/diy/event/movecard.rst @@ -0,0 +1,2 @@ +移动牌相关的事件 +===================== diff --git a/docs/diy/event/usecard.rst b/docs/diy/event/usecard.rst new file mode 100644 index 00000000..f0596449 --- /dev/null +++ b/docs/diy/event/usecard.rst @@ -0,0 +1,2 @@ +使用牌相关的事件 +==================== diff --git a/docs/diy/index.rst b/docs/diy/index.rst new file mode 100644 index 00000000..0778341b --- /dev/null +++ b/docs/diy/index.rst @@ -0,0 +1,11 @@ +Diy文档 +=============== + +.. toctree:: + :maxdepth: 1 + + 01-env.rst + 02-skilltype.rst + 03-newgeneral.rst + 04-newskill.rst + 03-events.rst diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..c31f303a --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,14 @@ +.. FreeKill documentation master file, created by + sphinx-quickstart on Sun Mar 26 02:58:53 2023. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +欢迎来到FreeKill文档! +==================================== + +.. toctree:: + :maxdepth: 1 + + diy/index.rst + dev/index.rst + api/index.rst diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..52e0f74b --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +sphinx-lua +sphinx-rtd-theme diff --git a/lua/client/client.lua b/lua/client/client.lua index 41321537..275e233a 100644 --- a/lua/client/client.lua +++ b/lua/client/client.lua @@ -1,11 +1,11 @@ ---@class Client ----@field client fk.Client ----@field players ClientPlayer[] ----@field alive_players ClientPlayer[] ----@field observers ClientPlayer[] ----@field current ClientPlayer ----@field discard_pile integer[] ----@field status_skills Skill[] +---@field public client fk.Client +---@field public players ClientPlayer[] +---@field public alive_players ClientPlayer[] +---@field public observers ClientPlayer[] +---@field public current ClientPlayer +---@field public discard_pile integer[] +---@field public status_skills Skill[] Client = class('Client') -- load client classes diff --git a/lua/client/client_util.lua b/lua/client/client_util.lua index 8ea5cbc3..6eee0b18 100644 --- a/lua/client/client_util.lua +++ b/lua/client/client_util.lua @@ -161,7 +161,6 @@ end ---@param card string | integer ---@param to_select integer @ id of the target ---@param selected integer[] @ ids of selected targets ----@param selected_cards integer[] @ ids of selected cards function CanUseCardToTarget(card, to_select, selected) if ClientInstance:getPlayerById(to_select).dead then return "false" @@ -192,7 +191,6 @@ end ---@param card string | integer ---@param to_select integer @ id of a card not selected ----@param selected integer[] @ ids of selected cards ---@param selected_targets integer[] @ ids of selected players function CanSelectCardForSkill(card, to_select, selected_targets) local c ---@type Card @@ -209,7 +207,6 @@ function CanSelectCardForSkill(card, to_select, selected_targets) end ---@param card string | integer ----@param selected integer[] @ ids of selected cards ---@param selected_targets integer[] @ ids of selected players function CardFeasible(card, selected_targets) local c ---@type Card diff --git a/lua/client/clientplayer.lua b/lua/client/clientplayer.lua index b39d23a9..365a20f3 100644 --- a/lua/client/clientplayer.lua +++ b/lua/client/clientplayer.lua @@ -1,7 +1,7 @@ ---@class ClientPlayer: Player ----@field player fk.Player ----@field known_cards integer[] ----@field global_known_cards integer[] +---@field public player fk.Player +---@field public known_cards integer[] +---@field public global_known_cards integer[] local ClientPlayer = Player:subclass("ClientPlayer") function ClientPlayer:initialize(cp) diff --git a/lua/core/card.lua b/lua/core/card.lua index dfdf5f92..34c6aeda 100644 --- a/lua/core/card.lua +++ b/lua/core/card.lua @@ -1,18 +1,18 @@ ---@class Card : Object ----@field package Package ----@field name string ----@field suit Suit ----@field number integer ----@field trueName string ----@field color Color ----@field id integer ----@field type CardType ----@field sub_type CardSubtype ----@field area CardArea ----@field subcards integer[] ----@field skillName string @ for virtual cards ----@field skill Skill ----@field special_skills string[] | nil +---@field public id integer +---@field public package Package +---@field public name string +---@field public suit Suit +---@field public number integer +---@field public trueName string +---@field public color Color +---@field public type CardType +---@field public sub_type CardSubtype +---@field public area CardArea +---@field public subcards integer[] +---@field public skillName string @ for virtual cards +---@field public skill Skill +---@field public special_skills string[] | nil local Card = class("Card") ---@alias Suit integer @@ -224,6 +224,10 @@ end ---@param c integer|integer[]|Card|Card[] ---@return integer[] +function Card:getIdList(c) + error("This is a static method. Please use Card:getIdList instead") +end + function Card.static:getIdList(c) if type(c) == "number" then return {c} diff --git a/lua/core/card_type/equip.lua b/lua/core/card_type/equip.lua index ddfa2dd5..1b474cfb 100644 --- a/lua/core/card_type/equip.lua +++ b/lua/core/card_type/equip.lua @@ -1,5 +1,5 @@ ---@class EquipCard : Card ----@field equip_skill Skill +---@field public equip_skill Skill local EquipCard = Card:subclass("EquipCard") function EquipCard:initialize(name, suit, number) diff --git a/lua/core/engine.lua b/lua/core/engine.lua index b0cd1e3f..7223999a 100644 --- a/lua/core/engine.lua +++ b/lua/core/engine.lua @@ -1,19 +1,29 @@ +--- Engine是整个FreeKill赖以运行的核心。 +--- +--- 它包含了FreeKill涉及的所有武将、卡牌、游戏模式等等 +--- +--- 同时也提供了许多常用的函数。 +--- ---@class Engine : Object ----@field packages table ----@field package_names string[] ----@field skills table ----@field related_skills table ----@field global_trigger TriggerSkill[] ----@field global_status_skill table ----@field generals table ----@field same_generals table ----@field lords string[] ----@field cards Card[] ----@field translations table> ----@field game_modes table ----@field disabled_packs string[] +---@field public packages table @ 所有拓展包的列表 +---@field public package_names string[] @ 含所有拓展包名字的数组,为了方便排序 +---@field public skills table @ 所有的技能 +---@field public related_skills table @ 所有技能的关联技能 +---@field public global_trigger TriggerSkill[] @ 所有的全局触发技 +---@field public global_status_skill table @ 所有的全局状态技 +---@field public generals table @ 所有武将 +---@field public same_generals table @ 所有同名武将组合 +---@field public lords string[] @ 所有主公武将,用于常备主公 +---@field public cards Card[] @ 所有卡牌 +---@field public translations table> @ 翻译表 +---@field public game_modes table @ 所有游戏模式 +---@field public disabled_packs string[] @ 禁用的拓展包列表 local Engine = class("Engine") +--- Engine的构造函数。 +--- +--- 这个函数只应该被执行一次。执行了之后,会创建一个Engine实例,并放入全局变量Fk中。 +---@return nil function Engine:initialize() -- Engine should be singleton if Fk ~= nil then @@ -41,7 +51,10 @@ function Engine:initialize() self:addSkills(AuxSkills) end ----@param pack Package +--- 向Engine中加载一个拓展包。 +--- +--- 会加载这个拓展包含有的所有武将、卡牌以及游戏模式。 +---@param pack Package @ 要加载的拓展包 function Engine:loadPackage(pack) assert(pack:isInstanceOf(Package)) if self.packages[pack.name] ~= nil then @@ -60,6 +73,14 @@ function Engine:loadPackage(pack) self:addGameModes(pack.game_modes) end +--- 加载所有拓展包。 +--- +--- Engine会在packages/下搜索所有含有init.lua的文件夹,并把它们作为拓展包加载进来。 +--- +--- 这样的init.lua可以返回单个拓展包,也可以返回拓展包数组,或者什么都不返回。 +--- +--- 标包和标准卡牌包比较特殊,它们永远会在第一个加载。 +---@return nil function Engine:loadPackages() local directories = FileIO.ls("packages") @@ -88,7 +109,9 @@ function Engine:loadPackages() end end ----@param t table +--- 向翻译表中加载新的翻译表。 +---@param t table @ 要加载的翻译表,这是一个 原文 --> 译文 的键值对表 +---@param lang string|nil @ 目标语言,默认为zh_CN function Engine:loadTranslationTable(t, lang) assert(type(t) == "table") lang = lang or "zh_CN" @@ -98,6 +121,8 @@ function Engine:loadTranslationTable(t, lang) end end +--- 翻译一段文本。其实就是从翻译表中去找 +---@param src string @ 要翻译的文本 function Engine:translate(src) local lang = Config.language or "zh_CN" if not self.translations[lang] then lang = "zh_CN" end @@ -105,7 +130,12 @@ function Engine:translate(src) return ret or src end ----@param skill Skill +--- 向Engine中加载一个技能。 +--- +--- 如果技能是global的,那么同时会将其放到那些global技能表中。 +--- +--- 如果技能有关联技能,那么递归地加载那些关联技能。 +---@param skill Skill @ 要加载的技能 function Engine:addSkill(skill) assert(skill.class:isSubclassOf(Skill)) if self.skills[skill.name] ~= nil then @@ -128,7 +158,8 @@ function Engine:addSkill(skill) end end ----@param skills Skill[] +--- 加载一系列技能。 +---@param skills Skill[] @ 要加载的技能数组 function Engine:addSkills(skills) assert(type(skills) == "table") for _, skill in ipairs(skills) do @@ -136,7 +167,10 @@ function Engine:addSkills(skills) end end ----@param general General +--- 加载一个武将到Engine中。 +--- +--- 如果武将的trueName和name不同的话,那么也会将其加到同将清单中。 +---@param general General @ 要添加的武将 function Engine:addGeneral(general) assert(general:isInstanceOf(General)) if self.generals[general.name] ~= nil then @@ -151,7 +185,8 @@ function Engine:addGeneral(general) end end ----@param generals General[] +--- 加载一系列武将。 +---@param generals General[] @ 要加载的武将列表 function Engine:addGenerals(generals) assert(type(generals) == "table") for _, general in ipairs(generals) do @@ -159,7 +194,11 @@ function Engine:addGenerals(generals) end end ----@param name string +--- 根据武将名称,获取它的同名武将。 +--- +--- 注意以此法返回的同名武将列表不包含他自己。 +---@param name string @ 要查询的武将名字 +---@return string[] @ 这个武将对应的同名武将列表 function Engine:getSameGenerals(name) local tmp = name:split("__") local tName = tmp[#tmp] @@ -172,7 +211,11 @@ end local cardId = 1 local _card_name_table = {} ----@param card Card + +--- 向Engine中加载一张卡牌。 +--- +--- 卡牌在加载的时候,会被赋予一个唯一的id。(从1开始) +---@param card Card @ 要加载的卡牌 function Engine:addCard(card) assert(card.class:isSubclassOf(Card)) card.id = cardId @@ -183,16 +226,20 @@ function Engine:addCard(card) end end ----@param cards Card[] +--- 向Engine中加载一系列卡牌。 +---@param cards Card[] @ 要加载的卡牌列表 function Engine:addCards(cards) for _, card in ipairs(cards) do self:addCard(card) end end ----@param name string ----@param suit Suit ----@param number integer +--- 根据牌名、花色、点数,复制一张牌。 +--- +--- 返回的牌是一张虚拟牌。 +---@param name string @ 牌名 +---@param suit Suit @ 花色 +---@param number integer @ 点数 ---@return Card function Engine:cloneCard(name, suit, number) local cd = _card_name_table[name] @@ -202,14 +249,16 @@ function Engine:cloneCard(name, suit, number) return ret end ----@param game_modes GameMode[] +--- 向Engine中添加一系列游戏模式。 +---@param game_modes GameMode[] @ 要添加的游戏模式列表 function Engine:addGameModes(game_modes) for _, s in ipairs(game_modes) do self:addGameMode(s) end end ----@param game_mode GameMode +--- 向Engine中添加一个游戏模式。 +---@param game_mode GameMode @ 要添加的游戏模式 function Engine:addGameMode(game_mode) assert(game_mode:isInstanceOf(GameMode)) if self.game_modes[game_mode.name] ~= nil then @@ -218,11 +267,16 @@ function Engine:addGameMode(game_mode) self.game_modes[game_mode.name] = game_mode end ----@param num integer ----@param generalPool General[] ----@param except string[] ----@param filter function ----@return General[] +--- 从已经开启的拓展包中,随机选出若干名武将。 +--- +--- 对于同名武将不会重复选取。 +--- +--- 如果符合条件的武将不够,那么就不能保证能选出那么多武将。 +---@param num integer @ 要选出的武将数量 +---@param generalPool General[] | nil @ 选择的范围,默认是已经启用的所有武将 +---@param except string[] | nil @ 特别要排除掉的武将名列表,默认是空表 +---@param filter fun(g: General): boolean | nil @ 可选参数,若这个函数返回true的话这个武将被排除在外 +---@return General[] @ 随机选出的武将列表 function Engine:getGeneralsRandomly(num, generalPool, except, filter) if filter then assert(type(filter) == "function") @@ -263,8 +317,9 @@ function Engine:getGeneralsRandomly(num, generalPool, except, filter) return result end ----@param except General[] ----@return General[] +--- 获取已经启用的所有武将的列表。 +---@param except General[] | nil @ 特别指明要排除在外的武将 +---@return General[] @ 所有武将的列表 function Engine:getAllGenerals(except) local result = {} for _, general in pairs(self.generals) do @@ -278,8 +333,9 @@ function Engine:getAllGenerals(except) return result end ----@param except integer[] ----@return integer[] +--- 获取当前已经启用的所有卡牌。 +---@param except integer[] | nil @ 特别指定要排除在外的id列表 +---@return integer[] @ 所有卡牌id的列表 function Engine:getAllCardIds(except) local result = {} for _, card in ipairs(self.cards) do @@ -295,9 +351,10 @@ end local filtered_cards = {} ----@param id integer ----@param ignoreFilter boolean ----@return Card +--- 根据id返回相应的卡牌。 +---@param id integer @ 牌的id +---@param ignoreFilter boolean @ 是否要无视掉锁定视为技,直接获得真牌 +---@return Card @ 这个id对应的卡牌 function Engine:getCardById(id, ignoreFilter) local ret = self.cards[id] if not ignoreFilter then @@ -306,9 +363,10 @@ function Engine:getCardById(id, ignoreFilter) return ret end ----@param id integer ----@param player Player ----@param data any @ may be JudgeStruct +--- 对那个id应用锁定视为技,将它变成要被锁定视为的牌。 +---@param id integer @ 要处理的id +---@param player Player @ 和这张牌扯上关系的那名玩家 +---@param data any @ 随意,目前只用到JudgeStruct,为了影响判定牌 function Engine:filterCard(id, player, data) local card = self:getCardById(id, true) if player == nil then @@ -370,6 +428,8 @@ function Engine:filterCard(id, player, data) end end +--- 获知当前的Engine是跑在服务端还是客户端,并返回相应的实例。 +---@return Room | Client function Engine:currentRoom() if RoomInstance then return RoomInstance @@ -377,6 +437,11 @@ function Engine:currentRoom() return ClientInstance end +--- 根据字符串获得这个技能或者这张牌的描述 +--- +--- 其实就是翻译了 ":" .. name 罢了 +---@param name string @ 要获得描述的名字 +---@return string @ 描述 function Engine:getDescription(name) return self:translate(":" .. name) end diff --git a/lua/core/exppattern.lua b/lua/core/exppattern.lua index 6459c2de..a949b85d 100644 --- a/lua/core/exppattern.lua +++ b/lua/core/exppattern.lua @@ -17,13 +17,13 @@ ]]-- ---@class Matcher ----@field name string[] ----@field number integer[] ----@field suit string[] ----@field place string[] ----@field generalName string[] ----@field cardType string[] ----@field id integer[] +---@field public name string[] +---@field public number integer[] +---@field public suit string[] +---@field public place string[] +---@field public generalName string[] +---@field public cardType string[] +---@field public id integer[] local numbertable = { ["A"] = 1, @@ -205,7 +205,7 @@ local function parseMatcher(str) end ---@class Exppattern: Object ----@field matchers Matcher[] +---@field public matchers Matcher[] local Exppattern = class("Exppattern") function Exppattern:initialize(spec) @@ -219,7 +219,12 @@ function Exppattern:initialize(spec) end end ----@param str string +---@param pattern string +---@return Exppattern +function Exppattern:Parse(pattern) + error("This is a static method. Please use Exppattern:Parse instead") +end + function Exppattern.static:Parse(str) local ret = Exppattern:new() local t = str:split(";") diff --git a/lua/core/game_mode.lua b/lua/core/game_mode.lua index 799329c8..44729729 100644 --- a/lua/core/game_mode.lua +++ b/lua/core/game_mode.lua @@ -1,9 +1,9 @@ ---@class GameMode: Object ----@field name string ----@field minPlayer integer ----@field maxPlayer integer ----@field rule TriggerSkill ----@field logic fun() +---@field public name string +---@field public minPlayer integer +---@field public maxPlayer integer +---@field public rule TriggerSkill +---@field public logic fun() local GameMode = class("GameMode") function GameMode:initialize(name, min, max) diff --git a/lua/core/general.lua b/lua/core/general.lua index 28e62f66..5a95439b 100644 --- a/lua/core/general.lua +++ b/lua/core/general.lua @@ -1,13 +1,13 @@ ---@class General : Object ----@field package Package ----@field name string ----@field trueName string ----@field kingdom string ----@field hp integer ----@field maxHp integer ----@field gender Gender ----@field skills Skill[] ----@field other_skills string[] +---@field public package Package +---@field public name string +---@field public trueName string +---@field public kingdom string +---@field public hp integer +---@field public maxHp integer +---@field public gender Gender +---@field public skills Skill[] +---@field public other_skills string[] General = class("General") ---@alias Gender integer diff --git a/lua/core/package.lua b/lua/core/package.lua index 97ff11b8..cc560fff 100644 --- a/lua/core/package.lua +++ b/lua/core/package.lua @@ -1,12 +1,16 @@ +--- Package用来描述一个FreeKill拓展包。 +--- +--- 所谓拓展包,就是武将/卡牌/游戏模式的一个集合而已。 +--- ---@class Package : Object ----@field name string ----@field extensionName string ----@field type PackageType ----@field generals General[] ----@field extra_skills Skill[] ----@field related_skills table ----@field cards Card[] ----@field game_modes GameMode[] +---@field public name string @ 拓展包的名字 +---@field public extensionName string @ 拓展包对应的mod的名字。 `详情... `_ +---@field public type PackageType @ 拓展包的类别,只会影响到选择拓展包的界面 +---@field public generals General[] @ 拓展包包含的所有武将的列表 +---@field public extra_skills Skill[] @ 拓展包包含的额外技能,即不属于武将的技能 +---@field public related_skills table @ 对于额外技能而言的关联技能 +---@field public cards Card[] @ 拓展包包含的卡牌 +---@field public game_modes GameMode[] @ 拓展包包含的游戏模式 local Package = class("Package") ---@alias PackageType integer @@ -15,6 +19,9 @@ Package.GeneralPack = 1 Package.CardPack = 2 Package.SpecialPack = 3 +--- 拓展包的构造函数。 +---@param name string @ 包的名字 +---@param _type integer|nil @ 包的类型,默认为武将包 function Package:initialize(name, _type) assert(type(name) == "string") assert(type(_type) == "nil" or type(_type) == "number") @@ -29,6 +36,9 @@ function Package:initialize(name, _type) self.game_modes = {} end +--- 获得这个包涉及的所有技能。 +--- +--- 这也就是说,所有的武将技能再加上和武将无关的技能。 ---@return Skill[] function Package:getSkills() local ret = {table.unpack(self.related_skills)} @@ -42,26 +52,31 @@ function Package:getSkills() return ret end ----@param general General +--- 向拓展包中添加武将。 +---@param general General @ 要添加的武将 function Package:addGeneral(general) assert(general.class and general:isInstanceOf(General)) table.insertIfNeed(self.generals, general) end ----@param card Card +--- 向拓展包中添加卡牌。 +---@param card Card @ 要添加的卡牌 function Package:addCard(card) assert(card.class and card:isInstanceOf(Card)) card.package = self table.insert(self.cards, card) end ----@param cards Card[] +--- 向拓展包中一次添加许多牌。 +---@param cards Card[] @ 要添加的卡牌的数组 function Package:addCards(cards) for _, card in ipairs(cards) do self:addCard(card) end end +--- 向拓展包中添加游戏模式。 +---@param game_mode GameMode @ 要添加的游戏模式。 function Package:addGameMode(game_mode) table.insert(self.game_modes, game_mode) end diff --git a/lua/core/player.lua b/lua/core/player.lua index 63862431..05ad4396 100644 --- a/lua/core/player.lua +++ b/lua/core/player.lua @@ -1,31 +1,35 @@ +--- 玩家分为客户端要处理的玩家,以及服务端处理的玩家两种。 +--- +--- 客户端能知道的玩家的信息十分有限,而服务端知道一名玩家的所有细节。 +--- +--- Player类就是这两种玩家的基类,包含它们共用的部分。 +--- ---@class Player : Object ----@field id integer ----@field hp integer ----@field maxHp integer ----@field kingdom string ----@field role string ----@field general string ----@field gender integer ----@field handcard_num integer ----@field seat integer ----@field next Player ----@field phase Phase ----@field faceup boolean ----@field chained boolean ----@field dying boolean ----@field dead boolean ----@field state string ----@field player_skills Skill[] ----@field derivative_skills table ----@field flag string[] ----@field tag table ----@field mark table ----@field player_cards table ----@field virtual_equips Card[] ----@field special_cards table ----@field cardUsedHistory table ----@field skillUsedHistory table ----@field fixedDistance table +---@field public id integer @ 玩家的id,每名玩家的id是唯一的。机器人的id是负数。 +---@field public hp integer @ 体力值 +---@field public maxHp integer @ 体力上限 +---@field public kingdom string @ 势力 +---@field public role string @ 身份 +---@field public general string @ 武将 +---@field public gender integer @ 性别 +---@field public seat integer @ 座位号 +---@field public next Player @ 下家 +---@field public phase Phase @ 当前阶段 +---@field public faceup boolean @ 是否正面朝上 +---@field public chained boolean @ 是否被横直 +---@field public dying boolean @ 是否处于濒死 +---@field public dead boolean @ 是否死亡 +---@field public player_skills Skill[] @ 当前拥有的所有技能 +---@field public derivative_skills table @ 当前拥有的派生技能 +---@field public flag string[] @ 当前拥有的flag,不过好像没用过 +---@field public tag table @ 当前拥有的所有tag,好像也没用过 +---@field public mark table @ 当前拥有的所有标记,用烂了 +---@field public player_cards table @ 当前拥有的所有牌,键是区域,值是id列表 +---@field public virtual_equips Card[] @ 当前的虚拟装备牌,其实也包含着虚拟延时锦囊这种 +---@field public special_cards table @ 类似“屯田”这种的私人牌堆 +---@field public cardUsedHistory table @ 用牌次数历史记录 +---@field public skillUsedHistory table @ 发动技能次数的历史记录 +---@field public fixedDistance table @ 与其他玩家的固定距离列表 local Player = class("Player") ---@alias Phase integer @@ -52,6 +56,7 @@ Player.HistoryTurn = 2 Player.HistoryRound = 3 Player.HistoryGame = 4 +--- 构造函数。总之这不是随便调用的函数 function Player:initialize() self.id = 114514 self.hp = 0 diff --git a/lua/core/skill.lua b/lua/core/skill.lua index 494429f0..10926d88 100644 --- a/lua/core/skill.lua +++ b/lua/core/skill.lua @@ -1,13 +1,13 @@ ---@class Skill : Object ----@field name string ----@field trueName string ----@field package Package ----@field frequency Frequency ----@field visible boolean ----@field mute boolean ----@field anim_type string ----@field related_skills Skill[] ----@field attached_equip string +---@field public name string +---@field public trueName string +---@field public package Package +---@field public frequency Frequency +---@field public visible boolean +---@field public mute boolean +---@field public anim_type string +---@field public related_skills Skill[] +---@field public attached_equip string local Skill = class("Skill") ---@alias Frequency integer diff --git a/lua/core/skill_type/active.lua b/lua/core/skill_type/active.lua index efa09c31..7e81ed87 100644 --- a/lua/core/skill_type/active.lua +++ b/lua/core/skill_type/active.lua @@ -1,12 +1,12 @@ ---@class ActiveSkill : UsableSkill ----@field min_target_num integer ----@field max_target_num integer ----@field target_num integer ----@field target_num_table integer[] ----@field min_card_num integer ----@field max_card_num integer ----@field card_num integer ----@field card_num_table integer[] +---@field public min_target_num integer +---@field public max_target_num integer +---@field public target_num integer +---@field public target_num_table integer[] +---@field public min_card_num integer +---@field public max_card_num integer +---@field public card_num integer +---@field public card_num_table integer[] local ActiveSkill = UsableSkill:subclass("ActiveSkill") function ActiveSkill:initialize(name) diff --git a/lua/core/skill_type/status_skill.lua b/lua/core/skill_type/status_skill.lua index 869c48f8..7bf2df30 100644 --- a/lua/core/skill_type/status_skill.lua +++ b/lua/core/skill_type/status_skill.lua @@ -1,5 +1,5 @@ ---@class StatusSkill : Skill ----@field global boolean +---@field public global boolean local StatusSkill = Skill:subclass("StatusSkill") function StatusSkill:initialize(name, frequency) diff --git a/lua/core/skill_type/trigger.lua b/lua/core/skill_type/trigger.lua index 19e78d6d..25bc5917 100644 --- a/lua/core/skill_type/trigger.lua +++ b/lua/core/skill_type/trigger.lua @@ -1,8 +1,8 @@ ---@class TriggerSkill : UsableSkill ----@field global boolean ----@field events Event[] ----@field refresh_events Event[] ----@field priority_table table +---@field public global boolean +---@field public events Event[] +---@field public refresh_events Event[] +---@field public priority_table table local TriggerSkill = UsableSkill:subclass("TriggerSkill") function TriggerSkill:initialize(name, frequency) diff --git a/lua/core/skill_type/usable_skill.lua b/lua/core/skill_type/usable_skill.lua index d7edf41f..efb62aac 100644 --- a/lua/core/skill_type/usable_skill.lua +++ b/lua/core/skill_type/usable_skill.lua @@ -1,6 +1,6 @@ ---@class UsableSkill : Skill ----@field max_use_time integer[] ----@field expand_pile string +---@field public max_use_time integer[] +---@field public expand_pile string local UsableSkill = Skill:subclass("UsableSkill") function UsableSkill:initialize(name, frequency) diff --git a/lua/core/skill_type/view_as.lua b/lua/core/skill_type/view_as.lua index aa787f70..efcc5513 100644 --- a/lua/core/skill_type/view_as.lua +++ b/lua/core/skill_type/view_as.lua @@ -1,5 +1,5 @@ ---@class ViewAsSkill : UsableSkill ----@field pattern string @ cards that can be viewAs'ed by this skill +---@field public pattern string @ cards that can be viewAs'ed by this skill local ViewAsSkill = UsableSkill:subclass("ViewAsSkill") function ViewAsSkill:initialize(name) diff --git a/lua/core/util.lua b/lua/core/util.lua index c5b5377a..2b9e9855 100644 --- a/lua/core/util.lua +++ b/lua/core/util.lua @@ -41,7 +41,7 @@ function table.filter(self, func) end ---@param func fun(element, index, array) -function table.map(self, func) +function table:map(func) local ret = {} for i, v in ipairs(self) do table.insert(ret, func(v, i, self)) @@ -142,11 +142,11 @@ end ---@param self T[] ---@param n integer ---@return T|T[] -function table.random(tab, n) +function table:random(n) local n0 = n n = n or 1 - if #tab == 0 then return nil end - local tmp = {table.unpack(tab)} + if #self == 0 then return nil end + local tmp = {table.unpack(self)} local ret = {} while n > 0 and #tmp > 0 do local i = math.random(1, #tmp) diff --git a/lua/fk_ex.lua b/lua/fk_ex.lua index b52fc2ab..6e3c6bbb 100644 --- a/lua/fk_ex.lua +++ b/lua/fk_ex.lua @@ -57,25 +57,25 @@ local function readStatusSpecToSkill(skill, spec) end ---@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 +---@field public max_phase_use_time integer +---@field public max_turn_use_time integer +---@field public max_round_use_time integer +---@field public max_game_use_time integer ---@class StatusSkillSpec: StatusSkill ---@alias TrigFunc fun(self: TriggerSkill, event: Event, target: ServerPlayer, player: ServerPlayer):boolean ---@class TriggerSkillSpec: UsableSkillSpec ----@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 ----@field can_refresh TrigFunc +---@field public global boolean +---@field public events Event | Event[] +---@field public refresh_events Event | Event[] +---@field public priority number | table +---@field public on_trigger TrigFunc +---@field public can_trigger TrigFunc +---@field public on_cost TrigFunc +---@field public on_use TrigFunc +---@field public on_refresh TrigFunc +---@field public can_refresh TrigFunc ---@param spec TriggerSkillSpec ---@return TriggerSkill @@ -140,14 +140,14 @@ function fk.CreateTriggerSkill(spec) end ---@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 ----@field feasible fun(self: ActiveSkill, selected: integer[], selected_cards: integer[]): boolean ----@field on_use fun(self: ActiveSkill, room: Room, cardUseEvent: CardUseStruct): boolean ----@field about_to_effect fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent): boolean ----@field on_effect fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent): boolean ----@field on_nullified fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent): boolean +---@field public can_use fun(self: ActiveSkill, player: Player): boolean +---@field public card_filter fun(self: ActiveSkill, to_select: integer, selected: integer[], selected_targets: integer[]): boolean +---@field public target_filter fun(self: ActiveSkill, to_select: integer, selected: integer[], selected_cards: integer[]): boolean +---@field public feasible fun(self: ActiveSkill, selected: integer[], selected_cards: integer[]): boolean +---@field public on_use fun(self: ActiveSkill, room: Room, cardUseEvent: CardUseStruct): boolean +---@field public about_to_effect fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent): boolean +---@field public on_effect fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent): boolean +---@field public on_nullified fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent): boolean ---@param spec ActiveSkillSpec ---@return ActiveSkill @@ -171,11 +171,11 @@ function fk.CreateActiveSkill(spec) end ---@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 ----@field enabled_at_play fun(self: ViewAsSkill, player: Player): boolean ----@field enabled_at_response fun(self: ViewAsSkill, player: Player): boolean +---@field public card_filter fun(self: ViewAsSkill, to_select: integer, selected: integer[]): boolean +---@field public view_as fun(self: ViewAsSkill, cards: integer[]) +---@field public pattern string +---@field public enabled_at_play fun(self: ViewAsSkill, player: Player): boolean +---@field public enabled_at_response fun(self: ViewAsSkill, player: Player): boolean ---@param spec ViewAsSkillSpec ---@return ViewAsSkill @@ -204,7 +204,7 @@ function fk.CreateViewAsSkill(spec) end ---@class DistanceSpec: StatusSkillSpec ----@field correct_func fun(self: DistanceSkill, from: Player, to: Player) +---@field public correct_func fun(self: DistanceSkill, from: Player, to: Player) ---@param spec DistanceSpec ---@return DistanceSkill @@ -220,10 +220,10 @@ function fk.CreateDistanceSkill(spec) end ---@class ProhibitSpec: StatusSkillSpec ----@field is_prohibited fun(self: ProhibitSkill, from: Player, to: Player, card: Card) ----@field prohibit_use fun(self: ProhibitSkill, player: Player, card: Card) ----@field prohibit_response fun(self: ProhibitSkill, player: Player, card: Card) ----@field prohibit_discard fun(self: ProhibitSkill, player: Player, card: Card) +---@field public is_prohibited fun(self: ProhibitSkill, from: Player, to: Player, card: Card) +---@field public prohibit_use fun(self: ProhibitSkill, player: Player, card: Card) +---@field public prohibit_response fun(self: ProhibitSkill, player: Player, card: Card) +---@field public prohibit_discard fun(self: ProhibitSkill, player: Player, card: Card) ---@param spec ProhibitSpec ---@return ProhibitSkill @@ -242,7 +242,7 @@ function fk.CreateProhibitSkill(spec) end ---@class AttackRangeSpec: StatusSkillSpec ----@field correct_func fun(self: AttackRangeSkill, from: Player, to: Player) +---@field public correct_func fun(self: AttackRangeSkill, from: Player, to: Player) ---@param spec AttackRangeSpec ---@return AttackRangeSkill @@ -258,8 +258,8 @@ function fk.CreateAttackRangeSkill(spec) end ---@class MaxCardsSpec: StatusSkillSpec ----@field correct_func fun(self: MaxCardsSkill, player: Player) ----@field fixed_func fun(self: MaxCardsSkill, from: Player) +---@field public correct_func fun(self: MaxCardsSkill, player: Player) +---@field public fixed_func fun(self: MaxCardsSkill, from: Player) ---@param spec MaxCardsSpec ---@return MaxCardsSkill @@ -280,9 +280,9 @@ function fk.CreateMaxCardsSkill(spec) 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) +---@field public residue_func fun(self: TargetModSkill, player: Player, skill: ActiveSkill, scope: integer) +---@field public distance_limit_func fun(self: TargetModSkill, player: Player, skill: ActiveSkill) +---@field public extra_target_func fun(self: TargetModSkill, player: Player, skill: ActiveSkill) ---@param spec TargetModSpec ---@return TargetModSkill @@ -305,8 +305,8 @@ function fk.CreateTargetModSkill(spec) end ---@class FilterSpec: StatusSkillSpec ----@field card_filter fun(self: FilterSkill, card: Card) ----@field view_as fun(self: FilterSkill, card: Card) +---@field public card_filter fun(self: FilterSkill, card: Card) +---@field public view_as fun(self: FilterSkill, card: Card) ---@param spec FilterSpec ---@return FilterSkill @@ -322,7 +322,7 @@ function fk.CreateFilterSkill(spec) end ---@class InvaliditySpec: StatusSkillSpec ----@field invalidity_func fun(self: InvaliditySkill, from: Player, skill: Skill) +---@field public invalidity_func fun(self: InvaliditySkill, from: Player, skill: Skill) ---@param spec InvaliditySpec ---@return InvaliditySkill @@ -337,8 +337,8 @@ function fk.CreateInvaliditySkill(spec) end ---@class CardSpec: Card ----@field skill Skill ----@field equip_skill Skill +---@field public skill Skill +---@field public equip_skill Skill local defaultCardSkill = fk.CreateActiveSkill{ name = "default_card_skill", diff --git a/lua/lsp/lib.lua b/lua/lsp/lib.lua index 4c2d8917..15664d7d 100644 --- a/lua/lsp/lib.lua +++ b/lua/lsp/lib.lua @@ -1,7 +1,7 @@ ---@meta ---@class class ----@field static any +---@field public static any --- middleclass class = {} @@ -10,7 +10,7 @@ class = {} function class:isSubclassOf(class) end ---@class Object ----@field class class +---@field public class class Object = { static = {} } ---@generic T diff --git a/lua/lsp/static.lua b/lua/lsp/static.lua deleted file mode 100644 index 04d7c8cf..00000000 --- a/lua/lsp/static.lua +++ /dev/null @@ -1,9 +0,0 @@ ----@meta - ----@param c integer|integer[]|Card|Card[] ----@return integer[] -function Card:getIdList(c) end - ----@param pattern string ----@return Exppattern -function Exppattern:Parse(pattern) end diff --git a/lua/server/ai/ai.lua b/lua/server/ai/ai.lua index b1eb2051..d8f6c5bf 100644 --- a/lua/server/ai/ai.lua +++ b/lua/server/ai/ai.lua @@ -2,11 +2,11 @@ -- Do nothing. ---@class AI: Object ----@field room Room ----@field player ServerPlayer ----@field command string ----@field jsonData string ----@field cb_table table +---@field public room Room +---@field public player ServerPlayer +---@field public command string +---@field public jsonData string +---@field public cb_table table local AI = class("AI") function AI:initialize(player) diff --git a/lua/server/gameevent.lua b/lua/server/gameevent.lua index cf16a5a5..c77d7fd3 100644 --- a/lua/server/gameevent.lua +++ b/lua/server/gameevent.lua @@ -1,11 +1,11 @@ ---@class GameEvent: Object ----@field room Room ----@field event integer ----@field data any ----@field main_func fun(self: GameEvent) ----@field clear_func fun(self: GameEvent) ----@field extra_clear_funcs any[] ----@field interrupted boolean +---@field public room Room +---@field public event integer +---@field public data any +---@field public main_func fun(self: GameEvent) +---@field public clear_func fun(self: GameEvent) +---@field public extra_clear_funcs any[] +---@field public interrupted boolean local GameEvent = class("GameEvent") GameEvent.functions = {} diff --git a/lua/server/gamelogic.lua b/lua/server/gamelogic.lua index b4159c59..31edc41b 100644 --- a/lua/server/gamelogic.lua +++ b/lua/server/gamelogic.lua @@ -1,11 +1,11 @@ ---@class GameLogic: Object ----@field room Room ----@field skill_table table ----@field refresh_skill_table table ----@field skills string[] ----@field event_stack Stack ----@field game_event_stack Stack ----@field role_table string[][] +---@field public room Room +---@field public skill_table table +---@field public refresh_skill_table table +---@field public skills string[] +---@field public event_stack Stack +---@field public game_event_stack Stack +---@field public role_table string[][] local GameLogic = class("GameLogic") function GameLogic:initialize(room) diff --git a/lua/server/room.lua b/lua/server/room.lua index 78493b0e..713fdd78 100644 --- a/lua/server/room.lua +++ b/lua/server/room.lua @@ -1,21 +1,21 @@ ---@class Room : Object ----@field room fk.Room ----@field players ServerPlayer[] ----@field alive_players ServerPlayer[] ----@field observers fk.ServerPlayer[] ----@field current ServerPlayer ----@field game_started boolean ----@field game_finished boolean ----@field timeout integer ----@field tag table ----@field draw_pile integer[] ----@field discard_pile integer[] ----@field processing_area integer[] ----@field void integer[] ----@field card_place table ----@field owner_map table ----@field status_skills Skill[] ----@field settings table +---@field public room fk.Room +---@field public players ServerPlayer[] +---@field public alive_players ServerPlayer[] +---@field public observers fk.ServerPlayer[] +---@field public current ServerPlayer +---@field public game_started boolean +---@field public game_finished boolean +---@field public timeout integer +---@field public tag table +---@field public draw_pile integer[] +---@field public discard_pile integer[] +---@field public processing_area integer[] +---@field public void integer[] +---@field public card_place table +---@field public owner_map table +---@field public status_skills Skill[] +---@field public settings table local Room = class("Room") -- load classes used by the game @@ -1707,7 +1707,7 @@ end ---@param player ServerPlayer ---@param num integer ---@param skillName string ----@param fromPlace "top"|"bottom" +---@param fromPlace string ---@return integer[] function Room:drawCards(player, num, skillName, fromPlace) local topCards = self:getNCards(num, fromPlace) @@ -1763,7 +1763,7 @@ end ---@param player ServerPlayer ---@param num integer ----@param reason "loseHp"|"damage"|"recover"|null +---@param reason string|nil ---@param skillName string ---@param damageStruct DamageStruct|null ---@return boolean diff --git a/lua/server/serverplayer.lua b/lua/server/serverplayer.lua index a08a3408..bad9e454 100644 --- a/lua/server/serverplayer.lua +++ b/lua/server/serverplayer.lua @@ -1,19 +1,19 @@ ---@class ServerPlayer : Player ----@field serverplayer fk.ServerPlayer ----@field room Room ----@field next ServerPlayer ----@field request_data string ----@field client_reply string ----@field default_reply string ----@field reply_ready boolean ----@field reply_cancel boolean ----@field phases Phase[] ----@field skipped_phases Phase[] ----@field phase_state table[] ----@field phase_index integer ----@field role_shown boolean ----@field ai AI ----@field ai_data any +---@field public serverplayer fk.ServerPlayer +---@field public room Room +---@field public next ServerPlayer +---@field public request_data string +---@field public client_reply string +---@field public default_reply string +---@field public reply_ready boolean +---@field public reply_cancel boolean +---@field public phases Phase[] +---@field public skipped_phases Phase[] +---@field public phase_state table[] +---@field public phase_index integer +---@field public role_shown boolean +---@field public ai AI +---@field public ai_data any local ServerPlayer = Player:subclass("ServerPlayer") function ServerPlayer:initialize(_self) diff --git a/lua/server/system_enum.lua b/lua/server/system_enum.lua index 86531b0c..318867d5 100644 --- a/lua/server/system_enum.lua +++ b/lua/server/system_enum.lua @@ -1,45 +1,45 @@ ---@class CardsMoveInfo ----@field ids integer[] ----@field from integer|null ----@field to integer|null ----@field toArea CardArea ----@field moveReason CardMoveReason ----@field proposer integer ----@field skillName string|null ----@field moveVisible boolean|null ----@field specialName string|null ----@field specialVisible boolean|null +---@field public ids integer[] +---@field public from integer|null +---@field public to integer|null +---@field public toArea CardArea +---@field public moveReason CardMoveReason +---@field public proposer integer +---@field public skillName string|null +---@field public moveVisible boolean|null +---@field public specialName string|null +---@field public specialVisible boolean|null ---@class MoveInfo ----@field cardId integer ----@field fromArea CardArea ----@field fromSpecialName string|null +---@field public cardId integer +---@field public fromArea CardArea +---@field public fromSpecialName string|null ---@class CardsMoveStruct ----@field moveInfo MoveInfo[] ----@field from integer|null ----@field to integer|null ----@field toArea CardArea ----@field moveReason CardMoveReason ----@field proposer integer|null ----@field skillName string|null ----@field moveVisible boolean|null ----@field specialName string|null ----@field specialVisible boolean|null +---@field public moveInfo MoveInfo[] +---@field public from integer|null +---@field public to integer|null +---@field public toArea CardArea +---@field public moveReason CardMoveReason +---@field public proposer integer|null +---@field public skillName string|null +---@field public moveVisible boolean|null +---@field public specialName string|null +---@field public specialVisible boolean|null ---@class PindianResult ----@field toCard Card ----@field winner ServerPlayer|null +---@field public toCard Card +---@field public winner ServerPlayer|null ---@class HpChangedData ----@field num integer ----@field reason string ----@field skillName string ----@field damageEvent DamageStruct|null +---@field public num integer +---@field public reason string +---@field public skillName string +---@field public damageEvent DamageStruct|null ---@class HpLostData ----@field num integer ----@field skillName string +---@field public num integer +---@field public skillName string ---@alias DamageType integer @@ -48,109 +48,109 @@ fk.ThunderDamage = 2 fk.FireDamage = 3 ---@class DamageStruct ----@field from ServerPlayer|null ----@field to ServerPlayer ----@field damage integer ----@field card Card ----@field chain boolean ----@field damageType DamageType ----@field skillName string ----@field beginnerOfTheDamage boolean|null +---@field public from ServerPlayer|null +---@field public to ServerPlayer +---@field public damage integer +---@field public card Card +---@field public chain boolean +---@field public damageType DamageType +---@field public skillName string +---@field public beginnerOfTheDamage boolean|null ---@class RecoverStruct ----@field who ServerPlayer ----@field num integer ----@field recoverBy ServerPlayer|null ----@field skillName string|null ----@field card Card|null +---@field public who ServerPlayer +---@field public num integer +---@field public recoverBy ServerPlayer|null +---@field public skillName string|null +---@field public card Card|null ---@class DyingStruct ----@field who integer ----@field damage DamageStruct +---@field public who integer +---@field public damage DamageStruct ---@class DeathStruct ----@field who integer ----@field damage DamageStruct +---@field public who integer +---@field public damage DamageStruct ---@class CardUseStruct ----@field from integer ----@field tos TargetGroup ----@field card Card ----@field toCard Card|null ----@field responseToEvent CardUseStruct|null ----@field nullifiedTargets interger[]|null ----@field extraUse boolean|null ----@field disresponsiveList integer[]|null ----@field unoffsetableList integer[]|null ----@field additionalDamage integer|null ----@field customFrom integer|null ----@field cardsResponded Card[]|null +---@field public from integer +---@field public tos TargetGroup +---@field public card Card +---@field public toCard Card|null +---@field public responseToEvent CardUseStruct|null +---@field public nullifiedTargets interger[]|null +---@field public extraUse boolean|null +---@field public disresponsiveList integer[]|null +---@field public unoffsetableList integer[]|null +---@field public additionalDamage integer|null +---@field public customFrom integer|null +---@field public cardsResponded Card[]|null ---@class AimStruct ----@field from integer ----@field card Card ----@field tos AimGroup ----@field to integer ----@field subTargets integer[]|null ----@field targetGroup TargetGroup|null ----@field nullifiedTargets integer[]|null ----@field firstTarget boolean ----@field additionalDamage integer|null ----@field disresponsive boolean|null ----@field unoffsetableList boolean|null ----@field additionalResponseTimes table|integer|null ----@field fixedAddTimesResponsors integer[] +---@field public from integer +---@field public card Card +---@field public tos AimGroup +---@field public to integer +---@field public subTargets integer[]|null +---@field public targetGroup TargetGroup|null +---@field public nullifiedTargets integer[]|null +---@field public firstTarget boolean +---@field public additionalDamage integer|null +---@field public disresponsive boolean|null +---@field public unoffsetableList boolean|null +---@field public additionalResponseTimes table|integer|null +---@field public fixedAddTimesResponsors integer[] ---@class CardEffectEvent ----@field from integer ----@field to integer ----@field subTargets integer[]|null ----@field tos TargetGroup ----@field card Card ----@field toCard Card|null ----@field responseToEvent CardEffectEvent|null ----@field nullifiedTargets interger[]|null ----@field extraUse boolean|null ----@field disresponsiveList integer[]|null ----@field unoffsetableList integer[]|null ----@field additionalDamage integer|null ----@field customFrom integer|null ----@field cardsResponded Card[]|null ----@field disresponsive boolean|null ----@field unoffsetable boolean|null ----@field isCancellOut boolean|null ----@field fixedResponseTimes table|integer|null ----@field fixedAddTimesResponsors integer[] +---@field public from integer +---@field public to integer +---@field public subTargets integer[]|null +---@field public tos TargetGroup +---@field public card Card +---@field public toCard Card|null +---@field public responseToEvent CardEffectEvent|null +---@field public nullifiedTargets interger[]|null +---@field public extraUse boolean|null +---@field public disresponsiveList integer[]|null +---@field public unoffsetableList integer[]|null +---@field public additionalDamage integer|null +---@field public customFrom integer|null +---@field public cardsResponded Card[]|null +---@field public disresponsive boolean|null +---@field public unoffsetable boolean|null +---@field public isCancellOut boolean|null +---@field public fixedResponseTimes table|integer|null +---@field public fixedAddTimesResponsors integer[] ---@class SkillEffectEvent ----@field from integer ----@field tos integer[] ----@field cards integer[] +---@field public from integer +---@field public tos integer[] +---@field public cards integer[] ---@class JudgeStruct ----@field who ServerPlayer ----@field card Card ----@field reason string ----@field pattern string +---@field public who ServerPlayer +---@field public card Card +---@field public reason string +---@field public pattern string ---@class CardResponseEvent ----@field from integer ----@field card Card ----@field responseToEvent CardEffectEvent|null ----@field skipDrop boolean|null ----@field customFrom integer|null +---@field public from integer +---@field public card Card +---@field public responseToEvent CardEffectEvent|null +---@field public skipDrop boolean|null +---@field public customFrom integer|null ---@class AskForCardUse ----@field user ServerPlayer ----@field cardName string ----@field pattern string ----@field result CardUseStruct +---@field public user ServerPlayer +---@field public cardName string +---@field public pattern string +---@field public result CardUseStruct ---@class AskForCardResponse ----@field user ServerPlayer ----@field cardName string ----@field pattern string ----@field result Card +---@field public user ServerPlayer +---@field public cardName string +---@field public pattern string +---@field public result Card ---@alias CardMoveReason integer @@ -166,17 +166,17 @@ fk.ReasonUse = 9 fk.ReasonResonpse = 10 ---@class PindianStruct ----@field from ServerPlayer ----@field tos ServerPlayer[] ----@field fromCard Card ----@field results table ----@field reason string +---@field public from ServerPlayer +---@field public tos ServerPlayer[] +---@field public fromCard Card +---@field public results table +---@field public reason string ---@class LogMessage ----@field type string ----@field from integer ----@field to integer[] ----@field card integer[] ----@field arg any ----@field arg2 any ----@field arg3 any +---@field public type string +---@field public from integer +---@field public to integer[] +---@field public card integer[] +---@field public arg any +---@field public arg2 any +---@field public arg3 any