From 85f53b1e8389284aaf2c11514d034e252ce1948b Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Thu, 2 Apr 2026 22:37:35 -0700 Subject: [PATCH] feat: catch-pending flag + wounded state for encounter resolution catch_pending: set immediately when /buddymon catch is invoked, suppresses auto-resolve while weakening Q&A is in progress. Cleared before catch roll (success clears encounter, failure leaves it without the flag so auto-resolve resumes naturally on the next clean Bash run). wounded: first clean Bash run without catch_pending drops the encounter to 5% strength and re-announces via UserPromptSubmit with a fleeing message. Second clean run auto-resolves it (it fled). UserPromptSubmit now shows distinct announcement text for wounded vs fresh encounters. --- hooks-handlers/post-tool-use.py | 37 +++++++++++++++++---- hooks-handlers/user-prompt-submit.py | 48 +++++++++++++++++----------- skills/buddymon/SKILL.md | 27 ++++++++++++++-- 3 files changed, 85 insertions(+), 27 deletions(-) diff --git a/hooks-handlers/post-tool-use.py b/hooks-handlers/post-tool-use.py index 011b14e..ae2bd1d 100755 --- a/hooks-handlers/post-tool-use.py +++ b/hooks-handlers/post-tool-use.py @@ -169,6 +169,20 @@ def set_active_encounter(encounter: dict): save_json(enc_file, data) +def wound_encounter() -> None: + """Drop active encounter to minimum strength and flag for re-announcement.""" + enc_file = BUDDYMON_DIR / "encounters.json" + data = load_json(enc_file) + enc = data.get("active_encounter") + if not enc: + return + enc["current_strength"] = 5 + enc["wounded"] = True + enc["announced"] = False # triggers UserPromptSubmit re-announcement + data["active_encounter"] = enc + save_json(enc_file, data) + + def match_bug_monster(output_text: str, catalog: dict) -> dict | None: """Return the first matching bug monster from the catalog, or None.""" if not output_text: @@ -416,14 +430,23 @@ def main(): existing = get_active_encounter() if existing: - # Auto-resolve if the monster's patterns no longer appear in output + # On a clean Bash run (monster patterns gone), respect catch_pending, + # wound a healthy monster, or auto-resolve a wounded one. if output and not encounter_still_present(existing, output, catalog): - xp, display = auto_resolve_encounter(existing, buddy_id) - messages.append( - f"\n⚔️ **{buddy_display} defeated {display}!** (auto-resolved)\n" - f" +{xp} XP\n" - ) - # else: monster persists, no message — don't spam every tool call + if existing.get("catch_pending"): + # User invoked /buddymon catch — hold the monster for them + pass + elif existing.get("wounded"): + # Already wounded on last clean run — auto-resolve (it fled) + xp, display = auto_resolve_encounter(existing, buddy_id) + messages.append( + f"\n💨 **{display} fled!** (escaped while wounded)\n" + f" {buddy_display} gets partial XP: +{xp}\n" + ) + else: + # First clean run — wound it and re-announce so user can catch + wound_encounter() + # else: monster still present, no message — don't spam every tool call elif output or command: # No active encounter — check for bug monster first, then event encounters session = load_json(BUDDYMON_DIR / "session.json") diff --git a/hooks-handlers/user-prompt-submit.py b/hooks-handlers/user-prompt-submit.py index 3b8a923..3620128 100644 --- a/hooks-handlers/user-prompt-submit.py +++ b/hooks-handlers/user-prompt-submit.py @@ -80,24 +80,36 @@ def main(): catchable = enc.get("catchable", True) flavor = monster.get("flavor", "") - catchable_str = "[catchable · catch only]" if not defeatable else f"[{rarity} · {'catchable' if catchable else ''}]" - - lines = [ - f"\n💀 **{enc['display']} appeared!** {catchable_str}", - f" Strength: {strength}% · Rarity: {stars}", - ] - if flavor: - lines.append(f" *{flavor}*") - if not defeatable: - lines.append(" ⚠️ CANNOT BE DEFEATED — catch only") - lines += [ - "", - f" **{buddy_display}** is ready to battle!", - "", - " `[FIGHT]` Fix the bug → `/buddymon fight` to claim XP", - " `[CATCH]` Weaken first (test/repro/comment) → `/buddymon catch`", - " `[FLEE]` Ignore → monster grows stronger", - ] + if enc.get("wounded"): + # Wounded re-announcement — urgent, catch-or-lose framing + lines = [ + f"\n🩹 **{enc['display']} is wounded and fleeing!**", + f" Strength: {strength}% · This is your last chance to catch it.", + "", + f" **{buddy_display}** is ready — move fast!", + "", + " `[CATCH]` → `/buddymon catch` (near-guaranteed at 5% strength)", + " `[IGNORE]` → it flees on the next clean run", + ] + else: + # Normal first appearance + catchable_str = "[catchable · catch only]" if not defeatable else f"[{rarity} · {'catchable' if catchable else ''}]" + lines = [ + f"\n💀 **{enc['display']} appeared!** {catchable_str}", + f" Strength: {strength}% · Rarity: {stars}", + ] + if flavor: + lines.append(f" *{flavor}*") + if not defeatable: + lines.append(" ⚠️ CANNOT BE DEFEATED — catch only") + lines += [ + "", + f" **{buddy_display}** is ready to battle!", + "", + " `[FIGHT]` Fix the bug → `/buddymon fight` to claim XP", + " `[CATCH]` Weaken first (test/repro/comment) → `/buddymon catch`", + " `[FLEE]` Ignore → monster grows stronger", + ] msg = "\n".join(lines) print(json.dumps({ diff --git a/skills/buddymon/SKILL.md b/skills/buddymon/SKILL.md index 90135b2..51f1c0c 100644 --- a/skills/buddymon/SKILL.md +++ b/skills/buddymon/SKILL.md @@ -175,14 +175,31 @@ ShadowBit (🔒) cannot be defeated — redirect to catch. Read active encounter. If none: "No active encounter." -Show strength and weakening status. Explain weaken actions: +**Immediately set `catch_pending = True`** on the encounter to suppress auto-resolve +while the weakening Q&A is in progress: + +```python +import json, os +BUDDYMON_DIR = os.path.expanduser("~/.claude/buddymon") +enc_file = f"{BUDDYMON_DIR}/encounters.json" +encounters = json.load(open(enc_file)) +enc = encounters.get("active_encounter") +if enc: + enc["catch_pending"] = True + encounters["active_encounter"] = enc + json.dump(encounters, open(enc_file, "w"), indent=2) +``` + +Show strength and weakening status. If `enc.get("wounded")` is True, note that +it's already at 5% and a catch is near-guaranteed. Explain weaken actions: - Write a failing test → -20% strength - Isolate reproduction case → -20% strength - Add documenting comment → -10% strength Ask which weakening actions have been done. Apply reductions to `current_strength`. -Catch roll: +Catch roll (clear `catch_pending` before rolling — success clears encounter, failure +leaves it active without the flag so auto-resolve resumes naturally): ```python import json, os, random from datetime import datetime, timezone @@ -202,6 +219,9 @@ roster = json.load(open(roster_file)) enc = encounters.get("active_encounter") buddy_id = active.get("buddymon_id") +# Clear catch_pending before rolling (win or lose) +enc["catch_pending"] = False + buddy_data = (catalog.get("buddymon", {}).get(buddy_id) or catalog.get("evolutions", {}).get(buddy_id) or {}) buddy_level = roster.get("owned", {}).get(buddy_id, {}).get("level", 1) @@ -233,6 +253,9 @@ if success: json.dump(encounters, open(enc_file, "w"), indent=2) print(f"caught:{xp}") else: + # Save cleared catch_pending back on failure + encounters["active_encounter"] = enc + json.dump(encounters, open(enc_file, "w"), indent=2) print(f"failed:{int(catch_rate * 100)}") ```