use std::collections::{BTreeMap, BTreeSet};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::layout::StationKind;
use crate::ids::{
ActorId, AmbushId, AnchorId, AreaId, BranchId, BranchPointId, ClassId, DialogueId, EditBatchId,
EndingId, FlagId, LethalVolumeId, LootId, NpcId, ObjectiveId, PoolId, PrefabId, QuestId,
RegionId, ShopId, ShortcutId, StakeId, StateId, TimedGateId, TrapId, TriggerId, WaveId,
};
fn default_true() -> bool {
true
}
fn is_false(b: &bool) -> bool {
!*b
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorldContent {
pub title: String,
pub theme: String,
pub premise: String,
pub seed: u64,
pub target_minutes: u32,
pub areas: Vec<Area>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub languages: Vec<String>,
pub time: WorldTime,
pub weather: WorldWeather,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub difficulty: Option<WorldDifficulty>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub horizon: Option<Horizon>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub boundary: Option<Boundary>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub outro: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_players: Option<u8>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum Carrier {
#[default]
All,
One,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum WorldTime {
Day,
Noon,
Dusk,
Night,
Midnight,
#[serde(alias = "sunrise")]
Dawn,
}
impl WorldTime {
const fn spec(self) -> (&'static str, i64) {
match self {
WorldTime::Day => ("day", 1000),
WorldTime::Noon => ("noon", 6000),
WorldTime::Dusk => ("12000", 12000),
WorldTime::Night => ("night", 13000),
WorldTime::Midnight => ("midnight", 18000),
WorldTime::Dawn => ("23000", 23000),
}
}
pub fn token(self) -> &'static str {
self.spec().0
}
pub fn daytime_ticks(self) -> i64 {
self.spec().1
}
pub fn keyword(self) -> &'static str {
match self {
WorldTime::Day => "day",
WorldTime::Noon => "noon",
WorldTime::Dusk => "dusk",
WorldTime::Night => "night",
WorldTime::Midnight => "midnight",
WorldTime::Dawn => "dawn",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum WorldWeather {
Clear,
Rain,
Thunder,
}
impl WorldWeather {
pub fn keyword(self) -> &'static str {
self.token()
}
pub fn token(self) -> &'static str {
match self {
WorldWeather::Clear => "clear",
WorldWeather::Rain => "rain",
WorldWeather::Thunder => "thunder",
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum WorldDifficulty {
#[default]
Easy,
Normal,
Hard,
Peaceful,
}
impl WorldDifficulty {
pub fn token(self) -> &'static str {
match self {
WorldDifficulty::Peaceful => "peaceful",
WorldDifficulty::Easy => "easy",
WorldDifficulty::Normal => "normal",
WorldDifficulty::Hard => "hard",
}
}
pub fn id(self) -> i32 {
match self {
WorldDifficulty::Peaceful => 0,
WorldDifficulty::Easy => 1,
WorldDifficulty::Normal => 2,
WorldDifficulty::Hard => 3,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum Horizon {
Name(HorizonBase),
Spec(HorizonSpec),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum HorizonBase {
#[default]
Void,
Ocean,
Valley,
}
impl HorizonBase {
pub fn token(self) -> &'static str {
match self {
HorizonBase::Void => "void",
HorizonBase::Ocean => "ocean",
HorizonBase::Valley => "valley",
}
}
pub fn has_surround(self) -> bool {
matches!(self, HorizonBase::Valley)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct HorizonSpec {
pub base: HorizonBase,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ratio: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rim_height: Option<i32>,
}
pub mod horizon_defaults {
pub const RATIO: f64 = 2.5;
pub const RATIO_MIN: f64 = 2.0;
pub const RATIO_MAX: f64 = 3.0;
pub const RIM_HEIGHT: i32 = 48;
pub const RIM_HEIGHT_MIN: i32 = 16;
pub const RIM_HEIGHT_MAX: i32 = 128;
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ResolvedHorizon {
pub base: HorizonBase,
pub ratio: f64,
pub rim_height: i32,
}
impl Default for ResolvedHorizon {
fn default() -> Self {
ResolvedHorizon {
base: HorizonBase::Void,
ratio: horizon_defaults::RATIO,
rim_height: horizon_defaults::RIM_HEIGHT,
}
}
}
impl ResolvedHorizon {
pub fn of_base(base: HorizonBase) -> Self {
ResolvedHorizon {
base,
..Default::default()
}
}
}
impl Horizon {
pub fn resolved(&self) -> ResolvedHorizon {
match self {
Horizon::Name(base) => ResolvedHorizon::of_base(*base),
Horizon::Spec(s) => ResolvedHorizon {
base: s.base,
ratio: s.ratio.unwrap_or(horizon_defaults::RATIO),
rim_height: s.rim_height.unwrap_or(horizon_defaults::RIM_HEIGHT),
},
}
}
pub fn base(&self) -> HorizonBase {
self.resolved().base
}
pub fn needs_horizon_library(&self) -> bool {
match self {
Horizon::Spec(_) => true,
Horizon::Name(base) => match base {
HorizonBase::Void | HorizonBase::Ocean => false,
HorizonBase::Valley => true,
},
}
}
}
pub fn horizon_base(horizon: &Option<Horizon>) -> HorizonBase {
horizon.as_ref().map(|h| h.base()).unwrap_or_default()
}
pub fn resolved_horizon(horizon: &Option<Horizon>) -> ResolvedHorizon {
horizon.as_ref().map(|h| h.resolved()).unwrap_or_default()
}
fn default_margin() -> u16 {
16
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Boundary {
#[serde(default = "default_margin")]
pub margin: u16,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum Fixture {
Torch,
Lantern,
Campfire,
Shroomlight,
}
impl Fixture {
pub fn token(self) -> &'static str {
match self {
Fixture::Torch => "torch",
Fixture::Lantern => "lantern",
Fixture::Campfire => "campfire",
Fixture::Shroomlight => "shroomlight",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct AreaLighting {
pub fixture: Fixture,
#[serde(default = "default_min_light")]
pub min_light: u8,
}
fn default_min_light() -> u8 {
7
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum AreaMitigation {
NightVision,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Area {
pub id: AreaId,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prefab: Option<PrefabId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prefab_pool: Option<PoolId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pieces: Option<Pieces>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lighting: Option<AreaLighting>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mitigation: Option<AreaMitigation>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Pieces {
pub min: u32,
pub max: u32,
}
#[derive(
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "kebab-case")]
pub enum Locomotion {
Ground,
Climber,
Flier,
Aquatic,
}
impl Locomotion {
pub fn token(self) -> &'static str {
match self {
Locomotion::Ground => "ground",
Locomotion::Climber => "climber",
Locomotion::Flier => "flier",
Locomotion::Aquatic => "aquatic",
}
}
pub const ALL: [Locomotion; 4] = [
Locomotion::Ground,
Locomotion::Climber,
Locomotion::Flier,
Locomotion::Aquatic,
];
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct BodyTraversal {
pub locomotion: Locomotion,
}
#[derive(Clone, Copy, Debug)]
pub enum BodyRef<'a> {
Npc(&'a Npc),
Actor(&'a Actor),
}
impl<'a> BodyRef<'a> {
pub fn stage(self) -> &'static str {
match self {
BodyRef::Npc(_) => "npcs",
BodyRef::Actor(_) => "quests",
}
}
pub fn id(self) -> &'a str {
match self {
BodyRef::Npc(n) => n.id.as_str(),
BodyRef::Actor(a) => a.id.as_str(),
}
}
pub fn declared_entity(self) -> &'a str {
match self {
BodyRef::Npc(n) => n.base_entity.as_str(),
BodyRef::Actor(a) => a.entity.as_str(),
}
}
pub fn traversal(self) -> Option<&'a BodyTraversal> {
match self {
BodyRef::Npc(n) => n.traversal.as_ref(),
BodyRef::Actor(a) => a.traversal.as_ref(),
}
}
pub fn skin(self) -> Option<&'a NpcSkin> {
match self {
BodyRef::Npc(n) => n.skin.as_ref(),
BodyRef::Actor(a) => a.skin.as_ref(),
}
}
pub fn class(self) -> &'static str {
match self {
BodyRef::Npc(_) => "Npc",
BodyRef::Actor(_) => "Actor",
}
}
pub const ALL_CLASSES: [&'static str; 2] = ["Npc", "Actor"];
}
#[derive(Clone, Debug)]
pub struct BodySite<'a> {
pub body: BodyRef<'a>,
pub path: String,
}
pub fn body_sites(c: &crate::envelope::Campaign) -> Vec<BodySite<'_>> {
let mut out: Vec<BodySite<'_>> = Vec::new();
for (i, n) in c.npcs.content.npcs.iter().enumerate() {
out.push(BodySite {
body: BodyRef::Npc(n),
path: format!("/content/npcs/{i}"),
});
}
for (i, a) in c.quests.content.actors.iter().enumerate() {
out.push(BodySite {
body: BodyRef::Actor(a),
path: format!("/content/actors/{i}"),
});
}
out
}
#[derive(Clone, Debug)]
pub struct BodyTraversalSite<'a> {
pub body: BodyRef<'a>,
pub path: String,
pub traversal: &'a BodyTraversal,
}
pub fn body_traversal_sites(c: &crate::envelope::Campaign) -> Vec<BodyTraversalSite<'_>> {
body_sites(c)
.into_iter()
.filter_map(|s| {
s.body.traversal().map(|t| BodyTraversalSite {
body: s.body,
path: format!("{}/traversal", s.path),
traversal: t,
})
})
.collect()
}
#[derive(Clone, Debug)]
pub struct BodySkinSite<'a> {
pub body: BodyRef<'a>,
pub path: String,
pub skin: &'a NpcSkin,
}
pub fn body_skin_sites(c: &crate::envelope::Campaign) -> Vec<BodySkinSite<'_>> {
body_sites(c)
.into_iter()
.filter_map(|s| {
s.body.skin().map(|k| BodySkinSite {
body: s.body,
path: format!("{}/skin", s.path),
skin: k,
})
})
.collect()
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct NpcsContent {
pub npcs: Vec<Npc>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Npc {
pub id: NpcId,
pub name: String,
pub role: Role,
pub area: AreaId,
pub anchor: AnchorId,
pub base_entity: String,
pub persona: Persona,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub skin: Option<NpcSkin>,
#[serde(default, skip_serializing_if = "is_false")]
pub deferred: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub traversal: Option<BodyTraversal>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct NpcSkin {
pub texture_id: String,
pub model: SkinModel,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum SkinModel {
Wide,
Slim,
}
impl SkinModel {
pub fn token(self) -> &'static str {
match self {
SkinModel::Wide => "wide",
SkinModel::Slim => "slim",
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Persona {
pub archetype: String,
pub speech_style: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub demeanor: Option<String>,
pub motivation: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub secret: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub backstory: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub relationships: Vec<Relationship>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Relationship {
pub npc: NpcId,
pub attitude: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum Role {
QuestGiver,
Flavor,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct DialogueContent {
pub dialogues: Vec<NpcDialogue>,
}
impl DialogueContent {
pub fn tree_for(&self, npc: &str) -> Option<&NpcDialogue> {
self.dialogues.iter().find(|t| t.npc.as_str() == npc)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct NpcDialogue {
pub npc: NpcId,
pub root: DialogueId,
pub nodes: Vec<DialogueNode>,
}
impl NpcDialogue {
pub fn reachable_from<'a>(&'a self, roots: &[&str]) -> BTreeSet<&'a str> {
let by_id: BTreeMap<&'a str, &'a DialogueNode> =
self.nodes.iter().map(|n| (n.id.as_str(), n)).collect();
let mut seen: BTreeSet<&'a str> = BTreeSet::new();
let mut stack: Vec<&'a str> = roots
.iter()
.filter_map(|r| by_id.get_key_value(*r).map(|(k, _)| *k))
.collect();
while let Some(cur) = stack.pop() {
if !seen.insert(cur) {
continue;
}
let Some(node) = by_id.get(cur) else { continue };
for opt in &node.options {
if let Some(next) = &opt.next
&& let Some((k, _)) = by_id.get_key_value(next.as_str())
{
stack.push(k);
}
}
}
seen
}
pub fn completes_from<'a>(&'a self, roots: &[&str]) -> BTreeSet<&'a str> {
let seen = self.reachable_from(roots);
let mut out: BTreeSet<&'a str> = BTreeSet::new();
for node in &self.nodes {
if !seen.contains(node.id.as_str()) {
continue;
}
for opt in &node.options {
for eff in &opt.effects {
if let DialogueEffect::CompleteObjective { objective } = eff {
out.insert(objective.as_str());
}
}
}
}
out
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct DialogueNode {
pub id: DialogueId,
pub text: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub options: Vec<DialogueOption>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct DialogueOption {
pub label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tooltip: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next: Option<DialogueId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requires_flags: Vec<FlagId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub forbids_flags: Vec<FlagId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requires_state: Vec<StateCompare>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub effects: Vec<DialogueEffect>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub happening: Option<Happening>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "kebab-case", deny_unknown_fields)]
pub enum DialogueEffect {
CompleteObjective {
objective: ObjectiveId,
},
SetFlag {
flag: FlagId,
},
SetTime {
time: WorldTime,
},
SetWeather {
weather: WorldWeather,
},
SetCheckpoint {
anchor: AnchorId,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
on_respawn: Vec<QuestEffect>,
},
SpawnNpc {
npc: NpcId,
},
}
impl DialogueEffect {
pub fn v05_effect(&self) -> Option<&'static str> {
match self {
DialogueEffect::SetTime { .. } => Some("set-time"),
DialogueEffect::SetWeather { .. } => Some("set-weather"),
_ => None,
}
}
pub fn v06_effect(&self) -> Option<&'static str> {
match self {
DialogueEffect::SetCheckpoint { .. } => Some("set-checkpoint"),
DialogueEffect::SpawnNpc { .. } => Some("spawn-npc"),
_ => None,
}
}
pub fn spawn_npc(&self) -> Option<&NpcId> {
match self {
DialogueEffect::SpawnNpc { npc } => Some(npc),
_ => None,
}
}
pub fn set_checkpoint(&self) -> Option<(&AnchorId, &[QuestEffect])> {
match self {
DialogueEffect::SetCheckpoint { anchor, on_respawn } => {
Some((anchor, on_respawn.as_slice()))
}
_ => None,
}
}
pub fn set_checkpoint_on_respawn(&self) -> Option<&[QuestEffect]> {
match self {
DialogueEffect::SetCheckpoint { on_respawn, .. } => Some(on_respawn.as_slice()),
_ => None,
}
}
pub fn set_checkpoint_on_respawn_mut(&mut self) -> Option<&mut [QuestEffect]> {
match self {
DialogueEffect::SetCheckpoint { on_respawn, .. } => Some(on_respawn.as_mut_slice()),
_ => None,
}
}
}
impl DialogueEffect {
pub fn set_flag(&self) -> Option<&FlagId> {
match self {
DialogueEffect::SetFlag { flag } => Some(flag),
_ => None,
}
}
pub fn v04_effect(&self) -> Option<&'static str> {
match self {
DialogueEffect::SetFlag { .. } => Some("set-flag"),
_ => None,
}
}
pub fn set_time(&self) -> Option<WorldTime> {
match self {
DialogueEffect::SetTime { time } => Some(*time),
_ => None,
}
}
pub fn set_weather(&self) -> Option<WorldWeather> {
match self {
DialogueEffect::SetWeather { weather } => Some(*weather),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ClassesContent {
pub classes: Vec<Class>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Class {
pub id: ClassId,
pub name: String,
pub blurb: String,
pub kit: Vec<KitItem>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct KitItem {
pub item: String,
pub count: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub carrier: Option<Carrier>,
#[serde(default, skip_serializing_if = "is_false")]
pub flask: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub contents: Option<PotionContents>,
}
pub const POTION_BEARING_ITEMS: &[&str] = &[
"minecraft:lingering_potion",
"minecraft:potion",
"minecraft:splash_potion",
"minecraft:tipped_arrow",
];
pub fn is_potion_bearing_item(item_id: &str) -> bool {
let norm = if item_id.contains(':') {
item_id.to_string()
} else {
format!("minecraft:{item_id}")
};
POTION_BEARING_ITEMS.contains(&norm.as_str())
}
pub const INSTANT_EFFECTS: &[&str] = &["minecraft:instant_health", "minecraft:instant_damage"];
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct PotionContents {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub potion: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub effects: Vec<PotionEffect>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub color: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct PotionEffect {
pub effect: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub duration: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub amplifier: Option<u32>,
}
impl PotionEffect {
pub fn is_instant(&self) -> bool {
let norm = crate::registry::namespaced_effect_id(&self.effect);
INSTANT_EFFECTS.contains(&norm.as_str())
}
}
pub const MAX_POTION_DURATION_TICKS: u32 = 1_000_000;
pub const MAX_POTION_AMPLIFIER: u32 = 255;
pub const MAX_EFFECT_SECONDS: u32 = MAX_POTION_DURATION_TICKS / 20;
pub const BONFIRE_PROMPT_EN: &str = "Bonfire";
pub const BONFIRE_REST_LABEL_EN: &str = "Rest and save";
pub const BONFIRE_SAVE_LABEL_EN: &str = "Save only";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BonfireLabels<'a> {
pub prompt: Option<&'a str>,
pub rest_label: Option<&'a str>,
pub save_label: Option<&'a str>,
}
impl BonfireLabels<'_> {
pub fn prompt_or_default(&self) -> &str {
self.prompt.unwrap_or(BONFIRE_PROMPT_EN)
}
pub fn rest_or_default(&self) -> &str {
self.rest_label.unwrap_or(BONFIRE_REST_LABEL_EN)
}
pub fn save_or_default(&self) -> &str {
self.save_label.unwrap_or(BONFIRE_SAVE_LABEL_EN)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct QuestPlanContent {
pub quests: Vec<PlannedQuest>,
pub finale: QuestId,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub branch_points: Vec<BranchPoint>,
}
impl QuestPlanContent {
#[must_use]
pub fn spine(&self) -> BTreeSet<&str> {
let deps: BTreeMap<&str, &[QuestId]> = self
.quests
.iter()
.map(|q| (q.id.as_str(), q.depends_on.as_slice()))
.collect();
let mut spine: BTreeSet<&str> = BTreeSet::new();
let mut stack = vec![self.finale.as_str()];
while let Some(q) = stack.pop() {
if !spine.insert(q) {
continue;
}
for dep in deps.get(q).copied().unwrap_or(&[]) {
stack.push(dep.as_str());
}
}
spine
}
#[must_use]
pub fn optional(&self) -> BTreeSet<&str> {
self.quests
.iter()
.filter(|q| !q.mandatory)
.map(|q| q.id.as_str())
.collect()
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct BranchPoint {
pub id: BranchPointId,
pub opens_at: QuestId,
pub forks_on: Vec<FlagId>,
pub branches: Vec<BranchDecl>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct BranchDecl {
pub id: BranchId,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub flags: Vec<FlagId>,
pub leads_to: String,
}
impl BranchDecl {
pub fn converges_at(&self) -> Option<QuestId> {
let q = QuestId(self.leads_to.clone());
q.is_valid_syntax().then_some(q)
}
pub fn ending(&self) -> Option<EndingId> {
let e = EndingId(self.leads_to.clone());
e.is_valid_syntax().then_some(e)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Happening {
pub verb: HappeningVerb,
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subject: Option<String>,
}
#[derive(
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "kebab-case")]
pub enum HappeningVerb {
Dies,
Survives,
Departs,
Arrives,
Learns,
Believes,
Gains,
Loses,
Opens,
Seals,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct PlannedQuest {
pub id: QuestId,
pub goal: String,
pub area: AreaId,
pub npcs: Vec<NpcId>,
pub depends_on: Vec<QuestId>,
pub mandatory: bool,
pub act: u32,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct QuestsContent {
pub quests: Vec<Quest>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub waves: Vec<Wave>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub triggers: Vec<EnvTrigger>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub actors: Vec<Actor>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub traps: Vec<Trap>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub shortcuts: Vec<Shortcut>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub timed_gates: Vec<TimedGate>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub loot: Vec<Loot>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub state: Vec<StateDecl>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub on_death: Vec<QuestEffect>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub lethal_volumes: Vec<LethalVolume>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub shops: Vec<Shop>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub stakes: Vec<Stake>,
#[serde(default, skip_serializing)]
pub ambushes: Vec<Ambush>,
#[serde(skip)]
pub ambushes_expanded: bool,
}
impl QuestsContent {
pub fn all_triggers(&self) -> Vec<EnvTrigger> {
let mut out = self.triggers.clone();
out.extend(self.ambushes.iter().map(Ambush::to_trigger));
out
}
pub fn answers_press_at(&self, anchor: &str) -> bool {
self.all_triggers()
.iter()
.any(|t| matches!(t.on, TriggerOn::Use) && t.at_anchor() == Some(anchor))
}
pub fn expand_ambushes(&mut self) {
if self.ambushes_expanded || self.ambushes.is_empty() {
return;
}
self.triggers = self.all_triggers();
self.ambushes_expanded = true;
}
pub fn state_decl(&self, id: &str) -> Option<&StateDecl> {
self.state.iter().find(|s| s.id.as_str() == id)
}
pub fn stake_decl(&self, id: &str) -> Option<&Stake> {
self.stakes.iter().find(|s| s.id.as_str() == id)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum StateScope {
Player,
Party,
}
impl StateScope {
pub fn token(self) -> &'static str {
match self {
StateScope::Player => "player",
StateScope::Party => "party",
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct StateDecl {
pub id: StateId,
pub scope: StateScope,
#[serde(default, skip_serializing_if = "is_zero_i32")]
pub initial: i32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
fn is_zero_i32(v: &i32) -> bool {
*v == 0
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum CompareOp {
Equals,
NotEquals,
AtLeast,
AtMost,
}
impl CompareOp {
pub fn token(self) -> &'static str {
match self {
CompareOp::Equals => "equals",
CompareOp::NotEquals => "not-equals",
CompareOp::AtLeast => "at-least",
CompareOp::AtMost => "at-most",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StateWrite {
Set(i32),
Add(i32),
Clear,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct StateCompare {
pub state: StateId,
pub op: CompareOp,
pub value: i32,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Loot {
pub id: LootId,
pub anchor: AnchorId,
pub items: Vec<LootItem>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct LootItem {
pub item: String,
#[serde(default = "one_u32")]
pub count: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub enchantments: BTreeMap<String, u32>,
}
fn one_u32() -> u32 {
1
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Trap {
pub id: TrapId,
pub at: AnchorId,
pub trigger: TrapTrigger,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub effect: Option<TrapEffect>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub payload: Vec<QuestEffect>,
#[serde(default)]
pub lethality: Lethality,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disarm: Option<TrapDisarm>,
#[serde(default)]
pub reset: TrapReset,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requires_flags: Vec<FlagId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub forbids_flags: Vec<FlagId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requires_state: Vec<StateCompare>,
}
impl Trap {
pub fn dispense(&self) -> Option<(&str, u32)> {
match &self.effect {
Some(TrapEffect::Dispense { item, count }) => Some((item.as_str(), *count)),
None => None,
}
}
pub fn is_lethal(&self) -> bool {
matches!(self.lethality, Lethality::Lethal)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum TrapTrigger {
PressurePlate,
Tripwire,
TrappedChest,
}
impl TrapTrigger {
pub fn kind(&self) -> &'static str {
match self {
TrapTrigger::PressurePlate => "pressure-plate",
TrapTrigger::Tripwire => "tripwire",
TrapTrigger::TrappedChest => "trapped-chest",
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub enum TrapEffect {
Dispense {
item: String,
count: u32,
},
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum Lethality {
Lethal,
#[default]
Harmful,
Nonlethal,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum TrapReset {
Once,
#[default]
Rearm,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct TrapDisarm {
pub via: AnchorId,
pub sets_flag: FlagId,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct TimedGate {
pub id: TimedGateId,
pub gate: AnchorId,
pub open_ticks: u32,
pub closed_ticks: u32,
#[serde(default, skip_serializing_if = "is_zero")]
pub phase: u32,
#[serde(default, skip_serializing_if = "is_false")]
pub crush: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disarm: Option<TimedGateDisarm>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct TimedGateDisarm {
pub via: AnchorId,
pub sets_flag: FlagId,
}
fn is_zero(n: &u32) -> bool {
*n == 0
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Ambush {
pub id: AmbushId,
pub at: AnchorId,
pub actors: Vec<ActorId>,
pub trigger: TriggerOn,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub telegraph: Vec<QuestEffect>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub happening: Option<Happening>,
}
impl Ambush {
pub fn to_trigger(&self) -> EnvTrigger {
let mut effects = self.telegraph.clone();
for (i, a) in self.actors.iter().enumerate() {
effects.push(QuestEffect {
when: None,
happening: if i == 0 { self.happening.clone() } else { None },
verb: Verb::SpawnActor { actor: a.clone() },
});
}
for a in &self.actors {
effects.push(Verb::UnleashActor { actor: a.clone() }.into());
}
EnvTrigger {
id: TriggerId(format!(
"trigger/{}",
crate::l10n::local_id(self.id.as_str())
)),
at: Some(self.at.clone()),
on: self.trigger.clone(),
requires_flags: Vec::new(),
forbids_flags: Vec::new(),
requires_state: Vec::new(),
once: true,
audience: TriggerAudience::Party,
effects,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Shortcut {
pub id: ShortcutId,
pub gate: AnchorId,
pub unlock: AnchorId,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub on_unlock: Vec<QuestEffect>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct EnvTrigger {
pub id: TriggerId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub at: Option<AnchorId>,
pub on: TriggerOn,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requires_flags: Vec<FlagId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub forbids_flags: Vec<FlagId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requires_state: Vec<StateCompare>,
#[serde(default = "default_true")]
pub once: bool,
#[serde(default, skip_serializing_if = "TriggerAudience::is_party")]
pub audience: TriggerAudience,
pub effects: Vec<QuestEffect>,
}
impl EnvTrigger {
pub fn at_anchor(&self) -> Option<&str> {
self.at.as_ref().map(|a| a.as_str())
}
pub fn addresses_presser(&self) -> bool {
self.audience == TriggerAudience::Presser
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum TriggerAudience {
#[default]
Party,
Presser,
}
impl TriggerAudience {
fn is_party(&self) -> bool {
*self == TriggerAudience::Party
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "on", rename_all = "kebab-case", deny_unknown_fields)]
pub enum TriggerOn {
Strike,
Use,
Approach {
range: u32,
},
StrikeNpc {
npc: NpcId,
},
}
impl TriggerOn {
pub fn kind(&self) -> &'static str {
match self {
TriggerOn::Strike => "strike",
TriggerOn::Use => "use",
TriggerOn::Approach { .. } => "approach",
TriggerOn::StrikeNpc { .. } => "strike-npc",
}
}
pub fn needs_anchor(&self) -> bool {
!matches!(self, TriggerOn::StrikeNpc { .. })
}
pub fn npc_target(&self) -> Option<&NpcId> {
match self {
TriggerOn::StrikeNpc { npc } => Some(npc),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Wave {
pub id: WaveId,
pub anchor: AnchorId,
pub mobs: Vec<WaveMob>,
#[serde(default, skip_serializing_if = "is_false")]
pub respawns_on_rest: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lane: Option<WaveLane>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summon: Option<WaveSummon>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tier: Option<EncounterTier>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum EncounterTier {
#[default]
Ordinary,
Elite,
Boss,
}
impl EncounterTier {
pub fn token(self) -> &'static str {
match self {
EncounterTier::Ordinary => "ordinary",
EncounterTier::Elite => "elite",
EncounterTier::Boss => "boss",
}
}
pub fn has_floor_expectation(self) -> bool {
matches!(self, EncounterTier::Elite | EncounterTier::Boss)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum WaveSummon {
Anchor,
AggroEdge,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WaveLane {
pub waypoints: Vec<AnchorId>,
pub aggro_radius: u32,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WaveMob {
pub entity: String,
pub count: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attributes: Option<MobAttributes>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub effects: Vec<MobEffect>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub equipment: Option<MobEquipment>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub drops: Vec<MobDrop>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MobEquipment {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub head: Option<EquipItem>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub chest: Option<EquipItem>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub legs: Option<EquipItem>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub feet: Option<EquipItem>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub main_hand: Option<EquipItem>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub off_hand: Option<EquipItem>,
}
impl MobEquipment {
pub fn slots(&self) -> [(&'static str, Option<&EquipItem>); 6] {
[
("head", self.head.as_ref()),
("chest", self.chest.as_ref()),
("legs", self.legs.as_ref()),
("feet", self.feet.as_ref()),
("main_hand", self.main_hand.as_ref()),
("off_hand", self.off_hand.as_ref()),
]
}
pub fn filled(&self, slot: EquipSlot) -> Option<&EquipItem> {
match slot {
EquipSlot::Head => self.head.as_ref(),
EquipSlot::Chest => self.chest.as_ref(),
EquipSlot::Legs => self.legs.as_ref(),
EquipSlot::Feet => self.feet.as_ref(),
EquipSlot::MainHand => self.main_hand.as_ref(),
EquipSlot::OffHand => self.off_hand.as_ref(),
}
}
}
#[derive(
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum EquipSlot {
Head,
Chest,
Legs,
Feet,
MainHand,
OffHand,
}
impl EquipSlot {
pub fn field(self) -> &'static str {
match self {
EquipSlot::Head => "head",
EquipSlot::Chest => "chest",
EquipSlot::Legs => "legs",
EquipSlot::Feet => "feet",
EquipSlot::MainHand => "main_hand",
EquipSlot::OffHand => "off_hand",
}
}
pub fn nbt(self) -> &'static str {
match self {
EquipSlot::Head => "head",
EquipSlot::Chest => "chest",
EquipSlot::Legs => "legs",
EquipSlot::Feet => "feet",
EquipSlot::MainHand => "mainhand",
EquipSlot::OffHand => "offhand",
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum MobDrop {
Slot(SlotDrop),
Item(ItemDrop),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SlotDrop {
pub slot: EquipSlot,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ItemDrop {
pub item: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl MobDrop {
pub fn slot(&self) -> Option<EquipSlot> {
match self {
MobDrop::Slot(s) => Some(s.slot),
MobDrop::Item(_) => None,
}
}
pub fn item(&self) -> Option<&str> {
match self {
MobDrop::Slot(_) => None,
MobDrop::Item(i) => Some(&i.item),
}
}
pub fn name(&self) -> Option<&String> {
match self {
MobDrop::Slot(_) => None,
MobDrop::Item(i) => i.name.as_ref(),
}
}
pub fn name_mut(&mut self) -> Option<&mut String> {
match self {
MobDrop::Slot(_) => None,
MobDrop::Item(i) => i.name.as_mut(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum EquipItem {
Plain(String),
Enchanted(EnchantedItem),
}
impl EquipItem {
pub fn item(&self) -> &str {
match self {
EquipItem::Plain(s) => s,
EquipItem::Enchanted(e) => &e.item,
}
}
pub fn enchantments(&self) -> &BTreeMap<String, u32> {
static EMPTY: std::sync::LazyLock<BTreeMap<String, u32>> =
std::sync::LazyLock::new(BTreeMap::new);
match self {
EquipItem::Plain(_) => &EMPTY,
EquipItem::Enchanted(e) => &e.enchantments,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct EnchantedItem {
pub item: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub enchantments: BTreeMap<String, u32>,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MobAttributes {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_health: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attack_damage: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub movement_speed: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub follow_range: Option<f64>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MobEffect {
pub effect: String,
pub amplifier: u32,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Quest {
pub id: QuestId,
pub trigger: Trigger,
pub objectives: Vec<Objective>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub on_objective_complete: BTreeMap<ObjectiveId, Vec<QuestEffect>>,
pub on_complete: Vec<QuestEffect>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub cast: BTreeMap<NpcId, CastEntry>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub happening: Option<Happening>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum CastEntry {
Absent(CastAbsence),
Placement(CastPlacement),
Branches(Vec<CastPlacement>),
}
impl CastEntry {
pub fn placements(&self) -> Vec<&CastPlacement> {
match self {
CastEntry::Absent(_) => Vec::new(),
CastEntry::Placement(p) => vec![p],
CastEntry::Branches(ps) => ps.iter().collect(),
}
}
pub fn placements_mut(&mut self) -> Vec<&mut CastPlacement> {
match self {
CastEntry::Absent(_) => Vec::new(),
CastEntry::Placement(p) => vec![p],
CastEntry::Branches(ps) => ps.iter_mut().collect(),
}
}
pub fn absence(&self) -> Option<CastAbsence> {
match self {
CastEntry::Absent(a) => Some(*a),
_ => None,
}
}
pub fn is_absent(&self) -> bool {
match self {
CastEntry::Absent(_) => true,
CastEntry::Placement(p) => p.at.absence().is_some(),
CastEntry::Branches(ps) => {
!ps.is_empty() && ps.iter().all(|p| p.at.absence().is_some())
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum CastAbsence {
Dead,
Offstage,
}
impl CastAbsence {
pub fn token(self) -> &'static str {
match self {
CastAbsence::Dead => "dead",
CastAbsence::Offstage => "offstage",
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum CastPlace {
Absent(CastAbsence),
Anchor(AnchorId),
}
impl CastPlace {
pub fn anchor(&self) -> Option<&AnchorId> {
match self {
CastPlace::Anchor(a) => Some(a),
CastPlace::Absent(_) => None,
}
}
pub fn absence(&self) -> Option<CastAbsence> {
match self {
CastPlace::Absent(a) => Some(*a),
CastPlace::Anchor(_) => None,
}
}
pub fn token(&self) -> &str {
match self {
CastPlace::Absent(a) => a.token(),
CastPlace::Anchor(a) => a.as_str(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CastPlacement {
pub at: CastPlace,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub doing: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dialogue: Option<CastDialogue>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requires_flags: Vec<FlagId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub forbids_flags: Vec<FlagId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requires_state: Vec<StateCompare>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum CastDialogue {
Keyword(CastDialogueKeyword),
Barks(CastBarks),
Root(DialogueId),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum CastDialogueKeyword {
None,
Unchanged,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CastBarks {
pub barks: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "kebab-case", deny_unknown_fields)]
pub enum Trigger {
CampaignStart,
QuestComplete {
quest: QuestId,
},
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "kebab-case", deny_unknown_fields)]
pub enum Objective {
TalkTo {
id: ObjectiveId,
#[serde(default, skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
hint: Option<String>,
npc: NpcId,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
after: Vec<ObjectiveId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
requires_flags: Vec<FlagId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
forbids_flags: Vec<FlagId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
requires_state: Vec<StateCompare>,
#[serde(default, skip_serializing_if = "is_false")]
stealth: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
happening: Option<Happening>,
},
ReachAnchor {
id: ObjectiveId,
#[serde(default, skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
hint: Option<String>,
anchor: AnchorId,
radius: u32,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
after: Vec<ObjectiveId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
requires_flags: Vec<FlagId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
forbids_flags: Vec<FlagId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
requires_state: Vec<StateCompare>,
#[serde(default, skip_serializing_if = "is_false")]
stealth: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
happening: Option<Happening>,
},
Kill {
id: ObjectiveId,
#[serde(default, skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
hint: Option<String>,
wave: WaveId,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
after: Vec<ObjectiveId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
requires_flags: Vec<FlagId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
forbids_flags: Vec<FlagId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
requires_state: Vec<StateCompare>,
#[serde(default, skip_serializing_if = "is_false")]
stealth: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
happening: Option<Happening>,
},
Collect {
id: ObjectiveId,
#[serde(default, skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
hint: Option<String>,
item: String,
count: u32,
anchor: AnchorId,
#[serde(default, skip_serializing_if = "Option::is_none")]
container: Option<AnchorId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
dropped_by: Option<WaveId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
item_name: Option<String>,
#[serde(default, skip_serializing_if = "is_zero")]
fill_count: u32,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
after: Vec<ObjectiveId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
requires_flags: Vec<FlagId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
forbids_flags: Vec<FlagId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
requires_state: Vec<StateCompare>,
#[serde(default, skip_serializing_if = "is_false")]
stealth: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
happening: Option<Happening>,
},
Interact {
id: ObjectiveId,
#[serde(default, skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
hint: Option<String>,
anchor: AnchorId,
#[serde(default, skip_serializing_if = "Option::is_none")]
requires_item: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
missing_item_hint: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
prop: Option<Prop>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
after: Vec<ObjectiveId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
requires_flags: Vec<FlagId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
forbids_flags: Vec<FlagId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
requires_state: Vec<StateCompare>,
#[serde(default, skip_serializing_if = "is_false")]
stealth: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
happening: Option<Happening>,
},
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Prop {
pub block: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Actor {
pub id: ActorId,
pub entity: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub skin: Option<NpcSkin>,
pub anchor: AnchorId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub facing: Option<Facing>,
#[serde(default, skip_serializing_if = "is_false")]
pub vulnerable: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub equipment: Option<MobEquipment>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attributes: Option<MobAttributes>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tier: Option<EncounterTier>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub drops: Vec<MobDrop>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub traversal: Option<BodyTraversal>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum Facing {
South,
North,
West,
East,
}
impl Facing {
pub fn token(self) -> &'static str {
match self {
Facing::South => "south",
Facing::North => "north",
Facing::West => "west",
Facing::East => "east",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum DespawnStyle {
Vanish,
Kill,
}
impl DespawnStyle {
pub fn token(self) -> &'static str {
match self {
DespawnStyle::Vanish => "vanish",
DespawnStyle::Kill => "kill",
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SequenceStep {
pub at_ticks: u32,
pub effects: Vec<QuestEffect>,
}
impl Objective {
pub fn id(&self) -> &ObjectiveId {
match self {
Objective::TalkTo { id, .. }
| Objective::ReachAnchor { id, .. }
| Objective::Kill { id, .. }
| Objective::Collect { id, .. }
| Objective::Interact { id, .. } => id,
}
}
pub fn after(&self) -> &[ObjectiveId] {
match self {
Objective::TalkTo { after, .. }
| Objective::ReachAnchor { after, .. }
| Objective::Kill { after, .. }
| Objective::Collect { after, .. }
| Objective::Interact { after, .. } => after,
}
}
pub fn title(&self) -> Option<&str> {
match self {
Objective::TalkTo { title, .. }
| Objective::ReachAnchor { title, .. }
| Objective::Kill { title, .. }
| Objective::Collect { title, .. }
| Objective::Interact { title, .. } => title.as_deref(),
}
}
pub fn hint(&self) -> Option<&str> {
match self {
Objective::TalkTo { hint, .. }
| Objective::ReachAnchor { hint, .. }
| Objective::Kill { hint, .. }
| Objective::Collect { hint, .. }
| Objective::Interact { hint, .. } => hint.as_deref(),
}
}
pub fn title_mut(&mut self) -> &mut Option<String> {
match self {
Objective::TalkTo { title, .. }
| Objective::ReachAnchor { title, .. }
| Objective::Kill { title, .. }
| Objective::Collect { title, .. }
| Objective::Interact { title, .. } => title,
}
}
pub fn hint_mut(&mut self) -> &mut Option<String> {
match self {
Objective::TalkTo { hint, .. }
| Objective::ReachAnchor { hint, .. }
| Objective::Kill { hint, .. }
| Objective::Collect { hint, .. }
| Objective::Interact { hint, .. } => hint,
}
}
pub fn requires_flags(&self) -> &[FlagId] {
match self {
Objective::TalkTo { requires_flags, .. }
| Objective::ReachAnchor { requires_flags, .. }
| Objective::Kill { requires_flags, .. }
| Objective::Collect { requires_flags, .. }
| Objective::Interact { requires_flags, .. } => requires_flags,
}
}
pub fn forbids_flags(&self) -> &[FlagId] {
match self {
Objective::TalkTo { forbids_flags, .. }
| Objective::ReachAnchor { forbids_flags, .. }
| Objective::Kill { forbids_flags, .. }
| Objective::Collect { forbids_flags, .. }
| Objective::Interact { forbids_flags, .. } => forbids_flags,
}
}
pub fn requires_state(&self) -> &[StateCompare] {
match self {
Objective::TalkTo { requires_state, .. }
| Objective::ReachAnchor { requires_state, .. }
| Objective::Kill { requires_state, .. }
| Objective::Collect { requires_state, .. }
| Objective::Interact { requires_state, .. } => requires_state,
}
}
pub fn happening(&self) -> Option<&Happening> {
match self {
Objective::TalkTo { happening, .. }
| Objective::ReachAnchor { happening, .. }
| Objective::Kill { happening, .. }
| Objective::Collect { happening, .. }
| Objective::Interact { happening, .. } => happening.as_ref(),
}
}
pub fn stealth(&self) -> bool {
match self {
Objective::TalkTo { stealth, .. }
| Objective::ReachAnchor { stealth, .. }
| Objective::Kill { stealth, .. }
| Objective::Collect { stealth, .. }
| Objective::Interact { stealth, .. } => *stealth,
}
}
pub fn collect_container(&self) -> Option<&AnchorId> {
match self {
Objective::Collect { container, .. } => container.as_ref(),
_ => None,
}
}
pub fn collect_dropped_by(&self) -> Option<&WaveId> {
match self {
Objective::Collect { dropped_by, .. } => dropped_by.as_ref(),
_ => None,
}
}
pub fn collect_fill_count(&self) -> u32 {
match self {
Objective::Collect { fill_count, .. } => *fill_count,
_ => 0,
}
}
pub fn prop(&self) -> Option<&Prop> {
match self {
Objective::Interact { prop, .. } => prop.as_ref(),
_ => None,
}
}
pub fn kind(&self) -> &'static str {
match self {
Objective::TalkTo { .. } => "talk-to",
Objective::ReachAnchor { .. } => "reach-anchor",
Objective::Kill { .. } => "kill",
Objective::Collect { .. } => "collect",
Objective::Interact { .. } => "interact",
}
}
pub fn v03_verb(&self) -> Option<&'static str> {
match self {
Objective::Kill { .. } => Some("kill"),
Objective::Collect { .. } => Some("collect"),
Objective::Interact { .. } => Some("interact"),
Objective::TalkTo { .. } | Objective::ReachAnchor { .. } => None,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Guard {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requires_flags: Vec<FlagId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub forbids_flags: Vec<FlagId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requires_state: Vec<StateCompare>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct QuestEffect {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub when: Option<Guard>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub happening: Option<Happening>,
#[serde(flatten)]
pub verb: Verb,
}
impl From<Verb> for QuestEffect {
fn from(verb: Verb) -> Self {
QuestEffect {
when: None,
happening: None,
verb,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "kebab-case", deny_unknown_fields)]
pub enum Verb {
OpenGate {
anchor: AnchorId,
},
CloseGate {
anchor: AnchorId,
#[serde(default, skip_serializing_if = "Option::is_none")]
sealed_hint: Option<String>,
},
CampaignComplete {
#[serde(default, skip_serializing_if = "Option::is_none")]
ending: Option<EndingId>,
},
GiveItem {
item: String,
count: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
carrier: Option<Carrier>,
},
SetFlag {
flag: FlagId,
},
SetState {
state: StateId,
value: i32,
},
AddState {
state: StateId,
amount: i32,
},
DropStake {
stake: StakeId,
},
ClearState {
state: StateId,
},
SpawnWave {
wave: WaveId,
},
Narrate {
text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
style: Option<NarrateStyle>,
#[serde(default, skip_serializing_if = "Option::is_none")]
sound: Option<String>,
},
SetBlock {
anchor: AnchorId,
block: String,
},
FillRegion {
region: StealthZone,
block: String,
},
ClearRegion {
region: StealthZone,
},
OpenWay {
piece: PrefabId,
way: String,
},
DespawnNpc {
npc: NpcId,
},
MoveNpc {
npc: NpcId,
to_anchor: AnchorId,
#[serde(default, skip_serializing_if = "Option::is_none")]
speed: Option<f64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
on_arrive: Vec<QuestEffect>,
},
Cutscene {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
shots: Vec<CameraShot>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
path: Vec<CameraWaypoint>,
#[serde(default, skip_serializing_if = "Option::is_none")]
seconds: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
look_at: Option<CameraTarget>,
},
SetTime {
time: WorldTime,
},
SetWeather {
weather: WorldWeather,
},
PlaySound {
sound: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
at: Option<SoundAt>,
#[serde(default, skip_serializing_if = "Option::is_none")]
volume: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pitch: Option<f64>,
},
DamagePlayers {
amount: u32,
#[serde(default, rename = "in", skip_serializing_if = "Option::is_none")]
within: Option<StealthZone>,
#[serde(default, skip_serializing_if = "Option::is_none")]
damage_type: Option<DamageKind>,
},
SetCheckpoint {
anchor: AnchorId,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
on_respawn: Vec<QuestEffect>,
},
Bonfire {
anchor: AnchorId,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
on_rest: Vec<QuestEffect>,
#[serde(default, skip_serializing_if = "Option::is_none")]
prompt: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
rest_label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
save_label: Option<String>,
},
BeginStealth {
zones: Vec<StealthZone>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
on_caught: Vec<QuestEffect>,
#[serde(default = "default_grace_ticks")]
grace_ticks: u32,
},
EndStealth,
SpawnNpc {
npc: NpcId,
},
SpawnActor {
actor: ActorId,
},
DespawnActor {
actor: ActorId,
style: DespawnStyle,
},
MoveActor {
actor: ActorId,
to_anchor: AnchorId,
#[serde(default, skip_serializing_if = "Option::is_none")]
speed: Option<f64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
on_arrive: Vec<QuestEffect>,
},
UnleashActor {
actor: ActorId,
},
Sequence {
steps: Vec<SequenceStep>,
},
Volley {
#[serde(default, skip_serializing_if = "Option::is_none")]
projectile: Option<String>,
from_anchor: AnchorId,
kill_zone: StealthZone,
#[serde(default, skip_serializing_if = "Option::is_none")]
salvos: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
interval: Option<u32>,
},
Collapse {
region_anchor: StealthZone,
#[serde(default, skip_serializing_if = "Option::is_none")]
falling_block: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
then_floor: Option<String>,
},
GiveEffect {
effect: String,
seconds: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
amplifier: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
hide_particles: Option<bool>,
#[serde(default, rename = "in", skip_serializing_if = "Option::is_none")]
within: Option<StealthZone>,
},
ClearEffect {
#[serde(default, skip_serializing_if = "Option::is_none")]
effect: Option<String>,
#[serde(default, rename = "in", skip_serializing_if = "Option::is_none")]
within: Option<StealthZone>,
},
Teleport {
from: StealthZone,
to: AnchorId,
},
}
fn default_grace_ticks() -> u32 {
20
}
pub const DEFAULT_VOLLEY_PROJECTILE: &str = "minecraft:arrow";
pub const DEFAULT_VOLLEY_SALVOS: u32 = 3;
pub const DEFAULT_VOLLEY_INTERVAL: u32 = 10;
pub const MAX_VOLLEY_SALVOS: u32 = 16;
pub const MAX_VOLLEY_INTERVAL: u32 = 200;
pub const DEFAULT_COLLAPSE_FALLING_BLOCK: &str = "minecraft:gravel";
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct StealthZone {
pub anchor: AnchorId,
pub extent: [u32; 3],
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "at", rename_all = "kebab-case", deny_unknown_fields)]
pub enum SoundAt {
Anchor {
anchor: AnchorId,
},
Players,
Actor {
actor: String,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum DamageKind {
Generic,
Magic,
Wither,
Fire,
Drown,
Freeze,
Fall,
LightningBolt,
Explosion,
}
impl DamageKind {
pub fn id(self) -> &'static str {
match self {
DamageKind::Generic => "minecraft:generic",
DamageKind::Magic => "minecraft:magic",
DamageKind::Wither => "minecraft:wither",
DamageKind::Fire => "minecraft:on_fire",
DamageKind::Drown => "minecraft:drown",
DamageKind::Freeze => "minecraft:freeze",
DamageKind::Fall => "minecraft:fall",
DamageKind::LightningBolt => "minecraft:lightning_bolt",
DamageKind::Explosion => "minecraft:explosion",
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct LethalVolume {
pub id: LethalVolumeId,
pub region: StealthZone,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub damage_type: Option<DamageKind>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Shop {
pub id: ShopId,
pub anchor: AnchorId,
pub title: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub marker_item: Option<String>,
pub offers: Vec<ShopOffer>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ShopOffer {
pub label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tooltip: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requires_flags: Vec<FlagId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub forbids_flags: Vec<FlagId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requires_state: Vec<StateCompare>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub effects: Vec<QuestEffect>,
}
impl ShopOffer {
pub fn gate(&self) -> crate::gate::Gate<'_> {
crate::gate::Gate::of(
&self.requires_flags,
&self.forbids_flags,
&self.requires_state,
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
pub enum Forfeit {
All,
Proportion {
percent: u32,
},
Fixed {
amount: i32,
},
None,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum OnFull {
Replace,
Keep,
}
impl OnFull {
pub fn token(self) -> &'static str {
match self {
OnFull::Replace => "replace",
OnFull::Keep => "keep",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum CollectBy {
Owner,
Anyone,
}
impl CollectBy {
pub fn token(self) -> &'static str {
match self {
CollectBy::Owner => "owner",
CollectBy::Anyone => "anyone",
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Stake {
pub id: StakeId,
pub state: StateId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub forfeit: Option<Forfeit>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_live: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub on_full: Option<OnFull>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub collect_by: Option<CollectBy>,
pub collected_message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub marker_item: Option<String>,
}
impl Stake {
pub fn forfeit(&self) -> Forfeit {
self.forfeit.unwrap_or(Forfeit::All)
}
pub fn max_live(&self) -> u32 {
self.max_live.unwrap_or(1)
}
pub fn on_full(&self) -> OnFull {
self.on_full.unwrap_or(OnFull::Replace)
}
pub fn collect_by(&self) -> CollectBy {
self.collect_by.unwrap_or(CollectBy::Owner)
}
pub fn marker_item(&self) -> &str {
self.marker_item
.as_deref()
.unwrap_or("minecraft:soul_lantern")
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum NarrateStyle {
Chat,
Title,
Subtitle,
Art,
Actionbar,
}
impl NarrateStyle {
pub fn token(self) -> &'static str {
match self {
NarrateStyle::Chat => "chat",
NarrateStyle::Title => "title",
NarrateStyle::Subtitle => "subtitle",
NarrateStyle::Art => "art",
NarrateStyle::Actionbar => "actionbar",
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CameraWaypoint {
pub anchor: AnchorId,
#[serde(default, skip_serializing_if = "is_zero3")]
pub offset: [i32; 3],
}
#[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CameraShot {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub path: Vec<CameraWaypoint>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub seconds: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub look_at: Option<CameraTarget>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub shot_style: Option<ShotStyle>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subject: Option<CameraSubject>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subject_b: Option<CameraSubject>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dist: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub degrees: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bearing: Option<f64>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum ShotStyle {
Insert,
LockedOff,
PushIn,
PullBackReveal,
EstablishingCrane,
OrbitArc,
SideTrack,
TwoShot,
LowFollow,
}
impl ShotStyle {
pub fn token(self) -> &'static str {
match self {
ShotStyle::Insert => "insert",
ShotStyle::LockedOff => "locked-off",
ShotStyle::PushIn => "push-in",
ShotStyle::PullBackReveal => "pull-back-reveal",
ShotStyle::EstablishingCrane => "establishing-crane",
ShotStyle::OrbitArc => "orbit-arc",
ShotStyle::SideTrack => "side-track",
ShotStyle::TwoShot => "two-shot",
ShotStyle::LowFollow => "low-follow",
}
}
pub fn default_seconds(self) -> u32 {
match self {
ShotStyle::Insert => 2,
ShotStyle::LockedOff => 6,
ShotStyle::PushIn => 4,
ShotStyle::PullBackReveal => 6,
ShotStyle::EstablishingCrane => 8,
ShotStyle::OrbitArc => 8,
ShotStyle::SideTrack => 8,
ShotStyle::TwoShot => 5,
ShotStyle::LowFollow => 5,
}
}
pub fn needs_moving_subject(self) -> bool {
matches!(self, ShotStyle::SideTrack | ShotStyle::LowFollow)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum CameraSubject {
Anchor(AnchorSubject),
Npc(NpcSubject),
Actor(ActorSubject),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct AnchorSubject {
pub anchor: AnchorId,
#[serde(default, skip_serializing_if = "is_zero3")]
pub offset: [i32; 3],
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct NpcSubject {
pub npc: NpcId,
#[serde(default, skip_serializing_if = "is_zero3")]
pub offset: [i32; 3],
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ActorSubject {
pub actor: ActorId,
#[serde(default, skip_serializing_if = "is_zero3")]
pub offset: [i32; 3],
}
impl CameraSubject {
pub fn offset(&self) -> [i32; 3] {
match self {
CameraSubject::Anchor(s) => s.offset,
CameraSubject::Npc(s) => s.offset,
CameraSubject::Actor(s) => s.offset,
}
}
pub fn canon(&self) -> String {
let (kind, id, o) = match self {
CameraSubject::Anchor(s) => ("a", s.anchor.as_str(), &s.offset),
CameraSubject::Npc(s) => ("n", s.npc.as_str(), &s.offset),
CameraSubject::Actor(s) => ("c", s.actor.as_str(), &s.offset),
};
format!("{kind}:{id}@{},{},{}", o[0], o[1], o[2])
}
}
impl std::fmt::Debug for CameraShot {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut d = f.debug_struct("CameraShot");
d.field("path", &self.path);
match self.seconds {
Some(v) => d.field("seconds", &v),
None => d.field("seconds", &self.seconds),
};
d.field("look_at", &self.look_at);
if self.shot_style.is_some() {
d.field("shot_style", &self.shot_style);
}
if self.subject.is_some() {
d.field("subject", &self.subject);
}
if self.subject_b.is_some() {
d.field("subject_b", &self.subject_b);
}
if self.dist.is_some() {
d.field("dist", &self.dist);
}
if self.degrees.is_some() {
d.field("degrees", &self.degrees);
}
if self.bearing.is_some() {
d.field("bearing", &self.bearing);
}
d.finish()
}
}
impl CameraShot {
pub fn resolved_seconds(&self) -> u32 {
self.seconds
.or(self.shot_style.map(ShotStyle::default_seconds))
.unwrap_or(1)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CameraTarget {
pub anchor: AnchorId,
#[serde(default, skip_serializing_if = "is_zero3")]
pub offset: [i32; 3],
}
fn is_zero3(v: &[i32; 3]) -> bool {
*v == [0, 0, 0]
}
impl Verb {
pub fn tag(&self) -> &'static str {
match self {
Verb::OpenGate { .. } => "open-gate",
Verb::CloseGate { .. } => "close-gate",
Verb::CampaignComplete { .. } => "campaign-complete",
Verb::GiveItem { .. } => "give-item",
Verb::SetFlag { .. } => "set-flag",
Verb::SetState { .. } => "set-state",
Verb::AddState { .. } => "add-state",
Verb::ClearState { .. } => "clear-state",
Verb::DropStake { .. } => "drop-stake",
Verb::SpawnWave { .. } => "spawn-wave",
Verb::Narrate { .. } => "narrate",
Verb::SetBlock { .. } => "set-block",
Verb::FillRegion { .. } => "fill-region",
Verb::ClearRegion { .. } => "clear-region",
Verb::OpenWay { .. } => "open-way",
Verb::DespawnNpc { .. } => "despawn-npc",
Verb::MoveNpc { .. } => "move-npc",
Verb::Cutscene { .. } => "cutscene",
Verb::SetTime { .. } => "set-time",
Verb::SetWeather { .. } => "set-weather",
Verb::PlaySound { .. } => "play-sound",
Verb::DamagePlayers { .. } => "damage-players",
Verb::SetCheckpoint { .. } => "set-checkpoint",
Verb::Bonfire { .. } => "bonfire",
Verb::BeginStealth { .. } => "begin-stealth",
Verb::EndStealth => "end-stealth",
Verb::SpawnNpc { .. } => "spawn-npc",
Verb::SpawnActor { .. } => "spawn-actor",
Verb::DespawnActor { .. } => "despawn-actor",
Verb::MoveActor { .. } => "move-actor",
Verb::UnleashActor { .. } => "unleash-actor",
Verb::Sequence { .. } => "sequence",
Verb::Volley { .. } => "volley",
Verb::Collapse { .. } => "collapse",
Verb::GiveEffect { .. } => "give-effect",
Verb::ClearEffect { .. } => "clear-effect",
Verb::Teleport { .. } => "teleport",
}
}
}
impl QuestEffect {
pub fn open_gate_anchor(&self) -> Option<&AnchorId> {
match &self.verb {
Verb::OpenGate { anchor, .. } => Some(anchor),
_ => None,
}
}
pub fn close_gate_anchor(&self) -> Option<&AnchorId> {
match &self.verb {
Verb::CloseGate { anchor, .. } => Some(anchor),
_ => None,
}
}
pub fn close_gate_sealed_hint(&self) -> Option<&str> {
match &self.verb {
Verb::CloseGate { sealed_hint, .. } => sealed_hint.as_deref(),
_ => None,
}
}
pub fn spawn_wave(&self) -> Option<&WaveId> {
match &self.verb {
Verb::SpawnWave { wave, .. } => Some(wave),
_ => None,
}
}
pub fn set_flag(&self) -> Option<&FlagId> {
match &self.verb {
Verb::SetFlag { flag, .. } => Some(flag),
_ => None,
}
}
pub fn give_item(&self) -> Option<&str> {
match &self.verb {
Verb::GiveItem { item, .. } => Some(item),
_ => None,
}
}
pub fn give_item_named(&self) -> bool {
matches!(self.verb, Verb::GiveItem { name: Some(_), .. })
}
pub fn give_carrier(&self) -> Option<Carrier> {
match &self.verb {
Verb::GiveItem { carrier, .. } => *carrier,
_ => None,
}
}
pub fn gives_to_one(&self) -> bool {
matches!(self.give_carrier(), Some(Carrier::One))
}
pub fn set_block(&self) -> Option<(&AnchorId, &str)> {
match &self.verb {
Verb::SetBlock { anchor, block, .. } => Some((anchor, block.as_str())),
_ => None,
}
}
pub fn despawn_npc(&self) -> Option<&NpcId> {
match &self.verb {
Verb::DespawnNpc { npc, .. } => Some(npc),
_ => None,
}
}
pub fn move_npc(&self) -> Option<(&NpcId, &AnchorId)> {
match &self.verb {
Verb::MoveNpc { npc, to_anchor, .. } => Some((npc, to_anchor)),
_ => None,
}
}
pub fn v03_effect(&self) -> Option<&'static str> {
match &self.verb {
Verb::GiveItem { .. } => Some("give-item"),
Verb::SetFlag { .. } => Some("set-flag"),
Verb::SpawnWave { .. } => Some("spawn-wave"),
Verb::OpenGate { .. }
| Verb::CloseGate { .. }
| Verb::CampaignComplete { .. } => None,
Verb::Narrate { .. }
| Verb::SetBlock { .. }
| Verb::DespawnNpc { .. }
| Verb::MoveNpc { .. }
| Verb::Cutscene { .. }
| Verb::SetTime { .. }
| Verb::SetWeather { .. }
| Verb::PlaySound { .. }
| Verb::DamagePlayers { .. }
| Verb::SetCheckpoint { .. }
| Verb::Bonfire { .. }
| Verb::BeginStealth { .. }
| Verb::EndStealth
| Verb::SpawnActor { .. }
| Verb::DespawnActor { .. }
| Verb::MoveActor { .. }
| Verb::UnleashActor { .. }
| Verb::SpawnNpc { .. }
| Verb::Sequence { .. }
| Verb::Volley { .. }
| Verb::Collapse { .. }
| Verb::SetState { .. }
| Verb::AddState { .. }
| Verb::ClearState { .. }
| Verb::FillRegion { .. }
| Verb::ClearRegion { .. }
| Verb::OpenWay { .. }
| Verb::GiveEffect { .. }
| Verb::ClearEffect { .. }
| Verb::Teleport { .. }
| Verb::DropStake { .. } => None,
}
}
pub fn v04_effect(&self) -> Option<&'static str> {
match &self.verb {
Verb::Narrate { .. } => Some("narrate"),
Verb::SetBlock { .. } => Some("set-block"),
Verb::DespawnNpc { .. } => Some("despawn-npc"),
Verb::MoveNpc { .. } => Some("move-npc"),
Verb::Cutscene { .. } => Some("cutscene"),
_ => None,
}
}
pub fn v05_effect(&self) -> Option<&'static str> {
match &self.verb {
Verb::SetTime { .. } => Some("set-time"),
Verb::SetWeather { .. } => Some("set-weather"),
_ => None,
}
}
pub fn v06_effect(&self) -> Option<&'static str> {
match &self.verb {
Verb::CloseGate { .. } => Some("close-gate"),
Verb::SetCheckpoint { .. } => Some("set-checkpoint"),
Verb::Bonfire { .. } => Some("bonfire"),
Verb::BeginStealth { .. } => Some("begin-stealth"),
Verb::EndStealth => Some("end-stealth"),
Verb::PlaySound { .. } => Some("play-sound"),
Verb::DamagePlayers { .. } => Some("damage-players"),
Verb::SpawnActor { .. } => Some("spawn-actor"),
Verb::DespawnActor { .. } => Some("despawn-actor"),
Verb::MoveActor { .. } => Some("move-actor"),
Verb::UnleashActor { .. } => Some("unleash-actor"),
Verb::Sequence { .. } => Some("sequence"),
Verb::SpawnNpc { .. } => Some("spawn-npc"),
Verb::Volley { .. } => Some("volley"),
Verb::Collapse { .. } => Some("collapse"),
_ => None,
}
}
pub fn v10_effect(&self) -> Option<&'static str> {
match &self.verb {
Verb::SetState { .. } => Some("set-state"),
Verb::AddState { .. } => Some("add-state"),
Verb::ClearState { .. } => Some("clear-state"),
Verb::FillRegion { .. } => Some("fill-region"),
Verb::ClearRegion { .. } => Some("clear-region"),
Verb::GiveEffect { .. } => Some("give-effect"),
Verb::ClearEffect { .. } => Some("clear-effect"),
Verb::Teleport { .. } => Some("teleport"),
Verb::DropStake { .. } => Some("drop-stake"),
_ => None,
}
}
pub fn v12_effect(&self) -> Option<&'static str> {
match &self.verb {
Verb::OpenWay { .. } => Some("open-way"),
_ => None,
}
}
pub fn way_write(&self) -> Option<(&PrefabId, &str)> {
match &self.verb {
Verb::OpenWay { piece, way, .. } => Some((piece, way.as_str())),
_ => None,
}
}
pub fn region_write(&self) -> Option<(&StealthZone, Option<&str>)> {
match &self.verb {
Verb::FillRegion { region, block, .. } => Some((region, Some(block.as_str()))),
Verb::ClearRegion { region, .. } => Some((region, None)),
_ => None,
}
}
pub fn gate_region_write(&self) -> Option<(&AnchorId, bool)> {
match &self.verb {
Verb::CloseGate { anchor, .. } => Some((anchor, true)),
Verb::OpenGate { anchor, .. } => Some((anchor, false)),
_ => None,
}
}
pub fn volley(&self) -> Option<(&str, &AnchorId, &StealthZone, u32, u32)> {
match &self.verb {
Verb::Volley {
projectile,
from_anchor,
kill_zone,
salvos,
interval,
..
} => Some((
projectile.as_deref().unwrap_or(DEFAULT_VOLLEY_PROJECTILE),
from_anchor,
kill_zone,
salvos.unwrap_or(DEFAULT_VOLLEY_SALVOS),
interval.unwrap_or(DEFAULT_VOLLEY_INTERVAL),
)),
_ => None,
}
}
pub fn collapse(&self) -> Option<(&StealthZone, &str, Option<&str>)> {
match &self.verb {
Verb::Collapse {
region_anchor,
falling_block,
then_floor,
..
} => Some((
region_anchor,
falling_block
.as_deref()
.unwrap_or(DEFAULT_COLLAPSE_FALLING_BLOCK),
then_floor.as_deref(),
)),
_ => None,
}
}
pub fn spawn_npc(&self) -> Option<&NpcId> {
match &self.verb {
Verb::SpawnNpc { npc, .. } => Some(npc),
_ => None,
}
}
pub fn set_checkpoint(&self) -> Option<(&AnchorId, &[QuestEffect])> {
match &self.verb {
Verb::SetCheckpoint { anchor, on_respawn } => Some((anchor, on_respawn.as_slice())),
_ => None,
}
}
pub fn bonfire(&self) -> Option<(&AnchorId, &[QuestEffect])> {
match &self.verb {
Verb::Bonfire {
anchor, on_rest, ..
} => Some((anchor, on_rest.as_slice())),
_ => None,
}
}
pub fn bonfire_labels(&self) -> Option<BonfireLabels<'_>> {
match &self.verb {
Verb::Bonfire {
prompt,
rest_label,
save_label,
..
} => Some(BonfireLabels {
prompt: prompt.as_deref(),
rest_label: rest_label.as_deref(),
save_label: save_label.as_deref(),
}),
_ => None,
}
}
pub fn damage_within(&self) -> Option<&StealthZone> {
match &self.verb {
Verb::DamagePlayers { within, .. } => within.as_ref(),
_ => None,
}
}
pub fn begin_stealth(&self) -> Option<(&[StealthZone], &[QuestEffect], u32)> {
match &self.verb {
Verb::BeginStealth {
zones,
on_caught,
grace_ticks,
} => Some((zones.as_slice(), on_caught.as_slice(), *grace_ticks)),
_ => None,
}
}
pub fn set_time(&self) -> Option<WorldTime> {
match &self.verb {
Verb::SetTime { time, .. } => Some(*time),
_ => None,
}
}
pub fn set_weather(&self) -> Option<WorldWeather> {
match &self.verb {
Verb::SetWeather { weather, .. } => Some(*weather),
_ => None,
}
}
pub fn nested_effect_lists(&self) -> Vec<&[QuestEffect]> {
match &self.verb {
Verb::Sequence { steps } => steps.iter().map(|s| s.effects.as_slice()).collect(),
Verb::SetCheckpoint { on_respawn, .. } => vec![on_respawn.as_slice()],
Verb::Bonfire { on_rest, .. } => vec![on_rest.as_slice()],
Verb::BeginStealth { on_caught, .. } => vec![on_caught.as_slice()],
Verb::MoveActor { on_arrive, .. } | Verb::MoveNpc { on_arrive, .. } => {
vec![on_arrive.as_slice()]
}
_ => Vec::new(),
}
}
pub fn visit_deep<'a>(&'a self, f: &mut dyn FnMut(&'a QuestEffect)) {
f(self);
for list in self.nested_effect_lists() {
for e in list {
e.visit_deep(f);
}
}
}
pub fn nested_effect_lists_keyed_mut(&mut self) -> Vec<(String, &mut [QuestEffect])> {
match &mut self.verb {
Verb::Sequence { steps } => steps
.iter_mut()
.enumerate()
.map(|(s, st)| (format!("seq.{s}"), st.effects.as_mut_slice()))
.collect(),
Verb::SetCheckpoint { on_respawn, .. } => {
vec![("respawn".to_string(), on_respawn.as_mut_slice())]
}
Verb::Bonfire { on_rest, .. } => {
vec![("rest".to_string(), on_rest.as_mut_slice())]
}
Verb::BeginStealth { on_caught, .. } => {
vec![("caught".to_string(), on_caught.as_mut_slice())]
}
Verb::MoveActor { on_arrive, .. } | Verb::MoveNpc { on_arrive, .. } => {
vec![("arrive".to_string(), on_arrive.as_mut_slice())]
}
_ => Vec::new(),
}
}
pub fn nested_effect_lists_labeled(&self) -> Vec<(String, String, &[QuestEffect])> {
match &self.verb {
Verb::Sequence { steps } => steps
.iter()
.enumerate()
.map(|(s, st)| {
(
format!("steps/{s}/effects"),
format!("seq.{s}"),
st.effects.as_slice(),
)
})
.collect(),
Verb::SetCheckpoint { on_respawn, .. } => vec![(
"on_respawn".to_string(),
"respawn".to_string(),
on_respawn.as_slice(),
)],
Verb::Bonfire { on_rest, .. } => vec![(
"on_rest".to_string(),
"rest".to_string(),
on_rest.as_slice(),
)],
Verb::BeginStealth { on_caught, .. } => vec![(
"on_caught".to_string(),
"caught".to_string(),
on_caught.as_slice(),
)],
Verb::MoveActor { on_arrive, .. } | Verb::MoveNpc { on_arrive, .. } => {
vec![(
"on_arrive".to_string(),
"arrive".to_string(),
on_arrive.as_slice(),
)]
}
_ => Vec::new(),
}
}
pub fn anchor_refs(&self) -> Vec<(String, &AnchorId, Option<StationKind>)> {
fn shot_refs<'a>(
base: &str,
shot: &'a CameraShot,
) -> Vec<(String, &'a AnchorId, Option<StationKind>)> {
let mut out: Vec<(String, &AnchorId, Option<StationKind>)> = shot
.path
.iter()
.enumerate()
.map(|(j, w)| (format!("{base}path/{j}/anchor"), &w.anchor, None))
.collect();
if let Some(t) = &shot.look_at {
out.push((format!("{base}look_at/anchor"), &t.anchor, None));
}
for (field, subject) in [("subject", &shot.subject), ("subject_b", &shot.subject_b)] {
if let Some(CameraSubject::Anchor(s)) = subject {
out.push((format!("{base}{field}/anchor"), &s.anchor, None));
}
}
out
}
match &self.verb {
Verb::OpenGate { anchor, .. } | Verb::CloseGate { anchor, .. } => {
vec![("anchor".to_string(), anchor, Some(StationKind::Gate))]
}
Verb::SetCheckpoint { anchor, .. } | Verb::Bonfire { anchor, .. } => {
vec![("anchor".to_string(), anchor, Some(StationKind::Point))]
}
Verb::SetBlock { anchor, .. } => {
vec![("anchor".to_string(), anchor, None)]
}
Verb::MoveNpc { to_anchor, .. } | Verb::MoveActor { to_anchor, .. } => {
vec![("to_anchor".to_string(), to_anchor, Some(StationKind::Point))]
}
Verb::DamagePlayers {
within: Some(zone), ..
}
| Verb::GiveEffect {
within: Some(zone), ..
}
| Verb::ClearEffect {
within: Some(zone), ..
} => vec![("in/anchor".to_string(), &zone.anchor, None)],
Verb::Teleport { from, to, .. } => vec![
("from/anchor".to_string(), &from.anchor, None),
("to".to_string(), to, Some(StationKind::Point)),
],
Verb::BeginStealth { zones, .. } => zones
.iter()
.enumerate()
.map(|(i, z)| (format!("zones/{i}/anchor"), &z.anchor, None))
.collect(),
Verb::PlaySound {
at: Some(SoundAt::Anchor { anchor }),
..
} => vec![("at/anchor".to_string(), anchor, None)],
Verb::Volley {
from_anchor,
kill_zone,
..
} => vec![
("from_anchor".to_string(), from_anchor, None),
("kill_zone/anchor".to_string(), &kill_zone.anchor, None),
],
Verb::Collapse { region_anchor, .. } => {
vec![(
"region_anchor/anchor".to_string(),
®ion_anchor.anchor,
None,
)]
}
Verb::FillRegion { region, .. } | Verb::ClearRegion { region, .. } => {
vec![("region/anchor".to_string(), ®ion.anchor, None)]
}
Verb::Cutscene {
shots,
path,
look_at,
..
} => {
let mut out: Vec<(String, &AnchorId, Option<StationKind>)> = shots
.iter()
.enumerate()
.flat_map(|(i, s)| shot_refs(&format!("shots/{i}/"), s))
.collect();
out.extend(
path.iter()
.enumerate()
.map(|(j, w)| (format!("path/{j}/anchor"), &w.anchor, None)),
);
if let Some(t) = look_at {
out.push(("look_at/anchor".to_string(), &t.anchor, None));
}
out
}
_ => Vec::new(),
}
}
pub fn cutscene_look_at(&self) -> Option<&CameraTarget> {
match &self.verb {
Verb::Cutscene { look_at, .. } => look_at.as_ref(),
_ => None,
}
}
pub fn cutscene_multi_shot(&self) -> bool {
matches!(&self.verb, Verb::Cutscene { shots, .. } if !shots.is_empty())
}
pub fn cutscene_shots(&self) -> Option<Vec<CameraShot>> {
match &self.verb {
Verb::Cutscene {
shots,
path,
seconds,
look_at,
..
} => {
if !shots.is_empty() {
return Some(shots.clone());
}
match seconds {
Some(secs) => Some(vec![CameraShot {
path: path.clone(),
seconds: Some(*secs),
look_at: look_at.clone(),
shot_style: None,
subject: None,
subject_b: None,
dist: None,
degrees: None,
bearing: None,
}]),
None => Some(Vec::new()),
}
}
_ => None,
}
}
pub fn narrate_art(&self) -> bool {
matches!(
&self.verb,
Verb::Narrate {
style: Some(NarrateStyle::Art),
..
}
)
}
pub fn narrate_art_text(&self) -> Option<&str> {
match &self.verb {
Verb::Narrate {
text,
style: Some(NarrateStyle::Art),
..
} => Some(text.as_str()),
_ => None,
}
}
pub fn narrate_on_screen(&self) -> Option<(NarrateStyle, &str)> {
match &self.verb {
Verb::Narrate {
text,
style: Some(s),
..
} if matches!(
s,
NarrateStyle::Title | NarrateStyle::Subtitle | NarrateStyle::Art
) =>
{
Some((*s, text.as_str()))
}
_ => None,
}
}
pub fn sound_refs(&self) -> Vec<(&'static str, &str)> {
match &self.verb {
Verb::PlaySound { sound, .. } => vec![("sound", sound.as_str())],
Verb::Narrate { sound: Some(s), .. } => vec![("sound", s.as_str())],
_ => Vec::new(),
}
}
pub fn play_sound_actor(&self) -> Option<&str> {
match &self.verb {
Verb::PlaySound {
at: Some(SoundAt::Actor { actor }),
..
} => Some(actor.as_str()),
_ => None,
}
}
pub fn requires_flags(&self) -> &[FlagId] {
self.when.as_ref().map_or(&[][..], |g| &g.requires_flags)
}
pub fn forbids_flags(&self) -> &[FlagId] {
self.when.as_ref().map_or(&[][..], |g| &g.forbids_flags)
}
pub fn requires_state(&self) -> &[StateCompare] {
self.when.as_ref().map_or(&[][..], |g| &g.requires_state)
}
pub fn writes_state(&self) -> Option<(&StateId, StateWrite)> {
match &self.verb {
Verb::SetState { state, value, .. } => Some((state, StateWrite::Set(*value))),
Verb::AddState { state, amount, .. } => Some((state, StateWrite::Add(*amount))),
Verb::ClearState { state, .. } => Some((state, StateWrite::Clear)),
_ => None,
}
}
pub fn move_npc_on_arrive(&self) -> Option<&[QuestEffect]> {
match &self.verb {
Verb::MoveNpc { on_arrive, .. } if !on_arrive.is_empty() => Some(on_arrive.as_slice()),
_ => None,
}
}
pub fn actor_ref(&self) -> Option<&ActorId> {
match &self.verb {
Verb::SpawnActor { actor, .. }
| Verb::DespawnActor { actor, .. }
| Verb::MoveActor { actor, .. }
| Verb::UnleashActor { actor, .. } => Some(actor),
_ => None,
}
}
pub fn status_effect_refs(&self) -> Vec<(&'static str, &str)> {
match &self.verb {
Verb::GiveEffect { effect, .. } => vec![("effect", effect.as_str())],
Verb::ClearEffect {
effect: Some(e), ..
} => vec![("effect", e.as_str())],
_ => Vec::new(),
}
}
pub fn give_effect(&self) -> Option<(&str, u32, u32, bool, Option<&StealthZone>)> {
match &self.verb {
Verb::GiveEffect {
effect,
seconds,
amplifier,
hide_particles,
within,
..
} => Some((
effect.as_str(),
*seconds,
amplifier.unwrap_or(0),
hide_particles.unwrap_or(false),
within.as_ref(),
)),
_ => None,
}
}
pub fn clear_effect(&self) -> Option<(Option<&str>, Option<&StealthZone>)> {
match &self.verb {
Verb::ClearEffect { effect, within, .. } => Some((effect.as_deref(), within.as_ref())),
_ => None,
}
}
pub fn teleport(&self) -> Option<(&StealthZone, &AnchorId)> {
match &self.verb {
Verb::Teleport { from, to, .. } => Some((from, to)),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorldEditsContent {
pub batches: Vec<EditBatch>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct EditBatch {
pub id: EditBatchId,
pub area: AreaId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
pub edits: Vec<WorldEdit>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "verb", rename_all = "kebab-case", deny_unknown_fields)]
pub enum WorldEdit {
Select {
name: RegionId,
shape: RegionShape,
},
Fill {
region: RegionId,
recipe: PaletteRecipe,
},
Replace {
region: RegionId,
matching: Vec<String>,
recipe: PaletteRecipe,
},
Carve {
region: RegionId,
},
Morph {
region: RegionId,
op: MorphOp,
},
Scatter {
region: RegionId,
items: Vec<PaletteBlock>,
density: f64,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
avoid: Vec<RegionId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
spacing: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
limit: Option<u32>,
},
Plant {
region: RegionId,
tree: TreeKind,
count: u32,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
avoid: Vec<RegionId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
spacing: Option<u32>,
},
Fragment {
prefab: PrefabId,
frame: EditFrame,
at: [i32; 3],
#[serde(default, skip_serializing_if = "Option::is_none")]
rotation: Option<FragmentRotation>,
},
Relight {
region: RegionId,
#[serde(default, skip_serializing_if = "Option::is_none")]
fixture: Option<Fixture>,
#[serde(default, skip_serializing_if = "Option::is_none")]
min_light: Option<u8>,
},
SwapPiece {
piece: u32,
prefab: PrefabId,
with: PrefabId,
},
InsertPiece {
at_piece: u32,
prefab: PrefabId,
socket: u32,
insert: PrefabId,
},
RemovePiece {
piece: u32,
prefab: PrefabId,
},
RewireSocket {
piece: u32,
prefab: PrefabId,
socket: u32,
state: SocketState,
},
ReseedPiece {
piece: u32,
prefab: PrefabId,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum SocketState {
Open,
Sealed,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum TreeKind {
Oak,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum FragmentRotation {
None,
Clockwise90,
Clockwise180,
Counterclockwise90,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
pub enum RegionShape {
Box {
frame: EditFrame,
min: [i32; 3],
max: [i32; 3],
},
SurfaceBand {
over: RegionId,
from: i32,
to: i32,
},
PaletteMatch {
within: RegionId,
blocks: Vec<String>,
},
Union {
of: Vec<RegionId>,
},
Intersect {
of: Vec<RegionId>,
},
Subtract {
base: RegionId,
remove: Vec<RegionId>,
},
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
pub enum EditFrame {
PieceLocal {
piece: u32,
prefab: PrefabId,
},
AnchorRelative {
anchor: AnchorId,
},
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
pub enum MorphOp {
Raise {
by: u32,
recipe: PaletteRecipe,
},
Lower {
by: u32,
},
Smooth {
passes: u32,
recipe: PaletteRecipe,
},
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct PaletteRecipe {
pub blocks: Vec<PaletteBlock>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scale: Option<f64>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct PaletteBlock {
pub block: String,
pub weight: f64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EffectSite {
Objective {
quest: String,
objective: String,
},
QuestComplete {
quest: String,
},
Trigger {
trigger: String,
},
Trap {
trap: String,
},
DialogueRespawn {
npc: String,
node: String,
},
ShortcutUnlock {
shortcut: String,
},
ShopOffer {
shop: String,
offer: usize,
},
OnDeath,
}
impl EffectSite {
pub fn quest(&self) -> Option<&str> {
match self {
EffectSite::Objective { quest, .. } | EffectSite::QuestComplete { quest } => {
Some(quest)
}
EffectSite::Trigger { .. }
| EffectSite::Trap { .. }
| EffectSite::DialogueRespawn { .. }
| EffectSite::ShortcutUnlock { .. }
| EffectSite::ShopOffer { .. }
| EffectSite::OnDeath => None,
}
}
}
pub fn for_each_campaign_effect<'a>(
c: &'a crate::envelope::Campaign,
f: &mut dyn FnMut(&str, &EffectSite, &'a QuestEffect),
) {
crate::effects::for_each_effect_root(c, &mut |root, list| {
let site = match root.owner {
crate::effects::EffectRootOwner::ObjectiveComplete { quest, objective } => {
EffectSite::Objective {
quest: quest.id.as_str().to_string(),
objective: objective.to_string(),
}
}
crate::effects::EffectRootOwner::QuestComplete { quest } => EffectSite::QuestComplete {
quest: quest.id.as_str().to_string(),
},
crate::effects::EffectRootOwner::Trigger(t) => EffectSite::Trigger {
trigger: t.id.as_str().to_string(),
},
crate::effects::EffectRootOwner::TrapPayload(t) => EffectSite::Trap {
trap: t.id.as_str().to_string(),
},
crate::effects::EffectRootOwner::DialogueRespawn => {
let seg = |n: usize| -> String {
root.path.split('/').nth(n).unwrap_or_default().to_string()
};
EffectSite::DialogueRespawn {
npc: seg(3),
node: seg(5),
}
}
crate::effects::EffectRootOwner::ShortcutUnlock(s) => EffectSite::ShortcutUnlock {
shortcut: s.id.as_str().to_string(),
},
crate::effects::EffectRootOwner::OnDeath => EffectSite::OnDeath,
crate::effects::EffectRootOwner::ShopOffer(h) => EffectSite::ShopOffer {
shop: h.id.as_str().to_string(),
offer: root
.path
.split('/')
.nth(4)
.and_then(|n| n.parse().ok())
.unwrap_or(0),
},
};
for (i, eff) in list.iter().enumerate() {
campaign_effect_deep(eff, &format!("{}/{i}", root.path), &site, f);
}
});
}
fn campaign_effect_deep<'a>(
eff: &'a QuestEffect,
path: &str,
site: &EffectSite,
f: &mut dyn FnMut(&str, &EffectSite, &'a QuestEffect),
) {
f(path, site, eff);
for (pseg, _kseg, list) in eff.nested_effect_lists_labeled() {
for (j, inner) in list.iter().enumerate() {
campaign_effect_deep(inner, &format!("{path}/{pseg}/{j}"), site, f);
}
}
}
#[cfg(test)]
mod spine_tests {
use super::QuestPlanContent;
fn plan(finale: &str, quests: &[(&str, &[&str])]) -> QuestPlanContent {
let quests: Vec<serde_json::Value> = quests
.iter()
.map(|(id, deps)| {
serde_json::json!({
"id": id,
"goal": "g",
"area": "area/keep",
"npcs": [],
"depends_on": deps,
"mandatory": true,
"act": 1,
})
})
.collect();
serde_json::from_value(serde_json::json!({
"finale": finale,
"quests": quests,
}))
.expect("plan fixture parses")
}
fn sorted(p: &QuestPlanContent) -> Vec<String> {
p.spine().into_iter().map(str::to_owned).collect()
}
#[test]
fn a_chain_is_wholly_spine() {
let p = plan(
"quest/c",
&[
("quest/a", &[]),
("quest/b", &["quest/a"]),
("quest/c", &["quest/b"]),
],
);
assert_eq!(sorted(&p), ["quest/a", "quest/b", "quest/c"]);
}
#[test]
fn a_quest_the_finale_does_not_depend_on_is_off_the_spine() {
let p = plan("quest/end", &[("quest/end", &[]), ("quest/side-trip", &[])]);
assert_eq!(sorted(&p), ["quest/end"]);
}
#[test]
fn a_diamond_counts_the_join_once() {
let p = plan(
"quest/d",
&[
("quest/a", &[]),
("quest/b", &["quest/a"]),
("quest/c", &["quest/a"]),
("quest/d", &["quest/b", "quest/c"]),
],
);
assert_eq!(sorted(&p), ["quest/a", "quest/b", "quest/c", "quest/d"]);
}
#[test]
fn a_cycle_terminates_and_yields_a_set() {
let p = plan(
"quest/b",
&[("quest/a", &["quest/b"]), ("quest/b", &["quest/a"])],
);
assert_eq!(sorted(&p), ["quest/a", "quest/b"]);
}
#[test]
fn a_dangling_dependency_is_reported_and_expands_no_further() {
let p = plan("quest/end", &[("quest/end", &["quest/ghost"])]);
assert_eq!(sorted(&p), ["quest/end", "quest/ghost"]);
}
#[test]
fn an_undeclared_finale_is_the_whole_spine() {
let p = plan(
"quest/ghost",
&[("quest/a", &[]), ("quest/b", &["quest/a"])],
);
assert_eq!(sorted(&p), ["quest/ghost"]);
}
}