feat: add subweapon support, weapon type logic, and compact item card UI options

This commit is contained in:
2026-04-28 01:39:39 +09:00
parent 0420e23939
commit 0e0748540e
46 changed files with 1795 additions and 577 deletions
+36 -35
View File
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../providers/settings_provider.dart';
import '../../game/enums.dart';
import '../../game/config.dart';
class BattleAnimationWidget extends StatefulWidget {
final Widget child;
@@ -24,11 +25,11 @@ class BattleAnimationWidgetState extends State<BattleAnimationWidget>
super.initState();
_scaleController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
duration: AnimationConfig.attackRiskyScale,
);
_translateController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1000),
duration: AnimationConfig.attackRiskyDash,
);
_scaleAnimation = Tween<double>(
@@ -58,80 +59,69 @@ class BattleAnimationWidgetState extends State<BattleAnimationWidget>
VoidCallback? onAnimationMiddle,
VoidCallback? onAnimationEnd,
}) async {
// onAnimationStart?.call(); // Start Phase
_resetControllers();
onAnimationStart?.call();
if (risk == RiskLevel.safe || risk == RiskLevel.normal) {
// Safe & Normal: Dash/Wobble without scale
final isSafe = risk == RiskLevel.safe;
final duration = isSafe ? 500 : 400;
final duration = AnimationConfig.getAttackDuration(risk);
final offsetFactor = isSafe ? 0.2 : 0.5;
final curve = isSafe
? AnimationConfig.attackSafeCurve
: AnimationConfig.attackNormalCurve;
_translateController.duration = Duration(milliseconds: duration);
_translateAnimation =
Tween<Offset>(
begin: Offset.zero,
end: targetOffset * offsetFactor,
).animate(
CurvedAnimation(
parent: _translateController,
curve: Curves.easeOutQuad,
),
);
_translateController.duration = duration;
_translateAnimation = Tween<Offset>(
begin: Offset.zero,
end: targetOffset * offsetFactor,
).animate(CurvedAnimation(parent: _translateController, curve: curve));
await _translateController.forward();
if (!mounted) return;
// onAnimationMiddle?.call(); // Middle Phase
onAnimationEnd?.call();
onImpact();
await _translateController.reverse();
} else {
onAnimationStart?.call(); // Start Phase
// Risky: Scale + Heavy Dash
final attackScale = context.read<SettingsProvider>().attackAnimScale;
_scaleAnimation = Tween<double>(begin: 1.0, end: attackScale).animate(
CurvedAnimation(parent: _scaleController, curve: Curves.easeOut),
);
_scaleController.duration = const Duration(milliseconds: 600);
_translateController.duration = const Duration(milliseconds: 500);
_scaleController.duration = AnimationConfig.attackRiskyScale;
_translateController.duration = AnimationConfig.attackRiskyDash;
// 1. Scale Up (Preparation)
await _scaleController.forward();
if (!mounted) return;
onAnimationMiddle?.call(); // Middle Phase
onAnimationMiddle?.call();
// 2. Dash to Target (Impact)
// Adjust offset to prevent complete overlap (stop slightly short) since both share the same layer stack
final adjustedOffset = targetOffset * 0.5;
_translateAnimation =
Tween<Offset>(begin: Offset.zero, end: adjustedOffset).animate(
CurvedAnimation(
parent: _translateController,
curve: Curves.easeInExpo, // Heavy impact curve
curve: AnimationConfig.attackRiskyDashCurve,
),
);
await _translateController.forward();
if (!mounted) return;
// onAnimationEnd?.call(); // End Phase (Moved before Impact)
// 3. Impact Callback (Shake)
onAnimationEnd?.call();
onImpact();
// 4. Return (Reset)
_scaleController.reverse();
await _translateController.reverse();
await Future.wait([
_scaleController.reverse(),
_translateController.reverse(),
]);
}
// onAnimationEnd removed from here
}
Future<void> animateDefense(VoidCallback onImpact) async {
// Defense: Wobble/Shake horizontally
_resetControllers();
_translateController.duration = const Duration(milliseconds: 800);
// Sequence: Left -> Right -> Center
@@ -165,6 +155,17 @@ class BattleAnimationWidgetState extends State<BattleAnimationWidget>
_translateController.reset();
}
void _resetControllers() {
if (_scaleController.isAnimating) {
_scaleController.stop();
}
if (_translateController.isAnimating) {
_translateController.stop();
}
_scaleController.reset();
_translateController.reset();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
@@ -84,7 +84,9 @@ class CharacterStatusCard extends StatelessWidget {
borderRadius: BorderRadius.circular(4),
),
child: Text(
"${effect.type.name.toUpperCase()} (${effect.duration})",
effect.stacks > 1
? "${effect.type.name.toUpperCase()} x${effect.stacks} (${effect.duration})"
: "${effect.type.name.toUpperCase()} (${effect.duration})",
style: const TextStyle(
color: ThemeConfig.effectText,
fontSize: ThemeConfig.statusEffectFontSize,
@@ -0,0 +1,173 @@
import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class SpriteEffect {
final Offset position;
final String assetPath;
final int frameCount;
final double tileWidth;
final double tileHeight;
final double scale;
ui.Image? image;
int currentFrame = 0;
bool isFinished = false;
SpriteEffect({
required this.position,
required this.assetPath,
required this.frameCount,
this.tileWidth = 100.0,
this.tileHeight = 100.0,
this.scale = 2.0,
});
}
class EffectSpriteWidget extends StatefulWidget {
const EffectSpriteWidget({super.key});
@override
EffectSpriteWidgetState createState() => EffectSpriteWidgetState();
}
class EffectSpriteWidgetState extends State<EffectSpriteWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
final List<SpriteEffect> _effects = [];
final Map<String, ui.Image> _imageCache = {};
@override
void initState() {
super.initState();
// Approximately 10 FPS (100ms per frame)
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 100),
);
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_updateFrames();
if (_effects.isNotEmpty) {
_controller.forward(from: 0);
}
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> playEffect({
required Offset position,
required String assetPath,
required int frameCount,
double tileWidth = 100.0,
double tileHeight = 100.0,
double scale = 2.0,
}) async {
final effect = SpriteEffect(
position: position,
assetPath: assetPath,
frameCount: frameCount,
tileWidth: tileWidth,
tileHeight: tileHeight,
scale: scale,
);
// Preload image if not cached
if (!_imageCache.containsKey(assetPath)) {
try {
final ByteData data = await rootBundle.load(assetPath);
final List<int> bytes = data.buffer.asUint8List();
final Completer<ui.Image> completer = Completer();
ui.decodeImageFromList(Uint8List.fromList(bytes), (ui.Image img) {
completer.complete(img);
});
_imageCache[assetPath] = await completer.future;
} catch (e) {
debugPrint('Failed to load effect image $assetPath: $e');
return;
}
}
effect.image = _imageCache[assetPath];
setState(() {
_effects.add(effect);
if (!_controller.isAnimating) {
_controller.forward(from: 0);
}
});
}
void _updateFrames() {
if (_effects.isEmpty) return;
setState(() {
for (var i = _effects.length - 1; i >= 0; i--) {
final effect = _effects[i];
effect.currentFrame++;
if (effect.currentFrame >= effect.frameCount) {
effect.isFinished = true;
_effects.removeAt(i);
}
}
});
}
@override
Widget build(BuildContext context) {
if (_effects.isEmpty) return const SizedBox.shrink();
return IgnorePointer(
child: CustomPaint(
size: Size.infinite,
painter: MultiSpriteEffectPainter(effects: _effects),
),
);
}
}
class MultiSpriteEffectPainter extends CustomPainter {
final List<SpriteEffect> effects;
MultiSpriteEffectPainter({required this.effects});
@override
void paint(Canvas canvas, Size size) {
for (final effect in effects) {
if (effect.image == null) continue;
final double srcX = effect.currentFrame * effect.tileWidth;
final double srcY = 0.0;
final Rect src = Rect.fromLTWH(srcX, srcY, effect.tileWidth, effect.tileHeight);
final double drawWidth = effect.tileWidth * effect.scale;
final double drawHeight = effect.tileHeight * effect.scale;
// Center the effect on the position
final Rect dst = Rect.fromLTWH(
effect.position.dx - drawWidth / 2,
effect.position.dy - drawHeight / 2,
drawWidth,
drawHeight,
);
canvas.drawImageRect(
effect.image!,
src,
dst,
Paint()..filterQuality = FilterQuality.none,
);
}
}
@override
bool shouldRepaint(covariant MultiSpriteEffectPainter oldDelegate) {
return true; // Repaint constantly while animating
}
}
+3 -1
View File
@@ -32,6 +32,8 @@ class ExplosionWidgetState extends State<ExplosionWidget>
final List<Particle> _particles = [];
final Random _random = Random();
bool get isAnimating => _controller.isAnimating || _particles.isNotEmpty;
@override
void initState() {
super.initState();
@@ -127,7 +129,7 @@ class ExplosionPainter extends CustomPainter {
void paint(Canvas canvas, Size size) {
for (final p in particles) {
final paint = Paint()
..color = p.color.withOpacity(p.life.clamp(0.0, 1.0))
..color = p.color.withValues(alpha: p.life.clamp(0.0, 1.0))
..style = PaintingStyle.fill;
canvas.drawCircle(p.position, p.size, paint);
@@ -8,11 +8,11 @@ class FloatingDamageText extends StatefulWidget {
final VoidCallback onRemove;
const FloatingDamageText({
Key? key,
super.key,
required this.damage,
required this.color,
required this.onRemove,
}) : super(key: key);
});
@override
FloatingDamageTextState createState() => FloatingDamageTextState();
@@ -111,12 +111,12 @@ class FloatingEffect extends StatefulWidget {
final VoidCallback onRemove;
const FloatingEffect({
Key? key,
super.key,
required this.icon,
required this.color,
required this.size,
required this.onRemove,
}) : super(key: key);
});
@override
FloatingEffectState createState() => FloatingEffectState();
@@ -193,11 +193,11 @@ class FloatingFeedbackText extends StatefulWidget {
final VoidCallback onRemove;
const FloatingFeedbackText({
Key? key,
super.key,
required this.feedback,
required this.color,
required this.onRemove,
}) : super(key: key);
});
@override
FloatingFeedbackTextState createState() => FloatingFeedbackTextState();
+63 -13
View File
@@ -9,6 +9,7 @@ class ItemCardWidget extends StatelessWidget {
final VoidCallback? onTap;
final bool showPrice;
final bool canBuy;
final bool compact;
const ItemCardWidget({
super.key,
@@ -16,6 +17,7 @@ class ItemCardWidget extends StatelessWidget {
this.onTap,
this.showPrice = false,
this.canBuy = true,
this.compact = false,
});
@override
@@ -38,12 +40,12 @@ class ItemCardWidget extends StatelessWidget {
children: [
// Background Watermark/Silhouette Icon (Top-Left)
Positioned(
left: 8,
top: 8,
left: compact ? 4 : 8,
top: compact ? 4 : 8,
child: Image.asset(
ItemUtils.getIconPath(item.slot),
width: 32,
height: 32,
width: compact ? 24 : 32,
height: compact ? 24 : 32,
fit: BoxFit.contain,
color: Colors.black12, // Shadow silhouette
),
@@ -51,12 +53,12 @@ class ItemCardWidget extends StatelessWidget {
// Main Content (Centered)
Center(
child: Padding(
padding: const EdgeInsets.all(4.0),
padding: EdgeInsets.all(compact ? 2.0 : 4.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 12),
if (!compact) const SizedBox(height: 12),
Text(
item.name,
maxLines: 1,
@@ -65,15 +67,23 @@ class ItemCardWidget extends StatelessWidget {
style: TextStyle(
fontWeight: FontWeight.bold,
color: ItemUtils.getRarityColor(item.rarity),
fontSize: 12,
fontSize: compact ? 10 : 12,
),
),
const SizedBox(height: 4),
if (item.weaponType != null)
Text(
item.weaponType == WeaponType.oneHanded ? "1-Handed" : "2-Handed",
style: const TextStyle(fontSize: 9, color: ThemeConfig.textColorGrey),
),
SizedBox(height: compact ? 1 : 4),
// Show Item Stats
FittedBox(
fit: BoxFit.scaleDown,
child: _buildItemStatText(item),
),
if (compact)
_buildCompactItemStatText(item)
else
FittedBox(
fit: BoxFit.scaleDown,
child: _buildItemStatText(item),
),
if (showPrice) ...[
const SizedBox(height: 4),
Text(
@@ -98,12 +108,52 @@ class ItemCardWidget extends StatelessWidget {
);
}
Widget _buildCompactItemStatText(Item item) {
final stats = <String>[];
if (item.atkBonus != 0) {
stats.add("${_sign(item.atkBonus)}${item.atkBonus}A");
}
if (item.hpBonus != 0) {
stats.add("${_sign(item.hpBonus)}${item.hpBonus}H");
}
if (item.armorBonus != 0) {
stats.add("${_sign(item.armorBonus)}${item.armorBonus}D");
}
if (item.luck != 0) {
stats.add("${_sign(item.luck)}${item.luck}L");
}
final effect = item.effects.isNotEmpty
? item.effects.first.type.name.toUpperCase()
: null;
final text = [
if (stats.isNotEmpty) stats.join(" "),
if (effect != null) effect,
].join(" ");
if (text.isEmpty) return const SizedBox.shrink();
return Text(
text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: ThemeConfig.fontSizeTiny,
color: ThemeConfig.statAtkColor,
),
);
}
String _sign(int value) => value > 0 ? "+" : "";
Widget _buildItemStatText(Item item) {
List<String> stats = [];
// Helper to format stat string
String formatStat(int value, String label) {
String sign = value > 0 ? "+" : ""; // Negative values already have '-'
String sign = _sign(value); // Negative values already have '-'
return "$sign$value $label";
}
@@ -33,6 +33,14 @@ class EquippedItemsWidget extends StatelessWidget {
.where((slot) => slot != EquipmentSlot.consumable)
.map((slot) {
final item = player.equipment[slot];
bool isShieldLocked = false;
if (slot == EquipmentSlot.shield) {
final mainWeapon = player.equipment[EquipmentSlot.weapon];
if (mainWeapon != null && mainWeapon.weaponType == WeaponType.twoHanded) {
isShieldLocked = true;
}
}
return Expanded(
child: InkWell(
onTap: item != null
@@ -45,7 +53,7 @@ class EquippedItemsWidget extends StatelessWidget {
child: Card(
color: item != null
? ThemeConfig.equipmentCardBg
: ThemeConfig.emptySlotBg,
: (isShieldLocked ? Colors.black26 : ThemeConfig.emptySlotBg),
shape:
item != null && item.rarity != ItemRarity.magic
? RoundedRectangleBorder(
@@ -65,7 +73,7 @@ class EquippedItemsWidget extends StatelessWidget {
right: 4,
top: 4,
child: Text(
slot.name.toUpperCase(),
ItemUtils.getSlotLabel(slot),
style: const TextStyle(
fontSize: ThemeConfig.fontSizeTiny,
fontWeight: ThemeConfig.fontWeightBold,
@@ -78,7 +86,7 @@ class EquippedItemsWidget extends StatelessWidget {
left: 4,
top: 4,
child: Opacity(
opacity: item != null ? 0.5 : 0.2,
opacity: item != null ? 0.5 : (isShieldLocked ? 0.1 : 0.2),
child: Image.asset(
ItemUtils.getIconPath(slot),
width: 40,
@@ -100,7 +108,7 @@ class EquippedItemsWidget extends StatelessWidget {
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
item?.name ?? AppStrings.emptySlot,
item?.name ?? (isShieldLocked ? "Locked (2H)" : AppStrings.emptySlot),
textAlign: TextAlign.center,
style: TextStyle(
fontSize:
@@ -111,7 +119,7 @@ class EquippedItemsWidget extends StatelessWidget {
? ItemUtils.getRarityColor(
item.rarity,
)
: ThemeConfig.textColorGrey,
: (isShieldLocked ? Colors.red.withOpacity(0.5) : ThemeConfig.textColorGrey),
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
@@ -181,6 +189,16 @@ class EquippedItemsWidget extends StatelessWidget {
_buildStatChangeRow("Current HP", currentHp, newHp),
_buildStatChangeRow(AppStrings.atk, currentAtk, newAtk),
_buildStatChangeRow(AppStrings.def, currentDef, newDef),
if (itemToUnequip.effects.isNotEmpty) ...[
const Divider(color: ThemeConfig.textColorGrey, height: 16),
...itemToUnequip.effects.map((e) => Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("- ", style: TextStyle(color: ThemeConfig.textColorGrey, fontSize: 12)),
Expanded(child: Text(e.description, style: const TextStyle(color: ThemeConfig.textColorGrey, fontSize: 12))),
],
)),
],
],
),
actions: [
+201 -72
View File
@@ -4,75 +4,123 @@ import '../../providers.dart';
import '../../game/models.dart';
import '../../game/enums.dart';
import '../../game/config.dart';
import '../../utils.dart';
import '../common/item_card_widget.dart';
enum InventoryGridMode { normal, shop, equipmentSwap }
class InventoryGridWidget extends StatelessWidget {
const InventoryGridWidget({super.key});
final InventoryGridMode mode;
final bool equipmentOnly;
final bool showHeader;
final int crossAxisCount;
final EdgeInsetsGeometry gridPadding;
final double childAspectRatio;
const InventoryGridWidget({
super.key,
this.mode = InventoryGridMode.normal,
this.equipmentOnly = false,
this.showHeader = true,
this.crossAxisCount = 4,
this.gridPadding = const EdgeInsets.all(16.0),
this.childAspectRatio = 1.0,
});
@override
Widget build(BuildContext context) {
return Consumer<BattleProvider>(
builder: (context, battleProvider, child) {
final player = battleProvider.player;
final items = equipmentOnly
? player.inventory
.where((item) => item.slot != EquipmentSlot.consumable)
.toList()
: player.inventory;
final itemCount = mode == InventoryGridMode.equipmentSwap
? items.length
: player.maxInventorySize;
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
"${AppStrings.bag} (${player.inventory.length}/${player.maxInventorySize})",
style: const TextStyle(
fontSize: ThemeConfig.fontSizeHeader,
fontWeight: ThemeConfig.fontWeightBold,
if (showHeader)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
"${equipmentOnly ? AppStrings.equipment : AppStrings.bag} (${items.length}/${player.maxInventorySize})",
style: const TextStyle(
fontSize: ThemeConfig.fontSizeHeader,
fontWeight: ThemeConfig.fontWeightBold,
),
),
),
),
),
Expanded(
child: GridView.builder(
padding: const EdgeInsets.all(16.0),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 8.0,
mainAxisSpacing: 8.0,
),
itemCount: player.maxInventorySize,
itemBuilder: (context, index) {
if (index < player.inventory.length) {
final item = player.inventory[index];
return InkWell(
onTap: () {
_showItemActionDialog(context, battleProvider, item);
child: itemCount == 0
? const Center(
child: Text(
"No equipment",
style: TextStyle(color: ThemeConfig.textColorGrey),
),
)
: GridView.builder(
padding: gridPadding,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
crossAxisSpacing: 8.0,
mainAxisSpacing: 8.0,
childAspectRatio: childAspectRatio,
),
itemCount: itemCount,
itemBuilder: (context, index) {
if (index < items.length) {
final item = items[index];
return InkWell(
onTap: () {
if (mode == InventoryGridMode.equipmentSwap) {
_showEquipSlotDialog(
context,
battleProvider,
item,
);
} else {
_showItemActionDialog(
context,
battleProvider,
item,
);
}
},
child: ItemCardWidget(
item: item,
showPrice: false,
canBuy: false,
compact: mode == InventoryGridMode.equipmentSwap,
),
);
} else {
return Container(
decoration: BoxDecoration(
border: Border.all(
color: ThemeConfig.textColorGrey,
),
color: ThemeConfig.emptySlotBg,
),
child: const Center(
child: Icon(
Icons.add_box,
color: ThemeConfig.textColorGrey,
),
),
);
}
},
child: ItemCardWidget(
item: item,
// Inventory items usually don't show price unless in sell mode,
// but logic here implies standard view.
// If needed, we can toggle showPrice based on context.
showPrice: false,
canBuy: false,
),
);
} else {
return Container(
decoration: BoxDecoration(
border: Border.all(color: ThemeConfig.textColorGrey),
color: ThemeConfig.emptySlotBg,
),
child: const Center(
child: Icon(
Icons.add_box,
color: ThemeConfig.textColorGrey,
),
),
);
}
},
),
),
),
],
);
@@ -85,7 +133,11 @@ class InventoryGridWidget extends StatelessWidget {
BattleProvider provider,
Item item,
) {
bool isShop = provider.currentStage.type == StageType.shop;
final isShop =
mode == InventoryGridMode.shop ||
(mode == InventoryGridMode.normal &&
provider.currentStage.type == StageType.shop);
final isEquipmentSwap = mode == InventoryGridMode.equipmentSwap;
int sellPrice = (item.price * GameConfig.sellPriceMultiplier).floor();
showDialog(
@@ -114,7 +166,7 @@ class InventoryGridWidget extends StatelessWidget {
SimpleDialogOption(
onPressed: () {
Navigator.pop(ctx);
_showEquipConfirmationDialog(context, provider, item);
_showEquipSlotDialog(context, provider, item);
},
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
@@ -147,27 +199,84 @@ class InventoryGridWidget extends StatelessWidget {
),
),
),
SimpleDialogOption(
onPressed: () {
Navigator.pop(ctx);
_showDiscardConfirmationDialog(context, provider, item);
},
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
Icon(Icons.delete, color: ThemeConfig.btnActionActive),
SizedBox(width: 10),
Text(AppStrings.discard),
],
if (!isEquipmentSwap)
SimpleDialogOption(
onPressed: () {
Navigator.pop(ctx);
_showDiscardConfirmationDialog(context, provider, item);
},
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
Icon(Icons.delete, color: ThemeConfig.btnActionActive),
SizedBox(width: 10),
Text(AppStrings.discard),
],
),
),
),
),
],
),
);
}
void _showEquipSlotDialog(
BuildContext context,
BattleProvider provider,
Item newItem,
) {
final compatibleSlots = newItem.compatibleEquipSlots;
if (compatibleSlots.isEmpty) return;
if (compatibleSlots.length == 1) {
_showEquipConfirmationDialog(
context,
provider,
newItem,
compatibleSlots.first,
);
return;
}
showDialog(
context: context,
builder: (ctx) => SimpleDialog(
title: Text("Equip ${newItem.name}"),
children: compatibleSlots
.map(
(slot) => SimpleDialogOption(
onPressed: () {
Navigator.pop(ctx);
_showEquipConfirmationDialog(
context,
provider,
newItem,
slot,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
Image.asset(
ItemUtils.getIconPath(slot),
width: ThemeConfig.itemIconSizeMedium,
height: ThemeConfig.itemIconSizeMedium,
color: ThemeConfig.textColorWhite,
),
const SizedBox(width: 10),
Text(ItemUtils.getSlotName(slot)),
],
),
),
),
)
.toList(),
),
);
}
void _showSellConfirmationDialog(
BuildContext context,
BattleProvider provider,
@@ -237,9 +346,10 @@ class InventoryGridWidget extends StatelessWidget {
BuildContext context,
BattleProvider provider,
Item newItem,
EquipmentSlot targetSlot,
) {
final player = provider.player;
final oldItem = player.equipment[newItem.slot];
final oldItem = player.equipment[targetSlot];
final currentMaxHp = player.totalMaxHp;
final currentAtk = player.totalAtk;
@@ -263,7 +373,7 @@ class InventoryGridWidget extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Text(
"${AppStrings.equip} ${newItem.name}?",
"${AppStrings.equip} ${newItem.name} as ${ItemUtils.getSlotName(targetSlot)}?",
style: const TextStyle(fontWeight: ThemeConfig.fontWeightBold),
),
if (oldItem != null)
@@ -289,6 +399,25 @@ class InventoryGridWidget extends StatelessWidget {
player.totalDodge,
player.totalDodge - (oldItem?.dodge ?? 0) + newItem.dodge,
),
if (newItem.effects.isNotEmpty || (oldItem != null && oldItem.effects.isNotEmpty)) ...[
const Divider(color: ThemeConfig.textColorGrey, height: 16),
if (oldItem != null && oldItem.effects.isNotEmpty)
...oldItem.effects.map((e) => Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("- ", style: TextStyle(color: ThemeConfig.textColorGrey, fontSize: 12)),
Expanded(child: Text(e.description, style: const TextStyle(color: ThemeConfig.textColorGrey, fontSize: 12))),
],
)),
if (newItem.effects.isNotEmpty)
...newItem.effects.map((e) => Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("+ ", style: TextStyle(color: ThemeConfig.rarityLegendary, fontSize: 12, fontWeight: FontWeight.bold)),
Expanded(child: Text(e.description, style: const TextStyle(color: ThemeConfig.rarityLegendary, fontSize: 12, fontWeight: FontWeight.bold))),
],
)),
],
],
),
actions: [
@@ -298,7 +427,7 @@ class InventoryGridWidget extends StatelessWidget {
),
ElevatedButton(
onPressed: () {
provider.equipItem(newItem);
provider.equipItem(newItem, targetSlot: targetSlot);
Navigator.pop(ctx);
},
child: const Text(AppStrings.confirm),
+2 -2
View File
@@ -6,11 +6,11 @@ class ResponsiveContainer extends StatelessWidget {
final double maxHeight;
const ResponsiveContainer({
Key? key,
super.key,
required this.child,
this.maxWidth = 600.0,
this.maxHeight = 1000.0,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {
+4 -1
View File
@@ -134,7 +134,10 @@ class ShopUI extends StatelessWidget {
const Divider(color: ThemeConfig.textColorGrey),
// Player Inventory (Bottom Half)
const Expanded(flex: 5, child: InventoryGridWidget()),
const Expanded(
flex: 5,
child: InventoryGridWidget(mode: InventoryGridMode.shop),
),
const SizedBox(height: 8),