Add personality system with emote variations and animations

- Rotating optimal faces with paired animations (breathing, floating, bouncing, swaying)
- Occasional idle expressions (winks/blinks) with 15% chance
- Recovery celebration emote with bounce animation
- Connection lost state with searching animation
- Face rotation every 5 minutes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-02 21:31:27 -06:00
parent 11896919e4
commit af4ccb9a35
2 changed files with 160 additions and 4 deletions

View File

@@ -4,6 +4,7 @@ A lightweight event broker that manages priority-based system status.
""" """
import json import json
import random
import threading import threading
import time import time
from datetime import datetime from datetime import datetime
@@ -16,6 +17,22 @@ ROOT_DIR = Path(__file__).parent
# Configuration # Configuration
STATUS_FILE = Path(__file__).parent / "status.json" STATUS_FILE = Path(__file__).parent / "status.json"
DEFAULT_NOTIFY_TTL = 10 # Default TTL for Priority 3 (Notify) events DEFAULT_NOTIFY_TTL = 10 # Default TTL for Priority 3 (Notify) events
CELEBRATION_DURATION = 5 # Seconds to show celebration after recovery
# Emote variations with paired animations
OPTIMAL_EMOTES = [
("( ^_^)", "breathing"), # calm, content
("( ᵔᴥᵔ)", "floating"), # dreamy
("(◕‿◕)", "bouncing"), # cheerful
("( ・ω・)", "swaying"), # curious
("( ˘▽˘)", "breathing"), # cozy
]
IDLE_EMOTES = [
("( -_^)", "blink"), # wink
("( ^_~)", "blink"), # wink
("( ᵕ.ᵕ)", "blink"), # blink
]
CELEBRATION_EMOTE = ("\\(^o^)/", "celebrating")
# Priority definitions # Priority definitions
PRIORITY_CONFIG = { PRIORITY_CONFIG = {
@@ -29,9 +46,20 @@ PRIORITY_CONFIG = {
events_lock = threading.Lock() events_lock = threading.Lock()
active_events = {} # id -> {priority, message, timestamp, ttl} active_events = {} # id -> {priority, message, timestamp, ttl}
# State tracking for personality
previous_priority = 4
celebrating_until = 0
last_emote_change = 0
current_optimal_emote = OPTIMAL_EMOTES[0][0]
current_optimal_animation = OPTIMAL_EMOTES[0][1]
def get_current_state(): def get_current_state():
"""Determine current state based on active events.""" """Determine current state based on active events."""
global previous_priority, celebrating_until, last_emote_change, current_optimal_emote, current_optimal_animation
now = time.time()
with events_lock: with events_lock:
if not active_events: if not active_events:
priority = 4 priority = 4
@@ -45,11 +73,38 @@ def get_current_state():
] ]
config = PRIORITY_CONFIG[priority] config = PRIORITY_CONFIG[priority]
emote = config["emote"]
animation = config["animation"]
color = config["color"]
# Check for recovery (was bad, now optimal)
if priority == 4 and previous_priority < 4:
celebrating_until = now + CELEBRATION_DURATION
previous_priority = priority
# Handle optimal state personality
if priority == 4:
if now < celebrating_until:
# Celebration mode
emote, animation = CELEBRATION_EMOTE
else:
# Rotate optimal emotes every 5 minutes, occasional idle expression
if now - last_emote_change > 300:
last_emote_change = now
# 15% chance of an idle expression (wink/blink)
if random.random() < 0.15:
current_optimal_emote, current_optimal_animation = random.choice(IDLE_EMOTES)
else:
current_optimal_emote, current_optimal_animation = random.choice(OPTIMAL_EMOTES)
emote = current_optimal_emote
animation = current_optimal_animation
return { return {
"current_state": config["name"].lower(), "current_state": config["name"].lower(),
"active_emote": config["emote"], "active_emote": emote,
"color": config["color"], "color": color,
"animation": config["animation"], "animation": animation,
"message": config["name"] if priority == 4 else f"{config['name']} state active", "message": config["name"] if priority == 4 else f"{config['name']} state active",
"active_events": sorted(events_list, key=lambda x: x["priority"]), "active_events": sorted(events_list, key=lambda x: x["priority"]),
"last_updated": datetime.now().isoformat(timespec="seconds"), "last_updated": datetime.now().isoformat(timespec="seconds"),

View File

@@ -76,6 +76,102 @@
transform: scale(1.08); transform: scale(1.08);
} }
} }
/* Celebrating animation - bounce and wiggle */
.celebrating {
animation: celebrate 0.5s ease-in-out infinite;
}
@keyframes celebrate {
0%, 100% {
transform: translateY(0) rotate(0deg);
}
25% {
transform: translateY(-10px) rotate(-5deg);
}
75% {
transform: translateY(-10px) rotate(5deg);
}
}
/* Floating animation - gentle drift */
.floating {
animation: float 4s ease-in-out infinite;
}
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-8px);
}
}
/* Bouncing animation - playful hop */
.bouncing {
animation: bounce 1s ease-in-out infinite;
}
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-12px);
}
}
/* Swaying animation - curious side-to-side */
.swaying {
animation: sway 3s ease-in-out infinite;
}
@keyframes sway {
0%, 100% {
transform: rotate(0deg);
}
25% {
transform: rotate(-3deg);
}
75% {
transform: rotate(3deg);
}
}
/* Blink animation - quick fade for winks */
.blink {
animation: blink 0.3s ease-in-out 1;
}
@keyframes blink {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.3;
}
}
/* Searching animation - looking around for connection */
.searching {
animation: search 2s ease-in-out infinite;
}
@keyframes search {
0%, 100% {
transform: translateX(0);
opacity: 0.6;
}
25% {
transform: translateX(-10px);
opacity: 0.8;
}
75% {
transform: translateX(10px);
opacity: 0.8;
}
}
</style> </style>
</head> </head>
<body> <body>
@@ -94,7 +190,12 @@
const data = await response.json(); const data = await response.json();
updateDisplay(data); updateDisplay(data);
} catch (err) { } catch (err) {
messageEl.textContent = 'Connection lost...'; // Connection lost state
emoteEl.textContent = '( ?.?)';
emoteEl.style.color = '#888888';
emoteEl.className = 'searching';
messageEl.style.color = '#888888';
messageEl.textContent = '';
} }
} }