diff --git a/Cargo.toml b/Cargo.toml
index dcda92a1..9a38251d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,6 +6,7 @@ members = [
"macro_ui",
"rpg_tools_core",
"rpg_tools_rendering",
+ "rpg_tools_ui",
"rpg_tools_editor",
]
resolver = "2"
\ No newline at end of file
diff --git a/macro_convert/Cargo.toml b/macro_convert/Cargo.toml
index 05ddda0d..0267e325 100644
--- a/macro_convert/Cargo.toml
+++ b/macro_convert/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "macro_convert"
-version = "0.3.0"
+version = "0.4.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
diff --git a/macro_core/Cargo.toml b/macro_core/Cargo.toml
index 7fca3dfd..e7e27d11 100644
--- a/macro_core/Cargo.toml
+++ b/macro_core/Cargo.toml
@@ -1,4 +1,4 @@
[package]
name = "macro_core"
-version = "0.3.0"
+version = "0.4.0"
edition = "2021"
diff --git a/macro_ui/Cargo.toml b/macro_ui/Cargo.toml
index 292a582e..f2346dfc 100644
--- a/macro_ui/Cargo.toml
+++ b/macro_ui/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "macro_ui"
-version = "0.3.0"
+version = "0.4.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
diff --git a/resources/characters/characters.yaml b/resources/characters/characters.yaml
index fe67a41f..4e2f93ff 100644
--- a/resources/characters/characters.yaml
+++ b/resources/characters/characters.yaml
@@ -1,5 +1,6 @@
- id: 0
name: Elf
+ race: 1
gender: Male
appearance:
type: Humanoid
@@ -88,6 +89,7 @@
millimetre: 1500
- id: 1
name: Devil
+ race: 4
gender: Male
appearance:
type: Humanoid
@@ -139,45 +141,8 @@
height:
millimetre: 2500
- id: 2
- name: Watcher
- gender: Genderless
- appearance:
- type: HeadOnly
- head:
- ears:
- type: None
- eyes:
- type: One
- eye:
- type: Normal
- eye_shape: Circle
- pupil_shape: VerticalSlit
- pupil_color: Blue
- background_color: White
- eyebrows:
- type: Normal
- color: Fuchsia
- shape: Curved
- style: Even
- width: Average
- hair:
- type: None
- mouth:
- type: Simple
- beard:
- type: None
- width: Medium
- teeth:
- type: None
- teeth_color: White
- shape: Round
- skin:
- type: Scales
- color: Purple
- height:
- millimetre: 1500
-- id: 3
name: Vampire
+ race: 5
gender: Female
appearance:
type: Humanoid
@@ -248,8 +213,9 @@
color: White
height:
millimetre: 1500
-- id: 4
+- id: 3
name: Orc
+ race: 2
gender: Male
appearance:
type: Humanoid
@@ -320,8 +286,9 @@
color: Green
height:
millimetre: 1500
-- id: 5
+- id: 4
name: Dwarf
+ race: 3
gender: Male
appearance:
type: Humanoid
diff --git a/resources/races.yaml b/resources/races.yaml
new file mode 100644
index 00000000..cb5b19fa
--- /dev/null
+++ b/resources/races.yaml
@@ -0,0 +1,18 @@
+- id: 0
+ name: Human
+ gender_option: TwoGenders
+- id: 1
+ name: Elf
+ gender_option: TwoGenders
+- id: 2
+ name: Orc
+ gender_option: TwoGenders
+- id: 3
+ name: Dwarf
+ gender_option: TwoGenders
+- id: 4
+ name: Devil
+ gender_option: NoGender
+- id: 5
+ name: Vampire
+ gender_option: TwoGenders
diff --git a/resources/templates/appearance_edit.html.tera b/resources/templates/appearance_edit.html.tera
index 4d4f35f3..ab86319d 100644
--- a/resources/templates/appearance_edit.html.tera
+++ b/resources/templates/appearance_edit.html.tera
@@ -8,7 +8,7 @@
{% endblock content %}
diff --git a/resources/templates/character.html.tera b/resources/templates/character.html.tera
deleted file mode 100644
index 0913f4f1..00000000
--- a/resources/templates/character.html.tera
+++ /dev/null
@@ -1,22 +0,0 @@
-{% extends "base" %}
-
-{% block content %}
-
-
{{ name }}
-
Data
-
-
Id: {{ id }}
-
Gender: {{ gender }}
-
Edit Data
-
-
Appearance
-
-
-
-
-
-
-{% endblock content %}
diff --git a/resources/templates/character/all.html.tera b/resources/templates/character/all.html.tera
new file mode 100644
index 00000000..d9101c44
--- /dev/null
+++ b/resources/templates/character/all.html.tera
@@ -0,0 +1,20 @@
+{% extends "base" %}
+
+{% block content %}
+ Characters
+
+
Total: {{ total }}
+
Add
+
+
+ {% for c in characters %}
+
+ {% endfor %}
+
+ Back
+{% endblock content %}
diff --git a/resources/templates/character/details.html.tera b/resources/templates/character/details.html.tera
new file mode 100644
index 00000000..762d2c10
--- /dev/null
+++ b/resources/templates/character/details.html.tera
@@ -0,0 +1,23 @@
+{% extends "base" %}
+
+{% block content %}
+
+
Character: {{ name }}
+
Data
+
+
Id: {{ id }}
+
Race: {{ race }}
+
Gender: {{ gender }}
+
Edit Data
+
+
Appearance
+
+
+
+
+
+
+{% endblock content %}
diff --git a/resources/templates/character/edit.html.tera b/resources/templates/character/edit.html.tera
new file mode 100644
index 00000000..4d410edf
--- /dev/null
+++ b/resources/templates/character/edit.html.tera
@@ -0,0 +1,27 @@
+{% extends "base" %}
+
+{% block content %}
+ Edit data of {{ name }}
+
+
Id: {{ id }}
+
+
Back
+
+{% endblock content %}
diff --git a/resources/templates/character_edit.html.tera b/resources/templates/character_edit.html.tera
deleted file mode 100644
index cfe23731..00000000
--- a/resources/templates/character_edit.html.tera
+++ /dev/null
@@ -1,18 +0,0 @@
-{% extends "base" %}
-
-{% block content %}
- Edit data of {{ name }}
- Id: {{ id }}
-
- Back
-{% endblock content %}
diff --git a/resources/templates/characters.html.tera b/resources/templates/characters.html.tera
deleted file mode 100644
index c6dbd8ed..00000000
--- a/resources/templates/characters.html.tera
+++ /dev/null
@@ -1,18 +0,0 @@
-{% extends "base" %}
-
-{% block content %}
- Characters
- Total: {{ total }}
- Add
-
- {% for c in characters %}
-
- {% endfor %}
-
- Back
-{% endblock content %}
diff --git a/resources/templates/home.html.tera b/resources/templates/home.html.tera
index 823f40d2..16935c0a 100644
--- a/resources/templates/home.html.tera
+++ b/resources/templates/home.html.tera
@@ -3,5 +3,8 @@
{% block content %}
RPG Tools - Editor
Overview
- Characters: {{ characters }}
+
{% endblock content %}
diff --git a/resources/templates/race/all.html.tera b/resources/templates/race/all.html.tera
new file mode 100644
index 00000000..739fcdbe
--- /dev/null
+++ b/resources/templates/race/all.html.tera
@@ -0,0 +1,15 @@
+{% extends "base" %}
+
+{% block content %}
+ Races
+
+
Total: {{ total }}
+
+ {% for r in races %}
+ - {{ r.1 }}
+ {% endfor %}
+
+
Add
+
Back
+
+{% endblock content %}
diff --git a/resources/templates/race/details.html.tera b/resources/templates/race/details.html.tera
new file mode 100644
index 00000000..2bcedf2e
--- /dev/null
+++ b/resources/templates/race/details.html.tera
@@ -0,0 +1,22 @@
+{% extends "base" %}
+
+{% block content %}
+
+
Race: {{ name }}
+
Data
+
+
Id: {{ id }}
+
Gender Option: {{ gender_option }}
+
Edit Data
+
+
Characters
+
+
+ {% for c in characters %}
+ - {{ c.1 }}
+ {% endfor %}
+
+
Back
+
+
+{% endblock content %}
diff --git a/resources/templates/race/edit.html.tera b/resources/templates/race/edit.html.tera
new file mode 100644
index 00000000..4469a04c
--- /dev/null
+++ b/resources/templates/race/edit.html.tera
@@ -0,0 +1,22 @@
+{% extends "base" %}
+
+{% block content %}
+ Edit Race: {{ name }}
+
+
Id: {{ id }}
+
+
Back
+
+{% endblock content %}
diff --git a/rpg_tools_core/Cargo.toml b/rpg_tools_core/Cargo.toml
index 57c52130..e2fe46d7 100644
--- a/rpg_tools_core/Cargo.toml
+++ b/rpg_tools_core/Cargo.toml
@@ -1,13 +1,13 @@
[package]
name = "rpg_tools_core"
-version = "0.3.0"
+version = "0.4.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
+anyhow = "1.0"
macro_core = { path = "../macro_core" }
macro_convert = { path = "../macro_convert" }
macro_ui = { path = "../macro_ui" }
-serde = { version = "1.0", features = ["derive"] }
-titlecase = "2.2"
\ No newline at end of file
+serde = { version = "1.0", features = ["derive"] }
\ No newline at end of file
diff --git a/rpg_tools_core/src/lib.rs b/rpg_tools_core/src/lib.rs
index 51ff0b59..074ba809 100644
--- a/rpg_tools_core/src/lib.rs
+++ b/rpg_tools_core/src/lib.rs
@@ -1,5 +1,5 @@
pub mod model;
-pub mod ui;
+pub mod usecase;
extern crate macro_core;
extern crate macro_ui;
diff --git a/rpg_tools_core/src/model/character/mod.rs b/rpg_tools_core/src/model/character/mod.rs
index 2b196e5a..e51bc76d 100644
--- a/rpg_tools_core/src/model/character/mod.rs
+++ b/rpg_tools_core/src/model/character/mod.rs
@@ -1,5 +1,6 @@
use crate::model::character::appearance::Appearance;
use crate::model::character::gender::Gender;
+use crate::model::race::RaceId;
use serde::{Deserialize, Serialize};
pub mod appearance;
@@ -25,6 +26,7 @@ impl CharacterId {
pub struct Character {
id: CharacterId,
name: String,
+ race: RaceId,
gender: Gender,
appearance: Appearance,
}
@@ -34,6 +36,7 @@ impl Character {
Character {
id,
name: format!("Character {}", id.0),
+ race: RaceId::new(0),
gender: Gender::default(),
appearance: Appearance::default(),
}
@@ -51,6 +54,14 @@ impl Character {
self.name = name;
}
+ pub fn race(&self) -> RaceId {
+ self.race
+ }
+
+ pub fn set_race(&mut self, race: RaceId) {
+ self.race = race;
+ }
+
pub fn gender(&self) -> Gender {
self.gender
}
diff --git a/rpg_tools_core/src/model/mod.rs b/rpg_tools_core/src/model/mod.rs
index 53fc634a..7b278110 100644
--- a/rpg_tools_core/src/model/mod.rs
+++ b/rpg_tools_core/src/model/mod.rs
@@ -1,8 +1,18 @@
+use crate::model::character::manager::CharacterMgr;
+use crate::model::race::manager::RaceMgr;
+
pub mod character;
pub mod color;
pub mod equipment;
pub mod length;
+pub mod race;
pub mod side;
pub mod size;
pub mod transparency;
pub mod width;
+
+#[derive(Default, Debug)]
+pub struct RpgData {
+ pub character_manager: CharacterMgr,
+ pub race_manager: RaceMgr,
+}
diff --git a/rpg_tools_core/src/model/race/gender.rs b/rpg_tools_core/src/model/race/gender.rs
new file mode 100644
index 00000000..83d89e97
--- /dev/null
+++ b/rpg_tools_core/src/model/race/gender.rs
@@ -0,0 +1,33 @@
+use crate::model::character::gender::Gender;
+use macro_convert::Convert;
+use serde::{Deserialize, Serialize};
+
+/// Which [`genders`](Gender) are available for members of this [`race`](crate::model::race::Race)?
+#[derive(Convert, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
+pub enum GenderOption {
+ NoGender,
+ TwoGenders,
+}
+
+impl GenderOption {
+ /// Is the [`gender`](Gender) valid for this option?
+ ///
+ /// ```
+ ///# use rpg_tools_core::model::race::gender::GenderOption::*;
+ ///# use rpg_tools_core::model::character::gender::Gender::*;
+ ///
+ /// assert!(!NoGender.is_valid(Female));
+ /// assert!(!NoGender.is_valid(Male));
+ /// assert!(NoGender.is_valid(Genderless));
+ ///
+ /// assert!(TwoGenders.is_valid(Female));
+ /// assert!(TwoGenders.is_valid(Male));
+ /// assert!(!TwoGenders.is_valid(Genderless));
+ /// ```
+ pub fn is_valid(&self, gender: Gender) -> bool {
+ match self {
+ GenderOption::NoGender => gender == Gender::Genderless,
+ GenderOption::TwoGenders => gender == Gender::Female || gender == Gender::Male,
+ }
+ }
+}
diff --git a/rpg_tools_core/src/model/race/manager.rs b/rpg_tools_core/src/model/race/manager.rs
new file mode 100644
index 00000000..89e0893e
--- /dev/null
+++ b/rpg_tools_core/src/model/race/manager.rs
@@ -0,0 +1,31 @@
+use crate::model::race::{Race, RaceId};
+
+/// Manages & stores the [`race`](Race).
+#[derive(Default, Debug)]
+pub struct RaceMgr {
+ races: Vec,
+}
+
+impl RaceMgr {
+ pub fn new(races: Vec) -> Self {
+ Self { races }
+ }
+
+ pub fn create(&mut self) -> RaceId {
+ let id = RaceId::new(self.races.len());
+ self.races.push(Race::new(id));
+ id
+ }
+
+ pub fn get_all(&self) -> &Vec {
+ &self.races
+ }
+
+ pub fn get(&self, id: RaceId) -> Option<&Race> {
+ self.races.get(id.0)
+ }
+
+ pub fn get_mut(&mut self, id: RaceId) -> Option<&mut Race> {
+ self.races.get_mut(id.0)
+ }
+}
diff --git a/rpg_tools_core/src/model/race/mod.rs b/rpg_tools_core/src/model/race/mod.rs
new file mode 100644
index 00000000..aa047cad
--- /dev/null
+++ b/rpg_tools_core/src/model/race/mod.rs
@@ -0,0 +1,57 @@
+use crate::model::race::gender::GenderOption;
+use serde::{Deserialize, Serialize};
+
+pub mod gender;
+pub mod manager;
+
+/// The unique identifier of a [`race`](Race).
+#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
+pub struct RaceId(usize);
+
+impl RaceId {
+ pub fn new(id: usize) -> Self {
+ Self(id)
+ }
+
+ pub fn id(&self) -> usize {
+ self.0
+ }
+}
+
+/// A race like human, elf or dragon.
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct Race {
+ id: RaceId,
+ name: String,
+ gender_option: GenderOption,
+}
+
+impl Race {
+ pub fn new(id: RaceId) -> Self {
+ Race {
+ id,
+ name: format!("Race {}", id.0),
+ gender_option: GenderOption::TwoGenders,
+ }
+ }
+
+ pub fn id(&self) -> &RaceId {
+ &self.id
+ }
+
+ pub fn name(&self) -> &str {
+ &self.name
+ }
+
+ pub fn set_name(&mut self, name: String) {
+ self.name = name;
+ }
+
+ pub fn gender_option(&self) -> GenderOption {
+ self.gender_option
+ }
+
+ pub fn set_gender_option(&mut self, gender_option: GenderOption) {
+ self.gender_option = gender_option;
+ }
+}
diff --git a/rpg_tools_core/src/usecase/edit/character.rs b/rpg_tools_core/src/usecase/edit/character.rs
new file mode 100644
index 00000000..4ea5f245
--- /dev/null
+++ b/rpg_tools_core/src/usecase/edit/character.rs
@@ -0,0 +1,245 @@
+use crate::model::character::gender::Gender;
+use crate::model::character::CharacterId;
+use crate::model::RpgData;
+use anyhow::{bail, Context, Result};
+
+/// Tries to update the name of a [`character`](crate::model::character::Character).
+pub fn update_character_name(data: &mut RpgData, id: CharacterId, name: &str) -> Result<()> {
+ let trimmed = name.trim().to_string();
+
+ if trimmed.is_empty() {
+ bail!("Name is empty!")
+ } else if data
+ .character_manager
+ .get_all()
+ .iter()
+ .filter(|r| r.id().ne(&id))
+ .any(|r| r.name().eq(&trimmed))
+ {
+ bail!("Name '{}' already exists!", trimmed)
+ }
+
+ data.character_manager
+ .get_mut(id)
+ .map(|r| r.set_name(trimmed))
+ .context("Character doesn't exist!")?;
+
+ Ok(())
+}
+
+/// Tries to update the [`gender`](Gender) of a [`character`](crate::model::character::Character).
+pub fn update_character_gender(data: &mut RpgData, id: CharacterId, gender: Gender) -> Result<()> {
+ let race_id = data
+ .character_manager
+ .get(id)
+ .map(|c| c.race())
+ .context("Character doesn't exist!")?;
+ let option = data
+ .race_manager
+ .get(race_id)
+ .map(|r| r.gender_option())
+ .context("Character's race doesn't exist!")?;
+
+ if !option.is_valid(gender) {
+ bail!("Gender is not valid for the race's gender option!");
+ }
+
+ if let Some(c) = data.character_manager.get_mut(id) {
+ c.set_gender(gender)
+ }
+
+ Ok(())
+}
+
+/// Tries to update the [`race`](crate::model::race::Race) of a [`character`](crate::model::character::Character).
+pub fn update_character_race(data: &mut RpgData, id: CharacterId, race_name: &str) -> Result<()> {
+ let race = data
+ .race_manager
+ .get_all()
+ .iter()
+ .find(|race| race.name().eq(race_name))
+ .context("Race doesn't exist!")?;
+ let gender = data
+ .character_manager
+ .get(id)
+ .map(|c| c.gender())
+ .context("Character doesn't exist!")?;
+
+ if !race.gender_option().is_valid(gender) {
+ bail!("Race's gender option conflicts with the gender!")
+ }
+
+ let race_id = *race.id();
+
+ if let Some(r) = data.character_manager.get_mut(id) {
+ r.set_race(race_id)
+ }
+
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::model::character::gender::Gender::Female;
+ use Gender::{Genderless, Male};
+
+ // update_character_name()
+
+ #[test]
+ fn test_empty_name() {
+ let mut data = RpgData::default();
+
+ assert!(update_character_name(&mut data, CharacterId::new(0), "").is_err());
+ }
+
+ #[test]
+ fn test_name_contains_only_whitespaces() {
+ let mut data = RpgData::default();
+
+ assert!(update_character_name(&mut data, CharacterId::new(0), " ").is_err());
+ }
+
+ #[test]
+ fn test_update_name_of_non_existing_race() {
+ let mut data = RpgData::default();
+
+ assert!(update_character_name(&mut data, CharacterId::new(0), "Test").is_err());
+ }
+
+ #[test]
+ fn test_update_valid_name() {
+ test_update_name("Test", "Test");
+ }
+
+ #[test]
+ fn test_update_trimmed_name() {
+ test_update_name(" Name ", "Name");
+ }
+
+ fn test_update_name(input: &str, result: &str) {
+ let mut data = RpgData::default();
+ let id = data.character_manager.create();
+
+ assert!(update_character_name(&mut data, id, input).is_ok());
+
+ assert_eq!(
+ result,
+ data.character_manager.get(id).map(|r| r.name()).unwrap()
+ );
+ }
+
+ #[test]
+ fn test_update_duplicate_name() {
+ let mut data = RpgData::default();
+ let id0 = data.character_manager.create();
+ let id1 = data.character_manager.create();
+
+ assert!(update_character_name(&mut data, id0, "Test").is_ok());
+ assert!(update_character_name(&mut data, id1, "Test").is_err());
+ }
+
+ // update_character_gender()
+
+ #[test]
+ fn test_update_gender_of_non_existing_character() {
+ let mut data = RpgData::default();
+
+ assert!(update_character_gender(&mut data, CharacterId::new(0), Male).is_err());
+ }
+
+ #[test]
+ fn test_update_gender_with_non_existing_race() {
+ let mut data = RpgData::default();
+ let character_id = data.character_manager.create();
+
+ assert!(update_character_gender(&mut data, character_id, Male).is_err());
+ }
+
+ #[test]
+ fn test_update_genders() {
+ test_update_gender(Male);
+ test_update_gender(Female);
+ }
+
+ fn test_update_gender(gender: Gender) {
+ let mut data = RpgData::default();
+ data.race_manager.create();
+ let character_id = data.character_manager.create();
+
+ assert!(update_character_gender(&mut data, character_id, gender).is_ok());
+
+ assert_eq!(
+ gender,
+ data.character_manager
+ .get(character_id)
+ .map(|r| r.gender())
+ .unwrap()
+ );
+ }
+
+ #[test]
+ fn test_update_invalid_genders() {
+ let mut data = RpgData::default();
+ data.race_manager.create();
+ let character_id = data.character_manager.create();
+
+ assert!(update_character_gender(&mut data, character_id, Genderless).is_err());
+ }
+
+ // update_character_gender()
+
+ #[test]
+ fn test_update_race_with_non_existing_race() {
+ let mut data = RpgData::default();
+ let character_id = data.character_manager.create();
+
+ assert!(update_character_race(&mut data, character_id, "Test").is_err());
+ }
+
+ #[test]
+ fn test_update_race_of_non_existing_character() {
+ let mut data = RpgData::default();
+ let race_id = data.race_manager.create();
+ data.race_manager
+ .get_mut(race_id)
+ .map(|r| r.set_name("Test".to_string()));
+
+ assert!(update_character_race(&mut data, CharacterId::new(0), "Test").is_err());
+ }
+
+ #[test]
+ fn test_update_race() {
+ let mut data = RpgData::default();
+ let character_id = data.character_manager.create();
+ let race_id = data.race_manager.create();
+ data.race_manager
+ .get_mut(race_id)
+ .map(|r| r.set_name("Test".to_string()));
+
+ assert!(update_character_race(&mut data, character_id, "Test").is_ok());
+
+ assert_eq!(
+ race_id,
+ data.character_manager
+ .get(character_id)
+ .map(|r| r.race())
+ .unwrap()
+ );
+ }
+
+ #[test]
+ fn test_update_race_with_invalid_gender() {
+ let mut data = RpgData::default();
+ let character_id = data.character_manager.create();
+ data.character_manager
+ .get_mut(character_id)
+ .map(|c| c.set_gender(Genderless));
+ let race_id = data.race_manager.create();
+ data.race_manager
+ .get_mut(race_id)
+ .map(|r| r.set_name("Test".to_string()));
+
+ assert!(update_character_race(&mut data, character_id, "Test").is_err());
+ }
+}
diff --git a/rpg_tools_core/src/usecase/edit/mod.rs b/rpg_tools_core/src/usecase/edit/mod.rs
new file mode 100644
index 00000000..00ed6aa1
--- /dev/null
+++ b/rpg_tools_core/src/usecase/edit/mod.rs
@@ -0,0 +1,2 @@
+pub mod character;
+pub mod race;
diff --git a/rpg_tools_core/src/usecase/edit/race.rs b/rpg_tools_core/src/usecase/edit/race.rs
new file mode 100644
index 00000000..35bd4e3b
--- /dev/null
+++ b/rpg_tools_core/src/usecase/edit/race.rs
@@ -0,0 +1,152 @@
+use crate::model::race::gender::GenderOption;
+use crate::model::race::RaceId;
+use crate::model::RpgData;
+use anyhow::{bail, Context, Result};
+
+/// Tries to update the name of a [`race`](crate::model::race::Race).
+pub fn update_race_name(data: &mut RpgData, id: RaceId, name: &str) -> Result<()> {
+ let trimmed = name.trim().to_string();
+
+ if trimmed.is_empty() {
+ bail!("Name is empty!")
+ } else if data
+ .race_manager
+ .get_all()
+ .iter()
+ .filter(|r| r.id().ne(&id))
+ .any(|r| r.name().eq(&trimmed))
+ {
+ bail!("Name '{}' already exists!", trimmed)
+ }
+
+ data.race_manager
+ .get_mut(id)
+ .map(|r| r.set_name(trimmed))
+ .context("Race doesn't exist")?;
+
+ Ok(())
+}
+
+/// Tries to update the gender option of a [`race`](crate::model::race::Race).
+pub fn update_gender_option(data: &mut RpgData, id: RaceId, option: GenderOption) -> Result<()> {
+ if data
+ .race_manager
+ .get(id)
+ .map(|r| r.gender_option() == option)
+ .context("Race doesn't exist")?
+ {
+ return Ok(());
+ }
+
+ if !data.character_manager.get_all().is_empty() {
+ bail!("Cannot change, because the race is used by characters!")
+ }
+
+ if let Some(r) = data.race_manager.get_mut(id) {
+ r.set_gender_option(option)
+ }
+
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use GenderOption::{NoGender, TwoGenders};
+
+ #[test]
+ fn test_empty_name() {
+ let mut data = RpgData::default();
+
+ assert!(update_race_name(&mut data, RaceId::new(0), "").is_err());
+ }
+
+ #[test]
+ fn test_name_contains_only_whitespaces() {
+ let mut data = RpgData::default();
+
+ assert!(update_race_name(&mut data, RaceId::new(0), " ").is_err());
+ }
+
+ #[test]
+ fn test_update_name_of_non_existing_race() {
+ let mut data = RpgData::default();
+
+ assert!(update_race_name(&mut data, RaceId::new(0), "Test").is_err());
+ }
+
+ #[test]
+ fn test_update_valid_name() {
+ test_update_name("Test", "Test");
+ }
+
+ #[test]
+ fn test_update_trimmed_name() {
+ test_update_name(" Name ", "Name");
+ }
+
+ fn test_update_name(input: &str, result: &str) {
+ let mut data = RpgData::default();
+ let id = data.race_manager.create();
+
+ assert!(update_race_name(&mut data, id, input).is_ok());
+
+ assert_eq!(result, data.race_manager.get(id).map(|r| r.name()).unwrap());
+ }
+
+ #[test]
+ fn test_update_duplicate_name() {
+ let mut data = RpgData::default();
+ let id0 = data.race_manager.create();
+ let id1 = data.race_manager.create();
+
+ assert!(update_race_name(&mut data, id0, "Test").is_ok());
+ assert!(update_race_name(&mut data, id1, "Test").is_err());
+ }
+
+ #[test]
+ fn test_update_gender_of_non_existing_race() {
+ let mut data = RpgData::default();
+
+ assert!(update_gender_option(&mut data, RaceId::new(0), TwoGenders).is_err());
+ }
+
+ #[test]
+ fn test_update_gender_options() {
+ test_update_gender_option(NoGender);
+ test_update_gender_option(TwoGenders);
+ }
+
+ fn test_update_gender_option(option: GenderOption) {
+ let mut data = RpgData::default();
+ let id = data.race_manager.create();
+
+ assert!(update_gender_option(&mut data, id, option).is_ok());
+
+ assert_eq!(
+ option,
+ data.race_manager
+ .get(id)
+ .map(|r| r.gender_option())
+ .unwrap()
+ );
+ }
+
+ #[test]
+ fn test_update_gender_options_while_used_by_characters() {
+ let mut data = RpgData::default();
+ let id = data.race_manager.create();
+ data.character_manager.create();
+
+ assert!(update_gender_option(&mut data, id, NoGender).is_err());
+ }
+
+ #[test]
+ fn test_update_gender_options_ignore_characters_if_unchanged() {
+ let mut data = RpgData::default();
+ let id = data.race_manager.create();
+ data.character_manager.create();
+
+ assert!(update_gender_option(&mut data, id, TwoGenders).is_ok());
+ }
+}
diff --git a/rpg_tools_core/src/usecase/mod.rs b/rpg_tools_core/src/usecase/mod.rs
new file mode 100644
index 00000000..99946a87
--- /dev/null
+++ b/rpg_tools_core/src/usecase/mod.rs
@@ -0,0 +1 @@
+pub mod edit;
diff --git a/rpg_tools_editor/Cargo.toml b/rpg_tools_editor/Cargo.toml
index a9daf3ed..6c6ff945 100644
--- a/rpg_tools_editor/Cargo.toml
+++ b/rpg_tools_editor/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "rpg_tools_editor"
-version = "0.3.0"
+version = "0.4.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
diff --git a/rpg_tools_editor/src/appearance.rs b/rpg_tools_editor/src/appearance.rs
deleted file mode 100644
index 34446e92..00000000
--- a/rpg_tools_editor/src/appearance.rs
+++ /dev/null
@@ -1,40 +0,0 @@
-use crate::parser::UrlParser;
-use rpg_tools_core::model::character::appearance::Appearance;
-use rpg_tools_rendering::math::aabb2d::AABB;
-use rpg_tools_rendering::renderer::svg::SvgBuilder;
-use rpg_tools_rendering::renderer::Renderer;
-use rpg_tools_rendering::rendering::character::{
- calculate_character_size, render_character_from_back, render_character_from_front,
-};
-use rpg_tools_rendering::rendering::config::example::create_border_options;
-use rpg_tools_rendering::rendering::config::RenderConfig;
-use url_encoded_data::UrlEncodedData;
-
-pub fn apply_update_to_appearance(update: &str) -> Appearance {
- let data = UrlEncodedData::parse_str(update);
- let parser = UrlParser::new(data);
-
- Appearance::parse(&parser, "appearance", "")
-}
-
-#[derive(Responder)]
-#[response(status = 200, content_type = "image/svg+xml")]
-pub struct RawSvg(String);
-
-pub fn render_to_svg(config: &RenderConfig, appearance: &Appearance, front: bool) -> RawSvg {
- let size = calculate_character_size(config, appearance);
- let aabb = AABB::with_size(size);
- let options = create_border_options();
- let mut svg_builder = SvgBuilder::new(size);
-
- svg_builder.render_rectangle(&aabb, &options);
-
- if front {
- render_character_from_front(&mut svg_builder, config, &aabb, appearance);
- } else {
- render_character_from_back(&mut svg_builder, config, &aabb, appearance);
- }
-
- let svg = svg_builder.finish();
- RawSvg(svg.export())
-}
diff --git a/rpg_tools_editor/src/main.rs b/rpg_tools_editor/src/main.rs
index 0ff25207..a923b1bf 100644
--- a/rpg_tools_editor/src/main.rs
+++ b/rpg_tools_editor/src/main.rs
@@ -2,30 +2,40 @@ extern crate macro_core;
#[macro_use]
extern crate rocket;
-use crate::appearance::{apply_update_to_appearance, render_to_svg, RawSvg};
-use crate::io::{read, write};
+use crate::io::read;
+use crate::route::appearance::{
+ edit_appearance, get_appearance_back, get_appearance_front, get_preview_back,
+ get_preview_front, update_appearance, update_appearance_preview,
+};
+use crate::route::character::{
+ add_character, edit_character, get_all_characters, get_character_details, update_character,
+ CHARACTER_FILE,
+};
+use crate::route::race::{
+ add_race, edit_race, get_all_races, get_race_details, update_race, RACES_FILE,
+};
use anyhow::Result;
-use rocket::form::Form;
use rocket::fs::FileServer;
use rocket::State;
use rocket_dyn_templates::{context, Template};
use rpg_tools_core::model::character::appearance::Appearance;
use rpg_tools_core::model::character::manager::CharacterMgr;
-use rpg_tools_core::model::character::{Character, CharacterId};
+use rpg_tools_core::model::character::Character;
+use rpg_tools_core::model::race::manager::RaceMgr;
+use rpg_tools_core::model::race::Race;
+use rpg_tools_core::model::RpgData;
use rpg_tools_rendering::rendering::config::example::create_config;
use rpg_tools_rendering::rendering::config::RenderConfig;
use std::path::Path;
use std::sync::Mutex;
-pub mod appearance;
pub mod io;
pub mod parser;
+pub mod route;
-const FILE: &str = "resources/characters/characters.yaml";
-
-struct EditorData {
+pub struct EditorData {
config: RenderConfig,
- data: Mutex,
+ data: Mutex,
preview: Mutex,
}
@@ -35,189 +45,8 @@ fn home(data: &State) -> Template {
Template::render(
"home",
context! {
- characters: data.get_all().len(),
- },
- )
-}
-
-#[get("/character")]
-fn get_characters(data: &State) -> Template {
- let data = data.data.lock().expect("lock shared data");
- let characters: Vec<(usize, &str)> = data
- .get_all()
- .iter()
- .map(|c| (c.id().id(), c.name()))
- .collect();
- let total = characters.len();
-
- Template::render(
- "characters",
- context! {
- characters: characters,
- total: total,
- },
- )
-}
-
-#[get("/character/new")]
-fn add_character(data: &State) -> Option {
- let mut data = data.data.lock().expect("lock shared data");
-
- let id = data.create();
-
- println!("Create character {}", id.id());
-
- data.get_mut(id)
- .map(|character| edit_character_template(id.id(), character))
-}
-
-#[get("/character/")]
-fn get_character(data: &State, id: usize) -> Option {
- let data = data.data.lock().expect("lock shared data");
- data.get(CharacterId::new(id))
- .map(|character| show_character_template(id, character))
-}
-
-#[get("/character//edit")]
-fn edit_character(data: &State, id: usize) -> Option {
- let data = data.data.lock().expect("lock shared data");
- data.get(CharacterId::new(id))
- .map(|character| edit_character_template(id, character))
-}
-
-#[derive(FromForm, Debug)]
-struct CharacterUpdate<'r> {
- name: &'r str,
- gender: &'r str,
-}
-
-#[post("/character//update", data = "")]
-fn update_character(
- data: &State,
- id: usize,
- update: Form>,
-) -> Option {
- let mut data = data.data.lock().expect("lock shared data");
-
- println!("Update character {} with {:?}", id, update);
-
- data.get_mut(CharacterId::new(id))
- .map(|character| {
- character.set_name(update.name.trim().to_string());
- character.set_gender(update.gender.into());
- character
- })
- .map(|character| show_character_template(id, character))
-}
-
-#[get("/character//front.svg")]
-fn get_front(state: &State, id: usize) -> Option {
- let data = state.data.lock().expect("lock shared data");
- data.get(CharacterId::new(id))
- .map(|character| render_to_svg(&state.config, character.appearance(), true))
-}
-
-#[get("/character//back.svg")]
-fn get_back(state: &State, id: usize) -> Option {
- let data = state.data.lock().expect("lock shared data");
- data.get(CharacterId::new(id))
- .map(|character| render_to_svg(&state.config, character.appearance(), false))
-}
-
-#[get("/appearance/preview/front.svg")]
-fn get_preview_front(state: &State) -> RawSvg {
- let preview = state.preview.lock().expect("lock shared preview");
- render_to_svg(&state.config, &preview, true)
-}
-
-#[get("/appearance/preview/back.svg")]
-fn get_preview_back(state: &State) -> RawSvg {
- let preview = state.preview.lock().expect("lock shared preview");
- render_to_svg(&state.config, &preview, false)
-}
-
-#[get("/appearance//edit")]
-fn edit_appearance(state: &State, id: usize) -> Option {
- let data = state.data.lock().expect("lock shared data");
- let mut preview = state.preview.lock().expect("lock shared preview");
-
- data.get(CharacterId::new(id)).map(|character| {
- *preview = *character.appearance();
-
- edit_appearance_template(character, &preview)
- })
-}
-
-#[post("/appearance//update", data = "")]
-fn update_appearance(data: &State, id: usize, update: String) -> Option {
- let mut data = data.data.lock().expect("lock shared data");
-
- println!("Update appearance of character {} with {:?}", id, update);
-
- let result = data
- .get_mut(CharacterId::new(id))
- .map(|character| {
- character.set_appearance(apply_update_to_appearance(&update));
- character
- })
- .map(|character| show_character_template(id, character));
-
- if let Err(e) = write(data.get_all(), Path::new(FILE)) {
- println!("Failed to save the characters: {}", e);
- }
-
- result
-}
-
-#[post("/appearance//preview", data = "")]
-fn update_appearance_preview(
- state: &State,
- id: usize,
- update: String,
-) -> Option {
- let mut data = state.data.lock().expect("lock shared data");
- let mut preview = state.preview.lock().expect("lock shared preview");
-
- println!("Preview appearance of character {} with {:?}", id, update);
-
- data.get_mut(CharacterId::new(id)).map(|character| {
- *preview = apply_update_to_appearance(&update);
-
- edit_appearance_template(character, &preview)
- })
-}
-
-fn show_character_template(id: usize, character: &Character) -> Template {
- Template::render(
- "character",
- context! {
- id: id,
- name: character.name(),
- gender: character.gender(),
- appearance: character.appearance(),
- },
- )
-}
-
-fn edit_character_template(id: usize, character: &Character) -> Template {
- Template::render(
- "character_edit",
- context! {
- id: id,
- name: character.name(),
- genders: vec!["Female", "Genderless", "Male"],
- gender: character.gender(),
- },
- )
-}
-
-fn edit_appearance_template(character: &Character, appearance: &Appearance) -> Template {
- Template::render(
- "appearance_edit",
- context! {
- id: character.id().id(),
- name: character.name(),
- appearance: appearance,
+ races: data.race_manager.get_all().len(),
+ characters: data.character_manager.get_all().len(),
},
)
}
@@ -235,9 +64,9 @@ async fn main() -> Result<()> {
"/",
routes![
home,
- get_characters,
+ get_all_characters,
add_character,
- get_character,
+ get_character_details,
edit_character,
update_character,
edit_appearance,
@@ -245,8 +74,13 @@ async fn main() -> Result<()> {
get_preview_front,
get_preview_back,
update_appearance_preview,
- get_front,
- get_back,
+ get_appearance_front,
+ get_appearance_back,
+ get_all_races,
+ get_race_details,
+ add_race,
+ edit_race,
+ update_race,
],
)
.attach(Template::fairing())
@@ -259,10 +93,23 @@ async fn main() -> Result<()> {
Ok(())
}
-fn init() -> CharacterMgr {
- let characters: Result> = read(Path::new(FILE));
+fn init() -> RpgData {
+ let races: Result> = read(Path::new(RACES_FILE));
- match characters {
+ let race_manager = match races {
+ Ok(races) => {
+ println!("Loaded {} races.", races.len());
+ RaceMgr::new(races)
+ }
+ Err(e) => {
+ println!("Failed to load the races: {}", e);
+ return RpgData::default();
+ }
+ };
+
+ let characters: Result> = read(Path::new(CHARACTER_FILE));
+
+ let character_manager = match characters {
Ok(characters) => {
println!("Loaded {} characters.", characters.len());
CharacterMgr::new(characters)
@@ -271,5 +118,10 @@ fn init() -> CharacterMgr {
println!("Failed to load the characters: {}", e);
CharacterMgr::default()
}
+ };
+
+ RpgData {
+ character_manager,
+ race_manager,
}
}
diff --git a/rpg_tools_editor/src/route/appearance.rs b/rpg_tools_editor/src/route/appearance.rs
new file mode 100644
index 00000000..b7645a17
--- /dev/null
+++ b/rpg_tools_editor/src/route/appearance.rs
@@ -0,0 +1,134 @@
+use crate::parser::UrlParser;
+use crate::route::character::save_and_show_character;
+use crate::EditorData;
+use rocket::State;
+use rocket_dyn_templates::{context, Template};
+use rpg_tools_core::model::character::appearance::Appearance;
+use rpg_tools_core::model::character::{Character, CharacterId};
+use rpg_tools_rendering::math::aabb2d::AABB;
+use rpg_tools_rendering::renderer::svg::SvgBuilder;
+use rpg_tools_rendering::renderer::Renderer;
+use rpg_tools_rendering::rendering::character::{
+ calculate_character_size, render_character_from_back, render_character_from_front,
+};
+use rpg_tools_rendering::rendering::config::example::create_border_options;
+use rpg_tools_rendering::rendering::config::RenderConfig;
+use url_encoded_data::UrlEncodedData;
+
+#[get("/appearance/render//front.svg")]
+pub fn get_appearance_front(state: &State, id: usize) -> Option {
+ let data = state.data.lock().expect("lock shared data");
+ data.character_manager
+ .get(CharacterId::new(id))
+ .map(|character| render_to_svg(&state.config, character.appearance(), true))
+}
+
+#[get("/appearance/render//back.svg")]
+pub fn get_appearance_back(state: &State, id: usize) -> Option {
+ let data = state.data.lock().expect("lock shared data");
+ data.character_manager
+ .get(CharacterId::new(id))
+ .map(|character| render_to_svg(&state.config, character.appearance(), false))
+}
+
+#[get("/appearance/preview/front.svg")]
+pub fn get_preview_front(state: &State) -> RawSvg {
+ let preview = state.preview.lock().expect("lock shared preview");
+ render_to_svg(&state.config, &preview, true)
+}
+
+#[get("/appearance/preview/back.svg")]
+pub fn get_preview_back(state: &State) -> RawSvg {
+ let preview = state.preview.lock().expect("lock shared preview");
+ render_to_svg(&state.config, &preview, false)
+}
+
+#[get("/appearance/edit/")]
+pub fn edit_appearance(state: &State, id: usize) -> Option {
+ let data = state.data.lock().expect("lock shared data");
+ let mut preview = state.preview.lock().expect("lock shared preview");
+
+ data.character_manager
+ .get(CharacterId::new(id))
+ .map(|character| {
+ *preview = *character.appearance();
+
+ edit_appearance_template(character, &preview)
+ })
+}
+
+#[post("/appearance/update/", data = "")]
+pub fn update_appearance(data: &State, id: usize, update: String) -> Option {
+ let mut data = data.data.lock().expect("lock shared data");
+
+ println!("Update appearance of character {} with {:?}", id, update);
+
+ data.character_manager
+ .get_mut(CharacterId::new(id))
+ .map(|character| {
+ character.set_appearance(apply_update_to_appearance(&update));
+ character
+ });
+
+ save_and_show_character(&data, id)
+}
+
+#[post("/appearance/preview/", data = "")]
+pub fn update_appearance_preview(
+ state: &State,
+ id: usize,
+ update: String,
+) -> Option {
+ let data = state.data.lock().expect("lock shared data");
+ let mut preview = state.preview.lock().expect("lock shared preview");
+
+ println!("Preview appearance of character {} with {:?}", id, update);
+
+ data.character_manager
+ .get(CharacterId::new(id))
+ .map(|character| {
+ *preview = apply_update_to_appearance(&update);
+
+ edit_appearance_template(character, &preview)
+ })
+}
+
+fn edit_appearance_template(character: &Character, appearance: &Appearance) -> Template {
+ Template::render(
+ "appearance_edit",
+ context! {
+ id: character.id().id(),
+ name: character.name(),
+ appearance: appearance,
+ },
+ )
+}
+
+pub fn apply_update_to_appearance(update: &str) -> Appearance {
+ let data = UrlEncodedData::parse_str(update);
+ let parser = UrlParser::new(data);
+
+ Appearance::parse(&parser, "appearance", "")
+}
+
+#[derive(Responder)]
+#[response(status = 200, content_type = "image/svg+xml")]
+pub struct RawSvg(String);
+
+pub fn render_to_svg(config: &RenderConfig, appearance: &Appearance, front: bool) -> RawSvg {
+ let size = calculate_character_size(config, appearance);
+ let aabb = AABB::with_size(size);
+ let options = create_border_options();
+ let mut svg_builder = SvgBuilder::new(size);
+
+ svg_builder.render_rectangle(&aabb, &options);
+
+ if front {
+ render_character_from_front(&mut svg_builder, config, &aabb, appearance);
+ } else {
+ render_character_from_back(&mut svg_builder, config, &aabb, appearance);
+ }
+
+ let svg = svg_builder.finish();
+ RawSvg(svg.export())
+}
diff --git a/rpg_tools_editor/src/route/character.rs b/rpg_tools_editor/src/route/character.rs
new file mode 100644
index 00000000..a17dda3c
--- /dev/null
+++ b/rpg_tools_editor/src/route/character.rs
@@ -0,0 +1,191 @@
+use crate::io::write;
+use crate::EditorData;
+use rocket::form::Form;
+use rocket::State;
+use rocket_dyn_templates::{context, Template};
+use rpg_tools_core::model::character::gender::Gender;
+use rpg_tools_core::model::character::{Character, CharacterId};
+use rpg_tools_core::model::RpgData;
+use rpg_tools_core::usecase::edit::character::{
+ update_character_gender, update_character_name, update_character_race,
+};
+use std::path::Path;
+
+pub const CHARACTER_FILE: &str = "resources/characters/characters.yaml";
+
+#[get("/character/all")]
+pub fn get_all_characters(data: &State) -> Template {
+ let data = data.data.lock().expect("lock shared data");
+ let characters: Vec<(usize, &str)> = data
+ .character_manager
+ .get_all()
+ .iter()
+ .map(|c| (c.id().id(), c.name()))
+ .collect();
+ let total = characters.len();
+
+ Template::render(
+ "character/all",
+ context! {
+ characters: characters,
+ total: total,
+ },
+ )
+}
+
+#[get("/character/details/")]
+pub fn get_character_details(data: &State, id: usize) -> Option {
+ let data = data.data.lock().expect("lock shared data");
+ data.character_manager
+ .get(CharacterId::new(id))
+ .map(|character| get_details_template(&data, id, character))
+}
+
+#[get("/character/edit/")]
+pub fn edit_character(data: &State, id: usize) -> Option {
+ let data = data.data.lock().expect("lock shared data");
+ data.character_manager
+ .get(CharacterId::new(id))
+ .map(|character| get_edit_template(&data, id, character, "", "", ""))
+}
+
+#[get("/character/new")]
+pub fn add_character(data: &State) -> Option {
+ let mut data = data.data.lock().expect("lock shared data");
+
+ let id = data.character_manager.create();
+
+ println!("Create character {}", id.id());
+
+ data.character_manager
+ .get(id)
+ .map(|character| get_edit_template(&data, id.id(), character, "", "", ""))
+}
+
+#[derive(FromForm, Debug)]
+pub struct CharacterUpdate<'r> {
+ name: &'r str,
+ race: &'r str,
+ gender: &'r str,
+}
+
+#[post("/character/update/", data = "")]
+pub fn update_character(
+ data: &State,
+ id: usize,
+ update: Form>,
+) -> Option {
+ let mut data = data.data.lock().expect("lock shared data");
+
+ println!("Update character {} with {:?}", id, update);
+
+ let character_id = CharacterId::new(id);
+
+ if let Err(e) = update_character_name(&mut data, character_id, update.name) {
+ return data
+ .character_manager
+ .get(character_id)
+ .map(|character| get_edit_template(&data, id, character, &e.to_string(), "", ""));
+ }
+
+ if let Err(e) = update_character_gender(&mut data, character_id, update.gender.into()) {
+ return data
+ .character_manager
+ .get(character_id)
+ .map(|character| get_edit_template(&data, id, character, "", &e.to_string(), ""));
+ }
+
+ if let Err(e) = update_character_race(&mut data, character_id, update.race) {
+ return data
+ .character_manager
+ .get(character_id)
+ .map(|character| get_edit_template(&data, id, character, "", "", &e.to_string()));
+ }
+
+ let race = data
+ .race_manager
+ .get_all()
+ .iter()
+ .find(|race| race.name().eq(update.race))
+ .map(|race| *race.id());
+
+ data.character_manager
+ .get_mut(CharacterId::new(id))
+ .map(|character| {
+ if let Some(id) = race {
+ character.set_race(id);
+ }
+ character
+ });
+
+ save_and_show_character(&data, id)
+}
+
+fn get_details_template(data: &RpgData, id: usize, character: &Character) -> Template {
+ let race = data
+ .race_manager
+ .get(character.race())
+ .map(|race| race.name())
+ .unwrap_or("Unknown");
+
+ Template::render(
+ "character/details",
+ context! {
+ id: id,
+ name: character.name(),
+ race_id: character.race(),
+ race: race,
+ gender: character.gender(),
+ appearance: character.appearance(),
+ },
+ )
+}
+
+fn get_edit_template(
+ data: &RpgData,
+ id: usize,
+ character: &Character,
+ name_error: &str,
+ gender_error: &str,
+ race_error: &str,
+) -> Template {
+ let races: Vec<&str> = data
+ .race_manager
+ .get_all()
+ .iter()
+ .map(|race| race.name())
+ .collect();
+ let race = data
+ .race_manager
+ .get(character.race())
+ .map(|race| race.name())
+ .unwrap_or("Unknown");
+
+ Template::render(
+ "character/edit",
+ context! {
+ id: id,
+ name: character.name(),
+ name_error: name_error,
+ races: races,
+ race: race,
+ race_error: race_error,
+ genders: Gender::get_all(),
+ gender: character.gender(),
+ gender_error: gender_error,
+ },
+ )
+}
+
+pub fn save_and_show_character(data: &RpgData, id: usize) -> Option {
+ let result = data
+ .character_manager
+ .get(CharacterId::new(id))
+ .map(|character| get_details_template(data, id, character));
+
+ if let Err(e) = write(data.character_manager.get_all(), Path::new(CHARACTER_FILE)) {
+ println!("Failed to save the characters: {}", e);
+ }
+
+ result
+}
diff --git a/rpg_tools_editor/src/route/mod.rs b/rpg_tools_editor/src/route/mod.rs
new file mode 100644
index 00000000..c1ba5c58
--- /dev/null
+++ b/rpg_tools_editor/src/route/mod.rs
@@ -0,0 +1,3 @@
+pub mod appearance;
+pub mod character;
+pub mod race;
diff --git a/rpg_tools_editor/src/route/race.rs b/rpg_tools_editor/src/route/race.rs
new file mode 100644
index 00000000..e50e7863
--- /dev/null
+++ b/rpg_tools_editor/src/route/race.rs
@@ -0,0 +1,139 @@
+use crate::io::write;
+use crate::EditorData;
+use rocket::form::Form;
+use rocket::State;
+use rocket_dyn_templates::{context, Template};
+use rpg_tools_core::model::race::gender::GenderOption;
+use rpg_tools_core::model::race::{Race, RaceId};
+use rpg_tools_core::model::RpgData;
+use rpg_tools_core::usecase::edit::race::{update_gender_option, update_race_name};
+use std::path::Path;
+
+pub const RACES_FILE: &str = "resources/races.yaml";
+
+#[get("/race/all")]
+pub fn get_all_races(data: &State) -> Template {
+ let data = data.data.lock().expect("lock shared data");
+ let races: Vec<(usize, &str)> = data
+ .race_manager
+ .get_all()
+ .iter()
+ .map(|r| (r.id().id(), r.name()))
+ .collect();
+
+ Template::render(
+ "race/all",
+ context! {
+ total: races.len(),
+ races: races,
+ },
+ )
+}
+
+#[get("/race/details/")]
+pub fn get_race_details(data: &State, id: usize) -> Option {
+ let data = data.data.lock().expect("lock shared data");
+
+ data.race_manager
+ .get(RaceId::new(id))
+ .map(|race| get_details_template(&data, id, race))
+}
+
+#[get("/race/edit/")]
+pub fn edit_race(data: &State, id: usize) -> Option {
+ let data = data.data.lock().expect("lock shared data");
+ data.race_manager
+ .get(RaceId::new(id))
+ .map(|race| get_edit_template(id, race, "", ""))
+}
+
+#[get("/race/new")]
+pub fn add_race(data: &State) -> Option {
+ let mut data = data.data.lock().expect("lock shared data");
+
+ let id = data.race_manager.create();
+
+ println!("Create race {}", id.id());
+
+ data.race_manager
+ .get(id)
+ .map(|race| get_edit_template(id.id(), race, "", ""))
+}
+
+#[derive(FromForm, Debug)]
+pub struct RaceUpdate<'r> {
+ name: &'r str,
+ gender_option: &'r str,
+}
+
+#[post("/race/update/", data = "")]
+pub fn update_race(
+ data: &State,
+ id: usize,
+ update: Form>,
+) -> Option {
+ let mut data = data.data.lock().expect("lock shared data");
+
+ println!("Update race {} with {:?}", id, update);
+
+ let race_id = RaceId::new(id);
+
+ if let Err(e) = update_race_name(&mut data, race_id, update.name) {
+ return data
+ .race_manager
+ .get(race_id)
+ .map(|race| get_edit_template(id, race, &e.to_string(), ""));
+ }
+
+ if let Err(e) = update_gender_option(&mut data, race_id, update.gender_option.into()) {
+ return data
+ .race_manager
+ .get(race_id)
+ .map(|race| get_edit_template(id, race, "", &e.to_string()));
+ }
+
+ let result = data
+ .race_manager
+ .get(race_id)
+ .map(|race| get_details_template(&data, id, race));
+
+ if let Err(e) = write(data.race_manager.get_all(), Path::new(RACES_FILE)) {
+ println!("Failed to save the races: {}", e);
+ }
+
+ result
+}
+
+fn get_details_template(data: &RpgData, id: usize, race: &Race) -> Template {
+ let characters: Vec<(usize, &str)> = data
+ .character_manager
+ .get_all()
+ .iter()
+ .filter(|c| c.race().eq(race.id()))
+ .map(|c| (c.id().id(), c.name()))
+ .collect();
+
+ Template::render(
+ "race/details",
+ context! {
+ name: race.name(),
+ id: id,
+ gender_option: format!("{:?}", race.gender_option()),
+ characters: characters,
+ },
+ )
+}
+
+fn get_edit_template(id: usize, race: &Race, name_error: &str, gender_error: &str) -> Template {
+ Template::render(
+ "race/edit",
+ context! {
+ id: id,
+ name: race.name(),
+ name_error: name_error,
+ gender_options: GenderOption::get_all(),
+ gender_option: race.gender_option(),
+ gender_error: gender_error,
+ },
+ )
+}
diff --git a/rpg_tools_editor/static/style.css b/rpg_tools_editor/static/style.css
index 875b1f75..51d992ed 100644
--- a/rpg_tools_editor/static/style.css
+++ b/rpg_tools_editor/static/style.css
@@ -29,6 +29,10 @@ h1 {
padding-left: 20px;
}
+.error {
+ color: red;
+}
+
.editor {
font-size: 20px;
padding-left: 20px;
diff --git a/rpg_tools_rendering/Cargo.toml b/rpg_tools_rendering/Cargo.toml
index b181e9e0..052148d4 100644
--- a/rpg_tools_rendering/Cargo.toml
+++ b/rpg_tools_rendering/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "rpg_tools_rendering"
-version = "0.3.0"
+version = "0.4.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
diff --git a/rpg_tools_ui/Cargo.toml b/rpg_tools_ui/Cargo.toml
new file mode 100644
index 00000000..94ad774a
--- /dev/null
+++ b/rpg_tools_ui/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "rpg_tools_ui"
+version = "0.4.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+macro_core = { path = "../macro_core" }
+macro_convert = { path = "../macro_convert" }
+macro_ui = { path = "../macro_ui" }
+rpg_tools_core = { path = "../rpg_tools_core" }
+serde = { version = "1.0", features = ["derive"] }
+titlecase = "2.2"
\ No newline at end of file
diff --git a/rpg_tools_core/examples/editor_macro.rs b/rpg_tools_ui/src/bin/create_editor.rs
similarity index 84%
rename from rpg_tools_core/examples/editor_macro.rs
rename to rpg_tools_ui/src/bin/create_editor.rs
index 541c2c03..e6361972 100644
--- a/rpg_tools_core/examples/editor_macro.rs
+++ b/rpg_tools_ui/src/bin/create_editor.rs
@@ -1,12 +1,10 @@
extern crate macro_ui;
extern crate rpg_tools_core;
-pub mod utils;
-
-use crate::utils::write_each;
use macro_core::visitor::UI;
use rpg_tools_core::model::character::appearance::Appearance;
-use rpg_tools_core::ui::editor::EditorVisitor;
+use rpg_tools_ui::io::write_each;
+use rpg_tools_ui::ui::editor::EditorVisitor;
fn main() {
println!("Generate tera code for editor");
diff --git a/rpg_tools_core/examples/viewer_macro.rs b/rpg_tools_ui/src/bin/create_viewer.rs
similarity index 84%
rename from rpg_tools_core/examples/viewer_macro.rs
rename to rpg_tools_ui/src/bin/create_viewer.rs
index e6ecdf66..3123abc6 100644
--- a/rpg_tools_core/examples/viewer_macro.rs
+++ b/rpg_tools_ui/src/bin/create_viewer.rs
@@ -1,12 +1,10 @@
extern crate macro_ui;
extern crate rpg_tools_core;
-pub mod utils;
-
-use crate::utils::write_each;
use macro_core::visitor::UI;
use rpg_tools_core::model::character::appearance::Appearance;
-use rpg_tools_core::ui::viewer::ViewerVisitor;
+use rpg_tools_ui::io::write_each;
+use rpg_tools_ui::ui::viewer::ViewerVisitor;
fn main() {
println!("Generate tera code for viewer");
diff --git a/rpg_tools_core/examples/utils/mod.rs b/rpg_tools_ui/src/io.rs
similarity index 100%
rename from rpg_tools_core/examples/utils/mod.rs
rename to rpg_tools_ui/src/io.rs
diff --git a/rpg_tools_ui/src/lib.rs b/rpg_tools_ui/src/lib.rs
new file mode 100644
index 00000000..6b01785e
--- /dev/null
+++ b/rpg_tools_ui/src/lib.rs
@@ -0,0 +1,5 @@
+pub mod io;
+pub mod ui;
+
+extern crate macro_core;
+extern crate macro_ui;
diff --git a/rpg_tools_core/src/ui/editor.rs b/rpg_tools_ui/src/ui/editor.rs
similarity index 100%
rename from rpg_tools_core/src/ui/editor.rs
rename to rpg_tools_ui/src/ui/editor.rs
diff --git a/rpg_tools_core/src/ui/mod.rs b/rpg_tools_ui/src/ui/mod.rs
similarity index 100%
rename from rpg_tools_core/src/ui/mod.rs
rename to rpg_tools_ui/src/ui/mod.rs
diff --git a/rpg_tools_core/src/ui/viewer.rs b/rpg_tools_ui/src/ui/viewer.rs
similarity index 100%
rename from rpg_tools_core/src/ui/viewer.rs
rename to rpg_tools_ui/src/ui/viewer.rs