1use std::collections::{HashMap, HashSet};
9use std::path::{Path, PathBuf};
10
11use serde::{Deserialize, Serialize};
12
13pub const DEFAULT_SCENES_DIR: &str = "assets/scenes";
15
16pub const DEFAULT_STORY_SCENES_DIR: &str = "maps";
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct Manifest {
21 pub name: String,
23 #[serde(rename = "dataRoot")]
25 pub data_root: String,
26 #[serde(rename = "gfxRoot", default, skip_serializing_if = "Option::is_none")]
28 pub gfx_root: Option<String>,
29 #[serde(default)]
30 pub activities: Vec<Activity>,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub game: Option<GameSection>,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub battle: Option<BattleSection>,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub shop: Option<ShopSection>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct Activity {
45 pub id: String,
46 #[serde(rename = "type")]
47 pub kind: String,
48 #[serde(default)]
49 pub label: String,
50 #[serde(default)]
51 pub icon: String,
52 #[serde(default)]
53 pub enabled: bool,
54 #[serde(default)]
56 pub config: serde_json::Value,
57}
58
59#[derive(Debug, Clone, Default, Serialize, Deserialize)]
60pub struct GameSection {
61 #[serde(rename = "entryScene", default, skip_serializing_if = "Option::is_none")]
63 pub entry_scene: Option<String>,
64 #[serde(rename = "entryMap", default, skip_serializing_if = "Option::is_none")]
66 pub entry_map: Option<String>,
67 #[serde(rename = "scenesDir", default, skip_serializing_if = "Option::is_none")]
69 pub scenes_dir: Option<String>,
70}
71
72pub const DEFAULT_SKILLS_FIELD: &str = "skills";
76pub const DEFAULT_CATEGORY_FIELD: &str = "type";
78pub const DEFAULT_COST_FIELD: &str = "mpCost";
80pub const DEFAULT_RULES_FILE: &str = "data/rules.ron";
83
84pub const DEFAULT_HEAL_FIELD: &str = "healHp";
86
87#[derive(Debug, Clone, Default, Serialize, Deserialize)]
91pub struct BattleSection {
92 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub party: Option<BattleTableRef>,
96 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub enemies: Option<BattleTableRef>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub encounters: Option<BattleTableRef>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub skills: Option<BattleSkills>,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub stats: Option<BattleStats>,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub resource: Option<String>,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub rules: Option<String>,
121 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub items: Option<BattleItems>,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub levels: Option<BattleLevels>,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct BattleItems {
134 pub table: String,
136 #[serde(rename = "healField", default = "default_heal_field")]
140 pub heal_field: String,
141 #[serde(default)]
143 pub starting: HashMap<String, u32>,
144}
145
146fn default_heal_field() -> String {
147 DEFAULT_HEAL_FIELD.to_string()
148}
149
150pub const DEFAULT_EXP_FIELD: &str = "exp";
154pub const DEFAULT_LEVEL_FIELD: &str = "level";
156pub const DEFAULT_GROWTH: f64 = 0.05;
158pub const DEFAULT_MAX_LEVEL: u32 = 100;
160pub const DEFAULT_CURVE_BASE: u32 = 8;
162pub const DEFAULT_CURVE_EXPONENT: u32 = 3;
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct BattleLevels {
171 #[serde(rename = "expField", default = "default_exp_field")]
173 pub exp_field: String,
174 #[serde(rename = "levelField", default = "default_level_field")]
176 pub level_field: String,
177 #[serde(default)]
179 pub curve: ExpCurve,
180 #[serde(default = "default_growth")]
183 pub growth: f64,
184 #[serde(rename = "maxLevel", default = "default_max_level")]
186 pub max_level: u32,
187}
188
189impl Default for BattleLevels {
190 fn default() -> Self {
191 Self {
192 exp_field: default_exp_field(),
193 level_field: default_level_field(),
194 curve: ExpCurve::default(),
195 growth: DEFAULT_GROWTH,
196 max_level: DEFAULT_MAX_LEVEL,
197 }
198 }
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct ExpCurve {
204 #[serde(default = "default_curve_base")]
205 pub base: u32,
206 #[serde(default = "default_curve_exponent")]
207 pub exponent: u32,
208}
209
210impl Default for ExpCurve {
211 fn default() -> Self {
212 Self {
213 base: DEFAULT_CURVE_BASE,
214 exponent: DEFAULT_CURVE_EXPONENT,
215 }
216 }
217}
218
219fn default_exp_field() -> String {
220 DEFAULT_EXP_FIELD.to_string()
221}
222fn default_level_field() -> String {
223 DEFAULT_LEVEL_FIELD.to_string()
224}
225fn default_growth() -> f64 {
226 DEFAULT_GROWTH
227}
228fn default_max_level() -> u32 {
229 DEFAULT_MAX_LEVEL
230}
231fn default_curve_base() -> u32 {
232 DEFAULT_CURVE_BASE
233}
234fn default_curve_exponent() -> u32 {
235 DEFAULT_CURVE_EXPONENT
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct BattleTableRef {
241 pub table: String,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct BattleSkills {
248 pub table: String,
250 #[serde(default = "default_skills_field")]
252 pub field: String,
253 #[serde(rename = "categoryField", default = "default_category_field")]
255 pub category_field: String,
256 #[serde(rename = "costField", default = "default_cost_field")]
258 pub cost_field: String,
259}
260
261fn default_skills_field() -> String {
262 DEFAULT_SKILLS_FIELD.to_string()
263}
264fn default_category_field() -> String {
265 DEFAULT_CATEGORY_FIELD.to_string()
266}
267fn default_cost_field() -> String {
268 DEFAULT_COST_FIELD.to_string()
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct BattleStats {
274 #[serde(default = "default_hp_field")]
275 pub hp: String,
276 #[serde(default = "default_atk_field")]
277 pub attack: String,
278 #[serde(default = "default_def_field")]
279 pub defense: String,
280 #[serde(default = "default_spd_field")]
281 pub speed: String,
282}
283
284impl Default for BattleStats {
285 fn default() -> Self {
286 Self {
287 hp: default_hp_field(),
288 attack: default_atk_field(),
289 defense: default_def_field(),
290 speed: default_spd_field(),
291 }
292 }
293}
294
295fn default_hp_field() -> String {
296 "hp".to_string()
297}
298fn default_atk_field() -> String {
299 "atk".to_string()
300}
301fn default_def_field() -> String {
302 "def".to_string()
303}
304fn default_spd_field() -> String {
305 "spd".to_string()
306}
307
308pub const DEFAULT_CURRENCY: &str = "G";
312pub const DEFAULT_START_MONEY: u32 = 100;
314
315#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct ShopSection {
319 #[serde(default = "default_currency")]
321 pub currency: String,
322 #[serde(rename = "startMoney", default = "default_start_money")]
324 pub start_money: u32,
325}
326
327fn default_currency() -> String {
328 DEFAULT_CURRENCY.to_string()
329}
330fn default_start_money() -> u32 {
331 DEFAULT_START_MONEY
332}
333
334#[derive(Debug, Clone, PartialEq, Eq)]
339pub struct TableDef {
340 pub id: String,
342 pub dir: String,
344 pub fields: Vec<String>,
346}
347
348impl Manifest {
349 pub fn data_tables(&self) -> Vec<TableDef> {
351 let mut out = Vec::new();
352 for activity in &self.activities {
353 if activity.kind != "data" {
354 continue;
355 }
356 let Some(tables) = activity.config.get("tables").and_then(|v| v.as_array()) else {
357 continue;
358 };
359 for table in tables {
360 let (Some(id), Some(dir)) = (
361 table.get("id").and_then(|v| v.as_str()),
362 table.get("dir").and_then(|v| v.as_str()),
363 ) else {
364 continue;
365 };
366 let fields = table
367 .get("fields")
368 .and_then(|v| v.as_array())
369 .map(|fs| {
370 fs.iter()
371 .filter_map(|f| {
372 f.as_str().map(str::to_string).or_else(|| {
373 f.get("key")
375 .or_else(|| f.get("id"))
376 .and_then(|v| v.as_str())
377 .map(str::to_string)
378 })
379 })
380 .collect()
381 })
382 .unwrap_or_default();
383 out.push(TableDef {
384 id: id.to_string(),
385 dir: dir.to_string(),
386 fields,
387 });
388 }
389 }
390 out
391 }
392
393 pub fn data_table(&self, table_id: &str) -> Option<TableDef> {
395 self.data_tables().into_iter().find(|t| t.id == table_id)
396 }
397}
398
399impl Manifest {
400 pub fn scenes_dir(&self) -> &str {
402 self.game
403 .as_ref()
404 .and_then(|g| g.scenes_dir.as_deref())
405 .unwrap_or(DEFAULT_SCENES_DIR)
406 }
407
408 pub fn dsl_dirs(&self, root: &Path) -> Vec<PathBuf> {
414 self.dsl_dirs_rel().iter().map(|d| root.join(d)).collect()
415 }
416
417 pub fn dsl_dirs_rel(&self) -> Vec<String> {
425 let data_root = crate::vfs::join_path("", &self.data_root);
426 let mut dirs = vec![crate::vfs::join_path("", self.scenes_dir())];
427 for activity in &self.activities {
428 match activity.kind.as_str() {
429 "script" => {
430 if let Some(dir) = config_str(&activity.config, "scriptsDir") {
431 dirs.push(crate::vfs::join_path(&data_root, dir));
432 }
433 }
434 "story" => {
435 let dir = config_str(&activity.config, "scenesDir")
436 .unwrap_or(DEFAULT_STORY_SCENES_DIR);
437 dirs.push(crate::vfs::join_path(&data_root, dir));
438 }
439 "ui" => {
440 if let Some(dir) = config_str(&activity.config, "guiRoot") {
441 dirs.push(crate::vfs::join_path("", dir));
442 }
443 }
444 _ => {}
445 }
446 }
447 let mut seen = HashSet::new();
448 dirs.retain(|d| seen.insert(d.clone()));
449 dirs
450 }
451}
452
453fn config_str<'a>(config: &'a serde_json::Value, key: &str) -> Option<&'a str> {
454 config.get(key).and_then(|v| v.as_str())
455}