feat(statistics): 게임 통계 시스템 추가

- GameStatistics 모델 (전투, 퀘스트, 아이템 통계)
- StatisticsStorage 영구 저장
- StatisticsDialog UI
This commit is contained in:
JiWoong Sul
2025-12-30 15:58:22 +09:00
parent 80b6cd63e3
commit d64b9654a3
3 changed files with 1457 additions and 0 deletions

View 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,
);
}
}

View 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;
}
}
}

View 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();
}