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:
JiWoong Sul
2025-12-22 19:00:58 +09:00
parent f606fca063
commit 99f5b74802
63 changed files with 3403 additions and 2740 deletions

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:askiineverdie/data/game_text_l10n.dart' as game_l10n;
import 'package:askiineverdie/l10n/app_localizations.dart';
class FrontScreen extends StatelessWidget {
@@ -128,7 +129,7 @@ class _HeroHeader extends StatelessWidget {
),
const SizedBox(height: 6),
Text(
'Offline Progress Quest (PQ 6.4) rebuilt with Flutter.',
game_l10n.frontDescription,
style: theme.textTheme.titleMedium?.copyWith(
color: colorScheme.onPrimary.withValues(alpha: 0.9),
),
@@ -146,9 +147,15 @@ class _HeroHeader extends StatelessWidget {
spacing: 8,
runSpacing: 8,
children: [
_Tag(icon: Icons.cloud_off_outlined, label: l10n.tagNoNetwork),
_Tag(
icon: Icons.cloud_off_outlined,
label: l10n.tagNoNetwork,
),
_Tag(icon: Icons.timer_outlined, label: l10n.tagIdleRpg),
_Tag(icon: Icons.storage_rounded, label: l10n.tagLocalSaves),
_Tag(
icon: Icons.storage_rounded,
label: l10n.tagLocalSaves,
),
],
);
},
@@ -208,7 +215,7 @@ class _ActionRow extends StatelessWidget {
TextButton.icon(
onPressed: onHallOfFame,
icon: const Icon(Icons.emoji_events_outlined),
label: const Text('Hall of Fame'),
label: Text(game_l10n.uiHallOfFame),
),
],
);
@@ -245,7 +252,7 @@ class _StatusCards extends StatelessWidget {
SizedBox(height: 16),
_InfoCard(
icon: Icons.checklist_rtl,
title: 'Todays focus',
title: game_l10n.frontTodayFocus,
points: [
'Set up scaffold + lints.',
'Wire seed theme and initial navigation shell.',

View File

@@ -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);
}
}

View File

@@ -186,5 +186,6 @@ class GameSessionController extends ChangeNotifier {
}
/// 사망 상태 여부
bool get isDead => _status == GameSessionStatus.dead || (_state?.isDead ?? false);
bool get isDead =>
_status == GameSessionStatus.dead || (_state?.isDead ?? false);
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
import 'package:askiineverdie/src/core/model/skill.dart';
/// 활성 버프 패널 위젯
@@ -18,10 +19,10 @@ class ActiveBuffPanel extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (activeBuffs.isEmpty) {
return const Center(
return Center(
child: Text(
'No active buffs',
style: TextStyle(
l10n.uiNoActiveBuffs,
style: const TextStyle(
fontSize: 11,
color: Colors.grey,
fontStyle: FontStyle.italic,
@@ -43,10 +44,7 @@ class ActiveBuffPanel extends StatelessWidget {
/// 개별 버프 행 위젯
class _BuffRow extends StatelessWidget {
const _BuffRow({
required this.buff,
required this.currentMs,
});
const _BuffRow({required this.buff, required this.currentMs});
final ActiveBuff buff;
final int currentMs;
@@ -66,11 +64,7 @@ class _BuffRow extends StatelessWidget {
Row(
children: [
// 버프 아이콘
const Icon(
Icons.trending_up,
size: 14,
color: Colors.lightBlue,
),
const Icon(Icons.trending_up, size: 14, color: Colors.lightBlue),
const SizedBox(width: 4),
// 버프 이름
@@ -92,8 +86,9 @@ class _BuffRow extends StatelessWidget {
style: TextStyle(
fontSize: 10,
color: remainingMs < 3000 ? Colors.orange : Colors.grey,
fontWeight:
remainingMs < 3000 ? FontWeight.bold : FontWeight.normal,
fontWeight: remainingMs < 3000
? FontWeight.bold
: FontWeight.normal,
),
),
],
@@ -117,11 +112,7 @@ class _BuffRow extends StatelessWidget {
// 효과 목록
if (modifiers.isNotEmpty) ...[
const SizedBox(height: 2),
Wrap(
spacing: 6,
runSpacing: 2,
children: modifiers,
),
Wrap(spacing: 6, runSpacing: 2, children: modifiers),
],
],
),
@@ -134,35 +125,43 @@ class _BuffRow extends StatelessWidget {
final effect = buff.effect;
if (effect.atkModifier != 0) {
modifiers.add(_ModifierChip(
label: 'ATK',
value: effect.atkModifier,
isPositive: effect.atkModifier > 0,
));
modifiers.add(
_ModifierChip(
label: 'ATK',
value: effect.atkModifier,
isPositive: effect.atkModifier > 0,
),
);
}
if (effect.defModifier != 0) {
modifiers.add(_ModifierChip(
label: 'DEF',
value: effect.defModifier,
isPositive: effect.defModifier > 0,
));
modifiers.add(
_ModifierChip(
label: 'DEF',
value: effect.defModifier,
isPositive: effect.defModifier > 0,
),
);
}
if (effect.criRateModifier != 0) {
modifiers.add(_ModifierChip(
label: 'CRI',
value: effect.criRateModifier,
isPositive: effect.criRateModifier > 0,
));
modifiers.add(
_ModifierChip(
label: 'CRI',
value: effect.criRateModifier,
isPositive: effect.criRateModifier > 0,
),
);
}
if (effect.evasionModifier != 0) {
modifiers.add(_ModifierChip(
label: 'EVA',
value: effect.evasionModifier,
isPositive: effect.evasionModifier > 0,
));
modifiers.add(
_ModifierChip(
label: 'EVA',
value: effect.evasionModifier,
isPositive: effect.evasionModifier > 0,
),
);
}
return modifiers;

View File

@@ -117,6 +117,9 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
// 전투 이벤트 동기화용 (Phase 5)
int? _lastEventTimestamp;
bool _showCriticalEffect = false;
bool _showBlockEffect = false;
bool _showParryEffect = false;
bool _showSkillEffect = false;
// 특수 애니메이션 프레임 수는 ascii_animation_type.dart의
// specialAnimationFrameCounts 상수 사용
@@ -177,33 +180,114 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
// 전투 모드가 아니면 무시
if (_animationMode != AnimationMode.battle) return;
// 이벤트 타입에 따라 페이즈 강제 전환
final (targetPhase, isCritical) = switch (event.type) {
// 이벤트 타입에 따라 페이즈 및 효과 결정
final (
targetPhase,
isCritical,
isBlock,
isParry,
isSkill,
) = switch (event.type) {
// 플레이어 공격 → attack 페이즈
CombatEventType.playerAttack => (BattlePhase.attack, event.isCritical),
CombatEventType.playerSkill => (BattlePhase.attack, event.isCritical),
CombatEventType.playerAttack => (
BattlePhase.attack,
event.isCritical,
false,
false,
false,
),
// 스킬 사용 → attack 페이즈 + 스킬 이펙트
CombatEventType.playerSkill => (
BattlePhase.attack,
event.isCritical,
false,
false,
true,
),
// 몬스터 공격/플레이어 피격 → hit 페이즈
CombatEventType.monsterAttack => (BattlePhase.hit, false),
CombatEventType.playerBlock => (BattlePhase.hit, false),
CombatEventType.playerParry => (BattlePhase.hit, false),
// 몬스터 공격 → hit 페이즈
CombatEventType.monsterAttack => (
BattlePhase.hit,
false,
false,
false,
false,
),
// 블록 → hit 페이즈 + 블록 이펙트
CombatEventType.playerBlock => (
BattlePhase.hit,
false,
true,
false,
false,
),
// 패리 → hit 페이즈 + 패리 이펙트
CombatEventType.playerParry => (
BattlePhase.hit,
false,
false,
true,
false,
),
// 회피 → recover 페이즈 (빠른 회피 동작)
CombatEventType.playerEvade => (BattlePhase.recover, false),
CombatEventType.monsterEvade => (BattlePhase.idle, false),
CombatEventType.playerEvade => (
BattlePhase.recover,
false,
false,
false,
false,
),
CombatEventType.monsterEvade => (
BattlePhase.idle,
false,
false,
false,
false,
),
// 회복/버프 → idle 페이즈 유지
CombatEventType.playerHeal => (BattlePhase.idle, false),
CombatEventType.playerBuff => (BattlePhase.idle, false),
CombatEventType.playerHeal => (
BattlePhase.idle,
false,
false,
false,
false,
),
CombatEventType.playerBuff => (
BattlePhase.idle,
false,
false,
false,
false,
),
// DOT 틱 → attack 페이즈 (지속 피해)
CombatEventType.dotTick => (BattlePhase.attack, false),
CombatEventType.dotTick => (
BattlePhase.attack,
false,
false,
false,
false,
),
// 물약 사용 → idle 페이즈 유지
CombatEventType.playerPotion => (BattlePhase.idle, false),
CombatEventType.playerPotion => (
BattlePhase.idle,
false,
false,
false,
false,
),
// 물약 드랍 → idle 페이즈 유지
CombatEventType.potionDrop => (BattlePhase.idle, false),
CombatEventType.potionDrop => (
BattlePhase.idle,
false,
false,
false,
false,
),
};
setState(() {
@@ -211,6 +295,9 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
_battleSubFrame = 0;
_phaseFrameCount = 0;
_showCriticalEffect = isCritical;
_showBlockEffect = isBlock;
_showParryEffect = isParry;
_showSkillEffect = isSkill;
// 페이즈 인덱스 동기화
_phaseIndex = _battlePhaseSequence.indexWhere((p) => p.$1 == targetPhase);
@@ -322,8 +409,11 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
_phaseIndex = (_phaseIndex + 1) % _battlePhaseSequence.length;
_phaseFrameCount = 0;
_battleSubFrame = 0;
// 크리티컬 이펙트 리셋 (페이즈 전환 시)
// 이펙트 리셋 (페이즈 전환 시)
_showCriticalEffect = false;
_showBlockEffect = false;
_showParryEffect = false;
_showSkillEffect = false;
} else {
_battleSubFrame++;
}
@@ -340,21 +430,22 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
/// 현재 애니메이션 레이어 생성
List<AsciiLayer> _composeLayers() {
return switch (_animationMode) {
AnimationMode.battle => _battleComposer?.composeLayers(
_battlePhase,
_battleSubFrame,
widget.monsterBaseName,
_environment,
_globalTick,
) ??
[AsciiLayer.empty()],
AnimationMode.battle =>
_battleComposer?.composeLayers(
_battlePhase,
_battleSubFrame,
widget.monsterBaseName,
_environment,
_globalTick,
) ??
[AsciiLayer.empty()],
AnimationMode.walking => _walkingComposer.composeLayers(_globalTick),
AnimationMode.town => _townComposer.composeLayers(_globalTick),
AnimationMode.special => _specialComposer.composeLayers(
_currentSpecialAnimation ?? AsciiAnimationType.levelUp,
_currentFrame,
_globalTick,
),
_currentSpecialAnimation ?? AsciiAnimationType.levelUp,
_currentFrame,
_globalTick,
),
};
}
@@ -363,17 +454,38 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
// Phase 7: 고정 4색 팔레트 사용 (colorTheme 무시)
const bgColor = AsciiColors.background;
// 테두리 효과 결정 (특수 애니메이션 또는 크리티컬 히트)
// 테두리 효과 결정 (전투 이벤트 또는 특수 애니메이션)
final isSpecial = _currentSpecialAnimation != null;
Border? borderEffect;
if (_showCriticalEffect) {
// 크리티컬 히트: 노란색 테두리 (Phase 5)
borderEffect =
Border.all(color: Colors.yellow.withValues(alpha: 0.8), width: 2);
// 크리티컬 히트: 노란색 테두리
borderEffect = Border.all(
color: Colors.yellow.withValues(alpha: 0.8),
width: 2,
);
} else if (_showBlockEffect) {
// 블록 (방패 방어): 파란색 테두리
borderEffect = Border.all(
color: Colors.blue.withValues(alpha: 0.8),
width: 2,
);
} else if (_showParryEffect) {
// 패리 (무기 쳐내기): 주황색 테두리
borderEffect = Border.all(
color: Colors.orange.withValues(alpha: 0.8),
width: 2,
);
} else if (_showSkillEffect) {
// 스킬 사용: 마젠타 테두리
borderEffect = Border.all(
color: Colors.purple.withValues(alpha: 0.8),
width: 2,
);
} else if (isSpecial) {
// 특수 애니메이션: 시안 테두리
borderEffect =
Border.all(color: AsciiColors.positive.withValues(alpha: 0.5));
borderEffect = Border.all(
color: AsciiColors.positive.withValues(alpha: 0.5),
);
}
return Container(

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
import 'package:askiineverdie/data/story_data.dart';
/// 시네마틱 뷰 위젯 (Phase 9: Cinematic UI)
@@ -162,12 +163,9 @@ class _CinematicViewState extends State<CinematicView>
right: 16,
child: TextButton(
onPressed: _skip,
child: const Text(
'SKIP',
style: TextStyle(
color: Colors.white54,
fontSize: 14,
),
child: Text(
l10n.uiSkip,
style: const TextStyle(color: Colors.white54, fontSize: 14),
),
),
),
@@ -178,7 +176,7 @@ class _CinematicViewState extends State<CinematicView>
left: 0,
right: 0,
child: Text(
'Tap to continue',
l10n.uiTapToContinue,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.3),
fontSize: 12,
@@ -246,8 +244,8 @@ class _ProgressDots extends StatelessWidget {
color: isActive
? Colors.cyan
: isPast
? Colors.cyan.withValues(alpha: 0.5)
: Colors.white.withValues(alpha: 0.2),
? Colors.cyan.withValues(alpha: 0.5)
: Colors.white.withValues(alpha: 0.2),
),
);
}),

View File

@@ -148,7 +148,10 @@ class _LogEntryTile extends StatelessWidget {
(Color?, IconData?) _getStyleForType(CombatLogType type) {
return switch (type) {
CombatLogType.normal => (null, null),
CombatLogType.damage => (Colors.red.shade300, Icons.local_fire_department),
CombatLogType.damage => (
Colors.red.shade300,
Icons.local_fire_department,
),
CombatLogType.heal => (Colors.green.shade300, Icons.healing),
CombatLogType.levelUp => (Colors.amber, Icons.arrow_upward),
CombatLogType.questComplete => (Colors.blue.shade300, Icons.check_circle),
@@ -158,7 +161,10 @@ class _LogEntryTile extends StatelessWidget {
CombatLogType.evade => (Colors.cyan.shade300, Icons.directions_run),
CombatLogType.block => (Colors.blueGrey.shade300, Icons.shield),
CombatLogType.parry => (Colors.teal.shade300, Icons.sports_kabaddi),
CombatLogType.monsterAttack => (Colors.deepOrange.shade300, Icons.dangerous),
CombatLogType.monsterAttack => (
Colors.deepOrange.shade300,
Icons.dangerous,
),
CombatLogType.buff => (Colors.lightBlue.shade300, Icons.trending_up),
CombatLogType.dotTick => (Colors.deepPurple.shade300, Icons.whatshot),
CombatLogType.potion => (Colors.pink.shade300, Icons.local_drink),

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
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/model/combat_event.dart';
import 'package:askiineverdie/src/core/model/game_state.dart';
@@ -109,7 +111,7 @@ class DeathOverlay extends StatelessWidget {
),
const SizedBox(height: 16),
Text(
'YOU DIED',
l10n.deathYouDied,
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
@@ -133,7 +135,7 @@ class DeathOverlay extends StatelessWidget {
),
const SizedBox(height: 4),
Text(
'Level ${deathInfo.levelAtDeath} ${traits.klass}',
'Level ${deathInfo.levelAtDeath} ${GameDataL10n.getKlassName(context, traits.klass)}',
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
@@ -174,9 +176,9 @@ class DeathOverlay extends StatelessWidget {
String _getDeathCauseText() {
return switch (deathInfo.cause) {
DeathCause.monster => 'Killed by ${deathInfo.killerName}',
DeathCause.selfDamage => 'Self-inflicted damage',
DeathCause.environment => 'Environmental hazard',
DeathCause.monster => l10n.deathKilledBy(deathInfo.killerName),
DeathCause.selfDamage => l10n.deathSelfInflicted,
DeathCause.environment => l10n.deathEnvironmentalHazard,
};
}
@@ -210,7 +212,7 @@ class DeathOverlay extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Sacrificed to Resurrect',
l10n.deathSacrificedToResurrect,
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
@@ -234,8 +236,8 @@ class DeathOverlay extends StatelessWidget {
_buildInfoRow(
context,
icon: Icons.check_circle_outline,
label: 'Equipment',
value: 'No sacrifice needed',
label: l10n.deathEquipment,
value: l10n.deathNoSacrificeNeeded,
isNegative: false,
),
const SizedBox(height: 8),
@@ -243,7 +245,7 @@ class DeathOverlay extends StatelessWidget {
_buildInfoRow(
context,
icon: Icons.monetization_on_outlined,
label: 'Gold Remaining',
label: l10n.deathGoldRemaining,
value: _formatGold(deathInfo.goldAtDeath),
isNegative: false,
),
@@ -306,7 +308,7 @@ class DeathOverlay extends StatelessWidget {
child: FilledButton.icon(
onPressed: onResurrect,
icon: const Icon(Icons.replay),
label: const Text('Resurrect'),
label: Text(l10n.deathResurrect),
style: FilledButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
padding: const EdgeInsets.symmetric(vertical: 16),
@@ -324,7 +326,7 @@ class DeathOverlay extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Combat Log',
l10n.deathCombatLog,
style: theme.textTheme.labelMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
fontWeight: FontWeight.bold,
@@ -378,69 +380,70 @@ class DeathOverlay extends StatelessWidget {
/// 전투 이벤트를 아이콘, 색상, 메시지로 포맷
(IconData, Color, String) _formatCombatEvent(CombatEvent event) {
final target = event.targetName ?? '';
return switch (event.type) {
CombatEventType.playerAttack => (
event.isCritical ? Icons.flash_on : Icons.local_fire_department,
event.isCritical ? Colors.yellow.shade300 : Colors.green.shade300,
event.isCritical
? 'CRITICAL! ${event.damage} damage to ${event.targetName}'
: 'Hit ${event.targetName} for ${event.damage} damage',
),
event.isCritical ? Icons.flash_on : Icons.local_fire_department,
event.isCritical ? Colors.yellow.shade300 : Colors.green.shade300,
event.isCritical
? l10n.combatCritical(event.damage, target)
: l10n.combatYouHit(target, event.damage),
),
CombatEventType.monsterAttack => (
Icons.dangerous,
Colors.red.shade300,
'${event.targetName} hits you for ${event.damage} damage',
),
Icons.dangerous,
Colors.red.shade300,
l10n.combatMonsterHitsYou(target, event.damage),
),
CombatEventType.playerEvade => (
Icons.directions_run,
Colors.cyan.shade300,
'Evaded attack from ${event.targetName}',
),
Icons.directions_run,
Colors.cyan.shade300,
l10n.combatEvadedAttackFrom(target),
),
CombatEventType.monsterEvade => (
Icons.directions_run,
Colors.orange.shade300,
'${event.targetName} evaded your attack',
),
Icons.directions_run,
Colors.orange.shade300,
l10n.combatMonsterEvaded(target),
),
CombatEventType.playerBlock => (
Icons.shield,
Colors.blueGrey.shade300,
'Blocked ${event.targetName}\'s attack (${event.damage} reduced)',
),
Icons.shield,
Colors.blueGrey.shade300,
l10n.combatBlockedAttack(target, event.damage),
),
CombatEventType.playerParry => (
Icons.sports_kabaddi,
Colors.teal.shade300,
'Parried ${event.targetName}\'s attack (${event.damage} reduced)',
),
Icons.sports_kabaddi,
Colors.teal.shade300,
l10n.combatParriedAttack(target, event.damage),
),
CombatEventType.playerSkill => (
Icons.auto_fix_high,
Colors.purple.shade300,
'${event.skillName} deals ${event.damage} damage',
),
Icons.auto_fix_high,
Colors.purple.shade300,
l10n.combatSkillDamage(event.skillName ?? '', event.damage),
),
CombatEventType.playerHeal => (
Icons.healing,
Colors.green.shade300,
'Healed for ${event.healAmount} HP',
),
Icons.healing,
Colors.green.shade300,
l10n.combatHealedFor(event.healAmount),
),
CombatEventType.playerBuff => (
Icons.trending_up,
Colors.lightBlue.shade300,
'${event.skillName} activated',
),
Icons.trending_up,
Colors.lightBlue.shade300,
l10n.combatBuffActivated(event.skillName ?? ''),
),
CombatEventType.dotTick => (
Icons.whatshot,
Colors.deepOrange.shade300,
'${event.skillName} ticks for ${event.damage} damage',
),
Icons.whatshot,
Colors.deepOrange.shade300,
l10n.combatDotTick(event.skillName ?? '', event.damage),
),
CombatEventType.playerPotion => (
Icons.local_drink,
Colors.lightGreen.shade300,
'${event.skillName}: +${event.healAmount} ${event.targetName}',
),
Icons.local_drink,
Colors.lightGreen.shade300,
l10n.combatPotionUsed(event.skillName ?? '', event.healAmount, target),
),
CombatEventType.potionDrop => (
Icons.card_giftcard,
Colors.lime.shade300,
'Dropped: ${event.skillName}',
),
Icons.card_giftcard,
Colors.lime.shade300,
l10n.combatPotionDrop(event.skillName ?? ''),
),
};
}
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
import 'package:askiineverdie/src/core/engine/item_service.dart';
import 'package:askiineverdie/src/core/model/equipment_item.dart';
import 'package:askiineverdie/src/core/model/equipment_slot.dart';
@@ -135,7 +136,7 @@ class _EmptySlotTile extends StatelessWidget {
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
leading: _SlotIcon(slot: slot, isEmpty: true),
title: Text(
'[${_getSlotName(slot)}] (empty)',
'[${_getSlotName(slot)}] ${l10n.uiEmpty}',
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade600,
@@ -222,10 +223,7 @@ class _TotalScoreHeader extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.blueGrey.shade700,
Colors.blueGrey.shade600,
],
colors: [Colors.blueGrey.shade700, Colors.blueGrey.shade600],
),
borderRadius: BorderRadius.circular(8),
boxShadow: [
@@ -239,11 +237,7 @@ class _TotalScoreHeader extends StatelessWidget {
child: Row(
children: [
// 장비 아이콘
const Icon(
Icons.shield,
size: 20,
color: Colors.white70,
),
const Icon(Icons.shield, size: 20, color: Colors.white70),
const SizedBox(width: 8),
// 총합 점수
@@ -251,12 +245,9 @@ class _TotalScoreHeader extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Equipment Score',
style: TextStyle(
fontSize: 10,
color: Colors.white70,
),
Text(
l10n.uiEquipmentScore,
style: const TextStyle(fontSize: 10, color: Colors.white70),
),
Text(
'$totalScore',
@@ -304,46 +295,80 @@ class _StatsGrid extends StatelessWidget {
final entries = <_StatEntry>[];
// 공격 스탯
if (stats.atk > 0) entries.add(_StatEntry('ATK', '+${stats.atk}'));
if (stats.magAtk > 0) entries.add(_StatEntry('MATK', '+${stats.magAtk}'));
if (stats.atk > 0) entries.add(_StatEntry(l10n.statAtk, '+${stats.atk}'));
if (stats.magAtk > 0) {
entries.add(_StatEntry(l10n.statMAtk, '+${stats.magAtk}'));
}
if (stats.criRate > 0) {
entries.add(_StatEntry('CRI', '${(stats.criRate * 100).toStringAsFixed(1)}%'));
entries.add(
_StatEntry(l10n.statCri, '${(stats.criRate * 100).toStringAsFixed(1)}%'),
);
}
if (stats.parryRate > 0) {
entries.add(_StatEntry('PARRY', '${(stats.parryRate * 100).toStringAsFixed(1)}%'));
entries.add(
_StatEntry(
l10n.statParry,
'${(stats.parryRate * 100).toStringAsFixed(1)}%',
),
);
}
// 방어 스탯
if (stats.def > 0) entries.add(_StatEntry('DEF', '+${stats.def}'));
if (stats.magDef > 0) entries.add(_StatEntry('MDEF', '+${stats.magDef}'));
if (stats.def > 0) entries.add(_StatEntry(l10n.statDef, '+${stats.def}'));
if (stats.magDef > 0) {
entries.add(_StatEntry(l10n.statMDef, '+${stats.magDef}'));
}
if (stats.blockRate > 0) {
entries.add(_StatEntry('BLOCK', '${(stats.blockRate * 100).toStringAsFixed(1)}%'));
entries.add(
_StatEntry(
l10n.statBlock,
'${(stats.blockRate * 100).toStringAsFixed(1)}%',
),
);
}
if (stats.evasion > 0) {
entries.add(_StatEntry('EVA', '${(stats.evasion * 100).toStringAsFixed(1)}%'));
entries.add(
_StatEntry(l10n.statEva, '${(stats.evasion * 100).toStringAsFixed(1)}%'),
);
}
// 자원 스탯
if (stats.hpBonus > 0) entries.add(_StatEntry('HP', '+${stats.hpBonus}'));
if (stats.mpBonus > 0) entries.add(_StatEntry('MP', '+${stats.mpBonus}'));
if (stats.hpBonus > 0) {
entries.add(_StatEntry(l10n.statHp, '+${stats.hpBonus}'));
}
if (stats.mpBonus > 0) {
entries.add(_StatEntry(l10n.statMp, '+${stats.mpBonus}'));
}
// 능력치 보너스
if (stats.strBonus > 0) entries.add(_StatEntry('STR', '+${stats.strBonus}'));
if (stats.conBonus > 0) entries.add(_StatEntry('CON', '+${stats.conBonus}'));
if (stats.dexBonus > 0) entries.add(_StatEntry('DEX', '+${stats.dexBonus}'));
if (stats.intBonus > 0) entries.add(_StatEntry('INT', '+${stats.intBonus}'));
if (stats.wisBonus > 0) entries.add(_StatEntry('WIS', '+${stats.wisBonus}'));
if (stats.chaBonus > 0) entries.add(_StatEntry('CHA', '+${stats.chaBonus}'));
if (stats.strBonus > 0) {
entries.add(_StatEntry(l10n.statStr, '+${stats.strBonus}'));
}
if (stats.conBonus > 0) {
entries.add(_StatEntry(l10n.statCon, '+${stats.conBonus}'));
}
if (stats.dexBonus > 0) {
entries.add(_StatEntry(l10n.statDex, '+${stats.dexBonus}'));
}
if (stats.intBonus > 0) {
entries.add(_StatEntry(l10n.statInt, '+${stats.intBonus}'));
}
if (stats.wisBonus > 0) {
entries.add(_StatEntry(l10n.statWis, '+${stats.wisBonus}'));
}
if (stats.chaBonus > 0) {
entries.add(_StatEntry(l10n.statCha, '+${stats.chaBonus}'));
}
// 무기 공속
if (slot == EquipmentSlot.weapon && stats.attackSpeed > 0) {
entries.add(_StatEntry('SPEED', '${stats.attackSpeed}ms'));
entries.add(_StatEntry(l10n.statSpeed, '${stats.attackSpeed}ms'));
}
if (entries.isEmpty) {
return const Text(
'No bonus stats',
style: TextStyle(fontSize: 10, color: Colors.grey),
return Text(
l10n.uiNoBonusStats,
style: const TextStyle(fontSize: 10, color: Colors.grey),
);
}
@@ -406,7 +431,7 @@ class _ItemMetaRow extends StatelessWidget {
return Row(
children: [
Text(
'Lv.${item.level}',
l10n.uiLevel(item.level),
style: const TextStyle(fontSize: 9, color: Colors.grey),
),
const SizedBox(width: 8),
@@ -420,7 +445,7 @@ class _ItemMetaRow extends StatelessWidget {
),
const SizedBox(width: 8),
Text(
'Wt.${item.weight}',
l10n.uiWeight(item.weight),
style: const TextStyle(fontSize: 9, color: Colors.grey),
),
],
@@ -441,16 +466,16 @@ class _ItemMetaRow extends StatelessWidget {
/// 슬롯 이름 반환
String _getSlotName(EquipmentSlot slot) {
return switch (slot) {
EquipmentSlot.weapon => 'Weapon',
EquipmentSlot.shield => 'Shield',
EquipmentSlot.helm => 'Helm',
EquipmentSlot.hauberk => 'Hauberk',
EquipmentSlot.brassairts => 'Brassairts',
EquipmentSlot.vambraces => 'Vambraces',
EquipmentSlot.gauntlets => 'Gauntlets',
EquipmentSlot.gambeson => 'Gambeson',
EquipmentSlot.cuisses => 'Cuisses',
EquipmentSlot.greaves => 'Greaves',
EquipmentSlot.sollerets => 'Sollerets',
EquipmentSlot.weapon => l10n.slotWeapon,
EquipmentSlot.shield => l10n.slotShield,
EquipmentSlot.helm => l10n.slotHelm,
EquipmentSlot.hauberk => l10n.slotHauberk,
EquipmentSlot.brassairts => l10n.slotBrassairts,
EquipmentSlot.vambraces => l10n.slotVambraces,
EquipmentSlot.gauntlets => l10n.slotGauntlets,
EquipmentSlot.gambeson => l10n.slotGambeson,
EquipmentSlot.cuisses => l10n.slotCuisses,
EquipmentSlot.greaves => l10n.slotGreaves,
EquipmentSlot.sollerets => l10n.slotSollerets,
};
}

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
/// HP/MP 바 위젯 (Phase 8: 변화 시 시각 효과)
///
/// - HP가 20% 미만일 때 빨간색 깜빡임
@@ -151,7 +153,8 @@ class _HpMpBarState extends State<HpMpBar> with TickerProviderStateMixin {
final hpRatio = widget.hpMax > 0 ? widget.hpCurrent / widget.hpMax : 0.0;
final mpRatio = widget.mpMax > 0 ? widget.mpCurrent / widget.mpMax : 0.0;
final hasMonster = widget.monsterHpCurrent != null &&
final hasMonster =
widget.monsterHpCurrent != null &&
widget.monsterHpMax != null &&
widget.monsterHpMax! > 0;
@@ -162,7 +165,7 @@ class _HpMpBarState extends State<HpMpBar> with TickerProviderStateMixin {
children: [
// HP 바 (플래시 효과 포함)
_buildAnimatedBar(
label: 'HP',
label: l10n.statHp,
current: widget.hpCurrent,
max: widget.hpMax,
ratio: hpRatio,
@@ -176,7 +179,7 @@ class _HpMpBarState extends State<HpMpBar> with TickerProviderStateMixin {
// MP 바 (플래시 효과 포함)
_buildAnimatedBar(
label: 'MP',
label: l10n.statMp,
current: widget.mpCurrent,
max: widget.mpMax,
ratio: mpRatio,
@@ -188,10 +191,7 @@ class _HpMpBarState extends State<HpMpBar> with TickerProviderStateMixin {
),
// 몬스터 HP 바 (전투 중일 때만)
if (hasMonster) ...[
const SizedBox(height: 8),
_buildMonsterBar(),
],
if (hasMonster) ...[const SizedBox(height: 8), _buildMonsterBar()],
],
),
);
@@ -228,7 +228,13 @@ class _HpMpBarState extends State<HpMpBar> with TickerProviderStateMixin {
),
child: Stack(
children: [
_buildBar(label: label, current: current, max: max, ratio: ratio, color: color),
_buildBar(
label: label,
current: current,
max: max,
ratio: ratio,
color: color,
),
// 플로팅 변화량 텍스트 (위로 떠오르며 사라짐)
if (change != 0 && flashController.value > 0.05)
@@ -340,8 +346,9 @@ class _HpMpBarState extends State<HpMpBar> with TickerProviderStateMixin {
child: LinearProgressIndicator(
value: ratio.clamp(0.0, 1.0),
backgroundColor: Colors.orange.withValues(alpha: 0.2),
valueColor:
const AlwaysStoppedAnimation<Color>(Colors.orange),
valueColor: const AlwaysStoppedAnimation<Color>(
Colors.orange,
),
minHeight: 8,
),
),

View File

@@ -40,20 +40,21 @@ class _NotificationOverlayState extends State<NotificationOverlay>
vsync: this,
);
_slideAnimation = Tween<Offset>(
begin: const Offset(0, -1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeOutBack,
));
_slideAnimation =
Tween<Offset>(begin: const Offset(0, -1), end: Offset.zero).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeOutBack,
),
);
_fadeAnimation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeIn),
);
_notificationSub =
widget.notificationService.notifications.listen(_onNotification);
_notificationSub = widget.notificationService.notifications.listen(
_onNotification,
);
_dismissSub = widget.notificationService.dismissals.listen(_onDismiss);
}
@@ -184,35 +185,35 @@ class _NotificationCard extends StatelessWidget {
(Color, IconData, Color) _getStyleForType(NotificationType type) {
return switch (type) {
NotificationType.levelUp => (
const Color(0xFF1565C0),
Icons.trending_up,
Colors.amber,
),
const Color(0xFF1565C0),
Icons.trending_up,
Colors.amber,
),
NotificationType.questComplete => (
const Color(0xFF2E7D32),
Icons.check_circle,
Colors.lightGreen,
),
const Color(0xFF2E7D32),
Icons.check_circle,
Colors.lightGreen,
),
NotificationType.actComplete => (
const Color(0xFF6A1B9A),
Icons.flag,
Colors.purpleAccent,
),
const Color(0xFF6A1B9A),
Icons.flag,
Colors.purpleAccent,
),
NotificationType.newSpell => (
const Color(0xFF4527A0),
Icons.auto_fix_high,
Colors.deepPurpleAccent,
),
const Color(0xFF4527A0),
Icons.auto_fix_high,
Colors.deepPurpleAccent,
),
NotificationType.newEquipment => (
const Color(0xFFE65100),
Icons.shield,
Colors.orange,
),
const Color(0xFFE65100),
Icons.shield,
Colors.orange,
),
NotificationType.bossDefeat => (
const Color(0xFFC62828),
Icons.whatshot,
Colors.redAccent,
),
const Color(0xFFC62828),
Icons.whatshot,
Colors.redAccent,
),
};
}
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
import 'package:askiineverdie/data/potion_data.dart';
import 'package:askiineverdie/src/core/model/potion.dart';
@@ -22,10 +23,10 @@ class PotionInventoryPanel extends StatelessWidget {
final potionEntries = _buildPotionEntries();
if (potionEntries.isEmpty) {
return const Center(
return Center(
child: Text(
'No potions',
style: TextStyle(
l10n.uiNoPotions,
style: const TextStyle(
fontSize: 11,
color: Colors.grey,
fontStyle: FontStyle.italic,
@@ -146,11 +147,7 @@ class _PotionRow extends StatelessWidget {
// 전투 중 사용 불가 표시
if (isUsedThisBattle) ...[
const SizedBox(width: 4),
const Icon(
Icons.block,
size: 12,
color: Colors.grey,
),
const Icon(Icons.block, size: 12, color: Colors.grey),
],
],
),
@@ -213,10 +210,7 @@ class _HealBadge extends StatelessWidget {
),
child: Text(
healText,
style: TextStyle(
fontSize: 9,
color: Colors.grey.shade700,
),
style: TextStyle(fontSize: 9, color: Colors.grey.shade700),
),
);
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
import 'package:askiineverdie/data/skill_data.dart';
import 'package:askiineverdie/src/core/model/game_state.dart';
import 'package:askiineverdie/src/core/model/skill.dart';
@@ -90,8 +91,8 @@ class _SkillPanelState extends State<SkillPanel>
final skillStates = widget.skillSystem.skillStates;
if (skillStates.isEmpty) {
return const Center(
child: Text('No skills', style: TextStyle(fontSize: 11)),
return Center(
child: Text(l10n.uiNoSkills, style: const TextStyle(fontSize: 11)),
);
}
@@ -143,7 +144,7 @@ class _SkillRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cooldownText = isReady
? 'Ready'
? l10n.uiReady
: '${(remainingMs / 1000).toStringAsFixed(1)}s';
final skillIcon = _getSkillIcon(skill.type);
@@ -192,9 +193,9 @@ class _SkillRow extends StatelessWidget {
color: elementColor.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(3),
),
child: const Text(
'DOT',
style: TextStyle(fontSize: 7, color: Colors.white70),
child: Text(
l10n.uiDot,
style: const TextStyle(fontSize: 7, color: Colors.white70),
),
),
const SizedBox(width: 4),
@@ -202,7 +203,7 @@ class _SkillRow extends StatelessWidget {
// 랭크
Text(
'Lv.$rank',
l10n.uiLevel(rank),
style: const TextStyle(fontSize: 9, color: Colors.grey),
),
const SizedBox(width: 4),
@@ -233,7 +234,9 @@ class _SkillRow extends StatelessWidget {
borderRadius: BorderRadius.circular(4),
boxShadow: [
BoxShadow(
color: skillColor.withValues(alpha: glowAnimation.value * 0.5),
color: skillColor.withValues(
alpha: glowAnimation.value * 0.5,
),
blurRadius: 8 * glowAnimation.value,
spreadRadius: 2 * glowAnimation.value,
),
@@ -332,11 +335,7 @@ class _ElementBadge extends StatelessWidget {
? Border.all(color: color.withValues(alpha: 0.7), width: 1)
: null,
),
child: Icon(
icon,
size: 10,
color: color,
),
child: Icon(icon, size: 10, color: color),
);
}
}

View File

@@ -137,8 +137,9 @@ class TaskProgressPanel extends StatelessWidget {
child: Text(
'${speedMultiplier}x',
style: TextStyle(
fontWeight:
speedMultiplier > 1 ? FontWeight.bold : FontWeight.normal,
fontWeight: speedMultiplier > 1
? FontWeight.bold
: FontWeight.normal,
color: speedMultiplier > 1
? Theme.of(context).colorScheme.primary
: null,
@@ -157,8 +158,9 @@ class TaskProgressPanel extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: LinearProgressIndicator(
value: progressValue,
backgroundColor:
Theme.of(context).colorScheme.primary.withValues(alpha: 0.2),
backgroundColor: Theme.of(
context,
).colorScheme.primary.withValues(alpha: 0.2),
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
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/model/hall_of_fame.dart';
import 'package:askiineverdie/src/core/storage/hall_of_fame_storage.dart';
@@ -36,10 +37,7 @@ class _HallOfFameScreenState extends State<HallOfFameScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Hall of Fame'),
centerTitle: true,
),
appBar: AppBar(title: Text(l10n.uiHallOfFame), centerTitle: true),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _buildContent(),
@@ -67,19 +65,13 @@ class _HallOfFameScreenState extends State<HallOfFameScreen> {
),
const SizedBox(height: 16),
Text(
'No heroes yet',
style: TextStyle(
fontSize: 20,
color: Colors.grey.shade600,
),
l10n.hofNoHeroes,
style: TextStyle(fontSize: 20, color: Colors.grey.shade600),
),
const SizedBox(height: 8),
Text(
'Defeat the Glitch God to enshrine your legend!',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade500,
),
l10n.hofDefeatGlitchGod,
style: TextStyle(fontSize: 14, color: Colors.grey.shade500),
textAlign: TextAlign.center,
),
],
@@ -106,22 +98,22 @@ class _HallOfFameScreenState extends State<HallOfFameScreen> {
topRight: Radius.circular(6),
),
),
child: const Row(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.emoji_events, color: Colors.white),
SizedBox(width: 8),
const Icon(Icons.emoji_events, color: Colors.white),
const SizedBox(width: 8),
Text(
'HALL OF FAME',
style: TextStyle(
l10n.uiHallOfFame.toUpperCase(),
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
),
SizedBox(width: 8),
Icon(Icons.emoji_events, color: Colors.white),
const SizedBox(width: 8),
const Icon(Icons.emoji_events, color: Colors.white),
],
),
),
@@ -132,10 +124,7 @@ class _HallOfFameScreenState extends State<HallOfFameScreen> {
itemCount: hallOfFame.entries.length,
itemBuilder: (context, index) {
final entry = hallOfFame.entries[index];
return _HallOfFameEntryCard(
entry: entry,
rank: index + 1,
);
return _HallOfFameEntryCard(entry: entry, rank: index + 1);
},
),
),
@@ -147,10 +136,7 @@ class _HallOfFameScreenState extends State<HallOfFameScreen> {
/// 명예의 전당 엔트리 카드
class _HallOfFameEntryCard extends StatelessWidget {
const _HallOfFameEntryCard({
required this.entry,
required this.rank,
});
const _HallOfFameEntryCard({required this.entry, required this.rank});
final HallOfFameEntry entry;
final int rank;
@@ -217,7 +203,7 @@ class _HallOfFameEntryCard extends StatelessWidget {
borderRadius: BorderRadius.circular(4),
),
child: Text(
'Lv.${entry.level}',
l10n.uiLevel(entry.level),
style: TextStyle(
color: Colors.blue.shade800,
fontWeight: FontWeight.bold,
@@ -232,10 +218,7 @@ class _HallOfFameEntryCard extends StatelessWidget {
Text(
'${GameDataL10n.getRaceName(context, entry.race)} '
'${GameDataL10n.getKlassName(context, entry.klass)}',
style: TextStyle(
fontSize: 13,
color: Colors.grey.shade700,
),
style: TextStyle(fontSize: 13, color: Colors.grey.shade700),
),
const SizedBox(height: 4),
// 통계
@@ -267,14 +250,15 @@ class _HallOfFameEntryCard extends StatelessWidget {
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Icon(Icons.calendar_today, size: 14, color: Colors.grey.shade500),
Icon(
Icons.calendar_today,
size: 14,
color: Colors.grey.shade500,
),
const SizedBox(height: 4),
Text(
entry.formattedClearedDate,
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade600,
),
style: TextStyle(fontSize: 11, color: Colors.grey.shade600),
),
],
),
@@ -362,22 +346,22 @@ class _GameClearDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Row(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.emoji_events, color: Colors.amber, size: 32),
SizedBox(width: 8),
Text('VICTORY!'),
SizedBox(width: 8),
Icon(Icons.emoji_events, color: Colors.amber, size: 32),
const Icon(Icons.emoji_events, color: Colors.amber, size: 32),
const SizedBox(width: 8),
Text(l10n.hofVictory),
const SizedBox(width: 8),
const Icon(Icons.emoji_events, color: Colors.amber, size: 32),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'You have defeated the Glitch God!',
style: TextStyle(fontSize: 16),
Text(
l10n.hofDefeatedGlitchGod,
style: const TextStyle(fontSize: 16),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
@@ -386,14 +370,11 @@ class _GameClearDialog extends StatelessWidget {
// 캐릭터 정보
Text(
'"${entry.characterName}"',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(
'${entry.race} ${entry.klass}',
'${GameDataL10n.getRaceName(context, entry.race)} ${GameDataL10n.getKlassName(context, entry.klass)}',
style: TextStyle(color: Colors.grey.shade600),
),
const SizedBox(height: 16),
@@ -401,16 +382,16 @@ class _GameClearDialog extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildStat('Level', '${entry.level}'),
_buildStat('Time', entry.formattedPlayTime),
_buildStat('Deaths', '${entry.totalDeaths}'),
_buildStat('Quests', '${entry.questsCompleted}'),
_buildStat(l10n.hofLevel, '${entry.level}'),
_buildStat(l10n.hofTime, entry.formattedPlayTime),
_buildStat(l10n.hofDeaths, '${entry.totalDeaths}'),
_buildStat(l10n.hofQuests, '${entry.questsCompleted}'),
],
),
const SizedBox(height: 16),
const Text(
'Your legend has been enshrined in the Hall of Fame!',
style: TextStyle(
Text(
l10n.hofLegendEnshrined,
style: const TextStyle(
fontStyle: FontStyle.italic,
color: Colors.amber,
),
@@ -424,14 +405,14 @@ class _GameClearDialog extends StatelessWidget {
Navigator.of(context).pop();
onViewHallOfFame();
},
child: const Text('View Hall of Fame'),
child: Text(l10n.hofViewHallOfFame),
),
FilledButton(
onPressed: () {
Navigator.of(context).pop();
onNewGame();
},
child: const Text('New Game'),
child: Text(l10n.hofNewGame),
),
],
);
@@ -442,17 +423,11 @@ class _GameClearDialog extends StatelessWidget {
children: [
Text(
value,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Text(
label,
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade600,
),
style: TextStyle(fontSize: 11, color: Colors.grey.shade600),
),
],
);

View File

@@ -3,12 +3,14 @@ import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:askiineverdie/data/class_data.dart';
import 'package:askiineverdie/data/game_text_l10n.dart' as game_l10n;
import 'package:askiineverdie/data/race_data.dart';
import 'package:askiineverdie/l10n/app_localizations.dart';
import 'package:askiineverdie/src/core/model/class_traits.dart';
import 'package:askiineverdie/src/core/model/game_state.dart';
import 'package:askiineverdie/src/core/model/race_traits.dart';
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart';
import 'package:askiineverdie/src/core/util/pq_logic.dart';
/// 캐릭터 생성 화면 (NewGuy.pas 포팅)
@@ -396,7 +398,10 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(L10n.of(context).race, style: Theme.of(context).textTheme.titleMedium),
Text(
L10n.of(context).race,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
SizedBox(
height: 300,
@@ -415,7 +420,7 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
: null,
),
title: Text(
race.name,
GameDataL10n.getRaceName(context, race.name),
style: TextStyle(
fontWeight: isSelected
? FontWeight.bold
@@ -445,7 +450,7 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
}
final passiveDesc = race.passives.isNotEmpty
? race.passives.map((p) => p.description).join(', ')
? race.passives.map((p) => _translateRacePassive(p)).join(', ')
: '';
return Column(
@@ -460,21 +465,35 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
Text(
passiveDesc,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
),
color: Theme.of(context).colorScheme.primary,
),
),
],
);
}
/// 종족 패시브 설명 번역
String _translateRacePassive(PassiveAbility passive) {
final percent = (passive.value * 100).round();
return switch (passive.type) {
PassiveType.hpBonus => game_l10n.passiveHpBonus(percent),
PassiveType.mpBonus => game_l10n.passiveMpBonus(percent),
PassiveType.defenseBonus => game_l10n.passiveDefenseBonus(percent),
PassiveType.magicDamageBonus => game_l10n.passiveMagicBonus(percent),
PassiveType.criticalBonus => game_l10n.passiveCritBonus(percent),
PassiveType.expBonus => passive.description,
PassiveType.deathEquipmentPreserve => passive.description,
};
}
String _statName(StatType type) {
return switch (type) {
StatType.str => 'STR',
StatType.con => 'CON',
StatType.dex => 'DEX',
StatType.intelligence => 'INT',
StatType.wis => 'WIS',
StatType.cha => 'CHA',
StatType.str => game_l10n.statStr,
StatType.con => game_l10n.statCon,
StatType.dex => game_l10n.statDex,
StatType.intelligence => game_l10n.statInt,
StatType.wis => game_l10n.statWis,
StatType.cha => game_l10n.statCha,
};
}
@@ -485,7 +504,10 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(L10n.of(context).classTitle, style: Theme.of(context).textTheme.titleMedium),
Text(
L10n.of(context).classTitle,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
SizedBox(
height: 300,
@@ -504,7 +526,7 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
: null,
),
title: Text(
klass.name,
GameDataL10n.getKlassName(context, klass.name),
style: TextStyle(
fontWeight: isSelected
? FontWeight.bold
@@ -534,7 +556,7 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
}
final passiveDesc = klass.passives.isNotEmpty
? klass.passives.map((p) => p.description).join(', ')
? klass.passives.map((p) => _translateClassPassive(p)).join(', ')
: '';
return Column(
@@ -549,10 +571,28 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
Text(
passiveDesc,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.secondary,
),
color: Theme.of(context).colorScheme.secondary,
),
),
],
);
}
/// 클래스 패시브 설명 번역
String _translateClassPassive(ClassPassive passive) {
final percent = (passive.value * 100).round();
return switch (passive.type) {
ClassPassiveType.hpBonus => game_l10n.passiveHpBonus(percent),
ClassPassiveType.physicalDamageBonus =>
game_l10n.passivePhysicalBonus(percent),
ClassPassiveType.defenseBonus => game_l10n.passiveDefenseBonus(percent),
ClassPassiveType.magicDamageBonus => game_l10n.passiveMagicBonus(percent),
ClassPassiveType.evasionBonus => game_l10n.passiveEvasionBonus(percent),
ClassPassiveType.criticalBonus => game_l10n.passiveCritBonus(percent),
ClassPassiveType.postCombatHeal => game_l10n.passiveHpRegen(percent),
ClassPassiveType.healingBonus => passive.description,
ClassPassiveType.multiAttack => passive.description,
ClassPassiveType.firstStrikeBonus => passive.description,
};
}
}