feat(engine): persistenza enigmi, inventario strutturato, HP mostri
test / test (push) Has been cancelled

Fase 2 del remediation plan:
- set_puzzle_state / is_puzzle_solved per enigmi area
- update_inventory su character_items con add/remove/equip/list
- combat() legge HP da combat_state e persiste danno
- dm_cycle inietta campaign_id nei tool che ne hanno bisogno
- helpers db: dedup inventario, remove/equip per nome

Fase 4 del remediation plan:
- test integrazione dm_cycle con LLM mockato (5 test)
- test WebSocket handler con mock (4 test)
- CI minimo .gitea/workflows/test.yml
This commit is contained in:
truetype committed 2026-06-27 18:29:32 +02:00
1 parent cb45172717
commit 6b048dc0cc
7 files changed
+386 -18

No files matched your search

+67 -9
View File
@@ -62,6 +62,8 @@ def combat(
mighty_deed_die: str = "",
luck_mod: int = 0,
armor_type: str = "none",
monster_id: str = "",
campaign_id: int = 0,
) -> ToolResult:
char = storage.get_character(character_id)
if not char:
@@ -71,7 +73,6 @@ def combat(
atk_roll = dice.roll("d20", modifier=attack_bonus)
raw_total = atk_roll["total"]
deed_roll = None
if mighty_deed_die:
deed_roll = dice.roll(mighty_deed_die)
@@ -81,6 +82,17 @@ def combat(
result_parts = []
crit_res = None
# HP persistenti: carica mostro da combat_state se disponibile
monster_name = ""
monster_hp_before = None
if monster_id and campaign_id:
state = storage.get_combat_state(campaign_id)
monster_state = next((m for m in state if m["monster_id"] == monster_id), None)
if monster_state:
monster_name = monster_state["monster_name"]
monster_hp_before = monster_state["current_hp"]
result_parts.append(f"{monster_name}: PF {monster_state['current_hp']}/{monster_state['max_hp']}")
if atk_roll["fumble"]:
fumble_die_name = storage.get_fumble_die(armor_type) or "d4"
fumble_roll = dice.roll(fumble_die_name)
@@ -101,6 +113,10 @@ def combat(
+ (crit_res["result"] if crit_res else "Nessun risultato trovato.")
)
# Calcolo danno e persistenza
total_dmg = 0
new_hp = None
monster_defeated = False
if hit and not atk_roll["fumble"]:
dmg = dice.roll(damage_die, modifier=damage_bonus)
crit_dmg_bonus = 0
@@ -108,6 +124,15 @@ def combat(
crit_dmg_bonus = _parse_extra_damage(crit_res.get("result", ""))
total_dmg = dmg["total"] + crit_dmg_bonus
result_parts.append(f"Colpito! Danni: {dmg['results']} + {crit_dmg_bonus} extra = {total_dmg}")
if monster_id and campaign_id and monster_hp_before is not None:
hp_result = storage.update_combat_hp(campaign_id, monster_id, total_dmg)
if hp_result:
new_hp = hp_result["new_hp"]
monster_defeated = hp_result["status"] == "defeated"
result_parts.append(f"PF {monster_name}: {monster_hp_before} → {new_hp}")
if monster_defeated:
result_parts.append(f"{monster_name} sconfitto!")
elif not atk_roll["fumble"]:
result_parts.append("Mancato!")
@@ -125,6 +150,9 @@ def combat(
"crit": atk_roll["crit"],
"fumble": atk_roll["fumble"],
"deed": deed_roll["total"] if deed_roll else None,
"monster_hp_before": monster_hp_before,
"monster_hp_after": new_hp,
"monster_defeated": monster_defeated,
"message": f"Iniziativa: {init_roll['total']} | Attacco d20+{attack_bonus}: {atk_roll['results'][0]}+{attack_bonus}={atk_roll['total']} vs CA {target_ac} | {'COLPI TO!' if hit else 'Mancato.'}{deed_msg} | " + " | ".join(result_parts),
}
@@ -273,25 +301,55 @@ def spellcasting(character_id: int, spell_name: str, spell_check: int, dc: int =
@tool
def update_inventory(character_id: int, item: str, action: str = "add") -> ToolResult:
def update_inventory(character_id: int, item: str = "", action: str = "add") -> ToolResult:
char = storage.get_character(character_id)
if not char:
return {"success": False, "tool": "update-inventory", "message": "Personaggio non trovato."}
inv = json.loads(char.get("equipment", "[]"))
if action == "add":
inv.append(item)
if not item:
return {"success": False, "tool": "update-inventory", "message": "Specificare un oggetto da aggiungere."}
storage.add_character_item(character_id, item)
elif action == "remove":
inv = [i for i in inv if i != item]
if not item:
return {"success": False, "tool": "update-inventory", "message": "Specificare un oggetto da rimuovere."}
if not storage.remove_character_item(character_id, item):
return {"success": False, "tool": "update-inventory", "message": f"Oggetto '{item}' non trovato."}
elif action == "equip":
if not item:
return {"success": False, "tool": "update-inventory", "message": "Specificare un oggetto da equipaggiare."}
if not storage.equip_character_item(character_id, item, True):
return {"success": False, "tool": "update-inventory", "message": f"Oggetto '{item}' non trovato."}
elif action == "unequip":
if not item:
return {"success": False, "tool": "update-inventory", "message": "Specificare un oggetto da rimuovere."}
if not storage.equip_character_item(character_id, item, False):
return {"success": False, "tool": "update-inventory", "message": f"Oggetto '{item}' non trovato."}
elif action == "list":
items = storage.get_character_items(character_id)
names = [f"{i['name']} x{i['quantity']}{' (eq.)' if i['equipped'] else ''}" for i in items]
return {
"success": True,
"tool": "update-inventory",
"action": action,
"inventory": [dict(i) for i in items],
"message": f"Inventario: {len(items)} oggetti — " + (", ".join(names) if names else "vuoto."),
}
else:
return {"success": False, "tool": "update-inventory", "message": f"Azione '{action}' non valida. Usa 'add' o 'remove'."}
storage.update_character(character_id, equipment=json.dumps(inv))
return {"success": False, "tool": "update-inventory", "message": f"Azione '{action}' non valida. Usa: add, remove, equip, unequip, list."}
# Sincronizza campo equipment legacy per backward compat
items = storage.get_character_items(character_id)
names = [i["name"] for i in items if i.get("quantity", 1) > 0]
storage.update_character(character_id, equipment=json.dumps(names))
return {
"success": True,
"tool": "update-inventory",
"action": action,
"item": item,
"inventory": inv,
"message": f"Inventario: {action} '{item}' → {len(inv)} oggetti.",
"inventory": [dict(i) for i in storage.get_character_items(character_id)],
"message": f"Inventario: {action} '{item}' → aggiornato.",
}