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
+154
View File
@@ -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 };
+118
View File
@@ -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 };
+162
View File
@@ -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 };
+155
View File
@@ -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 };
+68
View File
@@ -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 };