generated from nhcarrigan/template
3e34701d32
Implements the three read-only vampire expansion display panels: - VampireZonesPanel: zone grid with lock state, boss/quest unlock requirements - VampireQuestsPanel: zone-filtered quest list with duration, rewards, progress badge - VampireAchievementsPanel: achievement cards with progress bars and ichor/soul shard rewards Wires all three into GameLayout, replacing the corresponding tab placeholders.
252 lines
7.8 KiB
TypeScript
252 lines
7.8 KiB
TypeScript
/**
|
|
* @file Vampire achievements panel component displaying all vampire expansion achievements.
|
|
* @copyright nhcarrigan
|
|
* @license Naomi's Public License
|
|
* @author Naomi Carrigan
|
|
*/
|
|
/* eslint-disable react/no-multi-comp -- Sub-component is tightly coupled to this panel */
|
|
/* eslint-disable max-lines-per-function -- Achievement panel renders many achievement states */
|
|
import { type JSX, useState } from "react";
|
|
import { useGame } from "../../context/gameContext.js";
|
|
import { LockToggle } from "../ui/lockToggle.js";
|
|
import type { VampireAchievement, VampireState } from "@elysium/types";
|
|
|
|
/**
|
|
* Returns the plural form of a word based on a count.
|
|
* @param count - The count to check.
|
|
* @param word - The base word to pluralise.
|
|
* @returns The pluralised word string.
|
|
*/
|
|
const pluralise = (count: number, word: string): string => {
|
|
return count > 1
|
|
? `${word}s`
|
|
: word;
|
|
};
|
|
|
|
/**
|
|
* Generates a human-readable condition description for a vampire achievement.
|
|
* @param achievement - The vampire achievement to describe.
|
|
* @param formatNumber - The number formatting utility function.
|
|
* @returns A string describing the achievement condition.
|
|
*/
|
|
const conditionDescription = (
|
|
achievement: VampireAchievement,
|
|
formatNumber: (n: number)=> string,
|
|
): string => {
|
|
const { condition } = achievement;
|
|
switch (condition.type) {
|
|
case "totalBloodEarned":
|
|
return `Earn ${formatNumber(condition.amount)} total blood`;
|
|
case "vampireBossesDefeated":
|
|
return `Defeat ${String(condition.amount)} vampire ${pluralise(condition.amount, "boss")}`;
|
|
case "vampireQuestsCompleted":
|
|
return `Complete ${String(condition.amount)} vampire ${pluralise(condition.amount, "quest")}`;
|
|
case "thrallTotal":
|
|
return `Recruit ${formatNumber(condition.amount)} total ${pluralise(condition.amount, "thrall")}`;
|
|
case "siringCount":
|
|
return `Sire ${String(condition.amount)} ${pluralise(condition.amount, "time")}`;
|
|
case "vampireEquipmentOwned":
|
|
return `Own ${String(condition.amount)} vampire equipment ${pluralise(condition.amount, "item")}`;
|
|
default:
|
|
return "Unknown condition";
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns the player's current progress value toward a vampire achievement's unlock condition.
|
|
* @param achievement - The achievement to evaluate progress for.
|
|
* @param vampire - The current vampire state.
|
|
* @returns The current numeric progress toward the achievement condition.
|
|
*/
|
|
const getCurrentProgress = (
|
|
achievement: VampireAchievement,
|
|
vampire: VampireState,
|
|
): number => {
|
|
const { condition } = achievement;
|
|
switch (condition.type) {
|
|
case "totalBloodEarned":
|
|
return vampire.lifetimeBloodEarned;
|
|
case "vampireBossesDefeated":
|
|
return vampire.lifetimeBossesDefeated;
|
|
case "vampireQuestsCompleted":
|
|
return vampire.lifetimeQuestsCompleted;
|
|
case "thrallTotal":
|
|
return vampire.thralls.reduce((sum, thrall) => {
|
|
return sum + thrall.count;
|
|
}, 0);
|
|
case "siringCount":
|
|
return vampire.siring.count;
|
|
case "vampireEquipmentOwned":
|
|
return vampire.equipment.filter((item) => {
|
|
return item.owned;
|
|
}).length;
|
|
default:
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
interface VampireAchievementCardProperties {
|
|
readonly achievement: VampireAchievement;
|
|
readonly formatNumber: (n: number)=> string;
|
|
readonly progressValue: number;
|
|
}
|
|
|
|
/**
|
|
* Renders a single vampire achievement card.
|
|
* @param props - The achievement card properties.
|
|
* @param props.achievement - The achievement to display.
|
|
* @param props.formatNumber - The number formatting utility function.
|
|
* @param props.progressValue - The player's current progress toward the unlock condition.
|
|
* @returns The JSX element.
|
|
*/
|
|
const VampireAchievementCard = ({
|
|
achievement,
|
|
formatNumber,
|
|
progressValue,
|
|
}: VampireAchievementCardProperties): JSX.Element => {
|
|
const isUnlocked = achievement.unlockedAt !== null;
|
|
const cappedProgress = Math.min(progressValue, achievement.condition.amount);
|
|
|
|
return (
|
|
<div className={`achievement-card ${isUnlocked
|
|
? "unlocked"
|
|
: "locked"}`}>
|
|
<div className="achievement-icon">
|
|
<span className="achievement-emoji">{achievement.icon}</span>
|
|
</div>
|
|
<div className="achievement-info">
|
|
<h3>{achievement.name}</h3>
|
|
<p>{achievement.description}</p>
|
|
<p className="achievement-condition">
|
|
{conditionDescription(achievement, formatNumber)}
|
|
</p>
|
|
{!isUnlocked
|
|
&& <div className="achievement-progress">
|
|
<progress
|
|
max={achievement.condition.amount}
|
|
value={cappedProgress}
|
|
/>
|
|
<span className="achievement-progress-label">
|
|
{formatNumber(progressValue)}
|
|
{" / "}
|
|
{formatNumber(achievement.condition.amount)}
|
|
</span>
|
|
</div>
|
|
}
|
|
{achievement.reward !== undefined
|
|
&& <div className="achievement-reward">
|
|
{achievement.reward.ichor !== undefined
|
|
&& <p>
|
|
{"💧 +"}
|
|
{achievement.reward.ichor}
|
|
{" Ichor"}
|
|
</p>
|
|
}
|
|
{achievement.reward.soulShards !== undefined
|
|
&& <p>
|
|
{"💠+"}
|
|
{achievement.reward.soulShards}
|
|
{" Soul Shards"}
|
|
</p>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
<div className="achievement-status">
|
|
{isUnlocked
|
|
? <>
|
|
<span className="achievement-unlocked-badge">{"✓ Unlocked"}</span>
|
|
{achievement.unlockedAt !== null
|
|
&& <span className="achievement-unlocked-at">
|
|
{new Date(achievement.unlockedAt).toLocaleDateString("en-GB", {
|
|
day: "numeric",
|
|
month: "short",
|
|
year: "numeric",
|
|
})}
|
|
</span>
|
|
}
|
|
</>
|
|
: <span className="achievement-locked-badge">{"🔒"}</span>
|
|
}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Renders the vampire achievements panel with all vampire expansion achievements.
|
|
* @returns The JSX element.
|
|
*/
|
|
const VampireAchievementsPanel = (): JSX.Element => {
|
|
const { state, formatNumber } = useGame();
|
|
const [ showLocked, setShowLocked ] = useState(true);
|
|
|
|
if (state === null) {
|
|
return (
|
|
<section className="panel">
|
|
<p>{"Loading..."}</p>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
const { vampire } = state;
|
|
|
|
if (vampire === undefined) {
|
|
return (
|
|
<section className="panel">
|
|
<p>{"The Vampire expansion is not yet unlocked."}</p>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
const achievementList = vampire.achievements;
|
|
const unlocked = achievementList.filter((achievement) => {
|
|
return achievement.unlockedAt !== null;
|
|
});
|
|
const locked = achievementList.filter((achievement) => {
|
|
return achievement.unlockedAt === null;
|
|
});
|
|
const visible = showLocked
|
|
? achievementList
|
|
: unlocked;
|
|
|
|
function handleToggle(): void {
|
|
setShowLocked((current) => {
|
|
return !current;
|
|
});
|
|
}
|
|
|
|
return (
|
|
<section className="panel achievement-panel vampire-achievements-panel">
|
|
<div className="panel-header">
|
|
<h2>{"🩸 Vampire Achievements"}</h2>
|
|
<LockToggle
|
|
lockedCount={locked.length}
|
|
onToggle={handleToggle}
|
|
showLocked={showLocked}
|
|
/>
|
|
</div>
|
|
<p className="achievement-progress">
|
|
{unlocked.length}
|
|
{" / "}
|
|
{achievementList.length}
|
|
{" unlocked"}
|
|
</p>
|
|
<div className="achievement-list">
|
|
{visible.map((achievement) => {
|
|
return (
|
|
<VampireAchievementCard
|
|
achievement={achievement}
|
|
formatNumber={formatNumber}
|
|
key={achievement.id}
|
|
progressValue={getCurrentProgress(achievement, vampire)}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
export { VampireAchievementsPanel };
|