mod common;
use delvewright_dsl::{DSL_VERSION, RawCampaign, Severity, check_campaign};
fn raw_with(world: Option<&str>, quests: Option<&str>) -> RawCampaign {
RawCampaign {
world: world
.map(str::to_string)
.unwrap_or_else(|| common::read_valid("world.json")),
npcs: common::read_valid("npcs.json"),
classes: common::read_valid("classes.json"),
quest_plan: common::read_valid("quest-plan.json"),
quests: quests
.map(str::to_string)
.unwrap_or_else(|| common::read_valid("quests.json")),
dialogue: common::read_valid("dialogue.json"),
world_edits: None,
geometry_brief: None,
layout_graph: None,
site_plan: None,
detail_plan: None,
design: None,
}
}
fn world_with_difficulty(value: &str) -> String {
common::read_valid("world.json")
.replacen("\"0.2.0\"", &format!("\"{DSL_VERSION}\""), 1)
.replacen(
"\"target_minutes\": 5,",
&format!("\"target_minutes\": 5,\n \"difficulty\": \"{value}\","),
1,
)
}
fn quests_with_actor(attrs: &str) -> String {
format!(
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" }},
{{ "type": "spawn-actor", "actor": "actor/giant" }},
{{ "type": "unleash-actor", "actor": "actor/giant" }}
]
}},
"on_complete": [ {{ "type": "campaign-complete" }} ]
}}
],
"actors": [
{{ "id": "actor/giant", "entity": "minecraft:zombie", "name": "The Sleeper",
"anchor": "anchor/keeper-stand"{attrs} }}
]
}}
}}"#
)
}
fn codes(raw: &RawCampaign) -> Vec<String> {
check_campaign(raw).iter().map(|d| d.code.clone()).collect()
}
#[test]
fn easy_normal_hard_all_validate() {
for value in ["easy", "normal", "hard"] {
let raw = raw_with(Some(&world_with_difficulty(value)), None);
let d = check_campaign(&raw);
assert!(d.is_empty(), "`{value}` must validate clean: {d:#?}");
}
}
#[test]
fn peaceful_is_rejected_with_its_rationale() {
let raw = raw_with(Some(&world_with_difficulty("peaceful")), None);
let d = check_campaign(&raw);
let hit = d
.iter()
.find(|x| x.code == "DW0468")
.expect("peaceful raises DW0468");
assert_eq!(hit.severity, Severity::Error);
assert_eq!(hit.stage, "world");
assert_eq!(hit.path, "/content/difficulty");
assert!(
hit.message.contains("discards every")
&& hit.message.contains("hostile")
&& hit.message.contains("ambush"),
"the diagnostic must explain that every wave/actor/ambush would vanish: {}",
hit.message
);
assert!(
hit.message.contains("easy") && hit.message.contains("hard"),
"the diagnostic must prescribe the legal keywords: {}",
hit.message
);
}
#[test]
fn absent_difficulty_is_clean() {
assert!(check_campaign(&raw_with(None, None)).is_empty());
}
#[test]
fn actors_without_waves_or_declared_difficulty_warn() {
let raw = raw_with(None, Some(&quests_with_actor("")));
let d = check_campaign(&raw);
let hit = d
.iter()
.find(|x| x.code == "DW0469")
.unwrap_or_else(|| panic!("actors + derived peaceful must warn: {d:#?}"));
assert_eq!(hit.severity, Severity::Warning);
assert_eq!(hit.path, "/content/difficulty");
assert!(
hit.message.contains("peaceful"),
"the warning must name the setting it is about: {}",
hit.message
);
assert!(
d.iter().all(|x| x.severity == Severity::Warning),
"DW0469 must not fail the run: {d:#?}"
);
}
#[test]
fn declared_difficulty_silences_the_actor_warning() {
let raw = raw_with(
Some(&world_with_difficulty("normal")),
Some(&quests_with_actor("")),
);
assert!(
!codes(&raw).contains(&"DW0469".to_string()),
"a declared difficulty answers the question DW0469 asks"
);
}
#[test]
fn no_actors_means_no_warning() {
assert!(!codes(&raw_with(None, None)).contains(&"DW0469".to_string()));
}
#[test]
fn actor_attributes_validate_under_v06() {
let attrs =
r#", "attributes": { "max_health": 200.0, "attack_damage": 12.0, "follow_range": 24.0 }"#;
let raw = raw_with(
Some(&world_with_difficulty("hard")),
Some(&quests_with_actor(attrs)),
);
let d = check_campaign(&raw);
assert!(d.is_empty(), "actor attributes must validate clean: {d:#?}");
}
#[test]
fn unknown_actor_attribute_is_a_schema_error() {
let attrs = r#", "attributes": { "armor_toughness": 8.0 }"#;
let raw = raw_with(
Some(&world_with_difficulty("hard")),
Some(&quests_with_actor(attrs)),
);
assert!(
codes(&raw).contains(&"DW0100".to_string()),
"an attribute the DSL does not expose must be rejected, not dropped"
);
}