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

@@ -3,7 +3,7 @@
use ratatui::crossterm::event::KeyCode;
use crate::data::Registries;
use crate::game::{Cheat, Command, CreatureRef, Direction, Game};
use crate::game::{Attribute, Cheat, Command, CreatureRef, Direction, Game};
/// Popups sind gewoehnliche Screens, die sich beim Rendern als Overlay
/// ausweisen — ein gemeinsamer `Vec<Screen>` statt `current` plus
@@ -25,7 +25,15 @@ pub enum Screen {
Inventory {
pick: usize,
},
CharacterSheet,
/// Zugleich der Verteil-Screen — **kein zwoelfter Screen** (Ticket 15).
CharacterSheet {
/// Cursor in `Attribute::ASSIGNABLE`.
pick: usize,
/// Vorgemerkte Punkte, noch nicht abgeschickt. Ein festes Feld statt
/// eines `Vec`, weil die fuenf verteilbaren Grundwerte eine feste
/// Liste sind.
pending: [u32; 5],
},
/// Vollbild, kein Overlay. Bewusst nicht die lazy Variante mit einer
/// Meldungszeile im Hauptmenue: Ein Abstiegs-Roguelike lebt davon, dass der
/// Lauf am Ende *erzaehlt* wird (Ticket 14).
@@ -64,6 +72,10 @@ pub enum Response {
}
impl Screen {
pub fn sheet() -> Screen {
Screen::CharacterSheet { pick: 0, pending: [0; 5] }
}
pub fn is_overlay(&self) -> bool {
// TODO(M2): `Confirm` ist das erste echte Overlay. Bis dahin haelt die
// Regel „ein Command schliesst alle Overlays" nur die Luft.
@@ -104,7 +116,7 @@ impl Screen {
KeyCode::Char('t') => Response::Command(Command::Talk),
KeyCode::F(12) => Response::Push(Screen::Debug { pick: 0 }),
// Screen-Navigation ist reine UI und fasst `Game` nie an.
KeyCode::Char('c') => Response::Push(Screen::CharacterSheet),
KeyCode::Char('c') => Response::Push(Screen::sheet()),
KeyCode::Char('i') => Response::Push(Screen::Inventory { pick: 0 }),
KeyCode::Esc => Response::Pop,
_ => Response::None,
@@ -161,7 +173,7 @@ impl Screen {
})
}
KeyCode::Char('.') => Response::Command(Command::Wait),
KeyCode::Char('c') => Response::Push(Screen::CharacterSheet),
KeyCode::Char('c') => Response::Push(Screen::sheet()),
// Derselbe Screen wie draussen. `rejects` verbietet
// `UseItem` im Kampf nicht — es fehlte nur die Tuer.
KeyCode::Char('i') => Response::Push(Screen::Inventory { pick: 0 }),
@@ -206,10 +218,39 @@ impl Screen {
}
}
Screen::CharacterSheet => match key {
KeyCode::Esc | KeyCode::Char('c') | KeyCode::Char('q') => Response::Pop,
_ => Response::None,
},
Screen::CharacterSheet { pick, pending } => {
let offen = game.unspent_points - pending.iter().sum::<u32>();
match key {
KeyCode::Up | KeyCode::Char('k') => {
*pick = pick.saturating_sub(1);
Response::None
}
KeyCode::Down | KeyCode::Char('j') => {
*pick = (*pick + 1).min(Attribute::ASSIGNABLE.len() - 1);
Response::None
}
KeyCode::Right | KeyCode::Char('+' | 'l') if offen > 0 => {
pending[*pick] += 1;
Response::None
}
KeyCode::Left | KeyCode::Char('-' | 'h') if pending[*pick] > 0 => {
pending[*pick] -= 1;
Response::None
}
// **Alle** Punkte in einem Kommando und damit in einem Zug
// (Ticket 15).
KeyCode::Enter => Response::Command(Command::AssignPoints(
Attribute::ASSIGNABLE
.iter()
.zip(pending.iter())
.filter(|(_, n)| **n > 0)
.map(|(a, n)| (*a, *n))
.collect(),
)),
KeyCode::Esc | KeyCode::Char('c' | 'q') => Response::Pop,
_ => Response::None,
}
}
Screen::Dialog { pick } => {
let options = game.dialog_options(reg);