Skip to content

Commit

Permalink
Campaign skip feature (#777)
Browse files Browse the repository at this point in the history
I have implemented the campaign skip (story skip) functionality. When
the "Skip Campaign" button is clicked on the client application (only
visible when player has not cleared chapter 10), player data will be
adjusted as described in the Dragalia Lost Wiki.

- Player level: 60
- Stamina: 999
- Getherwings: 99
- All quest first clear and endeavors completed on Easy mode from
chapters 1-10, granting Wyrmite rewards
- All quest stories marked as "read" from chapters 1-10
- Story units and dragon rewards added to collection
- Tutorials skipped through chapter 10
- Story progression Halidom facilities added to storage with their
appropriate levels

I also included a fairly detailed unit test, which will check player
level, Halidom buildings, etc.

[Api
Doc](https://dragalia-api-docs.readthedocs.io/en/latest/dragalia/story_skip_skip.html)
[Dragalia Lost
Wiki](https://dragalialost.wiki/w/Beginner%27s_Guide#The_Story_Skip_Option)

This was my first dive into a C# web app, so hopefully my code fits with
your coding conventions.
  • Loading branch information
jaredstrong89 authored May 7, 2024
1 parent daa3478 commit e16c9fb
Show file tree
Hide file tree
Showing 6 changed files with 541 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public interface IQuestRepository
{
IQueryable<DbQuest> Quests { get; }
IQueryable<DbQuestEvent> QuestEvents { get; }
IQueryable<DbQuestTreasureList> QuestTreasureList { get; }

Task<DbQuest> GetQuestDataAsync(int questId);
Task<DbQuestEvent> GetQuestEventAsync(int questEventId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using DragaliaAPI.Database.Entities;
using DragaliaAPI.Features.StorySkip;
using DragaliaAPI.Shared.Features.StorySkip;
using Microsoft.EntityFrameworkCore;
using static DragaliaAPI.Shared.Features.StorySkip.StorySkipRewards;

namespace DragaliaAPI.Integration.Test.Features.StorySkip;

/// <summary>
/// Tests <see cref="StorySkipController"/>
/// </summary>
public class StorySkipTest : TestFixture
{
public StorySkipTest(CustomWebApplicationFactory factory, ITestOutputHelper testOutputHelper)
: base(factory, testOutputHelper) { }

[Fact]
public async Task StorySkip_CheckStatsAfterSkip()
{
int questId = 100_100_107;
int storyId = 1_001_009;
Dictionary<FortPlants, FortConfig> fortConfigs = StorySkipRewards.FortConfigs;
List<FortPlants> uniqueFortPlants = new(fortConfigs.Keys);

await this
.ApiContext.PlayerUserData.Where(x => x.ViewerId == this.ViewerId)
.ExecuteUpdateAsync(u =>
u.SetProperty(e => e.Level, 5)
.SetProperty(e => e.Exp, 1)
.SetProperty(e => e.StaminaSingle, 10)
.SetProperty(e => e.StaminaMulti, 10)
);

await this
.ApiContext.PlayerQuests.Where(x => x.ViewerId == this.ViewerId && x.QuestId <= questId)
.ExecuteDeleteAsync();

await this
.ApiContext.PlayerStoryState.Where(x =>
x.ViewerId == this.ViewerId
&& x.StoryType == StoryTypes.Quest
&& x.StoryId <= storyId
)
.ExecuteDeleteAsync();

await this
.ApiContext.PlayerCharaData.Where(x =>
x.ViewerId == this.ViewerId && x.CharaId != Charas.ThePrince
)
.ExecuteDeleteAsync();

await this
.ApiContext.PlayerDragonData.Where(x => x.ViewerId == this.ViewerId)
.ExecuteDeleteAsync();

await this
.ApiContext.PlayerFortBuilds.Where(x => x.ViewerId == this.ViewerId)
.ExecuteDeleteAsync();

StorySkipSkipResponse data = (
await this.Client.PostMsgpack<StorySkipSkipResponse>("story_skip/skip")
).Data;

DbPlayerUserData userData = await this.ApiContext.PlayerUserData.SingleAsync(x =>
x.ViewerId == this.ViewerId
);

data.Should().BeEquivalentTo(new StorySkipSkipResponse() { ResultState = 1 });
userData.Level.Should().Be(60);
userData.Exp.Should().Be(69990);
userData.StaminaSingle.Should().Be(999);
userData.StaminaMulti.Should().Be(99);
userData.TutorialFlag.Should().Be(16640603);
userData.TutorialStatus.Should().Be(60999);
this.ApiContext.PlayerQuests.Count(x => x.ViewerId == this.ViewerId && x.QuestId == questId)
.Should()
.Be(1);
this.ApiContext.PlayerStoryState.Count(x =>
x.ViewerId == this.ViewerId && x.StoryId == storyId
)
.Should()
.Be(1);
this.ApiContext.PlayerCharaData.Count(x => x.ViewerId == this.ViewerId).Should().Be(6);
this.ApiContext.PlayerDragonData.Count(x => x.ViewerId == this.ViewerId).Should().Be(5);

foreach ((FortPlants fortPlant, FortConfig fortConfig) in fortConfigs)
{
List<DbFortBuild> forts = await this
.ApiContext.PlayerFortBuilds.Where(x =>
x.ViewerId == this.ViewerId && x.PlantId == fortPlant
)
.ToListAsync();

forts.Count.Should().Be(fortConfig.BuildCount);

foreach (DbFortBuild fort in forts)
{
fort.Level.Should().Be(fortConfig.Level);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using DragaliaAPI.Shared.Definitions.Enums;

namespace DragaliaAPI.Shared.Features.StorySkip;

public static class StorySkipRewards
{
public struct FortConfig
{
public int BuildCount { get; }
public int Level { get; }
public int PositionX { get; }
public int PositionZ { get; }

public FortConfig(int level, int buildCount, int positionX = -1, int positionZ = -1)
{
BuildCount = buildCount;
Level = level;
PositionX = positionX;
PositionZ = positionZ;
}
}

public static readonly List<Charas> CharasList =
new() { Charas.Elisanne, Charas.Ranzal, Charas.Cleo, Charas.Luca, Charas.Alex };

public static readonly List<Dragons> DragonList =
new()
{
Dragons.Brunhilda,
Dragons.Mercury,
Dragons.Midgardsormr,
Dragons.Jupiter,
Dragons.Zodiark,
};

public static readonly Dictionary<FortPlants, FortConfig> FortConfigs =
new()
{
[FortPlants.TheHalidom] = new FortConfig(6, 1, 16, 17),
[FortPlants.Smithy] = new FortConfig(6, 1, 21, 3),
[FortPlants.RupieMine] = new FortConfig(15, 4),
[FortPlants.Dragontree] = new FortConfig(15, 1),
[FortPlants.FlameAltar] = new FortConfig(10, 2),
[FortPlants.WaterAltar] = new FortConfig(10, 2),
[FortPlants.WindAltar] = new FortConfig(10, 2),
[FortPlants.LightAltar] = new FortConfig(10, 2),
[FortPlants.ShadowAltar] = new FortConfig(10, 2),
[FortPlants.SwordDojo] = new FortConfig(10, 2),
[FortPlants.BladeDojo] = new FortConfig(10, 2),
[FortPlants.DaggerDojo] = new FortConfig(10, 2),
[FortPlants.LanceDojo] = new FortConfig(10, 2),
[FortPlants.AxeDojo] = new FortConfig(10, 2),
[FortPlants.BowDojo] = new FortConfig(10, 2),
[FortPlants.WandDojo] = new FortConfig(10, 2),
[FortPlants.StaffDojo] = new FortConfig(10, 2),
[FortPlants.ManacasterDojo] = new FortConfig(10, 2)
};
}
65 changes: 65 additions & 0 deletions DragaliaAPI/DragaliaAPI/Features/StorySkip/StorySkipController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using DragaliaAPI.Controllers;
using DragaliaAPI.Database.Repositories;
using DragaliaAPI.Models.Generated;
using DragaliaAPI.Services;
using DragaliaAPI.Shared.PlayerDetails;
using Microsoft.AspNetCore.Mvc;

namespace DragaliaAPI.Features.StorySkip;

[Route("story_skip")]
public class StorySkipController : DragaliaControllerBase
{
private readonly ILogger<StorySkipController> logger;
private readonly IPlayerIdentityService playerIdentityService;
private readonly IQuestRepository questRepository;
private readonly StorySkipService storySkipService;
private readonly IUpdateDataService updateDataService;
private readonly IUserDataRepository userDataRepository;

public StorySkipController(
ILogger<StorySkipController> logger,
IPlayerIdentityService playerIdentityService,
IQuestRepository questRepository,
StorySkipService storySkipService,
IUpdateDataService updateDataService,
IUserDataRepository userDataRepository
)
{
this.logger = logger;
this.playerIdentityService = playerIdentityService;
this.questRepository = questRepository;
this.storySkipService = storySkipService;
this.updateDataService = updateDataService;
this.userDataRepository = userDataRepository;
}

[HttpPost("skip")]
public async Task<DragaliaResult> Read(CancellationToken cancellationToken)
{
string accountId = playerIdentityService.AccountId;
long viewerId = playerIdentityService.ViewerId;

this.logger.LogDebug("Beginning story skip for player {accountId}.", accountId);

int wyrmite1 = await storySkipService.ProcessQuestCompletions(viewerId);
this.logger.LogDebug("Wyrmite earned from quests: {wyrmite}", wyrmite1);

int wyrmite2 = await storySkipService.ProcessStoryCompletions(viewerId);
this.logger.LogDebug("Wyrmite earned from quest stories: {wyrmite}", wyrmite2);

await storySkipService.UpdateUserData(wyrmite1 + wyrmite2);

await storySkipService.IncreaseFortLevels(viewerId);

await storySkipService.RewardCharas(viewerId);

await storySkipService.RewardDragons(viewerId);

await updateDataService.SaveChangesAsync(cancellationToken);

this.logger.LogDebug("Story Skip completed for player {accountId}.", accountId);

return this.Ok(new StorySkipSkipResponse() { ResultState = 1 });
}
}
Loading

0 comments on commit e16c9fb

Please sign in to comment.