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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user