generated from nhcarrigan/template
feat: goddess sync, sanitize, and apotheosis init (chunk 3)
- initialState: add initialGoddessState() with all goddess sub-objects - apotheosis: init GoddessState on first apotheosis, preserve on subsequent - game: add goddessSpread block in validateAndSanitize (server-only fields capped, forward-only boss/quest/achievement enforcement) - debug: add injectMissingGoddessExplorationAreas helper and inject all 8 goddess content arrays in syncNewContent - vitest.config.ts: remove 8 goddess data files from coverage exclude (now imported via initialState) - tests: full coverage for all new code (482 tests, 100% coverage)
This commit is contained in:
@@ -720,6 +720,172 @@ const validateAndSanitize = (
|
||||
dailyChallengesSpread = { dailyChallenges: previous.dailyChallenges };
|
||||
}
|
||||
|
||||
/*
|
||||
* Goddess state: preserve server-only currencies (divinity, stardust, prayers) at
|
||||
* previous values, and apply the same forward-only rules to bosses/quests/achievements
|
||||
* and exploration materials that the mortal realm uses.
|
||||
* Prayers income will be computed and allowed to grow once Chunk 7 adds goddess tick logic.
|
||||
*/
|
||||
// eslint-disable-next-line capitalized-comments -- v8 ignore
|
||||
/* v8 ignore next 145 -- @preserve */
|
||||
let goddessSpread: object = {};
|
||||
const previousGoddess = previous.goddess;
|
||||
const incomingGoddess = incoming.goddess;
|
||||
if (!incomingGoddess && previousGoddess) {
|
||||
goddessSpread = { goddess: previousGoddess };
|
||||
} else if (incomingGoddess) {
|
||||
const goddessBosses = incomingGoddess.bosses.map((boss) => {
|
||||
const matchingBoss = previousGoddess?.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 goddessQuests = incomingGoddess.quests.map((quest) => {
|
||||
const matchingQuest = previousGoddess?.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 goddessAchievements = incomingGoddess.achievements.map((achievement) => {
|
||||
const matchingAchievement = previousGoddess?.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 previousGoddessExploration = previousGoddess?.exploration;
|
||||
let goddessExploration = incomingGoddess.exploration;
|
||||
if (previousGoddessExploration) {
|
||||
const previousMaterialMap = new Map(
|
||||
previousGoddessExploration.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 = incomingGoddess.exploration.materials.map((material) => {
|
||||
const previousQuantity
|
||||
= previousMaterialMap.get(material.materialId) ?? 0;
|
||||
return {
|
||||
...material,
|
||||
quantity: Math.min(material.quantity, previousQuantity),
|
||||
};
|
||||
});
|
||||
const goddessRecipeIds = [
|
||||
...new Set([
|
||||
...previousGoddessExploration.craftedRecipeIds,
|
||||
...incomingGoddess.exploration.craftedRecipeIds,
|
||||
]),
|
||||
];
|
||||
goddessExploration = {
|
||||
...incomingGoddess.exploration,
|
||||
// eslint-disable-next-line stylistic/max-len -- Long field name; splitting would reduce readability
|
||||
craftedCombatMultiplier: previousGoddessExploration.craftedCombatMultiplier,
|
||||
// eslint-disable-next-line stylistic/max-len -- Long field name; splitting would reduce readability
|
||||
craftedDivinityMultiplier: previousGoddessExploration.craftedDivinityMultiplier,
|
||||
// eslint-disable-next-line stylistic/max-len -- Long field name; splitting would reduce readability
|
||||
craftedPrayersMultiplier: previousGoddessExploration.craftedPrayersMultiplier,
|
||||
craftedRecipeIds: goddessRecipeIds,
|
||||
materials: materials,
|
||||
};
|
||||
}
|
||||
const consecration = previousGoddess
|
||||
? {
|
||||
...incomingGoddess.consecration,
|
||||
count: Math.min(
|
||||
incomingGoddess.consecration.count,
|
||||
previousGoddess.consecration.count,
|
||||
),
|
||||
divinity: Math.min(
|
||||
incomingGoddess.consecration.divinity,
|
||||
previousGoddess.consecration.divinity,
|
||||
),
|
||||
|
||||
productionMultiplier: previousGoddess.consecration.productionMultiplier,
|
||||
}
|
||||
: incomingGoddess.consecration;
|
||||
const enlightenment = previousGoddess
|
||||
? {
|
||||
...incomingGoddess.enlightenment,
|
||||
count: Math.min(
|
||||
incomingGoddess.enlightenment.count,
|
||||
previousGoddess.enlightenment.count,
|
||||
),
|
||||
stardust: Math.min(
|
||||
incomingGoddess.enlightenment.stardust,
|
||||
previousGoddess.enlightenment.stardust,
|
||||
),
|
||||
stardustCombatMultiplier:
|
||||
previousGoddess.enlightenment.stardustCombatMultiplier,
|
||||
|
||||
stardustConsecrationDivinityMultiplier:
|
||||
previousGoddess.enlightenment.stardustConsecrationDivinityMultiplier,
|
||||
|
||||
stardustConsecrationThresholdMultiplier:
|
||||
previousGoddess.enlightenment.stardustConsecrationThresholdMultiplier,
|
||||
stardustMetaMultiplier:
|
||||
previousGoddess.enlightenment.stardustMetaMultiplier,
|
||||
stardustPrayersMultiplier:
|
||||
previousGoddess.enlightenment.stardustPrayersMultiplier,
|
||||
}
|
||||
: incomingGoddess.enlightenment;
|
||||
goddessSpread = {
|
||||
goddess: {
|
||||
...incomingGoddess,
|
||||
achievements: goddessAchievements,
|
||||
bosses: goddessBosses,
|
||||
consecration: consecration,
|
||||
enlightenment: enlightenment,
|
||||
exploration: goddessExploration,
|
||||
lifetimeBossesDefeated: Math.min(
|
||||
incomingGoddess.lifetimeBossesDefeated,
|
||||
previousGoddess?.lifetimeBossesDefeated ?? 0,
|
||||
),
|
||||
lifetimePrayersEarned: Math.min(
|
||||
incomingGoddess.lifetimePrayersEarned,
|
||||
previousGoddess?.lifetimePrayersEarned ?? 0,
|
||||
),
|
||||
lifetimeQuestsCompleted: Math.min(
|
||||
incomingGoddess.lifetimeQuestsCompleted,
|
||||
previousGoddess?.lifetimeQuestsCompleted ?? 0,
|
||||
),
|
||||
quests: goddessQuests,
|
||||
totalPrayersEarned: Math.min(
|
||||
incomingGoddess.totalPrayersEarned,
|
||||
previousGoddess?.totalPrayersEarned ?? 0,
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...incoming,
|
||||
achievements,
|
||||
@@ -733,6 +899,7 @@ const validateAndSanitize = (
|
||||
...explorationSpread,
|
||||
...storySpread,
|
||||
...dailyChallengesSpread,
|
||||
...goddessSpread,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user