saveload system

localstorage isn't supported by wasm, so playing online will probably just not have save games for a while
This commit is contained in:
Llywelwyn 2023-07-09 12:09:30 +01:00
parent dd91a8cca7
commit 51060f1a85
11 changed files with 290 additions and 63 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
/target
.rustfmt.toml
.vscode/*
savegame.json

6
Cargo.lock generated
View file

@ -142,6 +142,7 @@ dependencies = [
"byteorder",
"lazy_static",
"parking_lot 0.11.2",
"serde",
]
[[package]]
@ -169,6 +170,7 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f31b525fcd65027885f3a1e3a250a5dd397d70de4a6a5a125f03e0bef951499"
dependencies = [
"serde",
"ultraviolet 0.9.1",
]
@ -262,6 +264,7 @@ dependencies = [
"rand",
"rand_xorshift",
"regex",
"serde",
"wasm-bindgen",
]
@ -2275,6 +2278,8 @@ dependencies = [
"bracket-lib 0.8.7 (git+https://github.com/amethyst/bracket-lib.git?rev=851f6f08675444fb6fa088b9e67bee9fd75554c6)",
"criterion",
"rltk",
"serde",
"serde_json",
"specs",
"specs-derive",
]
@ -2545,6 +2550,7 @@ dependencies = [
"hibitset",
"log",
"rayon",
"serde",
"shred",
"shrev",
"tuple_utils",

View file

@ -6,10 +6,12 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rltk = { version = "^0.8.7" }
rltk = { version = "^0.8.7", features = ["serde"] }
bracket-lib = { git = "https://github.com/amethyst/bracket-lib.git", rev = "851f6f08675444fb6fa088b9e67bee9fd75554c6", features = ["serde"] }
specs = "0.16.1"
specs = { version = "0.16.1", features = ["serde"] }
specs-derive = "0.4.1"
serde = { version = "1.0.93", features = ["derive"]}
serde_json = "1.0.39"
[dev-dependencies]
criterion = { version = "^0.5" }

View file

@ -1,14 +1,26 @@
use rltk::RGB;
use serde::{Deserialize, Serialize};
use specs::error::NoError;
use specs::prelude::*;
use specs::saveload::{ConvertSaveload, Marker};
use specs_derive::*;
#[derive(Component)]
// Serialization helper code. We need to implement ConvertSaveload for each type that contains an
// Entity.
pub struct SerializeMe;
// Special component that exists to help serialize the game data
#[derive(Component, Serialize, Deserialize, Clone)]
pub struct SerializationHelper {
pub map: super::map::Map,
}
#[derive(Component, ConvertSaveload, Clone)]
pub struct Position {
pub x: i32,
pub y: i32,
}
#[derive(Component)]
#[derive(Component, ConvertSaveload, Clone)]
pub struct Renderable {
pub glyph: rltk::FontCharType,
pub fg: RGB,
@ -16,28 +28,28 @@ pub struct Renderable {
pub render_order: i32,
}
#[derive(Component, Debug)]
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct Player {}
#[derive(Component, Debug)]
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct Monster {}
#[derive(Component)]
#[derive(Component, ConvertSaveload, Clone)]
pub struct Viewshed {
pub visible_tiles: Vec<rltk::Point>,
pub range: i32,
pub dirty: bool,
}
#[derive(Component, Debug)]
#[derive(Component, Debug, ConvertSaveload, Clone)]
pub struct Name {
pub name: String,
}
#[derive(Component, Debug)]
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct BlocksTile {}
#[derive(Component, Debug)]
#[derive(Component, Debug, ConvertSaveload, Clone)]
pub struct CombatStats {
pub max_hp: i32,
pub hp: i32,
@ -45,12 +57,12 @@ pub struct CombatStats {
pub power: i32,
}
#[derive(Component, Debug, Clone)]
#[derive(Component, Debug, ConvertSaveload, Clone)]
pub struct WantsToMelee {
pub target: Entity,
}
#[derive(Component, Debug)]
#[derive(Component, Debug, ConvertSaveload, Clone)]
pub struct SufferDamage {
pub amount: Vec<i32>,
}
@ -66,63 +78,63 @@ impl SufferDamage {
}
}
#[derive(Component, Debug)]
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct Item {}
#[derive(Component, Debug)]
#[derive(Component, Debug, ConvertSaveload, Clone)]
pub struct ProvidesHealing {
pub amount: i32,
}
#[derive(Component, Debug)]
#[derive(Component, Debug, ConvertSaveload, Clone)]
pub struct InflictsDamage {
pub amount: i32,
}
#[derive(Component, Debug)]
#[derive(Component, Debug, ConvertSaveload, Clone)]
pub struct Ranged {
pub range: i32,
}
#[derive(Component, Debug)]
#[derive(Component, Debug, ConvertSaveload, Clone)]
pub struct AOE {
pub radius: i32,
}
#[derive(Component, Debug)]
#[derive(Component, Debug, ConvertSaveload, Clone)]
pub struct Confusion {
pub turns: i32,
}
#[derive(Component, Debug, Clone)]
#[derive(Component, Debug, ConvertSaveload)]
pub struct InBackpack {
pub owner: Entity,
}
#[derive(Component, Debug, Clone)]
#[derive(Component, Debug, ConvertSaveload)]
pub struct WantsToPickupItem {
pub collected_by: Entity,
pub item: Entity,
}
#[derive(Component, Debug, Clone)]
#[derive(Component, Debug, ConvertSaveload)]
pub struct WantsToDropItem {
pub item: Entity,
}
#[derive(Component, Debug)]
#[derive(Component, Debug, ConvertSaveload)]
pub struct WantsToUseItem {
pub item: Entity,
pub target: Option<rltk::Point>,
}
#[derive(Component, Debug)]
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct Consumable {}
#[derive(Component, Debug)]
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct Destructible {}
#[derive(Component, Clone)]
#[derive(Component, Clone, ConvertSaveload)]
pub struct ParticleLifetime {
pub lifetime_ms: f32,
}

View file

@ -273,34 +273,40 @@ pub enum MainMenuResult {
}
pub fn main_menu(gs: &mut State, ctx: &mut Rltk) -> MainMenuResult {
let save_exists = super::saveload_system::does_save_exist();
let runstate = gs.ecs.fetch::<RunState>();
let assets = gs.ecs.fetch::<RexAssets>();
ctx.render_xp_sprite(&assets.menu, 0, 0);
ctx.print_color(38, 21, RGB::named(rltk::GREEN), RGB::from_f32(0.11, 0.11, 0.11), "RUST-RL");
ctx.print_color(40, 21, RGB::named(rltk::GREEN), RGB::from_f32(0.11, 0.11, 0.11), "RUST-RL");
if let RunState::MainMenu { menu_selection: selection } = *runstate {
let mut y = 24;
if selection == MainMenuSelection::NewGame {
ctx.print_color(34, 24, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "[");
ctx.print_color(36, 24, RGB::named(rltk::GREEN), RGB::from_f32(0.11, 0.11, 0.11), "new game");
ctx.print_color(45, 24, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "]");
ctx.print_color(37, 24, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "[");
ctx.print_color(39, 24, RGB::named(rltk::GREEN), RGB::from_f32(0.11, 0.11, 0.11), "new game");
ctx.print_color(48, 24, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "]");
} else {
ctx.print_color(36, 24, RGB::named(rltk::WHITE), RGB::from_f32(0.11, 0.11, 0.11), "new game");
ctx.print_color(39, 24, RGB::named(rltk::WHITE), RGB::from_f32(0.11, 0.11, 0.11), "new game");
}
y += 2;
if save_exists {
if selection == MainMenuSelection::LoadGame {
ctx.print_color(38, 26, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "[");
ctx.print_color(40, 26, RGB::named(rltk::GREEN), RGB::from_f32(0.11, 0.11, 0.11), "load game");
ctx.print_color(50, 26, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "]");
ctx.print_color(36, y, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "[");
ctx.print_color(38, y, RGB::named(rltk::GREEN), RGB::from_f32(0.11, 0.11, 0.11), "load game");
ctx.print_color(48, y, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "]");
} else {
ctx.print_color(40, 26, RGB::named(rltk::WHITE), RGB::from_f32(0.11, 0.11, 0.11), "load game");
ctx.print_color(38, y, RGB::named(rltk::WHITE), RGB::from_f32(0.11, 0.11, 0.11), "load game");
}
y += 2;
}
if selection == MainMenuSelection::Quit {
ctx.print_color(34, 28, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "[");
ctx.print_color(36, 28, RGB::named(rltk::GREEN), RGB::from_f32(0.11, 0.11, 0.11), "goodbye!");
ctx.print_color(45, 28, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "]");
ctx.print_color(37, y, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "[");
ctx.print_color(39, y, RGB::named(rltk::GREEN), RGB::from_f32(0.11, 0.11, 0.11), "goodbye!");
ctx.print_color(48, y, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "]");
} else {
ctx.print_color(36, 28, RGB::named(rltk::WHITE), RGB::from_f32(0.11, 0.11, 0.11), "quit");
ctx.print_color(43, y, RGB::named(rltk::WHITE), RGB::from_f32(0.11, 0.11, 0.11), "quit");
}
match ctx.key {
@ -312,21 +318,27 @@ pub fn main_menu(gs: &mut State, ctx: &mut Rltk) -> MainMenuResult {
VirtualKeyCode::N => return MainMenuResult::NoSelection { selected: MainMenuSelection::NewGame },
VirtualKeyCode::L => return MainMenuResult::NoSelection { selected: MainMenuSelection::LoadGame },
VirtualKeyCode::Up => {
let new_selection;
let mut new_selection;
match selection {
MainMenuSelection::NewGame => new_selection = MainMenuSelection::Quit,
MainMenuSelection::LoadGame => new_selection = MainMenuSelection::NewGame,
MainMenuSelection::Quit => new_selection = MainMenuSelection::LoadGame,
}
if new_selection == MainMenuSelection::LoadGame && !save_exists {
new_selection = MainMenuSelection::NewGame;
}
return MainMenuResult::NoSelection { selected: new_selection };
}
VirtualKeyCode::Down => {
let new_selection;
let mut new_selection;
match selection {
MainMenuSelection::NewGame => new_selection = MainMenuSelection::LoadGame,
MainMenuSelection::LoadGame => new_selection = MainMenuSelection::Quit,
MainMenuSelection::Quit => new_selection = MainMenuSelection::NewGame,
}
if new_selection == MainMenuSelection::LoadGame && !save_exists {
new_selection = MainMenuSelection::Quit;
}
return MainMenuResult::NoSelection { selected: new_selection };
}
VirtualKeyCode::Return => return MainMenuResult::Selected { selected: selection },

View file

@ -1,6 +1,8 @@
use rltk::{GameState, Point, Rltk, RGB};
use specs::prelude::*;
use specs::saveload::{SimpleMarker, SimpleMarkerAllocator};
use std::ops::Add;
extern crate serde;
mod components;
pub use components::*;
@ -12,6 +14,7 @@ mod rect;
pub use rect::Rect;
mod gamelog;
mod gui;
mod saveload_system;
mod spawner;
mod visibility_system;
use visibility_system::VisibilitySystem;
@ -44,6 +47,7 @@ pub enum RunState {
ShowDropItem,
ShowTargeting { range: i32, item: Entity, aoe: i32 },
MainMenu { menu_selection: gui::MainMenuSelection },
SaveGame,
}
pub struct State {
@ -200,21 +204,22 @@ impl GameState for State {
gui::MainMenuResult::NoSelection { selected } => {
new_runstate = RunState::MainMenu { menu_selection: selected }
}
gui::MainMenuResult::Selected { selected } => {
match selected {
gui::MainMenuResult::Selected { selected } => match selected {
gui::MainMenuSelection::NewGame => new_runstate = RunState::PreRun,
gui::MainMenuSelection::LoadGame => {
new_runstate = RunState::PreRun;
//saveload_system::load_game(&mut self.ecs);
//rew_runstate = RunState::AwaitingInput;
//saveload_system::delete_save();
saveload_system::load_game(&mut self.ecs);
new_runstate = RunState::AwaitingInput;
saveload_system::delete_save();
}
gui::MainMenuSelection::Quit => {
::std::process::exit(0);
}
},
}
}
}
RunState::SaveGame => {
saveload_system::save_game(&mut self.ecs);
new_runstate = RunState::MainMenu { menu_selection: gui::MainMenuSelection::LoadGame };
}
}
@ -268,6 +273,9 @@ fn main() -> rltk::BError {
gs.ecs.register::<Consumable>();
gs.ecs.register::<Destructible>();
gs.ecs.register::<ParticleLifetime>();
gs.ecs.register::<SimpleMarker<SerializeMe>>();
gs.ecs.register::<SerializationHelper>();
gs.ecs.insert(SimpleMarkerAllocator::<SerializeMe>::new());
let map = Map::new_map_rooms_and_corridors();
let (player_x, player_y) = map.rooms[0].centre();

View file

@ -1,11 +1,12 @@
use super::Rect;
use rltk::{Algorithm2D, BaseMap, Point, RandomNumberGenerator, Rltk, RGB};
use serde::{Deserialize, Serialize};
use specs::prelude::*;
use std::cmp::{max, min};
use std::collections::HashSet;
use std::ops::{Add, Mul};
#[derive(PartialEq, Copy, Clone)]
#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)]
pub enum TileType {
Wall,
Floor,
@ -14,9 +15,9 @@ pub enum TileType {
pub const MAPWIDTH: usize = 80;
pub const MAPHEIGHT: usize = 43;
const MAX_OFFSET: u8 = 32;
const MAPCOUNT: usize = MAPHEIGHT * MAPWIDTH;
pub const MAPCOUNT: usize = MAPHEIGHT * MAPWIDTH;
#[derive(Default)]
#[derive(Default, Serialize, Deserialize, Clone)]
pub struct Map {
pub tiles: Vec<TileType>,
pub rooms: Vec<Rect>,
@ -28,8 +29,11 @@ pub struct Map {
pub green_offset: Vec<u8>,
pub blue_offset: Vec<u8>,
pub blocked: Vec<bool>,
pub tile_content: Vec<Vec<Entity>>,
pub bloodstains: HashSet<usize>,
#[serde(skip_serializing)]
#[serde(skip_deserializing)]
pub tile_content: Vec<Vec<Entity>>,
}
impl Map {
@ -98,8 +102,8 @@ impl Map {
green_offset: vec![0; MAPCOUNT],
blue_offset: vec![0; MAPCOUNT],
blocked: vec![false; MAPCOUNT],
tile_content: vec![Vec::new(); MAPCOUNT],
bloodstains: HashSet::new(),
tile_content: vec![Vec::new(); MAPCOUNT],
};
const MAX_ROOMS: i32 = 30;

View file

@ -98,7 +98,7 @@ pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState {
VirtualKeyCode::G => get_item(&mut gs.ecs),
VirtualKeyCode::I => return RunState::ShowInventory,
VirtualKeyCode::D => return RunState::ShowDropItem,
VirtualKeyCode::Escape => return RunState::MainMenu { menu_selection: gui::MainMenuSelection::NewGame },
VirtualKeyCode::Escape => return RunState::SaveGame,
_ => {
return RunState::AwaitingInput;
}

View file

@ -1,3 +1,6 @@
use serde::{Deserialize, Serialize};
#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)]
pub struct Rect {
pub x1: i32,
pub x2: i32,

170
src/saveload_system.rs Normal file
View file

@ -0,0 +1,170 @@
use super::components::*;
use specs::error::NoError;
use specs::prelude::*;
use specs::saveload::{DeserializeComponents, MarkedBuilder, SerializeComponents, SimpleMarker, SimpleMarkerAllocator};
use std::fs;
use std::fs::File;
use std::path::Path;
macro_rules! serialize_individually {
($ecs:expr, $ser:expr, $data:expr, $( $type:ty),*) => {
$(
SerializeComponents::<NoError, SimpleMarker<SerializeMe>>::serialize(
&( $ecs.read_storage::<$type>(), ),
&$data.0,
&$data.1,
&mut $ser,
)
.unwrap();
)*
};
}
#[cfg(target_arch = "wasm32")]
pub fn save_game(_ecs: &mut World) {}
#[cfg(not(target_arch = "wasm32"))]
pub fn save_game(ecs: &mut World) {
// Create helper
let mapcopy = ecs.get_mut::<super::map::Map>().unwrap().clone();
let savehelper =
ecs.create_entity().with(SerializationHelper { map: mapcopy }).marked::<SimpleMarker<SerializeMe>>().build();
// Actually serialize
{
let data = (ecs.entities(), ecs.read_storage::<SimpleMarker<SerializeMe>>());
let writer = File::create("./savegame.json").unwrap();
let mut serializer = serde_json::Serializer::new(writer);
serialize_individually!(
ecs,
serializer,
data,
Position,
Renderable,
Player,
Viewshed,
Monster,
Name,
BlocksTile,
CombatStats,
SufferDamage,
WantsToMelee,
Item,
Consumable,
Destructible,
Ranged,
InflictsDamage,
AOE,
Confusion,
ProvidesHealing,
InBackpack,
WantsToPickupItem,
WantsToUseItem,
WantsToDropItem,
SerializationHelper
);
}
// Clean up
ecs.delete_entity(savehelper).expect("Crash on cleanup");
}
pub fn does_save_exist() -> bool {
Path::new("./savegame.json").exists()
}
macro_rules! deserialize_individually {
($ecs:expr, $de:expr, $data:expr, $( $type:ty),*) => {
$(
DeserializeComponents::<NoError, _>::deserialize(
&mut ( &mut $ecs.write_storage::<$type>(), ),
&$data.0, // entities
&mut $data.1, // marker
&mut $data.2, // allocater
&mut $de,
)
.unwrap();
)*
};
}
pub fn load_game(ecs: &mut World) {
{
// Delete everything
let mut to_delete = Vec::new();
for e in ecs.entities().join() {
to_delete.push(e);
}
for del in to_delete.iter() {
ecs.delete_entity(*del).expect("Deletion failed");
}
}
let data = fs::read_to_string("./savegame.json").unwrap();
let mut de = serde_json::Deserializer::from_str(&data);
{
let mut d = (
&mut ecs.entities(),
&mut ecs.write_storage::<SimpleMarker<SerializeMe>>(),
&mut ecs.write_resource::<SimpleMarkerAllocator<SerializeMe>>(),
);
deserialize_individually!(
ecs,
de,
d,
Position,
Renderable,
Player,
Viewshed,
Monster,
Name,
BlocksTile,
CombatStats,
SufferDamage,
WantsToMelee,
Item,
Consumable,
Destructible,
Ranged,
InflictsDamage,
AOE,
Confusion,
ProvidesHealing,
InBackpack,
WantsToPickupItem,
WantsToUseItem,
WantsToDropItem,
SerializationHelper
);
}
let mut deleteme: Option<Entity> = None;
{
let entities = ecs.entities();
let helper = ecs.read_storage::<SerializationHelper>();
let player = ecs.read_storage::<Player>();
let position = ecs.read_storage::<Position>();
for (e, h) in (&entities, &helper).join() {
let mut worldmap = ecs.write_resource::<super::map::Map>();
*worldmap = h.map.clone();
worldmap.tile_content = vec![Vec::new(); super::map::MAPCOUNT];
deleteme = Some(e);
}
for (e, _p, pos) in (&entities, &player, &position).join() {
let mut ppos = ecs.write_resource::<rltk::Point>();
*ppos = rltk::Point::new(pos.x, pos.y);
let mut player_resource = ecs.write_resource::<Entity>();
*player_resource = e;
}
}
ecs.delete_entity(deleteme.unwrap()).expect("Unable to delete helper");
}
pub fn delete_save() {
if Path::new("./savegame.json").exists() {
std::fs::remove_file("./savegame.json").expect("Unable to delete file");
}
}

View file

@ -1,9 +1,10 @@
use super::{
BlocksTile, CombatStats, Confusion, Consumable, Destructible, InflictsDamage, Item, Monster, Name, Player,
Position, ProvidesHealing, Ranged, Rect, Renderable, Viewshed, AOE, MAPWIDTH,
Position, ProvidesHealing, Ranged, Rect, Renderable, SerializeMe, Viewshed, AOE, MAPWIDTH,
};
use rltk::{RandomNumberGenerator, RGB};
use specs::prelude::*;
use specs::saveload::{MarkedBuilder, SimpleMarker};
/// Spawns the player and returns his/her entity object.
pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity {
@ -19,6 +20,7 @@ pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity {
.with(Viewshed { visible_tiles: Vec::new(), range: 12, dirty: true })
.with(Name { name: "hero (you)".to_string() })
.with(CombatStats { max_hp: 30, hp: 30, defence: 2, power: 5 })
.marked::<SimpleMarker<SerializeMe>>()
.build()
}
@ -64,6 +66,7 @@ fn monster<S: ToString>(ecs: &mut World, x: i32, y: i32, glyph: rltk::FontCharTy
.with(Name { name: name.to_string() })
.with(BlocksTile {})
.with(CombatStats { max_hp: 16, hp: 16, defence: 1, power: 4 })
.marked::<SimpleMarker<SerializeMe>>()
.build();
}
@ -143,6 +146,7 @@ fn health_potion(ecs: &mut World, x: i32, y: i32) {
.with(Consumable {})
.with(Destructible {})
.with(ProvidesHealing { amount: 12 })
.marked::<SimpleMarker<SerializeMe>>()
.build();
}
@ -160,6 +164,7 @@ fn weak_health_potion(ecs: &mut World, x: i32, y: i32) {
.with(Consumable {})
.with(Destructible {})
.with(ProvidesHealing { amount: 6 })
.marked::<SimpleMarker<SerializeMe>>()
.build();
}
@ -177,6 +182,7 @@ fn poison_potion(ecs: &mut World, x: i32, y: i32) {
.with(Consumable {})
.with(Destructible {})
.with(ProvidesHealing { amount: -12 })
.marked::<SimpleMarker<SerializeMe>>()
.build();
}
@ -197,6 +203,7 @@ fn magic_missile_scroll(ecs: &mut World, x: i32, y: i32) {
.with(Destructible {})
.with(Ranged { range: 12 }) // Long range - as far as default vision range
.with(InflictsDamage { amount: 10 }) // Low~ damage
.marked::<SimpleMarker<SerializeMe>>()
.build();
}
@ -216,6 +223,7 @@ fn fireball_scroll(ecs: &mut World, x: i32, y: i32) {
.with(Ranged { range: 10 })
.with(InflictsDamage { amount: 20 })
.with(AOE { radius: 3 })
.marked::<SimpleMarker<SerializeMe>>()
.build();
}
@ -234,5 +242,6 @@ fn confusion_scroll(ecs: &mut World, x: i32, y: i32) {
.with(Destructible {})
.with(Ranged { range: 10 })
.with(Confusion { turns: 4 })
.marked::<SimpleMarker<SerializeMe>>()
.build();
}