generated from nhcarrigan/template
feat: add equipment, achievements, and visual polish
- Equipment system: 12 items across weapon/armour/trinket slots with common/rare/epic/legendary rarities; starter commons auto-equipped, higher tiers drop from boss victories - Achievement system: 15 milestones with typed conditions; checked each tick and crystal rewards applied automatically - Achievement toast: slide-in notification, auto-dismisses after 4s - Floating click text: +X gold floats on each manual click - Expanded quests (9 total) and upgrades (12 total) - Upgrade panel now shows locked upgrades so players can see their progression path - formatNumber utility (K/M/B/T) used consistently across all panels - Backfill logic for existing saves to add new content gracefully - types package now emits .d.ts declarations
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import type { Equipment, EquipmentType } from "@elysium/types";
|
||||
import { useGame } from "../../context/GameContext.js";
|
||||
|
||||
const RARITY_LABEL: Record<string, string> = {
|
||||
common: "Common",
|
||||
rare: "Rare",
|
||||
epic: "Epic",
|
||||
legendary: "Legendary",
|
||||
};
|
||||
|
||||
const TYPE_ICON: Record<EquipmentType, string> = {
|
||||
weapon: "⚔️",
|
||||
armour: "🛡️",
|
||||
trinket: "💍",
|
||||
};
|
||||
|
||||
const bonusDescription = (item: Equipment): string => {
|
||||
const parts: string[] = [];
|
||||
if (item.bonus.combatMultiplier != null) {
|
||||
parts.push(`+${Math.round((item.bonus.combatMultiplier - 1) * 100)}% Combat`);
|
||||
}
|
||||
if (item.bonus.goldMultiplier != null) {
|
||||
parts.push(`+${Math.round((item.bonus.goldMultiplier - 1) * 100)}% Gold/s`);
|
||||
}
|
||||
if (item.bonus.clickMultiplier != null) {
|
||||
parts.push(`+${Math.round((item.bonus.clickMultiplier - 1) * 100)}% Click`);
|
||||
}
|
||||
return parts.join(", ");
|
||||
};
|
||||
|
||||
interface EquipmentCardProps {
|
||||
item: Equipment;
|
||||
}
|
||||
|
||||
const EquipmentCard = ({ item }: EquipmentCardProps): React.JSX.Element => {
|
||||
const { equipItem } = useGame();
|
||||
|
||||
return (
|
||||
<div className={`equipment-card rarity-${item.rarity} ${item.equipped ? "equipped" : ""} ${!item.owned ? "not-owned" : ""}`}>
|
||||
<div className="equipment-icon">{TYPE_ICON[item.type]}</div>
|
||||
<div className="equipment-info">
|
||||
<div className="equipment-name-row">
|
||||
<h3>{item.name}</h3>
|
||||
<span className={`rarity-badge rarity-${item.rarity}`}>{RARITY_LABEL[item.rarity]}</span>
|
||||
</div>
|
||||
<p className="equipment-description">{item.description}</p>
|
||||
<p className="equipment-bonus">{bonusDescription(item)}</p>
|
||||
</div>
|
||||
<div className="equipment-action">
|
||||
{!item.owned && <span className="equipment-locked">🔒 Not yet obtained</span>}
|
||||
{item.owned && item.equipped && <span className="equipment-equipped-badge">✓ Equipped</span>}
|
||||
{item.owned && !item.equipped && (
|
||||
<button
|
||||
className="equip-button"
|
||||
onClick={() => { equipItem(item.id); }}
|
||||
type="button"
|
||||
>
|
||||
Equip
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SLOT_ORDER: EquipmentType[] = ["weapon", "armour", "trinket"];
|
||||
const SLOT_LABEL: Record<EquipmentType, string> = {
|
||||
weapon: "⚔️ Weapons",
|
||||
armour: "🛡️ Armour",
|
||||
trinket: "💍 Trinkets",
|
||||
};
|
||||
|
||||
export const EquipmentPanel = (): React.JSX.Element => {
|
||||
const { state } = useGame();
|
||||
|
||||
if (!state) return <section className="panel"><p>Loading...</p></section>;
|
||||
|
||||
const equipment = state.equipment ?? [];
|
||||
|
||||
return (
|
||||
<section className="panel equipment-panel">
|
||||
<h2>Equipment</h2>
|
||||
<p className="equipment-intro">
|
||||
Equipment drops from bosses and grants passive bonuses. Only one item per slot can be equipped at a time.
|
||||
</p>
|
||||
|
||||
{SLOT_ORDER.map((slotType) => {
|
||||
const items = equipment.filter((e) => e.type === slotType);
|
||||
return (
|
||||
<div key={slotType} className="equipment-slot-section">
|
||||
<h3 className="slot-heading">{SLOT_LABEL[slotType]}</h3>
|
||||
<div className="equipment-list">
|
||||
{items.map((item) => (
|
||||
<EquipmentCard key={item.id} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user