1use std::collections::HashMap;
4use super::Element;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub enum ResourceType {
10 Mana,
11 Rage,
12 Energy,
13 Focus,
14 Entropy,
15 Charges,
16}
17
18impl ResourceType {
19 pub fn name(self) -> &'static str {
20 match self {
21 ResourceType::Mana => "Mana",
22 ResourceType::Rage => "Rage",
23 ResourceType::Energy => "Energy",
24 ResourceType::Focus => "Focus",
25 ResourceType::Entropy => "Entropy",
26 ResourceType::Charges => "Charges",
27 }
28 }
29
30 pub fn color(self) -> glam::Vec4 {
31 match self {
32 ResourceType::Mana => glam::Vec4::new(0.20, 0.40, 1.00, 1.0),
33 ResourceType::Rage => glam::Vec4::new(1.00, 0.15, 0.10, 1.0),
34 ResourceType::Energy => glam::Vec4::new(1.00, 0.90, 0.10, 1.0),
35 ResourceType::Focus => glam::Vec4::new(0.30, 0.90, 0.60, 1.0),
36 ResourceType::Entropy => glam::Vec4::new(0.60, 0.10, 0.80, 1.0),
37 ResourceType::Charges => glam::Vec4::new(0.90, 0.90, 0.90, 1.0),
38 }
39 }
40
41 pub fn regenerates(self) -> bool {
43 matches!(self, ResourceType::Mana | ResourceType::Energy | ResourceType::Focus)
44 }
45
46 pub fn decays_out_of_combat(self) -> bool {
48 matches!(self, ResourceType::Rage | ResourceType::Entropy)
49 }
50}
51
52#[derive(Debug, Clone)]
55pub struct ResourcePool {
56 pub kind: ResourceType,
57 pub current: f32,
58 pub maximum: f32,
59 pub regen: f32, pub decay: f32, }
62
63impl ResourcePool {
64 pub fn new(kind: ResourceType, max: f32) -> Self {
65 let regen = match kind {
66 ResourceType::Mana => max * 0.05,
67 ResourceType::Energy => max * 0.15,
68 ResourceType::Focus => max * 0.08,
69 _ => 0.0,
70 };
71 Self { kind, current: max, maximum: max, regen, decay: max * 0.10 }
72 }
73
74 pub fn update(&mut self, dt: f32, in_combat: bool) {
75 if self.kind.regenerates() && !in_combat {
76 self.current = (self.current + self.regen * dt).min(self.maximum);
77 }
78 if self.kind.decays_out_of_combat() && !in_combat {
79 self.current = (self.current - self.decay * dt).max(0.0);
80 }
81 }
82
83 pub fn spend(&mut self, amount: f32) -> bool {
84 if self.current >= amount {
85 self.current -= amount;
86 true
87 } else {
88 false
89 }
90 }
91
92 pub fn restore(&mut self, amount: f32) {
93 self.current = (self.current + amount).min(self.maximum);
94 }
95
96 pub fn fill(&mut self) { self.current = self.maximum; }
97 pub fn empty(&mut self) { self.current = 0.0; }
98
99 pub fn percent(&self) -> f32 {
100 if self.maximum > 0.0 { self.current / self.maximum } else { 0.0 }
101 }
102
103 pub fn is_empty(&self) -> bool { self.current <= 0.0 }
104 pub fn is_full(&self) -> bool { self.current >= self.maximum }
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
110pub enum AbilityTag {
111 Attack,
112 Spell,
113 Movement,
114 Channel,
115 Toggle,
116 Passive,
117 Summon,
118 Utility,
119 Defensive,
120 Ultimate,
121 AoE,
122 SingleTarget,
123 Projectile,
124 Melee,
125 Ranged,
126 Instant,
127 Delayed,
128}
129
130#[derive(Debug, Clone)]
133pub enum AbilityEffect {
134 Damage { base: f32, element: Element, scaling: f32 },
136 Heal { amount: f32, scaling: f32 },
138 ApplyStatus { name: String, duration: f32, stacks: u32 },
140 Teleport { range: f32 },
142 Knockback { force: f32, direction_from_caster: bool },
144 Pull { force: f32 },
146 Stun { duration: f32 },
148 DotDamage { dps: f32, element: Element, duration: f32 },
150 Shield { amount: f32, duration: f32 },
152 Chain { max_jumps: u32, falloff: f32 },
154 Explosion { radius: f32, damage: f32, element: Element },
156 Summon { entity_id: String, duration: f32 },
158 ModifyResource { kind: ResourceType, amount: f32 },
160 StatBuff { stat_name: String, multiplier: f32, duration: f32 },
162}
163
164#[derive(Debug, Clone)]
167pub struct Ability {
168 pub id: u32,
169 pub name: String,
170 pub description: String,
171 pub tags: Vec<AbilityTag>,
172 pub effects: Vec<AbilityEffect>,
173 pub cooldown: f32,
174 pub cast_time: f32,
175 pub channel_time: f32,
176 pub resource_cost: Vec<(ResourceType, f32)>,
177 pub range: f32,
178 pub radius: f32,
179 pub level: u32,
180 pub max_level: u32,
181 pub glyph: char,
182 pub rank_bonuses: Vec<RankBonus>,
183 pub combo_points_generated: u32,
184 pub combo_points_consumed: Option<u32>,
185 pub interrupt_flags: InterruptFlags,
186}
187
188#[derive(Debug, Clone)]
189pub struct RankBonus {
190 pub rank: u32,
191 pub description: String,
192 pub effect_modifier: f32,
193}
194
195#[derive(Debug, Clone, Copy, Default)]
196pub struct InterruptFlags {
197 pub interrupted_by_damage: bool,
198 pub interrupted_by_cc: bool,
199 pub interrupted_by_movement: bool,
200}
201
202impl Ability {
203 pub fn new(id: u32, name: impl Into<String>) -> Self {
204 Self {
205 id,
206 name: name.into(),
207 description: String::new(),
208 tags: Vec::new(),
209 effects: Vec::new(),
210 cooldown: 0.0,
211 cast_time: 0.0,
212 channel_time: 0.0,
213 resource_cost: Vec::new(),
214 range: 1.0,
215 radius: 0.0,
216 level: 1,
217 max_level: 5,
218 glyph: '◆',
219 rank_bonuses: Vec::new(),
220 combo_points_generated: 0,
221 combo_points_consumed: None,
222 interrupt_flags: InterruptFlags::default(),
223 }
224 }
225
226 pub fn with_description(mut self, d: impl Into<String>) -> Self { self.description = d.into(); self }
227 pub fn with_cooldown(mut self, cd: f32) -> Self { self.cooldown = cd; self }
228 pub fn with_cast_time(mut self, ct: f32) -> Self { self.cast_time = ct; self }
229 pub fn with_cost(mut self, resource: ResourceType, amount: f32) -> Self {
230 self.resource_cost.push((resource, amount)); self
231 }
232 pub fn with_range(mut self, r: f32) -> Self { self.range = r; self }
233 pub fn with_radius(mut self, r: f32) -> Self { self.radius = r; self }
234 pub fn with_tag(mut self, tag: AbilityTag) -> Self { self.tags.push(tag); self }
235 pub fn with_effect(mut self, eff: AbilityEffect) -> Self { self.effects.push(eff); self }
236 pub fn with_glyph(mut self, g: char) -> Self { self.glyph = g; self }
237
238 pub fn has_tag(&self, tag: AbilityTag) -> bool { self.tags.contains(&tag) }
239
240 pub fn is_instant(&self) -> bool { self.cast_time <= 0.0 && self.channel_time <= 0.0 }
241
242 pub fn scaled_damage(&self, base_attack: f32) -> f32 {
243 let level_mult = 1.0 + (self.level as f32 - 1.0) * 0.15;
244 self.effects.iter()
245 .filter_map(|e| match e {
246 AbilityEffect::Damage { base, scaling, .. } => Some(base + scaling * base_attack),
247 _ => None,
248 })
249 .sum::<f32>() * level_mult
250 }
251
252 pub fn tooltip(&self) -> Vec<String> {
253 let mut lines = vec![
254 format!("{} (Rank {})", self.name, self.level),
255 self.description.clone(),
256 format!("Cooldown: {:.1}s | Range: {:.1}", self.cooldown, self.range),
257 ];
258 if self.cast_time > 0.0 {
259 lines.push(format!("Cast time: {:.1}s", self.cast_time));
260 }
261 for (res, cost) in &self.resource_cost {
262 lines.push(format!("Cost: {:.0} {}", cost, res.name()));
263 }
264 for tag in &self.tags {
265 lines.push(format!("[{:?}]", tag));
266 }
267 lines
268 }
269
270 pub fn fireball() -> Self {
273 Ability::new(1, "Fireball")
274 .with_description("Hurls a sphere of entropic fire at the target.")
275 .with_cooldown(3.0)
276 .with_cast_time(1.0)
277 .with_cost(ResourceType::Mana, 40.0)
278 .with_range(12.0)
279 .with_radius(3.0)
280 .with_tag(AbilityTag::Spell)
281 .with_tag(AbilityTag::AoE)
282 .with_tag(AbilityTag::Projectile)
283 .with_effect(AbilityEffect::Explosion {
284 radius: 3.0,
285 damage: 80.0,
286 element: Element::Fire,
287 })
288 .with_glyph('♨')
289 }
290
291 pub fn blink() -> Self {
292 Ability::new(2, "Blink")
293 .with_description("Instantly teleport a short distance.")
294 .with_cooldown(8.0)
295 .with_cost(ResourceType::Mana, 25.0)
296 .with_range(8.0)
297 .with_tag(AbilityTag::Movement)
298 .with_tag(AbilityTag::Instant)
299 .with_effect(AbilityEffect::Teleport { range: 8.0 })
300 .with_glyph('⟿')
301 }
302
303 pub fn void_strike() -> Self {
304 Ability::new(3, "Void Strike")
305 .with_description("A heavy melee strike that tears through dimensional barriers.")
306 .with_cooldown(6.0)
307 .with_cast_time(0.3)
308 .with_cost(ResourceType::Rage, 30.0)
309 .with_range(2.0)
310 .with_tag(AbilityTag::Attack)
311 .with_tag(AbilityTag::Melee)
312 .with_tag(AbilityTag::SingleTarget)
313 .with_effect(AbilityEffect::Damage { base: 120.0, element: Element::Void, scaling: 1.8 })
314 .with_effect(AbilityEffect::ApplyStatus { name: "Void Shred".to_string(), duration: 4.0, stacks: 1 })
315 .with_glyph('◈')
316 }
317
318 pub fn temporal_freeze() -> Self {
319 Ability::new(4, "Temporal Freeze")
320 .with_description("Slows time around the target, stunnning them briefly.")
321 .with_cooldown(12.0)
322 .with_cast_time(0.5)
323 .with_cost(ResourceType::Mana, 60.0)
324 .with_range(10.0)
325 .with_tag(AbilityTag::Spell)
326 .with_tag(AbilityTag::SingleTarget)
327 .with_effect(AbilityEffect::Stun { duration: 2.5 })
328 .with_effect(AbilityEffect::DotDamage { dps: 20.0, element: Element::Temporal, duration: 3.0 })
329 .with_glyph('⧗')
330 }
331
332 pub fn entropy_cascade() -> Self {
333 Ability::new(5, "Entropy Cascade")
334 .with_description("Unleash a wave of pure entropy that chains between enemies.")
335 .with_cooldown(20.0)
336 .with_cast_time(1.5)
337 .with_cost(ResourceType::Entropy, 80.0)
338 .with_range(15.0)
339 .with_tag(AbilityTag::Spell)
340 .with_tag(AbilityTag::AoE)
341 .with_tag(AbilityTag::Ultimate)
342 .with_effect(AbilityEffect::Damage { base: 200.0, element: Element::Entropy, scaling: 2.5 })
343 .with_effect(AbilityEffect::Chain { max_jumps: 5, falloff: 0.15 })
344 .with_glyph('∞')
345 }
346
347 pub fn iron_skin() -> Self {
348 Ability::new(6, "Iron Skin")
349 .with_description("Harden your body, gaining a protective shield.")
350 .with_cooldown(15.0)
351 .with_tag(AbilityTag::Defensive)
352 .with_tag(AbilityTag::Instant)
353 .with_effect(AbilityEffect::Shield { amount: 150.0, duration: 8.0 })
354 .with_effect(AbilityEffect::StatBuff { stat_name: "armor".to_string(), multiplier: 1.5, duration: 8.0 })
355 .with_glyph('⚙')
356 }
357}
358
359#[derive(Debug, Clone)]
362pub struct AbilityState {
363 pub ability: Ability,
364 pub cooldown_remaining: f32,
365 pub is_casting: bool,
366 pub cast_progress: f32,
367 pub is_channeling: bool,
368 pub channel_elapsed: f32,
369 pub is_on_gcd: bool, }
371
372impl AbilityState {
373 pub fn new(ability: Ability) -> Self {
374 Self {
375 ability,
376 cooldown_remaining: 0.0,
377 is_casting: false,
378 cast_progress: 0.0,
379 is_channeling: false,
380 channel_elapsed: 0.0,
381 is_on_gcd: false,
382 }
383 }
384
385 pub fn update(&mut self, dt: f32) {
386 if self.cooldown_remaining > 0.0 {
387 self.cooldown_remaining = (self.cooldown_remaining - dt).max(0.0);
388 }
389 if self.is_casting {
390 self.cast_progress += dt;
391 if self.cast_progress >= self.ability.cast_time {
392 self.is_casting = false;
393 self.cast_progress = 0.0;
394 }
395 }
396 if self.is_channeling {
397 self.channel_elapsed += dt;
398 if self.channel_elapsed >= self.ability.channel_time {
399 self.is_channeling = false;
400 self.channel_elapsed = 0.0;
401 }
402 }
403 }
404
405 pub fn is_ready(&self) -> bool {
406 self.cooldown_remaining <= 0.0 && !self.is_casting && !self.is_channeling && !self.is_on_gcd
407 }
408
409 pub fn trigger(&mut self) {
410 self.cooldown_remaining = self.ability.cooldown;
411 if self.ability.cast_time > 0.0 {
412 self.is_casting = true;
413 self.cast_progress = 0.0;
414 }
415 }
416
417 pub fn interrupt(&mut self) {
418 if self.ability.interrupt_flags.interrupted_by_damage {
419 self.is_casting = false;
420 self.cast_progress = 0.0;
421 self.is_channeling = false;
422 self.channel_elapsed = 0.0;
423 }
424 }
425
426 pub fn cast_percent(&self) -> f32 {
427 if self.ability.cast_time > 0.0 {
428 (self.cast_progress / self.ability.cast_time).min(1.0)
429 } else { 0.0 }
430 }
431
432 pub fn cooldown_percent(&self) -> f32 {
433 if self.ability.cooldown > 0.0 {
434 1.0 - (self.cooldown_remaining / self.ability.cooldown).min(1.0)
435 } else { 1.0 }
436 }
437}
438
439pub const MAX_ABILITY_SLOTS: usize = 12;
442
443#[derive(Debug, Clone)]
444pub struct AbilityBar {
445 pub slots: Vec<Option<AbilityState>>,
446 pub resources: HashMap<ResourceType, ResourcePool>,
447 pub global_cooldown: f32,
448 pub gcd_remaining: f32,
449 pub combo_points: u32,
450 pub max_combo_points: u32,
451 pub in_combat: bool,
452 pub combat_timer: f32,
453}
454
455impl AbilityBar {
456 pub fn new() -> Self {
457 Self {
458 slots: vec![None; MAX_ABILITY_SLOTS],
459 resources: HashMap::new(),
460 global_cooldown: 1.5,
461 gcd_remaining: 0.0,
462 combo_points: 0,
463 max_combo_points: 5,
464 in_combat: false,
465 combat_timer: 0.0,
466 }
467 }
468
469 pub fn add_resource(&mut self, kind: ResourceType, max: f32) {
470 self.resources.insert(kind, ResourcePool::new(kind, max));
471 }
472
473 pub fn assign(&mut self, slot: usize, ability: Ability) {
474 if slot < MAX_ABILITY_SLOTS {
475 self.slots[slot] = Some(AbilityState::new(ability));
476 }
477 }
478
479 pub fn unassign(&mut self, slot: usize) {
480 if slot < MAX_ABILITY_SLOTS {
481 self.slots[slot] = None;
482 }
483 }
484
485 pub fn update(&mut self, dt: f32) {
486 if self.gcd_remaining > 0.0 {
488 self.gcd_remaining = (self.gcd_remaining - dt).max(0.0);
489 for slot in self.slots.iter_mut().flatten() {
490 slot.is_on_gcd = self.gcd_remaining > 0.0;
491 }
492 }
493
494 for slot in self.slots.iter_mut().flatten() {
496 slot.update(dt);
497 }
498
499 for pool in self.resources.values_mut() {
501 pool.update(dt, self.in_combat);
502 }
503
504 if self.in_combat {
506 self.combat_timer -= dt;
507 if self.combat_timer <= 0.0 {
508 self.in_combat = false;
509 }
510 }
511 }
512
513 pub fn can_use(&self, slot: usize) -> Result<(), AbilityCastError> {
514 let state = self.slots.get(slot)
515 .and_then(|s| s.as_ref())
516 .ok_or(AbilityCastError::NoAbilityInSlot)?;
517
518 if !state.is_ready() {
519 return Err(if state.cooldown_remaining > 0.0 {
520 AbilityCastError::OnCooldown { remaining: state.cooldown_remaining }
521 } else {
522 AbilityCastError::AlreadyCasting
523 });
524 }
525
526 for (res_type, cost) in &state.ability.resource_cost {
527 let pool = self.resources.get(res_type)
528 .ok_or(AbilityCastError::NotEnoughResource(*res_type))?;
529 if pool.current < *cost {
530 return Err(AbilityCastError::NotEnoughResource(*res_type));
531 }
532 }
533
534 if let Some(req) = state.ability.combo_points_consumed {
535 if self.combo_points < req {
536 return Err(AbilityCastError::NotEnoughComboPoints { need: req, have: self.combo_points });
537 }
538 }
539
540 Ok(())
541 }
542
543 pub fn use_ability(&mut self, slot: usize) -> Result<AbilityUseResult, AbilityCastError> {
544 self.can_use(slot)?;
545
546 let state = self.slots[slot].as_mut().unwrap();
547 let ability_clone = state.ability.clone();
548 state.trigger();
549
550 for (res_type, cost) in &ability_clone.resource_cost {
552 if let Some(pool) = self.resources.get_mut(res_type) {
553 pool.spend(*cost);
554 }
555 }
556
557 if let Some(req) = ability_clone.combo_points_consumed {
559 self.combo_points = self.combo_points.saturating_sub(req);
560 }
561
562 self.combo_points = (self.combo_points + ability_clone.combo_points_generated)
564 .min(self.max_combo_points);
565
566 if !ability_clone.is_instant() {
568 self.gcd_remaining = self.global_cooldown;
569 }
570
571 self.in_combat = true;
573 self.combat_timer = 5.0;
574
575 Ok(AbilityUseResult {
576 ability: ability_clone,
577 instant: self.slots[slot].as_ref().unwrap().ability.is_instant(),
578 })
579 }
580
581 pub fn interrupt_all(&mut self) {
582 for slot in self.slots.iter_mut().flatten() {
583 slot.interrupt();
584 }
585 }
586
587 pub fn get_slot(&self, slot: usize) -> Option<&AbilityState> {
588 self.slots.get(slot)?.as_ref()
589 }
590
591 pub fn resource(&self, kind: ResourceType) -> Option<&ResourcePool> {
592 self.resources.get(&kind)
593 }
594
595 pub fn all_on_cooldown(&self) -> bool {
596 self.slots.iter().flatten().all(|s| !s.is_ready())
597 }
598}
599
600#[derive(Debug, Clone)]
601pub struct AbilityUseResult {
602 pub ability: Ability,
603 pub instant: bool,
604}
605
606#[derive(Debug, Clone)]
607pub enum AbilityCastError {
608 NoAbilityInSlot,
609 OnCooldown { remaining: f32 },
610 AlreadyCasting,
611 NotEnoughResource(ResourceType),
612 NotEnoughComboPoints { need: u32, have: u32 },
613 OutOfRange,
614 InvalidTarget,
615}
616
617impl std::fmt::Display for AbilityCastError {
618 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
619 match self {
620 Self::NoAbilityInSlot => write!(f, "No ability in that slot"),
621 Self::OnCooldown { remaining } => write!(f, "On cooldown ({:.1}s)", remaining),
622 Self::AlreadyCasting => write!(f, "Already casting"),
623 Self::NotEnoughResource(r) => write!(f, "Not enough {}", r.name()),
624 Self::NotEnoughComboPoints { need, have } => write!(f, "Need {} combo points, have {}", need, have),
625 Self::OutOfRange => write!(f, "Target is out of range"),
626 Self::InvalidTarget => write!(f, "Invalid target"),
627 }
628 }
629}
630
631#[derive(Debug, Clone)]
634pub struct AbilityNode {
635 pub id: u32,
636 pub ability: Ability,
637 pub required_points: u32,
638 pub prerequisites: Vec<u32>, pub position: (f32, f32), pub unlocked: bool,
641}
642
643#[derive(Debug, Clone)]
644pub struct AbilityTree {
645 pub name: String,
646 pub nodes: Vec<AbilityNode>,
647 pub spent_points: u32,
648}
649
650impl AbilityTree {
651 pub fn new(name: impl Into<String>) -> Self {
652 Self { name: name.into(), nodes: Vec::new(), spent_points: 0 }
653 }
654
655 pub fn add_node(&mut self, ability: Ability, required_points: u32, prereqs: Vec<u32>, pos: (f32, f32)) -> u32 {
656 let id = self.nodes.len() as u32;
657 self.nodes.push(AbilityNode {
658 id,
659 ability,
660 required_points,
661 prerequisites: prereqs,
662 position: pos,
663 unlocked: false,
664 });
665 id
666 }
667
668 pub fn can_unlock(&self, node_id: u32, available_points: u32) -> bool {
669 if let Some(node) = self.nodes.iter().find(|n| n.id == node_id) {
670 if node.unlocked { return false; }
671 if available_points < node.required_points + self.spent_points { return false; }
672 node.prerequisites.iter().all(|&prereq_id| {
673 self.nodes.iter().find(|n| n.id == prereq_id).map(|n| n.unlocked).unwrap_or(false)
674 })
675 } else {
676 false
677 }
678 }
679
680 pub fn unlock(&mut self, node_id: u32, available_points: u32) -> bool {
681 if !self.can_unlock(node_id, available_points) { return false; }
682 if let Some(node) = self.nodes.iter_mut().find(|n| n.id == node_id) {
683 node.unlocked = true;
684 self.spent_points += node.required_points;
685 true
686 } else {
687 false
688 }
689 }
690
691 pub fn unlocked_abilities(&self) -> Vec<&Ability> {
692 self.nodes.iter().filter(|n| n.unlocked).map(|n| &n.ability).collect()
693 }
694
695 pub fn available_nodes(&self, available_points: u32) -> Vec<u32> {
696 self.nodes.iter()
697 .filter(|n| !n.unlocked && self.can_unlock(n.id, available_points))
698 .map(|n| n.id)
699 .collect()
700 }
701}
702
703#[cfg(test)]
706mod tests {
707 use super::*;
708
709 #[test]
710 fn test_resource_pool_spend() {
711 let mut pool = ResourcePool::new(ResourceType::Mana, 100.0);
712 assert!(pool.spend(40.0));
713 assert!((pool.current - 60.0).abs() < 0.01);
714 assert!(!pool.spend(80.0)); }
716
717 #[test]
718 fn test_ability_bar_use() {
719 let mut bar = AbilityBar::new();
720 bar.add_resource(ResourceType::Mana, 200.0);
721 bar.assign(0, Ability::fireball());
722 assert!(bar.can_use(0).is_ok());
723 let result = bar.use_ability(0);
724 assert!(result.is_ok());
725 assert!(bar.can_use(0).is_err());
727 }
728
729 #[test]
730 fn test_ability_tree_unlock() {
731 let mut tree = AbilityTree::new("Mage");
732 let root = tree.add_node(Ability::fireball(), 1, vec![], (0.0, 0.0));
733 let branch = tree.add_node(Ability::blink(), 2, vec![root], (1.0, 0.0));
734
735 assert!(tree.unlock(root, 5));
736 assert!(!tree.unlock(branch, 2)); assert!(tree.unlock(branch, 10));
738 assert_eq!(tree.unlocked_abilities().len(), 2);
739 }
740
741 #[test]
742 fn test_cooldown_tracking() {
743 let mut state = AbilityState::new(Ability::fireball());
744 assert!(state.is_ready());
745 state.trigger();
746 assert!(!state.is_ready());
747 state.update(10.0); assert!(state.is_ready());
749 }
750}