87 lines
3.4 KiB
TypeScript
87 lines
3.4 KiB
TypeScript
/** 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)})`;
|
||
}
|