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 return events;
433 }
434
435 if self.post_pending {
437 self.post_timer -= dt;
438 if self.post_timer <= 0.0 {
439 self.post_pending = false;
440 self.advance_wave();
441 }
442 return events;
443 }
444
445 let wave = match self.waves.get(self.current_wave).cloned() {
447 Some(w) => w,
448 None => return events,
449 };
450
451 let mut all_done = true;
452
453 for (gi, group) in wave.groups.iter().enumerate() {
454 let state = &mut self.group_states[gi];
455 if state.complete { continue; }
456
457 if !state.delay_done {
459 state.delay_timer += dt;
460 if state.delay_timer < group.delay { all_done = false; continue; }
461 state.delay_done = true;
462 }
463
464 let remaining = group.count - state.spawned;
466 if remaining > 0 {
467 all_done = false;
468 if group.rate <= 0.0 {
469 let positions = group.pattern.positions(
471 remaining as usize,
472 group.zone.sample(&mut self.rng, self.player_pos),
473 &mut self.rng,
474 );
475 for pos in positions {
476 events.push(SpawnEvent {
477 blueprint: group.blueprint.clone(),
478 position: pos,
479 tag: group.tag.clone(),
480 wave_name: wave.name.clone(),
481 });
482 state.spawned += 1;
483 }
484 } else {
485 state.timer += dt;
486 while state.timer >= 1.0 / group.rate && state.spawned < group.count {
487 state.timer -= 1.0 / group.rate;
488 let pos = group.zone.sample(&mut self.rng, self.player_pos);
489 events.push(SpawnEvent {
490 blueprint: group.blueprint.clone(),
491 position: pos,
492 tag: group.tag.clone(),
493 wave_name: wave.name.clone(),
494 });
495 state.spawned += 1;
496 }
497 }
498 } else if group.blocking {
499 let needed = group.count;
501 if state.killed < needed {
502 all_done = false;
503 } else {
504 state.complete = true;
505 }
506 } else {
507 state.complete = true;
508 }
509 }
510
511 if all_done && self.active {
512 self.on_wave_cleared(&wave.clone());
513 }
514
515 events
516 }
517
518 fn activate_current_wave(&mut self) {
519 let wave = match self.waves.get(self.current_wave) {
520 Some(w) => w,
521 None => { self.finished = true; return; }
522 };
523 let n = wave.groups.len();
524 self.group_states = vec![GroupState {
525 spawned: 0, killed: 0, timer: 0.0,
526 delay_done: false, delay_timer: 0.0, complete: false,
527 }; n];
528 self.active = true;
529 }
530
531 fn on_wave_cleared(&mut self, wave: &SpawnWave) {
532 self.active = false;
533 self.wave_count += 1;
534
535 if let Some(flag) = &wave.on_clear {
536 self.flags.insert(flag.clone(), true);
537 }
538
539 if wave.repeat {
540 self.wave_timer = wave.pre_delay;
541 self.post_pending = true;
542 self.post_timer = wave.post_delay;
543 } else {
544 self.post_pending = true;
545 self.post_timer = wave.post_delay;
546 }
547 }
548
549 fn advance_wave(&mut self) {
550 if self.waves.get(self.current_wave).map(|w| w.repeat).unwrap_or(false) {
551 self.wave_timer = self.waves[self.current_wave].pre_delay;
553 } else {
554 self.current_wave += 1;
555 if self.current_wave >= self.waves.len() {
556 self.finished = true;
557 return;
558 }
559 self.wave_timer = self.waves[self.current_wave].pre_delay;
560 }
561 }
562
563 pub fn current_wave_name(&self) -> &str {
564 self.waves.get(self.current_wave).map(|w| w.name.as_str()).unwrap_or("none")
565 }
566
567 pub fn total_waves(&self) -> usize { self.waves.len() }
568 pub fn is_active(&self) -> bool { self.active }
569 pub fn get_flag(&self, k: &str) -> bool { self.flags.get(k).copied().unwrap_or(false) }
570}
571
572#[derive(Default)]
576pub struct BlueprintLibrary {
577 pub blueprints: HashMap<String, EntityBlueprint>,
578}
579
580impl BlueprintLibrary {
581 pub fn new() -> Self { Self::default() }
582
583 pub fn register(&mut self, blueprint: EntityBlueprint) {
584 self.blueprints.insert(blueprint.name.clone(), blueprint);
585 }
586
587 pub fn get(&self, name: &str) -> Option<&EntityBlueprint> {
588 self.blueprints.get(name)
589 }
590
591 pub fn with_defaults(mut self) -> Self {
593 self.register(EntityBlueprint::new("grunt")
594 .with_hp(60.0).with_speed(2.5).with_damage(8.0)
595 .with_color([0.8, 0.2, 0.2, 1.0]).with_glyph('g').tagged("enemy"));
596 self.register(EntityBlueprint::new("archer")
597 .with_hp(40.0).with_speed(2.0).with_damage(15.0)
598 .with_color([0.8, 0.5, 0.2, 1.0]).with_glyph('a').tagged("enemy"));
599 self.register(EntityBlueprint::new("tank")
600 .with_hp(200.0).with_speed(1.5).with_damage(25.0)
601 .with_color([0.5, 0.2, 0.8, 1.0]).with_glyph('T').tagged("enemy"));
602 self.register(EntityBlueprint::new("healer")
603 .with_hp(50.0).with_speed(2.0).with_damage(5.0)
604 .with_color([0.2, 0.9, 0.4, 1.0]).with_glyph('h').tagged("enemy"));
605 self.register(EntityBlueprint::new("boss")
606 .with_hp(1000.0).with_speed(3.5).with_damage(50.0)
607 .with_color([1.0, 0.1, 0.1, 1.0]).with_glyph('B').tagged("enemy").tagged("boss")
608 .with_attr("enrage_threshold", 0.3));
609 self
610 }
611}
612
613#[cfg(test)]
616mod tests {
617 use super::*;
618
619 #[test]
620 fn spawn_zone_point() {
621 let z = SpawnZone::Point(Vec3::new(1.0, 2.0, 3.0));
622 let mut rng = 12345u64;
623 let p = z.sample(&mut rng, Vec3::ZERO);
624 assert_eq!(p, Vec3::new(1.0, 2.0, 3.0));
625 }
626
627 #[test]
628 fn spawn_zone_sphere_bounded() {
629 let z = SpawnZone::Sphere { center: Vec3::ZERO, radius: 5.0 };
630 let mut rng = 42u64;
631 for _ in 0..100 {
632 let p = z.sample(&mut rng, Vec3::ZERO);
633 assert!(p.length() <= 5.05, "Point outside sphere: {:?}", p);
634 }
635 }
636
637 #[test]
638 fn spawn_pattern_ring_count() {
639 let p = SpawnPattern::Ring { radius: 3.0, phase_offset: 0.0 };
640 let positions = p.positions(8, Vec3::ZERO, &mut 0u64);
641 assert_eq!(positions.len(), 8);
642 }
643
644 #[test]
645 fn spawn_pattern_grid() {
646 let p = SpawnPattern::Grid { cols: 3, spacing: Vec3::ONE };
647 let positions = p.positions(9, Vec3::ZERO, &mut 0u64);
648 assert_eq!(positions.len(), 9);
649 }
650
651 #[test]
652 fn blueprint_library_default() {
653 let lib = BlueprintLibrary::new().with_defaults();
654 assert!(lib.get("grunt").is_some());
655 assert!(lib.get("boss").is_some());
656 assert!(lib.get("nobody").is_none());
657 }
658
659 #[test]
660 fn wave_manager_starts_and_spawns() {
661 let lib = BlueprintLibrary::new().with_defaults();
662 let wave = SpawnWave::new("w1", vec![
663 SpawnGroup::new("grunt", 3, SpawnZone::Point(Vec3::ZERO))
664 .with_rate(0.0) .non_blocking(),
666 ]).with_pre_delay(0.0).with_post_delay(0.0);
667
668 let mut mgr = WaveManager::new(vec![wave], lib);
669 mgr.start();
670
671 let events = mgr.tick(0.016);
673 assert!(!events.is_empty(), "Expected spawn events");
674 }
675
676 #[test]
677 fn wave_manager_rate_spawn() {
678 let lib = BlueprintLibrary::new().with_defaults();
679 let wave = SpawnWave::new("w1", vec![
680 SpawnGroup::new("grunt", 10, SpawnZone::Point(Vec3::ZERO))
681 .with_rate(5.0) .non_blocking(),
683 ]).with_pre_delay(0.0).with_post_delay(0.0);
684
685 let mut mgr = WaveManager::new(vec![wave], lib);
686 mgr.start();
687
688 let mut total = 0;
689 for _ in 0..60 {
690 total += mgr.tick(1.0 / 60.0).len();
691 }
692 assert!(total >= 4 && total <= 6, "Expected ~5 spawns, got {}", total);
694 }
695
696 #[test]
697 fn wave_advances() {
698 let lib = BlueprintLibrary::new().with_defaults();
699 let w1 = SpawnWave::new("w1", vec![
700 SpawnGroup::new("grunt", 1, SpawnZone::Point(Vec3::ZERO))
701 .non_blocking(),
702 ]).with_pre_delay(0.0).with_post_delay(0.0);
703 let w2 = SpawnWave::new("w2", vec![
704 SpawnGroup::new("tank", 1, SpawnZone::Point(Vec3::ONE))
705 .non_blocking(),
706 ]).with_pre_delay(0.0).with_post_delay(0.0);
707
708 let mut mgr = WaveManager::new(vec![w1, w2], lib);
709 mgr.start();
710
711 for _ in 0..30 { mgr.tick(0.1); }
713 assert_eq!(mgr.current_wave_name(), "w2");
714 }
715}