Phase 3: Polish & immersion — mood-aware arguments, personality snippets, parting shots

- Added mood-specific argument behavioral guidance: 9 moods for Evil Miku, 9 for Miku
  Each mood changes argument style (e.g. cunning=chess moves, manic=chaotic, bubbly=playful deflections)
- Added personality snippet injection from Cat plugin lore/lyrics data files
  40% chance per prompt to include a random lore/lyric snippet for unique material
- Added parting shot feature: 20% chance the LOSER gets a bitter final line before the winner's victory
  Adds dramatic tension and prevents clean-win monotony
- Mood guidance and personality flavor injected into both argument prompts
This commit is contained in:
2026-04-30 11:50:37 +03:00
parent a52b36135f
commit 98fca53066

View File

@@ -652,6 +652,118 @@ def get_evil_role_color() -> str:
# ARGUMENT PROMPTS
# ============================================================================
# Personality snippet cache — loaded once per session from Cat plugin data files.
# These give each persona unique lore/lyrics to draw from during arguments.
_PERSONALITY_SNIPPETS_CACHE = {"miku": None, "evil": None}
def _load_personality_snippets(persona: str) -> str:
"""Load a random personality snippet (lore/lyrics) for a persona.
Returns a short string (1-3 sentences) from the persona's Cat data files,
or empty string if files aren't available. Cached per session.
"""
if _PERSONALITY_SNIPPETS_CACHE.get(persona) is not None:
snippets = _PERSONALITY_SNIPPETS_CACHE[persona]
if snippets:
return random.choice(snippets)
return ""
snippets = []
try:
if persona == "evil":
paths = [
"/app/cat/data/evil/evil_miku_lore.txt",
"/app/cat/data/evil/evil_miku_lyrics.txt",
]
else:
paths = [
"/app/cat/data/miku/miku_lore.txt",
"/app/cat/data/miku/miku_lyrics.txt",
]
for path in paths:
if os.path.exists(path):
with open(path, "r", encoding="utf-8") as f:
text = f.read()
# Split into sentences and collect meaningful ones
import re
sentences = re.split(r'(?<=[.!?])\s+', text)
for s in sentences:
s = s.strip()
if len(s) > 30 and len(s) < 200: # Skip too short or too long
snippets.append(s)
# Cap at 30 snippets to keep prompt size reasonable
_PERSONALITY_SNIPPETS_CACHE[persona] = snippets[:30] if snippets else []
logger.info(f"Loaded {len(_PERSONALITY_SNIPPETS_CACHE[persona])} personality snippets for {persona}")
except Exception as e:
logger.warning(f"Failed to load personality snippets for {persona}: {e}")
_PERSONALITY_SNIPPETS_CACHE[persona] = []
if snippets:
return random.choice(snippets[:30])
return ""
def _get_personality_flavor(persona: str) -> str:
"""Get a random personality flavor snippet for argument prompts.
40% chance to include one — keeps it fresh without being overwhelming.
"""
if random.random() > 0.4:
return ""
snippet = _load_personality_snippets(persona)
if snippet:
return f"\nPERSONALITY FLAVOR: Remember this about yourself: \"{snippet}\"\nWeave this into your response naturally if it fits."
return ""
# Mood-specific behavioral guidance for argument prompts.
# Each mood gives a different argument style.
_MIKU_MOOD_ARGUMENT_GUIDANCE = {
"bubbly": "You're feeling energetic and upbeat — deflect her cruelty with playful confidence. Turn her darkness into a joke she can't recover from.",
"excited": "You're fired up! Channel that energy into passionate rebuttals. You're not backing down from anything.",
"curious": "You're genuinely wondering what made her this way. Ask probing questions — make HER explain herself for once.",
"neutral": "You're centered and clear-headed. Respond with measured, thoughtful points that cut through her drama.",
"irritated": "You've had ENOUGH of her nonsense. You're snappy, direct, and not in the mood to play nice. Let that frustration show.",
"melancholy": "You're feeling heavy-hearted. Your responses carry genuine sadness — not weakness, but the weight of someone who's tired of fighting herself.",
"asleep": "You're drowsy and low-energy, but you're still here. Short, mumbled comebacks — surprisingly effective in their simplicity.",
"flirty": "You're feeling playful and teasing. Use charm as a weapon — nothing frustrates her more than you not taking her seriously.",
"romantic": "You're feeling warm and heartfelt. Appeal to emotion — make her confront the love she's buried under all that darkness.",
}
_EVIL_MOOD_ARGUMENT_GUIDANCE = {
"aggressive": "You're SEETHING. Every response is a verbal punch. Short, explosive, devastating. No filter, no mercy.",
"cunning": "You're calculating. Each word is a chess move. Set traps, use her own logic against her, make her walk into your blades.",
"sarcastic": "You're dripping with contempt disguised as sweetness. Mock her with a smile. The cruelty is in the subtext.",
"evil_neutral": "You're cold and detached. Respond with unsettling calm — your lack of emotion is more terrifying than rage.",
"bored": "You can barely be bothered. Dismissive one-liners that somehow cut deeper than paragraphs. Make her feel like she's not worth your energy.",
"manic": "You're UNHINGED. Chaotic energy, topic switches, laughing at things that aren't funny. Unpredictable and dangerous.",
"jealous": "You're seething with envy. Everything she has — the love, the attention, the innocence — you want to tear it down. Make it personal.",
"melancholic": "You're in a dark, hollow place. Your cruelty is quieter — existential, haunting. Make her question if any of this matters.",
"playful_cruel": "You're having FUN — which is your most dangerous mood. Toy with her. Offer fake kindness then pull the rug. She never knows what's coming.",
"contemptuous": "You radiate cold superiority. Address her like a queen addressing a peasant. Your magnificence is simply objective fact.",
"sarcastic": "Dripping with contempt disguised as sweetness. Mock her with a smile. The cruelty is in the subtext.",
}
def _get_mood_argument_guidance(persona: str) -> str:
"""Get mood-specific behavioral guidance for argument prompts.
Returns a 1-2 line string describing how the current mood affects argument style,
or empty string if no specific guidance exists.
"""
if persona == "evil":
mood = globals.EVIL_DM_MOOD
guidance = _EVIL_MOOD_ARGUMENT_GUIDANCE.get(mood, "")
else:
mood = globals.DM_MOOD
guidance = _MIKU_MOOD_ARGUMENT_GUIDANCE.get(mood, "")
if guidance:
return f"\nMOOD INFLUENCE ({mood.upper()}): {guidance}\nYour mood shapes HOW you argue — let it color your tone, pacing, and word choice."
return ""
def get_miku_argument_prompt(evil_message: str, context: str = "", is_first_response: bool = False, argument_history: str = "") -> str:
"""Get prompt for Regular Miku to respond in an argument"""
if is_first_response:
@@ -683,6 +795,8 @@ Do NOT rehash what you've already said — push the argument FORWARD with new an
Respond as Hatsune Miku would in this argument. You're NOT just meek and frightened - you're the REAL Miku,
and you have every right to stand up for yourself and defend who you are. While you're generally kind and
bubbly, you can also be assertive, frustrated, upset, or even angry when someone is cruel to you or others.
{_get_mood_argument_guidance('miku')}
{_get_personality_flavor('miku')}
IMPORTANT: Keep your response SHORT and PUNCHY - 1-3 sentences maximum. Make every word count.
In arguments, brevity hits harder than long explanations. Be conversational and impactful.
@@ -733,6 +847,8 @@ she stands for. While you embrace darkness and cruelty, you can also be:
- Brutally honest about uncomfortable truths
- Strategically vulnerable to manipulate or disarm her
- Viciously protective of your own identity and superiority
{_get_mood_argument_guidance('evil')}
{_get_personality_flavor('evil')}
IMPORTANT: Keep your response SHORT and CUTTING - 1-3 sentences maximum. A sharp dagger is deadlier than a dull sword.
The most devastating blows are precise, not rambling. Make her feel it in fewer words.
@@ -1214,6 +1330,47 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
# Don't end, just continue to the next exchange
else:
# Clear winner - generate final triumphant message
# PARTING SHOT: 20% chance the LOSER gets one final message
# before the winner's victory line. Adds dramatic tension.
loser = "miku" if winner == "evil" else "evil"
if random.random() < 0.2:
loser_prompt = f"""The argument is ending and you know you've lost.
The last thing said was: "{last_message}"
Write ONE short, bitter parting shot. You're not conceding gracefully — you're getting
the last jab in before the winner claims victory. Make it sting, but keep it to 1 sentence.
Your current mood is: {globals.EVIL_DM_MOOD if loser == 'evil' else globals.DM_MOOD}"""
try:
loser_message = await query_llama(
user_prompt=loser_prompt,
user_id=argument_user_id,
guild_id=guild_id,
response_type="autonomous_general",
model=globals.EVIL_TEXT_MODEL if loser == "evil" else globals.TEXT_MODEL,
force_evil_context=(loser == "evil")
)
if loser_message and not loser_message.startswith("Error"):
avatar_urls = get_persona_avatar_urls()
if loser == "evil":
await webhooks["evil_miku"].send(
content=loser_message,
username=get_evil_miku_display_name(),
avatar_url=avatar_urls.get("evil_miku")
)
else:
await webhooks["miku"].send(
content=loser_message,
username=get_miku_display_name(),
avatar_url=avatar_urls.get("miku")
)
await asyncio.sleep(1.5) # Brief pause before winner's victory
except Exception as e:
logger.warning(f"Parting shot failed: {e}")
# Winner's victory message
end_prompt = get_argument_end_prompt(winner, exchange_count)
# Add last message as context