This chapter preliminarily realizes the core function of the game – combat logic.

Combat system involves a very wide range, such as early character attributes, monster configuration, etc., are paving the way for combat.

In combat, characters can cast magic and skills, which require skill system support.

After a victory in battle, xp and loot settle. Need backpack, equipment system support. The equipping system also requires a random affix enchant system.

Arguably the most hardcore system in the game.

Skills, packs, equipment systems are not implemented yet. Let’s start with a preliminary design to implement a simple combat logic.

Combat moves consist only of normal attacks, which may result in a miss, dodge, or critical hit.

The flow of the entire combat logic is roughly as shown in the figure below:

 

First, combat message design

With reference to other messages, the combat action needs to send a request and receive a return message. Let’s define two message codes:

CBattleMob = “30003001”

SBattleMob = “60003001”

 

Here we only consider online combat, send battle request, we only need to know the monster ID, battle from the database to read the monster attributes.

Create a client message class as follows:

@Data
public final class CBattleMobMessage extends ClientMessage {
    private String mobId;
}
Copy the code

The server needs to return the final result information of the battle and record the battle action of each character in each turn to the client for playback.

The new server message class is as follows:

@Data
public class SBattleMobMessage extends ServerMessage {
    private BattleMobResult battleMobResult;
}
Copy the code
@data public class BattleMobResult implements Serializable {// Total number of rounds private Integer totalRound; Private List<BattleRound> roundList; // Whether the player wins private Boolean playerWin; Private String resultMessage; public BattleMobResult() { this.roundList = new ArrayList<>(); } public void putBattleRound(BattleRound battleRound) { this.roundList.add(battleRound); }}Copy the code
@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); }}Copy the code

Here BattleMobResult and BattleRound two classes, it is returned to the page using view model, in the game. When the new hub. Message. Vo. Raged in the package.

Combat unit modeling

At the beginning of the battle, we take out a copy of the unit’s attributes at that point in time, and we use that copy to calculate everything from there.

Monster and player pack classes have very different attributes, so for the sake of uniform calculation, we abstract a BattleUnit class and store some common attributes, such as rank and health.

Some abstract methods are also defined, such as getting attack strength getAP() and armor level getAC(). The player and the monster need to implement these two abstractions separately.

Player, combat stats (level 2 stats) are further calculated from the level 1 stats of strength, agility, endurance, and intelligence. For example, a warrior’s attack power = Level *3+ Strength *2-20. Speed = agility. Armor Level = Stamina *2. Hit ratio =0.95. Dodge and Critical strike = Agility *0.0005.

Monsters, just for training, is not so troublesome, data entry only damage and armor two attributes. Attack power directly takes the damage value. The velocity is just 0. The default hit rate is 0.95. Dodge and crit chance default 0.05.

There is another clever real method, getDR(), in the virtual BattleUnit class to get damage relief. By writing this in a virtual base class, both the player and the monster instance can calculate the corresponding DR based on their AC.

Here is the formula for calculating DR: Damage reduction = Armor level/(Armor level + 85* Player (Monster level + 400)

Public Double getDR() {Integer ac = this.getac (); /** * public Double getDR() {Integer ac = this.getac (); Return ac/(ac + 85 * this.level + 400.0); }Copy the code

The UML diagrams of the three classes are shown below, and the specific implementation can be viewed by downloading the source code.

3. Combat mechanism

Once the model is built, all that is left is the combat logic. Among them, a core problem is the determination of combat action. Whether a normal attack is dodged, blocked, crit, or just a hit. Do we need separate ROLL points for each of the possible outcomes? Different games have different implementations here. We refer to the world of Warcraft decision method, the round table theory, which only rolls once, so that the logic is easier to deal with.

Theory of the round table

“The area of a round table is fixed. If several items occupy all the area of the round table, other items cannot be placed on the round table.”

This theory, in combat logic, involves placing possible outcomes on the table in order of priority, such as the following scenario (where probabilities vary by attributes, equipment, etc., just to name a few) :

  • Miss (5%)
  • Dodge (5%)
  • Parry (20%)
  • Block (20%)
  • Critical hits (5%)
  • Normal attack

ROLL only once. If you ROLL to 3, the player misses the monster. If rolled to 49, the player’s attack is blocked by the monster; Anything over 55 is a normal attack.

If the player changes into a crit suit, the crit rate reaches 60%. ROLL to 50-100 is considered critical hits. Normal attacks are kicked off the table and never occur.

 

In this implementation, we consider only physical misses, dodges, and crits. Leave out secondary ROLL points (attacks that generate critical hits but are dodged or blocked) and ROLL points for spells.

Fourth, the implementation of combat logic

With this foundation in place, we can implement the complete combat logic in code.

For now, it only includes online combat, but in the future it may include platoon combat, dungeon combat, PVP, etc. In a separate bag we put combat logic, com. Idlewow. Game. Logic. The battle, the core of the new combat logic class BattleCore here, specific implementation code is as follows:

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;
    }
}
Copy the code

BattleCore.java

As shown in the above code, we first initialize a copy of each unit’s attributes and add it to the three lists we create: atkList, defList to check if all of the units are dead, and battleList to rank the units in order of attack by speed.

Merge sort is used to sort collections in the BattleUtil class. Given that there is a lot of adding, modifying, and deleting of collections, use LinkedList to save the participating collections. (There’s actually less data, and an ArrayList might make no difference).

The order of the shot is only determined once before the turn begins, because there are currently no skills introduced. If they are introduced, for example, if the hunter casts a leopard guard and all of us have +50 speed, then the order of the shot list will need to be reordered.

After entering the loop, randomly select the attack target -> determine the action -> survival detection -> end of battle detection, here notes and code more clear, not one explanation.

After the attack action and result are determined, the description will be added to the returned information. Later, if the back end is not elegant enough to transmit such content, it can also define a set of rules to transmit only key data, and the battle record will be generated by the front end. But not for now.

After the battle, if the player wins, experience points need to be settled. In ExpUtil, there is a formula for calculating experience.

5. Play battle records

After the battle calculation is complete, the back end returns the battle information to the front end, which only plays the battle information.

The method code for playing records is as follows:

 

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> "< /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); / /}); }},Copy the code

In the above code, the last 3 lines of code are commented out, i.e., 5 seconds later, attack this monster again. If you just consider hitting monsters and using the setTimeout method, there is no difference.

In business, however, if the player might need to click to stop the attack, use setTimeout to execute the loop, and clearInterval to terminate the function execution.

/ / function battleMob(mobId) {let diff = new Date().getTime() -wowClient.battlemobTime; if (diff < TimeLag.BattleMob * 1000) { let waitSeconds = parseInt(TimeLag.BattleMob - diff / 1000); 'Please don't click too often! '+ waitSeconds +' '); return; } if (mobId === wowClient.battlemobid) {alert(" already in battle! Do not click again!" ); return; } wowClient.battleMobId = mobId; wowClient.battleMobTime = new Date().getTime(); if (! wowClient.isBattle) { wowClient.isBattle = true; wowClient.sendBattleMob(mobId); }}Copy the code

Above is the method of clicking the “fight monsters” button, here I directly posted the code, it is relatively clear and simple. In fact, when we do it, we change it and think about it. Some problems solved in the code may not be well reflected in a few words, you need to actually write code to understand.

Consider, for example, the scenario where player A, attacking monster A online, opens A battle loop against Player A. After A level up, he wants to attack the more advanced monster B. The logical thing to do is to click on B’s battle button.

Then we might want to consider a few questions:

[Fixed] Does the battle loop of monster A need to be stopped and how? If you want to stop the battle, but the battle record is playing at this time, and the 5-second loop has not entered, the stop loop function does not take effect, what should I do? Does the battle record of A need to be cleared immediately during playback? The battle against B should start immediately after clicking…

At first I was implemented according to the thinking of the two threads, namely a thread is still on, stop its logo, click open immediately after b threads, but it’s very complicated to implement and bad some problems to solve, such as a combat log didn’t play out, b fight has sent a request, you will need to stop playing a record, and clear screen, Start playing B’s battle log.

As it turns out, you only need one thread. Need only mark combat target monster id, only the tag monsters fight thread id request, to the combat target after switch, because the marked monster id has changed, so a combat log after the broadcast, request automatically after 5 seconds fighting monsters id has become b, so automatically switches to the battle of b. From the page presentation, it is also more logical.

F&Q

Q. Why put players and monsters in a list when initializing battles?

A. Consider A platoon battle later. And combat skills such as mages summoning water elementals and hunters with pets. Although it is currently only 1V1, when implemented it is considered more scalable as a team.

  

Q. why is it only removed from the attack (defense) list when a character is killed, but not from the all shots list?

A. In consideration of priests and knights, resurrection skills can be cast, and characters killed in battle are still kept in the list, which has little impact on performance and facilitates the implementation of skills in the future.

Attached – Empirical value calculation

The monster experience formula in Azeroth is 45+ level *5. The monster experience formula in Outland is 235+ level *5. The skeleton level Monster level greater than or equal to the player level of player red monster level greater than or equal to 10 level of level 5 orange monster level greater than or equal to the player level of grade 3 or 4 yellow Monster level is less than or equal to the player level grade 2 and greater than the level of greater than or equal to the player between green monster level is less than level players level 3, but has not yet become gray Gray player levels 1-5: Gray ≤ 0(no gray monster) Player level 6-39: Gray ≤ or equal to player level -(Player level ÷10 integer upper limit) -5 Player level 40-59: Gray ≤ or equal to player level -(Player level ÷5 integer upper limit) -1 Player level Level 60-70: gray level less than or equal to the player level - 9 note: integer limit refers to the smallest integer not less than the value, such as integer limit is 4.2 5,3.0 integer limit is 3 single kill the monster Kill the gray levels of the blame is inexperienced, other color level of single fault kill experience value calculation is as follows: () the same level of azeroth strange: Experience = (player level ×5+45) Advanced monsters: Experience = (player level ×5+45) × (1+0.05× (Monster level - player level), when the monster level is greater than the player level 4 or above, the monster level is calculated as level 4, even elite monsters of low level: 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+ Experience = (player Level ×5+45) × (1- (player Level - monster Level) ÷ zero difference coefficient) example calculation: Assume player Level = 20. So gray-named monster level = 13, according to the table above. Killing MOBS at level 13 or below gives no xp. Base xp at the same level is (20 * 5 + 45) = 145. Killing a level 20 monster gains 145 xp. For a level 21 monster, you'll get 145 * (1 + 0.05 * 1) = 152.2 rounded to 152 xp. According to the table above, the ZD value is 11. For level 18 monsters, we will have 145 * (1-2/11) = 118.6 rounded to 119 xp. For level 16 monsters, we will have 145 * (1-4/11) = 92.3 rounded to 92 xp points. For level 14 monsters, we will have 145 * (1-6/11) = 65.91 rounded to 66 xp points. For burning outland monsters, experience calculation is more, the author deduces the following formula according to the table values: Monsters of the same level: Experience = (player level ×5+235) Advanced monsters: Experience = (player level ×5+235) × (1+0.05× (monster level - player level), monsters whose level is greater than 4 are counted as 4, even elite monsters. Low level monsters: Experience = (player level ×5+235) × (1- (player level - monster level) ÷ zero difference coefficient) Elite strange experience = ordinary same level strange experience ×2 energetic time experience = ordinary calculation experience ×2 (exhausted energetic points, So the last MOBS killed during the full energy period will not reach xp ×2) Factors affecting the experience of killing MOBS for large with small or snatching MOBS, the experience value of killing MOBS will change. Generally speaking, the principle is as follows: if you create a monster and cause damage, the monster is yours; At this time, if there is another player or a large to kill the monster, then help kill the monster for this level of the monster he can gain experience, it is loot, will not affect your experience gain; If the person who helps you kill monsters is not experienced at this level, then it belongs to the trumpet, you get very little experience, very not cost-effective. Therefore, for a level 60 player to bring a trumpet, regardless of the level of monsters, he has no experience (before TBC), so the trumpet gains very little experience! If 59 players help kill 50+ MOBS, the experience will be small! If someone else helps you add health in battle, adding health will only take away a little of your experience. It is a good way to follow the large health trumpet. Damage shields (thorns, for example) will only affect very little of your experience, 5-10 worst case, you can ignore it, feel free to add it. To sum up, it is the most efficient to use the number with the trumpet in the same grade interval of the dissatisfaction level, such as 49 with 40, 59 with 50... However, the large size is basically 60, there is no way, ha ha, can only help with tasks or copies. If everyone in a team is of the same level, then each person's experience = the experience of killing monsters in a single team ÷ the number of people × the coefficient is: 1 person: 1 2 people: 1 3 people: 1.166 4 people: 1.3 5 people: 1.4 Example: Kill 100xp monsters 1 = 100xp 2 = 50xp per person 3 = ~39xp per person 4 = ~33xp per person 5 = ~28xp per person The calculation formula for a two-player team assumes that player level 1 > Player level 2, then the basic experience is calculated according to player level 1. The final experience is gained by player 1 × Player level 1 ÷ (Player level 1 + Player Level 2) - Discount xp in teams (divided by 2)Copy the code

Empirical calculation

Results demonstrate

The summary of this chapter

Note that there was a word error in the column name of the database and model, which I have fixed in the source code.

The armor field of map_mob should be Armour, which was previously written as amour. To run the source code, correct the column names in the database.

 

At this point, the game’s most important combat feature is in place.

Later you can start to gradually expand the backpack, equipment, drop, random enchantment and other important features.

 

In this paper, the original address: www.cnblogs.com/lyosaki88/p…

Download this chapter source code address: 474b.com/file/149603…

Project Exchange Group: 329989095

  

Demo presentation address: http://175.24.107.27:20010/ (such as server expires, has time to set up a new server to update here)

When creating a character, select Human-warrior, as the values for other races and classes are not configured.