import 'package:flutter/material.dart'; import 'package:asciineverdie/data/game_text_l10n.dart' as l10n; import 'package:asciineverdie/data/skill_data.dart'; import 'package:asciineverdie/l10n/app_localizations.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/features/game/widgets/active_buff_panel.dart'; import 'package:asciineverdie/src/shared/retro_colors.dart'; /// 스킬 페이지 (캐로셀) /// /// SkillBook 기반 스킬 목록과 활성 버프 표시. class SkillsPage extends StatelessWidget { const SkillsPage({ super.key, required this.skillBook, required this.skillSystem, }); final SkillBook skillBook; final SkillSystemState skillSystem; @override Widget build(BuildContext context) { final localizations = L10n.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 스킬 목록 _buildSectionHeader(context, localizations.spellBook), Expanded(flex: 3, child: _buildSkillsList(context)), // 활성 버프 _buildSectionHeader(context, l10n.uiBuffs), Expanded( flex: 2, child: ActiveBuffPanel( activeBuffs: skillSystem.activeBuffs, currentMs: skillSystem.elapsedMs, ), ), ], ); } Widget _buildSectionHeader(BuildContext context, String title) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), color: Theme.of(context).colorScheme.primaryContainer, child: Text( title, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 18, color: Theme.of(context).colorScheme.onPrimaryContainer, ), ), ); } Widget _buildSkillsList(BuildContext context) { if (skillBook.skills.isEmpty) { return Center( child: Text( L10n.of(context).noSpellsYet, style: const TextStyle(fontSize: 17, color: Colors.grey), ), ); } return ListView.builder( itemCount: skillBook.skills.length, padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), itemBuilder: (context, index) { final skillEntry = skillBook.skills[index]; final skill = SkillData.getSkillBySpellName(skillEntry.name); final skillName = GameDataL10n.getSpellName(context, skillEntry.name); // 쿨타임 상태 확인 final skillState = skill != null ? skillSystem.getSkillState(skill.id) : null; final isOnCooldown = skillState != null && !skillState.isReady(skillSystem.elapsedMs, skill!.cooldownMs); return _SkillTile( skillName: skillName, rank: skillEntry.rank, skill: skill, isOnCooldown: isOnCooldown, ); }, ); } } /// 스킬 타일 위젯 (ExpansionTile 기반) class _SkillTile extends StatelessWidget { const _SkillTile({ 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) { // 스킬 데이터가 없으면 단순 행으로 표시 if (skill == null) { return _SimpleSkillRow( skillName: skillName, rank: rank, isOnCooldown: isOnCooldown, ); } final typeColor = _getTypeColor(skill!.type); return ExpansionTile( tilePadding: const EdgeInsets.symmetric(horizontal: 8), childrenPadding: const EdgeInsets.only(left: 16, right: 8, bottom: 8), dense: true, title: Row( children: [ _SkillTypeIcon(type: skill!.type), const SizedBox(width: 8), Expanded( child: Text( skillName, style: TextStyle( fontSize: 18, color: isOnCooldown ? Colors.grey : typeColor, fontWeight: FontWeight.w500, ), overflow: TextOverflow.ellipsis, ), ), if (isOnCooldown) const Padding( padding: EdgeInsets.only(right: 8), child: Icon(Icons.hourglass_empty, size: 14, color: Colors.orange), ), _RankBadge(rank: rank), ], ), children: [ _SkillStatsGrid(skill: skill!), const SizedBox(height: 4), _SkillMetaRow(skill: skill!), ], ); } Color _getTypeColor(SkillType type) { return switch (type) { SkillType.attack => Colors.red.shade300, SkillType.heal => Colors.green.shade300, SkillType.buff => Colors.blue.shade300, SkillType.debuff => Colors.purple.shade300, }; } } /// 스킬 데이터가 없는 경우의 단순 행 class _SimpleSkillRow extends StatelessWidget { const _SimpleSkillRow({ required this.skillName, required this.rank, required this.isOnCooldown, }); final String skillName; final String rank; final bool isOnCooldown; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 8), child: Row( children: [ const SizedBox(width: 24), Expanded( child: Text( skillName, style: TextStyle( fontSize: 18, color: isOnCooldown ? Colors.grey : null, ), overflow: TextOverflow.ellipsis, ), ), if (isOnCooldown) const Icon(Icons.hourglass_empty, size: 14, color: Colors.orange), const SizedBox(width: 8), Text( rank, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.bold), ), ], ), ); } } /// 스킬 타입 아이콘 class _SkillTypeIcon extends StatelessWidget { const _SkillTypeIcon({required this.type}); final SkillType type; @override Widget build(BuildContext context) { final (IconData icon, Color color) = switch (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: 18, color: color); } } /// 랭크 배지 class _RankBadge extends StatelessWidget { const _RankBadge({required this.rank}); final String rank; @override Widget build(BuildContext context) { final gold = RetroColors.goldOf(context); final surface = RetroColors.surfaceOf(context); return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: surface, border: Border.all(color: gold, width: 1), borderRadius: BorderRadius.circular(4), ), child: Text( rank, style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: gold, ), ), ); } } /// 스킬 스탯 그리드 (타입별 분기) class _SkillStatsGrid extends StatelessWidget { const _SkillStatsGrid({required this.skill}); final Skill skill; @override Widget build(BuildContext context) { final entries = <_StatEntry>[]; // 공통: MP, 쿨타임 entries.add(_StatEntry(l10n.skillMpCost, '${skill.mpCost}')); entries.add(_StatEntry( l10n.skillCooldown, '${(skill.cooldownMs / 1000).toStringAsFixed(1)}${l10n.skillSeconds}', )); // 타입별 스탯 추가 switch (skill.type) { case SkillType.attack: _addAttackStats(entries); case SkillType.heal: _addHealStats(entries); case SkillType.buff: _addBuffStats(entries); case SkillType.debuff: _addDebuffStats(entries); } return Wrap( spacing: 8, runSpacing: 4, children: entries.map((e) => _StatChip(entry: e)).toList(), ); } void _addAttackStats(List<_StatEntry> entries) { // 위력 (power × multiplier) final power = (skill.power * skill.damageMultiplier).round(); entries.add(_StatEntry(l10n.skillPower, '$power')); // 타격 횟수 (1보다 클 때만) if (skill.hitCount > 1) { entries.add(_StatEntry(l10n.skillHits, '${skill.hitCount}')); } // DOT 정보 if (skill.isDot && skill.baseDotDamage != null) { final dotDps = skill.baseDotDamage! * (skill.baseDotDurationMs! / skill.baseDotTickMs!); entries.add(_StatEntry(l10n.skillDot, '${dotDps.round()}')); } // HP 흡수 if (skill.lifestealPercent > 0) { entries.add(_StatEntry( l10n.skillLifesteal, '${(skill.lifestealPercent * 100).round()}%', )); } // 방어 무시 if (skill.targetDefReduction > 0) { entries.add(_StatEntry( l10n.skillDefPen, '${(skill.targetDefReduction * 100).round()}%', )); } // 자해 데미지 if (skill.selfDamagePercent > 0) { entries.add(_StatEntry( l10n.skillSelfDmg, '${(skill.selfDamagePercent * 100).round()}%', )); } } void _addHealStats(List<_StatEntry> entries) { // 고정 회복 if (skill.healAmount > 0) { entries.add(_StatEntry(l10n.skillHealFixed, '+${skill.healAmount}')); } // % 회복 if (skill.healPercent > 0) { entries.add(_StatEntry( l10n.skillHealPercent, '${(skill.healPercent * 100).round()}%', )); } // MP 회복 if (skill.mpHealAmount > 0) { entries.add(_StatEntry(l10n.skillMpHeal, '+${skill.mpHealAmount}')); } // 부가 버프 if (skill.buff != null) { final buff = skill.buff!; entries.add(_StatEntry( l10n.skillBuffDuration, '${(buff.durationMs / 1000).round()}${l10n.skillSeconds}', )); } } void _addBuffStats(List<_StatEntry> entries) { if (skill.buff == null) return; final buff = skill.buff!; // 지속시간 entries.add(_StatEntry( l10n.skillBuffDuration, '${(buff.durationMs / 1000).round()}${l10n.skillSeconds}', )); // 각 보정치 if (buff.atkModifier != 0) { final sign = buff.atkModifier > 0 ? '+' : ''; entries.add(_StatEntry( l10n.skillAtkMod, '$sign${(buff.atkModifier * 100).round()}%', )); } if (buff.defModifier != 0) { final sign = buff.defModifier > 0 ? '+' : ''; entries.add(_StatEntry( l10n.skillDefMod, '$sign${(buff.defModifier * 100).round()}%', )); } if (buff.criRateModifier != 0) { final sign = buff.criRateModifier > 0 ? '+' : ''; entries.add(_StatEntry( l10n.skillCriMod, '$sign${(buff.criRateModifier * 100).round()}%', )); } if (buff.evasionModifier != 0) { final sign = buff.evasionModifier > 0 ? '+' : ''; entries.add(_StatEntry( l10n.skillEvaMod, '$sign${(buff.evasionModifier * 100).round()}%', )); } } void _addDebuffStats(List<_StatEntry> entries) { if (skill.buff == null) return; final buff = skill.buff!; // 지속시간 entries.add(_StatEntry( l10n.skillBuffDuration, '${(buff.durationMs / 1000).round()}${l10n.skillSeconds}', )); // 디버프 효과 (보통 음수) if (buff.atkModifier != 0) { entries.add(_StatEntry( l10n.skillAtkMod, '${(buff.atkModifier * 100).round()}%', )); } if (buff.defModifier != 0) { entries.add(_StatEntry( l10n.skillDefMod, '${(buff.defModifier * 100).round()}%', )); } } } /// 스탯 엔트리 class _StatEntry { const _StatEntry(this.label, this.value); final String label; final String value; } /// 스탯 칩 (레트로 스타일) class _StatChip extends StatelessWidget { const _StatChip({required this.entry}); final _StatEntry entry; @override Widget build(BuildContext context) { final surface = RetroColors.surfaceOf(context); final border = RetroColors.borderOf(context); final textMuted = RetroColors.textMutedOf(context); final textPrimary = RetroColors.textPrimaryOf(context); return Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), decoration: BoxDecoration( color: surface, border: Border.all(color: border, width: 1), borderRadius: BorderRadius.circular(2), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( '${entry.label}: ', style: TextStyle(fontSize: 15, color: textMuted), ), Text( entry.value, style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: textPrimary, ), ), ], ), ); } } /// 스킬 메타 정보 행 class _SkillMetaRow extends StatelessWidget { const _SkillMetaRow({required this.skill}); final Skill skill; @override Widget build(BuildContext context) { final textMuted = RetroColors.textMutedOf(context); final typeColor = _getTypeColor(skill.type); return Row( children: [ // 티어 Text( '${l10n.skillTier} ${_tierToRoman(skill.tier)}', style: TextStyle(fontSize: 15, color: textMuted), ), const SizedBox(width: 8), // 타입 Text( _getTypeName(skill.type), style: TextStyle( fontSize: 15, color: typeColor, fontWeight: FontWeight.bold, ), ), // 속성 (있는 경우) if (skill.element != null) ...[ const SizedBox(width: 8), Text( _getElementName(skill.element!), style: TextStyle( fontSize: 15, color: _getElementColor(skill.element!), fontWeight: FontWeight.w500, ), ), ], ], ); } Color _getTypeColor(SkillType type) { return switch (type) { SkillType.attack => Colors.red, SkillType.heal => Colors.green, SkillType.buff => Colors.blue, SkillType.debuff => Colors.purple, }; } Color _getElementColor(SkillElement element) { return switch (element) { SkillElement.logic => Colors.cyan, SkillElement.memory => Colors.teal, SkillElement.network => Colors.indigo, SkillElement.fire => Colors.orange, SkillElement.ice => Colors.lightBlue, SkillElement.lightning => Colors.yellow.shade700, SkillElement.voidElement => Colors.grey, SkillElement.chaos => Colors.pink, }; } String _tierToRoman(int tier) { return switch (tier) { 1 => 'I', 2 => 'II', 3 => 'III', 4 => 'IV', 5 => 'V', _ => '$tier', }; } String _getTypeName(SkillType type) { return switch (type) { SkillType.attack => l10n.skillTypeAttack, SkillType.heal => l10n.skillTypeHeal, SkillType.buff => l10n.skillTypeBuff, SkillType.debuff => l10n.skillTypeDebuff, }; } String _getElementName(SkillElement element) { return switch (element) { SkillElement.logic => l10n.elementLogic, SkillElement.memory => l10n.elementMemory, SkillElement.network => l10n.elementNetwork, SkillElement.fire => l10n.elementFire, SkillElement.ice => l10n.elementIce, SkillElement.lightning => l10n.elementLightning, SkillElement.voidElement => l10n.elementVoid, SkillElement.chaos => l10n.elementChaos, }; } }