/** Night spans 22:00–06:00 (8 game-hours); the rest of the clock ("day", covering morning/midday/ * evening) is the other time-speed regime — see GameStore.updateClock. Lighting still varies * continuously across all of these (see overlayColorForHour) even though only two speeds exist. */ export const NIGHT_START_HOUR = 22; export const NIGHT_END_HOUR = 6; export const NIGHT_HOURS = 24 - NIGHT_START_HOUR + NIGHT_END_HOUR; export const DAY_HOURS = 24 - NIGHT_HOURS; /** Fixed (non-upgradable) night time-flow speed, in game-hours per real second — derived from the * default 10s night. */ export const NIGHT_SPEED = NIGHT_HOURS / 20; export function isNightHour(hour: number): boolean { return hour >= NIGHT_START_HOUR || hour < NIGHT_END_HOUR; } export const BLOOD_MOON_INTERVAL_DAYS = 5; /** Seconds the "КРОВАВАЯ ЛУНА!" banner stays on screen once a blood moon night begins. */ export const BLOOD_MOON_BANNER_SECONDS = 4; /** A single night spans a day-counter rollover (22:00 of day N through 06:00 of day N+1) — this * says which day that night belongs to throughout, for period-based night events. */ export function nightBelongsToDay(day: number, hour: number): number { return hour >= NIGHT_START_HOUR ? day : day - 1; } export function isBloodMoonNight(day: number, hour: number): boolean { if (!isNightHour(hour)) return false; const nightDay = nightBelongsToDay(day, hour); return nightDay > 0 && nightDay % BLOOD_MOON_INTERVAL_DAYS === 0; } /** Formats an in-game clock hour (0-24, fractional) as "HH:MM". */ export function formatClock(hour: number): string { const totalMinutes = Math.round(hour * 60); const wrapped = ((totalMinutes % (24 * 60)) + 24 * 60) % (24 * 60); const h = Math.floor(wrapped / 60); const m = wrapped % 60; return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; } interface OverlayKeyframe { hour: number; r: number; g: number; b: number; a: number; } // Night (dark) -> dawn (warm glow) -> day (clear) -> dusk (warm glow) -> night, keyed by hour. const OVERLAY_KEYFRAMES: OverlayKeyframe[] = [ { hour: 0, r: 10, g: 10, b: 35, a: 0.55 }, { hour: 5, r: 10, g: 10, b: 35, a: 0.55 }, { hour: 6, r: 255, g: 170, b: 110, a: 0.3 }, { hour: 9, r: 0, g: 0, b: 0, a: 0 }, { hour: 18, r: 0, g: 0, b: 0, a: 0 }, { hour: 19, r: 255, g: 140, b: 70, a: 0.3 }, { hour: 22, r: 10, g: 10, b: 35, a: 0.55 }, { hour: 24, r: 10, g: 10, b: 35, a: 0.55 }, ]; /** The ambient darkness/tint overlay for the given time of day, as a CSS rgba() string — * interpolated piecewise-linearly between OVERLAY_KEYFRAMES. */ export function overlayColorForHour(hour: number): string { const h = ((hour % 24) + 24) % 24; let prev = OVERLAY_KEYFRAMES[0]; let next = OVERLAY_KEYFRAMES[OVERLAY_KEYFRAMES.length - 1]; for (let i = 0; i < OVERLAY_KEYFRAMES.length - 1; i++) { if (h >= OVERLAY_KEYFRAMES[i].hour && h <= OVERLAY_KEYFRAMES[i + 1].hour) { prev = OVERLAY_KEYFRAMES[i]; next = OVERLAY_KEYFRAMES[i + 1]; break; } } const span = next.hour - prev.hour; const t = span === 0 ? 0 : (h - prev.hour) / span; const lerp = (a: number, b: number) => a + (b - a) * t; const r = lerp(prev.r, next.r); const g = lerp(prev.g, next.g); const b = lerp(prev.b, next.b); const a = lerp(prev.a, next.a); return `rgba(${r.toFixed(0)}, ${g.toFixed(0)}, ${b.toFixed(0)}, ${a.toFixed(3)})`; }