Enhance sports feature: add asset management for various sports, improve animation effects, and refine confetti generation logic

This commit is contained in:
CodeDevMLH
2026-02-26 21:52:42 +01:00
parent a162b30bcd
commit 540d7f9baa
2 changed files with 247 additions and 27 deletions

View File

@@ -13,9 +13,14 @@
.sports-symbol {
position: absolute;
top: -10vh;
animation: sports-fall linear infinite;
/* Default is empty, assigned in JS */
font-size: 3rem; /* Fallback emoji size */
opacity: 0.9;
z-index: 40;
}
.sports-inner {
display: inline-block;
}
.sports-symbol img {
@@ -35,6 +40,19 @@
border-radius: 2px;
}
.sports-confetti.circle {
border-radius: 50%;
}
.sports-confetti.triangle {
width: 0 !important;
height: 0 !important;
background-color: transparent !important;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 10px solid var(--shape-color, #FFCC00);
}
.sports-turf {
position: absolute;
bottom: 0;
@@ -46,26 +64,60 @@
z-index: 10;
}
@keyframes sports-fall {
@keyframes sports-bounce {
0% {
transform: translateY(-10vh) rotate(var(--start-rot, 0deg));
transform: translateY(-10vh);
opacity: 0;
animation-timing-function: ease-in;
}
10% {
5% {
opacity: 1;
}
85% {
30% {
transform: translateY(85vh);
animation-timing-function: ease-out;
} /* hit ground, start bouncing up */
50% {
transform: translateY(40vh);
animation-timing-function: ease-in;
} /* peak of bounce, start falling */
70% {
transform: translateY(85vh);
animation-timing-function: ease-out;
} /* hit ground, bounce up */
85% {
transform: translateY(70vh);
animation-timing-function: ease-in;
} /* smaller peak */
95% {
opacity: 1;
}
100% {
transform: translateY(110vh) rotate(var(--end-rot, 360deg));
transform: translateY(110vh);
opacity: 0;
}
}
@keyframes sports-fall {
0% { transform: translateY(-10vh); opacity: 0; }
5% { opacity: 1; }
90% { opacity: 1; }
100% { transform: translateY(110vh); opacity: 0; }
}
@keyframes sports-sway {
0% { transform: rotate(-15deg) translateX(-10px); }
100% { transform: rotate(15deg) translateX(10px); }
}
@keyframes sports-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(var(--spin-rot, 360deg)); }
}
@keyframes sports-confetti-fall {
0% {
transform: translateY(-5vh) rotateX(0deg) rotateY(0deg);
transform: translateY(-5vh) rotate3d(var(--rx), var(--ry), var(--rz), 0deg);
opacity: 0;
}
5% {
@@ -75,7 +127,25 @@
opacity: 1;
}
100% {
transform: translateY(105vh) rotateX(720deg) rotateY(360deg);
transform: translateY(105vh) rotate3d(var(--rx), var(--ry), var(--rz), var(--rot-dir));
opacity: 0;
}
}
@keyframes sports-arc-x-right {
0% { transform: translateX(0); }
100% { transform: translateX(130vw); }
}
@keyframes sports-arc-x-left {
0% { transform: translateX(0); }
100% { transform: translateX(-130vw); }
}
@keyframes sports-arc-y {
0% { transform: translateY(110vh) scale(0.5) rotate(-30deg); opacity: 0; animation-timing-function: ease-out; }
5% { opacity: 1; }
50% { transform: translateY(10vh) scale(1.5) rotate(0deg); animation-timing-function: ease-in; }
95% { opacity: 1; }
100% { transform: translateY(110vh) scale(0.5) rotate(30deg); opacity: 0; }
}

View File

@@ -1,11 +1,29 @@
const config = window.SeasonalsPluginConfig?.Sports || {};
const sports = config.EnableSports !== undefined ? config.EnableSports : true;
const symbolCount = config.SymbolCount || 25;
const symbolCount = config.SymbolCount || 5;
const useRandomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true;
const enableRandomMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false;
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
// Pre-declare and manage image assets
const SPORTS_ASSETS = {
badminton: ['badminton_1', 'badminton_2'],
baseball: ['baseball_1', 'baseball_2'],
basketball: ['basketball_1', 'basketball_2'],
billiard: Array.from({length: 14}, (_, i) => `billiard_ball_${i + 1}`),
bowling: ['bowling_1', 'bowling_2'],
football: Array.from({length: 5}, (_, i) => `football_${i + 1}`),
golf: ['golf_ball_1', 'golf_ball_2'],
rugby: ['rugby_ball_1', 'rugby_ball_2'],
table_tennis: ['table_tennis_ball_1', 'table_tennis_ball_2'],
tennis: ['tennis_ball_1', 'tennis_ball_2'],
volleyball: ['volleyball_1', 'volleyball_2'],
waterball: ['waterball_1', 'waterball_2']
};
const turfColorHex = config.TurfColor || '#228b22';
let msgPrinted = false;
function toggleSports() {
@@ -48,9 +66,12 @@ function createSports() {
document.body.appendChild(container);
}
// Create a turf/grass overlay at the bottom
// Parse turf color config
// Create a turf/grass overlay at the bottom using the provided hex
const turf = document.createElement('div');
turf.className = 'sports-turf';
// Using hex with transparency (e.g., 4D = 30%, CC = 80%)
turf.style.background = `linear-gradient(180deg, transparent 0%, ${turfColorHex}4D 30%, ${turfColorHex}CC 100%)`;
container.appendChild(turf);
const standardCount = 15;
@@ -65,27 +86,64 @@ function createSports() {
const useRandomDuration = enableDifferentDuration !== false;
// Standard sports items to spawn
const activeItems = ['soccer', 'football', 'yellow_card', 'red_card', 'trophy'];
// Map standard sports balls to spawn based on category configuration
const rawSportsBalls = config.SportsBalls || 'football,basketball,tennis,volleyball';
const chosenCategories = rawSportsBalls.split(',').map(s => s.trim()).filter(s => s !== '');
// Assemble activeItems from categories
let activeItems = [];
chosenCategories.forEach(category => {
if (SPORTS_ASSETS[category]) {
activeItems.push(...SPORTS_ASSETS[category]);
} else {
// Legacy fallback (in case older explicit filenames remain in config string)
activeItems.push(category);
}
});
// Create falling sports items
if (activeItems.length === 0) activeItems.push(...SPORTS_ASSETS['football']); // fallback
// Track items we still need to show at least once
let guaranteedItems = [...activeItems];
// Create falling sports balls
for (let i = 0; i < finalCount; i++) {
let symbol = document.createElement('div');
// Randomly pick an item
const randomItem = activeItems[Math.floor(Math.random() * activeItems.length)];
// Pick a guaranteed ball first, otherwise pick completely randomly
let randomItem;
if (guaranteedItems.length > 0) {
const index = Math.floor(Math.random() * guaranteedItems.length);
randomItem = guaranteedItems[index];
guaranteedItems.splice(index, 1);
} else {
randomItem = activeItems[Math.floor(Math.random() * activeItems.length)];
}
symbol.className = `sports-symbol sports-${randomItem}`;
// Create inner div for spinning rotation
let innerDiv = document.createElement('div');
innerDiv.className = 'sports-inner';
// Try load image
let img = document.createElement('img');
img.src = `../Seasonals/Resources/sports_images/${randomItem}.png`;
img.src = `../Seasonals/Resources/sport_assets/${randomItem}.png`;
img.onerror = function() {
this.style.display = 'none'; // hide broken image icon
this.parentElement.innerHTML = getEmojiFallback(randomItem); // inject emoji fallback
};
symbol.appendChild(img);
innerDiv.appendChild(img);
// Balls should bounce infinitely
symbol.style.animationName = 'sports-bounce';
symbol.style.animationIterationCount = 'infinite';
innerDiv.style.animationName = 'sports-spin';
innerDiv.style.animationIterationCount = 'infinite';
symbol.appendChild(innerDiv);
const leftPos = Math.random() * 100;
const leftPos = Math.random() * 95;
const delaySeconds = Math.random() * 10;
let durationSeconds = 8;
@@ -93,10 +151,13 @@ function createSports() {
durationSeconds = Math.random() * 4 + 6; // 6 to 10 seconds
}
// Add a random slight rotation difference
const startRot = Math.random() * 360;
symbol.style.setProperty('--start-rot', `${startRot}deg`);
symbol.style.setProperty('--end-rot', `${startRot + (Math.random() > 0.5 ? 360 : -360)}deg`);
// Add a random spin
const spinRot = (Math.random() > 0.5 ? 360 : -360) + "deg";
innerDiv.style.setProperty('--spin-rot', spinRot);
// Duration for the spin should be different from fall to look natural
const spinDuration = Math.random() * 2 + 2;
innerDiv.style.animationDuration = `${spinDuration}s`;
symbol.style.left = `${leftPos}vw`;
symbol.style.animationDuration = `${durationSeconds}s`;
@@ -104,6 +165,66 @@ function createSports() {
container.appendChild(symbol);
}
// Create the periodic flying trophy arc
function launchTrophy() {
if (!document.querySelector('.sports-container')) return;
const flyFromLeft = Math.random() > 0.5;
let trophySymbol = document.createElement('div');
trophySymbol.className = "sports-symbol sports-trophy-wrapper";
let trophyInner = document.createElement('div');
trophyInner.className = "sports-inner sports-trophy-inner";
let trophyImg = document.createElement('img');
trophyImg.src = `../Seasonals/Resources/sport_assets/trophy.gif`;
// Randomly scale trophy slightly larger
trophyImg.style.transform = `scale(${Math.random() * 0.5 + 0.8})`;
trophyImg.onerror = function() {
this.style.display = 'none';
};
trophyInner.appendChild(trophyImg);
trophySymbol.appendChild(trophyInner);
if (flyFromLeft) {
trophySymbol.style.animationName = "sports-arc-x-right";
trophySymbol.style.left = "-15vw";
} else {
trophySymbol.style.animationName = "sports-arc-x-left";
trophySymbol.style.left = "115vw";
}
trophyInner.style.animationName = "sports-arc-y";
// Appearance timing
const arcDuration = 6 + Math.random() * 2;
trophySymbol.style.animationDuration = `${arcDuration}s`;
trophyInner.style.animationDuration = `${arcDuration}s`;
// Prevent looping for the trophy
trophySymbol.style.animationIterationCount = "1";
trophyInner.style.animationIterationCount = "1";
trophySymbol.style.animationFillMode = "forwards";
trophyInner.style.animationFillMode = "forwards";
container.appendChild(trophySymbol);
// Remove node after animation completes
setTimeout(() => {
if (trophySymbol && trophySymbol.parentNode) {
trophySymbol.parentNode.removeChild(trophySymbol);
}
}, arcDuration * 1000 + 500);
// Schedule the next trophy
setTimeout(launchTrophy, Math.random() * 20000 + 10000); // Wait 10-30s until next trophy
}
// Launch initial trophy after a short delay
setTimeout(launchTrophy, Math.random() * 5000 + 2000);
// Add Germany Colored confetti (Black, Red, Gold)
const confettiColors = ['#000000', '#FF0000', '#FFCC00'];
@@ -116,6 +237,23 @@ function createSports() {
const color = confettiColors[Math.floor(Math.random() * confettiColors.length)];
confetti.style.backgroundColor = color;
// Random shape generator for varied confetti
const shape = Math.random();
if (shape > 0.66) {
confetti.classList.add('circle');
const size = Math.random() * 5 + 5; // 5-10px
confetti.style.width = `${size}px`;
confetti.style.height = `${size}px`;
} else if (shape > 0.33) {
confetti.classList.add('rect');
const width = Math.random() * 4 + 4; // 4-8px
const height = Math.random() * 5 + 8; // 8-13px
confetti.style.width = `${width}px`;
confetti.style.height = `${height}px`;
} else {
confetti.classList.add('triangle');
}
const leftPos = Math.random() * 100;
const delaySeconds = Math.random() * 8;
const duration = Math.random() * 3 + 4; // 4 to 7 seconds
@@ -123,17 +261,29 @@ function createSports() {
confetti.style.left = `${leftPos}vw`;
confetti.style.animationDuration = `${duration}s`;
confetti.style.animationDelay = `${delaySeconds}s`;
// Random 3D Rotation for flutter
confetti.style.setProperty('--rx', Math.random().toFixed(2));
confetti.style.setProperty('--ry', Math.random().toFixed(2));
confetti.style.setProperty('--rz', (Math.random() * 0.5).toFixed(2));
confetti.style.setProperty('--rot-dir', `${(Math.random() > 0.5 ? 1 : -1) * 360}deg`);
container.appendChild(confetti);
}
}
function getEmojiFallback(type) {
if (type === 'soccer') return '⚽';
if (type === 'football') return '🏈';
if (type === 'yellow_card') return '🟨';
if (type === 'red_card') return '🟥';
if (type === 'trophy') return '🏆';
if (type.includes('soccer') || type.includes('football')) return '⚽';
if (type.includes('baseball')) return '';
if (type.includes('basketball')) return '🏀';
if (type.includes('billiard')) return '🎱';
if (type.includes('bowling')) return '🎳';
if (type.includes('golf')) return '⛳';
if (type.includes('rugby')) return '🏈';
if (type.includes('tennis')) return '🎾';
if (type.includes('volleyball')) return '🏐';
if (type.includes('badminton')) return '🏸';
if (type.includes('waterball')) return '🤽';
return '';
}