use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
pub const DEFAULT_SCENES_DIR: &str = "assets/scenes";
pub const DEFAULT_STORY_SCENES_DIR: &str = "maps";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Manifest {
pub name: String,
#[serde(rename = "dataRoot")]
pub data_root: String,
#[serde(rename = "gfxRoot", default, skip_serializing_if = "Option::is_none")]
pub gfx_root: Option<String>,
#[serde(default)]
pub activities: Vec<Activity>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub game: Option<GameSection>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub battle: Option<BattleSection>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub shop: Option<ShopSection>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Activity {
pub id: String,
#[serde(rename = "type")]
pub kind: String,
#[serde(default)]
pub label: String,
#[serde(default)]
pub icon: String,
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub config: serde_json::Value,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GameSection {
#[serde(rename = "entryScene", default, skip_serializing_if = "Option::is_none")]
pub entry_scene: Option<String>,
#[serde(rename = "entryMap", default, skip_serializing_if = "Option::is_none")]
pub entry_map: Option<String>,
#[serde(rename = "scenesDir", default, skip_serializing_if = "Option::is_none")]
pub scenes_dir: Option<String>,
}
pub const DEFAULT_SKILLS_FIELD: &str = "skills";
pub const DEFAULT_CATEGORY_FIELD: &str = "type";
pub const DEFAULT_COST_FIELD: &str = "mpCost";
pub const DEFAULT_RULES_FILE: &str = "data/rules.ron";
pub const DEFAULT_HEAL_FIELD: &str = "healHp";
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct BattleSection {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub party: Option<BattleTableRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enemies: Option<BattleTableRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub encounters: Option<BattleTableRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub skills: Option<BattleSkills>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stats: Option<BattleStats>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resource: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rules: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub items: Option<BattleItems>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub levels: Option<BattleLevels>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BattleItems {
pub table: String,
#[serde(rename = "healField", default = "default_heal_field")]
pub heal_field: String,
#[serde(default)]
pub starting: HashMap<String, u32>,
}
fn default_heal_field() -> String {
DEFAULT_HEAL_FIELD.to_string()
}
pub const DEFAULT_EXP_FIELD: &str = "exp";
pub const DEFAULT_LEVEL_FIELD: &str = "level";
pub const DEFAULT_GROWTH: f64 = 0.05;
pub const DEFAULT_MAX_LEVEL: u32 = 100;
pub const DEFAULT_CURVE_BASE: u32 = 8;
pub const DEFAULT_CURVE_EXPONENT: u32 = 3;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BattleLevels {
#[serde(rename = "expField", default = "default_exp_field")]
pub exp_field: String,
#[serde(rename = "levelField", default = "default_level_field")]
pub level_field: String,
#[serde(default)]
pub curve: ExpCurve,
#[serde(default = "default_growth")]
pub growth: f64,
#[serde(rename = "maxLevel", default = "default_max_level")]
pub max_level: u32,
}
impl Default for BattleLevels {
fn default() -> Self {
Self {
exp_field: default_exp_field(),
level_field: default_level_field(),
curve: ExpCurve::default(),
growth: DEFAULT_GROWTH,
max_level: DEFAULT_MAX_LEVEL,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExpCurve {
#[serde(default = "default_curve_base")]
pub base: u32,
#[serde(default = "default_curve_exponent")]
pub exponent: u32,
}
impl Default for ExpCurve {
fn default() -> Self {
Self {
base: DEFAULT_CURVE_BASE,
exponent: DEFAULT_CURVE_EXPONENT,
}
}
}
fn default_exp_field() -> String {
DEFAULT_EXP_FIELD.to_string()
}
fn default_level_field() -> String {
DEFAULT_LEVEL_FIELD.to_string()
}
fn default_growth() -> f64 {
DEFAULT_GROWTH
}
fn default_max_level() -> u32 {
DEFAULT_MAX_LEVEL
}
fn default_curve_base() -> u32 {
DEFAULT_CURVE_BASE
}
fn default_curve_exponent() -> u32 {
DEFAULT_CURVE_EXPONENT
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BattleTableRef {
pub table: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BattleSkills {
pub table: String,
#[serde(default = "default_skills_field")]
pub field: String,
#[serde(rename = "categoryField", default = "default_category_field")]
pub category_field: String,
#[serde(rename = "costField", default = "default_cost_field")]
pub cost_field: String,
}
fn default_skills_field() -> String {
DEFAULT_SKILLS_FIELD.to_string()
}
fn default_category_field() -> String {
DEFAULT_CATEGORY_FIELD.to_string()
}
fn default_cost_field() -> String {
DEFAULT_COST_FIELD.to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BattleStats {
#[serde(default = "default_hp_field")]
pub hp: String,
#[serde(default = "default_atk_field")]
pub attack: String,
#[serde(default = "default_def_field")]
pub defense: String,
#[serde(default = "default_spd_field")]
pub speed: String,
}
impl Default for BattleStats {
fn default() -> Self {
Self {
hp: default_hp_field(),
attack: default_atk_field(),
defense: default_def_field(),
speed: default_spd_field(),
}
}
}
fn default_hp_field() -> String {
"hp".to_string()
}
fn default_atk_field() -> String {
"atk".to_string()
}
fn default_def_field() -> String {
"def".to_string()
}
fn default_spd_field() -> String {
"spd".to_string()
}
pub const DEFAULT_CURRENCY: &str = "G";
pub const DEFAULT_START_MONEY: u32 = 100;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShopSection {
#[serde(default = "default_currency")]
pub currency: String,
#[serde(rename = "startMoney", default = "default_start_money")]
pub start_money: u32,
}
fn default_currency() -> String {
DEFAULT_CURRENCY.to_string()
}
fn default_start_money() -> u32 {
DEFAULT_START_MONEY
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TableDef {
pub id: String,
pub dir: String,
pub fields: Vec<String>,
}
impl Manifest {
pub fn data_tables(&self) -> Vec<TableDef> {
let mut out = Vec::new();
for activity in &self.activities {
if activity.kind != "data" {
continue;
}
let Some(tables) = activity.config.get("tables").and_then(|v| v.as_array()) else {
continue;
};
for table in tables {
let (Some(id), Some(dir)) = (
table.get("id").and_then(|v| v.as_str()),
table.get("dir").and_then(|v| v.as_str()),
) else {
continue;
};
let fields = table
.get("fields")
.and_then(|v| v.as_array())
.map(|fs| {
fs.iter()
.filter_map(|f| {
f.as_str().map(str::to_string).or_else(|| {
f.get("key")
.or_else(|| f.get("id"))
.and_then(|v| v.as_str())
.map(str::to_string)
})
})
.collect()
})
.unwrap_or_default();
out.push(TableDef {
id: id.to_string(),
dir: dir.to_string(),
fields,
});
}
}
out
}
pub fn data_table(&self, table_id: &str) -> Option<TableDef> {
self.data_tables().into_iter().find(|t| t.id == table_id)
}
}
impl Manifest {
pub fn scenes_dir(&self) -> &str {
self.game
.as_ref()
.and_then(|g| g.scenes_dir.as_deref())
.unwrap_or(DEFAULT_SCENES_DIR)
}
pub fn dsl_dirs(&self, root: &Path) -> Vec<PathBuf> {
self.dsl_dirs_rel().iter().map(|d| root.join(d)).collect()
}
pub fn dsl_dirs_rel(&self) -> Vec<String> {
let data_root = crate::vfs::join_path("", &self.data_root);
let mut dirs = vec![crate::vfs::join_path("", self.scenes_dir())];
for activity in &self.activities {
match activity.kind.as_str() {
"script" => {
if let Some(dir) = config_str(&activity.config, "scriptsDir") {
dirs.push(crate::vfs::join_path(&data_root, dir));
}
}
"story" => {
let dir = config_str(&activity.config, "scenesDir")
.unwrap_or(DEFAULT_STORY_SCENES_DIR);
dirs.push(crate::vfs::join_path(&data_root, dir));
}
"ui" => {
if let Some(dir) = config_str(&activity.config, "guiRoot") {
dirs.push(crate::vfs::join_path("", dir));
}
}
_ => {}
}
}
let mut seen = HashSet::new();
dirs.retain(|d| seen.insert(d.clone()));
dirs
}
}
fn config_str<'a>(config: &'a serde_json::Value, key: &str) -> Option<&'a str> {
config.get(key).and_then(|v| v.as_str())
}