从零开始实现放置游戏(十五)——实现战斗挂机(6)在线打怪练级

  本章初步实现游戏的核心功能——战斗逻辑。
  战斗系统牵涉的范围非常广,比如前期人物的属性、怪物的配置等,都是在为战斗做铺垫。
  战斗中,人物可以施放魔法、技能,需要技能系统支持。
  战斗胜利后,进行经验、掉落结算。又需要背包、装备系统支持。装备系统又需要随机词缀附魔系统。
  可以说是本游戏最硬核的系统。
  因为目前技能、背包、装备系统都还没有实现。我们先初步设计实现一个简易战斗逻辑。
  战斗动作仅包括普通攻击,有可能产生未命中、闪避和暴击。
  整个战斗逻辑的流程大致如下图所示:
 

一、战斗消息设计

  参照其他消息,战斗动作需要发送请求并接收返回消息,我们先定义两个消息代码 :
    CBattleMob = "30003001"
    SBattleMob = "60003001"
 
  这里我们先仅考虑在线打怪,发送战斗请求,我们仅需要知道怪物id即可,战斗时从数据库读取怪物属性。
  新建客户端消息类如下:
@Data
public final class CBattleMobMessage extends ClientMessage {
    private String mobId;
}
  服务端需要返回战斗的最终结果信息,以及每个回合每个角色的战斗动作记录作给客户端,一遍客户端播放。
  新建服务端的消息类如下:
@Data
public class SBattleMobMessage extends ServerMessage {
    private BattleMobResult battleMobResult;
}
@Data
public class BattleMobResult implements Serializable {
    // 总回合数
    private Integer totalRound;
    // 回合列表
    private List<BattleRound> roundList;
    // 是否玩家获胜
    private Boolean playerWin;
    // 战斗结果信息
    private String resultMessage;

    public BattleMobResult() {
        this.roundList = new ArrayList<>();
    }

    public void putBattleRound(BattleRound battleRound) {
        this.roundList.add(battleRound);
    }
}
@Data
public class BattleRound implements Serializable {
    // 当前回合数
    private Integer number;
    // 回合内战斗记录
    private List<String> messages;
    // 是否战斗结束
    private Boolean end;

    public BattleRound() {
        this.messages = new ArrayList<>();
        this.end = false;
    }

    public BattleRound(Integer roundNum) {
        this();
        this.number = roundNum;
    }

    public void putMessage(String message) {
        this.messages.add(message);
    }
}
  这里 BattleMobResult 和 BattleRound 两个类,是返回给页面使用的视图模型,新建时放在game.hub.message.vo.battle包中。

二、战斗单位建模

  在战斗开始时,我们把参战单位那一时刻的属性取出来存一份副本,此后,均以此副本为准进行计算。
  怪物和玩家包的类含的属性差别较大,为了方便统一计算,我们抽象一个BattleUnit类,存储一些通用属性,比如等级,血量。
  其中还定义了一些抽象方法,比如获取攻击强度getAP(),和获取护甲等级getAC()。玩家和怪物需要分别实现这两个抽象方法。
  玩家,战斗属性(二级属性)是由力量、敏捷、耐力、智力这些一级属性进一步计算得出。比如,战士的攻击强度=等级*3+力量*2-20。速度=敏捷。护甲等级=耐力*2。命中率=0.95。闪避和暴击=敏捷*0.0005。
  怪物,只是用来练级的,则没那么麻烦,录入数据时就只有伤害和护甲两项属性。攻击强度直接取伤害值即可。速度直接取0。命中率默认0.95。闪避和暴击率默认0.05。
  这里虚类BattleUnit中又有一个巧妙的实方法getDR(),获取伤害减免。将其写在虚基类中,不管是玩家还是怪物实例,都可以根据自身的AC,计算出相应的DR。
  这里DR的计算公式: 伤害减免 = 护甲等级 / (护甲等级 + 85*玩家(怪物等级 + 400)
    /**
     * 获取伤害减免Damage Reduce
     *
     * @return
     */
    public Double getDR() {
        Integer ac = this.getAC();
        return ac / (ac + 85 * this.level + 400.0);
    }
  3个类的UML图如下,具体实现可以下载源代码查看。

三、战斗机制

  模型建完,就剩战斗逻辑了。其中,一个核心的问题就是战斗动作的判定。即发起一次普通攻击后,到底是被闪避了,还是被格挡了,还是产生了暴击,或者仅仅是命中。其中,每一项可能的结果需要单独ROLL点吗?这里不同的游戏会有不同的实现。我们参考使用魔兽的判定方法,圆桌理论,即只ROLL一次点,这样逻辑更加容易处理。

  圆桌理论

  "一个圆桌的面积是固定的,如果几件物品已经占据了圆桌的所有面积时,其它的物品将无法再被摆上圆桌"
  这个理论在战斗逻辑中,即把可能产生的结果按优先级摆放到桌上,比如以下这种情形(其中的概率会因属性、装备等的不同而变化,这里只是举例):
  • 未命中(5%)
  • 躲闪(5%)
  • 招架(20%)
  • 格挡(20%)
  • 暴击(5%)
  • 普通攻击
  只ROLL一次点,如果ROLL到3,则玩家未命中怪物;如果ROLL到49,则玩家的攻击被怪物格挡;超过55的部分,都是普通攻击。
  假如这里玩家换上暴击装,暴击率达到60%。则圆桌上全部结果的概率已超出100%,ROLL到50-100全部判定为暴击,普通攻击被踢下圆桌,永远不会发生。
 
  在本此实现中,我们仅考虑物理未命中、闪避和暴击。暂不考虑二次ROLL点(攻击产生暴击,但被闪避或格挡了),以及法术技能的ROLL点。

四、战斗逻辑实现

  有了以上基础,我们就可以通过代码实现完整的战斗逻辑了。
  这里,虽然目前仅包含在线打怪,但以后可能会包含组队战斗,副本战斗,PVP等逻辑。我们把战斗逻辑放到单独的包里,com.idlewow.game.logic.battle,在这里新建战斗逻辑的核心类BattleCore,具体实现代码如下:
package com.idlewow.game.logic.battle;

import com.idlewow.character.model.Character;
import com.idlewow.character.model.LevelProp;
import com.idlewow.character.service.CharacterService;
import com.idlewow.character.service.LevelPropService;
import com.idlewow.common.model.CommonResult;
import com.idlewow.game.GameConst;
import com.idlewow.game.logic.battle.dto.BattleMonster;
import com.idlewow.game.logic.battle.dto.BattlePlayer;
import com.idlewow.game.logic.battle.util.ExpUtil;
import com.idlewow.game.hub.message.vo.battle.BattleMobResult;
import com.idlewow.game.logic.battle.dto.BattleUnit;
import com.idlewow.game.logic.battle.util.BattleUtil;
import com.idlewow.game.hub.message.vo.battle.BattleRound;
import com.idlewow.mob.model.MapMob;
import com.idlewow.mob.service.MapMobService;
import com.idlewow.support.util.CacheUtil;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.LinkedList;
import java.util.List;
import java.util.Random;

@Component
public final class BattleCore {
    private static final Logger logger = LogManager.getLogger(BattleCore.class);
    // 战斗最大回合数
    private static final Integer MaxRound = 20;
    // 暴击系数
    private static final Integer CriticalFactor = 2;

    @Autowired
    MapMobService mapMobService;
    @Autowired
    LevelPropService levelPropService;
    @Autowired
    CharacterService characterService;

    /**
     * 在线打怪
     *
     * @param character
     * @param mobId
     * @return
     */
    public BattleMobResult battleMapMob(Character character, String mobId) {
        // 获取地图怪物信息
        CommonResult commonResult = mapMobService.find(mobId);
        if (!commonResult.isSuccess()) {
            logger.error("未找到指定怪物:id" + mobId);
            return null;
        }

        // 初始化参战方信息
        MapMob mapMob = (MapMob) commonResult.getData();
        List<BattleUnit> atkList = new LinkedList<>();
        atkList.add(this.getBattlePlayer(character, GameConst.BattleTeam.ATK));
        List<BattleUnit> defList = new LinkedList<>();
        defList.add(this.getBattleMonster(mapMob, GameConst.BattleTeam.DEF));
        List<BattleUnit> battleList = new LinkedList<>();
        battleList.addAll(atkList);
        battleList.addAll(defList);
        battleList = BattleUtil.sortUnitBySpeed(battleList);
        // 回合循环
        BattleMobResult battleMobResult = new BattleMobResult();
        for (int i = 0; i < MaxRound; i++) {
            BattleRound battleRound = new BattleRound(i + 1);
            for (BattleUnit battleUnit : battleList) {
                if (!battleUnit.getIsDefeat()) {
                    // 选定攻击目标
                    BattleUnit targetUnit = null;
                    if (battleUnit.getTeam().equals(GameConst.BattleTeam.ATK)) {
                        Integer targetIndex = new Random().nextInt(defList.size());
                        targetUnit = defList.get(targetIndex);
                    } else if (battleUnit.getTeam().equals(GameConst.BattleTeam.DEF)) {
                        Integer targetIndex = new Random().nextInt(atkList.size());
                        targetUnit = atkList.get(targetIndex);
                    }

                    // 攻方出手ROLL点
                    Integer roll = new Random().nextInt(100);
                    Double miss = (1 - battleUnit.getHitRate() / (battleUnit.getHitRate() + battleUnit.getDodgeRate())) * 100;
                    Double critical = battleUnit.getCriticalRate() * 100;
                    logger.info("round: " + i + "atk: " + battleUnit.getName() + " def: " + targetUnit.getName() + " roll:" + roll + " miss: " + miss + " cri: " + critical);
                    String desc = "";
                    if (roll <= miss) {
                        desc = battleUnit.getName() + " 的攻击未命中 " + targetUnit.getName();
                    } else if (roll <= miss + critical) {
                        Integer damage = BattleUtil.actualDamage(battleUnit.getAP(), targetUnit.getDR()) * CriticalFactor;
                        desc = battleUnit.getName() + " 的攻击暴击,对 " + targetUnit.getName() + " 造成 " + damage + " 点伤害(" + targetUnit.getHp() + " - " + damage + " )";
                        targetUnit.setHp(targetUnit.getHp() - damage);
                    } else {
                        Integer damage = BattleUtil.actualDamage(battleUnit.getAP(), targetUnit.getDR());
                        desc = battleUnit.getName() + " 的攻击,对 " + targetUnit.getName() + " 造成 " + damage + " 点伤害(" + targetUnit.getHp() + " - " + damage + " )";
                        targetUnit.setHp(targetUnit.getHp() - damage);
                    }

                    // 检测守方存活
                    if (targetUnit.getHp() <= 0) {
                        targetUnit.setIsDefeat(true);
                        desc += ", " + targetUnit.getName() + " 阵亡";
                        if (battleUnit.getTeam().equals(GameConst.BattleTeam.ATK)) {
                            defList.remove(targetUnit);
                        } else if (battleUnit.getTeam().equals(GameConst.BattleTeam.DEF)) {
                            atkList.remove(targetUnit);
                        }
                    } else {
                        // 检测守方反击动作
                        // todo
                    }

                    battleRound.putMessage(desc);
                    // 检测战斗结束
                    if (atkList.size() == 0 || defList.size() == 0) {
                        Boolean playerWin = defList.size() == 0;
                        battleRound.setEnd(true);
                        battleMobResult.setTotalRound(i);
                        battleMobResult.setPlayerWin(playerWin);
                        String resultMessage = "战斗结束! " + character.getName() + (playerWin ? " 获得胜利!" : " 不幸战败!");
                        battleMobResult.putBattleRound(battleRound);
                        battleMobResult.setResultMessage(resultMessage);
                        // 玩家获胜 进行战斗结算
                        if (playerWin) {
                            // 经验结算
                            this.settleExp(character.getLevel(), mapMob.getLevel(), character);
                            // 更新角色数据
                            characterService.updateSettle(character);
                        }

                        return battleMobResult;
                    }
                }
            }

            battleMobResult.putBattleRound(battleRound);
        }

        battleMobResult.setTotalRound(MaxRound);
        battleMobResult.setResultMessage("战斗回合数已用尽!守方获胜!");
        return battleMobResult;
    }

    /**
     * 经验值结算
     * @param charLevel 角色等级
     * @param mobLevel 怪物等级
     * @param character 角色信息
     */
    private void settleExp(Integer charLevel, Integer mobLevel, Character character) {
        Integer exp = ExpUtil.getBattleMobExp(charLevel, mobLevel);
        if (exp > 0) {
            Integer levelUpExp = CacheUtil.getLevelExp(charLevel);
            if (character.getExperience() + exp >= levelUpExp) {
                character.setLevel(charLevel + 1);
                character.setExperience(character.getExperience() + exp - levelUpExp);
            } else {
                character.setExperience(character.getExperience() + exp);
            }
        }
    }


    /**
     * 获取角色战斗状态
     * @param character 角色信息
     * @param battleTeam 所属队伍
     * @return
     */
    private BattlePlayer getBattlePlayer(Character character, String battleTeam) {
        LevelProp levelProp = levelPropService.findByJobAndLevel(character.getJob(), character.getLevel());
        BattlePlayer battlePlayer = new BattlePlayer();
        battlePlayer.setId(character.getId());
        battlePlayer.setName(character.getName());
        battlePlayer.setJob(character.getJob());
        battlePlayer.setLevel(character.getLevel());
        battlePlayer.setHp(levelProp.getHp());
        battlePlayer.setStrength(levelProp.getStrength());
        battlePlayer.setStamina(levelProp.getStamina());
        battlePlayer.setAgility(levelProp.getAgility());
        battlePlayer.setIntellect(levelProp.getIntellect());
        battlePlayer.setTeam(battleTeam);
        return battlePlayer;
    }

    /**
     * 获取怪物战斗状态
     * @param mapMob 怪物信息
     * @param battleTeam 所属队伍
     * @return
     */
    private BattleMonster getBattleMonster(MapMob mapMob, String battleTeam) {
        BattleMonster battleMonster = new BattleMonster();
        battleMonster.setId(mapMob.getId());
        battleMonster.setName(mapMob.getName());
        battleMonster.setLevel(mapMob.getLevel());
        battleMonster.setHp(mapMob.getHp());
        battleMonster.setDamage(mapMob.getDamage());
        battleMonster.setArmour(mapMob.getArmour());
        battleMonster.setTeam(battleTeam);
        return battleMonster;
    }
}
BattleCore.java
  如上图代码,首先我们初始化一份各参战单位的属性副本,并添加到创建的3个列表中,其中atkList, defList用来检测是否其中一方全部阵亡,battleList则用来对参战单位按速度排序,确定出手顺序。
  这里使用了归并排序来对集合进行排序,具体算法在BattleUtil类中。考虑到这里对集合的添加、修改、删除操作较多,使用LinkedList链表来保存参战集合。(实际上数据较少,使用ArrayList可能也没什么差别)。
  这里仅仅在回合开始前确定了一次出手顺序,因为目前没有引入技能,假如引入技能后,比如猎人施放豹群守护,我方全体速度+50,那么需要对出手列表进行重新排序。
  进入循环后,随机选定攻击目标 --> 确定出手动作 --> 存活检测 --> 战斗结束检测, 这里注释和代码比较清楚,就不一一讲解了。
  这里攻击动作和结果确定后,会在返回信息中添加对此的描述,后面考虑如果后端传输这些内容太多不够优雅,也可以定义一套规则,只传输关键数据,战斗记录由前端生成。不过目前先不考虑。
  战斗结束后,如果玩家胜利,需要结算经验值。经验值相关的计算在ExpUtil中,文后会附上经验值计算公式。

五、播放战斗记录

  战斗计算完成后,后端会返回战斗信息给前端,前端只负责播放即可。
  播放记录的方法代码如下:
 
    // 加载在线打怪战况
    loadBattleMobResult: async function (data) {
        let that = this;
        $('.msg-battle').html('');
        let rounds = data.battleMobResult.roundList;
        if (data.battleMobResult.totalRound > 0) {
            for (let i = 0; i < rounds.length; i++) {
                let round = rounds[i];
                let content = "<p>【第" + round.number + "回合】</p>";
                for (let j = 0; j < round.messages.length; j++) {
                    content += "<p>" + round.messages[j] + "</p>";
                }

                content += "<hr />";
                $('.msg-battle').append(content);
                await this.sleep(1500);
            }

            $('.msg-battle').append("<p><strong>" + data.battleMobResult.resultMessage + "</strong></p>");
            if (data.battleMobResult.playerWin) {
                that.sendLoadCharacter();
            }

            if (that.isBattle) {
                that.battleInterval = setTimeout(function () {
                    that.sendBattleMob(that.battleMobId);
                }, 5000);
            }

            // await this.sleep(5000).then(function () {
            //     that.sendBattleMob(data.battleMobResult.mobId);
            // });
        }
    },
  上面的代码中,最后3行被注释掉的代码,即5秒钟后,再次攻击此怪。如果只考虑打怪和用setTimeout方法实现,其实没有差别。
  但在业务上,考虑玩家可能需要点击停止打怪,那么用setTimeout来执行循环,可以用clearInterval来终止函数执行。
/* 在线打怪 */
function battleMob(mobId) {
    let diff = new Date().getTime() - wowClient.battleMobTime;
    if (diff < TimeLag.BattleMob * 1000) {
        let waitSeconds = parseInt(TimeLag.BattleMob - diff / 1000);
        alert('请勿频繁点击!' + waitSeconds + '秒后可再操作!');
        return;
    }

    if (mobId === wowClient.battleMobId) {
        alert("已经在战斗中!请勿重复点击!");
        return;
    }

    wowClient.battleMobId = mobId;
    wowClient.battleMobTime = new Date().getTime();
    if (!wowClient.isBattle) {
        wowClient.isBattle = true;
        wowClient.sendBattleMob(mobId);
    }
}
  上图中是点击‘打怪’按钮的方法,这里我直接把代码贴出来,显得比较清晰简单。实际上做的时候,经过反复改动和考虑。代码中解决的一些问题,可能三言两语也不太好体现出来,需要自己实际编写代码才能体会。
  比如考虑这个场景,玩家A,在线攻击怪物a , 开启的对a的战斗循环。A升级后,想攻击更高级的怪物b。这时比较合理的操作方式就是玩家直接点击b的战斗按钮。
  那么我们可能要考虑几个问题:
    怪物a的战斗循环需不需要停止,怎么停止;如果要停止战斗,但此时正在播放战斗记录,还没进入5秒的循环,停止循环函数不会生效,该怎么办;播放中对a的战斗记录需不需要立即清除;对b的战斗需不需点击后立即开始。。。
  起初我是按照两条线程的思路来进行实现,即a的线程仍在进行,建立标志位将其停止,点击后立即开启b的线程,但实现起来非常复杂,而且有些问题不好解决,比如a的战斗记录没播放完,b已经发送了战斗请求,那么就需要停止播放a的记录,并清屏,开始播放b的战斗记录。
  后来发现,只需要一个线程即可。仅需要标记战斗目标的怪物id,战斗线程仅对标记的怪物id发送战斗请求,切换战斗目标后,因为被标记的怪物id已经变了,所以a的战斗记录播放完毕后,5秒后自动请求战斗的怪物id已变成了b,这样自动切换到了对b的战斗。从页面表现上,也更符合逻辑。

F&Q

  Q.在初始化战斗时,为什么要把玩家和怪物放到列表中?
  A.考虑后面会有组队战斗。以及战斗技能,比如法师召唤水元素,猎人带宠物。虽然目前仅是1v1,但实现时作为队伍来考虑更方便扩展。
  
  Q.为什么角色阵亡后,仅把其从攻方(守方)列表中移除,不从全体出手列表中移除?
  A.考虑到牧师,骑士可以施放复活技能,阵亡后的角色仍保留在列表中,对性能影响不大,方便以后技能的实现。

附-经验值计算

艾泽拉斯的怪物经验公式是 45+等级*5
外域的怪物经验公式是 235+等级*5
基础知识
魔兽里你选取怪物以后一般名字级别上面有颜色,它指示你和怪物之间的等级差别,
骷髅级别 怪物级别大于等于玩家级别的10级
红色 怪物级别大于等于玩家级别的5级
橙色 怪物级别大于等于玩家级别的3或者4级
黄色 怪物级别小于等于玩家级别2级和大于大于等于玩家级别2级之间
绿色 怪物级别小于玩家级别3级,但是还未变灰
灰色 玩家级别1-5级: 灰色级别小于等于0(没有灰色的怪)
玩家级别 6-39级:灰色级别小于等于 玩家级别-(玩家级别÷10取整数上限) -5
玩家级别 40-59级:灰色级别小于等于 玩家级别-(玩家级别÷5取整数上限) -1
玩家级别 60-70级:灰色级别小于等于 玩家级别-9
注:整数上限是指不小于该值的最小整数,比如4.2整数上限是5,3.0整数上限是3
单杀怪的经验
杀死灰色级别的怪是没有经验的,其他颜色级别的怪单杀的经验值计算如下:(艾泽拉斯)
相同级别的怪:
经验=(玩家等级×5+45)
高等级怪:
经验=(玩家等级×5+45)×(1+0.05×(怪物等级-玩家等级) ,当怪物等级大于玩家等级4级以上均按4级计算,哪怕精英怪
低等级怪:
有一个零差值系数ZD (zero difference value)
ZD = 5, when Char Level = 1 - 7
ZD = 6, when Char Level = 8 - 9
ZD = 7, when Char Level = 10 - 11
ZD = 8, when Char Level = 12 - 15
ZD = 9, when Char Level = 16 - 19
ZD = 11, when Char Level = 20 - 29
ZD = 12, when Char Level = 30 - 39
ZD = 13, when Char Level = 40 - 44
ZD = 14, when Char Level = 45 - 49
ZD = 15, when Char Level = 50 - 54
ZD = 16, when Char Level = 55 - 59
ZD = 17, when Char Level = 60+
经验=(玩家等级×5+45)×(1-(玩家等级-怪物等级)÷ 零差值系数)
计算的例子如下:
假设玩家等级 = 20.
那么灰名怪物等级 = 13, 根据以上表格获得.
杀掉任何 13 级或者以下的怪得不到经验。
同等级基础经验为 (20 * 5 + 45) = 145. 杀掉一个 20 级别的怪将能获得145点经验。
对于一个 21级怪, 你将获得 145 * (1 + 0.05 * 1) = 152.2 四舍五入为 152 点经验.
根据上面表格ZD值是11。
对于18级的怪, 我们将有 145 * (1 - 2/11) = 118.6 四舍五入为 119点经验。
对于16级的怪, 我们将有 145 * (1 - 4/11) = 92.3四舍五入为 92点经验.
对于14级的怪, 我们将有 145 * (1 - 6/11) = 65.91四舍五入为 66点经验.


对于燃烧的远征外域的怪,经验计算较多,笔者根据表格值推论公式如下:
相同级别的怪:
经验=(玩家等级×5+235)
高等级怪:
经验=(玩家等级×5+235)×(1+0.05×(怪物等级-玩家等级) ,当怪物等级大于玩家等级4级以上均按4级计算,哪怕精英怪。
低等级怪:
经验=(玩家等级×5+235)×(1-(玩家等级-怪物等级)÷ 零差值系数)
精英怪经验=普通同等级怪经验×2
精力充沛时间经验=普通计算经验×2 (耗尽精力充沛点数为止,故而最后一个精力充沛期间杀的怪未必能达到经验×2)
影响杀怪经验的因素
对于大号带小号或者抢怪的情况,玩家杀怪的经验值就会有变化。
一般来说,原则如下:
如果你开怪并造成了伤害,那么怪物就是你的;这个时候,如果有别的玩家或者大号来杀了这个怪,那么如果帮助杀怪的人对于这个级别的怪他能够获得经验,则属于抢怪,不会影响你的经验获得;
如果帮助你杀怪的人对于这个级别的怪或不得经验,那么就是就属于带小号了,你获得很少很少的经验,非常的不划算。因此,对于一个60级的玩家来带小号,不管什么级别的怪,他都没有经验(TBC以前),所以小号获得的经验非常少!而如果是59的玩家帮忙杀50+的怪,那么经验都是小号的!
战斗中如果有别人帮你加血,加血只会扣掉你很少的经验,用大号跟随加血小号练级是不错的办法;别人给你加的伤害护盾(比如说荆棘术什么的)只会影响你非常少的经验,5-10点最坏情况,基本可以无视了,放心的加吧。
综上所述,用不满级的相同等级区间里的号带小号效率最高,比如49的带40的,59的带50的…… 但是,大号基本都是60的,没办法,呵呵,只能帮带任务或者副本了。
组队经验值
根据wiki的资料,这个只是推论,未必精确
假设一个队伍中的人都是同等级的,那么
每个人的经验=单杀怪的经验÷人数×系数
系数是:
1人:1
2人:1
3人:1.166
4人:1.3
5人:1.4
例子如下:
杀100经验的怪
1人 = 100xp
2人 = 50xp 每人.
3人 = ~39xp 每人.
4人 = ~33xp 每人.
5人 = ~28xp 每人.
两人队伍计算公式
假设 玩家1级别>玩家2级别
那么 基础经验按玩家1级别计算
最后分得的经验
玩家1获得 基础经验×玩家1级别÷(玩家1级别+玩家2级别)
玩家1获得 基础经验×玩家2级别÷(玩家1级别+玩家2级别)
团队里面经验值要打折(除以2)
经验值计算

效果演示

本章小结

  注意,之前数据库和模型有个列名的单词写错了,我在源码中修正了。
  即map_mob的护甲字段,应为armour,之前写成了amour。如需运行源码,请先修正数据库中的列名。
 
  至此,游戏最重要的战斗功能已有了。
  后面可以开始逐步扩展背包,装备,掉落,随机附魔等重要功能。
 
  本章源码下载地址: https://474b.com/file/14960372-444702415
  项目交流群:329989095
  

猜你喜欢

转载自www.cnblogs.com/lyosaki88/p/idlewow_15.html