1use crate::prelude::*;
10use core::fmt::Write as _;
11
12use serde::{Deserialize, Serialize};
13use shuyi_assets::grid_map::MapLook;
14use shuyi_core::{Pos, TerrainId};
15
16use super::skin::Terrains;
17
18#[derive(Debug, Clone, PartialEq)]
23#[non_exhaustive]
24pub struct MapScene<U> {
25 pub origin: Pos,
28 pub size: (i32, i32),
30 pub more: MapEdges,
33 pub tiles: Vec<TileView>,
35 pub ranges: Vec<RangeLayer>,
38 pub rim: Vec<Option<TerrainId>>,
44 pub look: MapLook,
48 pub units: Vec<U>,
50 pub cursor: Option<CursorView>,
52 pub path: Vec<Pos>,
55 pub threat: Option<ThreatView>,
58 pub clock_ms: u64,
61}
62
63#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
66pub struct MapEdges {
67 pub x: Ends,
70 pub y: Ends,
72}
73
74#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
76pub struct Ends {
77 pub before: bool,
79 pub after: bool,
81}
82
83impl Ends {
84 pub const NONE: Self = Self {
86 before: false,
87 after: false,
88 };
89
90 fn of_view(start: i32, len: i32, map_len: i32) -> Self {
93 let end = i64::from(start) + i64::from(len.max(0));
94 Self {
95 before: start > 0,
96 after: end < i64::from(map_len),
97 }
98 }
99}
100
101impl MapEdges {
102 pub const NONE: Self = Self {
104 x: Ends::NONE,
105 y: Ends::NONE,
106 };
107
108 pub fn of_view(origin: Pos, size: (i32, i32), map_size: (i32, i32)) -> Self {
114 Self {
115 x: Ends::of_view(origin.x, size.0, map_size.0),
116 y: Ends::of_view(origin.y, size.1, map_size.1),
117 }
118 }
119
120 pub const fn left(self) -> bool {
122 self.x.before
123 }
124
125 pub const fn right(self) -> bool {
127 self.x.after
128 }
129
130 pub const fn up(self) -> bool {
132 self.y.before
133 }
134
135 pub const fn down(self) -> bool {
137 self.y.after
138 }
139
140 pub const fn any(self) -> bool {
142 self.left() || self.right() || self.up() || self.down()
143 }
144
145 pub fn names(self) -> Vec<&'static str> {
147 let sides = [
148 (self.left(), "left"),
149 (self.right(), "right"),
150 (self.up(), "up"),
151 (self.down(), "down"),
152 ];
153 sides
154 .into_iter()
155 .filter_map(|(set, name)| set.then_some(name))
156 .collect()
157 }
158}
159
160#[derive(Debug, Clone, PartialEq, Default)]
162pub struct TileView {
163 pub terrain: Option<TerrainId>,
165 pub flashes: Vec<f32>,
169 pub becomes: Option<TerrainId>,
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
179pub struct RangeName(pub &'static str);
180
181impl RangeName {
182 pub fn mark(self) -> char {
185 self.0.chars().next().unwrap_or('?')
186 }
187}
188
189#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct RangeLayer {
193 pub kind: RangeName,
195 pub tiles: Vec<Pos>,
197}
198
199#[derive(Debug, Clone, PartialEq, Eq)]
202pub struct Tints {
203 across: i32,
205 tiles: Vec<Vec<RangeName>>,
207}
208
209impl Tints {
210 pub fn at(&self, dx: i32, dy: i32) -> &[RangeName] {
213 let index = dy
214 .checked_mul(self.across)
215 .and_then(|row| row.checked_add(dx))
216 .filter(|_| (0..self.across).contains(&dx))
217 .and_then(|i| usize::try_from(i).ok());
218 index
219 .and_then(|i| self.tiles.get(i))
220 .map_or(&[], Vec::as_slice)
221 }
222}
223
224#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
227pub enum CursorStyle {
228 #[default]
230 Corners,
231 LargeCorners,
233 TileGlow,
235}
236
237impl CursorStyle {
238 pub const fn name(self) -> &'static str {
240 match self {
241 CursorStyle::Corners => "corners",
242 CursorStyle::LargeCorners => "large-corners",
243 CursorStyle::TileGlow => "glow",
244 }
245 }
246}
247
248#[derive(Debug, Clone, Copy, PartialEq)]
251#[non_exhaustive]
252pub struct CursorView {
253 pub pos: Pos,
255 pub brightness: f32,
257 pub style: CursorStyle,
259}
260
261impl CursorView {
262 #[must_use]
265 pub const fn new(pos: Pos, brightness: f32, style: CursorStyle) -> Self {
266 Self {
267 pos,
268 brightness,
269 style,
270 }
271 }
272}
273
274#[derive(Debug, Clone, PartialEq, Eq)]
277pub struct ThreatView {
278 pub tile: Pos,
280 pub enemies: Vec<Pos>,
283}
284
285pub trait MapUnit {
288 type Id: Copy + PartialEq;
290
291 fn id(&self) -> Self::Id;
293
294 fn pos(&self) -> Pos;
296
297 fn text(&self) -> String;
300}
301
302impl<U> MapScene<U> {
303 pub fn new(origin: Pos, size: (i32, i32)) -> Self {
306 let size = (size.0.max(0), size.1.max(0));
307 let count = usize::try_from(size.0).unwrap_or(0) * usize::try_from(size.1).unwrap_or(0);
308 Self {
309 origin,
310 size,
311 more: MapEdges::NONE,
312 tiles: vec![TileView::default(); count],
313 ranges: Vec::new(),
314 rim: vec![None; rim_len(size)],
315 look: MapLook::default(),
316 units: Vec::new(),
317 cursor: None,
318 path: Vec::new(),
319 threat: None,
320 clock_ms: 0,
321 }
322 }
323
324 pub fn offset(&self, pos: Pos) -> Option<(i32, i32)> {
327 let dx = pos.x.checked_sub(self.origin.x)?;
328 let dy = pos.y.checked_sub(self.origin.y)?;
329 let inside = (0..self.size.0).contains(&dx) && (0..self.size.1).contains(&dy);
330 inside.then_some((dx, dy))
331 }
332
333 pub fn contains(&self, pos: Pos) -> bool {
335 self.offset(pos).is_some()
336 }
337
338 fn index(&self, dx: i32, dy: i32) -> Option<usize> {
341 if !(0..self.size.0).contains(&dx) || !(0..self.size.1).contains(&dy) {
342 return None;
343 }
344 let (dx, dy) = (usize::try_from(dx).ok()?, usize::try_from(dy).ok()?);
345 Some(dy * usize::try_from(self.size.0).ok()? + dx)
346 }
347
348 pub fn tile_at(&self, dx: i32, dy: i32) -> Option<&TileView> {
350 self.tiles.get(self.index(dx, dy)?)
351 }
352
353 pub fn tile(&self, pos: Pos) -> Option<&TileView> {
355 let (dx, dy) = self.offset(pos)?;
356 self.tile_at(dx, dy)
357 }
358
359 pub fn tile_mut(&mut self, pos: Pos) -> Option<&mut TileView> {
361 let (dx, dy) = self.offset(pos)?;
362 let index = self.index(dx, dy)?;
363 self.tiles.get_mut(index)
364 }
365
366 fn rim_index(&self, dx: i32, dy: i32) -> Option<usize> {
369 let (w, h) = self.size;
370 if !(-1..=w).contains(&dx) || !(-1..=h).contains(&dy) {
371 return None;
372 }
373 let across = w + 2;
374 let index = if dy == -1 {
375 dx + 1
376 } else if dy == h {
377 across + 2 * h + dx + 1
378 } else if dx == -1 {
379 across + 2 * dy
380 } else if dx == w {
381 across + 2 * dy + 1
382 } else {
383 return None;
384 };
385 usize::try_from(index).ok()
386 }
387
388 pub fn terrain_near(&self, dx: i32, dy: i32) -> Option<TerrainId> {
392 match self.tile_at(dx, dy) {
393 Some(tile) => tile.terrain,
394 None => self.rim.get(self.rim_index(dx, dy)?).copied().flatten(),
395 }
396 }
397
398 pub fn set_terrain(&mut self, terrain: impl Fn(Pos) -> Option<TerrainId>) {
401 let (w, h) = self.size;
402 let origin = self.origin;
403 for (dx, dy) in (-1..=h).flat_map(|dy| (-1..=w).map(move |dx| (dx, dy))) {
404 let id = terrain(Pos::new(origin.x + dx, origin.y + dy));
405 let tile = self.index(dx, dy).and_then(|i| self.tiles.get_mut(i));
406 if let Some(tile) = tile {
407 tile.terrain = id;
408 } else if let Some(slot) = self.rim_index(dx, dy).and_then(|i| self.rim.get_mut(i)) {
409 *slot = id;
410 }
411 }
412 }
413
414 pub fn tint(&mut self, tiles: impl IntoIterator<Item = Pos>, kind: impl Into<RangeName>) {
417 let tiles: Vec<Pos> = tiles
418 .into_iter()
419 .filter(|&pos| self.tile(pos).is_some())
420 .collect();
421 if !tiles.is_empty() {
422 let kind = kind.into();
423 self.ranges.push(RangeLayer { kind, tiles });
424 }
425 }
426
427 pub fn tints(&self) -> Tints {
430 let mut tiles = vec![Vec::new(); self.tiles.len()];
431 for layer in &self.ranges {
432 for &pos in &layer.tiles {
433 let at = self.offset(pos).and_then(|(dx, dy)| self.index(dx, dy));
434 if let Some(list) = at.and_then(|i| tiles.get_mut(i)) {
435 list.push(layer.kind);
436 }
437 }
438 }
439 Tints {
440 across: self.size.0,
441 tiles,
442 }
443 }
444
445 pub fn tinted(&self, kind: impl Into<RangeName>) -> Vec<Pos> {
447 let kind = kind.into();
448 let tints = self.tints();
449 let (w, h) = self.size;
450 let offsets = (0..h).flat_map(|dy| (0..w).map(move |dx| (dx, dy)));
451 offsets
452 .filter(|&(dx, dy)| tints.at(dx, dy).contains(&kind))
453 .map(|(dx, dy)| Pos::new(self.origin.x + dx, self.origin.y + dy))
454 .collect()
455 }
456
457 pub fn tints_at(&self, pos: Pos) -> Vec<RangeName> {
460 match self.offset(pos) {
461 Some((dx, dy)) => self.tints().at(dx, dy).to_vec(),
462 None => Vec::new(),
463 }
464 }
465
466 pub fn terrain_at(&self, pos: Pos) -> Option<TerrainId> {
468 self.tile(pos).and_then(|t| t.terrain)
469 }
470
471 pub fn cursor_tile(&self) -> Option<Pos> {
473 self.cursor.map(|c| c.pos)
474 }
475
476 fn row_text(&self, terrains: &dyn Terrains, tints: &Tints, dy: i32) -> String {
479 let mut runs: Vec<(String, usize)> = Vec::new();
480 for dx in 0..self.size.0 {
481 let name = self.tile_at(dx, dy).map_or_else(
482 || "!".to_owned(),
483 |t| tile_text(terrains, t, tints.at(dx, dy)),
484 );
485 match runs.last_mut() {
486 Some((last, n)) if *last == name => *n += 1,
487 _ => runs.push((name, 1)),
488 }
489 }
490 let words: Vec<String> = runs
491 .into_iter()
492 .map(|(name, n)| if n > 1 { format!("{name}*{n}") } else { name })
493 .collect();
494 words.join(" ")
495 }
496}
497
498impl<U: MapUnit> MapScene<U> {
499 pub fn push_unit(&mut self, unit: U) {
501 if self.contains(unit.pos()) {
502 self.units.push(unit);
503 }
504 }
505
506 pub fn unit(&self, id: U::Id) -> Option<&U> {
508 self.units.iter().find(|u| u.id() == id)
509 }
510
511 pub fn unit_at(&self, pos: Pos) -> Option<&U> {
514 self.units.iter().rev().find(|u| u.pos() == pos)
515 }
516
517 pub fn to_text(&self, terrains: &dyn Terrains) -> String {
547 let mut out = String::new();
548 let Pos { x, y } = self.origin;
549 let (w, h) = self.size;
550 let _ = write!(out, "origin ({x},{y}) size {w}x{h}");
552 let usual = MapLook::default();
553 if self.look.tiles != usual.tiles {
554 let _ = write!(out, " look {}", self.look.tiles);
555 }
556 if self.look.light != usual.light {
557 let _ = write!(out, " light {}", self.look.light);
558 }
559 out.push('\n');
560 if self.more.any() {
561 let _ = writeln!(out, "more {}", self.more.names().join(" "));
562 }
563 let tints = self.tints();
564 for dy in 0..h {
565 let row = self.row_text(terrains, &tints, dy);
566 let _ = writeln!(out, "{:>4}: {row}", i64::from(y) + i64::from(dy));
567 }
568 let _ = writeln!(out, "units {}", self.units.len());
569 for u in &self.units {
570 let _ = writeln!(out, " {}", u.text());
571 }
572 match &self.cursor {
573 Some(c) => {
574 let Pos { x, y } = c.pos;
575 let (style, bright) = (c.style.name(), c.brightness);
576 let _ = writeln!(out, "cursor ({x},{y}) {style} {bright:.2}");
577 }
578 None => out.push_str("cursor -\n"),
579 }
580 out.push_str("path");
581 if self.path.is_empty() {
582 out.push_str(" -");
583 }
584 for p in &self.path {
585 let _ = write!(out, " ({},{})", p.x, p.y);
586 }
587 out.push('\n');
588 if let Some(threat) = &self.threat {
589 let Pos { x, y } = threat.tile;
590 let _ = write!(out, "threat ({x},{y}) from");
591 for p in &threat.enemies {
592 let _ = write!(out, " ({},{})", p.x, p.y);
593 }
594 out.push('\n');
595 }
596 out
597 }
598}
599
600fn rim_len(size: (i32, i32)) -> usize {
602 let count = |tiles: i32| usize::try_from(tiles).unwrap_or(0);
603 2 * (count(size.0) + 2) + 2 * count(size.1)
604}
605
606fn tile_text(terrains: &dyn Terrains, tile: &TileView, ranges: &[RangeName]) -> String {
610 let name = |id: TerrainId| match terrains.name(id) {
611 Some(name) => name.to_owned(),
612 None => format!("?{}", id.0),
613 };
614 let mut text = tile.terrain.map_or_else(|| "-".to_owned(), name);
615 if !tile.flashes.is_empty() {
616 text.push('!');
617 }
618 if !ranges.is_empty() {
619 text.push('+');
620 text.extend(ranges.iter().map(|k| k.mark()));
621 }
622 if let Some(id) = tile.becomes {
623 text.push('>');
624 text.push_str(&name(id));
625 }
626 text
627}
628
629#[cfg(test)]
630mod tests {
631 use super::*;
632 use crate::toy::grid::{GRASS, Piece, SAND, Terrain, WATER};
633
634 fn p(x: i32, y: i32) -> Pos {
635 Pos::new(x, y)
636 }
637
638 fn piece(id: u32, pos: Pos) -> Piece {
639 Piece::new(id, 'k', pos)
640 }
641
642 const MOVE: RangeName = RangeName("move");
643 const DANGER: RangeName = RangeName("danger");
644 const ATTACK: RangeName = RangeName("attack");
645 const HEAL: RangeName = RangeName("heal");
646
647 #[test]
648 fn a_new_scene_is_empty_tiles_off_the_map() {
649 let s = MapScene::<Piece>::new(p(-2, 3), (4, 2));
650 assert_eq!((s.origin, s.size), (p(-2, 3), (4, 2)));
651 assert_eq!(s.tiles, vec![TileView::default(); 8]);
652 assert_eq!(s.tiles[0].terrain, None);
653 assert!(s.units.is_empty() && s.path.is_empty() && s.cursor.is_none());
654 assert!(s.ranges.is_empty(), "{:?}", s.ranges);
655 assert_eq!(s.threat, None);
656 assert_eq!(s.clock_ms, 0);
657 assert_eq!(s.rim, vec![None; 16]);
659 assert_eq!(s.look, MapLook::default());
660 assert_eq!(s.more, MapEdges::NONE);
661 let none = MapScene::<Piece>::new(p(0, 0), (-3, 5));
663 assert_eq!((none.size, none.tiles.len()), ((0, 5), 0));
664 let none = MapScene::<Piece>::new(p(0, 0), (3, -5));
665 assert_eq!((none.size, none.tiles.len()), ((3, 0), 0));
666 assert!(!none.contains(p(0, 0)));
667 }
668
669 #[test]
670 fn tiles_are_found_by_map_position_inside_the_view_only() {
671 let mut s = MapScene::<Piece>::new(p(-2, 3), (4, 2));
672 for (i, t) in (0..).zip(&mut s.tiles) {
673 t.terrain = Some(TerrainId(i));
674 }
675 let id = |pos| s.tile(pos).and_then(|t| t.terrain).map(|t| t.0);
676 let last_row = (id(p(-2, 4)), id(p(1, 4)));
677 assert_eq!(id(p(-2, 3)), Some(0));
678 assert_eq!(id(p(1, 3)), Some(3));
679 assert_eq!(last_row, (Some(4), Some(7)));
680 for out in [p(-3, 3), p(2, 3), p(-2, 2), p(-2, 5), p(2, 4)] {
681 assert_eq!(s.tile(out), None, "{out:?}");
682 assert!(!s.contains(out), "{out:?}");
683 }
684 assert_eq!(s.offset(p(1, 4)), Some((3, 1)));
685 assert_eq!(s.offset(p(i32::MIN, 3)), None);
686 assert_eq!(s.offset(p(0, i32::MAX)), None);
687 assert_eq!(s.tile_at(3, 1).and_then(|t| t.terrain), Some(TerrainId(7)));
688 for (dx, dy) in [(4, 0), (-1, 0), (0, 2), (0, -1)] {
689 assert_eq!(s.tile_at(dx, dy), None, "({dx}, {dy})");
690 }
691 s.tiles.truncate(5);
693 assert_eq!(s.tile(p(-2, 4)).and_then(|t| t.terrain), Some(TerrainId(4)));
694 assert_eq!(s.tile(p(-1, 4)), None);
695 s.tile_mut(p(-2, 4)).unwrap().terrain = None;
696 assert_eq!(s.terrain_at(p(-2, 4)), None);
697 assert!(s.tile_mut(p(-1, 4)).is_none());
698 }
699
700 #[test]
701 fn the_ring_round_the_view_holds_the_terrain_one_tile_out() {
702 let mut s = MapScene::<Piece>::new(p(5, -2), (3, 2));
703 assert_eq!((rim_len((3, 2)), rim_len((0, 0))), (14, 4));
704 assert_eq!((rim_len((-3, 2)), rim_len((3, -2))), (8, 10));
705 let ring = [
707 (-1, -1),
708 (0, -1),
709 (1, -1),
710 (2, -1),
711 (3, -1),
712 (-1, 0),
713 (3, 0),
714 (-1, 1),
715 (3, 1),
716 (-1, 2),
717 (0, 2),
718 (1, 2),
719 (2, 2),
720 (3, 2),
721 ];
722 for (i, (dx, dy)) in ring.into_iter().enumerate() {
723 assert_eq!(s.rim_index(dx, dy), Some(i), "({dx}, {dy})");
724 }
725 let outside = [
728 (0, 0),
729 (2, 1),
730 (-2, 0),
731 (4, 0),
732 (0, -2),
733 (0, 3),
734 (-2, -1),
735 (4, -1),
736 (4, 2),
737 (-1, 3),
738 (3, -2),
739 ];
740 for (dx, dy) in outside {
741 assert_eq!(s.rim_index(dx, dy), None, "({dx}, {dy})");
742 assert_eq!(s.terrain_near(dx, dy), None, "({dx}, {dy})");
743 }
744 let id = |pos: Pos| u16::try_from(10 * pos.x + pos.y + 2).ok().map(TerrainId);
746 s.set_terrain(|pos| id(pos).filter(|_| pos != p(6, -1) && pos != p(8, 0)));
747 assert_eq!(s.terrain_at(p(5, -2)), Some(TerrainId(50)));
748 assert_eq!(s.terrain_at(p(7, -1)), Some(TerrainId(71)));
749 assert_eq!(s.terrain_at(p(6, -1)), None);
751 assert_eq!(s.terrain_near(1, 1), None);
752 assert_eq!(s.terrain_near(3, 2), None);
753 for (dx, dy) in ring {
754 let pos = p(5 + dx, -2 + dy);
755 let expect = id(pos).filter(|_| pos != p(8, 0));
756 assert_eq!(s.terrain_near(dx, dy), expect, "({dx}, {dy})");
757 }
758 assert_eq!(s.terrain_near(0, 0), Some(TerrainId(50)));
759 assert_eq!(s.terrain_near(2, 1), Some(TerrainId(71)));
760 assert_eq!(s.terrain_near(-1, -1), Some(TerrainId(39)));
761 assert_eq!(s.terrain_near(3, 1), Some(TerrainId(81)));
762 s.rim.truncate(3);
764 assert_eq!(s.terrain_near(1, -1), Some(TerrainId(59)));
765 assert_eq!(s.terrain_near(2, -1), None);
766 s.set_terrain(|_| Some(TerrainId(1)));
767 assert_eq!(s.rim, vec![Some(TerrainId(1)); 3]);
768 }
769
770 #[test]
771 fn the_map_goes_on_past_the_sides_the_view_leaves_map_beyond() {
772 let edges = |left, right, up, down| MapEdges {
773 x: Ends {
774 before: left,
775 after: right,
776 },
777 y: Ends {
778 before: up,
779 after: down,
780 },
781 };
782 let wide = |x| MapEdges::of_view(p(x, 0), (8, 4), (20, 4));
785 assert_eq!(wide(0), edges(false, true, false, false));
786 assert_eq!(wide(6), edges(true, true, false, false));
787 assert_eq!(wide(12), edges(true, false, false, false));
788 let tall = |y| MapEdges::of_view(p(0, y), (5, 10), (5, 30));
790 assert_eq!(tall(0), edges(false, false, false, true));
791 assert_eq!(tall(10), edges(false, false, true, true));
792 assert_eq!(tall(20), edges(false, false, true, false));
793 let both = MapEdges::of_view(p(3, 2), (4, 4), (10, 10));
795 assert_eq!(both, edges(true, true, true, true));
796 let small = MapEdges::of_view(p(-3, -2), (12, 8), (6, 4));
798 assert_eq!(small, MapEdges::NONE);
799 assert!(!small.any());
800 assert_eq!(MapEdges::default(), MapEdges::NONE);
801 let far = MapEdges::of_view(p(i32::MAX, i32::MAX), (i32::MAX, i32::MAX), (1, 1));
803 assert_eq!(far, edges(true, false, true, false));
804 let empty = MapEdges::of_view(p(0, 0), (-4, -4), (1, 1));
805 assert_eq!(empty, edges(false, true, false, true));
806 for one in [
808 edges(true, false, false, false),
809 edges(false, true, false, false),
810 edges(false, false, true, false),
811 edges(false, false, false, true),
812 ] {
813 assert!(one.any(), "{one:?}");
814 assert_eq!(one.names().len(), 1, "{one:?}");
815 }
816 let all = edges(true, true, true, true).names();
817 assert_eq!(all, ["left", "right", "up", "down"]);
818 }
819
820 #[test]
821 fn the_text_lists_the_sides_the_map_goes_on_past_only_when_there_are_some() {
822 let mut s = MapScene::<Piece>::new(p(0, -1), (2, 1));
823 let plain = s.to_text(&Terrain);
824 assert!(!plain.contains("more"), "{plain}");
825 s.more = MapEdges::of_view(p(0, 0), s.size, (2, 1));
827 assert_eq!(s.to_text(&Terrain), plain);
828 let after = Ends {
829 before: false,
830 after: true,
831 };
832 s.more = MapEdges { x: after, y: after };
833 assert_eq!(
834 s.to_text(&Terrain),
835 "origin (0,-1) size 2x1\nmore right down\n -1: -*2\nunits 0\ncursor -\npath -\n"
836 );
837 }
838
839 #[test]
840 fn the_text_names_a_look_other_than_the_usual_one() {
841 let mut s = MapScene::<Piece>::new(p(0, -1), (2, 1));
842 let text = s.to_text(&Terrain);
843 assert!(text.starts_with("origin (0,-1) size 2x1\n"), "{text}");
844 s.look.tiles = "indoor".to_owned();
845 let text = s.to_text(&Terrain);
846 let first = "origin (0,-1) size 2x1 look indoor\n";
847 assert!(text.starts_with(first), "{text}");
848 s.look.light = "dusk".to_owned();
850 let text = s.to_text(&Terrain);
851 let first = "origin (0,-1) size 2x1 look indoor light dusk\n";
852 assert!(text.starts_with(first), "{text}");
853 s.look.tiles = "outdoor".to_owned();
854 let text = s.to_text(&Terrain);
855 assert!(
856 text.starts_with("origin (0,-1) size 2x1 light dusk\n"),
857 "{text}"
858 );
859 }
860
861 #[test]
862 fn tints_go_on_visible_tiles_in_the_order_laid_on() {
863 let mut s = MapScene::<Piece>::new(p(1, 1), (3, 2));
864 s.tint([p(1, 1), p(3, 2), p(0, 0), p(4, 1)], DANGER);
865 s.tint([p(3, 2), p(2, 1)], MOVE);
866 assert_eq!(s.tints_at(p(1, 1)), [DANGER]);
867 assert_eq!(s.tints_at(p(3, 2)), [DANGER, MOVE]);
868 let found = s.tints_at(p(2, 2));
869 assert!(found.is_empty(), "{found:?}");
870 let found = s.tints_at(p(0, 0));
871 assert!(found.is_empty(), "{found:?}");
872 assert_eq!(s.tinted(DANGER), [p(1, 1), p(3, 2)]);
873 assert_eq!(s.tinted(MOVE), [p(2, 1), p(3, 2)]);
874 let found = s.tinted(ATTACK);
875 assert!(found.is_empty(), "{found:?}");
876 let layer = |kind, tiles: &[Pos]| RangeLayer {
878 kind,
879 tiles: tiles.to_vec(),
880 };
881 assert_eq!(
882 s.ranges,
883 [
884 layer(DANGER, &[p(1, 1), p(3, 2)]),
885 layer(MOVE, &[p(3, 2), p(2, 1)]),
886 ]
887 );
888 let before = s.clone();
890 s.tint([p(0, 0), p(9, 9)], HEAL);
891 s.tint([], HEAL);
892 assert_eq!(s, before);
893 s.tint([p(2, 2), p(2, 2)], HEAL);
896 s.tint([p(2, 2)], DANGER);
897 assert_eq!(s.tints_at(p(2, 2)), [HEAL, HEAL, DANGER]);
898 assert_eq!(s.tinted(HEAL), [p(2, 2)]);
899 assert_eq!(s.tinted(DANGER), [p(1, 1), p(2, 2), p(3, 2)]);
901 }
902
903 #[test]
904 fn tints_are_read_by_the_tiles_place_in_the_view() {
905 let mut s = MapScene::<Piece>::new(p(1, 1), (3, 2));
906 s.tint([p(3, 1), p(1, 2)], MOVE);
907 s.tint([p(1, 2)], HEAL);
908 let tints = s.tints();
909 assert_eq!(tints.at(2, 0), [MOVE]);
910 assert_eq!(tints.at(0, 1), [MOVE, HEAL]);
911 let found = tints.at(0, 0);
912 assert!(found.is_empty(), "{found:?}");
913 let far = [
915 (3, 0),
916 (-1, 1),
917 (0, 2),
918 (0, -1),
919 (i32::MAX, 0),
920 (0, i32::MAX),
921 (0, i32::MIN),
922 ];
923 for (dx, dy) in far {
924 assert!(tints.at(dx, dy).is_empty(), "({dx}, {dy})");
925 }
926 s.ranges.push(RangeLayer {
929 kind: DANGER,
930 tiles: vec![p(0, 1), p(4, 1)],
931 });
932 assert_eq!(s.tints(), tints);
933 s.tiles.truncate(3);
935 let short = s.tints();
936 assert_eq!(short.at(2, 0), [MOVE]);
937 let found = short.at(0, 1);
938 assert!(found.is_empty(), "{found:?}");
939 s.tint([p(1, 2)], DANGER);
940 let found = s.tints_at(p(1, 2));
941 assert!(found.is_empty(), "{found:?}");
942 }
943
944 #[test]
945 fn a_ranges_mark_is_its_names_first_letter() {
946 assert_eq!(MOVE.mark(), 'm');
947 assert_eq!(RangeName("Zone").mark(), 'Z');
948 assert_eq!(RangeName("").mark(), '?');
949 }
950
951 #[test]
952 fn only_units_on_visible_tiles_are_kept() {
953 let mut s = MapScene::new(p(1, 1), (3, 2));
954 s.push_unit(piece(4, p(3, 2)));
955 s.push_unit(piece(5, p(4, 2)));
956 s.push_unit(piece(6, p(1, 1)));
957 let ids: Vec<u32> = s.units.iter().map(|u| u.id).collect();
958 assert_eq!(ids, [4, 6]);
959 assert_eq!(s.unit(6).map(|u| u.pos), Some(p(1, 1)));
960 assert_eq!(s.unit(5), None);
961 }
962
963 #[test]
964 fn tiles_answer_what_is_on_them() {
965 let mut s = MapScene::new(p(1, 1), (3, 2));
966 s.tile_mut(p(2, 1)).unwrap().terrain = Some(TerrainId(4));
967 s.tint([p(2, 1)], DANGER);
968 s.tint([p(2, 1)], MOVE);
969 s.push_unit(piece(4, p(2, 1)));
970 s.push_unit(piece(6, p(3, 2)));
971 s.push_unit(piece(7, p(3, 2)));
972 assert_eq!(s.unit_at(p(2, 1)).map(|u| u.id), Some(4));
973 assert_eq!(s.unit_at(p(3, 2)).map(|u| u.id), Some(7));
974 assert_eq!(s.unit_at(p(1, 1)), None);
975 assert_eq!(s.tints_at(p(2, 1)), [DANGER, MOVE]);
976 let found = s.tints_at(p(1, 1));
977 assert!(found.is_empty(), "{found:?}");
978 let found = s.tints_at(p(0, 0));
979 assert!(found.is_empty(), "{found:?}");
980 assert_eq!(s.terrain_at(p(2, 1)), Some(TerrainId(4)));
981 assert_eq!(s.terrain_at(p(1, 1)), None);
982 assert_eq!(s.terrain_at(p(9, 9)), None);
983 assert_eq!(s.cursor_tile(), None);
984 s.cursor = Some(CursorView {
985 pos: p(3, 2),
986 brightness: 1.0,
987 style: CursorStyle::Corners,
988 });
989 assert_eq!(s.cursor_tile(), Some(p(3, 2)));
990 }
991
992 #[test]
993 fn to_text_lists_tiles_units_cursor_and_path() {
994 let mut s = MapScene::new(p(-1, 2), (5, 3));
995 for pos in [p(0, 2), p(1, 2), p(2, 2), p(0, 3), p(1, 3)] {
996 s.tile_mut(pos).unwrap().terrain = Some(GRASS);
997 }
998 s.tile_mut(p(3, 2)).unwrap().terrain = Some(SAND);
999 s.tile_mut(p(2, 3)).unwrap().terrain = Some(TerrainId(999));
1000 s.tint([p(1, 2), p(2, 2)], MOVE);
1001 s.tint([p(3, 2)], DANGER);
1002 s.tint([p(3, 2)], ATTACK);
1003 s.tint([p(0, 3)], HEAL);
1004 s.tile_mut(p(1, 3)).unwrap().flashes.push(0.5);
1005 s.tile_mut(p(0, 2)).unwrap().becomes = Some(WATER);
1006 s.tile_mut(p(2, 2)).unwrap().becomes = Some(TerrainId(998));
1007 s.push_unit(piece(4, p(0, 2)));
1008 s.push_unit(Piece::new(1, 'a', p(1, 3)));
1009 s.cursor = Some(CursorView {
1010 pos: p(1, 3),
1011 brightness: 0.756,
1012 style: CursorStyle::Corners,
1013 });
1014 s.path = vec![p(1, 3), p(1, 2)];
1015 assert_eq!(
1016 s.to_text(&Terrain),
1017 "origin (-1,2) size 5x3\n\
1018 \x20 2: - grass>water grass+m grass+m>?998 sand+da\n\
1019 \x20 3: - grass+h grass! ?999 -\n\
1020 \x20 4: -*5\n\
1021 units 2\n\
1022 \x20 #4 k (0,2)\n\
1023 \x20 #1 a (1,3)\n\
1024 cursor (1,3) corners 0.76\n\
1025 path (1,3) (1,2)\n"
1026 );
1027 s.threat = Some(ThreatView {
1029 tile: p(1, 2),
1030 enemies: vec![p(0, 2), p(9, -4)],
1031 });
1032 let text = s.to_text(&Terrain);
1033 let end = "path (1,3) (1,2)\nthreat (1,2) from (0,2) (9,-4)\n";
1034 assert!(text.ends_with(end), "{text}");
1035 }
1036
1037 #[test]
1038 fn to_text_of_an_empty_scene_and_every_cursor_style() {
1039 let mut s = MapScene::<Piece>::new(p(0, -1), (2, 1));
1040 assert_eq!(
1041 s.to_text(&Terrain),
1042 "origin (0,-1) size 2x1\n -1: -*2\nunits 0\ncursor -\npath -\n"
1043 );
1044 s.tiles.truncate(1);
1046 assert!(s.to_text(&Terrain).contains(" -1: - !\n"));
1047 let styles = [
1048 (CursorStyle::Corners, "corners"),
1049 (CursorStyle::LargeCorners, "large-corners"),
1050 (CursorStyle::TileGlow, "glow"),
1051 ];
1052 for (style, name) in styles {
1053 assert_eq!(style.name(), name);
1054 s.cursor = Some(CursorView {
1055 pos: p(1, -1),
1056 brightness: 1.0,
1057 style,
1058 });
1059 let line = format!("cursor (1,-1) {name} 1.00\n");
1060 assert!(s.to_text(&Terrain).contains(&line), "{name}");
1061 }
1062 assert_eq!(CursorStyle::default(), CursorStyle::Corners);
1063 }
1064
1065 #[test]
1066 fn a_new_cursor_is_the_literal() {
1067 assert_eq!(
1068 CursorView::new(Pos::new(2, 3), 0.5, CursorStyle::TileGlow),
1069 CursorView {
1070 pos: Pos::new(2, 3),
1071 brightness: 0.5,
1072 style: CursorStyle::TileGlow,
1073 }
1074 );
1075 }
1076}