update
This commit is contained in:
parent
c029cd1e10
commit
f5a7eb2db9
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
"baseAtk": 5,
|
||||
"baseDefense": 5,
|
||||
"baseDodge": 2,
|
||||
"image": "assets/images/players/warrior.png"
|
||||
"image": "assets/images/character/warrior.png"
|
||||
},
|
||||
{
|
||||
"id": "rogue",
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 420 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 345 KiB |
|
|
@ -13,5 +13,5 @@ class ItemConfig {
|
|||
};
|
||||
|
||||
// Loot Generation
|
||||
static const double magicPrefixChance = 0.5; // 50%
|
||||
static const double magicPrefixChance = 1.0; // 100%
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class PlayerTemplate {
|
|||
baseDefense: baseDefense,
|
||||
baseDodge: baseDodge, // Use template value
|
||||
armor: 0,
|
||||
image: image, // Pass image path
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<StatusEffect> 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}!");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -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<BattleScreen> {
|
|||
// 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,14 +366,18 @@ class _BattleScreenState extends State<BattleScreen> {
|
|||
: event.risk;
|
||||
|
||||
_playerAnimKey.currentState
|
||||
?.animateAttack(offset, () {
|
||||
?.animateAttack(
|
||||
offset,
|
||||
() {
|
||||
showEffect();
|
||||
context.read<BattleProvider>().handleImpact(event);
|
||||
|
||||
if (event.risk == RiskLevel.risky && event.feedbackType == null) {
|
||||
if (event.risk == RiskLevel.risky &&
|
||||
event.feedbackType == null) {
|
||||
_shakeKey.currentState?.shake();
|
||||
RenderBox? stackBox =
|
||||
_stackKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
_stackKey.currentContext?.findRenderObject()
|
||||
as RenderBox?;
|
||||
if (stackBox != null) {
|
||||
Offset localEnemyPos = stackBox.globalToLocal(enemyPos);
|
||||
localEnemyPos += Offset(
|
||||
|
|
@ -365,11 +387,29 @@ class _BattleScreenState extends State<BattleScreen> {
|
|||
_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<BattleScreen> {
|
|||
key: _playerKey,
|
||||
animationKey: _playerAnimKey,
|
||||
hideStats: _isPlayerAttacking,
|
||||
overrideImage: _getOverrideImage(true),
|
||||
),
|
||||
),
|
||||
// Enemy (Top Right) - Rendered Last (On Top)
|
||||
|
|
|
|||
|
|
@ -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<BattleProvider>().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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<StoryScreen> createState() => _StoryScreenState();
|
||||
}
|
||||
|
||||
class _StoryScreenState extends State<StoryScreen> {
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -53,8 +53,13 @@ class BattleAnimationWidgetState extends State<BattleAnimationWidget>
|
|||
Future<void> 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<BattleAnimationWidget>
|
|||
|
||||
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<SettingsProvider>().attackAnimScale;
|
||||
_scaleAnimation = Tween<double>(begin: 1.0, end: attackScale).animate(
|
||||
|
|
@ -91,6 +100,8 @@ class BattleAnimationWidgetState extends State<BattleAnimationWidget>
|
|||
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<BattleAnimationWidget>
|
|||
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<void> animateDefense(VoidCallback onImpact) async {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ class CharacterStatusCard extends StatelessWidget {
|
|||
final bool isTurn;
|
||||
final GlobalKey<BattleAnimationWidgetState>? 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<BattleProvider>(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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개 장착' 적 데이터를 수정하고, 능력치 재조정을 통해 밸런스 보정.
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
@ -29,3 +29,4 @@ flutter:
|
|||
- assets/data/icon/
|
||||
- assets/images/enemies/
|
||||
- assets/images/background/
|
||||
- assets/images/character/
|
||||
|
|
|
|||
Loading…
Reference in New Issue