feat(midsummer): rain of flowers/frogs/Swedish flags, dots become frogs, drop jingle
Per request: remove the WebAudio jingle (+ its 🔊 toggle and sound state); replace the one-shot confetti with a continuous rain of 🌼🌸🐸🇸🇪🌿 over the screen (MidsummerRain, gated by the theme, reduced-motion aware, leak-free); and replace player-dot markers with frogs themselves (override the inline dot color/border) instead of a flower-crown on top. Still toggled by the 🐸 Midsommar switch. Includes rebuilt static bundle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
7141a38c5c
commit
d86bc48862
24 changed files with 129 additions and 202 deletions
|
|
@ -1,8 +1,8 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useLiveData } from '../hooks/useLiveData';
|
||||
import { useMidsummerSound } from '../hooks/useMidsummerSound';
|
||||
import { PlayerDashboardContent } from './windows/PlayerDashboardWindow';
|
||||
import { MidsummerBanner } from './midsummer/MidsummerBanner';
|
||||
import { MidsummerRain } from './midsummer/MidsummerRain';
|
||||
|
||||
/**
|
||||
* Fullscreen "Player Dashboard" page — rendered when the React app loads
|
||||
|
|
@ -16,7 +16,6 @@ import { MidsummerBanner } from './midsummer/MidsummerBanner';
|
|||
*/
|
||||
export const PlayerDashboardFullPage: React.FC = () => {
|
||||
const data = useLiveData();
|
||||
useMidsummerSound();
|
||||
const [version, setVersion] = useState('');
|
||||
|
||||
// Set tab title.
|
||||
|
|
@ -41,6 +40,7 @@ export const PlayerDashboardFullPage: React.FC = () => {
|
|||
return (
|
||||
<div className="ml-dashboard-page">
|
||||
<MidsummerBanner />
|
||||
<MidsummerRain />
|
||||
<header className="ml-dashboard-header">
|
||||
<span className="ml-dashboard-title">👥 Player Dashboard</span>
|
||||
<span className="ml-dashboard-count">{count} online</span>
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import { WindowRenderer } from '../windows/WindowRenderer';
|
|||
import { RareNotification } from '../effects/RareNotification';
|
||||
import { DeathNotification } from '../effects/DeathNotification';
|
||||
import { usePlayerColors } from '../../hooks/usePlayerColors';
|
||||
import { useMidsummerSound } from '../../hooks/useMidsummerSound';
|
||||
import { MidsummerBanner } from '../midsummer/MidsummerBanner';
|
||||
import { MidsummerRain } from '../midsummer/MidsummerRain';
|
||||
import type { DashboardState } from '../../hooks/useLiveData';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -17,7 +17,6 @@ interface Props {
|
|||
|
||||
export const MapLayout: React.FC<Props> = ({ data }) => {
|
||||
const getColor = usePlayerColors();
|
||||
useMidsummerSound();
|
||||
const [showHeatmap, setShowHeatmap] = useState(false);
|
||||
const [showPortals, setShowPortals] = useState(false);
|
||||
const [selectedPlayer, setSelectedPlayer] = useState<string | null>(null);
|
||||
|
|
@ -45,6 +44,7 @@ export const MapLayout: React.FC<Props> = ({ data }) => {
|
|||
<WindowManagerProvider>
|
||||
<div className="ml-layout">
|
||||
<MidsummerBanner />
|
||||
<MidsummerRain />
|
||||
<Sidebar
|
||||
players={players}
|
||||
vitals={vitalsMap}
|
||||
|
|
|
|||
|
|
@ -1,34 +1,17 @@
|
|||
import React from 'react';
|
||||
import { useMidsummer } from '../../hooks/useMidsummer';
|
||||
import { playSmaGrodorna } from '../../hooks/useMidsummerSound';
|
||||
|
||||
/** 🐸 theme toggle + 🔊 jingle toggle, rendered among the sidebar tool links. */
|
||||
/** 🐸 midsummer theme toggle, rendered among the sidebar tool links. */
|
||||
export const FrogToggle: React.FC = () => {
|
||||
const { enabled, toggle, soundOn, toggleSound } = useMidsummer();
|
||||
const { enabled, toggle } = useMidsummer();
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
className="ml-tool-link"
|
||||
style={{ cursor: 'pointer' }}
|
||||
title={enabled ? 'Turn off the midsummer theme' : 'Turn on the midsummer theme'}
|
||||
onClick={toggle}
|
||||
>
|
||||
🐸 Midsommar {enabled ? 'on' : 'off'}
|
||||
</span>
|
||||
{enabled && (
|
||||
<span
|
||||
className="ml-tool-link"
|
||||
style={{ cursor: 'pointer' }}
|
||||
title={soundOn ? 'Mute the Små grodorna jingle' : 'Unmute the Små grodorna jingle'}
|
||||
onClick={() => {
|
||||
const turningOn = !soundOn;
|
||||
toggleSound();
|
||||
if (turningOn) playSmaGrodorna(); // this click is a user gesture
|
||||
}}
|
||||
>
|
||||
{soundOn ? '🔊' : '🔇'} Jingle
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
<span
|
||||
className="ml-tool-link"
|
||||
style={{ cursor: 'pointer' }}
|
||||
title={enabled ? 'Turn off the midsummer theme' : 'Turn on the midsummer theme'}
|
||||
onClick={toggle}
|
||||
>
|
||||
🐸 Midsommar {enabled ? 'on' : 'off'}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,23 +1,9 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { useMidsummer } from '../../hooks/useMidsummer';
|
||||
import { burstConfetti } from './confetti';
|
||||
|
||||
const CONFETTI_FLAG = 'mo-midsummer-confetti';
|
||||
|
||||
/** Festive top strip + a one-shot confetti burst on the first load of a
|
||||
* session while the theme is on. */
|
||||
/** Festive top strip shown while the theme is on. */
|
||||
export const MidsummerBanner: React.FC = () => {
|
||||
const { enabled } = useMidsummer();
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
if (sessionStorage.getItem(CONFETTI_FLAG)) return;
|
||||
sessionStorage.setItem(CONFETTI_FLAG, '1');
|
||||
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
burstConfetti();
|
||||
}
|
||||
}, [enabled]);
|
||||
|
||||
if (!enabled) return null;
|
||||
return (
|
||||
<div className="ms-banner" role="status">
|
||||
|
|
|
|||
44
frontend/src/components/midsummer/MidsummerRain.tsx
Normal file
44
frontend/src/components/midsummer/MidsummerRain.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useMidsummer } from '../../hooks/useMidsummer';
|
||||
|
||||
// Flowers, frogs and Swedish flags drifting down over the screen.
|
||||
const PIECES = ['🐸', '🌼', '🌸', '🇸🇪', '🌿'];
|
||||
|
||||
/**
|
||||
* Continuous gentle rain of flowers, frogs and Swedish flags while the theme
|
||||
* is on. Pure DOM + CSS (no React re-renders) so it doesn't fight the map's
|
||||
* high-frequency telemetry updates. Cleans up its layer, interval and any
|
||||
* in-flight pieces when the theme is toggled off or the component unmounts.
|
||||
* Skipped entirely under prefers-reduced-motion.
|
||||
*/
|
||||
export const MidsummerRain: React.FC = () => {
|
||||
const { enabled } = useMidsummer();
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||||
|
||||
const layer = document.createElement('div');
|
||||
layer.className = 'ms-rain';
|
||||
document.body.appendChild(layer);
|
||||
|
||||
const spawn = () => {
|
||||
const piece = document.createElement('span');
|
||||
piece.textContent = PIECES[Math.floor(Math.random() * PIECES.length)];
|
||||
piece.style.left = Math.floor(Math.random() * 100) + 'vw';
|
||||
const dur = 6 + Math.random() * 5; // 6–11s to fall
|
||||
piece.style.animationDuration = dur.toFixed(2) + 's';
|
||||
piece.style.fontSize = 14 + Math.floor(Math.random() * 16) + 'px';
|
||||
layer.appendChild(piece);
|
||||
window.setTimeout(() => piece.remove(), dur * 1000 + 250);
|
||||
};
|
||||
|
||||
const id = window.setInterval(spawn, 450);
|
||||
return () => {
|
||||
window.clearInterval(id);
|
||||
layer.remove();
|
||||
};
|
||||
}, [enabled]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
const EMOJIS = ['🐸', '🌼', '🌸', '🥂', '🌿'];
|
||||
|
||||
/** One-shot falling-emoji burst. Self-removes after the animation. */
|
||||
export function burstConfetti(count = 28): void {
|
||||
const layer = document.createElement('div');
|
||||
layer.className = 'ms-confetti';
|
||||
for (let i = 0; i < count; i++) {
|
||||
const p = document.createElement('span');
|
||||
p.textContent = EMOJIS[i % EMOJIS.length];
|
||||
p.style.left = Math.floor(Math.random() * 100) + 'vw';
|
||||
p.style.animationDelay = (Math.random() * 0.6).toFixed(2) + 's';
|
||||
p.style.fontSize = 12 + Math.floor(Math.random() * 14) + 'px';
|
||||
layer.appendChild(p);
|
||||
}
|
||||
document.body.appendChild(layer);
|
||||
window.setTimeout(() => layer.remove(), 4200);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue