feat: vampire expansion chunk 4 — API routes and services

Adds six new routes (vampire-boss, vampire-upgrade, vampire-craft,
vampire-explore, siring, vampire-awakening) with matching siring and
awakening services, and all necessary request/response types.
This commit is contained in:
2026-04-16 09:48:50 -07:00
committed by Naomi Carrigan
parent 7f43dc725e
commit 8fa5d12f05
11 changed files with 2026 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
/**
* @file Awakening service handling eligibility checks and post-awakening state building.
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/* eslint-disable stylistic/max-len -- Service logic requires long lines */
import { initialVampireState } from "../data/initialState.js";
import { defaultVampireAwakeningUpgrades } from "../data/vampireAwakeningUpgrades.js";
import type { AwakeningData, GameState } from "@elysium/types";
/**
* The ID of the final vampire boss whose defeat triggers eligibility for awakening.
*/
const finalVampireBossId = "eternal_darkness";
const getCategoryMultiplier = (
purchasedIds: Array<string>,
category: string,
): number => {
return defaultVampireAwakeningUpgrades.filter((upgrade) => {
return upgrade.category === category && purchasedIds.includes(upgrade.id);
}).reduce((mult, upgrade) => {
return mult * upgrade.multiplier;
}, 1);
};
/**
* Computes all five soul shard multipliers from the purchased awakening upgrade IDs.
* @param purchasedUpgradeIds - The array of purchased awakening upgrade IDs.
* @returns An object containing all five soul shard multiplier values.
*/
const computeAwakeningMultipliers = (
purchasedUpgradeIds: Array<string>,
): Omit<AwakeningData, "count" | "soulShards" | "purchasedUpgradeIds"> => {
return {
soulShardsBloodMultiplier: getCategoryMultiplier(purchasedUpgradeIds, "blood"),
soulShardsCombatMultiplier: getCategoryMultiplier(purchasedUpgradeIds, "combat"),
soulShardsMetaMultiplier: getCategoryMultiplier(purchasedUpgradeIds, "soulshards_meta"),
soulShardsSiringIchorMultiplier: getCategoryMultiplier(purchasedUpgradeIds, "siring_ichor"),
soulShardsSiringThresholdMultiplier: getCategoryMultiplier(purchasedUpgradeIds, "siring_threshold"),
};
};
/**
* Returns true if the player is eligible to awaken:
* the final vampire boss must have been defeated.
* @param state - The current game state.
* @returns Whether the player is eligible for awakening.
*/
const isEligibleForAwakening = (state: GameState): boolean => {
// eslint-disable-next-line capitalized-comments -- v8 ignore
/* v8 ignore next 3 -- @preserve */
if (state.vampire === undefined) {
return false;
}
return state.vampire.bosses.some((boss) => {
return boss.id === finalVampireBossId && boss.status === "defeated";
});
};
/**
* Calculates the soul shards yield from an awakening.
* Formula: MAX(1, FLOOR(SQRT(siringCount) * metaMultiplier)).
* @param siringCount - The number of sirings completed.
* @param metaMultiplier - Multiplier from soul shard meta upgrades applied to yield.
* @returns The soul shards earned.
*/
const calculateSoulShardsYield = (
siringCount: number,
metaMultiplier: number,
): number => {
return Math.max(1, Math.floor(Math.sqrt(siringCount) * metaMultiplier));
};
/**
* Builds the updated vampire state after an awakening reset.
* Resets the current run including siring data (bosses, quests, thralls, upgrades, zones, siring data).
* Preserves: equipment, achievements, awakening data (updated), eternal sovereignty, lifetime stats.
* @param state - The current game state before awakening.
* @returns The soul shards earned and the updated vampire state.
*/
const buildPostAwakeningState = (
state: GameState,
): { soulShardsEarned: number; updatedVampire: NonNullable<GameState["vampire"]> } => {
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Caller must ensure vampire exists */
const vampire = state.vampire as NonNullable<GameState["vampire"]>;
const metaMultiplier = vampire.awakening.soulShardsMetaMultiplier;
const soulShardsEarned = calculateSoulShardsYield(vampire.siring.count, metaMultiplier);
const updatedCount = vampire.awakening.count + 1;
const updatedSoulShards = vampire.awakening.soulShards + soulShardsEarned;
const updatedPurchasedIds = vampire.awakening.purchasedUpgradeIds;
const updatedMultipliers = computeAwakeningMultipliers(updatedPurchasedIds);
const updatedAwakening: AwakeningData = {
count: updatedCount,
purchasedUpgradeIds: updatedPurchasedIds,
soulShards: updatedSoulShards,
...updatedMultipliers,
};
const freshVampire = initialVampireState();
const updatedVampire: NonNullable<GameState["vampire"]> = {
...freshVampire,
achievements: vampire.achievements,
awakening: updatedAwakening,
bosses: freshVampire.bosses.map((b) => {
const existing = vampire.bosses.find((vb) => {
return vb.id === b.id;
});
return {
...b,
bountyIchorClaimed: existing?.bountyIchorClaimed ?? false,
};
}),
equipment: vampire.equipment,
eternalSovereignty: vampire.eternalSovereignty,
lastTickAt: Date.now(),
lifetimeBloodEarned: vampire.lifetimeBloodEarned,
lifetimeBossesDefeated: vampire.lifetimeBossesDefeated,
lifetimeQuestsCompleted: vampire.lifetimeQuestsCompleted,
totalBloodEarned: 0,
};
return { soulShardsEarned, updatedVampire };
};
export {
buildPostAwakeningState,
calculateSoulShardsYield,
computeAwakeningMultipliers,
isEligibleForAwakening,
};