feat(kampf): verbindliche Gegner-Ansage und Zielcursor
Am Zugende legt jeder Gegner fest, was er im naechsten Zug tut; der Kampf-Screen zeigt es unter dem Lebensbalken. Verbindlich statt Vorschau, denn nur so entsteht ein Fenster: Wer dem Gegner zwischen Ansage und Ausfuehrung die Ausdauer nimmt, kostet ihn den Zug. Zielwahl bekommt einen eigenen Cursor, Tab schaltet zwischen Gegner- und Faehigkeitsliste um — dieselbe Bedienung wie im Laden. Die Ziffern springen nur noch aufs Ziel, ausgeloest wird mit Enter.
This commit is contained in:
@@ -33,7 +33,7 @@ impl App {
|
||||
let shows_combat = self.screens.iter().any(|s| matches!(s, Screen::Combat { .. }));
|
||||
|
||||
if in_combat && !shows_combat {
|
||||
self.screens.push(Screen::Combat { pick: 0 });
|
||||
self.screens.push(Screen::Combat { pick: 0, target: 0, targeting: false });
|
||||
} else if !in_combat {
|
||||
self.screens.retain(|s| !matches!(s, Screen::Combat { .. }));
|
||||
}
|
||||
@@ -132,7 +132,9 @@ fn draw(frame: &mut Frame, app: &App) {
|
||||
match screen {
|
||||
Screen::MainMenu => draw_main_menu(frame, area, app),
|
||||
Screen::Explore => draw_explore(frame, area, app),
|
||||
Screen::Combat { pick } => draw_combat(frame, area, app, *pick),
|
||||
Screen::Combat { pick, target, targeting } => {
|
||||
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::Dialog { pick } => draw_dialog(frame, area, app, *pick),
|
||||
@@ -345,7 +347,14 @@ fn draw_shop(frame: &mut Frame, area: Rect, app: &App, pick: usize, selling: boo
|
||||
/// Menue-Kampf (Charter: Dungeon Master / Etrian Odyssey). Links die Gegner mit
|
||||
/// Ziffern, rechts die Faehigkeiten mit Kosten und **Sperrgrund** — der kommt
|
||||
/// aus dem Spielkern, nicht aus der UI (Ticket 14).
|
||||
fn draw_combat(frame: &mut Frame, area: Rect, app: &App, pick: usize) {
|
||||
fn draw_combat(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
app: &App,
|
||||
pick: usize,
|
||||
target: usize,
|
||||
targeting: bool,
|
||||
) {
|
||||
let block = Block::bordered().title(" Kampf ");
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
@@ -355,7 +364,7 @@ fn draw_combat(frame: &mut Frame, area: Rect, app: &App, pick: usize) {
|
||||
let [gegner_area, log_area] =
|
||||
Layout::vertical([Constraint::Percentage(45), Constraint::Min(0)]).areas(links);
|
||||
|
||||
// Gegner mit Zielziffer und Lebensbalken.
|
||||
// Gegner mit Zielziffer, Lebensbalken und **Ansage**.
|
||||
let mut zeilen = Vec::new();
|
||||
for (i, e) in app.game.enemies.iter().enumerate() {
|
||||
let max = app
|
||||
@@ -364,21 +373,36 @@ fn draw_combat(frame: &mut Frame, area: Rect, app: &App, pick: usize) {
|
||||
let anteil = if max > 0.0 { (e.health / max).clamp(0.0, 1.0) } else { 0.0 };
|
||||
let balken = "█".repeat((anteil * 10.0).round() as usize);
|
||||
|
||||
let gewaehlt = targeting && i == target;
|
||||
let marken: String = e.modifiers.iter().map(|m| m.symbol).collect();
|
||||
zeilen.push(Line::from(format!(
|
||||
"{} {:<14} {:<10} {:>4.0}/{:<4.0} {marken}",
|
||||
i + 1,
|
||||
e.name,
|
||||
balken,
|
||||
e.health.max(0.0),
|
||||
max
|
||||
)));
|
||||
zeilen.push(
|
||||
Line::from(format!(
|
||||
"{} {} {:<12} {:<10} {:>4.0}/{:<4.0} {marken}",
|
||||
if gewaehlt { "›" } else { " " },
|
||||
i + 1,
|
||||
e.name,
|
||||
balken,
|
||||
e.health.max(0.0),
|
||||
max
|
||||
))
|
||||
.style(Style::new().fg(if gewaehlt { Color::Yellow } else { Color::Reset })),
|
||||
);
|
||||
|
||||
// Die Ansage ist verbindlich — wer sie stoert, kostet dem Gegner den
|
||||
// Zug. Deshalb steht sie so gross wie die Lebensanzeige.
|
||||
let ansage = match app.game.intent_label(i, &app.registries) {
|
||||
Some(name) => format!(" ↳ {name} → dich"),
|
||||
None => String::from(" ↳ zögert"),
|
||||
};
|
||||
zeilen.push(Line::from(ansage).style(Style::new().fg(Color::Magenta)));
|
||||
}
|
||||
if zeilen.is_empty() {
|
||||
zeilen.push(Line::from("—").style(Style::new().fg(Color::DarkGray)));
|
||||
}
|
||||
frame.render_widget(
|
||||
Paragraph::new(zeilen).block(Block::bordered().title(" Gegner ")),
|
||||
Paragraph::new(zeilen).block(
|
||||
Block::bordered().title(if targeting { " Gegner · aktiv " } else { " Gegner " }),
|
||||
),
|
||||
gegner_area,
|
||||
);
|
||||
|
||||
@@ -400,7 +424,7 @@ fn draw_combat(frame: &mut Frame, area: Rect, app: &App, pick: usize) {
|
||||
let Some(def) = app.registries.abilities.get(id) else { continue };
|
||||
let block = app.game.ability_block(CreatureRef::Player, id, &app.registries);
|
||||
|
||||
let zeiger = if i == pick { "›" } else { " " };
|
||||
let zeiger = if !targeting && i == pick { "›" } else { " " };
|
||||
let text = format!(
|
||||
"{zeiger} {:<16} {:>3} Ausd. {}",
|
||||
def.name,
|
||||
@@ -430,12 +454,15 @@ fn draw_combat(frame: &mut Frame, area: Rect, app: &App, pick: usize) {
|
||||
zeilen.push(Line::from(namen.join(", ")).style(Style::new().fg(Color::Green)));
|
||||
}
|
||||
zeilen.push(
|
||||
Line::from("↑↓ wählen · 1-9 Ziel · Enter erstes Ziel · . warten · c Bogen")
|
||||
Line::from("Tab Seite · ↑↓ wählen · 1-9 Ziel · Enter angreifen · . warten · c Bogen")
|
||||
.style(Style::new().fg(Color::DarkGray)),
|
||||
);
|
||||
|
||||
frame.render_widget(
|
||||
Paragraph::new(zeilen).block(Block::bordered().title(" Fähigkeiten ")),
|
||||
Paragraph::new(zeilen).block(
|
||||
Block::bordered()
|
||||
.title(if targeting { " Fähigkeiten " } else { " Fähigkeiten · aktiv " }),
|
||||
),
|
||||
rechts,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user