use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{Actor, AreaId, Mark, Npc, NpcSkin};
#[cfg(doc)]
use crate::WaveMob;
#[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 worn_entity(self) -> &'a str {
match self.skin() {
Some(_) => "minecraft:mannequin",
None => self.declared_entity(),
}
}
pub fn mark(self) -> Mark {
let (anchor, offset) = match self {
BodyRef::Npc(n) => (&n.anchor, n.offset),
BodyRef::Actor(a) => (&a.anchor, a.offset),
};
Mark {
anchor: anchor.clone(),
offset,
}
}
pub fn area(self) -> Option<&'a AreaId> {
match self {
BodyRef::Npc(n) => Some(&n.area),
BodyRef::Actor(_) => None,
}
}
pub fn at_world_init(self) -> bool {
match self {
BodyRef::Npc(n) => !n.deferred,
BodyRef::Actor(_) => false,
}
}
pub fn killable_by_players(self) -> bool {
match self {
BodyRef::Npc(_) => false,
BodyRef::Actor(a) => a.vulnerable,
}
}
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()
}
pub fn body_skins_mut(c: &mut crate::envelope::Campaign) -> Vec<&mut NpcSkin> {
let mut out: Vec<&mut NpcSkin> = Vec::new();
out.extend(
c.npcs
.content
.npcs
.iter_mut()
.filter_map(|n| n.skin.as_mut()),
);
out.extend(
c.quests
.content
.actors
.iter_mut()
.filter_map(|a| a.skin.as_mut()),
);
out
}
use crate::diagnostic::{Diagnostic, DwCode, ExitTier};
use crate::envelope::Campaign;
crate::dw_code! {
pub const TRAVERSAL_UNPROVABLE: DwCode = DwCode::new("DW0455", ExitTier::Build);
}
pub(crate) fn body_traversal_checks(c: &Campaign, d: &mut Vec<Diagnostic>) {
for site in body_traversal_sites(c) {
if site.traversal.locomotion != Locomotion::Aquatic {
continue;
}
let (stage, path, id) = (site.body.stage(), &site.path, site.body.id());
d.push(Diagnostic::error(
TRAVERSAL_UNPROVABLE,
stage,
format!("{path}/locomotion"),
format!(
"`{id}` declares `locomotion: aquatic`, which the compiler cannot hold it to. \
`aquatic` is the one class that carries no exemption and governs no rule — it is \
a ledger label derived from vanilla's own `#minecraft:aquatic` tag — so the \
declaration could never change a verdict and would be reported inert (`DW0454`). \
The gap, stated rather than left to folklore: routing has ONE reachability model, \
standable ground, and water-flooded cells are impassable and never floor for \
EVERY body, so there is nothing for an aquatic claim to feed. Prescription: \
remove the declaration — a route that crosses water is already governed by the \
flooded-cell rules, and a body vanilla itself calls aquatic still reaches the \
traversal proof's binding ledger under its derived class."
),
));
}
}