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:
@@ -523,7 +523,13 @@ impl Game {
|
||||
self.drain_events(reg);
|
||||
}
|
||||
|
||||
// 6.
|
||||
// 6. Ansagen fuer den naechsten Zug. **Nach** dem Entfernen der Toten,
|
||||
// sonst sagt eine Leiche an.
|
||||
for i in 0..self.enemies.len() {
|
||||
self.choose_intent(i, reg);
|
||||
}
|
||||
|
||||
// 7.
|
||||
self.turn += 1;
|
||||
Outcome::TurnPassed
|
||||
}
|
||||
@@ -924,6 +930,13 @@ impl Game {
|
||||
self.count_of("core:gold")
|
||||
}
|
||||
|
||||
/// Was der Gegner angesagt hat, als lesbarer Name. `None` heisst: Er hat
|
||||
/// nichts gefunden, was er tun koennte — der Screen zeigt „zoegert".
|
||||
pub fn intent_label(&self, i: usize, reg: &Registries) -> Option<String> {
|
||||
let id = self.enemies.get(i)?.intent.as_ref()?;
|
||||
Some(reg.abilities.get(id).map_or_else(|| id.clone(), |a| a.name.clone()))
|
||||
}
|
||||
|
||||
/// Der Anzeigename eines Exemplars: Affixe werden vorangestellt —
|
||||
/// „Flammendes Eisenschwert" (Ticket 07).
|
||||
pub fn item_label(&self, inst: &ItemInstance, reg: &Registries) -> String {
|
||||
@@ -971,28 +984,40 @@ impl Game {
|
||||
}
|
||||
}
|
||||
|
||||
/// Erster passender **und verfuegbarer** Eintrag der `ai`-Liste gewinnt.
|
||||
/// Passt nichts, setzt der Gegner aus — einer, der die Runde blockiert,
|
||||
/// waere schlimmer als einer, der nichts tut (Ticket 14).
|
||||
/// Legt fest, was ein Gegner im **naechsten** Zug tut. Erster passender
|
||||
/// **und verfuegbarer** Eintrag der `ai`-Liste gewinnt (Ticket 14) — nur
|
||||
/// eine Runde frueher ausgewertet als bisher. Passt nichts, setzt der
|
||||
/// Gegner aus; einer, der die Runde blockiert, waere schlimmer.
|
||||
fn choose_intent(&mut self, i: usize, reg: &Registries) {
|
||||
let me = CreatureRef::Enemy(i);
|
||||
let Some(def) = self.creature(me).def.clone().and_then(|id| reg.creatures.get(&id)) else {
|
||||
return;
|
||||
};
|
||||
let choice = def.ai.iter().find(|entry| {
|
||||
self.conditions_hold(&entry.condition, me, Some(CreatureRef::Player), reg)
|
||||
&& self.ability_block(me, &entry.ability, reg).is_none()
|
||||
});
|
||||
self.enemies[i].intent = choice.map(|e| e.ability.clone());
|
||||
}
|
||||
|
||||
fn enemy_acts(&mut self, i: usize, reg: &Registries) {
|
||||
let me = CreatureRef::Enemy(i);
|
||||
if !self.creature(me).is_alive() {
|
||||
return;
|
||||
}
|
||||
// Ohne Ansage kein Zug. Wer mitten im Zug auftaucht, steht ohnehin
|
||||
// nicht in der Initiativreihenfolge — er sagt am Zugende erstmals an.
|
||||
let Some(ability) = self.enemies[i].intent.take() else { return };
|
||||
|
||||
let Some(def) = self.creature(me).def.clone().and_then(|id| reg.creatures.get(&id)) else {
|
||||
// Die Ansage ist verbindlich — und genau deshalb **stoerbar**: Wer dem
|
||||
// Gegner zwischen Ansage und Ausfuehrung die Ausdauer nimmt oder die
|
||||
// Bedingung kippt, kostet ihn den Zug. Das ist der taktische Griff.
|
||||
if let Some(grund) = self.ability_block(me, &ability, reg) {
|
||||
let name = self.creature(me).name.clone();
|
||||
self.log(format!("{name} kommt nicht dazu — {grund}."));
|
||||
return;
|
||||
};
|
||||
|
||||
let choice = def.ai.iter().find(|entry| {
|
||||
self.conditions_hold(&entry.condition, me, Some(CreatureRef::Player), reg)
|
||||
&& self.ability_block(me, &entry.ability, reg).is_none()
|
||||
});
|
||||
|
||||
if let Some(entry) = choice {
|
||||
let ability = entry.ability.clone();
|
||||
self.use_ability(me, &ability, CreatureRef::Player, reg);
|
||||
}
|
||||
self.use_ability(me, &ability, CreatureRef::Player, reg);
|
||||
}
|
||||
|
||||
fn step_target(&self, dir: Direction) -> (u16, u16) {
|
||||
@@ -1595,4 +1620,39 @@ mod tests {
|
||||
}
|
||||
assert!(game.enemies.is_empty(), "in 30 Zuegen muss eine Flucht geglueckt sein");
|
||||
}
|
||||
|
||||
/// Die Ansage ist verbindlich — und genau deshalb stoerbar. Wer dem Gegner
|
||||
/// zwischen Ansage und Ausfuehrung die Ausdauer nimmt, kostet ihn den Zug.
|
||||
/// Ohne die Verbindlichkeit gaebe es dieses Fenster nicht: Eine Vorschau
|
||||
/// wuerde einfach auf die naechstbeste Faehigkeit umspringen.
|
||||
#[test]
|
||||
fn angesagte_faehigkeit_laesst_sich_stoeren() {
|
||||
let (mut game, reg) = zwergenkrieger();
|
||||
|
||||
let lord = reg.creatures.get("core:bone_lord").expect("Knochenfuerst");
|
||||
let mut e = Creature::from_def(lord);
|
||||
e.base.set(Attribute::MaxStamina, 100.0);
|
||||
e.stamina = 100.0;
|
||||
game.enemies = vec![e];
|
||||
|
||||
// Erster Zug: Der Fuerst steht noch ohne Ansage da und handelt nicht,
|
||||
// sagt am Zugende aber seinen teuersten Schlag an.
|
||||
game.execute(Command::Wait, ®);
|
||||
assert_eq!(game.enemies[0].intent.as_deref(), Some("core:power_strike"));
|
||||
assert_eq!(game.intent_label(0, ®).as_deref(), Some("Wuchtschlag"));
|
||||
|
||||
game.enemies[0].stamina = 0.0;
|
||||
let vorher = game.player.health;
|
||||
|
||||
// Die Regeneration von 4 reicht fuer die angesagten 15 nicht.
|
||||
game.execute(Command::Wait, ®);
|
||||
|
||||
assert!(
|
||||
game.journal.iter().any(|z| z.contains("kommt nicht dazu")),
|
||||
"die gestoerte Ansage muss im Verlauf stehen: {:?}",
|
||||
game.journal
|
||||
);
|
||||
assert_eq!(game.player.health, vorher, "ein gestoerter Gegner richtet nichts an");
|
||||
assert!(game.enemies[0].intent.is_some(), "und er sagt fuer den naechsten Zug neu an");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user