kt-template-online-api/src/qqbot/plugins/bangDream/tsugu/models/song.ts

437 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Image, loadImage } from 'skia-canvas';
import { bangDreamBestdoriProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/bestdori-provider';
import {
getServerByPriority,
Server,
} from '@/qqbot/plugins/bangDream/tsugu/models/server';
import { bangDreamMainDataRepository } from '@/qqbot/plugins/bangDream/tsugu/models/main-data-repository';
import { stringToNumberArray } from '@/qqbot/plugins/bangDream/tsugu/models/model-utils';
import { assetErrorImageBuffer } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils';
import {
BANGDREAM_DIFFICULTY_COLORS,
BANGDREAM_DIFFICULTY_NAME_BY_ID,
BANGDREAM_DIFFICULTY_NAMES,
BANGDREAM_SERVER_CODES,
BANGDREAM_SONG_TAG_NAME,
} from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-constants';
export const difficultyName: Record<number, string> =
BANGDREAM_DIFFICULTY_NAME_BY_ID;
export const tagNameList: Record<string, string> = BANGDREAM_SONG_TAG_NAME;
export const difficultyColorList = [...BANGDREAM_DIFFICULTY_COLORS];
export const difficultyNameList: string[] = [...BANGDREAM_DIFFICULTY_NAMES];
export class Song {
songId: number;
isExist = false;
data: object;
tag: string;
bandId: number;
jacketImage: Array<string>;
musicTitle: Array<string | null>;
publishedAt: Array<number | null>;
closedAt: Array<number | null>;
difficulty: {
[difficultyId: number]: {
playLevel: number;
multiLiveScoreMap?: object;
notesQuantity?: number;
scoreC?: number;
scoreB?: number;
scoreA?: number;
scoreS?: number;
scoreSS?: number;
publishedAt?: Array<number | null>;
};
};
length: number;
notes: {
[difficultyId: number]: number;
};
bpm: {
[difficultyId: number]: Array<{
bpm: number;
start: number;
end: number;
}>;
};
//other
bgmId: string;
bgmFile: string;
seq: number;
achievements: Array<{
musicId: number;
achievementType: string;
rewardType: string;
quantity: number;
}>;
detail: {
lyricist: string[];
composer: string[];
arranger: string[];
};
howToGet: Array<string | null>;
//用于模糊搜索
songLevels: number[] = [];
nickname: string | null = null;
//meta数据
hasMeta = false;
meta: {
[difficultyId: number]: {
[skillDuration: number]: [
withoutFeverWithoutSkill: number,
withoutFeverWithSkill: number,
withFeverWithoutSkill: number,
withFeverWithSkill: number,
];
};
};
isInitfull = false;
/**
* 构造 Song 实例,并初始化该模型的本地基础字段。
*
* @param songId - 歌曲 ID。
*/
constructor(songId: number) {
this.songId = songId;
const songData = bangDreamMainDataRepository.getEntity<Record<string, any>>(
'songs',
songId,
);
if (songData == undefined) {
this.isExist = false;
return;
}
this.isExist = true;
this.data = songData;
this.tag = songData['tag'];
this.bandId = songData['bandId'];
this.jacketImage = songData['jacketImage'];
this.musicTitle = songData['musicTitle'];
this.publishedAt = songData['publishedAt']
? stringToNumberArray(songData['publishedAt'])
: [];
this.closedAt = songData['closedAt']
? stringToNumberArray(songData['closedAt'])
: [];
this.difficulty = songData['difficulty'];
this.length = songData['length'];
this.notes = songData['notes'];
this.bpm = songData['bpm'];
this.nickname = songData['nickname'];
for (const i in this.difficulty) {
const playLevel = this.difficulty[i].playLevel;
this.songLevels.push(playLevel !== undefined ? playLevel : 0);
}
//meta数据
const metaData = bangDreamMainDataRepository.getEntity<Record<string, any>>(
'meta',
songId,
);
if (metaData == undefined) {
return;
}
this.hasMeta = true;
this.meta = metaData;
}
/**
* 在 Song 模型中加载远端完整详情并标记初始化状态。
*/
async initFull() {
if (this.isInitfull) {
return;
}
if (this.isExist == false) {
return;
}
const songData = await this.getData();
this.data = songData;
this.tag = songData['tag'];
this.bandId = songData['bandId'];
this.jacketImage = songData['jacketImage'];
this.musicTitle = songData['musicTitle'];
this.publishedAt = songData['publishedAt']
? stringToNumberArray(songData['publishedAt'])
: [];
this.closedAt = songData['closedAt']
? stringToNumberArray(songData['closedAt'])
: [];
this.difficulty = songData['difficulty'];
this.length = songData['length'];
this.notes = songData['notes'];
this.bpm = songData['bpm'];
//other
this.bgmId = songData['bgmId'];
this.bgmFile = songData['bgmFile'];
this.achievements = songData['achievements'];
this.seq = songData['seq'];
this.detail = {
lyricist: songData['lyricist'],
composer: songData['composer'],
arranger: songData['arranger'],
};
this.howToGet = songData['howToGet'];
this.isInitfull = true;
}
/**
* 在 Song 模型中请求当前模型的远端详情数据。
*/
async getData() {
const songData = await bangDreamBestdoriProvider.getJson<
Record<string, any>
>(
`/api/songs/${this.songId}.json`,
);
return songData;
}
/**
* 在 Song 模型中获取歌曲资源批次。
*
* @returns 计算后的数值。
*/
getSongRip(): number {
return Math.ceil(this.songId / 10) * 10;
}
/**
* 在 Song 模型中获取歌曲封面图片。
*
* @param displayedServerList - 允许展示或下载资源的服务器优先级列表,未传入时使用默认值。
* @returns 异步处理结果。
*/
async getSongJacketImage(
displayedServerList: Server[] = [Server.jp, Server.cn],
): Promise<Image> {
let jacketImageBuffer = await bangDreamBestdoriProvider.getAsset(
this.getSongJacketImagePath(displayedServerList),
{ memoryCache: false },
);
//下载失败自动尝试切换服务器下载
if (jacketImageBuffer.equals(assetErrorImageBuffer)) {
const jacketImageName = this.jacketImage[this.jacketImage.length - 1];
for (const server of BANGDREAM_SERVER_CODES) {
const retryPath = `/assets/${server}/musicjacket/musicjacket${this.getSongRip()}_rip/assets-star-forassetbundle-startapp-musicjacket-musicjacket${this.getSongRip()}-${jacketImageName}-jacket.png`;
jacketImageBuffer = await bangDreamBestdoriProvider.getAsset(
retryPath,
{ ignoreError: true, memoryCache: false, retryCount: 1 },
);
if (!jacketImageBuffer.equals(assetErrorImageBuffer)) break;
}
}
return await loadImage(jacketImageBuffer);
}
/**
* 在 Song 模型中获取歌曲封面图片URL。
*
* @param displayedServerList - 允许展示或下载资源的服务器优先级列表,未传入时使用默认值。
* @returns 格式化后的文本。
*/
getSongJacketImageURL(displayedServerList?: Server[]): string {
return bangDreamBestdoriProvider.resolveUrl(
this.getSongJacketImagePath(displayedServerList),
);
}
/**
* 在 Song 模型中获取歌曲封面图片资源路径。
*
* @param displayedServerList - 允许展示或下载资源的服务器优先级列表,未传入时使用默认值。
* @returns 格式化后的资源路径。
*/
getSongJacketImagePath(displayedServerList?: Server[]): string {
let server = getServerByPriority(this.publishedAt, displayedServerList);
const jacketImageName = this.jacketImage[this.jacketImage.length - 1];
let songRip = this.getSongRip();
if (this.songId == 13 || this.songId == 40) {
songRip = 30;
} else if (this.songId == 273) {
//针对273的修复
server = Server.cn;
}
return `/assets/${Server[server]}/musicjacket/musicjacket${songRip}_rip/assets-star-forassetbundle-startapp-musicjacket-musicjacket${songRip}-${jacketImageName.toLowerCase()}-jacket.png`;
}
/**
* 在 Song 模型中获取Tag名称。
*
* @returns 格式化后的文本。
*/
getTagName(): string {
if (this.tag == undefined) {
return this.tag;
}
return tagNameList[this.tag];
}
/**
* 在 Song 模型中获取歌曲谱面。
*
* @param difficultyId - 难度ID参数。
* @returns 异步处理结果。
*/
async getSongChart(difficultyId: number): Promise<object> {
const songChart = await bangDreamBestdoriProvider.getJson<object>(
`/api/charts/${this.songId}/${difficultyName[difficultyId]}.json`,
);
return songChart;
}
/*
第一个键是歌曲ID第二个键是难度ID第三个键是技能时长
取到的数组是[ 非fever非技能占比, 非fever技能占比, fever非技能占比, fever技能占比 ]
天下EX7秒技能的话就取meta[125][3][7]
返回[ 1.7464, 2.1164, 2.0527, 2.789 ]
协力带fever只看2.0527, 2.789
如果技能是115%的话总百分比为2.0527 + 215% * 2.789
上面那个算出来之后最后再乘准确度加成1.1 * P% + 0.8 * (1 - P%)
得到的就和站上meta的数字一样了
然后乘上队伍综合力就行
*/
/**
* 在 Song 模型中计算Meta。
*
* @param withFever - withFever参数。
* @param difficultyId - 难度ID参数。
* @param scoreUpMaxValue - 分数UpMax值参数未传入时使用默认值。
* @param skillDuration - 技能Duration参数未传入时使用默认值。
* @param accruacy - accruacy参数未传入时使用默认值。
* @returns 计算后的数值。
*/
calcMeta(
withFever: boolean,
difficultyId: number,
scoreUpMaxValue: number = 100,
skillDuration: number = 7,
accruacy: number = 100,
): number {
if (this.hasMeta == false) {
return 0;
}
let skillParameter: number;
if (withFever) {
skillParameter =
this.meta[difficultyId][skillDuration][2] +
((100 + scoreUpMaxValue) / 100) *
this.meta[difficultyId][skillDuration][3];
} else {
skillParameter =
this.meta[difficultyId][skillDuration][0] +
((100 + scoreUpMaxValue) / 100) *
this.meta[difficultyId][skillDuration][1];
}
const scoreParameter =
skillParameter * ((1.1 * accruacy) / 100 + 0.8 * (1 - accruacy / 100));
return scoreParameter;
}
}
//获取时间范围内指定服务器推出的新歌
/**
* 在BangDream 领域模型层中获取Present歌曲列表。
*
* @param mainServer - 主数据服务器参数。
* @param start - start参数未传入时使用默认值。
* @param end - end参数未传入时使用默认值。
* @returns 处理后的列表。
*/
export function getPresentSongList(
mainServer: Server,
start: number = Date.now(),
end: number = Date.now(),
): Song[] {
const songList: Array<Song> = [];
const songListMain = bangDreamMainDataRepository.getCollection('songs');
for (const songId in songListMain) {
if (Object.prototype.hasOwnProperty.call(songListMain, songId)) {
const song = new Song(parseInt(songId));
// 检查活动的发布时间和结束时间是否在指定范围内
if (song.publishedAt[mainServer] == null) {
continue;
}
if (
song.publishedAt[mainServer] <= end &&
song.publishedAt[mainServer] >= start
) {
songList.push(song);
}
for (const i in song.difficulty) {
if (song.difficulty[i].publishedAt != undefined) {
if (
song.difficulty[i].publishedAt[mainServer] <= end &&
song.difficulty[i].publishedAt[mainServer] >= start
) {
songList.push(song);
}
}
}
}
}
return songList;
}
export interface SongInRank {
songId: number;
difficulty: number;
meta: number;
rank: number;
}
/**
* 在BangDream 领域模型层中获取MetaRanking。
*
* @param withFever - withFever参数。
* @param mainServer - 主数据服务器参数。
* @returns 处理后的列表。
*/
export function getMetaRanking(
withFever: boolean,
mainServer: Server,
): SongInRank[] {
const songIdList = bangDreamMainDataRepository.getNumericIds('meta');
const songRankList: SongInRank[] = [];
for (let i = 0; i < songIdList.length; i++) {
const songId = songIdList[i];
const song = new Song(songId);
//如果在所选服务器都没有发布,或者难度信息缺失,则跳过
if (
song.publishedAt[mainServer] == null ||
Object.keys(song.notes).length == 0
) {
continue;
}
//如果没有meta数据则跳过
if (song.hasMeta == false) {
continue;
}
//有一些song没有4 difficulty
for (const j in song.difficulty) {
const difficulty = parseInt(j);
const meta = song.calcMeta(withFever, difficulty);
songRankList.push({
songId: song.songId,
difficulty: difficulty,
meta: meta,
rank: 0,
});
}
}
songRankList.sort((a, b) => {
return b.meta - a.meta;
});
for (let i = 0; i < songRankList.length; i++) {
songRankList[i].rank = i;
}
return songRankList;
}