You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
357 lines
14 KiB
357 lines
14 KiB
using Build_God_Api.DB;
|
|
using Build_God_Api.Dto;
|
|
using Build_God_Api.Services.Game;
|
|
using SqlSugar;
|
|
using System.Security.Cryptography;
|
|
using static Build_God_Api.DB.BagItem;
|
|
|
|
namespace Build_God_Api.Services
|
|
{
|
|
public interface ICharacterService
|
|
{
|
|
public Task<Character?> GetCharacterByAccountId(int accountId);
|
|
public Task<List<Character>> GetCharactersByAccountId(int accountId);
|
|
public Task<List<CharacterDto>> GetCharacterListWithDetails(int accountId);
|
|
public Task<List<Character>> GetAllCharacters();
|
|
public Task<Character?> GetCharacterById(int characterId);
|
|
|
|
public Task<bool> ExistsByNameAsync(string name);
|
|
|
|
public Task<bool> RegisterCharacter(CharacterRegisterDto character);
|
|
|
|
public Task<bool> DeleteCharacter(int characterId);
|
|
|
|
/// <summary>
|
|
/// 选择角色(更新最后登录时间)
|
|
/// </summary>
|
|
public Task<bool> SelectCharacter(int characterId);
|
|
|
|
/// <summary>
|
|
/// 突破
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public Task<bool> Breakthrough(int characterId);
|
|
|
|
/// <summary>
|
|
/// 开始打坐
|
|
/// </summary>
|
|
public Task<bool> StartTraining(int characterId);
|
|
|
|
/// <summary>
|
|
/// 停止打坐并结算经验
|
|
/// </summary>
|
|
/// <returns>获得的经验值</returns>
|
|
public Task<decimal> StopTraining(int characterId);
|
|
}
|
|
|
|
public class CharacterService(ISqlSugarClient db,
|
|
ICurrentUserService currentUserService,
|
|
ICharacterAttributeCalculateService calculateService,
|
|
IBagService bagService,
|
|
IDailyMissionService dailyMissionService) : ICharacterService
|
|
{
|
|
private readonly ISqlSugarClient db = db;
|
|
private readonly ICurrentUserService currentUserService = currentUserService;
|
|
private readonly ICharacterAttributeCalculateService calculateService = calculateService;
|
|
private readonly IBagService bagService = bagService;
|
|
private readonly IDailyMissionService dailyMissionService = dailyMissionService;
|
|
|
|
public async Task<bool> Breakthrough(int characterId)
|
|
{
|
|
var character = await db.Queryable<Character>().FirstAsync(x => x.Id == characterId) ?? throw new Exception("角色不存在");
|
|
var currentLevel = await db.Queryable<Level>().FirstAsync(x => x.LevelId == character.LevelId);
|
|
var nextLevelId = currentLevel?.NextLevelId ?? 1;
|
|
var nextLevel = await db.Queryable<Level>().FirstAsync(x => x.LevelId == nextLevelId) ?? throw new Exception("你暂时无敌了,没有等级可以突破了");
|
|
|
|
if (character.CurrentExp < nextLevel.CurrentLevelMinExp)
|
|
{
|
|
throw new Exception($"距离可以突破你还差 {nextLevel.CurrentLevelMinExp - character.CurrentExp}");
|
|
}
|
|
|
|
if (currentLevel?.BaseBreakthroughRate <= 0)
|
|
{
|
|
throw new Exception("当前境界无法突破");
|
|
}
|
|
|
|
// 检查突破是否需要消耗丹药
|
|
int? requiredPillId = currentLevel?.RequiredPillId;
|
|
int requiredPillQuantity = currentLevel?.RequiredPillQuantity ?? 0;
|
|
|
|
// 如果有配置突破消耗的丹药
|
|
if (requiredPillId.HasValue && requiredPillId.Value > 0 && requiredPillQuantity > 0)
|
|
{
|
|
var characterBag = await db.Queryable<CharacterBag>().FirstAsync(x => x.CharacterId == characterId);
|
|
if (characterBag == null)
|
|
throw new Exception("角色没有背包");
|
|
|
|
// 检查背包是否有足够的丹药
|
|
int pillQuantity = await bagService.GetItemQuantity(characterBag.Id, (int)BagItemType.Pill, requiredPillId.Value);
|
|
if (pillQuantity < requiredPillQuantity)
|
|
{
|
|
var pill = await db.Queryable<Pill>().FirstAsync(x => x.Id == requiredPillId);
|
|
throw new Exception($"突破所需{pill?.Name ?? "丹药"}数量不足,需要{requiredPillQuantity}个,当前只有{pillQuantity}个");
|
|
}
|
|
|
|
// 扣除丹药(无论成功或失败都扣除)
|
|
await bagService.ReduceItemQuantity(characterBag.Id, (int)BagItemType.Pill, requiredPillId.Value, requiredPillQuantity);
|
|
}
|
|
|
|
if (character.BreakthroughRate >= 100)
|
|
{
|
|
character.LevelId = nextLevelId;
|
|
character.BreakthroughRate = nextLevel.BaseBreakthroughRate;
|
|
await calculateService.RecalculateMaxHPAsync(character);
|
|
return true;
|
|
}
|
|
|
|
decimal randomValue = GenerateSecureRandomDecimal(0, 100, 1);
|
|
bool isSuccess = randomValue < character.BreakthroughRate;
|
|
|
|
if (isSuccess)
|
|
{
|
|
character.LevelId = nextLevelId;
|
|
character.BreakthroughRate = nextLevel.BaseBreakthroughRate;
|
|
await calculateService.RecalculateMaxHPAsync(character);
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
character.BreakthroughRate += currentLevel!.FailIncrement;
|
|
if (character.BreakthroughRate > 100)
|
|
{
|
|
character.BreakthroughRate = 100;
|
|
}
|
|
}
|
|
|
|
await db.Updateable(character).ExecuteCommandAsync();
|
|
return false;
|
|
}
|
|
|
|
public async Task<bool> StartTraining(int characterId)
|
|
{
|
|
var character = await db.Queryable<Character>().FirstAsync(x => x.Id == characterId)
|
|
?? throw new Exception("角色不存在");
|
|
|
|
if (character.TrainingOn.HasValue)
|
|
{
|
|
throw new Exception("你已经在打坐了");
|
|
}
|
|
|
|
character.TrainingOn = DateTime.Now;
|
|
await db.Updateable(character).ExecuteCommandAsync();
|
|
return true;
|
|
}
|
|
|
|
public async Task<decimal> StopTraining(int characterId)
|
|
{
|
|
var character = await db.Queryable<Character>().FirstAsync(x => x.Id == characterId)
|
|
?? throw new Exception("角色不存在");
|
|
|
|
if (!character.TrainingOn.HasValue)
|
|
{
|
|
throw new Exception("你还没有开始打坐");
|
|
}
|
|
|
|
var duration = DateTime.Now - character.TrainingOn.Value;
|
|
var seconds = (decimal)duration.TotalSeconds;
|
|
|
|
// exp = 0.1 * seconds * level * level * (1 + buff)
|
|
var expGained = 0.01m * seconds * character.LevelId * character.LevelId * (1 + 0);
|
|
|
|
character.CurrentExp += Math.Round(expGained);
|
|
character.TrainingOn = null;
|
|
await db.Updateable(character).ExecuteCommandAsync();
|
|
|
|
return expGained;
|
|
}
|
|
|
|
private readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create();
|
|
|
|
/// <summary>
|
|
/// 生成安全的高精度随机小数(生产环境用)
|
|
/// </summary>
|
|
/// <param name="min">最小值</param>
|
|
/// <param name="max">最大值</param>
|
|
/// <param name="decimalPlaces">小数位数</param>
|
|
/// <returns>随机数</returns>
|
|
private decimal GenerateSecureRandomDecimal(decimal min, decimal max, int decimalPlaces)
|
|
{
|
|
byte[] randomBytes = new byte[4];
|
|
_rng.GetBytes(randomBytes);
|
|
int randomInt = BitConverter.ToInt32(randomBytes, 0);
|
|
|
|
decimal randomDecimal = (decimal)Math.Abs(randomInt) / int.MaxValue;
|
|
decimal result = min + (randomDecimal * (max - min));
|
|
|
|
return Math.Round(result, decimalPlaces);
|
|
}
|
|
|
|
public async Task<bool> DeleteCharacter(int characterId)
|
|
{
|
|
var item = await db.Queryable<Character>().FirstAsync(x => x.Id == characterId && x.AccountId == currentUserService.UserId) ?? throw new Exception("你没有权力这么做");
|
|
item.isLocked = true;
|
|
await db.Updateable(item).ExecuteCommandAsync();
|
|
return true;
|
|
}
|
|
|
|
public async Task<bool> SelectCharacter(int characterId)
|
|
{
|
|
var character = await db.Queryable<Character>().FirstAsync(x => x.Id == characterId && x.AccountId == currentUserService.UserId && x.isLocked == false)
|
|
?? throw new Exception("角色不存在");
|
|
character.LastLogin = DateTime.Now;
|
|
await db.Updateable(character).ExecuteCommandAsync();
|
|
|
|
await dailyMissionService.AssignDailyMissionsIfNeeded(characterId);
|
|
|
|
return true;
|
|
}
|
|
|
|
public async Task<bool> ExistsByNameAsync(string name)
|
|
{
|
|
return await db.Queryable<Character>().AnyAsync(x => x.Name == name);
|
|
}
|
|
|
|
public async Task<Character?> GetCharacterByAccountId(int accountId)
|
|
{
|
|
return await db.Queryable<Character>().FirstAsync(x => x.AccountId == accountId && x.isLocked == false);
|
|
}
|
|
|
|
public async Task<List<Character>> GetCharactersByAccountId(int accountId)
|
|
{
|
|
return await db.Queryable<Character>()
|
|
.Where(x => x.AccountId == accountId && x.isLocked == false)
|
|
.OrderBy(x => x.CreatedOn)
|
|
.ToListAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取角色列表(包含境界和灵根信息)
|
|
/// </summary>
|
|
public async Task<List<CharacterDto>> GetCharacterListWithDetails(int accountId)
|
|
{
|
|
var characters = await db.Queryable<Character>()
|
|
.Where(x => x.AccountId == accountId && x.isLocked == false)
|
|
.OrderBy(x => x.CreatedOn)
|
|
.ToListAsync();
|
|
|
|
var result = new List<CharacterDto>();
|
|
foreach (var c in characters)
|
|
{
|
|
var level = await db.Queryable<Level>().FirstAsync(x => x.LevelId == c.LevelId);
|
|
Level? nextLevel = null;
|
|
if (level?.NextLevelId.HasValue == true)
|
|
{
|
|
nextLevel = await db.Queryable<Level>().FirstAsync(x => x.LevelId == level.NextLevelId);
|
|
}
|
|
Profession? profession = null;
|
|
if (c.ProfessionId.HasValue && c.ProfessionId > 0)
|
|
{
|
|
profession = await db.Queryable<Profession>().FirstAsync(x => x.Id == c.ProfessionId);
|
|
}
|
|
|
|
bool canBreakthrough = false;
|
|
if (nextLevel != null && c.CurrentExp >= nextLevel.CurrentLevelMinExp)
|
|
{
|
|
canBreakthrough = true;
|
|
}
|
|
|
|
// 获取突破所需丹药信息
|
|
int? nextLevelRequiredPillId = null;
|
|
string? nextLevelRequiredPillName = null;
|
|
int nextLevelRequiredPillQuantity = 0;
|
|
if (level?.RequiredPillId > 0 && level.RequiredPillQuantity > 0)
|
|
{
|
|
nextLevelRequiredPillId = level.RequiredPillId;
|
|
nextLevelRequiredPillQuantity = level.RequiredPillQuantity;
|
|
var pill = await db.Queryable<Pill>().FirstAsync(x => x.Id == level.RequiredPillId);
|
|
nextLevelRequiredPillName = pill?.Name;
|
|
}
|
|
|
|
var attrs = await calculateService.CalculateAttributesAsync(c);
|
|
|
|
result.Add(new CharacterDto
|
|
{
|
|
Id = c.Id,
|
|
Name = c.Name,
|
|
LevelName = level?.Name ?? "未知",
|
|
LevelId = c.LevelId,
|
|
MaxHP = attrs.MaxHP,
|
|
CurrentHP = c.CurrentHP,
|
|
Attack = attrs.Attack,
|
|
Defend = attrs.Defend,
|
|
CriticalRate = attrs.CriticalRate,
|
|
ProfessionName = profession?.Name,
|
|
Money = c.Money,
|
|
CurrentExp = c.CurrentExp,
|
|
TrainingOn = c.TrainingOn,
|
|
NextLevelName = nextLevel?.Name,
|
|
NextLevelMinExp = nextLevel?.CurrentLevelMinExp,
|
|
BreakthroughRate = c.BreakthroughRate,
|
|
CanBreakthrough = canBreakthrough,
|
|
NextLevelRequiredPillId = nextLevelRequiredPillId,
|
|
NextLevelRequiredPillName = nextLevelRequiredPillName,
|
|
NextLevelRequiredPillQuantity = nextLevelRequiredPillQuantity,
|
|
TrainingExpRate = 0.01m,
|
|
LastLogin = c.LastLogin,
|
|
CreatedOn = c.CreatedOn
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public async Task<List<Character>> GetAllCharacters()
|
|
{
|
|
return await db.Queryable<Character>().Where(x => x.isLocked == false).ToListAsync();
|
|
}
|
|
|
|
public async Task<Character?> GetCharacterById(int characterId)
|
|
{
|
|
return await db.Queryable<Character>().FirstAsync(x => x.Id == characterId && x.isLocked == false);
|
|
}
|
|
|
|
public async Task<bool> RegisterCharacter(CharacterRegisterDto character)
|
|
{
|
|
var bo = await ExistsByNameAsync(character.Name);
|
|
if (bo)
|
|
{
|
|
throw new Exception("已经有道友叫这个名字了,请想另外一个吊炸天的名字吧");
|
|
}
|
|
|
|
// 限制每个账号最多3个角色
|
|
var existingCount = await db.Queryable<Character>()
|
|
.CountAsync(x => x.AccountId == currentUserService.UserId && x.isLocked == false);
|
|
if (existingCount >= 3)
|
|
{
|
|
throw new Exception("每个账号最多只能创建3个角色");
|
|
}
|
|
|
|
Character newOne = new()
|
|
{
|
|
Name = character.Name,
|
|
AccountId = currentUserService.UserId,
|
|
CurrentExp = 0,
|
|
LevelId = 1,
|
|
Money = 0,
|
|
CurrentHP = 100,
|
|
ProfessionId = character.ProfessionId,
|
|
SpiritFieldId = 0,
|
|
BreakthroughRate = 0,
|
|
LastLogin = DateTime.Now
|
|
};
|
|
|
|
await db.Insertable(newOne).ExecuteCommandAsync();
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 注册角色
|
|
/// </summary>
|
|
public class CharacterRegisterDto
|
|
{
|
|
public string Name { get; set; } = string.Empty;
|
|
public int ProfessionId { get; set; }
|
|
}
|
|
}
|
|
|