魔法物品与物品辨识
关于本教程
本教程是免费且开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用。我希望您会喜欢本教程,并制作出精彩的游戏!
如果您喜欢这个教程并希望我继续写作,请考虑支持我的 Patreon。
魔法物品是 D&D 和 roguelike 游戏的中流砥柱。从不起眼的 “+1 剑” 到强大的 “圣复仇者” - 再到 “诅咒反噬者” - 物品帮助定义了这个游戏类型。在 roguelike 游戏中,不自动知晓物品是什么也是一种传统;你找到一把 “未鉴定的长剑”,在找到鉴定它的方法之前,你不知道它有什么用(或者是否被诅咒)。你找到一张 “猫咪在键盘上走过 卷轴”(无法发音的名字似乎是其特色!),在你鉴定或阅读它之前 - 你不知道会发生什么。有些游戏将此变成完整的元游戏 - 通过赌频率、供应商价格和类似信息来给你关于你刚刚找到的东西的线索。即使是 暗黑破坏神,最主流的 roguelike 游戏(即使它变成了实时制!)也保留了这个游戏特色 - 但往往会使鉴定卷轴非常丰富(还有乐于助人的苏格兰老人们)。
魔法物品的类别
在现代游戏中,区分魔法物品为 魔法的、稀有的 或 传说的(以及物品套装,我们暂且不讨论)。这些通常通过颜色来区分,因此您可以一目了然地判断物品是否值得考虑。这也提供了一个机会来表示某物 是 魔法物品 - 因此我们将打开 components.rs
(并在 main.rs
和 saveload_system.rs
中注册)并创建 MagicItem
:
#![allow(unused)] fn main() { #[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)] pub enum MagicItemClass { Common, Rare, Legendary } #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct MagicItem { pub class : MagicItemClass } }
下一步是让物品被标记为魔法物品,并拥有这些类别之一。将以下内容添加到 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>, pub initiative_penalty : Option<f32>, pub weight_lbs : Option<f32>, pub base_value : Option<f32>, pub vendor_category : Option<String>, pub magic : Option<MagicItem> } #[derive(Deserialize, Debug)] pub struct MagicItem { pub class: String } }
为什么我们在这里使用完整的结构体,而不是仅仅一个字符串?当我们开始充实魔法物品时,我们稍后将在本章中想要指定更多信息。
现在你可以在 spawns.json
中装饰物品,例如:
{
"name" : "Health Potion",
"renderable": {
"glyph" : "!",
"fg" : "#FF00FF",
"bg" : "#000000",
"order" : 2
},
"consumable" : {
"effects" : { "provides_healing" : "8" }
},
"weight_lbs" : 0.5,
"base_value" : 50.0,
"vendor_category" : "alchemy",
"magic" : { "class" : "common" }
},
我已经将 common
魔法标签添加到 JSON 列表中的魔法卷轴和药剂中,详情请参阅源代码 - 目前这非常简单。接下来,我们需要修改 raws/rawmaster.rs
中的 spawn_named_item
以应用适当的组件标签:
#![allow(unused)] fn main() { if let Some(magic) = &item_template.magic { let class = match magic.class.as_str() { "rare" => MagicItemClass::Rare, "legendary" => MagicItemClass::Legendary, _ => MagicItemClass::Common }; eb = eb.with(MagicItem{ class }); } }
现在我们有了这些数据,我们需要 使用 它。目前,我们只想设置物品名称在 GUI 中出现时的显示 颜色 - 以更好地了解魔法物品的价值(就像所有那些 MMO 游戏一样!)。在 gui.rs
中,我们将为此目的创建一个通用函数:
#![allow(unused)] fn main() { pub fn get_item_color(ecs : &World, item : Entity) -> RGB { if let Some(magic) = ecs.read_storage::<MagicItem>().get(item) { match magic.class { MagicItemClass::Common => return RGB::from_f32(0.5, 1.0, 0.5), MagicItemClass::Rare => return RGB::from_f32(0.0, 1.0, 1.0), MagicItemClass::Legendary => return RGB::from_f32(0.71, 0.15, 0.93) } } RGB::from_f32(1.0, 1.0, 1.0) } }
现在我们需要遍历 gui.rs
中所有显示物品名称的函数,并将硬编码的颜色替换为对这个函数的调用。在 draw_ui
中(gui.rs
的第 121 行),稍微展开一下已装备列表:
#![allow(unused)] fn main() { // Equipped let mut y = 13; let entities = ecs.entities(); let equipped = ecs.read_storage::<Equipped>(); let name = ecs.read_storage::<Name>(); for (entity, equipped_by, item_name) in (&entities, &equipped, &name).join() { if equipped_by.owner == *player_entity { ctx.print_color(50, y, get_item_color(ecs, entity), black, &item_name.name); y += 1; } } }
消耗品部分也进行相同的更改:
#![allow(unused)] fn main() { ctx.print_color(53, y, get_item_color(ecs, entity), black, &item_name.name); }
我们将暂时不修改工具提示,改进它们(和日志)是(目前假设的)未来章节的主题。在 show_inventory
(大约第 321 行)、drop_item_menu
(大约第 373 行)、remove_item_menu
(大约第 417 行)和 vendor_sell_menu
(大约第 660 行):
#![allow(unused)] fn main() { ctx.print_color(21, y, get_item_color(&gs.ecs, entity), RGB::from_f32(0.0, 0.0, 0.0), &name.name.to_string()); }
请注意:一旦我们添加物品辨识,这些行将 再次 更改!
完成这些后,如果您 cargo run
,您将看到您的 城镇传送卷轴
现在已被很好地突出显示为普通魔法物品:
辨识:卷轴
在 Roguelike 游戏中,当你不知道药剂的作用时,药剂具有完全无法发音的名称是很常见的。据推测,这代表了某种喉音,可以触发魔法效果(并且围绕此构建一个巨大的语法会非常有趣,但本教程会变得更大!)。因此,一张 Lorem Ipsum 卷轴 可能是游戏中的 任何 卷轴,这取决于您决定通过使用它来辨识(一种赌博,它可能根本不是您想要的!),对其进行鉴定,或者只是忽略它,因为您不喜欢这种风险。
让我们从打开 spawner.rs
开始,转到 player
函数并删除赠送免费 城镇传送
的行。它太慷慨了,并且意味着你一开始就知道它是什么!
所以这里是有趣的部分:如果我们只是简单地为卷轴分配一个未鉴定的名称,玩家可以简单地记住这些名称 - 并且辨识将只不过是一个记忆游戏。因此,我们需要在 游戏开始时 分配名称(而不是在加载原始文件时,因为您可能在每个会话中玩多次)。让我们从 raws/item_structs.rs
开始,并在 MagicItem
中添加另一个字段,指示 “这是一个卷轴,应该使用卷轴命名。”
#![allow(unused)] fn main() { #[derive(Deserialize, Debug)] pub struct MagicItem { pub class: String, pub naming: String } }
现在我们必须遍历 spawns.json
并在我们的 “magic” 条目中添加命名标签。我为命名卷轴选择了 “scroll”(并将其他名称留空)。例如,这是魔法飞弹卷轴:
{
"name" : "Magic Missile Scroll",
"renderable": {
"glyph" : ")",
"fg" : "#00FFFF",
"bg" : "#000000",
"order" : 2
},
"consumable" : {
"effects" : {
"ranged" : "6",
"damage" : "20"
}
},
"weight_lbs" : 0.5,
"base_value" : 50.0,
"vendor_category" : "alchemy",
"magic" : { "class" : "common", "naming" : "scroll" }
},
我们已经有一个贯穿整个游戏(但仍然是全局资源)的结构,并且在每次我们更改关卡时都会重置:MasterDungeonMap
。使用它来存储关于整个游戏的状态是有意义的,因为它已经是地下城主了!我们也在序列化它,这很有帮助!因此我们将打开 map/dungeon.rs
并添加几个结构:
#![allow(unused)] fn main() { #[derive(Default, Serialize, Deserialize, Clone)] pub struct MasterDungeonMap { maps : HashMap<i32, Map>, identified_items : HashSet<String>, scroll_mappings : HashMap<String, String> } }
我们还必须更新构造函数以提供空值(目前):
#![allow(unused)] fn main() { impl MasterDungeonMap { pub fn new() -> MasterDungeonMap { MasterDungeonMap{ maps: HashMap::new() , pub identified_items : HashSet::new(), pub scroll_mappings : HashMap::new() } } }
这个想法是,当物品被鉴定时,我们将把它的名称标签放入 identified_items
中,从而提供一种快速的方法来判断物品是否已被鉴定。scroll_mappings
旨在将卷轴的实际名称与随机名称映射。这些将持续整个游戏会话期间(并自动包含在保存游戏中!)。为了填充卷轴映射,我们需要一种方法来获取原始文件中标记为卷轴的物品名称。因此,在 raws/rawmaster.rs
中,我们将创建一个新函数:
#![allow(unused)] fn main() { pub fn get_scroll_tags() -> Vec<String> { let raws = &super::RAWS.lock().unwrap(); let mut result = Vec::new(); for item in raws.raws.items.iter() { if let Some(magic) = &item.magic { if &magic.naming == "scroll" { result.push(item.name.clone()); } } } result } }
这获取了对全局 raws
的访问权限,遍历所有物品以查找具有 scroll
命名约定的魔法物品,并将名称作为字符串向量返回。我们不会经常这样做,因此我们不会尝试在性能方面做得聪明(克隆所有这些字符串有点慢)。因此,现在在 map/dungeon.rs
中,我们进一步扩展构造函数以进行卷轴名称映射:
#![allow(unused)] fn main() { impl MasterDungeonMap { pub fn new() -> MasterDungeonMap { let mut dm = MasterDungeonMap{ maps: HashMap::new() , identified_items : HashSet::new(), scroll_mappings : HashMap::new() }; let mut rng = rltk::RandomNumberGenerator::new(); for scroll_tag in crate::raws::get_scroll_tags().iter() { let masked_name = make_scroll_name(&mut rng); dm.scroll_mappings.insert(scroll_tag.to_string(), masked_name); } dm } }
这引用了一个新函数 make_scroll_name
,它看起来像这样:
#![allow(unused)] fn main() { fn make_scroll_name(rng: &mut rltk::RandomNumberGenerator) -> String { let length = 4 + rng.roll_dice(1, 4); let mut name = "Scroll of ".to_string(); for i in 0..length { if i % 2 == 0 { name += match rng.roll_dice(1, 5) { 1 => "a", 2 => "e", 3 => "i", 4 => "o", _ => "u" } } else { name += match rng.roll_dice(1, 21) { 1 => "b", 2 => "c", 3 => "d", 4 => "f", 5 => "g", 6 => "h", 7 => "j", 8 => "k", 9 => "l", 10 => "m", 11 => "n", 12 => "p", 13 => "q", 14 => "r", 15 => "s", 16 => "t", 17 => "v", 18 => "w", 19 => "x", 20 => "y", _ => "z" } } } name } }
此函数以词干 “Scroll of ” 开头,然后添加随机字母。每隔一个字母是元音,中间是辅音。这会让你得到无意义的词,但它是 可发音的 无意义词,例如 iladi
或 omuruxo
。它没有提供关于底层卷轴性质的任何线索。
接下来,我们需要一种方法来表示实体 具有 混淆的名称。我们将为此任务创建一个新组件,因此在 components.rs
中(并在 main.rs
和 saveload_system.rs
中注册)我们添加:
#![allow(unused)] fn main() { #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct ObfuscatedName { pub name : String } }
当我们生成物品时,我们也需要添加这些标签,因此在 raws/rawmaster.rs
中,我们将以下内容添加到 spawn_named_item
中。首先,在顶部,我们复制名称映射(以避免借用问题):
#![allow(unused)] fn main() { let item_template = &raws.raws.items[raws.item_index[key]]; let scroll_names = ecs.fetch::<crate::map::MasterDungeonMap>().scroll_mappings.clone(); let mut eb = ecs.create_entity().marked::<SimpleMarker<SerializeMe>>(); }
然后我们扩展 magic
处理程序:
#![allow(unused)] fn main() { if let Some(magic) = &item_template.magic { let class = match magic.class.as_str() { "rare" => MagicItemClass::Rare, "legendary" => MagicItemClass::Legendary, _ => MagicItemClass::Common }; eb = eb.with(MagicItem{ class }); #[allow(clippy::single_match)] // 为了阻止 Clippy 在我们添加更多之前抱怨 match magic.naming.as_str() { "scroll" => { eb = eb.with(ObfuscatedName{ name : scroll_names[&item_template.name].clone() }); } _ => {} } } }
现在,我们回到 gui.rs
并创建一个新函数来获取物品的显示名称:
#![allow(unused)] fn main() { pub fn get_item_display_name(ecs: &World, item : Entity) -> String { if let Some(name) = ecs.read_storage::<Name>().get(item) { if ecs.read_storage::<MagicItem>().get(item).is_some() { let dm = ecs.fetch::<crate::map::MasterDungeonMap>(); if dm.identified_items.contains(&name.name) { name.name.clone() } else if let Some(obfuscated) = ecs.read_storage::<ObfuscatedName>().get(item) { obfuscated.name.clone() } else { "Unidentified magic item".to_string() } } else { name.name.clone() } } else { "Nameless item (bug)".to_string() } } }
再一次,我们需要遍历 gui.rs
中所有引用物品名称的地方,并将它们更改为使用此函数。在 draw_ui
中,这实际上缩短了一些代码,因为我们不再需要实际的 Name
组件:
#![allow(unused)] fn main() { // Equipped let mut y = 13; let entities = ecs.entities(); let equipped = ecs.read_storage::<Equipped>(); for (entity, equipped_by) in (&entities, &equipped).join() { if equipped_by.owner == *player_entity { ctx.print_color(50, y, get_item_color(ecs, entity), black, &get_item_display_name(ecs, entity)); y += 1; } } // Consumables y += 1; let yellow = RGB::named(rltk::YELLOW); let consumables = ecs.read_storage::<Consumable>(); let backpack = ecs.read_storage::<InBackpack>(); let mut index = 1; for (entity, carried_by, _consumable) in (&entities, &backpack, &consumables).join() { if carried_by.owner == *player_entity && index < 10 { ctx.print_color(50, y, yellow, black, &format!("↑{}", index)); ctx.print_color(53, y, get_item_color(ecs, entity), black, &get_item_display_name(ecs, entity)); y += 1; index += 1; } } }
再一次,我们将稍后担心工具提示(尽管我们稍后会调整它们,使其不会泄露对象的实际身份!)。我们之前更改的其他项更改为 ctx.print_color(21, y, get_item_color(&gs.ecs, entity), RGB::from_f32(0.0, 0.0, 0.0), &get_item_display_name(&gs.ecs, entity));
,也可以删除一些 name
组件。
完成这些之后,如果您 cargo run
该项目,您找到的卷轴将显示混淆的名称:
鉴定混淆的卷轴
现在我们正确地隐藏了它们,让我们引入一种鉴定卷轴的机制。最明显的是,如果您 使用 卷轴,它应该被鉴定 - 并且该卷轴类型的所有现有/未来实例都将被鉴定。identified_items
列表处理未来,但我们将不得不做一些额外的工作来处理现有卷轴。我们将会有相当多的潜在鉴定发生 - 当您使用鉴定魔法(最终),当您使用或装备物品,当您购买物品时(因为您知道您正在购买魔法地图卷轴,您对其进行鉴定是有道理的) - 并且随着我们的进展,可能会有更多。
我们将使用一个新组件来处理这个问题,以指示物品可能已被鉴定,并使用一个系统来处理数据。首先,在 components.rs
中(并在 main.rs
和 saveload_system.rs
中注册):
#![allow(unused)] fn main() { #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct IdentifiedItem { pub name : String } }
现在我们将转到我们已经拥有的可以鉴定物品的各种位置,并在玩家使用物品时将此组件附加到玩家。首先,扩展 inventory_system.rs
以能够写入适当的存储:
#![allow(unused)] fn main() { impl<'a> System<'a> for ItemUseSystem { #[allow(clippy::type_complexity)] type SystemData = ( ReadExpect<'a, Entity>, WriteExpect<'a, GameLog>, WriteExpect<'a, Map>, Entities<'a>, WriteStorage<'a, WantsToUseItem>, ReadStorage<'a, Name>, ReadStorage<'a, Consumable>, ReadStorage<'a, ProvidesHealing>, ReadStorage<'a, InflictsDamage>, WriteStorage<'a, Pools>, WriteStorage<'a, SufferDamage>, ReadStorage<'a, AreaOfEffect>, WriteStorage<'a, Confusion>, ReadStorage<'a, Equippable>, WriteStorage<'a, Equipped>, WriteStorage<'a, InBackpack>, WriteExpect<'a, ParticleBuilder>, ReadStorage<'a, Position>, ReadStorage<'a, ProvidesFood>, WriteStorage<'a, HungerClock>, ReadStorage<'a, MagicMapper>, WriteExpect<'a, RunState>, WriteStorage<'a, EquipmentChanged>, ReadStorage<'a, TownPortal>, WriteStorage<'a, IdentifiedItem> ); #[allow(clippy::cognitive_complexity)] fn run(&mut self, data : Self::SystemData) { let (player_entity, mut gamelog, map, entities, mut wants_use, names, consumables, healing, inflict_damage, mut combat_stats, mut suffer_damage, aoe, mut confused, equippable, mut equipped, mut backpack, mut particle_builder, positions, provides_food, mut hunger_clocks, magic_mapper, mut runstate, mut dirty, town_portal, mut identified_item) = data; ... }
然后,在定位之后(第 113 行):
#![allow(unused)] fn main() { // Identify if entity == *player_entity { identified_item.insert(entity, IdentifiedItem{ name: names.get(useitem.item).unwrap().name.clone() }) .expect("Unable to insert"); } }
此外,在 main.rs
中,当我们处理生成购买的物品时,我们也应该鉴定这些物品:
#![allow(unused)] fn main() { gui::VendorResult::Buy => { let tag = result.2.unwrap(); let price = result.3.unwrap(); let mut pools = self.ecs.write_storage::<Pools>(); let player_entity = self.ecs.fetch::<Entity>(); let mut identified = self.ecs.write_storage::<IdentifiedItem>(); identified.insert(*player_entity, IdentifiedItem{ name : tag.clone() }).expect("Unable to insert"); std::mem::drop(identified); let player_pools = pools.get_mut(*player_entity).unwrap(); std::mem::drop(player_entity); if player_pools.gold >= price { player_pools.gold -= price; std::mem::drop(pools); let player_entity = *self.ecs.fetch::<Entity>(); crate::raws::spawn_named_item(&RAWS.lock().unwrap(), &mut self.ecs, &tag, SpawnType::Carried{ by: player_entity }); } } }
现在我们正在添加组件,我们需要读取它们并利用这些知识做些事情!
我们需要在 raws/rawmaster.rs
中再添加一个辅助函数来帮助这个过程:
#![allow(unused)] fn main() { pub fn is_tag_magic(tag : &str) -> bool { let raws = &super::RAWS.lock().unwrap(); if raws.item_index.contains_key(tag) { let item_template = &raws.raws.items[raws.item_index[tag]]; item_template.magic.is_some() } else { false } } }
由于鉴定物品纯粹是一个物品栏问题,我们将在已经很大的 inventory_system.rs
中添加另一个系统(注意到有一天我们会把它变成一个模块的暗示了吗?):
#![allow(unused)] fn main() { pub struct ItemIdentificationSystem {} impl<'a> System<'a> for ItemIdentificationSystem { #[allow(clippy::type_complexity)] type SystemData = ( ReadStorage<'a, crate::components::Player>, WriteStorage<'a, IdentifiedItem>, WriteExpect<'a, crate::map::MasterDungeonMap>, ReadStorage<'a, Item>, ReadStorage<'a, Name>, WriteStorage<'a, ObfuscatedName>, Entities<'a> ); fn run(&mut self, data : Self::SystemData) { let (player, mut identified, mut dm, items, names, mut obfuscated_names, entities) = data; for (_p, id) in (&player, &identified).join() { if !dm.identified_items.contains(&id.name) && crate::raws::is_tag_magic(&id.name) { dm.identified_items.insert(id.name.clone()); for (entity, _item, name) in (&entities, &items, &names).join() { if name.name == id.name { obfuscated_names.remove(entity); } } } } // Clean up identified.clear(); } } }
我们还需要修改 raws/rawmaster.rs
中的 spawn_named_item
,使其不混淆我们已经识别的物品的名称。我们将首先获取已鉴定物品列表:
#![allow(unused)] fn main() { let dm = ecs.fetch::<crate::map::MasterDungeonMap>(); let scroll_names = dm.scroll_mappings.clone(); let identified = dm.identified_items.clone(); std::mem::drop(dm); }
然后我们将使名称混淆取决于现在是否知道物品是什么:
#![allow(unused)] fn main() { if !identified.contains(&item_template.name) { #[allow(clippy::single_match)] // 为了阻止 Clippy 在我们添加更多之前抱怨 match magic.naming.as_str() { "scroll" => { eb = eb.with(ObfuscatedName{ name : scroll_names[&item_template.name].clone() }); } _ => {} } } }
最后,我们将其添加到 main.rs
中的 run_systems
:
#![allow(unused)] fn main() { let mut item_id = inventory_system::ItemIdentificationSystem{}; item_id.run_now(&self.ecs); }
因此,如果您现在 cargo run
,您将能够通过使用或购买物品来鉴定物品。
混淆药剂
我们可以为药剂使用非常相似的设置,但我们需要考虑如何命名它们。通常,药剂将一些形容词与药剂一词结合起来:“粘稠的黑色药剂”、“漩涡状的绿色药剂” 等。幸运的是,我们现在已经构建了很多基础设施框架 - 因此这只是插入细节的问题。
我们将从打开 spawns.json
开始,并使用命名约定 “potion” 注释我们的生命药剂:
"items" : [
{
"name" : "Health Potion",
"renderable": {
"glyph" : "!",
"fg" : "#FF00FF",
"bg" : "#000000",
"order" : 2
},
"consumable" : {
"effects" : { "provides_healing" : "8" }
},
"weight_lbs" : 0.5,
"base_value" : 50.0,
"vendor_category" : "alchemy",
"magic" : { "class" : "common", "naming" : "potion" }
},
...
然后,在 raws/rawmaster.rs
中,我们将重复 get_scroll_tags
功能 - 但用于药剂。我们想要检索所有具有 “potion” 命名方案的物品 - 我们需要它来生成药剂名称。这是新函数:
#![allow(unused)] fn main() { pub fn get_potion_tags() -> Vec<String> { let raws = &super::RAWS.lock().unwrap(); let mut result = Vec::new(); for item in raws.raws.items.iter() { if let Some(magic) = &item.magic { if &magic.naming == "potion" { result.push(item.name.clone()); } } } result } }
现在我们将重新访问 map/dungeon.rs
并访问 MasterDungeonMap
。我们需要添加一个用于存储药剂名称的结构(并将其添加到构造函数)。它将像卷轴映射一样:
#![allow(unused)] fn main() { #[derive(Default, Serialize, Deserialize, Clone)] pub struct MasterDungeonMap { maps : HashMap<i32, Map>, pub identified_items : HashSet<String>, pub scroll_mappings : HashMap<String, String>, pub potion_mappings : HashMap<String, String> } impl MasterDungeonMap { pub fn new() -> MasterDungeonMap { let mut dm = MasterDungeonMap{ maps: HashMap::new() , identified_items : HashSet::new(), scroll_mappings : HashMap::new(), potion_mappings : HashMap::new() }; ... }
现在,在 make_scroll_name
函数下面,我们将定义一些字符串常量数组。这些代表药剂的可用描述符;您可以(并且应该!)添加/编辑这些内容以适应您想要制作的游戏:
#![allow(unused)] fn main() { const POTION_COLORS: &[&str] = &["Red", "Orange", "Yellow", "Green", "Brown", "Indigo", "Violet"]; const POTION_ADJECTIVES : &[&str] = &["Swirling", "Effervescent", "Slimey", "Oiley", "Viscous", "Smelly", "Glowing"]; }
我们还需要一个函数来组合这些名称,包括重复检查(以确保我们永远不会有两种药剂类型具有相同的名称):
#![allow(unused)] fn main() { fn make_potion_name(rng: &mut rltk::RandomNumberGenerator, used_names : &mut HashSet<String>) -> String { loop { let mut name : String = POTION_ADJECTIVES[rng.roll_dice(1, POTION_ADJECTIVES.len() as i32) as usize -1].to_string(); name += " "; name += POTION_COLORS[rng.roll_dice(1, POTION_COLORS.len() as i32) as usize -1]; name += " Potion"; if !used_names.contains(&name) { used_names.insert(name.clone()); return name; } } } }
然后在 MasterDungeonMap
构造函数中,我们重复卷轴逻辑 - 但使用我们的新命名方案,以及 HashSet
以避免重复名称:
#![allow(unused)] fn main() { let mut used_potion_names : HashSet<String> = HashSet::new(); for potion_tag in crate::raws::get_potion_tags().iter() { let masked_name = make_potion_name(&mut rng, &mut used_potion_names); dm.potion_mappings.insert(potion_tag.to_string(), masked_name); } }
这为我们提供了一组不错的随机名称;在我刚刚运行的测试中,Health Potion
的混淆名称是 Slimey Violet Potion
。听起来不好吃!
我们最后需要做的是向药剂生成添加 ObfuscatedName
组件。在 raws/rawmaster.rs
中,我们已经为卷轴执行了此操作 - 因此我们复制药剂的功能:
#![allow(unused)] fn main() { let scroll_names = dm.scroll_mappings.clone(); let potion_names = dm.potion_mappings.clone(); ... if !identified.contains(&item_template.name) { match magic.naming.as_str() { "scroll" => { eb = eb.with(ObfuscatedName{ name : scroll_names[&item_template.name].clone() }); } "potion" => { eb = eb.with(ObfuscatedName{ name: potion_names[&item_template.name].clone() }); } _ => {} } } }
我们已经完成了所有剩余的艰苦工作!所以现在,如果您 cargo run
,四处走走并找到药剂,您会发现它有一个混淆的名称:
其他魔法物品
我们也应该支持其他魔法物品,而无需特殊的命名方案。让我们打开 spawns.json
并定义一个魔法 +1 长剑,并在命名方案中给它一个通用名称:
{
"name" : "Longsword +1",
"renderable": {
"glyph" : "/",
"fg" : "#FFAAFF",
"bg" : "#000000",
"order" : 2
},
"weapon" : {
"range" : "melee",
"attribute" : "might",
"base_damage" : "1d8+1",
"hit_bonus" : 1
},
"weight_lbs" : 2.0,
"base_value" : 100.0,
"initiative_penalty" : 1,
"vendor_category" : "weapon",
"magic" : { "class" : "common", "naming" : "Unidentified Longsword" }
},
看看我们是如何调整统计数据以反映其魔法状态的?它更频繁地击中,造成更多伤害,重量更轻,价值更高,并且先攻惩罚更少。这将是一个不错的发现!我们也应该将其添加到生成表中;我们现在给它一个非常高的出现可能性,以便我们可以测试它:
{ "name" : "Longsword +1", "weight" : 100, "min_depth" : 3, "max_depth" : 100 },
我们没有生成任何新名称,因此无需在 dungeon.rs
中构建命名系统(除非您想这样做 - 制作自己的游戏总是比我的好!) - 因此我们将直接跳到 raws/rawmaster.rs
中的 spawn_named_items
,并扩展魔法物品代码以在未提供名称时包含指定的名称:
#![allow(unused)] fn main() { if !identified.contains(&item_template.name) { match magic.naming.as_str() { "scroll" => { eb = eb.with(ObfuscatedName{ name : scroll_names[&item_template.name].clone() }); } "potion" => { eb = eb.with(ObfuscatedName{ name: potion_names[&item_template.name].clone() }); } _ => { eb = eb.with(ObfuscatedName{ name : magic.naming.clone() }); } } } }
如果您现在 cargo run
,并冲到 3 级(我使用作弊码,反斜杠
是您的朋友),您 很可能 找到一把魔法长剑:
清理
我们应该将魔法长剑的生成权重改回合理的值,并使普通长剑更频繁地出现。在 spawns.json
中:
{ "name" : "Longsword", "weight" : 2, "min_depth" : 3, "max_depth" : 100 },
{ "name" : "Longsword +1", "weight" : 1, "min_depth" : 3, "max_depth" : 100 },
此外,如果您在 spawner.rs
中为角色提供了任何免费物品,请继续删除它们(除非您希望它们无处不在)!
工具提示
我们仍然有一个问题需要处理。如果您将鼠标悬停在物品上,它会显示其真实名称 - 而不是其混淆的名称。打开 gui.rs
,我们将修复它。幸运的是,现在我们已经构建了名称框架,这非常容易!我们可以从 ECS 结构中完全删除名称,只需将实体和 ECS 传递给 get_item_display_name
以获取实体的名称:
#![allow(unused)] fn main() { fn draw_tooltips(ecs: &World, ctx : &mut Rltk) { use rltk::to_cp437; let (min_x, _max_x, min_y, _max_y) = camera::get_screen_bounds(ecs, ctx); let map = ecs.fetch::<Map>(); let positions = ecs.read_storage::<Position>(); let hidden = ecs.read_storage::<Hidden>(); let attributes = ecs.read_storage::<Attributes>(); let pools = ecs.read_storage::<Pools>(); let entities = ecs.entities(); let mouse_pos = ctx.mouse_pos(); let mut mouse_map_pos = mouse_pos; mouse_map_pos.0 += min_x - 1; mouse_map_pos.1 += min_y - 1; if mouse_pos.0 < 1 || mouse_pos.0 > 49 || mouse_pos.1 < 1 || mouse_pos.1 > 40 { return; } if mouse_map_pos.0 >= map.width-1 || mouse_map_pos.1 >= map.height-1 || mouse_map_pos.0 < 1 || mouse_map_pos.1 < 1 { return; } if !map.visible_tiles[map.xy_idx(mouse_map_pos.0, mouse_map_pos.1)] { return; } let mut tip_boxes : Vec<Tooltip> = Vec::new(); for (entity, position, _hidden) in (&entities, &positions, !&hidden).join() { if position.x == mouse_map_pos.0 && position.y == mouse_map_pos.1 { let mut tip = Tooltip::new(); tip.add(get_item_display_name(ecs, entity)); ... }
如果您现在将鼠标悬停在事物上,您将看到混淆的名称。如果您想将间谍融入您的游戏中,您甚至可以使用它来混淆 NPC 名称!
通过日志泄露信息
还有另一个明显的问题:如果您在拾取或掉落物品时观看日志,它会显示物品的真实名称!问题都发生在 inventory_system.rs
中,因此我们将从 gui.rs
中取出我们的 “ECS 外部” 函数,并使其适应在系统内部工作。这是函数:
#![allow(unused)] fn main() { fn obfuscate_name( item: Entity, names: &ReadStorage::<Name>, magic_items : &ReadStorage::<MagicItem>, obfuscated_names : &ReadStorage::<ObfuscatedName>, dm : &MasterDungeonMap, ) -> String { if let Some(name) = names.get(item) { if magic_items.get(item).is_some() { if dm.identified_items.contains(&name.name) { name.name.clone() } else if let Some(obfuscated) = obfuscated_names.get(item) { obfuscated.name.clone() } else { "Unidentified magic item".to_string() } } else { name.name.clone() } } else { "Nameless item (bug)".to_string() } } }
然后我们可以更改 ItemCollectionSystem
以使用它。涉及相当多的其他系统:
#![allow(unused)] fn main() { pub struct ItemCollectionSystem {} impl<'a> System<'a> for ItemCollectionSystem { #[allow(clippy::type_complexity)] type SystemData = ( ReadExpect<'a, Entity>, WriteExpect<'a, GameLog>, WriteStorage<'a, WantsToPickupItem>, WriteStorage<'a, Position>, ReadStorage<'a, Name>, WriteStorage<'a, InBackpack>, WriteStorage<'a, EquipmentChanged>, ReadStorage<'a, MagicItem>, ReadStorage<'a, ObfuscatedName>, ReadExpect<'a, MasterDungeonMap> ); fn run(&mut self, data : Self::SystemData) { let (player_entity, mut gamelog, mut wants_pickup, mut positions, names, mut backpack, mut dirty, magic_items, obfuscated_names, dm) = data; for pickup in wants_pickup.join() { positions.remove(pickup.item); backpack.insert(pickup.item, InBackpack{ owner: pickup.collected_by }).expect("Unable to insert backpack entry"); dirty.insert(pickup.collected_by, EquipmentChanged{}).expect("Unable to insert"); if pickup.collected_by == *player_entity { gamelog.entries.push( format!( "You pick up the {}.", obfuscate_name(pickup.item, &names, &magic_items, &obfuscated_names, &dm) ) ); } } wants_pickup.clear(); } } }
同样,我们需要调整物品掉落系统:
#![allow(unused)] fn main() { pub struct ItemDropSystem {} impl<'a> System<'a> for ItemDropSystem { #[allow(clippy::type_complexity)] type SystemData = ( ReadExpect<'a, Entity>, WriteExpect<'a, GameLog>, Entities<'a>, WriteStorage<'a, WantsToDropItem>, ReadStorage<'a, Name>, WriteStorage<'a, Position>, WriteStorage<'a, InBackpack>, WriteStorage<'a, EquipmentChanged>, ReadStorage<'a, MagicItem>, ReadStorage<'a, ObfuscatedName>, ReadExpect<'a, MasterDungeonMap> ); fn run(&mut self, data : Self::SystemData) { let (player_entity, mut gamelog, entities, mut wants_drop, names, mut positions, mut backpack, mut dirty, magic_items, obfuscated_names, dm) = data; for (entity, to_drop) in (&entities, &wants_drop).join() { let mut dropper_pos : Position = Position{x:0, y:0}; { let dropped_pos = positions.get(entity).unwrap(); dropper_pos.x = dropped_pos.x; dropper_pos.y = dropped_pos.y; } positions.insert(to_drop.item, Position{ x : dropper_pos.x, y : dropper_pos.y }).expect("Unable to insert position"); backpack.remove(to_drop.item); dirty.insert(entity, EquipmentChanged{}).expect("Unable to insert"); if entity == *player_entity { gamelog.entries.push( format!( "You drop the {}.", obfuscate_name(to_drop.item, &names, &magic_items, &obfuscated_names, &dm) ) ); } } wants_drop.clear(); } } }
修复物品颜色
另一个问题是我们已经为各种魔法物品进行了颜色编码。眼尖的玩家可能会因为颜色而知道 “blah 卷轴” 实际上是火球卷轴!解决方案是遍历 spawns.json
并确保物品具有相同的颜色。
总结
这为我们提供了物品辨识迷你游戏的基础知识。我们尚未触及诅咒物品 - 这将在未来的章节中介绍(在我们清理物品系统中的一些问题之后;更多内容将在下一章中介绍)。
...
本章的源代码可以在这里找到
在您的浏览器中使用 web assembly 运行本章的示例(需要 WebGL2)
版权所有 (C) 2019, Herbert Wolverson.