use dotzuki_engine::battle::{
BattleAI, BattleProvider, BattleState, BattlerState, DamageResult, EffectHandler,
EffectResult, EnumMap, MoveEffect, TypeChart,
};
use dotzuki_engine::items::{ItemProvider, ItemResult, ShopProvider};
use dotzuki_engine::map::MapTrait;
use dotzuki_engine::menu::{MenuInput, MenuLayout, MenuOption, MenuProvider, MenuSystem};
use dotzuki_engine::overworld::{
CollisionProvider, Direction, MapConnections, MapData, NpcDefinition, NpcMovementType,
NpcRuntimeState, OverworldState, try_move,
};
use dotzuki_engine::save::{InMemoryStorage, SaveData, SaveError, SaveManager, SaveSlot};
use dotzuki_engine::text::{
ControlAction, DialogEngine, DialogState, TextProvider, TileBuffer,
};
use dotzuki_engine::tileset::TilesetTrait;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Element { Physical, Magical }
#[derive(Clone, Debug, PartialEq, Eq)]
enum Species { Warrior, Mage }
impl Species {
fn name(&self) -> &str { match self { Self::Warrior => "Warrior", Self::Mage => "Mage" } }
fn element(&self) -> Element { match self { Self::Warrior => Element::Physical, Self::Mage => Element::Magical } }
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum MoveKind { Slash, Fireball }
impl MoveKind {
fn name(&self) -> &str { match self { Self::Slash => "Slash", Self::Fireball => "Fireball" } }
fn power(&self) -> u16 { match self { Self::Slash => 10, Self::Fireball => 12 } }
fn element(&self) -> Element { match self { Self::Slash => Element::Physical, Self::Fireball => Element::Magical } }
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum StatId { HP, ATK, DEF, SPD }
#[derive(Clone, Debug, PartialEq, Eq)]
enum StatusKind { Healthy, Burned }
#[derive(Clone, Debug, PartialEq, Eq)]
enum Ability { Brave, Arcane }
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum ItemKind { Potion, Elixir }
#[derive(Clone, Debug, PartialEq, Eq)]
enum ItemEffect { Heal(u16), CureStatus, None }
#[derive(Clone, Debug, PartialEq, Eq)]
struct MonsterData { species: Species, max_hp: u16, current_hp: u16 }
#[derive(Clone, Debug, PartialEq, Eq)]
enum AsciiChar { Char(char), Newline, WaitInput, Done }
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct MapId(u8);
impl MapTrait for MapId {}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct Tileset(u8);
impl TilesetTrait for Tileset {
fn id(&self) -> u8 { self.0 }
fn name(&self) -> &'static str { "TownTiles" }
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum MenuScreen { Main }
struct HelloConfig {
main_menu_options: Vec<MenuOption>,
}
impl HelloConfig {
fn new() -> Self {
Self {
main_menu_options: vec![
MenuOption::new("Hello"),
MenuOption::new("Battle"),
MenuOption::new("Quit"),
],
}
}
}
impl Default for HelloConfig {
fn default() -> Self {
Self::new()
}
}
impl TypeChart for HelloConfig {
type Type = Element;
fn effectiveness(attacking: &Self::Type, defending: &[Self::Type]) -> f32 {
let def = defending.first().copied().unwrap_or(Element::Physical);
match (attacking, def) {
(Element::Physical, Element::Magical) => 2.0,
(Element::Magical, Element::Physical) => 0.5,
_ => 1.0,
}
}
}
impl BattleProvider for HelloConfig {
type Monster = MonsterData;
type Move = MoveKind;
type Ability = Ability;
type Status = StatusKind;
type Stat = StatId;
type Species = Species;
type Type = Element;
type Item = ItemKind;
fn calculate_damage(
&self, move_: &MoveKind, attacker: &BattlerState<Self>,
defender: &BattlerState<Self>, _random: u8, _is_critical: bool,
) -> DamageResult {
let atk = attacker.stats.get(StatId::ATK).copied().unwrap_or(10);
let def = defender.stats.get(StatId::DEF).copied().unwrap_or(5).max(1);
let eff = HelloConfig::effectiveness(&move_.element(), &[defender.species.element()]);
let raw = (move_.power() as f32 * eff * atk as f32 / def as f32) as u16;
DamageResult { damage: raw.max(1), effectiveness: eff, is_miss: false }
}
fn select_move(&self, battler: &BattlerState<Self>, _state: &BattleState<Self>) -> MoveKind {
battler.moves.first().cloned().unwrap_or(MoveKind::Slash)
}
fn apply_move_effect(
&self, effect: MoveEffect, user: &mut BattlerState<Self>,
target: &mut BattlerState<Self>,
) -> EffectResult {
match effect {
MoveEffect::Damage => {
let mv = user.moves.first().cloned().unwrap_or(MoveKind::Slash);
let result = self.calculate_damage(&mv, user, target, 100, false);
target.take_damage(result.damage);
EffectResult::DamageDealt { amount: result.damage }
}
MoveEffect::Heal => {
let amount = target.max_hp / 4;
target.heal(amount);
EffectResult::Healed { amount }
}
MoveEffect::StatusCondition => EffectResult::StatusInflicted,
MoveEffect::StatChange => EffectResult::StatModified { stages: 1 },
_ => EffectResult::NoEffect,
}
}
fn create_monster(&self, species: Species, level: u8) -> BattlerState<Self> {
let (hp, atk, def, spd, moves) = match species {
Species::Warrior => (100u16, 15u16, 10u16, 8u16, vec![MoveKind::Slash]),
Species::Mage => (80u16, 20u16, 5u16, 12u16, vec![MoveKind::Fireball]),
};
let mut stats = EnumMap::new();
stats.set(StatId::HP, hp);
stats.set(StatId::ATK, atk);
stats.set(StatId::DEF, def);
stats.set(StatId::SPD, spd + level as u16);
BattlerState::new(species, hp, hp, stats, moves)
}
}
impl BattleAI<HelloConfig> for HelloConfig {
fn select_move(&self, battler: &BattlerState<HelloConfig>, _state: &BattleState<HelloConfig>) -> MoveKind {
battler.moves.first().cloned().unwrap_or(MoveKind::Slash)
}
fn should_switch(&self, _battler: &BattlerState<HelloConfig>) -> bool { false }
fn should_use_item(&self, _battler: &BattlerState<HelloConfig>) -> Option<ItemKind> { None }
}
impl EffectHandler<HelloConfig> for HelloConfig {
fn handle_effect(
&self, effect: MoveEffect, user: &mut BattlerState<HelloConfig>,
target: &mut BattlerState<HelloConfig>, _provider: &HelloConfig,
) -> EffectResult {
self.apply_move_effect(effect, user, target)
}
}
impl ItemProvider for HelloConfig {
type Item = ItemKind;
type Effect = ItemEffect;
type Monster = MonsterData;
type CustomKind = ();
fn item_name(&self, item: &ItemKind) -> &str {
match item { ItemKind::Potion => "Potion", ItemKind::Elixir => "Elixir" }
}
fn item_description(&self, item: &ItemKind) -> &str {
match item { ItemKind::Potion => "Restores 20 HP.", ItemKind::Elixir => "Cures status." }
}
fn item_effect(&self, item: &ItemKind) -> ItemEffect {
match item { ItemKind::Potion => ItemEffect::Heal(20), ItemKind::Elixir => ItemEffect::CureStatus }
}
fn item_price(&self, item: &ItemKind) -> u32 {
match item { ItemKind::Potion => 100, ItemKind::Elixir => 300 }
}
fn can_use_outside_battle(&self, _item: &ItemKind) -> bool { true }
fn can_use_in_battle(&self, _item: &ItemKind) -> bool { true }
fn use_on_monster(&self, item: &ItemKind, monster: &mut MonsterData) -> ItemResult {
match self.item_effect(item) {
ItemEffect::Heal(amount) => {
if monster.current_hp >= monster.max_hp { return ItemResult::NoEffect; }
monster.current_hp = (monster.current_hp + amount).min(monster.max_hp);
ItemResult::Used
}
ItemEffect::CureStatus => ItemResult::Used,
ItemEffect::None => ItemResult::NoEffect,
}
}
fn consume(&self, _item: &ItemKind) -> bool { true }
fn item_kind(&self, item: &ItemKind) -> dotzuki_engine::items::ItemKind<()> {
let _ = item;
dotzuki_engine::items::ItemKind::Consumable
}
}
impl ShopProvider for HelloConfig {
type Item = ItemKind;
type ShopId = ();
fn shop_inventory(&self, _shop_id: &()) -> Vec<(ItemKind, u32)> {
vec![(ItemKind::Potion, 100), (ItemKind::Elixir, 300)]
}
fn shop_name(&self, _shop_id: &()) -> &str { "Town Shop" }
}
impl TextProvider for HelloConfig {
type Char = AsciiChar;
fn decode_byte(&self, byte: u8) -> Option<AsciiChar> {
match byte {
0xFE => Some(AsciiChar::Newline),
0xFF => Some(AsciiChar::Done),
0xFD => Some(AsciiChar::WaitInput),
b @ 0x20..=0x7E => Some(AsciiChar::Char(b as char)),
_ => None,
}
}
fn render_char(&self, c: &AsciiChar, buffer: &mut TileBuffer) {
if let AsciiChar::Char(ch) = c {
let pos = buffer.cursor;
buffer.set_tile(pos, *ch as u16, 0);
buffer.cursor.x += 1;
}
}
fn string_width(&self, text: &[AsciiChar]) -> u16 {
text.iter().filter(|c| matches!(c, AsciiChar::Char(_))).count() as u16 * 8
}
fn is_control_code(&self, c: &AsciiChar) -> bool { !matches!(c, AsciiChar::Char(_)) }
fn process_control(&self, c: &AsciiChar, _state: &mut DialogState) -> ControlAction {
match c {
AsciiChar::Newline => ControlAction::Newline,
AsciiChar::Done => ControlAction::Done,
AsciiChar::WaitInput => ControlAction::WaitInput,
_ => ControlAction::None,
}
}
}
impl MenuProvider for HelloConfig {
type MenuId = MenuScreen;
fn title(&self, _menu: MenuScreen) -> &str { "Hello JRPG" }
fn options(&self, _menu: MenuScreen) -> &[MenuOption] { &self.main_menu_options }
fn option_count(&self, _menu: MenuScreen) -> u8 { self.main_menu_options.len() as u8 }
fn scrollable(&self, _menu: MenuScreen) -> bool { false }
fn layout(&self, _menu: MenuScreen) -> MenuLayout {
MenuLayout::new(5, 5, 10, 8)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct GameSave { player_name: String, player_level: u8 }
impl SaveData for GameSave {
fn serialize(&self) -> Vec<u8> {
let name = self.player_name.as_bytes();
let len = name.len().min(32);
let mut v = Vec::with_capacity(1 + len + 1);
v.push(len as u8);
v.extend_from_slice(&name[..len]);
v.push(self.player_level);
v
}
fn deserialize(data: &[u8]) -> Result<Self, SaveError> {
if data.len() < 2 { return Err(SaveError::InvalidData); }
let name_len = data[0] as usize;
if data.len() < 1 + name_len + 1 { return Err(SaveError::InvalidData); }
let name = String::from_utf8(data[1..1 + name_len].to_vec())
.map_err(|_| SaveError::InvalidData)?;
let level = data[1 + name_len];
Ok(GameSave { player_name: name, player_level: level })
}
fn save_size() -> usize { 35 }
}
struct SimpleCollision;
impl CollisionProvider<Tileset> for SimpleCollision {
fn is_tile_passable(&self, _tileset: Tileset, tile_id: u8) -> bool { tile_id != 0xFF }
fn check_tile_pair_collision(&self, _t: Tileset, _a: u8, _b: u8, _w: bool) -> bool { false }
fn check_ledge_jump(&self, _t: Tileset, _f: u8, _s: u8, _tg: u8, _i: u8) -> bool { false }
fn is_counter_tile(&self, _t: Tileset, _id: u8) -> bool { false }
fn get_tile_at_position(&self, _t: Tileset, blk: &[u8], w: u8, x: u16, y: u16) -> u8 {
blk.get((y as usize * w as usize + x as usize).min(blk.len().saturating_sub(1))).copied().unwrap_or(0)
}
fn is_door_tile(&self, _t: Tileset, _id: u8) -> bool { false }
fn is_warp_tile(&self, _t: Tileset, _id: u8) -> bool { false }
fn is_warp_carpet_tile_in_front(&self, _t: Tileset, _f: u8, _id: u8) -> bool { false }
fn uses_warp_tile_in_front_check(&self, _t: Tileset) -> bool { false }
fn check_extra_warp_special(&self, _t: Tileset, _id: u8) -> Option<bool> { None }
}
fn build_town_map() -> MapData<MapId, Tileset, ()> {
let blocks: Vec<u8> = vec![
0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0x01, 0x01, 0x01, 0xFF,
0xFF, 0x01, 0x01, 0x01, 0xFF,
0xFF, 0x01, 0x01, 0x01, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
];
let npcs = vec![NpcDefinition::new(0, 2, 2, NpcMovementType::Stationary, Direction::Down, 0, 0)];
MapData::new(
MapId(0), 5, 5, Tileset(0), (),
blocks, vec![], npcs, vec![],
MapConnections::default(),
)
}
fn build_npc_states(npcs: &[NpcDefinition]) -> Vec<NpcRuntimeState> {
npcs.iter().enumerate().map(|(i, def)| {
let s = NpcRuntimeState {
npc_index: i as u8, sprite_id: def.sprite_id,
x: def.x as u16, y: def.y as u16,
home_x: def.x as u16, home_y: def.y as u16,
facing: def.facing, scripted_frame: None,
movement_type: def.movement, range: def.range,
walk_counter: 0, delay_counter: 0, text_id: def.text_id,
defeated: false, visible: true,
scripted_path: std::collections::VecDeque::new(),
};
s
}).collect()
}
fn show_map(blocks: &[u8], _w: u8, px: u16, py: u16, npcs: &[NpcRuntimeState]) {
for y in 0..5u16 {
print!(" ");
for x in 0..5u16 {
let idx = (y * 5 + x) as usize;
if px == x && py == y {
print!("P");
} else if npcs.iter().any(|n| n.x == x && n.y == y) {
print!("N");
} else if idx < blocks.len() && blocks[idx] == 0xFF {
print!("#");
} else {
print!(".");
}
}
println!();
}
}
fn demo_overworld() {
println!("\n╔══ OVERWORLD MOVEMENT ═══════════════════╗");
let map = build_town_map();
let mut state = OverworldState::new(MapId(0));
let collision = SimpleCollision;
let npcs = build_npc_states(&map.npcs);
let npc_positions = dotzuki_engine::overworld::get_npc_positions(&npcs);
state.player.x = 3; state.player.y = 3;
println!("║ Initial map:");
show_map(&map.blocks, 5, state.player.x, state.player.y, &npcs);
let standing = collision.get_tile_at_position(Tileset(0), &map.blocks, 5, 3, 3);
let target = collision.get_tile_at_position(Tileset(0), &map.blocks, 5, 4, 3);
let result = try_move(&mut state, Direction::Right, Tileset(0), 5, 5, standing, target, &npc_positions, 0, &collision);
println!("║ Move Right → {:?}", result);
let standing = collision.get_tile_at_position(Tileset(0), &map.blocks, 5, 3, 3);
let target = collision.get_tile_at_position(Tileset(0), &map.blocks, 5, 3, 2);
let result = try_move(&mut state, Direction::Up, Tileset(0), 5, 5, standing, target, &npc_positions, 0, &collision);
println!("║ Move Up → {:?}", result);
println!("║ Player at ({}, {}), facing {:?}", state.player.x, state.player.y, state.player.facing);
dotzuki_engine::overworld::advance_step(&mut state);
dotzuki_engine::overworld::advance_step(&mut state);
for _ in 0..8 { dotzuki_engine::overworld::advance_step(&mut state); }
println!("║ After walk: player at ({}, {})", state.player.x, state.player.y);
let interaction = dotzuki_engine::overworld::try_interact(
&npcs, state.player.x, state.player.y, state.player.facing,
Some(&map), &collision,
);
println!("║ Interact → {:?}", interaction);
println!("╚══════════════════════════════════════════╝");
}
fn demo_dialog() {
println!("\n╔══ NPC DIALOG ═══════════════════════════╗");
let provider = HelloConfig::new();
let mut engine = DialogEngine::new(provider);
let mut buffer = TileBuffer::new(20, 18);
let text: &[u8] = b"Greetings, young hero!";
let mut full = text.to_vec();
full.push(0xFF);
engine.open_dialog(&full);
while engine.is_active() { engine.update(&mut buffer); }
print!("║ Merlin: \"");
for i in 0..20usize {
let t = buffer.tiles[i].tile_id;
if (0x20..=0x7E).contains(&t) { print!("{}", t as u8 as char); }
}
println!("\"");
println!("╚══════════════════════════════════════════╝");
}
fn demo_battle() {
println!("\n╔══ BATTLE ENCOUNTER ═════════════════════╗");
let provider = HelloConfig::new();
let warrior = provider.create_monster(Species::Warrior, 5);
let mage = provider.create_monster(Species::Mage, 5);
println!("║ {} (HP:{}/{}) vs {} (HP:{}/{})",
warrior.species.name(), warrior.hp, warrior.max_hp,
mage.species.name(), mage.hp, mage.max_hp);
let result = provider.calculate_damage(&MoveKind::Slash, &warrior, &mage, 100, false);
println!("║ {} uses Slash! {} damage ({}x effective)",
warrior.species.name(), result.damage, result.effectiveness);
let mut enemy = mage.clone();
enemy.take_damage(result.damage);
println!("║ {} HP: {} → {}", mage.species.name(), mage.hp, enemy.hp);
let result = provider.calculate_damage(&MoveKind::Fireball, &mage, &warrior, 100, false);
println!("║ {} uses Fireball! {} damage ({}x effective)",
mage.species.name(), result.damage, result.effectiveness);
let mut hero = warrior.clone();
hero.take_damage(result.damage);
println!("║ {} HP: {} → {}", warrior.species.name(), warrior.hp, hero.hp);
let mut user = warrior.clone();
let mut target = mage.clone();
let eff_result = HelloConfig::new().handle_effect(MoveEffect::Damage, &mut user, &mut target, &HelloConfig::new());
println!("║ EffectHandler(Damage) → {:?}", eff_result);
let state = BattleState::new(vec![warrior.clone()], vec![mage.clone()]);
let ai_move = BattleAI::select_move(&HelloConfig::default(), &warrior, &state);
println!("║ BattleAI chose: {}", ai_move.name());
println!("╚══════════════════════════════════════════╝");
}
fn demo_menu() {
println!("\n╔══ MENU NAVIGATION ═════════════════════╗");
let provider = HelloConfig::new();
let mut menu = MenuSystem::new(&provider);
menu.open(MenuScreen::Main);
println!("║ Menu '{}' — 3 options", provider.title(MenuScreen::Main));
for (i, opt) in provider.main_menu_options.iter().enumerate() {
let marker = if i == menu.cursor as usize { ">" } else { " " };
println!("║ {} {}{}", marker, opt.label, if opt.enabled { "" } else { " (disabled)" });
}
menu.handle_input(&MenuInput { down: true, ..Default::default() });
menu.handle_input(&MenuInput { down: true, ..Default::default() });
println!("║ Cursor after 2× Down: {}", menu.cursor);
let action = menu.handle_input(&MenuInput { confirm: true, ..Default::default() });
println!("║ Confirm → {:?} ('{}')", action,
menu.selected_option().map(|o| o.label.as_str()).unwrap_or("?"));
println!("╚══════════════════════════════════════════╝");
}
fn demo_items() {
println!("\n╔══ ITEM SYSTEM ═════════════════════════╗");
let provider = HelloConfig::new();
let mut monster = MonsterData { species: Species::Warrior, max_hp: 100, current_hp: 30 };
println!("║ {} HP before: {}/{}", monster.species.name(), monster.current_hp, monster.max_hp);
let r = provider.use_on_monster(&ItemKind::Potion, &mut monster);
println!("║ Used Potion → {:?}, HP now: {}/{}", r, monster.current_hp, monster.max_hp);
let shop = provider.shop_inventory(&());
println!("║ Shop inventory: {:?}", shop);
println!("╚══════════════════════════════════════════╝");
}
fn demo_save_load() {
println!("\n╔══ SAVE / LOAD ═════════════════════════╗");
let storage = Box::new(InMemoryStorage::new());
let manager = SaveManager::<GameSave>::new(storage);
let save = GameSave { player_name: "Hero".to_string(), player_level: 7 };
manager.save(SaveSlot::Slot1, &save).expect("save");
let loaded = manager.load(SaveSlot::Slot1).expect("load");
println!("║ Saved: {:?}", save);
println!("║ Loaded: {:?}", loaded);
assert_eq!(save, loaded);
println!("║ ✓ Round-trip verified");
println!("╚══════════════════════════════════════════╝");
}
fn main() {
println!("╔══════════════════════════════════════════╗");
println!("║ Hello JRPG — Engine Demo v0.2 ║");
println!("║ Zero pokered-* imports. Pure engine. ║");
println!("╚══════════════════════════════════════════╝");
demo_overworld();
demo_dialog();
demo_battle();
demo_items();
demo_menu();
demo_save_load();
println!("\nAll demos complete. ✓");
}
#[cfg(test)]
mod tests {
use super::*;
use dotzuki_engine::items::Inventory;
use dotzuki_engine::menu::MenuAction;
#[test]
fn test_warrior_beats_mage() {
let provider = HelloConfig::new();
let w = provider.create_monster(Species::Warrior, 5);
let m = provider.create_monster(Species::Mage, 5);
assert_eq!(w.hp, 100);
assert_eq!(m.hp, 80);
let r = provider.calculate_damage(&MoveKind::Slash, &w, &m, 100, false);
assert!((r.effectiveness - 2.0).abs() < f32::EPSILON);
assert!(r.damage > 0);
}
#[test]
fn test_mage_weak_to_warrior() {
let provider = HelloConfig::new();
let w = provider.create_monster(Species::Warrior, 5);
let m = provider.create_monster(Species::Mage, 5);
let r = provider.calculate_damage(&MoveKind::Fireball, &m, &w, 100, false);
assert!((r.effectiveness - 0.5).abs() < f32::EPSILON);
}
#[test]
fn test_dialog_stream() {
let provider = HelloConfig::new();
let mut engine = DialogEngine::new(provider);
let mut buffer = TileBuffer::new(20, 18);
engine.open_dialog(&[b'H', b'I', 0xFF]);
assert!(engine.is_active());
engine.update(&mut buffer); engine.update(&mut buffer); engine.update(&mut buffer); assert!(!engine.is_active());
assert_eq!(buffer.tiles[0].tile_id, b'H' as u16);
assert_eq!(buffer.tiles[1].tile_id, b'I' as u16);
}
#[test]
fn test_dialog_newline_control() {
let provider = HelloConfig::new();
let mut engine = DialogEngine::new(provider);
let mut buffer = TileBuffer::new(20, 18);
engine.open_dialog(&[b'A', 0xFE, b'B', 0xFF]);
for _ in 0..4 { engine.update(&mut buffer); }
assert_eq!(buffer.tiles[0].tile_id, b'A' as u16);
assert_eq!(buffer.tiles[20].tile_id, b'B' as u16); }
#[test]
fn test_menu_cursor_movement() {
let provider = HelloConfig::new();
let mut menu = MenuSystem::new(&provider);
menu.open(MenuScreen::Main);
assert_eq!(menu.cursor, 0);
let a = menu.handle_input(&MenuInput { down: true, ..Default::default() });
assert_eq!(a, MenuAction::Down);
assert_eq!(menu.cursor, 1);
let a = menu.handle_input(&MenuInput { down: true, ..Default::default() });
assert_eq!(a, MenuAction::Down);
assert_eq!(menu.cursor, 2);
let a = menu.handle_input(&MenuInput { down: true, ..Default::default() });
assert_eq!(a, MenuAction::None);
}
#[test]
fn test_menu_selection_and_cancel() {
let provider = HelloConfig::new();
let mut menu = MenuSystem::new(&provider);
menu.open(MenuScreen::Main);
menu.handle_input(&MenuInput { down: true, ..Default::default() });
let a = menu.handle_input(&MenuInput { confirm: true, ..Default::default() });
assert_eq!(a, MenuAction::Selected(1));
menu.open(MenuScreen::Main);
let a = menu.handle_input(&MenuInput { cancel: true, ..Default::default() });
assert_eq!(a, MenuAction::Cancelled);
assert!(!menu.is_open());
}
#[test]
fn test_save_load_roundtrip() {
let storage = Box::new(InMemoryStorage::new());
let manager = SaveManager::<GameSave>::new(storage);
let save = GameSave { player_name: "Hero".to_string(), player_level: 42 };
manager.save(SaveSlot::Slot1, &save).expect("save");
let loaded = manager.load(SaveSlot::Slot1).expect("load");
assert_eq!(loaded, save);
}
#[test]
fn test_save_empty_slot_error() {
let storage = Box::new(InMemoryStorage::new());
let manager = SaveManager::<GameSave>::new(storage);
let result = manager.load(SaveSlot::Slot1);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), SaveError::SlotEmpty);
}
#[test]
fn test_potion_heals() {
let provider = HelloConfig::new();
let mut m = MonsterData { species: Species::Warrior, max_hp: 100, current_hp: 50 };
assert_eq!(provider.use_on_monster(&ItemKind::Potion, &mut m), ItemResult::Used);
assert_eq!(m.current_hp, 70);
}
#[test]
fn test_potion_no_effect_full_hp() {
let provider = HelloConfig::new();
let mut m = MonsterData { species: Species::Warrior, max_hp: 100, current_hp: 100 };
assert_eq!(provider.use_on_monster(&ItemKind::Potion, &mut m), ItemResult::NoEffect);
}
#[test]
fn test_physical_beats_magical() {
let eff = HelloConfig::effectiveness(&Element::Physical, &[Element::Magical]);
assert!((eff - 2.0).abs() < f32::EPSILON);
}
#[test]
fn test_magical_weak_to_physical() {
let eff = HelloConfig::effectiveness(&Element::Magical, &[Element::Physical]);
assert!((eff - 0.5).abs() < f32::EPSILON);
}
#[test]
fn test_inventory_ops() {
let mut inv: Inventory<ItemKind, 64> = Inventory::new();
inv.add(ItemKind::Potion, 5);
assert!(inv.contains(&ItemKind::Potion, 3));
assert!(!inv.contains(&ItemKind::Potion, 6));
assert!(inv.remove(&ItemKind::Potion, 2));
assert!(inv.contains(&ItemKind::Potion, 3));
}
}