From 45c6185d3e235f515efdbbae56a2ec7552bc7e58 Mon Sep 17 00:00:00 2001 From: Horoli Date: Tue, 2 Dec 2025 01:35:39 +0900 Subject: [PATCH] update --- devtools_options.yaml | 3 + lib/game/data/item_table.dart | 101 +++++- lib/game/model/entity.dart | 45 +++ lib/game/model/item.dart | 32 ++ lib/game/model/stage.dart | 21 ++ lib/game/model/status_effect.dart | 18 + lib/main.dart | 4 +- lib/providers/battle_provider.dart | 335 +++++++++++++++--- lib/screens/battle_screen.dart | 111 +++++- lib/screens/character_selection_screen.dart | 77 ++++ lib/screens/inventory_screen.dart | 173 ++++++++- lib/screens/main_menu_screen.dart | 67 ++++ prompt/13_item_status_options.md | 26 ++ ..._integrate_status_effects_and_item_data.md | 27 ++ prompt/15_ui_display_status_effects.md | 20 ++ .../16_status_effect_duration_adjustment.md | 21 ++ prompt/17_inventory_discard_feature.md | 26 ++ prompt/18_implement_stage_class_and_types.md | 32 ++ prompt/19_shop_sell_and_gold.md | 30 ++ .../20_main_menu_and_character_selection.md | 27 ++ 20 files changed, 1104 insertions(+), 92 deletions(-) create mode 100644 devtools_options.yaml create mode 100644 lib/game/model/stage.dart create mode 100644 lib/game/model/status_effect.dart create mode 100644 lib/screens/character_selection_screen.dart create mode 100644 lib/screens/main_menu_screen.dart create mode 100644 prompt/13_item_status_options.md create mode 100644 prompt/14_integrate_status_effects_and_item_data.md create mode 100644 prompt/15_ui_display_status_effects.md create mode 100644 prompt/16_status_effect_duration_adjustment.md create mode 100644 prompt/17_inventory_discard_feature.md create mode 100644 prompt/18_implement_stage_class_and_types.md create mode 100644 prompt/19_shop_sell_and_gold.md create mode 100644 prompt/20_main_menu_and_character_selection.md diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/game/data/item_table.dart b/lib/game/data/item_table.dart index e8b0376..09dc081 100644 --- a/lib/game/data/item_table.dart +++ b/lib/game/data/item_table.dart @@ -1,4 +1,5 @@ import '../model/item.dart'; +import '../model/status_effect.dart'; // Import StatusEffect for ItemEffect class ItemTemplate { final String name; @@ -7,6 +8,7 @@ class ItemTemplate { final int baseHp; final int baseArmor; final EquipmentSlot slot; + final List effects; // New: Effects this item can inflict const ItemTemplate({ required this.name, @@ -15,6 +17,7 @@ class ItemTemplate { this.baseHp = 0, this.baseArmor = 0, required this.slot, + this.effects = const [], // Default to no effects }); // Create an instance of Item based on this template, optionally scaling with stage @@ -25,6 +28,13 @@ class ItemTemplate { int scaledHp = baseHp > 0 ? baseHp + (stage - 1) * 5 : 0; int scaledArmor = baseArmor > 0 ? baseArmor + (stage - 1) : 0; + // Calculate price based on stats + int calculatedPrice = (scaledAtk * 10) + (scaledHp * 2) + (scaledArmor * 5); + if (effects.isNotEmpty) { + calculatedPrice += effects.length * 50; // Bonus value for special effects + } + if (calculatedPrice < 10) calculatedPrice = 10; // Minimum price + return Item( name: "$name${stage > 1 ? ' +${stage - 1}' : ''}", // Append +1, +2 etc. description: description, @@ -32,30 +42,73 @@ class ItemTemplate { hpBonus: scaledHp, armorBonus: scaledArmor, slot: slot, + effects: effects, // Pass the effects to the Item + price: calculatedPrice, ); } } class ItemTable { - static const List weapons = [ - ItemTemplate( + static final List weapons = [ + const ItemTemplate( name: "Rusty Dagger", description: "Old and rusty, but better than nothing.", baseAtk: 3, slot: EquipmentSlot.weapon, ), - ItemTemplate( + const ItemTemplate( name: "Iron Sword", description: "A standard soldier's sword.", baseAtk: 8, slot: EquipmentSlot.weapon, ), - ItemTemplate( + const ItemTemplate( name: "Battle Axe", description: "Heavy but powerful.", baseAtk: 12, slot: EquipmentSlot.weapon, ), + // New: Weapons with status effects + ItemTemplate( + name: "Stunning Hammer", + description: "A heavy hammer that can stun foes.", + baseAtk: 10, + slot: EquipmentSlot.weapon, + effects: [ + ItemEffect( + type: StatusEffectType.stun, + probability: 20, + duration: 1, + ), // 20% chance to stun for 1 turn + ], + ), + ItemTemplate( + name: "Jagged Dagger", + description: "A cruel dagger that causes bleeding.", + baseAtk: 7, + slot: EquipmentSlot.weapon, + effects: [ + ItemEffect( + type: StatusEffectType.bleed, + probability: 30, + duration: 3, + value: 5, + ), // 30% chance to bleed (5 dmg/turn for 3 turns) + ], + ), + ItemTemplate( + name: "Sunderer Axe", + description: "An axe that exposes enemy weaknesses.", + baseAtk: 11, + slot: EquipmentSlot.weapon, + effects: [ + ItemEffect( + type: StatusEffectType.vulnerable, + probability: 100, + duration: 2, + ), // 100% chance to make vulnerable for 2 turns + ], + ), ]; static const List armors = [ @@ -79,25 +132,40 @@ class ItemTable { ), ]; - static const List shields = [ - ItemTemplate( + static final List shields = [ + const ItemTemplate( name: "Pot Lid", description: "It was used for cooking.", baseArmor: 1, slot: EquipmentSlot.shield, ), - ItemTemplate( + const ItemTemplate( name: "Wooden Shield", description: "Sturdy oak wood.", baseArmor: 3, slot: EquipmentSlot.shield, ), - ItemTemplate( + const ItemTemplate( name: "Kite Shield", description: "Used by knights.", baseArmor: 6, slot: EquipmentSlot.shield, ), + // New: Shield with Defense Forbidden effect (example) + ItemTemplate( + name: "Cursed Shield", + description: + "A shield that prevents the wielder from defending themselves.", + baseArmor: 5, + slot: EquipmentSlot.shield, + effects: [ + ItemEffect( + type: StatusEffectType.defenseForbidden, + probability: 100, + duration: 999, + ), // Always prevent defending (long duration for testing) + ], + ), ]; static const List accessories = [ @@ -108,6 +176,13 @@ class ItemTable { baseHp: 5, slot: EquipmentSlot.accessory, ), + ItemTemplate( + name: "Copper Ring", + description: "A simple ring", + baseAtk: 1, + baseHp: 5, + slot: EquipmentSlot.accessory, + ), ItemTemplate( name: "Ruby Amulet", description: "Glows with a faint red light.", @@ -126,9 +201,9 @@ class ItemTable { ]; static List get allItems => [ - ...weapons, - ...armors, - ...shields, - ...accessories, - ]; + ...weapons, + ...armors, + ...shields, + ...accessories, + ]; } diff --git a/lib/game/model/entity.dart b/lib/game/model/entity.dart index ae220ee..1e43cee 100644 --- a/lib/game/model/entity.dart +++ b/lib/game/model/entity.dart @@ -1,4 +1,5 @@ import 'item.dart'; +import 'status_effect.dart'; class Character { String name; @@ -7,10 +8,14 @@ class Character { int armor; // Current temporary shield/armor points in battle int baseAtk; int baseDefense; // Base defense stat + int gold; // New: Currency Map equipment = {}; List inventory = []; final int maxInventorySize = 16; + // Active status effects + List statusEffects = []; + Character({ required this.name, int? hp, @@ -18,10 +23,50 @@ class Character { required this.armor, required int atk, this.baseDefense = 0, + this.gold = 0, }) : baseMaxHp = maxHp, baseAtk = atk, hp = hp ?? maxHp; + /// Adds a status effect. If it already exists, it refreshes duration or stacks based on logic. + /// For now, we'll implement a simple refresh/overwrite logic. + void addStatusEffect(StatusEffect newEffect) { + // Check if effect exists + var existing = statusEffects + .where((e) => e.type == newEffect.type) + .firstOrNull; + + if (existing != null) { + // Refresh duration if the new one is longer, or just reset it? + // Let's max the duration for now. + if (newEffect.duration > existing.duration) { + existing.duration = newEffect.duration; + } + // Logic for 'value' (stacking bleed?) can be added here. + } else { + statusEffects.add(newEffect); + } + } + + /// Decrements duration of all effects and removes expired ones. + /// Returns a list of expired effects if needed for UI logs. + void updateStatusEffects() { + // Remove effects with 0 or less duration first (safety cleanup) + statusEffects.removeWhere((e) => e.duration <= 0); + + for (var effect in statusEffects) { + effect.duration--; + } + + // Remove effects that just expired (duration went to 0 or -1) + statusEffects.removeWhere((e) => e.duration <= 0); + } + + /// Helper to check if character has a specific status + bool hasStatus(StatusEffectType type) { + return statusEffects.any((e) => e.type == type); + } + int get totalMaxHp { int bonus = equipment.values.fold(0, (sum, item) => sum + item.hpBonus); return baseMaxHp + bonus; diff --git a/lib/game/model/item.dart b/lib/game/model/item.dart index c820b06..f00b841 100644 --- a/lib/game/model/item.dart +++ b/lib/game/model/item.dart @@ -1,5 +1,33 @@ +import 'status_effect.dart'; + enum EquipmentSlot { weapon, armor, shield, accessory } +/// Defines an effect that an item can apply (e.g., 10% chance to Stun for 1 turn) +class ItemEffect { + final StatusEffectType type; + final int probability; // 0 to 100 + final int duration; + final int value; // e.g., bleed damage amount + + ItemEffect({ + required this.type, + required this.probability, + required this.duration, + this.value = 0, + }); + + String get description { + String typeStr = type.name.toUpperCase(); + // Customize names if needed + if (type == StatusEffectType.defenseForbidden) typeStr = "UNBLOCKABLE"; + + String durationStr = "${duration}t"; + String valStr = value > 0 ? " ($value dmg)" : ""; + + return "$typeStr ${probability}% ($durationStr)$valStr"; + } +} + class Item { final String name; final String description; @@ -7,6 +35,8 @@ class Item { final int hpBonus; final int armorBonus; // New stat for defense final EquipmentSlot slot; + final List effects; // Status effects this item can inflict + final int price; // New: Sell/Buy value Item({ required this.name, @@ -15,6 +45,8 @@ class Item { required this.hpBonus, this.armorBonus = 0, // Default to 0 for backward compatibility required this.slot, + this.effects = const [], // Default to no effects + this.price = 0, }); String get typeName { diff --git a/lib/game/model/stage.dart b/lib/game/model/stage.dart new file mode 100644 index 0000000..622a29c --- /dev/null +++ b/lib/game/model/stage.dart @@ -0,0 +1,21 @@ +import 'entity.dart'; +import 'item.dart'; + +enum StageType { + battle, // Normal battle + elite, // Stronger enemy + shop, // Buy/Sell items + rest, // Heal or repair +} + +class StageModel { + final StageType type; + final Character? enemy; // For battle/elite + final List shopItems; // For shop + + StageModel({ + required this.type, + this.enemy, + this.shopItems = const [], + }); +} diff --git a/lib/game/model/status_effect.dart b/lib/game/model/status_effect.dart new file mode 100644 index 0000000..185ee45 --- /dev/null +++ b/lib/game/model/status_effect.dart @@ -0,0 +1,18 @@ +enum StatusEffectType { + stun, // Cannot act this turn + vulnerable, // Takes 50% more damage + bleed, // Takes damage at start/end of turn + defenseForbidden, // Cannot use Defend action +} + +class StatusEffect { + final StatusEffectType type; + int duration; // Turns remaining + final int value; // Intensity (e.g., bleed damage amount) + + StatusEffect({ + required this.type, + required this.duration, + this.value = 0, + }); +} diff --git a/lib/main.dart b/lib/main.dart index 415e57b..a016010 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'providers/battle_provider.dart'; -import 'screens/main_wrapper.dart'; +import 'screens/main_menu_screen.dart'; void main() { runApp(const MyApp()); @@ -19,7 +19,7 @@ class MyApp extends StatelessWidget { child: MaterialApp( title: "Colosseum's Choice", theme: ThemeData.dark(), - home: const MainWrapper(), + home: const MainMenuScreen(), ), ); } diff --git a/lib/providers/battle_provider.dart b/lib/providers/battle_provider.dart index a228238..4dfd7c5 100644 --- a/lib/providers/battle_provider.dart +++ b/lib/providers/battle_provider.dart @@ -3,15 +3,21 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; import '../game/model/entity.dart'; import '../game/model/item.dart'; -import '../game/data/item_table.dart'; // Import ItemTable -import '../utils/game_math.dart'; // Import GameMath +import '../game/model/status_effect.dart'; +import '../game/model/stage.dart'; // Import StageModel +import '../game/data/item_table.dart'; +import '../utils/game_math.dart'; enum ActionType { attack, defend } + enum RiskLevel { safe, normal, risky } class BattleProvider with ChangeNotifier { late Character player; - late Character enemy; + late Character enemy; // Kept for compatibility, active during Battle/Elite + + late StageModel currentStage; // The current stage object + List battleLogs = []; bool isPlayerTurn = true; @@ -20,51 +26,169 @@ class BattleProvider with ChangeNotifier { bool showRewardPopup = false; BattleProvider() { - initializeBattle(); + // initializeBattle(); // Do not auto-start logic } void initializeBattle() { stage = 1; - player = Character(name: "Player", maxHp: 100, armor: 0, atk: 10, baseDefense: 5); // Added baseDefense 5 - + player = Character( + name: "Player", + maxHp: 100, + armor: 0, + atk: 10, + baseDefense: 5, + ); + // Provide starter equipment - final starterSword = Item(name: "Wooden Sword", description: "A basic sword", atkBonus: 5, hpBonus: 0, slot: EquipmentSlot.weapon); - final starterArmor = Item(name: "Leather Armor", description: "Basic protection", atkBonus: 0, hpBonus: 20, slot: EquipmentSlot.armor); - final starterShield = Item(name: "Wooden Shield", description: "A small shield", atkBonus: 0, hpBonus: 0, armorBonus: 3, slot: EquipmentSlot.shield); - final starterRing = Item(name: "Copper Ring", description: "A simple ring", atkBonus: 1, hpBonus: 5, slot: EquipmentSlot.accessory); + final starterSword = Item( + name: "Wooden Sword", + description: "A basic sword", + atkBonus: 5, + hpBonus: 0, + slot: EquipmentSlot.weapon, + ); + final starterArmor = Item( + name: "Leather Armor", + description: "Basic protection", + atkBonus: 0, + hpBonus: 20, + slot: EquipmentSlot.armor, + ); + final starterShield = Item( + name: "Wooden Shield", + description: "A small shield", + atkBonus: 0, + hpBonus: 0, + armorBonus: 3, + slot: EquipmentSlot.shield, + ); + final starterRing = Item( + name: "Copper Ring", + description: "A simple ring", + atkBonus: 1, + hpBonus: 5, + slot: EquipmentSlot.accessory, + ); player.addToInventory(starterSword); player.equip(starterSword); - + player.addToInventory(starterArmor); player.equip(starterArmor); - + player.addToInventory(starterShield); player.equip(starterShield); - + player.addToInventory(starterRing); player.equip(starterRing); - _spawnEnemy(); + // Add new status effect items for testing + player.addToInventory(ItemTable.weapons[3].createItem()); // Stunning Hammer + player.addToInventory(ItemTable.weapons[4].createItem()); // Jagged Dagger + player.addToInventory(ItemTable.weapons[5].createItem()); // Sunderer Axe + player.addToInventory(ItemTable.shields[3].createItem()); // Cursed Shield + + _prepareNextStage(); battleLogs.clear(); - _addLog("Battle started! Stage $stage"); - isPlayerTurn = true; - showRewardPopup = false; + _addLog("Game Started! Stage 1"); notifyListeners(); } - void _spawnEnemy() { - int enemyHp = 5 + (stage - 1) * 20; - int enemyAtk = 8 + (stage - 1) * 2; - enemy = Character(name: "Enemy", maxHp: enemyHp, armor: 0, atk: enemyAtk); + void _prepareNextStage() { + StageType type; + + // Stage Type Logic + if (stage % 10 == 0) { + type = StageType.elite; // Every 10th stage is a Boss/Elite + } else if (stage % 5 == 0) { + type = StageType.shop; // Every 5th stage is a Shop (except 10, 20...) + } else if (stage % 8 == 0) { + type = StageType.rest; // Every 8th stage is a Rest + } else { + type = StageType.battle; + } + + // Prepare Data based on Type + Character? newEnemy; + List shopItems = []; + + if (type == StageType.battle || type == StageType.elite) { + bool isElite = type == StageType.elite; + int hpMultiplier = isElite ? 1 : 1; + int atkMultiplier = isElite ? 4 : 2; + + int enemyHp = 1 + (stage - 1) * hpMultiplier; + int enemyAtk = 8 + (stage - 1) * atkMultiplier; + + String name = isElite ? "Elite Guardian" : "Enemy"; + newEnemy = Character(name: name, maxHp: enemyHp, armor: 0, atk: enemyAtk); + + // Assign to the main 'enemy' field for UI compatibility + enemy = newEnemy; + isPlayerTurn = true; + showRewardPopup = false; + + _addLog("Stage $stage ($type) started! A wild ${enemy.name} appeared."); + } else if (type == StageType.shop) { + // Generate random items for shop + final random = Random(); + List allTemplates = List.from(ItemTable.allItems); + allTemplates.shuffle(random); + + int count = min(4, allTemplates.length); + shopItems = allTemplates + .sublist(0, count) + .map((t) => t.createItem(stage: stage)) + .toList(); + + // Dummy enemy to prevent null errors in existing UI (until UI is fully updated) + enemy = Character(name: "Merchant", maxHp: 9999, armor: 0, atk: 0); + + _addLog("Stage $stage: Entered a Shop."); + } else if (type == StageType.rest) { + // Dummy enemy + enemy = Character(name: "Campfire", maxHp: 9999, armor: 0, atk: 0); + _addLog("Stage $stage: Found a safe resting spot."); + } + + currentStage = StageModel( + type: type, + enemy: newEnemy, + shopItems: shopItems, + ); + notifyListeners(); } + // Replaces _spawnEnemy + // void _spawnEnemy() { ... } - Removed + + /// Handle player's action choice + void playerAction(ActionType type, RiskLevel risk) { - if (!isPlayerTurn || player.isDead || enemy.isDead || showRewardPopup) return; + if (!isPlayerTurn || player.isDead || enemy.isDead || showRewardPopup) + return; + + // Update Enemy Status Effects at the start of Player's turn (user request) + + enemy.updateStatusEffects(); + + // 1. Check for Defense Forbidden status + if (type == ActionType.defend && + player.hasStatus(StatusEffectType.defenseForbidden)) { + _addLog("Cannot defend! You are under Defense Forbidden status."); + return; + } isPlayerTurn = false; notifyListeners(); + // 2. Process Start-of-Turn Effects (Stun, Bleed) + bool canAct = _processStartTurnEffects(player); + if (!canAct) { + _endPlayerTurn(); // Skip turn if stunned + return; + } + _addLog("Player chose to ${type.name} with ${risk.name} risk."); final random = Random(); @@ -91,8 +215,11 @@ class BattleProvider with ChangeNotifier { int damage = (player.totalAtk * efficiency).toInt(); _applyDamage(enemy, damage); _addLog("Player dealt $damage damage to Enemy."); + + // Try applying status effects from items + _tryApplyStatusEffects(player, enemy); } else { - int armorGained = (player.totalDefense * efficiency).toInt(); // Changed to totalDefense + int armorGained = (player.totalDefense * efficiency).toInt(); player.armor += armorGained; _addLog("Player gained $armorGained armor."); } @@ -105,40 +232,70 @@ class BattleProvider with ChangeNotifier { return; } + _endPlayerTurn(); + } + + void _endPlayerTurn() { + // Update durations at end of turn + player.updateStatusEffects(); + + // Check if enemy is dead from bleed + if (enemy.isDead) { + _onVictory(); + return; + } + Future.delayed(const Duration(seconds: 1), () => _enemyTurn()); } Future _enemyTurn() async { - if (!isPlayerTurn && (player.isDead || enemy.isDead)) return; // Check if it's the enemy's turn and battle is over + if (!isPlayerTurn && (player.isDead || enemy.isDead)) return; _addLog("Enemy's turn..."); + await Future.delayed(const Duration(seconds: 1)); - // Enemy attacks player - await Future.delayed(const Duration(seconds: 1)); // Simulating thinking time + // 1. Process Start-of-Turn Effects for Enemy + bool canAct = _processStartTurnEffects(enemy); - int incomingDamage = enemy.totalAtk; - int damageToHp = 0; + // Check death from bleed before acting + if (enemy.isDead) { + _onVictory(); + return; + } - if (player.armor > 0) { - if (player.armor >= incomingDamage) { - player.armor -= incomingDamage; - damageToHp = 0; - _addLog("Armor absorbed all $incomingDamage damage."); + if (canAct) { + int incomingDamage = enemy.totalAtk; + int damageToHp = 0; + + // Enemy attack logic + // (Simple logic: Enemy always attacks for now) + // Note: Enemy doesn't have equipment yet, so no effects applied by enemy. + + // Handle Player Armor + if (player.armor > 0) { + if (player.armor >= incomingDamage) { + player.armor -= incomingDamage; + damageToHp = 0; + _addLog("Armor absorbed all $incomingDamage damage."); + } else { + damageToHp = incomingDamage - player.armor; + _addLog("Armor absorbed ${player.armor} damage."); + player.armor = 0; + } } else { - damageToHp = incomingDamage - player.armor; - _addLog("Armor absorbed ${player.armor} damage."); - player.armor = 0; + damageToHp = incomingDamage; + } + + if (damageToHp > 0) { + _applyDamage(player, damageToHp); + _addLog("Enemy dealt $damageToHp damage to Player HP."); } } else { - damageToHp = incomingDamage; + _addLog("Enemy is stunned and cannot act!"); } - if (damageToHp > 0) { - _applyDamage(player, damageToHp); - _addLog("Enemy dealt $damageToHp damage to Player HP."); - } - - // Player's turn starts, armor decays + // Player Turn Start Logic + // Armor decay if (player.armor > 0) { player.armor = (player.armor * 0.5).toInt(); _addLog("Player's armor decayed to ${player.armor}."); @@ -152,7 +309,59 @@ class BattleProvider with ChangeNotifier { notifyListeners(); } + /// Process effects that happen at the start of the turn (Bleed, Stun). + /// Returns true if the character can act, false if stunned. + bool _processStartTurnEffects(Character character) { + bool canAct = true; + + // 1. Bleed Damage + var bleedEffects = character.statusEffects + .where((e) => e.type == StatusEffectType.bleed) + .toList(); + if (bleedEffects.isNotEmpty) { + int totalBleed = bleedEffects.fold(0, (sum, e) => sum + e.value); + character.hp -= totalBleed; + if (character.hp < 0) character.hp = 0; + _addLog("${character.name} takes $totalBleed bleed damage!"); + } + + // 2. Stun Check + if (character.hasStatus(StatusEffectType.stun)) { + canAct = false; + _addLog("${character.name} is stunned!"); + } + + return canAct; + } + + /// Tries to apply status effects from attacker's equipment to the target. + void _tryApplyStatusEffects(Character attacker, Character target) { + final random = Random(); + + for (var item in attacker.equipment.values) { + for (var effect in item.effects) { + // Roll for probability (0-100) + if (random.nextInt(100) < effect.probability) { + // Apply effect + final newStatus = StatusEffect( + type: effect.type, + duration: effect.duration, + value: effect.value, + ); + target.addStatusEffect(newStatus); + _addLog("Applied ${effect.type.name} to ${target.name}!"); + } + } + } + } + void _applyDamage(Character target, int damage) { + // Check Vulnerable + if (target.hasStatus(StatusEffectType.vulnerable)) { + damage = (damage * 1.5).toInt(); + _addLog("Vulnerable! Damage increased to $damage."); + } + target.hp -= damage; if (target.hp < 0) target.hp = 0; } @@ -164,7 +373,7 @@ class BattleProvider with ChangeNotifier { void _onVictory() { _addLog("Enemy defeated! Choose a reward."); - + final random = Random(); List allTemplates = List.from(ItemTable.allItems); allTemplates.shuffle(random); // Shuffle to randomize selection @@ -186,7 +395,7 @@ class BattleProvider with ChangeNotifier { } else { _addLog("Inventory is full! ${item.name} discarded."); } - + // Heal player after selecting reward int healAmount = GameMath.floor(player.totalMaxHp * 0.5); player.heal(healAmount); @@ -194,11 +403,12 @@ class BattleProvider with ChangeNotifier { stage++; showRewardPopup = false; - - _spawnEnemy(); - _addLog("Stage $stage started! A wild ${enemy.name} appeared."); - - isPlayerTurn = true; + + _prepareNextStage(); + + // Log moved to _prepareNextStage + + // isPlayerTurn = true; // Handled in _prepareNextStage for battles notifyListeners(); } @@ -206,7 +416,9 @@ class BattleProvider with ChangeNotifier { if (player.equip(item)) { _addLog("Equipped ${item.name}."); } else { - _addLog("Failed to equip ${item.name}."); // Should not happen if logic is correct + _addLog( + "Failed to equip ${item.name}.", + ); // Should not happen if logic is correct } notifyListeners(); } @@ -219,4 +431,25 @@ class BattleProvider with ChangeNotifier { } notifyListeners(); } + + void discardItem(Item item) { + if (player.inventory.remove(item)) { + _addLog("Discarded ${item.name}."); + notifyListeners(); + } + } + + void sellItem(Item item) { + if (player.inventory.remove(item)) { + player.gold += item.price; + _addLog("Sold ${item.name} for ${item.price} G."); + notifyListeners(); + } + } + + /// Proceed to next stage from non-battle stages (Shop, Rest) + void proceedToNextStage() { + stage++; + _prepareNextStage(); + } } diff --git a/lib/screens/battle_screen.dart b/lib/screens/battle_screen.dart index 5901028..f5d4956 100644 --- a/lib/screens/battle_screen.dart +++ b/lib/screens/battle_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:game_test/game/model/item.dart'; +import 'package:game_test/game/model/stage.dart'; // Import StageModel import 'package:provider/provider.dart'; import '../providers/battle_provider.dart'; import '../game/model/entity.dart'; @@ -19,7 +20,9 @@ class _BattleScreenState extends State { super.initState(); // Scroll to the bottom of the log when new messages are added WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollController.jumpTo(_scrollController.position.maxScrollExtent); + if (_scrollController.hasClients) { + _scrollController.jumpTo(_scrollController.position.maxScrollExtent); + } }); } @@ -120,8 +123,9 @@ class _BattleScreenState extends State { return Scaffold( appBar: AppBar( title: Consumer( - builder: (context, provider, child) => - Text("Colosseum's Choice - Stage ${provider.stage}"), + builder: (context, provider, child) => Text( + "Colosseum - Stage ${provider.stage} (${provider.currentStage.type.name.toUpperCase()})", + ), ), actions: [ IconButton( @@ -132,6 +136,49 @@ class _BattleScreenState extends State { ), body: Consumer( builder: (context, battleProvider, child) { + // UI Switching based on Stage Type + if (battleProvider.currentStage.type == StageType.shop) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.store, size: 64, color: Colors.amber), + const SizedBox(height: 16), + const Text("Merchant Shop", style: TextStyle(fontSize: 24)), + const SizedBox(height: 8), + const Text("Buying/Selling feature coming soon!"), + const SizedBox(height: 32), + ElevatedButton( + onPressed: () => battleProvider.proceedToNextStage(), + child: const Text("Leave Shop"), + ), + ], + ), + ); + } else if (battleProvider.currentStage.type == StageType.rest) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.local_hotel, size: 64, color: Colors.blue), + const SizedBox(height: 16), + const Text("Rest Area", style: TextStyle(fontSize: 24)), + const SizedBox(height: 8), + const Text("Take a breath and heal."), + const SizedBox(height: 32), + ElevatedButton( + onPressed: () { + battleProvider.player.heal(20); // Simple heal + battleProvider.proceedToNextStage(); + }, + child: const Text("Rest & Leave (+20 HP)"), + ), + ], + ), + ); + } + + // Default: Battle UI (for Battle and Elite) return Stack( children: [ Column( @@ -251,14 +298,30 @@ class _BattleScreenState extends State { if (item.hpBonus > 0) stats.add("+${item.hpBonus} HP"); if (item.armorBonus > 0) stats.add("+${item.armorBonus} DEF"); - if (stats.isEmpty) return const SizedBox.shrink(); // Hide if no stats + List effectTexts = item.effects.map((e) => e.description).toList(); - return Padding( - padding: const EdgeInsets.only(top: 4.0, bottom: 4.0), - child: Text( - stats.join(", "), - style: const TextStyle(fontSize: 12, color: Colors.blueAccent), - ), + if (stats.isEmpty && effectTexts.isEmpty) return const SizedBox.shrink(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (stats.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 4.0, bottom: 4.0), + child: Text( + stats.join(", "), + style: const TextStyle(fontSize: 12, color: Colors.blueAccent), + ), + ), + if (effectTexts.isNotEmpty) + Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Text( + effectTexts.join(", "), + style: const TextStyle(fontSize: 11, color: Colors.orangeAccent), + ), + ), + ], ); } @@ -282,6 +345,34 @@ class _BattleScreenState extends State { backgroundColor: Colors.grey, ), ), + // Display Active Status Effects + if (character.statusEffects.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Wrap( + spacing: 4.0, + children: character.statusEffects.map((effect) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.deepOrange, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + "${effect.type.name.toUpperCase()} (${effect.duration})", + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ); + }).toList(), + ), + ), if (!isEnemy) ...[ Text("Armor: ${character.armor}"), Text("ATK: ${character.totalAtk}"), diff --git a/lib/screens/character_selection_screen.dart b/lib/screens/character_selection_screen.dart new file mode 100644 index 0000000..02a8695 --- /dev/null +++ b/lib/screens/character_selection_screen.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../providers/battle_provider.dart'; +import 'main_wrapper.dart'; + +class CharacterSelectionScreen extends StatelessWidget { + const CharacterSelectionScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Choose Your Hero"), + centerTitle: true, + ), + body: Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: InkWell( + onTap: () { + // Initialize Game + context.read().initializeBattle(); + + // Navigate to Game Screen (MainWrapper) + // Using pushReplacement to prevent going back to selection + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (context) => const MainWrapper()), + (route) => false, + ); + }, + child: Card( + color: Colors.blueGrey[800], + elevation: 8, + child: Container( + width: 300, + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.shield, size: 80, color: Colors.blue), + const SizedBox(height: 16), + const Text( + "Warrior", + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 8), + const Text( + "A balanced fighter with a sword and shield. Great for beginners.", + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey), + ), + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 8), + const Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Text("HP: 100", style: TextStyle(fontWeight: FontWeight.bold)), + Text("ATK: 10", style: TextStyle(fontWeight: FontWeight.bold)), + Text("DEF: 5", style: TextStyle(fontWeight: FontWeight.bold)), + ], + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/inventory_screen.dart b/lib/screens/inventory_screen.dart index 321bfd1..e9422f3 100644 --- a/lib/screens/inventory_screen.dart +++ b/lib/screens/inventory_screen.dart @@ -3,6 +3,7 @@ import 'package:provider/provider.dart'; import '../providers/battle_provider.dart'; import '../game/model/item.dart'; import '../game/model/entity.dart'; +import '../game/model/stage.dart'; // Import StageModel class InventoryScreen extends StatelessWidget { const InventoryScreen({super.key}); @@ -40,7 +41,8 @@ class InventoryScreen extends StatelessWidget { ), _buildStatItem("ATK", "${player.totalAtk}"), _buildStatItem("DEF", "${player.totalDefense}"), - _buildStatItem("Shield", "${player.armor}"), // Temporary armor points + _buildStatItem("Shield", "${player.armor}"), + _buildStatItem("Gold", "${player.gold} G", color: Colors.amber), ], ), ], @@ -154,12 +156,8 @@ class InventoryScreen extends StatelessWidget { final item = player.inventory[index]; return InkWell( onTap: () { - // Show confirmation dialog before equipping - _showEquipConfirmationDialog( - context, - battleProvider, - item, - ); + // Show Action Dialog instead of direct Equip + _showItemActionDialog(context, battleProvider, item); }, child: Card( color: Colors.blueGrey[700], @@ -216,18 +214,143 @@ class InventoryScreen extends StatelessWidget { } } - Widget _buildStatItem(String label, String value) { + Widget _buildStatItem(String label, String value, {Color? color}) { return Column( children: [ Text(label, style: const TextStyle(color: Colors.grey, fontSize: 12)), Text( value, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: color, + ), ), ], ); } + /// Shows a menu with actions for the selected item (Equip, Discard, etc.) + void _showItemActionDialog( + BuildContext context, BattleProvider provider, Item item) { + + bool isShop = provider.currentStage.type == StageType.shop; + + showDialog( + context: context, + 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: Colors.blue), + SizedBox(width: 10), + Text("Equip"), + ], + ), + ), + ), + if (isShop) + SimpleDialogOption( + onPressed: () { + Navigator.pop(ctx); + _showSellConfirmationDialog(context, provider, item); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + const Icon(Icons.attach_money, color: Colors.amber), + const SizedBox(width: 10), + Text("Sell (${item.price} G)"), + ], + ), + ), + ), + SimpleDialogOption( + onPressed: () { + Navigator.pop(ctx); + _showDiscardConfirmationDialog(context, provider, item); + }, + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + Icon(Icons.delete, color: Colors.red), + SizedBox(width: 10), + Text("Discard"), + ], + ), + ), + ), + ], + ), + ); + } + + void _showSellConfirmationDialog( + BuildContext context, + BattleProvider provider, + Item item, + ) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text("Sell Item"), + content: Text("Sell ${item.name} for ${item.price} G?"), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text("Cancel"), + ), + ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: Colors.amber), + onPressed: () { + provider.sellItem(item); + Navigator.pop(ctx); + }, + child: const Text("Sell", style: TextStyle(color: Colors.black)), + ), + ], + ), + ); + } + + void _showDiscardConfirmationDialog( + BuildContext context, + BattleProvider provider, + Item item, + ) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text("Discard Item"), + content: Text("Are you sure you want to discard ${item.name}?"), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text("Cancel"), + ), + ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: Colors.red), + onPressed: () { + provider.discardItem(item); + Navigator.pop(ctx); + }, + child: const Text("Discard"), + ), + ], + ), + ); + } + void _showEquipConfirmationDialog( BuildContext context, BattleProvider provider, @@ -395,14 +518,32 @@ class InventoryScreen extends StatelessWidget { if (item.hpBonus > 0) stats.add("+${item.hpBonus} HP"); if (item.armorBonus > 0) stats.add("+${item.armorBonus} DEF"); - if (stats.isEmpty) return const SizedBox.shrink(); // Hide if no stats + // Include effects + List effectTexts = item.effects.map((e) => e.description).toList(); - return Padding( - padding: const EdgeInsets.only(top: 2.0, bottom: 2.0), - child: Text( - stats.join(", "), - style: const TextStyle(fontSize: 10, color: Colors.blueAccent), - ), + 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: 10, color: Colors.blueAccent), + textAlign: TextAlign.center, + ), + ), + if (effectTexts.isNotEmpty) + Padding( + padding: const EdgeInsets.only(bottom: 2.0), + child: Text( + effectTexts.join("\n"), + style: const TextStyle(fontSize: 9, color: Colors.orangeAccent), + textAlign: TextAlign.center, + ), + ), + ], ); } } \ No newline at end of file diff --git a/lib/screens/main_menu_screen.dart b/lib/screens/main_menu_screen.dart new file mode 100644 index 0000000..c507e95 --- /dev/null +++ b/lib/screens/main_menu_screen.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'character_selection_screen.dart'; + +class MainMenuScreen extends StatelessWidget { + const MainMenuScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + width: double.infinity, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.black, Colors.blueGrey[900]!], + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.gavel, size: 100, color: Colors.amber), + const SizedBox(height: 20), + const Text( + "COLOSSEUM'S CHOICE", + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + letterSpacing: 2.0, + color: Colors.white, + ), + ), + const SizedBox(height: 10), + const Text( + "Rise as a Legend", + style: TextStyle( + fontSize: 16, + color: Colors.grey, + fontStyle: FontStyle.italic, + ), + ), + const SizedBox(height: 60), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const CharacterSelectionScreen(), + ), + ); + }, + style: ElevatedButton.styleFrom( + padding: + const EdgeInsets.symmetric(horizontal: 50, vertical: 15), + backgroundColor: Colors.amber[700], + foregroundColor: Colors.black, + textStyle: + const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + child: const Text("START GAME"), + ), + ], + ), + ), + ); + } +} diff --git a/prompt/13_item_status_options.md b/prompt/13_item_status_options.md new file mode 100644 index 0000000..5fa3943 --- /dev/null +++ b/prompt/13_item_status_options.md @@ -0,0 +1,26 @@ +## 13. 아이템 옵션: 상태이상 부여 (Stun, Vulnerable, Bleed, DefenseForbidden) + +### 목표 +아이템에 상태이상 부여 옵션을 추가하고, 캐릭터가 이를 관리할 수 있도록 시스템을 구축한다. + +### 구현 내용 + +1. **`StatusEffectType` 및 `StatusEffect` 정의 (lib/game/model/status_effect.dart)** + * 기존 `entity.dart`에 정의되어 있던 `StatusEffectType` (Enum)과 `StatusEffect` (Class)를 `lib/game/model/status_effect.dart` 파일로 분리하여 순환 참조 문제를 해결하고 모듈성을 높였다. + * `StatusEffectType`에는 `stun` (기절), `vulnerable` (취약), `bleed` (출혈), `defenseForbidden` (방어불가)가 포함되었다. + * `StatusEffect` 클래스는 `type`, `duration`, `value` 필드를 가진다. + +2. **`Character` 클래스 업데이트 (lib/game/model/entity.dart)** + * `Character` 클래스에 `List statusEffects` 필드를 추가하여 현재 적용 중인 상태이상 목록을 관리할 수 있도록 했다. + * 상태이상을 추가하는 `addStatusEffect(StatusEffect newEffect)` 메서드를 추가했다 (기존 효과가 있다면 갱신). + * 상태이상의 지속 시간을 업데이트하고 만료된 효과를 제거하는 `updateStatusEffects()` 메서드를 추가했다. + * 특정 상태이상 보유 여부를 확인하는 `hasStatus(StatusEffectType type)` 헬퍼 메서드를 추가했다. + +3. **`Item` 클래스 업데이트 (lib/game/model/item.dart)** + * 아이템이 부여할 수 있는 상태이상의 종류, 확률, 지속시간, 강도를 정의하는 `ItemEffect` 클래스를 새로 추가했다. + * `Item` 클래스에 `List effects` 필드를 추가하여 아이템이 가질 수 있는 상태이상 부여 옵션을 정의할 수 있도록 했다. 이 필드는 생성자에서 `this.effects = const []`로 기본 초기화된다. + +### 다음 단계 +* `item_table.dart`에 상태이상 옵션을 포함한 아이템 데이터를 추가. +* `BattleProvider`에서 아이템의 `ItemEffect`를 기반으로 전투 중 상태이상을 발동시키고, `Character`의 `statusEffects`를 관리하는 로직 구현. +* `BattleProvider`에서 각 상태이상(`stun`, `vulnerable`, `bleed`, `defenseForbidden`)의 실제 효과를 적용하는 로직 구현. diff --git a/prompt/14_integrate_status_effects_and_item_data.md b/prompt/14_integrate_status_effects_and_item_data.md new file mode 100644 index 0000000..52a4b8d --- /dev/null +++ b/prompt/14_integrate_status_effects_and_item_data.md @@ -0,0 +1,27 @@ +## 14. BattleProvider에 상태이상 로직 통합 및 아이템 데이터 추가 준비 + +### 목표 +상태이상 시스템을 BattleProvider에 통합하고, 상태이상을 가진 아이템을 게임에 추가하기 위한 준비를 마친다. + +### 구현 내용 + +1. **`BattleProvider`의 상태이상 로직 통합 (lib/providers/battle_provider.dart)** + * `StatusEffect` 관련 정의 (`lib/game/model/status_effect.dart`)를 임포트했다. + * **턴 시작 시 효과 처리 (`_processStartTurnEffects`):** + * 캐릭터의 턴 시작 시 호출되어 출혈(Bleed) 데미지를 적용하고, 기절(Stun) 상태 여부를 체크하여 캐릭터의 행동 가능 여부를 결정한다. + * 기절 상태인 경우, 캐릭터는 해당 턴에 아무런 행동도 할 수 없다. + * **턴 종료 시 효과 갱신:** + * `playerAction` 및 `_enemyTurn` 메서드 내에서 턴 종료 시 `Character.updateStatusEffects()`를 호출하여 모든 상태이상의 `duration`을 감소시키고 만료된 효과를 제거한다. + * **공격 시 상태이상 부여 (`_tryApplyStatusEffects`):** + * 공격이 성공했을 때, 공격자(`player`)가 장착한 아이템들의 `ItemEffect` 목록을 순회한다. + * 각 `ItemEffect`의 `probability`에 따라 랜덤하게 상태이상을 굴려 피격자(`enemy`)에게 `Character.addStatusEffect()`를 통해 상태이상을 부여한다. + * **취약(Vulnerable) 효과 적용 (`_applyDamage`):** + * `_applyDamage` 메서드 내에서 피격자가 `vulnerable` 상태일 경우, 최종 데미지를 1.5배 증폭시킨다. + * **방어불가(DefenseForbidden) 효과 적용 (`playerAction`):** + * `playerAction` 메서드 초반에 플레이어가 `defenseForbidden` 상태일 경우, 방어 행동을 선택할 수 없도록 막는 로직을 추가했다. + +2. **`Character` 클래스 `onHitEffects` getter 추가 (lib/game/model/entity.dart) - 미실현** + * 이전 대화에서 `Character`에 `onHitEffects` getter를 추가하여 모든 장비의 효과를 모아주는 것을 논의했으나, `_tryApplyStatusEffects`가 `attacker.equipment.values`를 직접 순회하는 방식으로 구현되면서 현재는 미실현 상태이다. 필요시 추후 리팩토링될 수 있다. + +3. **상태이상을 가진 아이템 데이터 추가 준비:** + * 다음 단계로 `lib/game/data/item_table.dart`에 상태이상 효과를 가진 새로운 아이템들을 추가할 예정이다. 이 아이템들은 게임 시작 시 플레이어 인벤토리에 지급되어 테스트에 활용될 것이다. diff --git a/prompt/15_ui_display_status_effects.md b/prompt/15_ui_display_status_effects.md new file mode 100644 index 0000000..23411c5 --- /dev/null +++ b/prompt/15_ui_display_status_effects.md @@ -0,0 +1,20 @@ +## 15. UI 개선: 아이템 옵션 및 상태이상 표시 + +### 목표 +새로 추가된 아이템의 상태이상 부여 옵션을 `InventoryScreen`에서 확인할 수 있게 하고, 전투 중(`BattleScreen`) 캐릭터에게 적용된 상태이상을 시각적으로 표시한다. + +### 구현 내용 + +1. **ItemEffect 설명 텍스트 생성 로직** + * `ItemEffect` 클래스(혹은 `Item` 클래스)에 효과 정보를 읽기 쉬운 문자열(예: "Stun 20% (1 turn)")로 변환하는 헬퍼 메서드 또는 Getter를 추가한다. + +2. **InventoryScreen 수정** + * 아이템 상세 정보 팝업(혹은 리스트 아이템)에 기존 스탯(공격력, 방어력 등) 외에 `effects` 리스트를 순회하며 상태이상 옵션을 표시하는 UI를 추가한다. + +3. **BattleScreen 수정** + * **Active Status 표시:** 플레이어와 적의 정보 패널(HP 바 근처)에 현재 적용 중인 상태이상(`stun`, `bleed` 등)을 텍스트나 아이콘 형태(여기선 텍스트 칩 형태)로 표시한다. + * (선택 사항) 장착된 무기의 효과를 전투 화면에서도 알 수 있다면 좋지만, 우선순위는 현재 상태이상 상태(Active Status) 표시에 둔다. + +### 예상 결과 +* 인벤토리에서 "Stunning Hammer"를 클릭하면 "Atk: 10" 밑에 "Chance to Stun: 20% (1t)" 같은 설명이 보인다. +* 전투 중 적에게 기절을 걸면, 적 이름/HP 근처에 "[Stun: 1t]" 태그가 나타난다. diff --git a/prompt/16_status_effect_duration_adjustment.md b/prompt/16_status_effect_duration_adjustment.md new file mode 100644 index 0000000..94a61d2 --- /dev/null +++ b/prompt/16_status_effect_duration_adjustment.md @@ -0,0 +1,21 @@ +## 16. 상태이상 지속 턴 계산 로직 조정 (플레이어 턴 기준) + +### 목표 +상태이상(특히 적에게 부여된 디버프)의 지속 턴 계산 방식을 플레이어 턴을 기준으로 조정하여, 게임 플레이의 직관성을 높인다. + +### 문제점 인식 +이전 구현에서는 적에게 부여된 디버프의 지속 시간이 적의 턴이 종료될 때 감소했다. 이로 인해 아이템 설명에 "2턴 지속"이라고 명시된 효과가 실제 플레이어 체감 상으로는 1번의 공격 기회만 제공하는 등, 직관성과 밸런스 문제가 발생할 수 있었다. 플레이어는 자신이 부여한 디버프가 '자신의 턴'을 기준으로 유지되기를 기대한다. + +### 구현 내용 + +1. **적의 상태이상 지속 턴 감소 로직 이동 (lib/providers/battle_provider.dart)** + * 기존 `_enemyTurn` 메서드에서 적의 `enemy.updateStatusEffects()` 호출을 제거했다. 이는 적이 행동을 마칠 때 상태이상이 감소하던 로직을 없앤다. + * `playerAction` 메서드의 **시작 부분**에 `enemy.updateStatusEffects()` 호출을 추가했다. + * **결과:** 이제 적에게 걸린 상태이상의 지속 시간은 **플레이어의 턴이 시작될 때**마다 1씩 감소한다. 이는 플레이어가 자신의 턴이 시작될 때 '시간이 흘렀다'고 인식하는 직관적인 흐름에 맞추어진다. + +### 기대 효과 +* 플레이어는 적에게 부여한 디버프의 남은 턴 수를 자신의 턴 흐름에 맞춰 보다 직관적으로 이해하고 전략을 세울 수 있다. +* 예: "2턴 지속" 취약 디버프를 걸었을 때, 다음 내 턴에 공격하면 여전히 디버프가 유효한 것을 확인할 수 있다. (단, 부여된 턴 포함 2턴인 경우 여전히 1회 공격이므로, 2회 공격 혜택을 위해서는 아이템의 지속시간 데이터를 3턴 등으로 조정할 필요가 있을 수 있다.) + +### 다음 단계 +* 플레이어 테스트를 통해 상태이상 턴 계산 로직의 체감 및 밸런스를 검증하고, 필요시 아이템의 지속시간 데이터를 조정한다. diff --git a/prompt/17_inventory_discard_feature.md b/prompt/17_inventory_discard_feature.md new file mode 100644 index 0000000..20d8f86 --- /dev/null +++ b/prompt/17_inventory_discard_feature.md @@ -0,0 +1,26 @@ +## 17. 인벤토리 아이템 버리기 기능 (상점 기능 확장 대비) + +### 목표 +인벤토리(Bag)에 있는 아이템을 선택하여 삭제(버리기)할 수 있는 기능을 추가한다. 이 과정에서 실수로 버리는 것을 방지하기 위한 확인 다이얼로그를 구현하며, 향후 상점(판매) 기능 추가 시 UI 및 로직을 재활용할 수 있는 구조를 고려한다. + +### 구현 내용 + +1. **BattleProvider 수정 (lib/providers/battle_provider.dart)** + * `discardItem(Item item)` 메서드 추가: 인벤토리 리스트에서 해당 아이템을 제거하고 로그를 남긴다. + * 이 메서드는 추후 `sellItem` 등과 유사한 구조를 가지게 된다. + +2. **InventoryScreen 수정 (lib/screens/inventory_screen.dart)** + * **인터랙션 변경:** 가방(Bag)의 아이템 클릭 시, 기존에는 바로 '장착 확인창'이 떴으나, 이제는 **'아이템 옵션 메뉴(SimpleDialog)'**가 먼저 뜨도록 변경한다. + * **아이템 옵션 메뉴:** + * 옵션 1: **Equip** (기존 장착 로직 연결) + * 옵션 2: **Discard** (버리기 확인창 연결) + * (추후 Sell 옵션이 이곳에 추가될 수 있음) + * **버리기 확인 다이얼로그 (`_showDiscardConfirmationDialog`):** + * "정말 버리시겠습니까?" 메시지와 아이템 정보를 보여준다. + * 확인 시 `provider.discardItem(item)`을 호출한다. + * 이 다이얼로그 구조는 제목과 콜백 함수만 바꾸면 '판매 확인창'으로도 쉽게 재활용 가능하다. + +### 예상 결과 +* 인벤토리 아이템 클릭 -> [Equip, Discard] 메뉴 팝업. +* Discard 선택 -> "Discard [Item Name]?" 확인 팝업. +* Confirm -> 아이템 삭제 및 로그 출력. diff --git a/prompt/18_implement_stage_class_and_types.md b/prompt/18_implement_stage_class_and_types.md new file mode 100644 index 0000000..226dd44 --- /dev/null +++ b/prompt/18_implement_stage_class_and_types.md @@ -0,0 +1,32 @@ +## 18. 스테이지 클래스 도입 및 스테이지 타입(상점, 엘리트, 휴식) 구현 + +### 목표 +단순 `int`로 관리되던 스테이지 시스템을 `Stage` 클래스와 `StageType`으로 리팩토링하여 다양한 게임 모드(상점, 보스/엘리트, 휴식)를 지원할 수 있는 기반을 마련한다. + +### 구현 내용 + +1. **Stage 모델 정의 (lib/game/model/stage.dart)** + * `StageType` Enum: `battle`, `elite` (또는 boss), `shop`, `rest`. + * `Stage` Class: + * `type`: 스테이지 종류. + * `enemy`: 전투/엘리트 스테이지용 적 캐릭터 객체. + * `shopItems`: 상점 스테이지용 판매 아이템 리스트. + +2. **BattleProvider 리팩토링 (lib/providers/battle_provider.dart)** + * `Stage currentStage` 객체를 관리하도록 변경. + * `_spawnEnemy()` 메서드를 `_generateNextStage()`로 대체 및 확장. + * **스테이지 생성 규칙 (예시):** + * 10, 20 스테이지... : **Elite/Boss** + * 5, 15 스테이지... : **Shop** (아이템 3개 랜덤 생성) + * 8, 18 스테이지... : **Rest** (체력 회복 기회) + * 그 외: **Battle** (일반 몬스터) + * 상점 기능을 위한 `buyItem(Item item)`, `sellItem(Item item)` 메서드 준비 (UI 연결은 추후). + +3. **UI 대응 (BattleScreen)** + * `currentStage.type`에 따라 화면을 다르게 그릴 수 있도록 `build` 메서드 내 분기 처리. + * 우선 Battle 타입 외의 경우(Shop, Rest)에는 간단한 "Coming Soon" 또는 기본 UI 틀을 표시하여 에러를 방지한다. + +### 기대 효과 +* 게임의 흐름이 다채로워짐. +* 상점 구현을 위한 데이터 구조가 마련됨 (판매 기능 구현 가능). +* 추후 이벤트나 스토리 컷신 등을 스테이지 단위로 끼워 넣기 쉬워짐. diff --git a/prompt/19_shop_sell_and_gold.md b/prompt/19_shop_sell_and_gold.md new file mode 100644 index 0000000..397a1ff --- /dev/null +++ b/prompt/19_shop_sell_and_gold.md @@ -0,0 +1,30 @@ +## 19. 상점 판매 기능 및 골드 시스템 구현 + +### 목표 +캐릭터에게 화폐(Gold) 개념을 도입하고, 상점 스테이지(`StageType.shop`)에 진입했을 때 인벤토리에서 아이템을 판매하여 골드를 획득하는 기능을 구현한다. + +### 구현 내용 + +1. **Character 모델 수정 (lib/game/model/entity.dart)** + * `int gold` 필드를 추가한다 (기본값: 0 또는 초기 자금). + +2. **Item 모델 및 가격 책정 로직 (lib/game/model/item.dart, lib/game/data/item_table.dart)** + * `Item` 클래스에 `int price` 필드를 추가한다. + * 모든 아이템 데이터(`ItemTable`)에 일일이 가격을 적는 대신, `ItemTemplate.createItem()` 메서드 내부에서 스탯(ATK, HP, DEF)을 기반으로 가격을 자동 산출하는 공식을 적용하여 효율성을 높인다. + * 공식 예시: `(atk * 10) + (hp * 2) + (armor * 5) + (effects.length * 20)`. + +3. **BattleProvider 수정 (lib/providers/battle_provider.dart)** + * `sellItem(Item item)` 메서드 구현: + * 인벤토리에서 아이템 제거. + * 아이템 가격만큼 플레이어 `gold` 증가. + * 로그 기록 ("Sold [Item] for [Price] G"). + +4. **InventoryScreen 수정 (lib/screens/inventory_screen.dart)** + * 화면 상단(스탯 영역)에 현재 보유 `Gold` 표시. + * 아이템 클릭 시 뜨는 `_showItemActionDialog` 수정: + * 현재 스테이지가 `StageType.shop`일 경우에만 **[Sell]** 버튼을 활성화(또는 표시). + * **판매 확인 다이얼로그:** "Sell [Item] for [Price] G?" 확인 창 구현. + +### 예상 결과 +* 상점 스테이지에서 인벤토리를 열면 아이템을 팔아 골드를 모을 수 있다. +* 모은 골드는 추후 아이템 구매 기능에 사용된다. diff --git a/prompt/20_main_menu_and_character_selection.md b/prompt/20_main_menu_and_character_selection.md new file mode 100644 index 0000000..16fa5cf --- /dev/null +++ b/prompt/20_main_menu_and_character_selection.md @@ -0,0 +1,27 @@ +## 20. 메인 메뉴 및 캐릭터 선택 화면 구현 + +### 목표 +게임 실행 시 바로 전투 화면으로 진입하지 않고, 메인 타이틀 화면과 캐릭터 선택 과정을 거쳐 게임에 진입하도록 흐름을 변경한다. + +### 구현 내용 + +1. **Main Menu Screen (lib/screens/main_menu_screen.dart)** + * 게임 타이틀 ("Colosseum's Choice") 표시. + * [START] 버튼: 누르면 캐릭터 선택 화면으로 이동. + +2. **Character Selection Screen (lib/screens/character_selection_screen.dart)** + * 플레이어블 캐릭터(직업) 목록을 보여준다. + * 현재는 기본 "Warrior" (기존 initBattle 스탯) 하나만 제공한다. + * 캐릭터 카드/버튼 클릭 시: + * `BattleProvider.initializeBattle()`을 호출하여 게임 데이터를 초기화한다. + * `MainWrapper`(게임 메인 화면)로 이동한다 (`Navigator.pushReplacement`). + +3. **Main Entry Point 수정 (lib/main.dart)** + * 앱의 `home`을 `MainWrapper`에서 `MainMenuScreen`으로 변경한다. + +4. **BattleProvider 수정 (lib/providers/battle_provider.dart)** + * 생성자에서 `initializeBattle()`을 호출하지 않도록 변경한다 (게임 시작 시 명시적으로 호출하기 위함). + * `initializeBattle` 메서드가 호출될 때 기존 데이터가 확실히 초기화되도록 보장한다. + +### 예상 결과 +* 앱 실행 -> 메인 메뉴 -> Start -> 캐릭터 선택(Warrior) -> 게임 시작(1스테이지)