1#![allow(dead_code)]
2#![allow(clippy::needless_return)]
3#![allow(clippy::implicit_return)]
4#![allow(clippy::uninlined_format_args)]
5#![allow(clippy::items_after_statements)]
6#![allow(clippy::unnecessary_cast)]
7#![allow(clippy::doc_markdown)]
8#![allow(clippy::cast_sign_loss)]
9#![allow(clippy::explicit_iter_loop)]
10#![allow(clippy::format_in_format_args)]
11#![allow(clippy::cast_precision_loss)]
12#![allow(clippy::wildcard_imports)]
13#![allow(clippy::too_many_lines)]
14#![allow(clippy::std_instead_of_core)]
15#![allow(clippy::similar_names)]
16#![allow(clippy::duplicated_attributes)]
17#![allow(clippy::cast_possible_truncation)]
18#![allow(clippy::trivially_copy_pass_by_ref)]
19#![allow(clippy::missing_inline_in_public_items)]
20#![allow(clippy::useless_vec)]
21#![allow(clippy::unnested_or_patterns)]
22#![allow(clippy::else_if_without_else)]
23#![allow(clippy::unreadable_literal)]
24#![allow(clippy::redundant_else)]
25#![allow(clippy::cast_lossless)]
26#![allow(clippy::map_unwrap_or)]
27#![allow(clippy::unused_self)]
28#![allow(clippy::min_ident_chars)]
29#![allow(clippy::std_instead_of_alloc)]
30#![allow(clippy::struct_field_names)]
31use tiles_tools::{
47 ecs::{World, Position, Health, Stats, Team, AI, Movable, Size},
48 coordinates::{
49 hexagonal::{Coordinate as HexCoord, Axial, Pointy},
50 },
51 pathfind::astar,
52};
53use std::collections::VecDeque;
54
55#[derive(Debug, Clone, Copy)]
61struct Experience {
62 current_xp: u32,
63 level: u32,
64 xp_to_next_level: u32,
65}
66
67impl Experience {
68 pub fn new(level: u32) -> Self {
69 Self {
70 current_xp: 0,
71 level,
72 xp_to_next_level: Self::xp_required_for_level(level + 1),
73 }
74 }
75
76 pub fn add_xp(&mut self, xp: u32) -> bool {
77 self.current_xp += xp;
78 if self.current_xp >= self.xp_to_next_level {
79 self.level_up();
80 true
81 } else {
82 false
83 }
84 }
85
86 fn level_up(&mut self) {
87 self.level += 1;
88 self.current_xp -= self.xp_to_next_level;
89 self.xp_to_next_level = Self::xp_required_for_level(self.level + 1);
90 }
91
92 fn xp_required_for_level(level: u32) -> u32 {
93 level * level * 100
94 }
95}
96
97#[derive(Debug, Clone, Copy)]
99struct Initiative {
100 base_initiative: u32,
101 current_initiative: u32,
102 has_acted: bool,
103}
104
105impl Initiative {
106 pub fn new(base: u32) -> Self {
107 Self {
108 base_initiative: base,
109 current_initiative: base,
110 has_acted: false,
111 }
112 }
113
114 pub fn reset_turn(&mut self) {
115 self.current_initiative = self.base_initiative;
116 self.has_acted = false;
117 }
118
119 pub fn act(&mut self) {
120 self.has_acted = true;
121 }
122}
123
124#[derive(Debug, Clone)]
126struct Equipment {
127 weapon: Option<Weapon>,
128 armor: Option<Armor>,
129 accessories: Vec<Accessory>,
130 inventory_slots: u32,
131}
132
133#[derive(Debug, Clone)]
134struct Weapon {
135 name: String,
136 attack_bonus: u32,
137 range: u32,
138 damage_type: DamageType,
139}
140
141#[derive(Debug, Clone)]
142struct Armor {
143 name: String,
144 defense_bonus: u32,
145 resistances: Vec<DamageType>,
146}
147
148#[derive(Debug, Clone)]
149struct Accessory {
150 name: String,
151 effect: AccessoryEffect,
152}
153
154#[derive(Debug, Clone, Copy, PartialEq)]
155enum DamageType {
156 Physical,
157 Fire,
158 Ice,
159 Lightning,
160 Healing,
161}
162
163#[derive(Debug, Clone)]
164enum AccessoryEffect {
165 StatBonus(StatType, u32),
166 Resistance(DamageType),
167 ExtraMovement(u32),
168}
169
170#[derive(Debug, Clone, Copy)]
171enum StatType {
172 Attack,
173 Defense,
174 Speed,
175}
176
177#[derive(Debug, Clone)]
179struct Abilities {
180 abilities: Vec<Ability>,
181 mana: u32,
182 max_mana: u32,
183}
184
185#[derive(Debug, Clone)]
186struct Ability {
187 name: String,
188 mana_cost: u32,
189 range: u32,
190 area_of_effect: u32,
191 damage: u32,
192 damage_type: DamageType,
193 cooldown: u32,
194 current_cooldown: u32,
195}
196
197impl Abilities {
198 pub fn new(max_mana: u32) -> Self {
199 Self {
200 abilities: Vec::new(),
201 mana: max_mana,
202 max_mana,
203 }
204 }
205
206 pub fn add_ability(&mut self, ability: Ability) {
207 self.abilities.push(ability);
208 }
209
210 pub fn can_use_ability(&self, ability_index: usize) -> bool {
211 if let Some(ability) = self.abilities.get(ability_index) {
212 self.mana >= ability.mana_cost && ability.current_cooldown == 0
213 } else {
214 false
215 }
216 }
217
218 pub fn use_ability(&mut self, ability_index: usize) -> Option<&Ability> {
219 if self.can_use_ability(ability_index) {
220 let ability = &mut self.abilities[ability_index];
221 self.mana -= ability.mana_cost;
222 ability.current_cooldown = ability.cooldown;
223 Some(&*ability)
224 } else {
225 None
226 }
227 }
228}
229
230struct TacticalRPG {
236 world: World,
237 turn_queue: VecDeque<hecs::Entity>,
238 current_turn: Option<hecs::Entity>,
239 turn_number: u32,
240 player_team: Team,
241 enemy_team: Team,
242 game_phase: GamePhase,
243}
244
245#[derive(Debug, Clone, Copy, PartialEq)]
246enum GamePhase {
247 Planning, Execution, AI, Resolution, }
252
253impl TacticalRPG {
254 pub fn new() -> Self {
256 let mut world = World::new();
257 let player_team = Team::new(0);
258 let enemy_team = Team::hostile(1);
259
260 let player_warrior = world.spawn((
262 Position::new(HexCoord::<Axial, Pointy>::new(-2, 1)),
263 Health::new(120),
264 Stats::new(18, 12, 10, 1),
265 player_team,
266 Movable::new(3),
267 Experience::new(1),
268 Initiative::new(15),
269 Equipment {
270 weapon: Some(Weapon {
271 name: "Iron Sword".to_string(),
272 attack_bonus: 5,
273 range: 1,
274 damage_type: DamageType::Physical,
275 }),
276 armor: Some(Armor {
277 name: "Chain Mail".to_string(),
278 defense_bonus: 3,
279 resistances: vec![DamageType::Physical],
280 }),
281 accessories: Vec::new(),
282 inventory_slots: 10,
283 },
284 Size::single(),
285 ));
286
287 let player_mage = world.spawn((
288 Position::new(HexCoord::<Axial, Pointy>::new(-1, 0)),
289 Health::new(80),
290 Stats::new(12, 8, 14, 1),
291 player_team,
292 Movable::new(2),
293 Experience::new(1),
294 Initiative::new(12),
295 Equipment {
296 weapon: Some(Weapon {
297 name: "Fire Staff".to_string(),
298 attack_bonus: 2,
299 range: 3,
300 damage_type: DamageType::Fire,
301 }),
302 armor: None,
303 accessories: vec![Accessory {
304 name: "Mana Crystal".to_string(),
305 effect: AccessoryEffect::StatBonus(StatType::Speed, 2),
306 }],
307 inventory_slots: 8,
308 },
309 {
310 let mut abilities = Abilities::new(50);
311 abilities.add_ability(Ability {
312 name: "Fireball".to_string(),
313 mana_cost: 10,
314 range: 4,
315 area_of_effect: 1,
316 damage: 25,
317 damage_type: DamageType::Fire,
318 cooldown: 2,
319 current_cooldown: 0,
320 });
321 abilities.add_ability(Ability {
322 name: "Heal".to_string(),
323 mana_cost: 8,
324 range: 2,
325 area_of_effect: 0,
326 damage: 0, damage_type: DamageType::Healing,
328 cooldown: 1,
329 current_cooldown: 0,
330 });
331 abilities
332 },
333 Size::single(),
334 ));
335
336 let enemy_goblin = world.spawn((
338 Position::new(HexCoord::<Axial, Pointy>::new(2, -1)),
339 Health::new(60),
340 Stats::new(12, 6, 12, 1),
341 enemy_team,
342 Movable::new(4),
343 AI::new(1.0),
344 Initiative::new(14),
345 Equipment {
346 weapon: Some(Weapon {
347 name: "Rusty Dagger".to_string(),
348 attack_bonus: 2,
349 range: 1,
350 damage_type: DamageType::Physical,
351 }),
352 armor: None,
353 accessories: Vec::new(),
354 inventory_slots: 5,
355 },
356 Size::single(),
357 ));
358
359 let enemy_orc = world.spawn((
360 Position::new(HexCoord::<Axial, Pointy>::new(3, -2)),
361 Health::new(100),
362 Stats::new(16, 10, 8, 1),
363 enemy_team,
364 Movable::new(2),
365 AI::new(1.5),
366 Initiative::new(10),
367 Equipment {
368 weapon: Some(Weapon {
369 name: "War Axe".to_string(),
370 attack_bonus: 6,
371 range: 1,
372 damage_type: DamageType::Physical,
373 }),
374 armor: Some(Armor {
375 name: "Hide Armor".to_string(),
376 defense_bonus: 2,
377 resistances: Vec::new(),
378 }),
379 accessories: Vec::new(),
380 inventory_slots: 6,
381 },
382 Size::single(),
383 ));
384
385 let mut turn_queue = VecDeque::new();
386 turn_queue.extend([player_warrior, player_mage, enemy_goblin, enemy_orc]);
387
388 Self {
389 world,
390 turn_queue,
391 current_turn: None,
392 turn_number: 1,
393 player_team,
394 enemy_team,
395 game_phase: GamePhase::Planning,
396 }
397 }
398
399 pub fn start_turn(&mut self) {
401 if let Some(entity) = self.turn_queue.pop_front() {
402 self.current_turn = Some(entity);
403 println!("\n=== Turn {} ===", self.turn_number);
404 self.print_unit_status(entity);
405
406 let team_id = {
408 if let Ok(team) = self.world.get::<Team>(entity) {
409 team.id
410 } else {
411 return;
412 }
413 };
414
415 if team_id == self.player_team.id {
416 self.game_phase = GamePhase::Planning;
417 self.handle_player_turn(entity);
418 } else {
419 self.game_phase = GamePhase::AI;
420 self.handle_ai_turn(entity);
421 }
422 } else {
423 self.reset_turn_queue();
425 self.turn_number += 1;
426 }
427 }
428
429 fn handle_player_turn(&mut self, entity: hecs::Entity) {
431 println!("š® Player turn - planning actions...");
432
433 let (pos_coord, target) = {
437 if let Ok(pos) = self.world.get::<Position<HexCoord<Axial, Pointy>>>(entity) {
438 let pos_coord = pos.coord;
439 println!("Player unit at ({}, {})", pos_coord.q, pos_coord.r);
440
441 let target = self.find_nearest_enemy(entity);
443 (pos_coord, target)
444 } else {
445 return;
446 }
447 };
448
449 if let Some(target) = target {
450 let pos = Position::new(pos_coord);
451 println!("Targeting enemy at distance {}", pos.distance_to(&target.1));
452
453 if pos.distance_to(&target.1) <= 2 {
455 self.execute_attack(entity, target.0);
456 } else {
457 self.execute_move_toward(entity, &target.1.coord);
458 }
459 }
460
461 self.game_phase = GamePhase::Resolution;
462 }
463
464 fn handle_ai_turn(&mut self, entity: hecs::Entity) {
466 println!("š¤ AI turn - calculating optimal action...");
467
468 let (pos_coord, target) = {
469 if let Ok(pos) = self.world.get::<Position<HexCoord<Axial, Pointy>>>(entity) {
470 let pos_coord = pos.coord;
471 println!("AI unit at ({}, {})", pos_coord.q, pos_coord.r);
472
473 let target = self.find_nearest_player(entity);
475 (pos_coord, target)
476 } else {
477 return;
478 }
479 };
480
481 if let Some(target) = target {
482 let pos = Position::new(pos_coord);
483 let distance = pos.distance_to(&target.1);
484 println!("AI targeting player at distance {}", distance);
485
486 if distance <= 1 {
487 self.execute_attack(entity, target.0);
489 } else if distance <= 4 {
490 self.execute_move_toward(entity, &target.1.coord);
492 } else {
493 println!("AI unit holding position");
495 }
496 }
497
498 self.game_phase = GamePhase::Resolution;
499 }
500
501 fn execute_attack(&mut self, attacker: hecs::Entity, target: hecs::Entity) {
503 let (final_damage, target_level) = {
504 let attacker_stats = self.world.get::<Stats>(attacker).expect("attacker should have stats");
505 let attacker_equipment = self.world.get::<Equipment>(attacker).expect("attacker should have equipment");
506 let target_stats = self.world.get::<Stats>(target).expect("target should have stats");
507
508 let mut base_damage = attacker_stats.attack;
509 if let Some(weapon) = &attacker_equipment.weapon {
510 base_damage += weapon.attack_bonus;
511 }
512
513 let final_damage = base_damage.saturating_sub(target_stats.defense / 2).max(1);
514 (final_damage, target_stats.level)
515 };
516
517 let target_defeated = {
519 if let Ok(mut target_health) = self.world.get_mut::<Health>(target) {
520 let old_health = target_health.current;
521 target_health.damage(final_damage);
522
523 println!("š„ Attack! {} damage dealt ({} -> {} HP)",
524 final_damage, old_health, target_health.current);
525
526 !target_health.is_alive()
527 } else {
528 false
529 }
530 };
531
532 if target_defeated {
533 println!("š Unit defeated!");
534
535 if let Ok(mut exp) = self.world.get_mut::<Experience>(attacker) {
537 let xp_gained = target_level * 50;
538 if exp.add_xp(xp_gained) {
539 println!("š Level up! Now level {}", exp.level);
540 }
541 }
542 }
543 }
544
545 fn execute_move_toward(&mut self, entity: hecs::Entity, target: &HexCoord<Axial, Pointy>) {
547 if let Ok(pos) = self.world.get::<Position<HexCoord<Axial, Pointy>>>(entity) {
548 if let Ok(movable) = self.world.get::<Movable>(entity) {
549 let path_result = astar(
551 &pos.coord,
552 target,
553 |coord| self.is_position_passable(coord),
554 |_| 1,
555 );
556
557 if let Some((path, _cost)) = path_result {
558 let move_distance = movable.range.min(path.len() as u32 - 1);
559 if move_distance > 0 {
560 let new_pos = path[move_distance as usize];
561
562 println!("š¶ Moving from ({}, {}) to ({}, {})",
564 pos.coord.q, pos.coord.r, new_pos.q, new_pos.r);
565 }
566 }
567 }
568 }
569 }
570
571 fn find_nearest_enemy(&self, entity: hecs::Entity) -> Option<(hecs::Entity, Position<HexCoord<Axial, Pointy>>)> {
573 if let Ok(our_team) = self.world.get::<Team>(entity) {
574 if let Ok(our_pos) = self.world.get::<Position<HexCoord<Axial, Pointy>>>(entity) {
575 return self.world.find_nearest_entity(&our_pos)
576 .and_then(|(nearest_entity, nearest_pos, _distance)| {
577 if let Ok(their_team) = self.world.get::<Team>(nearest_entity) {
578 if our_team.is_hostile_to(&their_team) {
579 Some((nearest_entity, nearest_pos))
580 } else {
581 None
582 }
583 } else {
584 None
585 }
586 });
587 }
588 }
589 None
590 }
591
592 fn find_nearest_player(&self, entity: hecs::Entity) -> Option<(hecs::Entity, Position<HexCoord<Axial, Pointy>>)> {
594 if let Ok(our_pos) = self.world.get::<Position<HexCoord<Axial, Pointy>>>(entity) {
595 return self.world.find_nearest_entity(&our_pos)
596 .and_then(|(nearest_entity, nearest_pos, _distance)| {
597 if let Ok(their_team) = self.world.get::<Team>(nearest_entity) {
598 if their_team.id == self.player_team.id {
599 Some((nearest_entity, nearest_pos))
600 } else {
601 None
602 }
603 } else {
604 None
605 }
606 });
607 }
608 None
609 }
610
611 fn is_position_passable(&self, _coord: &HexCoord<Axial, Pointy>) -> bool {
613 true
615 }
616
617 fn reset_turn_queue(&mut self) {
619 let mut units_by_initiative = Vec::new();
621
622 for (entity, (init, health)) in self.world.query::<(&Initiative, &Health)>().iter() {
623 if health.is_alive() {
624 units_by_initiative.push((entity, init.current_initiative));
625 }
626 }
627
628 units_by_initiative.sort_by(|a, b| b.1.cmp(&a.1)); self.turn_queue.clear();
631 for (entity, _init) in units_by_initiative {
632 self.turn_queue.push_back(entity);
633 }
634 }
635
636 fn print_unit_status(&self, entity: hecs::Entity) {
638 if let Ok(health) = self.world.get::<Health>(entity) {
639 if let Ok(stats) = self.world.get::<Stats>(entity) {
640 if let Ok(pos) = self.world.get::<Position<HexCoord<Axial, Pointy>>>(entity) {
641 if let Ok(team) = self.world.get::<Team>(entity) {
642 let team_name = if team.id == self.player_team.id { "Player" } else { "Enemy" };
643
644 println!("{} Unit at ({}, {}): {}/{} HP, Level {} (ATK:{} DEF:{} SPD:{})",
645 team_name,
646 pos.coord.q, pos.coord.r,
647 health.current, health.maximum,
648 stats.level, stats.attack, stats.defense, stats.speed);
649
650 if let Ok(equipment) = self.world.get::<Equipment>(entity) {
651 if let Some(weapon) = &equipment.weapon {
652 println!(" š Equipped: {} (+{} attack)", weapon.name, weapon.attack_bonus);
653 }
654 }
655 }
656 }
657 }
658 }
659 }
660
661 pub fn print_battlefield(&self) {
663 println!("\nš Battlefield Status:");
664
665 let mut units = Vec::new();
667 for (_entity, (pos, health, team)) in self.world.query::<(&Position<HexCoord<Axial, Pointy>>, &Health, &Team)>().iter() {
668 if health.is_alive() {
669 let symbol = if team.id == self.player_team.id { "š¢" } else { "š“" };
670 units.push((pos.coord.q, pos.coord.r, symbol));
671 }
672 }
673
674 if units.is_empty() {
675 println!("Battle concluded!");
676 return;
677 }
678
679 let min_q = units.iter().map(|(q, _, _)| *q).min().unwrap_or(0) - 1;
681 let max_q = units.iter().map(|(q, _, _)| *q).max().unwrap_or(0) + 1;
682 let min_r = units.iter().map(|(_, r, _)| *r).min().unwrap_or(0) - 1;
683 let max_r = units.iter().map(|(_, r, _)| *r).max().unwrap_or(0) + 1;
684
685 for r in min_r..=max_r {
687 if r % 2 == 1 {
689 print!(" ");
690 }
691
692 for q in min_q..=max_q {
693 let symbol = units.iter()
694 .find(|(unit_q, unit_r, _)| *unit_q == q && *unit_r == r)
695 .map(|(_, _, symbol)| *symbol)
696 .unwrap_or("⬔");
697 print!("{} ", symbol);
698 }
699 println!();
700 }
701 }
702
703 pub fn run_simulation(&mut self) {
705 println!("šÆ Tactical RPG Combat Simulation");
706 println!("=================================");
707 println!("š¢ = Player Units");
708 println!("š“ = Enemy Units");
709 println!("⬔ = Empty Hex");
710
711 self.print_battlefield();
712
713 for turn in 1..=10 {
715 self.start_turn();
716 self.print_battlefield();
717
718 let player_units_alive = self.count_living_units(self.player_team.id);
720 let enemy_units_alive = self.count_living_units(self.enemy_team.id);
721
722 if player_units_alive == 0 {
723 println!("š Defeat! All player units have fallen.");
724 break;
725 } else if enemy_units_alive == 0 {
726 println!("š Victory! All enemies defeated.");
727 break;
728 }
729
730 if turn >= 10 {
731 println!("ā° Battle continues...");
732 break;
733 }
734
735 std::thread::sleep(std::time::Duration::from_millis(1500));
736 }
737 }
738
739 fn count_living_units(&self, team_id: u32) -> usize {
741 let mut count = 0;
742 for (_entity, (health, team)) in self.world.query::<(&Health, &Team)>().iter() {
743 if health.is_alive() && team.id == team_id {
744 count += 1;
745 }
746 }
747 count
748 }
749}
750
751fn main() {
753 let mut game = TacticalRPG::new();
754 game.run_simulation();
755
756 println!("\n⨠Tactical RPG Demo Complete!");
757 println!("This example showcases:");
758 println!("⢠Turn-based combat with initiative system");
759 println!("⢠AI decision-making and pathfinding");
760 println!("⢠Equipment and stat systems");
761 println!("⢠Experience and leveling mechanics");
762 println!("⢠Grid-aware tactical positioning");
763}