mod common;
use delvewright_dsl::{RawCampaign, check_campaign};
use std::sync::LazyLock;
static NPCS: LazyLock<String> = LazyLock::new(|| {
common::at_dsl_version(
r#"{
"dsl_version": "%dsl_version%",
"campaign_id": "hello-world",
"stage": "npcs",
"content": {
"npcs": [
{
"id": "npc/keeper",
"name": "The Keeper",
"role": "quest-giver",
"area": "area/keep",
"anchor": "anchor/keeper-stand",
"base_entity": "minecraft:villager",
"persona": {
"archetype": "stoic gatekeeper",
"speech_style": "Terse and formal.",
"demeanor": "Wary and unmoving.",
"motivation": "Keep the gate shut.",
"secret": "He blames himself for the drowned road.",
"backstory": "A road-warden who held his post alone."
}
},
{
"id": "npc/latecomer",
"name": "The Latecomer",
"role": "flavor",
"area": "area/keep",
"anchor": "anchor/exit",
"base_entity": "minecraft:villager",
"deferred": true,
"persona": {
"archetype": "hooded stranger",
"speech_style": "Low and hurried.",
"demeanor": "Appears only once the door is open.",
"motivation": "Slip through the gate behind the party.",
"secret": "She has been waiting outside for years.",
"backstory": "Turned away from the keep on the night the road drowned."
}
}
]
}
}"#,
)
});
static QUEST_PLAN: LazyLock<String> = LazyLock::new(|| {
common::at_dsl_version(
r#"{
"dsl_version": "%dsl_version%",
"campaign_id": "hello-world",
"stage": "quest-plan",
"content": {
"quests": [
{
"id": "quest/open-the-door",
"goal": "Get the Keeper to open the door.",
"area": "area/keep",
"npcs": ["npc/keeper"],
"depends_on": [],
"mandatory": true,
"act": 1
},
{
"id": "quest/second",
"goal": "Meet whoever was waiting outside.",
"area": "area/keep",
"npcs": ["npc/latecomer"],
"depends_on": ["quest/open-the-door"],
"mandatory": true,
"act": 1
}
],
"finale": "quest/second"
}
}"#,
)
});
static DIALOGUE: LazyLock<String> = LazyLock::new(|| {
common::at_dsl_version(
r#"{
"dsl_version": "%dsl_version%",
"campaign_id": "hello-world",
"stage": "dialogue",
"content": {
"dialogues": [
{
"npc": "npc/keeper",
"root": "dlg/greeting",
"nodes": [
{
"id": "dlg/greeting",
"text": "Halt, traveler. The door stays shut.",
"options": [
{ "label": "Open the door, please.", "effects": [ { "type": "complete-objective", "objective": "obj/talk" } ] }
]
}
]
},
{
"npc": "npc/latecomer",
"root": "dlg/hello",
"nodes": [
{
"id": "dlg/hello",
"text": "You opened it. I have waited a long time for that.",
"options": [
{ "label": "Who are you?", "effects": [ { "type": "complete-objective", "objective": "obj/meet" } ] }
]
}
]
}
]
}
}"#,
)
});
static QUESTS: LazyLock<String> = LazyLock::new(|| {
common::at_dsl_version(
r#"{
"dsl_version": "%dsl_version%",
"campaign_id": "hello-world",
"stage": "quests",
"content": {
"quests": [
{
"id": "quest/open-the-door",
"trigger": { "type": "campaign-start" },
"objectives": [
{ "type": "talk-to", "id": "obj/talk", "npc": "npc/keeper" }
],
"on_objective_complete": {
"obj/talk": [
{ "type": "open-gate", "anchor": "anchor/door" },
{ "type": "spawn-npc", "npc": "npc/latecomer" }
]
},
"on_complete": []
},
{
"id": "quest/second",
"trigger": { "type": "quest-complete", "quest": "quest/open-the-door" },
"objectives": [
{ "type": "talk-to", "id": "obj/meet", "npc": "npc/latecomer" }
],
"on_complete": [ { "type": "campaign-complete" } ]
}
]
}
}"#,
)
});
fn campaign(npcs: &str, quests: &str, dialogue: &str) -> RawCampaign {
RawCampaign {
world: common::read_valid("world.json"),
npcs: npcs.to_string(),
classes: common::read_valid("classes.json"),
quest_plan: QUEST_PLAN.to_string(),
quests: quests.to_string(),
dialogue: dialogue.to_string(),
world_edits: None,
geometry_brief: None,
layout_graph: None,
site_plan: None,
detail_plan: None,
design: None,
}
}
fn base() -> RawCampaign {
campaign(NPCS.as_str(), QUESTS.as_str(), DIALOGUE.as_str())
}
#[test]
fn deferred_npc_with_spawn_validates_clean() {
let diags = check_campaign(&base());
assert!(
diags.is_empty(),
"expected zero diagnostics for a v0.6 deferred npc + spawn-npc, got: {diags:#?}"
);
}
#[test]
fn spawn_npc_unknown_npc_is_dw0112() {
let bad = QUESTS.replace(
r#"{ "type": "spawn-npc", "npc": "npc/latecomer" }"#,
r#"{ "type": "spawn-npc", "npc": "npc/nobody" }"#,
);
let diags = check_campaign(&campaign(NPCS.as_str(), &bad, DIALOGUE.as_str()));
assert!(
diags.iter().any(|d| d.code == "DW0112"),
"an unknown spawn-npc target must be DW0112: {diags:#?}"
);
}
#[test]
fn dialogue_spawn_npc_unknown_npc_is_dw0112() {
let bad_dlg = DIALOGUE.replace(
r#"{ "label": "Who are you?", "effects": [ { "type": "complete-objective", "objective": "obj/meet" } ] }"#,
r#"{ "label": "Who are you?", "effects": [ { "type": "complete-objective", "objective": "obj/meet" }, { "type": "spawn-npc", "npc": "npc/nobody" } ] }"#,
);
let diags = check_campaign(&campaign(NPCS.as_str(), QUESTS.as_str(), &bad_dlg));
assert!(
diags.iter().any(|d| d.code == "DW0112"),
"an unknown dialogue spawn-npc target must be DW0112: {diags:#?}"
);
}
#[test]
fn deferred_npc_never_spawned_is_dw0197() {
let no_spawn = QUESTS.replace(
r#",
{ "type": "spawn-npc", "npc": "npc/latecomer" }"#,
"",
);
assert!(
!no_spawn.contains("spawn-npc"),
"fixture edit must remove the spawn"
);
let diags = check_campaign(&campaign(NPCS.as_str(), &no_spawn, DIALOGUE.as_str()));
assert!(
diags.iter().any(|d| d.code == "DW0197"),
"a deferred npc nobody spawns must be DW0197: {diags:#?}"
);
}
#[test]
fn talk_to_before_spawn_in_dag_is_dw0198() {
let swapped = common::at_dsl_version(
r#"{
"dsl_version": "%dsl_version%",
"campaign_id": "hello-world",
"stage": "quests",
"content": {
"quests": [
{
"id": "quest/open-the-door",
"trigger": { "type": "campaign-start" },
"objectives": [
{ "type": "talk-to", "id": "obj/talk", "npc": "npc/keeper" },
{ "type": "talk-to", "id": "obj/meet", "npc": "npc/latecomer", "after": ["obj/talk"] }
],
"on_objective_complete": {
"obj/talk": [ { "type": "open-gate", "anchor": "anchor/door" } ]
},
"on_complete": []
},
{
"id": "quest/second",
"trigger": { "type": "quest-complete", "quest": "quest/open-the-door" },
"objectives": [
{ "type": "reach-anchor", "id": "obj/exit", "anchor": "anchor/exit", "radius": 2 }
],
"on_complete": [
{ "type": "spawn-npc", "npc": "npc/latecomer" },
{ "type": "campaign-complete" }
]
}
]
}
}"#,
);
let diags = check_campaign(&campaign(NPCS.as_str(), &swapped, DIALOGUE.as_str()));
assert!(
diags.iter().any(|d| d.code == "DW0198"),
"a talk-to that provably precedes every spawn-npc must be DW0198: {diags:#?}"
);
}