{"id":225,"date":"2026-01-29T11:55:40","date_gmt":"2026-01-29T11:55:40","guid":{"rendered":"https:\/\/stygianrealms.com\/?page_id=225"},"modified":"2026-01-29T23:07:22","modified_gmt":"2026-01-29T23:07:22","slug":"generators","status":"publish","type":"page","link":"https:\/\/stygianrealms.com\/?page_id=225","title":{"rendered":"DM_Tools"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"225\" class=\"elementor elementor-225\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-9fe9e34 e-flex e-con-boxed e-con e-parent\" data-id=\"9fe9e34\" data-element_type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-11904d0 elementor-widget elementor-widget-html\" data-id=\"11904d0\" data-element_type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t<!-- Stygian Realms \u2014 NPC Generator (JSON-loaded, lockable traits, rarity + brag rating) -->\r\n<div id=\"sr-npc-tool\" class=\"sr-npc\">\r\n  <div class=\"sr-npc__wrap\">\r\n    <header class=\"sr-npc__header\">\r\n      <h2 class=\"sr-npc__title\">NPC Generator<\/h2>\r\n      <p class=\"sr-npc__subtitle\">Forge memorable characters for your adventures<\/p>\r\n    <\/header>\r\n\r\n    <div class=\"sr-npc__controls\">\r\n      <button class=\"sr-npc__btn sr-npc__btn--primary\" data-action=\"generate-all\" type=\"button\">Generate NPC<\/button>\r\n      <button class=\"sr-npc__btn\" data-action=\"regen-unlocked\" type=\"button\" disabled>Re-roll Unlocked<\/button>\r\n      <button class=\"sr-npc__btn\" data-action=\"unlock-all\" type=\"button\" disabled>Unlock All<\/button>\r\n    <\/div>\r\n\r\n    <div class=\"sr-npc__status\" aria-live=\"polite\">\r\n      <span class=\"sr-npc__status-dot\"><\/span>\r\n      <span class=\"sr-npc__status-text\">Loading data\u2026<\/span>\r\n    <\/div>\r\n\r\n    <div class=\"sr-npc__card\" hidden>\r\n      <div class=\"sr-npc__card-top\">\r\n        <div class=\"sr-npc__top-left\">\r\n          <div class=\"sr-npc__name\" data-field=\"name\">\u2014<\/div>\r\n          <div class=\"sr-npc__coreline\" data-field=\"core\">\u2014<\/div>\r\n        <\/div>\r\n\r\n        <div class=\"sr-npc__brag\">\r\n          <div class=\"sr-npc__brag-label\">Brag Rating<\/div>\r\n          <div class=\"sr-npc__brag-value\" data-field=\"brag\">\u2014<\/div>\r\n          <div class=\"sr-npc__brag-sub\" data-field=\"bragSub\">\u2014<\/div>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <div class=\"sr-npc__sections\">\r\n        <section class=\"sr-npc__section\">\r\n          <div class=\"sr-npc__section-head\">\r\n            <h3>Core<\/h3>\r\n            <button class=\"sr-npc__mini\" data-action=\"regen-section\" data-section=\"core\" type=\"button\">Re-roll<\/button>\r\n          <\/div>\r\n          <div class=\"sr-npc__rows\" data-section=\"core\"><\/div>\r\n        <\/section>\r\n\r\n        <section class=\"sr-npc__section\">\r\n          <div class=\"sr-npc__section-head\">\r\n            <h3>Physical<\/h3>\r\n            <button class=\"sr-npc__mini\" data-action=\"regen-section\" data-section=\"physical\" type=\"button\">Re-roll<\/button>\r\n          <\/div>\r\n          <div class=\"sr-npc__rows\" data-section=\"physical\"><\/div>\r\n        <\/section>\r\n\r\n        <section class=\"sr-npc__section\">\r\n          <div class=\"sr-npc__section-head\">\r\n            <h3>Personality<\/h3>\r\n            <button class=\"sr-npc__mini\" data-action=\"regen-section\" data-section=\"personality\" type=\"button\">Re-roll<\/button>\r\n          <\/div>\r\n          <div class=\"sr-npc__rows\" data-section=\"personality\"><\/div>\r\n        <\/section>\r\n      <\/div>\r\n\r\n      <div class=\"sr-npc__actions\">\r\n        <button class=\"sr-npc__btn\" data-action=\"copy\" type=\"button\">Copy<\/button>\r\n\r\n        <div class=\"sr-npc__email\">\r\n          <input class=\"sr-npc__input\" type=\"email\" placeholder=\"your@email.com\" autocomplete=\"email\" inputmode=\"email\" data-field=\"email\" \/>\r\n          <button class=\"sr-npc__btn\" data-action=\"email\" type=\"button\">Email to me<\/button>\r\n        <\/div>\r\n\r\n        <div class=\"sr-npc__disclaimer\">\r\n          We don\u2019t store or save your email address. \u201cEmail to me\u201d opens your device\u2019s email app.\r\n        <\/div>\r\n      <\/div>\r\n    <\/div>\r\n  <\/div>\r\n<\/div>\r\n\r\n<link rel=\"stylesheet\" href=\"https:\/\/stygianrealms.com\/wp-content\/uploads\/stygian-tools\/shared\/sr-tools.v1.css\">\r\n\r\n\r\n<script>\r\n(() => {\r\n  const ROOT = document.getElementById(\"sr-npc-tool\");\r\n  if (!ROOT) return;\r\n\r\n  const BASE = \"https:\/\/stygianrealms.com\/wp-content\/uploads\/stygian-tools\/npc\/\";\r\n  const URLS = {\r\n    traitsAll:   BASE + \"npc.traits.all.json\",\r\n    namesMale:   BASE + \"npc.names.male.json\",\r\n    namesFemale: BASE + \"npc.names.female.json\",\r\n    namesNeutral:BASE + \"npc.names.neutral.json\",\r\n    namesLast:   BASE + \"npc.names.last.json\",\r\n    config:      BASE + \"npc.config.json\"\r\n  };\r\n\r\n  const els = {\r\n    statusDot:  ROOT.querySelector(\".sr-npc__status-dot\"),\r\n    statusText: ROOT.querySelector(\".sr-npc__status-text\"),\r\n    card:       ROOT.querySelector(\".sr-npc__card\"),\r\n    name:       ROOT.querySelector('[data-field=\"name\"]'),\r\n    core:       ROOT.querySelector('[data-field=\"core\"]'),\r\n    brag:       ROOT.querySelector('[data-field=\"brag\"]'),\r\n    bragSub:    ROOT.querySelector('[data-field=\"bragSub\"]'),\r\n    email:      ROOT.querySelector('[data-field=\"email\"]'),\r\n    rowsCore:   ROOT.querySelector('[data-section=\"core\"]'),\r\n    rowsPhysical: ROOT.querySelector('[data-section=\"physical\"]'),\r\n    rowsPersonality: ROOT.querySelector('[data-section=\"personality\"]'),\r\n    btnAll:     ROOT.querySelector('[data-action=\"generate-all\"]'),\r\n    btnUnlocked:ROOT.querySelector('[data-action=\"regen-unlocked\"]'),\r\n    btnUnlockAll:ROOT.querySelector('[data-action=\"unlock-all\"]')\r\n  };\r\n\r\n  const state = {\r\n    ready:false,\r\n    traits:null,\r\n    names:{ male:[], female:[], neutral:[], last:[] },\r\n    locks:new Set(),             \/\/ \"section.id\"\r\n    current:{},                  \/\/ \"section.id\" => { value, pct }\r\n    namePct:null,                \/\/ percent (0-100)\r\n    config:{\r\n      bragTiers:[\r\n        {min:1.47,label:\"Mythic\"},\r\n        {min:1.40,label:\"Epic\"},\r\n        {min:1.32,label:\"Rare\"},\r\n        {min:1.25,label:\"Uncommon\"},\r\n        {min:0.00,label:\"Common\"}\r\n      ],\r\n      historyLimit:20\r\n    }\r\n  };\r\n\r\n  const k = (section,id) => `${section}.${id}`;\r\n  const clamp = (n,min,max) => Math.max(min, Math.min(max,n));\r\n\r\n  function setStatus(kind, msg){\r\n    els.statusText.textContent = msg;\r\n    els.statusDot.style.background =\r\n      kind === \"ok\" ? \"var(--good)\" :\r\n      kind === \"err\" ? \"var(--bad)\" : \"var(--muted2)\";\r\n  }\r\n\r\n  async function fetchJSON(url){\r\n    const res = await fetch(url, { cache:\"force-cache\" });\r\n    if (!res.ok) throw new Error(`Fetch failed (${res.status}) for ${url}`);\r\n    return res.json();\r\n  }\r\n\r\n  function sumWeights(arr){ return arr.reduce((s,x)=> s + (Number(x.weight)||0), 0); }\r\n\r\n  function pickWeighted(arr){\r\n    const total = sumWeights(arr);\r\n    let r = Math.random() * total;\r\n    for (const item of arr){\r\n      r -= (Number(item.weight)||0);\r\n      if (r <= 0) return item;\r\n    }\r\n    return arr[arr.length-1];\r\n  }\r\n\r\n  function pctFromWeightedPick(item, arr){\r\n    const total = sumWeights(arr);\r\n    const w = Number(item.weight)||0;\r\n    if (!total || !w) return 0;\r\n    return (w \/ total) * 100;\r\n  }\r\n\r\n  function pickUniform(arr){\r\n    const idx = Math.floor(Math.random() * arr.length);\r\n    return { value: arr[idx], pct: (1 \/ arr.length) * 100 };\r\n  }\r\n\r\n  function normalizeTraitsAll(raw){\r\n    return (raw && raw.traits) ? raw.traits : raw;\r\n  }\r\n\r\n  function updateButtons(){\r\n    const hasCard = !els.card.hidden;\r\n    els.btnUnlocked.disabled = !hasCard;\r\n    els.btnUnlockAll.disabled = !hasCard || state.locks.size === 0;\r\n  }\r\n\r\n  function setLock(section,id,locked){\r\n    const key = k(section,id);\r\n    locked ? state.locks.add(key) : state.locks.delete(key);\r\n    updateButtons();\r\n    updateBrag();\r\n  }\r\n\r\n  function chooseTrait(section,id,arrKey){\r\n    const arr = state.traits[arrKey];\r\n    const item = pickWeighted(arr);\r\n    const pct = pctFromWeightedPick(item, arr);\r\n    state.current[k(section,id)] = { value:item.trait, pct };\r\n  }\r\n\r\n  function generateName(gender){\r\n    const last = state.names.last.length ? pickUniform(state.names.last) : { value:\"\u2014\", pct:0 };\r\n    let pool = state.names.neutral;\r\n    if (gender === \"Male\") pool = state.names.male;\r\n    if (gender === \"Female\") pool = state.names.female;\r\n    if (!pool.length) pool = state.names.neutral;\r\n\r\n    const first = pool.length ? pickUniform(pool) : { value:\"\u2014\", pct:0 };\r\n    const p = (first.pct\/100) * (last.pct\/100);     \/\/ probability (0..1)\r\n    return { value:`${first.value} ${last.value}`.trim(), pct:p*100 };\r\n  }\r\n\r\n  function tierFromAvgScore(avgScore){\r\n    const tiers = (state.config && Array.isArray(state.config.bragTiers)) ? state.config.bragTiers : [];\r\n    for (const t of tiers){\r\n      if (avgScore >= Number(t.min)) return t.label;\r\n    }\r\n    return \"Common\";\r\n  }\r\n\r\n  function updateBrag(){\r\n    const vals = Object.values(state.current);\r\n    const scores = [];\r\n\r\n    for (const v of vals){\r\n      const p = clamp((v.pct||0)\/100, 1e-12, 1);\r\n      scores.push(-Math.log10(p));\r\n    }\r\n    if (state.namePct != null){\r\n      const pName = clamp(state.namePct\/100, 1e-12, 1);\r\n      scores.push(-Math.log10(pName));\r\n    }\r\n\r\n    const avgScore = scores.length ? (scores.reduce((a,b)=>a+b,0) \/ scores.length) : 0;\r\n    const tier = tierFromAvgScore(avgScore);\r\n\r\n    els.brag.textContent = tier;\r\n\r\n    const avgProb = Math.pow(10, -avgScore);\r\n    const oneIn = Math.round(1 \/ Math.max(avgProb, 1e-12));\r\n    els.bragSub.textContent = `~ 1 in ${oneIn.toLocaleString()} per trait (score ${avgScore.toFixed(2)})`;\r\n  }\r\n\r\n  function buildCoreLine(){\r\n    const gender = state.current[k(\"core\",\"gender\")]?.value ?? \"\u2014\";\r\n    const alignment = state.current[k(\"core\",\"alignment\")]?.value ?? \"\u2014\";\r\n    const race = state.current[k(\"core\",\"race\")]?.value ?? \"\u2014\";\r\n    els.core.textContent = `${gender} ${alignment} ${race}`.replace(\/\\s+\/g,\" \").trim();\r\n  }\r\n\r\n  function renderRows(section, container, schema){\r\n    container.innerHTML = \"\";\r\n\r\n    for (const rowDef of schema){\r\n      const id = rowDef.id;\r\n      const label = rowDef.label;\r\n      const key = k(section,id);\r\n      const cur = state.current[key];\r\n\r\n      const row = document.createElement(\"div\");\r\n      row.className = \"sr-npc__row\";\r\n\r\n      const labelEl = document.createElement(\"div\");\r\n      labelEl.className = \"sr-npc__label\";\r\n      labelEl.textContent = label;\r\n\r\n      const valueEl = document.createElement(\"div\");\r\n      valueEl.className = \"sr-npc__value\";\r\n\r\n      const rarityEl = document.createElement(\"div\");\r\n      rarityEl.className = \"sr-npc__rarity\";\r\n\r\n      const lockBtn = document.createElement(\"button\");\r\n      lockBtn.className = \"sr-npc__lock\";\r\n      lockBtn.type = \"button\";\r\n\r\n      \/\/ Fill\r\n      valueEl.textContent = cur?.value ?? \"\u2014\";\r\n      rarityEl.textContent = cur ? `${cur.pct.toFixed(2)}%` : \"\u2014\";\r\n\r\n      \/\/ Lock state\r\n      const locked = state.locks.has(key);\r\n      lockBtn.classList.toggle(\"is-locked\", locked);\r\n      lockBtn.textContent = locked ? \"\ud83d\udd12\" : \"\ud83d\udd13\";\r\n      lockBtn.title = locked ? \"Locked\" : \"Unlocked\";\r\n\r\n      \/\/ IMPORTANT: lock should NEVER trigger any re-roll\r\n      lockBtn.addEventListener(\"click\", (ev) => {\r\n        ev.preventDefault();\r\n        ev.stopPropagation();\r\n        const nowLocked = !state.locks.has(key);\r\n        setLock(section,id,nowLocked);\r\n        lockBtn.classList.toggle(\"is-locked\", nowLocked);\r\n        lockBtn.textContent = nowLocked ? \"\ud83d\udd12\" : \"\ud83d\udd13\";\r\n        lockBtn.title = nowLocked ? \"Locked\" : \"Unlocked\";\r\n      });\r\n\r\n      row.append(labelEl, valueEl, rarityEl, lockBtn);\r\n      container.appendChild(row);\r\n    }\r\n  }\r\n\r\n  function renderAll(){\r\n    buildCoreLine();\r\n\r\n    \/\/ Core rows (name uses state.namePct + els.name text)\r\n    renderRows(\"core\", els.rowsCore, [\r\n      {id:\"name\", label:\"Name\"},\r\n      {id:\"gender\", label:\"Gender\"},\r\n      {id:\"alignment\", label:\"Alignment\"},\r\n      {id:\"race\", label:\"Race\"}\r\n    ]);\r\n\r\n    \/\/ Patch the name row display (value + rarity)\r\n    const nameRow = els.rowsCore.firstElementChild;\r\n    if (nameRow){\r\n      nameRow.querySelector(\".sr-npc__value\").textContent = els.name.textContent;\r\n      nameRow.querySelector(\".sr-npc__rarity\").textContent = (state.namePct != null) ? `${state.namePct.toFixed(2)}%` : \"\u2014\";\r\n    }\r\n\r\n    \/\/ Physical rows\r\n    renderRows(\"physical\", els.rowsPhysical, [\r\n      {id:\"eyes\", label:\"Eyes\"},\r\n      {id:\"hair\", label:\"Hair\"},\r\n      {id:\"build\", label:\"Build\"},\r\n      {id:\"skin\", label:\"Skin\"},\r\n      {id:\"face\", label:\"Face\"}\r\n    ]);\r\n\r\n    \/\/ Personality rows\r\n    renderRows(\"personality\", els.rowsPersonality, [\r\n      {id:\"type\", label:\"Type\"},\r\n      {id:\"mood\", label:\"Mood\"},\r\n      {id:\"quirk\", label:\"Quirk\"},\r\n      {id:\"flaw\", label:\"Flaw\"},\r\n      {id:\"motivation\", label:\"Motivation\"},\r\n      {id:\"inspiration\", label:\"Inspiration\"},\r\n      {id:\"talent\", label:\"Talent\"},\r\n      {id:\"secret\", label:\"Secret\"},\r\n      {id:\"reputation\", label:\"Reputation\"}\r\n    ]);\r\n\r\n    updateBrag();\r\n    updateButtons();\r\n    els.card.hidden = false;\r\n    setStatus(\"ok\",\"Generated.\");\r\n  }\r\n\r\n  function generate({ force=false, section=null } = {}){\r\n    if (!state.ready) return;\r\n\r\n    \/\/ Core\r\n    if (section == null || section === \"core\"){\r\n      const map = { gender:\"genders\", alignment:\"alignments\", race:\"races\" };\r\n      for (const id of [\"gender\",\"alignment\",\"race\"]){\r\n        if (!force && state.locks.has(k(\"core\",id))) continue;\r\n        chooseTrait(\"core\", id, map[id]);\r\n      }\r\n\r\n      const gender = state.current[k(\"core\",\"gender\")]?.value ?? \"Non-Binary\";\r\n      if (force || !state.locks.has(k(\"core\",\"name\"))){\r\n        const nm = generateName(gender);\r\n        els.name.textContent = nm.value;\r\n        state.namePct = nm.pct;\r\n      }\r\n    }\r\n\r\n    \/\/ Physical\r\n    if (section == null || section === \"physical\"){\r\n      \/\/ Eyes combo\r\n      if (force || !state.locks.has(k(\"physical\",\"eyes\"))){\r\n        const sh = state.traits.eye_shapes, co = state.traits.eye_colors;\r\n        const a = pickWeighted(sh), b = pickWeighted(co);\r\n        const pa = pctFromWeightedPick(a, sh)\/100, pb = pctFromWeightedPick(b, co)\/100;\r\n        state.current[k(\"physical\",\"eyes\")] = { value:`${a.trait}, ${b.trait}`, pct:(pa*pb)*100 };\r\n      }\r\n\r\n      \/\/ Hair combo\r\n      if (force || !state.locks.has(k(\"physical\",\"hair\"))){\r\n        const hs = state.traits.hair_styles, ht = state.traits.hair_textures, hc = state.traits.hair_colors, hcl = state.traits.hair_cleanliness;\r\n        const s = pickWeighted(hs), t = pickWeighted(ht), c = pickWeighted(hc), cl = pickWeighted(hcl);\r\n        const ps = pctFromWeightedPick(s, hs)\/100, pt = pctFromWeightedPick(t, ht)\/100, pc = pctFromWeightedPick(c, hc)\/100, pcl = pctFromWeightedPick(cl, hcl)\/100;\r\n        state.current[k(\"physical\",\"hair\")] = { value:`${s.trait}, ${t.trait}, ${c.trait}, ${cl.trait}`, pct:(ps*pt*pc*pcl)*100 };\r\n      }\r\n\r\n      for (const id of [\"build\",\"skin\"]){\r\n        if (!force && state.locks.has(k(\"physical\",id))) continue;\r\n        chooseTrait(\"physical\", id, id === \"build\" ? \"build_types\" : \"skin_colors\");\r\n      }\r\n\r\n      \/\/ Face combo\r\n      if (force || !state.locks.has(k(\"physical\",\"face\"))){\r\n        const ft = state.traits.face_types, ff = state.traits.face_features;\r\n        const a = pickWeighted(ft), b = pickWeighted(ff);\r\n        const pa = pctFromWeightedPick(a, ft)\/100, pb = pctFromWeightedPick(b, ff)\/100;\r\n        state.current[k(\"physical\",\"face\")] = { value:`${a.trait}, ${b.trait}`, pct:(pa*pb)*100 };\r\n      }\r\n    }\r\n\r\n    \/\/ Personality\r\n    if (section == null || section === \"personality\"){\r\n      const per = [\r\n        [\"type\",\"personality_types\"],\r\n        [\"mood\",\"personality_moods\"],\r\n        [\"quirk\",\"personality_quirks\"],\r\n        [\"flaw\",\"personality_flaws\"],\r\n        [\"motivation\",\"personality_motivations\"],\r\n        [\"inspiration\",\"personality_inspirations\"],\r\n        [\"talent\",\"personality_talents\"],\r\n        [\"secret\",\"personality_secrets\"],\r\n        [\"reputation\",\"personality_reputations\"]\r\n      ];\r\n      for (const [id, arrKey] of per){\r\n        if (!force && state.locks.has(k(\"personality\",id))) continue;\r\n        chooseTrait(\"personality\", id, arrKey);\r\n      }\r\n    }\r\n\r\n    renderAll();\r\n  }\r\n\r\n  function getNPCText(){\r\n    const lines = [];\r\n    lines.push(els.name.textContent);\r\n    lines.push(els.core.textContent);\r\n    lines.push(\"\");\r\n    lines.push(`Brag Rating: ${els.brag.textContent} (${els.bragSub.textContent})`);\r\n    lines.push(\"\");\r\n\r\n    const dump = (title, pairs, section) => {\r\n      lines.push(title + \":\");\r\n      for (const [id, pretty] of pairs){\r\n        const v = state.current[k(section,id)];\r\n        if (id === \"name\"){\r\n          lines.push(`- ${pretty}: ${els.name.textContent} (${(state.namePct ?? 0).toFixed(2)}%)`);\r\n        } else if (v){\r\n          lines.push(`- ${pretty}: ${v.value} (${v.pct.toFixed(2)}%)`);\r\n        }\r\n      }\r\n      lines.push(\"\");\r\n    };\r\n\r\n    dump(\"Core\", [[\"name\",\"Name\"],[\"gender\",\"Gender\"],[\"alignment\",\"Alignment\"],[\"race\",\"Race\"]], \"core\");\r\n    dump(\"Physical\", [[\"eyes\",\"Eyes\"],[\"hair\",\"Hair\"],[\"build\",\"Build\"],[\"skin\",\"Skin\"],[\"face\",\"Face\"]], \"physical\");\r\n    dump(\"Personality\", [[\"type\",\"Type\"],[\"mood\",\"Mood\"],[\"quirk\",\"Quirk\"],[\"flaw\",\"Flaw\"],[\"motivation\",\"Motivation\"],[\"inspiration\",\"Inspiration\"],[\"talent\",\"Talent\"],[\"secret\",\"Secret\"],[\"reputation\",\"Reputation\"]], \"personality\");\r\n    return lines.join(\"\\n\");\r\n  }\r\n\r\n  async function copyToClipboard(){\r\n    const text = getNPCText();\r\n    try{\r\n      await navigator.clipboard.writeText(text);\r\n      setStatus(\"ok\",\"Copied to clipboard.\");\r\n    }catch(_){\r\n      const ta = document.createElement(\"textarea\");\r\n      ta.value = text;\r\n      ta.style.position = \"fixed\";\r\n      ta.style.opacity = \"0\";\r\n      document.body.appendChild(ta);\r\n      ta.select();\r\n      document.execCommand(\"copy\");\r\n      document.body.removeChild(ta);\r\n      setStatus(\"ok\",\"Copied to clipboard.\");\r\n    }\r\n  }\r\n\r\n  function emailToMe(){\r\n    const to = (els.email.value || \"\").trim();\r\n    if (!to){ setStatus(\"err\",\"Enter an email address first.\"); return; }\r\n    const subject = \"My Generated NPC (Stygian Realms)\";\r\n    const body = getNPCText();\r\n    window.location.href = `mailto:${encodeURIComponent(to)}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;\r\n    setStatus(\"ok\",\"Opening your email app\u2026\");\r\n  }\r\n\r\n  function unlockAll(){\r\n    state.locks.clear();\r\n    updateButtons();\r\n    if (!els.card.hidden){\r\n      \/\/ Just re-render so icons flip without changing results\r\n      renderAll();\r\n      setStatus(\"ok\",\"All unlocked.\");\r\n    }\r\n  }\r\n\r\n  \/\/ Controls (delegated)\r\n  ROOT.addEventListener(\"click\", (e) => {\r\n    const btn = e.target.closest(\"[data-action]\");\r\n    if (!btn) return;\r\n    const action = btn.getAttribute(\"data-action\");\r\n\r\n    if (action === \"generate-all\") return generate({ force:true, section:null });\r\n    if (action === \"regen-unlocked\") return generate({ force:false, section:null });\r\n    if (action === \"unlock-all\") return unlockAll();\r\n    if (action === \"regen-section\") return generate({ force:false, section: btn.getAttribute(\"data-section\") });\r\n    if (action === \"copy\") return copyToClipboard();\r\n    if (action === \"email\") return emailToMe();\r\n  });\r\n\r\n  async function init(){\r\n    try{\r\n      \/\/ Try config, but do not fail if missing\r\n      try{\r\n        const cfg = await fetchJSON(URLS.config);\r\n        if (cfg && typeof cfg === \"object\") state.config = { ...state.config, ...cfg };\r\n      }catch(_){}\r\n\r\n      const [traitsRaw, male, female, neutral, last] = await Promise.all([\r\n        fetchJSON(URLS.traitsAll),\r\n        fetchJSON(URLS.namesMale).catch(()=>[]),\r\n        fetchJSON(URLS.namesFemale).catch(()=>[]),\r\n        fetchJSON(URLS.namesNeutral).catch(()=>[]),\r\n        fetchJSON(URLS.namesLast).catch(()=>[])\r\n      ]);\r\n\r\n      state.traits = normalizeTraitsAll(traitsRaw);\r\n      state.names = { male, female, neutral, last };\r\n\r\n      const required = [\r\n        \"races\",\"genders\",\"alignments\",\r\n        \"eye_shapes\",\"eye_colors\",\r\n        \"hair_styles\",\"hair_textures\",\"hair_cleanliness\",\"hair_colors\",\r\n        \"build_types\",\"skin_colors\",\r\n        \"face_types\",\"face_features\",\r\n        \"personality_types\",\"personality_moods\",\"personality_quirks\",\"personality_flaws\",\r\n        \"personality_motivations\",\"personality_inspirations\",\"personality_talents\",\"personality_secrets\",\"personality_reputations\"\r\n      ];\r\n      const missing = required.filter(x => !Array.isArray(state.traits[x]));\r\n      if (missing.length) throw new Error(\"Missing arrays in npc.traits.all.json: \" + missing.join(\", \"));\r\n\r\n      state.ready = true;\r\n      setStatus(\"ok\",\"Ready.\");\r\n      updateButtons();\r\n    }catch(err){\r\n      console.error(err);\r\n      setStatus(\"err\",\"Data load failed. Check JSON paths + file contents.\");\r\n    }\r\n  }\r\n\r\n  init();\r\n})();\r\n<\/script>\r\n\r\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-407933b e-flex e-con-boxed e-con e-parent\" data-id=\"407933b\" data-element_type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>NPC Generator Forge memorable characters for your adventures Generate NPC Re-roll Unlocked Unlock All Loading data\u2026 \u2014 \u2014 Brag Rating \u2014 \u2014 Core Re-roll Physical Re-roll Personality Re-roll Copy Email to me We don\u2019t store or save your email address. \u201cEmail to me\u201d opens your device\u2019s email app.<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-225","page","type-page","status-publish","hentry"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/stygianrealms.com\/index.php?rest_route=\/wp\/v2\/pages\/225","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/stygianrealms.com\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/stygianrealms.com\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/stygianrealms.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/stygianrealms.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=225"}],"version-history":[{"count":0,"href":"https:\/\/stygianrealms.com\/index.php?rest_route=\/wp\/v2\/pages\/225\/revisions"}],"wp:attachment":[{"href":"https:\/\/stygianrealms.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=225"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}