chore: initialize homebrew-magic-rpg monorepo
Consolidates the HbM: RPG project (formerly the loose hbm-books working folder) into a single repo. ObsidianNotes, foundry-lore, and foundry-system are wired in as submodules pointing at their existing Gitea repos. Loose tooling scripts are organized under tools/, obsolete/one-off scripts (fix_vault.py, generate_mocs.py, debug-*.ts, write_file_helper.py) and _books/ (superseded by ObsidianNotes) are excluded via .gitignore rather than deleted. foundryvtt-admin is excluded pending its own separate repo.
This commit is contained in:
+32
@@ -0,0 +1,32 @@
|
|||||||
|
# Secrets - never commit
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
*.env
|
||||||
|
gdrive-token.json
|
||||||
|
client_secret*
|
||||||
|
.thorclient/
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Generated / compiled - regenerate via tools/
|
||||||
|
NotebookLM_Uploads/
|
||||||
|
.output/
|
||||||
|
__pycache__/
|
||||||
|
vault_analysis_report.json
|
||||||
|
batch_gen.log
|
||||||
|
scratch_images.txt
|
||||||
|
Foundry-Data/data/books-cache.json
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Excluded from this repo for now (see cleanup notes)
|
||||||
|
_books/
|
||||||
|
fix_vault.py
|
||||||
|
generate_mocs.py
|
||||||
|
debug-parse.ts
|
||||||
|
debug-spell.ts
|
||||||
|
debug-walker.ts
|
||||||
|
write_file_helper.py
|
||||||
|
foundryvtt-admin/
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[submodule "Foundry-Data/foundry-lore"]
|
||||||
|
path = Foundry-Data/foundry-lore
|
||||||
|
url = https://git.octoturge.com/octoturge/hbm-foundry-lore.git
|
||||||
|
[submodule "Foundry-Data/foundry-system"]
|
||||||
|
path = Foundry-Data/foundry-system
|
||||||
|
url = https://git.octoturge.com/octoturge/hbm-foundry-system.git
|
||||||
|
[submodule "ObsidianNotes"]
|
||||||
|
path = ObsidianNotes
|
||||||
|
url = https://git.octoturge.com/octoturge/hbm-obsidian.git
|
||||||
Vendored
+71
@@ -0,0 +1,71 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "HbM: Main CLI",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "bun",
|
||||||
|
"args": ["run", "hbm"],
|
||||||
|
"problemMatcher": [],
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "shared"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Google Drive: Authenticate",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "bun",
|
||||||
|
"args": ["run", "gdrive:auth"],
|
||||||
|
"problemMatcher": [],
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "shared"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Google Drive: List Books",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "bun",
|
||||||
|
"args": ["run", "gdrive:list"],
|
||||||
|
"problemMatcher": [],
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "shared"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Google Drive: Sync Books",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "bun",
|
||||||
|
"args": ["run", "gdrive:sync"],
|
||||||
|
"problemMatcher": [],
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "shared"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Content: Initialize New",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "bun",
|
||||||
|
"args": ["run", "content:init"],
|
||||||
|
"problemMatcher": [],
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "shared"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Content: Search Books",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "bun",
|
||||||
|
"args": ["run", "search"],
|
||||||
|
"problemMatcher": [],
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "shared"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/server-gdrive": "^2025.1.14",
|
||||||
|
"googleapis": "^170.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@google-cloud/local-auth": ["@google-cloud/local-auth@3.0.1", "", { "dependencies": { "arrify": "^2.0.1", "google-auth-library": "^9.0.0", "open": "^7.0.3", "server-destroy": "^1.0.1" } }, "sha512-YJ3GFbksfHyEarbVHPSCzhKpjbnlAhdzg2SEf79l6ODukrSM1qUOqfopY232Xkw26huKSndyzmJz+A6b2WYn7Q=="],
|
||||||
|
|
||||||
|
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.0.1", "", { "dependencies": { "content-type": "^1.0.5", "raw-body": "^3.0.0", "zod": "^3.23.8" } }, "sha512-slLdFaxQJ9AlRg+hw28iiTtGvShAOgOKXcD0F91nUcRYiOMuS9ZBYjcdNZRXW9G5JQ511GRTdUy1zQVZDpJ+4w=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/server-gdrive": ["@modelcontextprotocol/server-gdrive@2025.1.14", "", { "dependencies": { "@google-cloud/local-auth": "^3.0.1", "@modelcontextprotocol/sdk": "1.0.1", "googleapis": "^144.0.0" }, "bin": { "mcp-server-gdrive": "dist/index.js" } }, "sha512-MWrvgaHARzAsFEYpzOrG/ewYoWrDSvF1ybmW56cMIrnVQekTP0iGWKC/aEXVcc+V5hAmMGm7iABSJFNrBX9a0A=="],
|
||||||
|
|
||||||
|
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||||
|
|
||||||
|
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||||
|
|
||||||
|
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||||
|
|
||||||
|
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||||
|
|
||||||
|
"arrify": ["arrify@2.0.1", "", {}, "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug=="],
|
||||||
|
|
||||||
|
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
|
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||||
|
|
||||||
|
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
|
||||||
|
|
||||||
|
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||||
|
|
||||||
|
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||||
|
|
||||||
|
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||||
|
|
||||||
|
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||||
|
|
||||||
|
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||||
|
|
||||||
|
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
|
|
||||||
|
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
|
|
||||||
|
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||||
|
|
||||||
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
|
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
|
||||||
|
|
||||||
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||||
|
|
||||||
|
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
|
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||||
|
|
||||||
|
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||||
|
|
||||||
|
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||||
|
|
||||||
|
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||||
|
|
||||||
|
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||||
|
|
||||||
|
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||||
|
|
||||||
|
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||||
|
|
||||||
|
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
||||||
|
|
||||||
|
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||||
|
|
||||||
|
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
||||||
|
|
||||||
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
|
"gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="],
|
||||||
|
|
||||||
|
"gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
|
||||||
|
|
||||||
|
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||||
|
|
||||||
|
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||||
|
|
||||||
|
"glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
|
||||||
|
|
||||||
|
"google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="],
|
||||||
|
|
||||||
|
"google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
|
||||||
|
|
||||||
|
"googleapis": ["googleapis@170.0.0", "", { "dependencies": { "google-auth-library": "^10.2.0", "googleapis-common": "^8.0.0" } }, "sha512-UJz71WZ3ubMr4NhkEU+CFTS0CMrrXq+ltrFnAQo8Llf9M3cy0AIfKLyFQdUJyhqIpJ4jPW4SRCcBBntrLQ72/A=="],
|
||||||
|
|
||||||
|
"googleapis-common": ["googleapis-common@8.0.1", "", { "dependencies": { "extend": "^3.0.2", "gaxios": "^7.0.0-rc.4", "google-auth-library": "^10.1.0", "qs": "^6.7.0", "url-template": "^2.0.8" } }, "sha512-eCzNACUXPb1PW5l0ULTzMHaL/ltPRADoPgjBlT8jWsTbxkCp6siv+qKJ/1ldaybCthGwsYFYallF7u9AkU4L+A=="],
|
||||||
|
|
||||||
|
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||||
|
|
||||||
|
"gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="],
|
||||||
|
|
||||||
|
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||||
|
|
||||||
|
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
|
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||||
|
|
||||||
|
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||||
|
|
||||||
|
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||||
|
|
||||||
|
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||||
|
|
||||||
|
"is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
|
||||||
|
|
||||||
|
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||||
|
|
||||||
|
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||||
|
|
||||||
|
"is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
|
||||||
|
|
||||||
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
|
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||||
|
|
||||||
|
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
|
||||||
|
|
||||||
|
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
|
||||||
|
|
||||||
|
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
|
||||||
|
|
||||||
|
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||||
|
|
||||||
|
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
|
|
||||||
|
"minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|
||||||
|
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||||
|
|
||||||
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
||||||
|
|
||||||
|
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||||
|
|
||||||
|
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||||
|
|
||||||
|
"open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
|
||||||
|
|
||||||
|
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
||||||
|
|
||||||
|
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
|
|
||||||
|
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||||
|
|
||||||
|
"qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="],
|
||||||
|
|
||||||
|
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||||
|
|
||||||
|
"rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="],
|
||||||
|
|
||||||
|
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
|
|
||||||
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
|
"server-destroy": ["server-destroy@1.0.1", "", {}, "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ=="],
|
||||||
|
|
||||||
|
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||||
|
|
||||||
|
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||||
|
|
||||||
|
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||||
|
|
||||||
|
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||||
|
|
||||||
|
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||||
|
|
||||||
|
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||||
|
|
||||||
|
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||||
|
|
||||||
|
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||||
|
|
||||||
|
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||||
|
|
||||||
|
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||||
|
|
||||||
|
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
|
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||||
|
|
||||||
|
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
|
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||||
|
|
||||||
|
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||||
|
|
||||||
|
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||||
|
|
||||||
|
"url-template": ["url-template@2.0.8", "", {}, "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw=="],
|
||||||
|
|
||||||
|
"uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
|
||||||
|
|
||||||
|
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
||||||
|
|
||||||
|
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||||
|
|
||||||
|
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||||
|
|
||||||
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
|
"wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||||
|
|
||||||
|
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
|
|
||||||
|
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
|
"@google-cloud/local-auth/google-auth-library": ["google-auth-library@9.15.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^6.1.1", "gcp-metadata": "^6.1.0", "gtoken": "^7.0.0", "jws": "^4.0.0" } }, "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/server-gdrive/googleapis": ["googleapis@144.0.0", "", { "dependencies": { "google-auth-library": "^9.0.0", "googleapis-common": "^7.0.0" } }, "sha512-ELcWOXtJxjPX4vsKMh+7V+jZvgPwYMlEhQFiu2sa9Qmt5veX8nwXPksOWGGN6Zk4xCiLygUyaz7xGtcMO+Onxw=="],
|
||||||
|
|
||||||
|
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
|
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
|
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
|
"wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
|
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
|
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
|
"@google-cloud/local-auth/google-auth-library/gaxios": ["gaxios@6.7.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9", "uuid": "^9.0.1" } }, "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ=="],
|
||||||
|
|
||||||
|
"@google-cloud/local-auth/google-auth-library/gcp-metadata": ["gcp-metadata@6.1.1", "", { "dependencies": { "gaxios": "^6.1.1", "google-logging-utils": "^0.0.2", "json-bigint": "^1.0.0" } }, "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A=="],
|
||||||
|
|
||||||
|
"@google-cloud/local-auth/google-auth-library/gtoken": ["gtoken@7.1.0", "", { "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" } }, "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/server-gdrive/googleapis/google-auth-library": ["google-auth-library@9.15.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^6.1.1", "gcp-metadata": "^6.1.0", "gtoken": "^7.0.0", "jws": "^4.0.0" } }, "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/server-gdrive/googleapis/googleapis-common": ["googleapis-common@7.2.0", "", { "dependencies": { "extend": "^3.0.2", "gaxios": "^6.0.3", "google-auth-library": "^9.7.0", "qs": "^6.7.0", "url-template": "^2.0.8", "uuid": "^9.0.0" } }, "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA=="],
|
||||||
|
|
||||||
|
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
|
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
|
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
|
"@google-cloud/local-auth/google-auth-library/gaxios/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||||
|
|
||||||
|
"@google-cloud/local-auth/google-auth-library/gcp-metadata/google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/server-gdrive/googleapis/google-auth-library/gaxios": ["gaxios@6.7.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9", "uuid": "^9.0.1" } }, "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/server-gdrive/googleapis/google-auth-library/gcp-metadata": ["gcp-metadata@6.1.1", "", { "dependencies": { "gaxios": "^6.1.1", "google-logging-utils": "^0.0.2", "json-bigint": "^1.0.0" } }, "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/server-gdrive/googleapis/google-auth-library/gtoken": ["gtoken@7.1.0", "", { "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" } }, "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/server-gdrive/googleapis/googleapis-common/gaxios": ["gaxios@6.7.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9", "uuid": "^9.0.1" } }, "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/server-gdrive/googleapis/google-auth-library/gaxios/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/server-gdrive/googleapis/google-auth-library/gcp-metadata/google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/server-gdrive/googleapis/googleapis-common/gaxios/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HbM: RPG v3 - Main CLI Tool
|
||||||
|
*
|
||||||
|
* Unified interface for content creation and management
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { GDriveClient } from './gdrive-client.js';
|
||||||
|
import { ContentManager } from './content-manager.js';
|
||||||
|
import { formatSpellForBook, spellTemplate } from './templates/spell.js';
|
||||||
|
import { formatCreatureForBook, creatureTemplate } from './templates/creature.js';
|
||||||
|
import { formatItemForBook, itemTemplate } from './templates/item.js';
|
||||||
|
import { formatNPCForBook, npcTemplate } from './templates/npc.js';
|
||||||
|
import { formatLocationForBook, locationTemplate } from './templates/location.js';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
import readline from 'readline';
|
||||||
|
|
||||||
|
const BOOKS = {
|
||||||
|
'podrecznik': 'HbM: RPG v3 - Podręcznik Gry',
|
||||||
|
'magia': 'HbM: RPG v3 - Księga Magii',
|
||||||
|
'przewodnik': 'HbM: RPG v3 - Przewodnik Ludzkości po Magicznym Świecie',
|
||||||
|
'bestiariusz': 'HbM: RPG v3 - Bestiariusz',
|
||||||
|
'arcanum': 'HbM: RPG v3 - Arcanum Sanguinis',
|
||||||
|
'kult': 'HbM: RPG v3 - Chwała Szkarłatnemu Kultowi',
|
||||||
|
'klatwa': 'HbM: RPG v3 - Klątwa Otchłani',
|
||||||
|
};
|
||||||
|
|
||||||
|
async function prompt(question) {
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
rl.question(question, answer => {
|
||||||
|
rl.close();
|
||||||
|
resolve(answer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function printHelp() {
|
||||||
|
console.log(`
|
||||||
|
╔═══════════════════════════════════════════════════════════════════╗
|
||||||
|
║ 🎲 HbM: RPG v3 - Content Creation Environment 🎲 ║
|
||||||
|
╚═══════════════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
|
GOOGLE DRIVE COMMANDS:
|
||||||
|
gdrive:auth Authenticate with Google Drive
|
||||||
|
gdrive:list List all HbM books on Drive
|
||||||
|
gdrive:sync Download all books to local cache
|
||||||
|
gdrive:get <book> Get specific book content
|
||||||
|
|
||||||
|
CONTENT COMMANDS:
|
||||||
|
new:spell Create a new spell
|
||||||
|
new:creature Create a new creature/monster
|
||||||
|
new:item Create a new magic item
|
||||||
|
new:npc Create a new NPC
|
||||||
|
new:location Create a new location
|
||||||
|
|
||||||
|
draft <book> <section> Create a new draft for a book section
|
||||||
|
revision <book> Create a revision (copy-paste ready)
|
||||||
|
|
||||||
|
SEARCH & BROWSE:
|
||||||
|
search <query> Search across all downloaded books
|
||||||
|
list <type> List content (spells/creatures/items/npcs/locations)
|
||||||
|
show <type> <name> Show specific content item
|
||||||
|
|
||||||
|
TEMPLATES:
|
||||||
|
template:spell Show spell template structure
|
||||||
|
template:creature Show creature template structure
|
||||||
|
template:item Show item template structure
|
||||||
|
template:npc Show NPC template structure
|
||||||
|
template:location Show location template structure
|
||||||
|
|
||||||
|
BOOK SHORTCUTS:
|
||||||
|
podrecznik → Podręcznik Gry (Game Manual)
|
||||||
|
magia → Księga Magii (Magic Book)
|
||||||
|
przewodnik → Przewodnik Ludzkości (Humanity's Guide)
|
||||||
|
bestiariusz → Bestiariusz (Bestiary)
|
||||||
|
arcanum → Arcanum Sanguinis
|
||||||
|
kult → Chwała Szkarłatnemu Kultowi
|
||||||
|
klatwa → Klątwa Otchłani
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
bun run hbm gdrive:sync
|
||||||
|
bun run hbm search "ognista kula"
|
||||||
|
bun run hbm new:spell
|
||||||
|
bun run hbm draft magia "Nowy Czar"
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const command = args[0];
|
||||||
|
|
||||||
|
if (!command || command === 'help' || command === '--help') {
|
||||||
|
await printHelp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const manager = new ContentManager();
|
||||||
|
await manager.init();
|
||||||
|
|
||||||
|
// Google Drive commands
|
||||||
|
if (command.startsWith('gdrive:')) {
|
||||||
|
const gdrive = new GDriveClient();
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case 'gdrive:auth':
|
||||||
|
const code = args[1];
|
||||||
|
await gdrive.initOAuth();
|
||||||
|
if (code) {
|
||||||
|
await gdrive.saveToken(code);
|
||||||
|
} else {
|
||||||
|
await gdrive.authenticate();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'gdrive:list':
|
||||||
|
await gdrive.init();
|
||||||
|
const books = await gdrive.searchBooks();
|
||||||
|
console.log('\n📚 HbM: RPG v3 Books on Google Drive:\n');
|
||||||
|
books.forEach(b => {
|
||||||
|
const modified = new Date(b.modifiedTime).toLocaleDateString('pl-PL');
|
||||||
|
console.log(` 📖 ${b.name}`);
|
||||||
|
console.log(` Last modified: ${modified}\n`);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'gdrive:sync':
|
||||||
|
await gdrive.init();
|
||||||
|
console.log('\n🔄 Syncing books from Google Drive...\n');
|
||||||
|
const results = await gdrive.downloadAllBooks();
|
||||||
|
console.log(`\n✅ Downloaded ${results.length} books to _books/`);
|
||||||
|
results.forEach(r => console.log(` 📖 ${r.name}`));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'gdrive:get':
|
||||||
|
const pattern = args[1] || '';
|
||||||
|
const bookName = BOOKS[pattern.toLowerCase()] || pattern;
|
||||||
|
await gdrive.init();
|
||||||
|
const book = await gdrive.downloadBook(bookName);
|
||||||
|
console.log(book.text);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// New content commands
|
||||||
|
if (command.startsWith('new:')) {
|
||||||
|
const type = command.split(':')[1];
|
||||||
|
const templates = {
|
||||||
|
spell: { template: spellTemplate, formatter: formatSpellForBook },
|
||||||
|
creature: { template: creatureTemplate, formatter: formatCreatureForBook },
|
||||||
|
item: { template: itemTemplate, formatter: formatItemForBook },
|
||||||
|
npc: { template: npcTemplate, formatter: formatNPCForBook },
|
||||||
|
location: { template: locationTemplate, formatter: formatLocationForBook },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!templates[type]) {
|
||||||
|
console.log(`Unknown type: ${type}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = await prompt(`Enter ${type} name: `);
|
||||||
|
if (!name) {
|
||||||
|
console.log('Name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = { ...templates[type].template, name };
|
||||||
|
const filePath = await manager.saveContent(`${type}s`, name, data);
|
||||||
|
|
||||||
|
console.log(`\n✅ Created ${type}: ${filePath}`);
|
||||||
|
console.log(`\nEdit the JSON file to add details, then run:`);
|
||||||
|
console.log(` npm run hbm show ${type}s "${name}"`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Template commands
|
||||||
|
if (command.startsWith('template:')) {
|
||||||
|
const type = command.split(':')[1];
|
||||||
|
const templates = {
|
||||||
|
spell: spellTemplate,
|
||||||
|
creature: creatureTemplate,
|
||||||
|
item: itemTemplate,
|
||||||
|
npc: npcTemplate,
|
||||||
|
location: locationTemplate,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (templates[type]) {
|
||||||
|
console.log(`\n${type.toUpperCase()} TEMPLATE:\n`);
|
||||||
|
console.log(JSON.stringify(templates[type], null, 2));
|
||||||
|
} else {
|
||||||
|
console.log(`Unknown template: ${type}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other commands
|
||||||
|
switch (command) {
|
||||||
|
case 'search':
|
||||||
|
const query = args.slice(1).join(' ');
|
||||||
|
if (!query) {
|
||||||
|
console.log('Usage: npm run hbm search <query>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const searchResults = await manager.searchBooks(query);
|
||||||
|
console.log(`\n🔍 Found ${searchResults.length} results for "${query}":\n`);
|
||||||
|
searchResults.slice(0, 15).forEach(r => {
|
||||||
|
console.log(`📄 ${r.file}:${r.line}`);
|
||||||
|
console.log(` ${r.text}`);
|
||||||
|
console.log('');
|
||||||
|
});
|
||||||
|
if (searchResults.length > 15) {
|
||||||
|
console.log(`... and ${searchResults.length - 15} more results`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'list':
|
||||||
|
const listType = args[1] || 'spells';
|
||||||
|
const items = await manager.listContent(listType);
|
||||||
|
console.log(`\n📋 ${listType} (${items.length} items):\n`);
|
||||||
|
items.forEach(i => console.log(` • ${i.name}`));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'show':
|
||||||
|
const showType = args[1];
|
||||||
|
const showName = args.slice(2).join(' ');
|
||||||
|
if (!showType || !showName) {
|
||||||
|
console.log('Usage: npm run hbm show <type> <name>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const contentItems = await manager.listContent(showType);
|
||||||
|
const item = contentItems.find(i =>
|
||||||
|
i.name.toLowerCase().includes(showName.toLowerCase())
|
||||||
|
);
|
||||||
|
if (!item) {
|
||||||
|
console.log(`Not found: ${showName}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatters = {
|
||||||
|
spells: formatSpellForBook,
|
||||||
|
creatures: formatCreatureForBook,
|
||||||
|
items: formatItemForBook,
|
||||||
|
npcs: formatNPCForBook,
|
||||||
|
locations: formatLocationForBook,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (formatters[showType]) {
|
||||||
|
console.log('\n' + formatters[showType](item) + '\n');
|
||||||
|
} else {
|
||||||
|
console.log(JSON.stringify(item, null, 2));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'draft':
|
||||||
|
const draftBook = BOOKS[args[1]?.toLowerCase()] || args[1] || 'Unknown';
|
||||||
|
const draftSection = args.slice(2).join(' ') || 'New Section';
|
||||||
|
const draftPath = await manager.createDraft(draftBook, draftSection);
|
||||||
|
console.log(`\n✅ Draft created: ${draftPath}`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'revision':
|
||||||
|
const revBook = BOOKS[args[1]?.toLowerCase()] || args[1] || 'Unknown';
|
||||||
|
const revSection = await prompt('Section title: ');
|
||||||
|
const revOriginal = await prompt('Paste original text (end with empty line):\n');
|
||||||
|
const revNew = await prompt('Paste revised text (end with empty line):\n');
|
||||||
|
const revNotes = await prompt('Notes (optional): ');
|
||||||
|
|
||||||
|
const revPath = await manager.createRevision(revBook, revSection, revOriginal, revNew, revNotes);
|
||||||
|
console.log(`\n✅ Revision created: ${revPath}`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log(`Unknown command: ${command}`);
|
||||||
|
console.log('Run "npm run hbm help" for available commands');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
/**
|
||||||
|
* HbM: RPG v3 - Content Manager
|
||||||
|
*
|
||||||
|
* Tools for creating, editing, and organizing TT-RPG content
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
// Content lives directly at the repo root (Obsidian-vault-friendly layout).
|
||||||
|
// Scripts live in .src/, so go up one level to reach the vault root.
|
||||||
|
const CONTENT_DIR = path.resolve(import.meta.dirname, '..');
|
||||||
|
const DRAFTS_DIR = path.join(CONTENT_DIR, '_drafts');
|
||||||
|
const REVISIONS_DIR = path.join(CONTENT_DIR, 'revisions');
|
||||||
|
const TEMPLATES_DIR = path.join(CONTENT_DIR, '_templates');
|
||||||
|
|
||||||
|
export class ContentManager {
|
||||||
|
constructor() {
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
// Create directory structure (folders live at repo root)
|
||||||
|
await fs.mkdir(DRAFTS_DIR, { recursive: true });
|
||||||
|
await fs.mkdir(REVISIONS_DIR, { recursive: true });
|
||||||
|
await fs.mkdir(path.join(CONTENT_DIR, 'spells'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(CONTENT_DIR, 'creatures'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(CONTENT_DIR, 'items'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(CONTENT_DIR, 'locations'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(CONTENT_DIR, 'npcs'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(CONTENT_DIR, 'rules'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(CONTENT_DIR, 'adventures'), { recursive: true });
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new draft for a specific book
|
||||||
|
*/
|
||||||
|
async createDraft(bookName, sectionTitle, content = '') {
|
||||||
|
const timestamp = new Date().toISOString().split('T')[0];
|
||||||
|
const safeName = sectionTitle.replace(/[^a-zA-Z0-9ąćęłńóśźżĄĆĘŁŃÓŚŹŻ\-\s]/g, '').trim();
|
||||||
|
const fileName = `${timestamp}_${safeName}.md`;
|
||||||
|
|
||||||
|
const draft = `---
|
||||||
|
book: "${bookName}"
|
||||||
|
section: "${sectionTitle}"
|
||||||
|
created: "${new Date().toISOString()}"
|
||||||
|
status: draft
|
||||||
|
---
|
||||||
|
|
||||||
|
# ${sectionTitle}
|
||||||
|
|
||||||
|
${content}
|
||||||
|
|
||||||
|
---
|
||||||
|
<!-- REVISION NOTES -->
|
||||||
|
<!-- Add notes about changes here -->
|
||||||
|
`;
|
||||||
|
|
||||||
|
const filePath = path.join(DRAFTS_DIR, fileName);
|
||||||
|
await fs.writeFile(filePath, draft);
|
||||||
|
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a revision (ready to copy-paste version)
|
||||||
|
*/
|
||||||
|
async createRevision(bookName, sectionTitle, originalText, revisedText, notes = '') {
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const safeName = sectionTitle.replace(/[^a-zA-Z0-9ąćęłńóśźżĄĆĘŁŃÓŚŹŻ\-\s]/g, '').trim();
|
||||||
|
const fileName = `${safeName}_rev_${timestamp}.md`;
|
||||||
|
|
||||||
|
const revision = `---
|
||||||
|
book: "${bookName}"
|
||||||
|
section: "${sectionTitle}"
|
||||||
|
created: "${new Date().toISOString()}"
|
||||||
|
type: revision
|
||||||
|
---
|
||||||
|
|
||||||
|
# Revision: ${sectionTitle}
|
||||||
|
|
||||||
|
**Book:** ${bookName}
|
||||||
|
**Created:** ${new Date().toLocaleString('pl-PL')}
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
${notes || '_No notes provided_'}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 COPY-PASTE READY TEXT
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
${revisedText}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Original Text (for reference)
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Click to expand original</summary>
|
||||||
|
|
||||||
|
${originalText}
|
||||||
|
|
||||||
|
</details>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const filePath = path.join(REVISIONS_DIR, fileName);
|
||||||
|
await fs.writeFile(filePath, revision);
|
||||||
|
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save content by type (spell, creature, etc.)
|
||||||
|
*/
|
||||||
|
async saveContent(type, name, data) {
|
||||||
|
const typeDir = path.join(CONTENT_DIR, type);
|
||||||
|
const safeName = name.replace(/[^a-zA-Z0-9ąćęłńóśźżĄĆĘŁŃÓŚŹŻ\-\s]/g, '').trim();
|
||||||
|
const filePath = path.join(typeDir, `${safeName}.json`);
|
||||||
|
|
||||||
|
const content = {
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
created: new Date().toISOString(),
|
||||||
|
modified: new Date().toISOString(),
|
||||||
|
...data
|
||||||
|
};
|
||||||
|
|
||||||
|
await fs.writeFile(filePath, JSON.stringify(content, null, 2));
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all content of a type
|
||||||
|
*/
|
||||||
|
async listContent(type) {
|
||||||
|
const typeDir = path.join(CONTENT_DIR, type);
|
||||||
|
try {
|
||||||
|
const files = await fs.readdir(typeDir);
|
||||||
|
const contents = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.endsWith('.json')) {
|
||||||
|
const data = JSON.parse(await fs.readFile(path.join(typeDir, file), 'utf-8'));
|
||||||
|
contents.push(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return contents;
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search across all downloaded books
|
||||||
|
*/
|
||||||
|
async searchBooks(query, booksDir = '_books') {
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const files = await fs.readdir(booksDir);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (!file.endsWith('.md')) continue;
|
||||||
|
|
||||||
|
const content = await fs.readFile(path.join(booksDir, file), 'utf-8');
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
if (line.toLowerCase().includes(query.toLowerCase())) {
|
||||||
|
results.push({
|
||||||
|
file,
|
||||||
|
line: index + 1,
|
||||||
|
text: line.trim(),
|
||||||
|
context: lines.slice(Math.max(0, index - 2), index + 3).join('\n')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Search error:', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a section from a book
|
||||||
|
*/
|
||||||
|
async extractSection(bookPath, startPattern, endPattern = null) {
|
||||||
|
const content = await fs.readFile(bookPath, 'utf-8');
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
let capturing = false;
|
||||||
|
let section = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!capturing && line.includes(startPattern)) {
|
||||||
|
capturing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (capturing) {
|
||||||
|
if (endPattern && line.includes(endPattern)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
section.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return section.join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLI handler
|
||||||
|
if (process.argv[1].endsWith('content-manager.js')) {
|
||||||
|
const manager = new ContentManager();
|
||||||
|
await manager.init();
|
||||||
|
|
||||||
|
const command = process.argv[2];
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case 'init':
|
||||||
|
console.log('✅ Content directories initialized!');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'draft':
|
||||||
|
const book = process.argv[3] || 'Unknown';
|
||||||
|
const section = process.argv[4] || 'New Section';
|
||||||
|
const draftPath = await manager.createDraft(book, section);
|
||||||
|
console.log(`✅ Draft created: ${draftPath}`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'search':
|
||||||
|
const query = process.argv[3];
|
||||||
|
if (!query) {
|
||||||
|
console.log('Usage: node src/content-manager.js search <query>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const results = await manager.searchBooks(query);
|
||||||
|
console.log(`\n🔍 Found ${results.length} results for "${query}":\n`);
|
||||||
|
results.slice(0, 10).forEach(r => {
|
||||||
|
console.log(`📄 ${r.file}:${r.line}`);
|
||||||
|
console.log(` ${r.text}\n`);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'list':
|
||||||
|
const type = process.argv[3] || 'spells';
|
||||||
|
const items = await manager.listContent(type);
|
||||||
|
console.log(`\n📋 ${type} (${items.length} items):\n`);
|
||||||
|
items.forEach(i => console.log(` - ${i.name}`));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log(`
|
||||||
|
HbM: RPG v3 - Content Manager
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
init - Initialize content directories
|
||||||
|
draft <book> <section> - Create a new draft
|
||||||
|
search <query> - Search across downloaded books
|
||||||
|
list <type> - List content by type (spells, creatures, etc.)
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContentManager;
|
||||||
Submodule
+1
Submodule Foundry-Data/foundry-lore added at 447b90dbb7
Submodule
+1
Submodule Foundry-Data/foundry-system added at 0fe9dd02d6
@@ -0,0 +1,261 @@
|
|||||||
|
/**
|
||||||
|
* HbM: RPG v3 - Google Drive Client
|
||||||
|
*
|
||||||
|
* Downloads and processes books from Google Drive for content creation
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { google } from 'googleapis';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const SCOPES = [
|
||||||
|
'https://www.googleapis.com/auth/drive.readonly',
|
||||||
|
'https://www.googleapis.com/auth/documents.readonly'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Root of the vault (one level above .src/)
|
||||||
|
const ROOT_DIR = path.resolve(import.meta.dirname, '..');
|
||||||
|
|
||||||
|
const TOKEN_PATH = path.join(ROOT_DIR, 'gdrive-token.json');
|
||||||
|
const CREDENTIALS_PATH = path.join(import.meta.dirname, 'client_secret_837588858675-hp2m36p432m755aeuudtigo1225388i7.apps.googleusercontent.com.json');
|
||||||
|
|
||||||
|
// Book IDs will be stored here after first search
|
||||||
|
const BOOKS_CACHE_PATH = path.join(import.meta.dirname, 'data', 'books-cache.json');
|
||||||
|
|
||||||
|
export class GDriveClient {
|
||||||
|
constructor() {
|
||||||
|
this.auth = null;
|
||||||
|
this.drive = null;
|
||||||
|
this.docs = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize OAuth client only (without requiring token)
|
||||||
|
*/
|
||||||
|
async initOAuth() {
|
||||||
|
const credentials = JSON.parse(await fs.readFile(CREDENTIALS_PATH, 'utf-8'));
|
||||||
|
const { client_id, client_secret, redirect_uris } = credentials.installed || credentials.web;
|
||||||
|
|
||||||
|
this.auth = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await this.initOAuth();
|
||||||
|
|
||||||
|
// Try to load existing token
|
||||||
|
try {
|
||||||
|
const token = JSON.parse(await fs.readFile(TOKEN_PATH, 'utf-8'));
|
||||||
|
this.auth.setCredentials(token);
|
||||||
|
} catch (err) {
|
||||||
|
await this.authenticate();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.drive = google.drive({ version: 'v3', auth: this.auth });
|
||||||
|
this.docs = google.docs({ version: 'v1', auth: this.auth });
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async authenticate() {
|
||||||
|
const authUrl = this.auth.generateAuthUrl({
|
||||||
|
access_type: 'offline',
|
||||||
|
scope: SCOPES,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('\n🔐 Authorization required!');
|
||||||
|
console.log('Please visit this URL to authorize:\n');
|
||||||
|
console.log(authUrl);
|
||||||
|
console.log('\nThen run: node src/gdrive-client.js auth <CODE>\n');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveToken(code) {
|
||||||
|
const { tokens } = await this.auth.getToken(code);
|
||||||
|
this.auth.setCredentials(tokens);
|
||||||
|
await fs.writeFile(TOKEN_PATH, JSON.stringify(tokens, null, 2));
|
||||||
|
console.log('✅ Token saved successfully!');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for all HbM: RPG v3 books
|
||||||
|
*/
|
||||||
|
async searchBooks() {
|
||||||
|
const query = "name contains 'HbM: RPG v3' and mimeType = 'application/vnd.google-apps.document' and trashed = false";
|
||||||
|
|
||||||
|
const res = await this.drive.files.list({
|
||||||
|
q: query,
|
||||||
|
fields: 'files(id, name, modifiedTime, webViewLink, ownedByMe)',
|
||||||
|
orderBy: 'name',
|
||||||
|
includeItemsFromAllDrives: false,
|
||||||
|
supportsAllDrives: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.data.files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get document content as plain text
|
||||||
|
*/
|
||||||
|
async getDocumentAsText(fileId) {
|
||||||
|
const res = await this.drive.files.export({
|
||||||
|
fileId: fileId,
|
||||||
|
mimeType: 'text/plain',
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get document content as HTML (preserves some formatting)
|
||||||
|
*/
|
||||||
|
async getDocumentAsHtml(fileId) {
|
||||||
|
const res = await this.drive.files.export({
|
||||||
|
fileId: fileId,
|
||||||
|
mimeType: 'text/html',
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get document structure using Docs API (best for structured editing)
|
||||||
|
*/
|
||||||
|
async getDocumentStructure(documentId) {
|
||||||
|
const res = await this.docs.documents.get({
|
||||||
|
documentId: documentId,
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download a specific book by name pattern
|
||||||
|
*/
|
||||||
|
async downloadBook(namePattern) {
|
||||||
|
const books = await this.searchBooks();
|
||||||
|
const book = books.find(b => b.name.toLowerCase().includes(namePattern.toLowerCase()));
|
||||||
|
|
||||||
|
if (!book) {
|
||||||
|
throw new Error(`Book not found: ${namePattern}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📖 Downloading: ${book.name}`);
|
||||||
|
|
||||||
|
const text = await this.getDocumentAsText(book.id);
|
||||||
|
const html = await this.getDocumentAsHtml(book.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: book.id,
|
||||||
|
name: book.name,
|
||||||
|
modifiedTime: book.modifiedTime,
|
||||||
|
text,
|
||||||
|
html,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download all books and save to local cache
|
||||||
|
*/
|
||||||
|
async downloadAllBooks(outputDir = '_books') {
|
||||||
|
await fs.mkdir(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
const books = await this.searchBooks();
|
||||||
|
|
||||||
|
// Deduplicate by name (keep the most recently modified)
|
||||||
|
const uniqueBooks = new Map();
|
||||||
|
for (const book of books) {
|
||||||
|
const existing = uniqueBooks.get(book.name);
|
||||||
|
if (!existing || new Date(book.modifiedTime) > new Date(existing.modifiedTime)) {
|
||||||
|
uniqueBooks.set(book.name, book);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (const book of uniqueBooks.values()) {
|
||||||
|
// Skip character sheets
|
||||||
|
if (book.name.includes('KP v')) continue;
|
||||||
|
|
||||||
|
console.log(`📖 Downloading: ${book.name}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const text = await this.getDocumentAsText(book.id);
|
||||||
|
const safeName = book.name.replace(/[^a-zA-Z0-9ąćęłńóśźżĄĆĘŁŃÓŚŹŻ\-\s]/g, '').trim();
|
||||||
|
|
||||||
|
// Save as markdown
|
||||||
|
const mdPath = path.join(outputDir, `${safeName}.md`);
|
||||||
|
await fs.writeFile(mdPath, `# ${book.name}\n\n_Last synced: ${new Date().toISOString()}_\n\n---\n\n${text}`);
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
id: book.id,
|
||||||
|
name: book.name,
|
||||||
|
localPath: mdPath,
|
||||||
|
modifiedTime: book.modifiedTime,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`❌ Failed to download ${book.name}:`, err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save cache
|
||||||
|
await fs.mkdir('data', { recursive: true });
|
||||||
|
await fs.writeFile(BOOKS_CACHE_PATH, JSON.stringify(results, null, 2));
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLI handler
|
||||||
|
if (process.argv[1].endsWith('gdrive-client.js')) {
|
||||||
|
const client = new GDriveClient();
|
||||||
|
const command = process.argv[2];
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case 'auth':
|
||||||
|
const code = process.argv[3];
|
||||||
|
await client.initOAuth();
|
||||||
|
if (!code) {
|
||||||
|
// No code provided - show auth URL
|
||||||
|
await client.authenticate();
|
||||||
|
} else {
|
||||||
|
// Code provided - save token
|
||||||
|
await client.saveToken(code);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'list':
|
||||||
|
await client.init();
|
||||||
|
const books = await client.searchBooks();
|
||||||
|
console.log('\n📚 HbM: RPG v3 Books:\n');
|
||||||
|
books.forEach(b => console.log(` - ${b.name}`));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'download':
|
||||||
|
await client.init();
|
||||||
|
const results = await client.downloadAllBooks();
|
||||||
|
console.log(`\n✅ Downloaded ${results.length} books to _books/`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get':
|
||||||
|
const pattern = process.argv[3];
|
||||||
|
if (!pattern) {
|
||||||
|
console.log('Usage: node src/gdrive-client.js get <name-pattern>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
await client.init();
|
||||||
|
const book = await client.downloadBook(pattern);
|
||||||
|
console.log(book.text);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log(`
|
||||||
|
HbM: RPG v3 - Google Drive Client
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
auth <code> - Authenticate with Google (follow OAuth flow)
|
||||||
|
list - List all HbM books on Google Drive
|
||||||
|
download - Download all books to _books/
|
||||||
|
get <pattern> - Get a specific book by name pattern
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GDriveClient;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "hbm-rpg-content-tools",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Content creation tools for Homebrew Magic: Role Playing Game v3",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"hbm": "bun cli.js",
|
||||||
|
"gdrive:auth": "bun gdrive-client.js auth",
|
||||||
|
"gdrive:list": "bun gdrive-client.js list",
|
||||||
|
"gdrive:sync": "bun gdrive-client.js download",
|
||||||
|
"content:init": "bun content-manager.js init",
|
||||||
|
"search": "bun content-manager.js search"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"hbm": "./cli.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/server-gdrive": "^2025.1.14",
|
||||||
|
"googleapis": "^170.0.0"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"ttrpg",
|
||||||
|
"homebrew",
|
||||||
|
"rpg",
|
||||||
|
"content-creation"
|
||||||
|
],
|
||||||
|
"license": "PRIVATE"
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
/**
|
||||||
|
* HbM: RPG v3 - Creature/Monster Template
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const creatureTemplate = {
|
||||||
|
name: '',
|
||||||
|
nameEN: '',
|
||||||
|
type: '', // Typ (np. Humanoid, Bestia, Nieumarły)
|
||||||
|
size: '', // Rozmiar
|
||||||
|
alignment: '', // Charakter
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
stats: {
|
||||||
|
hp: 0,
|
||||||
|
hpFormula: '',
|
||||||
|
ac: 0,
|
||||||
|
acType: '',
|
||||||
|
speed: '',
|
||||||
|
|
||||||
|
// Attributes
|
||||||
|
str: 10,
|
||||||
|
dex: 10,
|
||||||
|
con: 10,
|
||||||
|
int: 10,
|
||||||
|
wis: 10,
|
||||||
|
cha: 10,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Combat
|
||||||
|
combat: {
|
||||||
|
attacks: [],
|
||||||
|
specialAbilities: [],
|
||||||
|
reactions: [],
|
||||||
|
legendaryActions: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Defenses
|
||||||
|
defenses: {
|
||||||
|
savingThrows: [],
|
||||||
|
skills: [],
|
||||||
|
damageResistances: [],
|
||||||
|
damageImmunities: [],
|
||||||
|
conditionImmunities: [],
|
||||||
|
senses: '',
|
||||||
|
languages: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Challenge
|
||||||
|
challenge: {
|
||||||
|
cr: 0,
|
||||||
|
xp: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Lore
|
||||||
|
lore: {
|
||||||
|
description: '',
|
||||||
|
habitat: '',
|
||||||
|
behavior: '',
|
||||||
|
history: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
notes: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
function getModifier(score) {
|
||||||
|
return Math.floor((score - 10) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatModifier(mod) {
|
||||||
|
return mod >= 0 ? `+${mod}` : `${mod}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatCreatureForBook(creature) {
|
||||||
|
const stats = creature.stats;
|
||||||
|
const mods = {
|
||||||
|
str: formatModifier(getModifier(stats.str)),
|
||||||
|
dex: formatModifier(getModifier(stats.dex)),
|
||||||
|
con: formatModifier(getModifier(stats.con)),
|
||||||
|
int: formatModifier(getModifier(stats.int)),
|
||||||
|
wis: formatModifier(getModifier(stats.wis)),
|
||||||
|
cha: formatModifier(getModifier(stats.cha)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let output = `## ${creature.name}
|
||||||
|
*${creature.size} ${creature.type}, ${creature.alignment}*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Klasa Pancerza:** ${stats.ac}${stats.acType ? ` (${stats.acType})` : ''}
|
||||||
|
**Punkty Wytrzymałości:** ${stats.hp}${stats.hpFormula ? ` (${stats.hpFormula})` : ''}
|
||||||
|
**Szybkość:** ${stats.speed}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
| SIŁ | ZRE | KON | INT | MDR | CHA |
|
||||||
|
|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||||
|
| ${stats.str} (${mods.str}) | ${stats.dex} (${mods.dex}) | ${stats.con} (${mods.con}) | ${stats.int} (${mods.int}) | ${stats.wis} (${mods.wis}) | ${stats.cha} (${mods.cha}) |
|
||||||
|
|
||||||
|
---
|
||||||
|
`;
|
||||||
|
|
||||||
|
const def = creature.defenses;
|
||||||
|
if (def.savingThrows.length) output += `**Rzuty Obronne:** ${def.savingThrows.join(', ')}\n`;
|
||||||
|
if (def.skills.length) output += `**Umiejętności:** ${def.skills.join(', ')}\n`;
|
||||||
|
if (def.damageResistances.length) output += `**Odporności na Obrażenia:** ${def.damageResistances.join(', ')}\n`;
|
||||||
|
if (def.damageImmunities.length) output += `**Niewrażliwości na Obrażenia:** ${def.damageImmunities.join(', ')}\n`;
|
||||||
|
if (def.conditionImmunities.length) output += `**Niewrażliwości na Stany:** ${def.conditionImmunities.join(', ')}\n`;
|
||||||
|
if (def.senses) output += `**Zmysły:** ${def.senses}\n`;
|
||||||
|
if (def.languages) output += `**Języki:** ${def.languages}\n`;
|
||||||
|
output += `**Poziom Wyzwania:** ${creature.challenge.cr} (${creature.challenge.xp} PD)\n`;
|
||||||
|
|
||||||
|
output += '\n---\n\n';
|
||||||
|
|
||||||
|
// Special Abilities
|
||||||
|
if (creature.combat.specialAbilities.length) {
|
||||||
|
creature.combat.specialAbilities.forEach(ability => {
|
||||||
|
output += `***${ability.name}.*** ${ability.description}\n\n`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
if (creature.combat.attacks.length) {
|
||||||
|
output += '### Akcje\n\n';
|
||||||
|
creature.combat.attacks.forEach(attack => {
|
||||||
|
output += `***${attack.name}.*** ${attack.description}\n\n`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reactions
|
||||||
|
if (creature.combat.reactions.length) {
|
||||||
|
output += '### Reakcje\n\n';
|
||||||
|
creature.combat.reactions.forEach(reaction => {
|
||||||
|
output += `***${reaction.name}.*** ${reaction.description}\n\n`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legendary Actions
|
||||||
|
if (creature.combat.legendaryActions.length) {
|
||||||
|
output += '### Akcje Legendarne\n\n';
|
||||||
|
creature.combat.legendaryActions.forEach(action => {
|
||||||
|
output += `***${action.name}.*** ${action.description}\n\n`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lore
|
||||||
|
if (creature.lore.description) {
|
||||||
|
output += '---\n\n';
|
||||||
|
output += creature.lore.description + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { creatureTemplate, formatCreatureForBook };
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
/**
|
||||||
|
* HbM: RPG v3 - Item Template
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const itemTemplate = {
|
||||||
|
name: '',
|
||||||
|
nameEN: '',
|
||||||
|
type: '', // Weapon, Armor, Potion, Wondrous, etc.
|
||||||
|
rarity: '', // Common, Uncommon, Rare, Very Rare, Legendary, Artifact
|
||||||
|
attunement: false,
|
||||||
|
attunementReq: '', // e.g., "by a spellcaster"
|
||||||
|
|
||||||
|
// For weapons
|
||||||
|
weapon: {
|
||||||
|
damage: '',
|
||||||
|
damageType: '',
|
||||||
|
properties: [], // e.g., ["Finesse", "Light", "Thrown (20/60)"]
|
||||||
|
},
|
||||||
|
|
||||||
|
// For armor
|
||||||
|
armor: {
|
||||||
|
ac: 0,
|
||||||
|
acBonus: 0,
|
||||||
|
type: '', // Light, Medium, Heavy, Shield
|
||||||
|
stealthDisadv: false,
|
||||||
|
strRequirement: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
description: '',
|
||||||
|
properties: [], // Special properties/abilities
|
||||||
|
history: '', // Item lore/backstory
|
||||||
|
|
||||||
|
// Pricing
|
||||||
|
value: '',
|
||||||
|
weight: '',
|
||||||
|
|
||||||
|
notes: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const rarityPL = {
|
||||||
|
'Common': 'Pospolity',
|
||||||
|
'Uncommon': 'Niepospolity',
|
||||||
|
'Rare': 'Rzadki',
|
||||||
|
'Very Rare': 'Bardzo Rzadki',
|
||||||
|
'Legendary': 'Legendarny',
|
||||||
|
'Artifact': 'Artefakt',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function formatItemForBook(item) {
|
||||||
|
const rarity = rarityPL[item.rarity] || item.rarity;
|
||||||
|
let typeStr = item.type;
|
||||||
|
|
||||||
|
if (item.weapon.damage) {
|
||||||
|
typeStr = `Broń (${item.weapon.properties.join(', ')})`;
|
||||||
|
} else if (item.armor.type) {
|
||||||
|
typeStr = `Zbroja (${item.armor.type})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let attunement = '';
|
||||||
|
if (item.attunement) {
|
||||||
|
attunement = item.attunementReq
|
||||||
|
? ` (wymaga dostrojenia ${item.attunementReq})`
|
||||||
|
: ' (wymaga dostrojenia)';
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = `### ${item.name}
|
||||||
|
*${typeStr}, ${rarity}${attunement}*
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Weapon stats
|
||||||
|
if (item.weapon.damage) {
|
||||||
|
output += `**Obrażenia:** ${item.weapon.damage} ${item.weapon.damageType}\n`;
|
||||||
|
if (item.weapon.properties.length) {
|
||||||
|
output += `**Właściwości:** ${item.weapon.properties.join(', ')}\n`;
|
||||||
|
}
|
||||||
|
output += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Armor stats
|
||||||
|
if (item.armor.type) {
|
||||||
|
if (item.armor.ac) {
|
||||||
|
output += `**KP:** ${item.armor.ac}`;
|
||||||
|
} else if (item.armor.acBonus) {
|
||||||
|
output += `**Bonus KP:** +${item.armor.acBonus}`;
|
||||||
|
}
|
||||||
|
if (item.armor.stealthDisadv) output += ` (utrudnia skradanie)`;
|
||||||
|
if (item.armor.strRequirement) output += ` (wymaga SIŁ ${item.armor.strRequirement})`;
|
||||||
|
output += '\n\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
output += item.description + '\n';
|
||||||
|
|
||||||
|
// Special properties
|
||||||
|
if (item.properties.length) {
|
||||||
|
output += '\n';
|
||||||
|
item.properties.forEach(prop => {
|
||||||
|
output += `• **${prop.name}:** ${prop.description}\n`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value and weight
|
||||||
|
if (item.value || item.weight) {
|
||||||
|
output += '\n---\n';
|
||||||
|
if (item.value) output += `*Wartość: ${item.value}* `;
|
||||||
|
if (item.weight) output += `*Waga: ${item.weight}*`;
|
||||||
|
output += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lore/History
|
||||||
|
if (item.history) {
|
||||||
|
output += `\n*${item.history}*\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { itemTemplate, formatItemForBook };
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
/**
|
||||||
|
* HbM: RPG v3 - Location Template
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const locationTemplate = {
|
||||||
|
name: '',
|
||||||
|
nameEN: '',
|
||||||
|
type: '', // City, Village, Dungeon, Forest, etc.
|
||||||
|
region: '',
|
||||||
|
|
||||||
|
// Basic info
|
||||||
|
population: '',
|
||||||
|
government: '',
|
||||||
|
economy: '',
|
||||||
|
|
||||||
|
// Description
|
||||||
|
description: {
|
||||||
|
overview: '',
|
||||||
|
atmosphere: '', // Mood, feeling, sounds, smells
|
||||||
|
history: '',
|
||||||
|
secrets: '', // Hidden info for GM
|
||||||
|
},
|
||||||
|
|
||||||
|
// Points of Interest
|
||||||
|
landmarks: [], // { name, description }
|
||||||
|
|
||||||
|
// Inhabitants
|
||||||
|
notableNPCs: [], // References or brief descriptions
|
||||||
|
factions: [], // { name, description, goals }
|
||||||
|
|
||||||
|
// For adventures
|
||||||
|
encounters: [], // Possible random encounters
|
||||||
|
hooks: [], // Adventure hooks
|
||||||
|
rumors: [], // Things NPCs might say
|
||||||
|
|
||||||
|
// For dungeons/adventure sites
|
||||||
|
rooms: [], // { number, name, description, contents }
|
||||||
|
|
||||||
|
// Maps
|
||||||
|
mapDescription: '', // Text description if no map available
|
||||||
|
|
||||||
|
notes: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function formatLocationForBook(location) {
|
||||||
|
let output = `## ${location.name}\n`;
|
||||||
|
if (location.type || location.region) {
|
||||||
|
output += `*${[location.type, location.region].filter(Boolean).join(', ')}*\n`;
|
||||||
|
}
|
||||||
|
output += '\n';
|
||||||
|
|
||||||
|
// Overview
|
||||||
|
if (location.description.overview) {
|
||||||
|
output += location.description.overview + '\n\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic info
|
||||||
|
const info = [];
|
||||||
|
if (location.population) info.push(`**Populacja:** ${location.population}`);
|
||||||
|
if (location.government) info.push(`**Władza:** ${location.government}`);
|
||||||
|
if (location.economy) info.push(`**Gospodarka:** ${location.economy}`);
|
||||||
|
if (info.length) {
|
||||||
|
output += info.join(' | ') + '\n\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atmosphere
|
||||||
|
if (location.description.atmosphere) {
|
||||||
|
output += '### Atmosfera\n\n';
|
||||||
|
output += location.description.atmosphere + '\n\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Landmarks
|
||||||
|
if (location.landmarks.length) {
|
||||||
|
output += '### Ważne Miejsca\n\n';
|
||||||
|
location.landmarks.forEach(landmark => {
|
||||||
|
output += `#### ${landmark.name}\n`;
|
||||||
|
output += landmark.description + '\n\n';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notable NPCs
|
||||||
|
if (location.notableNPCs.length) {
|
||||||
|
output += '### Ważne Postacie\n\n';
|
||||||
|
location.notableNPCs.forEach(npc => {
|
||||||
|
if (typeof npc === 'string') {
|
||||||
|
output += `• ${npc}\n`;
|
||||||
|
} else {
|
||||||
|
output += `• **${npc.name}** - ${npc.description}\n`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
output += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Factions
|
||||||
|
if (location.factions.length) {
|
||||||
|
output += '### Frakcje\n\n';
|
||||||
|
location.factions.forEach(faction => {
|
||||||
|
output += `#### ${faction.name}\n`;
|
||||||
|
output += faction.description + '\n';
|
||||||
|
if (faction.goals) output += `*Cele: ${faction.goals}*\n`;
|
||||||
|
output += '\n';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// History
|
||||||
|
if (location.description.history) {
|
||||||
|
output += '### Historia\n\n';
|
||||||
|
output += location.description.history + '\n\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rumors
|
||||||
|
if (location.rumors.length) {
|
||||||
|
output += '### Plotki\n\n';
|
||||||
|
location.rumors.forEach((rumor, i) => {
|
||||||
|
output += `${i + 1}. ${rumor}\n`;
|
||||||
|
});
|
||||||
|
output += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adventure Hooks
|
||||||
|
if (location.hooks.length) {
|
||||||
|
output += '### Zaczepki Przygodowe\n\n';
|
||||||
|
location.hooks.forEach(hook => {
|
||||||
|
output += `• ${hook}\n`;
|
||||||
|
});
|
||||||
|
output += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encounters
|
||||||
|
if (location.encounters.length) {
|
||||||
|
output += '### Możliwe Spotkania\n\n';
|
||||||
|
output += '| k20 | Spotkanie |\n';
|
||||||
|
output += '|:---:|:----------|\n';
|
||||||
|
location.encounters.forEach((enc, i) => {
|
||||||
|
output += `| ${i + 1} | ${enc} |\n`;
|
||||||
|
});
|
||||||
|
output += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dungeon Rooms
|
||||||
|
if (location.rooms.length) {
|
||||||
|
output += '---\n\n## Pomieszczenia\n\n';
|
||||||
|
location.rooms.forEach(room => {
|
||||||
|
output += `### ${room.number}. ${room.name}\n\n`;
|
||||||
|
output += room.description + '\n';
|
||||||
|
if (room.contents) {
|
||||||
|
output += `\n**Zawartość:** ${room.contents}\n`;
|
||||||
|
}
|
||||||
|
output += '\n';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// GM Secrets
|
||||||
|
if (location.description.secrets) {
|
||||||
|
output += '---\n\n';
|
||||||
|
output += '> **[DLA MG]** ' + location.description.secrets + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { locationTemplate, formatLocationForBook };
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
/**
|
||||||
|
* HbM: RPG v3 - NPC Template
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const npcTemplate = {
|
||||||
|
name: '',
|
||||||
|
title: '', // e.g., "The Blacksmith", "High Priest"
|
||||||
|
race: '',
|
||||||
|
gender: '',
|
||||||
|
age: '',
|
||||||
|
occupation: '',
|
||||||
|
location: '',
|
||||||
|
|
||||||
|
// Appearance
|
||||||
|
appearance: {
|
||||||
|
height: '',
|
||||||
|
build: '',
|
||||||
|
hair: '',
|
||||||
|
eyes: '',
|
||||||
|
distinguishing: '', // Scars, tattoos, etc.
|
||||||
|
clothing: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Personality
|
||||||
|
personality: {
|
||||||
|
traits: [],
|
||||||
|
ideals: [],
|
||||||
|
bonds: [],
|
||||||
|
flaws: [],
|
||||||
|
mannerisms: '',
|
||||||
|
voice: '', // How they speak
|
||||||
|
},
|
||||||
|
|
||||||
|
// Background
|
||||||
|
background: {
|
||||||
|
history: '',
|
||||||
|
secrets: '',
|
||||||
|
goals: '',
|
||||||
|
fears: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
relationships: [], // { name, relation, notes }
|
||||||
|
|
||||||
|
// For combat NPCs
|
||||||
|
statBlock: null, // Reference to creature template if needed
|
||||||
|
|
||||||
|
// Roleplay hooks
|
||||||
|
hooks: [], // Quest hooks, rumors, etc.
|
||||||
|
|
||||||
|
notes: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function formatNPCForBook(npc) {
|
||||||
|
let output = `## ${npc.name}`;
|
||||||
|
if (npc.title) output += `, ${npc.title}`;
|
||||||
|
output += '\n\n';
|
||||||
|
|
||||||
|
// Basic info
|
||||||
|
const basicInfo = [npc.race, npc.gender, npc.age, npc.occupation].filter(Boolean);
|
||||||
|
if (basicInfo.length) {
|
||||||
|
output += `*${basicInfo.join(', ')}*\n`;
|
||||||
|
}
|
||||||
|
if (npc.location) {
|
||||||
|
output += `**Lokacja:** ${npc.location}\n`;
|
||||||
|
}
|
||||||
|
output += '\n';
|
||||||
|
|
||||||
|
// Appearance
|
||||||
|
if (npc.appearance.distinguishing || npc.appearance.clothing) {
|
||||||
|
output += '### Wygląd\n\n';
|
||||||
|
const appearanceDetails = [];
|
||||||
|
if (npc.appearance.height) appearanceDetails.push(npc.appearance.height);
|
||||||
|
if (npc.appearance.build) appearanceDetails.push(npc.appearance.build);
|
||||||
|
if (npc.appearance.hair) appearanceDetails.push(`włosy: ${npc.appearance.hair}`);
|
||||||
|
if (npc.appearance.eyes) appearanceDetails.push(`oczy: ${npc.appearance.eyes}`);
|
||||||
|
|
||||||
|
if (appearanceDetails.length) {
|
||||||
|
output += appearanceDetails.join(', ') + '.\n\n';
|
||||||
|
}
|
||||||
|
if (npc.appearance.distinguishing) {
|
||||||
|
output += `**Znaki szczególne:** ${npc.appearance.distinguishing}\n`;
|
||||||
|
}
|
||||||
|
if (npc.appearance.clothing) {
|
||||||
|
output += `**Ubiór:** ${npc.appearance.clothing}\n`;
|
||||||
|
}
|
||||||
|
output += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Personality
|
||||||
|
output += '### Osobowość\n\n';
|
||||||
|
if (npc.personality.traits.length) {
|
||||||
|
output += `**Cechy:** ${npc.personality.traits.join(', ')}\n`;
|
||||||
|
}
|
||||||
|
if (npc.personality.ideals.length) {
|
||||||
|
output += `**Ideały:** ${npc.personality.ideals.join(', ')}\n`;
|
||||||
|
}
|
||||||
|
if (npc.personality.bonds.length) {
|
||||||
|
output += `**Więzi:** ${npc.personality.bonds.join(', ')}\n`;
|
||||||
|
}
|
||||||
|
if (npc.personality.flaws.length) {
|
||||||
|
output += `**Wady:** ${npc.personality.flaws.join(', ')}\n`;
|
||||||
|
}
|
||||||
|
if (npc.personality.mannerisms) {
|
||||||
|
output += `**Maniery:** ${npc.personality.mannerisms}\n`;
|
||||||
|
}
|
||||||
|
if (npc.personality.voice) {
|
||||||
|
output += `**Głos:** ${npc.personality.voice}\n`;
|
||||||
|
}
|
||||||
|
output += '\n';
|
||||||
|
|
||||||
|
// Background
|
||||||
|
if (npc.background.history || npc.background.goals) {
|
||||||
|
output += '### Historia\n\n';
|
||||||
|
if (npc.background.history) {
|
||||||
|
output += npc.background.history + '\n\n';
|
||||||
|
}
|
||||||
|
if (npc.background.goals) {
|
||||||
|
output += `**Cele:** ${npc.background.goals}\n`;
|
||||||
|
}
|
||||||
|
if (npc.background.fears) {
|
||||||
|
output += `**Lęki:** ${npc.background.fears}\n`;
|
||||||
|
}
|
||||||
|
output += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
if (npc.relationships.length) {
|
||||||
|
output += '### Relacje\n\n';
|
||||||
|
npc.relationships.forEach(rel => {
|
||||||
|
output += `• **${rel.name}** (${rel.relation})`;
|
||||||
|
if (rel.notes) output += ` - ${rel.notes}`;
|
||||||
|
output += '\n';
|
||||||
|
});
|
||||||
|
output += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hooks
|
||||||
|
if (npc.hooks.length) {
|
||||||
|
output += '### Zaczepki Fabularne\n\n';
|
||||||
|
npc.hooks.forEach(hook => {
|
||||||
|
output += `• ${hook}\n`;
|
||||||
|
});
|
||||||
|
output += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secrets (for GM)
|
||||||
|
if (npc.background.secrets) {
|
||||||
|
output += '> **[DLA MG]** ' + npc.background.secrets + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { npcTemplate, formatNPCForBook };
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* HbM: RPG v3 - Spell Template
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const spellTemplate = {
|
||||||
|
name: '',
|
||||||
|
nameEN: '', // English translation
|
||||||
|
school: '', // Szkoła magii
|
||||||
|
circle: 1, // Krąg (1-9)
|
||||||
|
castingTime: '',
|
||||||
|
range: '',
|
||||||
|
duration: '',
|
||||||
|
components: {
|
||||||
|
verbal: false,
|
||||||
|
somatic: false,
|
||||||
|
material: '',
|
||||||
|
},
|
||||||
|
description: '',
|
||||||
|
higherCircles: '', // Opis dla wyższych kręgów
|
||||||
|
notes: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function formatSpellForBook(spell) {
|
||||||
|
const components = [];
|
||||||
|
if (spell.components.verbal) components.push('W');
|
||||||
|
if (spell.components.somatic) components.push('S');
|
||||||
|
if (spell.components.material) components.push(`M (${spell.components.material})`);
|
||||||
|
|
||||||
|
return `### ${spell.name}
|
||||||
|
*${spell.school}, ${spell.circle}. krąg*
|
||||||
|
|
||||||
|
**Czas rzucania:** ${spell.castingTime}
|
||||||
|
**Zasięg:** ${spell.range}
|
||||||
|
**Komponenty:** ${components.join(', ')}
|
||||||
|
**Czas trwania:** ${spell.duration}
|
||||||
|
|
||||||
|
${spell.description}
|
||||||
|
|
||||||
|
${spell.higherCircles ? `**Na wyższych kręgach:** ${spell.higherCircles}` : ''}
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseSpellFromText(text) {
|
||||||
|
// Basic parser for spell text - customize based on your format
|
||||||
|
const spell = { ...spellTemplate };
|
||||||
|
|
||||||
|
const nameMatch = text.match(/^###?\s*(.+)/m);
|
||||||
|
if (nameMatch) spell.name = nameMatch[1].trim();
|
||||||
|
|
||||||
|
const schoolMatch = text.match(/\*([^,]+),\s*(\d+)\.\s*krąg\*/);
|
||||||
|
if (schoolMatch) {
|
||||||
|
spell.school = schoolMatch[1].trim();
|
||||||
|
spell.circle = parseInt(schoolMatch[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const castingMatch = text.match(/Czas rzucania:\*?\*?\s*(.+)/i);
|
||||||
|
if (castingMatch) spell.castingTime = castingMatch[1].trim();
|
||||||
|
|
||||||
|
const rangeMatch = text.match(/Zasięg:\*?\*?\s*(.+)/i);
|
||||||
|
if (rangeMatch) spell.range = rangeMatch[1].trim();
|
||||||
|
|
||||||
|
const durationMatch = text.match(/Czas trwania:\*?\*?\s*(.+)/i);
|
||||||
|
if (durationMatch) spell.duration = durationMatch[1].trim();
|
||||||
|
|
||||||
|
return spell;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { spellTemplate, formatSpellForBook, parseSpellFromText };
|
||||||
Submodule
+1
Submodule ObsidianNotes added at 2a147496e9
@@ -0,0 +1,677 @@
|
|||||||
|
# FoundryVTT Spell Reference - HbM RPG v3
|
||||||
|
|
||||||
|
> **Status**: Draft / Planning Document
|
||||||
|
> **Audience**: Developer (next iteration of `hbm-rpg-v3` Foundry system)
|
||||||
|
> **Purpose**: Single source of truth for all spells across all books + gap analysis for `SpellData` model and casting logic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Schools & Disciplines Inventory](#1-schools--disciplines-inventory)
|
||||||
|
2. [Witch Magic Symbols Inventory](#2-witch-magic-symbols-inventory)
|
||||||
|
3. [Sacred Magic Deities Inventory](#3-sacred-magic-deities-inventory)
|
||||||
|
4. [Unified Spell Catalog](#4-unified-spell-catalog)
|
||||||
|
5. [SpellData Model - Gap Analysis](#5-spelldata-model--gap-analysis)
|
||||||
|
6. [Casting Logic - Gap Analysis](#6-casting-logic--gap-analysis)
|
||||||
|
7. [Constants Module - Required Additions](#7-constants-module--required-additions)
|
||||||
|
8. [Character Sheet Spell Tab - Enhancement Spec](#8-character-sheet-spell-tab--enhancement-spec)
|
||||||
|
9. [Spell Item Sheet - Enhancement Spec](#9-spell-item-sheet--enhancement-spec)
|
||||||
|
10. [Compendium Pack Plan](#10-compendium-pack-plan)
|
||||||
|
11. [Implementation Phases](#11-implementation-phases)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Schools & Disciplines Inventory
|
||||||
|
|
||||||
|
### 1.1 Academic disciplines (Eastern European School of Magic)
|
||||||
|
|
||||||
|
| Discipline ID (en) | Polish name | Color | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `transmutation` | Alchemia Transmutacji | Brązowy + fioletowy | Subset of legacy Taumaturgia |
|
||||||
|
| `brewing` | Warzenie Eliksirów | Brązowy + zielony | Subset of legacy Taumaturgia |
|
||||||
|
| `botany` | Botanika | Zielony | - |
|
||||||
|
| `elementsAir` | Magia Żywiołów - Powietrze | Żółty | - |
|
||||||
|
| `elementsWater` | Magia Żywiołów - Woda | Niebieski | - |
|
||||||
|
| `elementsFire` | Magia Żywiołów - Ogień | Pomarańczowy | - |
|
||||||
|
| `elementsEarth` | Magia Żywiołów - Ziemia | Jasnozielony | - |
|
||||||
|
| `artifacts` | Rzemiosło Artefaktów | Fioletowy | Subset of legacy Taumaturgia |
|
||||||
|
| `golemancy` | Golemancja | - | Subset of legacy Taumaturgia, no spells in books |
|
||||||
|
| `runes` | Magia Runiczna | - | Subset of legacy Taumaturgia, no spells in books |
|
||||||
|
| `sources` | Źródła Mocy | Tęcza | - |
|
||||||
|
| `illusion` | Magia Iluzji | Bladoniebieski | - |
|
||||||
|
| `sacred` | Magia Sakralna | Biały (egzorcyści: czarny) | Sub-divided by deity |
|
||||||
|
| `witch` | Wiedźmia Magia | - | Symbol-based casting |
|
||||||
|
| `necromancy` | Nekromancja | (kanon) | - |
|
||||||
|
|
||||||
|
### 1.2 Forbidden / extra-academic disciplines
|
||||||
|
|
||||||
|
| Discipline ID (en) | Polish name | Source book |
|
||||||
|
|---|---|---|
|
||||||
|
| `blood` | Magia Krwi | *Arcanum Sanguinis* |
|
||||||
|
| `crimson` | Magia Szkarłatu | *Chwała Szkarłatnemu Kultowi* |
|
||||||
|
| `abyssAspects` | Magia Otchłani - Magia Aspektów | *Klątwa Otchłani* |
|
||||||
|
| `abyssPrimal` | Magia Otchłani - Pierwotna Magia | *Klątwa Otchłani* |
|
||||||
|
| `wildWitch` | Dzika Wiedźmia Magia | *Klątwa Otchłani* / *Księga Magii* |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Witch Magic Symbols Inventory
|
||||||
|
|
||||||
|
> **Note**: List is **extensible** - more symbols may be added in future content. Stored as `string[]` in `constants.ts`, used for autocomplete only; spell data accepts arbitrary strings.
|
||||||
|
|
||||||
|
| Symbol | Domain meaning |
|
||||||
|
|---|---|
|
||||||
|
| `Potentia` | Power / damage |
|
||||||
|
| `Tutamen` | Protection / shield |
|
||||||
|
| `Lux` | Light |
|
||||||
|
| `Motus` | Movement |
|
||||||
|
| `Iter` | Travel / path |
|
||||||
|
| `Vacuos` | Void / displacement |
|
||||||
|
| `Vitium` | Decay / corruption |
|
||||||
|
| `Praecantatio` | Magic itself |
|
||||||
|
| `Aer` | Air |
|
||||||
|
| `Aqua` | Water |
|
||||||
|
| `Gelum` | Cold / ice |
|
||||||
|
| `Ignis` | Fire |
|
||||||
|
| `Terra` | Earth |
|
||||||
|
| `Cognitio` | Knowledge / mind |
|
||||||
|
| `Alienis` | Alien / foreign |
|
||||||
|
| `Illusio` | Illusion |
|
||||||
|
| `Somnium` | Dream |
|
||||||
|
| `Tenebrae` | Shadow / darkness |
|
||||||
|
| `Auram` | Aura |
|
||||||
|
| `Vinculum` | Binding |
|
||||||
|
| `Telum` | Weapon / projectile |
|
||||||
|
| `Sensus` | Sense / perception |
|
||||||
|
| `Perditio` | Destruction |
|
||||||
|
| `Perfodio` | Piercing / digging |
|
||||||
|
| `Sano` | Healing |
|
||||||
|
| `Volatus` | Flight |
|
||||||
|
| `Tempestas` | Storm |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Sacred Magic Deities Inventory
|
||||||
|
|
||||||
|
| Deity ID | Polish label | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `common` | Modlitwy Powszechne | Available to all sacred casters |
|
||||||
|
| `jahwe` | Bóg-Jahwe-Jedyny | Some spells race-gated (Anioł, Człowiek) |
|
||||||
|
| `zeus` | Zeus-Jowisz | Storm domain |
|
||||||
|
| `demeter` | Demeter-Ceres | Agriculture / nature |
|
||||||
|
| `artemis` | Artemida-Diana | Hunt / beasts |
|
||||||
|
| `hekate` | Hekate | **Hybrid sacred/witch** - symbols + zeal |
|
||||||
|
| `aphrodite` | Afrodyta-Wenus | Charm / persuasion |
|
||||||
|
| `eros` | Amor-Eros | Bonds / desire |
|
||||||
|
|
||||||
|
> **Hekate is special**: her spells have witch-magic symbols and complexity, allowing them to be cast either as sacred (zeal) or witch (mana + symbols) prayers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Unified Spell Catalog
|
||||||
|
|
||||||
|
> Format per spell:
|
||||||
|
> - **Name** (Polish), `id` (slug for compendium)
|
||||||
|
> - `school` / `discipline` / `deity` / `castingMode`
|
||||||
|
> - `circle` (T:Y) → `difficulty.threshold:difficulty.successes`
|
||||||
|
> - `manaCost` / `bloodCost` (where applicable)
|
||||||
|
> - `castingTime` / `range` / `areaOfEffect` / `targets` / `duration`
|
||||||
|
> - `components` (verbal / somatic / material / symbols)
|
||||||
|
> - `requirements` (race / talent / discipline mastery)
|
||||||
|
> - `flags` (`isSuperspell`, `nonCombatOnly`, `requiresGroupCast`)
|
||||||
|
> - `damageBase` (formula reference)
|
||||||
|
> - `statusEffects[]` (conditions applied)
|
||||||
|
> - `overcastOptions[]`
|
||||||
|
> - `sourceBook`
|
||||||
|
|
||||||
|
### 4.1 Magia Ogólna (Księga Magii)
|
||||||
|
|
||||||
|
| # | id | Name | T:Y | Mana | Range | Duration | Symbols | Damage | Status | Notes |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| 1 | `magic-missile` | Magiczny Pocisk | 4:1 | 1 | 15 m | inst. | `Potentia` | `½ magicalAbilities` | - | Overcast: +1 dmg |
|
||||||
|
| 2 | `magic-shield` | Magiczna Tarcza | 5:1 | 2 | 5 m | 2 r | `Tutamen` | - | - | HP = ½ MA + 1; OC: +1d2 HP / +1 r |
|
||||||
|
| 3 | `gleam` | Poblask | 3:1 | 1 | self | 1 h | `Lux` | - | - | Item glows; OC: +1 h |
|
||||||
|
| 4 | `telekinesis` | Telekineza | 4:1 | 1 | 20 m | 10 r | `Motus` | - | - | ≤1 kg; OC: +10 r / +1 kg |
|
||||||
|
| 5 | `blink` | Przeskok | 5:2 | 3 | 20 m | inst. | `Iter, Praecantatio` | - | - | No AoO; OC: +5 m |
|
||||||
|
| 6 | `teleport` | Teleportacja | 6:3 | 5 | known location | inst. (cast 10 min) | `Iter, Praecantatio, Vacuos` | - | - | OC: +1 person (3 mana) |
|
||||||
|
| 7 | `ether-pierce` | Przebicie Eteru | 5:3 | 4 | 10 m | inst. | `Tutamen, Vitium` | `½ MA, ignores armor` | - | 2 dmg = 1 Rune Shield; OC: +1d2 dmg |
|
||||||
|
| 8 | `energy-wave` | Fala Energii | 5:2 | 4 | 10 m | inst. | `Praecantatio, Potentia` | `½ MA AoE` | Oszołomiony (Reflex save) | Rect 3×8 m; OC: +1d3 dmg / +2 Y |
|
||||||
|
| 9 | `truth-glimpse` | Przebłysk Prawdy | 4:1 | 1 | touch | 1 r | `Cognitio` | - | - | +1 die lie/behavior; OC: +1 r / +1 die |
|
||||||
|
| 10 | `quick-thought` | Szybka Myśl | 5:1 | 1 | self | 1 r | `Cognitio` | - | - | +1 init, +1 die; OC stacking |
|
||||||
|
|
||||||
|
### 4.2 Magia Żywiołów - Powietrze
|
||||||
|
|
||||||
|
| # | id | Name | T:Y | Mana | Range | Duration | Symbols | Damage | Status |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| 11 | `flight` | Latanie | 5:2 | 3 | self | 10 r | `Volatus` | - | - |
|
||||||
|
| 12 | `air-wave` | Powietrzna Fala | 5:1 | 3 | 10 m | inst. | `Aer, Potentia` | `½ MA AoE` | Powalony (Strength save) |
|
||||||
|
| 13 | `danger-sense` | Wyczucie Zagrożenia | 6:1 | 4 | self | 2 r | `Aer, Sensus` | - | - |
|
||||||
|
| 14 | `accuracy` | Celność | 6:1 | 2 | self | 2 r | `Sensus, Telum` | - | - |
|
||||||
|
| 15 | `wind-swift` | Szybki jak Wiatr | 5:1 | 1 | self | 1 r | `Motus` | - | Immune Powalony |
|
||||||
|
|
||||||
|
### 4.3 Magia Żywiołów - Woda
|
||||||
|
|
||||||
|
| # | id | Name | T:Y | Mana | Range | Duration | Symbols | Damage | Status |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| 16 | `water-create` | Tworzenie i Kontrolowanie Wody | 5:1 | 1 | 5 m | 10 min | `Aqua` | - | - |
|
||||||
|
| 17 | `ice-floor` | Lodowa Podłoga | 6:1 | 3 | 10 m | 2 r | `Gelum, Vinculum` | - | Powalony (Athletics save) |
|
||||||
|
| 18 | `solace` | Ukojenie | 5:1 | 2 | self | 1 r | `Sano` | `heal ½ MA` | - |
|
||||||
|
| 19 | `hailstorm` | Gradobicie | 5:4 | 5 | 10 m | 2 r | `Gelum, Praecantatio, Tempestas` | `MA dmg` | Trudny Teren; Reflex save |
|
||||||
|
| 20 | `mystic-fog` | Mistyczna Mgła | 5:3 | 4 | self | 1 r | `Aqua, Tenebrae` | - | Oślepiony (enemies) |
|
||||||
|
|
||||||
|
### 4.4 Magia Żywiołów - Ogień
|
||||||
|
|
||||||
|
| # | id | Name | T:Y | Mana | Range | Duration | Symbols | Damage | Status |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| 21 | `ignition` | Podpalenie | 5:2 | 3 | 15 m | 1 r | `Ignis, Vinculum` | `3 + 3/turn` | Podpalony |
|
||||||
|
| 22 | `kindle` | Wzniecanie Ognia | 3:1 | 1 | touch | inst. | `Ignis` | - | - |
|
||||||
|
| 23 | `fire-cloak` | Ognista Powłoka | 5:2 | 4 | self | 3 r | `Ignis, Telum` | weapon +2 magical | - |
|
||||||
|
| 24 | `fireball` | Kula Ognia | 6:1 | 2 | 15 m | inst. | `Ignis, Potentia` | `MA` | Podpalony 3 r (Reflex save) |
|
||||||
|
| 25 | `burning-skin` | Gorejąca Skóra | 4:3 | 5 | self | 1 r | `Ignis, Tutamen` | `½ MA reactive` | - |
|
||||||
|
|
||||||
|
### 4.5 Magia Żywiołów - Ziemia
|
||||||
|
|
||||||
|
| # | id | Name | T:Y | Mana | Range | Duration | Symbols | Damage | Status |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| 26 | `stone-shield` | Skalista Tarcza | 5:1 | 2 | 5 m | 1 r | `Terra` | wall HP = MA | blocks LoS |
|
||||||
|
| 27 | `quicksand` | Ruchome Piaski | 5:3 | 3 | 10 m | 2 r | `Terra, Perfodio, Vinculum` | - | half movement |
|
||||||
|
| 28 | `earthquake` | Trzęsienie Ziemi | 4:6 | 5 | 10 m | 3 r | `Terra, Potentia, Perditio` | - | Unieruchomiony+Powalony, Nieprzytomny on 3 fails |
|
||||||
|
| 29 | `gravel-armor` | Żwirowa Zbroja | 4:1 | 2 | self | 2 r | `Tutamen, Terra` | - | +1 armor, –½ speed |
|
||||||
|
| 30 | `stone-trap` | Skalista Pułapka | 5:4 | 4 | 15 m | 1 min | `Terra, Vinculum, Telum` | `MA` (trigger) | - |
|
||||||
|
|
||||||
|
### 4.6 Magia Sakralna - Modlitwy Powszechne
|
||||||
|
|
||||||
|
| # | id | Name | T:Y | Mana | Range | Duration | Damage | Notes |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| 31 | `sanctify` | Uświęcenie | 4:1 | 2 | self | 1 r | weapon → magical | - |
|
||||||
|
| 32 | `holy-steel` | Święta Stal | 5:1 | 3 | self | 1 r | weapon +1 magical | –1 die Reflex on hit |
|
||||||
|
| 33 | `healing-touch` | Leczący Dotyk | 3:1 | 1 | touch | inst. | `heal MA or Devotion` | - |
|
||||||
|
| 34 | `protection-prayer` | Modlitwa o Opiekę | 5:1 | 1 | self | 1 r | - | +1 die vs spells/curses |
|
||||||
|
| 35 | `divine-justice` | Boska Sprawiedliwość | 6:1 | 2 | 10 m | inst. | `½ MA/Dev (+1d6 vs heretic)` | - |
|
||||||
|
|
||||||
|
### 4.7 Magia Sakralna - Bóg-Jahwe-Jedyny
|
||||||
|
|
||||||
|
| # | id | Name | T:Y | Mana | Race req | Damage | Notes |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| 36 | `david-blade` | Ostrze Dawida | 5:1 | 3 | **Anioł** | weapon S+1 magical slash | - |
|
||||||
|
| 37 | `last-judgement` | Sąd Ostateczny | 6:3 | 6 | - | `3× MA/Dev` | Instant death heretic/demon/undead if Devotion ≥ Soul |
|
||||||
|
| 38 | `solomon-wisdom` | Mądrość Salomona | 5:1 | 2 | - | - | +1 die all Mind tests, 1 h |
|
||||||
|
| 39 | `fanatic-sacrifice` | Fanatyczne Poświęcenie | 6:2 | 5 | **Człowiek** | reciprocal ×2 | Przerażenie aura |
|
||||||
|
| 40 | `cure-disease` | Uzdrowienie Chorych | 5:2 | 4 | - | - | Removes 1 disease/poison |
|
||||||
|
|
||||||
|
### 4.8 Magia Sakralna - Zeus-Jowisz
|
||||||
|
|
||||||
|
| # | id | Name | T:Y | Mana | Damage | Status |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 41 | `thunderbolt` | Grom z Nieba | 5:4 | 4 | `2× MA/Dev AoE` | Ogłuszony |
|
||||||
|
| 42 | `chain-lightning` | Błyskawice Łańcuchowe | 5:3 | 3 | `MA/Dev per chain` | - |
|
||||||
|
| 43 | `aegis` | Egida | 6:1 | 2 | - | +1 armor, attacks –1 step |
|
||||||
|
| 44 | `static-discharge` | Wyładowanie Elektrostatyczne | 4:4 | 4 | `½ MA/Dev` | Przewrócony |
|
||||||
|
| 45 | `king-presence` | Obecność Króla Bogów | 6:3 | 6 | - | Authority aura |
|
||||||
|
|
||||||
|
### 4.9 Magia Sakralna - Demeter-Ceres
|
||||||
|
|
||||||
|
| # | id | Name | T:Y | Mana | Notes | nonCombatOnly |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 46 | `harvest-blessing` | Błogosławieństwo Zasiewów | 6:2 | 3 | +20% yields | ✓ |
|
||||||
|
| 47 | `earth-power` | Moc Ziemi | 5:2 | 4 | regen on natural soil | - |
|
||||||
|
| 48 | `accelerated-harvest` | Przyspieszone Żniwa | 6:4 | 5 | rzucanie 1 h | ✓ |
|
||||||
|
| 49 | `season-cycle` | Cykl Pór Roku | 6:6 | 6 | microclimate | - |
|
||||||
|
| 50 | `mother-wrath` | Gniew Matki Ziemi | 4:4 | 4 | `½ MA/Dev` per move | - |
|
||||||
|
|
||||||
|
### 4.10 Magia Sakralna - Artemida-Diana
|
||||||
|
|
||||||
|
| # | id | Name | T:Y | Mana | Damage | Notes |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 51 | `bridge` | Pomost | 6:1 | 4 | - | partial beast morph |
|
||||||
|
| 52 | `awakening` | Przebudzenie | 5:3 | 5 | - | full beast morph |
|
||||||
|
| 53 | `insect-swarm` | Chmara Insektów | 4:5 | 4 | - | –1 armor, –50% speed |
|
||||||
|
| 54 | `divine-aim` | Boska Celność | 4:3 | 2 | - | +3 dice ranged |
|
||||||
|
| 55 | `divine-spear` | Boska Włócznia | 5:2 | 3 | `MA/Dev ignore armor` | +1d3/extra success |
|
||||||
|
|
||||||
|
### 4.11 Magia Sakralna - Hekate (hybrid sacred/witch)
|
||||||
|
|
||||||
|
> **Special**: cast as either sacred (zeal cost) or witch (mana + symbols).
|
||||||
|
|
||||||
|
| # | id | Name | T:Y | Mana | Symbols | Notes |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 56 | `magic-amplify` | Wzmocnienie Magii | 4:3 | 4 | `Praecantatio, Auram` | +4 dice Magical Abilities |
|
||||||
|
| 57 | `mana-theft` | Kradzież Many | 6:3 | 5 | `Praecantatio, Telum, Auram` | +2 mana / +1 max cost |
|
||||||
|
| 58 | `magic-acceleration` | Magiczne Przyspieszenie | 5:2 | 3 | `Praecantatio, Potentia` | +1 Zapał |
|
||||||
|
| 59 | `hekate-curse` | Klątwa Hekate | 6:2 | 6 | `Praecantatio, Vitium, Vinculum` | –1 die MA + Przerażony |
|
||||||
|
| 60 | `arcane-knowledge` | Tajemne Poznanie | 5:1 | 2 | `Cognitio` | +1 die magic/theology lore |
|
||||||
|
|
||||||
|
### 4.12 Magia Sakralna - Afrodyta-Wenus
|
||||||
|
|
||||||
|
| # | id | Name | T:Y | Mana | Notes | nonCombatOnly |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 61 | `innocence-charm` | Urok Niewinności | 5:1 | 2 | +2 Y to attack | - |
|
||||||
|
| 62 | `heart-bond` | Więź Serc | 6:3 | 4 | empathic link | ✓ |
|
||||||
|
| 63 | `passion-flame` | Płomień Namiętności | 6:1 | 5 | +2 die empathy/persuasion | - |
|
||||||
|
| 64 | `goddess-beauty` | Piękno Bogini | 5:5 | 6 | +3 die persuasion | - |
|
||||||
|
| 65 | `harmony-ray` | Promień Harmonii | 5:4 | 5 | suppress conflict | - |
|
||||||
|
|
||||||
|
### 4.13 Magia Sakralna - Amor-Eros
|
||||||
|
|
||||||
|
| # | id | Name | T:Y | Mana | Status | nonCombatOnly |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 66 | `cupid-arrow` | Strzała Kupidyna | 4:3 | 3 | Zauroczony | - |
|
||||||
|
| 67 | `heart-longing` | Tęsknota Serca | 5:1 | 2 | obsession | ✓ |
|
||||||
|
| 68 | `senses-desire` | Pożądanie Zmysłów | 3:5 | 5 | +5 die romantic | - |
|
||||||
|
| 69 | `destiny-thread` | Nić Przeznaczenia | (none) | 6 | bond (Soul save) | ✓ |
|
||||||
|
| 70 | `desire-arrow` | Strzała Pożądania | 5:2 | 4 | Zauroczony if dmg > Soul | - |
|
||||||
|
|
||||||
|
### 4.14 Magia Alchemii Transmutacji
|
||||||
|
|
||||||
|
| # | id | Name | T:Y | Mana | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 71 | `fools-gold` | Złoto Głupców | 6:2 | 3 | item appears as gold |
|
||||||
|
| 72 | `detect-changes` | Wykrycie Zmian | `5:S` | 1 | GM-set difficulty; **no overcast** |
|
||||||
|
| 73 | `magic-resistance` | Magiczna Odporność | 5:1 | 2 | immunity to chosen status |
|
||||||
|
| 74 | `lead-transmute` | Przemiana w Ołów | 5:2 | 3 | –2 dice melee/ranged AoE |
|
||||||
|
| 75 | `magnetism` | Magnetyzm | 5:1 | 5 | metal items expelled |
|
||||||
|
|
||||||
|
### 4.15 Magia Iluzji
|
||||||
|
|
||||||
|
| # | id | Name | T:Y | Mana | Symbols | Notes |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 76 | `invisibility` | Niewidzialność | 5:2 | 5 | `Aer, Tenebrae` | - |
|
||||||
|
| 77 | `mirror-image` | Zwierciadlany Wizerunek | 5:1 | 3 | `Sensus, Vitium, Illusio` | duplicate ≤20 m |
|
||||||
|
| 78 | `mind-maze` | Labirynt Umysłu | 4:2 | 2 | `Cognitio, Alienis` | concentration check or lose action |
|
||||||
|
| 79 | `magic-shackles` | Magiczne Kajdany | 4:2 | 3 | `Vinculum, Cognitio` | Unieruchomiony + no casting |
|
||||||
|
| 80 | `dream-veil` | Woal Snów | 6:3 | 4 | `Somnium, Illusio, Cognitio` | `½ MA ignore armor` + Przerażony |
|
||||||
|
|
||||||
|
### 4.16 Nekromancja
|
||||||
|
|
||||||
|
| # | id | Name | T:Y | Mana | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 81 | `ethereal-guide` | Eteryczny Przewodnik | 6:1 | 2 | summons mindless ghost |
|
||||||
|
| 82 | `negation` | Negacja | 5:3 | 5 | anti-magic fog AoE |
|
||||||
|
| 83 | `animate-skeleton` | Ożywienie Szkieletu | 6:2 | 3 | mindless skeleton |
|
||||||
|
| 84 | `dispersal` | Rozsypanie | 5:1 | 4 | `2× MA`, destroy weak undead |
|
||||||
|
| 85 | `speak-with-dead` | Rozmawianie ze Zmarłymi | 5:1 | 3 | 1 question / soul |
|
||||||
|
|
||||||
|
### 4.17 Superzaklęcia (Księga Magii VI)
|
||||||
|
|
||||||
|
> **All flagged**: `isSuperspell: true`, requires Mistrzostwo talent.
|
||||||
|
|
||||||
|
| # | id | Name | T:Y | Mana | Mastery | Cast time | Group? | Notes |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| 86 | `super-destruction` | Destrukcja | 6:20 | 20 | any | 1 h | - | 50 m blast, 5 yr taint |
|
||||||
|
| 87 | `super-disintegration` | Prawdziwa Dezintegracja | 6:15 | 15 | any | 10 min | - | irreversible |
|
||||||
|
| 88 | `super-doom` | Zagłada | 6:30 | 30 | any | 3 h | **≥3 mages** | co-casters die |
|
||||||
|
| 89 | `super-magical-plague` | Magiczna Plaga | 6:25 | 25 | botany/witch/brewing/abyss | 30 min | - | 2d3 years |
|
||||||
|
| 90 | `super-aegis` | Aegis | 6:30 | 30 | any | 1 h | - | 24 h dome |
|
||||||
|
| 91 | `super-landscape` | Transmutacja Krajobrazu | 6:20 | 20 | transmutation/earth/botany | 30 min | - | 5 km² permanent |
|
||||||
|
| 92 | `super-portal` | Bramy Wymiarów | 6:25 | 25 | botany/witch/brewing/abyss | 2 h | - | 2d6 years |
|
||||||
|
| 93 | `super-reanimation` | Reanimacja | 6:15 | 15 | sacred | 3 h | - | participants age 1d6 |
|
||||||
|
|
||||||
|
### 4.18 Magia Szkarłatu (Chwała Szkarłatnemu Kultowi)
|
||||||
|
|
||||||
|
| # | id | Name | T:Y | Mana | Range | Damage | Notes |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| C1 | `crimson-bolt` | Szkarłatny Pocisk | 6:1 | 1 | 15 m | `MA, ignore armor` | - |
|
||||||
|
| C2 | `crimson-flames` | Szkarłatne Płomienie | 6:1 | 4 | 10 m | shield-eater | - |
|
||||||
|
| C3 | `crimson-justice` | Szkarłatna Sprawiedliwość | 5:3 | 5 | 10 m | `2× MA` + AoE | Ogłuszony+Oślepiony |
|
||||||
|
| C4 | `crimson-curse` | Klątwa Szkarłatu | 5:2 | 4 | 10 m | reactive | Nieprzytomny on cast |
|
||||||
|
| C5 | `crimson-dagger` | Szkarłatny Sztylet | 5:2 | 3 | self | weapon +2 magical | **trigger**: free spell after kill (1/round) |
|
||||||
|
| CS | `crimson-requiem` | Requiem | 6:25 | 25 | 100 m | `3d6 ignore all` | Superspell; 7 days "crimson dreams" |
|
||||||
|
|
||||||
|
### 4.19 Magia Otchłani (Klątwa Otchłani)
|
||||||
|
|
||||||
|
| # | id | Name | T:Y | Mana | Range | Damage | Notes |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| A1 | `primal-orb` | Pierwotna Kula | 5:2 | 2 | 15 m | `MA + 1d6` | chaotic targeting |
|
||||||
|
| A2 | `mysterious-stream` | Tajemniczy Strumień | 6:2 | 3 | 10 m | `MA` | +1d3 Insanity |
|
||||||
|
| A3 | `forbidden-ward` | Zakazana Osłona | 5:3 | 4 | self | reflect | reactive, 1 use |
|
||||||
|
| A4 | `neutralization` | Neutralizacja | 6:3 | 5 | 20 m | `Mind/turn` | blocks casting |
|
||||||
|
| A5 | `summon-abyss` | Przywołanie Istoty z Otchłani | `6:n` | 6 | 5 m | - | **variable Y per creature**: 2/4/6/10 |
|
||||||
|
|
||||||
|
### 4.20 Magia Krwi (Arcanum Sanguinis) - TBD
|
||||||
|
|
||||||
|
> **Status**: Per user clarification, Magia Krwi will get its own spell list in a future content release. Reserve `castingMode: 'blood'` and `bloodCost: number` field on `SpellData`. No spells to catalog yet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. SpellData Model - Gap Analysis
|
||||||
|
|
||||||
|
> Reference: [.src/foundry-system/src/data/item-spell.ts](../.src/foundry-system/src/data/item-spell.ts)
|
||||||
|
|
||||||
|
### 5.1 Current schema (v0.1.2)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
circle, school, discipline,
|
||||||
|
castingMode: 'standard' | 'sacred' | 'witch',
|
||||||
|
difficulty: { threshold, successes },
|
||||||
|
components: { verbal, somatic, material },
|
||||||
|
castingTime, range, targets, duration,
|
||||||
|
manaCost, overcasting,
|
||||||
|
complexityLevel, // witch only
|
||||||
|
description, higherCircles
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Missing fields → required additions
|
||||||
|
|
||||||
|
| New field | Type | Default | Why |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `deity` | `string` | `''` | Sacred spells gated by deity (8 entries; `''` = common) |
|
||||||
|
| `castingMode` (extend) | add `'blood'` to enum | - | Magia Krwi gets its own mode |
|
||||||
|
| `bloodCost` | `number` | `0` | Magia Krwi resource cost |
|
||||||
|
| `components.symbols` | `string[]` | `[]` | Witch / Hekate spell symbol list (replaces only-counting `complexityLevel`) |
|
||||||
|
| `requirements.race` | `string[]` | `[]` | e.g. `['angel']`, `['human']` |
|
||||||
|
| `requirements.talent` | `string[]` | `[]` | e.g. `['mastery']` for superspells |
|
||||||
|
| `requirements.discipline` | `string[]` | `[]` | e.g. `['transmutation', 'botany']` for Magiczna Plaga |
|
||||||
|
| `isSuperspell` | `boolean` | `false` | Distinguishes superzaklęcia |
|
||||||
|
| `requiresGroupCast` | `boolean` | `false` | True for Zagłada |
|
||||||
|
| `minCasters` | `number` | `1` | 3 for Zagłada |
|
||||||
|
| `nonCombatOnly` | `boolean` | `false` | Tęsknota Serca, Więź Serc, Demeter rituals, etc. |
|
||||||
|
| `areaOfEffect` | `SchemaField` | - | `{ shape: 'point'\|'square'\|'rectangle'\|'cone'\|'sphere'\|'line', x: number, y: number, unit: 'm' }` |
|
||||||
|
| `damageBase` | `string` | `''` | Formula reference: `'magicalAbilities'`, `'magicalAbilities/2'`, `'magicalAbilities*2'`, `'1d6+magicalAbilities'`, etc. Parsed at cast time. |
|
||||||
|
| `damageType` | `string` | `'magical'` | `'magical' \| 'physical-magical' \| 'pure'` (ignores armor) |
|
||||||
|
| `ignoresArmor` | `boolean` | `false` | Crimson, Ether Pierce, Divine Spear |
|
||||||
|
| `statusEffects` | `string[]` | `[]` | Conditions a spell may apply (`'stunned'`, `'prone'`, `'frightened'`, `'charmed'`, etc.) |
|
||||||
|
| `saveAttribute` | `string` | `''` | If non-empty, target rolls `<attribute>+<saveSkill>` to resist |
|
||||||
|
| `saveSkill` | `string` | `''` | e.g. `'reflex'`, `'determination'` |
|
||||||
|
| `overcastOptions` | `array of {description, manaPerStep}` | `[]` | Replace free-text `overcasting` with structured list (legacy field kept for migration) |
|
||||||
|
| `sourceBook` | `string` | `''` | `'kg-magii'` / `'crimson-cult'` / `'abyss-curse'` / `'arcanum-sanguinis'` / `'core-rules'` |
|
||||||
|
| `castingTimeRounds` | `number` | `0` | 0 = action; >0 = rounds (used for combat enforcement of long casts) |
|
||||||
|
| `castingTimeMinutes` | `number` | `0` | For non-combat scaling (rituals, superspells) |
|
||||||
|
| `triggers` | `array of {event, effect}` | `[]` | Reactive effects: `{event:'killWithWeapon', effect:'freeCrimsonSpell'}`, `{event:'targetCastsSpell', effect:'forceUnconscious'}` |
|
||||||
|
| `variableSuccesses` | `array of {label, successes}` | `[]` | For Przywołanie Istoty z Otchłani: `[{label:'Tajemny Krab', successes:2}, ...]` |
|
||||||
|
|
||||||
|
### 5.3 Backward compatibility / migration
|
||||||
|
|
||||||
|
- Keep legacy fields (`overcasting`, `complexityLevel`, `targets`) populated for one minor version.
|
||||||
|
- Migration script in `src/migrations/0.2.0.ts`:
|
||||||
|
- Move `complexityLevel` count → ensure `components.symbols.length` matches; if symbols empty, leave count.
|
||||||
|
- Best-effort parse of `targets` string → `areaOfEffect` (regex: `/(\d+)\s*[×x]\s*(\d+)/`, `/promień\s+(\d+)/`).
|
||||||
|
- `overcasting` text → seed first entry of `overcastOptions[0].description`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Casting Logic - Gap Analysis
|
||||||
|
|
||||||
|
> Reference: [.src/foundry-system/src/logic/spell-cast.ts](../.src/foundry-system/src/logic/spell-cast.ts)
|
||||||
|
|
||||||
|
### 6.1 Pre-cast validation gates (new)
|
||||||
|
|
||||||
|
```
|
||||||
|
function validateCast(actor, spell, opts) -> ValidationResult
|
||||||
|
- resourceCheck (mana / zeal / blood / mana+zeal for Hekate)
|
||||||
|
- raceCheck (spell.requirements.race)
|
||||||
|
- talentCheck (spell.requirements.talent - items on actor)
|
||||||
|
- disciplineCheck (spell.requirements.discipline - at least one match)
|
||||||
|
- deityCheck (sacred + non-empty deity → actor must be devoted to that deity)
|
||||||
|
- groupCastCheck (warn if requiresGroupCast and only 1 caster targeted)
|
||||||
|
- inCombatCheck for nonCombatOnly → block (warn-only override flag in opts)
|
||||||
|
- inCombatCheck for isSuperspell → WARN (do not block); bypass via opts.bypassSuperspellWarning
|
||||||
|
- witchSymbolCheck (count <= ceil(magic/2)) - already exists
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 New CastOptions
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface CastOptions {
|
||||||
|
manaSpent?: number;
|
||||||
|
zealSpent?: number; // for Hekate when cast as sacred
|
||||||
|
bloodSpent?: number; // for blood mode
|
||||||
|
hekateMode?: 'sacred' | 'witch'; // Hekate spell dispatch
|
||||||
|
bypassSuperspellWarning?: boolean; // Downcasting talent, NPC quick-cast abilities
|
||||||
|
bypassNonCombatBlock?: boolean; // GM override
|
||||||
|
groupCasters?: string[]; // actor IDs of co-casters
|
||||||
|
speaker?: ChatMessage.SpeakerData;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 New casting modes
|
||||||
|
|
||||||
|
#### `castBlood(actor, spell, opts)`
|
||||||
|
- Resource: deduct `spell.bloodCost` from `actor.system.attributes.blood.value` (new attribute on character data - or HP if blood pool not added)
|
||||||
|
- Pool: `magic + magicalAbilities` (placeholder; final formula TBD when Blood Magic spells released)
|
||||||
|
- No overcast for v1; revisit when content lands
|
||||||
|
|
||||||
|
#### `castSacred` extension
|
||||||
|
- If spell has `complexityLevel > 0` (Hekate), allow `opts.hekateMode === 'witch'` to dispatch to `castWitch` instead
|
||||||
|
|
||||||
|
### 6.4 Damage rolling integration
|
||||||
|
|
||||||
|
Currently `castSpell` only rolls the TS test - no damage application. Add:
|
||||||
|
|
||||||
|
```
|
||||||
|
function rollSpellDamage(actor, spell): Roll
|
||||||
|
- parse spell.damageBase formula
|
||||||
|
- resolve tokens: 'magicalAbilities', 'devotion', 'soul', '1d6', etc.
|
||||||
|
- return Roll for chat output
|
||||||
|
```
|
||||||
|
|
||||||
|
Display side-by-side in chat card: TS result | Damage formula result | Status effects to apply | Save target attribute+skill.
|
||||||
|
|
||||||
|
### 6.5 Status effect auto-apply
|
||||||
|
|
||||||
|
When TS test succeeds AND target has lower successes on save:
|
||||||
|
- For each entry in `spell.statusEffects[]`, apply ActiveEffect via Foundry condition system
|
||||||
|
- Hook into `conditions.ts` (already exists for combat status tracking)
|
||||||
|
|
||||||
|
### 6.6 Trigger registration
|
||||||
|
|
||||||
|
For reactive spells (Szkarłatny Sztylet, Klątwa Szkarłatu):
|
||||||
|
- On successful cast → register a hook on actor (`flag.hbm-rpg-v3.triggers[]`)
|
||||||
|
- Hook fires on matching combat event (`'killWithWeapon'` → free spell prompt; `'targetCastsSpell'` → opposed Determination test)
|
||||||
|
- Triggers expire on duration end or single use
|
||||||
|
|
||||||
|
### 6.7 Variable success summons
|
||||||
|
|
||||||
|
For Przywołanie Istoty z Otchłani:
|
||||||
|
- Cast UI prompts user to pick which entity (Tajemny Krab / Strażnik / Czempion / Simulacrum)
|
||||||
|
- Required successes loaded from `spell.variableSuccesses[selected].successes`
|
||||||
|
- Pass into TS test as `required` override
|
||||||
|
|
||||||
|
### 6.8 Group cast orchestration
|
||||||
|
|
||||||
|
For Zagłada:
|
||||||
|
- Cast UI lists co-casters from `opts.groupCasters[]`
|
||||||
|
- Each contributes their dice pool (sum)
|
||||||
|
- On success, all participants take "death" status (per spell description)
|
||||||
|
- Pre-cast warning chat card requires GM confirm
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Constants Module - Required Additions
|
||||||
|
|
||||||
|
> Reference: [.src/foundry-system/src/constants.ts](../.src/foundry-system/src/constants.ts)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const CASTING_MODES = ['standard', 'sacred', 'witch', 'blood'] as const;
|
||||||
|
|
||||||
|
export const SPELL_SCHOOLS = [
|
||||||
|
'general', 'elements', 'sacred', 'illusion', 'necromancy',
|
||||||
|
'transmutation', 'brewing', 'botany', 'artifacts', 'golemancy',
|
||||||
|
'runes', 'sources', 'witch',
|
||||||
|
'blood', 'crimson', 'abyssAspects', 'abyssPrimal', 'wildWitch',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const SACRED_DEITIES = [
|
||||||
|
'common', 'jahwe', 'zeus', 'demeter', 'artemis',
|
||||||
|
'hekate', 'aphrodite', 'eros',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Witch magic symbols. EXTENSIBLE - new content may add more.
|
||||||
|
* Used for autocomplete only; spell.components.symbols accepts arbitrary strings.
|
||||||
|
*/
|
||||||
|
export const WITCH_SYMBOLS: readonly string[] = Object.freeze([
|
||||||
|
'Potentia', 'Tutamen', 'Lux', 'Motus', 'Iter', 'Vacuos', 'Vitium',
|
||||||
|
'Praecantatio', 'Aer', 'Aqua', 'Gelum', 'Ignis', 'Terra',
|
||||||
|
'Cognitio', 'Alienis', 'Illusio', 'Somnium', 'Tenebrae', 'Auram',
|
||||||
|
'Vinculum', 'Telum', 'Sensus', 'Perditio', 'Perfodio', 'Sano',
|
||||||
|
'Volatus', 'Tempestas',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const SOURCE_BOOKS = [
|
||||||
|
'core-rules', 'magic-book', 'arcanum-sanguinis',
|
||||||
|
'crimson-cult', 'abyss-curse', 'humanity-guide',
|
||||||
|
'bestiary', 'gold-steel-magic',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const AOE_SHAPES = ['point', 'square', 'rectangle', 'cone', 'sphere', 'line'] as const;
|
||||||
|
|
||||||
|
export const CONDITIONS = [
|
||||||
|
'stunned', 'prone', 'frightened', 'charmed', 'unconscious',
|
||||||
|
'restrained', 'blinded', 'poisoned', 'burning',
|
||||||
|
'authority', 'dominated', 'concentrating',
|
||||||
|
] as const;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Character Sheet Spell Tab - Enhancement Spec
|
||||||
|
|
||||||
|
> Reference: [.src/foundry-system/src/sheets/character-sheet.ts](../.src/foundry-system/src/sheets/character-sheet.ts)
|
||||||
|
|
||||||
|
### 8.1 Grouping & filtering
|
||||||
|
|
||||||
|
- Replace flat `spells` list with grouped structure in `_prepareContext`:
|
||||||
|
```ts
|
||||||
|
spellsByDiscipline: Record<DisciplineId, Item[]>
|
||||||
|
spellsBySchool: Record<School, Item[]>
|
||||||
|
superspells: Item[] // separate list pulled out
|
||||||
|
```
|
||||||
|
- Sidebar filter UI: by `school`, `castingMode`, `deity`, `circle`, search-by-name
|
||||||
|
- Persist filter in `tabGroups['spellsFilter']`
|
||||||
|
|
||||||
|
### 8.2 Per-row badges
|
||||||
|
|
||||||
|
| Badge | Source field |
|
||||||
|
|---|---|
|
||||||
|
| Casting mode icon | `system.castingMode` (4 icons: ✦ standard, ✟ sacred, ✶ witch, ❤ blood) |
|
||||||
|
| Source book pip | `system.sourceBook` (color-coded) |
|
||||||
|
| Deity label | `system.deity` (only for sacred) |
|
||||||
|
| Superspell star | `system.isSuperspell === true` |
|
||||||
|
| Race-locked badge | `system.requirements.race.length > 0` |
|
||||||
|
|
||||||
|
### 8.3 Cast button enhancements
|
||||||
|
|
||||||
|
- Greyed out + tooltip when `validateCast()` fails (race/talent/discipline missing)
|
||||||
|
- Yellow warning icon when superspell + actor in combat
|
||||||
|
- Click → opens enhanced cast dialog with mana/zeal/blood field, hekate-mode toggle, group-caster picker, bypass-warning checkbox (GM only)
|
||||||
|
|
||||||
|
### 8.4 Resource displays
|
||||||
|
|
||||||
|
Add to stats tab header:
|
||||||
|
- Mana / max-per-spell / per-round (existing)
|
||||||
|
- **Blood pool** (new, only if any spell with `castingMode: 'blood'` is owned)
|
||||||
|
- Zeal (existing)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Spell Item Sheet - Enhancement Spec
|
||||||
|
|
||||||
|
> Reference: [.src/foundry-system/templates/item/spell.hbs](../.src/foundry-system/templates/item/spell.hbs)
|
||||||
|
|
||||||
|
### 9.1 Add fieldsets (in order)
|
||||||
|
|
||||||
|
1. **Identyfikacja** - name, sourceBook (select), school (select), discipline (select), deity (conditional select if school=sacred), circle
|
||||||
|
2. **Tryb rzucania** - castingMode (select), bloodCost (if blood), components.symbols (tag picker against `WITCH_SYMBOLS`, free-text allowed)
|
||||||
|
3. **Trudność** - threshold, successes, manaCost, castingTimeRounds, castingTimeMinutes
|
||||||
|
4. **Zasięg & Obszar** - range, areaOfEffect (shape select + x/y inputs), targets (kept as freetext fallback)
|
||||||
|
5. **Czas trwania** - duration, nonCombatOnly (checkbox)
|
||||||
|
6. **Obrażenia** - damageBase (text), damageType (select), ignoresArmor (checkbox)
|
||||||
|
7. **Efekty statusu** - statusEffects (multi-select against `CONDITIONS`), saveAttribute + saveSkill
|
||||||
|
8. **Wymagania** - requirements.race (multi-select), .talent (multi-select), .discipline (multi-select)
|
||||||
|
9. **Flagi** - isSuperspell, requiresGroupCast, minCasters
|
||||||
|
10. **Nadczarowanie** - repeatable list of `overcastOptions[]` (description + manaPerStep)
|
||||||
|
11. **Wyzwalacze (Triggery)** - repeatable list of `triggers[]` (event select + effect text)
|
||||||
|
12. **Warianty sukcesu** - repeatable list of `variableSuccesses[]` (label + successes)
|
||||||
|
13. **Opis & komponenty fabularne** - description (HTML), higherCircles (HTML, deprecated → migrated to overcastOptions)
|
||||||
|
|
||||||
|
### 9.2 Conditional rendering (Handlebars)
|
||||||
|
|
||||||
|
- Deity select: only if `system.school === 'sacred'`
|
||||||
|
- Blood cost: only if `system.castingMode === 'blood'`
|
||||||
|
- Witch complexity / symbols: if `castingMode === 'witch'` OR Hekate sacred spell
|
||||||
|
- Variable successes: only if non-empty (collapsible)
|
||||||
|
- Triggers: only if non-empty (collapsible)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Compendium Pack Plan
|
||||||
|
|
||||||
|
### 10.1 System-bundled packs
|
||||||
|
|
||||||
|
| Pack id | Type | Contents | Estimated count |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `hbm-rpg-v3.spells-general` | Item | Magia Ogólna | 10 |
|
||||||
|
| `hbm-rpg-v3.spells-elements` | Item | Żywioły × 4 | 20 |
|
||||||
|
| `hbm-rpg-v3.spells-sacred` | Item | Sacred + 7 deities | 35 |
|
||||||
|
| `hbm-rpg-v3.spells-academic` | Item | Transmutacja, Iluzja, Nekromancja | 15 |
|
||||||
|
| `hbm-rpg-v3.spells-superspells` | Item | Superzaklęcia | 8 |
|
||||||
|
| `hbm-rpg-v3.spells-crimson` | Item | Magia Szkarłatu | 6 |
|
||||||
|
| `hbm-rpg-v3.spells-abyss` | Item | Magia Otchłani | 5 |
|
||||||
|
| `hbm-rpg-v3.spells-blood` | Item | Magia Krwi (placeholder) | 0 (TBD) |
|
||||||
|
| `hbm-rpg-v3.talents` | Item | All talents | TBD |
|
||||||
|
| `hbm-rpg-v3.disciplines` | Item | All discipline definitions | ~15 |
|
||||||
|
| `hbm-rpg-v3.classes` | Item | Class progression items | 5 |
|
||||||
|
| `hbm-rpg-v3.races` | Item | Race definitions | 8 |
|
||||||
|
| `hbm-rpg-v3.gear-baseline` | Item | Core weapons/armor/equipment | TBD |
|
||||||
|
| `hbm-rpg-v3.elixirs-baseline` | Item | Aneks C eliksiry | TBD |
|
||||||
|
| `hbm-rpg-v3.actors-bestiary` | Actor | Bestiariusz creatures | TBD |
|
||||||
|
| `hbm-rpg-v3.actors-npcs` | Actor | Bestiariusz NPCs | TBD |
|
||||||
|
|
||||||
|
### 10.2 Generation pipeline
|
||||||
|
|
||||||
|
- Source: structured YAML/JSON in `.src/foundry-system/data/spells/*.yaml` (one file per discipline)
|
||||||
|
- Build script (`scripts/build-packs.ts`) reads YAML, validates against Zod schema mirroring `SpellData`, writes LevelDB pack files into `packs/`
|
||||||
|
- Run as part of `bun run package`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Implementation Phases
|
||||||
|
|
||||||
|
### Phase A - Data model expansion (no UI changes)
|
||||||
|
1. Extend `SpellData` schema with all fields from §5.2
|
||||||
|
2. Add constants from §7
|
||||||
|
3. Write migration `0.2.0` for existing world data
|
||||||
|
4. Bump version → 0.2.0
|
||||||
|
|
||||||
|
### Phase B - Compendium seeding
|
||||||
|
1. Build YAML source files for all ~93 spells (§4) under `.src/foundry-system/data/spells/`
|
||||||
|
2. Implement `scripts/build-packs.ts`
|
||||||
|
3. Wire `bun run package` to generate packs
|
||||||
|
|
||||||
|
### Phase C - Casting logic upgrade
|
||||||
|
1. `validateCast()` pre-cast gates (§6.1)
|
||||||
|
2. New `castBlood()` mode + extend `CastOptions` (§6.2-6.3)
|
||||||
|
3. `rollSpellDamage()` + chat card overhaul (§6.4)
|
||||||
|
4. Status effect auto-apply hook (§6.5)
|
||||||
|
5. Trigger registration system (§6.6)
|
||||||
|
6. Variable success / group cast UI (§6.7-6.8)
|
||||||
|
|
||||||
|
### Phase D - Sheet UX
|
||||||
|
1. Spell item sheet enhancements (§9)
|
||||||
|
2. Character sheet spell tab grouping/filtering (§8)
|
||||||
|
3. Cast dialog overhaul
|
||||||
|
|
||||||
|
### Phase E - Quality & polish
|
||||||
|
1. Localization keys (Polish + English) for all new fields
|
||||||
|
2. CSS for badges, condition icons, source book pips
|
||||||
|
3. Foundry compendium browser metadata (image, system tags)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix A - Spell Field Naming Convention
|
||||||
|
|
||||||
|
- IDs: `kebab-case` matching slug-of-name (English-translated where idiomatic, Polish-derived otherwise)
|
||||||
|
- All UI labels: localized via `lang/pl.json` & `lang/en.json` keys under `HBM.spell.*`
|
||||||
|
- Damage formulas: lowercase camelCase tokens (`magicalAbilities`, `devotion`, `soul`, `mind`, `body`, `magic`)
|
||||||
|
- Conditions: lowercase singular (`stunned`, `prone`, `frightened`)
|
||||||
|
|
||||||
|
## Appendix B - Outstanding Questions
|
||||||
|
|
||||||
|
1. **Blood Pool resource**: should `actor.system.attributes.blood` be a new dedicated pool, or does Magia Krwi simply consume HP? Pending Arcanum Sanguinis spell list.
|
||||||
|
2. **Hekate "hybrid" persistence**: when cast as witch, should the spell consume zeal *or* mana? Currently planned: choose one per cast via `opts.hekateMode`.
|
||||||
|
3. **Detect Changes (`detect-changes`)**: spell explicitly says "no overcast"; need a `noOvercast: boolean` flag, OR encode in `overcastOptions: []` (empty = no overcast).
|
||||||
|
4. **Status effect saves**: which spells require an opposed Magia/Zdolności Magiczne test vs target's `saveAttribute + saveSkill`, and which auto-apply on TS success? Each spell needs explicit annotation in compendium YAML.
|
||||||
|
5. **Dzika Wiedźmia Magia** spells: not enumerated separately in books; mostly variants of standard witch magic with chaos overlay. Defer to dedicated content drop.
|
||||||
@@ -0,0 +1,417 @@
|
|||||||
|
# Module / System Content Split - HbM RPG v3
|
||||||
|
|
||||||
|
> **Status**: Draft / Planning Document
|
||||||
|
> **Audience**: Developer (next iteration of `hbm-rpg-v3` Foundry system + companion lore module)
|
||||||
|
> **Purpose**: Classify every chapter of every rulebook as **SYSTEM** (mechanics → Foundry system code/compendia), **MODULE** (lore → separate companion module), or **BOTH** (split content). Drives the package architecture for the v0.3+ release.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Classification Legend](#1-classification-legend)
|
||||||
|
2. [Per-Book Classification](#2-per-book-classification)
|
||||||
|
1. [Podręcznik Gry](#21-podrecznik-gry)
|
||||||
|
2. [Księga Magii](#22-ksiega-magii)
|
||||||
|
3. [Arcanum Sanguinis](#23-arcanum-sanguinis)
|
||||||
|
4. [Chwała Szkarłatnemu Kultowi](#24-chwala-szkarlatnemu-kultowi)
|
||||||
|
5. [Klątwa Otchłani](#25-klatwa-otchlani)
|
||||||
|
6. [Przewodnik Ludzkości po Magicznym Świecie](#26-przewodnik-ludzkosci-po-magicznym-swiecie)
|
||||||
|
7. [Bestiariusz](#27-bestiariusz)
|
||||||
|
8. [Złoto Stal i Magia](#28-zloto-stal-i-magia)
|
||||||
|
3. [Aggregate Summary](#3-aggregate-summary)
|
||||||
|
4. [Proposed Package Architecture](#4-proposed-package-architecture)
|
||||||
|
5. [Compendium Pack Plan (System)](#5-compendium-pack-plan-system)
|
||||||
|
6. [Module Pack Plan (Lore)](#6-module-pack-plan-lore)
|
||||||
|
7. [Cross-References & Dependencies](#7-cross-references--dependencies)
|
||||||
|
8. [Migration & Build Pipeline](#8-migration--build-pipeline)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Classification Legend
|
||||||
|
|
||||||
|
| Tag | Meaning | Destination |
|
||||||
|
|---|---|---|
|
||||||
|
| **[SYSTEM]** | Pure mechanics: rules, tables, stat blocks, character options | `hbm-rpg-v3` system package + bundled compendium packs |
|
||||||
|
| **[MODULE]** | Pure lore/setting/fiction with no mechanical impact | `hbm-rpg-v3-lore` companion module (JournalEntry packs) |
|
||||||
|
| **[BOTH]** | Mixed content - split per section: stats stay in system, narrative moves to module, cross-linked | Both packages, with bidirectional `@UUID` links |
|
||||||
|
|
||||||
|
**Decision rule**:
|
||||||
|
> If removing the content would require renumbering rules, breaking character creation, or invalidating compendium entries → **SYSTEM**.
|
||||||
|
> If removing the content would only reduce flavor/setting depth → **MODULE**.
|
||||||
|
> If both apply → **BOTH** with explicit split points.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Per-Book Classification
|
||||||
|
|
||||||
|
### 2.1 Podręcznik Gry
|
||||||
|
|
||||||
|
| Chapter | Class | Destination | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Wstęp | **MODULE** | Lore module → JournalEntry "Świat HbM - Wprowadzenie" | Narrative about the Awakening |
|
||||||
|
| I - Klasy (lata 1–5) | **SYSTEM** | Compendium: `classes` | Year-by-year unlocks |
|
||||||
|
| II - Tworzenie Postaci (rasy, dziedziny) | **SYSTEM** | Compendia: `races`, `disciplines` | Race traits, discipline picks |
|
||||||
|
| III - Atrybuty i Umiejętności | **SYSTEM** | Hard-coded in `actor-character.ts` + `constants.ts` | 4 attributes, 24 skills |
|
||||||
|
| IV - Talenty | **SYSTEM** | Compendium: `talents` | All baseline talents |
|
||||||
|
| V - Walka i Magia | **SYSTEM** | `logic/combat.ts`, `logic/spell-cast.ts` | Initiative, actions, tests |
|
||||||
|
| VI - Odpoczywanie i Leczenie | **SYSTEM** | New `logic/rest.ts` | Short/long rest mechanics |
|
||||||
|
| VII - Religia | **BOTH** | Stats → `disciplines/sacred`, deity entries; Lore → module | 7 deities |
|
||||||
|
| Aneks A - Stany Postaci | **SYSTEM** | `logic/conditions.ts` (already exists) | Status effects |
|
||||||
|
| Aneks B - Podstawowe Zaklęcia | **SYSTEM** | Compendium: `spells-general` | Superseded by Księga Magii - keep as reference variants |
|
||||||
|
| Aneks C - Podstawowe Eliksiry | **SYSTEM** | Compendium: `elixirs-baseline` | Eliksir Zdrowia + others |
|
||||||
|
| Aneks D - Cechy Broni | **SYSTEM** | `constants.ts` weapon traits | "Eksperymentalny" etc. |
|
||||||
|
|
||||||
|
**Verdict**: ~90% SYSTEM. Only the foreword and parts of Religia (deity narratives) move to MODULE.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 Księga Magii
|
||||||
|
|
||||||
|
| Chapter | Class | Destination | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Wstęp | **MODULE** | JournalEntry "Filozofia Magii" | Philosophical narrative |
|
||||||
|
| I - Dziedziny Magii | **SYSTEM** | Compendium: `disciplines` | Discipline definitions |
|
||||||
|
| II - Teoria Rzucania Zaklęć | **SYSTEM** | Hard-coded in `spell-cast.ts` + GM rules journal | Overcast, Symbolika, Variety |
|
||||||
|
| III - Warzenie Eliksirów | **SYSTEM** | Compendium: `elixirs` + new `logic/brewing.ts` | Tolerance, brewing checks |
|
||||||
|
| IV - Artefakty | **SYSTEM** | Compendium: `artifacts` | Stat blocks with upgrades |
|
||||||
|
| V - Lista Zaklęć | **SYSTEM** | Compendia: `spells-*` (multiple packs by discipline) | All ~85 spells |
|
||||||
|
| VI - Superzaklęcia | **SYSTEM** | Compendium: `spells-superspells` | 8 superspells |
|
||||||
|
| VII - Tworzenie Zaklęć | **SYSTEM** | GM journal + future macro tooling | Creation rules + tables |
|
||||||
|
| VIII - Dzika Magia Dzikich Wiedźm | **BOTH** | Sabaty/hierarchia → MODULE; chaos rules → SYSTEM | |
|
||||||
|
|
||||||
|
**Verdict**: ~95% SYSTEM. Only the foreword and witch coven lore are MODULE.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3 Arcanum Sanguinis
|
||||||
|
|
||||||
|
| Chapter | Class | Destination | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Wstęp | **MODULE** | JournalEntry "Marginalia Magii Krwi" | Narrative about marginalization |
|
||||||
|
| I - Magia Krwi a Otchłań | **MODULE** | JournalEntry chapter | Worldview, Inquisition relations |
|
||||||
|
| II - Bóg Krwi / Robert Zaryn | **MODULE** | JournalEntries + Actor doc (Robert Zaryn) | NPC & deity lore |
|
||||||
|
| III - Teoria Magii Krwi | **SYSTEM** | New `logic/blood-magic.ts` + new actor field `attributes.blood` | Blood Points, Self-Harm, Life Steal |
|
||||||
|
| IV - Talenty | **SYSTEM** | Compendium: `talents-blood` | Szacunek do Życia → Rytualista / Pojedynkowicz trees |
|
||||||
|
|
||||||
|
**Verdict**: 50/50 split - clean separation between organisational/divine lore and Blood Magic mechanics.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.4 Chwała Szkarłatnemu Kultowi
|
||||||
|
|
||||||
|
| Chapter | Class | Destination | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Wstęp | **MODULE** | JournalEntry | Mysterious organisation tone |
|
||||||
|
| I - Pogłoski | **MODULE** | JournalEntry | In-world rumors |
|
||||||
|
| II - Struktura | **MODULE** | JournalEntry + Actor folder structure | Hierarchy: Rycerz→Pretor, Kultysta→Arcykapłan |
|
||||||
|
| III - Lokalizacje | **MODULE** | JournalEntries with embedded Scenes | Obeliski, Domena Szkarłatu, Twierdze |
|
||||||
|
| IV - Zaklęcia | **SYSTEM** | Compendium: `spells-crimson` | 5 + 1 superspell |
|
||||||
|
| V - Artefakty | **BOTH** | Stats → `artifacts-crimson`; History → MODULE | Cross-link via `@UUID` |
|
||||||
|
|
||||||
|
**Verdict**: 50/50.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.5 Klątwa Otchłani
|
||||||
|
|
||||||
|
| Chapter | Class | Destination | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Wstęp | **MODULE** | JournalEntry | Nature of the Abyss |
|
||||||
|
| I - Otchłań i Przebudzenie | **MODULE** | JournalEntry | History: pre-modern traces, 2023 |
|
||||||
|
| II - Potężniejsza Magia | **SYSTEM** | `logic/abyss-magic.ts` | Aspects vs Primal mechanics |
|
||||||
|
| III - Kara za nieudolność | **SYSTEM** | Roll table compendium: `roll-tables-abyss` | Failure penalty table |
|
||||||
|
| IV - Talenty | **SYSTEM** | Compendium: `talents-abyss` | Swobodny Przepływ, Dar Otchłani, Metamagia |
|
||||||
|
| V - Dary Otchłani | **SYSTEM** | Roll table d100 in `roll-tables-abyss` | Boons/banes |
|
||||||
|
| VI - Personifikacje Otchłani | **BOTH** | Stats → `actors-abyss`; Lore → MODULE | Nil/Mundus/Tempus |
|
||||||
|
| VII - Istoty Otchłani | **SYSTEM** | Compendium: `actors-abyss` | Mistrz Losu stat block |
|
||||||
|
| VIII - Choroby Psychiczne | **SYSTEM** | New `conditions-mental` set in `conditions.ts` | Insanity mechanics |
|
||||||
|
| IX - Zaklęcia | **SYSTEM** | Compendium: `spells-abyss` | 5 spells |
|
||||||
|
| X - Artefakty | **SYSTEM** | Compendium: `artifacts-abyss` | Miotacz Entropii |
|
||||||
|
| Aneks A - Kara Mistrza Losu | **SYSTEM** | Roll table | Penalty table |
|
||||||
|
|
||||||
|
**Verdict**: 60% SYSTEM / 40% MODULE.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.6 Przewodnik Ludzkości po Magicznym Świecie
|
||||||
|
|
||||||
|
| Chapter | Class | Destination | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Wstęp | **MODULE** | JournalEntry | |
|
||||||
|
| I - Teoria Jednego Wszechświata | **MODULE** | JournalEntry | Cosmology, philosophy |
|
||||||
|
| II - Planety i Frakcje | **MODULE** | JournalEntries + Scenes | Earth, Hiondirs, Xivell, Eldrakar, Malferian |
|
||||||
|
| III - Życie na Sol-3 | **MODULE** | JournalEntry | Magical education, Inquisition |
|
||||||
|
| IV - Cywilizacja Hiondirs-4 | **MODULE** | JournalEntry + Scene | |
|
||||||
|
| V - Cywilizacja Xivell | **MODULE** | JournalEntry | Royal lineage, Crimson Cult genesis |
|
||||||
|
| VI - Cywilizacja Eldrakar | **MODULE** | JournalEntry + Scene | |
|
||||||
|
| VII - Cywilizacja Malferian | **MODULE** | JournalEntry | Consortium houses, rituals |
|
||||||
|
| VIII - Federacja Sol-3 | **MODULE** | JournalEntry + Actor folder (Special Forces X, Gaja) | Syllia Orirel etc. |
|
||||||
|
| IX - Święta Inkwizycja | **MODULE** | JournalEntry + Actor folder | Genesis, structure, ops |
|
||||||
|
| X - Zaświaty | **MODULE** | JournalEntry | Hades, Niebo, Piekło hierarchies |
|
||||||
|
| Aneks A–D | **MODULE** | JournalEntries (in-world documents) | Federation reports, Narcyza interview, Inquisitor notes |
|
||||||
|
|
||||||
|
**Verdict**: ~98% MODULE. Race information is the only borderline - but actual race stats live in Podręcznik Gry, so this book stays purely lore.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.7 Bestiariusz
|
||||||
|
|
||||||
|
| Chapter | Class | Destination | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| I - Przeciwnicy | **SYSTEM** | Compendium: `actors-bestiary` | All hostile creature stat blocks |
|
||||||
|
| II - Inne Postacie (NPC) | **BOTH** | Stats → `actors-npcs`; personality → MODULE | Cross-link via `@UUID` |
|
||||||
|
| III - Talenty | **SYSTEM** | Compendium: `talents-special` | Wspomnienie Wspaniałego Świata, Pierwszy Patriarcha |
|
||||||
|
|
||||||
|
**Verdict**: ~80% SYSTEM. NPC personality blurbs and faction histories are the MODULE pieces.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.8 Złoto Stal i Magia
|
||||||
|
|
||||||
|
| Chapter | Class | Destination | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Wstęp | **MODULE** | JournalEntry | Post-Awakening economic revolution |
|
||||||
|
| I - Geopolityka | **MODULE** | JournalEntry | USA/Chiny/Rosja/Europa post-2023 |
|
||||||
|
| II - Ekonomia Magicznego Świata | **MODULE** | JournalEntry | Sectors, dwarf monopolies |
|
||||||
|
| III - Handel Międzyrasowy | **MODULE** | JournalEntry + Scene (trade routes) | |
|
||||||
|
| IV - Polityka na Ziemi | **MODULE** | JournalEntry | Dwarf lobby, Inquisition vs trade |
|
||||||
|
| V - Bank Krasnoludzki | **MODULE** | JournalEntry + Actor (org) | |
|
||||||
|
| VI - Waluty i Systemy Płatnicze | **BOTH** | Currency table → `roll-tables-economy`; cultural notes → MODULE | |
|
||||||
|
| VII - Czarny Rynek i Kontrabanda | **BOTH** | Risk/negotiation rules → SYSTEM; catalog → MODULE | |
|
||||||
|
| VIII - Krasnoludy | **BOTH** | Race traits → existing `races` compendium; culture → MODULE | |
|
||||||
|
| IX - Mechanika Handlu dla PG | **SYSTEM** | New `logic/trade.ts` + journal | Founding company, transactions, smuggling |
|
||||||
|
| Aneks A - Nowe Talenty | **SYSTEM** | Compendium: `talents-trade` | Zmysł Kamienia, Kupiecka Intuicja, Złoty Nos |
|
||||||
|
| Aneks B - Przedmioty i Towary | **SYSTEM** | Compendium: `gear-trade` + price tables | Dwarven items, price lists |
|
||||||
|
|
||||||
|
**Verdict**: 40% SYSTEM / 60% MODULE.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Aggregate Summary
|
||||||
|
|
||||||
|
| Book | SYSTEM % | MODULE % | Primary destination |
|
||||||
|
|---|---:|---:|---|
|
||||||
|
| Podręcznik Gry | ~90 | ~10 | System (core) |
|
||||||
|
| Księga Magii | ~95 | ~5 | System (core) |
|
||||||
|
| Arcanum Sanguinis | ~50 | ~50 | Both |
|
||||||
|
| Chwała Szkarłatnemu Kultowi | ~50 | ~50 | Both |
|
||||||
|
| Klątwa Otchłani | ~60 | ~40 | Both |
|
||||||
|
| Przewodnik Ludzkości | ~2 | ~98 | Module |
|
||||||
|
| Bestiariusz | ~80 | ~20 | System |
|
||||||
|
| Złoto Stal i Magia | ~40 | ~60 | Both |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Proposed Package Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
hbm-rpg-v3/ (Foundry SYSTEM package)
|
||||||
|
├── system.json
|
||||||
|
├── src/ (TypeScript)
|
||||||
|
│ ├── data/ (TypeDataModel for actors/items)
|
||||||
|
│ ├── logic/ (combat, spell-cast, brewing, blood-magic, abyss-magic, trade, rest, conditions)
|
||||||
|
│ ├── sheets/ (character, npc, item sheets)
|
||||||
|
│ └── dice/ (TS roll system)
|
||||||
|
├── packs/ (LevelDB compendia - see §5)
|
||||||
|
└── data/ (YAML source for pack generation)
|
||||||
|
|
||||||
|
hbm-rpg-v3-lore/ (Foundry MODULE package - separate repo / sub-folder)
|
||||||
|
├── module.json (depends on hbm-rpg-v3 system)
|
||||||
|
├── packs/ (JournalEntry packs - see §6)
|
||||||
|
├── data/ (Markdown source for journal generation)
|
||||||
|
└── assets/ (images, maps, scene backgrounds)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why split?
|
||||||
|
|
||||||
|
1. **System is mandatory**, module is optional → groups running custom settings can use the system without the canonical lore.
|
||||||
|
2. **Update cadence differs**: rules patches (system) vs. lore expansions (module).
|
||||||
|
3. **Distribution rights**: lore content may have separate licensing considerations.
|
||||||
|
4. **Clean dependency graph**: module can `@UUID` link into system compendia (talents, spells, actors), but system never depends on module.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Compendium Pack Plan (System)
|
||||||
|
|
||||||
|
### 5.1 Items
|
||||||
|
|
||||||
|
| Pack id | Item type | Source | Est. count |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `hbm-rpg-v3.spells-general` | spell | Księga Magii I-V | 10 |
|
||||||
|
| `hbm-rpg-v3.spells-elements` | spell | Księga Magii V | 20 |
|
||||||
|
| `hbm-rpg-v3.spells-sacred` | spell | Księga Magii V | 35 |
|
||||||
|
| `hbm-rpg-v3.spells-academic` | spell | Transmutacja, Iluzja, Nekromancja | 15 |
|
||||||
|
| `hbm-rpg-v3.spells-superspells` | spell | Księga Magii VI | 8 |
|
||||||
|
| `hbm-rpg-v3.spells-crimson` | spell | Chwała Szkarłatnemu Kultowi | 6 |
|
||||||
|
| `hbm-rpg-v3.spells-abyss` | spell | Klątwa Otchłani | 5 |
|
||||||
|
| `hbm-rpg-v3.spells-blood` | spell | Arcanum Sanguinis | TBD |
|
||||||
|
| `hbm-rpg-v3.talents-core` | talent | Podręcznik Gry IV | TBD |
|
||||||
|
| `hbm-rpg-v3.talents-blood` | talent | Arcanum Sanguinis IV | TBD |
|
||||||
|
| `hbm-rpg-v3.talents-abyss` | talent | Klątwa Otchłani IV | TBD |
|
||||||
|
| `hbm-rpg-v3.talents-trade` | talent | Złoto Stal i Magia A | TBD |
|
||||||
|
| `hbm-rpg-v3.talents-special` | talent | Bestiariusz III | TBD |
|
||||||
|
| `hbm-rpg-v3.disciplines` | discipline | Księga Magii I | ~15 |
|
||||||
|
| `hbm-rpg-v3.classes` | class | Podręcznik Gry I | 5 |
|
||||||
|
| `hbm-rpg-v3.races` | race | Podręcznik Gry II + Złoto Stal i Magia VIII | 8 |
|
||||||
|
| `hbm-rpg-v3.gear-baseline` | gear | Podręcznik Gry | TBD |
|
||||||
|
| `hbm-rpg-v3.gear-trade` | gear | Złoto Stal i Magia B | TBD |
|
||||||
|
| `hbm-rpg-v3.elixirs-baseline` | gear | Podręcznik Gry C | TBD |
|
||||||
|
| `hbm-rpg-v3.elixirs` | gear | Księga Magii III | TBD |
|
||||||
|
| `hbm-rpg-v3.artifacts` | gear | Księga Magii IV | TBD |
|
||||||
|
| `hbm-rpg-v3.artifacts-crimson` | gear | Chwała Szkarłatnemu Kultowi V | TBD |
|
||||||
|
| `hbm-rpg-v3.artifacts-abyss` | gear | Klątwa Otchłani X | TBD |
|
||||||
|
|
||||||
|
### 5.2 Actors
|
||||||
|
|
||||||
|
| Pack id | Actor type | Source | Est. count |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `hbm-rpg-v3.actors-bestiary` | npc | Bestiariusz I | TBD |
|
||||||
|
| `hbm-rpg-v3.actors-npcs` | npc | Bestiariusz II | TBD |
|
||||||
|
| `hbm-rpg-v3.actors-abyss` | npc | Klątwa Otchłani VI–VII | TBD |
|
||||||
|
|
||||||
|
### 5.3 Roll tables
|
||||||
|
|
||||||
|
| Pack id | Source |
|
||||||
|
|---|---|
|
||||||
|
| `hbm-rpg-v3.roll-tables-abyss` | Klątwa Otchłani III, V, Aneks A |
|
||||||
|
| `hbm-rpg-v3.roll-tables-economy` | Złoto Stal i Magia VI–VII |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Module Pack Plan (Lore)
|
||||||
|
|
||||||
|
### 6.1 JournalEntry packs
|
||||||
|
|
||||||
|
| Pack id | Source |
|
||||||
|
|---|---|
|
||||||
|
| `hbm-rpg-v3-lore.world-overview` | Podręcznik Gry Wstęp + Przewodnik I |
|
||||||
|
| `hbm-rpg-v3-lore.deities` | Podręcznik Gry VII (lore parts) + Klątwa Otchłani VI |
|
||||||
|
| `hbm-rpg-v3-lore.universe` | Przewodnik II–VII (planets & civilizations) |
|
||||||
|
| `hbm-rpg-v3-lore.sol3-society` | Przewodnik III, VIII, IX |
|
||||||
|
| `hbm-rpg-v3-lore.afterlife` | Przewodnik X |
|
||||||
|
| `hbm-rpg-v3-lore.documents` | Przewodnik Aneksy A–D |
|
||||||
|
| `hbm-rpg-v3-lore.blood-magic-history` | Arcanum Sanguinis Wstęp + I + II |
|
||||||
|
| `hbm-rpg-v3-lore.crimson-cult` | Chwała Szkarłatnemu Kultowi Wstęp, I, II, III, V (history) |
|
||||||
|
| `hbm-rpg-v3-lore.abyss` | Klątwa Otchłani Wstęp, I, VI (lore) |
|
||||||
|
| `hbm-rpg-v3-lore.witch-covens` | Księga Magii VIII (lore parts) |
|
||||||
|
| `hbm-rpg-v3-lore.economy` | Złoto Stal i Magia I–VIII (narrative parts) |
|
||||||
|
| `hbm-rpg-v3-lore.npc-personalities` | Bestiariusz II (personality parts) |
|
||||||
|
|
||||||
|
### 6.2 Scene packs (asset-bundled)
|
||||||
|
|
||||||
|
| Pack id | Source |
|
||||||
|
|---|---|
|
||||||
|
| `hbm-rpg-v3-lore.scenes-locations` | Locations folder + Chwała III + Przewodnik IV–VII |
|
||||||
|
|
||||||
|
### 6.3 Macro packs (optional)
|
||||||
|
|
||||||
|
- Quick-roll macros for common GM rolls
|
||||||
|
- Lore lookup macros (search across journal compendia)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Cross-References & Dependencies
|
||||||
|
|
||||||
|
### 7.1 Dependency direction
|
||||||
|
|
||||||
|
```
|
||||||
|
hbm-rpg-v3-lore ──depends-on──▶ hbm-rpg-v3 (system)
|
||||||
|
│
|
||||||
|
└─▶ no upward dependency
|
||||||
|
```
|
||||||
|
|
||||||
|
`module.json` declaration:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "hbm-rpg-v3-lore",
|
||||||
|
"title": "HbM RPG v3 - Lore Companion",
|
||||||
|
"system": "hbm-rpg-v3",
|
||||||
|
"compatibility": { "minimum": "13", "verified": "13" },
|
||||||
|
"relationships": {
|
||||||
|
"systems": [{ "id": "hbm-rpg-v3", "type": "system", "compatibility": { "minimum": "0.3.0" } }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 Cross-link patterns
|
||||||
|
|
||||||
|
- Lore JournalEntry references SYSTEM compendium item:
|
||||||
|
`@UUID[Compendium.hbm-rpg-v3.spells-crimson.{id}]{Szkarłatny Pocisk}`
|
||||||
|
- SYSTEM item description references lore JournalEntry:
|
||||||
|
`@UUID[Compendium.hbm-rpg-v3-lore.crimson-cult.JournalEntry.{id}.JournalEntryPage.{pageId}]{Szkarłatny Kult}`
|
||||||
|
- NPC actor (SYSTEM) → personality journal (MODULE) via biography `@UUID`
|
||||||
|
|
||||||
|
### 7.3 Stable IDs
|
||||||
|
|
||||||
|
All compendium documents must have **stable IDs** (slug-derived) to prevent cross-link breakage on rebuilds:
|
||||||
|
- Spells: `spell-{slug}` (e.g. `spell-magic-missile`)
|
||||||
|
- Talents: `talent-{slug}`
|
||||||
|
- NPCs: `npc-{slug}`
|
||||||
|
- Lore: `lore-{book}-{chapter}-{slug}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Migration & Build Pipeline
|
||||||
|
|
||||||
|
### 8.1 Source-of-truth strategy
|
||||||
|
|
||||||
|
Each domain has a **machine-readable source file** under `.src/foundry-system/data/` (system) or `.src/foundry-lore/data/` (module). The Foundry pack files (`packs/*.db` or LevelDB folders) are **build artifacts**, not source.
|
||||||
|
|
||||||
|
| Source format | Domain |
|
||||||
|
|---|---|
|
||||||
|
| YAML | spells, talents, gear, races, classes, disciplines |
|
||||||
|
| YAML + Markdown | NPCs / actors (YAML stat block + Markdown description) |
|
||||||
|
| Markdown | JournalEntry pages |
|
||||||
|
| JSON | RollTables |
|
||||||
|
|
||||||
|
### 8.2 Build scripts
|
||||||
|
|
||||||
|
| Script | Output |
|
||||||
|
|---|---|
|
||||||
|
| `scripts/build-spell-packs.ts` | All `spells-*` LevelDB packs |
|
||||||
|
| `scripts/build-actor-packs.ts` | All `actors-*` LevelDB packs |
|
||||||
|
| `scripts/build-journal-packs.ts` | All `lore.*` packs (in module repo) |
|
||||||
|
| `scripts/build-roll-tables.ts` | All `roll-tables-*` packs |
|
||||||
|
| `scripts/build-all.ts` | Wraps all above; called by `bun run package` |
|
||||||
|
|
||||||
|
### 8.3 Migration scripts
|
||||||
|
|
||||||
|
When `SpellData` / `ActorData` schemas change, write a `src/migrations/{version}.ts` that runs on world load (Foundry `Hooks.once('ready')`).
|
||||||
|
|
||||||
|
### 8.4 Validation
|
||||||
|
|
||||||
|
- Zod (or Foundry's own DataModel) schema validation in build scripts → fail build on invalid spell/actor entry
|
||||||
|
- Cross-reference linter: ensures every `@UUID[...]` in lore points to a real system compendium entry
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix A - Implementation Order
|
||||||
|
|
||||||
|
1. **v0.2.0 - System foundation**
|
||||||
|
- Expand `SpellData` schema (per `foundry-spell-reference.md` §5)
|
||||||
|
- Build `spells-*` packs from YAML sources
|
||||||
|
- Migration 0.1 → 0.2 for existing world spells
|
||||||
|
2. **v0.3.0 - Compendium completeness**
|
||||||
|
- All talent / discipline / race / class packs built
|
||||||
|
- Bestiariusz actors imported
|
||||||
|
3. **v0.4.0 - Mechanics expansion**
|
||||||
|
- `logic/blood-magic.ts`, `logic/abyss-magic.ts`, `logic/brewing.ts`, `logic/rest.ts`, `logic/trade.ts`
|
||||||
|
- Status conditions expanded (mental illnesses)
|
||||||
|
4. **v0.5.0 - Lore module 1.0**
|
||||||
|
- Companion module `hbm-rpg-v3-lore` first release
|
||||||
|
- Przewodnik Ludzkości fully ported
|
||||||
|
- Cross-links established
|
||||||
|
5. **v1.0.0 - Polish & full content**
|
||||||
|
- All 8 books represented
|
||||||
|
- English localization complete
|
||||||
|
- Compendium browser metadata, icons, art
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix B - Outstanding Questions
|
||||||
|
|
||||||
|
1. **Module repo location**: separate Git repo or sub-folder under `.src/`? Recommendation: separate folder `.src/foundry-lore/` for unified editing, separate `module.json` package output.
|
||||||
|
2. **Asset licensing**: scene images, NPC portraits - which assets can ship in the module vs. be GM-supplied?
|
||||||
|
3. **Polish-only vs bilingual**: should lore JournalEntries ship Polish-only initially, with English translation pack later?
|
||||||
|
4. **Adventure content** (`adventures/` folder): adventure scenarios are neither system nor module content - recommend a third package `hbm-rpg-v3-adventures` per published adventure.
|
||||||
|
5. **Foundry Module Marketplace listing**: needs cover art, description, screenshots before module submission.
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
# Plan: HbM RPG v3 - Comprehensive Lore Buildout
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
Phased, multi-batch buildout to bring the vault to player- and GM-ready status. We migrate canon from `_books/` into the vault (which becomes the new source of truth), make `concepts/` the canonical short-form definitions while `lore/` essays become longform narrative, fill empty stubs (races, organizations, disciplines), and introduce missing structural elements (in-world calendar, magical education, religion frameworks, tech-level table, gazetteer). Each phase is a reviewable batch; you approve before the next starts. A final pass audits all wikilinks/backlinks vault-wide.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Canon Decisions (locked in this plan)
|
||||||
|
|
||||||
|
These are now canon and must be reflected wherever relevant:
|
||||||
|
|
||||||
|
- **The Abyss / Otchłań** - both a cosmic force AND a fundamental law of reality. Originally a single being **named Abyss**; she split into Abyss + **Mundus**, then **Tempus** split off. The remaining part of Abyss became known as **Nil** - therefore Nil is the eldest of the three. Nil/Mundus/Tempus are not avatars - they are the **consciousness of the Abyss itself**.
|
||||||
|
- **Posłańcy (Harbingers)** - mortal vessels chosen by the Personifikacje. Once chosen, a Posłaniec is **transformed and quasi-immortal**; the slot only frees if the Personifikacja wills it. **Each Personifikacja has exactly 2 Posłańców**, so 6 slots total, each tied to a distinct domain:
|
||||||
|
- **Nil** (Przeszłość i Przyszłość):
|
||||||
|
- **Posłaniec Przeszłości** = Diana Koniecpolska
|
||||||
|
- **Posłaniec Przyszłości** = open hook
|
||||||
|
- **Mundus** (Życie i Teraźniejszość):
|
||||||
|
- **Posłaniec Życia** = **Szaarael** (sukkubica; also the demonic VP controlling the USA; Harbinger of Love)
|
||||||
|
- **Posłaniec Mutacji** = open hook (hinted form: naga/lamia)
|
||||||
|
- **Tempus** (Czas - kierunek biegu):
|
||||||
|
- **Posłaniec Cofnięcia** = **Yssariel** (elfka; Harbinger of Pain)
|
||||||
|
- **Posłaniec Postępu** = open hook (hinted form: elf)
|
||||||
|
Polish term throughout: **Posłaniec / Posłańcy**. English (internal/notes only): Harbinger.
|
||||||
|
- **USA Blackout** - officially unknown. Truth: the demonic VP is **Szaarael**, Posłaniec Życia Mundusa (Harbinger of Love). A sukkubica who assumed the VP role to extend Mundus's influence over the USA's magical and social development. "Harbinger of Love" reflects her domain over human connection, desire, and societal bonds.
|
||||||
|
- **Diana Koniecpolska** - ambition + research-induced madness; Nil began teaching her in dreams; she has now become Posłaniec Nil (Posłaniec Przeszłości).
|
||||||
|
- **Robert Zaryn** - technically the only mage truly worthy of "Blood God"; his body was destroyed and mind shattered centuries ago. Other powerful blood mages have since adopted the title. His sole living enemy is **Xillith / First Patriarch / Bezimienny**. Current goal: regain power and manipulate Federation Sol-3 into cleansing Xivell.
|
||||||
|
- **Xillith / First Patriarch / Bezimienny** (the elder) - Nadworny Taumaturg of Królestwo Xivell. Created the parasite that caused the Upadek. Struck down by Nil; only **partially** erased from reality - thanks to his artifacts he persists as a voice in his Avatars' minds and can take physical control of their bodies. After his death, taumaturgia splintered into the modern magic schools; his students founded the Szkarłatny Kult. **Current goal:** seize control of the Szkarłatny Kult. *Disambiguation: do **not** confuse with the modern student-NPC of the same name.*
|
||||||
|
- **Xillith (the student)** - separate, modern character. Member of the Szkarłatny Kult and student at Wschodnioeuropejska Szkoła Magii. Distinct file from the elder Xillith. Both pages must cross-link with a clear "Nie mylić z…" disambiguation note.
|
||||||
|
- **Xivell parasite** - magical bioweapon engineered by **the elder Xillith** to destroy Królestwo Zaryn during the inter-Xivell war; spiraled out of control and consumed the entire planet (the Upadek). Lore inspiration: Blightfall (Minecraft modpack).
|
||||||
|
- **Karolina Fey** - philosophical schism with her mother (unnamed). Mother *harmonized* (stabilize reality by appeasing the Abyss). Karolina *commands* (bends the Abyss to the mage's will). Karolina destroyed her mother **body and soul**. Abyss Sabat now follows her **out of fear** - you don't disobey someone proven capable of destroying planets.
|
||||||
|
- **Inkwizycja** - historically affiliated with the Church; separated thanks to a cardinal (canonical event documented in books). Now operates as a largely secular **world-wide magic police**, retaining religious aesthetics and some institutional ties.
|
||||||
|
- **Zakon Taumaturgów** - *not* a serious order. Full name: **Koło Wzajemnej Pomocy dla Uczniów Słabiej Utalentowanych Magicznie**. A student club at Wschodnioeuropejska Szkoła Magii for magically weak students whose discipline is treated as a sub-branch of taumaturgia. Tone: comedic / mundane. Not a power-broker faction - earlier assumptions were wrong.
|
||||||
|
- **Bill Cipher and Co.** - explicit *Gravity Falls* reference; tone is light/parodic relative to the rest of the lore.
|
||||||
|
- **Wielka Trzynastka** - the 13 founding magic schools. The core book specifies their **physical locations** but not in-universe names. **Wschodnioeuropejska Szkoła Magii** is one of them, anchored in a sub-dimension moored south of Kraków on the city's edge; divided into language-based sections. Other 12 schools' names = open hooks (location-only canon).
|
||||||
|
- **First War with Malferian Consortium (2022–2023)** - Federation Sol-3 actually struck first; official narrative blames Malferian aggression. Real motive: prevent anyone from slowing Sol-3 expansion.
|
||||||
|
- **Xivell colonization** - true reasons: secret magical knowledge buried on the planet + strategic position. The parasite plague is acceptable cost.
|
||||||
|
- **Calendar:** **Era Przebudzenia (EP)**, **EP 0 = 2023** (16.IV.2023 Awakening). Current = **EP 3 (2026)**. Convention: Polish "Era Przebudzenia" / abbrev "EP".
|
||||||
|
- **Federation Sol-3 / Malferian War truth visibility:** known to (a) top Federation leadership and Malferian Houses, and (b) leaked into conspiracy circles as deniable rumor. Plant subtle hints in conspiracy-adjacent NPC/org notes; keep mainstream lore on the official narrative.
|
||||||
|
- **Race source policy:** for races without rich vault content (Demon, Anioł, Feles, Lamia, Malferianin), **synthesize from `_books/`** during Phase 3 - no freestyle invention.
|
||||||
|
- **Named NPC canonicity:** Syllia Orirel, Serioża Żukow, Pelagius Caudex, Aurora Nylabelle, Thingrim Magnarson, Princess Zayis, Prince Arkthar, and the four Malferian Houses (Tyrris, Veyran, Kael, Serath) are all canonical names - keep verbatim.
|
||||||
|
- **Robert Zaryn alias** - "Ten, Który jest Mroczny" is an alias/title, same NPC. Canonical file: `npcs/Magowie Krwi/Ten, Który jest Mroczny.md` (aliases: Robert Zaryn, Bóg Krwi, Król Królestwa Zaryn). Do not create a separate file - the existing NPC file already uses the Zaryn aliases.
|
||||||
|
- **Aurora Nylabelle** - full canonical name. NPC file `npcs/Uczniowie/Aurora.md` carries `aliases: [Aurora Nylabelle, Księżniczka Aurora]`. The surname is canon and must be used in all cross-references.
|
||||||
|
- **Szaarael** - Posłaniec Życia (Mundus), sukkubica, demonic VP of USA. NPC file: `npcs/Kult Otchłani/Szaarael.md`.
|
||||||
|
- **Yssariel** - Posłaniec Cofnięcia (Tempus), elfka, Harbinger of Pain. NPC file: `npcs/Kult Otchłani/Yssariel.md`.
|
||||||
|
- **Plan file canonical location** - `.github/plans/lore-update-2026-05-06.md` (working copy may also exist at `_plans/` during editing).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Structural Decisions (locked)
|
||||||
|
|
||||||
|
- **`concepts/` = canonical short definitions.** In-world encyclopedia voice. Each concept page = crisp definition + summary + links to longform.
|
||||||
|
- **`lore/` = longform narrative essays.** Mixed voice (in-world body + GM callouts). Numbered files are author/GM-facing deep dives.
|
||||||
|
- **Voice:** mixed - in-world body text with `> [!gm]` (or equivalent) callouts for spoilers, secret truths, and Harbinger reveals.
|
||||||
|
- **Books → vault migration:** lift-and-restructure. Copy from `_books/`, rewrite for vault structure, stay close to source.
|
||||||
|
- **Wikilinks:** fix-as-you-go is allowed but a **dedicated final phase** does the vault-wide audit.
|
||||||
|
- **New structures to introduce:** in-world calendar, magical education system, per-race religion/spirituality, tech-level comparison table, city/geography gazetteer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phases
|
||||||
|
|
||||||
|
Each phase ends with a review checkpoint. You approve before the next starts.
|
||||||
|
|
||||||
|
### Phase 0 - Templates & Conventions (foundation, 1 batch)
|
||||||
|
|
||||||
|
Establish the patterns everything else uses.
|
||||||
|
|
||||||
|
- Create `_templates/Rasa.template.md` (race) - sections: krótki opis, biologia/morfologia, kultura, religia/duchowość, język, historia, relacje z innymi rasami, magia/wrażliwość na Otchłań, znani przedstawiciele, hooks dla MG.
|
||||||
|
- Create `_templates/Dyscyplina.template.md` (magic discipline) - sections: definicja (in-world), historia szkoły, filozofia/założenia, poddyscypliny, znani praktycy, status prawny (Inkwizycja), mechanika (krótko + link), GM callout.
|
||||||
|
- Create `_templates/Koncept.template.md` (concept) - sections: definicja, etymologia/pochodzenie nazwy, znaczenie w grze, powiązania, dalsze lektury (linki do `lore/`).
|
||||||
|
- Define the GM callout convention (Obsidian admonition / `> [!gm]` block). Document in `README.md` or a small `_templates/_styleguide.md`.
|
||||||
|
- Define the in-world calendar: e.g. **Era Przebudzenia** (EP) - Year 0 = 16.IV.2023; current = EP 3 (2026). Document conversion in calendar concept page.
|
||||||
|
|
||||||
|
**Verification:** templates exist; styleguide exists; calendar concept page exists; one example race file (e.g. Człowiek) drafted using the new template as proof.
|
||||||
|
|
||||||
|
### Phase 1 - Concepts (the canonical layer)
|
||||||
|
|
||||||
|
Make `concepts/` the source of truth that wikilinks resolve to.
|
||||||
|
|
||||||
|
- Fill `concepts/Otchłań.md` with the new canon (Abyss-as-being → split into Nil/Mundus/Tempus; cosmic force + fundamental law). Link to `lore/02. Otchłań i Magia.md` for the longform.
|
||||||
|
- Fill `concepts/Szkoły Magii.md` as an index of all 13+ disciplines with one-line summaries and links.
|
||||||
|
- Fill `concepts/Teoria Jednego Wszechświata.md` - extract from `lore/10.`, summarize, link.
|
||||||
|
- New concepts to create:
|
||||||
|
- `concepts/Posłańcy.md` (the six Posłańcy - 2 per Personifikacja; Past, Życie, Tempus pairs; what they are; named slots vs open hooks)
|
||||||
|
- `concepts/Personifikacje Otchłani.md` (Nil, Mundus, Tempus as consciousness fragments)
|
||||||
|
- `concepts/Era Przebudzenia.md` (calendar)
|
||||||
|
- `concepts/Magiczna Edukacja.md` (Wielka Trzynastka academies, durations, certification, Inquisition licensing)
|
||||||
|
- `concepts/Równowaga.md` (Equilibrium - what breaks it, consequences)
|
||||||
|
- Refactor `lore/02.` and `lore/10.` to **link to** the concept pages rather than re-defining inline.
|
||||||
|
|
||||||
|
**Verification:** every empty concept file now has content; `[[Otchłań]]` and `[[Teoria Jednego Wszechświata]]` resolve to real definitions; new concept files exist; longform `lore/` essays still read coherently after links inserted.
|
||||||
|
|
||||||
|
### Phase 2 - Disciplines (parity pass)
|
||||||
|
|
||||||
|
Bring all magic schools to comparable depth using the discipline template + `_books/Księga Magii.md` as source.
|
||||||
|
|
||||||
|
- **Empty → fill:** Nekromancja, Magia Runiczna, Taumaturgia, Golemancja, Magia Szkarłatu, Magia Otchłani.
|
||||||
|
- **Thin → expand:** Botanika, Alchemia, Magia Iluzji, Magia Żywiołów, Magia Sakralna.
|
||||||
|
- **Already strong (use as templates):** Dzika Wiedźmia Magia, Wiedźmia Magia, Magia Krwi, Rzemiosło Artefaktów. Light pass to add GM callouts and align format.
|
||||||
|
- For each: history, philosophy, famous practitioners (link to NPC file even if NPC stub), legal status (Inkwizycja stance), mechanics summary + cross-link to book.
|
||||||
|
- Special canon insertions:
|
||||||
|
- **Magia Krwi**: encode Zaryn canon (body destroyed, mind shattered, "Blood God" title diluted, enemy = First Patriarch).
|
||||||
|
- **Magia Otchłani**: encode the harmonize-vs-command schism; reference Karolina Fey vs her mother as case study.
|
||||||
|
- **Rzemiosło Artefaktów**: Diana subplot - her temple-generator tech, her descent, her becoming Harbinger of Past (GM callout).
|
||||||
|
|
||||||
|
**Verification:** every discipline page has the same section skeleton; word-count variance reduced (target: 600–1500 words each); every discipline references at least one named practitioner; GM callouts present where canon reveals exist.
|
||||||
|
|
||||||
|
### Phase 3 - Races (player-facing essentials)
|
||||||
|
|
||||||
|
Use `_templates/Rasa.template.md` and source from `lore/10. Przewodnik Ludzkości…` plus `_books/`.
|
||||||
|
|
||||||
|
- Fill: Człowiek, Elf (split notes for Wysokie/Leśne in subsections), Krasnolud, Demon, Anioł, Feles, Lamia, Malferianin.
|
||||||
|
- For each: biology, culture, religion (per phase decision), language notes, history with the Federation, magical affinities, notable NPCs, GM hooks.
|
||||||
|
- Cross-link from race pages to relevant locations (Elves ↔ Hiondirs-4, Lamiae ↔ Eldrakar, Malferianie ↔ Konsorcjum) and disciplines (Krasnoludy ↔ Magia Runiczna).
|
||||||
|
|
||||||
|
**Verification:** all 8 race files have full template completion; each links to its home location, primary discipline, and at least one organization.
|
||||||
|
|
||||||
|
### Phase 4 - Organizations (faction depth)
|
||||||
|
|
||||||
|
Fill the four critical stubs and lightly polish the strong ones.
|
||||||
|
|
||||||
|
- **Szkarłatny Kult** - full structure (13 Pretorów, Knights/Priests duality, Obsidian Obelisks, Crimson Domain). **Origin canon:** founded by Xillith's surviving students after the Upadek to preserve taumaturgical knowledge, though the cult has since lost most of its original purpose. **Current threat:** Xillith himself is trying to seize control via his Avatars. First Matriarch ambiguity preserved. Source: `lore/05.` + `_books/Chwała Szkarłatnemu Kultowi.md`.
|
||||||
|
- **Kult Otchłani** (Zakon Otwartego Oka) - theology of Personifikacje, Diana as Posłaniec Przeszłości (GM callout), Nil/Mundus/Tempus worship distinctions, Diana subplot.
|
||||||
|
- **Inkwizycja** - historical Church affiliation, cardinal-led separation, current role as world-wide magic police; selective enforcement doctrine; relationship to all magic schools; jurisdictional limits. Lift the Church-separation event from `_books/`.
|
||||||
|
- **Zakon Taumaturgów** - *recategorize as a minor / comedic entity*, not a major faction. Document as the school club at Wschodnioeuropejska Szkoła Magii ("Koło Wzajemnej Pomocy…"). Move the file to a more appropriate location if needed (e.g. as a sub-page of the school) - flag this for review during the phase.
|
||||||
|
- **Federacja Sol-3** - append GM callout encoding the truth about who started the Malferian War + Xivell's true purpose.
|
||||||
|
- **Konsorcjum Gwiezdne Malferian** - append the "they didn't start it" reveal as GM callout; document remaining 5 systems, House dynamics, war-II potential as in-world tension.
|
||||||
|
- **Bill Cipher and Co.** - light expansion; document imprisonment & residual threat.
|
||||||
|
|
||||||
|
**Verification:** every organization file has stat-block + body + GM callout; all reference their key NPCs; the Malferian War narrative is coherent across `Federacja`, `Konsorcjum`, and `conflicts/Pierwsza Wojna…` pages.
|
||||||
|
|
||||||
|
### Phase 5 - NPCs, Locations, Gazetteer
|
||||||
|
|
||||||
|
Connect the world.
|
||||||
|
|
||||||
|
- **Named NPCs** to create or expand: Diana Koniecpolska (Posłaniec Przeszłości reveal), Karolina Fey (command/harmonize doctrine), Robert Zaryn, **Xillith the Elder / First Patriarch / Bezimienny** (with Avatar control mechanic), **Xillith the Student** (separate file, Szkarłatny Kult member, Wschodnioeuropejska Szkoła Magii student - must include disambiguation note), Syllia Orirel, Serioża Żukow, Pelagius Caudex, Aurora Nylabelle, Thingrim Magnarson, the demonic VP / Posłaniec Życia (unnamed), Princess Zayis, Prince Arkthar.
|
||||||
|
- **Locations to add:** Eldrakar (mentioned but no file), Khazad-Morul (dwarven homeworld), Kraków (gateway to Hiondirs), Zürich (magical stock exchange), the Wielka Trzynastka academies as a gazetteer page or per-school stubs.
|
||||||
|
- **Tech-level comparison table** - single page comparing Earth / Federation / Krasnoludy / Elfy / Malferianie across categories (energy, transport, weapons, communication, magic-tech fusion).
|
||||||
|
- **Sub-conflicts** - short pages or sections for Kashmir / Nile / Taiwan crises mentioned in `lore/03.`.
|
||||||
|
|
||||||
|
**Verification:** every NPC referenced in disciplines/organizations resolves to a file; new locations exist with infobox + body; tech table exists and is referenced from at least 3 race/org pages.
|
||||||
|
|
||||||
|
### Phase 6 - Wikilink & Backlink Audit
|
||||||
|
|
||||||
|
Vault-wide cleanup.
|
||||||
|
|
||||||
|
- Scan all `[[wikilinks]]` for unresolved targets; create stubs or fix targets.
|
||||||
|
- Verify each major concept has reasonable backlinks (Dataview-style or manual).
|
||||||
|
- Verify Obsidian aliases are set where folder-paths might create ambiguity.
|
||||||
|
- Update `HBM_BOOKS_REFERENCE.md` and the root index files (`classes.md`, `concepts.md`, `disciplines.md`, etc.) to reflect new content.
|
||||||
|
|
||||||
|
**Verification:** zero red wikilinks vault-wide; root index files list everything; spot-check 10 random pages - all internal links work and lead to relevant content.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Relevant files (high-traffic)
|
||||||
|
|
||||||
|
- `_templates/` - new templates land here (Rasa, Dyscyplina, Koncept) plus styleguide.
|
||||||
|
- `concepts/` - becomes the canonical layer; all 3 existing files filled, 5 new ones added.
|
||||||
|
- `lore/02.` and `lore/10.` - refactored to link to concepts instead of duplicating definitions.
|
||||||
|
- `disciplines/` - 6 empty files filled, 5 thin files expanded, 4 strong files polished.
|
||||||
|
- `races/` - all 8 files written from scratch using the template.
|
||||||
|
- `organizations/Szkarłatny Kult.md`, `organizations/Kult Otchłani.md`, `organizations/Inkwizycja.md`, `organizations/Zakon Taumaturgów.md` - full rewrites.
|
||||||
|
- `organizations/Federacja Sol-3.md`, `organizations/Konsorcjum Gwiezdne Malferian.md` - append GM-callout reveals.
|
||||||
|
- `npcs/` - multiple new NPC files; Diana, Karolina, Zaryn, First Patriarch are highest priority.
|
||||||
|
- `locations/` - Eldrakar, Khazad-Morul, Kraków, Zürich added.
|
||||||
|
- Root index files (`concepts.md`, `disciplines.md`, `races.md`, `organizations.md`, `locations.md`, `npcs.md`, `classes.md`) - refreshed in Phase 6.
|
||||||
|
- `_books/` - read-only reference; content lifted-and-restructured into vault.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope boundaries
|
||||||
|
|
||||||
|
**In scope:** lore content, in-world voice, GM callouts, structural pages (calendar, education, gazetteer), wikilink hygiene, templates.
|
||||||
|
|
||||||
|
**Out of scope (separate plans if you want them):**
|
||||||
|
- Mechanics/crunch rewriting (the `rules/` and Foundry system in `.src/foundry-system/`).
|
||||||
|
- Adventure module writing (`adventures/`).
|
||||||
|
- Tabletop card layouts (`tabletop-cards/`).
|
||||||
|
- Translation of any content to English.
|
||||||
|
- Re-canonizing material against the books beyond what's needed to fill stubs (no second-guessing existing canon unless you flag it).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Further considerations (resolved)
|
||||||
|
|
||||||
|
1. **Open Posłaniec slots** - 3 slots remain unfilled: Posłaniec Przyszłości (Nil), Posłaniec Mutacji (Mundus), Posłaniec Postępu (Tempus). Szaarael (Życia) and Yssariel (Cofnięcia) are now named canon alongside Diana (Przeszłości). Left as adventure hooks; the 2-per-Personifikacja rule is canon. Documented in `concepts/Posłańcy.md`.
|
||||||
|
2. **Calendar adoption depth** - keep Gregorian dates, append EP in parentheses (e.g. "16.IV.2023 (EP 0)"). Backwards-compatible.
|
||||||
|
3. **GM callout syntax** - `> [!gm]+ Tylko dla MG` (collapsible Obsidian callout).
|
||||||
|
4. **Tempus Posłańcy names** - **Posłaniec Cofnięcia / Posłaniec Postępu**.
|
||||||
|
5. **Templates** - include YAML frontmatter (tags, aliases, status) matching existing strong pages.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task Checklist
|
||||||
|
|
||||||
|
- [x] **Krok 1 - Uzgodnienie planu** - plan skonsolidowany, decyzje kanoniczne zatwierdzone, lista zadań ustalona.
|
||||||
|
- [x] **Krok 2 - Głęboki pass dyscyplin** - Magia Krwi, Magia Otchłani, Taumaturgia, Rzemiosło Artefaktów zaktualizowane zgodnie z kanonem.
|
||||||
|
- [x] **Krok 3 - Organizacje** - Federacja Sol-3, Konsorcjum, Inkwizycja, Szkarłatny Kult, Kult Otchłani, Bill Cipher and Co. zaktualizowane.
|
||||||
|
- [x] **Krok 4 - Głęboki pass ras** - Krasnolud, Lamia, Malferianin rozbudowane; błąd Feles Aurora naprawiony.
|
||||||
|
- [x] **Krok 5 - Zaległości NPC** - Diana Koniecpolska, Ten Który jest Mroczny, Karolina Fey, Bezimienny Pierwszy Patriarcha, Xillith Isherwood, Szaarael, Yssariel, Thingrim Magnarson, Princess Zayis, Prince Arkthar ukończone.
|
||||||
|
- [x] **Krok 6 - Odniesienia do tabeli tech** - dodane do Krasnolud, Malferianin, Konsorcjum.
|
||||||
|
- [ ] **Krok 7 - Audyt Fazy 6** - odświeżenie indeksu, poprawki YAML frontmatter, audyt wikilinków.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stan na 2026-05-22
|
||||||
|
|
||||||
|
Sesja zakończyła się sukcesem. Ukończono kroki 1–6:
|
||||||
|
|
||||||
|
- **Plan** (krok 1) uzgodniony i skonsolidowany na początku sesji.
|
||||||
|
- **Dyscypliny** (krok 2): Magia Krwi, Magia Otchłani, Taumaturgia i Rzemiosło Artefaktów wzbogacone o sekcje historii, filozofii, znanych praktyków oraz callout-y GM z kanonem (Zaryn, Karolina Fey, Diana).
|
||||||
|
- **Organizacje** (krok 3): Federacja Sol-3 i Konsorcjum Gwiezdne Malferian uzupełnione o GM-callout z prawdą o wojnie; Inkwizycja, Szkarłatny Kult i Kult Otchłani przebudowane strukturalnie; Bill Cipher and Co. rozbudowane o kontekst więzienia i szczątkowego zagrożenia.
|
||||||
|
- **Rasy** (krok 4): Krasnolud i Malferianin mocno rozbudowane (biologia, kultura, historia, linki do lokacji i dyscyplin); Lamia uzupełniona; błąd Aurora Nylabelle w pliku Feles naprawiony.
|
||||||
|
- **NPC** (krok 5): stworzone lub zaktualizowane pliki dla wszystkich 10 priorytetowych postaci (Diana Koniecpolska, Ten Który jest Mroczny/Robert Zaryn, Karolina Fey, Bezimienny Pierwszy Patriarcha, Xillith Isherwood, Szaarael, Yssariel, Thingrim Magnarson, Princess Zayis, Prince Arkthar).
|
||||||
|
- **Cross-referencje do tabeli tech** (krok 6): wstawione do plików Krasnolud, Malferianin i Konsorcjum.
|
||||||
|
|
||||||
|
**Pozostałe:** krok 7 - vault-wide audyt wikilinków, poprawki YAML frontmatter, odświeżenie plików indeksowych (`disciplines.md`, `races.md`, `organizations.md`, `npcs.md` itp.).
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
|
||||||
|
VAULT_DIR = "ObsidianNotes"
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not os.path.exists(VAULT_DIR):
|
||||||
|
print(json.dumps({"error": f"Directory {VAULT_DIR} not found."}))
|
||||||
|
return
|
||||||
|
|
||||||
|
md_files = {} # filename (without .md) -> relative path
|
||||||
|
for root, dirs, files in os.walk(VAULT_DIR):
|
||||||
|
for file in files:
|
||||||
|
if file.endswith(".md"):
|
||||||
|
name = file[:-3]
|
||||||
|
rel_path = os.path.relpath(os.path.join(root, file), VAULT_DIR)
|
||||||
|
md_files[name] = rel_path
|
||||||
|
|
||||||
|
links = {} # source file rel_path -> list of target link names
|
||||||
|
broken_links = [] # list of (source file rel_path, broken link name)
|
||||||
|
incoming_links = {name: [] for name in md_files.keys()} # target name -> list of source file rel_paths
|
||||||
|
|
||||||
|
link_pattern = re.compile(r'\[\[(.*?)\]\]')
|
||||||
|
|
||||||
|
for name, rel_path in md_files.items():
|
||||||
|
full_path = os.path.join(VAULT_DIR, rel_path)
|
||||||
|
try:
|
||||||
|
with open(full_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading {rel_path}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
file_links = link_pattern.findall(content)
|
||||||
|
parsed_links = []
|
||||||
|
for raw_link in file_links:
|
||||||
|
# Handle aliases [[Link|Alias]] and headings [[Link#Heading]]
|
||||||
|
base_link = raw_link.split('|')[0].split('#')[0].strip()
|
||||||
|
if not base_link:
|
||||||
|
continue
|
||||||
|
parsed_links.append(base_link)
|
||||||
|
|
||||||
|
# Check if it exists
|
||||||
|
link_basename = os.path.basename(base_link)
|
||||||
|
if link_basename.endswith('.md'):
|
||||||
|
link_basename = link_basename[:-3]
|
||||||
|
|
||||||
|
if link_basename in md_files:
|
||||||
|
incoming_links[link_basename].append(rel_path)
|
||||||
|
else:
|
||||||
|
broken_links.append((rel_path, base_link))
|
||||||
|
|
||||||
|
links[rel_path] = parsed_links
|
||||||
|
|
||||||
|
orphans = [rel_path for name, rel_path in md_files.items() if len(incoming_links[name]) == 0]
|
||||||
|
|
||||||
|
report = {
|
||||||
|
"total_files": len(md_files),
|
||||||
|
"total_links": sum(len(l) for l in links.values()),
|
||||||
|
"broken_links_count": len(broken_links),
|
||||||
|
"orphans_count": len(orphans),
|
||||||
|
"broken_links": broken_links,
|
||||||
|
"orphans": orphans
|
||||||
|
}
|
||||||
|
|
||||||
|
with open("vault_analysis_report.json", "w", encoding="utf-8") as f:
|
||||||
|
json.dump(report, f, indent=2)
|
||||||
|
|
||||||
|
print("Analysis complete. Check vault_analysis_report.json")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
$repoRoot = (Resolve-Path "$PSScriptRoot\..").Path
|
||||||
|
$vaultDir = "$repoRoot\ObsidianNotes"
|
||||||
|
|
||||||
|
$targetDirs = @(
|
||||||
|
"$vaultDir\npcs",
|
||||||
|
"$vaultDir\concepts",
|
||||||
|
"$vaultDir\organizations",
|
||||||
|
"$vaultDir\locations",
|
||||||
|
"$vaultDir\races"
|
||||||
|
)
|
||||||
|
|
||||||
|
$reportFile = "$vaultDir\short_descriptions.md"
|
||||||
|
$threshold = 150
|
||||||
|
|
||||||
|
$shortFiles = @()
|
||||||
|
|
||||||
|
foreach ($dir in $targetDirs) {
|
||||||
|
if (-not (Test-Path $dir)) { continue }
|
||||||
|
|
||||||
|
$files = Get-ChildItem -Path $dir -Recurse -Filter "*.md"
|
||||||
|
foreach ($file in $files) {
|
||||||
|
$content = Get-Content $file.FullName -Raw -Encoding UTF8
|
||||||
|
|
||||||
|
# Remove YAML frontmatter
|
||||||
|
$contentWithoutYaml = $content -replace '(?s)^---\n.*?\n---\n', ''
|
||||||
|
|
||||||
|
# Count words
|
||||||
|
$words = $contentWithoutYaml -split '\s+' | Where-Object { $_ -ne '' }
|
||||||
|
$wordCount = $words.Count
|
||||||
|
|
||||||
|
if ($wordCount -lt $threshold) {
|
||||||
|
$shortFiles += [PSCustomObject]@{
|
||||||
|
Path = $file.FullName -replace [regex]::Escape("$vaultDir\"), ""
|
||||||
|
WordCount = $wordCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$reportContent = @("# Short Descriptions Report (< $threshold words)", "")
|
||||||
|
if ($shortFiles.Count -eq 0) {
|
||||||
|
$reportContent += "All analyzed files meet the word count threshold!"
|
||||||
|
} else {
|
||||||
|
$reportContent += "| File Path | Word Count |"
|
||||||
|
$reportContent += "|-----------|------------|"
|
||||||
|
foreach ($item in $shortFiles | Sort-Object WordCount) {
|
||||||
|
$reportContent += "| $($item.Path) | $($item.WordCount) |"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$reportContent -join "`n" | Set-Content -Path $reportFile -Encoding UTF8
|
||||||
|
|
||||||
|
Write-Host "Audit complete! Found $($shortFiles.Count) short files." -ForegroundColor Cyan
|
||||||
|
Write-Host "Report saved to: $reportFile" -ForegroundColor Green
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { resolve } from 'node:path';
|
||||||
|
import { readFileSync, writeFileSync } from 'node:fs';
|
||||||
|
import { walkBook } from '../Foundry-Data/foundry-system/scripts/parsers/book-walker.js';
|
||||||
|
import { slugify } from '../Foundry-Data/foundry-system/scripts/parsers/helpers.js';
|
||||||
|
|
||||||
|
const SPELL_BOOKS = [
|
||||||
|
'ObsidianNotes/rules/01. Księga Magii.md',
|
||||||
|
'ObsidianNotes/rules/04. Arcanum Sanguinis.md',
|
||||||
|
'ObsidianNotes/rules/05. Vivat Patriarcha coccineus!.md',
|
||||||
|
'ObsidianNotes/rules/02. Klątwa Otchłani.md'
|
||||||
|
];
|
||||||
|
|
||||||
|
const overrides = JSON.parse(readFileSync('Foundry-Data/foundry-system/scripts/parsers/_id-overrides.json', 'utf8'));
|
||||||
|
|
||||||
|
const missing = [];
|
||||||
|
|
||||||
|
for (const file of SPELL_BOOKS) {
|
||||||
|
const path = resolve(file);
|
||||||
|
let blocks;
|
||||||
|
try {
|
||||||
|
blocks = walkBook(path);
|
||||||
|
} catch(e) { continue; }
|
||||||
|
|
||||||
|
for (const block of blocks) {
|
||||||
|
if (!block.fields.some(f => f.key.toLowerCase().includes('próg') || f.key.toLowerCase().includes('poziom trud'))) continue;
|
||||||
|
let spellName = block.name;
|
||||||
|
if (spellName === 'Superzaklęcie - Requiem') spellName = 'Requiem';
|
||||||
|
const slug = slugify(spellName);
|
||||||
|
if (!overrides[slug]) {
|
||||||
|
missing.push({ name: spellName, slug });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Missing translations: ${missing.length}`);
|
||||||
|
missing.forEach(m => console.log(`"${m.slug}": "", // ${m.name}`));
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# --- CONFIGURATION ---
|
||||||
|
ROOT_DIR = Path(__file__).parent.parent
|
||||||
|
VAULT_DIR = ROOT_DIR / "ObsidianNotes"
|
||||||
|
OUTPUT_DIR = ROOT_DIR / "NotebookLM_Uploads"
|
||||||
|
|
||||||
|
# Folders to completely ignore
|
||||||
|
IGNORE_FOLDERS = {".obsidian", "_assets", "_templates", "revisions", "tabletop-cards"}
|
||||||
|
|
||||||
|
# Thematic mapping: Which Obsidian folders go into which macro file
|
||||||
|
THEME_MAP = {
|
||||||
|
"01_System_Rules_and_Mechanics.txt": ["rules", "disciplines", "spells", "items", "races"],
|
||||||
|
"02_World_Lore_and_NPCs.txt": ["concepts", "npcs", "organizations", "lore"],
|
||||||
|
"03_Atlas_and_Locations.txt": ["locations"],
|
||||||
|
"04_Campaigns_and_Characters.txt": ["classes", "player-characters", "conflicts"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- REGEX UTILITIES ---
|
||||||
|
FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
|
||||||
|
|
||||||
|
def parse_md_file(file_path, relative_path):
|
||||||
|
"""Reads a file, extracts YAML, cleans structure, and returns format text."""
|
||||||
|
try:
|
||||||
|
content = file_path.read_text(encoding="utf-8-sig")
|
||||||
|
except Exception as e:
|
||||||
|
return f"\n\n[ERROR READING FILE {relative_path}: {str(e)}]\n\n"
|
||||||
|
|
||||||
|
# Separate Frontmatter and Content
|
||||||
|
frontmatter = ""
|
||||||
|
match = FRONTMATTER_RE.match(content)
|
||||||
|
if match:
|
||||||
|
frontmatter = match.group(1).strip()
|
||||||
|
body = content[match.end():].strip()
|
||||||
|
else:
|
||||||
|
body = content.strip()
|
||||||
|
|
||||||
|
# Format the file section so NotebookLM sees it as a discrete structured document
|
||||||
|
builder = []
|
||||||
|
builder.append("\n" + "="*80)
|
||||||
|
builder.append(f"START_FILE: {relative_path}")
|
||||||
|
builder.append(f"FILENAME: {file_path.name}")
|
||||||
|
builder.append("="*80)
|
||||||
|
|
||||||
|
if frontmatter:
|
||||||
|
builder.append("\n--- METADATA ---")
|
||||||
|
builder.append(frontmatter)
|
||||||
|
builder.append("----------------\n")
|
||||||
|
|
||||||
|
builder.append(body)
|
||||||
|
builder.append(f"\nEND_FILE: {relative_path}\n")
|
||||||
|
|
||||||
|
return "\n".join(builder)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not VAULT_DIR.exists():
|
||||||
|
print(f"[-] Error: Vault directory not found at {VAULT_DIR}")
|
||||||
|
return
|
||||||
|
|
||||||
|
OUTPUT_DIR.mkdir(exist_ok=True)
|
||||||
|
print(f"[+] Initializing vault compilation from: {VAULT_DIR}")
|
||||||
|
|
||||||
|
# Initialize empty text collectors for our themes
|
||||||
|
compiled_data = {filename: [] for filename in THEME_MAP.keys()}
|
||||||
|
|
||||||
|
# Walk through the entire Obsidian Vault
|
||||||
|
for root, dirs, files in os.walk(VAULT_DIR):
|
||||||
|
# In-place filtering to skip ignored root directories
|
||||||
|
dirs[:] = [d for d in dirs if d not in IGNORE_FOLDERS]
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
if not file.endswith(".md"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
file_path = Path(root) / file
|
||||||
|
# Calculate path relative to the Vault root (e.g., 'spells/academic/fire-magic/fireball.md')
|
||||||
|
rel_path = file_path.relative_to(VAULT_DIR)
|
||||||
|
root_parent_folder = rel_path.parts[0] # The first subfolder name
|
||||||
|
|
||||||
|
# Find which macro file this note belongs to based on its root parent folder
|
||||||
|
target_macro = None
|
||||||
|
for macro_file, monitored_folders in THEME_MAP.items():
|
||||||
|
if root_parent_folder in monitored_folders:
|
||||||
|
target_macro = macro_file
|
||||||
|
break
|
||||||
|
|
||||||
|
# If it's a loose markdown file in the root vault or unmapped, default to Lore
|
||||||
|
if not target_macro:
|
||||||
|
target_macro = "02_World_Lore_and_NPCs.txt"
|
||||||
|
|
||||||
|
# Parse and append
|
||||||
|
formatted_note = parse_md_file(file_path, rel_path)
|
||||||
|
compiled_data[target_macro].append(formatted_note)
|
||||||
|
|
||||||
|
# Write the master compiled macro files
|
||||||
|
for filename, pieces in compiled_data.items():
|
||||||
|
if not pieces:
|
||||||
|
print(f"[!] Target file {filename} is empty. Skipping output.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
out_path = OUTPUT_DIR / filename
|
||||||
|
# Combine all files with clear division
|
||||||
|
final_content = f"MASTER COMPILATION FOR CATEGORY: {filename}\n" + "\n".join(pieces)
|
||||||
|
|
||||||
|
out_path.write_text(final_content, encoding="utf-8")
|
||||||
|
print(f"[+] Successfully wrote {len(pieces)} files to -> {out_path.name}")
|
||||||
|
|
||||||
|
print("\n[+] Done! Drag the contents of 'NotebookLM_Uploads' straight into NotebookLM.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||||
|
$wc = New-Object System.Net.WebClient
|
||||||
|
$wc.Encoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
$weaponsUrl = "https://docs.google.com/spreadsheets/d/e/2PACX-1vRcI3WQZb4HvKKZfJUbGC46D88HmJCJ_5qktlOFJT3v2yP0kV-uOR0V4yUCDkNOO-20tpOVQiekKLVA/pub?output=csv&gid=667492832"
|
||||||
|
$gearUrl = "https://docs.google.com/spreadsheets/d/e/2PACX-1vRcI3WQZb4HvKKZfJUbGC46D88HmJCJ_5qktlOFJT3v2yP0kV-uOR0V4yUCDkNOO-20tpOVQiekKLVA/pub?output=csv&gid=0"
|
||||||
|
|
||||||
|
Write-Host "Downloading Weapons and Armor..."
|
||||||
|
$weaponsContent = $wc.DownloadString($weaponsUrl)
|
||||||
|
$weaponsLines = $weaponsContent -split '\r?\n'
|
||||||
|
|
||||||
|
$repoRoot = (Resolve-Path "$PSScriptRoot\..").Path
|
||||||
|
$vaultDir = "$repoRoot\ObsidianNotes"
|
||||||
|
|
||||||
|
$outWeapons = "$vaultDir\items\weapons"
|
||||||
|
$outArmor = "$vaultDir\items\armor"
|
||||||
|
$outGear = "$vaultDir\items\gear"
|
||||||
|
|
||||||
|
if (-not (Test-Path $outWeapons)) { New-Item -ItemType Directory -Force -Path $outWeapons | Out-Null }
|
||||||
|
if (-not (Test-Path $outArmor)) { New-Item -ItemType Directory -Force -Path $outArmor | Out-Null }
|
||||||
|
if (-not (Test-Path $outGear)) { New-Item -ItemType Directory -Force -Path $outGear | Out-Null }
|
||||||
|
|
||||||
|
$mode = ""
|
||||||
|
foreach ($line in $weaponsLines) {
|
||||||
|
if ($line -match "^Broń:") { $mode = "Weapons"; continue }
|
||||||
|
if ($line -match "^Pancerz:") { $mode = "Armor"; continue }
|
||||||
|
if ($line -match "^Tarcza") { $mode = "Shield" }
|
||||||
|
|
||||||
|
if ($line -match "^\s*," -or $line -match "^\s*$") { continue }
|
||||||
|
|
||||||
|
if ($mode -eq "Weapons") {
|
||||||
|
$csv = $line | ConvertFrom-Csv -Header "Name","Price","Damage","Traits","Extra"
|
||||||
|
$name = $csv.Name
|
||||||
|
$price = $csv.Price
|
||||||
|
$dmg = $csv.Damage
|
||||||
|
$traits = $csv.Traits
|
||||||
|
if (-not $name) { continue }
|
||||||
|
|
||||||
|
$yaml = @("---", "tags:", " - foundry/compendium/items", " - weapon")
|
||||||
|
if ($price) { $yaml += "price: '$($price.Replace("'", "''"))'" }
|
||||||
|
if ($dmg) { $yaml += "damage: '$($dmg.Replace("'", "''"))'" }
|
||||||
|
if ($traits) { $yaml += "traits: '$($traits.Replace("'", "''"))'" }
|
||||||
|
$yaml += "---"
|
||||||
|
$yaml += "# $name"
|
||||||
|
$yaml += ""
|
||||||
|
if ($price) { $yaml += "* **Cena:** $price" }
|
||||||
|
if ($dmg) { $yaml += "* **Obrażenia:** $dmg" }
|
||||||
|
if ($traits) { $yaml += "* **Cechy:** $traits" }
|
||||||
|
|
||||||
|
$safeName = $name -replace '[\\/:\*\?"<>\|]', '_'
|
||||||
|
$yaml -join "`n" | Set-Content -Path (Join-Path $outWeapons "$safeName.md") -Encoding UTF8
|
||||||
|
Write-Host "Saved Weapon: $name"
|
||||||
|
}
|
||||||
|
elseif ($mode -eq "Armor") {
|
||||||
|
$csv = $line | ConvertFrom-Csv -Header "Name","Price","ArmorPoints","Reqs","Traits"
|
||||||
|
$name = $csv.Name
|
||||||
|
$price = $csv.Price
|
||||||
|
$ap = $csv.ArmorPoints
|
||||||
|
$reqs = $csv.Reqs
|
||||||
|
$traits = $csv.Traits
|
||||||
|
if (-not $name) { continue }
|
||||||
|
|
||||||
|
$yaml = @("---", "tags:", " - foundry/compendium/items", " - armor")
|
||||||
|
if ($price) { $yaml += "price: '$($price.Replace("'", "''"))'" }
|
||||||
|
if ($ap) { $yaml += "armor_points: '$($ap.Replace("'", "''"))'" }
|
||||||
|
if ($reqs) { $yaml += "requirements: '$($reqs.Replace("'", "''"))'" }
|
||||||
|
if ($traits) { $yaml += "traits: '$($traits.Replace("'", "''"))'" }
|
||||||
|
$yaml += "---"
|
||||||
|
$yaml += "# Pancerz $name"
|
||||||
|
$yaml += ""
|
||||||
|
if ($price) { $yaml += "* **Cena:** $price" }
|
||||||
|
if ($ap) { $yaml += "* **Punkty Pancerza:** $ap" }
|
||||||
|
if ($reqs) { $yaml += "* **Wymagania:** $reqs" }
|
||||||
|
if ($traits) { $yaml += "* **Cechy:** $traits" }
|
||||||
|
|
||||||
|
$safeName = "Pancerz " + ($name -replace '[\\/:\*\?"<>\|]', '_')
|
||||||
|
$yaml -join "`n" | Set-Content -Path (Join-Path $outArmor "$safeName.md") -Encoding UTF8
|
||||||
|
Write-Host "Saved Armor: Pancerz $name"
|
||||||
|
}
|
||||||
|
elseif ($mode -eq "Shield") {
|
||||||
|
$csv = $line | ConvertFrom-Csv -Header "Name","Price","Desc","Extra1","Extra2"
|
||||||
|
$name = $csv.Name
|
||||||
|
$price = $csv.Price
|
||||||
|
$desc = $csv.Desc
|
||||||
|
if (-not $name) { continue }
|
||||||
|
|
||||||
|
$yaml = @("---", "tags:", " - foundry/compendium/items", " - shield")
|
||||||
|
if ($price) { $yaml += "price: '$($price.Replace("'", "''"))'" }
|
||||||
|
$yaml += "---"
|
||||||
|
$yaml += "# $name"
|
||||||
|
$yaml += ""
|
||||||
|
if ($price) { $yaml += "* **Cena:** $price" }
|
||||||
|
$yaml += ""
|
||||||
|
$yaml += $desc
|
||||||
|
|
||||||
|
$safeName = $name -replace '[\\/:\*\?"<>\|]', '_'
|
||||||
|
$yaml -join "`n" | Set-Content -Path (Join-Path $outArmor "$safeName.md") -Encoding UTF8
|
||||||
|
Write-Host "Saved Shield: $name"
|
||||||
|
$mode = "Done"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Downloading Gear..."
|
||||||
|
$gearContent = $wc.DownloadString($gearUrl)
|
||||||
|
$gearLines = $gearContent -split '\r?\n'
|
||||||
|
|
||||||
|
$first = $true
|
||||||
|
foreach ($line in $gearLines) {
|
||||||
|
if ($first) { $first = $false; continue }
|
||||||
|
if ($line -match "^\s*," -or $line -match "^\s*$") { continue }
|
||||||
|
|
||||||
|
$csv = $line | ConvertFrom-Csv -Header "Name","Price","Desc","Extra1","Extra2"
|
||||||
|
$name = $csv.Name
|
||||||
|
$price = $csv.Price
|
||||||
|
$desc = $csv.Desc
|
||||||
|
if (-not $name) { continue }
|
||||||
|
if ($name -match "^\*") { continue } # Skip footer
|
||||||
|
|
||||||
|
$yaml = @("---", "tags:", " - foundry/compendium/items", " - gear")
|
||||||
|
if ($price) { $yaml += "price: '$($price.Replace("'", "''"))'" }
|
||||||
|
$yaml += "---"
|
||||||
|
$yaml += "# $name"
|
||||||
|
$yaml += ""
|
||||||
|
if ($price) { $yaml += "* **Cena:** $price" }
|
||||||
|
$yaml += ""
|
||||||
|
$yaml += $desc
|
||||||
|
|
||||||
|
$safeName = $name -replace '[\\/:\*\?"<>\|]', '_'
|
||||||
|
$yaml -join "`n" | Set-Content -Path (Join-Path $outGear "$safeName.md") -Encoding UTF8
|
||||||
|
Write-Host "Saved Gear: $name"
|
||||||
|
}
|
||||||
|
Write-Host "Item extraction complete!" -ForegroundColor Green
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
$repoRoot = (Resolve-Path "$PSScriptRoot\..").Path
|
||||||
|
$file = "$repoRoot\ObsidianNotes\rules\01. Księga Magii.md"
|
||||||
|
$content = Get-Content $file -Raw
|
||||||
|
$lines = $content -split '\r?\n'
|
||||||
|
|
||||||
|
$inSpells = $false
|
||||||
|
$currentDiscipline = "Ogólna"
|
||||||
|
$currentSpell = ""
|
||||||
|
$spellLines = @()
|
||||||
|
$outDir = "$repoRoot\ObsidianNotes\spells"
|
||||||
|
|
||||||
|
function SaveSpell {
|
||||||
|
param($disc, $name, $lines)
|
||||||
|
if ($name -eq "" -or $lines.Count -eq 0) { return }
|
||||||
|
|
||||||
|
$discSafe = ($disc.ToLower() -replace '\s+', '-') -replace '[^a-z0-9\-ąćęłńóśźż]', ''
|
||||||
|
if ($discSafe -eq "") { $discSafe = "ogolne" }
|
||||||
|
|
||||||
|
$yaml = @("---", "tags:", " - foundry/compendium/spells", " - spell", " - $discSafe")
|
||||||
|
foreach ($l in $lines) {
|
||||||
|
if ($l -match "^\*\s*Koszt Many:\s*(.*)") { $yaml += "mana_cost: '$($matches[1].Trim().Replace("'", "''"))'" }
|
||||||
|
if ($l -match "^\*\s*Czas(?: trwania)?:\s*(.*)") { $yaml += "duration: '$($matches[1].Trim().Replace("'", "''"))'" }
|
||||||
|
if ($l -match "^\*\s*Zasięg:\s*(.*)") { $yaml += "range: '$($matches[1].Trim().Replace("'", "''"))'" }
|
||||||
|
if ($l -match "^\*\s*Czas [Rr]zucania:\s*(.*)") { $yaml += "cast_time: '$($matches[1].Trim().Replace("'", "''"))'" }
|
||||||
|
}
|
||||||
|
$yaml += "---"
|
||||||
|
$yaml += "# $name"
|
||||||
|
$yaml += ""
|
||||||
|
$yaml += $lines
|
||||||
|
|
||||||
|
$safeName = $name -replace '[\\/:\*\?"<>\|]', '_'
|
||||||
|
$dir = Join-Path $outDir $discSafe
|
||||||
|
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null }
|
||||||
|
$outPath = Join-Path $dir "$safeName.md"
|
||||||
|
$yaml -join "`n" | Set-Content -Path $outPath -Encoding UTF8
|
||||||
|
Write-Host "Saved spell: $discSafe / $name"
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($i=0; $i -lt $lines.Count; $i++) {
|
||||||
|
$line = $lines[$i]
|
||||||
|
|
||||||
|
if ($line -match "^##\s*Rozdział V.*Lista Zaklęć") {
|
||||||
|
$inSpells = $true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ($line -match "^##\s*Rozdział VI.*Superzaklęcia") {
|
||||||
|
SaveSpell $currentDiscipline $currentSpell $spellLines
|
||||||
|
$inSpells = $true
|
||||||
|
$currentDiscipline = "Superzaklęcia"
|
||||||
|
$currentSpell = ""
|
||||||
|
$spellLines = @()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ($line -match "^##\s*Rozdział VII.*Tworzenie Zaklęć" -or $line -match "^##\s*Rozdział VII.*") {
|
||||||
|
SaveSpell $currentDiscipline $currentSpell $spellLines
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($inSpells) {
|
||||||
|
if ($line -match "^#+\s+(.*)") {
|
||||||
|
$headerText = $matches[1].Trim()
|
||||||
|
|
||||||
|
$isSpell = $false
|
||||||
|
$j = $i + 1
|
||||||
|
while ($j -lt $lines.Count -and $lines[$j] -match "^\s*$") { $j++ }
|
||||||
|
if ($j -lt $lines.Count -and ($lines[$j] -match "^\*\s*Próg [Zz]aklęcia:" -or $lines[$j] -match "^\*\s*Koszt Many:" -or $lines[$j] -match "^\s*Wymagania:")) {
|
||||||
|
$isSpell = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isSpell) {
|
||||||
|
SaveSpell $currentDiscipline $currentSpell $spellLines
|
||||||
|
$currentSpell = $headerText
|
||||||
|
$spellLines = @()
|
||||||
|
} else {
|
||||||
|
if ($currentSpell -eq "") {
|
||||||
|
$currentDiscipline = $headerText
|
||||||
|
} else {
|
||||||
|
SaveSpell $currentDiscipline $currentSpell $spellLines
|
||||||
|
$currentDiscipline = $headerText
|
||||||
|
$currentSpell = ""
|
||||||
|
$spellLines = @()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($currentSpell -ne "") {
|
||||||
|
$spellLines += $line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Host "Spell extraction complete!" -ForegroundColor Green
|
||||||
Reference in New Issue
Block a user