游戏属性
关于本教程
本教程是免费且开源的,所有代码均使用 MIT 许可证——因此您可以随意使用。 我希望您会喜欢本教程,并制作出伟大的游戏!
如果您喜欢本教程并希望我继续创作,请考虑支持我的 Patreon。
到目前为止,我们只有非常原始的属性:力量和防御。这并没有提供太多的变化空间,也不符合 Roguelike 游戏中让玩家沉浸在数字中的理想(好吧,这有点夸张了)。在设计文档中,我们提到希望采用类似 D&D 的游戏属性方法。这为各种玩法提供了大量空间,允许带有各种奖励(和惩罚)的物品,并且对于大多数可能玩这类游戏的玩家来说应该会感到熟悉。这也需要一些 UI 工作,但我们会将大部分工作推迟到下一章。
基础 6 属性 - 浓缩为 4 种
任何玩过 D&D 的人都知道,角色——以及后来的版本中的所有人——都拥有六个属性:
- 力量 (Strength),决定了你能携带多少东西、你击打东西的力度以及你一般的身体能力。
- 敏捷 (Dexterity),决定了你躲避事物的速度、你杂技般跳跃的能力,以及像开锁和瞄准弓箭之类的技能。
- 体质 (Constitution),决定了你的身体健康程度,调整你的生命值总量并帮助抵抗疾病。
- 智力 (Intelligence),代表你的聪明程度,帮助你施法、阅读事物。
- 感知 (Wisdom),代表你拥有的常识,以及与神灵的有益互动。
- 魅力 (Charisma),代表你与他人互动的能力。
对于我们正在制作的游戏来说,这有点过头了。智力和感知不需要分开(感知最终会成为每个人为了在其他地方获得点数而抛弃的“废属性”!),而魅力实际上只在与供应商互动时有用,因为我们在游戏中没有进行大量的社交互动。因此,我们将为这款游戏选择一组浓缩的属性:
- 力量 (Might),决定你击中目标的一般能力。
- 体魄 (Fitness),你的一般健康状况。
- 迅捷 (Quickness),你的一般敏捷替代属性。
- 智力 (Intelligence),实际上结合了 D&D 术语中的智力和感知。
这在其他游戏中也是相当常见的组合。让我们打开 components.rs
并创建一个新的组件来保存它们:
#![allow(unused)] fn main() { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Attribute { pub base : i32, pub modifiers : i32, pub bonus : i32 } #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Attributes { pub might : Attribute, pub fitness : Attribute, pub quickness : Attribute, pub intelligence : Attribute } }
所以我们为属性创建了一个结构体,并存储了三个值:
- 基础 (base) 值,这是完全未修改的值。
- 修正值 (modifiers),表示属性的任何有效奖励或惩罚(并且必须不时重新计算)。
- 奖励值 (bonus),它从最终的修正值派生而来——并且在大多数情况下,是我们实际要使用的值。
不要忘记在 main.rs
和 saveload_system.rs
中注册 Attributes
。 Attribute
实际上不是一个组件——它只是被一个组件使用——所以你不必注册它。
给玩家一些属性
现在,我们应该给玩家一些属性。我们将从简单的开始,并为每个属性赋予 11 的值(我们使用 D&D 风格的 3-18,3d6 生成的属性)。在 spawner.rs
中,修改 player
函数如下:
#![allow(unused)] fn main() { pub fn player(ecs : &mut World, player_x : i32, player_y : i32) -> Entity { ecs .create_entity() .with(Position { x: player_x, y: player_y }) .with(Renderable { glyph: rltk::to_cp437('@'), fg: RGB::named(rltk::YELLOW), bg: RGB::named(rltk::BLACK), render_order: 0 }) .with(Player{}) .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true }) .with(Name{name: "Player".to_string() }) .with(CombatStats{ max_hp: 30, hp: 30, defense: 2, power: 5 }) .with(HungerClock{ state: HungerState::WellFed, duration: 20 }) .with(Attributes{ might: Attribute{ base: 11, modifiers: 0, bonus: 0 }, fitness: Attribute{ base: 11, modifiers: 0, bonus: 0 }, quickness: Attribute{ base: 11, modifiers: 0, bonus: 0 }, intelligence: Attribute{ base: 11, modifiers: 0, bonus: 0 }, }) .marked::<SimpleMarker<SerializeMe>>() .build() } }
NPC 的属性
我们可能不想为 spawns.json
中的每个 NPC 都写出每个属性,但我们希望能够在需要时这样做。以普通的 NPC,比如 Barkeep
(酒保)为例。我们可以使用以下语法来表示他在所有方面都具有“正常”属性,但比普通农民更聪明:
{
"name" : "Barkeep",
"renderable": {
"glyph" : "☺",
"fg" : "#EE82EE",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "vendor",
"attributes" : {
"intelligence" : 13
}
},
这种模式很强大,因为我们可以忽略基本上是背景装饰的人物的细节——但可以为重要的怪物填写我们想要的尽可能多的细节!如果您没有指定属性,它将默认为一个中间值。
让我们扩展 raws/mob_structs.rs
中的结构体以支持这种灵活的格式:
#![allow(unused)] fn main() { use serde::{Deserialize}; use super::{Renderable}; #[derive(Deserialize, Debug)] pub struct Mob { pub name : String, pub renderable : Option<Renderable>, pub blocks_tile : bool, pub stats : MobStats, pub vision_range : i32, pub ai : String, pub quips : Option<Vec<String>>, pub attributes : MobAttributes } #[derive(Deserialize, Debug)] pub struct MobStats { pub max_hp : i32, pub hp : i32, pub power : i32, pub defense : i32 } #[derive(Deserialize, Debug)] pub struct MobAttributes { pub might : Option<i32>, pub fitness : Option<i32>, pub quickness : Option<i32>, pub intelligence : Option<i32> } }
请注意,我们将 attributes
设置为必需——因此您必须拥有一个属性才能加载 JSON。然后我们将所有属性值设置为可选;如果您没有指定它们,我们将使用一个不错的、正常的值。
现在让我们打开 raws/rasmaster.rs
并修改 spawn_named_mob
以生成此数据:
#![allow(unused)] fn main() { let mut attr = Attributes{ might: Attribute{ base: 11, modifiers: 0, bonus: 0 }, fitness: Attribute{ base: 11, modifiers: 0, bonus: 0 }, quickness: Attribute{ base: 11, modifiers: 0, bonus: 0 }, intelligence: Attribute{ base: 11, modifiers: 0, bonus: 0 }, }; if let Some(might) = mob_template.attributes.might { attr.might = Attribute{ base: might, modifiers: 0, bonus: 0 }; } if let Some(fitness) = mob_template.attributes.fitness { attr.fitness = Attribute{ base: fitness, modifiers: 0, bonus: 0 }; } if let Some(quickness) = mob_template.attributes.quickness { attr.quickness = Attribute{ base: quickness, modifiers: 0, bonus: 0 }; } if let Some(intelligence) = mob_template.attributes.intelligence { attr.intelligence = Attribute{ base: intelligence, modifiers: 0, bonus: 0 }; } eb = eb.with(attr); }
这将检查 JSON 中是否存在每个属性,并将其分配给 mob(怪物)。
属性奖励值
到目前为止,一切都很好——但是 bonus
(奖励值)字段呢?每个值的奖励值都为 0
是不对的!我们将需要进行大量的游戏系统计算——所以我们将在主项目中创建一个新文件 gamesystem.rs
:
#![allow(unused)] fn main() { pub fn attr_bonus(value: i32) -> i32 { (value-10)/2 // 参见:https://roll20.net/compendium/dnd5e/Ability%20Scores#content } }
这使用了标准的 D&D 规则来确定属性奖励值:减去 10 再除以 2。所以我们的 11 将给出 0 的奖励值——它是平均水平。我们的酒保将获得 1 点智力检定奖励。
在 main.rs
的顶部,添加 mod gamesystem
和 pub use gamesystem::*
以使这个模块在任何地方都可用。
现在修改 spawner.rs
中的玩家生成代码以使用它:
#![allow(unused)] fn main() { use crate::attr_bonus; ... .with(Attributes{ might: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) }, fitness: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) }, quickness: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) }, intelligence: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) }, }) ... }
在 rawmaster.rs
中做同样的事情:
#![allow(unused)] fn main() { use crate::attr_bonus; // 在顶部! ... let mut attr = Attributes{ might: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) }, fitness: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) }, quickness: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) }, intelligence: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) }, }; if let Some(might) = mob_template.attributes.might { attr.might = Attribute{ base: might, modifiers: 0, bonus: attr_bonus(might) }; } if let Some(fitness) = mob_template.attributes.fitness { attr.fitness = Attribute{ base: fitness, modifiers: 0, bonus: attr_bonus(fitness) }; } if let Some(quickness) = mob_template.attributes.quickness { attr.quickness = Attribute{ base: quickness, modifiers: 0, bonus: attr_bonus(quickness) }; } if let Some(intelligence) = mob_template.attributes.intelligence { attr.intelligence = Attribute{ base: intelligence, modifiers: 0, bonus: attr_bonus(intelligence) }; } eb = eb.with(attr); ... }
在编译/运行游戏之前,在 spawns.json
中为每个 mob 添加一个空白的 attributes
条目,以避免错误。这是一个例子:
{
"name" : "Shady Salesman",
"renderable": {
"glyph" : "h",
"fg" : "#EE82EE",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "vendor",
"attributes" : {}
},
技能
在我们开始使用属性之前,我们应该考虑与属性密切相关的另一个元素:技能。对于这个游戏,我们不想搞得太复杂,弄出数百个技能;我们永远无法完成本教程!相反,让我们使用一些非常基础的技能:近战、防御和魔法。我们以后总是可以添加更多技能(但删除它们可能会引起用户的一片嘲笑!)。
在 components.rs
中,让我们创建一个技能持有组件:
#![allow(unused)] fn main() { #[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)] pub enum Skill { Melee, Defense, Magic } #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Skills { pub skills : HashMap<Skill, i32> } }
因此,如果我们要添加技能,将来我们需要将它们添加到 enum
中——但我们的基本 skills
结构可以容纳我们想出的任何技能。不要忘记将 Skills
(而不是 Skill
)添加到 main.rs
和 saveload_system.rs
中进行注册!
打开 spawners.rs
,让我们给 player
在所有技能中都赋予 1
的技能等级:
#![allow(unused)] fn main() { pub fn player(ecs : &mut World, player_x : i32, player_y : i32) -> Entity { let mut skills = Skills{ skills: HashMap::new() }; skills.skills.insert(Skill::Melee, 1); skills.skills.insert(Skill::Defense, 1); skills.skills.insert(Skill::Magic, 1); ecs .create_entity() .with(Position { x: player_x, y: player_y }) .with(Renderable { glyph: rltk::to_cp437('@'), fg: RGB::named(rltk::YELLOW), bg: RGB::named(rltk::BLACK), render_order: 0 }) .with(Player{}) .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true }) .with(Name{name: "Player".to_string() }) .with(CombatStats{ max_hp: 30, hp: 30, defense: 2, power: 5 }) .with(HungerClock{ state: HungerState::WellFed, duration: 20 }) .with(Attributes{ might: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) }, fitness: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) }, quickness: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) }, intelligence: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) }, }) .with(skills) .marked::<SimpleMarker<SerializeMe>>() .build() } }
对于 mob,我们也将假定每个技能的技能等级为 1
,除非另有说明。在 raws/mob_structs.rs
中,更新 Mob
:
#![allow(unused)] fn main() { #[derive(Deserialize, Debug)] pub struct Mob { pub name : String, pub renderable : Option<Renderable>, pub blocks_tile : bool, pub stats : MobStats, pub vision_range : i32, pub ai : String, pub quips : Option<Vec<String>>, pub attributes : MobAttributes, pub skills : Option<HashMap<String, i32>> } }
这允许我们在许多情况下完全省略它(没有非默认技能),这将避免在我们忘记给他们技能时给 mob 带来惩罚! mob 可以覆盖技能,如果我们愿意的话。它们必须与 HashMap
结构对齐。让我们在 spawns.json
中给我们的酒保一个技能奖励(它不会做任何事情,但它可以作为一个例子):
{
"name" : "Barkeep",
"renderable": {
"glyph" : "☺",
"fg" : "#EE82EE",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "vendor",
"attributes" : {
"intelligence" : 13
},
"skills" : {
"Melee" : 2
}
},
让我们修改我们的 raws/rawmaster.rs
的 spawn_named_mob
函数来使用这些数据:
#![allow(unused)] fn main() { let mut skills = Skills{ skills: HashMap::new() }; skills.skills.insert(Skill::Melee, 1); skills.skills.insert(Skill::Defense, 1); skills.skills.insert(Skill::Magic, 1); if let Some(mobskills) = &mob_template.skills { for sk in mobskills.iter() { match sk.0.as_str() { "Melee" => { skills.skills.insert(Skill::Melee, *sk.1); } "Defense" => { skills.skills.insert(Skill::Defense, *sk.1); } "Magic" => { skills.skills.insert(Skill::Magic, *sk.1); } _ => { rltk::console::log(format!("Unknown skill referenced: [{}]", sk.0)); } } } } eb = eb.with(skills); }
将等级、经验和生命值设为组件,添加法力值
在 components.rs
中,继续添加另一个组件(然后在 main.rs
和 saveload_system.rs
中注册它):
#![allow(unused)] fn main() { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Pool { pub max: i32, pub current: i32 } #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Pools { pub hit_points : Pool, pub mana : Pool, pub xp : i32, pub level : i32 } }
这里有很多内容:
- 我们创建了一个新类型来表示可消耗的资源,
Pool
(池)。池具有最大值和当前值。这表示受伤或耗尽魔法力量;最大值不变,但当前值会波动。 - 我们使用
Pool
来存储hit_points
(生命值)和mana
(法力值)。 - 我们还存储
xp
,表示“经验值 (Experience Points)”。 - 我们存储
level
(等级),表示你(或 NPC)的等级。
我们应该为这些定义一些默认值,并确定你的属性如何影响它们。在 gamesystem.rs
中,我们将使用以下函数:
#![allow(unused)] fn main() { pub fn attr_bonus(value: i32) -> i32 { (value-10)/2 // 参见:https://roll20.net/compendium/dnd5e/Ability%20Scores#content } pub fn player_hp_per_level(fitness: i32) -> i32 { 10 + attr_bonus(fitness) } pub fn player_hp_at_level(fitness:i32, level:i32) -> i32 { player_hp_per_level(fitness) * level } pub fn npc_hp(fitness: i32, level: i32) -> i32 { let mut total = 1; for _i in 0..level { total += i32::max(1, 8 + attr_bonus(fitness)); } total } pub fn mana_per_level(intelligence: i32) -> i32 { i32::max(1, 4 + attr_bonus(intelligence)) } pub fn mana_at_level(intelligence: i32, level: i32) -> i32 { mana_per_level(intelligence) * level } pub fn skill_bonus(skill : Skill, skills: &Skills) -> i32 { if skills.skills.contains_key(&skill) { skills.skills[&skill] } else { -4 } } }
如果您一直在关注,这些应该非常容易理解:玩家每级获得 10 点生命值,并由他们的体魄属性修正。NPC 每级获得 8 点,也由体魄修正——每级至少 1 点(对于糟糕的掷骰结果)。
因此,在 spawner.rs
中,我们可以修改 player
函数来为初始角色填充这些池:
#![allow(unused)] fn main() { .with(Pools{ hit_points : Pool{ current: player_hp_at_level(11, 1), max: player_hp_at_level(11, 1) }, mana: Pool{ current: mana_at_level(11, 1), max: mana_at_level(11, 1) }, xp: 0, level: 1 }) }
同样,我们需要让 NPC 能够拥有池。至少,我们必须在他们的定义中添加一个 level
属性——但让我们使其可选,如果省略则默认为 1
(这样您就不需要修改每个普通的 NPC!)。我们还将使 hp
和 mana
字段可选——这样您就可以使用随机默认值,或者为重要的怪物覆盖它们。这是调整后的 raws/mob_structs.rs
:
#![allow(unused)] fn main() { #[derive(Deserialize, Debug)] pub struct Mob { pub name : String, pub renderable : Option<Renderable>, pub blocks_tile : bool, pub vision_range : i32, pub ai : String, pub quips : Option<Vec<String>>, pub attributes : MobAttributes, pub skills : Option<HashMap<String, i32>>, pub level : Option<i32>, pub hp : Option<i32>, pub mana : Option<i32> } }
我们还应该修改 spawn_named_mob
(来自 raws/rawmaster.rs
)以包含此内容:
#![allow(unused)] fn main() { let mut mob_fitness = 11; let mut mob_int = 11; let mut attr = Attributes{ might: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) }, fitness: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) }, quickness: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) }, intelligence: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) }, }; if let Some(might) = mob_template.attributes.might { attr.might = Attribute{ base: might, modifiers: 0, bonus: attr_bonus(might) }; } if let Some(fitness) = mob_template.attributes.fitness { attr.fitness = Attribute{ base: fitness, modifiers: 0, bonus: attr_bonus(fitness) }; mob_fitness = fitness; } if let Some(quickness) = mob_template.attributes.quickness { attr.quickness = Attribute{ base: quickness, modifiers: 0, bonus: attr_bonus(quickness) }; } if let Some(intelligence) = mob_template.attributes.intelligence { attr.intelligence = Attribute{ base: intelligence, modifiers: 0, bonus: attr_bonus(intelligence) }; mob_int = intelligence; } eb = eb.with(attr); let mob_level = if mob_template.level.is_some() { mob_template.level.unwrap() } else { 1 }; let mob_hp = npc_hp(mob_fitness, mob_level); let mob_mana = mana_at_level(mob_int, mob_level); let pools = Pools{ level: mob_level, xp: 0, hit_points : Pool{ current: mob_hp, max: mob_hp }, mana: Pool{current: mob_mana, max: mob_mana} }; eb = eb.with(pools); }
我们在构建 NPC 时捕获了相关的属性,并调用了新函数来帮助构建 NPC 的池。
是时候破坏东西了:删除旧的属性!
在 components.rs
中,删除 CombatStats
。您还需要在 main.rs
和 saveload_system.rs
中删除它。注意您的 IDE 将整个城镇都涂成红色——我们已经用了很多次了!由于我们正在制定一个新的类似 D&D 的系统,所以必须这样做……这也让我们有机会查看我们实际使用它的地方,并做出一些明智的决定。
如果您不想直接遵循所有这些更改,或者感到困惑(我们所有人都会发生这种情况!),本章的源代码 包含可用的版本。
以下是更简单的更改:
- 在
mob_structs.rs
中,您可以删除MobStats
及其在Mob
中的引用。 - 在
rawmaster.rs
中,删除将CombatStats
分配给 NPC 的代码。 - 在
spawns.json
中,您可以删除所有属性块。 - 在
damage_system.rs
中,将所有对CombatStats
的引用替换为Pools
,并将所有对stats.hp
的引用替换为stats.hit_points.current
或stats.hit_points.max
(对于max_hp
)。 - 在
inventory_system.rs
中,将所有对CombatStats
的引用替换为Pools
,并将引用max_hp
和hp
的行替换为stats.hit_points = i32::min(stats.hit_points.max, stats.hit_points.current + healer.heal_amount);
以及不太容易的更改:
在 player.rs
中,将 CombatStats
替换为 Pools
——它将起到相同的作用。此外,找到 can_heal
部分并将其替换为:
#![allow(unused)] fn main() { if can_heal { let mut health_components = ecs.write_storage::<Pools>(); let pools = health_components.get_mut(*player_entity).unwrap(); pools.hit_points.current = i32::min(pools.hit_points.current + 1, pools.hit_points.max); } }
在 main.rs
(第 345 行)中,我们引用了玩家的生命值——他们已经改变了地下城等级,我们给他们恢复了一些生命值。让我们不要那么好心,完全删除它。现在不友好的代码看起来像这样:
#![allow(unused)] fn main() { let player_entity = self.ecs.fetch::<Entity>(); let mut gamelog = self.ecs.fetch_mut::<gamelog::GameLog>(); gamelog.entries.push("You descend to the next level.".to_string()); }
gui.rs
是一个简单的修复。将 CombatStats
的导入替换为 Pools
;这是相关的部分:
#![allow(unused)] fn main() { ... use super::{Pools, Player, gamelog::GameLog, Map, Name, Position, State, InBackpack, Viewshed, RunState, Equipped, HungerClock, HungerState, rex_assets::RexAssets, Hidden, camera }; pub fn draw_ui(ecs: &World, ctx : &mut Rltk) { ctx.draw_box(0, 43, 79, 6, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK)); let combat_stats = ecs.read_storage::<Pools>(); let players = ecs.read_storage::<Player>(); let hunger = ecs.read_storage::<HungerClock>(); for (_player, stats, hc) in (&players, &combat_stats, &hunger).join() { let health = format!(" HP: {} / {} ", stats.hp, stats.max_hp); ctx.print_color(12, 43, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), &health); ctx.draw_bar_horizontal(28, 43, 51, stats.hit_points.current, stats.hit_points.max, RGB::named(rltk::RED), RGB::named(rltk::BLACK)); ... }
更新近战战斗系统。
我们只剩下一个“红色文件”(有错误的文件),在 melee_combat_system.rs
中,但它们现在与依赖于旧系统的核心游戏系统有关。我们希望使其更像 D20 (D&D) 游戏,因此无论如何都应该替换它们。
这意味着是时候讨论我们想要的战斗系统了。让我们采用非常类似 D&D 的(但又不完全是)设置:
- 我们查看攻击者正在使用什么武器。我们需要确定它是基于力量 (Might) 还是迅捷 (Quickness)。如果您是徒手,我们将使用力量 (Might)。
- 攻击者掷出 1d20(一个 20 面骰子)。
- 如果掷出的点数是自然值、未修改的 20,则总是命中。
- 自然值 1 总是未命中。
- 攻击者根据武器添加力量或迅捷的属性奖励值。
- 攻击者添加技能奖励值,等于在近战 (Melee) 技能上花费的点数。
- 攻击者添加武器本身赋予的任何奖励(以防它是魔法武器)。
- 攻击者添加任何情境或状态奖励,这些奖励尚未实现,但最好记住。
- 如果总攻击掷骰值等于或大于目标的护甲等级 (armor class),则目标被击中并将受到伤害。
护甲等级由以下因素决定:
- 从基础数字 10 开始。
- 添加防御 (Defense) 技能。
- 添加装备的护甲(尚未实现!和盾牌)的护甲奖励值。
然后根据近战武器确定伤害:
- 武器将指定骰子类型和奖励(例如
1d6+1
);如果没有装备武器,则徒手战斗造成1d4
伤害。 - 添加攻击者的力量 (Might) 奖励值。
- 添加攻击者的近战 (Melee) 奖励值。
现在我们已经定义了它应该如何工作,我们可以开始实现它了。在我们改进一些装备之前,它将是不完整的——但至少我们可以让它编译。
这是一个替换 melee_combat_system.rs
,它执行了我们描述的操作:
#![allow(unused)] fn main() { use specs::prelude::*; use super::{Attributes, Skills, WantsToMelee, Name, SufferDamage, gamelog::GameLog, particle_system::ParticleBuilder, Position, HungerClock, HungerState, Pools, skill_bonus, Skill}; pub struct MeleeCombatSystem {} impl<'a> System<'a> for MeleeCombatSystem { #[allow(clippy::type_complexity)] type SystemData = ( Entities<'a>, WriteExpect<'a, GameLog>, WriteStorage<'a, WantsToMelee>, ReadStorage<'a, Name>, ReadStorage<'a, Attributes>, ReadStorage<'a, Skills>, WriteStorage<'a, SufferDamage>, WriteExpect<'a, ParticleBuilder>, ReadStorage<'a, Position>, ReadStorage<'a, HungerClock>, ReadStorage<'a, Pools>, WriteExpect<'a, rltk::RandomNumberGenerator> ); fn run(&mut self, data : Self::SystemData) { let (entities, mut log, mut wants_melee, names, attributes, skills, mut inflict_damage, mut particle_builder, positions, hunger_clock, pools, mut rng) = data; for (entity, wants_melee, name, attacker_attributes, attacker_skills, attacker_pools) in (&entities, &wants_melee, &names, &attributes, &skills, &pools).join() { // 攻击者和防御者都活着吗? 只有当他们都活着时才攻击 let target_pools = pools.get(wants_melee.target).unwrap(); let target_attributes = attributes.get(wants_melee.target).unwrap(); let target_skills = skills.get(wants_melee.target).unwrap(); if attacker_pools.hit_points.current > 0 && target_pools.hit_points.current > 0 { let target_name = names.get(wants_melee.target).unwrap(); let natural_roll = rng.roll_dice(1, 20); let attribute_hit_bonus = attacker_attributes.might.bonus; let skill_hit_bonus = skill_bonus(Skill::Melee, &*attacker_skills); let weapon_hit_bonus = 0; // TODO: 一旦武器支持这个 let mut status_hit_bonus = 0; if let Some(hc) = hunger_clock.get(entity) { // 吃饱喝足状态给予 +1 if hc.state == HungerState::WellFed { status_hit_bonus += 1; } } let modified_hit_roll = natural_roll + attribute_hit_bonus + skill_hit_bonus + weapon_hit_bonus + status_hit_bonus; let base_armor_class = 10; let armor_quickness_bonus = target_attributes.quickness.bonus; let armor_skill_bonus = skill_bonus(Skill::Defense, &*target_skills); let armor_item_bonus = 0; // TODO: 一旦护甲支持这个 let armor_class = base_armor_class + armor_quickness_bonus + armor_skill_bonus + armor_item_bonus; if natural_roll != 1 && (natural_roll == 20 || modified_hit_roll > armor_class) { // 目标被击中! 在我们支持武器之前,我们使用 1d4 let base_damage = rng.roll_dice(1, 4); let attr_damage_bonus = attacker_attributes.might.bonus; let skill_damage_bonus = skill_bonus(Skill::Melee, &*attacker_skills); let weapon_damage_bonus = 0; let damage = i32::max(0, base_damage + attr_damage_bonus + skill_hit_bonus + skill_damage_bonus + weapon_damage_bonus); SufferDamage::new_damage(&mut inflict_damage, wants_melee.target, damage); log.entries.push(format!("{} hits {}, for {} hp.", &name.name, &target_name.name, damage)); if let Some(pos) = positions.get(wants_melee.target) { particle_builder.request(pos.x, pos.y, rltk::RGB::named(rltk::ORANGE), rltk::RGB::named(rltk::BLACK), rltk::to_cp437('‼'), 200.0); } } else if natural_roll == 1 { // 自然值 1 未命中 log.entries.push(format!("{} considers attacking {}, but misjudges the timing.", name.name, target_name.name)); if let Some(pos) = positions.get(wants_melee.target) { particle_builder.request(pos.x, pos.y, rltk::RGB::named(rltk::BLUE), rltk::RGB::named(rltk::BLACK), rltk::to_cp437('‼'), 200.0); } } else { // 未命中 log.entries.push(format!("{} attacks {}, but can't connect.", name.name, target_name.name)); if let Some(pos) = positions.get(wants_melee.target) { particle_builder.request(pos.x, pos.y, rltk::RGB::named(rltk::CYAN), rltk::RGB::named(rltk::BLACK), rltk::to_cp437('‼'), 200.0); } } } } wants_melee.clear(); } } }
这是一大段代码,但它与我们创建的轮廓非常相似。让我们逐步了解一下:
- 我们仔细定义了我们需要访问的许多 ECS 资源。
- 我们迭代所有想要近战、并且具有名称、技能、属性和池的实体。
- 我们获取目标的技能和池。
- 我们检查攻击者和目标是否都活着,如果他们没有活着就跳过。
- 我们获取目标的名称,我们将在日志记录中需要它。
- 我们通过掷出 1d20 获得
natural_roll
(自然掷骰值)。 - 我们计算属性命中奖励值,引用攻击者的力量 (Might) 奖励值。
- 我们计算技能命中奖励值,引用攻击者的近战 (Melee) 技能。
- 我们将
weapon_hit_bonus
(武器命中奖励值)设置为 0,因为我们尚未实现它。 - 我们查看攻击者是否处于吃饱喝足状态,如果是,则给予他们 +1 情境奖励。
- 我们现在可以通过添加步骤 6 到 10 来计算
modified_hit_roll
(修正后的命中掷骰值)。 - 我们将
base_armor_class
(基础护甲等级)设置为 10。 - 我们从目标获取迅捷奖励值,并将其设置为
armor_quickness_bonus
(护甲迅捷奖励值)。 - 我们从目标获取防御 (Defense) 技能,并将其设置为
armor_skill_bonus
(护甲技能奖励值)。 - 我们设置
armor_item_bonus
(护甲物品奖励值),因为我们尚未实现它。 - 我们通过添加步骤 12 到 15 来计算
armor_class
(护甲等级)。 - 如果
natural_roll
(自然掷骰值)不等于 1,并且是 20 或modified_hit_roll
(修正后的命中掷骰值)大于或等于armor_class
(护甲等级)——那么攻击者就命中了:- 由于我们尚未正确支持战斗物品,因此我们掷
1d4
作为基础伤害。 - 我们添加攻击者的力量 (Might) 奖励值。
- 我们添加攻击者的近战 (Melee) 技能。
- 我们向伤害系统发送
inflict_damage
(造成伤害)消息,记录攻击,并播放橙色粒子效果。
- 由于我们尚未正确支持战斗物品,因此我们掷
- 如果
natural_roll
(自然掷骰值)为 1,我们会提到这是一个壮观的失误并显示蓝色粒子效果。 - 否则,这是一个普通的失误——播放青色粒子效果并记录失误。
最后,让我们打开 spawns.json
并使老鼠非常弱。否则,在没有装备的情况下,当您找到它们时,您会被直接杀死:
{
"name" : "Rat",
"renderable": {
"glyph" : "r",
"fg" : "#FF0000",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"vision_range" : 8,
"ai" : "melee",
"attributes" : {
"Might" : 3,
"Fitness" : 3
},
"skills" : {
"Melee" : -1,
"Defense" : -1
}
},
您现在可以 cargo run
,慢跑到下楼梯处(它会在右边)或找到废弃的房屋,并进行战斗!由于尚未实现物品,因此内容仍然会相当缺乏——但基础知识已经存在,您可以亲眼看到“d20”系统的运作。战斗不再那么确定性,并且可能具有一些真正的紧张感,而不是“国际象棋般的紧张感”。
总结
我们现在已经实现了游戏属性和一个简单的类似 D&D 的近战系统。还有更多的事情要做,我们将在下一章中进入下一个阶段——装备。
本章的源代码可以在这里找到
使用 WebAssembly 在您的浏览器中运行本章的示例(需要 WebGL2)
版权所有 (C) 2019, Herbert Wolverson。