Skip to content

Commit

Permalink
Merge pull request foundryvtt#3096 from foundryvtt/summoning
Browse files Browse the repository at this point in the history
[foundryvtt#893] Add data model & configuration data for summoning
  • Loading branch information
arbron authored Feb 29, 2024
2 parents 8080489 + 0180b13 commit f9c6bb9
Show file tree
Hide file tree
Showing 9 changed files with 387 additions and 23 deletions.
46 changes: 46 additions & 0 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@
"DND5E.ActionRSAK": "Ranged Spell Attack",
"DND5E.ActionRWAK": "Ranged Weapon Attack",
"DND5E.ActionSave": "Saving Throw",
"DND5E.ActionSumm": "Summon",
"DND5E.ActionUtil": "Utility",
"DND5E.ActionWarningNoItem": "The requested item {item} no longer exists on Actor {name}",
"DND5E.ActionWarningNoToken": "You must have one or more controlled Tokens in order to use this option.",
Expand Down Expand Up @@ -1378,6 +1379,51 @@
"DND5E.SubclassMismatchWarn": "{name} subclass has no matching class with identifier '{class}'.",
"DND5E.SubclassName": "Subclass Name",
"DND5E.Subtype": "Subtype",
"DND5E.Summoning": {
"Label": "Summoning",
"Action": {
"Add": "Add Profile",
"Configure": "Configure Summons",
"Remove": "Remove Profile"
},
"ArmorClass": {
"Label": "Bonus Armor Class",
"Hint": "Bonus to the Armor Class set on the summoned creature added to what is specified in their statblock."
},
"Configuration": "Summoning Configuration",
"CreatureChanges": {
"Label": "Creature Changes",
"Hint": "Changes that will be made to the creature being summoned. Any @ references used in the following formulas will be based on the summoner's stats."
},
"DisplayName": "Display Name",
"DropHint": "Drop creature here",
"HitPoints": {
"Label": "Bonus Hit Points",
"Hint": "Additional hit points added to the creature on top of what is specified in their statblock."
},
"ItemChanges": {
"Label": "Item Changes",
"Hint": "Changes made to items on the summoned creature."
},
"Match": {
"Attacks": {
"Label": "Match Attacks",
"Hint": "Modify to hit values on the summoned creature's attacks to match that of the summoner."
},
"Proficiency": {
"Label": "Match Proficiency",
"Hint": "Modify the summoned creature's proficiency to match that of the summoner."
},
"Saves": {
"Label": "Match Saves",
"Hint": "Modify to saving throw DCs on the summoned creature's abilities to match that of the summoner."
}
},
"Profile": {
"Label": "Summons Profiles",
"Empty": "Click above to add a profile or drop an creature to summon here."
}
},
"DND5E.Supply": "Supply",
"DND5E.Suppressed": "Suppressed",
"DND5E.Target": "Target",
Expand Down
65 changes: 65 additions & 0 deletions less/v1/items.less
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,13 @@
}
}

.summoning.form-group {
.config-button {
opacity: 1;
font-size: var(--font-size-12);
}
}

/* ----------------------------------------- */
/* Item Actions */
/* ----------------------------------------- */
Expand Down Expand Up @@ -438,3 +445,61 @@
padding: 0.1em 0.5em;
}
}

/* ----------------------------------------- */
/* Summoning Configuration */
/* ----------------------------------------- */

.dnd5e.summoning-config {
.unbutton {
width: unset;
border: none;
background: none;
line-height: unset;

&:hover, &:focus { box-shadow: none; }
&:hover { text-shadow: 0 0 8px var(--color-shadow-primary); }
&:focus-visible { outline: 2px solid black; }
}

.form-header {
justify-content: space-between;
button { flex: unset; }
}

ul.profiles {
padding: 0;
list-style: none;
gap: 12px;
}
li.profile {
position: relative;
padding: 8px;
background: var(--dnd5e-color-card);
border: 2px solid var(--dnd5e-color-gold);
border-radius: 4px;
box-shadow: 0 0 4px var(--dnd5e-shadow-45);

.details {
gap: 4px;
input { height: unset; }
input::placeholder { opacity: .5; }
}
[data-action="delete-profile"] {
--size: 26px;
flex: 0 0 var(--size);
block-size: var(--size);
inline-size: var(--size);
}
.content-link, .drop-area {
flex: 0 0 175px;
display: flex;
align-items: center;
}
.drop-area {
border: 1px dashed black;
border-radius: 4px;
padding-inline: 4px;
}
}
}
1 change: 1 addition & 0 deletions module/applications/item/_module.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export {default as ItemDirectory5e} from "./item-directory.mjs";
export {default as ItemSheet5e} from "./item-sheet.mjs";

export {default as AbilityUseDialog} from "./ability-use-dialog.mjs";
export {default as SummoningConfig} from "./summoning-config.mjs";
8 changes: 6 additions & 2 deletions module/applications/item/item-sheet.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as Trait from "../../documents/actor/trait.mjs";
import { filteredKeys, sortObjectEntries } from "../../utils.mjs";
import ActorMovementConfig from "../actor/movement-config.mjs";
import ActorSensesConfig from "../actor/senses-config.mjs";
import ActorTypeConfig from "../actor/type-config.mjs";
Expand All @@ -6,8 +8,7 @@ import AdvancementMigrationDialog from "../advancement/advancement-migration-dia
import Accordion from "../accordion.mjs";
import EffectsElement from "../components/effects.mjs";
import SourceConfig from "../source-config.mjs";
import * as Trait from "../../documents/actor/trait.mjs";
import { filteredKeys, sortObjectEntries } from "../../utils.mjs";
import SummoningConfig from "./summoning-config.mjs";

/**
* Override and extend the core ItemSheet implementation to handle specific item types.
Expand Down Expand Up @@ -561,6 +562,9 @@ export default class ItemSheet5e extends ItemSheet {
case "source":
app = new SourceConfig(this.item, { keyPath: "system.source" });
break;
case "summoning":
app = new SummoningConfig(this.item);
break;
case "type":
app = new ActorTypeConfig(this.item, { keyPath: "system.type" });
break;
Expand Down
134 changes: 134 additions & 0 deletions module/applications/item/summoning-config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* Application for configuring summoning information for an item.
*/
export default class SummoningConfig extends DocumentSheet {

/** @inheritDoc */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["dnd5e", "summoning-config"],
dragDrop: [{ dropSelector: "form" }],
template: "systems/dnd5e/templates/apps/summoning-config.hbs",
width: 500,
height: "auto",
sheetConfig: false,
closeOnSubmit: false,
submitOnChange: true,
submitOnClose: true
});
}

/* -------------------------------------------- */
/* Properties */
/* -------------------------------------------- */

/**
* Shortcut to the summoning profiles.
* @type {object[]}
*/
get profiles() {
return this.document.system.summons.profiles;
}

/* -------------------------------------------- */

/** @inheritDoc */
get title() {
return `${game.i18n.localize("DND5E.Summoning.Configuration")}: ${this.document.name}`;
}

/* -------------------------------------------- */
/* Rendering */
/* -------------------------------------------- */

/** @inheritDoc */
async getData(options={}) {
const context = await super.getData(options);
context.profiles = this.profiles.map(p => {
const profile = { id: p._id, ...p };
if ( p.uuid ) profile.document = fromUuidSync(p.uuid);
return profile;
}).sort((lhs, rhs) =>
(lhs.name || lhs.document?.name || "").localeCompare(rhs.name || rhs.document?.name || "", game.i18n.lang)
);
context.summons = this.document.system.summons;
return context;
}

/* -------------------------------------------- */
/* Event Handling */
/* -------------------------------------------- */

/** @inheritDoc */
activateListeners(jQuery) {
super.activateListeners(jQuery);
const html = jQuery[0];

for ( const element of html.querySelectorAll("[data-action]") ) {
element.addEventListener("click", event => this.submit({ updateData: {
action: event.target.dataset.action,
profileId: event.target.closest("[data-profile-id]")?.dataset.profileId
} }));
}
}

/* -------------------------------------------- */

/** @inheritDoc */
_getSubmitData(...args) {
const data = foundry.utils.expandObject(super._getSubmitData(...args));
data.profiles = Object.values(data.profiles ?? {});

switch ( data.action ) {
case "add-profile":
data.profiles.push({
_id: foundry.utils.randomID(),
...(data.addDetails ?? {})
});
break;
case "delete-profile":
data.profiles = data.profiles.filter(e => e._id !== data.profileId);
break;
}

return data;
}

/* -------------------------------------------- */

/** @inheritDoc */
async _updateObject(event, formData) {
this.document.update({"system.summons": formData});
}

/* -------------------------------------------- */
/* Drag & Drop */
/* -------------------------------------------- */

/** @inheritDoc */
_canDragDrop() {
return this.isEditable;
}

/* -------------------------------------------- */

/** @inheritDoc */
async _onDrop(event) {
// Try to extract the data
const data = TextEditor.getDragEventData(event);

// Handle dropping linked items
if ( data?.type !== "Actor" ) return;
const actor = await Actor.implementation.fromDropData(data);

// Determine where this was dropped
const existingProfile = event.target.closest("[data-profile-id]");
const { profileId } = existingProfile?.dataset ?? {};

// If dropped onto existing profile, add or replace link
if ( profileId ) this.submit({ updateData: { [`profiles.${profileId}.uuid`]: actor.uuid } });

// Otherwise create a new profile
else this.submit({ updateData: { action: "add-profile", addDetails: { uuid: actor.uuid } } });
}
}
1 change: 1 addition & 0 deletions module/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,7 @@ DND5E.itemActionTypes = {
msak: "DND5E.ActionMSAK",
rsak: "DND5E.ActionRSAK",
save: "DND5E.ActionSave",
summ: "DND5E.ActionSumm",
heal: "DND5E.ActionHeal",
abil: "DND5E.ActionAbil",
util: "DND5E.ActionUtil",
Expand Down
Loading

0 comments on commit f9c6bb9

Please sign in to comment.