import { bangDreamBestdoriProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/bestdori-provider'; import { Server } from '@/qqbot/plugins/bangDream/tsugu/models/server'; import { Card, addStat, Stat, } from '@/qqbot/plugins/bangDream/tsugu/models/card'; import { AreaItem } from '@/qqbot/plugins/bangDream/tsugu/models/area-item'; import { Event } from '@/qqbot/plugins/bangDream/tsugu/models/event'; import { difficultyNameList } from '@/qqbot/plugins/bangDream/tsugu/models/song'; import { logger } from '@/qqbot/plugins/bangDream/tsugu/runtime/logger'; /* - mode=0 只从缓存取,无需等待队列立即返回缓存数据 - mode=1 同上立即返回缓存数据,但同时再放入队列请求服务器后台更新 - mode=2 放入队列等待新鲜数据,如果耗时过长就返回缓存数据 - mode=3 放入队列持续等待新鲜数据 */ export class Player { playerId: number; isExist: boolean = false; initError: boolean = false; cache: boolean; time: number; profile: { userId: string; userName: string; rank: number; degree: number; introduction: string; publishTotalDeckPowerFlg: boolean; publishBandRankFlg: boolean; publishMusicClearedFlg: boolean; publishMusicFullComboFlg: boolean; publishHighScoreRatingFlg: boolean; publishUserIdFlg: boolean; searchableFlg: boolean; publishUpdatedAtFlg: boolean; friendApplicableFlg: boolean; publishMusicAllPerfectFlg: boolean; publishDeckRankFlg: boolean; publishStageChallengeAchievementConditionsFlg: boolean; publishStageChallengeFriendRankingFlg: boolean; publishCharacterRankFlg: boolean; mainDeckUserSituations: { entries: Array<{ userId: string; situationId: number; level: number; exp: number; createdAt: string; addExp: number; trainingStatus: 'not_doing' | 'done'; duplicateCount: number; illust: 'after_training' | 'normal'; skillExp: number; skillLevel: number; userAppendParameter: { userId: string; situationId: number; performance: number; technique: number; visual: number; characterPotentialPerformance: number; characterPotentialTechnique: number; characterPotentialVisual: number; characterBonusPerformance?: number; characterBonusTechnique?: number; characterBonusVisual?: number; }; limitBreakRank: number; }>; }; enabledUserAreaItems: { entries: Array<{ userId: string; areaItemId: number; areaItemCategory: number; level: number; }>; }; bandRankMap: { entries: { [bandId: number]: number; }; }; userHighScoreRating: { [bandHighScoreRatingName: string]: { entries: Array<{ musicId: number; difficulty: string; rating: number; }>; }; }; mainUserDeck: { deckId: number; deckName: string; leader: number; member1: number; member2: number; member3: number; member4: number; deckType: string; }; userProfileSituation: { userId: string; situationId: number; illust: 'after_training' | 'normal'; viewProfileSituationStatus: 'deck_leader' | 'profile_situation'; }; userProfileDegreeMap: { entries: { first: { userId: string; profileDegreeType: string; degreeId: number; }; second: { userId: string; profileDegreeType: string; degreeId: number; }; }; }; userTwitter?: { twitterId: string; twitterName: string; screenName: string; url: string; profileImageUrl: string; }; userDeckTotalRatingMap: { entries: { [bandId: number]: { rank: string; score: number; level: number; lowerRating: number; upperRating: number; }; }; }; stageChallengeAchievementConditionsMap: { entries: { [bandId: number]: number; }; }; userMusicClearInfoMap: { entries: { [difficultyName: string]: { clearedMusicCount: number; fullComboMusicCount: number; allPerfectMusicCount: number; }; }; }; userCharacterRankMap: { entries: { [characterId: number]: { rank: number; exp: number; addExp: number; nextExp: number; totalExp: number; releasedPotentialLevel: number; }; }; }; //其他 //卡牌列表 cardList: Card[]; //插画 userIllustration: { cardId: number; trainingStatus: boolean }; }; server: Server; isInitfull: boolean = false; /** * 构造 Player 实例,并初始化该模型的本地基础字段。 * * @param playerId - 玩家ID参数。 * @param server - 目标服务器。 */ constructor(playerId: number, server: Server) { this.playerId = playerId; this.server = server; } /** * 在 Player 模型中加载远端完整详情并标记初始化状态。 * * @param useCache - use缓存参数,未传入时使用默认值。 * @param mode - mode参数,未传入时使用默认值。 */ async initFull(useCache: boolean = false, mode: 0 | 1 | 2 | 3 = 2) { if (this.isInitfull) { return; } const cacheTime = useCache ? 1 / 0 : 0; let playerData; try { playerData = await bangDreamBestdoriProvider.getJson( `/api/player/${Server[this.server]}/${this.playerId}?mode=${mode}`, { cacheTime, retryCount: 1 }, ); if (mode == 1 && !isFinite(cacheTime)) { // 后台更新玩家信息,确保下一次用最新的缓存去显示 bangDreamBestdoriProvider .getJson( `/api/player/${Server[this.server]}/${this.playerId}?mode=${mode}`, { cacheTime: 300, retryCount: 1 }, ) .catch((err) => { logger('InitPlayer', err); }); } } catch { this.isExist = false; this.initError = true; return; } if (!playerData['result'] || playerData['data']['profile'] == null) { this.isExist = false; this.initError = true; return; } this.isExist = true; this.cache = playerData['data']['cache']; this.time = playerData['data']['time']; this.profile = playerData['data']['profile']; //卡牌列表 this.profile.cardList = []; for ( let i = 0; i < this.profile.mainDeckUserSituations.entries.length; i++ ) { const cardData = this.profile.mainDeckUserSituations.entries[i]; const card = new Card(cardData.situationId); this.profile.cardList.push(card); } //插画 this.profile.userIllustration = this.getUserIllustration(); //修复新旧API难度信息不兼容问题 if (this.profile.userMusicClearInfoMap == undefined) { this.profile.userMusicClearInfoMap = { entries: {} }; for (let i = 0; i < difficultyNameList.length; i++) { const difficultyName = difficultyNameList[i]; this.profile.userMusicClearInfoMap.entries[difficultyName] = { clearedMusicCount: 0, fullComboMusicCount: 0, allPerfectMusicCount: 0, }; } if (this.profile['clearedMusicCountMap']?.['entries'] != undefined) { for (let i = 0; i < difficultyNameList.length; i++) { const difficultyName = difficultyNameList[i]; const number = this.profile['clearedMusicCountMap']['entries'][difficultyName] || 0; this.profile.userMusicClearInfoMap.entries[difficultyName][ 'clearedMusicCount' ] = number; } } if (this.profile['fullComboMusicCountMap']?.['entries'] != undefined) { for (let i = 0; i < difficultyNameList.length; i++) { const difficultyName = difficultyNameList[i]; const number = this.profile['fullComboMusicCountMap']['entries'][difficultyName] || 0; this.profile.userMusicClearInfoMap.entries[difficultyName][ 'fullComboMusicCount' ] = number; } } if (this.profile['allPerfectMusicCountMap']?.['entries'] != undefined) { for (let i = 0; i < difficultyNameList.length; i++) { const difficultyName = difficultyNameList[i]; const number = this.profile['allPerfectMusicCountMap']['entries'][ difficultyName ] || 0; this.profile.userMusicClearInfoMap.entries[difficultyName][ 'allPerfectMusicCount' ] = number; } } } this.isInitfull = true; } /** * 在 Player 模型中计算数值。 * * @param event - 活动参数,未传入时使用默认值。 * @returns 异步处理结果。 */ async calcStat(event?: Event): Promise { if (this.profile.publishTotalDeckPowerFlg == false) { return { performance: 0, technique: 0, visual: 0, }; } //计算卡牌本身属性 const cardDataList = this.profile.mainDeckUserSituations.entries; const cardStatList = []; const cardStat: Stat = { //所有卡牌的属性总和 performance: 0, technique: 0, visual: 0, }; for (let i = 0; i < cardDataList.length; i++) { const cardData = cardDataList[i]; const card = new Card(cardData.situationId); const tempStat = await card.calcStat(cardData); addStat(cardStat, tempStat); cardStatList.push(tempStat); } //计算区域道具属性 const extraStat: Stat = { //所有卡牌的额外属性总和 performance: 0, technique: 0, visual: 0, }; const areaItemList = this.profile.enabledUserAreaItems.entries; for (let i = 0; i < areaItemList.length; i++) { const element = areaItemList[i]; const areaItem = new AreaItem(element.areaItemCategory); const areaItemLevel = element.level; for (let j = 0; j < cardStatList.length; j++) { const cardStat = cardStatList[j]; const card = this.profile.cardList[j]; const tempStat = areaItem.calcStat( card, areaItemLevel, cardStat, this.server, ); addStat(extraStat, tempStat); } } const eventStat: Stat = { //所有卡牌的额外属性总和 performance: 0, technique: 0, visual: 0, }; if (event != undefined) { for (let i = 0; i < cardStatList.length; i++) { const cardStat = cardStatList[i]; const card = this.profile.cardList[i]; let isCharacter = false; let isAttribute = false; for (let j = 0; i < event.characters.length; j++) { const characterPercent = event.characters[j]; if (card.characterId == characterPercent.characterId) { const tempStat = { performance: (cardStat.performance * characterPercent.percent) / 100, technique: (cardStat.technique * characterPercent.percent) / 100, visual: (cardStat.visual * characterPercent.percent) / 100, }; addStat(eventStat, tempStat); isCharacter = true; } } for (let j = 0; j < event.attributes.length; j++) { const attributePercent = event.attributes[j]; if (card.attribute == attributePercent.attribute) { const tempStat = { performance: (cardStat.performance * attributePercent.percent) / 100, technique: (cardStat.technique * attributePercent.percent) / 100, visual: (cardStat.visual * attributePercent.percent) / 100, }; addStat(eventStat, tempStat); isAttribute = true; } } if ( isCharacter && isAttribute && event.eventAttributeAndCharacterBonus != undefined ) { if (event.eventAttributeAndCharacterBonus.parameterPercent != 0) { const tempStat = { performance: (cardStat.performance * event.eventAttributeAndCharacterBonus.parameterPercent) / 100, technique: (cardStat.technique * event.eventAttributeAndCharacterBonus.parameterPercent) / 100, visual: (cardStat.visual * event.eventAttributeAndCharacterBonus.parameterPercent) / 100, }; addStat(eventStat, tempStat); } } } addStat(extraStat, eventStat); } //相加 addStat(cardStat, extraStat); //将cardStat所有参数向下取整 cardStat.performance = cardStat.performance; cardStat.technique = cardStat.technique; cardStat.visual = cardStat.visual; return cardStat; } /** * 在 Player 模型中计算HSR。 * * @returns 计算后的数值。 */ calcHSR(): number { let hsr = 0; const userHighScoreRating = this.profile.userHighScoreRating; for (const i in userHighScoreRating) { if (Object.prototype.hasOwnProperty.call(userHighScoreRating, i)) { const userBandHighScoreRating = userHighScoreRating[i].entries; for (let j = 0; j < userBandHighScoreRating.length; j++) { const element = userBandHighScoreRating[j]; hsr += element.rating; } } } return hsr; } /** * 在 Player 模型中获取UserIllustration。 * * @returns 判断结果。 */ getUserIllustration(): { cardId: number; trainingStatus: boolean } { let illustrationCardId: number; let trainingStatus: boolean; let viewProfileSituationStatus: string; if (Object.keys(this.profile.userProfileSituation).length != 0) { viewProfileSituationStatus = this.profile.userProfileSituation.viewProfileSituationStatus; } else { viewProfileSituationStatus = 'deck_leader'; } if (viewProfileSituationStatus == 'deck_leader') { illustrationCardId = this.profile.mainDeckUserSituations.entries[0].situationId; trainingStatus = this.profile.mainDeckUserSituations.entries[0].illust === 'after_training' ? true : false; } else { illustrationCardId = this.profile.userProfileSituation.situationId; trainingStatus = this.profile.userProfileSituation.illust === 'after_training' ? true : false; } return { cardId: illustrationCardId, trainingStatus: trainingStatus }; } }