在教程01和02中介绍了如何新建一个技能、新建增益状态以及技能属性的详细解释。但是只依靠这些基本只能做到与游戏中已存在技能相同或相似的机制,很难做出更独特的技能机制。要做到这一点,就需要使用故事编辑器来编写Osiris脚本做到。本节教程会介绍故事编辑器的简单用法、Osiris脚本的基本语法以及几个我Mod中的实例讲解。官方的介绍。
故事编辑器
首先点击图标打开故事编辑器,在里边可以查看管理所有的脚本:
打开后左边列出了所有的故事脚本,其中灰色高亮的是游戏自带的。这里每一个脚本被称为一个“Goal”。可以看到有些Goal有子项(左侧有加号)。在左侧选择脚本后,右侧会显示其内容。最下边会显示编译后的报错信息,双击报错信息自动定位到出错位置。左上角File中有一些常用功能,例如保存、编译、生成定义头文件等。项目中第一次使用故事编辑器时需要先生成定义 (Generate Definitions)。另外生成成功(没有报错)的脚本要立即生效需要执行一次 Reload。
Osiris基本语法
完成父Goal目标
前面提到,这里每一个脚本被称为一个Goal,子Goal如果想要运行生效,就需要先完成父Goal的目标。完成目标可以理解成脚本加载的时机。完成目标需要调用函数GoalCompleted;
。这里你可以把你的脚本新建在__Start
下,成为它的子Goal,你也可以自己新建一个父Goal并完成它。下面我以自建父Goal为例。
方法一:
直接在父Goal的KB区域填写如下代码:
IF
GameStarted(_,_)
THEN
GoalCompleted;
这段代码的意思是在游戏开始后加载子脚本。
方法二:
在INIT区域填入
DB_Mytest_ModStarted(1);
这里必须以DB_
开头,名字独一无二即可,具体原因后边会讲到。
随后在KB区域填入:
IF
DB_Mytest_ModStarted(1)
THEN
GoalCompleted;
然后子Goal就会在父Goal完成后被加载了。
Osiris脚本基本结构和语法
你可能注意到了在查看脚本的编辑器界面有三个区域:INIT、KB、EXIT。INIT区域的代码会在Goal初始化时运行。KB(knowledge base)区域的代码在Goal初始化就生效,也就是说KB的代码也可以响应INIT区域的变化。EXIT的代码会在Goa完成时运行。
Osiris的数据类型
在Osiris中有以下数据类型:
- INTEGER ,32位整型变量,例如-10,-5,1,2,3,4
- INTEGER64 ,64位整型变量,例如-99999999999, -4, 0, 10, 12345678901
- REAL,实数,例如-10.0, -0.1, 0.0, 0.5, 100.123
- STRING ,字符串,例如"A", "ABC", "_This is a string"
- GUIDSTRING,一个对象的GUID,例如音效资源、物品、人物等,例如123e4567-e89b-12d3-a456-426655440000
- CHARACTERGUID ,一个游戏中人物的GUID,例如(CHARACTERGUID)123e4567-e89b-12d3-a456-426655440000
- ITEMGUID ,物品的GUID
- TRIGGERGUID ,触发器GUID
- SPLINEGUID , spline GUID
- LEVELTEMPLATEGUID ,关卡模板的GUID
数据库Databases
Osiris的DB类型必须以DB_
开头,括号内是你要添加的数据,数据类型参考上文。通常推荐写一个前缀YourPrefix
,这样可以保证DB名字的独一无二。这是因为所有的DB都共享一个命名空间,包括官方的DB变量和其他加载的Mod的DB变量。
DB_YourPrefix_DatabaseName(TypedValue1[,TypedValue2..]);
每一个添加的数据被称为一个fact
,同一个DB变量下可以有多个fact
。
// Type: String
DB_Overview_StringDB("SomeString");
DB_Overview_StringDB("AnotherString");
// Type: float
DB_Overview_FloatDB(1.0);
// Type: Integer, String
DB_Overview_IntegerStringDB(0, "String0");
DB_Overview_IntegerStringDB(1, "String1");
DB_Overview_IntegerStringDB(1, "String1");
// Type: GUIDSTRING (not CHARACTERGUID, because no typecast)
DB_Overview_Origins(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f);
DB_Overview_Origins(CHARACTERGUID_S_Player_Beast_f25ca124-a4d2-427b-af62-df66df41a978);
DB_Overview_Origins(CHARACTERGUID_S_Player_Lohse_bb932b13-8ebf-4ab4-aac0-83e6924e4295);
DB_Overview_Origins(CHARACTERGUID_S_Player_RedPrince_a26a1efb-cdc8-4cf3-a7b2-b2f9544add6f);
DB_Overview_Origins(CHARACTERGUID_S_Player_Sebille_c8d55eaf-e4eb-466a-8f0d-6a9447b5b24c);
DB_Overview_Origins(CHARACTERGUID_S_Player_Fane_02a77f1f-872b-49ca-91ab-32098c443beb);
// Type: CHARACTERGUID, String
DB_Overview_Origins((CHARACTERGUID)CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f, "IFAN");
DB_Overview_Origins(CHARACTERGUID_S_Player_Beast_f25ca124-a4d2-427b-af62-df66df41a978, "BEAST");
DB_Overview_Origins(CHARACTERGUID_S_Player_Lohse_bb932b13-8ebf-4ab4-aac0-83e6924e4295, "LOHSE");
DB_Overview_Origins(CHARACTERGUID_S_Player_RedPrince_a26a1efb-cdc8-4cf3-a7b2-b2f9544add6f, "RED PRINCE");
DB_Overview_Origins(CHARACTERGUID_S_Player_Sebille_c8d55eaf-e4eb-466a-8f0d-6a9447b5b24c, "SEBILLE");
DB_Overview_Origins(CHARACTERGUID_S_Player_Fane_02a77f1f-872b-49ca-91ab-32098c443beb, "FANE");
从第三类和第四类例子中可以看到DB变量是可以重载的,只需要它们有不同的变量数量。
删除fact
需要使用NOT指令
NOT DB_Overview_StringDB("SomeString");
NOT DB_Overview_StringDB("AnotherString");
DB可以作为全局变量使用,可以保存一些临时信息、作为判断的标志状态等。
基本规则Rules
一条规则Rule由一个或多个触发条件开始,随后是0个或多个额外条件,最后是1个或多个执行动作语句。当触发条件和额外条件都满足时执行动作语句。
IF
触发条件
[
AND
触发条件 或 额外条件
]
[
AND
触发条件 或 额外条件
..
]
THEN
动作语句1;
[
动作语句2;
..
]
注意:触发条件和额外条件结尾没有分号,而动作语句后要加英文分号。
触发条件有两种:
-
Osiris Event 事件:游戏定义的事件,例如角色死亡、受到攻击等,注意Event条件只能放在第一个触发条件位置,也就是开头。
-
Database触发:当一个Database fact被添加时(可以是第一个触发条件)或者删除fact。例如:
IF DB_IsPlayer(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f) // 触发条件1 AND NOT DB_Dead(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f) // 触发条件2 THEN DB_IfanIsAliveAsAPlayer(1);
这个例子中在两种情况会触发:
- 当
DB_IsPlayer()
添加伊凡 并且DB_Dead()
中没有伊凡的fact时 - 或者
DB_IsPlayer()
和DB_Dead()
都添加了伊凡,不过随后DB_Dead()
中的伊凡fact会被删除
- 当
额外条件包含以下几种形式:
- Osiris Query :Osiris定义的查询条件,查看引擎提供的Query有哪些,点我。
- 自定义 Query :详情见此,点我。。
- 数据比较 :使用比较符号:判断相等
==
、不等!=
、大于>
、小于<
、大于等于>=
、小于等于<=
IF
DB_IsPlayer(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f) // 触发条件
AND
CharacterIsDead(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f, 0) // 额外条件
AND
DB_Avatars(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f) // 触发条件 (!)
THEN
DB_IfanIsAnAvatarPlayerAndNotDead(1);
CharacterIsDead
是一个查询角色是否死亡的Query,它有两个参数,角色GUID和一个整数。这个整数代表是否死亡(0没死,1死了)。这个查询会在所有参数都匹配上时成立,即结果为真。所以这个Query就是询问伊凡死了没有。这个Query也可以写成以下形式,只不过更繁琐:
IF
DB_IsPlayer(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f) // 触发条件
AND
CharacterIsDead(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f, _Result) // 额外条件
AND
_Result == 0 // 额外条件
AND
DB_Avatars(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f) // 触发条件 (!)
THEN
DB_IfanIsAnAvatarPlayerAndNotDead(1);
这段规则的意思是在DB_IsPlayer
和DB_Avatars
中第一次出现伊凡后,并且伊凡活着,那么就给DB_IfanIsAnAvatarPlayerAndNotDead
添加一个fact
1。注意,这里只有两个DB变量 DB_IsPlayer
和DB_Avatars
都是第一次出现伊凡GUID的fact
才会执行动作。
动作语句Actions:
动作语句Actions负责执行一些函数来改变游戏状态或更新数据库,它包含以下形式:
- Osiris Call :Osiris定义的函数,用来改变某个游戏对象的状态。查看引擎提供的Call有哪些,点我
- Procedure :自定义的流程,与其他编程语言的函数概念类似。如何定义点这里。
- Database操作:例如添加或删除某个DB的
fact
。删除fact
使用 NOT 关键字。
IF
DB_IsPlayer(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f) // 触发条件
AND
CharacterIsDead(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f, 1) // 额外条件
THEN
CharacterResurrect(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f); // 动作语句
这段代码的作用是在DB_IsPlayer
中第一次出现伊凡GUID并且伊凡是死亡状态时,复活他。注意动作语句后以英文分号结尾,而条件语句不用。
定义变量:
有时候你不知道具体的值或者想获取某个值,这时候就需要使用变量来实现。在Osiris中你不需要定义变量的类型,变量的生命周期被限定在一条规则Rule、流程Procedure或者查询Query中。
示例:
IF
DB_IsPlayer(_Player)
AND
CharacterIsDead(_Player, 1)
THEN
CharacterResurrect(_Player)
与前面专注于复活伊凡不同,这里的代码实现复活死亡的任意角色,只要他的GUID第一次出现在DB_IsPlayer
中。
More powerful uses of variables are demonstrated in the section on Rule Matching below.
自定义查询Query
使用自定义的Query语句可以很方便地实现或条件(是的,规则Rule中只能AND做衔接,不能是其他的)。格式为:
QRY
QRY_MyPrefix_QueryName((TYPE1)_Param1[,(TYPE2)_Param2..])
[
AND
ExtraCondition1
..
]
THEN
Action1;
[
Action2;
..
]
第二行QRY_MyPrefix_QueryName
是自定义的Query名字,如同DB一样。紧跟着的括号内是本条Query用到的参数。
并且Query和Database一样可以通过设置不同的参数数量进行重载。并且相同名字的Query们在被调用时会全部执行。举例:
QRY
QRY_Overview_CharacterIsIncapacitated((CHARACTERGUID)_Char)
AND
HasActiveStatus(_Char,"FROZEN",1)
THEN
DB_NOOP(1);
QRY
QRY_Overview_CharacterIsIncapacitated(_Char)
AND
HasActiveStatus(_Char,"PETRIFIED",1)
THEN
DB_NOOP(1);
QRY
QRY_Overview_CharacterIsIncapacitated(_Char)
AND
HasActiveStatus(_Char,"KNOCKED_DOWN",1)
THEN
DB_NOOP(1);
IF
CharacterReceivedDamage(_Char)
AND
// check whether _Char is FROZEN, PETRIFIED or KNOCKED_DOWN
QRY_Overview_CharacterIsIncapacitated(_Char)
THEN
DB_Overview_CowardAttackingIncapacitatedCharacter(1);
在这段代码中定义了三个Query,名字为QRY_Overview_CharacterIsIncapacitated
,分别判断是否有冻结状态、石化状态和击倒状态并什么也不做(DB_NOOP(1);
意思往这个数据库中插入一条值为1的fact
,没有实际效果,常被用来占位,类似Python中的pass)。在随后的规则语句中如果满足三个QRY_Overview_CharacterIsIncapacitated
的其中一个条件,那么就会执行一个插入fact的操作。
另外你可能注意到了只有第一个Query指定了类型,这点也是和DB类似的。
自定义流程Procedure
流程Procedure的概念很类似于其他编程语言中的函数概念。这里的Procedure就是把一些关联的动作语句或者满足某个条件而执行的语句合并起来。
Procedure的格式如下:
PROC
PROC_MyPrefix_ProcName((TYPE1)_Param1[,(TYPE2)_Param2..])
[
AND
ExtraCondition1
..
]
THEN
Action1;
[
Action2;
..
]
Procedure的定义和Query很相似,不过多阐述了。举例:
PROC
PROC_Overview_TeleportAlive((CHARATERGUID)_Char)
AND
CharacterIsDead(_Char, 1)
THEN
CharacterResurrect(_Char);
IF
CharacterReceivedDamage(_Char)
THEN
PROC_Overview_TeleportAlive(_Char);
这段代码定义了一个名为PROC_Overview_TeleportAlive
的流程,流程用于复活死亡角色。并在角色受到伤害后执行该流程。注意和Query使用在额外条件处不同,流程Procedure应在使用在动作语句Action位置,也就是在THEN关键字后边,并且因为是Action语句,要以英文分号结尾。
如何查看有哪些Osiris定义的event、query、call
游戏引擎为我们预先定义了游戏事件、游戏状态的查询函数和改变游戏状态的函数。在生成了头文件定义(CTRL+F7
)后,就可以查看可以使用的全部函数。位置在Divinity Original Sin 2\DefEd\Data\Mods\你的Mod名称\Story\RawFiles\story_header.div
。可以使用Vs Code、Notepad++等文本查看器打开。打开后就可以看到所有的可用函数,例如:
query IntegerSum([in](INTEGER)_A, [in](INTEGER)_B, [out](INTEGER)_Sum) (2,0,0,1)
query Real([in](INTEGER)_I, [out](REAL)_R) (2,0,14,1)
call TransferItemsToUser((CHARACTERGUID)_Character) (1,0,44,1)
event CharacterUsedLadder((CHARACTERGUID)_Character) (3,0,305,1)
在函数名前标记了它是属于哪一类,括号内说明了参数类型和参数名。最后一个括号不用管,像(2,0,0,1)之类的。通过这些信息很容易判断函数的功能,如果你还是拿不准,可以尝试在此查询。
另外如果安装了Norbyte’s ositools,并且定义使用了其函数,那么除了引擎定义的函数外,还可以在文件末尾看到Norbyte定义的额外函数。里边有很多扩展的函数,其API文档在此。Norbyte定义的额外函数都以NRD开头。
实例讲解
在Mod 增强盾战中,我想要达到一个盾击的技能,在对敌人造成伤害的同时也对自己造成伤害。要达到这一效果就可以使用Osiris脚本来实现。
IF
CharacterUsedSkill(_, "Target_ShieldAttack", "target", "Warrior")
THEN
DB_UsedShieldAttack(1);
IF
NRD_OnPrepareHit(_Target,_Instigator, _Damage, _HitHandle)
AND
DB_UsedShieldAttack(1)
AND
IntegerDivide(_Damage, 2 ,_DamageToSelf)
THEN
NOT DB_UsedShieldAttack(1);
ApplyDamage(_Instigator, _DamageToSelf, "Physical", _Instigator);
在这段代码中的第一个规则中,如果有角色释放了技能盾击,技能ID为Target_ShieldAttack
,那么就在DB_UsedShieldAttack
中插入一个fact。
第二个规则:在技能命中前并且DB_UsedShieldAttack
中有值为1的fact,那么就把伤害的一半作为物理伤害返回给施法者。
第二个例子会更加复杂,牵扯到更多流程。在这段代码中,实现了盾战Mod中分享抗性这个技能的功能。这个技能会给两个人施加SHARING_RESISTANCE
状态,并且分享两个人的最高元素抗性。
代码比较长,首先来看第一部分:
// When apply status, play 5 beam effects, give resistance boost
IF
NRD_OnStatusAttempt(_Target,"SHARING_RESISTANCE",_StatusHandle,_Instigator)
AND
_Target != _Instigator
AND
PlayLoopBeamEffect(_Target, _Instigator, "RS3_FX_GP_Status_GuardianAngel_Beam_01", "Dummy_BodyFX", "Dummy_BodyFX",(INTEGER64)_FxHandle1)
AND
PlayLoopBeamEffect(_Target, _Instigator, "RS3_FX_Char_Creatures_Shriker_Lightning_Beam_01", "Dummy_BodyFX", "Dummy_BodyFX",(INTEGER64)_FxHandle2)
AND
PlayLoopBeamEffect(_Target, _Instigator, "RS3_FX_GP_Beams_NecroFireBeam_Loop_01", "Dummy_BodyFX", "Dummy_BodyFX",(INTEGER64)_FxHandle3)
AND
PlayLoopBeamEffect(_Target, _Instigator, "RS3_FX_Skills_Water_ChainHeal_Beam_01", "Dummy_BodyFX", "Dummy_BodyFX",(INTEGER64)_FxHandle4)
AND
PlayLoopBeamEffect(_Target, _Instigator, "RS3_FX_GP_Beams_Telekinesis_01", "Dummy_BodyFX", "Dummy_BodyFX",(INTEGER64)_FxHandle5)
AND
CharacterGetAttribute((CHARACTERGUID)_Target,"FireResistance",_fr1)
AND
CharacterGetAttribute((CHARACTERGUID)_Target,"EarthResistance",_er1)
AND
CharacterGetAttribute((CHARACTERGUID)_Target,"WaterResistance",_wr1)
AND
CharacterGetAttribute((CHARACTERGUID)_Target,"AirResistance",_ar1)
AND
CharacterGetAttribute((CHARACTERGUID)_Target,"PoisonResistance",_pr1)
AND
CharacterGetAttribute((CHARACTERGUID)_Instigator,"FireResistance",_fr2)
AND
CharacterGetAttribute((CHARACTERGUID)_Instigator,"EarthResistance",_er2)
AND
CharacterGetAttribute((CHARACTERGUID)_Instigator,"WaterResistance",_wr2)
AND
CharacterGetAttribute((CHARACTERGUID)_Instigator,"AirResistance",_ar2)
AND
CharacterGetAttribute((CHARACTERGUID)_Instigator,"PoisonResistance",_pr2)
THEN
//NRD_DebugLog("shared resitance:");
PROC_GiveResistanceBoost(_Target,_fr1,_Instigator,_fr2,"FireResistance");
PROC_GiveResistanceBoost(_Target,_er1,_Instigator,_er2,"EarthResistance");
PROC_GiveResistanceBoost(_Target,_wr1,_Instigator,_wr2,"WaterResistance");
PROC_GiveResistanceBoost(_Target,_ar1,_Instigator,_ar2,"AirResistance");
PROC_GiveResistanceBoost(_Target,_pr1,_Instigator,_pr2,"PoisonResistance");
PROC_GiveResistanceBoost(_Instigator,_fr2,_Target,_fr1,"FireResistance");
PROC_GiveResistanceBoost(_Instigator,_er2,_Target,_er1,"EarthResistance");
PROC_GiveResistanceBoost(_Instigator,_wr2,_Target,_wr1,"WaterResistance");
PROC_GiveResistanceBoost(_Instigator,_ar2,_Target,_ar1,"AirResistance");
PROC_GiveResistanceBoost(_Instigator,_pr2,_Target,_pr1,"PoisonResistance");
DB_FxHandle(_FxHandle1,_FxHandle2,_FxHandle3,_FxHandle4,_FxHandle5);
DB_ShareResitCharacters(_Target,_Instigator);
//NRD_DebugLog("Beam Added");
PROC
PROC_GiveResistanceBoost((CHARACTERGUID)_Target,(INTEGER)_TargetResist,(CHARACTERGUID)_Compare,(INTEGER)_CompareResist,(STRING)_ResistType)
AND
_CompareResist > _TargetResist
AND
IntegerSubtract(_CompareResist,_TargetResist,_Boost)
THEN
NRD_CharacterSetPermanentBoostInt(_Target, _ResistType, _Boost);
CharacterAddAttribute(_Target, "Dummy", 0); // Force boost sync
//NRD_DebugLog(_ResistType);
虽然代码有点长,但是分为了几个部分,每一部分的功能还是很明确的,这么长的原因主要是有5个抗性需要处理。
首先触发条件是在角色被上SHARING_RESISTANCE
这个增益状态之前,额外条件是状态来源不能是是自己,这个条件是防止重复执行下边的语句。因为自己给自己上状态也会触发这个条件。
然后的语句是给自己和目标添加链接双方的5个光束特效,通过函数PlayLoopBeamEffect
来实现。
然后是10个查询基本属性的语句。用来获取双方的5个元素抗性。
在这里定义的流程PROC_GiveResistanceBoost
,是用来给目标提供相应抗性的,但是如果当前目标已经是两者中的高抗性,那就不需要执行这段动作。
随后在DB_FxHandle
和DB_ShareResitCharacters
中存储了5个光束特效的Handle和两个角色GUID。目的是在状态消失可以停止播放特效,并且把双方的抗性还原。
// When SHARING_RESISTANCE is removed, stop playing beam effects
// and set a mark to remove resistance boost
IF
CharacterStatusRemoved(_Target,"SHARING_RESISTANCE",_Instigator)
AND
DB_FxHandle(_FxHandle1,_FxHandle2,_FxHandle3,_FxHandle4,_FxHandle5)
THEN
StopLoopEffect(_FxHandle1);
StopLoopEffect(_FxHandle2);
StopLoopEffect(_FxHandle3);
StopLoopEffect(_FxHandle4);
StopLoopEffect(_FxHandle5);
NOT DB_FxHandle(_FxHandle1,_FxHandle2,_FxHandle3,_FxHandle4,_FxHandle5);
DB_RemoveCasterResistanceBoost(1);
// remove resistance boost
IF
DB_RemoveCasterResistanceBoost(1)
AND
DB_ShareResitCharacters(_Target,_Instigator)
THEN
//NRD_DebugLog("Removed:");
NRD_CharacterSetPermanentBoostInt((CHARACTERGUID)_Target,"FireResistance", 0);
NRD_CharacterSetPermanentBoostInt((CHARACTERGUID)_Target,"EarthResistance", 0);
NRD_CharacterSetPermanentBoostInt((CHARACTERGUID)_Target,"WaterResistance", 0);
NRD_CharacterSetPermanentBoostInt((CHARACTERGUID)_Target,"AirResistance", 0);
NRD_CharacterSetPermanentBoostInt((CHARACTERGUID)_Target,"PoisonResistance", 0);
CharacterAddAttribute(_Target, "Dummy", 0);
NRD_CharacterSetPermanentBoostInt((CHARACTERGUID)_Instigator,"FireResistance", 0);
NRD_CharacterSetPermanentBoostInt((CHARACTERGUID)_Instigator,"EarthResistance", 0);
NRD_CharacterSetPermanentBoostInt((CHARACTERGUID)_Instigator,"WaterResistance", 0);
NRD_CharacterSetPermanentBoostInt((CHARACTERGUID)_Instigator,"AirResistance", 0);
NRD_CharacterSetPermanentBoostInt((CHARACTERGUID)_Instigator,"PoisonResistance", 0);
CharacterAddAttribute(_Instigator, "Dummy", 0);
NOT DB_RemoveCasterResistanceBoost(1);
NOT DB_ShareResitCharacters(_Target,_Instigator);
这段代码用于状态消失后停止播放特效,并把抗性增益清零。
0