feat: add 'state' field to mood activities for richer Discord presence
- Add 'state' field to all 139 activity entries in activities.yaml - Songs: state shows artist (e.g. 'by kz (livetune)') - Games: state shows genre (e.g. 'Rhythm Game', 'Sandbox', 'FPS') - Update pick_activity_for_mood() to return 3-tuple (type, name, state) - Update update_bot_presence() to pass state to discord.Activity() - Add state validation in set_activities_for_mood() (optional string) - Update Web UI editor: view shows state, edit form has state input - State is fully optional — backward compatible, no breaking changes The 'state' field appears as a secondary text line in Discord profile popup, the richest display possible for bot accounts (full Rich Presence with cover art/buttons is server-side restricted to OAuth applications).
This commit is contained in:
1008
bot/activities.yaml
1008
bot/activities.yaml
File diff suppressed because it is too large
Load Diff
@@ -6883,7 +6883,9 @@ function activitiesRenderView(section, mood, entries) {
|
||||
const label = entry.type === 'listening' ? 'Listening to' : 'Playing';
|
||||
html += `<div class="act-entry">`;
|
||||
html += `<span class="act-entry-icon">${icon}</span>`;
|
||||
html += `<span style="flex:1;">${escapeHtml(entry.name)}</span>`;
|
||||
html += `<span style="flex:1;">${escapeHtml(entry.name)}`;
|
||||
if (entry.state) html += ` <span style="color:#aaa; font-size:0.85rem;">— ${escapeHtml(entry.state)}</span>`;
|
||||
html += `</span>`;
|
||||
html += `<span style="color:#888; font-size:0.8rem;">weight: ${entry.weight}</span>`;
|
||||
html += `</div>`;
|
||||
}
|
||||
@@ -6902,8 +6904,9 @@ function activitiesRenderEditForm(section, mood, entries) {
|
||||
html += `<option value="listening" ${e.type === 'listening' ? 'selected' : ''}>🎵 Listening</option>`;
|
||||
html += `<option value="playing" ${e.type === 'playing' ? 'selected' : ''}>🎮 Playing</option>`;
|
||||
html += `</select>`;
|
||||
html += `<input type="text" id="act-name-${section}-${mood}-${i}" value="${escapeHtml(e.name)}" placeholder="Song/Game name">`;
|
||||
html += `<input type="number" id="act-weight-${section}-${mood}-${i}" value="${e.weight}" min="1" max="20">`;
|
||||
html += `<input type="text" id="act-name-${section}-${mood}-${i}" value="${escapeHtml(e.name)}" placeholder="Song/Game name" style="flex:2; min-width:120px;">`;
|
||||
html += `<input type="text" id="act-state-${section}-${mood}-${i}" value="${escapeHtml(e.state || '')}" placeholder="Artist / Genre (optional)" style="flex:1.5; min-width:100px;">`;
|
||||
html += `<input type="number" id="act-weight-${section}-${mood}-${i}" value="${e.weight}" min="1" max="20" style="width:60px;">`;
|
||||
html += `<button onclick="activitiesRemoveEntry('${section}','${mood}',${i})" style="background:#c0392b; padding:0.3rem 0.5rem;" title="Remove">✕</button>`;
|
||||
html += `</div>`;
|
||||
}
|
||||
@@ -6949,7 +6952,7 @@ function activitiesAddEntry(section, mood) {
|
||||
const key = `${section}/${mood}`;
|
||||
// First, sync current form values to cache
|
||||
activitiesSyncFormToCache(section, mood);
|
||||
activitiesEditCache[key].push({ type: 'listening', name: '', weight: 1 });
|
||||
activitiesEditCache[key].push({ type: 'listening', name: '', state: '', weight: 1 });
|
||||
activitiesRenderSection(section);
|
||||
// Keep the mood panel open
|
||||
const el = document.getElementById(`act-content-${section}-${mood}`);
|
||||
@@ -6971,9 +6974,11 @@ function activitiesSyncFormToCache(section, mood) {
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const typeEl = document.getElementById(`act-type-${section}-${mood}-${i}`);
|
||||
const nameEl = document.getElementById(`act-name-${section}-${mood}-${i}`);
|
||||
const stateEl = document.getElementById(`act-state-${section}-${mood}-${i}`);
|
||||
const weightEl = document.getElementById(`act-weight-${section}-${mood}-${i}`);
|
||||
if (typeEl) entries[i].type = typeEl.value;
|
||||
if (nameEl) entries[i].name = nameEl.value;
|
||||
if (stateEl) entries[i].state = stateEl.value || undefined;
|
||||
if (weightEl) entries[i].weight = parseInt(weightEl.value) || 1;
|
||||
}
|
||||
activitiesEditCache[key] = entries;
|
||||
|
||||
@@ -103,6 +103,8 @@ def set_activities_for_mood(mood_name: str, is_evil: bool, activities: list):
|
||||
raise ValueError(f"Entry {i} must have a non-empty string 'name'")
|
||||
if not isinstance(entry.get("weight", 0), int) or entry.get("weight", 0) < 1:
|
||||
raise ValueError(f"Entry {i} weight must be a positive integer")
|
||||
if "state" in entry and entry["state"] is not None and not isinstance(entry["state"], str):
|
||||
raise ValueError(f"Entry {i} 'state' must be a string if provided")
|
||||
|
||||
section = "evil" if is_evil else "normal"
|
||||
data = _load_activities()
|
||||
@@ -116,19 +118,19 @@ def pick_activity_for_mood(mood_name: str, is_evil: bool = False):
|
||||
"""Pick a weighted-random activity for a mood.
|
||||
|
||||
Returns:
|
||||
tuple: (activity_type, name) e.g. ("listening", "World is Mine")
|
||||
Returns ("listening", "Vocaloid") as fallback if mood has no entries.
|
||||
tuple: (activity_type, name, state) e.g. ("listening", "World is Mine", "by ryo (supercell)")
|
||||
state may be None if not defined. Fallback: ("listening", "Vocaloid", None)
|
||||
"""
|
||||
activities = get_activities_for_mood(mood_name, is_evil)
|
||||
|
||||
if not activities:
|
||||
logger.debug(f"No activities defined for {'evil/' if is_evil else ''}{mood_name}, using fallback")
|
||||
return ("listening", "Vocaloid")
|
||||
return ("listening", "Vocaloid", None)
|
||||
|
||||
# Weighted random selection
|
||||
weights = [entry.get("weight", 1) for entry in activities]
|
||||
chosen = random.choices(activities, weights=weights, k=1)[0]
|
||||
return (chosen["type"], chosen["name"])
|
||||
return (chosen["type"], chosen["name"], chosen.get("state"))
|
||||
|
||||
|
||||
async def update_bot_presence(mood_name: str, is_evil: bool = False):
|
||||
@@ -151,16 +153,19 @@ async def update_bot_presence(mood_name: str, is_evil: bool = False):
|
||||
logger.info("Set presence: idle (asleep)")
|
||||
return
|
||||
|
||||
activity_type, name = pick_activity_for_mood(mood_name, is_evil)
|
||||
activity_type, name, state = pick_activity_for_mood(mood_name, is_evil)
|
||||
|
||||
if activity_type == "listening":
|
||||
activity = discord.Activity(type=discord.ActivityType.listening, name=name)
|
||||
activity = discord.Activity(type=discord.ActivityType.listening, name=name, state=state)
|
||||
log_label = f"Listening to {name}"
|
||||
else:
|
||||
activity = discord.Game(name=name)
|
||||
activity = discord.Activity(type=discord.ActivityType.playing, name=name, state=state)
|
||||
log_label = f"Playing {name}"
|
||||
|
||||
await globals.client.change_presence(activity=activity)
|
||||
if state:
|
||||
log_label += f" ({state})"
|
||||
|
||||
await globals.client.change_presence(status=discord.Status.online, activity=activity)
|
||||
logger.info(f"Set presence: {log_label} (mood={'evil/' if is_evil else ''}{mood_name})")
|
||||
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user