const { HandlebarsApplicationMixin } = foundry.applications.api export default class CelestopolActorSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2) { static SHEET_MODES = { EDIT: 0, PLAY: 1 } constructor(options = {}) { super(options) this.#dragDrop = this.#createDragDropHandlers() } #dragDrop /** @override */ static DEFAULT_OPTIONS = { classes: ["fvtt-celestopol", "actor"], position: { width: 900, height: "auto" }, form: { submitOnChange: true }, window: { resizable: true }, dragDrop: [{ dragSelector: '[data-drag="true"], .rollable', dropSelector: null }], actions: { editImage: CelestopolActorSheet.#onEditImage, toggleSheet: CelestopolActorSheet.#onToggleSheet, edit: CelestopolActorSheet.#onItemEdit, delete: CelestopolActorSheet.#onItemDelete, }, } _sheetMode = this.constructor.SHEET_MODES.PLAY get isPlayMode() { return this._sheetMode === this.constructor.SHEET_MODES.PLAY } get isEditMode() { return this._sheetMode === this.constructor.SHEET_MODES.EDIT } /** @override */ async _prepareContext() { return { fields: this.document.schema.fields, systemFields: this.document.system.schema.fields, actor: this.document, system: this.document.system, source: this.document.toObject(), isEditMode: this.isEditMode, isPlayMode: this.isPlayMode, isEditable: this.isEditable, } } /** @override */ _onRender(context, options) { this.#dragDrop.forEach(d => d.bind(this.element)) this.element.querySelectorAll(".rollable").forEach(el => { el.addEventListener("click", this._onRoll.bind(this)) }) // Setup sequential checkbox logic for wound tracks this._setupSequentialCheckboxes() // Setup sequential checkbox logic for factions this._setupFactionCheckboxes() } /** @override */ _onClick(event) { // Skip checkbox clicks in edit mode if (this.isEditMode && event.target.classList.contains('skill-level-checkbox')) { return } super._onClick(event) } async _onRoll(event) { // Don't roll if clicking on a checkbox if (event.target.classList.contains('skill-level-checkbox')) { return } if (!this.isPlayMode) return const el = event.currentTarget const statId = el.dataset.statId const skillId = el.dataset.skillId if (!statId || !skillId) return await this.document.system.roll(statId, skillId) } #createDragDropHandlers() { return this.options.dragDrop.map(d => { d.permissions = { dragstart: this._canDragStart.bind(this), drop: this._canDragDrop.bind(this), } d.callbacks = { dragover: this._onDragOver.bind(this), drop: this._onDrop.bind(this), } return new foundry.applications.ux.DragDrop.implementation(d) }) } _canDragStart() { return this.isEditable } _canDragDrop() { return true } _onDragOver() {} async _onDrop(event) { if (!this.isEditable) return const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event) if (data.type === "Item") { const item = await fromUuid(data.uuid) if (item) return this._onDropItem(item) } } async _onDropItem(item) { if (item.type === "anomaly" && this.document.itemTypes.anomaly.length > 0) { ui.notifications.warn(game.i18n.localize("CELESTOPOL.Anomaly.maxAnomaly")) return } await this.document.createEmbeddedDocuments("Item", [item.toObject()], { renderSheet: false }) } static async #onEditImage(event, _target) { const current = this.document.img const fp = new FilePicker({ current, type: "image", callback: (path) => this.document.update({ img: path }), top: this.position.top + 40, left: this.position.left + 10, }) return fp.browse() } static #onToggleSheet() { const modes = this.constructor.SHEET_MODES this._sheetMode = this.isEditMode ? modes.PLAY : modes.EDIT this.render() } static async #onItemEdit(event, target) { const uuid = target.getAttribute("data-item-uuid") const id = target.getAttribute("data-item-id") const item = uuid ? await fromUuid(uuid) : this.document.items.get(id) item?.sheet.render(true) } static async #onItemDelete(event, target) { const uuid = target.getAttribute("data-item-uuid") const item = await fromUuid(uuid) await item?.deleteDialog() } /** * Setup sequential checkbox logic for wound/destin/spleen tracks * Only allows checking the next checkbox in sequence */ _setupSequentialCheckboxes() { this.element.querySelectorAll('.wound-checkbox').forEach(checkbox => { checkbox.addEventListener('change', (event) => { this._handleSequentialCheckboxChange(event) }) }) } /** * Handle sequential checkbox change logic * @param {Event} event - The change event */ _handleSequentialCheckboxChange(event) { const checkbox = event.target if (!checkbox.classList.contains('wound-checkbox') || checkbox.disabled) return const track = checkbox.dataset.track const currentIndex = parseInt(checkbox.dataset.index) const isChecked = checkbox.checked // Get all checkboxes in this track const trackCheckboxes = Array.from(this.element.querySelectorAll(`.wound-checkbox[data-track="${track}"]`)) if (isChecked) { // Checking a box: uncheck all boxes after this one for (let i = currentIndex + 1; i < trackCheckboxes.length; i++) { trackCheckboxes[i].checked = false } // Check all boxes before this one for (let i = 0; i < currentIndex; i++) { trackCheckboxes[i].checked = true } } else { // Unchecking a box: uncheck all boxes after this one for (let i = currentIndex; i < trackCheckboxes.length; i++) { trackCheckboxes[i].checked = false } } // Update the visual state this._updateTrackVisualState() } /** * Update visual state of track boxes based on checkbox states */ _updateTrackVisualState() { this.element.querySelectorAll('.track-box').forEach(box => { const checkbox = box.querySelector('.wound-checkbox') if (checkbox) { if (checkbox.checked) { box.classList.add('checked') } else { box.classList.remove('checked') } } }) } /** * Setup sequential checkbox logic for faction tracks */ _setupFactionCheckboxes() { this.element.querySelectorAll('.faction-checkbox').forEach(checkbox => { checkbox.addEventListener('change', (event) => { this._handleFactionCheckboxChange(event) }) }) } /** * Handle faction checkbox change logic * @param {Event} event - The change event */ _handleFactionCheckboxChange(event) { const checkbox = event.target if (!checkbox.classList.contains('faction-checkbox') || checkbox.disabled) return const factionId = checkbox.dataset.faction const currentLevel = parseInt(checkbox.dataset.level) const isChecked = checkbox.checked // Get all checkboxes for this faction const factionCheckboxes = Array.from(this.element.querySelectorAll(`.faction-checkbox[data-faction="${factionId}"]`)) if (isChecked) { // Checking a box: check all boxes before this one, uncheck all boxes after this one for (let i = 0; i < currentLevel; i++) { factionCheckboxes[i].checked = true } for (let i = currentLevel; i < factionCheckboxes.length; i++) { factionCheckboxes[i].checked = false } } else { // Unchecking a box: uncheck all boxes after this one for (let i = currentLevel - 1; i < factionCheckboxes.length; i++) { factionCheckboxes[i].checked = false } } // Update the count display this._updateFactionCount(factionId) } /** * Update the faction count display based on checked checkboxes * @param {string} factionId - The faction ID */ _updateFactionCount(factionId) { const checkboxes = Array.from(this.element.querySelectorAll(`.faction-checkbox[data-faction="${factionId}"]:checked`)) const count = checkboxes.length // Update the hidden input field const input = this.element.querySelector(`input[name="system.factions.${factionId}.value"]`) if (input) { input.value = count } // Update the visual count display const countDisplay = this.element.querySelector(`.faction-row[data-faction="${factionId}"] .faction-count`) if (countDisplay) { countDisplay.textContent = count } } }