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(
63 rename = "entryScene",
64 default,
65 skip_serializing_if = "Option::is_none"
66 )]
67 pub entry_scene: Option<String>,
68 #[serde(rename = "entryMap", default, skip_serializing_if = "Option::is_none")]
70 pub entry_map: Option<String>,
71 #[serde(rename = "scenesDir", default, skip_serializing_if = "Option::is_none")]
73 pub scenes_dir: Option<String>,
74}
75
76pub const DEFAULT_SKILLS_FIELD: &str = "skills";
80pub const DEFAULT_CATEGORY_FIELD: &str = "type";
82pub const DEFAULT_COST_FIELD: &str = "mpCost";
84pub const DEFAULT_RULES_FILE: &str = "data/rules.ron";
87
88pub const DEFAULT_HEAL_FIELD: &str = "healHp";
90
91#[derive(Debug, Clone, Default, Serialize, Deserialize)]
95pub struct BattleSection {
96 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub party: Option<BattleTableRef>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub enemies: Option<BattleTableRef>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub encounters: Option<BattleTableRef>,
110 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub skills: Option<BattleSkills>,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub stats: Option<BattleStats>,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub resource: Option<String>,
120 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub rules: Option<String>,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub items: Option<BattleItems>,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub levels: Option<BattleLevels>,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct BattleItems {
138 pub table: String,
140 #[serde(rename = "healField", default = "default_heal_field")]
144 pub heal_field: String,
145 #[serde(default)]
147 pub starting: HashMap<String, u32>,
148}
149
150fn default_heal_field() -> String {
151 DEFAULT_HEAL_FIELD.to_string()
152}
153
154pub const DEFAULT_EXP_FIELD: &str = "exp";
158pub const DEFAULT_LEVEL_FIELD: &str = "level";
160pub const DEFAULT_GROWTH: f64 = 0.05;
162pub const DEFAULT_MAX_LEVEL: u32 = 100;
164pub const DEFAULT_CURVE_BASE: u32 = 8;
166pub const DEFAULT_CURVE_EXPONENT: u32 = 3;
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct BattleLevels {
175 #[serde(rename = "expField", default = "default_exp_field")]
177 pub exp_field: String,
178 #[serde(rename = "levelField", default = "default_level_field")]
180 pub level_field: String,
181 #[serde(default)]
183 pub curve: ExpCurve,
184 #[serde(default = "default_growth")]
187 pub growth: f64,
188 #[serde(rename = "maxLevel", default = "default_max_level")]
190 pub max_level: u32,
191}
192
193impl Default for BattleLevels {
194 fn default() -> Self {
195 Self {
196 exp_field: default_exp_field(),
197 level_field: default_level_field(),
198 curve: ExpCurve::default(),
199 growth: DEFAULT_GROWTH,
200 max_level: DEFAULT_MAX_LEVEL,
201 }
202 }
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct ExpCurve {
208 #[serde(default = "default_curve_base")]
209 pub base: u32,
210 #[serde(default = "default_curve_exponent")]
211 pub exponent: u32,
212}
213
214impl Default for ExpCurve {
215 fn default() -> Self {
216 Self {
217 base: DEFAULT_CURVE_BASE,
218 exponent: DEFAULT_CURVE_EXPONENT,
219 }
220 }
221}
222
223fn default_exp_field() -> String {
224 DEFAULT_EXP_FIELD.to_string()
225}
226fn default_level_field() -> String {
227 DEFAULT_LEVEL_FIELD.to_string()
228}
229fn default_growth() -> f64 {
230 DEFAULT_GROWTH
231}
232fn default_max_level() -> u32 {
233 DEFAULT_MAX_LEVEL
234}
235fn default_curve_base() -> u32 {
236 DEFAULT_CURVE_BASE
237}
238fn default_curve_exponent() -> u32 {
239 DEFAULT_CURVE_EXPONENT
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct BattleTableRef {
245 pub table: String,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct BattleSkills {
252 pub table: String,
254 #[serde(default = "default_skills_field")]
256 pub field: String,
257 #[serde(rename = "categoryField", default = "default_category_field")]
259 pub category_field: String,
260 #[serde(rename = "costField", default = "default_cost_field")]
262 pub cost_field: String,
263}
264
265fn default_skills_field() -> String {
266 DEFAULT_SKILLS_FIELD.to_string()
267}
268fn default_category_field() -> String {
269 DEFAULT_CATEGORY_FIELD.to_string()
270}
271fn default_cost_field() -> String {
272 DEFAULT_COST_FIELD.to_string()
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct BattleStats {
278 #[serde(default = "default_hp_field")]
279 pub hp: String,
280 #[serde(default = "default_atk_field")]
281 pub attack: String,
282 #[serde(default = "default_def_field")]
283 pub defense: String,
284 #[serde(default = "default_spd_field")]
285 pub speed: String,
286}
287
288impl Default for BattleStats {
289 fn default() -> Self {
290 Self {
291 hp: default_hp_field(),
292 attack: default_atk_field(),
293 defense: default_def_field(),
294 speed: default_spd_field(),
295 }
296 }
297}
298
299fn default_hp_field() -> String {
300 "hp".to_string()
301}
302fn default_atk_field() -> String {
303 "atk".to_string()
304}
305fn default_def_field() -> String {
306 "def".to_string()
307}
308fn default_spd_field() -> String {
309 "spd".to_string()
310}
311
312pub const DEFAULT_CURRENCY: &str = "G";
316pub const DEFAULT_START_MONEY: u32 = 100;
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct ShopSection {
323 #[serde(default = "default_currency")]
325 pub currency: String,
326 #[serde(rename = "startMoney", default = "default_start_money")]
328 pub start_money: u32,
329}
330
331fn default_currency() -> String {
332 DEFAULT_CURRENCY.to_string()
333}
334fn default_start_money() -> u32 {
335 DEFAULT_START_MONEY
336}
337
338#[derive(Debug, Clone, PartialEq, Eq)]
343pub struct TableDef {
344 pub id: String,
346 pub dir: String,
348 pub fields: Vec<String>,
350}
351
352impl Manifest {
353 pub fn data_tables(&self) -> Vec<TableDef> {
355 let mut out = Vec::new();
356 for activity in &self.activities {
357 if activity.kind != "data" {
358 continue;
359 }
360 let Some(tables) = activity.config.get("tables").and_then(|v| v.as_array()) else {
361 continue;
362 };
363 for table in tables {
364 let (Some(id), Some(dir)) = (
365 table.get("id").and_then(|v| v.as_str()),
366 table.get("dir").and_then(|v| v.as_str()),
367 ) else {
368 continue;
369 };
370 let fields = table
371 .get("fields")
372 .and_then(|v| v.as_array())
373 .map(|fs| {
374 fs.iter()
375 .filter_map(|f| {
376 f.as_str().map(str::to_string).or_else(|| {
377 f.get("key")
379 .or_else(|| f.get("id"))
380 .and_then(|v| v.as_str())
381 .map(str::to_string)
382 })
383 })
384 .collect()
385 })
386 .unwrap_or_default();
387 out.push(TableDef {
388 id: id.to_string(),
389 dir: dir.to_string(),
390 fields,
391 });
392 }
393 }
394 out
395 }
396
397 pub fn data_table(&self, table_id: &str) -> Option<TableDef> {
399 self.data_tables().into_iter().find(|t| t.id == table_id)
400 }
401}
402
403impl Manifest {
404 pub fn scenes_dir(&self) -> &str {
406 self.game
407 .as_ref()
408 .and_then(|g| g.scenes_dir.as_deref())
409 .unwrap_or(DEFAULT_SCENES_DIR)
410 }
411
412 pub fn dsl_dirs(&self, root: &Path) -> Vec<PathBuf> {
418 self.dsl_dirs_rel().iter().map(|d| root.join(d)).collect()
419 }
420
421 pub fn dsl_dirs_rel(&self) -> Vec<String> {
429 let data_root = crate::vfs::join_path("", &self.data_root);
430 let mut dirs = vec![crate::vfs::join_path("", self.scenes_dir())];
431 for activity in &self.activities {
432 match activity.kind.as_str() {
433 "script" => {
434 if let Some(dir) = config_str(&activity.config, "scriptsDir") {
435 dirs.push(crate::vfs::join_path(&data_root, dir));
436 }
437 }
438 "story" => {
439 let dir = config_str(&activity.config, "scenesDir")
440 .unwrap_or(DEFAULT_STORY_SCENES_DIR);
441 dirs.push(crate::vfs::join_path(&data_root, dir));
442 }
443 "ui" => {
444 if let Some(dir) = config_str(&activity.config, "guiRoot") {
445 dirs.push(crate::vfs::join_path("", dir));
446 }
447 }
448 _ => {}
449 }
450 }
451 let mut seen = HashSet::new();
452 dirs.retain(|d| seen.insert(d.clone()));
453 dirs
454 }
455}
456
457fn config_str<'a>(config: &'a serde_json::Value, key: &str) -> Option<&'a str> {
458 config.get(key).and_then(|v| v.as_str())
459}