Skip to main content

BattlerState

Struct BattlerState 

Source
pub struct BattlerState<P: BattleProvider + ?Sized> {
    pub species: P::Species,
    pub hp: u16,
    pub max_hp: u16,
    pub level: u8,
    pub stats: EnumMap<P::Stat, u16>,
    pub stat_stages: EnumMap<P::Stat, i8>,
    pub status: Option<P::Status>,
    pub moves: Vec<P::Move>,
    pub resources: ResourcePool,
}
Expand description

The battle state of a single monster/character.

Tracks HP, stats, stat-stage modifiers, status condition, and known moves. Generic over the BattleProvider that supplies the concrete type identifiers.

Fields§

§species: P::Species

Species of this monster.

§hp: u16

Current hit points.

§max_hp: u16

Maximum hit points.

§level: u8

The battler’s level. Defaults to 50 in new (set it via with_level or the field directly). The engine never interprets it; a provider’s damage/effect logic reads it as needed (e.g. the level term in a damage formula). Additive — existing callers that ignore it keep the prior fixed-50 behaviour.

§stats: EnumMap<P::Stat, u16>

Base stat values, keyed by stat ID.

§stat_stages: EnumMap<P::Stat, i8>

Stat-stage modifiers (-6 to +6), keyed by stat ID.

§status: Option<P::Status>

Current status condition, if any.

§moves: Vec<P::Move>

Known moves.

§resources: ResourcePool

Generic, game-defined consumable resources (MP / SP / mana / charge — doc 13 §4). Defaults to EMPTY, so a battler that declares no resource behaves exactly as before. Keyed by an opaque game-assigned u16 id; the engine never interprets a resource’s meaning (it is not “MP” to the engine). See ResourcePool.

Implementations§

Source§

impl<P: BattleProvider + ?Sized> BattlerState<P>

Source

pub fn new( species: P::Species, hp: u16, max_hp: u16, stats: EnumMap<P::Stat, u16>, moves: Vec<P::Move>, ) -> Self

Create a new battler state.

Examples found in repository?
examples/hello_dotzuki.rs (line 179)
169    fn create_monster(&self, species: Species, level: u8) -> BattlerState<Self> {
170        let (hp, atk, def, spd, moves) = match species {
171            Species::Warrior => (100u16, 15u16, 10u16, 8u16, vec![MoveKind::Slash]),
172            Species::Mage => (80u16, 20u16, 5u16, 12u16, vec![MoveKind::Fireball]),
173        };
174        let mut stats = EnumMap::new();
175        stats.set(StatId::HP, hp);
176        stats.set(StatId::ATK, atk);
177        stats.set(StatId::DEF, def);
178        stats.set(StatId::SPD, spd + level as u16);
179        BattlerState::new(species, hp, hp, stats, moves)
180    }
Source

pub fn with_level(self, level: u8) -> Self

Builder: set the battler’s level.

Source

pub fn take_damage(&mut self, amount: u16)

Apply damage, clamping HP to zero (never negative).

Examples found in repository?
examples/hello_dotzuki.rs (line 155)
147    fn apply_move_effect(
148        &self, effect: MoveEffect, user: &mut BattlerState<Self>,
149        target: &mut BattlerState<Self>,
150    ) -> EffectResult {
151        match effect {
152            MoveEffect::Damage => {
153                let mv = user.moves.first().cloned().unwrap_or(MoveKind::Slash);
154                let result = self.calculate_damage(&mv, user, target, 100, false);
155                target.take_damage(result.damage);
156                EffectResult::DamageDealt { amount: result.damage }
157            }
158            MoveEffect::Heal => {
159                let amount = target.max_hp / 4;
160                target.heal(amount);
161                EffectResult::Healed { amount }
162            }
163            MoveEffect::StatusCondition => EffectResult::StatusInflicted,
164            MoveEffect::StatChange => EffectResult::StatModified { stages: 1 },
165            _ => EffectResult::NoEffect,
166        }
167    }
168
169    fn create_monster(&self, species: Species, level: u8) -> BattlerState<Self> {
170        let (hp, atk, def, spd, moves) = match species {
171            Species::Warrior => (100u16, 15u16, 10u16, 8u16, vec![MoveKind::Slash]),
172            Species::Mage => (80u16, 20u16, 5u16, 12u16, vec![MoveKind::Fireball]),
173        };
174        let mut stats = EnumMap::new();
175        stats.set(StatId::HP, hp);
176        stats.set(StatId::ATK, atk);
177        stats.set(StatId::DEF, def);
178        stats.set(StatId::SPD, spd + level as u16);
179        BattlerState::new(species, hp, hp, stats, moves)
180    }
181}
182
183// ── BattleAI ───────────────────────────────────────────────────────
184
185impl BattleAI<HelloConfig> for HelloConfig {
186    fn select_move(&self, battler: &BattlerState<HelloConfig>, _state: &BattleState<HelloConfig>) -> MoveKind {
187        battler.moves.first().cloned().unwrap_or(MoveKind::Slash)
188    }
189    fn should_switch(&self, _battler: &BattlerState<HelloConfig>) -> bool { false }
190    fn should_use_item(&self, _battler: &BattlerState<HelloConfig>) -> Option<ItemKind> { None }
191}
192
193// ── EffectHandler ──────────────────────────────────────────────────
194
195impl EffectHandler<HelloConfig> for HelloConfig {
196    fn handle_effect(
197        &self, effect: MoveEffect, user: &mut BattlerState<HelloConfig>,
198        target: &mut BattlerState<HelloConfig>, _provider: &HelloConfig,
199    ) -> EffectResult {
200        // Delegate to the BattleProvider impl.
201        self.apply_move_effect(effect, user, target)
202    }
203}
204
205// ── ItemProvider ──────────────────────────────────────────────────
206
207impl ItemProvider for HelloConfig {
208    type Item = ItemKind;
209    type Effect = ItemEffect;
210    type Monster = MonsterData;
211    type CustomKind = ();
212
213    fn item_name(&self, item: &ItemKind) -> &str {
214        match item { ItemKind::Potion => "Potion", ItemKind::Elixir => "Elixir" }
215    }
216    fn item_description(&self, item: &ItemKind) -> &str {
217        match item { ItemKind::Potion => "Restores 20 HP.", ItemKind::Elixir => "Cures status." }
218    }
219    fn item_effect(&self, item: &ItemKind) -> ItemEffect {
220        match item { ItemKind::Potion => ItemEffect::Heal(20), ItemKind::Elixir => ItemEffect::CureStatus }
221    }
222    fn item_price(&self, item: &ItemKind) -> u32 {
223        match item { ItemKind::Potion => 100, ItemKind::Elixir => 300 }
224    }
225    fn can_use_outside_battle(&self, _item: &ItemKind) -> bool { true }
226    fn can_use_in_battle(&self, _item: &ItemKind) -> bool { true }
227    fn use_on_monster(&self, item: &ItemKind, monster: &mut MonsterData) -> ItemResult {
228        match self.item_effect(item) {
229            ItemEffect::Heal(amount) => {
230                if monster.current_hp >= monster.max_hp { return ItemResult::NoEffect; }
231                monster.current_hp = (monster.current_hp + amount).min(monster.max_hp);
232                ItemResult::Used
233            }
234            ItemEffect::CureStatus => ItemResult::Used,
235            ItemEffect::None => ItemResult::NoEffect,
236        }
237    }
238    fn consume(&self, _item: &ItemKind) -> bool { true }
239    fn item_kind(&self, item: &ItemKind) -> dotzuki_engine::items::ItemKind<()> {
240        let _ = item;
241        dotzuki_engine::items::ItemKind::Consumable
242    }
243}
244
245// ── ShopProvider ───────────────────────────────────────────────────
246
247impl ShopProvider for HelloConfig {
248    type Item = ItemKind;
249    type ShopId = ();
250    fn shop_inventory(&self, _shop_id: &()) -> Vec<(ItemKind, u32)> {
251        vec![(ItemKind::Potion, 100), (ItemKind::Elixir, 300)]
252    }
253    fn shop_name(&self, _shop_id: &()) -> &str { "Town Shop" }
254}
255
256// ── TextProvider — ASCII charmap ──────────────────────────────────
257
258impl TextProvider for HelloConfig {
259    type Char = AsciiChar;
260
261    fn decode_byte(&self, byte: u8) -> Option<AsciiChar> {
262        match byte {
263            0xFE => Some(AsciiChar::Newline),
264            0xFF => Some(AsciiChar::Done),
265            0xFD => Some(AsciiChar::WaitInput),
266            b @ 0x20..=0x7E => Some(AsciiChar::Char(b as char)),
267            _ => None,
268        }
269    }
270    fn render_char(&self, c: &AsciiChar, buffer: &mut TileBuffer) {
271        if let AsciiChar::Char(ch) = c {
272            let pos = buffer.cursor;
273            buffer.set_tile(pos, *ch as u16, 0);
274            buffer.cursor.x += 1;
275        }
276    }
277    fn string_width(&self, text: &[AsciiChar]) -> u16 {
278        text.iter().filter(|c| matches!(c, AsciiChar::Char(_))).count() as u16 * 8
279    }
280    fn is_control_code(&self, c: &AsciiChar) -> bool { !matches!(c, AsciiChar::Char(_)) }
281    fn process_control(&self, c: &AsciiChar, _state: &mut DialogState) -> ControlAction {
282        match c {
283            AsciiChar::Newline => ControlAction::Newline,
284            AsciiChar::Done => ControlAction::Done,
285            AsciiChar::WaitInput => ControlAction::WaitInput,
286            _ => ControlAction::None,
287        }
288    }
289}
290
291impl MenuProvider for HelloConfig {
292    type MenuId = MenuScreen;
293
294    fn title(&self, _menu: MenuScreen) -> &str { "Hello JRPG" }
295    fn options(&self, _menu: MenuScreen) -> &[MenuOption] { &self.main_menu_options }
296    fn option_count(&self, _menu: MenuScreen) -> u8 { self.main_menu_options.len() as u8 }
297    fn scrollable(&self, _menu: MenuScreen) -> bool { false }
298    fn layout(&self, _menu: MenuScreen) -> MenuLayout {
299        MenuLayout::new(5, 5, 10, 8)
300    }
301}
302
303// ── SaveData — binary format: [1B name_len][0..32B name][1B level] ─
304
305#[derive(Debug, Clone, PartialEq, Eq)]
306struct GameSave { player_name: String, player_level: u8 }
307
308impl SaveData for GameSave {
309    fn serialize(&self) -> Vec<u8> {
310        let name = self.player_name.as_bytes();
311        let len = name.len().min(32);
312        let mut v = Vec::with_capacity(1 + len + 1);
313        v.push(len as u8);
314        v.extend_from_slice(&name[..len]);
315        v.push(self.player_level);
316        v
317    }
318    fn deserialize(data: &[u8]) -> Result<Self, SaveError> {
319        if data.len() < 2 { return Err(SaveError::InvalidData); }
320        let name_len = data[0] as usize;
321        if data.len() < 1 + name_len + 1 { return Err(SaveError::InvalidData); }
322        let name = String::from_utf8(data[1..1 + name_len].to_vec())
323            .map_err(|_| SaveError::InvalidData)?;
324        let level = data[1 + name_len];
325        Ok(GameSave { player_name: name, player_level: level })
326    }
327    fn save_size() -> usize { 35 }
328}
329
330// ═══════════════════════════════════════════════════════════════════════════
331// Overworld collision — simple provider: only tile 0xFF is a wall
332// ═══════════════════════════════════════════════════════════════════════════
333
334struct SimpleCollision;
335
336impl CollisionProvider<Tileset> for SimpleCollision {
337    fn is_tile_passable(&self, _tileset: Tileset, tile_id: u8) -> bool { tile_id != 0xFF }
338    fn check_tile_pair_collision(&self, _t: Tileset, _a: u8, _b: u8, _w: bool) -> bool { false }
339    fn check_ledge_jump(&self, _t: Tileset, _f: u8, _s: u8, _tg: u8, _i: u8) -> bool { false }
340    fn is_counter_tile(&self, _t: Tileset, _id: u8) -> bool { false }
341    fn get_tile_at_position(&self, _t: Tileset, blk: &[u8], w: u8, x: u16, y: u16) -> u8 {
342        blk.get((y as usize * w as usize + x as usize).min(blk.len().saturating_sub(1))).copied().unwrap_or(0)
343    }
344    fn is_door_tile(&self, _t: Tileset, _id: u8) -> bool { false }
345    fn is_warp_tile(&self, _t: Tileset, _id: u8) -> bool { false }
346    fn is_warp_carpet_tile_in_front(&self, _t: Tileset, _f: u8, _id: u8) -> bool { false }
347    fn uses_warp_tile_in_front_check(&self, _t: Tileset) -> bool { false }
348    fn check_extra_warp_special(&self, _t: Tileset, _id: u8) -> Option<bool> { None }
349}
350
351// ═══════════════════════════════════════════════════════════════════════════
352// Map builder — 5×5 town with walls, floor, NPC
353// ═══════════════════════════════════════════════════════════════════════════
354//  Layout:  # = wall (0xFF), . = floor (0x01), N=NPC, P=player start
355//  #####
356//  #...#
357//  #.N.#
358//  #.P.#
359//  #####
360
361fn build_town_map() -> MapData<MapId, Tileset, ()> {
362    let blocks: Vec<u8> = vec![
363        0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
364        0xFF, 0x01, 0x01, 0x01, 0xFF,
365        0xFF, 0x01, 0x01, 0x01, 0xFF,
366        0xFF, 0x01, 0x01, 0x01, 0xFF,
367        0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
368    ];
369    let npcs = vec![NpcDefinition::new(0, 2, 2, NpcMovementType::Stationary, Direction::Down, 0, 0)];
370    MapData::new(
371        MapId(0), 5, 5, Tileset(0), (),
372        blocks, vec![], npcs, vec![],
373        MapConnections::default(),
374    )
375}
376
377/// Build NPC runtime states from the map's NPC definitions.
378fn build_npc_states(npcs: &[NpcDefinition]) -> Vec<NpcRuntimeState> {
379    npcs.iter().enumerate().map(|(i, def)| {
380        let s = NpcRuntimeState {
381            npc_index: i as u8, sprite_id: def.sprite_id,
382            x: def.x as u16, y: def.y as u16,
383            home_x: def.x as u16, home_y: def.y as u16,
384            facing: def.facing, scripted_frame: None,
385            movement_type: def.movement, range: def.range,
386            walk_counter: 0, delay_counter: 0, text_id: def.text_id,
387            defeated: false, visible: true,
388            scripted_path: std::collections::VecDeque::new(),
389        };
390        s
391    }).collect()
392}
393
394/// Print the map as ASCII with player position.
395fn show_map(blocks: &[u8], _w: u8, px: u16, py: u16, npcs: &[NpcRuntimeState]) {
396    for y in 0..5u16 {
397        print!("  ");
398        for x in 0..5u16 {
399            let idx = (y * 5 + x) as usize;
400            if px == x && py == y {
401                print!("P");
402            } else if npcs.iter().any(|n| n.x == x && n.y == y) {
403                print!("N");
404            } else if idx < blocks.len() && blocks[idx] == 0xFF {
405                print!("#");
406            } else {
407                print!(".");
408            }
409        }
410        println!();
411    }
412}
413
414// ═══════════════════════════════════════════════════════════════════════════
415// Demo functions
416// ═══════════════════════════════════════════════════════════════════════════
417
418fn demo_overworld() {
419    println!("\n╔══ OVERWORLD MOVEMENT ═══════════════════╗");
420    let map = build_town_map();
421    let mut state = OverworldState::new(MapId(0));
422    let collision = SimpleCollision;
423    let npcs = build_npc_states(&map.npcs);
424    let npc_positions = dotzuki_engine::overworld::get_npc_positions(&npcs);
425
426    // Start at (3, 3) — bottom center
427    state.player.x = 3; state.player.y = 3;
428    println!("║ Initial map:");
429    show_map(&map.blocks, 5, state.player.x, state.player.y, &npcs);
430
431    // Move right → blocked by wall at (4, 3)
432    let standing = collision.get_tile_at_position(Tileset(0), &map.blocks, 5, 3, 3);
433    let target = collision.get_tile_at_position(Tileset(0), &map.blocks, 5, 4, 3);
434    let result = try_move(&mut state, Direction::Right, Tileset(0), 5, 5, standing, target, &npc_positions, 0, &collision);
435    println!("║ Move Right → {:?}", result);
436
437    // Move up to (3, 2) — floor is passable
438    let standing = collision.get_tile_at_position(Tileset(0), &map.blocks, 5, 3, 3);
439    let target = collision.get_tile_at_position(Tileset(0), &map.blocks, 5, 3, 2);
440    let result = try_move(&mut state, Direction::Up, Tileset(0), 5, 5, standing, target, &npc_positions, 0, &collision);
441    println!("║ Move Up   → {:?}", result);
442    println!("║ Player at ({}, {}), facing {:?}", state.player.x, state.player.y, state.player.facing);
443
444    // Advance the walk step
445    dotzuki_engine::overworld::advance_step(&mut state);
446    dotzuki_engine::overworld::advance_step(&mut state);
447    // ...walking finishes after WALK_COUNTER_INIT (8) frames
448    for _ in 0..8 { dotzuki_engine::overworld::advance_step(&mut state); }
449    println!("║ After walk: player at ({}, {})", state.player.x, state.player.y);
450
451    // NPC interaction: check if NPC is nearby
452    let interaction = dotzuki_engine::overworld::try_interact(
453        &npcs, state.player.x, state.player.y, state.player.facing,
454        Some(&map), &collision,
455    );
456    println!("║ Interact → {:?}", interaction);
457    println!("╚══════════════════════════════════════════╝");
458}
459
460fn demo_dialog() {
461    println!("\n╔══ NPC DIALOG ═══════════════════════════╗");
462    let provider = HelloConfig::new();
463    let mut engine = DialogEngine::new(provider);
464    let mut buffer = TileBuffer::new(20, 18);
465
466    // "Greetings, young hero!" + DONE
467    let text: &[u8] = b"Greetings, young hero!";
468    let mut full = text.to_vec();
469    full.push(0xFF); // DONE
470
471    engine.open_dialog(&full);
472    while engine.is_active() { engine.update(&mut buffer); }
473
474    print!("║ Merlin: \"");
475    for i in 0..20usize {
476        let t = buffer.tiles[i].tile_id;
477        if (0x20..=0x7E).contains(&t) { print!("{}", t as u8 as char); }
478    }
479    println!("\"");
480    println!("╚══════════════════════════════════════════╝");
481}
482
483fn demo_battle() {
484    println!("\n╔══ BATTLE ENCOUNTER ═════════════════════╗");
485    let provider = HelloConfig::new();
486    let warrior = provider.create_monster(Species::Warrior, 5);
487    let mage = provider.create_monster(Species::Mage, 5);
488
489    println!("║ {} (HP:{}/{}) vs {} (HP:{}/{})",
490        warrior.species.name(), warrior.hp, warrior.max_hp,
491        mage.species.name(), mage.hp, mage.max_hp);
492
493    // Warrior attacks with Slash → super effective (2×) vs Mage
494    let result = provider.calculate_damage(&MoveKind::Slash, &warrior, &mage, 100, false);
495    println!("║ {} uses Slash! {} damage ({}x effective)",
496        warrior.species.name(), result.damage, result.effectiveness);
497
498    let mut enemy = mage.clone();
499    enemy.take_damage(result.damage);
500    println!("║ {} HP: {} → {}", mage.species.name(), mage.hp, enemy.hp);
501
502    // Mage attacks with Fireball → not very effective (0.5×) vs Warrior
503    let result = provider.calculate_damage(&MoveKind::Fireball, &mage, &warrior, 100, false);
504    println!("║ {} uses Fireball! {} damage ({}x effective)",
505        mage.species.name(), result.damage, result.effectiveness);
506
507    let mut hero = warrior.clone();
508    hero.take_damage(result.damage);
509    println!("║ {} HP: {} → {}", warrior.species.name(), warrior.hp, hero.hp);
510
511    // EffectHandler
512    let mut user = warrior.clone();
513    let mut target = mage.clone();
514    let eff_result = HelloConfig::new().handle_effect(MoveEffect::Damage, &mut user, &mut target, &HelloConfig::new());
515    println!("║ EffectHandler(Damage) → {:?}", eff_result);
516
517    // BattleAI
518    let state = BattleState::new(vec![warrior.clone()], vec![mage.clone()]);
519    let ai_move = BattleAI::select_move(&HelloConfig::default(), &warrior, &state);
520    println!("║ BattleAI chose: {}", ai_move.name());
521    println!("╚══════════════════════════════════════════╝");
522}
Source

pub fn heal(&mut self, amount: u16)

Heal HP, clamping to max_hp.

Examples found in repository?
examples/hello_dotzuki.rs (line 160)
147    fn apply_move_effect(
148        &self, effect: MoveEffect, user: &mut BattlerState<Self>,
149        target: &mut BattlerState<Self>,
150    ) -> EffectResult {
151        match effect {
152            MoveEffect::Damage => {
153                let mv = user.moves.first().cloned().unwrap_or(MoveKind::Slash);
154                let result = self.calculate_damage(&mv, user, target, 100, false);
155                target.take_damage(result.damage);
156                EffectResult::DamageDealt { amount: result.damage }
157            }
158            MoveEffect::Heal => {
159                let amount = target.max_hp / 4;
160                target.heal(amount);
161                EffectResult::Healed { amount }
162            }
163            MoveEffect::StatusCondition => EffectResult::StatusInflicted,
164            MoveEffect::StatChange => EffectResult::StatModified { stages: 1 },
165            _ => EffectResult::NoEffect,
166        }
167    }
Source

pub fn with_resource(self, id: u16, max: u16) -> Self

Builder: declare a generic resource (id, current = max) on this battler. Returns self so it chains after new without changing the constructor’s signature (the additivity invariant).

Source

pub fn can_pay_resource(&self, id: u16, amount: u16) -> bool

Whether this battler can pay amount of resource id (delegates to ResourcePool::can_pay). A 0 cost is always payable; a positive cost on an undeclared resource is not. Pure — consumes no randomness.

Source

pub fn pay_resource(&mut self, id: u16, amount: u16) -> bool

Deduct amount of resource id (saturating). Pure arithmetic — no rng.

Trait Implementations§

Source§

impl<P: BattleProvider + ?Sized> Clone for BattlerState<P>

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl<P: BattleProvider + ?Sized> Debug for BattlerState<P>

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.