feat: m4 teil 1 — beute, inventar und affixe

- data/: [[loot_tables]] und [[affixes]] als Registries; LootEntry mit
  guaranteed, nothing, table-in-table, count und affixes
- game/loot.rs: EINE Ziehmaschinerie fuer Gegenstaende und Affixe,
  Tiefendeckel gegen wechselseitigen Bezug
- PickUp zieht erst beim Aufheben (Ticket 16) — moeglich nur, weil alle
  Haufen dasselbe Symbol zeigen
- UseItem/Equip/Unequip/Drop, Inventar-Screen als Registry-Schleife
- Affixe im Attributpfad, in der Resistenz und in der Trigger-Suche;
  item_label stellt Affixnamen voran, item_price multipliziert Faktoren
- Room haelt eine loot_table (nicht je Haufen eine), Prefab-loot
  ueberschreibt die Etagentabelle

53 Tests. Offen aus M4: Handel.
This commit is contained in:
2026-08-12 17:56:28 +02:00
parent cdcabcce21
commit 197f62ce33
16 changed files with 849 additions and 19 deletions

View File

@@ -111,6 +111,7 @@ fn draw(frame: &mut Frame, app: &App) {
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::Inventory { pick } => draw_inventory(frame, area, app, *pick),
Screen::CharacterSheet => draw_sheet(frame, area, app),
Screen::GameOver => draw_game_over(frame, area, app),
}
@@ -159,7 +160,7 @@ fn draw_explore(frame: &mut Frame, area: Rect, app: &App) {
};
frame.render_widget(
Paragraph::new(format!(
"Zug {} │ {} │ hjkl/Pfeile .warten >absteigen c Bogen │ {}",
"Zug {} │ {} │ .warten g aufheben >absteigen i Inventar c Bogen │ {}",
app.game.turn, app.game.player.name, note
))
.right_aligned()
@@ -168,6 +169,63 @@ fn draw_explore(frame: &mut Frame, area: Rect, app: &App) {
);
}
/// Links das Inventar mit Cursor, rechts die Ausrüstung als **Schleife über die
/// Slot-Registry** — Modder können Slots ergänzen, ohne dass hier etwas
/// geändert wird (Ticket 02).
fn draw_inventory(frame: &mut Frame, area: Rect, app: &App, pick: usize) {
let block = Block::bordered().title(" Inventar ");
let inner = block.inner(area);
frame.render_widget(block, area);
let [links, rechts] =
Layout::horizontal([Constraint::Percentage(60), Constraint::Min(0)]).areas(inner);
let mut zeilen = Vec::new();
for (i, inst) in app.game.player.inventory.iter().enumerate() {
let def = app.registries.items.get(&inst.def);
let name = app.game.item_label(inst, &app.registries);
let preis = app.game.item_price(inst, &app.registries);
let slot = def.and_then(|d| d.slot.as_deref()).unwrap_or("—");
let zeiger = if i == pick { "›" } else { " " };
let farbe = if i == pick { Color::Yellow } else { Color::Reset };
zeilen.push(
Line::from(format!("{zeiger} {name:<30} {slot:<12} {preis:>5} G"))
.style(Style::new().fg(farbe)),
);
}
if zeilen.is_empty() {
zeilen.push(Line::from("leer").style(Style::new().fg(Color::DarkGray)));
}
zeilen.push(Line::from(""));
zeilen.push(
Line::from("↑↓ wählen · u benutzen · e anlegen · d wegwerfen · 1-9 ablegen · Esc")
.style(Style::new().fg(Color::DarkGray)),
);
frame.render_widget(
Paragraph::new(zeilen).block(Block::bordered().title(" Getragen ")),
links,
);
let mut aus = Vec::new();
for (i, slot) in app.registries.slots_ordered().iter().enumerate() {
let getragen = app.game.player.equipment.get(&slot.id);
let text = match getragen {
Some(inst) => app.game.item_label(inst, &app.registries),
None => String::from("—"),
};
let farbe = if getragen.is_some() { Color::Reset } else { Color::DarkGray };
aus.push(
Line::from(format!("{} {:<10} {}", i + 1, slot.name, text))
.style(Style::new().fg(farbe)),
);
}
frame.render_widget(
Paragraph::new(aus).block(Block::bordered().title(" Ausrüstung ")),
rechts,
);
}
/// 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).