diff --git a/google.gemini-cli-vscode-ide-companion-0.7.0.vsix b/google.gemini-cli-vscode-ide-companion-0.7.0.vsix deleted file mode 100644 index 30ea7e8..0000000 Binary files a/google.gemini-cli-vscode-ide-companion-0.7.0.vsix and /dev/null differ diff --git a/lib/game/config/game_config.dart b/lib/game/config/game_config.dart index f6815cd..cb83d07 100644 --- a/lib/game/config/game_config.dart +++ b/lib/game/config/game_config.dart @@ -1,6 +1,6 @@ class GameConfig { // Inventory - static const int maxInventorySize = 5; + static const int maxInventorySize = 8; // Economy static const int startingGold = 50; diff --git a/lib/game/config/theme_config.dart b/lib/game/config/theme_config.dart index 3ad0e78..a9757f8 100644 --- a/lib/game/config/theme_config.dart +++ b/lib/game/config/theme_config.dart @@ -89,6 +89,8 @@ class ThemeConfig { // Status Effect Colors static const Color effectBg = Colors.deepOrange; + static const Color effectBuffBg = Colors.green; // New: Buff + static const Color effectDebuffBg = Colors.deepOrange; // New: Debuff static const Color effectText = Colors.white; // Rarity Colors diff --git a/lib/game/data/item_table.dart b/lib/game/data/item_table.dart index 74d2d0c..0b4b820 100644 --- a/lib/game/data/item_table.dart +++ b/lib/game/data/item_table.dart @@ -83,8 +83,75 @@ class ItemTable { static List armors = []; static List shields = []; static List accessories = []; + static List consumables = []; // New: Potions + + static final Map _items = {}; + + static void initialize() { + // 0. Consumables (Potions) + // Manually added for now, later move to JSON if preferred. + List potionTemplates = [ + ItemTemplate( + id: "potion_heal_small", + name: "Healing Potion", + description: "Restores 20 HP instantly.", + slot: EquipmentSlot.consumable, + atkBonus: 0, + hpBonus: 20, // Used as heal amount + armorBonus: 0, + effects: [], + price: 15, + rarity: ItemRarity.normal, + tier: ItemTier.tier1, + image: "assets/images/items/potion.png", // Valid placeholder + ), + ItemTemplate( + id: "potion_armor_small", + name: "Iron Skin Potion", + description: "Grants +10 Armor instantly.", + slot: EquipmentSlot.consumable, + atkBonus: 0, + hpBonus: 0, + armorBonus: 10, // Used as armor amount + effects: [], + price: 20, + rarity: ItemRarity.normal, + tier: ItemTier.tier1, + image: "assets/images/items/potion.png", + ), + ItemTemplate( + id: "potion_strength_small", + name: "Strength Potion", + description: "Increases Attack Power for 1 turn.", + slot: EquipmentSlot.consumable, + atkBonus: 0, + hpBonus: 0, + armorBonus: 0, + effects: [ + ItemEffect( + type: StatusEffectType.attackUp, + probability: 100, + duration: 1, + value: 5, // Flat +5 Attack (simple implementation) + ), + ], + price: 25, + rarity: ItemRarity.magic, + tier: ItemTier.tier1, + image: "assets/images/items/potion.png", + ), + ]; + + consumables = potionTemplates; + for (var p in potionTemplates) { + _items[p.id] = p; // Register to map + } + } static Future load() async { + // Initialize Manual Items first + initialize(); + final String jsonString = await rootBundle.loadString( 'assets/data/items.json', ); @@ -109,6 +176,7 @@ class ItemTable { ...armors, ...shields, ...accessories, + ...consumables, ]; static ItemTemplate? get(String id) { diff --git a/lib/game/data/name_generator.dart b/lib/game/data/name_generator.dart index ecb01f2..0d3c4b4 100644 --- a/lib/game/data/name_generator.dart +++ b/lib/game/data/name_generator.dart @@ -6,35 +6,93 @@ class NameGenerator { // Adjectives suitable for powerful items static const List _adjectives = [ - "Crimson", "Shadow", "Azure", "Burning", "Frozen", "Ancient", - "Cursed", "Blessed", "Savage", "Eternal", "Dark", "Holy", - "Storm", "Void", "Crystal", "Iron", "Blood", "Night" + "Crimson", + "Shadow", + "Azure", + "Burning", + "Frozen", + "Ancient", + "Cursed", + "Blessed", + "Savage", + "Eternal", + "Dark", + "Holy", + "Storm", + "Void", + "Crystal", + "Iron", + "Blood", + "Night", ]; // Nouns specifically for Weapons static const List _weaponNouns = [ - "Fang", "Claw", "Reaper", "Breaker", "Slayer", "Edge", - "Blade", "Spike", "Crusher", "Whisper", "Howl", "Strike", - "Bane", "Fury", "Vengeance", "Thorn" + "Fang", + "Claw", + "Reaper", + "Breaker", + "Slayer", + "Edge", + "Blade", + "Spike", + "Crusher", + "Whisper", + "Howl", + "Strike", + "Bane", + "Fury", + "Vengeance", + "Thorn", ]; // Nouns specifically for Armor static const List _armorNouns = [ - "Guard", "Wall", "Shelter", "Skin", "Scale", "Plate", - "Bulwark", "Veil", "Shroud", "Ward", "Barrier", "Bastion", - "Mantle", "Aegis", "Carapace" + "Guard", + "Wall", + "Shelter", + "Skin", + "Scale", + "Plate", + "Bulwark", + "Veil", + "Shroud", + "Ward", + "Barrier", + "Bastion", + "Mantle", + "Aegis", + "Carapace", ]; // Nouns specifically for Shields static const List _shieldNouns = [ - "Wall", "Barrier", "Aegis", "Defender", "Blockade", - "Resolve", "Sanctuary", "Buckler", "Tower", "Gate" + "Wall", + "Barrier", + "Aegis", + "Defender", + "Blockade", + "Resolve", + "Sanctuary", + "Buckler", + "Tower", + "Gate", ]; // Nouns specifically for Accessories static const List _accessoryNouns = [ - "Heart", "Soul", "Eye", "Tear", "Spark", "Ember", - "Drop", "Mark", "Sign", "Omen", "Wish", "Star" + "Heart", + "Soul", + "Eye", + "Tear", + "Spark", + "Ember", + "Drop", + "Mark", + "Sign", + "Omen", + "Wish", + "Star", ]; static String generateName(EquipmentSlot slot) { @@ -54,13 +112,19 @@ class NameGenerator { case EquipmentSlot.accessory: noun = _accessoryNouns[_random.nextInt(_accessoryNouns.length)]; break; + case EquipmentSlot.consumable: + noun = "Potion"; + break; } // 20% Chance for "Noun of Noun" format (e.g. "Fang of Shadow") if (_random.nextDouble() < 0.2) { - String suffixNoun = _adjectives[_random.nextInt(_adjectives.length)]; // Reuse adjectives as nouns sometimes works (e.g. "of Blood") - // Better: use a subset of adjectives that work as nouns or generic nouns - return "$noun of $suffixNoun"; + String suffixNoun = + _adjectives[_random.nextInt( + _adjectives.length, + )]; // Reuse adjectives as nouns sometimes works (e.g. "of Blood") + // Better: use a subset of adjectives that work as nouns or generic nouns + return "$noun of $suffixNoun"; } return "$adjective $noun"; diff --git a/lib/game/enums.dart b/lib/game/enums.dart index b0e5e10..d0395a9 100644 --- a/lib/game/enums.dart +++ b/lib/game/enums.dart @@ -10,6 +10,7 @@ enum StatusEffectType { bleed, // Takes damage at start/end of turn defenseForbidden, // Cannot use Defend action disarmed, // Attack strength reduced (e.g., 10%) + attackUp, // New: Increases Attack Power } /// 공격 실패 시 이펙트 피드백 타입 정의 @@ -31,7 +32,7 @@ enum StageType { rest, // Heal or repair } -enum EquipmentSlot { weapon, armor, shield, accessory } +enum EquipmentSlot { weapon, armor, shield, accessory, consumable } enum DamageType { normal, bleed, vulnerable } diff --git a/lib/game/model/entity.dart b/lib/game/model/entity.dart index 3bf8ea9..e03260a 100644 --- a/lib/game/model/entity.dart +++ b/lib/game/model/entity.dart @@ -167,6 +167,15 @@ class Character { int bonus = equipment.values.fold(0, (sum, item) => sum + item.atkBonus); int finalAtk = baseAtk + bonus; + // Apply Attack Up Buff + var attackBuff = statusEffects + .where((e) => e.type == StatusEffectType.attackUp) + .firstOrNull; + if (attackBuff != null) { + // Assuming value is Flat bonus based on ItemTemplate + finalAtk += attackBuff.value; + } + if (hasStatus(StatusEffectType.disarmed)) { finalAtk = (finalAtk * GameConfig.disarmedDamageMultiplier).toInt(); } diff --git a/lib/game/model/item.dart b/lib/game/model/item.dart index fb3431d..9aeb800 100644 --- a/lib/game/model/item.dart +++ b/lib/game/model/item.dart @@ -79,6 +79,8 @@ class Item { return "Shield"; case EquipmentSlot.accessory: return "Accessory"; + case EquipmentSlot.consumable: + return "Potion"; } } } diff --git a/lib/providers/battle_provider.dart b/lib/providers/battle_provider.dart index 41ffef8..1fc0b10 100644 --- a/lib/providers/battle_provider.dart +++ b/lib/providers/battle_provider.dart @@ -73,7 +73,7 @@ class BattleProvider with ChangeNotifier { final Random _random; // Injected Random instance BattleProvider({required this.shopProvider, Random? random}) - : _random = random ?? Random() { + : _random = random ?? Random() { // initializeBattle(); // Do not auto-start logic } @@ -125,6 +125,15 @@ class BattleProvider with ChangeNotifier { player.addToInventory(ItemTable.weapons[5].createItem()); // Sunderer Axe player.addToInventory(ItemTable.shields[3].createItem()); // Cursed Shield + // Add Potions for Testing (Requested by User) + var healPotion = ItemTable.get('potion_heal_small'); + var armorPotion = ItemTable.get('potion_armor_small'); + var strPotion = ItemTable.get('potion_strength_small'); + + if (healPotion != null) player.addToInventory(healPotion.createItem()); + if (armorPotion != null) player.addToInventory(armorPotion.createItem()); + if (strPotion != null) player.addToInventory(strPotion.createItem()); + _prepareNextStage(); _logManager.clear(); _addLog("Game Started! Stage 1"); @@ -222,7 +231,7 @@ class BattleProvider with ChangeNotifier { // _endPlayerTurn(); // Allow player to choose another action return; } - + isPlayerTurn = false; notifyListeners(); @@ -257,7 +266,11 @@ class BattleProvider with ChangeNotifier { if (result.success) { if (type == ActionType.attack) { // 1. Check for Dodge (Moved from _processAttackImpact) - if (CombatCalculator.calculateDodge(enemy.totalDodge, random: _random)) { // Pass injected random + if (CombatCalculator.calculateDodge( + enemy.totalDodge, + random: _random, + )) { + // Pass injected random _addLog("${enemy.name} dodged the attack!"); final event = EffectEvent( id: @@ -383,14 +396,22 @@ class BattleProvider with ChangeNotifier { // Recalculate value based on current stats if (intent.type == EnemyActionType.attack) { - newValue = (enemy.totalAtk * - CombatCalculator.getEfficiency(ActionType.attack, intent.risk)) - .toInt(); + newValue = + (enemy.totalAtk * + CombatCalculator.getEfficiency( + ActionType.attack, + intent.risk, + )) + .toInt(); if (newValue < 1 && enemy.totalAtk > 0) newValue = 1; } else { - newValue = (enemy.totalDefense * - CombatCalculator.getEfficiency(ActionType.defend, intent.risk)) - .toInt(); + newValue = + (enemy.totalDefense * + CombatCalculator.getEfficiency( + ActionType.defend, + intent.risk, + )) + .toInt(); if (newValue < 1 && enemy.totalDefense > 0) newValue = 1; } @@ -520,7 +541,11 @@ class BattleProvider with ChangeNotifier { // Attack Action (Animating) if (intent.isSuccess) { // 1. Check for Dodge - if (CombatCalculator.calculateDodge(player.totalDodge, random: _random)) { // Pass injected random + if (CombatCalculator.calculateDodge( + player.totalDodge, + random: _random, + )) { + // Pass injected random _addLog("${player.name} dodged the attack!"); final event = EffectEvent( id: @@ -540,9 +565,13 @@ class BattleProvider with ChangeNotifier { } // Recalculate damage to account for status changes (like Disarmed) - int finalDamage = (enemy.totalAtk * - CombatCalculator.getEfficiency(ActionType.attack, intent.risk)) - .toInt(); + int finalDamage = + (enemy.totalAtk * + CombatCalculator.getEfficiency( + ActionType.attack, + intent.risk, + )) + .toInt(); if (finalDamage < 1 && enemy.totalAtk > 0) finalDamage = 1; final event = EffectEvent( @@ -579,17 +608,18 @@ class BattleProvider with ChangeNotifier { _effectEventController.sink.add(event); return; } - } - } else if (!canAct) { // If cannot act (stunned) - _addLog("Enemy is stunned and cannot act!"); - int tid = _turnTransactionId; - Future.delayed(const Duration(milliseconds: 500), () { - if (tid != _turnTransactionId) return; - _endEnemyTurn(); - }); - } else { - _addLog("Enemy did nothing."); - + } + } else if (!canAct) { + // If cannot act (stunned) + _addLog("Enemy is stunned and cannot act!"); + int tid = _turnTransactionId; + Future.delayed(const Duration(milliseconds: 500), () { + if (tid != _turnTransactionId) return; + _endEnemyTurn(); + }); + } else { + _addLog("Enemy did nothing."); + int tid = _turnTransactionId; Future.delayed(const Duration(milliseconds: 500), () { if (tid != _turnTransactionId) return; @@ -777,6 +807,68 @@ class BattleProvider with ChangeNotifier { } } + /// Use a consumable item during battle (Free Action) + void useConsumable(Item item) { + if (item.slot != EquipmentSlot.consumable) { + _addLog("Cannot use ${item.name}!"); + return; + } + + // 1. Apply Immediate Effects + bool effectApplied = false; + + // Heal + if (item.hpBonus > 0) { + int currentHp = player.hp; + player.heal(item.hpBonus); + int healedAmount = player.hp - currentHp; + if (healedAmount > 0) { + _addLog("Used ${item.name}. Recovered $healedAmount HP."); + effectApplied = true; + } else { + _addLog("Used ${item.name}. HP is already full."); + // Still consume? Yes, usually potions are lost even if full HP if used. + // But maybe valid to just say "Recovered 0 HP". + effectApplied = true; + } + } + + // Armor + if (item.armorBonus > 0) { + player.armor += item.armorBonus; + _addLog("Used ${item.name}. Gained ${item.armorBonus} Armor."); + effectApplied = true; + } + + // 2. Apply Status Effects (Buffs) + if (item.effects.isNotEmpty) { + for (var effect in item.effects) { + player.addStatusEffect( + StatusEffect( + type: effect.type, + duration: effect.duration, + value: effect.value, + ), + ); + // Log handled? Character.addStatusEffect might need logging or we log here. + // Let's add specific logs for known buffs + if (effect.type == StatusEffectType.attackUp) { + _addLog( + "Used ${item.name}. Attack Up for ${effect.duration} turn(s)!", + ); + } else { + _addLog("Used ${item.name}. Applied ${effect.type.name}!"); + } + } + effectApplied = true; + } + + if (effectApplied) { + player.inventory.remove(item); + notifyListeners(); + } + } + /// Proceed to next stage from non-battle stages (Shop, Rest) void proceedToNextStage() { stage++; @@ -799,9 +891,11 @@ class BattleProvider with ChangeNotifier { // Decide Action Type // Check constraints - bool canDefend = enemy.baseDefense > 0 && + bool canDefend = + enemy.baseDefense > 0 && !enemy.hasStatus(StatusEffectType.defenseForbidden); - bool canAttack = true; // Attack is always possible, but strength is affected by status. + bool canAttack = + true; // Attack is always possible, but strength is affected by status. bool isAttack = true; // Default to attack @@ -981,7 +1075,7 @@ class BattleProvider with ChangeNotifier { // Try applying status effects _tryApplyStatusEffects(attacker, target); - + // If target is enemy, update intent to reflect potential status changes (e.g. Disarmed) if (target == enemy) { updateEnemyIntent(); diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 699b436..8af8404 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -3,10 +3,13 @@ import 'package:shared_preferences/shared_preferences.dart'; class SettingsProvider with ChangeNotifier { static const String _keyEnemyAnim = 'settings_enemy_anim'; + static const String _keyAttackAnimScale = 'settings_attack_anim_scale'; bool _enableEnemyAnimations = true; // Default: Enabled + double _attackAnimScale = 5.0; // Default: 5.0 bool get enableEnemyAnimations => _enableEnemyAnimations; + double get attackAnimScale => _attackAnimScale; SettingsProvider() { _loadSettings(); @@ -15,6 +18,7 @@ class SettingsProvider with ChangeNotifier { Future _loadSettings() async { final prefs = await SharedPreferences.getInstance(); _enableEnemyAnimations = prefs.getBool(_keyEnemyAnim) ?? true; + _attackAnimScale = prefs.getDouble(_keyAttackAnimScale) ?? 5.0; notifyListeners(); } @@ -24,4 +28,11 @@ class SettingsProvider with ChangeNotifier { final prefs = await SharedPreferences.getInstance(); await prefs.setBool(_keyEnemyAnim, value); } + + Future setAttackAnimScale(double value) async { + _attackAnimScale = value; + notifyListeners(); + final prefs = await SharedPreferences.getInstance(); + await prefs.setDouble(_keyAttackAnimScale, value); + } } diff --git a/lib/providers/shop_provider.dart b/lib/providers/shop_provider.dart index 68e7ad5..af82d68 100644 --- a/lib/providers/shop_provider.dart +++ b/lib/providers/shop_provider.dart @@ -23,11 +23,50 @@ class ShopProvider with ChangeNotifier { currentTier = ItemTier.tier2; availableItems = []; + availableItems = []; + + // 1. Generate 4 Random Equipment Items for (int i = 0; i < 4; i++) { - // Generate 4 items - ItemTemplate? template = ItemTable.getRandomItem(tier: currentTier); - if (template != null) { - availableItems.add(template.createItem(stage: stage)); + // Exclude consumables from this pool if getRandomItem includes them by default (it does if we don't filter) + // We need to implement slot exclusion or explicit slot inclusion in getRandomItem? + // Or simply cycle slots? + // ItemTable.getRandomItem picks from allItems which now includes consumables. + // We should add filtering to getRandomItem logic OR filter here. + // Let's filter here by retrying or explicitly asking for non-consumables. + // Actually, ItemTable.getRandomItem accepts 'slot'. But we want ANY equipment. + // Let's rely on type checking or add 'excludeSlot' to getRandomItem (too much change). + // Simpler: Just pick random, if consumable, reroll? Or better: + + // Let's update getRandomItem to support multiple allowed slots? No. + // Let's just pick strictly by slot rotation or random filtering. + // Let's try simple filtering loop. + + while (true) { + ItemTemplate? template = ItemTable.getRandomItem(tier: currentTier); + if (template != null && template.slot != EquipmentSlot.consumable) { + availableItems.add(template.createItem(stage: stage)); + break; + } + } + } + + // 2. Generate 2 Random Consumables + // Consumables might always be Tier 1 for now, or match current tier? + // Let's match current tier (though we only defined Tier 1 potions). + // If no potions at current tier, fallback to Tier 1? + // ItemTable.consumables currently only has items. + // Let's just pick from ItemTable.consumables directly for simplicity and safety. + + if (ItemTable.consumables.isNotEmpty) { + for (int i = 0; i < 2; i++) { + ItemTemplate? consTemplate = ItemTable.getRandomItem( + tier: ItemTier.tier1, // Potions are Tier 1 for now + slot: EquipmentSlot.consumable, + ); + + if (consTemplate != null) { + availableItems.add(consTemplate.createItem(stage: stage)); + } } } notifyListeners(); diff --git a/lib/screens/battle_screen.dart b/lib/screens/battle_screen.dart index 3eb3e54..e0df2b7 100644 --- a/lib/screens/battle_screen.dart +++ b/lib/screens/battle_screen.dart @@ -250,7 +250,8 @@ class _BattleScreenState extends State { break; case BattleFeedbackType.dodge: feedbackText = "DODGE"; - feedbackColor = ThemeConfig.statLuckColor; // Use Luck color (Greenish) + feedbackColor = + ThemeConfig.statLuckColor; // Use Luck color (Greenish) break; default: feedbackText = ""; @@ -564,6 +565,70 @@ class _BattleScreenState extends State { return false; } + void _showInventoryDialog(BuildContext context) { + final battleProvider = context.read(); + final List consumables = battleProvider.player.inventory + .where((item) => item.slot == EquipmentSlot.consumable) + .toList(); + + if (consumables.isEmpty) { + ToastUtils.showTopToast(context, "No consumable items!"); + return; + } + + showDialog( + context: context, + builder: (context) { + return SimpleDialog( + title: const Text("Use Item"), + children: consumables.map((item) { + return SimpleDialogOption( + onPressed: () { + battleProvider.useConsumable(item); + Navigator.pop(context); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: ThemeConfig.rewardItemBg, + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: ItemUtils.getRarityColor(item.rarity), + ), + ), + child: Image.asset( + ItemUtils.getIconPath(item.slot), + width: ThemeConfig.itemIconSizeMedium, + height: ThemeConfig.itemIconSizeMedium, + fit: BoxFit.contain, + ), + ), + const SizedBox(width: 12), + Text( + item.name, + style: TextStyle( + fontWeight: ThemeConfig.fontWeightBold, + fontSize: ThemeConfig.fontSizeLarge, + color: ItemUtils.getRarityColor(item.rarity), + ), + ), + ], + ), + _buildItemStatText(item), + ], + ), + ); + }).toList(), + ); + }, + ); + } + @override Widget build(BuildContext context) { // Sync animation setting to provider logic @@ -661,11 +726,14 @@ class _BattleScreenState extends State { !battleProvider.showRewardPopup && !_isPlayerAttacking && !_isEnemyAttacking && - !battleProvider.player.hasStatus(StatusEffectType.defenseForbidden), // Disable if defense is forbidden + !battleProvider.player.hasStatus( + StatusEffectType.defenseForbidden, + ), // Disable if defense is forbidden onAttackPressed: () => _showRiskLevelSelection(context, ActionType.attack), onDefendPressed: () => _showRiskLevelSelection(context, ActionType.defend), + onItemPressed: () => _showInventoryDialog(context), ), ), diff --git a/lib/screens/inventory_screen.dart b/lib/screens/inventory_screen.dart index 3105ccf..26f0d8e 100644 --- a/lib/screens/inventory_screen.dart +++ b/lib/screens/inventory_screen.dart @@ -1,10 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers.dart'; -import '../game/models.dart'; -import '../game/enums.dart'; -import '../utils.dart'; -import '../game/config.dart'; import '../widgets.dart'; class InventoryScreen extends StatelessWidget { @@ -16,139 +12,13 @@ class InventoryScreen extends StatelessWidget { appBar: AppBar(title: const Text("Inventory & Stats")), body: Consumer( builder: (context, battleProvider, child) { - final player = battleProvider.player; - return Column( children: [ // 1. Modularized Stats Widget const CharacterStatsWidget(), - // 2. Equipped Items Section (Kept here for now) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "Equipped Items", - style: TextStyle( - fontSize: ThemeConfig.fontSizeHeader, - fontWeight: ThemeConfig.fontWeightBold, - ), - ), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: EquipmentSlot.values.map((slot) { - final item = player.equipment[slot]; - return Expanded( - child: InkWell( - onTap: item != null - ? () => _showUnequipConfirmationDialog( - context, - battleProvider, - item, - ) - : null, - child: Card( - color: item != null - ? ThemeConfig.equipmentCardBg - : ThemeConfig.emptySlotBg, - shape: - item != null && - item.rarity != ItemRarity.magic - ? RoundedRectangleBorder( - side: BorderSide( - color: ItemUtils.getRarityColor( - item.rarity, - ), - width: 2.0, - ), - borderRadius: BorderRadius.circular(4.0), - ) - : null, - child: Stack( - children: [ - // Slot Name - Positioned( - right: 4, - top: 4, - child: Text( - slot.name.toUpperCase(), - style: const TextStyle( - fontSize: ThemeConfig.fontSizeTiny, - fontWeight: ThemeConfig.fontWeightBold, - color: Colors.white30, - ), - ), - ), - // Faded Icon - Positioned( - left: 4, - top: 4, - child: Opacity( - opacity: item != null ? 0.5 : 0.2, - child: Image.asset( - ItemUtils.getIconPath(slot), - width: 40, - height: 40, - fit: BoxFit.contain, - filterQuality: FilterQuality.high, - ), - ), - ), - // Content - Center( - child: Padding( - padding: const EdgeInsets.all(4.0), - child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - const SizedBox(height: 12), - FittedBox( - fit: BoxFit.scaleDown, - child: Text( - item?.name ?? - AppStrings.emptySlot, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: - ThemeConfig.fontSizeSmall, - fontWeight: - ThemeConfig.fontWeightBold, - color: item != null - ? ItemUtils.getRarityColor( - item.rarity, - ) - : ThemeConfig.textColorGrey, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - if (item != null) - FittedBox( - fit: BoxFit.scaleDown, - child: _buildItemStatText(item), - ), - ], - ), - ), - ), - ], - ), - ), - ), - ); - }).toList(), - ), - ], - ), - ), + // 2. Modularized Equipped Items Section + const EquippedItemsWidget(), // 3. Modularized Inventory Grid const Expanded(child: InventoryGridWidget()), @@ -158,152 +28,4 @@ class InventoryScreen extends StatelessWidget { ), ); } - - // --- Helper Methods for Equipped Items Section --- - - void _showUnequipConfirmationDialog( - BuildContext context, - BattleProvider provider, - Item itemToUnequip, - ) { - final player = provider.player; - - // Calculate predicted stats - final currentMaxHp = player.totalMaxHp; - final currentAtk = player.totalAtk; - final currentDef = player.totalDefense; - final currentHp = player.hp; - - // Predict new stats (Subtract item bonuses) - int newMaxHp = currentMaxHp - itemToUnequip.hpBonus; - int newAtk = currentAtk - itemToUnequip.atkBonus; - int newDef = currentDef - itemToUnequip.armorBonus; - - // Predict HP (Percentage Logic) - double ratio = currentMaxHp > 0 ? currentHp / currentMaxHp : 0.0; - int newHp = (newMaxHp * ratio).toInt(); - if (newHp < 0) newHp = 0; - if (newHp > newMaxHp) newHp = newMaxHp; - - showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text("Unequip Item"), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "${AppStrings.unequip} ${itemToUnequip.name}?", - style: const TextStyle(fontWeight: ThemeConfig.fontWeightBold), - ), - const SizedBox(height: 16), - _buildStatChangeRow("Max HP", currentMaxHp, newMaxHp), - _buildStatChangeRow("Current HP", currentHp, newHp), - _buildStatChangeRow(AppStrings.atk, currentAtk, newAtk), - _buildStatChangeRow(AppStrings.def, currentDef, newDef), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx), - child: const Text(AppStrings.cancel), - ), - ElevatedButton( - onPressed: () { - provider.unequipItem(itemToUnequip); - Navigator.pop(ctx); - }, - child: const Text(AppStrings.confirm), - ), - ], - ), - ); - } - - Widget _buildStatChangeRow(String label, int oldVal, int newVal) { - int diff = newVal - oldVal; - Color color = diff > 0 - ? ThemeConfig.statDiffPositive - : (diff < 0 - ? ThemeConfig.statDiffNegative - : ThemeConfig.statDiffNeutral); - String diffText = diff > 0 ? "(+$diff)" : (diff < 0 ? "($diff)" : ""); - - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(label), - Row( - children: [ - Text( - "$oldVal", - style: const TextStyle(color: ThemeConfig.textColorGrey), - ), - const Icon( - Icons.arrow_right, - size: 16, - color: ThemeConfig.textColorGrey, - ), - Text( - "$newVal", - style: const TextStyle(fontWeight: ThemeConfig.fontWeightBold), - ), - const SizedBox(width: 4), - Text( - diffText, - style: TextStyle( - color: color, - fontSize: 12, - fontWeight: ThemeConfig.fontWeightBold, - ), - ), - ], - ), - ], - ), - ); - } - - Widget _buildItemStatText(Item item) { - List stats = []; - if (item.atkBonus > 0) stats.add("+${item.atkBonus} ${AppStrings.atk}"); - if (item.hpBonus > 0) stats.add("+${item.hpBonus} ${AppStrings.hp}"); - if (item.armorBonus > 0) stats.add("+${item.armorBonus} ${AppStrings.def}"); - if (item.luck > 0) stats.add("+${item.luck} ${AppStrings.luck}"); - - // Include effects - List effectTexts = item.effects.map((e) => e.description).toList(); - - if (stats.isEmpty && effectTexts.isEmpty) return const SizedBox.shrink(); - - return Column( - children: [ - if (stats.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 2.0, bottom: 2.0), - child: Text( - stats.join(", "), - style: const TextStyle( - fontSize: ThemeConfig.fontSizeSmall, - color: ThemeConfig.statAtkColor, - ), - textAlign: TextAlign.center, - ), - ), - if (effectTexts.isNotEmpty) - Padding( - padding: const EdgeInsets.only(bottom: 2.0), - child: Text( - effectTexts.join("\n"), - style: const TextStyle( - fontSize: ThemeConfig.fontSizeTiny, - color: ThemeConfig.rarityLegendary, - ), - ), - ), - ], - ); - } } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 3a97e7b..8bb5e07 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -28,16 +28,58 @@ class SettingsScreen extends StatelessWidget { builder: (context, settings, child) { return SizedBox( width: 300, - child: SwitchListTile( - title: const Text( - AppStrings.enemyAnimations, - style: TextStyle(color: ThemeConfig.textColorWhite), - ), - value: settings.enableEnemyAnimations, - onChanged: (value) { - settings.toggleEnemyAnimations(value); - }, - activeColor: ThemeConfig.btnActionActive, + child: Column( + children: [ + SwitchListTile( + title: const Text( + AppStrings.enemyAnimations, + style: TextStyle(color: ThemeConfig.textColorWhite), + ), + value: settings.enableEnemyAnimations, + onChanged: (value) { + settings.toggleEnemyAnimations(value); + }, + activeColor: ThemeConfig.btnActionActive, + ), + const SizedBox(height: 20), + const Text( + 'Attack Animation Scale', + style: TextStyle(color: ThemeConfig.textColorWhite), + ), + Row( + children: [ + const Text( + '2.0', + style: TextStyle(color: ThemeConfig.textColorGrey), + ), + Expanded( + child: Slider( + value: settings.attackAnimScale, + min: 2.0, + max: 9.9, + divisions: 79, + label: settings.attackAnimScale.toStringAsFixed(1), + activeColor: ThemeConfig.btnActionActive, + inactiveColor: ThemeConfig.textColorGrey, + onChanged: (value) { + settings.setAttackAnimScale(value); + }, + ), + ), + const Text( + '9.9', + style: TextStyle(color: ThemeConfig.textColorGrey), + ), + ], + ), + Text( + 'Current: ${settings.attackAnimScale.toStringAsFixed(1)}', + style: const TextStyle( + color: ThemeConfig.textColorWhite, + fontSize: 12, + ), + ), + ], ), ); }, diff --git a/lib/utils/item_utils.dart b/lib/utils/item_utils.dart index a8c806c..e7aba5a 100644 --- a/lib/utils/item_utils.dart +++ b/lib/utils/item_utils.dart @@ -28,6 +28,8 @@ class ItemUtils { return 'assets/data/icon/icon_armor.png'; case EquipmentSlot.accessory: return 'assets/data/icon/icon_accessory.png'; + case EquipmentSlot.consumable: + return 'assets/data/icon/icon_accessory.png'; // Todo: Add potion icon } } } diff --git a/lib/widgets/battle/battle_animation_widget.dart b/lib/widgets/battle/battle_animation_widget.dart index 8dfba15..673b065 100644 --- a/lib/widgets/battle/battle_animation_widget.dart +++ b/lib/widgets/battle/battle_animation_widget.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../providers/settings_provider.dart'; import '../../game/enums.dart'; class BattleAnimationWidget extends StatefulWidget { @@ -77,6 +79,11 @@ class BattleAnimationWidgetState extends State await _translateController.reverse(); } else { // Risky: Scale + Heavy Dash + final attackScale = context.read().attackAnimScale; + _scaleAnimation = Tween(begin: 1.0, end: attackScale).animate( + CurvedAnimation(parent: _scaleController, curve: Curves.easeOut), + ); + _scaleController.duration = const Duration(milliseconds: 600); _translateController.duration = const Duration(milliseconds: 500); diff --git a/lib/widgets/battle/battle_controls.dart b/lib/widgets/battle/battle_controls.dart index 0e37f08..b021b5f 100644 --- a/lib/widgets/battle/battle_controls.dart +++ b/lib/widgets/battle/battle_controls.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import '../../game/enums.dart'; + import '../../game/config.dart'; class BattleControls extends StatelessWidget { @@ -7,6 +7,7 @@ class BattleControls extends StatelessWidget { final bool isDefendEnabled; final VoidCallback onAttackPressed; final VoidCallback onDefendPressed; + final VoidCallback onItemPressed; // New const BattleControls({ super.key, @@ -14,22 +15,16 @@ class BattleControls extends StatelessWidget { required this.isDefendEnabled, required this.onAttackPressed, required this.onDefendPressed, + required this.onItemPressed, // New }); Widget _buildFloatingActionButton({ required String label, required Color color, - required ActionType actionType, + required String iconPath, // Changed from ActionType to String required bool isEnabled, required VoidCallback onPressed, }) { - String iconPath; - if (actionType == ActionType.attack) { - iconPath = 'assets/data/icon/icon_weapon.png'; - } else { - iconPath = 'assets/data/icon/icon_shield.png'; - } - return FloatingActionButton( heroTag: label, onPressed: isEnabled ? onPressed : null, @@ -53,7 +48,7 @@ class BattleControls extends StatelessWidget { _buildFloatingActionButton( label: "ATK", color: ThemeConfig.btnActionActive, - actionType: ActionType.attack, + iconPath: 'assets/data/icon/icon_weapon.png', isEnabled: isAttackEnabled, onPressed: onAttackPressed, ), @@ -61,10 +56,20 @@ class BattleControls extends StatelessWidget { _buildFloatingActionButton( label: "DEF", color: ThemeConfig.btnDefendActive, - actionType: ActionType.defend, + iconPath: 'assets/data/icon/icon_shield.png', isEnabled: isDefendEnabled, onPressed: onDefendPressed, ), + const SizedBox(height: 16), + _buildFloatingActionButton( + label: "ITEM", + color: Colors.indigoAccent, // Distinct color for Item + iconPath: + 'assets/data/icon/icon_accessory.png', // Placeholder for Bag + isEnabled: + isAttackEnabled, // Enabled when it's player turn (same as attack) + onPressed: onItemPressed, + ), ], ); } diff --git a/lib/widgets/battle/character_status_card.dart b/lib/widgets/battle/character_status_card.dart index 7069c9c..66c679a 100644 --- a/lib/widgets/battle/character_status_card.dart +++ b/lib/widgets/battle/character_status_card.dart @@ -68,13 +68,16 @@ class CharacterStatusCard extends StatelessWidget { child: Wrap( spacing: 4.0, children: character.statusEffects.map((effect) { + final isBuff = effect.type == StatusEffectType.attackUp; return Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( - color: ThemeConfig.effectBg, + color: isBuff + ? ThemeConfig.effectBuffBg + : ThemeConfig.effectDebuffBg, borderRadius: BorderRadius.circular(4), ), child: Text( diff --git a/lib/widgets/battle/risk_selection_dialog.dart b/lib/widgets/battle/risk_selection_dialog.dart index c4dd609..21641bb 100644 --- a/lib/widgets/battle/risk_selection_dialog.dart +++ b/lib/widgets/battle/risk_selection_dialog.dart @@ -79,7 +79,7 @@ class RiskSelectionDialog extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - risk.name, + risk.name.toUpperCase(), style: const TextStyle(fontWeight: FontWeight.bold), ), Text(infoText, style: TextStyle(fontSize: 12, color: infoColor)), diff --git a/lib/widgets/inventory.dart b/lib/widgets/inventory.dart index 6561688..ba42846 100644 --- a/lib/widgets/inventory.dart +++ b/lib/widgets/inventory.dart @@ -1,2 +1,3 @@ export 'inventory/character_stats_widget.dart'; export 'inventory/inventory_grid_widget.dart'; +export 'inventory/equipped_items_widget.dart'; diff --git a/lib/widgets/inventory/equipped_items_widget.dart b/lib/widgets/inventory/equipped_items_widget.dart new file mode 100644 index 0000000..2e801dc --- /dev/null +++ b/lib/widgets/inventory/equipped_items_widget.dart @@ -0,0 +1,289 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../providers.dart'; +import '../../game/models.dart'; +import '../../game/enums.dart'; +import '../../utils.dart'; +import '../../game/config.dart'; + +class EquippedItemsWidget extends StatelessWidget { + const EquippedItemsWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, battleProvider, child) { + final player = battleProvider.player; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Equipped Items", + style: TextStyle( + fontSize: ThemeConfig.fontSizeHeader, + fontWeight: ThemeConfig.fontWeightBold, + ), + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: EquipmentSlot.values + .where((slot) => slot != EquipmentSlot.consumable) + .map((slot) { + final item = player.equipment[slot]; + return Expanded( + child: InkWell( + onTap: item != null + ? () => _showUnequipConfirmationDialog( + context, + battleProvider, + item, + ) + : null, + child: Card( + color: item != null + ? ThemeConfig.equipmentCardBg + : ThemeConfig.emptySlotBg, + shape: + item != null && item.rarity != ItemRarity.magic + ? RoundedRectangleBorder( + side: BorderSide( + color: ItemUtils.getRarityColor( + item.rarity, + ), + width: 2.0, + ), + borderRadius: BorderRadius.circular(4.0), + ) + : null, + child: Stack( + children: [ + // Slot Name + Positioned( + right: 4, + top: 4, + child: Text( + slot.name.toUpperCase(), + style: const TextStyle( + fontSize: ThemeConfig.fontSizeTiny, + fontWeight: ThemeConfig.fontWeightBold, + color: Colors.white30, + ), + ), + ), + // Faded Icon + Positioned( + left: 4, + top: 4, + child: Opacity( + opacity: item != null ? 0.5 : 0.2, + child: Image.asset( + ItemUtils.getIconPath(slot), + width: 40, + height: 40, + fit: BoxFit.contain, + filterQuality: FilterQuality.high, + ), + ), + ), + // Content + Center( + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + const SizedBox(height: 12), + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + item?.name ?? AppStrings.emptySlot, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: + ThemeConfig.fontSizeSmall, + fontWeight: + ThemeConfig.fontWeightBold, + color: item != null + ? ItemUtils.getRarityColor( + item.rarity, + ) + : ThemeConfig.textColorGrey, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + if (item != null) + FittedBox( + fit: BoxFit.scaleDown, + child: _buildItemStatText(item), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + }) + .toList(), + ), + ], + ), + ); + }, + ); + } + + void _showUnequipConfirmationDialog( + BuildContext context, + BattleProvider provider, + Item itemToUnequip, + ) { + final player = provider.player; + + // Calculate predicted stats + final currentMaxHp = player.totalMaxHp; + final currentAtk = player.totalAtk; + final currentDef = player.totalDefense; + final currentHp = player.hp; + + // Predict new stats (Subtract item bonuses) + int newMaxHp = currentMaxHp - itemToUnequip.hpBonus; + int newAtk = currentAtk - itemToUnequip.atkBonus; + int newDef = currentDef - itemToUnequip.armorBonus; + + // Predict HP (Percentage Logic) + double ratio = currentMaxHp > 0 ? currentHp / currentMaxHp : 0.0; + int newHp = (newMaxHp * ratio).toInt(); + if (newHp < 0) newHp = 0; + if (newHp > newMaxHp) newHp = newMaxHp; + + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text("Unequip Item"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "${AppStrings.unequip} ${itemToUnequip.name}?", + style: const TextStyle(fontWeight: ThemeConfig.fontWeightBold), + ), + const SizedBox(height: 16), + _buildStatChangeRow("Max HP", currentMaxHp, newMaxHp), + _buildStatChangeRow("Current HP", currentHp, newHp), + _buildStatChangeRow(AppStrings.atk, currentAtk, newAtk), + _buildStatChangeRow(AppStrings.def, currentDef, newDef), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text(AppStrings.cancel), + ), + ElevatedButton( + onPressed: () { + provider.unequipItem(itemToUnequip); + Navigator.pop(ctx); + }, + child: const Text(AppStrings.confirm), + ), + ], + ), + ); + } + + Widget _buildStatChangeRow(String label, int oldVal, int newVal) { + int diff = newVal - oldVal; + Color color = diff > 0 + ? ThemeConfig.statDiffPositive + : (diff < 0 + ? ThemeConfig.statDiffNegative + : ThemeConfig.statDiffNeutral); + String diffText = diff > 0 ? "(+$diff)" : (diff < 0 ? "($diff)" : ""); + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label), + Row( + children: [ + Text( + "$oldVal", + style: const TextStyle(color: ThemeConfig.textColorGrey), + ), + const Icon( + Icons.arrow_right, + size: 16, + color: ThemeConfig.textColorGrey, + ), + Text( + "$newVal", + style: const TextStyle(fontWeight: ThemeConfig.fontWeightBold), + ), + const SizedBox(width: 4), + Text( + diffText, + style: TextStyle( + color: color, + fontSize: 12, + fontWeight: ThemeConfig.fontWeightBold, + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildItemStatText(Item item) { + List stats = []; + if (item.atkBonus > 0) stats.add("+${item.atkBonus} ${AppStrings.atk}"); + if (item.hpBonus > 0) stats.add("+${item.hpBonus} ${AppStrings.hp}"); + if (item.armorBonus > 0) stats.add("+${item.armorBonus} ${AppStrings.def}"); + if (item.luck > 0) stats.add("+${item.luck} ${AppStrings.luck}"); + + // Include effects + List effectTexts = item.effects.map((e) => e.description).toList(); + + if (stats.isEmpty && effectTexts.isEmpty) return const SizedBox.shrink(); + + return Column( + children: [ + if (stats.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 2.0, bottom: 2.0), + child: Text( + stats.join(", "), + style: const TextStyle( + fontSize: ThemeConfig.fontSizeSmall, + color: ThemeConfig.statAtkColor, + ), + textAlign: TextAlign.center, + ), + ), + if (effectTexts.isNotEmpty) + Padding( + padding: const EdgeInsets.only(bottom: 2.0), + child: Text( + effectTexts.join("\n"), + style: const TextStyle( + fontSize: ThemeConfig.fontSizeTiny, + color: ThemeConfig.rarityLegendary, + ), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/inventory/inventory_grid_widget.dart b/lib/widgets/inventory/inventory_grid_widget.dart index bec9255..e3e3aa7 100644 --- a/lib/widgets/inventory/inventory_grid_widget.dart +++ b/lib/widgets/inventory/inventory_grid_widget.dart @@ -92,22 +92,40 @@ class InventoryGridWidget extends StatelessWidget { builder: (ctx) => SimpleDialog( title: Text("${item.name} Actions"), children: [ - SimpleDialogOption( - onPressed: () { - Navigator.pop(ctx); - _showEquipConfirmationDialog(context, provider, item); - }, - child: const Padding( - padding: EdgeInsets.symmetric(vertical: 8.0), - child: Row( - children: [ - Icon(Icons.shield, color: ThemeConfig.btnDefendActive), - SizedBox(width: 10), - Text(AppStrings.equip), - ], + if (item.slot == EquipmentSlot.consumable) + SimpleDialogOption( + onPressed: () { + Navigator.pop(ctx); + provider.useConsumable(item); + }, + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + Icon(Icons.science, color: ThemeConfig.btnActionActive), + SizedBox(width: 10), + Text("Use"), + ], + ), + ), + ) + else + SimpleDialogOption( + onPressed: () { + Navigator.pop(ctx); + _showEquipConfirmationDialog(context, provider, item); + }, + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + Icon(Icons.shield, color: ThemeConfig.btnDefendActive), + SizedBox(width: 10), + Text(AppStrings.equip), + ], + ), ), ), - ), if (isShop) SimpleDialogOption( onPressed: () { diff --git a/prompt/00_project_context_restore.md b/prompt/00_project_context_restore.md index 4a69b55..bb6bdc4 100644 --- a/prompt/00_project_context_restore.md +++ b/prompt/00_project_context_restore.md @@ -125,10 +125,11 @@ - **[UI] Top-Aligned Toast:** SnackBar를 상단 토스트 알림으로 교체하여 하단 네비게이션 가림 현상 해결 및 애니메이션 버그 수정. - **[Fix] Asset 404 Error:** 적 이미지 누락 문제 해결(Placeholder 적용) 및 `pubspec.yaml` 경로 업데이트. - **[Refactor] ShopUI:** 상점 UI의 문법 및 로직 오류 수정. +- **[Feature] Consumable Items:** 체력/방어/공격버프 물약 구현. 전투 중 사용 가능하며, 사용 시 턴을 소모하지 않음(Free Action). 상점에서 판매. - **[Feature] Enhanced Enemy Display:** 적 이미지 동적 로딩 및 크기 확대, 스테이지 헤더에 Boss/Tier 정보 상세 표시. ## 5. 다음 단계 (Next Steps) 1. **밸런싱:** 현재 몬스터 및 아이템 스탯 미세 조정. 2. **콘텐츠 확장:** 더 많은 아이템, 적, 스킬 패턴 추가. -3. **튜토리얼:** 신규 유저를 위한 가이드 추가. \ No newline at end of file +3. **튜토리얼:** 신규 유저를 위한 가이드 추가. diff --git a/prompt/66_consumable_items.md b/prompt/66_consumable_items.md new file mode 100644 index 0000000..c5d1646 --- /dev/null +++ b/prompt/66_consumable_items.md @@ -0,0 +1,42 @@ +# Consumable Items (Potions) Implementation + +## Objective + +Implement a consumable item system to provide immediate effects or short-term buffs during battle. + +## Features + +### 1. New Item Type: Consumables + +- Category: `EquipmentSlot.consumable` +- Items: + 1. **Healing Potion**: Restores HP immediately. + 2. **Ironskin Potion (Armor)**: Grants Armor immediately. + 3. **Strength Potion**: Grants "Attack Up" buff for 1 turn. + +### 2. Battle Mechanics + +- **Usage**: Consumables can be used from the inventory during the player's turn. +- **Action Cost**: Usage is a **Free Action** (does not consume the turn). Players can use a potion and then Attack/Defend in the same turn. +- **Effects**: + - **Heal**: `hp += value` (capped at maxHp) + - **Armor**: `armor += value` + - **Buff**: Apply `StatusEffectType.attackUp` (increases damage by 20% or flat amount). + +### 3. Shop Update + +- Shop now stocks **6 items** total: + - 4 Equipment (Weapons/Shields/Armor/Accessories) + - 2 Consumables (Potions) + +### 4. UI Updates + +- **Battle Controls**: Added an "Items" button (Bag icon) to open the battle inventory. +- **Shop UI**: Updated to display and sell consumable items. +- **Battle Inventory**: A dialog to view and use owned consumable items. + +## Technical Details + +- `Item` model updated to handle `hpBonus` (Heal), `armorBonus` (Armor), and `effects` (Buffs) for consumables. +- `BattleProvider.useItem(Item)` implements the application logic. +- `CombatCalculator` logic handles the `Attack Up` status effect multiplier.