diff --git a/src/fonts/laski-sans/Laski-Sans-Bold.woff2 b/src/assets/fonts/laski-sans/Laski-Sans-Bold.woff2 similarity index 100% rename from src/fonts/laski-sans/Laski-Sans-Bold.woff2 rename to src/assets/fonts/laski-sans/Laski-Sans-Bold.woff2 diff --git a/src/fonts/laski-sans/Laski-Sans-Regular.woff2 b/src/assets/fonts/laski-sans/Laski-Sans-Regular.woff2 similarity index 100% rename from src/fonts/laski-sans/Laski-Sans-Regular.woff2 rename to src/assets/fonts/laski-sans/Laski-Sans-Regular.woff2 diff --git a/src/fonts/laski-sans/Laski-Sans-SemiBold.woff2 b/src/assets/fonts/laski-sans/Laski-Sans-SemiBold.woff2 similarity index 100% rename from src/fonts/laski-sans/Laski-Sans-SemiBold.woff2 rename to src/assets/fonts/laski-sans/Laski-Sans-SemiBold.woff2 diff --git a/src/fonts/penumbra-web-pro/PenumbraWebPro-Serif.woff b/src/assets/fonts/penumbra-web-pro/PenumbraWebPro-Serif.woff similarity index 100% rename from src/fonts/penumbra-web-pro/PenumbraWebPro-Serif.woff rename to src/assets/fonts/penumbra-web-pro/PenumbraWebPro-Serif.woff diff --git a/src/declarations/foundry/client/data/abstract/client-document.d.ts b/src/declarations/foundry/client/data/abstract/client-document.d.ts index 23dfc4c2..581ac116 100644 --- a/src/declarations/foundry/client/data/abstract/client-document.d.ts +++ b/src/declarations/foundry/client/data/abstract/client-document.d.ts @@ -7,9 +7,9 @@ declare function _ClientDocumentMixin< Schema extends foundry.abstract.DataModel = foundry.abstract.DataModel, Parent extends foundry.abstract.Document | null = null, BaseClass extends typeof foundry.abstract.Document, ->(base: BaseClass): Mixin; +>(base: BaseClass): Mixin; -declare class _ClientDocument { +declare class ClientDocument { /** * A collection of Application instances which should be re-rendered whenever this document is updated. * The keys of this object are the application ids and the values are Application instances. Each @@ -40,4 +40,12 @@ declare class _ClientDocument { * Called before {@link ClientDocument#prepareDerivedData} in {@link ClientDocument#prepareData}. */ public prepareDerivedData(); + + /** + * Create a content link for this Document. + * @param options Additional options to configure how the link is constructed. + */ + public toAnchor( + options?: Partial, + ): HTMLAnchorElement; } diff --git a/src/declarations/foundry/common/abstract/fields.d.ts b/src/declarations/foundry/common/abstract/fields.d.ts index e67f81d7..6d5aca2d 100644 --- a/src/declarations/foundry/common/abstract/fields.d.ts +++ b/src/declarations/foundry/common/abstract/fields.d.ts @@ -326,6 +326,69 @@ declare namespace foundry { class SetField extends ArrayField {} class HTMLField extends StringField {} + + /** + * A subclass of {@link ObjectField} which embeds some other DataModel definition as an inner object. + */ + class EmbeddedDataField extends SchemaField { + constructor( + model: typeof foundry.data.DataModel, + options?: DataFieldOptions, + context?: DataFieldContext, + ); + } + + /** + * A subclass of {@link ArrayField} which supports an embedded Document collection. + * Invalid elements will be dropped from the collection during validation rather than failing for the field entirely. + */ + class EmbeddedCollectionField extends ArrayField { + constructor( + element: typeof foundry.abstract.Document, + options?: DataFieldOptions, + context?: DataFieldContext, + ); + } + + /** + * A subclass of {@link EmbeddedDataField} which supports a single embedded Document. + */ + class EmbeddedDocumentField extends EmbeddedDataField { + constructor( + model: typeof foundry.abstract.Document, + options?: DataFieldOptions, + context?: DataFieldContext, + ); + } + + /** + * A subclass of {@link StringField} which provides the primary _id for a Document. + * The field may be initially null, but it must be non-null when it is saved to the database. + */ + class DocumentIdField extends StringField {} + + interface DocumentUUIDFieldOptions extends StringFieldOptions { + /** + * A specific document type in CONST.ALL_DOCUMENT_TYPES required by this field + */ + type?: string; + + /** + * Does this field require (or prohibit) embedded documents? + */ + embedded?: boolean; + } + + /** + * A subclass of {@link StringField} which supports referencing some other Document by its UUID. + * This field may not be blank, but may be null to indicate that no UUID is referenced. + */ + class DocumentUUIDField extends StringField { + constructor( + options?: DocumentUUIDFieldOptions, + context?: DataFieldContext, + ); + } } } } diff --git a/src/index.ts b/src/index.ts index eef200c8..8fec169b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -58,6 +58,7 @@ Hooks.once('init', async () => { Items.unregisterSheet('core', ItemSheet); registerItemSheet(ItemType.Culture, applications.item.CultureItemSheet); + registerItemSheet(ItemType.Ancestry, applications.item.AncestrySheet); registerItemSheet(ItemType.Path, applications.item.PathItemSheet); registerItemSheet( ItemType.Connection, diff --git a/src/lang/en.json b/src/lang/en.json index dbb0ed63..e0227bc8 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -267,55 +267,68 @@ "Type": { "Weapon": { "label": "Weapon", - "label_plural": "Weapons" + "label_plural": "Weapons", + "desc_placeholder": "Edit this to describe what this weapon is & how it looks." }, "Armor": { "label": "Armor", - "label_plural": "Armor" + "label_plural": "Armor", + "desc_placeholder": "Edit this to describe what this armor is & how it looks." }, "Equipment": { "label": "Equipment", - "label_plural": "Equipment" + "label_plural": "Equipment", + "desc_placeholder": "Edit this to describe what this piece of equipment is & how it looks." }, "Loot": { "label": "Loot", - "label_plural": "Loot" + "label_plural": "Loot", + "desc_placeholder": "Edit this to describe the item." }, "Ancestry": { "label": "Ancestry", - "label_plural": "Ancestries" + "label_plural": "Ancestries", + "desc_placeholder": "Edit this to define a potential heritage for a character. Also include any special rules text for players to read." }, "Culture": { "label": "Culture", - "label_plural": "Cultures" + "label_plural": "Cultures", + "desc_placeholder": "Edit this to define one of the many cultures in the setting. Also include any special rules text for players to read." }, "Path": { "label": "Path", - "label_plural": "Paths" + "label_plural": "Paths", + "desc_placeholder": "Edit this to describe this the sorts of characters this progression path represents." }, "Specialty": { "label": "Specialty", - "label_plural": "Specialties" + "label_plural": "Specialties", + "desc_placeholder": "Edit this to describe the unique subset of characters this specialty represents." }, "Talent": { "label": "Talent", - "label_plural": "Talents" + "label_plural": "Talents", + "desc_placeholder": "Edit this to describe what this Talent does." }, "Action": { "label": "Action", - "label_plural": "Actions" + "label_plural": "Actions", + "desc_placeholder": "Edit this to describe what this action is and does." }, "Trait": { "label": "Trait", - "label_plural": "Traits" + "label_plural": "Traits", + "desc_placeholder": "Edit this to describe this trait." }, "Injury": { "label": "Injury", - "label_plural": "Injuries" + "label_plural": "Injuries", + "desc_placeholder": "Edit this to describe how this injury affects a character." }, "Connection": { "label": "Connection", - "label_plural": "Connections" + "label_plural": "Connections", + "desc_placeholder": "Edit this to create a description of the NPC and how they are connected to the character." } }, "Weapon": { @@ -454,6 +467,7 @@ }, "AttackFlavor": "[actor] attacks with their [item].", "Sheet": { + "Basics": "Basics", "Tabs": { "Description": "Description", "Effects": "Effects", @@ -469,6 +483,27 @@ "InitialDuration": "Initial", "RemainingDuration": "Remaining" }, + "Ancestry": { + "Size": "Size", + "Advancement": "Advancement", + "FreePath": "Free Path", + "FreeTalents": "Free Talents", + "BonusTalents": "Bonus Talents", + "Restrictions": "Restrictions", + "PathReferencePlaceholder": "Drop a Path here to set a free path", + "TalentIdDescription": "The identifier column should hold the identifier of the talent item you want characters with this Ancestry to have access to. The path identifier can only contain letters (a-z), numbers (0-9), dashes (-), and underscores (_).", + "Component": { + "AdvancementTalentList": { + "DropTalent": "Drop a Talent here to add it to the list", + "Warning": { + "WrongType": "The dropped Document must be of type Talent" + } + }, + "BonusTalents": { + "Add": "New Rule" + } + } + }, "Specialty": { "PathId": "Path Identifier", "PathIdDescription": "The identifier of the path this specialty belongs to. The path identifier can only contain letters (a-z), numbers (0-9), dashes (-), and underscores (_)." @@ -627,6 +662,21 @@ "Edit": "Edit Effect", "Delete": "Remove Effect", "ToggleActive": "Toggle Active" + }, + "Talents": { + "Label": "Talents", + "AcquiredAtLevel": "Acquired at Level", + "Add": "New Talent", + "Edit": "Edit Talent", + "Delete": "Remove Talent", + "Cancel": "Discard Changes", + "Submit": "Finish Editing" + }, + "TalentPicks": { + "Edit": "Edit", + "Delete": "Remove", + "Cancel": "Discard Changes", + "Submit": "Finish Editing" } }, "Attribute": { @@ -738,12 +788,29 @@ "Dice": "Dice" } } + }, + "EditBonusTalentsRule": { + "Title": "Edit Bonus Talents Rule", + "Quantity": "Amount", + "Warning": { + "DuplicateLevel": "A rule for level {level} already exists" + } + } + }, + "COMPONENT": { + "DocumentReferenceInput": { + "Placeholder": "Drop a {type} here to create a reference", + "Warning": { + "WrongType": "The dropped Document must be of type {type}", + "WrongSubtype": "The dropped {type} must be of type {subtype}" + } } }, "GENERIC": { "Unknown": "Unknown", "None": "None", "Default": "Default", + "Document": "Document", "Formula": "Formula", "Value": "Value", "Skill": "Skill", @@ -760,6 +827,9 @@ "Mode": "Mode", "Turns": "Turns", "Rounds": "Rounds", + "Clear": "Clear", + "Name": "Name", + "Level": "Level", "Button": { "Roll": "Roll", "Continue": "Continue", @@ -800,6 +870,7 @@ }, "Item": { "culture": "Culture", + "ancestry": "Ancestry", "path": "Path", "connection": "Connection", "injury": "Injury", diff --git a/src/style.scss b/src/style.scss index 6d5a0470..b7645e03 100644 --- a/src/style.scss +++ b/src/style.scss @@ -1,10 +1,5 @@ -@import './style/sheets/module.scss'; -@import './style/chat/module.scss'; -@import './style/sidebar/combat.scss'; -@import './style/dialog.scss'; -@import './style/components.scss'; +@import './style/module.scss'; -// @import './style/chat/module.scss'; /* ----------------------------------------- */ /* Globals */ /* ----------------------------------------- */ @@ -25,6 +20,9 @@ --plotweaver-color-invest-front: #3e6abb; --plotweaver-color-invest-back: #182845; + // --font-primary: 'Laski Sans'; + // --font-h5: 'Laski-Sans'; + /* ----------------------------------------- */ /* Default Generic Theme */ /* ----------------------------------------- */ @@ -67,29 +65,30 @@ font-family: 'Penumbra Web Pro'; font-style: normal; font-weight: 400; - src: url('../../fonts/penumbra-web-pro/PenumbraWebPro-Serif.woff2') - format('woff2'); + src: url('assets/fonts/penumbra-web-pro/PenumbraWebPro-Serif.woff') + format('woff'); } @font-face { font-family: 'Laski Sans'; font-style: normal; font-weight: 400; - src: url('../../fonts/laski-sans/Laski-Sans-Regular.woff2') format('woff2'); + src: url('assets/fonts/laski-sans/Laski-Sans-Regular.woff2') format('woff2'); } @font-face { font-family: 'Laski Sans'; font-style: normal; font-weight: 600; - src: url('../../fonts/laski-sans/Laski-Sans-SemiBold.woff2') format('woff2'); + src: url('assets/fonts/laski-sans/Laski-Sans-SemiBold.woff2') + format('woff2'); } @font-face { font-family: 'Laski Sans'; font-style: normal; font-weight: 700; - src: url('../../fonts/laski-sans/Laski-Sans-Bold.woff2') format('woff2'); + src: url('assets/fonts/laski-sans/Laski-Sans-Bold.woff2') format('woff2'); } @font-face { @@ -97,6 +96,24 @@ src: url('https://dl.dropboxusercontent.com/scl/fi/9909gen4fd0oveyzfposx/CosmereDingbats-Regular.otf?rlkey=ig6odq9hxyo1st8kt3ujp1czz&st=72qrads3&raw=1'); } +.application h1, +span.document-name { + font-family: 'Penumbra Web Pro'; + font-variant: small-caps; +} + +.application h4 { + font-variant: small-caps; +} + +.application h5 { + font-family: 'Laski Sans'; +} + +.application { + font-family: 'Laski Sans'; +} + em.cosmere-icon, i.cosmere-icon { font-family: 'cosmere-dingbats'; diff --git a/src/style/components.scss b/src/style/components.scss index 3e82df98..416ce0ed 100644 --- a/src/style/components.scss +++ b/src/style/components.scss @@ -29,3 +29,52 @@ app-multi-state-toggle { z-index: 0; } } + +app-document-reference-input { + --input-background-color: var(--color-cool-4); + --input-border-color: transparent; + --input-text-color: var(--color-light-3); + --input-placeholder-color: var(--color-light-4); + --input-focus-outline-color: var(--color-cool-3); + + &[readonly] { + --input-background-color: var(--color-cool-5); + --input-border-color: var(--color-cool-4); + --input-text-color: var(--color-light-4); + } +} + +app-document-reference-input { + height: var(--input-height); + line-height: var(--input-height); + padding: 0 0.5rem; + background: var(--input-background-color); + border: 1px solid var(--input-border-color); + border-radius: 4px; + outline: 1px solid transparent; + color: var(--input-text-color); + user-select: text; + font-size: var(--font-size-14); + transition: outline-color 0.5s; + + &.dragover { + outline: 2px solid var(--input-focus-outline-color); + box-shadow: 0 0 5px var(--color-shadow-primary); + } +} + +app-document-reference-input { + display: flex; + align-items: center; + padding: 0 0.5rem; + + .content { + flex: 1; + } + + .placeholder { + color: var(--input-placeholder-color); + opacity: 0.75; + font-style: italic; + } +} diff --git a/src/style/module.scss b/src/style/module.scss new file mode 100644 index 00000000..218f3120 --- /dev/null +++ b/src/style/module.scss @@ -0,0 +1,5 @@ +@import './style/sheets/module.scss'; +@import './style/chat/module.scss'; +@import './style/sidebar/combat.scss'; +@import './style/dialog.scss'; +@import './style/components.scss'; diff --git a/src/style/sheets/item/module.scss b/src/style/sheets/item/module.scss index 6003a2c5..96bca9ca 100644 --- a/src/style/sheets/item/module.scss +++ b/src/style/sheets/item/module.scss @@ -1,4 +1,46 @@ .sheet.item { + &.side-tabs { + overflow: visible; + + .window-content { + overflow: visible; + } + + .sheet-header { + border-bottom: 1px solid var(--plotweaver-color-grey-5); + } + + nav { + border: none; + flex-direction: column; + position: absolute; + left: 100%; + top: 15%; + + > a { + background: rgba(11, 10, 19, 0.9); + width: 2.88867rem; + height: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid #302831; + border-top-right-radius: 0.3rem; + border-bottom-right-radius: 0.3rem; + + &.active { + background-color: rgb(45 41 77 / 90%); + } + } + } + } + + &:not(.minimized) > .window-header { + .window-title { + visibility: hidden; + } + } + .sheet-content { display: flex; flex-direction: column; @@ -6,9 +48,9 @@ .sheet-header { position: relative; + padding-bottom: 0.5rem; .document-name { - font-weight: bold; font-size: 20pt; cursor: pointer; margin-right: 4rem; @@ -19,6 +61,10 @@ } } + img { + cursor: pointer; + } + .document-type { position: absolute; top: 0; @@ -29,13 +75,29 @@ } } + nav { + a { + label { + margin-left: 0.3rem; + } + } + } + .tab-body { flex: 1; display: flex; + margin-top: 0.5rem; .tab { height: 100%; width: 100%; + max-width: max(80ch, 80%); + margin-left: auto; + margin-right: auto; + } + + table { + margin: 0; } } @@ -44,10 +106,18 @@ width: 100%; } + .description-content-flat { + flex-direction: column; + } + p.notes { color: #efe6d8bf; } + fieldset:not(:first-child) { + margin-top: 0.5rem; + } + .form-group { span.units { color: #efe6d8bf; @@ -187,10 +257,6 @@ app-item-effects-list { } } -app-prose-mirror-editor { - flex: 1; -} - app-item-properties { .properties-list { height: 100%; @@ -337,6 +403,55 @@ app-talent-prerequisites { } } +app-ancestry-advancement-talent-list { + display: block; + border-radius: 0.3rem; + outline: 1px solid transparent; + transition: outline-color 0.5s; + + ul.talent-list { + li { + .col.name { + flex: 1; + } + + .col.level { + width: 10rem; + text-align: center; + } + + &.drop .col { + text-align: center; + font-style: italic; + opacity: 0.5; + } + } + } + + &.dragover { + outline: 2px solid var(--color-cool-3); + box-shadow: 0 0 5px var(--color-shadow-primary); + + ul.talent-list li.drop .col { + opacity: 1; + } + } +} + +app-ancestry-bonus-talents { + .list-table { + .col.quantity, + .col.level { + flex: 1; + text-align: center; + } + + .col.restrictions { + flex: 3; + } + } +} + app-talent-prerequisite-talent-list { display: block; border: 1px dashed #d3d3d342; diff --git a/src/style/sheets/module.scss b/src/style/sheets/module.scss index 539cc74c..0853217d 100644 --- a/src/style/sheets/module.scss +++ b/src/style/sheets/module.scss @@ -4,6 +4,42 @@ @import './actor/module.scss'; @import './item/module.scss'; -.prosemirror menu .pm-dropdown { - color: black; +ul.list-table { + list-style: none; + padding: 0; + margin: 0; + + li { + display: flex; + align-items: center; + background: #302831; + height: 2rem; + padding: 0 0.5rem; + margin: 0; + + &.header { + background: #0f0610; + border-top-left-radius: 0.3rem; + border-top-right-radius: 0.3rem; + font-weight: bold; + } + + &:not(.header):not(:last-child) { + border-bottom: 1px solid #463a47; + } + + &:last-child { + border-bottom-left-radius: 0.3rem; + border-bottom-right-radius: 0.3rem; + } + + .col.controls { + margin-left: 1rem; + + > * { + width: 1rem; + text-align: center; + } + } + } } diff --git a/src/system/applications/actor/character-sheet.ts b/src/system/applications/actor/character-sheet.ts index fa06391e..e957a0a0 100644 --- a/src/system/applications/actor/character-sheet.ts +++ b/src/system/applications/actor/character-sheet.ts @@ -85,8 +85,9 @@ export class CharacterSheet extends BaseActorSheet { paths: pathItems.filter((i) => i.system.type === type), })), - // TODO: Default localization - ancestryLabel: ancestryItem?.name ?? 'DEFAULT_ANCESTRY_LABEL', + ancestryLabel: + ancestryItem?.name ?? + game.i18n?.localize('COSMERE.Item.Type.Ancestry.label'), }; } } diff --git a/src/system/applications/component-system/component.ts b/src/system/applications/component-system/component.ts index 52f7eb42..c221ce0a 100644 --- a/src/system/applications/component-system/component.ts +++ b/src/system/applications/component-system/component.ts @@ -66,6 +66,11 @@ export class HandlebarsApplicationComponent< public static readonly ACTIONS: foundry.applications.api.ApplicationV2.Configuration['actions'] = {}; + /** + * Whether this Component is form associated. + */ + public static FORM_ASSOCIATED = false; + constructor( public readonly id: string, public readonly selector: string, diff --git a/src/system/applications/component-system/mixin.ts b/src/system/applications/component-system/mixin.ts index 0f88f768..b276d4cb 100644 --- a/src/system/applications/component-system/mixin.ts +++ b/src/system/applications/component-system/mixin.ts @@ -7,7 +7,7 @@ import ComponentSystem from './system'; import { HandlebarsApplicationComponent } from './component'; // Types -import { ApplicationV2Constructor, ComponentState } from './types'; +import { ApplicationV2Constructor, ComponentState, PartState } from './types'; const { HandlebarsApplicationMixin } = foundry.applications.api; @@ -167,6 +167,7 @@ export function ComponentHandlebarsApplicationMixin< string, HTMLCollection >; + const partStates = {} as Record; let states = {} as Record; // Extract existing component states @@ -177,9 +178,32 @@ export function ComponentHandlebarsApplicationMixin< }; }); - // Replace parts (if required) + // Replace parts if (Object.keys(parts).length > 0) { - super._replaceHTML(parts, content, options); + Object.entries(parts).forEach(([partId, htmlElement]) => { + // Get part element + const priorElement: HTMLElement = content.querySelector( + `[data-application-part="${partId}"]`, + )!; + const state: Partial = {}; + + if (priorElement) { + super._preSyncPartState( + partId, + htmlElement, + priorElement, + state as PartState, + ); + partStates[partId] = [priorElement, state as PartState]; + + priorElement.replaceWith(htmlElement); + } else { + content.appendChild(htmlElement); + } + + super._attachPartListeners(partId, htmlElement, options); + super.parts[partId] = htmlElement; + }); } // Replace components @@ -187,6 +211,19 @@ export function ComponentHandlebarsApplicationMixin< ComponentSystem.replaceComponent(ref, html), ); + // Apply part states + Object.entries(partStates).forEach( + ([partId, [priorElement, state]]) => { + const htmlElement = parts[partId]; + this._syncPartState( + partId, + htmlElement, + priorElement, + state, + ); + }, + ); + // Apply states Object.entries(states).forEach(([ref, state]) => ComponentSystem.applyComponentState(ref, state), diff --git a/src/system/applications/component-system/system.ts b/src/system/applications/component-system/system.ts index 73e04f14..5ee9cce2 100644 --- a/src/system/applications/component-system/system.ts +++ b/src/system/applications/component-system/system.ts @@ -145,6 +145,18 @@ export function registerComponent( `); }); + + if (componentCls.FORM_ASSOCIATED) { + customElements.define( + selector, + class extends HTMLElement { + static formAssociated = true; + + public value: unknown; + public name: string | undefined; + }, + ); + } } /* --- Helpers --- */ diff --git a/src/system/applications/component-system/types.ts b/src/system/applications/component-system/types.ts index 0adf84c8..8bfbf5a0 100644 --- a/src/system/applications/component-system/types.ts +++ b/src/system/applications/component-system/types.ts @@ -23,6 +23,15 @@ export type ComponentActionHandler = export type ComponentEvent = CustomEvent<{ params: T }>; +export interface PartState { + scrollPositions: [ + element: HTMLElement, + scrollTop: number, + scrollLeft: number, + ][]; + focus?: string; +} + export interface ComponentState { scrollPositions: [ selector: string, diff --git a/src/system/applications/components/document-reference-input.ts b/src/system/applications/components/document-reference-input.ts new file mode 100644 index 00000000..f2a04a16 --- /dev/null +++ b/src/system/applications/components/document-reference-input.ts @@ -0,0 +1,263 @@ +import { ConstructorOf, AnyObject } from '@system/types/utils'; + +// Component imports +import { HandlebarsApplicationComponent } from '@system/applications/component-system'; + +// Mixins +import { DragDropComponentMixin } from '@system/applications/mixins/drag-drop'; + +type DocumentType = (typeof CONST.ALL_DOCUMENT_TYPES)[number]; + +// NOTE: Must use type here instead of interface as an interface doesn't match AnyObject type +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +type Params = { + name?: string; + + /** + * The document UUID value + */ + value?: string; + + /** + * The specific type of document this field can reference (i.e. 'Item') + */ + type?: DocumentType; + + /** + * The specific subtype of document this field can reference (i.e. 'Weapon') + */ + subtype?: string; + + /** + * Whether the field is read-only + */ + readonly?: boolean; + + /** + * Placeholder text for the input + */ + placeholder?: string; +}; + +export class DocumentReferenceInputComponent extends DragDropComponentMixin( + HandlebarsApplicationComponent< + ConstructorOf, + Params + >, +) { + static FORM_ASSOCIATED = true; + + static readonly TEMPLATE = + 'systems/cosmere-rpg/templates/general/components/document-reference-input.hbs'; + + /** + * NOTE: Unbound methods is the standard for defining actions + * within ApplicationV2 + */ + /* eslint-disable @typescript-eslint/unbound-method */ + static ACTIONS = { + clear: this.onClear, + }; + /* eslint-enable @typescript-eslint/unbound-method */ + + static DRAG_DROP = [ + { + dropSelector: '*', + }, + ]; + + private _value = ''; + private _name?: string; + + /* --- Accessors --- */ + + public get element(): + | (HTMLElement & { name?: string; value: string }) + | undefined { + return super.element as unknown as + | (HTMLElement & { name?: string; value: string }) + | undefined; + } + + public get readonly() { + return this.params?.readonly !== false; + } + + public get value() { + return this._value; + } + + public set value(value: string) { + this._value = value; + + // Set value + this.element!.value = value; + + // Dispatch change event + this.element!.dispatchEvent(new Event('change', { bubbles: true })); + } + + public get name() { + return this._name; + } + + public set name(value: string | undefined) { + this._name = value; + + // Set name + this.element!.name = value; + $(this.element!).attr('name', value ?? ''); + } + + /* --- Actions --- */ + + public static async onClear(this: DocumentReferenceInputComponent) { + this._value = ''; + await this.render(); + } + + /* --- Drag drop --- */ + + protected override _canDragDrop() { + return !this.readonly; + } + + protected override _onDragOver(event: DragEvent) { + if (this.readonly) return; + + $(this.element!).addClass('dragover'); + } + + protected override async _onDrop(event: DragEvent) { + if (this.readonly) return; + + // Remove dragover class + $(this.element!).removeClass('dragover'); + + // Get data + const data = TextEditor.getDragEventData(event) as unknown as { + type: string; + uuid: string; + }; + + // Validate type + if (this.params!.type && data.type !== this.params!.type) { + return ui.notifications.warn( + game.i18n!.format( + 'COMPONENT.DocumentReferenceInput.Warning.WrongType', + { + type: this.params!.type, + }, + ), + ); + } + + if (this.params?.type && this.params?.subtype) { + // Get document + const doc = (await fromUuid(data.uuid)) as unknown as { + type: string; + }; + + // Validate subtype + if (doc.type !== this.params.subtype) { + const subtypeLabel = ( + CONFIG as unknown as Record< + DocumentType, + { typeLabels: Record } + > + )[this.params.type].typeLabels[this.params.subtype]; + + return ui.notifications.warn( + game.i18n!.format( + 'COMPONENT.DocumentReferenceInput.Warning.WrongSubtype', + { + type: this.params.type, + subtype: game.i18n!.localize(subtypeLabel), + }, + ), + ); + } + } + + // Set value + this.value = data.uuid; + + // Render + void this.render(); + } + + /* --- Lifecycle --- */ + + protected override _onInitialize() { + if (this.params!.value) { + this._value = this.params!.value ?? ''; + } + } + + public override _onAttachListeners(params: Params) { + super._onAttachListeners(params); + + $(this.element!).on('dragleave', () => { + $(this.element!).removeClass('dragover'); + }); + } + + protected override _onRender(params: Params) { + super._onRender(params); + + // Set name + if (this.params!.name) { + this.name = this.params!.name; + } + + // Set readonly + if (this.params!.readonly) { + $(this.element!).attr('readonly', 'readonly'); + } + } + + /* --- Context --- */ + + public async _prepareContext(params: Params) { + // Look up the document + const doc = this.value + ? ((await fromUuid(this.value)) as ClientDocument | null) + : undefined; + + // Generate content link + const contentLink = doc + ? new Handlebars.SafeString(doc.toAnchor().outerHTML) + : undefined; + + const subtypeLabel = + params.type && params.subtype + ? ( + CONFIG as unknown as Record< + DocumentType, + { typeLabels: Record } + > + )[params.type].typeLabels[params.subtype] + : undefined; + + // Format default placeholder + const defaultPlaceholder = game.i18n!.format( + 'COMPONENT.DocumentReferenceInput.Placeholder', + { + type: game.i18n!.localize( + subtypeLabel ?? params.type ?? 'GENERIC.Document', + ), + }, + ); + + return { + value: this.value, + placeholder: params.placeholder, + readonly: params.readonly, + contentLink, + defaultPlaceholder, + }; + } +} + +// Register the component +DocumentReferenceInputComponent.register('app-document-reference-input'); diff --git a/src/system/applications/components/index.ts b/src/system/applications/components/index.ts index 34ff5b6d..5f47b4cd 100644 --- a/src/system/applications/components/index.ts +++ b/src/system/applications/components/index.ts @@ -1,3 +1,3 @@ -import './prose-mirror-editor'; import './id-input'; import './multi-state-toggle'; +import './document-reference-input'; diff --git a/src/system/applications/components/multi-state-toggle.ts b/src/system/applications/components/multi-state-toggle.ts index 3dcd3f58..99a3a854 100644 --- a/src/system/applications/components/multi-state-toggle.ts +++ b/src/system/applications/components/multi-state-toggle.ts @@ -66,7 +66,6 @@ export class MultiStateToggleComponent extends HandlebarsApplicationComponent< /* --- Lifecyle --- */ protected _onRender(params: Params): void { - console.log('MultiStateToggleComponent._onRender', params); if (params.name) $(this.element!).attr('name', params.name); } diff --git a/src/system/applications/components/prose-mirror-editor.ts b/src/system/applications/components/prose-mirror-editor.ts deleted file mode 100644 index 3450df4d..00000000 --- a/src/system/applications/components/prose-mirror-editor.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { ConstructorOf } from '@system/types/utils'; - -// Component imports -import { HandlebarsApplicationComponent } from '@system/applications/component-system'; - -// NOTE: Must use type here instead of interface as an interface doesn't match AnyObject type -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -type Params = { - document: ClientDocument; - fieldName: string; - - button?: boolean; - collaborate?: boolean; - editable?: boolean; -}; - -export class ProseMirrorEditorComponent extends HandlebarsApplicationComponent< - ConstructorOf< - foundry.applications.api.DocumentSheetV2 - >, - Params -> { - static readonly TEMPLATE = - 'systems/cosmere-rpg/templates/general/components/prose-mirror-editor.hbs'; - - static readonly CLASSES = ['editor', 'prosemirror']; - - /** - * NOTE: Unbound methods is the standard for defining actions - * within ApplicationV2 - */ - /* eslint-disable @typescript-eslint/unbound-method */ - static ACTIONS = { - edit: this.onEdit, - }; - /* eslint-enable @typescript-eslint/unbound-method */ - - private instance?: ProseMirrorEditor; - private active = false; - - /* --- Accessors --- */ - - public get editable(): boolean { - return (this.params!.editable ?? true) && this.application.isEditable; - } - - /* --- Actions --- */ - - private static onEdit(this: ProseMirrorEditorComponent) { - void this.activate(); - } - - /* --- Public functions --- */ - - public async save(options: { remove?: boolean } = {}) { - const { remove = true } = options; - - // Get content - const content = ProseMirror.dom.serializeString( - /** - * NOTE: Casting to `Node` doesn't work here as prosemirror expects - * some internal `Node` type. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - this.instance!.view.state.doc.content as any, - ); - - // Update content - await ( - this.params!.document as unknown as foundry.abstract.Document - ).update({ - [this.params!.fieldName]: content, - }); - - // Close - if (remove) { - this.instance!.destroy(); - this.instance = undefined; - this.active = false; - - // Re-render - await this.render(); - } - } - - /* --- Lifecycle --- */ - - protected override _onRender(params: Params) { - if ((!this.params!.button || this.active) && this.editable) { - this.active = false; - void this.activate(); - } - } - - /* --- Context --- */ - - public async _prepareContext(params: Params) { - // Enrich content - const content = await TextEditor.enrichHTML( - foundry.utils.getProperty( - this.params!.document, - this.params!.fieldName, - ), - ); - - return { - button: params.button ?? false, - editable: this.editable, - content, - active: this.active, - }; - } - - /* --- Helpers --- */ - - private async activate() { - if (this.active) return; - this.active = true; - - // Enrich content - const content = await TextEditor.enrichHTML( - foundry.utils.getProperty( - this.params!.document, - this.params!.fieldName, - ), - ); - - // Hide button - if (this.params!.button) { - this.element!.querySelector('a.editor-edit')!.remove(); - } - - // Get element - const element = this.element!.querySelector('.editor-content')!; - - // Init editor - this.instance = await ProseMirrorEditor.create( - element as HTMLElement, - content, - { - document: this.params!.document, - fieldName: this.params!.fieldName, - collaborate: this.params!.collaborate ?? false, - plugins: this.configurePlugin({ remove: this.params!.button }), - }, - ); - } - - private configurePlugin(options: { remove?: boolean } = {}) { - const { remove = true } = options; - return { - /** - * NOTE: Must assign to `any` here as Foundry extends the ProseMirror - * object with `ProseMirrorMenu` and `ProseMirrorKeyMaps` which are not - * reflected in the types. - */ - /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */ - menu: (ProseMirror as any).ProseMirrorMenu.build( - ProseMirror.defaultSchema, - { - destroyOnSave: remove, - onSave: () => this.save(), - }, - ), - keyMaps: (ProseMirror as any).ProseMirrorKeyMaps.build( - ProseMirror.defaultSchema, - { - onSave: () => this.save(), - }, - ), - /* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */ - }; - } -} - -// Register -ProseMirrorEditorComponent.register('app-prose-mirror-editor'); diff --git a/src/system/applications/item/action-sheet.ts b/src/system/applications/item/action-sheet.ts index b19c59e1..ee375a18 100644 --- a/src/system/applications/item/action-sheet.ts +++ b/src/system/applications/item/action-sheet.ts @@ -24,6 +24,7 @@ export class ActionItemSheet extends BaseItemSheet { { details: { label: 'COSMERE.Item.Sheet.Tabs.Details', + icon: '', sortIndex: 15, }, }, diff --git a/src/system/applications/item/ancestry-sheet.ts b/src/system/applications/item/ancestry-sheet.ts new file mode 100644 index 00000000..ec356115 --- /dev/null +++ b/src/system/applications/item/ancestry-sheet.ts @@ -0,0 +1,57 @@ +import { AncestryItem } from '@system/documents/item'; +import { DeepPartial } from '@system/types/utils'; + +import { BaseItemSheet } from './base'; + +export class AncestrySheet extends BaseItemSheet { + static DEFAULT_OPTIONS = foundry.utils.mergeObject( + foundry.utils.deepClone(super.DEFAULT_OPTIONS), + { + classes: ['cosmere-rpg', 'sheet', 'item', 'ancestry'], + position: { + width: 600, + height: 550, + }, + window: { + resizable: true, + positioned: true, + }, + }, + ); + + static TABS = foundry.utils.mergeObject( + foundry.utils.deepClone(super.TABS), + { + details: { + label: 'COSMERE.Item.Sheet.Tabs.Details', + icon: '', + sortIndex: 15, + }, + }, + ); + + static PARTS = foundry.utils.mergeObject( + foundry.utils.deepClone(super.PARTS), + { + 'sheet-content': { + template: + 'systems/cosmere-rpg/templates/item/ancestry/parts/sheet-content.hbs', + scrollable: ['.tab-body'], + }, + }, + ); + + get item(): AncestryItem { + return super.document; + } + + /* --- Context --- */ + + public async _prepareContext( + options: DeepPartial, + ) { + return { + ...(await super._prepareContext(options)), + }; + } +} diff --git a/src/system/applications/item/armor-sheet.ts b/src/system/applications/item/armor-sheet.ts index 28ad6596..7b56eb54 100644 --- a/src/system/applications/item/armor-sheet.ts +++ b/src/system/applications/item/armor-sheet.ts @@ -30,6 +30,7 @@ export class ArmorItemSheet extends BaseItemSheet { { details: { label: 'COSMERE.Item.Sheet.Tabs.Details', + icon: '', sortIndex: 15, }, }, diff --git a/src/system/applications/item/base.ts b/src/system/applications/item/base.ts index 846a3d5a..fe7e5085 100644 --- a/src/system/applications/item/base.ts +++ b/src/system/applications/item/base.ts @@ -36,9 +36,11 @@ export class BaseItemSheet extends TabsApplicationMixin( { description: { label: 'COSMERE.Item.Sheet.Tabs.Description', + icon: '', }, effects: { label: 'COSMERE.Item.Sheet.Tabs.Effects', + icon: '', }, }, ); @@ -55,6 +57,9 @@ export class BaseItemSheet extends TabsApplicationMixin( form: HTMLFormElement, formData: FormDataExtended, ) { + if (event instanceof SubmitEvent) return; + if (!('name' in event.target!)) return; + if (this.item.isPhysical() && 'system.price.unit' in formData.object) { // Get currency id const [currencyId, denominationId] = ( @@ -244,10 +249,26 @@ export class BaseItemSheet extends TabsApplicationMixin( public async _prepareContext( options: DeepPartial, ) { + let enrichedDescValue = undefined; + if (this.item.hasDescription()) { + if ( + this.item.system.description!.value === + CONFIG.COSMERE.items.types[this.item.type].desc_placeholder + ) { + this.item.system.description!.value = game.i18n!.localize( + this.item.system.description!.value!, + ); + } + enrichedDescValue = await TextEditor.enrichHTML( + this.item.system.description!.value!, + ); + } return { ...(await super._prepareContext(options)), item: this.item, editable: this.isEditable, + descHtml: enrichedDescValue, + sideTabs: game.settings!.get('cosmere-rpg', 'itemSheetSideTabs'), }; } } diff --git a/src/system/applications/item/components/advancement-talent-list.ts b/src/system/applications/item/components/advancement-talent-list.ts new file mode 100644 index 00000000..c128096f --- /dev/null +++ b/src/system/applications/item/components/advancement-talent-list.ts @@ -0,0 +1,242 @@ +import { AnyObject, ConstructorOf } from '@system/types/utils'; + +// Component imports +import { HandlebarsApplicationComponent } from '@system/applications/component-system'; +import { AncestrySheet } from '../ancestry-sheet'; + +// Mixins +import { DragDropComponentMixin } from '@system/applications/mixins/drag-drop'; + +export class AdvancementTalentListComponent extends DragDropComponentMixin( + HandlebarsApplicationComponent>, +) { + static TEMPLATE = + 'systems/cosmere-rpg/templates/item/ancestry/components/advancement-talent-list.hbs'; + + /** + * NOTE: Unbound methods is the standard for defining actions + * within ApplicationV2 + */ + /* eslint-disable @typescript-eslint/unbound-method */ + static ACTIONS = { + 'remove-talent': this.onRemoveTalent, + 'edit-talent': this.onEditTalent, + }; + /* eslint-enable @typescript-eslint/unbound-method */ + + static DRAG_DROP = [ + { + dropSelector: '*', + }, + ]; + + /* --- Actions --- */ + + private static onRemoveTalent( + this: AdvancementTalentListComponent, + event: Event, + ) { + // Get the element + const el = $(event.currentTarget!).closest('.talent-ref'); + + // Get the index + const index = Number(el.data('item')); + + // Get the extra talents from the item + const { extraTalents } = this.application.item.system.advancement; + + // Remove the talent + extraTalents.splice(index, 1); + + // Remove the talent + void this.application.item.update({ + 'system.advancement.extraTalents': extraTalents, + }); + } + + private static onEditTalent( + this: AdvancementTalentListComponent, + event: Event, + ) { + // Get the element + const el = $(event.currentTarget!).closest('.talent-ref'); + + // Get the index + const index = Number(el.data('item')); + + this.editTalent(index); + } + + /* --- Drag drop --- */ + + protected override _canDragDrop() { + return this.application.isEditable; + } + + protected override _onDragOver(event: DragEvent) { + if (!this.application.isEditable) return; + + $(this.element!).addClass('dragover'); + } + + protected override async _onDrop(event: DragEvent) { + if (!this.application.isEditable) return; + + $(this.element!).removeClass('dragover'); + + // Get data + const data = TextEditor.getDragEventData(event) as unknown as { + type: string; + uuid: string; + }; + + // Validate type + if (data.type !== 'Item') { + return ui.notifications.warn( + game.i18n!.localize( + 'COSMERE.Item.Sheet.Ancestry.Component.AdvancementTalentList.Warning.WrongType', + ), + ); + } + + // Get the document + const doc = (await fromUuid(data.uuid)) as unknown as { + type: string; + }; + + // Ensure the document is a talent + if (doc.type !== 'talent') { + return ui.notifications.warn( + game.i18n!.localize( + 'COSMERE.Item.Sheet.Ancestry.Component.AdvancementTalentList.Warning.WrongType', + ), + ); + } + + // Get the talents list + let talents = this.application.item.system.advancement.extraTalents; + + // Append + talents.push({ + uuid: data.uuid, + level: 1, + }); + + // Sort + talents = talents.sort((a, b) => a.level - b.level); + + // Add the talent + await this.application.item.update({ + [`system.advancement.extraTalents`]: talents, + }); + + // Find the index + const index = talents.findIndex((talent) => talent.uuid === data.uuid); + + setTimeout(() => { + this.editTalent(index); + }); + } + + /* --- Lifecycle --- */ + + public override _onAttachListeners(params: never) { + super._onAttachListeners(params); + + $(this.element!).on('dragleave', () => { + $(this.element!).removeClass('dragover'); + }); + } + + /* --- Context --- */ + + public async _prepareContext(params: never, context: AnyObject) { + // Get the extra talents from the item + let { extraTalents } = this.application.item.system.advancement; + + // Sort + extraTalents = extraTalents.sort((a, b) => a.level - b.level); + + // Process uuids to content links + extraTalents = await Promise.all( + extraTalents.map(async (ref) => { + // Get the document + const doc = (await fromUuid( + ref.uuid, + )) as unknown as ClientDocument; + + // Generate content link + const link = new Handlebars.SafeString( + doc.toAnchor().outerHTML, + ); + + return { + ...ref, + link, + }; + }), + ); + + return { + ...context, + extraTalents, + }; + } + + /* --- Helpers --- */ + + protected editTalent(index: number) { + const extraTalents = + this.application.item.system.advancement.extraTalents; + + // Get talent + const talent = extraTalents[index]; + + // Find the new element + const el = $(this.element!).find(`.talent-ref[data-item="${index}"]`); + + // Get the level span + const level = el.find('.col.level span'); + + // Hide the level span + level.hide(); + + // Create the input + const input = $( + ``, + ); + + // Append input + level.after(input); + + setTimeout(() => { + // Focus the input + input.trigger('select'); + input.on('keydown', (event) => { + if (event.which === 13) { + input.trigger('blur'); + } + }); + input.on('blur', () => { + // Get the new level + const newLevel = Number(input.val()); + + // Update the item + if (!isNaN(newLevel) && newLevel !== talent.level) { + // Update + extraTalents[index].level = newLevel; + + void this.application.item.update({ + 'system.advancement.extraTalents': extraTalents, + }); + } else { + // Trigger render + void this.render(); + } + }); + }); + } +} + +// Register the component +AdvancementTalentListComponent.register('app-ancestry-advancement-talent-list'); diff --git a/src/system/applications/item/components/ancestry-bonus-talents.ts b/src/system/applications/item/components/ancestry-bonus-talents.ts new file mode 100644 index 00000000..479070c4 --- /dev/null +++ b/src/system/applications/item/components/ancestry-bonus-talents.ts @@ -0,0 +1,148 @@ +import { BonusTalentsRule } from '@system/data/item/ancestry'; +import { AnyObject, ConstructorOf } from '@system/types/utils'; + +// Dialogs +import { EditBonusTalentsRuleDialog } from '../dialogs/edit-bonus-talents-rule'; + +// Component imports +import { HandlebarsApplicationComponent } from '@system/applications/component-system'; +import { AncestrySheet } from '../ancestry-sheet'; + +export class AncestryBonusTalentsComponent extends HandlebarsApplicationComponent< + ConstructorOf +> { + static TEMPLATE = + 'systems/cosmere-rpg/templates/item/ancestry/components/bonus-talents.hbs'; + + /** + * NOTE: Unbound methods is the standard for defining actions + * within ApplicationV2 + */ + /* eslint-disable @typescript-eslint/unbound-method */ + static ACTIONS = { + 'add-rule': this.onAddRule, + 'edit-rule': this.onEditRule, + 'remove-rule': this.onRemoveRule, + }; + /* eslint-enable @typescript-eslint/unbound-method */ + + /* --- Actions --- */ + + private static async onAddRule( + this: AncestryBonusTalentsComponent, + event: Event, + ) { + // Get bonus talents + const { bonusTalents } = this.application.item.system.advancement; + + // Find the highest level + const highest = bonusTalents.reduce( + (acc, rule) => Math.max(acc, rule.level), + 0, + ); + + // Create a new rule + bonusTalents.push({ + level: highest + 1, + quantity: 1, + restrictions: '', + }); + + // Update the item + await this.application.item.update({ + 'system.advancement.bonusTalents': bonusTalents, + }); + + // Edit the rule + void this.editRule(bonusTalents.length - 1); + } + + private static onEditRule( + this: AncestryBonusTalentsComponent, + event: Event, + ) { + // Get the element + const el = $(event.currentTarget!).closest('li'); + + // Get the index + const index = Number(el.data('index')); + + // Edit the rule + void this.editRule(index); + } + + private static onRemoveRule( + this: AncestryBonusTalentsComponent, + event: Event, + ) { + // Get the element + const el = $(event.currentTarget!).closest('li'); + + // Get the index + const index = Number(el.data('index')); + + // Get the bonus talents + const { bonusTalents } = this.application.item.system.advancement; + + // Remove the rule + bonusTalents.splice(index, 1); + + // Update the item + void this.application.item.update({ + 'system.advancement.bonusTalents': bonusTalents, + }); + } + + /* --- Context --- */ + + public _prepareContext(params: never, context: AnyObject) { + const levels = + this.application.item.system.advancement.bonusTalents.sort( + (a, b) => a.level - b.level, + ); + + return Promise.resolve({ + ...context, + levels, + }); + } + + /* --- Helpers --- */ + + protected async editRule(index: number) { + // Get bonus talents + const { bonusTalents } = this.application.item.system.advancement; + + // Get the rule + const rule = bonusTalents[index]; + + const changes = await EditBonusTalentsRuleDialog.show(rule); + if (changes) { + if (changes.level !== rule.level) { + // Ensure no other rule has the same level + const existing = bonusTalents.find( + (r) => r.level === changes.level, + ); + + if (existing) { + return ui.notifications.warn( + game.i18n!.format( + 'DIALOG.EditBonusTalentsRule.Warning.DuplicateLevel', + { + level: changes.level, + }, + ), + ); + } + } + + bonusTalents[index] = changes; + void this.application.item.update({ + 'system.advancement.bonusTalents': bonusTalents, + }); + } + } +} + +// Register the component +AncestryBonusTalentsComponent.register('app-ancestry-bonus-talents'); diff --git a/src/system/applications/item/components/index.ts b/src/system/applications/item/components/index.ts index a3a1540a..1654fecc 100644 --- a/src/system/applications/item/components/index.ts +++ b/src/system/applications/item/components/index.ts @@ -8,4 +8,6 @@ import './details-attack'; import './details-damage'; import './properties'; import './talent-prerequisites'; +import './advancement-talent-list'; +import './ancestry-bonus-talents'; import './talent-prerequisite-talent-list'; diff --git a/src/system/applications/item/dialogs/edit-bonus-talents-rule.ts b/src/system/applications/item/dialogs/edit-bonus-talents-rule.ts new file mode 100644 index 00000000..aaeb1f85 --- /dev/null +++ b/src/system/applications/item/dialogs/edit-bonus-talents-rule.ts @@ -0,0 +1,117 @@ +import { BonusTalentsRule } from '@system/data/item/ancestry'; +import { AnyObject } from '@system/types/utils'; + +const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; + +export class EditBonusTalentsRuleDialog extends HandlebarsApplicationMixin( + ApplicationV2, +) { + /** + * NOTE: Unbound methods is the standard for defining actions and forms + * within ApplicationV2 + */ + /* eslint-disable @typescript-eslint/unbound-method */ + static DEFAULT_OPTIONS = foundry.utils.mergeObject( + foundry.utils.deepClone(super.DEFAULT_OPTIONS), + { + window: { + title: 'DIALOG.EditBonusTalentsRule.Title', + minimizable: false, + resizable: true, + positioned: true, + }, + classes: ['dialog', 'edit-bonus-talents-rule'], + tag: 'dialog', + position: { + width: 350, + }, + actions: { + update: this.onSubmit, + }, + }, + ); + + static PARTS = foundry.utils.mergeObject( + foundry.utils.deepClone(super.PARTS), + { + form: { + template: + 'systems/cosmere-rpg/templates/item/ancestry/dialogs/edit-bonus-talents-rule.hbs', + forms: { + form: { + handler: this.onFormEvent, + }, + }, + }, + }, + ); + /* eslint-enable @typescript-eslint/unbound-method */ + + private rule: BonusTalentsRule; + private submitted = false; + + private constructor( + rule: BonusTalentsRule, + private resolve: (value: BonusTalentsRule | null) => void, + ) { + super({}); + + this.rule = foundry.utils.deepClone(rule); + } + + /* --- Statics --- */ + + public static show(rule: BonusTalentsRule) { + return new Promise((resolve) => { + const dialog = new this(rule, resolve); + void dialog.render(true); + }); + } + + /* --- Form --- */ + + protected static onFormEvent( + this: EditBonusTalentsRuleDialog, + event: Event, + ) { + event.preventDefault(); + } + + /* --- Actions --- */ + + protected static onSubmit(this: EditBonusTalentsRuleDialog) { + const form = this.element.querySelector('form')! as HTMLFormElement & { + level: HTMLInputElement; + quantity: HTMLInputElement; + restrictions: HTMLInputElement; + }; + + this.rule.level = Number(form.level.value); + this.rule.quantity = Number(form.quantity.value); + this.rule.restrictions = form.restrictions.value; + + this.resolve(this.rule); + this.submitted = true; + void this.close(); + } + + /* --- Lifecycle --- */ + + protected _onRender(context: AnyObject, options: AnyObject): void { + super._onRender(context, options); + + $(this.element).prop('open', true); + } + + protected _onClose() { + if (!this.submitted) this.resolve(null); + } + + /* --- Context --- */ + + protected _prepareContext() { + return Promise.resolve({ + ...this.rule, + }); + } +} diff --git a/src/system/applications/item/equipment-sheet.ts b/src/system/applications/item/equipment-sheet.ts index 395446af..74e06188 100644 --- a/src/system/applications/item/equipment-sheet.ts +++ b/src/system/applications/item/equipment-sheet.ts @@ -25,6 +25,7 @@ export class EquipmentItemSheet extends BaseItemSheet { { details: { label: 'COSMERE.Item.Sheet.Tabs.Details', + icon: '', sortIndex: 15, }, }, diff --git a/src/system/applications/item/index.ts b/src/system/applications/item/index.ts index 88068df7..b810de7c 100644 --- a/src/system/applications/item/index.ts +++ b/src/system/applications/item/index.ts @@ -1,6 +1,7 @@ import './components'; export * from './culture-sheet'; +export * from './ancestry-sheet'; export * from './path-sheet'; export * from './connection-sheet'; export * from './injury-sheet'; diff --git a/src/system/applications/item/injury-sheet.ts b/src/system/applications/item/injury-sheet.ts index 566b5bab..d910ae51 100644 --- a/src/system/applications/item/injury-sheet.ts +++ b/src/system/applications/item/injury-sheet.ts @@ -26,6 +26,7 @@ export class InjuryItemSheet extends BaseItemSheet { { details: { label: 'COSMERE.Item.Sheet.Tabs.Details', + icon: '', sortIndex: 15, }, }, diff --git a/src/system/applications/item/loot-sheet.ts b/src/system/applications/item/loot-sheet.ts index bf4914a5..10f83ee5 100644 --- a/src/system/applications/item/loot-sheet.ts +++ b/src/system/applications/item/loot-sheet.ts @@ -25,6 +25,7 @@ export class LootItemSheet extends BaseItemSheet { { details: { label: 'COSMERE.Item.Sheet.Tabs.Details', + icon: '', sortIndex: 15, }, }, diff --git a/src/system/applications/item/path-sheet.ts b/src/system/applications/item/path-sheet.ts index 7c341a06..0acfe66a 100644 --- a/src/system/applications/item/path-sheet.ts +++ b/src/system/applications/item/path-sheet.ts @@ -25,6 +25,7 @@ export class PathItemSheet extends BaseItemSheet { { details: { label: 'COSMERE.Item.Sheet.Tabs.Details', + icon: '', sortIndex: 15, }, }, diff --git a/src/system/applications/item/specialty-sheet.ts b/src/system/applications/item/specialty-sheet.ts index 44c738aa..9ee7ccdc 100644 --- a/src/system/applications/item/specialty-sheet.ts +++ b/src/system/applications/item/specialty-sheet.ts @@ -25,6 +25,7 @@ export class SpecialtyItemSheet extends BaseItemSheet { { details: { label: 'COSMERE.Item.Sheet.Tabs.Details', + icon: '', sortIndex: 15, }, }, diff --git a/src/system/applications/item/talent-sheet.ts b/src/system/applications/item/talent-sheet.ts index 0d513c98..62dd317f 100644 --- a/src/system/applications/item/talent-sheet.ts +++ b/src/system/applications/item/talent-sheet.ts @@ -34,6 +34,7 @@ export class TalentItemSheet extends BaseItemSheet { { details: { label: 'COSMERE.Item.Sheet.Tabs.Details', + icon: '', sortIndex: 15, }, }, diff --git a/src/system/applications/item/trait-sheet.ts b/src/system/applications/item/trait-sheet.ts index e728d24f..d8ec1f75 100644 --- a/src/system/applications/item/trait-sheet.ts +++ b/src/system/applications/item/trait-sheet.ts @@ -25,6 +25,7 @@ export class TraitItemSheet extends BaseItemSheet { { details: { label: 'COSMERE.Item.Sheet.Tabs.Details', + icon: '', sortIndex: 15, }, }, diff --git a/src/system/applications/item/weapon-sheet.ts b/src/system/applications/item/weapon-sheet.ts index 5c3b6d9f..c20664e9 100644 --- a/src/system/applications/item/weapon-sheet.ts +++ b/src/system/applications/item/weapon-sheet.ts @@ -24,6 +24,7 @@ export class WeaponItemSheet extends BaseItemSheet { { details: { label: 'COSMERE.Item.Sheet.Tabs.Details', + icon: '', sortIndex: 15, }, }, diff --git a/src/system/config.ts b/src/system/config.ts index e29ae927..e78dbf9b 100644 --- a/src/system/config.ts +++ b/src/system/config.ts @@ -377,54 +377,70 @@ const COSMERE: CosmereRPGConfig = { [ItemType.Weapon]: { label: 'COSMERE.Item.Type.Weapon.label', labelPlural: 'COSMERE.Item.Type.Weapon.label_plural', + desc_placeholder: 'COSMERE.Item.Type.Weapon.desc_placeholder', }, [ItemType.Armor]: { label: 'COSMERE.Item.Type.Armor.label', labelPlural: 'COSMERE.Item.Type.Armor.label_plural', + desc_placeholder: 'COSMERE.Item.Type.Armor.desc_placeholder', }, [ItemType.Equipment]: { label: 'COSMERE.Item.Type.Equipment.label', labelPlural: 'COSMERE.Item.Type.Equipment.label_plural', + desc_placeholder: + 'COSMERE.Item.Type.Equipment.desc_placeholder', }, [ItemType.Loot]: { label: 'COSMERE.Item.Type.Loot.label', labelPlural: 'COSMERE.Item.Type.Loot.label_plural', + desc_placeholder: 'COSMERE.Item.Type.Loot.desc_placeholder', }, [ItemType.Ancestry]: { label: 'COSMERE.Item.Type.Ancestry.label', labelPlural: 'COSMERE.Item.Type.Ancestry.label_plural', + desc_placeholder: 'COSMERE.Item.Type.Ancestry.desc_placeholder', }, [ItemType.Culture]: { label: 'COSMERE.Item.Type.Culture.label', labelPlural: 'COSMERE.Item.Type.Culture.label_plural', + desc_placeholder: 'COSMERE.Item.Type.Culture.desc_placeholder', }, [ItemType.Path]: { label: 'COSMERE.Item.Type.Path.label', labelPlural: 'COSMERE.Item.Type.Path.label_plural', + desc_placeholder: 'COSMERE.Item.Type.Path.desc_placeholder', }, [ItemType.Specialty]: { label: 'COSMERE.Item.Type.Specialty.label', labelPlural: 'COSMERE.Item.Type.Specialty.label_plural', + desc_placeholder: + 'COSMERE.Item.Type.Specialty.desc_placeholder', }, [ItemType.Talent]: { label: 'COSMERE.Item.Type.Talent.label', labelPlural: 'COSMERE.Item.Type.Talent.label_plural', + desc_placeholder: 'COSMERE.Item.Type.Talent.desc_placeholder', }, [ItemType.Action]: { label: 'COSMERE.Item.Type.Action.label', labelPlural: 'COSMERE.Item.Type.Action.label_plural', + desc_placeholder: 'COSMERE.Item.Type.Action.desc_placeholder', }, [ItemType.Trait]: { label: 'COSMERE.Item.Type.Trait.label', labelPlural: 'COSMERE.Item.Type.Trait.label_plural', + desc_placeholder: 'COSMERE.Item.Type.Trait.desc_placeholder', }, [ItemType.Injury]: { label: 'COSMERE.Item.Type.Injury.label', labelPlural: 'COSMERE.Item.Type.Injury.label_plural', + desc_placeholder: 'COSMERE.Item.Type.Injury.desc_placeholder', }, [ItemType.Connection]: { label: 'COSMERE.Item.Type.Connection.label', labelPlural: 'COSMERE.Item.Type.Connection.label_plural', + desc_placeholder: + 'COSMERE.Item.Type.Connection.desc_placeholder', }, }, activation: { diff --git a/src/system/data/item/action.ts b/src/system/data/item/action.ts index 1a474269..c5960e81 100644 --- a/src/system/data/item/action.ts +++ b/src/system/data/item/action.ts @@ -35,7 +35,9 @@ export class ActionItemDataModel extends DataModelMixin< {} as Record, ), }), - DescriptionItemMixin(), + DescriptionItemMixin({ + value: 'COSMERE.Item.Type.Action.desc_placeholder', + }), ActivatableItemMixin(), DamagingItemMixin(), ) { diff --git a/src/system/data/item/ancestry.ts b/src/system/data/item/ancestry.ts index d7ce81d2..956f7130 100644 --- a/src/system/data/item/ancestry.ts +++ b/src/system/data/item/ancestry.ts @@ -1,5 +1,5 @@ import { Size, CreatureType } from '@system/types/cosmere'; -import { CosmereItem } from '@system/documents'; +import { CosmereItem, PathItem, TalentItem } from '@system/documents/item'; // Mixins import { DataModelMixin } from '../mixins'; @@ -9,6 +9,17 @@ import { DescriptionItemData, } from './mixins/description'; +interface TalentGrant { + uuid: string; + level: number; +} + +export interface BonusTalentsRule { + level: number; + quantity: number; + restrictions: string; +} + export interface AncestryItemData extends IdItemData, DescriptionItemData { size: Size; type: { @@ -16,6 +27,21 @@ export interface AncestryItemData extends IdItemData, DescriptionItemData { custom?: string | null; subtype?: string | null; }; + advancement: { + extraPath: string; // UUID of the PathItem + + /** + * This is a list of talents that are granted to the character + * at specific levels. + */ + extraTalents: TalentGrant[]; + + /** + * This is the number of bonus talents a character + * with this ancestry can pick at each level. + */ + bonusTalents: BonusTalentsRule[]; + }; } export class AncestryItemDataModel extends DataModelMixin< @@ -25,18 +51,24 @@ export class AncestryItemDataModel extends DataModelMixin< IdItemMixin({ initial: 'none', }), - DescriptionItemMixin(), + DescriptionItemMixin({ + value: 'COSMERE.Item.Type.Ancestry.desc_placeholder', + }), ) { static defineSchema() { return foundry.utils.mergeObject(super.defineSchema(), { - // TODO: Advancements - size: new foundry.data.fields.StringField({ required: true, nullable: false, blank: false, initial: Size.Medium, - choices: Object.keys(CONFIG.COSMERE.sizes), + choices: Object.entries(CONFIG.COSMERE.sizes).reduce( + (acc, [key, config]) => ({ + ...acc, + [key]: config.label, + }), + {}, + ), }), type: new foundry.data.fields.SchemaField({ id: new foundry.data.fields.StringField({ @@ -44,13 +76,64 @@ export class AncestryItemDataModel extends DataModelMixin< nullable: false, blank: false, initial: CreatureType.Humanoid, - choices: Object.keys(CONFIG.COSMERE.creatureTypes), + choices: Object.entries( + CONFIG.COSMERE.creatureTypes, + ).reduce( + (acc, [key, config]) => ({ + ...acc, + [key]: config.label, + }), + {}, + ), }), custom: new foundry.data.fields.StringField({ nullable: true }), subtype: new foundry.data.fields.StringField({ nullable: true, }), }), + advancement: new foundry.data.fields.SchemaField({ + extraPath: new foundry.data.fields.DocumentUUIDField({ + type: 'Item', + }), + extraTalents: new foundry.data.fields.ArrayField( + new foundry.data.fields.SchemaField({ + uuid: new foundry.data.fields.DocumentUUIDField({ + type: 'Item', + }), + level: new foundry.data.fields.NumberField(), + }), + ), + + bonusTalents: new foundry.data.fields.ArrayField( + new foundry.data.fields.SchemaField({ + level: new foundry.data.fields.NumberField({ + required: true, + min: 0, + initial: 0, + }), + quantity: new foundry.data.fields.NumberField({ + required: true, + min: 0, + initial: 0, + }), + restrictions: new foundry.data.fields.StringField(), + }), + ), + }), }); } + + get typeFieldId(): foundry.data.fields.StringField { + return this.schema.fields.type._getField([ + 'id', + ]) as foundry.data.fields.StringField; + } + + get sizeField(): foundry.data.fields.StringField { + return this.schema.fields.size as foundry.data.fields.StringField; + } + + get extraTalents(): TalentGrant[] { + return this.advancement.extraTalents; + } } diff --git a/src/system/data/item/armor.ts b/src/system/data/item/armor.ts index 975713f0..7acf3d09 100644 --- a/src/system/data/item/armor.ts +++ b/src/system/data/item/armor.ts @@ -30,7 +30,9 @@ export class ArmorItemDataModel extends DataModelMixin< IdItemMixin({ initial: 'none', }), - DescriptionItemMixin(), + DescriptionItemMixin({ + value: 'COSMERE.Item.Type.Armor.desc_placeholder', + }), EquippableItemMixin({ equipType: { initial: EquipType.Wear, diff --git a/src/system/data/item/connection.ts b/src/system/data/item/connection.ts index 1aa23593..1d0a850e 100644 --- a/src/system/data/item/connection.ts +++ b/src/system/data/item/connection.ts @@ -17,7 +17,11 @@ export interface ConnectionItemData extends DescriptionItemData {} export class ConnectionItemDataModel extends DataModelMixin< ConnectionItemData, CosmereItem ->(DescriptionItemMixin()) { +>( + DescriptionItemMixin({ + value: 'COSMERE.Item.Type.Connection.desc_placeholder', + }), +) { static defineSchema() { return foundry.utils.mergeObject(super.defineSchema(), {}); } diff --git a/src/system/data/item/culture.ts b/src/system/data/item/culture.ts index 1b7897f1..3800c004 100644 --- a/src/system/data/item/culture.ts +++ b/src/system/data/item/culture.ts @@ -18,7 +18,9 @@ export class CultureItemDataModel extends DataModelMixin< initial: 'none', choices: () => ['none', ...Object.keys(CONFIG.COSMERE.cultures)], }), - DescriptionItemMixin(), + DescriptionItemMixin({ + value: 'COSMERE.Item.Type.Culture.desc_placeholder', + }), ) { static defineSchema() { return foundry.utils.mergeObject(super.defineSchema(), {}); diff --git a/src/system/data/item/equipment.ts b/src/system/data/item/equipment.ts index 94569544..ebaf5649 100644 --- a/src/system/data/item/equipment.ts +++ b/src/system/data/item/equipment.ts @@ -35,7 +35,9 @@ export class EquipmentItemDataModel extends DataModelMixin< {} as Record, ), }), - DescriptionItemMixin(), + DescriptionItemMixin({ + value: 'COSMERE.Item.Type.Equipment.desc_placeholder', + }), PhysicalItemMixin(), ActivatableItemMixin(), ) { diff --git a/src/system/data/item/injury.ts b/src/system/data/item/injury.ts index 0b2c0445..799f7bc9 100644 --- a/src/system/data/item/injury.ts +++ b/src/system/data/item/injury.ts @@ -43,7 +43,9 @@ export class InjuryItemDataModel extends DataModelMixin< {} as Record, ), }), - DescriptionItemMixin(), + DescriptionItemMixin({ + value: 'COSMERE.Item.Type.Injury.desc_placeholder', + }), ) { static defineSchema() { return foundry.utils.mergeObject(super.defineSchema(), { diff --git a/src/system/data/item/loot.ts b/src/system/data/item/loot.ts index 2ac5da62..15faa604 100644 --- a/src/system/data/item/loot.ts +++ b/src/system/data/item/loot.ts @@ -19,7 +19,12 @@ export interface LootItemData extends DescriptionItemData, PhysicalItemData { export class LootItemDataModel extends DataModelMixin< LootItemData, CosmereItem ->(DescriptionItemMixin(), PhysicalItemMixin()) { +>( + DescriptionItemMixin({ + value: 'COSMERE.Item.Type.Loot.desc_placeholder', + }), + PhysicalItemMixin(), +) { static defineSchema() { return foundry.utils.mergeObject(super.defineSchema(), { isMoney: new foundry.data.fields.BooleanField({ diff --git a/src/system/data/item/mixins/description.ts b/src/system/data/item/mixins/description.ts index f9ea8372..2848fd12 100644 --- a/src/system/data/item/mixins/description.ts +++ b/src/system/data/item/mixins/description.ts @@ -8,7 +8,15 @@ export interface DescriptionItemData { }; } -export function DescriptionItemMixin

() { +export interface InitialDescriptionItemValues { + value: string; + short?: string; + chat?: string; +} + +export function DescriptionItemMixin

( + params?: InitialDescriptionItemValues, +) { return ( base: typeof foundry.abstract.TypeDataModel, ) => { @@ -18,11 +26,15 @@ export function DescriptionItemMixin

() { description: new foundry.data.fields.SchemaField({ value: new foundry.data.fields.HTMLField({ label: 'Description', + initial: params?.value ? params.value : '', }), chat: new foundry.data.fields.HTMLField({ label: 'Chat description', + initial: params?.chat ? params.chat : '', + }), + short: new foundry.data.fields.StringField({ + initial: params?.short ? params.short : '', }), - short: new foundry.data.fields.StringField(), }), }); } diff --git a/src/system/data/item/path.ts b/src/system/data/item/path.ts index f8ab8e61..ffbfe4c3 100644 --- a/src/system/data/item/path.ts +++ b/src/system/data/item/path.ts @@ -32,7 +32,9 @@ export class PathItemDataModel extends DataModelMixin< ); }, }), - DescriptionItemMixin(), + DescriptionItemMixin({ + value: 'COSMERE.Item.Type.Path.desc_placeholder', + }), ) { static defineSchema() { return foundry.utils.mergeObject(super.defineSchema(), { diff --git a/src/system/data/item/specialty.ts b/src/system/data/item/specialty.ts index 8d9ebeaa..05ae05b6 100644 --- a/src/system/data/item/specialty.ts +++ b/src/system/data/item/specialty.ts @@ -24,13 +24,19 @@ export interface SpecialtyItemData extends IdItemData, DescriptionItemData { export class SpecialtyItemDataModel extends DataModelMixin< SpecialtyItemData, CosmereItem ->(IdItemMixin(), DescriptionItemMixin()) { +>( + IdItemMixin({ initialFromName: true }), + DescriptionItemMixin({ + value: 'COSMERE.Item.Type.Specialty.desc_placeholder', + }), +) { static defineSchema() { return foundry.utils.mergeObject(super.defineSchema(), { path: new foundry.data.fields.StringField({ required: true, nullable: false, blank: false, + initial: 'parent-path', }), hasPath: new foundry.data.fields.BooleanField(), }); diff --git a/src/system/data/item/talent.ts b/src/system/data/item/talent.ts index 8701cc61..792ac0b6 100644 --- a/src/system/data/item/talent.ts +++ b/src/system/data/item/talent.ts @@ -91,7 +91,9 @@ export class TalentItemDataModel extends DataModelMixin< {} as Record, ), }), - DescriptionItemMixin(), + DescriptionItemMixin({ + value: 'COSMERE.Item.Type.Talent.desc_placeholder', + }), ActivatableItemMixin(), ) { static defineSchema() { diff --git a/src/system/data/item/trait.ts b/src/system/data/item/trait.ts index 5741c9f5..47472103 100644 --- a/src/system/data/item/trait.ts +++ b/src/system/data/item/trait.ts @@ -22,7 +22,12 @@ export interface TraitItemData export class TraitItemDataModel extends DataModelMixin< TraitItemData, CosmereItem ->(DescriptionItemMixin(), ActivatableItemMixin()) { +>( + DescriptionItemMixin({ + value: 'COSMERE.Item.Type.Trait.desc_placeholder', + }), + ActivatableItemMixin(), +) { static defineSchema() { return foundry.utils.mergeObject(super.defineSchema(), {}); } diff --git a/src/system/data/item/weapon.ts b/src/system/data/item/weapon.ts index e9fd1829..382eff3b 100644 --- a/src/system/data/item/weapon.ts +++ b/src/system/data/item/weapon.ts @@ -57,7 +57,9 @@ export class WeaponItemDataModel extends DataModelMixin< {} as Record, ), }), - DescriptionItemMixin(), + DescriptionItemMixin({ + value: 'COSMERE.Item.Type.Weapon.desc_placeholder', + }), EquippableItemMixin({ equipType: { initial: EquipType.Hold, diff --git a/src/system/documents/item.ts b/src/system/documents/item.ts index 96d9437e..202f60f2 100644 --- a/src/system/documents/item.ts +++ b/src/system/documents/item.ts @@ -950,6 +950,7 @@ export namespace CosmereItem { } export type CultureItem = CosmereItem; +export type AncestryItem = CosmereItem; export type PathItem = CosmereItem; export type ConnectionItem = CosmereItem; export type InjuryItem = CosmereItem; diff --git a/src/system/hooks/index.ts b/src/system/hooks/index.ts index fbba95b1..45b29610 100644 --- a/src/system/hooks/index.ts +++ b/src/system/hooks/index.ts @@ -1,2 +1,3 @@ import './modules/dice-so-nice'; import './welcome'; +import './sheets'; diff --git a/src/system/hooks/sheets.ts b/src/system/hooks/sheets.ts new file mode 100644 index 00000000..cec5f551 --- /dev/null +++ b/src/system/hooks/sheets.ts @@ -0,0 +1,10 @@ +import { BaseItemSheet } from '../applications/item/base'; + +Hooks.on( + 'renderItemSheetV2', + (itemSheet: BaseItemSheet, node: HTMLFormElement) => { + if (game.settings!.get('cosmere-rpg', 'itemSheetSideTabs')) { + node.classList.add('side-tabs'); + } + }, +); diff --git a/src/system/settings.ts b/src/system/settings.ts index c69b5c79..756a6d5c 100644 --- a/src/system/settings.ts +++ b/src/system/settings.ts @@ -14,4 +14,13 @@ export function registerSettings() { default: '0.0.0', type: String, }); + + game.settings!.register('cosmere-rpg', 'itemSheetSideTabs', { + name: 'Vertical Side Tabs for Item Sheets', + hint: 'Toggle whether Item sheets should use vertical tabs down the right-hand side, similar to the character sheet, or leave the in-line horizontal ones (default).', + scope: 'world', + config: true, + type: Boolean, + default: false, + }); } diff --git a/src/system/types/config.ts b/src/system/types/config.ts index 5d78f8b5..19491101 100644 --- a/src/system/types/config.ts +++ b/src/system/types/config.ts @@ -185,6 +185,7 @@ export interface DamageTypeConfig { export interface ItemTypeConfig { label: string; labelPlural: string; + desc_placeholder?: string; } export interface EquipTypeConfig { diff --git a/src/system/util/handlebars/index.ts b/src/system/util/handlebars/index.ts index def55d93..ed093ff8 100644 --- a/src/system/util/handlebars/index.ts +++ b/src/system/util/handlebars/index.ts @@ -473,6 +473,7 @@ export async function preloadHandlebarsTemplates() { 'systems/cosmere-rpg/templates/item/specialty/partials/specialty-details-tab.hbs', 'systems/cosmere-rpg/templates/item/loot/partials/loot-details-tab.hbs', 'systems/cosmere-rpg/templates/item/armor/partials/armor-details-tab.hbs', + 'systems/cosmere-rpg/templates/item/ancestry/partials/ancestry-details-tab.hbs', 'systems/cosmere-rpg/templates/item/talent/partials/talent-details-tab.hbs', 'systems/cosmere-rpg/templates/combat/combatant.hbs', 'systems/cosmere-rpg/templates/chat/parts/roll-details.hbs', diff --git a/src/templates/actors/parts/navigation.hbs b/src/templates/actors/parts/navigation.hbs index 444006d5..f6fddc48 100644 --- a/src/templates/actors/parts/navigation.hbs +++ b/src/templates/actors/parts/navigation.hbs @@ -1 +1 @@ -{{>tabs tabs=tabs includeLabel=false}} \ No newline at end of file +{{>tabs tabs=tabs ignoreLabel=true}} \ No newline at end of file diff --git a/src/templates/general/components/document-reference-input.hbs b/src/templates/general/components/document-reference-input.hbs new file mode 100644 index 00000000..57c7b1b0 --- /dev/null +++ b/src/templates/general/components/document-reference-input.hbs @@ -0,0 +1,22 @@ +

+ {{#if value}} + {{contentLink}} + {{else}} + + {{#if readonly}} + — + {{else}} + {{localize (default placeholder defaultPlaceholder)}} + {{/if}} + + {{/if}} +
+{{#if (not readonly)}} + +{{/if}} \ No newline at end of file diff --git a/src/templates/general/components/prose-mirror-editor.hbs b/src/templates/general/components/prose-mirror-editor.hbs deleted file mode 100644 index e054a06e..00000000 --- a/src/templates/general/components/prose-mirror-editor.hbs +++ /dev/null @@ -1,10 +0,0 @@ -{{#if (and editable button (not active))}} - - - -{{/if}} -
- {{#if (not active)}} - {{{content}}} - {{/if}} -
\ No newline at end of file diff --git a/src/templates/general/tabs.hbs b/src/templates/general/tabs.hbs index c08c716d..fb73c74c 100644 --- a/src/templates/general/tabs.hbs +++ b/src/templates/general/tabs.hbs @@ -8,13 +8,13 @@ data-tooltip="{{localize tab.label}}" {{/if}} > - {{#if (not (eq ../includeLabel false))}} - - {{/if}} - {{#if tab.icon}} {{{tab.icon}}} {{/if}} + + {{#if (not ../ignoreLabel)}} + + {{/if}} {{/each}} \ No newline at end of file diff --git a/src/templates/item/ancestry/components/advancement-talent-list.hbs b/src/templates/item/ancestry/components/advancement-talent-list.hbs new file mode 100644 index 00000000..c67a6002 --- /dev/null +++ b/src/templates/item/ancestry/components/advancement-talent-list.hbs @@ -0,0 +1,49 @@ +
    +
  • +
    + {{localize "TYPES.Item.talent"}} +
    +
    + {{localize "COSMERE.Sheet.Talents.AcquiredAtLevel"}} +
    +
    + {{#if editable}} +
    +
    + {{/if}} +
    +
  • + + {{#each extraTalents as |ref index|}} +
  • +
    + {{ref.link}} +
    +
    + {{ref.level}} +
    +
    + {{#if @root.editable}} + + + + + + + {{/if}} +
    +
  • + {{/each}} + + {{#if editable}} +
  • +
    + {{localize "COSMERE.Item.Sheet.Ancestry.Component.AdvancementTalentList.DropTalent"}} +
    +
  • + {{/if}} +
\ No newline at end of file diff --git a/src/templates/item/ancestry/components/bonus-talents.hbs b/src/templates/item/ancestry/components/bonus-talents.hbs new file mode 100644 index 00000000..d6d54aad --- /dev/null +++ b/src/templates/item/ancestry/components/bonus-talents.hbs @@ -0,0 +1,47 @@ +
    +
  • +
    + {{localize "GENERIC.Level"}} +
    +
    + # +
    +
    + {{localize "COSMERE.Item.Sheet.Ancestry.Restrictions"}} +
    +
    + {{#if editable}} +
    + + + + {{/if}} +
    +
  • + + {{#each levels as |rule index|}} +
  • +
    + {{rule.level}} +
    +
    + {{rule.quantity}} +
    +
    + {{default rule.restrictions '—'}} +
    +
    + {{#if @root.editable}} + + + + + + + {{/if}} +
    +
  • + {{/each}} +
\ No newline at end of file diff --git a/src/templates/item/ancestry/dialogs/edit-bonus-talents-rule.hbs b/src/templates/item/ancestry/dialogs/edit-bonus-talents-rule.hbs new file mode 100644 index 00000000..a64cb9e8 --- /dev/null +++ b/src/templates/item/ancestry/dialogs/edit-bonus-talents-rule.hbs @@ -0,0 +1,20 @@ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
\ No newline at end of file diff --git a/src/templates/item/ancestry/partials/ancestry-details-tab.hbs b/src/templates/item/ancestry/partials/ancestry-details-tab.hbs new file mode 100644 index 00000000..e56435d6 --- /dev/null +++ b/src/templates/item/ancestry/partials/ancestry-details-tab.hbs @@ -0,0 +1,40 @@ +{{#with (lookup tabsMap "details") as |tab|}} + +
+
+ {{localize "COSMERE.Item.Sheet.Basics"}} + {{app-item-details-id}} +
+ + {{formInput @root.item.system.typeFieldId value=@root.item.system.type.id localize=true}} +
+
+ + {{formInput @root.item.system.sizeField value=@root.item.system.size localize=true}} +
+
+
+ + + {{localize "COSMERE.Item.Sheet.Ancestry.Advancement"}} + +
+ + {{app-document-reference-input + name="system.advancement.extraPath" + value=@root.item.system.advancement.extraPath + type="Item" + subtype="path" + placeholder="COSMERE.Item.Sheet.Ancestry.PathReferencePlaceholder" + readonly=(not @root.editable) + }} +
+
{{localize "COSMERE.Item.Sheet.Ancestry.FreeTalents"}}
+ {{app-ancestry-advancement-talent-list}} + +
{{localize "COSMERE.Item.Sheet.Ancestry.BonusTalents"}}
+ {{app-ancestry-bonus-talents}} +
+
+ +{{/with}} \ No newline at end of file diff --git a/src/templates/item/ancestry/parts/sheet-content.hbs b/src/templates/item/ancestry/parts/sheet-content.hbs new file mode 100644 index 00000000..a9dad629 --- /dev/null +++ b/src/templates/item/ancestry/parts/sheet-content.hbs @@ -0,0 +1,9 @@ +
+ {{app-item-header}} + {{> tabs tabs=tabs ignoreLabel=sideTabs}} +
+ {{> item-description-tab}} + {{> ancestry-details-tab}} + {{> item-effects-tab}} +
+
\ No newline at end of file diff --git a/src/templates/item/armor/parts/sheet-content.hbs b/src/templates/item/armor/parts/sheet-content.hbs index ae2b4ecb..0b732f29 100644 --- a/src/templates/item/armor/parts/sheet-content.hbs +++ b/src/templates/item/armor/parts/sheet-content.hbs @@ -1,6 +1,6 @@
{{app-item-header}} - {{> tabs tabs=tabs}} + {{> tabs tabs=tabs ignoreLabel=sideTabs}}
{{> item-description-tab}} {{> armor-details-tab}} diff --git a/src/templates/item/injury/parts/sheet-content.hbs b/src/templates/item/injury/parts/sheet-content.hbs index f43ebcb2..040a8660 100644 --- a/src/templates/item/injury/parts/sheet-content.hbs +++ b/src/templates/item/injury/parts/sheet-content.hbs @@ -1,6 +1,6 @@
{{app-item-header}} - {{> tabs tabs=tabs}} + {{> tabs tabs=tabs ignoreLabel=sideTabs}}
{{> item-description-tab}} {{> injury-details-tab}} diff --git a/src/templates/item/loot/parts/sheet-content.hbs b/src/templates/item/loot/parts/sheet-content.hbs index 345d2e86..5c529c43 100644 --- a/src/templates/item/loot/parts/sheet-content.hbs +++ b/src/templates/item/loot/parts/sheet-content.hbs @@ -1,6 +1,6 @@
{{app-item-header}} - {{> tabs tabs=tabs}} + {{> tabs tabs=tabs ignoreLabel=sideTabs}}
{{> item-description-tab}} {{> loot-details-tab}} diff --git a/src/templates/item/partials/item-description-tab.hbs b/src/templates/item/partials/item-description-tab.hbs index b2508b30..62c47e0c 100644 --- a/src/templates/item/partials/item-description-tab.hbs +++ b/src/templates/item/partials/item-description-tab.hbs @@ -2,12 +2,19 @@
{{app-item-properties}} - {{app-prose-mirror-editor - button=true - document=@root.item - fieldName="system.description.value" - collaborate=true - }} + {{#if @root.editable}} + + {{{@root.descHtml}}} + + {{else}} +
+ {{{@root.descHtml}}} +
+ {{/if}}
{{/with}} \ No newline at end of file diff --git a/src/templates/item/parts/sheet-content.hbs b/src/templates/item/parts/sheet-content.hbs index e8c53e56..83ce0d4d 100644 --- a/src/templates/item/parts/sheet-content.hbs +++ b/src/templates/item/parts/sheet-content.hbs @@ -1,6 +1,6 @@
{{app-item-header}} - {{> tabs tabs=tabs}} + {{> tabs tabs=tabs ignoreLabel=sideTabs}}
{{> item-description-tab}} {{> item-details-tab}} diff --git a/src/templates/item/specialty/parts/sheet-content.hbs b/src/templates/item/specialty/parts/sheet-content.hbs index c3354056..7cd37d43 100644 --- a/src/templates/item/specialty/parts/sheet-content.hbs +++ b/src/templates/item/specialty/parts/sheet-content.hbs @@ -1,6 +1,6 @@
{{app-item-header}} - {{> tabs tabs=tabs}} + {{> tabs tabs=tabs ignoreLabel=sideTabs}}
{{> item-description-tab}} {{> specialty-details-tab}} diff --git a/src/templates/item/talent/parts/sheet-content.hbs b/src/templates/item/talent/parts/sheet-content.hbs index bda02da1..bff47c07 100644 --- a/src/templates/item/talent/parts/sheet-content.hbs +++ b/src/templates/item/talent/parts/sheet-content.hbs @@ -1,6 +1,6 @@
{{app-item-header}} - {{> tabs tabs=tabs}} + {{> tabs tabs=tabs ignoreLabel=sideTabs}}
{{> item-description-tab}} {{> talent-details-tab}}