This commit is contained in:
2025-12-04 16:50:57 +09:00
parent 0a7c50e6c9
commit 37a634643e
23 changed files with 1062 additions and 663 deletions
@@ -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});
}
+59
View File
@@ -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)"),
),
],
),
);
}
}