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::SpeciesSpecies of this monster.
hp: u16Current hit points.
max_hp: u16Maximum hit points.
level: u8The 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: ResourcePoolGeneric, 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>
impl<P: BattleProvider + ?Sized> BattlerState<P>
Sourcepub fn new(
species: P::Species,
hp: u16,
max_hp: u16,
stats: EnumMap<P::Stat, u16>,
moves: Vec<P::Move>,
) -> Self
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?
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 }Sourcepub fn with_level(self, level: u8) -> Self
pub fn with_level(self, level: u8) -> Self
Builder: set the battler’s level.
Sourcepub fn take_damage(&mut self, amount: u16)
pub fn take_damage(&mut self, amount: u16)
Apply damage, clamping HP to zero (never negative).
Examples found in repository?
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}Sourcepub fn heal(&mut self, amount: u16)
pub fn heal(&mut self, amount: u16)
Heal HP, clamping to max_hp.
Examples found in repository?
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 }Sourcepub fn with_resource(self, id: u16, max: u16) -> Self
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).
Sourcepub fn can_pay_resource(&self, id: u16, amount: u16) -> bool
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.
Sourcepub fn pay_resource(&mut self, id: u16, amount: u16) -> bool
pub fn pay_resource(&mut self, id: u16, amount: u16) -> bool
Deduct amount of resource id (saturating). Pure arithmetic — no rng.