mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-06-05 20:34:15 +02:00
Compare commits
135 commits
6147201117
...
d78927d0c2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d78927d0c2 | ||
|
|
3527fd7959 | ||
|
|
f0a7539018 | ||
|
|
5be79f4ab8 | ||
|
|
2fc5b01f09 | ||
|
|
52b81de11f | ||
|
|
c0c9095847 | ||
|
|
5ac4fc3b9c | ||
|
|
6747be49b2 | ||
|
|
77c5cfcbb7 | ||
|
|
5dbcd94480 | ||
|
|
d98a7c951e | ||
|
|
3c36c5747d | ||
|
|
bcf274f1d0 | ||
|
|
df4a2c5d57 | ||
|
|
646ebc8bdf | ||
|
|
6448666579 | ||
|
|
d0c29ede56 | ||
|
|
98ce49b928 | ||
|
|
318d00b47d | ||
|
|
c8d0df87c8 | ||
|
|
983f48b415 | ||
|
|
bfd483698b | ||
|
|
3eb33a71af | ||
|
|
3fbc1e97c6 | ||
|
|
729e8bca42 | ||
|
|
53f15a7fde | ||
|
|
c23ac61ee5 | ||
|
|
d3141059ac | ||
|
|
61db7ca371 | ||
|
|
2bc1c04c93 | ||
|
|
493998cc95 | ||
|
|
251d7e4e13 | ||
|
|
a209b035c8 | ||
|
|
9487b07e43 | ||
|
|
f1a530f57f | ||
|
|
ddf4747310 | ||
|
|
ac72012387 | ||
|
|
1ab8170d2f | ||
|
|
48f9ffc318 | ||
|
|
fa6f9d56b8 | ||
|
|
c2f8b34ef2 | ||
|
|
de0ab9d047 | ||
|
|
b9416ead5a | ||
|
|
ccc4186e42 | ||
|
|
e529dd0f88 | ||
|
|
0e8c3dc74a | ||
|
|
58824d5bbf | ||
|
|
e095587305 | ||
|
|
f7f1bdce2b | ||
|
|
e4a3f105dc | ||
|
|
2931377d53 | ||
|
|
53e8da77c6 | ||
|
|
bae9006f64 | ||
|
|
273f666784 | ||
|
|
d782b25254 | ||
|
|
f4c21a6a1b | ||
|
|
da06381748 | ||
|
|
ed53d9ed4c | ||
|
|
b631525b6e | ||
|
|
b145f515d0 | ||
|
|
b23095cb2f | ||
|
|
2f589c1b8e | ||
|
|
10a608a1a5 | ||
|
|
6a2d09caac | ||
|
|
4504379fcf | ||
|
|
d152bfc906 | ||
|
|
b91d943dd1 | ||
|
|
ac5f84fff7 | ||
|
|
d78c6b1183 | ||
|
|
98049bd76b | ||
|
|
ab412367f9 | ||
|
|
0492507bd1 | ||
|
|
47960fdd61 | ||
|
|
d372f3df9b | ||
|
|
e6c27926d0 | ||
|
|
88e64531b4 | ||
|
|
dd2aa10871 | ||
|
|
855f4549ec | ||
|
|
6b4de71a0a | ||
|
|
46e552eb3d | ||
|
|
bc3c09fa2e | ||
|
|
829a6161ff | ||
|
|
24993970da | ||
|
|
d86ab2053c | ||
|
|
24813e7e4f | ||
|
|
abd7824c96 | ||
|
|
80e314ca84 | ||
|
|
4064701c16 | ||
|
|
e8828b70db | ||
|
|
9ef4929693 | ||
|
|
40804f3339 | ||
|
|
b7bc452bf5 | ||
|
|
fb5e3672dc | ||
|
|
cca468e8af | ||
|
|
94852cec21 | ||
|
|
0128106de6 | ||
|
|
2ffe678503 | ||
|
|
e95ea3c281 | ||
|
|
c91d53b4d4 | ||
|
|
85ca7efc6d | ||
|
|
edbf5aa55f | ||
|
|
54d1b2bdc0 | ||
|
|
516928f92b | ||
|
|
94e93222a4 | ||
|
|
4685ec3c77 | ||
|
|
4558fbdcf6 | ||
|
|
c7159eff11 | ||
|
|
d0c2c783f1 | ||
|
|
905d1f7e88 | ||
|
|
b22ce9697d | ||
|
|
404640a0a3 | ||
|
|
20056cd950 | ||
|
|
118c52a996 | ||
|
|
ca32aa5d35 | ||
|
|
1cece731ee | ||
|
|
b186f22cc7 | ||
|
|
047e77154a | ||
|
|
53f77972e3 | ||
|
|
cc822856e5 | ||
|
|
6d09c5504d | ||
|
|
c82bcbeb01 | ||
|
|
d0afee59d8 | ||
|
|
4d17a7d9bf | ||
|
|
b8e00b2807 | ||
|
|
e5ae56f45c | ||
|
|
ccb0073cef | ||
|
|
4a60c56462 | ||
|
|
84afec31a7 | ||
|
|
da11510e02 | ||
|
|
f45b1210c7 | ||
|
|
41829bc9d5 | ||
|
|
d73760fc39 | ||
|
|
276aee4747 | ||
|
|
fae05c24a8 |
275 changed files with 5076 additions and 4132 deletions
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
* text=auto eol=lf
|
||||||
|
*.json text eol=lf
|
||||||
1
.github/workflows/deploy.yml
vendored
1
.github/workflows/deploy.yml
vendored
|
|
@ -37,6 +37,7 @@ jobs:
|
||||||
url: https://github.com/${{github.repository}}
|
url: https://github.com/${{github.repository}}
|
||||||
manifest: https://raw.githubusercontent.com/${{github.repository}}/v14/system.json
|
manifest: https://raw.githubusercontent.com/${{github.repository}}/v14/system.json
|
||||||
download: https://github.com/${{github.repository}}/releases/download/${{github.event.release.tag_name}}/system.zip
|
download: https://github.com/${{github.repository}}/releases/download/${{github.event.release.tag_name}}/system.zip
|
||||||
|
flags.hotReload: false
|
||||||
|
|
||||||
# Create a zip file with all files required by the module to add to the release
|
# Create a zip file with all files required by the module to add to the release
|
||||||
- run: zip -r ./system.zip system.json README.md LICENSE build/daggerheart.js build/tagify.css styles/daggerheart.css assets/ templates/ packs/ lang/
|
- run: zip -r ./system.zip system.json README.md LICENSE build/daggerheart.js build/tagify.css styles/daggerheart.css assets/ templates/ packs/ lang/
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -6,3 +6,4 @@ Build
|
||||||
build
|
build
|
||||||
foundry
|
foundry
|
||||||
styles/daggerheart.css
|
styles/daggerheart.css
|
||||||
|
styles/daggerheart.css.map
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,11 @@ You can find the documentation here: https://github.com/Foundryborne/daggerheart
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Looking to contribute to the project? Look no further, check out our [contributing guide](contributing.md), and keep the [Code of Conduct](coc.md) in mind when working on things.
|
Looking to contribute to the project? Look no further, check out our [contributing guide](CONTRIBUTING.md), and keep the [Code of Conduct](coc.md) in mind when working on things.
|
||||||
|
|
||||||
|
## AI Policy
|
||||||
|
|
||||||
|
The Foundryborne Daggerheart system does not make use of AI (generative or otherwise) for any area of its implementation. We expect all contributors to follow this same policy when contributing with a pull request; contributions made using AI will be rejected outright.
|
||||||
|
|
||||||
## Disclaimer:
|
## Disclaimer:
|
||||||
|
|
||||||
|
|
|
||||||
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,
|
||||||
|
|
@ -342,7 +347,8 @@ Hooks.on(CONFIG.DH.HOOKS.hooksConfig.tagTeamStart, async data => {
|
||||||
const party = game.actors.get(data.partyId);
|
const party = game.actors.get(data.partyId);
|
||||||
if (!party) return;
|
if (!party) return;
|
||||||
|
|
||||||
const dialog = new game.system.api.applications.dialogs.TagTeamDialog(party);
|
const TagTeamDialog = game.system.api.applications.dialogs.TagTeamDialog;
|
||||||
|
const dialog = foundry.applications.instances.get(`TagTeamDialog-${party.id}`) ?? new TagTeamDialog(party);
|
||||||
dialog.tabGroups.application = 'tagTeamRoll';
|
dialog.tabGroups.application = 'tagTeamRoll';
|
||||||
await dialog.render({ force: true });
|
await dialog.render({ force: true });
|
||||||
}
|
}
|
||||||
|
|
@ -353,7 +359,8 @@ Hooks.on(CONFIG.DH.HOOKS.hooksConfig.groupRollStart, async data => {
|
||||||
const party = game.actors.get(data.partyId);
|
const party = game.actors.get(data.partyId);
|
||||||
if (!party) return;
|
if (!party) return;
|
||||||
|
|
||||||
const dialog = new game.system.api.applications.dialogs.GroupRollDialog(party);
|
const GroupRollDialog = game.system.api.applications.dialogs.GroupRollDialog;
|
||||||
|
const dialog = foundry.applications.instances.get(`GroupRollDialog-${party.id}`) ?? new GroupRollDialog(party);
|
||||||
dialog.tabGroups.application = 'groupRoll';
|
dialog.tabGroups.application = 'groupRoll';
|
||||||
await dialog.render({ force: true });
|
await dialog.render({ force: true });
|
||||||
}
|
}
|
||||||
|
|
@ -439,3 +446,33 @@ Hooks.on('canvasTearDown', canvas => {
|
||||||
Hooks.on('canvasReady', canas => {
|
Hooks.on('canvasReady', canas => {
|
||||||
game.system.registeredTriggers.registerSceneTriggers(canvas.scene);
|
game.system.registeredTriggers.registerSceneTriggers(canvas.scene);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Make the user to select a document type, instead of having a default doc type for them to accidentally keep */
|
||||||
|
Hooks.on('renderDialogV2', (_dialog, html) => {
|
||||||
|
if (!html.classList.contains('dialog')) return;
|
||||||
|
const cls = html.classList.contains('item-create')
|
||||||
|
? documents.DHItem.implementation
|
||||||
|
: html.classList.contains('actor-create')
|
||||||
|
? documents.DhpActor.implementation
|
||||||
|
: null;
|
||||||
|
if (!cls) return;
|
||||||
|
|
||||||
|
const form = html.querySelector('form');
|
||||||
|
const submit = html.querySelector('button[type=submit]');
|
||||||
|
const select = html.querySelector('select[name=type]');
|
||||||
|
const nameInput = html.querySelector('input[name=name]');
|
||||||
|
if (!form || !select || !submit || !nameInput) return;
|
||||||
|
|
||||||
|
nameInput.placeholder = cls.defaultName({});
|
||||||
|
const emptyOption = document.createElement('option');
|
||||||
|
emptyOption.value = '';
|
||||||
|
emptyOption.selected = true;
|
||||||
|
select.required = true;
|
||||||
|
select.prepend(emptyOption);
|
||||||
|
submit.addEventListener('click', event => {
|
||||||
|
if (!form.reportValidity()) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,15 @@
|
||||||
// Less configuration
|
// Less configuration
|
||||||
var gulp = require('gulp');
|
var gulp = require('gulp');
|
||||||
var less = require('gulp-less');
|
var less = require('gulp-less');
|
||||||
|
var sourcemaps = require('gulp-sourcemaps');
|
||||||
|
|
||||||
gulp.task('less', function (cb) {
|
gulp.task('less', function (cb) {
|
||||||
gulp.src('styles/daggerheart.less').pipe(less()).pipe(gulp.dest('styles'));
|
gulp.src('styles/daggerheart.less')
|
||||||
|
.pipe(sourcemaps.init())
|
||||||
|
.pipe(less())
|
||||||
|
.on('error', console.error.bind(console))
|
||||||
|
.pipe(sourcemaps.write('.'))
|
||||||
|
.pipe(gulp.dest('styles'));
|
||||||
cb();
|
cb();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
71
lang/en.json
71
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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -119,8 +120,7 @@
|
||||||
"deleteTriggerContent": "Are you sure you want to delete the {trigger} trigger?",
|
"deleteTriggerContent": "Are you sure you want to delete the {trigger} trigger?",
|
||||||
"advantageState": "Advantage State",
|
"advantageState": "Advantage State",
|
||||||
"damageOnSave": "Damage on Save",
|
"damageOnSave": "Damage on Save",
|
||||||
"useDefaultItemValues": "Use default Item values",
|
"useDefaultItemValues": "Use default Item values"
|
||||||
"itemDamageIsUsed": "Item Damage Is Used"
|
|
||||||
},
|
},
|
||||||
"RollField": {
|
"RollField": {
|
||||||
"diceRolling": {
|
"diceRolling": {
|
||||||
|
|
@ -135,7 +135,7 @@
|
||||||
"attackModifier": "Attack Modifier",
|
"attackModifier": "Attack Modifier",
|
||||||
"attackName": "Attack Name",
|
"attackName": "Attack Name",
|
||||||
"criticalThreshold": "Critical Threshold",
|
"criticalThreshold": "Critical Threshold",
|
||||||
"includeBase": { "label": "Use Item Damage" },
|
"includeBase": { "label": "Include Item Damage" },
|
||||||
"groupAttack": { "label": "Group Attack" },
|
"groupAttack": { "label": "Group Attack" },
|
||||||
"multiplier": "Multiplier",
|
"multiplier": "Multiplier",
|
||||||
"saveHint": "Set a default Trait to enable Reaction Roll. It can be changed later in Reaction Roll Dialog.",
|
"saveHint": "Set a default Trait to enable Reaction Roll. It can be changed later in Reaction Roll Dialog.",
|
||||||
|
|
@ -340,6 +340,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}",
|
||||||
|
|
@ -413,7 +418,11 @@
|
||||||
"giveSpotlight": "Give The Spotlight",
|
"giveSpotlight": "Give The Spotlight",
|
||||||
"requestingSpotlight": "Requesting The Spotlight",
|
"requestingSpotlight": "Requesting The Spotlight",
|
||||||
"requestSpotlight": "Request The Spotlight",
|
"requestSpotlight": "Request The Spotlight",
|
||||||
"openCountdowns": "Countdowns"
|
"openCountdowns": "Countdowns",
|
||||||
|
"adversaryCategories": {
|
||||||
|
"friendly": "Friendly",
|
||||||
|
"adversaries": "Adversaries"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"CompendiumBrowserSettings": {
|
"CompendiumBrowserSettings": {
|
||||||
"title": "Enable Compendiums",
|
"title": "Enable Compendiums",
|
||||||
|
|
@ -711,19 +720,13 @@
|
||||||
},
|
},
|
||||||
"PendingReactionsDialog": {
|
"PendingReactionsDialog": {
|
||||||
"title": "Pending Reaction Rolls Found",
|
"title": "Pending Reaction Rolls Found",
|
||||||
"unfinishedRolls": "Some Tokens still need to roll their Reaction Roll.",
|
"unfinishedRolls": "Some Tokens have not finished their Reaction Rolls.",
|
||||||
"confirmation": "Are you sure you want to continue ?",
|
"warning": "Unfinished reaction rolls will be considered as failed.",
|
||||||
"warning": "Undone reaction rolls will be considered as failed"
|
"confirmation": "Are you sure you want to continue?"
|
||||||
},
|
},
|
||||||
"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"
|
||||||
|
|
@ -768,18 +771,25 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"GroupRollSelect": {
|
"GroupRollSelect": {
|
||||||
"title": "Group Roll",
|
"cancelConfirmText": "Are you sure you want to cancel the Group Roll? This will close it for all other players too.",
|
||||||
"aidingCharacters": "Aiding Characters",
|
"cancelConfirmTitle": "Cancel Group Roll",
|
||||||
|
"initializationTitle": "Character Selection",
|
||||||
|
"finishGroupRoll": "Finish Group Roll",
|
||||||
"leader": "Leader",
|
"leader": "Leader",
|
||||||
"leaderRoll": "Leader Roll",
|
"leaderRoll": "Leader Roll",
|
||||||
|
"members": "Members",
|
||||||
"openDialogForAll": "Open Dialog For All",
|
"openDialogForAll": "Open Dialog For All",
|
||||||
|
"removeRoll": "Remove Roll",
|
||||||
|
"resultsHint": "Results will appear when characters roll",
|
||||||
|
"selectLeaderHint": "Select one Character to be the leader",
|
||||||
|
"selectParticipantsHint": "Select one Character to be the leader",
|
||||||
"startGroupRoll": "Start Group Roll",
|
"startGroupRoll": "Start Group Roll",
|
||||||
"finishGroupRoll": "Finish Group Roll",
|
"title": "Group Roll"
|
||||||
"cancelConfirmTitle": "Cancel Group Roll",
|
|
||||||
"cancelConfirmText": "Are you sure you want to cancel the Group Roll? This will close it for all other players too."
|
|
||||||
},
|
},
|
||||||
"TokenConfig": {
|
"TokenConfig": {
|
||||||
"actorSizeUsed": "Actor size is set, determining the dimensions"
|
"actorSizeUsed": "Actor size is set, determining the dimensions",
|
||||||
|
"tokenSize": "Token Size",
|
||||||
|
"sizeCategory": "Size Category"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"CLASS": {
|
"CLASS": {
|
||||||
|
|
@ -2451,6 +2461,7 @@
|
||||||
"single": "Miss",
|
"single": "Miss",
|
||||||
"plural": "Miss"
|
"plural": "Miss"
|
||||||
},
|
},
|
||||||
|
"missingX": "Missing {x}",
|
||||||
"maxWithThing": "Max {thing}",
|
"maxWithThing": "Max {thing}",
|
||||||
"missingDragDropThing": "Drop {thing} here",
|
"missingDragDropThing": "Drop {thing} here",
|
||||||
"multiclass": "Multiclass",
|
"multiclass": "Multiclass",
|
||||||
|
|
@ -2542,6 +2553,9 @@
|
||||||
"recovery": { "label": "Recovery" },
|
"recovery": { "label": "Recovery" },
|
||||||
"type": { "label": "Type" },
|
"type": { "label": "Type" },
|
||||||
"value": { "label": "Value" }
|
"value": { "label": "Value" }
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"label": "Identifier"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Ancestry": {
|
"Ancestry": {
|
||||||
|
|
@ -2567,10 +2581,11 @@
|
||||||
"tokenImg": { "label": "Token Image" },
|
"tokenImg": { "label": "Token Image" },
|
||||||
"tokenRingImg": { "label": "Subject Texture" },
|
"tokenRingImg": { "label": "Subject Texture" },
|
||||||
"tokenSize": {
|
"tokenSize": {
|
||||||
"placeholder": "Using character dimensions",
|
"placeholder": "Token Size",
|
||||||
"disabledPlaceholder": "Set by character size",
|
"disabledPlaceholder": "Token Size",
|
||||||
"height": { "label": "Height" },
|
"height": { "label": "Height" },
|
||||||
"width": { "label": "Width" },
|
"width": { "label": "Width" },
|
||||||
|
"depth": { "label": "Depth" },
|
||||||
"scale": { "label": "Token Scale" }
|
"scale": { "label": "Token Scale" }
|
||||||
},
|
},
|
||||||
"evolved": {
|
"evolved": {
|
||||||
|
|
@ -2832,6 +2847,15 @@
|
||||||
"hideObserverPermissionInChat": {
|
"hideObserverPermissionInChat": {
|
||||||
"label": "Hide Chat Info From Players",
|
"label": "Hide Chat Info From Players",
|
||||||
"hint": "Information such as hit/miss on attack rolls against adversaries will be hidden"
|
"hint": "Information such as hit/miss on attack rolls against adversaries will be hidden"
|
||||||
|
},
|
||||||
|
"hidePartyStats": {
|
||||||
|
"label": "Hide Party Stats",
|
||||||
|
"hint": "Resources and stats in the party sheet's member list will be hidden to the following users, even if the user is part of the same party",
|
||||||
|
"choices": {
|
||||||
|
"never": "Never, always show",
|
||||||
|
"players": "Hide From Players",
|
||||||
|
"always": "Hide from Everyone"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -3087,6 +3111,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ChatLog": {
|
"ChatLog": {
|
||||||
|
"rerollActionRoll": "Reroll Action",
|
||||||
"rerollDamage": "Reroll Damage",
|
"rerollDamage": "Reroll Damage",
|
||||||
"assignTagRoll": "Assign as Tag Roll"
|
"assignTagRoll": "Assign as Tag Roll"
|
||||||
},
|
},
|
||||||
|
|
@ -3220,7 +3245,6 @@
|
||||||
"subclassesAlreadyPresent": "You already have a class and multiclass subclass",
|
"subclassesAlreadyPresent": "You already have a class and multiclass subclass",
|
||||||
"noDiceSystem": "Your selected dice {system} does not have a {faces} dice",
|
"noDiceSystem": "Your selected dice {system} does not have a {faces} dice",
|
||||||
"gmMenuRefresh": "You refreshed all actions and resources {types}",
|
"gmMenuRefresh": "You refreshed all actions and resources {types}",
|
||||||
"subclassAlreadyLinked": "{name} is already a subclass in the class {class}. Remove it from there if you want it to be a subclass to this class.",
|
|
||||||
"gmRequired": "This action requires an online GM",
|
"gmRequired": "This action requires an online GM",
|
||||||
"gmOnly": "This can only be accessed by the GM",
|
"gmOnly": "This can only be accessed by the GM",
|
||||||
"noActorOwnership": "You do not have permissions for this character",
|
"noActorOwnership": "You do not have permissions for this character",
|
||||||
|
|
@ -3230,7 +3254,8 @@
|
||||||
"domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used",
|
"domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used",
|
||||||
"knowTheTide": "Know The Tide gained a token",
|
"knowTheTide": "Know The Tide gained a token",
|
||||||
"lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}",
|
"lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}",
|
||||||
"noTokenTargeted": "No token is targeted"
|
"noTokenTargeted": "No token is targeted",
|
||||||
|
"behaviorRegionRequiresGM": "Creating a Region with an attached Behavior requires an online GM"
|
||||||
},
|
},
|
||||||
"Progress": {
|
"Progress": {
|
||||||
"migrationLabel": "Performing system migration. Please wait and do not close Foundry."
|
"migrationLabel": "Performing system migration. Please wait and do not close Foundry."
|
||||||
|
|
|
||||||
|
|
@ -439,10 +439,13 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
|
||||||
'system.domain': { key: 'system.domain', value: this.setup.class?.system.domains ?? null }
|
'system.domain': { key: 'system.domain', value: this.setup.class?.system.domains ?? null }
|
||||||
};
|
};
|
||||||
|
|
||||||
if (type === 'subclasses')
|
if (type === 'subclasses') {
|
||||||
|
const classItem = this.setup.class;
|
||||||
|
const uuid = classItem?._stats.compendiumSource ?? classItem?.uuid;
|
||||||
presets.filter = {
|
presets.filter = {
|
||||||
'system.linkedClass.uuid': { key: 'system.linkedClass.uuid', value: this.setup.class?.uuid }
|
'system.linkedClass': { key: 'system.linkedClass', value: uuid }
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (equipment.includes(type))
|
if (equipment.includes(type))
|
||||||
presets.filter = {
|
presets.filter = {
|
||||||
|
|
@ -610,7 +613,8 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
|
||||||
[foundry.utils.randomID()]: {}
|
[foundry.utils.randomID()]: {}
|
||||||
};
|
};
|
||||||
} else if (item.type === 'subclass' && event.target.closest('.subclass-card')) {
|
} else if (item.type === 'subclass' && event.target.closest('.subclass-card')) {
|
||||||
if (this.setup.class.system.subclasses.every(subclass => subclass.uuid !== item.uuid)) {
|
const classSubclasses = await this.setup.class.system.fetchSubclasses();
|
||||||
|
if (classSubclasses.every(subclass => subclass.uuid !== item.uuid)) {
|
||||||
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassNotInClass'));
|
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassNotInClass'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ export default class CompendiumBrowserSettings extends HandlebarsApplicationMixi
|
||||||
const excludedSourceData = this.browserSettings.excludedSources;
|
const excludedSourceData = this.browserSettings.excludedSources;
|
||||||
const excludedPackData = this.browserSettings.excludedPacks;
|
const excludedPackData = this.browserSettings.excludedPacks;
|
||||||
context.typePackCollections = game.packs.reduce((acc, pack) => {
|
context.typePackCollections = game.packs.reduce((acc, pack) => {
|
||||||
const { type, label, packageType, packageName: basePackageName, id } = pack.metadata;
|
const { type, label, packageType, packageName: basePackageName, name, id } = pack.metadata;
|
||||||
if (!CompendiumBrowserSettings.#browserPackTypes.includes(type)) return acc;
|
if (!CompendiumBrowserSettings.#browserPackTypes.includes(type)) return acc;
|
||||||
|
|
||||||
const isWorldPack = packageType === 'world';
|
const isWorldPack = packageType === 'world';
|
||||||
|
|
@ -68,13 +68,15 @@ export default class CompendiumBrowserSettings extends HandlebarsApplicationMixi
|
||||||
if (!acc[type].sources[packageName])
|
if (!acc[type].sources[packageName])
|
||||||
acc[type].sources[packageName] = { label: sourceLabel, checked: sourceChecked, packs: [] };
|
acc[type].sources[packageName] = { label: sourceLabel, checked: sourceChecked, packs: [] };
|
||||||
|
|
||||||
const checked = !excludedPackData[id] || !excludedPackData[id].excludedDocumentTypes.includes(type);
|
const included =
|
||||||
|
!excludedPackData[packageName] ||
|
||||||
|
!excludedPackData[packageName][name]?.excludedDocumentTypes.includes(type);
|
||||||
|
|
||||||
acc[type].sources[packageName].packs.push({
|
acc[type].sources[packageName].packs.push({
|
||||||
pack: id,
|
name,
|
||||||
type,
|
type,
|
||||||
label: id === game.system.id ? game.system.title : game.i18n.localize(label),
|
label: id === game.system.id ? game.system.title : game.i18n.localize(label),
|
||||||
checked: checked
|
checked: included
|
||||||
});
|
});
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
|
|
@ -106,16 +108,16 @@ export default class CompendiumBrowserSettings extends HandlebarsApplicationMixi
|
||||||
toggleTypedPack(event) {
|
toggleTypedPack(event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
const { type, pack } = event.target.dataset;
|
const { type, source, packName } = event.target.dataset;
|
||||||
const currentlyExcluded = this.browserSettings.excludedPacks[pack]
|
const currentlyExcluded = this.browserSettings.excludedPacks[source]?.[packName]
|
||||||
? this.browserSettings.excludedPacks[pack].excludedDocumentTypes.includes(type)
|
? this.browserSettings.excludedPacks[source][packName].excludedDocumentTypes.includes(type)
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
if (!this.browserSettings.excludedPacks[pack])
|
this.browserSettings.excludedPacks[source] ??= {};
|
||||||
this.browserSettings.excludedPacks[pack] = { excludedDocumentTypes: [] };
|
this.browserSettings.excludedPacks[source][packName] ??= { excludedDocumentTypes: [] };
|
||||||
this.browserSettings.excludedPacks[pack].excludedDocumentTypes = currentlyExcluded
|
this.browserSettings.excludedPacks[source][packName].excludedDocumentTypes = currentlyExcluded
|
||||||
? this.browserSettings.excludedPacks[pack].excludedDocumentTypes.filter(x => x !== type)
|
? this.browserSettings.excludedPacks[source][packName].excludedDocumentTypes.filter(x => x !== type)
|
||||||
: [...(this.browserSettings.excludedPacks[pack]?.excludedDocumentTypes ?? []), type];
|
: [...(this.browserSettings.excludedPacks[source][packName]?.excludedDocumentTypes ?? []), type];
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -72,8 +72,8 @@ export default class ActionSelectionDialog extends HandlebarsApplicationMixin(Ap
|
||||||
|
|
||||||
static async #onChooseAction(event, button) {
|
static async #onChooseAction(event, button) {
|
||||||
const { actionId } = button.dataset;
|
const { actionId } = button.dataset;
|
||||||
this.action = this.item.system.actionsList.find(a => a._id === actionId);
|
this.#action = this.item.system.actionsList.find(a => a._id === actionId);
|
||||||
Object.defineProperty(this.event, 'shiftKey', {
|
Object.defineProperty(this.#event, 'shiftKey', {
|
||||||
get() {
|
get() {
|
||||||
return event.shiftKey;
|
return event.shiftKey;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -175,14 +175,14 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
this.disadvantage = advantage === -1;
|
this.disadvantage = advantage === -1;
|
||||||
|
|
||||||
this.config.roll.advantage = this.config.roll.advantage === advantage ? 0 : advantage;
|
this.config.roll.advantage = this.config.roll.advantage === advantage ? 0 : advantage;
|
||||||
|
if (this.config.roll.advantage === 0) return this.render();
|
||||||
|
|
||||||
if (this.config.roll.advantage === 1 && this.config.data.rules.roll.advantageFaces) {
|
const defaultFaces =
|
||||||
const faces = Number.parseInt(this.config.data.rules.roll.advantageFaces);
|
this.config.roll.advantage === 1
|
||||||
|
? this.config.data.rules.roll.defaultAdvantageDice
|
||||||
|
: this.config.data.rules.roll.defaultDisadvantageDice;
|
||||||
|
const faces = Number.parseInt(defaultFaces);
|
||||||
this.roll.advantageFaces = Number.isNaN(faces) ? this.roll.advantageFaces : faces;
|
this.roll.advantageFaces = Number.isNaN(faces) ? this.roll.advantageFaces : faces;
|
||||||
} else if (this.config.roll.advantage === -1 && this.config.data.rules.roll.disadvantageFaces) {
|
|
||||||
const faces = Number.parseInt(this.config.data.rules.roll.disadvantageFaces);
|
|
||||||
this.roll.advantageFaces = Number.isNaN(faces) ? this.roll.advantageFaces : faces;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,10 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
||||||
);
|
);
|
||||||
|
|
||||||
const orderedArmorSources = getArmorSources(actor).filter(s => !s.disabled);
|
const orderedArmorSources = getArmorSources(actor).filter(s => !s.disabled);
|
||||||
const armor = orderedArmorSources.reduce((acc, { document }) => {
|
const armor = orderedArmorSources.reduce((acc, { name, document }) => {
|
||||||
const { current, max } = document.type === 'armor' ? document.system.armor : document.system.armorData;
|
const { current, max } = document.type === 'armor' ? document.system.armor : document.system.armorData;
|
||||||
acc.push({
|
acc.push({
|
||||||
|
name,
|
||||||
effect: document,
|
effect: document,
|
||||||
marks: [...Array(max).keys()].reduce((acc, _, index) => {
|
marks: [...Array(max).keys()].reduce((acc, _, index) => {
|
||||||
const spent = index < current;
|
const spent = index < current;
|
||||||
|
|
@ -152,14 +153,8 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
||||||
|
|
||||||
const armorSources = [];
|
const armorSources = [];
|
||||||
for (const source of this.marks.armor) {
|
for (const source of this.marks.armor) {
|
||||||
const parent = source.effect.origin
|
|
||||||
? await foundry.utils.fromUuid(source.effect.origin)
|
|
||||||
: source.effect.parent;
|
|
||||||
|
|
||||||
const useEffectName = parent.type === 'armor' || parent instanceof Actor;
|
|
||||||
const label = useEffectName ? source.effect.name : parent.name;
|
|
||||||
armorSources.push({
|
armorSources.push({
|
||||||
label: label,
|
label: source.name,
|
||||||
uuid: source.effect.uuid,
|
uuid: source.effect.uuid,
|
||||||
marks: source.marks
|
marks: source.marks
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
|
||||||
|
|
||||||
let returnMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.avoidScar');
|
let returnMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.avoidScar');
|
||||||
if (config.roll.fate.value <= this.actor.system.levelData.level.current) {
|
if (config.roll.fate.value <= this.actor.system.levelData.level.current) {
|
||||||
|
const maxHope = this.actor.system.resources.hope.max + this.actor.system.scars;
|
||||||
const newScarAmount = this.actor.system.scars + 1;
|
const newScarAmount = this.actor.system.scars + 1;
|
||||||
await this.actor.update({
|
await this.actor.update({
|
||||||
system: {
|
system: {
|
||||||
|
|
@ -64,7 +65,7 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (newScarAmount >= this.actor.system.resources.hope.max) {
|
if (newScarAmount >= maxHope) {
|
||||||
await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id);
|
await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id);
|
||||||
return game.i18n.format('DAGGERHEART.UI.Chat.deathMove.journeysEnd', { scars: newScarAmount });
|
return game.i18n.format('DAGGERHEART.UI.Chat.deathMove.journeysEnd', { scars: newScarAmount });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -259,7 +259,9 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
|
||||||
const resetValue = increasing
|
const resetValue = increasing
|
||||||
? 0
|
? 0
|
||||||
: feature.system.resource.max
|
: feature.system.resource.max
|
||||||
? new Roll(Roll.replaceFormulaData(feature.system.resource.max, this.actor)).evaluateSync().total
|
? new Roll(
|
||||||
|
Roll.replaceFormulaData(feature.system.resource.max, this.actor.getRollData())
|
||||||
|
).evaluateSync().total
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
await feature.update({ 'system.resource.value': resetValue });
|
await feature.update({ 'system.resource.value': resetValue });
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { ResourceUpdateMap } from '../../data/action/baseAction.mjs';
|
import { ResourceUpdateMap } from '../../data/action/baseAction.mjs';
|
||||||
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
|
import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
|
||||||
import Party from '../sheets/actors/party.mjs';
|
import Party from '../sheets/actors/party.mjs';
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
||||||
export default class GroupRollDialog extends HandlebarsApplicationMixin(ApplicationV2) {
|
export default class GroupRollDialog extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
constructor(party) {
|
constructor(party) {
|
||||||
super();
|
super({ id: `GroupRollDialog-${party.id}` });
|
||||||
|
|
||||||
this.party = party;
|
this.party = party;
|
||||||
this.partyMembers = party.system.partyMembers
|
this.partyMembers = party.system.partyMembers
|
||||||
|
|
@ -35,19 +35,18 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
|
||||||
|
|
||||||
static DEFAULT_OPTIONS = {
|
static DEFAULT_OPTIONS = {
|
||||||
tag: 'form',
|
tag: 'form',
|
||||||
id: 'GroupRollDialog',
|
|
||||||
classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'group-roll-dialog'],
|
classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'group-roll-dialog'],
|
||||||
position: { width: 550, height: 'auto' },
|
position: { width: 390, height: 'auto' },
|
||||||
|
window: {
|
||||||
|
icon: 'fa-solid fa-users'
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
toggleSelectMember: this.#toggleSelectMember,
|
toggleSelectMember: this.#toggleSelectMember,
|
||||||
startGroupRoll: this.#startGroupRoll,
|
startGroupRoll: this.#startGroupRoll,
|
||||||
makeRoll: this.#makeRoll,
|
makeRoll: this.#makeRoll,
|
||||||
removeRoll: this.#removeRoll,
|
removeRoll: this.#removeRoll,
|
||||||
rerollDice: this.#rerollDice,
|
rerollDice: this.#rerollDice,
|
||||||
makeLeaderRoll: this.#makeLeaderRoll,
|
markSuccessful: this.#markSuccessful,
|
||||||
removeLeaderRoll: this.#removeLeaderRoll,
|
|
||||||
rerollLeaderDice: this.#rerollLeaderDice,
|
|
||||||
markSuccessfull: this.#markSuccessfull,
|
|
||||||
cancelRoll: this.#onCancelRoll,
|
cancelRoll: this.#onCancelRoll,
|
||||||
finishRoll: this.#finishRoll
|
finishRoll: this.#finishRoll
|
||||||
},
|
},
|
||||||
|
|
@ -59,17 +58,21 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
|
||||||
id: 'initialization',
|
id: 'initialization',
|
||||||
template: 'systems/daggerheart/templates/dialogs/groupRollDialog/initialization.hbs'
|
template: 'systems/daggerheart/templates/dialogs/groupRollDialog/initialization.hbs'
|
||||||
},
|
},
|
||||||
|
main: {
|
||||||
|
id: 'main',
|
||||||
|
template: 'systems/daggerheart/templates/dialogs/groupRollDialog/main.hbs'
|
||||||
|
},
|
||||||
leader: {
|
leader: {
|
||||||
id: 'leader',
|
id: 'leader',
|
||||||
template: 'systems/daggerheart/templates/dialogs/groupRollDialog/leader.hbs'
|
template: 'systems/daggerheart/templates/dialogs/groupRollDialog/parts/member.hbs'
|
||||||
},
|
},
|
||||||
groupRoll: {
|
result: {
|
||||||
id: 'groupRoll',
|
id: 'result',
|
||||||
template: 'systems/daggerheart/templates/dialogs/groupRollDialog/groupRoll.hbs'
|
template: 'systems/daggerheart/templates/dialogs/groupRollDialog/parts/result.hbs'
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
id: 'footer',
|
id: 'footer',
|
||||||
template: 'systems/daggerheart/templates/dialogs/groupRollDialog/footer.hbs'
|
template: 'systems/daggerheart/templates/dialogs/groupRollDialog/parts/footer.hbs'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -89,51 +92,31 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
|
||||||
}
|
}
|
||||||
|
|
||||||
_configureRenderParts(options) {
|
_configureRenderParts(options) {
|
||||||
const { initialization, leader, groupRoll, footer } = super._configureRenderParts(options);
|
const parts = super._configureRenderParts(options);
|
||||||
const augmentedParts = { initialization };
|
|
||||||
for (const memberKey of Object.keys(this.party.system.groupRoll.aidingCharacters)) {
|
for (const memberKey of Object.keys(this.party.system.groupRoll.aidingCharacters)) {
|
||||||
augmentedParts[memberKey] = {
|
parts[memberKey] = {
|
||||||
id: memberKey,
|
id: memberKey,
|
||||||
template: 'systems/daggerheart/templates/dialogs/groupRollDialog/groupRollMember.hbs'
|
template: 'systems/daggerheart/templates/dialogs/groupRollDialog/parts/member.hbs'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
return parts;
|
||||||
augmentedParts.leader = leader;
|
|
||||||
augmentedParts.groupRoll = groupRoll;
|
|
||||||
augmentedParts.footer = footer;
|
|
||||||
|
|
||||||
return augmentedParts;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**@inheritdoc */
|
|
||||||
async _onRender(context, options) {
|
|
||||||
await super._onRender(context, options);
|
|
||||||
|
|
||||||
if (this.element.querySelector('.team-container')) return;
|
|
||||||
|
|
||||||
if (this.tabGroups.application !== this.constructor.PARTS.initialization.id) {
|
|
||||||
const initializationPart = this.element.querySelector('.initialization-container');
|
|
||||||
initializationPart.insertAdjacentHTML('afterend', '<div class="team-container"></div>');
|
|
||||||
initializationPart.insertAdjacentHTML(
|
|
||||||
'afterend',
|
|
||||||
`<div class="section-title">${game.i18n.localize('DAGGERHEART.APPLICATIONS.GroupRollSelect.aidingCharacters')}</div>`
|
|
||||||
);
|
|
||||||
|
|
||||||
const teamContainer = this.element.querySelector('.team-container');
|
|
||||||
for (const memberContainer of this.element.querySelectorAll('.team-member-container'))
|
|
||||||
teamContainer.appendChild(memberContainer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _prepareContext(_options) {
|
async _prepareContext(_options) {
|
||||||
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;
|
||||||
context.members = {};
|
context.members = {};
|
||||||
|
context.aidKeys = Object.keys(this.party.system.groupRoll.aidingCharacters);
|
||||||
context.allHaveRolled = Object.keys(context.data.participants).every(key => {
|
context.allHaveRolled = Object.keys(context.data.participants).every(key => {
|
||||||
const data = context.data.participants[key];
|
const data = context.data.participants[key];
|
||||||
return Boolean(data.rollData);
|
return Boolean(data.rollData);
|
||||||
|
|
@ -145,6 +128,7 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
|
||||||
async _preparePartContext(partId, context, options) {
|
async _preparePartContext(partId, context, options) {
|
||||||
const partContext = await super._preparePartContext(partId, context, options);
|
const partContext = await super._preparePartContext(partId, context, options);
|
||||||
partContext.partId = partId;
|
partContext.partId = partId;
|
||||||
|
partContext.leader = this.getRollCharacterData(this.party.system.groupRoll.leader);
|
||||||
|
|
||||||
switch (partId) {
|
switch (partId) {
|
||||||
case 'initialization':
|
case 'initialization':
|
||||||
|
|
@ -162,19 +146,14 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
|
||||||
partContext.canStartGroupRoll = selectedMembers.length > 1 && this.leader?.memberId;
|
partContext.canStartGroupRoll = selectedMembers.length > 1 && this.leader?.memberId;
|
||||||
partContext.openForAllPlayers = this.openForAllPlayers;
|
partContext.openForAllPlayers = this.openForAllPlayers;
|
||||||
break;
|
break;
|
||||||
case 'leader':
|
case 'result':
|
||||||
partContext.leader = this.getRollCharacterData(this.party.system.groupRoll.leader);
|
|
||||||
break;
|
|
||||||
case 'groupRoll':
|
|
||||||
const leader = this.party.system.groupRoll.leader;
|
const leader = this.party.system.groupRoll.leader;
|
||||||
partContext.hasRolled =
|
partContext.hasRolled =
|
||||||
leader?.rollData ||
|
leader?.rollData ||
|
||||||
Object.values(this.party.system.groupRoll?.aidingCharacters ?? {}).some(
|
Object.values(this.party.system.groupRoll?.aidingCharacters ?? {}).some(x => x.successful !== null);
|
||||||
x => x.successfull !== null
|
|
||||||
);
|
|
||||||
const { modifierTotal, modifiers } = Object.values(this.party.system.groupRoll.aidingCharacters).reduce(
|
const { modifierTotal, modifiers } = Object.values(this.party.system.groupRoll.aidingCharacters).reduce(
|
||||||
(acc, curr) => {
|
(acc, curr) => {
|
||||||
const modifier = curr.successfull === true ? 1 : curr.successfull === false ? -1 : null;
|
const modifier = curr.successful === true ? 1 : curr.successful === false ? -1 : null;
|
||||||
if (modifier) {
|
if (modifier) {
|
||||||
acc.modifierTotal += modifier;
|
acc.modifierTotal += modifier;
|
||||||
acc.modifiers.push(modifier);
|
acc.modifiers.push(modifier);
|
||||||
|
|
@ -200,7 +179,7 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
|
||||||
case 'footer':
|
case 'footer':
|
||||||
partContext.canFinishRoll =
|
partContext.canFinishRoll =
|
||||||
Boolean(this.party.system.groupRoll.leader?.rollData) &&
|
Boolean(this.party.system.groupRoll.leader?.rollData) &&
|
||||||
Object.values(this.party.system.groupRoll.aidingCharacters).every(x => x.successfull !== null);
|
Object.values(this.party.system.groupRoll.aidingCharacters).every(x => x.successful !== null);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -216,20 +195,42 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
|
||||||
if (!data) return {};
|
if (!data) return {};
|
||||||
|
|
||||||
const actor = game.actors.get(data.id);
|
const actor = game.actors.get(data.id);
|
||||||
|
const isLeader = data === this.party.system.groupRoll.leader;
|
||||||
|
|
||||||
|
const roll = data.roll;
|
||||||
|
const withTypeSuffix = !roll ? null : roll.isCritical ? 'criticalShort' : roll.withHope ? 'hope' : 'fear';
|
||||||
|
const thing = withTypeSuffix ? _loc(`DAGGERHEART.GENERAL.${withTypeSuffix}`) : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
|
type: isLeader ? 'leader' : 'aid',
|
||||||
|
basePath: isLeader ? 'system.groupRoll.leader' : `system.groupRoll.aidingCharacters.${data.id}`,
|
||||||
|
rollChoiceLabel: _loc(CONFIG.DH.ACTOR.abilities[data.rollChoice]?.label),
|
||||||
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),
|
||||||
|
modifier: data.successful ? 1 : data.successful === false ? -1 : 0,
|
||||||
|
withLabelShort: thing ? _loc('DAGGERHEART.GENERAL.withThing', { thing }) : null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#getCharacterDataById(id) {
|
||||||
|
if (!id) return null;
|
||||||
|
|
||||||
|
const groupRoll = this.party.system.groupRoll;
|
||||||
|
if (id === 'leader' || id === groupRoll.leader?.id) {
|
||||||
|
return { data: groupRoll.leader, basePath: 'system.groupRoll.leader' };
|
||||||
|
} else if (id in groupRoll.aidingCharacters) {
|
||||||
|
return { data: groupRoll.aidingCharacters[id], basePath: `system.groupRoll.aidingCharacters.${id}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
static async updateData(event, _, formData) {
|
static async updateData(event, _, formData) {
|
||||||
const partyData = foundry.utils.expandObject(formData.object);
|
const partyData = foundry.utils.expandObject(formData.object);
|
||||||
|
|
||||||
this.updatePartyData(partyData, this.getUpdatingParts(event.target));
|
this.updatePartyData(partyData, this.getUpdatingParts(event.target));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -246,7 +247,7 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
await emitAsGM(
|
await emitGMUpdate(
|
||||||
GMUpdateEvent.UpdateDocument,
|
GMUpdateEvent.UpdateDocument,
|
||||||
gmUpdate,
|
gmUpdate,
|
||||||
update,
|
update,
|
||||||
|
|
@ -256,26 +257,19 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
|
||||||
}
|
}
|
||||||
|
|
||||||
getUpdatingParts(target) {
|
getUpdatingParts(target) {
|
||||||
const { initialization, leader, groupRoll, footer } = this.constructor.PARTS;
|
const { initialization, leader, result, footer } = this.constructor.PARTS;
|
||||||
const isInitialization = this.tabGroups.application === initialization.id;
|
const isInitialization = this.tabGroups.application === initialization.id;
|
||||||
const updatingMember = target.closest('.team-member-container')?.dataset?.memberKey;
|
const updatingMember = target.closest('.member-roll-container.aid')?.dataset?.memberKey;
|
||||||
const updatingLeader = target.closest('.main-character-outer-container');
|
const updatingLeader = target.closest('.member-roll-container.leader');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...(isInitialization ? [initialization.id] : []),
|
...(isInitialization ? [initialization.id] : []),
|
||||||
...(updatingMember ? [updatingMember] : []),
|
...(updatingMember ? [updatingMember] : []),
|
||||||
...(updatingLeader ? [leader.id] : []),
|
...(updatingLeader ? [leader.id] : []),
|
||||||
...(!isInitialization ? [groupRoll.id, footer.id] : [])
|
...(!isInitialization ? [result.id, footer.id] : [])
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
|
|
@ -304,6 +298,9 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
|
||||||
static #toggleSelectMember(_, button) {
|
static #toggleSelectMember(_, button) {
|
||||||
const member = this.partyMembers.find(x => x.id === button.dataset.id);
|
const member = this.partyMembers.find(x => x.id === button.dataset.id);
|
||||||
member.selected = !member.selected;
|
member.selected = !member.selected;
|
||||||
|
if (this.leader?.memberId === member.id) {
|
||||||
|
this.leader = null;
|
||||||
|
}
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -343,11 +340,14 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
async makeRoll(button, characterData, path) {
|
/** @this GroupRollDialog */
|
||||||
const actor = game.actors.find(x => x.id === characterData.id);
|
static async #makeRoll(_event, button) {
|
||||||
|
const member = button.closest('[data-member-key]').dataset.memberKey;
|
||||||
|
const { data, basePath } = this.#getCharacterDataById(member);
|
||||||
|
const actor = game.actors.find(x => x.id === data.id);
|
||||||
if (!actor) return;
|
if (!actor) return;
|
||||||
|
|
||||||
const result = await actor.rollTrait(characterData.rollChoice, {
|
const result = await actor.rollTrait(data.rollChoice, {
|
||||||
skips: {
|
skips: {
|
||||||
createMessage: true,
|
createMessage: true,
|
||||||
resources: true,
|
resources: true,
|
||||||
|
|
@ -356,53 +356,38 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
|
||||||
});
|
});
|
||||||
|
|
||||||
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(
|
||||||
{
|
{
|
||||||
[path]: rollData
|
[basePath]: { rollData, successful: null }
|
||||||
},
|
},
|
||||||
this.getUpdatingParts(button)
|
this.getUpdatingParts(button)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async #makeRoll(_event, button) {
|
/** @this GroupRollDialog */
|
||||||
const { member } = button.dataset;
|
static async #removeRoll(_event, button) {
|
||||||
const character = this.party.system.groupRoll.aidingCharacters[member];
|
const member = button.closest('[data-member-key]').dataset.memberKey;
|
||||||
this.makeRoll(button, character, `system.groupRoll.aidingCharacters.${member}.rollData`);
|
const { basePath } = this.#getCharacterDataById(member);
|
||||||
}
|
|
||||||
|
|
||||||
static async #makeLeaderRoll(_event, button) {
|
|
||||||
const character = this.party.system.groupRoll.leader;
|
|
||||||
this.makeRoll(button, character, 'system.groupRoll.leader.rollData');
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeRoll(button, path) {
|
|
||||||
this.updatePartyData(
|
this.updatePartyData(
|
||||||
{
|
{
|
||||||
[path]: {
|
[basePath]: {
|
||||||
rollData: null,
|
rollData: null,
|
||||||
rollChoice: null,
|
rollChoice: null,
|
||||||
selected: false,
|
selected: false,
|
||||||
successfull: null
|
successful: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
this.getUpdatingParts(button)
|
this.getUpdatingParts(button)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async #removeRoll(_event, button) {
|
/** @this GroupRollDialog */
|
||||||
this.removeRoll(button, `system.groupRoll.aidingCharacters.${button.dataset.member}`);
|
static async #rerollDice(_, button) {
|
||||||
}
|
|
||||||
|
|
||||||
static async #removeLeaderRoll(_event, button) {
|
|
||||||
this.removeRoll(button, 'system.groupRoll.leader');
|
|
||||||
}
|
|
||||||
|
|
||||||
async rerollDice(button, data, path) {
|
|
||||||
const { diceType } = button.dataset;
|
const { diceType } = button.dataset;
|
||||||
|
const { data, basePath } = this.#getCharacterDataById(button.dataset.member);
|
||||||
|
|
||||||
const dieIndex = diceType === 'hope' ? 0 : diceType === 'fear' ? 1 : 2;
|
const dieIndex = diceType === 'hope' ? 0 : diceType === 'fear' ? 1 : 2;
|
||||||
const newRoll = game.system.api.dice.DualityRoll.fromData(data.rollData);
|
const newRoll = game.system.api.dice.DualityRoll.fromData(data.rollData);
|
||||||
|
|
@ -416,31 +401,19 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
|
||||||
const rollData = newRoll.toJSON();
|
const rollData = newRoll.toJSON();
|
||||||
this.updatePartyData(
|
this.updatePartyData(
|
||||||
{
|
{
|
||||||
[path]: rollData
|
[`${basePath}.rollData`]: rollData
|
||||||
},
|
},
|
||||||
this.getUpdatingParts(button)
|
this.getUpdatingParts(button)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async #rerollDice(_, button) {
|
static #markSuccessful(_event, button) {
|
||||||
const { member } = button.dataset;
|
const memberKey = button.closest('[data-member-key]').dataset.memberKey;
|
||||||
this.rerollDice(
|
const previousValue = this.party.system.groupRoll.aidingCharacters[memberKey].successful;
|
||||||
button,
|
const newValue = Boolean(button.dataset.success === 'true');
|
||||||
this.party.system.groupRoll.aidingCharacters[member],
|
|
||||||
`system.groupRoll.aidingCharacters.${member}.rollData`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async #rerollLeaderDice(_, button) {
|
|
||||||
this.rerollDice(button, this.party.system.groupRoll.leader, `system.groupRoll.leader.rollData`);
|
|
||||||
}
|
|
||||||
|
|
||||||
static #markSuccessfull(_event, button) {
|
|
||||||
const previousValue = this.party.system.groupRoll.aidingCharacters[button.dataset.member].successfull;
|
|
||||||
const newValue = Boolean(button.dataset.successfull === 'true');
|
|
||||||
this.updatePartyData(
|
this.updatePartyData(
|
||||||
{
|
{
|
||||||
[`system.groupRoll.aidingCharacters.${button.dataset.member}.successfull`]:
|
[`system.groupRoll.aidingCharacters.${memberKey}.successful`]:
|
||||||
previousValue === newValue ? null : newValue
|
previousValue === newValue ? null : newValue
|
||||||
},
|
},
|
||||||
this.getUpdatingParts(button)
|
this.getUpdatingParts(button)
|
||||||
|
|
@ -484,7 +457,7 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
|
||||||
static async #finishRoll() {
|
static async #finishRoll() {
|
||||||
const totalRoll = this.party.system.groupRoll.leader.roll;
|
const totalRoll = this.party.system.groupRoll.leader.roll;
|
||||||
for (const character of Object.values(this.party.system.groupRoll.aidingCharacters)) {
|
for (const character of Object.values(this.party.system.groupRoll.aidingCharacters)) {
|
||||||
totalRoll.terms.push(new foundry.dice.terms.OperatorTerm({ operator: character.successfull ? '+' : '-' }));
|
totalRoll.terms.push(new foundry.dice.terms.OperatorTerm({ operator: character.successful ? '+' : '-' }));
|
||||||
totalRoll.terms.push(new foundry.dice.terms.NumericTerm({ number: 1 }));
|
totalRoll.terms.push(new foundry.dice.terms.NumericTerm({ number: 1 }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { MemberData } from '../../data/tagTeamData.mjs';
|
import { MemberData } from '../../data/tagTeamData.mjs';
|
||||||
import { getCritDamageBonus } from '../../helpers/utils.mjs';
|
import { getCritDamageBonus } from '../../helpers/utils.mjs';
|
||||||
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
|
import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
|
||||||
import Party from '../sheets/actors/party.mjs';
|
import Party from '../sheets/actors/party.mjs';
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
||||||
export default class TagTeamDialog extends HandlebarsApplicationMixin(ApplicationV2) {
|
export default class TagTeamDialog extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
constructor(party) {
|
constructor(party) {
|
||||||
super();
|
super({ id: `TagTeamDialog-${party.id}` });
|
||||||
|
|
||||||
this.party = party;
|
this.party = party;
|
||||||
this.partyMembers = party.system.partyMembers
|
this.partyMembers = party.system.partyMembers
|
||||||
|
|
@ -36,9 +36,11 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
|
|
||||||
static DEFAULT_OPTIONS = {
|
static DEFAULT_OPTIONS = {
|
||||||
tag: 'form',
|
tag: 'form',
|
||||||
id: 'TagTeamDialog',
|
|
||||||
classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'tag-team-dialog'],
|
classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'tag-team-dialog'],
|
||||||
position: { width: 550, height: 'auto' },
|
position: { width: 550, height: 'auto' },
|
||||||
|
window: {
|
||||||
|
icon: 'fa-solid fa-user-group'
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
toggleSelectMember: TagTeamDialog.#toggleSelectMember,
|
toggleSelectMember: TagTeamDialog.#toggleSelectMember,
|
||||||
startTagTeamRoll: TagTeamDialog.#startTagTeamRoll,
|
startTagTeamRoll: TagTeamDialog.#startTagTeamRoll,
|
||||||
|
|
@ -60,13 +62,17 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
id: 'initialization',
|
id: 'initialization',
|
||||||
template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/initialization.hbs'
|
template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/initialization.hbs'
|
||||||
},
|
},
|
||||||
|
tagTeamRoll: {
|
||||||
|
id: 'tagTeamRoll',
|
||||||
|
template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/tagTeamRoll.hbs'
|
||||||
|
},
|
||||||
rollSelection: {
|
rollSelection: {
|
||||||
id: 'rollSelection',
|
id: 'rollSelection',
|
||||||
template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/rollSelection.hbs'
|
template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/rollSelection.hbs'
|
||||||
},
|
},
|
||||||
tagTeamRoll: {
|
result: {
|
||||||
id: 'tagTeamRoll',
|
id: 'result',
|
||||||
template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/tagTeamRoll.hbs'
|
template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/result.hbs'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -97,41 +103,25 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
}
|
}
|
||||||
|
|
||||||
_configureRenderParts(options) {
|
_configureRenderParts(options) {
|
||||||
const { initialization, rollSelection, tagTeamRoll } = super._configureRenderParts(options);
|
const parts = super._configureRenderParts(options);
|
||||||
const augmentedParts = { initialization };
|
|
||||||
for (const memberKey of Object.keys(this.party.system.tagTeam.members)) {
|
for (const memberKey of Object.keys(this.party.system.tagTeam.members)) {
|
||||||
augmentedParts[memberKey] = {
|
parts[memberKey] = {
|
||||||
id: memberKey,
|
id: memberKey,
|
||||||
template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/tagTeamMember.hbs'
|
template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/tagTeamMember.hbs'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
augmentedParts.rollSelection = rollSelection;
|
|
||||||
augmentedParts.tagTeamRoll = tagTeamRoll;
|
|
||||||
|
|
||||||
return augmentedParts;
|
return parts;
|
||||||
}
|
|
||||||
|
|
||||||
/**@inheritdoc */
|
|
||||||
async _onRender(context, options) {
|
|
||||||
await super._onRender(context, options);
|
|
||||||
|
|
||||||
// if (this.element.querySelector('.roll-selection')) {
|
|
||||||
// for (const element of this.element.querySelectorAll('.team-member-container')) {
|
|
||||||
// element.classList.add('select-padding');
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (this.element.querySelector('.team-container')) return;
|
|
||||||
const initializationPart = this.element.querySelector('.initialization-container');
|
|
||||||
initializationPart.insertAdjacentHTML('afterend', '<div class="team-container"></div>');
|
|
||||||
const teamContainer = this.element.querySelector('.team-container');
|
|
||||||
for (const memberContainer of this.element.querySelectorAll('.team-member-container'))
|
|
||||||
teamContainer.appendChild(memberContainer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -167,6 +157,9 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
partContext.initiatorDisabled = !selectedMembers.length;
|
partContext.initiatorDisabled = !selectedMembers.length;
|
||||||
partContext.openForAllPlayers = this.openForAllPlayers;
|
partContext.openForAllPlayers = this.openForAllPlayers;
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'tagTeamRoll':
|
||||||
|
partContext.memberKeys = Object.keys(this.party.system.tagTeam.members);
|
||||||
break;
|
break;
|
||||||
case 'rollSelection':
|
case 'rollSelection':
|
||||||
partContext.members = Object.keys(this.party.system.tagTeam.members).reduce((acc, key) => {
|
partContext.members = Object.keys(this.party.system.tagTeam.members).reduce((acc, key) => {
|
||||||
|
|
@ -175,7 +168,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
break;
|
break;
|
||||||
case 'tagTeamRoll':
|
case 'result':
|
||||||
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
|
? undefined
|
||||||
|
|
@ -191,59 +184,58 @@ 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, tagTeamRoll } = this.constructor.PARTS;
|
const { initialization, rollSelection, result } = this.constructor.PARTS;
|
||||||
const isInitialization = this.tabGroups.application === initialization.id;
|
const isInitialization = this.tabGroups.application === initialization.id;
|
||||||
const updatingMember = target.closest('.team-member-container')?.dataset?.memberKey;
|
const updatingMember = target.closest('.team-member-container')?.dataset?.memberKey;
|
||||||
|
|
||||||
|
|
@ -251,7 +243,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
...(isInitialization ? [initialization.id] : []),
|
...(isInitialization ? [initialization.id] : []),
|
||||||
...(updatingMember ? [updatingMember] : []),
|
...(updatingMember ? [updatingMember] : []),
|
||||||
...(!isInitialization ? [rollSelection.id] : []),
|
...(!isInitialization ? [rollSelection.id] : []),
|
||||||
...(!isInitialization ? [tagTeamRoll.id] : [])
|
...(!isInitialization ? [result.id] : [])
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -274,7 +266,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
await emitAsGM(
|
await emitGMUpdate(
|
||||||
GMUpdateEvent.UpdateDocument,
|
GMUpdateEvent.UpdateDocument,
|
||||||
gmUpdate,
|
gmUpdate,
|
||||||
update,
|
update,
|
||||||
|
|
@ -285,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;
|
||||||
|
|
||||||
|
|
@ -446,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(
|
||||||
|
|
@ -663,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]
|
||||||
|
|
@ -685,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;
|
||||||
|
|
@ -699,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 }) {
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,9 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
|
||||||
const animationDuration = 500;
|
const animationDuration = 500;
|
||||||
const scene = game.scenes.get(game.user.viewedScene);
|
const scene = game.scenes.get(game.user.viewedScene);
|
||||||
/* getDependentTokens returns already removed tokens with id = null. Need to filter that until it's potentially fixed from Foundry */
|
/* getDependentTokens returns already removed tokens with id = null. Need to filter that until it's potentially fixed from Foundry */
|
||||||
const activeTokens = actors.flatMap(member => member.getDependentTokens({ scenes: scene }).filter(x => x._id));
|
const activeTokens = actors.flatMap(member =>
|
||||||
|
member.getDependentTokens({ scenes: scene }).filter(x => x._id && !x._destroyed)
|
||||||
|
);
|
||||||
const { x: actorX, y: actorY } = this.document;
|
const { x: actorX, y: actorY } = this.document;
|
||||||
if (activeTokens.length > 0) {
|
if (activeTokens.length > 0) {
|
||||||
for (let token of activeTokens) {
|
for (let token of activeTokens) {
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,7 @@ export default class DhCharacterLevelUp extends LevelUpBase {
|
||||||
if (multiclasses?.[0]) {
|
if (multiclasses?.[0]) {
|
||||||
const data = multiclasses[0];
|
const data = multiclasses[0];
|
||||||
const multiclass = data.data.length > 0 ? await foundry.utils.fromUuid(data.data[0]) : {};
|
const multiclass = data.data.length > 0 ? await foundry.utils.fromUuid(data.data[0]) : {};
|
||||||
|
const subclasses = (await multiclass?.system?.fetchSubclasses()) ?? [];
|
||||||
|
|
||||||
context.multiclass = {
|
context.multiclass = {
|
||||||
...data,
|
...data,
|
||||||
|
|
@ -173,13 +174,12 @@ export default class DhCharacterLevelUp extends LevelUpBase {
|
||||||
alreadySelected
|
alreadySelected
|
||||||
};
|
};
|
||||||
}) ?? [],
|
}) ?? [],
|
||||||
subclasses:
|
subclasses: subclasses.map(subclass => ({
|
||||||
multiclass?.system?.subclasses.map(subclass => ({
|
|
||||||
...subclass,
|
...subclass,
|
||||||
uuid: subclass.uuid,
|
uuid: subclass.uuid,
|
||||||
selected: data.secondaryData.subclass === subclass.uuid,
|
selected: data.secondaryData.subclass === subclass.uuid,
|
||||||
disabled: data.secondaryData.subclass && data.secondaryData.subclass !== subclass.uuid
|
disabled: data.secondaryData.subclass && data.secondaryData.subclass !== subclass.uuid
|
||||||
})) ?? [],
|
})),
|
||||||
compendium: 'classes',
|
compendium: 'classes',
|
||||||
limit: 1
|
limit: 1
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -358,14 +358,14 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
|
||||||
const experienceIncreaseTagify = htmlElement.querySelector('.levelup-experience-increases');
|
const experienceIncreaseTagify = htmlElement.querySelector('.levelup-experience-increases');
|
||||||
if (experienceIncreaseTagify) {
|
if (experienceIncreaseTagify) {
|
||||||
const allExperiences = {
|
const allExperiences = {
|
||||||
...this.actor.system.experiences,
|
|
||||||
...Object.values(this.levelup.levels).reduce((acc, level) => {
|
...Object.values(this.levelup.levels).reduce((acc, level) => {
|
||||||
for (const key of Object.keys(level.achievements.experiences)) {
|
for (const key of Object.keys(level.achievements.experiences)) {
|
||||||
acc[key] = level.achievements.experiences[key];
|
acc[key] = level.achievements.experiences[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, {})
|
}, {}),
|
||||||
|
...this.actor.system.experiences
|
||||||
};
|
};
|
||||||
tagifyElement(
|
tagifyElement(
|
||||||
experienceIncreaseTagify,
|
experienceIncreaseTagify,
|
||||||
|
|
|
||||||
|
|
@ -120,12 +120,6 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
|
||||||
foundry.utils.fromUuidSync(x)
|
foundry.utils.fromUuidSync(x)
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const key of Object.keys(this.document._source.flags.daggerheart?.sceneEnvironments ?? {})) {
|
|
||||||
if (!submitData.flags.daggerheart.sceneEnvironments[key]) {
|
|
||||||
submitData.flags.daggerheart.sceneEnvironments[key] = _del;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
super._processSubmitData(event, form, submitData, options);
|
super._processSubmitData(event, form, submitData, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { DhHomebrew } from '../../data/settings/_module.mjs';
|
import { DhHomebrew } from '../../data/settings/_module.mjs';
|
||||||
import { Resource } from '../../data/settings/Homebrew.mjs';
|
import { Resource } from '../../data/settings/Homebrew.mjs';
|
||||||
import { slugify } from '../../helpers/utils.mjs';
|
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
||||||
|
|
@ -112,7 +111,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
|
|
||||||
switch (partId) {
|
switch (partId) {
|
||||||
case 'domains':
|
case 'domains':
|
||||||
const selectedDomain = this.selected.domain ? this.settings.domains[this.selected.domain] : null;
|
const selectedDomain = this.settings.domains[this.selected.domain] ?? null;
|
||||||
const enrichedDescription = selectedDomain
|
const enrichedDescription = selectedDomain
|
||||||
? await foundry.applications.ux.TextEditor.implementation.enrichHTML(selectedDomain.description)
|
? await foundry.applications.ux.TextEditor.implementation.enrichHTML(selectedDomain.description)
|
||||||
: null;
|
: null;
|
||||||
|
|
@ -403,12 +402,12 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
const domainName = button.form.elements.domainName.value;
|
const domainName = button.form.elements.domainName.value;
|
||||||
if (!domainName) return;
|
if (!domainName) return;
|
||||||
|
|
||||||
const newSlug = slugify(domainName);
|
const newSlug = domainName.slugify();
|
||||||
const existingDomains = [
|
const existingDomains = [
|
||||||
...Object.values(this.settings.domains),
|
...Object.values(this.settings.domains),
|
||||||
...Object.values(CONFIG.DH.DOMAIN.domains)
|
...Object.values(CONFIG.DH.DOMAIN.domains)
|
||||||
];
|
];
|
||||||
if (existingDomains.find(x => slugify(game.i18n.localize(x.label)) === newSlug)) {
|
if (existingDomains.find(x => x.id === newSlug)) {
|
||||||
ui.notifications.warn(game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.domains.duplicateDomain'));
|
ui.notifications.warn(game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.domains.duplicateDomain'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -529,7 +528,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
const identifier = button.form.elements.identifier.value;
|
const identifier = button.form.elements.identifier.value;
|
||||||
if (!identifier) return;
|
if (!identifier) return;
|
||||||
|
|
||||||
const sluggedIdentifier = slugify(identifier);
|
const sluggedIdentifier = identifier.slugify();
|
||||||
|
|
||||||
await this.settings.updateSource({
|
await this.settings.updateSource({
|
||||||
[`resources.${actorType}.resources.${sluggedIdentifier}`]: Resource.getDefaultResourceData(identifier)
|
[`resources.${actorType}.resources.${sluggedIdentifier}`]: Resource.getDefaultResourceData(identifier)
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
||||||
context.openSection = this.openSection;
|
context.openSection = this.openSection;
|
||||||
context.tabs = this._getTabs(this.constructor.TABS);
|
context.tabs = this._getTabs(this.constructor.TABS);
|
||||||
context.config = CONFIG.DH;
|
context.config = CONFIG.DH;
|
||||||
if (this.action.hasDamage) {
|
if (this.action.damage) {
|
||||||
context.allDamageTypesUsed = !getUnusedDamageTypes(this.action.damage.parts).length;
|
context.allDamageTypesUsed = !getUnusedDamageTypes(this.action.damage.parts).length;
|
||||||
|
|
||||||
if (this.action.damage.hasOwnProperty('includeBase') && this.action.type === 'attack')
|
if (this.action.damage.hasOwnProperty('includeBase') && this.action.type === 'attack')
|
||||||
|
|
@ -204,7 +204,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.action.parent.metadata?.isQuantifiable) {
|
if (this.action.parent.metadata?.isInventoryItem) {
|
||||||
options.quantity = {
|
options.quantity = {
|
||||||
label: 'DAGGERHEART.GENERAL.itemQuantity',
|
label: 'DAGGERHEART.GENERAL.itemQuantity',
|
||||||
group: 'Global'
|
group: 'Global'
|
||||||
|
|
@ -302,7 +302,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
||||||
static addDamage(_event) {
|
static addDamage(_event) {
|
||||||
if (!this.action.damage.parts) return;
|
if (!this.action.damage.parts) return;
|
||||||
|
|
||||||
const choices = getUnusedDamageTypes(this.action.damage.parts);
|
const choices = getUnusedDamageTypes(this.action._source.damage.parts);
|
||||||
const content = new foundry.data.fields.StringField({
|
const content = new foundry.data.fields.StringField({
|
||||||
label: game.i18n.localize('Damage Type'),
|
label: game.i18n.localize('Damage Type'),
|
||||||
choices,
|
choices,
|
||||||
|
|
|
||||||
|
|
@ -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 = [];
|
||||||
|
|
@ -175,6 +175,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
|
||||||
const partContext = await super._preparePartContext(partId, context);
|
const partContext = await super._preparePartContext(partId, context);
|
||||||
switch (partId) {
|
switch (partId) {
|
||||||
case 'details':
|
case 'details':
|
||||||
|
partContext.isItemEffect = partContext.isItemEffect || this.options.isSetting;
|
||||||
const useGeneric = game.settings.get(
|
const useGeneric = game.settings.get(
|
||||||
CONFIG.DH.id,
|
CONFIG.DH.id,
|
||||||
CONFIG.DH.SETTINGS.gameSettings.appearance
|
CONFIG.DH.SETTINGS.gameSettings.appearance
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,7 @@ export default class DHAdversarySettings extends DHBaseActorSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onDrop(event) {
|
async _onDrop(event) {
|
||||||
|
event.stopPropagation();
|
||||||
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
|
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
|
||||||
|
|
||||||
const item = await fromUuid(data.uuid);
|
const item = await fromUuid(data.uuid);
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,7 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onDrop(event) {
|
async _onDrop(event) {
|
||||||
|
event.stopPropagation();
|
||||||
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
|
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
|
||||||
const item = await fromUuid(data.uuid);
|
const item = await fromUuid(data.uuid);
|
||||||
if (data.fromInternal && item?.parent?.uuid === this.actor.uuid) return;
|
if (data.fromInternal && item?.parent?.uuid === this.actor.uuid) return;
|
||||||
|
|
@ -138,8 +139,4 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings {
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onDropItem(event, item) {
|
|
||||||
console.log(item);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -188,8 +188,9 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
|
||||||
if (type === 'effect') {
|
if (type === 'effect') {
|
||||||
const move = foundry.utils.getProperty(this.settings, this.movePath);
|
const move = foundry.utils.getProperty(this.settings, this.movePath);
|
||||||
for (const action of move.actions) {
|
for (const action of move.actions) {
|
||||||
const remainingEffects = action.effects.filter(x => x._id !== id);
|
const actionEffects = action.effects ?? [];
|
||||||
if (action.effects.length !== remainingEffects.length) {
|
const remainingEffects = actionEffects.filter(x => x._id !== id);
|
||||||
|
if (actionEffects.length !== remainingEffects.length) {
|
||||||
await action.update({
|
await action.update({
|
||||||
effects: remainingEffects.map(x => {
|
effects: remainingEffects.map(x => {
|
||||||
const { _id, ...rest } = x;
|
const { _id, ...rest } = x;
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,16 @@ export default class AdversarySheet extends DHBaseActorSheet {
|
||||||
dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]',
|
dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]',
|
||||||
dropSelector: null
|
dropSelector: null
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
contextMenus: [
|
||||||
|
{
|
||||||
|
handler: DHBaseActorSheet.getBaseAttackContextOptions,
|
||||||
|
selector: '[data-item-uuid][data-type="attack"]',
|
||||||
|
options: {
|
||||||
|
parentClassHooks: false,
|
||||||
|
fixed: true
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import DhDeathMove from '../../dialogs/deathMove.mjs';
|
||||||
import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs';
|
import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs';
|
||||||
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
|
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
|
||||||
import FilterMenu from '../../ux/filter-menu.mjs';
|
import FilterMenu from '../../ux/filter-menu.mjs';
|
||||||
import { getArmorSources, getDocFromElement, getDocFromElementSync } from '../../../helpers/utils.mjs';
|
import { getArmorSources, getDocFromElement, getDocFromElementSync, sortBy } from '../../../helpers/utils.mjs';
|
||||||
|
|
||||||
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
|
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
|
||||||
|
|
||||||
|
|
@ -57,6 +57,22 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
contextMenus: [
|
contextMenus: [
|
||||||
|
{
|
||||||
|
handler: CharacterSheet.#getCreationMainContextOptions,
|
||||||
|
selector: '.character-details [data-action="editDoc"]',
|
||||||
|
options: {
|
||||||
|
parentClassHooks: false,
|
||||||
|
fixed: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handler: DHBaseActorSheet.getBaseAttackContextOptions,
|
||||||
|
selector: '[data-item-uuid][data-type="attack"]',
|
||||||
|
options: {
|
||||||
|
parentClassHooks: false,
|
||||||
|
fixed: true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
handler: CharacterSheet.#getDomainCardContextOptions,
|
handler: CharacterSheet.#getDomainCardContextOptions,
|
||||||
selector: '[data-item-uuid][data-type="domainCard"]',
|
selector: '[data-item-uuid][data-type="domainCard"]',
|
||||||
|
|
@ -176,6 +192,9 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
for (const input of form.querySelectorAll('input:not([type=search]), .editor.prosemirror')) {
|
for (const input of form.querySelectorAll('input:not([type=search]), .editor.prosemirror')) {
|
||||||
input.disabled = disabled;
|
input.disabled = disabled;
|
||||||
}
|
}
|
||||||
|
for (const element of form.querySelectorAll('.input[contenteditable]')) {
|
||||||
|
element.classList.toggle('disabled', disabled);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @inheritDoc */
|
/** @inheritDoc */
|
||||||
|
|
@ -209,8 +228,9 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
context.attributes = Object.keys(this.document.system.traits).reduce((acc, key) => {
|
context.attributes = Object.keys(this.document.system.traits).reduce((acc, key) => {
|
||||||
acc[key] = {
|
acc[key] = {
|
||||||
...this.document.system.traits[key],
|
...this.document.system.traits[key],
|
||||||
name: game.i18n.localize(CONFIG.DH.ACTOR.abilities[key].name),
|
label: _loc(CONFIG.DH.ACTOR.abilities[key].label),
|
||||||
verbs: CONFIG.DH.ACTOR.abilities[key].verbs.map(x => game.i18n.localize(x))
|
verbs: CONFIG.DH.ACTOR.abilities[key].verbs.map(x => game.i18n.localize(x)),
|
||||||
|
isSpellcasting: this.document.system.spellcastModifierTrait?.key === key
|
||||||
};
|
};
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
|
|
@ -226,6 +246,11 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
context.resources.stress.emptyPips =
|
context.resources.stress.emptyPips =
|
||||||
context.resources.stress.max < maxResource ? maxResource - context.resources.stress.max : 0;
|
context.resources.stress.max < maxResource ? maxResource - context.resources.stress.max : 0;
|
||||||
|
|
||||||
|
context.equippedItems = sortBy(
|
||||||
|
this.document.items.filter(i => i.system.equipped && (i.type === 'weapon' || i.usable)),
|
||||||
|
i => (i.type === 'weapon' ? (i.system.secondary ? 1 : 0) : 2)
|
||||||
|
);
|
||||||
|
|
||||||
context.beastformActive = this.document.effects.find(x => x.type === 'beastform');
|
context.beastformActive = this.document.effects.find(x => x.type === 'beastform');
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
|
|
@ -313,6 +338,56 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
/* Context Menu */
|
/* Context Menu */
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
static #getCreationMainContextOptions() {
|
||||||
|
/** Returns true if the item is managed by the level up wizard. Such items shouldn't allow things like manual removal */
|
||||||
|
function isItemWizardManaged(item) {
|
||||||
|
const actor = item?.actor;
|
||||||
|
if (!actor) return false;
|
||||||
|
|
||||||
|
// If levelup automation is off in general or for this character, all items are unmanaged
|
||||||
|
// This is disabled until we have proper granted feature removal, for now this feature is to correct errors
|
||||||
|
// const levelupAuto = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto;
|
||||||
|
// if (!levelupAuto) return false;
|
||||||
|
|
||||||
|
// Core items aren't part of levelup data. TODO: add some way to flag a specific character as no auto leveling
|
||||||
|
const classPair = actor.system.class;
|
||||||
|
const coreItems = [actor.system.ancestry, actor.system.community, classPair?.value, classPair?.subclass];
|
||||||
|
if (coreItems.includes(item)) return true;
|
||||||
|
|
||||||
|
const levelups = Object.values(actor.system.levelData?.levelups) ?? [];
|
||||||
|
const uuid = item.uuid;
|
||||||
|
const sourceUuid = item._stats.compendiumSource; // on older characters this may be missing
|
||||||
|
return levelups.some(data => {
|
||||||
|
if (item.type === 'subclass') {
|
||||||
|
const selectedSubclasses = data.selections.map(s => s.secondaryData?.subclass).filter(s => !!s);
|
||||||
|
return sourceUuid
|
||||||
|
? selectedSubclasses.includes(sourceUuid)
|
||||||
|
: selectedSubclasses.length && item.system.isMulticlass;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchesCard = data.achievements.domainCards.some(i => i.itemUuid === uuid);
|
||||||
|
const matchesSelection = data.selections.some(s => s.itemUuid === uuid);
|
||||||
|
return matchesCard || matchesSelection;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'CONTROLS.CommonDelete',
|
||||||
|
icon: 'fa-solid fa-trash',
|
||||||
|
visible: target => {
|
||||||
|
const doc = getDocFromElementSync(target);
|
||||||
|
return doc?.isOwner && !isItemWizardManaged(doc);
|
||||||
|
},
|
||||||
|
onClick: async (event, target) => {
|
||||||
|
const doc = await getDocFromElement(target);
|
||||||
|
if (event.shiftKey) return doc.delete();
|
||||||
|
else return doc.deleteDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the set of ContextMenu options for DomainCards.
|
* Get the set of ContextMenu options for DomainCards.
|
||||||
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
|
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
|
||||||
|
|
@ -329,7 +404,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
const doc = getDocFromElementSync(target);
|
const doc = getDocFromElementSync(target);
|
||||||
return doc?.isOwner && doc.system.inVault;
|
return doc?.isOwner && doc.system.inVault;
|
||||||
},
|
},
|
||||||
callback: async target => {
|
onClick: async (_, target) => {
|
||||||
const doc = await getDocFromElement(target);
|
const doc = await getDocFromElement(target);
|
||||||
const actorLoadout = doc.actor.system.loadoutSlot;
|
const actorLoadout = doc.actor.system.loadoutSlot;
|
||||||
if (actorLoadout.available) return doc.update({ 'system.inVault': false });
|
if (actorLoadout.available) return doc.update({ 'system.inVault': false });
|
||||||
|
|
@ -343,7 +418,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
const doc = getDocFromElementSync(target);
|
const doc = getDocFromElementSync(target);
|
||||||
return doc?.isOwner && doc.system.inVault;
|
return doc?.isOwner && doc.system.inVault;
|
||||||
},
|
},
|
||||||
callback: async (target, event) => {
|
onClick: async (event, target) => {
|
||||||
const doc = await getDocFromElement(target);
|
const doc = await getDocFromElement(target);
|
||||||
const actorLoadout = doc.actor.system.loadoutSlot;
|
const actorLoadout = doc.actor.system.loadoutSlot;
|
||||||
if (!actorLoadout.available) {
|
if (!actorLoadout.available) {
|
||||||
|
|
@ -382,7 +457,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
const doc = getDocFromElementSync(target);
|
const doc = getDocFromElementSync(target);
|
||||||
return doc?.isOwner && !doc.system.inVault;
|
return doc?.isOwner && !doc.system.inVault;
|
||||||
},
|
},
|
||||||
callback: async target => (await getDocFromElement(target)).update({ 'system.inVault': true })
|
onClick: async (_, target) => (await getDocFromElement(target)).update({ 'system.inVault': true })
|
||||||
}
|
}
|
||||||
].map(option => ({
|
].map(option => ({
|
||||||
...option,
|
...option,
|
||||||
|
|
@ -408,7 +483,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
const doc = getDocFromElementSync(target);
|
const doc = getDocFromElementSync(target);
|
||||||
return doc.isOwner && doc && !doc.system.equipped;
|
return doc.isOwner && doc && !doc.system.equipped;
|
||||||
},
|
},
|
||||||
callback: (target, event) => CharacterSheet.#toggleEquipItem.call(this, event, target)
|
onClick: (event, target) => CharacterSheet.#toggleEquipItem.call(this, event, target)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'unequip',
|
label: 'unequip',
|
||||||
|
|
@ -417,7 +492,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
const doc = getDocFromElementSync(target);
|
const doc = getDocFromElementSync(target);
|
||||||
return doc.isOwner && doc && doc.system.equipped;
|
return doc.isOwner && doc && doc.system.equipped;
|
||||||
},
|
},
|
||||||
callback: (target, event) => CharacterSheet.#toggleEquipItem.call(this, event, target)
|
onClick: (event, target) => CharacterSheet.#toggleEquipItem.call(this, event, target)
|
||||||
}
|
}
|
||||||
].map(option => ({
|
].map(option => ({
|
||||||
...option,
|
...option,
|
||||||
|
|
@ -712,7 +787,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
? {
|
? {
|
||||||
'system.linkedClass.uuid': {
|
'system.linkedClass.uuid': {
|
||||||
key: 'system.linkedClass.uuid',
|
key: 'system.linkedClass.uuid',
|
||||||
value: this.document.system.class.value._stats.compendiumSource
|
value: this.document.system.class.value?._stats.compendiumSource
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|
@ -978,7 +1053,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
game.tooltip.activate(target, {
|
game.tooltip.activate(target, {
|
||||||
html,
|
html,
|
||||||
locked: true,
|
locked: true,
|
||||||
cssClass: 'bordered-tooltip',
|
cssClass: 'bordered-tooltip dh-style',
|
||||||
direction: 'DOWN'
|
direction: 'DOWN'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1074,7 +1149,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
game.tooltip.activate(target, {
|
game.tooltip.activate(target, {
|
||||||
html,
|
html,
|
||||||
locked: true,
|
locked: true,
|
||||||
cssClass: 'bordered-tooltip',
|
cssClass: 'bordered-tooltip dh-style',
|
||||||
direction: 'DOWN',
|
direction: 'DOWN',
|
||||||
noOffset: true
|
noOffset: true
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,17 @@ export default class DhCompanionSheet extends DHBaseActorSheet {
|
||||||
toggleStress: DhCompanionSheet.#toggleStress,
|
toggleStress: DhCompanionSheet.#toggleStress,
|
||||||
actionRoll: DhCompanionSheet.#actionRoll,
|
actionRoll: DhCompanionSheet.#actionRoll,
|
||||||
levelManagement: DhCompanionSheet.#levelManagement
|
levelManagement: DhCompanionSheet.#levelManagement
|
||||||
|
},
|
||||||
|
contextMenus: [
|
||||||
|
{
|
||||||
|
handler: DHBaseActorSheet.getBaseAttackContextOptions,
|
||||||
|
selector: '[data-item-uuid][data-type="attack"]',
|
||||||
|
options: {
|
||||||
|
parentClassHooks: false,
|
||||||
|
fixed: true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
static PARTS = {
|
static PARTS = {
|
||||||
|
|
|
||||||
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
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,7 +26,6 @@ export default class Party extends DHBaseActorSheet {
|
||||||
actions: {
|
actions: {
|
||||||
openDocument: Party.#openDocument,
|
openDocument: Party.#openDocument,
|
||||||
deletePartyMember: Party.#deletePartyMember,
|
deletePartyMember: Party.#deletePartyMember,
|
||||||
deleteItem: Party.#deleteItem,
|
|
||||||
toggleHope: Party.#toggleHope,
|
toggleHope: Party.#toggleHope,
|
||||||
toggleHitPoints: Party.#toggleHitPoints,
|
toggleHitPoints: Party.#toggleHitPoints,
|
||||||
toggleStress: Party.#toggleStress,
|
toggleStress: Party.#toggleStress,
|
||||||
|
|
@ -44,12 +43,10 @@ export default class Party extends DHBaseActorSheet {
|
||||||
static PARTS = {
|
static PARTS = {
|
||||||
header: { template: 'systems/daggerheart/templates/sheets/actors/party/header.hbs' },
|
header: { template: 'systems/daggerheart/templates/sheets/actors/party/header.hbs' },
|
||||||
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
|
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
|
||||||
partyMembers: { template: 'systems/daggerheart/templates/sheets/actors/party/party-members.hbs' },
|
partyMembers: {
|
||||||
/* NOT YET IMPLEMENTED */
|
template: 'systems/daggerheart/templates/sheets/actors/party/party-members.hbs',
|
||||||
// projects: {
|
scrollable: ['']
|
||||||
// template: 'systems/daggerheart/templates/sheets/actors/party/projects.hbs',
|
},
|
||||||
// scrollable: ['']
|
|
||||||
// },
|
|
||||||
inventory: {
|
inventory: {
|
||||||
template: 'systems/daggerheart/templates/sheets/actors/party/inventory.hbs',
|
template: 'systems/daggerheart/templates/sheets/actors/party/inventory.hbs',
|
||||||
scrollable: ['.tab.inventory .items-section']
|
scrollable: ['.tab.inventory .items-section']
|
||||||
|
|
@ -60,19 +57,13 @@ export default class Party extends DHBaseActorSheet {
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
static TABS = {
|
static TABS = {
|
||||||
primary: {
|
primary: {
|
||||||
tabs: [
|
tabs: [{ id: 'partyMembers' }, { id: 'inventory' }, { id: 'notes' }],
|
||||||
{ id: 'partyMembers' },
|
|
||||||
/* NOT YET IMPLEMENTED */
|
|
||||||
// { id: 'projects' },
|
|
||||||
{ id: 'inventory' },
|
|
||||||
{ id: 'notes' }
|
|
||||||
],
|
|
||||||
initial: 'partyMembers',
|
initial: 'partyMembers',
|
||||||
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
|
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
static ALLOWED_ACTOR_TYPES = ['character', 'companion', 'adversary'];
|
static ALLOWED_ACTOR_TYPES = ['character', 'companion', 'adversary', 'npc'];
|
||||||
static DICE_ROLL_ACTOR_TYPES = ['character'];
|
static DICE_ROLL_ACTOR_TYPES = ['character'];
|
||||||
|
|
||||||
async _onRender(context, options) {
|
async _onRender(context, options) {
|
||||||
|
|
@ -85,6 +76,14 @@ export default class Party extends DHBaseActorSheet {
|
||||||
/* Prepare Context */
|
/* Prepare Context */
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
async _prepareContext(options) {
|
||||||
|
const context = await super._prepareContext(options);
|
||||||
|
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming);
|
||||||
|
context.showStats =
|
||||||
|
settings.hidePartyStats === 'never' || (settings.hidePartyStats === 'players' && game.user.isGM);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
async _preparePartContext(partId, context, options) {
|
async _preparePartContext(partId, context, options) {
|
||||||
context = await super._preparePartContext(partId, context, options);
|
context = await super._preparePartContext(partId, context, options);
|
||||||
switch (partId) {
|
switch (partId) {
|
||||||
|
|
@ -498,23 +497,4 @@ export default class Party extends DHBaseActorSheet {
|
||||||
const newMembersList = currentMembers.filter(uuid => uuid !== doc.uuid);
|
const newMembersList = currentMembers.filter(uuid => uuid !== doc.uuid);
|
||||||
await this.document.update({ 'system.partyMembers': newMembersList });
|
await this.document.update({ 'system.partyMembers': newMembersList });
|
||||||
}
|
}
|
||||||
|
|
||||||
static async #deleteItem(event, target) {
|
|
||||||
const doc = await getDocFromElement(target.closest('.inventory-item'));
|
|
||||||
if (!event.shiftKey) {
|
|
||||||
const confirmed = await foundry.applications.api.DialogV2.confirm({
|
|
||||||
window: {
|
|
||||||
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
|
|
||||||
type: game.i18n.localize('TYPES.Actor.party'),
|
|
||||||
name: doc.name
|
|
||||||
})
|
|
||||||
},
|
|
||||||
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: doc.name })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!confirmed) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.document.deleteEmbeddedDocuments('Item', [doc.id]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ export default function DHApplicationMixin(Base) {
|
||||||
classes: ['daggerheart', 'sheet', 'dh-style'],
|
classes: ['daggerheart', 'sheet', 'dh-style'],
|
||||||
actions: {
|
actions: {
|
||||||
triggerContextMenu: DHSheetV2.#triggerContextMenu,
|
triggerContextMenu: DHSheetV2.#triggerContextMenu,
|
||||||
createDoc: DHSheetV2.#createDoc,
|
createDoc: DHSheetV2.#onCreateDoc,
|
||||||
editDoc: DHSheetV2.#editDoc,
|
editDoc: DHSheetV2.#editDoc,
|
||||||
deleteDoc: DHSheetV2.#deleteDoc,
|
deleteDoc: DHSheetV2.#deleteDoc,
|
||||||
toChat: DHSheetV2.#toChat,
|
toChat: DHSheetV2.#toChat,
|
||||||
|
|
@ -97,8 +97,8 @@ export default function DHApplicationMixin(Base) {
|
||||||
viewItem: DHSheetV2.#viewItem,
|
viewItem: DHSheetV2.#viewItem,
|
||||||
toggleEffect: DHSheetV2.#toggleEffect,
|
toggleEffect: DHSheetV2.#toggleEffect,
|
||||||
toggleExtended: DHSheetV2.#toggleExtended,
|
toggleExtended: DHSheetV2.#toggleExtended,
|
||||||
addNewItem: DHSheetV2.#addNewItem,
|
addNewItem: DHSheetV2.#onAddNewItem,
|
||||||
browseItem: DHSheetV2.#browseItem,
|
browseItem: DHSheetV2.#onBrowseItem,
|
||||||
editAttribution: DHSheetV2.#editAttribution,
|
editAttribution: DHSheetV2.#editAttribution,
|
||||||
configureLevelUpOptions: DHSheetV2.#configureLevelUpOptions
|
configureLevelUpOptions: DHSheetV2.#configureLevelUpOptions
|
||||||
},
|
},
|
||||||
|
|
@ -439,7 +439,7 @@ export default function DHApplicationMixin(Base) {
|
||||||
const target = element.closest('[data-item-uuid]');
|
const target = element.closest('[data-item-uuid]');
|
||||||
return !target.dataset.disabled && target.dataset.itemType !== 'beastform';
|
return !target.dataset.disabled && target.dataset.itemType !== 'beastform';
|
||||||
},
|
},
|
||||||
callback: async target => (await getDocFromElement(target)).update({ disabled: true })
|
onClick: async (_, target) => (await getDocFromElement(target)).update({ disabled: true })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'enableEffect',
|
label: 'enableEffect',
|
||||||
|
|
@ -448,7 +448,7 @@ export default function DHApplicationMixin(Base) {
|
||||||
const target = element.closest('[data-item-uuid]');
|
const target = element.closest('[data-item-uuid]');
|
||||||
return target.dataset.disabled && target.dataset.itemType !== 'beastform';
|
return target.dataset.disabled && target.dataset.itemType !== 'beastform';
|
||||||
},
|
},
|
||||||
callback: async target => (await getDocFromElement(target)).update({ disabled: false })
|
onClick: async (_, target) => (await getDocFromElement(target)).update({ disabled: false })
|
||||||
}
|
}
|
||||||
].map(option => ({
|
].map(option => ({
|
||||||
...option,
|
...option,
|
||||||
|
|
@ -493,7 +493,9 @@ export default function DHApplicationMixin(Base) {
|
||||||
(doc?.isOwner && (!doc?.hasOwnProperty('systemPath') || doc?.inCollection))
|
(doc?.isOwner && (!doc?.hasOwnProperty('systemPath') || doc?.inCollection))
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
callback: async target => (await getDocFromElement(target)).sheet.render({ force: true })
|
onClick: async (_, target) => {
|
||||||
|
return (await getDocFromElement(target)).sheet.render({ force: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -508,7 +510,7 @@ export default function DHApplicationMixin(Base) {
|
||||||
!foundry.utils.isEmpty(doc?.damage?.parts);
|
!foundry.utils.isEmpty(doc?.damage?.parts);
|
||||||
return doc?.isOwner && hasDamage;
|
return doc?.isOwner && hasDamage;
|
||||||
},
|
},
|
||||||
callback: async (target, event) => {
|
onClick: async (event, target) => {
|
||||||
const doc = await getDocFromElement(target),
|
const doc = await getDocFromElement(target),
|
||||||
action = doc?.system?.attack ?? doc;
|
action = doc?.system?.attack ?? doc;
|
||||||
const config = action.prepareConfig(event);
|
const config = action.prepareConfig(event);
|
||||||
|
|
@ -528,7 +530,7 @@ export default function DHApplicationMixin(Base) {
|
||||||
const doc = getDocFromElementSync(target);
|
const doc = getDocFromElementSync(target);
|
||||||
return doc?.isOwner && !(doc.type === 'domainCard' && doc.system.inVault);
|
return doc?.isOwner && !(doc.type === 'domainCard' && doc.system.inVault);
|
||||||
},
|
},
|
||||||
callback: async (target, event) => (await getDocFromElement(target)).use(event)
|
onClick: async (event, target) => (await getDocFromElement(target)).use(event)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -536,7 +538,7 @@ export default function DHApplicationMixin(Base) {
|
||||||
options.push({
|
options.push({
|
||||||
label: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat',
|
label: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat',
|
||||||
icon: 'fa-solid fa-message',
|
icon: 'fa-solid fa-message',
|
||||||
callback: async target => (await getDocFromElement(target)).toChat(this.document.uuid)
|
onClick: async (_, target) => (await getDocFromElement(target)).toChat(this.document.uuid)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (deletable)
|
if (deletable)
|
||||||
|
|
@ -546,9 +548,9 @@ export default function DHApplicationMixin(Base) {
|
||||||
visible: element => {
|
visible: element => {
|
||||||
const target = element.closest('[data-item-uuid]');
|
const target = element.closest('[data-item-uuid]');
|
||||||
const doc = getDocFromElementSync(target);
|
const doc = getDocFromElementSync(target);
|
||||||
return doc?.isOwner && target.dataset.itemType !== 'beastform';
|
return doc?.isOwner !== false && target.dataset.itemType !== 'beastform';
|
||||||
},
|
},
|
||||||
callback: async (target, event) => {
|
onClick: async (event, target) => {
|
||||||
const doc = await getDocFromElement(target);
|
const doc = await getDocFromElement(target);
|
||||||
if (event.shiftKey) return doc.delete();
|
if (event.shiftKey) return doc.delete();
|
||||||
else return doc.deleteDialog();
|
else return doc.deleteDialog();
|
||||||
|
|
@ -654,7 +656,7 @@ export default function DHApplicationMixin(Base) {
|
||||||
/* Application Clicks Actions */
|
/* Application Clicks Actions */
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
static async #addNewItem(event, target) {
|
static async #onAddNewItem(event, target) {
|
||||||
const createChoice = await foundry.applications.api.DialogV2.wait({
|
const createChoice = await foundry.applications.api.DialogV2.wait({
|
||||||
classes: ['dh-style', 'two-big-buttons'],
|
classes: ['dh-style', 'two-big-buttons'],
|
||||||
buttons: [
|
buttons: [
|
||||||
|
|
@ -673,11 +675,11 @@ export default function DHApplicationMixin(Base) {
|
||||||
|
|
||||||
if (!createChoice) return;
|
if (!createChoice) return;
|
||||||
|
|
||||||
if (createChoice === 'browse') return DHSheetV2.#browseItem.call(this, event, target);
|
if (createChoice === 'browse') return DHSheetV2.#onBrowseItem.call(this, event, target);
|
||||||
else return DHSheetV2.#createDoc.call(this, event, target);
|
else return DHSheetV2.#onCreateDoc.call(this, event, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async #browseItem(event, target) {
|
static async #onBrowseItem(_event, target) {
|
||||||
const type = target.dataset.compendium ?? target.dataset.type;
|
const type = target.dataset.compendium ?? target.dataset.type;
|
||||||
|
|
||||||
const presets = {
|
const presets = {
|
||||||
|
|
@ -732,7 +734,7 @@ export default function DHApplicationMixin(Base) {
|
||||||
* Create an embedded document.
|
* Create an embedded document.
|
||||||
* @type {ApplicationClickAction}
|
* @type {ApplicationClickAction}
|
||||||
*/
|
*/
|
||||||
static async #createDoc(event, target) {
|
static async #onCreateDoc(event, target) {
|
||||||
const { documentClass, type, inVault, disabled } = target.dataset;
|
const { documentClass, type, inVault, disabled } = target.dataset;
|
||||||
const parentIsItem = this.document.documentName === 'Item';
|
const parentIsItem = this.document.documentName === 'Item';
|
||||||
const featureOnCharacter = this.document.parent?.type === 'character' && type === 'feature';
|
const featureOnCharacter = this.document.parent?.type === 'character' && type === 'feature';
|
||||||
|
|
@ -760,11 +762,15 @@ export default function DHApplicationMixin(Base) {
|
||||||
type,
|
type,
|
||||||
system: systemData
|
system: systemData
|
||||||
};
|
};
|
||||||
|
|
||||||
if (inVault) data['system.inVault'] = true;
|
|
||||||
if (disabled) data.disabled = true;
|
if (disabled) data.disabled = true;
|
||||||
if (type === 'domainCard' && parent?.system.domains?.length) {
|
|
||||||
data.system.domain = parent.system.domains[0];
|
if (type === 'domainCard') {
|
||||||
|
if (parent?.system.domains?.length) data.system.domain = parent.system.domains[0];
|
||||||
|
if (inVault) data.system.inVault = true;
|
||||||
|
} else if (type === 'weapon') {
|
||||||
|
// Passing an empty system object to weapon causes validation failure due to attack action initialization
|
||||||
|
// todo: determine why, fix it at its source, then remove this fallback
|
||||||
|
delete data.system;
|
||||||
}
|
}
|
||||||
|
|
||||||
const doc = await cls.create(data, { parent, renderSheet: !event.shiftKey });
|
const doc = await cls.create(data, { parent, renderSheet: !event.shiftKey });
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,15 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Add support for input content editables */
|
||||||
|
_toggleDisabled(disabled) {
|
||||||
|
super._toggleDisabled(disabled);
|
||||||
|
const form = this.form;
|
||||||
|
for (const element of form.querySelectorAll('.input[contenteditable]')) {
|
||||||
|
element.classList.toggle('disabled', disabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
/* Context Menu */
|
/* Context Menu */
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
@ -180,6 +189,43 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
|
||||||
return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true });
|
return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the set of ContextMenu options for the base attack.
|
||||||
|
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
|
||||||
|
* @this {CharacterSheet}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
static getBaseAttackContextOptions() {
|
||||||
|
/**@type {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} */
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'DAGGERHEART.CONFIG.RollTypes.attack.name',
|
||||||
|
icon: 'fa-solid fa-burst',
|
||||||
|
onClick: async (event, target) => (await getDocFromElement(target)).use(event)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'DAGGERHEART.GENERAL.damage',
|
||||||
|
icon: 'fa-solid fa-explosion',
|
||||||
|
onClick: async (event, target) => {
|
||||||
|
const doc = await getDocFromElement(target),
|
||||||
|
action = doc?.system?.attack ?? doc;
|
||||||
|
const config = action.prepareConfig(event);
|
||||||
|
config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(
|
||||||
|
this.document,
|
||||||
|
doc
|
||||||
|
);
|
||||||
|
config.hasRoll = false;
|
||||||
|
return action && action.workflow.get('damage').execute(config, null, true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat',
|
||||||
|
icon: 'fa-solid fa-message',
|
||||||
|
onClick: async (_, target) => (await getDocFromElement(target)).toChat(this.document.uuid)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
/* Application Listener Actions */
|
/* Application Listener Actions */
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
|
||||||
options.push({
|
options.push({
|
||||||
name: 'CONTROLS.CommonDelete',
|
name: 'CONTROLS.CommonDelete',
|
||||||
icon: '<i class="fa-solid fa-trash"></i>',
|
icon: '<i class="fa-solid fa-trash"></i>',
|
||||||
callback: async target => {
|
onClick: async (_, target) => {
|
||||||
const feature = await getDocFromElement(target);
|
const feature = await getDocFromElement(target);
|
||||||
if (!feature) return;
|
if (!feature) return;
|
||||||
const confirmed = await foundry.applications.api.DialogV2.confirm({
|
const confirmed = await foundry.applications.api.DialogV2.confirm({
|
||||||
|
|
|
||||||
|
|
@ -104,9 +104,10 @@ export default class ClassSheet extends DHBaseItemSheet {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**@inheritdoc */
|
/**@inheritdoc */
|
||||||
async _prepareContext(_options) {
|
async _prepareContext(options) {
|
||||||
const context = await super._prepareContext(_options);
|
const context = await super._prepareContext(options);
|
||||||
context.domains = this.document.system.domains;
|
context.domains = this.document.system.domains;
|
||||||
|
context.subclasses = await this.document.system.fetchSubclasses();
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,20 +129,8 @@ export default class ClassSheet extends DHBaseItemSheet {
|
||||||
const item = await fromUuid(data.uuid);
|
const item = await fromUuid(data.uuid);
|
||||||
const itemType = data.type === 'ActiveEffect' ? data.type : item.type;
|
const itemType = data.type === 'ActiveEffect' ? data.type : item.type;
|
||||||
const target = event.target.closest('fieldset.drop-section');
|
const target = event.target.closest('fieldset.drop-section');
|
||||||
if (itemType === 'subclass') {
|
|
||||||
if (item.system.linkedClass) {
|
if (['feature', 'ActiveEffect'].includes(itemType)) {
|
||||||
return ui.notifications.warn(
|
|
||||||
game.i18n.format('DAGGERHEART.UI.Notifications.subclassAlreadyLinked', {
|
|
||||||
name: item.name,
|
|
||||||
class: this.document.name
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await item.update({ 'system.linkedClass': this.document.uuid });
|
|
||||||
await this.document.update({
|
|
||||||
'system.subclasses': [...this.document.system.subclasses.map(x => x.uuid), item.uuid]
|
|
||||||
});
|
|
||||||
} else if (['feature', 'ActiveEffect'].includes(itemType)) {
|
|
||||||
super._onDrop(event);
|
super._onDrop(event);
|
||||||
} else if (this.document.parent?.type !== 'character') {
|
} else if (this.document.parent?.type !== 'character') {
|
||||||
if (itemType === 'weapon') {
|
if (itemType === 'weapon') {
|
||||||
|
|
@ -200,12 +189,6 @@ export default class ClassSheet extends DHBaseItemSheet {
|
||||||
static async #removeItemFromCollection(_event, element) {
|
static async #removeItemFromCollection(_event, element) {
|
||||||
const { uuid, target } = element.dataset;
|
const { uuid, target } = element.dataset;
|
||||||
const prop = foundry.utils.getProperty(this.document.system, target);
|
const prop = foundry.utils.getProperty(this.document.system, target);
|
||||||
|
|
||||||
if (target === 'subclasses') {
|
|
||||||
const subclass = await foundry.utils.fromUuid(uuid);
|
|
||||||
await subclass?.update({ 'system.linkedClass': null });
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.document.update({ [`system.${target}`]: prop.filter(i => i && i.uuid !== uuid).map(x => x.uuid) });
|
await this.document.update({ [`system.${target}`]: prop.filter(i => i && i.uuid !== uuid).map(x => x.uuid) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,4 +40,36 @@ export default class SubclassSheet extends DHBaseItemSheet {
|
||||||
get relatedDocs() {
|
get relatedDocs() {
|
||||||
return this.document.system.features.map(x => x.item);
|
return this.document.system.features.map(x => x.item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _prepareContext(options) {
|
||||||
|
const context = await super._prepareContext(options);
|
||||||
|
if (this.document.system.linkedClass) {
|
||||||
|
const classData = await fromUuid(this.document.system.linkedClass);
|
||||||
|
context.class = classData ?? {
|
||||||
|
name: _loc('DAGGERHEART.GENERAL.missingX', { x: _loc('TYPES.Item.class') }),
|
||||||
|
missing: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _onDrop(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
const data = TextEditor.getDragEventData(event);
|
||||||
|
const item = await fromUuid(data.uuid);
|
||||||
|
const itemType = data.type === 'ActiveEffect' ? data.type : item.type;
|
||||||
|
if (itemType === 'class') {
|
||||||
|
const uuid = item._stats.compendiumSource ?? item.uuid;
|
||||||
|
if (this.document.system.linkedClass !== uuid) {
|
||||||
|
await this.document.update({ 'system.linkedClass': uuid });
|
||||||
|
// Re-render all class sheets for instant feedback
|
||||||
|
for (const app of foundry.applications.instances.values()) {
|
||||||
|
if (app.document?.type === 'class') app.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return super._onDrop(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,9 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
|
||||||
async _prepareTrackerContext(context, options) {
|
async _prepareTrackerContext(context, options) {
|
||||||
await super._prepareTrackerContext(context, options);
|
await super._prepareTrackerContext(context, options);
|
||||||
|
|
||||||
const adversaries = context.turns?.filter(x => x.isNPC) ?? [];
|
const npcs = context.turns?.filter(x => x.isNPC) ?? [];
|
||||||
|
const adversaries = npcs.filter(x => x.disposition !== CONST.TOKEN_DISPOSITIONS.FRIENDLY);
|
||||||
|
const friendlies = npcs.filter(x => x.disposition === CONST.TOKEN_DISPOSITIONS.FRIENDLY);
|
||||||
const characters = context.turns?.filter(x => !x.isNPC) ?? [];
|
const characters = context.turns?.filter(x => !x.isNPC) ?? [];
|
||||||
const spotlightQueueEnabled = game.settings.get(
|
const spotlightQueueEnabled = game.settings.get(
|
||||||
CONFIG.DH.id,
|
CONFIG.DH.id,
|
||||||
|
|
@ -75,25 +77,56 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
|
||||||
Object.assign(context, {
|
Object.assign(context, {
|
||||||
actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens,
|
actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens,
|
||||||
adversaries,
|
adversaries,
|
||||||
|
friendlies,
|
||||||
allCharacters: characters,
|
allCharacters: characters,
|
||||||
characters: characters.filter(x => !spotlightQueueEnabled || x.system.spotlight.requestOrderIndex == 0),
|
characters: characters.filter(x => !spotlightQueueEnabled || x.system.spotlight.requestOrderIndex == 0),
|
||||||
spotlightRequests
|
spotlightRequests
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the dialog used to edit the name of the currently viewed Combat encounter.
|
||||||
|
* @this {CombatTracker}
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async #onEditName() {
|
||||||
|
const combat = this.viewed;
|
||||||
|
if (!combat || !game.user.isGM) return null;
|
||||||
|
const field = combat.schema.fields.name;
|
||||||
|
const inputHTML = field.toFormGroup({}, { name: 'name', value: combat.name, autofocus: true }).outerHTML;
|
||||||
|
const formData = await foundry.applications.api.DialogV2.input({
|
||||||
|
window: { icon: 'fa-solid fa-tag', title: 'COMBAT.ACTIONS.EditNameTitle' },
|
||||||
|
position: { width: 480 },
|
||||||
|
content: inputHTML
|
||||||
|
});
|
||||||
|
await combat.update({ name: formData.name || '' });
|
||||||
|
}
|
||||||
|
|
||||||
_getCombatContextOptions() {
|
_getCombatContextOptions() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'COMBAT.ClearMovementHistories',
|
label: 'COMBAT.ACTIONS.EditName',
|
||||||
icon: '<i class="fa-solid fa-shoe-prints"></i>',
|
icon: 'fa-solid fa-tag',
|
||||||
visible: () => game.user.isGM && this.viewed?.combatants.size > 0,
|
visible: () => game.user.isGM && !!this.viewed,
|
||||||
callback: () => this.viewed.clearMovementHistories()
|
onClick: () => DhCombatTracker.#onEditName.call(this)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'COMBAT.Delete',
|
label: 'COMBAT.ACTIONS.LinkToScene',
|
||||||
icon: '<i class="fa-solid fa-trash"></i>',
|
icon: '<i class="fa-solid fa-link"></i>',
|
||||||
|
visible: () => game.user.isGM && !this.scene,
|
||||||
|
onClick: () => this.viewed.toggleSceneLink()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'COMBAT.ACTIONS.UnlinkFromScene',
|
||||||
|
icon: '<i class="fa-solid fa-unlink"></i>',
|
||||||
|
visible: () => game.user.isGM && !!this.scene,
|
||||||
|
onClick: () => this.viewed.toggleSceneLink()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'COMBAT.End',
|
||||||
|
icon: 'fa-solid fa-xmark',
|
||||||
visible: () => game.user.isGM && !!this.viewed,
|
visible: () => game.user.isGM && !!this.viewed,
|
||||||
callback: () => this.viewed.endCombat()
|
onClick: () => this.viewed.endCombat()
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -129,7 +162,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
|
||||||
active: index === combat.turn,
|
active: index === combat.turn,
|
||||||
canPing: combatant.sceneId === canvas.scene?.id && game.user.hasPermission('PING_CANVAS'),
|
canPing: combatant.sceneId === canvas.scene?.id && game.user.hasPermission('PING_CANVAS'),
|
||||||
type: combatant.actor?.system?.type,
|
type: combatant.actor?.system?.type,
|
||||||
img: await this._getCombatantThumbnail(combatant)
|
img: await this._getCombatantThumbnail(combatant),
|
||||||
|
disposition: combatant.token?.disposition
|
||||||
};
|
};
|
||||||
|
|
||||||
turn.css = [turn.active ? 'active' : null, hidden ? 'hide' : null, isDefeated ? 'defeated' : null].filterJoin(
|
turn.css = [turn.active ? 'active' : null, hidden ? 'hide' : null, isDefeated ? 'defeated' : null].filterJoin(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { DhCountdown } from '../../data/countdowns.mjs';
|
import { DhCountdown } from '../../data/countdowns.mjs';
|
||||||
import { waitForDiceSoNice } from '../../helpers/utils.mjs';
|
import { waitForDiceSoNice } from '../../helpers/utils.mjs';
|
||||||
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
|
import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
||||||
|
|
@ -114,7 +114,7 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.data.updateSource(update);
|
await this.data.updateSource(update);
|
||||||
await emitAsGM(GMUpdateEvent.UpdateCountdowns, this.gmSetSetting.bind(this.data), this.data, null, {
|
await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, this.gmSetSetting.bind(this.data), this.data, null, {
|
||||||
refreshType: RefreshType.Countdown
|
refreshType: RefreshType.Countdown
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { waitForDiceSoNice } from '../../helpers/utils.mjs';
|
import { waitForDiceSoNice } from '../../helpers/utils.mjs';
|
||||||
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
|
import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
||||||
|
|
@ -21,19 +21,19 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
|
||||||
static DEFAULT_OPTIONS = {
|
static DEFAULT_OPTIONS = {
|
||||||
id: 'countdowns',
|
id: 'countdowns',
|
||||||
tag: 'div',
|
tag: 'div',
|
||||||
classes: ['daggerheart', 'dh-style', 'countdowns', 'faded-ui'],
|
classes: ['daggerheart', 'dh-style', 'countdowns'],
|
||||||
window: {
|
window: {
|
||||||
icon: 'fa-solid fa-clock-rotate-left',
|
icon: 'fa-solid fa-clock-rotate-left',
|
||||||
frame: true,
|
frame: false,
|
||||||
title: 'DAGGERHEART.UI.Countdowns.title',
|
title: 'DAGGERHEART.UI.Countdowns.title',
|
||||||
positioned: false,
|
positioned: false,
|
||||||
resizable: false,
|
resizable: false,
|
||||||
minimizable: false
|
minimizable: false
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
toggleViewMode: DhCountdowns.#toggleViewMode,
|
toggleViewMode: DhCountdowns.#onToggleViewMode,
|
||||||
editCountdowns: DhCountdowns.#editCountdowns,
|
editCountdowns: DhCountdowns.#onEditCountdowns,
|
||||||
loopCountdown: DhCountdowns.#loopCountdown,
|
loopCountdown: DhCountdowns.#onLoopCountdown,
|
||||||
decreaseCountdown: (_, target) => this.editCountdown(false, target),
|
decreaseCountdown: (_, target) => this.editCountdown(false, target),
|
||||||
increaseCountdown: (_, target) => this.editCountdown(true, target)
|
increaseCountdown: (_, target) => this.editCountdown(true, target)
|
||||||
},
|
},
|
||||||
|
|
@ -62,20 +62,6 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
|
||||||
if (iconOnly) frame.classList.add('icon-only');
|
if (iconOnly) frame.classList.add('icon-only');
|
||||||
else frame.classList.remove('icon-only');
|
else frame.classList.remove('icon-only');
|
||||||
|
|
||||||
const header = frame.querySelector('.window-header');
|
|
||||||
header.querySelector('button[data-action="close"]').remove();
|
|
||||||
header.querySelector('button[data-action="toggleControls"]').remove();
|
|
||||||
|
|
||||||
if (game.user.isGM) {
|
|
||||||
const editTooltip = game.i18n.localize('DAGGERHEART.APPLICATIONS.CountdownEdit.editTitle');
|
|
||||||
const editButton = `<a style="margin-right: 8px;" class="header-control" data-tooltip="${editTooltip}" aria-label="${editTooltip}" data-action="editCountdowns"><i class="fa-solid fa-wrench"></i></a>`;
|
|
||||||
header.insertAdjacentHTML('beforeEnd', editButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
const minimizeTooltip = game.i18n.localize('DAGGERHEART.UI.Countdowns.toggleIconMode');
|
|
||||||
const minimizeButton = `<a class="header-control" data-tooltip="${minimizeTooltip}" aria-label="${minimizeTooltip}" data-action="toggleViewMode"><i class="fa-solid fa-down-left-and-up-right-to-center"></i></a>`;
|
|
||||||
header.insertAdjacentHTML('beforeEnd', minimizeButton);
|
|
||||||
|
|
||||||
return frame;
|
return frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,7 +147,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async #toggleViewMode() {
|
static async #onToggleViewMode() {
|
||||||
const currentMode = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode);
|
const currentMode = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode);
|
||||||
const appMode = CONFIG.DH.GENERAL.countdownAppMode;
|
const appMode = CONFIG.DH.GENERAL.countdownAppMode;
|
||||||
const newMode = currentMode === appMode.textIcon ? appMode.iconOnly : appMode.textIcon;
|
const newMode = currentMode === appMode.textIcon ? appMode.iconOnly : appMode.textIcon;
|
||||||
|
|
@ -172,15 +158,16 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
static async #editCountdowns() {
|
static async #onEditCountdowns() {
|
||||||
new game.system.api.applications.ui.CountdownEdit().render(true);
|
new game.system.api.applications.ui.CountdownEdit().render(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async #loopCountdown(_, target) {
|
static async #onLoopCountdown(_, target) {
|
||||||
if (!DhCountdowns.canPerformEdit()) return;
|
if (!DhCountdowns.canPerformEdit()) return;
|
||||||
|
|
||||||
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
|
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
|
||||||
const countdown = settings.countdowns[target.id];
|
const countdownId = target.closest('[data-countdown]').dataset.countdown;
|
||||||
|
const countdown = settings.countdowns[countdownId];
|
||||||
|
|
||||||
let progressMax = countdown.progress.start;
|
let progressMax = countdown.progress.start;
|
||||||
let message = null;
|
let message = null;
|
||||||
|
|
@ -199,12 +186,12 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
|
||||||
|
|
||||||
await waitForDiceSoNice(message);
|
await waitForDiceSoNice(message);
|
||||||
await settings.updateSource({
|
await settings.updateSource({
|
||||||
[`countdowns.${target.id}.progress`]: {
|
[`countdowns.${countdownId}.progress`]: {
|
||||||
current: newMax,
|
current: newMax,
|
||||||
start: newMax
|
start: newMax
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
|
await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
|
||||||
refreshType: RefreshType.Countdown
|
refreshType: RefreshType.Countdown
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -213,12 +200,13 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
|
||||||
if (!DhCountdowns.canPerformEdit()) return;
|
if (!DhCountdowns.canPerformEdit()) return;
|
||||||
|
|
||||||
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
|
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
|
||||||
const countdown = settings.countdowns[target.id];
|
const countdownId = target.closest('[data-countdown]').dataset.countdown;
|
||||||
|
const countdown = settings.countdowns[countdownId];
|
||||||
const newCurrent = increase
|
const newCurrent = increase
|
||||||
? Math.min(countdown.progress.current + 1, countdown.progress.start)
|
? Math.min(countdown.progress.current + 1, countdown.progress.start)
|
||||||
: Math.max(countdown.progress.current - 1, 0);
|
: Math.max(countdown.progress.current - 1, 0);
|
||||||
await settings.updateSource({ [`countdowns.${target.id}.progress.current`]: newCurrent });
|
await settings.updateSource({ [`countdowns.${countdownId}.progress.current`]: newCurrent });
|
||||||
await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
|
await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
|
||||||
refreshType: RefreshType.Countdown
|
refreshType: RefreshType.Countdown
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -277,7 +265,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
|
||||||
return acc;
|
return acc;
|
||||||
}, {})
|
}, {})
|
||||||
};
|
};
|
||||||
await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
|
await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
|
||||||
refreshType: RefreshType.Countdown
|
refreshType: RefreshType.Countdown
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
|
import { emitGMUpdate, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
||||||
|
|
@ -104,7 +104,7 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateFear(value) {
|
async updateFear(value) {
|
||||||
return emitAsGM(
|
return emitGMUpdate(
|
||||||
GMUpdateEvent.UpdateFear,
|
GMUpdateEvent.UpdateFear,
|
||||||
game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
|
game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
|
||||||
value
|
value
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { getDocFromElement } from '../../helpers/utils.mjs';
|
||||||
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
|
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
@ -47,7 +48,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
expandContent: this.expandContent,
|
expandContent: this.expandContent,
|
||||||
resetFilters: this.resetFilters,
|
resetFilters: this.resetFilters,
|
||||||
sortList: this.sortList,
|
sortList: this.sortList,
|
||||||
openSettings: this.openSettings
|
openSettings: this.openSettings,
|
||||||
|
viewSheet: this.#onViewSheet
|
||||||
},
|
},
|
||||||
position: {
|
position: {
|
||||||
left: 100,
|
left: 100,
|
||||||
|
|
@ -109,8 +111,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
CONFIG.DH.id,
|
CONFIG.DH.id,
|
||||||
CONFIG.DH.FLAGS[`${this.compendiumBrowserTypeKey}`].position
|
CONFIG.DH.FLAGS[`${this.compendiumBrowserTypeKey}`].position
|
||||||
);
|
);
|
||||||
|
|
||||||
options.position = userPresetPosition ?? ItemBrowser.DEFAULT_OPTIONS.position;
|
options.position = userPresetPosition ?? ItemBrowser.DEFAULT_OPTIONS.position;
|
||||||
|
delete options.position.zIndex;
|
||||||
|
|
||||||
if (!userPresetPosition) {
|
if (!userPresetPosition) {
|
||||||
const width = noFolder === true || lite === true ? 600 : 850;
|
const width = noFolder === true || lite === true ? 600 : 850;
|
||||||
|
|
@ -277,7 +279,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
(await foundry.applications.ux.TextEditor.implementation.enrichHTML(item.description));
|
(await foundry.applications.ux.TextEditor.implementation.enrichHTML(item.description));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fieldFilter = this._createFieldFilter();
|
this.fieldFilter = await this._createFieldFilter();
|
||||||
|
|
||||||
if (this.presets?.filter) {
|
if (this.presets?.filter) {
|
||||||
Object.entries(this.presets.filter).forEach(([k, v]) => {
|
Object.entries(this.presets.filter).forEach(([k, v]) => {
|
||||||
|
|
@ -306,7 +308,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
{
|
{
|
||||||
items: this.items,
|
items: this.items,
|
||||||
menu: this.selectedMenu,
|
menu: this.selectedMenu,
|
||||||
formatLabel: this.formatLabel
|
formatLabel: this.formatLabel,
|
||||||
|
viewSheet: this.items[0] instanceof Actor
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -355,12 +358,12 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_createFieldFilter() {
|
async _createFieldFilter() {
|
||||||
const filters = ItemBrowser.getFolderConfig(this.selectedMenu.data, 'filters');
|
const filters = ItemBrowser.getFolderConfig(this.selectedMenu.data, 'filters');
|
||||||
filters.forEach(f => {
|
for (const f of filters) {
|
||||||
if (typeof f.field === 'string') f.field = foundry.utils.getProperty(game, f.field);
|
if (typeof f.field === 'string') f.field = foundry.utils.getProperty(game, f.field);
|
||||||
else if (typeof f.choices === 'function') {
|
else if (typeof f.choices === 'function') {
|
||||||
f.choices = f.choices(this.items);
|
f.choices = await f.choices(this.items);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear field label so template uses our custom label parameter
|
// Clear field label so template uses our custom label parameter
|
||||||
|
|
@ -370,7 +373,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
|
|
||||||
f.name ??= f.key;
|
f.name ??= f.key;
|
||||||
f.value = this.presets?.filter?.[f.name]?.value ?? null;
|
f.value = this.presets?.filter?.[f.name]?.value ?? null;
|
||||||
});
|
}
|
||||||
|
|
||||||
return filters;
|
return filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -567,6 +571,11 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async #onViewSheet(_, target) {
|
||||||
|
const document = await getDocFromElement(target);
|
||||||
|
document?.sheet?.render(true);
|
||||||
|
}
|
||||||
|
|
||||||
_createDragProcess() {
|
_createDragProcess() {
|
||||||
new foundry.applications.ux.DragDrop.implementation({
|
new foundry.applications.ux.DragDrop.implementation({
|
||||||
dragSelector: '.item-container',
|
dragSelector: '.item-container',
|
||||||
|
|
@ -605,7 +614,16 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
items: {
|
items: {
|
||||||
folder: 'equipments',
|
folder: 'equipments',
|
||||||
render: {
|
render: {
|
||||||
noFolder: true
|
folders: [
|
||||||
|
'equipments',
|
||||||
|
'ancestries',
|
||||||
|
'classes',
|
||||||
|
'subclasses',
|
||||||
|
'domains',
|
||||||
|
'communities',
|
||||||
|
'beastforms'
|
||||||
|
// excluded: features
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
compendium: {}
|
compendium: {}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
|
import { emitGMUpdate, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
|
||||||
|
|
||||||
export default class DhSceneNavigation extends foundry.applications.ui.SceneNavigation {
|
export default class DhSceneNavigation extends foundry.applications.ui.SceneNavigation {
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
|
|
@ -68,7 +68,7 @@ export default class DhSceneNavigation extends foundry.applications.ui.SceneNavi
|
||||||
1
|
1
|
||||||
)[0];
|
)[0];
|
||||||
newEnvironments.unshift(newFirst);
|
newEnvironments.unshift(newFirst);
|
||||||
emitAsGM(
|
emitGMUpdate(
|
||||||
GMUpdateEvent.UpdateDocument,
|
GMUpdateEvent.UpdateDocument,
|
||||||
scene.update.bind(scene),
|
scene.update.bind(scene),
|
||||||
{ 'flags.daggerheart.sceneEnvironments': newEnvironments },
|
{ 'flags.daggerheart.sceneEnvironments': newEnvironments },
|
||||||
|
|
|
||||||
|
|
@ -57,14 +57,14 @@ export default class DhRegionLayer extends foundry.canvas.layers.RegionLayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
async placeRegion(data, options = {}) {
|
async placeRegion(data, options = {}) {
|
||||||
const preConfirm = ({ _event, document, _create, _options }) => {
|
const preConfirm = data => {
|
||||||
const shape = document.shapes[0];
|
const shape = data.document.shapes[0];
|
||||||
const isEmanation = shape.type === 'emanation';
|
const isEmanation = shape.type === 'emanation';
|
||||||
if (isEmanation) {
|
if (isEmanation) {
|
||||||
const token = this.#findTokenInBounds(shape.base.origin);
|
const token = this.#findTokenInBounds(shape.base.origin);
|
||||||
if (!token) return options.preConfirm?.() ?? true;
|
if (!token) return options.preConfirm?.(data) ?? true;
|
||||||
const shapeData = shape.toObject();
|
const shapeData = shape.toObject();
|
||||||
document.updateSource({
|
data.document.updateSource({
|
||||||
shapes: [
|
shapes: [
|
||||||
{
|
{
|
||||||
...shapeData,
|
...shapeData,
|
||||||
|
|
@ -80,10 +80,10 @@ export default class DhRegionLayer extends foundry.canvas.layers.RegionLayer {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return options?.preConfirm?.() ?? true;
|
return options?.preConfirm?.(data) ?? true;
|
||||||
};
|
};
|
||||||
|
|
||||||
super.placeRegion(data, { ...options, preConfirm });
|
return await super.placeRegion(data, { ...options, preConfirm });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Searches for token at origin point, returning null if there are no tokens or multiple overlapping tokens */
|
/** Searches for token at origin point, returning null if there are no tokens or multiple overlapping tokens */
|
||||||
|
|
|
||||||
|
|
@ -249,9 +249,6 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
|
||||||
|
|
||||||
/** @inheritDoc */
|
/** @inheritDoc */
|
||||||
_drawBar(number, bar, data) {
|
_drawBar(number, bar, data) {
|
||||||
const val = Number(data.value);
|
|
||||||
const pct = Math.clamp(val, 0, data.max) / data.max;
|
|
||||||
|
|
||||||
// Determine sizing
|
// Determine sizing
|
||||||
const { width, height } = this.document.getSize();
|
const { width, height } = this.document.getSize();
|
||||||
const s = canvas.dimensions.uiScale;
|
const s = canvas.dimensions.uiScale;
|
||||||
|
|
@ -259,17 +256,19 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
|
||||||
const bh = 8 * (this.document.height >= 2 ? 1.5 : 1) * s;
|
const bh = 8 * (this.document.height >= 2 ? 1.5 : 1) * s;
|
||||||
|
|
||||||
// Determine the color to use
|
// Determine the color to use
|
||||||
const fillColor =
|
const Color = foundry.utils.Color;
|
||||||
number === 0 ? foundry.utils.Color.fromRGB([1, 0, 0]) : foundry.utils.Color.fromString('#0032b1');
|
const fillColor = number === 0 ? Color.fromRGB([1, 0, 0]) : Color.fromString('#0032b1');
|
||||||
|
const emptyColor = Color.fromRGB([0, 0, 0]);
|
||||||
|
|
||||||
// Draw the bar
|
// Draw the bar (accounting floating point numbers from bar animations)
|
||||||
const widthUnit = bw / data.max;
|
const widthUnit = bw / Math.ceil(data.max);
|
||||||
bar.clear().lineStyle(s, 0x000000, 1.0);
|
bar.clear().lineStyle(s, 0x000000, 1.0);
|
||||||
const sections = [...Array(data.max).keys()];
|
const sections = [...Array(Math.ceil(data.max)).keys()];
|
||||||
for (let mark of sections) {
|
for (const mark of sections) {
|
||||||
const x = mark * widthUnit;
|
const x = mark * widthUnit;
|
||||||
const marked = mark + 1 <= data.value;
|
const marked = mark < Math.ceil(data.value);
|
||||||
const color = marked ? fillColor : foundry.utils.Color.fromRGB([0, 0, 0]);
|
const remainder = mark === Math.ceil(data.value) - 1 ? data.value % 1 : 0;
|
||||||
|
const color = !marked ? emptyColor : remainder ? emptyColor.mix(fillColor, remainder) : fillColor;
|
||||||
if (mark === 0 || mark === sections.length - 1) {
|
if (mark === 0 || mark === sections.length - 1) {
|
||||||
bar.beginFill(color, marked ? 1.0 : 0.5).drawRect(x, 0, widthUnit, bh, 2 * s); // Would like drawRoundedRect, but it's very troublsome with the corners. Leaving for now.
|
bar.beginFill(color, marked ? 1.0 : 0.5).drawRect(x, 0, widthUnit, bh, 2 * s); // Would like drawRoundedRect, but it's very troublsome with the corners. Leaving for now.
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,49 @@ export const typeConfig = {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
environments: {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: 'system.tier',
|
||||||
|
label: 'DAGGERHEART.GENERAL.Tiers.singular'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'system.type',
|
||||||
|
label: 'DAGGERHEART.GENERAL.type',
|
||||||
|
format: type => {
|
||||||
|
if (!type) return '-';
|
||||||
|
|
||||||
|
return CONFIG.DH.ACTOR.environmentTypes[type].label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
key: 'system.tier',
|
||||||
|
label: 'DAGGERHEART.GENERAL.Tiers.singular',
|
||||||
|
field: 'system.api.models.actors.DhEnvironment.schema.fields.tier'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'system.type',
|
||||||
|
label: 'DAGGERHEART.GENERAL.type',
|
||||||
|
field: 'system.api.models.actors.DhEnvironment.schema.fields.type'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'system.difficulty',
|
||||||
|
name: 'difficulty.min',
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.difficultyMin',
|
||||||
|
field: 'system.api.models.actors.DhEnvironment.schema.fields.difficulty',
|
||||||
|
operator: 'gte'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'system.difficulty',
|
||||||
|
name: 'difficulty.max',
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.difficultyMax',
|
||||||
|
field: 'system.api.models.actors.DhEnvironment.schema.fields.difficulty',
|
||||||
|
operator: 'lte'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
items: {
|
items: {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
|
|
@ -177,8 +220,8 @@ export const typeConfig = {
|
||||||
key: 'system.secondary',
|
key: 'system.secondary',
|
||||||
label: 'DAGGERHEART.UI.ItemBrowser.subtype',
|
label: 'DAGGERHEART.UI.ItemBrowser.subtype',
|
||||||
choices: [
|
choices: [
|
||||||
{ value: false, label: 'DAGGERHEART.ITEMS.Weapon.primaryWeapon' },
|
{ value: false, label: 'DAGGERHEART.ITEMS.Weapon.primaryWeapon.full' },
|
||||||
{ value: true, label: 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon' }
|
{ value: true, label: 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon.full' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -383,7 +426,8 @@ export const typeConfig = {
|
||||||
{
|
{
|
||||||
key: 'system.linkedClass',
|
key: 'system.linkedClass',
|
||||||
label: 'TYPES.Item.class',
|
label: 'TYPES.Item.class',
|
||||||
format: linkedClass => linkedClass?.name ?? 'DAGGERHEART.UI.ItemBrowser.missing'
|
format: linkedClass =>
|
||||||
|
foundry.utils.fromUuidSync(linkedClass)?.name ?? 'DAGGERHEART.UI.ItemBrowser.missing'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'system.spellcastingTrait',
|
key: 'system.spellcastingTrait',
|
||||||
|
|
@ -393,15 +437,20 @@ export const typeConfig = {
|
||||||
],
|
],
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
key: 'system.linkedClass.uuid',
|
key: 'system.linkedClass',
|
||||||
label: 'TYPES.Item.class',
|
label: 'TYPES.Item.class',
|
||||||
choices: items => {
|
choices: async items => {
|
||||||
const list = items
|
const list = [];
|
||||||
.filter(item => item.system.linkedClass)
|
for (const item of items.filter(item => item.system.linkedClass)) {
|
||||||
.map(item => ({
|
const linkedClass = await foundry.utils.fromUuid(item.system.linkedClass);
|
||||||
value: item.system.linkedClass.uuid,
|
if (linkedClass) {
|
||||||
label: item.system.linkedClass.name
|
list.push({
|
||||||
}));
|
value: linkedClass.uuid,
|
||||||
|
label: linkedClass.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return list.reduce((a, c) => {
|
return list.reduce((a, c) => {
|
||||||
if (!a.find(i => i.value === c.value)) a.push(c);
|
if (!a.find(i => i.value === c.value)) a.push(c);
|
||||||
return a;
|
return a;
|
||||||
|
|
@ -555,7 +604,8 @@ export const compendiumConfig = {
|
||||||
id: 'environments',
|
id: 'environments',
|
||||||
keys: ['environments'],
|
keys: ['environments'],
|
||||||
label: 'DAGGERHEART.UI.ItemBrowser.folders.environments',
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.environments',
|
||||||
type: ['environment']
|
type: ['environment'],
|
||||||
|
listType: 'environments'
|
||||||
},
|
},
|
||||||
beastforms: {
|
beastforms: {
|
||||||
id: 'beastforms',
|
id: 'beastforms',
|
||||||
|
|
|
||||||
|
|
@ -453,7 +453,7 @@ export const allArmorFeatures = () => {
|
||||||
const feature = homebrewFeatures[key];
|
const feature = homebrewFeatures[key];
|
||||||
const actions = feature.actions.map(action => ({
|
const actions = feature.actions.map(action => ({
|
||||||
...action,
|
...action,
|
||||||
effects: action.effects.map(effect => feature.effects.find(x => x.id === effect._id)),
|
effects: action.effects?.map(effect => feature.effects.find(x => x.id === effect._id)) ?? [],
|
||||||
type: action.type
|
type: action.type
|
||||||
}));
|
}));
|
||||||
const actionEffects = actions.flatMap(a => a.effects);
|
const actionEffects = actions.flatMap(a => a.effects);
|
||||||
|
|
@ -1407,7 +1407,7 @@ export const allWeaponFeatures = () => {
|
||||||
|
|
||||||
const actions = feature.actions.map(action => ({
|
const actions = feature.actions.map(action => ({
|
||||||
...action,
|
...action,
|
||||||
effects: action.effects.map(effect => feature.effects.find(x => x.id === effect._id)),
|
effects: action.effects?.map(effect => feature.effects.find(x => x.id === effect._id)) ?? [],
|
||||||
type: action.type
|
type: action.type
|
||||||
}));
|
}));
|
||||||
const actionEffects = actions.flatMap(a => a.effects);
|
const actionEffects = actions.flatMap(a => a.effects);
|
||||||
|
|
|
||||||
|
|
@ -57,15 +57,9 @@ const companionBaseResources = Object.freeze({
|
||||||
stress: {
|
stress: {
|
||||||
id: 'stress',
|
id: 'stress',
|
||||||
initial: 0,
|
initial: 0,
|
||||||
max: 0,
|
max: 3,
|
||||||
reverse: true,
|
reverse: true,
|
||||||
label: 'DAGGERHEART.GENERAL.stress'
|
label: 'DAGGERHEART.GENERAL.stress'
|
||||||
},
|
|
||||||
hope: {
|
|
||||||
id: 'hope',
|
|
||||||
initial: 0,
|
|
||||||
reverse: false,
|
|
||||||
label: 'DAGGERHEART.GENERAL.hope'
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { DHDamageData } from '../fields/action/damageField.mjs';
|
|
||||||
import DHDamageAction from './damageAction.mjs';
|
import DHDamageAction from './damageAction.mjs';
|
||||||
|
|
||||||
export default class DHAttackAction extends DHDamageAction {
|
export default class DHAttackAction extends DHDamageAction {
|
||||||
|
|
@ -12,8 +11,19 @@ export default class DHAttackAction extends DHDamageAction {
|
||||||
super.prepareData();
|
super.prepareData();
|
||||||
if (!!this.item?.system?.attack) {
|
if (!!this.item?.system?.attack) {
|
||||||
if (this.damage.includeBase) {
|
if (this.damage.includeBase) {
|
||||||
const baseDamage = this.getParentDamage();
|
const baseDamage = this.getParentHitPointDamage();
|
||||||
this.damage.parts.hitPoints = new DHDamageData(baseDamage);
|
if (baseDamage) {
|
||||||
|
if (!this.damage.parts.hitPoints) {
|
||||||
|
this.damage.parts.hitPoints = baseDamage;
|
||||||
|
} else {
|
||||||
|
for (const type of baseDamage.type) this.damage.parts.hitPoints.type.add(type);
|
||||||
|
|
||||||
|
this.damage.parts.hitPoints.value.custom = {
|
||||||
|
enabled: true,
|
||||||
|
formula: `${baseDamage.value.getFormula()} + ${this.damage.parts.hitPoints.value.getFormula()}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (this.roll.useDefault) {
|
if (this.roll.useDefault) {
|
||||||
this.roll.trait = this.item.system.attack.roll.trait;
|
this.roll.trait = this.item.system.attack.roll.trait;
|
||||||
|
|
@ -22,16 +32,8 @@ export default class DHAttackAction extends DHDamageAction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getParentDamage() {
|
getParentHitPointDamage() {
|
||||||
return {
|
return this.item?.system?.attack.damage.parts.hitPoints;
|
||||||
value: {
|
|
||||||
multiplier: 'prof',
|
|
||||||
dice: this.item?.system?.attack.damage.parts.hitPoints.value.dice,
|
|
||||||
bonus: this.item?.system?.attack.damage.parts.hitPoints.value.bonus ?? 0
|
|
||||||
},
|
|
||||||
type: this.item?.system?.attack.damage.parts.hitPoints.type,
|
|
||||||
base: true
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get damageFormula() {
|
get damageFormula() {
|
||||||
|
|
@ -73,7 +75,12 @@ export default class DHAttackAction extends DHDamageAction {
|
||||||
const useAltDamage = this.actor?.effects?.find(x => x.type === 'horde')?.active;
|
const useAltDamage = this.actor?.effects?.find(x => x.type === 'horde')?.active;
|
||||||
for (const { value, valueAlt, type } of damage.parts) {
|
for (const { value, valueAlt, type } of damage.parts) {
|
||||||
const usedValue = useAltDamage ? valueAlt : value;
|
const usedValue = useAltDamage ? valueAlt : value;
|
||||||
const str = Roll.replaceFormulaData(usedValue.getFormula(), this.actor?.getRollData() ?? {});
|
const damageString = Roll.replaceFormulaData(usedValue.getFormula(), this.actor?.getRollData() ?? {});
|
||||||
|
const str = damageString
|
||||||
|
? damageString
|
||||||
|
: game.i18n.format('DAGGERHEART.GENERAL.missingX', {
|
||||||
|
x: game.i18n.localize('DAGGERHEART.GENERAL.damage')
|
||||||
|
});
|
||||||
|
|
||||||
const icons = Array.from(type)
|
const icons = Array.from(type)
|
||||||
.map(t => CONFIG.DH.GENERAL.damageTypes[t]?.icon)
|
.map(t => CONFIG.DH.GENERAL.damageTypes[t]?.icon)
|
||||||
|
|
|
||||||
|
|
@ -148,10 +148,14 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns true if the action is usable */
|
/**
|
||||||
|
* Returns true if the action is usable.
|
||||||
|
* An action is usable on any actor type. For example, an adversary might have a base attack action.
|
||||||
|
*/
|
||||||
get usable() {
|
get usable() {
|
||||||
const actor = this.actor;
|
const actor = this.actor;
|
||||||
return this.isOwner && actor?.type === 'character';
|
const pack = actor?.pack ? game.packs.get(actor.pack) : null;
|
||||||
|
return !pack?.locked && this.isOwner;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getRollType(parent) {
|
static getRollType(parent) {
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export default class DhCountdownAction extends DHBaseAction {
|
||||||
|
|
||||||
/** @inheritDoc */
|
/** @inheritDoc */
|
||||||
static migrateData(source) {
|
static migrateData(source) {
|
||||||
for (const countdown of source.countdown) {
|
for (const countdown of Object.values(source.countdown)) {
|
||||||
if (countdown.progress.max) {
|
if (countdown.progress.max) {
|
||||||
countdown.progress.startFormula = countdown.progress.max;
|
countdown.progress.startFormula = countdown.progress.max;
|
||||||
countdown.progress.start = 1;
|
countdown.progress.start = 1;
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ export default class BeastformEffect extends BaseEffect {
|
||||||
static migrateData(source) {
|
static migrateData(source) {
|
||||||
if (!source.characterTokenData.tokenSize.height) source.characterTokenData.tokenSize.height = 1;
|
if (!source.characterTokenData.tokenSize.height) source.characterTokenData.tokenSize.height = 1;
|
||||||
if (!source.characterTokenData.tokenSize.width) source.characterTokenData.tokenSize.width = 1;
|
if (!source.characterTokenData.tokenSize.width) source.characterTokenData.tokenSize.width = 1;
|
||||||
|
if (!source.characterTokenData.tokenSize.depth) source.characterTokenData.tokenSize.depth = 1;
|
||||||
|
|
||||||
return super.migrateData(source);
|
return super.migrateData(source);
|
||||||
}
|
}
|
||||||
|
|
@ -52,7 +53,8 @@ export default class BeastformEffect extends BaseEffect {
|
||||||
if (this.parent.parent.type === 'character') {
|
if (this.parent.parent.type === 'character') {
|
||||||
const baseUpdate = {
|
const baseUpdate = {
|
||||||
height: this.characterTokenData.tokenSize.height,
|
height: this.characterTokenData.tokenSize.height,
|
||||||
width: this.characterTokenData.tokenSize.width
|
width: this.characterTokenData.tokenSize.width,
|
||||||
|
depth: this.characterTokenData.tokenSize.depth
|
||||||
};
|
};
|
||||||
const update = {
|
const update = {
|
||||||
...baseUpdate,
|
...baseUpdate,
|
||||||
|
|
|
||||||
|
|
@ -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'];
|
||||||
|
|
@ -189,6 +188,9 @@ export default class DhpAdversary extends DhCreature {
|
||||||
prepareDerivedData() {
|
prepareDerivedData() {
|
||||||
super.prepareDerivedData();
|
super.prepareDerivedData();
|
||||||
this.attack.roll.isStandardAttack = true;
|
this.attack.roll.isStandardAttack = true;
|
||||||
|
|
||||||
|
// Clamp resources (must be done last to ensure all updates occur)
|
||||||
|
this.resources.clamp();
|
||||||
}
|
}
|
||||||
|
|
||||||
_getTags() {
|
_getTags() {
|
||||||
|
|
@ -203,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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs';
|
import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs';
|
||||||
import DHItem from '../../documents/item.mjs';
|
import DHItem from '../../documents/item.mjs';
|
||||||
import { getScrollTextData } from '../../helpers/utils.mjs';
|
import { createShallowProxy, getScrollTextData } from '../../helpers/utils.mjs';
|
||||||
|
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
|
|
@ -180,8 +180,7 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
|
||||||
* @returns {object}
|
* @returns {object}
|
||||||
*/
|
*/
|
||||||
getRollData() {
|
getRollData() {
|
||||||
const data = { ...this };
|
return createShallowProxy(this);
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -399,8 +399,9 @@ export default class DhCharacter extends DhCreature {
|
||||||
return this.domains.map(key => {
|
return this.domains.map(key => {
|
||||||
const domain = allDomainData[key];
|
const domain = allDomainData[key];
|
||||||
return {
|
return {
|
||||||
|
id: key,
|
||||||
...domain,
|
...domain,
|
||||||
label: game.i18n.localize(domain.label)
|
label: game.i18n.localize(domain?.label) ?? key
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -418,14 +419,11 @@ export default class DhCharacter extends DhCreature {
|
||||||
}
|
}
|
||||||
|
|
||||||
get loadoutSlot() {
|
get loadoutSlot() {
|
||||||
const loadoutCount = this.domainCards.loadout?.length ?? 0,
|
const loadoutCount = this.domainCards.loadout?.length ?? 0;
|
||||||
worldSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxLoadout,
|
const worldSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxLoadout;
|
||||||
max = !worldSetting ? null : worldSetting + this.bonuses.maxLoadout;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
current: loadoutCount,
|
current: loadoutCount,
|
||||||
available: !max ? true : Math.max(max - loadoutCount, 0),
|
available: loadoutCount < worldSetting
|
||||||
max
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -600,6 +598,8 @@ export default class DhCharacter extends DhCreature {
|
||||||
communityFeatures = [],
|
communityFeatures = [],
|
||||||
classFeatures = [],
|
classFeatures = [],
|
||||||
subclassFeatures = [],
|
subclassFeatures = [],
|
||||||
|
multiclassFeatures = [],
|
||||||
|
multiclassSubclassFeatures = [],
|
||||||
companionFeatures = [],
|
companionFeatures = [],
|
||||||
features = [];
|
features = [];
|
||||||
|
|
||||||
|
|
@ -609,9 +609,9 @@ export default class DhCharacter extends DhCreature {
|
||||||
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.community.id) {
|
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.community.id) {
|
||||||
communityFeatures.push(item);
|
communityFeatures.push(item);
|
||||||
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.class.id) {
|
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.class.id) {
|
||||||
classFeatures.push(item);
|
(item.system.multiclassOrigin ? multiclassFeatures : classFeatures).push(item);
|
||||||
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.subclass.id) {
|
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.subclass.id) {
|
||||||
subclassFeatures.push(item);
|
(item.system.multiclassOrigin ? multiclassSubclassFeatures : subclassFeatures).push(item);
|
||||||
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.companion.id) {
|
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.companion.id) {
|
||||||
companionFeatures.push(item);
|
companionFeatures.push(item);
|
||||||
} else if (item.type === 'feature' && !item.system.type) {
|
} else if (item.type === 'feature' && !item.system.type) {
|
||||||
|
|
@ -640,6 +640,24 @@ export default class DhCharacter extends DhCreature {
|
||||||
type: 'subclass',
|
type: 'subclass',
|
||||||
values: subclassFeatures
|
values: subclassFeatures
|
||||||
},
|
},
|
||||||
|
...(multiclassFeatures.length
|
||||||
|
? {
|
||||||
|
multiclassFeatures: {
|
||||||
|
title: `${game.i18n.localize('DAGGERHEART.GENERAL.multiclass')} - ${this.multiclass.value?.name}`,
|
||||||
|
type: 'multiclass',
|
||||||
|
values: multiclassFeatures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(multiclassSubclassFeatures.length
|
||||||
|
? {
|
||||||
|
multiclassSubclassFeatures: {
|
||||||
|
title: `${game.i18n.localize('DAGGERHEART.GENERAL.multiclass')} ${game.i18n.localize('TYPES.Item.subclass')} - ${this.multiclass.subclass?.name}`,
|
||||||
|
type: 'multiclassSubclass',
|
||||||
|
values: multiclassSubclassFeatures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
companionFeatures: {
|
companionFeatures: {
|
||||||
title: game.i18n.localize('DAGGERHEART.ACTORS.Character.companionFeatures'),
|
title: game.i18n.localize('DAGGERHEART.ACTORS.Character.companionFeatures'),
|
||||||
type: 'companion',
|
type: 'companion',
|
||||||
|
|
@ -804,6 +822,8 @@ export default class DhCharacter extends DhCreature {
|
||||||
|
|
||||||
prepareDerivedData() {
|
prepareDerivedData() {
|
||||||
super.prepareDerivedData();
|
super.prepareDerivedData();
|
||||||
|
|
||||||
|
this.resources.hope.max -= this.scars;
|
||||||
if (this.companion) {
|
if (this.companion) {
|
||||||
for (let levelKey in this.companion.system.levelData.levelups) {
|
for (let levelKey in this.companion.system.levelData.levelups) {
|
||||||
const level = this.companion.system.levelData.levelups[levelKey];
|
const level = this.companion.system.levelData.levelups[levelKey];
|
||||||
|
|
@ -817,7 +837,6 @@ export default class DhCharacter extends DhCreature {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.resources.hope.max -= this.scars;
|
|
||||||
this.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait;
|
this.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait;
|
||||||
|
|
||||||
this.resources.armor = {
|
this.resources.armor = {
|
||||||
|
|
@ -835,6 +854,9 @@ export default class DhCharacter extends DhCreature {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.attack.damage.parts.hitPoints.value.custom.formula = `@prof${this.basicAttackDamageDice}${this.rules.attack.damage.bonus ? ` + ${this.rules.attack.damage.bonus}` : ''}`;
|
this.attack.damage.parts.hitPoints.value.custom.formula = `@prof${this.basicAttackDamageDice}${this.rules.attack.damage.bonus ? ` + ${this.rules.attack.damage.bonus}` : ''}`;
|
||||||
|
|
||||||
|
// Clamp resources (must be done last to ensure all updates occur)
|
||||||
|
this.resources.clamp();
|
||||||
}
|
}
|
||||||
|
|
||||||
getRollData() {
|
getRollData() {
|
||||||
|
|
@ -866,16 +888,17 @@ export default class DhCharacter extends DhCreature {
|
||||||
|
|
||||||
/* Scars can alter the amount of current hope */
|
/* Scars can alter the amount of current hope */
|
||||||
if (changes.system?.scars) {
|
if (changes.system?.scars) {
|
||||||
const diff = this.system.scars - changes.system.scars;
|
const diff = this.scars - changes.system.scars;
|
||||||
const newHopeMax = this.system.resources.hope.max + diff;
|
const newHopeMax = this.resources.hope.max + diff;
|
||||||
const newHopeValue = Math.min(newHopeMax, this.system.resources.hope.value);
|
const newHopeValue = Math.min(newHopeMax, this.resources.hope.value);
|
||||||
if (newHopeValue != this.system.resources.hope.value) {
|
if (newHopeValue != this.resources.hope.value) {
|
||||||
if (!changes.system.resources.hope) changes.system.resources.hope = { value: 0 };
|
changes.system = foundry.utils.mergeObject(changes.system ?? {}, {
|
||||||
|
resources: {
|
||||||
changes.system.resources.hope = {
|
hope: {
|
||||||
...changes.system.resources.hope,
|
value: (changes.system?.resources?.hope?.value ?? 0) + newHopeMax
|
||||||
value: changes.system.resources.hope.value + newHopeValue
|
}
|
||||||
};
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -146,9 +146,6 @@ export default class DhCompanion extends DhCreature {
|
||||||
const level = this.levelData.levelups[levelKey];
|
const level = this.levelData.levelups[levelKey];
|
||||||
for (let selection of level.selections) {
|
for (let selection of level.selections) {
|
||||||
switch (selection.type) {
|
switch (selection.type) {
|
||||||
case 'hope':
|
|
||||||
this.resources.hope += selection.value;
|
|
||||||
break;
|
|
||||||
case 'vicious':
|
case 'vicious':
|
||||||
if (selection.data[0] === 'damage') {
|
if (selection.data[0] === 'damage') {
|
||||||
this.attack.damage.parts.hitPoints.value.dice = adjustDice(
|
this.attack.damage.parts.hitPoints.value.dice = adjustDice(
|
||||||
|
|
@ -185,6 +182,9 @@ export default class DhCompanion extends DhCreature {
|
||||||
return acc;
|
return acc;
|
||||||
}, this.partner.system.companionData.levelupChoices);
|
}, this.partner.system.companionData.levelupChoices);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clamp resources (must be done last to ensure all updates occur)
|
||||||
|
this.resources.clamp();
|
||||||
}
|
}
|
||||||
|
|
||||||
async _preUpdate(changes, options, userId) {
|
async _preUpdate(changes, options, userId) {
|
||||||
|
|
|
||||||
|
|
@ -60,14 +60,4 @@ export default class DhCreature extends BaseDataActor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareDerivedData() {
|
|
||||||
const minLimitResource = resource => {
|
|
||||||
if (resource) resource.value = Math.min(resource.value, resource.max);
|
|
||||||
};
|
|
||||||
|
|
||||||
minLimitResource(this.resources.stress);
|
|
||||||
minLimitResource(this.resources.hitPoints);
|
|
||||||
minLimitResource(this.resources.hope);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,7 +18,7 @@ export default class DhParty extends BaseDataActor {
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
return {
|
return {
|
||||||
...super.defineSchema(),
|
...super.defineSchema(),
|
||||||
partyMembers: new ForeignDocumentUUIDArrayField({ type: 'Actor' }, { prune: true }),
|
partyMembers: new ForeignDocumentUUIDArrayField({ type: 'Actor' }),
|
||||||
notes: new fields.HTMLField(),
|
notes: new fields.HTMLField(),
|
||||||
gold: new GoldField(),
|
gold: new GoldField(),
|
||||||
tagTeam: new fields.EmbeddedDataField(TagTeamData),
|
tagTeam: new fields.EmbeddedDataField(TagTeamData),
|
||||||
|
|
|
||||||
219
module/data/actor/tierAdjustment.mjs
Normal file
219
module/data/actor/tierAdjustment.mjs
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
import { calculateExpectedValue, parseTermsFromSimpleFormula } from '../../helpers/utils.mjs';
|
||||||
|
import { adversaryExpectedDamage, adversaryScalingData } from '../../config/actorConfig.mjs';
|
||||||
|
import { parseInlineParams } from '../../enrichers/parser.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, { first: 'value' });
|
||||||
|
if (!formula) 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) {
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,14 @@ export default class CompendiumBrowserSettings extends foundry.abstract.DataMode
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
excludedPacks: new fields.TypedObjectField(
|
excludedPacks: new fields.TypedObjectField(
|
||||||
|
new fields.TypedObjectField(
|
||||||
new fields.SchemaField({
|
new fields.SchemaField({
|
||||||
excludedDocumentTypes: new fields.ArrayField(
|
excludedDocumentTypes: new fields.ArrayField(
|
||||||
new fields.StringField({ required: true, choices: CONST.SYSTEM_SPECIFIC_COMPENDIUM_TYPES })
|
new fields.StringField({ required: true, choices: CONST.SYSTEM_SPECIFIC_COMPENDIUM_TYPES })
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -28,7 +30,7 @@ export default class CompendiumBrowserSettings extends foundry.abstract.DataMode
|
||||||
const excludedSourceData = this.excludedSources[packageName];
|
const excludedSourceData = this.excludedSources[packageName];
|
||||||
if (excludedSourceData && excludedSourceData.excludedDocumentTypes.includes(pack.metadata.type)) return true;
|
if (excludedSourceData && excludedSourceData.excludedDocumentTypes.includes(pack.metadata.type)) return true;
|
||||||
|
|
||||||
const excludedPackData = this.excludedPacks[item.pack];
|
const excludedPackData = this.excludedPacks[packageName]?.[pack.metadata.name];
|
||||||
if (excludedPackData && excludedPackData.excludedDocumentTypes.includes(pack.metadata.type)) return true;
|
if (excludedPackData && excludedPackData.excludedDocumentTypes.includes(pack.metadata.type)) return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ export default class CostField extends fields.ArrayField {
|
||||||
static calcCosts(costs) {
|
static calcCosts(costs) {
|
||||||
const resources = CostField.getResources.call(this, costs);
|
const resources = CostField.getResources.call(this, costs);
|
||||||
let filteredCosts = costs;
|
let filteredCosts = costs;
|
||||||
if (this.parent?.metadata.isQuantifiable && this.parent.consumeOnUse === false) {
|
if (this.parent?.isInventoryItem && this.parent.consumeOnUse === false) {
|
||||||
filteredCosts = filteredCosts.filter(c => c.key !== 'quantity');
|
filteredCosts = filteredCosts.filter(c => c.key !== 'quantity');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../../systemRegistration/socket.mjs';
|
import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../../systemRegistration/socket.mjs';
|
||||||
|
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
|
|
@ -78,7 +78,7 @@ export default class CountdownField extends fields.ArrayField {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await emitAsGM(
|
await emitGMUpdate(
|
||||||
GMUpdateEvent.UpdateCountdowns,
|
GMUpdateEvent.UpdateCountdowns,
|
||||||
async () => {
|
async () => {
|
||||||
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
|
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
|
||||||
|
|
|
||||||
|
|
@ -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 { emitAsGM, 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 emitAsGM(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);
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,18 @@ class ResourcesField extends fields.TypedObjectField {
|
||||||
value.isReversed = resources[key].reverse;
|
value.isReversed = resources[key].reverse;
|
||||||
value.max = typeof resource.max === 'number' ? (value.max ?? resource.max) : null;
|
value.max = typeof resource.max === 'number' ? (value.max ?? resource.max) : null;
|
||||||
}
|
}
|
||||||
|
Object.defineProperty(data, 'clamp', {
|
||||||
|
value: function () {
|
||||||
|
for (const key of Object.keys(this)) {
|
||||||
|
const resource = this[key];
|
||||||
|
if (typeof resource?.max === 'number') {
|
||||||
|
resource.value = Math.clamp(resource.value, 0, resource.max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enumerable: false
|
||||||
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ export default class ForeignDocumentUUIDArrayField extends foundry.data.fields.A
|
||||||
*/
|
*/
|
||||||
constructor(fieldOption = {}, options = {}, context = {}) {
|
constructor(fieldOption = {}, options = {}, context = {}) {
|
||||||
super(new ForeignDocumentUUIDField(fieldOption), options, context);
|
super(new ForeignDocumentUUIDField(fieldOption), options, context);
|
||||||
|
this.options.prune ??= true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export class CharacterData extends foundry.abstract.DataModel {
|
||||||
}),
|
}),
|
||||||
rollData: new fields.JSONField({ nullable: true, initial: null }),
|
rollData: new fields.JSONField({ nullable: true, initial: null }),
|
||||||
selected: new fields.BooleanField({ initial: false }),
|
selected: new fields.BooleanField({ initial: false }),
|
||||||
successfull: new fields.BooleanField({ nullable: true, initial: null })
|
successful: new fields.BooleanField({ nullable: true, initial: null })
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import BaseDataItem from './base.mjs';
|
import BaseDataItem from './base.mjs';
|
||||||
import ItemLinkFields from '../../data/fields/itemLinkFields.mjs';
|
import ItemLinkFields from '../../data/fields/itemLinkFields.mjs';
|
||||||
import { getFeaturesHTMLData } from '../../helpers/utils.mjs';
|
import { fromUuids, getFeaturesHTMLData } from '../../helpers/utils.mjs';
|
||||||
|
|
||||||
export default class DHAncestry extends BaseDataItem {
|
export default class DHAncestry extends BaseDataItem {
|
||||||
/** @inheritDoc */
|
/** @inheritDoc */
|
||||||
|
|
@ -45,6 +45,10 @@ export default class DHAncestry extends BaseDataItem {
|
||||||
|
|
||||||
/**@inheritdoc */
|
/**@inheritdoc */
|
||||||
async getDescriptionData() {
|
async getDescriptionData() {
|
||||||
|
// Preload all ancestry 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 baseDescription = this.description;
|
const baseDescription = this.description;
|
||||||
const features = await getFeaturesHTMLData(this.features);
|
const features = await getFeaturesHTMLData(this.features);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -141,16 +141,6 @@ export default class DHArmor extends AttachableItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onUpdate(a, b, c) {
|
|
||||||
super._onUpdate(a, b, c);
|
|
||||||
|
|
||||||
if (this.actor?.type === 'character') {
|
|
||||||
for (const party of this.actor.parties) {
|
|
||||||
party.render();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @inheritDoc */
|
/** @inheritDoc */
|
||||||
static migrateDocumentData(source) {
|
static migrateDocumentData(source) {
|
||||||
if (!source.system.armor) {
|
if (!source.system.armor) {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,12 @@
|
||||||
* @property {boolean} isInventoryItem- Indicates whether items of this type is a Inventory Item
|
* @property {boolean} isInventoryItem- Indicates whether items of this type is a Inventory Item
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { addLinkedItemsDiff, getScrollTextData, updateLinkedItemApps } from '../../helpers/utils.mjs';
|
import {
|
||||||
|
addLinkedItemsDiff,
|
||||||
|
getScrollTextData,
|
||||||
|
createShallowProxy,
|
||||||
|
updateLinkedItemApps
|
||||||
|
} from '../../helpers/utils.mjs';
|
||||||
import { ActionsField } from '../fields/actionField.mjs';
|
import { ActionsField } from '../fields/actionField.mjs';
|
||||||
import FormulaField from '../fields/formulaField.mjs';
|
import FormulaField from '../fields/formulaField.mjs';
|
||||||
|
|
||||||
|
|
@ -159,8 +164,8 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
|
||||||
* @returns {object}
|
* @returns {object}
|
||||||
*/
|
*/
|
||||||
getRollData(options = {}) {
|
getRollData(options = {}) {
|
||||||
const actorRollData = this.actor?.getRollData() ?? {};
|
const data = this.actor?.getRollData() ?? {};
|
||||||
const data = { ...actorRollData, item: { ...this } };
|
data.item = createShallowProxy(this);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,7 +200,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
|
||||||
const features = [];
|
const features = [];
|
||||||
for (let f of this.features) {
|
for (let f of this.features) {
|
||||||
const fBase = f.item ?? f;
|
const fBase = f.item ?? f;
|
||||||
const feature = fBase.system ? fBase : await foundry.utils.fromUuid(fBase.uuid);
|
const feature = fBase.pack ? await foundry.utils.fromUuid(fBase.uuid) : fBase;
|
||||||
features.push(
|
features.push(
|
||||||
foundry.utils.mergeObject(
|
foundry.utils.mergeObject(
|
||||||
feature.toObject(),
|
feature.toObject(),
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,8 @@ export default class DHBeastform extends BaseDataItem {
|
||||||
}),
|
}),
|
||||||
scale: new fields.NumberField({ nullable: false, min: 0.2, max: 3, step: 0.05, initial: 1 }),
|
scale: new fields.NumberField({ nullable: false, min: 0.2, max: 3, step: 0.05, initial: 1 }),
|
||||||
height: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }),
|
height: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }),
|
||||||
width: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true })
|
width: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }),
|
||||||
|
depth: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true })
|
||||||
}),
|
}),
|
||||||
mainTrait: new fields.StringField({
|
mainTrait: new fields.StringField({
|
||||||
required: true,
|
required: true,
|
||||||
|
|
@ -192,7 +193,8 @@ export default class DHBeastform extends BaseDataItem {
|
||||||
tokenSize: {
|
tokenSize: {
|
||||||
scale: this.parent.parent.prototypeToken.texture.scaleX,
|
scale: this.parent.parent.prototypeToken.texture.scaleX,
|
||||||
height: this.parent.parent.prototypeToken.height,
|
height: this.parent.parent.prototypeToken.height,
|
||||||
width: this.parent.parent.prototypeToken.width
|
width: this.parent.parent.prototypeToken.width,
|
||||||
|
depth: this.parent.parent.prototypeToken.depth
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
advantageOn: this.advantageOn,
|
advantageOn: this.advantageOn,
|
||||||
|
|
@ -211,10 +213,12 @@ export default class DHBeastform extends BaseDataItem {
|
||||||
: null;
|
: null;
|
||||||
const width = autoTokenSize ?? this.tokenSize.width;
|
const width = autoTokenSize ?? this.tokenSize.width;
|
||||||
const height = autoTokenSize ?? this.tokenSize.height;
|
const height = autoTokenSize ?? this.tokenSize.height;
|
||||||
|
const depth = autoTokenSize ?? this.tokenSize.depth;
|
||||||
|
|
||||||
const prototypeTokenUpdate = {
|
const prototypeTokenUpdate = {
|
||||||
height,
|
height,
|
||||||
width,
|
width,
|
||||||
|
depth,
|
||||||
texture: {
|
texture: {
|
||||||
src: this.tokenImg,
|
src: this.tokenImg,
|
||||||
scaleX: this.tokenSize.scale,
|
scaleX: this.tokenSize.scale,
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
import { DhLevelOption } from '../levelTier.mjs';
|
import { DhLevelOption } from '../levelTier.mjs';
|
||||||
|
|
||||||
export default class DHClass extends BaseDataItem {
|
export default class DHClass extends BaseDataItem {
|
||||||
|
|
@ -32,7 +32,6 @@ export default class DHClass extends BaseDataItem {
|
||||||
}),
|
}),
|
||||||
evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }),
|
evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }),
|
||||||
features: new ItemLinkFields(),
|
features: new ItemLinkFields(),
|
||||||
subclasses: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
|
|
||||||
inventory: new fields.SchemaField({
|
inventory: new fields.SchemaField({
|
||||||
take: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
|
take: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
|
||||||
choiceA: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
|
choiceA: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
|
||||||
|
|
@ -78,6 +77,25 @@ export default class DHClass extends BaseDataItem {
|
||||||
return this.features.filter(x => x.type === CONFIG.DH.ITEM.featureSubTypes.class).map(x => x.item);
|
return this.features.filter(x => x.type === CONFIG.DH.ITEM.featureSubTypes.class).map(x => x.item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchSubclasses() {
|
||||||
|
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));
|
||||||
|
for (const pack of game.packs) {
|
||||||
|
const packIds = [];
|
||||||
|
const indexes = await pack.getIndex({ fields: ['system.linkedClass'] });
|
||||||
|
for (const index of indexes) {
|
||||||
|
if (index.type !== 'subclass') continue;
|
||||||
|
if (!uuids.includes(index.system?.linkedClass)) continue;
|
||||||
|
if (subclasses.find(x => x.uuid === index.uuid)) continue;
|
||||||
|
packIds.push(index._id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packIds.length > 0) subclasses.push(...(await pack.getDocuments({ _id__in: packIds })));
|
||||||
|
}
|
||||||
|
|
||||||
|
return subclasses;
|
||||||
|
}
|
||||||
|
|
||||||
async _preCreate(data, options, user) {
|
async _preCreate(data, options, user) {
|
||||||
if (this.actor?.type === 'character') {
|
if (this.actor?.type === 'character') {
|
||||||
const levelupAuto = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto;
|
const levelupAuto = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto;
|
||||||
|
|
@ -207,6 +225,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,4 +1,4 @@
|
||||||
import { getFeaturesHTMLData } from '../../helpers/utils.mjs';
|
import { fromUuids, getFeaturesHTMLData } from '../../helpers/utils.mjs';
|
||||||
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
|
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
|
||||||
import BaseDataItem from './base.mjs';
|
import BaseDataItem from './base.mjs';
|
||||||
|
|
||||||
|
|
@ -27,6 +27,10 @@ export default class DHCommunity extends BaseDataItem {
|
||||||
|
|
||||||
/**@inheritdoc */
|
/**@inheritdoc */
|
||||||
async getDescriptionData() {
|
async getDescriptionData() {
|
||||||
|
// Preload all community features for acquisition from the cache
|
||||||
|
// todo: make feature acquisition async and replace feature helpers for methods
|
||||||
|
await fromUuids(this._source.features);
|
||||||
|
|
||||||
const baseDescription = this.description;
|
const baseDescription = this.description;
|
||||||
const features = await getFeaturesHTMLData(this.features);
|
const features = await getFeaturesHTMLData(this.features);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
@ -28,7 +27,7 @@ export default class DHSubclass extends BaseDataItem {
|
||||||
features: new ItemLinkFields(),
|
features: new ItemLinkFields(),
|
||||||
featureState: new fields.NumberField({ required: true, initial: 1, min: 1 }),
|
featureState: new fields.NumberField({ required: true, initial: 1, min: 1 }),
|
||||||
isMulticlass: new fields.BooleanField({ initial: false }),
|
isMulticlass: new fields.BooleanField({ initial: false }),
|
||||||
linkedClass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true, initial: null })
|
linkedClass: new fields.DocumentUUIDField({ type: 'Item', nullable: true, initial: null })
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,38 +55,31 @@ export default class DHSubclass extends BaseDataItem {
|
||||||
if (allowed === false) return;
|
if (allowed === false) return;
|
||||||
|
|
||||||
if (this.actor?.type === 'character') {
|
if (this.actor?.type === 'character') {
|
||||||
const dataUuid = data.uuid ?? data._stats.compendiumSource ?? `Item.${data._id}`;
|
const { value: actorClass, subclass: existingSubclass } = this.actor.system.class;
|
||||||
if (this.actor.system.class.subclass) {
|
const { value: multiclass, subclass: existingMultisubclass } = this.actor.system.multiclass;
|
||||||
if (this.actor.system.multiclass.subclass) {
|
if (!actorClass && !multiclass) {
|
||||||
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassesAlreadyPresent'));
|
ui.notifications.warn('DAGGERHEART.UI.Notifications.missingClass', { localize: true });
|
||||||
return false;
|
return false;
|
||||||
} else {
|
}
|
||||||
const multiclass = this.actor.items.find(x => x.type === 'class' && x.system.isMulticlass);
|
if (existingSubclass && existingMultisubclass) {
|
||||||
if (!multiclass) {
|
ui.notifications.warn('DAGGERHEART.UI.Notifications.subclassesAlreadyPresent', { localize: true });
|
||||||
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.missingMulticlass'));
|
return false;
|
||||||
|
}
|
||||||
|
if (existingSubclass && !multiclass) {
|
||||||
|
ui.notifications.warn('DAGGERHEART.UI.Notifications.missingMulticlass', { localize: true });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (multiclass.system.subclasses.every(x => x.uuid !== dataUuid)) {
|
const match = [multiclass, actorClass].find(
|
||||||
ui.notifications.error(
|
c => c && (c._stats.compendiumSource ?? c.uuid) === this.linkedClass
|
||||||
game.i18n.localize('DAGGERHEART.UI.Notifications.subclassNotInMulticlass')
|
|
||||||
);
|
);
|
||||||
|
if (!match) {
|
||||||
|
const key = multiclass ? 'subclassNotInMulticlass' : 'subclassNotInClass';
|
||||||
|
ui.notifications.warn(`DAGGERHEART.UI.Notifications.${key}`, { localize: true });
|
||||||
return false;
|
return false;
|
||||||
}
|
} else if (match.system.isMulticlass) {
|
||||||
|
|
||||||
await this.updateSource({ isMulticlass: true });
|
await this.updateSource({ isMulticlass: true });
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
const actorClass = this.actor.items.find(x => x.type === 'class' && !x.system.isMulticlass);
|
|
||||||
if (!actorClass) {
|
|
||||||
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.missingClass'));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (actorClass.system.subclasses.every(x => x.uuid !== dataUuid)) {
|
|
||||||
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassNotInClass'));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,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 subclass 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);
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,10 @@ export default class DHWeapon extends AttachableItem {
|
||||||
equipped: new fields.BooleanField({ initial: false }),
|
equipped: new fields.BooleanField({ initial: false }),
|
||||||
|
|
||||||
//SETTINGS
|
//SETTINGS
|
||||||
secondary: new fields.BooleanField({ initial: false, label: 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon' }),
|
secondary: new fields.BooleanField({
|
||||||
|
initial: false,
|
||||||
|
label: 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon.full'
|
||||||
|
}),
|
||||||
burden: new fields.StringField({
|
burden: new fields.StringField({
|
||||||
required: true,
|
required: true,
|
||||||
choices: CONFIG.DH.GENERAL.burden,
|
choices: CONFIG.DH.GENERAL.burden,
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ export default class DHScene extends foundry.abstract.DataModel {
|
||||||
close: new fields.NumberField({ integer: true, label: 'DAGGERHEART.CONFIG.Range.close.name' }),
|
close: new fields.NumberField({ integer: true, label: 'DAGGERHEART.CONFIG.Range.close.name' }),
|
||||||
far: new fields.NumberField({ integer: true, label: 'DAGGERHEART.CONFIG.Range.far.name' })
|
far: new fields.NumberField({ integer: true, label: 'DAGGERHEART.CONFIG.Range.far.name' })
|
||||||
}),
|
}),
|
||||||
sceneEnvironments: new ForeignDocumentUUIDArrayField({ type: 'Actor', prune: true })
|
sceneEnvironments: new ForeignDocumentUUIDArrayField({ type: 'Actor' })
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { defaultRestOptions } from '../../config/generalConfig.mjs';
|
import { defaultRestOptions } from '../../config/generalConfig.mjs';
|
||||||
|
import { resetAndRerenderActors } from '../../helpers/utils.mjs';
|
||||||
import { ActionsField } from '../fields/actionField.mjs';
|
import { ActionsField } from '../fields/actionField.mjs';
|
||||||
|
|
||||||
const currencyField = (initial, label, icon) =>
|
const currencyField = (initial, label, icon) =>
|
||||||
|
|
@ -54,7 +55,7 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
|
||||||
maxDomains: new fields.NumberField({
|
maxDomains: new fields.NumberField({
|
||||||
required: true,
|
required: true,
|
||||||
integer: true,
|
integer: true,
|
||||||
min: 1,
|
min: 0,
|
||||||
initial: 2,
|
initial: 2,
|
||||||
label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxDomains.label'
|
label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxDomains.label'
|
||||||
}),
|
}),
|
||||||
|
|
@ -196,6 +197,12 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
|
||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_initialize(options) {
|
||||||
|
super._initialize(options);
|
||||||
|
this.maxDomains ||= Infinity;
|
||||||
|
this.maxLoadout ||= Infinity;
|
||||||
|
}
|
||||||
|
|
||||||
/** Invoked by the setting when data changes */
|
/** Invoked by the setting when data changes */
|
||||||
handleChange() {
|
handleChange() {
|
||||||
if (this.maxFear) {
|
if (this.maxFear) {
|
||||||
|
|
@ -203,7 +210,7 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.refreshConfig();
|
this.refreshConfig();
|
||||||
this.#resetActors();
|
resetAndRerenderActors();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update config values based on homebrew data. Make sure the references don't change */
|
/** Update config values based on homebrew data. Make sure the references don't change */
|
||||||
|
|
@ -224,29 +231,6 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Triggers a reset and non-forced re-render on all given actors (if given)
|
|
||||||
* or all world actors and actors in all scenes to show immediate results for a changed setting.
|
|
||||||
*/
|
|
||||||
#resetActors() {
|
|
||||||
const actors = new Set(
|
|
||||||
[
|
|
||||||
game.actors.contents,
|
|
||||||
game.scenes.contents.flatMap(s => s.tokens.contents).flatMap(t => t.actor ?? [])
|
|
||||||
].flat()
|
|
||||||
);
|
|
||||||
for (const actor of actors) {
|
|
||||||
for (const app of Object.values(actor.apps)) {
|
|
||||||
for (const element of app.element?.querySelectorAll('prose-mirror.active')) {
|
|
||||||
element.open = false; // This triggers a save
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
actor.reset();
|
|
||||||
actor.render();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Resource extends foundry.abstract.DataModel {
|
export class Resource extends foundry.abstract.DataModel {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { resetAndRerenderActors } from '../../helpers/utils.mjs';
|
||||||
|
|
||||||
export default class DhMetagaming extends foundry.abstract.DataModel {
|
export default class DhMetagaming extends foundry.abstract.DataModel {
|
||||||
static defineSchema() {
|
static defineSchema() {
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
@ -6,7 +8,24 @@ export default class DhMetagaming extends foundry.abstract.DataModel {
|
||||||
initial: false,
|
initial: false,
|
||||||
label: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hideObserverPermissionInChat.label',
|
label: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hideObserverPermissionInChat.label',
|
||||||
hint: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hideObserverPermissionInChat.hint'
|
hint: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hideObserverPermissionInChat.hint'
|
||||||
|
}),
|
||||||
|
hidePartyStats: new fields.StringField({
|
||||||
|
initial: 'never',
|
||||||
|
label: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hidePartyStats.label',
|
||||||
|
hint: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hidePartyStats.hint',
|
||||||
|
required: true,
|
||||||
|
nullable: false,
|
||||||
|
choices: {
|
||||||
|
never: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hidePartyStats.choices.never',
|
||||||
|
players: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hidePartyStats.choices.players',
|
||||||
|
always: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hidePartyStats.choices.always'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Invoked by the setting when data changes */
|
||||||
|
handleChange() {
|
||||||
|
resetAndRerenderActors();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = [];
|
||||||
|
|
@ -36,6 +37,7 @@ export default class DHRoll extends Roll {
|
||||||
static async buildConfigure(config = {}, message = {}) {
|
static async buildConfigure(config = {}, message = {}) {
|
||||||
config.hooks = [...this.getHooks(), ''];
|
config.hooks = [...this.getHooks(), ''];
|
||||||
config.dialog ??= {};
|
config.dialog ??= {};
|
||||||
|
config.damageOptions ??= {};
|
||||||
|
|
||||||
for (const hook of config.hooks) {
|
for (const hook of config.hooks) {
|
||||||
if (Hooks.call(`${CONFIG.DH.id}.preRoll${hook.capitalize()}`, config, message) === false) return null;
|
if (Hooks.call(`${CONFIG.DH.id}.preRoll${hook.capitalize()}`, config, message) === false) return null;
|
||||||
|
|
@ -75,9 +77,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 +85,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 => ({
|
||||||
|
|
@ -141,8 +142,8 @@ export default class DHRoll extends Roll {
|
||||||
const metagamingSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming);
|
const metagamingSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming);
|
||||||
const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options });
|
const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options });
|
||||||
return foundry.applications.handlebars.renderTemplate(template, {
|
return foundry.applications.handlebars.renderTemplate(template, {
|
||||||
...chatData,
|
|
||||||
roll: this,
|
roll: this,
|
||||||
|
...chatData,
|
||||||
parent: chatData.parent,
|
parent: chatData.parent,
|
||||||
targetMode: chatData.targetMode,
|
targetMode: chatData.targetMode,
|
||||||
areas: chatData.action?.areas,
|
areas: chatData.action?.areas,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -128,32 +130,34 @@ 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.roll?.hopeFaces ?? 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.roll?.fearFaces ?? 12
|
faces: this.terms[2]?.faces ?? this.data.rules.dualityRoll?.defaultFearDice ?? 12
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
applyAdvantage() {
|
applyAdvantage() {
|
||||||
if (this.hasAdvantage || this.hasDisadvantage) {
|
const advDieClass = this.hasAdvantage
|
||||||
const dieFaces = this.advantageFaces,
|
? game.system.api.dice.diceTypes.AdvantageDie
|
||||||
advDie = new foundry.dice.terms.Die({ faces: dieFaces, number: this.advantageNumber });
|
: this.hasDisadvantage
|
||||||
|
? game.system.api.dice.diceTypes.DisadvantageDie
|
||||||
|
: null;
|
||||||
|
if (advDieClass) {
|
||||||
|
const advDie = new advDieClass({ faces: this.advantageFaces, number: this.advantageNumber });
|
||||||
|
if (this.terms.length < 4) {
|
||||||
if (this.advantageNumber > 1) advDie.modifiers = ['kh'];
|
if (this.advantageNumber > 1) advDie.modifiers = ['kh'];
|
||||||
this.terms.push(
|
this.terms.push(
|
||||||
new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }),
|
new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }),
|
||||||
advDie
|
advDie
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
this.terms[4] = advDie;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (this.rallyFaces)
|
if (this.rallyFaces)
|
||||||
this.terms.push(
|
this.terms.push(
|
||||||
|
|
@ -378,4 +382,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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
19
module/dice/helpers.mjs
Normal file
19
module/dice/helpers.mjs
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { ResourceUpdateMap } from '../data/action/baseAction.mjs';
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
@ -169,27 +169,37 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
|
||||||
super._applyChangeUnguided(actor, change, changes, options);
|
super._applyChangeUnguided(actor, change, changes, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getChangeValue(model, change, effect) {
|
/** Recursively finds the first parent document of the given object */
|
||||||
let key = change.value.toString();
|
static #resolveParentDocument(model, documentClass) {
|
||||||
const isOriginTarget = key.toLowerCase().includes('origin.@');
|
if (!model) return null;
|
||||||
let parseModel = model;
|
return model instanceof documentClass
|
||||||
if (isOriginTarget && effect.origin) {
|
? model
|
||||||
key = change.key.replaceAll(/origin\.@/gi, '@');
|
: model.parent
|
||||||
try {
|
? this.#resolveParentDocument(model.parent, documentClass)
|
||||||
const originEffect = foundry.utils.fromUuidSync(effect.origin);
|
: null;
|
||||||
const doc =
|
|
||||||
originEffect.parent?.parent instanceof game.system.api.documents.DhpActor
|
|
||||||
? originEffect.parent
|
|
||||||
: originEffect.parent.parent;
|
|
||||||
if (doc) parseModel = doc;
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getChangeValue(model, change, effect) {
|
||||||
|
let value = change.value.toString();
|
||||||
|
const useOrigin = value.toLowerCase().includes('origin.@') && effect.origin;
|
||||||
|
let origin = null;
|
||||||
|
if (effect.origin) {
|
||||||
|
if (useOrigin) value = value.replaceAll(/origin\.@/gi, '@');
|
||||||
|
const originEffect = foundry.utils.fromUuidSync(effect.origin);
|
||||||
|
origin = this.#resolveParentDocument(originEffect, Item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the actor and item documents. Note that actor roll data is inclusive of system roll data
|
||||||
|
const actor = this.#resolveParentDocument(model, Actor);
|
||||||
|
const item =
|
||||||
|
(useOrigin ? origin : null) ??
|
||||||
|
this.#resolveParentDocument(effect.parent, Item) ??
|
||||||
|
(origin?.actor === actor ? origin : null);
|
||||||
const stackingParsedValue = effect.system.stacking
|
const stackingParsedValue = effect.system.stacking
|
||||||
? Roll.replaceFormulaData(key, { stacks: effect.system.stacking.value })
|
? Roll.replaceFormulaData(value, { stacks: effect.system.stacking.value })
|
||||||
: key;
|
: value;
|
||||||
const evalValue = itemAbleRollParse(stackingParsedValue, parseModel, effect.parent);
|
const evalValue = this.effectSafeEval(itemAbleRollParse(stackingParsedValue, actor, item));
|
||||||
return evalValue ?? key;
|
return evalValue ?? value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { emitAsGM, GMUpdateEvent } from '../systemRegistration/socket.mjs';
|
import { emitGMUpdate, GMUpdateEvent } from '../systemRegistration/socket.mjs';
|
||||||
import { LevelOptionType } from '../data/levelTier.mjs';
|
import { LevelOptionType } from '../data/levelTier.mjs';
|
||||||
import DHFeature from '../data/item/feature.mjs';
|
import DHFeature from '../data/item/feature.mjs';
|
||||||
import { createScrollText, damageKeyToNumber, getDamageKey } from '../helpers/utils.mjs';
|
import { createScrollText, damageKeyToNumber, getDamageKey, createShallowProxy } from '../helpers/utils.mjs';
|
||||||
import DhCompanionLevelUp from '../applications/levelup/companionLevelup.mjs';
|
import DhCompanionLevelUp from '../applications/levelup/companionLevelup.mjs';
|
||||||
import { ResourceUpdateMap } from '../data/action/baseAction.mjs';
|
import { ResourceUpdateMap } from '../data/action/baseAction.mjs';
|
||||||
import { abilities } from '../config/actorConfig.mjs';
|
import { abilities } from '../config/actorConfig.mjs';
|
||||||
|
|
@ -65,6 +65,11 @@ export default class DhpActor extends Actor {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static createDialog(data, createOptions, options, renderOptions) {
|
||||||
|
options.classes = [options.classes ?? [], 'actor-create'].flat(); // handled in hook
|
||||||
|
return super.createDialog(data, createOptions, options, renderOptions);
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
/** @inheritDoc */
|
/** @inheritDoc */
|
||||||
|
|
@ -99,7 +104,7 @@ export default class DhpActor extends Actor {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure prototype token settings
|
// Configure prototype token settings
|
||||||
if (['character', 'companion', 'party'].includes(this.type))
|
if (['character', 'companion', 'party'].includes(this.type)) {
|
||||||
Object.assign(update, {
|
Object.assign(update, {
|
||||||
prototypeToken: {
|
prototypeToken: {
|
||||||
sight: { enabled: true },
|
sight: { enabled: true },
|
||||||
|
|
@ -107,13 +112,35 @@ export default class DhpActor extends Actor {
|
||||||
disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY
|
disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.type === 'npc') {
|
||||||
|
Object.assign(update, {
|
||||||
|
prototypeToken: {
|
||||||
|
disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.updateSource(update);
|
this.updateSource(update);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Perform a render, debounced in order to prevent overloading repeat render requests */
|
||||||
|
renderDebounced = foundry.utils.debounce(options => {
|
||||||
|
return this.render(options);
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
_onUpdateDescendantDocuments(parent, collection, documents, changes, options, userId) {
|
||||||
|
super._onUpdateDescendantDocuments(parent, collection, documents, changes, options, userId);
|
||||||
|
for (const party of this.parties) {
|
||||||
|
party.renderDebounced({ parts: ['partyMembers'] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_onUpdate(changes, options, userId) {
|
_onUpdate(changes, options, userId) {
|
||||||
super._onUpdate(changes, options, userId);
|
super._onUpdate(changes, options, userId);
|
||||||
for (const party of this.parties) {
|
for (const party of this.parties) {
|
||||||
party.render({ parts: ['partyMembers'] });
|
party.renderDebounced({ parts: ['partyMembers'] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -132,17 +159,20 @@ export default class DhpActor extends Actor {
|
||||||
_onDelete(options, userId) {
|
_onDelete(options, userId) {
|
||||||
super._onDelete(options, userId);
|
super._onDelete(options, userId);
|
||||||
for (const party of this.parties) {
|
for (const party of this.parties) {
|
||||||
party.render({ parts: ['partyMembers'] });
|
party.renderDebounced({ parts: ['partyMembers'] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateLevel(newLevel) {
|
async updateLevel(newLevel) {
|
||||||
if (!['character', 'companion'].includes(this.type) || newLevel === this.system.levelData.level.changed) return;
|
if (!['character', 'companion'].includes(this.type) || newLevel === this.system.levelData.level.changed) return;
|
||||||
|
|
||||||
|
const tiers = Object.values(game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers);
|
||||||
|
const maxLevel = tiers.reduce((acc, tier) => Math.max(acc, tier.levels.end), 0);
|
||||||
|
const multiclassMinLevel = Math.min(
|
||||||
|
maxLevel,
|
||||||
|
...tiers.filter(t => t.options.multiclass).map(t => t.levels.start)
|
||||||
|
);
|
||||||
if (newLevel > this.system.levelData.level.current) {
|
if (newLevel > this.system.levelData.level.current) {
|
||||||
const maxLevel = Object.values(
|
|
||||||
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers
|
|
||||||
).reduce((acc, tier) => Math.max(acc, tier.levels.end), 0);
|
|
||||||
if (newLevel > maxLevel) {
|
if (newLevel > maxLevel) {
|
||||||
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.tooHighLevel'));
|
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.tooHighLevel'));
|
||||||
}
|
}
|
||||||
|
|
@ -217,18 +247,19 @@ export default class DhpActor extends Actor {
|
||||||
this.system.multiclass.subclass.update({ 'system.featureState': subclassFeatureState.multiclass });
|
this.system.multiclass.subclass.update({ 'system.featureState': subclassFeatureState.multiclass });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (multiclass) {
|
// Remove multiclass if we're removing a multiclass feature or if we're below the multiclass minimum level
|
||||||
const multiclassItem = this.items.find(x => x.uuid === multiclass.itemUuid);
|
// Multclasses cannot be manually removed on the sheet, so this allows recovering in the case of errors
|
||||||
const multiclassFeatures = this.items.filter(
|
if (multiclass || newLevel < multiclassMinLevel) {
|
||||||
x => x.system.originItemType === 'class' && x.system.multiclassOrigin
|
const multiclassItems = this.items.filter(
|
||||||
);
|
x =>
|
||||||
const subclassFeatures = this.items.filter(
|
x.uuid === multiclass?.itemUuid ||
|
||||||
x => x.system.originItemType === 'subclass' && x.system.multiclassOrigin
|
x.system.isMulticlass ||
|
||||||
|
(['class', 'subclass'].includes(x.system.originItemType) && x.system.multiclassOrigin)
|
||||||
);
|
);
|
||||||
|
|
||||||
this.deleteEmbeddedDocuments(
|
this.deleteEmbeddedDocuments(
|
||||||
'Item',
|
'Item',
|
||||||
[multiclassItem, ...multiclassFeatures, ...subclassFeatures].map(x => x.id)
|
multiclassItems.map(x => x.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
this.update({
|
this.update({
|
||||||
|
|
@ -267,6 +298,7 @@ export default class DhpActor extends Actor {
|
||||||
|
|
||||||
async levelUp(levelupData) {
|
async levelUp(levelupData) {
|
||||||
const levelupAuto = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto;
|
const levelupAuto = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto;
|
||||||
|
const getStatsWithSource = document => ({ ...(document._stats ?? {}), compendiumSource: document.uuid });
|
||||||
|
|
||||||
const levelups = {};
|
const levelups = {};
|
||||||
for (var levelKey of Object.keys(levelupData)) {
|
for (var levelKey of Object.keys(levelupData)) {
|
||||||
|
|
@ -379,8 +411,8 @@ export default class DhpActor extends Actor {
|
||||||
const embeddedItem = await this.createEmbeddedDocuments('Item', [
|
const embeddedItem = await this.createEmbeddedDocuments('Item', [
|
||||||
{
|
{
|
||||||
...multiclassData,
|
...multiclassData,
|
||||||
uuid: multiclassItem.uuid,
|
uuid: multiclassItem.uuid, // todo: replace with setting an id and using keepId
|
||||||
_stats: multiclassItem._stats,
|
_stats: getStatsWithSource(multiclassItem),
|
||||||
system: {
|
system: {
|
||||||
...multiclassData.system,
|
...multiclassData.system,
|
||||||
features: multiclassData.system.features.filter(x => x.type !== 'hope'),
|
features: multiclassData.system.features.filter(x => x.type !== 'hope'),
|
||||||
|
|
@ -393,8 +425,8 @@ export default class DhpActor extends Actor {
|
||||||
await this.createEmbeddedDocuments('Item', [
|
await this.createEmbeddedDocuments('Item', [
|
||||||
{
|
{
|
||||||
...subclassData,
|
...subclassData,
|
||||||
uuid: subclassItem.uuid,
|
uuid: subclassItem.uuid, // todo: replace with setting an id and using keepId
|
||||||
_stats: subclassItem._stats,
|
_stats: getStatsWithSource(subclassItem),
|
||||||
system: {
|
system: {
|
||||||
...subclassData.system,
|
...subclassData.system,
|
||||||
isMulticlass: true
|
isMulticlass: true
|
||||||
|
|
@ -414,8 +446,8 @@ export default class DhpActor extends Actor {
|
||||||
const embeddedItem = await this.createEmbeddedDocuments('Item', [
|
const embeddedItem = await this.createEmbeddedDocuments('Item', [
|
||||||
{
|
{
|
||||||
...cardData,
|
...cardData,
|
||||||
uuid: cardItem.uuid,
|
uuid: cardItem.uuid, // todo: replace with setting an id and using keepId
|
||||||
_stats: cardItem._stats,
|
_stats: getStatsWithSource(cardItem),
|
||||||
system: {
|
system: {
|
||||||
...cardData.system,
|
...cardData.system,
|
||||||
inVault: true
|
inVault: true
|
||||||
|
|
@ -436,8 +468,7 @@ export default class DhpActor extends Actor {
|
||||||
const embeddedItem = await this.createEmbeddedDocuments('Item', [
|
const embeddedItem = await this.createEmbeddedDocuments('Item', [
|
||||||
{
|
{
|
||||||
...cardData,
|
...cardData,
|
||||||
uuid: cardItem.uuid,
|
_stats: getStatsWithSource(cardItem),
|
||||||
_stats: cardItem._stats,
|
|
||||||
system: {
|
system: {
|
||||||
...cardData.system,
|
...cardData.system,
|
||||||
inVault: true
|
inVault: true
|
||||||
|
|
@ -593,10 +624,7 @@ export default class DhpActor extends Actor {
|
||||||
|
|
||||||
/**@inheritdoc */
|
/**@inheritdoc */
|
||||||
getRollData() {
|
getRollData() {
|
||||||
const rollData = foundry.utils.deepClone(super.getRollData());
|
const rollData = createShallowProxy(super.getRollData());
|
||||||
/* system gets repeated infinately which causes issues when trying to use the data for document creation */
|
|
||||||
delete rollData.system;
|
|
||||||
|
|
||||||
rollData.id = this.id;
|
rollData.id = this.id;
|
||||||
rollData.name = this.name;
|
rollData.name = this.name;
|
||||||
rollData.system = this.system.getRollData();
|
rollData.system = this.system.getRollData();
|
||||||
|
|
@ -828,7 +856,7 @@ export default class DhpActor extends Actor {
|
||||||
const u = updates[key];
|
const u = updates[key];
|
||||||
if (key === 'items') {
|
if (key === 'items') {
|
||||||
Object.values(u).forEach(async item => {
|
Object.values(u).forEach(async item => {
|
||||||
await emitAsGM(
|
await emitGMUpdate(
|
||||||
GMUpdateEvent.UpdateDocument,
|
GMUpdateEvent.UpdateDocument,
|
||||||
item.target.update.bind(item.target),
|
item.target.update.bind(item.target),
|
||||||
item.resources,
|
item.resources,
|
||||||
|
|
@ -837,7 +865,7 @@ export default class DhpActor extends Actor {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (Object.keys(u.resources).length > 0) {
|
if (Object.keys(u.resources).length > 0) {
|
||||||
await emitAsGM(
|
await emitGMUpdate(
|
||||||
GMUpdateEvent.UpdateDocument,
|
GMUpdateEvent.UpdateDocument,
|
||||||
u.target.update.bind(u.target),
|
u.target.update.bind(u.target),
|
||||||
u.resources,
|
u.resources,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { emitAsGM, GMUpdateEvent } from '../systemRegistration/socket.mjs';
|
import { emitGMUpdate, emitGMCreate, GMUpdateEvent } from '../systemRegistration/socket.mjs';
|
||||||
|
|
||||||
export default class DhpChatMessage extends foundry.documents.ChatMessage {
|
export default class DhpChatMessage extends foundry.documents.ChatMessage {
|
||||||
targetHook = null;
|
targetHook = null;
|
||||||
|
|
@ -183,7 +183,11 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
|
||||||
if (pendingingSaves.length) {
|
if (pendingingSaves.length) {
|
||||||
const confirm = await foundry.applications.api.DialogV2.confirm({
|
const confirm = await foundry.applications.api.DialogV2.confirm({
|
||||||
window: { title: game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.title') },
|
window: { title: game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.title') },
|
||||||
content: `<p>${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.unfinishedRolls')}</p><p>${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.confirmation')}</p><p><i>${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.warning')}</i></p>`
|
content: `
|
||||||
|
<p>${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.unfinishedRolls')}</p>
|
||||||
|
<p><i>${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.warning')}</i></p>
|
||||||
|
<p>${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.confirmation')}</p>
|
||||||
|
`
|
||||||
});
|
});
|
||||||
if (!confirm) return;
|
if (!confirm) return;
|
||||||
}
|
}
|
||||||
|
|
@ -214,7 +218,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
|
||||||
const action = this.system.action;
|
const action = this.system.action;
|
||||||
if (!action || !action?.hasSave) return;
|
if (!action || !action?.hasSave) return;
|
||||||
game.system.api.fields.ActionFields.SaveField.rollSave.call(action, token.actor, event).then(result =>
|
game.system.api.fields.ActionFields.SaveField.rollSave.call(action, token.actor, event).then(result =>
|
||||||
emitAsGM(
|
emitGMUpdate(
|
||||||
GMUpdateEvent.UpdateSaveMessage,
|
GMUpdateEvent.UpdateSaveMessage,
|
||||||
game.system.api.fields.ActionFields.SaveField.updateSaveMessage.bind(
|
game.system.api.fields.ActionFields.SaveField.updateSaveMessage.bind(
|
||||||
action,
|
action,
|
||||||
|
|
@ -247,8 +251,24 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
|
||||||
const targets = this.filterPermTargets(this.system.hitTargets),
|
const targets = this.filterPermTargets(this.system.hitTargets),
|
||||||
config = foundry.utils.deepClone(this.system);
|
config = foundry.utils.deepClone(this.system);
|
||||||
config.event = event;
|
config.event = event;
|
||||||
|
|
||||||
if (targets.length === 0)
|
if (targets.length === 0)
|
||||||
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelectedOrPerm'));
|
return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelectedOrPerm'));
|
||||||
|
else if (config.hasSave) {
|
||||||
|
const pendingingSaves = targets.filter(t => t.saved.success === null);
|
||||||
|
if (pendingingSaves.length) {
|
||||||
|
const confirm = await foundry.applications.api.DialogV2.confirm({
|
||||||
|
window: { title: game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.title') },
|
||||||
|
content: `
|
||||||
|
<p>${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.unfinishedRolls')}</p>
|
||||||
|
<p><i>${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.warning')}</i></p>
|
||||||
|
<p>${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.confirmation')}</p>
|
||||||
|
`
|
||||||
|
});
|
||||||
|
if (!confirm) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.consumeOnSuccess();
|
this.consumeOnSuccess();
|
||||||
this.system.action?.workflow.get('effects')?.execute(config, targets, true);
|
this.system.action?.workflow.get('effects')?.execute(config, targets, true);
|
||||||
}
|
}
|
||||||
|
|
@ -259,12 +279,17 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
|
||||||
const { shape: type, size: range } = selectedArea;
|
const { shape: type, size: range } = selectedArea;
|
||||||
const shapeData = CONFIG.Canvas.layers.regions.layerClass.getTemplateShape({ type, range });
|
const shapeData = CONFIG.Canvas.layers.regions.layerClass.getTemplateShape({ type, range });
|
||||||
|
|
||||||
await canvas.regions.placeRegion(
|
const scene = game.scenes.get(game.user.viewedScene);
|
||||||
{
|
const level = scene.levels.find(x => x.isView);
|
||||||
|
|
||||||
|
const regionData = {
|
||||||
name: selectedArea.name,
|
name: selectedArea.name,
|
||||||
|
levels: level ? [level.id] : [],
|
||||||
shapes: [shapeData],
|
shapes: [shapeData],
|
||||||
restriction: { enabled: false, type: 'move', priority: 0 },
|
restriction: { enabled: false, type: 'move', priority: 0 },
|
||||||
behaviors: [
|
behaviors:
|
||||||
|
effects.length > 0
|
||||||
|
? [
|
||||||
{
|
{
|
||||||
name: game.i18n.localize('TYPES.RegionBehavior.applyActiveEffect'),
|
name: game.i18n.localize('TYPES.RegionBehavior.applyActiveEffect'),
|
||||||
type: 'applyActiveEffect',
|
type: 'applyActiveEffect',
|
||||||
|
|
@ -272,14 +297,29 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
|
||||||
effects: effects
|
effects: effects
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
|
: [],
|
||||||
displayMeasurements: true,
|
displayMeasurements: true,
|
||||||
locked: false,
|
locked: false,
|
||||||
ownership: { default: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE },
|
ownership: { default: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE },
|
||||||
visibility: CONST.REGION_VISIBILITY.ALWAYS
|
visibility: CONST.REGION_VISIBILITY.ALWAYS
|
||||||
},
|
};
|
||||||
{ create: true }
|
const placeRegion = data => {
|
||||||
|
canvas.regions.placeRegion(data, { create: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Regions with effects must be placed by the GM
|
||||||
|
if (effects.length > 0 && !game.user.isGM) {
|
||||||
|
if (!game.users.activeGM)
|
||||||
|
return ui.notifications.error(
|
||||||
|
game.i18n.localize('DAGGERHEART.UI.Notifications.behaviorRegionRequiresGM')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const region = await canvas.regions.placeRegion(regionData, { create: false });
|
||||||
|
emitGMCreate('Region', placeRegion, region, scene.id);
|
||||||
|
} else {
|
||||||
|
placeRegion(regionData);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.system.action.areas.length === 1) createArea(this.system.action.areas[0]);
|
if (this.system.action.areas.length === 1) createArea(this.system.action.areas[0]);
|
||||||
|
|
|
||||||
|
|
@ -54,13 +54,7 @@ export default class DHItem extends foundry.documents.Item {
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
getRollData(options = {}) {
|
getRollData(options = {}) {
|
||||||
let data;
|
let data = this.system.getRollData(options);
|
||||||
if (this.system.getRollData) data = this.system.getRollData(options);
|
|
||||||
else {
|
|
||||||
const actorRollData = this.actor?.getRollData(options) ?? {};
|
|
||||||
data = { ...actorRollData, item: { ...this.system } };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data?.item) {
|
if (data?.item) {
|
||||||
data.item.flags = { ...this.flags };
|
data.item.flags = { ...this.flags };
|
||||||
data.item.name = this.name;
|
data.item.name = this.name;
|
||||||
|
|
@ -79,13 +73,16 @@ export default class DHItem extends foundry.documents.Item {
|
||||||
/** Returns true if the item can be used */
|
/** Returns true if the item can be used */
|
||||||
get usable() {
|
get usable() {
|
||||||
const actor = this.actor;
|
const actor = this.actor;
|
||||||
const actionsList = this.system.actionsList;
|
const pack = actor?.pack ? game.packs.get(actor.pack) : null;
|
||||||
return this.isOwner && actor?.type === 'character' && (actionsList?.size || actionsList?.length);
|
const hasActions = this.system.actionsList?.size || this.system.actionsList?.length;
|
||||||
|
const isValidType = actor?.type === 'character' || this.type === 'feature';
|
||||||
|
return !pack?.locked && this.isOwner && isValidType && hasActions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
static async createDialog(data = {}, createOptions = {}, options = {}) {
|
static async createDialog(data = {}, createOptions = {}, options = {}) {
|
||||||
const { folders, types, template, context = {}, ...dialogOptions } = options;
|
const { folders, types, template, context = {}, ...dialogOptions } = options;
|
||||||
|
dialogOptions.classes = [options.classes ?? [], 'item-create'].flat(); // handled in hook
|
||||||
|
|
||||||
if (types?.length === 0) {
|
if (types?.length === 0) {
|
||||||
throw new Error('The array of sub-types to restrict to must not be empty.');
|
throw new Error('The array of sub-types to restrict to must not be empty.');
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue