feat(quests): Quests als Trigger mit Zaehler
Eine Quest IST ein Trigger mit count = N — kein zweiter Mechanismus. Gezaehlt wird erst ab start_quest, erledigte bleiben mit done = true stehen, und die Belohnung darf die naechste starten: das ist die Kette. - fuenfzehnte Registry, Trigger per serde(flatten) eingebettet - give_item als Aktion, damit eine Belohnung mehr als Erfahrung zahlt - QuestLog auf q, Fortschritt im Speicherstand, unbekannte IDs verworfen - eine Funktion entscheidet fuer alle Traeger, wann ein Trigger feuert Befund: Die Ordnungszahlen im Aktionskatalog widersprechen sich — vier Tickets zaehlten je vom eigenen Stand. Sie sind raus, CONTEXT.md fuehrt die Liste.
This commit is contained in:
@@ -16,7 +16,7 @@ use toml::Spanned;
|
||||
|
||||
use super::registries::{
|
||||
Ability, Affix, Category, CharacterOption, CreatureDef, FloorDef, Item, LootTable, ModifierDef,
|
||||
Npc, Prefab, Registries, Slot,
|
||||
Npc, Prefab, Quest, Registries, Slot,
|
||||
};
|
||||
|
||||
/// Genau drei harte Fehler (Ticket 04). Alles Uebrige ist Warnung plus
|
||||
@@ -265,6 +265,8 @@ struct ModFile {
|
||||
loot_tables: Vec<Spanned<toml::Value>>,
|
||||
#[serde(default)]
|
||||
npcs: Vec<Spanned<toml::Value>>,
|
||||
#[serde(default)]
|
||||
quests: Vec<Spanned<toml::Value>>,
|
||||
/// **Keine** Tabelle mit `[[...]]`, sondern ein einzelner Block, der
|
||||
/// feldweise verschmilzt — die ausdrueckliche Ausnahme zu Ticket 03.
|
||||
#[serde(default)]
|
||||
@@ -305,6 +307,7 @@ impl_entry!(ModifierDef);
|
||||
impl_entry!(Affix);
|
||||
impl_entry!(LootTable);
|
||||
impl_entry!(Npc);
|
||||
impl_entry!(Quest);
|
||||
|
||||
struct FileCtx<'a> {
|
||||
module: &'a str,
|
||||
@@ -516,6 +519,7 @@ pub fn load_mods() -> Result<(Registries, Vec<String>), LoadError> {
|
||||
parse_entries(parsed.affixes, &ctx, &mut reg.affixes, &mut warnings);
|
||||
parse_entries(parsed.loot_tables, &ctx, &mut reg.loot_tables, &mut warnings);
|
||||
parse_entries(parsed.npcs, &ctx, &mut reg.npcs, &mut warnings);
|
||||
parse_entries(parsed.quests, &ctx, &mut reg.quests, &mut warnings);
|
||||
|
||||
// `[balance]` verschmilzt **feldweise** (Ticket 14): Ein Mod, der
|
||||
// einen Knopf dreht, schreibt eine Zeile — und ein neuer Knopf in
|
||||
|
||||
@@ -259,6 +259,28 @@ impl Default for Balance {
|
||||
}
|
||||
}
|
||||
|
||||
/// Die **siebte** Registry aus Ticket 08 (inzwischen die fuenfzehnte).
|
||||
///
|
||||
/// Eine Quest **ist ein Trigger mit `count = N`** — kein zweiter Mechanismus.
|
||||
/// „Besitze 100 Gold" braucht deshalb keine eigene Form: `on = "item_picked_up"`,
|
||||
/// `condition = has_item core:gold >= 100`, `count = 1`.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Quest {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
/// Wie oft das Ereignis passen muss.
|
||||
#[serde(default = "one")]
|
||||
pub count: u32,
|
||||
/// Der Trigger selbst — `on`, `when`, `condition`, `actions`, `chance`
|
||||
/// stehen flach in derselben Tabelle, weil die Quest *ist*, was er ist.
|
||||
#[serde(flatten)]
|
||||
pub trigger: Trigger,
|
||||
#[serde(default)]
|
||||
pub overrides: Option<String>,
|
||||
}
|
||||
|
||||
/// Eine Wahlmoeglichkeit im Gespraech. **Flach, kein Knotengraph** — was erst
|
||||
/// ein Graph koennte, ist Erzaehlung, und das Story-Modul steht unter
|
||||
/// *Out of scope*.
|
||||
@@ -475,6 +497,7 @@ pub struct Registries {
|
||||
pub affixes: HashMap<String, Affix>,
|
||||
pub loot_tables: HashMap<String, LootTable>,
|
||||
pub npcs: HashMap<String, Npc>,
|
||||
pub quests: HashMap<String, Quest>,
|
||||
/// Keine Registry — ein Block mit festem Schema, feldweise verschmolzen.
|
||||
pub balance: Balance,
|
||||
/// Wer geladen wurde, in Ladereihenfolge. **Einziger Leser ist der
|
||||
@@ -482,7 +505,7 @@ pub struct Registries {
|
||||
/// spaeterer Ladelauf „`meinmod` 1.0.0 → 1.2.0 seit dem Speichern" melden
|
||||
/// kann. Das ist die ganze Bilanz des `version`-Felds.
|
||||
pub mods: Vec<crate::data::save::ModStamp>,
|
||||
// TODO(M5+): `quests` (Ticket 08) und `scripts` (Ticket 13).
|
||||
// TODO(M8): `scripts` (Ticket 13).
|
||||
}
|
||||
|
||||
impl Registries {
|
||||
|
||||
@@ -169,6 +169,16 @@ fn version_drift(damals: &[ModStamp], reg: &Registries) -> Vec<String> {
|
||||
/// **Verwerfen, nie ablehnen** (Ticket 09): Ein Spieler soll nicht seinen
|
||||
/// ganzen Lauf verlieren, weil er einen Mod deinstalliert hat.
|
||||
fn prune(game: &mut Game, reg: &Registries, warnings: &mut Vec<String>) {
|
||||
// Ein Fortschritt ohne Definition ist eine Zahl ohne Bedeutung — die Regel
|
||||
// steht in Ticket 08, das Detail gehoert hierher.
|
||||
game.quests.retain(|q| {
|
||||
let da = reg.quests.contains_key(&q.quest);
|
||||
if !da {
|
||||
warnings.push(format!("Quest `{}` fehlt, Fortschritt entfällt", q.quest));
|
||||
}
|
||||
da
|
||||
});
|
||||
|
||||
prune_creature(&mut game.player, reg, warnings);
|
||||
for e in &mut game.enemies {
|
||||
prune_creature(e, reg, warnings);
|
||||
@@ -271,6 +281,11 @@ mod tests {
|
||||
game.unspent_points = 2;
|
||||
game.give(ItemInstance::stack("core:gold", 130), ®);
|
||||
game.player.health = 33.0;
|
||||
game.quests.push(crate::game::QuestProgress {
|
||||
quest: String::from("core:rattenplage"),
|
||||
count: 3,
|
||||
done: false,
|
||||
});
|
||||
|
||||
let (text, zurueck) = durch_toml(&game, ®);
|
||||
|
||||
@@ -291,6 +306,8 @@ mod tests {
|
||||
|
||||
// Die Etage kommt Kachel fuer Kachel zurueck — und steht als lesbare
|
||||
// Zeilen in der Datei, nicht als 3200 Enum-Eintraege (Ticket 09).
|
||||
assert_eq!(zurueck.quests.len(), 1, "der Questfortschritt ueberlebt");
|
||||
assert_eq!(zurueck.quests[0].count, 3);
|
||||
assert_eq!(zurueck.floor.rows(), game.floor.rows());
|
||||
assert_eq!(zurueck.floor.rooms.len(), game.floor.rooms.len());
|
||||
assert!(text.contains("####"), "die Kacheln muessen als Zeilen dastehen");
|
||||
@@ -310,9 +327,15 @@ mod tests {
|
||||
game.player
|
||||
.equipment
|
||||
.insert(String::from("core:head"), ItemInstance::new("weg:helm"));
|
||||
game.quests.push(crate::game::QuestProgress {
|
||||
quest: String::from("weg:auftrag"),
|
||||
count: 3,
|
||||
done: false,
|
||||
});
|
||||
|
||||
let mut warnungen = Vec::new();
|
||||
prune(&mut game, ®, &mut warnungen);
|
||||
assert!(game.quests.is_empty(), "ein Fortschritt ohne Definition ist eine Zahl ohne Sinn");
|
||||
|
||||
assert!(!game.player.inventory.iter().any(|i| i.def == "weg:phantom"));
|
||||
assert!(!game.player.options.iter().any(|o| o == "weg:klasse"));
|
||||
@@ -326,7 +349,7 @@ mod tests {
|
||||
.expect("der Gegenstand bleibt, nur das Affix faellt");
|
||||
assert_eq!(schwert.affixes, vec![String::from("core:sharp")]);
|
||||
|
||||
assert_eq!(warnungen.len(), 4, "jeder Verlust bekommt eine Zeile: {warnungen:?}");
|
||||
assert_eq!(warnungen.len(), 5, "jeder Verlust bekommt eine Zeile: {warnungen:?}");
|
||||
}
|
||||
|
||||
/// Falsches Schema: ablehnen mit Klartext, **Datei bleibt liegen**.
|
||||
|
||||
Reference in New Issue
Block a user