Merge 'standard' (#53)

* todo

* todo.md

* doc for move cards

* weapons excluding qinggang

* equip sound and emotion

* remove silence on the starting of using skill

* add audio skill for TMD equip

* fixbug: running and observing

* when PreUseCard is broken, jump to move cards to discardPile

* doc for diy

* addToPile

* viewPile (WIP)

* fix git bug

* auto update packages when md5 fail

* use thread when updating pack

* correct status() handling

* update fkp

* remove PKGBUILD since it's presented in AUR repo

* fix fkp md5 bug

* extensible qml

* set bigAnim's z to 999

* nioh sheild

* lijian

* now mod can return nil

* if dmg.nature == nil then = normal

* disable notifyUI when qWarning

* fix lijian and gender problem
This commit is contained in:
notify 2023-02-21 13:44:24 +08:00 committed by GitHub
parent 4e1385fa6f
commit afb537a661
527 changed files with 1468 additions and 407 deletions

View File

@ -1,44 +0,0 @@
# Maintainer: Notify-ctrl <notify-ctrl@qq.com>
pkgname=freekill
_upper_pkgname=FreeKill
pkgver=0.0.1
pkgrel=1
arch=('x86_64')
url='https://github.com/Notify-ctrl/FreeKill'
license=('GPL3')
pkgdesc='A Bang-like card game'
depends=('qt6-declarative' 'qt6-multimedia' 'qt6-5compat'
'qt6-shadertools' 'libgit2' 'lua' 'sqlite' 'openssl'
'readline' )
makedepends=('cmake' 'flex' 'bison' 'qt6-tools' 'swig')
# TODO: set source to release tarball
source=("file:///home/notify/develop/FreeKill-demo.tar.gz")
sha256sums=('SKIP')
prepare() {
cd ${srcdir}/${_upper_pkgname}
rm -rf build
}
build() {
cd ${srcdir}/${_upper_pkgname}
mkdir build && cd build
cmake ..
make
}
package() {
mkdir -p ${pkgdir}/usr/share/${_upper_pkgname}
mkdir -p ${pkgdir}/usr/share/icons
mkdir -p ${pkgdir}/usr/share/applications
mkdir -p ${pkgdir}/usr/bin
mkdir -p ${pkgdir}/usr/lib
cd ${srcdir}/${_upper_pkgname}
cmake --install build --prefix ${pkgdir}/usr --config Release
cp -r audio fonts image lua packages qml server build/zh_CN.qm \
${pkgdir}/usr/share/${_upper_pkgname}
install -Dm644 image/icon.png ${pkgdir}/usr/share/icons/freekill_logo.png
install -Dm644 freekill.desktop ${pkgdir}/usr/share/applications/freekill.desktop
}

View File

@ -17,6 +17,7 @@ cp -r ../image assets/res
cp -r ../lua assets/res cp -r ../lua assets/res
# TODO: Windows hosts machine # TODO: Windows hosts machine
cp -r /etc/ca-certificates/extracted/cadir assets/res/certs cp -r /etc/ca-certificates/extracted/cadir assets/res/certs
chmod 644 assets/res/certs/*
mkdir assets/res/packages mkdir assets/res/packages
cp -r ../packages/standard assets/res/packages cp -r ../packages/standard assets/res/packages
cp -r ../packages/standard_cards assets/res/packages cp -r ../packages/standard_cards assets/res/packages

View File

@ -122,6 +122,34 @@ ___
## 移动牌 ## 移动牌
移动牌的核心函数是`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
```
moveCards函数的第一步是将参数中所有的moveInfo都转化为CardsMoveStruct。CardsMoveStruct与CardsMoveInfo几乎没有区别除了它将每一张牌都单独划分出了一个moveinfo之外。这么做是为了在同时移动来源不同的牌的时候让牌能该明牌明牌该暗牌暗牌。
全部转化完成后先针对这个CardsMoveStruct[]触发一次BeforeCardsMove给各种奇怪的触发技修改移动牌信息的机会。如此如此之后就正式开始移动牌了移动完了之后再触发AfterCardsMove这样就完成了对卡牌的移动。
正式移牌中,首先服务器会向各个客户端发送一条消息让客户端知道牌被移动了。
然后对所有的CardsMoveStruct进行遍历根据move.from和move.fromArea获取这张牌的id实际所在的数组然后将这个id移动到目标数组中。如此就在服务端的数据层面移动了一张牌。移牌OK后Room会更新这张牌的位置信息然后视情况更新这张牌的锁定视为技信息。如果是装备牌的话那么就做一些跟装备技能有关的事情。
___ ___
## 使用牌 ## 使用牌
使用一张牌应该是全游戏最复杂而又最常见的一种事件了。说他复杂,其实也是被狗卡各种乱七八糟的技能和规则搞得很复杂的。
使用牌的核心函数是`Room:useCard`接收的参数是CardUseStruct。不行太复杂了过一阵子再来看吧。

42
doc/dev/todo.md Normal file
View File

@ -0,0 +1,42 @@
# TODO list
___
本文档用来记载一些可能会需要实现但暂且无暇的想法。留待日后再做或者同伴帮忙做了。
## 服务端的包验证
当客户端连接到服务端遇到MD5失败时考虑
1. 服务端除了告知失败之外还告知客户端自己的打包属性即自己启用了哪些包包的URL和版本等。
2. 客户端根据信息禁用掉不需要的包,下载没有的包,更新启用的包,将需要的包都切换到服务器提供的版本。
___
## UI主题可拓展
考虑影响一下skin-bank.js使其根据某个config的不同将相应的值设为不同的路径。然后确保所有图片资源关于页面和logo除外都通过skin-bank.js访问所需图片。
由于所有拓展包都是只能通过init.lua访问考虑为QmlBackend提供相应的Lua接口使其能够注册新的配置方案。配置文件本身的组织考虑JSON。
考虑在skin-bank.js中加入更多信息例如各个组件的x, y, width, height等。因为影响到的都是Image所以设置这些应该是够了。
___
## 对话框可拓展
考虑劫营,严教等含有特殊交互框的例子。
可以通过类似fk.Backend:loadDialog的方式弹出对话框。在Lua文件中相应的地方影响client_callback这张全局表。但同时考虑到拓展包比server/client相关的Lua先加载或许需要一些特殊的办法比如弄一张全局表然后client.lua初始回调函数列表的时候先把那个表的结果加进去之类的。
至于Lua如何与QML进行交互毫无疑问的是通过JSON字符串。QML最后需要自己将数据发回给服务端这里需要用到ClientInstance手动发送了不能用RoomLogic.js。同样的需要有统一的方法初始化QML对话框的数据考虑都传入JSON字符串然后QML在Component.onComplete时候加载初始化数据。
skin-bank.js的话依然可以用相对位置进行加载这个理论上应该是不会被影响到。
___
## 代码简洁化
目前FK的lua代码中仍有不少地方的代码重用度不高典型的例子是fk_ex.lua。考虑在这里用local function将重复代码合并一下。
还有视为技/触发技/主动技这些能够被“发动”的技能即继承于UsableSkill的基本它们的技能生效环节都有很多重复比如播放声音动画和具体生效等。考虑在某处用一个函数总结一下至于具体生效部分可以包在一个函数里面`function() skill:onEffect(room, effect) end`然后作为参数传递到useskill函数中。如果后面要做SkillUsed之类的时机的话这方面就更加重要了。

227
doc/diy/01-env.md Normal file
View File

@ -0,0 +1,227 @@
# 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)

45
doc/diy/02-skilltype.md Normal file
View File

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

9
doc/diy/03-events.md Normal file
View File

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

33
doc/diy/event/gameflow.md Normal file
View File

@ -0,0 +1,33 @@
# 与游戏流程有关的事件
先来看游戏流程本身。以下节选自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

11
doc/diy/event/hp.md Normal file
View File

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

0
doc/diy/event/misc.md Normal file
View File

View File

0
doc/diy/event/usecard.md Normal file
View File

View File

@ -3,3 +3,14 @@
> diy > diy
___ ___
以下是一系列文档旨在向从未接触过FreeKill以后简称为Fk的DIYer介绍Fk的DIY接口以及如何打包、发布。
本系列文档针对对神杀Lua有基础的读者编写。
由于对于Win系统而言fk仅仅支持Win 10及以上的64位系统因此本文档假设您正使用Windows 10作为操作系统且使用着64位的处理器。
文档中蓝色字均为超链接。
1. [环境搭建](./01-env.md)
2. [fk技能类型总览](./02-skilltype.md)

@ -1 +1 @@
Subproject commit d150d2eec986c49a16f9e84772525a4fb7a84926 Subproject commit 64481662879765f5631a291d512f7a74125051f3

View File

@ -51,6 +51,10 @@
<source>PackageManage</source> <source>PackageManage</source>
<translation></translation> <translation></translation>
</message> </message>
<message>
<source>updated packages for md5</source>
<translation></translation>
</message>
</context> </context>
<context> <context>

View File

@ -71,14 +71,14 @@ function Client:moveCards(moves)
table.remove(from.player_cards[Player.Hand]) table.remove(from.player_cards[Player.Hand])
end end
else else
from:removeCards(move.fromArea, move.ids) from:removeCards(move.fromArea, move.ids, move.fromSpecialName)
end end
elseif move.fromArea == Card.DiscardPile then elseif move.fromArea == Card.DiscardPile then
table.removeOne(self.discard_pile, move.ids[1]) table.removeOne(self.discard_pile, move.ids[1])
end end
if move.to and move.toArea then if move.to and move.toArea then
self:getPlayerById(move.to):addCards(move.toArea, move.ids) self:getPlayerById(move.to):addCards(move.toArea, move.ids, move.specialName)
elseif move.toArea == Card.DiscardPile then elseif move.toArea == Card.DiscardPile then
table.insert(self.discard_pile, move.ids[1]) table.insert(self.discard_pile, move.ids[1])
end end
@ -294,6 +294,8 @@ local function separateMoves(moves)
toArea = move.toArea, toArea = move.toArea,
fromArea = info.fromArea, fromArea = info.fromArea,
moveReason = move.moveReason, moveReason = move.moveReason,
specialName = move.specialName,
fromSpecialName = info.fromSpecialName,
}) })
end end
end end
@ -305,8 +307,9 @@ local function mergeMoves(moves)
local ret = {} local ret = {}
local temp = {} local temp = {}
for _, move in ipairs(moves) do for _, move in ipairs(moves) do
local info = string.format("%q,%q,%q,%q", local info = string.format("%q,%q,%q,%q,%s,%s",
move.from, move.to, move.fromArea, move.toArea) move.from, move.to, move.fromArea, move.toArea,
move.specialName, move.fromSpecialName)
if temp[info] == nil then if temp[info] == nil then
temp[info] = { temp[info] = {
ids = {}, ids = {},
@ -315,6 +318,8 @@ local function mergeMoves(moves)
fromArea = move.fromArea, fromArea = move.fromArea,
toArea = move.toArea, toArea = move.toArea,
moveReason = move.moveReason, moveReason = move.moveReason,
specialName = move.specialName,
fromSpecialName = move.fromSpecialName,
} }
end end
table.insert(temp[info].ids, move.ids[1]) table.insert(temp[info].ids, move.ids[1])

View File

@ -117,6 +117,10 @@ function DistanceTo(from, to)
return a:distanceTo(b) return a:distanceTo(b)
end end
function GetPile(id, name)
return json.encode(ClientInstance:getPlayerById(id):getPile(name) or {})
end
---@param card string | integer ---@param card string | integer
---@param player integer ---@param player integer
function CanUseCard(card, player) function CanUseCard(card, player)
@ -138,7 +142,7 @@ function CanUseCard(card, player)
end end
end end
local ret = c.skill:canUse(ClientInstance:getPlayerById(player)) local ret = c.skill:canUse(ClientInstance:getPlayerById(player), c)
return json.encode(ret) return json.encode(ret)
end end
@ -160,7 +164,7 @@ function CanUseCardToTarget(card, to_select, selected)
return ActiveTargetFilter(t.skill, to_select, selected, t.subcards) return ActiveTargetFilter(t.skill, to_select, selected, t.subcards)
end end
local ret = c.skill:targetFilter(to_select, selected, selected_cards) local ret = c.skill:targetFilter(to_select, selected, selected_cards, c)
if ret then if ret then
local r = Fk:currentRoom() local r = Fk:currentRoom()
local status_skills = r.status_skills[ProhibitSkill] or {} local status_skills = r.status_skills[ProhibitSkill] or {}
@ -242,7 +246,7 @@ function ActiveCanUse(skill_name)
end end
for _, n in ipairs(cnames) do for _, n in ipairs(cnames) do
local c = Fk:cloneCard(n) local c = Fk:cloneCard(n)
ret = c.skill:canUse(Self) ret = c.skill:canUse(Self, c)
if ret then break end if ret then break end
end end
end end

View File

@ -59,7 +59,9 @@ function Card:initialize(name, suit, number, color)
self.name = name self.name = name
self.suit = suit or Card.NoSuit self.suit = suit or Card.NoSuit
self.number = number or 0 self.number = number or 0
self.trueName = name
local name_splited = name:split("__")
self.trueName = name_splited[#name_splited]
if suit == Card.Spade or suit == Card.Club then if suit == Card.Spade or suit == Card.Club then
self.color = Card.Black self.color = Card.Black
@ -217,4 +219,26 @@ function Card:toLogString()
return ret return ret
end end
---@param c integer|integer[]|Card|Card[]
---@return integer[]
function Card.static:getIdList(c)
if type(c) == "number" then
return {c}
end
if c.class and c:isInstanceOf(Card) then
if c:isVirtual() then
return table.clone(c.subcards)
else
return {c.id}
end
end
-- array
local ret = {}
for _, c2 in ipairs(c) do
table.insertTable(ret, Card:getIdList(c))
end
return ret
end
return Card return Card

View File

@ -68,12 +68,14 @@ function Engine:loadPackages()
local pack = require(string.format("packages.%s", dir)) local pack = require(string.format("packages.%s", dir))
-- Note that instance of Package is a table too -- Note that instance of Package is a table too
-- so dont use type(pack) == "table" here -- so dont use type(pack) == "table" here
if pack[1] ~= nil then if type(pack) == "table" then
for _, p in ipairs(pack) do if pack[1] ~= nil then
self:loadPackage(p) for _, p in ipairs(pack) do
self:loadPackage(p)
end
else
self:loadPackage(pack)
end end
else
self:loadPackage(pack)
end end
end end
end end

View File

@ -259,6 +259,19 @@ function Player:getCardIds(playerAreas, specialName)
return cardIds return cardIds
end end
---@param name string
function Player:getPile(name)
return self.special_cards[name] or {}
end
---@param id integer
---@return string|null
function Player:getPileNameOfId(id)
for k, v in pairs(self.special_cards) do
if table.contains(v, id) then return k end
end
end
-- for fkp only -- for fkp only
function Player:getHandcardNum() function Player:getHandcardNum()
return #self:getCardIds(Player.Hand) return #self:getCardIds(Player.Hand)

View File

@ -3,19 +3,19 @@ local TargetModSkill = StatusSkill:subclass("TargetModSkill")
---@param player Player ---@param player Player
---@param card_skill ActiveSkill ---@param card_skill ActiveSkill
function TargetModSkill:getResidueNum(player, card_skill, scope) function TargetModSkill:getResidueNum(player, card_skill, scope, card)
return 0 return 0
end end
---@param player Player ---@param player Player
---@param card_skill ActiveSkill ---@param card_skill ActiveSkill
function TargetModSkill:getDistanceLimit(player, card_skill) function TargetModSkill:getDistanceLimit(player, card_skill, card)
return 0 return 0
end end
---@param player Player ---@param player Player
---@param card_skill ActiveSkill ---@param card_skill ActiveSkill
function TargetModSkill:getExtraTargetNum(player, card_skill) function TargetModSkill:getExtraTargetNum(player, card_skill, card)
return 0 return 0
end end

View File

@ -56,14 +56,9 @@ end
function TriggerSkill:doCost(event, target, player, data) function TriggerSkill:doCost(event, target, player, data)
local ret = self:cost(event, target, player, data) local ret = self:cost(event, target, player, data)
if ret then if ret then
local room = player.room return player.room:useSkill(player, self, function()
if not self.mute then return self:use(event, target, player, data)
room:broadcastSkillInvoke(self.name) end)
end
room:notifySkillInvoked(player, self.name)
player:addSkillUseHistory(self.name)
ret = self:use(event, target, player, data)
return ret
end end
end end

View File

@ -14,39 +14,39 @@ function UsableSkill:initialize(name, frequency)
end end
---@param player Player ---@param player Player
function UsableSkill:getMinTargetNum(player) function UsableSkill:getMinTargetNum(player, card)
local ret = type(self.target_num) == "table" and self.target_num[1] or self.target_num local ret = type(self.target_num) == "table" and self.target_num[1] or self.target_num
return ret return ret
end end
function UsableSkill:getMaxTargetNum(player) function UsableSkill:getMaxTargetNum(player, card)
local ret = type(self.target_num) == "table" and self.target_num[2] or self.target_num local ret = type(self.target_num) == "table" and self.target_num[2] or self.target_num
local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or {} local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or {}
for _, skill in ipairs(status_skills) do for _, skill in ipairs(status_skills) do
local correct = skill:getExtraTargetNum(player, self) local correct = skill:getExtraTargetNum(player, self, card)
if correct == nil then correct = 0 end if correct == nil then correct = 0 end
ret = ret + correct ret = ret + correct
end end
return ret return ret
end end
function UsableSkill:getMaxUseTime(player, scope) function UsableSkill:getMaxUseTime(player, scope, card)
scope = scope or Player.HistoryTurn scope = scope or Player.HistoryTurn
local ret = self.max_use_time[scope] local ret = self.max_use_time[scope]
local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or {} local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or {}
for _, skill in ipairs(status_skills) do for _, skill in ipairs(status_skills) do
local correct = skill:getResidueNum(player, self, scope) local correct = skill:getResidueNum(player, self, scope, card)
if correct == nil then correct = 0 end if correct == nil then correct = 0 end
ret = ret + correct ret = ret + correct
end end
return ret return ret
end end
function UsableSkill:getDistanceLimit(player) function UsableSkill:getDistanceLimit(player, card)
local ret = self.distance_limit local ret = self.distance_limit
local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or {} local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or {}
for _, skill in ipairs(status_skills) do for _, skill in ipairs(status_skills) do
local correct = skill:getDistanceLimit(player, self) local correct = skill:getDistanceLimit(player, self, card)
if correct == nil then correct = 0 end if correct == nil then correct = 0 end
ret = ret + correct ret = ret + correct
end end

View File

@ -9,6 +9,57 @@ function fk.qlist(list)
return qlist_iterator, list, -1 return qlist_iterator, list, -1
end end
---@param func fun(element, index, array)
function table:forEach(func)
for i, v in ipairs(self) do
func(v, i, self)
end
end
---@param func fun(element, index, array)
function table:every(func)
for i, v in ipairs(self) do
if not func(v, i, self) then
return false
end
end
return true
end
---@generic T
---@param self T[]
---@param func fun(element, index, array)
---@return T[]
function table.filter(self, func)
local ret = {}
for i, v in ipairs(self) do
if func(v, i, self) then
table.insert(ret, v)
end
end
return ret
end
---@param func fun(element, index, array)
function table.map(self, func)
local ret = {}
for i, v in ipairs(self) do
table.insert(ret, func(v, i, self))
end
return ret
end
---@generic T
---@param self T[]
---@return T[]
function table.reverse(self)
local ret = {}
for _, e in ipairs(self) do
table.insert(ret, 1, e)
end
return ret
end
function table:contains(element) function table:contains(element)
if #self == 0 then return false end if #self == 0 then return false end
for _, e in ipairs(self) do for _, e in ipairs(self) do
@ -120,7 +171,7 @@ Sql = {
--- Execute a `SELECT` SQL statement. --- Execute a `SELECT` SQL statement.
---@param db fk.SQLite3 ---@param db fk.SQLite3
---@param sql string ---@param sql string
---@return table @ { [columnName] --> result : string[] } ---@return table[] @ Array of Json object, the key is column name and value is row value
exec_select = function(db, sql) exec_select = function(db, sql)
return json.decode(fk.SelectFromDb(db, sql)) return json.decode(fk.SelectFromDb(db, sql))
end, end,

View File

@ -355,6 +355,7 @@ end
---@class CardSpec: Card ---@class CardSpec: Card
---@field skill Skill ---@field skill Skill
---@field equip_skill Skill
local defaultCardSkill = fk.CreateActiveSkill{ local defaultCardSkill = fk.CreateActiveSkill{
name = "default_card_skill", name = "default_card_skill",

View File

@ -96,6 +96,62 @@ end
fkp.functions.hasSkill = function(p, s) return p:hasSkill(s) end fkp.functions.hasSkill = function(p, s) return p:hasSkill(s) end
fkp.functions.turnOver = function(p) p:turnOver() end fkp.functions.turnOver = function(p) p:turnOver() end
fkp.functions.distanceTo = function(p1, p2) return p1:distanceTo(p2) end fkp.functions.distanceTo = function(p1, p2) return p1:distanceTo(p2) end
fkp.functions.getCards = function(p, area)
return table.map(p:getCardIds(area), function(id) return Fk:getCardById(id) end)
end
-- interactive methods
fkp.functions.buildPrompt = function(base, src, dest, arg, arg2)
if src == nil then
src = ""
else
src = src.id
end
if dest == nil then
dest = ""
else
dest = dest.id
end
if arg == nil then arg = "" end
if arg2 == nil then arg2 = "" end
local prompt_tab = {src, dest, arg, arg2}
if arg2 == "" then
table.remove(prompt_tab, 4)
if arg == "" then
table.remove(prompt_tab, 3)
if dest == "" then
table.remove(prompt_tab, 2)
if src == "" then
table.remove(prompt_tab, 1)
end
end
end
end
for _, str in ipairs(prompt_tab) do
base = base .. ":" .. str
end
return base
end
fkp.functions.askForChoice = function(player, choices, reason)
return player.room:askForChoice(player, choices, reason)
end
fkp.functions.askForPlayerChosen = function(player, targets, reason, prompt, optional, notify)
return player.room:askForChoosePlayers(player, targets, 1, 1, prompt, reason)
end
fkp.functions.askForSkillInvoke = function(player, skill)
return player:askForSkillInvoke(skill)
end
fkp.functions.askRespondForCard = function(player, pattern, prompt, isRetrial, skill_name)
return player.room:askForResponse(player, skill_name, pattern, prompt, true)
end
-- skill prototypes -- skill prototypes
-------------------------------------------- --------------------------------------------
@ -277,18 +333,18 @@ fkp.CreateTargetModSkill = function(_spec)
return Fk:cloneCard(str) return Fk:cloneCard(str)
end end
if _spec.residue_func then if _spec.residue_func then
spec.residue_func = function(self, target, skill, scope) spec.residue_func = function(self, target, skill, scope, card)
return _spec.residue_func(self, target, getVCardFromActiveSkill(skill)) return _spec.residue_func(self, target, card or getVCardFromActiveSkill(skill))
end end
end end
if _spec.distance_limit_func then if _spec.distance_limit_func then
spec.distance_limit_func = function(self, target, skill) spec.distance_limit_func = function(self, target, skill, card)
return _spec.distance_limit_func(self, target, getVCardFromActiveSkill(skill)) return _spec.distance_limit_func(self, target, card or getVCardFromActiveSkill(skill))
end end
end end
if _spec.extra_target_func then if _spec.extra_target_func then
spec.extra_target_func = function(self, target, skill) spec.extra_target_func = function(self, target, skill, card)
return _spec.extra_target_func(self, target, getVCardFromActiveSkill(skill)) return _spec.extra_target_func(self, target, card or getVCardFromActiveSkill(skill))
end end
end end
return fk.CreateTargetModSkill(spec) return fk.CreateTargetModSkill(spec)

View File

@ -63,7 +63,7 @@ function GameLogic:chooseGenerals()
player.general = general player.general = general
player.gender = Fk.generals[general].gender player.gender = Fk.generals[general].gender
self.room:notifyProperty(player, player, "general") self.room:notifyProperty(player, player, "general")
self.room:notifyProperty(player, player, "gender") self.room:broadcastProperty(player, "gender")
end end
local lord = room:getLord() local lord = room:getLord()
local lord_general = nil local lord_general = nil

View File

@ -491,9 +491,9 @@ end
-- delay function, should only be used in main coroutine -- delay function, should only be used in main coroutine
---@param ms integer @ millisecond to be delayed ---@param ms integer @ millisecond to be delayed
function Room:delay(ms) function Room:delay(ms)
local start = fk.GetMicroSecond() local start = os.getms()
while true do while true do
if fk.GetMicroSecond() - start >= ms * 1000 then if os.getms() - start >= ms * 1000 then
break break
end end
coroutine.yield() coroutine.yield()
@ -521,9 +521,9 @@ function Room:notifyMoveCards(players, card_moves, forceVisible)
-- forceVisible make the move visible -- forceVisible make the move visible
-- FIXME: move.moveInfo is an array, fix this -- FIXME: move.moveInfo is an array, fix this
move.moveVisible = (forceVisible) move.moveVisible = move.moveVisible or (forceVisible)
-- if move is relevant to player, it should be open -- if move is relevant to player, it should be open
or ((move.from == p.id) or (move.to == p.id and move.toArea ~= Card.PlayerSpecial)) or ((move.from == p.id) or (move.to == p.id))
-- cards move from/to equip/judge/discard/processing should be open -- cards move from/to equip/judge/discard/processing should be open
or infosContainArea(move.moveInfo, Card.PlayerEquip) or infosContainArea(move.moveInfo, Card.PlayerEquip)
or move.toArea == Card.PlayerEquip or move.toArea == Card.PlayerEquip
@ -589,6 +589,14 @@ function Room:setCardEmotion(cid, name)
}) })
end end
function Room:doSuperLightBox(path, extra_data)
path = path or "RoomElement/SuperLightBox.qml"
self:doAnimate("SuperLightBox", {
path = path,
data = extra_data,
})
end
function Room:sendLogEvent(type, data, players) function Room:sendLogEvent(type, data, players)
players = players or self.players players = players or self.players
data.type = type data.type = type
@ -885,26 +893,19 @@ function Room:handleUseCardReply(player, data)
local skill = Fk.skills[card_data.skill] local skill = Fk.skills[card_data.skill]
local selected_cards = card_data.subcards local selected_cards = card_data.subcards
if skill:isInstanceOf(ActiveSkill) then if skill:isInstanceOf(ActiveSkill) then
if not skill.mute then self:useSkill(player, skill, function()
self:broadcastSkillInvoke(skill.name) self:doIndicate(player.id, targets)
end skill:onUse(self, {
self:notifySkillInvoked(player, skill.name) from = player.id,
player:addSkillUseHistory(skill.name) cards = selected_cards,
self:doIndicate(player.id, targets) tos = targets,
skill:onUse(self, { })
from = player.id, end)
cards = selected_cards,
tos = targets,
})
return nil return nil
elseif skill:isInstanceOf(ViewAsSkill) then elseif skill:isInstanceOf(ViewAsSkill) then
local c = skill:viewAs(selected_cards) local c = skill:viewAs(selected_cards)
if c then if c then
if not skill.mute then self:useSkill(player, skill)
self:broadcastSkillInvoke(skill.name)
end
self:notifySkillInvoked(player, skill.name)
player:addSkillUseHistory(skill.name)
local use = {} ---@type CardUseStruct local use = {} ---@type CardUseStruct
use.from = player.id use.from = player.id
use.tos = {} use.tos = {}
@ -1004,10 +1005,52 @@ function Room:askForNullification(players, card_name, pattern, prompt, cancelabl
return nil return nil
end end
-- Show a qml dialog and return qml's ClientInstance.replyToServer
-- Do anything you like through this function
---@param player ServerPlayer
---@param focustxt string
---@param qmlPath string
---@param extra_data any
---@return string
function Room:askForCustomDialog(player, focustxt, qmlPath, extra_data)
local command = "CustomDialog"
self:notifyMoveFocus(player, focustxt)
return self:doRequest(player, command, json.encode{
path = qmlPath,
data = extra_data,
})
end
------------------------------------------------------------------------ ------------------------------------------------------------------------
-- use card logic, and wrappers -- use card logic, and wrappers
------------------------------------------------------------------------ ------------------------------------------------------------------------
local playCardEmotionAndSound = function(room, player, card)
if card.type ~= Card.TypeEquip then
room:setEmotion(player, "./packages/" ..
card.package.extensionName .. "/image/anim/" .. card.name)
end
local soundName
if card.type == Card.TypeEquip then
local subTypeStr
if card.sub_type == Card.SubtypeDefensiveRide or card.sub_type == Card.SubtypeOffensiveRide then
subTypeStr = "horse"
elseif card.sub_type == Card.SubtypeWeapon then
subTypeStr = "weapon"
else
subTypeStr = "armor"
end
soundName = "./audio/card/common/" .. subTypeStr
else
soundName = "./packages/" .. card.package.extensionName .. "/audio/card/"
.. (player.gender == General.Male and "male/" or "female/") .. card.name
end
room:broadcastPlaySound(soundName)
end
---@param room Room ---@param room Room
---@param cardUseEvent CardUseStruct ---@param cardUseEvent CardUseStruct
local sendCardEmotionAndLog = function(room, cardUseEvent) local sendCardEmotionAndLog = function(room, cardUseEvent)
@ -1023,26 +1066,7 @@ local sendCardEmotionAndLog = function(room, cardUseEvent)
card = temp.card card = temp.card
end end
room:setEmotion(room:getPlayerById(from), card.name) playCardEmotionAndSound(room, room:getPlayerById(from), card)
local soundName
if card.type == Card.TypeEquip then
local subTypeStr
if card.sub_type == Card.SubtypeDefensiveRide or card.sub_type == Card.SubtypeOffensiveRide then
subTypeStr = "horse"
elseif card.sub_type == Card.SubtypeWeapon then
subTypeStr = "weapon"
else
subTypeStr = "armor"
end
soundName = "./audio/card/common/" .. subTypeStr
else
soundName = "./packages/" .. card.package.extensionName .. "/audio/card/"
.. (room:getPlayerById(from).gender == General.Male and "male/" or "female/") .. card.name
end
room:broadcastPlaySound(soundName)
room:doAnimate("Indicate", { room:doAnimate("Indicate", {
from = from, from = from,
to = cardUseEvent.tos or {}, to = cardUseEvent.tos or {},
@ -1272,7 +1296,7 @@ function Room:useCard(cardUseEvent)
sendCardEmotionAndLog(self, cardUseEvent) sendCardEmotionAndLog(self, cardUseEvent)
if self.logic:trigger(fk.PreCardUse, self:getPlayerById(cardUseEvent.from), cardUseEvent) then if self.logic:trigger(fk.PreCardUse, self:getPlayerById(cardUseEvent.from), cardUseEvent) then
return false goto clean
end end
if not cardUseEvent.extraUse then if not cardUseEvent.extraUse then
@ -1291,153 +1315,13 @@ function Room:useCard(cardUseEvent)
self.logic:trigger(event, self:getPlayerById(cardUseEvent.from), cardUseEvent) self.logic:trigger(event, self:getPlayerById(cardUseEvent.from), cardUseEvent)
if event == fk.CardUsing then if event == fk.CardUsing then
---@type table<string, AimStruct> self:doCardUseEffect(cardUseEvent)
local aimEventCollaborators = {}
if cardUseEvent.tos and not onAim(self, cardUseEvent, aimEventCollaborators) then
break
end
local realCardIds = self:getSubcardsByRule(cardUseEvent.card, { Card.Processing })
if cardUseEvent.card.type == Card.TypeEquip then
if #realCardIds == 0 then
break
end
if self:getPlayerById(TargetGroup:getRealTargets(cardUseEvent.tos)[1]).dead then
self.moveCards({
ids = realCardIds,
toArea = Card.DiscardPile,
moveReason = fk.ReasonPutIntoDiscardPile,
})
else
local target = TargetGroup:getRealTargets(cardUseEvent.tos)[1]
local existingEquipId = self:getPlayerById(target):getEquipment(cardUseEvent.card.sub_type)
if existingEquipId then
self:moveCards(
{
ids = { existingEquipId },
from = target,
toArea = Card.DiscardPile,
moveReason = fk.ReasonPutIntoDiscardPile,
},
{
ids = realCardIds,
to = target,
toArea = Card.PlayerEquip,
moveReason = fk.ReasonUse,
}
)
else
self:moveCards({
ids = realCardIds,
to = target,
toArea = Card.PlayerEquip,
moveReason = fk.ReasonUse,
})
end
end
break
elseif cardUseEvent.card.sub_type == Card.SubtypeDelayedTrick then
if #realCardIds == 0 then
break
end
local target = TargetGroup:getRealTargets(cardUseEvent.tos)[1]
if not self:getPlayerById(target).dead then
local findSameCard = false
for _, cardId in ipairs(self:getPlayerById(target):getCardIds(Player.Judge)) do
if Fk:getCardById(cardId).trueName == cardUseEvent.card.trueName then
findSameCard = true
end
end
if not findSameCard then
if cardUseEvent.card:isVirtual() then
self:getPlayerById(target):addVirtualEquip(cardUseEvent.card)
end
self:moveCards({
ids = realCardIds,
to = target,
toArea = Card.PlayerJudge,
moveReason = fk.ReasonUse,
})
break
end
end
self:moveCards({
ids = realCardIds,
toArea = Card.DiscardPile,
moveReason = fk.ReasonPutIntoDiscardPile,
})
break
end
if cardUseEvent.card.skill then
---@type CardEffectEvent
local cardEffectEvent = {
from = cardUseEvent.from,
tos = cardUseEvent.tos,
card = cardUseEvent.card,
toCard = cardUseEvent.toCard,
responseToEvent = cardUseEvent.responseToEvent,
nullifiedTargets = cardUseEvent.nullifiedTargets,
disresponsiveList = cardUseEvent.disresponsiveList,
unoffsetableList = cardUseEvent.unoffsetableList,
addtionalDamage = cardUseEvent.addtionalDamage,
cardIdsResponded = cardUseEvent.nullifiedTargets,
}
if cardUseEvent.toCard ~= nil then
self:doCardEffect(cardEffectEvent)
else
local collaboratorsIndex = {}
for _, toId in ipairs(TargetGroup:getRealTargets(cardUseEvent.tos)) do
if not table.contains(cardUseEvent.nullifiedTargets, toId) and self:getPlayerById(toId):isAlive() then
if aimEventCollaborators[toId] then
cardEffectEvent.to = toId
collaboratorsIndex[toId] = collaboratorsIndex[toId] or 1
local curAimEvent = aimEventCollaborators[toId][collaboratorsIndex[toId]]
cardEffectEvent.subTargets = curAimEvent.subTargets
cardEffectEvent.addtionalDamage = curAimEvent.additionalDamage
if curAimEvent.disresponsiveList then
for _, disresponsivePlayer in ipairs(curAimEvent.disresponsiveList) do
if not table.contains(cardEffectEvent.disresponsiveList, disresponsivePlayer) then
table.insert(cardEffectEvent.disresponsiveList, disresponsivePlayer)
end
end
end
if curAimEvent.unoffsetableList then
for _, unoffsetablePlayer in ipairs(curAimEvent.unoffsetableList) do
if not table.contains(cardEffectEvent.unoffsetablePlayer, unoffsetablePlayer) then
table.insert(cardEffectEvent.unoffsetablePlayer, unoffsetablePlayer)
end
end
end
cardEffectEvent.disresponsive = curAimEvent.disresponsive
cardEffectEvent.unoffsetable = curAimEvent.unoffsetable
collaboratorsIndex[toId] = collaboratorsIndex[toId] + 1
self:doCardEffect(table.simpleClone(cardEffectEvent))
end
end
end
end
end
end end
end end
self.logic:trigger(fk.CardUseFinished, self:getPlayerById(cardUseEvent.from), cardUseEvent) self.logic:trigger(fk.CardUseFinished, self:getPlayerById(cardUseEvent.from), cardUseEvent)
::clean::
local leftRealCardIds = self:getSubcardsByRule(cardUseEvent.card, { Card.Processing }) local leftRealCardIds = self:getSubcardsByRule(cardUseEvent.card, { Card.Processing })
if #leftRealCardIds > 0 then if #leftRealCardIds > 0 then
self:moveCards({ self:moveCards({
@ -1448,6 +1332,159 @@ function Room:useCard(cardUseEvent)
end end
end end
---@param cardUseEvent CardUseStruct
function Room:doCardUseEffect(cardUseEvent)
---@type table<string, AimStruct>
local aimEventCollaborators = {}
if cardUseEvent.tos and not onAim(self, cardUseEvent, aimEventCollaborators) then
return
end
local realCardIds = self:getSubcardsByRule(cardUseEvent.card, { Card.Processing })
-- If using Equip or Delayed trick, move them to the area and return
if cardUseEvent.card.type == Card.TypeEquip then
if #realCardIds == 0 then
return
end
if self:getPlayerById(TargetGroup:getRealTargets(cardUseEvent.tos)[1]).dead then
self.moveCards({
ids = realCardIds,
toArea = Card.DiscardPile,
moveReason = fk.ReasonPutIntoDiscardPile,
})
else
local target = TargetGroup:getRealTargets(cardUseEvent.tos)[1]
local existingEquipId = self:getPlayerById(target):getEquipment(cardUseEvent.card.sub_type)
if existingEquipId then
self:moveCards(
{
ids = { existingEquipId },
from = target,
toArea = Card.DiscardPile,
moveReason = fk.ReasonPutIntoDiscardPile,
},
{
ids = realCardIds,
to = target,
toArea = Card.PlayerEquip,
moveReason = fk.ReasonUse,
}
)
else
self:moveCards({
ids = realCardIds,
to = target,
toArea = Card.PlayerEquip,
moveReason = fk.ReasonUse,
})
end
end
return
elseif cardUseEvent.card.sub_type == Card.SubtypeDelayedTrick then
if #realCardIds == 0 then
return
end
local target = TargetGroup:getRealTargets(cardUseEvent.tos)[1]
if not self:getPlayerById(target).dead then
local findSameCard = false
for _, cardId in ipairs(self:getPlayerById(target):getCardIds(Player.Judge)) do
if Fk:getCardById(cardId).trueName == cardUseEvent.card.trueName then
findSameCard = true
end
end
if not findSameCard then
if cardUseEvent.card:isVirtual() then
self:getPlayerById(target):addVirtualEquip(cardUseEvent.card)
end
self:moveCards({
ids = realCardIds,
to = target,
toArea = Card.PlayerJudge,
moveReason = fk.ReasonUse,
})
return
end
end
self:moveCards({
ids = realCardIds,
toArea = Card.DiscardPile,
moveReason = fk.ReasonPutIntoDiscardPile,
})
return
end
if not cardUseEvent.card.skill then
return
end
---@type CardEffectEvent
local cardEffectEvent = {
from = cardUseEvent.from,
tos = cardUseEvent.tos,
card = cardUseEvent.card,
toCard = cardUseEvent.toCard,
responseToEvent = cardUseEvent.responseToEvent,
nullifiedTargets = cardUseEvent.nullifiedTargets,
disresponsiveList = cardUseEvent.disresponsiveList,
unoffsetableList = cardUseEvent.unoffsetableList,
addtionalDamage = cardUseEvent.addtionalDamage,
cardIdsResponded = cardUseEvent.nullifiedTargets,
}
-- If using card to other card (like jink or nullification), simply effect and return
if cardUseEvent.toCard ~= nil then
self:doCardEffect(cardEffectEvent)
return
end
-- Else: do effect to all targets
local collaboratorsIndex = {}
for _, toId in ipairs(TargetGroup:getRealTargets(cardUseEvent.tos)) do
if not table.contains(cardUseEvent.nullifiedTargets, toId) and self:getPlayerById(toId):isAlive() then
if aimEventCollaborators[toId] then
cardEffectEvent.to = toId
collaboratorsIndex[toId] = collaboratorsIndex[toId] or 1
local curAimEvent = aimEventCollaborators[toId][collaboratorsIndex[toId]]
cardEffectEvent.subTargets = curAimEvent.subTargets
cardEffectEvent.addtionalDamage = curAimEvent.additionalDamage
if curAimEvent.disresponsiveList then
for _, disresponsivePlayer in ipairs(curAimEvent.disresponsiveList) do
if not table.contains(cardEffectEvent.disresponsiveList, disresponsivePlayer) then
table.insert(cardEffectEvent.disresponsiveList, disresponsivePlayer)
end
end
end
if curAimEvent.unoffsetableList then
for _, unoffsetablePlayer in ipairs(curAimEvent.unoffsetableList) do
if not table.contains(cardEffectEvent.unoffsetablePlayer, unoffsetablePlayer) then
table.insert(cardEffectEvent.unoffsetablePlayer, unoffsetablePlayer)
end
end
end
cardEffectEvent.disresponsive = curAimEvent.disresponsive
cardEffectEvent.unoffsetable = curAimEvent.unoffsetable
collaboratorsIndex[toId] = collaboratorsIndex[toId] + 1
self:doCardEffect(table.simpleClone(cardEffectEvent))
end
end
end
end
---@param cardEffectEvent CardEffectEvent ---@param cardEffectEvent CardEffectEvent
function Room:doCardEffect(cardEffectEvent) function Room:doCardEffect(cardEffectEvent)
for _, event in ipairs({ fk.PreCardEffect, fk.BeforeCardEffect, fk.CardEffecting, fk.CardEffectFinished }) do for _, event in ipairs({ fk.PreCardEffect, fk.BeforeCardEffect, fk.CardEffecting, fk.CardEffectFinished }) do
@ -1490,12 +1527,14 @@ function Room:doCardEffect(cardEffectEvent)
use.responseToEvent = cardEffectEvent use.responseToEvent = cardEffectEvent
self:useCard(use) self:useCard(use)
end end
elseif cardEffectEvent.card.type == Card.TypeTrick then elseif cardEffectEvent.card.type == Card.TypeTrick and
not cardEffectEvent.disresponsive then
local players = {} local players = {}
for _, p in ipairs(self.alive_players) do for _, p in ipairs(self.alive_players) do
local cards = p.player_cards[Player.Hand] local cards = p:getCardIds(Player.Hand)
for _, cid in ipairs(cards) do for _, cid in ipairs(cards) do
if Fk:getCardById(cid).name == "nullification" then if Fk:getCardById(cid).name == "nullification" and
not table.contains(cardEffectEvent.disresponsiveList or {}, p.id) then
table.insert(players, p) table.insert(players, p)
break break
end end
@ -1560,9 +1599,7 @@ function Room:responseCard(cardResponseEvent)
moveReason = fk.ReasonResonpse, moveReason = fk.ReasonResonpse,
}) })
self:setEmotion(self:getPlayerById(from), card.name) playCardEmotionAndSound(self, self:getPlayerById(from), card)
local soundName = (self:getPlayerById(from).gender == General.Male and "male/" or "female/") .. card.name
self:broadcastPlaySound("./audio/card/" .. soundName)
for _, event in ipairs({ fk.PreCardRespond, fk.CardResponding, fk.CardRespondFinished }) do for _, event in ipairs({ fk.PreCardRespond, fk.CardResponding, fk.CardRespondFinished }) do
self.logic:trigger(event, self:getPlayerById(cardResponseEvent.from), cardResponseEvent) self.logic:trigger(event, self:getPlayerById(cardResponseEvent.from), cardResponseEvent)
@ -1599,7 +1636,11 @@ function Room:moveCards(...)
---@type MoveInfo[] ---@type MoveInfo[]
local infos = {} local infos = {}
for _, id in ipairs(cardsMoveInfo.ids) do for _, id in ipairs(cardsMoveInfo.ids) do
table.insert(infos, { cardId = id, fromArea = self:getCardArea(id) }) table.insert(infos, {
cardId = id,
fromArea = self:getCardArea(id),
fromSpecialName = cardsMoveInfo.from and self:getPlayerById(cardsMoveInfo.from):getPileNameOfId(id),
})
end end
---@type CardsMoveStruct ---@type CardsMoveStruct
@ -1640,7 +1681,7 @@ function Room:moveCards(...)
local playerAreas = { Player.Hand, Player.Equip, Player.Judge, Player.Special } local playerAreas = { Player.Hand, Player.Equip, Player.Judge, Player.Special }
if table.contains(playerAreas, realFromArea) and data.from then if table.contains(playerAreas, realFromArea) and data.from then
self:getPlayerById(data.from):removeCards(realFromArea, { info.cardId }, data.specialName) self:getPlayerById(data.from):removeCards(realFromArea, { info.cardId }, info.fromSpecialName)
elseif realFromArea ~= Card.Unknown then elseif realFromArea ~= Card.Unknown then
local fromAreaIds = {} local fromAreaIds = {}
if realFromArea == Card.Processing then if realFromArea == Card.Processing then
@ -1743,18 +1784,12 @@ end
---@param reason integer ---@param reason integer
---@param skill_name string ---@param skill_name string
---@param special_name string ---@param special_name string
function Room:moveCardTo(card, to_place, target, reason, skill_name, special_name) ---@param visible boolean
function Room:moveCardTo(card, to_place, target, reason, skill_name, special_name, visible)
reason = reason or fk.ReasonJustMove reason = reason or fk.ReasonJustMove
skill_name = skill_name or "" skill_name = skill_name or ""
special_name = special_name or "" special_name = special_name or ""
local ids = {} local ids = Card:getIdList(card)
if card[1] ~= nil then
for _, cd in ipairs(card) do
table.insertTable(ids, self:getSubcardsByRule(card))
end
else
ids = self:getSubcardsByRule(card)
end
local to local to
if table.contains( if table.contains(
@ -1770,7 +1805,8 @@ function Room:moveCardTo(card, to_place, target, reason, skill_name, special_nam
toArea = to_place, toArea = to_place,
moveReason = reason, moveReason = reason,
skillName = skill_name, skillName = skill_name,
specialName = special_name specialName = special_name,
moveVisible = visible,
} }
end end
@ -1933,6 +1969,7 @@ function Room:damage(damageStruct)
if damageStruct.damage < 1 then if damageStruct.damage < 1 then
return false return false
end end
damageStruct.damageType = damageStruct.damageType or fk.NormalDamage
if damageStruct.from and not damageStruct.from:isAlive() then if damageStruct.from and not damageStruct.from:isAlive() then
damageStruct.from = nil damageStruct.from = nil
@ -2246,6 +2283,11 @@ function Room:throwCard(card_ids, skillName, who, thrower)
}) })
end end
---@param pindianStruct PindianStruct
function Room:pindian(pindianStruct)
end
-- other helpers -- other helpers
function Room:adjustSeats() function Room:adjustSeats()
@ -2289,7 +2331,30 @@ function Room:shuffleDrawPile()
table.shuffle(self.draw_pile) table.shuffle(self.draw_pile)
end end
---@param player ServerPlayer
---@param skill Skill
---@param effect_cb fun()
function Room:useSkill(player, skill, effect_cb)
if not skill.mute then
if skill.attached_equip then
local equip = Fk:cloneCard(skill.attached_equip)
local pkgPath = "./packages/" .. equip.package.extensionName
local soundName = pkgPath .. "/audio/card/" .. equip.name
self:broadcastPlaySound(soundName)
self:setEmotion(player, pkgPath .. "/image/anim/" .. equip.name)
else
self:broadcastSkillInvoke(skill.name)
self:notifySkillInvoked(player, skill.name)
end
end
player:addSkillUseHistory(skill.name)
if effect_cb then
return effect_cb()
end
end
function Room:gameOver(winner) function Room:gameOver(winner)
self.logic:trigger(fk.GameFinished, nil, winner)
self.game_started = false self.game_started = false
self.game_finished = true self.game_finished = true

View File

@ -60,13 +60,13 @@ end
local function _waitForReply(player, timeout) local function _waitForReply(player, timeout)
local result local result
local start = fk.GetMicroSecond() local start = os.getms()
while true do while true do
result = player.serverplayer:waitForReply(0) result = player.serverplayer:waitForReply(0)
if result ~= "__notready" then if result ~= "__notready" then
return result return result
end end
if timeout and (fk.GetMicroSecond() - start) / 1000 >= timeout * 1000 then if timeout and (os.getms() - start) / 1000 >= timeout * 1000 then
return "" return ""
end end
coroutine.yield() coroutine.yield()
@ -176,7 +176,9 @@ function ServerPlayer:marshal(player)
end end
for k, v in pairs(self.cardUsedHistory) do for k, v in pairs(self.cardUsedHistory) do
player:doNotify("AddCardUseHistory", json.encode{k, v[1]}) if v[1] > 0 then
player:doNotify("AddCardUseHistory", json.encode{k, v[1]})
end
end end
if self.role_shown then if self.role_shown then
@ -402,6 +404,15 @@ function ServerPlayer:drawCards(num, skillName, fromPlace)
return self.room:drawCards(self, num, skillName, fromPlace) return self.room:drawCards(self, num, skillName, fromPlace)
end end
---@param pile_name string
---@param card integer|Card
---@param visible boolean
---@param skillName string
function ServerPlayer:addToPile(pile_name, card, visible, skillName)
local room = self.room
room:moveCardTo(card, Card.PlayerSpecial, self, fk.ReasonJustMove, skillName, pile_name, visible)
end
function ServerPlayer:bury() function ServerPlayer:bury()
-- self:clearFlags() -- self:clearFlags()
-- self:clearHistory() -- self:clearHistory()

View File

@ -13,6 +13,7 @@
---@class MoveInfo ---@class MoveInfo
---@field cardId integer ---@field cardId integer
---@field fromArea CardArea ---@field fromArea CardArea
---@field fromSpecialName string|null
---@class CardsMoveStruct ---@class CardsMoveStruct
---@field moveInfo MoveInfo[] ---@field moveInfo MoveInfo[]
@ -25,7 +26,6 @@
---@field moveVisible boolean|null ---@field moveVisible boolean|null
---@field specialName string|null ---@field specialName string|null
---@field specialVisible boolean|null ---@field specialVisible boolean|null
---@field fromSpecialName string|null
---@class HpChangedData ---@class HpChangedData
---@field num integer ---@field num integer
@ -99,7 +99,7 @@ fk.FireDamage = 3
---@field tos TargetGroup ---@field tos TargetGroup
---@field card Card ---@field card Card
---@field toCard Card|null ---@field toCard Card|null
---@field responseToEvent CardEffectStruct|null ---@field responseToEvent CardEffectEvent|null
---@field nullifiedTargets interger[]|null ---@field nullifiedTargets interger[]|null
---@field extraUse boolean|null ---@field extraUse boolean|null
---@field disresponsiveList integer[]|null ---@field disresponsiveList integer[]|null
@ -142,6 +142,16 @@ fk.ReasonExchange = 8
fk.ReasonUse = 9 fk.ReasonUse = 9
fk.ReasonResonpse = 10 fk.ReasonResonpse = 10
---@class PindianStruct
---@field from ServerPlayer
---@field to ServerPlayer
---@field from_card Card
---@field to_card Card
---@field from_number integer
---@field to_number integer
---@field reason string
---@field winner ServerPlayer|null
---@class LogMessage ---@class LogMessage
---@field type string ---@field type string
---@field from integer ---@field from integer

Some files were not shown because too many files have changed in this diff Show More