广场之夜
关于本教程
本教程是免费且开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用。 我希望您会喜欢本教程,并制作出色的游戏!
如果您喜欢这个教程并希望我继续写作,请考虑支持 我的 Patreon。
城市关卡被特意设计得很混乱:英雄在狭窄、蔓延的黑暗精灵地下城市中战斗——面对不同贵族家族的部队,他们也一心想杀死彼此。 这使得战斗节奏快、紧张。 城市的最后一部分是广场——旨在提供更多的对比。 城市中的公园里有一个通往深渊的传送门,只有最富有/最有影响力的黑暗精灵才能在这里建造。 因此,尽管身处地下,但它更像是一个户外城市的感觉。
所以让我们思考一下构成广场关卡的要素:
- 一个规模适中的公园,由一些强大的坏蛋守卫。 我们可以第一次在这里添加一些恶魔,因为我们就位于通往他们家园的传送门旁边。
- 一些较大的建筑物。
- 雕像、喷泉和类似的装饰品。
继续思考黑暗精灵,他们实际上并不以城市规划而闻名。 从本质上讲,它们是混乱的物种。 因此,我们希望避免让他们感觉他们真的规划好了自己的城市,并一丝不苟地建造它使其有意义。 事实上,不合逻辑反而增加了超现实感。
生成广场
就像我们为其他关卡构建器所做的那样,我们需要为关卡 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。