装备
关于本教程
本教程是免费且开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用。我希望您会喜欢本教程,并制作出伟大的游戏!
如果您喜欢这个教程并希望我继续写作,请考虑支持我的 Patreon。
在上一章中,我们转到了 d20 风格(类似 D&D)的战斗系统和属性系统。它功能完善,但并没有真正提供任何通过装备来提升角色的机会。寻找酷炫的物品,并煞费苦心地最大化您的效率是 roguelike 游戏的基础 - 提供了大量的深度,以及一种感觉,即虽然游戏是随机的,但您可以极大地影响它以获得您想要的结果。
解析骰子字符串
我们将发现能够读取包含 D&D 骰子规格(例如 20d6+4
)的字符串并将其转换为计算机友好的数字非常有帮助。我们在读取原始文件时会经常使用它,所以我们会把它放在那里 - 但要公开它,以防我们在其他地方需要它。
解析像这样的文本片段是正则表达式的完美工作。Rust 通过 crate 支持正则表达式,所以我们必须打开 cargo.toml
并将 regex = "1.3.6"
添加到 [dependencies]
部分。实际上 教 正则表达式本身就可以写一本书;这是一个非常复杂(且强大)的系统,并且往往看起来像猫在您的键盘上走过一样。这是一个解析 1d20+4
类型字符串的正则表达式:
(\d+)d(\d+)([\+\-]\d+)?
这到底是什么 意思 呢?
- 括号
(..)
中包含的每个部分都是一个匹配组。您正在告诉正则表达式,括号中的任何内容对您都很重要,并且可以被捕获以供读取。 \d
是正则表达式的说法,表示“我希望这里有一个数字”。- 添加
+
表示“这里可能不止一个数字,继续读取直到遇到其他内容”。 - 因此,第一个
(\d+)
表示“捕获字符串开头的全部数字”。 - 组外的
d
是字面量d
字符。因此,我们用字母d
将第一组数字与后续部分分隔开。现在我们已经到了1d
。 - 下一个
(\d+)
工作方式相同 - 继续读取数字,并将它们捕获到第二个组中。所以现在我们已经读取到1d20
,并且在组中捕获了1
和20
。 - 最后一个组有点令人困惑。
[..]
表示“期望这些字符中的任何一个”。反斜杠 (\
) 转义 了后续字符,意思是“+ 或 - 在正则表达式语言中可能意味着某些东西;在这种情况下,请将其视为符号”。所以[\+\-]
表示“期望这里有一个加号或减号”。然后我们读取那里的数字。 - 所以现在我们已经将
1d20+4
分解为1
、20
和4
。
完全有可能我们会传递没有 +4
的骰子类型,例如 1d20
。在这种情况下,正则表达式将匹配 1
和 20
- 但最后一个组将为空。
这是我们函数的 Rust 代码:
#![allow(unused)] fn main() { pub fn parse_dice_string(dice : &str) -> (i32, i32, i32) { lazy_static! { static ref DICE_RE : Regex = Regex::new(r"(\d+)d(\d+)([\+\-]\d+)?").unwrap(); } let mut n_dice = 1; let mut die_type = 4; let mut die_bonus = 0; for cap in DICE_RE.captures_iter(dice) { if let Some(group) = cap.get(1) { n_dice = group.as_str().parse::<i32>().expect("Not a digit"); // 不是数字 } if let Some(group) = cap.get(2) { die_type = group.as_str().parse::<i32>().expect("Not a digit"); // 不是数字 } if let Some(group) = cap.get(3) { die_bonus = group.as_str().parse::<i32>().expect("Not a digit"); // 不是数字 } } (n_dice, die_type, die_bonus) } }
嗯,这简直是一团糟。让我们逐步分析并尝试稍微解读一下。
- 正则表达式在 Rust Regex 库首次解析时被编译成它们自己的内部格式。我们不想每次尝试读取骰子字符串时都这样做,所以我们采纳了 Rust Cookbook 的建议,并将读取表达式的操作烘焙到
lazy_static!
中(就像我们用于全局变量一样)。这样,它只会被解析一次,并且正则表达式在我们需要时就可以使用了。 - 我们将一些可变变量设置为骰子表达式的不同部分;骰子数量、它们的类型(面数)和奖励(如果是惩罚则为负数)。我们为它们提供了一些默认值,以防我们在读取字符串(或其部分)时遇到问题。
- 现在我们使用 regex 库的
captures_iter
功能;我们将我们正在查看的字符串传递给它,它返回所有捕获的迭代器(复杂的正则表达式可能有许多捕获)。在我们的例子中,这返回一个捕获集,其中可能包含我们上面讨论的所有组。 - 现在,任何组都可能不存在。所以我们对每个捕获组都执行
if let
。如果它确实存在,我们使用as_str
检索字符串并将其解析为整数 - 并将其分配给骰子读取器的正确部分。 - 我们将所有部分作为元组返回。
定义近战武器
目前,没有必要更改消耗品 - 系统运行良好。我们将专注于可装备物品:您可以挥舞、穿戴或以其他方式从中受益的物品。我们之前对“匕首”的定义如下所示:
{
"name" : "Dagger",
"renderable": {
"glyph" : "/",
"fg" : "#FFAAAA",
"bg" : "#000000",
"order" : 2
},
"weapon" : {
"range" : "melee",
"power_bonus" : 2
}
},
power_bonus
现在已经过时了;武器不再那样工作了。相反,我们希望能够为它们定义类似 D&D 的属性。这是一个现代化的匕首:
{
"name" : "Dagger",
"renderable": {
"glyph" : "/",
"fg" : "#FFAAAA",
"bg" : "#000000",
"order" : 2
},
"weapon" : {
"range" : "melee",
"attribute" : "Quickness",
"base_damage" : "1d4",
"hit_bonus" : 0
}
},
为了支持这一点,在 raws/item_structs.rs
中,我们更改 Weapon
结构体:
#![allow(unused)] fn main() { #[derive(Deserialize, Debug)] pub struct Weapon { pub range: String, pub attribute: String, pub base_damage: String, pub hit_bonus: i32 } }
现在打开 components.rs
,我们将更改 MeleePowerBonus
(并从 main.rs
和 saveload_system.rs
中重命名它)。我们将用 MeleeWeapon
替换它,它捕获了这些方面,但以更机器友好的格式(这样我们就不会一直解析字符串):
#![allow(unused)] fn main() { #[derive(PartialEq, Copy, Clone, Serialize, Deserialize)] pub enum WeaponAttribute { Might, Quickness } #[derive(Component, Serialize, Deserialize, Clone)] pub struct MeleeWeapon { pub attribute : WeaponAttribute, pub damage_n_dice : i32, pub damage_die_type : i32, pub damage_bonus : i32, pub hit_bonus : i32 } }
我们将 attribute 浓缩成一个 enum
(读取速度更快),并将 1d4+0
分解为以下含义:(1)damage_n_dice
,(4)damage_die_type
,加上 damage_bonus
。
我们还需要更改 raws/rawmaster.rs
中的 spawn_named_item
:
#![allow(unused)] fn main() { if let Some(weapon) = &item_template.weapon { eb = eb.with(Equippable{ slot: EquipmentSlot::Melee }); let (n_dice, die_type, bonus) = parse_dice_string(&weapon.base_damage); let mut wpn = MeleeWeapon{ attribute : WeaponAttribute::Might, damage_n_dice : n_dice, damage_die_type : die_type, damage_bonus : bonus, hit_bonus : weapon.hit_bonus }; match weapon.attribute.as_str() { "Quickness" => wpn.attribute = WeaponAttribute::Quickness, _ => wpn.attribute = WeaponAttribute::Might } eb = eb.with(wpn); } }
这应该足以读取我们更友好的武器格式,并使它们在游戏中使用。
从武器开始
如果您回到设计文档,我们声明您从一些最少的装备开始。我们将让您从您父亲的生锈的长剑开始。让我们将其添加到 spawns.json
文件中:
{
"name" : "Rusty Longsword",
"renderable": {
"glyph" : "/",
"fg" : "#BB77BB",
"bg" : "#000000",
"order" : 2
},
"weapon" : {
"range" : "melee",
"attribute" : "Might",
"base_damage" : "1d8-1",
"hit_bonus" : -1
}
},
我们稍微调暗了颜色(毕竟它是生锈的),并为剑添加了 -1
的惩罚(以说明其状况)。现在,我们希望玩家从它开始。目前在 spawners.rs
中,我们的 player
函数创建了玩家 - 仅此而已。我们希望他/她从一些初始装备开始。目前,我们只允许在地板上生成物品;那不行(您必须记住在开始时捡起它!) - 所以我们将扩展我们的原始文件生成系统来处理它(这就是为什么我们在 mod/rawmaster.rs
中有 SpawnType
枚举 - 即使它只有一个条目!)。让我们将 Equipped
和 Carried
添加到该枚举中:
#![allow(unused)] fn main() { pub enum SpawnType { AtPosition { x: i32, y: i32 }, Equipped { by: Entity }, Carried { by: Entity } } }
我们将需要一个函数来确定可装备物品应该放入哪个槽位。这将起到作用:
#![allow(unused)] fn main() { fn find_slot_for_equippable_item(tag : &str, raws: &RawMaster) -> EquipmentSlot { if !raws.item_index.contains_key(tag) { panic!("Trying to equip an unknown item: {}", tag); // 尝试装备未知物品 } let item_index = raws.item_index[tag]; let item = &raws.raws.items[item_index]; if let Some(_wpn) = &item.weapon { return EquipmentSlot::Melee; } else if let Some(wearable) = &item.wearable { return string_to_slot(&wearable.slot); } panic!("Trying to equip {}, but it has no slot tag.", tag); // 尝试装备 {}, 但它没有槽位标签。 } }
请注意,我们显式调用 panic!
来处理可能导致非常奇怪/意外的游戏行为的情况。现在您没有借口不小心处理您的原始文件条目了!它非常简单:它在索引中查找项目名称,并使用它来查找项目。如果是武器,它会从中派生槽位(目前始终为 Melee
)。如果是可穿戴物品,它会使用我们的 string_to_slot
函数从中计算出来。
我们还需要更新 spawn_position
函数来处理这个问题:
#![allow(unused)] fn main() { fn spawn_position<'a>(pos : SpawnType, new_entity : EntityBuilder<'a>, tag : &str, raws: &RawMaster) -> EntityBuilder<'a> { let eb = new_entity; // Spawn in the specified location match pos { SpawnType::AtPosition{x,y} => eb.with(Position{ x, y }), SpawnType::Carried{by} => eb.with(InBackpack{ owner: by }), SpawnType::Equipped{by} => { let slot = find_slot_for_equippable_item(tag, raws); eb.with(Equipped{ owner: by, slot }) } } } }
这里有一些值得注意的事情:
- 我们必须更改方法签名,因此您将必须修复对它的调用。它现在需要访问
raws
文件,以及您正在生成的项目的名称标签。 - 因为我们传递的是引用,而
EntityBuilder
实际上包含对 ECS 的引用,所以我们必须添加一些生命周期修饰,以告诉 Rust 返回的EntityBuilder
不依赖于标签或原始文件作为有效引用存在。所以我们命名一个生命周期a
- 并将其附加到函数名称(spawn_position<'a>
声明'a
是它使用的生命周期)。然后我们将<'a>
添加到共享该生命周期的类型。这足以提示以避免吓到生命周期检查器。 - 我们从
match
返回;AtPosition
和Carried
很简单;Equipped
使用了我们刚刚编写的标签查找器。
我们将不得不更改三行来使用新的函数签名。它们是相同的;找到 eb = spawn_position(pos, eb);
并替换为 eb = spawn_position(pos, eb, key, raws);
。
不幸的是,我在实现此功能时遇到了另一个问题。我们一直在将新实体 (ecs.create_entity()
) 传递到我们的 spawn_named_x
函数中。不幸的是,这将成为一个问题:我们开始需要实体生成来触发其他实体生成(例如,生成带有装备的 NPC - 下面 - 或生成带有内容的箱子)。让我们现在修复它,这样我们就不会在以后遇到问题。
我们将更改 spawn_named_item
的函数签名,并将 eb
的第一次使用更改为实际创建实体:
#![allow(unused)] fn main() { pub fn spawn_named_item(raws: &RawMaster, ecs : &mut World, key : &str, pos : SpawnType) -> Option<Entity> { if raws.item_index.contains_key(key) { let item_template = &raws.raws.items[raws.item_index[key]]; let mut eb = ecs.create_entity().marked::<SimpleMarker<SerializeMe>>(); ... }
我们将对 spawn_named_mob
执行相同的操作:
#![allow(unused)] fn main() { pub fn spawn_named_mob(raws: &RawMaster, ecs : &mut World, key : &str, pos : SpawnType) -> Option<Entity> { if raws.mob_index.contains_key(key) { let mob_template = &raws.raws.mobs[raws.mob_index[key]]; let mut eb = ecs.create_entity().marked::<SimpleMarker<SerializeMe>>(); ... }
再次对 spawn_named_prop
执行相同的操作:
#![allow(unused)] fn main() { pub fn spawn_named_prop(raws: &RawMaster, ecs : &mut World, key : &str, pos : SpawnType) -> Option<Entity> { if raws.prop_index.contains_key(key) { let prop_template = &raws.raws.props[raws.prop_index[key]]; let mut eb = ecs.create_entity().marked::<SimpleMarker<SerializeMe>>(); ... }
然后这要求我们更改 spawn_named_entity
的签名以及它传递的内容:
#![allow(unused)] fn main() { pub fn spawn_named_entity(raws: &RawMaster, ecs : &mut World, key : &str, pos : SpawnType) -> Option<Entity> { if raws.item_index.contains_key(key) { return spawn_named_item(raws, ecs, key, pos); } else if raws.mob_index.contains_key(key) { return spawn_named_mob(raws, ecs, key, pos); } else if raws.prop_index.contains_key(key) { return spawn_named_prop(raws, ecs, key, pos); } None } }
在 spawner.rs
中,我们需要更改 spawn_named_entity
的调用签名:
#![allow(unused)] fn main() { let spawn_result = spawn_named_entity(&RAWS.lock().unwrap(), ecs, &spawn.1, SpawnType::AtPosition{ x, y}); if spawn_result.is_some() { return; } }
如果您想知道为什么我们不能同时传递 ECS 和新实体,那是因为 Rust 的借用检查器。新实体实际上保留了对其父 ECS 的引用(因此当您调用 build
时,它们知道应该将它们插入哪个世界)。因此,如果您尝试发送 &mut World
和一个新实体 - 您会收到错误,因为您对同一对象有两个“借用”。这样做可能是安全的,但 Rust 无法证明这一点 - 所以它会警告您。这实际上防止了在 C/C++ 世界中经常发现的一整类错误,所以虽然这很痛苦 - 但这是为了我们自己好。
所以现在我们可以更新 spawners.rs
中的 player
函数,使其从生锈的长剑开始:
#![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); let player = 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(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) .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 }) .marked::<SimpleMarker<SerializeMe>>() .build(); // Starting equipment spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Rusty Longsword", SpawnType::Equipped{by : player}); player } }
这里的主要变化是我们将新实体放入名为 player
的变量中,然后再返回它。然后我们将其用作参数,用于谁持有我们通过 spawn_named_entity
生成的“生锈的长剑”。
如果您现在 cargo run
,您将从生锈的长剑开始。它不会 工作,但您拥有它:
让生锈的长剑造成一些伤害
我们在 melee_combat_system.rs
中留下了一些占位符。现在,是时候填补武器空白了。打开文件,我们首先添加一些我们需要的更多类型:
#![allow(unused)] fn main() { 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>, ReadStorage<'a, Equipped>, ReadStorage<'a, MeleeWeapon> ); 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, equipped_items, meleeweapons) = data; ... }
然后我们将添加一些代码来放入默认武器信息,然后在攻击者装备了某些东西时搜索替换:
#![allow(unused)] fn main() { let mut weapon_info = MeleeWeapon{ attribute : WeaponAttribute::Might, hit_bonus : 0, damage_n_dice : 1, damage_die_type : 4, damage_bonus : 0 }; for (wielded,melee) in (&equipped_items, &meleeweapons).join() { if wielded.owner == entity && wielded.slot == EquipmentSlot::Melee { weapon_info = melee.clone(); } } }
这使得替换代码中依赖于武器的部分非常容易:
#![allow(unused)] fn main() { let natural_roll = rng.roll_dice(1, 20); let attribute_hit_bonus = if weapon_info.attribute == WeaponAttribute::Might { attacker_attributes.might.bonus } else { attacker_attributes.quickness.bonus}; let skill_hit_bonus = skill_bonus(Skill::Melee, &*attacker_skills); let weapon_hit_bonus = weapon_info.hit_bonus; }
我们还可以换入武器的伤害信息:
#![allow(unused)] fn main() { let base_damage = rng.roll_dice(weapon_info.damage_n_dice, weapon_info.damage_die_type); let attr_damage_bonus = attacker_attributes.might.bonus; let skill_damage_bonus = skill_bonus(Skill::Melee, &*attacker_skills); let weapon_damage_bonus = weapon_info.damage_bonus; }
现在,如果您 cargo run
该项目,您可以使用您的剑将老鼠砍成碎片!
哦 - 嗯,那进展得不太顺利!我们造成了大量的伤害,但老鼠很快就压倒了我们 - 即使使用 1d4
的默认伤害和 力量 惩罚!正如您在录音中看到的那样,我试图撤退,打算治疗 - 并注意到我饿了(找到房子花了太长时间!)并且无法治疗。幸运的是,我们拥有添加一些食物到玩家背包所需的一切。设计文档声明您应该从一杯啤酒和一根干香肠开始。让我们将它们放入 spawns.json
中:
{
"name" : "Dried Sausage",
"renderable": {
"glyph" : "%",
"fg" : "#00FF00",
"bg" : "#000000",
"order" : 2
},
"consumable" : {
"effects" : {
"food" : ""
}
}
},
{
"name" : "Beer",
"renderable": {
"glyph" : "!",
"fg" : "#FF00FF",
"bg" : "#000000",
"order" : 2
},
"consumable" : {
"effects" : { "provides_healing" : "4" }
}
},
香肠是“口粮”的复制品 - 可以治愈饥饿。啤酒是一种超弱的治疗药水,但这足以击败啮齿动物的威胁!
让我们修改 spawner.rs
中的 player
以也将这些包含在玩家的背包中:
#![allow(unused)] fn main() { spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Dried Sausage", SpawnType::Carried{by : player} ); spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Beer", SpawnType::Carried{by : player}); }
现在玩家开始时拥有治疗选项和抗饥饿设备(通常称为食物)。
但是等等 - 我们是裸体的,只有香肠和一些啤酒?我不认为这是那种游戏?
游戏中没有人穿着任何东西。对于老鼠来说可能没问题,但我们并没有在这里为人类设想一个非常自由的社会。更重要的是,如果我们没有任何东西可以穿,我们也没有任何护甲等级奖励!
在 components.rs
中,我们将用 Wearable
替换 DefenseBonus
- 并将其充实一些。(不要忘记更改 main.rs
和 saveload_system.rs
中的组件):
#![allow(unused)] fn main() { #[derive(Component, Serialize, Deserialize, Clone)] pub struct Wearable { pub armor_class : f32 } }
这是一个非常简单的更改。让我们更新 raws/item_structs.rs
中的原始文件读取器,以反映我们想要的内容:
{
"name" : "Shield",
"renderable": {
"glyph" : "[",
"fg" : "#00AAFF",
"bg" : "#000000",
"order" : 2
},
"wearable" : {
"slot" : "Shield",
"armor_class" : 1.0
}
},
我们可能还想支持更多装备槽位!在 components.rs
中,我们应该更新 EquipmentSlot
以处理更多可能的位置:
#![allow(unused)] fn main() { #[derive(PartialEq, Copy, Clone, Serialize, Deserialize)] pub enum EquipmentSlot { Melee, Shield, Head, Torso, Legs, Feet, Hands } }
我们无疑会在稍后添加更多,但这涵盖了基础知识。我们需要更新 raws/item_structs.rs
以反映更改:
#![allow(unused)] fn main() { #[derive(Deserialize, Debug)] pub struct Item { pub name : String, pub renderable : Option<Renderable>, pub consumable : Option<Consumable>, pub weapon : Option<Weapon>, pub wearable : Option<Wearable> } ... #[derive(Deserialize, Debug)] pub struct Wearable { pub armor_class: f32, pub slot : String } }
我们将需要多次从字符串(在 JSON 中)转换为 EquipmentSlot
,因此我们将在 raws/rawmaster.rs
中添加一个函数来执行此操作:
#![allow(unused)] fn main() { pub fn string_to_slot(slot : &str) -> EquipmentSlot { match slot { "Shield" => EquipmentSlot::Shield, "Head" => EquipmentSlot::Head, "Torso" => EquipmentSlot::Torso, "Legs" => EquipmentSlot::Legs, "Feet" => EquipmentSlot::Feet, "Hands" => EquipmentSlot::Hands, "Melee" => EquipmentSlot::Melee, _ => { rltk::console::log(format!("Warning: unknown equipment slot type [{}]", slot)); EquipmentSlot::Melee } // 警告:未知的装备槽位类型 } } }
我们将要扩展 raws/rawmaster.rs
中的 spawn_named_item
代码以处理扩展的选项:
#![allow(unused)] fn main() { if let Some(wearable) = &item_template.wearable { let slot = string_to_slot(&wearable.slot); eb = eb.with(Equippable{ slot }); eb = eb.with(Wearable{ slot, armor_class: wearable.armor_class }); } }
让我们在 spawns.json
中制作更多物品,让玩家可以穿戴:
{
"name" : "Stained Tunic",
"renderable": {
"glyph" : "[",
"fg" : "#00FF00",
"bg" : "#000000",
"order" : 2
},
"wearable" : {
"slot" : "Torso",
"armor_class" : 0.1
}
},
{
"name" : "Torn Trousers",
"renderable": {
"glyph" : "[",
"fg" : "#00FFFF",
"bg" : "#000000",
"order" : 2
},
"wearable" : {
"slot" : "Legs",
"armor_class" : 0.1
}
},
{
"name" : "Old Boots",
"renderable": {
"glyph" : "[",
"fg" : "#FF9999",
"bg" : "#000000",
"order" : 2
},
"wearable" : {
"slot" : "Legs",
"armor_class" : 0.1
}
}
现在我们将打开 spawner.rs
并将这些物品添加到玩家:
#![allow(unused)] fn main() { spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Rusty Longsword", SpawnType::Equipped{by : player}); spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Dried Sausage", SpawnType::Carried{by : player} ); spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Beer", SpawnType::Carried{by : player}); spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Stained Tunic", SpawnType::Equipped{by : player}); spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Torn Trousers", SpawnType::Equipped{by : player}); spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Old Boots", SpawnType::Equipped{by : player}); }
如果您现在运行游戏,检查您的背包并移除物品将显示您已正确开始 - 穿着您那件染色的衬衫、破烂的裤子、旧靴子、啤酒、香肠和一把生锈的剑。
我们还有一件关于可穿戴物品的事情要做;melee_system.rs
需要知道如何计算护甲等级。幸运的是,这非常容易:
#![allow(unused)] fn main() { let mut armor_item_bonus_f = 0.0; for (wielded,armor) in (&equipped_items, &wearables).join() { if wielded.owner == wants_melee.target { armor_item_bonus_f += armor.armor_class; } } 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 = armor_item_bonus_f as i32; }
我们遍历装备的盔甲,并将防御者佩戴的每件物品的奖励加在一起。然后我们将数字截断为整数。
那么我们为什么 要 使用浮点数呢?经典的 D&D 将护甲值分配给整套护甲。因此,在第 5 版中,皮甲的 AC 为 11(加上敏捷)。在我们的游戏中,您可以单独穿戴皮甲的各个部件 - 因此我们给它们一个相当于整套所需 AC 一部分 的值。然后我们将它们加在一起,以处理零散的盔甲(例如,您找到了一件漂亮的胸甲,但只有皮革护腿)。
好的,所以我穿着衣服 - 为什么其他人不穿呢?
我们已经实现了足够的衣服和武器,我们可以开始将它们交给 NPC。由于酒保是我们最喜欢的测试对象,让我们用我们 希望 生成物品的方式来装饰他的条目:
{
"name" : "Barkeep",
"renderable": {
"glyph" : "☺",
"fg" : "#EE82EE",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"vision_range" : 4,
"ai" : "vendor",
"attributes" : {
"intelligence" : 13
},
"skills" : {
"Melee" : 2
},
"equipped" : [ "Cudgel", "Cloth Tunic", "Cloth Pants", "Slippers" ]
},
很简单:我们添加了一个名为 equipped
的数组,并列出了我们希望酒保穿戴的所有物品。当然,我们现在必须 编写 这些物品。
{
"name" : "Cudgel",
"renderable": {
"glyph" : "/",
"fg" : "#A52A2A",
"bg" : "#000000",
"order" : 2
},
"weapon" : {
"range" : "melee",
"attribute" : "Quickness",
"base_damage" : "1d4",
"hit_bonus" : 0
}
},
{
"name" : "Cloth Tunic",
"renderable": {
"glyph" : "[",
"fg" : "#00FF00",
"bg" : "#000000",
"order" : 2
},
"wearable" : {
"slot" : "Torso",
"armor_class" : 0.1
}
},
{
"name" : "Cloth Pants",
"renderable": {
"glyph" : "[",
"fg" : "#00FFFF",
"bg" : "#000000",
"order" : 2
},
"wearable" : {
"slot" : "Legs",
"armor_class" : 0.1
}
},
{
"name" : "Slippers",
"renderable": {
"glyph" : "[",
"fg" : "#FF9999",
"bg" : "#000000",
"order" : 2
},
"wearable" : {
"slot" : "Legs",
"armor_class" : 0.1
}
}
那里没有什么 新 的东西,只是数据录入。我们需要修改我们的 raws/mob_structs.rs
文件,以适应为 NPC 提供装备:
#![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>, pub equipped : Option<Vec<String>> } }
再次,很简单 - 我们可选地为 mob 提供一个字符串列表(表示物品名称标签)。所以我们必须修改 raws/rawmaster.rs
中的 spawn_named_mob
来处理这个问题。我们将 Some(eb.build())
替换为:
#![allow(unused)] fn main() { let new_mob = eb.build(); // Are they wielding anyting? if let Some(wielding) = &mob_template.equipped { for tag in wielding.iter() { spawn_named_entity(raws, ecs, tag, SpawnType::Equipped{ by: new_mob }); } } return Some(new_mob); }
这创建了新的 mob,并将实体存储为 new_mob
。然后它查看 mob 模板中是否有 equipped
字段;如果有,它会遍历它,生成每个物品作为 mob 的装备。
如果您现在 cargo run
,您会发现您穿着衣服,挥舞着生锈的剑,并且拥有您的啤酒和香肠。
装饰您的 NPC
我不会在这里粘贴完整的 spawns.json
文件,但如果您 查看源代码,您会发现我为我们的基本 NPC 添加了服装。目前,它们都与酒保相同 - 希望我们将来会记得调整这些!
那么自然攻击和防御呢?
所以 NPC 们穿着得体并配备了装备,这给了他们战斗属性和护甲等级。但是我们的老鼠呢?老鼠通常不穿可爱的小老鼠套装和携带武器(如果您有这样做