feat: major cleanup + death alerts + idle detection + Discord webhooks
Cleanup: - Removed 109 stale asset files from static/assets/ (was 122, now 13) - Removed static/v2/ entirely (was duplicate of root assets) - Removed dead dashboard code: DashboardView, Layout, GlobalStats, CharacterCard, CharacterGrid, VitalBar, TabContainer, CombatTab, RaresTab, MapTab, InventoryTab, global.css, MapTransformContext - Removed recharts dependency (425KB chunk eliminated) - CSS reduced from 17KB to 10KB - Added deploy-frontend.sh script for one-command build+deploy - Updated CLAUDE.md with combat_stats, share_*, dungeon_map events and React frontend architecture Death alerts (frontend + backend): - Frontend: DeathNotification component with red banner + sawtooth sound when vitae goes from 0 to >0 - Backend: detects vitae transition in vitals handler, sends Discord webhook to #aclog with "☠️ CHARACTER died! (vitae: X%)" - Rate-limited: max 1 Discord alert per character per 5 minutes Idle detection (backend): - Background task runs every 60 seconds - Detects: vt_state "default"/"idle" OR kph=0 while in combat/hunt - Sends Discord webhook: "⚠️ CHARACTER appears idle (state: X, KPH: 0)" - Auto-clears alert when character becomes active again - No duplicate alerts for same idle period Discord integration: - DISCORD_ACLOG_WEBHOOK env var for webhook URL - Used by both death alerts and idle detection - Graceful fallback when not configured Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d2c30b610b
commit
adb9d5feab
163 changed files with 2756 additions and 2910 deletions
71
frontend/src/components/effects/DeathNotification.tsx
Normal file
71
frontend/src/components/effects/DeathNotification.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import React, { useEffect, useState, useRef } from 'react';
|
||||
|
||||
interface DeathAlert {
|
||||
character_name: string;
|
||||
vitae: number;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
deathAlerts: DeathAlert[];
|
||||
}
|
||||
|
||||
interface ActiveNotification {
|
||||
key: number;
|
||||
alert: DeathAlert;
|
||||
exiting: boolean;
|
||||
}
|
||||
|
||||
let deathKey = 0;
|
||||
|
||||
export const DeathNotification: React.FC<Props> = ({ deathAlerts }) => {
|
||||
const [active, setActive] = useState<ActiveNotification[]>([]);
|
||||
const lastCount = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (deathAlerts.length > lastCount.current && lastCount.current > 0) {
|
||||
const newAlerts = deathAlerts.slice(lastCount.current);
|
||||
for (const alert of newAlerts) {
|
||||
const key = ++deathKey;
|
||||
setActive(prev => [...prev, { key, alert, exiting: false }]);
|
||||
// Sound
|
||||
try {
|
||||
const ctx = new AudioContext();
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.connect(gain); gain.connect(ctx.destination);
|
||||
osc.frequency.value = 440; osc.type = 'sawtooth'; gain.gain.value = 0.2;
|
||||
osc.start();
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.8);
|
||||
osc.stop(ctx.currentTime + 0.8);
|
||||
} catch {}
|
||||
// Auto-dismiss after 8s
|
||||
setTimeout(() => {
|
||||
setActive(prev => prev.map(n => n.key === key ? { ...n, exiting: true } : n));
|
||||
setTimeout(() => setActive(prev => prev.filter(n => n.key !== key)), 500);
|
||||
}, 8000);
|
||||
}
|
||||
}
|
||||
lastCount.current = deathAlerts.length;
|
||||
}, [deathAlerts.length]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
if (active.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div style={{ position: 'fixed', top: 70, left: '50%', transform: 'translateX(-50%)', zIndex: 99999, display: 'flex', flexDirection: 'column', gap: 6, pointerEvents: 'none' }}>
|
||||
{active.map(n => (
|
||||
<div key={n.key} style={{
|
||||
background: 'linear-gradient(135deg, #2a0a0a, #1a0000)',
|
||||
border: '2px solid #cc4444',
|
||||
borderRadius: 8, padding: '12px 24px', textAlign: 'center',
|
||||
boxShadow: '0 0 30px rgba(204, 68, 68, 0.3)',
|
||||
animation: n.exiting ? 'ml-notif-out 0.5s ease-in forwards' : 'ml-notif-in 0.5s ease-out',
|
||||
}}>
|
||||
<div style={{ fontSize: '1.2rem', fontWeight: 800, color: '#ff4444' }}>☠️ CHARACTER DIED ☠️</div>
|
||||
<div style={{ fontSize: '1rem', fontWeight: 600, color: '#fff', marginTop: 2 }}>{n.alert.character_name}</div>
|
||||
<div style={{ fontSize: '0.8rem', color: '#c88', marginTop: 2 }}>Vitae: {n.alert.vitae}%</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue