diff --git a/assets/data/items.json b/assets/data/items.json index 51820d7..fbb707e 100644 --- a/assets/data/items.json +++ b/assets/data/items.json @@ -206,7 +206,7 @@ "id": "pot_lid", "name": "Pot Lid", "description": "It was used for cooking.", - "baseArmor": 1, + "baseArmor": 2, "slot": "shield", "price": 10, "image": "assets/images/items/pot_lid.png", diff --git a/assets/data/players.json b/assets/data/players.json index eeb70ea..e519a24 100644 --- a/assets/data/players.json +++ b/assets/data/players.json @@ -7,7 +7,7 @@ "baseAtk": 5, "baseDefense": 5, "baseDodge": 2, - "image": "assets/images/players/warrior.png" + "image": "assets/images/character/warrior.png" }, { "id": "rogue", diff --git a/assets/images/character/warrior.png b/assets/images/character/warrior.png new file mode 100644 index 0000000..e21cb9e Binary files /dev/null and b/assets/images/character/warrior.png differ diff --git a/assets/images/character/warrior_attack_1.png b/assets/images/character/warrior_attack_1.png new file mode 100644 index 0000000..f4f4eec Binary files /dev/null and b/assets/images/character/warrior_attack_1.png differ diff --git a/assets/images/character/warrior_attack_2.png b/assets/images/character/warrior_attack_2.png new file mode 100644 index 0000000..bff9db5 Binary files /dev/null and b/assets/images/character/warrior_attack_2.png differ diff --git a/lib/game/config/item_config.dart b/lib/game/config/item_config.dart index 04d3772..f0d8c53 100644 --- a/lib/game/config/item_config.dart +++ b/lib/game/config/item_config.dart @@ -13,5 +13,5 @@ class ItemConfig { }; // Loot Generation - static const double magicPrefixChance = 0.5; // 50% + static const double magicPrefixChance = 1.0; // 100% } diff --git a/lib/game/config/theme_config.dart b/lib/game/config/theme_config.dart index a9757f8..f138182 100644 --- a/lib/game/config/theme_config.dart +++ b/lib/game/config/theme_config.dart @@ -105,4 +105,13 @@ class ThemeConfig { static const Color riskSafe = Colors.green; static const Color riskNormal = Colors.blue; static const Color riskRisky = Colors.red; + + // Character Status Card + static const double characterIconSize = 60.0; + static const double playerImageSize = 200.0; + static const double enemyImageSize = 200.0; + static const Color playerImageBgColor = Colors.lightBlue; + static const double statusEffectFontSize = 10.0; + static const double intentFontSize = 12.0; + static const double intentIconSize = 16.0; } diff --git a/lib/game/data/player_table.dart b/lib/game/data/player_table.dart index ef2f752..526df14 100644 --- a/lib/game/data/player_table.dart +++ b/lib/game/data/player_table.dart @@ -44,6 +44,7 @@ class PlayerTemplate { baseDefense: baseDefense, baseDodge: baseDodge, // Use template value armor: 0, + image: image, // Pass image path ); } } diff --git a/lib/providers/battle_provider.dart b/lib/providers/battle_provider.dart index 1fc0b10..c848411 100644 --- a/lib/providers/battle_provider.dart +++ b/lib/providers/battle_provider.dart @@ -90,6 +90,15 @@ class BattleProvider with ChangeNotifier { turnCount = data['turnCount']; player = Character.fromJson(data['player']); + // [Fix] Update player image path from latest data (in case of legacy save data) + // This ensures that even if the save file has an old path, the UI uses the correct asset. + if (player.name == "Warrior") { + final template = PlayerTable.get("warrior"); + if (template != null && template.image != null) { + player.image = template.image; + } + } + _logManager.clear(); _addLog("Game Loaded! Resuming Stage $stage"); @@ -513,8 +522,7 @@ class BattleProvider with ChangeNotifier { } // Process Start-of-Turn Effects - final result = CombatCalculator.processStartTurnEffects(enemy); - bool canAct = !result['isStunned']; + bool canAct = _processStartTurnEffects(enemy); if (enemy.isDead) { _onVictory(); @@ -1074,7 +1082,7 @@ class BattleProvider with ChangeNotifier { } // Try applying status effects - _tryApplyStatusEffects(attacker, target); + _tryApplyStatusEffects(attacker, target, damageToHp); // If target is enemy, update intent to reflect potential status changes (e.g. Disarmed) if (target == enemy) { @@ -1105,13 +1113,18 @@ class BattleProvider with ChangeNotifier { } /// Tries to applyStatus effects from attacker's equipment to the target. - void _tryApplyStatusEffects(Character attacker, Character target) { + void _tryApplyStatusEffects(Character attacker, Character target, int damageToHp) { List effectsToApply = CombatCalculator.getAppliedEffects( attacker, random: _random, // Pass injected random ); for (var effect in effectsToApply) { + // Logic: Bleed requires HP damage (penetrating armor) + if (effect.type == StatusEffectType.bleed && damageToHp <= 0) { + continue; + } + target.addStatusEffect(effect); _addLog("Applied ${effect.type.name} to ${target.name}!"); } diff --git a/lib/screens.dart b/lib/screens.dart index 1e9f8e0..9e3bed3 100644 --- a/lib/screens.dart +++ b/lib/screens.dart @@ -4,3 +4,4 @@ export 'screens/inventory_screen.dart'; export 'screens/main_menu_screen.dart'; export 'screens/main_wrapper.dart'; export 'screens/settings_screen.dart'; +export 'screens/story_screen.dart'; \ No newline at end of file diff --git a/lib/screens/battle_screen.dart b/lib/screens/battle_screen.dart index 8e298c1..ce5f09c 100644 --- a/lib/screens/battle_screen.dart +++ b/lib/screens/battle_screen.dart @@ -11,6 +11,8 @@ import '../utils.dart'; import 'main_menu_screen.dart'; import '../game/config.dart'; +enum AnimationPhase { none, start, middle, end } + class BattleScreen extends StatefulWidget { const BattleScreen({super.key}); @@ -42,6 +44,22 @@ class _BattleScreenState extends State { // New State for Interactive Defense Animation int _lastTurnCount = -1; bool _hasShownEnemyDefense = false; + AnimationPhase _playerAnimPhase = AnimationPhase.none; + + String? _getOverrideImage(bool isPlayer) { + if (!isPlayer) + return null; // Enemy animation image logic can be added later + + if (_playerAnimPhase == AnimationPhase.start) { + return "assets/images/character/warrior_attack_1.png"; + } else if (_playerAnimPhase == AnimationPhase.middle) { + return null; // Middle phase now uses default image or another image + } else if (_playerAnimPhase == AnimationPhase.end) { + return "assets/images/character/warrior_attack_2.png"; + } + + return null; + } @override void initState() { @@ -348,28 +366,50 @@ class _BattleScreenState extends State { : event.risk; _playerAnimKey.currentState - ?.animateAttack(offset, () { - showEffect(); - context.read().handleImpact(event); + ?.animateAttack( + offset, + () { + showEffect(); + context.read().handleImpact(event); - if (event.risk == RiskLevel.risky && event.feedbackType == null) { - _shakeKey.currentState?.shake(); - RenderBox? stackBox = - _stackKey.currentContext?.findRenderObject() as RenderBox?; - if (stackBox != null) { - Offset localEnemyPos = stackBox.globalToLocal(enemyPos); - localEnemyPos += Offset( - enemyBox.size.width / 2, - enemyBox.size.height / 2, - ); - _explosionKey.currentState?.explode(localEnemyPos); + if (event.risk == RiskLevel.risky && + event.feedbackType == null) { + _shakeKey.currentState?.shake(); + RenderBox? stackBox = + _stackKey.currentContext?.findRenderObject() + as RenderBox?; + if (stackBox != null) { + Offset localEnemyPos = stackBox.globalToLocal(enemyPos); + localEnemyPos += Offset( + enemyBox.size.width / 2, + enemyBox.size.height / 2, + ); + _explosionKey.currentState?.explode(localEnemyPos); + } } - } - }, animRisk) + }, + animRisk, + onAnimationStart: () { + if (mounted) { + setState(() => _playerAnimPhase = AnimationPhase.start); + } + }, + onAnimationMiddle: () { + if (mounted) { + setState(() => _playerAnimPhase = AnimationPhase.middle); + } + }, + onAnimationEnd: () { + if (mounted) { + setState(() => _playerAnimPhase = AnimationPhase.end); + } + }, + ) .then((_) { if (mounted) { setState(() { _isPlayerAttacking = false; + _playerAnimPhase = AnimationPhase.none; }); } }); @@ -685,6 +725,7 @@ class _BattleScreenState extends State { key: _playerKey, animationKey: _playerAnimKey, hideStats: _isPlayerAttacking, + overrideImage: _getOverrideImage(true), ), ), // Enemy (Top Right) - Rendered Last (On Top) diff --git a/lib/screens/character_selection_screen.dart b/lib/screens/character_selection_screen.dart index dd3238b..38e611c 100644 --- a/lib/screens/character_selection_screen.dart +++ b/lib/screens/character_selection_screen.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers.dart'; import '../game/data.dart'; -import 'main_wrapper.dart'; +import 'story_screen.dart'; import '../widgets.dart'; import '../game/config.dart'; @@ -37,12 +37,12 @@ class CharacterSelectionScreen extends StatelessWidget { // Initialize Game context.read().initializeBattle(); - // Navigate to Game Screen (MainWrapper) + // Navigate to Story Screen first // Using pushReplacement to prevent going back to selection Navigator.pushAndRemoveUntil( context, MaterialPageRoute( - builder: (context) => const MainWrapper(), + builder: (context) => const StoryScreen(), ), (route) => false, ); diff --git a/lib/screens/story_screen.dart b/lib/screens/story_screen.dart new file mode 100644 index 0000000..bd05d8b --- /dev/null +++ b/lib/screens/story_screen.dart @@ -0,0 +1,167 @@ +import 'package:flutter/material.dart'; +import 'main_wrapper.dart'; +import '../widgets.dart'; +import '../game/config.dart'; + +class StoryScreen extends StatefulWidget { + const StoryScreen({super.key}); + + @override + State createState() => _StoryScreenState(); +} + +class _StoryScreenState extends State { + final PageController _pageController = PageController(); + int _currentPage = 0; + final int _totalPages = 3; + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + void _onSkip() { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (context) => const MainWrapper()), + (route) => false, + ); + } + + void _onNext() { + if (_currentPage < _totalPages - 1) { + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } else { + _onSkip(); // Start Game + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, // Dark background for story + body: Center( + child: ResponsiveContainer( + child: SafeArea( + child: Stack( + children: [ + // 1. PageView for Story Content + PageView( + controller: _pageController, + physics: + const NeverScrollableScrollPhysics(), // Disable swipe + onPageChanged: (index) { + setState(() { + _currentPage = index; + }); + }, + children: [ + _buildStoryPage(1), + _buildStoryPage(2), + _buildStoryPage(3), + ], + ), + + // 2. Skip Button (Top Right) + Positioned( + top: 16, + right: 16, + child: TextButton( + onPressed: _onSkip, + child: const Text( + "SKIP", + style: TextStyle( + color: ThemeConfig.textColorGrey, // Reverted to Grey + fontSize: + ThemeConfig.fontSizeMedium, // Reverted to Medium + fontWeight: FontWeight.bold, + ), + ), + ), + ), + + // 3. Next/Start Button (Bottom Center or Right) + Positioned( + bottom: 32, + left: 16, + right: 16, + child: Center( + child: SizedBox( + width: 200, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: ThemeConfig.btnActionActive, + padding: const EdgeInsets.symmetric(vertical: 16), + ), + onPressed: _onNext, + child: Text( + _currentPage == _totalPages - 1 + ? "START BATTLE" + : "NEXT", + style: const TextStyle( + color: ThemeConfig.textColorWhite, + fontSize: ThemeConfig.fontSizeHeader, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildStoryPage(int pageIndex) { + return Container( + color: Colors.black, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Placeholder for Image + Container( + width: 300, + height: 300, + decoration: BoxDecoration( + color: Colors.grey[800], + border: Border.all(color: ThemeConfig.textColorGrey), + ), + child: Center( + child: Icon(Icons.image, size: 64, color: Colors.grey[600]), + ), + ), + const SizedBox(height: 32), + Text( + "Story Image $pageIndex", + style: const TextStyle( + color: ThemeConfig.textColorWhite, + fontSize: ThemeConfig.fontSizeHeader, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32.0), + child: Text( + "This is the placeholder text for the story part $pageIndex. Describe the lore or setting here.", + textAlign: TextAlign.center, + style: const TextStyle( + color: ThemeConfig.textColorGrey, + fontSize: ThemeConfig.fontSizeMedium, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/battle/battle_animation_widget.dart b/lib/widgets/battle/battle_animation_widget.dart index 673b065..b3641bc 100644 --- a/lib/widgets/battle/battle_animation_widget.dart +++ b/lib/widgets/battle/battle_animation_widget.dart @@ -53,8 +53,13 @@ class BattleAnimationWidgetState extends State Future animateAttack( Offset targetOffset, VoidCallback onImpact, - RiskLevel risk, - ) async { + RiskLevel risk, { + VoidCallback? onAnimationStart, + VoidCallback? onAnimationMiddle, + VoidCallback? onAnimationEnd, + }) async { + // onAnimationStart?.call(); // Start Phase + if (risk == RiskLevel.safe || risk == RiskLevel.normal) { // Safe & Normal: Dash/Wobble without scale final isSafe = risk == RiskLevel.safe; @@ -75,9 +80,13 @@ class BattleAnimationWidgetState extends State await _translateController.forward(); if (!mounted) return; + + // onAnimationMiddle?.call(); // Middle Phase onImpact(); + await _translateController.reverse(); } else { + onAnimationStart?.call(); // Start Phase // Risky: Scale + Heavy Dash final attackScale = context.read().attackAnimScale; _scaleAnimation = Tween(begin: 1.0, end: attackScale).animate( @@ -91,6 +100,8 @@ class BattleAnimationWidgetState extends State await _scaleController.forward(); if (!mounted) return; + onAnimationMiddle?.call(); // Middle Phase + // 2. Dash to Target (Impact) // Adjust offset to prevent complete overlap (stop slightly short) since both share the same layer stack final adjustedOffset = targetOffset * 0.5; @@ -106,13 +117,17 @@ class BattleAnimationWidgetState extends State await _translateController.forward(); if (!mounted) return; + // onAnimationEnd?.call(); // End Phase (Moved before Impact) + // 3. Impact Callback (Shake) onImpact(); // 4. Return (Reset) _scaleController.reverse(); - _translateController.reverse(); + await _translateController.reverse(); } + + // onAnimationEnd removed from here } Future animateDefense(VoidCallback onImpact) async { diff --git a/lib/widgets/battle/character_status_card.dart b/lib/widgets/battle/character_status_card.dart index 91bded4..c2ac49f 100644 --- a/lib/widgets/battle/character_status_card.dart +++ b/lib/widgets/battle/character_status_card.dart @@ -12,6 +12,7 @@ class CharacterStatusCard extends StatelessWidget { final bool isTurn; final GlobalKey? animationKey; final bool hideStats; + final String? overrideImage; const CharacterStatusCard({ super.key, @@ -20,6 +21,7 @@ class CharacterStatusCard extends StatelessWidget { this.isTurn = false, this.animationKey, this.hideStats = false, + this.overrideImage, }); @override @@ -51,7 +53,7 @@ class CharacterStatusCard extends StatelessWidget { ), ), SizedBox( - width: 100, + width: ThemeConfig.playerImageSize, child: LinearProgressIndicator( value: character.totalMaxHp > 0 ? character.hp / character.totalMaxHp @@ -84,7 +86,7 @@ class CharacterStatusCard extends StatelessWidget { "${effect.type.name.toUpperCase()} (${effect.duration})", style: const TextStyle( color: ThemeConfig.effectText, - fontSize: 10, + fontSize: ThemeConfig.statusEffectFontSize, fontWeight: FontWeight.bold, ), ), @@ -92,64 +94,51 @@ class CharacterStatusCard extends StatelessWidget { }).toList(), ), ), - // Text( - // "ATK: ${character.totalAtk}", - // style: const TextStyle(color: ThemeConfig.textColorWhite), - // ), - // Text( - // "DEF: ${character.totalDefense}", - // style: const TextStyle(color: ThemeConfig.textColorWhite), - // ), - // Text( - // "LUCK: ${character.totalLuck}", - // style: const TextStyle(color: ThemeConfig.textColorWhite), - // ), ], ), ), - const SizedBox(height: 8), // 아이콘과 정보 사이 간격 - // 캐릭터 아이콘/이미지 영역 추가 + const SizedBox(height: 8), BattleAnimationWidget( key: animationKey, child: Container( - width: isPlayer ? 100 : 200, // 플레이어 100, 적 200 - height: isPlayer ? 100 : 200, // 플레이어 100, 적 200 + width: isPlayer + ? ThemeConfig.playerImageSize + : ThemeConfig.enemyImageSize, + height: isPlayer + ? ThemeConfig.playerImageSize + : ThemeConfig.enemyImageSize, decoration: BoxDecoration( - color: isPlayer ? Colors.lightBlue : null, + // color: isPlayer ? ThemeConfig.playerImageBgColor : null, borderRadius: BorderRadius.circular(8), ), child: Center( - child: isPlayer - ? const Icon( - Icons.person, - size: 60, - color: ThemeConfig.textColorWhite, - ) // 플레이어 아이콘 - : (character.image != null && character.image!.isNotEmpty) + child: (overrideImage != null || + (character.image != null && character.image!.isNotEmpty)) ? Image.asset( - character.image!, - width: 200, - height: 200, + overrideImage ?? character.image!, + width: isPlayer + ? ThemeConfig.playerImageSize + : ThemeConfig.enemyImageSize, + height: isPlayer + ? ThemeConfig.playerImageSize + : ThemeConfig.enemyImageSize, fit: BoxFit.contain, - // color: Colors.white, - // colorBlendMode: BlendMode.screen, errorBuilder: (context, error, stackTrace) { return const Icon( - Icons.psychology, - size: 60, + Icons.error_outline, + size: ThemeConfig.characterIconSize, color: ThemeConfig.textColorWhite, ); }, ) - : const Icon( - Icons.psychology, - size: 60, + : Icon( + isPlayer ? Icons.person : Icons.psychology, + size: ThemeConfig.characterIconSize, color: ThemeConfig.textColorWhite, - ), // 적 이미지 + ), ), ), ), - // const SizedBox(height: 8), // 아이콘과 정보 사이 간격 if (!isPlayer && !hideStats) Consumer( builder: (context, provider, child) { @@ -166,14 +155,6 @@ class CharacterStatusCard extends StatelessWidget { ), child: Column( children: [ - // Text( - // "INTENT", - // style: TextStyle( - // color: ThemeConfig.enemyIntentBorder, - // fontSize: 10, - // fontWeight: FontWeight.bold, - // ), - // ), Row( mainAxisSize: MainAxisSize.min, children: [ @@ -181,20 +162,18 @@ class CharacterStatusCard extends StatelessWidget { intent.type == EnemyActionType.attack ? Icons.flash_on : Icons.shield, - color: ThemeConfig.rarityRare, // Yellow - size: 16, + color: ThemeConfig.rarityRare, + size: ThemeConfig.intentIconSize, ), const SizedBox(width: 4), Flexible( - // Use Flexible to allow text to shrink child: FittedBox( - fit: - BoxFit.scaleDown, // Shrink text if too long + fit: BoxFit.scaleDown, child: Text( intent.description, style: const TextStyle( color: ThemeConfig.textColorWhite, - fontSize: 12, + fontSize: ThemeConfig.intentFontSize, ), ), ), diff --git a/lib/widgets/inventory/inventory_grid_widget.dart b/lib/widgets/inventory/inventory_grid_widget.dart index e3e3aa7..afcef61 100644 --- a/lib/widgets/inventory/inventory_grid_widget.dart +++ b/lib/widgets/inventory/inventory_grid_widget.dart @@ -86,6 +86,7 @@ class InventoryGridWidget extends StatelessWidget { Item item, ) { bool isShop = provider.currentStage.type == StageType.shop; + int sellPrice = (item.price * GameConfig.sellPriceMultiplier).floor(); showDialog( context: context, @@ -141,7 +142,7 @@ class InventoryGridWidget extends StatelessWidget { color: ThemeConfig.statGoldColor, ), const SizedBox(width: 10), - Text("${AppStrings.sell} (${item.price} G)"), + Text("${AppStrings.sell} ($sellPrice G)"), ], ), ), @@ -172,11 +173,13 @@ class InventoryGridWidget extends StatelessWidget { BattleProvider provider, Item item, ) { + int sellPrice = (item.price * GameConfig.sellPriceMultiplier).floor(); + showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text("Sell Item"), - content: Text("Sell ${item.name} for ${item.price} G?"), + content: Text("Sell ${item.name} for $sellPrice G?"), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), diff --git a/prompt/00_project_context_restore.md b/prompt/00_project_context_restore.md index 2277311..dc7b7ab 100644 --- a/prompt/00_project_context_restore.md +++ b/prompt/00_project_context_restore.md @@ -15,11 +15,12 @@ 1. **메인 메뉴 (`MainMenuScreen`):** 게임 시작, 이어하기(저장된 데이터 있을 시), 설정 버튼. 2. **캐릭터 선택 (`CharacterSelectionScreen`):** 'Warrior' 직업 구현 (스탯 확인 후 시작). -3. **메인 게임 (`MainWrapper`):** 하단 탭 네비게이션 (Battle / Inventory / Settings). -4. **설정 (`SettingsScreen`):** +3. **스토리 (`StoryScreen`):** 게임 시작 전 3페이지 분량의 스토리 컷신 제공 (Skip 가능). +4. **메인 게임 (`MainWrapper`):** 하단 탭 네비게이션 (Battle / Inventory / Settings). +5. **설정 (`SettingsScreen`):** - 적 애니메이션 활성화/비활성화 토글 (`SettingsProvider` 연동). - 게임 재시작, 메인 메뉴로 돌아가기 기능. -5. **반응형 레이아웃 (Responsive UI):** +6. **반응형 레이아웃 (Responsive UI):** - `ResponsiveContainer`를 통해 다양한 화면 크기 대응 (최대 너비/높이 제한). - Battle UI: 플레이어(좌하단) vs 적(우상단) 대각선 구도. @@ -44,9 +45,11 @@ - **애니메이션 및 타격감 (Visuals & Impact):** - **UI 주도 Impact 처리:** 애니메이션 타격 시점(`onImpact`)에 정확히 데미지가 적용되고 텍스트가 뜸 (완벽한 동기화). - **적 돌진:** 적도 공격 시 플레이어 위치로 돌진함 (설정에서 끄기 가능). + - **다이내믹 애니메이션 페이즈:** 공격 애니메이션을 Start, Middle, End 단계로 나누어 각 단계별로 캐릭터 이미지가 변경됨 (예: 공격 준비 -> 타격 모션). - **이펙트:** 타격 아이콘, 데미지 텍스트(Floating Text, Risky 공격 시 크기 확대), 화면 흔들림(`ShakeWidget`), 폭발(`ExplosionWidget`). **적 방어 시 성공/실패 이펙트 추가.** - **배경:** 전투 화면에 경기장 테마의 배경 이미지가 적용되어 있으며, 투명도 레이어를 통해 깊이감을 더함. -- **상태이상:** `Stun`, `Bleed`, `Vulnerable`, `DefenseForbidden`, `Disarmed`. +- **상태이상:** `Stun`, `Vulnerable`, `DefenseForbidden`, `Disarmed`. + - **Bleed (출혈):** 공격이 방어도에 완전히 막히지 않고 HP 피해를 1 이상 입혀야 적용됨. 일단 적용되면 매 턴 시작 시 방어도를 무시하고 HP에 직접 피해를 입힘. - **UI 알림 (Toast):** 하단 네비게이션을 가리지 않는 상단 `Overlay` 기반 알림 시스템. ### C. 데이터 및 로직 (Architecture) @@ -114,6 +117,14 @@ ## 4. 최근 주요 변경 사항 (Change Log) +- **[Feature] Dynamic Animations:** 공격 애니메이션을 페이즈(Start/Middle/End)별로 세분화하고, 각 단계마다 캐릭터 이미지를 변경하는 기능 구현. 리스크 수준(Risky vs Safe)에 따라 애니메이션 시퀀스와 이미지 변경 타이밍을 다르게 적용. +- **[Feature] Story Screen:** 게임 시작 시 세계관을 설명하는 `StoryScreen`을 추가하고, 캐릭터 선택 후 진입하도록 흐름 변경. +- **[Fix] Bleed & Selling & Prefixes:** + - **매직 아이템 접두사:** 생성 확률 100%로 상향. + - **판매 가격 UI:** 실제 판매가(60%) 표시로 수정. + - **출혈(Bleed):** 적 턴 시작 시 피해 미적용 버그 해결 및 "방어도에 막히면(HP 피해 0) 출혈 미적용" 로직 구현. +- **[Fix] Player Image Load:** 저장된 게임 로드 시 또는 새 게임 시작 시 플레이어 이미지가 누락되는 문제를 해결하고, 데이터 로드 로직을 강화. +- **[Refactor] UI Config:** `CharacterStatusCard`의 하드코딩된 스타일 값을 `ThemeConfig`로 추출하여 유지보수성 향상. - **[Overhaul] Gladiator Theme & Balance Pass:** 게임의 테마를 '글래디에이터'로 전면 개편. 몬스터를 전부 '[별명] 이름' 형식의 인간형 검투사로 교체하고, 활과 같은 무기를 트라이던트, 플레일 등으로 교체. 이에 맞춰 아이템과 적 데이터 밸런스를 전체적으로 재조정. - **[Refactor] Item Data System:** 소비용품 데이터를 JSON으로 이전하고, 모든 아이템을 Map 기반으로 조회하도록 리팩토링하여 성능 개선. 소비용품에는 접두사가 붙지 않도록 수정. - **[Fix] Dual-Wielding Data:** 시스템에서 지원하지 않는 '무기 2개 장착' 적 데이터를 수정하고, 능력치 재조정을 통해 밸런스 보정. diff --git a/prompt/68_bleeding_and_selling_fix.md b/prompt/68_bleeding_and_selling_fix.md new file mode 100644 index 0000000..9d3672b --- /dev/null +++ b/prompt/68_bleeding_and_selling_fix.md @@ -0,0 +1,50 @@ +# Bleeding Logic and Item Selling Fixes + +## 1. Magic Item Prefix Chance +- **Issue:** Users felt that magic items often lacked prefixes, making them indistinguishable from normal items in terms of stats. +- **Fix:** Increased `ItemConfig.magicPrefixChance` from `0.5` (50%) to `1.0` (100%). Now, all items generated with `ItemRarity.magic` will guaranteed have a prefix (and associated stat boost). + +## 2. Enemy Bleed Damage Bug +- **Issue:** Enemies were applying bleed to players, but when enemies themselves were bleeding, they took no damage at the start of their turn. +- **Fix:** Updated `BattleProvider._startEnemyTurn` to properly call `_processStartTurnEffects(enemy)`. This ensures that bleed damage is calculated and applied to the enemy's HP, and stun status is checked correctly. + +## 3. Item Selling Price UI +- **Issue:** The "Sell Item" confirmation dialog displayed the item's *base price* instead of the actual *selling price* (which is 60% of base). This confused players into thinking they were getting full price, or that the system was broken when they received less gold. +- **Fix:** Updated `InventoryGridWidget` (`_showItemActionDialog` and `_showSellConfirmationDialog`) to calculate and display the final sell price: `floor(item.price * GameConfig.sellPriceMultiplier)`. + +## 4. Bleed Application Logic +- **Issue:** Bleed status was being applied even when an attack was fully blocked by armor (0 HP damage). +- **Change:** Modified `BattleProvider._tryApplyStatusEffects` and its call site in `_processAttackImpact`. +- **New Logic:** Bleed status is now only applied if `damageToHp > 0`. If an attack is fully absorbed by armor, bleed will not be applied. +- **Note:** Once applied, bleed damage (at start of turn) continues to ignore armor and directly reduces HP, regardless of how much armor the target gains afterwards. + +## 5. Dynamic Attack Animation Images +- **Feature:** Added support for changing character images during attack animation phases (Start, Middle, End). +- **Implementation:** + - Modified `BattleAnimationWidget` to accept callbacks for `onAnimationStart`, `onAnimationMiddle`, and `onAnimationEnd`. + - Updated `BattleScreen` to manage animation phase state and pass override images to `CharacterStatusCard`. + - Configured phase logic: + - **Risky:** Start -> ScaleUp -> Middle -> Dash -> End -> Impact. + - **Safe/Normal:** Start -> Middle -> Impact (No End phase). +- **Assets:** Applied `warrior_attack_1.png` for Start and `warrior_attack_2.png` for End phases. + +## 6. Story Screen & Navigation +- **Feature:** Added a `StoryScreen` sequence after character selection and before the main game. +- **Details:** 3-page PageView with "Next" and "Skip" buttons. Uses `ResponsiveContainer` and consistent styling. +- **Navigation:** `CharacterSelectionScreen` -> `StoryScreen` -> `MainWrapper`. + +## 7. Player Image & UI Fixes +- **Fix:** Corrected `PlayerTemplate.createCharacter` to pass the image path properly. +- **Fix:** Added logic in `BattleProvider.loadFromSave` to force update the player's image path from the latest data, fixing issues with old save files. +- **Refactor:** Extracted hardcoded UI constants in `CharacterStatusCard` to `ThemeConfig`. + +## Files Changed +- `lib/game/config/item_config.dart` +- `lib/providers/battle_provider.dart` +- `lib/widgets/inventory/inventory_grid_widget.dart` +- `lib/screens/battle_screen.dart` +- `lib/widgets/battle/battle_animation_widget.dart` +- `lib/widgets/battle/character_status_card.dart` +- `lib/screens/story_screen.dart` +- `lib/game/data/player_table.dart` +- `lib/game/config/theme_config.dart` \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 8a924e0..ebd7a15 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,3 +29,4 @@ flutter: - assets/data/icon/ - assets/images/enemies/ - assets/images/background/ + - assets/images/character/