魔法物品与物品辨识


关于本教程

本教程是免费且开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用。我希望您会喜欢本教程,并制作出精彩的游戏!

如果您喜欢这个教程并希望我继续写作,请考虑支持我的 Patreon

Hands-On Rust


魔法物品是 D&D 和 roguelike 游戏的中流砥柱。从不起眼的 “+1 剑” 到强大的 “圣复仇者” - 再到 “诅咒反噬者” - 物品帮助定义了这个游戏类型。在 roguelike 游戏中,不自动知晓物品是什么也是一种传统;你找到一把 “未鉴定的长剑”,在找到鉴定它的方法之前,你不知道它有什么用(或者是否被诅咒)。你找到一张 “猫咪在键盘上走过 卷轴”(无法发音的名字似乎是其特色!),在你鉴定或阅读它之前 - 你不知道会发生什么。有些游戏将此变成完整的元游戏 - 通过赌频率、供应商价格和类似信息来给你关于你刚刚找到的东西的线索。即使是 暗黑破坏神,最主流的 roguelike 游戏(即使它变成了实时制!)也保留了这个游戏特色 - 但往往会使鉴定卷轴非常丰富(还有乐于助人的苏格兰老人们)。

魔法物品的类别

在现代游戏中,区分魔法物品为 魔法的稀有的传说的(以及物品套装,我们暂且不讨论)。这些通常通过颜色来区分,因此您可以一目了然地判断物品是否值得考虑。这也提供了一个机会来表示某物 魔法物品 - 因此我们将打开 components.rs(并在 main.rssaveload_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,您将看到您的 城镇传送卷轴 现在已被很好地突出显示为普通魔法物品:

Screenshot

辨识:卷轴

在 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 ” 开头,然后添加随机字母。每隔一个字母是元音,中间是辅音。这会让你得到无意义的词,但它是 可发音的 无意义词,例如 iladiomuruxo。它没有提供关于底层卷轴性质的任何线索。

接下来,我们需要一种方法来表示实体 具有 混淆的名称。我们将为此任务创建一个新组件,因此在 components.rs 中(并在 main.rssaveload_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 该项目,您找到的卷轴将显示混淆的名称:

Screenshot

鉴定混淆的卷轴

现在我们正确地隐藏了它们,让我们引入一种鉴定卷轴的机制。最明显的是,如果您 使用 卷轴,它应该被鉴定 - 并且该卷轴类型的所有现有/未来实例都将被鉴定。identified_items 列表处理未来,但我们将不得不做一些额外的工作来处理现有卷轴。我们将会有相当多的潜在鉴定发生 - 当您使用鉴定魔法(最终),当您使用或装备物品,当您购买物品时(因为您知道您正在购买魔法地图卷轴,您对其进行鉴定是有道理的) - 并且随着我们的进展,可能会有更多。

我们将使用一个新组件来处理这个问题,以指示物品可能已被鉴定,并使用一个系统来处理数据。首先,在 components.rs 中(并在 main.rssaveload_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,您将能够通过使用或购买物品来鉴定物品。

Screenshot

混淆药剂

我们可以为药剂使用非常相似的设置,但我们需要考虑如何命名它们。通常,药剂将一些形容词与药剂一词结合起来:“粘稠的黑色药剂”、“漩涡状的绿色药剂” 等。幸运的是,我们现在已经构建了很多基础设施框架 - 因此这只是插入细节的问题。

我们将从打开 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,四处走走并找到药剂,您会发现它有一个混淆的名称:

Screenshot

其他魔法物品

我们也应该支持其他魔法物品,而无需特殊的命名方案。让我们打开 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 级(我使用作弊码,反斜杠 是您的朋友),您 很可能 找到一把魔法长剑:

Screenshot

清理

我们应该将魔法长剑的生成权重改回合理的值,并使普通长剑更频繁地出现。在 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.