mirror of
https://github.com/Qsgs-Fans/FreeKill.git
synced 2024-11-16 03:32:34 +08:00
parent
4c2db268f6
commit
a1ae83c562
19
.github/workflows/sphinx.yml
vendored
Normal file
19
.github/workflows/sphinx.yml
vendored
Normal file
|
@ -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
|
|
@ -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即可。每次编译资源文件总要消耗相当多的时间。
|
|
@ -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)
|
|
@ -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)
|
|
@ -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的内容判断技能出牌阶段是否可用、是否能够响应等。
|
|
@ -1,9 +0,0 @@
|
|||
# fk中的游戏事件
|
||||
|
||||
在进行DIY时,需要对三国杀的规则有一定了解;在编写技能时,也要熟悉游戏提供的各种事件,他的触发方式、触发时机、相关数据。必须要知道这些才能写出正确的代码。
|
||||
|
||||
- [与游戏流程相关的事件](./event/gameflow.md)
|
||||
- [与体力值相关的事件](./event/hp.md)
|
||||
- [与卡牌使用有关的事件](./event/usecard.md)
|
||||
- [与移动牌有关的事件](./event/movecard.md)
|
||||
- [杂项](./event/misc.md)
|
|
@ -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
|
|
@ -1,11 +0,0 @@
|
|||
# 与体力值相关的事件
|
||||
|
||||
___
|
||||
|
||||
## 伤害
|
||||
|
||||
## 失去体力/体力上限
|
||||
|
||||
## 回复体力
|
||||
|
||||
## 濒死和死亡
|
|
@ -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)
|
|
@ -1,6 +0,0 @@
|
|||
# FreeKill 文档
|
||||
|
||||
___
|
||||
|
||||
- [开发者文档](./dev/index.md)
|
||||
- [DIY玩家文档](./diy/index.md)
|
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
|
@ -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)
|
2
docs/api/client.rst
Normal file
2
docs/api/client.rst
Normal file
|
@ -0,0 +1,2 @@
|
|||
Client
|
||||
============
|
15
docs/api/core.rst
Normal file
15
docs/api/core.rst
Normal file
|
@ -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
|
||||
|
4
docs/api/core/card.rst
Normal file
4
docs/api/core/card.rst
Normal file
|
@ -0,0 +1,4 @@
|
|||
Card
|
||||
==============
|
||||
|
||||
.. lua:autoclass:: Card
|
4
docs/api/core/engine.rst
Normal file
4
docs/api/core/engine.rst
Normal file
|
@ -0,0 +1,4 @@
|
|||
Engine
|
||||
==============
|
||||
|
||||
.. lua:autoclass:: Engine
|
5
docs/api/core/game_mode.rst
Normal file
5
docs/api/core/game_mode.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
GameMode
|
||||
==============
|
||||
|
||||
.. lua:autoclass:: GameMode
|
||||
|
5
docs/api/core/general.rst
Normal file
5
docs/api/core/general.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
General
|
||||
==============
|
||||
|
||||
.. lua:autoclass:: General
|
||||
|
24
docs/api/core/package.rst
Normal file
24
docs/api/core/package.rst
Normal file
|
@ -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,所以需要手动指定。
|
5
docs/api/core/player.rst
Normal file
5
docs/api/core/player.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
Player
|
||||
==============
|
||||
|
||||
.. lua:autoclass:: Player
|
||||
|
4
docs/api/core/skill.rst
Normal file
4
docs/api/core/skill.rst
Normal file
|
@ -0,0 +1,4 @@
|
|||
Skill
|
||||
==============
|
||||
|
||||
.. lua:autoclass:: Skill
|
9
docs/api/index.rst
Normal file
9
docs/api/index.rst
Normal file
|
@ -0,0 +1,9 @@
|
|||
API文档
|
||||
============
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
core.rst
|
||||
server.rst
|
||||
client.rst
|
2
docs/api/server.rst
Normal file
2
docs/api/server.rst
Normal file
|
@ -0,0 +1,2 @@
|
|||
Server
|
||||
============
|
80
docs/conf.py
Normal file
80
docs/conf.py
Normal file
|
@ -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']
|
|
@ -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才行。
|
146
docs/dev/compile.rst
Normal file
146
docs/dev/compile.rst
Normal file
|
@ -0,0 +1,146 @@
|
|||
编译 FreeKill
|
||||
=============
|
||||
|
||||
全平台通用步骤
|
||||
--------------
|
||||
|
||||
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一家子:
|
||||
|
||||
.. 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即可。每次编译资源文件总要消耗相当多的时间。
|
|
@ -1,15 +1,14 @@
|
|||
# FreeKill 的数据库
|
||||
|
||||
> [dev](./index.md) > 数据库
|
||||
|
||||
___
|
||||
FreeKill 的数据库
|
||||
=================
|
||||
|
||||
FreeKill 使用 sqlite3 数据库。
|
||||
|
||||
关于数据库的组织,详见server/init.sql。单纯存个用户名和密码而已
|
||||
|
||||
## 服务端用来管理用户的数据库
|
||||
服务端用来管理用户的数据库
|
||||
--------------------------
|
||||
|
||||
保存用户名与密码而已。
|
||||
|
||||
## 包管理用的数据库
|
||||
包管理用的数据库
|
||||
----------------
|
|
@ -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会认为协程是挂起的,用户可能想要恢复,于是一直保存着。
|
||||
|
|
@ -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<Event, number>
|
||||
---@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<Event, number>
|
||||
---@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。不行太复杂了,过一阵子再来看吧。
|
16
docs/dev/index.rst
Normal file
16
docs/dev/index.rst
Normal file
|
@ -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
|
|
@ -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 \<hash\>。
|
||||
3. 客户端根据文件内容检查自己的拓展包。如果那个文件夹存在,那么就git
|
||||
fetch -> git checkout <hash>。
|
||||
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。
|
|
@ -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 <port>
|
||||
```
|
||||
.. code:: sh
|
||||
|
||||
`<port>`是服务器运行的端口号,如果不带任何参数则启动GUI界面,在GUI界面里面只能加入服务器或者单机游戏。
|
||||
$ ./FreeKill -s <port>
|
||||
|
||||
``<port>``\ 是服务器运行的端口号,如果不带任何参数则启动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)
|
||||
------------
|
||||
|
||||
因为房间不允许加入比玩家上限的玩家,可以考虑在房间里新建一个列表存储旁观中的玩家。但是这样或许会让某些处理(如掉线)变得复杂化。
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
# 关于扩展FreeKill玩法的思考
|
||||
|
||||
___
|
||||
关于扩展FreeKill玩法的思考
|
||||
==========================
|
||||
|
||||
要扩展玩法,大概就这些:
|
||||
|
||||
|
@ -14,14 +13,16 @@ ___
|
|||
3. 加载GameRule后根据模式加载特殊规则
|
||||
4. 开始玩
|
||||
|
||||
___
|
||||
--------------
|
||||
|
||||
## 拓展新规
|
||||
拓展新规
|
||||
--------
|
||||
|
||||
首先就是如何覆盖老规则,这个可以通过设置一个特殊tag
|
||||
|
||||
___
|
||||
--------------
|
||||
|
||||
## 拓展logic
|
||||
拓展logic
|
||||
---------
|
||||
|
||||
从GameLogic继承然后重写有关函数就行
|
|
@ -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之类的时机的话,这方面就更加重要了。
|
|
@ -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,这里只关心前者。
|
||||
|
239
docs/diy/01-env.rst
Normal file
239
docs/diy/01-env.rst
Normal file
|
@ -0,0 +1,239 @@
|
|||
Fk DIY - 环境搭建
|
||||
=================
|
||||
|
||||
DIY总览
|
||||
-------
|
||||
|
||||
正如项目README所言,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文件,写入以下内容:
|
||||
|
||||
.. 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以及更新的流程。至于资源文件组织等等杂七杂八的问题,请参考已有的例子拓展包。
|
45
docs/diy/02-skilltype.rst
Normal file
45
docs/diy/02-skilltype.rst
Normal file
|
@ -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的内容判断技能出牌阶段是否可用、是否能够响应等。
|
14
docs/diy/03-events.rst
Normal file
14
docs/diy/03-events.rst
Normal file
|
@ -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
|
199
docs/diy/03-newgeneral.rst
Normal file
199
docs/diy/03-newgeneral.rst
Normal file
|
@ -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
|
||||
如有疑问,可以去查看那个仓库是怎么弄的。)
|
108
docs/diy/04-newskill.rst
Normal file
108
docs/diy/04-newskill.rst
Normal file
|
@ -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一一查看,或者直接去看素材也行。但是记住一点,这个属性除了控制技能触发的特效之外,和技能本身并没有任何联系,你想指定啥都行。
|
34
docs/diy/event/gameflow.rst
Normal file
34
docs/diy/event/gameflow.rst
Normal file
|
@ -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
|
14
docs/diy/event/hp.rst
Normal file
14
docs/diy/event/hp.rst
Normal file
|
@ -0,0 +1,14 @@
|
|||
与体力值相关的事件
|
||||
==================
|
||||
|
||||
伤害
|
||||
----
|
||||
|
||||
失去体力/体力上限
|
||||
-----------------
|
||||
|
||||
回复体力
|
||||
--------
|
||||
|
||||
濒死和死亡
|
||||
----------
|
2
docs/diy/event/misc.rst
Normal file
2
docs/diy/event/misc.rst
Normal file
|
@ -0,0 +1,2 @@
|
|||
杂项事件
|
||||
=============
|
2
docs/diy/event/movecard.rst
Normal file
2
docs/diy/event/movecard.rst
Normal file
|
@ -0,0 +1,2 @@
|
|||
移动牌相关的事件
|
||||
=====================
|
2
docs/diy/event/usecard.rst
Normal file
2
docs/diy/event/usecard.rst
Normal file
|
@ -0,0 +1,2 @@
|
|||
使用牌相关的事件
|
||||
====================
|
11
docs/diy/index.rst
Normal file
11
docs/diy/index.rst
Normal file
|
@ -0,0 +1,11 @@
|
|||
Diy文档
|
||||
===============
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
01-env.rst
|
||||
02-skilltype.rst
|
||||
03-newgeneral.rst
|
||||
04-newskill.rst
|
||||
03-events.rst
|
14
docs/index.rst
Normal file
14
docs/index.rst
Normal file
|
@ -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
|
2
docs/requirements.txt
Normal file
2
docs/requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
sphinx-lua
|
||||
sphinx-rtd-theme
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,19 +1,29 @@
|
|||
--- Engine是整个FreeKill赖以运行的核心。
|
||||
---
|
||||
--- 它包含了FreeKill涉及的所有武将、卡牌、游戏模式等等
|
||||
---
|
||||
--- 同时也提供了许多常用的函数。
|
||||
---
|
||||
---@class Engine : Object
|
||||
---@field packages table<string, Package>
|
||||
---@field package_names string[]
|
||||
---@field skills table<string, Skill>
|
||||
---@field related_skills table<string, Skill[]>
|
||||
---@field global_trigger TriggerSkill[]
|
||||
---@field global_status_skill table<class, Skill[]>
|
||||
---@field generals table<string, General>
|
||||
---@field same_generals table<string, string[]>
|
||||
---@field lords string[]
|
||||
---@field cards Card[]
|
||||
---@field translations table<string, table<string, string>>
|
||||
---@field game_modes table<string, GameMode>
|
||||
---@field disabled_packs string[]
|
||||
---@field public packages table<string, Package> @ 所有拓展包的列表
|
||||
---@field public package_names string[] @ 含所有拓展包名字的数组,为了方便排序
|
||||
---@field public skills table<string, Skill> @ 所有的技能
|
||||
---@field public related_skills table<string, Skill[]> @ 所有技能的关联技能
|
||||
---@field public global_trigger TriggerSkill[] @ 所有的全局触发技
|
||||
---@field public global_status_skill table<class, Skill[]> @ 所有的全局状态技
|
||||
---@field public generals table<string, General> @ 所有武将
|
||||
---@field public same_generals table<string, string[]> @ 所有同名武将组合
|
||||
---@field public lords string[] @ 所有主公武将,用于常备主公
|
||||
---@field public cards Card[] @ 所有卡牌
|
||||
---@field public translations table<string, table<string, string>> @ 翻译表
|
||||
---@field public game_modes table<string, GameMode> @ 所有游戏模式
|
||||
---@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
|
||||
|
|
|
@ -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(";")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<string, string>
|
||||
---@field cards Card[]
|
||||
---@field game_modes GameMode[]
|
||||
---@field public name string @ 拓展包的名字
|
||||
---@field public extensionName string @ 拓展包对应的mod的名字。 `详情... <extension name_>`_
|
||||
---@field public type PackageType @ 拓展包的类别,只会影响到选择拓展包的界面
|
||||
---@field public generals General[] @ 拓展包包含的所有武将的列表
|
||||
---@field public extra_skills Skill[] @ 拓展包包含的额外技能,即不属于武将的技能
|
||||
---@field public related_skills table<string, string> @ 对于额外技能而言的关联技能
|
||||
---@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
|
||||
|
|
|
@ -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<Skill, Skill[]>
|
||||
---@field flag string[]
|
||||
---@field tag table<string, any>
|
||||
---@field mark table<string, integer>
|
||||
---@field player_cards table<integer, integer[]>
|
||||
---@field virtual_equips Card[]
|
||||
---@field special_cards table<string, integer[]>
|
||||
---@field cardUsedHistory table<string, integer[]>
|
||||
---@field skillUsedHistory table<string, integer[]>
|
||||
---@field fixedDistance table<Player, integer>
|
||||
---@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<Skill, Skill[]> @ 当前拥有的派生技能
|
||||
---@field public flag string[] @ 当前拥有的flag,不过好像没用过
|
||||
---@field public tag table<string, any> @ 当前拥有的所有tag,好像也没用过
|
||||
---@field public mark table<string, integer> @ 当前拥有的所有标记,用烂了
|
||||
---@field public player_cards table<integer, integer[]> @ 当前拥有的所有牌,键是区域,值是id列表
|
||||
---@field public virtual_equips Card[] @ 当前的虚拟装备牌,其实也包含着虚拟延时锦囊这种
|
||||
---@field public special_cards table<string, integer[]> @ 类似“屯田”这种的私人牌堆
|
||||
---@field public cardUsedHistory table<string, integer[]> @ 用牌次数历史记录
|
||||
---@field public skillUsedHistory table<string, integer[]> @ 发动技能次数的历史记录
|
||||
---@field public fixedDistance table<Player, integer> @ 与其他玩家的固定距离列表
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---@class StatusSkill : Skill
|
||||
---@field global boolean
|
||||
---@field public global boolean
|
||||
local StatusSkill = Skill:subclass("StatusSkill")
|
||||
|
||||
function StatusSkill:initialize(name, frequency)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
---@class TriggerSkill : UsableSkill
|
||||
---@field global boolean
|
||||
---@field events Event[]
|
||||
---@field refresh_events Event[]
|
||||
---@field priority_table table<Event, number>
|
||||
---@field public global boolean
|
||||
---@field public events Event[]
|
||||
---@field public refresh_events Event[]
|
||||
---@field public priority_table table<Event, number>
|
||||
local TriggerSkill = UsableSkill:subclass("TriggerSkill")
|
||||
|
||||
function TriggerSkill:initialize(name, frequency)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<Event, number>
|
||||
---@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<Event, number>
|
||||
---@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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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<string, fun(jsonData: string)>
|
||||
---@field public room Room
|
||||
---@field public player ServerPlayer
|
||||
---@field public command string
|
||||
---@field public jsonData string
|
||||
---@field public cb_table table<string, fun(jsonData: string)>
|
||||
local AI = class("AI")
|
||||
|
||||
function AI:initialize(player)
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
---@class GameLogic: Object
|
||||
---@field room Room
|
||||
---@field skill_table table<Event, TriggerSkill[]>
|
||||
---@field refresh_skill_table table<Event, TriggerSkill[]>
|
||||
---@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<Event, TriggerSkill[]>
|
||||
---@field public refresh_skill_table table<Event, TriggerSkill[]>
|
||||
---@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)
|
||||
|
|
|
@ -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<string, any>
|
||||
---@field draw_pile integer[]
|
||||
---@field discard_pile integer[]
|
||||
---@field processing_area integer[]
|
||||
---@field void integer[]
|
||||
---@field card_place table<integer, CardArea>
|
||||
---@field owner_map table<integer, integer>
|
||||
---@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<string, any>
|
||||
---@field public draw_pile integer[]
|
||||
---@field public discard_pile integer[]
|
||||
---@field public processing_area integer[]
|
||||
---@field public void integer[]
|
||||
---@field public card_place table<integer, CardArea>
|
||||
---@field public owner_map table<integer, integer>
|
||||
---@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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<string, integer>|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<string, integer>|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<string, integer>|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<string, integer>|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<integer, PindianResult>
|
||||
---@field reason string
|
||||
---@field public from ServerPlayer
|
||||
---@field public tos ServerPlayer[]
|
||||
---@field public fromCard Card
|
||||
---@field public results table<integer, PindianResult>
|
||||
---@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
|
||||
|
|
Loading…
Reference in New Issue
Block a user