two-shots-later / vitruvian-vtt

A TTRP companion that is free, open-source and supported by the community.
BSD 3-Clause "New" or "Revised" License
2 stars 3 forks source link

Create Composition based Data Model Standard that can be Translated. #16

Open MrVintage710 opened 2 weeks ago

MrVintage710 commented 2 weeks ago

Proposed Feature

We need a unified data model that will allow data to efficiently transfer from our backend to frontend, and visa versa. Based on previous discussions that I have had with @Gearhartlove, we want this data to be compositional. Then like items that share the same components can be sorted into groups called archetypes. For example, you could have an archetype called Named Entities that only requires that the Name Component is present on any given entity for it to be part of the archetype.

To make this work, we need to have a standard that can be passed from one side of the app to the other. On the front end (typescript), and entity may look like this:

// These types represent components
type NameComponent = string;
type ArmorClassBonusComponent = number;
type BulkComponent = number;
type TypeComponent = "Item" | "Spell" | "ClassAbility"

// This interface is an interface that is the most general case entity for a rule set
interface PathfinderEntity {
  name? : NameComponent,
  armorClassBonus? : ArmorClassBonusComponent,
  bulk? : BulkComponent,
  type? : TypeComponent
}

// These interfaces are examples of archetypes
interface AcItem extends PathfinderEntity {
  name : NameComponent,
  armorClassBonus : ArmorClassBonusComponent,
  bulk : BulkComponent
}

interface NamedSpells extends PathfinderEntity {
  name : NameComponent,
  type : "Spell"
}

// A concrete instance of the type
const leatherArmor : AcItem = {
  name : "Leather Armor",
  armorClassBonus : 1,
  bulk : 1
}

while on the back end (rust), it may look like this:

// These traits represent components
pub trait NameComponent {
    fn name(&self) -> String;
}

pub trait ArmorClassBonusComponent {
    fn armor_class_bonus(&self) -> u32;
}

pub trait BulkComponent {
    fn bulk(&self) -> u32;
}

pub trait EntityTypeComponent {
    fn entity_type(&self) -> EntityType;
}

pub enum EntityType {
    Item,
    Spell,
    ClassAbility
}

// These traits represent archetypes
pub struct AcItem {
    name: String,
    armor_class_bonus: u32,
    bulk: u32
}

impl NameComponent for AcItem {
    fn name(&self) -> &str {
        &self.name
    }
}

impl ArmorClassBonusComponent for AcItem {
    fn armor_class_bonus(&self) -> u32 {
        self.armor_class_bonus
    }
}

impl BulkComponent for AcItem {
    fn bulk(&self) -> u32 {
        self.bulk
    }
}

pub struct NamedSpell {
    name: String
}

impl NameComponent for NamedSpell {
    fn name(&self) -> &str {
        &self.name
    }
}

impl EntityTypeComponent for NamedSpell {
    fn entity_type(&self) -> EntityType {
        EntityType::Spell
    }
}

// Concrete Type would look like this
let leather_armor = AcItem {
    name: "Leather Armor".to_string(),
    armor_class_bonus: 1,
    bulk: 1
};

The rust version is a bit more verbose and doesn't have the general Entity that typescript has. However this seems to be a good starting place.

Without looking to much into the problem, I would imagine that the best place for this type of translation would be the backend tauri side. There we can define some serde traits that can translate between the front end json and whatever database format we choose.

This issue should be somewhat related to issue #13

Acceptance Criteria

Gearhartlove commented 2 weeks ago

Hey @MrVintage710 thanks for writing this up! I like the tying of traits to a property on the internal data model. I also like how you could just create a 'tag' by implementing a marker trait.

Gearhartlove commented 2 weeks ago

I have some questions about how similar items can be identified differently. This example talks about two different types of armor, heavy_armor and light_armor.

Lets say heavy_armor has a physical_damage_reduction field. How could we add a PhysicalDamageReductionComponent to heavy_armor without modifying the leather_armor definition?

I think updating the AcItem would feel bad because leather_armor doesn't need to know anything about physical_damage_reduction.

I think this is important because we would want to handle leather_armor and physical_damage_reduction the same ways because they are both fundamentally armor, but we want to identify them as separate in a potential query to "select all armor that has a PhysicalDamageReductionComponent".

Put another way, how are these two objects different component wise? With the proposed schema how can we share a base data container yet extend different components?

Gearhartlove commented 2 weeks ago

I'm almost feeling like we should not have any base data containers like AcItem and instead rely solely on component mashups. Archetypes are just groups of components at the end of the day.

MrVintage710 commented 2 weeks ago

I see what you are saying, and I agree. However I think that Archetypes is still a concept that we need for this app. Like say I want to find all Armor Items in my inventory. How would you search for that? Is it all armor? what about items that give you AC but aren't armor? An archetype helps you define what items to accept and reject.

When you are making an entity, don't think "what archetype should this be a part of", just add the components that it needs and then naturally it will arrive at the archetype needed to describe that item.

In short, Archetypes are just minimum set of components needed for an entity to be something. Every entity will have multiple archetypes that it is apart of.

That being said, maybe Archetypes in the rust side should be defined like so:

trait AcItem {}
impl <T : NameComponent + ArmorClassBonusComponent + BulkComponent> AcItem for T {}

Lets say heavy_armor has a physical_damage_reduction field. How could we add a PhysicalDamageReductionComponent to heavy_armor without modifying the leather_armor definition?

You would have to make a new archetype for Heavy Armor. While this isn't optimal, it is a byproduct of rusts type system. I should look into more options for storing this...

Maybe we use something like bevy_ecs to store this data in ram. Then we would just have to make a translation layer for the front and backend (probably with a serde json parser)

MrVintage710 commented 2 weeks ago

Also here is something else that we have to think about: How should we do data validation? I don't think that we should have it in more than one spot. Probably put it in with the translation layer on the rust side.

Gearhartlove commented 2 weeks ago

Another thing to note is the debt accrued by managing component definitions in both TS and Rust. I wonder if defining these simple components in a language agnostic format like JSON or protobuff would work better. Both TS and Rust could then read from this shared object state.

MrVintage710 commented 2 weeks ago

Oh I love the idea of something like protobuff, however I think protobuff itself might be a bad fit for the project as it has no support for js or rust. What about Cap'n Proto?

MrVintage710 commented 2 weeks ago

Or what about Flat Buffer this one look promising as it has ts and rust supported, and they claim no parsing required!

MrVintage710 commented 2 weeks ago

Here is a good argument for flat buffers : https://www.youtube.com/watch?v=iQTxMkSJ1dQ

Gearhartlove commented 2 weeks ago

After talking yesterday with @MrVintage710, we decided it is best to use Cap'n Proto as our shared data serialization layer. We will define schemas, generate code, and then use this code in queries / rendering.

UPDATE - this issue will be where most of the discussion happens, its basically the parent to all the spun off issues. We will talk about features and create new issues from here.

MrVintage710 commented 6 days ago

The data model has been made with PR #25 and now data can be shared with PR #28.