refactor(ui): 위젯 분리 및 화면 개선
- game_play_screen에서 desktop 패널 위젯 분리 - death_overlay에서 death_buttons, death_combat_log 분리 - mobile_carousel_layout에서 mobile_options_menu 분리 - 아레나 위젯 개선 (arena_hp_bar, result_panel 등) - settings_screen에서 retro_settings_widgets 분리 - 기타 위젯 리팩토링 및 import 경로 업데이트
This commit is contained in:
@@ -5,31 +5,24 @@ import 'package:flutter/scheduler.dart' show SchedulerBinding, SchedulerPhase;
|
||||
import 'package:flutter/services.dart' show KeyDownEvent, LogicalKeyboardKey;
|
||||
|
||||
import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n;
|
||||
import 'package:asciineverdie/data/skill_data.dart';
|
||||
import 'package:asciineverdie/src/core/engine/iap_service.dart';
|
||||
import 'package:asciineverdie/src/core/model/treasure_chest.dart';
|
||||
import 'package:asciineverdie/data/story_data.dart';
|
||||
import 'package:asciineverdie/l10n/app_localizations.dart';
|
||||
import 'package:asciineverdie/src/core/animation/ascii_animation_type.dart';
|
||||
import 'package:asciineverdie/src/shared/animation/ascii_animation_type.dart';
|
||||
import 'package:asciineverdie/src/core/engine/story_service.dart';
|
||||
import 'package:asciineverdie/src/core/l10n/game_data_l10n.dart';
|
||||
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||
import 'package:asciineverdie/src/core/model/skill.dart';
|
||||
import 'package:asciineverdie/src/core/notification/notification_service.dart';
|
||||
import 'package:asciineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
||||
import 'package:asciineverdie/src/features/game/game_session_controller.dart';
|
||||
import 'package:asciineverdie/src/features/hall_of_fame/hall_of_fame_screen.dart';
|
||||
import 'package:asciineverdie/src/features/game/widgets/cinematic_view.dart';
|
||||
import 'package:asciineverdie/src/features/game/widgets/combat_log.dart';
|
||||
import 'package:asciineverdie/src/features/game/widgets/death_overlay.dart';
|
||||
import 'package:asciineverdie/src/features/game/widgets/desktop_character_panel.dart';
|
||||
import 'package:asciineverdie/src/features/game/widgets/desktop_equipment_panel.dart';
|
||||
import 'package:asciineverdie/src/features/game/widgets/desktop_quest_panel.dart';
|
||||
import 'package:asciineverdie/src/features/game/widgets/victory_overlay.dart';
|
||||
import 'package:asciineverdie/src/features/game/widgets/hp_mp_bar.dart';
|
||||
import 'package:asciineverdie/src/features/game/widgets/notification_overlay.dart';
|
||||
import 'package:asciineverdie/src/features/game/widgets/stats_panel.dart';
|
||||
import 'package:asciineverdie/src/features/game/widgets/equipment_stats_panel.dart';
|
||||
import 'package:asciineverdie/src/features/game/widgets/potion_inventory_panel.dart';
|
||||
import 'package:asciineverdie/src/features/game/widgets/task_progress_panel.dart';
|
||||
import 'package:asciineverdie/src/features/game/widgets/active_buff_panel.dart';
|
||||
import 'package:asciineverdie/src/features/game/widgets/return_rewards_dialog.dart';
|
||||
import 'package:asciineverdie/src/features/game/layouts/mobile_carousel_layout.dart';
|
||||
import 'package:asciineverdie/src/features/settings/settings_screen.dart';
|
||||
@@ -796,9 +789,21 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(flex: 2, child: _buildCharacterPanel(state)),
|
||||
Expanded(flex: 3, child: _buildEquipmentPanel(state)),
|
||||
Expanded(flex: 2, child: _buildQuestPanel(state)),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: DesktopCharacterPanel(state: state),
|
||||
),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: DesktopEquipmentPanel(
|
||||
state: state,
|
||||
combatLogEntries: _combatLogController.entries,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: DesktopQuestPanel(state: state),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -871,667 +876,4 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
/// 좌측 패널: Character Sheet (Traits, Stats, Experience, Spells)
|
||||
Widget _buildCharacterPanel(GameState state) {
|
||||
final l10n = L10n.of(context);
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(4),
|
||||
color: RetroColors.panelBg,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: const BorderSide(color: RetroColors.panelBorderOuter, width: 2),
|
||||
borderRadius: BorderRadius.circular(0),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildPanelHeader(l10n.characterSheet),
|
||||
|
||||
// Traits 목록
|
||||
_buildSectionHeader(l10n.traits),
|
||||
_buildTraitsList(state),
|
||||
|
||||
// Stats 목록 (Phase 8: 애니메이션 변화 표시)
|
||||
_buildSectionHeader(l10n.stats),
|
||||
Expanded(flex: 2, child: StatsPanel(stats: state.stats)),
|
||||
|
||||
// Phase 8: HP/MP 바 (사망 위험 시 깜빡임, 전투 중 몬스터 HP 표시)
|
||||
// 전투 중에는 전투 스탯의 HP/MP 사용, 비전투 시 기본 스탯 사용
|
||||
HpMpBar(
|
||||
hpCurrent:
|
||||
state.progress.currentCombat?.playerStats.hpCurrent ??
|
||||
state.stats.hp,
|
||||
hpMax:
|
||||
state.progress.currentCombat?.playerStats.hpMax ??
|
||||
state.stats.hpMax,
|
||||
mpCurrent:
|
||||
state.progress.currentCombat?.playerStats.mpCurrent ??
|
||||
state.stats.mp,
|
||||
mpMax:
|
||||
state.progress.currentCombat?.playerStats.mpMax ??
|
||||
state.stats.mpMax,
|
||||
// 전투 중일 때 몬스터 HP 정보 전달
|
||||
monsterHpCurrent:
|
||||
state.progress.currentCombat?.monsterStats.hpCurrent,
|
||||
monsterHpMax: state.progress.currentCombat?.monsterStats.hpMax,
|
||||
monsterName: state.progress.currentCombat?.monsterStats.name,
|
||||
monsterLevel: state.progress.currentCombat?.monsterStats.level,
|
||||
),
|
||||
|
||||
// Experience 바
|
||||
_buildSectionHeader(l10n.experience),
|
||||
_buildProgressBar(
|
||||
state.progress.exp.position,
|
||||
state.progress.exp.max,
|
||||
Colors.blue,
|
||||
tooltip:
|
||||
'${state.progress.exp.position} / ${state.progress.exp.max}',
|
||||
),
|
||||
|
||||
// 스킬 (Skills - SpellBook 기반)
|
||||
_buildSectionHeader(l10n.spellBook),
|
||||
Expanded(flex: 3, child: _buildSkillsList(state)),
|
||||
|
||||
// 활성 버프 (Active Buffs)
|
||||
_buildSectionHeader(game_l10n.uiBuffs),
|
||||
Expanded(
|
||||
child: ActiveBuffPanel(
|
||||
activeBuffs: state.skillSystem.activeBuffs,
|
||||
currentMs: state.skillSystem.elapsedMs,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 중앙 패널: Equipment/Inventory
|
||||
Widget _buildEquipmentPanel(GameState state) {
|
||||
final l10n = L10n.of(context);
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(4),
|
||||
color: RetroColors.panelBg,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: const BorderSide(color: RetroColors.panelBorderOuter, width: 2),
|
||||
borderRadius: BorderRadius.circular(0),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildPanelHeader(l10n.equipment),
|
||||
|
||||
// Equipment 목록 (확장 가능 스탯 패널)
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: EquipmentStatsPanel(equipment: state.equipment),
|
||||
),
|
||||
|
||||
// Inventory
|
||||
_buildPanelHeader(l10n.inventory),
|
||||
Expanded(child: _buildInventoryList(state)),
|
||||
|
||||
// Potions (물약 인벤토리)
|
||||
_buildSectionHeader(game_l10n.uiPotions),
|
||||
Expanded(
|
||||
child: PotionInventoryPanel(inventory: state.potionInventory),
|
||||
),
|
||||
|
||||
// Encumbrance 바
|
||||
_buildSectionHeader(l10n.encumbrance),
|
||||
_buildProgressBar(
|
||||
state.progress.encumbrance.position,
|
||||
state.progress.encumbrance.max,
|
||||
Colors.orange,
|
||||
),
|
||||
|
||||
// Phase 8: 전투 로그 (Combat Log)
|
||||
_buildPanelHeader(l10n.combatLog),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: CombatLog(entries: _combatLogController.entries),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 우측 패널: Plot/Quest
|
||||
Widget _buildQuestPanel(GameState state) {
|
||||
final l10n = L10n.of(context);
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(4),
|
||||
color: RetroColors.panelBg,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: const BorderSide(color: RetroColors.panelBorderOuter, width: 2),
|
||||
borderRadius: BorderRadius.circular(0),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildPanelHeader(l10n.plotDevelopment),
|
||||
|
||||
// Plot 목록
|
||||
Expanded(child: _buildPlotList(state)),
|
||||
|
||||
// Plot 바
|
||||
_buildProgressBar(
|
||||
state.progress.plot.position,
|
||||
state.progress.plot.max,
|
||||
Colors.purple,
|
||||
tooltip: state.progress.plot.max > 0
|
||||
? '${pq_logic.roughTime(state.progress.plot.max - state.progress.plot.position)} remaining'
|
||||
: null,
|
||||
),
|
||||
|
||||
_buildPanelHeader(l10n.quests),
|
||||
|
||||
// Quest 목록
|
||||
Expanded(child: _buildQuestList(state)),
|
||||
|
||||
// Quest 바
|
||||
_buildProgressBar(
|
||||
state.progress.quest.position,
|
||||
state.progress.quest.max,
|
||||
Colors.green,
|
||||
tooltip: state.progress.quest.max > 0
|
||||
? l10n.percentComplete(
|
||||
100 *
|
||||
state.progress.quest.position ~/
|
||||
state.progress.quest.max,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPanelHeader(String title) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
decoration: const BoxDecoration(
|
||||
color: RetroColors.darkBrown,
|
||||
border: Border(bottom: BorderSide(color: RetroColors.gold, width: 2)),
|
||||
),
|
||||
child: Text(
|
||||
title.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: RetroColors.gold,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader(String title) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Text(
|
||||
title.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 13,
|
||||
color: RetroColors.textDisabled,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 레트로 스타일 세그먼트 프로그레스 바
|
||||
Widget _buildProgressBar(
|
||||
int position,
|
||||
int max,
|
||||
Color color, {
|
||||
String? tooltip,
|
||||
}) {
|
||||
final progress = max > 0 ? (position / max).clamp(0.0, 1.0) : 0.0;
|
||||
const segmentCount = 20;
|
||||
final filledSegments = (progress * segmentCount).round();
|
||||
|
||||
final bar = Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Container(
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.15),
|
||||
border: Border.all(color: RetroColors.panelBorderOuter, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
children: List.generate(segmentCount, (index) {
|
||||
final isFilled = index < filledSegments;
|
||||
return Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isFilled ? color : color.withValues(alpha: 0.1),
|
||||
border: Border(
|
||||
right: index < segmentCount - 1
|
||||
? BorderSide(
|
||||
color: RetroColors.panelBorderOuter.withValues(
|
||||
alpha: 0.3,
|
||||
),
|
||||
width: 1,
|
||||
)
|
||||
: BorderSide.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (tooltip != null && tooltip.isNotEmpty) {
|
||||
return Tooltip(message: tooltip, child: bar);
|
||||
}
|
||||
return bar;
|
||||
}
|
||||
|
||||
Widget _buildTraitsList(GameState state) {
|
||||
final l10n = L10n.of(context);
|
||||
final traits = [
|
||||
(l10n.traitName, state.traits.name),
|
||||
(l10n.traitRace, GameDataL10n.getRaceName(context, state.traits.race)),
|
||||
(l10n.traitClass, GameDataL10n.getKlassName(context, state.traits.klass)),
|
||||
(l10n.traitLevel, '${state.traits.level}'),
|
||||
];
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
child: Column(
|
||||
children: traits.map((t) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 1),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 50,
|
||||
child: Text(
|
||||
t.$1.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 13,
|
||||
color: RetroColors.textDisabled,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
t.$2,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 14,
|
||||
color: RetroColors.textLight,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 통합 스킬 목록 (SkillBook 기반)
|
||||
///
|
||||
/// 스킬 이름, 랭크, 스킬 타입, 쿨타임 표시
|
||||
Widget _buildSkillsList(GameState state) {
|
||||
if (state.skillBook.skills.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
L10n.of(context).noSpellsYet,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 14,
|
||||
color: RetroColors.textDisabled,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: state.skillBook.skills.length,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final skillEntry = state.skillBook.skills[index];
|
||||
final skill = SkillData.getSkillBySpellName(skillEntry.name);
|
||||
final skillName = GameDataL10n.getSpellName(context, skillEntry.name);
|
||||
|
||||
// 쿨타임 상태 확인
|
||||
final skillState = skill != null
|
||||
? state.skillSystem.getSkillState(skill.id)
|
||||
: null;
|
||||
final isOnCooldown =
|
||||
skillState != null &&
|
||||
!skillState.isReady(state.skillSystem.elapsedMs, skill!.cooldownMs);
|
||||
|
||||
return _SkillRow(
|
||||
skillName: skillName,
|
||||
rank: skillEntry.rank,
|
||||
skill: skill,
|
||||
isOnCooldown: isOnCooldown,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInventoryList(GameState state) {
|
||||
final l10n = L10n.of(context);
|
||||
if (state.inventory.items.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
l10n.goldAmount(state.inventory.gold),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 14,
|
||||
color: RetroColors.gold,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: state.inventory.items.length + 1, // +1 for gold
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.monetization_on,
|
||||
size: 10,
|
||||
color: RetroColors.gold,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.gold.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 13,
|
||||
color: RetroColors.gold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${state.inventory.gold}',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 14,
|
||||
color: RetroColors.gold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
final item = state.inventory.items[index - 1];
|
||||
// 아이템 이름 번역
|
||||
final translatedName = GameDataL10n.translateItemString(
|
||||
context,
|
||||
item.name,
|
||||
);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 1),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
translatedName,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 13,
|
||||
color: RetroColors.textLight,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${item.count}',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 13,
|
||||
color: RetroColors.cream,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPlotList(GameState state) {
|
||||
// 플롯 단계를 표시 (Act I, Act II, ...)
|
||||
final l10n = L10n.of(context);
|
||||
final plotCount = state.progress.plotStageCount;
|
||||
if (plotCount == 0) {
|
||||
return Center(
|
||||
child: Text(
|
||||
l10n.prologue.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 14,
|
||||
color: RetroColors.textDisabled,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: plotCount,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final isCompleted = index < plotCount - 1;
|
||||
final isCurrent = index == plotCount - 1;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 1),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isCompleted
|
||||
? Icons.check_box
|
||||
: (isCurrent
|
||||
? Icons.arrow_right
|
||||
: Icons.check_box_outline_blank),
|
||||
size: 12,
|
||||
color: isCompleted
|
||||
? RetroColors.expGreen
|
||||
: (isCurrent ? RetroColors.gold : RetroColors.textDisabled),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
index == 0 ? l10n.prologue : l10n.actNumber(_toRoman(index)),
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 13,
|
||||
color: isCompleted
|
||||
? RetroColors.textDisabled
|
||||
: (isCurrent
|
||||
? RetroColors.gold
|
||||
: RetroColors.textLight),
|
||||
decoration: isCompleted
|
||||
? TextDecoration.lineThrough
|
||||
: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuestList(GameState state) {
|
||||
final l10n = L10n.of(context);
|
||||
final questHistory = state.progress.questHistory;
|
||||
|
||||
if (questHistory.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
l10n.noActiveQuests.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 14,
|
||||
color: RetroColors.textDisabled,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 원본처럼 퀘스트 히스토리를 리스트로 표시
|
||||
// 완료된 퀘스트는 체크박스, 현재 퀘스트는 화살표
|
||||
return ListView.builder(
|
||||
itemCount: questHistory.length,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final quest = questHistory[index];
|
||||
final isCurrentQuest =
|
||||
index == questHistory.length - 1 && !quest.isComplete;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 1),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isCurrentQuest
|
||||
? Icons.arrow_right
|
||||
: (quest.isComplete
|
||||
? Icons.check_box
|
||||
: Icons.check_box_outline_blank),
|
||||
size: 12,
|
||||
color: isCurrentQuest
|
||||
? RetroColors.gold
|
||||
: (quest.isComplete
|
||||
? RetroColors.expGreen
|
||||
: RetroColors.textDisabled),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
quest.caption,
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 13,
|
||||
color: isCurrentQuest
|
||||
? RetroColors.gold
|
||||
: (quest.isComplete
|
||||
? RetroColors.textDisabled
|
||||
: RetroColors.textLight),
|
||||
decoration: quest.isComplete
|
||||
? TextDecoration.lineThrough
|
||||
: TextDecoration.none,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 로마 숫자 변환 (간단 버전)
|
||||
String _toRoman(int number) {
|
||||
const romanNumerals = [
|
||||
(1000, 'M'),
|
||||
(900, 'CM'),
|
||||
(500, 'D'),
|
||||
(400, 'CD'),
|
||||
(100, 'C'),
|
||||
(90, 'XC'),
|
||||
(50, 'L'),
|
||||
(40, 'XL'),
|
||||
(10, 'X'),
|
||||
(9, 'IX'),
|
||||
(5, 'V'),
|
||||
(4, 'IV'),
|
||||
(1, 'I'),
|
||||
];
|
||||
|
||||
var result = '';
|
||||
var remaining = number;
|
||||
for (final (value, numeral) in romanNumerals) {
|
||||
while (remaining >= value) {
|
||||
result += numeral;
|
||||
remaining -= value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// 스킬 행 위젯
|
||||
///
|
||||
/// 스킬 이름, 랭크, 스킬 타입 아이콘, 쿨타임 상태 표시
|
||||
class _SkillRow extends StatelessWidget {
|
||||
const _SkillRow({
|
||||
required this.skillName,
|
||||
required this.rank,
|
||||
required this.skill,
|
||||
required this.isOnCooldown,
|
||||
});
|
||||
|
||||
final String skillName;
|
||||
final String rank;
|
||||
final Skill? skill;
|
||||
final bool isOnCooldown;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 1),
|
||||
child: Row(
|
||||
children: [
|
||||
// 스킬 타입 아이콘
|
||||
_buildTypeIcon(),
|
||||
const SizedBox(width: 4),
|
||||
// 스킬 이름
|
||||
Expanded(
|
||||
child: Text(
|
||||
skillName,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: isOnCooldown ? Colors.grey : null,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
// 쿨타임 표시
|
||||
if (isOnCooldown)
|
||||
const Icon(Icons.hourglass_empty, size: 10, color: Colors.orange),
|
||||
const SizedBox(width: 4),
|
||||
// 랭크
|
||||
Text(
|
||||
rank,
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 스킬 타입별 아이콘
|
||||
Widget _buildTypeIcon() {
|
||||
if (skill == null) {
|
||||
return const SizedBox(width: 12);
|
||||
}
|
||||
|
||||
final (IconData icon, Color color) = switch (skill!.type) {
|
||||
SkillType.attack => (Icons.flash_on, Colors.red),
|
||||
SkillType.heal => (Icons.favorite, Colors.green),
|
||||
SkillType.buff => (Icons.arrow_upward, Colors.blue),
|
||||
SkillType.debuff => (Icons.arrow_downward, Colors.purple),
|
||||
};
|
||||
|
||||
return Icon(icon, size: 12, color: color);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user