Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public static IServiceCollection AddDungeonServices(this IServiceCollection serv
services.AddScoped<IBossDungeonService, BossDungeonService>();
services.AddScoped<IAccountDungeonTicketRepository, AccountDungeonTicketRepository>();
services.AddScoped<IGoldDungeonRunRepository, GoldDungeonRunRepository>();
services.AddScoped<IPlayerDungeonProgressRepository, PlayerDungeonProgressRepository>();
services.AddScoped<IGoldDungeonRunService, GoldDungeonRunService>();
services.AddScoped<IGoldDungeonClaimService, GoldDungeonClaimService>();
services.AddScoped<IDungeonTicketService, DungeonTicketService>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ public record BossDungeonResponse(
long EarnedMithril,
DroppedWeaponInfo? DroppedWeapon,
long EarnedXp,
List<LevelUpResult> LevelUps
List<LevelUpResult> LevelUps,
long HighestClearedStage
);
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public record GoldDungeonClaimResponse(
Guid RunId,
long EarnedGold,
int EarnedMithril,
long HighScore,
ChangesDto Changes,
PlayerDataResponse Player
);
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ namespace Fantasy.Server.Domain.Dungeon.Dto.Response;
public record WeaponDungeonResponse(
bool Cleared,
List<DroppedWeaponInfo> DroppedWeapons,
long DroppedScrolls
long DroppedScrolls,
long HighestClearedStage
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Fantasy.Server.Domain.Dungeon.Entity;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using PlayerEntity = Fantasy.Server.Domain.Player.Entity.Player;

namespace Fantasy.Server.Domain.Dungeon.Entity.Config;

public class PlayerDungeonProgressConfig : IEntityTypeConfiguration<PlayerDungeonProgress>
{
public void Configure(EntityTypeBuilder<PlayerDungeonProgress> builder)
{
builder.ToTable("player_dungeon_progress", "dungeon");

builder.HasKey(p => p.Id);

builder.Property(p => p.Id)
.ValueGeneratedOnAdd();

builder.Property(p => p.PlayerId)
.IsRequired();

builder.Property(p => p.DungeonType)
.HasConversion<string>()
.IsRequired();

builder.Property(p => p.HighestClearedStage)
.IsRequired()
.HasDefaultValue(1L);

builder.Property(p => p.HighScore)
.IsRequired()
.HasDefaultValue(0L);

builder.Property(p => p.LastClearedAt)
.IsRequired(false);

builder.HasIndex(p => new { p.PlayerId, p.DungeonType })
.IsUnique();

builder.HasOne<PlayerEntity>()
.WithMany()
.HasForeignKey(p => p.PlayerId)
.OnDelete(DeleteBehavior.Cascade);

builder.Property<uint>("xmin")
.HasColumnName("xmin")
.HasColumnType("xid")
.ValueGeneratedOnAddOrUpdate()
.IsConcurrencyToken();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Fantasy.Server.Domain.Dungeon.Enum;

namespace Fantasy.Server.Domain.Dungeon.Entity;

public class PlayerDungeonProgress
{
public long Id { get; private set; }
public long PlayerId { get; private set; }
public DungeonType DungeonType { get; private set; }
public long HighestClearedStage { get; private set; }
public long HighScore { get; private set; }
public DateTime? LastClearedAt { get; private set; }

public static PlayerDungeonProgress Create(long playerId, DungeonType dungeonType) => new()
{
PlayerId = playerId,
DungeonType = dungeonType,
HighestClearedStage = 1,
HighScore = 0,
LastClearedAt = null
};

public void ClearStage(long stage)
{
if (stage > HighestClearedStage) HighestClearedStage = stage;
LastClearedAt = DateTime.UtcNow;
}

public void UpdateHighScore(long score)
{
if (score > HighScore) HighScore = score;
LastClearedAt = DateTime.UtcNow;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Fantasy.Server.Domain.Dungeon.Enum;

public enum DungeonType
{
Basic,
Gold,
Weapon,
Boss
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ public AccountDungeonTicketRepository(AppDbContext dbContext)

public async Task<AccountDungeonTicket?> FindByAccountIdAsync(long accountId) =>
await _dbContext.AccountDungeonTickets
.AsNoTracking()
.FirstOrDefaultAsync(t => t.AccountId == accountId);

public async Task SaveAsync(AccountDungeonTicket ticket)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ public GoldDungeonRunRepository(AppDbContext dbContext)

public async Task<GoldDungeonRun?> FindByIdAsync(Guid runId) =>
await _dbContext.GoldDungeonRuns
.AsNoTracking()
.FirstOrDefaultAsync(r => r.Id == runId);

public async Task SaveAsync(GoldDungeonRun run)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Fantasy.Server.Domain.Dungeon.Entity;
using Fantasy.Server.Domain.Dungeon.Enum;

namespace Fantasy.Server.Domain.Dungeon.Repository.Interface;

public interface IPlayerDungeonProgressRepository
{
Task<PlayerDungeonProgress?> FindByPlayerIdAndDungeonTypeAsync(long playerId, DungeonType dungeonType);
Task SaveAsync(PlayerDungeonProgress progress);
Task UpdateAsync(PlayerDungeonProgress progress);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Fantasy.Server.Domain.Dungeon.Entity;
using Fantasy.Server.Domain.Dungeon.Enum;
using Fantasy.Server.Domain.Dungeon.Repository.Interface;
using Fantasy.Server.Global.Infrastructure;
using Microsoft.EntityFrameworkCore;

namespace Fantasy.Server.Domain.Dungeon.Repository;

public class PlayerDungeonProgressRepository : IPlayerDungeonProgressRepository
{
private readonly AppDbContext _db;

public PlayerDungeonProgressRepository(AppDbContext db) => _db = db;

public async Task<PlayerDungeonProgress?> FindByPlayerIdAndDungeonTypeAsync(long playerId, DungeonType dungeonType)
=> await _db.PlayerDungeonProgresses
.FirstOrDefaultAsync(p => p.PlayerId == playerId && p.DungeonType == dungeonType);
Comment thread
Sean-mn marked this conversation as resolved.

public async Task SaveAsync(PlayerDungeonProgress progress)
{
await _db.PlayerDungeonProgresses.AddAsync(progress);
await _db.SaveChangesAsync();
}

public async Task UpdateAsync(PlayerDungeonProgress progress)
{
_db.PlayerDungeonProgresses.Update(progress);
await _db.SaveChangesAsync();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using Fantasy.Server.Domain.Dungeon.Dto.Response;
using Fantasy.Server.Domain.Dungeon.Entity;
using Fantasy.Server.Domain.Dungeon.Enum;
using Fantasy.Server.Domain.Dungeon.Repository.Interface;
using Fantasy.Server.Domain.Dungeon.Service.Interface;
using Fantasy.Server.Domain.GameData.Entity;
using Fantasy.Server.Domain.GameData.Enum;
Expand Down Expand Up @@ -26,6 +29,7 @@ public class BossDungeonService : IBossDungeonService
private readonly IPlayerRedisRepository _playerRedisRepository;
private readonly IGameDataCacheService _gameDataCacheService;
private readonly ILevelUpService _levelUpService;
private readonly IPlayerDungeonProgressRepository _playerDungeonProgressRepository;
private readonly IAppDbTransactionRunner _transactionRunner;
private readonly ICurrentUserProvider _currentUserProvider;
private readonly ICombatStatCalculator _calculator;
Expand All @@ -40,6 +44,7 @@ public BossDungeonService(
IPlayerRedisRepository playerRedisRepository,
IGameDataCacheService gameDataCacheService,
ILevelUpService levelUpService,
IPlayerDungeonProgressRepository playerDungeonProgressRepository,
IAppDbTransactionRunner transactionRunner,
ICurrentUserProvider currentUserProvider,
ICombatStatCalculator calculator)
Expand All @@ -53,6 +58,7 @@ public BossDungeonService(
_playerRedisRepository = playerRedisRepository;
_gameDataCacheService = gameDataCacheService;
_levelUpService = levelUpService;
_playerDungeonProgressRepository = playerDungeonProgressRepository;
_transactionRunner = transactionRunner;
_currentUserProvider = currentUserProvider;
_calculator = calculator;
Expand Down Expand Up @@ -100,17 +106,23 @@ public async Task<BossDungeonResponse> ExecuteAsync()

var combatStat = _calculator.Calculate(player.Level, jobStat, weaponData, weaponEnhancement, unlockedPassiveSkills);

var stageData = await _gameDataCacheService.GetStageDataAsync(stage.MaxStage);
if (stageData is null)
throw new NotFoundException("스테이지 데이터를 찾을 수 없습니다.");
var progress = await _playerDungeonProgressRepository.FindByPlayerIdAndDungeonTypeAsync(player.Id, DungeonType.Boss);
var currentStage = progress?.HighestClearedStage ?? 1;

var stageData = await _gameDataCacheService.GetStageDataAsync(currentStage)
?? throw new NotFoundException("스테이지 데이터를 찾을 수 없습니다.");

// 보스는 일반 몬스터의 5배 체력
var bossHp = stageData.MonsterHp * 5;
var dps = _calculator.CalculateDps(combatStat);
var cleared = dps * 30 >= bossHp;

if (!cleared)
return new BossDungeonResponse(false, 0, null, 0, []);
return new BossDungeonResponse(false, 0, null, 0, [], currentStage);

var isNewProgress = progress is null;
progress ??= PlayerDungeonProgress.Create(player.Id, DungeonType.Boss);
progress.ClearStage(currentStage + 1);

var earnedXp = stageData.XpPerSecond * BossXpMultiplier;
var levelUps = await _levelUpService.ExecuteAsync(player, resource, earnedXp);
Expand All @@ -137,10 +149,15 @@ await _transactionRunner.ExecuteAsync(async () =>
await _playerResourceRepository.UpdateAsync(resource);
if (weaponChanges.Count > 0)
await _playerWeaponRepository.UpsertRangeAsync(player.Id, weaponChanges);

if (isNewProgress)
await _playerDungeonProgressRepository.SaveAsync(progress);
else
await _playerDungeonProgressRepository.UpdateAsync(progress);
});

await _playerRedisRepository.DeleteAsync(accountId);

return new BossDungeonResponse(true, BossMithrilReward, droppedWeapon, earnedXp, levelUps);
return new BossDungeonResponse(true, BossMithrilReward, droppedWeapon, earnedXp, levelUps, progress.HighestClearedStage);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using Fantasy.Server.Domain.Dungeon.Dto.Request;
using Fantasy.Server.Domain.Dungeon.Dto.Response;
using Fantasy.Server.Domain.Dungeon.Entity;
using Fantasy.Server.Domain.Dungeon.Enum;
using Fantasy.Server.Domain.Dungeon.Repository.Interface;
using Fantasy.Server.Domain.Dungeon.Service.Interface;
using Fantasy.Server.Domain.Player.Dto.Response;
Expand All @@ -23,6 +25,7 @@ public class GoldDungeonClaimService : IGoldDungeonClaimService
private readonly IPlayerSkillRepository _playerSkillRepository;
private readonly IPlayerRedisRepository _playerRedisRepository;
private readonly IGoldDungeonRunRepository _goldDungeonRunRepository;
private readonly IPlayerDungeonProgressRepository _playerDungeonProgressRepository;
private readonly IRandomProvider _randomProvider;
private readonly IAppDbTransactionRunner _transactionRunner;
private readonly ICurrentUserProvider _currentUserProvider;
Expand All @@ -37,6 +40,7 @@ public GoldDungeonClaimService(
IPlayerSkillRepository playerSkillRepository,
IPlayerRedisRepository playerRedisRepository,
IGoldDungeonRunRepository goldDungeonRunRepository,
IPlayerDungeonProgressRepository playerDungeonProgressRepository,
IRandomProvider randomProvider,
IAppDbTransactionRunner transactionRunner,
ICurrentUserProvider currentUserProvider,
Expand All @@ -50,6 +54,7 @@ public GoldDungeonClaimService(
_playerSkillRepository = playerSkillRepository;
_playerRedisRepository = playerRedisRepository;
_goldDungeonRunRepository = goldDungeonRunRepository;
_playerDungeonProgressRepository = playerDungeonProgressRepository;
_randomProvider = randomProvider;
_transactionRunner = transactionRunner;
_currentUserProvider = currentUserProvider;
Expand Down Expand Up @@ -85,6 +90,8 @@ public async Task<GoldDungeonClaimResponse> ExecuteAsync(Guid runId, GoldDungeon

var playerResponse = PlayerDataResponseBuilder.Build(player, resource, stage, session, weapons, skills);

var existingProgress = await _playerDungeonProgressRepository.FindByPlayerIdAndDungeonTypeAsync(player.Id, DungeonType.Gold);

var idempotentChanges = new ChangesDto(
Gold: run.EarnedGold!.Value,
Exp: 0,
Expand All @@ -98,7 +105,8 @@ public async Task<GoldDungeonClaimResponse> ExecuteAsync(Guid runId, GoldDungeon
MaxStage: 0
);

return new GoldDungeonClaimResponse(run.Id, run.EarnedGold!.Value, run.EarnedMithril!.Value, idempotentChanges, playerResponse);
return new GoldDungeonClaimResponse(run.Id, run.EarnedGold!.Value, run.EarnedMithril!.Value,
existingProgress?.HighScore ?? 0, idempotentChanges, playerResponse);
}

var now = _timeProvider.GetUtcNow().UtcDateTime;
Expand Down Expand Up @@ -138,10 +146,20 @@ public async Task<GoldDungeonClaimResponse> ExecuteAsync(Guid runId, GoldDungeon

run.Claim(request.Clicks, earnedGold, mithrilDropped ? 1 : 0);

var progress = await _playerDungeonProgressRepository.FindByPlayerIdAndDungeonTypeAsync(claimPlayer.Id, DungeonType.Gold);
var isNewProgress = progress is null;
progress ??= PlayerDungeonProgress.Create(claimPlayer.Id, DungeonType.Gold);
progress.UpdateHighScore(earnedGold);

await _transactionRunner.ExecuteAsync(async () =>
{
await _playerResourceRepository.UpdateAsync(claimResource);
await _goldDungeonRunRepository.UpdateAsync(run);

if (isNewProgress)
await _playerDungeonProgressRepository.SaveAsync(progress);
else
await _playerDungeonProgressRepository.UpdateAsync(progress);
});

var claimPlayerResponse = PlayerDataResponseBuilder.Build(claimPlayer, claimResource, claimStage, claimSession, claimWeapons, claimSkills);
Expand All @@ -160,6 +178,6 @@ await _transactionRunner.ExecuteAsync(async () =>
MaxStage: 0
);

return new GoldDungeonClaimResponse(run.Id, earnedGold, mithrilDropped ? 1 : 0, changes, claimPlayerResponse);
return new GoldDungeonClaimResponse(run.Id, earnedGold, mithrilDropped ? 1 : 0, progress.HighScore, changes, claimPlayerResponse);
}
}
Loading
Loading