293 lines
9.3 KiB
Dart
293 lines
9.3 KiB
Dart
import 'dart:math';
|
||
import '../model/song_model.dart';
|
||
|
||
class RecommendationHelper {
|
||
|
||
/// 基于 Java MusicService 逻辑的智能推荐
|
||
///
|
||
/// [allSongs]: 全量歌曲库 (SongModel)
|
||
/// [userMusicList]: 用户成绩列表 (dynamic)
|
||
/// - 情况A: List<Map>,每个 Map 包含 musicId, level, achievement, dx_rating
|
||
/// - 情况B: List<Map>,每个 Map 包含 userMusicDetailList (嵌套结构)
|
||
/// [userRating]: 用户当前总 Rating (DX Rating),用于确定推荐难度区间
|
||
/// [b35MinRating]: 用户当前 B35 中最低的那首曲子的 Rating (用于判断推分是否有意义)
|
||
/// [count]: 推荐数量
|
||
static List<SongModel> getSmartRecommendations({
|
||
required List<SongModel> allSongs,
|
||
required List<dynamic> userMusicList,
|
||
required int userRating,
|
||
required int b35MinRating,
|
||
int count = 6,
|
||
}) {
|
||
if (allSongs.isEmpty) return [];
|
||
|
||
final random = Random();
|
||
|
||
// 1. 解析用户成绩,构建映射: Key = "musicId_level", Value = { achievement, rating }
|
||
// 使用 HashMap 提高查找速度
|
||
final Map<String, Map<String, dynamic>> playedMap = {};
|
||
|
||
for (var group in userMusicList) {
|
||
if (group is! Map) continue;
|
||
|
||
List<dynamic> details = [];
|
||
|
||
// 兼容两种数据结构:
|
||
if (group.containsKey('userMusicDetailList')) {
|
||
details = group['userMusicDetailList'] as List? ?? [];
|
||
} else if (group.containsKey('achievement') || group.containsKey('dx_rating')) {
|
||
// 如果当前 group 本身就是成绩对象
|
||
details = [group];
|
||
}
|
||
|
||
if (details.isEmpty) continue;
|
||
|
||
for (var detail in details) {
|
||
if (detail is! Map) continue;
|
||
|
||
// 提取关键字段
|
||
final int musicId = detail['musicId'] ?? detail['id'] ?? 0;
|
||
if(musicId>16000) continue;
|
||
final int level = detail['level'] ?? detail['levelIndex'] ?? 3; // 默认 Master
|
||
final int achievement = detail['achievement'] ?? 0;
|
||
// 确保 rating 是 double
|
||
final dynamic rawRating = detail['dx_rating'] ?? detail['rating'] ?? 0;
|
||
final double rating = rawRating is num ? rawRating.toDouble() : 0.0;
|
||
|
||
if (musicId == 0) continue;
|
||
|
||
String key = "${musicId}_${level}";
|
||
|
||
// 只保留该难度下的最高成绩
|
||
if (!playedMap.containsKey(key) || achievement > (playedMap[key]!['achievement'] as int)) {
|
||
playedMap[key] = {
|
||
'musicId': musicId,
|
||
'level': level,
|
||
'achievement': achievement,
|
||
'rating': rating,
|
||
};
|
||
}
|
||
}
|
||
}
|
||
|
||
// 2. 计算推荐难度区间 (基于 Java logic)
|
||
double minRecommendedLevel = 0.0;
|
||
|
||
if (userRating >= 15500) {
|
||
minRecommendedLevel = 13.6;
|
||
} else if (userRating >= 15000) {
|
||
minRecommendedLevel = 13.0;
|
||
} else if (userRating >= 14500) {
|
||
minRecommendedLevel = 12.5;
|
||
} else if (userRating >= 14000) {
|
||
minRecommendedLevel = 12.0;
|
||
} else if (userRating >= 13000) {
|
||
minRecommendedLevel = 11.5;
|
||
} else if (userRating >= 12000) {
|
||
minRecommendedLevel = 11.0;
|
||
} else if (userRating >= 11000) {
|
||
minRecommendedLevel = 10.5;
|
||
} else {
|
||
minRecommendedLevel = 9.0; // 新手保护线
|
||
}
|
||
|
||
List<SongModel> candidatesForImprovement = []; // 用于推分
|
||
List<SongModel> candidatesNew = []; // 新曲/未玩
|
||
|
||
// 用于快速判断是否已加入结果集,避免 O(N^2) 的 contains 检查
|
||
final Set<int> addedSongIds = {};
|
||
|
||
for (var song in allSongs) {
|
||
// 过滤无效 ID
|
||
if (song.id < 100) continue;
|
||
|
||
// 获取 Master (Level 3) 的定数,如果没有则获取 Expert (Level 2)
|
||
double? masterLevel = _getSongLevel(song, 3);
|
||
double? expertLevel = _getSongLevel(song, 2);
|
||
|
||
double? targetLevel;
|
||
int targetLevelIndex = 3; // 默认优先推荐 Master
|
||
|
||
if (masterLevel != null) {
|
||
targetLevel = masterLevel;
|
||
} else if (expertLevel != null) {
|
||
targetLevel = expertLevel;
|
||
targetLevelIndex = 2;
|
||
} else {
|
||
continue; // 没有可用难度数据
|
||
}
|
||
|
||
// --- 检查是否已游玩 ---
|
||
bool isPlayed = false;
|
||
Map<String, dynamic>? bestChart;
|
||
|
||
// 尝试直接匹配 (SD 或 基础ID)
|
||
String keyDirect = "${song.id}_$targetLevelIndex";
|
||
if (playedMap.containsKey(keyDirect)) {
|
||
isPlayed = true;
|
||
bestChart = playedMap[keyDirect];
|
||
}
|
||
// 尝试匹配 DX 偏移 (DX 歌曲 ID 通常 +10000)
|
||
else {
|
||
int dxId = song.id + 10000;
|
||
String keyDx = "${dxId}_$targetLevelIndex";
|
||
if (playedMap.containsKey(keyDx)) {
|
||
isPlayed = true;
|
||
bestChart = playedMap[keyDx];
|
||
}
|
||
}
|
||
|
||
if (isPlayed && bestChart != null) {
|
||
// --- 策略 A: 推分逻辑 (Improvement) ---
|
||
int currentAch = bestChart['achievement'] as int;
|
||
|
||
// 如果已经 AP+ (100.5%),跳过
|
||
if (currentAch >= 1005000) continue;
|
||
|
||
// 计算下一个档位的目标达成率
|
||
double nextTargetAch = 0;
|
||
const List<double> targets = [97.0, 98.0, 99.0, 99.5, 100.0, 100.5];
|
||
|
||
double currentPercent = currentAch / 10000.0;
|
||
for (double t in targets) {
|
||
if (currentPercent < t) {
|
||
nextTargetAch = t;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (nextTargetAch == 0) continue;
|
||
|
||
// 计算目标 Rating
|
||
int targetRating = _calculateRating(targetLevel!, nextTargetAch);
|
||
|
||
// 核心判断:如果推分后的 Rating 大于当前 B35 最低分,则值得推荐
|
||
if (targetRating > b35MinRating) {
|
||
candidatesForImprovement.add(song);
|
||
}
|
||
|
||
} else {
|
||
// --- 策略 B: 新曲逻辑 (New) ---
|
||
// 判断定数是否在用户能力范围内
|
||
if (targetLevel != null && targetLevel >= minRecommendedLevel && targetLevel <= 15.0) {
|
||
candidatesNew.add(song);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. 混合推荐结果
|
||
List<SongModel> result = [];
|
||
|
||
// 60% 来自“提升空间大”的曲子
|
||
candidatesForImprovement.shuffle(random);
|
||
int improveCount = (count * 0.6).round();
|
||
for (int i = 0; i < improveCount && i < candidatesForImprovement.length; i++) {
|
||
final song = candidatesForImprovement[i];
|
||
result.add(song);
|
||
addedSongIds.add(song.id);
|
||
}
|
||
|
||
// 40% 来自“新曲/潜力曲”
|
||
candidatesNew.shuffle(random);
|
||
int remaining = count - result.length;
|
||
for (int i = 0; i < remaining && i < candidatesNew.length; i++) {
|
||
final song = candidatesNew[i];
|
||
// 避免重复添加(虽然逻辑上 New 和 Improve 不重叠,但防万一)
|
||
if (!addedSongIds.contains(song.id)) {
|
||
result.add(song);
|
||
addedSongIds.add(song.id);
|
||
}
|
||
}
|
||
|
||
// 如果不够,用纯随机补齐
|
||
if (result.length < count) {
|
||
// 复制一份并打乱,避免修改原数组
|
||
List<SongModel> fallback = List.from(allSongs);
|
||
fallback.shuffle(random);
|
||
|
||
for (var s in fallback) {
|
||
if (result.length >= count) break;
|
||
// 使用 Set 进行 O(1) 查找,避免 List.contains 的 O(N) 查找
|
||
if (!addedSongIds.contains(s.id)) {
|
||
result.add(s);
|
||
addedSongIds.add(s.id);
|
||
}
|
||
}
|
||
}
|
||
|
||
return result.take(count).toList();
|
||
}
|
||
|
||
/// 辅助:获取歌曲指定难度的定数 (Level Value)
|
||
static double? _getSongLevel(SongModel song, int levelIndex) {
|
||
// levelIndex: 0=Basic, 1=Advanced, 2=Expert, 3=Master, 4=Re:Master
|
||
|
||
// 1. 尝试从 SD (Standard) 获取
|
||
if (song.sd != null && song.sd is Map) {
|
||
final sdMap = song.sd as Map;
|
||
// 键可能是字符串 "3" 或整数 3,这里做兼容
|
||
var data = sdMap["$levelIndex"] ?? sdMap[levelIndex];
|
||
if (data is Map && data.containsKey("level_value")) {
|
||
final val = data["level_value"];
|
||
if (val is num) return val.toDouble();
|
||
}
|
||
}
|
||
|
||
// 2. 尝试从 DX 获取
|
||
if (song.dx != null && song.dx is Map) {
|
||
final dxMap = song.dx as Map;
|
||
var data = dxMap["$levelIndex"] ?? dxMap[levelIndex];
|
||
if (data is Map && data.containsKey("level_value")) {
|
||
final val = data["level_value"];
|
||
if (val is num) return val.toDouble();
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// 辅助:根据定数和达成率计算 Rating (完全复刻 Java getRatingChart)
|
||
static int _calculateRating(double diff, double achievementPercent) {
|
||
double sys = 22.4;
|
||
double ach = achievementPercent; // 例如 99.5
|
||
|
||
if (ach >= 100.5000) {
|
||
return (diff * 22.512).floor();
|
||
}
|
||
if (ach == 100.4999) {
|
||
sys = 22.2;
|
||
} else if (ach >= 100.0000) {
|
||
sys = 21.6;
|
||
} else if (ach == 99.9999) {
|
||
sys = 21.4;
|
||
} else if (ach >= 99.5000) {
|
||
sys = 21.1;
|
||
} else if (ach >= 99.0000) {
|
||
sys = 20.8;
|
||
} else if (ach >= 98.0000) {
|
||
sys = 20.3;
|
||
} else if (ach >= 97.0000) {
|
||
sys = 20.0;
|
||
} else if (ach >= 94.0000) {
|
||
sys = 16.8;
|
||
} else if (ach >= 90.0000) {
|
||
sys = 15.2;
|
||
} else if (ach >= 80.0000) {
|
||
sys = 13.6;
|
||
} else if (ach >= 75.0000) {
|
||
sys = 12.0;
|
||
} else if (ach >= 70.0000) {
|
||
sys = 11.2;
|
||
} else if (ach >= 60.0000) {
|
||
sys = 9.6;
|
||
} else if (ach >= 50.0000) {
|
||
sys = 8.0;
|
||
} else {
|
||
sys = 0.0;
|
||
}
|
||
|
||
if (sys == 0.0) return 0;
|
||
|
||
// Java: (int) (diff * sys * achievement / 100)
|
||
return (diff * sys * ach / 100).floor();
|
||
}
|
||
} |