This commit is contained in:
2025-12-02 01:35:39 +09:00
parent ae1ebdc6bf
commit 45c6185d3e
20 changed files with 1104 additions and 92 deletions
+88 -13
View File
@@ -1,4 +1,5 @@
import '../model/item.dart';
import '../model/status_effect.dart'; // Import StatusEffect for ItemEffect
class ItemTemplate {
final String name;
@@ -7,6 +8,7 @@ class ItemTemplate {
final int baseHp;
final int baseArmor;
final EquipmentSlot slot;
final List<ItemEffect> effects; // New: Effects this item can inflict
const ItemTemplate({
required this.name,
@@ -15,6 +17,7 @@ class ItemTemplate {
this.baseHp = 0,
this.baseArmor = 0,
required this.slot,
this.effects = const [], // Default to no effects
});
// Create an instance of Item based on this template, optionally scaling with stage
@@ -25,6 +28,13 @@ class ItemTemplate {
int scaledHp = baseHp > 0 ? baseHp + (stage - 1) * 5 : 0;
int scaledArmor = baseArmor > 0 ? baseArmor + (stage - 1) : 0;
// Calculate price based on stats
int calculatedPrice = (scaledAtk * 10) + (scaledHp * 2) + (scaledArmor * 5);
if (effects.isNotEmpty) {
calculatedPrice += effects.length * 50; // Bonus value for special effects
}
if (calculatedPrice < 10) calculatedPrice = 10; // Minimum price
return Item(
name: "$name${stage > 1 ? ' +${stage - 1}' : ''}", // Append +1, +2 etc.
description: description,
@@ -32,30 +42,73 @@ class ItemTemplate {
hpBonus: scaledHp,
armorBonus: scaledArmor,
slot: slot,
effects: effects, // Pass the effects to the Item
price: calculatedPrice,
);
}
}
class ItemTable {
static const List<ItemTemplate> weapons = [
ItemTemplate(
static final List<ItemTemplate> weapons = [
const ItemTemplate(
name: "Rusty Dagger",
description: "Old and rusty, but better than nothing.",
baseAtk: 3,
slot: EquipmentSlot.weapon,
),
ItemTemplate(
const ItemTemplate(
name: "Iron Sword",
description: "A standard soldier's sword.",
baseAtk: 8,
slot: EquipmentSlot.weapon,
),
ItemTemplate(
const ItemTemplate(
name: "Battle Axe",
description: "Heavy but powerful.",
baseAtk: 12,
slot: EquipmentSlot.weapon,
),
// New: Weapons with status effects
ItemTemplate(
name: "Stunning Hammer",
description: "A heavy hammer that can stun foes.",
baseAtk: 10,
slot: EquipmentSlot.weapon,
effects: [
ItemEffect(
type: StatusEffectType.stun,
probability: 20,
duration: 1,
), // 20% chance to stun for 1 turn
],
),
ItemTemplate(
name: "Jagged Dagger",
description: "A cruel dagger that causes bleeding.",
baseAtk: 7,
slot: EquipmentSlot.weapon,
effects: [
ItemEffect(
type: StatusEffectType.bleed,
probability: 30,
duration: 3,
value: 5,
), // 30% chance to bleed (5 dmg/turn for 3 turns)
],
),
ItemTemplate(
name: "Sunderer Axe",
description: "An axe that exposes enemy weaknesses.",
baseAtk: 11,
slot: EquipmentSlot.weapon,
effects: [
ItemEffect(
type: StatusEffectType.vulnerable,
probability: 100,
duration: 2,
), // 100% chance to make vulnerable for 2 turns
],
),
];
static const List<ItemTemplate> armors = [
@@ -79,25 +132,40 @@ class ItemTable {
),
];
static const List<ItemTemplate> shields = [
ItemTemplate(
static final List<ItemTemplate> shields = [
const ItemTemplate(
name: "Pot Lid",
description: "It was used for cooking.",
baseArmor: 1,
slot: EquipmentSlot.shield,
),
ItemTemplate(
const ItemTemplate(
name: "Wooden Shield",
description: "Sturdy oak wood.",
baseArmor: 3,
slot: EquipmentSlot.shield,
),
ItemTemplate(
const ItemTemplate(
name: "Kite Shield",
description: "Used by knights.",
baseArmor: 6,
slot: EquipmentSlot.shield,
),
// New: Shield with Defense Forbidden effect (example)
ItemTemplate(
name: "Cursed Shield",
description:
"A shield that prevents the wielder from defending themselves.",
baseArmor: 5,
slot: EquipmentSlot.shield,
effects: [
ItemEffect(
type: StatusEffectType.defenseForbidden,
probability: 100,
duration: 999,
), // Always prevent defending (long duration for testing)
],
),
];
static const List<ItemTemplate> accessories = [
@@ -108,6 +176,13 @@ class ItemTable {
baseHp: 5,
slot: EquipmentSlot.accessory,
),
ItemTemplate(
name: "Copper Ring",
description: "A simple ring",
baseAtk: 1,
baseHp: 5,
slot: EquipmentSlot.accessory,
),
ItemTemplate(
name: "Ruby Amulet",
description: "Glows with a faint red light.",
@@ -126,9 +201,9 @@ class ItemTable {
];
static List<ItemTemplate> get allItems => [
...weapons,
...armors,
...shields,
...accessories,
];
...weapons,
...armors,
...shields,
...accessories,
];
}
+45
View File
@@ -1,4 +1,5 @@
import 'item.dart';
import 'status_effect.dart';
class Character {
String name;
@@ -7,10 +8,14 @@ class Character {
int armor; // Current temporary shield/armor points in battle
int baseAtk;
int baseDefense; // Base defense stat
int gold; // New: Currency
Map<EquipmentSlot, Item> equipment = {};
List<Item> inventory = [];
final int maxInventorySize = 16;
// Active status effects
List<StatusEffect> statusEffects = [];
Character({
required this.name,
int? hp,
@@ -18,10 +23,50 @@ class Character {
required this.armor,
required int atk,
this.baseDefense = 0,
this.gold = 0,
}) : baseMaxHp = maxHp,
baseAtk = atk,
hp = hp ?? maxHp;
/// Adds a status effect. If it already exists, it refreshes duration or stacks based on logic.
/// For now, we'll implement a simple refresh/overwrite logic.
void addStatusEffect(StatusEffect newEffect) {
// Check if effect exists
var existing = statusEffects
.where((e) => e.type == newEffect.type)
.firstOrNull;
if (existing != null) {
// Refresh duration if the new one is longer, or just reset it?
// Let's max the duration for now.
if (newEffect.duration > existing.duration) {
existing.duration = newEffect.duration;
}
// Logic for 'value' (stacking bleed?) can be added here.
} else {
statusEffects.add(newEffect);
}
}
/// Decrements duration of all effects and removes expired ones.
/// Returns a list of expired effects if needed for UI logs.
void updateStatusEffects() {
// Remove effects with 0 or less duration first (safety cleanup)
statusEffects.removeWhere((e) => e.duration <= 0);
for (var effect in statusEffects) {
effect.duration--;
}
// Remove effects that just expired (duration went to 0 or -1)
statusEffects.removeWhere((e) => e.duration <= 0);
}
/// Helper to check if character has a specific status
bool hasStatus(StatusEffectType type) {
return statusEffects.any((e) => e.type == type);
}
int get totalMaxHp {
int bonus = equipment.values.fold(0, (sum, item) => sum + item.hpBonus);
return baseMaxHp + bonus;
+32
View File
@@ -1,5 +1,33 @@
import 'status_effect.dart';
enum EquipmentSlot { weapon, armor, shield, accessory }
/// Defines an effect that an item can apply (e.g., 10% chance to Stun for 1 turn)
class ItemEffect {
final StatusEffectType type;
final int probability; // 0 to 100
final int duration;
final int value; // e.g., bleed damage amount
ItemEffect({
required this.type,
required this.probability,
required this.duration,
this.value = 0,
});
String get description {
String typeStr = type.name.toUpperCase();
// Customize names if needed
if (type == StatusEffectType.defenseForbidden) typeStr = "UNBLOCKABLE";
String durationStr = "${duration}t";
String valStr = value > 0 ? " ($value dmg)" : "";
return "$typeStr ${probability}% ($durationStr)$valStr";
}
}
class Item {
final String name;
final String description;
@@ -7,6 +35,8 @@ class Item {
final int hpBonus;
final int armorBonus; // New stat for defense
final EquipmentSlot slot;
final List<ItemEffect> effects; // Status effects this item can inflict
final int price; // New: Sell/Buy value
Item({
required this.name,
@@ -15,6 +45,8 @@ class Item {
required this.hpBonus,
this.armorBonus = 0, // Default to 0 for backward compatibility
required this.slot,
this.effects = const [], // Default to no effects
this.price = 0,
});
String get typeName {
+21
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 [],
});
}
+18
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,
});
}
+2 -2
View File
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/battle_provider.dart';
import 'screens/main_wrapper.dart';
import 'screens/main_menu_screen.dart';
void main() {
runApp(const MyApp());
@@ -19,7 +19,7 @@ class MyApp extends StatelessWidget {
child: MaterialApp(
title: "Colosseum's Choice",
theme: ThemeData.dark(),
home: const MainWrapper(),
home: const MainMenuScreen(),
),
);
}
+284 -51
View File
@@ -3,15 +3,21 @@ import 'dart:math';
import 'package:flutter/foundation.dart';
import '../game/model/entity.dart';
import '../game/model/item.dart';
import '../game/data/item_table.dart'; // Import ItemTable
import '../utils/game_math.dart'; // Import GameMath
import '../game/model/status_effect.dart';
import '../game/model/stage.dart'; // Import StageModel
import '../game/data/item_table.dart';
import '../utils/game_math.dart';
enum ActionType { attack, defend }
enum RiskLevel { safe, normal, risky }
class BattleProvider with ChangeNotifier {
late Character player;
late Character enemy;
late Character enemy; // Kept for compatibility, active during Battle/Elite
late StageModel currentStage; // The current stage object
List<String> battleLogs = [];
bool isPlayerTurn = true;
@@ -20,51 +26,169 @@ class BattleProvider with ChangeNotifier {
bool showRewardPopup = false;
BattleProvider() {
initializeBattle();
// initializeBattle(); // Do not auto-start logic
}
void initializeBattle() {
stage = 1;
player = Character(name: "Player", maxHp: 100, armor: 0, atk: 10, baseDefense: 5); // Added baseDefense 5
player = Character(
name: "Player",
maxHp: 100,
armor: 0,
atk: 10,
baseDefense: 5,
);
// Provide starter equipment
final starterSword = Item(name: "Wooden Sword", description: "A basic sword", atkBonus: 5, hpBonus: 0, slot: EquipmentSlot.weapon);
final starterArmor = Item(name: "Leather Armor", description: "Basic protection", atkBonus: 0, hpBonus: 20, slot: EquipmentSlot.armor);
final starterShield = Item(name: "Wooden Shield", description: "A small shield", atkBonus: 0, hpBonus: 0, armorBonus: 3, slot: EquipmentSlot.shield);
final starterRing = Item(name: "Copper Ring", description: "A simple ring", atkBonus: 1, hpBonus: 5, slot: EquipmentSlot.accessory);
final starterSword = Item(
name: "Wooden Sword",
description: "A basic sword",
atkBonus: 5,
hpBonus: 0,
slot: EquipmentSlot.weapon,
);
final starterArmor = Item(
name: "Leather Armor",
description: "Basic protection",
atkBonus: 0,
hpBonus: 20,
slot: EquipmentSlot.armor,
);
final starterShield = Item(
name: "Wooden Shield",
description: "A small shield",
atkBonus: 0,
hpBonus: 0,
armorBonus: 3,
slot: EquipmentSlot.shield,
);
final starterRing = Item(
name: "Copper Ring",
description: "A simple ring",
atkBonus: 1,
hpBonus: 5,
slot: EquipmentSlot.accessory,
);
player.addToInventory(starterSword);
player.equip(starterSword);
player.addToInventory(starterArmor);
player.equip(starterArmor);
player.addToInventory(starterShield);
player.equip(starterShield);
player.addToInventory(starterRing);
player.equip(starterRing);
_spawnEnemy();
// Add new status effect items for testing
player.addToInventory(ItemTable.weapons[3].createItem()); // Stunning Hammer
player.addToInventory(ItemTable.weapons[4].createItem()); // Jagged Dagger
player.addToInventory(ItemTable.weapons[5].createItem()); // Sunderer Axe
player.addToInventory(ItemTable.shields[3].createItem()); // Cursed Shield
_prepareNextStage();
battleLogs.clear();
_addLog("Battle started! Stage $stage");
isPlayerTurn = true;
showRewardPopup = false;
_addLog("Game Started! Stage 1");
notifyListeners();
}
void _spawnEnemy() {
int enemyHp = 5 + (stage - 1) * 20;
int enemyAtk = 8 + (stage - 1) * 2;
enemy = Character(name: "Enemy", maxHp: enemyHp, armor: 0, atk: enemyAtk);
void _prepareNextStage() {
StageType type;
// Stage Type Logic
if (stage % 10 == 0) {
type = StageType.elite; // Every 10th stage is a Boss/Elite
} else if (stage % 5 == 0) {
type = StageType.shop; // Every 5th stage is a Shop (except 10, 20...)
} else if (stage % 8 == 0) {
type = StageType.rest; // Every 8th stage is a Rest
} else {
type = StageType.battle;
}
// Prepare Data based on Type
Character? newEnemy;
List<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) {
if (!isPlayerTurn || player.isDead || enemy.isDead || showRewardPopup) return;
if (!isPlayerTurn || player.isDead || enemy.isDead || showRewardPopup)
return;
// Update Enemy Status Effects at the start of Player's turn (user request)
enemy.updateStatusEffects();
// 1. Check for Defense Forbidden status
if (type == ActionType.defend &&
player.hasStatus(StatusEffectType.defenseForbidden)) {
_addLog("Cannot defend! You are under Defense Forbidden status.");
return;
}
isPlayerTurn = false;
notifyListeners();
// 2. Process Start-of-Turn Effects (Stun, Bleed)
bool canAct = _processStartTurnEffects(player);
if (!canAct) {
_endPlayerTurn(); // Skip turn if stunned
return;
}
_addLog("Player chose to ${type.name} with ${risk.name} risk.");
final random = Random();
@@ -91,8 +215,11 @@ class BattleProvider with ChangeNotifier {
int damage = (player.totalAtk * efficiency).toInt();
_applyDamage(enemy, damage);
_addLog("Player dealt $damage damage to Enemy.");
// Try applying status effects from items
_tryApplyStatusEffects(player, enemy);
} else {
int armorGained = (player.totalDefense * efficiency).toInt(); // Changed to totalDefense
int armorGained = (player.totalDefense * efficiency).toInt();
player.armor += armorGained;
_addLog("Player gained $armorGained armor.");
}
@@ -105,40 +232,70 @@ class BattleProvider with ChangeNotifier {
return;
}
_endPlayerTurn();
}
void _endPlayerTurn() {
// Update durations at end of turn
player.updateStatusEffects();
// Check if enemy is dead from bleed
if (enemy.isDead) {
_onVictory();
return;
}
Future.delayed(const Duration(seconds: 1), () => _enemyTurn());
}
Future<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...");
await Future.delayed(const Duration(seconds: 1));
// Enemy attacks player
await Future.delayed(const Duration(seconds: 1)); // Simulating thinking time
// 1. Process Start-of-Turn Effects for Enemy
bool canAct = _processStartTurnEffects(enemy);
int incomingDamage = enemy.totalAtk;
int damageToHp = 0;
// Check death from bleed before acting
if (enemy.isDead) {
_onVictory();
return;
}
if (player.armor > 0) {
if (player.armor >= incomingDamage) {
player.armor -= incomingDamage;
damageToHp = 0;
_addLog("Armor absorbed all $incomingDamage damage.");
if (canAct) {
int incomingDamage = enemy.totalAtk;
int damageToHp = 0;
// Enemy attack logic
// (Simple logic: Enemy always attacks for now)
// Note: Enemy doesn't have equipment yet, so no effects applied by enemy.
// Handle Player Armor
if (player.armor > 0) {
if (player.armor >= incomingDamage) {
player.armor -= incomingDamage;
damageToHp = 0;
_addLog("Armor absorbed all $incomingDamage damage.");
} else {
damageToHp = incomingDamage - player.armor;
_addLog("Armor absorbed ${player.armor} damage.");
player.armor = 0;
}
} else {
damageToHp = incomingDamage - player.armor;
_addLog("Armor absorbed ${player.armor} damage.");
player.armor = 0;
damageToHp = incomingDamage;
}
if (damageToHp > 0) {
_applyDamage(player, damageToHp);
_addLog("Enemy dealt $damageToHp damage to Player HP.");
}
} else {
damageToHp = incomingDamage;
_addLog("Enemy is stunned and cannot act!");
}
if (damageToHp > 0) {
_applyDamage(player, damageToHp);
_addLog("Enemy dealt $damageToHp damage to Player HP.");
}
// Player's turn starts, armor decays
// Player Turn Start Logic
// Armor decay
if (player.armor > 0) {
player.armor = (player.armor * 0.5).toInt();
_addLog("Player's armor decayed to ${player.armor}.");
@@ -152,7 +309,59 @@ class BattleProvider with ChangeNotifier {
notifyListeners();
}
/// Process effects that happen at the start of the turn (Bleed, Stun).
/// Returns true if the character can act, false if stunned.
bool _processStartTurnEffects(Character character) {
bool canAct = true;
// 1. Bleed Damage
var bleedEffects = character.statusEffects
.where((e) => e.type == StatusEffectType.bleed)
.toList();
if (bleedEffects.isNotEmpty) {
int totalBleed = bleedEffects.fold(0, (sum, e) => sum + e.value);
character.hp -= totalBleed;
if (character.hp < 0) character.hp = 0;
_addLog("${character.name} takes $totalBleed bleed damage!");
}
// 2. Stun Check
if (character.hasStatus(StatusEffectType.stun)) {
canAct = false;
_addLog("${character.name} is stunned!");
}
return canAct;
}
/// Tries to apply status effects from attacker's equipment to the target.
void _tryApplyStatusEffects(Character attacker, Character target) {
final random = Random();
for (var item in attacker.equipment.values) {
for (var effect in item.effects) {
// Roll for probability (0-100)
if (random.nextInt(100) < effect.probability) {
// Apply effect
final newStatus = StatusEffect(
type: effect.type,
duration: effect.duration,
value: effect.value,
);
target.addStatusEffect(newStatus);
_addLog("Applied ${effect.type.name} to ${target.name}!");
}
}
}
}
void _applyDamage(Character target, int damage) {
// Check Vulnerable
if (target.hasStatus(StatusEffectType.vulnerable)) {
damage = (damage * 1.5).toInt();
_addLog("Vulnerable! Damage increased to $damage.");
}
target.hp -= damage;
if (target.hp < 0) target.hp = 0;
}
@@ -164,7 +373,7 @@ class BattleProvider with ChangeNotifier {
void _onVictory() {
_addLog("Enemy defeated! Choose a reward.");
final random = Random();
List<ItemTemplate> allTemplates = List.from(ItemTable.allItems);
allTemplates.shuffle(random); // Shuffle to randomize selection
@@ -186,7 +395,7 @@ class BattleProvider with ChangeNotifier {
} else {
_addLog("Inventory is full! ${item.name} discarded.");
}
// Heal player after selecting reward
int healAmount = GameMath.floor(player.totalMaxHp * 0.5);
player.heal(healAmount);
@@ -194,11 +403,12 @@ class BattleProvider with ChangeNotifier {
stage++;
showRewardPopup = false;
_spawnEnemy();
_addLog("Stage $stage started! A wild ${enemy.name} appeared.");
isPlayerTurn = true;
_prepareNextStage();
// Log moved to _prepareNextStage
// isPlayerTurn = true; // Handled in _prepareNextStage for battles
notifyListeners();
}
@@ -206,7 +416,9 @@ class BattleProvider with ChangeNotifier {
if (player.equip(item)) {
_addLog("Equipped ${item.name}.");
} else {
_addLog("Failed to equip ${item.name}."); // Should not happen if logic is correct
_addLog(
"Failed to equip ${item.name}.",
); // Should not happen if logic is correct
}
notifyListeners();
}
@@ -219,4 +431,25 @@ class BattleProvider with ChangeNotifier {
}
notifyListeners();
}
void discardItem(Item item) {
if (player.inventory.remove(item)) {
_addLog("Discarded ${item.name}.");
notifyListeners();
}
}
void sellItem(Item item) {
if (player.inventory.remove(item)) {
player.gold += item.price;
_addLog("Sold ${item.name} for ${item.price} G.");
notifyListeners();
}
}
/// Proceed to next stage from non-battle stages (Shop, Rest)
void proceedToNextStage() {
stage++;
_prepareNextStage();
}
}
+101 -10
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:game_test/game/model/item.dart';
import 'package:game_test/game/model/stage.dart'; // Import StageModel
import 'package:provider/provider.dart';
import '../providers/battle_provider.dart';
import '../game/model/entity.dart';
@@ -19,7 +20,9 @@ class _BattleScreenState extends State<BattleScreen> {
super.initState();
// Scroll to the bottom of the log when new messages are added
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
if (_scrollController.hasClients) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
});
}
@@ -120,8 +123,9 @@ class _BattleScreenState extends State<BattleScreen> {
return Scaffold(
appBar: AppBar(
title: Consumer<BattleProvider>(
builder: (context, provider, child) =>
Text("Colosseum's Choice - Stage ${provider.stage}"),
builder: (context, provider, child) => Text(
"Colosseum - Stage ${provider.stage} (${provider.currentStage.type.name.toUpperCase()})",
),
),
actions: [
IconButton(
@@ -132,6 +136,49 @@ class _BattleScreenState extends State<BattleScreen> {
),
body: Consumer<BattleProvider>(
builder: (context, battleProvider, child) {
// UI Switching based on Stage Type
if (battleProvider.currentStage.type == StageType.shop) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.store, size: 64, color: Colors.amber),
const SizedBox(height: 16),
const Text("Merchant Shop", style: TextStyle(fontSize: 24)),
const SizedBox(height: 8),
const Text("Buying/Selling feature coming soon!"),
const SizedBox(height: 32),
ElevatedButton(
onPressed: () => battleProvider.proceedToNextStage(),
child: const Text("Leave Shop"),
),
],
),
);
} else if (battleProvider.currentStage.type == StageType.rest) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.local_hotel, size: 64, color: Colors.blue),
const SizedBox(height: 16),
const Text("Rest Area", style: TextStyle(fontSize: 24)),
const SizedBox(height: 8),
const Text("Take a breath and heal."),
const SizedBox(height: 32),
ElevatedButton(
onPressed: () {
battleProvider.player.heal(20); // Simple heal
battleProvider.proceedToNextStage();
},
child: const Text("Rest & Leave (+20 HP)"),
),
],
),
);
}
// Default: Battle UI (for Battle and Elite)
return Stack(
children: [
Column(
@@ -251,14 +298,30 @@ class _BattleScreenState extends State<BattleScreen> {
if (item.hpBonus > 0) stats.add("+${item.hpBonus} HP");
if (item.armorBonus > 0) stats.add("+${item.armorBonus} DEF");
if (stats.isEmpty) return const SizedBox.shrink(); // Hide if no stats
List<String> effectTexts = item.effects.map((e) => e.description).toList();
return Padding(
padding: const EdgeInsets.only(top: 4.0, bottom: 4.0),
child: Text(
stats.join(", "),
style: const TextStyle(fontSize: 12, color: Colors.blueAccent),
),
if (stats.isEmpty && effectTexts.isEmpty) return const SizedBox.shrink();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (stats.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 4.0, bottom: 4.0),
child: Text(
stats.join(", "),
style: const TextStyle(fontSize: 12, color: Colors.blueAccent),
),
),
if (effectTexts.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Text(
effectTexts.join(", "),
style: const TextStyle(fontSize: 11, color: Colors.orangeAccent),
),
),
],
);
}
@@ -282,6 +345,34 @@ class _BattleScreenState extends State<BattleScreen> {
backgroundColor: Colors.grey,
),
),
// Display Active Status Effects
if (character.statusEffects.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Wrap(
spacing: 4.0,
children: character.statusEffects.map((effect) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.deepOrange,
borderRadius: BorderRadius.circular(4),
),
child: Text(
"${effect.type.name.toUpperCase()} (${effect.duration})",
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
);
}).toList(),
),
),
if (!isEnemy) ...[
Text("Armor: ${character.armor}"),
Text("ATK: ${character.totalAtk}"),
@@ -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)),
],
),
],
),
),
),
),
),
),
);
}
}
+157 -16
View File
@@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
import '../providers/battle_provider.dart';
import '../game/model/item.dart';
import '../game/model/entity.dart';
import '../game/model/stage.dart'; // Import StageModel
class InventoryScreen extends StatelessWidget {
const InventoryScreen({super.key});
@@ -40,7 +41,8 @@ class InventoryScreen extends StatelessWidget {
),
_buildStatItem("ATK", "${player.totalAtk}"),
_buildStatItem("DEF", "${player.totalDefense}"),
_buildStatItem("Shield", "${player.armor}"), // Temporary armor points
_buildStatItem("Shield", "${player.armor}"),
_buildStatItem("Gold", "${player.gold} G", color: Colors.amber),
],
),
],
@@ -154,12 +156,8 @@ class InventoryScreen extends StatelessWidget {
final item = player.inventory[index];
return InkWell(
onTap: () {
// Show confirmation dialog before equipping
_showEquipConfirmationDialog(
context,
battleProvider,
item,
);
// Show Action Dialog instead of direct Equip
_showItemActionDialog(context, battleProvider, item);
},
child: Card(
color: Colors.blueGrey[700],
@@ -216,18 +214,143 @@ class InventoryScreen extends StatelessWidget {
}
}
Widget _buildStatItem(String label, String value) {
Widget _buildStatItem(String label, String value, {Color? color}) {
return Column(
children: [
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 12)),
Text(
value,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: color,
),
),
],
);
}
/// Shows a menu with actions for the selected item (Equip, Discard, etc.)
void _showItemActionDialog(
BuildContext context, BattleProvider provider, Item item) {
bool isShop = provider.currentStage.type == StageType.shop;
showDialog(
context: context,
builder: (ctx) => SimpleDialog(
title: Text("${item.name} Actions"),
children: [
SimpleDialogOption(
onPressed: () {
Navigator.pop(ctx);
_showEquipConfirmationDialog(context, provider, item);
},
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
Icon(Icons.shield, color: Colors.blue),
SizedBox(width: 10),
Text("Equip"),
],
),
),
),
if (isShop)
SimpleDialogOption(
onPressed: () {
Navigator.pop(ctx);
_showSellConfirmationDialog(context, provider, item);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
const Icon(Icons.attach_money, color: Colors.amber),
const SizedBox(width: 10),
Text("Sell (${item.price} G)"),
],
),
),
),
SimpleDialogOption(
onPressed: () {
Navigator.pop(ctx);
_showDiscardConfirmationDialog(context, provider, item);
},
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
Icon(Icons.delete, color: Colors.red),
SizedBox(width: 10),
Text("Discard"),
],
),
),
),
],
),
);
}
void _showSellConfirmationDialog(
BuildContext context,
BattleProvider provider,
Item item,
) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text("Sell Item"),
content: Text("Sell ${item.name} for ${item.price} G?"),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text("Cancel"),
),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.amber),
onPressed: () {
provider.sellItem(item);
Navigator.pop(ctx);
},
child: const Text("Sell", style: TextStyle(color: Colors.black)),
),
],
),
);
}
void _showDiscardConfirmationDialog(
BuildContext context,
BattleProvider provider,
Item item,
) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text("Discard Item"),
content: Text("Are you sure you want to discard ${item.name}?"),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text("Cancel"),
),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
onPressed: () {
provider.discardItem(item);
Navigator.pop(ctx);
},
child: const Text("Discard"),
),
],
),
);
}
void _showEquipConfirmationDialog(
BuildContext context,
BattleProvider provider,
@@ -395,14 +518,32 @@ class InventoryScreen extends StatelessWidget {
if (item.hpBonus > 0) stats.add("+${item.hpBonus} HP");
if (item.armorBonus > 0) stats.add("+${item.armorBonus} DEF");
if (stats.isEmpty) return const SizedBox.shrink(); // Hide if no stats
// Include effects
List<String> effectTexts = item.effects.map((e) => e.description).toList();
return Padding(
padding: const EdgeInsets.only(top: 2.0, bottom: 2.0),
child: Text(
stats.join(", "),
style: const TextStyle(fontSize: 10, color: Colors.blueAccent),
),
if (stats.isEmpty && effectTexts.isEmpty) return const SizedBox.shrink();
return Column(
children: [
if (stats.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 2.0, bottom: 2.0),
child: Text(
stats.join(", "),
style: const TextStyle(fontSize: 10, color: Colors.blueAccent),
textAlign: TextAlign.center,
),
),
if (effectTexts.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 2.0),
child: Text(
effectTexts.join("\n"),
style: const TextStyle(fontSize: 9, color: Colors.orangeAccent),
textAlign: TextAlign.center,
),
),
],
);
}
}
+67
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"),
),
],
),
),
);
}
}