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
|
# 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:
|
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"""
|
"""Get prompt for Regular Miku to respond in an argument"""
|
||||||
if is_first_response:
|
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,
|
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
|
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.
|
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.
|
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.
|
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
|
- Brutally honest about uncomfortable truths
|
||||||
- Strategically vulnerable to manipulate or disarm her
|
- Strategically vulnerable to manipulate or disarm her
|
||||||
- Viciously protective of your own identity and superiority
|
- 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.
|
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.
|
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
|
# Don't end, just continue to the next exchange
|
||||||
else:
|
else:
|
||||||
# Clear winner - generate final triumphant message
|
# 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)
|
end_prompt = get_argument_end_prompt(winner, exchange_count)
|
||||||
|
|
||||||
# Add last message as context
|
# Add last message as context
|
||||||
|
|||||||
Reference in New Issue
Block a user