太阳神三国杀Lua编写高级教程
太阳神三国杀,是一款基于C++ QT GUI框架的三国杀非官方开源软件,开发者:Moligaloo(太阳神上),现由Mogara团队,继续更新。拥有智能AI可以实现联机和单机的两种游戏方式,并能通过DIY接口进行自由的个性化修改和添加更多元素。本经验教你编写属于自己的lua!(基础教程)适用于1217(V2)版。
本教程废话不多说,直接代码!
ps:手牌教程。
制作一张基本牌的结构为:
name,
class_name,
subtype,
target_fixed,
can_recast,
suit,
number,
filter,
feasible,
available,
about_to_use,
on_use,
on_effect,
}
下面介绍每个成员的含义。
name:字符串类型,卡牌的显示名称。函数objectName()所获得的名称就是这个。而在LoadTranslationTable中会将字符串翻译成所对应的新字符串。
target_fixed:布尔类型,目标是否已固定。如果为true,那么使用该牌时无法指定目标,直接使用。否则的话还要经历选择目标的过程。
filter:布尔类型,目标筛选器。函数的形式为function(self, targets, to_select),返回值的真假决定着被筛选目标(to_select)是否可选。该成员不可省略,但若target_fixed为true,则可以省略。
feasible:布尔类型,卡牌可用与否。函数的形式为function(self, targets),返回值决定了卡牌在目前情况下是否可以使用(即点击“确定”)。缺省值在target_fixed=false时,为return #targets > 0。
on_use:卡牌使用后的执行函数。函数的形式为function(self, room, source, targets)。缺省时为对targets中每个目标角色执行一次效果,亦即on_effect。
on_effect:卡牌生效时的执行函数。函数的形式为function(self, effect)。缺省值为什么也不执行。
注意卡牌没有will_throw成员,也就是说,不存在使用后不弃置的卡牌。
而另外一些就是全新的了——
class_name:字符串类型,卡牌的类名,我们平常在Lua中见到的isKindOf()的参数就是这个。和name不同在于,几种卡牌可以使用同一个class_name,比如不同属性的【杀】,它们的class_name都是“Slash”。
subtype:字符串类型,卡牌的子类名。实际上一般情况下该成员并没有实际意义,仅仅是在“卡牌一览”中显示其为“攻击牌”或者“防御牌”来用的;不过用getSubType()函数仍然可以获得之。这个和name一样可以被翻译。
can_recast:布尔类型,能否重铸。重铸和铁索连环的第二个用法一样,不属于使用、打出、弃置。缺省值为false,即不可重铸。
suit:整型,卡牌的花色。如果该牌的花色是唯一的,那么便可以在Lua中写上此成员。
number:整型,卡牌的点数。如果该牌的点数是唯一的,那么便可以在Lua中写上此成员。正常情况下点数是1~13,但是其他的值也是可以使用的,不过在卡牌上面没有点数文字罢了。
available:布尔类型,是否可直接使用。函数的形式为function(self, player),这决定了该卡牌是否可以在出牌阶段中使用,类似于ViewAsSkill中的enabled_at_play。
about_to_use:卡牌在点击“确定”的时候所执行的函数。函数的形式为function(self, room, use)。
制作一张类似“杀”可以主动使用的基本牌。
模板:
name = "cardname",
class_name = "classname",
subtype = "attack_card",
target_fixed = false,
can_recast = false,
suit =
number =
filter = function(self, targets, to_select)
...
end,
available = function(self, player)
...
end,
on_effect = function(self, effect)
...
end,
}
其中 on_effect中写入执行效果。例如要让目标角色摸一张牌,那么就将on_effect写成:
on_effect = function(self, effect)
effect.to:drawCards(1)
end,
effect.to是目标角色,而effect.from则是效果来源。
比如我要让目标摸一张牌,我摸三张牌,那么就写成:
on_effect = function(self, effect)
effect.to:drawCards(1)
effect.from:drawCards(3)
end,
如果是对目标角色造成一点伤害,那就是:
on_effect = function(self, effect)
local room = effect.from:getRoom()
room:damage(sgs.DamageStruct(card, effect.from, effect.to,
1, sgs. DamageStruct_Normal))
end,
我说几点跟锦囊牌编程不同的地方。
首先,注意on_use和on_effect。在编写锦囊牌的时候,因为几乎没有哪个技能有“锦囊牌对你无效”之类的语句,所以两者可以随便互换用。但是现在编写的不是锦囊牌,而是基本牌,所以要考虑卡牌生效的问题了。于是,我们需要这样子写:
on_use = function(self, room, source, targets)
--在对所有目标生效前执行的语句
...
–-注意,这里的
for _, target in ipairs(
room:cardEffect(self, source, target)
end
--在对所有目标生效后执行的语句
...
end,
on_effect = function(self, effect)
--对每名目标的效果
...
end,
制作一张【闪】
因为它只能在【杀】或【万箭齐发】需要响应的时候打出,反倒是在出牌阶段不能使用。
代码:
name = "cardname",
class_name = "Jink",
subtype = "defense_card",
target_fixed = true,
can_recast = false,
suit =
number =
available = function(self, player)
return false
end,
其中 available下的 "return false"表示使用后返回“假”值,如果改为return true表示使用后返回“真”值,还是会造成伤害。
这种卡牌没有使用能力,所以省略了on_use和on_effect,而且available返回值恒为false。至于如何像【闪】一样能在【杀】或【万箭齐发】需要响应的时候打出,很简单——class_name="Jink"即可。这样的话,能够打出【闪】的时机,也能打出这张牌。类似的,为Slash时可以在【决斗】【南蛮入侵】时打出此牌。
打出和使用并不矛盾,你可以制作既能使用又能打出的牌。值得注意的是,尽管【桃】在濒死时候使用是“打出”,但仍旧会触发on_use和on_effect效果,类似的还有【借刀杀人】、挑衅时候的【杀】。
如果想制作一张“抵消【杀】的效果并摸一张牌”的卡牌,那么光靠CreateBasicCard就不够了,还得使用一个触发技来实现额外的效果。
制作【杀】类卡牌
说到【杀】类卡牌,我就要在这里额外谈论一段了。因为【杀】不同于其他的基本牌,它有专门的触发事件。也就是sgs.SlashEffect,sgs.SlashEffected,sgs.SlashProceed,sgs.SlsahHit,sgs.SlashMissed等。而如果单纯地写效果,则显然不会触发这些,如:
on_effect = function(self, effect)
local source = effect.from
local target = effect.to
local room = source:getRoom()
room:setEmotion(source, "killer");
if not room:askForCard(target, "jink",
"slash-jink:"..source:objectName(), sgs.QVariant(),
sgs.Card_MethodResponse) then
local damage = sgs.DamageStruct()
damage.from = source
damage.to = target
damage.damage = 1
damage.card = self
room:damage(damage)
end
这是简单地描述【杀】的过程的on_effect。它只有要求出闪与造成伤害的作用,但没有触发任何与【杀】本身相关的事件。比如技能【铁骑】,虽然可以在此牌指定目标后询问是否发动,但不能在判定为红后使其不可闪避。此外,该代码中询问【闪】的部分给AI使用的data为空,使得AI不能通过【杀】的特征来决定是否出【闪】。
要让卡牌能够触发【杀】相关的事件,你可能会想到在语句中插入相应的trigger()函数。但是问题在于:①事件的参数和返回值太复杂,②即使能够正确地插入trigger(),但是在触发技中常见的slashResult()函数并不会按照预想的效果执行。
而笔者之前试过直接使用slashEffect()的on_effect,即:
on_effect = function(self, effect)
local room = effect.from:getRoom()
local slasheffect = sgs.SlashEffectStruct()
slasheffect.from = effect.from
slasheffect.slash = self
slasheffect.to = effect.to
room:slashEffect(slasheffect)
end
与此配套的还有一个全局的触发技能,用于设定【杀】在造成伤害后的效果。但是这样写之后,并没有让这张【杀】类卡牌触发相关的技能。和前面的一样,“不可被闪避”仍旧无效。所以我们只能想到这种描述方式:
on_use = function(self, room, source, targets)
local slash = sgs.Sanguosha:cloneCard
("slash", self:getSuit(), self:getNumber())
slash:addSubcard(self)
room:setCardFlag(slash, "special_flag")
local use = sgs.CardUseStruct()
use.card = slash
use.from = source
for _, target in ipairs(targets) do
use.to:append(target)
end
room:useCard(use)
end
其含义是“将这张卡牌当做一张普通的【杀】来使用”。至于这张【杀】会造成什么样的效果,就只有靠另外的一个触发技能了。触发技能全文如下:
frequency = sgs.Skill_Compulsory,
events = {sgs.Predamage},
name = "skillname",
can_trigger = function(self, target)
return target
end,
global = true,
priority = 10,
on_trigger=function(self,event,player,data)
local damage = data:toDamage()
if damage.card:hasFlag("special_flag") then
--在这里填写卡牌造成伤害时执行的语句
--如用data:setValue(damage)设置伤害值
...
end
end
}
--将技能直接添加到sgs.Sanguosha中
local skillList=sgs.SkillList()
if not sgs.Sanguosha:getSkill("skillname") then
skillList:append(
end
sgs.Sanguosha:addSkills(skillList)
用以上的方式所写的卡牌,可以触发【杀】类的事件。但是会存在其他的问题,就是在使用的时候会显示“玩家将XX当做【杀】使用”。
单体锦囊牌基本的上个教程已经说了,所以来制作延时锦囊。
当subclass为sgs.LuaTrickCard_TypeDelayedTrick时会默认一些符合延时锦囊效果的成员。这里我们详细地解释一下。
on_use会将锦囊移动到目标角色的判定区内。
on_nullified会像标准的延时锦囊一样作出决定。如果判定阶段被【无懈可击】,那么根据是否为传递型延时锦囊(由一个额外的成员movable决定),决定是弃置或者移动到下家。
事实上,延时锦囊弃置的语句为:
local reason = sgs.CardMoveReason(
sgs.CardMoveReason_S_REASON_NATURAL_ENTER, to:objectName()
)
room:throwCard(self, reason, nil)
以上两个成员便可以省略不写。
这里还会多出一个成员movable。它的取值为布尔类型,true代表传递型延时锦囊(如【闪电】),false代表非传递型延时锦囊(如【乐不思蜀】)。
另外,由于锦囊位于判定区,on_effect会在判定阶段才执行,其中也就是延时锦囊的判定效果。这与subclass无关。
在on_effect中也可以调用on_nullified的效果,同样无需额外代码,方法是使用函数self.on_nullified(self, player)。
制作一张装备牌
相比之下,装备牌的代码就不是特别复杂了,因为许多东西是已经预定好了的。
首先我们看武器:
name,
class_name,
suit,
number,
range,
on_install,
on_uninstall,
}
其中一些成员已经在前面介绍过了,我们说一下与之前不同的成员含义。
range:整数类型,顾名思义,是用来指定武器攻击范围的。值为1,就是攻击范围为1,以此类推。不要考虑什么动态的攻击范围(比如回合开始判定,并以判定牌点数作为攻击范围),那在lua里面只用range是无法实现的。
on_install:当该装备进入装备区时所执行的函数。这是装备牌的核心成员,有了它,才能赋予卡牌对应的装备技能。后面我们将会详细地说明一些典型的用法。
on_uninstall:当该装备离开装备区时所执行的函数。比如【白银狮子】的回复体力效果。
然后以下是防具的创建形式:
name,
class_name,
suit,
number,
on_install,
on_uninstall,
}
与武器牌几乎一样,只不过少了个range而已。
骑乘牌没有对应的创建函数,但是可以通过另外的方式来创建。
其创建的代码如下:
local
这是创建防御马(也就是常说的+1马)的代码,如果创建-1马,那么就把"DefensiveHorse"换成"OffensiveHorse"就可以了。
为装备牌赋予技能
和武将技能类似,装备技能也分为视为技(如丈八蛇矛)、触发技(如官方包的所有防具)、手牌上限技(民间包的【圣光白衣】)、距离技、禁止技等。
由于手牌上限技、距离技、禁止技本身就是全局技能,所以无需在创建函数中再写入相关代码。
如【圣光白衣】的手牌上限+2效果:
light_coatKeep = sgs.CreateMaxCardsSkill{
name = "lightcoatKeep",
extra_func = function(self, target)
local armor = target:getArmor()
if armor and armor:isKindOf("LightCoat") then
return 2
end
end
}
记得,因为这些技能并未给予任何武将,所以技能都需要添加进sgs.Sanguosha中,才能有效。
圣光白衣的类名为LightCoat,便能让所有在装备区装有该装备的角色手牌上限+2。
如果是武器,那条件就是
target:getWeapon() and target:getWeapon():isKindOf(
或target:getWeapon () and target: getWeapon ():objectName() ==
但如果是防御马,那就只能是
target:getDefensiveHorse() and target: getDefensiveHorse():objectName() ==
为什么不用isKindOf(),这是因为所有的防御马的class_name都是"DefensiveHorse",所以isKindOf()在这里不起作用。
其次是触发技。虽然可以通过global=true来实现,但其实有一种方法可以减少运行时候的计算量,这是通过以下的语句实现的:
on_install = function(self,player)
local room = player:getRoom()
local skill = sgs.Sanguosha:getTriggerSkill("skillname")
room:getThread():addTriggerSkill(skill)
end
那么,只有当一名玩家装备了该装备,系统才会开始执行这个触发技。这样做的话即使没有global=true也会生效。注意技能仍旧需要加进sgs.Sanguosha中。
最后是视为技和锁定视为技。视为技不仅要在on_install中执行,也要在on_uninstall中设置。如下所示:
on_install = function(self,player)
local room = player:getRoom()
room:attachSkillToPlayer(player, "skillname")
end,
on_uninstall = function(self,player)
local room = player:getRoom()
room:detachSkillFromPlayer(player, "skillname")
end,
这样做时,如果skillname和装备牌的名称相同,则右下方不会出现相应的技能按钮,但是可以通过直接点击装备牌来发动技能。如果不同的话,那么就只能通过点击右下方的技能按钮来进行了。锁定视为技也是类似的,只不过没有点击发动的动作。
对于骑乘的触发技,就只有通过global成员来进行了。而视为技和锁定视为技的添加,则也需要通过触发技来实现。亦即事件为sgs.CardsMoveOneTime,在move.from_places中骑乘对应的位置,以及move.to为sgs.Player_PlaceEquip的时候分别失去和添加技能,这两者就相当于on_uninstall和on_install。
如此的触发技如下:
name = "skillname",
events = sgs.CardsMoveOneTime,
global = true,
can_trigger = function(self, target)
return target
end,
on_trigger = function(self, event, player, data)
local move = data:toMoveOneTime()
local l = move.card_ids:length() - 1
if move.from and
move.from:objectName() == player:objectName() then
for i=0, l, 1 do
if move.from_places:at(i) == sgs.Player_PlaceEquip then
local card =
sgs.Sanguosha:getCard(move.card_ids:at(i))
if card:objectName() == "horsename" then
--填失去骑乘horse时候执行的内容,注意目标为player
end
end
end
end
if move.to and
move.to:objectName() == player:objectName() then
if move.to_place == sgs.Player_PlaceEquip then
for i=0, l, 1 do
local card =
sgs.Sanguosha:getCard(move.card_ids:at(i))
if card:objectName() == "horsename" then
--填装入骑乘horse时候执行的内容,注意目标为player
end
end
end
end
end
}
如果有多个骑乘需要加效果,那么最好使用同一个触发技,而在判断card的objectName的时候产生分支,这样可以提高性能。
事实上,通过这种方式所写的触发可以有比on_install/uninstall更强大的效果,只不过后者比较简易而已。
版权声明:本站【趣百科】文章素材来源于网络或者用户投稿,未经许可不得用于商用,如转载保留本文链接:https://www.qubaik.com/answer/128666.html