feat: implement core D&D helpers logic and system architecture

This commit is contained in:
2026-05-25 22:14:58 -07:00
parent 5bb483431f
commit 685586318f
36 changed files with 1137 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
# Persistence module for D&D Helpers.
Binary file not shown.
Binary file not shown.
+90
View File
@@ -0,0 +1,90 @@
import json
from pathlib import Path
from typing import Any, Dict, Optional
from src.llm.models import CharacterStateUpdate
DATA_CHARS_DIR = Path("data/chars")
def ensure_chars_dir():
"""Ensures the character data directory exists."""
DATA_CHARS_DIR.mkdir(parents=True, exist_ok=True)
def get_character_state(character_name: str) -> Dict[str, Any]:
"""
Reads character state from a JSON file.
If the character doesn't exist, returns a default state.
"""
ensure_chars_dir()
file_path = DATA_CHARS_DIR / f"{character_name}.json"
if not file_path.exists():
return {
"character_name": character_name,
"stats": {"hp": 0, "max_hp": 0, "ac": 0},
"status_effects": [],
"inventory": [],
}
with open(file_path, "r", encoding="utf-8") as f:
return json.load(f)
def update_character_state(update: CharacterStateUpdate):
"""
Updates character state based on a CharacterStateUpdate model.
Reads the current state, applies changes, and writes it back.
"""
ensure_chars_dir()
state = get_character_state(update.character_name)
# Update HP
if update.hp_change is not None:
current_hp = state.get("stats", {}).get("hp", 0)
state["stats"]["hp"] = current_hp + update.hp_change
# Update Status Effects
status_effects = set(state.get("status_effects", []))
for effect in update.status_effects_added:
status_effects.add(effect)
for effect in update.status_effects_removed:
status_effects.discard(effect)
state["status_effects"] = list(status_effects)
# Update Inventory
inventory = state.get("inventory", [])
for change in update.inventory_changes:
# Find if item already exists
item_exists = False
for item in inventory:
if item["item"] == change.item:
if change.action == "added":
item["quantity"] = item.get("quantity", 1) + change.quantity
elif change.action == "removed":
item["quantity"] = max(0, item.get("quantity", 1) - change.quantity)
item_exists = True
break
if not item_exists:
if change.action == "added":
inventory.append(
{
"item": change.item,
"quantity": change.quantity,
"weight": 0, # Default weight if not provided
}
)
elif change.action == "removed":
# Item not in inventory, do nothing or log
pass
state["inventory"] = inventory
file_path = DATA_CHARS_DIR / f"{update.character_name}.json"
with open(file_path, "w", encoding="utf-8") as f:
json.dump(state, f, indent=4)
return str(file_path)
+55
View File
@@ -0,0 +1,55 @@
import os
from pathlib import Path
from typing import Optional
from src.llm.models import LoreUpdate
DATA_LORE_DIR = Path("data/lore")
CATEGORY_MAP = {
"NPC": "NPCs",
"Location": "Locations",
"WorldBuilding": "World",
"Plot": "Timeline",
}
def ensure_lore_dir():
"""Ensures the lore data directory and subdirectories exist."""
DATA_LORE_DIR.mkdir(parents=True, exist_ok=True)
for folder in CATEGORY_MAP.values():
if folder != "Timeline":
(DATA_LORE_DIR / folder).mkdir(parents=True, exist_ok=True)
def update_lore(update: LoreUpdate):
"""
Updates lore based on a LoreUpdate model.
- For NPC/Location/WorldBuilding: Updates the specific entity's file.
- For Plot: Appends to Timeline.md.
"""
ensure_lore_dir()
category = update.category
folder = CATEGORY_MAP.get(category, "Other")
if category == "Plot":
file_path = DATA_LORE_DIR / "Timeline.md"
content_to_append = f"- {update.content}\n"
elif update.entity_name:
category_folder = CATEGORY_MAP.get(category, "Other")
file_path = DATA_LORE_DIR / category_folder / f"{update.entity_name}.md"
# For entity-specific files, we can append the content as a list item.
# In a more complex system, we might check for existing headers.
content_to_append = f"- {update.content}\n"
else:
# Fallback if no entity name is provided for a category that expects one.
# We'll put it in a general file for that category.
category_folder = CATEGORY_MAP.get(category, "Other")
file_path = DATA_LORE_DIR / category_folder / "General.md"
content_to_append = f"- {update.content}\n"
with open(file_path, "a", encoding="utf-8") as f:
f.write(content_to_append)
return str(file_path)