update
This commit is contained in:
@@ -2,12 +2,15 @@ import 'dart:convert';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../model/entity.dart';
|
||||
|
||||
import 'item_table.dart';
|
||||
|
||||
class EnemyTemplate {
|
||||
final String name;
|
||||
final int baseHp;
|
||||
final int baseAtk;
|
||||
final int baseDefense;
|
||||
final String? image;
|
||||
final List<String> equipmentIds;
|
||||
|
||||
const EnemyTemplate({
|
||||
required this.name,
|
||||
@@ -15,6 +18,7 @@ class EnemyTemplate {
|
||||
required this.baseAtk,
|
||||
required this.baseDefense,
|
||||
this.image,
|
||||
this.equipmentIds = const [],
|
||||
});
|
||||
|
||||
factory EnemyTemplate.fromJson(Map<String, dynamic> json) {
|
||||
@@ -24,6 +28,7 @@ class EnemyTemplate {
|
||||
baseAtk: json['baseAtk'] ?? 1,
|
||||
baseDefense: json['baseDefense'] ?? 0,
|
||||
image: json['image'],
|
||||
equipmentIds: (json['equipment'] as List<dynamic>?)?.cast<String>() ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,7 +38,7 @@ class EnemyTemplate {
|
||||
int scaledAtk = baseAtk + (stage - 1);
|
||||
int scaledDefense = baseDefense + (stage ~/ 5); // +1 defense every 5 stages
|
||||
|
||||
return Character(
|
||||
final character = Character(
|
||||
name: name,
|
||||
maxHp: scaledHp,
|
||||
atk: scaledAtk,
|
||||
@@ -41,6 +46,20 @@ class EnemyTemplate {
|
||||
armor: 0,
|
||||
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';
|
||||
|
||||
class ItemTemplate {
|
||||
final String id;
|
||||
final String name;
|
||||
final String description;
|
||||
final int baseAtk;
|
||||
@@ -15,6 +16,7 @@ class ItemTemplate {
|
||||
final String? image;
|
||||
|
||||
const ItemTemplate({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.description,
|
||||
this.baseAtk = 0,
|
||||
@@ -28,6 +30,9 @@ class ItemTemplate {
|
||||
|
||||
factory ItemTemplate.fromJson(Map<String, dynamic> json) {
|
||||
return ItemTemplate(
|
||||
id:
|
||||
json['id'] ??
|
||||
json['name'], // Fallback to name if id is missing (for backward compatibility during dev)
|
||||
name: json['name'],
|
||||
description: json['description'],
|
||||
baseAtk: json['baseAtk'] ?? 0,
|
||||
@@ -60,6 +65,7 @@ class ItemTemplate {
|
||||
}
|
||||
|
||||
return Item(
|
||||
id: id,
|
||||
name: "$name${stage > 1 ? ' +${stage - 1}' : ''}", // Append +1, +2 etc.
|
||||
description: description,
|
||||
atkBonus: scaledAtk,
|
||||
@@ -105,4 +111,12 @@ class ItemTable {
|
||||
...shields,
|
||||
...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 {
|
||||
final String id; // Unique identifier
|
||||
final String name;
|
||||
final String description;
|
||||
final int atkBonus;
|
||||
@@ -47,6 +48,7 @@ class Item {
|
||||
final String? image; // New: Image path
|
||||
|
||||
Item({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.atkBonus,
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'game/data/item_table.dart';
|
||||
import 'game/data/enemy_table.dart';
|
||||
import 'game/data/player_table.dart';
|
||||
import 'providers/battle_provider.dart';
|
||||
import 'screens/main_menu_screen.dart';
|
||||
|
||||
@@ -9,6 +10,7 @@ void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await ItemTable.load();
|
||||
await EnemyTable.load();
|
||||
await PlayerTable.load();
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ import '../game/model/item.dart';
|
||||
import '../game/model/status_effect.dart';
|
||||
import '../game/model/stage.dart';
|
||||
import '../game/data/item_table.dart';
|
||||
|
||||
import '../game/data/enemy_table.dart';
|
||||
import '../game/data/player_table.dart';
|
||||
import '../utils/game_math.dart';
|
||||
import '../game/enums.dart';
|
||||
import '../game/model/damage_event.dart'; // DamageEvent import
|
||||
@@ -70,16 +72,24 @@ class BattleProvider with ChangeNotifier {
|
||||
void initializeBattle() {
|
||||
stage = 1;
|
||||
turnCount = 1;
|
||||
player = Character(
|
||||
name: "Player",
|
||||
maxHp: 80,
|
||||
armor: 0,
|
||||
atk: 5,
|
||||
baseDefense: 5,
|
||||
);
|
||||
// Load player from PlayerTable
|
||||
final playerTemplate = PlayerTable.get("warrior");
|
||||
if (playerTemplate != null) {
|
||||
player = playerTemplate.createCharacter();
|
||||
} else {
|
||||
// Fallback if data is missing
|
||||
player = Character(
|
||||
name: "Player",
|
||||
maxHp: 50,
|
||||
armor: 0,
|
||||
atk: 5,
|
||||
baseDefense: 5,
|
||||
);
|
||||
}
|
||||
|
||||
// Provide starter equipment
|
||||
final starterSword = Item(
|
||||
id: "starter_sword",
|
||||
name: "Wooden Sword",
|
||||
description: "A basic sword",
|
||||
atkBonus: 5,
|
||||
@@ -87,6 +97,7 @@ class BattleProvider with ChangeNotifier {
|
||||
slot: EquipmentSlot.weapon,
|
||||
);
|
||||
final starterArmor = Item(
|
||||
id: "starter_armor",
|
||||
name: "Leather Armor",
|
||||
description: "Basic protection",
|
||||
atkBonus: 0,
|
||||
@@ -94,6 +105,7 @@ class BattleProvider with ChangeNotifier {
|
||||
slot: EquipmentSlot.armor,
|
||||
);
|
||||
final starterShield = Item(
|
||||
id: "starter_shield",
|
||||
name: "Wooden Shield",
|
||||
description: "A small shield",
|
||||
atkBonus: 0,
|
||||
@@ -102,6 +114,7 @@ class BattleProvider with ChangeNotifier {
|
||||
slot: EquipmentSlot.shield,
|
||||
);
|
||||
final starterRing = Item(
|
||||
id: "starter_ring",
|
||||
name: "Copper Ring",
|
||||
description: "A simple ring",
|
||||
atkBonus: 1,
|
||||
@@ -436,6 +449,9 @@ class BattleProvider with ChangeNotifier {
|
||||
_applyDamage(player, damageToHp, targetType: DamageTarget.player);
|
||||
_addLog("Enemy dealt $damageToHp damage to Player HP.");
|
||||
}
|
||||
|
||||
// Try applying status effects from enemy equipment
|
||||
_tryApplyStatusEffects(enemy, player);
|
||||
} else {
|
||||
_addLog("Enemy's ${intent.risk.name} attack missed!");
|
||||
_effectEventController.sink.add(
|
||||
|
||||
+139
-591
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/battle_provider.dart';
|
||||
import '../game/model/entity.dart';
|
||||
|
||||
import '../game/enums.dart';
|
||||
import '../game/model/item.dart';
|
||||
import '../game/model/damage_event.dart';
|
||||
@@ -9,6 +9,10 @@ import '../game/model/effect_event.dart';
|
||||
import 'dart:async';
|
||||
import '../widgets/responsive_container.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 {
|
||||
const BattleScreen({super.key});
|
||||
@@ -18,25 +22,19 @@ class BattleScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _BattleScreenState extends State<BattleScreen> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final List<_DamageTextData> _floatingDamageTexts = [];
|
||||
final List<_FloatingEffectData> _floatingEffects = [];
|
||||
final List<_FeedbackTextData> _floatingFeedbackTexts = [];
|
||||
final List<DamageTextData> _floatingDamageTexts = [];
|
||||
final List<FloatingEffectData> _floatingEffects = [];
|
||||
final List<FeedbackTextData> _floatingFeedbackTexts = [];
|
||||
StreamSubscription<DamageEvent>? _damageSubscription;
|
||||
StreamSubscription<EffectEvent>? _effectSubscription;
|
||||
final GlobalKey _playerKey = GlobalKey();
|
||||
final GlobalKey _enemyKey = GlobalKey();
|
||||
final GlobalKey _stackKey = GlobalKey();
|
||||
bool _showLogs = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_scrollController.hasClients) {
|
||||
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
|
||||
}
|
||||
});
|
||||
|
||||
final battleProvider = context.read<BattleProvider>();
|
||||
_damageSubscription = battleProvider.damageStream.listen(
|
||||
_addFloatingDamageText,
|
||||
@@ -48,7 +46,6 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
_damageSubscription?.cancel();
|
||||
_effectSubscription?.cancel();
|
||||
super.dispose();
|
||||
@@ -82,12 +79,12 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||
|
||||
setState(() {
|
||||
_floatingDamageTexts.add(
|
||||
_DamageTextData(
|
||||
DamageTextData(
|
||||
id: id,
|
||||
widget: Positioned(
|
||||
left: position.dx,
|
||||
top: position.dy,
|
||||
child: _FloatingDamageText(
|
||||
child: FloatingDamageText(
|
||||
key: ValueKey(id),
|
||||
damage: event.damage.toString(),
|
||||
color: event.color,
|
||||
@@ -153,12 +150,12 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||
final String id = UniqueKey().toString();
|
||||
setState(() {
|
||||
_floatingFeedbackTexts.add(
|
||||
_FeedbackTextData(
|
||||
FeedbackTextData(
|
||||
id: id,
|
||||
widget: Positioned(
|
||||
left: position.dx,
|
||||
top: position.dy,
|
||||
child: _FloatingFeedbackText(
|
||||
child: FloatingFeedbackText(
|
||||
key: ValueKey(id),
|
||||
feedback: feedbackText,
|
||||
color: feedbackColor,
|
||||
@@ -213,12 +210,12 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||
|
||||
setState(() {
|
||||
_floatingEffects.add(
|
||||
_FloatingEffectData(
|
||||
FloatingEffectData(
|
||||
id: id,
|
||||
widget: Positioned(
|
||||
left: position.dx,
|
||||
top: position.dy,
|
||||
child: _FloatingEffect(
|
||||
child: FloatingEffect(
|
||||
key: ValueKey(id),
|
||||
icon: icon,
|
||||
color: color,
|
||||
@@ -295,13 +292,6 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||
onPressed: () {
|
||||
context.read<BattleProvider>().playerAction(actionType, risk);
|
||||
Navigator.pop(context);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_scrollController.animateTo(
|
||||
_scrollController.position.maxScrollExtent,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
});
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -329,15 +319,18 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||
child: Consumer<BattleProvider>(
|
||||
builder: (context, battleProvider, child) {
|
||||
if (battleProvider.currentStage.type == StageType.shop) {
|
||||
return _buildShopUI(context, battleProvider);
|
||||
return ShopUI(battleProvider: battleProvider);
|
||||
} else if (battleProvider.currentStage.type == StageType.rest) {
|
||||
return _buildRestUI(context, battleProvider);
|
||||
return RestUI(battleProvider: battleProvider);
|
||||
}
|
||||
|
||||
return Stack(
|
||||
key: _stackKey,
|
||||
children: [
|
||||
// 1. Background (Black)
|
||||
Container(color: Colors.black87),
|
||||
|
||||
// 2. Battle Content (Top Bar + Characters)
|
||||
Column(
|
||||
children: [
|
||||
// Top Bar
|
||||
@@ -346,125 +339,135 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Stage ${battleProvider.stage}",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
Flexible(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
"Stage ${battleProvider.stage}",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"Turn ${battleProvider.turnCount}",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
Flexible(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
"Turn ${battleProvider.turnCount}",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Battle Area
|
||||
// Battle Area (Characters) - Expanded to fill available space
|
||||
Expanded(
|
||||
child: Padding(
|
||||
// padding: const EdgeInsets.symmetric(horizontal: 40.0),
|
||||
padding: const EdgeInsets.all(70.0),
|
||||
child: Column(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Stack(
|
||||
children: [
|
||||
// 적 영역 (우측 상단)
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: _buildCharacterStatus(
|
||||
battleProvider.enemy,
|
||||
isPlayer: false,
|
||||
isTurn: !battleProvider.isPlayerTurn,
|
||||
key: _enemyKey,
|
||||
),
|
||||
// Enemy (Top Right)
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: CharacterStatusCard(
|
||||
character: battleProvider.enemy,
|
||||
isPlayer: false,
|
||||
isTurn: !battleProvider.isPlayerTurn,
|
||||
key: _enemyKey,
|
||||
),
|
||||
),
|
||||
// 플레이어 영역 (좌측 하단)
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: _buildCharacterStatus(
|
||||
battleProvider.player,
|
||||
isPlayer: true,
|
||||
isTurn: battleProvider.isPlayerTurn,
|
||||
key: _playerKey,
|
||||
),
|
||||
// Player (Bottom Left)
|
||||
Positioned(
|
||||
bottom: 80, // Space for FABs
|
||||
left: 0,
|
||||
child: CharacterStatusCard(
|
||||
character: battleProvider.player,
|
||||
isPlayer: true,
|
||||
isTurn: battleProvider.isPlayerTurn,
|
||||
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)
|
||||
Container(
|
||||
color: Colors.black54,
|
||||
@@ -519,9 +522,11 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Floating Effects
|
||||
..._floatingDamageTexts.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) {
|
||||
List<String> stats = [];
|
||||
if (item.atkBonus > 0) stats.add("+${item.atkBonus} ATK");
|
||||
@@ -605,435 +567,21 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCharacterStatus(
|
||||
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(
|
||||
Widget _buildFloatingActionButton(
|
||||
BuildContext context,
|
||||
String text,
|
||||
String label,
|
||||
IconData icon,
|
||||
Color color,
|
||||
ActionType actionType,
|
||||
bool isEnabled,
|
||||
) {
|
||||
return ElevatedButton(
|
||||
return FloatingActionButton(
|
||||
heroTag: label,
|
||||
onPressed: isEnabled
|
||||
? () => _showRiskLevelSelection(context, actionType)
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15),
|
||||
backgroundColor: Colors.blueGrey,
|
||||
foregroundColor: Colors.white,
|
||||
textStyle: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
child: Text(text),
|
||||
backgroundColor: isEnabled ? color : Colors.grey,
|
||||
child: Icon(icon),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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:provider/provider.dart';
|
||||
import '../providers/battle_provider.dart';
|
||||
import '../game/data/player_table.dart';
|
||||
import 'main_wrapper.dart';
|
||||
import '../widgets/responsive_container.dart';
|
||||
|
||||
@@ -9,6 +10,15 @@ class CharacterSelectionScreen extends StatelessWidget {
|
||||
|
||||
@override
|
||||
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(
|
||||
backgroundColor: Colors.black, // Outer background
|
||||
body: Center(
|
||||
@@ -51,37 +61,43 @@ class CharacterSelectionScreen extends StatelessWidget {
|
||||
color: Colors.blue,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"Warrior",
|
||||
style: TextStyle(
|
||||
Text(
|
||||
warrior.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
"A balanced fighter with a sword and shield. Great for beginners.",
|
||||
Text(
|
||||
warrior.description,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey),
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
const Row(
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Text(
|
||||
"HP: 80",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
"HP: ${warrior.baseHp}",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"ATK: 5",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
"ATK: ${warrior.baseAtk}",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"DEF: 5",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
"DEF: ${warrior.baseDefense}",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -88,41 +88,73 @@ class InventoryScreen extends StatelessWidget {
|
||||
color: item != null
|
||||
? Colors.blueGrey[600]
|
||||
: Colors.grey[800],
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
child: Stack(
|
||||
children: [
|
||||
// Slot Name (Top Right)
|
||||
Positioned(
|
||||
right: 4,
|
||||
top: 4,
|
||||
child: Text(
|
||||
slot.name.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
fontSize: 8,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
color: Colors.white30,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Icon(
|
||||
ItemUtils.getIcon(slot),
|
||||
size: 24,
|
||||
color: item != null
|
||||
? ItemUtils.getColor(slot)
|
||||
: Colors.grey,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
item?.name ?? "Empty",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
),
|
||||
// Faded Icon (Top Left)
|
||||
Positioned(
|
||||
left: 4,
|
||||
top: 4,
|
||||
child: Opacity(
|
||||
opacity: item != null ? 0.2 : 0.1,
|
||||
child: Icon(
|
||||
ItemUtils.getIcon(slot),
|
||||
size: 40,
|
||||
color: item != null
|
||||
? Colors.white
|
||||
? ItemUtils.getColor(slot)
|
||||
: 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(
|
||||
color: Colors.blueGrey[700],
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
child: Stack(
|
||||
children: [
|
||||
Icon(
|
||||
ItemUtils.getIcon(item.slot),
|
||||
size: 32,
|
||||
color: ItemUtils.getColor(item.slot),
|
||||
// Faded Icon in Top-Left
|
||||
Positioned(
|
||||
left: 4,
|
||||
top: 4,
|
||||
child: Opacity(
|
||||
opacity: 0.2,
|
||||
child: Icon(
|
||||
ItemUtils.getIcon(item.slot),
|
||||
size: 40,
|
||||
color: ItemUtils.getColor(item.slot),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Text(
|
||||
item.name,
|
||||
textAlign: TextAlign.center,
|
||||
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)"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user