diff --git a/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Amity Cornwallis.md b/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Amity Cornwallis.md index 900b3f0..12d0a20 100644 --- a/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Amity Cornwallis.md +++ b/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Amity Cornwallis.md @@ -3,6 +3,7 @@ Level_of_Influence: 3 Faction: "[[The Archmasons]]" Tags: - npc +Intel: --- # Amity Cornwallis @@ -43,8 +44,107 @@ Tags: --- +## 🧠 Intel (auto) +```dataviewjs +const THIS = dv.current(); +const thisName = String(THIS.file.name); +const thisLC = thisName.toLowerCase(); + +// Scan faction & NPC notes that have Intel arrays +const pages = dv.pages("#npc or #faction").where(p => p.Intel); + +// Does an Intel entry reference THIS NPC? +function mentionsThisNPC(entry) { + const sum = String(entry.summary ?? ""); + + // 1) Explicit wikilink in summary + if (sum.includes(`[[${thisName}]]`) || sum.includes(`[[${thisName}|`)) return true; + + // 2) Source field (string/link/list) + const src = entry.source; + const arr = Array.isArray(src) ? src : (src ? [src] : []); + for (const s of arr) { + if (typeof s === "string") { + // quoted link or plain text + const unq = s.replace(/^"+|"+$/g, ""); // drop surrounding quotes if any + if (unq.includes(`[[${thisName}]]`) || unq.includes(`[[${thisName}|`)) return true; + // if they typed just the name + if (unq.replace(/\[\[|\]\]/g, "").trim().toLowerCase() === thisLC) return true; + } else if (s && typeof s === "object" && s.path) { + // wikilink object + const base = s.path.split("/").pop().replace(/\.md$/i, "").toLowerCase(); + if (base === thisLC) return true; + } + } + + // Optional (looser): plain text mention in summary + // return sum.toLowerCase().includes(thisLC); + + return false; +} + +// Collect rows: 1) own Intel, 2) mentions from other notes +const rows = []; + +// Own Intel (from this NPC note) +for (const it of (THIS.Intel ?? [])) { + rows.push({ + from: THIS.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + session: it.session ?? "", + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); +} + +// Mentions elsewhere +for (const p of pages.where(p => p.file.path !== THIS.file.path)) { + for (const it of (p.Intel ?? [])) { + if (mentionsThisNPC(it)) { + rows.push({ + from: p.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + session: it.session ?? "", + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); + } + } +} + +// Sort newest first +rows.sort((a,b) => String(b.date).localeCompare(String(a.date))); + +if (!rows.length) { + dv.paragraph("_No intel yet._"); +} else { + dv.paragraph(`_Entries: ${rows.length} (own ${(THIS.Intel ?? []).length} + mentions ${Math.max(0, rows.length - (THIS.Intel ?? []).length)})_`); + dv.table( + ["From Note","Date","Type","Summary","Session","Source","Rel","Cred","Tags"], + rows.map(r => [r.from, r.date, r.type, r.summary, r.session, r.source, r.rel, r.cred, r.tags]) + ); +} +``` + +```button +name βž• Add Intel +type command +action Templater: Insert Add Intel Entry to NPC +``` + +--- + ## πŸ›οΈ Relations & Factions - **Primary Faction:** [[The Archmasons]] + - **Allied Factions:** [[Faction A]], [[Faction B]] - **Rival Factions:** [[Faction C]] - **Personal Enemies / Allies:** [[NPC or PC]] diff --git a/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Atmaka Sayin.md b/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Atmaka Sayin.md index e32243c..efa3e0d 100644 --- a/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Atmaka Sayin.md +++ b/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Atmaka Sayin.md @@ -38,6 +38,103 @@ Tags: --- +## 🧠 Intel (auto) +```dataviewjs +const THIS = dv.current(); +const thisName = String(THIS.file.name); +const thisLC = thisName.toLowerCase(); + +// Scan faction & NPC notes that have Intel arrays +const pages = dv.pages("#npc or #faction").where(p => p.Intel); + +// Does an Intel entry reference THIS NPC? +function mentionsThisNPC(entry) { + const sum = String(entry.summary ?? ""); + + // 1) Explicit wikilink in summary + if (sum.includes(`[[${thisName}]]`) || sum.includes(`[[${thisName}|`)) return true; + + // 2) Source field (string/link/list) + const src = entry.source; + const arr = Array.isArray(src) ? src : (src ? [src] : []); + for (const s of arr) { + if (typeof s === "string") { + // quoted link or plain text + const unq = s.replace(/^"+|"+$/g, ""); // drop surrounding quotes if any + if (unq.includes(`[[${thisName}]]`) || unq.includes(`[[${thisName}|`)) return true; + // if they typed just the name + if (unq.replace(/\[\[|\]\]/g, "").trim().toLowerCase() === thisLC) return true; + } else if (s && typeof s === "object" && s.path) { + // wikilink object + const base = s.path.split("/").pop().replace(/\.md$/i, "").toLowerCase(); + if (base === thisLC) return true; + } + } + + // Optional (looser): plain text mention in summary + // return sum.toLowerCase().includes(thisLC); + + return false; +} + +// Collect rows: 1) own Intel, 2) mentions from other notes +const rows = []; + +// Own Intel (from this NPC note) +for (const it of (THIS.Intel ?? [])) { + rows.push({ + from: THIS.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + session: it.session ?? "", + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); +} + +// Mentions elsewhere +for (const p of pages.where(p => p.file.path !== THIS.file.path)) { + for (const it of (p.Intel ?? [])) { + if (mentionsThisNPC(it)) { + rows.push({ + from: p.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + session: it.session ?? "", + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); + } + } +} + +// Sort newest first +rows.sort((a,b) => String(b.date).localeCompare(String(a.date))); + +if (!rows.length) { + dv.paragraph("_No intel yet._"); +} else { + dv.paragraph(`_Entries: ${rows.length} (own ${(THIS.Intel ?? []).length} + mentions ${Math.max(0, rows.length - (THIS.Intel ?? []).length)})_`); + dv.table( + ["From Note","Date","Type","Summary","Session","Source","Rel","Cred","Tags"], + rows.map(r => [r.from, r.date, r.type, r.summary, r.session, r.source, r.rel, r.cred, r.tags]) + ); +} +``` + +```button +name βž• Add Intel +type command +action Templater: Insert Add Intel Entry to NPC +``` + +--- ## πŸ›οΈ Relations & Factions - **Primary Faction:** [[Faction Name]] - **Allied Factions:** [[Faction A]], [[Faction B]] diff --git a/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Calia Noctilun.md b/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Calia Noctilun.md index a316664..a0e5bef 100644 --- a/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Calia Noctilun.md +++ b/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Calia Noctilun.md @@ -38,6 +38,104 @@ Tags: --- +## 🧠 Intel (auto) +```dataviewjs +const THIS = dv.current(); +const thisName = String(THIS.file.name); +const thisLC = thisName.toLowerCase(); + +// Scan faction & NPC notes that have Intel arrays +const pages = dv.pages("#npc or #faction").where(p => p.Intel); + +// Does an Intel entry reference THIS NPC? +function mentionsThisNPC(entry) { + const sum = String(entry.summary ?? ""); + + // 1) Explicit wikilink in summary + if (sum.includes(`[[${thisName}]]`) || sum.includes(`[[${thisName}|`)) return true; + + // 2) Source field (string/link/list) + const src = entry.source; + const arr = Array.isArray(src) ? src : (src ? [src] : []); + for (const s of arr) { + if (typeof s === "string") { + // quoted link or plain text + const unq = s.replace(/^"+|"+$/g, ""); // drop surrounding quotes if any + if (unq.includes(`[[${thisName}]]`) || unq.includes(`[[${thisName}|`)) return true; + // if they typed just the name + if (unq.replace(/\[\[|\]\]/g, "").trim().toLowerCase() === thisLC) return true; + } else if (s && typeof s === "object" && s.path) { + // wikilink object + const base = s.path.split("/").pop().replace(/\.md$/i, "").toLowerCase(); + if (base === thisLC) return true; + } + } + + // Optional (looser): plain text mention in summary + // return sum.toLowerCase().includes(thisLC); + + return false; +} + +// Collect rows: 1) own Intel, 2) mentions from other notes +const rows = []; + +// Own Intel (from this NPC note) +for (const it of (THIS.Intel ?? [])) { + rows.push({ + from: THIS.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + session: it.session ?? "", + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); +} + +// Mentions elsewhere +for (const p of pages.where(p => p.file.path !== THIS.file.path)) { + for (const it of (p.Intel ?? [])) { + if (mentionsThisNPC(it)) { + rows.push({ + from: p.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + session: it.session ?? "", + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); + } + } +} + +// Sort newest first +rows.sort((a,b) => String(b.date).localeCompare(String(a.date))); + +if (!rows.length) { + dv.paragraph("_No intel yet._"); +} else { + dv.paragraph(`_Entries: ${rows.length} (own ${(THIS.Intel ?? []).length} + mentions ${Math.max(0, rows.length - (THIS.Intel ?? []).length)})_`); + dv.table( + ["From Note","Date","Type","Summary","Session","Source","Rel","Cred","Tags"], + rows.map(r => [r.from, r.date, r.type, r.summary, r.session, r.source, r.rel, r.cred, r.tags]) + ); +} +``` + +```button +name βž• Add Intel +type command +action Templater: Insert Add Intel Entry to NPC +``` + +--- + ## πŸ›οΈ Relations & Factions - **Primary Faction:** [[House Noctilun]] - However, in conversation with Calia, she mentions that she is not in the houses good graces, hence why she is stationed in Y'athyr as opposed to Ri'lesera diff --git a/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Deidre Gille.md b/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Deidre Gille.md index 8b1748d..f2cc58a 100644 --- a/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Deidre Gille.md +++ b/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Deidre Gille.md @@ -41,6 +41,104 @@ Tags: --- +## 🧠 Intel (auto) +```dataviewjs +const THIS = dv.current(); +const thisName = String(THIS.file.name); +const thisLC = thisName.toLowerCase(); + +// Scan faction & NPC notes that have Intel arrays +const pages = dv.pages("#npc or #faction").where(p => p.Intel); + +// Does an Intel entry reference THIS NPC? +function mentionsThisNPC(entry) { + const sum = String(entry.summary ?? ""); + + // 1) Explicit wikilink in summary + if (sum.includes(`[[${thisName}]]`) || sum.includes(`[[${thisName}|`)) return true; + + // 2) Source field (string/link/list) + const src = entry.source; + const arr = Array.isArray(src) ? src : (src ? [src] : []); + for (const s of arr) { + if (typeof s === "string") { + // quoted link or plain text + const unq = s.replace(/^"+|"+$/g, ""); // drop surrounding quotes if any + if (unq.includes(`[[${thisName}]]`) || unq.includes(`[[${thisName}|`)) return true; + // if they typed just the name + if (unq.replace(/\[\[|\]\]/g, "").trim().toLowerCase() === thisLC) return true; + } else if (s && typeof s === "object" && s.path) { + // wikilink object + const base = s.path.split("/").pop().replace(/\.md$/i, "").toLowerCase(); + if (base === thisLC) return true; + } + } + + // Optional (looser): plain text mention in summary + // return sum.toLowerCase().includes(thisLC); + + return false; +} + +// Collect rows: 1) own Intel, 2) mentions from other notes +const rows = []; + +// Own Intel (from this NPC note) +for (const it of (THIS.Intel ?? [])) { + rows.push({ + from: THIS.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + session: it.session ?? "", + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); +} + +// Mentions elsewhere +for (const p of pages.where(p => p.file.path !== THIS.file.path)) { + for (const it of (p.Intel ?? [])) { + if (mentionsThisNPC(it)) { + rows.push({ + from: p.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + session: it.session ?? "", + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); + } + } +} + +// Sort newest first +rows.sort((a,b) => String(b.date).localeCompare(String(a.date))); + +if (!rows.length) { + dv.paragraph("_No intel yet._"); +} else { + dv.paragraph(`_Entries: ${rows.length} (own ${(THIS.Intel ?? []).length} + mentions ${Math.max(0, rows.length - (THIS.Intel ?? []).length)})_`); + dv.table( + ["From Note","Date","Type","Summary","Session","Source","Rel","Cred","Tags"], + rows.map(r => [r.from, r.date, r.type, r.summary, r.session, r.source, r.rel, r.cred, r.tags]) + ); +} +``` + +```button +name βž• Add Intel +type command +action Templater: Insert Add Intel Entry to NPC +``` + +--- + ## πŸ›οΈ Relations & Factions - **Primary Faction:** [[House Gille]] - **Allied Factions:** [[Faction A]], [[Faction B]] diff --git a/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Elthea Rinvale.md b/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Elthea Rinvale.md index de2ff16..ca00c72 100644 --- a/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Elthea Rinvale.md +++ b/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Elthea Rinvale.md @@ -44,6 +44,104 @@ Tags: --- +## 🧠 Intel (auto) +```dataviewjs +const THIS = dv.current(); +const thisName = String(THIS.file.name); +const thisLC = thisName.toLowerCase(); + +// Scan faction & NPC notes that have Intel arrays +const pages = dv.pages("#npc or #faction").where(p => p.Intel); + +// Does an Intel entry reference THIS NPC? +function mentionsThisNPC(entry) { + const sum = String(entry.summary ?? ""); + + // 1) Explicit wikilink in summary + if (sum.includes(`[[${thisName}]]`) || sum.includes(`[[${thisName}|`)) return true; + + // 2) Source field (string/link/list) + const src = entry.source; + const arr = Array.isArray(src) ? src : (src ? [src] : []); + for (const s of arr) { + if (typeof s === "string") { + // quoted link or plain text + const unq = s.replace(/^"+|"+$/g, ""); // drop surrounding quotes if any + if (unq.includes(`[[${thisName}]]`) || unq.includes(`[[${thisName}|`)) return true; + // if they typed just the name + if (unq.replace(/\[\[|\]\]/g, "").trim().toLowerCase() === thisLC) return true; + } else if (s && typeof s === "object" && s.path) { + // wikilink object + const base = s.path.split("/").pop().replace(/\.md$/i, "").toLowerCase(); + if (base === thisLC) return true; + } + } + + // Optional (looser): plain text mention in summary + // return sum.toLowerCase().includes(thisLC); + + return false; +} + +// Collect rows: 1) own Intel, 2) mentions from other notes +const rows = []; + +// Own Intel (from this NPC note) +for (const it of (THIS.Intel ?? [])) { + rows.push({ + from: THIS.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + session: it.session ?? "", + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); +} + +// Mentions elsewhere +for (const p of pages.where(p => p.file.path !== THIS.file.path)) { + for (const it of (p.Intel ?? [])) { + if (mentionsThisNPC(it)) { + rows.push({ + from: p.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + session: it.session ?? "", + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); + } + } +} + +// Sort newest first +rows.sort((a,b) => String(b.date).localeCompare(String(a.date))); + +if (!rows.length) { + dv.paragraph("_No intel yet._"); +} else { + dv.paragraph(`_Entries: ${rows.length} (own ${(THIS.Intel ?? []).length} + mentions ${Math.max(0, rows.length - (THIS.Intel ?? []).length)})_`); + dv.table( + ["From Note","Date","Type","Summary","Session","Source","Rel","Cred","Tags"], + rows.map(r => [r.from, r.date, r.type, r.summary, r.session, r.source, r.rel, r.cred, r.tags]) + ); +} +``` + +```button +name βž• Add Intel +type command +action Templater: Insert Add Intel Entry to NPC +``` + +--- + ## πŸ›οΈ Relations & Factions - **Primary Faction:** [[Faction Name]] - **Allied Factions:** [[Faction A]], [[Faction B]] diff --git a/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Fenric Draycott.md b/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Fenric Draycott.md index 0c7d464..6a3bd48 100644 --- a/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Fenric Draycott.md +++ b/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Fenric Draycott.md @@ -38,6 +38,104 @@ Tags: --- +## 🧠 Intel (auto) +```dataviewjs +const THIS = dv.current(); +const thisName = String(THIS.file.name); +const thisLC = thisName.toLowerCase(); + +// Scan faction & NPC notes that have Intel arrays +const pages = dv.pages("#npc or #faction").where(p => p.Intel); + +// Does an Intel entry reference THIS NPC? +function mentionsThisNPC(entry) { + const sum = String(entry.summary ?? ""); + + // 1) Explicit wikilink in summary + if (sum.includes(`[[${thisName}]]`) || sum.includes(`[[${thisName}|`)) return true; + + // 2) Source field (string/link/list) + const src = entry.source; + const arr = Array.isArray(src) ? src : (src ? [src] : []); + for (const s of arr) { + if (typeof s === "string") { + // quoted link or plain text + const unq = s.replace(/^"+|"+$/g, ""); // drop surrounding quotes if any + if (unq.includes(`[[${thisName}]]`) || unq.includes(`[[${thisName}|`)) return true; + // if they typed just the name + if (unq.replace(/\[\[|\]\]/g, "").trim().toLowerCase() === thisLC) return true; + } else if (s && typeof s === "object" && s.path) { + // wikilink object + const base = s.path.split("/").pop().replace(/\.md$/i, "").toLowerCase(); + if (base === thisLC) return true; + } + } + + // Optional (looser): plain text mention in summary + // return sum.toLowerCase().includes(thisLC); + + return false; +} + +// Collect rows: 1) own Intel, 2) mentions from other notes +const rows = []; + +// Own Intel (from this NPC note) +for (const it of (THIS.Intel ?? [])) { + rows.push({ + from: THIS.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + session: it.session ?? "", + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); +} + +// Mentions elsewhere +for (const p of pages.where(p => p.file.path !== THIS.file.path)) { + for (const it of (p.Intel ?? [])) { + if (mentionsThisNPC(it)) { + rows.push({ + from: p.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + session: it.session ?? "", + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); + } + } +} + +// Sort newest first +rows.sort((a,b) => String(b.date).localeCompare(String(a.date))); + +if (!rows.length) { + dv.paragraph("_No intel yet._"); +} else { + dv.paragraph(`_Entries: ${rows.length} (own ${(THIS.Intel ?? []).length} + mentions ${Math.max(0, rows.length - (THIS.Intel ?? []).length)})_`); + dv.table( + ["From Note","Date","Type","Summary","Session","Source","Rel","Cred","Tags"], + rows.map(r => [r.from, r.date, r.type, r.summary, r.session, r.source, r.rel, r.cred, r.tags]) + ); +} +``` + +```button +name βž• Add Intel +type command +action Templater: Insert Add Intel Entry to NPC +``` + +--- + ## πŸ›οΈ Relations & Factions - **Primary Faction:** [[Draycott Family]] - **Allied Factions:** [[House Valora]] diff --git a/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Iniana Asase.md b/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Iniana Asase.md index 8769217..6ccd0a7 100644 --- a/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Iniana Asase.md +++ b/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Iniana Asase.md @@ -48,6 +48,104 @@ Tags: --- +## 🧠 Intel (auto) +```dataviewjs +const THIS = dv.current(); +const thisName = String(THIS.file.name); +const thisLC = thisName.toLowerCase(); + +// Scan faction & NPC notes that have Intel arrays +const pages = dv.pages("#npc or #faction").where(p => p.Intel); + +// Does an Intel entry reference THIS NPC? +function mentionsThisNPC(entry) { + const sum = String(entry.summary ?? ""); + + // 1) Explicit wikilink in summary + if (sum.includes(`[[${thisName}]]`) || sum.includes(`[[${thisName}|`)) return true; + + // 2) Source field (string/link/list) + const src = entry.source; + const arr = Array.isArray(src) ? src : (src ? [src] : []); + for (const s of arr) { + if (typeof s === "string") { + // quoted link or plain text + const unq = s.replace(/^"+|"+$/g, ""); // drop surrounding quotes if any + if (unq.includes(`[[${thisName}]]`) || unq.includes(`[[${thisName}|`)) return true; + // if they typed just the name + if (unq.replace(/\[\[|\]\]/g, "").trim().toLowerCase() === thisLC) return true; + } else if (s && typeof s === "object" && s.path) { + // wikilink object + const base = s.path.split("/").pop().replace(/\.md$/i, "").toLowerCase(); + if (base === thisLC) return true; + } + } + + // Optional (looser): plain text mention in summary + // return sum.toLowerCase().includes(thisLC); + + return false; +} + +// Collect rows: 1) own Intel, 2) mentions from other notes +const rows = []; + +// Own Intel (from this NPC note) +for (const it of (THIS.Intel ?? [])) { + rows.push({ + from: THIS.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + session: it.session ?? "", + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); +} + +// Mentions elsewhere +for (const p of pages.where(p => p.file.path !== THIS.file.path)) { + for (const it of (p.Intel ?? [])) { + if (mentionsThisNPC(it)) { + rows.push({ + from: p.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + session: it.session ?? "", + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); + } + } +} + +// Sort newest first +rows.sort((a,b) => String(b.date).localeCompare(String(a.date))); + +if (!rows.length) { + dv.paragraph("_No intel yet._"); +} else { + dv.paragraph(`_Entries: ${rows.length} (own ${(THIS.Intel ?? []).length} + mentions ${Math.max(0, rows.length - (THIS.Intel ?? []).length)})_`); + dv.table( + ["From Note","Date","Type","Summary","Session","Source","Rel","Cred","Tags"], + rows.map(r => [r.from, r.date, r.type, r.summary, r.session, r.source, r.rel, r.cred, r.tags]) + ); +} +``` + +```button +name βž• Add Intel +type command +action Templater: Insert Add Intel Entry to NPC +``` + +--- + ## πŸ›οΈ Relations & Factions - **Primary Faction:** [[House Asase]] - **Allied Factions:** [[Faction A]], [[Faction B]] diff --git a/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Laughlin.md b/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Laughlin.md index 29ed1ed..f2a7724 100644 --- a/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Laughlin.md +++ b/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Laughlin.md @@ -3,8 +3,10 @@ Level_of_Influence: 2 Faction: Tags: - npc +Intel: --- + # Laughlin *Figment Grandhall Scholar, Solicitor, Schmoozer and Fundraiser* @@ -47,6 +49,104 @@ Tags: --- +## 🧠 Intel (auto) +```dataviewjs +const THIS = dv.current(); +const thisName = String(THIS.file.name); +const thisLC = thisName.toLowerCase(); + +// Scan faction & NPC notes that have Intel arrays +const pages = dv.pages("#npc or #faction").where(p => p.Intel); + +// Does an Intel entry reference THIS NPC? +function mentionsThisNPC(entry) { + const sum = String(entry.summary ?? ""); + + // 1) Explicit wikilink in summary + if (sum.includes(`[[${thisName}]]`) || sum.includes(`[[${thisName}|`)) return true; + + // 2) Source field (string/link/list) + const src = entry.source; + const arr = Array.isArray(src) ? src : (src ? [src] : []); + for (const s of arr) { + if (typeof s === "string") { + // quoted link or plain text + const unq = s.replace(/^"+|"+$/g, ""); // drop surrounding quotes if any + if (unq.includes(`[[${thisName}]]`) || unq.includes(`[[${thisName}|`)) return true; + // if they typed just the name + if (unq.replace(/\[\[|\]\]/g, "").trim().toLowerCase() === thisLC) return true; + } else if (s && typeof s === "object" && s.path) { + // wikilink object + const base = s.path.split("/").pop().replace(/\.md$/i, "").toLowerCase(); + if (base === thisLC) return true; + } + } + + // Optional (looser): plain text mention in summary + // return sum.toLowerCase().includes(thisLC); + + return false; +} + +// Collect rows: 1) own Intel, 2) mentions from other notes +const rows = []; + +// Own Intel (from this NPC note) +for (const it of (THIS.Intel ?? [])) { + rows.push({ + from: THIS.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + session: it.session ?? "", + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); +} + +// Mentions elsewhere +for (const p of pages.where(p => p.file.path !== THIS.file.path)) { + for (const it of (p.Intel ?? [])) { + if (mentionsThisNPC(it)) { + rows.push({ + from: p.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + session: it.session ?? "", + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); + } + } +} + +// Sort newest first +rows.sort((a,b) => String(b.date).localeCompare(String(a.date))); + +if (!rows.length) { + dv.paragraph("_No intel yet._"); +} else { + dv.paragraph(`_Entries: ${rows.length} (own ${(THIS.Intel ?? []).length} + mentions ${Math.max(0, rows.length - (THIS.Intel ?? []).length)})_`); + dv.table( + ["From Note","Date","Type","Summary","Session","Source","Rel","Cred","Tags"], + rows.map(r => [r.from, r.date, r.type, r.summary, r.session, r.source, r.rel, r.cred, r.tags]) + ); +} +``` + +```button +name βž• Add Intel +type command +action Templater: Insert Add Intel Entry to NPC +``` + +--- + ## πŸ›οΈ Relations & Factions - **Primary Faction:** [[Faction Name]] - **Allied Factions:** [[Faction A]], [[Faction B]] diff --git a/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Lord Ellin Vaust.md b/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Lord Ellin Vaust.md index d1e4190..95fd2f0 100644 --- a/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Lord Ellin Vaust.md +++ b/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Lord Ellin Vaust.md @@ -40,6 +40,104 @@ Tags: --- +## 🧠 Intel (auto) +```dataviewjs +const THIS = dv.current(); +const thisName = String(THIS.file.name); +const thisLC = thisName.toLowerCase(); + +// Scan faction & NPC notes that have Intel arrays +const pages = dv.pages("#npc or #faction").where(p => p.Intel); + +// Does an Intel entry reference THIS NPC? +function mentionsThisNPC(entry) { + const sum = String(entry.summary ?? ""); + + // 1) Explicit wikilink in summary + if (sum.includes(`[[${thisName}]]`) || sum.includes(`[[${thisName}|`)) return true; + + // 2) Source field (string/link/list) + const src = entry.source; + const arr = Array.isArray(src) ? src : (src ? [src] : []); + for (const s of arr) { + if (typeof s === "string") { + // quoted link or plain text + const unq = s.replace(/^"+|"+$/g, ""); // drop surrounding quotes if any + if (unq.includes(`[[${thisName}]]`) || unq.includes(`[[${thisName}|`)) return true; + // if they typed just the name + if (unq.replace(/\[\[|\]\]/g, "").trim().toLowerCase() === thisLC) return true; + } else if (s && typeof s === "object" && s.path) { + // wikilink object + const base = s.path.split("/").pop().replace(/\.md$/i, "").toLowerCase(); + if (base === thisLC) return true; + } + } + + // Optional (looser): plain text mention in summary + // return sum.toLowerCase().includes(thisLC); + + return false; +} + +// Collect rows: 1) own Intel, 2) mentions from other notes +const rows = []; + +// Own Intel (from this NPC note) +for (const it of (THIS.Intel ?? [])) { + rows.push({ + from: THIS.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + session: it.session ?? "", + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); +} + +// Mentions elsewhere +for (const p of pages.where(p => p.file.path !== THIS.file.path)) { + for (const it of (p.Intel ?? [])) { + if (mentionsThisNPC(it)) { + rows.push({ + from: p.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + session: it.session ?? "", + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); + } + } +} + +// Sort newest first +rows.sort((a,b) => String(b.date).localeCompare(String(a.date))); + +if (!rows.length) { + dv.paragraph("_No intel yet._"); +} else { + dv.paragraph(`_Entries: ${rows.length} (own ${(THIS.Intel ?? []).length} + mentions ${Math.max(0, rows.length - (THIS.Intel ?? []).length)})_`); + dv.table( + ["From Note","Date","Type","Summary","Session","Source","Rel","Cred","Tags"], + rows.map(r => [r.from, r.date, r.type, r.summary, r.session, r.source, r.rel, r.cred, r.tags]) + ); +} +``` + +```button +name βž• Add Intel +type command +action Templater: Insert Add Intel Entry to NPC +``` + +--- + ## πŸ›οΈ Relations & Factions - **Primary Faction:** [[Faction Name]] - **Allied Factions:** [[Faction A]], [[Faction B]] diff --git a/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Maltrix.md b/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Maltrix.md index d53a601..518a588 100644 --- a/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Maltrix.md +++ b/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Maltrix.md @@ -38,6 +38,104 @@ Tags: --- +## 🧠 Intel (auto) +```dataviewjs +const THIS = dv.current(); +const thisName = String(THIS.file.name); +const thisLC = thisName.toLowerCase(); + +// Scan faction & NPC notes that have Intel arrays +const pages = dv.pages("#npc or #faction").where(p => p.Intel); + +// Does an Intel entry reference THIS NPC? +function mentionsThisNPC(entry) { + const sum = String(entry.summary ?? ""); + + // 1) Explicit wikilink in summary + if (sum.includes(`[[${thisName}]]`) || sum.includes(`[[${thisName}|`)) return true; + + // 2) Source field (string/link/list) + const src = entry.source; + const arr = Array.isArray(src) ? src : (src ? [src] : []); + for (const s of arr) { + if (typeof s === "string") { + // quoted link or plain text + const unq = s.replace(/^"+|"+$/g, ""); // drop surrounding quotes if any + if (unq.includes(`[[${thisName}]]`) || unq.includes(`[[${thisName}|`)) return true; + // if they typed just the name + if (unq.replace(/\[\[|\]\]/g, "").trim().toLowerCase() === thisLC) return true; + } else if (s && typeof s === "object" && s.path) { + // wikilink object + const base = s.path.split("/").pop().replace(/\.md$/i, "").toLowerCase(); + if (base === thisLC) return true; + } + } + + // Optional (looser): plain text mention in summary + // return sum.toLowerCase().includes(thisLC); + + return false; +} + +// Collect rows: 1) own Intel, 2) mentions from other notes +const rows = []; + +// Own Intel (from this NPC note) +for (const it of (THIS.Intel ?? [])) { + rows.push({ + from: THIS.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + session: it.session ?? "", + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); +} + +// Mentions elsewhere +for (const p of pages.where(p => p.file.path !== THIS.file.path)) { + for (const it of (p.Intel ?? [])) { + if (mentionsThisNPC(it)) { + rows.push({ + from: p.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + session: it.session ?? "", + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); + } + } +} + +// Sort newest first +rows.sort((a,b) => String(b.date).localeCompare(String(a.date))); + +if (!rows.length) { + dv.paragraph("_No intel yet._"); +} else { + dv.paragraph(`_Entries: ${rows.length} (own ${(THIS.Intel ?? []).length} + mentions ${Math.max(0, rows.length - (THIS.Intel ?? []).length)})_`); + dv.table( + ["From Note","Date","Type","Summary","Session","Source","Rel","Cred","Tags"], + rows.map(r => [r.from, r.date, r.type, r.summary, r.session, r.source, r.rel, r.cred, r.tags]) + ); +} +``` + +```button +name βž• Add Intel +type command +action Templater: Insert Add Intel Entry to NPC +``` + +--- + ## πŸ›οΈ Relations & Factions - **Primary Faction:** [[Faction Name]] - **Allied Factions:** [[Faction A]], [[Faction B]] diff --git a/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Ronan Elidari.md b/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Ronan Elidari.md index d265458..9aa210d 100644 --- a/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Ronan Elidari.md +++ b/Dungeons and Dragons/A Legacy Forged/People, Factions and Relationships/NPCs/Ronan Elidari.md @@ -47,6 +47,104 @@ Tags: --- + +## 🧠 Intel (auto) +```dataviewjs +const THIS = dv.current(); +const thisName = String(THIS.file.name); +const thisLC = thisName.toLowerCase(); + +// Scan faction & NPC notes that have Intel arrays +const pages = dv.pages("#npc or #faction").where(p => p.Intel); + +// Does an Intel entry reference THIS NPC? +function mentionsThisNPC(entry) { + const sum = String(entry.summary ?? ""); + + // 1) Explicit wikilink in summary + if (sum.includes(`[[${thisName}]]`) || sum.includes(`[[${thisName}|`)) return true; + + // 2) Source field (string/link/list) + const src = entry.source; + const arr = Array.isArray(src) ? src : (src ? [src] : []); + for (const s of arr) { + if (typeof s === "string") { + // quoted link or plain text + const unq = s.replace(/^"+|"+$/g, ""); // drop surrounding quotes if any + if (unq.includes(`[[${thisName}]]`) || unq.includes(`[[${thisName}|`)) return true; + // if they typed just the name + if (unq.replace(/\[\[|\]\]/g, "").trim().toLowerCase() === thisLC) return true; + } else if (s && typeof s === "object" && s.path) { + // wikilink object + const base = s.path.split("/").pop().replace(/\.md$/i, "").toLowerCase(); + if (base === thisLC) return true; + } + } + + // Optional (looser): plain text mention in summary + // return sum.toLowerCase().includes(thisLC); + + return false; +} + +// Collect rows: 1) own Intel, 2) mentions from other notes +const rows = []; + +// Own Intel (from this NPC note) +for (const it of (THIS.Intel ?? [])) { + rows.push({ + from: THIS.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + session: it.session ?? "", + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); +} + +// Mentions elsewhere +for (const p of pages.where(p => p.file.path !== THIS.file.path)) { + for (const it of (p.Intel ?? [])) { + if (mentionsThisNPC(it)) { + rows.push({ + from: p.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + session: it.session ?? "", + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); + } + } +} + +// Sort newest first +rows.sort((a,b) => String(b.date).localeCompare(String(a.date))); + +if (!rows.length) { + dv.paragraph("_No intel yet._"); +} else { + dv.paragraph(`_Entries: ${rows.length} (own ${(THIS.Intel ?? []).length} + mentions ${Math.max(0, rows.length - (THIS.Intel ?? []).length)})_`); + dv.table( + ["From Note","Date","Type","Summary","Session","Source","Rel","Cred","Tags"], + rows.map(r => [r.from, r.date, r.type, r.summary, r.session, r.source, r.rel, r.cred, r.tags]) + ); +} +``` + +```button +name βž• Add Intel +type command +action Templater: Insert Add Intel Entry to NPC +``` + +--- ## πŸ›οΈ Relations & Factions - **Primary Faction:** [[House Elidari]] - **Allied Factions:** [[Faction A]], [[Faction B]] diff --git a/Dungeons and Dragons/A Legacy Forged/Session Notes/Session 10 - Wined, Dined and Influenced.md b/Dungeons and Dragons/A Legacy Forged/Session Notes/Session 10 - Wined, Dined and Influenced.md index c7234ff..a6f225a 100644 --- a/Dungeons and Dragons/A Legacy Forged/Session Notes/Session 10 - Wined, Dined and Influenced.md +++ b/Dungeons and Dragons/A Legacy Forged/Session Notes/Session 10 - Wined, Dined and Influenced.md @@ -8,6 +8,7 @@ Tags: + # Session 10 - Wined, Dined and Influenced > **Date:** `= this.Date` β€’ **Name:** `= this.Session_Name` @@ -16,19 +17,6 @@ Tags: ## πŸ“„ Session Summary (auto) -At dinner, the party split across three tables to engage the city’s notables. -- **Val & Calen with [[Iniana Asase]]** - - Learned her activism roots in the Drenches and struggles with the [[Thin Wisps]]. - - She advised the party to seek official **licensure** for legitimacy. - - Discussions focused on improving Y’athyr and how to confront entrenched problems. -- **[[Faylen Valora]] & Metternich with [[Ronan Elidari]], [[Amity Cornwallis]], [[Deidre Gille]]** - - [[Faylen Valora]]’s speech earned **+1 Influence with every notable**, though reactions varied. - - Later debate with Ronan won **+1 Influence with [[Amity Cornwallis**]]. - - Metternich bonded with Amity privately, trading cynical and practical views on leadership and power. - - His attempt at Discovery with Deidre Gille failed. -- **Kite with Elthea Rinvale** - - Discussed her presence in Y’athyr, her business with the Consortium, and his reasons for leaving Castore. - - Conversation touched on Trackless vs. E’in society. ```button @@ -152,7 +140,65 @@ action Templater: Insert Generate Session Summary --- -## 🧠 Intel Captured +## 🧠 Intel Captured (auto) + +```dataviewjs +const THIS = dv.current(); +const sessName = THIS.file.name; +const sessDate = THIS.Date ?? dv.current().file.frontmatter?.Date ?? ""; +const sessLC = sessName.toLowerCase(); + +const pages = dv.pages("#npc or #faction").where(p => p.Intel); + +function matchesSessionField(sessionField) { + const list = Array.isArray(sessionField) ? sessionField : [sessionField]; + for (const s of list) { + if (!s) continue; + if (typeof s === "object" && s.path) { + const base = s.path.split("/").pop().replace(/\.md$/i,""); + if (base.toLowerCase() === sessLC) return true; + } else { + const text = String(s); + if (text.includes(`[[${sessName}]]`)) return true; + if (text.toLowerCase() === sessLC) return true; + } + } + return false; +} + +const rows = []; +for (const p of pages) { + for (const it of p.Intel) { + let ok = false; + if (it.session) ok = matchesSessionField(it.session); + if (!ok && sessDate && it.date) ok = String(it.date) === String(sessDate); // fallback + if (ok) { + rows.push({ + from: p.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); + } + } +} + +rows.sort((a,b)=> String(b.date).localeCompare(String(a.date))); + +if (!rows.length) { + dv.paragraph("_No intel captured for this session yet._"); +} else { + dv.table( + ["From", "Date", "Type", "Summary", "Source", "Rel", "Cred", "Tags"], + rows.map(r => [r.from, r.date, r.type, r.summary, r.source, r.rel, r.cred, r.tags]) + ); +} +``` + --- diff --git a/Templates/Add Intel Entry to NPC.md b/Templates/Add Intel Entry to NPC.md new file mode 100644 index 0000000..ac5362f --- /dev/null +++ b/Templates/Add Intel Entry to NPC.md @@ -0,0 +1,128 @@ +<%* +/* Add Intel Entry (NPC) β€” clear, step-labeled prompts */ + +const file = app.workspace.getActiveFile(); +if (!file) { new Notice("Open an NPC note first."); return; } + +const today = tp.date.now("YYYY-MM-DD"); + +/* β€”β€”β€” Step 1: TYPE β€”β€”β€” */ +new Notice("Step 1/7 β€” Type (what kind of intel?)"); +const typeDisp = [ + "interaction β€” direct conversation/action with this NPC", + "report β€” secondhand info relayed by someone else", + "rumor β€” unverified hearsay", + "asset β€” resource/contact identified or secured", + "op β€” operation/mission detail" +]; +const typeVals = ["interaction","report","rumor","asset","op"]; +const TYPE = await tp.system.suggester(typeDisp, typeVals); +if (!TYPE) { new Notice("Cancelled."); return; } + +/* β€”β€”β€” Step 2: SUMMARY β€”β€”β€” */ +new Notice("Step 2/7 β€” Summary (what happened?)"); +const summary = await tp.system.prompt( + "Summary (1–2 sentences). You can include [[links]] to NPCs/factions/places.\nExample: Says that [[House Asase]] is rebuilding the Drenches walls." +) ?? ""; + +/* β€”β€”β€” Step 3: SESSION link (optional) β€”β€”β€” */ +new Notice("Step 3/7 β€” Session link (optional)"); +const allFiles = app.vault.getMarkdownFiles(); +function hasSessionTag(f) { + const fm = app.metadataCache.getFileCache(f)?.frontmatter ?? {}; + const tags = fm.Tags ?? fm.tags ?? []; + const arr = Array.isArray(tags) ? tags : [tags]; + const lower = arr.map(x => String(x||"").toLowerCase()); + return lower.includes("session") || lower.includes("pf2e"); +} +const sessions = allFiles.filter(hasSessionTag).sort((a,b)=>{ + const fa = app.metadataCache.getFileCache(a)?.frontmatter ?? {}; + const fb = app.metadataCache.getFileCache(b)?.frontmatter ?? {}; + const da = String(fa.Date||""); + const db = String(fb.Date||""); + return db.localeCompare(da) || b.basename.localeCompare(a.basename); +}); +const sessionChoicesDisp = ["(skip β€” no session link)"].concat( + sessions.map(f=>{ + const d = app.metadataCache.getFileCache(f)?.frontmatter?.Date ?? ""; + return d ? `Link to session: [[${f.basename}]] β€” ${d}` : `Link to session: [[${f.basename}]]`; + }) +); +const sessionChoicesVal = ["__skip__"].concat(sessions.map(f=>`[[${f.basename}]]`)); +let sesPick = await tp.system.suggester(sessionChoicesDisp, sessionChoicesVal); +if (!sesPick) sesPick = "__skip__"; +const sessionField = (sesPick === "__skip__") ? undefined : sesPick; + +/* β€”β€”β€” Step 4: SOURCE (who/what told us?) β€”β€”β€” */ +new Notice("Step 4/7 β€” Source (who/what told us?)"); +const srcChoicesDisp = ["(skip)","(manual entry β€” type your own)"].concat( + allFiles.map(f=>`[[${f.basename}]]`).sort((a,b)=>a.localeCompare(b, undefined, {sensitivity:"base"})) +); +const srcChoicesVal = ["__skip__","__manual__"].concat( + allFiles.map(f=>`[[${f.basename}]]`).sort((a,b)=>a.localeCompare(b, undefined, {sensitivity:"base"})) +); +let srcPick = await tp.system.suggester(srcChoicesDisp, srcChoicesVal); +if (!srcPick) srcPick = "__skip__"; +let sourceField = ""; +if (srcPick === "__manual__") { + sourceField = (await tp.system.prompt("Type any source (free text or [[Link]])")) ?? ""; +} else if (srcPick !== "__skip__") { + sourceField = srcPick; +} + +/* β€”β€”β€” Step 5: RELIABILITY (A–E) β€”β€”β€” */ +new Notice("Step 5/7 β€” Reliability (A–E)"); +const relDisp = [ + "A β€” confirmed (firsthand/corroborated)", + "B β€” likely (trustworthy witness/evidence)", + "C β€” uncertain (unverified details)", + "D β€” doubtful (conflicts / low trust)", + "E β€” unknown (no basis to judge)" +]; +const relVals = ["A","B","C","D","E"]; +const reliability = await tp.system.suggester(relDisp, relVals) ?? "C"; + +/* β€”β€”β€” Step 6: CREDIBILITY (1–5) β€”β€”β€” */ +new Notice("Step 6/7 β€” Credibility (1–5)"); +const credDisp = ["5 β€” strong","4 β€” good","3 β€” fair","2 β€” weak","1 β€” very weak"]; +const credVals = ["5","4","3","2","1"]; +const credibility = Number(await tp.system.suggester(credDisp, credVals) ?? "3"); + +/* β€”β€”β€” Step 7: TAGS (multi-select, optional) β€”β€”β€” */ +new Notice("Step 7/7 β€” Tags (optional)"); +const tagMap = app.metadataCache.getTags?.() ?? {}; +let allTags = Array.from(new Set(Object.keys(tagMap).map(t => t.replace(/^#/, "")))) + .sort((a,b)=>a.localeCompare(b, undefined, {sensitivity:"base"})); +const chosen = new Set(); +while (true) { + const remaining = allTags.filter(t => !chosen.has(t)); + const disp = ["(done)","(new tag…)"].concat(remaining.map(t=>`#${t}`)); + const vals = ["__done__","__new__"].concat(remaining); + const pick = await tp.system.suggester(disp, vals); + if (!pick || pick === "__done__") break; + if (pick === "__new__") { + const nt = (await tp.system.prompt("New tag (without #)"))?.trim(); + if (nt) { chosen.add(nt); if (!allTags.includes(nt)) allTags.push(nt); } + } else { + chosen.add(pick); + } +} +const tags = Array.from(chosen); + +/* β€”β€”β€” Write to frontmatter β€”β€”β€” */ +await app.fileManager.processFrontMatter(file, (fm) => { + if (!Array.isArray(fm.Intel)) fm.Intel = []; + fm.Intel.unshift({ + date: today, + type: TYPE, + summary, + session: sessionField, // shows up in NPC/Session tables + source: sourceField || undefined, // e.g., [[Ronan Elidari]] or free text + reliability, + credibility, + tags + }); +}); + +new Notice("NPC Intel entry added."); +%> diff --git a/Templates/Generate Session Summary.md b/Templates/Generate Session Summary.md index 34a939b..e40a196 100644 --- a/Templates/Generate Session Summary.md +++ b/Templates/Generate Session Summary.md @@ -1,101 +1,120 @@ <%* -/* PF2e AI Session Summary β€” FULL NOTE MODE (single-string call) - - Uses the whole note body (not just bullets) - - Strips frontmatter, code fences, and prior summaries/history - - Lets you pick Short / Standard / Detailed length +/* PF2e AI Session Summary β€” CHUNKED + ROBUST (AI for Templater) + - Summarizes each section separately to avoid long-context issues + - Preserves [[wikilinks]]; strips headings/labels/scaffolding + - Writes the final recap between SUMMARY markers */ const file = app.workspace.getActiveFile(); if (!file) { new Notice("Open a session note first."); return; } -/* ---------- Read & sanitize whole note ---------- */ -let body = await app.vault.read(file); +// ---------- read & basic cleaning ---------- +let raw = await app.vault.read(file); +raw = raw.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, ""); // strip YAML +raw = raw.replace(/[\s\S]*?/gi, ""); +raw = raw.replace(/```[\s\S]*?```/g, ""); // code fences +raw = raw.replace(/`=\s*[^`]+`/g, ""); // inline dataview +raw = raw.replace(/\r/g, ""); -// strip YAML frontmatter -body = body.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, ""); +// ---------- helpers ---------- +function sectionBetween(names){ + for (const name of names){ + const reHead = new RegExp(`^##\\s*.*${name}.*$`,"im"); + const m = raw.match(reHead); + if (!m) continue; + const start = m.index + m[0].length; + const rest = raw.slice(start); + const next = rest.search(/^##\s+/m); + return raw.slice(start, next === -1 ? raw.length : start + next).trim(); + } + return ""; +} +function linesClean(block){ + if (!block) return []; + const BOLD_LABEL=/^\s*(?:[-*]\s+)?\*\*[^*]+:\*\*\s*$/i; // **Label:** + const ROUND=/^\s*(?:[-*]\s*)?round\s+\d+\s*$/i; + const HR=/^\s*[-*_]{3,}\s*$/; + const QUOTE=/^\s*>/; + const H=/^\s*#{1,6}\s+/; + return block.split("\n") + .map(s=>s.trim()) + .filter(s=>s && !BOLD_LABEL.test(s) && !ROUND.test(s) && !HR.test(s) && !QUOTE.test(s) && !H.test(s)) + .map(s=>s.replace(/\[\[\s*\]\]/g,"")) + .map(s=>s.replace(/\s+β€”\s*$/,"")); +} -// strip prior summaries & history blocks if present -body = body.replace(/[\s\S]*?/gi, ""); -body = body.replace(/[\s\S]*?/gi, ""); +// ---------- pull sections (accept emoji or plain headings) ---------- +const S = { + whereWhen : sectionBetween(["Where / When"]), + pcs : sectionBetween(["PCs Present / Absent","PCs Present","Present / Absent"]), + highlights: sectionBetween(["⭐ Highlights","Highlights"]), + quests : sectionBetween(["🎯 Quests & Progress","Quests & Progress","Quests"]), + npcs : sectionBetween(["πŸ—£οΈ NPCs Met / Factions","NPCs Met / Factions","NPCs / Factions","NPCs"]), + encounters: sectionBetween(["βš”οΈ Encounters","Encounters"]), + intel : sectionBetween(["🧠 Intel Captured","Intel Captured"]), + treasure : sectionBetween(["πŸ“¦ Treasure & Rewards","Treasure & Rewards","Treasure"]), + nextHooks : sectionBetween(["πŸ”œ Next Hooks / To-Dos","Next Hooks / To-Dos","Next Hooks","Hooks"]), +}; -// strip code fences & inline dataview -body = body.replace(/```[\s\S]*?```/g, ""); -body = body.replace(/`=\s*[^`]+`/g, ""); +// ---------- chunk prompts ---------- +async function summarizeBullets(label, text, max=6){ + text = linesClean(text).join("\n").trim(); + if (!text) return []; + const prompt = `You are a concise Pathfinder 2e GM assistant. +Summarize the "${label}" section below as **bullet points only** (max ${max}). +Keep existing [[wikilinks]] exactly as written. Do not echo headings, labels, or placeholders. -// normalize whitespace -body = body.replace(/\r/g, "").trim(); +=== ${label.toUpperCase()} START === +${text} +=== ${label.toUpperCase()} END ===`; + const out = await tp.ai.chat(prompt); + return String(out) + .split(/\n+/) + .map(s=>s.replace(/^\s*[-*β€’]\s*/,"").trim()) + .filter(Boolean) + .slice(0, max); +} -// If the note is very long, cap input (you can raise this safely) -const MAX_INPUT_CHARS = 40000; -if (body.length > MAX_INPUT_CHARS) body = body.slice(0, MAX_INPUT_CHARS); +async function oneLineRecap(name, date, allBullets){ + const prompt = `Write a **single bold one-line recap** for a PF2e session called "${name}" on ${date}. +Base it ONLY on these bullets:\n${allBullets.map(b=>`- ${b}`).join("\n")}\nReturn exactly one bold line.`; + return String(await tp.ai.chat(prompt)).trim(); +} -/* ---------- Metadata ---------- */ +// ---------- run chunked summaries ---------- const cache = app.metadataCache.getFileCache(file) || {}; const fm = cache.frontmatter || {}; const seshName = fm.Session_Name || tp.file.title || file.basename; const seshDate = fm.Date || tp.date.now("YYYY-MM-DD"); -/* ---------- Length selector ---------- */ -const lenPick = await tp.system.suggester( - ["Short (~6 bullets)","Standard (~12 bullets)","Detailed (~20 bullets)"], - ["short","standard","detailed"] -) ?? "standard"; -const bulletTarget = (lenPick === "short") ? 6 : (lenPick === "detailed") ? 20 : 12; +const hi = await summarizeBullets("Highlights", S.highlights, 8); +const enc = await summarizeBullets("Encounters", S.encounters, 6); +const intel = await summarizeBullets("Intel", S.intel, 6); +const quests= await summarizeBullets("Quests", S.quests, 6); +const nexts = await summarizeBullets("Next Hooks", S.nextHooks, 6); +const npcfx = await summarizeBullets("NPCs & Factions", S.npcs, 1); // we’ll show names inline instead of bullets +const loot = await summarizeBullets("Treasure & Rewards", S.treasure, 3); -/* ---------- Build single-string prompt ---------- */ -const prompt = `You are a concise Pathfinder 2e GM assistant. Summarize the FULL session note below. +const allForRecap = [].concat(hi, enc, intel, quests, nexts).slice(0, 24); +let oneLine = await oneLineRecap(seshName, seshDate, allForRecap); +if (!oneLine.startsWith("**")) oneLine = `**${seshName} β€” ${seshDate}:** ${allForRecap[0] || "Session recap."}`; -Return ONLY: -**One-line recap (bold)** -- Key beats (bulleted, chronological, ${bulletTarget} bullets max) -- NPCs & factions touched (preserve existing [[wikilinks]] verbatim if present) -- Encounters/checks/outcomes (brief) -- Loot/rewards/XP (brief) -- Next hooks/to-dos (bullets) +// ---------- compose final ---------- +const out = []; +out.push(oneLine); +if (hi.length) out.push(...hi.map(b=>`- ${b}`)); +if (enc.length) out.push(...enc.map(b=>`- ${b}`)); +if (intel.length) out.push(...intel.map(b=>`- ${b}`)); +if (quests.length)out.push(...quests.map(b=>`- ${b}`)); +if (npcfx.length) out.push(`- NPCs/Factions touched: ${npcfx[0].replace(/^NPCs?:\s*/i,"")}`); +if (loot.length) out.push(...loot.map(b=>`- ${b}`)); +if (nexts.length) out.push(...nexts.map(b=>`- ${b}`)); -DO NOT echo raw headings, β€œHighlight::”, or placeholder text. Cover ALL major content, not just bullet lists. - -Session: "${seshName}" on ${seshDate} - -=== NOTE START === -${body} -=== NOTE END ===`; - -/* ---------- Call AI (string-only) ---------- */ -let summary = ""; -try { - const resp = await tp.ai.chat(prompt); - summary = (typeof resp === "string") ? resp : (resp?.content ?? resp?.text ?? ""); -} catch (e) { - console.error("AI error:", e); -} - -/* ---------- Fallback if model returns empty ---------- */ -if (!summary || !summary.trim()) { - // Build a tidy, non-noisy fallback from the whole note (no headings/labels) - const lines = body - .split("\n") - .map(s => s.trim()) - .filter(s => s && !/^#{1,6}\s/.test(s)) // no markdown headings - .filter(s => !/^\*\*[^*]+:\*\*$/.test(s)) // no bold-only section labels - .filter(s => !/^Highlight::/i.test(s)) // no "Highlight::" - .filter(s => !/^Round\s+\d+/i.test(s)) // no "Round X" - .filter(s => s !== "[[ ]]"); // no empty wikilinks - - const oneLine = `**${seshName} β€” ${seshDate}:** ${lines[0] || "Session recap unavailable."}`; - const bullets = lines.slice(1, 1 + Math.min(20, bulletTarget + 4)).map(s => `- ${s}`); - summary = [oneLine, ...bullets].join("\n"); -} - -/* ---------- Inject between SUMMARY markers ---------- */ -const START = ""; -const END = ""; +// ---------- inject between markers ---------- +const START="", END=""; let content = await app.vault.read(file); const sIdx = content.indexOf(START), eIdx = content.indexOf(END); -if (sIdx === -1 || eIdx === -1 || eIdx < sIdx) { new Notice("Could not find SUMMARY markers."); return; } - -const before = content.slice(0, sIdx + START.length); -const after = content.slice(eIdx); -await app.vault.modify(file, before + "\n" + summary.trim() + "\n" + after); -new Notice("AI session summary generated."); +if (sIdx===-1 || eIdx===-1 || eIdx diff --git a/Templates/NPC Template.md b/Templates/NPC Template.md index ec918d6..630c695 100644 --- a/Templates/NPC Template.md +++ b/Templates/NPC Template.md @@ -38,6 +38,104 @@ Tags: [npc] --- +## 🧠 Intel (auto) +```dataviewjs +const THIS = dv.current(); +const thisName = String(THIS.file.name); +const thisLC = thisName.toLowerCase(); + +// Scan faction & NPC notes that have Intel arrays +const pages = dv.pages("#npc or #faction").where(p => p.Intel); + +// Does an Intel entry reference THIS NPC? +function mentionsThisNPC(entry) { + const sum = String(entry.summary ?? ""); + + // 1) Explicit wikilink in summary + if (sum.includes(`[[${thisName}]]`) || sum.includes(`[[${thisName}|`)) return true; + + // 2) Source field (string/link/list) + const src = entry.source; + const arr = Array.isArray(src) ? src : (src ? [src] : []); + for (const s of arr) { + if (typeof s === "string") { + // quoted link or plain text + const unq = s.replace(/^"+|"+$/g, ""); // drop surrounding quotes if any + if (unq.includes(`[[${thisName}]]`) || unq.includes(`[[${thisName}|`)) return true; + // if they typed just the name + if (unq.replace(/\[\[|\]\]/g, "").trim().toLowerCase() === thisLC) return true; + } else if (s && typeof s === "object" && s.path) { + // wikilink object + const base = s.path.split("/").pop().replace(/\.md$/i, "").toLowerCase(); + if (base === thisLC) return true; + } + } + + // Optional (looser): plain text mention in summary + // return sum.toLowerCase().includes(thisLC); + + return false; +} + +// Collect rows: 1) own Intel, 2) mentions from other notes +const rows = []; + +// Own Intel (from this NPC note) +for (const it of (THIS.Intel ?? [])) { + rows.push({ + from: THIS.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + session: it.session ?? "", + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); +} + +// Mentions elsewhere +for (const p of pages.where(p => p.file.path !== THIS.file.path)) { + for (const it of (p.Intel ?? [])) { + if (mentionsThisNPC(it)) { + rows.push({ + from: p.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + session: it.session ?? "", + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); + } + } +} + +// Sort newest first +rows.sort((a,b) => String(b.date).localeCompare(String(a.date))); + +if (!rows.length) { + dv.paragraph("_No intel yet._"); +} else { + dv.paragraph(`_Entries: ${rows.length} (own ${(THIS.Intel ?? []).length} + mentions ${Math.max(0, rows.length - (THIS.Intel ?? []).length)})_`); + dv.table( + ["From Note","Date","Type","Summary","Session","Source","Rel","Cred","Tags"], + rows.map(r => [r.from, r.date, r.type, r.summary, r.session, r.source, r.rel, r.cred, r.tags]) + ); +} +``` + +```button +name βž• Add Intel +type command +action Templater: Insert Add Intel Entry to NPC +``` + +--- + ## πŸ›οΈ Relations & Factions - **Primary Faction:** [[Faction Name]] - **Allied Factions:** [[Faction A]], [[Faction B]] diff --git a/Templates/Session Template (PF2e).md b/Templates/Session Template (PF2e).md index ec33447..a9bfadb 100644 --- a/Templates/Session Template (PF2e).md +++ b/Templates/Session Template (PF2e).md @@ -84,7 +84,65 @@ templater true --- -## 🧠 Intel Captured +## 🧠 Intel Captured (auto) + +```dataviewjs +const THIS = dv.current(); +const sessName = THIS.file.name; +const sessDate = THIS.Date ?? dv.current().file.frontmatter?.Date ?? ""; +const sessLC = sessName.toLowerCase(); + +const pages = dv.pages("#npc or #faction").where(p => p.Intel); + +function matchesSessionField(sessionField) { + const list = Array.isArray(sessionField) ? sessionField : [sessionField]; + for (const s of list) { + if (!s) continue; + if (typeof s === "object" && s.path) { + const base = s.path.split("/").pop().replace(/\.md$/i,""); + if (base.toLowerCase() === sessLC) return true; + } else { + const text = String(s); + if (text.includes(`[[${sessName}]]`)) return true; + if (text.toLowerCase() === sessLC) return true; + } + } + return false; +} + +const rows = []; +for (const p of pages) { + for (const it of p.Intel) { + let ok = false; + if (it.session) ok = matchesSessionField(it.session); + if (!ok && sessDate && it.date) ok = String(it.date) === String(sessDate); // fallback + if (ok) { + rows.push({ + from: p.file.link, + date: it.date ?? "", + type: it.type ?? "", + summary: String(it.summary ?? ""), + source: it.source ?? "", + rel: it.reliability ?? "", + cred: it.credibility ?? "", + tags: Array.isArray(it.tags) ? it.tags.join(", ") : (it.tags ?? "") + }); + } + } +} + +rows.sort((a,b)=> String(b.date).localeCompare(String(a.date))); + +if (!rows.length) { + dv.paragraph("_No intel captured for this session yet._"); +} else { + dv.table( + ["From", "Date", "Type", "Summary", "Source", "Rel", "Cred", "Tags"], + rows.map(r => [r.from, r.date, r.type, r.summary, r.source, r.rel, r.cred, r.tags]) + ); +} +``` + ---