nightshade 0.13.0

A cross-platform data-oriented game engine.
Documentation
//! Player choices.
//!
//! Each turn, the engine assembles a menu of [`Choice`] values from:
//! - visible exits from the current room,
//! - visible items in the room (take, examine),
//! - items in inventory (drop, use, read, examine),
//! - entities in the room (talk/open + examine),
//! - the active dialogue's options if one is running,
//! - any `Effect::OfferChoices` currently in effect.
//!
//! A choice, when picked, either enacts a structured [`ChoiceAction`] or runs
//! a free-form list of effects.

use crate::interactive_fiction::data::condition::Condition;
use crate::interactive_fiction::data::effect::Effect;
use crate::interactive_fiction::data::ids::{EntityId, ItemId, RoomId};
use crate::interactive_fiction::data::text::Text;
use serde::{Deserialize, Serialize};

/// A menu entry offered to the player.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Choice {
    /// Player-facing label.
    pub label: Text,
    /// Optional gate. If unset, the choice is always selectable.
    pub condition: Option<Condition>,
    /// If true, the choice appears greyed out (with `locked_reason`) when its
    /// condition does not hold.
    pub visible_when_locked: bool,
    /// Message shown next to a greyed-out entry.
    pub locked_reason: Option<Text>,
    /// What picking the choice does.
    pub action: ChoiceAction,
}

impl Choice {
    pub fn new(label: Text, action: ChoiceAction) -> Self {
        Self {
            label,
            condition: None,
            visible_when_locked: false,
            locked_reason: None,
            action,
        }
    }

    pub fn with_condition(mut self, condition: Condition) -> Self {
        self.condition = Some(condition);
        self
    }

    pub fn visible_when_locked(mut self, reason: Text) -> Self {
        self.visible_when_locked = true;
        self.locked_reason = Some(reason);
        self
    }
}

/// What a [`Choice`] does when picked.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ChoiceAction {
    /// Move to the target room through a specific exit index.
    Go {
        to: RoomId,
        exit_index: usize,
    },
    /// Take the item from the current room into inventory.
    Take(ItemId),
    /// Drop the item from inventory into the current room.
    Drop(ItemId),
    /// Use the item in the current context (fires `Trigger::OnUse`).
    Use(ItemId),
    /// Read the item's `read` text.
    Read(ItemId),
    /// Examine an item, entity, or room-feature keyword. The
    /// [`ExamineTarget`] inner enum carries which of those was picked.
    Examine(ExamineTarget),
    /// Open / talk-to an entity. Begins its dialogue if any, and fires
    /// `Trigger::OnOpen`. The menu label is chosen by the entity's
    /// `kind` — characters surface as `choice_talk`, objects as
    /// `choice_open_object`.
    Open(EntityId),
    /// Pick option `index` inside the active dialogue node.
    DialogueOption(usize),
    /// End the active dialogue.
    LeaveDialogue,
    /// Inspect current room / inventory etc. without advancing a turn.
    Look,
    Inventory,
    /// Pass the turn.
    Wait,
    /// Run arbitrary effects.
    Effects(Vec<Effect>),
}

impl ChoiceAction {
    /// Whether picking this action should advance the turn counter and
    /// fire TurnStart/TurnEnd rules. Examining, reading, opening menus,
    /// and dialogue navigation are "free" (they don't burn a turn);
    /// everything else does. Exhaustive — adding a variant to
    /// [`ChoiceAction`] forces a decision here.
    pub const fn advances_turn(&self) -> bool {
        match self {
            ChoiceAction::Go { .. }
            | ChoiceAction::Take(_)
            | ChoiceAction::Drop(_)
            | ChoiceAction::Use(_)
            | ChoiceAction::Wait
            | ChoiceAction::Effects(_) => true,
            ChoiceAction::Look
            | ChoiceAction::Inventory
            | ChoiceAction::Examine(_)
            | ChoiceAction::Open(_)
            | ChoiceAction::Read(_)
            | ChoiceAction::DialogueOption(_)
            | ChoiceAction::LeaveDialogue => false,
        }
    }
}

/// What the player is examining. Lets a single
/// `ChoiceAction::Examine(...)` and `Trigger::OnExamine(...)` speak for
/// items, entities, and room-feature keywords without three parallel
/// variants.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ExamineTarget {
    /// An item (in inventory or on the floor).
    Item(ItemId),
    /// An entity in the current room — character or object.
    Entity(EntityId),
    /// A room-feature keyword registered via `Room::with_examine`.
    Keyword(String),
}