From 3797e21912a3e298f7e5b8fb4940d64e3222770d Mon Sep 17 00:00:00 2001 From: tomt610 Date: Mon, 12 Jan 2026 22:45:48 +0000 Subject: [PATCH 1/3] Improve clear weather effects with day/night cycle - Replace blinking sunray lines with pleasant daytime effects: - Warm ambient glow overlay - Floating golden dust motes/pollen particles - Soft drifting light orbs - Subtle lens flare in corner - Add automatic nighttime detection from Info Box time data: - Parses various time formats (12h, 24h, descriptive) - Night mode activates 8 PM - 5 AM - Add nighttime clear weather effects: - Moon with realistic shading and glow (positioned left) - Twinkling stars with bright star cross-flares - Floating fireflies with gentle glow - Occasional shooting star animation - Add mobile optimizations for all new effects --- src/systems/ui/weatherEffects.js | 233 +++++++++++++++- style.css | 463 +++++++++++++++++++++++++++++-- 2 files changed, 658 insertions(+), 38 deletions(-) diff --git a/src/systems/ui/weatherEffects.js b/src/systems/ui/weatherEffects.js index 5328019..04228b1 100644 --- a/src/systems/ui/weatherEffects.js +++ b/src/systems/ui/weatherEffects.js @@ -4,9 +4,105 @@ */ import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js'; +import { repairJSON } from '../../utils/jsonRepair.js'; let weatherContainer = null; let currentWeatherType = null; +let currentTimeOfDay = null; + +/** + * Parse time string to extract hour (24-hour format) + * Supports formats like "3:00 PM", "15:00", "3 PM", "Evening", etc. + */ +function parseHourFromTime(timeStr) { + if (!timeStr) return null; + + const text = timeStr.toLowerCase().trim(); + + // Check for descriptive time words first + if (text.includes('dawn') || text.includes('sunrise')) return 6; + if (text.includes('early morning')) return 7; + if (text.includes('morning')) return 9; + if (text.includes('midday') || text.includes('noon') || text.includes('mid-day')) return 12; + if (text.includes('afternoon')) return 14; + if (text.includes('late afternoon')) return 16; + if (text.includes('evening') || text.includes('dusk') || text.includes('sunset')) return 19; + if (text.includes('twilight')) return 20; + if (text.includes('night') || text.includes('nighttime')) return 22; + if (text.includes('midnight')) return 0; + if (text.includes('late night')) return 2; + + // Try to parse numeric time formats + // Format: "3:00 PM" or "3:00PM" or "3 PM" + const ampmMatch = text.match(/(\d{1,2})(?::(\d{2}))?\s*(am|pm)/i); + if (ampmMatch) { + let hour = parseInt(ampmMatch[1], 10); + const isPM = ampmMatch[3].toLowerCase() === 'pm'; + if (isPM && hour !== 12) hour += 12; + if (!isPM && hour === 12) hour = 0; + return hour; + } + + // Format: "15:00" (24-hour) + const militaryMatch = text.match(/(\d{1,2}):(\d{2})/); + if (militaryMatch) { + return parseInt(militaryMatch[1], 10); + } + + return null; +} + +/** + * Determine time of day based on hour + */ +function getTimeOfDay(hour) { + if (hour === null) return 'unknown'; + + // Night: 8 PM (20:00) to 5 AM (05:00) + if (hour >= 20 || hour < 5) return 'night'; + + // Dawn/Dusk: 5 AM - 7 AM and 6 PM - 8 PM + if (hour >= 5 && hour < 7) return 'dawn'; + if (hour >= 18 && hour < 20) return 'dusk'; + + // Day: 7 AM to 6 PM + return 'day'; +} + +/** + * Extract time from Info Box data + */ +function getCurrentTime() { + const infoBoxData = lastGeneratedData.infoBox || committedTrackerData.infoBox || ''; + + // Try to parse as JSON first (new format) + try { + const parsed = typeof infoBoxData === 'string' ? repairJSON(infoBoxData) : infoBoxData; + if (parsed && parsed.time) { + // Use the end time if available (current time), otherwise start time + return parsed.time.end || parsed.time.start || null; + } + } catch (e) { + // Not JSON, try old text format + } + + // Fallback: Parse the old text format to find Time field + const lines = infoBoxData.split('\n'); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed.startsWith('Time:')) { + const timeStr = trimmed.substring('Time:'.length).trim(); + // If it contains →, take the end time (after arrow) + if (timeStr.includes('→')) { + const parts = timeStr.split('→'); + return parts[1]?.trim() || parts[0]?.trim(); + } + return timeStr; + } + } + + return null; +} /** * Parse weather text to determine effect type @@ -136,22 +232,119 @@ function createMist() { } /** - * Create sunshine rays effect + * Create clear/sunny weather effect with floating particles and warm glow */ function createSunshine() { const container = document.createElement('div'); - container.className = 'rpg-weather-particles'; + container.className = 'rpg-weather-particles rpg-clear-weather'; - // Create 8 sun rays - for (let i = 0; i < 8; i++) { - const ray = document.createElement('div'); - ray.className = 'rpg-weather-particle rpg-sunray'; - ray.style.left = `${10 + i * 12}%`; - ray.style.animationDelay = `${i * 0.5}s`; - ray.style.animationDuration = `${8 + Math.random() * 4}s`; - container.appendChild(ray); + // Create warm ambient glow overlay + const ambientGlow = document.createElement('div'); + ambientGlow.className = 'rpg-weather-particle rpg-clear-ambient-glow'; + container.appendChild(ambientGlow); + + // Create floating dust motes / pollen particles (golden sparkles) + for (let i = 0; i < 25; i++) { + const particle = document.createElement('div'); + particle.className = 'rpg-weather-particle rpg-clear-dust-mote'; + particle.style.left = `${Math.random() * 100}%`; + particle.style.top = `${Math.random() * 100}%`; + particle.style.animationDelay = `${Math.random() * 15}s`; + particle.style.animationDuration = `${12 + Math.random() * 8}s`; + // Vary the size slightly + const size = 2 + Math.random() * 4; + particle.style.width = `${size}px`; + particle.style.height = `${size}px`; + container.appendChild(particle); } + // Create soft light orbs that drift gently + for (let i = 0; i < 6; i++) { + const orb = document.createElement('div'); + orb.className = 'rpg-weather-particle rpg-clear-light-orb'; + orb.style.left = `${10 + Math.random() * 80}%`; + orb.style.top = `${10 + Math.random() * 80}%`; + orb.style.animationDelay = `${i * 2}s`; + orb.style.animationDuration = `${20 + Math.random() * 10}s`; + // Vary the size + const size = 80 + Math.random() * 120; + orb.style.width = `${size}px`; + orb.style.height = `${size}px`; + container.appendChild(orb); + } + + // Create lens flare effect in corner + const lensFlare = document.createElement('div'); + lensFlare.className = 'rpg-weather-particle rpg-clear-lens-flare'; + container.appendChild(lensFlare); + + return container; +} + +/** + * Create clear nighttime weather effect with moon, stars, and fireflies + */ +function createNighttime() { + const container = document.createElement('div'); + container.className = 'rpg-weather-particles rpg-night-weather'; + + // Create dark blue ambient overlay + const nightOverlay = document.createElement('div'); + nightOverlay.className = 'rpg-weather-particle rpg-night-overlay'; + container.appendChild(nightOverlay); + + // Create the moon + const moon = document.createElement('div'); + moon.className = 'rpg-weather-particle rpg-night-moon'; + container.appendChild(moon); + + // Create moon glow + const moonGlow = document.createElement('div'); + moonGlow.className = 'rpg-weather-particle rpg-night-moon-glow'; + container.appendChild(moonGlow); + + // Create twinkling stars + for (let i = 0; i < 60; i++) { + const star = document.createElement('div'); + star.className = 'rpg-weather-particle rpg-night-star'; + star.style.left = `${Math.random() * 100}%`; + star.style.top = `${Math.random() * 60}%`; // Stars mostly in upper portion + star.style.animationDelay = `${Math.random() * 5}s`; + star.style.animationDuration = `${2 + Math.random() * 3}s`; + // Vary the size + const size = 1 + Math.random() * 2; + star.style.width = `${size}px`; + star.style.height = `${size}px`; + container.appendChild(star); + } + + // Create a few brighter stars + for (let i = 0; i < 8; i++) { + const brightStar = document.createElement('div'); + brightStar.className = 'rpg-weather-particle rpg-night-star rpg-night-star-bright'; + brightStar.style.left = `${Math.random() * 100}%`; + brightStar.style.top = `${Math.random() * 50}%`; + brightStar.style.animationDelay = `${Math.random() * 4}s`; + brightStar.style.animationDuration = `${3 + Math.random() * 2}s`; + container.appendChild(brightStar); + } + + // Create fireflies / floating light particles + for (let i = 0; i < 15; i++) { + const firefly = document.createElement('div'); + firefly.className = 'rpg-weather-particle rpg-night-firefly'; + firefly.style.left = `${Math.random() * 100}%`; + firefly.style.top = `${40 + Math.random() * 55}%`; // Fireflies in lower portion + firefly.style.animationDelay = `${Math.random() * 10}s`; + firefly.style.animationDuration = `${8 + Math.random() * 7}s`; + container.appendChild(firefly); + } + + // Create subtle shooting star occasionally + const shootingStar = document.createElement('div'); + shootingStar.className = 'rpg-weather-particle rpg-night-shooting-star'; + container.appendChild(shootingStar); + return container; } @@ -198,11 +391,12 @@ function removeWeatherEffect() { weatherContainer.remove(); weatherContainer = null; currentWeatherType = null; + currentTimeOfDay = null; } } /** - * Update weather effect based on current weather + * Update weather effect based on current weather and time */ export function updateWeatherEffect() { // Check if dynamic weather is enabled @@ -214,8 +408,13 @@ export function updateWeatherEffect() { const weather = getCurrentWeather(); const weatherType = parseWeatherType(weather); - // Don't recreate if weather hasn't changed - if (weatherType === currentWeatherType) { + // Get current time of day + const timeStr = getCurrentTime(); + const hour = parseHourFromTime(timeStr); + const timeOfDay = getTimeOfDay(hour); + + // Don't recreate if weather and time haven't changed + if (weatherType === currentWeatherType && timeOfDay === currentTimeOfDay) { return; } @@ -228,6 +427,7 @@ export function updateWeatherEffect() { } currentWeatherType = weatherType; + currentTimeOfDay = timeOfDay; switch (weatherType) { case 'snow': @@ -240,7 +440,12 @@ export function updateWeatherEffect() { weatherContainer = createMist(); break; case 'sunny': - weatherContainer = createSunshine(); + // Use nighttime effect for clear weather at night + if (timeOfDay === 'night') { + weatherContainer = createNighttime(); + } else { + weatherContainer = createSunshine(); + } break; case 'wind': weatherContainer = createWind(); diff --git a/style.css b/style.css index 0d0b9c3..313c75e 100644 --- a/style.css +++ b/style.css @@ -9643,40 +9643,425 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play { background: radial-gradient(ellipse at center, rgba(220, 220, 240, 0.25) 0%, transparent 70%); } -/* Sunshine rays */ -.rpg-sunray { - width: 3px; - height: 100dvh; - background: linear-gradient(to bottom, - rgba(255, 250, 200, 0) 0%, - rgba(255, 250, 200, 0.3) 20%, - rgba(255, 250, 200, 0.15) 50%, - rgba(255, 250, 200, 0) 100%); - transform-origin: top; - animation: rpg-sunrayShine ease-in-out infinite; - top: -20%; - filter: blur(2px); +/* Clear/Sunny Weather Effects */ + +/* Container modifier for clear weather */ +.rpg-clear-weather { + overflow: hidden; } -@keyframes rpg-sunrayShine { +/* Warm ambient glow overlay */ +.rpg-clear-ambient-glow { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: radial-gradient(ellipse at 15% 10%, + rgba(255, 245, 200, 0.15) 0%, + rgba(255, 240, 180, 0.08) 30%, + transparent 70%); + animation: rpg-clear-ambient-pulse 8s ease-in-out infinite; + pointer-events: none; +} + +@keyframes rpg-clear-ambient-pulse { 0%, 100% { - opacity: 0.2; - transform: translateY(0) scaleY(1); + opacity: 0.6; } 50% { - opacity: 0.4; - transform: translateY(5%) scaleY(1.05); + opacity: 1; } } -.rpg-sunray:nth-child(2n) { - width: 4px; - animation-duration: 10s; +/* Floating dust motes / pollen particles */ +.rpg-clear-dust-mote { + position: fixed; + border-radius: 50%; + background: radial-gradient(circle at 30% 30%, + rgba(255, 250, 220, 0.9) 0%, + rgba(255, 235, 180, 0.6) 50%, + rgba(255, 220, 150, 0) 100%); + box-shadow: 0 0 6px 2px rgba(255, 245, 200, 0.4); + animation: rpg-clear-dust-float linear infinite; + pointer-events: none; } -.rpg-sunray:nth-child(3n) { +@keyframes rpg-clear-dust-float { + 0% { + transform: translate(0, 0) scale(1); + opacity: 0; + } + 10% { + opacity: 0.8; + } + 50% { + transform: translate(30px, -50px) scale(1.2); + opacity: 0.6; + } + 90% { + opacity: 0.8; + } + 100% { + transform: translate(60px, -100px) scale(1); + opacity: 0; + } +} + +/* Soft drifting light orbs */ +.rpg-clear-light-orb { + position: fixed; + border-radius: 50%; + background: radial-gradient(circle at center, + rgba(255, 250, 230, 0.12) 0%, + rgba(255, 245, 200, 0.06) 40%, + transparent 70%); + filter: blur(20px); + animation: rpg-clear-orb-drift ease-in-out infinite; + pointer-events: none; +} + +@keyframes rpg-clear-orb-drift { + 0%, 100% { + transform: translate(0, 0) scale(1); + opacity: 0.4; + } + 25% { + transform: translate(20px, 15px) scale(1.1); + opacity: 0.6; + } + 50% { + transform: translate(-10px, 30px) scale(0.95); + opacity: 0.5; + } + 75% { + transform: translate(-25px, 10px) scale(1.05); + opacity: 0.7; + } +} + +/* Lens flare effect */ +.rpg-clear-lens-flare { + position: fixed; + top: 5%; + left: 10%; + width: 150px; + height: 150px; + background: radial-gradient(circle at center, + rgba(255, 255, 255, 0.3) 0%, + rgba(255, 250, 220, 0.15) 20%, + rgba(255, 240, 180, 0.08) 40%, + transparent 60%); + border-radius: 50%; + animation: rpg-clear-lens-flare-pulse 6s ease-in-out infinite; + pointer-events: none; +} + +.rpg-clear-lens-flare::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 60px; + height: 60px; + transform: translate(-50%, -50%); + background: radial-gradient(circle at center, + rgba(255, 255, 255, 0.5) 0%, + rgba(255, 250, 230, 0.2) 50%, + transparent 70%); + border-radius: 50%; + animation: rpg-clear-lens-core-pulse 4s ease-in-out infinite; +} + +.rpg-clear-lens-flare::after { + content: ''; + position: absolute; + top: 120%; + left: 150%; + width: 40px; + height: 40px; + background: radial-gradient(circle at center, + rgba(255, 200, 150, 0.4) 0%, + rgba(255, 180, 100, 0.1) 50%, + transparent 70%); + border-radius: 50%; + animation: rpg-clear-secondary-flare 8s ease-in-out infinite; +} + +@keyframes rpg-clear-lens-flare-pulse { + 0%, 100% { + opacity: 0.7; + transform: scale(1); + } + 50% { + opacity: 1; + transform: scale(1.1); + } +} + +@keyframes rpg-clear-lens-core-pulse { + 0%, 100% { + opacity: 0.8; + transform: translate(-50%, -50%) scale(1); + } + 50% { + opacity: 1; + transform: translate(-50%, -50%) scale(1.15); + } +} + +@keyframes rpg-clear-secondary-flare { + 0%, 100% { + opacity: 0.3; + transform: scale(1); + } + 30% { + opacity: 0.6; + transform: scale(1.2); + } + 70% { + opacity: 0.4; + transform: scale(0.9); + } +} + +/* ===== Nighttime Clear Weather Effects ===== */ + +/* Container modifier for night weather */ +.rpg-night-weather { + overflow: hidden; +} + +/* Dark blue night overlay */ +.rpg-night-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(to bottom, + rgba(10, 15, 40, 0.3) 0%, + rgba(15, 25, 55, 0.2) 40%, + rgba(20, 30, 60, 0.15) 100%); + pointer-events: none; +} + +/* Moon */ +.rpg-night-moon { + position: fixed; + top: 8%; + left: 12%; + width: 60px; + height: 60px; + background: radial-gradient(circle at 35% 35%, + rgba(255, 255, 245, 1) 0%, + rgba(240, 240, 230, 0.95) 40%, + rgba(220, 220, 210, 0.9) 60%, + rgba(200, 200, 195, 0.8) 80%, + rgba(180, 180, 175, 0) 100%); + border-radius: 50%; + box-shadow: + 0 0 20px 5px rgba(255, 255, 240, 0.4), + 0 0 40px 10px rgba(200, 210, 255, 0.2), + inset -8px -5px 15px rgba(150, 150, 140, 0.3); + animation: rpg-night-moon-float 30s ease-in-out infinite; + pointer-events: none; +} + +@keyframes rpg-night-moon-float { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(10px); + } +} + +/* Moon glow aura */ +.rpg-night-moon-glow { + position: fixed; + top: 5%; + left: 9%; + width: 120px; + height: 120px; + background: radial-gradient(circle at center, + rgba(200, 220, 255, 0.15) 0%, + rgba(180, 200, 240, 0.08) 40%, + transparent 70%); + border-radius: 50%; + filter: blur(10px); + animation: rpg-night-moon-glow-pulse 8s ease-in-out infinite; + pointer-events: none; +} + +@keyframes rpg-night-moon-glow-pulse { + 0%, 100% { + opacity: 0.6; + transform: scale(1); + } + 50% { + opacity: 1; + transform: scale(1.1); + } +} + +/* Twinkling stars */ +.rpg-night-star { + position: fixed; + background: radial-gradient(circle at center, + rgba(255, 255, 255, 1) 0%, + rgba(200, 220, 255, 0.8) 40%, + transparent 70%); + border-radius: 50%; + animation: rpg-night-star-twinkle ease-in-out infinite; + pointer-events: none; +} + +@keyframes rpg-night-star-twinkle { + 0%, 100% { + opacity: 0.3; + transform: scale(1); + } + 50% { + opacity: 1; + transform: scale(1.3); + } +} + +/* Brighter stars with cross-flare effect */ +.rpg-night-star-bright { + width: 4px !important; + height: 4px !important; + background: rgba(255, 255, 255, 1); + box-shadow: + 0 0 4px 2px rgba(255, 255, 255, 0.8), + 0 0 8px 4px rgba(200, 220, 255, 0.4); +} + +.rpg-night-star-bright::before, +.rpg-night-star-bright::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + background: linear-gradient(90deg, + transparent 0%, + rgba(255, 255, 255, 0.8) 45%, + rgba(255, 255, 255, 1) 50%, + rgba(255, 255, 255, 0.8) 55%, + transparent 100%); +} + +.rpg-night-star-bright::before { + width: 16px; + height: 2px; + transform: translate(-50%, -50%); +} + +.rpg-night-star-bright::after { width: 2px; - animation-duration: 12s; + height: 16px; + transform: translate(-50%, -50%); +} + +/* Fireflies */ +.rpg-night-firefly { + position: fixed; + width: 4px; + height: 4px; + background: radial-gradient(circle at center, + rgba(200, 255, 150, 1) 0%, + rgba(180, 255, 120, 0.8) 30%, + rgba(150, 230, 100, 0.4) 60%, + transparent 100%); + border-radius: 50%; + box-shadow: + 0 0 6px 3px rgba(180, 255, 120, 0.6), + 0 0 12px 6px rgba(150, 230, 100, 0.3); + animation: rpg-night-firefly-float ease-in-out infinite; + pointer-events: none; +} + +@keyframes rpg-night-firefly-float { + 0% { + opacity: 0; + transform: translate(0, 0) scale(0.5); + } + 10% { + opacity: 1; + transform: translate(5px, -10px) scale(1); + } + 30% { + opacity: 0.3; + transform: translate(-15px, -25px) scale(0.8); + } + 50% { + opacity: 1; + transform: translate(10px, -40px) scale(1.1); + } + 70% { + opacity: 0.4; + transform: translate(-10px, -55px) scale(0.9); + } + 90% { + opacity: 0.8; + transform: translate(5px, -70px) scale(1); + } + 100% { + opacity: 0; + transform: translate(0, -80px) scale(0.5); + } +} + +/* Shooting star */ +.rpg-night-shooting-star { + position: fixed; + top: 15%; + left: -10%; + width: 100px; + height: 2px; + background: linear-gradient(90deg, + transparent 0%, + rgba(255, 255, 255, 0.2) 20%, + rgba(255, 255, 255, 0.8) 60%, + rgba(255, 255, 255, 1) 100%); + border-radius: 50%; + transform: rotate(35deg); + animation: rpg-night-shooting-star-fly 12s ease-in-out infinite; + pointer-events: none; + opacity: 0; +} + +.rpg-night-shooting-star::after { + content: ''; + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); + width: 6px; + height: 6px; + background: rgba(255, 255, 255, 1); + border-radius: 50%; + box-shadow: 0 0 10px 4px rgba(255, 255, 255, 0.8); +} + +@keyframes rpg-night-shooting-star-fly { + 0%, 85%, 100% { + opacity: 0; + left: -10%; + top: 15%; + } + 88% { + opacity: 1; + } + 95% { + opacity: 1; + left: 110%; + top: 45%; + } + 96% { + opacity: 0; + } } /* Mobile optimizations */ @@ -9689,9 +10074,39 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play { animation-duration: 20s; } - .rpg-sunray { + /* Clear weather mobile optimizations */ + .rpg-clear-dust-mote { animation-duration: 15s; } + + .rpg-clear-light-orb { + animation-duration: 25s; + filter: blur(15px); + } + + .rpg-clear-lens-flare { + width: 100px; + height: 100px; + } + + /* Nighttime mobile optimizations */ + .rpg-night-moon { + width: 45px; + height: 45px; + } + + .rpg-night-moon-glow { + width: 90px; + height: 90px; + } + + .rpg-night-firefly { + animation-duration: 12s; + } + + .rpg-night-shooting-star { + animation-duration: 18s; + } } /* Lightning flash effect */ From 6e9ff9812d2288b97419fa534538d5fdc7877282 Mon Sep 17 00:00:00 2001 From: tomt610 Date: Mon, 12 Jan 2026 22:54:46 +0000 Subject: [PATCH 2/3] Fix mobile view for weather effects - Replace % units with dvh/vw for dynamic viewport sizing - Fix stars, fireflies, dust motes, and light orbs positioning - Fix moon and moon glow positioning to use dvh/vw - Update snowfall and rainfall animations to use 100dvh - Ensures proper distribution across full mobile viewport --- src/systems/ui/weatherEffects.js | 20 ++++++++++---------- style.css | 14 +++++++------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/systems/ui/weatherEffects.js b/src/systems/ui/weatherEffects.js index 04228b1..0d24ca8 100644 --- a/src/systems/ui/weatherEffects.js +++ b/src/systems/ui/weatherEffects.js @@ -247,8 +247,8 @@ function createSunshine() { for (let i = 0; i < 25; i++) { const particle = document.createElement('div'); particle.className = 'rpg-weather-particle rpg-clear-dust-mote'; - particle.style.left = `${Math.random() * 100}%`; - particle.style.top = `${Math.random() * 100}%`; + particle.style.left = `${Math.random() * 100}vw`; + particle.style.top = `${Math.random() * 100}dvh`; particle.style.animationDelay = `${Math.random() * 15}s`; particle.style.animationDuration = `${12 + Math.random() * 8}s`; // Vary the size slightly @@ -262,8 +262,8 @@ function createSunshine() { for (let i = 0; i < 6; i++) { const orb = document.createElement('div'); orb.className = 'rpg-weather-particle rpg-clear-light-orb'; - orb.style.left = `${10 + Math.random() * 80}%`; - orb.style.top = `${10 + Math.random() * 80}%`; + orb.style.left = `${10 + Math.random() * 80}vw`; + orb.style.top = `${10 + Math.random() * 80}dvh`; orb.style.animationDelay = `${i * 2}s`; orb.style.animationDuration = `${20 + Math.random() * 10}s`; // Vary the size @@ -307,8 +307,8 @@ function createNighttime() { for (let i = 0; i < 60; i++) { const star = document.createElement('div'); star.className = 'rpg-weather-particle rpg-night-star'; - star.style.left = `${Math.random() * 100}%`; - star.style.top = `${Math.random() * 60}%`; // Stars mostly in upper portion + star.style.left = `${Math.random() * 100}vw`; + star.style.top = `${Math.random() * 60}dvh`; // Stars mostly in upper portion star.style.animationDelay = `${Math.random() * 5}s`; star.style.animationDuration = `${2 + Math.random() * 3}s`; // Vary the size @@ -322,8 +322,8 @@ function createNighttime() { for (let i = 0; i < 8; i++) { const brightStar = document.createElement('div'); brightStar.className = 'rpg-weather-particle rpg-night-star rpg-night-star-bright'; - brightStar.style.left = `${Math.random() * 100}%`; - brightStar.style.top = `${Math.random() * 50}%`; + brightStar.style.left = `${Math.random() * 100}vw`; + brightStar.style.top = `${Math.random() * 50}dvh`; brightStar.style.animationDelay = `${Math.random() * 4}s`; brightStar.style.animationDuration = `${3 + Math.random() * 2}s`; container.appendChild(brightStar); @@ -333,8 +333,8 @@ function createNighttime() { for (let i = 0; i < 15; i++) { const firefly = document.createElement('div'); firefly.className = 'rpg-weather-particle rpg-night-firefly'; - firefly.style.left = `${Math.random() * 100}%`; - firefly.style.top = `${40 + Math.random() * 55}%`; // Fireflies in lower portion + firefly.style.left = `${Math.random() * 100}vw`; + firefly.style.top = `${40 + Math.random() * 55}dvh`; // Fireflies in lower portion firefly.style.animationDelay = `${Math.random() * 10}s`; firefly.style.animationDuration = `${8 + Math.random() * 7}s`; container.appendChild(firefly); diff --git a/style.css b/style.css index 313c75e..4b6bc55 100644 --- a/style.css +++ b/style.css @@ -9524,11 +9524,11 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play { /* Snowfall animation */ @keyframes rpg-snowfall { 0% { - transform: translateY(0vh) rotate(0deg); + transform: translateY(0) rotate(0deg); opacity: 0.8; } 100% { - transform: translateY(100vh) rotate(360deg); + transform: translateY(100dvh) rotate(360deg); opacity: 0.2; } } @@ -9598,7 +9598,7 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play { opacity: 0.8; } 100% { - transform: translateY(100vh); + transform: translateY(100dvh); opacity: 0.3; } } @@ -9849,8 +9849,8 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play { /* Moon */ .rpg-night-moon { position: fixed; - top: 8%; - left: 12%; + top: 8dvh; + left: 12vw; width: 60px; height: 60px; background: radial-gradient(circle at 35% 35%, @@ -9880,8 +9880,8 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play { /* Moon glow aura */ .rpg-night-moon-glow { position: fixed; - top: 5%; - left: 9%; + top: 5dvh; + left: 9vw; width: 120px; height: 120px; background: radial-gradient(circle at center, From 0066b6174684f85d1248f9449f43181cdee41e28 Mon Sep 17 00:00:00 2001 From: tomt610 Date: Mon, 12 Jan 2026 23:21:19 +0000 Subject: [PATCH 3/3] Add sun/moon traveling across sky based on hour - Sun position calculated from hour (5 AM - 8 PM arc trajectory) - Moon position calculated from hour (8 PM - 5 AM arc trajectory) - Celestial bodies move smoothly without resetting particles - Reduced opacity for sun/moon in foreground mode for readability - Fixed mobile viewport units (dvh/vw) for proper positioning --- src/systems/ui/weatherEffects.js | 146 +++++++++++++++++++++++++++++-- style.css | 126 +++++++++++++++++++++++++- 2 files changed, 262 insertions(+), 10 deletions(-) diff --git a/src/systems/ui/weatherEffects.js b/src/systems/ui/weatherEffects.js index 0d24ca8..727f8cb 100644 --- a/src/systems/ui/weatherEffects.js +++ b/src/systems/ui/weatherEffects.js @@ -9,6 +9,7 @@ import { repairJSON } from '../../utils/jsonRepair.js'; let weatherContainer = null; let currentWeatherType = null; let currentTimeOfDay = null; +let currentHour = null; /** * Parse time string to extract hour (24-hour format) @@ -231,13 +232,58 @@ function createMist() { return container; } +/** + * Calculate sun position based on hour (arc across sky) + * Returns { left: vw%, top: dvh% } + */ +function calculateSunPosition(hour) { + // Daytime is roughly 6 AM to 8 PM (6-20) + // Map hour to position along an arc + // 6 AM = far left, low | 12 PM = center, high | 6 PM = far right, low + + if (hour === null) hour = 12; // Default to noon if unknown + + // Clamp to daytime hours + const clampedHour = Math.max(5, Math.min(20, hour)); + + // Normalize to 0-1 range (5 AM = 0, 20 PM = 1) + const progress = (clampedHour - 5) / 15; + + // Horizontal position: 5% to 85% (left to right) + const left = 5 + progress * 80; + + // Vertical position: parabolic arc (high at noon, low at dawn/dusk) + // At progress 0.5 (noon), top should be ~8% (high) + // At progress 0 or 1, top should be ~35% (low, near horizon) + const normalizedProgress = (progress - 0.5) * 2; // -1 to 1 + const top = 8 + 27 * (normalizedProgress * normalizedProgress); + + return { left, top }; +} + /** * Create clear/sunny weather effect with floating particles and warm glow */ -function createSunshine() { +function createSunshine(hour) { const container = document.createElement('div'); container.className = 'rpg-weather-particles rpg-clear-weather'; + // Create the sun based on current hour + const sunPos = calculateSunPosition(hour); + + const sun = document.createElement('div'); + sun.className = 'rpg-weather-particle rpg-clear-sun'; + sun.style.left = `${sunPos.left}vw`; + sun.style.top = `${sunPos.top}dvh`; + container.appendChild(sun); + + // Create sun glow + const sunGlow = document.createElement('div'); + sunGlow.className = 'rpg-weather-particle rpg-clear-sun-glow'; + sunGlow.style.left = `${sunPos.left}vw`; + sunGlow.style.top = `${sunPos.top}dvh`; + container.appendChild(sunGlow); + // Create warm ambient glow overlay const ambientGlow = document.createElement('div'); ambientGlow.className = 'rpg-weather-particle rpg-clear-ambient-glow'; @@ -284,7 +330,7 @@ function createSunshine() { /** * Create clear nighttime weather effect with moon, stars, and fireflies */ -function createNighttime() { +function createNighttime(hour) { const container = document.createElement('div'); container.className = 'rpg-weather-particles rpg-night-weather'; @@ -293,14 +339,21 @@ function createNighttime() { nightOverlay.className = 'rpg-weather-particle rpg-night-overlay'; container.appendChild(nightOverlay); + // Calculate moon position based on hour + const moonPos = calculateMoonPosition(hour); + // Create the moon const moon = document.createElement('div'); moon.className = 'rpg-weather-particle rpg-night-moon'; + moon.style.left = `${moonPos.left}vw`; + moon.style.top = `${moonPos.top}dvh`; container.appendChild(moon); // Create moon glow const moonGlow = document.createElement('div'); moonGlow.className = 'rpg-weather-particle rpg-night-moon-glow'; + moonGlow.style.left = `${moonPos.left - 3}vw`; + moonGlow.style.top = `${moonPos.top - 3}dvh`; container.appendChild(moonGlow); // Create twinkling stars @@ -383,6 +436,75 @@ function createWind() { return container; } +/** + * Calculate moon position based on hour (arc across sky at night) + * Returns { left: vw%, top: dvh% } + */ +function calculateMoonPosition(hour) { + // Nighttime is roughly 8 PM to 5 AM (20-5) + // Map hour to position along an arc + // 8 PM = far left, low | midnight = center-left, high | 5 AM = far right, low + + if (hour === null) hour = 0; // Default to midnight if unknown + + // Normalize night hours to 0-1 range + // 20 (8 PM) = 0, 0 (midnight) = ~0.44, 5 (5 AM) = 1 + let progress; + if (hour >= 20) { + // 8 PM to midnight: 20-24 maps to 0-0.44 + progress = (hour - 20) / 9; + } else { + // Midnight to 5 AM: 0-5 maps to 0.44-1 + progress = (hour + 4) / 9; + } + + // Horizontal position: 10% to 80% (left to right) + const left = 10 + progress * 70; + + // Vertical position: parabolic arc (high at ~2 AM, low at dusk/dawn) + // Peak should be around progress 0.67 (~2 AM) + const peakProgress = 0.5; + const normalizedProgress = (progress - peakProgress) * 2; // -1 to 1 + const top = 8 + 25 * (normalizedProgress * normalizedProgress); + + return { left, top }; +} + +/** + * Update sun/moon position without recreating the whole effect + */ +function updateCelestialPosition(hour) { + if (!weatherContainer) return false; + + // Update sun position if it exists + const sun = weatherContainer.querySelector('.rpg-clear-sun'); + const sunGlow = weatherContainer.querySelector('.rpg-clear-sun-glow'); + + if (sun && sunGlow) { + const sunPos = calculateSunPosition(hour); + sun.style.left = `${sunPos.left}vw`; + sun.style.top = `${sunPos.top}dvh`; + sunGlow.style.left = `${sunPos.left}vw`; + sunGlow.style.top = `${sunPos.top}dvh`; + return true; + } + + // Update moon position if it exists + const moon = weatherContainer.querySelector('.rpg-night-moon'); + const moonGlow = weatherContainer.querySelector('.rpg-night-moon-glow'); + + if (moon && moonGlow) { + const moonPos = calculateMoonPosition(hour); + moon.style.left = `${moonPos.left}vw`; + moon.style.top = `${moonPos.top}dvh`; + moonGlow.style.left = `${moonPos.left - 3}vw`; + moonGlow.style.top = `${moonPos.top - 3}dvh`; + return true; + } + + return false; +} + /** * Remove current weather effect */ @@ -392,6 +514,7 @@ function removeWeatherEffect() { weatherContainer = null; currentWeatherType = null; currentTimeOfDay = null; + currentHour = null; } } @@ -413,8 +536,16 @@ export function updateWeatherEffect() { const hour = parseHourFromTime(timeStr); const timeOfDay = getTimeOfDay(hour); - // Don't recreate if weather and time haven't changed - if (weatherType === currentWeatherType && timeOfDay === currentTimeOfDay) { + // If only the hour changed (same weather and time of day), just update celestial position + if (weatherType === currentWeatherType && timeOfDay === currentTimeOfDay && hour !== currentHour) { + if (updateCelestialPosition(hour)) { + currentHour = hour; + return; // Successfully updated position without recreating + } + } + + // Don't recreate if nothing has changed + if (weatherType === currentWeatherType && timeOfDay === currentTimeOfDay && hour === currentHour) { return; } @@ -428,6 +559,7 @@ export function updateWeatherEffect() { currentWeatherType = weatherType; currentTimeOfDay = timeOfDay; + currentHour = hour; switch (weatherType) { case 'snow': @@ -442,9 +574,9 @@ export function updateWeatherEffect() { case 'sunny': // Use nighttime effect for clear weather at night if (timeOfDay === 'night') { - weatherContainer = createNighttime(); + weatherContainer = createNighttime(hour); } else { - weatherContainer = createSunshine(); + weatherContainer = createSunshine(hour); } break; case 'wind': @@ -478,8 +610,10 @@ export function updateWeatherEffect() { // Apply z-index based on background/foreground settings if (extensionSettings.weatherForeground) { weatherContainer.style.zIndex = '9998'; // In front of chat + weatherContainer.classList.add('rpg-weather-foreground'); } else if (extensionSettings.weatherBackground) { weatherContainer.style.zIndex = '1'; // Behind chat (default) + weatherContainer.classList.remove('rpg-weather-foreground'); } else { // Both disabled - don't show weather return; diff --git a/style.css b/style.css index 4b6bc55..596438b 100644 --- a/style.css +++ b/style.css @@ -9740,6 +9740,72 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play { } } +/* Sun - positioned dynamically based on hour */ +.rpg-clear-sun { + position: fixed; + width: 50px; + height: 50px; + background: radial-gradient(circle at 40% 40%, + rgba(255, 255, 240, 1) 0%, + rgba(255, 250, 200, 1) 30%, + rgba(255, 220, 120, 0.9) 60%, + rgba(255, 180, 80, 0.6) 80%, + rgba(255, 150, 50, 0) 100%); + border-radius: 50%; + box-shadow: + 0 0 30px 10px rgba(255, 220, 100, 0.5), + 0 0 60px 20px rgba(255, 200, 80, 0.3), + 0 0 100px 40px rgba(255, 180, 60, 0.15); + transform: translate(-50%, -50%); + animation: rpg-clear-sun-pulse 8s ease-in-out infinite; + pointer-events: none; +} + +@keyframes rpg-clear-sun-pulse { + 0%, 100% { + transform: translate(-50%, -50%) scale(1); + box-shadow: + 0 0 30px 10px rgba(255, 220, 100, 0.5), + 0 0 60px 20px rgba(255, 200, 80, 0.3), + 0 0 100px 40px rgba(255, 180, 60, 0.15); + } + 50% { + transform: translate(-50%, -50%) scale(1.05); + box-shadow: + 0 0 35px 12px rgba(255, 220, 100, 0.6), + 0 0 70px 25px rgba(255, 200, 80, 0.35), + 0 0 110px 45px rgba(255, 180, 60, 0.2); + } +} + +/* Sun glow aura */ +.rpg-clear-sun-glow { + position: fixed; + width: 150px; + height: 150px; + background: radial-gradient(circle at center, + rgba(255, 240, 180, 0.25) 0%, + rgba(255, 220, 150, 0.15) 30%, + rgba(255, 200, 120, 0.08) 50%, + transparent 70%); + border-radius: 50%; + transform: translate(-50%, -50%); + filter: blur(8px); + animation: rpg-clear-sun-glow-pulse 6s ease-in-out infinite; + pointer-events: none; +} + +@keyframes rpg-clear-sun-glow-pulse { + 0%, 100% { + opacity: 0.7; + transform: translate(-50%, -50%) scale(1); + } + 50% { + opacity: 1; + transform: translate(-50%, -50%) scale(1.15); + } +} + /* Lens flare effect */ .rpg-clear-lens-flare { position: fixed; @@ -9849,8 +9915,6 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play { /* Moon */ .rpg-night-moon { position: fixed; - top: 8dvh; - left: 12vw; width: 60px; height: 60px; background: radial-gradient(circle at 35% 35%, @@ -9880,8 +9944,6 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play { /* Moon glow aura */ .rpg-night-moon-glow { position: fixed; - top: 5dvh; - left: 9vw; width: 120px; height: 120px; background: radial-gradient(circle at center, @@ -10089,6 +10151,17 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play { height: 100px; } + /* Sun mobile optimizations */ + .rpg-clear-sun { + width: 40px; + height: 40px; + } + + .rpg-clear-sun-glow { + width: 100px; + height: 100px; + } + /* Nighttime mobile optimizations */ .rpg-night-moon { width: 45px; @@ -10109,6 +10182,51 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play { } } +/* Foreground mode - reduced opacity for celestial bodies to not obstruct content */ +.rpg-weather-foreground .rpg-clear-sun { + opacity: 0.5; + box-shadow: + 0 0 20px 8px rgba(255, 220, 100, 0.3), + 0 0 40px 15px rgba(255, 200, 80, 0.15), + 0 0 60px 25px rgba(255, 180, 60, 0.08); +} + +.rpg-weather-foreground .rpg-clear-sun-glow { + opacity: 0.3; +} + +.rpg-weather-foreground .rpg-clear-lens-flare { + opacity: 0.3; +} + +.rpg-weather-foreground .rpg-clear-ambient-glow { + opacity: 0.4; +} + +.rpg-weather-foreground .rpg-night-moon { + opacity: 0.7; + box-shadow: + 0 0 15px 4px rgba(255, 255, 240, 0.25), + 0 0 30px 8px rgba(200, 210, 255, 0.12); +} + +.rpg-weather-foreground .rpg-night-moon-glow { + opacity: 0.3; +} + +.rpg-weather-foreground .rpg-night-overlay { + opacity: 0.4; +} + +.rpg-weather-foreground .rpg-night-star, +.rpg-weather-foreground .rpg-night-star-bright { + opacity: 0.5; +} + +.rpg-weather-foreground .rpg-night-firefly { + opacity: 0.6; +} + /* Lightning flash effect */ .rpg-lightning { position: fixed;