feat(animation): ASCII 애니메이션 시스템 구현

- TaskType별 애니메이션 (전투, 마을, 걷기)
- 몬스터 카테고리별 전투 애니메이션 (7종)
- 특수 애니메이션 (레벨업, 퀘스트 완료, Act 완료)
- 색상 테마 옵션 (green, amber, white, system)
- 테마 설정 SharedPreferences 저장
- 프로그레스 바를 상단으로 이동
This commit is contained in:
JiWoong Sul
2025-12-11 16:49:02 +09:00
parent b450bf2600
commit 2b10deba5d
6 changed files with 1306 additions and 68 deletions

View File

@@ -0,0 +1,157 @@
import 'package:flutter/material.dart';
import 'package:askiineverdie/src/core/animation/ascii_animation_data.dart';
import 'package:askiineverdie/src/core/animation/ascii_animation_type.dart';
import 'package:askiineverdie/src/core/model/game_state.dart';
import 'package:askiineverdie/src/features/game/widgets/ascii_animation_card.dart';
/// 상단 패널: ASCII 애니메이션 + Task Progress 바
class TaskProgressPanel extends StatelessWidget {
const TaskProgressPanel({
super.key,
required this.progress,
required this.speedMultiplier,
required this.onSpeedCycle,
required this.colorTheme,
required this.onThemeCycle,
this.specialAnimation,
});
final ProgressState progress;
final int speedMultiplier;
final VoidCallback onSpeedCycle;
final AsciiColorTheme colorTheme;
final VoidCallback onThemeCycle;
/// 특수 애니메이션 (레벨업, 퀘스트 완료 등)
final AsciiAnimationType? specialAnimation;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
border: Border(
bottom: BorderSide(color: Theme.of(context).dividerColor),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// ASCII 애니메이션 카드
SizedBox(
height: 120,
child: AsciiAnimationCard(
taskType: progress.currentTask.type,
monsterBaseName: progress.currentTask.monsterBaseName,
colorTheme: colorTheme,
specialAnimation: specialAnimation,
),
),
const SizedBox(height: 8),
// 상태 메시지 + 버튼들
Row(
children: [
_buildThemeButton(context),
const SizedBox(width: 8),
Expanded(
child: Text(
progress.currentTask.caption.isNotEmpty
? progress.currentTask.caption
: 'Welcome to Progress Quest!',
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
),
const SizedBox(width: 8),
_buildSpeedButton(context),
],
),
const SizedBox(height: 4),
// Task Progress 바
_buildProgressBar(context),
],
),
);
}
Widget _buildThemeButton(BuildContext context) {
final themeLabel = switch (colorTheme) {
AsciiColorTheme.green => 'G',
AsciiColorTheme.amber => 'A',
AsciiColorTheme.white => 'W',
AsciiColorTheme.system => 'S',
};
final themeColor = switch (colorTheme) {
AsciiColorTheme.green => const Color(0xFF00FF00),
AsciiColorTheme.amber => const Color(0xFFFFB000),
AsciiColorTheme.white => Colors.white,
AsciiColorTheme.system => Theme.of(context).colorScheme.primary,
};
return SizedBox(
height: 28,
child: OutlinedButton(
onPressed: onThemeCycle,
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 8),
visualDensity: VisualDensity.compact,
side: BorderSide(color: themeColor.withValues(alpha: 0.5)),
),
child: Text(
themeLabel,
style: TextStyle(
fontWeight: FontWeight.bold,
color: themeColor,
),
),
),
);
}
Widget _buildSpeedButton(BuildContext context) {
return SizedBox(
height: 28,
child: OutlinedButton(
onPressed: onSpeedCycle,
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 8),
visualDensity: VisualDensity.compact,
),
child: Text(
'${speedMultiplier}x',
style: TextStyle(
fontWeight:
speedMultiplier > 1 ? FontWeight.bold : FontWeight.normal,
color: speedMultiplier > 1
? Theme.of(context).colorScheme.primary
: null,
),
),
),
);
}
Widget _buildProgressBar(BuildContext context) {
final progressValue = progress.task.max > 0
? (progress.task.position / progress.task.max).clamp(0.0, 1.0)
: 0.0;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: LinearProgressIndicator(
value: progressValue,
backgroundColor:
Theme.of(context).colorScheme.primary.withValues(alpha: 0.2),
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
minHeight: 12,
),
);
}
}