Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { Stat } from '../constants/stats.js';
import type { FortuneUpgrade } from '../constants/upgrades.js';
import { resolveDropEffects } from '../effects/resolver.js';
import type { DropTag } from '../effects/types.js';
import { PestFarmingPhase, type PestFarmingPlayer } from '../player/pestfarmingplayer.js';
import type { FarmingPet } from '../fortune/farmingpet.js';
import { createPestFarmingPlayer, PestFarmingPhase, type PestFarmingPlayer } from '../player/pestfarmingplayer.js';
import type { DetailedDropsFromEffectsResult } from '../util/ratecalc-effects.js';
import {
calculatePestCropDropAmount,
Expand Down Expand Up @@ -32,6 +33,7 @@ import type {
PestRateValuationDelta,
PestRateValuationResult,
PestSpawnDistribution,
PetsCoinsPerHour,
} from './pest-rate-types.js';

const DEFAULT_INTERVAL_SECONDS = 3600;
Expand Down Expand Up @@ -251,6 +253,44 @@ export class PestFarmingRateCalculator {
return bestId;
}

getPetsCoinsPerHour(pets: FarmingPet[], phase: PestFarmingPhase): PetsCoinsPerHour[] {
const petRates = pets.map((pet) => {
const petId = pet.pet.uuid;
if (!petId) return { pet, coinsPerHour: 0 };

const phaseLoadouts = {
...this.player.phaseLoadouts,
[phase]: {
...this.player.phaseLoadouts[phase],
petId: petId,
},
};

// Calculate rates using a new calculator, skipping pet sort to avoid recursion
const calculator = new PestFarmingRateCalculator({
player: createPestFarmingPlayer({
...this.player.options,
phaseLoadouts,
}),
options: this.options,
priceBook: this.priceBook,
});
const result = calculator.calculate();
const coinsPerHour = result.valuation.coinsPerHour;

return { pet, coinsPerHour };
});

// Sort petRates by coinsPerHour descending
return petRates.sort((a, b) => b.coinsPerHour - a.coinsPerHour);
}

getBestPhasePetId(phase: PestFarmingPhase): string | undefined {
const phasePlayer = this.player.getPhasePlayer(phase);
const petRates = this.getPetsCoinsPerHour(phasePlayer.pets, phase);
return petRates[0]?.pet.pet.uuid ?? undefined;
}

private calculateSpawnArmorSetRate(armorSetId: string): number {
const player = this.player.clone();
player.setPhaseArmorSet(PestFarmingPhase.Spawn, armorSetId);
Expand Down
6 changes: 6 additions & 0 deletions packages/farming-weight/src/pests/pest-rate-types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Crop } from '../constants/crops.js';
import type { Pest } from '../constants/pests.js';
import type { FortuneUpgrade } from '../constants/upgrades.js';
import type { FarmingPet } from '../fortune/farmingpet.js';
import type { PestFarmingPhase, PestFarmingPlayer } from '../player/pestfarmingplayer.js';
import type { DetailedDropsFromEffectsDelta, UpgradeRateImpact } from '../player/player.js';
import type { DetailedDropsFromEffectsResult } from '../util/ratecalc-effects.js';
Expand Down Expand Up @@ -234,3 +235,8 @@ export interface PestFarmingUpgradeRateImpact
upgradeKey: string;
valuationDelta: PestRateValuationDelta;
}

export interface PetsCoinsPerHour {
pet: FarmingPet;
coinsPerHour: number;
}
7 changes: 5 additions & 2 deletions packages/farming-weight/src/player/pestfarmingplayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export interface PestArmorSetLoadout {

export interface PestPhaseLoadout {
armorSetId: string;
petId?: string;
petId: string | undefined;
}

export const PEST_MAIN_ARMOR_SET_ID = 'main';
Expand Down Expand Up @@ -674,7 +674,10 @@ export class PestFarmingPlayer {
const armorSetIds = itemUuid ? this.getArmorSetIdsForUuid(itemUuid) : [];
if (armorSetIds.length > 0) {
for (const phaseKey of PEST_FARMING_PHASES) {
if (armorSetIds.includes(this.phaseLoadouts[phaseKey].armorSetId)) {
if (
this.phaseLoadouts[phaseKey].armorSetId &&
armorSetIds.includes(this.phaseLoadouts[phaseKey].armorSetId)
) {
this.phases[phaseKey].applyUpgrade(upgrade);
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/lib/stores/ratesData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Pest,
type PestAttractionSettings,
PestFarmingPhase,
type PestPhaseLoadout,
ZorroMode,
type FarmingTool,
type PestCycleSettings,
Expand All @@ -20,7 +21,7 @@ export type PestFarmingTimeOfDay = 'day' | 'night';

export interface PestFarmingData {
selectedCrop?: string;
phaseLoadouts: Partial<Record<PestFarmingPhase, { armorSetId: string }>>;
phaseLoadouts: Partial<Record<PestFarmingPhase, PestPhaseLoadout>>;
sprayedPlot: boolean;
pesthunterAccessoryEnabled: boolean;
timeOfDay: PestFarmingTimeOfDay;
Expand Down Expand Up @@ -141,8 +142,9 @@ function normalizePestFarmingData(data?: Partial<PestFarmingData>): PestFarmingD
if (!isLegacyDefaultPhaseLoadouts(data?.phaseLoadouts)) {
for (const phase of [PestFarmingPhase.Farm, PestFarmingPhase.Spawn, PestFarmingPhase.Kill]) {
const armorSetId = data?.phaseLoadouts?.[phase]?.armorSetId;
const petId = data?.phaseLoadouts?.[phase]?.petId;
if (armorSetId && armorSetIds.has(armorSetId)) {
phaseLoadouts[phase] = { armorSetId };
phaseLoadouts[phase] = { armorSetId, petId };
}
}
}
Expand Down
21 changes: 10 additions & 11 deletions src/routes/(main)/@[id=id]/[[profile]]/pest-farming/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -252,26 +252,25 @@
onValueChange={(value) =>
value && pest.selectPhasePet(pest.activePhase, value)}
>
{#each pest.pets
.filter((pet) => !!pet.pet.uuid)
.sort((a, b) => pest.getPetRateImpact(b, pest.activePhase) - pest.getPetRateImpact(a, pest.activePhase)) as pet, i (pet.pet.uuid ?? i)}
{@const petRateDelta = pest.getPetRateImpact(pet, pest.activePhase)}
<DropdownMenu.RadioItem value={pet.pet.uuid ?? ''}>
{#each pest
.getPhasePets(pest.activePhase)
.filter((petData) => !!petData.pet.pet.uuid) as petData, i (petData.pet.pet.uuid ?? i)}
<DropdownMenu.RadioItem value={petData.pet.pet.uuid ?? ''}>
<div class="flex flex-row items-center gap-2">
<ItemRender
skyblockId={pet.pet.type ?? ''}
skyblockId={petData.pet.pet.type ?? ''}
pet
class="size-6"
/>
<FormattedText text={pet.getFormattedName()} />
{#if petRateDelta !== 0}
<FormattedText text={petData.pet.getFormattedName()} />
{#if petData.coinsPerHour !== 0}
<span
class="{petRateDelta > 0
class="{petData.coinsPerHour > 0
? 'dark:text-completed'
: 'text-muted-foreground'} ml-auto text-xs whitespace-nowrap tabular-nums"
>
{petRateDelta > 0 ? '+' : ''}{formatRate(
petRateDelta
{petData.coinsPerHour > 0 ? '+' : ''}{formatRate(
petData.coinsPerHour
)}
</span>
{/if}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
type PestPhaseLoadout,
type PestRateItemPrice,
type PestRatePriceBook,
type PetsCoinsPerHour,
type StatBreakdown,
type TemporaryFarmingFortune,
type UpgradeTreeNode,
Expand Down Expand Up @@ -449,20 +450,22 @@ export class PestFarmingPageContext {
$effect(() => this.#syncExternalState());
$effect(() => this.#syncSelectedCrop());
$effect(() => this.#syncVacuumSelection());
$effect(() => this.#syncDefaultSpawnArmorSelection());
$effect(() => this.#syncDefaultPhaseLoadouts());

onMount(() => this.#restoreSavedCrop());
}

refreshPestPlayer() {
refreshPestPlayer(skip_default = false) {
let player = createPestFarmingPlayer(this.options);
const phaseLoadouts = this.#getRateSelectedDefaultPhaseLoadouts(player);
if (phaseLoadouts) {
this.options = {
...this.options,
phaseLoadouts,
} as PestFarmingPlayerOptions;
player = createPestFarmingPlayer(this.options);
if (!skip_default) {
const phaseLoadouts = this.#getRateSelectedDefaultPhaseLoadouts(player);
if (phaseLoadouts) {
this.options = {
...this.options,
phaseLoadouts,
} as PestFarmingPlayerOptions;
player = createPestFarmingPlayer(this.options);
}
}
this.pestPlayer = player;
this.#syncSessionSelectionsFromPestPlayer();
Expand All @@ -484,7 +487,8 @@ export class PestFarmingPageContext {
[PestFarmingPhase.Farm, PestFarmingPhase.Spawn, PestFarmingPhase.Kill]
.map((phase) => {
const armorSetId = loadouts[phase]?.armorSetId;
return armorSetId ? [phase, { armorSetId }] : undefined;
const petId = loadouts[phase]?.petId;
return armorSetId ? [phase, { armorSetId, petId }] : undefined;
})
.filter((entry): entry is [PestFarmingPhase, PestPhaseLoadout] => entry !== undefined)
);
Expand All @@ -494,9 +498,18 @@ export class PestFarmingPageContext {
loadouts: Record<PestFarmingPhase, PestPhaseLoadout>
): PestFarmingData['phaseLoadouts'] {
return {
[PestFarmingPhase.Farm]: { armorSetId: loadouts[PestFarmingPhase.Farm].armorSetId },
[PestFarmingPhase.Spawn]: { armorSetId: loadouts[PestFarmingPhase.Spawn].armorSetId },
[PestFarmingPhase.Kill]: { armorSetId: loadouts[PestFarmingPhase.Kill].armorSetId },
[PestFarmingPhase.Farm]: {
armorSetId: loadouts[PestFarmingPhase.Farm].armorSetId,
petId: loadouts[PestFarmingPhase.Farm].petId,
},
[PestFarmingPhase.Spawn]: {
armorSetId: loadouts[PestFarmingPhase.Spawn].armorSetId,
petId: loadouts[PestFarmingPhase.Spawn].petId,
},
[PestFarmingPhase.Kill]: {
armorSetId: loadouts[PestFarmingPhase.Kill].armorSetId,
petId: loadouts[PestFarmingPhase.Kill].petId,
},
};
}

Expand All @@ -519,34 +532,45 @@ export class PestFarmingPageContext {
this.selectedVacuumId = this.pestPlayer.selectedVacuum?.item.uuid ?? this.selectedVacuumId;
}

#syncDefaultSpawnArmorSelection(): void {
#syncDefaultPhaseLoadouts(): void {
void this.pestRatePriceBook.version;
if (this.rates.pestFarming.phaseLoadouts[PestFarmingPhase.Spawn]?.armorSetId) return;

const phaseLoadouts = this.#getRateSelectedDefaultPhaseLoadouts(this.pestPlayer);
if (!phaseLoadouts) return;

this.options = {
...this.options,
phaseLoadouts,
} as PestFarmingPlayerOptions;
untrack(() => this.refreshPestPlayer());
untrack(() => this.refreshPestPlayer(true));
}

#getRateSelectedDefaultPhaseLoadouts(
player: PestFarmingPlayer
): Record<PestFarmingPhase, PestPhaseLoadout> | undefined {
const candidates = this.#getDefaultSpawnArmorCandidateIds(player);
if (!candidates) return;

const bestId = this.#createRateCalculator(player).getBestSpawnPhaseArmorSetId(candidates);
if (!bestId || player.phaseLoadouts[PestFarmingPhase.Spawn]?.armorSetId === bestId) return;

const bestId = candidates
? this.#createRateCalculator(player).getBestSpawnPhaseArmorSetId(candidates)
: undefined;
return {
...player.phaseLoadouts,
[PestFarmingPhase.Farm]: {
...player.phaseLoadouts[PestFarmingPhase.Farm],
...(this.#getDefaultPhasePetId(player, PestFarmingPhase.Farm) !== undefined && {
petId: this.#getDefaultPhasePetId(player, PestFarmingPhase.Farm),
}),
},
[PestFarmingPhase.Spawn]: {
...player.phaseLoadouts[PestFarmingPhase.Spawn],
armorSetId: bestId,
...(bestId !== undefined && { armorSetId: bestId }),
...(this.#getDefaultPhasePetId(player, PestFarmingPhase.Spawn) !== undefined && {
petId: this.#getDefaultPhasePetId(player, PestFarmingPhase.Spawn),
}),
},
[PestFarmingPhase.Kill]: {
...player.phaseLoadouts[PestFarmingPhase.Kill],
...(this.#getDefaultPhasePetId(player, PestFarmingPhase.Kill) !== undefined && {
petId: this.#getDefaultPhasePetId(player, PestFarmingPhase.Kill),
}),
},
};
}
Expand All @@ -563,6 +587,11 @@ export class PestFarmingPageContext {
return [mainId, spawnId];
}

#getDefaultPhasePetId(player: PestFarmingPlayer, phase: PestFarmingPhase): string | undefined {
if (this.rates.pestFarming.phaseLoadouts[phase]?.petId) return undefined;
return this.#createRateCalculator(player).getBestPhasePetId(phase);
}

trackPestVersion(): number {
return this.pestVersion;
}
Expand Down Expand Up @@ -673,6 +702,10 @@ export class PestFarmingPageContext {
return breakdown;
}

getPhasePets(phase: PestFarmingPhase): PetsCoinsPerHour[] {
return this.#createRateCalculator(this.pestPlayer).getPetsCoinsPerHour(this.pets, phase);
}

getPetRateImpact(pet: FarmingPet, phase: PestFarmingPhase): number {
const uuid = pet.pet.uuid;
if (!uuid) return 0;
Expand Down Expand Up @@ -846,6 +879,7 @@ export class PestFarmingPageContext {
};
this.phaseLoadouts = phaseLoadouts;
this.options = { ...this.options, phaseLoadouts } as PestFarmingPlayerOptions;
this.#updatePestFarmingData({ phaseLoadouts: this.#getPersistablePhaseLoadouts(phaseLoadouts) });
this.pestVersion++;
trackAnalytics('pest_farming.phase_pet_selected', { phase });
}
Expand Down
Loading