mod common;
use delvewright_dsl::{RawCampaign, check_campaign};
use std::sync::LazyLock;
fn campaign_with(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: common::read_valid("quest-plan.json"),
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 valid_npcs_v04() -> String {
common::read_valid("npcs.json").replacen("\"0.2.0\"", "\"0.4.0\"", 1)
}
fn valid_dialogue_v04() -> String {
common::read_valid("dialogue.json").replacen("\"0.2.0\"", "\"0.4.0\"", 1)
}
static QUESTS_BASE: 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" },
{ "type": "reach-anchor", "id": "obj/exit", "anchor": "anchor/exit", "radius": 2, "after": ["obj/talk"] }
],
"on_objective_complete": {
"obj/talk": [ { "type": "open-gate", "anchor": "anchor/door" } ]
},
"on_complete": [ { "type": "campaign-complete" } ]
}
]
}
}"#,
)
});
#[test]
fn v04_base_campaign_validates_clean() {
let diags = check_campaign(&campaign_with(
&valid_npcs_v04(),
QUESTS_BASE.as_str(),
&valid_dialogue_v04(),
));
assert!(
diags.is_empty(),
"expected zero diagnostics for the v0.4 base campaign, got: {diags:#?}"
);
}
#[test]
fn malformed_skin_texture_id_is_dw0190() {
let npcs = valid_npcs_v04().replacen(
"\"base_entity\": \"minecraft:villager\",",
"\"base_entity\": \"minecraft:villager\", \"skin\": { \"texture_id\": \"Not Kebab!\", \
\"model\": \"wide\" },",
1,
);
let diags = check_campaign(&campaign_with(
&npcs,
QUESTS_BASE.as_str(),
&valid_dialogue_v04(),
));
assert!(
diags.iter().any(|d| d.code == "DW0190"),
"malformed texture_id must be DW0190: {diags:#?}"
);
}
#[test]
fn two_mannequins_may_wear_one_texture() {
let npcs = common::patch_doc(&valid_npcs_v04(), |d| {
let npcs = d["content"]["npcs"].as_array_mut().unwrap();
npcs[0]["skin"] = serde_json::json!({ "texture_id": "keeper", "model": "wide" });
let mut twin = npcs[0].clone();
twin["id"] = serde_json::json!("npc/keeper-twin");
twin["skin"] = serde_json::json!({
"texture_id": "keeper", "model": "wide", "hidden_layers": ["hat"]
});
npcs.push(twin);
});
let diags = check_campaign(&campaign_with(
&npcs,
QUESTS_BASE.as_str(),
&valid_dialogue_v04(),
));
assert!(
!diags.iter().any(|d| d.code == "DW0190"),
"one file, two bodies is not a malformed id: {diags:#?}"
);
}
fn npcs_hiding(layers: &str) -> String {
valid_npcs_v04().replacen(
"\"base_entity\": \"minecraft:villager\",",
&format!(
"\"base_entity\": \"minecraft:villager\", \"skin\": {{ \"texture_id\": \"keeper\", \
\"model\": \"wide\", \"hidden_layers\": {layers} }},"
),
1,
)
}
#[test]
fn a_layer_hidden_twice_is_dw0980_and_each_layer_once_is_clean() {
let twice = check_campaign(&campaign_with(
&npcs_hiding(r#"["hat", "jacket", "hat"]"#),
QUESTS_BASE.as_str(),
&valid_dialogue_v04(),
));
let hit: Vec<_> = twice.iter().filter(|d| d.code == "DW0980").collect();
assert_eq!(hit.len(), 1, "one repeat, one refusal: {twice:#?}");
assert!(
hit[0].path.ends_with("/skin/hidden_layers/2"),
"{:?}",
hit[0].path
);
let once = check_campaign(&campaign_with(
&npcs_hiding(r#"["hat", "jacket", "cape"]"#),
QUESTS_BASE.as_str(),
&valid_dialogue_v04(),
));
assert!(
!once.iter().any(|d| d.code == "DW0980"),
"distinct layers are clean: {once:#?}"
);
let unknown = check_campaign(&campaign_with(
&npcs_hiding(r#"["helmet"]"#),
QUESTS_BASE.as_str(),
&valid_dialogue_v04(),
));
assert!(
unknown.iter().any(|d| d.code == "DW0100"),
"a layer the client does not name is a schema error: {unknown:#?}"
);
}
#[test]
fn all_completing_options_flag_gated_is_dw0191() {
let dialogue = common::patch_doc(&valid_dialogue_v04(), |d| {
let opts = d["content"]["dialogues"][0]["nodes"][0]["options"]
.as_array_mut()
.expect("the greeting node has options");
let completing = opts
.iter_mut()
.find(|o| o["effects"][0]["type"] == "complete-objective")
.expect("the greeting node still holds the only completing option");
completing["requires_flags"] = serde_json::json!(["flag/never-set"]);
});
let diags = check_campaign(&campaign_with(
&valid_npcs_v04(),
QUESTS_BASE.as_str(),
&dialogue,
));
assert!(
diags.iter().any(|d| d.code == "DW0191"),
"an all-flag-gated talk-to must be DW0191: {diags:#?}"
);
}
static QUESTS_BAD_WAVE_EFFECT: 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" },
{ "type": "reach-anchor", "id": "obj/exit", "anchor": "anchor/exit", "radius": 2, "after": ["obj/talk"] }
],
"on_objective_complete": {
"obj/talk": [ { "type": "open-gate", "anchor": "anchor/door" } ]
},
"on_complete": [ { "type": "campaign-complete" } ]
}
],
"waves": [
{
"id": "wave/ambush",
"anchor": "anchor/keeper-stand",
"mobs": [
{ "entity": "minecraft:zombie", "count": 1, "effects": [ { "effect": "minecraft:not_a_real_effect", "amplifier": 0 } ] }
]
}
]
}
}"#,
)
});
#[test]
fn unknown_wave_mob_effect_is_dw0192() {
let diags = check_campaign(&campaign_with(
&valid_npcs_v04(),
QUESTS_BAD_WAVE_EFFECT.as_str(),
&valid_dialogue_v04(),
));
assert!(
diags.iter().any(|d| d.code == "DW0192"),
"unknown wave-mob effect id must be DW0192: {diags:#?}"
);
}
#[test]
fn unknown_set_block_id_is_dw0193() {
let quests = QUESTS_BASE.replacen(
"{ \"type\": \"open-gate\", \"anchor\": \"anchor/door\" }",
"{ \"type\": \"open-gate\", \"anchor\": \"anchor/door\" }, \
{ \"type\": \"set-block\", \"anchor\": \"anchor/exit\", \"block\": \"minecraft:not_a_real_block\" }",
1,
);
let diags = check_campaign(&campaign_with(
&valid_npcs_v04(),
&quests,
&valid_dialogue_v04(),
));
assert!(
diags.iter().any(|d| d.code == "DW0193"),
"unknown set-block block id must be DW0193: {diags:#?}"
);
}
static QUESTS_BAD_TRIGGER: 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" },
{ "type": "reach-anchor", "id": "obj/exit", "anchor": "anchor/exit", "radius": 2, "after": ["obj/talk"] }
],
"on_objective_complete": {
"obj/talk": [ { "type": "open-gate", "anchor": "anchor/door" } ]
},
"on_complete": [ { "type": "campaign-complete" } ]
}
],
"triggers": [
{
"id": "not-a-valid-trigger-id",
"at": "anchor/exit",
"on": { "on": "strike" },
"effects": []
}
]
}
}"#,
)
});
#[test]
fn malformed_trigger_id_is_dw0194() {
let diags = check_campaign(&campaign_with(
&valid_npcs_v04(),
QUESTS_BAD_TRIGGER.as_str(),
&valid_dialogue_v04(),
));
assert!(
diags.iter().any(|d| d.code == "DW0194"),
"malformed trigger id must be DW0194: {diags:#?}"
);
}
fn quests_trigger_on_keeper_stand(on: &str) -> String {
QUESTS_BAD_TRIGGER.replace(
r#""id": "not-a-valid-trigger-id",
"at": "anchor/exit",
"on": { "on": "strike" },"#,
&format!(
r#""id": "trigger/nudge",
"at": "anchor/keeper-stand",
"on": {{ "on": "{on}" }},"#
),
)
}
#[test]
fn use_trigger_on_an_npc_anchor_is_dw0350() {
let quests = quests_trigger_on_keeper_stand("use");
assert!(
quests.contains("anchor/keeper-stand"),
"fixture patch applied"
);
let diags = check_campaign(&campaign_with(
&valid_npcs_v04(),
&quests,
&valid_dialogue_v04(),
));
assert!(
diags.iter().any(|d| d.code == "DW0350"),
"a `use` trigger on an NPC's anchor must be DW0350: {diags:#?}"
);
}
#[test]
fn strike_trigger_on_an_npc_anchor_is_not_dw0350() {
let quests = quests_trigger_on_keeper_stand("strike");
let diags = check_campaign(&campaign_with(
&valid_npcs_v04(),
&quests,
&valid_dialogue_v04(),
));
assert!(
!diags.iter().any(|d| d.code == "DW0350"),
"`strike` on an NPC's anchor is the sanctioned shared-hitbox form: {diags:#?}"
);
}
static QUEST_PLAN_TWO_QUESTS: 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 and leave the keep.",
"area": "area/keep",
"npcs": ["npc/keeper"],
"depends_on": [],
"mandatory": true,
"act": 1
},
{
"id": "quest/second",
"goal": "Speak to the Keeper once more.",
"area": "area/keep",
"npcs": ["npc/keeper"],
"depends_on": ["quest/open-the-door"],
"mandatory": true,
"act": 1
}
],
"finale": "quest/second"
}
}"#,
)
});
static QUESTS_DESPAWNED_REF: 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": "despawn-npc", "npc": "npc/keeper" }
]
},
"on_complete": []
},
{
"id": "quest/second",
"trigger": { "type": "quest-complete", "quest": "quest/open-the-door" },
"objectives": [
{ "type": "talk-to", "id": "obj/talk2", "npc": "npc/keeper" }
],
"on_objective_complete": {},
"on_complete": [ { "type": "campaign-complete" } ]
}
]
}
}"#,
)
});
static DIALOGUE_TWO_OBJECTIVES: 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. This keep is mine to guard, and the door stays shut.",
"options": [
{ "label": "Who are you?", "next": "dlg/lore" },
{ "label": "Open the door, please.", "effects": [ { "type": "complete-objective", "objective": "obj/talk" } ] },
{ "label": "Anything else?", "effects": [ { "type": "complete-objective", "objective": "obj/talk2" } ] }
]
},
{
"id": "dlg/lore",
"text": "I am the Keeper. I have watched this gate since the moor swallowed the old road.",
"options": [ { "label": "Back.", "next": "dlg/greeting" } ]
}
]
}
]
}
}"#,
)
});
#[test]
fn talk_to_targets_despawned_npc_is_dw0195() {
let raw = RawCampaign {
world: common::read_valid("world.json"),
npcs: valid_npcs_v04(),
classes: common::read_valid("classes.json"),
quest_plan: QUEST_PLAN_TWO_QUESTS.to_string(),
quests: QUESTS_DESPAWNED_REF.to_string(),
dialogue: DIALOGUE_TWO_OBJECTIVES.to_string(),
world_edits: None,
geometry_brief: None,
layout_graph: None,
site_plan: None,
detail_plan: None,
design: None,
};
let diags = check_campaign(&raw);
assert!(
diags.iter().any(|d| d.code == "DW0195"),
"a talk-to on an npc despawned by a prerequisite quest must be DW0195: {diags:#?}"
);
}