feat(progression): Erfahrung, Aufstieg und Punkteverteilung

Toeten gibt Erfahrung (exp_reward stand seit M3 ungelesen an jedem
Gegner), give_exp als zwoelfte Aktion fuer spaetere Quests. Aufstieg in
einer Schleife am Zugende, ein Punkt je Stufe, keine Obergrenze.

Verteilt wird im Charakterbogen — kein zwoelfter Screen. AssignPoints
nimmt alle Punkte auf einmal und kostet damit einen Zug.

luck wirkt jetzt: eine Chance auf einen Zusatzzug auf jeder Beutetabelle.

Drei Befunde:
- strength und constitution las niemand, zwei der fuenf verteilbaren
  Grundwerte waren Fallen. Zwei balance-Knoepfe rechnen sie beim
  Verteilen um.
- Rng::chance(0.0) verbrauchte eine Zahl und verschob den Zufallsstrom.
- Cheat::ClearRoom ging am Todesweg vorbei.
This commit is contained in:
2026-08-12 21:15:25 +02:00
parent b072e5ade3
commit b19492bb51
12 changed files with 421 additions and 45 deletions

View File

@@ -136,7 +136,9 @@ fn draw(frame: &mut Frame, app: &App) {
draw_combat(frame, area, app, *pick, *target, *targeting)
}
Screen::Inventory { pick } => draw_inventory(frame, area, app, *pick),
Screen::CharacterSheet => draw_sheet(frame, area, app),
Screen::CharacterSheet { pick, pending } => {
draw_sheet(frame, area, app, *pick, pending)
}
Screen::Dialog { pick } => draw_dialog(frame, area, app, *pick),
Screen::Shop { pick, selling } => draw_shop(frame, area, app, *pick, *selling),
Screen::Debug { pick } => draw_debug(frame, area, app, *pick),
@@ -530,7 +532,7 @@ fn draw_game_over(frame: &mut Frame, area: Rect, app: &App) {
/// Der Charakterbogen ist eine **Schleife ueber Registries**, kein gemauertes
/// Layout — Modder koennen Slots und Wahlkategorien ergaenzen (Ticket 02).
fn draw_sheet(frame: &mut Frame, area: Rect, app: &App) {
fn draw_sheet(frame: &mut Frame, area: Rect, app: &App, pick: usize, pending: &[u32; 5]) {
let block = Block::bordered().title(" Charakterbogen ");
let inner = block.inner(area);
frame.render_widget(block, area);
@@ -554,7 +556,16 @@ fn draw_sheet(frame: &mut Frame, area: Rect, app: &App) {
} else {
format!("{total:.0} ({base:.0} Basis)")
};
Row::new(vec![a.name().to_string(), cell])
// Verteilbare Grundwerte tragen Cursor und Vormerkung — der Bogen
// ist zugleich der Verteil-Screen (Ticket 15).
let Some(i) = Attribute::ASSIGNABLE.iter().position(|x| x == a) else {
return Row::new(vec![format!(" {}", a.name()), cell]);
};
let zeiger = if i == pick { "›" } else { " " };
let vorgemerkt = if pending[i] > 0 { format!(" +{}", pending[i]) } else { String::new() };
Row::new(vec![format!("{zeiger} {} {vorgemerkt}", a.name()), cell])
.style(Style::new().fg(if i == pick { Color::Yellow } else { Color::Reset }))
})
.collect();
@@ -566,6 +577,21 @@ fn draw_sheet(frame: &mut Frame, area: Rect, app: &App) {
let mut lines = Vec::new();
// Erfahrung und Punkte stehen an `Game`, nicht am `Creature` (Ticket 15).
let offen = app.game.unspent_points - pending.iter().sum::<u32>();
lines.push(Line::from(format!(
"Erfahrung {}/{}",
app.game.current_exp,
app.game.exp_needed(&app.registries)
)));
if app.game.unspent_points > 0 {
lines.push(
Line::from(format!("{offen} Punkte offen · ←→ verteilen · Enter bestätigen"))
.style(Style::new().fg(Color::Yellow)),
);
}
lines.push(Line::from(""));
lines.push(Line::from("Herkunft".bold()));
for id in &app.game.player.options {
let name = app.registries.options.get(id).map(|o| o.name.as_str()).unwrap_or(id);