nightshade 0.13.0

A cross-platform data-oriented game engine.
Documentation
//! Unified room-pinned interactables.
//!
//! [`Entity`] collapses what used to be two parallel types — `Npc` and
//! `Fixture` — into one. Both kinds are named, addressable, optionally
//! carry a [`Dialogue`](super::Dialogue), and live in a room (or off-
//! stage). The only genuine semantic distinction is display-framing and
//! disposition: *characters* surface as "Talk to X" and carry an
//! `initial_disposition`; *objects* surface as "Open the X" / "Examine
//! the X" and have no disposition. Both kinds share all the rest.
//!
//! Construction is kind-typed: [`Entity::character`] returns a
//! [`CharacterBuilder`] and [`Entity::object`] returns an
//! [`ObjectBuilder`]. Character-only knobs (`with_disposition`) only
//! exist on `CharacterBuilder`, so `Entity::object(...).with_disposition(5)`
//! is a compile error rather than a silent no-op.
//!
//! Both builders implement `Into<Entity>`; `AreaContents::add_entity`
//! accepts either, so author code reads the same.

use crate::interactive_fiction::data::ids::{DialogueId, RoomId};
use crate::interactive_fiction::data::text::Text;
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;

/// Where an entity is right now. Entities live in a specific room or
/// off-stage; a rule moves them between these states with
/// [`Effect::MoveEntity`](super::Effect::MoveEntity).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum EntityLocation {
    /// Present in the named room.
    Room(RoomId),
    /// Removed from play. The entity isn't listed or addressable until
    /// a rule moves it back.
    Nowhere,
}

/// Whether an entity is a person or an object. Determines menu framing
/// and whether the runtime tracks a disposition for it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum EntityKind {
    /// A person. Surfaces in the menu as `choice_talk` ("Talk to X").
    /// Rules can gate on the `dispositions` map.
    Character {
        /// Starting feeling value stored in
        /// `RuntimeState.dispositions` at world start.
        initial_disposition: i64,
    },
    /// An object, furniture, or UI surface. Surfaces as
    /// `choice_open_object` ("Open the X"). Has no disposition.
    Object,
}

/// A room-pinned thing the player can address — either a character or
/// an object. Replaces the old separate `Npc` and `Fixture` types.
/// Construct via [`Entity::character`] or [`Entity::object`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Entity {
    pub name: String,
    pub synonyms: Vec<String>,
    pub description: Text,
    /// Dialogue triggered by `talk to` (characters) or `open` / `use`
    /// (objects). Absent for pure examine targets.
    pub dialogue: Option<DialogueId>,
    /// Where the entity starts. `None` is treated the same as
    /// `Some(EntityLocation::Nowhere)`.
    pub initial_location: Option<EntityLocation>,
    /// Free-form tags.
    pub tags: BTreeSet<String>,
    /// Whether this entity is a character or an object.
    pub kind: EntityKind,
}

impl Entity {
    /// Start building a character (a person). Returns a
    /// [`CharacterBuilder`] that exposes character-only methods like
    /// `with_disposition`.
    pub fn character(name: impl Into<String>, description: Text) -> CharacterBuilder {
        CharacterBuilder(Self::new(
            name,
            description,
            EntityKind::Character {
                initial_disposition: 0,
            },
        ))
    }

    /// Start building an object (a thing, fixture, or UI surface).
    /// Returns an [`ObjectBuilder`]; has no `with_disposition` method —
    /// attempting to call it is a compile error.
    pub fn object(name: impl Into<String>, description: Text) -> ObjectBuilder {
        ObjectBuilder(Self::new(name, description, EntityKind::Object))
    }

    fn new(name: impl Into<String>, description: Text, kind: EntityKind) -> Self {
        Self {
            name: name.into(),
            synonyms: Vec::new(),
            description,
            dialogue: None,
            initial_location: None,
            tags: BTreeSet::new(),
            kind,
        }
    }

    /// `true` if this entity is a character (person).
    pub const fn is_character(&self) -> bool {
        matches!(self.kind, EntityKind::Character { .. })
    }

    /// `true` if this entity is an object (thing/fixture/UI surface).
    pub const fn is_object(&self) -> bool {
        matches!(self.kind, EntityKind::Object)
    }
}

macro_rules! common_builder_methods {
    ($builder:ident) => {
        impl $builder {
            pub fn with_synonyms(
                mut self,
                synonyms: impl IntoIterator<Item = &'static str>,
            ) -> Self {
                self.0
                    .synonyms
                    .extend(synonyms.into_iter().map(String::from));
                self
            }

            pub fn with_dialogue(mut self, dialogue: DialogueId) -> Self {
                self.0.dialogue = Some(dialogue);
                self
            }

            /// Place this entity in `room` at the start of play.
            pub fn starting_in(mut self, room: RoomId) -> Self {
                self.0.initial_location = Some(EntityLocation::Room(room));
                self
            }

            /// Mark the entity as starting off-stage.
            pub fn starting_nowhere(mut self) -> Self {
                self.0.initial_location = Some(EntityLocation::Nowhere);
                self
            }

            pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
                self.0.tags.insert(tag.into());
                self
            }

            /// Finish building and return the `Entity`. Prefer the
            /// `Into<Entity>` conversion, which lets `AreaContents::add_entity`
            /// take either builder directly.
            pub fn build(self) -> Entity {
                self.0
            }
        }

        impl From<$builder> for Entity {
            fn from(builder: $builder) -> Entity {
                builder.0
            }
        }
    };
}

/// Builder for a character entity (a person). Produced by
/// [`Entity::character`]. Exposes `with_disposition` in addition to
/// the shared builder methods.
pub struct CharacterBuilder(Entity);

common_builder_methods!(CharacterBuilder);

impl CharacterBuilder {
    /// Set the starting disposition. Stored in
    /// `RuntimeState.dispositions` at world start; rules can gate on
    /// it via `Condition::DispositionAtLeast`.
    pub fn with_disposition(mut self, disposition: i64) -> Self {
        if let EntityKind::Character {
            initial_disposition,
        } = &mut self.0.kind
        {
            *initial_disposition = disposition;
        }
        self
    }
}

/// Builder for an object entity (a thing, fixture, or UI surface).
/// Produced by [`Entity::object`]. No disposition method — objects
/// don't have dispositions.
pub struct ObjectBuilder(Entity);

common_builder_methods!(ObjectBuilder);