1use glam::Vec3;
8use std::collections::HashMap;
9
10#[derive(Clone, Debug)]
14pub enum SpawnZone {
15 Point(Vec3),
17 Box { min: Vec3, max: Vec3 },
19 Sphere { center: Vec3, radius: f32 },
21 SphereSurface { center: Vec3, radius: f32 },
23 Disc { center: Vec3, inner_radius: f32, outer_radius: f32 },
25 Line { start: Vec3, end: Vec3 },
27 Ring { center: Vec3, radius: f32, count: usize, phase: f32 },
29 AroundPlayer { offset_min: f32, offset_max: f32 },
31}
32
33impl SpawnZone {
34 pub fn sample(&self, rng: &mut u64, player_pos: Vec3) -> Vec3 {
36 match self {
37 SpawnZone::Point(p) => *p,
38
39 SpawnZone::Box { min, max } => {
40 Vec3::new(
41 min.x + rng_f32(rng) * (max.x - min.x),
42 min.y + rng_f32(rng) * (max.y - min.y),
43 min.z + rng_f32(rng) * (max.z - min.z),
44 )
45 }
46
47 SpawnZone::Sphere { center, radius } => {
48 let (center, radius) = (*center, *radius);
49
50 loop {
52 let x = rng_f32_signed(rng);
53 let y = rng_f32_signed(rng);
54 let z = rng_f32_signed(rng);
55 if x*x + y*y + z*z <= 1.0 {
56 return center + Vec3::new(x, y, z) * radius;
57 }
58 }
59 }
60
61 SpawnZone::SphereSurface { center, radius } => {
62 let (center, radius) = (*center, *radius);
63
64 let theta = rng_f32(rng) * std::f32::consts::TAU;
65 let phi = (rng_f32_signed(rng)).acos();
66 center + Vec3::new(
67 phi.sin() * theta.cos(),
68 phi.sin() * theta.sin(),
69 phi.cos(),
70 ) * radius
71 }
72
73 SpawnZone::Disc { center, inner_radius, outer_radius } => {
74 let (center, inner_radius, outer_radius) = (*center, *inner_radius, *outer_radius);
75 let angle = rng_f32(rng) * std::f32::consts::TAU;
76 let r = (inner_radius + rng_f32(rng) * (outer_radius - inner_radius)).sqrt();
77 center + Vec3::new(r * angle.cos(), 0.0, r * angle.sin())
78 }
79
80 SpawnZone::Line { start, end } => {
81 let (start, end) = (*start, *end);
82 start.lerp(end, rng_f32(rng))
83 }
84
85 SpawnZone::Ring { center, radius, count, phase } => {
86 let (center, radius, count, phase) = (*center, *radius, *count, *phase);
87 let idx = (rng_f32(rng) * count as f32) as usize % count;
88 let angle = phase + std::f32::consts::TAU * idx as f32 / count as f32;
89 center + Vec3::new(angle.cos() * radius, 0.0, angle.sin() * radius)
90 }
91
92 SpawnZone::AroundPlayer { offset_min, offset_max } => {
93 let angle = rng_f32(rng) * std::f32::consts::TAU;
94 let radius = offset_min + rng_f32(rng) * (offset_max - offset_min);
95 player_pos + Vec3::new(angle.cos() * radius, 0.0, angle.sin() * radius)
96 }
97 }
98 }
99}
100
101fn rng_f32(rng: &mut u64) -> f32 {
102 *rng ^= *rng << 13; *rng ^= *rng >> 7; *rng ^= *rng << 17;
103 (*rng & 0xFFFF) as f32 / 65535.0
104}
105
106fn rng_f32_signed(rng: &mut u64) -> f32 {
107 rng_f32(rng) * 2.0 - 1.0
108}
109
110#[derive(Clone, Debug)]
114pub enum SpawnPattern {
115 Random,
117 Ring { radius: f32, phase_offset: f32 },
119 Grid { cols: u32, spacing: Vec3 },
121 VFormation { spread: f32, depth: f32 },
123 Line { direction: Vec3, spacing: f32 },
125 Burst { radius: f32 },
127 Escort { leader_offset: Vec3, follower_offsets: Vec<Vec3> },
129}
130
131impl SpawnPattern {
132 pub fn positions(&self, count: usize, zone_center: Vec3, rng: &mut u64) -> Vec<Vec3> {
134 match self {
135 SpawnPattern::Random => {
136 (0..count).map(|_| {
137 zone_center + Vec3::new(
138 rng_f32_signed(rng),
139 0.0,
140 rng_f32_signed(rng),
141 )
142 }).collect()
143 }
144
145 SpawnPattern::Ring { radius, phase_offset } => {
146 (0..count).map(|i| {
147 let angle = phase_offset + std::f32::consts::TAU * i as f32 / count as f32;
148 zone_center + Vec3::new(angle.cos() * radius, 0.0, angle.sin() * radius)
149 }).collect()
150 }
151
152 SpawnPattern::Grid { cols, spacing } => {
153 let cols = (*cols).max(1) as usize;
154 (0..count).map(|i| {
155 let col = i % cols;
156 let row = i / cols;
157 zone_center + Vec3::new(col as f32, 0.0, row as f32) * *spacing
158 }).collect()
159 }
160
161 SpawnPattern::VFormation { spread, depth } => {
162 (0..count).map(|i| {
163 let offset_x = (i as f32 - count as f32 * 0.5) * spread;
164 let offset_z = (i as f32 * 0.5).abs() * depth;
165 zone_center + Vec3::new(offset_x, 0.0, offset_z)
166 }).collect()
167 }
168
169 SpawnPattern::Line { direction, spacing } => {
170 let dir = direction.normalize_or_zero();
171 (0..count).map(|i| {
172 zone_center + dir * (i as f32 * spacing)
173 }).collect()
174 }
175
176 SpawnPattern::Burst { radius } => {
177 (0..count).map(|_| {
178 let angle = rng_f32(rng) * std::f32::consts::TAU;
179 let r = rng_f32(rng).sqrt() * radius;
180 zone_center + Vec3::new(angle.cos() * r, 0.0, angle.sin() * r)
181 }).collect()
182 }
183
184 SpawnPattern::Escort { leader_offset, follower_offsets } => {
185 let mut positions = vec![zone_center + *leader_offset];
186 for (i, off) in follower_offsets.iter().enumerate() {
187 if i + 1 >= count { break; }
188 positions.push(zone_center + *off);
189 }
190 while positions.len() < count {
191 positions.push(zone_center);
192 }
193 positions
194 }
195 }
196 }
197}
198
199#[derive(Clone, Debug)]
203pub struct EntityBlueprint {
204 pub name: String,
205 pub tags: Vec<String>,
206 pub hp: f32,
207 pub speed: f32,
208 pub damage: f32,
209 pub scale: Vec3,
210 pub color: [f32; 4],
211 pub ai: Option<String>,
213 pub attrs: HashMap<String, f32>,
215 pub glyphs: Vec<char>,
217}
218
219impl EntityBlueprint {
220 pub fn new(name: &str) -> Self {
221 Self {
222 name: name.into(),
223 tags: Vec::new(),
224 hp: 100.0,
225 speed: 3.0,
226 damage: 10.0,
227 scale: Vec3::ONE,
228 color: [1.0, 1.0, 1.0, 1.0],
229 ai: None,
230 attrs: HashMap::new(),
231 glyphs: vec!['@'],
232 }
233 }
234
235 pub fn with_hp(mut self, hp: f32) -> Self { self.hp = hp; self }
236 pub fn with_speed(mut self, s: f32) -> Self { self.speed = s; self }
237 pub fn with_damage(mut self, d: f32) -> Self { self.damage = d; self }
238 pub fn with_color(mut self, c: [f32; 4]) -> Self { self.color = c; self }
239 pub fn with_ai(mut self, ai: &str) -> Self { self.ai = Some(ai.into()); self }
240 pub fn with_glyph(mut self, g: char) -> Self { self.glyphs = vec![g]; self }
241 pub fn with_glyphs(mut self, g: Vec<char>) -> Self { self.glyphs = g; self }
242 pub fn tagged(mut self, tag: &str) -> Self { self.tags.push(tag.into()); self }
243 pub fn with_attr(mut self, k: &str, v: f32) -> Self { self.attrs.insert(k.into(), v); self }
244}
245
246#[derive(Clone, Debug)]
250pub struct SpawnGroup {
251 pub blueprint: String, pub count: u32,
253 pub zone: SpawnZone,
254 pub pattern: SpawnPattern,
255 pub rate: f32,
257 pub delay: f32,
259 pub tag: Option<String>,
261 pub blocking: bool,
263}
264
265impl SpawnGroup {
266 pub fn new(blueprint: &str, count: u32, zone: SpawnZone) -> Self {
267 Self {
268 blueprint: blueprint.into(),
269 count,
270 zone,
271 pattern: SpawnPattern::Random,
272 rate: 0.0,
273 delay: 0.0,
274 tag: None,
275 blocking: true,
276 }
277 }
278
279 pub fn with_pattern(mut self, p: SpawnPattern) -> Self { self.pattern = p; self }
280 pub fn with_rate(mut self, r: f32) -> Self { self.rate = r; self }
281 pub fn with_delay(mut self, d: f32) -> Self { self.delay = d; self }
282 pub fn tagged(mut self, t: &str) -> Self { self.tag = Some(t.into()); self }
283 pub fn non_blocking(mut self) -> Self { self.blocking = false; self }
284}
285
286#[derive(Clone, Debug)]
290pub struct SpawnWave {
291 pub name: String,
292 pub groups: Vec<SpawnGroup>,
293 pub pre_delay: f32,
295 pub post_delay: f32,
297 pub music_vibe: Option<String>,
299 pub on_clear: Option<String>,
301 pub repeat: bool,
303}
304
305impl SpawnWave {
306 pub fn new(name: &str, groups: Vec<SpawnGroup>) -> Self {
307 Self {
308 name: name.into(),
309 groups,
310 pre_delay: 0.0,
311 post_delay: 2.0,
312 music_vibe: None,
313 on_clear: None,
314 repeat: false,
315 }
316 }
317
318 pub fn with_pre_delay(mut self, d: f32) -> Self { self.pre_delay = d; self }
319 pub fn with_post_delay(mut self, d: f32) -> Self { self.post_delay = d; self }
320 pub fn with_music(mut self, v: &str) -> Self { self.music_vibe = Some(v.into()); self }
321 pub fn on_clear(mut self, flag: &str) -> Self { self.on_clear = Some(flag.into()); self }
322 pub fn repeating(mut self) -> Self { self.repeat = true; self }
323}
324
325#[derive(Clone, Debug)]
328struct GroupState {
329 pub spawned: u32,
330 pub killed: u32,
331 pub timer: f32, pub delay_done: bool,
333 pub delay_timer: f32,
334 pub complete: bool,
335}
336
337#[derive(Clone, Debug)]
341pub struct SpawnEvent {
342 pub blueprint: String,
343 pub position: Vec3,
344 pub tag: Option<String>,
345 pub wave_name: String,
346}
347
348pub struct WaveManager {
352 waves: Vec<SpawnWave>,
353 current_wave: usize,
354 group_states: Vec<GroupState>,
355 wave_timer: f32,
357 active: bool,
359 post_timer: f32,
361 post_pending: bool,
362 rng: u64,
363 pub flags: HashMap<String, bool>,
364 pub player_pos: Vec3,
365 pub blueprints: BlueprintLibrary,
367 pub finished: bool,
368 pub wave_count: u32,
369}
370
371impl WaveManager {
372 pub fn new(waves: Vec<SpawnWave>, blueprints: BlueprintLibrary) -> Self {
373 let n = waves.first().map(|w| w.groups.len()).unwrap_or(0);
374 let pre = waves.first().map(|w| w.pre_delay).unwrap_or(0.0);
375 let group_states = vec![GroupState {
376 spawned: 0, killed: 0, timer: 0.0,
377 delay_done: false, delay_timer: 0.0, complete: false,
378 }; n];
379
380 Self {
381 waves,
382 current_wave: 0,
383 group_states,
384 wave_timer: pre,
385 active: false,
386 post_timer: 0.0,
387 post_pending: false,
388 rng: 0xDEADBEEF_CAFEBABE,
389 flags: HashMap::new(),
390 player_pos: Vec3::ZERO,
391 blueprints,
392 finished: false,
393 wave_count: 0,
394 }
395 }
396
397 pub fn start(&mut self) {
398 if self.waves.is_empty() {
399 self.finished = true;
400 return;
401 }
402 self.active = false;
403 self.wave_timer = self.waves[0].pre_delay;
404 }
405
406 pub fn on_entity_killed(&mut self, tag: &str) {
408 let wave = match self.waves.get(self.current_wave) {
409 Some(w) => w,
410 None => return,
411 };
412 for (i, group) in wave.groups.iter().enumerate() {
413 if group.tag.as_deref() == Some(tag) || group.blocking {
414 if let Some(s) = self.group_states.get_mut(i) {
415 s.killed += 1;
416 }
417 }
418 }
419 }
420
421 pub fn tick(&mut self, dt: f32) -> Vec<SpawnEvent> {
423 if self.finished { return Vec::new(); }
424 let mut events = Vec::new();
425
426 if !self.active && !self.post_pending {
428 self.wave_timer -= dt;
429 if self.wave_timer <= 0.0 {
430 self.activate_current_wave();
431 }
432 if !self.active || self.finished {
436 return events;
437 }
438 }
439
440 if self.post_pending {
442 self.post_timer -= dt;
443 if self.post_timer <= 0.0 {
444 self.post_pending = false;
445 self.advance_wave();
446 }
447 return events;
448 }
449
450 let wave = match self.waves.get(self.current_wave).cloned() {
452 Some(w) => w,
453 None => return events,
454 };
455
456 let mut all_done = true;
457
458 for (gi, group) in wave.groups.iter().enumerate() {
459 let state = &mut self.group_states[gi];
460 if state.complete { continue; }
461
462 if !state.delay_done {
464 state.delay_timer += dt;
465 if state.delay_timer < group.delay { all_done = false; continue; }
466 state.delay_done = true;
467 }
468
469 let remaining = group.count - state.spawned;
471 if remaining > 0 {
472 all_done = false;
473 if group.rate <= 0.0 {
474 let positions = group.pattern.positions(
476 remaining as usize,
477 group.zone.sample(&mut self.rng, self.player_pos),
478 &mut self.rng,
479 );
480 for pos in positions {
481 events.push(SpawnEvent {
482 blueprint: group.blueprint.clone(),
483 position: pos,
484 tag: group.tag.clone(),
485 wave_name: wave.name.clone(),
486 });
487 state.spawned += 1;
488 }
489 } else {
490 state.timer += dt;
491 while state.timer >= 1.0 / group.rate && state.spawned < group.count {
492 state.timer -= 1.0 / group.rate;
493 let pos = group.zone.sample(&mut self.rng, self.player_pos);
494 events.push(SpawnEvent {
495 blueprint: group.blueprint.clone(),
496 position: pos,
497 tag: group.tag.clone(),
498 wave_name: wave.name.clone(),
499 });
500 state.spawned += 1;
501 }
502 }
503 } else if group.blocking {
504 let needed = group.count;
506 if state.killed < needed {
507 all_done = false;
508 } else {
509 state.complete = true;
510 }
511 } else {
512 state.complete = true;
513 }
514 }
515
516 if all_done && self.active {
517 self.on_wave_cleared(&wave.clone());
518 }
519
520 events
521 }
522
523 fn activate_current_wave(&mut self) {
524 let wave = match self.waves.get(self.current_wave) {
525 Some(w) => w,
526 None => { self.finished = true; return; }
527 };
528 let n = wave.groups.len();
529 self.group_states = vec![GroupState {
530 spawned: 0, killed: 0, timer: 0.0,
531 delay_done: false, delay_timer: 0.0, complete: false,
532 }; n];
533 self.active = true;
534 }
535
536 fn on_wave_cleared(&mut self, wave: &SpawnWave) {
537 self.active = false;
538 self.wave_count += 1;
539
540 if let Some(flag) = &wave.on_clear {
541 self.flags.insert(flag.clone(), true);
542 }
543
544 if wave.repeat {
545 self.wave_timer = wave.pre_delay;
546 self.post_pending = true;
547 self.post_timer = wave.post_delay;
548 } else {
549 self.post_pending = true;
550 self.post_timer = wave.post_delay;
551 }
552 }
553
554 fn advance_wave(&mut self) {
555 if self.waves.get(self.current_wave).map(|w| w.repeat).unwrap_or(false) {
556 self.wave_timer = self.waves[self.current_wave].pre_delay;
558 } else {
559 self.current_wave += 1;
560 if self.current_wave >= self.waves.len() {
561 self.finished = true;
562 return;
563 }
564 self.wave_timer = self.waves[self.current_wave].pre_delay;
565 }
566 }
567
568 pub fn current_wave_name(&self) -> &str {
569 self.waves.get(self.current_wave).map(|w| w.name.as_str()).unwrap_or("none")
570 }
571
572 pub fn total_waves(&self) -> usize { self.waves.len() }
573 pub fn is_active(&self) -> bool { self.active }
574 pub fn get_flag(&self, k: &str) -> bool { self.flags.get(k).copied().unwrap_or(false) }
575}
576
577#[derive(Default)]
581pub struct BlueprintLibrary {
582 pub blueprints: HashMap<String, EntityBlueprint>,
583}
584
585impl BlueprintLibrary {
586 pub fn new() -> Self { Self::default() }
587
588 pub fn register(&mut self, blueprint: EntityBlueprint) {
589 self.blueprints.insert(blueprint.name.clone(), blueprint);
590 }
591
592 pub fn get(&self, name: &str) -> Option<&EntityBlueprint> {
593 self.blueprints.get(name)
594 }
595
596 pub fn with_defaults(mut self) -> Self {
598 self.register(EntityBlueprint::new("grunt")
599 .with_hp(60.0).with_speed(2.5).with_damage(8.0)
600 .with_color([0.8, 0.2, 0.2, 1.0]).with_glyph('g').tagged("enemy"));
601 self.register(EntityBlueprint::new("archer")
602 .with_hp(40.0).with_speed(2.0).with_damage(15.0)
603 .with_color([0.8, 0.5, 0.2, 1.0]).with_glyph('a').tagged("enemy"));
604 self.register(EntityBlueprint::new("tank")
605 .with_hp(200.0).with_speed(1.5).with_damage(25.0)
606 .with_color([0.5, 0.2, 0.8, 1.0]).with_glyph('T').tagged("enemy"));
607 self.register(EntityBlueprint::new("healer")
608 .with_hp(50.0).with_speed(2.0).with_damage(5.0)
609 .with_color([0.2, 0.9, 0.4, 1.0]).with_glyph('h').tagged("enemy"));
610 self.register(EntityBlueprint::new("boss")
611 .with_hp(1000.0).with_speed(3.5).with_damage(50.0)
612 .with_color([1.0, 0.1, 0.1, 1.0]).with_glyph('B').tagged("enemy").tagged("boss")
613 .with_attr("enrage_threshold", 0.3));
614 self
615 }
616}
617
618#[cfg(test)]
621mod tests {
622 use super::*;
623
624 #[test]
625 fn spawn_zone_point() {
626 let z = SpawnZone::Point(Vec3::new(1.0, 2.0, 3.0));
627 let mut rng = 12345u64;
628 let p = z.sample(&mut rng, Vec3::ZERO);
629 assert_eq!(p, Vec3::new(1.0, 2.0, 3.0));
630 }
631
632 #[test]
633 fn spawn_zone_sphere_bounded() {
634 let z = SpawnZone::Sphere { center: Vec3::ZERO, radius: 5.0 };
635 let mut rng = 42u64;
636 for _ in 0..100 {
637 let p = z.sample(&mut rng, Vec3::ZERO);
638 assert!(p.length() <= 5.05, "Point outside sphere: {:?}", p);
639 }
640 }
641
642 #[test]
643 fn spawn_pattern_ring_count() {
644 let p = SpawnPattern::Ring { radius: 3.0, phase_offset: 0.0 };
645 let positions = p.positions(8, Vec3::ZERO, &mut 0u64);
646 assert_eq!(positions.len(), 8);
647 }
648
649 #[test]
650 fn spawn_pattern_grid() {
651 let p = SpawnPattern::Grid { cols: 3, spacing: Vec3::ONE };
652 let positions = p.positions(9, Vec3::ZERO, &mut 0u64);
653 assert_eq!(positions.len(), 9);
654 }
655
656 #[test]
657 fn blueprint_library_default() {
658 let lib = BlueprintLibrary::new().with_defaults();
659 assert!(lib.get("grunt").is_some());
660 assert!(lib.get("boss").is_some());
661 assert!(lib.get("nobody").is_none());
662 }
663
664 #[test]
665 fn wave_manager_starts_and_spawns() {
666 let lib = BlueprintLibrary::new().with_defaults();
667 let wave = SpawnWave::new("w1", vec![
668 SpawnGroup::new("grunt", 3, SpawnZone::Point(Vec3::ZERO))
669 .with_rate(0.0) .non_blocking(),
671 ]).with_pre_delay(0.0).with_post_delay(0.0);
672
673 let mut mgr = WaveManager::new(vec![wave], lib);
674 mgr.start();
675
676 let events = mgr.tick(0.016);
678 assert!(!events.is_empty(), "Expected spawn events");
679 }
680
681 #[test]
682 fn wave_manager_rate_spawn() {
683 let lib = BlueprintLibrary::new().with_defaults();
684 let wave = SpawnWave::new("w1", vec![
685 SpawnGroup::new("grunt", 10, SpawnZone::Point(Vec3::ZERO))
686 .with_rate(5.0) .non_blocking(),
688 ]).with_pre_delay(0.0).with_post_delay(0.0);
689
690 let mut mgr = WaveManager::new(vec![wave], lib);
691 mgr.start();
692
693 let mut total = 0;
694 for _ in 0..60 {
695 total += mgr.tick(1.0 / 60.0).len();
696 }
697 assert!(total >= 4 && total <= 6, "Expected ~5 spawns, got {}", total);
699 }
700
701 #[test]
702 fn wave_advances() {
703 let lib = BlueprintLibrary::new().with_defaults();
704 let w1 = SpawnWave::new("w1", vec![
705 SpawnGroup::new("grunt", 1, SpawnZone::Point(Vec3::ZERO))
706 .non_blocking(),
707 ]).with_pre_delay(0.0).with_post_delay(0.0);
708 let w2 = SpawnWave::new("w2", vec![
709 SpawnGroup::new("tank", 1, SpawnZone::Point(Vec3::ONE))
710 .non_blocking(),
711 ]).with_pre_delay(0.0).with_post_delay(0.0);
712
713 let mut mgr = WaveManager::new(vec![w1, w2], lib);
714 mgr.start();
715
716 let mut ticks = 0;
720 while mgr.current_wave_name() == "w1" && ticks < 30 {
721 mgr.tick(0.1);
722 ticks += 1;
723 }
724 assert_eq!(mgr.current_wave_name(), "w2");
725 for _ in 0..30 { mgr.tick(0.1); }
726 assert_eq!(mgr.current_wave_name(), "none", "both waves done");
727 }
728}