feat(game): 게임 시스템 전면 개편 및 다국어 지원 확장
## 스킬 시스템 개선 - skill_data.dart: 스킬 데이터 구조 전면 개편 (+1176 라인) - skill_service.dart: 스킬 발동 로직 확장 및 버프 시스템 연동 - skill.dart: 스킬 모델 개선, 쿨다운/효과 타입 추가 ## Canvas 애니메이션 리팩토링 - battle_composer.dart 삭제 (레거시 위젯 기반 렌더러) - monster_colors.dart 삭제 (AsciiCell 색상 시스템으로 통합) - canvas_battle_composer.dart: z-index 정렬 (몬스터 z=1, 캐릭터 z=2, 이펙트 z=3) - ascii_cell.dart, ascii_layer.dart: 코드 정리 ## UI/UX 개선 - hp_mp_bar.dart: l10n 적용, 몬스터 HP 바 컴팩트화 - death_overlay.dart: 사망 화면 개선 - equipment_stats_panel.dart: 장비 스탯 표시 확장 - active_buff_panel.dart: 버프 패널 개선 - notification_overlay.dart: 알림 시스템 개선 ## 다국어 지원 확장 - game_text_l10n.dart: 게임 텍스트 통합 (+758 라인) - 한국어/일본어/영어/중국어 번역 업데이트 - ARB 파일 동기화 ## 게임 로직 개선 - progress_service.dart: 진행 로직 리팩토링 - combat_calculator.dart: 전투 계산 로직 개선 - stat_calculator.dart: 스탯 계산 시스템 개선 - story_service.dart: 스토리 진행 로직 개선 ## 기타 - theme_preferences.dart 삭제 (미사용) - 테스트 파일 업데이트 - class_data.dart: 클래스 데이터 정리
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:askiineverdie/data/game_text_l10n.dart' as game_l10n;
|
||||
import 'package:askiineverdie/data/skill_data.dart';
|
||||
import 'package:askiineverdie/data/story_data.dart';
|
||||
import 'package:askiineverdie/l10n/app_localizations.dart';
|
||||
import 'package:askiineverdie/src/core/animation/ascii_animation_type.dart';
|
||||
@@ -8,6 +10,7 @@ import 'package:askiineverdie/src/core/model/combat_event.dart';
|
||||
import 'package:askiineverdie/src/core/l10n/game_data_l10n.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/skill.dart';
|
||||
import 'package:askiineverdie/src/core/notification/notification_service.dart';
|
||||
import 'package:askiineverdie/src/core/storage/hall_of_fame_storage.dart';
|
||||
import 'package:askiineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
||||
@@ -18,7 +21,6 @@ import 'package:askiineverdie/src/features/game/widgets/combat_log.dart';
|
||||
import 'package:askiineverdie/src/features/game/widgets/death_overlay.dart';
|
||||
import 'package:askiineverdie/src/features/game/widgets/hp_mp_bar.dart';
|
||||
import 'package:askiineverdie/src/features/game/widgets/notification_overlay.dart';
|
||||
import 'package:askiineverdie/src/features/game/widgets/skill_panel.dart';
|
||||
import 'package:askiineverdie/src/features/game/widgets/stats_panel.dart';
|
||||
import 'package:askiineverdie/src/features/game/widgets/equipment_stats_panel.dart';
|
||||
import 'package:askiineverdie/src/features/game/widgets/potion_inventory_panel.dart';
|
||||
@@ -78,7 +80,10 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
if (state.traits.level > _lastLevel && _lastLevel > 0) {
|
||||
_specialAnimation = AsciiAnimationType.levelUp;
|
||||
_notificationService.showLevelUp(state.traits.level);
|
||||
_addCombatLog('Level Up! Now level ${state.traits.level}', CombatLogType.levelUp);
|
||||
_addCombatLog(
|
||||
'${game_l10n.uiLevelUp} Lv.${state.traits.level}',
|
||||
CombatLogType.levelUp,
|
||||
);
|
||||
_resetSpecialAnimationAfterFrame();
|
||||
|
||||
// Phase 9: Act 변경 감지 (레벨 기반)
|
||||
@@ -111,7 +116,10 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
.lastOrNull;
|
||||
if (completedQuest != null) {
|
||||
_notificationService.showQuestComplete(completedQuest.caption);
|
||||
_addCombatLog('Quest Complete: ${completedQuest.caption}', CombatLogType.questComplete);
|
||||
_addCombatLog(
|
||||
game_l10n.uiQuestComplete(completedQuest.caption),
|
||||
CombatLogType.questComplete,
|
||||
);
|
||||
}
|
||||
_resetSpecialAnimationAfterFrame();
|
||||
}
|
||||
@@ -131,11 +139,9 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
|
||||
/// Phase 8: 전투 로그 추가 (Add Combat Log Entry)
|
||||
void _addCombatLog(String message, CombatLogType type) {
|
||||
_combatLogEntries.add(CombatLogEntry(
|
||||
message: message,
|
||||
timestamp: DateTime.now(),
|
||||
type: type,
|
||||
));
|
||||
_combatLogEntries.add(
|
||||
CombatLogEntry(message: message, timestamp: DateTime.now(), type: type),
|
||||
);
|
||||
// 최대 50개 유지
|
||||
if (_combatLogEntries.length > 50) {
|
||||
_combatLogEntries.removeAt(0);
|
||||
@@ -167,53 +173,78 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
|
||||
/// 전투 이벤트를 메시지와 타입으로 변환
|
||||
(String, CombatLogType) _formatCombatEvent(CombatEvent event) {
|
||||
final target = event.targetName ?? '';
|
||||
return switch (event.type) {
|
||||
CombatEventType.playerAttack => event.isCritical
|
||||
? ('CRITICAL! ${event.damage} damage to ${event.targetName}!', CombatLogType.critical)
|
||||
: ('You hit ${event.targetName} for ${event.damage} damage', CombatLogType.damage),
|
||||
CombatEventType.playerAttack =>
|
||||
event.isCritical
|
||||
? (
|
||||
game_l10n.combatCritical(event.damage, target),
|
||||
CombatLogType.critical,
|
||||
)
|
||||
: (
|
||||
game_l10n.combatYouHit(target, event.damage),
|
||||
CombatLogType.damage,
|
||||
),
|
||||
CombatEventType.monsterAttack => (
|
||||
'${event.targetName} hits you for ${event.damage} damage',
|
||||
CombatLogType.monsterAttack,
|
||||
),
|
||||
game_l10n.combatMonsterHitsYou(target, event.damage),
|
||||
CombatLogType.monsterAttack,
|
||||
),
|
||||
CombatEventType.playerEvade => (
|
||||
'You evaded ${event.targetName}\'s attack!',
|
||||
CombatLogType.evade,
|
||||
),
|
||||
game_l10n.combatYouEvaded(target),
|
||||
CombatLogType.evade,
|
||||
),
|
||||
CombatEventType.monsterEvade => (
|
||||
'${event.targetName} evaded your attack!',
|
||||
CombatLogType.evade,
|
||||
),
|
||||
game_l10n.combatMonsterEvaded(target),
|
||||
CombatLogType.evade,
|
||||
),
|
||||
CombatEventType.playerBlock => (
|
||||
'Blocked! Reduced to ${event.damage} damage',
|
||||
CombatLogType.block,
|
||||
),
|
||||
game_l10n.combatBlocked(event.damage),
|
||||
CombatLogType.block,
|
||||
),
|
||||
CombatEventType.playerParry => (
|
||||
'Parried! Reduced to ${event.damage} damage',
|
||||
CombatLogType.parry,
|
||||
),
|
||||
CombatEventType.playerSkill => event.isCritical
|
||||
? ('CRITICAL ${event.skillName}! ${event.damage} damage!', CombatLogType.critical)
|
||||
: ('${event.skillName}: ${event.damage} damage', CombatLogType.spell),
|
||||
game_l10n.combatParried(event.damage),
|
||||
CombatLogType.parry,
|
||||
),
|
||||
CombatEventType.playerSkill =>
|
||||
event.isCritical
|
||||
? (
|
||||
game_l10n.combatSkillCritical(
|
||||
event.skillName ?? '',
|
||||
event.damage,
|
||||
),
|
||||
CombatLogType.critical,
|
||||
)
|
||||
: (
|
||||
game_l10n.combatSkillDamage(event.skillName ?? '', event.damage),
|
||||
CombatLogType.spell,
|
||||
),
|
||||
CombatEventType.playerHeal => (
|
||||
'${event.skillName ?? "Heal"}: +${event.healAmount} HP',
|
||||
CombatLogType.heal,
|
||||
game_l10n.combatSkillHeal(
|
||||
event.skillName ?? game_l10n.uiHeal,
|
||||
event.healAmount,
|
||||
),
|
||||
CombatLogType.heal,
|
||||
),
|
||||
CombatEventType.playerBuff => (
|
||||
'${event.skillName} activated!',
|
||||
CombatLogType.buff,
|
||||
),
|
||||
game_l10n.combatBuffActivated(event.skillName ?? ''),
|
||||
CombatLogType.buff,
|
||||
),
|
||||
CombatEventType.dotTick => (
|
||||
'${event.skillName} ticks for ${event.damage} damage',
|
||||
CombatLogType.dotTick,
|
||||
),
|
||||
game_l10n.combatDotTick(event.skillName ?? '', event.damage),
|
||||
CombatLogType.dotTick,
|
||||
),
|
||||
CombatEventType.playerPotion => (
|
||||
'${event.skillName}: +${event.healAmount} ${event.targetName}',
|
||||
CombatLogType.potion,
|
||||
game_l10n.combatPotionUsed(
|
||||
event.skillName ?? '',
|
||||
event.healAmount,
|
||||
target,
|
||||
),
|
||||
CombatLogType.potion,
|
||||
),
|
||||
CombatEventType.potionDrop => (
|
||||
'Dropped: ${event.skillName}',
|
||||
CombatLogType.potionDrop,
|
||||
),
|
||||
game_l10n.combatPotionDrop(event.skillName ?? ''),
|
||||
CombatLogType.potionDrop,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -394,105 +425,107 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10n.of(context).progressQuestTitle(state.traits.name)),
|
||||
actions: [
|
||||
// 치트 버튼 (디버그용)
|
||||
if (widget.controller.cheatsEnabled) ...[
|
||||
IconButton(
|
||||
icon: const Text('L+1'),
|
||||
tooltip: L10n.of(context).levelUp,
|
||||
onPressed: () => widget.controller.loop?.cheatCompleteTask(),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Text('Q!'),
|
||||
tooltip: L10n.of(context).completeQuest,
|
||||
onPressed: () => widget.controller.loop?.cheatCompleteQuest(),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Text('P!'),
|
||||
tooltip: L10n.of(context).completePlot,
|
||||
onPressed: () => widget.controller.loop?.cheatCompletePlot(),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
// 메인 게임 UI
|
||||
Column(
|
||||
children: [
|
||||
// 상단: ASCII 애니메이션 + Task Progress (Phase 7: 고정 4색 팔레트)
|
||||
TaskProgressPanel(
|
||||
progress: state.progress,
|
||||
speedMultiplier: widget.controller.loop?.speedMultiplier ?? 1,
|
||||
onSpeedCycle: () {
|
||||
widget.controller.loop?.cycleSpeed();
|
||||
setState(() {});
|
||||
},
|
||||
isPaused: !widget.controller.isRunning,
|
||||
onPauseToggle: () async {
|
||||
await widget.controller.togglePause();
|
||||
setState(() {});
|
||||
},
|
||||
specialAnimation: _specialAnimation,
|
||||
weaponName: state.equipment.weapon,
|
||||
shieldName: state.equipment.shield,
|
||||
characterLevel: state.traits.level,
|
||||
monsterLevel: state.progress.currentTask.monsterLevel,
|
||||
latestCombatEvent: state.progress.currentCombat?.recentEvents.lastOrNull,
|
||||
appBar: AppBar(
|
||||
title: Text(L10n.of(context).progressQuestTitle(state.traits.name)),
|
||||
actions: [
|
||||
// 치트 버튼 (디버그용)
|
||||
if (widget.controller.cheatsEnabled) ...[
|
||||
IconButton(
|
||||
icon: const Text('L+1'),
|
||||
tooltip: L10n.of(context).levelUp,
|
||||
onPressed: () => widget.controller.loop?.cheatCompleteTask(),
|
||||
),
|
||||
|
||||
// 메인 3패널 영역
|
||||
Expanded(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 좌측 패널: Character Sheet
|
||||
Expanded(flex: 2, child: _buildCharacterPanel(state)),
|
||||
|
||||
// 중앙 패널: Equipment/Inventory
|
||||
Expanded(flex: 3, child: _buildEquipmentPanel(state)),
|
||||
|
||||
// 우측 패널: Plot/Quest
|
||||
Expanded(flex: 2, child: _buildQuestPanel(state)),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: const Text('Q!'),
|
||||
tooltip: L10n.of(context).completeQuest,
|
||||
onPressed: () => widget.controller.loop?.cheatCompleteQuest(),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Text('P!'),
|
||||
tooltip: L10n.of(context).completePlot,
|
||||
onPressed: () => widget.controller.loop?.cheatCompletePlot(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
// 메인 게임 UI
|
||||
Column(
|
||||
children: [
|
||||
// 상단: ASCII 애니메이션 + Task Progress (Phase 7: 고정 4색 팔레트)
|
||||
TaskProgressPanel(
|
||||
progress: state.progress,
|
||||
speedMultiplier:
|
||||
widget.controller.loop?.speedMultiplier ?? 1,
|
||||
onSpeedCycle: () {
|
||||
widget.controller.loop?.cycleSpeed();
|
||||
setState(() {});
|
||||
},
|
||||
isPaused: !widget.controller.isRunning,
|
||||
onPauseToggle: () async {
|
||||
await widget.controller.togglePause();
|
||||
setState(() {});
|
||||
},
|
||||
specialAnimation: _specialAnimation,
|
||||
weaponName: state.equipment.weapon,
|
||||
shieldName: state.equipment.shield,
|
||||
characterLevel: state.traits.level,
|
||||
monsterLevel: state.progress.currentTask.monsterLevel,
|
||||
latestCombatEvent:
|
||||
state.progress.currentCombat?.recentEvents.lastOrNull,
|
||||
),
|
||||
|
||||
// Phase 4: 사망 오버레이 (Death Overlay)
|
||||
if (state.isDead && state.deathInfo != null)
|
||||
DeathOverlay(
|
||||
deathInfo: state.deathInfo!,
|
||||
traits: state.traits,
|
||||
onResurrect: () async {
|
||||
// 1. 부활 처리 (HP/MP 회복, 장비 구매 - 게임 재개 없음)
|
||||
await widget.controller.resurrect();
|
||||
// 메인 3패널 영역
|
||||
Expanded(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 좌측 패널: Character Sheet
|
||||
Expanded(flex: 2, child: _buildCharacterPanel(state)),
|
||||
|
||||
// 2. 부활 애니메이션 재생
|
||||
setState(() {
|
||||
_specialAnimation = AsciiAnimationType.resurrection;
|
||||
});
|
||||
// 중앙 패널: Equipment/Inventory
|
||||
Expanded(flex: 3, child: _buildEquipmentPanel(state)),
|
||||
|
||||
// 3. 애니메이션 종료 후 게임 재개
|
||||
final duration = getSpecialAnimationDuration(
|
||||
AsciiAnimationType.resurrection,
|
||||
);
|
||||
Future.delayed(Duration(milliseconds: duration), () async {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_specialAnimation = null;
|
||||
});
|
||||
// 부활 후 게임 재개 (새 루프 시작)
|
||||
await widget.controller.resumeAfterResurrection();
|
||||
}
|
||||
});
|
||||
},
|
||||
// 우측 패널: Plot/Quest
|
||||
Expanded(flex: 2, child: _buildQuestPanel(state)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Phase 4: 사망 오버레이 (Death Overlay)
|
||||
if (state.isDead && state.deathInfo != null)
|
||||
DeathOverlay(
|
||||
deathInfo: state.deathInfo!,
|
||||
traits: state.traits,
|
||||
onResurrect: () async {
|
||||
// 1. 부활 처리 (HP/MP 회복, 장비 구매 - 게임 재개 없음)
|
||||
await widget.controller.resurrect();
|
||||
|
||||
// 2. 부활 애니메이션 재생
|
||||
setState(() {
|
||||
_specialAnimation = AsciiAnimationType.resurrection;
|
||||
});
|
||||
|
||||
// 3. 애니메이션 종료 후 게임 재개
|
||||
final duration = getSpecialAnimationDuration(
|
||||
AsciiAnimationType.resurrection,
|
||||
);
|
||||
Future.delayed(Duration(milliseconds: duration), () async {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_specialAnimation = null;
|
||||
});
|
||||
// 부활 후 게임 재개 (새 루프 시작)
|
||||
await widget.controller.resumeAfterResurrection();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -519,13 +552,17 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
// Phase 8: HP/MP 바 (사망 위험 시 깜빡임, 전투 중 몬스터 HP 표시)
|
||||
// 전투 중에는 전투 스탯의 HP/MP 사용, 비전투 시 기본 스탯 사용
|
||||
HpMpBar(
|
||||
hpCurrent: state.progress.currentCombat?.playerStats.hpCurrent ??
|
||||
hpCurrent:
|
||||
state.progress.currentCombat?.playerStats.hpCurrent ??
|
||||
state.stats.hp,
|
||||
hpMax: state.progress.currentCombat?.playerStats.hpMax ??
|
||||
hpMax:
|
||||
state.progress.currentCombat?.playerStats.hpMax ??
|
||||
state.stats.hpMax,
|
||||
mpCurrent: state.progress.currentCombat?.playerStats.mpCurrent ??
|
||||
mpCurrent:
|
||||
state.progress.currentCombat?.playerStats.mpCurrent ??
|
||||
state.stats.mp,
|
||||
mpMax: state.progress.currentCombat?.playerStats.mpMax ??
|
||||
mpMax:
|
||||
state.progress.currentCombat?.playerStats.mpMax ??
|
||||
state.stats.mpMax,
|
||||
// 전투 중일 때 몬스터 HP 정보 전달
|
||||
monsterHpCurrent:
|
||||
@@ -545,16 +582,12 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
'${l10n.xpNeededForNextLevel}',
|
||||
),
|
||||
|
||||
// Spell Book
|
||||
// 스킬 (Skills - SpellBook 기반)
|
||||
_buildSectionHeader(l10n.spellBook),
|
||||
Expanded(flex: 2, child: _buildSpellsList(state)),
|
||||
|
||||
// Phase 8: 스킬 (Skills with cooldown glow)
|
||||
_buildSectionHeader('Skills'),
|
||||
Expanded(flex: 2, child: SkillPanel(skillSystem: state.skillSystem)),
|
||||
Expanded(flex: 3, child: _buildSkillsList(state)),
|
||||
|
||||
// 활성 버프 (Active Buffs)
|
||||
_buildSectionHeader('Buffs'),
|
||||
_buildSectionHeader(game_l10n.uiBuffs),
|
||||
Expanded(
|
||||
child: ActiveBuffPanel(
|
||||
activeBuffs: state.skillSystem.activeBuffs,
|
||||
@@ -587,7 +620,7 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
Expanded(child: _buildInventoryList(state)),
|
||||
|
||||
// Potions (물약 인벤토리)
|
||||
_buildSectionHeader('Potions'),
|
||||
_buildSectionHeader(game_l10n.uiPotions),
|
||||
Expanded(
|
||||
child: PotionInventoryPanel(
|
||||
inventory: state.potionInventory,
|
||||
@@ -647,7 +680,8 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
Colors.green,
|
||||
tooltip: state.progress.quest.max > 0
|
||||
? l10n.percentComplete(
|
||||
100 * state.progress.quest.position ~/
|
||||
100 *
|
||||
state.progress.quest.position ~/
|
||||
state.progress.quest.max,
|
||||
)
|
||||
: null,
|
||||
@@ -737,10 +771,16 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSpellsList(GameState state) {
|
||||
/// 통합 스킬 목록 (SpellBook 기반)
|
||||
///
|
||||
/// 스펠 이름, 랭크, 스킬 타입, 쿨타임 표시
|
||||
Widget _buildSkillsList(GameState state) {
|
||||
if (state.spellBook.spells.isEmpty) {
|
||||
return Center(
|
||||
child: Text(L10n.of(context).noSpellsYet, style: const TextStyle(fontSize: 11)),
|
||||
child: Text(
|
||||
L10n.of(context).noSpellsYet,
|
||||
style: const TextStyle(fontSize: 11),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -749,21 +789,22 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final spell = state.spellBook.spells[index];
|
||||
final skill = SkillData.getSkillBySpellName(spell.name);
|
||||
final spellName = GameDataL10n.getSpellName(context, spell.name);
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
spellName,
|
||||
style: const TextStyle(fontSize: 11),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
spell.rank,
|
||||
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
|
||||
// 쿨타임 상태 확인
|
||||
final skillState = skill != null
|
||||
? state.skillSystem.getSkillState(skill.id)
|
||||
: null;
|
||||
final isOnCooldown =
|
||||
skillState != null &&
|
||||
!skillState.isReady(state.skillSystem.elapsedMs, skill!.cooldownMs);
|
||||
|
||||
return _SkillRow(
|
||||
spellName: spellName,
|
||||
rank: spell.rank,
|
||||
skill: skill,
|
||||
isOnCooldown: isOnCooldown,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -880,7 +921,8 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final quest = questHistory[index];
|
||||
final isCurrentQuest = index == questHistory.length - 1 && !quest.isComplete;
|
||||
final isCurrentQuest =
|
||||
index == questHistory.length - 1 && !quest.isComplete;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
@@ -942,3 +984,70 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// 스킬 행 위젯
|
||||
///
|
||||
/// 스펠 이름, 랭크, 스킬 타입 아이콘, 쿨타임 상태 표시
|
||||
class _SkillRow extends StatelessWidget {
|
||||
const _SkillRow({
|
||||
required this.spellName,
|
||||
required this.rank,
|
||||
required this.skill,
|
||||
required this.isOnCooldown,
|
||||
});
|
||||
|
||||
final String spellName;
|
||||
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(
|
||||
spellName,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
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: 11, 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