update
This commit is contained in:
parent
ae1ebdc6bf
commit
45c6185d3e
|
|
@ -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:
|
||||||
|
|
@ -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.",
|
||||||
|
|
@ -126,9 +201,9 @@ class ItemTable {
|
||||||
];
|
];
|
||||||
|
|
||||||
static List<ItemTemplate> get allItems => [
|
static List<ItemTemplate> get allItems => [
|
||||||
...weapons,
|
...weapons,
|
||||||
...armors,
|
...armors,
|
||||||
...shields,
|
...shields,
|
||||||
...accessories,
|
...accessories,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,51 +26,169 @@ 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);
|
||||||
|
|
||||||
player.addToInventory(starterArmor);
|
player.addToInventory(starterArmor);
|
||||||
player.equip(starterArmor);
|
player.equip(starterArmor);
|
||||||
|
|
||||||
player.addToInventory(starterShield);
|
player.addToInventory(starterShield);
|
||||||
player.equip(starterShield);
|
player.equip(starterShield);
|
||||||
|
|
||||||
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,40 +232,70 @@ 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);
|
||||||
|
|
||||||
int incomingDamage = enemy.totalAtk;
|
// Check death from bleed before acting
|
||||||
int damageToHp = 0;
|
if (enemy.isDead) {
|
||||||
|
_onVictory();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (player.armor > 0) {
|
if (canAct) {
|
||||||
if (player.armor >= incomingDamage) {
|
int incomingDamage = enemy.totalAtk;
|
||||||
player.armor -= incomingDamage;
|
int damageToHp = 0;
|
||||||
damageToHp = 0;
|
|
||||||
_addLog("Armor absorbed all $incomingDamage damage.");
|
// 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 {
|
} else {
|
||||||
damageToHp = incomingDamage - player.armor;
|
damageToHp = incomingDamage;
|
||||||
_addLog("Armor absorbed ${player.armor} damage.");
|
}
|
||||||
player.armor = 0;
|
|
||||||
|
if (damageToHp > 0) {
|
||||||
|
_applyDamage(player, damageToHp);
|
||||||
|
_addLog("Enemy dealt $damageToHp damage to Player HP.");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
damageToHp = incomingDamage;
|
_addLog("Enemy is stunned and cannot act!");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (damageToHp > 0) {
|
// Player Turn Start Logic
|
||||||
_applyDamage(player, damageToHp);
|
// Armor decay
|
||||||
_addLog("Enemy dealt $damageToHp damage to Player HP.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Player's turn starts, armor decays
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
@ -164,7 +373,7 @@ class BattleProvider with ChangeNotifier {
|
||||||
|
|
||||||
void _onVictory() {
|
void _onVictory() {
|
||||||
_addLog("Enemy defeated! Choose a reward.");
|
_addLog("Enemy defeated! Choose a reward.");
|
||||||
|
|
||||||
final random = Random();
|
final random = Random();
|
||||||
List<ItemTemplate> allTemplates = List.from(ItemTable.allItems);
|
List<ItemTemplate> allTemplates = List.from(ItemTable.allItems);
|
||||||
allTemplates.shuffle(random); // Shuffle to randomize selection
|
allTemplates.shuffle(random); // Shuffle to randomize selection
|
||||||
|
|
@ -186,7 +395,7 @@ class BattleProvider with ChangeNotifier {
|
||||||
} else {
|
} else {
|
||||||
_addLog("Inventory is full! ${item.name} discarded.");
|
_addLog("Inventory is full! ${item.name} discarded.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Heal player after selecting reward
|
// Heal player after selecting reward
|
||||||
int healAmount = GameMath.floor(player.totalMaxHp * 0.5);
|
int healAmount = GameMath.floor(player.totalMaxHp * 0.5);
|
||||||
player.heal(healAmount);
|
player.heal(healAmount);
|
||||||
|
|
@ -194,11 +403,12 @@ class BattleProvider with ChangeNotifier {
|
||||||
|
|
||||||
stage++;
|
stage++;
|
||||||
showRewardPopup = false;
|
showRewardPopup = false;
|
||||||
|
|
||||||
_spawnEnemy();
|
_prepareNextStage();
|
||||||
_addLog("Stage $stage started! A wild ${enemy.name} appeared.");
|
|
||||||
|
// Log moved to _prepareNextStage
|
||||||
isPlayerTurn = true;
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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((_) {
|
||||||
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
|
if (_scrollController.hasClients) {
|
||||||
|
_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();
|
||||||
padding: const EdgeInsets.only(top: 4.0, bottom: 4.0),
|
|
||||||
child: Text(
|
return Column(
|
||||||
stats.join(", "),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
style: const TextStyle(fontSize: 12, color: Colors.blueAccent),
|
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<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}"),
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
padding: const EdgeInsets.only(top: 2.0, bottom: 2.0),
|
|
||||||
child: Text(
|
return Column(
|
||||||
stats.join(", "),
|
children: [
|
||||||
style: const TextStyle(fontSize: 10, color: Colors.blueAccent),
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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`)의 실제 효과를 적용하는 로직 구현.
|
||||||
|
|
@ -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`에 상태이상 효과를 가진 새로운 아이템들을 추가할 예정이다. 이 아이템들은 게임 시작 시 플레이어 인벤토리에 지급되어 테스트에 활용될 것이다.
|
||||||
|
|
@ -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]" 태그가 나타난다.
|
||||||
|
|
@ -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턴 등으로 조정할 필요가 있을 수 있다.)
|
||||||
|
|
||||||
|
### 다음 단계
|
||||||
|
* 플레이어 테스트를 통해 상태이상 턴 계산 로직의 체감 및 밸런스를 검증하고, 필요시 아이템의 지속시간 데이터를 조정한다.
|
||||||
|
|
@ -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 -> 아이템 삭제 및 로그 출력.
|
||||||
|
|
@ -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 틀을 표시하여 에러를 방지한다.
|
||||||
|
|
||||||
|
### 기대 효과
|
||||||
|
* 게임의 흐름이 다채로워짐.
|
||||||
|
* 상점 구현을 위한 데이터 구조가 마련됨 (판매 기능 구현 가능).
|
||||||
|
* 추후 이벤트나 스토리 컷신 등을 스테이지 단위로 끼워 넣기 쉬워짐.
|
||||||
|
|
@ -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?" 확인 창 구현.
|
||||||
|
|
||||||
|
### 예상 결과
|
||||||
|
* 상점 스테이지에서 인벤토리를 열면 아이템을 팔아 골드를 모을 수 있다.
|
||||||
|
* 모은 골드는 추후 아이템 구매 기능에 사용된다.
|
||||||
|
|
@ -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스테이지)
|
||||||
Loading…
Reference in New Issue