Skip to content

Commit

Permalink
Merge pull request #4072 from foundryvtt/advancement/subclass-simple
Browse files Browse the repository at this point in the history
[#1407] Add Subclass advancement & selection interface on sheet
  • Loading branch information
arbron authored Aug 30, 2024
2 parents 4634d08 + 4b89f50 commit 4954cec
Show file tree
Hide file tree
Showing 27 changed files with 227 additions and 8 deletions.
1 change: 1 addition & 0 deletions icons/LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The dnd5e system for Foundry Virtual Tabletop includes icon artwork licensed fro
/svg/original-class.svg - "Barbute" by Lorc under CC BY 3.0
/svg/scale-value.svg - "Dice target" by Delapouite under CC BY 3.0
/svg/size.svg - "Body height" by Delapouite under CC BY 3.0
/svg/subclass.svg - "Elf helmet" by Kier Heyl under CC BY 3.0
/svg/trait.svg - "Scroll unfurled" by Lorc under CC BY 3.0
/svg/trait-armor-proficiencies.svg - "Leather armor" by Delapouite under CC BY 3.0
/svg/trait-damage-immunities.svg - "Aura" by Lorc under CC BY 3.0
Expand Down
6 changes: 6 additions & 0 deletions icons/svg/subclass.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,11 @@
"Level": "Must be at least level {level} to take this feat.",
"Type": "Only features with the \"feat\" type can be selected."
}
},
"Subclass": {
"Title": "Subclass",
"Hint": "Specify what level this class receives its subclass.",
"FlowHint": "Select a subclass in the Features tab after level up has completed."
}
},

Expand Down Expand Up @@ -2436,6 +2441,7 @@
"Hint": "Formula in GP that can be used in place of starting equipment."
}
},
"DND5E.SubclassAdd": "Add Subclass",
"DND5E.SubclassIdentifierHint": "This identifier should match the identifier on the parent class to ensure they are properly linked.",
"DND5E.SubclassAssignmentError": "{class} already has a subclass. Remove the existing '{subclass}' subclass before adding a new one.",
"DND5E.SubclassDuplicateError": "A subclass with the identifier {identifier} already exists on this actor.",
Expand Down
8 changes: 8 additions & 0 deletions less/v2/actors.less
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,14 @@
bottom: 2px;
}
}

.subclass-icon {
display: flex;
align-items: center;
justify-content: center;
color: var(--dnd5e-color-card);
font-size: var(--font-size-20);
}
}

.name-stacked { flex: 1; }
Expand Down
16 changes: 11 additions & 5 deletions module/applications/actor/character-sheet-2.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import CharacterData from "../../data/actor/character.mjs";
import * as Trait from "../../documents/actor/trait.mjs";
import { formatNumber, simplifyBonus } from "../../utils.mjs";
import { formatNumber } from "../../utils.mjs";
import CompendiumBrowser from "../compendium-browser.mjs";
import ContextMenu5e from "../context-menu.mjs";
import SheetConfig5e from "../sheet-config.mjs";
Expand Down Expand Up @@ -385,7 +385,9 @@ export default class ActorSheet5eCharacter2 extends ActorSheetV2Mixin(ActorSheet
_onAction(event) {
const target = event.currentTarget;
switch ( target.dataset.action ) {
case "findItem": this._onFindItem(target.dataset.itemType); break;
case "findItem":
this._onFindItem(target.dataset.itemType, { classIdentifier: target.dataset.classIdentifier });
break;
case "removeFavorite": this._onRemoveFavorite(event); break;
case "spellcasting": this._onToggleSpellcasting(event); break;
case "toggleInspiration": this._onToggleInspiration(); break;
Expand Down Expand Up @@ -419,11 +421,15 @@ export default class ActorSheet5eCharacter2 extends ActorSheetV2Mixin(ActorSheet

/**
* Show available items of a given type.
* @param {string} type The item type.
* @param {string} type The item type.
* @param {object} [options={}]
* @param {string} [options.classIdentifier] Identifier of the class when finding a subclass.
* @protected
*/
async _onFindItem(type) {
const result = await CompendiumBrowser.selectOne({ filters: { locked: { types: new Set([type]) } } });
async _onFindItem(type, { classIdentifier }={}) {
const filters = { locked: { types: new Set([type]) } };
if ( classIdentifier ) filters.locked.additional = { class: { [classIdentifier]: 1 } };
const result = await CompendiumBrowser.selectOne({ filters });
if ( result ) this._onDropItemCreate(await fromUuid(result));
}

Expand Down
6 changes: 5 additions & 1 deletion module/applications/actor/character-sheet.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {

// Partition items by category
let {items, spells, feats, races, backgrounds, classes, subclasses} = context.items.reduce((obj, item) => {
const {quantity, uses} = item.system;
const { quantity } = item.system;

// Item details
const ctx = context.itemContext[item.id] ??= {};
Expand Down Expand Up @@ -142,6 +142,10 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
const identifier = cls.system.identifier || cls.name.slugify({strict: true});
const subclass = subclasses.findSplice(s => s.system.classIdentifier === identifier);
if ( subclass ) arr.push(subclass);
else {
const subclassAdvancement = cls.advancement.byType.Subclass?.[0];
if ( subclassAdvancement && (subclassAdvancement.level <= cls.system.levels) ) ctx.needsSubclass = true;
}
return arr;
}, []);
for ( const subclass of subclasses ) {
Expand Down
1 change: 1 addition & 0 deletions module/applications/advancement/_module.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ export {default as ScaleValueConfig} from "./scale-value-config.mjs";
export {default as ScaleValueFlow} from "./scale-value-flow.mjs";
export {default as SizeConfig} from "./size-config.mjs";
export {default as SizeFlow} from "./size-flow.mjs";
export {default as SubclassFlow} from "./subclass-flow.mjs";
export {default as TraitConfig} from "./trait-config.mjs";
export {default as TraitFlow} from "./trait-flow.mjs";
1 change: 1 addition & 0 deletions module/applications/advancement/advancement-flow.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export default class AdvancementFlow extends FormApplication {
advancement: this.advancement,
type: this.advancement.constructor.typeName,
title: this.title,
hint: this.advancement.hint,
summary: this.advancement.summaryForLevel(this.level),
level: this.level
};
Expand Down
13 changes: 13 additions & 0 deletions module/applications/advancement/subclass-flow.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import AdvancementFlow from "./advancement-flow.mjs";

/**
* Inline application that presents the player with the subclass hint.
*/
export default class SubclassFlow extends AdvancementFlow {
/** @inheritDoc */
async getData() {
return foundry.utils.mergeObject(super.getData(), {
hint: `${this.advancement.hint ?? ""} ${game.i18n.localize("DND5E.ADVANCEMENT.Subclass.FlowHint")}`
});
}
}
4 changes: 4 additions & 0 deletions module/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3506,6 +3506,10 @@ DND5E.advancementTypes = {
documentClass: advancement.SizeAdvancement,
validItemTypes: new Set(["race"])
},
Subclass: {
documentClass: advancement.SubclassAdvancement,
validItemTypes: new Set(["class"])
},
Trait: {
documentClass: advancement.TraitAdvancement,
validItemTypes: new Set(_ALL_ITEM_TYPES)
Expand Down
1 change: 1 addition & 0 deletions module/documents/advancement/_module.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export {default as ItemChoiceAdvancement} from "./item-choice.mjs";
export {default as ItemGrantAdvancement} from "./item-grant.mjs";
export {default as ScaleValueAdvancement} from "./scale-value.mjs";
export {default as SizeAdvancement} from "./size.mjs";
export {default as SubclassAdvancement} from "./subclass.mjs";
export {default as TraitAdvancement} from "./trait.mjs";
42 changes: 42 additions & 0 deletions module/documents/advancement/subclass.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Advancement from "./advancement.mjs";
import SubclassFlow from "../../applications/advancement/subclass-flow.mjs";

/**
* Advancement that indicates when a class takes a subclass. Only allowed on class items and can only be taken once.
*/
export default class SubclassAdvancement extends Advancement {

/** @inheritDoc */
static get metadata() {
return foundry.utils.mergeObject(super.metadata, {
order: 70,
icon: "icons/skills/trades/mining-pickaxe-yellow-blue.webp",
typeIcon: "systems/dnd5e/icons/svg/subclass.svg",
title: game.i18n.localize("DND5E.ADVANCEMENT.Subclass.Title"),
hint: game.i18n.localize("DND5E.ADVANCEMENT.Subclass.Hint"),
apps: {
flow: SubclassFlow
}
});
}

/* -------------------------------------------- */
/* Display Methods */
/* -------------------------------------------- */

/** @inheritDoc */
summaryforLevel(level, { configMode=false }={}) {
const subclass = this.item.subclass;
if ( configMode || !subclass ) return "";
return subclass.toAnchor().outerHTML;
}

/* -------------------------------------------- */
/* Editing Methods */
/* -------------------------------------------- */

/** @inheritDoc */
static availableForItem(item) {
return !item.advancement.byType.Subclass?.length;
}
}
10 changes: 10 additions & 0 deletions packs/_source/classes/barbarian.json
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,16 @@
},
"level": 19,
"title": ""
},
{
"_id": "ok09058sgbpt0w6b",
"type": "Subclass",
"configuration": {},
"value": {},
"level": 3,
"title": "Primal Path",
"icon": "",
"classRestriction": ""
}
],
"spellcasting": {
Expand Down
10 changes: 10 additions & 0 deletions packs/_source/classes/bard.json
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,16 @@
},
"level": 19,
"title": ""
},
{
"_id": "wjdgb3r8pyeix83o",
"type": "Subclass",
"configuration": {},
"value": {},
"level": 3,
"title": "Bard College",
"icon": "",
"classRestriction": ""
}
],
"spellcasting": {
Expand Down
10 changes: 10 additions & 0 deletions packs/_source/classes/cleric.json
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,16 @@
},
"level": 19,
"title": ""
},
{
"_id": "8k7cukgzwwl67h6c",
"type": "Subclass",
"configuration": {},
"value": {},
"level": 1,
"title": "Divine Domain",
"icon": "",
"classRestriction": ""
}
],
"spellcasting": {
Expand Down
10 changes: 10 additions & 0 deletions packs/_source/classes/druid.json
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,16 @@
"value": {},
"title": "Wild Shape Uses",
"icon": null
},
{
"_id": "p9otj81aosxpn6wn",
"type": "Subclass",
"configuration": {},
"value": {},
"level": 2,
"title": "Druid Circle",
"icon": "",
"classRestriction": ""
}
],
"spellcasting": {
Expand Down
10 changes: 10 additions & 0 deletions packs/_source/classes/fighter.json
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,16 @@
},
"level": 14,
"title": ""
},
{
"_id": "aqampk2fc5jk5jqh",
"type": "Subclass",
"configuration": {},
"value": {},
"level": 3,
"title": "Martial Archetype",
"icon": "",
"classRestriction": ""
}
],
"spellcasting": {
Expand Down
10 changes: 10 additions & 0 deletions packs/_source/classes/monk.json
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,16 @@
},
"level": 19,
"title": ""
},
{
"_id": "0awj2yq115ev9u9o",
"type": "Subclass",
"configuration": {},
"value": {},
"level": 3,
"title": "Monastic Tradition",
"icon": "",
"classRestriction": ""
}
],
"spellcasting": {
Expand Down
10 changes: 10 additions & 0 deletions packs/_source/classes/paladin.json
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,16 @@
},
"value": {},
"title": "Aura Radius"
},
{
"_id": "n22j60yn24fqr3sy",
"type": "Subclass",
"configuration": {},
"value": {},
"level": 3,
"title": "Sacred Oath",
"icon": "",
"classRestriction": ""
}
],
"spellcasting": {
Expand Down
10 changes: 10 additions & 0 deletions packs/_source/classes/ranger.json
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,16 @@
},
"level": 19,
"title": ""
},
{
"_id": "gb53865sgbtx8xr2",
"type": "Subclass",
"configuration": {},
"value": {},
"level": 3,
"title": "Ranger Archetype",
"icon": "",
"classRestriction": ""
}
],
"spellcasting": {
Expand Down
10 changes: 10 additions & 0 deletions packs/_source/classes/rogue.json
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,16 @@
"level": 15,
"title": "",
"value": {}
},
{
"_id": "n0yoxbkyz32yd942",
"type": "Subclass",
"configuration": {},
"value": {},
"level": 3,
"title": "Roguish Archetype",
"icon": "",
"classRestriction": ""
}
],
"spellcasting": {
Expand Down
10 changes: 10 additions & 0 deletions packs/_source/classes/sorcerer.json
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,16 @@
},
"level": 19,
"title": ""
},
{
"_id": "f06tge0vsvdhb0sw",
"type": "Subclass",
"configuration": {},
"value": {},
"level": 1,
"title": "Sorcerous Origin",
"icon": "",
"classRestriction": ""
}
],
"spellcasting": {
Expand Down
10 changes: 10 additions & 0 deletions packs/_source/classes/warlock.json
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,16 @@
},
"level": 19,
"title": ""
},
{
"_id": "vibumq2vxjl9tmk4",
"type": "Subclass",
"configuration": {},
"value": {},
"level": 1,
"title": "Otherworldly Patron",
"icon": "",
"classRestriction": ""
}
],
"spellcasting": {
Expand Down
Loading

0 comments on commit 4954cec

Please sign in to comment.