ENH - refine native weather presentation
@@ -4,3 +4,6 @@ dist/
|
||||
*.local
|
||||
dev-server*.log
|
||||
|
||||
# Image-generation source files retained locally for asset iteration.
|
||||
/src/assets/img/weather-icons/source/
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import clearIcon from '@/assets/img/weather-icons/clear.webp'
|
||||
import cloudyIcon from '@/assets/img/weather-icons/cloudy.webp'
|
||||
import fogIcon from '@/assets/img/weather-icons/fog.webp'
|
||||
import partlyCloudyIcon from '@/assets/img/weather-icons/partly_cloudy.webp'
|
||||
import rainIcon from '@/assets/img/weather-icons/rain.webp'
|
||||
import snowIcon from '@/assets/img/weather-icons/snow.webp'
|
||||
import sunnyIcon from '@/assets/img/weather-icons/sunny.webp'
|
||||
import thunderIcon from '@/assets/img/weather-icons/thunder.webp'
|
||||
import type { WeatherConditionId } from '@/types/weather'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
condition: WeatherConditionId
|
||||
size?: number
|
||||
timestamp?: number
|
||||
}>(),
|
||||
{ size: 24, timestamp: undefined },
|
||||
)
|
||||
|
||||
const icons: Record<WeatherConditionId, string> = {
|
||||
sunny: sunnyIcon,
|
||||
clear: clearIcon,
|
||||
partly_cloudy: partlyCloudyIcon,
|
||||
cloudy: cloudyIcon,
|
||||
rain: rainIcon,
|
||||
thunder: thunderIcon,
|
||||
fog: fogIcon,
|
||||
snow: snowIcon,
|
||||
}
|
||||
|
||||
const iconSource = computed(() => {
|
||||
if (props.condition !== 'clear' || props.timestamp === undefined) {
|
||||
return icons[props.condition]
|
||||
}
|
||||
|
||||
const hour = new Date(props.timestamp).getUTCHours()
|
||||
return hour >= 7 && hour < 20 ? sunnyIcon : clearIcon
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img
|
||||
class="weather-condition-icon"
|
||||
:src="iconSource"
|
||||
:width="size"
|
||||
:height="size"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
draggable="false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.weather-condition-icon {
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type {
|
||||
DailyWeather,
|
||||
HourlyWeather,
|
||||
RawWeatherSnapshot,
|
||||
WeatherConditionId,
|
||||
@@ -57,37 +56,10 @@ const HUMIDITY: Record<WeatherConditionId, number> = {
|
||||
snow: 76,
|
||||
}
|
||||
|
||||
const TRANSITIONS: Record<WeatherConditionId, WeatherConditionId[]> = {
|
||||
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,
|
||||
|
||||
@@ -1,27 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { kCard, kLink, kNavbar, kPage, kPreloader } from 'konsta/vue'
|
||||
import {
|
||||
Cloud,
|
||||
CloudFog,
|
||||
CloudLightning,
|
||||
CloudRain,
|
||||
CloudSun,
|
||||
Droplets,
|
||||
Gauge,
|
||||
MoonStar,
|
||||
Navigation,
|
||||
RefreshCw,
|
||||
Snowflake,
|
||||
Sun,
|
||||
ThermometerSun,
|
||||
Umbrella,
|
||||
Wind,
|
||||
} from 'lucide-vue-next'
|
||||
import { computed, type Component } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import WeatherConditionIcon from '@/components/WeatherConditionIcon.vue'
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
import { useWeatherStore } from '@/stores/weather'
|
||||
import type { DailyWeather, WeatherConditionId } from '@/types/weather'
|
||||
import type { WeatherConditionId } from '@/types/weather'
|
||||
|
||||
const phone = usePhoneStore()
|
||||
const weather = useWeatherStore()
|
||||
@@ -31,23 +25,6 @@ const cardColors = {
|
||||
textIos: 'text-white',
|
||||
}
|
||||
|
||||
const conditionIcons: Record<WeatherConditionId, Component> = {
|
||||
sunny: Sun,
|
||||
clear: MoonStar,
|
||||
partly_cloudy: CloudSun,
|
||||
cloudy: Cloud,
|
||||
rain: CloudRain,
|
||||
thunder: CloudLightning,
|
||||
fog: CloudFog,
|
||||
snow: Snowflake,
|
||||
}
|
||||
|
||||
function conditionIcon(condition: WeatherConditionId, timestamp: number): Component {
|
||||
if (condition !== 'clear') return conditionIcons[condition]
|
||||
const hour = new Date(timestamp).getUTCHours()
|
||||
return hour >= 7 && hour < 20 ? Sun : MoonStar
|
||||
}
|
||||
|
||||
function conditionLabel(condition: WeatherConditionId): string {
|
||||
return phone.t(`Apps.weather.conditions.${condition}`)
|
||||
}
|
||||
@@ -59,26 +36,6 @@ function formatHour(timestamp: number, index: number): string {
|
||||
timeZone: 'UTC',
|
||||
}).format(timestamp)
|
||||
}
|
||||
|
||||
function formatDay(day: DailyWeather, index: number): string {
|
||||
if (index === 0) return phone.t('Apps.weather.today')
|
||||
return new Intl.DateTimeFormat(phone.lang, {
|
||||
timeZone: 'UTC',
|
||||
weekday: 'short',
|
||||
}).format(day.timestamp)
|
||||
}
|
||||
|
||||
function temperatureRange(day: DailyWeather): Record<string, string> {
|
||||
const lows = forecast.value?.daily.map((item) => item.low) ?? [day.low]
|
||||
const highs = forecast.value?.daily.map((item) => item.high) ?? [day.high]
|
||||
const minimum = Math.min(...lows)
|
||||
const maximum = Math.max(...highs)
|
||||
const span = Math.max(1, maximum - minimum)
|
||||
return {
|
||||
'--range-left': `${((day.low - minimum) / span) * 58}%`,
|
||||
'--range-width': `${Math.max(18, ((day.high - day.low) / span) * 58)}%`,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -97,7 +54,10 @@ function temperatureRange(day: DailyWeather): Record<string, string> {
|
||||
:disabled="weather.isLoading"
|
||||
@click="weather.refresh(true)"
|
||||
>
|
||||
<RefreshCw :size="17" :class="{ 'weather-spin': weather.isLoading }" />
|
||||
<RefreshCw
|
||||
:size="17"
|
||||
:class="{ 'weather-spin': weather.isLoading }"
|
||||
/>
|
||||
</k-link>
|
||||
</template>
|
||||
</k-navbar>
|
||||
@@ -108,12 +68,11 @@ function temperatureRange(day: DailyWeather): Record<string, string> {
|
||||
<Navigation :size="13" fill="currentColor" />
|
||||
{{ phone.t(`Apps.weather.regions.${forecast.region}`) }}
|
||||
</div>
|
||||
<component
|
||||
:is="conditionIcon(forecast.condition, forecast.timestamp)"
|
||||
<WeatherConditionIcon
|
||||
:condition="forecast.condition"
|
||||
:timestamp="forecast.timestamp"
|
||||
class="weather-hero__icon"
|
||||
:size="82"
|
||||
:stroke-width="1.25"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div class="weather-temperature">{{ forecast.temperature }}°</div>
|
||||
<strong>{{ conditionLabel(forecast.condition) }}</strong>
|
||||
@@ -124,23 +83,42 @@ function temperatureRange(day: DailyWeather): Record<string, string> {
|
||||
{{ phone.t('Apps.weather.stale') }}
|
||||
</p>
|
||||
|
||||
<section class="weather-details" :aria-label="phone.t('Apps.weather.details')">
|
||||
<k-card :colors="cardColors" :content-wrap="false" class="weather-detail-card">
|
||||
<section
|
||||
class="weather-details"
|
||||
:aria-label="phone.t('Apps.weather.details')"
|
||||
>
|
||||
<k-card
|
||||
:colors="cardColors"
|
||||
:content-wrap="false"
|
||||
class="weather-detail-card"
|
||||
>
|
||||
<ThermometerSun :size="18" />
|
||||
<span>{{ phone.t('Apps.weather.feelsLike') }}</span>
|
||||
<strong>{{ forecast.feelsLike }}°</strong>
|
||||
</k-card>
|
||||
<k-card :colors="cardColors" :content-wrap="false" class="weather-detail-card">
|
||||
<k-card
|
||||
:colors="cardColors"
|
||||
:content-wrap="false"
|
||||
class="weather-detail-card"
|
||||
>
|
||||
<Wind :size="18" />
|
||||
<span>{{ phone.t('Apps.weather.wind') }}</span>
|
||||
<strong>{{ forecast.windSpeed }} km/h</strong>
|
||||
</k-card>
|
||||
<k-card :colors="cardColors" :content-wrap="false" class="weather-detail-card">
|
||||
<k-card
|
||||
:colors="cardColors"
|
||||
:content-wrap="false"
|
||||
class="weather-detail-card"
|
||||
>
|
||||
<Droplets :size="18" />
|
||||
<span>{{ phone.t('Apps.weather.humidity') }}</span>
|
||||
<strong>{{ forecast.humidity }}%</strong>
|
||||
</k-card>
|
||||
<k-card :colors="cardColors" :content-wrap="false" class="weather-detail-card">
|
||||
<k-card
|
||||
:colors="cardColors"
|
||||
:content-wrap="false"
|
||||
class="weather-detail-card"
|
||||
>
|
||||
<Umbrella :size="18" />
|
||||
<span>{{ phone.t('Apps.weather.rain') }}</span>
|
||||
<strong>{{ forecast.rainChance }}%</strong>
|
||||
@@ -160,53 +138,30 @@ function temperatureRange(day: DailyWeather): Record<string, string> {
|
||||
class="weather-hour"
|
||||
>
|
||||
<span>{{ formatHour(hour.timestamp, index) }}</span>
|
||||
<component
|
||||
:is="conditionIcon(hour.condition, hour.timestamp)"
|
||||
:size="24"
|
||||
:stroke-width="1.7"
|
||||
aria-hidden="true"
|
||||
<WeatherConditionIcon
|
||||
:condition="hour.condition"
|
||||
:timestamp="hour.timestamp"
|
||||
/>
|
||||
<small v-if="hour.rainChance >= 30">{{ hour.rainChance }}%</small>
|
||||
<strong>{{ hour.temperature }}°</strong>
|
||||
</div>
|
||||
</div>
|
||||
</k-card>
|
||||
|
||||
<k-card
|
||||
:colors="cardColors"
|
||||
:content-wrap="false"
|
||||
class="weather-panel weather-daily-panel"
|
||||
>
|
||||
<h2><CloudSun :size="15" />{{ phone.t('Apps.weather.daily') }}</h2>
|
||||
<div
|
||||
v-for="(day, index) in forecast.daily"
|
||||
:key="day.timestamp"
|
||||
class="weather-day"
|
||||
>
|
||||
<strong>{{ formatDay(day, index) }}</strong>
|
||||
<span class="weather-day__condition">
|
||||
<component
|
||||
:is="conditionIcon(day.condition, day.timestamp)"
|
||||
:size="24"
|
||||
:stroke-width="1.7"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<small v-if="day.rainChance >= 30">{{ day.rainChance }}%</small>
|
||||
</span>
|
||||
<span class="weather-day__low">{{ day.low }}°</span>
|
||||
<span class="weather-day__range" :style="temperatureRange(day)">
|
||||
<i></i>
|
||||
</span>
|
||||
<span>{{ day.high }}°</span>
|
||||
</div>
|
||||
</k-card>
|
||||
</div>
|
||||
|
||||
<div v-else class="weather-empty">
|
||||
<k-preloader v-if="weather.isLoading" />
|
||||
<CloudSun v-else :size="52" :stroke-width="1.4" />
|
||||
<strong>{{ phone.t(weather.isLoading ? 'Common.loading' : 'Apps.weather.unavailable') }}</strong>
|
||||
<k-link v-if="!weather.isLoading" component="button" @click="weather.refresh(true)">
|
||||
<strong>{{
|
||||
phone.t(
|
||||
weather.isLoading ? 'Common.loading' : 'Apps.weather.unavailable',
|
||||
)
|
||||
}}</strong>
|
||||
<k-link
|
||||
v-if="!weather.isLoading"
|
||||
component="button"
|
||||
@click="weather.refresh(true)"
|
||||
>
|
||||
{{ phone.t('Apps.weather.tryAgain') }}
|
||||
</k-link>
|
||||
</div>
|
||||
|
||||
@@ -933,6 +933,7 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
data: {
|
||||
clock: { year: 2026, month: 8, day: 5, hour: 17, minute: 20 },
|
||||
condition: 'partly_cloudy',
|
||||
nextCondition: 'rain',
|
||||
rainLevel: 0.08,
|
||||
region: 'los_santos',
|
||||
windSpeed: 3.2,
|
||||
|
||||
@@ -257,10 +257,12 @@ end
|
||||
RegisterNUICallback("weather:get", function(_, cb)
|
||||
local coords = GetEntityCoords(PlayerPedId())
|
||||
local weather_hash = GetPrevWeatherTypeHashName()
|
||||
local next_weather_hash = GetNextWeatherTypeHashName()
|
||||
cb({
|
||||
success = true,
|
||||
data = {
|
||||
condition = weather_types[weather_hash] or "clear",
|
||||
nextCondition = weather_types[next_weather_hash] or weather_types[weather_hash] or "clear",
|
||||
region = weather_region(coords),
|
||||
clock = {
|
||||
year = GetClockYear(),
|
||||
|
||||