mirror of
https://github.com/migatu/vtt_work.git
synced 2026-07-14 21:38:39 +00:00
Import gk-greyknight-0.6.4.8.zip do wg-greyknights (2025-08-25T13:36:01Z)
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "gk-greyknight",
|
"id": "gk-greyknight",
|
||||||
"title": "Grey Knights — Compendium (Wrath & Glory)",
|
"title": "Grey Knights — Compendium (Wrath & Glory)",
|
||||||
"version": "0.6.4.5",
|
"version": "0.6.4.6",
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"minimum": "11",
|
"minimum": "11",
|
||||||
"verified": "13"
|
"verified": "13"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,133 +1,103 @@
|
|||||||
// gk-greyknight — helpers v0.6.4 (Brotherhood discipline prompt)
|
// gk-greyknight helpers.js — consolidated 0.6.4.6
|
||||||
Hooks.once("ready", () => {
|
// Zawiera: Brotherhood prompt + starter, powiadomienie (także poza walką), sheet summary,
|
||||||
const PACK_KEY = "gk-greyknight.gk-items";
|
// mini-widżet, oraz patcher traitów (Psycannon/Psilencer -> force).
|
||||||
const PACK_SRC_PREFIX = `Compendium.${PACK_KEY}.`;
|
|
||||||
const FLAG_SCOPE = "gk-greyknight";
|
|
||||||
|
|
||||||
async function promptDiscipline(actor) {
|
/* ---------------- Utilities ---------------- */
|
||||||
const existing = await actor.getFlag(FLAG_SCOPE, "discipline");
|
const GK_DEBUG = false;
|
||||||
if (existing) return existing;
|
const FLAG_SCOPE = "gk-greyknight";
|
||||||
return await new Promise(resolve => {
|
|
||||||
new Dialog({
|
async function findItemInAllPacksByName(name, type=null) {
|
||||||
title: "Brotherhood of Psykers — Discipline",
|
for (const pack of game.packs) {
|
||||||
content: `<p>Choose a psychic discipline for this character:</p>
|
if (pack.documentName !== "Item") continue;
|
||||||
<div style="display:flex; gap:.75rem; margin-top:.5rem;">
|
const idx = await pack.getIndex();
|
||||||
<label><input type="radio" name="disc" value="librarius" checked> Librarius</label>
|
const hit = idx.find(e => e.name === name && (!type || e.type === type));
|
||||||
<label><input type="radio" name="disc" value="sanctic"> Sanctic</label>
|
if (hit) return await pack.getDocument(hit._id);
|
||||||
</div>
|
|
||||||
<p style="margin:.5rem 0 0; font-size:.9em; opacity:.8;">(You can change this later in Actor → Flags → gk-greyknight → discipline.)</p>`,
|
|
||||||
buttons: {
|
|
||||||
ok: { label: "Confirm", icon: "<i class='fas fa-check'></i>", callback: html => resolve(html.find('input[name=\"disc\"]:checked').val() || "librarius") },
|
|
||||||
cancel: { label: "Cancel", callback: () => resolve(null) }
|
|
||||||
},
|
|
||||||
default: "ok"
|
|
||||||
}).render(true);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
if (window._gkBroHookId) { Hooks.off("createItem", window._gkBroHookId); window._gkBroHookId = null; }
|
}
|
||||||
|
async function ensureHasItemByName(actor, name, type=null) {
|
||||||
window._gkBroHookId = Hooks.on("createItem", async (item, opts, userId) => {
|
const existing = actor.items?.find(i => i.name === name && (!type || i.type === type));
|
||||||
try {
|
if (existing) return existing;
|
||||||
if (game.userId !== userId) return;
|
const doc = await findItemInAllPacksByName(name, type);
|
||||||
if (item?.type !== "ability") return;
|
if (!doc) return null;
|
||||||
const name = item?.name || "";
|
const data = doc.toObject(); delete data._id;
|
||||||
if (name !== "Brotherhood of Psykers") return;
|
const [created] = await actor.createEmbeddedDocuments("Item", [data]);
|
||||||
const src = item?.flags?.core?.sourceId || "";
|
return created;
|
||||||
const isFromOurPack = src.startsWith(PACK_SRC_PREFIX);
|
}
|
||||||
if (!isFromOurPack && name !== "Brotherhood of Psykers") return;
|
async function promptDiscipline(actor) {
|
||||||
const actor = item.parent; if (!actor) return;
|
const existing = await actor.getFlag(FLAG_SCOPE, "discipline");
|
||||||
|
if (existing) return existing;
|
||||||
const chosen = await promptDiscipline(actor);
|
return await new Promise(resolve => {
|
||||||
if (!chosen) return;
|
new Dialog({
|
||||||
await actor.setFlag(FLAG_SCOPE, "discipline", chosen);
|
title: "Brotherhood of Psykers — Choose Discipline",
|
||||||
|
content: `<p>Select a discipline for this character:</p>
|
||||||
if (chosen === "sanctic") {
|
<label style='display:block;margin-top:.5rem'><input type="radio" name="disc" value="sanctic" checked> Sanctic</label>
|
||||||
ui.notifications.info("Discipline set to SANCTIC. Open the GK compendium and pick one Sanctic power as your starter.");
|
<label style='display:block'><input type="radio" name="disc" value="librarius"> Librarius</label>`,
|
||||||
} else {
|
buttons: {
|
||||||
ui.notifications.info("Discipline set to LIBRARIUS. Use core Librarius powers from wng-core.");
|
ok: { label: "Confirm", icon: "<i class='fas fa-check'></i>", callback: html => resolve(html.find('input[name="disc"]:checked').val() || "sanctic") },
|
||||||
}
|
cancel: { label: "Cancel", callback: () => resolve(null) }
|
||||||
} catch (e) { console.error("gk-greyknight brotherhood helper:", e); }
|
},
|
||||||
|
default: "ok"
|
||||||
|
}).render(true);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
console.log("gk-greyknight helpers active (v0.6.4): Brotherhood discipline prompt enabled.");
|
async function gkResolveActorFromMessage(doc) {
|
||||||
});
|
const sp = doc.speaker || {};
|
||||||
|
let actor = sp.actor ? game.actors?.get(sp.actor) : null;
|
||||||
/* gk-greyknight 0.6.4.1 — house-rule reminder (non-blocking) */
|
if (!actor && canvas?.tokens) {
|
||||||
Hooks.on("preCreateChatMessage", (doc, data, options, userId) => {
|
const t = (sp.token && canvas.tokens.get(sp.token)) || canvas.tokens.controlled?.[0] || null;
|
||||||
|
actor = t?.actor || actor;
|
||||||
|
}
|
||||||
|
return actor;
|
||||||
|
}
|
||||||
|
async function gkIsPsychicMessage(doc) {
|
||||||
try {
|
try {
|
||||||
const flavor = (data.flavor || "").toLowerCase();
|
const raw = `${doc.flavor || ""} ${TextEditor?.stripHTML(doc.content || "") || ""}`.toLowerCase();
|
||||||
const isPsychic = flavor.includes("psychic") || flavor.includes("power");
|
const keys = ["psychic","psychic mastery","psychic power","moc psioniczna","smite","hammerhand","sanctuary","shrouding","might of titan"];
|
||||||
if (!isPsychic) return;
|
if (keys.some(k=>raw.includes(k))) return true;
|
||||||
const speaker = data.speaker || {}; const actor = game.actors?.get(speaker.actor);
|
const flagsStr = JSON.stringify(doc.flags || {}).toLowerCase();
|
||||||
if (!actor) return;
|
if (flagsStr.includes("psychic")) return true;
|
||||||
const hasBrotherhood = actor.items?.some(i => i.type === "ability" && i.name === "Brotherhood of Psykers");
|
if (flagsStr.includes("psychicpower")) return true;
|
||||||
if (!hasBrotherhood) return;
|
if (flagsStr.includes("\"power\"")) return true;
|
||||||
const combat = game.combat; if (!combat) return;
|
const tryUuids = [
|
||||||
const key = `gk-greyknight.usedPsychicAction.r${combat.round}`;
|
doc.getFlag?.("core","sourceId"),
|
||||||
const prev = combat.getFlag("gk-greyknight", key) || null;
|
doc.getFlag?.("wng-core","itemUuid"),
|
||||||
if (prev && prev !== actor.id) ui.notifications?.warn("House rule (Brotherhood Focus): only one Grey Knight should use a psychic power as their Action this round.");
|
doc.flags?.document?.uuid,
|
||||||
else if (!prev) combat.setFlag("gk-greyknight", key, actor.id);
|
doc.flags?.uuid
|
||||||
} catch (e) { console.error("gk-greyknight helpers (house-rule reminder):", e); }
|
].filter(Boolean);
|
||||||
});
|
for (const u of tryUuids) {
|
||||||
|
try { const it = await fromUuid(u); if (it?.type === "psychicPower") return true; } catch(e){}
|
||||||
// === gk-greyknight 0.6.4.3 — Brotherhood discipline prompt + starter grants ===
|
|
||||||
Hooks.once("ready", () => {
|
|
||||||
const FLAG_SCOPE = "gk-greyknight";
|
|
||||||
// Prevent duplicate hook registration on hot-reload
|
|
||||||
if (window._gkBroHookId) { Hooks.off("createItem", window._gkBroHookId); window._gkBroHookId = null; }
|
|
||||||
|
|
||||||
async function findItemInAllPacksByName(name, type=null) {
|
|
||||||
for (const pack of game.packs) {
|
|
||||||
if (pack.documentName !== "Item") continue;
|
|
||||||
const index = await pack.getIndex();
|
|
||||||
const hit = index.find(e => e.name === name && (!type || e.type === type));
|
|
||||||
if (hit) return await pack.getDocument(hit._id);
|
|
||||||
}
|
}
|
||||||
return null;
|
} catch(e) { if (GK_DEBUG) console.warn("gk IsPsychicMessage error:", e); }
|
||||||
}
|
return false;
|
||||||
async function ensureHasItemByName(actor, name, type=null) {
|
}
|
||||||
const existing = actor.items?.find(i => i.name === name && (!type || i.type === type));
|
function gkBrotherhoodAlliesAndBonus(actor) {
|
||||||
if (existing) return existing;
|
const tokens = canvas?.tokens?.placeables || [];
|
||||||
const doc = await findItemInAllPacksByName(name, type);
|
const ids = new Set();
|
||||||
if (!doc) return null;
|
for (const t of tokens) {
|
||||||
const data = doc.toObject(); delete data._id;
|
const a = t.actor; if (!a) continue;
|
||||||
const [created] = await actor.createEmbeddedDocuments("Item", [data]);
|
const ok = a.items?.some(i => i.type === "ability" && i.name === "Brotherhood of Psykers");
|
||||||
return created;
|
if (ok) ids.add(a.id);
|
||||||
}
|
|
||||||
async function promptDiscipline(actor) {
|
|
||||||
const existing = await actor.getFlag(FLAG_SCOPE, "discipline");
|
|
||||||
if (existing) return existing;
|
|
||||||
return await new Promise(resolve => {
|
|
||||||
new Dialog({
|
|
||||||
title: "Brotherhood of Psykers — Choose Discipline",
|
|
||||||
content: `<p>Select a discipline for this character:</p>
|
|
||||||
<label style='display:block;margin-top:.5rem'><input type="radio" name="disc" value="sanctic" checked> Sanctic</label>
|
|
||||||
<label style='display:block'><input type="radio" name="disc" value="librarius"> Librarius</label>`,
|
|
||||||
buttons: {
|
|
||||||
ok: { label: "Confirm", icon: "<i class='fas fa-check'></i>", callback: html => resolve(html.find('input[name="disc"]:checked').val() || "sanctic") },
|
|
||||||
cancel: { label: "Cancel", callback: () => resolve(null) }
|
|
||||||
},
|
|
||||||
default: "ok"
|
|
||||||
}).render(true);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
const total = ids.size;
|
||||||
|
const bonus = ids.has(actor?.id) ? Math.max(0, total - 1) : total;
|
||||||
|
return { total, bonus };
|
||||||
|
}
|
||||||
|
|
||||||
window._gkBroHookId = Hooks.on("createItem", async (item, opts, userId) => {
|
/* ---------------- 1) Brotherhood: prompt + starter ---------------- */
|
||||||
|
Hooks.once("ready", () => {
|
||||||
|
// Avoid duplicate hook if hot-reloaded
|
||||||
|
if (window._gkBroCreateHookId) { Hooks.off("createItem", window._gkBroCreateHookId); window._gkBroCreateHookId = null; }
|
||||||
|
window._gkBroCreateHookId = Hooks.on("createItem", async (item, opts, userId) => {
|
||||||
try {
|
try {
|
||||||
if (game.userId !== userId) return;
|
if (game.userId !== userId) return;
|
||||||
if (item?.type !== "ability" || item?.name !== "Brotherhood of Psykers") return;
|
if (item?.type !== "ability" || item?.name !== "Brotherhood of Psykers") return;
|
||||||
const actor = item.parent; if (!actor) return;
|
const actor = item.parent; if (!actor) return;
|
||||||
|
|
||||||
const chosen = await promptDiscipline(actor);
|
const chosen = await promptDiscipline(actor);
|
||||||
if (!chosen) return;
|
if (!chosen) return;
|
||||||
await actor.setFlag(FLAG_SCOPE, "discipline", chosen);
|
await actor.setFlag(FLAG_SCOPE, "discipline", chosen);
|
||||||
|
|
||||||
// Ensure PSYKER keyword so powers are available
|
|
||||||
const psyker = await ensureHasItemByName(actor, "PSYKER", "keyword");
|
const psyker = await ensureHasItemByName(actor, "PSYKER", "keyword");
|
||||||
if (!psyker) ui.notifications?.warn("PSYKER keyword not found in loaded packs. Add it manually from wng-core.");
|
if (!psyker) ui.notifications?.warn("PSYKER keyword not found in loaded packs. Add it manually from wng-core.");
|
||||||
|
|
||||||
// Starter power by discipline
|
|
||||||
if (chosen === "sanctic") {
|
if (chosen === "sanctic") {
|
||||||
const ok = await ensureHasItemByName(actor, "Hammerhand (Sanctic)", "psychicPower");
|
const ok = await ensureHasItemByName(actor, "Hammerhand (Sanctic)", "psychicPower");
|
||||||
if (!ok) ui.notifications?.warn("Couldn't auto-add Hammerhand (Sanctic). Drag a Sanctic power from the GK pack.");
|
if (!ok) ui.notifications?.warn("Couldn't auto-add Hammerhand (Sanctic). Drag a Sanctic power from the GK pack.");
|
||||||
@@ -141,96 +111,3 @@ Hooks.once("ready", () => {
|
|||||||
} catch (e) { console.error("gk-greyknight — Brotherhood prompt:", e); }
|
} catch (e) { console.error("gk-greyknight — Brotherhood prompt:", e); }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// gk-greyknight 0.6.4.4 — QoL: Brotherhood Focus summary on sheet open
|
|
||||||
Hooks.on("renderActorSheet", async (sheet, htmlEl, data) => {
|
|
||||||
try {
|
|
||||||
const actor = sheet.actor; if (!actor) return;
|
|
||||||
const hasBro = actor.items?.some(i => i.type === "ability" && i.name === "Brotherhood of Psykers");
|
|
||||||
if (!hasBro) return;
|
|
||||||
const tokens = canvas?.tokens?.placeables || [];
|
|
||||||
const allied = new Set();
|
|
||||||
for (const t of tokens) {
|
|
||||||
const a = t.actor; if (!a) continue;
|
|
||||||
const ok = a.items?.some(i => i.type === "ability" && i.name === "Brotherhood of Psykers");
|
|
||||||
if (ok) allied.add(a.id);
|
|
||||||
}
|
|
||||||
const total = allied.size;
|
|
||||||
const bonus = allied.has(actor.id) ? Math.max(0, total - 1) : total;
|
|
||||||
const owners = Object.keys(actor.ownership || {}).filter(uid => actor.ownership[uid] >= CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER);
|
|
||||||
const content = `<div class="gk-brotherhood-summary">
|
|
||||||
<h3 style="margin:0 0 .25rem 0;">Brotherhood Focus — Summary</h3>
|
|
||||||
<p style="margin:0;">Allies with Brotherhood in scene: <b>${total}</b> (excluding self: <b>${bonus}</b> bonus die to <em>Psychic Mastery</em>).</p>
|
|
||||||
<p style="margin:.25rem 0 0;font-size:.9em;opacity:.85;">Reminder: each subsequent power cast this round using the shared focus adds one extra <em>Wrath die</em>. Only one linked character should use a psychic power as their <em>Action</em> per round.</p>
|
|
||||||
</div>`;
|
|
||||||
ChatMessage.create({content, whisper: owners, speaker: {actor: actor.id}});
|
|
||||||
} catch (e) { console.error("gk-greyknight QoL summary:", e); }
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// gk-greyknight 0.6.4.5 — Reliable Brotherhood notification on psychic power use
|
|
||||||
Hooks.on("createChatMessage", async (doc, options, userId) => {
|
|
||||||
try {
|
|
||||||
// Heuristic detection of psychic power usage
|
|
||||||
const flavor = (doc.flavor || "").toLowerCase();
|
|
||||||
const contentText = (ui?.chat && doc.content) ? TextEditor?.stripHTML(doc.content)?.toLowerCase?.() : (doc.content || "").toLowerCase();
|
|
||||||
const keyWords = ["psychic", "psychic mastery", "power", "smite", "hammerhand", "sanctuary", "shrouding", "might of titan"];
|
|
||||||
const looksPsychic = keyWords.some(k => flavor.includes(k) || contentText.includes(k));
|
|
||||||
if (!looksPsychic) return;
|
|
||||||
|
|
||||||
// Resolve actor
|
|
||||||
let actor = null;
|
|
||||||
const sp = doc.speaker || {};
|
|
||||||
if (sp.actor) actor = game.actors?.get(sp.actor);
|
|
||||||
if (!actor && canvas?.tokens) {
|
|
||||||
const t = canvas.tokens.get(sp.token) || canvas.tokens.controlled?.[0];
|
|
||||||
actor = t?.actor || actor;
|
|
||||||
}
|
|
||||||
if (!actor) return;
|
|
||||||
|
|
||||||
// Check Brotherhood ability on actor
|
|
||||||
const hasBrotherhood = actor.items?.some(i => i.type === "ability" && i.name === "Brotherhood of Psykers");
|
|
||||||
if (!hasBrotherhood) return;
|
|
||||||
|
|
||||||
// Count allies with Brotherhood in the scene
|
|
||||||
const tokens = canvas?.tokens?.placeables || [];
|
|
||||||
const allied = new Set();
|
|
||||||
for (const t of tokens) {
|
|
||||||
const a = t.actor; if (!a) continue;
|
|
||||||
const ok = a.items?.some(i => i.type === "ability" && i.name === "Brotherhood of Psykers");
|
|
||||||
if (ok) allied.add(a.id);
|
|
||||||
}
|
|
||||||
const total = allied.size;
|
|
||||||
const bonus = allied.has(actor.id) ? Math.max(0, total - 1) : total;
|
|
||||||
|
|
||||||
// Compose message
|
|
||||||
const msg = `<div class="gk-brotherhood-notice">
|
|
||||||
<b>Brotherhood Focus</b>: Allies with Brotherhood here: <b>${total}</b> → bonus to <em>Psychic Mastery</em>: <b>+${bonus} dice</b>.
|
|
||||||
<div style="opacity:.85;font-size:.9em;margin-top:.25rem;">House rule: only one linked Grey Knight should use a psychic power as their <em>Action</em> per round; subsequent powers add +1 <em>Wrath</em> die.</div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
// Whisper to owners + GM
|
|
||||||
const ownerIds = Object.entries(actor.ownership || {})
|
|
||||||
.filter(([uid, lvl]) => lvl >= (CONST.DOCUMENT_OWNERSHIP_LEVELS?.OWNER ?? 3))
|
|
||||||
.map(([uid]) => uid);
|
|
||||||
const gmIds = game.users?.filter(u => u.isGM).map(u => u.id) ?? [];
|
|
||||||
const whisper = Array.from(new Set([...ownerIds, ...gmIds]));
|
|
||||||
await ChatMessage.create({content: msg, whisper, speaker: {actor: actor.id}});
|
|
||||||
|
|
||||||
// If in combat, enforce one-per-round reminder
|
|
||||||
const combat = game.combat;
|
|
||||||
if (combat) {
|
|
||||||
const key = `gk-greyknight.usedPsychicAction.r${combat.round}`;
|
|
||||||
const prev = combat.getFlag("gk-greyknight", key) || null;
|
|
||||||
if (prev && prev !== actor.id) {
|
|
||||||
ui.notifications?.warn("House rule (Brotherhood Focus): only one Grey Knight should use a psychic power as their Action this round.");
|
|
||||||
} else if (!prev) {
|
|
||||||
await combat.setFlag("gk-greyknight", key, actor.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("gk-greyknight 0.6.4.5 notification error:", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user