feat(hall-of-fame): 명예의 전당 상세 UI 및 전투 스탯 저장 추가
- CombatStats에 toJson/fromJson 직렬화 메서드 추가 - HallOfFameEntry에 finalStats(CombatStats) 필드 추가 - 명예의 전당 상세 다이얼로그에서 전투 스탯, 장비, 스펠 표시 - GameState에 combatStats 접근자 추가 - game_text_l10n에 명예의 전당 관련 텍스트 추가
This commit is contained in:
@@ -1254,6 +1254,42 @@ String get hofQuests {
|
|||||||
return 'Quests';
|
return 'Quests';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get hofStats {
|
||||||
|
if (isKoreanLocale) return '통계';
|
||||||
|
if (isJapaneseLocale) return '統計';
|
||||||
|
return 'Statistics';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get hofMonsters {
|
||||||
|
if (isKoreanLocale) return '몬스터';
|
||||||
|
if (isJapaneseLocale) return 'モンスター';
|
||||||
|
return 'Monsters';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get hofCleared {
|
||||||
|
if (isKoreanLocale) return '클리어';
|
||||||
|
if (isJapaneseLocale) return 'クリア';
|
||||||
|
return 'Cleared';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get hofSpells {
|
||||||
|
if (isKoreanLocale) return '스펠';
|
||||||
|
if (isJapaneseLocale) return 'スペル';
|
||||||
|
return 'Spells';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get hofCombatStats {
|
||||||
|
if (isKoreanLocale) return '전투 스탯';
|
||||||
|
if (isJapaneseLocale) return '戦闘ステータス';
|
||||||
|
return 'Combat Stats';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get buttonClose {
|
||||||
|
if (isKoreanLocale) return '닫기';
|
||||||
|
if (isJapaneseLocale) return '閉じる';
|
||||||
|
return 'Close';
|
||||||
|
}
|
||||||
|
|
||||||
String uiLevel(int level) {
|
String uiLevel(int level) {
|
||||||
if (isKoreanLocale) return 'Lv.$level';
|
if (isKoreanLocale) return 'Lv.$level';
|
||||||
if (isJapaneseLocale) return 'Lv.$level';
|
if (isJapaneseLocale) return 'Lv.$level';
|
||||||
|
|||||||
@@ -287,8 +287,11 @@ class ProgressService {
|
|||||||
progress = progress.copyWith(currentCombat: combatForReset);
|
progress = progress.copyWith(currentCombat: combatForReset);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 전투 상태 초기화 및 물약 사용 기록 초기화
|
// 전투 상태 초기화, 몬스터 처치 수 증가 및 물약 사용 기록 초기화
|
||||||
progress = progress.copyWith(currentCombat: null);
|
progress = progress.copyWith(
|
||||||
|
currentCombat: null,
|
||||||
|
monstersKilled: progress.monstersKilled + 1,
|
||||||
|
);
|
||||||
final resetPotionInventory = nextState.potionInventory.resetBattleUsage();
|
final resetPotionInventory = nextState.potionInventory.resetBattleUsage();
|
||||||
nextState = nextState.copyWith(
|
nextState = nextState.copyWith(
|
||||||
progress: progress,
|
progress: progress,
|
||||||
@@ -1331,8 +1334,11 @@ class ProgressService {
|
|||||||
lastCombatEvents: lastCombatEvents,
|
lastCombatEvents: lastCombatEvents,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 전투 상태 초기화
|
// 전투 상태 초기화 및 사망 횟수 증가
|
||||||
final progress = state.progress.copyWith(currentCombat: null);
|
final progress = state.progress.copyWith(
|
||||||
|
currentCombat: null,
|
||||||
|
deathCount: state.progress.deathCount + 1,
|
||||||
|
);
|
||||||
|
|
||||||
return state.copyWith(
|
return state.copyWith(
|
||||||
equipment: emptyEquipment,
|
equipment: emptyEquipment,
|
||||||
|
|||||||
@@ -391,6 +391,60 @@ class CombatStats {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// JSON으로 직렬화
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'str': str,
|
||||||
|
'con': con,
|
||||||
|
'dex': dex,
|
||||||
|
'intelligence': intelligence,
|
||||||
|
'wis': wis,
|
||||||
|
'cha': cha,
|
||||||
|
'atk': atk,
|
||||||
|
'def': def,
|
||||||
|
'magAtk': magAtk,
|
||||||
|
'magDef': magDef,
|
||||||
|
'criRate': criRate,
|
||||||
|
'criDamage': criDamage,
|
||||||
|
'evasion': evasion,
|
||||||
|
'accuracy': accuracy,
|
||||||
|
'blockRate': blockRate,
|
||||||
|
'parryRate': parryRate,
|
||||||
|
'attackDelayMs': attackDelayMs,
|
||||||
|
'hpMax': hpMax,
|
||||||
|
'hpCurrent': hpCurrent,
|
||||||
|
'mpMax': mpMax,
|
||||||
|
'mpCurrent': mpCurrent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// JSON에서 역직렬화
|
||||||
|
factory CombatStats.fromJson(Map<String, dynamic> json) {
|
||||||
|
return CombatStats(
|
||||||
|
str: json['str'] as int,
|
||||||
|
con: json['con'] as int,
|
||||||
|
dex: json['dex'] as int,
|
||||||
|
intelligence: json['intelligence'] as int,
|
||||||
|
wis: json['wis'] as int,
|
||||||
|
cha: json['cha'] as int,
|
||||||
|
atk: json['atk'] as int,
|
||||||
|
def: json['def'] as int,
|
||||||
|
magAtk: json['magAtk'] as int,
|
||||||
|
magDef: json['magDef'] as int,
|
||||||
|
criRate: (json['criRate'] as num).toDouble(),
|
||||||
|
criDamage: (json['criDamage'] as num).toDouble(),
|
||||||
|
evasion: (json['evasion'] as num).toDouble(),
|
||||||
|
accuracy: (json['accuracy'] as num).toDouble(),
|
||||||
|
blockRate: (json['blockRate'] as num).toDouble(),
|
||||||
|
parryRate: (json['parryRate'] as num).toDouble(),
|
||||||
|
attackDelayMs: json['attackDelayMs'] as int,
|
||||||
|
hpMax: json['hpMax'] as int,
|
||||||
|
hpCurrent: json['hpCurrent'] as int,
|
||||||
|
mpMax: json['mpMax'] as int,
|
||||||
|
mpCurrent: json['mpCurrent'] as int,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// 테스트/디버그용 기본값
|
/// 테스트/디버그용 기본값
|
||||||
factory CombatStats.empty() => const CombatStats(
|
factory CombatStats.empty() => const CombatStats(
|
||||||
str: 10,
|
str: 10,
|
||||||
|
|||||||
@@ -741,6 +741,8 @@ class ProgressState {
|
|||||||
this.questHistory = const [],
|
this.questHistory = const [],
|
||||||
this.currentQuestMonster,
|
this.currentQuestMonster,
|
||||||
this.currentCombat,
|
this.currentCombat,
|
||||||
|
this.monstersKilled = 0,
|
||||||
|
this.deathCount = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ProgressBarState task;
|
final ProgressBarState task;
|
||||||
@@ -764,6 +766,12 @@ class ProgressState {
|
|||||||
/// 현재 전투 상태 (킬 태스크 진행 중)
|
/// 현재 전투 상태 (킬 태스크 진행 중)
|
||||||
final CombatState? currentCombat;
|
final CombatState? currentCombat;
|
||||||
|
|
||||||
|
/// 처치한 몬스터 수
|
||||||
|
final int monstersKilled;
|
||||||
|
|
||||||
|
/// 사망 횟수
|
||||||
|
final int deathCount;
|
||||||
|
|
||||||
factory ProgressState.empty() => ProgressState(
|
factory ProgressState.empty() => ProgressState(
|
||||||
task: ProgressBarState.empty(),
|
task: ProgressBarState.empty(),
|
||||||
quest: ProgressBarState.empty(),
|
quest: ProgressBarState.empty(),
|
||||||
@@ -792,6 +800,8 @@ class ProgressState {
|
|||||||
List<HistoryEntry>? questHistory,
|
List<HistoryEntry>? questHistory,
|
||||||
QuestMonsterInfo? currentQuestMonster,
|
QuestMonsterInfo? currentQuestMonster,
|
||||||
CombatState? currentCombat,
|
CombatState? currentCombat,
|
||||||
|
int? monstersKilled,
|
||||||
|
int? deathCount,
|
||||||
}) {
|
}) {
|
||||||
return ProgressState(
|
return ProgressState(
|
||||||
task: task ?? this.task,
|
task: task ?? this.task,
|
||||||
@@ -806,6 +816,8 @@ class ProgressState {
|
|||||||
questHistory: questHistory ?? this.questHistory,
|
questHistory: questHistory ?? this.questHistory,
|
||||||
currentQuestMonster: currentQuestMonster ?? this.currentQuestMonster,
|
currentQuestMonster: currentQuestMonster ?? this.currentQuestMonster,
|
||||||
currentCombat: currentCombat ?? this.currentCombat,
|
currentCombat: currentCombat ?? this.currentCombat,
|
||||||
|
monstersKilled: monstersKilled ?? this.monstersKilled,
|
||||||
|
deathCount: deathCount ?? this.deathCount,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ class HallOfFameEntry {
|
|||||||
'monstersKilled': monstersKilled,
|
'monstersKilled': monstersKilled,
|
||||||
'questsCompleted': questsCompleted,
|
'questsCompleted': questsCompleted,
|
||||||
'clearedAt': clearedAt.toIso8601String(),
|
'clearedAt': clearedAt.toIso8601String(),
|
||||||
|
'finalStats': finalStats?.toJson(),
|
||||||
'finalEquipment': finalEquipment,
|
'finalEquipment': finalEquipment,
|
||||||
'finalSpells': finalSpells,
|
'finalSpells': finalSpells,
|
||||||
};
|
};
|
||||||
@@ -148,6 +149,9 @@ class HallOfFameEntry {
|
|||||||
monstersKilled: json['monstersKilled'] as int? ?? 0,
|
monstersKilled: json['monstersKilled'] as int? ?? 0,
|
||||||
questsCompleted: json['questsCompleted'] as int? ?? 0,
|
questsCompleted: json['questsCompleted'] as int? ?? 0,
|
||||||
clearedAt: DateTime.parse(json['clearedAt'] as String),
|
clearedAt: DateTime.parse(json['clearedAt'] as String),
|
||||||
|
finalStats: json['finalStats'] != null
|
||||||
|
? CombatStats.fromJson(json['finalStats'] as Map<String, dynamic>)
|
||||||
|
: null,
|
||||||
finalEquipment: json['finalEquipment'] != null
|
finalEquipment: json['finalEquipment'] != null
|
||||||
? Map<String, String>.from(json['finalEquipment'] as Map)
|
? Map<String, String>.from(json['finalEquipment'] as Map)
|
||||||
: null,
|
: null,
|
||||||
|
|||||||
@@ -117,6 +117,8 @@ class GameSave {
|
|||||||
'index': progress.currentQuestMonster!.monsterIndex,
|
'index': progress.currentQuestMonster!.monsterIndex,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
'monstersKilled': progress.monstersKilled,
|
||||||
|
'deathCount': progress.deathCount,
|
||||||
},
|
},
|
||||||
'queue': queue.entries
|
'queue': queue.entries
|
||||||
.map(
|
.map(
|
||||||
@@ -225,6 +227,8 @@ class GameSave {
|
|||||||
currentQuestMonster: _questMonsterFromJson(
|
currentQuestMonster: _questMonsterFromJson(
|
||||||
progressJson['questMonster'] as Map<String, dynamic>?,
|
progressJson['questMonster'] as Map<String, dynamic>?,
|
||||||
),
|
),
|
||||||
|
monstersKilled: progressJson['monstersKilled'] as int? ?? 0,
|
||||||
|
deathCount: progressJson['deathCount'] as int? ?? 0,
|
||||||
),
|
),
|
||||||
queue: QueueState(
|
queue: QueueState(
|
||||||
entries: Queue<QueueEntry>.from(
|
entries: Queue<QueueEntry>.from(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:askiineverdie/src/core/animation/ascii_animation_type.dart';
|
|||||||
import 'package:askiineverdie/src/core/engine/story_service.dart';
|
import 'package:askiineverdie/src/core/engine/story_service.dart';
|
||||||
import 'package:askiineverdie/src/core/model/combat_event.dart';
|
import 'package:askiineverdie/src/core/model/combat_event.dart';
|
||||||
import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart';
|
import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/combat_stats.dart';
|
||||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||||
import 'package:askiineverdie/src/core/model/hall_of_fame.dart';
|
import 'package:askiineverdie/src/core/model/hall_of_fame.dart';
|
||||||
import 'package:askiineverdie/src/core/model/skill.dart';
|
import 'package:askiineverdie/src/core/model/skill.dart';
|
||||||
@@ -291,11 +292,19 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
// 게임 일시 정지
|
// 게임 일시 정지
|
||||||
await widget.controller.pause(saveOnStop: true);
|
await widget.controller.pause(saveOnStop: true);
|
||||||
|
|
||||||
|
// 최종 전투 스탯 계산
|
||||||
|
final combatStats = CombatStats.fromStats(
|
||||||
|
stats: state.stats,
|
||||||
|
equipment: state.equipment,
|
||||||
|
level: state.traits.level,
|
||||||
|
);
|
||||||
|
|
||||||
// 명예의 전당 엔트리 생성
|
// 명예의 전당 엔트리 생성
|
||||||
final entry = HallOfFameEntry.fromGameState(
|
final entry = HallOfFameEntry.fromGameState(
|
||||||
state: state,
|
state: state,
|
||||||
totalDeaths: 0, // TODO: 사망 횟수 추적 구현 시 연결
|
totalDeaths: state.progress.deathCount,
|
||||||
monstersKilled: 0, // TODO: 처치 수 추적 구현 시 연결
|
monstersKilled: state.progress.monstersKilled,
|
||||||
|
combatStats: combatStats,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 명예의 전당에 저장
|
// 명예의 전당에 저장
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart';
|
import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/combat_stats.dart';
|
||||||
import 'package:askiineverdie/src/core/model/hall_of_fame.dart';
|
import 'package:askiineverdie/src/core/model/hall_of_fame.dart';
|
||||||
import 'package:askiineverdie/src/core/storage/hall_of_fame_storage.dart';
|
import 'package:askiineverdie/src/core/storage/hall_of_fame_storage.dart';
|
||||||
|
|
||||||
@@ -175,6 +176,29 @@ HallOfFameEntry _createDebugSampleEntry() {
|
|||||||
{'name': 'Null Pointer Strike', 'rank': 'IX'},
|
{'name': 'Null Pointer Strike', 'rank': 'IX'},
|
||||||
{'name': 'Thread Lock', 'rank': 'VII'},
|
{'name': 'Thread Lock', 'rank': 'VII'},
|
||||||
],
|
],
|
||||||
|
finalStats: const CombatStats(
|
||||||
|
str: 85,
|
||||||
|
con: 72,
|
||||||
|
dex: 68,
|
||||||
|
intelligence: 90,
|
||||||
|
wis: 65,
|
||||||
|
cha: 55,
|
||||||
|
atk: 450,
|
||||||
|
def: 280,
|
||||||
|
magAtk: 520,
|
||||||
|
magDef: 195,
|
||||||
|
criRate: 0.35,
|
||||||
|
criDamage: 2.2,
|
||||||
|
evasion: 0.18,
|
||||||
|
accuracy: 0.95,
|
||||||
|
blockRate: 0.25,
|
||||||
|
parryRate: 0.15,
|
||||||
|
attackDelayMs: 650,
|
||||||
|
hpMax: 2500,
|
||||||
|
hpCurrent: 2500,
|
||||||
|
mpMax: 1800,
|
||||||
|
mpCurrent: 1800,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,28 +544,37 @@ class _HallOfFameDetailDialog extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// 통계 섹션
|
// 통계 섹션 (Statistics Section)
|
||||||
_buildSection(
|
_buildSection(
|
||||||
icon: Icons.analytics,
|
icon: Icons.analytics,
|
||||||
title: 'Statistics',
|
title: l10n.hofStats,
|
||||||
child: _buildStatsGrid(),
|
child: _buildStatsGrid(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// 장비 섹션
|
// 전투 스탯 섹션 (Combat Stats Section)
|
||||||
if (entry.finalEquipment != null) ...[
|
if (entry.finalStats != null) ...[
|
||||||
_buildSection(
|
_buildSection(
|
||||||
icon: Icons.shield,
|
icon: Icons.sports_mma,
|
||||||
title: 'Equipment',
|
title: l10n.hofCombatStats,
|
||||||
child: _buildEquipmentList(),
|
child: _buildCombatStatsGrid(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
// 스펠 섹션
|
// 장비 섹션 (Equipment Section)
|
||||||
|
if (entry.finalEquipment != null) ...[
|
||||||
|
_buildSection(
|
||||||
|
icon: Icons.shield,
|
||||||
|
title: l10n.navEquipment,
|
||||||
|
child: _buildEquipmentList(context),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
// 스펠 섹션 (Spells Section)
|
||||||
if (entry.finalSpells != null && entry.finalSpells!.isNotEmpty)
|
if (entry.finalSpells != null && entry.finalSpells!.isNotEmpty)
|
||||||
_buildSection(
|
_buildSection(
|
||||||
icon: Icons.auto_fix_high,
|
icon: Icons.auto_fix_high,
|
||||||
title: 'Spells',
|
title: l10n.hofSpells,
|
||||||
child: _buildSpellList(),
|
child: _buildSpellList(context),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -550,7 +583,7 @@ class _HallOfFameDetailDialog extends StatelessWidget {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
child: const Text('Close'),
|
child: Text(l10n.buttonClose),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -592,7 +625,7 @@ class _HallOfFameDetailDialog extends StatelessWidget {
|
|||||||
_buildStatItem(Icons.timer, l10n.hofTime, entry.formattedPlayTime),
|
_buildStatItem(Icons.timer, l10n.hofTime, entry.formattedPlayTime),
|
||||||
_buildStatItem(
|
_buildStatItem(
|
||||||
Icons.pest_control,
|
Icons.pest_control,
|
||||||
'Monsters',
|
l10n.hofMonsters,
|
||||||
'${entry.monstersKilled}',
|
'${entry.monstersKilled}',
|
||||||
),
|
),
|
||||||
_buildStatItem(
|
_buildStatItem(
|
||||||
@@ -607,13 +640,109 @@ class _HallOfFameDetailDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
_buildStatItem(
|
_buildStatItem(
|
||||||
Icons.calendar_today,
|
Icons.calendar_today,
|
||||||
'Cleared',
|
l10n.hofCleared,
|
||||||
entry.formattedClearedDate,
|
entry.formattedClearedDate,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildCombatStatsGrid() {
|
||||||
|
final stats = entry.finalStats!;
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
// 기본 스탯 행 (Basic Stats Row)
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: [
|
||||||
|
_buildCombatStatChip(l10n.statStr, '${stats.str}', Colors.red),
|
||||||
|
_buildCombatStatChip(l10n.statCon, '${stats.con}', Colors.orange),
|
||||||
|
_buildCombatStatChip(l10n.statDex, '${stats.dex}', Colors.green),
|
||||||
|
_buildCombatStatChip(l10n.statInt, '${stats.intelligence}', Colors.blue),
|
||||||
|
_buildCombatStatChip(l10n.statWis, '${stats.wis}', Colors.purple),
|
||||||
|
_buildCombatStatChip(l10n.statCha, '${stats.cha}', Colors.pink),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Divider(height: 1),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
// 공격 스탯 행 (Attack Stats Row)
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: [
|
||||||
|
_buildCombatStatChip(l10n.statAtk, '${stats.atk}', Colors.red.shade700),
|
||||||
|
_buildCombatStatChip(l10n.statMAtk, '${stats.magAtk}', Colors.blue.shade700),
|
||||||
|
_buildCombatStatChip(
|
||||||
|
l10n.statCri,
|
||||||
|
'${(stats.criRate * 100).toStringAsFixed(1)}%',
|
||||||
|
Colors.amber.shade700,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
// 방어 스탯 행 (Defense Stats Row)
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: [
|
||||||
|
_buildCombatStatChip(l10n.statDef, '${stats.def}', Colors.brown),
|
||||||
|
_buildCombatStatChip(l10n.statMDef, '${stats.magDef}', Colors.indigo),
|
||||||
|
_buildCombatStatChip(
|
||||||
|
l10n.statEva,
|
||||||
|
'${(stats.evasion * 100).toStringAsFixed(1)}%',
|
||||||
|
Colors.teal,
|
||||||
|
),
|
||||||
|
_buildCombatStatChip(
|
||||||
|
l10n.statBlock,
|
||||||
|
'${(stats.blockRate * 100).toStringAsFixed(1)}%',
|
||||||
|
Colors.blueGrey,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
// HP/MP 행 (Resource Stats Row)
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: [
|
||||||
|
_buildCombatStatChip(l10n.statHp, '${stats.hpMax}', Colors.red.shade400),
|
||||||
|
_buildCombatStatChip(l10n.statMp, '${stats.mpMax}', Colors.blue.shade400),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCombatStatChip(String label, String value, Color color) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
border: Border.all(color: color.withValues(alpha: 0.3)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'$label ',
|
||||||
|
style: TextStyle(fontSize: 11, color: color),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildStatItem(IconData icon, String label, String value) {
|
Widget _buildStatItem(IconData icon, String label, String value) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
@@ -647,26 +776,31 @@ class _HallOfFameDetailDialog extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEquipmentList() {
|
Widget _buildEquipmentList(BuildContext context) {
|
||||||
final equipment = entry.finalEquipment!;
|
final equipment = entry.finalEquipment!;
|
||||||
|
// 슬롯 키, 아이콘, l10n 슬롯 이름
|
||||||
final slots = [
|
final slots = [
|
||||||
('weapon', Icons.gavel, 'Weapon'),
|
('weapon', Icons.gavel, l10n.slotWeapon, 0),
|
||||||
('shield', Icons.shield, 'Shield'),
|
('shield', Icons.shield, l10n.slotShield, 1),
|
||||||
('helm', Icons.sports_mma, 'Helm'),
|
('helm', Icons.sports_mma, l10n.slotHelm, 2),
|
||||||
('hauberk', Icons.checkroom, 'Hauberk'),
|
('hauberk', Icons.checkroom, l10n.slotHauberk, 2),
|
||||||
('brassairts', Icons.front_hand, 'Brassairts'),
|
('brassairts', Icons.front_hand, l10n.slotBrassairts, 2),
|
||||||
('vambraces', Icons.back_hand, 'Vambraces'),
|
('vambraces', Icons.back_hand, l10n.slotVambraces, 2),
|
||||||
('gauntlets', Icons.sports_handball, 'Gauntlets'),
|
('gauntlets', Icons.sports_handball, l10n.slotGauntlets, 2),
|
||||||
('gambeson', Icons.dry_cleaning, 'Gambeson'),
|
('gambeson', Icons.dry_cleaning, l10n.slotGambeson, 2),
|
||||||
('cuisses', Icons.airline_seat_legroom_normal, 'Cuisses'),
|
('cuisses', Icons.airline_seat_legroom_normal, l10n.slotCuisses, 2),
|
||||||
('greaves', Icons.snowshoeing, 'Greaves'),
|
('greaves', Icons.snowshoeing, l10n.slotGreaves, 2),
|
||||||
('sollerets', Icons.do_not_step, 'Sollerets'),
|
('sollerets', Icons.do_not_step, l10n.slotSollerets, 2),
|
||||||
];
|
];
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: slots.map((slot) {
|
children: slots.map((slot) {
|
||||||
final (key, icon, label) = slot;
|
final (key, icon, label, slotIndex) = slot;
|
||||||
final value = equipment[key] ?? '-';
|
final rawValue = equipment[key] ?? '';
|
||||||
|
// 장비 이름 번역 적용
|
||||||
|
final value = rawValue.isEmpty
|
||||||
|
? l10n.uiEmpty
|
||||||
|
: GameDataL10n.translateEquipString(context, rawValue, slotIndex);
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -694,7 +828,7 @@ class _HallOfFameDetailDialog extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSpellList() {
|
Widget _buildSpellList(BuildContext context) {
|
||||||
final spells = entry.finalSpells!;
|
final spells = entry.finalSpells!;
|
||||||
return Wrap(
|
return Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
@@ -702,6 +836,8 @@ class _HallOfFameDetailDialog extends StatelessWidget {
|
|||||||
children: spells.map((spell) {
|
children: spells.map((spell) {
|
||||||
final name = spell['name'] ?? '';
|
final name = spell['name'] ?? '';
|
||||||
final rank = spell['rank'] ?? '';
|
final rank = spell['rank'] ?? '';
|
||||||
|
// 스펠 이름 번역 적용
|
||||||
|
final translatedName = GameDataL10n.getSpellName(context, name);
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -710,7 +846,7 @@ class _HallOfFameDetailDialog extends StatelessWidget {
|
|||||||
border: Border.all(color: Colors.purple.shade200),
|
border: Border.all(color: Colors.purple.shade200),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'$name $rank',
|
'$translatedName $rank',
|
||||||
style: TextStyle(fontSize: 12, color: Colors.purple.shade700),
|
style: TextStyle(fontSize: 12, color: Colors.purple.shade700),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user