diff --git a/data/constants.json b/data/constants.json index dea4ff033..6c0cc3f47 100644 --- a/data/constants.json +++ b/data/constants.json @@ -26,13 +26,14 @@ "tournamentLevel": 30, "newUserStartingCharacterLevel": 30, "experienceForLevelCoefs": [13, 200], + "highLevelCutoff": 30, "defaultStrength": 3, "defaultAgility": 3, "defaultHealthPoints": 60, /* A 33 STR, 3 AGI character with 11 IF would reach 137 HP. */ "defaultGeneration": 0, - "defaultAttributePoints": 0, + "defaultAttributePoints": 1, "attributePointsPerLevel": 1, - "defaultSkillPoints": 2, + "defaultSkillPoints": 3, "skillPointsPerLevel": 1, "healthPointsForStrength": 1, "healthPointsForIronFlesh": 4, diff --git a/src/Application/Characters/Commands/RespecializeAllCharactersCommand.cs b/src/Application/Characters/Commands/RespecializeAllCharactersCommand.cs new file mode 100644 index 000000000..2f1c578c5 --- /dev/null +++ b/src/Application/Characters/Commands/RespecializeAllCharactersCommand.cs @@ -0,0 +1,37 @@ +using Crpg.Application.Common.Interfaces; +using Crpg.Application.Common.Mediator; +using Crpg.Application.Common.Results; +using Crpg.Application.Common.Services; +using Microsoft.EntityFrameworkCore; + +namespace Crpg.Application.Characters.Commands; + +public record RespecializeAllCharactersCommand : IMediatorRequest +{ + internal class Handler : IMediatorRequestHandler + { + private readonly ICrpgDbContext _db; + private readonly ICharacterService _characterService; + + public Handler(ICrpgDbContext db, ICharacterService characterService) + { + _db = db; + _characterService = characterService; + } + + public async Task Handle(RespecializeAllCharactersCommand req, CancellationToken cancellationToken) + { + var characters = await _db.Characters.ToArrayAsync(cancellationToken: cancellationToken); + + foreach (var character in characters) + { + _characterService.ResetCharacterCharacteristics(character, true); + // Trick to avoid UpdatedAt to be updated. + character.UpdatedAt = character.UpdatedAt; + } + + await _db.SaveChangesAsync(cancellationToken); + return Result.NoErrors; + } + } +} diff --git a/src/Application/Common/Constants.cs b/src/Application/Common/Constants.cs index 2bbe292c7..fe4661909 100644 --- a/src/Application/Common/Constants.cs +++ b/src/Application/Common/Constants.cs @@ -29,6 +29,7 @@ public class Constants public int TournamentLevel { get; set; } public int NewUserStartingCharacterLevel { get; set; } public float[] ExperienceForLevelCoefs { get; set; } = Array.Empty(); + public int HighLevelCutoff { get; set; } public int DefaultStrength { get; set; } public int DefaultAgility { get; set; } public int DefaultHealthPoints { get; set; } diff --git a/src/Application/Common/Services/ICharacterService.cs b/src/Application/Common/Services/ICharacterService.cs index 3f6b49e26..25edd69d0 100644 --- a/src/Application/Common/Services/ICharacterService.cs +++ b/src/Application/Common/Services/ICharacterService.cs @@ -77,11 +77,25 @@ public void SetDefaultValuesForCharacter(Character character) /// public void ResetCharacterCharacteristics(Character character, bool respecialization = false) { + int CalculateAttributePoints(int level) + { + int points = 0; + for (int i = 1; i < level; i++) + { + if (i < _constants.HighLevelCutoff) + { + points += _constants.AttributePointsPerLevel; + } + } + + return points; + } + character.Characteristics = new CharacterCharacteristics { Attributes = new CharacterAttributes { - Points = _constants.DefaultAttributePoints + (respecialization ? (character.Level - 1) * _constants.AttributePointsPerLevel : 0), + Points = _constants.DefaultAttributePoints + (respecialization ? CalculateAttributePoints(character.Level) : 0), Strength = _constants.DefaultStrength, Agility = _constants.DefaultAgility, }, @@ -187,7 +201,14 @@ public void GiveExperience(Character character, int experience, bool useExperien int levelDiff = newLevel - character.Level; if (levelDiff != 0) // if character leveled up { - character.Characteristics.Attributes.Points += levelDiff * _constants.AttributePointsPerLevel; + for (int i = character.Level; i < newLevel; i++) + { + if (i < _constants.HighLevelCutoff) // reward attribute points for lower levels + { + character.Characteristics.Attributes.Points += _constants.AttributePointsPerLevel; + } + } + character.Characteristics.Skills.Points += levelDiff * _constants.SkillPointsPerLevel; character.Characteristics.WeaponProficiencies.Points += WeaponProficiencyPointsForLevel(newLevel) - WeaponProficiencyPointsForLevel(character.Level); character.Level = newLevel; diff --git a/src/Module.Server/Common/CrpgConstants.cs b/src/Module.Server/Common/CrpgConstants.cs index d7a7ef634..e0ac3cfa3 100644 --- a/src/Module.Server/Common/CrpgConstants.cs +++ b/src/Module.Server/Common/CrpgConstants.cs @@ -29,6 +29,7 @@ internal class CrpgConstants public int TournamentLevel { get; set; } public int NewUserStartingCharacterLevel { get; set; } public float[] ExperienceForLevelCoefs { get; set; } = Array.Empty(); + public int HighLevelCutoff { get; set; } public int DefaultStrength { get; set; } public int DefaultAgility { get; set; } public int DefaultHealthPoints { get; set; } diff --git a/src/WebApi/Controllers/UsersController.cs b/src/WebApi/Controllers/UsersController.cs index 133101315..5da977d2e 100644 --- a/src/WebApi/Controllers/UsersController.cs +++ b/src/WebApi/Controllers/UsersController.cs @@ -264,6 +264,19 @@ public Task UpdateEveryCharacterCompetitiveRating() return ResultToActionAsync(Mediator.Send(cmd)); } + /// + /// Respecializes every character. + /// > + /// Updated. + /// Bad Request. + [Authorize(AdminPolicy)] + [HttpPut("characters/respecialize")] + public Task RespecializeAllCharacters() + { + RespecializeAllCharactersCommand cmd = new(); + return ResultToActionAsync(Mediator.Send(cmd)); + } + /// /// Updates character characteristics for the current user. /// diff --git a/src/WebUI/src/__mocks__/constants.json b/src/WebUI/src/__mocks__/constants.json index 0da962211..e5c5880a5 100644 --- a/src/WebUI/src/__mocks__/constants.json +++ b/src/WebUI/src/__mocks__/constants.json @@ -24,13 +24,14 @@ "maximumLevel": 38, "tournamentLevel": 30, "experienceForLevelCoefs": [13, 200], + "highLevelCutoff": 30, "defaultStrength": 3, "defaultAgility": 3, "defaultHealthPoints": 60, "defaultGeneration": 0, - "defaultAttributePoints": 0, + "defaultAttributePoints": 1, "attributePointsPerLevel": 1, - "defaultSkillPoints": 2, + "defaultSkillPoints": 3, "skillPointsPerLevel": 1, "healthPointsForStrength": 1, "healthPointsForIronFlesh": 4, diff --git a/src/WebUI/src/services/characters-service.spec.ts b/src/WebUI/src/services/characters-service.spec.ts index a791d0c58..3fca55681 100644 --- a/src/WebUI/src/services/characters-service.spec.ts +++ b/src/WebUI/src/services/characters-service.spec.ts @@ -180,21 +180,23 @@ it.each([ }); it.each([ - [0, 0], - [1, 0], - [2, 1], - [10, 9], - [38, 37], + [0, 1], + [1, 1], + [2, 2], + [10, 10], + [30, 30], + [38, 30], ])('attributePointsForLevel - level: %s', (level, expectation) => { expect(attributePointsForLevel(level)).toEqual(expectation); }); it.each([ - [0, 2], - [1, 2], - [2, 3], - [10, 11], - [38, 39], + [0, 3], + [1, 3], + [2, 4], + [10, 12], + [30, 32], + [38, 40], ])('skillPointsForLevel - level: %s', (level, expectation) => { expect(skillPointsForLevel(level)).toEqual(expectation); }); diff --git a/src/WebUI/src/services/characters-service.ts b/src/WebUI/src/services/characters-service.ts index d540c6536..23eda857b 100644 --- a/src/WebUI/src/services/characters-service.ts +++ b/src/WebUI/src/services/characters-service.ts @@ -6,6 +6,7 @@ import { weaponProficiencyPointsForAgility, weaponProficiencyPointsForWeaponMasterCoefs, experienceForLevelCoefs, + highLevelCutoff, defaultStrength, defaultAgility, defaultHealthPoints, @@ -266,7 +267,16 @@ export const getMaximumExperience = () => getExperienceForLevel(maximumLevel); export const attributePointsForLevel = (level: number): number => { if (level <= 0) level = minimumLevel; - return defaultAttributePoints + (level - 1) * attributePointsPerLevel; + + let points = defaultAttributePoints; + + for (let i = 1; i < level; i++) { + if (i < highLevelCutoff) { + points += attributePointsPerLevel; + } + } + + return points; }; export const skillPointsForLevel = (level: number): number => { diff --git a/test/Application.UTest/Common/Services/CharacterServiceTest.cs b/test/Application.UTest/Common/Services/CharacterServiceTest.cs index 0f86e5b1b..51558581a 100644 --- a/test/Application.UTest/Common/Services/CharacterServiceTest.cs +++ b/test/Application.UTest/Common/Services/CharacterServiceTest.cs @@ -21,6 +21,7 @@ public class CharacterServiceTest ExperienceMultiplierByGeneration = 0.03f, MaxExperienceMultiplierForGeneration = 1.48f, ExperienceForLevelCoefs = new[] { 2f, 0 }, + HighLevelCutoff = 30, DefaultAttributePoints = 0, AttributePointsPerLevel = 1, DefaultSkillPoints = 2,