feat: add subweapon support, weapon type logic, and compact item card UI options
This commit is contained in:
@@ -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: [
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user