战斗

2018/09/19 Mmo-Game

战斗系统设计

目录

类的继承关系

/**
 * 抽象的模块基类,挂在character上的模块都需要继承此模块
 */
public abstract class AbstractCharacterModule implements ModuleInteface
{
    ...
    protected AbstractCharacter character;

    public AbstractCharacter getCharacter()
    {
        return character;
    }
    ...
}

挂在character上的模块都需要继承AbstractCharacterModule此模块,因为该模块存放着AbstractCharacter的引用,

我们通过getCharacter.getXXMoudle就可以拿到对应需要的挂在AbstractCharacter下的模块。

所以AbstractCharacterModule相当于一个中转站。

/**
 * 技能模块基类
 */
public abstract class AbstractSkillModule extends AbstractCharacterModule

技能抽象模块基类需要继承AbstractCharacterModule,以便拿到对应需要的挂在AbstractCharacter下的模块。

/**
 * 生物类的技能模块,是技能系统的基础,有逻辑也有数据
 */
public class CharacterSkillModule extends AbstractSkillModule

CharacterSkillModule继承技能模块基类,并且CharacterSkillModule需要挂在AbstractCharacter生物特性基类。

/**
 * 生物基类 玩家、npc、宠物都继承与此基类 Created by wangqiang on 2017/7/6.
 */
public abstract class AbstractCharacter extends BPObject
{
    /**
     * 技能模块
     */
    protected CharacterSkillModule skillModule = new CharacterSkillModule(this);
}

CharacterSkillModule技能模块需要怪在生物基类下,因为不只玩家有技能,npc、侠客、宠物…都会有技能。

玩家,npc、侠客、宠物…都是生物,所以继承自生物特性基类AbstractCharacter。

技能和其它模块最相关是buff的派发,所以我们先看buff派发。

任务派发buff,场景事件派发buff,各种道具派发buff…

1.向目标发送一个效果,主要用于非技能模块的效果发送

/**
* 向目标发送一个效果,主要用于技能模块的效果发送
*
* @param sender           效果的发送者
* @param receiver         效果接受者
* @param impactData       效果配置
* @param skillId          技能Id( * 没有填-1) ,可使用 {@link SkillCommonConstant#NO_SKILL_ID} 常量
* @param skillSign        技能标识 ( * 没有填 0),可使用 {@link SkillCommonConstant#NO_SKILL_SIGN} 常量
* @param skillLevel       技能等级 ( * 没有填 0),可使用 {@link SkillCommonConstant#NO_SKILL_LEVEL} 常量
* @param actualSkillLevel 效果步长,( * 没有填 0),可使用 {@link SkillCommonConstant#NO_SKILL_ACTUAL_LEVEL} 常量
*/
public int sendImpactToForSkill(AbstractCharacter sender, AbstractCharacter receiver, DictStandardImpactData impactData, int skillId,int skillSign, int skillLevel,int actualSkillLevel)
{
    if (impactData == null)
    {
        return BPErrorCodeEnum.SKILL_SKILL_IMPACT_ID_IS_NOT_EXISTED;
    }

    return doSendImpactTo(sender, receiver, impactData,  skillId, skillSign,  skillLevel, actualSkillLevel);
}

向目标发送一个效果,主要用于技能模块的效果发送接口。

/**
* 向目标效果
*
* @param sender           效果的发送者
* @param receiver         效果接受者
* @param impactData       效果配置
* @param skillId          技能Id( * 没有填-1) ,可使用 {@link SkillCommonConstant#NO_SKILL_ID} 常量
* @param skillSign        技能标识 ( * 没有填 0),可使用 {@link SkillCommonConstant#NO_SKILL_SIGN} 常量
* @param skillLevel       技能等级 ( * 没有填 0),可使用 {@link SkillCommonConstant#NO_SKILL_LEVEL} 常量
* @param actualSkillLevel 效果步长,( * 没有填 0),可使用 {@link SkillCommonConstant#NO_SKILL_ACTUAL_LEVEL} 常量
* @return
*/
private int doSendImpactTo(AbstractCharacter sender, AbstractCharacter receiver, DictStandardImpactData impactData, int skillId,int skillSign, int skillLevel,int actualSkillLevel)
{
    ...
    // 判断能否施加效果
    int rs = canImpactTo(sender, receiver, impactData, actualSkillLevel);

    ...
    // 创建效果实例,例如无敌buff配的逻辑id为1,即持续性技能
    int impactLogicID = impactData.getLogicID();
    AbstractSkillImpact skillImpact = SkillHelper.createSkillImpact(impactLogicID, sender.getScene());

     //初始化buff时长,根据StandardImpact表里配置durationTimeSpecial时长
    skillImpact.init(sender.getObjectID(), receiver,impactData);
    ...

    //计算buff时长,其它修改引起修正
    skillImpact.calcBuffTime(receiver, sender);

    ...
    // 派发逻辑
    int receiveRS = receiver.getSkillModule().receivedImpact(skillImpact);

}

列举了些重要的处理,buff时长读表,派发逻辑receivedImpact

/**
* 接收一个impact效果
*
* @param skillImpact
* @return
*/
public int receivedImpact(AbstractSkillImpact skillImpact)
{
    ...
     // 能否接受效果检测
    int canRs = canReceiveImpact(skillImpact);
    if(canRs < 0)
    {
        return canRs;
    }
    ...

    // 处理互斥
    int rs = dealImpactMutexAndRepeat(skillImpact);
    if (rs < 0)
    {
        return rs;
    }
    ...
}

能否接受效果检测,例如:之前上了一个增益类型的buff无敌,那么此时接收一个负面类的buff不通过。

处理互斥,走表配置。

1.非持续性不处理互斥。

2.来源于同一个单位的同一BUFF组的叠加规则:(可叠加的BUFF在层数满之后只刷新BUFF时间)
    0:替换,互斥优先级低的无法替换高级的
    1:刷新BUFF时间(刷新BUFF时间的效果不会充值周期触发效果的周期循环)
    2:添加不影响现有
    3:无法添加新BUFF
(前两种情况下,BUFF等级低于目标身上同组BUFF等级的无法生效)

3.不同源BUFF叠加规则:
    0:直接替换(低级BUFF不可替换高级的)
    1:刷新BUFF时间(同等级BUFF刷新BUFF时间,BUFF来源不变,高级的BUFF覆盖低级的,BUFF来源改为高级BUFF的施加者)
    2:各自添加不互相影响

现在无敌buff配的是,互斥组配的是1也就是自身互斥,同源叠加规则配的是0,即替换。

那么假设同时给自身上两个无敌buff,第一个无敌buff直接被第二个无敌buff替换掉。

/**
    * 处理BUFF的互斥
    * @param mutexType 互斥类型
    * @param buff      身上的BUFF
    * @param addImpact 要添加的BUFF效果
    * @return
    */
private int doMutexImpact(int mutexType,AbstractSkillImpact buff,AbstractSkillImpact addImpact)
{
    ...
    ...
     else
    {
        // 中断互斥顶替掉的buff
        buff.onBeMutexBreeak(getCharacter(),addImpact);
        buff.breakImpact(getCharacter());

        return 0;
    }
    ...
    ...
}

同时给自身上两个无敌buff,可以根据配置规则让第一个无敌buff直接被第二个无敌buff替换掉。

需要先调用breakImpact(AbstractCharacter character)中断第一个buff效果。

然后将新来的无敌buff添加到buff列表里。

技能系统图示

技能系统服务器客户端流程:

技能系统控制流程抽象:

技能释放流程

1.前端使用技能请求

// 使用技能请求
message CSUseSkillRequest{//20300
	required int32 skillID = 1; // 技能id
	required int32 targetObjectID = 2; // 选择的目标id
	required int32 rotation = 3; // 朝向, 传0->360
	required int32 sign = 4; // 技能标识
	required int32 type = 5; // 类型:0玩家 1宠物 2傀儡 3侠客
	optional int32 dummyObjectID = 6; // 傀儡ObjectID(用于傀儡释放技能);当type为3时,该值为侠客的KnightID
	repeated int32 paths = 7; //x,y,z坐标结合(位移技能路径)
	repeated int32 hitPoints = 8; // x,y,z生效的打击点坐标集合(目前主要给侠客大招使用)
	optional bool isStandalone = 9; // 是否是单机释放技能(目前主要给侠客大招使用)
}

2.玩家技能释放

目前版本很多废弃了,例如:宠物、轻功,所以我们直接从玩家技能释放开始看。

/**
* 玩家使用技能
*
* @param message
* @param response
* @param actor
* @param isStandalone 是否是单机释放
*/
private void acoterUseSkill(BPFight.CSUseSkillRequest message, BPFight.SCUseSkillResponse.Builder response,
                            Actor actor,boolean isManualMoveSkill, boolean isStandalone)
{
    ...
    ...
    //旧的目标
    int lastTargetID = actor.getAbstractCharacterCommonModule().getTargetObjectID();
    //旧的朝向
    int lastRotation = actor.getRotation();

    //技能id
    int skillID = message.getSkillID();
    //目标id
    int targetID = message.getTargetObjectID();
    //技能标识
    int skillSign = message.getSign();
    ...
    ...

    //检测是否坐骑,坐骑不能使用技能

    //检测变身后能否使用技能

    //检查移动状态能否使用技能

    //设置客户端上传的最新目标和朝向

    //调用useSkill
    result = actor.getSkillModule().useSkill(actor, skillID, skillSign, isStandalone, true);

    //设置技能使用响应客户端

    //技能使用事件

    //客户端使用技能成功广播
}
/**
* 使用技能
*
* @param character 使用者
* @param skillID   技能id
* @param sign      技能标识(客户端释放时指定,服务器主动释放填-1,内部会自动生成)
* @param isStandalone 是否是单机释放
* @param cltUse    是否客户端释放
* @return
*/
public int useSkill(AbstractCharacter character, int skillID, int sign, boolean isStandalone, boolean cltUse)
{
    //检查当前场景能否使用技能

    //是否正在使用技能
    boolean isContinuityUse = isUsingSkill();

    //根据技能id获取,技能模板表、技能编辑表、技能数据表数据

    //根据技能模板表,检查是否释放被动技能错误
    if (!skillTemplate.isActive())
    {
        return BPErrorCodeEnum.SKILL_USE_PASSIVE_SKILL_USE_REPEAT;
    }

    // 获取技能组
    DictSkillGroupData skillGroup = null;
    if (skillTemplate.getSkillGroupType() > 0)
    {
        skillGroup = (DictSkillGroupData) DictSkillGroupData.getRecordById(skillTemplate.getSkillGroupType());
        if (skillGroup == null)
        {
            return BPErrorCodeEnum.SKILL_SKILL_GROUP_NOT_MATCH;
        }
    }

    // 如果当前正在使用技能
    if(isContinuityUse)
    {
        //如果当前正在使用技能,做一个临时缓存
        tempSkillInfo.copyFrom(skillInfo);
    }

    //设置当前技能释放的技能信息
    skillInfo.setSkillGroup(skillGroup);
    skillInfo.setSkillEditorData(skillEditorData);
    skillInfo.setSkillData(skillData);
    skillInfo.setSkillTemplate(skillTemplate);

    // 检测技能使用条件(传入是否客户端释放)
    // 检查cd、消耗、技能组和单体技能检测、位移、自身、目标。
    int result = checkUseSkill(cltUse);

    ...

    // 连携释放,先打断当前技能(正在使用当前技能)
    if(isContinuityUse)
    {
        // 必须先将skillInfo设置回当前技能的,再进行中断操作
        skillInfo.copyFrom(tempSkillInfo);
        tempSkillInfo.clear();

        // 中断当前技能
        doFinishCurrentSkillLogic(true,true);

        // 再将skillInfo设置回要使用的技能
        skillInfo.setSkillGroup(skillGroup);
        skillInfo.setSkillEditorData(skillEditorData);
        skillInfo.setSkillData(skillData);
        skillInfo.setSkillTemplate(skillTemplate);
    }

    // 创建技能逻辑实例
    result = createSkillLogic(character, skillTemplate);
    if (result != 0)
    {
        // 创建技能逻辑失败,返回错误码
        return result;
    }

    // 扣除消耗
    skillConsume();

    // 激活技能逻辑,开始tick
    currentSkillLogic.init();
    currentSkillLogic.setSkillId(skillTemplate.getId());
    currentSkillLogic.setSkillSign(sign);
    currentSkillLogic.setClientUse(cltUse);
    currentSkillLogic.setSkillModule(this);
    currentSkillLogic.setActualSkillLevel(actualSkillLevel);

    // 增加cd时间
    addCooldownForCurrentSkillLogic();

    // 将预使用技能主动位移路径设置到当前使用技能路径中
    if(skillEditorData.getChaseNum() > 0)
    {
        if(cltUse)
        {
            setAllChasePath(this.prepareUseChasePath);
        }
        else
        {
            preCalcChasePath(skillEditorData,this.chasePath);
        }
    }
    
    ...

    // 技能使用事件
    ...

}

检查当前场景能否使用技能。

根据技能id获取,技能模板表、技能编辑表、技能数据表数据。

根据技能模板表,检查是否释放被动技能错误。

根据技能模板表获取技能组。

如果当前正在使用技能,将当前使用的技能信息做一个临时缓存。

设置当前技能释放的技能信息。

检测技能使用条件(传入是否客户端释放)。检查cd、消耗、技能组和单体技能检测、位移、自身、目标。

连携释放,先打断当前技能(正在使用当前技能)。

创建技能逻辑实例。

扣除消耗。

激活技能逻辑,开始tick。

增加cd时间。

将预使用技能主动位移路径设置到当前使用技能路径中。

技能使用事件(切换战斗状态…)。

3.玩家技能释放过程各个方法调用

1.检查cd、消耗、技能组和单体技能检测、位移、自身、目标

* 使用技能检测
*
* @param isCltUse 是否客户端使用
*/
@Override
public int checkUseSkill(boolean isCltUse)
{
    // 此技能是否CD
    int result = checkCooldown(skillTemplate,skillData);

    ...

     // 检测消耗
    result = checkConsume();

    ...

    // 检测技能释放位移
    if(isCltUse)
    {
        // 客户端释放的检测客户端发来的位移点
        result = checkChase(skillEditorData,prepareUseChasePath);
        if (result != 0)
        {
            clearPrepareUseChasePath();
            return result;
        }
    }
    else
    {
        // 服务器释放的检测 冲向瞄准目标的位移类型是否有瞄准目标
        result = checkChaseAim(skillEditorData);
        if (result != 0)
        {
            return result;
        }
    }

    ...
    // 检测自身条件
    result = checkSelf();

    ...
    // 选择目标检测
    result = checkTarget();

}
1.检查技能是否在冷却(也检查公共冷却),根据技能模板表配置skillTemplate

// 此技能是否CD

int result = checkCooldown(skillTemplate,skillData);

/**
* 检查技能是否在冷却(也检查公共冷却)
*/
public int checkCooldown(DictSkillTemplate skillTemplate,DictSkillDataData skillData)
{
    //commonCoolDownRemian公共冷却时间优先级最大,类似:放了一个大招导致某段时间内所有技能都不能使用的情况。
    if(skillTemplate.getIsCommonCD() == 1 && commonCoolDownRemian > 0)
    {
        return BPErrorCodeEnum.SKILL_IN_COOLDOWN;
    }

    ...
}

根据skillTemplate表配置的检测是否受公共冷却影响。

检查表配置的冷却id是否大于0。

迭代缓存的冷却的列表,检测当前的技能是否冷却完毕。

2.检测消耗(只是检测不扣除,根据DictSkillDataData表)

// 检测消耗

result = checkConsume();

**
* 检查技能消耗
*
* @param character     角色类
* @param skillTemplate 技能主表
* @param skillData     技能效果表
* @param skillGroup    技能组
* @return <0 不够消耗的理由  =0足够消耗
*/
public static int checkConsume(AbstractCharacter character, DictSkillTemplateData skillTemplate, DictSkillDataData skillData, DictSkillGroupData skillGroup)
{
    //DictSkillDataData表里配置的节能消耗类型有两列:conditionLogicID_0和conditionLogicID_1
    for(int i = 0 , size =  skillData.getConditionCount(); i < size; i++)
    {
        SkillConsumeTypeEnum consumeType = skillData.getConditions()[i];
        if (consumeType == null)
        {
            continue;
        }
        int valueBase = skillData.getConditionConsumeBases()[i];

        // 计算一次消耗
        int value;
        // 根据类型扣除数值
        switch (consumeType)
        {
          case CONSUME_HP_VALUE:
                case CONDITION_HP_VALUE:
                {
                    int need = valueBase;
                    value = character.getAttributeLogic().getAttribute(AttributeTypeEnum.HP);
                    if (value < need)
                    {
                        return BPErrorCodeEnum.SKILL_USE_SKILL_HP_NOT_ENOUGH;
                    }
                    break;
                }
            ...
            ...
        }

        ...
        ...
    }
    
}

根据DictSkillDataData表配置,只检测没有扣除。

3.客户端使用技能位移检测(SkillDataEditorData技能编辑表)
/**
* 检查技能位移使用
* @param dict
* @return
*/
private int checkChase(DictSkillDataEditorData dict,TIntArrayList pathList)
{
    // 检查主动位移, SkillDataEditor技能编辑表里配置的chaseLength_0、chaseLength_1...chaseLength_20
    // 表里配置的主动位移大于0,那么chaseNum数量累加
    int configChaseNum = dict.getChaseNum();
    if(configChaseNum <= 0)
    {
        return 0;
    }

    // 客户端上发的路径是否为空
    if(CollectionUtils.isBlank(pathList))
    {
        return BPErrorCodeEnum.SKILL_MANUAL_MOVE_SKILL_NO_PATH;
    }
    int pathSize = pathList.size();

    //校验客户端发上来的路径段数是否与表里配置主动位移路段数一致
    //客户端上发的是xyz
    if(pathSize != configChaseNum * 3)
    {
        return BPErrorCodeEnum.SKILL_MANUAL_MOVE_SKILL_PATH_PARAM_ERROR;
    }
    //取最后路径的X坐标
    int finalX = pathList.get(pathSize-3);
    //取最后路径的Z坐标
    int finalZ = pathList.get(pathSize-1);

    // 判断终点是否是阻挡区域
    boolean bolckFlag = getCharacter().getScene().getSceneGirdModule().checkWalkableForClientMove(finalX,finalZ);
    if(!bolckFlag)
    {
        if(BPGlobals.getInstance().isDebugVersion())
        {
            // TODO 修改日志级别,适应压测。压测后重新改成warn日志
            BPLog.BP_FIGHT.debug("【主动位移技能错误】客户端发来的位置是阻挡 [objectID] {} [skillID] {} [pos] ({},{})"
                    , character.getObjectID(), dict.getId(), finalX,finalZ);
        }
        return BPErrorCodeEnum.SKILL_MANUAL_MOVE_DESTINATION_BLOCK;
    }

    ...

    // 校验距离是不是过大
    int[] chaseLengths = dict.getChaseLengths();
    int index = 0;
    //注意这里是将值赋给了临时变量pre,数组才会是地址
    int preX = character.getX();
    int preZ = character.getZ();
    for (int i = 0; i < chaseLengths.length; i++)
    {
        //依次拿出每次移动的值
        int chaseLength = chaseLengths[i];
        if(chaseLength <= 0)
        {
            continue;
        }

        // 增加3米容错
        chaseLength += 300;

        int lengthSQ = chaseLength * chaseLength;

        int x =  pathList.get(index * 3);
        int z =  pathList.get(index * 3 + 2);
        //下一次循环preX,preZ是当前的最新值
        float disSQ = MapUtils.distanceSqBetweenPos(preX, preZ, x, z);

        if(disSQ > lengthSQ)
        {
            if(BPGlobals.getInstance().isDebugVersion())
            {
                // TODO 修改日志级别,适应压测。压测后重新改成warn日志
                BPLog.BP_FIGHT.debug("【主动位移技能错误】客户端发来的位移距离超过技能配置最大距离限制 [objectID] {} [skillID] {} [派发次数] {} [实际距离] {} [最大距离] {}"
                        , character.getObjectID(), dict.getId(), i + 1, (int)Math.sqrt(disSQ),chaseLengths[i]);
            }
            //主动位移类技能的目的地超过范围
            return BPErrorCodeEnum.SKILL_MANUAL_MOVE_SKILL_DESTINATION_TOO_FAR;
        }

        preX = x;
        preZ = z;
        index++;

        if(index >= configChaseNum)
        {
            break;
        }
    }

    ...

}

SkillDataEditor技能编辑表里配置的chaseLength_0、chaseLength_1…chaseLength_20,校验当前技能位移是否合法。

例如袁天罡有些技能是带位移的,那么表里配置的chaseLength值会大于0。客户端在请求使用技能时会发送技能的路径点组到服务端。

服务端会校验路径点组长度是否和表里配置的长度相等。并校验终点坐标的是否阻挡区域。

校验客户端上发的点组移动距离是不是和配置的表距离过大。

4.服务端自身使用技能位移检测(SkillDataEditorData技能编辑表)
/**
* 检测服务器释放的技能,是否有冲向目标的技能
* @param skillDataEditorData
* @return
*/
private int checkChaseAim(DictSkillDataEditorData skillDataEditorData)
{
    //表配置不用校验安全距离
    if(!skillDataEditorData.isChaseLockTarget())
    {
        return 0;
    }

    AbstractCharacter character = getCharacter();
    //当前瞄准的目标object id
    int aimObjectID = character.getAbstractCharacterCommonModule().getAimObjectID();
    if(aimObjectID < 0)
    {
        if(BPGlobals.getInstance().isDebugVersion())
        {
            // TODO 修改日志级别,适应压测。压测后重新改成warn日志
            BPLog.BP_FIGHT.debug("【技能释放错误】首段位移是冲向瞄准目标的技能,释放的时候没有设置瞄准目标 [objectId] {} [objectType] {} [skllId] {}",
                    character.getObjectID(),character.getObjectType().name(),skillDataEditorData.getId());
        }
        //服务器使用的冲向目标技能没有设置瞄准目标
        return BPErrorCodeEnum.SKILL_CHASE_SERVER_USE_NO_AIM_OBJECT;
    }

    return 0;
}

表配置是否校验安全距离。

服务器释放技能必须有当前瞄准的目标object id。

5.检测自身条件
/**
* 技能释放检查自身条件是否满足
*
* @return
*/
public int checkSelf()
{
    DictSkillTemplateData skillTemplate = skillInfo.getSkillTemplate();
    DictSkillDataData skillData = skillInfo.getSkillData();
    ...
    ...

     // 是否携带条件BUFF
    int buffRs = checkConditionBuff(skillTemplate);
    if(buffRs < 0)
    {
        return buffRs;
    }

    // 是否受限于BUFF
    buffRs = checkLimitBuff(skillTemplate);
    if (buffRs < 0)
    {
        return buffRs;
    }

}

是否禁止释放技能

自己是否活着

受身技能的释放限制

是否在移动中(正常移动和特殊移动)

如果正在使用轻功

正在使用绝学

跳跃中

当前等级小于技能需要的最低等级

检测当前玩家身上挂有需要的buff。

是否有控制该技能释放的BUFF,中buff了有些技能不用。

检查玩家特殊条件,变身和读条。

6.技能选择目标检测
/**
* 技能释放目标检测
*
* @return
*/
public int checkTarget()
{
    ...
    ...
    // AOE 不选敌
    boolean isAOE = SkillHelper.isAOE(skillEditorData);
    if (isAOE)
    {
        if(!skillEditorData.isLockTargetFirstOnAOE())
        {
            // AOE不优先索敌目标则清空目标
            character.getAbstractCharacterCommonModule().setTargetObjectID(SkillCommonConstant.TARGET_ID_NONE);
        }
        return 0;
    }

    // 如果技能目标是自己,就将自己设置为目标
    if (skillTemplate.getEffectTargetEnum() == SkillEffectTargetEnum.SELF)
    {
        character.getAbstractCharacterCommonModule().setTargetObjectID(character.getObjectID());
        return 0;
    }
    ...
    ...
}

这里如果当前技能是aoe技能,则清空目标。

如果技能目标是自己,就将自己设置为目标。

2.连携释放,先打断当前技能(正在使用当前技能)

**
* 技能结束
* @param isBreak     是否中断
* @param isSelfBreak 是否自己主动中断(例如连携释放新技能中断当前技能)
*/
private void doFinishCurrentSkillLogic(boolean isBreak, boolean isSelfBreak)
{
    //各种清空

    //打断通知客户端
}

3.创建技能逻辑实例

// 创建技能逻辑实例(现在去掉所有的技能逻辑,只保留17号技能逻辑)

result = createSkillLogic(character, skillTemplate);

/**
* 创建技能逻辑实例,之创建出logic类
*
* @param skillTemplate
* @param skillData
* @param scene         当前所在场景
* @return
*/
public static AbstractSkillLogic createSkillLogic(DictSkillTemplateData skillTemplate, DictSkillDataEditorData dictSkillDataEditorData,DictSkillDataData skillData, AbstractBPScene scene)
{
    ...
    ...
    AbstractSkillLogic skillLogic = scene.getSkillCache().createSkillLogic(17);
    if (skillLogic != null)
    {
        SkillLogicTypeEnum logicTypeEnum = skillTemplate.getLogicTypeEnum();
        //设置技能逻辑类型(顺发,引导,持续,特殊逻辑控制)
        skillLogic.setLogicType(logicTypeEnum);
        skillLogic.setDictSkillTemplateData(skillTemplate);
        skillLogic.setDictSkillDataEditorData(dictSkillDataEditorData);
        skillLogic.setDictSkillDataData(skillData);
    }
    ...
    ...s
}

创建技能逻辑类,设置技能逻辑类型(顺发,引导,持续,特殊逻辑控制)

4.扣除消耗(实际扣除消耗)

/**
* 扣除技能消耗
*
* @return
*/
private int skillConsume()
{
    //DictSkillDataData表里配置的节能消耗类型有两列:conditionLogicID_0和conditionLogicID_1
    for(int i = 0 , size =  skillData.getConditionCount(); i < size; i++)
    {
        SkillConsumeTypeEnum consumeType = skillData.getConditions()[i];
        if (consumeType == null)
        {
            continue;
        }
        int valueBase = skillData.getConditionConsumeBases()[i];

        // 计算一次消耗
        int value;
        // 根据类型扣除数值
        switch (consumeType)
        {
            case CONSUME_HP_VALUE:
            {
                int need = valueBase;
                character.getAttributeLogic().minusOneAttribute(AttributeTypeEnum.HP, need);
                break;
            }
            ...
            ...
        }

        ...
        ...
    }

}

DictSkillDataData表里配置的节能消耗类型有两列:conditionLogicID_0和conditionLogicID_1

根据表配置扣除消耗,实际扣除了!

5.技能标识

// 技能sign
if (sign <= 0)
{
    // 如果释放技能时没有sign,则主动生成一个sign
    sign = getAndIncreaseSkillSign();
}
/**
* 计算当前技能的技能标识
*
* @return
*/
private int getAndIncreaseSkillSign()
{
    //技能标识,主要发送给客户端用于前端伤害识别、技能效果匹配等用途
    skillSignCounter++;
    // 防止有0,客户端要求skill_sign不能为0,所以加1
    return (skillSignCounter & SkillCommonConstant.SKILL_SIGN_MOD) + 1;
}

6.激活技能逻辑,开始tick

// 激活技能逻辑,开始tick
currentSkillLogic.init();
currentSkillLogic.setSkillId(skillTemplate.getId());
currentSkillLogic.setSkillSign(sign);
currentSkillLogic.setClientUse(cltUse);
currentSkillLogic.setSkillModule(this);
currentSkillLogic.setActualSkillLevel(actualSkillLevel);

当前技能逻辑类的一些初始化。

7.增加cd时间

// 增加cd时间

addCooldownForCurrentSkillLogic();

8.将预使用技能主动位移路径设置到当前使用技能路径中

if(skillEditorData.getChaseNum() > 0)
{
    if(cltUse)
    {
        setAllChasePath(this.prepareUseChasePath);
    }
    else
    {
        preCalcChasePath(skillEditorData,this.chasePath);
    }
}

9.使用技能事件,清楚相关身上的buff

// 使用技能事件,清楚相关身上的buff

onStatChange(character, SkillStateChangeEnum.START_ATTACK);

/**
* 相关状态变化时调用
*
* @param sender 发生变化的发送者。
*               比如玩家受击,sender为发出攻击的玩家
*               切换场景由于是玩家自己主动驱动,所以sender为玩家自己
*               1:下线
*               2:切换场景
*               3:开始移动
*               4:玩家受到伤害
*               5: 开始攻击
*/
public void onStatChange(AbstractCharacter sender, SkillStateChangeEnum stateChangeEnum)
{
    ...
    ...
    switch (stateChangeEnum)
    {
        case START_MOVE:
        {
            if (currentSkillLogic != null)
            {
                currentSkillLogic.onStartMove(sender);
            }
            int size = buffList.size();
            for (int i = 0; i < size; i++)
            {
                AbstractSkillImpact impact = buffList.get(i);
                impact.onStartMove(sender);
            }
            break;
        }
        ...
        ...
    }
}

使用技能就会调用onStatchange,然后迭代玩家身上所有的效果,根据standardimpact表配置使用技能是否打断buff列状态来判断是否打断。

/**
*  中断效果
* (非计时结束导致的buff结束,都要调用buff中断结束)
* @param character
* @param canBreakDispel 是否能驱散 不可驱散 特性BUFF| true = 能驱散不可驱散buff   false = 不可驱散buff不能被驱散
*/
private void doBreakImpact(AbstractCharacter character,boolean canBreakDispel)
{
    if (isFinished)
    {
        return;
    }

    //如果不是持续性buff效果,直接返回
    if(impactData.getImpactType() != SkillImpactTypeEnum.PERSISTENT.getValue())
    {
        return;
    }

    //传入不是驱散, 并且当前buff表里配置不能被中断
    if(!canBreakDispel && !getImpactData().isBeDispel())
    {
        return;
    }

     // notice client,
    // 效果已经激活并且不是位移效果打断
    if (isActive() && !isBeBreakBeatBackImpact())
    {
    }
    
    ...
    ...
}

中断效果。是否通知并选择通知类型。

10.根据释放技能事件,处理相应的buff逻辑

private void onUseSkill(DictSkillTemplateData skillTemplate)
{
    // 释放技能事件
    handleBuffEventOnBatch(BuffListenEventEnum.EVENT_TYPE_USE_SKILL,skillTemplate.getId(),0,0,0,0,0,0,0,null,null);
}

释放技能事件,传入释放技能枚举和技能id。

/**
* 批量处理处理监听buff事件
* @param eventEnum
* @param intParam0
* @param intParam1
* @param intParam2
* @param intParam3
* @param intParam4
* @param intParam5
* @param longParam0
* @param longParam1
* @param objectParam0
* @param objectParam1
*/
public void handleBuffEventOnBatch(BuffListenEventEnum eventEnum,int intParam0, int intParam1, int intParam2, int intParam3, int intParam4, int intParam5, long longParam0, long longParam1, Object objectParam0, Object objectParam1)
{
    //传入的枚举为释放技能
    List<AbstractSkillImpact> listenBuffList = getEventListenBuffsByEvent(eventEnum.getType());
    if(!CollectionUtils.isBlank(listenBuffList))
    {
        for (int i = 0,iSize = listenBuffList.size(); i < iSize; i++)
        {
            listenBuffList.get(i).handleEvent(eventEnum,character, intParam0, intParam1, intParam2, intParam3, intParam4, intParam5, longParam0, longParam1, objectParam0, objectParam1);
        }
    }
    ...
    ...
}

根据释放技能枚举,获取所有与释放技能有关的buff列表,迭代列表依次处理逻辑。

/**
* 为效果注册到BUFF事件监听
*
* @param toAddImpact
*/
public void registeToEventListenMap(AbstractSkillImpact toAddImpact)
{
    ...
    //不是持续性,返回
    if (toAddImpact.getImpactData().getImpactType() != SkillImpactTypeEnum.PERSISTENT.getValue())
    {
        return;
    }

    //获取当前buff的事件集合, 例如46号技能效果buff,监听血量上限会有多个事件,所以是事件组
    BuffEventHandleEnum[] events = toAddImpact.getBuffEventHandles();
    if (events == null || events.length < 1)
    {
        return;
    }

    OUTER_LOOP:
    for(int i = 0 , size =  events.length; i < size; i++)
    {
        //BUFF事件监听处理类型
        BuffEventHandleEnum event = events[i];
        if (event.getType() < 0)
        {
            continue;
        }

        //根据事件类型加入到  BUFF事件监听map
        ArrayList<AbstractSkillImpact> listenImpacts = eventListenMap.get(event.getType());
        ...
    }
    ...
}

每个buff都有它关心的事件类型,每个buff在初始化和添加时,会根据类型注册到缓存。

例如buff如果和释放技能事件有关,那么释放技能时需要处理对应buff逻辑。

11.切换战斗状态

/**
* 使用技能
*/
public void onSkillUse()
{
    isUseSkill = true;
    lastFightInterval = 0;
    if(!this.isFightStatus)
    {
        switchFightStatus(true);
    }
}

使用技能,切换战斗状态

4.tick当前技能逻辑

@Override
public void tickSkillLogic(int interval)
{
    if (currentSkillLogic == null)
    {
        lastSkillEndInterval += interval;
        return;
    }

    currentSkillLogic.tick(interval);

    if (currentSkillLogic.isFinished())
    {
        BPLog.BP_FIGHT.debug("技能结束, id:{}", currentSkillLogic.getSkillID());

        // 结束技能
        boolean isBreak = currentSkillLogic.isBreak();

        doFinishCurrentSkillLogic(isBreak,false);

        return;
    }
}

1.激活后会进行tick

/**
* 激活后会进行tick
*
* @param interval
*/
public void tick(int interval)
{
    ...
    ...
    if (!isActive)
    {
        //初始化是否AOI
        //根据skillDataEditor表配置的技能打击形状判断是否AOE
        //初始化持续时间
        //初始化延迟生效时间
        onActivate();
        interval = 0;
        isActive = true;
    }
    ...
    ...
     // 判断血量大于零
    if (!isHpPositive())
    {
        breakSkill();
        return;
    }
    ...

    // tick技能逻辑
    tickSkillLogic(interval);
    ...
}

初始化是否AOI

根据skillDataEditor表配置的技能打击形状判断是否AOE

初始化持续时间

初始化延迟生效时间

tick技能逻辑

2.瞬发技能的tick处理

/**
* 瞬发技能的tick处理
*
* @param interval
*/
public void tickInstantSkillLogic(int interval)
{
    if (!isTakeEffect)
    {
        startEffect();
        //是否生效
        isTakeEffect = true;
    }
}

技能开始生效。

3.持续技能的tick处理

4.引导技能的tick处理

5.技能开始生效

/**
* 技能开始生效
*/
public void startEffect()
{
    //技能生效的次数
    takeEffectCount++;
    ...
    ...

     // 处理主动位移
    int chaseLength = skillEditorData.getChaseLengthByIndex(takeEffectCount - 1);
    if(chaseLength > 0)
    {
        TIntArrayList chasePath = skillModule.getChasePath();
        int chaseX = chasePath.get(0);
        int chaseY = chasePath.get(1);
        int chaseZ = chasePath.get(2);
        chasePath.remove(0,3);

        // 设置玩家的位置(!!! 暂时只改变位置,不改变朝向!!! -- 20190117)
        self.getMoveModule().specialMoveTo(SpecialMoveType.TRANSPORT_SKILL, chaseX, chaseY, chaseZ, self.getRotation());
    }

    ...
    ...
    //技能效果组ID,调用skillassit表中的组ID
    DictSkillDataAssitData skillDataAssitData = DictSkillDataAssitData.getParamsByGroupIdAndEffectNum(skillData.getEffectGroupID(), takeEffectCount);
    if(skillDataAssitData == null)
    {
        BPLog.BP_FIGHT.error("【技能错误】技能逻辑作用时,找不到技能段SkillDataAssit配置 [skillID] {} [level] {} [effectNum] {} [EffectGroupID] {}",skillTemplateData.getId(),skillData.getLevel(),takeEffectCount,skillData.getEffectGroupID());
        return;
    }

    // 固定对自己作用  skillDataAssitData表里配置了技能会固定给自己上效果
    effectToSelf(skillDataAssitData);


    // 空打击点  根据当前技能释放的次数获取当前技能打击的形状
    int scope = skillEditorData.getScopeByIndex(takeEffectCount - 1);
    if(scope == SkillScopeConstant.VOID)
    {
        return;
    }

    ...
    ...

    //单体技能逻辑

    //AOE技能逻辑

}

根据技能组id和技能生效的次数,根据SkillDataAssit表配置固定给自己上效果。

处理主动位移,根据技能点位移调用特殊移动方法直接设置玩家位置,暂不改变朝向。

根据技能生效次数,从skillEditor表里获取当前段数的打击行状。如果是空打击点直接返回(空打击点(用于补足主动位移的打击点,枚举类型5))。

对自己固定 概率 效果(无论是否命中,都上效果) skillDataAssitData表里配置了技能会固定给自己上效果

单体技能逻辑。

AOE技能逻辑。

6.技能作用逻辑 - 单体

// 技能作用逻辑 - 单体
if (!isAOE)
{
    ...
     // 检查目标
    int rs = checkTarget();
    ...
    //计算命中率和霸体
    if(skillTemplateData.getSkillType() == SkillTypeConstant.NORMAL_ATTACK && selfCommonModule.isWarState(WarStateEnum.BLIND_NORMAL_SKILL_NO_HURT.getId()))
    {
        // 普攻技能 && 被致盲了,那么普攻就miss
        this.isHit = false;
    }
    else
    {
        //计算是否命中
        this.isHit = calculateHit(target,skillDataAssitData);
    }

    // 计算霸体
    calcAttackLevelAndEndure(target,skillDataAssitData);

    /执行技能逻辑
    doStartEffect(skillDataAssitData);

    //技能作用事件
    self.onSkillEffect(skillTemplateData, getTargetObjectID(),skillSign,getHitIndex(),isHit(),isEndure());

    //技能每段命中目标(AOE打中多个人只调用一次)
    onSkillEachEffectHit(self,skillTemplateData,skillDataAssitData,1);

    //pvp使用技能的处理(玩家对玩家)
    SkillHelper.pvpUseSkill(self,target , skillTemplateData);

    // 确定下一次目标,因为有些技能需要在logic的不同触发阶段选择不同的目标
    // 派生类根据具体的技能逻辑,找单体目标
    // 默认不改变目标
    int result = findSingleTarget();
}
1.检查目标

// 检查目标

int rs = checkTarget();

/**
* 检查目标
* @return =0 通过 <0 失败原因
*/
private int doCheckTarget(AbstractCharacter character,AbstractCharacter target,DictSkillTemplateData skillTemplate, DictSkillDataEditorData skillDataEditorData, boolean isActorAndInSafeRegion)
{
    ...
    ...
    目标是否在安全区

    是否正在使用轻功

    是否隐身不可见

    目标是否需要存活

    不可被攻击

    不可被选中(不能被使用单体技能)

    目标细分

    //第一次技能作用距离
    int distance = skillDataEditorData.getFirstAngle();
    distance += SkillCommonConstant.SKILL_TOLERATE_DISTANCE;    // 增加一定距离的容错
    float distanceSQ = (long)distance * (long)distance;

    // 判断两个实体之间的距离
    float actualDistanceSQ = MapUtils.distanceSqBetweenPos(character.getX(), character.getZ(), target.getX(), target.getZ());
    if (actualDistanceSQ > distanceSQ)
    {
        if(BPGlobals.getInstance().isDebugVersion())
        {
            int diffDis = (int)(Math.sqrt(actualDistanceSQ) - distance);
            // TODO 修改日志级别,适应压测。压测后重新改成info日志
            BPLog.BP_FIGHT.debug("【技能效果检查目标失败】原因: 目标距离过远 [skillId] {} [myPos] ({},{},{}) [targetPos] ({},{},{}) [ 配置距离 ] {} [比配置距离远] {}"
                    ,skillTemplate.getId(),character.getX(),character.getY(),character.getZ(),target.getX(),target.getY(),target.getZ(),distance,diffDis);
        }
        return BPErrorCodeEnum.SKILL_TARGET_DISTANCE_NOT_ENOUGH;
    }

    // 高度差
    // 技能作用的高度范围
    int effectHight = skillTemplate.getEffectHight();
    if(effectHight > 0)
    {
        int sampleHeightDiff = character.getScene().getSceneGirdModule().calcHeightDiff(character, target);
        if(sampleHeightDiff  == Integer.MIN_VALUE)
        {
            // 玩家不在可行走区域内,认为技能高度无限制,防止出现卡阻挡无敌的问题
//                return BPErrorCodeEnum.SKILL_TARGET_HEIGHT_ERROR;
        }
        else
        {
            if(Math.abs(sampleHeightDiff) > effectHight)
            {
                return BPErrorCodeEnum.SKILL_TARGET_HEIGHT_DIFF_NOT_SUIT;
            }
        }
    }

    // 目标选择是友好,玩家阵营也是友好,但是PVP规则中是敌对关系,也是不符合条件的
    if (skillTemplate.getEffectTargetEnum().isFrindly())
    {
        if (SkillHelper.isPvP(character, target, skillTemplate))
        {
            Actor selfActor = SkillHelper.getObjectActor(character);
            Actor targetActor = SkillHelper.getObjectActor(target);
            if (selfActor != null && targetActor != null)
            {
                ActorPvPModule pvPModule = selfActor.getPvPModule();
                if (!pvPModule.canAttack(targetActor))
                {
                    return BPErrorCodeEnum.SKILL_TARGET_FORCE_RELATIONSHIP_NOT_MATCH;
                }
            }
        }
    }
    ...
    ...
    ...
}

单体技能,检查目标主要是:

判断两个实体之间的距离

技能作用的高度范围

目标选择是友好,玩家阵营也是友好,但是PVP规则中是敌对关系,也是不符合条件的

2.单体技能作用-计算命中率
int targetID = selfCommonModule.getTargetObjectID();
    target = getCharacter(targetID);

//当前是普攻并且玩家被致盲了
if(skillTemplateData.getSkillType() == SkillTypeConstant.NORMAL_ATTACK && selfCommonModule.isWarState(WarStateEnum.BLIND_NORMAL_SKILL_NO_HURT.getId()))
{
    // 普攻技能 && 被致盲了,那么普攻就miss
    this.isHit = false;
}
else
{
    //计算命中率
    this.isHit = calculateHit(target,skillDataAssitData);
}

if (!this.isHit)
{
    //如果没有命中,发送未命中通知客户端
    sendNoHitNotice(self,target,targetID);
}
2.单体技能作用-计算攻击等级和霸体
/**
* 计算攻击等级和霸体
* @param target
* @param skillAssitData
*/
public void calcAttackLevelAndEndure(AbstractCharacter target,DictSkillDataAssitData skillAssitData)
{
    ...
    ...
     //判断攻击对象是npc或者玩家
    int baseAttackLevel = 0;
    if(target.getObjectType() == ObjectTypeEnum.NPC)
    {
        //攻击等级(对怪物)
        baseAttackLevel = skillAssitData.getAttackLevelNpc();
    }
    else
    {
        //攻击等级(对玩家
        baseAttackLevel = skillAssitData.getAttackLevel();
    }

    // 攻击等级
    this.realAttackLevel = FormulaFacade.countAttackLevel(attacker, target,baseAttackLevel);

    // 是否霸体
    this.isEndure = target.getAbstractCharacterCommonModule().isEndure();

    // 扣除霸体值(传入实际攻击等级)
    target.getAbstractCharacterCommonModule().decreaseEndure(realAttackLevel);

    ..
    
}
3.单体技能作用-操作
技能作用事件
self.onSkillEffect(skillTemplateData, skillDataAssitData,getTargetObjectID(),skillSign,getHitIndex(),isHit(),isEndure());

技能每段命中目标(AOE打中多个人只调用一次)
self.onSkillEffect(skillTemplateData, skillDataAssitData,getTargetObjectID(),skillSign,getHitIndex(),isHit(),isEndure());

6.技能作用逻辑 - AOE

1.普攻技能 && 被致盲了,那么就不扫敌
// 技能作用逻辑 - AOE
this.isHit = true;

if(skillTemplateData.getSkillType() == SkillTypeConstant.NORMAL_ATTACK && self.getAbstractCharacterCommonModule().isWarState(WarStateEnum.BLIND_NORMAL_SKILL_NO_HURT.getId()))
{
    // 普攻技能 && 被致盲了,那么就不扫敌也不作用了
    this.isHit = false;
}

如果是普攻并且被致盲,那么不作用

2.扫描周围可作为目标的对象
// 当前生效次数
int arrayIndex = takeEffectCount - 1;

//获取当前生效次数的技能范围形状
int scope = skillEditorData.getScopeByIndex(arrayIndex);
//偏移长度
int offsetLength = skillEditorData.getOffsetLengthByIndex(arrayIndex);
//偏移角度
int offsetAngle = skillEditorData.getOffsetAngleByIndex(arrayIndex);

// 索敌中心点偏移
int currentRotation = self.getRotation();
int currentX = self.getX();
int currentZ = self.getZ();

int rotation = self.getRotation() + offsetAngle % 361;
self.setRotation(rotation);

//计算偏移长度
if (offsetLength != 0)
{
    // Math.toRadians(rotation) 将度转换为弧度
    // Math.cos()再取余弦
    int x = (int) (currentX + offsetLength * Math.cos(Math.toRadians(rotation)));
    int z = (int) (currentZ + offsetLength * Math.sin(Math.toRadians(rotation)));
    //不置脏标记位置(也就是不执行视野更新)
    self.setPosNoDirty(x, self.getY(), z);
}

获取技能形状,计算位置和朝向。

不置脏标记设置位置(也就是不执行视野更新)。

 switch (scope)
{
    case SkillScopeConstant.SINGLE:
    case SkillScopeConstant.VOID:
    {
        break;
    }
    ...
    ...
}

根据形状,遍历单位。

// 恢复正确的朝向
self.setRotation(currentRotation);
// 恢复正确的坐标
if (offsetLength != 0)
{
    self.setPosNoDirty(currentX, self.getY(), currentZ);
}

恢复到使用技能之前的位置。

// 首选目标筛选出加入队列头
if(tendencyObjectID !=  SkillCommonConstant.TARGET_ID_NONE)
{
    //猜想是list有玩家自己,所以需要长度大于1
    if(list.size() > 1)
    {
        for(int i = 0 , size = list.size(); i < size; i++)
        {
            BPObject object = list.get(i);
            if(tendencyObjectID == object.getObjectID())
            {
                //调换位置
                BPObject firstObj = list.set(0,object);
                list.set(i,firstObj);
                break;
            }
        }
    }
}

//迭代所有目标,校验是否符合,加入外部传入的目标targetList列表,并清楚用于aoi扫描的list列表!!
int size = list.size();
for (int i = 0; i < size; i++)
{
    if (sendedCountForNpc >= skillDataAssitData.getMaxMonsterNum() || sendedCountForPlayer >= skillDataAssitData.getMaxPlayerNum())
    {
        // 人数/怪物数到达上限
        break;
    }

    BPObject bpObject = list.get(i);

    if(bpObject == null)
    {
        continue;
    }

    if (!bpObject.getObjectType().isCharacter())
    {
        continue;
    }

    AbstractCharacter target = (AbstractCharacter) bpObject;

    //检查AOE扫描的目标是否合法
    int rs = checkAOEScanedTarget(scene, self, target, skillTemplate,skillEditorData, skillDataAssitData, isActorAndInSafeRegion);

    if(mapViewModule != null)
    {
        mapViewModule.recordSkillEffecScanPlayer(skillId,takeEffectCount,target.getObjectID(),rs);
    }

    if(rs < 0)
    {
        continue;
    }

    targetList.add(target);

    if(bpObject.getObjectType() == ObjectTypeEnum.NPC)
    {
        sendedCountForNpc++;
    }
    else
    {
        sendedCountForPlayer++;
    }
}
...

list.clear();

首选目标筛选出加入队列头。

迭代所有目标,校验是否符合,加入外部传入的目标targetList列表,并清楚用于aoi扫描的list列表!!

这里个人认为两个list的迭代可以合并为1次迭代,提高效率。

3.迭代目标对象,技能作用
int size = list.size();
for (int i = 0; i < size; i++)
{

}

迭代对象


//设置目标
selfCommonModule.setTargetObjectID(target.getObjectID());

// 计算攻击等级和霸体
calcAttackLevelAndEndure(target,skillDataAssitData);

// 作用逻辑
doStartEffect(skillDataAssitData);

// 技能派发事件
self.onSkillEffect(skillTemplateData,skillDataAssitData, target.getObjectID(),skillSign,getHitIndex(),isHit(),isEndure());

// 被技能派发事件
if(isHit() && target != null)
{
    target.onBeSkillEffect(self,skillTemplateData,realAttackLevel,isEndure);
}

SkillHelper.pvpUseSkill(self,target , skillTemplateData);
...


// 技能命中事件(AOE打中多个人只调用一次)
if(size > 0)
{
    onSkillEachEffectHit(self,skillTemplateData,skillDataAssitData,size);
}
1.作用逻辑
// 作用逻辑
doStartEffect(skillDataAssitData);

private void doStartEffect(DictSkillDataAssitData skillDataAssitData)
{
    // XXX 记录技能对单个角色作用,用于后台地图工具
    CharacterMapViewModule mapViewModule = getSkillModule().getCharacter().getCharacterMapViewModule();
    if(mapViewModule != null)
    {
        mapViewModule.recordSkillEffectHitPlayer(skillId,takeEffectCount,getTargetObjectID(),isHit,isEndure,realAttackLevel);
    }

    startEffectImpl(skillDataAssitData);
}

技能作用逻辑,现在只保留17号技能作用逻辑。也就是说所有技能逻辑都是17号。

public class SkillLogic017 extends AbstractSkillLogic
{
    @Override
    protected void startEffectImpl(DictSkillDataAssitData skillDataAssitData)
    {
        ...

        if(isHit() &&effectNumSelfOnceMap.get(effectCount) == effectNumSelfOnceMap.getNoEntryValue())
        {
            effectNumSelfOnceMap.put(effectCount,1);

            //对自己单次 概率 效果(命中多个敌人只上一次,未命中不上)
            TIntArrayList selfOnceImpactIds = skillDataAssitData.getSelfOnceImpactIds();
            if(!CollectionUtils.isBlank(selfOnceImpactIds))
            {
                TIntArrayList selfOnceProbablitity = skillDataAssitData.getSelfOnceProbablitity();
                for(int i = 0 , size = selfOnceImpactIds.size(); i < size; i++)
                {
                    int selfOnceProb = selfOnceProbablitity.get(i);
                    int selfOnceImpactID = selfOnceImpactIds.get(i);

                    if(selfOnceImpactID <= 0 || selfOnceProb <= 0)
                    {
                        continue;
                    }

                    if(selfOnceProb >= GameConstant.TEN_THOUSAND || RandomUtil.randomInt(GameConstant.TEN_THOUSAND) <= selfOnceProb)
                    {
                        //向自己派发效果
                        sendImpactToSelf(selfOnceImpactID);
                    }
                }
            }
        }

        ...

        //  二 、 给自己多次,命中才给自己加
        TIntArrayList selfRepeatImpactIds = skillDataAssitData.getSelfRepeatImpactIds();
        if(!CollectionUtils.isBlank(selfRepeatImpactIds) && isHit())
        {
            //对自己多次 概率 效果 (命中多个敌人上多次次,未命中不上)
            TIntArrayList selfRepeatProbablitity = skillDataAssitData.getSelfRepeatProbablitity();
            for(int i = 0 , size = selfRepeatImpactIds.size(); i < size; i++)
            {
                int selfRepeatProb = selfRepeatProbablitity.get(i);
                int selfRepeatImpactID = selfRepeatImpactIds.get(i);

                if(selfRepeatImpactID <= 0 || selfRepeatProb <= 0)
                {
                    continue;
                }

                if(selfRepeatProb >= GameConstant.TEN_THOUSAND || RandomUtil.randomInt(GameConstant.TEN_THOUSAND) <= selfRepeatProb)
                {
                    //向自己派发效果
                    sendImpactToSelf(selfRepeatImpactID);
                }
            }
        }

        ...
        ...

        // 三 、给敌方的固定伤害效果
        // 根据技能辅助表SkillDataAssit获取固定伤害效果
        int damageImpactId = skillDataAssitData.getDamageImpact();
        if(damageImpactId > 0)
        {
            //派发效果,根据技能的配置,来确定派发给单体还是周围
            sendImpactToTarget(damageImpactId);
        }

         // 四、 给敌方上的条件效果
        // 判断是否满足条件
        boolean isInCondtion = false;
        //条件Idcollection中的所有元素的集合(一般来讲是impact的groupId)
        TIntHashSet conditionCollectionIdSet = skillDataAssitData.getConditionCollectionIdSet();
        ...
        ...

         // 上效果逻辑
        TIntArrayList targetImpactIds = null;
        TIntArrayList targetProbablitity = null;
        TIntArrayList targetEnchanceAttrIds = null;
        TIntArrayList targetDeclineAttrIds = null;
        if(isInCondtion)
        {
            //满足条件 对目标 概率 / 效果 / 增强属性 / 抵抗属性
            targetImpactIds = skillDataAssitData.getConditionTargetImpactIds();
            targetProbablitity =  skillDataAssitData.getConditionTargetProbablitity();
            targetEnchanceAttrIds = skillDataAssitData.getConditionTargetEnchanceAttrIds();
            targetDeclineAttrIds = skillDataAssitData.getConditionTargetDeclineAttrIds();
        }
        else
        {
            //满足条件 对目标 概率 / 效果 / 增强属性 / 抵抗属性
            targetImpactIds = skillDataAssitData.getInconformityTargetImpactIds();
            targetProbablitity =  skillDataAssitData.getInconformityTargetProbablitity();
            targetEnchanceAttrIds = skillDataAssitData.getInconformityTargetEnchanceAttrIds();
            targetDeclineAttrIds = skillDataAssitData.getInconformityTargetDeclineAttrIds();
        }

        ...
        ...

            // 五 、技能编辑器 skillDataEditor对敌人效果,后发技能效果编辑器的效果
        DictSkillDataEditorData dictSkillDataEditorData = getDictSkillDataEditorData();
        int skillEditorImpcatId = dictSkillDataEditorData.getImpcatByIndex(effectCount - 1);
        if(skillEditorImpcatId > 0)
        {
            sendImpactToTarget(skillEditorImpcatId);
        }
        ...
        
    }

}

1.对自己单次 概率 效果(命中多个敌人只上一次,未命中不上)

2.对自己多次 概率 效果 (命中多个敌人上多次次,未命中不上)

3.给敌方的固定伤害效果

4.给敌方上的条件效果

5.技能编辑器 skillDataEditor对敌人效果,后发技能效果编辑器的效果

2.单独看下给敌方的固定伤害效果
// 三 、给敌方的固定伤害效果
// 根据技能辅助表SkillDataAssit获取固定伤害效果
int damageImpactId = skillDataAssitData.getDamageImpact();
if(damageImpactId > 0)
{
    //派发效果,根据技能的配置,来确定派发给单体还是周围
    sendImpactToTarget(damageImpactId);
}
/**
* 向目标派发效果
*
* @param targetID 目标在场景内的object id
* @param impactID
* @return
*/
private int doSendImpactTo(int targetID, int impactID)
{
    ...
    ...
    boolean unBeneficial = impactData.getBuffFriendly() == SkillFriendlyEnum.UNBENEFICIAL.getVal();
    if (unBeneficial)
    {
        // 负面buff,战斗处理
        character.getFightModule().addObjectToFightSet(targetID,impactData.getId(),skillId);
        target.getFightModule().addObjectToFightSet(character.getObjectID(),impactData.getId(),skillId);
    }
    ...
    ...

    // 派发效果 向目标发送一个效果(仅仅用于当前技能发送,其他处请勿使用!!!)
    int result = skillModule.sendImpactToOnlyForCurrentSkillLogic(character, target, impactData);
    ...
    ...
}

战斗状态处理,当前技能派发效果。派发效果和开头派发buff逻辑是一致的。

技能学习流程

Search

    Table of Contents