feat(city): style pack, tileset and lighting

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jeffrey Wilcke
2026-10-12 17:37:36 +02:00
parent 0e14cd14fc
commit f005d0c1b2
7 changed files with 753 additions and 0 deletions

View File

@@ -1,5 +1,6 @@
//! Application state, the screen trait and the fixed-rate frame loop. //! Application state, the screen trait and the fixed-rate frame loop.
use crate::render::style::StylePack;
use crate::term::{TerminalGuard, install_panic_hook}; use crate::term::{TerminalGuard, install_panic_hook};
use anyhow::Result; use anyhow::Result;
use citygen::{City, GenParams}; use citygen::{City, GenParams};
@@ -19,6 +20,7 @@ pub const FRAME: Duration = Duration::from_micros(33_333);
pub struct AppState { pub struct AppState {
pub params: GenParams, pub params: GenParams,
pub city: Option<City>, pub city: Option<City>,
pub style: StylePack,
pub tick: u64, pub tick: u64,
status: Option<(String, u64)>, status: Option<(String, u64)>,
} }
@@ -34,6 +36,7 @@ impl AppState {
AppState { AppState {
params: GenParams::default(), params: GenParams::default(),
city: None, city: None,
style: StylePack::load().expect("embedded style pack is valid"),
tick: 0, tick: 0,
status: None, status: None,
} }

View File

@@ -6,6 +6,7 @@
mod anim; mod anim;
mod app; mod app;
mod cli; mod cli;
mod render;
mod save; mod save;
mod screens; mod screens;
mod term; mod term;

View File

@@ -0,0 +1,51 @@
//! Light and visibility applied to cells.
use crate::render::{Cell, Rgb, Visibility};
/// Darkens a colour by the tile's light level; never below a quarter so the map stays legible.
pub fn lit(c: Rgb, light: u8) -> Rgb {
c.scale(0.25 + 0.75 * (light as f32) / 255.0)
}
/// Remembered tiles are dim and static; unseen tiles are black.
pub fn apply_vis(cell: Cell, vis: Visibility) -> Cell {
match vis {
Visibility::Visible => cell,
Visibility::Remembered => Cell {
fg: cell.fg.scale(0.35),
bg: cell.bg.scale(0.35),
bold: false,
dim: true,
..cell
},
Visibility::Unseen => Cell {
ch: ' ',
fg: Rgb(0, 0, 0),
bg: Rgb(0, 0, 0),
bold: false,
dim: false,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::{Cell, Rgb};
#[test]
fn lit_scales_between_quarter_and_full() {
assert_eq!(lit(Rgb(200, 100, 40), 255), Rgb(200, 100, 40));
assert_eq!(lit(Rgb(200, 100, 40), 0), Rgb(50, 25, 10));
let c = Cell {
ch: 'x',
fg: Rgb(200, 200, 200),
bg: Rgb(20, 20, 20),
bold: true,
dim: false,
};
let r = apply_vis(c, Visibility::Remembered);
assert!(r.fg.0 < 100 && r.dim && !r.bold);
assert_eq!(apply_vis(c, Visibility::Visible), c);
}
}

View File

@@ -0,0 +1,112 @@
//! Rendering: colours, cells, the render context and the tileset.
pub mod lighting;
pub mod style;
pub mod tileset;
use citygen::{BuildingKind, ConnectorKind, Pos, StratumKind, Theme};
use ratatui::buffer::Buffer;
use ratatui::style::{Color, Modifier, Style};
use serde::{Deserialize, Serialize};
use style::StylePack;
/// A truecolor value.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Rgb(pub u8, pub u8, pub u8);
impl Rgb {
pub fn to_color(self) -> Color {
Color::Rgb(self.0, self.1, self.2)
}
/// Multiplies every channel, saturating at 255.
pub fn scale(self, f: f32) -> Rgb {
let s = |c: u8| ((c as f32) * f).round().clamp(0.0, 255.0) as u8;
Rgb(s(self.0), s(self.1), s(self.2))
}
/// Linear blend toward `other` by `t` in 0..=1.
pub fn mix(self, other: Rgb, t: f32) -> Rgb {
let t = t.clamp(0.0, 1.0);
let m = |a: u8, b: u8| ((a as f32) + ((b as f32) - (a as f32)) * t).round() as u8;
Rgb(m(self.0, other.0), m(self.1, other.1), m(self.2, other.2))
}
/// Adds a signed offset per channel, clamped.
pub fn tint(self, dr: i16, dg: i16, db: i16) -> Rgb {
let t = |c: u8, d: i16| (c as i16 + d).clamp(0, 255) as u8;
Rgb(t(self.0, dr), t(self.1, dg), t(self.2, db))
}
}
/// One rendered terminal cell.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Cell {
pub ch: char,
pub fg: Rgb,
pub bg: Rgb,
pub bold: bool,
pub dim: bool,
}
impl Cell {
pub fn write(&self, buf: &mut Buffer, x: u16, y: u16) {
if x < buf.area.x || y < buf.area.y || x >= buf.area.right() || y >= buf.area.bottom() {
return;
}
let mut style = Style::default()
.fg(self.fg.to_color())
.bg(self.bg.to_color());
if self.bold {
style = style.add_modifier(Modifier::BOLD);
}
if self.dim {
style = style.add_modifier(Modifier::DIM);
}
buf[(x, y)].set_char(self.ch).set_style(style);
}
}
/// What the walker knows about a tile.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Visibility {
Unseen,
Remembered,
Visible,
}
/// A connector under the tile and which way it leads from the current stratum.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ConnDir {
pub kind: ConnectorKind,
pub up: bool,
pub down: bool,
}
/// Everything `tileset::cell` needs besides the tile itself.
pub struct RenderCtx<'a> {
pub style: &'a StylePack,
pub theme: Option<Theme>,
pub stratum: StratumKind,
pub light: u8,
pub tick: u64,
pub pos: Pos,
pub vis: Visibility,
pub connector: Option<ConnDir>,
pub kind: Option<BuildingKind>,
/// Bits N=1, E=2, S=4, W=8: neighbours that connect to this tile (see `tileset::connects`).
pub wall_mask: u8,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rgb_math() {
assert_eq!(Rgb(100, 200, 50).scale(0.5), Rgb(50, 100, 25));
assert_eq!(Rgb(100, 100, 100).scale(10.0), Rgb(255, 255, 255));
assert_eq!(Rgb(0, 0, 0).mix(Rgb(100, 200, 50), 0.5), Rgb(50, 100, 25));
assert_eq!(Rgb(10, 10, 10).tint(-20, 5, 300), Rgb(0, 15, 255));
}
}

View File

@@ -0,0 +1,122 @@
//! The style pack: palettes and glyph sets loaded from `flavor/style.ron`.
use crate::render::Rgb;
use anyhow::{Context, Result};
use citygen::{Hue, StratumKind, Theme};
use serde::{Deserialize, Serialize};
const DEFAULT_STYLE: &str = include_str!("../../../../flavor/style.ron");
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ThemeStyle {
pub accent: Rgb,
pub accent2: Rgb,
pub wall: Rgb,
pub floor: Rgb,
pub road: Rgb,
pub sidewalk: Rgb,
pub roof: Rgb,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct StratumStyle {
pub tint: (i16, i16, i16),
pub fog: bool,
/// 0 none, 1 light, 2 medium, 3 heavy.
pub rain: u8,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GlyphSets {
pub holo: Vec<char>,
pub rain: Vec<char>,
pub water: Vec<char>,
pub steam: Vec<char>,
pub mass: Vec<char>,
pub net: Vec<char>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct StylePack {
pub background: Rgb,
pub neutral: ThemeStyle,
pub themes: Vec<(Theme, ThemeStyle)>,
pub strata: Vec<(StratumKind, StratumStyle)>,
pub hues: Vec<(Hue, Rgb)>,
pub glyphs: GlyphSets,
}
impl StylePack {
/// Loads the embedded default pack.
pub fn load() -> Result<StylePack> {
let pack: StylePack = ron::from_str(DEFAULT_STYLE).context("parsing flavor/style.ron")?;
for set in [
&pack.glyphs.holo,
&pack.glyphs.rain,
&pack.glyphs.water,
&pack.glyphs.steam,
&pack.glyphs.mass,
&pack.glyphs.net,
] {
anyhow::ensure!(!set.is_empty(), "style.ron: a glyph set is empty");
}
Ok(pack)
}
pub fn theme(&self, t: Theme) -> &ThemeStyle {
self.themes
.iter()
.find(|(k, _)| *k == t)
.map(|(_, s)| s)
.unwrap_or(&self.neutral)
}
pub fn theme_or_neutral(&self, t: Option<Theme>) -> &ThemeStyle {
match t {
Some(t) => self.theme(t),
None => &self.neutral,
}
}
pub fn stratum(&self, k: StratumKind) -> &StratumStyle {
static FALLBACK: StratumStyle = StratumStyle {
tint: (0, 0, 0),
fog: false,
rain: 0,
};
self.strata
.iter()
.find(|(s, _)| *s == k)
.map(|(_, s)| s)
.unwrap_or(&FALLBACK)
}
pub fn hue(&self, h: Hue) -> Rgb {
self.hues
.iter()
.find(|(k, _)| *k == h)
.map(|(_, c)| *c)
.unwrap_or(Rgb(255, 255, 255))
}
}
#[cfg(test)]
mod tests {
use super::*;
use citygen::{Hue, StratumKind, Theme};
#[test]
fn style_pack_loads_and_covers_every_theme() {
let s = StylePack::load().unwrap();
for t in Theme::ALL {
let _ = s.theme(t);
}
for k in StratumKind::PHYSICAL {
let _ = s.stratum(k);
}
assert_ne!(s.hue(Hue::Magenta), s.hue(Hue::Cyan));
assert!(s.glyphs.holo.len() >= 8 && s.glyphs.rain.len() >= 4);
assert_eq!(s.stratum(StratumKind::Undercity).rain, 0);
assert!(s.stratum(StratumKind::Heights).rain >= 3);
}
}

View File

@@ -0,0 +1,429 @@
//! The tileset: one pure function from a tile and its context to a styled cell.
use crate::anim::effects::{flicker, pulse};
use crate::render::lighting::lit;
use crate::render::style::StylePack;
use crate::render::{Cell, ConnDir, RenderCtx, Rgb, Visibility};
use citygen::{BuildingKind, ConnectorKind, Feature, StratumKind, Terrain, Tile};
const LIGHT: [char; 16] = [
'─', '│', '─', '└', '│', '│', '┌', '├', '─', '┘', '─', '┴', '┐', '┤', '┬', '┼',
];
const HEAVY: [char; 16] = [
'━', '┃', '━', '┗', '┃', '┃', '┏', '┣', '━', '┛', '━', '┻', '┓', '┫', '┳', '╋',
];
const DOUBLE: [char; 16] = [
'═', '║', '═', '╚', '║', '║', '╔', '╠', '═', '╝', '═', '╩', '╗', '╣', '╦', '╬',
];
pub const MASK_N: u8 = 1;
pub const MASK_E: u8 = 2;
pub const MASK_S: u8 = 4;
pub const MASK_W: u8 = 8;
/// Whether two adjacent terrains draw as one continuous structure (walls, walkways).
pub fn connects(a: Terrain, b: Terrain) -> bool {
use Terrain::*;
let wallish = |t| matches!(t, Wall | Door | Window);
let spanish = |t| matches!(t, Walkway | Bridge);
(wallish(a) && wallish(b))
|| (spanish(a) && (spanish(b) || matches!(b, Door | Floor | Stair | Lift | Ladder)))
}
fn wall_set(kind: Option<BuildingKind>) -> &'static [char; 16] {
match kind {
Some(k) if k.is_corp() => &DOUBLE,
Some(
BuildingKind::Apartment
| BuildingKind::Hotel
| BuildingKind::Factory
| BuildingKind::Warehouse,
) => &HEAVY,
_ => &LIGHT,
}
}
/// The glyph for a tile, before colour. Pure in all of its inputs.
#[allow(clippy::too_many_arguments)]
pub fn glyph_for(
terrain: Terrain,
feature: Option<Feature>,
connector: Option<ConnDir>,
kind: Option<BuildingKind>,
variant: u8,
tick: u64,
mask: u8,
style: &StylePack,
) -> char {
let g = &style.glyphs;
let pick = |set: &[char], i: u64| set[(i % set.len() as u64) as usize];
if let Some(f) = feature {
return match f {
Feature::Neon(_) => '▪',
Feature::HoloAd => pick(&g.holo, tick / 8 + variant as u64),
Feature::Lamp => '●',
Feature::Vent => {
if (tick / 4 + variant as u64) % 10 < 3 {
pick(&g.steam, tick / 4 + variant as u64)
} else {
'☰'
}
}
Feature::Puddle => '~',
Feature::Trash => '%',
Feature::Barrier => '▬',
Feature::Stall => '▤',
Feature::Sign(c) => c,
Feature::Terminal => '▣',
Feature::Camera => {
if (tick / 15).is_multiple_of(4) {
'○'
} else {
'◉'
}
}
Feature::AcUnit => '▩',
Feature::Helipad => 'H',
Feature::Pipe => '╌',
Feature::Cable => '╌',
};
}
let horizontal = mask & (MASK_E | MASK_W) != 0;
match terrain {
Terrain::Void => ' ',
Terrain::Water => pick(&g.water, tick / 6 + variant as u64),
Terrain::Sewage => '≈',
Terrain::Road => {
if variant.is_multiple_of(16) {
'·'
} else {
' '
}
}
Terrain::Sidewalk => '░',
Terrain::Alley => ' ',
Terrain::Plaza => {
if variant.is_multiple_of(3) {
'▫'
} else {
' '
}
}
Terrain::Floor => {
if variant.is_multiple_of(23) {
'·'
} else {
' '
}
}
Terrain::Wall => wall_set(kind)[(mask & 15) as usize],
Terrain::Door => '▯',
Terrain::Window => '▫',
Terrain::Rooftop => pick(&g.mass, variant as u64),
Terrain::Walkway => {
if horizontal {
'═'
} else {
'║'
}
}
Terrain::Bridge => {
if horizontal {
'╍'
} else {
'╏'
}
}
Terrain::Tunnel => {
if variant.is_multiple_of(19) {
'.'
} else {
' '
}
}
Terrain::Rail => {
if (tick / 20).is_multiple_of(2) {
'╬'
} else {
'╫'
}
}
Terrain::Stair => match connector {
Some(ConnDir {
up: true,
down: true,
..
}) => '⇅',
Some(ConnDir { down: true, .. }) => '▼',
_ => '▲',
},
Terrain::Lift => {
if (tick / 10).is_multiple_of(2) {
'≡'
} else {
'⇅'
}
}
Terrain::Ladder => match connector {
Some(ConnDir {
kind: ConnectorKind::Manhole,
..
}) => '◍',
_ => '╫',
},
Terrain::Grate => '▦',
}
}
/// Legend rows for a stratum: glyph and label.
pub fn legend_entries(k: StratumKind) -> Vec<(char, &'static str)> {
let common = [
('▲', "stairs"),
('≡', "lift"),
('╫', "ladder"),
('▣', "terminal"),
('●', "lamp"),
];
let mut out: Vec<(char, &'static str)> = match k {
StratumKind::Undercity => vec![
(' ', "tunnel"),
('≈', "sewage"),
('▦', "grate"),
('╬', "rail"),
('◍', "manhole"),
('╌', "pipe"),
('☰', "vent"),
],
StratumKind::Street => vec![
(' ', "road"),
('░', "sidewalk"),
('▫', "plaza/window"),
('│', "wall"),
('▯', "door"),
('▪', "neon"),
('▣', "holo ad"),
('▬', "barrier"),
('▤', "stall"),
('◉', "camera"),
('~', "puddle/water"),
],
StratumKind::Skyways => vec![
('═', "walkway"),
('╌', "cable"),
('│', "wall"),
('▯', "door"),
],
StratumKind::Heights => vec![
('█', "rooftop"),
('╍', "bridge"),
('H', "helipad"),
('▩', "ac unit"),
],
StratumKind::Net => vec![
('◇', "node"),
('◆', "server"),
('⬡', "backbone"),
('◈', "ice"),
],
};
out.extend(common);
out
}
/// Base colours for a terrain within a theme: (fg, bg).
fn terrain_colors(terrain: Terrain, ctx: &RenderCtx) -> (Rgb, Rgb) {
let ts = ctx.style.theme_or_neutral(ctx.theme);
let bg0 = ctx.style.background;
match terrain {
Terrain::Void => (bg0, bg0),
Terrain::Water => (Rgb(50, 110, 170), Rgb(8, 22, 48)),
Terrain::Sewage => (Rgb(70, 150, 70), Rgb(10, 28, 12)),
Terrain::Road => (ts.road.scale(1.8), ts.road),
Terrain::Sidewalk => (ts.sidewalk, ts.road.scale(1.1)),
Terrain::Alley => (ts.road.scale(1.6), ts.road.scale(1.25)),
Terrain::Plaza => (ts.accent2.scale(0.5), ts.sidewalk.scale(0.85)),
Terrain::Floor => (ts.floor.scale(1.6), ts.floor),
Terrain::Wall => (ts.wall, bg0),
Terrain::Door => (ts.accent2, ts.floor),
Terrain::Window => (ts.accent.scale(0.75), bg0),
Terrain::Rooftop => (ts.roof, ts.roof.scale(0.4)),
Terrain::Walkway => (ts.accent.scale(0.85), Rgb(18, 22, 30)),
Terrain::Bridge => (ts.wall.scale(1.3), Rgb(18, 22, 30)),
Terrain::Tunnel => (Rgb(48, 58, 48), Rgb(13, 16, 14)),
Terrain::Rail => (Rgb(140, 125, 95), Rgb(13, 16, 14)),
Terrain::Stair | Terrain::Lift | Terrain::Ladder => (ts.accent2, ts.floor),
Terrain::Grate => (Rgb(100, 112, 100), Rgb(13, 16, 14)),
}
}
/// Feature foreground colour, if the feature overrides it.
fn feature_color(f: Feature, tile: &Tile, ctx: &RenderCtx, tick: u64) -> Rgb {
let ts = ctx.style.theme_or_neutral(ctx.theme);
match f {
Feature::Neon(h) => {
ctx.style
.hue(h)
.scale(flicker(tick, tile.variant, tile.variant.is_multiple_of(5)))
}
Feature::HoloAd => ts
.accent2
.scale(0.7 + 0.3 * pulse(tick + tile.variant as u64, 24)),
Feature::Lamp => Rgb(255, 240, 200),
Feature::Vent => Rgb(170, 175, 185),
Feature::Puddle => Rgb(80, 115, 165),
Feature::Trash => Rgb(115, 105, 85),
Feature::Barrier => Rgb(255, 60, 60),
Feature::Stall => ts.accent,
Feature::Sign(_) => ts.accent2,
Feature::Terminal => Rgb(0, 230, 210),
Feature::Camera => Rgb(255, 60, 70),
Feature::AcUnit => Rgb(125, 135, 145),
Feature::Helipad => Rgb(230, 230, 130),
Feature::Pipe => Rgb(125, 105, 85),
Feature::Cable => Rgb(85, 85, 95),
}
}
/// The styled cell for a tile. Remembered tiles freeze at tick 0.
pub fn cell(tile: &Tile, ctx: &RenderCtx) -> Cell {
let tick = if ctx.vis == Visibility::Remembered {
0
} else {
ctx.tick
};
let ch = glyph_for(
tile.terrain,
tile.feature,
ctx.connector,
ctx.kind,
tile.variant,
tick,
ctx.wall_mask,
ctx.style,
);
let (mut fg, mut bg) = terrain_colors(tile.terrain, ctx);
if let Some(f) = tile.feature {
fg = feature_color(f, tile, ctx, tick);
}
let (dr, dg, db) = ctx.style.stratum(ctx.stratum).tint;
fg = fg.tint(dr, dg, db);
bg = bg.tint(dr, dg, db);
let emits = matches!(
tile.feature,
Some(
Feature::Neon(_)
| Feature::HoloAd
| Feature::Lamp
| Feature::Terminal
| Feature::Camera
)
);
let fg = if emits { fg } else { lit(fg, ctx.light) };
let bg = lit(bg, ctx.light);
let bold =
tile.is_connector() || matches!(tile.feature, Some(Feature::Sign(_) | Feature::Neon(_)));
Cell {
ch,
fg,
bg,
bold,
dim: false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::lighting::apply_vis;
use crate::render::style::StylePack;
use crate::render::{RenderCtx, Visibility};
use citygen::{
ALL_FEATURES, ALL_TERRAIN, Feature, Hue, Pos, StratumKind, Terrain, Theme, Tile,
};
use unicode_width::UnicodeWidthChar;
#[test]
fn style_glyphs_are_single_width() {
let s = StylePack::load().unwrap();
for set in [
&s.glyphs.holo,
&s.glyphs.rain,
&s.glyphs.water,
&s.glyphs.steam,
&s.glyphs.mass,
&s.glyphs.net,
] {
for c in set {
assert_eq!(c.width(), Some(1), "{c:?}");
}
}
for terrain in ALL_TERRAIN {
for v in [0u8, 77, 200] {
for tick in [0u64, 7, 20] {
for mask in 0..16u8 {
let c = glyph_for(terrain, None, None, None, v, tick, mask, &s);
assert_eq!(c.width(), Some(1), "{terrain:?} mask {mask}");
}
}
}
}
for f in ALL_FEATURES {
for tick in [0u64, 4, 15] {
let c = glyph_for(Terrain::Floor, Some(f), None, None, 1, tick, 0, &s);
assert_eq!(c.width(), Some(1), "{f:?}");
}
}
for k in StratumKind::PHYSICAL {
for (c, label) in legend_entries(k) {
assert_eq!(c.width(), Some(1), "{label}");
}
}
}
#[test]
fn cell_mapping_is_pure_and_lit() {
let s = StylePack::load().unwrap();
let ctx = |light, tick| RenderCtx {
style: &s,
theme: Some(Theme::CorpCore),
stratum: StratumKind::Street,
light,
tick,
pos: Pos::new(1, 1),
vis: Visibility::Visible,
connector: None,
kind: None,
wall_mask: 0,
};
let wall = Tile::new(Terrain::Wall);
assert_eq!(cell(&wall, &ctx(255, 0)), cell(&wall, &ctx(255, 0)));
assert!(cell(&wall, &ctx(255, 0)).fg.0 > cell(&wall, &ctx(0, 0)).fg.0);
let mut neon = Tile::new(Terrain::Wall);
neon.feature = Some(Feature::Neon(Hue::Magenta));
neon.variant = 5; // broken sign: variant.is_multiple_of(5)
let on = cell(&neon, &ctx(200, 0));
let off = cell(&neon, &ctx(200, 117));
assert!(off.fg.0 < on.fg.0 / 2, "on {:?} off {:?}", on.fg, off.fg);
let water = Tile::new(Terrain::Water);
assert_ne!(cell(&water, &ctx(100, 0)).ch, cell(&water, &ctx(100, 6)).ch);
let unseen = apply_vis(cell(&wall, &ctx(255, 0)), Visibility::Unseen);
assert_eq!(unseen.ch, ' ');
let remembered = apply_vis(cell(&wall, &ctx(255, 0)), Visibility::Remembered);
assert!(remembered.fg.0 < cell(&wall, &ctx(255, 0)).fg.0);
assert_eq!(
glyph_for(Terrain::Wall, None, None, None, 0, 0, 0b0101, &s),
'│'
);
assert_eq!(
glyph_for(Terrain::Wall, None, None, None, 0, 0, 0b0110, &s),
'┌'
);
assert_eq!(
glyph_for(Terrain::Wall, None, None, None, 0, 0, 0b1111, &s),
'┼'
);
assert!(connects(Terrain::Wall, Terrain::Door));
assert!(!connects(Terrain::Wall, Terrain::Floor));
assert!(connects(Terrain::Walkway, Terrain::Bridge));
}
}

35
flavor/style.ron Normal file
View File

@@ -0,0 +1,35 @@
// Visual style pack: palettes per theme and stratum, neon hues, glyph sets.
// Every glyph must be exactly one terminal cell wide.
StylePack(
background: (10, 11, 16),
neutral: ThemeStyle(accent: (120, 140, 160), accent2: (200, 210, 220), wall: (90, 100, 115), floor: (28, 32, 40), road: (24, 26, 32), sidewalk: (46, 50, 60), roof: (70, 78, 90)),
themes: [
(CorpCore, ThemeStyle(accent: (0, 229, 255), accent2: (220, 235, 245), wall: (110, 135, 160), floor: (26, 34, 46), road: (26, 29, 36), sidewalk: (52, 58, 70), roof: (80, 95, 115))),
(NightMarket, ThemeStyle(accent: (255, 0, 200), accent2: (255, 150, 40), wall: (130, 95, 80), floor: (40, 30, 26), road: (30, 26, 28), sidewalk: (60, 48, 44), roof: (95, 70, 60))),
(Megablocks, ThemeStyle(accent: (255, 190, 0), accent2: (210, 210, 200), wall: (120, 120, 125), floor: (34, 34, 36), road: (26, 26, 28), sidewalk: (56, 56, 58), roof: (85, 85, 90))),
(CombatZone, ThemeStyle(accent: (255, 40, 60), accent2: (255, 200, 60), wall: (110, 80, 70), floor: (36, 26, 24), road: (28, 22, 20), sidewalk: (54, 42, 38), roof: (80, 60, 55))),
(Docks, ThemeStyle(accent: (0, 200, 190), accent2: (190, 110, 60), wall: (90, 110, 110), floor: (26, 36, 38), road: (24, 30, 32), sidewalk: (46, 58, 60), roof: (70, 90, 92))),
(Industrial, ThemeStyle(accent: (230, 230, 0), accent2: (120, 230, 90), wall: (110, 115, 95), floor: (34, 36, 28), road: (28, 30, 24), sidewalk: (54, 58, 46), roof: (84, 88, 72))),
(NeonStrip, ThemeStyle(accent: (255, 80, 180), accent2: (60, 140, 255), wall: (120, 90, 140), floor: (36, 26, 44), road: (28, 22, 34), sidewalk: (58, 46, 68), roof: (90, 70, 105))),
(OldTown, ThemeStyle(accent: (255, 150, 60), accent2: (90, 200, 140), wall: (130, 115, 90), floor: (40, 36, 28), road: (30, 28, 24), sidewalk: (62, 56, 46), roof: (95, 85, 65))),
],
strata: [
(Undercity, StratumStyle(tint: (-8, 8, -6), fog: true, rain: 0)),
(Street, StratumStyle(tint: (0, 0, 0), fog: false, rain: 1)),
(Skyways, StratumStyle(tint: (0, 2, 8), fog: false, rain: 2)),
(Heights, StratumStyle(tint: (0, 6, 22), fog: false, rain: 3)),
(Net, StratumStyle(tint: (-10, 20, 0), fog: false, rain: 0)),
],
hues: [
(Cyan, (0, 255, 230)), (Magenta, (255, 0, 200)), (Pink, (255, 90, 190)), (Orange, (255, 140, 0)),
(Yellow, (255, 220, 0)), (Green, (90, 255, 130)), (Blue, (70, 130, 255)), (Red, (255, 40, 60)), (White, (240, 240, 255)),
],
glyphs: GlyphSets(
holo: ['▣', '▤', '▥', '▦', '▧', '▨', '▩', '◫', 'ア', 'イ', 'ウ', 'エ', '¥', '$', '€', '§'],
rain: ['⠁', '⠂', '⠄', '⡀', '⢀', '⠈', '⠐', '⠠', '╵', '╷'],
water: ['≈', '~', '∼'],
steam: ['°', '∘', '○'],
mass: ['█', '▓', '▒', '░'],
net: ['◇', '◆', '⬡', '⌗', '▪', '◈'],
),
)