diff --git a/.gitignore b/.gitignore index a01ec03..99e5bd9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ obj appsettings.Development.json *.db *.db-shm +.*.db-wal *.csproj.user \ No newline at end of file diff --git a/Controllers/PetController.cs b/Controllers/PetController.cs index d36734a..883cc58 100644 --- a/Controllers/PetController.cs +++ b/Controllers/PetController.cs @@ -36,44 +36,7 @@ namespace pet_companion_api.Controllers return CreatedAtAction(nameof(GetAllPets), new { id = createdPet.Id }, createdPet); } - /// - /// - /// - /// - /// One of `feed`, `play`, `sleep` - /// - [HttpPut("{petId}/action/{action}")] - public IActionResult UpdatePetAction(string petId, string action) - { - try - { - PetAction petAction; - switch (action.ToLower()) - { - case "feed": - petAction = PetAction.FEED; - break; - case "play": - petAction = PetAction.PLAY; - break; - case "sleep": - petAction = PetAction.SLEEP; - break; - default: - return BadRequest("Invalid action. Valid actions are: feed, play, sleep"); - } - - var actionRequest = new PetUpdateActionRequest { Action = petAction }; - var updatedPet = petService.UpdatePetAction(petId, userId.ToString(), actionRequest); - return Ok(updatedPet); - } - catch (Exception ex) - { - return NotFound(ex.Message); - } - } - - [HttpPut("{petId}/action/gather")] + [HttpPut("{petId}/action")] public IActionResult UpdatePetActionGather(string petId, [FromBody] PetUpdateActionRequest actionRequest) { try diff --git a/Data/ApplicationDbContext.cs b/Data/ApplicationDbContext.cs index ece3bd8..d187299 100644 --- a/Data/ApplicationDbContext.cs +++ b/Data/ApplicationDbContext.cs @@ -12,5 +12,23 @@ namespace pet_companion_api.Data public DbSet Pets { get; set; } public DbSet PetStats { get; set; } public DbSet Resources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // Configure DateTime properties to be stored as UTC + modelBuilder.Entity() + .Property(p => p.GatherActionSince) + .HasConversion( + v => v.ToUniversalTime(), + v => DateTime.SpecifyKind(v, DateTimeKind.Utc)); + + modelBuilder.Entity() + .Property(p => p.BasicActionCooldown) + .HasConversion( + v => v != DateTime.MinValue ? v.ToUniversalTime() : v, + v => v != DateTime.MinValue ? DateTime.SpecifyKind(v, DateTimeKind.Utc) : v); + } } } diff --git a/Migrations/20250201022754_InitialCreate.Designer.cs b/Migrations/20250201022754_InitialCreate.Designer.cs deleted file mode 100644 index 98a8957..0000000 --- a/Migrations/20250201022754_InitialCreate.Designer.cs +++ /dev/null @@ -1,116 +0,0 @@ -// -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using pet_companion_api.Data; - -#nullable disable - -namespace pet_companion_api.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20250201022754_InitialCreate")] - partial class InitialCreate - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.1"); - - modelBuilder.Entity("pet_companion_api.Models.Pet", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Class") - .HasColumnType("INTEGER"); - - b.Property("Level") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UserId") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Pets"); - }); - - modelBuilder.Entity("pet_companion_api.Models.PetStats", b => - { - b.Property("PetId") - .HasColumnType("TEXT"); - - b.Property("Charisma") - .HasColumnType("INTEGER"); - - b.Property("Intelligence") - .HasColumnType("INTEGER"); - - b.Property("Strength") - .HasColumnType("INTEGER"); - - b.HasKey("PetId"); - - b.ToTable("PetStats"); - }); - - modelBuilder.Entity("pet_companion_api.Models.Resources", b => - { - b.Property("PetId") - .HasColumnType("TEXT"); - - b.Property("Food") - .HasColumnType("INTEGER"); - - b.Property("Gold") - .HasColumnType("INTEGER"); - - b.Property("Junk") - .HasColumnType("INTEGER"); - - b.Property("Wisdom") - .HasColumnType("INTEGER"); - - b.HasKey("PetId"); - - b.ToTable("Resources"); - }); - - modelBuilder.Entity("pet_companion_api.Models.PetStats", b => - { - b.HasOne("pet_companion_api.Models.Pet", null) - .WithOne("Stats") - .HasForeignKey("pet_companion_api.Models.PetStats", "PetId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("pet_companion_api.Models.Resources", b => - { - b.HasOne("pet_companion_api.Models.Pet", null) - .WithOne("Resources") - .HasForeignKey("pet_companion_api.Models.Resources", "PetId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("pet_companion_api.Models.Pet", b => - { - b.Navigation("Resources") - .IsRequired(); - - b.Navigation("Stats") - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Migrations/20250201025206_AddActions.cs b/Migrations/20250201025206_AddActions.cs deleted file mode 100644 index 6e7717b..0000000 --- a/Migrations/20250201025206_AddActions.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace pet_companion_api.Migrations -{ - /// - public partial class AddActions : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "ActionSince", - table: "Pets", - type: "TEXT", - nullable: false, - defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); - - migrationBuilder.AddColumn( - name: "IsDead", - table: "Pets", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "PetAction", - table: "Pets", - type: "INTEGER", - nullable: false, - defaultValue: 0); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "ActionSince", - table: "Pets"); - - migrationBuilder.DropColumn( - name: "IsDead", - table: "Pets"); - - migrationBuilder.DropColumn( - name: "PetAction", - table: "Pets"); - } - } -} diff --git a/Migrations/20250201025206_AddActions.Designer.cs b/Migrations/20250201173643_Initial.Designer.cs similarity index 84% rename from Migrations/20250201025206_AddActions.Designer.cs rename to Migrations/20250201173643_Initial.Designer.cs index 05664b0..2c0016a 100644 --- a/Migrations/20250201025206_AddActions.Designer.cs +++ b/Migrations/20250201173643_Initial.Designer.cs @@ -11,8 +11,8 @@ using pet_companion_api.Data; namespace pet_companion_api.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20250201025206_AddActions")] - partial class AddActions + [Migration("20250201173643_Initial")] + partial class Initial { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -25,12 +25,15 @@ namespace pet_companion_api.Migrations b.Property("Id") .HasColumnType("TEXT"); - b.Property("ActionSince") + b.Property("BasicActionCooldown") .HasColumnType("TEXT"); b.Property("Class") .HasColumnType("INTEGER"); + b.Property("GatherActionSince") + .HasColumnType("TEXT"); + b.Property("IsDead") .HasColumnType("INTEGER"); @@ -41,7 +44,10 @@ namespace pet_companion_api.Migrations .IsRequired() .HasColumnType("TEXT"); - b.Property("PetAction") + b.Property("PetBasicAction") + .HasColumnType("INTEGER"); + + b.Property("PetGatherAction") .HasColumnType("INTEGER"); b.Property("UserId") @@ -64,6 +70,15 @@ namespace pet_companion_api.Migrations b.Property("Intelligence") .HasColumnType("INTEGER"); + b.Property("MaxCharisma") + .HasColumnType("INTEGER"); + + b.Property("MaxIntelligence") + .HasColumnType("INTEGER"); + + b.Property("MaxStrength") + .HasColumnType("INTEGER"); + b.Property("Strength") .HasColumnType("INTEGER"); diff --git a/Migrations/20250201022754_InitialCreate.cs b/Migrations/20250201173643_Initial.cs similarity index 78% rename from Migrations/20250201022754_InitialCreate.cs rename to Migrations/20250201173643_Initial.cs index 35b8e48..73b7145 100644 --- a/Migrations/20250201022754_InitialCreate.cs +++ b/Migrations/20250201173643_Initial.cs @@ -1,11 +1,12 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using System; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable namespace pet_companion_api.Migrations { /// - public partial class InitialCreate : Migration + public partial class Initial : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -18,7 +19,12 @@ namespace pet_companion_api.Migrations Name = table.Column(type: "TEXT", nullable: false), Class = table.Column(type: "INTEGER", nullable: false), Level = table.Column(type: "INTEGER", nullable: false), - UserId = table.Column(type: "TEXT", nullable: false) + UserId = table.Column(type: "TEXT", nullable: false), + IsDead = table.Column(type: "INTEGER", nullable: false), + PetGatherAction = table.Column(type: "INTEGER", nullable: false), + GatherActionSince = table.Column(type: "TEXT", nullable: false), + PetBasicAction = table.Column(type: "INTEGER", nullable: false), + BasicActionCooldown = table.Column(type: "TEXT", nullable: false) }, constraints: table => { @@ -32,7 +38,10 @@ namespace pet_companion_api.Migrations PetId = table.Column(type: "TEXT", nullable: false), Intelligence = table.Column(type: "INTEGER", nullable: false), Strength = table.Column(type: "INTEGER", nullable: false), - Charisma = table.Column(type: "INTEGER", nullable: false) + Charisma = table.Column(type: "INTEGER", nullable: false), + MaxIntelligence = table.Column(type: "INTEGER", nullable: false), + MaxStrength = table.Column(type: "INTEGER", nullable: false), + MaxCharisma = table.Column(type: "INTEGER", nullable: false) }, constraints: table => { diff --git a/Migrations/ApplicationDbContextModelSnapshot.cs b/Migrations/ApplicationDbContextModelSnapshot.cs index 99dae93..3e63dbe 100644 --- a/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Migrations/ApplicationDbContextModelSnapshot.cs @@ -22,12 +22,15 @@ namespace pet_companion_api.Migrations b.Property("Id") .HasColumnType("TEXT"); - b.Property("ActionSince") + b.Property("BasicActionCooldown") .HasColumnType("TEXT"); b.Property("Class") .HasColumnType("INTEGER"); + b.Property("GatherActionSince") + .HasColumnType("TEXT"); + b.Property("IsDead") .HasColumnType("INTEGER"); @@ -38,7 +41,10 @@ namespace pet_companion_api.Migrations .IsRequired() .HasColumnType("TEXT"); - b.Property("PetAction") + b.Property("PetBasicAction") + .HasColumnType("INTEGER"); + + b.Property("PetGatherAction") .HasColumnType("INTEGER"); b.Property("UserId") @@ -61,6 +67,15 @@ namespace pet_companion_api.Migrations b.Property("Intelligence") .HasColumnType("INTEGER"); + b.Property("MaxCharisma") + .HasColumnType("INTEGER"); + + b.Property("MaxIntelligence") + .HasColumnType("INTEGER"); + + b.Property("MaxStrength") + .HasColumnType("INTEGER"); + b.Property("Strength") .HasColumnType("INTEGER"); diff --git a/Models/Pet.cs b/Models/Pet.cs index 679f935..73cc59f 100644 --- a/Models/Pet.cs +++ b/Models/Pet.cs @@ -14,7 +14,28 @@ namespace pet_companion_api.Models public string UserId { get; set; } public bool IsDead { get; set; } - public PetActionGather PetAction { get; set; } - public DateTime ActionSince { get; set; } + public PetActionGather PetGatherAction { get; set; } + public DateTime GatherActionSince { get; set; } + + public PetBasicAction PetBasicAction { get; set; } + public DateTime BasicActionCooldown { get; set; } + + public void IncrementIntelligence(int amount) + { + var newValue = Stats.Intelligence + amount; + Stats.Intelligence = Math.Min(newValue, Stats.MaxIntelligence); + } + + public void IncrementStrength(int amount) + { + var newValue = Stats.Strength + amount; + Stats.Strength = Math.Min(newValue, Stats.MaxStrength); + } + + public void IncrementCharisma(int amount) + { + var newValue = Stats.Charisma + amount; + Stats.Charisma = Math.Min(newValue, Stats.MaxCharisma); + } } } diff --git a/Models/PetActionGather.cs b/Models/PetActionGather.cs index 8b4aa3c..bc336cf 100644 --- a/Models/PetActionGather.cs +++ b/Models/PetActionGather.cs @@ -1,6 +1,6 @@ namespace pet_companion_api.Models { - public enum PetAction + public enum PetBasicAction { UNKNOWN, FEED, diff --git a/Models/PetStats.cs b/Models/PetStats.cs index cd48302..baa8650 100644 --- a/Models/PetStats.cs +++ b/Models/PetStats.cs @@ -11,6 +11,10 @@ namespace pet_companion_api.Models public int Strength { get; set; } public int Charisma { get; set; } + public int MaxIntelligence { get; set; } + public int MaxStrength { get; set; } + public int MaxCharisma { get; set; } + public static PetStats BuildFromClass(PetClass petClass) { var stats = new PetStats(); @@ -59,6 +63,10 @@ namespace pet_companion_api.Models break; } + stats.MaxIntelligence = stats.Intelligence; + stats.MaxStrength = stats.Strength; + stats.MaxCharisma = stats.Charisma; + return stats; } } diff --git a/Models/PetUpdateActionRequest.cs b/Models/PetUpdateActionRequest.cs index 1c4be84..74bce90 100644 --- a/Models/PetUpdateActionRequest.cs +++ b/Models/PetUpdateActionRequest.cs @@ -2,7 +2,7 @@ namespace pet_companion_api.Models { public class PetUpdateActionRequest { - public PetAction? Action { get; set; } - public PetActionGather? PetActionGather { get; set; } + public PetBasicAction? BasicAction { get; set; } + public PetActionGather? GatherAction { get; set; } } } diff --git a/Repositories/PetRepository.cs b/Repositories/PetRepository.cs index 6ef0c31..13bdb98 100644 --- a/Repositories/PetRepository.cs +++ b/Repositories/PetRepository.cs @@ -45,11 +45,20 @@ namespace pet_companion_api.Repositories return pet; } - public Pet UpdatePetAction(Pet pet) + public Pet UpdatePetBasicAction(Pet pet) { context.Pets.Attach(pet); - context.Entry(pet).Property(p => p.PetAction).IsModified = true; - context.Entry(pet).Property(p => p.ActionSince).IsModified = true; + context.Entry(pet).Property(p => p.PetBasicAction).IsModified = true; + context.Entry(pet).Property(p => p.BasicActionCooldown).IsModified = true; + context.SaveChanges(); + return pet; + } + + public Pet UpdatePetGatherAction(Pet pet) + { + context.Pets.Attach(pet); + context.Entry(pet).Property(p => p.PetGatherAction).IsModified = true; + context.Entry(pet).Property(p => p.GatherActionSince).IsModified = true; context.SaveChanges(); return pet; } @@ -58,7 +67,7 @@ namespace pet_companion_api.Repositories { context.Pets.Attach(pet); context.Entry(pet).Reference(p => p.Resources).IsModified = true; - context.Entry(pet).Property(p => p.ActionSince).IsModified = true; + context.Entry(pet).Property(p => p.GatherActionSince).IsModified = true; context.SaveChanges(); return pet; } diff --git a/Services/PetService.cs b/Services/PetService.cs index 1804b8b..aa004d1 100644 --- a/Services/PetService.cs +++ b/Services/PetService.cs @@ -21,17 +21,18 @@ namespace pet_companion_api.Services public Pet CreatePet(Guid userId, PetCreationRequest petRequest) { + var petId = Guid.NewGuid().ToString(); var pet = new Pet { - Id = Guid.NewGuid().ToString(), + Id = petId, UserId = userId.ToString(), Name = petRequest.Name, Class = petRequest.Class, Level = 1, Stats = PetStats.BuildFromClass(petRequest.Class), Resources = new Resources(), - ActionSince = DateTime.UtcNow, - PetAction = PetActionGather.IDLE, + GatherActionSince = DateTime.UtcNow, + PetGatherAction = PetActionGather.IDLE, IsDead = false }; @@ -46,21 +47,63 @@ namespace pet_companion_api.Services throw new Exception("Pet not found"); } - if (actionRequest.Action.HasValue) + if (actionRequest.BasicAction.HasValue) { - // not implemented - } - else if (actionRequest.PetActionGather.HasValue) - { - pet.PetAction = actionRequest.PetActionGather.Value; - pet.ActionSince = DateTime.UtcNow; + var currentTime = DateTime.UtcNow; + if (pet.PetBasicAction != PetBasicAction.UNKNOWN && + currentTime < pet.BasicActionCooldown.ToUniversalTime().AddSeconds(10)) + { + throw new Exception("Pet is still on cooldown"); + } - return petRepository.UpdatePetAction(pet); + pet.BasicActionCooldown = DateTime.UtcNow.AddMinutes(GetCooldownForBasicAction(actionRequest.BasicAction.Value)); + pet.PetBasicAction = actionRequest.BasicAction.Value; + + switch (actionRequest.BasicAction.Value) + { + case PetBasicAction.FEED: + pet.Resources.Food -= 1; + pet.IncrementStrength(1); + break; + case PetBasicAction.SLEEP: + pet.IncrementIntelligence(1); + pet.IncrementStrength(1); + break; + case PetBasicAction.PLAY: + pet.Resources.Junk -= 1; + pet.IncrementCharisma(1); + break; + } + + return petRepository.UpdatePetBasicAction(pet); + } + else if (actionRequest.GatherAction.HasValue) + { + pet.PetGatherAction = actionRequest.GatherAction.Value; + pet.GatherActionSince = DateTime.UtcNow; + + return petRepository.UpdatePetGatherAction(pet); } return pet; } + // returns in minutes + private int GetCooldownForBasicAction(PetBasicAction value) + { + switch (value) + { + case PetBasicAction.FEED: + return 5; + case PetBasicAction.PLAY: + return 10; + case PetBasicAction.SLEEP: + return 15; + default: + return 0; + } + } + public Resources GetGatheredResources(string petId, string userId) { var pet = petRepository.GetPetById(petId, userId); @@ -70,7 +113,7 @@ namespace pet_companion_api.Services throw new Exception("Pet not found"); } - return _petClassService.CalculateGatheredResources(pet.Stats, pet.Level, pet.PetAction, pet.ActionSince); + return _petClassService.CalculateGatheredResources(pet.Stats, pet.Level, pet.PetGatherAction, pet.GatherActionSince); } public Pet UpdatePetResources(string petId, string userId) @@ -81,13 +124,13 @@ namespace pet_companion_api.Services throw new Exception("Pet not found"); } - var gatheredResources = _petClassService.CalculateGatheredResources(pet.Stats, pet.Level, pet.PetAction, pet.ActionSince); + var gatheredResources = _petClassService.CalculateGatheredResources(pet.Stats, pet.Level, pet.PetGatherAction, pet.GatherActionSince); pet.Resources.Wisdom += gatheredResources.Wisdom; pet.Resources.Gold += gatheredResources.Gold; pet.Resources.Food += gatheredResources.Food; pet.Resources.Junk += gatheredResources.Junk; - pet.ActionSince = DateTime.UtcNow; + pet.GatherActionSince = DateTime.UtcNow; return petRepository.UpdatePetResources(pet); }