This commit is contained in:
2025-12-02 17:28:16 +09:00
parent b4c17a3a00
commit 457eed4d3e
14 changed files with 740 additions and 191 deletions
+60
View File
@@ -0,0 +1,60 @@
import 'dart:convert';
import 'package:flutter/services.dart';
import '../model/entity.dart';
class EnemyTemplate {
final String name;
final int baseHp;
final int baseAtk;
final int baseDefense;
const EnemyTemplate({
required this.name,
required this.baseHp,
required this.baseAtk,
required this.baseDefense,
});
factory EnemyTemplate.fromJson(Map<String, dynamic> json) {
return EnemyTemplate(
name: json['name'],
baseHp: json['baseHp'] ?? 10,
baseAtk: json['baseAtk'] ?? 1,
baseDefense: json['baseDefense'] ?? 0,
);
}
Character createCharacter({int stage = 1}) {
// Simple additive scaling
int scaledHp = baseHp + (stage - 1) * 5;
int scaledAtk = baseAtk + (stage - 1);
int scaledDefense = baseDefense + (stage ~/ 5); // +1 defense every 5 stages
return Character(
name: name,
maxHp: scaledHp,
atk: scaledAtk,
baseDefense: scaledDefense,
armor: 0,
);
}
}
class EnemyTable {
static List<EnemyTemplate> normalEnemies = [];
static List<EnemyTemplate> eliteEnemies = [];
static Future<void> load() async {
final String jsonString = await rootBundle.loadString(
'assets/data/enemies.json',
);
final Map<String, dynamic> data = jsonDecode(jsonString);
normalEnemies = (data['normal'] as List)
.map((e) => EnemyTemplate.fromJson(e))
.toList();
eliteEnemies = (data['elite'] as List)
.map((e) => EnemyTemplate.fromJson(e))
.toList();
}
}
+32 -151
View File
@@ -1,5 +1,7 @@
import 'dart:convert';
import 'package:flutter/services.dart';
import '../model/item.dart';
import '../model/status_effect.dart'; // Import StatusEffect for ItemEffect
import '../model/status_effect.dart';
class ItemTemplate {
final String name;
@@ -8,7 +10,7 @@ class ItemTemplate {
final int baseHp;
final int baseArmor;
final EquipmentSlot slot;
final List<ItemEffect> effects; // New: Effects this item can inflict
final List<ItemEffect> effects;
const ItemTemplate({
required this.name,
@@ -17,9 +19,24 @@ class ItemTemplate {
this.baseHp = 0,
this.baseArmor = 0,
required this.slot,
this.effects = const [], // Default to no effects
this.effects = const [],
});
factory ItemTemplate.fromJson(Map<String, dynamic> json) {
return ItemTemplate(
name: json['name'],
description: json['description'],
baseAtk: json['baseAtk'] ?? 0,
baseHp: json['baseHp'] ?? 0,
baseArmor: json['baseArmor'] ?? 0,
slot: EquipmentSlot.values.firstWhere((e) => e.name == json['slot']),
effects: (json['effects'] as List<dynamic>?)
?.map((e) => ItemEffect.fromJson(e))
.toList() ??
[],
);
}
// Create an instance of Item based on this template, optionally scaling with stage
Item createItem({int stage = 1}) {
// Simple scaling logic: add stage-1 to relevant stats
@@ -49,156 +66,20 @@ class ItemTemplate {
}
class ItemTable {
static final List<ItemTemplate> weapons = [
const ItemTemplate(
name: "Rusty Dagger",
description: "Old and rusty, but better than nothing.",
baseAtk: 3,
slot: EquipmentSlot.weapon,
),
const ItemTemplate(
name: "Iron Sword",
description: "A standard soldier's sword.",
baseAtk: 8,
slot: EquipmentSlot.weapon,
),
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 List<ItemTemplate> weapons = [];
static List<ItemTemplate> armors = [];
static List<ItemTemplate> shields = [];
static List<ItemTemplate> accessories = [];
static const List<ItemTemplate> armors = [
ItemTemplate(
name: "Torn Tunic",
description: "Offers minimal protection.",
baseHp: 10,
slot: EquipmentSlot.armor,
),
ItemTemplate(
name: "Leather Vest",
description: "Light and flexible.",
baseHp: 30,
slot: EquipmentSlot.armor,
),
ItemTemplate(
name: "Chainmail",
description: "Reliable protection against cuts.",
baseHp: 60,
slot: EquipmentSlot.armor,
),
];
static Future<void> load() async {
final String jsonString = await rootBundle.loadString('assets/data/items.json');
final Map<String, dynamic> data = jsonDecode(jsonString);
static final List<ItemTemplate> shields = [
const ItemTemplate(
name: "Pot Lid",
description: "It was used for cooking.",
baseArmor: 1,
slot: EquipmentSlot.shield,
),
const ItemTemplate(
name: "Wooden Shield",
description: "Sturdy oak wood.",
baseArmor: 3,
slot: EquipmentSlot.shield,
),
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 = [
ItemTemplate(
name: "Old Ring",
description: "A tarnished ring.",
baseAtk: 1,
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.",
baseAtk: 3,
baseHp: 15,
slot: EquipmentSlot.accessory,
),
ItemTemplate(
name: "Hero's Badge",
description: "A badge of honor.",
baseAtk: 5,
baseHp: 25,
baseArmor: 1,
slot: EquipmentSlot.accessory,
),
];
weapons = (data['weapons'] as List).map((e) => ItemTemplate.fromJson(e)).toList();
armors = (data['armors'] as List).map((e) => ItemTemplate.fromJson(e)).toList();
shields = (data['shields'] as List).map((e) => ItemTemplate.fromJson(e)).toList();
accessories = (data['accessories'] as List).map((e) => ItemTemplate.fromJson(e)).toList();
}
static List<ItemTemplate> get allItems => [
...weapons,
+11 -2
View File
@@ -16,14 +16,23 @@ class ItemEffect {
this.value = 0,
});
factory ItemEffect.fromJson(Map<String, dynamic> json) {
return ItemEffect(
type: StatusEffectType.values.firstWhere((e) => e.name == json['type']),
probability: json['probability'] ?? 0,
duration: json['duration'] ?? 0,
value: json['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";
}
}
+8 -5
View File
@@ -1,9 +1,14 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'game/data/item_table.dart';
import 'game/data/enemy_table.dart';
import 'providers/battle_provider.dart';
import 'screens/main_menu_screen.dart';
void main() {
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await ItemTable.load();
await EnemyTable.load();
runApp(const MyApp());
}
@@ -13,9 +18,7 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => BattleProvider()),
],
providers: [ChangeNotifierProvider(create: (_) => BattleProvider())],
child: MaterialApp(
title: "Colosseum's Choice",
theme: ThemeData.dark(),
@@ -23,4 +26,4 @@ class MyApp extends StatelessWidget {
),
);
}
}
}
+169 -30
View File
@@ -6,17 +6,35 @@ import '../game/model/item.dart';
import '../game/model/status_effect.dart';
import '../game/model/stage.dart'; // Import StageModel
import '../game/data/item_table.dart';
import '../game/data/enemy_table.dart';
import '../utils/game_math.dart';
enum ActionType { attack, defend }
enum RiskLevel { safe, normal, risky }
enum EnemyActionType { attack, defend }
class EnemyIntent {
final EnemyActionType type;
final int value;
final RiskLevel risk;
final String description;
EnemyIntent({
required this.type,
required this.value,
required this.risk,
required this.description,
});
}
class BattleProvider with ChangeNotifier {
late Character player;
late Character enemy; // Kept for compatibility, active during Battle/Elite
late StageModel currentStage; // The current stage object
EnemyIntent? currentEnemyIntent;
List<String> battleLogs = [];
bool isPlayerTurn = true;
@@ -114,20 +132,46 @@ class BattleProvider with ChangeNotifier {
if (type == StageType.battle || type == StageType.elite) {
bool isElite = type == StageType.elite;
int hpMultiplier = isElite ? 1 : 1;
int atkMultiplier = isElite ? 4 : 2;
// Select random enemy template
final random = Random();
EnemyTemplate template;
if (isElite) {
if (EnemyTable.eliteEnemies.isNotEmpty) {
template = EnemyTable
.eliteEnemies[random.nextInt(EnemyTable.eliteEnemies.length)];
} else {
// Fallback if no elite enemies loaded
template = const EnemyTemplate(
name: "Elite Guardian",
baseHp: 50,
baseAtk: 10,
baseDefense: 2,
);
}
} else {
if (EnemyTable.normalEnemies.isNotEmpty) {
template = EnemyTable
.normalEnemies[random.nextInt(EnemyTable.normalEnemies.length)];
} else {
// Fallback
template = const EnemyTemplate(
name: "Enemy",
baseHp: 20,
baseAtk: 5,
baseDefense: 0,
);
}
}
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);
newEnemy = template.createCharacter(stage: stage);
// Assign to the main 'enemy' field for UI compatibility
enemy = newEnemy;
isPlayerTurn = true;
showRewardPopup = false;
_generateEnemyIntent(); // Generate first intent
_addLog("Stage $stage ($type) started! A wild ${enemy.name} appeared.");
} else if (type == StageType.shop) {
// Generate random items for shop
@@ -263,35 +307,65 @@ class BattleProvider with ChangeNotifier {
return;
}
if (canAct) {
int incomingDamage = enemy.totalAtk;
int damageToHp = 0;
if (canAct && currentEnemyIntent != null) {
final intent = currentEnemyIntent!;
// Enemy attack logic
// (Simple logic: Enemy always attacks for now)
// Note: Enemy doesn't have equipment yet, so no effects applied by enemy.
// Check Success Rate based on Risk
final random = Random();
bool success = false;
switch (intent.risk) {
case RiskLevel.safe:
success = random.nextDouble() < 1.0;
break;
case RiskLevel.normal:
success = random.nextDouble() < 0.8;
break;
case RiskLevel.risky:
success = random.nextDouble() < 0.4;
break;
}
// 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;
if (success) {
if (intent.type == EnemyActionType.attack) {
int incomingDamage = intent.value;
int damageToHp = 0;
// 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;
}
if (damageToHp > 0) {
_applyDamage(player, damageToHp);
_addLog("Enemy dealt $damageToHp damage to Player HP.");
}
} else if (intent.type == EnemyActionType.defend) {
int armorGained = intent.value;
enemy.armor += armorGained;
_addLog("Enemy gained $armorGained armor.");
}
} else {
damageToHp = incomingDamage;
_addLog("Enemy's ${intent.risk.name} action missed!");
}
if (damageToHp > 0) {
_applyDamage(player, damageToHp);
_addLog("Enemy dealt $damageToHp damage to Player HP.");
}
} else {
} else if (!canAct) {
_addLog("Enemy is stunned and cannot act!");
} else {
_addLog("Enemy did nothing.");
}
// Generate next intent
if (!player.isDead) {
_generateEnemyIntent();
}
// Player Turn Start Logic
@@ -452,4 +526,69 @@ class BattleProvider with ChangeNotifier {
stage++;
_prepareNextStage();
}
void _generateEnemyIntent() {
if (enemy.isDead) {
currentEnemyIntent = null;
return;
}
final random = Random();
// Decide Action Type
// If baseDefense is 0, CANNOT defend.
bool canDefend = enemy.baseDefense > 0;
bool isAttack = true;
if (canDefend) {
// 70% Attack, 30% Defend
isAttack = random.nextDouble() < 0.7;
} else {
isAttack = true;
}
// Decide Risk Level
RiskLevel risk = RiskLevel.values[random.nextInt(RiskLevel.values.length)];
double efficiency = 1.0;
switch (risk) {
case RiskLevel.safe:
efficiency = 0.5;
break;
case RiskLevel.normal:
efficiency = 1.0;
break;
case RiskLevel.risky:
efficiency = 2.0;
break;
}
if (isAttack) {
// Attack Intent
// Variance: +/- 20%
double variance = 0.8 + random.nextDouble() * 0.4;
int damage = (enemy.totalAtk * efficiency * variance).toInt();
if (damage < 1) damage = 1;
currentEnemyIntent = EnemyIntent(
type: EnemyActionType.attack,
value: damage,
risk: risk,
description: "Attacks for $damage (${risk.name})",
);
} else {
// Defend Intent
int baseDef = enemy.totalDefense;
// Variance
double variance = 0.8 + random.nextDouble() * 0.4;
int armor = (baseDef * 2 * efficiency * variance).toInt();
currentEnemyIntent = EnemyIntent(
type: EnemyActionType.defend,
value: armor,
risk: risk,
description: "Defends for $armor (${risk.name})",
);
}
notifyListeners();
}
}
+52
View File
@@ -373,6 +373,58 @@ class _BattleScreenState extends State<BattleScreen> {
}).toList(),
),
),
if (isEnemy)
Consumer<BattleProvider>(
builder: (context, provider, child) {
if (provider.currentEnemyIntent != null && !character.isDead) {
final intent = provider.currentEnemyIntent!;
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.redAccent),
),
child: Column(
children: [
Text(
"INTENT",
style: TextStyle(
color: Colors.redAccent,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
intent.type == EnemyActionType.attack
? Icons.flash_on
: Icons.shield,
color: Colors.yellow,
size: 16,
),
const SizedBox(width: 4),
Text(
intent.description,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
),
],
),
],
),
),
);
}
return const SizedBox.shrink();
},
),
if (!isEnemy) ...[
Text("Armor: ${character.armor}"),
Text("ATK: ${character.totalAtk}"),