foundryvtt / dnd5e

An implementation of the 5th Edition game system for Foundry Virtual Tabletop (http://foundryvtt.com).
MIT License
321 stars 218 forks source link

Character Advancement #1353

Closed Fyorl closed 2 years ago

Fyorl commented 2 years ago

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:

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:

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 commented 2 years ago

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.

Fyorl commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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

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

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:

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

A advancement document can be either inside:

Which means we theoretically have the following tree structure:

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.

A choice is a tuple of two elements:

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

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:

Advancements triggers

An advancement trigger should be able to propose to:

Other considerations

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

Originally in GitLab by @clay.sweetser

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

Fyorl commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

Originally in GitLab by @Fyorl

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

Fyorl commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

Originally in GitLab by @arbron

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

Item Grants

Item Choices

Hit Points

Traits

Ability Score Improvement

Scale Values

Expertise

Fyorl commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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
  2. 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.

  3. 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 commented 2 years ago

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:

Fyorl commented 2 years ago

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.