use serde::Serialize;
use crate::import::{BattleSize, Roster, RosterUnit, RosterWargear};
use super::helpers::{pretty_json, title_case_id, total_army_points};
use super::{ExportFormat, RosterSerializer};
const PTS_TYPE_ID: &str = "pts-type";
const NEWRECRUIT_XMLNS: &str = "http://www.battlescribe.net/schema/rosterSchema";
const NEWRECRUIT_GENERATED_BY: &str = "https://newrecruit.eu";
#[derive(Serialize)]
struct Category {
name: String,
primary: bool,
}
#[derive(Serialize)]
struct Cost {
name: &'static str,
#[serde(rename = "typeId")]
type_id: &'static str,
value: u64,
}
#[derive(Serialize)]
struct Selection {
id: String,
name: String,
#[serde(rename = "type")]
kind: &'static str,
number: u64,
#[serde(skip_serializing_if = "Option::is_none")]
group: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
categories: Option<Vec<Category>>,
#[serde(skip_serializing_if = "Option::is_none")]
costs: Option<Vec<Cost>>,
#[serde(skip_serializing_if = "Option::is_none")]
selections: Option<Vec<Selection>>,
}
#[derive(Serialize)]
struct Force {
id: &'static str,
name: &'static str,
#[serde(rename = "catalogueName")]
catalogue_name: String,
selections: Vec<Selection>,
}
#[derive(Serialize)]
struct RosterPayload {
name: String,
xmlns: &'static str,
#[serde(rename = "generatedBy")]
generated_by: &'static str,
costs: Vec<Cost>,
forces: Vec<Force>,
}
#[derive(Serialize)]
struct Payload {
name: String,
#[serde(rename = "generatedBy")]
generated_by: &'static str,
roster: RosterPayload,
}
fn faction_category(roster: &Roster) -> Option<Category> {
let display = title_case_id(roster.faction_id.as_deref())?;
Some(Category {
name: format!("Faction: {display}"),
primary: false,
})
}
fn wargear_selection(idx: usize, w: &RosterWargear) -> Selection {
Selection {
id: format!("w-{idx}"),
name: w.ref_.raw_name.clone(),
kind: "upgrade",
number: w.count,
group: None,
categories: Some(vec![Category {
name: "Ranged Weapon".to_string(),
primary: false,
}]),
costs: None,
selections: None,
}
}
fn unit_selection(idx: usize, u: &RosterUnit, faction: Option<&Category>) -> Selection {
let mut inner: Vec<Selection> = Vec::new();
if u.is_warlord {
inner.push(Selection {
id: format!("u{idx}-warlord"),
name: "Warlord".to_string(),
kind: "upgrade",
number: 1,
group: None,
categories: None,
costs: None,
selections: None,
});
}
if let Some(enh) = &u.enhancement {
let costs = u.enhancement_points.map(|p| {
vec![Cost {
name: "pts",
type_id: PTS_TYPE_ID,
value: p,
}]
});
inner.push(Selection {
id: format!("u{idx}-enh"),
name: enh.raw_name.clone(),
kind: "upgrade",
number: 1,
group: Some("Enhancements"),
categories: None,
costs,
selections: None,
});
}
let wargear_selections: Vec<Selection> = u
.wargear
.iter()
.enumerate()
.map(|(wi, w)| wargear_selection(wi, w))
.collect();
let own_categories = faction.map(|f| {
vec![Category {
name: f.name.clone(),
primary: f.primary,
}]
});
let unit_costs = u.points.map(|p| {
vec![Cost {
name: "pts",
type_id: PTS_TYPE_ID,
value: p,
}]
});
if u.model_count <= 1 {
let mut selections = inner;
selections.extend(wargear_selections);
Selection {
id: format!("u-{idx}"),
name: u.ref_.raw_name.clone(),
kind: "model",
number: 1,
group: None,
categories: own_categories,
costs: unit_costs,
selections: Some(selections),
}
} else {
let model_child = Selection {
id: format!("u{idx}-model"),
name: u.ref_.raw_name.clone(),
kind: "model",
number: u.model_count,
group: None,
categories: None,
costs: None,
selections: Some(wargear_selections),
};
let mut selections = inner;
selections.push(model_child);
Selection {
id: format!("u-{idx}"),
name: u.ref_.raw_name.clone(),
kind: "unit",
number: 1,
group: None,
categories: own_categories,
costs: unit_costs,
selections: Some(selections),
}
}
}
fn config_selection(name: &'static str, value: String, idx: &'static str) -> Selection {
Selection {
id: format!("cfg-{idx}"),
name: name.to_string(),
kind: "upgrade",
number: 1,
group: None,
categories: Some(vec![Category {
name: "Configuration".to_string(),
primary: true,
}]),
costs: None,
selections: Some(vec![Selection {
id: format!("cfg-{idx}-val"),
name: value,
kind: "upgrade",
number: 1,
group: None,
categories: None,
costs: None,
selections: None,
}]),
}
}
fn battle_size_label(roster: &Roster) -> Option<String> {
match roster.battle_size? {
BattleSize::StrikeForce => Some(format!(
"Strike Force ({} Point limit)",
roster.points.declared_limit.unwrap_or(2000)
)),
BattleSize::Incursion => Some(format!(
"Incursion ({} Point limit)",
roster.points.declared_limit.unwrap_or(1000)
)),
}
}
pub struct NewRecruitJsonSerializer;
impl RosterSerializer for NewRecruitJsonSerializer {
fn id(&self) -> ExportFormat {
ExportFormat::NewrecruitJson
}
fn serialize(&self, roster: &Roster) -> String {
let faction = faction_category(roster);
let faction_display =
title_case_id(roster.faction_id.as_deref()).unwrap_or_else(|| "Unknown".to_string());
let detachment_display = title_case_id(roster.detachment_id.as_deref());
let battle_size = battle_size_label(roster);
let mut config: Vec<Selection> = Vec::new();
if let Some(b) = battle_size {
config.push(config_selection("Battle Size", b, "battle-size"));
}
if let Some(d) = detachment_display {
config.push(config_selection("Detachment", d, "detachment"));
}
let mut force_selections: Vec<Selection> = config;
for (i, u) in roster.units.iter().enumerate() {
force_selections.push(unit_selection(i, u, faction.as_ref()));
}
let force = Force {
id: "force-1",
name: "Army Roster",
catalogue_name: faction_display,
selections: force_selections,
};
let total = total_army_points(roster);
let payload = Payload {
name: roster.name.clone(),
generated_by: NEWRECRUIT_GENERATED_BY,
roster: RosterPayload {
name: roster.name.clone(),
xmlns: NEWRECRUIT_XMLNS,
generated_by: NEWRECRUIT_GENERATED_BY,
costs: vec![Cost {
name: "pts",
type_id: PTS_TYPE_ID,
value: total,
}],
forces: vec![force],
},
};
pretty_json(&payload)
}
}