diff --git a/css/fvtt-prism-rpg.css b/css/fvtt-prism-rpg.css index fabed71..b6d73b2 100644 --- a/css/fvtt-prism-rpg.css +++ b/css/fvtt-prism-rpg.css @@ -54,6 +54,13 @@ i.prismrpg { font-family: var(--font-primary); font-size: calc(var(--font-size-standard) * 1); background-image: var(--background-image-base); + background-size: 100% 100%; + background-repeat: no-repeat; +} +.application.dialog.prismrpg .window-content { + background-image: var(--background-image-base); + background-size: 100% 100%; + background-repeat: no-repeat; } .application.dialog.prismrpg button:hover { background: var(--color-dark-6); @@ -737,6 +744,76 @@ i.prismrpg { font-size: 11px; padding: 4px; } +.prismrpg .tab.character-equipment .main-div .inv-section .inv-container[data-container-id] { + border: 1px dashed transparent; + transition: border-color 0.15s, background 0.15s; +} +.prismrpg .tab.character-equipment .main-div .inv-section .inv-container[data-container-id].drag-over { + border-color: rgba(100, 150, 255, 0.7); + background: rgba(100, 150, 255, 0.12); +} +.prismrpg .tab.character-equipment .main-div .inv-section .inv-container-items { + margin: 2px 0 6px 28px; + display: flex; + flex-direction: column; + gap: 2px; + border-left: 2px solid rgba(0, 0, 0, 0.15); + padding-left: 8px; +} +.prismrpg .tab.character-equipment .main-div .inv-section .inv-container-item { + display: flex; + align-items: center; + gap: 6px; + padding: 2px 4px; + border-radius: 3px; + background: rgba(0, 0, 0, 0.06); +} +.prismrpg .tab.character-equipment .main-div .inv-section .inv-container-item:hover { + background: rgba(0, 0, 0, 0.12); +} +.prismrpg .tab.character-equipment .main-div .inv-section .inv-container-item .item-img { + width: 20px; + height: 20px; + cursor: pointer; + flex-shrink: 0; +} +.prismrpg .tab.character-equipment .main-div .inv-section .inv-container-item .inv-name { + flex: 1; + font-size: 11px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.prismrpg .tab.character-equipment .main-div .inv-section .inv-container-item .inv-enc { + font-size: 10px; + color: #555; + min-width: 24px; + text-align: center; +} +.prismrpg .tab.character-equipment .main-div .inv-section .inv-container-item .inv-container-type-badge { + font-size: 9px; + text-transform: uppercase; + letter-spacing: 0.05em; + color: rgba(0, 0, 0, 0.45); + background: rgba(0, 0, 0, 0.07); + border-radius: 3px; + padding: 1px 4px; + flex-shrink: 0; +} +.prismrpg .tab.character-equipment .main-div .inv-section .inv-container-item .controls { + display: flex; + gap: 4px; +} +.prismrpg .tab.character-equipment .main-div .inv-section .inv-container-item .controls a { + font-size: 11px; + cursor: pointer; +} +.prismrpg .tab.character-equipment .main-div .inv-section .inv-container-empty { + margin: 2px 0 4px 36px; + font-size: 10px; + font-style: italic; + color: rgba(0, 0, 0, 0.35); +} .prismrpg .tab.character-equipment .main-div .pack-burden-fieldset .pack-burden-display { display: flex; align-items: center; @@ -3775,6 +3852,105 @@ i.prismrpg { .prismrpg .character-path-content input[type="checkbox"]:checked::after { color: rgba(0, 0, 0, 0.1); } +.prismrpg .container-content { + font-family: var(--font-primary); + font-size: calc(var(--font-size-standard) * 1); + color: var(--color-dark-1); + background-image: var(--background-image-base); + background-repeat: no-repeat; + background-size: 100% 100%; + overflow: auto; +} +.prismrpg .container-content nav.tabs [data-tab] { + color: #636060; +} +.prismrpg .container-content nav.tabs [data-tab].active { + color: #252424; +} +.prismrpg .container-content input:disabled, +.prismrpg .container-content select:disabled { + background-color: rgba(0, 0, 0, 0.2); + border-color: transparent; + color: var(--color-dark-3); +} +.prismrpg .container-content input, +.prismrpg .container-content select { + height: 1.5rem; + background-color: rgba(0, 0, 0, 0.1); + border-color: var(--color-dark-6); + color: var(--color-dark-2); +} +.prismrpg .container-content input[name="name"] { + height: 2.5rem; + margin-right: 4px; + font-family: var(--font-secondary); + font-size: calc(var(--font-size-standard) * 1.2); + font-weight: bold; + border: none; +} +.prismrpg .container-content fieldset { + margin-bottom: 4px; + border-radius: 4px; +} +.prismrpg .container-content .form-fields input, +.prismrpg .container-content .form-fields select { + text-align: center; + font-size: calc(var(--font-size-standard) * 1); +} +.prismrpg .container-content .form-fields select { + font-family: var(--font-secondary); + font-size: calc(var(--font-size-standard) * 1); +} +.prismrpg .container-content legend { + font-family: var(--font-secondary); + font-size: calc(var(--font-size-standard) * 1.2); + font-weight: bold; + letter-spacing: 1px; +} +.prismrpg .container-content .form-fields { + padding-top: 4px; +} +.prismrpg .container-content .form-group { + display: flex; + flex: 1; + flex-direction: row; +} +.prismrpg .container-content .form-group label { + align-content: center; + min-width: 10rem; + max-width: 10rem; +} +.prismrpg .container-content .form-group select, +.prismrpg .container-content .form-group input { + text-align: left; + min-width: 12rem; + max-width: 12rem; +} +.prismrpg .container-content .form-group input[type="checkbox"] { + min-width: 1.2rem; + max-width: 1.2rem; + margin-right: 0.5rem; +} +.prismrpg .container-content label { + font-family: var(--font-secondary); + font-size: calc(var(--font-size-standard) * 1); + flex: 50%; +} +.prismrpg .container-content .align-top { + align-self: flex-start; + padding: 0.1rem; + margin-right: 0.2rem; +} +.prismrpg .container-content .shift-right { + margin-left: 2rem; +} +.prismrpg .container-content .header { + display: flex; +} +.prismrpg .container-content .header img { + width: 50px; + height: 50px; +} .prismrpg .container-content .item-img { width: 64px; height: 64px; @@ -5147,15 +5323,15 @@ i.prismrpg { .prismrpg-roll-dialog-modern .checkbox-group .checkbox-label input[type="checkbox"]:checked ~ .checkbox-text i { color: #d4af37; } -.application.dialog.prismrpg .window-content { +.application.dialog.prismrpg:has(.prismrpg-roll-dialog-modern) .window-content { background: linear-gradient(135deg, #f5f5f5 0%, #e0e0e0 100%); padding: 8px; } -.application.dialog.prismrpg .dialog-buttons { +.application.dialog.prismrpg:has(.prismrpg-roll-dialog-modern) .dialog-buttons { padding: 6px 8px; gap: 6px; } -.application.dialog.prismrpg .dialog-buttons button { +.application.dialog.prismrpg:has(.prismrpg-roll-dialog-modern) .dialog-buttons button { background: linear-gradient(135deg, #4a4a4a 0%, #6a6a6a 100%); border: 1px solid #3a3a3a; color: white; @@ -5164,19 +5340,19 @@ i.prismrpg { border-radius: 4px; transition: all 0.2s ease; } -.application.dialog.prismrpg .dialog-buttons button:hover { +.application.dialog.prismrpg:has(.prismrpg-roll-dialog-modern) .dialog-buttons button:hover { background: linear-gradient(135deg, #5a5a5a 0%, #7a7a7a 100%); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); transform: translateY(-1px); } -.application.dialog.prismrpg .dialog-buttons button.default, -.application.dialog.prismrpg .dialog-buttons button[data-button="roll"] { +.application.dialog.prismrpg:has(.prismrpg-roll-dialog-modern) .dialog-buttons button.default, +.application.dialog.prismrpg:has(.prismrpg-roll-dialog-modern) .dialog-buttons button[data-button="roll"] { background: linear-gradient(135deg, #d4af37 0%, #f4cf67 100%); border-color: #b49030; color: #2a2a2a; } -.application.dialog.prismrpg .dialog-buttons button.default:hover, -.application.dialog.prismrpg .dialog-buttons button[data-button="roll"]:hover { +.application.dialog.prismrpg:has(.prismrpg-roll-dialog-modern) .dialog-buttons button.default:hover, +.application.dialog.prismrpg:has(.prismrpg-roll-dialog-modern) .dialog-buttons button[data-button="roll"]:hover { background: linear-gradient(135deg, #e4bf47 0%, #ffdf77 100%); } #token-hud .hp-loss-wrap { diff --git a/lang/en.json b/lang/en.json index 326fb3f..0953230 100644 --- a/lang/en.json +++ b/lang/en.json @@ -940,12 +940,16 @@ "addContainer": "Add container", "addRacialAbility": "Add racial ability", "addAbility": "Add ability", - "excessBurden": "Equipped burden exceeds max — excess reduces Movement Rating" + "excessBurden": "Equipped burden exceeds max — excess reduces Movement Rating", + "assignToContainer": "Assign to container", + "removeFromContainer": "Remove from container", + "packBurden": "Pack Burden" }, "RollSavingThrow": "Roll Saving Throw", "Dialog": { "useConsumable": "Use Consumable", - "useConsumableContent": "Use one charge of {name}? ({uses} remaining)" + "useConsumableContent": "Use one charge of {name}? ({uses} remaining)", + "assignToContainer": "Assign to Container" }, "Message": { "selectCoreSkill": "You must select a Core Skill for your character. Each character chooses one Core Skill at creation.", @@ -957,7 +961,8 @@ "noWeapons": "No weapons", "noArmor": "No armor or shields", "noKits": "No kits", - "noEquipment": "No equipment" + "noEquipment": "No equipment", + "noStoredItems": "Nothing stored" }, "Miracle": { "FIELDS": { diff --git a/module/applications/sheets/character-sheet.mjs b/module/applications/sheets/character-sheet.mjs index c2405c3..cc05a42 100644 --- a/module/applications/sheets/character-sheet.mjs +++ b/module/applications/sheets/character-sheet.mjs @@ -32,6 +32,8 @@ export default class PrismRPGCharacterSheet extends PrismRPGActorSheet { useConsumable: PrismRPGCharacterSheet.#onUseConsumable, toggleContainerEquipped: PrismRPGCharacterSheet.#onToggleContainerEquipped, toggleEquipped: PrismRPGCharacterSheet.#onToggleEquipped, + assignToContainer: PrismRPGCharacterSheet.#onAssignToContainer, + removeFromContainer: PrismRPGCharacterSheet.#onRemoveFromContainer, }, } @@ -162,25 +164,45 @@ export default class PrismRPGCharacterSheet extends PrismRPGActorSheet { break case "equipment": context.tab = context.tabs.equipment - context.weapons = doc.itemTypes.weapon - context.armors = [...doc.itemTypes.armor, ...doc.itemTypes.shield] - context.consumables = doc.itemTypes.consumable - context.kits = doc.itemTypes.equipment.filter(i => i.system.isKit) - context.equipmentItems = doc.itemTypes.equipment.filter(i => !i.system.isKit) - context.loots = doc.itemTypes.loot - context.containers = doc.itemTypes.container - context.packBurdenMax = doc.itemTypes.container - .filter(c => c.system.equipped) - .reduce((sum, c) => sum + (c.system.packBurden ?? 0), 0) - context.packBurdenUsed = [ - ...doc.itemTypes.equipment, - ...doc.itemTypes.consumable, - ...doc.itemTypes.loot, - ...doc.itemTypes.container, + // All items that can be stored in containers + const allStorable = [ ...doc.itemTypes.weapon, ...doc.itemTypes.armor, ...doc.itemTypes.shield, - ].reduce((sum, i) => sum + (i.system.encLoad ?? 0), 0) + ...doc.itemTypes.equipment, + ...doc.itemTypes.consumable, + ...doc.itemTypes.loot, + ] + // Build a map: containerId → items[] + const containerGroups = {} + for (const container of doc.itemTypes.container) { + containerGroups[container.id] = { container, items: [] } + } + for (const item of allStorable) { + const cid = item.system.containerId + if (cid && containerGroups[cid]) { + containerGroups[cid].items.push(item) + } + } + context.containerGroups = Object.values(containerGroups) + + // Items are "uncontained" if they have no containerId, or if their container was deleted + const isUncontained = i => !i.system.containerId || !containerGroups[i.system.containerId] + context.weapons = doc.itemTypes.weapon.filter(isUncontained) + context.armors = [...doc.itemTypes.armor, ...doc.itemTypes.shield].filter(isUncontained) + context.consumables = doc.itemTypes.consumable.filter(isUncontained) + context.kits = doc.itemTypes.equipment.filter(i => i.system.isKit && isUncontained(i)) + context.equipmentItems = doc.itemTypes.equipment.filter(i => !i.system.isKit && isUncontained(i)) + context.loots = doc.itemTypes.loot.filter(isUncontained) + context.containers = doc.itemTypes.container + + context.packBurdenMax = doc.itemTypes.container + .filter(c => c.system.equipped) + .reduce((sum, c) => sum + (c.system.packBurden ?? 0), 0) + // Pack burden = items stored in an existing container + context.packBurdenUsed = allStorable + .filter(i => i.system.containerId && containerGroups[i.system.containerId]) + .reduce((sum, i) => sum + (i.system.encLoad ?? 0), 0) break case "biography": context.tab = context.tabs.biography @@ -205,6 +227,18 @@ export default class PrismRPGCharacterSheet extends PrismRPGActorSheet { // Handle different data types if (data.type === "Item") { const item = await fromUuid(data.uuid) + + // Check if dropped onto a container row + const containerEl = event.target.closest("[data-container-id]") + if (containerEl && item?.parent === this.document) { + const containerId = containerEl.dataset.containerId + // Don't store containers inside containers + if (item.type !== "container") { + await item.update({ "system.containerId": containerId }) + return + } + } + return this._onDropItem(item) } } @@ -331,6 +365,48 @@ export default class PrismRPGCharacterSheet extends PrismRPGActorSheet { await item.update({ "system.equipped": !item.system.equipped }) } + static async #onAssignToContainer(event, target) { + const itemElement = target.closest("[data-item-id]") + if (!itemElement) return + const item = this.document.items.get(itemElement.dataset.itemId) + if (!item || item.type === "container") return + + const containers = this.document.itemTypes.container + if (!containers.length) { + ui.notifications.warn(game.i18n.localize("PRISMRPG.Message.noContainers")) + return + } + + const options = containers.map(c => { + const escapedName = foundry.utils.escapeHTML(c.name) + return `` + }).join("") + const content = `
{{localize "PRISMRPG.Message.noContainers"}}
{{/unless}} + {{#unless containerGroups.length}}{{localize "PRISMRPG.Message.noContainers"}}
{{/unless}}