1use glam::{Vec2, Vec4};
29use std::collections::HashMap;
30
31#[derive(Debug, Clone)]
35pub struct FrameGlyph {
36 pub character: char,
37 pub offset: Vec2,
39 pub color: Vec4,
41 pub emission: f32,
43 pub scale: f32,
45}
46
47impl FrameGlyph {
48 pub fn new(ch: char, offset: Vec2, color: Vec4) -> Self {
49 Self { character: ch, offset, color, emission: 0.0, scale: 1.0 }
50 }
51
52 pub fn colored(ch: char, x: f32, y: f32, r: f32, g: f32, b: f32) -> Self {
53 Self::new(ch, Vec2::new(x, y), Vec4::new(r, g, b, 1.0))
54 }
55
56 pub fn white(ch: char, x: f32, y: f32) -> Self {
57 Self::colored(ch, x, y, 1.0, 1.0, 1.0)
58 }
59
60 pub fn with_emission(mut self, e: f32) -> Self { self.emission = e; self }
61 pub fn with_scale(mut self, s: f32) -> Self { self.scale = s; self }
62}
63
64#[derive(Debug, Clone)]
68pub struct SpriteFrame {
69 pub glyphs: Vec<FrameGlyph>,
71 pub event: Option<String>,
73 pub duration_override: Option<f32>,
75}
76
77impl SpriteFrame {
78 pub fn new(glyphs: Vec<FrameGlyph>) -> Self {
79 Self { glyphs, event: None, duration_override: None }
80 }
81
82 pub fn with_event(mut self, event: impl Into<String>) -> Self {
83 self.event = Some(event.into());
84 self
85 }
86
87 pub fn with_duration(mut self, d: f32) -> Self {
88 self.duration_override = Some(d);
89 self
90 }
91
92 pub fn from_ascii(art: &str, color: Vec4) -> Self {
95 let lines: Vec<&str> = art.lines().collect();
96 let height = lines.len() as f32;
97 let mut glyphs = Vec::new();
98
99 for (row, line) in lines.iter().enumerate() {
100 let width = line.len() as f32;
101 for (col, ch) in line.chars().enumerate() {
102 if ch == ' ' { continue; }
103 let x = col as f32 - width * 0.5;
104 let y = -(row as f32 - height * 0.5); glyphs.push(FrameGlyph::new(ch, Vec2::new(x, y), color));
106 }
107 }
108
109 Self::new(glyphs)
110 }
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum LoopMode {
118 Once,
120 Loop,
122 PingPong,
124 OnceAndDone,
126}
127
128#[derive(Debug, Clone)]
132pub struct SpriteAnimation {
133 pub name: String,
134 pub frames: Vec<SpriteFrame>,
135 pub frame_duration: f32,
137 pub loop_mode: LoopMode,
138 pub speed: f32,
140}
141
142impl SpriteAnimation {
143 pub fn new(name: impl Into<String>, frames: Vec<SpriteFrame>, frame_duration: f32, loop_mode: LoopMode) -> Self {
144 Self {
145 name: name.into(),
146 frames,
147 frame_duration,
148 loop_mode,
149 speed: 1.0,
150 }
151 }
152
153 pub fn with_speed(mut self, s: f32) -> Self { self.speed = s; self }
154
155 pub fn total_duration(&self) -> f32 {
157 let mut total = 0.0;
158 for frame in &self.frames {
159 total += frame.duration_override.unwrap_or(self.frame_duration);
160 }
161 total / self.speed.max(0.01)
162 }
163
164 pub fn frame_count(&self) -> usize { self.frames.len() }
166
167 pub fn frame_time(&self, index: usize) -> f32 {
169 self.frames.get(index)
170 .and_then(|f| f.duration_override)
171 .unwrap_or(self.frame_duration) / self.speed.max(0.01)
172 }
173}
174
175#[derive(Debug, Clone)]
179pub struct AnimState {
180 pub name: String,
181 pub animation: String,
183 pub speed: f32,
185}
186
187impl AnimState {
188 pub fn new(name: impl Into<String>, animation: impl Into<String>) -> Self {
189 Self { name: name.into(), animation: animation.into(), speed: 1.0 }
190 }
191
192 pub fn with_speed(mut self, s: f32) -> Self { self.speed = s; self }
193}
194
195#[derive(Debug, Clone)]
197pub enum AnimCondition {
198 ParamGt { param: String, value: f32 },
200 ParamLt { param: String, value: f32 },
202 Trigger(String),
204 AnimationDone,
206 Always,
208}
209
210impl AnimCondition {
211 pub fn trigger(name: impl Into<String>) -> Self { Self::Trigger(name.into()) }
212 pub fn param_gt(name: impl Into<String>, v: f32) -> Self {
213 Self::ParamGt { param: name.into(), value: v }
214 }
215 pub fn param_lt(name: impl Into<String>, v: f32) -> Self {
216 Self::ParamLt { param: name.into(), value: v }
217 }
218}
219
220#[derive(Debug, Clone)]
222pub struct AnimTransition {
223 pub from: String,
224 pub to: String,
225 pub condition: AnimCondition,
226 pub blend_time: f32,
228}
229
230impl AnimTransition {
231 pub fn new(from: impl Into<String>, to: impl Into<String>, condition: AnimCondition) -> Self {
232 Self {
233 from: from.into(),
234 to: to.into(),
235 condition,
236 blend_time: 0.0,
237 }
238 }
239
240 pub fn with_blend(mut self, t: f32) -> Self { self.blend_time = t; self }
241}
242
243pub struct AnimationStateMachine {
245 pub states: HashMap<String, AnimState>,
246 pub transitions: Vec<AnimTransition>,
247 pub params: HashMap<String, f32>,
248 pub triggers: HashMap<String, bool>,
249 pub current_state: Option<String>,
250}
251
252impl AnimationStateMachine {
253 pub fn new() -> Self {
254 Self {
255 states: HashMap::new(),
256 transitions: Vec::new(),
257 params: HashMap::new(),
258 triggers: HashMap::new(),
259 current_state: None,
260 }
261 }
262
263 pub fn add_state(&mut self, state: AnimState) {
264 self.states.insert(state.name.clone(), state);
265 }
266
267 pub fn add_transition(&mut self, transition: AnimTransition) {
268 self.transitions.push(transition);
269 }
270
271 pub fn set_param(&mut self, name: &str, value: f32) {
272 self.params.insert(name.to_owned(), value);
273 }
274
275 pub fn set_trigger(&mut self, name: &str) {
276 self.triggers.insert(name.to_owned(), true);
277 }
278
279 pub fn start(&mut self, state: &str) {
280 self.current_state = Some(state.to_owned());
281 }
282
283 pub fn evaluate(&mut self, animation_done: bool) -> Option<String> {
285 let current = self.current_state.as_ref()?;
286 let current = current.clone();
287
288 for trans in &self.transitions {
289 if trans.from != current && trans.from != "*" { continue; }
290
291 let satisfied = match &trans.condition {
292 AnimCondition::ParamGt { param, value } => {
293 self.params.get(param).copied().unwrap_or(0.0) > *value
294 }
295 AnimCondition::ParamLt { param, value } => {
296 self.params.get(param).copied().unwrap_or(0.0) < *value
297 }
298 AnimCondition::Trigger(name) => {
299 self.triggers.get(name).copied().unwrap_or(false)
300 }
301 AnimCondition::AnimationDone => animation_done,
302 AnimCondition::Always => true,
303 };
304
305 if satisfied {
306 if let AnimCondition::Trigger(name) = &trans.condition {
308 self.triggers.insert(name.clone(), false);
309 }
310
311 let new_state = trans.to.clone();
312 let anim = self.states.get(&new_state)
313 .map(|s| s.animation.clone());
314 self.current_state = Some(new_state);
315 return anim;
316 }
317 }
318
319 None
320 }
321
322 pub fn current_animation(&self) -> Option<&str> {
324 let state_name = self.current_state.as_ref()?;
325 self.states.get(state_name).map(|s| s.animation.as_str())
326 }
327}
328
329impl Default for AnimationStateMachine {
330 fn default() -> Self { Self::new() }
331}
332
333#[derive(Debug, Clone)]
337pub struct AnimEvent {
338 pub animation: String,
339 pub frame_index: usize,
340 pub tag: String,
341}
342
343pub struct SpriteAnimator {
345 pub animations: HashMap<String, SpriteAnimation>,
346 current: String,
347 frame_index: usize,
348 frame_timer: f32,
349 playing: bool,
350 finished: bool,
352 ping_pong_forward: bool,
354 pub state_machine: Option<AnimationStateMachine>,
356 pending_events: Vec<AnimEvent>,
358}
359
360impl SpriteAnimator {
361 pub fn new() -> Self {
362 Self {
363 animations: HashMap::new(),
364 current: String::new(),
365 frame_index: 0,
366 frame_timer: 0.0,
367 playing: false,
368 finished: false,
369 ping_pong_forward: true,
370 state_machine: None,
371 pending_events: Vec::new(),
372 }
373 }
374
375 pub fn add_animation(&mut self, anim: SpriteAnimation) {
377 self.animations.insert(anim.name.clone(), anim);
378 }
379
380 pub fn play(&mut self, name: &str) {
382 if self.animations.contains_key(name) {
383 self.current = name.to_owned();
384 self.frame_index = 0;
385 self.frame_timer = 0.0;
386 self.playing = true;
387 self.finished = false;
388 self.ping_pong_forward = true;
389 }
390 }
391
392 pub fn play_if_different(&mut self, name: &str) {
394 if self.current != name {
395 self.play(name);
396 }
397 }
398
399 pub fn stop(&mut self) { self.playing = false; }
401
402 pub fn resume(&mut self) { self.playing = true; }
404
405 pub fn is_finished(&self) -> bool { self.finished }
407
408 pub fn is_playing(&self) -> bool { self.playing }
410
411 pub fn current_animation(&self) -> &str { &self.current }
413
414 pub fn current_frame_index(&self) -> usize { self.frame_index }
416
417 pub fn tick(&mut self, dt: f32) {
419 self.finished = false;
420
421 if let Some(ref mut sm) = self.state_machine {
423 if let Some(new_anim) = sm.evaluate(self.finished) {
424 self.play(&new_anim);
425 }
426 }
427
428 if !self.playing { return; }
429
430 let anim = match self.animations.get(&self.current) {
431 Some(a) => a.clone(), None => return,
433 };
434
435 if anim.frames.is_empty() { return; }
436
437 let frame_dur = anim.frame_time(self.frame_index);
438 self.frame_timer += dt;
439
440 while self.frame_timer >= frame_dur && frame_dur > 0.0 {
441 self.frame_timer -= frame_dur;
442
443 if let Some(ref event) = anim.frames[self.frame_index].event {
445 self.pending_events.push(AnimEvent {
446 animation: self.current.clone(),
447 frame_index: self.frame_index,
448 tag: event.clone(),
449 });
450 }
451
452 match anim.loop_mode {
454 LoopMode::Loop => {
455 self.frame_index = (self.frame_index + 1) % anim.frames.len();
456 }
457 LoopMode::Once => {
458 if self.frame_index + 1 < anim.frames.len() {
459 self.frame_index += 1;
460 } else {
461 self.playing = false;
462 self.finished = true;
463 }
464 }
465 LoopMode::OnceAndDone => {
466 if self.frame_index + 1 < anim.frames.len() {
467 self.frame_index += 1;
468 } else {
469 self.playing = false;
470 self.finished = true;
471 }
472 }
473 LoopMode::PingPong => {
474 if self.ping_pong_forward {
475 if self.frame_index + 1 < anim.frames.len() {
476 self.frame_index += 1;
477 } else {
478 self.ping_pong_forward = false;
479 if self.frame_index > 0 {
480 self.frame_index -= 1;
481 }
482 }
483 } else {
484 if self.frame_index > 0 {
485 self.frame_index -= 1;
486 } else {
487 self.ping_pong_forward = true;
488 self.frame_index += 1;
489 }
490 }
491 }
492 }
493
494 break;
496 }
497 }
498
499 pub fn current_glyphs(&self) -> &[FrameGlyph] {
501 self.animations.get(&self.current)
502 .and_then(|a| a.frames.get(self.frame_index))
503 .map(|f| f.glyphs.as_slice())
504 .unwrap_or(&[])
505 }
506
507 pub fn current_frame(&self) -> Option<&SpriteFrame> {
509 self.animations.get(&self.current)
510 .and_then(|a| a.frames.get(self.frame_index))
511 }
512
513 pub fn drain_events(&mut self) -> Vec<AnimEvent> {
515 std::mem::take(&mut self.pending_events)
516 }
517}
518
519impl Default for SpriteAnimator {
520 fn default() -> Self { Self::new() }
521}
522
523pub struct AnimationLibrary;
529
530impl AnimationLibrary {
531 fn white() -> Vec4 { Vec4::new(1.0, 1.0, 1.0, 1.0) }
534 fn red() -> Vec4 { Vec4::new(1.0, 0.3, 0.2, 1.0) }
535 fn blue() -> Vec4 { Vec4::new(0.3, 0.5, 1.0, 1.0) }
536 fn gold() -> Vec4 { Vec4::new(1.0, 0.85, 0.3, 1.0) }
537 fn green() -> Vec4 { Vec4::new(0.3, 1.0, 0.4, 1.0) }
538 fn gray() -> Vec4 { Vec4::new(0.5, 0.5, 0.5, 1.0) }
539 fn dark() -> Vec4 { Vec4::new(0.2, 0.2, 0.2, 0.5) }
540 fn purple() -> Vec4 { Vec4::new(0.7, 0.2, 1.0, 1.0) }
541 fn cyan() -> Vec4 { Vec4::new(0.2, 0.9, 1.0, 1.0) }
542 fn orange() -> Vec4 { Vec4::new(1.0, 0.5, 0.1, 1.0) }
543
544 pub fn player_idle() -> SpriteAnimation {
548 let c = Self::white();
549 let frame1 = SpriteFrame::new(vec![
550 FrameGlyph::white('O', 0.0, 2.0), FrameGlyph::white('|', 0.0, 1.0), FrameGlyph::white('/', -1.0, 1.0), FrameGlyph::white('\\', 1.0, 1.0), FrameGlyph::white('|', 0.0, 0.0), FrameGlyph::white('/', -0.5, -1.0), FrameGlyph::white('\\', 0.5, -1.0), ]);
558
559 let frame2 = SpriteFrame::new(vec![
561 FrameGlyph::white('O', 0.0, 2.1),
562 FrameGlyph::white('|', 0.0, 1.0),
563 FrameGlyph::white('/', -1.1, 1.1),
564 FrameGlyph::white('\\', 1.1, 1.1),
565 FrameGlyph::white('|', 0.0, 0.0),
566 FrameGlyph::white('/', -0.5, -1.0),
567 FrameGlyph::white('\\', 0.5, -1.0),
568 ]);
569
570 SpriteAnimation::new("player_idle", vec![frame1, frame2], 0.6, LoopMode::PingPong)
571 }
572
573 pub fn player_attack() -> SpriteAnimation {
575 let f1 = SpriteFrame::new(vec![
577 FrameGlyph::white('O', 0.0, 2.0),
578 FrameGlyph::white('|', 0.0, 1.0),
579 FrameGlyph::white('/', -1.0, 1.0),
580 FrameGlyph::colored('\\', 1.0, 2.0, 1.0, 0.9, 0.3), FrameGlyph::colored('/', 1.5, 2.5, 0.8, 0.8, 0.8), FrameGlyph::white('|', 0.0, 0.0),
583 FrameGlyph::white('/', -0.5, -1.0),
584 FrameGlyph::white('\\', 0.5, -1.0),
585 ]);
586
587 let f2 = SpriteFrame::new(vec![
589 FrameGlyph::white('O', 0.0, 2.0),
590 FrameGlyph::white('|', 0.0, 1.0),
591 FrameGlyph::white('/', -1.0, 1.0),
592 FrameGlyph::colored('-', 1.5, 1.0, 1.0, 0.9, 0.3), FrameGlyph::colored('>', 2.5, 1.0, 1.0, 0.7, 0.2).with_emission(0.5), FrameGlyph::white('|', 0.0, 0.0),
595 FrameGlyph::white('/', -0.5, -1.0),
596 FrameGlyph::white('\\', 0.5, -1.0),
597 ]).with_event("hit");
598
599 let f3 = SpriteFrame::new(vec![
601 FrameGlyph::white('O', 0.0, 2.0),
602 FrameGlyph::white('|', 0.0, 1.0),
603 FrameGlyph::white('/', -1.0, 1.0),
604 FrameGlyph::colored('\\', 1.5, 0.0, 1.0, 0.9, 0.3), FrameGlyph::colored('\\', 2.0, -0.5, 0.8, 0.8, 0.8), FrameGlyph::white('|', 0.0, 0.0),
607 FrameGlyph::white('/', -0.5, -1.0),
608 FrameGlyph::white('\\', 0.5, -1.0),
609 ]);
610
611 let f4 = SpriteFrame::new(vec![
613 FrameGlyph::white('O', 0.0, 2.0),
614 FrameGlyph::white('|', 0.0, 1.0),
615 FrameGlyph::white('/', -1.0, 1.0),
616 FrameGlyph::white('\\', 1.0, 1.0),
617 FrameGlyph::white('|', 0.0, 0.0),
618 FrameGlyph::white('/', -0.5, -1.0),
619 FrameGlyph::white('\\', 0.5, -1.0),
620 ]);
621
622 SpriteAnimation::new("player_attack", vec![f1, f2, f3, f4], 0.08, LoopMode::OnceAndDone)
623 }
624
625 pub fn player_cast() -> SpriteAnimation {
627 let f1 = SpriteFrame::new(vec![
629 FrameGlyph::white('O', 0.0, 2.0),
630 FrameGlyph::white('|', 0.0, 1.0),
631 FrameGlyph::colored('/', -1.0, 2.0, 0.3, 0.5, 1.0), FrameGlyph::colored('\\', 1.0, 2.0, 0.3, 0.5, 1.0), FrameGlyph::white('|', 0.0, 0.0),
634 FrameGlyph::white('/', -0.5, -1.0),
635 FrameGlyph::white('\\', 0.5, -1.0),
636 ]);
637
638 let f2 = SpriteFrame::new(vec![
640 FrameGlyph::white('O', 0.0, 2.0),
641 FrameGlyph::white('|', 0.0, 1.0),
642 FrameGlyph::colored('/', -1.0, 2.0, 0.3, 0.5, 1.0),
643 FrameGlyph::colored('\\', 1.0, 2.0, 0.3, 0.5, 1.0),
644 FrameGlyph::colored('*', 0.0, 2.5, 0.5, 0.7, 1.0).with_emission(1.0), FrameGlyph::colored('·', -0.5, 2.8, 0.4, 0.6, 1.0).with_emission(0.6),
646 FrameGlyph::colored('·', 0.5, 2.8, 0.4, 0.6, 1.0).with_emission(0.6),
647 FrameGlyph::white('|', 0.0, 0.0),
648 FrameGlyph::white('/', -0.5, -1.0),
649 FrameGlyph::white('\\', 0.5, -1.0),
650 ]);
651
652 let f3 = SpriteFrame::new(vec![
654 FrameGlyph::white('O', 0.0, 2.0),
655 FrameGlyph::white('|', 0.0, 1.0),
656 FrameGlyph::colored('-', -1.5, 1.5, 0.3, 0.5, 1.0), FrameGlyph::colored('-', 1.5, 1.5, 0.3, 0.5, 1.0),
658 FrameGlyph::colored('★', 0.0, 3.0, 0.5, 0.8, 1.0).with_emission(1.5), FrameGlyph::white('|', 0.0, 0.0),
660 FrameGlyph::white('/', -0.5, -1.0),
661 FrameGlyph::white('\\', 0.5, -1.0),
662 ]).with_event("cast_release");
663
664 SpriteAnimation::new("player_cast", vec![f1, f2, f3], 0.12, LoopMode::OnceAndDone)
665 }
666
667 pub fn player_hurt() -> SpriteAnimation {
669 let f1 = SpriteFrame::new(vec![
671 FrameGlyph::colored('O', -0.3, 2.1, 1.0, 0.5, 0.5),
672 FrameGlyph::colored('\\', -0.2, 1.0, 1.0, 0.5, 0.5),
673 FrameGlyph::colored('/', -1.3, 0.8, 1.0, 0.5, 0.5),
674 FrameGlyph::colored('\\', 0.8, 0.8, 1.0, 0.5, 0.5),
675 FrameGlyph::colored('|', -0.1, 0.0, 1.0, 0.5, 0.5),
676 FrameGlyph::colored('/', -0.6, -1.0, 1.0, 0.5, 0.5),
677 FrameGlyph::colored('\\', 0.4, -1.0, 1.0, 0.5, 0.5),
678 ]);
679
680 let f2 = SpriteFrame::new(vec![
682 FrameGlyph::white('O', 0.0, 2.0),
683 FrameGlyph::white('|', 0.0, 1.0),
684 FrameGlyph::white('/', -1.0, 1.0),
685 FrameGlyph::white('\\', 1.0, 1.0),
686 FrameGlyph::white('|', 0.0, 0.0),
687 FrameGlyph::white('/', -0.5, -1.0),
688 FrameGlyph::white('\\', 0.5, -1.0),
689 ]);
690
691 SpriteAnimation::new("player_hurt", vec![f1, f2], 0.15, LoopMode::OnceAndDone)
692 }
693
694 pub fn player_defend() -> SpriteAnimation {
696 let f1 = SpriteFrame::new(vec![
698 FrameGlyph::white('O', 0.0, 2.0),
699 FrameGlyph::white('|', 0.0, 1.0),
700 FrameGlyph::colored('X', 0.0, 1.5, 0.7, 0.9, 1.0), FrameGlyph::colored('[', -0.5, 1.5, 0.5, 0.5, 0.6), FrameGlyph::colored(']', 0.5, 1.5, 0.5, 0.5, 0.6), FrameGlyph::white('|', 0.0, 0.0),
704 FrameGlyph::white('/', -0.5, -1.0),
705 FrameGlyph::white('\\', 0.5, -1.0),
706 ]);
707
708 let f2 = SpriteFrame::new(vec![
710 FrameGlyph::white('O', 0.0, 2.0),
711 FrameGlyph::white('|', 0.0, 1.0),
712 FrameGlyph::colored('X', 0.0, 1.5, 0.8, 1.0, 1.0).with_emission(0.3),
713 FrameGlyph::colored('[', -0.5, 1.5, 0.6, 0.6, 0.7),
714 FrameGlyph::colored(']', 0.5, 1.5, 0.6, 0.6, 0.7),
715 FrameGlyph::white('|', 0.0, 0.0),
716 FrameGlyph::white('/', -0.5, -1.0),
717 FrameGlyph::white('\\', 0.5, -1.0),
718 ]);
719
720 SpriteAnimation::new("player_defend", vec![f1, f2], 0.3, LoopMode::Loop)
721 }
722
723 pub fn enemy_idle() -> SpriteAnimation {
727 let c = Self::red();
728 let f1 = SpriteFrame::new(vec![
729 FrameGlyph::colored('▼', 0.0, 2.0, 1.0, 0.3, 0.2),
730 FrameGlyph::colored('█', 0.0, 1.0, 0.8, 0.2, 0.1),
731 FrameGlyph::colored('/', -1.0, 0.5, 0.8, 0.2, 0.1),
732 FrameGlyph::colored('\\', 1.0, 0.5, 0.8, 0.2, 0.1),
733 FrameGlyph::colored('▲', -0.5, -0.5, 0.6, 0.15, 0.1),
734 FrameGlyph::colored('▲', 0.5, -0.5, 0.6, 0.15, 0.1),
735 ]);
736
737 let f2 = SpriteFrame::new(vec![
738 FrameGlyph::colored('▼', 0.2, 2.0, 1.0, 0.3, 0.2),
739 FrameGlyph::colored('█', 0.2, 1.0, 0.8, 0.2, 0.1),
740 FrameGlyph::colored('/', -0.8, 0.5, 0.8, 0.2, 0.1),
741 FrameGlyph::colored('\\', 1.2, 0.5, 0.8, 0.2, 0.1),
742 FrameGlyph::colored('▲', -0.3, -0.5, 0.6, 0.15, 0.1),
743 FrameGlyph::colored('▲', 0.7, -0.5, 0.6, 0.15, 0.1),
744 ]);
745
746 SpriteAnimation::new("enemy_idle", vec![f1, f2], 0.5, LoopMode::PingPong)
747 }
748
749 pub fn enemy_attack() -> SpriteAnimation {
751 let f1 = SpriteFrame::new(vec![
753 FrameGlyph::colored('▼', 0.5, 2.0, 1.0, 0.3, 0.2),
754 FrameGlyph::colored('█', 0.3, 1.0, 0.8, 0.2, 0.1),
755 FrameGlyph::colored('/', -0.5, 0.8, 0.8, 0.2, 0.1),
756 FrameGlyph::colored('-', 1.5, 1.0, 1.0, 0.3, 0.2),
757 FrameGlyph::colored('▲', -0.3, -0.5, 0.6, 0.15, 0.1),
758 FrameGlyph::colored('▲', 0.5, -0.5, 0.6, 0.15, 0.1),
759 ]);
760
761 let f2 = SpriteFrame::new(vec![
763 FrameGlyph::colored('▼', 1.0, 1.8, 1.0, 0.4, 0.2),
764 FrameGlyph::colored('█', 0.5, 1.0, 0.8, 0.2, 0.1),
765 FrameGlyph::colored('/', -0.3, 0.8, 0.8, 0.2, 0.1),
766 FrameGlyph::colored('>', 2.0, 1.0, 1.0, 0.5, 0.2).with_emission(0.8),
767 FrameGlyph::colored('▲', -0.1, -0.5, 0.6, 0.15, 0.1),
768 FrameGlyph::colored('▲', 0.7, -0.5, 0.6, 0.15, 0.1),
769 ]).with_event("hit");
770
771 let f3 = SpriteFrame::new(vec![
773 FrameGlyph::colored('▼', 0.0, 2.0, 1.0, 0.3, 0.2),
774 FrameGlyph::colored('█', 0.0, 1.0, 0.8, 0.2, 0.1),
775 FrameGlyph::colored('/', -1.0, 0.5, 0.8, 0.2, 0.1),
776 FrameGlyph::colored('\\', 1.0, 0.5, 0.8, 0.2, 0.1),
777 FrameGlyph::colored('▲', -0.5, -0.5, 0.6, 0.15, 0.1),
778 FrameGlyph::colored('▲', 0.5, -0.5, 0.6, 0.15, 0.1),
779 ]);
780
781 SpriteAnimation::new("enemy_attack", vec![f1, f2, f3], 0.1, LoopMode::OnceAndDone)
782 }
783
784 pub fn enemy_death() -> SpriteAnimation {
786 let f1 = SpriteFrame::new(vec![
788 FrameGlyph::colored('▼', 0.0, 2.0, 1.0, 0.3, 0.2),
789 FrameGlyph::colored('█', 0.0, 1.0, 0.8, 0.2, 0.1),
790 FrameGlyph::colored('/', -1.0, 0.5, 0.8, 0.2, 0.1),
791 FrameGlyph::colored('\\', 1.0, 0.5, 0.8, 0.2, 0.1),
792 FrameGlyph::colored('▲', -0.5, -0.5, 0.6, 0.15, 0.1),
793 FrameGlyph::colored('▲', 0.5, -0.5, 0.6, 0.15, 0.1),
794 ]);
795
796 let f2 = SpriteFrame::new(vec![
798 FrameGlyph::colored('▼', 0.1, 2.0, 0.8, 0.3, 0.2),
799 FrameGlyph::colored('░', 0.0, 1.0, 0.7, 0.2, 0.1),
800 FrameGlyph::colored('/', -1.2, 0.3, 0.6, 0.15, 0.1),
801 FrameGlyph::colored('\\', 1.2, 0.3, 0.6, 0.15, 0.1),
802 FrameGlyph::colored('·', -0.5, -0.5, 0.5, 0.1, 0.1),
803 FrameGlyph::colored('·', 0.5, -0.5, 0.5, 0.1, 0.1),
804 ]);
805
806 let f3 = SpriteFrame::new(vec![
808 FrameGlyph::colored('·', 0.3, 2.3, 0.5, 0.2, 0.15),
809 FrameGlyph::colored('░', -0.2, 1.2, 0.4, 0.15, 0.1),
810 FrameGlyph::colored('·', -1.5, 0.1, 0.3, 0.1, 0.05),
811 FrameGlyph::colored('·', 1.5, 0.1, 0.3, 0.1, 0.05),
812 FrameGlyph::colored('·', 0.0, -0.8, 0.2, 0.05, 0.05),
813 ]);
814
815 let f4 = SpriteFrame::new(vec![
817 FrameGlyph::colored('·', 0.5, 2.5, 0.2, 0.1, 0.1),
818 FrameGlyph::colored('·', -0.5, 0.5, 0.15, 0.05, 0.05),
819 ]).with_event("death_complete");
820
821 SpriteAnimation::new("enemy_death", vec![f1, f2, f3, f4], 0.2, LoopMode::OnceAndDone)
822 }
823
824 pub fn boss_idle(boss_name: &str) -> SpriteAnimation {
828 match boss_name {
829 "Mirror" => Self::boss_mirror_idle(),
830 "Null" => Self::boss_null_idle(),
831 "Committee" => Self::boss_committee_idle(),
832 "FibonacciHydra" => Self::boss_hydra_idle(),
833 "Eigenstate" => Self::boss_eigenstate_idle(),
834 "Ouroboros" => Self::boss_ouroboros_idle(),
835 "AlgorithmReborn" => Self::boss_algorithm_idle(),
836 "ChaosWeaver" => Self::boss_chaos_weaver_idle(),
837 "VoidSerpent" => Self::boss_void_serpent_idle(),
838 "PrimeFactorial" => Self::boss_prime_idle(),
839 _ => Self::enemy_idle(),
840 }
841 }
842
843 pub fn boss_attack(boss_name: &str) -> SpriteAnimation {
845 match boss_name {
846 "Mirror" => Self::boss_mirror_attack(),
847 "Null" => Self::boss_null_attack(),
848 "Committee" => Self::boss_committee_attack(),
849 "FibonacciHydra" => Self::boss_hydra_attack(),
850 "Eigenstate" => Self::boss_eigenstate_attack(),
851 "Ouroboros" => Self::boss_ouroboros_attack(),
852 "AlgorithmReborn" => Self::boss_algorithm_attack(),
853 "ChaosWeaver" => Self::boss_chaos_weaver_attack(),
854 "VoidSerpent" => Self::boss_void_serpent_attack(),
855 "PrimeFactorial" => Self::boss_prime_attack(),
856 _ => Self::enemy_attack(),
857 }
858 }
859
860 fn boss_mirror_idle() -> SpriteAnimation {
863 let f1 = SpriteFrame::new(vec![
864 FrameGlyph::colored('◇', 0.0, 3.0, 0.8, 0.9, 1.0).with_emission(0.5),
865 FrameGlyph::colored('│', 0.0, 2.0, 0.7, 0.8, 0.9),
866 FrameGlyph::colored('◇', -1.0, 1.0, 0.6, 0.7, 0.8),
867 FrameGlyph::colored('◇', 1.0, 1.0, 0.6, 0.7, 0.8),
868 FrameGlyph::colored('│', 0.0, 0.0, 0.7, 0.8, 0.9),
869 FrameGlyph::colored('△', -0.5, -1.0, 0.5, 0.6, 0.7),
870 FrameGlyph::colored('△', 0.5, -1.0, 0.5, 0.6, 0.7),
871 ]);
872 let f2 = SpriteFrame::new(vec![
873 FrameGlyph::colored('◆', 0.0, 3.0, 0.9, 1.0, 1.0).with_emission(0.8),
874 FrameGlyph::colored('│', 0.0, 2.0, 0.8, 0.9, 1.0),
875 FrameGlyph::colored('◆', -1.0, 1.0, 0.7, 0.8, 0.9),
876 FrameGlyph::colored('◆', 1.0, 1.0, 0.7, 0.8, 0.9),
877 FrameGlyph::colored('│', 0.0, 0.0, 0.8, 0.9, 1.0),
878 FrameGlyph::colored('△', -0.5, -1.0, 0.6, 0.7, 0.8),
879 FrameGlyph::colored('△', 0.5, -1.0, 0.6, 0.7, 0.8),
880 ]);
881 SpriteAnimation::new("boss_mirror_idle", vec![f1, f2], 0.7, LoopMode::PingPong)
882 }
883
884 fn boss_null_idle() -> SpriteAnimation {
885 let f1 = SpriteFrame::new(vec![
886 FrameGlyph::colored('∅', 0.0, 3.0, 0.3, 0.3, 0.3).with_emission(0.3),
887 FrameGlyph::colored('█', 0.0, 2.0, 0.1, 0.1, 0.1),
888 FrameGlyph::colored('░', -1.0, 1.0, 0.2, 0.2, 0.2),
889 FrameGlyph::colored('░', 1.0, 1.0, 0.2, 0.2, 0.2),
890 FrameGlyph::colored('▓', 0.0, 0.0, 0.15, 0.15, 0.15),
891 ]);
892 let f2 = SpriteFrame::new(vec![
893 FrameGlyph::colored('∅', 0.0, 3.0, 0.2, 0.2, 0.2),
894 FrameGlyph::colored('░', 0.0, 2.0, 0.08, 0.08, 0.08),
895 FrameGlyph::colored(' ', -1.0, 1.0, 0.0, 0.0, 0.0),
896 FrameGlyph::colored('░', 1.0, 1.0, 0.15, 0.15, 0.15),
897 FrameGlyph::colored('▒', 0.0, 0.0, 0.1, 0.1, 0.1),
898 ]);
899 SpriteAnimation::new("boss_null_idle", vec![f1, f2], 0.8, LoopMode::PingPong)
900 }
901
902 fn boss_committee_idle() -> SpriteAnimation {
903 let f1 = SpriteFrame::new(vec![
904 FrameGlyph::colored('☻', -2.0, 2.0, 1.0, 0.8, 0.3),
906 FrameGlyph::colored('☻', -1.0, 2.0, 0.3, 1.0, 0.4),
907 FrameGlyph::colored('☻', 0.0, 2.5, 1.0, 0.3, 0.3), FrameGlyph::colored('☻', 1.0, 2.0, 0.3, 0.5, 1.0),
909 FrameGlyph::colored('☻', 2.0, 2.0, 0.8, 0.3, 1.0),
910 FrameGlyph::colored('═', -2.0, 1.0, 0.5, 0.4, 0.2),
911 FrameGlyph::colored('═', -1.0, 1.0, 0.5, 0.4, 0.2),
912 FrameGlyph::colored('═', 0.0, 1.0, 0.5, 0.4, 0.2),
913 FrameGlyph::colored('═', 1.0, 1.0, 0.5, 0.4, 0.2),
914 FrameGlyph::colored('═', 2.0, 1.0, 0.5, 0.4, 0.2),
915 ]);
916 let f2 = SpriteFrame::new(vec![
917 FrameGlyph::colored('☻', -2.0, 2.1, 1.0, 0.8, 0.3),
918 FrameGlyph::colored('☻', -1.0, 1.9, 0.3, 1.0, 0.4),
919 FrameGlyph::colored('☻', 0.0, 2.5, 1.0, 0.3, 0.3),
920 FrameGlyph::colored('☻', 1.0, 2.1, 0.3, 0.5, 1.0),
921 FrameGlyph::colored('☻', 2.0, 1.9, 0.8, 0.3, 1.0),
922 FrameGlyph::colored('═', -2.0, 1.0, 0.5, 0.4, 0.2),
923 FrameGlyph::colored('═', -1.0, 1.0, 0.5, 0.4, 0.2),
924 FrameGlyph::colored('═', 0.0, 1.0, 0.5, 0.4, 0.2),
925 FrameGlyph::colored('═', 1.0, 1.0, 0.5, 0.4, 0.2),
926 FrameGlyph::colored('═', 2.0, 1.0, 0.5, 0.4, 0.2),
927 ]);
928 SpriteAnimation::new("boss_committee_idle", vec![f1, f2], 0.6, LoopMode::PingPong)
929 }
930
931 fn boss_hydra_idle() -> SpriteAnimation {
932 let f1 = SpriteFrame::new(vec![
933 FrameGlyph::colored('◆', -1.0, 3.0, 0.2, 0.8, 0.3),
934 FrameGlyph::colored('◆', 1.0, 3.0, 0.2, 0.8, 0.3),
935 FrameGlyph::colored('\\', -0.5, 2.0, 0.15, 0.6, 0.2),
936 FrameGlyph::colored('/', 0.5, 2.0, 0.15, 0.6, 0.2),
937 FrameGlyph::colored('█', 0.0, 1.0, 0.1, 0.5, 0.15),
938 FrameGlyph::colored('▲', 0.0, -0.5, 0.08, 0.4, 0.1),
939 ]);
940 let f2 = SpriteFrame::new(vec![
941 FrameGlyph::colored('◆', -1.2, 3.2, 0.2, 0.8, 0.3),
942 FrameGlyph::colored('◆', 1.2, 2.8, 0.2, 0.8, 0.3),
943 FrameGlyph::colored('\\', -0.6, 2.1, 0.15, 0.6, 0.2),
944 FrameGlyph::colored('/', 0.6, 1.9, 0.15, 0.6, 0.2),
945 FrameGlyph::colored('█', 0.0, 1.0, 0.1, 0.5, 0.15),
946 FrameGlyph::colored('▲', 0.0, -0.5, 0.08, 0.4, 0.1),
947 ]);
948 SpriteAnimation::new("boss_hydra_idle", vec![f1, f2], 0.5, LoopMode::PingPong)
949 }
950
951 fn boss_eigenstate_idle() -> SpriteAnimation {
952 let f1 = SpriteFrame::new(vec![
954 FrameGlyph::colored('ψ', 0.0, 3.0, 0.5, 0.2, 1.0).with_emission(0.6),
955 FrameGlyph::colored('|', 0.0, 2.0, 0.4, 0.15, 0.8),
956 FrameGlyph::colored('◇', -1.0, 1.5, 0.3, 0.1, 0.7),
957 FrameGlyph::colored('◇', 1.0, 1.5, 0.3, 0.1, 0.7),
958 FrameGlyph::colored('▽', 0.0, 0.0, 0.2, 0.1, 0.6),
959 ]);
960 let f2 = SpriteFrame::new(vec![
961 FrameGlyph::colored('φ', 0.0, 3.0, 1.0, 0.2, 0.5).with_emission(0.6),
962 FrameGlyph::colored('│', 0.0, 2.0, 0.8, 0.15, 0.4),
963 FrameGlyph::colored('◆', -1.0, 1.5, 0.7, 0.1, 0.3),
964 FrameGlyph::colored('◆', 1.0, 1.5, 0.7, 0.1, 0.3),
965 FrameGlyph::colored('△', 0.0, 0.0, 0.6, 0.1, 0.2),
966 ]);
967 SpriteAnimation::new("boss_eigenstate_idle", vec![f1, f2], 0.3, LoopMode::PingPong)
968 }
969
970 fn boss_ouroboros_idle() -> SpriteAnimation {
971 let f1 = SpriteFrame::new(vec![
972 FrameGlyph::colored('◆', 0.0, 2.0, 0.2, 0.8, 0.5).with_emission(0.4),
973 FrameGlyph::colored('~', 1.0, 1.5, 0.15, 0.6, 0.4),
974 FrameGlyph::colored('~', 1.5, 0.5, 0.15, 0.6, 0.4),
975 FrameGlyph::colored('~', 1.0, -0.5, 0.15, 0.6, 0.4),
976 FrameGlyph::colored('~', 0.0, -1.0, 0.15, 0.6, 0.4),
977 FrameGlyph::colored('~', -1.0, -0.5, 0.15, 0.6, 0.4),
978 FrameGlyph::colored('~', -1.5, 0.5, 0.15, 0.6, 0.4),
979 FrameGlyph::colored('~', -1.0, 1.5, 0.15, 0.6, 0.4),
980 ]);
981 let f2 = SpriteFrame::new(vec![
982 FrameGlyph::colored('◆', 1.0, 1.5, 0.2, 0.8, 0.5).with_emission(0.4),
983 FrameGlyph::colored('~', 1.5, 0.5, 0.15, 0.6, 0.4),
984 FrameGlyph::colored('~', 1.0, -0.5, 0.15, 0.6, 0.4),
985 FrameGlyph::colored('~', 0.0, -1.0, 0.15, 0.6, 0.4),
986 FrameGlyph::colored('~', -1.0, -0.5, 0.15, 0.6, 0.4),
987 FrameGlyph::colored('~', -1.5, 0.5, 0.15, 0.6, 0.4),
988 FrameGlyph::colored('~', -1.0, 1.5, 0.15, 0.6, 0.4),
989 FrameGlyph::colored('~', 0.0, 2.0, 0.15, 0.6, 0.4),
990 ]);
991 SpriteAnimation::new("boss_ouroboros_idle", vec![f1, f2], 0.4, LoopMode::Loop)
992 }
993
994 fn boss_algorithm_idle() -> SpriteAnimation {
995 let f1 = SpriteFrame::new(vec![
996 FrameGlyph::colored('Σ', 0.0, 3.0, 0.2, 1.0, 0.8).with_emission(0.7),
997 FrameGlyph::colored('█', 0.0, 2.0, 0.1, 0.6, 0.5),
998 FrameGlyph::colored('0', -1.5, 1.0, 0.0, 0.4, 0.3),
999 FrameGlyph::colored('1', 1.5, 1.0, 0.0, 0.4, 0.3),
1000 FrameGlyph::colored('λ', -0.5, 0.0, 0.0, 0.3, 0.25),
1001 FrameGlyph::colored('λ', 0.5, 0.0, 0.0, 0.3, 0.25),
1002 ]);
1003 let f2 = SpriteFrame::new(vec![
1004 FrameGlyph::colored('Σ', 0.0, 3.0, 0.3, 1.0, 0.9).with_emission(0.9),
1005 FrameGlyph::colored('█', 0.0, 2.0, 0.15, 0.7, 0.6),
1006 FrameGlyph::colored('1', -1.5, 1.0, 0.0, 0.5, 0.4),
1007 FrameGlyph::colored('0', 1.5, 1.0, 0.0, 0.5, 0.4),
1008 FrameGlyph::colored('λ', -0.5, 0.0, 0.0, 0.35, 0.3),
1009 FrameGlyph::colored('λ', 0.5, 0.0, 0.0, 0.35, 0.3),
1010 ]);
1011 SpriteAnimation::new("boss_algorithm_idle", vec![f1, f2], 0.5, LoopMode::PingPong)
1012 }
1013
1014 fn boss_chaos_weaver_idle() -> SpriteAnimation {
1015 let f1 = SpriteFrame::new(vec![
1016 FrameGlyph::colored('∞', 0.0, 3.0, 1.0, 0.2, 0.8).with_emission(0.8),
1017 FrameGlyph::colored('▓', 0.0, 2.0, 0.8, 0.1, 0.6),
1018 FrameGlyph::colored('~', -1.5, 1.5, 0.6, 0.1, 0.5),
1019 FrameGlyph::colored('~', 1.5, 1.5, 0.6, 0.1, 0.5),
1020 FrameGlyph::colored('▲', -0.5, 0.0, 0.5, 0.05, 0.4),
1021 FrameGlyph::colored('▲', 0.5, 0.0, 0.5, 0.05, 0.4),
1022 ]);
1023 let f2 = SpriteFrame::new(vec![
1024 FrameGlyph::colored('∞', 0.0, 3.0, 0.8, 0.3, 1.0).with_emission(1.0),
1025 FrameGlyph::colored('▓', 0.0, 2.0, 0.6, 0.2, 0.8),
1026 FrameGlyph::colored('~', -1.8, 1.2, 0.5, 0.15, 0.6),
1027 FrameGlyph::colored('~', 1.8, 1.8, 0.5, 0.15, 0.6),
1028 FrameGlyph::colored('▲', -0.5, 0.0, 0.4, 0.1, 0.5),
1029 FrameGlyph::colored('▲', 0.5, 0.0, 0.4, 0.1, 0.5),
1030 ]);
1031 SpriteAnimation::new("boss_chaos_weaver_idle", vec![f1, f2], 0.35, LoopMode::PingPong)
1032 }
1033
1034 fn boss_void_serpent_idle() -> SpriteAnimation {
1035 let f1 = SpriteFrame::new(vec![
1036 FrameGlyph::colored('◆', 0.0, 3.0, 0.1, 0.0, 0.3).with_emission(0.3),
1037 FrameGlyph::colored('S', 0.5, 2.0, 0.08, 0.0, 0.25),
1038 FrameGlyph::colored('S', -0.5, 1.0, 0.08, 0.0, 0.25),
1039 FrameGlyph::colored('S', 0.5, 0.0, 0.08, 0.0, 0.25),
1040 FrameGlyph::colored('▲', 0.0, -1.0, 0.06, 0.0, 0.2),
1041 ]);
1042 let f2 = SpriteFrame::new(vec![
1043 FrameGlyph::colored('◆', 0.3, 3.0, 0.15, 0.0, 0.4).with_emission(0.4),
1044 FrameGlyph::colored('S', -0.3, 2.0, 0.1, 0.0, 0.3),
1045 FrameGlyph::colored('S', 0.3, 1.0, 0.1, 0.0, 0.3),
1046 FrameGlyph::colored('S', -0.3, 0.0, 0.1, 0.0, 0.3),
1047 FrameGlyph::colored('▲', 0.0, -1.0, 0.08, 0.0, 0.25),
1048 ]);
1049 SpriteAnimation::new("boss_void_serpent_idle", vec![f1, f2], 0.45, LoopMode::PingPong)
1050 }
1051
1052 fn boss_prime_idle() -> SpriteAnimation {
1053 let f1 = SpriteFrame::new(vec![
1054 FrameGlyph::colored('π', 0.0, 3.0, 1.0, 0.85, 0.3).with_emission(0.5),
1055 FrameGlyph::colored('█', 0.0, 2.0, 0.8, 0.7, 0.2),
1056 FrameGlyph::colored('2', -1.5, 1.0, 0.7, 0.6, 0.15),
1057 FrameGlyph::colored('3', 1.5, 1.0, 0.7, 0.6, 0.15),
1058 FrameGlyph::colored('▲', -0.5, 0.0, 0.6, 0.5, 0.1),
1059 FrameGlyph::colored('▲', 0.5, 0.0, 0.6, 0.5, 0.1),
1060 ]);
1061 let f2 = SpriteFrame::new(vec![
1062 FrameGlyph::colored('π', 0.0, 3.0, 1.0, 0.9, 0.4).with_emission(0.7),
1063 FrameGlyph::colored('█', 0.0, 2.0, 0.85, 0.75, 0.25),
1064 FrameGlyph::colored('5', -1.5, 1.0, 0.75, 0.65, 0.2),
1065 FrameGlyph::colored('7', 1.5, 1.0, 0.75, 0.65, 0.2),
1066 FrameGlyph::colored('▲', -0.5, 0.0, 0.65, 0.55, 0.15),
1067 FrameGlyph::colored('▲', 0.5, 0.0, 0.65, 0.55, 0.15),
1068 ]);
1069 SpriteAnimation::new("boss_prime_idle", vec![f1, f2], 0.6, LoopMode::PingPong)
1070 }
1071
1072 fn boss_mirror_attack() -> SpriteAnimation {
1075 let f1 = SpriteFrame::new(vec![
1076 FrameGlyph::colored('◇', 0.0, 3.0, 1.0, 1.0, 1.0).with_emission(1.0),
1077 FrameGlyph::colored('│', 0.0, 2.0, 0.9, 0.9, 1.0),
1078 FrameGlyph::colored('>', 2.0, 2.0, 1.0, 1.0, 1.0).with_emission(0.8),
1079 ]);
1080 let f2 = SpriteFrame::new(vec![
1081 FrameGlyph::colored('◆', 0.0, 3.0, 1.0, 1.0, 1.0).with_emission(1.5),
1082 FrameGlyph::colored('─', 1.0, 2.0, 1.0, 1.0, 1.0),
1083 FrameGlyph::colored('─', 2.0, 2.0, 1.0, 1.0, 1.0),
1084 FrameGlyph::colored('★', 3.0, 2.0, 1.0, 1.0, 1.0).with_emission(1.2),
1085 ]).with_event("hit");
1086 let f3 = SpriteFrame::new(vec![
1087 FrameGlyph::colored('◇', 0.0, 3.0, 0.8, 0.9, 1.0).with_emission(0.5),
1088 FrameGlyph::colored('│', 0.0, 2.0, 0.7, 0.8, 0.9),
1089 FrameGlyph::colored('◇', -1.0, 1.0, 0.6, 0.7, 0.8),
1090 FrameGlyph::colored('◇', 1.0, 1.0, 0.6, 0.7, 0.8),
1091 ]);
1092 SpriteAnimation::new("boss_mirror_attack", vec![f1, f2, f3], 0.1, LoopMode::OnceAndDone)
1093 }
1094
1095 fn boss_null_attack() -> SpriteAnimation {
1096 let f1 = SpriteFrame::new(vec![
1097 FrameGlyph::colored('∅', 0.0, 3.0, 0.5, 0.5, 0.5).with_emission(0.8),
1098 FrameGlyph::colored('█', 0.0, 2.0, 0.2, 0.2, 0.2),
1099 ]);
1100 let f2 = SpriteFrame::new(vec![
1101 FrameGlyph::colored('∅', 0.0, 3.0, 0.1, 0.1, 0.1).with_emission(1.5),
1102 FrameGlyph::colored(' ', 0.0, 2.0, 0.0, 0.0, 0.0),
1103 FrameGlyph::colored(' ', 1.0, 2.0, 0.0, 0.0, 0.0),
1104 FrameGlyph::colored(' ', 2.0, 2.0, 0.0, 0.0, 0.0),
1105 ]).with_event("erase");
1106 let f3 = SpriteFrame::new(vec![
1107 FrameGlyph::colored('∅', 0.0, 3.0, 0.3, 0.3, 0.3).with_emission(0.3),
1108 FrameGlyph::colored('█', 0.0, 2.0, 0.1, 0.1, 0.1),
1109 FrameGlyph::colored('░', -1.0, 1.0, 0.2, 0.2, 0.2),
1110 FrameGlyph::colored('░', 1.0, 1.0, 0.2, 0.2, 0.2),
1111 ]);
1112 SpriteAnimation::new("boss_null_attack", vec![f1, f2, f3], 0.12, LoopMode::OnceAndDone)
1113 }
1114
1115 fn boss_committee_attack() -> SpriteAnimation {
1116 let f1 = SpriteFrame::new(vec![
1117 FrameGlyph::colored('☻', -2.0, 2.0, 1.0, 0.0, 0.0), FrameGlyph::colored('☻', -1.0, 2.0, 1.0, 0.0, 0.0),
1119 FrameGlyph::colored('☻', 0.0, 2.5, 1.0, 0.0, 0.0).with_emission(0.5),
1120 FrameGlyph::colored('☻', 1.0, 2.0, 0.0, 1.0, 0.0), FrameGlyph::colored('☻', 2.0, 2.0, 1.0, 0.0, 0.0),
1122 FrameGlyph::colored('═', -2.0, 1.0, 0.8, 0.2, 0.2),
1123 FrameGlyph::colored('═', 0.0, 1.0, 0.8, 0.2, 0.2),
1124 FrameGlyph::colored('═', 2.0, 1.0, 0.8, 0.2, 0.2),
1125 ]);
1126 let f2 = SpriteFrame::new(vec![
1127 FrameGlyph::colored('!', -2.0, 3.0, 1.0, 0.3, 0.2),
1128 FrameGlyph::colored('!', -1.0, 3.0, 1.0, 0.3, 0.2),
1129 FrameGlyph::colored('!', 0.0, 3.5, 1.0, 0.5, 0.3).with_emission(1.0),
1130 FrameGlyph::colored('?', 1.0, 3.0, 0.3, 1.0, 0.3),
1131 FrameGlyph::colored('!', 2.0, 3.0, 1.0, 0.3, 0.2),
1132 ]).with_event("verdict");
1133 SpriteAnimation::new("boss_committee_attack", vec![f1, f2], 0.15, LoopMode::OnceAndDone)
1134 }
1135
1136 fn boss_hydra_attack() -> SpriteAnimation {
1137 let f1 = SpriteFrame::new(vec![
1138 FrameGlyph::colored('◆', -1.5, 3.5, 0.2, 0.9, 0.3),
1139 FrameGlyph::colored('◆', 1.5, 3.5, 0.2, 0.9, 0.3),
1140 FrameGlyph::colored('>', -0.5, 3.0, 0.3, 1.0, 0.4).with_emission(0.5),
1141 FrameGlyph::colored('>', 0.5, 3.0, 0.3, 1.0, 0.4).with_emission(0.5),
1142 FrameGlyph::colored('█', 0.0, 1.0, 0.1, 0.5, 0.15),
1143 ]);
1144 let f2 = SpriteFrame::new(vec![
1145 FrameGlyph::colored('>', -0.5, 4.0, 0.4, 1.0, 0.5).with_emission(1.0),
1146 FrameGlyph::colored('>', 0.5, 4.0, 0.4, 1.0, 0.5).with_emission(1.0),
1147 FrameGlyph::colored('*', 0.0, 4.5, 0.5, 1.0, 0.6).with_emission(1.2),
1148 FrameGlyph::colored('█', 0.0, 1.0, 0.1, 0.5, 0.15),
1149 ]).with_event("bite");
1150 SpriteAnimation::new("boss_hydra_attack", vec![f1, f2], 0.12, LoopMode::OnceAndDone)
1151 }
1152
1153 fn boss_eigenstate_attack() -> SpriteAnimation {
1154 let f1 = SpriteFrame::new(vec![
1156 FrameGlyph::colored('ψ', 0.0, 3.0, 1.0, 0.5, 1.0).with_emission(1.2),
1157 FrameGlyph::colored('φ', 0.2, 3.0, 0.5, 0.2, 1.0).with_emission(0.8),
1158 ]);
1159 let f2 = SpriteFrame::new(vec![
1160 FrameGlyph::colored('Ψ', 0.0, 3.0, 1.0, 0.2, 1.0).with_emission(2.0),
1161 FrameGlyph::colored('─', 1.0, 3.0, 0.8, 0.1, 0.8),
1162 FrameGlyph::colored('─', 2.0, 3.0, 0.6, 0.1, 0.6),
1163 FrameGlyph::colored('★', 3.0, 3.0, 1.0, 0.3, 1.0).with_emission(1.5),
1164 ]).with_event("collapse");
1165 SpriteAnimation::new("boss_eigenstate_attack", vec![f1, f2], 0.12, LoopMode::OnceAndDone)
1166 }
1167
1168 fn boss_ouroboros_attack() -> SpriteAnimation {
1169 let f1 = SpriteFrame::new(vec![
1170 FrameGlyph::colored('◆', 0.0, 2.0, 0.3, 1.0, 0.6).with_emission(0.8),
1171 FrameGlyph::colored('O', 0.0, 0.5, 0.2, 0.8, 0.5).with_emission(1.0),
1172 ]);
1173 let f2 = SpriteFrame::new(vec![
1174 FrameGlyph::colored('◆', 0.0, 2.0, 0.5, 1.0, 0.8).with_emission(1.5),
1175 FrameGlyph::colored('∞', 0.0, 0.5, 0.4, 1.0, 0.7).with_emission(1.5),
1176 FrameGlyph::colored('~', 2.0, 0.5, 0.3, 0.8, 0.5),
1177 FrameGlyph::colored('~', -2.0, 0.5, 0.3, 0.8, 0.5),
1178 ]).with_event("reverse");
1179 SpriteAnimation::new("boss_ouroboros_attack", vec![f1, f2], 0.15, LoopMode::OnceAndDone)
1180 }
1181
1182 fn boss_algorithm_attack() -> SpriteAnimation {
1183 let f1 = SpriteFrame::new(vec![
1184 FrameGlyph::colored('Σ', 0.0, 3.0, 0.4, 1.0, 0.9).with_emission(1.0),
1185 FrameGlyph::colored('█', 0.0, 2.0, 0.2, 0.7, 0.6),
1186 FrameGlyph::colored('>', 1.0, 2.0, 0.3, 0.9, 0.8),
1187 ]);
1188 let f2 = SpriteFrame::new(vec![
1189 FrameGlyph::colored('Σ', 0.0, 3.0, 0.5, 1.0, 1.0).with_emission(1.5),
1190 FrameGlyph::colored('>', 1.5, 2.5, 0.4, 1.0, 0.9).with_emission(0.8),
1191 FrameGlyph::colored('>', 2.5, 2.0, 0.4, 1.0, 0.9).with_emission(0.8),
1192 FrameGlyph::colored('>', 3.5, 1.5, 0.4, 1.0, 0.9).with_emission(0.8),
1193 ]).with_event("predict");
1194 let f3 = SpriteFrame::new(vec![
1195 FrameGlyph::colored('Σ', 0.0, 3.0, 0.3, 0.8, 0.7).with_emission(0.5),
1196 FrameGlyph::colored('█', 0.0, 2.0, 0.15, 0.6, 0.5),
1197 ]);
1198 SpriteAnimation::new("boss_algorithm_attack", vec![f1, f2, f3], 0.1, LoopMode::OnceAndDone)
1199 }
1200
1201 fn boss_chaos_weaver_attack() -> SpriteAnimation {
1202 let f1 = SpriteFrame::new(vec![
1203 FrameGlyph::colored('∞', 0.0, 3.0, 1.0, 0.3, 1.0).with_emission(1.5),
1204 FrameGlyph::colored('~', -1.0, 2.0, 0.8, 0.2, 0.8),
1205 FrameGlyph::colored('~', 1.0, 2.0, 0.8, 0.2, 0.8),
1206 ]);
1207 let f2 = SpriteFrame::new(vec![
1208 FrameGlyph::colored('∞', 0.0, 3.0, 1.0, 0.5, 1.0).with_emission(2.0),
1209 FrameGlyph::colored('★', -2.0, 1.0, 1.0, 0.2, 0.8).with_emission(1.0),
1210 FrameGlyph::colored('★', 2.0, 1.0, 1.0, 0.2, 0.8).with_emission(1.0),
1211 FrameGlyph::colored('★', 0.0, -1.0, 1.0, 0.2, 0.8).with_emission(1.0),
1212 ]).with_event("warp");
1213 SpriteAnimation::new("boss_chaos_weaver_attack", vec![f1, f2], 0.12, LoopMode::OnceAndDone)
1214 }
1215
1216 fn boss_void_serpent_attack() -> SpriteAnimation {
1217 let f1 = SpriteFrame::new(vec![
1218 FrameGlyph::colored('◆', 0.0, 3.0, 0.2, 0.0, 0.5).with_emission(0.5),
1219 FrameGlyph::colored('O', 0.0, 4.0, 0.1, 0.0, 0.4).with_emission(0.8), ]);
1221 let f2 = SpriteFrame::new(vec![
1222 FrameGlyph::colored('◆', 0.0, 3.0, 0.3, 0.0, 0.6).with_emission(1.0),
1223 FrameGlyph::colored('O', 0.0, 4.5, 0.0, 0.0, 0.0).with_emission(2.0), FrameGlyph::colored('·', 1.0, 4.0, 0.1, 0.0, 0.3),
1225 FrameGlyph::colored('·', -1.0, 4.0, 0.1, 0.0, 0.3),
1226 ]).with_event("consume");
1227 SpriteAnimation::new("boss_void_serpent_attack", vec![f1, f2], 0.15, LoopMode::OnceAndDone)
1228 }
1229
1230 fn boss_prime_attack() -> SpriteAnimation {
1231 let f1 = SpriteFrame::new(vec![
1232 FrameGlyph::colored('π', 0.0, 3.0, 1.0, 0.9, 0.5).with_emission(1.0),
1233 FrameGlyph::colored('!', 0.0, 4.0, 1.0, 0.85, 0.3),
1234 ]);
1235 let f2 = SpriteFrame::new(vec![
1236 FrameGlyph::colored('π', 0.0, 3.0, 1.0, 1.0, 0.6).with_emission(1.5),
1237 FrameGlyph::colored('1', -1.0, 4.0, 1.0, 0.9, 0.4).with_emission(0.5),
1238 FrameGlyph::colored('3', 0.0, 4.5, 1.0, 0.9, 0.4).with_emission(0.5),
1239 FrameGlyph::colored('7', 1.0, 4.0, 1.0, 0.9, 0.4).with_emission(0.5),
1240 ]).with_event("calculate");
1241 SpriteAnimation::new("boss_prime_attack", vec![f1, f2], 0.12, LoopMode::OnceAndDone)
1242 }
1243
1244 pub fn player_animator() -> SpriteAnimator {
1248 let mut animator = SpriteAnimator::new();
1249 animator.add_animation(Self::player_idle());
1250 animator.add_animation(Self::player_attack());
1251 animator.add_animation(Self::player_cast());
1252 animator.add_animation(Self::player_hurt());
1253 animator.add_animation(Self::player_defend());
1254
1255 let mut sm = AnimationStateMachine::new();
1257 sm.add_state(AnimState::new("idle", "player_idle"));
1258 sm.add_state(AnimState::new("attack", "player_attack"));
1259 sm.add_state(AnimState::new("cast", "player_cast"));
1260 sm.add_state(AnimState::new("hurt", "player_hurt"));
1261 sm.add_state(AnimState::new("defend", "player_defend"));
1262
1263 sm.add_transition(AnimTransition::new("idle", "attack", AnimCondition::trigger("attack")));
1264 sm.add_transition(AnimTransition::new("idle", "cast", AnimCondition::trigger("cast")));
1265 sm.add_transition(AnimTransition::new("idle", "defend", AnimCondition::trigger("defend")));
1266 sm.add_transition(AnimTransition::new("*", "hurt", AnimCondition::trigger("hurt")));
1267 sm.add_transition(AnimTransition::new("attack", "idle", AnimCondition::AnimationDone));
1268 sm.add_transition(AnimTransition::new("cast", "idle", AnimCondition::AnimationDone));
1269 sm.add_transition(AnimTransition::new("hurt", "idle", AnimCondition::AnimationDone));
1270 sm.add_transition(AnimTransition::new("defend", "idle", AnimCondition::trigger("release_defend")));
1271
1272 sm.start("idle");
1273 animator.state_machine = Some(sm);
1274 animator.play("player_idle");
1275
1276 animator
1277 }
1278
1279 pub fn enemy_animator() -> SpriteAnimator {
1281 let mut animator = SpriteAnimator::new();
1282 animator.add_animation(Self::enemy_idle());
1283 animator.add_animation(Self::enemy_attack());
1284 animator.add_animation(Self::enemy_death());
1285
1286 let mut sm = AnimationStateMachine::new();
1287 sm.add_state(AnimState::new("idle", "enemy_idle"));
1288 sm.add_state(AnimState::new("attack", "enemy_attack"));
1289 sm.add_state(AnimState::new("death", "enemy_death"));
1290
1291 sm.add_transition(AnimTransition::new("idle", "attack", AnimCondition::trigger("attack")));
1292 sm.add_transition(AnimTransition::new("attack", "idle", AnimCondition::AnimationDone));
1293 sm.add_transition(AnimTransition::new("*", "death", AnimCondition::trigger("death")));
1294
1295 sm.start("idle");
1296 animator.state_machine = Some(sm);
1297 animator.play("enemy_idle");
1298
1299 animator
1300 }
1301
1302 pub fn boss_animator(boss_name: &str) -> SpriteAnimator {
1304 let mut animator = SpriteAnimator::new();
1305 let idle = Self::boss_idle(boss_name);
1306 let attack = Self::boss_attack(boss_name);
1307 let idle_name = idle.name.clone();
1308 let attack_name = attack.name.clone();
1309
1310 animator.add_animation(idle);
1311 animator.add_animation(attack);
1312 animator.add_animation(Self::enemy_death()); let mut sm = AnimationStateMachine::new();
1315 sm.add_state(AnimState::new("idle", &idle_name));
1316 sm.add_state(AnimState::new("attack", &attack_name));
1317 sm.add_state(AnimState::new("death", "enemy_death"));
1318
1319 sm.add_transition(AnimTransition::new("idle", "attack", AnimCondition::trigger("attack")));
1320 sm.add_transition(AnimTransition::new("attack", "idle", AnimCondition::AnimationDone));
1321 sm.add_transition(AnimTransition::new("*", "death", AnimCondition::trigger("death")));
1322
1323 sm.start("idle");
1324 animator.state_machine = Some(sm);
1325 animator.play(&idle_name);
1326
1327 animator
1328 }
1329}
1330
1331#[cfg(test)]
1334mod tests {
1335 use super::*;
1336
1337 #[test]
1338 fn frame_from_ascii() {
1339 let frame = SpriteFrame::from_ascii("AB\nCD", Vec4::ONE);
1340 assert_eq!(frame.glyphs.len(), 4);
1341 }
1342
1343 #[test]
1344 fn loop_mode_cycles() {
1345 let mut animator = SpriteAnimator::new();
1346 animator.add_animation(SpriteAnimation::new(
1347 "test",
1348 vec![
1349 SpriteFrame::new(vec![FrameGlyph::white('A', 0.0, 0.0)]),
1350 SpriteFrame::new(vec![FrameGlyph::white('B', 0.0, 0.0)]),
1351 SpriteFrame::new(vec![FrameGlyph::white('C', 0.0, 0.0)]),
1352 ],
1353 0.1,
1354 LoopMode::Loop,
1355 ));
1356 animator.play("test");
1357
1358 animator.tick(0.1);
1360 assert_eq!(animator.current_frame_index(), 1);
1361 animator.tick(0.1);
1362 assert_eq!(animator.current_frame_index(), 2);
1363 animator.tick(0.1);
1364 assert_eq!(animator.current_frame_index(), 0); }
1366
1367 #[test]
1368 fn once_mode_stops() {
1369 let mut animator = SpriteAnimator::new();
1370 animator.add_animation(SpriteAnimation::new(
1371 "test",
1372 vec![
1373 SpriteFrame::new(vec![FrameGlyph::white('A', 0.0, 0.0)]),
1374 SpriteFrame::new(vec![FrameGlyph::white('B', 0.0, 0.0)]),
1375 ],
1376 0.1,
1377 LoopMode::Once,
1378 ));
1379 animator.play("test");
1380 animator.tick(0.1); animator.tick(0.1); assert!(animator.is_finished());
1383 assert!(!animator.is_playing());
1384 }
1385
1386 #[test]
1387 fn frame_events_fire() {
1388 let mut animator = SpriteAnimator::new();
1389 animator.add_animation(SpriteAnimation::new(
1390 "test",
1391 vec![
1392 SpriteFrame::new(vec![FrameGlyph::white('A', 0.0, 0.0)]).with_event("start"),
1393 SpriteFrame::new(vec![FrameGlyph::white('B', 0.0, 0.0)]).with_event("hit"),
1394 ],
1395 0.1,
1396 LoopMode::Once,
1397 ));
1398 animator.play("test");
1399 animator.tick(0.1); let events = animator.drain_events();
1401 assert_eq!(events.len(), 1);
1402 assert_eq!(events[0].tag, "start");
1403 }
1404
1405 #[test]
1406 fn player_animator_builds() {
1407 let animator = AnimationLibrary::player_animator();
1408 assert!(animator.animations.contains_key("player_idle"));
1409 assert!(animator.animations.contains_key("player_attack"));
1410 assert!(animator.animations.contains_key("player_cast"));
1411 assert!(animator.animations.contains_key("player_hurt"));
1412 assert!(animator.animations.contains_key("player_defend"));
1413 assert!(animator.is_playing());
1414 }
1415
1416 #[test]
1417 fn enemy_animator_builds() {
1418 let animator = AnimationLibrary::enemy_animator();
1419 assert!(animator.animations.contains_key("enemy_idle"));
1420 assert!(animator.animations.contains_key("enemy_attack"));
1421 assert!(animator.animations.contains_key("enemy_death"));
1422 }
1423
1424 #[test]
1425 fn all_boss_animators_build() {
1426 let bosses = [
1427 "Mirror", "Null", "Committee", "FibonacciHydra", "Eigenstate",
1428 "Ouroboros", "AlgorithmReborn", "ChaosWeaver", "VoidSerpent", "PrimeFactorial",
1429 ];
1430 for boss in &bosses {
1431 let animator = AnimationLibrary::boss_animator(boss);
1432 assert!(animator.animations.len() >= 3, "Boss {} has <3 animations", boss);
1433 }
1434 }
1435
1436 #[test]
1437 fn state_machine_transitions() {
1438 let mut sm = AnimationStateMachine::new();
1439 sm.add_state(AnimState::new("idle", "idle_anim"));
1440 sm.add_state(AnimState::new("attack", "attack_anim"));
1441 sm.add_transition(AnimTransition::new("idle", "attack", AnimCondition::trigger("attack")));
1442 sm.add_transition(AnimTransition::new("attack", "idle", AnimCondition::AnimationDone));
1443 sm.start("idle");
1444
1445 assert!(sm.evaluate(false).is_none());
1447
1448 sm.set_trigger("attack");
1450 let result = sm.evaluate(false);
1451 assert_eq!(result, Some("attack_anim".to_string()));
1452 assert_eq!(sm.current_state.as_deref(), Some("attack"));
1453
1454 assert!(!sm.triggers.get("attack").copied().unwrap_or(false));
1456
1457 let result = sm.evaluate(true);
1459 assert_eq!(result, Some("idle_anim".to_string()));
1460 }
1461
1462 #[test]
1463 fn animation_duration() {
1464 let anim = SpriteAnimation::new(
1465 "test",
1466 vec![
1467 SpriteFrame::new(vec![]).with_duration(0.2),
1468 SpriteFrame::new(vec![]).with_duration(0.3),
1469 SpriteFrame::new(vec![]),
1470 ],
1471 0.1,
1472 LoopMode::Loop,
1473 );
1474 assert!((anim.total_duration() - 0.6).abs() < 1e-6);
1476 }
1477
1478 #[test]
1479 fn pingpong_mode() {
1480 let mut animator = SpriteAnimator::new();
1481 animator.add_animation(SpriteAnimation::new(
1482 "pp",
1483 vec![
1484 SpriteFrame::new(vec![FrameGlyph::white('A', 0.0, 0.0)]),
1485 SpriteFrame::new(vec![FrameGlyph::white('B', 0.0, 0.0)]),
1486 SpriteFrame::new(vec![FrameGlyph::white('C', 0.0, 0.0)]),
1487 ],
1488 0.1,
1489 LoopMode::PingPong,
1490 ));
1491 animator.play("pp");
1492
1493 animator.tick(0.1);
1495 assert_eq!(animator.current_frame_index(), 1);
1496 animator.tick(0.1);
1497 assert_eq!(animator.current_frame_index(), 2);
1498 animator.tick(0.1);
1500 assert_eq!(animator.current_frame_index(), 1);
1501 animator.tick(0.1);
1503 assert_eq!(animator.current_frame_index(), 0);
1504 animator.tick(0.1);
1506 assert_eq!(animator.current_frame_index(), 1);
1507 }
1508}