1use crate::prelude::*;
15use shuyi_core::Pos;
16
17use super::layout::Grid;
18use super::scene::{MapScene, ThreatView};
19use crate::frame::color::Rgb;
20use crate::frame::glyph_buffer::{GlyphBuffer, Layer, Overlay, PxRect, Rect};
21
22pub const TIP_SHORT: f32 = 3.0;
25
26pub const BARB_LEN: f32 = 4.0;
28
29const BARB_TURN: (f32, f32) = (0.82, 0.57);
32
33pub const MARK_W: i32 = 3;
35
36pub const MARK_BAR_H: i32 = 6;
38const MARK_GAP_H: i32 = 1;
39const MARK_DOT_H: i32 = 2;
40
41pub const MARK_ABOVE: i32 = 3;
45
46pub const MARK_H: i32 = MARK_BAR_H + MARK_GAP_H + MARK_DOT_H;
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum Start {
52 Middle,
54 Edge,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct Colors {
62 pub arrow: Rgb,
64 pub mark: Rgb,
66 pub edge: Rgb,
68}
69
70fn line(a: (i32, i32), b: (i32, i32)) -> Vec<PxRect> {
74 let (dx, dy) = ((b.0 - a.0).abs(), (b.1 - a.1).abs());
75 let (sx, sy) = ((b.0 - a.0).signum(), (b.1 - a.1).signum());
76 let flat = dx >= dy;
77 let (long, short) = if flat { (dx, dy) } else { (dy, dx) };
78 let run = |from: (i32, i32), to: (i32, i32)| {
80 let (x, y) = (from.0.min(to.0), from.1.min(to.1));
81 Rect::new(x, y, (to.0 - from.0).abs() + 1, (to.1 - from.1).abs() + 1)
82 };
83 let mut runs = Vec::new();
84 let (mut from, mut at) = (a, a);
85 let mut err = long / 2;
86 for _ in 0..long {
87 err -= short;
88 let turn = err < 0;
89 if turn {
90 err += long;
91 runs.push(run(from, at));
92 }
93 at = match (flat, turn) {
94 (true, false) => (at.0 + sx, at.1),
95 (false, false) => (at.0, at.1 + sy),
96 (_, true) => (at.0 + sx, at.1 + sy),
97 };
98 if turn {
99 from = at;
100 }
101 }
102 runs.push(run(from, at));
103 runs
104}
105
106const fn grown(r: PxRect) -> PxRect {
108 Rect::new(r.x - 1, r.y - 1, r.w + 2, r.h + 2)
109}
110
111const FAR: f32 = 32_000.0;
114
115#[allow(clippy::cast_possible_truncation)] fn pixel(x: f32, y: f32) -> (i32, i32) {
118 let whole = |v: f32| v.round().clamp(-FAR, FAR) as i32;
119 (whole(x), whole(y))
120}
121
122fn arrow(from: Pos, to: Pos, art: &Grid, start: Start) -> Vec<PxRect> {
127 let middle = |tile: Pos| {
128 let (x, y) = art.tile_px(tile);
129 (x + art.tile.0 / 2, y + art.tile.1 / 2)
130 };
131 let (mid, end) = (middle(from), middle(to));
132 #[allow(clippy::cast_precision_loss)] let (dx, dy) = ((end.0 - mid.0) as f32, (end.1 - mid.1) as f32);
134 if from == to {
135 return Vec::new();
136 }
137 let len = dx.hypot(dy);
138 let (ux, uy) = (dx / len, dy / len);
140 #[allow(clippy::cast_precision_loss)]
141 let (tx, ty) = (end.0 as f32 - ux * TIP_SHORT, end.1 as f32 - uy * TIP_SHORT);
142 let tip = pixel(tx, ty);
143 let (cos, sin) = BARB_TURN;
144 let barb = |sin: f32| {
146 let (bx, by) = (-ux * cos + uy * sin, -uy * cos - ux * sin);
147 pixel(tx + bx * BARB_LEN, ty + by * BARB_LEN)
148 };
149 let first = match start {
150 Start::Middle => mid,
151 Start::Edge => {
152 let (x, y) = art.tile_px(from);
154 let tile = Rect::new(x, y, art.tile.0, art.tile.1);
155 #[allow(clippy::cast_precision_loss)]
156 let along = |k: i32| pixel(mid.0 as f32 + ux * k as f32, mid.1 as f32 + uy * k as f32);
157 let reach = art.tile.0.max(art.tile.1);
158 let out = (1..=reach).map(along).find(|&(x, y)| !tile.contains(x, y));
159 out.unwrap_or(mid)
160 }
161 };
162 let mut rects = line(first, tip);
163 rects.extend(line(tip, barb(sin)));
164 rects.extend(line(tip, barb(-sin)));
165 rects
166}
167
168fn mark(tile: Pos, art: &Grid) -> [PxRect; 2] {
173 let (x, y) = art.tile_px(tile);
174 let x = x + (art.tile.0 - MARK_W) / 2;
175 let top = (y - MARK_ABOVE - MARK_H).max(1);
177 [
178 Rect::new(x, top, MARK_W, MARK_BAR_H),
179 Rect::new(x, top + MARK_BAR_H + MARK_GAP_H, MARK_W, MARK_DOT_H),
180 ]
181}
182
183pub fn threat_overlays(
188 threat: &ThreatView,
189 grid: &Grid,
190 start: Start,
191 colors: Colors,
192) -> Vec<Overlay> {
193 let view = grid.bounds();
194 let art = grid.at_art_size();
195 let mut out = Vec::new();
196 let mut add = |rect: PxRect, color: Rgb| {
197 if let Some(rect) = grid.zoomed(grid.corner, rect).intersect(&view) {
198 out.push(Overlay::new(rect, color, Layer::Over));
199 }
200 };
201 let arrows: Vec<PxRect> = threat
202 .enemies
203 .iter()
204 .flat_map(|&enemy| arrow(enemy, threat.tile, &art, start))
205 .collect();
206 for &rect in &arrows {
207 add(grown(rect), colors.edge);
208 }
209 for &rect in &arrows {
210 add(rect, colors.arrow);
211 }
212 let shown = threat.enemies.iter().filter(|&&e| art.offset(e).is_some());
213 let marks: Vec<[PxRect; 2]> = shown.map(|&enemy| mark(enemy, &art)).collect();
214 for [bar, _] in &marks {
215 let both = Rect::new(bar.x, bar.y, MARK_W, MARK_H);
217 add(grown(both), colors.edge);
218 }
219 for part in marks.iter().flatten() {
220 add(*part, colors.mark);
221 }
222 out
223}
224
225pub fn paint<U>(
229 scene: &MapScene<U>,
230 grid: &Grid,
231 start: Start,
232 colors: Colors,
233 buf: &mut GlyphBuffer,
234) {
235 let Some(threat) = &scene.threat else {
236 return;
237 };
238 for overlay in threat_overlays(threat, grid, start, colors) {
239 buf.add_overlay(overlay);
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 const COLORS: Colors = Colors {
248 arrow: Rgb::new(255, 0, 0),
249 mark: Rgb::new(255, 255, 0),
250 edge: Rgb::new(0, 0, 0),
251 };
252
253 fn p(x: i32, y: i32) -> Pos {
254 Pos::new(x, y)
255 }
256
257 fn grid() -> Grid {
259 Grid::top_down(p(0, 0), (0, 0), (16, 16), (10, 10), 1)
260 }
261
262 fn threat(tile: Pos, enemies: &[Pos]) -> ThreatView {
263 ThreatView {
264 tile,
265 enemies: enemies.to_vec(),
266 }
267 }
268
269 fn rects(threat: &ThreatView, grid: &Grid, color: Rgb) -> Vec<(i32, i32, i32, i32)> {
271 let overlays = threat_overlays(threat, grid, Start::Middle, COLORS);
272 assert!(overlays.iter().all(|o| o.layer == Layer::Over));
273 let of = overlays.iter().filter(|o| o.color == color);
274 of.map(|o| (o.rect.x, o.rect.y, o.rect.w, o.rect.h))
275 .collect()
276 }
277
278 fn pixels(rects: &[PxRect]) -> Vec<(i32, i32)> {
280 let all = rects.iter().flat_map(|r| {
281 (r.y..r.y + r.h).flat_map(move |y| (r.x..r.x + r.w).map(move |x| (x, y)))
282 });
283 all.collect()
284 }
285
286 #[test]
287 fn a_line_is_one_rectangle_per_run_of_pixels() {
288 assert_eq!(line((2, 5), (9, 5)), [Rect::new(2, 5, 8, 1)]);
290 assert_eq!(line((9, 5), (2, 5)), [Rect::new(2, 5, 8, 1)]);
291 assert_eq!(line((4, 1), (4, 6)), [Rect::new(4, 1, 1, 6)]);
292 assert_eq!(line((4, 6), (4, 1)), [Rect::new(4, 1, 1, 6)]);
293 assert_eq!(line((3, 3), (3, 3)), [Rect::new(3, 3, 1, 1)]);
294 assert_eq!(
296 line((0, 0), (9, 2)),
297 [
298 Rect::new(0, 0, 3, 1),
299 Rect::new(3, 1, 4, 1),
300 Rect::new(7, 2, 3, 1)
301 ]
302 );
303 assert_eq!(
305 line((9, 0), (0, 2)),
306 [
307 Rect::new(7, 0, 3, 1),
308 Rect::new(3, 1, 4, 1),
309 Rect::new(0, 2, 3, 1)
310 ]
311 );
312 assert_eq!(
314 line((0, 0), (1, 5)),
315 [Rect::new(0, 0, 1, 3), Rect::new(1, 3, 1, 3)]
316 );
317 assert_eq!(
318 line((1, 5), (0, 0)),
319 [Rect::new(1, 3, 1, 3), Rect::new(0, 0, 1, 3)]
320 );
321 let diagonal = line((0, 0), (3, 3));
323 assert_eq!(pixels(&diagonal), [(0, 0), (1, 1), (2, 2), (3, 3)]);
324 assert_eq!(diagonal.len(), 4);
325 }
326
327 #[test]
328 fn a_pixel_is_the_nearest_one_and_never_far_off_the_console() {
329 assert_eq!(pixel(2.4, -2.6), (2, -3));
330 assert_eq!(pixel(2.5, 7.0), (3, 7));
331 assert_eq!(pixel(f32::INFINITY, f32::NEG_INFINITY), (32_000, -32_000));
332 assert_eq!(pixel(1e9, -1e9), (32_000, -32_000));
333 assert_eq!(pixel(f32::NAN, 31_999.6), (0, 32_000));
334 }
335
336 #[test]
337 fn a_line_has_one_pixel_per_step_from_end_to_end() {
338 for (a, b) in [
339 ((0, 0), (40, 13)),
340 ((40, 13), (0, 0)),
341 ((5, 30), (9, -7)),
342 ((-3, 2), (20, 2)),
343 ((7, 7), (-9, 23)),
344 ] {
345 let runs = line(a, b);
346 assert!(runs.iter().all(|r| r.w == 1 || r.h == 1), "{a:?} {b:?}");
347 let px = pixels(&runs);
348 let steps = (b.0 - a.0).abs().max((b.1 - a.1).abs());
349 assert_eq!(px.len(), usize::try_from(steps).unwrap() + 1, "{a:?} {b:?}");
350 assert!(px.contains(&a) && px.contains(&b), "{a:?} {b:?}");
351 for pair in runs.windows(2) {
353 let grown = grown(pair[0]);
354 assert!(grown.intersect(&pair[1]).is_some(), "{pair:?}");
355 assert_eq!(pair[0].intersect(&pair[1]), None, "{pair:?}");
356 }
357 }
358 }
359
360 #[test]
361 fn an_arrow_runs_from_the_enemys_middle_to_just_short_of_the_tiles() {
362 let t = threat(p(1, 1), &[p(4, 1)]);
365 let red = rects(&t, &grid(), COLORS.arrow);
366 assert_eq!(red[0], (27, 24, 46, 1));
369 assert_eq!(
370 &red[1..],
371 [(27, 24, 1, 1), (28, 25, 2, 1), (30, 26, 1, 1)]
372 .into_iter()
373 .chain([(27, 24, 1, 1), (28, 23, 2, 1), (30, 22, 1, 1)])
374 .collect::<Vec<_>>()
375 );
376 let all = threat_overlays(&t, &grid(), Start::Middle, COLORS);
379 let dark: Vec<_> = all.iter().take_while(|o| o.color == COLORS.edge).collect();
380 assert_eq!(dark.len(), red.len());
381 assert_eq!(dark[0].rect, Rect::new(26, 23, 48, 3));
382 for (edge, line) in dark.iter().zip(&red) {
383 let r = edge.rect;
384 assert_eq!((r.x + 1, r.y + 1, r.w - 2, r.h - 2), *line);
385 }
386 let reds = &all[dark.len()..dark.len() + red.len()];
387 assert!(reds.iter().all(|o| o.color == COLORS.arrow));
388 }
389
390 #[test]
391 fn arrows_point_at_the_tile_from_any_side() {
392 let tip = |enemy: Pos| {
395 let px = pixels(&arrow(enemy, p(4, 4), &grid(), Start::Middle));
396 assert!(px.contains(&(enemy.x * 16 + 8, enemy.y * 16 + 8)));
398 let thrice = |q: &&(i32, i32)| px.iter().filter(|r| r == q).count() == 3;
399 *px.iter().find(thrice).unwrap()
400 };
401 assert_eq!(tip(p(7, 4)), (75, 72));
402 assert_eq!(tip(p(1, 4)), (69, 72));
403 assert_eq!(tip(p(4, 1)), (72, 69));
404 assert_eq!(tip(p(4, 8)), (72, 75));
405 assert_eq!(tip(p(6, 6)), (74, 74));
407 assert_eq!(tip(p(2, 2)), (70, 70));
408 assert_eq!(tip(p(6, 2)), (74, 70));
409 assert_eq!(tip(p(5, 4)), (75, 72));
411 for start in [Start::Middle, Start::Edge] {
413 let found = arrow(p(4, 4), p(4, 4), &grid(), start);
414 assert!(found.is_empty(), "{found:?}");
415 }
416 let none = threat(p(4, 4), &[p(4, 4)]);
417 let found = rects(&none, &grid(), COLORS.arrow);
418 assert!(found.is_empty(), "{found:?}");
419 }
420
421 #[test]
422 fn barbs_are_four_pixels_back_from_the_tip_on_both_sides() {
423 let t = threat(p(4, 4), &[p(4, 1)]);
424 let red = rects(&t, &grid(), COLORS.arrow);
425 assert_eq!(red[0], (72, 24, 1, 46));
427 let barb = |x: i32, end: i32| [(72, 69, 1, 1), (x, 67, 1, 2), (end, 66, 1, 1)];
429 assert_eq!(red[1..4], barb(73, 74));
430 assert_eq!(red[4..], barb(71, 70));
431 }
432
433 #[test]
434 fn an_arrow_from_the_edge_starts_on_the_first_pixel_off_the_enemys_tile() {
435 let first = |enemy: Pos, start| {
438 let all = arrow(enemy, p(4, 4), &grid(), start);
439 let (x, y) = grid().tile_px(enemy);
440 let tile = Rect::new(x, y, 16, 16);
441 let clear = pixels(&all).iter().all(|&(x, y)| !tile.contains(x, y));
442 assert_eq!(clear, start == Start::Edge, "{enemy:?}");
443 all[0]
444 };
445 assert_eq!(first(p(7, 4), Start::Middle), Rect::new(75, 72, 46, 1));
447 assert_eq!(first(p(7, 4), Start::Edge), Rect::new(75, 72, 37, 1));
449 assert_eq!(first(p(1, 4), Start::Edge), Rect::new(32, 72, 38, 1));
450 assert_eq!(first(p(4, 1), Start::Edge), Rect::new(72, 32, 1, 38));
451 assert_eq!(first(p(4, 8), Start::Edge), Rect::new(72, 75, 1, 53));
452 assert_eq!(first(p(6, 6), Start::Edge), Rect::new(95, 95, 1, 1));
454 assert_eq!(first(p(2, 2), Start::Edge), Rect::new(48, 48, 1, 1));
455 assert_eq!(first(p(5, 4), Start::Edge), Rect::new(75, 72, 5, 1));
457 let barbs = arrow(p(5, 4), p(4, 4), &grid(), Start::Edge);
458 assert_eq!(barbs.len(), 7);
459 let (edge, middle) = (
461 arrow(p(7, 4), p(4, 4), &grid(), Start::Edge),
462 arrow(p(7, 4), p(4, 4), &grid(), Start::Middle),
463 );
464 assert_eq!(edge[1..], middle[1..]);
465 }
466
467 #[test]
468 fn a_mark_sits_above_each_enemy_clear_of_its_head() {
469 let t = threat(p(1, 5), &[p(4, 3), p(6, 5)]);
472 let yellow = rects(&t, &grid(), COLORS.mark);
473 assert_eq!(
474 yellow,
475 [
476 (70, 36, 3, 6),
477 (70, 43, 3, 2),
478 (102, 68, 3, 6),
479 (102, 75, 3, 2)
480 ]
481 );
482 assert_eq!((MARK_H, 48 - MARK_ABOVE - MARK_H), (9, 36));
483 let all = threat_overlays(&t, &grid(), Start::Middle, COLORS);
486 let n = all.len();
487 let tail: Vec<_> = all[n - 6..].iter().map(|o| (o.rect, o.color)).collect();
488 assert_eq!(tail[0], (Rect::new(69, 35, 5, 11), COLORS.edge));
489 assert_eq!(tail[1], (Rect::new(101, 67, 5, 11), COLORS.edge));
490 assert!(tail[2..].iter().all(|&(_, c)| c == COLORS.mark));
491 assert!(all[..n - 6].iter().all(|o| o.color != COLORS.mark));
492 }
493
494 #[test]
495 fn a_mark_on_the_views_top_row_moves_down_to_stay_inside() {
496 let t = threat(p(1, 5), &[p(4, 0)]);
497 let yellow = rects(&t, &grid(), COLORS.mark);
498 assert_eq!(yellow, [(70, 1, 3, 6), (70, 8, 3, 2)]);
499 let dark = rects(&t, &grid(), COLORS.edge);
500 assert_eq!(dark[dark.len() - 1], (69, 0, 5, 11));
501 let t = threat(p(1, 5), &[p(4, 1)]);
503 let yellow = rects(&t, &grid(), COLORS.mark);
504 assert_eq!(yellow, [(70, 4, 3, 6), (70, 11, 3, 2)]);
505 }
506
507 #[test]
508 fn an_enemy_outside_the_view_has_its_arrow_cut_at_the_edge_and_no_mark() {
509 let view = Grid::top_down(p(2, 2), (8, 4), (16, 16), (4, 4), 1);
511 let t = threat(p(3, 3), &[p(8, 3)]);
513 let found = rects(&t, &view, COLORS.mark);
514 assert!(found.is_empty(), "{found:?}");
515 let red = rects(&t, &view, COLORS.arrow);
516 assert_eq!(red[0], (35, 28, 37, 1));
519 let bounds = view.bounds();
520 assert_eq!(bounds, Rect::new(8, 4, 64, 64));
521 for o in threat_overlays(&t, &view, Start::Middle, COLORS) {
522 assert_eq!(o.rect.intersect(&bounds), Some(o.rect), "{o:?}");
523 }
524 let wide = Grid {
526 tiles: (8, 4),
527 ..view
528 };
529 assert_eq!(rects(&t, &wide, COLORS.arrow)[0], (35, 28, 78, 1));
530 assert_eq!(rects(&t, &wide, COLORS.mark).len(), 2);
531 let t = threat(p(0, 3), &[p(4, 3)]);
533 let red = rects(&t, &view, COLORS.arrow);
534 assert_eq!(red, [(8, 28, 41, 1)]);
535 assert_eq!(rects(&t, &view, COLORS.mark).len(), 2);
536 }
537
538 #[test]
539 fn on_a_zoomed_grid_every_pixel_is_a_square_of_the_zooms() {
540 let t = threat(p(1, 3), &[p(4, 1), p(3, 0)]);
541 let once = threat_overlays(&t, &grid(), Start::Edge, COLORS);
542 let zoomed = Grid {
543 corner: (8, 16),
544 tile: (32, 32),
545 scale: 2,
546 ..grid()
547 };
548 let twice = threat_overlays(&t, &zoomed, Start::Edge, COLORS);
549 let doubled: Vec<Overlay> = once
550 .iter()
551 .map(|o| {
552 let r = o.rect;
553 let rect = Rect::new(8 + 2 * r.x, 16 + 2 * r.y, 2 * r.w, 2 * r.h);
554 Overlay::new(rect, o.color, o.layer)
555 })
556 .collect();
557 assert_eq!(twice, doubled);
558 assert!(!twice.is_empty(), "{twice:?}");
559 }
560
561 #[test]
562 fn the_geometry_follows_the_tile_size() {
563 let big = Grid {
565 tile: (24, 20),
566 ..grid()
567 };
568 let t = threat(p(1, 1), &[p(3, 1)]);
569 assert_eq!(rects(&t, &big, COLORS.arrow)[0], (39, 30, 46, 1));
570 assert_eq!(rects(&t, &big, COLORS.mark)[0], (82, 8, 3, 6));
572 }
573
574 #[test]
575 fn paint_adds_the_scenes_threat_over_everything_and_nothing_without_one() {
576 let mut scene = MapScene::<crate::toy::grid::Piece>::new(p(0, 0), (10, 10));
577 let blank = || {
578 let c = crate::frame::glyph_buffer::Cell::new('.', COLORS.edge, COLORS.edge);
579 GlyphBuffer::new(20, 10, c)
580 };
581 let mut buf = blank();
582 paint(&scene, &grid(), Start::Middle, COLORS, &mut buf);
583 assert_eq!(buf, blank());
584 let t = threat(p(1, 1), &[p(4, 2), p(0, 5)]);
585 scene.threat = Some(t.clone());
586 paint(&scene, &grid(), Start::Edge, COLORS, &mut buf);
587 assert_eq!(
588 buf.overlays(),
589 threat_overlays(&t, &grid(), Start::Edge, COLORS)
590 );
591 let found = buf.overlays();
592 assert!(!found.is_empty(), "{found:?}");
593 }
594}