Skip to content

Commit 3a6edd2

Browse files
committed
Added item containers.
1 parent cb82e95 commit 3a6edd2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+595
-187
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Changelog
22

3+
## New in 1.5.0-beta4
4+
5+
- Further sorted Assets compendium into required rating folders.
6+
- Fixed up the chat display of initiative rolls for both individual and group initiative.
7+
- Fixed issue where combat would hang if multiple combatants were deleted from the canvas mid-combat, if initiative type was set to group.
8+
- Added container items.
9+
- If a container is equipped or stowed, all items inside are considered stowed.
10+
- If a container is neither equipped or stowed, all items inside are neither equipped nor stowed.
11+
- This should easily allow for things like dropping backpacks to move more quickly, track things stowed at a home base, and so on.
12+
- Only ordinary items may be containers, but they may be of treasure or personal type if desired.
13+
- There is no logic checking to see if a treasure bag contains only treasure items, so if you throw an item in a treasure bag and your treasure value doesn't go up... that's probably why.
14+
- Ordinary items, weapons, and armor may all be placed in bags. Things like foci and spells shouldn't allow that, though I'm not sure how you would even make that happen.
15+
- Drag an item to somewhere else on the sheet (even another non-bag item) to remove it from the container.
16+
317
## New in 1.5.0-beta3
418

519
- Fixed some more compendium errors.

module/actor/actor-sheet.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export class WwnActorSheet extends ActorSheet {
4141
let div = $(
4242
`<div class="item-summary"><ol class="tag-list">${item.getTags()}</ol><div>${description}</div></div>`
4343
);
44-
li.parents(".item-entry").append(div.hide());
44+
li.parent(".item-entry").append(div.hide());
4545
div.slideDown(200);
4646
}
4747
li.toggleClass("expanded");

module/actor/character-sheet.js

Lines changed: 160 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,26 @@ export class WwnActorSheetCharacter extends WwnActorSheet {
5757
[[], [], [], [], [], [], [], []]
5858
);
5959

60+
// Validate and clean up container relationships
61+
const containers = items.filter(item => item.system.container.isContainer);
62+
const containerIds = new Set(containers.map(item => item.id));
63+
64+
// Check all items for invalid containerIds
65+
const itemsToUpdate = [];
66+
for (const item of this.actor.items) {
67+
if (item.system.containerId && !containerIds.has(item.system.containerId)) {
68+
itemsToUpdate.push({
69+
_id: item.id,
70+
"system.containerId": ""
71+
});
72+
}
73+
}
74+
75+
// Update items with invalid containerIds
76+
if (itemsToUpdate.length > 0) {
77+
this.actor.updateEmbeddedDocuments("Item", itemsToUpdate);
78+
}
79+
6080
// Sort spells by level
6181
var sortedSpells = {};
6282
var slots = {};
@@ -216,6 +236,95 @@ export class WwnActorSheetCharacter extends WwnActorSheet {
216236
});
217237
}
218238

239+
_onDropItem(event, dragData) {
240+
241+
// If the drag originated from outside our sheet, let Foundry handle it
242+
if (!event.target.closest('.wwn.sheet.actor')) {
243+
super._onDropItem(event, dragData);
244+
return;
245+
}
246+
247+
// Get the dragged item from the UUID
248+
const draggedItem = fromUuidSync(dragData.uuid);
249+
if (!draggedItem) return;
250+
251+
// Find the target item under the drop point
252+
const targetElement = event.target.closest('.item');
253+
if (!targetElement) {
254+
// If dropped on empty space, remove from container if it was in one
255+
if (draggedItem.system?.containerId) {
256+
draggedItem.update({
257+
"system.containerId": "",
258+
"system.equipped": false,
259+
"system.stowed": true,
260+
});
261+
}
262+
super._onDropItem(event, dragData);
263+
return;
264+
}
265+
266+
const targetItemId = targetElement.dataset.itemId;
267+
if (!targetItemId) {
268+
// If dropped on something that isn't an item, remove from container if it was in one
269+
if (draggedItem.system?.containerId) {
270+
draggedItem.update({
271+
"system.containerId": "",
272+
"system.equipped": false,
273+
"system.stowed": true,
274+
});
275+
}
276+
super._onDropItem(event, dragData);
277+
return;
278+
}
279+
280+
// Get the target item
281+
const targetItem = this.actor.items.get(targetItemId);
282+
if (!targetItem || !targetItem.system.container.isContainer) {
283+
// If dropped on a non-container item, remove from container if it was in one
284+
if (draggedItem.system?.containerId) {
285+
draggedItem.update({
286+
"system.containerId": "",
287+
"system.equipped": false,
288+
"system.stowed": true
289+
});
290+
}
291+
super._onDropItem(event, dragData);
292+
return;
293+
}
294+
295+
// Only allow certain item types to be added to containers
296+
const allowedTypes = ['item', 'weapon', 'armor'];
297+
if (!allowedTypes.includes(draggedItem.type)) {
298+
super._onDropItem(event, dragData);
299+
return;
300+
}
301+
302+
// Update the dragged item's containerId
303+
draggedItem.system && draggedItem.update({
304+
"system.containerId": targetItemId,
305+
"system.equipped": false,
306+
"system.stowed": targetItem.system.equipped || targetItem.system.stowed,
307+
});
308+
309+
super._onDropItem(event, dragData);
310+
}
311+
312+
async _onContainerItemAdd(item, target) {
313+
const alreadyExistsInActor = target.parent.items.find((i) => i.id === item.id);
314+
let latestItem = item;
315+
if (!alreadyExistsInActor) {
316+
const newItem = await this._onDropItemCreate([item.toObject()]);
317+
latestItem = newItem.pop();
318+
}
319+
320+
const alreadyExistsInContainer = target.system.itemIds.find((i) => i.id === latestItem.id);
321+
if (!alreadyExistsInContainer) {
322+
const newList = [...target.system.itemIds, latestItem.id];
323+
await target.update({ system: { itemIds: newList } });
324+
await latestItem.update({ system: { containerId: target.id } });
325+
}
326+
}
327+
219328
_pushLang(table) {
220329
const data = this.actor.system;
221330
let update = duplicate(data[table]);
@@ -309,6 +418,31 @@ export class WwnActorSheetCharacter extends WwnActorSheet {
309418
}
310419
});
311420

421+
// Container toggle listener
422+
html.find(".inventory .container-arrow").click(async (ev) => {
423+
const container = $(ev.currentTarget).closest('.item-entry');
424+
const itemId = container.find('.item').data('itemId');
425+
const item = this.actor.items.get(itemId);
426+
const items = container.find('.container-items');
427+
428+
if (item && item.system.container.isContainer) {
429+
const isCurrentlyOpen = item.system.container.isOpen;
430+
const icon = $(ev.currentTarget).find('i');
431+
432+
// Animate the container
433+
if (!isCurrentlyOpen) {
434+
items.slideDown(200);
435+
} else {
436+
items.slideUp(200);
437+
}
438+
439+
// Update the item state
440+
await item.update({
441+
"system.container.isOpen": !isCurrentlyOpen
442+
});
443+
}
444+
});
445+
312446
html.find("a[data-action='modifiers']").click((ev) => {
313447
this._onShowModifiers(ev);
314448
});
@@ -351,26 +485,48 @@ export class WwnActorSheetCharacter extends WwnActorSheet {
351485
await item.update({
352486
system: {
353487
equipped: !item.system.equipped,
488+
stowed: item.system.equipped ? item.system.stowed : false,
354489
},
355490
});
491+
492+
// Update contained items
493+
if (item.system.container.isContainer) {
494+
const containedItems = item.actor.items.filter((i) => i.system.containerId === item.id);
495+
item.actor.updateEmbeddedDocuments("Item", containedItems.map((i) => ({
496+
_id: i.id,
497+
"system.equipped": false,
498+
"system.stowed": item.system.equipped || item.system.stowed,
499+
})));
500+
}
356501
});
357502

358-
html.find(".item-prep").click(async (ev) => {
503+
html.find(".stow-toggle").click(async (ev) => {
359504
const li = $(ev.currentTarget).parents(".item");
360505
const item = this.actor.items.get(li.data("itemId"));
361506
await item.update({
362507
system: {
363-
prepared: !item.system.prepared,
508+
equipped: item.system.stowed ? item.system.equipped : false,
509+
stowed: !item.system.stowed,
364510
},
365511
});
512+
513+
// Update contained items
514+
if (item.system.container.isContainer) {
515+
const containedItems = item.actor.items.filter((i) => i.system.containerId === item.id);
516+
item.actor.updateEmbeddedDocuments("Item", containedItems.map((i) => ({
517+
_id: i.id,
518+
"system.equipped": false,
519+
"system.stowed": item.system.equipped || item.system.stowed,
520+
})));
521+
}
366522
});
367523

368-
html.find(".stow-toggle").click(async (ev) => {
524+
html.find(".item-prep").click(async (ev) => {
369525
const li = $(ev.currentTarget).parents(".item");
370526
const item = this.actor.items.get(li.data("itemId"));
371527
await item.update({
372528
system: {
373-
stowed: !item.system.stowed,
529+
prepared: !item.system.prepared,
374530
},
375531
});
376532
});

module/actor/entity.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1268,7 +1268,7 @@ export class WwnActor extends Actor {
12681268
const data = this.system;
12691269
const saves = data.saves;
12701270
const baseSave = this.type === "monster" ? 15 : 16;
1271-
console.log(baseSave);
1271+
12721272
Object.keys(saves).forEach((s) => {
12731273
if (!saves[s].mod) {
12741274
saves[s].mod = 0;

module/combat/combat-group.js

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,27 +42,22 @@ export class WWNGroupCombat extends WWNCombat {
4242
}), {});
4343
const results = await this.#prepareGroupInitiativeDice(rollPerGroup);
4444

45-
// Rewrite to make new roll for each black group combatant, using similar logic to what
46-
// is done in rollPerGroup / #prepareGroupInitiativeDice
47-
4845
const alertCombatants = this.combatants.filter(c => c.group === "black");
49-
const alertResults = {};
46+
this.alertResults = {};
5047
for (const combatant of alertCombatants) {
5148
const combatantData = combatant.token.delta.syntheticActor.system;
5249
const roll = new Roll(`${combatantData.initiative.roll}+${combatantData.initiative.value}`);
5350
const evaluatedRoll = await roll.evaluate();
54-
alertResults[combatant.id] = {
51+
this.alertResults[combatant.id] = {
5552
initiative: evaluatedRoll.total,
5653
roll: evaluatedRoll
5754
};
5855
}
5956

6057
const updates = this.combatants.map(
61-
(c) => ({ _id: c.id, initiative: c.group === "black" ? alertResults[c.id].initiative : results[c.group].initiative })
58+
(c) => ({ _id: c.id, initiative: c.group === "black" ? this.alertResults[c.id].initiative : results[c.group].initiative })
6259
)
6360

64-
65-
6661
await this.updateEmbeddedDocuments("Combatant", updates);
6762
await this.#rollInitiativeUIFeedback(results);
6863
await this.activateCombatant(0);
@@ -73,15 +68,18 @@ export class WWNGroupCombat extends WWNCombat {
7368
const pool = foundry.dice.terms.PoolTerm.fromRolls(Object.values(rollPerGroup));
7469
const evaluatedRolls = await Roll.fromTerms([pool]).roll();
7570
const rollValues = evaluatedRolls.dice.map(d => d.total);
71+
const diceArray = [...evaluatedRolls.dice];
72+
7673
if (this.availableGroups.includes("black")) {
77-
rollValues.splice(this.availableGroups.indexOf('black'), 0, 0)
74+
rollValues.splice(this.availableGroups.indexOf('black'), 0, 0);
75+
diceArray.splice(this.availableGroups.indexOf('black'), 0, null);
7876
}
7977

8078
return this.availableGroups.reduce((prev, curr, i) => ({
8179
...prev,
82-
[[curr]]: {
80+
[curr]: {
8381
initiative: rollValues[i],
84-
roll: evaluatedRolls.dice[i]
82+
roll: diceArray[i]
8583
}
8684
}), {});
8785
}
@@ -104,9 +102,33 @@ export class WWNGroupCombat extends WWNCombat {
104102

105103
#constructInitiativeOutputForGroup(group, roll) {
106104
if (group === "black") {
107-
// TODO: Display individual initiative rolls for black group combatants
105+
const alertCombatants = this.combatants.filter(c => c.group === "black");
106+
return alertCombatants.map(combatant => {
107+
const alertResult = this.alertResults[combatant.id];
108+
if (!alertResult) return '';
109+
110+
return `
111+
<p>${game.i18n.format("WWN.roll.initiative", { group: combatant.name })}
112+
<div class="dice-roll">
113+
<div class="dice-result">
114+
<div class="dice-formula">${alertResult.roll.formula}</div>
115+
<div class="dice-tooltip">
116+
<section class="tooltip-part">
117+
<div class="dice">
118+
<header class="part-header flexrow">
119+
<span class="part-formula">${alertResult.roll.formula}</span>
120+
<span class="part-total">${alertResult.roll.total}</span>
121+
</header>
122+
</div>
123+
</section>
124+
</div>
125+
<h4 class="dice-total">${alertResult.roll.total}</h4>
126+
</div>
127+
</div>
128+
`;
129+
}).join("\n");
108130
} else {
109-
return `
131+
return `
110132
<p>${game.i18n.format("WWN.roll.initiative", { group })}
111133
<div class="dice-roll">
112134
<div class="dice-result">

module/combat/combat.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,52 @@ export class WWNCombat extends Combat {
7777

7878
const updates = this.combatants.map((c) => ({ _id: c.id, initiative: results[c.id].initiative }));
7979
await this.updateEmbeddedDocuments("Combatant", updates);
80+
await this.#rollInitiativeUIFeedback(results);
8081
await this.activateCombatant(0);
8182
return this;
8283
}
8384

85+
async #rollInitiativeUIFeedback(results = {}) {
86+
const content = [
87+
Object.entries(results).map(
88+
([id, result]) => this.#constructInitiativeOutputForIndividual(id, result.roll)
89+
).join("\n")
90+
];
91+
const chatData = content.map(c => {
92+
return {
93+
speaker: { alias: game.i18n.localize("WWN.Initiative") },
94+
sound: CONFIG.sounds.dice,
95+
content: c
96+
};
97+
});
98+
ChatMessage.implementation.createDocuments(chatData);
99+
}
100+
101+
#constructInitiativeOutputForIndividual(id, roll) {
102+
const combatant = this.combatants.get(id);
103+
if (!combatant) return '';
104+
105+
return `
106+
<p>${game.i18n.format("WWN.roll.initiative", { group: combatant.name })}
107+
<div class="dice-roll">
108+
<div class="dice-result">
109+
<div class="dice-formula">${roll.formula}</div>
110+
<div class="dice-tooltip">
111+
<section class="tooltip-part">
112+
<div class="dice">
113+
<header class="part-header flexrow">
114+
<span class="part-formula">${roll.formula}</span>
115+
<span class="part-total">${roll.total}</span>
116+
</header>
117+
</div>
118+
</section>
119+
</div>
120+
<h4 class="dice-total">${roll.total}</h4>
121+
</div>
122+
</div>
123+
`;
124+
}
125+
84126
// ===========================================================================
85127
// Randomize NPC HP
86128
// ===========================================================================

module/combat/combatant-group.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ export class WWNGroupCombatant extends WWNCombatant {
1010
if (assignedGroup)
1111
return assignedGroup;
1212

13-
if (canvas.tokens) {
13+
if (canvas.tokens && this.token) {
1414
const token = canvas.tokens.get(this.token.id);
15+
if (!token) return 'white';
16+
1517
const disposition = token.document.disposition;
1618
const alertTwo = token.document.delta.syntheticActor.system.initiative.alertTwo;
1719
if (alertTwo) return "black";
Binary file not shown.

packs/adventuring-gear/002527.log

2.55 KB
Binary file not shown.

0 commit comments

Comments
 (0)