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

[#1407] Add Subclass advancement & selection interface on sheet #4072

Merged
merged 2 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -2432,6 +2437,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 @@ -3496,6 +3496,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