过渡:从洞穴到矮人要塞


关于本教程

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

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

Hands-On Rust


设计文档中提到,洞穴将会过渡到一个精心雕琢的矮人要塞——现在被邪恶的野兽和一条龙占据着。如果下一层突然变成了一个四四方方的矮人堡垒,那将会非常突兀——所以这一层将完全关于过渡。

让我们从主题开始。我们希望将地图分割为石灰岩洞穴的外观和地牢的外观——因此我们在 themes.rstile_glyph 函数中添加一个新的条目来实现这一点:

#![allow(unused)]
fn main() {
pub fn tile_glyph(idx: usize, map : &Map) -> (rltk::FontCharType, RGB, RGB) {
    let (glyph, mut fg, mut bg) = match map.depth {
        5 => {
            let x = idx as i32 % map.width;
            if x < map.width/2 {
                get_limestone_cavern_glyph(idx, map)
            } else {
                get_tile_glyph_default(idx, map)
            }
        }
    ...
}

现在我们打开 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),
        _ => random_builder(new_depth, rng, width, height)
    }
}
}

打开 limestone_cavern.rs,我们将创建一个新函数:

#![allow(unused)]
fn main() {
pub fn limestone_transition_builder(new_depth: i32, _rng: &mut rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain {
    let mut chain = BuilderChain::new(new_depth, width, height, "Dwarf Fort - Upper Reaches");
    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::LEFT, YStart::CENTER));
    chain.with(VoronoiSpawning::new());
    chain.with(CaveDecorator::new());
    chain
}
}

这非常简单:它创建了一个细胞自动机地图,然后用波形坍缩对其进行卷积;我们在之前的章节中已经介绍过这些,所以它们应该很熟悉。它实现了我们想要一半的效果:一个开放、自然的地牢外观。但是我们需要更多的工作来生成矮人部分!让我们添加更多步骤:

#![allow(unused)]
fn main() {
pub fn limestone_transition_builder(new_depth: i32, _rng: &mut rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain {
    let mut chain = BuilderChain::new(new_depth, width, height, "Dwarf Fort - Upper Reaches");
    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::LEFT, YStart::CENTER));
    chain.with(VoronoiSpawning::new());
    chain.with(CaveDecorator::new());
    chain.with(CaveTransition::new());
    chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::CENTER));
    chain.with(DistantExit::new());
    chain
}
}

所以现在我们经历了相同的地图生成过程,调用一个尚未编写的 CaveTransition 构建器,并重置起点和终点。那么 CaveTransition 中包含了什么呢?

#![allow(unused)]
fn main() {
pub struct CaveTransition {}

impl MetaMapBuilder for CaveTransition {
    fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap)  {
        self.build(rng, build_data);
    }
}

impl CaveTransition {
    #[allow(dead_code)]
    pub fn new() -> Box<CaveTransition> {
        Box::new(CaveTransition{})
    }

    fn build(&mut self, rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) {
        build_data.map.depth = 5;
        build_data.take_snapshot();

        // 构建一个基于 BSP 的地下城
        let mut builder = BuilderChain::new(5, build_data.width, build_data.height, "New Map");
        builder.start_with(BspDungeonBuilder::new());
        builder.with(RoomDrawer::new());
        builder.with(RoomSorter::new(RoomSort::RIGHTMOST));
        builder.with(NearestCorridors::new());
        builder.with(RoomExploder::new());
        builder.with(RoomBasedSpawner::new());
        builder.build_map(rng);

        // 将历史记录添加到我们的历史记录中
        for h in builder.build_data.history.iter() {
            build_data.history.push(h.clone());
        }
        build_data.take_snapshot();

        // 将 BSP 地图的右半部分复制到我们的地图中
        for x in build_data.map.width / 2 .. build_data.map.width {
            for y in 0 .. build_data.map.height {
                let idx = build_data.map.xy_idx(x, y);
                build_data.map.tiles[idx] = builder.build_data.map.tiles[idx];
            }
        }
        build_data.take_snapshot();

        // 保留地图左半部分的 Voronoi 生成数据
        let w = build_data.map.width;
        build_data.spawn_list.retain(|s| {
            let x = s.0 as i32 / w;
            x < w / 2
        });

        // 保留地图右半部分的房间生成数据
        for s in builder.build_data.spawn_list.iter() {
            let x = s.0 as i32 / w;
            if x > w / 2 {
                build_data.spawn_list.push(s.clone());
            }
        }
    }
}
}

所以这里有制作构建器的所有常用样板代码,然后我们进入 build 函数。让我们逐步了解它:

  1. 我们首先重置关卡的深度。波函数坍缩中存在一个错误,这使得它成为必要(它将在本章的修订版中修复)。
  2. 然后我们创建一个新的构建器!它被设置为生成一个非常普通的基于 BSP 的地牢,具有短而直接的走廊,然后侵蚀房间。
  3. 我们运行构建器,并将它的历史记录复制到我们的历史记录的末尾——这样我们也可以看到它采取的步骤。
  4. 我们将 BSP 地图的整个右半部分复制到我们实际构建的地图上。
  5. 我们从当前地图中删除地图右半部分的所有生成点。
  6. 如果 BSP 地图的所有生成点都在地图的右半部分,则将它们复制到当前地图。

所有这些的结果是什么?一个分裂的地牢!

Screenshot

我们依赖于两个半部分之间没有任何连接的几率非常低。为了确保万无一失,我们还添加一个不可达剔除循环并移除波形坍缩——它使得地图太有可能没有出口:

#![allow(unused)]
fn main() {
pub fn limestone_transition_builder(new_depth: i32, _rng: &mut rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain {
    let mut chain = BuilderChain::new(new_depth, width, height, "Dwarf Fort - Upper Reaches");
    chain.start_with(CellularAutomataBuilder::new());
    chain.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER));
    chain.with(CullUnreachable::new());
    chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::CENTER));
    chain.with(VoronoiSpawning::new());
    chain.with(CaveDecorator::new());
    chain.with(CaveTransition::new());
    chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::CENTER));
    chain.with(CullUnreachable::new());
    chain.with(AreaEndingPosition::new(XEnd::RIGHT, YEnd::CENTER));
    chain
}
}

等等——AreaEndingPosition 是新的!我想要一种保证出口在地图右侧的方法,所以我制作了一个新的构建器层。它就像 AreaStartingPosition,但设置的是楼梯而不是起点。它在文件 map_builders/area_ending_point.rs 中:

#![allow(unused)]
fn main() {
use super::{MetaMapBuilder, BuilderMap, TileType};
use crate::map;
use rltk::RandomNumberGenerator;

#[allow(dead_code)]
pub enum XEnd { LEFT, CENTER, RIGHT }

#[allow(dead_code)]
pub enum YEnd{ TOP, CENTER, BOTTOM }

pub struct AreaEndingPosition {
    x : XEnd,
    y : YEnd
}

impl MetaMapBuilder for AreaEndingPosition {
    fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap)  {
        self.build(rng, build_data);
    }
}

impl AreaEndingPosition {
    #[allow(dead_code)]
    pub fn new(x : XEnd, y : YEnd) -> Box<AreaEndingPosition> {
        Box::new(AreaEndingPosition{
            x, y
        })
    }

    fn build(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) {
        let seed_x;
        let seed_y;

        match self.x {
            XEnd::LEFT => seed_x = 1,
            XEnd::CENTER => seed_x = build_data.map.width / 2,
            XEnd::RIGHT => seed_x = build_data.map.width - 2
        }

        match self.y {
            YEnd::TOP => seed_y = 1,
            YEnd::CENTER => seed_y = build_data.map.height / 2,
            YEnd::BOTTOM => seed_y = build_data.map.height - 2
        }

        let mut available_floors : Vec<(usize, f32)> = Vec::new();
        for (idx, tiletype) in build_data.map.tiles.iter().enumerate() {
            if map::tile_walkable(*tiletype) {
                available_floors.push(
                    (
                        idx,
                        rltk::DistanceAlg::PythagorasSquared.distance2d(
                            rltk::Point::new(idx as i32 % build_data.map.width, idx as i32 / build_data.map.width),
                            rltk::Point::new(seed_x, seed_y)
                        )
                    )
                );
            }
        }
        if available_floors.is_empty() {
            panic!("No valid floors to start on");
        }

        available_floors.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap());

        build_data.map.tiles[available_floors[0].0] = TileType::DownStairs;
    }
}
}

所以将所有这些放在一起并运行它——你将拥有一个与我们目标非常一致的地牢:

Screenshot

填充我们的新关卡

除了各种掉落物,例如口粮之外,这个关卡基本上是空的!我们限制了上一关的掉落物,这很好——我们希望开始向更偏“怪物”的关卡过渡。据称,堡垒的陷落是因为一条讨厌的龙(而不是友善的那种!),所以更多龙族爪牙是有道理的。希望到目前为止,玩家将接近 3 级或 4 级,因此我们可以向他们扔一些更难的怪物,而不会使游戏变得不可能。

spawns.json 中,在 spawn_table 部分——让我们为龙类生物添加一些占位符生成点:

{ "name" : "Dragon Wyrmling", "weight" : 1, "min_depth" : 5, "max_depth" : 7 },
{ "name" : "Lizardman", "weight" : 10, "min_depth" : 5, "max_depth" : 7 },
{ "name" : "Giant Lizard", "weight" : 4, "min_depth" : 5, "max_depth" : 7 }

考虑到这曾经是矮人的领地,让我们也添加一些矮人可能会留下的东西:

{ "name" : "Rock Golem", "weight" : 4, "min_depth" : 5, "max_depth" : 7 },
{ "name" : "Stonefall Trap", "weight" : 4, "min_depth" : 5, "max_depth" : 7 },
{ "name" : "Landmine", "weight" : 1, "min_depth" : 5, "max_depth" : 7 }

矮人也以其盔甲和武器而闻名,因此为他们的装备添加一些占位符听起来不错:

{ "name" : "Breastplate", "weight" : 7, "min_depth" : 5, "max_depth" : 7 },
{ "name" : "War Axe", "weight" : 7, "min_depth" : 5, "max_depth" : 7 },
{ "name" : "Dwarf-Steel Shirt", "weight" : 1, "min_depth" : 5, "max_depth" : 7 }

这总共有 9 个新实体要创建!我们将首先使用我们已经拥有的系统来构建它们,在未来的章节中,我们将为它们添加一些特殊效果。(“矮人钢”以前是“秘银”——但托尔金基金会过去以对这个词有点律师函成瘾而闻名。所以矮人钢就是矮人钢了!)

龙类生物

我们将首先在 spawns.json 中为它们提供一个新的阵营

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

我们还将稍微推断一下,并想出一些他们可能掉落的战利品(对于 loot_tables):

{ "name" : "Wyrms",
    "drops" : [
        { "name" : "Dragon Scale", "weight" : 10 },
        { "name" : "Meat", "weight" : 10 }
    ]
}

现在,让我们进入 mobs 部分,制作我们的小龙:

{
    "name" : "Dragon Wyrmling",
    "renderable": {
        "glyph" : "d",
        "fg" : "#FF0000",
        "bg" : "#000000",
        "order" : 1
    },
    "blocks_tile" : true,
    "vision_range" : 12,
    "movement" : "random_waypoint",
    "attributes" : {
        "might" : 3,
        "fitness" : 3
    },
    "skills" : {
        "Melee" : 15,
        "Defense" : 14
    },
    "natural" : {
        "armor_class" : 15,
        "attacks" : [
            { "name" : "bite", "hit_bonus" : 4, "damage" : "1d10+2" }
        ]
    },
    "loot_table" : "Wyrms",
    "faction" : "Wyrm",
    "level" : 3,
    "gold" : "3d6"
}

即使没有特殊能力,这也是一个强大的敌人!TODO

我们绝对应该通过使蜥蜴人和巨蜥蜴相对较弱来抵消幼龙的强大本质(因为它们可能会更多):

{
    "name" : "Lizardman",
    "renderable": {
        "glyph" : "l",
        "fg" : "#FF0000",
        "bg" : "#000000",
        "order" : 1
    },
    "blocks_tile" : true,
    "vision_range" : 4,
    "movement" : "random_waypoint",
    "attributes" : {},
    "faction" : "Wyrm",
    "gold" : "1d12",
    "level" : 2
},

{
    "name" : "Giant Lizard",
    "renderable": {
        "glyph" : "l",
        "fg" : "#FFFF00",
        "bg" : "#000000",
        "order" : 1
    },
    "blocks_tile" : true,
    "vision_range" : 4,
    "movement" : "random",
    "attributes" : {},
    "faction" : "Wyrm",
    "level" : 2,
    "loot_table" : "Animal"
}

我们还需要添加“龙鳞”作为一种非常值得奖励的商品。在 spawns.json 的 items 部分:

{
    "name" : "Dragon Scale",
    "renderable": {
        "glyph" : "ß",
        "fg" : "#FFD700",
        "bg" : "#000000",
        "order" : 2
    },
    "weight_lbs" : 2.0,
    "base_value" : 75.0
},

矮人生成物

由于矮人已经死了(据推测他们又挖得太深了……),我们只有他们文明的一些遗留物需要处理。魔像、陷阱和地雷(哦,我的天!)。让我们为魔像创建一个新的阵营;他们应该不太喜欢龙,但让我们对玩家好一点,让他们被忽略:

{ "name" : "Dwarven Remnant", "responses": { "Default" : "attack", "Player" : "ignore", "Dwarven Remnant" : "ignore" }}

这使我们能够构建一个相对强大的魔像。它可以很强大,因为它将与蜥蜴战斗:

{
    "name" : "Rock Golem",
    "renderable": {
        "glyph" : "g",
        "fg" : "#AAAAAA",
        "bg" : "#000000",
        "order" : 1
    },
    "blocks_tile" : true,
    "vision_range" : 6,
    "movement" : "random_waypoint",
    "attributes" : {},
    "faction" : "Dwarven Remnant",
    "level" : 3
}

落石陷阱和地雷就像一个格外危险的捕熊陷阱:

{
    "name" : "Stonefall Trap",
    "renderable": {
        "glyph" : "^",
        "fg" : "#FF0000",
        "bg" : "#000000",
        "order" : 2
    },
    "hidden" : true,
    "entry_trigger" : {
        "effects" : {
            "damage" : "12",
            "single_activation" : "1"
        }
    }
},

{
    "name" : "Landmine",
    "renderable": {
        "glyph" : "^",
        "fg" : "#FF0000",
        "bg" : "#000000",
        "order" : 2
    },
    "hidden" : true,
    "entry_trigger" : {
        "effects" : {
            "damage" : "18",
            "single_activation" : "1"
        }
    }
},

矮人战利品

这些只是 spawns.jsonitems 部分的更多物品:

{
    "name" : "Breastplate",
    "renderable": {
        "glyph" : "[",
        "fg" : "#00FF00",
        "bg" : "#000000",
        "order" : 2
    },
    "wearable" : {
        "slot" : "Torso",
        "armor_class" : 3.0
    },
    "weight_lbs" : 25.0,
    "base_value" : 100.0,
    "initiative_penalty" : 2.0,
    "vendor_category" : "armor"
},

{
    "name" : "Dwarf-Steel Shirt",
    "renderable": {
        "glyph" : "[",
        "fg" : "#00FF00",
        "bg" : "#000000",
        "order" : 2
    },
    "wearable" : {
        "slot" : "Torso",
        "armor_class" : 3.0
    },
    "weight_lbs" : 5.0,
    "base_value" : 500.0,
    "initiative_penalty" : 0.0,
    "vendor_category" : "armor"
},
{
    "name" : "War Axe",
    "renderable": {
        "glyph" : "¶",
        "fg" : "#FF55FF",
        "bg" : "#000000",
        "order" : 2
    },
    "weapon" : {
        "range" : "melee",
        "attribute" : "might",
        "base_damage" : "1d12",
        "hit_bonus" : 0
    },
    "weight_lbs" : 4.0,
    "base_value" : 100.0,
    "initiative_penalty" : 2,
    "vendor_category" : "weapon"
},

总结

这个关卡仍然有点太容易杀死你,但它有效。我们将在接下来的章节中使事情变得更容易一些,所以我们暂时将难度保持在“钢铁侠”级别!

...

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

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

版权 (C) 2019, Herbert Wolverson。