| 1 | //! The native marine habitat: a small school, one jellyfish and rising bubbles. |
| 2 | //! |
| 3 | //! Travel and authored dot poses are adapted from Codewhale Engine's |
| 4 | //! `ambient_life.rs` at `a79ce5c4d`; the pose module carries its MIT notice. |
| 5 | //! These components own no clock, simulation, input handler or runtime state. |
| 6 | //! The host supplies elapsed time and motion policy. Reduced and still motion |
| 7 | //! remove ambient life, as the Engine does; a host may separately paint its |
| 8 | //! existing [`crate::Whale`] in the host's reported state. |
| 9 | //! |
| 10 | //! Paint content first, then habitat. Every creature requires clear water |
| 11 | //! including a row and column of clearance around its full silhouette. No |
| 12 | //! component clears its area or repaints existing text. Filled blank overlays |
| 13 | //! need explicit protection through [`Habitat::protected`]. Painting into a fresh |
| 14 | //! buffer each frame lets creatures move without leaving trails. |
| 15 | |
| 16 | use std::time::Duration; |
| 17 | |
| 18 | use ratatui::{buffer::Buffer, layout::Rect}; |
| 19 | |
| 20 | use crate::{MotionMode, Paint, Role, Theme}; |
| 21 | |
| 22 | #[path = "habitat_poses.rs"] |
| 23 | mod poses; |
| 24 | |
| 25 | /// A bounded marine population. Auto follows the Engine's terminal tiers. |
| 26 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] |
| 27 | pub enum HabitatDensity { |
| 28 | #[default] |
| 29 | Auto, |
| 30 | /// Three fish, two bubble streams. |
| 31 | Sparse, |
| 32 | /// Five fish, four bubble streams. |
| 33 | Normal, |
| 34 | /// Seven fish, six bubble streams. There is still at most one jellyfish. |
| 35 | Rich, |
| 36 | } |
| 37 | |
| 38 | impl HabitatDensity { |
| 39 | #[must_use] |
| 40 | pub const fn for_area(self, area: Rect) -> Self { |
| 41 | match self { |
| 42 | Self::Auto if area.width < 56 || area.height < 12 => Self::Sparse, |
| 43 | Self::Auto if area.width < 88 || area.height < 20 => Self::Normal, |
| 44 | Self::Auto => Self::Rich, |
| 45 | other => other, |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | fn fish(self) -> usize { |
| 50 | match self { |
| 51 | Self::Sparse => 3, |
| 52 | Self::Normal => 5, |
| 53 | Self::Rich | Self::Auto => 7, |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | fn bubbles(self) -> usize { |
| 58 | match self { |
| 59 | Self::Sparse => 2, |
| 60 | Self::Normal => 4, |
| 61 | Self::Rich | Self::Auto => 6, |
| 62 | } |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | const WEDGE: &[(i32, u16)] = &[(0, 0), (-1, 4), (1, 6), (-1, 9), (1, 11), (0, 14), (-1, 17)]; |
| 67 | const SCHOOL_CELL_MS: u128 = 380; |
| 68 | |
| 69 | /// One loose school near the floor, with four native tail poses and half-cell |
| 70 | /// travel. ASCII terminals retain the Engine's eyed lead and smaller members. |
| 71 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 72 | pub struct FishSchool { |
| 73 | pub elapsed: Duration, |
| 74 | pub motion: MotionMode, |
| 75 | pub density: HabitatDensity, |
| 76 | } |
| 77 | |
| 78 | impl FishSchool { |
| 79 | #[must_use] |
| 80 | pub const fn new(elapsed: Duration, motion: MotionMode) -> Self { |
| 81 | Self { |
| 82 | elapsed, |
| 83 | motion, |
| 84 | density: HabitatDensity::Auto, |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | #[must_use] |
| 89 | pub const fn density(mut self, density: HabitatDensity) -> Self { |
| 90 | self.density = density; |
| 91 | self |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | impl FishSchool { |
| 96 | fn paint_except(&self, area: Rect, buf: &mut Buffer, theme: &Theme, protected: &[Rect]) { |
| 97 | let area = area.intersection(buf.area); |
| 98 | if !self.motion.animates() || area.width < 4 || area.height < 4 { |
| 99 | return; |
| 100 | } |
| 101 | let count = self.density.for_area(area).fish(); |
| 102 | let span = WEDGE[count - 1].1 + 4; |
| 103 | let travel = u128::from(area.width) + u128::from(span); |
| 104 | let cycle_ms = travel * SCHOOL_CELL_MS; |
| 105 | let t = self.elapsed.as_millis(); |
| 106 | // A fresh scene starts mid-crossing, rather than with an empty ocean. |
| 107 | let clock = t.saturating_add(cycle_ms / 2); |
| 108 | let right = ((clock / cycle_ms).wrapping_mul(0x9E37_79B9_7F4A_7C15) >> 7) & 1 == 0; |
| 109 | let step = ((clock % cycle_ms) * travel * 2 / cycle_ms) as i32; |
| 110 | let mut marks = Vec::with_capacity(count); |
| 111 | for (member, &(dy, behind)) in WEDGE.iter().take(count).enumerate() { |
| 112 | let bob = sine_bob(t, 3_000 + jitter(member as u128 + 41) % 2_600, 1); |
| 113 | let body = if theme.ascii() { |
| 114 | match (right, member == 0) { |
| 115 | (true, true) => "><o>", |
| 116 | (true, false) => "><>", |
| 117 | (false, true) => "<o><", |
| 118 | (false, false) => "<><", |
| 119 | } |
| 120 | } else { |
| 121 | let drift = |
| 122 | i32::from(sine_bob(t, 5_200 + jitter(member as u128 + 617) % 3_400, 2)) - 1; |
| 123 | let dot_x = if right { |
| 124 | step - i32::from(behind) * 2 - 8 + drift |
| 125 | } else { |
| 126 | i32::from(area.width) * 2 - step + i32::from(behind) * 2 - drift |
| 127 | }; |
| 128 | poses::fish( |
| 129 | right, |
| 130 | ((t / 300 + member as u128) % 4) as usize, |
| 131 | dot_x.rem_euclid(2) as usize, |
| 132 | usize::from(bob), |
| 133 | ) |
| 134 | }; |
| 135 | let width = if theme.ascii() { body.len() as u16 } else { 4 }; |
| 136 | let x = if theme.ascii() { |
| 137 | if right { |
| 138 | step / 2 - i32::from(behind) - i32::from(width) |
| 139 | } else { |
| 140 | i32::from(area.width) - step / 2 + i32::from(behind) |
| 141 | } |
| 142 | } else { |
| 143 | let drift = |
| 144 | i32::from(sine_bob(t, 5_200 + jitter(member as u128 + 617) % 3_400, 2)) - 1; |
| 145 | let dot_x = if right { |
| 146 | step - i32::from(behind) * 2 - 8 + drift |
| 147 | } else { |
| 148 | i32::from(area.width) * 2 - step + i32::from(behind) * 2 - drift |
| 149 | }; |
| 150 | dot_x.div_euclid(2) |
| 151 | }; |
| 152 | let y = |
| 153 | i32::from(area.height) - 3 + dy + if theme.ascii() { i32::from(bob) } else { 0 }; |
| 154 | let Some(at) = local_rect(area, x, y, width, 1) else { |
| 155 | continue; |
| 156 | }; |
| 157 | if open_water(at, buf, protected) { |
| 158 | let role = if member == 0 { |
| 159 | Role::Primary |
| 160 | } else { |
| 161 | Role::Hint |
| 162 | }; |
| 163 | marks.push((at, body, role)); |
| 164 | } |
| 165 | } |
| 166 | // All school members see the host's original text, rather than |
| 167 | // mistaking a neighbouring member of their wedge for a word. |
| 168 | for (at, body, role) in marks { |
| 169 | buf.set_stringn(at.x, at.y, body, usize::from(at.width), theme.fg(role)); |
| 170 | } |
| 171 | } |
| 172 | } |
| 173 | |
| 174 | impl Paint for FishSchool { |
| 175 | fn paint(&self, area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 176 | self.paint_except(area, buf, theme, &[]); |
| 177 | } |
| 178 | fn height(&self, _width: u16, _theme: &Theme) -> u16 { |
| 179 | 6 |
| 180 | } |
| 181 | } |
| 182 | |
| 183 | /// One complete bell with two lagging arms. The standalone component is |
| 184 | /// always present under Full motion. `visitor(true)` gives it the Engine's |
| 185 | /// slow six-row rise and long absence between visits, suitable for a habitat. |
| 186 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 187 | pub struct Jellyfish { |
| 188 | pub elapsed: Duration, |
| 189 | pub motion: MotionMode, |
| 190 | pub visitor: bool, |
| 191 | /// Keep a visitor's rise cycling during the Engine's long absence. |
| 192 | /// Collision checks remain authoritative even for a held visit. |
| 193 | pub hold_visit: bool, |
| 194 | } |
| 195 | |
| 196 | impl Jellyfish { |
| 197 | #[must_use] |
| 198 | pub const fn new(elapsed: Duration, motion: MotionMode) -> Self { |
| 199 | Self { |
| 200 | elapsed, |
| 201 | motion, |
| 202 | visitor: false, |
| 203 | hold_visit: false, |
| 204 | } |
| 205 | } |
| 206 | |
| 207 | #[must_use] |
| 208 | pub const fn visitor(mut self, visitor: bool) -> Self { |
| 209 | self.visitor = visitor; |
| 210 | self |
| 211 | } |
| 212 | |
| 213 | #[must_use] |
| 214 | pub const fn hold_visit(mut self, hold: bool) -> Self { |
| 215 | self.hold_visit = hold; |
| 216 | self |
| 217 | } |
| 218 | } |
| 219 | |
| 220 | impl Jellyfish { |
| 221 | fn paint_except(&self, area: Rect, buf: &mut Buffer, theme: &Theme, protected: &[Rect]) { |
| 222 | let area = area.intersection(buf.area); |
| 223 | if !self.motion.animates() || area.width < 7 || area.height < 5 { |
| 224 | return; |
| 225 | } |
| 226 | let t = self.elapsed.as_millis(); |
| 227 | let compact = theme.ascii() && area.width < 56; |
| 228 | let width = if compact { 3 } else { 5 }; |
| 229 | let wobble = sine_bob(t, 8_300, 2); |
| 230 | let (x, y, dy) = if self.visitor { |
| 231 | if area.height < 9 { |
| 232 | return; |
| 233 | } |
| 234 | const ROW_MS: u128 = 9_400; |
| 235 | let cycle = t.saturating_add(3_100) % (ROW_MS * 32); |
| 236 | if !self.hold_visit && cycle >= ROW_MS * 6 { |
| 237 | return; |
| 238 | } |
| 239 | let cycle = cycle % (ROW_MS * 6); |
| 240 | let dots = i32::from(area.height) * 4 - 32 - (cycle * 4 / ROW_MS) as i32; |
| 241 | let x = (u32::from(area.width) * 5 / 6) as u16; |
| 242 | ( |
| 243 | i32::from(x.min(area.width - width - 1)), |
| 244 | dots.div_euclid(4), |
| 245 | dots.rem_euclid(4) as usize, |
| 246 | ) |
| 247 | } else { |
| 248 | ( |
| 249 | i32::from((area.width - width) / 2), |
| 250 | i32::from((area.height - 3) / 2), |
| 251 | usize::from(sine_bob(t, 9_400, 3)), |
| 252 | ) |
| 253 | }; |
| 254 | let Some(target) = local_rect(area, x, y, width, 3) else { |
| 255 | return; |
| 256 | }; |
| 257 | // Whole-silhouette clearance, including blank cells between arms. |
| 258 | // Withhold rather than splitting the bell or dodging around live text. |
| 259 | if target.x == area.x |
| 260 | || target.y == area.y |
| 261 | || target.right() == area.right() |
| 262 | || target.bottom() == area.bottom() |
| 263 | || !open_water(target, buf, protected) |
| 264 | { |
| 265 | return; |
| 266 | } |
| 267 | if theme.ascii() { |
| 268 | let pulse = ((t % 5_200) * 2 / 5_200) as usize; |
| 269 | let arms = ((t.saturating_add(3_100) / 1_100) % 4) as usize; |
| 270 | let tops = if compact { |
| 271 | [".-.", "'-'"] |
| 272 | } else { |
| 273 | [".-~-.", ".'-'."] |
| 274 | }; |
| 275 | let skirts = if compact { |
| 276 | ["\\_/", "(_)"] |
| 277 | } else { |
| 278 | ["\\___/", "(___)"] |
| 279 | }; |
| 280 | let trails = if compact { |
| 281 | ["| |", "/ \\", "| /", "\\ |"] |
| 282 | } else { |
| 283 | [" | | ", " / \\ ", " | / ", " \\ | "] |
| 284 | }; |
| 285 | for (row, body) in [tops[pulse], skirts[pulse], trails[arms]] |
| 286 | .into_iter() |
| 287 | .enumerate() |
| 288 | { |
| 289 | buf.set_stringn( |
| 290 | target.x, |
| 291 | target.y + row as u16, |
| 292 | body, |
| 293 | usize::from(width), |
| 294 | theme.fg(Role::Hint), |
| 295 | ); |
| 296 | } |
| 297 | } else { |
| 298 | let pose = ((t % 5_200) * 16 / 5_200) as usize; |
| 299 | for (row, body) in poses::jelly(pose, usize::from(wobble % 2), dy) |
| 300 | .iter() |
| 301 | .enumerate() |
| 302 | { |
| 303 | let role = if row == 0 { Role::Hint } else { Role::Dim }; |
| 304 | buf.set_stringn( |
| 305 | target.x, |
| 306 | target.y + row as u16, |
| 307 | body, |
| 308 | usize::from(width), |
| 309 | theme.fg(role), |
| 310 | ); |
| 311 | } |
| 312 | } |
| 313 | } |
| 314 | } |
| 315 | |
| 316 | impl Paint for Jellyfish { |
| 317 | fn paint(&self, area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 318 | self.paint_except(area, buf, theme, &[]); |
| 319 | } |
| 320 | fn height(&self, _width: u16, _theme: &Theme) -> u16 { |
| 321 | 7 |
| 322 | } |
| 323 | } |
| 324 | |
| 325 | /// Two, four or six irregular bubble streams. Their short five-row ascent |
| 326 | /// stays near the floor and gives way to prose and other marine silhouettes. |
| 327 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 328 | pub struct BubbleField { |
| 329 | pub elapsed: Duration, |
| 330 | pub motion: MotionMode, |
| 331 | pub density: HabitatDensity, |
| 332 | } |
| 333 | |
| 334 | impl BubbleField { |
| 335 | #[must_use] |
| 336 | pub const fn new(elapsed: Duration, motion: MotionMode) -> Self { |
| 337 | Self { |
| 338 | elapsed, |
| 339 | motion, |
| 340 | density: HabitatDensity::Auto, |
| 341 | } |
| 342 | } |
| 343 | |
| 344 | #[must_use] |
| 345 | pub const fn density(mut self, density: HabitatDensity) -> Self { |
| 346 | self.density = density; |
| 347 | self |
| 348 | } |
| 349 | } |
| 350 | |
| 351 | impl BubbleField { |
| 352 | fn paint_except(&self, area: Rect, buf: &mut Buffer, theme: &Theme, protected: &[Rect]) { |
| 353 | let area = area.intersection(buf.area); |
| 354 | if !self.motion.animates() || area.width < 3 || area.height < 3 { |
| 355 | return; |
| 356 | } |
| 357 | let t = self.elapsed.as_millis(); |
| 358 | for stream in 0..self.density.for_area(area).bubbles() { |
| 359 | let seed = stream as u128; |
| 360 | let phase = jitter(seed) % 9_000; |
| 361 | let period = 3_200 + jitter(seed + 313) % 2_600; |
| 362 | let rise = (t.saturating_add(phase) % period) * 5 / period; |
| 363 | let lane = (jitter(seed + 977) % u128::from(area.width)) as i32; |
| 364 | let drift = i32::from(sine_bob(t.saturating_add(phase), 2_100, 2)) - 1; |
| 365 | let x = (lane + drift).clamp(0, i32::from(area.width) - 1); |
| 366 | let y = (i32::from(area.height) - 2 - rise as i32).max(0); |
| 367 | let Some(at) = local_rect(area, x, y, 1, 1) else { |
| 368 | continue; |
| 369 | }; |
| 370 | if !open_water(at, buf, protected) { |
| 371 | continue; |
| 372 | } |
| 373 | let glyph = match (theme.ascii(), rise) { |
| 374 | (true, 0..=1) => ".", |
| 375 | (true, 2..=3) => "o", |
| 376 | (true, _) => "O", |
| 377 | (false, 0..=1) => "·", |
| 378 | (false, 2..=3) => "˚", |
| 379 | (false, _) => "°", |
| 380 | }; |
| 381 | buf.set_stringn(at.x, at.y, glyph, 1, theme.fg(Role::Dim)); |
| 382 | } |
| 383 | } |
| 384 | } |
| 385 | |
| 386 | impl Paint for BubbleField { |
| 387 | fn paint(&self, area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 388 | self.paint_except(area, buf, theme, &[]); |
| 389 | } |
| 390 | fn height(&self, _width: u16, _theme: &Theme) -> u16 { |
| 391 | 8 |
| 392 | } |
| 393 | } |
| 394 | |
| 395 | /// The three marine layers, painted over the host's existing content. The |
| 396 | /// school owns the lower band, one occasional jellyfish visits above it, |
| 397 | /// and bubbles occupy whatever clear water remains. No new whale is created. |
| 398 | #[derive(Clone, Debug, PartialEq, Eq)] |
| 399 | pub struct Habitat { |
| 400 | pub elapsed: Duration, |
| 401 | pub motion: MotionMode, |
| 402 | pub density: HabitatDensity, |
| 403 | pub fish: bool, |
| 404 | pub jellyfish: bool, |
| 405 | pub bubbles: bool, |
| 406 | pub hold_jellyfish_visit: bool, |
| 407 | /// Host-owned overlays and other areas that must remain undisturbed, |
| 408 | /// including their blank cells. Coordinates are absolute buffer positions. |
| 409 | pub protected: Vec<Rect>, |
| 410 | } |
| 411 | |
| 412 | impl Habitat { |
| 413 | #[must_use] |
| 414 | pub const fn new(elapsed: Duration, motion: MotionMode) -> Self { |
| 415 | Self { |
| 416 | elapsed, |
| 417 | motion, |
| 418 | density: HabitatDensity::Auto, |
| 419 | fish: true, |
| 420 | jellyfish: true, |
| 421 | bubbles: true, |
| 422 | hold_jellyfish_visit: false, |
| 423 | protected: Vec::new(), |
| 424 | } |
| 425 | } |
| 426 | |
| 427 | #[must_use] |
| 428 | pub const fn density(mut self, density: HabitatDensity) -> Self { |
| 429 | self.density = density; |
| 430 | self |
| 431 | } |
| 432 | #[must_use] |
| 433 | pub const fn fish(mut self, show: bool) -> Self { |
| 434 | self.fish = show; |
| 435 | self |
| 436 | } |
| 437 | #[must_use] |
| 438 | pub const fn jellyfish(mut self, show: bool) -> Self { |
| 439 | self.jellyfish = show; |
| 440 | self |
| 441 | } |
| 442 | #[must_use] |
| 443 | pub const fn bubbles(mut self, show: bool) -> Self { |
| 444 | self.bubbles = show; |
| 445 | self |
| 446 | } |
| 447 | /// Protect filled blank overlays, focus surfaces or other host-owned |
| 448 | /// regions. A creature that touches a region's one-cell clearance is |
| 449 | /// withheld as a complete silhouette. |
| 450 | #[must_use] |
| 451 | pub fn protected(mut self, areas: impl Into<Vec<Rect>>) -> Self { |
| 452 | self.protected = areas.into(); |
| 453 | self |
| 454 | } |
| 455 | |
| 456 | #[must_use] |
| 457 | pub fn exclude_rect(mut self, area: Rect) -> Self { |
| 458 | self.protected.push(area); |
| 459 | self |
| 460 | } |
| 461 | |
| 462 | /// Keep the corner visitor present for a preview or dedicated marine |
| 463 | /// surface. False (the default) retains its long absence between visits. |
| 464 | /// Full-silhouette collision checks still give all existing text priority. |
| 465 | #[must_use] |
| 466 | pub const fn hold_jellyfish_visit(mut self, hold: bool) -> Self { |
| 467 | self.hold_jellyfish_visit = hold; |
| 468 | self |
| 469 | } |
| 470 | } |
| 471 | |
| 472 | impl Paint for Habitat { |
| 473 | fn paint(&self, area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 474 | if self.fish { |
| 475 | FishSchool::new(self.elapsed, self.motion) |
| 476 | .density(self.density) |
| 477 | .paint_except(area, buf, theme, &self.protected); |
| 478 | } |
| 479 | if self.jellyfish { |
| 480 | Jellyfish::new(self.elapsed, self.motion) |
| 481 | .visitor(true) |
| 482 | .hold_visit(self.hold_jellyfish_visit) |
| 483 | .paint_except(area, buf, theme, &self.protected); |
| 484 | } |
| 485 | if self.bubbles { |
| 486 | BubbleField::new(self.elapsed, self.motion) |
| 487 | .density(self.density) |
| 488 | .paint_except(area, buf, theme, &self.protected); |
| 489 | } |
| 490 | } |
| 491 | |
| 492 | fn height(&self, _width: u16, _theme: &Theme) -> u16 { |
| 493 | 16 |
| 494 | } |
| 495 | } |
| 496 | |
| 497 | // Signed local coordinates make off-screen school travel and saturated |
| 498 | // absolute buffer edges ordinary clipping, rather than u16 wraparound. |
| 499 | fn local_rect(area: Rect, x: i32, y: i32, width: u16, height: u16) -> Option<Rect> { |
| 500 | if x < 0 |
| 501 | || y < 0 |
| 502 | || x + i32::from(width) > i32::from(area.width) |
| 503 | || y + i32::from(height) > i32::from(area.height) |
| 504 | { |
| 505 | return None; |
| 506 | } |
| 507 | Some(Rect::new( |
| 508 | area.x + x as u16, |
| 509 | area.y + y as u16, |
| 510 | width, |
| 511 | height, |
| 512 | )) |
| 513 | } |
| 514 | |
| 515 | fn open_water(at: Rect, buf: &Buffer, protected: &[Rect]) -> bool { |
| 516 | let x0 = at.x.saturating_sub(1).max(buf.area.x); |
| 517 | let y0 = at.y.saturating_sub(1).max(buf.area.y); |
| 518 | let x1 = at.right().saturating_add(1).min(buf.area.right()); |
| 519 | let y1 = at.bottom().saturating_add(1).min(buf.area.bottom()); |
| 520 | let clearance = Rect::new(x0, y0, x1 - x0, y1 - y0); |
| 521 | if protected |
| 522 | .iter() |
| 523 | .any(|r| !clearance.intersection(*r).is_empty()) |
| 524 | { |
| 525 | return false; |
| 526 | } |
| 527 | (y0..y1).all(|y| (x0..x1).all(|x| buf[(x, y)].symbol().chars().all(char::is_whitespace))) |
| 528 | } |
| 529 | |
| 530 | // Engine's deterministic phase spread; no RNG and no animation state. |
| 531 | fn jitter(seed: u128) -> u128 { |
| 532 | let mut hash = 0xcbf2_9ce4_8422_2325u64; |
| 533 | for byte in seed.to_le_bytes() { |
| 534 | hash ^= u64::from(byte); |
| 535 | hash = hash.wrapping_mul(0x0000_0100_0000_01b3); |
| 536 | } |
| 537 | u128::from(hash) |
| 538 | } |
| 539 | |
| 540 | fn sine_bob(elapsed: u128, period: u128, amplitude: u16) -> u16 { |
| 541 | let fraction = (elapsed % period) as f64 / period as f64; |
| 542 | (((fraction * std::f64::consts::TAU).sin() + 1.0) * 0.5 * f64::from(amplitude)).round() as u16 |
| 543 | } |
| 544 |