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:
Octoturge
2026-08-17 21:28:50 +02:00
commit a4f8941fca
25 changed files with 3702 additions and 0 deletions
+73
View File
@@ -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()
+54
View File
@@ -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
+36
View File
@@ -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}`));
+113
View File
@@ -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()
+132
View File
@@ -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
+91
View File
@@ -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