Browse Source

新增一个人物穿戴的装备表

master
hanqin 1 week ago
parent
commit
08b873cf0c
  1. 41
      Build_God_Api/Build_God_Api/Controllers/BagController.cs
  2. 18
      Build_God_Api/Build_God_Api/DB/CharacterEquippedSlot.cs
  3. 6
      Build_God_Api/Build_God_Api/DB/Equipment.cs
  4. 1
      Build_God_Api/Build_God_Api/Program.cs
  5. 25
      Build_God_Api/Build_God_Api/Scripts/equipment_character_id_postgresql.sql
  6. 133
      Build_God_Api/Build_God_Api/Services/BagService.cs
  7. 1
      Build_God_Api/Build_God_Api/Services/CharacterService.cs
  8. 20
      Build_God_Api/Build_God_Api/Services/Game/CharacterAttributeCalculateService.cs
  9. 51
      Build_God_Api/Build_God_Api/Services/Game/CharacterEquippedSlotQueries.cs
  10. 6
      Build_God_Api/Build_God_Api/Services/ShopService.cs

41
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<ActionResult<List<EquippedSlotDto>>> GetEquippedSlots(int characterId)
{
return await service.GetEquippedSlots(characterId);
}
[HttpGet("character/{characterId}/equipment-instances")]
[Authorize]
public async Task<ActionResult<List<EquipmentInstanceBagDto>>> GetBagEquipmentInstances(int characterId)
{
return await service.GetBagEquipmentInstances(characterId);
}
[HttpPost("character/{characterId}/equipped")]
[Authorize]
public async Task<ActionResult<bool>> EquipEquipmentInstance(int characterId, [FromBody] EquipEquipmentInstanceDto dto)
{
return await service.EquipEquipmentInstance(characterId, dto.EquipmentInstanceId);
}
[HttpPost("character/{characterId}/equipped/unequip")]
[Authorize]
public async Task<ActionResult<bool>> 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
{
/// <summary>EquipmentType:1 武器 2 防具 3 饰品</summary>
public int Slot { get; set; }
}
}

18
Build_God_Api/Build_God_Api/DB/CharacterEquippedSlot.cs

@ -0,0 +1,18 @@
using SqlSugar;
namespace Build_God_Api.DB
{
/// <summary>
/// 角色装备槽位(武器/防具/饰品各一条记录,未穿时 EquipmentInstanceId 为空)
/// </summary>
public class CharacterEquippedSlot : BaseEntity
{
public int CharacterId { get; set; }
/// <summary>武器、防具、饰品</summary>
public EquipmentType Slot { get; set; }
[SugarColumn(IsNullable = true)]
public int? EquipmentInstanceId { get; set; }
}
}

6
Build_God_Api/Build_God_Api/DB/Equipment.cs

@ -66,14 +66,14 @@ namespace Build_God_Api.DB
}
/// <summary>
/// 装备实例 - 角色拥有的具体装备
/// 装备实例 - 角色拥有的具体装备(归属角色;是否在背上由 <see cref="CharacterEquippedSlot"/> 判定)
/// </summary>
public class EquipmentInstance : BaseEntity
{
/// <summary>
/// 角色背包关联ID
/// 所属角色 ID
/// </summary>
public int CharacterBagId { get; set; }
public int CharacterId { get; set; }
/// <summary>
/// 装备模板ID

1
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));

25
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";

133
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<bool> DestroyBagItem(int characterBagId, int itemType, int itemDbId);
// 装备穿戴(仅统计 EquipmentInstance 槽位,参与属性计算);入参为角色 ID
Task<List<EquippedSlotDto>> GetEquippedSlots(int characterId);
Task<bool> EquipEquipmentInstance(int characterId, int equipmentInstanceId);
Task<bool> UnequipSlot(int characterId, EquipmentType slot);
/// <summary>背包内未穿戴的装备实例(与槽位互斥)</summary>
Task<List<EquipmentInstanceBagDto>> GetBagEquipmentInstances(int characterId);
}
public class BagItemDto
@ -58,6 +66,29 @@ namespace Build_God_Api.Services
public int BagCapacity { get; set; }
}
public class EquippedSlotDto
{
/// <summary>EquipmentType:1 武器 2 防具 3 饰品</summary>
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<CharacterEquippedSlot>().Where(x => x.CharacterId == cb.CharacterId).ExecuteCommandAsync();
await db.Deleteable<EquipmentInstance>().Where(x => x.CharacterId == cb.CharacterId).ExecuteCommandAsync();
await db.Deleteable<BagItem>().Where(x => x.CharacterBagId == cb.Id).ExecuteCommandAsync();
}
await db.Deleteable<CharacterBag>().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<CharacterBag>()
.FirstAsync(x => x.Id == characterBagId)
?? throw new Exception("角色背包不存在");
var equipment = await db.Queryable<EquipmentInstance>()
.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<List<EquippedSlotDto>> GetEquippedSlots(int characterId)
{
if (!await db.Queryable<Character>().AnyAsync(x => x.Id == characterId))
throw new Exception("角色不存在");
await CharacterEquippedSlotQueries.EnsureSlotsAsync(db, characterId);
return await db.Queryable<CharacterEquippedSlot>()
.Where(x => x.CharacterId == characterId)
.OrderBy(x => x.Slot)
.Select(x => new EquippedSlotDto
{
Slot = (int)x.Slot,
EquipmentInstanceId = x.EquipmentInstanceId
})
.ToListAsync();
}
public async Task<List<EquipmentInstanceBagDto>> GetBagEquipmentInstances(int characterId)
{
if (!await db.Queryable<Character>().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<EquipmentInstance>()
.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<bool> EquipEquipmentInstance(int characterId, int equipmentInstanceId)
{
if (!await db.Queryable<Character>().AnyAsync(x => x.Id == characterId))
throw new Exception("角色不存在");
var instance = await db.Queryable<EquipmentInstance>()
.FirstAsync(x => x.Id == equipmentInstanceId && x.CharacterId == characterId);
if (instance == null)
throw new Exception("装备不存在或不属于该角色");
await CharacterEquippedSlotQueries.EnsureSlotsAsync(db, characterId);
await db.Updateable<CharacterEquippedSlot>()
.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<CharacterEquippedSlot>()
.SetColumns(x => new CharacterEquippedSlot { EquipmentInstanceId = equipmentInstanceId, UpdatedOn = DateTime.UtcNow })
.Where(x => x.CharacterId == characterId && x.Slot == slotType)
.ExecuteCommandAsync();
return true;
}
public async Task<bool> UnequipSlot(int characterId, EquipmentType slot)
{
if (!await db.Queryable<Character>().AnyAsync(x => x.Id == characterId))
throw new Exception("角色不存在");
await CharacterEquippedSlotQueries.EnsureSlotsAsync(db, characterId);
await db.Updateable<CharacterEquippedSlot>()
.SetColumns(x => new CharacterEquippedSlot { EquipmentInstanceId = null, UpdatedOn = DateTime.UtcNow })
.Where(x => x.CharacterId == characterId && x.Slot == slot)
.ExecuteCommandAsync();
return true;
}
}
}

1
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;
}

20
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<EquipmentAttributeTotals> CalculateEquipmentBonusAsync(int characterBagId)
private async Task<EquipmentAttributeTotals> 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<EquipmentInstance>()
.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<CharacterBag>().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);

51
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
{
/// <summary>装备槽位表读写(供属性计算与 BagService 共用,避免重复逻辑)</summary>
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<CharacterEquippedSlot>()
.Where(x => x.CharacterId == characterId)
.Select(x => x.Slot)
.ToListAsync();
var toInsert = new List<CharacterEquippedSlot>();
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<List<int>> GetEquippedEquipmentInstanceIdsAsync(ISqlSugarClient db, int characterId)
{
var ids = await db.Queryable<CharacterEquippedSlot>()
.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<CharacterEquippedSlot>()
.SetColumns(x => new CharacterEquippedSlot { EquipmentInstanceId = null, UpdatedOn = DateTime.UtcNow })
.Where(x => x.CharacterId == characterId && x.EquipmentInstanceId == equipmentInstanceId)
.ExecuteCommandAsync();
}
}

6
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<EquipmentTemplate>()
.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,

Loading…
Cancel
Save