This commit is contained in:
Horoli 2025-12-02 01:35:39 +09:00
parent ae1ebdc6bf
commit 45c6185d3e
20 changed files with 1104 additions and 92 deletions

3
devtools_options.yaml Normal file
View File

@ -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:

View File

@ -1,4 +1,5 @@
import '../model/item.dart'; import '../model/item.dart';
import '../model/status_effect.dart'; // Import StatusEffect for ItemEffect
class ItemTemplate { class ItemTemplate {
final String name; final String name;
@ -7,6 +8,7 @@ class ItemTemplate {
final int baseHp; final int baseHp;
final int baseArmor; final int baseArmor;
final EquipmentSlot slot; final EquipmentSlot slot;
final List<ItemEffect> effects; // New: Effects this item can inflict
const ItemTemplate({ const ItemTemplate({
required this.name, required this.name,
@ -15,6 +17,7 @@ class ItemTemplate {
this.baseHp = 0, this.baseHp = 0,
this.baseArmor = 0, this.baseArmor = 0,
required this.slot, required this.slot,
this.effects = const [], // Default to no effects
}); });
// Create an instance of Item based on this template, optionally scaling with stage // 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 scaledHp = baseHp > 0 ? baseHp + (stage - 1) * 5 : 0;
int scaledArmor = baseArmor > 0 ? baseArmor + (stage - 1) : 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( return Item(
name: "$name${stage > 1 ? ' +${stage - 1}' : ''}", // Append +1, +2 etc. name: "$name${stage > 1 ? ' +${stage - 1}' : ''}", // Append +1, +2 etc.
description: description, description: description,
@ -32,30 +42,73 @@ class ItemTemplate {
hpBonus: scaledHp, hpBonus: scaledHp,
armorBonus: scaledArmor, armorBonus: scaledArmor,
slot: slot, slot: slot,
effects: effects, // Pass the effects to the Item
price: calculatedPrice,
); );
} }
} }
class ItemTable { class ItemTable {
static const List<ItemTemplate> weapons = [ static final List<ItemTemplate> weapons = [
ItemTemplate( const ItemTemplate(
name: "Rusty Dagger", name: "Rusty Dagger",
description: "Old and rusty, but better than nothing.", description: "Old and rusty, but better than nothing.",
baseAtk: 3, baseAtk: 3,
slot: EquipmentSlot.weapon, slot: EquipmentSlot.weapon,
), ),
ItemTemplate( const ItemTemplate(
name: "Iron Sword", name: "Iron Sword",
description: "A standard soldier's sword.", description: "A standard soldier's sword.",
baseAtk: 8, baseAtk: 8,
slot: EquipmentSlot.weapon, slot: EquipmentSlot.weapon,
), ),
ItemTemplate( const ItemTemplate(
name: "Battle Axe", name: "Battle Axe",
description: "Heavy but powerful.", description: "Heavy but powerful.",
baseAtk: 12, baseAtk: 12,
slot: EquipmentSlot.weapon, 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<ItemTemplate> armors = [ static const List<ItemTemplate> armors = [
@ -79,25 +132,40 @@ class ItemTable {
), ),
]; ];
static const List<ItemTemplate> shields = [ static final List<ItemTemplate> shields = [
ItemTemplate( const ItemTemplate(
name: "Pot Lid", name: "Pot Lid",
description: "It was used for cooking.", description: "It was used for cooking.",
baseArmor: 1, baseArmor: 1,
slot: EquipmentSlot.shield, slot: EquipmentSlot.shield,
), ),
ItemTemplate( const ItemTemplate(
name: "Wooden Shield", name: "Wooden Shield",
description: "Sturdy oak wood.", description: "Sturdy oak wood.",
baseArmor: 3, baseArmor: 3,
slot: EquipmentSlot.shield, slot: EquipmentSlot.shield,
), ),
ItemTemplate( const ItemTemplate(
name: "Kite Shield", name: "Kite Shield",
description: "Used by knights.", description: "Used by knights.",
baseArmor: 6, baseArmor: 6,
slot: EquipmentSlot.shield, 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<ItemTemplate> accessories = [ static const List<ItemTemplate> accessories = [
@ -108,6 +176,13 @@ class ItemTable {
baseHp: 5, baseHp: 5,
slot: EquipmentSlot.accessory, slot: EquipmentSlot.accessory,
), ),
ItemTemplate(
name: "Copper Ring",
description: "A simple ring",
baseAtk: 1,
baseHp: 5,
slot: EquipmentSlot.accessory,
),
ItemTemplate( ItemTemplate(
name: "Ruby Amulet", name: "Ruby Amulet",
description: "Glows with a faint red light.", description: "Glows with a faint red light.",

View File

@ -1,4 +1,5 @@
import 'item.dart'; import 'item.dart';
import 'status_effect.dart';
class Character { class Character {
String name; String name;
@ -7,10 +8,14 @@ class Character {
int armor; // Current temporary shield/armor points in battle int armor; // Current temporary shield/armor points in battle
int baseAtk; int baseAtk;
int baseDefense; // Base defense stat int baseDefense; // Base defense stat
int gold; // New: Currency
Map<EquipmentSlot, Item> equipment = {}; Map<EquipmentSlot, Item> equipment = {};
List<Item> inventory = []; List<Item> inventory = [];
final int maxInventorySize = 16; final int maxInventorySize = 16;
// Active status effects
List<StatusEffect> statusEffects = [];
Character({ Character({
required this.name, required this.name,
int? hp, int? hp,
@ -18,10 +23,50 @@ class Character {
required this.armor, required this.armor,
required int atk, required int atk,
this.baseDefense = 0, this.baseDefense = 0,
this.gold = 0,
}) : baseMaxHp = maxHp, }) : baseMaxHp = maxHp,
baseAtk = atk, baseAtk = atk,
hp = hp ?? maxHp; 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 get totalMaxHp {
int bonus = equipment.values.fold(0, (sum, item) => sum + item.hpBonus); int bonus = equipment.values.fold(0, (sum, item) => sum + item.hpBonus);
return baseMaxHp + bonus; return baseMaxHp + bonus;

View File

@ -1,5 +1,33 @@
import 'status_effect.dart';
enum EquipmentSlot { weapon, armor, shield, accessory } 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 { class Item {
final String name; final String name;
final String description; final String description;
@ -7,6 +35,8 @@ class Item {
final int hpBonus; final int hpBonus;
final int armorBonus; // New stat for defense final int armorBonus; // New stat for defense
final EquipmentSlot slot; final EquipmentSlot slot;
final List<ItemEffect> effects; // Status effects this item can inflict
final int price; // New: Sell/Buy value
Item({ Item({
required this.name, required this.name,
@ -15,6 +45,8 @@ class Item {
required this.hpBonus, required this.hpBonus,
this.armorBonus = 0, // Default to 0 for backward compatibility this.armorBonus = 0, // Default to 0 for backward compatibility
required this.slot, required this.slot,
this.effects = const [], // Default to no effects
this.price = 0,
}); });
String get typeName { String get typeName {

21
lib/game/model/stage.dart Normal file
View File

@ -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<Item> shopItems; // For shop
StageModel({
required this.type,
this.enemy,
this.shopItems = const [],
});
}

View File

@ -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,
});
}

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'providers/battle_provider.dart'; import 'providers/battle_provider.dart';
import 'screens/main_wrapper.dart'; import 'screens/main_menu_screen.dart';
void main() { void main() {
runApp(const MyApp()); runApp(const MyApp());
@ -19,7 +19,7 @@ class MyApp extends StatelessWidget {
child: MaterialApp( child: MaterialApp(
title: "Colosseum's Choice", title: "Colosseum's Choice",
theme: ThemeData.dark(), theme: ThemeData.dark(),
home: const MainWrapper(), home: const MainMenuScreen(),
), ),
); );
} }

View File

@ -3,15 +3,21 @@ import 'dart:math';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import '../game/model/entity.dart'; import '../game/model/entity.dart';
import '../game/model/item.dart'; import '../game/model/item.dart';
import '../game/data/item_table.dart'; // Import ItemTable import '../game/model/status_effect.dart';
import '../utils/game_math.dart'; // Import GameMath import '../game/model/stage.dart'; // Import StageModel
import '../game/data/item_table.dart';
import '../utils/game_math.dart';
enum ActionType { attack, defend } enum ActionType { attack, defend }
enum RiskLevel { safe, normal, risky } enum RiskLevel { safe, normal, risky }
class BattleProvider with ChangeNotifier { class BattleProvider with ChangeNotifier {
late Character player; late Character player;
late Character enemy; late Character enemy; // Kept for compatibility, active during Battle/Elite
late StageModel currentStage; // The current stage object
List<String> battleLogs = []; List<String> battleLogs = [];
bool isPlayerTurn = true; bool isPlayerTurn = true;
@ -20,18 +26,49 @@ class BattleProvider with ChangeNotifier {
bool showRewardPopup = false; bool showRewardPopup = false;
BattleProvider() { BattleProvider() {
initializeBattle(); // initializeBattle(); // Do not auto-start logic
} }
void initializeBattle() { void initializeBattle() {
stage = 1; 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 // Provide starter equipment
final starterSword = Item(name: "Wooden Sword", description: "A basic sword", atkBonus: 5, hpBonus: 0, slot: EquipmentSlot.weapon); final starterSword = Item(
final starterArmor = Item(name: "Leather Armor", description: "Basic protection", atkBonus: 0, hpBonus: 20, slot: EquipmentSlot.armor); name: "Wooden Sword",
final starterShield = Item(name: "Wooden Shield", description: "A small shield", atkBonus: 0, hpBonus: 0, armorBonus: 3, slot: EquipmentSlot.shield); description: "A basic sword",
final starterRing = Item(name: "Copper Ring", description: "A simple ring", atkBonus: 1, hpBonus: 5, slot: EquipmentSlot.accessory); 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.addToInventory(starterSword);
player.equip(starterSword); player.equip(starterSword);
@ -45,26 +82,113 @@ class BattleProvider with ChangeNotifier {
player.addToInventory(starterRing); player.addToInventory(starterRing);
player.equip(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(); battleLogs.clear();
_addLog("Battle started! Stage $stage"); _addLog("Game Started! Stage 1");
isPlayerTurn = true;
showRewardPopup = false;
notifyListeners(); notifyListeners();
} }
void _spawnEnemy() { void _prepareNextStage() {
int enemyHp = 5 + (stage - 1) * 20; StageType type;
int enemyAtk = 8 + (stage - 1) * 2;
enemy = Character(name: "Enemy", maxHp: enemyHp, armor: 0, atk: enemyAtk); // 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<Item> 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<ItemTemplate> 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) { 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; isPlayerTurn = false;
notifyListeners(); 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."); _addLog("Player chose to ${type.name} with ${risk.name} risk.");
final random = Random(); final random = Random();
@ -91,8 +215,11 @@ class BattleProvider with ChangeNotifier {
int damage = (player.totalAtk * efficiency).toInt(); int damage = (player.totalAtk * efficiency).toInt();
_applyDamage(enemy, damage); _applyDamage(enemy, damage);
_addLog("Player dealt $damage damage to Enemy."); _addLog("Player dealt $damage damage to Enemy.");
// Try applying status effects from items
_tryApplyStatusEffects(player, enemy);
} else { } else {
int armorGained = (player.totalDefense * efficiency).toInt(); // Changed to totalDefense int armorGained = (player.totalDefense * efficiency).toInt();
player.armor += armorGained; player.armor += armorGained;
_addLog("Player gained $armorGained armor."); _addLog("Player gained $armorGained armor.");
} }
@ -105,20 +232,46 @@ class BattleProvider with ChangeNotifier {
return; 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.delayed(const Duration(seconds: 1), () => _enemyTurn());
} }
Future<void> _enemyTurn() async { Future<void> _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..."); _addLog("Enemy's turn...");
await Future.delayed(const Duration(seconds: 1));
// Enemy attacks player // 1. Process Start-of-Turn Effects for Enemy
await Future.delayed(const Duration(seconds: 1)); // Simulating thinking time bool canAct = _processStartTurnEffects(enemy);
// Check death from bleed before acting
if (enemy.isDead) {
_onVictory();
return;
}
if (canAct) {
int incomingDamage = enemy.totalAtk; int incomingDamage = enemy.totalAtk;
int damageToHp = 0; 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 > 0) {
if (player.armor >= incomingDamage) { if (player.armor >= incomingDamage) {
player.armor -= incomingDamage; player.armor -= incomingDamage;
@ -137,8 +290,12 @@ class BattleProvider with ChangeNotifier {
_applyDamage(player, damageToHp); _applyDamage(player, damageToHp);
_addLog("Enemy dealt $damageToHp damage to Player HP."); _addLog("Enemy dealt $damageToHp damage to Player HP.");
} }
} else {
_addLog("Enemy is stunned and cannot act!");
}
// Player's turn starts, armor decays // Player Turn Start Logic
// Armor decay
if (player.armor > 0) { if (player.armor > 0) {
player.armor = (player.armor * 0.5).toInt(); player.armor = (player.armor * 0.5).toInt();
_addLog("Player's armor decayed to ${player.armor}."); _addLog("Player's armor decayed to ${player.armor}.");
@ -152,7 +309,59 @@ class BattleProvider with ChangeNotifier {
notifyListeners(); 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) { 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; target.hp -= damage;
if (target.hp < 0) target.hp = 0; if (target.hp < 0) target.hp = 0;
} }
@ -195,10 +404,11 @@ class BattleProvider with ChangeNotifier {
stage++; stage++;
showRewardPopup = false; showRewardPopup = false;
_spawnEnemy(); _prepareNextStage();
_addLog("Stage $stage started! A wild ${enemy.name} appeared.");
isPlayerTurn = true; // Log moved to _prepareNextStage
// isPlayerTurn = true; // Handled in _prepareNextStage for battles
notifyListeners(); notifyListeners();
} }
@ -206,7 +416,9 @@ class BattleProvider with ChangeNotifier {
if (player.equip(item)) { if (player.equip(item)) {
_addLog("Equipped ${item.name}."); _addLog("Equipped ${item.name}.");
} else { } 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(); notifyListeners();
} }
@ -219,4 +431,25 @@ class BattleProvider with ChangeNotifier {
} }
notifyListeners(); 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();
}
} }

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_test/game/model/item.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 'package:provider/provider.dart';
import '../providers/battle_provider.dart'; import '../providers/battle_provider.dart';
import '../game/model/entity.dart'; import '../game/model/entity.dart';
@ -19,7 +20,9 @@ class _BattleScreenState extends State<BattleScreen> {
super.initState(); super.initState();
// Scroll to the bottom of the log when new messages are added // Scroll to the bottom of the log when new messages are added
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent); _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
}); });
} }
@ -120,8 +123,9 @@ class _BattleScreenState extends State<BattleScreen> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Consumer<BattleProvider>( title: Consumer<BattleProvider>(
builder: (context, provider, child) => builder: (context, provider, child) => Text(
Text("Colosseum's Choice - Stage ${provider.stage}"), "Colosseum - Stage ${provider.stage} (${provider.currentStage.type.name.toUpperCase()})",
),
), ),
actions: [ actions: [
IconButton( IconButton(
@ -132,6 +136,49 @@ class _BattleScreenState extends State<BattleScreen> {
), ),
body: Consumer<BattleProvider>( body: Consumer<BattleProvider>(
builder: (context, battleProvider, child) { 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( return Stack(
children: [ children: [
Column( Column(
@ -251,14 +298,30 @@ class _BattleScreenState extends State<BattleScreen> {
if (item.hpBonus > 0) stats.add("+${item.hpBonus} HP"); if (item.hpBonus > 0) stats.add("+${item.hpBonus} HP");
if (item.armorBonus > 0) stats.add("+${item.armorBonus} DEF"); if (item.armorBonus > 0) stats.add("+${item.armorBonus} DEF");
if (stats.isEmpty) return const SizedBox.shrink(); // Hide if no stats List<String> effectTexts = item.effects.map((e) => e.description).toList();
return Padding( 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), padding: const EdgeInsets.only(top: 4.0, bottom: 4.0),
child: Text( child: Text(
stats.join(", "), stats.join(", "),
style: const TextStyle(fontSize: 12, color: Colors.blueAccent), 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<BattleScreen> {
backgroundColor: Colors.grey, 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) ...[ if (!isEnemy) ...[
Text("Armor: ${character.armor}"), Text("Armor: ${character.armor}"),
Text("ATK: ${character.totalAtk}"), Text("ATK: ${character.totalAtk}"),

View File

@ -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<BattleProvider>().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)),
],
),
],
),
),
),
),
),
),
);
}
}

View File

@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
import '../providers/battle_provider.dart'; import '../providers/battle_provider.dart';
import '../game/model/item.dart'; import '../game/model/item.dart';
import '../game/model/entity.dart'; import '../game/model/entity.dart';
import '../game/model/stage.dart'; // Import StageModel
class InventoryScreen extends StatelessWidget { class InventoryScreen extends StatelessWidget {
const InventoryScreen({super.key}); const InventoryScreen({super.key});
@ -40,7 +41,8 @@ class InventoryScreen extends StatelessWidget {
), ),
_buildStatItem("ATK", "${player.totalAtk}"), _buildStatItem("ATK", "${player.totalAtk}"),
_buildStatItem("DEF", "${player.totalDefense}"), _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]; final item = player.inventory[index];
return InkWell( return InkWell(
onTap: () { onTap: () {
// Show confirmation dialog before equipping // Show Action Dialog instead of direct Equip
_showEquipConfirmationDialog( _showItemActionDialog(context, battleProvider, item);
context,
battleProvider,
item,
);
}, },
child: Card( child: Card(
color: Colors.blueGrey[700], 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( return Column(
children: [ children: [
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 12)), Text(label, style: const TextStyle(color: Colors.grey, fontSize: 12)),
Text( Text(
value, 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( void _showEquipConfirmationDialog(
BuildContext context, BuildContext context,
BattleProvider provider, BattleProvider provider,
@ -395,14 +518,32 @@ class InventoryScreen extends StatelessWidget {
if (item.hpBonus > 0) stats.add("+${item.hpBonus} HP"); if (item.hpBonus > 0) stats.add("+${item.hpBonus} HP");
if (item.armorBonus > 0) stats.add("+${item.armorBonus} DEF"); if (item.armorBonus > 0) stats.add("+${item.armorBonus} DEF");
if (stats.isEmpty) return const SizedBox.shrink(); // Hide if no stats // Include effects
List<String> effectTexts = item.effects.map((e) => e.description).toList();
return Padding( 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), padding: const EdgeInsets.only(top: 2.0, bottom: 2.0),
child: Text( child: Text(
stats.join(", "), stats.join(", "),
style: const TextStyle(fontSize: 10, color: Colors.blueAccent), 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,
),
),
],
); );
} }
} }

View File

@ -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"),
),
],
),
),
);
}
}

View File

@ -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<StatusEffect> statusEffects` 필드를 추가하여 현재 적용 중인 상태이상 목록을 관리할 수 있도록 했다.
* 상태이상을 추가하는 `addStatusEffect(StatusEffect newEffect)` 메서드를 추가했다 (기존 효과가 있다면 갱신).
* 상태이상의 지속 시간을 업데이트하고 만료된 효과를 제거하는 `updateStatusEffects()` 메서드를 추가했다.
* 특정 상태이상 보유 여부를 확인하는 `hasStatus(StatusEffectType type)` 헬퍼 메서드를 추가했다.
3. **`Item` 클래스 업데이트 (lib/game/model/item.dart)**
* 아이템이 부여할 수 있는 상태이상의 종류, 확률, 지속시간, 강도를 정의하는 `ItemEffect` 클래스를 새로 추가했다.
* `Item` 클래스에 `List<ItemEffect> effects` 필드를 추가하여 아이템이 가질 수 있는 상태이상 부여 옵션을 정의할 수 있도록 했다. 이 필드는 생성자에서 `this.effects = const []`로 기본 초기화된다.
### 다음 단계
* `item_table.dart`에 상태이상 옵션을 포함한 아이템 데이터를 추가.
* `BattleProvider`에서 아이템의 `ItemEffect`를 기반으로 전투 중 상태이상을 발동시키고, `Character``statusEffects`를 관리하는 로직 구현.
* `BattleProvider`에서 각 상태이상(`stun`, `vulnerable`, `bleed`, `defenseForbidden`)의 실제 효과를 적용하는 로직 구현.

View File

@ -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`에 상태이상 효과를 가진 새로운 아이템들을 추가할 예정이다. 이 아이템들은 게임 시작 시 플레이어 인벤토리에 지급되어 테스트에 활용될 것이다.

View File

@ -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]" 태그가 나타난다.

View File

@ -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턴 등으로 조정할 필요가 있을 수 있다.)
### 다음 단계
* 플레이어 테스트를 통해 상태이상 턴 계산 로직의 체감 및 밸런스를 검증하고, 필요시 아이템의 지속시간 데이터를 조정한다.

View File

@ -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 -> 아이템 삭제 및 로그 출력.

View File

@ -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 틀을 표시하여 에러를 방지한다.
### 기대 효과
* 게임의 흐름이 다채로워짐.
* 상점 구현을 위한 데이터 구조가 마련됨 (판매 기능 구현 가능).
* 추후 이벤트나 스토리 컷신 등을 스테이지 단위로 끼워 넣기 쉬워짐.

View File

@ -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?" 확인 창 구현.
### 예상 결과
* 상점 스테이지에서 인벤토리를 열면 아이템을 팔아 골드를 모을 수 있다.
* 모은 골드는 추후 아이템 구매 기능에 사용된다.

View File

@ -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스테이지)