update
This commit is contained in:
parent
0a7c50e6c9
commit
37a634643e
|
|
@ -5,35 +5,40 @@
|
||||||
"baseHp": 20,
|
"baseHp": 20,
|
||||||
"baseAtk": 5,
|
"baseAtk": 5,
|
||||||
"baseDefense": 5,
|
"baseDefense": 5,
|
||||||
"image": "assets/images/enemies/goblin.png"
|
"image": "assets/images/enemies/goblin.png",
|
||||||
|
"equipment": ["rusty_dagger"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Slime",
|
"name": "Slime",
|
||||||
"baseHp": 30,
|
"baseHp": 30,
|
||||||
"baseAtk": 3,
|
"baseAtk": 3,
|
||||||
"baseDefense": 5,
|
"baseDefense": 5,
|
||||||
"image": "assets/images/enemies/slime.png"
|
"image": "assets/images/enemies/slime.png",
|
||||||
|
"equipment": ["rusty_dagger"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Wolf",
|
"name": "Wolf",
|
||||||
"baseHp": 25,
|
"baseHp": 25,
|
||||||
"baseAtk": 7,
|
"baseAtk": 7,
|
||||||
"baseDefense": 5,
|
"baseDefense": 5,
|
||||||
"image": "assets/images/enemies/wolf.png"
|
"image": "assets/images/enemies/wolf.png",
|
||||||
|
"equipment": ["rusty_dagger"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Bandit",
|
"name": "Bandit",
|
||||||
"baseHp": 35,
|
"baseHp": 35,
|
||||||
"baseAtk": 6,
|
"baseAtk": 6,
|
||||||
"baseDefense": 5,
|
"baseDefense": 5,
|
||||||
"image": "assets/images/enemies/bandit.png"
|
"image": "assets/images/enemies/bandit.png",
|
||||||
|
"equipment": ["rusty_dagger"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Skeleton",
|
"name": "Skeleton",
|
||||||
"baseHp": 15,
|
"baseHp": 15,
|
||||||
"baseAtk": 8,
|
"baseAtk": 8,
|
||||||
"baseDefense": 5,
|
"baseDefense": 5,
|
||||||
"image": "assets/images/enemies/skeleton.png"
|
"image": "assets/images/enemies/skeleton.png",
|
||||||
|
"equipment": ["rusty_dagger"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"elite": [
|
"elite": [
|
||||||
|
|
@ -42,21 +47,24 @@
|
||||||
"baseHp": 60,
|
"baseHp": 60,
|
||||||
"baseAtk": 12,
|
"baseAtk": 12,
|
||||||
"baseDefense": 3,
|
"baseDefense": 3,
|
||||||
"image": "assets/images/enemies/orc_warrior.png"
|
"image": "assets/images/enemies/orc_warrior.png",
|
||||||
|
"equipment": ["battle_axe", "leather_vest"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Giant Spider",
|
"name": "Giant Spider",
|
||||||
"baseHp": 50,
|
"baseHp": 50,
|
||||||
"baseAtk": 15,
|
"baseAtk": 15,
|
||||||
"baseDefense": 2,
|
"baseDefense": 2,
|
||||||
"image": "assets/images/enemies/giant_spider.png"
|
"image": "assets/images/enemies/giant_spider.png",
|
||||||
|
"equipment": ["jagged_dagger"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Dark Knight",
|
"name": "Dark Knight",
|
||||||
"baseHp": 80,
|
"baseHp": 80,
|
||||||
"baseAtk": 10,
|
"baseAtk": 10,
|
||||||
"baseDefense": 5,
|
"baseDefense": 5,
|
||||||
"image": "assets/images/enemies/dark_knight.png"
|
"image": "assets/images/enemies/dark_knight.png",
|
||||||
|
"equipment": ["stunning_hammer", "kite_shield"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"weapons": [
|
"weapons": [
|
||||||
{
|
{
|
||||||
|
"id": "rusty_dagger",
|
||||||
"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,
|
||||||
|
|
@ -9,6 +10,7 @@
|
||||||
"image": "assets/images/items/rusty_dagger.png"
|
"image": "assets/images/items/rusty_dagger.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "iron_sword",
|
||||||
"name": "Iron Sword",
|
"name": "Iron Sword",
|
||||||
"description": "A standard soldier's sword.",
|
"description": "A standard soldier's sword.",
|
||||||
"baseAtk": 8,
|
"baseAtk": 8,
|
||||||
|
|
@ -17,6 +19,7 @@
|
||||||
"image": "assets/images/items/iron_sword.png"
|
"image": "assets/images/items/iron_sword.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "battle_axe",
|
||||||
"name": "Battle Axe",
|
"name": "Battle Axe",
|
||||||
"description": "Heavy but powerful.",
|
"description": "Heavy but powerful.",
|
||||||
"baseAtk": 12,
|
"baseAtk": 12,
|
||||||
|
|
@ -25,6 +28,7 @@
|
||||||
"image": "assets/images/items/battle_axe.png"
|
"image": "assets/images/items/battle_axe.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "stunning_hammer",
|
||||||
"name": "Stunning Hammer",
|
"name": "Stunning Hammer",
|
||||||
"description": "A heavy hammer that can stun foes.",
|
"description": "A heavy hammer that can stun foes.",
|
||||||
"baseAtk": 10,
|
"baseAtk": 10,
|
||||||
|
|
@ -40,6 +44,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "jagged_dagger",
|
||||||
"name": "Jagged Dagger",
|
"name": "Jagged Dagger",
|
||||||
"description": "A cruel dagger that causes bleeding.",
|
"description": "A cruel dagger that causes bleeding.",
|
||||||
"baseAtk": 7,
|
"baseAtk": 7,
|
||||||
|
|
@ -56,6 +61,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "sunderer_axe",
|
||||||
"name": "Sunderer Axe",
|
"name": "Sunderer Axe",
|
||||||
"description": "An axe that exposes enemy weaknesses.",
|
"description": "An axe that exposes enemy weaknesses.",
|
||||||
"baseAtk": 11,
|
"baseAtk": 11,
|
||||||
|
|
@ -73,6 +79,7 @@
|
||||||
],
|
],
|
||||||
"armors": [
|
"armors": [
|
||||||
{
|
{
|
||||||
|
"id": "torn_tunic",
|
||||||
"name": "Torn Tunic",
|
"name": "Torn Tunic",
|
||||||
"description": "Offers minimal protection.",
|
"description": "Offers minimal protection.",
|
||||||
"baseHp": 10,
|
"baseHp": 10,
|
||||||
|
|
@ -81,6 +88,7 @@
|
||||||
"image": "assets/images/items/torn_tunic.png"
|
"image": "assets/images/items/torn_tunic.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "leather_vest",
|
||||||
"name": "Leather Vest",
|
"name": "Leather Vest",
|
||||||
"description": "Light and flexible.",
|
"description": "Light and flexible.",
|
||||||
"baseHp": 30,
|
"baseHp": 30,
|
||||||
|
|
@ -89,6 +97,7 @@
|
||||||
"image": "assets/images/items/leather_vest.png"
|
"image": "assets/images/items/leather_vest.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "chainmail",
|
||||||
"name": "Chainmail",
|
"name": "Chainmail",
|
||||||
"description": "Reliable protection against cuts.",
|
"description": "Reliable protection against cuts.",
|
||||||
"baseHp": 60,
|
"baseHp": 60,
|
||||||
|
|
@ -99,6 +108,7 @@
|
||||||
],
|
],
|
||||||
"shields": [
|
"shields": [
|
||||||
{
|
{
|
||||||
|
"id": "pot_lid",
|
||||||
"name": "Pot Lid",
|
"name": "Pot Lid",
|
||||||
"description": "It was used for cooking.",
|
"description": "It was used for cooking.",
|
||||||
"baseArmor": 1,
|
"baseArmor": 1,
|
||||||
|
|
@ -107,6 +117,7 @@
|
||||||
"image": "assets/images/items/pot_lid.png"
|
"image": "assets/images/items/pot_lid.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "wooden_shield",
|
||||||
"name": "Wooden Shield",
|
"name": "Wooden Shield",
|
||||||
"description": "Sturdy oak wood.",
|
"description": "Sturdy oak wood.",
|
||||||
"baseArmor": 3,
|
"baseArmor": 3,
|
||||||
|
|
@ -115,6 +126,7 @@
|
||||||
"image": "assets/images/items/wooden_shield.png"
|
"image": "assets/images/items/wooden_shield.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "kite_shield",
|
||||||
"name": "Kite Shield",
|
"name": "Kite Shield",
|
||||||
"description": "Used by knights.",
|
"description": "Used by knights.",
|
||||||
"baseArmor": 6,
|
"baseArmor": 6,
|
||||||
|
|
@ -123,6 +135,7 @@
|
||||||
"image": "assets/images/items/kite_shield.png"
|
"image": "assets/images/items/kite_shield.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "cursed_shield",
|
||||||
"name": "Cursed Shield",
|
"name": "Cursed Shield",
|
||||||
"description": "A shield that prevents the wielder from defending themselves.",
|
"description": "A shield that prevents the wielder from defending themselves.",
|
||||||
"baseArmor": 5,
|
"baseArmor": 5,
|
||||||
|
|
@ -140,6 +153,7 @@
|
||||||
],
|
],
|
||||||
"accessories": [
|
"accessories": [
|
||||||
{
|
{
|
||||||
|
"id": "old_ring",
|
||||||
"name": "Old Ring",
|
"name": "Old Ring",
|
||||||
"description": "A tarnished ring.",
|
"description": "A tarnished ring.",
|
||||||
"baseAtk": 1,
|
"baseAtk": 1,
|
||||||
|
|
@ -149,6 +163,7 @@
|
||||||
"image": "assets/images/items/old_ring.png"
|
"image": "assets/images/items/old_ring.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "copper_ring",
|
||||||
"name": "Copper Ring",
|
"name": "Copper Ring",
|
||||||
"description": "A simple ring",
|
"description": "A simple ring",
|
||||||
"baseAtk": 1,
|
"baseAtk": 1,
|
||||||
|
|
@ -158,6 +173,7 @@
|
||||||
"image": "assets/images/items/copper_ring.png"
|
"image": "assets/images/items/copper_ring.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "ruby_amulet",
|
||||||
"name": "Ruby Amulet",
|
"name": "Ruby Amulet",
|
||||||
"description": "Glows with a faint red light.",
|
"description": "Glows with a faint red light.",
|
||||||
"baseAtk": 3,
|
"baseAtk": 3,
|
||||||
|
|
@ -167,6 +183,7 @@
|
||||||
"image": "assets/images/items/ruby_amulet.png"
|
"image": "assets/images/items/ruby_amulet.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "heros_badge",
|
||||||
"name": "Hero's Badge",
|
"name": "Hero's Badge",
|
||||||
"description": "A badge of honor.",
|
"description": "A badge of honor.",
|
||||||
"baseAtk": 5,
|
"baseAtk": 5,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "warrior",
|
||||||
|
"name": "Warrior",
|
||||||
|
"description": "A balanced fighter with a sword and shield. Great for beginners.",
|
||||||
|
"baseHp": 50,
|
||||||
|
"baseAtk": 5,
|
||||||
|
"baseDefense": 5,
|
||||||
|
"image": "assets/images/players/warrior.png"
|
||||||
|
}
|
||||||
|
]
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
|
|
@ -2,12 +2,15 @@ import 'dart:convert';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import '../model/entity.dart';
|
import '../model/entity.dart';
|
||||||
|
|
||||||
|
import 'item_table.dart';
|
||||||
|
|
||||||
class EnemyTemplate {
|
class EnemyTemplate {
|
||||||
final String name;
|
final String name;
|
||||||
final int baseHp;
|
final int baseHp;
|
||||||
final int baseAtk;
|
final int baseAtk;
|
||||||
final int baseDefense;
|
final int baseDefense;
|
||||||
final String? image;
|
final String? image;
|
||||||
|
final List<String> equipmentIds;
|
||||||
|
|
||||||
const EnemyTemplate({
|
const EnemyTemplate({
|
||||||
required this.name,
|
required this.name,
|
||||||
|
|
@ -15,6 +18,7 @@ class EnemyTemplate {
|
||||||
required this.baseAtk,
|
required this.baseAtk,
|
||||||
required this.baseDefense,
|
required this.baseDefense,
|
||||||
this.image,
|
this.image,
|
||||||
|
this.equipmentIds = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
factory EnemyTemplate.fromJson(Map<String, dynamic> json) {
|
factory EnemyTemplate.fromJson(Map<String, dynamic> json) {
|
||||||
|
|
@ -24,6 +28,7 @@ class EnemyTemplate {
|
||||||
baseAtk: json['baseAtk'] ?? 1,
|
baseAtk: json['baseAtk'] ?? 1,
|
||||||
baseDefense: json['baseDefense'] ?? 0,
|
baseDefense: json['baseDefense'] ?? 0,
|
||||||
image: json['image'],
|
image: json['image'],
|
||||||
|
equipmentIds: (json['equipment'] as List<dynamic>?)?.cast<String>() ?? [],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,7 +38,7 @@ class EnemyTemplate {
|
||||||
int scaledAtk = baseAtk + (stage - 1);
|
int scaledAtk = baseAtk + (stage - 1);
|
||||||
int scaledDefense = baseDefense + (stage ~/ 5); // +1 defense every 5 stages
|
int scaledDefense = baseDefense + (stage ~/ 5); // +1 defense every 5 stages
|
||||||
|
|
||||||
return Character(
|
final character = Character(
|
||||||
name: name,
|
name: name,
|
||||||
maxHp: scaledHp,
|
maxHp: scaledHp,
|
||||||
atk: scaledAtk,
|
atk: scaledAtk,
|
||||||
|
|
@ -41,6 +46,20 @@ class EnemyTemplate {
|
||||||
armor: 0,
|
armor: 0,
|
||||||
image: image,
|
image: image,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Equip items
|
||||||
|
for (final itemId in equipmentIds) {
|
||||||
|
final itemTemplate = ItemTable.get(itemId);
|
||||||
|
if (itemTemplate != null) {
|
||||||
|
// Create item scaled to stage (optional, currently stage 1)
|
||||||
|
// Enemies might get stronger items at higher stages
|
||||||
|
final item = itemTemplate.createItem(stage: stage);
|
||||||
|
character.addToInventory(item);
|
||||||
|
character.equip(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return character;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import '../model/item.dart';
|
||||||
import '../enums.dart';
|
import '../enums.dart';
|
||||||
|
|
||||||
class ItemTemplate {
|
class ItemTemplate {
|
||||||
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
final String description;
|
final String description;
|
||||||
final int baseAtk;
|
final int baseAtk;
|
||||||
|
|
@ -15,6 +16,7 @@ class ItemTemplate {
|
||||||
final String? image;
|
final String? image;
|
||||||
|
|
||||||
const ItemTemplate({
|
const ItemTemplate({
|
||||||
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.description,
|
required this.description,
|
||||||
this.baseAtk = 0,
|
this.baseAtk = 0,
|
||||||
|
|
@ -28,6 +30,9 @@ class ItemTemplate {
|
||||||
|
|
||||||
factory ItemTemplate.fromJson(Map<String, dynamic> json) {
|
factory ItemTemplate.fromJson(Map<String, dynamic> json) {
|
||||||
return ItemTemplate(
|
return ItemTemplate(
|
||||||
|
id:
|
||||||
|
json['id'] ??
|
||||||
|
json['name'], // Fallback to name if id is missing (for backward compatibility during dev)
|
||||||
name: json['name'],
|
name: json['name'],
|
||||||
description: json['description'],
|
description: json['description'],
|
||||||
baseAtk: json['baseAtk'] ?? 0,
|
baseAtk: json['baseAtk'] ?? 0,
|
||||||
|
|
@ -60,6 +65,7 @@ class ItemTemplate {
|
||||||
}
|
}
|
||||||
|
|
||||||
return Item(
|
return Item(
|
||||||
|
id: id,
|
||||||
name: "$name${stage > 1 ? ' +${stage - 1}' : ''}", // Append +1, +2 etc.
|
name: "$name${stage > 1 ? ' +${stage - 1}' : ''}", // Append +1, +2 etc.
|
||||||
description: description,
|
description: description,
|
||||||
atkBonus: scaledAtk,
|
atkBonus: scaledAtk,
|
||||||
|
|
@ -105,4 +111,12 @@ class ItemTable {
|
||||||
...shields,
|
...shields,
|
||||||
...accessories,
|
...accessories,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
static ItemTemplate? get(String id) {
|
||||||
|
try {
|
||||||
|
return allItems.firstWhere((item) => item.id == id);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import '../model/entity.dart';
|
||||||
|
|
||||||
|
class PlayerTemplate {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String description;
|
||||||
|
final int baseHp;
|
||||||
|
final int baseAtk;
|
||||||
|
final int baseDefense;
|
||||||
|
final String? image;
|
||||||
|
|
||||||
|
const PlayerTemplate({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.description,
|
||||||
|
required this.baseHp,
|
||||||
|
required this.baseAtk,
|
||||||
|
required this.baseDefense,
|
||||||
|
this.image,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory PlayerTemplate.fromJson(Map<String, dynamic> json) {
|
||||||
|
return PlayerTemplate(
|
||||||
|
id: json['id'],
|
||||||
|
name: json['name'],
|
||||||
|
description: json['description'],
|
||||||
|
baseHp: json['baseHp'],
|
||||||
|
baseAtk: json['baseAtk'],
|
||||||
|
baseDefense: json['baseDefense'],
|
||||||
|
image: json['image'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Character createCharacter() {
|
||||||
|
return Character(
|
||||||
|
name: name,
|
||||||
|
maxHp: baseHp,
|
||||||
|
atk: baseAtk,
|
||||||
|
baseDefense: baseDefense,
|
||||||
|
armor: 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlayerTable {
|
||||||
|
static List<PlayerTemplate> players = [];
|
||||||
|
|
||||||
|
static Future<void> load() async {
|
||||||
|
final String jsonString = await rootBundle.loadString(
|
||||||
|
'assets/data/players.json',
|
||||||
|
);
|
||||||
|
final List<dynamic> data = jsonDecode(jsonString);
|
||||||
|
|
||||||
|
players = data.map((e) => PlayerTemplate.fromJson(e)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static PlayerTemplate? get(String id) {
|
||||||
|
try {
|
||||||
|
return players.firstWhere((player) => player.id == id);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -36,6 +36,7 @@ class ItemEffect {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Item {
|
class Item {
|
||||||
|
final String id; // Unique identifier
|
||||||
final String name;
|
final String name;
|
||||||
final String description;
|
final String description;
|
||||||
final int atkBonus;
|
final int atkBonus;
|
||||||
|
|
@ -47,6 +48,7 @@ class Item {
|
||||||
final String? image; // New: Image path
|
final String? image; // New: Image path
|
||||||
|
|
||||||
Item({
|
Item({
|
||||||
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.description,
|
required this.description,
|
||||||
required this.atkBonus,
|
required this.atkBonus,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'game/data/item_table.dart';
|
import 'game/data/item_table.dart';
|
||||||
import 'game/data/enemy_table.dart';
|
import 'game/data/enemy_table.dart';
|
||||||
|
import 'game/data/player_table.dart';
|
||||||
import 'providers/battle_provider.dart';
|
import 'providers/battle_provider.dart';
|
||||||
import 'screens/main_menu_screen.dart';
|
import 'screens/main_menu_screen.dart';
|
||||||
|
|
||||||
|
|
@ -9,6 +10,7 @@ void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await ItemTable.load();
|
await ItemTable.load();
|
||||||
await EnemyTable.load();
|
await EnemyTable.load();
|
||||||
|
await PlayerTable.load();
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@ import '../game/model/item.dart';
|
||||||
import '../game/model/status_effect.dart';
|
import '../game/model/status_effect.dart';
|
||||||
import '../game/model/stage.dart';
|
import '../game/model/stage.dart';
|
||||||
import '../game/data/item_table.dart';
|
import '../game/data/item_table.dart';
|
||||||
|
|
||||||
import '../game/data/enemy_table.dart';
|
import '../game/data/enemy_table.dart';
|
||||||
|
import '../game/data/player_table.dart';
|
||||||
import '../utils/game_math.dart';
|
import '../utils/game_math.dart';
|
||||||
import '../game/enums.dart';
|
import '../game/enums.dart';
|
||||||
import '../game/model/damage_event.dart'; // DamageEvent import
|
import '../game/model/damage_event.dart'; // DamageEvent import
|
||||||
|
|
@ -70,16 +72,24 @@ class BattleProvider with ChangeNotifier {
|
||||||
void initializeBattle() {
|
void initializeBattle() {
|
||||||
stage = 1;
|
stage = 1;
|
||||||
turnCount = 1;
|
turnCount = 1;
|
||||||
player = Character(
|
// Load player from PlayerTable
|
||||||
name: "Player",
|
final playerTemplate = PlayerTable.get("warrior");
|
||||||
maxHp: 80,
|
if (playerTemplate != null) {
|
||||||
armor: 0,
|
player = playerTemplate.createCharacter();
|
||||||
atk: 5,
|
} else {
|
||||||
baseDefense: 5,
|
// Fallback if data is missing
|
||||||
);
|
player = Character(
|
||||||
|
name: "Player",
|
||||||
|
maxHp: 50,
|
||||||
|
armor: 0,
|
||||||
|
atk: 5,
|
||||||
|
baseDefense: 5,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Provide starter equipment
|
// Provide starter equipment
|
||||||
final starterSword = Item(
|
final starterSword = Item(
|
||||||
|
id: "starter_sword",
|
||||||
name: "Wooden Sword",
|
name: "Wooden Sword",
|
||||||
description: "A basic sword",
|
description: "A basic sword",
|
||||||
atkBonus: 5,
|
atkBonus: 5,
|
||||||
|
|
@ -87,6 +97,7 @@ class BattleProvider with ChangeNotifier {
|
||||||
slot: EquipmentSlot.weapon,
|
slot: EquipmentSlot.weapon,
|
||||||
);
|
);
|
||||||
final starterArmor = Item(
|
final starterArmor = Item(
|
||||||
|
id: "starter_armor",
|
||||||
name: "Leather Armor",
|
name: "Leather Armor",
|
||||||
description: "Basic protection",
|
description: "Basic protection",
|
||||||
atkBonus: 0,
|
atkBonus: 0,
|
||||||
|
|
@ -94,6 +105,7 @@ class BattleProvider with ChangeNotifier {
|
||||||
slot: EquipmentSlot.armor,
|
slot: EquipmentSlot.armor,
|
||||||
);
|
);
|
||||||
final starterShield = Item(
|
final starterShield = Item(
|
||||||
|
id: "starter_shield",
|
||||||
name: "Wooden Shield",
|
name: "Wooden Shield",
|
||||||
description: "A small shield",
|
description: "A small shield",
|
||||||
atkBonus: 0,
|
atkBonus: 0,
|
||||||
|
|
@ -102,6 +114,7 @@ class BattleProvider with ChangeNotifier {
|
||||||
slot: EquipmentSlot.shield,
|
slot: EquipmentSlot.shield,
|
||||||
);
|
);
|
||||||
final starterRing = Item(
|
final starterRing = Item(
|
||||||
|
id: "starter_ring",
|
||||||
name: "Copper Ring",
|
name: "Copper Ring",
|
||||||
description: "A simple ring",
|
description: "A simple ring",
|
||||||
atkBonus: 1,
|
atkBonus: 1,
|
||||||
|
|
@ -436,6 +449,9 @@ class BattleProvider with ChangeNotifier {
|
||||||
_applyDamage(player, damageToHp, targetType: DamageTarget.player);
|
_applyDamage(player, damageToHp, targetType: DamageTarget.player);
|
||||||
_addLog("Enemy dealt $damageToHp damage to Player HP.");
|
_addLog("Enemy dealt $damageToHp damage to Player HP.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try applying status effects from enemy equipment
|
||||||
|
_tryApplyStatusEffects(enemy, player);
|
||||||
} else {
|
} else {
|
||||||
_addLog("Enemy's ${intent.risk.name} attack missed!");
|
_addLog("Enemy's ${intent.risk.name} attack missed!");
|
||||||
_effectEventController.sink.add(
|
_effectEventController.sink.add(
|
||||||
|
|
|
||||||
|
|
@ -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 '../game/model/entity.dart';
|
|
||||||
import '../game/enums.dart';
|
import '../game/enums.dart';
|
||||||
import '../game/model/item.dart';
|
import '../game/model/item.dart';
|
||||||
import '../game/model/damage_event.dart';
|
import '../game/model/damage_event.dart';
|
||||||
|
|
@ -9,6 +9,10 @@ import '../game/model/effect_event.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import '../widgets/responsive_container.dart';
|
import '../widgets/responsive_container.dart';
|
||||||
import '../utils/item_utils.dart';
|
import '../utils/item_utils.dart';
|
||||||
|
import '../widgets/battle/character_status_card.dart';
|
||||||
|
import '../widgets/battle/battle_log_overlay.dart';
|
||||||
|
import '../widgets/battle/floating_battle_texts.dart';
|
||||||
|
import '../widgets/battle/stage_ui.dart';
|
||||||
|
|
||||||
class BattleScreen extends StatefulWidget {
|
class BattleScreen extends StatefulWidget {
|
||||||
const BattleScreen({super.key});
|
const BattleScreen({super.key});
|
||||||
|
|
@ -18,25 +22,19 @@ class BattleScreen extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BattleScreenState extends State<BattleScreen> {
|
class _BattleScreenState extends State<BattleScreen> {
|
||||||
final ScrollController _scrollController = ScrollController();
|
final List<DamageTextData> _floatingDamageTexts = [];
|
||||||
final List<_DamageTextData> _floatingDamageTexts = [];
|
final List<FloatingEffectData> _floatingEffects = [];
|
||||||
final List<_FloatingEffectData> _floatingEffects = [];
|
final List<FeedbackTextData> _floatingFeedbackTexts = [];
|
||||||
final List<_FeedbackTextData> _floatingFeedbackTexts = [];
|
|
||||||
StreamSubscription<DamageEvent>? _damageSubscription;
|
StreamSubscription<DamageEvent>? _damageSubscription;
|
||||||
StreamSubscription<EffectEvent>? _effectSubscription;
|
StreamSubscription<EffectEvent>? _effectSubscription;
|
||||||
final GlobalKey _playerKey = GlobalKey();
|
final GlobalKey _playerKey = GlobalKey();
|
||||||
final GlobalKey _enemyKey = GlobalKey();
|
final GlobalKey _enemyKey = GlobalKey();
|
||||||
final GlobalKey _stackKey = GlobalKey();
|
final GlobalKey _stackKey = GlobalKey();
|
||||||
|
bool _showLogs = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
if (_scrollController.hasClients) {
|
|
||||||
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
final battleProvider = context.read<BattleProvider>();
|
final battleProvider = context.read<BattleProvider>();
|
||||||
_damageSubscription = battleProvider.damageStream.listen(
|
_damageSubscription = battleProvider.damageStream.listen(
|
||||||
_addFloatingDamageText,
|
_addFloatingDamageText,
|
||||||
|
|
@ -48,7 +46,6 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_scrollController.dispose();
|
|
||||||
_damageSubscription?.cancel();
|
_damageSubscription?.cancel();
|
||||||
_effectSubscription?.cancel();
|
_effectSubscription?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
|
@ -82,12 +79,12 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_floatingDamageTexts.add(
|
_floatingDamageTexts.add(
|
||||||
_DamageTextData(
|
DamageTextData(
|
||||||
id: id,
|
id: id,
|
||||||
widget: Positioned(
|
widget: Positioned(
|
||||||
left: position.dx,
|
left: position.dx,
|
||||||
top: position.dy,
|
top: position.dy,
|
||||||
child: _FloatingDamageText(
|
child: FloatingDamageText(
|
||||||
key: ValueKey(id),
|
key: ValueKey(id),
|
||||||
damage: event.damage.toString(),
|
damage: event.damage.toString(),
|
||||||
color: event.color,
|
color: event.color,
|
||||||
|
|
@ -153,12 +150,12 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||||
final String id = UniqueKey().toString();
|
final String id = UniqueKey().toString();
|
||||||
setState(() {
|
setState(() {
|
||||||
_floatingFeedbackTexts.add(
|
_floatingFeedbackTexts.add(
|
||||||
_FeedbackTextData(
|
FeedbackTextData(
|
||||||
id: id,
|
id: id,
|
||||||
widget: Positioned(
|
widget: Positioned(
|
||||||
left: position.dx,
|
left: position.dx,
|
||||||
top: position.dy,
|
top: position.dy,
|
||||||
child: _FloatingFeedbackText(
|
child: FloatingFeedbackText(
|
||||||
key: ValueKey(id),
|
key: ValueKey(id),
|
||||||
feedback: feedbackText,
|
feedback: feedbackText,
|
||||||
color: feedbackColor,
|
color: feedbackColor,
|
||||||
|
|
@ -213,12 +210,12 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_floatingEffects.add(
|
_floatingEffects.add(
|
||||||
_FloatingEffectData(
|
FloatingEffectData(
|
||||||
id: id,
|
id: id,
|
||||||
widget: Positioned(
|
widget: Positioned(
|
||||||
left: position.dx,
|
left: position.dx,
|
||||||
top: position.dy,
|
top: position.dy,
|
||||||
child: _FloatingEffect(
|
child: FloatingEffect(
|
||||||
key: ValueKey(id),
|
key: ValueKey(id),
|
||||||
icon: icon,
|
icon: icon,
|
||||||
color: color,
|
color: color,
|
||||||
|
|
@ -295,13 +292,6 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.read<BattleProvider>().playerAction(actionType, risk);
|
context.read<BattleProvider>().playerAction(actionType, risk);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
_scrollController.animateTo(
|
|
||||||
_scrollController.position.maxScrollExtent,
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
curve: Curves.easeOut,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|
@ -329,15 +319,18 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||||
child: Consumer<BattleProvider>(
|
child: Consumer<BattleProvider>(
|
||||||
builder: (context, battleProvider, child) {
|
builder: (context, battleProvider, child) {
|
||||||
if (battleProvider.currentStage.type == StageType.shop) {
|
if (battleProvider.currentStage.type == StageType.shop) {
|
||||||
return _buildShopUI(context, battleProvider);
|
return ShopUI(battleProvider: battleProvider);
|
||||||
} else if (battleProvider.currentStage.type == StageType.rest) {
|
} else if (battleProvider.currentStage.type == StageType.rest) {
|
||||||
return _buildRestUI(context, battleProvider);
|
return RestUI(battleProvider: battleProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
key: _stackKey,
|
key: _stackKey,
|
||||||
children: [
|
children: [
|
||||||
|
// 1. Background (Black)
|
||||||
Container(color: Colors.black87),
|
Container(color: Colors.black87),
|
||||||
|
|
||||||
|
// 2. Battle Content (Top Bar + Characters)
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
// Top Bar
|
// Top Bar
|
||||||
|
|
@ -346,125 +339,135 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Flexible(
|
||||||
"Stage ${battleProvider.stage}",
|
child: FittedBox(
|
||||||
style: const TextStyle(
|
fit: BoxFit.scaleDown,
|
||||||
color: Colors.white,
|
child: Text(
|
||||||
fontSize: 18,
|
"Stage ${battleProvider.stage}",
|
||||||
fontWeight: FontWeight.bold,
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Flexible(
|
||||||
"Turn ${battleProvider.turnCount}",
|
child: FittedBox(
|
||||||
style: const TextStyle(
|
fit: BoxFit.scaleDown,
|
||||||
color: Colors.white,
|
child: Text(
|
||||||
fontSize: 18,
|
"Turn ${battleProvider.turnCount}",
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Battle Area
|
// Battle Area (Characters) - Expanded to fill available space
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
// padding: const EdgeInsets.symmetric(horizontal: 40.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
padding: const EdgeInsets.all(70.0),
|
child: Stack(
|
||||||
child: Column(
|
|
||||||
children: [
|
children: [
|
||||||
// 적 영역 (우측 상단)
|
// Enemy (Top Right)
|
||||||
Expanded(
|
Positioned(
|
||||||
child: Align(
|
top: 0,
|
||||||
alignment: Alignment.topRight,
|
right: 0,
|
||||||
child: _buildCharacterStatus(
|
child: CharacterStatusCard(
|
||||||
battleProvider.enemy,
|
character: battleProvider.enemy,
|
||||||
isPlayer: false,
|
isPlayer: false,
|
||||||
isTurn: !battleProvider.isPlayerTurn,
|
isTurn: !battleProvider.isPlayerTurn,
|
||||||
key: _enemyKey,
|
key: _enemyKey,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// 플레이어 영역 (좌측 하단)
|
// Player (Bottom Left)
|
||||||
Expanded(
|
Positioned(
|
||||||
child: Align(
|
bottom: 80, // Space for FABs
|
||||||
alignment: Alignment.bottomLeft,
|
left: 0,
|
||||||
child: _buildCharacterStatus(
|
child: CharacterStatusCard(
|
||||||
battleProvider.player,
|
character: battleProvider.player,
|
||||||
isPlayer: true,
|
isPlayer: true,
|
||||||
isTurn: battleProvider.isPlayerTurn,
|
isTurn: battleProvider.isPlayerTurn,
|
||||||
key: _playerKey,
|
key: _playerKey,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Action Buttons
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
if (battleProvider.logs.isNotEmpty)
|
|
||||||
Container(
|
|
||||||
height: 60,
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.black54,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: ListView.builder(
|
|
||||||
reverse: true,
|
|
||||||
itemCount: battleProvider.logs.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final logIndex =
|
|
||||||
battleProvider.logs.length - 1 - index;
|
|
||||||
return Text(
|
|
||||||
battleProvider.logs[logIndex],
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white70,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Card(
|
|
||||||
color: Colors.grey[900],
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
||||||
children: [
|
|
||||||
_buildActionButton(
|
|
||||||
context,
|
|
||||||
"ATTACK",
|
|
||||||
ActionType.attack,
|
|
||||||
battleProvider.isPlayerTurn &&
|
|
||||||
!battleProvider.player.isDead &&
|
|
||||||
!battleProvider.enemy.isDead &&
|
|
||||||
!battleProvider.showRewardPopup,
|
|
||||||
),
|
|
||||||
_buildActionButton(
|
|
||||||
context,
|
|
||||||
"DEFEND",
|
|
||||||
ActionType.defend,
|
|
||||||
battleProvider.isPlayerTurn &&
|
|
||||||
!battleProvider.player.isDead &&
|
|
||||||
!battleProvider.enemy.isDead &&
|
|
||||||
!battleProvider.showRewardPopup,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// 3. Logs Overlay
|
||||||
|
if (_showLogs && battleProvider.logs.isNotEmpty)
|
||||||
|
Positioned(
|
||||||
|
top: 60,
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
height: 150,
|
||||||
|
child: BattleLogOverlay(logs: battleProvider.logs),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 4. Floating Action Buttons (Bottom Right)
|
||||||
|
Positioned(
|
||||||
|
bottom: 20,
|
||||||
|
right: 20,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
_buildFloatingActionButton(
|
||||||
|
context,
|
||||||
|
"ATK",
|
||||||
|
Icons.whatshot,
|
||||||
|
Colors.redAccent,
|
||||||
|
ActionType.attack,
|
||||||
|
battleProvider.isPlayerTurn &&
|
||||||
|
!battleProvider.player.isDead &&
|
||||||
|
!battleProvider.enemy.isDead &&
|
||||||
|
!battleProvider.showRewardPopup,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildFloatingActionButton(
|
||||||
|
context,
|
||||||
|
"DEF",
|
||||||
|
Icons.shield,
|
||||||
|
Colors.blueAccent,
|
||||||
|
ActionType.defend,
|
||||||
|
battleProvider.isPlayerTurn &&
|
||||||
|
!battleProvider.player.isDead &&
|
||||||
|
!battleProvider.enemy.isDead &&
|
||||||
|
!battleProvider.showRewardPopup,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 5. Log Toggle Button (Bottom Left)
|
||||||
|
Positioned(
|
||||||
|
bottom: 20,
|
||||||
|
left: 20,
|
||||||
|
child: FloatingActionButton(
|
||||||
|
heroTag: "logToggle",
|
||||||
|
mini: true,
|
||||||
|
backgroundColor: Colors.grey[800],
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_showLogs = !_showLogs;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Icon(
|
||||||
|
_showLogs ? Icons.visibility_off : Icons.visibility,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Reward Popup
|
||||||
if (battleProvider.showRewardPopup)
|
if (battleProvider.showRewardPopup)
|
||||||
Container(
|
Container(
|
||||||
color: Colors.black54,
|
color: Colors.black54,
|
||||||
|
|
@ -519,9 +522,11 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Floating Effects
|
||||||
..._floatingDamageTexts.map((e) => e.widget),
|
..._floatingDamageTexts.map((e) => e.widget),
|
||||||
..._floatingEffects.map((e) => e.widget),
|
..._floatingEffects.map((e) => e.widget),
|
||||||
..._floatingFeedbackTexts.map((e) => e.widget), // 새로운 피드백 텍스트 추가
|
..._floatingFeedbackTexts.map((e) => e.widget),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -529,49 +534,6 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildShopUI(BuildContext context, BattleProvider battleProvider) {
|
|
||||||
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"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildRestUI(BuildContext context, BattleProvider battleProvider) {
|
|
||||||
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);
|
|
||||||
battleProvider.proceedToNextStage();
|
|
||||||
},
|
|
||||||
child: const Text("Rest & Leave (+20 HP)"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildItemStatText(Item item) {
|
Widget _buildItemStatText(Item item) {
|
||||||
List<String> stats = [];
|
List<String> stats = [];
|
||||||
if (item.atkBonus > 0) stats.add("+${item.atkBonus} ATK");
|
if (item.atkBonus > 0) stats.add("+${item.atkBonus} ATK");
|
||||||
|
|
@ -605,435 +567,21 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCharacterStatus(
|
Widget _buildFloatingActionButton(
|
||||||
Character character, {
|
|
||||||
bool isPlayer = false,
|
|
||||||
bool isTurn = false,
|
|
||||||
Key? key,
|
|
||||||
}) {
|
|
||||||
return Column(
|
|
||||||
key: key,
|
|
||||||
children: [
|
|
||||||
Text("Armor: ${character.armor}"),
|
|
||||||
Text(
|
|
||||||
"${character.name}: HP ${character.hp}/${character.totalMaxHp}",
|
|
||||||
style: TextStyle(
|
|
||||||
color: character.isDead ? Colors.red : Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 100,
|
|
||||||
child: LinearProgressIndicator(
|
|
||||||
value: character.totalMaxHp > 0
|
|
||||||
? character.hp / character.totalMaxHp
|
|
||||||
: 0,
|
|
||||||
color: !isPlayer ? Colors.red : Colors.green,
|
|
||||||
backgroundColor: Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
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(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text("ATK: ${character.totalAtk}"),
|
|
||||||
Text("DEF: ${character.totalDefense}"),
|
|
||||||
// 캐릭터 아이콘/이미지 영역 추가
|
|
||||||
Container(
|
|
||||||
width: 100, // 임시 크기
|
|
||||||
height: 100, // 임시 크기
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isPlayer
|
|
||||||
? Colors.lightBlue
|
|
||||||
: Colors.deepOrange, // 플레이어/적 구분 색상
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: isPlayer
|
|
||||||
? const Icon(
|
|
||||||
Icons.person,
|
|
||||||
size: 60,
|
|
||||||
color: Colors.white,
|
|
||||||
) // 플레이어 아이콘
|
|
||||||
: const Icon(
|
|
||||||
Icons.psychology,
|
|
||||||
size: 60,
|
|
||||||
color: Colors.white,
|
|
||||||
), // 적 아이콘 (몬스터 대신)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8), // 아이콘과 정보 사이 간격
|
|
||||||
|
|
||||||
if (!isPlayer)
|
|
||||||
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();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildActionButton(
|
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
String text,
|
String label,
|
||||||
|
IconData icon,
|
||||||
|
Color color,
|
||||||
ActionType actionType,
|
ActionType actionType,
|
||||||
bool isEnabled,
|
bool isEnabled,
|
||||||
) {
|
) {
|
||||||
return ElevatedButton(
|
return FloatingActionButton(
|
||||||
|
heroTag: label,
|
||||||
onPressed: isEnabled
|
onPressed: isEnabled
|
||||||
? () => _showRiskLevelSelection(context, actionType)
|
? () => _showRiskLevelSelection(context, actionType)
|
||||||
: null,
|
: null,
|
||||||
style: ElevatedButton.styleFrom(
|
backgroundColor: isEnabled ? color : Colors.grey,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15),
|
child: Icon(icon),
|
||||||
backgroundColor: Colors.blueGrey,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
textStyle: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
child: Text(text),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FloatingDamageText extends StatefulWidget {
|
|
||||||
final String damage;
|
|
||||||
final Color color;
|
|
||||||
final VoidCallback onRemove;
|
|
||||||
|
|
||||||
const _FloatingDamageText({
|
|
||||||
Key? key,
|
|
||||||
required this.damage,
|
|
||||||
required this.color,
|
|
||||||
required this.onRemove,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
__FloatingDamageTextState createState() => __FloatingDamageTextState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class __FloatingDamageTextState extends State<_FloatingDamageText>
|
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
late AnimationController _controller;
|
|
||||||
late Animation<Offset> _offsetAnimation;
|
|
||||||
late Animation<double> _opacityAnimation;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_controller = AnimationController(
|
|
||||||
duration: const Duration(milliseconds: 1000),
|
|
||||||
vsync: this,
|
|
||||||
);
|
|
||||||
|
|
||||||
_offsetAnimation = Tween<Offset>(
|
|
||||||
begin: const Offset(0.0, 0.0),
|
|
||||||
end: const Offset(0.0, -1.5),
|
|
||||||
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
|
|
||||||
|
|
||||||
_opacityAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
|
|
||||||
CurvedAnimation(
|
|
||||||
parent: _controller,
|
|
||||||
curve: const Interval(0.5, 1.0, curve: Curves.easeOut),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
_controller.forward().then((_) {
|
|
||||||
if (mounted) {
|
|
||||||
widget.onRemove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_controller.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AnimatedBuilder(
|
|
||||||
animation: _controller,
|
|
||||||
builder: (context, child) {
|
|
||||||
return FractionalTranslation(
|
|
||||||
translation: _offsetAnimation.value,
|
|
||||||
child: Opacity(
|
|
||||||
opacity: _opacityAnimation.value,
|
|
||||||
child: Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: Text(
|
|
||||||
widget.damage,
|
|
||||||
style: TextStyle(
|
|
||||||
color: widget.color,
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
shadows: const [
|
|
||||||
Shadow(
|
|
||||||
blurRadius: 2.0,
|
|
||||||
color: Colors.black,
|
|
||||||
offset: Offset(1.0, 1.0),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DamageTextData {
|
|
||||||
final String id;
|
|
||||||
final Widget widget;
|
|
||||||
|
|
||||||
_DamageTextData({required this.id, required this.widget});
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FloatingEffect extends StatefulWidget {
|
|
||||||
final IconData icon;
|
|
||||||
final Color color;
|
|
||||||
final double size;
|
|
||||||
final VoidCallback onRemove;
|
|
||||||
|
|
||||||
const _FloatingEffect({
|
|
||||||
Key? key,
|
|
||||||
required this.icon,
|
|
||||||
required this.color,
|
|
||||||
required this.size,
|
|
||||||
required this.onRemove,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
__FloatingEffectState createState() => __FloatingEffectState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class __FloatingEffectState extends State<_FloatingEffect>
|
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
late AnimationController _controller;
|
|
||||||
late Animation<double> _scaleAnimation;
|
|
||||||
late Animation<double> _opacityAnimation;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_controller = AnimationController(
|
|
||||||
duration: const Duration(milliseconds: 800),
|
|
||||||
vsync: this,
|
|
||||||
);
|
|
||||||
|
|
||||||
_scaleAnimation = Tween<double>(
|
|
||||||
begin: 0.5,
|
|
||||||
end: 1.5,
|
|
||||||
).animate(CurvedAnimation(parent: _controller, curve: Curves.elasticOut));
|
|
||||||
|
|
||||||
_opacityAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
|
|
||||||
CurvedAnimation(
|
|
||||||
parent: _controller,
|
|
||||||
curve: const Interval(0.5, 1.0, curve: Curves.easeOut),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
_controller.forward().then((_) {
|
|
||||||
if (mounted) {
|
|
||||||
widget.onRemove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_controller.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AnimatedBuilder(
|
|
||||||
animation: _controller,
|
|
||||||
builder: (context, child) {
|
|
||||||
return Transform.scale(
|
|
||||||
scale: _scaleAnimation.value,
|
|
||||||
child: Opacity(
|
|
||||||
opacity: _opacityAnimation.value,
|
|
||||||
child: Icon(widget.icon, color: widget.color, size: widget.size),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FloatingEffectData {
|
|
||||||
final String id;
|
|
||||||
final Widget widget;
|
|
||||||
|
|
||||||
_FloatingEffectData({required this.id, required this.widget});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 새로운 _FloatingFeedbackText 위젯
|
|
||||||
class _FloatingFeedbackText extends StatefulWidget {
|
|
||||||
final String feedback;
|
|
||||||
final Color color;
|
|
||||||
final VoidCallback onRemove;
|
|
||||||
|
|
||||||
const _FloatingFeedbackText({
|
|
||||||
Key? key,
|
|
||||||
required this.feedback,
|
|
||||||
required this.color,
|
|
||||||
required this.onRemove,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
__FloatingFeedbackTextState createState() => __FloatingFeedbackTextState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class __FloatingFeedbackTextState extends State<_FloatingFeedbackText>
|
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
late AnimationController _controller;
|
|
||||||
late Animation<Offset> _offsetAnimation;
|
|
||||||
late Animation<double> _opacityAnimation;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_controller = AnimationController(
|
|
||||||
duration: const Duration(milliseconds: 1000),
|
|
||||||
vsync: this,
|
|
||||||
);
|
|
||||||
|
|
||||||
_offsetAnimation = Tween<Offset>(
|
|
||||||
begin: const Offset(0.0, 0.0),
|
|
||||||
end: const Offset(0.0, -1.5),
|
|
||||||
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
|
|
||||||
|
|
||||||
_opacityAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
|
|
||||||
CurvedAnimation(
|
|
||||||
parent: _controller,
|
|
||||||
curve: const Interval(0.5, 1.0, curve: Curves.easeOut),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
_controller.forward().then((_) {
|
|
||||||
if (mounted) {
|
|
||||||
widget.onRemove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_controller.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AnimatedBuilder(
|
|
||||||
animation: _controller,
|
|
||||||
builder: (context, child) {
|
|
||||||
return FractionalTranslation(
|
|
||||||
translation: _offsetAnimation.value,
|
|
||||||
child: Opacity(
|
|
||||||
opacity: _opacityAnimation.value,
|
|
||||||
child: Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: Text(
|
|
||||||
widget.feedback,
|
|
||||||
style: TextStyle(
|
|
||||||
color: widget.color,
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
shadows: const [
|
|
||||||
Shadow(
|
|
||||||
blurRadius: 2.0,
|
|
||||||
color: Colors.black,
|
|
||||||
offset: Offset(1.0, 1.0),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FeedbackTextData {
|
|
||||||
final String id;
|
|
||||||
final Widget widget;
|
|
||||||
|
|
||||||
_FeedbackTextData({required this.id, required this.widget});
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +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 '../game/data/player_table.dart';
|
||||||
import 'main_wrapper.dart';
|
import 'main_wrapper.dart';
|
||||||
import '../widgets/responsive_container.dart';
|
import '../widgets/responsive_container.dart';
|
||||||
|
|
||||||
|
|
@ -9,6 +10,15 @@ class CharacterSelectionScreen extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// Fetch Warrior data
|
||||||
|
final warrior = PlayerTable.get("warrior");
|
||||||
|
|
||||||
|
if (warrior == null) {
|
||||||
|
return const Scaffold(
|
||||||
|
body: Center(child: Text("Error: Player data not found")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.black, // Outer background
|
backgroundColor: Colors.black, // Outer background
|
||||||
body: Center(
|
body: Center(
|
||||||
|
|
@ -51,37 +61,43 @@ class CharacterSelectionScreen extends StatelessWidget {
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Text(
|
Text(
|
||||||
"Warrior",
|
warrior.name,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const Text(
|
Text(
|
||||||
"A balanced fighter with a sword and shield. Great for beginners.",
|
warrior.description,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(color: Colors.grey),
|
style: const TextStyle(color: Colors.grey),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"HP: 80",
|
"HP: ${warrior.baseHp}",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"ATK: 5",
|
"ATK: ${warrior.baseAtk}",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"DEF: 5",
|
"DEF: ${warrior.baseDefense}",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -88,41 +88,73 @@ class InventoryScreen extends StatelessWidget {
|
||||||
color: item != null
|
color: item != null
|
||||||
? Colors.blueGrey[600]
|
? Colors.blueGrey[600]
|
||||||
: Colors.grey[800],
|
: Colors.grey[800],
|
||||||
child: Padding(
|
child: Stack(
|
||||||
padding: const EdgeInsets.all(8.0),
|
children: [
|
||||||
child: Column(
|
// Slot Name (Top Right)
|
||||||
children: [
|
Positioned(
|
||||||
Text(
|
right: 4,
|
||||||
|
top: 4,
|
||||||
|
child: Text(
|
||||||
slot.name.toUpperCase(),
|
slot.name.toUpperCase(),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 8,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.grey,
|
color: Colors.white30,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
),
|
||||||
Icon(
|
// Faded Icon (Top Left)
|
||||||
ItemUtils.getIcon(slot),
|
Positioned(
|
||||||
size: 24,
|
left: 4,
|
||||||
color: item != null
|
top: 4,
|
||||||
? ItemUtils.getColor(slot)
|
child: Opacity(
|
||||||
: Colors.grey,
|
opacity: item != null ? 0.2 : 0.1,
|
||||||
),
|
child: Icon(
|
||||||
const SizedBox(height: 4),
|
ItemUtils.getIcon(slot),
|
||||||
Text(
|
size: 40,
|
||||||
item?.name ?? "Empty",
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: item != null
|
color: item != null
|
||||||
? Colors.white
|
? ItemUtils.getColor(slot)
|
||||||
: Colors.grey,
|
: Colors.grey,
|
||||||
),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
),
|
||||||
if (item != null) _buildItemStatText(item),
|
),
|
||||||
],
|
// Content
|
||||||
),
|
Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(4.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
), // Spacing for top elements
|
||||||
|
FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Text(
|
||||||
|
item?.name ?? "Empty",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: item != null
|
||||||
|
? Colors.white
|
||||||
|
: Colors.grey,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (item != null)
|
||||||
|
FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: _buildItemStatText(item),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -169,24 +201,49 @@ class InventoryScreen extends StatelessWidget {
|
||||||
},
|
},
|
||||||
child: Card(
|
child: Card(
|
||||||
color: Colors.blueGrey[700],
|
color: Colors.blueGrey[700],
|
||||||
child: Column(
|
child: Stack(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
// Faded Icon in Top-Left
|
||||||
ItemUtils.getIcon(item.slot),
|
Positioned(
|
||||||
size: 32,
|
left: 4,
|
||||||
color: ItemUtils.getColor(item.slot),
|
top: 4,
|
||||||
),
|
child: Opacity(
|
||||||
Padding(
|
opacity: 0.2,
|
||||||
padding: const EdgeInsets.all(4.0),
|
child: Icon(
|
||||||
child: Text(
|
ItemUtils.getIcon(item.slot),
|
||||||
item.name,
|
size: 40,
|
||||||
textAlign: TextAlign.center,
|
color: ItemUtils.getColor(item.slot),
|
||||||
style: const TextStyle(fontSize: 10),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
),
|
||||||
|
),
|
||||||
|
// Centered Content
|
||||||
|
Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(4.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Text(
|
||||||
|
item.name,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: _buildItemStatText(item),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildItemStatText(item),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class BattleLogOverlay extends StatelessWidget {
|
||||||
|
final List<String> logs;
|
||||||
|
|
||||||
|
const BattleLogOverlay({super.key, required this.logs});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (logs.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black54,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: ListView.builder(
|
||||||
|
reverse: true,
|
||||||
|
itemCount: logs.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final logIndex = logs.length - 1 - index;
|
||||||
|
return Text(
|
||||||
|
logs[logIndex],
|
||||||
|
style: const TextStyle(color: Colors.white70, fontSize: 12),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import '../../game/model/entity.dart';
|
||||||
|
import '../../game/enums.dart';
|
||||||
|
import '../../providers/battle_provider.dart';
|
||||||
|
|
||||||
|
class CharacterStatusCard extends StatelessWidget {
|
||||||
|
final Character character;
|
||||||
|
final bool isPlayer;
|
||||||
|
final bool isTurn;
|
||||||
|
|
||||||
|
const CharacterStatusCard({
|
||||||
|
super.key,
|
||||||
|
required this.character,
|
||||||
|
this.isPlayer = false,
|
||||||
|
this.isTurn = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Text(
|
||||||
|
"Armor: ${character.armor}",
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Text(
|
||||||
|
"${character.name}: HP ${character.hp}/${character.totalMaxHp}",
|
||||||
|
style: TextStyle(
|
||||||
|
color: character.isDead ? Colors.red : Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 100,
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: character.totalMaxHp > 0
|
||||||
|
? character.hp / character.totalMaxHp
|
||||||
|
: 0,
|
||||||
|
color: !isPlayer ? Colors.red : Colors.green,
|
||||||
|
backgroundColor: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text("ATK: ${character.totalAtk}"),
|
||||||
|
Text("DEF: ${character.totalDefense}"),
|
||||||
|
// 캐릭터 아이콘/이미지 영역 추가
|
||||||
|
Container(
|
||||||
|
width: 100, // 임시 크기
|
||||||
|
height: 100, // 임시 크기
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isPlayer
|
||||||
|
? Colors.lightBlue
|
||||||
|
: Colors.deepOrange, // 플레이어/적 구분 색상
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: isPlayer
|
||||||
|
? const Icon(
|
||||||
|
Icons.person,
|
||||||
|
size: 60,
|
||||||
|
color: Colors.white,
|
||||||
|
) // 플레이어 아이콘
|
||||||
|
: const Icon(
|
||||||
|
Icons.psychology,
|
||||||
|
size: 60,
|
||||||
|
color: Colors.white,
|
||||||
|
), // 적 아이콘 (몬스터 대신)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8), // 아이콘과 정보 사이 간격
|
||||||
|
|
||||||
|
if (!isPlayer)
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,274 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class FloatingDamageText extends StatefulWidget {
|
||||||
|
final String damage;
|
||||||
|
final Color color;
|
||||||
|
final VoidCallback onRemove;
|
||||||
|
|
||||||
|
const FloatingDamageText({
|
||||||
|
Key? key,
|
||||||
|
required this.damage,
|
||||||
|
required this.color,
|
||||||
|
required this.onRemove,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
FloatingDamageTextState createState() => FloatingDamageTextState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class FloatingDamageTextState extends State<FloatingDamageText>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<Offset> _offsetAnimation;
|
||||||
|
late Animation<double> _opacityAnimation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 1000),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
|
||||||
|
_offsetAnimation = Tween<Offset>(
|
||||||
|
begin: const Offset(0.0, 0.0),
|
||||||
|
end: const Offset(0.0, -1.5),
|
||||||
|
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
|
||||||
|
|
||||||
|
_opacityAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: const Interval(0.5, 1.0, curve: Curves.easeOut),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_controller.forward().then((_) {
|
||||||
|
if (mounted) {
|
||||||
|
widget.onRemove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, child) {
|
||||||
|
return FractionalTranslation(
|
||||||
|
translation: _offsetAnimation.value,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: _opacityAnimation.value,
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Text(
|
||||||
|
widget.damage,
|
||||||
|
style: TextStyle(
|
||||||
|
color: widget.color,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
shadows: const [
|
||||||
|
Shadow(
|
||||||
|
blurRadius: 2.0,
|
||||||
|
color: Colors.black,
|
||||||
|
offset: Offset(1.0, 1.0),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DamageTextData {
|
||||||
|
final String id;
|
||||||
|
final Widget widget;
|
||||||
|
|
||||||
|
DamageTextData({required this.id, required this.widget});
|
||||||
|
}
|
||||||
|
|
||||||
|
class FloatingEffect extends StatefulWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final Color color;
|
||||||
|
final double size;
|
||||||
|
final VoidCallback onRemove;
|
||||||
|
|
||||||
|
const FloatingEffect({
|
||||||
|
Key? key,
|
||||||
|
required this.icon,
|
||||||
|
required this.color,
|
||||||
|
required this.size,
|
||||||
|
required this.onRemove,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
FloatingEffectState createState() => FloatingEffectState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class FloatingEffectState extends State<FloatingEffect>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<double> _scaleAnimation;
|
||||||
|
late Animation<double> _opacityAnimation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 800),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
|
||||||
|
_scaleAnimation = Tween<double>(
|
||||||
|
begin: 0.5,
|
||||||
|
end: 1.5,
|
||||||
|
).animate(CurvedAnimation(parent: _controller, curve: Curves.elasticOut));
|
||||||
|
|
||||||
|
_opacityAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: const Interval(0.5, 1.0, curve: Curves.easeOut),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_controller.forward().then((_) {
|
||||||
|
if (mounted) {
|
||||||
|
widget.onRemove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Transform.scale(
|
||||||
|
scale: _scaleAnimation.value,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: _opacityAnimation.value,
|
||||||
|
child: Icon(widget.icon, color: widget.color, size: widget.size),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FloatingEffectData {
|
||||||
|
final String id;
|
||||||
|
final Widget widget;
|
||||||
|
|
||||||
|
FloatingEffectData({required this.id, required this.widget});
|
||||||
|
}
|
||||||
|
|
||||||
|
class FloatingFeedbackText extends StatefulWidget {
|
||||||
|
final String feedback;
|
||||||
|
final Color color;
|
||||||
|
final VoidCallback onRemove;
|
||||||
|
|
||||||
|
const FloatingFeedbackText({
|
||||||
|
Key? key,
|
||||||
|
required this.feedback,
|
||||||
|
required this.color,
|
||||||
|
required this.onRemove,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
FloatingFeedbackTextState createState() => FloatingFeedbackTextState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class FloatingFeedbackTextState extends State<FloatingFeedbackText>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<Offset> _offsetAnimation;
|
||||||
|
late Animation<double> _opacityAnimation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 1000),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
|
||||||
|
_offsetAnimation = Tween<Offset>(
|
||||||
|
begin: const Offset(0.0, 0.0),
|
||||||
|
end: const Offset(0.0, -1.5),
|
||||||
|
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
|
||||||
|
|
||||||
|
_opacityAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: const Interval(0.5, 1.0, curve: Curves.easeOut),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_controller.forward().then((_) {
|
||||||
|
if (mounted) {
|
||||||
|
widget.onRemove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, child) {
|
||||||
|
return FractionalTranslation(
|
||||||
|
translation: _offsetAnimation.value,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: _opacityAnimation.value,
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Text(
|
||||||
|
widget.feedback,
|
||||||
|
style: TextStyle(
|
||||||
|
color: widget.color,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
shadows: const [
|
||||||
|
Shadow(
|
||||||
|
blurRadius: 2.0,
|
||||||
|
color: Colors.black,
|
||||||
|
offset: Offset(1.0, 1.0),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FeedbackTextData {
|
||||||
|
final String id;
|
||||||
|
final Widget widget;
|
||||||
|
|
||||||
|
FeedbackTextData({required this.id, required this.widget});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../providers/battle_provider.dart';
|
||||||
|
|
||||||
|
class ShopUI extends StatelessWidget {
|
||||||
|
final BattleProvider battleProvider;
|
||||||
|
|
||||||
|
const ShopUI({super.key, required this.battleProvider});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
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"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RestUI extends StatelessWidget {
|
||||||
|
final BattleProvider battleProvider;
|
||||||
|
|
||||||
|
const RestUI({super.key, required this.battleProvider});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
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);
|
||||||
|
battleProvider.proceedToNextStage();
|
||||||
|
},
|
||||||
|
child: const Text("Rest & Leave (+20 HP)"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
- `ResponsiveContainer` 위젯을 통해 최대 너비(600px) 및 높이(1000px) 제한.
|
- `ResponsiveContainer` 위젯을 통해 최대 너비(600px) 및 높이(1000px) 제한.
|
||||||
- 웹/태블릿 환경에서도 모바일 앱처럼 중앙 정렬된 화면 제공.
|
- 웹/태블릿 환경에서도 모바일 앱처럼 중앙 정렬된 화면 제공.
|
||||||
- **Battle UI Layout:** 플레이어(좌측 하단) vs 적(우측 상단) 대각선 구도 배치 및 캐릭터 임시 아이콘 적용.
|
- **Battle UI Layout:** 플레이어(좌측 하단) vs 적(우측 상단) 대각선 구도 배치 및 캐릭터 임시 아이콘 적용.
|
||||||
|
- **Widget Refactoring:** `BattleScreen`의 주요 UI 컴포넌트(`CharacterStatusCard`, `BattleLogOverlay` 등)를 `lib/widgets/battle/`로 분리하여 모듈화.
|
||||||
|
|
||||||
### B. 전투 시스템 (`BattleProvider`)
|
### B. 전투 시스템 (`BattleProvider`)
|
||||||
|
|
||||||
|
|
@ -31,6 +32,9 @@
|
||||||
- **선제 방어 (Pre-emptive Defense):** 적이 방어를 선택하면 턴 시작 전에 즉시 방어도가 적용됨.
|
- **선제 방어 (Pre-emptive Defense):** 적이 방어를 선택하면 턴 시작 전에 즉시 방어도가 적용됨.
|
||||||
- **Defense Restriction:** `DefenseForbidden` 상태 시 방어 행동 선택 불가.
|
- **Defense Restriction:** `DefenseForbidden` 상태 시 방어 행동 선택 불가.
|
||||||
- **Variance Removed:** 적의 공격/방어 수치 계산 시 랜덤 분산(Variance) 제거 (고정 수치).
|
- **Variance Removed:** 적의 공격/방어 수치 계산 시 랜덤 분산(Variance) 제거 (고정 수치).
|
||||||
|
- **적 장비 시스템 (Enemy Equipment):**
|
||||||
|
- 적에게 아이템 장착 가능 (`enemies.json`의 `equipment` 필드).
|
||||||
|
- 장착된 아이템의 스탯 및 특수 효과(상태이상 등)가 전투 시 적용됨.
|
||||||
- **시각 효과 (Visual Effects):**
|
- **시각 효과 (Visual Effects):**
|
||||||
- **Floating Text:** 데미지 발생 시 캐릭터 위에 데미지 수치가 떠오름 (일반: 빨강, 출혈: 보라, 취약: 주황).
|
- **Floating Text:** 데미지 발생 시 캐릭터 위에 데미지 수치가 떠오름 (일반: 빨강, 출혈: 보라, 취약: 주황).
|
||||||
- **Action Feedback:** 공격 빗나감(MISS - 적 위치/회색) 및 방어 실패(FAILED - 내 위치/빨강) 텍스트 오버레이.
|
- **Action Feedback:** 공격 빗나감(MISS - 적 위치/회색) 및 방어 실패(FAILED - 내 위치/빨강) 텍스트 오버레이.
|
||||||
|
|
@ -39,8 +43,8 @@
|
||||||
|
|
||||||
### C. 데이터 주도 설계 (Data-Driven Design)
|
### C. 데이터 주도 설계 (Data-Driven Design)
|
||||||
|
|
||||||
- **JSON 데이터:** `assets/data/items.json`, `assets/data/enemies.json`.
|
- **JSON 데이터:** `assets/data/items.json` (ID 포함), `assets/data/enemies.json` (장비 포함).
|
||||||
- **데이터 로더:** `ItemTable`, `EnemyTable`.
|
- **데이터 로더:** `ItemTable` (ID 조회 지원), `EnemyTable` (장비 장착 지원).
|
||||||
|
|
||||||
### D. 아이템 및 경제 (`Item`, `Inventory`)
|
### D. 아이템 및 경제 (`Item`, `Inventory`)
|
||||||
|
|
||||||
|
|
@ -65,13 +69,15 @@
|
||||||
- **Streams:** `damageStream`, `effectStream`을 통해 UI(`BattleScreen`)에 비동기 이벤트 전달.
|
- **Streams:** `damageStream`, `effectStream`을 통해 UI(`BattleScreen`)에 비동기 이벤트 전달.
|
||||||
- **`lib/game/enums.dart`:** 프로젝트 전반의 Enum 통합 관리 (`ActionType`, `RiskLevel`, `StageType` 등).
|
- **`lib/game/enums.dart`:** 프로젝트 전반의 Enum 통합 관리 (`ActionType`, `RiskLevel`, `StageType` 등).
|
||||||
- **`lib/utils/item_utils.dart`:** 아이템 타입별 아이콘 및 색상 로직 중앙화.
|
- **`lib/utils/item_utils.dart`:** 아이템 타입별 아이콘 및 색상 로직 중앙화.
|
||||||
|
- **`lib/widgets/battle/`:** `BattleScreen`에서 분리된 재사용 가능한 위젯들 (`CharacterStatusCard`, `BattleLogOverlay`, `FloatingBattleTexts`, `StageUI`).
|
||||||
- **`lib/widgets/responsive_container.dart`:** 반응형 레이아웃 컨테이너.
|
- **`lib/widgets/responsive_container.dart`:** 반응형 레이아웃 컨테이너.
|
||||||
- **`lib/game/model/`:**
|
- **`lib/game/model/`:**
|
||||||
- `damage_event.dart`, `effect_event.dart`: 이벤트 모델.
|
- `damage_event.dart`, `effect_event.dart`: 이벤트 모델.
|
||||||
- `entity.dart`: `Character` (Player/Enemy).
|
- `entity.dart`: `Character` (Player/Enemy).
|
||||||
- `item.dart`: `Item`.
|
- `item.dart`: `Item` (ID 필드 포함).
|
||||||
- **`lib/screens/battle_screen.dart`:**
|
- **`lib/screens/battle_screen.dart`:**
|
||||||
- `StreamSubscription`을 통해 이펙트 이벤트 수신 및 `Overlay` 애니메이션 렌더링.
|
- `StreamSubscription`을 통해 이펙트 이벤트 수신 및 `Overlay` 애니메이션 렌더링.
|
||||||
|
- `Stack` 및 `Positioned` 기반의 정교한 레이아웃.
|
||||||
|
|
||||||
## 4. 작업 컨벤션 (Working Conventions)
|
## 4. 작업 컨벤션 (Working Conventions)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
# 배틀 화면 위젯 리팩토링
|
||||||
|
|
||||||
|
## 목표
|
||||||
|
|
||||||
|
UI 컴포넌트를 별도의 위젯으로 추출하여 `BattleScreen`의 코드 유지보수성, 가독성 및 구조를 개선합니다.
|
||||||
|
|
||||||
|
## 변경 사항
|
||||||
|
|
||||||
|
1. **디렉토리 구조:**
|
||||||
|
|
||||||
|
- `lib/widgets/battle/` 디렉토리를 생성했습니다.
|
||||||
|
|
||||||
|
2. **추출된 위젯:**
|
||||||
|
|
||||||
|
- `CharacterStatusCard` (`lib/widgets/battle/character_status_card.dart`): 캐릭터 스탯(HP, 방어도, 공격력, 방어력), 체력 바, 상태 효과 및 의도를 표시합니다.
|
||||||
|
- `BattleLogOverlay` (`lib/widgets/battle/battle_log_overlay.dart`): 스크롤 가능한 배틀 로그 목록을 표시합니다.
|
||||||
|
- `FloatingBattleTexts` (`lib/widgets/battle/floating_battle_texts.dart`): 애니메이션 시각 효과를 위한 `FloatingDamageText`, `FloatingEffect`, `FloatingFeedbackText`를 포함합니다.
|
||||||
|
- `StageUI` (`lib/widgets/battle/stage_ui.dart`): 비전투 스테이지를 위한 `ShopUI`와 `RestUI`를 포함합니다.
|
||||||
|
|
||||||
|
3. **BattleScreen 업데이트:**
|
||||||
|
- 추출된 위젯을 임포트하고 사용하도록 `lib/screens/battle_screen.dart`를 리팩토링했습니다.
|
||||||
|
- 인라인 위젯 빌드 메서드(`_buildCharacterStatus`, `_buildShopUI`, `_buildRestUI` 등)와 내부 클래스(`_FloatingDamageText` 등)를 제거했습니다.
|
||||||
|
|
||||||
|
## 이점
|
||||||
|
|
||||||
|
- **복잡도 감소:** `BattleScreen`은 이제 레이아웃과 상태 관리에 집중합니다.
|
||||||
|
- **재사용성:** 위젯은 필요에 따라 앱의 다른 부분에서 재사용할 수 있습니다.
|
||||||
|
- **유지보수성:** 특정 UI 요소를 찾고 수정하기가 더 쉬워졌습니다.
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
# 적 장비 시스템 구현 (Enemy Equipment System)
|
||||||
|
|
||||||
|
## 목표
|
||||||
|
|
||||||
|
적에게 아이템을 장착시켜 전투의 다양성을 높이고, 아이템 데이터 구조를 개선합니다.
|
||||||
|
|
||||||
|
## 주요 변경 사항
|
||||||
|
|
||||||
|
### 1. 데이터 구조 개선
|
||||||
|
|
||||||
|
- **`assets/data/items.json`**: 모든 아이템에 고유 `id` 필드를 추가했습니다. (예: `"id": "rusty_dagger"`)
|
||||||
|
- **`assets/data/enemies.json`**: 적 정보에 `equipment` 필드(아이템 ID 리스트)를 추가했습니다. (예: `Goblin`은 `rusty_dagger` 장착)
|
||||||
|
|
||||||
|
### 2. 게임 로직 업데이트
|
||||||
|
|
||||||
|
- **`lib/game/model/item.dart`**: `Item` 클래스에 `id` 필드를 추가했습니다.
|
||||||
|
- **`lib/game/data/item_table.dart`**: ID로 아이템을 조회하는 `get(String id)` 메서드를 구현했습니다.
|
||||||
|
- **`lib/game/data/enemy_table.dart`**: 적 생성(`createCharacter`) 시 `equipment` 필드에 명시된 아이템을 자동으로 인벤토리에 추가하고 장착하도록 수정했습니다.
|
||||||
|
- **`lib/providers/battle_provider.dart`**: 초기 플레이어 지급 아이템 생성 시 `id`를 포함하도록 수정했습니다.
|
||||||
|
|
||||||
|
### 3. 버그 수정
|
||||||
|
|
||||||
|
- **`lib/screens/battle_screen.dart`**: `ScrollController`가 연결되지 않아 발생하던 에러를 수정했습니다. (불필요한 컨트롤러 제거)
|
||||||
|
|
||||||
|
## 결과
|
||||||
|
|
||||||
|
이제 `enemies.json` 설정만으로 적에게 다양한 장비를 입혀 스탯과 특수 효과(출혈, 스턴 등)를 부여할 수 있습니다.
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
# 플레이어 데이터 중앙화 (Centralize Player Data)
|
||||||
|
|
||||||
|
## 목표
|
||||||
|
|
||||||
|
캐릭터 선택 화면과 전투 시스템(`BattleProvider`)에서 사용하는 플레이어 스탯이 하드코딩되어 불일치하는 문제를 해결하기 위해, 플레이어 데이터를 JSON 파일로 중앙화하여 관리합니다.
|
||||||
|
|
||||||
|
## 주요 변경 사항
|
||||||
|
|
||||||
|
### 1. 데이터 구조 추가
|
||||||
|
|
||||||
|
- **`assets/data/players.json`**: 플레이어 템플릿 데이터를 정의했습니다.
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "warrior",
|
||||||
|
"name": "Warrior",
|
||||||
|
"description": "A balanced fighter...",
|
||||||
|
"baseHp": 50,
|
||||||
|
"baseAtk": 5,
|
||||||
|
"baseDefense": 5,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
- **`lib/game/data/player_table.dart`**: `players.json`을 로드하고 파싱하는 `PlayerTable` 클래스를 구현했습니다.
|
||||||
|
|
||||||
|
### 2. 게임 로직 업데이트
|
||||||
|
|
||||||
|
- **`lib/main.dart`**: 앱 시작 시 `PlayerTable.load()`를 호출하여 데이터를 메모리에 적재합니다.
|
||||||
|
- **`lib/screens/character_selection_screen.dart`**: 하드코딩된 텍스트 대신 `PlayerTable.get("warrior")`를 사용하여 UI를 렌더링합니다.
|
||||||
|
- **`lib/providers/battle_provider.dart`**: 전투 초기화(`initializeBattle`) 시 `PlayerTable`에서 캐릭터 정보를 가져와 생성합니다.
|
||||||
|
|
||||||
|
## 결과
|
||||||
|
|
||||||
|
이제 `players.json` 파일만 수정하면 게임 내 모든 곳(선택 화면, 전투 시작 스탯 등)에 일관되게 반영됩니다.
|
||||||
|
|
@ -13,6 +13,7 @@ void main() {
|
||||||
setUp(() {
|
setUp(() {
|
||||||
player = Character(name: "TestPlayer", maxHp: 100, armor: 0, atk: 10);
|
player = Character(name: "TestPlayer", maxHp: 100, armor: 0, atk: 10);
|
||||||
armorHp50 = Item(
|
armorHp50 = Item(
|
||||||
|
id: "armor_hp_50",
|
||||||
name: "Armor +50",
|
name: "Armor +50",
|
||||||
description: "HP +50",
|
description: "HP +50",
|
||||||
atkBonus: 0,
|
atkBonus: 0,
|
||||||
|
|
@ -21,6 +22,7 @@ void main() {
|
||||||
price: 100,
|
price: 100,
|
||||||
);
|
);
|
||||||
armorHp100 = Item(
|
armorHp100 = Item(
|
||||||
|
id: "armor_hp_100",
|
||||||
name: "Armor +100",
|
name: "Armor +100",
|
||||||
description: "HP +100",
|
description: "HP +100",
|
||||||
atkBonus: 0,
|
atkBonus: 0,
|
||||||
|
|
@ -29,6 +31,7 @@ void main() {
|
||||||
price: 200,
|
price: 200,
|
||||||
);
|
);
|
||||||
armorHp20 = Item(
|
armorHp20 = Item(
|
||||||
|
id: "armor_hp_20",
|
||||||
name: "Armor +20",
|
name: "Armor +20",
|
||||||
description: "HP +20",
|
description: "HP +20",
|
||||||
atkBonus: 0,
|
atkBonus: 0,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue