Skip to content

Commit

Permalink
[#681] Assign Spells to Source Class (#685)
Browse files Browse the repository at this point in the history
* Added application for assigning spells to source classes.

Added needed loc keys.

Added Tools button menu to character / npc spellbook tab toolbars.

* Removed TidyHooks comment. That's not the right place or foundry hooks that I'm using but not broadcasting.
  • Loading branch information
kgar authored Jul 24, 2024
1 parent 8eadccd commit e725d3b
Show file tree
Hide file tree
Showing 7 changed files with 308 additions and 1 deletion.
5 changes: 5 additions & 0 deletions public/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -808,8 +808,10 @@
"TIDY5E.ItemFilters.Filter.Other": "Other",
"TIDY5E.GMOnly.Message": "GM Only: {message}",
"TIDY5E.ReminderToBackUp": "Always remember to back up your world.",
"TIDY5E.Utilities.Tools": "Tools",
"TIDY5E.Utilities.GMTools": "GM Tools",
"TIDY5E.Utilities.IdentifyAll": "Identify All Items",
"TIDY5E.Utilities.AssignSpellsToClasses": "Assign Spells to Classes",
"TIDY5E.GenericErrorNotification": "An error occurred while performing this action. See the devtools console for more details.",
"TIDY5E.Utilities.MarkAllAsUnidentified": "Mark All Items as Unidentified",
"TIDY5E.Utilities.ConfigureSections": "Configure Sections",
Expand All @@ -830,5 +832,8 @@
"text": "Are you sure you wish to use default?"
},
"TIDY5E.RollRecharge.Hint": "{rechargeLabel} (Shift+Click to skip the roll and instantly recharge) ",
"TIDY5E.SpellSourceClassAssignments.Identifier": "Source Class Identifier",
"TIDY5E.SpellSourceClassAssignments.IdentifierHint": "The spell's source class identifier appears here. To assign an identifier which the character doesn't own, enter the class identifier in this text field.",
"TIDY5E.SpellSourceClassAssignments.ShowUnassignedOnly.Text": "Show Unassigned Only",
"TIDY5E.LocalizationTestKey": "Localization Test Key"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<script lang="ts">
import type { Readable, Writable } from 'svelte/store';
import type { SpellSourceClassAssignmentsContext } from './SpellSourceClassAssignmentsFormApplication';
import { getContext } from 'svelte';
import { CONSTANTS } from 'src/constants';
import Search from 'src/components/utility-bar/Search.svelte';
import { FoundryAdapter } from 'src/foundry/foundry-adapter';
import TidyTable from 'src/components/table/TidyTable.svelte';
import type { Item5e } from 'src/types/item.types';
import TidyTableHeaderCell from 'src/components/table/TidyTableHeaderCell.svelte';
import TidyTableHeaderRow from 'src/components/table/TidyTableHeaderRow.svelte';
import TidyTableRow from 'src/components/table/TidyTableRow.svelte';
import TidyTableCell from 'src/components/table/TidyTableCell.svelte';
import TidySwitch from 'src/components/toggle/TidySwitch.svelte';
import TextInput from 'src/components/inputs/TextInput.svelte';
let context = getContext<Writable<SpellSourceClassAssignmentsContext>>(
CONSTANTS.SVELTE_CONTEXT.CONTEXT,
);
let searchCriteria: string = '';
$: visibleSelectablesIdSubset = new Set<string>(
$context.assignments
.filter(
(s) =>
searchCriteria.trim() === '' ||
s.item.name?.toLowerCase().includes(searchCriteria.toLowerCase()),
)
.map((d) => d.item.id),
);
$: classColumns = Object.entries<Item5e>(
$context.actor.spellcastingClasses,
).map(([key, value]) => ({
key: key,
item: value,
}));
let gridTemplateColumns: string = '';
$: {
let standardClassColumnWidth = '10rem';
let columns = '/* Spell Name */ minmax(200px, 1fr)';
classColumns.forEach((column) => {
columns += ` /* ${column.item.name} */ ${standardClassColumnWidth}`;
});
columns += ' /* Identifier */ 200px';
gridTemplateColumns = columns;
}
async function setItemSourceClass(item: Item5e, sourceClass: string) {
await item.update({
'system.sourceClass': sourceClass,
});
}
const localize = FoundryAdapter.localize;
var showUnassignedOnly = false;
</script>

<section class="flex-column small-gap full-height">
<div role="presentation" class="flex-row small-gap">
<Search bind:value={searchCriteria} />
<label class="flex-row extra-small-gap align-items-center">
<input type="checkbox" bind:checked={showUnassignedOnly} />
{localize('TIDY5E.SpellSourceClassAssignments.ShowUnassignedOnly.Text')}
</label>
</div>
<div role="presentation" class="scroll-container flex-1">
<TidyTable
key="spell-source-class-assignments-matrix"
toggleable={false}
--grid-template-columns={gridTemplateColumns}
>
<svelte:fragment slot="header">
<TidyTableHeaderRow>
<TidyTableHeaderCell primary={true} class="p-1 capitalize">
{localize('DND5E.spell')}
</TidyTableHeaderCell>
{#each classColumns as classColumn}
<TidyTableHeaderCell>
{classColumn.item.name}
</TidyTableHeaderCell>
{/each}
<TidyTableHeaderCell class="flex-row small-gap">
<span
>{localize('TIDY5E.SpellSourceClassAssignments.Identifier')}</span
>
<i
class="fas fa-question-circle"
title={localize(
'TIDY5E.SpellSourceClassAssignments.IdentifierHint',
)}
></i>
</TidyTableHeaderCell>
</TidyTableHeaderRow>
</svelte:fragment>
<svelte:fragment slot="body">
{#each $context.assignments as assignment (assignment.item.id)}
{@const sourceClassIsUnassigned =
(assignment.item.system.sourceClass?.trim() ?? '') === ''}
{@const hideRow =
!visibleSelectablesIdSubset.has(assignment.item.id) ||
(showUnassignedOnly && !sourceClassIsUnassigned)}
<TidyTableRow hidden={hideRow}>
<TidyTableCell primary={true} class="p-1 semibold">
<button
type="button"
class="inline-transparent-button highlight-on-hover"
on:click={async () =>
FoundryAdapter.renderSheetFromUuid(assignment.item.uuid)}
>
{assignment.item.name}
</button>
</TidyTableCell>
{#each classColumns as classColumn}
{@const selected =
assignment.item.system.sourceClass === classColumn.key}
<TidyTableHeaderCell>
<TidySwitch
value={selected}
on:change={() =>
setItemSourceClass(
assignment.item,
selected ? '' : classColumn.key,
)}
/>
</TidyTableHeaderCell>
{/each}
<TidyTableCell>
<TextInput
document={assignment.item}
disabled={!assignment.item.isOwner}
field="system.sourceClass"
selectOnFocus={true}
value={assignment.item.system.sourceClass}
/>
</TidyTableCell>
</TidyTableRow>
{/each}
</svelte:fragment>
</TidyTable>
</div>
</section>
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type { SvelteComponent } from 'svelte';
import AssignSpellsToSourceClasses from './SpellSourceClassAssignments.svelte';
import SvelteFormApplicationBase from '../SvelteFormApplicationBase';
import type { Actor5e } from 'src/types/types';
import { writable, type Writable } from 'svelte/store';
import type { Item5e } from 'src/types/item.types';
import { CONSTANTS } from 'src/constants';
import { StoreSubscriptionsService } from 'src/features/store/StoreSubscriptionsService';
import { FoundryAdapter } from 'src/foundry/foundry-adapter';

export type SpellSourceClassAssignment = {
/**
* The spell to receive an assignment.
*/
item: Item5e;
/**
* Represents the chosen source class.
*/
sourceClass: string;
};

export type SpellSourceClassAssignmentsContext = {
actor: Actor5e;
assignments: SpellSourceClassAssignment[];
};

export default class SpellSourceClassAssignmentsFormApplication extends SvelteFormApplicationBase {
context: Writable<SpellSourceClassAssignmentsContext> = writable();
actor: Actor5e;
subscriptionsService: StoreSubscriptionsService;
updateHook: number | undefined;

constructor(actor: Actor5e, ...args: any[]) {
super(...args);
this.actor = actor;
this.subscriptionsService = new StoreSubscriptionsService();
}

createComponent(node: HTMLElement): SvelteComponent {
this.context.set(this.getData());

return new AssignSpellsToSourceClasses({
target: node,
context: new Map<any, any>([
['appId', this.appId],
['context', this.context],
]),
});
}

getData(): SpellSourceClassAssignmentsContext {
return {
actor: this.actor,
assignments: this.actor.items
.filter((item: Item5e) => item.type === CONSTANTS.ITEM_TYPE_SPELL)
.map((item: Item5e) => ({
item,
sourceClass: 'test',
})),
};
}

activateListeners(html: any): void {
Hooks.off('updateItem', this.updateHook);
this.trackActorChanges();
super.activateListeners(html);
}

private trackActorChanges() {
this.updateHook = Hooks.on('updateItem', (item: Item5e) => {
if (item.actor?.id !== this.actor.id) {
return;
}

this.context.set(this.getData());
});
}

get title() {
return FoundryAdapter.localize('TIDY5E.Utilities.AssignSpellsToClasses');
}

close(options: unknown = {}) {
Hooks.off('updateItem', this.updateHook);
return super.close(options);
}
}
3 changes: 3 additions & 0 deletions src/foundry/foundry-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,9 @@ export const FoundryAdapter = {
true
);
},
async renderSheetFromUuid(uuid: string) {
(await fromUuid(uuid))?.sheet?.render(true);
},
renderImagePopout(...args: any[]) {
return new ImagePopout(...args).render(true);
},
Expand Down
18 changes: 17 additions & 1 deletion src/scss/core.scss
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@
border: 0.125rem solid var(--t5e-separator-color);
border-radius: 0.25rem;
box-shadow: 0 0 0.25rem var(--dnd5e-shadow-45);

+ li {
margin-top: 0.25rem;
}
Expand Down Expand Up @@ -379,6 +379,22 @@
margin: 0.25rem 0;
}
}

/* Spacing utilities */

// TODO: "draw more inspiration" from https://getbootstrap.com/docs/5.3/utilities/spacing/
.p-1 {
padding: 0.25rem;
}

/* Text utilities */
.capitalize {
text-transform: capitalize;
}

.semibold {
font-weight: 500;
}
}

@import './partials/floating-context-menu';
23 changes: 23 additions & 0 deletions src/sheets/character/tabs/CharacterSpellbookTab.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
import { TidyFlags } from 'src/foundry/TidyFlags';
import { SheetSections } from 'src/features/sections/SheetSections';
import { ItemVisibility } from 'src/features/sections/ItemVisibility';
import ButtonMenu from 'src/components/button-menu/ButtonMenu.svelte';
import ButtonMenuCommand from 'src/components/button-menu/ButtonMenuCommand.svelte';
import SpellSourceClassAssignmentsFormApplication from 'src/applications/spell-source-class-assignments/SpellSourceClassAssignmentsFormApplication';
let context = getContext<Readable<CharacterSheetContext>>(
CONSTANTS.SVELTE_CONTEXT.CONTEXT,
Expand Down Expand Up @@ -100,6 +103,26 @@
)}
/>
<FilterMenu {tabId} />
<ButtonMenu
iconClass="ra ra-fairy-wand"
buttonClass="inline-icon-button"
position="bottom"
anchor="right"
title={localize('TIDY5E.Utilities.Tools')}
menuElement="div"
>
<ButtonMenuCommand
on:click={() => {
new SpellSourceClassAssignmentsFormApplication($context.actor).render(
true,
);
}}
iconClass="fas fa-list-check"
disabled={!$context.editable}
>
{localize('TIDY5E.Utilities.AssignSpellsToClasses')}
</ButtonMenuCommand>
</ButtonMenu>
{#each utilityBarCommands as command (command.title)}
<UtilityToolbarCommand
title={command.title}
Expand Down
26 changes: 26 additions & 0 deletions src/sheets/npc/tabs/NpcSpellbookTab.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
import { SheetSections } from 'src/features/sections/SheetSections';
import { ItemVisibility } from 'src/features/sections/ItemVisibility';
import { CONSTANTS } from 'src/constants';
import ButtonMenu from 'src/components/button-menu/ButtonMenu.svelte';
import { FoundryAdapter } from 'src/foundry/foundry-adapter';
import ButtonMenuCommand from 'src/components/button-menu/ButtonMenuCommand.svelte';
import SpellSourceClassAssignmentsFormApplication from 'src/applications/spell-source-class-assignments/SpellSourceClassAssignmentsFormApplication';
let context = getContext<Readable<NpcSheetContext>>(
CONSTANTS.SVELTE_CONTEXT.CONTEXT,
Expand Down Expand Up @@ -49,6 +53,8 @@
$: utilityBarCommands =
$context.utilities[tabId]?.utilityToolbarCommands ?? [];
const localize = FoundryAdapter.localize;
</script>

<UtilityToolbar>
Expand All @@ -62,6 +68,26 @@
)}
/>
<FilterMenu {tabId} />
<ButtonMenu
iconClass="ra ra-fairy-wand"
buttonClass="inline-icon-button"
position="bottom"
anchor="right"
title={localize('TIDY5E.Utilities.Tools')}
menuElement="div"
>
<ButtonMenuCommand
on:click={() => {
new SpellSourceClassAssignmentsFormApplication($context.actor).render(
true,
);
}}
iconClass="fas fa-list-check"
disabled={!$context.editable}
>
{localize('TIDY5E.Utilities.AssignSpellsToClasses')}
</ButtonMenuCommand>
</ButtonMenu>
{#each utilityBarCommands as command (command.title)}
<UtilityToolbarCommand
title={command.title}
Expand Down

0 comments on commit e725d3b

Please sign in to comment.