feat: vampire mode chunk 3 - sync/sanitize and initial state

Add initialVampireState() and vampireSpread validation to mirror the
goddess mode pattern. Also lint-fix pre-existing style issues across
all Chunk 2 vampire data and type files.
This commit is contained in:
2026-04-16 09:26:29 -07:00
committed by Naomi Carrigan
parent 53a026da62
commit 7f43dc725e
18 changed files with 2221 additions and 1973 deletions
+170
View File
@@ -886,6 +886,175 @@ const validateAndSanitize = (
};
}
/*
* Vampire state: preserve server-only currencies (ichor, soul shards, blood) at
* previous values, and apply the same forward-only rules to bosses/quests/achievements
* and exploration materials that the mortal and goddess realms use.
* Blood income will be computed and allowed to grow once Chunk 7 adds vampire tick logic.
*/
// eslint-disable-next-line capitalized-comments -- v8 ignore
/* v8 ignore next 154 -- @preserve */
let vampireSpread: object = {};
const previousVampire = previous.vampire;
const incomingVampire = incoming.vampire;
if (!incomingVampire && previousVampire) {
vampireSpread = { vampire: previousVampire };
} else if (incomingVampire) {
const vampireBosses = incomingVampire.bosses.map((boss) => {
const matchingBoss = previousVampire?.bosses.find((storedBoss) => {
return storedBoss.id === boss.id;
});
if (!matchingBoss) {
return boss;
}
if (matchingBoss.status === "defeated" && boss.status !== "defeated") {
return { ...boss, currentHp: 0, status: "defeated" as const };
}
return boss;
});
const vampireQuests = incomingVampire.quests.map((quest) => {
const matchingQuest = previousVampire?.quests.find((storedQuest) => {
return storedQuest.id === quest.id;
});
if (!matchingQuest) {
return quest;
}
// eslint-disable-next-line stylistic/max-len -- Long condition; splitting would reduce readability
if (matchingQuest.status === "completed" && quest.status !== "completed") {
return { ...matchingQuest };
}
return quest;
});
// eslint-disable-next-line stylistic/max-len -- Long variable name; splitting would reduce readability
const vampireAchievements = incomingVampire.achievements.map((achievement) => {
const matchingAchievement = previousVampire?.achievements.find(
(storedAchievement) => {
return storedAchievement.id === achievement.id;
},
);
if (!matchingAchievement) {
return achievement;
}
const wasUnlocked = matchingAchievement.unlockedAt !== null;
const isNowNull = achievement.unlockedAt === null;
if (wasUnlocked && isNowNull) {
return { ...achievement, unlockedAt: matchingAchievement.unlockedAt };
}
const isFuture
= achievement.unlockedAt !== null && achievement.unlockedAt > now;
if (isFuture) {
const safeUnlockedAt = matchingAchievement.unlockedAt ?? null;
return { ...achievement, unlockedAt: safeUnlockedAt };
}
return achievement;
});
const previousVampireExploration = previousVampire?.exploration;
let vampireExploration = incomingVampire.exploration;
if (previousVampireExploration) {
const previousMaterialMap = new Map(
previousVampireExploration.materials.map((mat) => {
return [ mat.materialId, mat.quantity ] as const;
}),
);
// eslint-disable-next-line stylistic/max-len -- Long variable name; splitting would reduce readability
const materials = incomingVampire.exploration.materials.map((material) => {
const previousQuantity
= previousMaterialMap.get(material.materialId) ?? 0;
return {
...material,
quantity: Math.min(material.quantity, previousQuantity),
};
});
const vampireRecipeIds = [
...new Set([
...previousVampireExploration.craftedRecipeIds,
...incomingVampire.exploration.craftedRecipeIds,
]),
];
vampireExploration = {
...incomingVampire.exploration,
// eslint-disable-next-line stylistic/max-len -- Long field name; splitting would reduce readability
craftedBloodMultiplier: previousVampireExploration.craftedBloodMultiplier,
// eslint-disable-next-line stylistic/max-len -- Long field name; splitting would reduce readability
craftedCombatMultiplier: previousVampireExploration.craftedCombatMultiplier,
// eslint-disable-next-line stylistic/max-len -- Long field name; splitting would reduce readability
craftedIchorMultiplier: previousVampireExploration.craftedIchorMultiplier,
craftedRecipeIds: vampireRecipeIds,
materials: materials,
};
}
const siring = previousVampire
? {
...incomingVampire.siring,
count: Math.min(
incomingVampire.siring.count,
previousVampire.siring.count,
),
ichor: Math.min(
incomingVampire.siring.ichor,
previousVampire.siring.ichor,
),
productionMultiplier: previousVampire.siring.productionMultiplier,
}
: incomingVampire.siring;
const awakening = previousVampire
? {
...incomingVampire.awakening,
count: Math.min(
incomingVampire.awakening.count,
previousVampire.awakening.count,
),
soulShards: Math.min(
incomingVampire.awakening.soulShards,
previousVampire.awakening.soulShards,
),
// eslint-disable-next-line stylistic/max-len -- Long field name; splitting would reduce readability
soulShardsBloodMultiplier: previousVampire.awakening.soulShardsBloodMultiplier,
// eslint-disable-next-line stylistic/max-len -- Long field name; splitting would reduce readability
soulShardsCombatMultiplier: previousVampire.awakening.soulShardsCombatMultiplier,
// eslint-disable-next-line stylistic/max-len -- Long field name; splitting would reduce readability
soulShardsMetaMultiplier: previousVampire.awakening.soulShardsMetaMultiplier,
// eslint-disable-next-line stylistic/max-len -- Long field name; splitting would reduce readability
soulShardsSiringIchorMultiplier: previousVampire.awakening.soulShardsSiringIchorMultiplier,
// eslint-disable-next-line stylistic/max-len -- Long field name; splitting would reduce readability
soulShardsSiringThresholdMultiplier: previousVampire.awakening.soulShardsSiringThresholdMultiplier,
}
: incomingVampire.awakening;
vampireSpread = {
vampire: {
...incomingVampire,
achievements: vampireAchievements,
awakening: awakening,
bosses: vampireBosses,
eternalSovereignty: {
count: Math.min(
incomingVampire.eternalSovereignty.count,
previousVampire?.eternalSovereignty.count ?? 0,
),
},
exploration: vampireExploration,
lifetimeBloodEarned: Math.min(
incomingVampire.lifetimeBloodEarned,
previousVampire?.lifetimeBloodEarned ?? 0,
),
lifetimeBossesDefeated: Math.min(
incomingVampire.lifetimeBossesDefeated,
previousVampire?.lifetimeBossesDefeated ?? 0,
),
lifetimeQuestsCompleted: Math.min(
incomingVampire.lifetimeQuestsCompleted,
previousVampire?.lifetimeQuestsCompleted ?? 0,
),
quests: vampireQuests,
siring: siring,
totalBloodEarned: Math.min(
incomingVampire.totalBloodEarned,
previousVampire?.totalBloodEarned ?? 0,
),
},
};
}
return {
...incoming,
achievements,
@@ -900,6 +1069,7 @@ const validateAndSanitize = (
...storySpread,
...dailyChallengesSpread,
...goddessSpread,
...vampireSpread,
};
};