feat: major content expansion with essence and crystal sinks

- 6 zones (up from 3): Shadow Marshes, Volcanic Depths, Astral Void
- 18 bosses (up from 4): 3 per zone with zone-based sequential unlock
- 24 quests (up from 9): 3-4 per zone, reorganised by zone
- 15 adventurer tiers (up from 10): Shadow Assassin through Divine Champion
- 28 equipment pieces (up from 12): boss drops + purchasable with essence/crystals
- 24 upgrades (up from 13): crystal-cost upgrades + new adventurer upgrades
- 22 achievements (up from 14): new milestones for bosses, quests, gold, armies
- Purchasable equipment system: buy items directly with essence or crystals
- Crystal-cost upgrades: spend crystals on global and click power boosts
- Zone-based boss progression: defeating a zone's last boss unlocks the next zone
This commit is contained in:
2026-03-06 14:36:41 -08:00
committed by Naomi Carrigan
parent 653c36c886
commit 772d733e86
15 changed files with 1202 additions and 81 deletions
@@ -32,10 +32,25 @@ const bonusDescription = (item: Equipment): string => {
interface EquipmentCardProps {
item: Equipment;
gold: number;
essence: number;
crystals: number;
}
const EquipmentCard = ({ item }: EquipmentCardProps): React.JSX.Element => {
const { equipItem } = useGame();
const costLabel = (cost: { gold: number; essence: number; crystals: number }): string => {
const parts: string[] = [];
if (cost.gold > 0) parts.push(`🪙 ${cost.gold.toLocaleString()}`);
if (cost.essence > 0) parts.push(`${cost.essence.toLocaleString()}`);
if (cost.crystals > 0) parts.push(`💎 ${cost.crystals.toLocaleString()}`);
return parts.join(" ");
};
const EquipmentCard = ({ item, gold, essence, crystals }: EquipmentCardProps): React.JSX.Element => {
const { equipItem, buyEquipment } = useGame();
const canAfford = item.cost
? gold >= item.cost.gold && essence >= item.cost.essence && crystals >= item.cost.crystals
: false;
return (
<div className={`equipment-card rarity-${item.rarity} ${item.equipped ? "equipped" : ""} ${!item.owned ? "not-owned" : ""}`}>
@@ -47,9 +62,22 @@ const EquipmentCard = ({ item }: EquipmentCardProps): React.JSX.Element => {
</div>
<p className="equipment-description">{item.description}</p>
<p className="equipment-bonus">{bonusDescription(item)}</p>
{!item.owned && item.cost && (
<p className="equipment-cost">{costLabel(item.cost)}</p>
)}
</div>
<div className="equipment-action">
{!item.owned && <span className="equipment-locked">🔒 Not yet obtained</span>}
{!item.owned && !item.cost && <span className="equipment-locked">🔒 Boss drop</span>}
{!item.owned && item.cost && (
<button
className="equip-button"
disabled={!canAfford}
onClick={() => { buyEquipment(item.id); }}
type="button"
>
{canAfford ? "Purchase" : "Can't afford"}
</button>
)}
{item.owned && item.equipped && <span className="equipment-equipped-badge"> Equipped</span>}
{item.owned && !item.equipped && (
<button
@@ -104,7 +132,13 @@ export const EquipmentPanel = (): React.JSX.Element => {
<h3 className="slot-heading">{SLOT_LABEL[slotType]}</h3>
<div className="equipment-list">
{items.map((item) => (
<EquipmentCard key={item.id} item={item} />
<EquipmentCard
key={item.id}
item={item}
gold={state.resources.gold}
essence={state.resources.essence}
crystals={state.resources.crystals}
/>
))}
{items.length === 0 && (
<p className="empty-zone">No items to show in this slot.</p>
+10 -2
View File
@@ -7,12 +7,15 @@ interface UpgradeCardProps {
upgrade: Upgrade;
currentGold: number;
currentEssence: number;
currentCrystals: number;
}
const UpgradeCard = ({ upgrade, currentGold, currentEssence }: UpgradeCardProps): React.JSX.Element => {
const UpgradeCard = ({ upgrade, currentGold, currentEssence, currentCrystals }: UpgradeCardProps): React.JSX.Element => {
const { buyUpgrade } = useGame();
const canAfford =
currentGold >= upgrade.costGold && currentEssence >= upgrade.costEssence;
currentGold >= upgrade.costGold &&
currentEssence >= upgrade.costEssence &&
currentCrystals >= (upgrade.costCrystals ?? 0);
if (!upgrade.unlocked) {
return (
@@ -25,6 +28,7 @@ const UpgradeCard = ({ upgrade, currentGold, currentEssence }: UpgradeCardProps)
<div className="upgrade-cost">
{upgrade.costGold > 0 && <span>🪙 {upgrade.costGold.toLocaleString()}</span>}
{upgrade.costEssence > 0 && <span> {upgrade.costEssence.toLocaleString()}</span>}
{(upgrade.costCrystals ?? 0) > 0 && <span>💎 {upgrade.costCrystals?.toLocaleString()}</span>}
</div>
<span className="upgrade-locked-label">Locked</span>
</div>
@@ -50,6 +54,7 @@ const UpgradeCard = ({ upgrade, currentGold, currentEssence }: UpgradeCardProps)
<div className="upgrade-cost">
{upgrade.costGold > 0 && <span>🪙 {upgrade.costGold.toLocaleString()}</span>}
{upgrade.costEssence > 0 && <span> {upgrade.costEssence.toLocaleString()}</span>}
{(upgrade.costCrystals ?? 0) > 0 && <span>💎 {upgrade.costCrystals?.toLocaleString()}</span>}
</div>
<button
className="buy-button"
@@ -94,6 +99,7 @@ export const UpgradePanel = (): React.JSX.Element => {
upgrade={upgrade}
currentGold={state.resources.gold}
currentEssence={state.resources.essence}
currentCrystals={state.resources.crystals}
/>
))}
{purchased.map((upgrade) => (
@@ -102,6 +108,7 @@ export const UpgradePanel = (): React.JSX.Element => {
upgrade={upgrade}
currentGold={state.resources.gold}
currentEssence={state.resources.essence}
currentCrystals={state.resources.crystals}
/>
))}
{showLocked && locked.map((upgrade) => (
@@ -110,6 +117,7 @@ export const UpgradePanel = (): React.JSX.Element => {
upgrade={upgrade}
currentGold={state.resources.gold}
currentEssence={state.resources.essence}
currentCrystals={state.resources.crystals}
/>
))}
</div>