Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Character Advancement #1353

Closed
Fyorl opened this issue Nov 25, 2021 · 35 comments
Closed

Character Advancement #1353

Fyorl opened this issue Nov 25, 2021 · 35 comments

Comments

@Fyorl
Copy link
Contributor

Fyorl commented Nov 25, 2021

Originally in GitLab by @Fyorl

Introduction

I've put together a draft of the pieces of the character advancement epic, and some of the design questions that might need to be answered as part of each. If we can agree on this list then they can be split out into individual issues that can hopefully be worked on with some degree of parallelism.

Some design decisions that were agreed on already:

  • Advancement data will be stored on the class item.
  • Advancement data will be represented as a simple array of objects.
  • Each advancement object has a type, and can be handled by its own custom handler.
  • We will be creating a DM view to allow configuration of the advancement data, as well as a player view to allow selection of those advancement options.
  • For the first pass, players will make choices at a prompt which are then finalised and written to the actor. Later versions may have this fully derived.
  • For the first pass, where a player is asked to pick from a number of items (e.g. Fighting Styles, Eldritch Invocations, Skills, etc.), these choices will be 'hard-coded' from the SRD. Later versions may have them be discoverable in some way.

Pieces

1. Advancement configuration UI. We need a place on the class item sheet where a user can see and configure the advancement objects for that class. That might be a new tab, an additional dialog, or something else. Note that the actual rendering of each type of advancement object will be handled by a specific handler for that advancement object type.

As a part of this we will need a clear way to determine whether we are in an editing context or a viewing context. This might take the form of an explicit toggle, checking for a GM user, or checking whether the item is owned or unowned.

2. Character advancement UI. We already have a level-up prompt which should be a good place to start. It needs to support going backwards and forwards through each of the advancement objects configured for that level-up. We should consider supporting jumping to an arbitrary stage as well as a simple 'Forward' and 'Back'.

It is possible that a player gains more than one level at once, or they are starting their character at a higher level, so the prompt should support collecting all the objects available for all of the levels gained, and should present them in the correct order.

As choices are made through the advancement prompt, a working copy of the actor needs to be maintained so that further steps can react to changes accordingly. This should support a player jumping back to a previous step, changing something, and potentially invalidating some later choices.

Once a player has chosen to finalise those choices, they should be written to the sheet in a permanent fashion. We will not support undoing those changes at this stage, except for the normal methods of deleting items, etc.

As with (1), this piece does not involve actually rendering any advancement objects, it just provides a framework for them to be rendered into.

3. Advancement object API. Each type of advancement 'primitive' available to a character needs to be represented by its own discrete data schema. We should consider whether we want to wrap these in their own classes. We might have an abstract base Advancement class that can be implemented by subclasses representing each of the different advancement types, for example.

However it is designed, we need a way to handle the following operations:

  • Organising advancement objects (ordering by level, selecting the appropriate ones for a given level-up, etc.)
  • Rendering a DM-configurable view of the object.
  • Rendering a player view of the object during level-up.
  • Saving the configuration to its parent item.
  • Providing some update delta to be applied to an actor that represents the choices they made.

Not all of the above operations necessarily need to be part of the API, but they do need to be handled somewhere.

4. Advancement objects. Each type of advancement primitive needs to be realised and plugged into the framework above.

Further Comments

The pieces above have a number for ease of reference but don't necessarily imply an order that they need to be completed in.

I don't believe we should tackle Spellcasting as part of this work. We may integrate it later, but I don't think we should consider it for the initial design.

After having worked through this list, it looks like it could be quite useful to do the work for subclass items first (#1080), or at least try to keep them in mind throughout the process as they will likely inform the design of quite a few areas.

@Fyorl
Copy link
Contributor Author

Fyorl commented Nov 25, 2021

Originally in GitLab by @Fyorl

Prototypes

Some example advancement primitives that were prototyped are included below:

1. Feature grant
Examples: Cunning Action, Second Wind, Draconic Resilience, Necromancy Savant, Mystic Arcarnum

// Cunning Action
{
    class: "Rogue.uuid",
    level: 2,
    feature: "CunningAction.uuid"
}

// Draconic Resilience
{
    class: "DraconicBloodline.uuid",
    level: 1,
    feature: "DraconicResilience.uuid"
}

2. Optional feature grant
Examples: Steady Aim, Magical Guidance, Quickened Healing, Harness Divine Power

// Steady Aim
{
    class: "Rogue.uuid",
    level: 3,
    feature: "SteadyAim.uuid",
    optional: true
}

3. Optional feature replacement
Examples: Nature's Veil, Primal Companion

// Nature's Veil
{
    class: "Ranger.uuid",
    level: 10,
    feature: "NaturesVeil.uuid",
    replaces: "HideInPlainSight.uuid",
    optional: true
}

// Primal Companion
{
    class: "BeastMaster.uuid",
    level: 3,
    feature: "PrimalCompanion.uuid",
    replaces: "RangersCompanion.uuid",
    optional: true
}

4. Feature upgrade
Examples: Sneak Attack, Martial Arts, Channel Divinity, Psionic Power

// Channel Divinity
{
    class: "Cleric.uuid",
    level: 6,
    feature: "ChannelDivinity.uuid",
    upgrades: [{
        key: "data.uses.max",
        value: 2
    }]
}

{
    class: "Cleric.uuid",
    level: 18,
    feature: "ChannelDivinity.uuid",
    upgrades: [{
        key: "data.uses.max",
        value: 3
    }]
}

// Psionic Power
{
    class: "PsiWarrior.uuid",
    level: 5,
    feature: "PsionicPower.uuid",
    upgrades: [{
        key: "data.formula",
        value: "1d8"
    }]
}

5. Feature pool
Examples: Arcane Shot, Metamagic, Eldritch Invocations, Fighting Style

// Fighting Style
{
    class: "Fighter.uuid",
    level: 1,
    feature: "FightingStyle.uuid",
    choices: 1,
    pool: ["Archery.uuid", "Defense.uuid", ...] // Possibly discoverable
}

// Arcane Shot
{
    class: "ArcaneArcher.uuid",
    level: 3,
    feature: "ArcaneShot.uuid",
    choices: 2,
    pool: ["BanishingArrow.uuid", "EnfeeblingArrow.uuid", ...]
}

// Eldritch Invocations
{
    class: "Warlock.uuid",
    level: 2,
    feature: "EldritchInvocations.uuid",
    choices: 2,
    pool: ["DevilsSight.uuid", "AgonizingBlast.uuid", ...]
}

6. Feature pool addition

// Fighting Style
{
    class: "Champion.uuid",
    level: 10,
    feature: "FightingStyle.uuid",
    additions: 1,
    pool: [...] // Same options as before
}

// Eldritch Invocations
{
    class: "Warlock.uuid",
    level: 5,
    feature: "EldritchInvocations.uuid",
    additions: 1,
    // Same options as before plus additional options that only become available at this level
    pool: [..., "ImprovedPactWeapon.uuid", "CloakOfFlies.uuid"]
}

// Channel Divinity
{
    class: "OathOfVengeance.uuid",
    level: 3,
    feature: "ChannelDivinity.uuid",
    additions: 2,
    // The number of additions is equal to the size of the pool to indicate that we get all of them
    pool: ["AbjureEnemy.uuid", "VowOfEmnity.uuid"]
}

7. Feature pool replacement
Examples: Combat Superiority, Eldritch Invocations, Infuse Item

// Combat Superiority
{
    class: "BattleMaster.uuid",
    levels: [4, 6, 8, 12, 14, 16, 19], // This could be flattened instead
    feature: "CombatSuperiority.uuid",
    replacements: 1,
    // Replacing a manoeuvre is an optional rule from Tasha's
    optional: true
}

// Eldritch Invocations
{
    class: "Warlock.uuid",
    levels: [3, 4, 5, 6, 7, 8, 9, ...],
    feature: "EldritchInvocations.uuid",
    replacements: 1
}

As was rightly pointed out, where we refer to 'feature' above, could just as easily apply to an item of any type not just feat items. For example, starting equipment could be chosen from and granted in the same way as (1) or (5).

@akrigline Had some additional refinement:

{
    characterLevel: 1,
    classLevel: 1,
    type: 'item',
    class: "Champion.uuid",
    advancementData: {
        item: "CombatSuperiority.uuid"
    }
}

The distinction between character level and class level is probably important, and we likely need both. Additionally, where a class or subclass is referenced above as a uuid, it might be preferable to instead use the string identity for that class/subclass as per #530. We potentially want to be able to handle either format.

@Fyorl
Copy link
Contributor Author

Fyorl commented Nov 25, 2021

Originally in GitLab by @Fyorl

We are missing primitives for most proficiency grants, though some of the work has been done already as part of backgrounds that we can hopefully reuse in some way:

  • Armour
  • Weapons
  • Tools
  • Saving throws
  • Skills

@Fyorl
Copy link
Contributor Author

Fyorl commented Nov 25, 2021

Originally in GitLab by @akrigline

Some considerations for the base advancement object structure:

  1. There is a distinction between character level and class level for advancement things
  • Some class level 1 things aren't provided unless the character level is also 1.
  • Background/Race advancement only cares about character level
  1. I think having a sub-object of the 'payload' of the advancement data is going to be helpful as we expand the concept compared to having all of the data in the root of the advancement object. The reasoning here is mostly QOL for working with the data. If everything in the root of the advancement object is a required field but what is within the nested advancementData isn't we'll be able to make more defensive code more easily.

  2. class is probably not necessary since this data is stored on the parent item granting the advancement. We could introduce a derived parent field if we think that's necessary.

@Fyorl
Copy link
Contributor Author

Fyorl commented Nov 25, 2021

Originally in GitLab by @akrigline

Our intial advancment work will focus on the existing mechanisms that allow class features to be 'granted' on levelup.

Proposed data format:

{
  "advancement": {
    "level": 1, // assuming we derive that this is driven by class level vs character level
    "type": "itemGrant",
    "advancementData": {
      "itemUuid": "Some.Item.uuid",
    }
  }
}

itemGrant as a type should be handled simplistically as: "Ask the user if they want to add this item to their actor." same as the existing classFeatures functionality.

@Fyorl
Copy link
Contributor Author

Fyorl commented Nov 25, 2021

Originally in GitLab by @akrigline

Thinking about the idea of class level and character level distinction, this is only really applicable to Class advancement and only for proficiency grants a character gets from secondary classes.

I'm wondering if we should simply derive whether the advancement applies to 'parent class level' or 'parent actor level'.

For multiclassing cases a simple checkbox might remove the need for this distinction, "Only for Primary Class".

@Fyorl
Copy link
Contributor Author

Fyorl commented Nov 27, 2021

Originally in GitLab by @arbron

I think there should be one section for type specific configuration and a second section for user selection:

{
  "level": 1,
  "type": "itemChoice",
  "configuration": {
    "options": {
      "first": "Some.Item.UUID",
      "second": "Other.Item.UUID"
    }
  },
  "value": {
    "selected": "first"
  }
}

This will make it easy to isolate the options that a GM configures versus what the player has selected.

@Fyorl
Copy link
Contributor Author

Fyorl commented Nov 27, 2021

Originally in GitLab by @aaclayton

I do think we still need to discuss whether we store the chosen values from advancement decisions on the individual items which provided them or on the base actor. There are merits of each. I agree with Jeff about storing an inner object of type-specific configuration, although I think that's what @akrigline was going for with advancementData. We just need to align on what key name to use.

@Fyorl
Copy link
Contributor Author

Fyorl commented Nov 29, 2021

Originally in GitLab by @arbron

Some thoughts on the various types of advancements we might like to eventually offer:

Item Grants

  • Grants one or more items automatically
  • GM Configuration: List of item UUIDs
  • Player Data: Keeps track of which features have been added or skipped

Item Choices

  • Either presents a list of item choices to the player or gives them a drop area to deposit arbitrary items
  • GM Configuration: List of item UUIDs from which the player can select and how many the player can select at which levels
  • Player Data: Track which features the player selected/dropped

Hit Points

  • Only available for classes and is automatically added to classes
  • Each class can only have this once, but it automatically appears at all levels
  • GM Configuration: None
  • Player Data: For each level, player's manual hit points rolls or indication that they have taken the flat HP increase

Traits

  • A version for all of the different proficiencies & traits that the player might need
  • Armor, Weapon, Tool, Saving Throw, Skill, Languages, Damage & Condition Resistance/Immunities
  • GM Configuration: Which traits are granted automatically and which the player is given a choice in. See the Background item MR for examples
  • Player Data: Which of the choices the player has selected

Ability Score Improvement

  • Give the player the choice of increasing their ability scores or adding a feat
  • GM Configuration: None
  • Player Data: Abilities improved at various levels or feats taken

Scale Values

  • Arbitrary values that change with levels that are easily accessible in dice formulas
  • GM Configuration: Value at the levels where it changes
  • Player Data: None

Expertise

  • Allow player to select skills or tools in which they gain expertise
  • GM Configuration: Which skills and tools are available as choices and how many choices the player can make (similar interface to the traits option above, but applied differently)
  • Player Data: Which skills or tools the player has chosen

@Fyorl
Copy link
Contributor Author

Fyorl commented Nov 29, 2021

Originally in GitLab by @arbron

All advancement items that are added to classes should have an extra option to determine whether they are available always, only on the primary class, or only on multiclassing. This should make it easy to set up saving throw and other proficiencies that behave differently depending on multiclassing.

@Fyorl
Copy link
Contributor Author

Fyorl commented Nov 30, 2021

Originally in GitLab by @akrigline

In cases where there are multiple items granted (or multiple expertises/ASI/Traits, etc), is each individual one an object within the array of advancements or do we pile them up?

advancement: [
{
  "level": 1,
  "type": "itemGrant",
  "configuration": {
    "itemUuid": 'single-item-uuid'
  },
  "value": {
    "skipped": true
  }
},
{
  "level": 1,
  "type": "itemGrant",
  "configuration": {
    "itemUuid": 'other-item-uuid'
  },
  "value": {
    "skipped": false
  }
},
]

vs

advancement: [
{
  "level": 1,
  "type": "itemGrant",
  "configuration": {
    "itemUuids": ['single-item-uuid', 'other-item-uuid']
  },
  "value": {
    "skipped": ['single-item-uuid']
  }
},
]

I think having each individual advancement 'point' in its own advancement entry is cleaner, but the UX of editing all of them individually could be a pain.

@Fyorl
Copy link
Contributor Author

Fyorl commented Nov 30, 2021

Originally in GitLab by @akrigline

Yeah, advancementData in mine is what @arbron is calling configuration. I'm happy with whatever, we could even call it data, but I was getting a little "data" fatigued :P

@Fyorl
Copy link
Contributor Author

Fyorl commented Nov 30, 2021

Originally in GitLab by @akrigline

Something to note about all of these advancement enabled items:
The Player will have (and should have) control to edit the advancement data of the item once it is on their actor sheet as they will be the owner of that item.

There should be no distinction between "GM View" and "Player View" for the item editing as both will see it.

@Fyorl
Copy link
Contributor Author

Fyorl commented Nov 30, 2021

Originally in GitLab by @arbron

The nice thing about being able to group is it would allows us to group features that are granted together. Like if someone wanted to use this system for automatically granting spells to clerics when they gain a new spell level, their advancement might have one entry for "Features" and another for "Spells" creating a nice, clear grouping.

@Fyorl
Copy link
Contributor Author

Fyorl commented Dec 1, 2021

Originally in GitLab by @Fyorl

I think there's merit, from a UX perspective, of clearly separating the editing of the configuration of choices, vs. the editing of those choices themselves. In this case the 'GM View' is where we edit the configuration, and the 'Player View' is where we see that configuration laid out. It's not planned for stage 1 of this work, but I imagine in future that the player view will be where we allow for earlier advancement choices that were made to be modified.

For an existing example of this, we can look at class skills (and a lesser extent, the saving throws):

image

When I, as a player, open that interface up, I can see that I am able to edit the eligible class skills, which has no effect other than to configure the list I am presented with when I click to edit the chosen class skills. As accurate as the labelling is, I think this is a very weird user experience. If this were P&P, I would look up my class in the Player's Handbook, and it would tell me to pick 2 skills from a list. I can't edit that list because I can't edit the Player's Handbook, and I don't think a player should be presented with that ability to edit it either.

When this functionality was initially presented, there was some similar feedback: https://gitlab.com/foundrynet/dnd5e/-/merge_requests/318#note_618811658 and it feels like allowing players to edit the advancement configuration of their class once it's on their sheet is a larger version of the above, because players are now able to edit things that, from their perspective, should be set in stone.

The easiest solution is to only allow GMs to edit advancement data, but we should also consider whether advancement data should be editable on owned items at all. If we don't allow editing on owned items, then we don't need to restrict editing to GMs, we can restrict it to those with owner permission of the (unowned) item. But then the only way to edit advancement for an existing character would be to edit the unowned class item, delete the owned one from the sheet, then drop a fresh copy of the original class item to the sheet. That's not viable if we store advancement choices on the item, as those choices would be lost, it would require us to store these choices on the actor instead.

If we do allow for editing the owned item, then we can store choices on the item again, but we have to restrict who can edit the advancement configuration to GMs only, in order to avoid the UX issues above (that I claim are a problem).

@Fyorl
Copy link
Contributor Author

Fyorl commented Dec 1, 2021

Originally in GitLab by @aaclayton

I think I agree with Kim's proposal that we would have:

Unowned Items: allow editing of the advancement configuration, but does not allow making advancement choices
Owned Items: allow making advancement choices, but not editing advancement configuration

I think this is the cleanest way to split it without needing to develop separate or specialized logic.

@Fyorl
Copy link
Contributor Author

Fyorl commented Dec 1, 2021

Originally in GitLab by @Fyorl

I've tried to sum up the discussion and decisions so far. Please let me know if I've missed anything.

  1. No-one has expressed any issues with the breakdown of the work into the four sections mentioned in the parent description. I assume we all agree with them then, and will write up separate issues for them shortly.
  2. We do not need a class property for the advancement objects as they will belong to a parent item anyway.
  3. Top-level properties for the advancement objects should be common to them all and allow for their organisation and grouping by level, there should be an inner object to store data specific to that type of advancement object.
  4. We can use the level property and infer whether that means class level or character level from what type of item the advancement object belongs to.
  5. Advancement objects should distinguish whether they are gained only if this class is the primary class, or whether they are gained only if this class has been multiclassed into, or whether they are always gained regardless.

Some open questions:

  1. Do we allow for feature grouping, i.e. can a feature grant provide an array of features, or do we normalise it with several feature grant objects that each provide 1 feature?
  2. Do we allow for editing advancement configuration on owned items? See https://gitlab.com/foundrynet/dnd5e/-/issues/1353#note_746941534 for discussion there.
  3. Related to (7), do we store advancement choices on the class item, or on the actor?
  4. (From discord) Do we need a unique identifier for each advancement object, and should it be derived or persisted?

@Fyorl
Copy link
Contributor Author

Fyorl commented Dec 1, 2021

Originally in GitLab by @Fyorl

Seems reasonable to allow for multiple item grants to be grouped together since we want to allow for choices from several options, and the former can be written in terms of the latter. For example:

Picking from several options:

{
  type: "item",
  choices: 2,
  items: [A, B, C]
}

Gaining all options:

{
  type: "item",
  choices: 2,
  items: [A, B] // The number of choices equals the length of the options, so you gain them all.
}

And you can then infer that you get them all by omitting the choices, leaving:

{
  type: "item",
  items: [A, B]
}

@Fyorl
Copy link
Contributor Author

Fyorl commented Dec 1, 2021

Originally in GitLab by @arbron

One thing to consider is things that allow you to select from a single pool at multiple different levels. We should be able to specify choices-per-level.

{
  …
  “choices”: {
    3: 2,
    7: 2,
    14: 2
  }
}

@Fyorl
Copy link
Contributor Author

Fyorl commented Dec 1, 2021

Originally in GitLab by @arbron

I think the unowned/owned distinction is a good place to start, but later we can maybe add an edit button that allows for switching into an edit mode to configure owned items (similar to how TidySheet has a lock button to prevent certain edits).

@Fyorl
Copy link
Contributor Author

Fyorl commented Dec 2, 2021

Originally in GitLab by @Fyorl

Seems we have an issue to that effect, noting it here to keep track: #1191

@Fyorl
Copy link
Contributor Author

Fyorl commented Dec 2, 2021

Originally in GitLab by @akrigline

I do not agree that owned items should have configuration disabled for all users. As far as I know there is no technical limitation to doing so and preventing it will make the UX worse for GMs ("Oh wait that's supposed to be a level 5 feature let me fix it really quick") and Players who know what they're doing ("My GM has allowed me to add level 4 optional features to my class").

Disabling the configuration for players once it's an owned item... I can see both ways but personally I would be annoyed if I had to pester my GM to fix something wrong. If this were a module, it would probably have a setting "Restrict editing to Trusted Players".

Separating the configuration UI from the election UI should be sufficient.

@Fyorl
Copy link
Contributor Author

Fyorl commented Dec 2, 2021

Originally in GitLab by @akrigline

Consider that most of the Elections a player will make will happen during the level up prompts, diving into the item itself is something that I assume will be rarer once advancement is fully fleshed out. If it is necessary, something has probably gone wrong and it would be unfortunate to put artificial blockers on fixing such a thing.

@Fyorl
Copy link
Contributor Author

Fyorl commented Dec 2, 2021

Originally in GitLab by @Fyorl

I've opted to create only one issue for part (4) here which focuses on item grants. I had less of a clear picture about the other types and didn't want to commit to them yet, and I believe item grants comprise the vast bulk of character advancement, while the others are pretty much entirely the purview of level 1. Some of them are also partially covered by the Backgrounds work and it wasn't immediately obvious to me yet how we want to separate that out.

@Fyorl
Copy link
Contributor Author

Fyorl commented Dec 2, 2021

Originally in GitLab by @Fyorl

As long as the UI for editing the advancement configuration is not in the same place or mixed-in with the UI for viewing the advancement or editing advancement choices (when we eventually implement that), then it's probably fine. An edit toggle would probably also be fine so long as users aren't confused about what they're editing (i.e. they're not editing their choices, they're editing the advancement configuration itself).

@Fyorl
Copy link
Contributor Author

Fyorl commented Dec 2, 2021

Originally in GitLab by @akrigline

I'm thinking each level should be its own advancement object, even in cases where the pool of items is the same? Usually the pool expands on itself i.e. invocations.

So rough example warlock advancement to me should have:

[
  { level: 3, type: 'itemPool', configuration: { choices: 2, itemUuids: [] } },
  { level: 7, type: 'itemPool', configuration: { choices: 2, itemUuids: [] } }
]

@Fyorl
Copy link
Contributor Author

Fyorl commented Dec 2, 2021

Originally in GitLab by @akrigline

We will want to have some data sanitation guards on Embedded Item creation if we store choices for the advancement on the item that removes those choices when the item is added to an actor.

@Fyorl
Copy link
Contributor Author

Fyorl commented Jan 21, 2022

Originally in GitLab by @clay.sweetser

Just curious - will this system support the addition of custom advancement types by modules?

@Fyorl
Copy link
Contributor Author

Fyorl commented Jan 21, 2022

Originally in GitLab by @arbron

Yes, all a module will have to do is create a new subclass of the Advancement type and then add it to game.dnd5e.advancement.types and it will appear alongside all of the default types included with the system.

@Fyorl
Copy link
Contributor Author

Fyorl commented Jan 21, 2022

Originally in GitLab by @playest

Just a side note, you can use jsonc as the name of the code block to enable coloration for comments in your json.

```jsonc
{
    "some": "json" // with comments
}
```

which render as:

{
    "some": "json" // with comments
}

instead of:

{
    "some": "json" // with comments
}

@Fyorl
Copy link
Contributor Author

Fyorl commented Jan 21, 2022

Originally in GitLab by @playest

I'm really glad you are thinking about all of this. I have given it some thoughts and I found this epic much later so I realize that I may have gotten too far with my ideas :/

Feel free to ignore everything, but who knows, may it will help you. At least, it will show you that people care :)

Anyway! Here they are!

Advancement

For the purpose of this I will use the term "document" (which is a term internally used by Foundry) to designate feature, class, subclass, feat, races, subraces, background, items, anything that can be added to the character sheet basically. If you have a better term for this feel free to talk about it and I may edit this in accordance.

I will use the term "advancement" to designate the character creation process or the leveling up process which should be the same in my opinion. If you have a better term for this feel free to talk about it and I may edit this in accordance.

In general, we want to make it easier for the player to build a valid character without forbidding any combination of documents. If they want to put something invalid on their sheet (like 2 races) they should be able to.

I personally think that all of theses documents should have some kind of advancement tab. Why not have an item that gives you proficiency once you reached a certain level for example? I'll admit it's a little weird but it may open the door to amazing modules and automation.

Efforts should go into 3 directions :

  • Data: provide a way to represent the links between all of those documents
  • Edit: Provide a way to view and edit those links
  • Workflow: provide a way to view and follow the advancement workflow

Data

The links between the documents must be stored somewhere. I see two choices here:

  • We store the links in the document themselves
  • We create some kind of "Advancement Path" object that describe how a character can advance

I wont go into the "Advancement Path" thing here because I think it's unnecessary but we may want to explore this possibility in the future.

A character is defined by:

  • Its classes and their levels
  • Its subclasses
  • Its races (let's say a character can have multiple, just for the fun of it)
  • Its backgrounds (let's say a character can have multiple, just for the fun of it)
  • Its miscellaneous features (feats and boons for example)
  • Its items

I think that everything in this list is or has a feature:

  • Classes are features that provides features at certain level
  • Items provide 0 or more features to the owner/wearer

A advancement document can be either inside:

  • The root of character (classes, backgrounds and races are at this level for example)
  • Inside a class (class features are there)
  • Inside a background (background features are there)
  • Inside a race (subraces and race features are there)
  • Inside a subrace (subrace features are there)
  • Inside a feature

Which means we theoretically have the following tree structure:

  • Character
    • Class
      • Class features
      • Subclasses
        • Subclasses features
    • Background
      • Background features
    • Race
      • Race features
      • Subrace
        • Subrace features
    • Feats
    • Boons

See character-sheet-tree.ts.

interface CharacterSheet {
    classes: Class[],
    feats: Feature[],
    boons: Feature[],
}

interface Class {
    subclasses: Subclasses[],
    features: Feature[],
}

interface Background {
    features: Feature[],
}

interface Race {
    subraces: Subrace[],
    features: Feature[],
}

interface Subrace {
    features: Feature[],
}

interface Subclasses {
    features: Feature[],
}

interface Feature {
    // Something
}

We could enforce all of those types a create as many documents types as there is lines elements in this tree but in my opinion it would be better to treat everything the same way: as a super-feature (this term is obviously subject to change).

A super-feature differs from a feature in that it may be "connected" to another super-feature and can be activated by what I call an advancement trigger.

A super-feature is represented as a list of choices that can be in 3 states: dormant, waiting, processed.

  • dormant means that its condition to be on the character sheet is not met
  • waiting means that its condition to be on the character sheet is met but all choices have not been made
  • *processed" means that its condition to be on the character sheet is met and all choices have been made

A choice is a tuple of two elements:

  • rules for how you can choose the super-features in the least (like "choose one", or "choose two", or "choose either elements 1, 2 and 4 OR elements 1, 3 and 5", ...)
  • a list of super-features

A choice has 2 state : not-made, made and not-interested.

  • not-made is the initial state
  • waiting

Here is are TS types that could be used to represent that:

interface CharacterSheet {
    superFeatures: SuperFeature[],
    /** What is contained in the "Features" tab in the current character sheet */
    features: Feature[],
}

interface SuperFeature {
    name: string,
    level?: number,
    state: SuperFeatureState,
    choiceRule: ChoiceRule,
    choices: Choice[],
}

type Choice = SuperFeature | AdvancementAction;

type SuperFeatureState = "dormant" | "waiting" | "processed";

type ChoiceRule = ChooseAll | ChooseSome | ChooseGroups | ChooseSomeWithMandatory;

interface ChooseAll {
    chooseAll: true,
}

interface ChooseSome {
    /** How many choice can be chosen in the `choices array`. Means something like "Choose one in..." or "Choose two in..." */
    howMany: number,
}

interface ChooseSomeWithMandatory {
    mandatory: number[],
    howMany: number,
}

interface ChooseGroups {
    /** The numbers specify the indices in the `choices` array in the parent SuperFeature. Could be something like [ [1, 2], [1, 3], [2, 3] ] */
    groups: number[][],
}

type AdvancementAction = (GroupedAdvancementAction | BasicAdvancementAction) & { level?: number };
type BasicAdvancementAction = GainProficiency | GainFeature | GainLanguage | GainEquipement | GainCurrency | GainAbilityPoints | GainBaseSpeed | GainHitDice | GainMaxHp;

interface GroupedAdvancementAction {
    name: string,
    advancementActions: BasicAdvancementAction[]
};

interface GainProficiency {
    skill: string,
}

interface GainFeature {
    feature: string,
}

interface GainLanguage {
    language: string,
}

interface GainEquipement {
    itemName: string,
}

interface GainCurrency {
    gp: number,
}

interface GainHitDice {
    hitDiceGain: number,
    type: "d4" | "d6" | "d8" | "d10" | "d12"
}

interface GainMaxHp {
    maxHpGain: number,
}

interface GainBaseSpeed {
    type: "walk" | "fly" | "burrow" | "climb" | "swim",
    distance: number,
}

interface GainAbilityPoints {
    attribute: Attributes,
    bonus: number,
}

interface GainSavingThrow {
    savingThrow: Attributes,
}

type Attributes = "strength" | "dexterity" | "constitution" | "intelligence" | "wisdom" | "charisma";

/** Some basic feature that already exists in the game */
interface Feature {
    // We put only those 2 fields for this example
    name: string,
    type: FeatureType,
}

type FeatureType = "feature" | "race" | "subrace" | "class" | "subclass" | "feat" | "boon";

And how a sample could look like (see at the end of this file):

let myCleric: CharacterSheet = {
    features: [], // empty, fill this array is not the point of this example, this will be done by the advancement workflow
    superFeatures: [
        {
            name: "Choose Background",
            state: "dormant",
            choiceRule: { howMany: 1 },
            choices: [
                {
                    name: "Acolyte",
                    state: "dormant",
                    choiceRule: { chooseAll: true },
                    choices: [
                        {
                            name: "Languages",
                            state: "dormant",
                            choiceRule: { howMany: 2 },
                            choices: [
                                { language: "common" },
                                { language: "dwarvish" },
                                { language: "elvish" },
                                { language: "draconic" },
                                // I'm not gonna list all languages here...
                            ]
                        },
                        {
                            name: "Skill Proficiencies",
                            advancementActions: [
                                { skill: "insight" },
                                { skill: "religion" }
                            ]
                        },
                        {
                            name: "Equipement",
                            state: "dormant",
                            choiceRule: { chooseAll: true },
                            choices: [
                                { itemName: "holy symbol" },
                                {
                                    name: "Prayer book or Prayer wheel",
                                    state: "dormant",
                                    choiceRule: { howMany: 1 },
                                    choices: [
                                        { itemName: "prayer book" },
                                        { itemName: "prayer wheel" },
                                    ]
                                },
                                { itemName: "stick of incense x 5" },
                                { itemName: "vestments" },
                                { itemName: "common clothes" },
                                { gp: 15 }
                            ]
                        },
                        {
                            name: "Shelter of the Faithful",
                            state: "dormant",
                            choiceRule: { chooseAll: true },
                            choices: [{ feature: "Shelter of the Faithful" }]
                        }
                    ]
                },
                {
                    name: "Non-SRD Background",
                    state: "dormant",
                    choiceRule: { chooseAll: true },
                    choices: [
                        {
                            name: "Languages",
                            state: "dormant",
                            choiceRule: { howMany: 1 },
                            choices: [
                                { language: "common" },
                                { language: "dwarvish" }
                            ]
                        },
                        {
                            name: "Skill Proficiencies",
                            advancementActions: [
                                { skill: "arcana" },
                                { skill: "acrobatics" }
                            ]
                        }
                    ]
                },
            ]
        },
        {
            name: "Choose Race",
            state: "dormant",
            choiceRule: { howMany: 1 },
            choices: [
                {
                    name: "Elf",
                    state: "dormant",
                    choiceRule: { chooseAll: true },
                    choices: [
                        {
                            name: "Ability Score Increase",
                            advancementActions: [{ attribute: "dexterity", bonus: 2 }]
                        },
                        {
                            name: "Speed",
                            advancementActions: [{ type: "walk", distance: 30 }]
                        },
                        { feature: "Keen Senses" },
                        {
                            name: "Languages",
                            advancementActions: [
                                { language: "common" },
                                { language: "elvish" }
                            ]
                        }
                        // and other stuff
                    ]
                },
                {
                    name: "Human",
                    state: "dormant",
                    choiceRule: { chooseAll: true },
                    choices: [
                        {
                            name: "Ability Score Increase",
                            advancementActions: [
                                { attribute: "strength", bonus: 1 },
                                { attribute: "dexterity", bonus: 1 },
                                { attribute: "constitution", bonus: 1 },
                                { attribute: "intelligence", bonus: 1 },
                                { attribute: "wisdom", bonus: 1 },
                                { attribute: "charisma", bonus: 1 },
                            ]
                        },
                        {
                            name: "Speed",
                            advancementActions: [{ type: "walk", distance: 30 }]
                        },
                        { feature: "Keen Senses" },
                        {
                            name: "Languages",
                            state: "dormant",
                            choiceRule: {
                                mandatory: [0], // 0 is the index of "common" in the choices array below
                                howMany: 2 // including the mandatory choices
                            },
                            choices: [
                                { language: "common" },
                                { language: "dwarvish" },
                                { language: "elvish" },
                                { language: "draconic" },
                                // I'm not gonna list all languages here...
                            ]
                        }
                        // and other stuff
                    ]
                }
            ]
        },
        {
            name: "Choose Class",
            state: "dormant",
            choiceRule: { howMany: 1 },
            choices: [
                {
                    name: "Fighter",
                    state: "dormant",
                    choiceRule: { chooseAll: true },
                    choices: [
                        {
                            name: "Hit points",
                            advancementActions: [{ hitDiceGain: 1, type: "d10" }, { maxHpGain: 10 }]
                        },
                        {
                            name: "Armors",
                            advancementActions: [{ skill: "all armors" }, { skill: "shields" }]
                        },
                        {
                            name: "Weapons",
                            advancementActions: [{ skill: "simple weapons" }, { skill: "martial weapons" }]
                        },
                        {
                            name: "Skills",
                            state: "dormant",
                            choiceRule: { howMany: 2 },
                            choices: [
                                { skill: "acrobatics" },
                                { skill: "animal handling" },
                                { skill: "athletics" },
                                { skill: "history" },
                                { skill: "insight" },
                                { skill: "intimidation" },
                                { skill: "perception" },
                                { skill: "survival" },
                            ]
                        },
                        // enough with the non levelled stuff, let's try to do levelled features
                        {
                            name: "Second Wind",
                            level: 1,
                            advancementActions: [{ feature: "Second Wind" }]
                        },
                        {
                            name: "Fighting Style",
                            level: 1,
                            state: "dormant",
                            choiceRule: { howMany: 1 },
                            choices: [
                                { feature: "Archery" },
                                { feature: "Defense" },
                                { feature: "Dueling" },
                                { feature: "Great Weapon Fighting" },
                                { feature: "Protection" },
                                { feature: "Two Weapon Fighting" },
                            ]
                        },
                        {
                            name: "Action Surge",
                            level: 2,
                            advancementActions: [{ feature: "Action Surge" }]
                        },
                        {
                            level: 3,
                            name: "Martial Archtype",
                            choiceRule: { howMany: 1 },
                            state: "dormant",
                            choices: [
                                {
                                    name: "Champion",
                                    choiceRule: { chooseAll: true },
                                    state: "dormant",
                                    choices: [
                                        { level: 3, feature: "Improved Critical" },
                                        { level: 7, feature: "Remarkable Athlete" },
                                    ]
                                },
                                {
                                    name: "Non-SRD Subclass for Fighter",
                                    advancementActions: [{ feature: "Placeholder for subclass" }]
                                }
                            ]
                        }
                    ]
                }
            ]
        }
    ]
};

Edit

I have not a lot to say here.

Workflow

See here

Weird Cases

I try to think of weird cases when building something because I feel that if I manage to deal with those cases the normal cases will be more than correctly managed. Of course, sometime you have to give up on one of those weird cases to simplify the task but we are not here yet.

So here are some weird use case that could be managed:

  • Classes and players have more level than 20
  • Multiple subclasses (even from the same class) are taken
  • A document has been updated by the DM so to get the last version the player (or the DM) remove it from its character sheet and add the new one, there should be no duplicate in this case
  • The players adds a document but doesn't want to (or can't) follow the advancement workflow (they may also be interrupted by a crash or a missclick)
  • A player should be able to start building its character from any point, they may start by adding the class, or the race, or the subclass (yes it's weird, that's the point), or a random feature, an item
  • If a duplicate Document would be added during the advancement workflow to the character sheet a confirmation (or a warning before adding it) would be
  • No warning/confirmation would be needed if a Document is added by hand to the character sheet, if we find a way to do it elegantly then why not but it's not necessary
  • Documents may be coming from multiple compendium
  • There may be multiple Documents with the same name but different characteristics
  • The advancement process should be viewable to see all the choice that have been made during the advancement process (like what Ability did I increase with my ability score improvement, what proficiency did I choose with my background, ...)
  • It should be possible to go back on our choices
    • Let's say I added a class that give me a choice of proficiencies
    • among those I chose arcana and athletism
    • once they reach they choose their background they realize that athletism is mandatory in this background so they go back and choose an other class proficiency
    • I know that RAW getting a the same proficiency a second time gives you the proficiency of your choice, we may do that but I feel that my example stands
  • I should be able to backtrack on any choice, changing the subclass (or subrace, or any document) should be possible and should remove any associated document but no more. It may possible to do that so we may ask the player to validate what to remove here.
  • The feature tab should still be able to contain any document, and this advancement system should not forbid any combination, just guide the player during the process by showing reports of what could/should be added or removed and why

Advancements triggers

An advancement trigger should be able to propose to:

  • create a resource on the character sheet
  • add a feature with specific configuration (like make the future use a existing resource on the character sheet)
  • add a spell with a different ability and/or a different pool of spell slot (my way of enabling spells that can be cast once per long rest)
  • add a proficiency
  • add an item
  • ask for a choose between any combination of the choices above (mostly a way of managing the choices in background but could be used in other cases)

Other considerations

  • How will it integrate with custom character sheets?
    • We could not care and let them update their own sheet
    • We could provide our own interface separated with a button somewhere that opens the advancement interface
    • Any other way
  • Should we really manage the whole workflow?
    • Maybe we should just focus on the data part and let the community develop modules for the edit and workflow parts?

Ideas

An elegant way to deal with subclasses could be to represent them with a class document that takes its level from an other class.

@Fyorl
Copy link
Contributor Author

Fyorl commented Jan 27, 2022

Originally in GitLab by @Fyorl

Thank you for your comprehensive and thoughtful input on this. You have, unfortunately, missed the boat a little as this work is well underway but hopefully I can reassure you that many of the things you brought up were considered as part of our initial design work.

I feel I should also mention that part of our design philosophy is to try to keep scope as minimal as possible to ensure we create a well thought-out, solid foundation that can be iterated on, rather than attempting to cover every possible use case with one huge, complex system. Therefore, it is unlikely everything you've mentioned will be accommodated for in the initial version, but the system will be improved upon gradually in subsequent releases.

@Fyorl
Copy link
Contributor Author

Fyorl commented Jan 27, 2022

Originally in GitLab by @playest

You're right, I think it's safe to say I missed the boat. I guess I bought Foudry too late ^^

I feel I should also mention that part of our design philosophy is to try to keep scope as minimal as possible

Very good design philosophy :) Good luck!

@Fyorl
Copy link
Contributor Author

Fyorl commented Jan 27, 2022

Originally in GitLab by @Fyorl

I guess I bought Foudry too late

Too late for this piece of work, perhaps, but if you're interested in contributing to the dnd5e system, I can recommend you wait for when we have locked down scope for the next milestone and see if there's anything you'd like to get involved with there. We do try to review a handful of MRs that are off-scope each milestone but there's no guarantee we can get to any particular one. You can review our CONTRIBUTING.md for more details.

@Fyorl
Copy link
Contributor Author

Fyorl commented Jan 27, 2022

Originally in GitLab by @playest

I'm definitely interested in contributing. I'll take a look at the CONTRIBUTING.md. How can I be updated when you will have locked down the scope? Do you have a timeline?

@Fyorl
Copy link
Contributor Author

Fyorl commented Jan 27, 2022

Originally in GitLab by @Fyorl

I don't actually have a very good answer for you there. At some point after 1.6 is released we will scope out 1.7 at which point we'll create a new milestone (or modify an existing one), and then start assigning issues and epics to it. I will see if @akrigline has any better advice for you on that front.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants