import { SCHEDULE as S } from './rules' const HOUR = 3_600_000 /** "HH:MM" → 當天的分鐘數;格式錯誤回 null */ export function minutesOf(hhmm: string): number | null { const m = /^(\d{1,2}):(\d{2})$/.exec(hhmm) if (!m) return null const h = Number(m[1]) const mm = Number(m[2]) return h < 24 && mm < 60 ? h * 60 + mm : null } const localMinutes = (now: number, tzOffsetMin: number) => (((Math.floor(now / 60_000) + tzOffsetMin) % 1440) + 1440) % 1440 /** 離上次起床過了幾小時;起床時間格式錯誤時回 0 */ export function awakeHours(now: number, tzOffsetMin: number, wake: string): number { const w = minutesOf(wake) if (w === null) return 0 return (((localMinutes(now, tzOffsetMin) - w) % 1440) + 1440) % 1440 / 60 } /** 現在是否落在使用者的睡眠時段(就寢 → 起床,可跨午夜) */ export function inSleepWindow(now: number, tzOffsetMin: number, wake: string, sleep: string): boolean { const w = minutesOf(wake) const s = minutesOf(sleep) if (w === null || s === null || w === s) return false const t = localMinutes(now, tzOffsetMin) return s < w ? t >= s && t < w : t >= s || t < w } export type Rhythm = { wake: string; sleep: string; tzOffsetMin: number } /** * 精力基準的作息位移:醒太久、連續工作太久、在使用者睡眠時段仍在線都會更累;剛起床稍有回復。 * workMs 為連續工作時間(0 表示沒在工作)。 */ export function energyShift(now: number, workMs: number, r: Rhythm): number { const hours = awakeHours(now, r.tzOffsetMin, r.wake) const sleeping = inSleepWindow(now, r.tzOffsetMin, r.wake, r.sleep) let shift = 0 if (sleeping) shift -= S.lateShift else shift -= Math.min(S.awakeMax, Math.max(0, hours - S.awakeFreeH) * S.perHour) shift -= Math.min(S.workMax, Math.max(0, workMs / HOUR - S.workFreeH) * S.workPerHour) if (!sleeping && hours < S.wakeBoostH) shift += S.wakeBoost return shift } export type Period = '清晨' | '上午' | '下午' | '傍晚' | '夜晚' | '深夜' /** 時間感:本地時段名稱 */ export function periodOf(now: number, tzOffsetMin: number): Period { const h = Math.floor(localMinutes(now, tzOffsetMin) / 60) if (h < 5) return '深夜' if (h < 9) return '清晨' if (h < 12) return '上午' if (h < 17) return '下午' if (h < 19) return '傍晚' return h < 23 ? '夜晚' : '深夜' }