Skip to main content

steel_core/entity/entities/objects/projectiles/
firework_rocket.rs

1//! Firework rocket projectile entity (`FireworkRocketEntity`).
2//!
3//! The server owns rocket movement, collision, Elytra boosting, lifetime,
4//! explosion damage, and entity-event dispatch. Firework trail and explosion
5//! particles are created by the client from synced rocket data and event 17.
6
7use std::sync::{Arc, Weak};
8
9use glam::DVec3;
10use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
11use simdnbt::owned::NbtCompound;
12use steel_macros::entity_behavior;
13use steel_protocol::packets::game::SoundSource;
14use steel_registry::blocks::block_state_ext::BlockStateExt as _;
15use steel_registry::data_components::vanilla_components::FIREWORKS;
16use steel_registry::entity_type::EntityTypeRef;
17use steel_registry::item_stack::ItemStack;
18use steel_registry::vanilla_entity_data::FireworkRocketEntityData;
19use steel_registry::{sound_events, vanilla_damage_types, vanilla_game_events, vanilla_items};
20use steel_utils::entity_events::EntityStatus;
21use steel_utils::locks::SyncMutex;
22use steel_utils::{DowncastType, DowncastTypeKey};
23
24use crate::behavior::BLOCK_BEHAVIORS;
25use crate::entity::damage::DamageSource;
26use crate::entity::{
27    Entity, EntityBase, EntityBaseLoad, EntityEventSource, EntitySyncedData,
28    InsideBlockEffectCollector, LivingEntity, Projectile, ProjectileBase, ProjectileHit,
29    RemovalReason, SharedEntity,
30};
31use crate::physics::MoverType;
32use crate::world::{ClipBlockShape, ClipFluid, ClipHitResult, World};
33
34const INITIAL_VERTICAL_VELOCITY: f64 = 0.05;
35const INITIAL_HORIZONTAL_DEVIATION: f64 = 0.002_297;
36const HORIZONTAL_ACCELERATION: f64 = 1.15;
37const VERTICAL_ACCELERATION: f64 = 0.04;
38const ELYTRA_TARGET_SPEED: f64 = 1.5;
39const ELYTRA_POWER_ADD: f64 = 0.1;
40const ELYTRA_VELOCITY_BLEND: f64 = 0.5;
41const EXPLOSION_RADIUS: f64 = 5.0;
42const EXPLOSION_RADIUS_SQUARED: f64 = EXPLOSION_RADIUS * EXPLOSION_RADIUS;
43
44struct FireworkRocketState {
45    life: i32,
46    lifetime: i32,
47    attached_to_entity: Option<Weak<dyn Entity>>,
48}
49
50/// A launched firework rocket.
51#[entity_behavior(class = "FireworkRocketEntity")]
52pub struct FireworkRocketEntity {
53    base: EntityBase,
54    entity_type: EntityTypeRef,
55    entity_data: SyncMutex<FireworkRocketEntityData>,
56    projectile_base: ProjectileBase,
57    state: SyncMutex<FireworkRocketState>,
58}
59
60// SAFETY: This key is owned by Steel and uniquely identifies `FireworkRocketEntity`.
61unsafe impl DowncastType for FireworkRocketEntity {
62    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/firework_rocket");
63}
64
65impl FireworkRocketEntity {
66    /// Creates an uninitialized rocket for the entity factory.
67    #[must_use]
68    pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
69        Self {
70            base: EntityBase::new(id, position, entity_type.dimensions, world),
71            entity_type,
72            entity_data: SyncMutex::new(FireworkRocketEntityData::new()),
73            projectile_base: ProjectileBase::new(),
74            state: SyncMutex::new(FireworkRocketState {
75                life: 0,
76                lifetime: 0,
77                attached_to_entity: None,
78            }),
79        }
80    }
81
82    /// Creates a rocket from saved base data.
83    #[must_use]
84    pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
85        Self {
86            base: EntityBase::from_load(load, entity_type.dimensions),
87            entity_type,
88            entity_data: SyncMutex::new(FireworkRocketEntityData::new()),
89            projectile_base: ProjectileBase::new(),
90            state: SyncMutex::new(FireworkRocketState {
91                life: 0,
92                lifetime: 0,
93                attached_to_entity: None,
94            }),
95        }
96    }
97
98    /// Creates a normally launched rocket at an exact position.
99    #[must_use]
100    pub fn launched(
101        entity_type: EntityTypeRef,
102        id: i32,
103        position: DVec3,
104        world: Weak<World>,
105        source_item: ItemStack,
106    ) -> Self {
107        let rocket = Self::new(entity_type, id, position, world);
108        rocket.initialize_launch(source_item);
109        rocket
110    }
111
112    /// Creates a rocket attached to a living entity that will be resolved from
113    /// its synced runtime ID after the rocket enters the world.
114    #[must_use]
115    pub fn attached_to_living(
116        entity_type: EntityTypeRef,
117        id: i32,
118        world: Weak<World>,
119        source_item: ItemStack,
120        attached_to: &dyn LivingEntity,
121    ) -> Self {
122        let rocket = Self::launched(entity_type, id, attached_to.position(), world, source_item);
123        rocket.set_owner_uuid(Some(attached_to.uuid()));
124        if let Ok(attached_id) = u32::try_from(attached_to.id()) {
125            rocket
126                .entity_data
127                .lock()
128                .attached_to_target
129                .set(Some(attached_id));
130        }
131        rocket
132    }
133
134    fn initialize_launch(&self, source_item: ItemStack) {
135        let flight_count = source_item
136            .get(FIREWORKS)
137            .map_or(1, |fireworks| 1 + fireworks.flight_duration());
138        self.entity_data.lock().id_fireworks_item.set(source_item);
139        self.set_velocity(DVec3::new(
140            triangle_random(0.0, INITIAL_HORIZONTAL_DEVIATION),
141            INITIAL_VERTICAL_VELOCITY,
142            triangle_random(0.0, INITIAL_HORIZONTAL_DEVIATION),
143        ));
144        self.state.lock().lifetime =
145            10 * flight_count + rand::random_range(0..6) + rand::random_range(0..7);
146    }
147
148    /// Sets whether this rocket was fired at an angle.
149    pub fn set_shot_at_angle(&self, shot_at_angle: bool) {
150        self.entity_data.lock().shot_at_angle.set(shot_at_angle);
151    }
152
153    /// Returns whether this rocket was fired at an angle.
154    #[must_use]
155    pub fn is_shot_at_angle(&self) -> bool {
156        *self.entity_data.lock().shot_at_angle.get()
157    }
158
159    fn is_attached_to_entity(&self) -> bool {
160        self.entity_data.lock().attached_to_target.get().is_some()
161    }
162
163    fn attached_entity(&self, world: &Arc<World>) -> Option<SharedEntity> {
164        if let Some(attached) = self
165            .state
166            .lock()
167            .attached_to_entity
168            .as_ref()
169            .and_then(Weak::upgrade)
170            && !attached.is_removed()
171            && attached.as_living_entity().is_some()
172        {
173            return Some(attached);
174        }
175
176        let attached_id = *self.entity_data.lock().attached_to_target.get();
177        let attached_id = i32::try_from(attached_id?).ok()?;
178        let attached = world.get_entity_by_id(attached_id)?;
179        attached.as_living_entity()?;
180        self.state.lock().attached_to_entity = Some(Arc::downgrade(&attached));
181        Some(attached)
182    }
183
184    fn tick_attached(&self, world: &Arc<World>) -> Option<ProjectileHit> {
185        if let Some(attached) = self.attached_entity(world)
186            && let Some(living) = attached.as_living_entity()
187        {
188            let hand_angle = if living.is_fall_flying() {
189                let look_angle = living.look_angle();
190                let movement = living.velocity();
191                living.set_velocity(elytra_boosted_velocity(movement, look_angle));
192                living.hand_holding_item_angle(&vanilla_items::FIREWORK_ROCKET)
193            } else {
194                DVec3::ZERO
195            };
196
197            if let Err(error) = self.try_set_position(living.position() + hand_angle) {
198                log::debug!("failed to move attached firework rocket: {error}");
199            }
200            self.set_velocity(living.velocity());
201        }
202
203        self.get_hit_result_on_move_vector()
204    }
205
206    fn tick_free_flying(&self) -> Option<ProjectileHit> {
207        if !self.is_shot_at_angle() {
208            let horizontal_acceleration = if self.horizontal_collision() {
209                1.0
210            } else {
211                HORIZONTAL_ACCELERATION
212            };
213            let movement = self.velocity();
214            self.set_velocity(DVec3::new(
215                movement.x * horizontal_acceleration,
216                movement.y + VERTICAL_ACCELERATION,
217                movement.z * horizontal_acceleration,
218            ));
219        }
220
221        let movement = self.velocity();
222        let hit = self.get_hit_result_on_move_vector();
223        self.move_entity(MoverType::SelfMovement, movement);
224        self.apply_effects_from_blocks();
225        self.set_velocity(movement);
226        hit
227    }
228
229    fn explosion_count(&self) -> usize {
230        self.entity_data
231            .lock()
232            .id_fireworks_item
233            .get()
234            .get(FIREWORKS)
235            .map_or(0, |fireworks| fireworks.explosions().len())
236    }
237
238    fn has_explosion(&self) -> bool {
239        self.explosion_count() != 0
240    }
241
242    fn fireworks_damage_source(&self) -> DamageSource {
243        let mut source = DamageSource::environment(&vanilla_damage_types::FIREWORKS)
244            .with_direct_entity(self.id());
245        if let Some(owner) = self.get_owner() {
246            source = source.with_causing_entity(owner.id());
247        }
248        source
249    }
250
251    fn deal_explosion_damage(&self, world: &Arc<World>) {
252        let explosion_count = self.explosion_count();
253        if explosion_count == 0 {
254            return;
255        }
256        let damage_amount = 5.0 + explosion_count as f32 * 2.0;
257        let attached = self.attached_entity(world);
258        let attached_id = attached.as_ref().map(|entity| entity.id());
259
260        if let Some(attached) = &attached {
261            attached.hurt(world, &self.fireworks_damage_source(), damage_amount);
262        }
263
264        let rocket_position = self.position();
265        let search_box = self.bounding_box().inflate(EXPLOSION_RADIUS);
266        for target in world.get_entities_in_aabb_matching(&search_box, Entity::is_living_entity) {
267            if attached_id == Some(target.id()) {
268                continue;
269            }
270            let distance_squared = rocket_position.distance_squared(target.position());
271            if distance_squared > EXPLOSION_RADIUS_SQUARED {
272                continue;
273            }
274
275            let target_height = f64::from(target.base().dimensions().height);
276            let can_see = [0.0, 0.5].into_iter().any(|height_scale| {
277                let target_position = target.position();
278                let to = DVec3::new(
279                    target_position.x,
280                    target_position.y + target_height * height_scale,
281                    target_position.z,
282                );
283                world
284                    .clip(
285                        rocket_position,
286                        to,
287                        ClipBlockShape::Collider,
288                        ClipFluid::None,
289                    )
290                    .is_miss()
291            });
292            if !can_see {
293                continue;
294            }
295
296            let distance = distance_squared.sqrt();
297            let distance_scale = ((EXPLOSION_RADIUS - distance) / EXPLOSION_RADIUS).sqrt();
298            target.hurt(
299                world,
300                &self.fireworks_damage_source(),
301                damage_amount * distance_scale as f32,
302            );
303        }
304    }
305
306    fn explode(&self, world: &Arc<World>) {
307        self.broadcast_entity_event(EntityStatus::FireworksExplode);
308        let owner = self.get_owner();
309        self.game_event_with_source_entity(&vanilla_game_events::EXPLODE, owner.as_deref());
310        self.deal_explosion_damage(world);
311        self.set_removed(RemovalReason::Discarded);
312    }
313
314    fn run_hit_block_entity_inside(&self, world: &Arc<World>, hit: &ClipHitResult) {
315        let state = world.get_block_state(hit.block_pos);
316        let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
317        let mut ignored_effects = InsideBlockEffectCollector::new();
318        behavior.entity_inside(
319            state,
320            world,
321            hit.block_pos,
322            self.as_entity_event_source(),
323            &mut ignored_effects,
324            true,
325        );
326    }
327
328    #[cfg(test)]
329    fn life_and_lifetime(&self) -> (i32, i32) {
330        let state = self.state.lock();
331        (state.life, state.lifetime)
332    }
333}
334
335impl Entity for FireworkRocketEntity {
336    fn base(&self) -> &EntityBase {
337        &self.base
338    }
339
340    fn entity_type(&self) -> EntityTypeRef {
341        self.entity_type
342    }
343
344    fn tick(&self) {
345        self.projectile_base_tick();
346        let Some(world) = self.level() else {
347            return;
348        };
349
350        let hit = if self.is_attached_to_entity() {
351            self.tick_attached(&world)
352        } else {
353            self.tick_free_flying()
354        };
355        if !self.no_physics()
356            && self.is_alive()
357            && let Some(hit) = &hit
358        {
359            self.hit_target_or_deflect_self(hit);
360            self.mark_velocity_sync();
361        }
362
363        self.update_rotation();
364        let (play_launch_sound, expired) = {
365            let mut state = self.state.lock();
366            let play_launch_sound = state.life == 0;
367            state.life = state.life.wrapping_add(1);
368            (play_launch_sound, state.life > state.lifetime)
369        };
370        if play_launch_sound && !self.is_silent() {
371            world.play_sound_at(
372                &sound_events::ENTITY_FIREWORK_ROCKET_LAUNCH,
373                SoundSource::Ambient,
374                self.position(),
375                3.0,
376                1.0,
377                None,
378            );
379        }
380        if expired {
381            self.explode(&world);
382        }
383    }
384
385    fn spawn_data(&self) -> i32 {
386        self.get_owner().map_or(0, |owner| owner.id())
387    }
388
389    fn restore_owner_reference(&self, owner: &SharedEntity) {
390        self.cache_owner_entity(owner);
391    }
392
393    fn projectile_owner_uuid(&self) -> Option<uuid::Uuid> {
394        self.owner_uuid()
395    }
396
397    fn projectile_owner(&self) -> Option<SharedEntity> {
398        self.get_owner()
399    }
400
401    fn attackable(&self) -> bool {
402        false
403    }
404
405    fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
406        Some(&self.entity_data)
407    }
408
409    fn save_additional(&self, nbt: &mut NbtCompound) {
410        self.save_projectile(nbt);
411        let state = self.state.lock();
412        nbt.insert("Life", state.life);
413        nbt.insert("LifeTime", state.lifetime);
414        drop(state);
415        nbt.insert("FireworksItem", self.get_item().to_nbt_tag_ref());
416        nbt.insert("ShotAtAngle", i8::from(self.is_shot_at_angle()));
417    }
418
419    fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
420        self.load_projectile(nbt);
421        {
422            let mut state = self.state.lock();
423            state.life = nbt.int("Life").unwrap_or(0);
424            state.lifetime = nbt.int("LifeTime").unwrap_or(0);
425        }
426        let item = nbt
427            .compound("FireworksItem")
428            .and_then(|item| ItemStack::from_borrowed_compound(&item))
429            .unwrap_or_else(|| ItemStack::new(&vanilla_items::FIREWORK_ROCKET));
430        self.set_item(item);
431        self.set_shot_at_angle(nbt.byte("ShotAtAngle").is_some_and(|value| value != 0));
432    }
433}
434
435impl Projectile for FireworkRocketEntity {
436    fn projectile_base(&self) -> &ProjectileBase {
437        &self.projectile_base
438    }
439
440    fn calculate_horizontal_hurt_knockback_direction(
441        &self,
442        hurt_entity: &dyn LivingEntity,
443        _damage_source: &DamageSource,
444    ) -> (f64, f64) {
445        let delta = hurt_entity.position() - self.position();
446        (delta.x, delta.z)
447    }
448
449    fn on_hit_entity(&self, _entity: &SharedEntity, _location: DVec3) {
450        if let Some(world) = self.level() {
451            self.explode(&world);
452        }
453    }
454
455    fn on_hit_block(&self, hit: &ClipHitResult) {
456        if let Some(world) = self.level() {
457            self.run_hit_block_entity_inside(&world, hit);
458            if self.has_explosion() {
459                self.explode(&world);
460            }
461        }
462        self.projectile_on_hit_block(hit);
463    }
464}
465
466impl FireworkRocketEntity {
467    /// Returns the synced source stack rendered by the client.
468    #[must_use]
469    pub fn get_item(&self) -> ItemStack {
470        self.entity_data.lock().id_fireworks_item.get().clone()
471    }
472
473    /// Replaces the synced source stack.
474    pub fn set_item(&self, item: ItemStack) {
475        self.entity_data.lock().id_fireworks_item.set(item);
476    }
477}
478
479fn triangle_random(mode: f64, deviation: f64) -> f64 {
480    mode + deviation * (rand::random::<f64>() - rand::random::<f64>())
481}
482
483fn elytra_boosted_velocity(movement: DVec3, look_angle: DVec3) -> DVec3 {
484    movement
485        + look_angle * ELYTRA_POWER_ADD
486        + (look_angle * ELYTRA_TARGET_SPEED - movement) * ELYTRA_VELOCITY_BLEND
487}
488
489#[cfg(test)]
490mod tests {
491    use std::io::Cursor;
492
493    use simdnbt::borrow::read_compound as read_borrowed_compound;
494    use simdnbt::owned::NbtCompound;
495    use steel_registry::data_components::components::Fireworks;
496    use steel_registry::data_components::vanilla_components::FIREWORKS;
497    use steel_registry::item_stack::ItemStack;
498    use steel_registry::{init_vanilla_registry, vanilla_entities, vanilla_items};
499
500    use crate::{
501        entity::{Entity, Projectile, entities::PigEntity},
502        test_support::test_world,
503    };
504
505    use super::*;
506
507    #[test]
508    fn launched_rocket_uses_fireworks_flight_duration_for_lifetime() {
509        init_vanilla_registry();
510        let mut item = ItemStack::new(&vanilla_items::FIREWORK_ROCKET);
511        item.set(
512            FIREWORKS,
513            Fireworks::new(3, Vec::new()).unwrap_or_else(|error| {
514                panic!("valid firework component should construct: {error}")
515            }),
516        );
517        let rocket = FireworkRocketEntity::launched(
518            &vanilla_entities::FIREWORK_ROCKET,
519            1,
520            DVec3::ZERO,
521            Weak::new(),
522            item,
523        );
524
525        let (_, lifetime) = rocket.life_and_lifetime();
526        assert!((40..=51).contains(&lifetime));
527        assert_eq!(
528            rocket.velocity().y.to_bits(),
529            INITIAL_VERTICAL_VELOCITY.to_bits()
530        );
531    }
532
533    #[test]
534    fn firework_uses_vanilla_neutral_sound_source() {
535        init_vanilla_registry();
536        let rocket = FireworkRocketEntity::new(
537            &vanilla_entities::FIREWORK_ROCKET,
538            1,
539            DVec3::ZERO,
540            Weak::new(),
541        );
542
543        assert_eq!(rocket.sound_source(), SoundSource::Neutral);
544    }
545
546    #[test]
547    fn hurt_marks_rocket_unless_base_invulnerable_and_always_returns_false() {
548        init_vanilla_registry();
549        let rocket = FireworkRocketEntity::new(
550            &vanilla_entities::FIREWORK_ROCKET,
551            1,
552            DVec3::ZERO,
553            Weak::new(),
554        );
555        let source = DamageSource::environment(&vanilla_damage_types::GENERIC);
556
557        assert!(!Entity::hurt(&rocket, test_world(), &source, 1.0));
558        assert!(rocket.hurt_marked());
559
560        rocket.clear_hurt_mark();
561        rocket.set_invulnerable(true);
562        assert!(!Entity::hurt(&rocket, test_world(), &source, 1.0));
563        assert!(!rocket.hurt_marked());
564    }
565
566    #[test]
567    fn firework_metadata_carries_item_attachment_and_angle() {
568        init_vanilla_registry();
569        let target: SharedEntity = Arc::new(PigEntity::new(
570            &vanilla_entities::PIG,
571            19,
572            DVec3::new(1.0, 2.0, 3.0),
573            Weak::new(),
574        ));
575        let Some(living_target) = target.as_living_entity() else {
576            panic!("pig test entity should be living");
577        };
578        let rocket = FireworkRocketEntity::attached_to_living(
579            &vanilla_entities::FIREWORK_ROCKET,
580            2,
581            Weak::new(),
582            ItemStack::new(&vanilla_items::FIREWORK_ROCKET),
583            living_target,
584        );
585        rocket.set_shot_at_angle(true);
586
587        let data = rocket.entity_data.lock();
588        assert_eq!(*data.attached_to_target.get(), Some(19));
589        assert!(*data.shot_at_angle.get());
590        assert!(
591            data.id_fireworks_item
592                .get()
593                .is(&vanilla_items::FIREWORK_ROCKET)
594        );
595        assert_eq!(rocket.owner_uuid(), Some(target.uuid()));
596    }
597
598    #[test]
599    fn firework_state_persists_with_vanilla_keys() {
600        init_vanilla_registry();
601        let rocket = FireworkRocketEntity::launched(
602            &vanilla_entities::FIREWORK_ROCKET,
603            1,
604            DVec3::ZERO,
605            Weak::new(),
606            ItemStack::new(&vanilla_items::FIREWORK_ROCKET),
607        );
608        {
609            let mut state = rocket.state.lock();
610            state.life = 7;
611            state.lifetime = 29;
612        }
613        rocket.set_shot_at_angle(true);
614        rocket.set_owner_uuid(Some(uuid::Uuid::from_u128(42)));
615
616        let mut nbt = NbtCompound::new();
617        rocket.save_additional(&mut nbt);
618        assert_eq!(nbt.int("Life"), Some(7));
619        assert_eq!(nbt.int("LifeTime"), Some(29));
620        assert_eq!(nbt.byte("ShotAtAngle"), Some(1));
621
622        let mut bytes = Vec::new();
623        nbt.write(&mut bytes);
624        let borrowed = read_borrowed_compound(&mut Cursor::new(&bytes))
625            .unwrap_or_else(|error| panic!("test NBT should reborrow: {error}"));
626        let loaded = FireworkRocketEntity::new(
627            &vanilla_entities::FIREWORK_ROCKET,
628            2,
629            DVec3::ZERO,
630            Weak::new(),
631        );
632        loaded.load_additional((&borrowed).into());
633
634        assert_eq!(loaded.life_and_lifetime(), (7, 29));
635        assert!(loaded.is_shot_at_angle());
636        assert_eq!(loaded.owner_uuid(), Some(uuid::Uuid::from_u128(42)));
637        assert!(loaded.get_item().is(&vanilla_items::FIREWORK_ROCKET));
638    }
639
640    #[test]
641    fn firework_knockback_direction_points_from_rocket_to_target() {
642        init_vanilla_registry();
643        let rocket = FireworkRocketEntity::new(
644            &vanilla_entities::FIREWORK_ROCKET,
645            1,
646            DVec3::new(2.0, 0.0, 3.0),
647            Weak::new(),
648        );
649        let target = PigEntity::new(
650            &vanilla_entities::PIG,
651            2,
652            DVec3::new(5.0, 0.0, 1.0),
653            Weak::new(),
654        );
655        let source = DamageSource::environment(&vanilla_damage_types::FIREWORKS);
656
657        assert_eq!(
658            rocket.calculate_horizontal_hurt_knockback_direction(&target, &source),
659            (3.0, -2.0)
660        );
661        assert!(rocket.as_projectile().is_some());
662    }
663
664    #[test]
665    fn firework_damage_source_has_no_raw_position() {
666        init_vanilla_registry();
667        let rocket = FireworkRocketEntity::new(
668            &vanilla_entities::FIREWORK_ROCKET,
669            23,
670            DVec3::new(1.0, 2.0, 3.0),
671            Weak::new(),
672        );
673
674        let source = rocket.fireworks_damage_source();
675
676        assert_eq!(source.direct_entity_id, Some(23));
677        assert!(source.source_position.is_none());
678    }
679
680    #[test]
681    fn elytra_boost_matches_vanilla_vector_formula() {
682        let movement = DVec3::new(0.2, -0.1, 0.4);
683        let look_angle = DVec3::new(0.0, 0.0, 1.0);
684
685        assert_eq!(
686            elytra_boosted_velocity(movement, look_angle),
687            DVec3::new(0.1, -0.05, 1.05)
688        );
689    }
690}