lighting
This commit is contained in:
@@ -424,10 +424,10 @@ fn draw_board_area(frame: &mut Frame, inner: Rect, game: &GameState, ui: &mut Ui
|
||||
);
|
||||
} else {
|
||||
let board = &game.board();
|
||||
// On a dark board this is the player's field of view; on a lit board it's
|
||||
// None (draw everything). Computed once and shared by both widgets so the
|
||||
// board and its speech bubbles agree on what's visible.
|
||||
let fov = board.player_fov();
|
||||
// On a dark board this is the player's lighting (LOS + colored light); on a
|
||||
// lit board it's None (draw everything). Computed once and shared by both
|
||||
// widgets so the board and its speech bubbles agree on what's visible.
|
||||
let fov = board.lighting(game.torch());
|
||||
frame.render_widget(BoardWidget::new(board).fov(fov.as_ref()), inner);
|
||||
frame.render_widget(
|
||||
SpeechBubblesWidget::new(board, &game.speech_bubbles).fov(fov.as_ref()),
|
||||
|
||||
+56
-18
@@ -8,7 +8,7 @@
|
||||
use crate::utils::rgba8_to_color;
|
||||
use color::Rgba8;
|
||||
use kiln_core::Board;
|
||||
use kiln_core::Visibility;
|
||||
use kiln_core::Lighting;
|
||||
use kiln_core::cp437::tile_to_char;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
@@ -27,10 +27,10 @@ pub struct BoardWidget<'a> {
|
||||
/// The board cell (x, y) the viewport scrolls to keep visible. Defaults to
|
||||
/// the player position so play mode requires no extra setup.
|
||||
focus: (i32, i32),
|
||||
/// The player's field of view on a [`dark`](Board::dark) board, if any. When
|
||||
/// `Some`, cells not in view are drawn as darkness instead of their glyph.
|
||||
/// `None` (a lit board) draws every cell normally.
|
||||
fov: Option<&'a Visibility>,
|
||||
/// The player's lighting on a [`dark`](Board::dark) board, if any. When
|
||||
/// `Some`, cells that aren't visible are drawn as darkness and visible cells
|
||||
/// are tinted by the light they receive. `None` (a lit board) draws normally.
|
||||
fov: Option<&'a Lighting>,
|
||||
}
|
||||
|
||||
impl<'a> BoardWidget<'a> {
|
||||
@@ -47,9 +47,10 @@ impl<'a> BoardWidget<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Supplies the player's field of view (see [`Board::player_fov`]). When
|
||||
/// `Some`, cells outside it render as darkness; `None` renders everything.
|
||||
pub fn fov(mut self, fov: Option<&'a Visibility>) -> Self {
|
||||
/// Supplies the player's lighting (see [`Board::lighting`]). When `Some`,
|
||||
/// cells outside view render as darkness and visible cells are tinted by
|
||||
/// their received light; `None` renders everything at full color.
|
||||
pub fn fov(mut self, fov: Option<&'a Lighting>) -> Self {
|
||||
self.fov = fov;
|
||||
self
|
||||
}
|
||||
@@ -147,16 +148,22 @@ impl Widget for BoardWidget<'_> {
|
||||
let bx = off_x + col;
|
||||
let sx = area.x + pad_x + col as u16;
|
||||
|
||||
// On a dark board, a cell outside the player's field of view is
|
||||
// drawn as darkness (blank cell, dark-gray background) instead of
|
||||
// its real glyph. A lit board (`fov == None`) shows every cell.
|
||||
let visible = self.fov.is_none_or(|v| v.is_visible(bx, by));
|
||||
// On a dark board, a cell the player can't see (no sightline or
|
||||
// unlit) is drawn as darkness (blank on dark gray). A visible cell
|
||||
// is drawn with its glyph, tinted by the light it receives. A lit
|
||||
// board (`fov == None`) shows every cell at full color.
|
||||
let visible = self.fov.is_none_or(|l| l.is_visible(bx, by));
|
||||
if let Some(cell) = buf.cell_mut((sx, sy)) {
|
||||
if visible {
|
||||
let glyph = board.glyph_at(bx, by);
|
||||
// Modulate by lighting when the board is dark; pass through otherwise.
|
||||
let (fg, bg) = match self.fov {
|
||||
Some(l) => (l.tint(bx, by, glyph.fg), l.tint(bx, by, glyph.bg)),
|
||||
None => (glyph.fg, glyph.bg),
|
||||
};
|
||||
cell.set_char(tile_to_char(glyph.tile))
|
||||
.set_fg(rgba8_to_color(glyph.fg))
|
||||
.set_bg(rgba8_to_color(glyph.bg));
|
||||
.set_fg(rgba8_to_color(fg))
|
||||
.set_bg(rgba8_to_color(bg));
|
||||
} else {
|
||||
cell.set_char(' ')
|
||||
.set_fg(rgba8_to_color(DARKNESS_BG))
|
||||
@@ -176,6 +183,7 @@ mod tests {
|
||||
use kiln_core::map_file::MapFile;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::widgets::Widget;
|
||||
|
||||
/// Builds a tiny dark board: a 5×1 corridor with the player at the left end
|
||||
@@ -200,8 +208,8 @@ mod tests {
|
||||
#[test]
|
||||
fn dark_board_renders_occluded_cells_as_darkness() {
|
||||
let board = dark_corridor();
|
||||
let fov = board.player_fov();
|
||||
assert!(fov.is_some(), "a dark board must produce a field of view");
|
||||
let fov = board.lighting(10);
|
||||
assert!(fov.is_some(), "a dark board must produce lighting");
|
||||
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
@@ -220,10 +228,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn lit_board_renders_every_cell_normally() {
|
||||
// The same board without `dark` gets no FOV, so nothing is hidden.
|
||||
// The same board without `dark` gets no lighting, so nothing is hidden.
|
||||
let mut board = dark_corridor();
|
||||
board.dark = false;
|
||||
assert!(board.player_fov().is_none());
|
||||
assert!(board.lighting(10).is_none());
|
||||
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
@@ -232,6 +240,36 @@ mod tests {
|
||||
assert_ne!(buf[(4, 0)].bg, rgba8_to_color(DARKNESS_BG));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colored_object_light_tints_nearby_and_darkens_far() {
|
||||
// A dark 6×1 corridor, no player torch: a single red light object at x=2
|
||||
// (radius 2) tints its own cell red; a cell beyond its reach is darkness.
|
||||
let toml = r##"
|
||||
[map]
|
||||
name = "lit"
|
||||
width = 6
|
||||
height = 1
|
||||
dark = true
|
||||
[grid]
|
||||
sparse = [ { x = 2, y = 0, ch = "L" } ]
|
||||
[grid.palette]
|
||||
"L" = { kind = "object", tile = 1, fg = "#ff0000", bg = "#000000", solid = false, light = 2 }
|
||||
"##;
|
||||
let board = Board::try_from(toml::from_str::<MapFile>(toml).unwrap()).unwrap();
|
||||
let fov = board.lighting(0); // no player torch — only the object lights
|
||||
|
||||
let area = Rect::new(0, 0, 6, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
BoardWidget::new(&board).fov(fov.as_ref()).render(area, &mut buf);
|
||||
|
||||
// The light's own cell is visible and tinted red (green/blue killed).
|
||||
let lit = buf[(2, 0)].fg;
|
||||
assert!(matches!(lit, Color::Rgb(r, 0, 0) if r > 0), "light cell tinted red, got {lit:?}");
|
||||
// x=5 is out of the light's radius → darkness.
|
||||
assert_eq!(buf[(5, 0)].symbol(), " ");
|
||||
assert_eq!(buf[(5, 0)].bg, rgba8_to_color(DARKNESS_BG));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn axis_centers_a_small_board() {
|
||||
// 10-cell board in a 20-cell view: no scroll, 5 cells of pad each side.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::render::BoardWidget;
|
||||
use crate::utils::rects_overlap;
|
||||
use kiln_core::game::SpeechBubble;
|
||||
use kiln_core::{Board, Direction, Visibility};
|
||||
use kiln_core::{Board, Direction, Lighting};
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::prelude::{Color, Line, Style, Widget};
|
||||
@@ -19,10 +19,10 @@ pub struct SpeechBubblesWidget<'a> {
|
||||
pub board: &'a Board,
|
||||
/// The active speech bubbles to render.
|
||||
pub bubbles: &'a [SpeechBubble],
|
||||
/// The player's field of view on a dark board, if any. A speaker outside it
|
||||
/// still gets a bubble, but with no tail (like an off-screen speaker) —
|
||||
/// The player's lighting on a dark board, if any. A speaker the player can't
|
||||
/// see still gets a bubble, but with no tail (like an off-screen speaker) —
|
||||
/// you hear the unseen thing without a line pointing to where it hides.
|
||||
fov: Option<&'a Visibility>,
|
||||
fov: Option<&'a Lighting>,
|
||||
}
|
||||
|
||||
/// A fully-resolved bubble placement ready for the three draw passes.
|
||||
@@ -47,9 +47,9 @@ impl<'a> SpeechBubblesWidget<'a> {
|
||||
Self { board, bubbles, fov: None }
|
||||
}
|
||||
|
||||
/// Supplies the player's field of view (see [`Board::player_fov`]). A speaker
|
||||
/// outside it has its tail suppressed; `None` (a lit board) leaves tails as-is.
|
||||
pub fn fov(mut self, fov: Option<&'a Visibility>) -> Self {
|
||||
/// Supplies the player's lighting (see [`Board::lighting`]). A speaker the
|
||||
/// player can't see has its tail suppressed; `None` (a lit board) leaves tails as-is.
|
||||
pub fn fov(mut self, fov: Option<&'a Lighting>) -> Self {
|
||||
self.fov = fov;
|
||||
self
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user