use serde_json::Value;
use super::buffs::{
Buff, BuffContribution, BuffSource, EngineContext, RerollSubset, RollKind, WeaponKeywordRef,
};
const ENGINE_DISPATCH_KEYWORDS: &[&str] = &[
"lethal-hits",
"sustained-hits",
"devastating-wounds",
"anti",
"melta",
"rapid-fire",
"torrent",
"ignores-cover",
];
pub fn buffs_from_keyword(
keyword_id: &str,
weapon_id: &str,
effect: Option<&Value>,
parameters: Option<&Value>,
context: &EngineContext,
) -> Vec<Buff> {
let source = BuffSource::WeaponKeyword {
weapon_id: weapon_id.to_string(),
keyword_id: keyword_id.to_string(),
};
if ENGINE_DISPATCH_KEYWORDS.contains(&keyword_id) {
let mut keyword_ref = WeaponKeywordRef {
keyword_id: keyword_id.to_string(),
parameters: None,
};
if let Some(p) = parameters {
keyword_ref.parameters = Some(p.clone());
}
return vec![Buff {
source,
applicable_when: None,
contribution: BuffContribution::ExtraKeyword { keyword_ref },
}];
}
let Some(effect) = effect else {
return Vec::new();
};
walk(effect, &source, context)
}
fn walk(node: &Value, source: &BuffSource, ctx: &EngineContext) -> Vec<Buff> {
let Some(obj) = node.as_object() else {
return Vec::new();
};
let Some(node_type) = obj.get("type").and_then(Value::as_str) else {
return Vec::new();
};
match node_type {
"re-roll" => reroll_buffs(obj, source),
"roll-modifier" => roll_modifier_buffs(obj, source),
"feel-no-pain" => feel_no_pain_buffs(obj, source),
"keyword-grant" => keyword_grant_buffs(obj, source),
"conditional" => conditional_buffs(obj, source, ctx),
"sequence" => walk_children(obj.get("steps"), source, ctx),
_ => Vec::new(),
}
}
fn walk_children(steps: Option<&Value>, source: &BuffSource, ctx: &EngineContext) -> Vec<Buff> {
let Some(arr) = steps.and_then(Value::as_array) else {
return Vec::new();
};
arr.iter().flat_map(|c| walk(c, source, ctx)).collect()
}
fn reroll_buffs(obj: &serde_json::Map<String, Value>, source: &BuffSource) -> Vec<Buff> {
let Some(modifier) = obj.get("modifier").and_then(Value::as_object) else {
return Vec::new();
};
let roll = match modifier.get("roll").and_then(Value::as_str) {
Some("hit") => RollKind::Hit,
Some("wound") => RollKind::Wound,
Some("save") => RollKind::Save,
Some("damage") => RollKind::Damage,
_ => return Vec::new(),
};
let subset = match modifier.get("subset").and_then(Value::as_str) {
Some("ones") => RerollSubset::Ones,
Some("all-failures") => RerollSubset::AllFailures,
_ => return Vec::new(),
};
vec![Buff {
source: source.clone(),
applicable_when: None,
contribution: BuffContribution::Reroll { roll, subset },
}]
}
fn roll_modifier_buffs(obj: &serde_json::Map<String, Value>, source: &BuffSource) -> Vec<Buff> {
let Some(modifier) = obj.get("modifier").and_then(Value::as_object) else {
return Vec::new();
};
if modifier.get("operation").and_then(Value::as_str) != Some("add") {
return Vec::new();
}
let Some(value) = modifier.get("value").and_then(Value::as_f64) else {
return Vec::new();
};
let contribution = match modifier.get("roll").and_then(Value::as_str) {
Some("hit") => BuffContribution::HitMod { value },
Some("wound") => BuffContribution::WoundMod { value },
Some("save") => BuffContribution::SaveMod { value },
Some("damage") => BuffContribution::DamageMod { value },
_ => return Vec::new(),
};
vec![Buff {
source: source.clone(),
applicable_when: None,
contribution,
}]
}
fn feel_no_pain_buffs(obj: &serde_json::Map<String, Value>, source: &BuffSource) -> Vec<Buff> {
let Some(modifier) = obj.get("modifier").and_then(Value::as_object) else {
return Vec::new();
};
let Some(threshold) = modifier.get("threshold").and_then(Value::as_f64) else {
return Vec::new();
};
vec![Buff {
source: source.clone(),
applicable_when: None,
contribution: BuffContribution::FeelNoPain {
threshold,
scope: Default::default(),
},
}]
}
fn keyword_grant_buffs(obj: &serde_json::Map<String, Value>, source: &BuffSource) -> Vec<Buff> {
let Some(modifier) = obj.get("modifier").and_then(Value::as_object) else {
return Vec::new();
};
let id = modifier
.get("keyword_id")
.or_else(|| modifier.get("id"))
.and_then(Value::as_str);
let Some(id) = id.filter(|s| !s.is_empty()) else {
return Vec::new();
};
let parameters = modifier
.get("parameters")
.filter(|v| v.is_object())
.cloned();
let keyword_ref = WeaponKeywordRef {
keyword_id: id.to_string(),
parameters,
};
vec![Buff {
source: source.clone(),
applicable_when: None,
contribution: BuffContribution::ExtraKeyword { keyword_ref },
}]
}
fn conditional_buffs(
obj: &serde_json::Map<String, Value>,
source: &BuffSource,
ctx: &EngineContext,
) -> Vec<Buff> {
let Some(condition) = obj.get("condition").and_then(Value::as_object) else {
return Vec::new();
};
let negated = condition.get("negated").and_then(Value::as_bool) == Some(true);
let verdict = evaluate_condition(condition, ctx);
let active = match verdict {
Verdict::Unknown => return Vec::new(),
Verdict::True => !negated,
Verdict::False => negated,
};
if !active {
return Vec::new();
}
obj.get("effect")
.map(|e| walk(e, source, ctx))
.unwrap_or_default()
}
enum Verdict {
True,
False,
Unknown,
}
fn evaluate_condition(condition: &serde_json::Map<String, Value>, ctx: &EngineContext) -> Verdict {
match condition.get("type").and_then(Value::as_str) {
Some("remained-stationary") => {
if ctx.attacker_stationary == Some(true) {
Verdict::True
} else {
Verdict::False
}
}
Some("target-has-keyword") => {
let parameters = condition.get("parameters").and_then(Value::as_object);
let Some(kw) = parameters
.and_then(|p| p.get("keyword"))
.and_then(Value::as_str)
else {
return Verdict::Unknown;
};
let kw_lower = kw.to_lowercase();
let hit = ctx
.target_keywords
.as_ref()
.is_some_and(|kws| kws.iter().any(|k| k == &kw_lower));
if hit {
Verdict::True
} else {
Verdict::False
}
}
_ => Verdict::Unknown,
}
}