Finished section 1

This commit is contained in:
Kasper Juul Hermansen 2022-01-28 14:06:20 +01:00
parent fb5b70ce3b
commit aad1402f81
Signed by: kjuulh
GPG Key ID: DCD9397082D97069
10 changed files with 563 additions and 23 deletions

View File

@ -135,3 +135,41 @@ pub struct SerializeMe;
pub struct SerializationHelper {
pub map: super::map::Map,
}
#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)]
pub enum EquipmentSlot {
Melee,
Shield,
Head,
Shoulder,
Chest,
Legs,
Hands,
Feet,
}
#[derive(Component, Copy, Clone, Serialize, Deserialize)]
pub struct Equippable {
pub slot: EquipmentSlot,
}
#[derive(Component, ConvertSaveload, Clone)]
pub struct Equipped {
pub owner: Entity,
pub slot: EquipmentSlot,
}
#[derive(Component, ConvertSaveload, Clone)]
pub struct MeleePowerBonus {
pub power: i32,
}
#[derive(Component, ConvertSaveload, Clone)]
pub struct DefenseBonus {
pub defense: i32,
}
#[derive(Component, ConvertSaveload, Clone)]
pub struct WantsToRemoveItem {
pub item: Entity,
}

View File

@ -1,9 +1,9 @@
use crate::gamelog::GameLog;
use crate::Name;
use rltk::console;
use specs::prelude::*;
use crate::{Name, RunState};
use crate::components::{CombatStats, Player, SufferDamage};
use crate::gamelog::GameLog;
pub struct DamageSystem {}
@ -44,7 +44,10 @@ pub fn delete_the_dead(ecs: &mut World) {
dead.push(entity)
}
Some(_) => console::log("You are dead"),
Some(_) => {
let mut runstate = ecs.write_resource::<RunState>();
*runstate = RunState::GameOver;
}
}
}
}

View File

@ -1,14 +1,14 @@
use rltk::Rltk;
use rltk::RGB;
use rltk::{Rltk};
use rltk::{Point, VirtualKeyCode};
use rltk::RGB;
use specs::prelude::*;
use crate::{CombatStats, InBackpack, RunState, State, Viewshed};
use crate::{Equipped, Map};
use crate::gamelog::GameLog;
use crate::Map;
use crate::Name;
use crate::Player;
use crate::Position;
use crate::{CombatStats, InBackpack, RunState, State, Viewshed};
pub fn draw_ui(ecs: &World, ctx: &mut Rltk) {
ctx.draw_box(
@ -514,3 +514,106 @@ pub fn main_menu(gs: &mut State, ctx: &mut Rltk) -> MainMenuResult {
selected: MainMenuSelection::NewGame,
}
}
pub fn remove_item_menu(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Option<Entity>) {
let player_entity = gs.ecs.fetch::<Entity>();
let names = gs.ecs.read_storage::<Name>();
let backpack = gs.ecs.read_storage::<Equipped>();
let entities = gs.ecs.entities();
let inventory = (&backpack, &names)
.join()
.filter(|item| item.0.owner == *player_entity);
let count = inventory.count();
let mut y = (25 - (count / 2)) as i32;
ctx.draw_box(
15,
y - 2,
31,
(count + 3) as i32,
RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK),
);
ctx.print_color(
18,
y - 2,
RGB::named(rltk::YELLOW),
RGB::named(rltk::BLACK),
"Remove Which Item?",
);
ctx.print_color(
18,
y + count as i32 + 1,
RGB::named(rltk::YELLOW),
RGB::named(rltk::BLACK),
"ESCAPE to cancel",
);
let mut equippable: Vec<Entity> = Vec::new();
let mut j = 0;
for (entity, _pack, name) in (&entities, &backpack, &names)
.join()
.filter(|item| item.1.owner == *player_entity)
{
ctx.set(
17,
y,
RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK),
rltk::to_cp437('('),
);
ctx.set(
18,
y,
RGB::named(rltk::YELLOW),
RGB::named(rltk::BLACK),
97 + j as rltk::FontCharType,
);
ctx.set(
19,
y,
RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK),
rltk::to_cp437(')'),
);
ctx.print(21, y, &name.name.to_string());
equippable.push(entity);
y += 1;
j += 1;
}
match ctx.key {
None => (ItemMenuResult::NoResponse, None),
Some(key) => match key {
VirtualKeyCode::Escape => (ItemMenuResult::Cancel, None),
_ => {
let selection = rltk::letter_to_option(key);
if selection > -1 && selection < count as i32 {
return (
ItemMenuResult::Selected,
Some(equippable[selection as usize]),
);
}
(ItemMenuResult::NoResponse, None)
}
},
}
}
#[derive(PartialEq, Copy, Clone)]
pub enum GameOverResult { NoSelection, QuitToMenu }
pub fn game_over(ctx: &mut Rltk) -> GameOverResult {
ctx.print_color_centered(15, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Your journey has ended!");
ctx.print_color_centered(17, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), "One day, we'll tell you all about how you did.");
ctx.print_color_centered(18, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), "That day, sadly, is not in this chapter..");
ctx.print_color_centered(20, RGB::named(rltk::MAGENTA), RGB::named(rltk::BLACK), "Press any key to return to the menu.");
match ctx.key {
None => GameOverResult::NoSelection,
Some(_) => GameOverResult::QuitToMenu
}
}

View File

@ -2,14 +2,15 @@ use specs::prelude::*;
use crate::gamelog::GameLog;
use crate::{
AreaOfEffect, CombatStats, Confusion, Consumable, InBackpack, InflictsDamage, Map, Name,
Position, ProvidesHealing, Ranged, SufferDamage, WantsToDropItem, WantsToPickupItem,
WantsToUseItem,
AreaOfEffect, CombatStats, Confusion, Consumable, Equippable, Equipped, InBackpack,
InflictsDamage, Map, Name, Position, ProvidesHealing, Ranged, SufferDamage, WantsToDropItem,
WantsToPickupItem, WantsToRemoveItem, WantsToUseItem,
};
pub struct ItemCollectionSystem {}
impl<'a> System<'a> for ItemCollectionSystem {
#[allow(clippy::complexity)]
type SystemData = (
ReadExpect<'a, Entity>,
WriteExpect<'a, GameLog>,
@ -49,6 +50,7 @@ impl<'a> System<'a> for ItemCollectionSystem {
pub struct ItemUseSystem {}
impl<'a> System<'a> for ItemUseSystem {
#[allow(clippy::complexity)]
type SystemData = (
ReadExpect<'a, Entity>,
WriteExpect<'a, GameLog>,
@ -64,6 +66,9 @@ impl<'a> System<'a> for ItemUseSystem {
WriteStorage<'a, SufferDamage>,
ReadStorage<'a, AreaOfEffect>,
WriteStorage<'a, Confusion>,
ReadStorage<'a, Equippable>,
WriteStorage<'a, Equipped>,
WriteStorage<'a, InBackpack>,
);
fn run(&mut self, data: Self::SystemData) {
@ -82,6 +87,9 @@ impl<'a> System<'a> for ItemUseSystem {
mut suffer_damage,
aoe,
mut confused,
equippable,
mut equipped,
mut backpack,
) = data;
for (entity, use_item) in (&entities, &wants_use).join() {
@ -116,6 +124,43 @@ impl<'a> System<'a> for ItemUseSystem {
}
}
if let Some(item_equippable) = equippable.get(use_item.item) {
let target_slot = item_equippable.slot;
let target = targets[0];
let mut to_unequip: Vec<Entity> = Vec::new();
for (item_entity, already_equipped, name) in (&entities, &equipped, &names).join() {
if already_equipped.owner == target && already_equipped.slot == target_slot {
to_unequip.push(item_entity);
if target == *player_entity {
game_log.entries.push(format!("You unequip {}.", name.name));
}
}
}
for item in to_unequip.iter() {
equipped.remove(*item);
backpack
.insert(*item, InBackpack { owner: target })
.expect("Unable to insert item into backpack");
}
equipped
.insert(
use_item.item,
Equipped {
owner: target,
slot: target_slot,
},
)
.expect("Unable to equip item");
backpack.remove(use_item.item);
if target == *player_entity {
game_log.entries.push(format!(
"You equip item {}.",
names.get(use_item.item).unwrap().name
))
}
}
if let Some(item_damages) = inflicts_damage.get(use_item.item) {
used_item = false;
for mob in targets.iter() {
@ -242,3 +287,26 @@ impl<'a> System<'a> for ItemDropSystem {
wants_drop.clear();
}
}
pub struct ItemRemoveSystem {}
impl<'a> System<'a> for ItemRemoveSystem {
type SystemData = (
Entities<'a>,
WriteStorage<'a, WantsToRemoveItem>,
WriteStorage<'a, Equipped>,
WriteStorage<'a, InBackpack>,
);
fn run(&mut self, data: Self::SystemData) {
let (entities, mut wants_to_remove_item, mut equipped, mut backpack) = data;
for (entity, to_remove) in (&entities, &wants_to_remove_item).join() {
equipped.remove(to_remove.item);
backpack
.insert(to_remove.item, InBackpack { owner: entity })
.expect("Unable to insert item into backpack");
}
wants_to_remove_item.clear();
}
}

View File

@ -14,7 +14,7 @@ use player::*;
use visibility_system::*;
use crate::gamelog::GameLog;
use crate::inventory_system::{ItemCollectionSystem, ItemDropSystem, ItemUseSystem};
use crate::inventory_system::{ItemCollectionSystem, ItemDropSystem, ItemRemoveSystem, ItemUseSystem};
mod components;
mod damage_system;
@ -49,25 +49,75 @@ pub enum RunState {
},
SaveGame,
NextLevel,
ShowRemoveItem,
GameOver,
}
pub struct State {
pub ecs: World,
}
impl State {
pub fn game_over_cleanup(&mut self) {
// Delete everything
let mut to_delete = Vec::new();
for e in self.ecs.entities().join() {
to_delete.push(e);
}
for del in to_delete.iter() {
self.ecs.delete_entity(*del).expect("Deletion failed");
}
// Build a new map and place the player
let worldmap;
{
let mut worldmap_resource = self.ecs.write_resource::<Map>();
*worldmap_resource = Map::new_map_rooms_and_corridors(1);
worldmap = worldmap_resource.clone();
}
// Spawn bad guys
for room in worldmap.rooms.iter().skip(1) {
spawner::spawn_room(&mut self.ecs, room, 1);
}
// Place the player and update resources
let (player_x, player_y) = worldmap.rooms[0].center();
let player_entity = spawner::player(&mut self.ecs, player_x, player_y);
let mut player_position = self.ecs.write_resource::<Point>();
*player_position = Point::new(player_x, player_y);
let mut position_components = self.ecs.write_storage::<Position>();
let mut player_entity_writer = self.ecs.write_resource::<Entity>();
*player_entity_writer = player_entity;
let player_pos_comp = position_components.get_mut(player_entity);
if let Some(player_pos_comp) = player_pos_comp {
player_pos_comp.x = player_x;
player_pos_comp.y = player_y;
}
// Mark the player's visibility as dirty
let mut viewshed_components = self.ecs.write_storage::<Viewshed>();
let vs = viewshed_components.get_mut(player_entity);
if let Some(vs) = vs {
vs.dirty = true;
}
}
}
impl State {
fn entities_to_remove_on_level_change(&mut self) -> Vec<Entity> {
let entities = self.ecs.entities();
let player = self.ecs.read_storage::<Player>();
let backpack = self.ecs.read_storage::<InBackpack>();
let player_entity = self.ecs.fetch::<Entity>();
let equipped = self.ecs.read_storage::<Equipped>();
let mut to_delete: Vec<Entity> = Vec::new();
for entity in entities.join() {
let mut should_delete = true;
let p = player.get(entity);
if let Some(p) = p {
if let Some(_p) = p {
should_delete = false;
}
@ -78,6 +128,12 @@ impl State {
}
}
if let Some(eq) = equipped.get(entity) {
if eq.owner == *player_entity {
should_delete = false;
}
}
if should_delete {
to_delete.push(entity);
}
@ -162,6 +218,9 @@ impl State {
let mut drop_items = ItemDropSystem {};
drop_items.run_now(&self.ecs);
let mut remove_items = ItemRemoveSystem {};
remove_items.run_now(&self.ecs);
self.ecs.maintain();
}
}
@ -309,6 +368,34 @@ impl GameState for State {
self.goto_next_level();
new_run_state = RunState::PreRun;
}
RunState::ShowRemoveItem => {
let result = gui::remove_item_menu(self, ctx);
match result.0 {
gui::ItemMenuResult::Cancel => new_run_state = RunState::AwaitingInput,
gui::ItemMenuResult::NoResponse => {}
gui::ItemMenuResult::Selected => {
let item_entity = result.1.unwrap();
let mut intent = self.ecs.write_storage::<WantsToRemoveItem>();
intent
.insert(
*self.ecs.fetch::<Entity>(),
WantsToRemoveItem { item: item_entity },
)
.expect("Unable to insert intent");
new_run_state = RunState::PlayerTurn;
}
}
}
RunState::GameOver => {
let result = gui::game_over(ctx);
match result {
gui::GameOverResult::NoSelection => {}
gui::GameOverResult::QuitToMenu => {
self.game_over_cleanup();
new_run_state = RunState::MainMenu { menu_selection: gui::MainMenuSelection::NewGame };
}
}
}
}
{
@ -354,6 +441,11 @@ fn main() -> rltk::BError {
gs.ecs.register::<Confusion>();
gs.ecs.register::<SimpleMarker<SerializeMe>>();
gs.ecs.register::<SerializationHelper>();
gs.ecs.register::<Equippable>();
gs.ecs.register::<Equipped>();
gs.ecs.register::<MeleePowerBonus>();
gs.ecs.register::<DefenseBonus>();
gs.ecs.register::<WantsToRemoveItem>();
gs.ecs.insert(SimpleMarkerAllocator::<SerializeMe>::new());

View File

@ -2,6 +2,7 @@ use specs::prelude::*;
use crate::components::{CombatStats, Name, SufferDamage, WantsToMelee};
use crate::gamelog::GameLog;
use crate::{DefenseBonus, Equipped, MeleePowerBonus};
pub struct MeleeCombatSystem {}
@ -13,20 +14,54 @@ impl<'a> System<'a> for MeleeCombatSystem {
ReadStorage<'a, Name>,
ReadStorage<'a, CombatStats>,
WriteStorage<'a, SufferDamage>,
ReadStorage<'a, MeleePowerBonus>,
ReadStorage<'a, DefenseBonus>,
ReadStorage<'a, Equipped>,
);
fn run(&mut self, data: Self::SystemData) {
let (entities, mut log, mut wants_melee, names, combat_stats, mut inflict_damage) = data;
let (
entities,
mut log,
mut wants_melee,
names,
combat_stats,
mut inflict_damage,
melee_bonus,
defense_bonus,
equipped,
) = data;
for (_entity, wants_melee, name, stats) in
for (entity, wants_melee, name, stats) in
(&entities, &wants_melee, &names, &combat_stats).join()
{
if stats.hp > 0 {
let mut offensive_bonus = 0;
for (_item_entity, power_bonus, equipped_by) in
(&entities, &melee_bonus, &equipped).join()
{
if equipped_by.owner == entity {
offensive_bonus += power_bonus.power;
}
}
let target_stats = combat_stats.get(wants_melee.target).unwrap();
if target_stats.hp > 0 {
let target_name = names.get(wants_melee.target).unwrap();
let damage = i32::max(0, stats.power - target_stats.defense);
let mut defensive_bonus = 0;
for (_item_entity, defense_bonus, equipped_by) in
(&entities, &defense_bonus, &equipped).join()
{
if equipped_by.owner == wants_melee.target {
defensive_bonus += defense_bonus.defense;
}
}
let damage = i32::max(
0,
(stats.power + offensive_bonus) - (target_stats.defense + defensive_bonus),
);
if damage == 0 {
log.entries.push(format!(

View File

@ -109,6 +109,7 @@ pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState {
VirtualKeyCode::I => return RunState::ShowInventory,
VirtualKeyCode::D => return RunState::ShowDropItem,
VirtualKeyCode::Escape => return RunState::SaveGame,
VirtualKeyCode::R => return RunState::ShowRemoveItem,
VirtualKeyCode::Period => {
if try_next_level(&mut gs.ecs) {
return RunState::NextLevel;
@ -132,7 +133,7 @@ fn skip_turn(ecs: &mut World) -> RunState {
for tile in viewshed.visible_tiles.iter() {
let idx = worldmap_resource.xy_idx(tile.x, tile.y);
for entity_id in worldmap_resource.tile_content[idx].iter() {
if let Some(mob) = monsters.get(*entity_id) {
if let Some(_mob) = monsters.get(*entity_id) {
can_heal = true;
}
}

View File

@ -29,8 +29,10 @@ impl RandomTable {
}
pub fn add<S: ToString>(mut self, name: S, weight: i32) -> RandomTable {
self.total_weight += weight;
self.entries.push(RandomEntry::new(name, weight));
if weight > 0 {
self.total_weight += weight;
self.entries.push(RandomEntry::new(name, weight));
}
self
}

View File

@ -71,7 +71,12 @@ pub fn save_game(ecs: &mut World) {
WantsToPickupItem,
WantsToUseItem,
WantsToDropItem,
SerializationHelper
SerializationHelper,
Equippable,
Equipped,
MeleePowerBonus,
DefenseBonus,
WantsToRemoveItem
);
}
@ -145,7 +150,12 @@ pub fn load_game(ecs: &mut World) {
WantsToPickupItem,
WantsToUseItem,
WantsToDropItem,
SerializationHelper
SerializationHelper,
Equippable,
Equipped,
MeleePowerBonus,
DefenseBonus,
WantsToRemoveItem
);
}

View File

@ -7,9 +7,9 @@ use specs::saveload::{MarkedBuilder, SimpleMarker};
use crate::random_table::RandomTable;
use crate::rect::Rect;
use crate::{
AreaOfEffect, BlocksTile, CombatStats, Confusion, Consumable, InflictsDamage, Item, Monster,
Name, Player, Position, ProvidesHealing, Ranged, Renderable, SerializeMe, Viewshed, MAP_WIDTH,
MAX_ITEMS, MAX_MONSTER,
AreaOfEffect, BlocksTile, CombatStats, Confusion, Consumable, DefenseBonus, EquipmentSlot,
Equippable, InflictsDamage, Item, MeleePowerBonus, Monster, Name, Player, Position,
ProvidesHealing, Ranged, Renderable, SerializeMe, Viewshed, MAP_WIDTH, MAX_ITEMS, MAX_MONSTER,
};
pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity {
@ -117,6 +117,14 @@ pub fn spawn_room(ecs: &mut World, room: &Rect, map_depth: i32) {
"Fireball Scroll" => fireball_scroll(ecs, x, y),
"Confusion Scroll" => confusion_scroll(ecs, x, y),
"Magic Missile Scroll" => magic_missile_scroll(ecs, x, y),
"Dagger" => dagger(ecs, x, y),
"Longsword" => longsword(ecs, x, y),
"Shield" => shield(ecs, x, y),
"Tower Shield" => tower_shield(ecs, x, y),
"Helmet" => helmet(ecs, x, y),
"Breastplate" => breastplate(ecs, x, y),
"Leggings" => leggings(ecs, x, y),
"Sabatons" => sabatons(ecs, x, y),
_ => {}
}
}
@ -210,4 +218,184 @@ pub fn room_table(map_depth: i32) -> RandomTable {
.add("Fireball Scroll", 2 + map_depth)
.add("Confusion Scroll", 2 + map_depth)
.add("Magic Missile Scroll", 4)
.add("Dagger", 3)
.add("Longsword", map_depth - 1)
.add("Shield", 3)
.add("Tower Shield", map_depth - 1)
.add("Helmet", map_depth - 2)
.add("Breastplate", map_depth - 3)
.add("Leggings", map_depth - 4)
.add("Sabatons", map_depth - 4)
}
fn dagger(ecs: &mut World, x: i32, y: i32) {
ecs.create_entity()
.with(Position { x, y })
.with(Renderable {
glyph: rltk::to_cp437('/'),
render_order: 2,
fg: RGB::named(rltk::CYAN),
bg: RGB::named(rltk::BLACK),
})
.with(Item {})
.with(Name {
name: "Dagger".to_string(),
})
.with(Equippable {
slot: EquipmentSlot::Melee,
})
.with(MeleePowerBonus { power: 2 })
.marked::<SimpleMarker<SerializeMe>>()
.build();
}
fn longsword(ecs: &mut World, x: i32, y: i32) {
ecs.create_entity()
.with(Position { x, y })
.with(Renderable {
glyph: rltk::to_cp437('/'),
render_order: 2,
fg: RGB::named(rltk::CYAN),
bg: RGB::named(rltk::BLACK),
})
.with(Item {})
.with(Name {
name: "Longsword".to_string(),
})
.with(Equippable {
slot: EquipmentSlot::Melee,
})
.with(MeleePowerBonus { power: 4 })
.marked::<SimpleMarker<SerializeMe>>()
.build();
}
fn shield(ecs: &mut World, x: i32, y: i32) {
ecs.create_entity()
.with(Position { x, y })
.with(Renderable {
glyph: rltk::to_cp437('('),
render_order: 2,
fg: RGB::named(rltk::CYAN),
bg: RGB::named(rltk::BLACK),
})
.with(Item {})
.with(Name {
name: "Shield".to_string(),
})
.with(Equippable {
slot: EquipmentSlot::Shield,
})
.with(DefenseBonus { defense: 1 })
.marked::<SimpleMarker<SerializeMe>>()
.build();
}
fn tower_shield(ecs: &mut World, x: i32, y: i32) {
ecs.create_entity()
.with(Position { x, y })
.with(Renderable {
glyph: rltk::to_cp437('('),
render_order: 2,
fg: RGB::named(rltk::CYAN),
bg: RGB::named(rltk::BLACK),
})
.with(Item {})
.with(Name {
name: "Tower Shield".to_string(),
})
.with(Equippable {
slot: EquipmentSlot::Shield,
})
.with(DefenseBonus { defense: 3 })
.marked::<SimpleMarker<SerializeMe>>()
.build();
}
fn helmet(ecs: &mut World, x: i32, y: i32) {
ecs.create_entity()
.with(Position { x, y })
.with(Renderable {
glyph: rltk::to_cp437('^'),
render_order: 2,
fg: RGB::named(rltk::CYAN),
bg: RGB::named(rltk::BLACK),
})
.with(Item {})
.with(Name {
name: "Helmet".to_string(),
})
.with(Equippable {
slot: EquipmentSlot::Head,
})
.with(DefenseBonus { defense: 1 })
.with(MeleePowerBonus { power: 1 })
.marked::<SimpleMarker<SerializeMe>>()
.build();
}
fn breastplate(ecs: &mut World, x: i32, y: i32) {
ecs.create_entity()
.with(Position { x, y })
.with(Renderable {
glyph: rltk::to_cp437('x'),
render_order: 2,
fg: RGB::named(rltk::CYAN),
bg: RGB::named(rltk::BLACK),
})
.with(Item {})
.with(Name {
name: "Breastplate".to_string(),
})
.with(Equippable {
slot: EquipmentSlot::Chest,
})
.with(DefenseBonus { defense: 2 })
.marked::<SimpleMarker<SerializeMe>>()
.build();
}
fn leggings(ecs: &mut World, x: i32, y: i32) {
ecs.create_entity()
.with(Position { x, y })
.with(Renderable {
glyph: rltk::to_cp437('"'),
render_order: 2,
fg: RGB::named(rltk::CYAN),
bg: RGB::named(rltk::BLACK),
})
.with(Item {})
.with(Name {
name: "Leggings".to_string(),
})
.with(Equippable {
slot: EquipmentSlot::Legs,
})
.with(DefenseBonus { defense: 1 })
.marked::<SimpleMarker<SerializeMe>>()
.build();
}
fn sabatons(ecs: &mut World, x: i32, y: i32) {
ecs.create_entity()
.with(Position { x, y })
.with(Renderable {
glyph: rltk::to_cp437(','),
render_order: 2,
fg: RGB::named(rltk::CYAN),
bg: RGB::named(rltk::BLACK),
})
.with(Item {})
.with(Name {
name: "Sabatons".to_string(),
})
.with(Equippable {
slot: EquipmentSlot::Feet,
})
.with(DefenseBonus { defense: 1 })
.with(MeleePowerBonus { power: 1 })
.marked::<SimpleMarker<SerializeMe>>()
.build();
}