feat: Noita-style humanoid entities with per-part colors

Redesigned entity from solid 5x5 block to humanoid silhouette:

Shape (23 bodies, was 27):
     H H H        (head, 3 wide)
     H H H
   A T T T A      (shoulders + arms, 5 wide)
   A T T T A
     T T T        (torso, 3 wide)
     L   R        (legs, split with gap)
     L   R

Per-part colors (Noita-inspired):
- Player: head=bright yellow, torso=golden, arms=amber, legs=dark gold
- Goblin: head=bright green, torso=green, arms=olive, legs=dark green
- Corpse: desaturated browns

SubBody now has color: [u8; 4] field, set by build_humanoid.
All renderers (terminal, ascii, graphics) use b.color directly.
Fire effect: flickering orange overlay on burning entities.

half_w adjusted from 3.5 to 3.0, half_h from 2.5 to 3.5
(taller shape, narrower than old block).

109 tests, 0 failures
This commit is contained in:
Emil
2026-06-21 10:13:22 +03:00
parent 586c25f44c
commit 5cd1a5e197
6 changed files with 283 additions and 101 deletions
+32 -15
View File
@@ -1,11 +1,16 @@
use std::io::{self, Write, stdout};
use crossterm::{
cursor::{Hide, MoveTo, Show},
event::{DisableMouseCapture, EnableMouseCapture, KeyboardEnhancementFlags, PushKeyboardEnhancementFlags, PopKeyboardEnhancementFlags},
event::{
DisableMouseCapture, EnableMouseCapture, KeyboardEnhancementFlags,
PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
},
execute, queue,
style::{Color, SetBackgroundColor, SetForegroundColor, ResetColor, Print},
terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, size as term_size},
style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor},
terminal::{
self, size as term_size, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen,
},
};
use std::io::{self, stdout, Write};
use crate::entity::EntityManager;
use crate::render::Renderer;
@@ -64,7 +69,13 @@ impl Renderer for TerminalRenderer {
Ok(())
}
fn render(&mut self, grid: &Grid, entities: &EntityManager, cam_x: i32, cam_y: i32) -> io::Result<()> {
fn render(
&mut self,
grid: &Grid,
entities: &EntityManager,
cam_x: i32,
cam_y: i32,
) -> io::Result<()> {
if !self.initialized {
return Ok(());
}
@@ -85,7 +96,11 @@ impl Renderer for TerminalRenderer {
}
let cell = grid.get(wx, wy);
if cell.is_empty() {
frame[idx] = (' ', (cell.fg[0], cell.fg[1], cell.fg[2]), (cell.bg[0], cell.bg[1], cell.bg[2]));
frame[idx] = (
' ',
(cell.fg[0], cell.fg[1], cell.fg[2]),
(cell.bg[0], cell.bg[1], cell.bg[2]),
);
} else {
let ch = cell.material.display_char();
let fg = if cell.material == MaterialId::Lava {
@@ -115,14 +130,8 @@ impl Renderer for TerminalRenderer {
};
let fg = if e.on_fire {
(255, 160, 40)
} else if !e.alive {
(100, 60, 60)
} else {
match e.kind {
crate::entity::EntityKind::Player => (255, 255, 100),
crate::entity::EntityKind::Goblin => (100, 220, 100),
_ => (180, 50, 50),
}
(b.color[0], b.color[1], b.color[2])
};
frame[idx] = (ch, fg, (20, 10, 10));
}
@@ -140,8 +149,16 @@ impl Renderer for TerminalRenderer {
queue!(out, MoveTo(dx as u16, dy as u16))?;
let new_fg = Color::Rgb { r: fg.0, g: fg.1, b: fg.2 };
let new_bg = Color::Rgb { r: bg.0, g: bg.1, b: bg.2 };
let new_fg = Color::Rgb {
r: fg.0,
g: fg.1,
b: fg.2,
};
let new_bg = Color::Rgb {
r: bg.0,
g: bg.1,
b: bg.2,
};
if prev_color != Some((new_fg, new_bg)) {
queue!(out, SetForegroundColor(new_fg), SetBackgroundColor(new_bg))?;
prev_color = Some((new_fg, new_bg));