攻击!- 随机选择敌人
激动人心的部分来了:战斗!战斗系统是一个复杂的工作,我们将分两课来讲解。在本课中,我们将重点讲解如何随机选择一个敌人进行攻击以及如何确定胜负。
为了简化,每场战斗涉及两个城堡。攻击方城堡主动发起战斗,而防御方城堡被动参与战斗。然而,匹配过程是随机的,这意味着攻击者不能选择特定的城堡进行攻击。
战斗结束后,双方都进入战斗冷却状态。胜方进入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
}