幽深蘑菇森林


关于本教程

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

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

Hands-On Rust


本章将为游戏添加另一个蘑菇林地关卡,这次没有矮人要塞。它还将添加最终的蘑菇关卡,根据设计文档,这个关卡将通往黑暗精灵城市。最后,我们将通过自动化添加魔法和诅咒物品的一些繁琐工作,进一步改进我们的物品故事。

构建蘑菇森林

我们将从打开 map_builders/mod.rs 并向地图构建器调用添加另一行开始:

#![allow(unused)]
fn main() {
pub fn level_builder(new_depth: i32, rng: &mut rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain {
    rltk::console::log(format!("Depth: {}", new_depth));
    match new_depth {
        1 => town_builder(new_depth, rng, width, height),
        2 => forest_builder(new_depth, rng, width, height),
        3 => limestone_cavern_builder(new_depth, rng, width, height),
        4 => limestone_deep_cavern_builder(new_depth, rng, width, height),
        5 => limestone_transition_builder(new_depth, rng, width, height),
        6 => dwarf_fort_builder(new_depth, rng, width, height),
        7 => mushroom_entrance(new_depth, rng, width, height),
        8 => mushroom_builder(new_depth, rng, width, height),
        _ => random_builder(new_depth, rng, width, height)
    }
}
}

然后我们将打开 map_builders/mushroom_forest.rs 并为该关卡存根一个基本的地图构建器:

#![allow(unused)]
fn main() {
pub fn mushroom_builder(new_depth: i32, _rng: &mut rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain {
    let mut chain = BuilderChain::new(new_depth, width, height, "Into The Mushroom Grove");
    chain.start_with(CellularAutomataBuilder::new());
    chain.with(WaveformCollapseBuilder::new());
    chain.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER));
    chain.with(CullUnreachable::new());
    chain.with(AreaStartingPosition::new(XStart::RIGHT, YStart::CENTER));
    chain.with(AreaEndingPosition::new(XEnd::LEFT, YEnd::CENTER));
    chain.with(VoronoiSpawning::new());
    chain
}
}

这基本上与另一个蘑菇构建器相同,但没有预制件覆盖。如果您进入 main.rs 并更改起始关卡:

#![allow(unused)]
fn main() {
gs.generate_world_map(8, 0);

rltk::main_loop(context, gs)
}

cargo run,您将获得一个相当不错的关卡。它保留了我们之前关卡的怪物生成,因为我们仔细地将它们包含在我们的生成关卡范围中。

真菌森林的尽头

再一次,我们将在 map_builders/mod.rs 中添加另一个关卡:

#![allow(unused)]
fn main() {
pub fn level_builder(new_depth: i32, rng: &mut rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain {
    rltk::console::log(format!("Depth: {}", new_depth));
    match new_depth {
        1 => town_builder(new_depth, rng, width, height),
        2 => forest_builder(new_depth, rng, width, height),
        3 => limestone_cavern_builder(new_depth, rng, width, height),
        4 => limestone_deep_cavern_builder(new_depth, rng, width, height),
        5 => limestone_transition_builder(new_depth, rng, width, height),
        6 => dwarf_fort_builder(new_depth, rng, width, height),
        7 => mushroom_entrance(new_depth, rng, width, height),
        8 => mushroom_builder(new_depth, rng, width, height),
        9 => mushroom_exit(new_depth, rng, width, height),
        _ => random_builder(new_depth, rng, width, height)
    }
}
}

并赋予它与 mushroom_builder 相同的起始代码:

#![allow(unused)]
fn main() {
pub fn mushroom_exit(new_depth: i32, _rng: &mut rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain {
    let mut chain = BuilderChain::new(new_depth, width, height, "Into The Mushroom Grove");
    chain.start_with(CellularAutomataBuilder::new());
    chain.with(WaveformCollapseBuilder::new());
    chain.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER));
    chain.with(CullUnreachable::new());
    chain.with(AreaStartingPosition::new(XStart::RIGHT, YStart::CENTER));
    chain.with(AreaEndingPosition::new(XEnd::LEFT, YEnd::CENTER));
    chain.with(VoronoiSpawning::new());
    chain
}
}

我们还将访问 main.rs 以使我们从这个关卡开始:

#![allow(unused)]
fn main() {
gs.generate_world_map(9, 0);
}

连续两个相同的关卡(设计方面;内容会因程序生成而异)非常枯燥,我们需要传达这里有一个通往黑暗精灵城市的入口的想法。我们将从向地图添加一个新的预制件截面开始:

#![allow(unused)]
fn main() {
#[allow(dead_code)]
pub const DROW_ENTRY : PrefabSection = PrefabSection{
    template : DROW_ENTRY_TXT,
    width: 12,
    height: 10,
    placement: ( HorizontalPlacement::Center, VerticalPlacement::Center )
};

#[allow(dead_code)]
const DROW_ENTRY_TXT : &str = "
            
 ######### 
        # 
   >    # 
        # 
 e       # 
    e     # 
 e       # 
 ######### 
            
";
}

注意空格:预制件周围有空格,这些空格是有意的 - 以确保它周围有一个“沟槽”。现在我们修改我们的 mushroom_exit 函数来生成它:

#![allow(unused)]
fn main() {
pub fn mushroom_exit(new_depth: i32, _rng: &mut rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain {
    let mut chain = BuilderChain::new(new_depth, width, height, "Into The Mushroom Grove");
    chain.start_with(CellularAutomataBuilder::new());
    chain.with(WaveformCollapseBuilder::new());
    chain.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER));
    chain.with(CullUnreachable::new());
    chain.with(AreaStartingPosition::new(XStart::RIGHT, YStart::CENTER));
    chain.with(AreaEndingPosition::new(XEnd::LEFT, YEnd::CENTER));
    chain.with(VoronoiSpawning::new());
    chain.with(PrefabBuilder::sectional(DROW_ENTRY));
    chain
}
}

未知字形加载地图: e

您可以 cargo run 并找到现在位于中间的出口,但是没有黑暗精灵! “e”什么也没生成,并生成一个警告。没关系 - 我们还没有实现任何黑暗精灵。在 map_builders/prefab_builder/mod.rs 中,我们将添加 e 以在加载器文件中表示“Dark Elf”:

#![allow(unused)]
fn main() {
fn char_to_map(&mut self, ch : char, idx: usize, build_data : &mut BuilderMap) {
    // 边界检查
    if idx >= build_data.map.tiles.len()-1 {
        return;
    }
    match ch {
        ' ' => build_data.map.tiles[idx] = TileType::Floor,
        '#' => build_data.map.tiles[idx] = TileType::Wall,
        '≈' => build_data.map.tiles[idx] = TileType::DeepWater,
        '@' => {
            let x = idx as i32 % build_data.map.width;
            let y = idx as i32 / build_data.map.width;
            build_data.map.tiles[idx] = TileType::Floor;
            build_data.starting_position = Some(Position{ x:x as i32, y:y as i32 });
        }
        '>' => build_data.map.tiles[idx] = TileType::DownStairs,
        'e' => {
            build_data.map.tiles[idx] = TileType::Floor;
            build_data.spawn_list.push((idx, "Dark Elf".to_string()));
        }
        'g' => {
            build_data.map.tiles[idx] = TileType::Floor;
            build_data.spawn_list.push((idx, "Goblin".to_string()));
        }
        'o' => {
            build_data.map.tiles[idx] = TileType::Floor;
            build_data.spawn_list.push((idx, "Orc".to_string()));
        }
        'O' => {
            build_data.map.tiles[idx] = TileType::Floor;
            build_data.spawn_list.push((idx, "Orc Leader".to_string()));
        }
        '^' => {
            build_data.map.tiles[idx] = TileType::Floor;
            build_data.spawn_list.push((idx, "Bear Trap".to_string()));
        }
        '%' => {
            build_data.map.tiles[idx] = TileType::Floor;
            build_data.spawn_list.push((idx, "Rations".to_string()));
        }
        '!' => {
            build_data.map.tiles[idx] = TileType::Floor;
            build_data.spawn_list.push((idx, "Health Potion".to_string()));
        }
        '☼' => {
            build_data.map.tiles[idx] = TileType::Floor;
            build_data.spawn_list.push((idx, "Watch Fire".to_string()));
        }
        _ => {
            rltk::console::log(format!("Unknown glyph loading map: {}", (ch as u8) as char));
        }
    }
}
}

如果您 cargo run,则错误现在被 WARNING: We don't know how to spawn [Dark Elf]! 取代 - 这是进步。

为了解决这个问题,我们将定义黑暗精灵!让我们从一个非常简单的 spawns.json 条目开始:

{
    "name" : "Dark Elf",
    "renderable": {
        "glyph" : "e",
        "fg" : "#FF0000",
        "bg" : "#000000",
        "order" : 1
    },
    "blocks_tile" : true,
    "vision_range" : 8,
    "movement" : "random_waypoint",
    "attributes" : {},
    "equipped" : [ "Dagger", "Shield", "Leather Armor", "Leather Boots" ],
    "faction" : "DarkElf",
    "gold" : "3d6",
    "level" : 6
},

我们还将为他们提供一个 faction 条目:

{ "name" : "DarkElf", "responses" : { "Default" : "attack", "DarkElf" : "ignore" } }

如果您现在 cargo run,您将有一些中等强度的黑暗精灵需要对付。问题是,它们不是很“黑暗精灵”:它们基本上是重新蒙皮的土匪。当您想到“黑暗精灵”时,您会想到什么(除了 Drizzt Do'Urden,如果我包含他,他的版权所有者会从远处击打我)?他们非常邪恶,魔法,行动迅速,并且通常非常强大。他们也倾向于拥有自己的黑暗技术,并用远程武器攻击他们的敌人!

在下一章之前,我们不会支持远程武器,但是我们可以采取一些步骤使它们更像黑暗精灵。让我们给他们一套更像黑暗精灵的物品。在 equipped 标签中,我们将使用:

"equipped" : [ "Scimitar", "Buckler", "Drow Chain", "Drow Leggings", "Drow Boots" ],

我们还需要为这些制作物品条目。我们将 弯刀 基本视为长剑,但稍微好一点:

{
    "name" : "Scimitar",
    "renderable": {
        "glyph" : "/",
        "fg" : "#FFAAFF",
        "bg" : "#000000",
        "order" : 2
    },
    "weapon" : {
        "range" : "melee",
        "attribute" : "might",
        "base_damage" : "1d6+2",
        "hit_bonus" : 1
    },
    "weight_lbs" : 2.5,
    "base_value" : 25.0,
    "initiative_penalty" : 1,
    "vendor_category" : "weapon"
},

我们将遵循 卓尔盔甲 的趋势:它基本上是链甲,但启动惩罚要小得多:

{
    "name" : "Drow Leggings",
    "renderable": {
        "glyph" : "[",
        "fg" : "#00FFFF",
        "bg" : "#000000",
        "order" : 2
    },
    "wearable" : {
        "slot" : "Legs",
        "armor_class" : 0.4
    },
    "weight_lbs" : 10.0,
    "base_value" : 50.0,
    "initiative_penalty" : 0.1,
    "vendor_category" : "clothes"
},

{
    "name" : "Drow Chain",
    "renderable": {
        "glyph" : "[",
        "fg" : "#00FF00",
        "bg" : "#000000",
        "order" : 2
    },
    "wearable" : {
        "slot" : "Torso",
        "armor_class" : 3.0
    },
    "weight_lbs" : 5.0,
    "base_value" : 50.0,
    "initiative_penalty" : 0.0,
    "vendor_category" : "armor"
},

{
    "name" : "Drow Boots",
    "renderable": {
        "glyph" : "[",
        "fg" : "#00FF00",
        "bg" : "#000000",
        "order" : 2
    },
    "wearable" : {
        "slot" : "Feet",
        "armor_class" : 0.4
    },
    "weight_lbs" : 2.0,
    "base_value" : 10.0,
    "initiative_penalty" : 0.1,
    "vendor_category" : "armor"
},

这些的结果是它们很快 - 它们的启动惩罚比类似装甲的玩家小得多。另一个好处是,您可以杀死一个,拿走他们的东西 - 并获得相同的好处!

至此,我们已经添加了两个可玩的关卡 - 只需几行代码。收获在通用系统上努力工作的回报!所以现在,让我们使事情更通用一些 - 并为自己节省一些打字时间。

程序化生成的魔法物品

我们一直在添加“长剑 +1”、“长剑 -1”等等。我们可以坐下来费力地输入每个物品的每个魔法变体,我们将拥有一个相当可玩的游戏。或者 - 我们可以自动化一些繁琐的工作!

如果我们可以在 spawns.json 中的武器定义中附加一个“template”属性,并使其自动为我们生成变体呢?这并不像听起来那么牵强。让我们草拟一下我们想要的东西:

{
    "name" : "Longsword",
    "renderable": {
        "glyph" : "/",
        "fg" : "#FFAAFF",
        "bg" : "#000000",
        "order" : 2
    },
    "weapon" : {
        "range" : "melee",
        "attribute" : "might",
        "base_damage" : "1d8",
        "hit_bonus" : 0
    },
    "weight_lbs" : 3.0,
    "base_value" : 15.0,
    "initiative_penalty" : 2,
    "vendor_category" : "weapon",
    "template_magic" : {
        "unidentified_name" : "Unidentified Longsword",
        "bonus_min" : 1,
        "bonus_max" : 5,
        "include_cursed" : true
    }
},

因此,我们添加了一个 template_magic 部分,描述了我们想要添加的物品类型。我们需要扩展 raws/item_structs.rs 以支持加载此信息:

#![allow(unused)]
fn main() {
#[derive(Deserialize, Debug, Clone)]
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>,
    pub attributes : Option<ItemAttributeBonus>,
    pub template_magic : Option<ItemMagicTemplate>
}
...
#[derive(Deserialize, Debug, Clone)]
pub struct ItemMagicTemplate {
    pub unidentified_name: String,
    pub bonus_min: i32,
    pub bonus_max: i32,
    pub include_cursed: bool
}
}

这足以加载额外的信息 - 它只是什么也不做。我们还需要遍历并为该文件中的所有结构在 #[derive] 列表中添加 Clone。我们将使用 clone() 制作副本,然后为每个变体进行修改。

与其他添加不同,这不会修改我们在 rawmaster.rs 中的 spawn_named_item 函数;我们希望在开始生成之前修改原始文件模板。相反,我们将后处理 load 函数本身构建的物品列表(包括修改生成列表)。在该函数的顶部,我们将读取每个物品,如果它附加了模板(并且是武器或盔甲物品),我们将它添加到要处理的列表中:

#![allow(unused)]
fn main() {
pub fn load(&mut self, raws : Raws) {
    self.raws = raws;
    self.item_index = HashMap::new();
    let mut used_names : HashSet<String> = HashSet::new();

    struct NewMagicItem {
        name : String,
        bonus : i32
    }
    let mut items_to_build : Vec<NewMagicItem> = Vec::new();

    for (i,item) in self.raws.items.iter().enumerate() {
        if used_names.contains(&item.name) {
            rltk::console::log(format!("WARNING -  duplicate item name in raws [{}]", item.name));
        }
        self.item_index.insert(item.name.clone(), i);
        used_names.insert(item.name.clone());

        if let Some(template) = &item.template_magic {
            if item.weapon.is_some() || item.wearable.is_some() {
                if template.include_cursed {
                    items_to_build.push(NewMagicItem{
                        name : item.name.clone(),
                        bonus : -1
                    });
                }
                for bonus in template.bonus_min ..= template.bonus_max {
                    items_to_build.push(NewMagicItem{
                        name : item.name.clone(),
                        bonus
                    });
                }
            } else {
                rltk::console::log(format!("{} is marked as templated, but isn't a weapon or armor.", item.name));
            }
        }
    }
}

然后,在我们完成读取物品后,我们将在最后添加一个循环来创建这些物品:

#![allow(unused)]
fn main() {
for nmw in items_to_build.iter() {
    let base_item_index = self.item_index[&nmw.name];
    let mut base_item_copy = self.raws.items[base_item_index].clone();

    if nmw.bonus == -1 {
        base_item_copy.name = format!("{} -1", nmw.name);
    } else {
        base_item_copy.name = format!("{} +{}", nmw.name, nmw.bonus);
    }

    base_item_copy.magic = Some(super::MagicItem{
        class : match nmw.bonus {
            2 => "rare".to_string(),
            3 => "rare".to_string(),
            4 => "rare".to_string(),
            5 => "legendary".to_string(),
            _ => "common".to_string()
        },
        naming : base_item_copy.template_magic.as_ref().unwrap().unidentified_name.clone(),
        cursed: if nmw.bonus == -1 { Some(true) } else { None }
    });

    if let Some(initiative_penalty) = base_item_copy.initiative_penalty.as_mut() {
        *initiative_penalty -= nmw.bonus as f32;
    }
    if let Some(base_value) = base_item_copy.base_value.as_mut() {
        *base_value += (nmw.bonus as f32 + 1.0) * 50.0;
    }
    if let Some(mut weapon) = base_item_copy.weapon.as_mut() {
        weapon.hit_bonus += nmw.bonus;
        let (n,die,plus) = parse_dice_string(&weapon.base_damage);
        let final_bonus = plus+nmw.bonus;
        if final_bonus > 0 {
            weapon.base_damage = format!("{}d{}+{}", n, die, final_bonus);
        } else if final_bonus < 0 {
            weapon.base_damage = format!("{}d{}-{}", n, die, i32::abs(final_bonus));
        }
    }
    if let Some(mut armor) = base_item_copy.wearable.as_mut() {
        armor.armor_class += nmw.bonus as f32;
    }

    let real_name = base_item_copy.name.clone();
    self.raws.items.push(base_item_copy);
    self.item_index.insert(real_name.clone(), self.raws.items.len()-1);

    self.raws.spawn_table.push(super::SpawnTableEntry{
        name : real_name.clone(),
        weight : 10 - i32::abs(nmw.bonus),
        min_depth : 1 + i32::abs((nmw.bonus-1)*3),
        max_depth : 100,
        add_map_depth_to_weight : None
    });
}
}

因此,这循环遍历我们在初始解析期间创建的所有“长剑 +1”、“长剑 -1”、“长剑 +2”等。然后它:

  1. 获取原始物品的副本。
  2. 如果奖励是 -1,则将其重命名为“物品 -x”;否则将其重命名为“物品 +x”,其中 x 是奖励。
  3. 它为物品创建一个新的 magic 条目,并按奖励设置 common/rare/legendary 状态,并根据需要设置诅咒标志。
  4. 如果该物品有启动惩罚,它会从其中减去奖励(使诅咒物品更糟,魔法物品更好)。
  5. 它将基本价值提高奖励 +1 * 50 金币。
  6. 如果是武器,则将奖励添加到 to_hit 奖励和伤害骰子。它通过重新格式化骰子数字来完成伤害骰子。
  7. 如果是盔甲,它会将奖励添加到护甲等级。
  8. 然后,它将新物品插入生成表,更好的物品权重更低,更好的物品稍后出现在地下城中。

如果您查看 在线源代码 - 我已经遍历并删除了所有 +1、+2 和简单的诅咒盔甲和武器 - 并将 template_magic 附加到它们中的每一个。这导致生成了 168 个新物品!这比全部输入它们要好得多。

如果您现在 cargo run,您会在整个地下城中找到逐渐改进的各种类型的魔法物品。更好的物品会在您深入地下城时出现,因此玩家的力量会很好地提升。

特性物品

通过 dagger of venom,我们引入了一种新型的物品:一种在您击中时会产生效果的物品。鉴于这可以是游戏中的任何效果,因此效果有很多可能性!手动添加所有效果将花费一段时间 - 可能更快地提出一个通用系统,并因此在我们的物品中获得真正的多样性(以及不会忘记添加它们!)。

让我们从在 spawns.json 中添加一个新部分开始,专门用于武器特性

"weapon_traits" : [
    {
        "name" : "Venomous",
        "effects" : { "damage_over_time" : "2" }
    }
]

稍后我们将添加更多特性,现在我们将专注于使系统完全工作!为了读取数据,我们将创建一个新文件 raws/weapon_traits.rs(不要将 Rust 特性和武器特性混淆;它们根本不是一回事)。我们将放入足够的结构以允许 Serde 读取 JSON 文件:

#![allow(unused)]
fn main() {
use serde::{Deserialize};
use std::collections::HashMap;

#[derive(Deserialize, Debug)]
pub struct WeaponTrait {
    pub name : String,
    pub effects : HashMap<String, String>
}
}

现在我们需要扩展 raws/mod.rs 中的数据以包含它。在文件的顶部,包含:

#![allow(unused)]
fn main() {
mod weapon_traits;
pub use weapon_traits::*;
}

然后我们将它添加到 Raws 结构中,就像我们对法术所做的那样:

#![allow(unused)]
fn main() {
#[derive(Deserialize, Debug)]
pub struct Raws {
    pub items : Vec<Item>,
    pub mobs : Vec<Mob>,
    pub props : Vec<Prop>,
    pub spawn_table : Vec<SpawnTableEntry>,
    pub loot_tables : Vec<LootTable>,
    pub faction_table : Vec<FactionInfo>,
    pub spells : Vec<Spell>,
    pub weapon_traits : Vec<WeaponTrait>
}
}

反过来,我们必须扩展 raws/rawmaster.rs 中的构造函数以包含一个空的特性列表:

#![allow(unused)]
fn main() {
impl RawMaster {
    pub fn empty() -> RawMaster {
        RawMaster {
            raws : Raws{
                items: Vec::new(),
                mobs: Vec::new(),
                props: Vec::new(),
                spawn_table: Vec::new(),
                loot_tables: Vec::new(),
                faction_table : Vec::new(),
                spells : Vec::new(),
                weapon_traits : Vec::new()
            },
            item_index : HashMap::new(),
            mob_index : HashMap::new(),
            prop_index : HashMap::new(),
            loot_index : HashMap::new(),
            faction_index : HashMap::new(),
            spell_index : HashMap::new()
        }
    }
    ...
}

感谢 Serde 的魔力,这就是实际加载数据的所有内容!现在是困难的部分:程序化生成具有一个或多个特性的魔法物品。为了避免重复自己,我们将把之前编写的代码分离成可重用的函数:

#![allow(unused)]
fn main() {
// 将此放在 raws 实现之上
struct NewMagicItem {
    name : String,
    bonus : i32
}
...

// 在 raws 实现内部
fn append_magic_template(items_to_build : &mut Vec<NewMagicItem>, item : &super::Item) {
    if let Some(template) = &item.template_magic {
        if item.weapon.is_some() || item.wearable.is_some() {
            if template.include_cursed {
                items_to_build.push(NewMagicItem{
                    name : item.name.clone(),
                    bonus : -1
                });
            }
            for bonus in template.bonus_min ..= template.bonus_max {
                items_to_build.push(NewMagicItem{
                    name : item.name.clone(),
                    bonus
                });
            }
        } else {
            rltk::console::log(format!("{} is marked as templated, but isn't a weapon or armor.", item.name));
        }
    }
}

fn build_base_magic_item(&self, nmw : &NewMagicItem) -> super::Item {
    let base_item_index = self.item_index[&nmw.name];
    let mut base_item_copy = self.raws.items[base_item_index].clone();
    base_item_copy.vendor_category = None; // 不要出售魔法物品!

    if nmw.bonus == -1 {
        base_item_copy.name = format!("{} -1", nmw.name);
    } else {
        base_item_copy.name = format!("{} +{}", nmw.name, nmw.bonus);
    }

    base_item_copy.magic = Some(super::MagicItem{
        class : match nmw.bonus {
            2 => "rare".to_string(),
            3 => "rare".to_string(),
            4 => "rare".to_string(),
            5 => "legendary".to_string(),
            _ => "common".to_string()
        },
        naming : base_item_copy.template_magic.as_ref().unwrap().unidentified_name.clone(),
        cursed: if nmw.bonus == -1 { Some(true) } else { None }
    });

    if let Some(initiative_penalty) = base_item_copy.initiative_penalty.as_mut() {
        *initiative_penalty -= nmw.bonus as f32;
    }
    if let Some(base_value) = base_item_copy.base_value.as_mut() {
        *base_value += (nmw.bonus as f32 + 1.0) * 50.0;
    }
    if let Some(mut weapon) = base_item_copy.weapon.as_mut() {
        weapon.hit_bonus += nmw.bonus;
        let (n,die,plus) = parse_dice_string(&weapon.base_damage);
        let final_bonus = plus+nmw.bonus;
        if final_bonus > 0 {
            weapon.base_damage = format!("{}d{}+{}", n, die, final_bonus);
        } else if final_bonus < 0 {
            weapon.base_damage = format!("{}d{}-{}", n, die, i32::abs(final_bonus));
        }
    }
    if let Some(mut armor) = base_item_copy.wearable.as_mut() {
        armor.armor_class += nmw.bonus as f32;
    }
    base_item_copy
}

fn build_magic_weapon_or_armor(&mut self, items_to_build : &[NewMagicItem]) {
    for nmw in items_to_build.iter() {
        let base_item_copy = self.build_base_magic_item(&nmw);

        let real_name = base_item_copy.name.clone();
        self.raws.items.push(base_item_copy);
        self.item_index.insert(real_name.clone(), self.raws.items.len()-1);

        self.raws.spawn_table.push(super::SpawnTableEntry{
            name : real_name.clone(),
            weight : 10 - i32::abs(nmw.bonus),
            min_depth : 1 + i32::abs((nmw.bonus-1)*3),
            max_depth : 100,
            add_map_depth_to_weight : None
        });
    }
}

fn build_traited_weapons(&mut self, items_to_build : &[NewMagicItem]) {
    items_to_build.iter().filter(|i| i.bonus > 0).for_each(|nmw| {
        for wt in self.raws.weapon_traits.iter() {
            let mut base_item_copy = self.build_base_magic_item(&nmw);
            if let Some(mut weapon) = base_item_copy.weapon.as_mut() {
                base_item_copy.name = format!("{} {}", wt.name, base_item_copy.name);
                if let Some(base_value) = base_item_copy.base_value.as_mut() {
                    *base_value *= 2.0;
                }
                    weapon.proc_chance = Some(0.25);
                    weapon.proc_effects = Some(wt.effects.clone());

                let real_name = base_item_copy.name.clone();
                self.raws.items.push(base_item_copy);
                self.item_index.insert(real_name.clone(), self.raws.items.len()-1);

                self.raws.spawn_table.push(super::SpawnTableEntry{
                    name : real_name.clone(),
                    weight : 9 - i32::abs(nmw.bonus),
                    min_depth : 2 + i32::abs((nmw.bonus-1)*3),
                    max_depth : 100,
                    add_map_depth_to_weight : None
                });
            }
        }
    });
}

pub fn load(&mut self, raws : Raws) {
    self.raws = raws;
    self.item_index = HashMap::new();
    let mut used_names : HashSet<String> = HashSet::new();
    let mut items_to_build = Vec::new();

    for (i,item) in self.raws.items.iter().enumerate() {
        if used_names.contains(&item.name) {
            rltk::console::log(format!("WARNING -  duplicate item name in raws [{}]", item.name));
        }
        self.item_index.insert(item.name.clone(), i);
        used_names.insert(item.name.clone());

        RawMaster::append_magic_template(&mut items_to_build, item);
    }
    for (i,mob) in self.raws.mobs.iter().enumerate() {
        if used_names.contains(&mob.name) {
            rltk::console::log(format!("WARNING -  duplicate mob name in raws [{}]", mob.name));
        }
        self.mob_index.insert(mob.name.clone(), i);
        used_names.insert(mob.name.clone());
    }
    for (i,prop) in self.raws.props.iter().enumerate() {
        if used_names.contains(&prop.name) {
            rltk::console::log(format!("WARNING -  duplicate prop name in raws [{}]", prop.name));
        }
        self.prop_index.insert(prop.name.clone(), i);
        used_names.insert(prop.name.clone());
    }

    for spawn in self.raws.spawn_table.iter() {
        if !used_names.contains(&spawn.name) {
            rltk::console::log(format!("WARNING - Spawn tables references unspecified entity {}", spawn.name));
        }
    }

    for (i,loot) in self.raws.loot_tables.iter().enumerate() {
        self.loot_index.insert(loot.name.clone(), i);
    }

    for faction in self.raws.faction_table.iter() {
        let mut reactions : HashMap<String, Reaction> = HashMap::new();
        for other in faction.responses.iter() {
            reactions.insert(
                other.0.clone(),
                match other.1.as_str() {
                    "ignore" => Reaction::Ignore,
                    "flee" => Reaction::Flee,
                    _ => Reaction::Attack
                }
            );
        }
        self.faction_index.insert(faction.name.clone(), reactions);
    }

    for (i,spell) in self.raws.spells.iter().enumerate() {
        self.spell_index.insert(spell.name.clone(), i);
    }

    self.build_magic_weapon_or_armor(&items_to_build);
    self.build_traited_weapons(&items_to_build);
}
}

您会注意到其中有一个新函数 build_traited_weapons。它迭代魔法物品,仅过滤武器 - 并且仅过滤那些带有奖励的武器(我现在真的不想深入研究诅咒的剧毒匕首的作用)。它读取所有特性,并使用应用的特性制作每个魔法武器的(更稀有的)版本。

让我们继续在 spawns.json 中添加一个特性:

"weapon_traits" : [
    {
        "name" : "Venomous",
        "effects" : { "damage_over_time" : "2" }
    },
    {
        "name" : "Dazzling",
        "effects" : { "confusion" : "2" }
    }
]

如果您现在 cargo run 并玩游戏,您有时会发现诸如 Dazzling Longsword +1Venomous Dagger +2 之类的奇迹。

总结

在本章中,我们构建了一个蘑菇林地关卡,以及一个过渡到黑暗精灵据点的第二个关卡。我们已经开始添加黑暗精灵,为了增强功能(并节省打字时间),我们正在自动生成从 -1 到 +5 的魔法物品。然后我们生成了相同武器的“特性化”版本。现在,运行之间存在着巨大的差异,这应该会让注重装备的玩家感到高兴。关卡也有很好的进展,我们已准备好迎接黑暗精灵城市 - 以及远程武器!

...

本章的源代码可以在这里找到

在您的浏览器中使用 web assembly 运行本章的示例 (需要 WebGL2)

版权所有 (C) 2019, Herbert Wolverson。