影响属性的物品和更好的状态效果
关于本教程
本教程是免费和开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用。我希望您会喜欢本教程,并制作出色的游戏!
如果您喜欢这个教程并希望我继续写作,请考虑支持我的 Patreon。
我们仍然有一些常见的物品类型尚未支持。本章将完成这些,并为施法(在下一章中)奠定基础框架。
提升属性的物品
在类 D&D 游戏中,一种常见的物品类型是增强(或降低!)你的属性的物品。 例如,食人魔力量手套 赋予力量加值,而 巫师之帽 赋予智力加值。 我们已经有了支持这些物品的大部分框架,所以让我们完成最后一步,使它们能够工作! 打开 spawns.json
,我们将定义手套可能的样子:
{
"name" : "食人魔力量手套",
"renderable": {
"glyph" : "[",
"fg" : "#00FF00",
"bg" : "#000000",
"order" : 2
},
"wearable" : {
"slot" : "Hands",
"armor_class" : 0.1,
"might" : 5
},
"weight_lbs" : 1.0,
"base_value" : 300.0,
"initiative_penalty" : 0.0,
"vendor_category" : "armor",
"magic" : { "class" : "common", "naming" : "未鉴定的手套" },
"attributes" : { "might" : 5 }
}
为什么我们不直接将其添加到 "wearable" 中? 我们可能希望为其他事物提供属性提升! 为了支持加载这个 - 和其他属性提升 - 我们需要编辑 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>, pub attributes : Option<ItemAttributeBonus> } ... #[derive(Deserialize, Debug)] pub struct ItemAttributeBonus { pub might : Option<i32>, pub fitness : Option<i32>, pub quickness : Option<i32>, pub intelligence : Option<i32> } }
和之前一样,我们需要一个 component 来支持这些数据。 在 components.rs
中(并在 main.rs
和 saveload_system.rs
中注册):
#![allow(unused)] fn main() { #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct AttributeBonus { pub might : Option<i32>, pub fitness : Option<i32>, pub quickness : Option<i32>, pub intelligence : Option<i32> } }
我们将修改 raws/rawmaster.rs
的函数 spawn_named_item
以支持添加此 component 类型:
#![allow(unused)] fn main() { if let Some(ab) = &item_template.attributes { eb = eb.with(AttributeBonus{ might : ab.might, fitness : ab.fitness, quickness : ab.quickness, intelligence : ab.intelligence, }); } }
现在 component 可以应用于物品了,让我们将其放入生成表 (spawn table) 中,使其非常常见以便于测试:
{ "name" : "食人魔力量手套", "weight" : 100, "min_depth" : 0, "max_depth" : 100 },
最后,我们需要让它真正发挥作用。 我们在 ai/encumbrance_system.rs
中做了非常相似的事情 - 所以这是放置它的自然位置。 我们将向系统添加很多内容,所以这里是整个系统:
#![allow(unused)] fn main() { use specs::prelude::*; use crate::{EquipmentChanged, Item, InBackpack, Equipped, Pools, Attributes, gamelog::GameLog, AttributeBonus, gamesystem::attr_bonus}; use std::collections::HashMap; pub struct EncumbranceSystem {} impl<'a> System<'a> for EncumbranceSystem { #[allow(clippy::type_complexity)] type SystemData = ( WriteStorage<'a, EquipmentChanged>, Entities<'a>, ReadStorage<'a, Item>, ReadStorage<'a, InBackpack>, ReadStorage<'a, Equipped>, WriteStorage<'a, Pools>, WriteStorage<'a, Attributes>, ReadExpect<'a, Entity>, WriteExpect<'a, GameLog>, ReadStorage<'a, AttributeBonus> ); fn run(&mut self, data : Self::SystemData) { let (mut equip_dirty, entities, items, backpacks, wielded, mut pools, mut attributes, player, mut gamelog, attrbonus) = data; if equip_dirty.is_empty() { return; } struct ItemUpdate { weight : f32, initiative : f32, might : i32, fitness : i32, quickness : i32, intelligence : i32 } // Build the map of who needs updating let mut to_update : HashMap<Entity, ItemUpdate> = HashMap::new(); // (weight, intiative) for (entity, _dirty) in (&entities, &equip_dirty).join() { to_update.insert(entity, ItemUpdate{ weight: 0.0, initiative: 0.0, might: 0, fitness: 0, quickness: 0, intelligence: 0 }); } // Remove all dirty statements equip_dirty.clear(); // Total up equipped items for (item, equipped, entity) in (&items, &wielded, &entities).join() { if to_update.contains_key(&equipped.owner) { let totals = to_update.get_mut(&equipped.owner).unwrap(); totals.weight += item.weight_lbs; totals.initiative += item.initiative_penalty; if let Some(attr) = attrbonus.get(entity) { totals.might += attr.might.unwrap_or(0); totals.fitness += attr.fitness.unwrap_or(0); totals.quickness += attr.quickness.unwrap_or(0); totals.intelligence += attr.intelligence.unwrap_or(0); } } } // Total up carried items for (item, carried, entity) in (&items, &backpacks, &entities).join() { if to_update.contains_key(&carried.owner) { let totals = to_update.get_mut(&carried.owner).unwrap(); totals.weight += item.weight_lbs; totals.initiative += item.initiative_penalty; } } // Apply the data to Pools for (entity, item) in to_update.iter() { if let Some(pool) = pools.get_mut(*entity) { pool.total_weight = item.weight; pool.total_initiative_penalty = item.initiative; if let Some(attr) = attributes.get_mut(*entity) { attr.might.modifiers = item.might; attr.fitness.modifiers = item.fitness; attr.quickness.modifiers = item.quickness; attr.intelligence.modifiers = item.intelligence; attr.might.bonus = attr_bonus(attr.might.base + attr.might.modifiers); attr.fitness.bonus = attr_bonus(attr.fitness.base + attr.fitness.modifiers); attr.quickness.bonus = attr_bonus(attr.quickness.base + attr.quickness.modifiers); attr.intelligence.bonus = attr_bonus(attr.intelligence.base + attr.intelligence.modifiers); let carry_capacity_lbs = (attr.might.base + attr.might.modifiers) * 15; if pool.total_weight as i32 > carry_capacity_lbs { // Overburdened pool.total_initiative_penalty += 4.0; if *entity == *player { gamelog.entries.push("你超负重了,并受到了先攻惩罚。".to_string()); } } } } } } } }
这与之前的逻辑基本相同,但我们更改了很多内容:
- 我们不再使用元组来保存重量和先攻效果,而是添加了一个
struct
来保存我们想要累加的所有内容。Rust 很棒,如果你只需要使用一次 struct,你可以在函数内部声明它! - 和以前一样,我们累加所有物品的重量,以及已装备物品的先攻惩罚。
- 如果物品具有属性加成/惩罚,我们也会累加每个物品的属性加成/惩罚。
- 然后我们将它们应用于属性的
modifiers
部分,并重新计算加值 (bonuses)。
最棒的是,由于使用这些属性的其他系统已经在查看加值(并且 GUI 正在查看 modifiers 以进行显示),很多东西 就能正常工作 (而且并非完全是 Bethesda 意义上的短语……是的,我实际上很喜欢 Fallout 76,但如果事情真的可以正常工作就好了!)。
现在,如果你 cargo run
该项目,你可以找到 食人魔力量手套 并装备它们以获得加值 - 然后移除它们以取消加值:
充能物品
并非所有物品在使用后都会化为尘土。一个药水瓶可能装有多剂药剂,一根魔法棒可能多次施放其效果(像往常一样,你的想象力是无限的!)。 让我们制作一件新物品,火球法杖。 在 spawns.json
中,我们将定义基本属性;它基本上是一个火球卷轴,但带有充能:
{
"name" : "火球法杖",
"renderable": {
"glyph" : "/",
"fg" : "#FFAAAA",
"bg" : "#000000",
"order" : 2
},
"consumable" : {
"effects" : {
"ranged" : "6",
"damage" : "20",
"area_of_effect" : "3",
"particle" : "▓;#FFA500;200.0"
},
"charges" : 5
},
"weight_lbs" : 0.5,
"base_value" : 500.0,
"vendor_category" : "alchemy",
"magic" : { "class" : "common", "naming" : "未鉴定的法杖" }
}
我们需要扩展 raws/item_structs.rs
中的物品定义以处理新数据:
#![allow(unused)] fn main() { #[derive(Deserialize, Debug)] pub struct Consumable { pub effects : HashMap<String, String>, pub charges : Option<i32> } }
我们还将扩展 components.rs
中的 Consumable
component:
#![allow(unused)] fn main() { #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Consumable { pub max_charges : i32, pub charges : i32 } }
请注意,我们同时存储了最大值和当前值。 这样我们以后就可以允许充能。 我们需要扩展 raws/rawmaster.rs
以应用此信息:
#![allow(unused)] fn main() { if let Some(consumable) = &item_template.consumable { let max_charges = consumable.charges.unwrap_or(1); eb = eb.with(crate::components::Consumable{ max_charges, charges : max_charges }); apply_effects!(consumable.effects, eb); } }
现在我们需要让带有充能的消耗品利用它们。 这意味着如果 max_charges
大于 1,则不会自毁,只有在剩余充能时才会触发,并在使用后减少充能计数。 幸运的是,这很容易在 effects/triggers.rs
的 item_trigger
函数中进行更改:
#![allow(unused)] fn main() { pub fn item_trigger(creator : Option<Entity>, item: Entity, targets : &Targets, ecs: &mut World) { // 检查充能 if let Some(c) = ecs.write_storage::<Consumable>().get_mut(item) { if c.charges < 1 { // 取消 let mut gamelog = ecs.fetch_mut::<GameLog>(); gamelog.entries.push(format!("{} 的充能耗尽了!", ecs.read_storage::<Name>().get(item).unwrap().name)); return; } else { c.charges -= 1; } } // 通过通用系统使用物品 let did_something = event_trigger(creator, item, targets, ecs); // 如果它是一个消耗品,那么它将被删除 if did_something { if let Some(c) = ecs.read_storage::<Consumable>().get(item) { if c.max_charges == 0 { ecs.entities().delete(item).expect("删除失败"); } } } } }
这样你就得到了一个可多次使用的火球法杖! 但是,我们应该有一些方法让玩家知道是否剩余充能 - 以帮助进行物品管理。 毕竟,当你用你的法杖指向强大的巨龙并听到 "噗" 的一声,然后被它吃掉时,真的太糟糕了。 我们将进入 gui.rs
并扩展 get_item_display_name
:
#![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) { if let Some(c) = ecs.read_storage::<Consumable>().get(item) { if c.max_charges > 1 { format!("{} ({})", name.name.clone(), c.charges).to_string() } else { name.name.clone() } } else { name.name.clone() } } else if let Some(obfuscated) = ecs.read_storage::<ObfuscatedName>().get(item) { obfuscated.name.clone() } else { "未鉴定的魔法物品".to_string() } } else { name.name.clone() } } else { "无名物品 (bug)".to_string() } } }
所以这个函数基本上没有改变,但是一旦我们确定物品是魔法的并且已被鉴定,我们就会查看它是否有充能。 如果有,我们将充能数量以括号括起来附加到显示列表中的物品名称。
如果你现在 cargo run
该项目,你可以找到你的 火球法杖 并尽情轰炸,直到充能耗尽:
状态效果
目前,我们正在逐个案例地处理状态效果,并且应用它们相对来说并不常见。 大多数深度 Roguelike 游戏都有 很多 可能的效果 - 从蘑菇引起的幻觉到喝了一些棕色粘液后以超音速移动! 我们之所以将这些留到现在,是因为它们与我们在本章中所做的其他事情完美地结合在一起。
直到本章为止,我们都将 Confusion 作为标签添加到目标 - 并依赖于该标签来存储持续时间。 这实际上不符合 ECS 的精神! 相反,Confusion 是一种实体 效果,它应用于 目标,持续 duration 回合。 像往常一样,检查分类法是弄清楚某些东西应该具有哪些实体/component 组的好方法。 因此,我们将访问 components.rs
并创建两个新的 component (也在 main.rs
和 saveload_system.rs
中注册它们),并修改 Confusion
component 以匹配此项:
#![allow(unused)] fn main() { #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Confusion {} #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Duration { pub turns : i32 } #[derive(Component, Debug, ConvertSaveload, Clone)] pub struct StatusEffect { pub target : Entity } }
同样因为我们正在存储一个 Entity
,我们需要编写一个包装器来保持序列化正常工作:
rust
这很好 - 但我们破坏了一些东西! 所有期望 Confusion
具有 turns
字段的东西现在都在抱怨。
我们将从 raws/rawmaster.rs
开始,将效果与持续时间分开:
#![allow(unused)] fn main() { "confusion" => { $eb = $eb.with(Confusion{}); $eb = $eb.with(Duration{ turns: effect.1.parse::<i32>().unwrap() }); } }
在 effects/triggers.rs
中,我们将使持续时间从效果的 Duration
component 中获取:
#![allow(unused)] fn main() { // Confusion if let Some(confusion) = ecs.read_storage::<Confusion>().get(entity) { if let Some(duration) = ecs.read_storage::<Duration>().get(entity) { add_effect(creator, EffectType::Confusion{ turns : duration.turns }, targets.clone()); did_something = true; } } }
我们将更改 effects/damage.rs
的 confusion 函数以匹配新的方案:
#![allow(unused)] fn main() { pub fn add_confusion(ecs: &mut World, effect: &EffectSpawner, target: Entity) { if let EffectType::Confusion{turns} = &effect.effect_type { ecs.create_entity() .with(StatusEffect{ target }) .with(Confusion{}) .with(Duration{ turns : *turns}) .with(Name{ name : "Confusion".to_string() }) .marked::<SimpleMarker<SerializeMe>>() .build(); } } }
剩下的是 ai/effect_status.rs
。我们将更改此项,使其不再担心持续时间,而只是检查效果的存在 - 如果是 Confusion,则取消目标的 turn:
#![allow(unused)] fn main() { use specs::prelude::*; use crate::{MyTurn, Confusion, RunState, StatusEffect}; use std::collections::HashSet; pub struct TurnStatusSystem {} impl<'a> System<'a> for TurnStatusSystem { #[allow(clippy::type_complexity)] type SystemData = ( WriteStorage<'a, MyTurn>, ReadStorage<'a, Confusion>, Entities<'a>, ReadExpect<'a, RunState>, ReadStorage<'a, StatusEffect> ); fn run(&mut self, data : Self::SystemData) { let (mut turns, confusion, entities, runstate, statuses) = data; if *runstate != RunState::Ticking { return; } // Collect a set of all entities whose turn it is let mut entity_turns = HashSet::new(); for (entity, _turn) in (&entities, &turns).join() { entity_turns.insert(entity); } // Find status effects affecting entities whose turn it is let mut not_my_turn : Vec<Entity> = Vec::new(); for (effect_entity, status_effect) in (&entities, &statuses).join() { if entity_turns.contains(&status_effect.target) { // Skip turn for confusion if confusion.get(effect_entity).is_some() { not_my_turn.push(status_effect.target); } } } for e in not_my_turn { turns.remove(e); } } } }
如果你 cargo run
,这将起作用 - 但存在一个明显的题:一旦陷入 confusion,你就会 永远 处于 confusion 状态(或者直到有人把你从痛苦中解脱出来)。 这不太符合我们的想法。 我们已经将效果的持续时间与效果的发生脱钩(这是一件好事!),但这意味着我们必须处理持续时间!
这是一个有趣的难题:状态效果是它们自己的实体,但没有 Initiative。 回合是相对的,因为实体可以以不同的速度运行。 那么我们什么时候想要处理持续时间呢? 答案是 玩家的回合; 时间可能是相对的,但从玩家的角度来看,回合是定义明确的。 实际上,当您被减速时,世界的其余部分都在加速 - 因为我们不想强迫玩家坐着感到无聊,而世界在他们周围缓慢移动。 由于我们正在 ai/initiative_system.rs
中切换到玩家控制 - 我们将在其中处理它:
#![allow(unused)] fn main() { use specs::prelude::*; use crate::{Initiative, Position, MyTurn, Attributes, RunState, Pools, Duration, EquipmentChanged, StatusEffect}; pub struct InitiativeSystem {} impl<'a> System<'a> for InitiativeSystem { #[allow(clippy::type_complexity)] type SystemData = ( WriteStorage<'a, Initiative>, ReadStorage<'a, Position>, WriteStorage<'a, MyTurn>, Entities<'a>, WriteExpect<'a, rltk::RandomNumberGenerator>, ReadStorage<'a, Attributes>, WriteExpect<'a, RunState>, ReadExpect<'a, Entity>, ReadExpect<'a, rltk::Point>, ReadStorage<'a, Pools>, WriteStorage<'a, Duration>, WriteStorage<'a, EquipmentChanged>, ReadStorage<'a, StatusEffect> ); fn run(&mut self, data : Self::SystemData) { let (mut initiatives, positions, mut turns, entities, mut rng, attributes, mut runstate, player, player_pos, pools, mut durations, mut dirty, statuses) = data; if *runstate != RunState::Ticking { return; } // Clear any remaining MyTurn we left by mistkae turns.clear(); // Roll initiative for (entity, initiative, pos) in (&entities, &mut initiatives, &positions).join() { initiative.current -= 1; if initiative.current < 1 { let mut myturn = true; // Re-roll initiative.current = 6 + rng.roll_dice(1, 6); // Give a bonus for quickness if let Some(attr) = attributes.get(entity) { initiative.current -= attr.quickness.bonus; } // Apply pool penalty if let Some(pools) = pools.get(entity) { initiative.current += f32::floor(pools.total_initiative_penalty) as i32; } // TODO: More initiative granting boosts/penalties will go here later // TODO: 稍后将在此处添加更多授予/惩罚先攻的加成 // If its the player, we want to go to an AwaitingInput state // 如果是玩家,我们希望进入 AwaitingInput 状态 if entity == *player { // Give control to the player // 将控制权交给玩家 *runstate = RunState::AwaitingInput; } else { let distance = rltk::DistanceAlg::Pythagoras.distance2d(*player_pos, rltk::Point::new(pos.x, pos.y)); if distance > 20.0 { myturn = false; } } // It's my turn! // 轮到我了! if myturn { turns.insert(entity, MyTurn{}).expect("无法插入 turn"); } } } // Handle durations // 处理持续时间 if *runstate == RunState::AwaitingInput { for (effect_entity, duration, status) in (&entities, &mut durations, &statuses).join() { duration.turns -= 1; if duration.turns < 1 { dirty.insert(status.target, EquipmentChanged{}).expect("无法插入"); entities.delete(effect_entity).expect("无法删除"); } } } } } }
该系统基本保持不变,但我们在不同的 component storage 中添加了一些访问器 - 并在末尾添加了 "Handle durations" 部分。 这只是连接具有持续时间和状态效果的实体,并减少持续时间。 如果持续时间完成,它会将状态的目标标记为 dirty(以便发生任何需要发生的重新计算),并删除状态效果实体。
显示玩家状态
现在我们有了一个通用的状态效果系统,我们应该修改 UI 以显示正在进行的状态。 饥饿的处理方式不同,所以我们将它保留在那里 - 但让我们完成 gui.rs
的那部分。 在 draw_ui
中,将 Status 部分替换为:
#![allow(unused)] fn main() { // Status // 状态 let mut y = 44; let hunger = ecs.read_storage::<HungerClock>(); let hc = hunger.get(*player_entity).unwrap(); match hc.state { HungerState::WellFed => { ctx.print_color(50, y, RGB::named(rltk::GREEN), RGB::named(rltk::BLACK), "Well Fed"); y -= 1; } HungerState::Normal => {} HungerState::Hungry => { ctx.print_color(50, y, RGB::named(rltk::ORANGE), RGB::named(rltk::BLACK), "Hungry"); y -= 1; } HungerState::Starving => { ctx.print_color(50, y, RGB::named(rltk::RED), RGB::named(rltk::BLACK), "Starving"); y -= 1; } } let statuses = ecs.read_storage::<StatusEffect>(); let durations = ecs.read_storage::<Duration>(); let names = ecs.read_storage::<Name>(); for (status, duration, name) in (&statuses, &durations, &names).join() { if status.target == *player_entity { ctx.print_color( 50, y, RGB::named(rltk::RED), RGB::named(rltk::BLACK), &format!("{} ({})", name.name, duration.turns) ); y -= 1; } } }
这与我们之前的情况非常相似,但我们将 y
存储为一个变量 - 因此状态效果列表可以向上增长。 然后我们查询 ECS 以查找具有状态、持续时间和名称的实体 - 如果它是以玩家为目标的,我们就使用它来显示状态。
显示怪物状态
如果能有一些迹象表明状态效果正在应用于 NPC,那也很不错。 这有两个层面 - 我们可以将状态显示在工具提示中,也可以使用粒子效果来指示常规游戏中发生的事情。
为了处理工具提示,打开 gui.rs
并转到 draw_tooltips
函数。 在 "Comment on Pools" 下面,添加以下内容:
#![allow(unused)] fn main() { // Status effects // 状态效果 let statuses = ecs.read_storage::<StatusEffect>(); let durations = ecs.read_storage::<Duration>(); let names = ecs.read_storage::<Name>(); for (status, duration, name) in (&statuses, &durations, &names).join() { if status.target == entity { tip.add(format!("{} ({})", name.name, duration.turns)); } } }
因此,现在如果你让一个怪物陷入 confusion 状态,它会在工具提示中显示效果。 这是一个很好的开始,可以解释为什么怪物没有移动!
另一半是在因 confusion 失去回合时显示粒子效果。 我们将在效果系统中添加一个调用以请求粒子效果! 在 ai/turn_status.rs
中展开 confusion 部分:
#![allow(unused)] fn main() { // Skip turn for confusion // 因 confusion 跳过回合 if confusion.get(effect_entity).is_some() { add_effect( None, EffectType::Particle{ glyph : rltk::to_cp437('?'), fg : rltk::RGB::named(rltk::CYAN), bg : rltk::RGB::named(rltk::BLACK), lifespan: 200.0 }, Targets::Single{ target:status_effect.target } ); not_my_turn.push(status_effect.target); } }
因此,如果你现在 cargo run
该项目,你可以在游戏中看到 confusion 的效果:
宿醉 (Hangovers)
回到设计文档,我们提到你开始游戏时会宿醉。 我们终于可以实现它了! 由于你开始游戏时会宿醉,请打开 spawner.rs
并在玩家生成结束时添加以下内容以创建一个宿醉实体:
#![allow(unused)] fn main() { // Starting hangover // 初始宿醉 ecs.create_entity() .with(StatusEffect{ target : player }) .with(Duration{ turns:10 }) .with(Name{ name: "Hangover".to_string() }) .with(AttributeBonus{ might : Some(-1), fitness : None, quickness : Some(-1), intelligence : Some(-1) }) .marked::<SimpleMarker<SerializeMe>>() .build(); }
宿醉很糟糕! 你会变得更虚弱、更慢且更不聪明。 或者一旦我们修改了 encumbrance 系统(它真的需要一个新名称)来处理状态引起的属性变化,你就会这样。 该系统需要一个小小的改进:
#![allow(unused)] fn main() { // Total up status effect modifiers // 累加状态效果 modifiers for (status, attr) in (&statuses, &attrbonus).join() { if to_update.contains_key(&status.target) { let totals = to_update.get_mut(&status.target).unwrap(); totals.might += attr.might.unwrap_or(0); totals.fitness += attr.fitness.unwrap_or(0); totals.quickness += attr.quickness.unwrap_or(0); totals.intelligence += attr.intelligence.unwrap_or(0); } } }
这显示了拥有宿醉系统的 真正 原因:它使我们能够安全地测试改变你属性的效果,并确保到期时间有效!
如果你现在 cargo run
游戏,你可以观察到宿醉的效果以及它的消退:
力量药水 (Potion of Strength)
现在我们拥有了所有这些,让我们用它来制作一种力量药水(我总是想到旧的 Asterix The Gaul 漫画)。 在 spawns.json
中,我们定义了新的药水:
{
"name" : "力量药水",
"renderable": {
"glyph" : "!",
"fg" : "#FF00FF",
"bg" : "#000000",
"order" : 2
},
"consumable" : {
"effects" : { "particle" : "!;#FF0000;200.0" }
},
"weight_lbs" : 0.5,
"base_value" : 50.0,
"vendor_category" : "alchemy",
"magic" : { "class" : "common", "naming" : "potion" },
"attributes" : { "might" : 5 }
},
这里没有什么新的东西:我们将显示一个粒子效果,并且我们像其他药水一样为药水附加了一个 attributes
部分。 但是,我们将不得不调整效果系统,使其知道如何应用瞬时属性效果。 在 effects/mod.rs
中,我们将添加一个新的效果类型:
#![allow(unused)] fn main() { #[derive(Debug)] pub enum EffectType { Damage { amount : i32 }, Bloodstain, Particle { glyph: rltk::FontCharType, fg : rltk::RGB, bg: rltk::RGB, lifespan: f32 }, EntityDeath, ItemUse { item: Entity }, WellFed, Healing { amount : i32 }, Confusion { turns : i32 }, TriggerFire { trigger: Entity }, TeleportTo { x:i32, y:i32, depth: i32, player_only : bool }, AttributeEffect { bonus : AttributeBonus, name : String, duration : i32 } } }
我们将它标记为影响实体:
#![allow(unused)] fn main() { fn tile_effect_hits_entities(effect: &EffectType) -> bool { match effect { EffectType::Damage{..} => true, EffectType::WellFed => true, EffectType::Healing{..} => true, EffectType::Confusion{..} => true, EffectType::TeleportTo{..} => true, EffectType::AttributeEffect{..} => true, _ => false } } }
并告诉它调用一个我们尚未编写的新函数:
#![allow(unused)] fn main() { fn affect_entity(ecs: &mut World, effect: &EffectSpawner, target: Entity) { match &effect.effect_type { EffectType::Damage{..} => damage::inflict_damage(ecs, effect, target), EffectType::EntityDeath => damage::death(ecs, effect, target), EffectType::Bloodstain{..} => if let Some(pos) = entity_position(ecs, target) { damage::bloodstain(ecs, pos) }, EffectType::Particle{..} => if let Some(pos) = entity_position(ecs, target) { particles::particle_to_tile(ecs, pos, &effect) }, EffectType::WellFed => hunger::well_fed(ecs, effect, target), EffectType::Healing{..} => damage::heal_damage(ecs, effect, target), EffectType::Confusion{..} => damage::add_confusion(ecs, effect, target), EffectType::TeleportTo{..} => movement::apply_teleport(ecs, effect, target), EffectType::AttributeEffect{..} => damage::attribute_effect(ecs, effect, target), _ => {} } } }
现在我们需要进入 effects/damage.rs
并编写新函数:
#![allow(unused)] fn main() { pub fn attribute_effect(ecs: &mut World, effect: &EffectSpawner, target: Entity) { if let EffectType::AttributeEffect{bonus, name, duration} = &effect.effect_type { ecs.create_entity() .with(StatusEffect{ target }) .with(bonus.clone()) .with(Duration { turns : *duration }) .with(Name { name : name.clone() }) .marked::<SimpleMarker<SerializeMe>>() .build(); ecs.write_storage::<EquipmentChanged>().insert(target, EquipmentChanged{}).expect("插入失败"); } } }
剩下的就是打开 effects/triggers.rs
并添加属性加成效果作为触发器类型:
#![allow(unused)] fn main() { // Attribute Modifiers // 属性修改器 if let Some(attr) = ecs.read_storage::<AttributeBonus>().get(entity) { add_effect( creator, EffectType::AttributeEffect{ bonus : attr.clone(), duration : 10, name : ecs.read_storage::<Name>().get(entity).unwrap().name.clone() }, targets.clone() ); did_something = true; } }
这与其他触发器类似 - 它会触发另一个事件,这次是属性效果生效。 你现在可以 cargo run
,力量药水在游戏中可以正常工作了。 这是在宿醉时喝下力量药水的截图,向您展示了效果现在可以正确叠加:
总结
我们有了:一个良好且通用的状态效果系统,以及一个允许物品触发它们以及提供属性加成和惩罚的系统。 到目前为止,物品系统就到此为止了。 在下一章中,我们将转向魔法咒语 - 这将使用我们在这些章节中构建的许多基础。
...
本章的源代码可以在这里找到
在您的浏览器中使用 Web 程序集运行本章的示例(需要 WebGL2)
版权所有 (C) 2019, Herbert Wolverson。