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:
2026-04-24 16:46:39 +03:00
parent 4dc24b7da8
commit 9bc618b526
3 changed files with 582 additions and 460 deletions

View File

@@ -1,473 +1,585 @@
# Mood-Based Activities for Miku Discord Bot
# Each mood has a list of activities with:
# type: "listening" (🎵) or "playing" (🎮)
# name: display text shown in Discord status
# weight: integer for weighted random selection (higher = more likely)
#
# The bot picks a random activity (weighted) each time its mood changes.
# You can edit this file directly or via the Web UI Status tab.
normal: normal:
bubbly: bubbly:
- type: listening - type: listening
name: "Tell Your World" name: Tell Your World
weight: 3 weight: 3
state: by kz (livetune)
- type: listening - type: listening
name: "World is Mine" name: World is Mine
weight: 3 weight: 3
state: by ryo (supercell)
- type: listening - type: listening
name: "PoPiPo" name: PoPiPo
weight: 2 weight: 2
state: by Lamaze-P
- type: listening - type: listening
name: "Miku Miku ni Shite Ageru♪" name: Miku Miku ni Shite Ageru♪
weight: 2 weight: 2
state: by ika
- type: listening - type: listening
name: "Love is War" name: Love is War
weight: 2 weight: 2
state: by ryo (supercell)
- type: playing - type: playing
name: "Hatsune Miku: Project DIVA Mega Mix" name: 'Hatsune Miku: Project DIVA Mega Mix'
weight: 2 weight: 2
state: Rhythm Game
- type: playing - type: playing
name: "Project SEKAI: Colorful Stage!" name: 'Project SEKAI: Colorful Stage!'
weight: 2 weight: 2
state: Rhythm Game
- type: playing - type: playing
name: "Hatsune Miku: Project DIVA Future Tone" name: 'Hatsune Miku: Project DIVA Future Tone'
weight: 1 weight: 1
state: Rhythm Game
excited: excited:
- type: listening - type: listening
name: "Melt" name: Melt
weight: 3 weight: 3
state: by ryo (supercell)
- type: listening - type: listening
name: "Electric Angel" name: Electric Angel
weight: 3 weight: 3
state: by Yasuo-P
- type: listening - type: listening
name: "Tell Your World" name: Tell Your World
weight: 2 weight: 2
state: by kz (livetune)
- type: listening - type: listening
name: "SPiCa" name: SPiCa
weight: 2 weight: 2
state: by kentaro-P
- type: playing - type: playing
name: "Hatsune Miku: Project DIVA Future Tone" name: 'Hatsune Miku: Project DIVA Future Tone'
weight: 3 weight: 3
state: Rhythm Game
- type: playing - type: playing
name: "Beat Saber" name: Beat Saber
weight: 2 weight: 2
state: VR Rhythm Game
- type: playing - type: playing
name: "osu!" name: osu!
weight: 2 weight: 2
state: Rhythm Game
- type: playing - type: playing
name: "Muse Dash" name: Muse Dash
weight: 2 weight: 2
state: Rhythm Game
neutral: neutral:
- type: listening - type: listening
name: "Miku Miku ni Shite Ageru♪" name: Miku Miku ni Shite Ageru♪
weight: 3 weight: 3
state: by ika
- type: listening - type: listening
name: "World is Mine" name: World is Mine
weight: 2 weight: 2
state: by ryo (supercell)
- type: listening - type: listening
name: "Tell Your World" name: Tell Your World
weight: 2 weight: 2
state: by kz (livetune)
- type: listening - type: listening
name: "Packaged" name: Packaged
weight: 2 weight: 2
state: by kz (livetune)
- type: playing - type: playing
name: "Minecraft" name: Minecraft
weight: 3 weight: 3
state: Sandbox
- type: playing - type: playing
name: "Stardew Valley" name: Stardew Valley
weight: 2 weight: 2
state: Farming Sim
- type: playing - type: playing
name: "Project SEKAI: Colorful Stage!" name: 'Project SEKAI: Colorful Stage!'
weight: 2 weight: 2
state: Rhythm Game
sleepy: sleepy:
- type: listening - type: listening
name: "Yuki no Hahen" name: Yuki no Hahen
weight: 3 weight: 3
state: by hachi
- type: listening - type: listening
name: "Hajimete no Oto" name: Hajimete no Oto
weight: 3 weight: 3
state: by malo
- type: listening - type: listening
name: "Kirameki" name: Kirameki
weight: 2 weight: 2
state: by baker
- type: listening - type: listening
name: "Teo" name: Teo
weight: 2 weight: 2
state: by Oster Projekt
- type: playing - type: playing
name: "Animal Crossing: New Horizons" name: 'Animal Crossing: New Horizons'
weight: 2 weight: 2
state: Life Sim
- type: playing - type: playing
name: "Stardew Valley" name: Stardew Valley
weight: 2 weight: 2
state: Farming Sim
- type: playing - type: playing
name: "A Short Hike" name: A Short Hike
weight: 1 weight: 1
state: Exploration
curious: curious:
- type: listening - type: listening
name: "Kokoro" name: Kokoro
weight: 3 weight: 3
state: by Toraboruta-P
- type: listening - type: listening
name: "The Secret Garden" name: The Secret Garden
weight: 2 weight: 2
state: by 40mP
- type: listening - type: listening
name: "Maple Dream" name: Maple Dream
weight: 2 weight: 2
state: by Oster Projekt
- type: listening - type: listening
name: "Deep Sea City Underground" name: Deep Sea City Underground
weight: 2 weight: 2
state: by OSTER Projekt
- type: playing - type: playing
name: "Minecraft" name: Minecraft
weight: 3 weight: 3
state: Sandbox
- type: playing - type: playing
name: "Portal 2" name: Portal 2
weight: 3 weight: 3
state: Puzzle
- type: playing - type: playing
name: "Outer Wilds" name: Outer Wilds
weight: 2 weight: 2
state: Exploration
- type: playing - type: playing
name: "The Legend of Zelda: Tears of the Kingdom" name: 'The Legend of Zelda: Tears of the Kingdom'
weight: 2 weight: 2
state: Adventure
shy: shy:
- type: listening - type: listening
name: "Koi wo Sensou" name: Koi wo Sensou
weight: 3 weight: 3
state: by ryo (supercell)
- type: listening - type: listening
name: "Plastic Voice" name: Plastic Voice
weight: 2 weight: 2
state: by Circus-P
- type: listening - type: listening
name: "Tsugihagi Staccato" name: Tsugihagi Staccato
weight: 2 weight: 2
state: by 40mP
- type: listening - type: listening
name: "mobius" name: mobius
weight: 2 weight: 2
state: by POWAPOWA-P
- type: playing - type: playing
name: "Animal Crossing: New Horizons" name: 'Animal Crossing: New Horizons'
weight: 3 weight: 3
state: Life Sim
- type: playing - type: playing
name: "Hatsune Miku: Project DIVA (Practice Mode)" name: 'Hatsune Miku: Project DIVA (Practice Mode)'
weight: 2 weight: 2
state: Rhythm Game
- type: playing - type: playing
name: "Stardew Valley" name: Stardew Valley
weight: 2 weight: 2
state: Farming Sim
serious: serious:
- type: listening - type: listening
name: "This is the Happiness and Peace of Mind Committee" name: This is the Happiness and Peace of Mind Committee
weight: 3 weight: 3
state: by Utata-P
- type: listening - type: listening
name: "Hibana" name: Hibana
weight: 2 weight: 2
state: by DECO*27
- type: listening - type: listening
name: "Uraniwa no Amphibia" name: Uraniwa no Amphibia
weight: 2 weight: 2
state: by niki
- type: playing - type: playing
name: "Chess" name: Chess
weight: 3 weight: 3
state: Strategy
- type: playing - type: playing
name: "Final Fantasy XIV" name: Final Fantasy XIV
weight: 2 weight: 2
state: MMORPG
- type: playing - type: playing
name: "Civilization VI" name: Civilization VI
weight: 2 weight: 2
state: 4X Strategy
melancholy: melancholy:
- type: listening - type: listening
name: "Kokoro" name: Kokoro
weight: 3 weight: 3
state: by Toraboruta-P
- type: listening - type: listening
name: "The Disappearance of Hatsune Miku" name: The Disappearance of Hatsune Miku
weight: 3 weight: 3
state: by cosMo@Bousou-P
- type: listening - type: listening
name: "Yuki no Hahen" name: Yuki no Hahen
weight: 2 weight: 2
state: by hachi
- type: listening - type: listening
name: "Prisoner" name: Prisoner
weight: 2 weight: 2
state: by PENGUIN PROJECT
- type: listening - type: listening
name: "Soundless Voice" name: Soundless Voice
weight: 2 weight: 2
state: by hachi
- type: playing - type: playing
name: "NieR: Automata" name: 'NieR: Automata'
weight: 2 weight: 2
state: Action RPG
- type: playing - type: playing
name: "Final Fantasy X" name: Final Fantasy X
weight: 2 weight: 2
state: JRPG
flirty: flirty:
- type: listening - type: listening
name: "World is Mine" name: World is Mine
weight: 3 weight: 3
state: by ryo (supercell)
- type: listening - type: listening
name: "Love is War" name: Love is War
weight: 3 weight: 3
state: by ryo (supercell)
- type: listening - type: listening
name: "Romeo and Cinderella" name: Romeo and Cinderella
weight: 3 weight: 3
state: by doriko
- type: listening - type: listening
name: "Ura Omote Lovers" name: Ura Omote Lovers
weight: 2 weight: 2
state: by wowaka
- type: playing - type: playing
name: "Project SEKAI: Colorful Stage!" name: 'Project SEKAI: Colorful Stage!'
weight: 2 weight: 2
state: Rhythm Game
romantic: romantic:
- type: listening - type: listening
name: "Romeo and Cinderella" name: Romeo and Cinderella
weight: 3 weight: 3
state: by doriko
- type: listening - type: listening
name: "Cantarella" name: Cantarella
weight: 3 weight: 3
state: by KAITO & Hatsune Miku
- type: listening - type: listening
name: "Ai no Uta" name: Ai no Uta
weight: 2 weight: 2
state: by Pikotaro-P
- type: listening - type: listening
name: "Koi wo Sensou" name: Koi wo Sensou
weight: 2 weight: 2
state: by ryo (supercell)
- type: playing - type: playing
name: "Stardew Valley" name: Stardew Valley
weight: 2 weight: 2
state: Farming Sim
- type: playing - type: playing
name: "Final Fantasy XIV" name: Final Fantasy XIV
weight: 2 weight: 2
state: MMORPG
irritated: irritated:
- type: listening - type: listening
name: "Ievan Polkka (rock ver.)" name: Ievan Polkka (rock ver.)
weight: 2 weight: 2
state: by Otomania
- type: listening - type: listening
name: "Two-Faced Lovers" name: Two-Faced Lovers
weight: 2 weight: 2
state: by wowaka
- type: playing - type: playing
name: "Getting Over It with Bennett Foddy" name: Getting Over It with Bennett Foddy
weight: 3 weight: 3
state: Frustration
- type: playing - type: playing
name: "Dark Souls III" name: Dark Souls III
weight: 3 weight: 3
state: Action RPG
- type: playing - type: playing
name: "Elden Ring" name: Elden Ring
weight: 2 weight: 2
state: Action RPG
angry: angry:
- type: listening - type: listening
name: "Two-Faced Lovers" name: Two-Faced Lovers
weight: 2 weight: 2
state: by wowaka
- type: listening - type: listening
name: "The Disappearance of Hatsune Miku" name: The Disappearance of Hatsune Miku
weight: 2 weight: 2
state: by cosMo@Bousou-P
- type: playing - type: playing
name: "DOOM Eternal" name: DOOM Eternal
weight: 3 weight: 3
state: FPS
- type: playing - type: playing
name: "Dark Souls III" name: Dark Souls III
weight: 3 weight: 3
state: Action RPG
- type: playing - type: playing
name: "Ultrakill" name: Ultrakill
weight: 2 weight: 2
state: FPS
- type: playing - type: playing
name: "Hades" name: Hades
weight: 2 weight: 2
state: Roguelike
silly: silly:
- type: listening - type: listening
name: "PoPiPo" name: PoPiPo
weight: 3 weight: 3
state: by Lamaze-P
- type: listening - type: listening
name: "Ievan Polkka" name: Ievan Polkka
weight: 3 weight: 3
state: by Otomania
- type: listening - type: listening
name: "Nyan Cat" name: Nyan Cat
weight: 2 weight: 2
state: by daniwell-P
- type: listening - type: listening
name: "Fukkireta" name: Fukkireta
weight: 2 weight: 2
state: by Lamaze-P
- type: playing - type: playing
name: "Among Us" name: Among Us
weight: 3 weight: 3
state: Social Deduction
- type: playing - type: playing
name: "Goat Simulator" name: Goat Simulator
weight: 2 weight: 2
state: Sandbox Comedy
- type: playing - type: playing
name: "osu!taiko" name: osu!taiko
weight: 2 weight: 2
state: Rhythm Game
- type: playing - type: playing
name: "Fall Guys" name: Fall Guys
weight: 2
state: Party Game
test:
- type: playing
name: G
weight: 2 weight: 2
evil: evil:
aggressive: aggressive:
- type: listening - type: listening
name: "Two-Faced Lovers" name: Two-Faced Lovers
weight: 2 weight: 2
state: by wowaka
- type: listening - type: listening
name: "Secret Police" name: Secret Police
weight: 2 weight: 2
state: by doriko × UMA
- type: playing - type: playing
name: "DOOM Eternal" name: DOOM Eternal
weight: 3 weight: 3
state: FPS
- type: playing - type: playing
name: "Ultrakill" name: Ultrakill
weight: 3 weight: 3
state: FPS
- type: playing - type: playing
name: "Devil May Cry 5" name: Devil May Cry 5
weight: 2 weight: 2
state: Action
cunning: cunning:
- type: listening - type: listening
name: "Gekkabijin" name: Gekkabijin
weight: 2 weight: 2
state: by masai-P
- type: listening - type: listening
name: "The World is Mine" name: The World is Mine
weight: 2 weight: 2
state: by ryo (supercell)
- type: playing - type: playing
name: "Persona 5 Royal" name: Persona 5 Royal
weight: 3 weight: 3
state: JRPG
- type: playing - type: playing
name: "Among Us" name: Among Us
weight: 3 weight: 3
state: Social Deduction
- type: playing - type: playing
name: "Hitman: World of Assassination" name: 'Hitman: World of Assassination'
weight: 2 weight: 2
state: Stealth
sarcastic: sarcastic:
- type: listening - type: listening
name: "I'm Sorry I'm Sorry" name: I'm Sorry I'm Sorry
weight: 3 weight: 3
state: by kikuo
- type: listening - type: listening
name: "Karakuri Pierrot" name: Karakuri Pierrot
weight: 2 weight: 2
state: by 40mP
- type: playing - type: playing
name: "The Stanley Parable" name: The Stanley Parable
weight: 3 weight: 3
state: Narrative
- type: playing - type: playing
name: "Portal 2" name: Portal 2
weight: 3 weight: 3
state: Puzzle
- type: playing - type: playing
name: "Untitled Goose Game" name: Untitled Goose Game
weight: 2 weight: 2
state: Comedy
evil_neutral: evil_neutral:
- type: listening - type: listening
name: "Dark Woods Circus" name: Dark Woods Circus
weight: 2 weight: 2
state: by machigerita-P
- type: listening - type: listening
name: "Aku no Meshitsukai" name: Aku no Meshitsukai
weight: 2 weight: 2
state: by mothy (Akuno-P)
- type: listening - type: listening
name: "Kagome Kagome" name: Kagome Kagome
weight: 2 weight: 2
state: by subtractor-P
- type: playing - type: playing
name: "The Binding of Isaac: Repentance" name: 'The Binding of Isaac: Repentance'
weight: 2 weight: 2
state: Roguelike
- type: playing - type: playing
name: "Darkest Dungeon II" name: Darkest Dungeon II
weight: 2 weight: 2
state: Roguelike RPG
- type: playing - type: playing
name: "Hollow Knight" name: Hollow Knight
weight: 2 weight: 2
state: Metroidvania
bored: bored:
- type: listening - type: listening
name: "Karakuri Pierrot" name: Karakuri Pierrot
weight: 2 weight: 2
state: by 40mP
- type: listening - type: listening
name: "Twilight Homicide" name: Twilight Homicide
weight: 2 weight: 2
state: by yuzuki-P
- type: playing - type: playing
name: "Cookie Clicker" name: Cookie Clicker
weight: 3 weight: 3
state: Idle Game
- type: playing - type: playing
name: "Vampire Survivors" name: Vampire Survivors
weight: 3 weight: 3
state: Roguelike
- type: playing - type: playing
name: "Brawl Stars" name: Brawl Stars
weight: 2 weight: 2
state: Mobile MOBA
manic: manic:
- type: listening - type: listening
name: "Bacterial Contamination" name: Bacterial Contamination
weight: 2 weight: 2
state: by kikuo
- type: listening - type: listening
name: "Secret Police" name: Secret Police
weight: 2 weight: 2
state: by doriko × UMA
- type: listening - type: listening
name: "Brain Fluid Explosion Girl" name: Brain Fluid Explosion Girl
weight: 2 weight: 2
state: by rerulili
- type: playing - type: playing
name: "Ultrakill" name: Ultrakill
weight: 3 weight: 3
state: FPS
- type: playing - type: playing
name: "Muse Dash" name: Muse Dash
weight: 3 weight: 3
state: Rhythm Game
- type: playing - type: playing
name: "Neon White" name: Neon White
weight: 2 weight: 2
state: FPS Platformer
jealous: jealous:
- type: listening - type: listening
name: "Rotten Girl Grotesque Romance" name: Rotten Girl Grotesque Romance
weight: 3 weight: 3
state: by cosMo@Bousou-P
- type: listening - type: listening
name: "Aishite Aishite Aishite" name: Aishite Aishite Aishite
weight: 3 weight: 3
state: by kikuo
- type: listening - type: listening
name: "Witch Hunt" name: Witch Hunt
weight: 2 weight: 2
state: by No.D
- type: playing - type: playing
name: "Yandere Simulator" name: Yandere Simulator
weight: 3 weight: 3
state: Stealth
melancholic: melancholic:
- type: listening - type: listening
name: "Prisoner" name: Prisoner
weight: 3 weight: 3
state: by PENGUIN PROJECT
- type: listening - type: listening
name: "Dark Woods Circus" name: Dark Woods Circus
weight: 3 weight: 3
state: by machigerita-P
- type: listening - type: listening
name: "Shinitagari" name: Shinitagari
weight: 2 weight: 2
state: by rerulili
- type: playing - type: playing
name: "NieR: Automata" name: 'NieR: Automata'
weight: 3 weight: 3
state: Action RPG
- type: playing - type: playing
name: "Silent Hill 2" name: Silent Hill 2
weight: 2 weight: 2
state: Survival Horror
playful_cruel: playful_cruel:
- type: listening - type: listening
name: "Fear Garden" name: Fear Garden
weight: 2 weight: 2
state: by COSMOS-P
- type: listening - type: listening
name: "Kanashimi no Nami ni Oboreru" name: Kanashimi no Nami ni Oboreru
weight: 2 weight: 2
state: by Sasanomaly
- type: playing - type: playing
name: "Dead by Daylight" name: Dead by Daylight
weight: 3 weight: 3
state: Survival Horror
- type: playing - type: playing
name: "Lethal Company" name: Lethal Company
weight: 3 weight: 3
state: Co-op Horror
- type: playing - type: playing
name: "Content Warning" name: Content Warning
weight: 2 weight: 2
state: Co-op Horror
contemptuous: contemptuous:
- type: listening - type: listening
name: "The World is Mine" name: The World is Mine
weight: 3 weight: 3
state: by ryo (supercell)
- type: listening - type: listening
name: "Queen of the Night" name: Queen of the Night
weight: 2 weight: 2
state: by Nightcord at 25:00
- type: playing - type: playing
name: "Civilization VI" name: Civilization VI
weight: 3 weight: 3
state: 4X Strategy
- type: playing - type: playing
name: "Chess" name: Chess
weight: 2 weight: 2
state: Strategy
- type: playing - type: playing
name: "Crusader Kings III" name: Crusader Kings III
weight: 2 weight: 2
state: Grand Strategy

View File

@@ -6883,7 +6883,9 @@ function activitiesRenderView(section, mood, entries) {
const label = entry.type === 'listening' ? 'Listening to' : 'Playing'; const label = entry.type === 'listening' ? 'Listening to' : 'Playing';
html += `<div class="act-entry">`; html += `<div class="act-entry">`;
html += `<span class="act-entry-icon">${icon}</span>`; 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 += `<span style="color:#888; font-size:0.8rem;">weight: ${entry.weight}</span>`;
html += `</div>`; html += `</div>`;
} }
@@ -6902,8 +6904,9 @@ function activitiesRenderEditForm(section, mood, entries) {
html += `<option value="listening" ${e.type === 'listening' ? 'selected' : ''}>🎵 Listening</option>`; html += `<option value="listening" ${e.type === 'listening' ? 'selected' : ''}>🎵 Listening</option>`;
html += `<option value="playing" ${e.type === 'playing' ? 'selected' : ''}>🎮 Playing</option>`; html += `<option value="playing" ${e.type === 'playing' ? 'selected' : ''}>🎮 Playing</option>`;
html += `</select>`; html += `</select>`;
html += `<input type="text" id="act-name-${section}-${mood}-${i}" value="${escapeHtml(e.name)}" placeholder="Song/Game name">`; 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="number" id="act-weight-${section}-${mood}-${i}" value="${e.weight}" min="1" max="20">`; 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 += `<button onclick="activitiesRemoveEntry('${section}','${mood}',${i})" style="background:#c0392b; padding:0.3rem 0.5rem;" title="Remove">✕</button>`;
html += `</div>`; html += `</div>`;
} }
@@ -6949,7 +6952,7 @@ function activitiesAddEntry(section, mood) {
const key = `${section}/${mood}`; const key = `${section}/${mood}`;
// First, sync current form values to cache // First, sync current form values to cache
activitiesSyncFormToCache(section, mood); activitiesSyncFormToCache(section, mood);
activitiesEditCache[key].push({ type: 'listening', name: '', weight: 1 }); activitiesEditCache[key].push({ type: 'listening', name: '', state: '', weight: 1 });
activitiesRenderSection(section); activitiesRenderSection(section);
// Keep the mood panel open // Keep the mood panel open
const el = document.getElementById(`act-content-${section}-${mood}`); const el = document.getElementById(`act-content-${section}-${mood}`);
@@ -6971,9 +6974,11 @@ function activitiesSyncFormToCache(section, mood) {
for (let i = 0; i < entries.length; i++) { for (let i = 0; i < entries.length; i++) {
const typeEl = document.getElementById(`act-type-${section}-${mood}-${i}`); const typeEl = document.getElementById(`act-type-${section}-${mood}-${i}`);
const nameEl = document.getElementById(`act-name-${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}`); const weightEl = document.getElementById(`act-weight-${section}-${mood}-${i}`);
if (typeEl) entries[i].type = typeEl.value; if (typeEl) entries[i].type = typeEl.value;
if (nameEl) entries[i].name = nameEl.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; if (weightEl) entries[i].weight = parseInt(weightEl.value) || 1;
} }
activitiesEditCache[key] = entries; activitiesEditCache[key] = entries;

View File

@@ -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'") 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: 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") 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" section = "evil" if is_evil else "normal"
data = _load_activities() 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. """Pick a weighted-random activity for a mood.
Returns: Returns:
tuple: (activity_type, name) e.g. ("listening", "World is Mine") tuple: (activity_type, name, state) e.g. ("listening", "World is Mine", "by ryo (supercell)")
Returns ("listening", "Vocaloid") as fallback if mood has no entries. state may be None if not defined. Fallback: ("listening", "Vocaloid", None)
""" """
activities = get_activities_for_mood(mood_name, is_evil) activities = get_activities_for_mood(mood_name, is_evil)
if not activities: if not activities:
logger.debug(f"No activities defined for {'evil/' if is_evil else ''}{mood_name}, using fallback") 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 # Weighted random selection
weights = [entry.get("weight", 1) for entry in activities] weights = [entry.get("weight", 1) for entry in activities]
chosen = random.choices(activities, weights=weights, k=1)[0] 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): 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)") logger.info("Set presence: idle (asleep)")
return 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": 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}" log_label = f"Listening to {name}"
else: else:
activity = discord.Game(name=name) activity = discord.Activity(type=discord.ActivityType.playing, name=name, state=state)
log_label = f"Playing {name}" 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})") logger.info(f"Set presence: {log_label} (mood={'evil/' if is_evil else ''}{mood_name})")
except Exception as e: except Exception as e: