广场之夜


关于本教程

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

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

Hands-On Rust


城市关卡被特意设计得很混乱:英雄在狭窄、蔓延的黑暗精灵地下城市中战斗——面对不同贵族家族的部队,他们也一心想杀死彼此。 这使得战斗节奏快、紧张。 城市的最后一部分是广场——旨在提供更多的对比。 城市中的公园里有一个通往深渊的传送门,只有最富有/最有影响力的黑暗精灵才能在这里建造。 因此,尽管身处地下,但它更像是一个户外城市的感觉。

所以让我们思考一下构成广场关卡的要素:

  • 一个规模适中的公园,由一些强大的坏蛋守卫。 我们可以第一次在这里添加一些恶魔,因为我们就位于通往他们家园的传送门旁边。
  • 一些较大的建筑物。
  • 雕像、喷泉和类似的装饰品。

继续思考黑暗精灵,他们实际上并不以城市规划而闻名。 从本质上讲,它们是混乱的物种。 因此,我们希望避免让他们感觉他们真的规划好了自己的城市,并一丝不苟地建造它使其有意义。 事实上,不合逻辑反而增加了超现实感。

生成广场

就像我们为其他关卡构建器所做的那样,我们需要为关卡 11 添加一个占位符构建器。 打开 map_builders/mod.rs 并为关卡 11 添加对 dark_elf_plaza 的调用:

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

现在打开 map_builders/dark_elves.rs 并创建新的地图构建器函数——dark_elf_plaza。 我们将从生成 BSP 内部地图开始; 这将会改变,但先让它编译通过总是好的:

#![allow(unused)]
fn main() {
pub fn dark_elf_plaza(new_depth: i32, width: i32, height: i32) -> BuilderChain {
    println!("Dark elf plaza builder");
    let mut chain = BuilderChain::new(new_depth, width, height, "Dark Elven Plaza");
    chain.start_with(BspInteriorBuilder::new());
    chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::CENTER));
    chain.with(CullUnreachable::new());
    chain.with(AreaEndingPosition::new(XEnd::RIGHT, YEnd::CENTER));
    chain.with(VoronoiSpawning::new());
    chain
}
}

刻意糟糕的城市规划

既然我们有了和上一个关卡完全相同的地图,那么让我们构建一个生成器来创建广场。 我们将从制作一个无聊的空地图开始——只是为了验证我们的地图构建器是否正常工作。 在 dark_elves.rs 的末尾,粘贴以下代码:

#![allow(unused)]
fn main() {
// Plaza Builder
use super::{InitialMapBuilder, BuilderMap, TileType };

pub struct PlazaMapBuilder {}

impl InitialMapBuilder for PlazaMapBuilder {
    #[allow(dead_code)]
    fn build_map(&mut self, build_data : &mut BuilderMap) {
        self.empty_map(build_data);
    }
}

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

    fn empty_map(&mut self, build_data : &mut BuilderMap) {
        build_data.map.tiles.iter_mut().for_each(|t| *t = TileType::Floor);
    }
}
}

您还需要进入 dark_elf_plaza 并更改初始构建器以使用它:

#![allow(unused)]
fn main() {
pub fn dark_elf_plaza(new_depth: i32, width: i32, height: i32) -> BuilderChain {
    println!("Dark elf plaza builder");
    let mut chain = BuilderChain::new(new_depth, width, height, "Dark Elven Plaza");
    chain.start_with(PlazaMapBuilder::new());
    chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::CENTER));
    chain.with(CullUnreachable::new());
    chain.with(AreaEndingPosition::new(XEnd::RIGHT, YEnd::CENTER));
    chain.with(VoronoiSpawning::new());
    chain
}
}

如果您现在运行游戏并传送至最后一关,“广场”将是一个巨大的开放空间,到处都是互相残杀的人。 我觉得这很有趣,但这不是我们想要的。

广场需要划分为区域,这些区域包含广场内容。 这类似于我们对 Voronoi 地图所做的,但我们不是要创建蜂窝墙——只是放置内容的区域。 让我们从创建一个基本的 Voronoi 单元区域开始。 扩展您的地图构建器以调用一个名为 spawn_zones 的新函数:

#![allow(unused)]
fn main() {
impl InitialMapBuilder for PlazaMapBuilder {
    #[allow(dead_code)]
    fn build_map(&mut self, build_data : &mut BuilderMap) {
        self.empty_map(build_data);
        self.spawn_zones(build_data);
    }
}
}

我们将从采用之前的 Voronoi 代码开始,并使其始终具有 32 个种子并使用毕达哥拉斯距离:

#![allow(unused)]
fn main() {
fn spawn_zones(&mut self, build_data : &mut BuilderMap) {
    let mut voronoi_seeds : Vec<(usize, rltk::Point)> = Vec::new();

    while voronoi_seeds.len() < 32 {
        let vx = crate::rng::roll_dice(1, build_data.map.width-1);
        let vy = crate::rng::roll_dice(1, build_data.map.height-1);
        let vidx = build_data.map.xy_idx(vx, vy);
        let candidate = (vidx, rltk::Point::new(vx, vy));
        if !voronoi_seeds.contains(&candidate) {
            voronoi_seeds.push(candidate);
        }
    }

    let mut voronoi_distance = vec![(0, 0.0f32) ; 32];
    let mut voronoi_membership : Vec<i32> = vec![0 ; build_data.map.width as usize * build_data.map.height as usize];
    for (i, vid) in voronoi_membership.iter_mut().enumerate() {
        let x = i as i32 % build_data.map.width;
        let y = i as i32 / build_data.map.width;

        for (seed, pos) in voronoi_seeds.iter().enumerate() {
            let distance = rltk::DistanceAlg::PythagorasSquared.distance2d(
                rltk::Point::new(x, y),
                pos.1
            );
            voronoi_distance[seed] = (seed, distance);
        }

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

        *vid = voronoi_distance[0].0 as i32;
    }

    // Spawning code will go here
    // 生成代码将放在这里
}
}

在新的 spawn_zones 函数的末尾,我们有一个名为 voronoi_membership 的数组,它将每个地块分类为 32 个区域之一。 这些区域保证是连续的。 让我们编写一些快速代码来计算每个区域的大小,以验证我们的工作:

#![allow(unused)]
fn main() {
// Make a list of zone sizes and cull empty ones
// 制作区域大小列表并剔除空区域
let mut zone_sizes : Vec<(i32, usize)> = Vec::with_capacity(32);
for zone in 0..32 {
    let num_tiles = voronoi_membership.iter().filter(|z| **z == zone).count();
    if num_tiles > 0 {
        zone_sizes.push((zone, num_tiles));
    }
}
println!("{:?}", zone_sizes);
}

这将每次给出不同的结果,但会很好地了解我们创建了多少区域以及它们有多大。 这是一个快速测试运行的输出:

[(0, 88), (1, 60), (2, 143), (3, 261), (4, 192), (5, 165), (6, 271), (7, 68), (8, 151), (9, 78), (10, 45), (11, 154), (12, 132), (13, 88), (14, 162), (15, 49), (16, 138), (17, 57), (18, 206), (19, 117), (20, 168), (21, 67), (22, 153), (23, 119), (24, 41), (25, 48), (26, 78), (27, 118), (28, 197), (29, 129), (30, 163), (31, 94)]

所以我们知道区域创建工作正常:有 32 个区域,没有一个区域过小——尽管有些区域相当大。 让我们按大小降序对列表进行排序:

#![allow(unused)]
fn main() {
zone_sizes.sort_by(|a,b| b.1.cmp(&a.1));
}

这产生了加权的“重要性”地图:大区域在前,小区域在后。 我们将使用它来按重要性顺序生成内容。 大型“传送门公园”保证是最大的区域。 这是我们创建系统的开始:

#![allow(unused)]
fn main() {
// Start making zonal terrain
// 开始制作区域地形
zone_sizes.iter().enumerate().for_each(|(i, (zone, _))| {
    match i {
        0 => self.portal_park(build_data, &voronoi_membership, *zone),
        _ => {}
    }
});
}

portal_park 的占位符签名如下:

#![allow(unused)]
fn main() {
fn portal_park(&mut self, build_data : &mut BuilderMap, voronoi_membership: &[i32], zone: i32) {
}
}

我们将使用这种模式逐步填充广场。 现在,我们将跳过传送门公园,先添加一些其他功能。

坚固的岩石

让我们从最简单的开始:我们将把一些较小的区域变成坚固的岩石。 这些可能是精灵尚未开采的区域,或者——更有可能——他们将它们留在原位以支撑洞穴。 我们将使用一个以前没有接触过的功能:“匹配守卫”。 您可以使 match 适用于“大于”,如下所示:

#![allow(unused)]
fn main() {
// Start making zonal terrain
// 开始制作区域地形
zone_sizes.iter().enumerate().for_each(|(i, (zone, _))| {
    match i {
        0 => self.portal_park(build_data, &voronoi_membership, *zone),
        i if i > 20 => self.fill_zone(build_data, &voronoi_membership, *zone, TileType::Wall),
        _ => {}
    }
});
}

实际的 fill_zone 函数非常简单:它查找区域中的地块并将它们变成墙壁:

#![allow(unused)]
fn main() {
fn fill_zone(&mut self, build_data : &mut BuilderMap, voronoi_membership: &[i32], zone: i32, tile_type: TileType) {
        voronoi_membership
            .iter()
            .enumerate()
            .filter(|(_, tile_zone)| **tile_zone == zone)
            .for_each(|(idx, _)| build_data.map.tiles[idx] = tile_type);
    }
}

这已经为我们的地图注入了一些活力:

水池

洞穴往往是阴暗潮湿的地方。 黑暗精灵可能喜欢一些水池——广场以壮丽的水池而闻名! 让我们扩展“默认”匹配,有时创建区域水池:

#![allow(unused)]
fn main() {
// Start making zonal terrain
// 开始制作区域地形
zone_sizes.iter().enumerate().for_each(|(i, (zone, _))| {
    match i {
        0 => self.portal_park(build_data, &voronoi_membership, *zone),
        i if i > 20 => self.fill_zone(build_data, &voronoi_membership, *zone, TileType::Wall),
        _ => {
            let roll = crate::rng::roll_dice(1, 6);
            match roll {
                1 => self.fill_zone(build_data, &voronoi_membership, *zone, TileType::DeepWater),
                2 => self.fill_zone(build_data, &voronoi_membership, *zone, TileType::ShallowWater),
                _ => {}
            }
        }
    }
});
}

看看如果我们没有匹配任何其他内容,我们是如何掷骰子的? 如果掷出 1 或 2,我们会添加一个深度不同的水池。 实际上添加水池就像添加坚固的岩石一样——但我们添加的是水而不是岩石。

一些水景的加入继续为区域带来生机:

钟乳石公园

钟乳石(以及据推测的它们的孪生兄弟,石笋)可以是真实洞穴的美丽特征。 它们是黑暗精灵公园中包含的天然候选者。 如果城市里能有一些色彩就太好了,所以让我们用草地环绕它们。 它们是一个精心培育的公园,为黑暗精灵在业余时间所做的任何事情提供隐私(你不会想知道的......)。

将其添加到“未知”区域选项中:

#![allow(unused)]
fn main() {
// Start making zonal terrain
// 开始制作区域地形
zone_sizes.iter().enumerate().for_each(|(i, (zone, _))| {
    match i {
        0 => self.portal_park(build_data, &voronoi_membership, *zone),
        i if i > 20 => self.fill_zone(build_data, &voronoi_membership, *zone, TileType::Wall),
        _ => {
            let roll = crate::rng::roll_dice(1, 6);
            match roll {
                1 => self.fill_zone(build_data, &voronoi_membership, *zone, TileType::DeepWater),
                2 => self.fill_zone(build_data, &voronoi_membership, *zone, TileType::ShallowWater),
                3 => self.stalactite_display(build_data, &voronoi_membership, *zone),
                _ => {}
            }
        }
    }
});
}

并使用类似于 fill_zone 的函数来填充区域中的每个地块,可以是草地或钟乳石:

#![allow(unused)]
fn main() {
fn stalactite_display(&mut self, build_data : &mut BuilderMap, voronoi_membership: &[i32], zone: i32) {
        voronoi_membership
            .iter()
            .enumerate()
            .filter(|(_, tile_zone)| **tile_zone == zone)
            .for_each(|(idx, _)| {
                build_data.map.tiles[idx] = match crate::rng::roll_dice(1,10) {
                    1 => TileType::Stalactite,
                    2 => TileType::Stalagmite,
                    _ => TileType::Grass,
                };
            });
    }
}

公园和献祭区

一些植被覆盖的区域,加上座位,增加了公园的感觉。 这些应该是较大的区域——那是该区域的主要主题。 我真的不认为黑暗精灵会坐在那里听一场美好的音乐会---所以让我们在中间放一个祭坛,上面布满血迹。 请注意,我们如何在 match 中使用“或”语句来匹配第二大和第三大区域:

#![allow(unused)]
fn main() {
// Start making zonal terrain
// 开始制作区域地形
zone_sizes.iter().enumerate().for_each(|(i, (zone, _))| {
    match i {
        0 => self.portal_park(build_data, &voronoi_membership, *zone),
        1 | 2 => self.park(build_data, &voronoi_membership, *zone),
        i if i > 20 => self.fill_zone(build_data, &voronoi_membership, *zone, TileType::Wall),
        _ => {
            let roll = crate::rng::roll_dice(1, 6);
            match roll {
                1 => self.fill_zone(build_data, &voronoi_membership, *zone, TileType::DeepWater),
                2 => self.fill_zone(build_data, &voronoi_membership, *zone, TileType::ShallowWater),
                3 => self.stalactite_display(build_data, &voronoi_membership, *zone),
                _ => {}
            }
        }
    }
});
}

实际填充公园稍微复杂一些:

#![allow(unused)]
fn main() {
fn park(&mut self, build_data : &mut BuilderMap, voronoi_membership: &[i32], zone: i32, seeds: &[(usize, rltk::Point)]) {
    let zone_tiles : Vec<usize> = voronoi_membership
        .iter()
        .enumerate()
        .filter(|(_, tile_zone)| **tile_zone == zone)
        .map(|(idx, _)| idx)
        .collect();

    // Start all grass
    // 开始全部铺上草地
    zone_tiles.iter().for_each(|idx| build_data.map.tiles[*idx] = TileType::Grass);

    // Add a stone area in the middle
    // 在中间添加一个石头区域
    let center = seeds[zone as usize].1;
    for y in center.y-2 ..= center.y+2 {
        for x in center.x-2 ..= center.x+2 {
            let idx = build_data.map.xy_idx(x, y);
            build_data.map.tiles[idx] = TileType::Road;
            if crate::rng::roll_dice(1,6) > 2 {
                build_data.map.bloodstains.insert(idx);
            }
        }
    }

    // With an altar at the center
    // 中心有一个祭坛
    build_data.spawn_list.push((
        build_data.map.xy_idx(center.x, center.y),
        "Altar".to_string()
    ));

    // And chairs for spectators
    // 以及供观众就座的椅子
    zone_tiles.iter().for_each(|idx| {
        if build_data.map.tiles[*idx] == TileType::Grass && crate::rng::roll_dice(1, 6)==1 {
            build_data.spawn_list.push((
                *idx,
                "Chair".to_string()
            ));
        }
    });
}
}

我们首先收集可用地块的列表。 然后我们用漂亮的草地覆盖它们。 找到 Voronoi 区域的中心点(它将是生成它的种子),并用道路覆盖该区域。 在中间生成一个祭坛,一些随机的血迹和一堆椅子。 这都是我们之前做过的生成——但汇集在一起形成一个(并非完全令人愉悦的)主题公园。

公园区域看起来足够混乱:

添加走道

此时,不能保证您实际上可以穿过地图。 水和墙壁完全有可能以错误的方式重合,从而阻碍您的前进。 这可不好! 让我们使用我们在创建第一个 Voronoi 构建器时遇到的系统来识别 Voronoi 区域之间的边缘——并将边缘地块替换为道路。 这确保了区域之间有通道,并在地图上产生漂亮的蜂窝效果。

首先在 spawn_zones 的末尾添加一个调用,调用道路构建器:

#![allow(unused)]
fn main() {
// Clear the path
// 清理路径
self.make_roads(build_data, &voronoi_membership);
}

现在我们实际上必须建造一些道路。 大部分代码与 Voronoi 边缘检测相同。 我们不是在区域内放置地板,而是在边缘放置道路:

#![allow(unused)]
fn main() {
fn make_roads(&mut self, build_data : &mut BuilderMap, voronoi_membership: &[i32]) {
    for y in 1..build_data.map.height-1 {
        for x in 1..build_data.map.width-1 {
            let mut neighbors = 0;
            let my_idx = build_data.map.xy_idx(x, y);
            let my_seed = voronoi_membership[my_idx];
            if voronoi_membership[build_data.map.xy_idx(x-1, y)] != my_seed { neighbors += 1; }
            if voronoi_membership[build_data.map.xy_idx(x+1, y)] != my_seed { neighbors += 1; }
            if voronoi_membership[build_data.map.xy_idx(x, y-1)] != my_seed { neighbors += 1; }
            if voronoi_membership[build_data.map.xy_idx(x, y+1)] != my_seed { neighbors += 1; }

            if neighbors > 1 {
                build_data.map.tiles[my_idx] = TileType::Road;
            }
        }
    }
}
}

有了这个功能,地图就可以通行了。 道路划定了边缘,看起来又不太方正:

清理生成物

目前,地图非常混乱——并且很可能很快杀死你。 有很大的开放区域,到处都是坏人、陷阱(你为什么要在一个公园里建造陷阱?),以及散落的物品。 混乱是好的,但随机性太多也是不好的。 我们希望地图具有某种意义——以一种随机的方式。

让我们首先从构建器链中完全删除随机实体生成器:

#![allow(unused)]
fn main() {
pub fn dark_elf_plaza(new_depth: i32, width: i32, height: i32) -> BuilderChain {
    println!("Dark elf plaza builder");
    let mut chain = BuilderChain::new(new_depth, width, height, "Dark Elven Plaza");
    chain.start_with(PlazaMapBuilder::new());
    chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::CENTER));
    chain.with(CullUnreachable::new());
    chain.with(AreaEndingPosition::new(XEnd::RIGHT, YEnd::CENTER));
    chain
}
}

这为您提供了一个没有敌人的地图,尽管它仍然有一些椅子和祭坛。 这是一个“主题公园”地图——所以我们将保留对给定区域中生成内容的某些控制。 它对玩家的帮助很少——我们快到最后了,所以希望他们已经储备了物资!

让我们首先在公园/祭坛区域放置一些怪物。 一个黑暗精灵家族或其他家族在那里,导致成群的敌人。 现在找到 park 函数,我们将扩展“添加椅子”部分:

#![allow(unused)]
fn main() {
// And chairs for spectators, and the spectators themselves
// 以及供观众就座的椅子,以及观众本身
let available_enemies = match crate::rng::roll_dice(1, 3) {
    1 => vec![
        "Arbat Dark Elf",
        "Arbat Dark Elf Leader",
        "Arbat Orc Slave",
    ],
    2 => vec![
        "Barbo Dark Elf",
        "Barbo Goblin Archer",
    ],
    _ => vec![
        "Cirro Dark Elf",
        "Cirro Dark Priestess",
        "Cirro Spider",
    ]
};

zone_tiles.iter().for_each(|idx| {
    if build_data.map.tiles[*idx] == TileType::Grass {
            match crate::rng::roll_dice(1, 10) {
            1 => build_data.spawn_list.push((
                    *idx,
                    "Chair".to_string()
                )),
            2 => {
                let to_spawn = crate::rng::range(0, available_enemies.len() as i32);
                build_data.spawn_list.push((
                    *idx,
                    available_enemies[to_spawn as usize].to_string()
                ));
            }
            _ => {}
        }
    }
});
}

我们在这里做一些新的事情。 我们随机为公园分配一个所有者——A、B 或 C 组黑暗精灵。 然后我们为每个组制作一个可用的生成列表,并在该公园中生成一些。 这确保了公园开始时由一个派系拥有。 由于他们经常可以看到彼此,屠杀将开始——但至少这是主题屠杀。

我们将让钟乳石画廊和水池空无一人,没有敌人。 它们只是装饰,并提供一个安静的区域来隐藏/休息(看到了吗?我们并没有完全不公平!)。

传送门公园

既然我们已经确定了地图的基本形状,那么是时候关注公园了。 首先要做的是阻止出口随机生成。 更改基本地图构建器以不包括出口放置:

#![allow(unused)]
fn main() {
pub fn dark_elf_plaza(new_depth: i32, width: i32, height: i32) -> BuilderChain {
    println!("Dark elf plaza builder");
    let mut chain = BuilderChain::new(new_depth, width, height, "Dark Elven Plaza");
    chain.start_with(PlazaMapBuilder::new());
    chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::CENTER));
    chain.with(CullUnreachable::new());
    chain
}
}

这让您完全没有出口。 我们想把它放在传送门公园的中间。 让我们扩展函数签名以包含 Voronoi 种子,并使用种子点放置出口——就像我们对其他公园所做的那样:

#![allow(unused)]
fn main() {
fn portal_park(&mut self, build_data : &mut BuilderMap, voronoi_membership: &[i32], zone: i32, seeds: &[(usize, rltk::Point)]) {
    let center = seeds[zone as usize].1;
    let idx = build_data.map.xy_idx(center.x, center.y);
    build_data.map.tiles[idx] = TileType::DownStairs;
}
}

现在,让我们通过用砾石覆盖传送门公园来使其更加突出:

#![allow(unused)]
fn main() {
fn portal_park(&mut self, build_data : &mut BuilderMap, voronoi_membership: &[i32], zone: i32, seeds: &[(usize, rltk::Point)]) {
    let zone_tiles : Vec<usize> = voronoi_membership
        .iter()
        .enumerate()
        .filter(|(_, tile_zone)| **tile_zone == zone)
        .map(|(idx, _)| idx)
        .collect();

    // Start all gravel
    // 开始全部铺上砾石
    zone_tiles.iter().for_each(|idx| build_data.map.tiles[*idx] = TileType::Gravel);

    // Add the exit
    // 添加出口
    let center = seeds[zone as usize].1;
    let idx = build_data.map.xy_idx(center.x, center.y);
    build_data.map.tiles[idx] = TileType::DownStairs;
}
}

接下来,我们将在出口周围添加一些祭坛:

#![allow(unused)]
fn main() {
// Add some altars around the exit
// 在出口周围添加一些祭坛
let altars = [
    build_data.map.xy_idx(center.x - 2, center.y),
    build_data.map.xy_idx(center.x + 2, center.y),
    build_data.map.xy_idx(center.x, center.y - 2),
    build_data.map.xy_idx(center.x, center.y + 2),
];
altars.iter().for_each(|idx| build_data.spawn_list.push((*idx, "Altar".to_string())));
}

这为深渊的出口提供了一个非常好的开始。 您在正确的位置有出口、令人毛骨悚然的祭坛和清晰标记的入口。 它也避免了风险(除了精灵在地图上到处互相残杀)。

让我们通过在出口处添加一场 Boss 战来使出口更具挑战性。 这是进入深渊之前的最后一次重大推进,因此它是 Boss 战的自然地点。 我随机生成了一个恶魔的名字,并决定将 Boss 命名为“沃科斯 (Vokoth)”。 让我们把它生成在出口旁边的一个地块上:

#![allow(unused)]
fn main() {
let demon_spawn = build_data.map.xy_idx(center.x+1, center.y+1);
build_data.spawn_list.push((demon_spawn, "Vokoth".to_string()));
}

在我们定义沃科斯之前,这不会有任何作用! 我们想要一个强大的坏蛋。 让我们在 spawns.json 中快速回顾一下,并提醒自己我们是如何定义黑龙的:

{
    "name" : "Black Dragon",
    "renderable": {
        "glyph" : "D",
        "fg" : "#FF0000",
        "bg" : "#000000",
        "order" : 1,
        "x_size" : 2,
        "y_size" : 2
    },
    "blocks_tile" : true,
    "vision_range" : 12,
    "movement" : "static",
    "attributes" : {
        "might" : 13,
        "fitness" : 13
    },
    "skills" : {
        "Melee" : 18,
        "Defense" : 16
    },
    "natural" : {
        "armor_class" : 17,
        "attacks" : [
            { "name" : "bite", "hit_bonus" : 4, "damage" : "1d10+2" },
            { "name" : "left_claw", "hit_bonus" : 2, "damage" : "1d10" },
            { "name" : "right_claw", "hit_bonus" : 2, "damage" : "1d10" }
        ]
    },
    "loot_table" : "Wyrms",
    "faction" : "Wyrm",
    "level" : 6,
    "gold" : "20d10",
    "abilities" : [
        { "spell" : "Acid Breath", "chance" : 0.2, "range" : 8.0, "min_range" : 2.0 }
    ]
},

那是一个非常强大的怪物,是深渊恶魔的良好模板。 让我们克隆它(复制/粘贴时间!)并为沃科斯构建一个条目:

{
    "name" : "Vokoth",
    "renderable": {
        "glyph" : "&",
        "fg" : "#FF0000",
        "bg" : "#000000",
        "order" : 1,
        "x_size" : 2,
        "y_size" : 2
    },
    "blocks_tile" : true,
    "vision_range" : 6,
    "movement" : "static",
    "attributes" : {
        "might" : 13,
        "fitness" : 13
    },
    "skills" : {
        "Melee" : 18,
        "Defense" : 16
    },
    "natural" : {
        "armor_class" : 17,
        "attacks" : [
            { "name" : "whip", "hit_bonus" : 4, "damage" : "1d10+2" }
        ]
    },
    "loot_table" : "Wyrms",
    "faction" : "Wyrm",
    "level" : 8,
    "gold" : "20d10",
    "abilities" : []
}

现在,如果您玩游戏,您会发现自己在深渊出口处面对着一个讨厌的恶魔怪物。

总结

我们现在完成了倒数第二个部分! 您可以一路战斗到黑暗精灵广场,并找到通往深渊的门户——但前提是您能够避开一个笨重的恶魔和一大群精灵——并且几乎没有提供任何帮助。 接下来,我们将开始构建深渊。


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

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

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