Files
2026-03-31 17:00:15 +01:00

178 lines
6.4 KiB
JavaScript

// portrait-dialog.mjs — Portrait Picker dialog (Foundry v11/v12/v13 compatible)
import { MODULE_ID } from './settings.mjs';
// The categories available in the picker UI, with display labels.
// Three broad aesthetic types: human | mutant | animal
const CATEGORIES = [
{ value: 'any', label: 'All Portraits (799)' },
// ── Human-presenting ──────────────────────────────
{ value: 'human_female', label: 'Human — Female (97)' },
{ value: 'human_male', label: 'Human — Male (98)' },
{ value: 'human_unknown', label: 'Human — Unknown gender (367)' },
// ── Mutant / altered humans ────────────────────────
{ value: 'mutant_female', label: 'Mutant — Female (41)' },
{ value: 'mutant_male', label: 'Mutant — Male (41)' },
{ value: 'mutant_unknown', label: 'Mutant — Unknown gender (57)' },
// ── Non-human / alien ─────────────────────────────
{ value: 'animal_female', label: 'Animal / Alien — Female (38)' },
{ value: 'animal_male', label: 'Animal / Alien — Male (36)' },
{ value: 'animal_unknown', label: 'Animal / Alien — Unknown (24)' },
];
// ---------------------------------------------------------------------------
// Foundry v13 uses ApplicationV2; v11/v12 uses FormApplication.
// We detect which is available and subclass accordingly.
// ---------------------------------------------------------------------------
function buildPickerClass() {
// Foundry v13+ — ApplicationV2 available
if (foundry.applications?.api?.ApplicationV2) {
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
return class PortraitPickerDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(actor, manager, opts = {}) {
super(opts);
this.actor = actor;
this.manager = manager;
this._selectedCategory = game.settings.get(MODULE_ID, 'defaultCategory') ?? 'any';
}
static DEFAULT_OPTIONS = {
id: 'npc-portrait-picker',
tag: 'form',
classes: ['npc-portrait-pack', 'portrait-picker'],
window: {
title: 'NPC Portrait Picker',
resizable: true,
},
position: { width: 720, height: 580 },
actions: {
selectPortrait: PortraitPickerDialog._onSelectPortrait,
randomPortrait: PortraitPickerDialog._onRandomPortrait,
},
};
static PARTS = {
form: {
template: `modules/${MODULE_ID}/templates/portrait-picker.hbs`,
}
};
async _prepareContext(_opts) {
const portraits = this.manager.filterPortraits({ category: this._selectedCategory });
return {
actor: this.actor,
portraits,
categories: CATEGORIES,
selectedCategory: this._selectedCategory,
};
}
_onRender(context, opts) {
super._onRender?.(context, opts);
// Category dropdown change
this.element.querySelector('.category-filter')?.addEventListener('change', async (ev) => {
this._selectedCategory = ev.currentTarget.value;
await this.render();
});
}
static async _onSelectPortrait(event, target) {
const path = target.dataset.path;
await this._applyPortrait(path);
}
static async _onRandomPortrait(_event, _target) {
const portraits = this.manager.filterPortraits({ category: this._selectedCategory });
if (!portraits.length) return;
const chosen = portraits[Math.floor(Math.random() * portraits.length)];
await this._applyPortrait(chosen.path);
}
async _applyPortrait(path) {
const setAvatar = game.settings.get(MODULE_ID, 'setAvatar');
const updateData = { 'prototypeToken.texture.src': path };
if (setAvatar) updateData.img = path;
await this.actor.update(updateData);
ui.notifications.info(
game.i18n.format('NPC_PORTRAIT_PACK.Notifications.Assigned', { actorName: this.actor.name })
);
this.close();
}
};
}
// Foundry v11/v12 — FormApplication fallback
return class PortraitPickerDialog extends FormApplication {
constructor(actor, manager) {
super();
this.actor = actor;
this.manager = manager;
this._category = game.settings.get(MODULE_ID, 'defaultCategory') ?? 'any';
}
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
id: 'npc-portrait-picker',
title: 'NPC Portrait Picker',
template: `modules/${MODULE_ID}/templates/portrait-picker.hbs`,
width: 720,
height: 580,
resizable: true,
classes: ['npc-portrait-pack', 'portrait-picker'],
});
}
getData() {
const portraits = this.manager.filterPortraits({ category: this._category });
return {
actor: this.actor,
portraits,
categories: CATEGORIES,
selectedCategory: this._category,
};
}
activateListeners(html) {
super.activateListeners(html);
html.find('.portrait-thumb').on('click', async (ev) => {
const path = ev.currentTarget.dataset.path;
await this._applyPortrait(path);
});
html.find('.btn-random').on('click', async () => {
const portraits = this.manager.filterPortraits({ category: this._category });
if (!portraits.length) return;
const chosen = portraits[Math.floor(Math.random() * portraits.length)];
await this._applyPortrait(chosen.path);
});
html.find('.category-filter').on('change', (ev) => {
this._category = ev.currentTarget.value;
this.render(true);
});
}
async _applyPortrait(path) {
const setAvatar = game.settings.get(MODULE_ID, 'setAvatar');
const updateData = { 'prototypeToken.texture.src': path };
if (setAvatar) updateData.img = path;
await this.actor.update(updateData);
ui.notifications.info(
game.i18n.format('NPC_PORTRAIT_PACK.Notifications.Assigned', { actorName: this.actor.name })
);
this.close();
}
async _updateObject(_event, _formData) {
// Handled by activateListeners
}
};
}
export const PortraitPickerDialog = buildPickerClass();