{"id":26181,"date":"2025-10-20T17:53:12","date_gmt":"2025-10-20T08:53:12","guid":{"rendered":"http:\/\/www.tyosuke20xx.com\/blog\/?p=26181"},"modified":"2025-10-20T17:53:14","modified_gmt":"2025-10-20T08:53:14","slug":"questfoundry","status":"publish","type":"post","link":"http:\/\/www.tyosuke20xx.com\/blog\/?p=26181","title":{"rendered":"QuestFoundry"},"content":{"rendered":"\n<pre class=\"wp-block-code\"><code>&lt;!DOCTYPE html>\n&lt;html lang=\"ja\">\n&lt;head>\n  &lt;meta charset=\"utf-8\" \/>\n  &lt;meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" \/>\n  &lt;title>Quest Foundry | \u4e16\u754c\u89b3\u304b\u3089\u30af\u30a8\u30b9\u30c8\u81ea\u52d5\u8a2d\u8a08&lt;\/title>\n  &lt;meta name=\"description\" content=\"\u4e16\u754c\u89b3\u306e\u30ad\u30fc\u30ef\u30fc\u30c9\u304b\u3089NPC\u30fb\u30a2\u30a4\u30c6\u30e0\u30fb\u5834\u6240\u30fb\u30af\u30a8\u30b9\u30c8\u3092\u4e00\u62ec\u751f\u6210\u3002JSON\/CSV\u30a8\u30af\u30b9\u30dd\u30fc\u30c8\u3001\u4f9d\u5b58\u95a2\u4fc2\u3001\u96e3\u6613\u5ea6\u30d0\u30e9\u30f3\u30b9\u3001\u30b7\u30fc\u30c9\u56fa\u5b9a\u5bfe\u5fdc\u3002\" \/>\n  &lt;!-- Tailwind CDN (Node\u4e0d\u8981) -->\n  &lt;script src=\"https:\/\/cdn.tailwindcss.com\">&lt;\/script>\n  &lt;script>\n    tailwind.config = {\n      theme: {\n        extend: {\n          fontFamily: { sans: &#91;\"Noto Sans JP\", \"ui-sans-serif\", \"system-ui\"] },\n          colors: { brand: { 50: '#eef2ff', 100:'#e0e7ff', 200:'#c7d2fe', 300:'#a5b4fc', 400:'#818cf8', 500:'#6366f1', 600:'#4f46e5', 700:'#4338ca', 800:'#3730a3', 900:'#312e81'} }\n        }\n      }\n    };\n  &lt;\/script>\n  &lt;link rel=\"preconnect\" href=\"https:\/\/fonts.googleapis.com\">\n  &lt;link rel=\"preconnect\" href=\"https:\/\/fonts.gstatic.com\" crossorigin>\n  &lt;link href=\"https:\/\/fonts.googleapis.com\/css2?family=Noto+Sans+JP:wght@400;700;900&amp;display=swap\" rel=\"stylesheet\">\n  &lt;style>\n    html, body { height: 100%; }\n    .glass { backdrop-filter: blur(10px); background: rgba(255,255,255,0.7); }\n    .prose pre { white-space: pre-wrap; word-break: break-word; }\n    .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace; }\n    .card { @apply rounded-2xl shadow-lg p-5 bg-white; }\n      .prose h1{font-size:1.5rem;line-height:1.3;margin:0 0 .6rem;font-weight:800}\n    .prose h2{font-size:1.2rem;line-height:1.35;margin:1.2rem 0 .4rem;font-weight:700;border-left:4px solid #6366f1;padding-left:.6rem}\n    .prose h3{font-size:1rem;line-height:1.4;margin:1rem 0 .3rem;font-weight:700}\n    .prose ul{list-style:disc;padding-left:1.25rem;margin:.4rem 0 .8rem}\n    .prose li{margin:.2rem 0}\n    .badge{display:inline-block;font-size:.72rem;line-height:1;background:#eef2ff;color:#3730a3;border:1px solid #c7d2fe;border-radius:.5rem;padding:.15rem .45rem;margin-right:.25rem}\n    details.quest{border:1px solid #e5e7eb;border-radius:.75rem;padding:.6rem .8rem;margin:.5rem 0;background:#fff}\n    details.quest > summary{cursor:pointer;list-style:none}\n    details.quest > summary::-webkit-details-marker{display:none}\n    .kv{display:inline-grid;grid-template-columns:auto auto;gap:.2rem .6rem;align-items:center}\n  &lt;\/style>\n&lt;\/head>\n&lt;body class=\"min-h-screen bg-gradient-to-br from-brand-50 to-white text-slate-800\">\n  &lt;header class=\"sticky top-0 z-40 border-b bg-white\/80 backdrop-blur\">\n    &lt;div class=\"mx-auto max-w-7xl px-4 py-3 flex items-center gap-4\">\n      &lt;div class=\"text-2xl font-black tracking-tight\">&lt;span class=\"text-brand-700\">Quest&lt;\/span> Foundry&lt;\/div>\n      &lt;div class=\"text-xs text-slate-500\">\u4e16\u754c\u89b3\u2192NPC\/\u30a2\u30a4\u30c6\u30e0\/\u5834\u6240\/\u30af\u30a8\u30b9\u30c8\u3092\u81ea\u52d5\u751f\u6210\uff08JSON\/CSV\u51fa\u529b\u53ef\uff09&lt;\/div>\n      &lt;div class=\"ml-auto flex items-center gap-2\">\n        &lt;button id=\"btnSave\" class=\"px-3 py-2 text-sm rounded-lg border hover:bg-slate-50\">\u4fdd\u5b58&lt;\/button>\n        &lt;button id=\"btnLoad\" class=\"px-3 py-2 text-sm rounded-lg border hover:bg-slate-50\">\u8aad\u8fbc&lt;\/button>\n        &lt;button id=\"btnPrint\" class=\"px-3 py-2 text-sm rounded-lg border hover:bg-slate-50\">\u5370\u5237\/PDF&lt;\/button>\n      &lt;\/div>\n    &lt;\/div>\n  &lt;\/header>\n\n  &lt;main class=\"mx-auto max-w-7xl px-4 py-6 grid grid-cols-1 lg:grid-cols-3 gap-6\">\n    &lt;!-- \u5de6\uff1a\u8a2d\u5b9a\u30d5\u30a9\u30fc\u30e0 -->\n    &lt;section class=\"lg:col-span-1 card\">\n      &lt;h2 class=\"text-lg font-bold mb-4\">\u30ef\u30fc\u30eb\u30c9\u8a2d\u5b9a&lt;\/h2>\n      &lt;form id=\"worldForm\" class=\"space-y-4\">\n        &lt;div>\n          &lt;label class=\"block text-sm font-medium\">\u4e16\u754c\u540d&lt;\/label>\n          &lt;input id=\"worldName\" type=\"text\" class=\"w-full mt-1 rounded-lg border px-3 py-2\" placeholder=\"\u4f8b\uff1a\u30a2\u30c8\u30e9\u30c6\u30a3\u30a2\" \/>\n        &lt;\/div>\n        &lt;div>\n          &lt;label class=\"block text-sm font-medium\">\u30c6\u30fc\u30de\u30fb\u30ad\u30fc\u30ef\u30fc\u30c9\uff08\u8aad\u70b9\u30fb\u30b9\u30da\u30fc\u30b9\u533a\u5207\u308a\uff09&lt;\/label>\n          &lt;input id=\"themes\" type=\"text\" class=\"w-full mt-1 rounded-lg border px-3 py-2\" placeholder=\"\u4f8b\uff1a\u53e4\u4ee3\u907a\u8de1 \u7802\u6f20 \u7cbe\u970a \u5192\u967a\u8005\u30ae\u30eb\u30c9\" \/>\n        &lt;\/div>\n        &lt;div class=\"grid grid-cols-2 gap-4\">\n          &lt;div>\n            &lt;label class=\"block text-sm font-medium\">\u96e3\u6613\u5ea6&lt;\/label>\n            &lt;select id=\"difficulty\" class=\"w-full mt-1 rounded-lg border px-3 py-2\">\n              &lt;option value=\"easy\">Easy&lt;\/option>\n              &lt;option value=\"normal\" selected>Normal&lt;\/option>\n              &lt;option value=\"hard\">Hard&lt;\/option>\n              &lt;option value=\"epic\">Epic&lt;\/option>\n            &lt;\/select>\n          &lt;\/div>\n          &lt;div>\n            &lt;label class=\"block text-sm font-medium\">\u30af\u30a8\u30b9\u30c8\u6570&lt;\/label>\n            &lt;input id=\"questCount\" type=\"number\" min=\"1\" max=\"30\" value=\"8\" class=\"w-full mt-1 rounded-lg border px-3 py-2\" \/>\n          &lt;\/div>\n        &lt;\/div>\n        &lt;div class=\"grid grid-cols-2 gap-4\">\n          &lt;div>\n            &lt;label class=\"block text-sm font-medium\">\u30b7\u30fc\u30c9\uff08\u540c\u3058\u7d50\u679c\u3092\u518d\u73fe\uff09&lt;\/label>\n            &lt;input id=\"seed\" type=\"text\" class=\"w-full mt-1 rounded-lg border px-3 py-2\" placeholder=\"\u672a\u5165\u529b\u306a\u3089\u81ea\u52d5\" \/>\n          &lt;\/div>\n          &lt;div class=\"flex items-end gap-2\">\n            &lt;input id=\"lockSeed\" type=\"checkbox\" class=\"h-5 w-5\" \/>\n            &lt;label for=\"lockSeed\" class=\"text-sm\">\u30b7\u30fc\u30c9\u56fa\u5b9a\uff08\u518d\u751f\u6210\u3067\u3082\u5909\u5316\u3057\u306a\u3044\uff09&lt;\/label>\n          &lt;\/div>\n        &lt;\/div>\n        &lt;div>\n          &lt;label class=\"block text-sm font-medium\">\u30c8\u30fc\u30f3&lt;\/label>\n          &lt;select id=\"tone\" class=\"w-full mt-1 rounded-lg border px-3 py-2\">\n            &lt;option value=\"classic\" selected>\u53e4\u5178\u30d5\u30a1\u30f3\u30bf\u30b8\u30fc&lt;\/option>\n            &lt;option value=\"dark\">\u30c0\u30fc\u30af&lt;\/option>\n            &lt;option value=\"steampunk\">\u30b9\u30c1\u30fc\u30e0\u30d1\u30f3\u30af&lt;\/option>\n            &lt;option value=\"myth\">\u795e\u8a71\/\u53d9\u4e8b\u8a69&lt;\/option>\n            &lt;option value=\"sci\">\u30b5\u30a4\u30d5\u30a1\u30f3\u30bf\u30b8\u30fc&lt;\/option>\n          &lt;\/select>\n        &lt;\/div>\n        &lt;div class=\"flex flex-wrap gap-2 pt-2\">\n          &lt;button id=\"btnGenerate\" type=\"button\" class=\"px-4 py-2 rounded-xl bg-brand-600 text-white hover:bg-brand-700\">\u751f\u6210&lt;\/button>\n          &lt;button id=\"btnRegenerate\" type=\"button\" class=\"px-4 py-2 rounded-xl bg-slate-800 text-white hover:bg-slate-900\">\u518d\u751f\u6210\uff08\u540c\u6761\u4ef6\uff09&lt;\/button>\n          &lt;button id=\"btnShuffleSeed\" type=\"button\" class=\"px-4 py-2 rounded-xl border\">\u30b7\u30fc\u30c9\u518d\u62bd\u9078&lt;\/button>\n        &lt;\/div>\n      &lt;\/form>\n      &lt;p class=\"text-xs text-slate-500 mt-4\">\u203b\u5916\u90e8API\u4e0d\u4f7f\u7528\u3002\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8\u00d7\u78ba\u7387\u30e2\u30c7\u30eb\u3067\u30ed\u30fc\u30ab\u30eb\u751f\u6210\u3002\u30d6\u30e9\u30a6\u30b6\u4e0a\u3067\u5b8c\u7d50\u3002&lt;\/p>\n    &lt;\/section>\n\n    &lt;!-- \u4e2d\u592e\uff1a\u7d50\u679c\uff08\u30c6\u30ad\u30b9\u30c8\uff09 -->\n    &lt;section class=\"lg:col-span-2 card\">\n      &lt;div class=\"flex items-center gap-2 mb-4\">\n        &lt;h2 class=\"text-lg font-bold\">\u751f\u6210\u7d50\u679c&lt;\/h2>\n        &lt;span id=\"meta\" class=\"ml-auto text-xs text-slate-500\">&lt;\/span>\n      &lt;\/div>\n      &lt;div class=\"flex flex-wrap gap-2 mb-4\">\n        &lt;button id=\"btnCopyText\" class=\"px-3 py-2 rounded-lg border\">\u30c6\u30ad\u30b9\u30c8\u3092\u30b3\u30d4\u30fc&lt;\/button>\n        &lt;button id=\"btnDownloadJSON\" class=\"px-3 py-2 rounded-lg border\">JSON\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9&lt;\/button>\n        &lt;button id=\"btnExportCSV\" class=\"px-3 py-2 rounded-lg border\">CSV\u66f8\u304d\u51fa\u3057&lt;\/button>\n        &lt;button id=\"btnToggleJson\" class=\"px-3 py-2 rounded-lg border\">JSON\u8868\u793a\u5207\u66ff&lt;\/button>\n      &lt;\/div>\n      &lt;div id=\"outText\" class=\"prose max-w-none text-sm leading-6\">&lt;\/div>\n      &lt;details id=\"jsonBlock\" class=\"mt-4 hidden\">\n        &lt;summary class=\"cursor-pointer select-none text-sm text-slate-600\">JSON\u8868\u793a&lt;\/summary>\n        &lt;pre id=\"outJSON\" class=\"mono text-xs bg-slate-50 p-3 rounded-lg overflow-x-auto\">&lt;\/pre>\n      &lt;\/details>\n    &lt;\/section>\n\n    &lt;!-- \u4e0b\uff1a\u30d7\u30ec\u30d3\u30e5\u30fc\uff08\u30ab\u30fc\u30c9\u30ec\u30a4\u30a2\u30a6\u30c8\uff09 -->\n    &lt;section class=\"lg:col-span-3 card\">\n      &lt;h2 class=\"text-lg font-bold mb-4\">\u30ab\u30fc\u30c9\u30d3\u30e5\u30fc&lt;\/h2>\n      &lt;div class=\"grid md:grid-cols-3 gap-4\" id=\"cards\">&lt;\/div>\n    &lt;\/section>\n  &lt;\/main>\n\n  &lt;footer class=\"py-8 text-center text-xs text-slate-500\">\n    &amp;copy; 2025 Quest Foundry \u2014 Local-first Fantasy Content Generator\n  &lt;\/footer>\n\n  &lt;script>\n    \/* =========================\n     *  \u4e71\u6570\u3068\u30e6\u30fc\u30c6\u30a3\u30ea\u30c6\u30a3\n     * ========================= *\/\n    function cyrb128(str){ let h1=1779033703,h2=3144134277,h3=1013904242,h4=2773480762; for(let i=0;i&lt;str.length;i++){ let k=str.charCodeAt(i); h1=h2^(Math.imul(h1^k,597399067)); h2=h3^(Math.imul(h2^k,2869860233)); h3=h4^(Math.imul(h3^k,951274213)); h4=h1^(Math.imul(h4^k,2716044179)); } h1=Math.imul(h3^(h1>>>18),597399067); h2=Math.imul(h4^(h2>>>22),2869860233); h3=Math.imul(h1^(h3>>>17),951274213); h4=Math.imul(h2^(h4>>>19),2716044179); let r=(h1^h2^h3^h4)>>>0; return r.toString(36); }\n    function mulberry32(a){ return function(){ let t=a+=0x6D2B79F5; t=Math.imul(t^(t>>>15), t|1); t^=t+Math.imul(t^(t>>>7), t|61); return ((t^(t>>>14))>>>0)\/4294967296; } }\n    function rngFromSeed(seed){ let n=0; for(const ch of seed) n=(n*31 + ch.charCodeAt(0))>>>0; return mulberry32(n||1); }\n    function choice(r, arr){ return arr&#91;Math.floor(r()*arr.length)] }\n    function pickN(r, arr, n){ const a=&#91;...arr]; const out=&#91;]; for(let i=0;i&lt;n &amp;&amp; a.length;i++){ out.push(a.splice(Math.floor(r()*a.length),1)&#91;0]); } return out; }\n    function cap(s){ return s.charAt(0).toUpperCase()+s.slice(1) }\n    function id(prefix, i){ return `${prefix}-${String(i).padStart(3,'0')}` }\n\n    function syllableName(r, tone){\n      const syll = {\n        classic:&#91;\"an\",\"ar\",\"bel\",\"ca\",\"da\",\"el\",\"fa\",\"gal\",\"har\",\"is\",\"jor\",\"kel\",\"lir\",\"mor\",\"nel\",\"or\",\"pa\",\"qua\",\"rhi\",\"sa\",\"tor\",\"ur\",\"val\",\"wen\",\"xel\",\"yor\",\"zel\"],\n        dark:&#91;\"mor\",\"noir\",\"gloam\",\"umb\",\"dol\",\"grav\",\"nek\",\"var\",\"zul\",\"vex\",\"drei\",\"thar\",\"khar\",\"wyrm\"],\n        steampunk:&#91;\"gear\",\"steam\",\"bolt\",\"cog\",\"brass\",\"tink\",\"pneu\",\"copper\",\"fuse\",\"riv\",\"spindle\"],\n        myth:&#91;\"aeg\",\"od\",\"ish\",\"ra\",\"zeph\",\"io\",\"sol\",\"lun\",\"tyr\",\"fre\",\"eir\",\"hel\"],\n        sci:&#91;\"neo\",\"ion\",\"quant\",\"cyber\",\"astra\",\"plasma\",\"proto\",\"omega\",\"nova\",\"phase\",\"flux\"]\n      };\n      const pool = (syll&#91;tone]||&#91;]).concat(syll.classic);\n      const len = 2 + Math.floor(r()*2);\n      let s=\"\"; for(let i=0;i&lt;len;i++) s+= choice(r,pool);\n      return cap(s);\n    }\n\n    \/* =========================\n     *  \u30c6\u30f3\u30d7\u30ec\uff0f\u8a9e\u5f59\n     * ========================= *\/\n    const LEX = {\n      roles: &#91;\"\u30ae\u30eb\u30c9\u30de\u30b9\u30bf\u30fc\",\"\u8003\u53e4\u5b66\u8005\",\"\u5de1\u56de\u9a0e\u58eb\",\"\u5bc6\u5075\",\"\u5360\u661f\u8853\u5e2b\",\"\u932c\u91d1\u8853\u5e2b\",\"\u65c5\u306e\u5546\u4eba\",\"\u5deb\u5973\",\"\u53f8\u66f8\",\"\u935b\u51b6\u5e2b\",\"\u8239\u4e57\u308a\",\"\u85ac\u5e2b\",\"\u72e9\u4eba\",\"\u541f\u904a\u8a69\u4eba\",\"\u4fee\u9053\u58eb\"],\n      traits: &#91;\"\u52c7\u6562\",\"\u72e1\u733e\",\"\u535a\u8b58\",\"\u77ed\u6c17\",\"\u8aa0\u5b9f\",\"\u731c\u7591\u5fc3\u304c\u5f37\u3044\",\"\u967d\u6c17\",\"\u51b7\u9759\",\"\u8a08\u7b97\u9ad8\u3044\",\"\u81c6\u75c5\",\"\u7fa9\u7406\u5805\u3044\",\"\u91ce\u5fc3\u5bb6\"],\n      factions: &#91;\"\u78a7\u661f\u540c\u76df\",\"\u7802\u51a0\u5546\u4f1a\",\"\u87ba\u65cb\u6559\u56e3\",\"\u53e4\u56f3\u66f8\u9a0e\u58eb\u56e3\",\"\u767d\u9727\u65c5\u56e3\",\"\u9306\u9244\u5de5\u623f\",\"\u98a8\u8a60\u307f\u96c6\u843d\",\"\u8d64\u7802\u76d7\u8cca\u56e3\"],\n      biomes: &#91;\"\u7802\u6f20\",\"\u6e7f\u539f\",\"\u9ed2\u68ee\",\"\u9ad8\u5730\",\"\u6cbf\u5cb8\",\"\u96ea\u539f\",\"\u706b\u5c71\u5730\u5e2f\",\"\u53e4\u4ee3\u90fd\u5e02\u8de1\"],\n      itemTypes: &#91;\"\u5263\",\"\u77ed\u5263\",\"\u69cd\",\"\u6756\",\"\u5f13\",\"\u8b77\u7b26\",\"\u6307\u8f2a\",\"\u66f8\",\"\u8a2d\u8a08\u56f3\",\"\u85ac\",\"\u9271\u77f3\",\"\u5e03\",\"\u30ec\u30f3\u30ba\",\"\u30b3\u30a4\u30eb\"],\n      rarities: &#91;\"Common\",\"Uncommon\",\"Rare\",\"Epic\",\"Legendary\"],\n      verbs: &#91;\"\u6551\u51fa\u305b\u3088\",\"\u8b77\u885b\u305b\u3088\",\"\u63a2\u7d22\u305b\u3088\",\"\u596a\u9084\u305b\u3088\",\"\u8abf\u67fb\u305b\u3088\",\"\u8a0e\u4f10\u305b\u3088\",\"\u4fee\u5fa9\u305b\u3088\",\"\u5c01\u5370\u305b\u3088\",\"\u4ea4\u6e09\u305b\u3088\",\"\u8b77\u9001\u305b\u3088\",\"\u6f5c\u5165\u305b\u3088\"],\n      twists: &#91;\"\u4f9d\u983c\u4e3b\u306f\u771f\u72af\u4eba\",\"\u5b9f\u306f\u6642\u9593\u5236\u9650\u3042\u308a\",\"\u4e8c\u91cd\u30b9\u30d1\u30a4\u304c\u3044\u308b\",\"\u507d\u7269\u304c\u6df7\u3058\u3063\u3066\u3044\u308b\",\"\u53e4\u304d\u546a\u3044\u304c\u518d\u767a\",\"\u5929\u5019\u7570\u5e38\u304c\u767a\u751f\",\"\u5100\u5f0f\u306e\u65e5\u304c\u524d\u5012\u3057\"],\n      rewardsExtra: &#91;\"\u8a55\u5224+10\",\"\u30ae\u30eb\u30c9\u30e9\u30f3\u30af\u6607\u683c\",\"\u96a0\u3057\u5e97\u8217\u306e\u89e3\u653e\",\"\u65c5\u4eba\u306e\u52a0\u8b77\",\"\u5feb\u901f\u79fb\u52d5\u306e\u89e3\u653e\"]\n    };\n\n    const DIFF_MULT = { easy: 0.8, normal: 1.0, hard: 1.3, epic: 1.7 };\n\n    \/* =========================\n     *  \u751f\u6210\u5668\n     * ========================= *\/\n    function genFactions(r, themes){\n      const count = Math.min(5, 2 + Math.floor(r()*4));\n      return Array.from({length:count}, (_,i)=>({ id: id('F',i+1), name: `${choice(r,LEX.factions)}`, goal: `${choice(r,&#91;\"\u907a\u7269\u306e\u72ec\u5360\",\"\u53e4\u6587\u66f8\u306e\u89e3\u8aad\",\"\u4ea4\u6613\u8def\u306e\u638c\u63e1\",\"\u7981\u8853\u306e\u5fa9\u6d3b\",\"\u8fba\u5883\u9632\u885b\"])}`, vibe: choice(r,&#91;\"\u5354\u8abf\u7684\",\"\u4e2d\u7acb\",\"\u6575\u5bfe\u7684\"]) }));\n    }\n\n    function genLocations(r, themes){\n      const count = Math.min(8, 4 + Math.floor(r()*5));\n      return Array.from({length:count}, (_,i)=>({ id: id('L',i+1), name: `${choice(r,LEX.biomes)}\u306e${syllableName(r,'classic')}`, feature: choice(r,&#91;\"\u5d29\u308c\u305f\u9580\",\"\u5c01\u3058\u77f3\",\"\u5149\u308b\u7891\u6587\",\"\u96a0\u3057\u6c34\u8def\",\"\u6d6e\u904a\u8db3\u5834\",\"\u53e4\u4ee3\u6a5f\u69cb\"]) }));\n    }\n\n    function genNPCs(r, tone, factions){\n      const count = Math.min(12, 6 + Math.floor(r()*6));\n      return Array.from({length:count}, (_,i)=>{\n        const fac = choice(r, factions);\n        return {\n          id: id('N',i+1),\n          name: syllableName(r,tone),\n          role: choice(r, LEX.roles),\n          trait: choice(r, LEX.traits),\n          faction: fac?.id || null\n        }\n      });\n    }\n\n    function genItems(r, tone){\n      const count = Math.min(18, 8 + Math.floor(r()*10));\n      return Array.from({length:count}, (_,i)=>{\n        const t = choice(r, LEX.itemTypes);\n        const rare = choice(r, LEX.rarities);\n        return {\n          id: id('I',i+1),\n          name: `${syllableName(r,tone)}\u306e${t}`,\n          type: t,\n          rarity: rare,\n          value: Math.floor((10+ r()*90) * (1 + 0.3*LEX.rarities.indexOf(rare)))\n        }\n      });\n    }\n\n    function genQuests(r, tone, count, npcs, locations, items, difficulty){\n      const q = &#91;];\n      const scale = DIFF_MULT&#91;difficulty] || 1.0;\n      for(let i=0;i&lt;count;i++){\n        const giver = choice(r, npcs);\n        const loc = choice(r, locations);\n        const verb = choice(r, LEX.verbs);\n        const keyItem = choice(r, items);\n        const level = Math.max(1, Math.round((i+1)*scale + r()*3));\n        const objectives = &#91;\n          `${loc.name}\u3067\u624b\u639b\u304b\u308a\u3092\u898b\u3064\u3051\u308b`,\n          `${giver.name}\uff08${giver.role}\uff09\u306b\u5831\u544a\u3059\u308b`,\n          `${keyItem.name}\u3092\u5165\u624b\u3059\u308b`\n        ];\n        \/\/ \u4f9d\u5b58\u95a2\u4fc2\uff1a\u7a00\u306b\u524d\u306e\u30af\u30a8\u30b9\u30c8\u3092\u524d\u63d0\u306b\u3059\u308b\n        let dependsOn = null;\n        if(i>0 &amp;&amp; r()&lt;0.4){ dependsOn = q&#91;Math.floor(r()*i)].id; }\n        \/\/ \u30c4\u30a4\u30b9\u30c8\u306f\u4f4e\u78ba\u7387\u3067\n        const twist = r()&lt;0.35 ? choice(r, LEX.twists) : null;\n        const rewardGold = Math.floor((100+ r()*200) * scale * (1 + i*0.05));\n        const rewardItems = pickN(r, items, r()&lt;0.6?1:2).map(o=>o.id);\n        q.push({\n          id: id('Q',i+1),\n          title: `${verb}\uff1a${loc.name}`,\n          level,\n          giver: giver.id,\n          location: loc.id,\n          objectives,\n          requires: dependsOn,\n          reward: { gold: rewardGold, items: rewardItems, extra: r()&lt;0.25? choice(r, LEX.rewardsExtra): null },\n          twist\n        });\n      }\n      return q;\n    }\n\n    function assembleWorld(input){\n      const seed = input.seed || `${Date.now().toString(36)}-${cyrb128(input.worldName + (input.themes||''))}`;\n      const r = rngFromSeed(seed);\n      const tone = input.tone || 'classic';\n      const factions = genFactions(r, input.themes);\n      const locations = genLocations(r, input.themes);\n      const npcs = genNPCs(r, tone, factions);\n      const items = genItems(r, tone);\n      const quests = genQuests(r, tone, input.questCount, npcs, locations, items, input.difficulty);\n      return { meta: { seed, createdAt: new Date().toISOString(), worldName: input.worldName||syllableName(r,tone), themes: input.themes, difficulty: input.difficulty, tone }, factions, locations, npcs, items, quests };\n    }\n\n    \/* =========================\n     *  \u51fa\u529b\u30ec\u30f3\u30c0\u30ea\u30f3\u30b0\n     * ========================= *\/\n    function renderText(world){\n      const idmap = (arr)=> Object.fromEntries(arr.map(a=>&#91;a.id,a]));\n      const NPC = idmap(world.npcs);\n      const LOC = idmap(world.locations);\n      const ITM = idmap(world.items);\n\n      const lines = &#91;];\n      lines.push(`# \u4e16\u754c\uff1a${world.meta.worldName}`);\n      lines.push(`- \u30c6\u30fc\u30de\uff1a${world.meta.themes||'\u2014'} \/ \u30c8\u30fc\u30f3\uff1a${world.meta.tone} \/ \u96e3\u6613\u5ea6\uff1a${world.meta.difficulty}`);\n      lines.push(`- \u751f\u6210\u65e5\u6642\uff1a${new Date(world.meta.createdAt).toLocaleString()}`);\n      lines.push(`- \u30b7\u30fc\u30c9\uff1a${world.meta.seed}`);\n      lines.push(`\\n## \u52e2\u529b\uff08${world.factions.length}\uff09`);\n      world.factions.forEach(f=>{ lines.push(`- &#91;${f.id}] ${f.name}\uff5c\u76ee\u7684\uff1a${f.goal}\uff5c\u614b\u5ea6\uff1a${f.vibe}`) });\n      lines.push(`\\n## \u5834\u6240\uff08${world.locations.length}\uff09`);\n      world.locations.forEach(l=>{ lines.push(`- &#91;${l.id}] ${l.name}\uff5c\u7279\u5fb4\uff1a${l.feature}`) });\n      lines.push(`\\n## NPC\uff08${world.npcs.length}\uff09`);\n      world.npcs.forEach(n=>{ lines.push(`- &#91;${n.id}] ${n.name}\uff08${n.role}\/${n.trait}\uff09 \u6240\u5c5e\uff1a${n.faction||'\u306a\u3057'}`) });\n      lines.push(`\\n## \u30a2\u30a4\u30c6\u30e0\uff08${world.items.length}\uff09`);\n      world.items.forEach(i=>{ lines.push(`- &#91;${i.id}] ${i.name}\uff5c\u7a2e\u985e\uff1a${i.type}\uff5c\u5e0c\u5c11\u5ea6\uff1a${i.rarity}\uff5c\u4fa1\u5024\uff1a${i.value}`) });\n      lines.push(`\\n## \u30af\u30a8\u30b9\u30c8\uff08${world.quests.length}\uff09`);\n      world.quests.forEach(q=>{\n        const giver = NPC&#91;q.giver]?.name || q.giver;\n        const loc = LOC&#91;q.location]?.name || q.location;\n        const req = q.requires? `\uff08\u524d\u63d0\uff1a${q.requires}\uff09` : '';\n        lines.push(`\\n### &#91;${q.id}] ${q.title} Lv.${q.level} ${req}`);\n        lines.push(`- \u4f9d\u983c\u4e3b\uff1a${giver}`);\n        lines.push(`- \u5834\u6240\uff1a${loc}`);\n        lines.push(`- \u76ee\u7684\uff1a`);\n        q.objectives.forEach(o=>lines.push(`  - ${o}`));\n        const rewardItems = q.reward.items.map(id=> ITM&#91;id]?.name || id).join('\u3001');\n        lines.push(`- \u5831\u916c\uff1a${q.reward.gold}G \/ \u30a2\u30a4\u30c6\u30e0\uff1a${rewardItems}${q.reward.extra? ' \/ '+q.reward.extra:''}`);\n        if(q.twist) lines.push(`- \u30c4\u30a4\u30b9\u30c8\uff1a${q.twist}`);\n      });\n      return lines.join('\\n');\n    }\n\n    function renderHTML(world){\n      const idmap = (arr)=> Object.fromEntries(arr.map(a=>&#91;a.id,a]));\n      const NPC = idmap(world.npcs);\n      const LOC = idmap(world.locations);\n      const ITM = idmap(world.items);\n\n      const head = `\n        &lt;h1>\u4e16\u754c\uff1a${world.meta.worldName}&lt;\/h1>\n        &lt;div class=\"kv text-sm text-slate-600 gap-x-2\">\n          &lt;span class=\"badge\">\u30c8\u30fc\u30f3\uff1a${world.meta.tone}&lt;\/span>\n          &lt;span class=\"badge\">\u96e3\u6613\u5ea6\uff1a${world.meta.difficulty}&lt;\/span>\n          &lt;span class=\"badge\">\u30af\u30a8\u30b9\u30c8\uff1a${world.quests.length}&lt;\/span>\n          &lt;span class=\"badge\">\u30b7\u30fc\u30c9\uff1a${world.meta.seed}&lt;\/span>\n        &lt;\/div>\n        &lt;p class=\"mt-2 text-sm text-slate-600\">\u30c6\u30fc\u30de\uff1a${world.meta.themes||'\u2014'} \uff0f \u751f\u6210\u65e5\u6642\uff1a${new Date(world.meta.createdAt).toLocaleString()}&lt;\/p>\n      `;\n\n      const factions = `\n        &lt;h2>\u52e2\u529b\uff08${world.factions.length}\uff09&lt;\/h2>\n        &lt;ul>\n          ${world.factions.map(f=>`&lt;li>&lt;code>&#91;${f.id}]&lt;\/code> ${f.name}\uff5c\u76ee\u7684\uff1a${f.goal}\uff5c\u614b\u5ea6\uff1a${f.vibe}&lt;\/li>`).join('')}\n        &lt;\/ul>\n      `;\n\n      const locs = `\n        &lt;h2>\u5834\u6240\uff08${world.locations.length}\uff09&lt;\/h2>\n        &lt;ul>\n          ${world.locations.map(l=>`&lt;li>&lt;code>&#91;${l.id}]&lt;\/code> ${l.name}\uff5c\u7279\u5fb4\uff1a${l.feature}&lt;\/li>`).join('')}\n        &lt;\/ul>\n      `;\n\n      const npcs = `\n        &lt;h2>NPC\uff08${world.npcs.length}\uff09&lt;\/h2>\n        &lt;ul>\n          ${world.npcs.map(n=>`&lt;li>&lt;code>&#91;${n.id}]&lt;\/code> ${n.name}\uff08${n.role}\/${n.trait}\uff09 \u6240\u5c5e\uff1a${n.faction||'\u306a\u3057'}&lt;\/li>`).join('')}\n        &lt;\/ul>\n      `;\n\n      const items = `\n        &lt;h2>\u30a2\u30a4\u30c6\u30e0\uff08${world.items.length}\uff09&lt;\/h2>\n        &lt;ul>\n          ${world.items.map(i=>`&lt;li>&lt;code>&#91;${i.id}]&lt;\/code> ${i.name}\uff5c\u7a2e\u985e\uff1a${i.type}\uff5c\u5e0c\u5c11\u5ea6\uff1a${i.rarity}\uff5c\u4fa1\u5024\uff1a${i.value}&lt;\/li>`).join('')}\n        &lt;\/ul>\n      `;\n\n      const quests = `\n        &lt;h2>\u30af\u30a8\u30b9\u30c8\uff08${world.quests.length}\uff09&lt;\/h2>\n        ${world.quests.map(q=>{\n          const giver = NPC&#91;q.giver]?.name || q.giver;\n          const loc = LOC&#91;q.location]?.name || q.location;\n          const req = q.requires? `\uff08\u524d\u63d0\uff1a${q.requires}\uff09` : '';\n          const rewardItems = q.reward.items.map(id=> ITM&#91;id]?.name || id).join('\u3001');\n          return `\n            &lt;details class=\"quest\">\n              &lt;summary>&lt;strong>&lt;code>&#91;${q.id}]&lt;\/code> ${q.title}&lt;\/strong> &lt;span class=\"text-sm text-slate-600\">Lv.${q.level} ${req}&lt;\/span>&lt;\/summary>\n              &lt;div class=\"mt-2 text-sm\">\n                &lt;div>\u4f9d\u983c\u4e3b\uff1a${giver}&lt;\/div>\n                &lt;div>\u5834\u6240\uff1a${loc}&lt;\/div>\n                &lt;div class=\"mt-1\">\u76ee\u7684\uff1a&lt;\/div>\n                &lt;ul>\n                  ${q.objectives.map(o=>`&lt;li>${o}&lt;\/li>`).join('')}\n                &lt;\/ul>\n                &lt;div class=\"mt-1\">\u5831\u916c\uff1a${q.reward.gold}G \uff0f \u30a2\u30a4\u30c6\u30e0\uff1a${rewardItems}${q.reward.extra? ' \uff0f '+q.reward.extra:''}&lt;\/div>\n                ${q.twist? `&lt;div class=\"mt-1 text-rose-700\">\u30c4\u30a4\u30b9\u30c8\uff1a${q.twist}&lt;\/div>`:''}\n              &lt;\/div>\n            &lt;\/details>`;\n        }).join('')}\n      `;\n\n      return &#91;head, factions, locs, npcs, items, quests].join('');\n    }\n\n    function renderCards(world){\n      const $cards = document.getElementById('cards');\n      $cards.innerHTML = '';\n      const make = (title, body)=>{\n        const el = document.createElement('div');\n        el.className = 'rounded-2xl border p-4 bg-white';\n        el.innerHTML = `&lt;div class=\"text-sm font-bold mb-2\">${title}&lt;\/div>&lt;div class=\"text-xs text-slate-700 whitespace-pre-wrap\">${body}&lt;\/div>`;\n        $cards.appendChild(el);\n      };\n      make('\u30ef\u30fc\u30eb\u30c9', `\u540d\u524d\uff1a${world.meta.worldName}\\n\u96e3\u6613\u5ea6\uff1a${world.meta.difficulty}\\n\u30c8\u30fc\u30f3\uff1a${world.meta.tone}\\n\u30b7\u30fc\u30c9\uff1a${world.meta.seed}`);\n      make('\u52e2\u529b', world.factions.map(f=>`&#91;${f.id}] ${f.name}\uff0f\u76ee\u7684:${f.goal}`).join('\\n'));\n      make('\u5834\u6240', world.locations.map(l=>`&#91;${l.id}] ${l.name}\uff0f${l.feature}`).join('\\n'));\n      make('NPC', world.npcs.slice(0,12).map(n=>`&#91;${n.id}] ${n.name}\uff0f${n.role}`).join('\\n'));\n      make('\u30a2\u30a4\u30c6\u30e0', world.items.slice(0,15).map(i=>`&#91;${i.id}] ${i.name}\uff0f${i.rarity}`).join('\\n'));\n      make('\u30af\u30a8\u30b9\u30c8', world.quests.map(q=>`&#91;${q.id}] ${q.title} Lv.${q.level}${q.requires? '\uff08\u524d\u63d0:'+q.requires+'\uff09':''}`).join('\\n'));\n    }\n\n    \/* =========================\n     *  CSV\/JSON\/\u30b3\u30d4\u30fc\/\u4fdd\u5b58\n     * ========================= *\/\n    function toCSV(rows){\n      return rows.map(r=> r.map(v=>`\"${String(v).replaceAll('\"','\"\"')}\"`).join(',')).join('\\n');\n    }\n    function exportCSVs(world){\n      const npcRows = &#91;&#91;\"id\",\"name\",\"role\",\"trait\",\"faction\"]].concat(world.npcs.map(n=>&#91;n.id,n.name,n.role,n.trait,n.faction||'']));\n      const itemRows = &#91;&#91;\"id\",\"name\",\"type\",\"rarity\",\"value\"]].concat(world.items.map(i=>&#91;i.id,i.name,i.type,i.rarity,i.value]));\n      const questRows = &#91;&#91;\"id\",\"title\",\"level\",\"giver\",\"location\",\"requires\",\"objectives\",\"reward_gold\",\"reward_items\",\"twist\"]].concat(\n        world.quests.map(q=>&#91;\n          q.id, q.title, q.level, q.giver, q.location, q.requires||'', q.objectives.join(' \/ '), q.reward.gold, q.reward.items.join('|'), q.twist||''\n        ])\n      );\n      const files = &#91;\n        {name:`${world.meta.worldName}_NPC.csv`, data: toCSV(npcRows)},\n        {name:`${world.meta.worldName}_Items.csv`, data: toCSV(itemRows)},\n        {name:`${world.meta.worldName}_Quests.csv`, data: toCSV(questRows)}\n      ];\n      files.forEach(f=>{\n        const blob = new Blob(&#91;\"\\ufeff\"+f.data], {type:'text\/csv'});\n        const a = document.createElement('a');\n        a.href = URL.createObjectURL(blob);\n        a.download = f.name; a.click(); URL.revokeObjectURL(a.href);\n      });\n    }\n    function downloadJSON(world){\n      const blob = new Blob(&#91;JSON.stringify(world, null, 2)], {type:'application\/json'});\n      const a = document.createElement('a');\n      a.href = URL.createObjectURL(blob);\n      a.download = `${world.meta.worldName}_world.json`; a.click(); URL.revokeObjectURL(a.href);\n    }\n    function copyText(text){\n      navigator.clipboard.writeText(text).then(()=>{\n        toast('\u30c6\u30ad\u30b9\u30c8\u3092\u30b3\u30d4\u30fc\u3057\u307e\u3057\u305f');\n      });\n    }\n    function saveLocal(world){ localStorage.setItem('quest_foundry_last', JSON.stringify(world)); toast('\u4fdd\u5b58\u3057\u307e\u3057\u305f'); }\n    function loadLocal(){ const s=localStorage.getItem('quest_foundry_last'); if(!s){ toast('\u4fdd\u5b58\u30c7\u30fc\u30bf\u306a\u3057'); return null; } try{ return JSON.parse(s);}catch(e){ toast('\u8aad\u8fbc\u5931\u6557'); return null; } }\n\n    \/* =========================\n     *  UI\n     * ========================= *\/\n    function toast(msg){\n      const t = document.createElement('div');\n      t.className = 'fixed bottom-4 left-1\/2 -translate-x-1\/2 bg-slate-900 text-white text-sm px-4 py-2 rounded-xl shadow-lg';\n      t.textContent = msg; document.body.appendChild(t);\n      setTimeout(()=>{ t.classList.add('opacity-0'); t.style.transition='opacity .6s'; }, 1600);\n      setTimeout(()=> t.remove(), 2300);\n    }\n\n    let lastInput = null;\n    let lastWorld = null;\n\n    function currentInput(){\n      const worldName = document.getElementById('worldName').value.trim();\n      const themes = document.getElementById('themes').value.trim();\n      const difficulty = document.getElementById('difficulty').value;\n      const questCount = Math.max(1, Math.min(30, parseInt(document.getElementById('questCount').value || '8')));\n      const seed = document.getElementById('seed').value.trim();\n      const tone = document.getElementById('tone').value;\n      return { worldName, themes, difficulty, questCount, seed, tone };\n    }\n\n    function applyWorld(world){\n      lastWorld = world;\n      document.getElementById('meta').textContent = `\u30ef\u30fc\u30eb\u30c9\uff1a${world.meta.worldName} \/ \u30af\u30a8\u30b9\u30c8\uff1a${world.quests.length}\u4ef6`;\n      document.getElementById('outText').innerHTML = renderHTML(world);\n      document.getElementById('outJSON').textContent = JSON.stringify(world, null, 2);\n      renderCards(world);\n    }\n\n    function generate(withNewSeed=false){\n      const input = currentInput();\n      if(withNewSeed &amp;&amp; !document.getElementById('lockSeed').checked){ input.seed = ''; }\n      if(!input.seed) { input.seed = cyrb128((input.worldName||'World') + (input.themes||'') + Date.now()); document.getElementById('seed').value = input.seed; }\n      lastInput = input;\n      const world = assembleWorld(input);\n      applyWorld(world);\n    }\n\n    \/\/ \u30a4\u30d9\u30f3\u30c8\n    document.getElementById('btnGenerate').addEventListener('click', ()=> generate(false));\n    document.getElementById('btnRegenerate').addEventListener('click', ()=> generate(false));\n    document.getElementById('btnShuffleSeed').addEventListener('click', ()=> generate(true));\n    document.getElementById('btnCopyText').addEventListener('click', ()=>{ if(lastWorld) copyText(renderText(lastWorld)); });\n    document.getElementById('btnDownloadJSON').addEventListener('click', ()=>{ if(lastWorld) downloadJSON(lastWorld); });\n    document.getElementById('btnExportCSV').addEventListener('click', ()=>{ if(lastWorld) exportCSVs(lastWorld); });\n    document.getElementById('btnToggleJson').addEventListener('click', ()=>{ document.getElementById('jsonBlock').classList.toggle('hidden'); });\n    document.getElementById('btnSave').addEventListener('click', ()=>{ if(lastWorld) saveLocal(lastWorld); });\n    document.getElementById('btnLoad').addEventListener('click', ()=>{ const w=loadLocal(); if(w) applyWorld(w); });\n    document.getElementById('btnPrint').addEventListener('click', ()=> window.print());\n\n    \/\/ \u521d\u671f\u30d7\u30ec\u30fc\u30b9\u30db\u30eb\u30c0\u751f\u6210\n    window.addEventListener('DOMContentLoaded', ()=>{\n      document.getElementById('worldName').value = '\u904b\u547d\u306e\u5263\u754c';\n      document.getElementById('themes').value = '\u53e4\u4ee3\u907a\u8de1 \u98a8\u306e\u7cbe\u970a \u7802\u6f20 \u65c5\u4eba\u30ae\u30eb\u30c9';\n      generate(true);\n    });\n  &lt;\/script>\n&lt;\/body>\n&lt;\/html>\n<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"_uf_show_specific_survey":0,"_uf_disable_surveys":false,"footnotes":""},"categories":[80,87],"tags":[3],"class_list":["post-26181","post","type-post","status-publish","format-standard","hentry","category-html","category-web","tag-programming"],"aioseo_notices":[],"jetpack_featured_media_url":"","_links":{"self":[{"href":"http:\/\/www.tyosuke20xx.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/26181","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/www.tyosuke20xx.com\/blog\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/www.tyosuke20xx.com\/blog\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/www.tyosuke20xx.com\/blog\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/www.tyosuke20xx.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=26181"}],"version-history":[{"count":1,"href":"http:\/\/www.tyosuke20xx.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/26181\/revisions"}],"predecessor-version":[{"id":26182,"href":"http:\/\/www.tyosuke20xx.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/26181\/revisions\/26182"}],"wp:attachment":[{"href":"http:\/\/www.tyosuke20xx.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=26181"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/www.tyosuke20xx.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=26181"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/www.tyosuke20xx.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=26181"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}