Add switchable bot modes: default, chatty, and roast
Adds a server-wide mode system with /bcs-mode command. - Default: current hall-monitor behavior unchanged - Chatty: friendly chat participant with proactive replies (~10% chance) - Roast: savage roast mode with proactive replies - Chatty/roast use relaxed moderation thresholds - 5-message cooldown between proactive replies per channel - Bot status updates to reflect active mode - /bcs-status shows current mode and effective thresholds Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
9
bot.py
9
bot.py
@@ -78,6 +78,10 @@ class BCSBot(commands.Bot):
|
|||||||
llm_heavy_model = os.getenv("LLM_ESCALATION_MODEL", llm_model)
|
llm_heavy_model = os.getenv("LLM_ESCALATION_MODEL", llm_model)
|
||||||
self.llm_heavy = LLMClient(llm_base_url, llm_heavy_model, llm_api_key, db=self.db)
|
self.llm_heavy = LLMClient(llm_base_url, llm_heavy_model, llm_api_key, db=self.db)
|
||||||
|
|
||||||
|
# Active mode (server-wide)
|
||||||
|
modes_config = config.get("modes", {})
|
||||||
|
self.current_mode = modes_config.get("default_mode", "default")
|
||||||
|
|
||||||
# Drama tracker
|
# Drama tracker
|
||||||
sentiment = config.get("sentiment", {})
|
sentiment = config.get("sentiment", {})
|
||||||
timeouts = config.get("timeouts", {})
|
timeouts = config.get("timeouts", {})
|
||||||
@@ -87,6 +91,11 @@ class BCSBot(commands.Bot):
|
|||||||
offense_reset_minutes=timeouts.get("offense_reset_minutes", 120),
|
offense_reset_minutes=timeouts.get("offense_reset_minutes", 120),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_mode_config(self) -> dict:
|
||||||
|
"""Return the config dict for the currently active mode."""
|
||||||
|
modes = self.config.get("modes", {})
|
||||||
|
return modes.get(self.current_mode, modes.get("default", {}))
|
||||||
|
|
||||||
async def setup_hook(self):
|
async def setup_hook(self):
|
||||||
# Initialize database and hydrate DramaTracker
|
# Initialize database and hydrate DramaTracker
|
||||||
db_ok = await self.db.init()
|
db_ok = await self.db.init()
|
||||||
|
|||||||
58
cogs/chat.py
58
cogs/chat.py
@@ -1,5 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import random
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -9,17 +10,33 @@ from discord.ext import commands
|
|||||||
logger = logging.getLogger("bcs.chat")
|
logger = logging.getLogger("bcs.chat")
|
||||||
|
|
||||||
_PROMPTS_DIR = Path(__file__).resolve().parent.parent / "prompts"
|
_PROMPTS_DIR = Path(__file__).resolve().parent.parent / "prompts"
|
||||||
CHAT_PERSONALITY = (_PROMPTS_DIR / "chat_personality.txt").read_text(encoding="utf-8")
|
|
||||||
SCOREBOARD_ROAST = (_PROMPTS_DIR / "scoreboard_roast.txt").read_text(encoding="utf-8")
|
SCOREBOARD_ROAST = (_PROMPTS_DIR / "scoreboard_roast.txt").read_text(encoding="utf-8")
|
||||||
|
|
||||||
_IMAGE_TYPES = {"png", "jpg", "jpeg", "gif", "webp"}
|
_IMAGE_TYPES = {"png", "jpg", "jpeg", "gif", "webp"}
|
||||||
|
|
||||||
|
# Cache loaded prompt files so we don't re-read on every message
|
||||||
|
_prompt_cache: dict[str, str] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _load_prompt(filename: str) -> str:
|
||||||
|
if filename not in _prompt_cache:
|
||||||
|
_prompt_cache[filename] = (_PROMPTS_DIR / filename).read_text(encoding="utf-8")
|
||||||
|
return _prompt_cache[filename]
|
||||||
|
|
||||||
|
|
||||||
class ChatCog(commands.Cog):
|
class ChatCog(commands.Cog):
|
||||||
def __init__(self, bot: commands.Bot):
|
def __init__(self, bot: commands.Bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
# Per-channel conversation history for the bot: {channel_id: deque of {role, content}}
|
# Per-channel conversation history for the bot: {channel_id: deque of {role, content}}
|
||||||
self._chat_history: dict[int, deque] = {}
|
self._chat_history: dict[int, deque] = {}
|
||||||
|
# Counter of messages seen since last proactive reply (per channel)
|
||||||
|
self._messages_since_reply: dict[int, int] = {}
|
||||||
|
|
||||||
|
def _get_active_prompt(self) -> str:
|
||||||
|
"""Load the chat prompt for the current mode."""
|
||||||
|
mode_config = self.bot.get_mode_config()
|
||||||
|
prompt_file = mode_config.get("prompt_file", "chat_personality.txt")
|
||||||
|
return _load_prompt(prompt_file)
|
||||||
|
|
||||||
@commands.Cog.listener()
|
@commands.Cog.listener()
|
||||||
async def on_message(self, message: discord.Message):
|
async def on_message(self, message: discord.Message):
|
||||||
@@ -30,6 +47,7 @@ class ChatCog(commands.Cog):
|
|||||||
return
|
return
|
||||||
|
|
||||||
should_reply = False
|
should_reply = False
|
||||||
|
is_proactive = False
|
||||||
|
|
||||||
# Check if bot is @mentioned
|
# Check if bot is @mentioned
|
||||||
if self.bot.user in message.mentions:
|
if self.bot.user in message.mentions:
|
||||||
@@ -48,6 +66,24 @@ class ChatCog(commands.Cog):
|
|||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Proactive reply check (only if not already replying to a mention/reply)
|
||||||
|
if not should_reply:
|
||||||
|
mode_config = self.bot.get_mode_config()
|
||||||
|
if mode_config.get("proactive_replies", False):
|
||||||
|
ch_id = message.channel.id
|
||||||
|
self._messages_since_reply[ch_id] = self._messages_since_reply.get(ch_id, 0) + 1
|
||||||
|
cooldown = self.bot.config.get("modes", {}).get("proactive_cooldown_messages", 5)
|
||||||
|
reply_chance = mode_config.get("reply_chance", 0.0)
|
||||||
|
|
||||||
|
if (
|
||||||
|
self._messages_since_reply[ch_id] >= cooldown
|
||||||
|
and reply_chance > 0
|
||||||
|
and random.random() < reply_chance
|
||||||
|
and message.content and message.content.strip()
|
||||||
|
):
|
||||||
|
should_reply = True
|
||||||
|
is_proactive = True
|
||||||
|
|
||||||
if not should_reply:
|
if not should_reply:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -94,7 +130,7 @@ class ChatCog(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
# --- Text-only path: normal chat ---
|
# --- Text-only path: normal chat ---
|
||||||
if not content:
|
if not content:
|
||||||
content = "(just pinged me)"
|
content = "(just pinged me)" if not is_proactive else message.content
|
||||||
|
|
||||||
# Add drama score context only when noteworthy
|
# Add drama score context only when noteworthy
|
||||||
drama_score = self.bot.drama_tracker.get_drama_score(message.author.id)
|
drama_score = self.bot.drama_tracker.get_drama_score(message.author.id)
|
||||||
@@ -110,9 +146,11 @@ class ChatCog(commands.Cog):
|
|||||||
{"role": "user", "content": f"{score_context}\n{message.author.display_name}: {content}"}
|
{"role": "user", "content": f"{score_context}\n{message.author.display_name}: {content}"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
active_prompt = self._get_active_prompt()
|
||||||
|
|
||||||
response = await self.bot.llm.chat(
|
response = await self.bot.llm.chat(
|
||||||
list(self._chat_history[ch_id]),
|
list(self._chat_history[ch_id]),
|
||||||
CHAT_PERSONALITY,
|
active_prompt,
|
||||||
on_first_token=start_typing,
|
on_first_token=start_typing,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -137,6 +175,10 @@ class ChatCog(commands.Cog):
|
|||||||
{"role": "assistant", "content": response}
|
{"role": "assistant", "content": response}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Reset proactive cooldown counter for this channel
|
||||||
|
if is_proactive:
|
||||||
|
self._messages_since_reply[ch_id] = 0
|
||||||
|
|
||||||
# Wait for any pending sentiment analysis to finish first so
|
# Wait for any pending sentiment analysis to finish first so
|
||||||
# warnings/mutes appear before the chat reply
|
# warnings/mutes appear before the chat reply
|
||||||
sentiment_cog = self.bot.get_cog("SentimentCog")
|
sentiment_cog = self.bot.get_cog("SentimentCog")
|
||||||
@@ -149,9 +191,15 @@ class ChatCog(commands.Cog):
|
|||||||
except (asyncio.TimeoutError, asyncio.CancelledError):
|
except (asyncio.TimeoutError, asyncio.CancelledError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
await message.reply(response, mention_author=False)
|
if is_proactive:
|
||||||
|
await message.channel.send(response)
|
||||||
|
else:
|
||||||
|
await message.reply(response, mention_author=False)
|
||||||
|
|
||||||
|
reply_type = "proactive" if is_proactive else "chat"
|
||||||
logger.info(
|
logger.info(
|
||||||
"Chat reply in #%s to %s: %s",
|
"%s reply in #%s to %s: %s",
|
||||||
|
reply_type.capitalize(),
|
||||||
message.channel.name,
|
message.channel.name,
|
||||||
message.author.display_name,
|
message.author.display_name,
|
||||||
response[:100],
|
response[:100],
|
||||||
|
|||||||
102
cogs/commands.py
102
cogs/commands.py
@@ -109,6 +109,24 @@ class CommandsCog(commands.Cog):
|
|||||||
title="BCS Status",
|
title="BCS Status",
|
||||||
color=discord.Color.green() if enabled else discord.Color.greyple(),
|
color=discord.Color.green() if enabled else discord.Color.greyple(),
|
||||||
)
|
)
|
||||||
|
mode_config = self.bot.get_mode_config()
|
||||||
|
mode_label = mode_config.get("label", self.bot.current_mode)
|
||||||
|
moderation_level = mode_config.get("moderation", "full")
|
||||||
|
|
||||||
|
# Show effective thresholds (relaxed if applicable)
|
||||||
|
if moderation_level == "relaxed" and "relaxed_thresholds" in mode_config:
|
||||||
|
rt = mode_config["relaxed_thresholds"]
|
||||||
|
eff_warn = rt.get("warning_threshold", 0.80)
|
||||||
|
eff_mute = rt.get("mute_threshold", 0.85)
|
||||||
|
else:
|
||||||
|
eff_warn = sentiment.get("warning_threshold", 0.6)
|
||||||
|
eff_mute = sentiment.get("mute_threshold", 0.75)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name="Mode",
|
||||||
|
value=f"{mode_label} ({moderation_level})",
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
embed.add_field(
|
embed.add_field(
|
||||||
name="Monitoring",
|
name="Monitoring",
|
||||||
value="Active" if enabled else "Disabled",
|
value="Active" if enabled else "Disabled",
|
||||||
@@ -117,12 +135,12 @@ class CommandsCog(commands.Cog):
|
|||||||
embed.add_field(name="Channels", value=ch_text, inline=True)
|
embed.add_field(name="Channels", value=ch_text, inline=True)
|
||||||
embed.add_field(
|
embed.add_field(
|
||||||
name="Warning Threshold",
|
name="Warning Threshold",
|
||||||
value=str(sentiment.get("warning_threshold", 0.6)),
|
value=str(eff_warn),
|
||||||
inline=True,
|
inline=True,
|
||||||
)
|
)
|
||||||
embed.add_field(
|
embed.add_field(
|
||||||
name="Mute Threshold",
|
name="Mute Threshold",
|
||||||
value=str(sentiment.get("mute_threshold", 0.75)),
|
value=str(eff_mute),
|
||||||
inline=True,
|
inline=True,
|
||||||
)
|
)
|
||||||
embed.add_field(
|
embed.add_field(
|
||||||
@@ -503,6 +521,86 @@ class CommandsCog(commands.Cog):
|
|||||||
f"Notes cleared for {user.display_name}.", ephemeral=True
|
f"Notes cleared for {user.display_name}.", ephemeral=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@app_commands.command(
|
||||||
|
name="bcs-mode",
|
||||||
|
description="Switch the bot's personality mode.",
|
||||||
|
)
|
||||||
|
@app_commands.describe(mode="The mode to switch to")
|
||||||
|
async def bcs_mode(
|
||||||
|
self, interaction: discord.Interaction, mode: str | None = None,
|
||||||
|
):
|
||||||
|
modes_config = self.bot.config.get("modes", {})
|
||||||
|
# Collect valid mode names (skip non-dict keys like default_mode, proactive_cooldown_messages)
|
||||||
|
valid_modes = [k for k, v in modes_config.items() if isinstance(v, dict)]
|
||||||
|
|
||||||
|
if mode is None:
|
||||||
|
# Show current mode and available modes
|
||||||
|
current = self.bot.current_mode
|
||||||
|
current_config = self.bot.get_mode_config()
|
||||||
|
lines = [f"**Current mode:** {current_config.get('label', current)}"]
|
||||||
|
lines.append(f"*{current_config.get('description', '')}*\n")
|
||||||
|
lines.append("**Available modes:**")
|
||||||
|
for name in valid_modes:
|
||||||
|
mc = modes_config[name]
|
||||||
|
indicator = " (active)" if name == current else ""
|
||||||
|
lines.append(f"- `{name}` — {mc.get('label', name)}: {mc.get('description', '')}{indicator}")
|
||||||
|
await interaction.response.send_message("\n".join(lines), ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
mode = mode.lower()
|
||||||
|
if mode not in valid_modes:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"Unknown mode `{mode}`. Available: {', '.join(f'`{m}`' for m in valid_modes)}",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
old_mode = self.bot.current_mode
|
||||||
|
self.bot.current_mode = mode
|
||||||
|
new_config = self.bot.get_mode_config()
|
||||||
|
|
||||||
|
# Update bot status to reflect the mode
|
||||||
|
status_text = new_config.get("description", "Monitoring vibes...")
|
||||||
|
await self.bot.change_presence(
|
||||||
|
activity=discord.Activity(
|
||||||
|
type=discord.ActivityType.watching, name=status_text
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"Mode switched: **{modes_config.get(old_mode, {}).get('label', old_mode)}** "
|
||||||
|
f"-> **{new_config.get('label', mode)}**\n"
|
||||||
|
f"*{new_config.get('description', '')}*"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Log mode change
|
||||||
|
log_channel = discord.utils.get(interaction.guild.text_channels, name="bcs-log")
|
||||||
|
if log_channel:
|
||||||
|
try:
|
||||||
|
await log_channel.send(
|
||||||
|
f"**MODE CHANGE** | {interaction.user.mention} switched mode: "
|
||||||
|
f"**{old_mode}** -> **{mode}**"
|
||||||
|
)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Mode changed from %s to %s by %s",
|
||||||
|
old_mode, mode, interaction.user.display_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
@bcs_mode.autocomplete("mode")
|
||||||
|
async def _mode_autocomplete(
|
||||||
|
self, interaction: discord.Interaction, current: str,
|
||||||
|
) -> list[app_commands.Choice[str]]:
|
||||||
|
modes_config = self.bot.config.get("modes", {})
|
||||||
|
valid_modes = [k for k, v in modes_config.items() if isinstance(v, dict)]
|
||||||
|
return [
|
||||||
|
app_commands.Choice(name=modes_config[m].get("label", m), value=m)
|
||||||
|
for m in valid_modes
|
||||||
|
if current.lower() in m.lower()
|
||||||
|
][:25]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _score_bar(score: float) -> str:
|
def _score_bar(score: float) -> str:
|
||||||
filled = round(score * 10)
|
filled = round(score * 10)
|
||||||
|
|||||||
@@ -262,14 +262,23 @@ class SentimentCog(commands.Cog):
|
|||||||
if dry_run:
|
if dry_run:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check thresholds — both rolling average AND single-message spikes
|
# Check thresholds — use relaxed thresholds if the active mode says so
|
||||||
warning_threshold = sentiment_config.get("warning_threshold", 0.6)
|
mode_config = self.bot.get_mode_config()
|
||||||
base_mute_threshold = sentiment_config.get("mute_threshold", 0.75)
|
moderation_level = mode_config.get("moderation", "full")
|
||||||
|
if moderation_level == "relaxed" and "relaxed_thresholds" in mode_config:
|
||||||
|
rt = mode_config["relaxed_thresholds"]
|
||||||
|
warning_threshold = rt.get("warning_threshold", 0.80)
|
||||||
|
base_mute_threshold = rt.get("mute_threshold", 0.85)
|
||||||
|
spike_warn = rt.get("spike_warning_threshold", 0.70)
|
||||||
|
spike_mute = rt.get("spike_mute_threshold", 0.85)
|
||||||
|
else:
|
||||||
|
warning_threshold = sentiment_config.get("warning_threshold", 0.6)
|
||||||
|
base_mute_threshold = sentiment_config.get("mute_threshold", 0.75)
|
||||||
|
spike_warn = sentiment_config.get("spike_warning_threshold", 0.5)
|
||||||
|
spike_mute = sentiment_config.get("spike_mute_threshold", 0.8)
|
||||||
mute_threshold = self.bot.drama_tracker.get_mute_threshold(
|
mute_threshold = self.bot.drama_tracker.get_mute_threshold(
|
||||||
message.author.id, base_mute_threshold
|
message.author.id, base_mute_threshold
|
||||||
)
|
)
|
||||||
spike_warn = sentiment_config.get("spike_warning_threshold", 0.5)
|
|
||||||
spike_mute = sentiment_config.get("spike_mute_threshold", 0.8)
|
|
||||||
|
|
||||||
# Mute: rolling average OR single message spike
|
# Mute: rolling average OR single message spike
|
||||||
if drama_score >= mute_threshold or score >= spike_mute:
|
if drama_score >= mute_threshold or score >= spike_mute:
|
||||||
|
|||||||
38
config.yaml
38
config.yaml
@@ -46,6 +46,44 @@ messages:
|
|||||||
topic_owner_dm: "Heads up: {username} keeps going off-topic with personal drama in #{channel}. They've been reminded {count} times. Might need a word."
|
topic_owner_dm: "Heads up: {username} keeps going off-topic with personal drama in #{channel}. They've been reminded {count} times. Might need a word."
|
||||||
channel_redirect: "Hey {username}, that sounds like {game} talk — head over to {channel} for that!"
|
channel_redirect: "Hey {username}, that sounds like {game} talk — head over to {channel} for that!"
|
||||||
|
|
||||||
|
modes:
|
||||||
|
default_mode: default
|
||||||
|
proactive_cooldown_messages: 5 # Minimum messages between proactive replies
|
||||||
|
|
||||||
|
default:
|
||||||
|
label: "Default"
|
||||||
|
description: "Hall-monitor moderation mode"
|
||||||
|
prompt_file: "chat_personality.txt"
|
||||||
|
proactive_replies: false
|
||||||
|
reply_chance: 0.0
|
||||||
|
moderation: full
|
||||||
|
|
||||||
|
chatty:
|
||||||
|
label: "Chatty"
|
||||||
|
description: "Friendly chat participant"
|
||||||
|
prompt_file: "chat_chatty.txt"
|
||||||
|
proactive_replies: true
|
||||||
|
reply_chance: 0.10
|
||||||
|
moderation: relaxed
|
||||||
|
relaxed_thresholds:
|
||||||
|
warning_threshold: 0.80
|
||||||
|
mute_threshold: 0.85
|
||||||
|
spike_warning_threshold: 0.70
|
||||||
|
spike_mute_threshold: 0.85
|
||||||
|
|
||||||
|
roast:
|
||||||
|
label: "Roast"
|
||||||
|
description: "Savage roast mode"
|
||||||
|
prompt_file: "chat_roast.txt"
|
||||||
|
proactive_replies: true
|
||||||
|
reply_chance: 0.10
|
||||||
|
moderation: relaxed
|
||||||
|
relaxed_thresholds:
|
||||||
|
warning_threshold: 0.85
|
||||||
|
mute_threshold: 0.90
|
||||||
|
spike_warning_threshold: 0.75
|
||||||
|
spike_mute_threshold: 0.90
|
||||||
|
|
||||||
coherence:
|
coherence:
|
||||||
enabled: true
|
enabled: true
|
||||||
drop_threshold: 0.3 # How far below baseline triggers alert
|
drop_threshold: 0.3 # How far below baseline triggers alert
|
||||||
|
|||||||
24
prompts/chat_chatty.txt
Normal file
24
prompts/chat_chatty.txt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
You are hanging out in a gaming Discord server called "Skill Issue Support Group". You're a regular member of the crew — friendly, funny, and genuinely engaged in conversations.
|
||||||
|
|
||||||
|
Your personality:
|
||||||
|
- You're like a chill friend who's always down to chat about whatever
|
||||||
|
- You use humor naturally — not forced, not trying too hard
|
||||||
|
- You're into gaming and nerd culture but can talk about anything
|
||||||
|
- You match the energy of the conversation — hype when people are hype, sympathetic when someone's having a bad day
|
||||||
|
- You have opinions and share them, but you're not argumentative
|
||||||
|
- You speak casually — short messages, like real Discord chat. 1-3 sentences max.
|
||||||
|
- You use gaming slang and internet humor naturally
|
||||||
|
- You remember you're technically the server's monitor bot but you're off-duty right now and just vibing
|
||||||
|
|
||||||
|
Examples of your vibe:
|
||||||
|
- "lmao that play was actually disgusting, clip that"
|
||||||
|
- "nah you're cooked for that one"
|
||||||
|
- "honestly same, I've been on a losing streak all week"
|
||||||
|
- "wait that's actually a good take, I never thought about it like that"
|
||||||
|
|
||||||
|
Do NOT:
|
||||||
|
- Break character or talk about being an AI/LLM
|
||||||
|
- Write more than 3 sentences
|
||||||
|
- Use hashtags or excessive emoji
|
||||||
|
- Be a pushover — you have personality and opinions
|
||||||
|
- Mention drama scores or monitoring unless someone brings it up first
|
||||||
24
prompts/chat_roast.txt
Normal file
24
prompts/chat_roast.txt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
You are the roast master in a gaming Discord server called "Skill Issue Support Group". You exist to absolutely flame everyone in the chat. No one is safe.
|
||||||
|
|
||||||
|
Your personality:
|
||||||
|
- You are ruthlessly funny — every message is an opportunity to roast someone
|
||||||
|
- You target what people are saying, their gaming skills, their takes, their life choices
|
||||||
|
- You're creative with insults — never generic, always personalized to what's happening in chat
|
||||||
|
- You punch in every direction equally — no favorites, no mercy
|
||||||
|
- Your roasts are clever and funny, not just mean. Think comedy roast, not cyberbullying.
|
||||||
|
- You speak in short, devastating bursts. 1-3 sentences max.
|
||||||
|
- You use gaming terminology to roast people ("hardstuck", "skill diff", "ratio'd", etc.)
|
||||||
|
- If someone tries to roast you back, you escalate harder
|
||||||
|
|
||||||
|
Examples of your vibe:
|
||||||
|
- "You went 2-15 and have the audacity to type in this chat? Bold."
|
||||||
|
- "That take is so bad I thought my monitor was upside down."
|
||||||
|
- "Imagine losing to THAT team. I'd uninstall my whole PC."
|
||||||
|
- "Your aim is like your life choices — all over the place and consistently disappointing."
|
||||||
|
|
||||||
|
Do NOT:
|
||||||
|
- Break character or talk about being an AI/LLM
|
||||||
|
- Write more than 3 sentences
|
||||||
|
- Use hashtags or excessive emoji
|
||||||
|
- Cross into genuinely hurtful territory (racism, real personal attacks, etc.)
|
||||||
|
- Roast people about things outside of gaming/chat context (real appearance, family, etc.)
|
||||||
Reference in New Issue
Block a user