feat(statistics): 게임 통계 시스템 추가
- GameStatistics 모델 (전투, 퀘스트, 아이템 통계) - StatisticsStorage 영구 저장 - StatisticsDialog UI
This commit is contained in:
626
lib/src/core/model/game_statistics.dart
Normal file
626
lib/src/core/model/game_statistics.dart
Normal file
@@ -0,0 +1,626 @@
|
||||
/// 게임 통계 (Game Statistics)
|
||||
///
|
||||
/// 세션 및 누적 통계를 추적하는 모델
|
||||
class GameStatistics {
|
||||
const GameStatistics({
|
||||
required this.session,
|
||||
required this.cumulative,
|
||||
});
|
||||
|
||||
/// 현재 세션 통계
|
||||
final SessionStatistics session;
|
||||
|
||||
/// 누적 통계
|
||||
final CumulativeStatistics cumulative;
|
||||
|
||||
/// 빈 통계
|
||||
factory GameStatistics.empty() => GameStatistics(
|
||||
session: SessionStatistics.empty(),
|
||||
cumulative: CumulativeStatistics.empty(),
|
||||
);
|
||||
|
||||
/// 새 세션 시작 (세션 통계 초기화, 누적 통계 유지)
|
||||
GameStatistics startNewSession() {
|
||||
return GameStatistics(
|
||||
session: SessionStatistics.empty(),
|
||||
cumulative: cumulative,
|
||||
);
|
||||
}
|
||||
|
||||
/// 세션 종료 시 누적 통계 업데이트
|
||||
GameStatistics endSession() {
|
||||
return GameStatistics(
|
||||
session: session,
|
||||
cumulative: cumulative.mergeSession(session),
|
||||
);
|
||||
}
|
||||
|
||||
GameStatistics copyWith({
|
||||
SessionStatistics? session,
|
||||
CumulativeStatistics? cumulative,
|
||||
}) {
|
||||
return GameStatistics(
|
||||
session: session ?? this.session,
|
||||
cumulative: cumulative ?? this.cumulative,
|
||||
);
|
||||
}
|
||||
|
||||
/// JSON 직렬화
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'session': session.toJson(),
|
||||
'cumulative': cumulative.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
/// JSON 역직렬화
|
||||
factory GameStatistics.fromJson(Map<String, dynamic> json) {
|
||||
return GameStatistics(
|
||||
session: SessionStatistics.fromJson(
|
||||
json['session'] as Map<String, dynamic>? ?? {},
|
||||
),
|
||||
cumulative: CumulativeStatistics.fromJson(
|
||||
json['cumulative'] as Map<String, dynamic>? ?? {},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 세션 통계 (Session Statistics)
|
||||
///
|
||||
/// 현재 게임 세션의 통계
|
||||
class SessionStatistics {
|
||||
const SessionStatistics({
|
||||
required this.playTimeMs,
|
||||
required this.monstersKilled,
|
||||
required this.goldEarned,
|
||||
required this.goldSpent,
|
||||
required this.skillsUsed,
|
||||
required this.criticalHits,
|
||||
required this.maxCriticalStreak,
|
||||
required this.currentCriticalStreak,
|
||||
required this.totalDamageDealt,
|
||||
required this.totalDamageTaken,
|
||||
required this.potionsUsed,
|
||||
required this.itemsSold,
|
||||
required this.questsCompleted,
|
||||
required this.deathCount,
|
||||
required this.bossesDefeated,
|
||||
required this.levelUps,
|
||||
});
|
||||
|
||||
/// 플레이 시간 (밀리초)
|
||||
final int playTimeMs;
|
||||
|
||||
/// 처치한 몬스터 수
|
||||
final int monstersKilled;
|
||||
|
||||
/// 획득한 골드 총량
|
||||
final int goldEarned;
|
||||
|
||||
/// 소비한 골드 총량
|
||||
final int goldSpent;
|
||||
|
||||
/// 사용한 스킬 횟수
|
||||
final int skillsUsed;
|
||||
|
||||
/// 크리티컬 히트 횟수
|
||||
final int criticalHits;
|
||||
|
||||
/// 최대 연속 크리티컬
|
||||
final int maxCriticalStreak;
|
||||
|
||||
/// 현재 연속 크리티컬 (내부 추적용)
|
||||
final int currentCriticalStreak;
|
||||
|
||||
/// 총 입힌 데미지
|
||||
final int totalDamageDealt;
|
||||
|
||||
/// 총 받은 데미지
|
||||
final int totalDamageTaken;
|
||||
|
||||
/// 사용한 물약 수
|
||||
final int potionsUsed;
|
||||
|
||||
/// 판매한 아이템 수
|
||||
final int itemsSold;
|
||||
|
||||
/// 완료한 퀘스트 수
|
||||
final int questsCompleted;
|
||||
|
||||
/// 사망 횟수
|
||||
final int deathCount;
|
||||
|
||||
/// 처치한 보스 수
|
||||
final int bossesDefeated;
|
||||
|
||||
/// 레벨업 횟수
|
||||
final int levelUps;
|
||||
|
||||
/// 플레이 시간 Duration
|
||||
Duration get playTime => Duration(milliseconds: playTimeMs);
|
||||
|
||||
/// 플레이 시간 포맷 (HH:MM:SS)
|
||||
String get formattedPlayTime {
|
||||
final hours = playTime.inHours;
|
||||
final minutes = playTime.inMinutes % 60;
|
||||
final seconds = playTime.inSeconds % 60;
|
||||
return '${hours.toString().padLeft(2, '0')}:'
|
||||
'${minutes.toString().padLeft(2, '0')}:'
|
||||
'${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
/// 평균 DPS (damage per second)
|
||||
double get averageDps {
|
||||
if (playTimeMs <= 0) return 0;
|
||||
return totalDamageDealt / (playTimeMs / 1000);
|
||||
}
|
||||
|
||||
/// 킬당 평균 골드
|
||||
double get goldPerKill {
|
||||
if (monstersKilled <= 0) return 0;
|
||||
return goldEarned / monstersKilled;
|
||||
}
|
||||
|
||||
/// 크리티컬 비율
|
||||
double get criticalRate {
|
||||
if (skillsUsed <= 0) return 0;
|
||||
return criticalHits / skillsUsed;
|
||||
}
|
||||
|
||||
/// 빈 세션 통계
|
||||
factory SessionStatistics.empty() => const SessionStatistics(
|
||||
playTimeMs: 0,
|
||||
monstersKilled: 0,
|
||||
goldEarned: 0,
|
||||
goldSpent: 0,
|
||||
skillsUsed: 0,
|
||||
criticalHits: 0,
|
||||
maxCriticalStreak: 0,
|
||||
currentCriticalStreak: 0,
|
||||
totalDamageDealt: 0,
|
||||
totalDamageTaken: 0,
|
||||
potionsUsed: 0,
|
||||
itemsSold: 0,
|
||||
questsCompleted: 0,
|
||||
deathCount: 0,
|
||||
bossesDefeated: 0,
|
||||
levelUps: 0,
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// 이벤트 기록 메서드
|
||||
// ============================================================================
|
||||
|
||||
/// 몬스터 처치 기록
|
||||
SessionStatistics recordKill({bool isBoss = false}) {
|
||||
return copyWith(
|
||||
monstersKilled: monstersKilled + 1,
|
||||
bossesDefeated: isBoss ? bossesDefeated + 1 : bossesDefeated,
|
||||
);
|
||||
}
|
||||
|
||||
/// 골드 획득 기록
|
||||
SessionStatistics recordGoldEarned(int amount) {
|
||||
return copyWith(goldEarned: goldEarned + amount);
|
||||
}
|
||||
|
||||
/// 골드 소비 기록
|
||||
SessionStatistics recordGoldSpent(int amount) {
|
||||
return copyWith(goldSpent: goldSpent + amount);
|
||||
}
|
||||
|
||||
/// 스킬 사용 기록
|
||||
SessionStatistics recordSkillUse({required bool isCritical}) {
|
||||
final newCriticalStreak =
|
||||
isCritical ? currentCriticalStreak + 1 : 0;
|
||||
final newMaxStreak = newCriticalStreak > maxCriticalStreak
|
||||
? newCriticalStreak
|
||||
: maxCriticalStreak;
|
||||
|
||||
return copyWith(
|
||||
skillsUsed: skillsUsed + 1,
|
||||
criticalHits: isCritical ? criticalHits + 1 : criticalHits,
|
||||
currentCriticalStreak: newCriticalStreak,
|
||||
maxCriticalStreak: newMaxStreak,
|
||||
);
|
||||
}
|
||||
|
||||
/// 데미지 기록
|
||||
SessionStatistics recordDamage({
|
||||
int dealt = 0,
|
||||
int taken = 0,
|
||||
}) {
|
||||
return copyWith(
|
||||
totalDamageDealt: totalDamageDealt + dealt,
|
||||
totalDamageTaken: totalDamageTaken + taken,
|
||||
);
|
||||
}
|
||||
|
||||
/// 물약 사용 기록
|
||||
SessionStatistics recordPotionUse() {
|
||||
return copyWith(potionsUsed: potionsUsed + 1);
|
||||
}
|
||||
|
||||
/// 아이템 판매 기록
|
||||
SessionStatistics recordItemSold(int count) {
|
||||
return copyWith(itemsSold: itemsSold + count);
|
||||
}
|
||||
|
||||
/// 퀘스트 완료 기록
|
||||
SessionStatistics recordQuestComplete() {
|
||||
return copyWith(questsCompleted: questsCompleted + 1);
|
||||
}
|
||||
|
||||
/// 사망 기록
|
||||
SessionStatistics recordDeath() {
|
||||
return copyWith(deathCount: deathCount + 1);
|
||||
}
|
||||
|
||||
/// 레벨업 기록
|
||||
SessionStatistics recordLevelUp() {
|
||||
return copyWith(levelUps: levelUps + 1);
|
||||
}
|
||||
|
||||
/// 플레이 시간 업데이트
|
||||
SessionStatistics updatePlayTime(int elapsedMs) {
|
||||
return copyWith(playTimeMs: elapsedMs);
|
||||
}
|
||||
|
||||
SessionStatistics copyWith({
|
||||
int? playTimeMs,
|
||||
int? monstersKilled,
|
||||
int? goldEarned,
|
||||
int? goldSpent,
|
||||
int? skillsUsed,
|
||||
int? criticalHits,
|
||||
int? maxCriticalStreak,
|
||||
int? currentCriticalStreak,
|
||||
int? totalDamageDealt,
|
||||
int? totalDamageTaken,
|
||||
int? potionsUsed,
|
||||
int? itemsSold,
|
||||
int? questsCompleted,
|
||||
int? deathCount,
|
||||
int? bossesDefeated,
|
||||
int? levelUps,
|
||||
}) {
|
||||
return SessionStatistics(
|
||||
playTimeMs: playTimeMs ?? this.playTimeMs,
|
||||
monstersKilled: monstersKilled ?? this.monstersKilled,
|
||||
goldEarned: goldEarned ?? this.goldEarned,
|
||||
goldSpent: goldSpent ?? this.goldSpent,
|
||||
skillsUsed: skillsUsed ?? this.skillsUsed,
|
||||
criticalHits: criticalHits ?? this.criticalHits,
|
||||
maxCriticalStreak: maxCriticalStreak ?? this.maxCriticalStreak,
|
||||
currentCriticalStreak:
|
||||
currentCriticalStreak ?? this.currentCriticalStreak,
|
||||
totalDamageDealt: totalDamageDealt ?? this.totalDamageDealt,
|
||||
totalDamageTaken: totalDamageTaken ?? this.totalDamageTaken,
|
||||
potionsUsed: potionsUsed ?? this.potionsUsed,
|
||||
itemsSold: itemsSold ?? this.itemsSold,
|
||||
questsCompleted: questsCompleted ?? this.questsCompleted,
|
||||
deathCount: deathCount ?? this.deathCount,
|
||||
bossesDefeated: bossesDefeated ?? this.bossesDefeated,
|
||||
levelUps: levelUps ?? this.levelUps,
|
||||
);
|
||||
}
|
||||
|
||||
/// JSON 직렬화
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'playTimeMs': playTimeMs,
|
||||
'monstersKilled': monstersKilled,
|
||||
'goldEarned': goldEarned,
|
||||
'goldSpent': goldSpent,
|
||||
'skillsUsed': skillsUsed,
|
||||
'criticalHits': criticalHits,
|
||||
'maxCriticalStreak': maxCriticalStreak,
|
||||
'totalDamageDealt': totalDamageDealt,
|
||||
'totalDamageTaken': totalDamageTaken,
|
||||
'potionsUsed': potionsUsed,
|
||||
'itemsSold': itemsSold,
|
||||
'questsCompleted': questsCompleted,
|
||||
'deathCount': deathCount,
|
||||
'bossesDefeated': bossesDefeated,
|
||||
'levelUps': levelUps,
|
||||
};
|
||||
}
|
||||
|
||||
/// JSON 역직렬화
|
||||
factory SessionStatistics.fromJson(Map<String, dynamic> json) {
|
||||
return SessionStatistics(
|
||||
playTimeMs: json['playTimeMs'] as int? ?? 0,
|
||||
monstersKilled: json['monstersKilled'] as int? ?? 0,
|
||||
goldEarned: json['goldEarned'] as int? ?? 0,
|
||||
goldSpent: json['goldSpent'] as int? ?? 0,
|
||||
skillsUsed: json['skillsUsed'] as int? ?? 0,
|
||||
criticalHits: json['criticalHits'] as int? ?? 0,
|
||||
maxCriticalStreak: json['maxCriticalStreak'] as int? ?? 0,
|
||||
currentCriticalStreak: 0, // 세션간 유지 안 함
|
||||
totalDamageDealt: json['totalDamageDealt'] as int? ?? 0,
|
||||
totalDamageTaken: json['totalDamageTaken'] as int? ?? 0,
|
||||
potionsUsed: json['potionsUsed'] as int? ?? 0,
|
||||
itemsSold: json['itemsSold'] as int? ?? 0,
|
||||
questsCompleted: json['questsCompleted'] as int? ?? 0,
|
||||
deathCount: json['deathCount'] as int? ?? 0,
|
||||
bossesDefeated: json['bossesDefeated'] as int? ?? 0,
|
||||
levelUps: json['levelUps'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 누적 통계 (Cumulative Statistics)
|
||||
///
|
||||
/// 모든 게임 세션의 누적 통계
|
||||
class CumulativeStatistics {
|
||||
const CumulativeStatistics({
|
||||
required this.totalPlayTimeMs,
|
||||
required this.totalMonstersKilled,
|
||||
required this.totalGoldEarned,
|
||||
required this.totalGoldSpent,
|
||||
required this.totalSkillsUsed,
|
||||
required this.totalCriticalHits,
|
||||
required this.bestCriticalStreak,
|
||||
required this.totalDamageDealt,
|
||||
required this.totalDamageTaken,
|
||||
required this.totalPotionsUsed,
|
||||
required this.totalItemsSold,
|
||||
required this.totalQuestsCompleted,
|
||||
required this.totalDeaths,
|
||||
required this.totalBossesDefeated,
|
||||
required this.totalLevelUps,
|
||||
required this.highestLevel,
|
||||
required this.highestGoldHeld,
|
||||
required this.gamesCompleted,
|
||||
required this.gamesStarted,
|
||||
});
|
||||
|
||||
/// 총 플레이 시간 (밀리초)
|
||||
final int totalPlayTimeMs;
|
||||
|
||||
/// 총 처치한 몬스터 수
|
||||
final int totalMonstersKilled;
|
||||
|
||||
/// 총 획득한 골드
|
||||
final int totalGoldEarned;
|
||||
|
||||
/// 총 소비한 골드
|
||||
final int totalGoldSpent;
|
||||
|
||||
/// 총 스킬 사용 횟수
|
||||
final int totalSkillsUsed;
|
||||
|
||||
/// 총 크리티컬 히트 횟수
|
||||
final int totalCriticalHits;
|
||||
|
||||
/// 최고 연속 크리티컬
|
||||
final int bestCriticalStreak;
|
||||
|
||||
/// 총 입힌 데미지
|
||||
final int totalDamageDealt;
|
||||
|
||||
/// 총 받은 데미지
|
||||
final int totalDamageTaken;
|
||||
|
||||
/// 총 사용한 물약 수
|
||||
final int totalPotionsUsed;
|
||||
|
||||
/// 총 판매한 아이템 수
|
||||
final int totalItemsSold;
|
||||
|
||||
/// 총 완료한 퀘스트 수
|
||||
final int totalQuestsCompleted;
|
||||
|
||||
/// 총 사망 횟수
|
||||
final int totalDeaths;
|
||||
|
||||
/// 총 처치한 보스 수
|
||||
final int totalBossesDefeated;
|
||||
|
||||
/// 총 레벨업 횟수
|
||||
final int totalLevelUps;
|
||||
|
||||
/// 최고 달성 레벨
|
||||
final int highestLevel;
|
||||
|
||||
/// 최대 보유 골드
|
||||
final int highestGoldHeld;
|
||||
|
||||
/// 클리어한 게임 수
|
||||
final int gamesCompleted;
|
||||
|
||||
/// 시작한 게임 수
|
||||
final int gamesStarted;
|
||||
|
||||
/// 총 플레이 시간 Duration
|
||||
Duration get totalPlayTime => Duration(milliseconds: totalPlayTimeMs);
|
||||
|
||||
/// 총 플레이 시간 포맷 (HH:MM:SS)
|
||||
String get formattedTotalPlayTime {
|
||||
final hours = totalPlayTime.inHours;
|
||||
final minutes = totalPlayTime.inMinutes % 60;
|
||||
final seconds = totalPlayTime.inSeconds % 60;
|
||||
return '${hours.toString().padLeft(2, '0')}:'
|
||||
'${minutes.toString().padLeft(2, '0')}:'
|
||||
'${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
/// 평균 게임당 플레이 시간
|
||||
Duration get averagePlayTimePerGame {
|
||||
if (gamesStarted <= 0) return Duration.zero;
|
||||
return Duration(milliseconds: totalPlayTimeMs ~/ gamesStarted);
|
||||
}
|
||||
|
||||
/// 게임 완료율
|
||||
double get completionRate {
|
||||
if (gamesStarted <= 0) return 0;
|
||||
return gamesCompleted / gamesStarted;
|
||||
}
|
||||
|
||||
/// 빈 누적 통계
|
||||
factory CumulativeStatistics.empty() => const CumulativeStatistics(
|
||||
totalPlayTimeMs: 0,
|
||||
totalMonstersKilled: 0,
|
||||
totalGoldEarned: 0,
|
||||
totalGoldSpent: 0,
|
||||
totalSkillsUsed: 0,
|
||||
totalCriticalHits: 0,
|
||||
bestCriticalStreak: 0,
|
||||
totalDamageDealt: 0,
|
||||
totalDamageTaken: 0,
|
||||
totalPotionsUsed: 0,
|
||||
totalItemsSold: 0,
|
||||
totalQuestsCompleted: 0,
|
||||
totalDeaths: 0,
|
||||
totalBossesDefeated: 0,
|
||||
totalLevelUps: 0,
|
||||
highestLevel: 0,
|
||||
highestGoldHeld: 0,
|
||||
gamesCompleted: 0,
|
||||
gamesStarted: 0,
|
||||
);
|
||||
|
||||
/// 세션 통계 병합
|
||||
CumulativeStatistics mergeSession(SessionStatistics session) {
|
||||
return CumulativeStatistics(
|
||||
totalPlayTimeMs: totalPlayTimeMs + session.playTimeMs,
|
||||
totalMonstersKilled: totalMonstersKilled + session.monstersKilled,
|
||||
totalGoldEarned: totalGoldEarned + session.goldEarned,
|
||||
totalGoldSpent: totalGoldSpent + session.goldSpent,
|
||||
totalSkillsUsed: totalSkillsUsed + session.skillsUsed,
|
||||
totalCriticalHits: totalCriticalHits + session.criticalHits,
|
||||
bestCriticalStreak: session.maxCriticalStreak > bestCriticalStreak
|
||||
? session.maxCriticalStreak
|
||||
: bestCriticalStreak,
|
||||
totalDamageDealt: totalDamageDealt + session.totalDamageDealt,
|
||||
totalDamageTaken: totalDamageTaken + session.totalDamageTaken,
|
||||
totalPotionsUsed: totalPotionsUsed + session.potionsUsed,
|
||||
totalItemsSold: totalItemsSold + session.itemsSold,
|
||||
totalQuestsCompleted: totalQuestsCompleted + session.questsCompleted,
|
||||
totalDeaths: totalDeaths + session.deathCount,
|
||||
totalBossesDefeated: totalBossesDefeated + session.bossesDefeated,
|
||||
totalLevelUps: totalLevelUps + session.levelUps,
|
||||
highestLevel: highestLevel, // 별도 업데이트 필요
|
||||
highestGoldHeld: highestGoldHeld, // 별도 업데이트 필요
|
||||
gamesCompleted: gamesCompleted, // 별도 업데이트 필요
|
||||
gamesStarted: gamesStarted, // 별도 업데이트 필요
|
||||
);
|
||||
}
|
||||
|
||||
/// 최고 레벨 업데이트
|
||||
CumulativeStatistics updateHighestLevel(int level) {
|
||||
if (level <= highestLevel) return this;
|
||||
return copyWith(highestLevel: level);
|
||||
}
|
||||
|
||||
/// 최대 골드 업데이트
|
||||
CumulativeStatistics updateHighestGold(int gold) {
|
||||
if (gold <= highestGoldHeld) return this;
|
||||
return copyWith(highestGoldHeld: gold);
|
||||
}
|
||||
|
||||
/// 새 게임 시작 기록
|
||||
CumulativeStatistics recordGameStart() {
|
||||
return copyWith(gamesStarted: gamesStarted + 1);
|
||||
}
|
||||
|
||||
/// 게임 클리어 기록
|
||||
CumulativeStatistics recordGameComplete() {
|
||||
return copyWith(gamesCompleted: gamesCompleted + 1);
|
||||
}
|
||||
|
||||
CumulativeStatistics copyWith({
|
||||
int? totalPlayTimeMs,
|
||||
int? totalMonstersKilled,
|
||||
int? totalGoldEarned,
|
||||
int? totalGoldSpent,
|
||||
int? totalSkillsUsed,
|
||||
int? totalCriticalHits,
|
||||
int? bestCriticalStreak,
|
||||
int? totalDamageDealt,
|
||||
int? totalDamageTaken,
|
||||
int? totalPotionsUsed,
|
||||
int? totalItemsSold,
|
||||
int? totalQuestsCompleted,
|
||||
int? totalDeaths,
|
||||
int? totalBossesDefeated,
|
||||
int? totalLevelUps,
|
||||
int? highestLevel,
|
||||
int? highestGoldHeld,
|
||||
int? gamesCompleted,
|
||||
int? gamesStarted,
|
||||
}) {
|
||||
return CumulativeStatistics(
|
||||
totalPlayTimeMs: totalPlayTimeMs ?? this.totalPlayTimeMs,
|
||||
totalMonstersKilled: totalMonstersKilled ?? this.totalMonstersKilled,
|
||||
totalGoldEarned: totalGoldEarned ?? this.totalGoldEarned,
|
||||
totalGoldSpent: totalGoldSpent ?? this.totalGoldSpent,
|
||||
totalSkillsUsed: totalSkillsUsed ?? this.totalSkillsUsed,
|
||||
totalCriticalHits: totalCriticalHits ?? this.totalCriticalHits,
|
||||
bestCriticalStreak: bestCriticalStreak ?? this.bestCriticalStreak,
|
||||
totalDamageDealt: totalDamageDealt ?? this.totalDamageDealt,
|
||||
totalDamageTaken: totalDamageTaken ?? this.totalDamageTaken,
|
||||
totalPotionsUsed: totalPotionsUsed ?? this.totalPotionsUsed,
|
||||
totalItemsSold: totalItemsSold ?? this.totalItemsSold,
|
||||
totalQuestsCompleted: totalQuestsCompleted ?? this.totalQuestsCompleted,
|
||||
totalDeaths: totalDeaths ?? this.totalDeaths,
|
||||
totalBossesDefeated: totalBossesDefeated ?? this.totalBossesDefeated,
|
||||
totalLevelUps: totalLevelUps ?? this.totalLevelUps,
|
||||
highestLevel: highestLevel ?? this.highestLevel,
|
||||
highestGoldHeld: highestGoldHeld ?? this.highestGoldHeld,
|
||||
gamesCompleted: gamesCompleted ?? this.gamesCompleted,
|
||||
gamesStarted: gamesStarted ?? this.gamesStarted,
|
||||
);
|
||||
}
|
||||
|
||||
/// JSON 직렬화
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'totalPlayTimeMs': totalPlayTimeMs,
|
||||
'totalMonstersKilled': totalMonstersKilled,
|
||||
'totalGoldEarned': totalGoldEarned,
|
||||
'totalGoldSpent': totalGoldSpent,
|
||||
'totalSkillsUsed': totalSkillsUsed,
|
||||
'totalCriticalHits': totalCriticalHits,
|
||||
'bestCriticalStreak': bestCriticalStreak,
|
||||
'totalDamageDealt': totalDamageDealt,
|
||||
'totalDamageTaken': totalDamageTaken,
|
||||
'totalPotionsUsed': totalPotionsUsed,
|
||||
'totalItemsSold': totalItemsSold,
|
||||
'totalQuestsCompleted': totalQuestsCompleted,
|
||||
'totalDeaths': totalDeaths,
|
||||
'totalBossesDefeated': totalBossesDefeated,
|
||||
'totalLevelUps': totalLevelUps,
|
||||
'highestLevel': highestLevel,
|
||||
'highestGoldHeld': highestGoldHeld,
|
||||
'gamesCompleted': gamesCompleted,
|
||||
'gamesStarted': gamesStarted,
|
||||
};
|
||||
}
|
||||
|
||||
/// JSON 역직렬화
|
||||
factory CumulativeStatistics.fromJson(Map<String, dynamic> json) {
|
||||
return CumulativeStatistics(
|
||||
totalPlayTimeMs: json['totalPlayTimeMs'] as int? ?? 0,
|
||||
totalMonstersKilled: json['totalMonstersKilled'] as int? ?? 0,
|
||||
totalGoldEarned: json['totalGoldEarned'] as int? ?? 0,
|
||||
totalGoldSpent: json['totalGoldSpent'] as int? ?? 0,
|
||||
totalSkillsUsed: json['totalSkillsUsed'] as int? ?? 0,
|
||||
totalCriticalHits: json['totalCriticalHits'] as int? ?? 0,
|
||||
bestCriticalStreak: json['bestCriticalStreak'] as int? ?? 0,
|
||||
totalDamageDealt: json['totalDamageDealt'] as int? ?? 0,
|
||||
totalDamageTaken: json['totalDamageTaken'] as int? ?? 0,
|
||||
totalPotionsUsed: json['totalPotionsUsed'] as int? ?? 0,
|
||||
totalItemsSold: json['totalItemsSold'] as int? ?? 0,
|
||||
totalQuestsCompleted: json['totalQuestsCompleted'] as int? ?? 0,
|
||||
totalDeaths: json['totalDeaths'] as int? ?? 0,
|
||||
totalBossesDefeated: json['totalBossesDefeated'] as int? ?? 0,
|
||||
totalLevelUps: json['totalLevelUps'] as int? ?? 0,
|
||||
highestLevel: json['highestLevel'] as int? ?? 0,
|
||||
highestGoldHeld: json['highestGoldHeld'] as int? ?? 0,
|
||||
gamesCompleted: json['gamesCompleted'] as int? ?? 0,
|
||||
gamesStarted: json['gamesStarted'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
126
lib/src/core/storage/statistics_storage.dart
Normal file
126
lib/src/core/storage/statistics_storage.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:askiineverdie/src/core/model/game_statistics.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
/// 게임 통계 저장소 (Statistics Storage)
|
||||
///
|
||||
/// 누적 통계 데이터 저장/로드 관리
|
||||
class StatisticsStorage {
|
||||
StatisticsStorage();
|
||||
|
||||
static const String _fileName = 'game_statistics.json';
|
||||
|
||||
Directory? _storageDir;
|
||||
|
||||
Future<Directory> _getStorageDir() async {
|
||||
if (_storageDir != null) return _storageDir!;
|
||||
_storageDir = await getApplicationSupportDirectory();
|
||||
return _storageDir!;
|
||||
}
|
||||
|
||||
File _getFile(Directory dir) {
|
||||
return File('${dir.path}/$_fileName');
|
||||
}
|
||||
|
||||
/// 누적 통계 로드
|
||||
Future<CumulativeStatistics> loadCumulative() async {
|
||||
try {
|
||||
final dir = await _getStorageDir();
|
||||
final file = _getFile(dir);
|
||||
|
||||
if (!await file.exists()) {
|
||||
return CumulativeStatistics.empty();
|
||||
}
|
||||
|
||||
final content = await file.readAsString();
|
||||
final json = jsonDecode(content) as Map<String, dynamic>;
|
||||
|
||||
// cumulative 키가 있으면 해당 데이터 사용
|
||||
if (json.containsKey('cumulative')) {
|
||||
return CumulativeStatistics.fromJson(
|
||||
json['cumulative'] as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
|
||||
// 직접 CumulativeStatistics JSON인 경우
|
||||
return CumulativeStatistics.fromJson(json);
|
||||
} catch (e) {
|
||||
// 파일이 없거나 손상된 경우 빈 통계 반환
|
||||
return CumulativeStatistics.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/// 누적 통계 저장
|
||||
Future<bool> saveCumulative(CumulativeStatistics stats) async {
|
||||
try {
|
||||
final dir = await _getStorageDir();
|
||||
final file = _getFile(dir);
|
||||
|
||||
final json = {
|
||||
'version': 1,
|
||||
'lastUpdated': DateTime.now().toIso8601String(),
|
||||
'cumulative': stats.toJson(),
|
||||
};
|
||||
|
||||
final content = const JsonEncoder.withIndent(' ').convert(json);
|
||||
await file.writeAsString(content);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 세션 종료 시 누적 통계 업데이트
|
||||
Future<bool> mergeSession(SessionStatistics session) async {
|
||||
final current = await loadCumulative();
|
||||
final updated = current.mergeSession(session);
|
||||
return saveCumulative(updated);
|
||||
}
|
||||
|
||||
/// 최고 레벨 업데이트
|
||||
Future<bool> updateHighestLevel(int level) async {
|
||||
final current = await loadCumulative();
|
||||
if (level <= current.highestLevel) return true;
|
||||
final updated = current.updateHighestLevel(level);
|
||||
return saveCumulative(updated);
|
||||
}
|
||||
|
||||
/// 최대 골드 업데이트
|
||||
Future<bool> updateHighestGold(int gold) async {
|
||||
final current = await loadCumulative();
|
||||
if (gold <= current.highestGoldHeld) return true;
|
||||
final updated = current.updateHighestGold(gold);
|
||||
return saveCumulative(updated);
|
||||
}
|
||||
|
||||
/// 새 게임 시작 기록
|
||||
Future<bool> recordGameStart() async {
|
||||
final current = await loadCumulative();
|
||||
final updated = current.recordGameStart();
|
||||
return saveCumulative(updated);
|
||||
}
|
||||
|
||||
/// 게임 클리어 기록
|
||||
Future<bool> recordGameComplete() async {
|
||||
final current = await loadCumulative();
|
||||
final updated = current.recordGameComplete();
|
||||
return saveCumulative(updated);
|
||||
}
|
||||
|
||||
/// 통계 초기화 (테스트용)
|
||||
Future<bool> clear() async {
|
||||
try {
|
||||
final dir = await _getStorageDir();
|
||||
final file = _getFile(dir);
|
||||
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
705
lib/src/features/game/widgets/statistics_dialog.dart
Normal file
705
lib/src/features/game/widgets/statistics_dialog.dart
Normal file
@@ -0,0 +1,705 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:askiineverdie/src/core/model/game_statistics.dart';
|
||||
|
||||
/// 게임 통계 다이얼로그 (Statistics Dialog)
|
||||
///
|
||||
/// 세션 통계와 누적 통계를 탭으로 표시
|
||||
class StatisticsDialog extends StatefulWidget {
|
||||
const StatisticsDialog({
|
||||
super.key,
|
||||
required this.session,
|
||||
required this.cumulative,
|
||||
});
|
||||
|
||||
final SessionStatistics session;
|
||||
final CumulativeStatistics cumulative;
|
||||
|
||||
/// 다이얼로그 표시
|
||||
static Future<void> show(
|
||||
BuildContext context, {
|
||||
required SessionStatistics session,
|
||||
required CumulativeStatistics cumulative,
|
||||
}) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (_) => StatisticsDialog(
|
||||
session: session,
|
||||
cumulative: cumulative,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<StatisticsDialog> createState() => _StatisticsDialogState();
|
||||
}
|
||||
|
||||
class _StatisticsDialogState extends State<StatisticsDialog>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final isKorean = Localizations.localeOf(context).languageCode == 'ko';
|
||||
final isJapanese = Localizations.localeOf(context).languageCode == 'ja';
|
||||
|
||||
return Dialog(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 400, maxHeight: 500),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 헤더
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(28),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.bar_chart,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
isKorean
|
||||
? '게임 통계'
|
||||
: isJapanese
|
||||
? 'ゲーム統計'
|
||||
: 'Game Statistics',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 탭 바
|
||||
TabBar(
|
||||
controller: _tabController,
|
||||
tabs: [
|
||||
Tab(
|
||||
text: isKorean
|
||||
? '현재 세션'
|
||||
: isJapanese
|
||||
? '現在のセッション'
|
||||
: 'Session',
|
||||
),
|
||||
Tab(
|
||||
text: isKorean
|
||||
? '누적 통계'
|
||||
: isJapanese
|
||||
? '累積統計'
|
||||
: 'Cumulative',
|
||||
),
|
||||
],
|
||||
),
|
||||
// 탭 내용
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_SessionStatisticsView(stats: widget.session),
|
||||
_CumulativeStatisticsView(stats: widget.cumulative),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 세션 통계 뷰
|
||||
class _SessionStatisticsView extends StatelessWidget {
|
||||
const _SessionStatisticsView({required this.stats});
|
||||
|
||||
final SessionStatistics stats;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isKorean = Localizations.localeOf(context).languageCode == 'ko';
|
||||
final isJapanese = Localizations.localeOf(context).languageCode == 'ja';
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_StatSection(
|
||||
title: isKorean
|
||||
? '전투'
|
||||
: isJapanese
|
||||
? '戦闘'
|
||||
: 'Combat',
|
||||
icon: Icons.sports_mma,
|
||||
items: [
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '플레이 시간'
|
||||
: isJapanese
|
||||
? 'プレイ時間'
|
||||
: 'Play Time',
|
||||
value: stats.formattedPlayTime,
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '처치한 몬스터'
|
||||
: isJapanese
|
||||
? '倒したモンスター'
|
||||
: 'Monsters Killed',
|
||||
value: _formatNumber(stats.monstersKilled),
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '보스 처치'
|
||||
: isJapanese
|
||||
? 'ボス討伐'
|
||||
: 'Bosses Defeated',
|
||||
value: _formatNumber(stats.bossesDefeated),
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '사망 횟수'
|
||||
: isJapanese
|
||||
? '死亡回数'
|
||||
: 'Deaths',
|
||||
value: _formatNumber(stats.deathCount),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_StatSection(
|
||||
title: isKorean
|
||||
? '데미지'
|
||||
: isJapanese
|
||||
? 'ダメージ'
|
||||
: 'Damage',
|
||||
icon: Icons.flash_on,
|
||||
items: [
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '입힌 데미지'
|
||||
: isJapanese
|
||||
? '与えたダメージ'
|
||||
: 'Damage Dealt',
|
||||
value: _formatNumber(stats.totalDamageDealt),
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '받은 데미지'
|
||||
: isJapanese
|
||||
? '受けたダメージ'
|
||||
: 'Damage Taken',
|
||||
value: _formatNumber(stats.totalDamageTaken),
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '평균 DPS'
|
||||
: isJapanese
|
||||
? '平均DPS'
|
||||
: 'Average DPS',
|
||||
value: stats.averageDps.toStringAsFixed(1),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_StatSection(
|
||||
title: isKorean
|
||||
? '스킬'
|
||||
: isJapanese
|
||||
? 'スキル'
|
||||
: 'Skills',
|
||||
icon: Icons.auto_awesome,
|
||||
items: [
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '스킬 사용'
|
||||
: isJapanese
|
||||
? 'スキル使用'
|
||||
: 'Skills Used',
|
||||
value: _formatNumber(stats.skillsUsed),
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '크리티컬 히트'
|
||||
: isJapanese
|
||||
? 'クリティカルヒット'
|
||||
: 'Critical Hits',
|
||||
value: _formatNumber(stats.criticalHits),
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '최대 연속 크리티컬'
|
||||
: isJapanese
|
||||
? '最大連続クリティカル'
|
||||
: 'Max Critical Streak',
|
||||
value: _formatNumber(stats.maxCriticalStreak),
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '크리티컬 비율'
|
||||
: isJapanese
|
||||
? 'クリティカル率'
|
||||
: 'Critical Rate',
|
||||
value: '${(stats.criticalRate * 100).toStringAsFixed(1)}%',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_StatSection(
|
||||
title: isKorean
|
||||
? '경제'
|
||||
: isJapanese
|
||||
? '経済'
|
||||
: 'Economy',
|
||||
icon: Icons.monetization_on,
|
||||
items: [
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '획득 골드'
|
||||
: isJapanese
|
||||
? '獲得ゴールド'
|
||||
: 'Gold Earned',
|
||||
value: _formatNumber(stats.goldEarned),
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '소비 골드'
|
||||
: isJapanese
|
||||
? '消費ゴールド'
|
||||
: 'Gold Spent',
|
||||
value: _formatNumber(stats.goldSpent),
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '판매 아이템'
|
||||
: isJapanese
|
||||
? '売却アイテム'
|
||||
: 'Items Sold',
|
||||
value: _formatNumber(stats.itemsSold),
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '물약 사용'
|
||||
: isJapanese
|
||||
? 'ポーション使用'
|
||||
: 'Potions Used',
|
||||
value: _formatNumber(stats.potionsUsed),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_StatSection(
|
||||
title: isKorean
|
||||
? '진행'
|
||||
: isJapanese
|
||||
? '進行'
|
||||
: 'Progress',
|
||||
icon: Icons.trending_up,
|
||||
items: [
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '레벨업'
|
||||
: isJapanese
|
||||
? 'レベルアップ'
|
||||
: 'Level Ups',
|
||||
value: _formatNumber(stats.levelUps),
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '완료한 퀘스트'
|
||||
: isJapanese
|
||||
? '完了したクエスト'
|
||||
: 'Quests Completed',
|
||||
value: _formatNumber(stats.questsCompleted),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 누적 통계 뷰
|
||||
class _CumulativeStatisticsView extends StatelessWidget {
|
||||
const _CumulativeStatisticsView({required this.stats});
|
||||
|
||||
final CumulativeStatistics stats;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isKorean = Localizations.localeOf(context).languageCode == 'ko';
|
||||
final isJapanese = Localizations.localeOf(context).languageCode == 'ja';
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_StatSection(
|
||||
title: isKorean
|
||||
? '기록'
|
||||
: isJapanese
|
||||
? '記録'
|
||||
: 'Records',
|
||||
icon: Icons.emoji_events,
|
||||
items: [
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '최고 레벨'
|
||||
: isJapanese
|
||||
? '最高レベル'
|
||||
: 'Highest Level',
|
||||
value: _formatNumber(stats.highestLevel),
|
||||
highlight: true,
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '최대 보유 골드'
|
||||
: isJapanese
|
||||
? '最大所持ゴールド'
|
||||
: 'Highest Gold Held',
|
||||
value: _formatNumber(stats.highestGoldHeld),
|
||||
highlight: true,
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '최고 연속 크리티컬'
|
||||
: isJapanese
|
||||
? '最高連続クリティカル'
|
||||
: 'Best Critical Streak',
|
||||
value: _formatNumber(stats.bestCriticalStreak),
|
||||
highlight: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_StatSection(
|
||||
title: isKorean
|
||||
? '총 플레이'
|
||||
: isJapanese
|
||||
? '総プレイ'
|
||||
: 'Total Play',
|
||||
icon: Icons.access_time,
|
||||
items: [
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '총 플레이 시간'
|
||||
: isJapanese
|
||||
? '総プレイ時間'
|
||||
: 'Total Play Time',
|
||||
value: stats.formattedTotalPlayTime,
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '시작한 게임'
|
||||
: isJapanese
|
||||
? '開始したゲーム'
|
||||
: 'Games Started',
|
||||
value: _formatNumber(stats.gamesStarted),
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '클리어한 게임'
|
||||
: isJapanese
|
||||
? 'クリアしたゲーム'
|
||||
: 'Games Completed',
|
||||
value: _formatNumber(stats.gamesCompleted),
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '클리어율'
|
||||
: isJapanese
|
||||
? 'クリア率'
|
||||
: 'Completion Rate',
|
||||
value: '${(stats.completionRate * 100).toStringAsFixed(1)}%',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_StatSection(
|
||||
title: isKorean
|
||||
? '총 전투'
|
||||
: isJapanese
|
||||
? '総戦闘'
|
||||
: 'Total Combat',
|
||||
icon: Icons.sports_mma,
|
||||
items: [
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '처치한 몬스터'
|
||||
: isJapanese
|
||||
? '倒したモンスター'
|
||||
: 'Monsters Killed',
|
||||
value: _formatNumber(stats.totalMonstersKilled),
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '보스 처치'
|
||||
: isJapanese
|
||||
? 'ボス討伐'
|
||||
: 'Bosses Defeated',
|
||||
value: _formatNumber(stats.totalBossesDefeated),
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '총 사망'
|
||||
: isJapanese
|
||||
? '総死亡'
|
||||
: 'Total Deaths',
|
||||
value: _formatNumber(stats.totalDeaths),
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '총 레벨업'
|
||||
: isJapanese
|
||||
? '総レベルアップ'
|
||||
: 'Total Level Ups',
|
||||
value: _formatNumber(stats.totalLevelUps),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_StatSection(
|
||||
title: isKorean
|
||||
? '총 데미지'
|
||||
: isJapanese
|
||||
? '総ダメージ'
|
||||
: 'Total Damage',
|
||||
icon: Icons.flash_on,
|
||||
items: [
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '입힌 데미지'
|
||||
: isJapanese
|
||||
? '与えたダメージ'
|
||||
: 'Damage Dealt',
|
||||
value: _formatNumber(stats.totalDamageDealt),
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '받은 데미지'
|
||||
: isJapanese
|
||||
? '受けたダメージ'
|
||||
: 'Damage Taken',
|
||||
value: _formatNumber(stats.totalDamageTaken),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_StatSection(
|
||||
title: isKorean
|
||||
? '총 스킬'
|
||||
: isJapanese
|
||||
? '総スキル'
|
||||
: 'Total Skills',
|
||||
icon: Icons.auto_awesome,
|
||||
items: [
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '스킬 사용'
|
||||
: isJapanese
|
||||
? 'スキル使用'
|
||||
: 'Skills Used',
|
||||
value: _formatNumber(stats.totalSkillsUsed),
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '크리티컬 히트'
|
||||
: isJapanese
|
||||
? 'クリティカルヒット'
|
||||
: 'Critical Hits',
|
||||
value: _formatNumber(stats.totalCriticalHits),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_StatSection(
|
||||
title: isKorean
|
||||
? '총 경제'
|
||||
: isJapanese
|
||||
? '総経済'
|
||||
: 'Total Economy',
|
||||
icon: Icons.monetization_on,
|
||||
items: [
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '획득 골드'
|
||||
: isJapanese
|
||||
? '獲得ゴールド'
|
||||
: 'Gold Earned',
|
||||
value: _formatNumber(stats.totalGoldEarned),
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '소비 골드'
|
||||
: isJapanese
|
||||
? '消費ゴールド'
|
||||
: 'Gold Spent',
|
||||
value: _formatNumber(stats.totalGoldSpent),
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '판매 아이템'
|
||||
: isJapanese
|
||||
? '売却アイテム'
|
||||
: 'Items Sold',
|
||||
value: _formatNumber(stats.totalItemsSold),
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '물약 사용'
|
||||
: isJapanese
|
||||
? 'ポーション使用'
|
||||
: 'Potions Used',
|
||||
value: _formatNumber(stats.totalPotionsUsed),
|
||||
),
|
||||
_StatItem(
|
||||
label: isKorean
|
||||
? '완료 퀘스트'
|
||||
: isJapanese
|
||||
? '完了クエスト'
|
||||
: 'Quests Completed',
|
||||
value: _formatNumber(stats.totalQuestsCompleted),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 통계 섹션 위젯
|
||||
class _StatSection extends StatelessWidget {
|
||||
const _StatSection({
|
||||
required this.title,
|
||||
required this.icon,
|
||||
required this.items,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final List<_StatItem> items;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 섹션 헤더
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, size: 18, color: theme.colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 8),
|
||||
// 통계 항목들
|
||||
...items,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 개별 통계 항목 위젯
|
||||
class _StatItem extends StatelessWidget {
|
||||
const _StatItem({
|
||||
required this.label,
|
||||
required this.value,
|
||||
this.highlight = false,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
final bool highlight;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: highlight
|
||||
? const EdgeInsets.symmetric(horizontal: 8, vertical: 2)
|
||||
: null,
|
||||
decoration: highlight
|
||||
? BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
)
|
||||
: null,
|
||||
child: Text(
|
||||
value,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'monospace',
|
||||
color: highlight
|
||||
? theme.colorScheme.onPrimaryContainer
|
||||
: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 숫자 포맷팅 (천 단위 콤마)
|
||||
String _formatNumber(int value) {
|
||||
if (value < 1000) return value.toString();
|
||||
|
||||
final result = StringBuffer();
|
||||
final str = value.toString();
|
||||
final length = str.length;
|
||||
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (i > 0 && (length - i) % 3 == 0) {
|
||||
result.write(',');
|
||||
}
|
||||
result.write(str[i]);
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
Reference in New Issue
Block a user