Sphinx doc (#86)

将文档系统改成Sphinx,并增加了新CI
This commit is contained in:
notify 2023-03-26 17:32:45 +08:00 committed by GitHub
parent 4c2db268f6
commit a1ae83c562
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
77 changed files with 1949 additions and 1277 deletions

19
.github/workflows/sphinx.yml vendored Normal file
View 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

View File

@ -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即可。每次编译资源文件总要消耗相当多的时间。

View File

@ -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)

View File

@ -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 参数。
```
看来我们初次安装GitGit还不知道我们的身份呢不过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)

View File

@ -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的内容判断技能出牌阶段是否可用、是否能够响应等。

View File

@ -1,9 +0,0 @@
# fk中的游戏事件
在进行DIY时需要对三国杀的规则有一定了解在编写技能时也要熟悉游戏提供的各种事件他的触发方式、触发时机、相关数据。必须要知道这些才能写出正确的代码。
- [与游戏流程相关的事件](./event/gameflow.md)
- [与体力值相关的事件](./event/hp.md)
- [与卡牌使用有关的事件](./event/usecard.md)
- [与移动牌有关的事件](./event/movecard.md)
- [杂项](./event/misc.md)

View File

@ -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

View File

@ -1,11 +0,0 @@
# 与体力值相关的事件
___
## 伤害
## 失去体力/体力上限
## 回复体力
## 濒死和死亡

View File

View File

@ -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)

View File

@ -1,6 +0,0 @@
# FreeKill 文档
___
- [开发者文档](./dev/index.md)
- [DIY玩家文档](./diy/index.md)

20
docs/Makefile Normal file
View 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
View File

@ -0,0 +1,2 @@
Client
============

15
docs/api/core.rst Normal file
View 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
View File

@ -0,0 +1,4 @@
Card
==============
.. lua:autoclass:: Card

4
docs/api/core/engine.rst Normal file
View File

@ -0,0 +1,4 @@
Engine
==============
.. lua:autoclass:: Engine

View File

@ -0,0 +1,5 @@
GameMode
==============
.. lua:autoclass:: GameMode

View File

@ -0,0 +1,5 @@
General
==============
.. lua:autoclass:: General

24
docs/api/core/package.rst Normal file
View 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
View File

@ -0,0 +1,5 @@
Player
==============
.. lua:autoclass:: Player

4
docs/api/core/skill.rst Normal file
View File

@ -0,0 +1,4 @@
Skill
==============
.. lua:autoclass:: Skill

9
docs/api/index.rst Normal file
View File

@ -0,0 +1,9 @@
API文档
============
.. toctree::
:maxdepth: 1
core.rst
server.rst
client.rst

2
docs/api/server.rst Normal file
View File

@ -0,0 +1,2 @@
Server
============

80
docs/conf.py Normal file
View 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']

View File

@ -1,19 +1,18 @@
# FreeKill 的 AI 系统
FreeKill 的 AI 系统
===================
> [dev](./index.md) > AI
___
## 概述
概述
----
备选算法:
- 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
View 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即可。每次编译资源文件总要消耗相当多的时间。

View File

@ -1,15 +1,14 @@
# FreeKill 的数据库
> [dev](./index.md) > 数据库
___
FreeKill 的数据库
=================
FreeKill 使用 sqlite3 数据库。
关于数据库的组织详见server/init.sql。单纯存个用户名和密码而已
## 服务端用来管理用户的数据库
服务端用来管理用户的数据库
--------------------------
保存用户名与密码而已。
## 包管理用的数据库
包管理用的数据库
----------------

View File

@ -1,28 +1,29 @@
# Fk的游戏事件
Fk的游戏事件
============
___
在Fk中“事件”指的大约是像`room:judge`, `room:damage`之类的操作。这些操作一般和某个游戏术语挂钩(“判定”、“伤害”),然后其中包含着一系列操作,比如伤害事件包含了与伤害事件有关的各种触发时机、以及扣减等实际的动作等。
在Fk中“事件”指的大约是像\ ``room:judge``,
``room:damage``\ 之类的操作。这些操作一般和某个游戏术语挂钩(“判定”、“伤害”),然后其中包含着一系列操作,比如伤害事件包含了与伤害事件有关的各种触发时机、以及扣减等实际的动作等。
之所以要把事件单独挑出来聊聊,是因为有以下几点需求:
* 事件要能够被半路中止。
* 对于被中止的事件,要能判断它能不能中止事件栈中的更低层事件。
* 对于被中止的事件,需要做“垃圾回收”(例如将处于处理区的相关卡牌移动到弃牌堆等等)
- 事件要能够被半路中止。
- 对于被中止的事件,要能判断它能不能中止事件栈中的更低层事件。
- 对于被中止的事件,需要做“垃圾回收”(例如将处于处理区的相关卡牌移动到弃牌堆等等)
___
--------------
## 对于如何实现的构想
对于如何实现的构想
------------------
(施工完成后再来修改这一节)
首先是如何实现事件类。初步构想一下,应该有以下的属性/方法:
* 事件名(也可以是枚举值)
* 事件数据(一个表,内含所有要用到的数据)
* 事件id在一局游戏中唯一标记某个事件
* 事件的函数体,也就是具体要做的事情
* 事件被中止或者正常结束时,用来清理现场的函数
- 事件名(也可以是枚举值)
- 事件数据(一个表,内含所有要用到的数据)
- 事件id在一局游戏中唯一标记某个事件
- 事件的函数体,也就是具体要做的事情
- 事件被中止或者正常结束时,用来清理现场的函数
问题来了,既然事件本质上还是个函数体,那要怎么才能中止呢?
@ -30,19 +31,20 @@ ___
事件必然会发生嵌套。所以对此更要慎重考虑。
现在只是设想!假设以`room:judge`入手:
现在只是设想!假设以\ ``room:judge``\ 入手:
```lua
function Room:judge(judgeStruct)
.. code:: lua
function Room:judge(judgeStruct)
local judgeEvent = GameEvent:new(GameEvent.Judge, judgeStruct)
judgeEvent:exec()
end
```
end
总之可能是这样的吧。exec()就是实际执行事件,可能如下:
```lua
function GameEvent:exec()
.. code:: lua
function GameEvent:exec()
local event_f = self.event_f
local co = coroutine.create(event_f)
while true do
@ -56,78 +58,86 @@ function GameEvent:exec()
break
end
end
end
```
end
现在考虑嵌套的情况event1和event2嵌套也就是event1的体里面又创建了event2并exec此时的协程调用关系如下
::
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
.. 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
.. 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会认为协程是挂起的用户可能想要恢复于是一直保存着。

View File

@ -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为必需项。
具体的\ ``fk.CreateTriggerSkill``\ 函数接受一个类型为如上所述的TriggerSkillSpec形式的表。这个表中的属性一共有一下这些
- 所有技能通用的\ ``name``\ 、\ ``anim_type``\ 、\ ``mute``\ 。其中name为必需项。
- global: 是否是全局技能。
- events: 技能的所有触发时机
- can_trigger: 技能能否被触发
- on_trigger: 技能触发时具体的行为
- on_cost: 技能如何执行消耗
- on_use: 技能被发动后,具体的生效内容
- priority: 技能的优先级。在同一时机有多个技能能够被触发时,先触发优先级高的。
- 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,21 +55,22 @@ refresh等一系列函数与前面同理下面会对其展开细说。
这部分相关的代码位于core/skill_type/trigger.lua中。来看看这些函数的默认值
```lua
function TriggerSkill:triggerable(event, target, player, data)
.. code:: lua
function TriggerSkill:triggerable(event, target, player, data)
return target and (target == player)
and (self.global or (target:isAlive() and target:hasSkill(self)))
end
end
function TriggerSkill:trigger(event, target, player, data)
function TriggerSkill:trigger(event, target, player, data)
return self:doCost(event, target, player, data)
end
```
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)
.. code:: lua
function TriggerSkill:doCost(event, target, player, data)
local ret = self:cost(event, target, player, data)
if ret then
local room = player.room
@ -81,15 +82,15 @@ function TriggerSkill:doCost(event, target, player, data)
ret = self:use(event, target, player, data)
return ret
end
end
```
end
这个函数首先调用self:cost即on_cost判断是否返回了true。返回true的话意味着玩家已经完成了消耗技能被正式发动了如果返回true的话那么就认为技能发动了这时会添加技能发动记录、播放配音等行为然后正式执行self:use即on_use。这就是触发技完整的从触发到使用的过程。
现在以鬼才为例packages/standard/init.lua
```lua
local guicai = fk.CreateTriggerSkill{
.. code:: lua
local guicai = fk.CreateTriggerSkill{
name = "guicai",
anim_type = "control",
events = {fk.AskForRetrial},
@ -109,8 +110,7 @@ local guicai = fk.CreateTriggerSkill{
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
View 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

View File

@ -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。

View File

@ -1,14 +1,12 @@
# FreeKill 的通信
FreeKill 的通信
===============
> [dev](./index.md) > 通信
___
## 概述
概述
----
FreeKill使用UTF-8文本进行通信。基本的通信格式为JSON数组
`[requestId, packetType, command, jsonData]`
``[requestId, packetType, command, jsonData]``
其中:
@ -19,17 +17,18 @@ FreeKill使用UTF-8文本进行通信。基本的通信格式为JSON数组
FreeKill通信有三大类型请求Request、回复Reply和通知Notification
___
--------------
## 从连接上到进入大厅
从连接上到进入大厅
------------------
想要启动服务器,需要通过命令行终端:
```sh
$ ./FreeKill -s <port>
```
.. code:: sh
`<port>`是服务器运行的端口号如果不带任何参数则启动GUI界面在GUI界面里面只能加入服务器或者单机游戏。
$ ./FreeKill -s <port>
``<port>``\ 是服务器运行的端口号如果不带任何参数则启动GUI界面在GUI界面里面只能加入服务器或者单机游戏。
服务器以TCP方式监听。在默认情况下比如单机启动服务器的端口号是9527。
@ -39,11 +38,12 @@ $ ./FreeKill -s <port>
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)
.. 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
```
end
在这期间,一共涉及两步走:
@ -84,17 +87,18 @@ end
首先看第一步通知。这里涉及的函数是doNotify。调查notifyMoveFocus的代码即可知道
调查`ServerPlayer:doNotify`发现:
调查\ ``ServerPlayer:doNotify``\ 发现:
.. code:: lua
```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)
{
.. code:: cpp
void ClientSocket::send(const QByteArray &msg)
{
if (msg.length() >= 1024) {
auto comp = qCompress(msg);
auto _msg = "Compressed" + comp.toBase64() + "\n";
@ -105,8 +109,7 @@ void ClientSocket::send(const QByteArray &msg)
if (!msg.endsWith("\n"))
socket->write("\n");
socket->flush();
}
```
}
核心在于socket->write这里其实就调用了QTcpSocket::write正式向网络中发送数据。从前面的分析也慢慢可以发现发送的其实就是json字符串。
@ -116,16 +119,17 @@ void ClientSocket::send(const QByteArray &msg)
其中有这样的一段:
```cpp
.. code:: cpp
if (type & TYPE_NOTIFICATION) {
if (type & DEST_CLIENT) {
ClientInstance->callLua(command, jsonData);
}
```
调用了ClientInstance::callLua函数这个函数不做详细追究只要知道他调用了这个lua函数即可
```lua
.. code:: lua
self.client.callback = function(_self, command, jsonData)
local cb = fk.client_callback[command]
if (type(cb) == "function") then
@ -134,17 +138,17 @@ void ClientSocket::send(const QByteArray &msg)
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
.. code:: lua
while true do
result = player.serverplayer:waitForReply(0)
if result ~= "__notready" then
@ -156,15 +160,15 @@ void ClientSocket::send(const QByteArray &msg)
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。
@ -220,23 +225,24 @@ ___
在这里让出主线程然后调度函数查找目前的请求列表。事实上整个Room的游戏主流程就是一个协程
```lua
-- room.lua:53
local co_func = function()
.. code:: lua
-- room.lua:53
local co_func = function()
self:run()
end
local co = coroutine.create(co_func)
while not self.game_finished do
end
local co = coroutine.create(co_func)
while not self.game_finished do
local ret, err_msg = coroutine.resume(co)
...
end
```
end
如果在游戏流程中调用yield的话那么这里的resume会返回true然后可以带有额外的返回值。不过只要返回true就好了这时候lua就可以做一些简单的任务。而这个简单的任务其实也可以另外写个协程解决。
___
--------------
## 旁观TODO
旁观TODO
------------
因为房间不允许加入比玩家上限的玩家,可以考虑在房间里新建一个列表存储旁观中的玩家。但是这样或许会让某些处理(如掉线)变得复杂化。

View File

@ -1,6 +1,5 @@
# 关于扩展FreeKill玩法的思考
___
关于扩展FreeKill玩法的思考
==========================
要扩展玩法,大概就这些:
@ -14,14 +13,16 @@ ___
3. 加载GameRule后根据模式加载特殊规则
4. 开始玩
___
--------------
## 拓展新规
拓展新规
--------
首先就是如何覆盖老规则这个可以通过设置一个特殊tag
___
--------------
## 拓展logic
拓展logic
---------
从GameLogic继承然后重写有关函数就行

View File

@ -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之类的时机的话这方面就更加重要了。

View File

@ -1,45 +1,47 @@
# 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
- 别的什么页面
___
--------------
## config
config
------
Config.qml存储一些客户端需要用到的设置或者即将发送的数据TODO
---
--------------
## Room和RoomLogic
Room和RoomLogic
---------------
这部分是整个UI体系中最复杂的一部分其中尤以手牌区的操作为甚。下面来整理一下与出牌相关的UI逻辑。
首先要指明一个常用函数:
```cpp
.. code:: cpp
Q_INVOKABLE QString callLuaFunction(const QString &func_name,
QVariantList params);
```
该函数声明位于qmlbackend.h中第一个参数是函数名必须是lua的全局函数第二个列表是参数列表。lua一侧应当返回字符串/数字/布尔值然后再在这里转成QString并返回qml中。这就是qml调用lua函数的核心。
@ -54,8 +56,9 @@ notactive和replying不是本次的重点重点在于playing和responding中
先看Room.qml中关于切换到这两个状态后的动作是什么
```js
Transition {
.. code:: js
Transition {
from: "*"; to: "playing"
ScriptAction {
script: {
@ -67,9 +70,9 @@ Transition {
respond_play = false;
}
}
},
},
Transition {
Transition {
from: "*"; to: "responding"
ScriptAction {
script: {
@ -79,8 +82,7 @@ Transition {
okCancel.visible = true;
}
}
},
```
},
其中涉及到的值得注意的函数是enableCards和enableSkills这里只关心前者。

239
docs/diy/01-env.rst Normal file
View 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 参数。
看来我们初次安装GitGit还不知道我们的身份呢不过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
View 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
View 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
View 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
View 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一一查看或者直接去看素材也行。但是记住一点这个属性除了控制技能触发的特效之外和技能本身并没有任何联系你想指定啥都行。

View 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
View File

@ -0,0 +1,14 @@
与体力值相关的事件
==================
伤害
----
失去体力/体力上限
-----------------
回复体力
--------
濒死和死亡
----------

2
docs/diy/event/misc.rst Normal file
View File

@ -0,0 +1,2 @@
杂项事件
=============

View File

@ -0,0 +1,2 @@
移动牌相关的事件
=====================

View File

@ -0,0 +1,2 @@
使用牌相关的事件
====================

11
docs/diy/index.rst Normal file
View 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
View 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
View File

@ -0,0 +1,2 @@
sphinx-lua
sphinx-rtd-theme

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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}

View File

@ -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)

View File

@ -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

View File

@ -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(";")

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -1,5 +1,5 @@
---@class StatusSkill : Skill
---@field global boolean
---@field public global boolean
local StatusSkill = Skill:subclass("StatusSkill")
function StatusSkill:initialize(name, frequency)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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 = {}

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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