Fork me on GitHub

马超就是神

日神杀实例——马神加强版的编写

前言

基于一些原因,最近打日神杀上头。在群友的怂恿下写了几个强力武将。其中就有阴间武将界徐盛,以及我们马上要提到的马超马孟起的界限突破界限突破版(大雾)。闲话休提,直奔主题,我们立即动手。

动手写代码

创建我们的武将

在前一篇的Lua实战中,笔者已经简述了如何创建自己的新武将,因此在这里不再赘述。我们直接使用如下的代码即可:

1
MachaoPlus = sgs.General(extension, 'MachaoPlus', 'shu', '4', true, true)

设计我们的武将技能

由于普通的马神太容易断杀了,再加上日神杀 AI 对原版【铁骑】的响应太蠢(完全不弃牌,仅在特定情况会脱装备),因此综合考虑,有如下的设计:(加粗部分是和原版不同的地方)
马术:锁定技,你计算与其他角色的距离-1;若你于出牌阶段未使用【杀】造成过伤害,你于结束阶段结束时,可以视为使用一张无距离限制的【杀】
铁骑:当你使用【杀】指定一个目标后,你可以令其于此回合内锁定技以外的技能无效,然后你判定,令其选择是否弃置与结果花色相同的一张牌,若其选择否,其不能使用【闪】响应此【杀】,且此【杀】造成的伤害+1

动手写码

在此之前,我们可以考虑参考已有的代码,马神的代码是刻在了源码里的,因此我们可以去官方的仓库里参照一下。而马术作为一个标准的距离修改技能以及一个阶段触发技的合体,我们完全有能力自己动手。所以我们即刻开始动工!

马术

作为一个杂糅技能,我们首先还是考虑马术的额外功能:未使用【杀】造成伤害,回合结束后可以使用一张无距离限制的【杀】。我们需要考虑的是两点,第一,判断回合内是否造成过【杀】的伤害,第二,选择目标。至于回合内是否造成过【杀】的伤害,我们可以参考技能【生息】,但实际上效果并不理想(可能是我哪里错了),因此我们做如下考虑。

触发时机

造成伤害后:我们使用【杀】造成伤害之后,需要设立一个标记(mark)或者 flag,来表示我们已经【杀】到掉血了。
阶段结束时:在回合结束阶段,我们需要检测是否具有对应的标记,若有,提示玩家选择目标;否则消除标记。

代码部分

综上,我们基本的代码框架已经出来了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
LuaMashu = sgs.CreateTriggerSkill {
name = 'LuaMashu',
frequency = sgs.Skill_Compulsory,
events = {sgs.Damage, sgs.EventPhaseEnd},
on_trigger = function(self, event, player, data, room)
-- 在新的 extra.lua 里,已经将 room 放在参数里了,因此可以省去 getRoom() 这一步
if event == sgs.Damage then
-- 处理【杀】造成的伤害,添加标记
elseif event == sgs.EventPhaseEnd then
-- 判断阶段并根据标记有无进行处理
end
return false
end
}

伤害处理

伤害处理很简单,我们只需要在造成伤害后,判断是否为【杀】造成的伤害即可,参见如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
if player:getPhase() == sgs.Player_Play and player:hasSkill(self:objectName()) then
-- 判断玩家是否是处于“出牌阶段”,是否拥有技能(其实可以略去,不指明 can_trigger 的情况下,默认有此条件)
local damage = data:toDamage()
-- data 是一个很神奇的东西,在这里我们使用 toDamage() 方法将其转换为对应的 damageStruct,具体的结构可以参看源码
if damage and damage.card then
-- 判断 damage 是否存在(其实可以不用),造成 damage 的卡牌是否存在
if damage.card:isKindOf('Slash') then
-- 如果这张卡存在,并且是【杀】(注意 Slash 开头大写,其他的卡牌类型也可以在源码中查看)
room:addPlayerMark(damage.from, 'MashuSlashDamage')
-- 对造成伤害的角色添加对应的标记(标记名可以自定义)
end
end
end

结束阶段判断

同样的,在回合结束阶段末尾,我们判断标记是否存在即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
if player:getPhase() == sgs.Player_Finish then
if player:getMark('MashuSlashDamage') == 0 then
-- 如果我们拥有的标记为0,代表我们未在出牌阶段以【杀】造成过伤害
local victim = room:askForPlayerChosen(player, room:getOtherPlayers(player), self:objectName(), '@LuaMashuSlashTo', true, true)
-- 提示选择目标
--[[
在这里我们参看源码,获取 askForPlayerChosen 函数的参数信息
ServerPlayer *askForPlayerChosen(ServerPlayer *player, const QList<ServerPlayer *> &targets, const QString &reason, const QString &prompt = QString(), bool optional = false, bool notify_skill = false);
它返回了一个 ServerPlayer 对象,也就是一个角色,我们在这里不做深究,在这里的参数类型我们也不过多深究,包括指针啥的也不做讨论
参数分别为:
player:询问的目标对象
targets:供选择的目标列表,通常使用的是 room:getAlivePlayers() 获取存活的所有角色,room:getOtherPlayers(except) 获取除 except 外的所有角色
reason:理由,可以用 self:objectName(),玩家不可见
prompt:提示格式,也就是显示在手牌上方的一行字,可以支持格式字符串(默认为空)
optional:可选(默认为否),即玩家是否可以选择放弃选择角色
notify_skill:是否提示发动技能(默认为否),你选择一名玩家后,是否在提示栏里显示“XX 发动了 YY,目标是 ZZ”
]]--
if victim then
-- 如果有我们的对应目标
local slash = sgs.Sanguosha:cloneCard('slash', sgs.Card_NoSuit, 0)
slash:setSkillName(self:objectName())
-- 我们创建一张虚拟的【杀】,并以我们的“马术”作为他的技能名
room:useCard(sgs.CardUseStruct(slash, player, victim))
-- 对目标使用【杀】
--[[
这是 useCard 的源码
bool useCard(const CardUseStruct &card_use, bool add_history = false);
我们一般不考虑后续参数,只关注前面的 CardUseStruct 即可
对于此,我们在后面列出(看看就好)
我们关心其中的一条构造函数:
CardUseStruct(const Card *card, ServerPlayer *from, ServerPlayer *target, bool isOwnerUse = true);
我们指定对应的卡牌,使用者、目标即可(目标也可以是一个 list),最后的参数我们忽略他
对应的就是 slash - 虚拟的【杀】 player - 当前的角色,也即是玩家 victim - 受害者
因此返回了一个 player 使用 slash 目标是 victim 的 CardUseStruct
然后再由 room 执行这个 CardUseStruct,完成【杀】的使用过程
]]--
end
end
room:setPlayerMark(player, 'MashuSlashDamage', 0)
-- 标记清空
end

这里是 CardUseStruct 的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct CardUseStruct
{
enum CardUseReason
{
CARD_USE_REASON_UNKNOWN = 0x00,
CARD_USE_REASON_PLAY = 0x01,
CARD_USE_REASON_RESPONSE = 0x02,
CARD_USE_REASON_RESPONSE_USE = 0x12
};

CardUseStruct();
CardUseStruct(const Card *card, ServerPlayer *from, QList<ServerPlayer *> to, bool isOwnerUse = true);
CardUseStruct(const Card *card, ServerPlayer *from, ServerPlayer *target, bool isOwnerUse = true);
bool isValid(const QString &pattern) const;
void parse(const QString &str, Room *room);
bool tryParse(const QVariant &usage, Room *room);

const Card *card;
ServerPlayer *from;
QList<ServerPlayer *> to;
bool m_isOwnerUse;
bool m_addHistory;
bool m_isHandcard;
QStringList nullified_list;
};

基础马术

基于此,我们的额外部分设计完成,当然,还有基础套餐也要加上,也即是:

1
2
3
4
5
6
7
8
9
10
11
LuaMashuDistance = sgs.CreateDistanceSkill {
-- 对于距离修改技能,他是可以不“在场”生效的
-- 也即是说,只要有角色拥有这个技能,他的修正就会存在
-- 因此我们用技能暗将获取这个技能即可
name = 'LuaMashuDistance',
correct_func = function(self, from, to)
if from:hasSkill('LuaMashu') then return -1 end
-- 如果计算起始角色拥有技能“马术(新)”,那么对距离做 -1 的修正
return 0
end
}

总代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
LuaMashu = sgs.CreateTriggerSkill {
name = 'LuaMashu',
frequency = sgs.Skill_Compulsory,
events = {sgs.Damage, sgs.EventPhaseEnd},
on_trigger = function(self, event, player, data, room)
if event == sgs.Damage then
if player:getPhase() == sgs.Player_Play and
player:hasSkill(self:objectName()) then
local damage = data:toDamage()
if damage and damage.card then
if damage.card:isKindOf('Slash') then
room:addPlayerMark(damage.from, 'MashuSlashDamage')
end
end
end
elseif event == sgs.EventPhaseEnd then
if player:getPhase() == sgs.Player_Finish then
if player:getMark('MashuSlashDamage') == 0 then
local victim = room:askForPlayerChosen(player,
room:getOtherPlayers(
player),
self:objectName(),
'@LuaMashuSlashTo',
true, true)
if victim then
local slash = sgs.Sanguosha:cloneCard('slash',
sgs.Card_NoSuit, 0)
slash:setSkillName(self:objectName())
room:useCard(sgs.CardUseStruct(slash, player, victim))
end
end
room:setPlayerMark(player, 'MashuSlashDamage', 0)
end
end
return false
end
}
LuaMashuDistance = sgs.CreateDistanceSkill {
name = 'LuaMashuDistance',
correct_func = function(self, from, to)
if from:hasSkill('LuaMashu') then return -1 end
return 0
end
}

MachaoPlus:addSkill(LuaMashu)
SkillAnjiang:addSkill(LuaMashuDistance)
-- 为我们的马超和暗将安排技能
-- 暗将的声明如下:SkillAnjiang = sgs.General(extension, 'SkillAnjiang', 'god', '6', true, true, true)
-- 最后一个参数代表其不会在武将一览中出现,彻底地被隐藏

铁骑

铁骑是一个标准的触发技能,我们基于原版的 C++ 铁骑代码进行修正即可。

触发时机

根据技能描述,我们对于铁骑的触发时机有如下考虑:
指定目标后(TargetSpecified):使用【杀】指定目标后,询问玩家是否发动技能
造成伤害时(DamageCaused):造成伤害时,判断是否符合条件
判定结束后(FinishJudge):处理无法闪避的问题
注意,我们的技能封锁只持续一个回合,因此解铃还须系铃人,还有下列的触发时机:
卡牌结算后(CardFinished):处理伤害增加的解除
阶段切换时(EventPhaseChanging):处理标记等
死亡时(Death):处理标记等

参考源码

这里列出了对应的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
class Tieji : public TriggerSkill
{
public:
Tieji() : TriggerSkill("tieji")
{
events << TargetSpecified << FinishJudge;
}

bool triggerable(const ServerPlayer *target) const
{
return target != NULL;
}

bool trigger(TriggerEvent triggerEvent, Room *room, ServerPlayer *player, QVariant &data) const
{
if (triggerEvent == TargetSpecified && TriggerSkill::triggerable(player)) {
CardUseStruct use = data.value<CardUseStruct>();
if (!use.card->isKindOf("Slash"))
return false;
QVariantList jink_list = player->tag["Jink_" + use.card->toString()].toList();
int index = 0;
QList<ServerPlayer *> tos;
foreach (ServerPlayer *p, use.to) {
if (!player->isAlive()) break;
if (player->askForSkillInvoke(this, QVariant::fromValue(p))) {
room->broadcastSkillInvoke(objectName());
if (!tos.contains(p)) {
p->addMark("tieji");
room->addPlayerMark(p, "@skill_invalidity");
tos << p;

foreach(ServerPlayer *pl, room->getAllPlayers())
room->filterCards(pl, pl->getCards("he"), true);
JsonArray args;
args << QSanProtocol::S_GAME_EVENT_UPDATE_SKILL;
room->doBroadcastNotify(QSanProtocol::S_COMMAND_LOG_EVENT, args);
}

JudgeStruct judge;
judge.pattern = ".";
judge.good = true;
judge.reason = objectName();
judge.who = player;
judge.play_animation = false;

room->judge(judge);

if ((p->isAlive() && !p->canDiscard(p, "he"))
|| !room->askForCard(p, ".|" + judge.pattern, "@tieji-discard:::" + judge.pattern, data, Card::MethodDiscard)) {
LogMessage log;
log.type = "#NoJink";
log.from = p;
room->sendLog(log);
jink_list.replace(index, QVariant(0));
}
}
index++;
}
player->tag["Jink_" + use.card->toString()] = QVariant::fromValue(jink_list);
return false;
} else if (triggerEvent == FinishJudge) {
JudgeStruct *judge = data.value<JudgeStruct *>();
if (judge->reason == objectName()) {
judge->pattern = judge->card->getSuitString();
}
}
return false;
}
};

class TiejiClear : public TriggerSkill
{
public:
TiejiClear() : TriggerSkill("#tieji-clear")
{
events << EventPhaseChanging << Death;
}

int getPriority(TriggerEvent) const
{
return 5;
}

bool triggerable(const ServerPlayer *target) const
{
return target != NULL;
}

bool trigger(TriggerEvent triggerEvent, Room *room, ServerPlayer *target, QVariant &data) const
{
if (triggerEvent == EventPhaseChanging) {
PhaseChangeStruct change = data.value<PhaseChangeStruct>();
if (change.to != Player::NotActive)
return false;
} else if (triggerEvent == Death) {
DeathStruct death = data.value<DeathStruct>();
if (death.who != target || target != room->getCurrent())
return false;
}
QList<ServerPlayer *> players = room->getAllPlayers();
foreach (ServerPlayer *player, players) {
if (player->getMark("tieji") == 0) continue;
room->removePlayerMark(player, "@skill_invalidity", player->getMark("tieji"));
player->setMark("tieji", 0);

foreach(ServerPlayer *p, room->getAllPlayers())
room->filterCards(p, p->getCards("he"), false);
JsonArray args;
args << QSanProtocol::S_GAME_EVENT_UPDATE_SKILL;
room->doBroadcastNotify(QSanProtocol::S_COMMAND_LOG_EVENT, args);
}
return false;
}
};

可以看到源码把它拆成了两个技能,但是实际使用中,我们会在小型场景/执行底端脚本中添加技能,如果只添加主技能,会有各种各样的问题,添加副技能也略显麻烦,基于此,我们还是考虑合并为一个技能。

基本框架

我们将源码 lua 化,然后抽出主要框架如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
LuaTieji = sgs.CreateTriggerSkill {
name = 'LuaTieji',
frequency = sgs.Skill_NotFrequent,
events = {
sgs.TargetSpecified, sgs.FinishJudge, sgs.EventPhaseChanging, sgs.Death,
sgs.DamageCaused, sgs.CardFinished
},
on_trigger = function(self, event, player, data, room)
if event == sgs.TargetSpecified then
-- 处理询问等事件,包括不能闪避也在这里
elseif event == sgs.FinishJudge then
-- 在这里根据判定结果对以上的询问的字符串做格式化处理
elseif event == sgs.DamageCaused then
-- 处理伤害增加
elseif event == sgs.CardFinished then
-- 处理解除伤害增加
else
-- 处理标记的消除
if event == sgs.EventPhaseChanging then

elseif event == sgs.Death then

end
-- 不同的时机需要不同的判断标准
-- 下面就处理标记的移除等
end
return false
end,
can_trigger = function(self, target) return target end
-- 由于【铁骑】可以在回合外发动,因此需要考虑全局性,所以 can_trigger 需要重写
}

目标选择时

首先我们处理 TargetSpecified 这一时机的问题,很简单,我们照着源码翻译下来就可以了,为了对应源码中的 triggerable,我们定义一个函数 RIGHT 来处理:

1
2
3
4
5
6
7
function RIGHT(self, player)
if player and player:isAlive() and player:hasSkill(self:objectName()) then
return true
else
return false
end
end

这个函数帮助游戏筛选是否符合条件(存活、有技能),因为我们重写了 can_trigger,所以是必要的,如果没有这几个判断,系统会对所有玩家在出【杀】时进行询问,这显然不符合设计初衷。
翻译过来的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
if RIGHT(self, player) then
-- 如果玩家符合我们的条件,拥有技能且存活(注意这里的 player 不是通常的玩家)
local use = data:toCardUse()
if not use.card:isKindOf('Slash') then
return false
end
-- 获取 CardUseStruct 并判断卡牌类型
local jink_table = sgs.QList2Table(player:getTag("Jink_" .. use.card:toString()):toIntList())
local index = 1
-- jink_table 这东西超纲了(),可以做如下理解,一个格子代表闪的使用情况,同样的效果可以使用 room:slashResult() 函数进行替代
local tos = sgs.SPlayerList()
-- 创建玩家列表
for _, p in sgs.qlist(use.to) do
-- 对于所有的指定为该【杀】的玩家
if not player:isAlive() then break end
-- 如果已阵亡,则跳过
local data2 = sgs.QVariant()
data2:setValue(p)
if room:askForSkillInvoke(player, self:objectName(), data2) then
-- 最后一个参数显示为“你想对 XX 发动技能 YY 吗”
room:broadcastSkillInvoke(self:objectName())
-- 播放技能语音
-- 常用的原型为: void broadcastSkillInvoke(const QString &skillName, int type);
-- 前一个是技能名称,后一个是第几个语音,不指定时随机播放
if not tos:contains(p) then
-- 防止重复询问
room:addPlayerMark(p, 'LuaTieji')
room:addPlayerMark(p, '@skill_invalidity')
-- @skill_invalidity 标记实现了非锁定技失效的功能
-- LuaTieji 用于计数,消去 @skill_invalidity 时使用
room:doAnimate(1, player:objectName(), p:objectName())
-- 画线,从 player 指向 p
tos:append(p)
-- 将 p 添加进列表,防止重复询问
for _, pl in sgs.qlist(room:getAllPlayers()) do
room:filterCards(pl, pl:getCards('he'), true)
-- 修改要求弃牌时的选择,h - 手牌区,e - 装备区
end
end
local judge = sgs.JudgeStruct()
-- 定义判定结构体
judge.pattern = "."
-- 判定的类型,这里是无论如何都成功,例如可以填成 heart 等
judge.good = true
-- 这里为 true,代表符合上述 pattern 的视为成功
-- 反之为 false 时符合 pattern 的视为实败
judge.reason = self:objectName()
-- 判定理由
judge.who = player
-- 执行判定的人
judge.play_animation = false
-- 是否出现动画(打勾打叉)
room:judge(judge)
-- 执行判定
if p:isAlive() and not p:canDiscard(p, "he") or not room:askForCard(p, ".|" .. judge.pattern, "@LuaTieji-discard:" .. judge.pattern, data, sgs.Card_MethodDiscard) then
--[[
canDiscard 函数返回布尔值,代表是否可以弃置对应区域的牌
askForCard 函数比较复杂,将其原型列出:
const Card *askForCard(ServerPlayer *player, const QString &pattern, const QString &prompt, const QVariant &data = QVariant(), Card::HandlingMethod method = Card::MethodDiscard, ServerPlayer *to = NULL, bool isRetrial = false, const QString &skill_name = QString(), bool isProvision = false);
我们关心前几个参数:
player:被询问要求选择卡牌的角色
pattern:要求卡牌的格式(例如花色、颜色、点数)
prompt:提示信息,可使用格式字符串
data:忽略
method:卡牌的选择类型,例如这里是要求弃牌
后略
]]--
room:addPlayerMark(p, 'LuaTiejiDamageUp')
-- 添加增伤害标记
local msg = sgs.LogMessage()
msg.type = '#NoJink'
msg.from = p
room:sendLog(msg)
-- 发送特定信息
jink_table[index] = 0
-- 可以理解为提前为闪的信息标记为无,代表玩家没有使用【闪】
end
end
index = index + 1
end
local jink_data = sgs.QVariant()
jink_data:setValue(Table2IntList(jink_table))
-- 应用【杀】的信息,把没有弃牌的统统安排
player:setTag("Jink_" .. use.card:toString(), jink_data)
-- 使用一个 Tag 存储对应的卡牌信息
return false
end

处理判定

还记得我们询问弃牌时的 pattern 吗?是和 judge.pattern 挂钩的,但是原来的 pattern.,也就是什么类型的都行,但是马神只要求一种花色,因此我们需要在判定结束后进行修正:

1
2
3
4
local judge = data:toJudge()
if judge.reason == self:objectName() then
judge.pattern = judge.card:getSuitString()
end

如此一来,judge 的 pattern 就被更换成了判定牌的花色了,也可以在格式字符串中进行说明。

造成伤害

马神对那些不弃牌的人从不留情!因此我们只需要判断之前是否在他身上留下过标记即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
local damage = data:toDamage()
-- 获取伤害结构体
if damage.chain then return false end
-- 马神是很精准的,只加伤一次,传导伤害将不受到此影响(铁索连环)
-- 如果需要改成伤害转移(例如天香、誓仇(☆SP刘备)),则是 damage.transfer
-- 当然也可以同时添加
if damage.to:getMark('LuaTiejiDamageUp') > 0 then
-- 如果有增伤标记
damage.damage = damage.damage + 1
-- 加伤害
room:removePlayerMark(damage.to, 'LuaTiejiDamageUp')
-- 移除标记
room:doAnimate(1, damage.from:objectName(), damage.to:objectName())
-- 画线
local msg = sgs.LogMessage()
msg.type = '#LuaTiejiDamageUp'
msg.from = player
if damage.card and not damage.card:isVirtualCard() then
msg.card_str = damage.card:getEffectiveId()
else
msg.type = '#LuaTiejiDamageUpVirtualCard'
end
-- 消息结构体,请参照学习手册
room:notifySkillInvoked(player, self:objectName())
-- 在你的武将处显示一下技能名称
room:sendLog(msg)
-- 发信息
data:setValue(damage)
-- 将 damage 回写,应用修改
end

清理残留

回收多余的增伤标记

有些情况可能会提前地将伤害过程取消掉,因此我们需要在【杀】结算完成后,清除掉尚存的标记。
时机是 CardFinished

1
2
3
4
5
6
7
local use = data:toCardUse()
local card = use.card
if card:isKindOf('Slash') then
for _, p in sgs.qlist(room:getAlivePlayers()) do
room:setPlayerMark(p, 'LuaTiejiDamageUp', 0)
end
end

取消技能封锁

马神的技能封锁仅仅持续一回合,因此需要在回合结束时解除,同时还要考虑到另一个情况——一个不可能的事态已然发生——马神的陨落。因此,我们对于这两种情况,有如下的判断考虑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if event == sgs.EventPhaseChanging then
local change = data:toPhaseChange()
if change.to == sgs.Player_NotActive then
else
return false
end
-- 判断是否切换到回合结束
elseif event == sgs.Death then
local death = data:toDeath()
if death.who:objectName() ~= player:objectName() or
player:objectName() ~= room:getCurrent():objectName() then
return false
end
-- 判断死的是否为当前角色/当前回合角色
end
-- 如果条件满足,我们撤销对应的标记

基于此,我们也是很常规的将标记取消,并恢复 filterCards 操作

1
2
3
4
5
6
7
8
9
10
11
12
for _, p in sgs.qlist(room:getAllPlayers()) do
if p:getMark("LuaTieji") > 0 then
room:removePlayerMark(p, "@skill_invalidity", p:getMark('LuaTieji'))
-- 根据 LuaTieji 的数量清除标记,因为其他技能可能引入
room:setPlayerMark(p, "LuaTieji", 0)
-- 清除 LuaTieji 标记
for _, pl in sgs.qlist(room:getAllPlayers()) do
room:filterCards(pl, pl:getCards('he'), false)
end
-- 还原所有角色的 filterCards
end
end

整体而言,我们的【铁骑】技能就完成了。总代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
LuaTieji = sgs.CreateTriggerSkill {
name = 'LuaTieji',
frequency = sgs.Skill_NotFrequent,
events = {
sgs.TargetSpecified, sgs.FinishJudge, sgs.EventPhaseChanging, sgs.Death,
sgs.DamageCaused, sgs.CardFinished
},
on_trigger = function(self, event, player, data, room)
if event == sgs.TargetSpecified then
if RIGHT(self, player) then
local use = data:toCardUse()
if not use.card:isKindOf('Slash') then
return false
end
local jink_table = sgs.QList2Table(
player:getTag(
"Jink_" .. use.card:toString())
:toIntList())
local index = 1
local tos = sgs.SPlayerList()
for _, p in sgs.qlist(use.to) do
if not player:isAlive() then break end
local data2 = sgs.QVariant()
data2:setValue(p)
if room:askForSkillInvoke(player, self:objectName(), data2) then
room:broadcastSkillInvoke(self:objectName())
if not tos:contains(p) then
room:addPlayerMark(p, 'LuaTieji')
room:addPlayerMark(p, '@skill_invalidity')
room:doAnimate(1, player:objectName(),
p:objectName())
tos:append(p)

for _, pl in sgs.qlist(room:getAllPlayers()) do
room:filterCards(pl, pl:getCards('he'), true)
end

end

local judge = sgs.JudgeStruct()
judge.pattern = "."
judge.good = true
judge.reason = self:objectName()
judge.who = player
judge.play_animation = false

room:judge(judge)

if p:isAlive() and not p:canDiscard(p, "he") or
not room:askForCard(p, ".|" .. judge.pattern,
"@LuaTieji-discard:" ..
judge.pattern, data,
sgs.Card_MethodDiscard) then
room:addPlayerMark(p, 'LuaTiejiDamageUp')
local msg = sgs.LogMessage()
msg.type = '#NoJink'
msg.from = p
room:sendLog(msg)
jink_table[index] = 0
end
end
index = index + 1
end
local jink_data = sgs.QVariant()
jink_data:setValue(Table2IntList(jink_table))
player:setTag("Jink_" .. use.card:toString(), jink_data)
return false
end
elseif event == sgs.FinishJudge then
local judge = data:toJudge()
if judge.reason == self:objectName() then
judge.pattern = judge.card:getSuitString()
end
elseif event == sgs.DamageCaused then
local damage = data:toDamage()
if damage.chain then return false end
if damage.to:getMark('LuaTiejiDamageUp') > 0 then
damage.damage = damage.damage + 1
room:removePlayerMark(damage.to, 'LuaTiejiDamageUp')
room:doAnimate(1, damage.from:objectName(),
damage.to:objectName())
local msg = sgs.LogMessage()
msg.type = '#LuaTiejiDamageUp'
msg.from = player
if damage.card and not damage.card:isVirtualCard() then
msg.card_str = damage.card:getEffectiveId()
else
msg.type = '#LuaTiejiDamageUpVirtualCard'
end
room:notifySkillInvoked(player, self:objectName())
room:sendLog(msg)
data:setValue(damage)
end
elseif event == sgs.CardFinished then
local use = data:toCardUse()
local card = use.card
if card:isKindOf('Slash') then
for _, p in sgs.qlist(room:getAlivePlayers()) do
room:setPlayerMark(p, 'LuaTiejiDamageUp', 0)
end
end
else
if event == sgs.EventPhaseChanging then
local change = data:toPhaseChange()
if change.to == sgs.Player_NotActive then
else
return false
end
elseif event == sgs.Death then
local death = data:toDeath()
if death.who:objectName() ~= player:objectName() or
player:objectName() ~= room:getCurrent():objectName() then
return false
end
end
for _, p in sgs.qlist(room:getAllPlayers()) do
if p:getMark("LuaTieji") > 0 then
room:removePlayerMark(p, "@skill_invalidity",
p:getMark('LuaTieji'))
room:setPlayerMark(p, "LuaTieji", 0)

for _, pl in sgs.qlist(room:getAllPlayers()) do
room:filterCards(pl, pl:getCards('he'), false)
end
end
end
end
return false
end,
can_trigger = function(self, target) return target end
}

到此为止,我们的马神技能就初步设计完成。

为马神的技能启用简单的 AI

在初步设计完成时,我们没有考虑到马神可能成为队友的情况,如此强劲的马神,如果成了我们的队友,国安民乐,岂不美哉?
但是在实际操作时发现,马神不会使用技能【铁骑】,【马术】也是乱指定目标,时不时给队友来一刀,严重影响了体验,因此,我们在这里简单地为马神写两个 AI。
说是 AI,其实就是决策树,满足某个条件时就返回对应的数据。
我们在 QSanguosha/lua/ai 下创建对应的 AI 文件,比方说我的马神放在 Jiaqiang.lua 文件中,对应的 AI 文件名则是 Jiaqiang-ai.lua。也即是 lua文件名-ai.lua

铁骑

对于【铁骑】,我们做如下考虑,很明显这技能对队友不能发动,对敌人那就一定发动。因此有:

1
2
3
4
5
sgs.ai_skill_invoke.LuaTieji = function(self,data)
local target = data:toPlayer()
if not self:isFriend(target) then return true end
return false
end

在这里,我们采用了回调函数 sgs.ai_skill_invoke.技能名,该函数返回对应的布尔值,作用于 room:askForSkillInvoke,因此我们判断对应的目标是否为队友即可。类似的函数可以参照目录下的其他 AI 文件。
对于【铁骑】的弃牌,我没有特别的写,因此 AI 会看情况是否弃牌,总体来讲弃牌效果是优于原版 AI 的,因此不做改动。

马术

马术的话,我们也简单考虑,考虑成对需要出杀的人使用技能,照搬现有代码我们有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sgs.ai_skill_playerchosen.LuaMashu = function(self, targetlist)
local targets = sgs.QList2Table(targetlist)
self:sort(targets)
local friends, enemies = {}, {}
for _, target in ipairs(targets) do
if self:cantbeHurt(target, self.player) or not self:damageIsEffective(target, nil, self.player) then continue end
if self:isEnemy(target) then table.insert(enemies, target)
elseif self:isFriend(target) then table.insert(friends, target) end
end
for _, enemy in ipairs(enemies) do
if not self:getDamagedEffects(enemy, self.player) and not self:needToLoseHp(enemy, self.player) then return enemy end
end
for _, friend in ipairs(friends) do
if self:getDamagedEffects(friend, self.player) and self:needToLoseHp(friend, self.player) then return friend end
end
return nil
end

至此,一个强大的、比较聪明的马神就诞生辣!

0%