refactor: 收口 BangDream 档线策略
This commit is contained in:
parent
25e39ae7ca
commit
47a48fb311
@ -364,6 +364,14 @@ export interface TsuguHook {
|
||||
- `models/cutoff.ts` 的请求、预测、展示规则分层。
|
||||
- policy 均有纯函数测试。
|
||||
|
||||
当前进度:
|
||||
|
||||
- 已新增 `models/server-policy.ts`,收口服务器 UTC 偏移、服务器时区 Date 转换、时间戳规范化和档线日增 checkpoint 服务器规则;`render-blocks/list-time.ts` 保留兼容入口但不再维护时区 switch。
|
||||
- 已新增 `models/cn-event-estimate-policy.ts`,把国服预估起始活动、跳过活动和无邦日规则从 `list-time.ts` 移出,提供 `calculateCnEventEstimateStartAt` 纯函数测试入口;`list-time.ts` 和 `event.ts` 改走 policy,不再让 Event 排序依赖渲染层预估实现。
|
||||
- 已新增 `models/cutoff-policy.ts`,收口档位列表、档位支持判断、国服缺失活动时间预估、档线活动状态、预测窗口、日增天数和最近同类型活动选择规则;`models/cutoff.ts` 和 `cutoff-all.ts` 已接入。
|
||||
- 已新增 `test/qqbot/plugins/bangDream/tsugu/policy.spec.ts`,覆盖服务器时区、国服预估纯计算、档位判断、状态/预测窗口、日增 checkpoint、活动天数和最近活动选择;policy 单测 mock `main-data-store`,避免纯测试启动主数据定时器。
|
||||
- 已生成 `phase6-policy-event-50.jpg`、`phase6-policy-cutoff-detail-100-50-cn.jpg`、`phase6-policy-cutoff-recent-100-50-cn.jpg`,验证查活动、单档线和历史档线图片输出非空。
|
||||
|
||||
### Phase 7:Facade 和 hook 接入
|
||||
|
||||
目标:Nest 边界稳定,Tsugu 内部可观测。
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { Event } from '@/qqbot/plugins/bangDream/tsugu/models/event';
|
||||
import { drawList, line } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-frame';
|
||||
import {
|
||||
drawList,
|
||||
line,
|
||||
} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-frame';
|
||||
import { drawDataBlock } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/data-block';
|
||||
import { Image, Canvas } from 'skia-canvas';
|
||||
import { formatTime } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-time';
|
||||
@ -8,13 +11,11 @@ import { drawTitle } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/title';
|
||||
import { outputEasyImages } from '@/qqbot/plugins/bangDream/tsugu/canvas/output';
|
||||
import { Cutoff } from '@/qqbot/plugins/bangDream/tsugu/models/cutoff';
|
||||
import { drawCutoffChart } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/cutoff-chart';
|
||||
import {
|
||||
serverNameFullList,
|
||||
tierListOfServer,
|
||||
} from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
|
||||
import { serverNameFullList } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
|
||||
import { drawEventDataBlock } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/detail-blocks';
|
||||
import { statusName } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
|
||||
import { BangDreamEventStatus } from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-constants';
|
||||
import { getCutoffTierList } from '@/qqbot/plugins/bangDream/tsugu/models/cutoff-policy';
|
||||
|
||||
/**
|
||||
* 在QQBot 图片视图层中绘制档线全部。
|
||||
@ -43,7 +44,7 @@ export async function drawCutoffAll(
|
||||
const list: Array<Image | Canvas> = [];
|
||||
|
||||
//初始化档线列表
|
||||
const tierList = tierListOfServer[Server[mainServer]];
|
||||
const tierList = getCutoffTierList(mainServer);
|
||||
const cutoffList: Array<Cutoff> = [];
|
||||
for (const i in tierList) {
|
||||
const tempCutoff = new Cutoff(eventId, mainServer, tierList[i]);
|
||||
|
||||
@ -0,0 +1,269 @@
|
||||
import { bangDreamMainDataRepository } from '@/qqbot/plugins/bangDream/tsugu/models/main-data-repository';
|
||||
import { stringToNumberArray } from '@/qqbot/plugins/bangDream/tsugu/models/model-utils';
|
||||
import { BangDreamServerId as Server } from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-protocol';
|
||||
import {
|
||||
BANGDREAM_CN_BLOCKED_EVENT_IDS,
|
||||
BANGDREAM_CN_ESTIMATE_START_EVENT_ID,
|
||||
BANGDREAM_DEFAULT_NO_BANG_DAYS,
|
||||
} from '@/qqbot/plugins/bangDream/tsugu/runtime/runtime-options';
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
const ACTIVE_EVENT_LOOKBACK_MS = DAY_MS;
|
||||
|
||||
export interface BangDreamEventSchedule {
|
||||
endAt: Array<number | null>;
|
||||
startAt: Array<number | null>;
|
||||
}
|
||||
|
||||
export interface BangDreamEventTimeLike extends BangDreamEventSchedule {
|
||||
eventId: number;
|
||||
}
|
||||
|
||||
export interface CnEventEstimatePolicyOptions {
|
||||
blockedEventIds?: readonly number[];
|
||||
defaultNoBangDays?: number;
|
||||
estimateStartEventId?: number;
|
||||
}
|
||||
|
||||
export interface CnEventEstimateCalculationContext {
|
||||
currentEvent: BangDreamEventTimeLike;
|
||||
eventId: number;
|
||||
getSchedule: (eventId: number) => BangDreamEventSchedule | null;
|
||||
options?: CnEventEstimatePolicyOptions;
|
||||
presentJpEventId: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取活动时间表,避免国服预估逻辑依赖 Event class。
|
||||
*
|
||||
* @param eventId - 活动 ID。
|
||||
* @returns 活动时间表。
|
||||
*/
|
||||
export function getBangDreamEventSchedule(
|
||||
eventId: number,
|
||||
): BangDreamEventSchedule | null {
|
||||
const eventData = bangDreamMainDataRepository.getEntity<Record<string, any>>(
|
||||
'events',
|
||||
eventId,
|
||||
);
|
||||
if (!eventData) return null;
|
||||
return {
|
||||
endAt: stringToNumberArray(eventData.endAt),
|
||||
startAt: stringToNumberArray(eventData.startAt),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取某服务器当前或最近结束的活动 ID。
|
||||
*
|
||||
* @param server - 目标服务器。
|
||||
* @param time - 查询时间,默认当前时间。
|
||||
* @returns 活动 ID。
|
||||
*/
|
||||
export function getPresentBangDreamEventId(
|
||||
server: number,
|
||||
time = Date.now(),
|
||||
): number | null {
|
||||
const eventIds = bangDreamMainDataRepository.getNumericIds('events');
|
||||
const activeEventIds: number[] = [];
|
||||
let latestEndedEventId: number | null = null;
|
||||
let latestEndedAt = 0;
|
||||
|
||||
for (const eventId of eventIds) {
|
||||
const schedule = getBangDreamEventSchedule(eventId);
|
||||
const startAt = schedule?.startAt[server];
|
||||
const endAt = schedule?.endAt[server];
|
||||
if (startAt == null || endAt == null) continue;
|
||||
|
||||
if (startAt - ACTIVE_EVENT_LOOKBACK_MS <= time && endAt >= time) {
|
||||
activeEventIds.push(eventId);
|
||||
continue;
|
||||
}
|
||||
if (endAt <= time && endAt > latestEndedAt) {
|
||||
latestEndedAt = endAt;
|
||||
latestEndedEventId = eventId;
|
||||
}
|
||||
}
|
||||
|
||||
if (activeEventIds.length > 0) {
|
||||
return activeEventIds[activeEventIds.length - 1];
|
||||
}
|
||||
return latestEndedEventId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算活动跨越的自然日数量,首尾日都计入。
|
||||
*
|
||||
* @param startTs - 开始时间。
|
||||
* @param endTs - 结束时间。
|
||||
* @returns 自然日数量。
|
||||
*/
|
||||
export function getBangDreamOccupiedDays(
|
||||
startTs: number,
|
||||
endTs: number,
|
||||
): number {
|
||||
const start = new Date(startTs);
|
||||
const end = new Date(endTs);
|
||||
const startDay = new Date(
|
||||
start.getFullYear(),
|
||||
start.getMonth(),
|
||||
start.getDate(),
|
||||
);
|
||||
const endDay = new Date(end.getFullYear(), end.getMonth(), end.getDate());
|
||||
return Math.floor((endDay.getTime() - startDay.getTime()) / DAY_MS) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 纯函数计算国服预估开放时间。
|
||||
*
|
||||
* @param context - 预估所需活动上下文。
|
||||
* @returns 预估开始时间。
|
||||
*/
|
||||
export function calculateCnEventEstimateStartAt({
|
||||
currentEvent,
|
||||
eventId,
|
||||
getSchedule,
|
||||
options,
|
||||
presentJpEventId,
|
||||
}: CnEventEstimateCalculationContext): number | null {
|
||||
const policyOptions = {
|
||||
blockedEventIds: BANGDREAM_CN_BLOCKED_EVENT_IDS,
|
||||
defaultNoBangDays: BANGDREAM_DEFAULT_NO_BANG_DAYS,
|
||||
estimateStartEventId: BANGDREAM_CN_ESTIMATE_START_EVENT_ID,
|
||||
...options,
|
||||
};
|
||||
const currentStartCn = currentEvent.startAt[Server.cn];
|
||||
const currentEndCn = currentEvent.endAt[Server.cn];
|
||||
const currentStartJp = currentEvent.startAt[Server.jp];
|
||||
const currentEndJp = currentEvent.endAt[Server.jp];
|
||||
if (
|
||||
currentStartCn == null ||
|
||||
currentEndCn == null ||
|
||||
currentStartJp == null ||
|
||||
currentEndJp == null
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const eventLenOffset =
|
||||
getBangDreamOccupiedDays(currentStartCn, currentEndCn) -
|
||||
getBangDreamOccupiedDays(currentStartJp, currentEndJp);
|
||||
const currentEventOffset = calculateCurrentEventOffset({
|
||||
currentEvent,
|
||||
eventLenOffset,
|
||||
getSchedule,
|
||||
policyOptions,
|
||||
presentJpEventId,
|
||||
});
|
||||
if (currentEventOffset == null) return null;
|
||||
|
||||
let probableTimeOffset = currentStartCn;
|
||||
for (let i = policyOptions.estimateStartEventId; i < presentJpEventId; i++) {
|
||||
const schedule = getSchedule(i);
|
||||
if (!schedule) continue;
|
||||
if (!schedule.startAt[Server.cn]) {
|
||||
const nextSchedule = getSchedule(i + 1);
|
||||
if (!nextSchedule) continue;
|
||||
probableTimeOffset +=
|
||||
(getBangDreamOccupiedDays(
|
||||
schedule.startAt[Server.jp],
|
||||
nextSchedule.startAt[Server.jp],
|
||||
) -
|
||||
1) *
|
||||
DAY_MS;
|
||||
}
|
||||
if (i + 1 === eventId) return probableTimeOffset + currentEventOffset;
|
||||
if (policyOptions.blockedEventIds.includes(i + 1)) continue;
|
||||
}
|
||||
|
||||
const presentJpSchedule = getSchedule(presentJpEventId);
|
||||
if (
|
||||
!presentJpSchedule ||
|
||||
presentJpSchedule.startAt[Server.jp] == null ||
|
||||
presentJpSchedule.endAt[Server.jp] == null
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const presentJpEventLength =
|
||||
(getBangDreamOccupiedDays(
|
||||
presentJpSchedule.startAt[Server.jp],
|
||||
presentJpSchedule.endAt[Server.jp],
|
||||
) +
|
||||
policyOptions.defaultNoBangDays) *
|
||||
DAY_MS;
|
||||
return probableTimeOffset + currentEventOffset + presentJpEventLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按当前主数据计算国服预估开放时间。
|
||||
*
|
||||
* @param eventId - 目标活动 ID。
|
||||
* @param currentEvent - 国服当前活动。
|
||||
* @returns 预估开始时间。
|
||||
*/
|
||||
export function estimateCnEventStartAt(
|
||||
eventId: number,
|
||||
currentEvent: BangDreamEventTimeLike | null,
|
||||
): number | null {
|
||||
if (!currentEvent) return null;
|
||||
const presentJpEventId = getPresentBangDreamEventId(Server.jp);
|
||||
if (presentJpEventId == null) return null;
|
||||
return calculateCnEventEstimateStartAt({
|
||||
currentEvent,
|
||||
eventId,
|
||||
getSchedule: getBangDreamEventSchedule,
|
||||
presentJpEventId,
|
||||
});
|
||||
}
|
||||
|
||||
function calculateCurrentEventOffset({
|
||||
currentEvent,
|
||||
eventLenOffset,
|
||||
getSchedule,
|
||||
policyOptions,
|
||||
presentJpEventId,
|
||||
}: {
|
||||
currentEvent: BangDreamEventTimeLike;
|
||||
eventLenOffset: number;
|
||||
getSchedule: (eventId: number) => BangDreamEventSchedule | null;
|
||||
policyOptions: Required<CnEventEstimatePolicyOptions>;
|
||||
presentJpEventId: number;
|
||||
}) {
|
||||
const currentEventId = currentEvent.eventId;
|
||||
if (currentEventId < presentJpEventId) {
|
||||
const nextSchedule = getSchedule(currentEventId + 1);
|
||||
if (!nextSchedule || nextSchedule.startAt[Server.jp] == null) return null;
|
||||
return (
|
||||
(getBangDreamOccupiedDays(
|
||||
currentEvent.startAt[Server.jp],
|
||||
nextSchedule.startAt[Server.jp],
|
||||
) -
|
||||
1 +
|
||||
eventLenOffset) *
|
||||
DAY_MS
|
||||
);
|
||||
}
|
||||
if (currentEventId === presentJpEventId) {
|
||||
const previousSchedule = getSchedule(currentEventId - 1);
|
||||
if (!previousSchedule || previousSchedule.endAt[Server.jp] == null) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
(getBangDreamOccupiedDays(
|
||||
previousSchedule.endAt[Server.jp],
|
||||
currentEvent.endAt[Server.jp],
|
||||
) -
|
||||
1 +
|
||||
eventLenOffset) *
|
||||
DAY_MS
|
||||
);
|
||||
}
|
||||
return (
|
||||
(getBangDreamOccupiedDays(
|
||||
currentEvent.startAt[Server.cn],
|
||||
currentEvent.endAt[Server.cn],
|
||||
) +
|
||||
policyOptions.defaultNoBangDays) *
|
||||
DAY_MS
|
||||
);
|
||||
}
|
||||
212
src/qqbot/plugins/bangDream/tsugu/models/cutoff-policy.ts
Normal file
212
src/qqbot/plugins/bangDream/tsugu/models/cutoff-policy.ts
Normal file
@ -0,0 +1,212 @@
|
||||
import {
|
||||
BangDreamEventStatus,
|
||||
BangDreamServerId as Server,
|
||||
} from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-protocol';
|
||||
import {
|
||||
estimateCnEventStartAt,
|
||||
type BangDreamEventTimeLike,
|
||||
} from '@/qqbot/plugins/bangDream/tsugu/models/cn-event-estimate-policy';
|
||||
import {
|
||||
getBangDreamDateByServerTimezone,
|
||||
getBangDreamServerUtcOffset,
|
||||
isBangDreamDailyCheckpoint,
|
||||
normalizeBangDreamTimestamp,
|
||||
} from '@/qqbot/plugins/bangDream/tsugu/models/server-policy';
|
||||
import { BANGDREAM_TIER_LIST_BY_SERVER } from '@/qqbot/plugins/bangDream/tsugu/runtime/runtime-options';
|
||||
|
||||
const FIRST_DAY_END_HOUR = 4;
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
export interface CutoffEventScheduleInput {
|
||||
currentEvent?: BangDreamEventTimeLike | null;
|
||||
endAt: Array<number | null>;
|
||||
eventId: number;
|
||||
server: number;
|
||||
startAt: Array<number | null>;
|
||||
}
|
||||
|
||||
export interface CutoffRecentEventCandidate {
|
||||
eventId: number;
|
||||
eventType: string;
|
||||
startAt: Array<number | null>;
|
||||
}
|
||||
|
||||
export interface CutoffRecentEventSelectionOptions {
|
||||
candidates: CutoffRecentEventCandidate[];
|
||||
count: number;
|
||||
event: CutoffRecentEventCandidate;
|
||||
sameType?: boolean;
|
||||
server: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定服务器支持的档位列表。
|
||||
*
|
||||
* @param server - 目标服务器。
|
||||
* @returns 档位列表。
|
||||
*/
|
||||
export function getCutoffTierList(server: number): readonly number[] {
|
||||
return BANGDREAM_TIER_LIST_BY_SERVER[Server[server]] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断服务器是否支持指定档位。
|
||||
*
|
||||
* @param server - 目标服务器。
|
||||
* @param tier - 目标档位。
|
||||
* @returns 是否支持。
|
||||
*/
|
||||
export function isCutoffTierSupported(server: number, tier: number): boolean {
|
||||
return getCutoffTierList(server).includes(tier);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析档线活动时间,国服缺失时使用预估开始时间。
|
||||
*
|
||||
* @param input - 活动时间输入。
|
||||
* @returns 档线可用的开始与结束时间。
|
||||
*/
|
||||
export function resolveCutoffEventSchedule({
|
||||
currentEvent,
|
||||
endAt,
|
||||
eventId,
|
||||
server,
|
||||
startAt,
|
||||
}: CutoffEventScheduleInput): { endAt: number | null; startAt: number | null } {
|
||||
const directStartAt = startAt[server];
|
||||
const directEndAt = endAt[server];
|
||||
if (directStartAt || server !== Server.cn) {
|
||||
return { endAt: directEndAt, startAt: directStartAt };
|
||||
}
|
||||
|
||||
const estimatedStartAt = estimateCnEventStartAt(eventId, currentEvent);
|
||||
const jpStartAt = startAt[Server.jp];
|
||||
const jpEndAt = endAt[Server.jp];
|
||||
if (estimatedStartAt == null || jpStartAt == null || jpEndAt == null) {
|
||||
return { endAt: directEndAt, startAt: directStartAt };
|
||||
}
|
||||
return {
|
||||
endAt: estimatedStartAt + (jpEndAt - jpStartAt),
|
||||
startAt: estimatedStartAt,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算活动档线状态。
|
||||
*
|
||||
* @param startAt - 开始时间。
|
||||
* @param endAt - 结束时间。
|
||||
* @param now - 当前时间。
|
||||
* @returns 活动状态。
|
||||
*/
|
||||
export function getCutoffEventStatus(
|
||||
startAt: number | null,
|
||||
endAt: number | null,
|
||||
now = Date.now(),
|
||||
): BangDreamEventStatus {
|
||||
if (startAt == null || endAt == null) return BangDreamEventStatus.ended;
|
||||
if (now < startAt) return BangDreamEventStatus.notStart;
|
||||
if (now > endAt) return BangDreamEventStatus.ended;
|
||||
return BangDreamEventStatus.inProgress;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预测窗口,统一使用档线对象已解析的时间。
|
||||
*
|
||||
* @param startAt - 开始时间。
|
||||
* @param endAt - 结束时间。
|
||||
* @returns 秒级预测窗口。
|
||||
*/
|
||||
export function getCutoffPredictionWindow(startAt: number, endAt: number) {
|
||||
return {
|
||||
endTs: Math.floor(endAt / 1000),
|
||||
startTs: Math.floor(startAt / 1000),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算时间点位于活动第几天。
|
||||
*
|
||||
* @param server - 目标服务器。
|
||||
* @param eventStartAt - 活动开始时间。
|
||||
* @param time - 目标时间。
|
||||
* @returns 活动天数索引。
|
||||
*/
|
||||
export function getCutoffDayIndex(
|
||||
server: number,
|
||||
eventStartAt: number,
|
||||
time: number | string,
|
||||
) {
|
||||
if (!eventStartAt) return 0;
|
||||
const offsetMs = getBangDreamServerUtcOffset(server) * 60 * 60 * 1000;
|
||||
const eventStartAtTime = normalizeBangDreamTimestamp(eventStartAt);
|
||||
const timestamp = normalizeBangDreamTimestamp(time);
|
||||
const serverStartTime = eventStartAtTime + offsetMs;
|
||||
const startDate = new Date(serverStartTime);
|
||||
const firstDayEndServerTime =
|
||||
serverStartTime +
|
||||
(DAY_MS +
|
||||
FIRST_DAY_END_HOUR * 60 * 60 * 1000 -
|
||||
startDate.getUTCHours() * 60 * 60 * 1000 -
|
||||
startDate.getUTCMinutes() * 60 * 1000 -
|
||||
startDate.getUTCSeconds() * 1000 -
|
||||
startDate.getUTCMilliseconds());
|
||||
const firstDayEndTime = firstDayEndServerTime - offsetMs;
|
||||
if (timestamp < firstDayEndTime) return 0;
|
||||
return Math.ceil((timestamp - firstDayEndTime) / DAY_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 Date 是否是档线日增 checkpoint。
|
||||
*
|
||||
* @param server - 目标服务器。
|
||||
* @param date - 已按服务器时区转换后的 Date。
|
||||
* @returns 是否为 checkpoint。
|
||||
*/
|
||||
export function isCutoffDailyCheckpoint(server: number, date: Date): boolean {
|
||||
return isBangDreamDailyCheckpoint(server, date);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将时间戳转换为服务器时区 Date。
|
||||
*
|
||||
* @param time - 秒级或毫秒级时间戳。
|
||||
* @param server - 目标服务器。
|
||||
* @returns 服务器时区 Date。
|
||||
*/
|
||||
export function getCutoffDateByServerTimezone(
|
||||
time: number | string,
|
||||
server: number,
|
||||
): Date {
|
||||
return getBangDreamDateByServerTimezone(time, server);
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择用于档线对比的最近活动 ID。
|
||||
*
|
||||
* @param options - 活动候选和过滤条件。
|
||||
* @returns 最近活动 ID 列表。
|
||||
*/
|
||||
export function selectRecentCutoffEventIds({
|
||||
candidates,
|
||||
count,
|
||||
event,
|
||||
sameType = false,
|
||||
server,
|
||||
}: CutoffRecentEventSelectionOptions): number[] {
|
||||
const eventStartAt = event.startAt[server];
|
||||
if (eventStartAt == null) return [];
|
||||
|
||||
const matched = [...candidates]
|
||||
.filter((candidate) => candidate.startAt[server] != null)
|
||||
.sort((a, b) => b.startAt[server] - a.startAt[server])
|
||||
.filter((candidate) => {
|
||||
if (sameType && candidate.eventType !== event.eventType) return false;
|
||||
return candidate.startAt[server] <= eventStartAt;
|
||||
})
|
||||
.sort((a, b) => a.startAt[server] - b.startAt[server]);
|
||||
|
||||
return matched
|
||||
.slice(Math.max(matched.length - count, 0))
|
||||
.map((item) => item.eventId);
|
||||
}
|
||||
@ -6,7 +6,6 @@ import {
|
||||
preferHhwxSource,
|
||||
reportDataSourceProblem,
|
||||
clearDataSourceProblem,
|
||||
tierListOfServer,
|
||||
} from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
|
||||
import { Server } from '@/qqbot/plugins/bangDream/tsugu/models/server';
|
||||
import {
|
||||
@ -16,11 +15,14 @@ import {
|
||||
import { predict } from '@/qqbot/plugins/bangDream/tsugu/calculations/cutoff-predictor';
|
||||
import { logger } from '@/qqbot/plugins/bangDream/tsugu/runtime/logger';
|
||||
import {
|
||||
normalizeTimestamp,
|
||||
getDateByServerTimezone,
|
||||
getServerUtcOffset,
|
||||
getProbableTimeDifference,
|
||||
} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-time';
|
||||
getCutoffDateByServerTimezone,
|
||||
getCutoffDayIndex,
|
||||
getCutoffEventStatus,
|
||||
getCutoffPredictionWindow,
|
||||
isCutoffDailyCheckpoint,
|
||||
isCutoffTierSupported,
|
||||
resolveCutoffEventSchedule,
|
||||
} from '@/qqbot/plugins/bangDream/tsugu/models/cutoff-policy';
|
||||
import { BangDreamEventStatus } from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-constants';
|
||||
|
||||
export class Cutoff {
|
||||
@ -58,7 +60,7 @@ export class Cutoff {
|
||||
this.eventId = eventId;
|
||||
this.server = server;
|
||||
//如果该档线不在该服的档线列表中,直接返回
|
||||
if (!tierListOfServer[Server[server]].includes(tier)) {
|
||||
if (!isCutoffTierSupported(server, tier)) {
|
||||
this.isExist = false;
|
||||
return;
|
||||
}
|
||||
@ -67,29 +69,17 @@ export class Cutoff {
|
||||
// this.startAt = event.startAt[server]
|
||||
// this.endAt = event.endAt[server]
|
||||
// 当该活动在服务器上尚未存在时,使用预测的时间去推断startAt以及endAt,以解决部分情况下卡死及档线状态不对的问题
|
||||
this.startAt =
|
||||
event.startAt[server] || server != Server.cn
|
||||
? event.startAt[server]
|
||||
: getProbableTimeDifference(this.eventId, getPresentEvent(this.server));
|
||||
this.endAt =
|
||||
event.endAt[server] || server != Server.cn
|
||||
? event.endAt[server]
|
||||
: getProbableTimeDifference(
|
||||
this.eventId,
|
||||
getPresentEvent(this.server),
|
||||
) +
|
||||
(event.endAt[Server.jp] - event.startAt[Server.jp]);
|
||||
const tempEvent = new Event(this.eventId);
|
||||
const schedule = resolveCutoffEventSchedule({
|
||||
currentEvent: getPresentEvent(this.server),
|
||||
endAt: event.endAt,
|
||||
eventId: this.eventId,
|
||||
server,
|
||||
startAt: event.startAt,
|
||||
});
|
||||
this.startAt = schedule.startAt;
|
||||
this.endAt = schedule.endAt;
|
||||
this.currentGetDataTime = new Date().getTime();
|
||||
//状态
|
||||
const time = new Date().getTime();
|
||||
if (time < tempEvent.startAt[this.server]) {
|
||||
this.status = BangDreamEventStatus.notStart;
|
||||
} else if (time > tempEvent.endAt[this.server]) {
|
||||
this.status = BangDreamEventStatus.ended;
|
||||
} else {
|
||||
this.status = BangDreamEventStatus.inProgress;
|
||||
}
|
||||
this.status = getCutoffEventStatus(this.startAt, this.endAt);
|
||||
}
|
||||
/**
|
||||
* 在 Cutoff 模型中获取最终 Tracker 数据源。
|
||||
@ -245,9 +235,10 @@ export class Cutoff {
|
||||
this.latestCutoff = this.cutoffs[this.cutoffs.length - 1];
|
||||
}
|
||||
//rate
|
||||
const rateDataList = bangDreamMainDataRepository.getValue<
|
||||
Array<{ server: number; type: string; tier: number; rate: number }>
|
||||
>('rates');
|
||||
const rateDataList =
|
||||
bangDreamMainDataRepository.getValue<
|
||||
Array<{ server: number; type: string; tier: number; rate: number }>
|
||||
>('rates');
|
||||
const rateData = rateDataList.find((element) => {
|
||||
return (
|
||||
element.server == this.server &&
|
||||
@ -294,11 +285,7 @@ export class Cutoff {
|
||||
* @returns 计算后的数值。
|
||||
*/
|
||||
getPredictionWindow(): { startTs: number; endTs: number } {
|
||||
const event = new Event(this.eventId);
|
||||
return {
|
||||
startTs: Math.floor(event.startAt[this.server] / 1000),
|
||||
endTs: Math.floor(event.endAt[this.server] / 1000),
|
||||
};
|
||||
return getCutoffPredictionWindow(this.startAt, this.endAt);
|
||||
}
|
||||
/**
|
||||
* 在 Cutoff 模型中获取档线列表InSeconds。
|
||||
@ -342,36 +329,7 @@ export class Cutoff {
|
||||
* @param ts - ts参数。
|
||||
*/
|
||||
getDaysOfEvent(ts: number) {
|
||||
if (!this.startAt) return 0;
|
||||
const offsetMs = getServerUtcOffset(this.server) * 60 * 60 * 1000;
|
||||
const eventStartAtTime = normalizeTimestamp(this.startAt);
|
||||
const timestamp = normalizeTimestamp(ts);
|
||||
|
||||
const serverStartTime = eventStartAtTime + offsetMs;
|
||||
|
||||
const startDate = new Date(serverStartTime);
|
||||
|
||||
const hour = startDate.getUTCHours();
|
||||
const minute = startDate.getUTCMinutes();
|
||||
const second = startDate.getUTCSeconds();
|
||||
const millisecond = startDate.getUTCMilliseconds();
|
||||
|
||||
const firstDayEndServerTime =
|
||||
serverStartTime +
|
||||
(86400000 +
|
||||
4 * 60 * 60 * 1000 -
|
||||
hour * 60 * 60 * 1000 -
|
||||
minute * 60 * 1000 -
|
||||
second * 1000 -
|
||||
millisecond);
|
||||
|
||||
const firstDayEndTime = firstDayEndServerTime - offsetMs;
|
||||
|
||||
if (timestamp < firstDayEndTime) {
|
||||
return 0;
|
||||
} else {
|
||||
return Math.ceil((timestamp - firstDayEndTime) / 86400000);
|
||||
}
|
||||
return getCutoffDayIndex(this.server, this.startAt, ts);
|
||||
}
|
||||
/**
|
||||
* 在 Cutoff 模型中判断日增Checkpoint。
|
||||
@ -380,13 +338,7 @@ export class Cutoff {
|
||||
* @returns 判断结果。
|
||||
*/
|
||||
isDailyCheckpoint(date: Date): boolean {
|
||||
return (
|
||||
(this.server == Server.cn ||
|
||||
this.server == Server.tw ||
|
||||
this.server == Server.jp) &&
|
||||
date.getUTCHours() === 3 &&
|
||||
date.getUTCMinutes() === 45
|
||||
);
|
||||
return isCutoffDailyCheckpoint(this.server, date);
|
||||
}
|
||||
/**
|
||||
* 在 Cutoff 模型中获取日增CheckpointSeries。
|
||||
@ -397,8 +349,8 @@ export class Cutoff {
|
||||
const score: number[] = [];
|
||||
const time: number[] = [];
|
||||
for (const c of this.cutoffs) {
|
||||
const timestamp = normalizeTimestamp(c.time);
|
||||
const date = getDateByServerTimezone(timestamp, this.server);
|
||||
const timestamp = c.time;
|
||||
const date = getCutoffDateByServerTimezone(timestamp, this.server);
|
||||
if (this.isDailyCheckpoint(date)) {
|
||||
score.push(c.ep);
|
||||
time.push(timestamp);
|
||||
@ -609,11 +561,11 @@ export class Cutoff {
|
||||
let lastCutoffTime = this.cutoffs[this.cutoffs.length - 1].time;
|
||||
// HHWX数据源会在快要结活的时候改为每15分钟抓取一次,因此需要主动规避
|
||||
let usePrevPoint = false;
|
||||
const UTCMin = getDateByServerTimezone(
|
||||
const UTCMin = getCutoffDateByServerTimezone(
|
||||
lastCutoffTime,
|
||||
this.server,
|
||||
).getUTCMinutes();
|
||||
const UTCHour = getDateByServerTimezone(
|
||||
const UTCHour = getCutoffDateByServerTimezone(
|
||||
lastCutoffTime,
|
||||
this.server,
|
||||
).getUTCHours();
|
||||
@ -630,13 +582,13 @@ export class Cutoff {
|
||||
const time: number[] = [];
|
||||
const scoreCur: number[] = [];
|
||||
const timeCur: number[] = [];
|
||||
const dateNow = getDateByServerTimezone(lastCutoffTime, this.server);
|
||||
const dateNow = getCutoffDateByServerTimezone(lastCutoffTime, this.server);
|
||||
const lastestUtcHour = dateNow.getUTCHours();
|
||||
const lastestUtcMinutes = dateNow.getUTCMinutes();
|
||||
|
||||
for (const c of this.cutoffs) {
|
||||
let allowPushFlag = false;
|
||||
const timestamp = normalizeTimestamp(c.time);
|
||||
const timestamp = c.time;
|
||||
const d = this.getDaysOfEvent(timestamp);
|
||||
if (d < curEventDays - 2) {
|
||||
continue;
|
||||
@ -644,7 +596,7 @@ export class Cutoff {
|
||||
if (d > curEventDays - 2) {
|
||||
allowPushFlag = true;
|
||||
}
|
||||
const date = getDateByServerTimezone(timestamp, this.server);
|
||||
const date = getCutoffDateByServerTimezone(timestamp, this.server);
|
||||
if (
|
||||
(this.server == Server.cn ||
|
||||
this.server == Server.tw ||
|
||||
|
||||
@ -3,13 +3,15 @@ import { Server } from '@/qqbot/plugins/bangDream/tsugu/models/server';
|
||||
import { bangDreamMainDataRepository } from '@/qqbot/plugins/bangDream/tsugu/models/main-data-repository';
|
||||
import { Attribute } from '@/qqbot/plugins/bangDream/tsugu/models/attribute';
|
||||
import { Character } from '@/qqbot/plugins/bangDream/tsugu/models/character';
|
||||
import {
|
||||
globalDefaultServer,
|
||||
} from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
|
||||
import { globalDefaultServer } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
|
||||
import { stringToNumberArray } from '@/qqbot/plugins/bangDream/tsugu/models/model-utils';
|
||||
import { getProbableTimeDifference } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-time';
|
||||
import { BANGDREAM_EVENT_TYPE_NAME } from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-constants';
|
||||
import { eventDataRepository } from '@/qqbot/plugins/bangDream/tsugu/models/event-data-repository';
|
||||
import { estimateCnEventStartAt } from '@/qqbot/plugins/bangDream/tsugu/models/cn-event-estimate-policy';
|
||||
import {
|
||||
selectRecentCutoffEventIds,
|
||||
type CutoffRecentEventCandidate,
|
||||
} from '@/qqbot/plugins/bangDream/tsugu/models/cutoff-policy';
|
||||
|
||||
const typeName: Record<string, string> = BANGDREAM_EVENT_TYPE_NAME;
|
||||
|
||||
@ -93,10 +95,9 @@ export class Event {
|
||||
*/
|
||||
constructor(eventId: number) {
|
||||
this.eventId = eventId;
|
||||
const eventData = bangDreamMainDataRepository.getEntity<Record<string, any>>(
|
||||
'events',
|
||||
eventId,
|
||||
);
|
||||
const eventData = bangDreamMainDataRepository.getEntity<
|
||||
Record<string, any>
|
||||
>('events', eventId);
|
||||
if (eventData == undefined) {
|
||||
this.isExist = false;
|
||||
return;
|
||||
@ -387,17 +388,17 @@ export function sortEventList(
|
||||
let prvEvent = null;
|
||||
let nxtEvent = null;
|
||||
if (a.startAt[server] == null) {
|
||||
prvEvent = getProbableTimeDifference(a.eventId, presentEventCN);
|
||||
prvEvent = estimateCnEventStartAt(a.eventId, presentEventCN);
|
||||
} else {
|
||||
prvEvent = a.startAt[server];
|
||||
}
|
||||
if (b.startAt[server] == null) {
|
||||
nxtEvent = getProbableTimeDifference(b.eventId, presentEventCN);
|
||||
nxtEvent = estimateCnEventStartAt(b.eventId, presentEventCN);
|
||||
} else {
|
||||
nxtEvent = b.startAt[server];
|
||||
}
|
||||
if (prvEvent != null || nxtEvent != null) {
|
||||
return prvEvent - nxtEvent;
|
||||
return (prvEvent ?? 0) - (nxtEvent ?? 0);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
@ -425,31 +426,18 @@ export function getRecentEventListByEventAndServer(
|
||||
sameType: boolean = false,
|
||||
) {
|
||||
const eventIdList = bangDreamMainDataRepository.getNumericIds('events');
|
||||
//对活动列表进行排序,从新到旧
|
||||
eventIdList.sort((a, b) => {
|
||||
const eventA = new Event(a);
|
||||
const eventB = new Event(b);
|
||||
if (eventA.startAt[server] == null || eventB.startAt[server] == null) {
|
||||
return 0;
|
||||
}
|
||||
return eventB.startAt[server] - eventA.startAt[server];
|
||||
});
|
||||
const tempEventList: Array<Event> = [];
|
||||
for (let i = 0; i < eventIdList.length; i++) {
|
||||
const tempEvent = new Event(eventIdList[i]);
|
||||
if (tempEvent.startAt[server] != null) {
|
||||
if (sameType && tempEvent.eventType != event.eventType) {
|
||||
continue;
|
||||
}
|
||||
if (tempEvent.startAt[server] > event.startAt[server]) {
|
||||
continue;
|
||||
}
|
||||
tempEventList.push(tempEvent);
|
||||
}
|
||||
}
|
||||
sortEventList(tempEventList, [server]);
|
||||
return tempEventList.slice(
|
||||
tempEventList.length - count,
|
||||
tempEventList.length,
|
||||
);
|
||||
const candidates: CutoffRecentEventCandidate[] = eventIdList
|
||||
.map((eventId) => new Event(eventId))
|
||||
.map((candidate) => ({
|
||||
eventId: candidate.eventId,
|
||||
eventType: candidate.eventType,
|
||||
startAt: candidate.startAt,
|
||||
}));
|
||||
return selectRecentCutoffEventIds({
|
||||
candidates,
|
||||
count,
|
||||
event,
|
||||
sameType,
|
||||
server,
|
||||
}).map((eventId) => new Event(eventId));
|
||||
}
|
||||
|
||||
80
src/qqbot/plugins/bangDream/tsugu/models/server-policy.ts
Normal file
80
src/qqbot/plugins/bangDream/tsugu/models/server-policy.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { BangDreamServerId as Server } from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-protocol';
|
||||
|
||||
const SERVER_UTC_OFFSET_BY_SERVER: Record<number, number> = {
|
||||
[Server.cn]: 8,
|
||||
[Server.en]: 0,
|
||||
[Server.jp]: 9,
|
||||
[Server.kr]: 9,
|
||||
[Server.tw]: 8,
|
||||
};
|
||||
|
||||
const DAILY_CHECKPOINT_SERVERS = new Set<number>([
|
||||
Server.cn,
|
||||
Server.jp,
|
||||
Server.tw,
|
||||
]);
|
||||
|
||||
/**
|
||||
* 规范化上游时间戳为毫秒。
|
||||
*
|
||||
* @param time - 秒级或毫秒级时间戳。
|
||||
* @returns 毫秒级时间戳。
|
||||
*/
|
||||
export function normalizeBangDreamTimestamp(time: number | string): number {
|
||||
const timestamp = Number(time);
|
||||
return timestamp < 1e12 ? timestamp * 1000 : timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器对应的 UTC 偏移小时数。
|
||||
*
|
||||
* @param server - 目标服务器。
|
||||
* @returns UTC 偏移小时数。
|
||||
*/
|
||||
export function getBangDreamServerUtcOffset(server: number): number {
|
||||
return SERVER_UTC_OFFSET_BY_SERVER[server] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将时间戳转换为目标服务器时区下的 Date。
|
||||
*
|
||||
* @param time - 秒级或毫秒级时间戳。
|
||||
* @param server - 目标服务器。
|
||||
* @returns 服务器时区 Date。
|
||||
*/
|
||||
export function getBangDreamDateByServerTimezone(
|
||||
time: number | string,
|
||||
server: number,
|
||||
): Date {
|
||||
const timestamp = normalizeBangDreamTimestamp(time);
|
||||
const offset = getBangDreamServerUtcOffset(server);
|
||||
return new Date(timestamp + offset * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断服务器是否存在每日档线 checkpoint。
|
||||
*
|
||||
* @param server - 目标服务器。
|
||||
* @returns 是否启用每日 checkpoint。
|
||||
*/
|
||||
export function hasBangDreamDailyCheckpoint(server: number): boolean {
|
||||
return DAILY_CHECKPOINT_SERVERS.has(server);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断时间是否命中档线日增 checkpoint。
|
||||
*
|
||||
* @param server - 目标服务器。
|
||||
* @param date - 已按服务器时区转换后的 Date。
|
||||
* @returns 是否命中 checkpoint。
|
||||
*/
|
||||
export function isBangDreamDailyCheckpoint(
|
||||
server: number,
|
||||
date: Date,
|
||||
): boolean {
|
||||
return (
|
||||
hasBangDreamDailyCheckpoint(server) &&
|
||||
date.getUTCHours() === 3 &&
|
||||
date.getUTCMinutes() === 45
|
||||
);
|
||||
}
|
||||
@ -6,13 +6,15 @@ import {
|
||||
} from '@/qqbot/plugins/bangDream/tsugu/models/server';
|
||||
import { drawListByServerList } from './list-frame';
|
||||
import { Canvas } from 'skia-canvas';
|
||||
import { Event } from '@/qqbot/plugins/bangDream/tsugu/models/event';
|
||||
import mainAPI from '@/qqbot/plugins/bangDream/tsugu/models/main-data-store';
|
||||
import {
|
||||
BANGDREAM_CN_BLOCKED_EVENT_IDS,
|
||||
BANGDREAM_CN_ESTIMATE_START_EVENT_ID,
|
||||
BANGDREAM_DEFAULT_NO_BANG_DAYS,
|
||||
} from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-constants';
|
||||
estimateCnEventStartAt,
|
||||
type BangDreamEventTimeLike,
|
||||
} from '@/qqbot/plugins/bangDream/tsugu/models/cn-event-estimate-policy';
|
||||
import {
|
||||
getBangDreamDateByServerTimezone,
|
||||
getBangDreamServerUtcOffset,
|
||||
normalizeBangDreamTimestamp,
|
||||
} from '@/qqbot/plugins/bangDream/tsugu/models/server-policy';
|
||||
|
||||
interface TimeInListOptions {
|
||||
key?: string;
|
||||
@ -35,13 +37,16 @@ export async function drawTimeInList(
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
const element = content[i];
|
||||
if (element == null) {
|
||||
if (i == 3 && estimateCNTime) {
|
||||
if (i == 3 && estimateCNTime && eventId != null) {
|
||||
const currentEvent = getPresentEvent(getServerByName('cn'));
|
||||
const currentEventId = currentEvent.eventId;
|
||||
if (eventId > currentEventId) {
|
||||
const currentEventId = currentEvent?.eventId;
|
||||
const estimatedStartAt = getProbableTimeDifference(
|
||||
eventId,
|
||||
currentEvent,
|
||||
);
|
||||
if (currentEventId != null && eventId > currentEventId) {
|
||||
formattedTimeList.push(
|
||||
formatTime(getProbableTimeDifference(eventId, currentEvent)) +
|
||||
' (预计开放时间)',
|
||||
formatTime(estimatedStartAt) + ' (预计开放时间)',
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -68,91 +73,9 @@ export async function drawTimeInList(
|
||||
*/
|
||||
export function getProbableTimeDifference(
|
||||
eventId: number,
|
||||
currentEvent: Event,
|
||||
): number {
|
||||
const eventsData = mainAPI['events'];
|
||||
const presentEventJP = getPresentEvent(Server.jp).eventId; // 取得日服最新一期的活动
|
||||
let currentEventWithNoBanGDaysTotalOffset = 0;
|
||||
const currentEventId = currentEvent.eventId;
|
||||
// 计算当前活动日服与国服的实际持续天数偏移
|
||||
const eventLenOffset =
|
||||
occupiedDays(
|
||||
new Event(currentEventId).startAt[Server.cn],
|
||||
new Event(currentEventId).endAt[Server.cn],
|
||||
) -
|
||||
occupiedDays(
|
||||
new Event(currentEventId).startAt[Server.jp],
|
||||
new Event(currentEventId).endAt[Server.jp],
|
||||
);
|
||||
// 计算当前正在进行的活动含无邦日当天一共有多少天
|
||||
if (currentEventId < presentEventJP) {
|
||||
currentEventWithNoBanGDaysTotalOffset =
|
||||
(occupiedDays(
|
||||
new Event(currentEventId).startAt[Server.jp],
|
||||
new Event(currentEventId + 1).startAt[Server.jp],
|
||||
) -
|
||||
1 +
|
||||
eventLenOffset) *
|
||||
24 *
|
||||
3600 *
|
||||
1000;
|
||||
} else if (currentEventId == presentEventJP) {
|
||||
// 如果当前活动与日服并行,则计算无邦日就从上一个endAt到这一个endAt
|
||||
currentEventWithNoBanGDaysTotalOffset =
|
||||
(occupiedDays(
|
||||
new Event(currentEventId - 1).endAt[Server.jp],
|
||||
new Event(currentEventId).endAt[Server.jp],
|
||||
) -
|
||||
1 +
|
||||
eventLenOffset) *
|
||||
24 *
|
||||
3600 *
|
||||
1000;
|
||||
} else {
|
||||
// 预防国服可能出现与台服一样存在自有活动的情形,如台服5001。
|
||||
currentEventWithNoBanGDaysTotalOffset =
|
||||
(occupiedDays(
|
||||
new Event(currentEventId).startAt[Server.cn],
|
||||
new Event(currentEventId).endAt[Server.cn],
|
||||
) +
|
||||
BANGDREAM_DEFAULT_NO_BANG_DAYS) *
|
||||
24 *
|
||||
3600 *
|
||||
1000;
|
||||
}
|
||||
let probableTimeOffset = currentEvent.startAt[Server.cn]; // 等于正在举办活动的StartAt
|
||||
for (let i = BANGDREAM_CN_ESTIMATE_START_EVENT_ID; i < presentEventJP; i++) {
|
||||
if (!eventsData[i.toString()]['startAt'][Server.cn]) {
|
||||
// 对于国服来说当前活动未举办
|
||||
// 计算活动相对于日服(含无邦日)的时长。
|
||||
probableTimeOffset +=
|
||||
(occupiedDays(
|
||||
new Event(i).startAt[Server.jp],
|
||||
new Event(i + 1).startAt[Server.jp],
|
||||
) -
|
||||
1) *
|
||||
24 *
|
||||
3600 *
|
||||
1000;
|
||||
}
|
||||
if (i + 1 == eventId)
|
||||
return probableTimeOffset + currentEventWithNoBanGDaysTotalOffset; //如果下一个循环是要获取的时间
|
||||
if (BANGDREAM_CN_BLOCKED_EVENT_IDS.includes(i + 1)) continue; // 对于国服而言不会举办的活动,跳过
|
||||
}
|
||||
const presentEventJPLen =
|
||||
(occupiedDays(
|
||||
new Event(presentEventJP).startAt[Server.jp],
|
||||
new Event(presentEventJP).endAt[Server.jp],
|
||||
) +
|
||||
BANGDREAM_DEFAULT_NO_BANG_DAYS) *
|
||||
24 *
|
||||
3600 *
|
||||
1000;
|
||||
return (
|
||||
probableTimeOffset +
|
||||
currentEventWithNoBanGDaysTotalOffset +
|
||||
presentEventJPLen
|
||||
); // 日服321,预测322+
|
||||
currentEvent: BangDreamEventTimeLike | null,
|
||||
): number | null {
|
||||
return estimateCnEventStartAt(eventId, currentEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -311,24 +234,6 @@ export function formatSeconds(value: number) {
|
||||
* @param endTs - endTs参数。
|
||||
* @returns 计算后的数值。
|
||||
*/
|
||||
function occupiedDays(startTs: number, endTs: number): number {
|
||||
const start = new Date(startTs);
|
||||
const end = new Date(endTs);
|
||||
|
||||
// 取年月日,忽略时分秒
|
||||
const startDay = new Date(
|
||||
start.getFullYear(),
|
||||
start.getMonth(),
|
||||
start.getDate(),
|
||||
);
|
||||
const endDay = new Date(end.getFullYear(), end.getMonth(), end.getDate());
|
||||
|
||||
const msPerDay = 1000 * 60 * 60 * 24;
|
||||
|
||||
// 计算跨越的天数,再加1包含第一天
|
||||
return Math.floor((endDay.getTime() - startDay.getTime()) / msPerDay) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在图片布局层中规范化Timestamp。
|
||||
*
|
||||
@ -336,8 +241,7 @@ function occupiedDays(startTs: number, endTs: number): number {
|
||||
* @returns 计算后的数值。
|
||||
*/
|
||||
export function normalizeTimestamp(time: number | string): number {
|
||||
const t = Number(time);
|
||||
return t < 1e12 ? t * 1000 : t;
|
||||
return normalizeBangDreamTimestamp(time);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -347,19 +251,7 @@ export function normalizeTimestamp(time: number | string): number {
|
||||
* @returns 计算后的数值。
|
||||
*/
|
||||
export function getServerUtcOffset(server: Server): number {
|
||||
switch (server) {
|
||||
case Server.cn:
|
||||
case Server.tw:
|
||||
return 8;
|
||||
|
||||
case Server.jp:
|
||||
case Server.kr:
|
||||
return 9;
|
||||
|
||||
case Server.en:
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
return getBangDreamServerUtcOffset(server);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -373,7 +265,5 @@ export function getDateByServerTimezone(
|
||||
time: number | string,
|
||||
server: Server,
|
||||
): Date {
|
||||
const timestamp = normalizeTimestamp(time);
|
||||
const offset = getServerUtcOffset(server);
|
||||
return new Date(timestamp + offset * 60 * 60 * 1000);
|
||||
return getBangDreamDateByServerTimezone(time, server);
|
||||
}
|
||||
|
||||
160
test/qqbot/plugins/bangDream/tsugu/policy.spec.ts
Normal file
160
test/qqbot/plugins/bangDream/tsugu/policy.spec.ts
Normal file
@ -0,0 +1,160 @@
|
||||
jest.mock('@/qqbot/plugins/bangDream/tsugu/models/main-data-store', () => ({
|
||||
__esModule: true,
|
||||
default: {},
|
||||
}));
|
||||
|
||||
import {
|
||||
calculateCnEventEstimateStartAt,
|
||||
getBangDreamOccupiedDays,
|
||||
type BangDreamEventSchedule,
|
||||
} from '@/qqbot/plugins/bangDream/tsugu/models/cn-event-estimate-policy';
|
||||
import {
|
||||
getCutoffDateByServerTimezone,
|
||||
getCutoffDayIndex,
|
||||
getCutoffEventStatus,
|
||||
getCutoffPredictionWindow,
|
||||
getCutoffTierList,
|
||||
isCutoffDailyCheckpoint,
|
||||
isCutoffTierSupported,
|
||||
selectRecentCutoffEventIds,
|
||||
} from '@/qqbot/plugins/bangDream/tsugu/models/cutoff-policy';
|
||||
import {
|
||||
getBangDreamServerUtcOffset,
|
||||
normalizeBangDreamTimestamp,
|
||||
} from '@/qqbot/plugins/bangDream/tsugu/models/server-policy';
|
||||
import {
|
||||
BangDreamEventStatus,
|
||||
BangDreamServerId as Server,
|
||||
} from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-protocol';
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
describe('BangDream server policy', () => {
|
||||
it('normalizes timestamps and applies server UTC offsets', () => {
|
||||
expect(normalizeBangDreamTimestamp(1_700_000_000)).toBe(1_700_000_000_000);
|
||||
expect(normalizeBangDreamTimestamp(1_700_000_000_000)).toBe(
|
||||
1_700_000_000_000,
|
||||
);
|
||||
expect(getBangDreamServerUtcOffset(Server.cn)).toBe(8);
|
||||
expect(getBangDreamServerUtcOffset(Server.jp)).toBe(9);
|
||||
expect(getBangDreamServerUtcOffset(Server.en)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BangDream CN event estimate policy', () => {
|
||||
it('keeps occupied day calculation inclusive', () => {
|
||||
expect(
|
||||
getBangDreamOccupiedDays(Date.UTC(2026, 0, 1), Date.UTC(2026, 0, 1)),
|
||||
).toBe(1);
|
||||
expect(
|
||||
getBangDreamOccupiedDays(Date.UTC(2026, 0, 1), Date.UTC(2026, 0, 3)),
|
||||
).toBe(3);
|
||||
});
|
||||
|
||||
it('calculates the next missing CN event start from pure schedules', () => {
|
||||
const schedules: Record<number, BangDreamEventSchedule> = {
|
||||
100: {
|
||||
endAt: [Date.UTC(2026, 0, 5), null, null, Date.UTC(2026, 0, 5), null],
|
||||
startAt: [Date.UTC(2026, 0, 1), null, null, Date.UTC(2026, 0, 1), null],
|
||||
},
|
||||
101: {
|
||||
endAt: [Date.UTC(2026, 0, 10), null, null, null, null],
|
||||
startAt: [Date.UTC(2026, 0, 6), null, null, null, null],
|
||||
},
|
||||
};
|
||||
|
||||
const result = calculateCnEventEstimateStartAt({
|
||||
currentEvent: { eventId: 100, ...schedules[100] },
|
||||
eventId: 101,
|
||||
getSchedule: (eventId) => schedules[eventId] || null,
|
||||
options: {
|
||||
defaultNoBangDays: 1,
|
||||
estimateStartEventId: 100,
|
||||
},
|
||||
presentJpEventId: 101,
|
||||
});
|
||||
|
||||
expect(result).toBe(Date.UTC(2026, 0, 6));
|
||||
});
|
||||
});
|
||||
|
||||
describe('BangDream cutoff policy', () => {
|
||||
it('checks tier support by server', () => {
|
||||
expect(getCutoffTierList(Server.cn)).toContain(1500);
|
||||
expect(isCutoffTierSupported(Server.cn, 1500)).toBe(true);
|
||||
expect(isCutoffTierSupported(Server.jp, 1500)).toBe(false);
|
||||
});
|
||||
|
||||
it('resolves event status and prediction windows', () => {
|
||||
const startAt = Date.UTC(2026, 0, 1);
|
||||
const endAt = Date.UTC(2026, 0, 5);
|
||||
|
||||
expect(getCutoffEventStatus(startAt, endAt, startAt - 1)).toBe(
|
||||
BangDreamEventStatus.notStart,
|
||||
);
|
||||
expect(getCutoffEventStatus(startAt, endAt, startAt + 1)).toBe(
|
||||
BangDreamEventStatus.inProgress,
|
||||
);
|
||||
expect(getCutoffEventStatus(startAt, endAt, endAt + 1)).toBe(
|
||||
BangDreamEventStatus.ended,
|
||||
);
|
||||
expect(getCutoffPredictionWindow(startAt, endAt)).toEqual({
|
||||
endTs: Math.floor(endAt / 1000),
|
||||
startTs: Math.floor(startAt / 1000),
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates daily checkpoints and event day index', () => {
|
||||
const checkpoint = getCutoffDateByServerTimezone(
|
||||
Date.UTC(2026, 0, 1, 18, 45),
|
||||
Server.jp,
|
||||
);
|
||||
expect(isCutoffDailyCheckpoint(Server.jp, checkpoint)).toBe(true);
|
||||
expect(isCutoffDailyCheckpoint(Server.en, checkpoint)).toBe(false);
|
||||
|
||||
const eventStartAt = Date.UTC(2026, 0, 1, 12);
|
||||
expect(getCutoffDayIndex(Server.jp, eventStartAt, eventStartAt + 1)).toBe(
|
||||
0,
|
||||
);
|
||||
expect(
|
||||
getCutoffDayIndex(Server.jp, eventStartAt, eventStartAt + DAY_MS),
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('selects recent comparable events by type and server start time', () => {
|
||||
const candidates = [
|
||||
eventCandidate(1, 'story', [Date.UTC(2026, 0, 1)]),
|
||||
eventCandidate(2, 'festival', [Date.UTC(2026, 0, 5)]),
|
||||
eventCandidate(3, 'story', [Date.UTC(2026, 0, 10)]),
|
||||
eventCandidate(4, 'story', [Date.UTC(2026, 0, 15)]),
|
||||
];
|
||||
|
||||
expect(
|
||||
selectRecentCutoffEventIds({
|
||||
candidates,
|
||||
count: 2,
|
||||
event: candidates[3],
|
||||
sameType: true,
|
||||
server: Server.jp,
|
||||
}),
|
||||
).toEqual([3, 4]);
|
||||
});
|
||||
});
|
||||
|
||||
function eventCandidate(
|
||||
eventId: number,
|
||||
eventType: string,
|
||||
jpStartAt: number[],
|
||||
) {
|
||||
return {
|
||||
eventId,
|
||||
eventType,
|
||||
startAt: [
|
||||
jpStartAt[0],
|
||||
null,
|
||||
null,
|
||||
jpStartAt[Server.cn] ?? jpStartAt[0],
|
||||
null,
|
||||
],
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user