影响属性的物品和更好的状态效果


关于本教程

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

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

Hands-On Rust


我们仍然有一些常见的物品类型尚未支持。本章将完成这些,并为施法(在下一章中)奠定基础框架。

提升属性的物品

在类 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.rssaveload_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 该项目,你可以找到 食人魔力量手套 并装备它们以获得加值 - 然后移除它们以取消加值:

Screenshot

充能物品

并非所有物品在使用后都会化为尘土。一个药水瓶可能装有多剂药剂,一根魔法棒可能多次施放其效果(像往常一样,你的想象力是无限的!)。 让我们制作一件新物品,火球法杖。 在 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.rsitem_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 该项目,你可以找到你的 火球法杖 并尽情轰炸,直到充能耗尽:

Screenshot

状态效果

目前,我们正在逐个案例地处理状态效果,并且应用它们相对来说并不常见。 大多数深度 Roguelike 游戏都有 很多 可能的效果 - 从蘑菇引起的幻觉到喝了一些棕色粘液后以超音速移动! 我们之所以将这些留到现在,是因为它们与我们在本章中所做的其他事情完美地结合在一起。

直到本章为止,我们都将 Confusion 作为标签添加到目标 - 并依赖于该标签来存储持续时间。 这实际上不符合 ECS 的精神! 相反,Confusion 是一种实体 效果,它应用于 目标,持续 duration 回合。 像往常一样,检查分类法是弄清楚某些东西应该具有哪些实体/component 组的好方法。 因此,我们将访问 components.rs 并创建两个新的 component (也在 main.rssaveload_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 的效果:

Screenshot

宿醉 (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 游戏,你可以观察到宿醉的效果以及它的消退:

Screenshot

力量药水 (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,力量药水在游戏中可以正常工作了。 这是在宿醉时喝下力量药水的截图,向您展示了效果现在可以正确叠加:

Screenshot

总结

我们有了:一个良好且通用的状态效果系统,以及一个允许物品触发它们以及提供属性加成和惩罚的系统。 到目前为止,物品系统就到此为止了。 在下一章中,我们将转向魔法咒语 - 这将使用我们在这些章节中构建的许多基础。

...

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

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

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