feat: m3 kampf, events und trigger

Der Dungeon hat jetzt Gegner, und man kann sterben.

- game/action.rs: sieben Schadenstypen, fuenf der zehn Aktionen, Target
- game/condition.rs: Bedingungsgrammatik (Liste = UND, not je Eintrag)
- game/event.rs: zwoelf Events, Trigger als vierte Wirkungsform
- game/combat.rs: Treffer- und Schadensformel mit drei Klemmbereichen
- Game::apply, drain_events (FIFO), Initiative, Rundenablauf aus Ticket 14
- data/: abilities-, creatures- und modifiers-Registry, [balance] feldweise
- ui/: Combat-Screen mit Faehigkeitsmenue und Sperrgrund

Drei Befunde:
- Die Trefferformel war toter Code — jeder Angriff traf. Nur der Compiler
  hat es gemeldet ('hit_chance is never used'). Test haelt sie am Leben.
- Die Warteschlange war ein Stack (pop statt pop_front), obwohl Ticket 08
  ausdruecklich 'haengt es hinten an' sagt. Jetzt VecDeque.
- Benannte Modifikatoren (core:poisoned) hatten keine Registry — Ticket 01
  laesst apply_modifier per ID referenzieren, aber keine der zwoelf
  definierte sie. Das ist die dreizehnte.

43 Tests.
This commit is contained in:
2026-08-12 17:03:00 +02:00
parent 8bcd53efa2
commit 217a7acbbc
20 changed files with 1990 additions and 75 deletions

View File

@@ -2,7 +2,8 @@
use ratatui::crossterm::event::KeyCode;
use crate::game::{Command, Direction};
use crate::data::Registries;
use crate::game::{Command, CreatureRef, Direction, Game};
/// Popups sind gewoehnliche Screens, die sich beim Rendern als Overlay
/// ausweisen — ein gemeinsamer `Vec<Screen>` statt `current` plus
@@ -10,6 +11,11 @@ use crate::game::{Command, Direction};
pub enum Screen {
MainMenu,
Explore,
/// Menue-Kampf (Charter): Faehigkeitsliste, Zielwahl mit den Zifferntasten.
Combat {
/// Cursor in der Faehigkeitsliste.
pick: usize,
},
CharacterSheet,
/// Vollbild, kein Overlay. Bewusst nicht die lazy Variante mit einer
/// Meldungszeile im Hauptmenue: Ein Abstiegs-Roguelike lebt davon, dass der
@@ -37,7 +43,12 @@ impl Screen {
false
}
pub fn handle_key(&mut self, key: KeyCode) -> Response {
/// **Befund aus M3:** Ticket 11 hatte `handle_key(&mut self, key)` ohne
/// Kontext festgelegt. Der Kampf-Screen muss aber wissen, wie viele
/// Faehigkeiten es gibt, um den Cursor zu klemmen, und welche der Cursor
/// meint. Lesender Zugriff genuegt — die Regel „ein Screen fasst `Game`
/// nicht an" bleibt, weil nur `Response::Command` etwas aendert.
pub fn handle_key(&mut self, key: KeyCode, game: &Game, reg: &Registries) -> Response {
match self {
Screen::MainMenu => match key {
KeyCode::Enter => Response::Push(Screen::Explore),
@@ -68,6 +79,45 @@ impl Screen {
_ => Response::None,
},
Screen::Combat { pick } => {
let abilities = game.abilities_of(CreatureRef::Player, reg);
if abilities.is_empty() {
return Response::None;
}
*pick = (*pick).min(abilities.len() - 1);
match key {
KeyCode::Up | KeyCode::Char('k') => {
*pick = pick.saturating_sub(1);
Response::None
}
KeyCode::Down | KeyCode::Char('j') => {
*pick = (*pick + 1).min(abilities.len() - 1);
Response::None
}
// Ziel ueber die Zifferntasten; ohne Gegner (Flucht,
// Selbstheilung) zaehlt der Held selbst als Ziel.
KeyCode::Enter | KeyCode::Char('1'..='9') => {
let index = match key {
KeyCode::Char(c @ '1'..='9') => (c as u8 - b'1') as usize,
_ => 0,
};
let target = if game.enemies.is_empty() {
CreatureRef::Player
} else {
CreatureRef::Enemy(index.min(game.enemies.len() - 1))
};
Response::Command(Command::UseAbility {
ability: abilities[*pick].clone(),
target,
})
}
KeyCode::Char('.') => Response::Command(Command::Wait),
KeyCode::Char('c') => Response::Push(Screen::CharacterSheet),
_ => Response::None,
}
}
Screen::CharacterSheet => match key {
KeyCode::Esc | KeyCode::Char('c') | KeyCode::Char('q') => Response::Pop,
_ => Response::None,