generated from nhcarrigan/template
feat: goddess expansion chunks 6–9 — UI panels, tick engine, CSS theme, about page
- Add 11 goddess panels (zones, bosses, quests, disciples, equipment, upgrades, consecration, enlightenment, crafting, exploration, achievements) - Wire all panels into gameLayout via mode/tab routing - Add goddess passive income, disciple tick, quest timers, zone/quest unlock logic, and achievement checking to the tick engine - Add goddess CSS variables, .goddess-mode overrides, 300ms fade transition, and full panel stylesheet coverage - Add 13 Goddess expansion entries to the How to Play guide - Add web-side data files for crafting recipes, exploration areas, materials
This commit is contained in:
@@ -16,6 +16,8 @@ import {
|
||||
type Achievement,
|
||||
type Equipment,
|
||||
type GameState,
|
||||
type GoddessAchievement,
|
||||
type GoddessState,
|
||||
computeSetBonuses,
|
||||
getActiveCompanionBonus,
|
||||
} from "@elysium/types";
|
||||
@@ -83,6 +85,62 @@ const checkAchievements = (state: GameState): Array<Achievement> => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks all goddess achievements against a snapshot of the goddess state
|
||||
* and returns an updated achievements array, marking newly-met conditions
|
||||
* with the current timestamp.
|
||||
* @param goddess - The current (or projected) goddess state.
|
||||
* @param now - Current Unix timestamp in milliseconds.
|
||||
* @returns Updated goddess achievements array with newly unlocked ones timestamped.
|
||||
*/
|
||||
const checkGoddessAchievements = (
|
||||
goddess: GoddessState,
|
||||
now: number,
|
||||
): Array<GoddessAchievement> => {
|
||||
return goddess.achievements.map((achievement) => {
|
||||
if (achievement.unlockedAt !== null) {
|
||||
return achievement;
|
||||
}
|
||||
|
||||
const { condition } = achievement;
|
||||
let met = false;
|
||||
|
||||
switch (condition.type) {
|
||||
case "totalPrayersEarned":
|
||||
met = goddess.lifetimePrayersEarned >= condition.amount;
|
||||
break;
|
||||
case "goddessBossesDefeated":
|
||||
met = goddess.lifetimeBossesDefeated >= condition.amount;
|
||||
break;
|
||||
case "goddessQuestsCompleted":
|
||||
met = goddess.lifetimeQuestsCompleted >= condition.amount;
|
||||
break;
|
||||
case "discipleTotal":
|
||||
met
|
||||
= goddess.disciples.reduce((sum, disciple) => {
|
||||
return sum + disciple.count;
|
||||
}, 0) >= condition.amount;
|
||||
break;
|
||||
case "consecrationCount":
|
||||
met = goddess.consecration.count >= condition.amount;
|
||||
break;
|
||||
case "goddessEquipmentOwned":
|
||||
met
|
||||
= goddess.equipment.filter((item) => {
|
||||
return item.owned;
|
||||
}).length >= condition.amount;
|
||||
break;
|
||||
default:
|
||||
// eslint-disable-next-line capitalized-comments -- v8 coverage ignore directive
|
||||
/* v8 ignore next -- @preserve */ break;
|
||||
}
|
||||
|
||||
return met
|
||||
? { ...achievement, unlockedAt: now }
|
||||
: achievement;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Exponential base for the prestige combat multiplier: Math.pow(base, prestigeCount).
|
||||
* Replaces the former linear formula (1 + count * 0.1) to enable late-game zone progression.
|
||||
@@ -770,6 +828,269 @@ export const applyTick = (
|
||||
: upgrade;
|
||||
});
|
||||
|
||||
// --- Goddess tick ---
|
||||
let prayersGained = 0;
|
||||
let divinityGained = 0;
|
||||
let stardustFromQuests = 0;
|
||||
// eslint-disable-next-line no-undef-init -- required by @typescript-eslint/init-declarations
|
||||
let updatedGoddess: GoddessState | undefined = undefined;
|
||||
|
||||
if (
|
||||
state.apotheosis !== undefined
|
||||
&& state.apotheosis.count > 0
|
||||
&& state.goddess !== undefined
|
||||
) {
|
||||
const { goddess } = state;
|
||||
|
||||
// Collect all multipliers for prayers/divinity income
|
||||
const consecMult = goddess.consecration.productionMultiplier;
|
||||
const divinityPrayersMult
|
||||
= goddess.consecration.divinityPrayersMultiplier ?? 1;
|
||||
const divinityDisciplesMult
|
||||
= goddess.consecration.divinityDisciplesMultiplier ?? 1;
|
||||
const stardustPrayersMult = goddess.enlightenment.stardustPrayersMultiplier;
|
||||
const craftedPrayersMult = goddess.exploration.craftedPrayersMultiplier;
|
||||
const craftedDivinityMult = goddess.exploration.craftedDivinityMultiplier;
|
||||
|
||||
let globalPrayersMult = 1;
|
||||
for (const upgrade of goddess.upgrades) {
|
||||
if (upgrade.purchased && upgrade.target === "prayers") {
|
||||
globalPrayersMult = globalPrayersMult * upgrade.multiplier;
|
||||
}
|
||||
}
|
||||
|
||||
for (const disciple of goddess.disciples) {
|
||||
if (disciple.count === 0) {
|
||||
continue;
|
||||
}
|
||||
let discipleUpgradeMult = 1;
|
||||
for (const upgrade of goddess.upgrades) {
|
||||
if (
|
||||
upgrade.purchased
|
||||
&& upgrade.target === "disciple"
|
||||
&& upgrade.discipleId === disciple.id
|
||||
) {
|
||||
discipleUpgradeMult = discipleUpgradeMult * upgrade.multiplier;
|
||||
}
|
||||
}
|
||||
const prayersTick
|
||||
= disciple.prayersPerSecond
|
||||
* disciple.count
|
||||
* discipleUpgradeMult
|
||||
* globalPrayersMult
|
||||
* consecMult
|
||||
* divinityPrayersMult
|
||||
* divinityDisciplesMult
|
||||
* stardustPrayersMult
|
||||
* craftedPrayersMult
|
||||
* deltaSeconds;
|
||||
prayersGained = prayersGained + prayersTick;
|
||||
const divinityTick
|
||||
= disciple.divinityPerSecond
|
||||
* disciple.count
|
||||
* discipleUpgradeMult
|
||||
* consecMult
|
||||
* divinityDisciplesMult
|
||||
* craftedDivinityMult
|
||||
* deltaSeconds;
|
||||
divinityGained = divinityGained + divinityTick;
|
||||
}
|
||||
|
||||
// Process goddess quest timers
|
||||
let goddessQuestPrayersGained = 0;
|
||||
let goddessQuestDivinityGained = 0;
|
||||
let updatedGoddessDisciples = goddess.disciples;
|
||||
let updatedGoddessEquipment = goddess.equipment;
|
||||
let updatedGoddessUpgrades = goddess.upgrades;
|
||||
let goddessQuestsThisTick = 0;
|
||||
|
||||
const updatedGoddessQuests = goddess.quests.map((quest) => {
|
||||
const questDurationMs = quest.durationSeconds * 1000;
|
||||
const questExpiry
|
||||
= quest.startedAt === undefined
|
||||
? Infinity
|
||||
: quest.startedAt + questDurationMs;
|
||||
if (quest.status !== "active" || now < questExpiry) {
|
||||
return quest;
|
||||
}
|
||||
|
||||
goddessQuestsThisTick = goddessQuestsThisTick + 1;
|
||||
for (const reward of quest.rewards) {
|
||||
if (reward.type === "prayers" && reward.amount !== undefined) {
|
||||
goddessQuestPrayersGained
|
||||
= goddessQuestPrayersGained + reward.amount;
|
||||
} else if (
|
||||
reward.type === "divinity"
|
||||
&& reward.amount !== undefined
|
||||
) {
|
||||
goddessQuestDivinityGained
|
||||
= goddessQuestDivinityGained + reward.amount;
|
||||
} else if (
|
||||
reward.type === "stardust"
|
||||
&& reward.amount !== undefined
|
||||
) {
|
||||
stardustFromQuests = stardustFromQuests + reward.amount;
|
||||
} else if (
|
||||
reward.type === "upgrade"
|
||||
&& reward.targetId !== undefined
|
||||
) {
|
||||
const { targetId } = reward;
|
||||
updatedGoddessUpgrades = updatedGoddessUpgrades.map((upgrade) => {
|
||||
return upgrade.id === targetId
|
||||
? { ...upgrade, unlocked: true }
|
||||
: upgrade;
|
||||
});
|
||||
} else if (
|
||||
reward.type === "disciple"
|
||||
&& reward.targetId !== undefined
|
||||
) {
|
||||
const { targetId } = reward;
|
||||
updatedGoddessDisciples = updatedGoddessDisciples.map((disciple) => {
|
||||
return disciple.id === targetId
|
||||
? { ...disciple, unlocked: true }
|
||||
: disciple;
|
||||
});
|
||||
} else if (
|
||||
reward.type === "equipment"
|
||||
&& reward.targetId !== undefined
|
||||
) {
|
||||
const rewardTargetId = reward.targetId;
|
||||
const currentEquipment = updatedGoddessEquipment;
|
||||
updatedGoddessEquipment = currentEquipment.map((item) => {
|
||||
if (item.id !== rewardTargetId) {
|
||||
return item;
|
||||
}
|
||||
const slotEmpty = !currentEquipment.some((other) => {
|
||||
return other.type === item.type && other.equipped;
|
||||
});
|
||||
return {
|
||||
...item,
|
||||
equipped: slotEmpty || item.equipped,
|
||||
owned: true,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
return { ...quest, status: "completed" as const };
|
||||
});
|
||||
|
||||
// Unlock goddess quests whose prerequisites are now all completed
|
||||
const completedGoddessIds = new Set(
|
||||
updatedGoddessQuests.
|
||||
filter((quest) => {
|
||||
return quest.status === "completed";
|
||||
}).
|
||||
map((quest) => {
|
||||
return quest.id;
|
||||
}),
|
||||
);
|
||||
const defeatedBossIds = new Set(
|
||||
goddess.bosses.
|
||||
filter((boss) => {
|
||||
return boss.status === "defeated";
|
||||
}).
|
||||
map((boss) => {
|
||||
return boss.id;
|
||||
}),
|
||||
);
|
||||
// Unlock goddess zones whose boss + quest requirements are now met
|
||||
const updatedGoddessZones = goddess.zones.map((zone) => {
|
||||
if (zone.status === "unlocked") {
|
||||
return zone;
|
||||
}
|
||||
const bossOk
|
||||
= zone.unlockBossId === null
|
||||
|| defeatedBossIds.has(zone.unlockBossId);
|
||||
const questOk
|
||||
= zone.unlockQuestId === null
|
||||
|| completedGoddessIds.has(zone.unlockQuestId);
|
||||
if (bossOk && questOk) {
|
||||
return { ...zone, status: "unlocked" as const };
|
||||
}
|
||||
return zone;
|
||||
});
|
||||
|
||||
const fullyUpdatedGoddessZones = updatedGoddessZones;
|
||||
const allUnlockedGoddessZoneIds = new Set(
|
||||
fullyUpdatedGoddessZones.
|
||||
filter((zone) => {
|
||||
return zone.status === "unlocked";
|
||||
}).
|
||||
map((zone) => {
|
||||
return zone.id;
|
||||
}),
|
||||
);
|
||||
|
||||
const fullyUpdatedGoddessQuests = updatedGoddessQuests.map((quest) => {
|
||||
if (quest.status !== "locked") {
|
||||
return quest;
|
||||
}
|
||||
if (!allUnlockedGoddessZoneIds.has(quest.zoneId)) {
|
||||
return quest;
|
||||
}
|
||||
if (
|
||||
quest.prerequisiteIds.every((id) => {
|
||||
return completedGoddessIds.has(id);
|
||||
})
|
||||
) {
|
||||
return { ...quest, status: "available" as const };
|
||||
}
|
||||
return quest;
|
||||
});
|
||||
|
||||
// Compute updated lifetime counters
|
||||
const totalPrayersThisTick
|
||||
= prayersGained + goddessQuestPrayersGained;
|
||||
const updatedTotalPrayersEarned
|
||||
= goddess.totalPrayersEarned + totalPrayersThisTick;
|
||||
const updatedLifetimePrayersEarned
|
||||
= goddess.lifetimePrayersEarned + totalPrayersThisTick;
|
||||
const updatedLifetimeQuestsCompleted
|
||||
= goddess.lifetimeQuestsCompleted + goddessQuestsThisTick;
|
||||
|
||||
// Build snapshot for achievement check
|
||||
const goddessSnapshot: GoddessState = {
|
||||
...goddess,
|
||||
disciples: updatedGoddessDisciples,
|
||||
equipment: updatedGoddessEquipment,
|
||||
lifetimeBossesDefeated: goddess.lifetimeBossesDefeated,
|
||||
lifetimePrayersEarned: updatedLifetimePrayersEarned,
|
||||
lifetimeQuestsCompleted: updatedLifetimeQuestsCompleted,
|
||||
quests: fullyUpdatedGoddessQuests,
|
||||
upgrades: updatedGoddessUpgrades,
|
||||
zones: fullyUpdatedGoddessZones,
|
||||
};
|
||||
const updatedGoddessAchievements
|
||||
= checkGoddessAchievements(goddessSnapshot, now);
|
||||
let divinityFromAchievements = 0;
|
||||
let stardustFromAchievements = 0;
|
||||
for (const [ index, achievement ] of updatedGoddessAchievements.entries()) {
|
||||
if (
|
||||
goddess.achievements[index]?.unlockedAt === null
|
||||
&& achievement.unlockedAt !== null
|
||||
) {
|
||||
divinityFromAchievements
|
||||
= divinityFromAchievements + (achievement.reward?.divinity ?? 0);
|
||||
stardustFromAchievements
|
||||
= stardustFromAchievements + (achievement.reward?.stardust ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
updatedGoddess = {
|
||||
...goddessSnapshot,
|
||||
achievements: updatedGoddessAchievements,
|
||||
lastTickAt: now,
|
||||
totalPrayersEarned: updatedTotalPrayersEarned,
|
||||
};
|
||||
|
||||
// Include quest divinity + achievement divinity in this tick's total
|
||||
divinityGained
|
||||
= divinityGained
|
||||
+ goddessQuestDivinityGained
|
||||
+ divinityFromAchievements;
|
||||
stardustFromQuests = stardustFromQuests + stardustFromAchievements;
|
||||
}
|
||||
|
||||
const goldValue = capResource(state.resources.gold + goldGained + questGold);
|
||||
const essenceValue = capResource(
|
||||
state.resources.essence + essenceGained + questEssence,
|
||||
@@ -784,8 +1105,17 @@ export const applyTick = (
|
||||
crystals: capResource(
|
||||
state.resources.crystals + questCrystals + challengeCrystals,
|
||||
),
|
||||
divinity: capResource(
|
||||
(state.resources.divinity ?? 0) + divinityGained,
|
||||
),
|
||||
essence: essenceValue,
|
||||
gold: goldValue,
|
||||
prayers: capResource(
|
||||
(state.resources.prayers ?? 0) + prayersGained,
|
||||
),
|
||||
stardust: capResource(
|
||||
(state.resources.stardust ?? 0) + stardustFromQuests,
|
||||
),
|
||||
},
|
||||
...updatedDailyChallenges === undefined
|
||||
? {}
|
||||
@@ -807,6 +1137,9 @@ export const applyTick = (
|
||||
}),
|
||||
},
|
||||
},
|
||||
...updatedGoddess === undefined
|
||||
? {}
|
||||
: { goddess: updatedGoddess },
|
||||
adventurers: updatedAdventurers,
|
||||
bosses: updatedBosses,
|
||||
equipment: updatedEquipmentReference,
|
||||
|
||||
Reference in New Issue
Block a user