chore: initial deploy — SLA Industries Foundry VTT
Co-authored-by: slaguru666 <timevans666@gmail.com>
This commit is contained in:
+15
@@ -0,0 +1,15 @@
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
.npm/
|
||||
dist/
|
||||
*.min.js
|
||||
*.min.css
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
Thumbs.db
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
worlds/
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"SLANPCDirector.Title": "SLA NPC Director"
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"id": "sla-npc-director",
|
||||
"title": "SLA NPC Director",
|
||||
"description": "Tracks NPC morale, suppression, stance, and intent for selected tokens or current combatants.",
|
||||
"version": "0.1.0",
|
||||
"compatibility": {
|
||||
"minimum": "13",
|
||||
"verified": "13.350",
|
||||
"maximum": "13"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Tim Evans"
|
||||
}
|
||||
],
|
||||
"esmodules": [
|
||||
"module/sla-npc-director.mjs"
|
||||
],
|
||||
"styles": [
|
||||
"styles/sla-npc-director.css"
|
||||
],
|
||||
"languages": [
|
||||
{
|
||||
"lang": "en",
|
||||
"name": "English",
|
||||
"path": "lang/en.json"
|
||||
}
|
||||
],
|
||||
"manifest": "https://raw.githubusercontent.com/slaguru666/sla-npc-director/main/module.json",
|
||||
"download": "https://github.com/slaguru666/sla-npc-director/releases/latest/download/sla-npc-director.zip",
|
||||
"url": "https://github.com/slaguru666/sla-npc-director"
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
const MODULE_ID = "sla-npc-director";
|
||||
const FLAG_KEY = "directorState";
|
||||
const STANCE_OPTIONS = ["Aggressive", "Holding", "Defensive", "Broken"];
|
||||
const MORALE_OPTIONS = ["Steady", "Shaken", "Fragile", "Collapsed"];
|
||||
const SUPPRESSION_OPTIONS = ["0", "1", "2", "3"];
|
||||
const INTENT_OPTIONS = ["Rush", "Flank", "Hold", "Suppress", "Fallback", "Flee"];
|
||||
|
||||
function getHtmlRoot(html) {
|
||||
return html instanceof HTMLElement ? html : html?.[0] ?? null;
|
||||
}
|
||||
|
||||
function getTargets() {
|
||||
const selectedTokens = canvas?.tokens?.controlled?.map((token) => token.document) ?? [];
|
||||
if (selectedTokens.length) return selectedTokens;
|
||||
|
||||
const combatants = game.combat?.combatants?.map((combatant) => combatant.token ?? combatant.actor)?.filter(Boolean) ?? [];
|
||||
return combatants;
|
||||
}
|
||||
|
||||
function readState(document) {
|
||||
return foundry.utils.mergeObject(
|
||||
{
|
||||
stance: "Holding",
|
||||
morale: "Steady",
|
||||
suppression: "0",
|
||||
intent: "Hold",
|
||||
note: ""
|
||||
},
|
||||
document?.getFlag?.(MODULE_ID, FLAG_KEY) ?? {}
|
||||
);
|
||||
}
|
||||
|
||||
async function writeState(document, state) {
|
||||
if (!document?.setFlag) return;
|
||||
await document.setFlag(MODULE_ID, FLAG_KEY, state);
|
||||
}
|
||||
|
||||
function buildSummary(entries) {
|
||||
const lines = entries.map((entry) => {
|
||||
const note = entry.note ? ` | ${foundry.utils.escapeHTML(entry.note)}` : "";
|
||||
return `<li><strong>${foundry.utils.escapeHTML(entry.name)}</strong>: ${entry.stance}, ${entry.morale}, suppression ${entry.suppression}, intent ${entry.intent}${note}</li>`;
|
||||
}).join("");
|
||||
return `<section class="sla-npc-director-summary"><h2>SLA NPC Director</h2><ul>${lines}</ul></section>`;
|
||||
}
|
||||
|
||||
class SlaNpcDirectorApp extends FormApplication {
|
||||
static get defaultOptions() {
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
id: "sla-npc-director-app",
|
||||
title: "SLA NPC Director",
|
||||
template: "modules/sla-npc-director/templates/npc-director.hbs",
|
||||
classes: ["sla-npc-director"],
|
||||
width: 980,
|
||||
height: "auto",
|
||||
closeOnSubmit: false,
|
||||
submitOnChange: false,
|
||||
resizable: true
|
||||
});
|
||||
}
|
||||
|
||||
async getData() {
|
||||
const entries = getTargets().map((document) => ({
|
||||
name: document.name ?? document.actor?.name ?? "Unknown",
|
||||
docId: document.id,
|
||||
docType: document.documentName,
|
||||
...readState(document)
|
||||
}));
|
||||
|
||||
return {
|
||||
entries,
|
||||
stanceOptions: STANCE_OPTIONS,
|
||||
moraleOptions: MORALE_OPTIONS,
|
||||
suppressionOptions: SUPPRESSION_OPTIONS,
|
||||
intentOptions: INTENT_OPTIONS
|
||||
};
|
||||
}
|
||||
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
html.find("[data-action='refresh']").on("click", () => this.render(false));
|
||||
html.find("[data-action='save']").on("click", async () => {
|
||||
await this._saveRows();
|
||||
ui.notifications.info("SLA NPC Director states saved.");
|
||||
});
|
||||
html.find("[data-action='chat']").on("click", async () => {
|
||||
const entries = await this._saveRows();
|
||||
await ChatMessage.create({
|
||||
user: game.user.id,
|
||||
speaker: { alias: "SLA NPC Director" },
|
||||
whisper: ChatMessage.getWhisperRecipients("GM"),
|
||||
content: buildSummary(entries)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async _saveRows() {
|
||||
const rows = Array.from(this.form?.querySelectorAll(".sla-npc-director-row") ?? []);
|
||||
const summaries = [];
|
||||
for (const row of rows) {
|
||||
const docId = row.dataset.docId;
|
||||
const document = canvas?.scene?.tokens?.get?.(docId) ?? game.combat?.combatants?.find((combatant) => combatant.token?.id === docId)?.token;
|
||||
if (!document) continue;
|
||||
const state = {
|
||||
stance: row.querySelector("[data-field='stance']")?.value ?? "Holding",
|
||||
morale: row.querySelector("[data-field='morale']")?.value ?? "Steady",
|
||||
suppression: row.querySelector("[data-field='suppression']")?.value ?? "0",
|
||||
intent: row.querySelector("[data-field='intent']")?.value ?? "Hold",
|
||||
note: row.querySelector("[data-field='note']")?.value ?? ""
|
||||
};
|
||||
await writeState(document, state);
|
||||
summaries.push({ name: document.name ?? document.actor?.name ?? "Unknown", ...state });
|
||||
}
|
||||
return summaries;
|
||||
}
|
||||
|
||||
async _updateObject() {
|
||||
await this._saveRows();
|
||||
}
|
||||
|
||||
static open() {
|
||||
return new SlaNpcDirectorApp().render(true);
|
||||
}
|
||||
}
|
||||
|
||||
function appendOpenButton(root, selector, className, label) {
|
||||
const header = root?.querySelector(selector);
|
||||
if (!header || header.querySelector(`.${className}`)) return false;
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = className;
|
||||
button.innerHTML = `<i class="fas fa-user-shield"></i> ${label}`;
|
||||
button.addEventListener("click", () => SlaNpcDirectorApp.open());
|
||||
header.appendChild(button);
|
||||
return true;
|
||||
}
|
||||
|
||||
function installCombatButton(app, html) {
|
||||
const root = getHtmlRoot(html);
|
||||
appendOpenButton(root, ".directory-header .header-actions, .directory-header .action-buttons, .combat-sidebar .encounter-controls", "sla-npc-director-open", "NPC Director");
|
||||
}
|
||||
|
||||
function installActorDirectoryButton(app, html) {
|
||||
const root = getHtmlRoot(html);
|
||||
appendOpenButton(root, ".directory-header .header-actions, .directory-header .action-buttons", "sla-npc-director-actor-open", "NPC Director");
|
||||
}
|
||||
|
||||
function installSettingsButton(app, html) {
|
||||
const root = getHtmlRoot(html);
|
||||
appendOpenButton(root, ".settings-sidebar #game-details, .settings-sidebar .directory-header .header-actions, .settings-sidebar .action-buttons", "sla-npc-director-settings-open", "NPC Director");
|
||||
}
|
||||
|
||||
Hooks.once("init", () => {
|
||||
Handlebars.registerHelper("eq", (left, right) => left === right);
|
||||
game.settings.registerMenu(MODULE_ID, "npcDirectorMenu", {
|
||||
name: "SLA NPC Director",
|
||||
label: "Open NPC Director",
|
||||
hint: "Track combat stance, morale, suppression, and intent for current targets.",
|
||||
icon: "fas fa-user-shield",
|
||||
type: SlaNpcDirectorApp,
|
||||
restricted: true
|
||||
});
|
||||
});
|
||||
|
||||
Hooks.on("renderCombatTracker", installCombatButton);
|
||||
Hooks.on("renderActorDirectory", installActorDirectoryButton);
|
||||
Hooks.on("renderSettings", installSettingsButton);
|
||||
@@ -0,0 +1,71 @@
|
||||
.sla-npc-director .window-content {
|
||||
background: linear-gradient(180deg, #10161d 0%, #171d25 100%);
|
||||
color: #f2eee5 !important;
|
||||
}
|
||||
|
||||
.sla-npc-director-form {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.sla-npc-director-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sla-npc-director-toolbar button {
|
||||
background: linear-gradient(180deg, #d6b24e 0%, #b88a24 100%) !important;
|
||||
border: 1px solid rgba(255, 232, 180, 0.55) !important;
|
||||
border-radius: 999px;
|
||||
color: #17140c !important;
|
||||
padding: 7px 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sla-npc-director-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1.4fr 0.9fr 0.9fr 0.9fr 1fr 1.2fr;
|
||||
gap: 8px;
|
||||
align-items: end;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(215, 181, 82, 0.24);
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.sla-npc-director-row label {
|
||||
color: #f2eee5 !important;
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.sla-npc-director-row input,
|
||||
.sla-npc-director-row select {
|
||||
width: 100%;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 10px;
|
||||
color: #f2eee5 !important;
|
||||
padding: 7px 9px;
|
||||
}
|
||||
|
||||
.sla-npc-director .window-title,
|
||||
.sla-npc-director p,
|
||||
.sla-npc-director span,
|
||||
.sla-npc-director strong,
|
||||
.sla-npc-director input,
|
||||
.sla-npc-director select,
|
||||
.sla-npc-director option {
|
||||
color: #f2eee5 !important;
|
||||
}
|
||||
|
||||
@media (max-width: 920px) {
|
||||
.sla-npc-director-row {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<form class="sla-npc-director-form">
|
||||
<div class="sla-npc-director-toolbar">
|
||||
<button type="button" data-action="refresh">Refresh Targets</button>
|
||||
<button type="button" data-action="save">Save States</button>
|
||||
<button type="button" data-action="chat">Whisper GM Summary</button>
|
||||
</div>
|
||||
|
||||
{{#if entries.length}}
|
||||
{{#each entries}}
|
||||
<section class="sla-npc-director-row" data-doc-id="{{docId}}" data-doc-type="{{docType}}">
|
||||
<div>
|
||||
<label>Target</label>
|
||||
<input type="text" value="{{name}}" disabled>
|
||||
</div>
|
||||
<div>
|
||||
<label>Stance</label>
|
||||
<select data-field="stance">
|
||||
{{#each ../stanceOptions}}
|
||||
<option value="{{this}}" {{#if (eq this ../stance)}}selected{{/if}}>{{this}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>Morale</label>
|
||||
<select data-field="morale">
|
||||
{{#each ../moraleOptions}}
|
||||
<option value="{{this}}" {{#if (eq this ../morale)}}selected{{/if}}>{{this}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>Suppression</label>
|
||||
<select data-field="suppression">
|
||||
{{#each ../suppressionOptions}}
|
||||
<option value="{{this}}" {{#if (eq this ../suppression)}}selected{{/if}}>{{this}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>Intent</label>
|
||||
<select data-field="intent">
|
||||
{{#each ../intentOptions}}
|
||||
<option value="{{this}}" {{#if (eq this ../intent)}}selected{{/if}}>{{this}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>Note</label>
|
||||
<input type="text" data-field="note" value="{{note}}">
|
||||
</div>
|
||||
</section>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<p>No selected tokens or active combatants found.</p>
|
||||
{{/if}}
|
||||
</form>
|
||||
Reference in New Issue
Block a user