diff --git a/Build_God_Api/Build_God_Api/Controllers/BagController.cs b/Build_God_Api/Build_God_Api/Controllers/BagController.cs index a1eb287..6e2f8cf 100644 --- a/Build_God_Api/Build_God_Api/Controllers/BagController.cs +++ b/Build_God_Api/Build_God_Api/Controllers/BagController.cs @@ -107,6 +107,36 @@ namespace Build_God_Api.Controllers return await service.DestroyBagItem(characterBagId, dto.ItemType, dto.ItemDbId); } + [HttpGet("character/{characterId}/equipped")] + [Authorize] + public async Task>> GetEquippedSlots(int characterId) + { + return await service.GetEquippedSlots(characterId); + } + + [HttpGet("character/{characterId}/equipment-instances")] + [Authorize] + public async Task>> GetBagEquipmentInstances(int characterId) + { + return await service.GetBagEquipmentInstances(characterId); + } + + [HttpPost("character/{characterId}/equipped")] + [Authorize] + public async Task> EquipEquipmentInstance(int characterId, [FromBody] EquipEquipmentInstanceDto dto) + { + return await service.EquipEquipmentInstance(characterId, dto.EquipmentInstanceId); + } + + [HttpPost("character/{characterId}/equipped/unequip")] + [Authorize] + public async Task> UnequipEquipmentSlot(int characterId, [FromBody] UnequipSlotDto dto) + { + if (!Enum.IsDefined(typeof(EquipmentType), dto.Slot)) + return BadRequest("无效的装备槽"); + return await service.UnequipSlot(characterId, (EquipmentType)dto.Slot); + } + // ============ 枚举 ============ [HttpGet("rarities")] @@ -142,4 +172,15 @@ namespace Build_God_Api.Controllers public int ItemType { get; set; } public int ItemDbId { get; set; } } + + public class EquipEquipmentInstanceDto + { + public int EquipmentInstanceId { get; set; } + } + + public class UnequipSlotDto + { + /// EquipmentType:1 武器 2 防具 3 饰品 + public int Slot { get; set; } + } } diff --git a/Build_God_Api/Build_God_Api/DB/CharacterEquippedSlot.cs b/Build_God_Api/Build_God_Api/DB/CharacterEquippedSlot.cs new file mode 100644 index 0000000..5685fa1 --- /dev/null +++ b/Build_God_Api/Build_God_Api/DB/CharacterEquippedSlot.cs @@ -0,0 +1,18 @@ +using SqlSugar; + +namespace Build_God_Api.DB +{ + /// + /// 角色装备槽位(武器/防具/饰品各一条记录,未穿时 EquipmentInstanceId 为空) + /// + public class CharacterEquippedSlot : BaseEntity + { + public int CharacterId { get; set; } + + /// 武器、防具、饰品 + public EquipmentType Slot { get; set; } + + [SugarColumn(IsNullable = true)] + public int? EquipmentInstanceId { get; set; } + } +} diff --git a/Build_God_Api/Build_God_Api/DB/Equipment.cs b/Build_God_Api/Build_God_Api/DB/Equipment.cs index ab685bf..87e73c4 100644 --- a/Build_God_Api/Build_God_Api/DB/Equipment.cs +++ b/Build_God_Api/Build_God_Api/DB/Equipment.cs @@ -66,14 +66,14 @@ namespace Build_God_Api.DB } /// - /// 装备实例 - 角色拥有的具体装备 + /// 装备实例 - 角色拥有的具体装备(归属角色;是否在背上由 判定) /// public class EquipmentInstance : BaseEntity { /// - /// 角色背包关联ID + /// 所属角色 ID /// - public int CharacterBagId { get; set; } + public int CharacterId { get; set; } /// /// 装备模板ID diff --git a/Build_God_Api/Build_God_Api/Program.cs b/Build_God_Api/Build_God_Api/Program.cs index f010f00..7d9be02 100644 --- a/Build_God_Api/Build_God_Api/Program.cs +++ b/Build_God_Api/Build_God_Api/Program.cs @@ -90,6 +90,7 @@ namespace Build_God_Api sqlSugarClient.CodeFirst.InitTables(typeof(MissionReward)); sqlSugarClient.CodeFirst.InitTables(typeof(Bag)); sqlSugarClient.CodeFirst.InitTables(typeof(CharacterBag)); + sqlSugarClient.CodeFirst.InitTables(typeof(CharacterEquippedSlot)); sqlSugarClient.CodeFirst.InitTables(typeof(BagItem)); sqlSugarClient.CodeFirst.InitTables(typeof(MissionProgress)); sqlSugarClient.CodeFirst.InitTables(typeof(CharacterMissionProgress)); diff --git a/Build_God_Api/Build_God_Api/Scripts/equipment_character_id_postgresql.sql b/Build_God_Api/Build_God_Api/Scripts/equipment_character_id_postgresql.sql new file mode 100644 index 0000000..275490c --- /dev/null +++ b/Build_God_Api/Build_God_Api/Scripts/equipment_character_id_postgresql.sql @@ -0,0 +1,25 @@ +-- 装备实例与穿戴槽:CharacterBagId -> CharacterId(PostgreSQL) +-- 仅用于「已有库且表上仍存在 CharacterBagId 列」的升级;全新库由 SqlSugar CodeFirst 建表即可,不要执行本脚本。 +-- 若执行时报错列不存在,说明已迁过或为新库,请跳过。 + +-- EquipmentInstance +ALTER TABLE "EquipmentInstance" ADD COLUMN IF NOT EXISTS "CharacterId" integer; + +UPDATE "EquipmentInstance" ei +SET "CharacterId" = cb."CharacterId" +FROM "CharacterBag" cb +WHERE ei."CharacterBagId" IS NOT NULL + AND cb."Id" = ei."CharacterBagId"; + +ALTER TABLE "EquipmentInstance" DROP COLUMN IF EXISTS "CharacterBagId"; + +-- CharacterEquippedSlot +ALTER TABLE "CharacterEquippedSlot" ADD COLUMN IF NOT EXISTS "CharacterId" integer; + +UPDATE "CharacterEquippedSlot" s +SET "CharacterId" = cb."CharacterId" +FROM "CharacterBag" cb +WHERE s."CharacterBagId" IS NOT NULL + AND cb."Id" = s."CharacterBagId"; + +ALTER TABLE "CharacterEquippedSlot" DROP COLUMN IF EXISTS "CharacterBagId"; diff --git a/Build_God_Api/Build_God_Api/Services/BagService.cs b/Build_God_Api/Build_God_Api/Services/BagService.cs index 1699b1c..a3bf757 100644 --- a/Build_God_Api/Build_God_Api/Services/BagService.cs +++ b/Build_God_Api/Build_God_Api/Services/BagService.cs @@ -1,5 +1,6 @@ using Build_God_Api.DB; using Build_God_Api.Dto; +using Build_God_Api.Services.Game; using SqlSugar; namespace Build_God_Api.Services @@ -29,6 +30,13 @@ namespace Build_God_Api.Services // 物品销毁 Task DestroyBagItem(int characterBagId, int itemType, int itemDbId); + + // 装备穿戴(仅统计 EquipmentInstance 槽位,参与属性计算);入参为角色 ID + Task> GetEquippedSlots(int characterId); + Task EquipEquipmentInstance(int characterId, int equipmentInstanceId); + Task UnequipSlot(int characterId, EquipmentType slot); + /// 背包内未穿戴的装备实例(与槽位互斥) + Task> GetBagEquipmentInstances(int characterId); } public class BagItemDto @@ -58,6 +66,29 @@ namespace Build_God_Api.Services public int BagCapacity { get; set; } } + public class EquippedSlotDto + { + /// EquipmentType:1 武器 2 防具 3 饰品 + public int Slot { get; set; } + + public int? EquipmentInstanceId { get; set; } + } + + public class EquipmentInstanceBagDto + { + public int Id { get; set; } + public int EquipmentTemplateId { get; set; } + public string Name { get; set; } = string.Empty; + public int Type { get; set; } + public int Rarity { get; set; } + public string Attributes { get; set; } = "[]"; + public int EnhanceLevel { get; set; } + public int EnhanceBonusPercent { get; set; } + public int RequirdLevelId { get; set; } + public int? SetId { get; set; } + public bool IsBound { get; set; } + } + public class BagService(ISqlSugarClient db) : IBagService { private readonly ISqlSugarClient db = db; @@ -107,6 +138,8 @@ namespace Build_God_Api.Services // 删除相关的物品和关联 foreach (var cb in characterBags) { + await db.Deleteable().Where(x => x.CharacterId == cb.CharacterId).ExecuteCommandAsync(); + await db.Deleteable().Where(x => x.CharacterId == cb.CharacterId).ExecuteCommandAsync(); await db.Deleteable().Where(x => x.CharacterBagId == cb.Id).ExecuteCommandAsync(); } await db.Deleteable().Where(x => x.BagId == id).ExecuteCommandAsync(); @@ -159,9 +192,11 @@ namespace Build_God_Api.Services CharacterId = characterId, BagId = bagId }; - await db.Insertable(characterBag).ExecuteCommandAsync(); + await db.Insertable(characterBag).ExecuteReturnIdentityAsync(); } + await CharacterEquippedSlotQueries.EnsureSlotsAsync(db, characterId); + return true; } @@ -357,12 +392,17 @@ namespace Build_God_Api.Services { if (itemType == (int)BagItemType.Equipment) { + var characterBag = await db.Queryable() + .FirstAsync(x => x.Id == characterBagId) + ?? throw new Exception("角色背包不存在"); + var equipment = await db.Queryable() - .FirstAsync(x => x.Id == itemDbId && x.CharacterBagId == characterBagId); + .FirstAsync(x => x.Id == itemDbId && x.CharacterId == characterBag.CharacterId); if (equipment == null) throw new Exception("装备不存在"); + await CharacterEquippedSlotQueries.ClearSlotReferencesToInstanceAsync(db, characterBag.CharacterId, itemDbId); await db.Deleteable(equipment).ExecuteCommandAsync(); } else @@ -378,5 +418,94 @@ namespace Build_God_Api.Services return true; } + + public async Task> GetEquippedSlots(int characterId) + { + if (!await db.Queryable().AnyAsync(x => x.Id == characterId)) + throw new Exception("角色不存在"); + + await CharacterEquippedSlotQueries.EnsureSlotsAsync(db, characterId); + + return await db.Queryable() + .Where(x => x.CharacterId == characterId) + .OrderBy(x => x.Slot) + .Select(x => new EquippedSlotDto + { + Slot = (int)x.Slot, + EquipmentInstanceId = x.EquipmentInstanceId + }) + .ToListAsync(); + } + + public async Task> GetBagEquipmentInstances(int characterId) + { + if (!await db.Queryable().AnyAsync(x => x.Id == characterId)) + throw new Exception("角色不存在"); + + await CharacterEquippedSlotQueries.EnsureSlotsAsync(db, characterId); + var equippedIds = await CharacterEquippedSlotQueries.GetEquippedEquipmentInstanceIdsAsync(db, characterId); + + var list = await db.Queryable() + .Where(x => x.CharacterId == characterId && !equippedIds.Contains(x.Id)) + .OrderBy(x => x.Id) + .ToListAsync(); + + return list.ConvertAll(x => new EquipmentInstanceBagDto + { + Id = x.Id, + EquipmentTemplateId = x.EquipmentTemplateId, + Name = x.Name, + Type = (int)x.Type, + Rarity = (int)x.Rarity, + Attributes = x.Attributes, + EnhanceLevel = x.EnhanceLevel, + EnhanceBonusPercent = x.EnhanceBonusPercent, + RequirdLevelId = x.RequirdLevelId, + SetId = x.SetId, + IsBound = x.IsBound + }); + } + + public async Task EquipEquipmentInstance(int characterId, int equipmentInstanceId) + { + if (!await db.Queryable().AnyAsync(x => x.Id == characterId)) + throw new Exception("角色不存在"); + + var instance = await db.Queryable() + .FirstAsync(x => x.Id == equipmentInstanceId && x.CharacterId == characterId); + + if (instance == null) + throw new Exception("装备不存在或不属于该角色"); + + await CharacterEquippedSlotQueries.EnsureSlotsAsync(db, characterId); + + await db.Updateable() + .SetColumns(x => new CharacterEquippedSlot { EquipmentInstanceId = null, UpdatedOn = DateTime.UtcNow }) + .Where(x => x.CharacterId == characterId && x.EquipmentInstanceId == equipmentInstanceId) + .ExecuteCommandAsync(); + + var slotType = instance.Type; + await db.Updateable() + .SetColumns(x => new CharacterEquippedSlot { EquipmentInstanceId = equipmentInstanceId, UpdatedOn = DateTime.UtcNow }) + .Where(x => x.CharacterId == characterId && x.Slot == slotType) + .ExecuteCommandAsync(); + + return true; + } + + public async Task UnequipSlot(int characterId, EquipmentType slot) + { + if (!await db.Queryable().AnyAsync(x => x.Id == characterId)) + throw new Exception("角色不存在"); + + await CharacterEquippedSlotQueries.EnsureSlotsAsync(db, characterId); + + await db.Updateable() + .SetColumns(x => new CharacterEquippedSlot { EquipmentInstanceId = null, UpdatedOn = DateTime.UtcNow }) + .Where(x => x.CharacterId == characterId && x.Slot == slot) + .ExecuteCommandAsync(); + + return true; + } } } diff --git a/Build_God_Api/Build_God_Api/Services/CharacterService.cs b/Build_God_Api/Build_God_Api/Services/CharacterService.cs index 2a2011c..ed4bf04 100644 --- a/Build_God_Api/Build_God_Api/Services/CharacterService.cs +++ b/Build_God_Api/Build_God_Api/Services/CharacterService.cs @@ -385,6 +385,7 @@ namespace Build_God_Api.Services BagId = bag.Id }; await db.Insertable(characterBag).ExecuteCommandAsync(); + await CharacterEquippedSlotQueries.EnsureSlotsAsync(db, characterId); return true; } diff --git a/Build_God_Api/Build_God_Api/Services/Game/CharacterAttributeCalculateService.cs b/Build_God_Api/Build_God_Api/Services/Game/CharacterAttributeCalculateService.cs index 4359ace..abd49cd 100644 --- a/Build_God_Api/Build_God_Api/Services/Game/CharacterAttributeCalculateService.cs +++ b/Build_God_Api/Build_God_Api/Services/Game/CharacterAttributeCalculateService.cs @@ -60,10 +60,15 @@ namespace Build_God_Api.Services.Game return (attackBonus, defendBonus, hpBonus); } - private async Task CalculateEquipmentBonusAsync(int characterBagId) + private async Task CalculateEquipmentBonusAsync(int characterId) { + await CharacterEquippedSlotQueries.EnsureSlotsAsync(_context, characterId); + var equippedIds = await CharacterEquippedSlotQueries.GetEquippedEquipmentInstanceIdsAsync(_context, characterId); + if (equippedIds.Count == 0) + return new EquipmentAttributeTotals(); + var equipmentInstances = await _context.Queryable() - .Where(x => x.CharacterBagId == characterBagId) + .Where(x => x.CharacterId == characterId && equippedIds.Contains(x.Id)) .ToListAsync(); var totals = new EquipmentAttributeTotals(); @@ -97,13 +102,12 @@ namespace Build_God_Api.Services.Game decimal bonusHPFlat = 0; decimal bonusCritical = 0; decimal bonusCriticalDamage = 0; - var equipTotals = new EquipmentAttributeTotals(); + var equipTotals = await CalculateEquipmentBonusAsync(character.Id); var characterBag = await _context.Queryable().FirstAsync(x => x.CharacterId == character.Id); if (characterBag != null) { var (scrapAttack, scrapDefend, scrapHP) = await CalculateScrapBonusAsync(characterBag.Id); - equipTotals = await CalculateEquipmentBonusAsync(characterBag.Id); bonusAttackFlat = scrapAttack + equipTotals.AttackFixed; bonusDefendFlat = scrapDefend + equipTotals.DefendFixed; @@ -111,6 +115,14 @@ namespace Build_God_Api.Services.Game bonusCritical = equipTotals.CriticalRate; bonusCriticalDamage = equipTotals.CriticalDamage; } + else + { + bonusAttackFlat = equipTotals.AttackFixed; + bonusDefendFlat = equipTotals.DefendFixed; + bonusHPFlat = equipTotals.HealthFixed; + bonusCritical = equipTotals.CriticalRate; + bonusCriticalDamage = equipTotals.CriticalDamage; + } decimal maxHp = EquipmentAttributeBonus.ApplyPercentToBaseAndFlat(baseMaxHP, bonusHPFlat, equipTotals.HealthPercent); decimal attack = EquipmentAttributeBonus.ApplyPercentToBaseAndFlat(baseAttack, bonusAttackFlat, equipTotals.AttackPercent); diff --git a/Build_God_Api/Build_God_Api/Services/Game/CharacterEquippedSlotQueries.cs b/Build_God_Api/Build_God_Api/Services/Game/CharacterEquippedSlotQueries.cs new file mode 100644 index 0000000..206fdc0 --- /dev/null +++ b/Build_God_Api/Build_God_Api/Services/Game/CharacterEquippedSlotQueries.cs @@ -0,0 +1,51 @@ +using Build_God_Api.DB; +using SqlSugar; + +namespace Build_God_Api.Services.Game +{ + /// 装备槽位表读写(供属性计算与 BagService 共用,避免重复逻辑) + public static class CharacterEquippedSlotQueries + { + private static readonly EquipmentType[] AllSlots = + [EquipmentType.Weapon, EquipmentType.Armor, EquipmentType.Accessory]; + + public static async Task EnsureSlotsAsync(ISqlSugarClient db, int characterId) + { + var existing = await db.Queryable() + .Where(x => x.CharacterId == characterId) + .Select(x => x.Slot) + .ToListAsync(); + + var toInsert = new List(); + foreach (var slot in AllSlots) + { + if (existing.Contains(slot)) continue; + toInsert.Add(new CharacterEquippedSlot + { + CharacterId = characterId, + Slot = slot, + EquipmentInstanceId = null + }); + } + + if (toInsert.Count > 0) + await db.Insertable(toInsert).ExecuteCommandAsync(); + } + + public static async Task> GetEquippedEquipmentInstanceIdsAsync(ISqlSugarClient db, int characterId) + { + var ids = await db.Queryable() + .Where(x => x.CharacterId == characterId && x.EquipmentInstanceId != null) + .Select(x => x.EquipmentInstanceId!.Value) + .ToListAsync(); + + return ids; + } + + public static Task ClearSlotReferencesToInstanceAsync(ISqlSugarClient db, int characterId, int equipmentInstanceId) => + db.Updateable() + .SetColumns(x => new CharacterEquippedSlot { EquipmentInstanceId = null, UpdatedOn = DateTime.UtcNow }) + .Where(x => x.CharacterId == characterId && x.EquipmentInstanceId == equipmentInstanceId) + .ExecuteCommandAsync(); + } +} diff --git a/Build_God_Api/Build_God_Api/Services/ShopService.cs b/Build_God_Api/Build_God_Api/Services/ShopService.cs index c9baf0f..7f901c1 100644 --- a/Build_God_Api/Build_God_Api/Services/ShopService.cs +++ b/Build_God_Api/Build_God_Api/Services/ShopService.cs @@ -165,7 +165,7 @@ namespace Build_God_Api.Services // 装备特殊处理:创建装备实例 if (shopItem.ItemType == BagItemType.Equipment) { - await CreateEquipmentInstance(characterBag.Id, shopItem.ItemId); + await CreateEquipmentInstance(characterId, shopItem.ItemId); } else { @@ -326,7 +326,7 @@ namespace Build_God_Api.Services return result; } - private async Task CreateEquipmentInstance(int characterBagId, int equipmentTemplateId) + private async Task CreateEquipmentInstance(int characterId, int equipmentTemplateId) { var template = await db.Queryable() .FirstAsync(x => x.Id == equipmentTemplateId) @@ -338,7 +338,7 @@ namespace Build_God_Api.Services var instance = new EquipmentInstance { - CharacterBagId = characterBagId, + CharacterId = characterId, EquipmentTemplateId = equipmentTemplateId, Name = template.Name, Type = template.Type,