feat(ui): 일시 정지 버튼 추가 및 배속 버그 수정
- 게임 중 일시 정지/재개 버튼 추가 (테마 버튼 옆) - 5x 배속이 2x와 동일하게 작동하던 버그 수정 - progress_service.dart clamp 제한을 100ms에서 500ms로 확장 - ASCII 애니메이션 40x8 규격 통일 - townAnimation, walkingAnimation, levelUpAnimation 등 8줄로 통일 - 레거시 애니메이션 TextAlign.left로 정렬 문제 수정 - 캐릭터 프레임 구조 통일 (머리/몸통/다리 3줄) - 몬스터 크기 enum 실제 프레임 줄 수와 일치하도록 수정
This commit is contained in:
@@ -226,6 +226,11 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
},
|
||||
colorTheme: _colorTheme,
|
||||
onThemeCycle: _cycleColorTheme,
|
||||
isPaused: !widget.controller.isRunning,
|
||||
onPauseToggle: () async {
|
||||
await widget.controller.togglePause();
|
||||
setState(() {});
|
||||
},
|
||||
specialAnimation: _specialAnimation,
|
||||
weaponName: state.equipment.weapon,
|
||||
shieldName: state.equipment.shield,
|
||||
|
||||
@@ -104,6 +104,21 @@ class GameSessionController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 일시 정지 상태에서 재개
|
||||
Future<void> resume() async {
|
||||
if (_state == null || _status != GameSessionStatus.idle) return;
|
||||
await startNew(_state!, cheatsEnabled: _cheatsEnabled, isNewGame: false);
|
||||
}
|
||||
|
||||
/// 일시 정지/재개 토글
|
||||
Future<void> togglePause() async {
|
||||
if (isRunning) {
|
||||
await pause(saveOnStop: true);
|
||||
} else if (_state != null && _status == GameSessionStatus.idle) {
|
||||
await resume();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
final stop = _stopLoop(saveOnStop: false);
|
||||
|
||||
@@ -7,7 +7,6 @@ import 'package:askiineverdie/src/core/animation/ascii_animation_type.dart';
|
||||
import 'package:askiineverdie/src/core/animation/background_layer.dart';
|
||||
import 'package:askiineverdie/src/core/animation/battle_composer.dart';
|
||||
import 'package:askiineverdie/src/core/animation/character_frames.dart';
|
||||
import 'package:askiineverdie/src/core/animation/monster_colors.dart';
|
||||
import 'package:askiineverdie/src/core/animation/monster_size.dart';
|
||||
import 'package:askiineverdie/src/core/animation/weapon_category.dart';
|
||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||
@@ -230,6 +229,54 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 이펙트 문자에 색상을 적용한 TextSpan 생성
|
||||
TextSpan _buildColoredTextSpan(String text, TextStyle baseStyle) {
|
||||
final spans = <TextSpan>[];
|
||||
final buffer = StringBuffer();
|
||||
|
||||
// 이펙트 문자 정의
|
||||
const effectChars = {'*', '!', '=', '>', '<', '~'};
|
||||
|
||||
for (var i = 0; i < text.length; i++) {
|
||||
final char = text[i];
|
||||
|
||||
if (effectChars.contains(char)) {
|
||||
// 버퍼에 쌓인 일반 텍스트 추가
|
||||
if (buffer.isNotEmpty) {
|
||||
spans.add(TextSpan(text: buffer.toString(), style: baseStyle));
|
||||
buffer.clear();
|
||||
}
|
||||
|
||||
// 이펙트 문자에 색상 적용
|
||||
final effectColor = _getEffectColor(char);
|
||||
spans.add(TextSpan(
|
||||
text: char,
|
||||
style: baseStyle.copyWith(color: effectColor),
|
||||
));
|
||||
} else {
|
||||
buffer.write(char);
|
||||
}
|
||||
}
|
||||
|
||||
// 남은 일반 텍스트 추가
|
||||
if (buffer.isNotEmpty) {
|
||||
spans.add(TextSpan(text: buffer.toString(), style: baseStyle));
|
||||
}
|
||||
|
||||
return TextSpan(children: spans);
|
||||
}
|
||||
|
||||
/// 이펙트 문자별 색상 반환
|
||||
Color _getEffectColor(String char) {
|
||||
return switch (char) {
|
||||
'*' => Colors.orange, // 히트/폭발
|
||||
'!' => Colors.yellow, // 강조
|
||||
'=' || '>' || '<' => Colors.cyan, // 슬래시/찌르기
|
||||
'~' => Colors.purple, // 물결/마법
|
||||
_ => Colors.white,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final brightness = Theme.of(context).brightness;
|
||||
@@ -254,13 +301,8 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
_environment,
|
||||
_globalTick,
|
||||
);
|
||||
|
||||
// 히트 페이즈면 몬스터 색상 변경
|
||||
if (_battlePhase == BattlePhase.hit) {
|
||||
final monsterColorCategory =
|
||||
getMonsterColorCategory(widget.monsterBaseName);
|
||||
textColor = getMonsterColors(monsterColorCategory).hit;
|
||||
}
|
||||
// 이펙트는 텍스트 자체로 구분 (*, !, =, ~ 등)
|
||||
// 전체 색상 변경 제거 - 기본 테마 색상 유지
|
||||
} else {
|
||||
// 기존 레거시 시스템 사용
|
||||
final frameIndex =
|
||||
@@ -293,14 +335,16 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
.clamp(6.0, 14.0);
|
||||
|
||||
return Center(
|
||||
child: Text(
|
||||
frameText,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Courier',
|
||||
fontSize: fontSize,
|
||||
color: textColor,
|
||||
height: 1.2,
|
||||
letterSpacing: 0,
|
||||
child: RichText(
|
||||
text: _buildColoredTextSpan(
|
||||
frameText,
|
||||
TextStyle(
|
||||
fontFamily: 'Courier',
|
||||
fontSize: fontSize,
|
||||
color: textColor,
|
||||
height: 1.2,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
@@ -317,7 +361,7 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
height: 1.1,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -15,6 +15,8 @@ class TaskProgressPanel extends StatelessWidget {
|
||||
required this.onSpeedCycle,
|
||||
required this.colorTheme,
|
||||
required this.onThemeCycle,
|
||||
required this.isPaused,
|
||||
required this.onPauseToggle,
|
||||
this.specialAnimation,
|
||||
this.weaponName,
|
||||
this.shieldName,
|
||||
@@ -28,6 +30,10 @@ class TaskProgressPanel extends StatelessWidget {
|
||||
final AsciiColorTheme colorTheme;
|
||||
final VoidCallback onThemeCycle;
|
||||
|
||||
/// 일시 정지 상태
|
||||
final bool isPaused;
|
||||
final VoidCallback onPauseToggle;
|
||||
|
||||
/// 특수 애니메이션 (레벨업, 퀘스트 완료 등)
|
||||
final AsciiAnimationType? specialAnimation;
|
||||
|
||||
@@ -70,6 +76,8 @@ class TaskProgressPanel extends StatelessWidget {
|
||||
Row(
|
||||
children: [
|
||||
_buildThemeButton(context),
|
||||
const SizedBox(width: 4),
|
||||
_buildPauseButton(context),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
@@ -128,6 +136,29 @@ class TaskProgressPanel extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPauseButton(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 28,
|
||||
child: OutlinedButton(
|
||||
onPressed: onPauseToggle,
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
visualDensity: VisualDensity.compact,
|
||||
side: BorderSide(
|
||||
color: isPaused
|
||||
? Colors.orange.withValues(alpha: 0.7)
|
||||
: Theme.of(context).colorScheme.outline.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
isPaused ? Icons.play_arrow : Icons.pause,
|
||||
size: 16,
|
||||
color: isPaused ? Colors.orange : null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSpeedButton(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 28,
|
||||
|
||||
Reference in New Issue
Block a user