攻击!- 随机选择敌人

激动人心的部分来了:战斗!战斗系统是一个复杂的工作,我们将分两课来讲解。在本课中,我们将重点讲解如何随机选择一个敌人进行攻击以及如何确定胜负。

为了简化,每场战斗涉及两个城堡。攻击方城堡主动发起战斗,而防御方城堡被动参与战斗。然而,匹配过程是随机的,这意味着攻击者不能选择特定的城堡进行攻击。

战斗结束后,双方都进入战斗冷却状态。胜方进入1小时的冷却期,此期间内城堡不能再发起战斗或被攻击。另一方面,败方则进入4小时的冷却期。

1. 随机选择敌人

首先,我们需要从游戏存储中的城堡ID池中随机选择一个敌人城堡ID。让我们在 core.move 中添加一个函数。

// 随机选择一个目标城堡ID
public(package) fun random_battle_target(from_castle: ID, game_store: &GameStore, ctx: &mut TxContext): ID {
    
}

随机数学类似于生成城堡序列号,这次我们需要从城堡ID向量中生成一个随机索引,在 utils.move 中添加一个 random_in_range 函数:

public(package) fun random_in_range(range: u64, ctx: &mut TxContext): u64 {
    let uid = object::new(ctx);
    let mut hash = hash::sha2_256(object::uid_to_bytes(&uid));
    object::delete(uid);

    let mut result_num: u64 = 0;
    while (vector::length(&hash) > 0) {
        let element = vector::remove(&mut hash, 0);
        result_num = (result_num << 8) | (element as u64);
    };
    result_num = result_num % range;

    result_num
}

填充 random_battle_target 函数如下:

use move_castle::utils;

// 随机选择一个目标城堡ID
public(package) fun random_battle_target(from_castle: ID, game_store: &GameStore, ctx: &mut TxContext): ID {
    let total_length = vector::length<ID>(&game_store.castle_ids);
    assert!(total_length > 1, 0);

    let mut random_index = utils::random_in_range(total_length, ctx);
    let mut target = vector::borrow<ID>(&game_store.castle_ids, random_index);

    while (object::id_to_address(&from_castle) == object::id_to_address(target)) {
        // 重新随机直到不相等
        random_index = utils::random_in_range(total_length, ctx);
        target = vector::borrow<ID>(&game_store.castle_ids, random_index);
    };

    object::id_from_address(object::id_to_address(target))
}

如你所见,我们确保池中至少有一个以上的城堡,并确保不会攻击自己。

“战斗”数据依赖于城堡的数据,在 core.move 中添加一个公共函数以帮助获取城堡数据:

public(package) fun fetch_castle_data(id1: ID, id2: ID, game_store: &mut GameStore): (CastleData, CastleData) {
    let castle_data1 = dynamic_field::remove<ID, CastleData>(&mut game_store.id, id1);
    let castle_data2 = dynamic_field::remove<ID, CastleData>(&mut game_store.id, id2);
    (castle_data1, castle_data2)
}

sources 目录下创建一个 battle.move 文件,插入一个战斗入口函数。

module move_castle::battle {
    use sui::clock::{Self, Clock};
    
    use move_castle::castle::Castle;
    use move_castle::core::{Self, GameStore};

    entry fun battle(castle: &Castle, clock: &Clock, game_store: &mut GameStore, ctx: &mut TxContext) {
        // 1. 随机选择一个目标
        let attacker_id = object::id(castle);
        let target_id = core::random_battle_target(attacker_id, game_store, ctx);

        // 2. 获取城堡数据
        let (attacker, defender) = core::fetch_castle_data(attacker_id, target_id, game_store);
    }

}

我们现在有了双方,检查他们的冷却状态:

// 3. 检查战斗冷却
let current_timestamp = clock::timestamp_ms(clock);
assert!(core::get_castle_battle_cooldown(&attacker) < current_timestamp, 0);
assert!(core::get_castle_battle_cooldown(&defender) < current_timestamp, 0);

core.move 中的 get_castle_battle_cooldown 函数:

public(package) fun get_castle_battle_cooldown(castle_data: &CastleData): u64 {
    castle_data.millitary.battle_cooldown
}

为什么我们不将所有战斗相关组件放在 battle.move 中?原因在于一个规则:结构字段不能在其定义模块外访问。例如,代码 castle_data.race 仅在 core.move 中有效。

2. 总攻击/防御力量

如果他们被允许战斗,那么是时候决定胜利者了。计算围绕总攻击力和总防御力进行。类似于经济实力,总攻击力或防御力由城堡的基础攻击/防御力和士兵提供的力量组成。

core.move 中,添加函数来计算城堡的总士兵攻击力和防御力:

/// 城堡的总士兵攻击力
public(package) fun get_castle_total_soldiers_attack_power(castle_data: &CastleData): u64 {
    let (soldier_attack_power, _) = get_castle_soldier_attack_defense_power(castle_data.race);
    castle_data.millitary.soldiers * soldier_attack_power
}

/// 城堡的总士兵防御力
public(package) fun get_castle_total_soldiers_defense_power(castle_data: &CastleData): u64 {
    let (_, soldier_defense_power) = get_castle_soldier_attack_defense_power(castle_data.race);
    castle_data.millitary.soldiers * soldier_defense_power
}

并将基础攻击/防御力量添加为总力量:

/// 城堡的总攻击力(基础+士兵)
public(package) fun get_castle_total_attack_power(castle_data: &CastleData): u64 {
    castle_data.millitary.attack_power + get_castle_total_soldiers_attack_power(castle_data)
}

/// 城堡的总防御力(基础+士兵)
public(package) fun get_castle_total_defense_power(castle_data: &CastleData): u64 {
    castle_data.millitary.defense_power + get_castle_total_soldiers_defense_power(castle_data)
}

由于士兵数量的变化(如招募士兵)会导致总攻击/防御力量的变化,因此我们需要在那时更新它们。在 core 模块的 recruit_soldiers 函数结束时:

public(package) fun recruit_soldiers (id: ID, count: u64, clock: &Clock, game_store: &mut GameStore) {
...

    // 7. 更新总攻击/防御力量
    castle_data.millitary.total_attack_power = get_castle_total_attack_power(freeze(castle_data));
    castle_data.millitary.total_defense_power = get_castle_total_defense_power(freeze(castle_data));
} 

3. 种族优势

正如我们在第一课的“战斗系统”中介绍的那样,种族之间存在战斗优势。拥有优势的一方可以享受50%的数值加成(攻击方的攻击力或防御方的防御力)。

我们需要一个函数来检查攻击者或防御者是否具有种族优势,在 core.move 中:

// 如果有种族优势
public(package) fun has_race_advantage(castle_data1: &CastleData, castle_data2: &CastleData): bool {
    let c1_race = castle_data1.race;
    let c2_race = castle_data2.race;

    let has;
    if (c1_race == c2_race) {
        has = false;
    } else if (c1_race < c2_race) {
        has = (c2_race - c1_race) == 1;
    } else {
        has = (c1_race - c2_race) == 4;
    };

    has
}

种族代码按种族优势顺序定义。

现在我们可以计算最终的攻击力和防御力,在 battle 函数中:

use sui::math;

// 4. 战斗
// 4.1 计算总攻击力和防御力
let mut attack_power = core::get_castle_total_attack_power(&attacker);
let mut defense_power = core::get_castle_total_defense_power(&defender);
if (core::has_race_advantage(&attacker, &defender)) {
    attack_power = math::divide_and_round_up(attack_power * 15, 10)
} else if (core::has_race_advantage(&defender, &attacker)) {
    defense_power = math::divide_and_round_up(defense_power * 15, 10)
};

然后我们确定赢家和输家:

// 4.2 确定胜负
let (winner, loser);
if (attack_power > defense_power) {
    winner = attacker;
    loser = defender;
} else {
    winner = defender;
    loser = attacker;
};
let winner_id = core::get_castle_id(&winner);
let loser_id = core::get_castle_id(&loser);

core.move 中的 get_castle_id 函数:

public(package) fun get_castle_id(castle_data: &CastleData): ID {
    castle_data.id
}