装备


关于本教程

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

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

Hands-On Rust


在上一章中,我们转到了 d20 风格(类似 D&D)的战斗系统和属性系统。它功能完善,但并没有真正提供任何通过装备来提升角色的机会。寻找酷炫的物品,并煞费苦心地最大化您的效率是 roguelike 游戏的基础 - 提供了大量的深度,以及一种感觉,即虽然游戏是随机的,但您可以极大地影响它以获得您想要的结果。

解析骰子字符串

我们将发现能够读取包含 D&D 骰子规格(例如 20d6+4)的字符串并将其转换为计算机友好的数字非常有帮助。我们在读取原始文件时会经常使用它,所以我们会把它放在那里 - 但要公开它,以防我们在其他地方需要它。

解析像这样的文本片段是正则表达式的完美工作。Rust 通过 crate 支持正则表达式,所以我们必须打开 cargo.toml 并将 regex = "1.3.6" 添加到 [dependencies] 部分。实际上 正则表达式本身就可以写一本书;这是一个非常复杂(且强大)的系统,并且往往看起来像猫在您的键盘上走过一样。这是一个解析 1d20+4 类型字符串的正则表达式:

(\d+)d(\d+)([\+\-]\d+)?

这到底是什么 意思 呢?

  • 括号 (..) 中包含的每个部分都是一个匹配组。您正在告诉正则表达式,括号中的任何内容对您都很重要,并且可以被捕获以供读取。
  • \d 是正则表达式的说法,表示“我希望这里有一个数字”。
  • 添加 + 表示“这里可能不止一个数字,继续读取直到遇到其他内容”。
  • 因此,第一个 (\d+) 表示“捕获字符串开头的全部数字”。
  • 组外的 d字面量 d 字符。因此,我们用字母 d 将第一组数字与后续部分分隔开。现在我们已经到了 1d
  • 下一个 (\d+) 工作方式相同 - 继续读取数字,并将它们捕获到第二个组中。所以现在我们已经读取到 1d20,并且在组中捕获120
  • 最后一个组有点令人困惑。 [..] 表示“期望这些字符中的任何一个”。反斜杠 (\) 转义 了后续字符,意思是“+ 或 - 在正则表达式语言中可能意味着某些东西;在这种情况下,请将其视为符号”。所以 [\+\-] 表示“期望这里有一个加号或减号”。然后我们读取那里的数字。
  • 所以现在我们已经将 1d20+4 分解为 1204

完全有可能我们会传递没有 +4 的骰子类型,例如 1d20。在这种情况下,正则表达式将匹配 120 - 但最后一个组将为空。

这是我们函数的 Rust 代码:

#![allow(unused)]
fn main() {
pub fn parse_dice_string(dice : &str) -> (i32, i32, i32) {
    lazy_static! {
        static ref DICE_RE : Regex = Regex::new(r"(\d+)d(\d+)([\+\-]\d+)?").unwrap();
    }
    let mut n_dice = 1;
    let mut die_type = 4;
    let mut die_bonus = 0;
    for cap in DICE_RE.captures_iter(dice) {
        if let Some(group) = cap.get(1) {
            n_dice = group.as_str().parse::<i32>().expect("Not a digit"); // 不是数字
        }
        if let Some(group) = cap.get(2) {
            die_type = group.as_str().parse::<i32>().expect("Not a digit"); // 不是数字
        }
        if let Some(group) = cap.get(3) {
            die_bonus = group.as_str().parse::<i32>().expect("Not a digit"); // 不是数字
        }

    }
    (n_dice, die_type, die_bonus)
}
}

嗯,这简直是一团糟。让我们逐步分析并尝试稍微解读一下。

  1. 正则表达式在 Rust Regex 库首次解析时被编译成它们自己的内部格式。我们不想每次尝试读取骰子字符串时都这样做,所以我们采纳了 Rust Cookbook 的建议,并将读取表达式的操作烘焙到 lazy_static! 中(就像我们用于全局变量一样)。这样,它只会被解析一次,并且正则表达式在我们需要时就可以使用了。
  2. 我们将一些可变变量设置为骰子表达式的不同部分;骰子数量、它们的类型(面数)和奖励(如果是惩罚则为负数)。我们为它们提供了一些默认值,以防我们在读取字符串(或其部分)时遇到问题。
  3. 现在我们使用 regex 库的 captures_iter 功能;我们将我们正在查看的字符串传递给它,它返回所有捕获的迭代器(复杂的正则表达式可能有许多捕获)。在我们的例子中,这返回一个捕获集,其中可能包含我们上面讨论的所有
  4. 现在,任何组都可能不存在。所以我们对每个捕获组都执行 if let。如果它确实存在,我们使用 as_str 检索字符串并将其解析为整数 - 并将其分配给骰子读取器的正确部分。
  5. 我们将所有部分作为元组返回。

定义近战武器

目前,没有必要更改消耗品 - 系统运行良好。我们将专注于可装备物品:您可以挥舞、穿戴或以其他方式从中受益的物品。我们之前对“匕首”的定义如下所示:

{
    "name" : "Dagger",
    "renderable": {
        "glyph" : "/",
        "fg" : "#FFAAAA",
        "bg" : "#000000",
        "order" : 2
    },
    "weapon" : {
        "range" : "melee",
        "power_bonus" : 2
    }
},

power_bonus 现在已经过时了;武器不再那样工作了。相反,我们希望能够为它们定义类似 D&D 的属性。这是一个现代化的匕首:

{
    "name" : "Dagger",
    "renderable": {
        "glyph" : "/",
        "fg" : "#FFAAAA",
        "bg" : "#000000",
        "order" : 2
    },
    "weapon" : {
        "range" : "melee",
        "attribute" : "Quickness",
        "base_damage" : "1d4",
        "hit_bonus" : 0
    }
},

为了支持这一点,在 raws/item_structs.rs 中,我们更改 Weapon 结构体:

#![allow(unused)]
fn main() {
#[derive(Deserialize, Debug)]
pub struct Weapon {
    pub range: String,
    pub attribute: String,
    pub base_damage: String,
    pub hit_bonus: i32
}
}

现在打开 components.rs,我们将更改 MeleePowerBonus(并从 main.rssaveload_system.rs 中重命名它)。我们将用 MeleeWeapon 替换它,它捕获了这些方面,但以更机器友好的格式(这样我们就不会一直解析字符串):

#![allow(unused)]
fn main() {
#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)]
pub enum WeaponAttribute { Might, Quickness }

#[derive(Component, Serialize, Deserialize, Clone)]
pub struct MeleeWeapon {
    pub attribute : WeaponAttribute,
    pub damage_n_dice : i32,
    pub damage_die_type : i32,
    pub damage_bonus : i32,
    pub hit_bonus : i32
}
}

我们将 attribute 浓缩成一个 enum(读取速度更快),并将 1d4+0 分解为以下含义:(1)damage_n_dice,(4)damage_die_type,加上 damage_bonus

我们还需要更改 raws/rawmaster.rs 中的 spawn_named_item

#![allow(unused)]
fn main() {
if let Some(weapon) = &item_template.weapon {
    eb = eb.with(Equippable{ slot: EquipmentSlot::Melee });
    let (n_dice, die_type, bonus) = parse_dice_string(&weapon.base_damage);
    let mut wpn = MeleeWeapon{
        attribute : WeaponAttribute::Might,
        damage_n_dice : n_dice,
        damage_die_type : die_type,
        damage_bonus : bonus,
        hit_bonus : weapon.hit_bonus
    };
    match weapon.attribute.as_str() {
        "Quickness" => wpn.attribute = WeaponAttribute::Quickness,
        _ => wpn.attribute = WeaponAttribute::Might
    }
    eb = eb.with(wpn);
}
}

这应该足以读取我们更友好的武器格式,并使它们在游戏中使用。

从武器开始

如果您回到设计文档,我们声明您从一些最少的装备开始。我们将让您从您父亲的生锈的长剑开始。让我们将其添加到 spawns.json 文件中:

{
    "name" : "Rusty Longsword",
    "renderable": {
        "glyph" : "/",
        "fg" : "#BB77BB",
        "bg" : "#000000",
        "order" : 2
    },
    "weapon" : {
        "range" : "melee",
        "attribute" : "Might",
        "base_damage" : "1d8-1",
        "hit_bonus" : -1
    }
},

我们稍微调暗了颜色(毕竟它是生锈的),并为剑添加了 -1 的惩罚(以说明其状况)。现在,我们希望玩家从它开始。目前在 spawners.rs 中,我们的 player 函数创建了玩家 - 仅此而已。我们希望他/她从一些初始装备开始。目前,我们只允许在地板上生成物品;那不行(您必须记住在开始时捡起它!) - 所以我们将扩展我们的原始文件生成系统来处理它(这就是为什么我们在 mod/rawmaster.rs 中有 SpawnType 枚举 - 即使它只有一个条目!)。让我们将 EquippedCarried 添加到该枚举中:

#![allow(unused)]
fn main() {
pub enum SpawnType {
    AtPosition { x: i32, y: i32 },
    Equipped { by: Entity },
    Carried { by: Entity }
}
}

我们将需要一个函数来确定可装备物品应该放入哪个槽位。这将起到作用:

#![allow(unused)]
fn main() {
fn find_slot_for_equippable_item(tag : &str, raws: &RawMaster) -> EquipmentSlot {
    if !raws.item_index.contains_key(tag) {
        panic!("Trying to equip an unknown item: {}", tag); // 尝试装备未知物品
    }
    let item_index = raws.item_index[tag];
    let item = &raws.raws.items[item_index];
    if let Some(_wpn) = &item.weapon {
        return EquipmentSlot::Melee;
    } else if let Some(wearable) = &item.wearable {
        return string_to_slot(&wearable.slot);
    }
    panic!("Trying to equip {}, but it has no slot tag.", tag); // 尝试装备 {}, 但它没有槽位标签。
}
}

请注意,我们显式调用 panic! 来处理可能导致非常奇怪/意外的游戏行为的情况。现在您没有借口不小心处理您的原始文件条目了!它非常简单:它在索引中查找项目名称,并使用它来查找项目。如果是武器,它会从中派生槽位(目前始终为 Melee)。如果是可穿戴物品,它会使用我们的 string_to_slot 函数从中计算出来。

我们还需要更新 spawn_position 函数来处理这个问题:

#![allow(unused)]
fn main() {
fn spawn_position<'a>(pos : SpawnType, new_entity : EntityBuilder<'a>, tag : &str, raws: &RawMaster) -> EntityBuilder<'a> {
    let eb = new_entity;

    // Spawn in the specified location
    match pos {
        SpawnType::AtPosition{x,y} => eb.with(Position{ x, y }),
        SpawnType::Carried{by} => eb.with(InBackpack{ owner: by }),
        SpawnType::Equipped{by} => {
            let slot = find_slot_for_equippable_item(tag, raws);
            eb.with(Equipped{ owner: by, slot })
        }
    }
}
}

这里有一些值得注意的事情:

  • 我们必须更改方法签名,因此您将必须修复对它的调用。它现在需要访问 raws 文件,以及您正在生成的项目的名称标签。
  • 因为我们传递的是引用,而 EntityBuilder 实际上包含对 ECS 的引用,所以我们必须添加一些生命周期修饰,以告诉 Rust 返回的 EntityBuilder 不依赖于标签或原始文件作为有效引用存在。所以我们命名一个生命周期 a - 并将其附加到函数名称(spawn_position<'a> 声明 'a 是它使用的生命周期)。然后我们将 <'a> 添加到共享该生命周期的类型。这足以提示以避免吓到生命周期检查器。
  • 我们从 match 返回;AtPositionCarried 很简单;Equipped 使用了我们刚刚编写的标签查找器。

我们将不得不更改三行来使用新的函数签名。它们是相同的;找到 eb = spawn_position(pos, eb); 并替换为 eb = spawn_position(pos, eb, key, raws);

不幸的是,我在实现此功能时遇到了另一个问题。我们一直在将新实体 (ecs.create_entity()) 传递到我们的 spawn_named_x 函数中。不幸的是,这将成为一个问题:我们开始需要实体生成来触发其他实体生成(例如,生成带有装备的 NPC - 下面 - 或生成带有内容的箱子)。让我们现在修复它,这样我们就不会在以后遇到问题。

我们将更改 spawn_named_item 的函数签名,并将 eb 的第一次使用更改为实际创建实体:

#![allow(unused)]
fn main() {
pub fn spawn_named_item(raws: &RawMaster, ecs : &mut World, key : &str, pos : SpawnType) -> Option<Entity> {
    if raws.item_index.contains_key(key) {
        let item_template = &raws.raws.items[raws.item_index[key]];

        let mut eb = ecs.create_entity().marked::<SimpleMarker<SerializeMe>>();
        ...
}

我们将对 spawn_named_mob 执行相同的操作:

#![allow(unused)]
fn main() {
pub fn spawn_named_mob(raws: &RawMaster, ecs : &mut World, key : &str, pos : SpawnType) -> Option<Entity> {
    if raws.mob_index.contains_key(key) {
        let mob_template = &raws.raws.mobs[raws.mob_index[key]];

        let mut eb = ecs.create_entity().marked::<SimpleMarker<SerializeMe>>();
        ...
}

再次对 spawn_named_prop 执行相同的操作:

#![allow(unused)]
fn main() {
pub fn spawn_named_prop(raws: &RawMaster, ecs : &mut World, key : &str, pos : SpawnType) -> Option<Entity> {
    if raws.prop_index.contains_key(key) {
        let prop_template = &raws.raws.props[raws.prop_index[key]];

        let mut eb = ecs.create_entity().marked::<SimpleMarker<SerializeMe>>();
        ...
}

然后这要求我们更改 spawn_named_entity 的签名以及它传递的内容:

#![allow(unused)]
fn main() {
pub fn spawn_named_entity(raws: &RawMaster, ecs : &mut World, key : &str, pos : SpawnType) -> Option<Entity> {
    if raws.item_index.contains_key(key) {
        return spawn_named_item(raws, ecs, key, pos);
    } else if raws.mob_index.contains_key(key) {
        return spawn_named_mob(raws, ecs, key, pos);
    } else if raws.prop_index.contains_key(key) {
        return spawn_named_prop(raws, ecs, key, pos);
    }

    None
}
}

spawner.rs 中,我们需要更改 spawn_named_entity 的调用签名:

#![allow(unused)]
fn main() {
let spawn_result = spawn_named_entity(&RAWS.lock().unwrap(), ecs, &spawn.1, SpawnType::AtPosition{ x, y});
if spawn_result.is_some() {
    return;
}
}

如果您想知道为什么我们不能同时传递 ECS 和新实体,那是因为 Rust 的借用检查器。新实体实际上保留了对其父 ECS 的引用(因此当您调用 build 时,它们知道应该将它们插入哪个世界)。因此,如果您尝试发送 &mut World 和一个新实体 - 您会收到错误,因为您对同一对象有两个“借用”。这样做可能是安全的,但 Rust 无法证明这一点 - 所以它会警告您。这实际上防止了在 C/C++ 世界中经常发现的一整类错误,所以虽然这很痛苦 - 但这是为了我们自己好。

所以现在我们可以更新 spawners.rs 中的 player 函数,使其从生锈的长剑开始:

#![allow(unused)]
fn main() {
pub fn player(ecs : &mut World, player_x : i32, player_y : i32) -> Entity {
    let mut skills = Skills{ skills: HashMap::new() };
    skills.skills.insert(Skill::Melee, 1);
    skills.skills.insert(Skill::Defense, 1);
    skills.skills.insert(Skill::Magic, 1);

    let player = ecs
        .create_entity()
        .with(Position { x: player_x, y: player_y })
        .with(Renderable {
            glyph: rltk::to_cp437('@'),
            fg: RGB::named(rltk::YELLOW),
            bg: RGB::named(rltk::BLACK),
            render_order: 0
        })
        .with(Player{})
        .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true })
        .with(Name{name: "Player".to_string() })
        .with(HungerClock{ state: HungerState::WellFed, duration: 20 })
        .with(Attributes{
            might: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) },
            fitness: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) },
            quickness: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) },
            intelligence: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) },
        })
        .with(skills)
        .with(Pools{
            hit_points : Pool{
                current: player_hp_at_level(11, 1),
                max: player_hp_at_level(11, 1)
            },
            mana: Pool{
                current: mana_at_level(11, 1),
                max: mana_at_level(11, 1)
            },
            xp: 0,
            level: 1
        })
        .marked::<SimpleMarker<SerializeMe>>()
        .build();

    // Starting equipment
    spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Rusty Longsword", SpawnType::Equipped{by : player});

    player
}
}

这里的主要变化是我们将新实体放入名为 player 的变量中,然后再返回它。然后我们将其用作参数,用于谁持有我们通过 spawn_named_entity 生成的“生锈的长剑”。

如果您现在 cargo run,您将从生锈的长剑开始。它不会 工作,但您拥有它:

Screenshot

让生锈的长剑造成一些伤害

我们在 melee_combat_system.rs 中留下了一些占位符。现在,是时候填补武器空白了。打开文件,我们首先添加一些我们需要的更多类型:

#![allow(unused)]
fn main() {
impl<'a> System<'a> for MeleeCombatSystem {
    #[allow(clippy::type_complexity)]
    type SystemData = ( Entities<'a>,
                        WriteExpect<'a, GameLog>,
                        WriteStorage<'a, WantsToMelee>,
                        ReadStorage<'a, Name>,
                        ReadStorage<'a, Attributes>,
                        ReadStorage<'a, Skills>,
                        WriteStorage<'a, SufferDamage>,
                        WriteExpect<'a, ParticleBuilder>,
                        ReadStorage<'a, Position>,
                        ReadStorage<'a, HungerClock>,
                        ReadStorage<'a, Pools>,
                        WriteExpect<'a, rltk::RandomNumberGenerator>,
                        ReadStorage<'a, Equipped>,
                        ReadStorage<'a, MeleeWeapon>
                      );

    fn run(&mut self, data : Self::SystemData) {
        let (entities, mut log, mut wants_melee, names, attributes, skills, mut inflict_damage,
            mut particle_builder, positions, hunger_clock, pools, mut rng,
            equipped_items, meleeweapons) = data;
            ...
}

然后我们将添加一些代码来放入默认武器信息,然后在攻击者装备了某些东西时搜索替换:

#![allow(unused)]
fn main() {
let mut weapon_info = MeleeWeapon{
    attribute : WeaponAttribute::Might,
    hit_bonus : 0,
    damage_n_dice : 1,
    damage_die_type : 4,
    damage_bonus : 0
};

for (wielded,melee) in (&equipped_items, &meleeweapons).join() {
    if wielded.owner == entity && wielded.slot == EquipmentSlot::Melee {
        weapon_info = melee.clone();
    }
}
}

这使得替换代码中依赖于武器的部分非常容易:

#![allow(unused)]
fn main() {
let natural_roll = rng.roll_dice(1, 20);
let attribute_hit_bonus = if weapon_info.attribute == WeaponAttribute::Might
    { attacker_attributes.might.bonus }
    else { attacker_attributes.quickness.bonus};
let skill_hit_bonus = skill_bonus(Skill::Melee, &*attacker_skills);
let weapon_hit_bonus = weapon_info.hit_bonus;
}

我们还可以换入武器的伤害信息:

#![allow(unused)]
fn main() {
let base_damage = rng.roll_dice(weapon_info.damage_n_dice, weapon_info.damage_die_type);
let attr_damage_bonus = attacker_attributes.might.bonus;
let skill_damage_bonus = skill_bonus(Skill::Melee, &*attacker_skills);
let weapon_damage_bonus = weapon_info.damage_bonus;
}

现在,如果您 cargo run 该项目,您可以使用您的剑将老鼠砍成碎片!

Screenshot

哦 - 嗯,那进展得不太顺利!我们造成了大量的伤害,但老鼠很快就压倒了我们 - 即使使用 1d4 的默认伤害和 力量 惩罚!正如您在录音中看到的那样,我试图撤退,打算治疗 - 并注意到我饿了(找到房子花了太长时间!)并且无法治疗。幸运的是,我们拥有添加一些食物到玩家背包所需的一切。设计文档声明您应该从一杯啤酒和一根干香肠开始。让我们将它们放入 spawns.json 中:

{
    "name" : "Dried Sausage",
    "renderable": {
        "glyph" : "%",
        "fg" : "#00FF00",
        "bg" : "#000000",
        "order" : 2
    },
    "consumable" : {
        "effects" : {
            "food" : ""
        }
    }
},

{
    "name" : "Beer",
    "renderable": {
        "glyph" : "!",
        "fg" : "#FF00FF",
        "bg" : "#000000",
        "order" : 2
    },
    "consumable" : {
        "effects" : { "provides_healing" : "4" }
    }
},

香肠是“口粮”的复制品 - 可以治愈饥饿。啤酒是一种超弱的治疗药水,但这足以击败啮齿动物的威胁!

让我们修改 spawner.rs 中的 player 以也将这些包含在玩家的背包中:

#![allow(unused)]
fn main() {
spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Dried Sausage", SpawnType::Carried{by : player} );
spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Beer", SpawnType::Carried{by : player});
}

现在玩家开始时拥有治疗选项和抗饥饿设备(通常称为食物)。

但是等等 - 我们是裸体的,只有香肠和一些啤酒?我不认为这是那种游戏?

游戏中没有人穿着任何东西。对于老鼠来说可能没问题,但我们并没有在这里为人类设想一个非常自由的社会。更重要的是,如果我们没有任何东西可以穿,我们也没有任何护甲等级奖励!

components.rs 中,我们将用 Wearable 替换 DefenseBonus - 并将其充实一些。(不要忘记更改 main.rssaveload_system.rs 中的组件):

#![allow(unused)]
fn main() {
#[derive(Component, Serialize, Deserialize, Clone)]
pub struct Wearable {
    pub armor_class : f32
}
}

这是一个非常简单的更改。让我们更新 raws/item_structs.rs 中的原始文件读取器,以反映我们想要的内容:

{
    "name" : "Shield",
    "renderable": {
        "glyph" : "[",
        "fg" : "#00AAFF",
        "bg" : "#000000",
        "order" : 2
    },
    "wearable" : {
        "slot" : "Shield",
        "armor_class" : 1.0
    }
},

我们可能还想支持更多装备槽位!在 components.rs 中,我们应该更新 EquipmentSlot 以处理更多可能的位置:

#![allow(unused)]
fn main() {
#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)]
pub enum EquipmentSlot { Melee, Shield, Head, Torso, Legs, Feet, Hands }
}

我们无疑会在稍后添加更多,但这涵盖了基础知识。我们需要更新 raws/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>
}
...
#[derive(Deserialize, Debug)]
pub struct Wearable {
    pub armor_class: f32,
    pub slot : String
}
}

我们将需要多次从字符串(在 JSON 中)转换为 EquipmentSlot,因此我们将在 raws/rawmaster.rs 中添加一个函数来执行此操作:

#![allow(unused)]
fn main() {
pub fn string_to_slot(slot : &str) -> EquipmentSlot {
    match slot {
        "Shield" => EquipmentSlot::Shield,
        "Head" => EquipmentSlot::Head,
        "Torso" => EquipmentSlot::Torso,
        "Legs" => EquipmentSlot::Legs,
        "Feet" => EquipmentSlot::Feet,
        "Hands" => EquipmentSlot::Hands,
        "Melee" => EquipmentSlot::Melee,
        _ => { rltk::console::log(format!("Warning: unknown equipment slot type [{}]", slot)); EquipmentSlot::Melee } // 警告:未知的装备槽位类型
    }
}
}

我们将要扩展 raws/rawmaster.rs 中的 spawn_named_item 代码以处理扩展的选项:

#![allow(unused)]
fn main() {
if let Some(wearable) = &item_template.wearable {
    let slot = string_to_slot(&wearable.slot);
    eb = eb.with(Equippable{ slot });
    eb = eb.with(Wearable{ slot, armor_class: wearable.armor_class });
}
}

让我们在 spawns.json 中制作更多物品,让玩家可以穿戴:

{
    "name" : "Stained Tunic",
    "renderable": {
        "glyph" : "[",
        "fg" : "#00FF00",
        "bg" : "#000000",
        "order" : 2
    },
    "wearable" : {
        "slot" : "Torso",
        "armor_class" : 0.1
    }
},

{
    "name" : "Torn Trousers",
    "renderable": {
        "glyph" : "[",
        "fg" : "#00FFFF",
        "bg" : "#000000",
        "order" : 2
    },
    "wearable" : {
        "slot" : "Legs",
        "armor_class" : 0.1
    }
},

{
    "name" : "Old Boots",
    "renderable": {
        "glyph" : "[",
        "fg" : "#FF9999",
        "bg" : "#000000",
        "order" : 2
    },
    "wearable" : {
        "slot" : "Legs",
        "armor_class" : 0.1
    }
}

现在我们将打开 spawner.rs 并将这些物品添加到玩家:

#![allow(unused)]
fn main() {
spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Rusty Longsword", SpawnType::Equipped{by : player});
spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Dried Sausage", SpawnType::Carried{by : player} );
spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Beer", SpawnType::Carried{by : player});
spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Stained Tunic", SpawnType::Equipped{by : player});
spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Torn Trousers", SpawnType::Equipped{by : player});
spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Old Boots", SpawnType::Equipped{by : player});
}

如果您现在运行游戏,检查您的背包并移除物品将显示您已正确开始 - 穿着您那件染色的衬衫、破烂的裤子、旧靴子、啤酒、香肠和一把生锈的剑。

我们还有一件关于可穿戴物品的事情要做;melee_system.rs 需要知道如何计算护甲等级。幸运的是,这非常容易:

#![allow(unused)]
fn main() {
let mut armor_item_bonus_f = 0.0;
for (wielded,armor) in (&equipped_items, &wearables).join() {
    if wielded.owner == wants_melee.target {
        armor_item_bonus_f += armor.armor_class;
    }
}
let base_armor_class = 10;
let armor_quickness_bonus = target_attributes.quickness.bonus;
let armor_skill_bonus = skill_bonus(Skill::Defense, &*target_skills);
let armor_item_bonus = armor_item_bonus_f as i32;
}

我们遍历装备的盔甲,并将防御者佩戴的每件物品的奖励加在一起。然后我们将数字截断为整数。

那么我们为什么 使用浮点数呢?经典的 D&D 将护甲值分配给整套护甲。因此,在第 5 版中,皮甲的 AC 为 11(加上敏捷)。在我们的游戏中,您可以单独穿戴皮甲的各个部件 - 因此我们给它们一个相当于整套所需 AC 一部分 的值。然后我们将它们加在一起,以处理零散的盔甲(例如,您找到了一件漂亮的胸甲,但只有皮革护腿)。

好的,所以我穿着衣服 - 为什么其他人不穿呢?

我们已经实现了足够的衣服和武器,我们可以开始将它们交给 NPC。由于酒保是我们最喜欢的测试对象,让我们用我们 希望 生成物品的方式来装饰他的条目:

{
    "name" : "Barkeep",
    "renderable": {
        "glyph" : "☺",
        "fg" : "#EE82EE",
        "bg" : "#000000",
        "order" : 1
    },
    "blocks_tile" : true,
    "vision_range" : 4,
    "ai" : "vendor",
    "attributes" : {
        "intelligence" : 13
    },
    "skills" : {
        "Melee" : 2
    },
    "equipped" : [ "Cudgel", "Cloth Tunic", "Cloth Pants", "Slippers" ]
},

很简单:我们添加了一个名为 equipped 的数组,并列出了我们希望酒保穿戴的所有物品。当然,我们现在必须 编写 这些物品。

{
    "name" : "Cudgel",
    "renderable": {
        "glyph" : "/",
        "fg" : "#A52A2A",
        "bg" : "#000000",
        "order" : 2
    },
    "weapon" : {
        "range" : "melee",
        "attribute" : "Quickness",
        "base_damage" : "1d4",
        "hit_bonus" : 0
    }
},

{
    "name" : "Cloth Tunic",
    "renderable": {
        "glyph" : "[",
        "fg" : "#00FF00",
        "bg" : "#000000",
        "order" : 2
    },
    "wearable" : {
        "slot" : "Torso",
        "armor_class" : 0.1
    }
},

{
    "name" : "Cloth Pants",
    "renderable": {
        "glyph" : "[",
        "fg" : "#00FFFF",
        "bg" : "#000000",
        "order" : 2
    },
    "wearable" : {
        "slot" : "Legs",
        "armor_class" : 0.1
    }
},

{
    "name" : "Slippers",
    "renderable": {
        "glyph" : "[",
        "fg" : "#FF9999",
        "bg" : "#000000",
        "order" : 2
    },
    "wearable" : {
        "slot" : "Legs",
        "armor_class" : 0.1
    }
}

那里没有什么 的东西,只是数据录入。我们需要修改我们的 raws/mob_structs.rs 文件,以适应为 NPC 提供装备:

#![allow(unused)]
fn main() {
#[derive(Deserialize, Debug)]
pub struct Mob {
    pub name : String,
    pub renderable : Option<Renderable>,
    pub blocks_tile : bool,
    pub vision_range : i32,
    pub ai : String,
    pub quips : Option<Vec<String>>,
    pub attributes : MobAttributes,
    pub skills : Option<HashMap<String, i32>>,
    pub level : Option<i32>,
    pub hp : Option<i32>,
    pub mana : Option<i32>,
    pub equipped : Option<Vec<String>>
}
}

再次,很简单 - 我们可选地为 mob 提供一个字符串列表(表示物品名称标签)。所以我们必须修改 raws/rawmaster.rs 中的 spawn_named_mob 来处理这个问题。我们将 Some(eb.build()) 替换为:

#![allow(unused)]
fn main() {
let new_mob = eb.build();

// Are they wielding anyting?
if let Some(wielding) = &mob_template.equipped {
    for tag in wielding.iter() {
        spawn_named_entity(raws, ecs, tag, SpawnType::Equipped{ by: new_mob });
    }
}

return Some(new_mob);
}

这创建了新的 mob,并将实体存储为 new_mob。然后它查看 mob 模板中是否有 equipped 字段;如果有,它会遍历它,生成每个物品作为 mob 的装备。

如果您现在 cargo run,您会发现您穿着衣服,挥舞着生锈的剑,并且拥有您的啤酒和香肠。

Screenshot

装饰您的 NPC

我不会在这里粘贴完整的 spawns.json 文件,但如果您 查看源代码,您会发现我为我们的基本 NPC 添加了服装。目前,它们都与酒保相同 - 希望我们将来会记得调整这些!

那么自然攻击和防御呢?

所以 NPC 们穿着得体并配备了装备,这给了他们战斗属性和护甲等级。但是我们的老鼠呢?老鼠通常不穿可爱的小老鼠套装和携带武器(如果您有这样做