diff --git a/frontend/.gitignore b/frontend/.gitignore index b482045..5fd4f20 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -4,3 +4,6 @@ dist/ *.local dev-server*.log +# Image-generation source files retained locally for asset iteration. +/src/assets/img/weather-icons/source/ + diff --git a/frontend/src/assets/img/weather-icons/clear.webp b/frontend/src/assets/img/weather-icons/clear.webp new file mode 100644 index 0000000..ea4240c Binary files /dev/null and b/frontend/src/assets/img/weather-icons/clear.webp differ diff --git a/frontend/src/assets/img/weather-icons/cloudy.webp b/frontend/src/assets/img/weather-icons/cloudy.webp new file mode 100644 index 0000000..3f2bc55 Binary files /dev/null and b/frontend/src/assets/img/weather-icons/cloudy.webp differ diff --git a/frontend/src/assets/img/weather-icons/fog.webp b/frontend/src/assets/img/weather-icons/fog.webp new file mode 100644 index 0000000..c8b0c59 Binary files /dev/null and b/frontend/src/assets/img/weather-icons/fog.webp differ diff --git a/frontend/src/assets/img/weather-icons/partly_cloudy.webp b/frontend/src/assets/img/weather-icons/partly_cloudy.webp new file mode 100644 index 0000000..4015ca9 Binary files /dev/null and b/frontend/src/assets/img/weather-icons/partly_cloudy.webp differ diff --git a/frontend/src/assets/img/weather-icons/rain.webp b/frontend/src/assets/img/weather-icons/rain.webp new file mode 100644 index 0000000..93b3116 Binary files /dev/null and b/frontend/src/assets/img/weather-icons/rain.webp differ diff --git a/frontend/src/assets/img/weather-icons/snow.webp b/frontend/src/assets/img/weather-icons/snow.webp new file mode 100644 index 0000000..2579ced Binary files /dev/null and b/frontend/src/assets/img/weather-icons/snow.webp differ diff --git a/frontend/src/assets/img/weather-icons/sunny.webp b/frontend/src/assets/img/weather-icons/sunny.webp new file mode 100644 index 0000000..d5a8ab3 Binary files /dev/null and b/frontend/src/assets/img/weather-icons/sunny.webp differ diff --git a/frontend/src/assets/img/weather-icons/thunder.webp b/frontend/src/assets/img/weather-icons/thunder.webp new file mode 100644 index 0000000..a61da1e Binary files /dev/null and b/frontend/src/assets/img/weather-icons/thunder.webp differ diff --git a/frontend/src/components/WeatherConditionIcon.vue b/frontend/src/components/WeatherConditionIcon.vue new file mode 100644 index 0000000..dd3f182 --- /dev/null +++ b/frontend/src/components/WeatherConditionIcon.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/frontend/src/stores/weather.test.ts b/frontend/src/stores/weather.test.ts index e18e846..a58d2f8 100644 --- a/frontend/src/stores/weather.test.ts +++ b/frontend/src/stores/weather.test.ts @@ -10,6 +10,7 @@ vi.mock('@/utils/nui', () => ({ nuiCall: vi.fn() })) const rawWeather: RawWeatherSnapshot = { clock: { day: 5, hour: 17, minute: 20, month: 8, year: 2026 }, condition: 'sunny', + nextCondition: 'clear', rainLevel: 0, region: 'los_santos', windSpeed: 2, @@ -32,7 +33,7 @@ describe('weather store', () => { expect(nuiCall).toHaveBeenCalledWith('weather:get') expect(weather.forecast?.condition).toBe('sunny') - expect(weather.forecast?.hourly).toHaveLength(24) + expect(weather.forecast?.hourly).toHaveLength(6) expect(weather.error).toBeNull() }) diff --git a/frontend/src/types/weather.ts b/frontend/src/types/weather.ts index a007506..14918a5 100644 --- a/frontend/src/types/weather.ts +++ b/frontend/src/types/weather.ts @@ -8,10 +8,7 @@ export type WeatherConditionId = | 'fog' | 'snow' -export type WeatherRegionId = - | 'los_santos' - | 'blaine_county' - | 'cayo_perico' +export type WeatherRegionId = 'los_santos' | 'blaine_county' | 'cayo_perico' export type WeatherClock = { day: number @@ -24,6 +21,7 @@ export type WeatherClock = { export type RawWeatherSnapshot = { clock: WeatherClock condition: WeatherConditionId + nextCondition: WeatherConditionId rainLevel: number region: WeatherRegionId windSpeed: number @@ -36,17 +34,8 @@ export type HourlyWeather = { timestamp: number } -export type DailyWeather = { - condition: WeatherConditionId - high: number - low: number - rainChance: number - timestamp: number -} - export type WeatherForecast = { condition: WeatherConditionId - daily: DailyWeather[] feelsLike: number hourly: HourlyWeather[] humidity: number diff --git a/frontend/src/utils/weather.test.ts b/frontend/src/utils/weather.test.ts index 412ee6f..91f6132 100644 --- a/frontend/src/utils/weather.test.ts +++ b/frontend/src/utils/weather.test.ts @@ -3,12 +3,11 @@ import { describe, expect, it } from 'vitest' import type { RawWeatherSnapshot, WeatherRegionId } from '@/types/weather' import { buildWeatherForecast } from './weather' -function snapshot( - region: WeatherRegionId = 'los_santos', -): RawWeatherSnapshot { +function snapshot(region: WeatherRegionId = 'los_santos'): RawWeatherSnapshot { return { clock: { day: 31, hour: 23, minute: 30, month: 12, year: 2026 }, condition: 'partly_cloudy', + nextCondition: 'rain', rainLevel: 0.1, region, windSpeed: 4, @@ -16,13 +15,15 @@ function snapshot( } describe('weather forecast', () => { - it('builds deterministic 24-hour and seven-day forecasts', () => { + it('builds a deterministic six-hour forecast from native weather states', () => { const first = buildWeatherForecast(snapshot()) const second = buildWeatherForecast(snapshot()) expect(first).toEqual(second) - expect(first.hourly).toHaveLength(24) - expect(first.daily).toHaveLength(7) + expect(first.hourly).toHaveLength(6) + expect(first.hourly[0]?.condition).toBe('partly_cloudy') + expect(first.hourly[1]?.condition).toBe('rain') + expect(first).not.toHaveProperty('daily') }) it('rolls hourly timestamps into the next year', () => { diff --git a/frontend/src/utils/weather.ts b/frontend/src/utils/weather.ts index 49283a6..c049b2d 100644 --- a/frontend/src/utils/weather.ts +++ b/frontend/src/utils/weather.ts @@ -1,5 +1,4 @@ import type { - DailyWeather, HourlyWeather, RawWeatherSnapshot, WeatherConditionId, @@ -57,37 +56,10 @@ const HUMIDITY: Record = { snow: 76, } -const TRANSITIONS: Record = { - sunny: ['sunny', 'sunny', 'clear', 'partly_cloudy'], - clear: ['clear', 'sunny', 'partly_cloudy', 'cloudy'], - partly_cloudy: ['partly_cloudy', 'clear', 'cloudy', 'rain'], - cloudy: ['cloudy', 'partly_cloudy', 'rain', 'fog'], - rain: ['rain', 'cloudy', 'partly_cloudy', 'thunder'], - thunder: ['rain', 'cloudy', 'thunder', 'partly_cloudy'], - fog: ['fog', 'cloudy', 'partly_cloudy', 'clear'], - snow: ['snow', 'cloudy', 'snow', 'partly_cloudy'], -} - function clamp(value: number, minimum: number, maximum: number): number { return Math.min(maximum, Math.max(minimum, value)) } -function seedFrom(value: string): number { - let hash = 2166136261 - for (let index = 0; index < value.length; index += 1) { - hash ^= value.charCodeAt(index) - hash = Math.imul(hash, 16777619) - } - return hash >>> 0 -} - -function random(seed: number): number { - let value = seed + 0x6d2b79f5 - value = Math.imul(value ^ (value >>> 15), value | 1) - value ^= value + Math.imul(value ^ (value >>> 7), value | 61) - return ((value ^ (value >>> 14)) >>> 0) / 4294967296 -} - function normalizedCondition(value: string): WeatherConditionId { return CONDITIONS.includes(value as WeatherConditionId) ? (value as WeatherConditionId) @@ -109,14 +81,6 @@ function temperatureAt( ) } -function nextCondition( - condition: WeatherConditionId, - seed: number, -): WeatherConditionId { - const choices = TRANSITIONS[condition] - return choices[Math.floor(random(seed) * choices.length)] ?? condition -} - function snapshotTimestamp(snapshot: RawWeatherSnapshot): number { const { year, month, day, hour, minute } = snapshot.clock return Date.UTC(year, month - 1, day, hour, minute) @@ -126,62 +90,29 @@ export function buildWeatherForecast( input: RawWeatherSnapshot, ): WeatherForecast { const condition = normalizedCondition(input.condition) - const region = input.region in REGION_BASE_TEMPERATURE - ? input.region - : 'los_santos' + const nextCondition = normalizedCondition(input.nextCondition) + const region = + input.region in REGION_BASE_TEMPERATURE ? input.region : 'los_santos' const timestamp = snapshotTimestamp(input) - const hourSeed = seedFrom(`${region}:${timestamp}:${condition}`) const hourly: HourlyWeather[] = [] - let forecastCondition = condition - for (let index = 0; index < 24; index += 1) { - if (index > 0 && index % 3 === 0) { - forecastCondition = nextCondition(forecastCondition, hourSeed + index) - } + for (let index = 0; index < 6; index += 1) { + const forecastCondition = index === 0 ? condition : nextCondition const itemTimestamp = timestamp + index * 3_600_000 const itemHour = new Date(itemTimestamp).getUTCHours() hourly.push({ condition: forecastCondition, - rainChance: clamp( - RAIN_CHANCE[forecastCondition] + Math.round(random(hourSeed + index) * 12 - 6), - 0, - 100, - ), - temperature: temperatureAt( - region, - forecastCondition, - itemHour, - random(hourSeed + index + 100) * 3 - 1.5, - ), + rainChance: RAIN_CHANCE[forecastCondition], + temperature: temperatureAt(region, forecastCondition, itemHour, 0), timestamp: itemTimestamp, }) } - const daily: DailyWeather[] = [] - let dailyCondition = condition - for (let index = 0; index < 7; index += 1) { - if (index > 0) dailyCondition = nextCondition(dailyCondition, hourSeed + index * 97) - const itemTimestamp = timestamp + index * 86_400_000 - const variation = random(hourSeed + index * 31) * 4 - 2 - daily.push({ - condition: dailyCondition, - high: temperatureAt(region, dailyCondition, 14, variation), - low: temperatureAt(region, dailyCondition, 4, variation - 1), - rainChance: clamp( - RAIN_CHANCE[dailyCondition] + Math.round(random(hourSeed + index * 43) * 10 - 5), - 0, - 100, - ), - timestamp: itemTimestamp, - }) - } - - const currentVariation = random(hourSeed) * 2 - 1 const temperature = temperatureAt( region, condition, input.clock.hour + input.clock.minute / 60, - currentVariation, + 0, ) const windSpeed = Math.round(Math.max(0, input.windSpeed) * 3.6) const nativeRainChance = clamp(Math.round(input.rainLevel * 100), 0, 100) @@ -189,10 +120,14 @@ export function buildWeatherForecast( return { condition, - daily, - feelsLike: temperature - Math.round(windSpeed / 18) - (rainChance > 60 ? 1 : 0), + feelsLike: + temperature - Math.round(windSpeed / 18) - (rainChance > 60 ? 1 : 0), hourly, - humidity: clamp(HUMIDITY[condition] + Math.round(input.rainLevel * 8), 0, 100), + humidity: clamp( + HUMIDITY[condition] + Math.round(input.rainLevel * 8), + 0, + 100, + ), rainChance, region, temperature, diff --git a/frontend/src/views/apps/WeatherApp.vue b/frontend/src/views/apps/WeatherApp.vue index 1b5b731..964353c 100644 --- a/frontend/src/views/apps/WeatherApp.vue +++ b/frontend/src/views/apps/WeatherApp.vue @@ -1,27 +1,21 @@ @@ -108,12 +68,11 @@ function temperatureRange(day: DailyWeather): Record { {{ phone.t(`Apps.weather.regions.${forecast.region}`) }} -