From 9ba7543bb04a748faab3666322109ef1caf7a8c0 Mon Sep 17 00:00:00 2001 From: Vadym Abakumov Date: Mon, 9 Mar 2026 00:09:50 +0100 Subject: [PATCH 1/2] fix: correct pet needs decay accumulation and reduce expedition check verbosity - snap needsUpdatedAt to last interval boundary instead of wall clock so sub-interval remainder carries over to the next run - return intervals from calculateDecayedNeeds to support snapping - condense urgent needs check in startExpedition using PET_NEED_KEYS.some Co-Authored-By: Claude Sonnet 4.6 --- .claude/settings.local.json | 3 ++- apps/server/src/pets/pets.service.ts | 23 +++++++++++------------ packages/types/src/pet/utils.ts | 6 +++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index b1ca533..25fec77 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -137,7 +137,8 @@ "Bash(npm show:*)", "Bash(git:*)", "Bash(cp:*)", - "Bash(/home/mozyonee/Projects/widgetable/apps/server/src/storage/storage.config.ts:*)" + "Bash(/home/mozyonee/Projects/widgetable/apps/server/src/storage/storage.config.ts:*)", + "mcp__github__create_branch" ], "deny": [], "ask": [] diff --git a/apps/server/src/pets/pets.service.ts b/apps/server/src/pets/pets.service.ts index 9d51941..d5d123f 100644 --- a/apps/server/src/pets/pets.service.ts +++ b/apps/server/src/pets/pets.service.ts @@ -18,6 +18,7 @@ import { MAX_EXPEDITION_SLOTS_RATIO, PET_NEED_KEYS, PET_THRESHOLDS, + PET_UPDATE_INTERVAL, PetType, PetUpdate, } from '@widgetable/types'; @@ -145,13 +146,17 @@ export class PetsService extends BaseService { if (pet.isEgg) return pet; - const lastUpdatedAt = pet.needsUpdatedAt ?? pet.updatedAt ?? new Date(); - const { needs: updatedNeeds, changed } = calculateDecayedNeeds(pet.needs, lastUpdatedAt, new Date()); + const now = new Date(); + const lastUpdatedAt = pet.needsUpdatedAt ?? pet.updatedAt ?? now; + const { needs: updatedNeeds, changed, intervals } = calculateDecayedNeeds(pet.needs, lastUpdatedAt, now); if (!changed) return pet; + // Snap to the last completed interval boundary so the remainder carries over to the next run + const snappedUpdatedAt = new Date(lastUpdatedAt.getTime() + intervals * PET_UPDATE_INTERVAL); + const updatedPet = await this.petModel - .findByIdAndUpdate(pet._id, { needs: updatedNeeds, needsUpdatedAt: new Date() }, { new: true }) + .findByIdAndUpdate(pet._id, { needs: updatedNeeds, needsUpdatedAt: snappedUpdatedAt }, { new: true }) .populate('parents', this.PARENT_FIELDS); return updatedPet || pet; @@ -163,16 +168,10 @@ export class PetsService extends BaseService { if (pet.isEgg) throw new UnprocessableEntityException(); if (pet.isOnExpedition) throw new ConflictException(); - if (pet.needs) { - const urgentNeeds: string[] = []; - if (pet.needs.hunger < PET_THRESHOLDS.URGENT) urgentNeeds.push('hunger'); - if (pet.needs.thirst < PET_THRESHOLDS.URGENT) urgentNeeds.push('thirst'); - if (pet.needs.hygiene < PET_THRESHOLDS.URGENT) urgentNeeds.push('hygiene'); - if (pet.needs.energy < PET_THRESHOLDS.URGENT) urgentNeeds.push('energy'); - if (pet.needs.toilet < PET_THRESHOLDS.URGENT) urgentNeeds.push('toilet'); - - if (urgentNeeds.length > 0) throw new BadRequestException(); + if (pet.needs && PET_NEED_KEYS.some((key) => pet.needs[key] < PET_THRESHOLDS.URGENT)) { + throw new BadRequestException(); } + // Only count pets still away, not returned but unclaimed const now = new Date(); const allPets = await this.getPetsForUser(userId); diff --git a/packages/types/src/pet/utils.ts b/packages/types/src/pet/utils.ts index 6e27818..6fda581 100644 --- a/packages/types/src/pet/utils.ts +++ b/packages/types/src/pet/utils.ts @@ -8,11 +8,11 @@ export const calculateDecayedNeeds = ( needs: PetNeeds, lastUpdatedAt: Date, now: Date, -): { needs: PetNeeds; changed: boolean } => { +): { needs: PetNeeds; changed: boolean; intervals: number } => { const timeDiff = now.getTime() - lastUpdatedAt.getTime(); const intervals = Math.floor(timeDiff / PET_UPDATE_INTERVAL); - if (intervals <= 0) return { needs, changed: false }; + if (intervals <= 0) return { needs, changed: false, intervals: 0 }; const updated = { ...needs }; for (const key of PET_NEED_KEYS) { @@ -20,7 +20,7 @@ export const calculateDecayedNeeds = ( updated[key] = Math.max(0, Math.min(100, needs[key] - decrease)); } - return { needs: updated, changed: true }; + return { needs: updated, changed: true, intervals }; }; const BASE_EXP = 50; From b0e33572955c972be46a1ae4298d527aa93a491e Mon Sep 17 00:00:00 2001 From: Vadym Abakumov Date: Mon, 9 Mar 2026 00:58:14 +0100 Subject: [PATCH 2/2] fix: extract plain needs object before decay calculation to avoid Mongoose subdoc serialization issue Co-Authored-By: Claude Sonnet 4.6 --- .claude/settings.local.json | 3 ++- apps/server/src/pets/pets.service.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 25fec77..3b9ed61 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -138,7 +138,8 @@ "Bash(git:*)", "Bash(cp:*)", "Bash(/home/mozyonee/Projects/widgetable/apps/server/src/storage/storage.config.ts:*)", - "mcp__github__create_branch" + "mcp__github__create_branch", + "mcp__github__update_pull_request" ], "deny": [], "ask": [] diff --git a/apps/server/src/pets/pets.service.ts b/apps/server/src/pets/pets.service.ts index d5d123f..6557177 100644 --- a/apps/server/src/pets/pets.service.ts +++ b/apps/server/src/pets/pets.service.ts @@ -19,6 +19,7 @@ import { PET_NEED_KEYS, PET_THRESHOLDS, PET_UPDATE_INTERVAL, + PetNeeds, PetType, PetUpdate, } from '@widgetable/types'; @@ -148,7 +149,8 @@ export class PetsService extends BaseService { const now = new Date(); const lastUpdatedAt = pet.needsUpdatedAt ?? pet.updatedAt ?? now; - const { needs: updatedNeeds, changed, intervals } = calculateDecayedNeeds(pet.needs, lastUpdatedAt, now); + const plainNeeds = Object.fromEntries(PET_NEED_KEYS.map((key) => [key, pet.needs[key]])) as PetNeeds; + const { needs: updatedNeeds, changed, intervals } = calculateDecayedNeeds(plainNeeds, lastUpdatedAt, now); if (!changed) return pet;