mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-06-05 20:34:15 +02:00
Compare commits
16 commits
ddf4747310
...
c8d0df87c8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8d0df87c8 | ||
|
|
983f48b415 | ||
|
|
bfd483698b | ||
|
|
3eb33a71af | ||
|
|
3fbc1e97c6 | ||
|
|
729e8bca42 | ||
|
|
53f15a7fde | ||
|
|
c23ac61ee5 | ||
|
|
d3141059ac | ||
|
|
61db7ca371 | ||
|
|
2bc1c04c93 | ||
|
|
493998cc95 | ||
|
|
251d7e4e13 | ||
|
|
a209b035c8 | ||
|
|
9487b07e43 | ||
|
|
f1a530f57f |
93 changed files with 1250 additions and 1290 deletions
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
* text=auto eol=lf
|
||||||
|
*.json text eol=lf
|
||||||
1
assets/icons/documents/actors/drama-masks.svg
Normal file
1
assets/icons/documents/actors/drama-masks.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="height: 512px; width: 512px;"><path d="M0 0h512v512H0z" fill="#000" fill-opacity="1"></path><g class="" transform="translate(0,0)" style=""><path d="M418.813 30.625c-21.178 26.27-49.712 50.982-84.125 70.844-36.778 21.225-75.064 33.62-110.313 38.06a310.317 310.317 0 0 0 6.813 18.25c16.01.277 29.366-.434 36.406-1.5l9.47-1.53 8.436-1.28.22 10.186a307.48 307.48 0 0 1-1.095 18.72l56.625 8.843c.86-.095 1.713-.15 2.563-.157 11.188-.114 21.44 7.29 24.468 18.593.657 2.448.922 4.903.845 7.313 5.972-2.075 11.753-4.305 17.28-6.72l9.595-4.188 2.313 10.22a340.211 340.211 0 0 1 7.375 48.062C438.29 247.836 468.438 225.71 493 197.5c-3.22-36.73-16.154-78.04-39.125-117.813a290.509 290.509 0 0 0-2.22-3.78l-27.56 71.374c5.154.762 10.123 3.158 14.092 7.126 9.81 9.807 9.813 25.69 0 35.5-9.812 9.81-25.722 9.807-35.53 0-8.86-8.858-9.69-22.68-2.532-32.5l38.938-100.844a322.02 322.02 0 0 0-20.25-25.937zM51.842 118.72c-8.46 17.373-15.76 36.198-21.187 56.436-14.108 52.617-13.96 103.682-2.812 143.438 13.3-2.605 26.442-3.96 39.312-4.03 1.855-.012 3.688.02 5.53.06 20.857.48 40.98 4.332 59.97 11.5a355.064 355.064 0 0 1-1.656-34.218c0-27.8 3.135-54.377 9-78.937l2.47-10.407 9.655 4.562c29.467 13.98 66.194 23.424 106.28 25.22 5.136-20.05 8.19-39.78 9.408-58.75-35.198 4.83-75.387 2.766-116.407-8.22-38.363-10.272-72.314-26.78-99.562-46.656zm230.594 82.218c-1.535 10.452-3.615 21.03-6.218 31.687a312.754 312.754 0 0 0 46-3.97 24.98 24.98 0 0 1-1.532-21.748l-38.25-5.97zM105 201.375l4.156 18.22-21.594 4.905c8.75 5.174 13.353 15.703 10.594 26-3.32 12.394-16.045 19.758-28.437 16.438-12.394-3.32-19.76-16.075-16.44-28.47a23.235 23.235 0 0 1 3.126-6.874l-21.062 4.78-4.125-18.218 73.78-16.78zm388.594 22.813c-25.53 25.46-55.306 45.445-86.906 60.5.05 2.397.093 4.8.093 7.218 0 9.188-.354 18.232-1.03 27.125 16.635 1.33 32.045-1.7 45.344-9.374 25.925-14.962 40.608-45.694 42.5-85.47zm-338.844 3c-4.03 19.993-6.33 41.31-6.406 63.593l.125-.342c30.568 10.174 62.622 17.572 95.25 21.375l7.5.875.718 7.5 5.687 60.125-18.625 1.75-2.53-26.75a23.117 23.117 0 0 1-14.845.968c-12.393-3.32-19.76-16.042-16.438-28.436.285-1.06.647-2.08 1.063-3.063a496.627 496.627 0 0 1-57.406-14.53c2.69 49.62 16.154 94.04 36.094 126.656 22.366 36.588 52.13 57.78 83.968 57.78 31.838.003 61.602-21.19 83.97-57.78 19.536-31.96 32.846-75.244 35.905-123.656a499.132 499.132 0 0 1-48.25 11.656c1.914 4.57 2.415 9.78 1.033 14.938-3.322 12.394-16.045 19.758-28.438 16.437a23.01 23.01 0 0 1-2.125-.686l-2.5 26.47-18.594-1.752 5.688-60.125.72-7.5 7.498-.875c29.245-3.407 57.995-9.717 85.657-18.312v-1.594c0-21.573-2.27-42.23-6.064-61.75C351.132 242.653 313.092 250 272.312 250c-43.59 0-83.986-8.658-117.562-22.813zm-87.5 105.968c-10.87.102-21.995 1.22-33.375 3.313 12.695 31.62 33.117 53.07 59 60 16.9 4.523 34.896 2.536 52.813-5.25-4.382-13.89-7.874-28.606-10.344-43.97-21.115-9.623-43.934-14.32-68.094-14.094zm137.5 80.22h130.813c-40.082 44.594-92.623 42.844-130.813 0z" fill="#fff" fill-opacity="1"></path></g></svg>
|
||||||
|
After Width: | Height: | Size: 3 KiB |
|
|
@ -196,6 +196,11 @@ Hooks.once('init', () => {
|
||||||
makeDefault: true,
|
makeDefault: true,
|
||||||
label: sheetLabel('TYPES.Actor.environment')
|
label: sheetLabel('TYPES.Actor.environment')
|
||||||
});
|
});
|
||||||
|
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.NPC, {
|
||||||
|
types: ['npc'],
|
||||||
|
makeDefault: true,
|
||||||
|
label: sheetLabel('TYPES.Actor.npc')
|
||||||
|
});
|
||||||
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Party, {
|
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Party, {
|
||||||
types: ['party'],
|
types: ['party'],
|
||||||
makeDefault: true,
|
makeDefault: true,
|
||||||
|
|
|
||||||
13
lang/en.json
13
lang/en.json
|
|
@ -23,6 +23,7 @@
|
||||||
"companion": "Companion",
|
"companion": "Companion",
|
||||||
"adversary": "Adversary",
|
"adversary": "Adversary",
|
||||||
"environment": "Environment",
|
"environment": "Environment",
|
||||||
|
"npc": "NPC",
|
||||||
"party": "Party"
|
"party": "Party"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -333,6 +334,11 @@
|
||||||
},
|
},
|
||||||
"newAdversary": "New Adversary"
|
"newAdversary": "New Adversary"
|
||||||
},
|
},
|
||||||
|
"NPC": {
|
||||||
|
"FIELDS": {
|
||||||
|
"motives": { "label": "Motives" }
|
||||||
|
}
|
||||||
|
},
|
||||||
"Party": {
|
"Party": {
|
||||||
"Subtitle": {
|
"Subtitle": {
|
||||||
"character": "{community} {ancestry} | {subclass} {class}",
|
"character": "{community} {ancestry} | {subclass} {class}",
|
||||||
|
|
@ -712,12 +718,6 @@
|
||||||
"ReactionRoll": {
|
"ReactionRoll": {
|
||||||
"title": "Reaction Roll: {trait}"
|
"title": "Reaction Roll: {trait}"
|
||||||
},
|
},
|
||||||
"RerollDialog": {
|
|
||||||
"title": "Reroll",
|
|
||||||
"damageTitle": "Reroll Damage",
|
|
||||||
"deselectDiceNotification": "Deselect one of the selected dice first",
|
|
||||||
"acceptCurrentRolls": "Accept Current Rolls"
|
|
||||||
},
|
|
||||||
"ResourceDice": {
|
"ResourceDice": {
|
||||||
"title": "{name} Resource",
|
"title": "{name} Resource",
|
||||||
"rerollDice": "Reroll Dice"
|
"rerollDice": "Reroll Dice"
|
||||||
|
|
@ -3097,6 +3097,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ChatLog": {
|
"ChatLog": {
|
||||||
|
"rerollActionRoll": "Reroll Action",
|
||||||
"rerollDamage": "Reroll Damage",
|
"rerollDamage": "Reroll Damage",
|
||||||
"assignTagRoll": "Assign as Tag Roll"
|
"assignTagRoll": "Assign as Tag Roll"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ export { default as ImageSelectDialog } from './imageSelectDialog.mjs';
|
||||||
export { default as ItemTransferDialog } from './itemTransfer.mjs';
|
export { default as ItemTransferDialog } from './itemTransfer.mjs';
|
||||||
export { default as MulticlassChoiceDialog } from './multiclassChoiceDialog.mjs';
|
export { default as MulticlassChoiceDialog } from './multiclassChoiceDialog.mjs';
|
||||||
export { default as OwnershipSelection } from './ownershipSelection.mjs';
|
export { default as OwnershipSelection } from './ownershipSelection.mjs';
|
||||||
export { default as RerollDamageDialog } from './rerollDamageDialog.mjs';
|
|
||||||
export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs';
|
export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs';
|
||||||
export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs';
|
export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs';
|
||||||
export { default as TagTeamDialog } from './tagTeamDialog.mjs';
|
export { default as TagTeamDialog } from './tagTeamDialog.mjs';
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,12 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
|
||||||
const context = await super._prepareContext(_options);
|
const context = await super._prepareContext(_options);
|
||||||
|
|
||||||
context.isGM = game.user.isGM;
|
context.isGM = game.user.isGM;
|
||||||
context.isEditable = this.getIsEditable();
|
context.isEditable =
|
||||||
|
game.user.isGM ||
|
||||||
|
this.party.system.partyMembers.some(actor => {
|
||||||
|
const selected = Boolean(this.party.system.groupRoll.participants[actor.id]);
|
||||||
|
return selected && actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER);
|
||||||
|
});
|
||||||
context.fields = this.party.system.schema.fields.groupRoll.fields;
|
context.fields = this.party.system.schema.fields.groupRoll.fields;
|
||||||
context.data = this.party.system.groupRoll;
|
context.data = this.party.system.groupRoll;
|
||||||
context.traitOptions = CONFIG.DH.ACTOR.abilities;
|
context.traitOptions = CONFIG.DH.ACTOR.abilities;
|
||||||
|
|
@ -265,13 +270,6 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
getIsEditable() {
|
|
||||||
return this.party.system.partyMembers.some(actor => {
|
|
||||||
const selected = Boolean(this.party.system.groupRoll.participants[actor.id]);
|
|
||||||
return selected && actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
groupRollRefresh = ({ refreshType, action, parts }) => {
|
groupRollRefresh = ({ refreshType, action, parts }) => {
|
||||||
if (refreshType !== RefreshType.GroupRoll) return;
|
if (refreshType !== RefreshType.GroupRoll) return;
|
||||||
|
|
||||||
|
|
@ -358,8 +356,6 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
// todo: move logic to actor.rollTrait() or actor.diceRoll()
|
|
||||||
if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
|
|
||||||
|
|
||||||
const rollData = result.messageRoll.toJSON();
|
const rollData = result.messageRoll.toJSON();
|
||||||
delete rollData.options.messageRoll;
|
delete rollData.options.messageRoll;
|
||||||
|
|
|
||||||
|
|
@ -1,280 +0,0 @@
|
||||||
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
|
||||||
|
|
||||||
export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) {
|
|
||||||
constructor(message, options = {}) {
|
|
||||||
super(options);
|
|
||||||
|
|
||||||
this.message = message;
|
|
||||||
this.damage = Object.keys(message.system.damage).reduce((acc, typeKey) => {
|
|
||||||
const type = message.system.damage[typeKey];
|
|
||||||
acc[typeKey] = Object.keys(type.parts).reduce((acc, partKey) => {
|
|
||||||
const part = type.parts[partKey];
|
|
||||||
acc[partKey] = Object.keys(part.dice).reduce((acc, diceKey) => {
|
|
||||||
const dice = part.dice[diceKey];
|
|
||||||
const activeResults = dice.results.filter(x => x.active);
|
|
||||||
acc[diceKey] = {
|
|
||||||
dice: dice.dice,
|
|
||||||
selectedResults: activeResults.length,
|
|
||||||
maxSelected: activeResults.length,
|
|
||||||
results: activeResults.map(x => ({ ...x, selected: true }))
|
|
||||||
};
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
static DEFAULT_OPTIONS = {
|
|
||||||
id: 'reroll-dialog',
|
|
||||||
classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'reroll-dialog'],
|
|
||||||
window: {
|
|
||||||
icon: 'fa-solid fa-dice'
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
toggleResult: RerollDamageDialog.#toggleResult,
|
|
||||||
selectRoll: RerollDamageDialog.#selectRoll,
|
|
||||||
doReroll: RerollDamageDialog.#doReroll,
|
|
||||||
save: RerollDamageDialog.#save
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @override */
|
|
||||||
static PARTS = {
|
|
||||||
main: {
|
|
||||||
id: 'main',
|
|
||||||
template: 'systems/daggerheart/templates/dialogs/rerollDialog/damage/main.hbs'
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
id: 'footer',
|
|
||||||
template: 'systems/daggerheart/templates/dialogs/rerollDialog/footer.hbs'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
get title() {
|
|
||||||
return game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.damageTitle');
|
|
||||||
}
|
|
||||||
|
|
||||||
_attachPartListeners(partId, htmlElement, options) {
|
|
||||||
super._attachPartListeners(partId, htmlElement, options);
|
|
||||||
|
|
||||||
htmlElement.querySelectorAll('.to-reroll-input').forEach(element => {
|
|
||||||
element.addEventListener('change', this.toggleDice.bind(this));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async _prepareContext(_options) {
|
|
||||||
const context = await super._prepareContext(_options);
|
|
||||||
context.damage = this.damage;
|
|
||||||
context.disabledReroll = !this.getRerollDice().length;
|
|
||||||
context.saveDisabled = !this.isSelectionDone();
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async #save() {
|
|
||||||
const update = {
|
|
||||||
'system.damage': Object.keys(this.damage).reduce((acc, typeKey) => {
|
|
||||||
const type = this.damage[typeKey];
|
|
||||||
let typeTotal = 0;
|
|
||||||
const messageType = this.message.system.damage[typeKey];
|
|
||||||
const parts = Object.keys(type).map(partKey => {
|
|
||||||
const part = type[partKey];
|
|
||||||
const messagePart = messageType.parts[partKey];
|
|
||||||
let partTotal = messagePart.modifierTotal;
|
|
||||||
const dice = Object.keys(part).map(diceKey => {
|
|
||||||
const dice = part[diceKey];
|
|
||||||
const total = dice.results.reduce((acc, result) => {
|
|
||||||
if (result.active) acc += result.result;
|
|
||||||
return acc;
|
|
||||||
}, 0);
|
|
||||||
partTotal += total;
|
|
||||||
const messageDice = messagePart.dice[diceKey];
|
|
||||||
return {
|
|
||||||
...messageDice,
|
|
||||||
total: total,
|
|
||||||
results: dice.results.map(x => ({
|
|
||||||
...x,
|
|
||||||
hasRerolls: dice.results.length > 1
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
typeTotal += partTotal;
|
|
||||||
return {
|
|
||||||
...messagePart,
|
|
||||||
total: partTotal,
|
|
||||||
dice: dice
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
acc[typeKey] = {
|
|
||||||
...messageType,
|
|
||||||
total: typeTotal,
|
|
||||||
parts: parts
|
|
||||||
};
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {})
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.message.update(update);
|
|
||||||
await this.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
getRerollDice() {
|
|
||||||
const rerollDice = [];
|
|
||||||
Object.keys(this.damage).forEach(typeKey => {
|
|
||||||
const type = this.damage[typeKey];
|
|
||||||
Object.keys(type).forEach(partKey => {
|
|
||||||
const part = type[partKey];
|
|
||||||
Object.keys(part).forEach(diceKey => {
|
|
||||||
const dice = part[diceKey];
|
|
||||||
Object.keys(dice.results).forEach(resultKey => {
|
|
||||||
const result = dice.results[resultKey];
|
|
||||||
if (result.toReroll) {
|
|
||||||
rerollDice.push({
|
|
||||||
...result,
|
|
||||||
dice: dice.dice,
|
|
||||||
type: typeKey,
|
|
||||||
part: partKey,
|
|
||||||
dice: diceKey,
|
|
||||||
result: resultKey
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return rerollDice;
|
|
||||||
}
|
|
||||||
|
|
||||||
isSelectionDone() {
|
|
||||||
const diceFinishedData = [];
|
|
||||||
Object.keys(this.damage).forEach(typeKey => {
|
|
||||||
const type = this.damage[typeKey];
|
|
||||||
Object.keys(type).forEach(partKey => {
|
|
||||||
const part = type[partKey];
|
|
||||||
Object.keys(part).forEach(diceKey => {
|
|
||||||
const dice = part[diceKey];
|
|
||||||
const selected = dice.results.reduce((acc, result) => acc + (result.active ? 1 : 0), 0);
|
|
||||||
diceFinishedData.push(selected === dice.maxSelected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return diceFinishedData.every(x => x);
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleDice(event) {
|
|
||||||
const target = event.target;
|
|
||||||
const { type, part, dice } = target.dataset;
|
|
||||||
const toggleDice = this.damage[type][part][dice];
|
|
||||||
|
|
||||||
const existingDiceRerolls = this.getRerollDice().filter(
|
|
||||||
x => x.type === type && x.part === part && x.dice === dice
|
|
||||||
);
|
|
||||||
|
|
||||||
const allRerolled = existingDiceRerolls.length === toggleDice.results.filter(x => x.active).length;
|
|
||||||
|
|
||||||
toggleDice.toReroll = !allRerolled;
|
|
||||||
toggleDice.results.forEach(result => {
|
|
||||||
if (result.active) {
|
|
||||||
result.toReroll = !allRerolled;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
static #toggleResult(event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
const target = event.target.closest('.to-reroll-result');
|
|
||||||
const { type, part, dice, result } = target.dataset;
|
|
||||||
const toggleDice = this.damage[type][part][dice];
|
|
||||||
const toggleResult = toggleDice.results[result];
|
|
||||||
toggleResult.toReroll = !toggleResult.toReroll;
|
|
||||||
|
|
||||||
const existingDiceRerolls = this.getRerollDice().filter(
|
|
||||||
x => x.type === type && x.part === part && x.dice === dice
|
|
||||||
);
|
|
||||||
|
|
||||||
const allToReroll = existingDiceRerolls.length === toggleDice.results.filter(x => x.active).length;
|
|
||||||
toggleDice.toReroll = allToReroll;
|
|
||||||
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
static async #selectRoll(_, button) {
|
|
||||||
const { type, part, dice, result } = button.dataset;
|
|
||||||
|
|
||||||
const diceVal = this.damage[type][part][dice];
|
|
||||||
const diceResult = diceVal.results[result];
|
|
||||||
if (!diceResult.active && diceVal.results.filter(x => x.active).length === diceVal.maxSelected) {
|
|
||||||
return ui.notifications.warn(
|
|
||||||
game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.deselectDiceNotification')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (diceResult.active) {
|
|
||||||
diceVal.toReroll = false;
|
|
||||||
diceResult.toReroll = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
diceVal.selectedResults += diceResult.active ? -1 : 1;
|
|
||||||
diceResult.active = !diceResult.active;
|
|
||||||
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
static async #doReroll() {
|
|
||||||
const toReroll = this.getRerollDice().map(x => {
|
|
||||||
const { type, part, dice, result } = x;
|
|
||||||
const diceData = this.damage[type][part][dice].results[result];
|
|
||||||
return {
|
|
||||||
...diceData,
|
|
||||||
dice: this.damage[type][part][dice].dice,
|
|
||||||
typeKey: type,
|
|
||||||
partKey: part,
|
|
||||||
diceKey: dice,
|
|
||||||
resultsIndex: result
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const roll = await new Roll(toReroll.map(x => `1${x.dice}`).join(' + ')).evaluate();
|
|
||||||
|
|
||||||
if (game.modules.get('dice-so-nice')?.active) {
|
|
||||||
const diceSoNiceRoll = {
|
|
||||||
_evaluated: true,
|
|
||||||
dice: roll.dice,
|
|
||||||
options: { appearance: {} }
|
|
||||||
};
|
|
||||||
|
|
||||||
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
toReroll.forEach((data, index) => {
|
|
||||||
const { typeKey, partKey, diceKey, resultsIndex } = data;
|
|
||||||
const rerolledDice = roll.dice[index];
|
|
||||||
|
|
||||||
const dice = this.damage[typeKey][partKey][diceKey];
|
|
||||||
dice.toReroll = false;
|
|
||||||
dice.results[resultsIndex].active = false;
|
|
||||||
dice.results[resultsIndex].discarded = true;
|
|
||||||
dice.results[resultsIndex].toReroll = false;
|
|
||||||
dice.results.splice(dice.results.length, 0, {
|
|
||||||
...rerolledDice.results[0],
|
|
||||||
toReroll: false,
|
|
||||||
selected: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,279 +0,0 @@
|
||||||
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
|
||||||
|
|
||||||
export default class RerollDialog extends HandlebarsApplicationMixin(ApplicationV2) {
|
|
||||||
constructor(message, options = {}) {
|
|
||||||
super(options);
|
|
||||||
|
|
||||||
this.message = message;
|
|
||||||
this.damage = Object.keys(message.system.damage).reduce((acc, typeKey) => {
|
|
||||||
const type = message.system.damage[typeKey];
|
|
||||||
acc[typeKey] = Object.keys(type.parts).reduce((acc, partKey) => {
|
|
||||||
const part = type.parts[partKey];
|
|
||||||
acc[partKey] = Object.keys(part.dice).reduce((acc, diceKey) => {
|
|
||||||
const dice = part.dice[diceKey];
|
|
||||||
const activeResults = dice.results.filter(x => x.active);
|
|
||||||
acc[diceKey] = {
|
|
||||||
dice: dice.dice,
|
|
||||||
selectedResults: activeResults.length,
|
|
||||||
maxSelected: activeResults.length,
|
|
||||||
results: activeResults.map(x => ({ ...x, selected: true }))
|
|
||||||
};
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
static DEFAULT_OPTIONS = {
|
|
||||||
id: 'reroll-dialog',
|
|
||||||
classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'reroll-dialog'],
|
|
||||||
window: {
|
|
||||||
icon: 'fa-solid fa-dice'
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
toggleResult: RerollDialog.#toggleResult,
|
|
||||||
selectRoll: RerollDialog.#selectRoll,
|
|
||||||
doReroll: RerollDialog.#doReroll,
|
|
||||||
save: RerollDialog.#save
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @override */
|
|
||||||
static PARTS = {
|
|
||||||
main: {
|
|
||||||
id: 'main',
|
|
||||||
template: 'systems/daggerheart/templates/dialogs/rerollDialog/main.hbs'
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
id: 'footer',
|
|
||||||
template: 'systems/daggerheart/templates/dialogs/rerollDialog/footer.hbs'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
get title() {
|
|
||||||
return game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.title');
|
|
||||||
}
|
|
||||||
|
|
||||||
_attachPartListeners(partId, htmlElement, options) {
|
|
||||||
super._attachPartListeners(partId, htmlElement, options);
|
|
||||||
|
|
||||||
htmlElement.querySelectorAll('.to-reroll-input').forEach(element => {
|
|
||||||
element.addEventListener('change', this.toggleDice.bind(this));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async _prepareContext(_options) {
|
|
||||||
const context = await super._prepareContext(_options);
|
|
||||||
context.damage = this.damage;
|
|
||||||
context.disabledReroll = !this.getRerollDice().length;
|
|
||||||
context.saveDisabled = !this.isSelectionDone();
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async #save() {
|
|
||||||
const update = {
|
|
||||||
'system.damage': Object.keys(this.damage).reduce((acc, typeKey) => {
|
|
||||||
const type = this.damage[typeKey];
|
|
||||||
let typeTotal = 0;
|
|
||||||
const messageType = this.message.system.damage[typeKey];
|
|
||||||
const parts = Object.keys(type).map(partKey => {
|
|
||||||
const part = type[partKey];
|
|
||||||
const messagePart = messageType.parts[partKey];
|
|
||||||
let partTotal = messagePart.modifierTotal;
|
|
||||||
const dice = Object.keys(part).map(diceKey => {
|
|
||||||
const dice = part[diceKey];
|
|
||||||
const total = dice.results.reduce((acc, result) => {
|
|
||||||
if (result.active) acc += result.result;
|
|
||||||
return acc;
|
|
||||||
}, 0);
|
|
||||||
partTotal += total;
|
|
||||||
const messageDice = messagePart.dice[diceKey];
|
|
||||||
return {
|
|
||||||
...messageDice,
|
|
||||||
total: total,
|
|
||||||
results: dice.results.map(x => ({
|
|
||||||
...x,
|
|
||||||
hasRerolls: dice.results.length > 1
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
typeTotal += partTotal;
|
|
||||||
return {
|
|
||||||
...messagePart,
|
|
||||||
total: partTotal,
|
|
||||||
dice: dice
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
acc[typeKey] = {
|
|
||||||
...messageType,
|
|
||||||
total: typeTotal,
|
|
||||||
parts: parts
|
|
||||||
};
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {})
|
|
||||||
};
|
|
||||||
await this.message.update(update);
|
|
||||||
await this.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
getRerollDice() {
|
|
||||||
const rerollDice = [];
|
|
||||||
Object.keys(this.damage).forEach(typeKey => {
|
|
||||||
const type = this.damage[typeKey];
|
|
||||||
Object.keys(type).forEach(partKey => {
|
|
||||||
const part = type[partKey];
|
|
||||||
Object.keys(part).forEach(diceKey => {
|
|
||||||
const dice = part[diceKey];
|
|
||||||
Object.keys(dice.results).forEach(resultKey => {
|
|
||||||
const result = dice.results[resultKey];
|
|
||||||
if (result.toReroll) {
|
|
||||||
rerollDice.push({
|
|
||||||
...result,
|
|
||||||
dice: dice.dice,
|
|
||||||
type: typeKey,
|
|
||||||
part: partKey,
|
|
||||||
dice: diceKey,
|
|
||||||
result: resultKey
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return rerollDice;
|
|
||||||
}
|
|
||||||
|
|
||||||
isSelectionDone() {
|
|
||||||
const diceFinishedData = [];
|
|
||||||
Object.keys(this.damage).forEach(typeKey => {
|
|
||||||
const type = this.damage[typeKey];
|
|
||||||
Object.keys(type).forEach(partKey => {
|
|
||||||
const part = type[partKey];
|
|
||||||
Object.keys(part).forEach(diceKey => {
|
|
||||||
const dice = part[diceKey];
|
|
||||||
const selected = dice.results.reduce((acc, result) => acc + (result.active ? 1 : 0), 0);
|
|
||||||
diceFinishedData.push(selected === dice.maxSelected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return diceFinishedData.every(x => x);
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleDice(event) {
|
|
||||||
const target = event.target;
|
|
||||||
const { type, part, dice } = target.dataset;
|
|
||||||
const toggleDice = this.damage[type][part][dice];
|
|
||||||
|
|
||||||
const existingDiceRerolls = this.getRerollDice().filter(
|
|
||||||
x => x.type === type && x.part === part && x.dice === dice
|
|
||||||
);
|
|
||||||
|
|
||||||
const allRerolled = existingDiceRerolls.length === toggleDice.results.filter(x => x.active).length;
|
|
||||||
|
|
||||||
toggleDice.toReroll = !allRerolled;
|
|
||||||
toggleDice.results.forEach(result => {
|
|
||||||
if (result.active) {
|
|
||||||
result.toReroll = !allRerolled;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
static #toggleResult(event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
const target = event.target.closest('.to-reroll-result');
|
|
||||||
const { type, part, dice, result } = target.dataset;
|
|
||||||
const toggleDice = this.damage[type][part][dice];
|
|
||||||
const toggleResult = toggleDice.results[result];
|
|
||||||
toggleResult.toReroll = !toggleResult.toReroll;
|
|
||||||
|
|
||||||
const existingDiceRerolls = this.getRerollDice().filter(
|
|
||||||
x => x.type === type && x.part === part && x.dice === dice
|
|
||||||
);
|
|
||||||
|
|
||||||
const allToReroll = existingDiceRerolls.length === toggleDice.results.length;
|
|
||||||
toggleDice.toReroll = allToReroll;
|
|
||||||
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
static async #selectRoll(_, button) {
|
|
||||||
const { type, part, dice, result } = button.dataset;
|
|
||||||
|
|
||||||
const diceVal = this.damage[type][part][dice];
|
|
||||||
const diceResult = diceVal.results[result];
|
|
||||||
if (!diceResult.active && diceVal.results.filter(x => x.active).length === diceVal.maxSelected) {
|
|
||||||
return ui.notifications.warn(
|
|
||||||
game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.deselectDiceNotification')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (diceResult.active) {
|
|
||||||
diceVal.toReroll = false;
|
|
||||||
diceResult.toReroll = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
diceVal.selectedResults += diceResult.active ? -1 : 1;
|
|
||||||
diceResult.active = !diceResult.active;
|
|
||||||
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
static async #doReroll() {
|
|
||||||
const toReroll = this.getRerollDice().map(x => {
|
|
||||||
const { type, part, dice, result } = x;
|
|
||||||
const diceData = this.damage[type][part][dice].results[result];
|
|
||||||
return {
|
|
||||||
...diceData,
|
|
||||||
dice: this.damage[type][part][dice].dice,
|
|
||||||
typeKey: type,
|
|
||||||
partKey: part,
|
|
||||||
diceKey: dice,
|
|
||||||
resultsIndex: result
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const roll = await new Roll(toReroll.map(x => `1${x.dice}`).join(' + ')).evaluate();
|
|
||||||
|
|
||||||
if (game.modules.get('dice-so-nice')?.active) {
|
|
||||||
const diceSoNiceRoll = {
|
|
||||||
_evaluated: true,
|
|
||||||
dice: roll.dice,
|
|
||||||
options: { appearance: {} }
|
|
||||||
};
|
|
||||||
|
|
||||||
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
toReroll.forEach((data, index) => {
|
|
||||||
const { typeKey, partKey, diceKey, resultsIndex } = data;
|
|
||||||
const rerolledDice = roll.dice[index];
|
|
||||||
|
|
||||||
const dice = this.damage[typeKey][partKey][diceKey];
|
|
||||||
dice.toReroll = false;
|
|
||||||
dice.results[resultsIndex].active = false;
|
|
||||||
dice.results[resultsIndex].discarded = true;
|
|
||||||
dice.results[resultsIndex].toReroll = false;
|
|
||||||
dice.results.splice(dice.results.length, 0, {
|
|
||||||
...rerolledDice.results[0],
|
|
||||||
toReroll: false,
|
|
||||||
selected: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { itemAbleRollParse } from '../../helpers/utils.mjs';
|
import { itemAbleRollParse, triggerChatRollFx } from '../../helpers/utils.mjs';
|
||||||
|
|
||||||
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
||||||
|
|
||||||
|
|
@ -69,7 +69,7 @@ export default class ResourceDiceDialog extends HandlebarsApplicationMixin(Appli
|
||||||
const max = itemAbleRollParse(this.item.system.resource.max, this.actor, this.item);
|
const max = itemAbleRollParse(this.item.system.resource.max, this.actor, this.item);
|
||||||
const diceFormula = `${max}${this.item.system.resource.dieFaces}`;
|
const diceFormula = `${max}${this.item.system.resource.dieFaces}`;
|
||||||
const roll = await new Roll(diceFormula).evaluate();
|
const roll = await new Roll(diceFormula).evaluate();
|
||||||
if (game.modules.get('dice-so-nice')?.active) await game.dice3d.showForRoll(roll, game.user, true);
|
await triggerChatRollFx([roll]);
|
||||||
this.rollValues = roll.terms[0].results.map(x => ({ value: x.result, used: false }));
|
this.rollValues = roll.terms[0].results.map(x => ({ value: x.result, used: false }));
|
||||||
this.resetUsed = true;
|
this.resetUsed = true;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,12 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
|
|
||||||
async _prepareContext(_options) {
|
async _prepareContext(_options) {
|
||||||
const context = await super._prepareContext(_options);
|
const context = await super._prepareContext(_options);
|
||||||
context.isEditable = this.getIsEditable();
|
context.isEditable =
|
||||||
|
game.user.isGM ||
|
||||||
|
this.party.system.partyMembers.some(actor => {
|
||||||
|
const selected = Boolean(this.party.system.tagTeam.members[actor.id]);
|
||||||
|
return selected && actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER);
|
||||||
|
});
|
||||||
context.fields = this.party.system.schema.fields.tagTeam.fields;
|
context.fields = this.party.system.schema.fields.tagTeam.fields;
|
||||||
context.data = this.party.system.tagTeam;
|
context.data = this.party.system.tagTeam;
|
||||||
context.rollTypes = CONFIG.DH.GENERAL.tagTeamRollTypes;
|
context.rollTypes = CONFIG.DH.GENERAL.tagTeamRollTypes;
|
||||||
|
|
@ -179,57 +184,56 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(this.party.system.tagTeam.members).includes(partId)) {
|
if (Object.keys(this.party.system.tagTeam.members).includes(partId)) {
|
||||||
const data = this.party.system.tagTeam.members[partId];
|
const data = await this.#prepareMemberContext(partId);
|
||||||
|
partContext.hasDamage |= Boolean(data?.damage);
|
||||||
|
partContext.members[partId] = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return partContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
async #prepareMemberContext(partId) {
|
||||||
|
const data = this.party.system.tagTeam.members[partId] ?? {};
|
||||||
const actor = game.actors.get(partId);
|
const actor = game.actors.get(partId);
|
||||||
|
if (!actor) console.error(`Failed to get actor ${partId}`);
|
||||||
|
|
||||||
const rollOptions = [];
|
const rollOptions = [];
|
||||||
const damageRollOptions = [];
|
const damageRollOptions = [];
|
||||||
for (const item of actor.items) {
|
for (const item of actor?.items ?? []) {
|
||||||
if (item.system.metadata.hasActions) {
|
if (!item.system.metadata.hasActions) continue;
|
||||||
const actions = [...item.system.actions, ...(item.system.attack ? [item.system.attack] : [])];
|
const actions = [...item.system.actions, ...(item.system.attack ? [item.system.attack] : [])];
|
||||||
for (const action of actions) {
|
for (const action of actions) {
|
||||||
if (action.hasRoll) {
|
if (action.hasRoll) {
|
||||||
const actionItem = {
|
const collection = action.hasDamage ? damageRollOptions : rollOptions;
|
||||||
|
collection.push({
|
||||||
value: action.uuid,
|
value: action.uuid,
|
||||||
label: action.name,
|
label: action.name,
|
||||||
group: item.name,
|
group: item.name,
|
||||||
baseAction: action.baseAction
|
baseAction: action.baseAction
|
||||||
};
|
});
|
||||||
|
|
||||||
if (action.hasDamage) damageRollOptions.push(actionItem);
|
|
||||||
else rollOptions.push(actionItem);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedRoll = Object.values(this.party.system.tagTeam.members).find(member => member.selected);
|
const selectedRoll = Object.values(this.party.system.tagTeam.members).find(member => member.selected);
|
||||||
const critSelected = !selectedRoll
|
const critSelected = !selectedRoll ? undefined : (selectedRoll?.rollData?.options?.roll?.isCritical ?? false);
|
||||||
? undefined
|
|
||||||
: (selectedRoll?.rollData?.options?.roll?.isCritical ?? false);
|
|
||||||
|
|
||||||
const damage = data.rollData?.options?.damage;
|
const damage = data.rollData?.options?.damage;
|
||||||
partContext.hasDamage |= Boolean(damage);
|
|
||||||
const critHitPointsDamage = await this.getCriticalDamage(damage);
|
|
||||||
|
|
||||||
partContext.members[partId] = {
|
return {
|
||||||
...data,
|
...data,
|
||||||
roll: data.roll,
|
roll: data.roll,
|
||||||
isEditable: actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER),
|
isEditable: actor?.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER),
|
||||||
key: partId,
|
key: partId,
|
||||||
readyToRoll: Boolean(data.rollChoice),
|
readyToRoll: Boolean(data.rollChoice),
|
||||||
hasRolled: Boolean(data.rollData),
|
hasRolled: Boolean(data.rollData),
|
||||||
rollOptions,
|
rollOptions,
|
||||||
damageRollOptions,
|
damageRollOptions,
|
||||||
damage: damage,
|
damage: damage,
|
||||||
critDamage: critHitPointsDamage,
|
critDamage: await this.getCriticalDamage(damage),
|
||||||
useCritDamage: critSelected || (critSelected === undefined && data.rollData?.options?.roll?.isCritical)
|
useCritDamage: critSelected || (critSelected === undefined && data.rollData?.options?.roll?.isCritical)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return partContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
getUpdatingParts(target) {
|
getUpdatingParts(target) {
|
||||||
const { initialization, rollSelection, result } = this.constructor.PARTS;
|
const { initialization, rollSelection, result } = this.constructor.PARTS;
|
||||||
const isInitialization = this.tabGroups.application === initialization.id;
|
const isInitialization = this.tabGroups.application === initialization.id;
|
||||||
|
|
@ -273,13 +277,6 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getIsEditable() {
|
|
||||||
return this.party.system.partyMembers.some(actor => {
|
|
||||||
const selected = Boolean(this.party.system.tagTeam.members[actor.id]);
|
|
||||||
return selected && actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
tagTeamRefresh = ({ refreshType, action, parts }) => {
|
tagTeamRefresh = ({ refreshType, action, parts }) => {
|
||||||
if (refreshType !== RefreshType.TagTeamRoll) return;
|
if (refreshType !== RefreshType.TagTeamRoll) return;
|
||||||
|
|
||||||
|
|
@ -434,8 +431,6 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
|
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
|
|
||||||
|
|
||||||
const rollData = result.messageRoll.toJSON();
|
const rollData = result.messageRoll.toJSON();
|
||||||
delete rollData.options.messageRoll;
|
delete rollData.options.messageRoll;
|
||||||
this.updatePartyData(
|
this.updatePartyData(
|
||||||
|
|
@ -651,10 +646,11 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
}
|
}
|
||||||
|
|
||||||
async getJoinedRoll({ overrideIsCritical, displayVersion } = {}) {
|
async getJoinedRoll({ overrideIsCritical, displayVersion } = {}) {
|
||||||
|
try {
|
||||||
const memberValues = Object.values(this.party.system.tagTeam.members);
|
const memberValues = Object.values(this.party.system.tagTeam.members);
|
||||||
const selectedRoll = memberValues.find(x => x.selected);
|
const selectedRoll = memberValues.find(x => x.selected);
|
||||||
let baseMainRoll = selectedRoll ?? memberValues[0];
|
const baseMainRoll = selectedRoll ?? memberValues[0];
|
||||||
let baseSecondaryRoll = selectedRoll
|
const baseSecondaryRoll = selectedRoll
|
||||||
? memberValues.find(x => !x.selected)
|
? memberValues.find(x => !x.selected)
|
||||||
: memberValues.length > 1
|
: memberValues.length > 1
|
||||||
? memberValues[1]
|
? memberValues[1]
|
||||||
|
|
@ -673,13 +669,16 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
? await this.getCriticalDamage(secondaryRollData.options.damage)
|
? await this.getCriticalDamage(secondaryRollData.options.damage)
|
||||||
: secondaryRollData.options.damage;
|
: secondaryRollData.options.damage;
|
||||||
if (systemData.damage) {
|
if (systemData.damage) {
|
||||||
for (const key in secondaryDamage) {
|
for (const [key, damage] of Object.entries(secondaryDamage ?? {})) {
|
||||||
const damage = secondaryDamage[key];
|
if (key in systemData.damage) {
|
||||||
systemData.damage[key].formula = [systemData.damage[key].formula, damage.formula]
|
systemData.damage[key].formula = [systemData.damage[key]?.formula, damage.formula]
|
||||||
.filter(x => x)
|
.filter(x => x)
|
||||||
.join(' + ');
|
.join(' + ');
|
||||||
systemData.damage[key].total += damage.total;
|
systemData.damage[key].total += damage.total;
|
||||||
systemData.damage[key].parts.push(...damage.parts);
|
systemData.damage[key].parts.push(...damage.parts);
|
||||||
|
} else {
|
||||||
|
systemData.damage[key] = damage;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
systemData.damage = secondaryDamage;
|
systemData.damage = secondaryDamage;
|
||||||
|
|
@ -687,6 +686,10 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
}
|
}
|
||||||
|
|
||||||
return mainRoll;
|
return mainRoll;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async #onCancelRoll(_event, _button, options = { confirm: true }) {
|
static async #onCancelRoll(_event, _button, options = { confirm: true }) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ export { default as ActionConfig } from './action-config.mjs';
|
||||||
export { default as ActionSettingsConfig } from './action-settings-config.mjs';
|
export { default as ActionSettingsConfig } from './action-settings-config.mjs';
|
||||||
export { default as CharacterSettings } from './character-settings.mjs';
|
export { default as CharacterSettings } from './character-settings.mjs';
|
||||||
export { default as AdversarySettings } from './adversary-settings.mjs';
|
export { default as AdversarySettings } from './adversary-settings.mjs';
|
||||||
|
export { default as NPCSettings } from './npc-settings.mjs';
|
||||||
export { default as CompanionSettings } from './companion-settings.mjs';
|
export { default as CompanionSettings } from './companion-settings.mjs';
|
||||||
export { default as SettingFeatureConfig } from './setting-feature-config.mjs';
|
export { default as SettingFeatureConfig } from './setting-feature-config.mjs';
|
||||||
export { default as EnvironmentSettings } from './environment-settings.mjs';
|
export { default as EnvironmentSettings } from './environment-settings.mjs';
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
|
||||||
* @returns {ChangeChoice { value: string, label: string, hint: string, group: string }[]}
|
* @returns {ChangeChoice { value: string, label: string, hint: string, group: string }[]}
|
||||||
*/
|
*/
|
||||||
static getChangeChoices() {
|
static getChangeChoices() {
|
||||||
const ignoredActorKeys = ['config', 'DhEnvironment', 'DhParty'];
|
const ignoredActorKeys = ['config', 'DhEnvironment', 'DhParty', 'DhNPC'];
|
||||||
|
|
||||||
const getAllLeaves = (root, group, parentPath = '') => {
|
const getAllLeaves = (root, group, parentPath = '') => {
|
||||||
const leaves = [];
|
const leaves = [];
|
||||||
|
|
|
||||||
85
module/applications/sheets-configs/npc-settings.mjs
Normal file
85
module/applications/sheets-configs/npc-settings.mjs
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import DHBaseActorSettings from '../sheets/api/actor-setting.mjs';
|
||||||
|
|
||||||
|
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
|
||||||
|
|
||||||
|
export default class DHNPCSettings extends DHBaseActorSettings {
|
||||||
|
/**@inheritdoc */
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
classes: ['npc-settings'],
|
||||||
|
position: { width: 455, height: 'auto' },
|
||||||
|
actions: {},
|
||||||
|
dragDrop: [
|
||||||
|
{ dragSelector: null, dropSelector: '.tab.features' },
|
||||||
|
{ dragSelector: '.feature-item', dropSelector: null }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
/**@override */
|
||||||
|
static PARTS = {
|
||||||
|
header: {
|
||||||
|
id: 'header',
|
||||||
|
template: 'systems/daggerheart/templates/sheets-settings/npc-settings/header.hbs'
|
||||||
|
},
|
||||||
|
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
|
||||||
|
details: {
|
||||||
|
id: 'details',
|
||||||
|
template: 'systems/daggerheart/templates/sheets-settings/npc-settings/details.hbs'
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
id: 'features',
|
||||||
|
template: 'systems/daggerheart/templates/sheets-settings/npc-settings/features.hbs'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
static TABS = {
|
||||||
|
primary: {
|
||||||
|
tabs: [{ id: 'details' }, { id: 'features' }],
|
||||||
|
initial: 'details',
|
||||||
|
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async _prepareContext(options) {
|
||||||
|
const context = await super._prepareContext(options);
|
||||||
|
|
||||||
|
const featureForms = ['passive', 'action', 'reaction'];
|
||||||
|
context.features = context.document.system.features.sort((a, b) =>
|
||||||
|
a.system.featureForm !== b.system.featureForm
|
||||||
|
? featureForms.indexOf(a.system.featureForm) - featureForms.indexOf(b.system.featureForm)
|
||||||
|
: a.sort - b.sort
|
||||||
|
);
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
async _onDragStart(event) {
|
||||||
|
const featureItem = event.currentTarget.closest('.feature-item');
|
||||||
|
|
||||||
|
if (featureItem) {
|
||||||
|
const feature = this.actor.items.get(featureItem.id);
|
||||||
|
const featureData = { type: 'Item', uuid: feature.uuid, fromInternal: true };
|
||||||
|
event.dataTransfer.setData('text/plain', JSON.stringify(featureData));
|
||||||
|
event.dataTransfer.setDragImage(featureItem.querySelector('img'), 60, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _onDrop(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
|
||||||
|
|
||||||
|
const item = await fromUuid(data.uuid);
|
||||||
|
if (item?.type === 'feature') {
|
||||||
|
if (data.fromInternal && item.parent?.uuid === this.actor.uuid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemData = item.toObject();
|
||||||
|
delete itemData._id;
|
||||||
|
|
||||||
|
await this.actor.createEmbeddedDocuments('Item', [itemData]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,4 +2,5 @@ export { default as Adversary } from './adversary.mjs';
|
||||||
export { default as Character } from './character.mjs';
|
export { default as Character } from './character.mjs';
|
||||||
export { default as Companion } from './companion.mjs';
|
export { default as Companion } from './companion.mjs';
|
||||||
export { default as Environment } from './environment.mjs';
|
export { default as Environment } from './environment.mjs';
|
||||||
|
export { default as NPC } from './npc.mjs';
|
||||||
export { default as Party } from './party.mjs';
|
export { default as Party } from './party.mjs';
|
||||||
|
|
|
||||||
136
module/applications/sheets/actors/npc.mjs
Normal file
136
module/applications/sheets/actors/npc.mjs
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
import DHBaseActorSheet from '../api/base-actor.mjs';
|
||||||
|
|
||||||
|
export default class NPCSheet extends DHBaseActorSheet {
|
||||||
|
/** @inheritDoc */
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
classes: ['npc'],
|
||||||
|
position: { width: 660, height: 600 },
|
||||||
|
window: { resizable: true },
|
||||||
|
actions: {},
|
||||||
|
window: {
|
||||||
|
resizable: true,
|
||||||
|
controls: [
|
||||||
|
{
|
||||||
|
icon: 'fa-solid fa-signature',
|
||||||
|
label: 'DAGGERHEART.UI.Tooltip.configureAttribution',
|
||||||
|
action: 'editAttribution'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
dragDrop: [
|
||||||
|
{
|
||||||
|
dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]',
|
||||||
|
dropSelector: null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
static PARTS = {
|
||||||
|
header: { template: 'systems/daggerheart/templates/sheets/actors/npc/header.hbs' },
|
||||||
|
tabs: { template: 'systems/daggerheart/templates/sheets/actors/npc/navigation.hbs' },
|
||||||
|
features: {
|
||||||
|
template: 'systems/daggerheart/templates/sheets/actors/npc/features.hbs',
|
||||||
|
scrollable: ['.feature-section']
|
||||||
|
},
|
||||||
|
notes: {
|
||||||
|
template: 'systems/daggerheart/templates/sheets/actors/npc/notes.hbs'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
static TABS = {
|
||||||
|
primary: {
|
||||||
|
tabs: [{ id: 'notes' }, { id: 'features' }],
|
||||||
|
initial: 'notes',
|
||||||
|
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
_prepareTabs(group) {
|
||||||
|
const result = super._prepareTabs(group);
|
||||||
|
if (group === 'primary') {
|
||||||
|
result.features.empty = this.document.system.features.length === 0;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
async _preparePartContext(partId, context, options) {
|
||||||
|
context = await super._preparePartContext(partId, context, options);
|
||||||
|
switch (partId) {
|
||||||
|
case 'header':
|
||||||
|
await this._prepareHeaderContext(context, options);
|
||||||
|
break;
|
||||||
|
case 'features':
|
||||||
|
await this._prepareFeaturesContext(context, options);
|
||||||
|
break;
|
||||||
|
case 'notes':
|
||||||
|
await this._prepareNotesContext(context, options);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare render context for the Header part.
|
||||||
|
* @param {ApplicationRenderContext} context
|
||||||
|
* @param {ApplicationRenderOptions} options
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
async _prepareHeaderContext(context, _options) {
|
||||||
|
const { system } = this.document;
|
||||||
|
const { TextEditor } = foundry.applications.ux;
|
||||||
|
|
||||||
|
context.description = await TextEditor.implementation.enrichHTML(system.description, {
|
||||||
|
secrets: this.document.isOwner,
|
||||||
|
relativeTo: this.document
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare render context for the Features part.
|
||||||
|
* @param {ApplicationRenderContext} context
|
||||||
|
* @param {ApplicationRenderOptions} options
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
async _prepareFeaturesContext(context, _options) {
|
||||||
|
const featureForms = ['passive', 'action', 'reaction'];
|
||||||
|
context.features = this.document.system.features.sort((a, b) =>
|
||||||
|
a.system.featureForm !== b.system.featureForm
|
||||||
|
? featureForms.indexOf(a.system.featureForm) - featureForms.indexOf(b.system.featureForm)
|
||||||
|
: a.sort - b.sort
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare render context for the Biography part.
|
||||||
|
* @param {ApplicationRenderContext} context
|
||||||
|
* @param {ApplicationRenderOptions} options
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
async _prepareNotesContext(context, _options) {
|
||||||
|
const { system } = this.document;
|
||||||
|
const { TextEditor } = foundry.applications.ux;
|
||||||
|
|
||||||
|
const paths = {
|
||||||
|
notes: 'notes'
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [key, path] of Object.entries(paths)) {
|
||||||
|
const value = foundry.utils.getProperty(system, path);
|
||||||
|
context[key] = {
|
||||||
|
field: system.schema.getField(path),
|
||||||
|
value,
|
||||||
|
enriched: await TextEditor.implementation.enrichHTML(value, {
|
||||||
|
secrets: this.document.isOwner,
|
||||||
|
relativeTo: this.document
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -103,6 +103,19 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
||||||
_getEntryContextOptions() {
|
_getEntryContextOptions() {
|
||||||
return [
|
return [
|
||||||
...super._getEntryContextOptions(),
|
...super._getEntryContextOptions(),
|
||||||
|
{
|
||||||
|
label: 'DAGGERHEART.UI.ChatLog.rerollActionRoll',
|
||||||
|
icon: '<i class="fa-solid fa-dice"></i>',
|
||||||
|
visible: li => {
|
||||||
|
const message = game.messages.get(li.dataset.messageId);
|
||||||
|
return message.system.hasRoll && (game.user.isGM || message.isAuthor);
|
||||||
|
},
|
||||||
|
callback: async li => {
|
||||||
|
const message = game.messages.get(li.dataset.messageId);
|
||||||
|
const reroll = await message.rolls[0].reroll({ liveRoll: true });
|
||||||
|
message.update({ rolls: [reroll] });
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'DAGGERHEART.UI.ChatLog.rerollDamage',
|
label: 'DAGGERHEART.UI.ChatLog.rerollDamage',
|
||||||
icon: '<i class="fa-solid fa-dice"></i>',
|
icon: '<i class="fa-solid fa-dice"></i>',
|
||||||
|
|
@ -113,9 +126,10 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
||||||
: false;
|
: false;
|
||||||
return (game.user.isGM || message.isAuthor) && hasRolledDamage;
|
return (game.user.isGM || message.isAuthor) && hasRolledDamage;
|
||||||
},
|
},
|
||||||
callback: li => {
|
callback: async li => {
|
||||||
const message = game.messages.get(li.dataset.messageId);
|
const message = game.messages.get(li.dataset.messageId);
|
||||||
new game.system.api.applications.dialogs.RerollDamageDialog(message).render({ force: true });
|
const update = await message.system.getRerolledDamage();
|
||||||
|
message.update(update);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
import DhCharacter from './character.mjs';
|
import DhCharacter from './character.mjs';
|
||||||
import DhCompanion from './companion.mjs';
|
import DhCompanion from './companion.mjs';
|
||||||
import DhAdversary from './adversary.mjs';
|
import DhAdversary from './adversary.mjs';
|
||||||
|
import DhNPC from './npc.mjs';
|
||||||
import DhEnvironment from './environment.mjs';
|
import DhEnvironment from './environment.mjs';
|
||||||
import DhParty from './party.mjs';
|
import DhParty from './party.mjs';
|
||||||
|
|
||||||
export { DhCharacter, DhCompanion, DhAdversary, DhEnvironment, DhParty };
|
export { DhCharacter, DhCompanion, DhAdversary, DhNPC, DhEnvironment, DhParty };
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
character: DhCharacter,
|
character: DhCharacter,
|
||||||
companion: DhCompanion,
|
companion: DhCompanion,
|
||||||
adversary: DhAdversary,
|
adversary: DhAdversary,
|
||||||
|
npc: DhNPC,
|
||||||
environment: DhEnvironment,
|
environment: DhEnvironment,
|
||||||
party: DhParty
|
party: DhParty
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@ import { ActionField } from '../fields/actionField.mjs';
|
||||||
import { commonActorRules } from './base.mjs';
|
import { commonActorRules } from './base.mjs';
|
||||||
import DhCreature from './creature.mjs';
|
import DhCreature from './creature.mjs';
|
||||||
import { bonusField } from '../fields/actorField.mjs';
|
import { bonusField } from '../fields/actorField.mjs';
|
||||||
import { calculateExpectedValue, parseTermsFromSimpleFormula } from '../../helpers/utils.mjs';
|
import { getTierAdjustedAdversary } from './tierAdjustment.mjs';
|
||||||
import { adversaryExpectedDamage, adversaryScalingData } from '../../config/actorConfig.mjs';
|
|
||||||
|
|
||||||
export default class DhpAdversary extends DhCreature {
|
export default class DhpAdversary extends DhCreature {
|
||||||
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Adversary'];
|
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Adversary'];
|
||||||
|
|
@ -206,205 +205,6 @@ export default class DhpAdversary extends DhCreature {
|
||||||
/** Returns source data for this actor adjusted to a new tier, which can be used to create a new actor. */
|
/** Returns source data for this actor adjusted to a new tier, which can be used to create a new actor. */
|
||||||
adjustForTier(tier) {
|
adjustForTier(tier) {
|
||||||
const source = this.parent.toObject(true);
|
const source = this.parent.toObject(true);
|
||||||
|
return getTierAdjustedAdversary(source, tier);
|
||||||
/** @type {(2 | 3 | 4)[]} */
|
|
||||||
const tiers = new Array(Math.abs(tier - this.tier))
|
|
||||||
.fill(0)
|
|
||||||
.map((_, idx) => idx + Math.min(tier, this.tier) + 1);
|
|
||||||
if (tier < this.tier) tiers.reverse();
|
|
||||||
const typeData = adversaryScalingData[source.system.type] ?? adversaryScalingData[source.system.standard];
|
|
||||||
const tierEntries = tiers.map(t => ({ tier: t, ...typeData[t] }));
|
|
||||||
|
|
||||||
// Apply simple tier changes
|
|
||||||
const scale = tier > this.tier ? 1 : -1;
|
|
||||||
for (const entry of tierEntries) {
|
|
||||||
source.system.difficulty += scale * entry.difficulty;
|
|
||||||
source.system.damageThresholds.major += scale * entry.majorThreshold;
|
|
||||||
source.system.damageThresholds.severe += scale * entry.severeThreshold;
|
|
||||||
source.system.resources.hitPoints.max += scale * entry.hp;
|
|
||||||
source.system.resources.stress.max += scale * entry.stress;
|
|
||||||
source.system.attack.roll.bonus += scale * entry.attack;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the mean and standard deviation of expected damage in the previous and new tier
|
|
||||||
// The data we have is for attack scaling, but we reuse this for action scaling later
|
|
||||||
const expectedDamageData = adversaryExpectedDamage[source.system.type] ?? adversaryExpectedDamage.basic;
|
|
||||||
const damageMeta = {
|
|
||||||
currentDamageRange: { tier: source.system.tier, ...expectedDamageData[source.system.tier] },
|
|
||||||
newDamageRange: { tier, ...expectedDamageData[tier] },
|
|
||||||
type: 'attack'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update damage of base attack
|
|
||||||
try {
|
|
||||||
this.#adjustActionDamage(source.system.attack, damageMeta);
|
|
||||||
} catch (err) {
|
|
||||||
ui.notifications.warn('Failed to convert attack damage of adversary');
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update damage of each item action, making sure to also update the description if possible
|
|
||||||
const damageRegex = /@Damage\[([^\[\]]*)\]({[^}]*})?/g;
|
|
||||||
for (const item of source.items) {
|
|
||||||
// Replace damage inlines with new formulas
|
|
||||||
for (const withDescription of [item.system, ...Object.values(item.system.actions)]) {
|
|
||||||
withDescription.description = withDescription.description.replace(damageRegex, (match, inner) => {
|
|
||||||
const { value: formula } = parseInlineParams(inner);
|
|
||||||
if (!formula || !type) return match;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const adjusted = this.#calculateAdjustedDamage(formula, { ...damageMeta, type: 'action' });
|
|
||||||
const newFormula = [
|
|
||||||
adjusted.diceQuantity ? `${adjusted.diceQuantity}d${adjusted.faces}` : null,
|
|
||||||
adjusted.bonus
|
|
||||||
]
|
|
||||||
.filter(p => !!p)
|
|
||||||
.join('+');
|
|
||||||
return match.replace(formula, newFormula);
|
|
||||||
} catch {
|
|
||||||
return match;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update damage in item actions
|
|
||||||
// Parse damage, and convert all formula matches in the descriptions to the new damage
|
|
||||||
for (const action of Object.values(item.system.actions)) {
|
|
||||||
try {
|
|
||||||
const result = this.#adjustActionDamage(action, { ...damageMeta, type: 'action' });
|
|
||||||
if (!result) continue;
|
|
||||||
|
|
||||||
for (const { previousFormula, formula } of Object.values(result)) {
|
|
||||||
const oldFormulaRegexp = new RegExp(
|
|
||||||
previousFormula.replace(' ', '').replace('+', '(?:\\s)?\\+(?:\\s)?')
|
|
||||||
);
|
|
||||||
item.system.description = item.system.description.replace(oldFormulaRegexp, formula);
|
|
||||||
action.description = action.description.replace(oldFormulaRegexp, formula);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
ui.notifications.warn(`Failed to convert action damage for item ${item.name}`);
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally set the tier of the source data, now that everything is complete
|
|
||||||
source.system.tier = tier;
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a damage object to a new damage range
|
|
||||||
* @returns {{ diceQuantity: number; faces: number; bonus: number }} the adjusted result as a combined term
|
|
||||||
* @throws error if the formula is the wrong type
|
|
||||||
*/
|
|
||||||
#calculateAdjustedDamage(formula, { currentDamageRange, newDamageRange, type }) {
|
|
||||||
const terms = parseTermsFromSimpleFormula(formula);
|
|
||||||
const flatTerms = terms.filter(t => t.diceQuantity === 0);
|
|
||||||
const diceTerms = terms.filter(t => t.diceQuantity > 0);
|
|
||||||
if (flatTerms.length > 1 || diceTerms.length > 1) {
|
|
||||||
throw new Error('invalid formula for conversion');
|
|
||||||
}
|
|
||||||
const value = {
|
|
||||||
...(diceTerms[0] ?? { diceQuantity: 0, faces: 1 }),
|
|
||||||
bonus: flatTerms[0]?.bonus ?? 0
|
|
||||||
};
|
|
||||||
const previousExpected = calculateExpectedValue(value);
|
|
||||||
if (previousExpected === 0) return value; // nothing to do
|
|
||||||
|
|
||||||
const dieSizes = [4, 6, 8, 10, 12, 20];
|
|
||||||
const steps = newDamageRange.tier - currentDamageRange.tier;
|
|
||||||
const increasing = steps > 0;
|
|
||||||
const deviation = (previousExpected - currentDamageRange.mean) / currentDamageRange.deviation;
|
|
||||||
const expected = Math.max(1, newDamageRange.mean + newDamageRange.deviation * deviation);
|
|
||||||
|
|
||||||
// If this was just a flat number, convert to the expected damage and exit
|
|
||||||
if (value.diceQuantity === 0) {
|
|
||||||
value.bonus = Math.round(expected);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getExpectedDie = () => calculateExpectedValue({ diceQuantity: 1, faces: value.faces }) || 1;
|
|
||||||
const getBaseAverage = () => calculateExpectedValue({ ...value, bonus: 0 });
|
|
||||||
|
|
||||||
// Check the number of base overages over the expected die. In the end, if the bonus inflates too much, we add a die
|
|
||||||
const baseOverages = Math.floor(value.bonus / getExpectedDie());
|
|
||||||
|
|
||||||
// Prestep. Change number of dice for attacks, bump up/down for actions
|
|
||||||
// We never bump up to d20, though we might bump down from it
|
|
||||||
if (type === 'attack') {
|
|
||||||
const minimum = increasing ? value.diceQuantity : 0;
|
|
||||||
value.diceQuantity = Math.max(minimum, newDamageRange.tier);
|
|
||||||
} else {
|
|
||||||
const currentIdx = dieSizes.indexOf(value.faces);
|
|
||||||
value.faces = dieSizes[Math.clamp(currentIdx + steps, 0, 4)];
|
|
||||||
}
|
|
||||||
|
|
||||||
value.bonus = Math.round(expected - getBaseAverage());
|
|
||||||
|
|
||||||
// Attempt to handle negative values.
|
|
||||||
// If we can do it with only step downs, do so. Otherwise remove tier dice, and try again
|
|
||||||
if (value.bonus < 0) {
|
|
||||||
let stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity);
|
|
||||||
const currentIdx = dieSizes.indexOf(value.faces);
|
|
||||||
|
|
||||||
// If step downs alone don't suffice, change the flat modifier, then calculate steps required again
|
|
||||||
// If this isn't sufficient, the result will be slightly off. This is unlikely to happen
|
|
||||||
if (type !== 'attack' && stepsRequired > currentIdx && value.diceQuantity > 0) {
|
|
||||||
value.diceQuantity -= increasing ? 1 : Math.abs(steps);
|
|
||||||
value.bonus = Math.round(expected - getBaseAverage());
|
|
||||||
if (value.bonus >= 0) return value; // complete
|
|
||||||
}
|
|
||||||
|
|
||||||
stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity);
|
|
||||||
value.faces = dieSizes[Math.max(0, currentIdx - stepsRequired)];
|
|
||||||
value.bonus = Math.max(0, Math.round(expected - getBaseAverage()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If value is really high, we add a number of dice based on the number of overages
|
|
||||||
// This attempts to preserve a similar amount of variance when increasing an action
|
|
||||||
const overagesToRemove = Math.floor(value.bonus / getExpectedDie()) - baseOverages;
|
|
||||||
if (type !== 'attack' && increasing && overagesToRemove > 0) {
|
|
||||||
value.diceQuantity += overagesToRemove;
|
|
||||||
value.bonus = Math.round(expected - getBaseAverage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates damage to reflect a specific value.
|
|
||||||
* @throws if damage structure is invalid for conversion
|
|
||||||
* @returns the converted formula and value as a simplified term, or null if it doesn't deal HP damage
|
|
||||||
*/
|
|
||||||
#adjustActionDamage(action, damageMeta) {
|
|
||||||
if (!action.damage?.parts.hitPoints) return null;
|
|
||||||
|
|
||||||
const result = {};
|
|
||||||
for (const property of ['value', 'valueAlt']) {
|
|
||||||
const data = action.damage.parts.hitPoints[property];
|
|
||||||
const previousFormula = data.custom.enabled
|
|
||||||
? data.custom.formula
|
|
||||||
: [data.flatMultiplier ? `${data.flatMultiplier}${data.dice}` : 0, data.bonus ?? 0]
|
|
||||||
.filter(p => !!p)
|
|
||||||
.join('+');
|
|
||||||
const value = this.#calculateAdjustedDamage(previousFormula, damageMeta);
|
|
||||||
const formula = [value.diceQuantity ? `${value.diceQuantity}d${value.faces}` : null, value.bonus]
|
|
||||||
.filter(p => !!p)
|
|
||||||
.join('+');
|
|
||||||
if (value.diceQuantity) {
|
|
||||||
data.custom.enabled = false;
|
|
||||||
data.bonus = value.bonus;
|
|
||||||
data.dice = `d${value.faces}`;
|
|
||||||
data.flatMultiplier = value.diceQuantity;
|
|
||||||
} else if (!value.diceQuantity) {
|
|
||||||
data.custom.enabled = true;
|
|
||||||
data.custom.formula = formula;
|
|
||||||
}
|
|
||||||
|
|
||||||
result[property] = { previousFormula, formula, value };
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
43
module/data/actor/npc.mjs
Normal file
43
module/data/actor/npc.mjs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import DHNPCSettings from '../../applications/sheets-configs/npc-settings.mjs';
|
||||||
|
import BaseDataActor from './base.mjs';
|
||||||
|
|
||||||
|
export default class DhpNPC extends BaseDataActor {
|
||||||
|
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.NPC'];
|
||||||
|
|
||||||
|
static get metadata() {
|
||||||
|
return foundry.utils.mergeObject(super.metadata, {
|
||||||
|
label: 'TYPES.Actor.npc',
|
||||||
|
type: 'npc',
|
||||||
|
settingSheet: DHNPCSettings,
|
||||||
|
hasResistances: false,
|
||||||
|
hasAttribution: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static defineSchema() {
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
return {
|
||||||
|
...super.defineSchema(),
|
||||||
|
difficulty: new fields.NumberField({
|
||||||
|
nullable: true,
|
||||||
|
initial: null,
|
||||||
|
integer: true,
|
||||||
|
label: 'DAGGERHEART.GENERAL.difficulty'
|
||||||
|
}),
|
||||||
|
description: new fields.HTMLField({ label: 'DAGGERHEART.GENERAL.description' }),
|
||||||
|
motives: new fields.StringField(),
|
||||||
|
notes: new fields.HTMLField()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**@inheritdoc */
|
||||||
|
static DEFAULT_ICON = 'systems/daggerheart/assets/icons/documents/actors/drama-masks.svg';
|
||||||
|
|
||||||
|
get features() {
|
||||||
|
return this.parent.items.filter(x => x.type === 'feature');
|
||||||
|
}
|
||||||
|
|
||||||
|
isItemValid(source) {
|
||||||
|
return super.isItemValid(source) || source.type === 'feature';
|
||||||
|
}
|
||||||
|
}
|
||||||
218
module/data/actor/tierAdjustment.mjs
Normal file
218
module/data/actor/tierAdjustment.mjs
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
import { calculateExpectedValue, parseTermsFromSimpleFormula } from '../../helpers/utils.mjs';
|
||||||
|
import { adversaryExpectedDamage, adversaryScalingData } from '../../config/actorConfig.mjs';
|
||||||
|
|
||||||
|
export function getTierAdjustedAdversary(source, tier) {
|
||||||
|
const currentTier = source.tier ?? 1;
|
||||||
|
|
||||||
|
/** @type {(2 | 3 | 4)[]} */
|
||||||
|
const tiers = new Array(Math.abs(tier - currentTier))
|
||||||
|
.fill(0)
|
||||||
|
.map((_, idx) => idx + Math.min(tier, currentTier) + 1);
|
||||||
|
if (tier < currentTier) tiers.reverse();
|
||||||
|
const typeData = adversaryScalingData[source.system.type] ?? adversaryScalingData[source.system.standard];
|
||||||
|
const tierEntries = tiers.map(t => ({ tier: t, ...typeData[t] }));
|
||||||
|
|
||||||
|
// Apply simple tier changes
|
||||||
|
const scale = tier > currentTier ? 1 : -1;
|
||||||
|
for (const entry of tierEntries) {
|
||||||
|
source.system.difficulty += scale * entry.difficulty;
|
||||||
|
source.system.damageThresholds.major += scale * entry.majorThreshold;
|
||||||
|
source.system.damageThresholds.severe += scale * entry.severeThreshold;
|
||||||
|
source.system.resources.hitPoints.max += scale * entry.hp;
|
||||||
|
source.system.resources.stress.max += scale * entry.stress;
|
||||||
|
source.system.attack.roll.bonus += scale * entry.attack;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the mean and standard deviation of expected damage in the previous and new tier
|
||||||
|
// The data we have is for attack scaling, but we reuse this for action scaling later
|
||||||
|
const expectedDamageData = adversaryExpectedDamage[source.system.type] ?? adversaryExpectedDamage.basic;
|
||||||
|
const damageMeta = {
|
||||||
|
currentDamageRange: { tier: source.system.tier, ...expectedDamageData[source.system.tier] },
|
||||||
|
newDamageRange: { tier, ...expectedDamageData[tier] }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store initial attack damage for abilities that have you deal a "standard attack"
|
||||||
|
const initialAttack = {
|
||||||
|
type: source.system.attack.damage?.parts.hitPoints?.type?.toSorted(),
|
||||||
|
value: getDamagePartsFormula(source.system.attack.damage?.parts.hitPoints?.value)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update damage of base attack.
|
||||||
|
try {
|
||||||
|
const damage = source.system.attack.damage;
|
||||||
|
if (!damage?.parts.hitPoints) throw new Error('Unexpected missing attack in adversary');
|
||||||
|
|
||||||
|
for (const property of ['value', 'valueAlt']) {
|
||||||
|
const data = damage.parts.hitPoints[property];
|
||||||
|
const previousFormula = getDamagePartsFormula(data);
|
||||||
|
const { value, formula } = calculateAdjustedDamage(previousFormula, 'attack', damageMeta);
|
||||||
|
applyAdjustedDamage(data, value, formula);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
ui.notifications.warn('Failed to convert attack damage of adversary');
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update damage of each item action, making sure to also update the description if possible
|
||||||
|
const damageRegex = /@Damage\[([^\[\]]*)\]({[^}]*})?/g;
|
||||||
|
for (const item of source.items) {
|
||||||
|
// Replace damage inlines with new formulas. Keep a record for a specific check later
|
||||||
|
const descriptionFormulas = [];
|
||||||
|
for (const withDescription of [item.system, ...Object.values(item.system.actions)]) {
|
||||||
|
withDescription.description = withDescription.description.replace(damageRegex, (match, inner) => {
|
||||||
|
const { value: formula } = parseInlineParams(inner);
|
||||||
|
if (!formula || !type) return match;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newFormula = calculateAdjustedDamage(formula, 'action', damageMeta)?.formula;
|
||||||
|
descriptionFormulas.push(formula);
|
||||||
|
return match.replace(formula, newFormula);
|
||||||
|
} catch {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update damage in item actions and convert all formula matches in the descriptions to the new damage
|
||||||
|
for (const action of Object.values(item.system.actions)) {
|
||||||
|
if (!action.damage?.parts.hitPoints) continue;
|
||||||
|
try {
|
||||||
|
// Apply conversions and save a record. If it matches attack damage *and* Its not in the description, use attack conversion instead
|
||||||
|
const result = [];
|
||||||
|
for (const property of ['value', 'valueAlt']) {
|
||||||
|
const { [property]: data, type: damageType } = action.damage.parts.hitPoints;
|
||||||
|
const previousFormula = getDamagePartsFormula(data);
|
||||||
|
const isActuallyAttack =
|
||||||
|
previousFormula === initialAttack.value &&
|
||||||
|
foundry.utils.equals(damageType.toSorted(), initialAttack.type) &&
|
||||||
|
!descriptionFormulas.includes(previousFormula);
|
||||||
|
const type = isActuallyAttack ? 'attack' : 'action';
|
||||||
|
const { value, formula } = calculateAdjustedDamage(previousFormula, type, damageMeta);
|
||||||
|
applyAdjustedDamage(data, value, formula);
|
||||||
|
result.push({ previousFormula, formula });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override text in the description with those values
|
||||||
|
for (const { previousFormula, formula } of Object.values(result)) {
|
||||||
|
const oldFormulaRegexp = new RegExp(
|
||||||
|
previousFormula.replace(' ', '').replace('+', '(?:\\s)?\\+(?:\\s)?')
|
||||||
|
);
|
||||||
|
item.system.description = item.system.description.replace(oldFormulaRegexp, formula);
|
||||||
|
action.description = action.description.replace(oldFormulaRegexp, formula);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
ui.notifications.warn(`Failed to convert action damage for item ${item.name}`);
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally set the tier of the source data, now that everything is complete
|
||||||
|
source.system.tier = tier;
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a damage object to a new damage range
|
||||||
|
* @returns {{ diceQuantity: number; faces: number; bonus: number }} the adjusted result as a combined term
|
||||||
|
* @throws error if the formula is the wrong type
|
||||||
|
*/
|
||||||
|
function calculateAdjustedDamage(formula, type, { currentDamageRange, newDamageRange }) {
|
||||||
|
const terms = parseTermsFromSimpleFormula(formula);
|
||||||
|
const flatTerms = terms.filter(t => t.diceQuantity === 0);
|
||||||
|
const diceTerms = terms.filter(t => t.diceQuantity > 0);
|
||||||
|
if (flatTerms.length > 1 || diceTerms.length > 1) {
|
||||||
|
throw new Error('invalid formula for conversion');
|
||||||
|
}
|
||||||
|
const value = {
|
||||||
|
...(diceTerms[0] ?? { diceQuantity: 0, faces: 1 }),
|
||||||
|
bonus: flatTerms[0]?.bonus ?? 0
|
||||||
|
};
|
||||||
|
const previousExpected = calculateExpectedValue(value);
|
||||||
|
if (previousExpected === 0) return value; // nothing to do
|
||||||
|
|
||||||
|
const dieSizes = [4, 6, 8, 10, 12, 20];
|
||||||
|
const steps = newDamageRange.tier - currentDamageRange.tier;
|
||||||
|
const increasing = steps > 0;
|
||||||
|
const deviation = (previousExpected - currentDamageRange.mean) / currentDamageRange.deviation;
|
||||||
|
const expected = Math.max(1, newDamageRange.mean + newDamageRange.deviation * deviation);
|
||||||
|
|
||||||
|
// If this was just a flat number, convert to the expected damage and exit
|
||||||
|
if (value.diceQuantity === 0) {
|
||||||
|
value.bonus = Math.round(expected);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getExpectedDie = () => calculateExpectedValue({ diceQuantity: 1, faces: value.faces }) || 1;
|
||||||
|
const getBaseAverage = () => calculateExpectedValue({ ...value, bonus: 0 });
|
||||||
|
|
||||||
|
// Check the number of base overages over the expected die. In the end, if the bonus inflates too much, we add a die
|
||||||
|
const baseOverages = Math.floor(value.bonus / getExpectedDie());
|
||||||
|
|
||||||
|
// Prestep. Change number of dice for attacks, bump up/down for actions
|
||||||
|
// We never bump up to d20, though we might bump down from it
|
||||||
|
if (type === 'attack') {
|
||||||
|
const minimum = increasing ? value.diceQuantity : 0;
|
||||||
|
value.diceQuantity = Math.max(minimum, newDamageRange.tier);
|
||||||
|
} else {
|
||||||
|
const currentIdx = dieSizes.indexOf(value.faces);
|
||||||
|
value.faces = dieSizes[Math.clamp(currentIdx + steps, 0, 4)];
|
||||||
|
}
|
||||||
|
|
||||||
|
value.bonus = Math.round(expected - getBaseAverage());
|
||||||
|
|
||||||
|
// Attempt to handle negative values.
|
||||||
|
// If we can do it with only step downs, do so. Otherwise remove tier dice, and try again
|
||||||
|
if (value.bonus < 0) {
|
||||||
|
let stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity);
|
||||||
|
const currentIdx = dieSizes.indexOf(value.faces);
|
||||||
|
|
||||||
|
// If step downs alone don't suffice, change the flat modifier, then calculate steps required again
|
||||||
|
// If this isn't sufficient, the result will be slightly off. This is unlikely to happen
|
||||||
|
if (type !== 'attack' && stepsRequired > currentIdx && value.diceQuantity > 0) {
|
||||||
|
value.diceQuantity -= increasing ? 1 : Math.abs(steps);
|
||||||
|
value.bonus = Math.round(expected - getBaseAverage());
|
||||||
|
if (value.bonus >= 0) return value; // complete
|
||||||
|
}
|
||||||
|
|
||||||
|
stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity);
|
||||||
|
value.faces = dieSizes[Math.max(0, currentIdx - stepsRequired)];
|
||||||
|
value.bonus = Math.max(0, Math.round(expected - getBaseAverage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If value is really high, we add a number of dice based on the number of overages
|
||||||
|
// This attempts to preserve a similar amount of variance when increasing an action
|
||||||
|
const overagesToRemove = Math.floor(value.bonus / getExpectedDie()) - baseOverages;
|
||||||
|
if (type !== 'attack' && increasing && overagesToRemove > 0) {
|
||||||
|
value.diceQuantity += overagesToRemove;
|
||||||
|
value.bonus = Math.round(expected - getBaseAverage());
|
||||||
|
}
|
||||||
|
|
||||||
|
const newFormula = [value.diceQuantity ? `${value.diceQuantity}d${value.faces}` : null, value.bonus]
|
||||||
|
.filter(p => !!p)
|
||||||
|
.join('+');
|
||||||
|
return { value, formula: newFormula };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDamagePartsFormula(data) {
|
||||||
|
return data.custom.enabled
|
||||||
|
? data.custom.formula
|
||||||
|
: [data.flatMultiplier ? `${data.flatMultiplier}${data.dice}` : 0, data.bonus ?? 0].filter(p => !!p).join('+');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates damage to reflect a specific value.
|
||||||
|
* @throws if damage structure is invalid for conversion
|
||||||
|
* @returns the converted formula and value as a simplified term, or null if it doesn't deal HP damage
|
||||||
|
*/
|
||||||
|
function applyAdjustedDamage(diceData, value, formula) {
|
||||||
|
if (value.diceQuantity) {
|
||||||
|
diceData.custom.enabled = false;
|
||||||
|
diceData.bonus = value.bonus;
|
||||||
|
diceData.dice = `d${value.faces}`;
|
||||||
|
diceData.flatMultiplier = value.diceQuantity;
|
||||||
|
} else if (!value.diceQuantity) {
|
||||||
|
diceData.custom.enabled = true;
|
||||||
|
diceData.custom.formula = formula;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { triggerChatRollFx } from '../../helpers/utils.mjs';
|
||||||
|
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
const targetsField = () =>
|
const targetsField = () =>
|
||||||
|
|
@ -130,6 +132,35 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* TODO: Change how damage data is stored somehow to enable better rerolling */
|
||||||
|
async getRerolledDamage() {
|
||||||
|
if (!this.damage) return;
|
||||||
|
|
||||||
|
const rerolls = [];
|
||||||
|
const update = { system: { damage: {} } };
|
||||||
|
for (const partKey in this.damage) {
|
||||||
|
const part = this.damage[partKey];
|
||||||
|
const testRoll = Roll.fromData(part.parts[0].roll);
|
||||||
|
const rerolled = await testRoll.reroll();
|
||||||
|
rerolls.push(rerolled);
|
||||||
|
|
||||||
|
if (!update.system.damage[partKey]) update.system.damage[partKey] = { parts: [part.parts[0]] };
|
||||||
|
const partData = update.system.damage[partKey].parts[0];
|
||||||
|
update.system.damage[partKey].total = rerolled.total;
|
||||||
|
partData.modifierTotal = rerolled.terms.reduce((acc, x) => {
|
||||||
|
if (x.isDeterministic && !x.operator) acc += x.total;
|
||||||
|
return acc;
|
||||||
|
}, 0);
|
||||||
|
partData.dice = rerolled.dice.map(d => ({ ...d.toJSON(), dice: d.denomination }));
|
||||||
|
partData.total = rerolled.total;
|
||||||
|
partData.roll = rerolled.toJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
await triggerChatRollFx(rerolls);
|
||||||
|
|
||||||
|
return update;
|
||||||
|
}
|
||||||
|
|
||||||
registerTargetHook() {
|
registerTargetHook() {
|
||||||
if (!this.parent.isAuthor || !this.hasTarget) return;
|
if (!this.parent.isAuthor || !this.hasTarget) return;
|
||||||
if (this.targetMode && this.parent.targetHook !== null) {
|
if (this.targetMode && this.parent.targetHook !== null) {
|
||||||
|
|
|
||||||
|
|
@ -72,9 +72,6 @@ export default class DamageField extends fields.SchemaField {
|
||||||
damageConfig.source.message = messageId;
|
damageConfig.source.message = messageId;
|
||||||
damageConfig.directDamage = !!damageConfig.source?.message;
|
damageConfig.directDamage = !!damageConfig.source?.message;
|
||||||
|
|
||||||
// if(damageConfig.source?.message && game.modules.get('dice-so-nice')?.active)
|
|
||||||
// await game.dice3d.waitFor3DAnimationByMessageID(damageConfig.source.message);
|
|
||||||
|
|
||||||
const damageResult = await CONFIG.Dice.daggerheart.DamageRoll.build(damageConfig);
|
const damageResult = await CONFIG.Dice.daggerheart.DamageRoll.build(damageConfig);
|
||||||
if (!damageResult) return false;
|
if (!damageResult) return false;
|
||||||
if (damageResult.actionChatMessageHandled) config.actionChatMessageHandled = true;
|
if (damageResult.actionChatMessageHandled) config.actionChatMessageHandled = true;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { emitGMUpdate, GMUpdateEvent } from '../../../systemRegistration/socket.mjs';
|
|
||||||
|
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
export default class EffectsField extends fields.ArrayField {
|
export default class EffectsField extends fields.ArrayField {
|
||||||
|
|
@ -34,8 +32,7 @@ export default class EffectsField extends fields.ArrayField {
|
||||||
}
|
}
|
||||||
if (EffectsField.getAutomation() || force) {
|
if (EffectsField.getAutomation() || force) {
|
||||||
targets ??= (message.system?.targets ?? config.targets).filter(t => !config.hasRoll || t.hit);
|
targets ??= (message.system?.targets ?? config.targets).filter(t => !config.hasRoll || t.hit);
|
||||||
await emitGMUpdate(GMUpdateEvent.UpdateEffect, EffectsField.applyEffects.bind(this), targets, this.uuid);
|
EffectsField.applyEffects.call(this, targets);
|
||||||
// EffectsField.applyEffects.call(this, config.targets.filter(t => !config.hasRoll || t.hit));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,7 +56,7 @@ export default class EffectsField extends fields.ArrayField {
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
|
||||||
const messageToken = token.document ?? token;
|
const messageToken = token.document ?? token;
|
||||||
const conditionImmunities = messageToken.actor.system.rules.conditionImmunities ?? {};
|
const conditionImmunities = messageToken.actor.system.rules?.conditionImmunities ?? {};
|
||||||
messageTargets.push({
|
messageTargets.push({
|
||||||
token: messageToken,
|
token: messageToken,
|
||||||
conditionImmunities: Object.values(conditionImmunities).some(x => x)
|
conditionImmunities: Object.values(conditionImmunities).some(x => x)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { itemAbleRollParse } from '../../../helpers/utils.mjs';
|
import { itemAbleRollParse, triggerChatRollFx } from '../../../helpers/utils.mjs';
|
||||||
import FormulaField from '../formulaField.mjs';
|
import FormulaField from '../formulaField.mjs';
|
||||||
|
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
@ -40,7 +40,7 @@ export default class DHSummonField extends fields.ArrayField {
|
||||||
const roll = new Roll(itemAbleRollParse(summon.count, this.actor, this.item));
|
const roll = new Roll(itemAbleRollParse(summon.count, this.actor, this.item));
|
||||||
await roll.evaluate();
|
await roll.evaluate();
|
||||||
const count = roll.total;
|
const count = roll.total;
|
||||||
if (!roll.isDeterministic && game.modules.get('dice-so-nice')?.active) rolls.push(roll);
|
if (!roll.isDeterministic) rolls.push(roll);
|
||||||
|
|
||||||
const actor = await DHSummonField.getWorldActor(await foundry.utils.fromUuid(summon.actorUUID));
|
const actor = await DHSummonField.getWorldActor(await foundry.utils.fromUuid(summon.actorUUID));
|
||||||
/* Extending summon data in memory so it's available in actionField.toChat. Think it's harmless, but ugly. Could maybe find a better way. */
|
/* Extending summon data in memory so it's available in actionField.toChat. Think it's harmless, but ugly. Could maybe find a better way. */
|
||||||
|
|
@ -56,7 +56,7 @@ export default class DHSummonField extends fields.ArrayField {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rolls.length) await Promise.all(rolls.map(roll => game.dice3d.showForRoll(roll, game.user, true)));
|
if (rolls.length) await triggerChatRollFx(rolls);
|
||||||
|
|
||||||
this.actor.sheet?.minimize();
|
this.actor.sheet?.minimize();
|
||||||
DHSummonField.handleSummon(summonData, this.actor);
|
DHSummonField.handleSummon(summonData, this.actor);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import BaseDataItem from './base.mjs';
|
||||||
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
|
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
|
||||||
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
|
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
|
||||||
import ItemLinkFields from '../fields/itemLinkFields.mjs';
|
import ItemLinkFields from '../fields/itemLinkFields.mjs';
|
||||||
import { addLinkedItemsDiff, getFeaturesHTMLData, updateLinkedItemApps } from '../../helpers/utils.mjs';
|
import { addLinkedItemsDiff, fromUuids, getFeaturesHTMLData, updateLinkedItemApps } from '../../helpers/utils.mjs';
|
||||||
|
|
||||||
export default class DHClass extends BaseDataItem {
|
export default class DHClass extends BaseDataItem {
|
||||||
/** @inheritDoc */
|
/** @inheritDoc */
|
||||||
|
|
@ -73,15 +73,16 @@ export default class DHClass extends BaseDataItem {
|
||||||
const uuids = [this.parent.uuid, this.parent._stats?.compendiumSource].filter(u => !!u);
|
const uuids = [this.parent.uuid, this.parent._stats?.compendiumSource].filter(u => !!u);
|
||||||
const subclasses = game.items.filter(x => x.type === 'subclass' && uuids.includes(x.system.linkedClass));
|
const subclasses = game.items.filter(x => x.type === 'subclass' && uuids.includes(x.system.linkedClass));
|
||||||
for (const pack of game.packs) {
|
for (const pack of game.packs) {
|
||||||
|
const packIds = [];
|
||||||
const indexes = await pack.getIndex({ fields: ['system.linkedClass'] });
|
const indexes = await pack.getIndex({ fields: ['system.linkedClass'] });
|
||||||
for (const index of indexes) {
|
for (const index of indexes) {
|
||||||
if (index.type !== 'subclass') continue;
|
if (index.type !== 'subclass') continue;
|
||||||
if (!uuids.includes(index.system?.linkedClass)) continue;
|
if (!uuids.includes(index.system?.linkedClass)) continue;
|
||||||
if (subclasses.find(x => x.uuid === index.uuid)) continue;
|
if (subclasses.find(x => x.uuid === index.uuid)) continue;
|
||||||
|
packIds.push(index._id);
|
||||||
const subclass = await foundry.utils.fromUuid(index.uuid);
|
|
||||||
subclasses.push(subclass);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (packIds.length > 0) subclasses.push(...(await pack.getDocuments({ _id__in: packIds })));
|
||||||
}
|
}
|
||||||
|
|
||||||
return subclasses;
|
return subclasses;
|
||||||
|
|
@ -216,6 +217,10 @@ export default class DHClass extends BaseDataItem {
|
||||||
classItems.push(contentLink.outerHTML);
|
classItems.push(contentLink.outerHTML);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Preload all class features for acquisition from the cache
|
||||||
|
// todo: make feature acquisition async and replace feature helpers for methods
|
||||||
|
await fromUuids(this._source.features.map(f => f.item));
|
||||||
|
|
||||||
const hopeFeatures = await getFeaturesHTMLData(this.hopeFeatures);
|
const hopeFeatures = await getFeaturesHTMLData(this.hopeFeatures);
|
||||||
const classFeatures = await getFeaturesHTMLData(this.classFeatures);
|
const classFeatures = await getFeaturesHTMLData(this.classFeatures);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { getFeaturesHTMLData } from '../../helpers/utils.mjs';
|
import { fromUuids, getFeaturesHTMLData } from '../../helpers/utils.mjs';
|
||||||
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
|
|
||||||
import ItemLinkFields from '../fields/itemLinkFields.mjs';
|
import ItemLinkFields from '../fields/itemLinkFields.mjs';
|
||||||
import BaseDataItem from './base.mjs';
|
import BaseDataItem from './base.mjs';
|
||||||
|
|
||||||
|
|
@ -91,6 +90,11 @@ export default class DHSubclass extends BaseDataItem {
|
||||||
const spellcastTrait = this.spellcastingTrait
|
const spellcastTrait = this.spellcastingTrait
|
||||||
? game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.spellcastingTrait].label)
|
? game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.spellcastingTrait].label)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
// Preload all class features for acquisition from the cache
|
||||||
|
// todo: make feature acquisition async and replace feature helpers for methods
|
||||||
|
await fromUuids(this._source.features.map(f => f.item));
|
||||||
|
|
||||||
const foundationFeatures = await getFeaturesHTMLData(this.foundationFeatures);
|
const foundationFeatures = await getFeaturesHTMLData(this.foundationFeatures);
|
||||||
const specializationFeatures = await getFeaturesHTMLData(this.specializationFeatures);
|
const specializationFeatures = await getFeaturesHTMLData(this.specializationFeatures);
|
||||||
const masteryFeatures = await getFeaturesHTMLData(this.masteryFeatures);
|
const masteryFeatures = await getFeaturesHTMLData(this.masteryFeatures);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
|
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
|
||||||
|
import { triggerChatRollFx } from '../helpers/utils.mjs';
|
||||||
import DHRoll from './dhRoll.mjs';
|
import DHRoll from './dhRoll.mjs';
|
||||||
|
|
||||||
export default class D20Roll extends DHRoll {
|
export default class D20Roll extends DHRoll {
|
||||||
|
|
@ -224,4 +225,15 @@ export default class D20Roll extends DHRoll {
|
||||||
resetFormula() {
|
resetFormula() {
|
||||||
return (this._formula = this.constructor.getFormula(this.terms));
|
return (this._formula = this.constructor.getFormula(this.terms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async reroll(options) {
|
||||||
|
const result = await super.reroll(options);
|
||||||
|
if (this instanceof game.system.api.dice.DualityRoll) return result;
|
||||||
|
|
||||||
|
if (options?.liveRoll) {
|
||||||
|
await triggerChatRollFx([result]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import DamageDialog from '../applications/dialogs/damageDialog.mjs';
|
import DamageDialog from '../applications/dialogs/damageDialog.mjs';
|
||||||
import { parseRallyDice } from '../helpers/utils.mjs';
|
import { parseRallyDice, triggerChatRollFx } from '../helpers/utils.mjs';
|
||||||
import DHRoll from './dhRoll.mjs';
|
import DHRoll from './dhRoll.mjs';
|
||||||
|
|
||||||
export default class DamageRoll extends DHRoll {
|
export default class DamageRoll extends DHRoll {
|
||||||
|
|
@ -18,7 +18,12 @@ export default class DamageRoll extends DHRoll {
|
||||||
if (config.evaluate !== false) for (const roll of config.roll) await roll.roll.evaluate();
|
if (config.evaluate !== false) for (const roll of config.roll) await roll.roll.evaluate();
|
||||||
|
|
||||||
roll._evaluated = true;
|
roll._evaluated = true;
|
||||||
const parts = config.roll.map(r => this.postEvaluate(r));
|
|
||||||
|
const parts = [];
|
||||||
|
for (const roll of config.roll) {
|
||||||
|
parts.push(this.postEvaluate(roll));
|
||||||
|
roll.roll = JSON.stringify(roll.roll.toJSON());
|
||||||
|
}
|
||||||
|
|
||||||
config.damage = this.unifyDamageRoll(parts);
|
config.damage = this.unifyDamageRoll(parts);
|
||||||
}
|
}
|
||||||
|
|
@ -38,25 +43,24 @@ export default class DamageRoll extends DHRoll {
|
||||||
const chatMessage = config.source?.message
|
const chatMessage = config.source?.message
|
||||||
? ui.chat.collection.get(config.source.message)
|
? ui.chat.collection.get(config.source.message)
|
||||||
: getDocumentClass('ChatMessage').applyMode({}, config.rollMode ?? 'public');
|
: getDocumentClass('ChatMessage').applyMode({}, config.rollMode ?? 'public');
|
||||||
|
|
||||||
|
const diceRolls = [];
|
||||||
if (game.modules.get('dice-so-nice')?.active) {
|
if (game.modules.get('dice-so-nice')?.active) {
|
||||||
|
config.mute = true;
|
||||||
const pool = foundry.dice.terms.PoolTerm.fromRolls(
|
const pool = foundry.dice.terms.PoolTerm.fromRolls(
|
||||||
Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll))
|
Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll))
|
||||||
),
|
|
||||||
diceRoll = Roll.fromTerms([pool]);
|
|
||||||
await game.dice3d.showForRoll(
|
|
||||||
diceRoll,
|
|
||||||
game.user,
|
|
||||||
true,
|
|
||||||
chatMessage.whisper?.length > 0 ? chatMessage.whisper : null,
|
|
||||||
chatMessage.blind
|
|
||||||
);
|
);
|
||||||
config.mute = true;
|
diceRolls.push(Roll.fromTerms([pool]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await triggerChatRollFx(diceRolls, {
|
||||||
|
whisper: chatMessage.whisper?.length > 0 ? chatMessage.whisper : null,
|
||||||
|
blind: chatMessage.blind
|
||||||
|
});
|
||||||
await super.buildPost(roll, config, message);
|
await super.buildPost(roll, config, message);
|
||||||
|
|
||||||
if (config.source?.message) {
|
if (config.source?.message) {
|
||||||
chatMessage.update({ 'system.damage': config.damage });
|
chatMessage.update({ 'system.damage': config.damage });
|
||||||
|
|
||||||
if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -319,9 +323,10 @@ export default class DamageRoll extends DHRoll {
|
||||||
const newIndex = parsedDiceTerms[dice].results.length;
|
const newIndex = parsedDiceTerms[dice].results.length;
|
||||||
await term.reroll(`/r1=${termResult.result}`);
|
await term.reroll(`/r1=${termResult.result}`);
|
||||||
|
|
||||||
|
const diceRolls = [];
|
||||||
if (game.modules.get('dice-so-nice')?.active) {
|
if (game.modules.get('dice-so-nice')?.active) {
|
||||||
const newResult = parsedDiceTerms[dice].results[newIndex];
|
const newResult = parsedDiceTerms[dice].results[newIndex];
|
||||||
const diceSoNiceRoll = {
|
diceRolls.push({
|
||||||
_evaluated: true,
|
_evaluated: true,
|
||||||
dice: [
|
dice: [
|
||||||
new foundry.dice.terms.Die({
|
new foundry.dice.terms.Die({
|
||||||
|
|
@ -332,11 +337,10 @@ export default class DamageRoll extends DHRoll {
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
options: { appearance: {} }
|
options: { appearance: {} }
|
||||||
};
|
});
|
||||||
|
|
||||||
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await triggerChatRollFx(diceRolls);
|
||||||
await parsedRoll.evaluate();
|
await parsedRoll.evaluate();
|
||||||
|
|
||||||
const results = parsedRoll.dice[dice].results.map(result => ({
|
const results = parsedRoll.dice[dice].results.map(result => ({
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
|
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
|
||||||
|
import { triggerChatRollFx } from '../helpers/utils.mjs';
|
||||||
|
|
||||||
export default class DHRoll extends Roll {
|
export default class DHRoll extends Roll {
|
||||||
baseTerms = [];
|
baseTerms = [];
|
||||||
|
|
@ -75,9 +76,7 @@ export default class DHRoll extends Roll {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.skips?.createMessage) {
|
if (config.skips?.createMessage) {
|
||||||
if (game.modules.get('dice-so-nice')?.active) {
|
await triggerChatRollFx([roll]);
|
||||||
await game.dice3d.showForRoll(roll, game.user, true);
|
|
||||||
}
|
|
||||||
} else if (!config.source?.message) {
|
} else if (!config.source?.message) {
|
||||||
config.message = await this.toMessage(roll, config);
|
config.message = await this.toMessage(roll, config);
|
||||||
}
|
}
|
||||||
|
|
@ -85,6 +84,7 @@ export default class DHRoll extends Roll {
|
||||||
|
|
||||||
static postEvaluate(roll, config = {}) {
|
static postEvaluate(roll, config = {}) {
|
||||||
return {
|
return {
|
||||||
|
...roll.options.roll,
|
||||||
total: roll.total,
|
total: roll.total,
|
||||||
formula: roll.formula,
|
formula: roll.formula,
|
||||||
dice: roll.dice.map(d => ({
|
dice: roll.dice.map(d => ({
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ResourceUpdateMap } from '../../data/action/baseAction.mjs';
|
import { updateResourcesForDualityReroll } from '../helpers.mjs';
|
||||||
|
|
||||||
export default class DualityDie extends foundry.dice.terms.Die {
|
export default class DualityDie extends foundry.dice.terms.Die {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
|
|
@ -12,24 +12,6 @@ export default class DualityDie extends foundry.dice.terms.Die {
|
||||||
return roll.withHope ? 1 : roll.withFear ? -1 : 0;
|
return roll.withHope ? 1 : roll.withFear ? -1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#updateResources(oldDuality, newDuality, actor) {
|
|
||||||
const { hopeFear } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
|
|
||||||
if (game.user.isGM ? !hopeFear.gm : !hopeFear.players) return;
|
|
||||||
|
|
||||||
const updates = [];
|
|
||||||
const hope = (newDuality >= 0 ? 1 : 0) - (oldDuality >= 0 ? 1 : 0);
|
|
||||||
const stress = (newDuality === 0 ? 1 : 0) - (oldDuality === 0 ? 1 : 0);
|
|
||||||
const fear = (newDuality === -1 ? 1 : 0) - (oldDuality === -1 ? 1 : 0);
|
|
||||||
|
|
||||||
if (hope !== 0) updates.push({ key: 'hope', value: hope, total: -1 * hope, enabled: true });
|
|
||||||
if (stress !== 0) updates.push({ key: 'stress', value: -1 * stress, total: stress, enabled: true });
|
|
||||||
if (fear !== 0) updates.push({ key: 'fear', value: fear, total: -1 * fear, enabled: true });
|
|
||||||
|
|
||||||
const resourceUpdates = new ResourceUpdateMap(actor);
|
|
||||||
resourceUpdates.addResources(updates);
|
|
||||||
resourceUpdates.updateResources();
|
|
||||||
}
|
|
||||||
|
|
||||||
async reroll(modifier, options) {
|
async reroll(modifier, options) {
|
||||||
const oldDuality = this.#getDualityState(options.liveRoll.roll);
|
const oldDuality = this.#getDualityState(options.liveRoll.roll);
|
||||||
await super.reroll(modifier, options);
|
await super.reroll(modifier, options);
|
||||||
|
|
@ -57,7 +39,7 @@ export default class DualityDie extends foundry.dice.terms.Die {
|
||||||
if (options.liveRoll.isReaction) return;
|
if (options.liveRoll.isReaction) return;
|
||||||
|
|
||||||
const newDuality = this.#getDualityState(options.liveRoll.roll);
|
const newDuality = this.#getDualityState(options.liveRoll.roll);
|
||||||
this.#updateResources(oldDuality, newDuality, options.liveRoll.actor);
|
updateResourcesForDualityReroll(oldDuality, newDuality, options.liveRoll.actor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
|
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
|
||||||
import D20Roll from './d20Roll.mjs';
|
import D20Roll from './d20Roll.mjs';
|
||||||
import { parseRallyDice, setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs';
|
import { parseRallyDice, setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs';
|
||||||
|
import { getDiceSoNicePresets } from '../config/generalConfig.mjs';
|
||||||
|
import { updateResourcesForDualityReroll } from './helpers.mjs';
|
||||||
|
|
||||||
export default class DualityRoll extends D20Roll {
|
export default class DualityRoll extends D20Roll {
|
||||||
_advantageNumber = 1;
|
_advantageNumber = 1;
|
||||||
|
|
@ -130,20 +132,14 @@ export default class DualityRoll extends D20Roll {
|
||||||
}
|
}
|
||||||
|
|
||||||
createBaseDice() {
|
createBaseDice() {
|
||||||
if (
|
|
||||||
this.dice[0] instanceof game.system.api.dice.diceTypes.HopeDie &&
|
|
||||||
this.dice[1] instanceof game.system.api.dice.diceTypes.FearDie
|
|
||||||
) {
|
|
||||||
this.terms = [this.terms[0], this.terms[1], this.terms[2]];
|
this.terms = [this.terms[0], this.terms[1], this.terms[2]];
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.terms[0] = new game.system.api.dice.diceTypes.HopeDie({
|
this.terms[0] = new game.system.api.dice.diceTypes.HopeDie({
|
||||||
faces: this.data.rules.dualityRoll?.defaultHopeDice ?? 12
|
faces: this.terms[0]?.faces ?? this.data.rules.dualityRoll?.defaultHopeDice ?? 12
|
||||||
});
|
});
|
||||||
this.terms[1] = new foundry.dice.terms.OperatorTerm({ operator: '+' });
|
this.terms[1] = new foundry.dice.terms.OperatorTerm({ operator: '+' });
|
||||||
this.terms[2] = new game.system.api.dice.diceTypes.FearDie({
|
this.terms[2] = new game.system.api.dice.diceTypes.FearDie({
|
||||||
faces: this.data.rules.dualityRoll?.defaultFearDice ?? 12
|
faces: this.terms[2]?.faces ?? this.data.rules.dualityRoll?.defaultFearDice ?? 12
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -388,4 +384,40 @@ export default class DualityRoll extends D20Roll {
|
||||||
if (currentCombatant?.actorId == config.data.id) ui.combat.setCombatantSpotlight(currentCombatant.id);
|
if (currentCombatant?.actorId == config.data.id) ui.combat.setCombatantSpotlight(currentCombatant.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async reroll(options) {
|
||||||
|
const oldDuality = this.withHope ? 1 : this.withFear ? -1 : 0;
|
||||||
|
const rerolled = DualityRoll.fromData((await super.reroll(options)).toJSON());
|
||||||
|
|
||||||
|
if (options?.liveRoll) {
|
||||||
|
if (game.modules.get('dice-so-nice')?.active) {
|
||||||
|
const diceAppearance = await getDiceSoNicePresets(
|
||||||
|
rerolled,
|
||||||
|
rerolled.dHope.denomination,
|
||||||
|
rerolled.dFear.denomination
|
||||||
|
);
|
||||||
|
rerolled.dHope.options.appearance = diceAppearance.hope.appearance;
|
||||||
|
rerolled.dFear.options.appearance = diceAppearance.fear.appearance;
|
||||||
|
if (rerolled.dAdvantage) rerolled.dAdvantage.options.appearance = diceAppearance.advantage.appearance;
|
||||||
|
if (rerolled.dDisadvantage)
|
||||||
|
rerolled.dDisadvantage.options.appearance = diceAppearance.disadvantage.appearance;
|
||||||
|
|
||||||
|
await game.dice3d.showForRoll(rerolled, game.user, true);
|
||||||
|
} else {
|
||||||
|
foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.actionType === 'reaction') return;
|
||||||
|
|
||||||
|
const newDuality = rerolled.withHope ? 1 : rerolled.withFear ? -1 : 0;
|
||||||
|
const actor = await foundry.utils.fromUuid(this.options.source.actor);
|
||||||
|
updateResourcesForDualityReroll(oldDuality, newDuality, actor);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rerolled;
|
||||||
|
}
|
||||||
|
|
||||||
|
fromJSON(json) {
|
||||||
|
return super.fromJSON(json);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
17
module/dice/helpers.mjs
Normal file
17
module/dice/helpers.mjs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
export function updateResourcesForDualityReroll(oldDuality, newDuality, actor) {
|
||||||
|
const { hopeFear } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
|
||||||
|
if (game.user.isGM ? !hopeFear.gm : !hopeFear.players) return;
|
||||||
|
|
||||||
|
const updates = [];
|
||||||
|
const hope = (newDuality >= 0 ? 1 : 0) - (oldDuality >= 0 ? 1 : 0);
|
||||||
|
const stress = (newDuality === 0 ? 1 : 0) - (oldDuality === 0 ? 1 : 0);
|
||||||
|
const fear = (newDuality === -1 ? 1 : 0) - (oldDuality === -1 ? 1 : 0);
|
||||||
|
|
||||||
|
if (hope !== 0) updates.push({ key: 'hope', value: hope, total: -1 * hope, enabled: true });
|
||||||
|
if (stress !== 0) updates.push({ key: 'stress', value: -1 * stress, total: stress, enabled: true });
|
||||||
|
if (fear !== 0) updates.push({ key: 'fear', value: fear, total: -1 * fear, enabled: true });
|
||||||
|
|
||||||
|
const resourceUpdates = new ResourceUpdateMap(actor);
|
||||||
|
resourceUpdates.addResources(updates);
|
||||||
|
resourceUpdates.updateResources();
|
||||||
|
}
|
||||||
|
|
@ -109,6 +109,14 @@ export default class DhpActor extends Actor {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.type === 'npc') {
|
||||||
|
Object.assign(update, {
|
||||||
|
prototypeToken: {
|
||||||
|
disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.updateSource(update);
|
this.updateSource(update);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -864,3 +864,57 @@ export function camelize(str) {
|
||||||
})
|
})
|
||||||
.replace(/\s+/g, '');
|
.replace(/\s+/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Bulk load a list of documents using uuids. Returns the documents in the same order */
|
||||||
|
export async function fromUuids(uuids) {
|
||||||
|
// Set up base entries. Each step works on a sublist of these objects
|
||||||
|
const entries = uuids.map(uuid => ({
|
||||||
|
uuid,
|
||||||
|
parsed: foundry.utils.parseUuid(uuid),
|
||||||
|
value: foundry.utils.fromUuidSync(uuid)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Handle missing uuids for embedded documents first
|
||||||
|
// A value may be index data, so we check if its a document
|
||||||
|
const packEmbeddedEntries = entries.filter(
|
||||||
|
e =>
|
||||||
|
!(e.value instanceof Document) &&
|
||||||
|
e.parsed.collection instanceof foundry.documents.collections.CompendiumCollection &&
|
||||||
|
e.parsed.embedded.length > 0
|
||||||
|
);
|
||||||
|
await Promise.all(
|
||||||
|
packEmbeddedEntries.map(async e => {
|
||||||
|
e.value = await fromUuid(e.uuid);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle missing top level pack stuff, by batching per pack
|
||||||
|
const missingTopLevel = entries.filter(e => !(e.value instanceof Document) && e.value?.pack);
|
||||||
|
for (const packGroup of Object.values(Object.groupBy(missingTopLevel, e => e.value.pack))) {
|
||||||
|
const pack = game.packs.get(packGroup[0].value.pack);
|
||||||
|
if (!pack) continue;
|
||||||
|
|
||||||
|
const ids = packGroup.map(p => p.parsed.id);
|
||||||
|
const documents = await pack.getDocuments({ _id__in: ids });
|
||||||
|
for (const p of packGroup) {
|
||||||
|
p.value = documents.find(d => d.id === p.parsed.id) ?? p.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries.map(e => e.value);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Triggers DiceSoNice rolls or dice roll audio for rolls. Not used for duality rolls.
|
||||||
|
* @param { Roll[] } rolls
|
||||||
|
* @return { void }
|
||||||
|
*/
|
||||||
|
export async function triggerChatRollFx(rolls, options = { whisper: false, blind: false }) {
|
||||||
|
const { whisper, blind } = options;
|
||||||
|
if (game.modules.get('dice-so-nice')?.active) {
|
||||||
|
const rerollPromises = rolls.map(roll => game.dice3d.showForRoll(roll, game.user, true, whisper, blind));
|
||||||
|
await Promise.allSettled(rerollPromises);
|
||||||
|
} else {
|
||||||
|
foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,6 @@
|
||||||
|
|
||||||
@import './multiclass-choice/sheet.less';
|
@import './multiclass-choice/sheet.less';
|
||||||
|
|
||||||
@import './reroll-dialog/sheet.less';
|
|
||||||
|
|
||||||
@import './tag-team-dialog/initialization.less';
|
@import './tag-team-dialog/initialization.less';
|
||||||
@import './tag-team-dialog/sheet.less';
|
@import './tag-team-dialog/sheet.less';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,7 @@
|
||||||
.tab.active {
|
.tab.active {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
scrollbar-width: thin;
|
.with-scroll-shadows();
|
||||||
scrollbar-color: light-dark(@dark-blue, @golden) transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div[data-application-part='form'] {
|
div[data-application-part='form'] {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
max-height: 700px;
|
max-height: 700px;
|
||||||
mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%);
|
.with-scroll-shadows();
|
||||||
|
|
||||||
.level-achievements-container,
|
.level-achievements-container,
|
||||||
.level-advancements-container {
|
.level-advancements-container {
|
||||||
|
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
.daggerheart.dialog.dh-style.views.reroll-dialog {
|
|
||||||
.window-content {
|
|
||||||
max-width: 648px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reroll-outer-container {
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dices-container {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dice-outer-container {
|
|
||||||
width: 300px;
|
|
||||||
|
|
||||||
legend {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
|
|
||||||
i {
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dice-container {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
|
|
||||||
|
|
||||||
.result-container {
|
|
||||||
position: relative;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 1.375rem;
|
|
||||||
opacity: 0.8;
|
|
||||||
|
|
||||||
&.selected {
|
|
||||||
opacity: 1;
|
|
||||||
border: 1px solid;
|
|
||||||
border-radius: 6px;
|
|
||||||
border-color: light-dark(@dark-blue, @golden);
|
|
||||||
filter: drop-shadow(0 0 3px @golden);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
content: ' ';
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: -1;
|
|
||||||
mask: var(--svg-die) no-repeat center;
|
|
||||||
mask-size: contain;
|
|
||||||
background: linear-gradient(139.01deg, #efe6d8 3.51%, #372e1f 96.49%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.d4:before {
|
|
||||||
--svg-die: url(../assets/icons/dice/default/d4.svg);
|
|
||||||
}
|
|
||||||
&.d6:before {
|
|
||||||
--svg-die: url(../assets/icons/dice/default/d6.svg);
|
|
||||||
}
|
|
||||||
&.d8:before {
|
|
||||||
--svg-die: url(../assets/icons/dice/default/d8.svg);
|
|
||||||
}
|
|
||||||
&.d10:before {
|
|
||||||
--svg-die: url(../assets/icons/dice/default/d10.svg);
|
|
||||||
}
|
|
||||||
&.d12:before {
|
|
||||||
--svg-die: url('../assets/icons/dice/default/d12.svg');
|
|
||||||
}
|
|
||||||
&.d20:before {
|
|
||||||
--svg-die: url(../assets/icons/dice/default/d20.svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.to-reroll-result {
|
|
||||||
position: absolute;
|
|
||||||
bottom: -7px;
|
|
||||||
gap: 2px;
|
|
||||||
border: 1px solid;
|
|
||||||
border-radius: 6px;
|
|
||||||
background-image: url(../assets/parchments/dh-parchment-dark.png);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 2px 6px;
|
|
||||||
|
|
||||||
input {
|
|
||||||
margin: 0;
|
|
||||||
height: 12px;
|
|
||||||
line-height: 0px;
|
|
||||||
position: relative;
|
|
||||||
top: 1px;
|
|
||||||
|
|
||||||
&:before,
|
|
||||||
&:after {
|
|
||||||
line-height: 12px;
|
|
||||||
font-size: var(--font-size-12);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
font-size: var(--font-size-10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
margin-top: 8px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -194,6 +194,7 @@
|
||||||
|
|
||||||
.roll-selection-container {
|
.roll-selection-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
.select-roll-button {
|
.select-roll-button {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
|
|
|
||||||
|
|
@ -283,7 +283,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.fit-height {
|
&.fit-height {
|
||||||
height: 95%;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.flex {
|
&.flex {
|
||||||
|
|
|
||||||
|
|
@ -41,5 +41,10 @@
|
||||||
list-style: disc;
|
list-style: disc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Fixes centering and makes it not render over scrollbar
|
||||||
|
&:hover button.toggle:enabled {
|
||||||
|
display: flex;
|
||||||
|
right: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@
|
||||||
|
|
||||||
.daggerheart.dh-style {
|
.daggerheart.dh-style {
|
||||||
.tab-navigation {
|
.tab-navigation {
|
||||||
margin: 5px 0;
|
margin: 5px 0 10px 0;
|
||||||
height: 40px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.navigation-container {
|
.navigation-container {
|
||||||
|
|
@ -21,6 +20,10 @@
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: @color-text-emphatic;
|
color: @color-text-emphatic;
|
||||||
|
|
||||||
|
&.empty:not(.active) {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
&.attack.active {
|
&.attack.active {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fieldsets-section {
|
.fieldsets-section {
|
||||||
|
|
|
||||||
|
|
@ -38,11 +38,23 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab.inventory {
|
.tab.inventory {
|
||||||
|
.gold-section {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 10px 0;
|
||||||
|
|
||||||
|
.input {
|
||||||
|
color: light-dark(@dark, @beige);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.search-section {
|
.search-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
justify-content: space-between;
|
||||||
.search-bar {
|
.search-bar {
|
||||||
position: relative;
|
position: relative;
|
||||||
color: light-dark(@dark-blue-50, @beige-50);
|
color: light-dark(@dark-blue-50, @beige-50);
|
||||||
|
|
@ -72,22 +84,11 @@
|
||||||
height: 32px;
|
height: 32px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
font-size: 16px;
|
font-size: var(--font-size-16);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
color: light-dark(@dark-blue-50, @beige-50);
|
color: light-dark(@dark-blue-50, @beige-50);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.gold-section {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 10px 10px 0;
|
|
||||||
|
|
||||||
.input {
|
|
||||||
color: light-dark(@dark, @beige);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.limited {
|
&.limited {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
@import '../../../utils/colors.less';
|
@import '../../../utils/colors.less';
|
||||||
|
@import '../../../utils/mixin.less';
|
||||||
|
|
||||||
.application.sheet.daggerheart.actor.dh-style.adversary {
|
.application.sheet.daggerheart.actor.dh-style.adversary {
|
||||||
.tab.effects {
|
.tab.effects {
|
||||||
|
|
@ -7,8 +8,8 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
mask-image: linear-gradient(0deg, transparent 0%, black 5%);
|
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
|
.with-scroll-shadows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
@import '../../../utils/colors.less';
|
@import '../../../utils/colors.less';
|
||||||
@import '../../../utils/fonts.less';
|
@import '../../../utils/fonts.less';
|
||||||
|
@import '../../../utils/mixin.less';
|
||||||
|
|
||||||
.application.sheet.daggerheart.actor.dh-style.adversary {
|
.application.sheet.daggerheart.actor.dh-style.adversary {
|
||||||
.tab.features {
|
.tab.features {
|
||||||
|
|
@ -8,8 +9,8 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
mask-image: linear-gradient(0deg, transparent 0%, black 5%);
|
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
|
.with-scroll-shadows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
.tags {
|
.tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding-bottom: 16px;
|
padding-bottom: 8px;
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -67,11 +67,5 @@
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 16px 0;
|
padding: 16px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.adversary-navigation {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
7
styles/less/sheets/actors/adversary/index.less
Normal file
7
styles/less/sheets/actors/adversary/index.less
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
@import './features.less';
|
||||||
|
@import './header.less';
|
||||||
|
@import './sheet.less';
|
||||||
|
@import './sidebar.less';
|
||||||
|
@import './effects.less';
|
||||||
|
@import './notes.less';
|
||||||
|
|
||||||
3
styles/less/sheets/actors/adversary/notes.less
Normal file
3
styles/less/sheets/actors/adversary/notes.less
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.application.sheet.daggerheart.actor.dh-style.adversary .tab.notes.active {
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
@ -286,9 +286,8 @@
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%);
|
|
||||||
|
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
|
.with-scroll-shadows();
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
@import '../../../utils/colors.less';
|
@import '../../../utils/colors.less';
|
||||||
@import '../../../utils/fonts.less';
|
@import '../../../utils/fonts.less';
|
||||||
|
@import '../../../utils/mixin.less';
|
||||||
|
|
||||||
.application.sheet.daggerheart.actor.dh-style.character {
|
.application.sheet.daggerheart.actor.dh-style.character {
|
||||||
.tab.biography {
|
.tab.biography {
|
||||||
|
|
@ -9,10 +10,10 @@
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 98%, transparent 100%);
|
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
.with-scroll-shadows();
|
||||||
}
|
}
|
||||||
|
|
||||||
.characteristics-section {
|
.characteristics-section {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
@import '../../../utils/colors.less';
|
@import '../../../utils/colors.less';
|
||||||
@import '../../../utils/fonts.less';
|
@import '../../../utils/fonts.less';
|
||||||
|
@import '../../../utils/mixin.less';
|
||||||
|
|
||||||
.application.sheet.daggerheart.actor.dh-style.character {
|
.application.sheet.daggerheart.actor.dh-style.character {
|
||||||
.tab.effects {
|
.tab.effects {
|
||||||
|
|
@ -8,8 +9,8 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
mask-image: linear-gradient(0deg, transparent 0%, black 5%);
|
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
|
.with-scroll-shadows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
@import '../../../utils/colors.less';
|
@import '../../../utils/colors.less';
|
||||||
@import '../../../utils/fonts.less';
|
@import '../../../utils/fonts.less';
|
||||||
|
@import '../../../utils/mixin.less';
|
||||||
|
|
||||||
.application.sheet.daggerheart.actor.dh-style.character {
|
.application.sheet.daggerheart.actor.dh-style.character {
|
||||||
.tab.features {
|
.tab.features {
|
||||||
|
|
@ -8,8 +9,8 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
mask-image: linear-gradient(0deg, transparent 0%, black 5%);
|
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
|
.with-scroll-shadows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
8
styles/less/sheets/actors/character/index.less
Normal file
8
styles/less/sheets/actors/character/index.less
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
@import './biography.less';
|
||||||
|
@import './effects.less';
|
||||||
|
@import './features.less';
|
||||||
|
@import './header.less';
|
||||||
|
@import './inventory.less';
|
||||||
|
@import './loadout.less';
|
||||||
|
@import './sheet.less';
|
||||||
|
@import './sidebar.less';
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
@import '../../../utils/colors.less';
|
@import '../../../utils/colors.less';
|
||||||
@import '../../../utils/fonts.less';
|
@import '../../../utils/fonts.less';
|
||||||
|
@import '../../../utils/mixin.less';
|
||||||
|
|
||||||
.application.sheet.daggerheart.actor.dh-style.character {
|
.application.sheet.daggerheart.actor.dh-style.character {
|
||||||
.tab.inventory {
|
.tab.inventory {
|
||||||
|
|
@ -8,8 +9,9 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%);
|
margin-top: 20px;
|
||||||
padding: 20px 0;
|
padding-bottom: 20px;
|
||||||
|
.with-scroll-shadows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,10 @@
|
||||||
@import '../../../utils/colors.less';
|
@import '../../../utils/colors.less';
|
||||||
@import '../../../utils/fonts.less';
|
@import '../../../utils/fonts.less';
|
||||||
|
@import '../../../utils/mixin.less';
|
||||||
|
|
||||||
.application.sheet.daggerheart.actor.dh-style.character {
|
.application.sheet.daggerheart.actor.dh-style.character {
|
||||||
.tab.loadout {
|
.tab.loadout {
|
||||||
.search-section {
|
.search-section {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.search-bar {
|
|
||||||
position: relative;
|
|
||||||
color: light-dark(@dark-blue-50, @beige-50);
|
|
||||||
width: 80%;
|
|
||||||
padding-top: 5px;
|
|
||||||
|
|
||||||
input {
|
|
||||||
border-radius: 50px;
|
|
||||||
background: light-dark(@dark-blue-10, @golden-10);
|
|
||||||
border: none;
|
|
||||||
outline: 2px solid transparent;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
padding: 0 20px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
outline: 2px solid light-dark(@dark, @golden);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-search-cancel-button {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
align-content: center;
|
|
||||||
height: 32px;
|
|
||||||
position: absolute;
|
|
||||||
right: 20px;
|
|
||||||
font-size: var(--font-size-16);
|
|
||||||
z-index: 1;
|
|
||||||
color: light-dark(@dark-blue-50, @beige-50);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-toggle-view {
|
.btn-toggle-view {
|
||||||
background: light-dark(@dark-blue-10, @dark-blue);
|
background: light-dark(@dark-blue-10, @dark-blue);
|
||||||
border: 1px solid @color-border;
|
border: 1px solid @color-border;
|
||||||
|
|
@ -90,8 +52,9 @@
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
mask-image: linear-gradient(0deg, transparent 0%, black 10%, black 98%, transparent 100%);
|
margin-top: 20px;
|
||||||
padding: 20px 0;
|
padding-bottom: 20px;
|
||||||
|
.with-scroll-shadows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -549,8 +549,8 @@
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
mask-image: linear-gradient(0deg, transparent 0%, black 5%);
|
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
|
.with-scroll-shadows();
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
mask-image: linear-gradient(0deg, transparent 0%, black 5%);
|
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
|
.with-scroll-shadows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -148,10 +148,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.companion-navigation {
|
.companion-navigation {
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: baseline;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
styles/less/sheets/actors/companion/index.less
Normal file
4
styles/less/sheets/actors/companion/index.less
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
@import './details.less';
|
||||||
|
@import './header.less';
|
||||||
|
@import './sheet.less';
|
||||||
|
@import './effects.less';
|
||||||
|
|
@ -10,3 +10,16 @@
|
||||||
background: url('../assets/parchments/dh-parchment-light.png');
|
background: url('../assets/parchments/dh-parchment-light.png');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
.application.sheet.daggerheart.actor.dh-style.companion {
|
||||||
|
.window-content {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
@import '../../../utils/colors.less';
|
@import '../../../utils/colors.less';
|
||||||
@import '../../../utils/fonts.less';
|
@import '../../../utils/fonts.less';
|
||||||
|
@import '../../../utils/mixin.less';
|
||||||
|
|
||||||
.application.sheet.daggerheart.actor.dh-style.environment {
|
.application.sheet.daggerheart.actor.dh-style.environment {
|
||||||
.tab.features {
|
.tab.features {
|
||||||
|
|
@ -8,8 +9,8 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
mask-image: linear-gradient(0deg, transparent 0%, black 5%);
|
padding-bottom: 4px;
|
||||||
padding-bottom: 20px;
|
.with-scroll-shadows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -138,10 +138,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.environment-navigation {
|
.environment-navigation {
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
align-items: baseline;
|
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
|
|
||||||
.tab-navigation {
|
.tab-navigation {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
styles/less/sheets/actors/environment/index.less
Normal file
4
styles/less/sheets/actors/environment/index.less
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
@import './features.less';
|
||||||
|
@import './header.less';
|
||||||
|
@import './potentialAdversaries.less';
|
||||||
|
@import './sheet.less';
|
||||||
|
|
@ -7,8 +7,8 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
mask-image: linear-gradient(0deg, transparent 0%, black 5%);
|
padding-bottom: 4px;
|
||||||
padding-bottom: 20px;
|
.with-scroll-shadows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
styles/less/sheets/actors/npc/features.less
Normal file
18
styles/less/sheets/actors/npc/features.less
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
.application.sheet.daggerheart.actor.dh-style.npc {
|
||||||
|
.tab.features {
|
||||||
|
&.active {
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
.with-scroll-shadows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
83
styles/less/sheets/actors/npc/header.less
Normal file
83
styles/less/sheets/actors/npc/header.less
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
.application.sheet.daggerheart.actor.dh-style.npc {
|
||||||
|
.npc-header-sheet {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.portrait {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 275px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 275px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 4px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 3px 5px;
|
||||||
|
font-size: var(--font-size-12);
|
||||||
|
font: @font-body;
|
||||||
|
|
||||||
|
background: light-dark(@dark-15, @beige-15);
|
||||||
|
border: 1px solid light-dark(@dark, @beige);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: var(--font-size-12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 15px;
|
||||||
|
padding-top: var(--header-height);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.name-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 0;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
padding: 6px 0 0 0;
|
||||||
|
font-size: var(--font-size-32);
|
||||||
|
text-align: start;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
outline: 2px solid transparent;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
word-break: break-word;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
outline: 2px solid light-dark(@dark, @golden);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.npc-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
styles/less/sheets/actors/npc/index.less
Normal file
3
styles/less/sheets/actors/npc/index.less
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
@import './sheet.less';
|
||||||
|
@import './header.less';
|
||||||
|
@import './features.less';
|
||||||
10
styles/less/sheets/actors/npc/sheet.less
Normal file
10
styles/less/sheets/actors/npc/sheet.less
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
.application.sheet.daggerheart.actor.dh-style.npc {
|
||||||
|
.window-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.npc-navigation {
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
4
styles/less/sheets/actors/party/index.less
Normal file
4
styles/less/sheets/actors/party/index.less
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
@import './header.less';
|
||||||
|
@import './party-members.less';
|
||||||
|
@import './sheet.less';
|
||||||
|
@import './inventory.less';
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
@import '../../../utils/colors.less';
|
@import '../../../utils/colors.less';
|
||||||
@import '../../../utils/fonts.less';
|
@import '../../../utils/fonts.less';
|
||||||
|
@import '../../../utils/mixin.less';
|
||||||
|
|
||||||
.application.sheet.daggerheart.actor.dh-style.party {
|
.application.sheet.daggerheart.actor.dh-style.party {
|
||||||
.tab.inventory {
|
.tab.inventory {
|
||||||
|
|
@ -8,8 +9,9 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%);
|
margin-top: 20px;
|
||||||
padding: 20px 0;
|
padding-bottom: 4px;
|
||||||
|
.with-scroll-shadows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,35 +2,12 @@
|
||||||
|
|
||||||
@import './actors/actor-sheet-shared.less';
|
@import './actors/actor-sheet-shared.less';
|
||||||
|
|
||||||
@import './actors/adversary/actions.less';
|
@import './actors/adversary/index.less';
|
||||||
@import './actors/adversary/header.less';
|
@import './actors/character/index.less';
|
||||||
@import './actors/adversary/sheet.less';
|
@import './actors/companion/index.less';
|
||||||
@import './actors/adversary/sidebar.less';
|
@import './actors/environment/index.less';
|
||||||
@import './actors/adversary/effects.less';
|
@import './actors/npc/index.less';
|
||||||
|
@import './actors/party/index.less';
|
||||||
@import './actors/character/biography.less';
|
|
||||||
@import './actors/character/effects.less';
|
|
||||||
@import './actors/character/features.less';
|
|
||||||
@import './actors/character/header.less';
|
|
||||||
@import './actors/character/inventory.less';
|
|
||||||
@import './actors/character/loadout.less';
|
|
||||||
@import './actors/character/sheet.less';
|
|
||||||
@import './actors/character/sidebar.less';
|
|
||||||
|
|
||||||
@import './actors/companion/details.less';
|
|
||||||
@import './actors/companion/header.less';
|
|
||||||
@import './actors/companion/sheet.less';
|
|
||||||
@import './actors/companion/effects.less';
|
|
||||||
|
|
||||||
@import './actors/environment/actions.less';
|
|
||||||
@import './actors/environment/header.less';
|
|
||||||
@import './actors/environment/potentialAdversaries.less';
|
|
||||||
@import './actors/environment/sheet.less';
|
|
||||||
|
|
||||||
@import './actors/party/header.less';
|
|
||||||
@import './actors/party/party-members.less';
|
|
||||||
@import './actors/party/sheet.less';
|
|
||||||
@import './actors/party/inventory.less';
|
|
||||||
|
|
||||||
@import './items/beastform.less';
|
@import './items/beastform.less';
|
||||||
@import './items/class.less';
|
@import './items/class.less';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
@import '../../utils/colors.less';
|
@import '../../utils/colors.less';
|
||||||
@import '../../utils/fonts.less';
|
@import '../../utils/fonts.less';
|
||||||
|
@import '../../utils/mixin.less';
|
||||||
|
|
||||||
.application.daggerheart.dh-style.compendium-browser {
|
.application.daggerheart.dh-style.compendium-browser {
|
||||||
border: initial;
|
border: initial;
|
||||||
|
|
@ -242,6 +243,7 @@
|
||||||
.compendium-sidebar > .folder-list {
|
.compendium-sidebar > .folder-list {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
|
.with-scroll-shadows();
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-list-header,
|
.item-list-header,
|
||||||
|
|
|
||||||
|
|
@ -160,3 +160,47 @@
|
||||||
@destination @length
|
@destination @length
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scroll shadows, but only if the browser supports. At the time of writing, this doesn't work on firefox
|
||||||
|
@supports ((animation-timeline: scroll()) and (animation-range: 0% 100%)) {
|
||||||
|
@property --fade-start {
|
||||||
|
syntax: "<length>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@property --fade-end {
|
||||||
|
syntax: "<length>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scrollfade {
|
||||||
|
0% {
|
||||||
|
--fade-start: 0;
|
||||||
|
}
|
||||||
|
10%, 100% {
|
||||||
|
--fade-start: 12px;
|
||||||
|
}
|
||||||
|
0%, 90% {
|
||||||
|
--fade-end: 12px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
--fade-end: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.with-scroll-shadows() {
|
||||||
|
animation: scrollfade;
|
||||||
|
animation-timeline: --scrollfade;
|
||||||
|
animation-range: entry 0% exit 100%;
|
||||||
|
scroll-timeline: --scrollfade y;
|
||||||
|
mask-image: linear-gradient(
|
||||||
|
0deg,
|
||||||
|
transparent 0%,
|
||||||
|
black var(--fade-end),
|
||||||
|
black calc(100% - var(--fade-start)),
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "daggerheart",
|
"id": "daggerheart",
|
||||||
"title": "Daggerheart",
|
"title": "Daggerheart",
|
||||||
"description": "An unofficial implementation of the Daggerheart system",
|
"description": "An unofficial implementation of the Daggerheart system",
|
||||||
"version": "2.2.7",
|
"version": "2.3.0",
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"minimum": "14.361",
|
"minimum": "14.361",
|
||||||
"verified": "14.363",
|
"verified": "14.363",
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
},
|
},
|
||||||
"url": "https://github.com/Foundryborne/daggerheart",
|
"url": "https://github.com/Foundryborne/daggerheart",
|
||||||
"manifest": "https://raw.githubusercontent.com/Foundryborne/daggerheart/v14/system.json",
|
"manifest": "https://raw.githubusercontent.com/Foundryborne/daggerheart/v14/system.json",
|
||||||
"download": "https://github.com/Foundryborne/daggerheart/releases/download/2.2.7/system.zip",
|
"download": "https://github.com/Foundryborne/daggerheart/releases/download/2.3.0/system.zip",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"name": "WBHarry"
|
"name": "WBHarry"
|
||||||
|
|
@ -244,11 +244,14 @@
|
||||||
"adversary": {
|
"adversary": {
|
||||||
"htmlFields": ["notes", "description"]
|
"htmlFields": ["notes", "description"]
|
||||||
},
|
},
|
||||||
|
"npc": {
|
||||||
|
"htmlFields": ["notes"]
|
||||||
|
},
|
||||||
"environment": {
|
"environment": {
|
||||||
"htmlFields": ["notes", "description"]
|
"htmlFields": ["notes", "description"]
|
||||||
},
|
},
|
||||||
"party": {
|
"party": {
|
||||||
"htmlFields": ["notes"]
|
"htmlFields": ["notes", "description"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Item": {
|
"Item": {
|
||||||
|
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
<div class="reroll-outer-container">
|
|
||||||
{{#each damage}}
|
|
||||||
<h2>{{localize (concat 'DAGGERHEART.CONFIG.HealingType.' @key '.name')}}</h2>
|
|
||||||
{{#each this}}
|
|
||||||
<div class="dices-container">
|
|
||||||
{{#each this}}
|
|
||||||
<fieldset class="dice-outer-container">
|
|
||||||
<legend>
|
|
||||||
<input class="to-reroll-input" type="checkbox" data-type="{{@../../key}}" data-part="{{@../key}}" data-dice="{{@key}}" {{checked this.toReroll}} />
|
|
||||||
<i class="fa-solid fa-dice"></i>
|
|
||||||
<span>{{this.selectedResults}}/{{this.maxSelected}} Selected</span>
|
|
||||||
</legend>
|
|
||||||
|
|
||||||
<div class="dice-container">
|
|
||||||
{{#each this.results}}
|
|
||||||
<div
|
|
||||||
class="result-container {{../dice}} {{#if this.active}}selected{{/if}}"
|
|
||||||
data-action="selectRoll" data-type="{{@../../../key}}" data-part="{{@../../key}}" data-dice="{{@../key}}" data-result="{{@key}}"
|
|
||||||
>
|
|
||||||
{{this.result}}
|
|
||||||
{{#if this.active}}
|
|
||||||
<a class="to-reroll-result" data-action="toggleResult" data-type="{{@../../../key}}" data-part="{{@../../key}}" data-dice="{{@../key}}" data-result="{{@key}}">
|
|
||||||
<input class="to-reroll-result-input" type="checkbox" {{checked this.toReroll}} />
|
|
||||||
<i class="fa-solid fa-dice"></i>
|
|
||||||
</a>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
<footer>
|
|
||||||
<button type="button" data-action="doReroll" {{disabled disabledReroll}}>{{localize "DAGGERHEART.GENERAL.reroll"}} <i class="fa-solid fa-dice"></i></button>
|
|
||||||
<button type="button" data-action="save" {{disabled saveDisabled}}>{{localize "DAGGERHEART.APPLICATIONS.RerollDialog.acceptCurrentRolls"}}</button>
|
|
||||||
</footer>
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
<div class="reroll-outer-container">
|
|
||||||
{{#each damage}}
|
|
||||||
<h2>{{localize (concat 'DAGGERHEART.CONFIG.HealingType.' @key '.name')}}</h2>
|
|
||||||
{{#each this}}
|
|
||||||
<div class="dices-container">
|
|
||||||
{{#each this}}
|
|
||||||
<fieldset class="dice-outer-container">
|
|
||||||
<legend>
|
|
||||||
<input class="to-reroll-input" type="checkbox" data-type="{{@../../key}}" data-part="{{@../key}}" data-dice="{{@key}}" {{checked this.toReroll}} />
|
|
||||||
<i class="fa-solid fa-dice"></i>
|
|
||||||
<span>{{this.selectedResults}}/{{this.results.length}} Selected</span>
|
|
||||||
</legend>
|
|
||||||
|
|
||||||
<div class="dice-container">
|
|
||||||
{{#each this.results}}
|
|
||||||
<div
|
|
||||||
class="result-container {{../dice}} {{#if this.active}}selected{{/if}}"
|
|
||||||
data-action="selectRoll" data-type="{{@../../../key}}" data-part="{{@../../key}}" data-dice="{{@../key}}" data-result="{{@key}}"
|
|
||||||
>
|
|
||||||
{{this.result}}
|
|
||||||
{{#if this.active}}
|
|
||||||
<a class="to-reroll-result" data-action="toggleResult" data-type="{{@../../../key}}" data-part="{{@../../key}}" data-dice="{{@../key}}" data-result="{{@key}}">
|
|
||||||
<input class="to-reroll-result-input" type="checkbox" {{checked this.toReroll}} />
|
|
||||||
<i class="fa-solid fa-dice"></i>
|
|
||||||
</a>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{#if part.modifierTotal}}
|
{{#if part.modifierTotal}}
|
||||||
<span class="roll-operator">{{#if (gte part.modifierTotal 0)}}+{{else}}-{{/if}}</span>
|
{{#if part.dice.length}}<span class="roll-operator">{{#if (gte part.modifierTotal 0)}}+{{else}}-{{/if}}</span>{{/if}}
|
||||||
<span class="roll-value">{{positive part.modifierTotal}}</span>
|
<span class="roll-value">{{positive part.modifierTotal}}</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,12 @@
|
||||||
{{formField systemFields.motivesAndTactics value=document._source.system.motivesAndTactics label=(localize "DAGGERHEART.ACTORS.Adversary.FIELDS.motivesAndTactics.label")}}
|
{{formField systemFields.motivesAndTactics value=document._source.system.motivesAndTactics label=(localize "DAGGERHEART.ACTORS.Adversary.FIELDS.motivesAndTactics.label")}}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="flex">
|
||||||
|
<legend>{{localize "DAGGERHEART.GENERAL.DamageThresholds.title"}}</legend>
|
||||||
|
{{formGroup systemFields.damageThresholds.fields.major value=document._source.system.damageThresholds.major label=(localize "DAGGERHEART.GENERAL.DamageThresholds.majorThreshold")}}
|
||||||
|
{{formGroup systemFields.damageThresholds.fields.severe value=document._source.system.damageThresholds.severe label=(localize "DAGGERHEART.GENERAL.DamageThresholds.severeThreshold")}}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<div class="fieldsets-section">
|
<div class="fieldsets-section">
|
||||||
<fieldset class="flex">
|
<fieldset class="flex">
|
||||||
<legend>{{localize "DAGGERHEART.GENERAL.Resource.plural"}}</legend>
|
<legend>{{localize "DAGGERHEART.GENERAL.Resource.plural"}}</legend>
|
||||||
|
|
@ -26,10 +32,4 @@
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<fieldset class="flex">
|
|
||||||
<legend>{{localize "DAGGERHEART.GENERAL.DamageThresholds.title"}}</legend>
|
|
||||||
{{formGroup systemFields.damageThresholds.fields.major value=document._source.system.damageThresholds.major label=(localize "DAGGERHEART.GENERAL.DamageThresholds.majorThreshold")}}
|
|
||||||
{{formGroup systemFields.damageThresholds.fields.severe value=document._source.system.damageThresholds.severe label=(localize "DAGGERHEART.GENERAL.DamageThresholds.severeThreshold")}}
|
|
||||||
</fieldset>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
13
templates/sheets-settings/npc-settings/details.hbs
Normal file
13
templates/sheets-settings/npc-settings/details.hbs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<section
|
||||||
|
class='tab {{tabs.details.cssClass}} {{tabs.details.id}}'
|
||||||
|
data-tab='{{tabs.details.id}}'
|
||||||
|
data-group='{{tabs.details.group}}'
|
||||||
|
>
|
||||||
|
<fieldset>
|
||||||
|
<legend>{{localize "DAGGERHEART.GENERAL.description"}}</legend>
|
||||||
|
{{formInput systemFields.description value=document._source.system.description}}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
{{formGroup systemFields.motives value=document._source.system.motives}}
|
||||||
|
{{formGroup systemFields.difficulty value=document._source.system.difficulty localize=true}}
|
||||||
|
</section>
|
||||||
29
templates/sheets-settings/npc-settings/features.hbs
Normal file
29
templates/sheets-settings/npc-settings/features.hbs
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<section
|
||||||
|
class='tab {{tabs.features.cssClass}} {{tabs.features.id}}'
|
||||||
|
data-tab='{{tabs.features.id}}'
|
||||||
|
data-group='{{tabs.features.group}}'
|
||||||
|
>
|
||||||
|
<button type="button" class="add-feature-btn" data-action="createDoc" data-document-class="Item" data-type="feature">
|
||||||
|
{{localize "DOCUMENT.New" type=(localize "TYPES.Item.feature")}}
|
||||||
|
</button>
|
||||||
|
<fieldset>
|
||||||
|
<legend>{{localize tabs.features.label}}</legend>
|
||||||
|
<ul class="feature-list">
|
||||||
|
{{#each @root.features as |feature|}}
|
||||||
|
<li class="feature-item" id="{{feature.id}}" draggable="true">
|
||||||
|
<img src="{{feature.img}}" alt="">
|
||||||
|
<div class="label">
|
||||||
|
<span>{{feature.name}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<a data-action="editDoc" data-item-uuid="{{feature.uuid}}" data-tooltip="{{localize 'CONTROLS.CommonEdit'}}"><i class="fa-solid fa-pen-to-square"></i></a>
|
||||||
|
<a data-action="deleteDoc" data-item-uuid="{{feature.uuid}}" data-tooltip="{{localize 'CONTROLS.CommonDelete'}}"><i class="fa-solid fa-trash"></i></a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
<div class="features-dragger">
|
||||||
|
<span>{{localize "DAGGERHEART.GENERAL.dropFeaturesHere"}}</span>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</section>
|
||||||
3
templates/sheets-settings/npc-settings/header.hbs
Normal file
3
templates/sheets-settings/npc-settings/header.hbs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<header class="dialog-header">
|
||||||
|
<h1>{{document.name}}</h1>
|
||||||
|
</header>
|
||||||
|
|
@ -44,10 +44,9 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="adversary-navigation">
|
{{#> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
|
||||||
{{> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
|
|
||||||
<button type="button" data-action="openSettings" data-tooltip-text="{{localize "DAGGERHEART.UI.Tooltip.openSheetSettings"}}">
|
<button type="button" data-action="openSettings" data-tooltip-text="{{localize "DAGGERHEART.UI.Tooltip.openSheetSettings"}}">
|
||||||
<i class="fa-solid fa-wrench"></i>
|
<i class="fa-solid fa-wrench"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
{{/ 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
|
||||||
</header>
|
</header>
|
||||||
|
|
@ -50,9 +50,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="companion-navigation">
|
<div class="companion-navigation">
|
||||||
{{> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
|
{{#> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
|
||||||
<button type="button" data-action="openSettings" data-tooltip-text="{{localize "DAGGERHEART.UI.Tooltip.openSheetSettings"}}">
|
<button type="button" data-action="openSettings" data-tooltip-text="{{localize "DAGGERHEART.UI.Tooltip.openSheetSettings"}}">
|
||||||
<i class="fa-solid fa-wrench"></i>
|
<i class="fa-solid fa-wrench"></i>
|
||||||
</button>
|
</button>
|
||||||
|
{{/ 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
@ -44,9 +44,10 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="environment-navigation">
|
<div class="environment-navigation">
|
||||||
{{> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
|
{{#> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
|
||||||
<button type="button" data-action="openSettings">
|
<button type="button" data-action="openSettings">
|
||||||
<i class="fa-solid fa-wrench"></i>
|
<i class="fa-solid fa-wrench"></i>
|
||||||
</button>
|
</button>
|
||||||
|
{{/ 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
14
templates/sheets/actors/npc/features.hbs
Normal file
14
templates/sheets/actors/npc/features.hbs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<section class='tab {{tabs.features.cssClass}} {{tabs.features.id}}' data-tab='{{tabs.features.id}}'
|
||||||
|
data-group='{{tabs.features.group}}'>
|
||||||
|
<div class="feature-section">
|
||||||
|
{{> 'daggerheart.inventory-items'
|
||||||
|
title=tabs.features.label
|
||||||
|
type='feature'
|
||||||
|
collection=@root.features
|
||||||
|
hideContextMenu=true
|
||||||
|
hideModifyControls=true
|
||||||
|
canCreate=@root.editable
|
||||||
|
showActions=@root.editable
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
40
templates/sheets/actors/npc/header.hbs
Normal file
40
templates/sheets/actors/npc/header.hbs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
<header class='npc-header-sheet'>
|
||||||
|
<div class="portrait">
|
||||||
|
<img src="{{source.img}}" alt="{{source.name}}" data-action='editImage' data-edit="img">
|
||||||
|
</div>
|
||||||
|
<div class="info-section">
|
||||||
|
<line-div></line-div>
|
||||||
|
|
||||||
|
<div class="name-row">
|
||||||
|
<h1
|
||||||
|
class="input actor-name"
|
||||||
|
contenteditable="plaintext-only"
|
||||||
|
data-property="name"
|
||||||
|
placeholder="{{localize "DAGGERHEART.GENERAL.actorName"}}"
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck="false"
|
||||||
|
>{{source.name}}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if source.system.difficulty}}
|
||||||
|
<div class="tags">
|
||||||
|
<div class="tag">
|
||||||
|
<span>{{localize "DAGGERHEART.GENERAL.difficulty"}}</span>
|
||||||
|
<span>{{source.system.difficulty}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<line-div></line-div>
|
||||||
|
|
||||||
|
<div class="npc-info">
|
||||||
|
<span class="description">
|
||||||
|
<i>{{{description}}}</i>
|
||||||
|
</span>
|
||||||
|
<div class="motives-and-tactics">
|
||||||
|
<b>{{localize 'DAGGERHEART.ACTORS.NPC.FIELDS.motives.label'}}: </b>
|
||||||
|
{{source.system.motives}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
7
templates/sheets/actors/npc/navigation.hbs
Normal file
7
templates/sheets/actors/npc/navigation.hbs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<div class="npc-navigation">
|
||||||
|
{{#> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
|
||||||
|
<button type="button" data-action="openSettings" data-tooltip-text="{{localize "DAGGERHEART.UI.Tooltip.openSheetSettings"}}">
|
||||||
|
<i class="fa-solid fa-wrench"></i>
|
||||||
|
</button>
|
||||||
|
{{/'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
|
||||||
|
</div>
|
||||||
11
templates/sheets/actors/npc/notes.hbs
Normal file
11
templates/sheets/actors/npc/notes.hbs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<section
|
||||||
|
class='tab {{tabs.notes.cssClass}} {{tabs.notes.id}}'
|
||||||
|
data-tab='{{tabs.notes.id}}'
|
||||||
|
data-group='{{tabs.notes.group}}'
|
||||||
|
>
|
||||||
|
{{formInput notes.field value=notes.value enriched=notes.enriched toggled=true}}
|
||||||
|
|
||||||
|
{{#if (and showAttribution document.system.attribution.artist)}}
|
||||||
|
<label class="artist-attribution">{{localize "DAGGERHEART.GENERAL.artistAttribution" artist=document.system.attribution.artist}}</label>
|
||||||
|
{{/if}}
|
||||||
|
</section>
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<line-div></line-div>
|
<line-div></line-div>
|
||||||
<nav class='feature-tab sheet-tabs tabs' data-group='primary'>
|
<nav class='feature-tab sheet-tabs tabs' data-group='primary'>
|
||||||
{{#each tabs as |tab|}}
|
{{#each tabs as |tab|}}
|
||||||
<a class='{{tab.id}} {{tab.cssClass}}' data-action='tab' data-group='{{tab.group}}' data-tab='{{tab.id}}'>
|
<a class='{{tab.id}} {{tab.cssClass}} {{#if tab.empty}}empty{{/if}}' data-action='tab' data-group='{{tab.group}}' data-tab='{{tab.id}}'>
|
||||||
<span>{{localize tab.label}}</span>
|
<span>{{localize tab.label}}</span>
|
||||||
</a>
|
</a>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,9 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{{#if roll.difficulty}}
|
{{#if roll.options.roll.difficulty}}
|
||||||
<span class="roll-difficulty{{#unless roll.success}} is-miss{{/unless}}">
|
<span class="roll-difficulty{{#unless roll.options.roll.success}} is-miss{{/unless}}">
|
||||||
{{!-- {{#if canViewSecret}} --}}
|
{{localize "DAGGERHEART.GENERAL.difficulty"}} {{roll.options.roll.difficulty}}
|
||||||
difficulty {{roll.difficulty}}
|
|
||||||
{{!-- {{else}}
|
|
||||||
{{localize (ifThen roll.success "DAGGERHEART.GENERAL.success" "DAGGERHEART.GENERAL.failure")}}
|
|
||||||
{{/if}} --}}
|
|
||||||
</span>
|
</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
<span>{{name}}</span>
|
<span>{{name}}</span>
|
||||||
{{#if (or (eq type "adversary") (eq type "environment"))}}
|
{{#if (or (eq type "adversary") (eq type "environment"))}}
|
||||||
<span class="entry-subtitle">{{localize "DAGGERHEART.UI.Sidebar.actorDirectory.tier" tier=system.tier type=(@root.getTypeLabel this)}}</span>
|
<span class="entry-subtitle">{{localize "DAGGERHEART.UI.Sidebar.actorDirectory.tier" tier=system.tier type=(@root.getTypeLabel this)}}</span>
|
||||||
|
{{else if (eq type "npc")}}
|
||||||
|
<span class="entry-subtitle">{{localize "TYPES.Actor.npc"}}</span>
|
||||||
{{else if (eq type "character")}}
|
{{else if (eq type "character")}}
|
||||||
<span class="entry-subtitle">{{localize "DAGGERHEART.UI.Sidebar.actorDirectory.character" level=system.levelData.level.current}}</span>
|
<span class="entry-subtitle">{{localize "DAGGERHEART.UI.Sidebar.actorDirectory.character" level=system.levelData.level.current}}</span>
|
||||||
{{else if (eq type "companion")}}
|
{{else if (eq type "companion")}}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue