1use std::collections::{HashMap, HashSet};
8use std::path::{Path, PathBuf};
9
10use serde::{Deserialize, Serialize};
11
12pub const DEFAULT_SCENES_DIR: &str = "assets/scenes";
14
15pub const DEFAULT_STORY_SCENES_DIR: &str = "maps";
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct Manifest {
20 pub name: String,
22 #[serde(rename = "dataRoot")]
24 pub data_root: String,
25 #[serde(rename = "gfxRoot", default, skip_serializing_if = "Option::is_none")]
27 pub gfx_root: Option<String>,
28 #[serde(default)]
29 pub activities: Vec<Activity>,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub game: Option<GameSection>,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub battle: Option<BattleSection>,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub shop: Option<ShopSection>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct Activity {
44 pub id: String,
45 #[serde(rename = "type")]
46 pub kind: String,
47 #[serde(default)]
48 pub label: String,
49 #[serde(default)]
50 pub icon: String,
51 #[serde(default)]
52 pub enabled: bool,
53 #[serde(default)]
55 pub config: serde_json::Value,
56}
57
58#[derive(Debug, Clone, Default, Serialize, Deserialize)]
59pub struct GameSection {
60 #[serde(rename = "entryScene", default, skip_serializing_if = "Option::is_none")]
62 pub entry_scene: Option<String>,
63 #[serde(rename = "entryMap", default, skip_serializing_if = "Option::is_none")]
65 pub entry_map: Option<String>,
66 #[serde(rename = "scenesDir", default, skip_serializing_if = "Option::is_none")]
68 pub scenes_dir: Option<String>,
69}
70
71pub const DEFAULT_SKILLS_FIELD: &str = "skills";
75pub const DEFAULT_CATEGORY_FIELD: &str = "type";
77pub const DEFAULT_COST_FIELD: &str = "mpCost";
79pub const DEFAULT_RULES_FILE: &str = "data/rules.ron";
82
83pub const DEFAULT_HEAL_FIELD: &str = "healHp";
85
86#[derive(Debug, Clone, Default, Serialize, Deserialize)]
90pub struct BattleSection {
91 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub party: Option<BattleTableRef>,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub enemies: Option<BattleTableRef>,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub encounters: Option<BattleTableRef>,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub skills: Option<BattleSkills>,
108 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub stats: Option<BattleStats>,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub resource: Option<String>,
115 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub rules: Option<String>,
120 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub items: Option<BattleItems>,
123 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub levels: Option<BattleLevels>,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct BattleItems {
133 pub table: String,
135 #[serde(rename = "healField", default = "default_heal_field")]
139 pub heal_field: String,
140 #[serde(default)]
142 pub starting: HashMap<String, u32>,
143}
144
145fn default_heal_field() -> String {
146 DEFAULT_HEAL_FIELD.to_string()
147}
148
149pub const DEFAULT_EXP_FIELD: &str = "exp";
153pub const DEFAULT_LEVEL_FIELD: &str = "level";
155pub const DEFAULT_GROWTH: f64 = 0.05;
157pub const DEFAULT_MAX_LEVEL: u32 = 100;
159pub const DEFAULT_CURVE_BASE: u32 = 8;
161pub const DEFAULT_CURVE_EXPONENT: u32 = 3;
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct BattleLevels {
170 #[serde(rename = "expField", default = "default_exp_field")]
172 pub exp_field: String,
173 #[serde(rename = "levelField", default = "default_level_field")]
175 pub level_field: String,
176 #[serde(default)]
178 pub curve: ExpCurve,
179 #[serde(default = "default_growth")]
182 pub growth: f64,
183 #[serde(rename = "maxLevel", default = "default_max_level")]
185 pub max_level: u32,
186}
187
188impl Default for BattleLevels {
189 fn default() -> Self {
190 Self {
191 exp_field: default_exp_field(),
192 level_field: default_level_field(),
193 curve: ExpCurve::default(),
194 growth: DEFAULT_GROWTH,
195 max_level: DEFAULT_MAX_LEVEL,
196 }
197 }
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct ExpCurve {
203 #[serde(default = "default_curve_base")]
204 pub base: u32,
205 #[serde(default = "default_curve_exponent")]
206 pub exponent: u32,
207}
208
209impl Default for ExpCurve {
210 fn default() -> Self {
211 Self {
212 base: DEFAULT_CURVE_BASE,
213 exponent: DEFAULT_CURVE_EXPONENT,
214 }
215 }
216}
217
218fn default_exp_field() -> String {
219 DEFAULT_EXP_FIELD.to_string()
220}
221fn default_level_field() -> String {
222 DEFAULT_LEVEL_FIELD.to_string()
223}
224fn default_growth() -> f64 {
225 DEFAULT_GROWTH
226}
227fn default_max_level() -> u32 {
228 DEFAULT_MAX_LEVEL
229}
230fn default_curve_base() -> u32 {
231 DEFAULT_CURVE_BASE
232}
233fn default_curve_exponent() -> u32 {
234 DEFAULT_CURVE_EXPONENT
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct BattleTableRef {
240 pub table: String,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct BattleSkills {
247 pub table: String,
249 #[serde(default = "default_skills_field")]
251 pub field: String,
252 #[serde(rename = "categoryField", default = "default_category_field")]
254 pub category_field: String,
255 #[serde(rename = "costField", default = "default_cost_field")]
257 pub cost_field: String,
258}
259
260fn default_skills_field() -> String {
261 DEFAULT_SKILLS_FIELD.to_string()
262}
263fn default_category_field() -> String {
264 DEFAULT_CATEGORY_FIELD.to_string()
265}
266fn default_cost_field() -> String {
267 DEFAULT_COST_FIELD.to_string()
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct BattleStats {
273 #[serde(default = "default_hp_field")]
274 pub hp: String,
275 #[serde(default = "default_atk_field")]
276 pub attack: String,
277 #[serde(default = "default_def_field")]
278 pub defense: String,
279 #[serde(default = "default_spd_field")]
280 pub speed: String,
281}
282
283impl Default for BattleStats {
284 fn default() -> Self {
285 Self {
286 hp: default_hp_field(),
287 attack: default_atk_field(),
288 defense: default_def_field(),
289 speed: default_spd_field(),
290 }
291 }
292}
293
294fn default_hp_field() -> String {
295 "hp".to_string()
296}
297fn default_atk_field() -> String {
298 "atk".to_string()
299}
300fn default_def_field() -> String {
301 "def".to_string()
302}
303fn default_spd_field() -> String {
304 "spd".to_string()
305}
306
307pub const DEFAULT_CURRENCY: &str = "G";
311pub const DEFAULT_START_MONEY: u32 = 100;
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct ShopSection {
318 #[serde(default = "default_currency")]
320 pub currency: String,
321 #[serde(rename = "startMoney", default = "default_start_money")]
323 pub start_money: u32,
324}
325
326fn default_currency() -> String {
327 DEFAULT_CURRENCY.to_string()
328}
329fn default_start_money() -> u32 {
330 DEFAULT_START_MONEY
331}
332
333#[derive(Debug, Clone, PartialEq, Eq)]
338pub struct TableDef {
339 pub id: String,
341 pub dir: String,
343 pub fields: Vec<String>,
345}
346
347impl Manifest {
348 pub fn data_tables(&self) -> Vec<TableDef> {
350 let mut out = Vec::new();
351 for activity in &self.activities {
352 if activity.kind != "data" {
353 continue;
354 }
355 let Some(tables) = activity.config.get("tables").and_then(|v| v.as_array()) else {
356 continue;
357 };
358 for table in tables {
359 let (Some(id), Some(dir)) = (
360 table.get("id").and_then(|v| v.as_str()),
361 table.get("dir").and_then(|v| v.as_str()),
362 ) else {
363 continue;
364 };
365 let fields = table
366 .get("fields")
367 .and_then(|v| v.as_array())
368 .map(|fs| {
369 fs.iter()
370 .filter_map(|f| {
371 f.as_str().map(str::to_string).or_else(|| {
372 f.get("key")
374 .or_else(|| f.get("id"))
375 .and_then(|v| v.as_str())
376 .map(str::to_string)
377 })
378 })
379 .collect()
380 })
381 .unwrap_or_default();
382 out.push(TableDef {
383 id: id.to_string(),
384 dir: dir.to_string(),
385 fields,
386 });
387 }
388 }
389 out
390 }
391
392 pub fn data_table(&self, table_id: &str) -> Option<TableDef> {
394 self.data_tables().into_iter().find(|t| t.id == table_id)
395 }
396}
397
398impl Manifest {
399 pub fn scenes_dir(&self) -> &str {
401 self.game
402 .as_ref()
403 .and_then(|g| g.scenes_dir.as_deref())
404 .unwrap_or(DEFAULT_SCENES_DIR)
405 }
406
407 pub fn dsl_dirs(&self, root: &Path) -> Vec<PathBuf> {
413 self.dsl_dirs_rel().iter().map(|d| root.join(d)).collect()
414 }
415
416 pub fn dsl_dirs_rel(&self) -> Vec<String> {
424 let data_root = crate::vfs::join_path("", &self.data_root);
425 let mut dirs = vec![crate::vfs::join_path("", self.scenes_dir())];
426 for activity in &self.activities {
427 match activity.kind.as_str() {
428 "script" => {
429 if let Some(dir) = config_str(&activity.config, "scriptsDir") {
430 dirs.push(crate::vfs::join_path(&data_root, dir));
431 }
432 }
433 "story" => {
434 let dir = config_str(&activity.config, "scenesDir")
435 .unwrap_or(DEFAULT_STORY_SCENES_DIR);
436 dirs.push(crate::vfs::join_path(&data_root, dir));
437 }
438 "ui" => {
439 if let Some(dir) = config_str(&activity.config, "guiRoot") {
440 dirs.push(crate::vfs::join_path("", dir));
441 }
442 }
443 _ => {}
444 }
445 }
446 let mut seen = HashSet::new();
447 dirs.retain(|d| seen.insert(d.clone()));
448 dirs
449 }
450}
451
452fn config_str<'a>(config: &'a serde_json::Value, key: &str) -> Option<&'a str> {
453 config.get(key).and_then(|v| v.as_str())
454}