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:
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* @file Goddess crafting panel component for crafting recipes from sacred materials.
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
/* eslint-disable max-lines-per-function -- Complex component with many render paths */
|
||||
/* eslint-disable max-nested-callbacks -- Nested recipe/material maps require nesting */
|
||||
|
||||
import { type JSX, useState } from "react";
|
||||
import { useGame } from "../../context/gameContext.js";
|
||||
import { GODDESS_RECIPES } from "../../data/goddessCraftingRecipes.js";
|
||||
import { GODDESS_MATERIALS } from "../../data/goddessMaterials.js";
|
||||
import { cdnImage } from "../../utils/cdn.js";
|
||||
|
||||
const bonusLabel: Record<string, string> = {
|
||||
click_power: "👆 Click Power",
|
||||
combat_power: "⚔️ Combat Power",
|
||||
essence_income: "✨ Essence Income",
|
||||
gold_income: "🪙 Gold Income",
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the goddess crafting panel for crafting recipes from sacred materials.
|
||||
* @returns The JSX element.
|
||||
*/
|
||||
const GoddessCraftingPanel = (): JSX.Element => {
|
||||
const { state, craftGoddessRecipe, formatNumber } = useGame();
|
||||
const [ activeZoneId, setActiveZoneId ] = useState(() => {
|
||||
return (
|
||||
sessionStorage.getItem("elysium_goddess_craft_zone")
|
||||
?? "goddess_celestial_garden"
|
||||
);
|
||||
});
|
||||
const [ pendingRecipeId, setPendingRecipeId ] = useState<string | null>(null);
|
||||
|
||||
if (state === null) {
|
||||
return (
|
||||
<section className="panel">
|
||||
<p>{"Loading..."}</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const { goddess } = state;
|
||||
const playerMaterials = goddess?.exploration.materials ?? [];
|
||||
const craftedIds = goddess?.exploration.craftedRecipeIds ?? [];
|
||||
|
||||
const goddessZones = goddess?.zones ?? [];
|
||||
const zoneRecipes = GODDESS_RECIPES.filter((recipe) => {
|
||||
return recipe.zoneId === activeZoneId;
|
||||
});
|
||||
const zoneMaterials = GODDESS_MATERIALS.filter((material) => {
|
||||
return material.zoneId === activeZoneId;
|
||||
});
|
||||
|
||||
function getQuantity(materialId: string): number {
|
||||
return (
|
||||
playerMaterials.find((playerMaterial) => {
|
||||
return playerMaterial.materialId === materialId;
|
||||
})?.quantity ?? 0
|
||||
);
|
||||
}
|
||||
|
||||
function canAffordRecipe(recipeId: string): boolean {
|
||||
const recipe = GODDESS_RECIPES.find((candidateRecipe) => {
|
||||
return candidateRecipe.id === recipeId;
|
||||
});
|
||||
if (recipe === undefined) {
|
||||
return false;
|
||||
}
|
||||
return recipe.requiredMaterials.every((request) => {
|
||||
return getQuantity(request.materialId) >= request.quantity;
|
||||
});
|
||||
}
|
||||
|
||||
function handleZoneSelect(zoneId: string): void {
|
||||
setActiveZoneId(zoneId);
|
||||
sessionStorage.setItem("elysium_goddess_craft_zone", zoneId);
|
||||
}
|
||||
|
||||
async function handleCraft(recipeId: string): Promise<void> {
|
||||
setPendingRecipeId(recipeId);
|
||||
try {
|
||||
await craftGoddessRecipe(recipeId);
|
||||
} finally {
|
||||
setPendingRecipeId(null);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="panel crafting-panel">
|
||||
<div className="panel-header">
|
||||
<h2>{"⚗️ Sacred Crafting"}</h2>
|
||||
</div>
|
||||
|
||||
<div className="zone-selector">
|
||||
{goddessZones.map((zone) => {
|
||||
const isLocked = zone.status === "locked";
|
||||
|
||||
function handleZoneClick(): void {
|
||||
handleZoneSelect(zone.id);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`zone-tab ${
|
||||
activeZoneId === zone.id
|
||||
? "zone-tab-active"
|
||||
: ""
|
||||
} ${isLocked
|
||||
? "zone-tab-locked"
|
||||
: ""}`}
|
||||
disabled={isLocked}
|
||||
key={zone.id}
|
||||
onClick={handleZoneClick}
|
||||
title={isLocked
|
||||
? "Zone locked"
|
||||
: zone.name}
|
||||
type="button"
|
||||
>
|
||||
{zone.name}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="crafting-content">
|
||||
<div className="materials-section">
|
||||
<h3>{"📦 Sacred Materials"}</h3>
|
||||
{zoneMaterials.length === 0
|
||||
? <p className="empty-zone">{"No materials in this zone."}</p>
|
||||
: <div className="materials-list">
|
||||
{zoneMaterials.map((material) => {
|
||||
const qty = getQuantity(material.id);
|
||||
return (
|
||||
<div
|
||||
className={`material-card rarity-${material.rarity} ${
|
||||
qty === 0
|
||||
? "material-empty"
|
||||
: ""
|
||||
}`}
|
||||
key={material.id}
|
||||
>
|
||||
<img
|
||||
alt={material.name}
|
||||
className="card-thumbnail"
|
||||
src={cdnImage("materials", material.id)}
|
||||
/>
|
||||
<div className="material-info">
|
||||
<span className="material-name">{material.name}</span>
|
||||
<span className="material-rarity">{material.rarity}</span>
|
||||
</div>
|
||||
<span className="material-quantity">
|
||||
{formatNumber(qty)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="recipes-section">
|
||||
<h3>{"📜 Sacred Recipes"}</h3>
|
||||
{zoneRecipes.length === 0
|
||||
? <p className="empty-zone">{"No recipes in this zone."}</p>
|
||||
: <div className="recipes-list">
|
||||
{zoneRecipes.map((recipe) => {
|
||||
const crafted = craftedIds.includes(recipe.id);
|
||||
const affordable = canAffordRecipe(recipe.id);
|
||||
const isPending = pendingRecipeId === recipe.id;
|
||||
|
||||
function handleCraftClick(): void {
|
||||
void handleCraft(recipe.id);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`recipe-card ${
|
||||
crafted
|
||||
? "recipe-crafted"
|
||||
: ""
|
||||
} ${!affordable && !crafted
|
||||
? "recipe-unaffordable"
|
||||
: ""}`}
|
||||
key={recipe.id}
|
||||
>
|
||||
<img
|
||||
alt={recipe.name}
|
||||
className="card-thumbnail"
|
||||
src={cdnImage("recipes", recipe.id)}
|
||||
/>
|
||||
<div className="recipe-info">
|
||||
<h4>{recipe.name}</h4>
|
||||
<p className="recipe-description">{recipe.description}</p>
|
||||
<div className="recipe-bonus">
|
||||
<span className="bonus-label">
|
||||
{bonusLabel[recipe.bonus.type] ?? recipe.bonus.type}
|
||||
</span>
|
||||
<span className="bonus-value">
|
||||
{"×"}
|
||||
{recipe.bonus.value.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="recipe-requirements">
|
||||
{recipe.requiredMaterials.map((request) => {
|
||||
const have = getQuantity(request.materialId);
|
||||
const enough = have >= request.quantity;
|
||||
const matName
|
||||
= GODDESS_MATERIALS.find((mat) => {
|
||||
return mat.id === request.materialId;
|
||||
})?.name ?? request.materialId;
|
||||
return (
|
||||
<span
|
||||
className={`req-tag ${
|
||||
enough
|
||||
? "req-met"
|
||||
: "req-missing"
|
||||
}`}
|
||||
key={request.materialId}
|
||||
>
|
||||
{matName}
|
||||
{": "}
|
||||
{formatNumber(have)}
|
||||
{"/"}
|
||||
{formatNumber(request.quantity)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="recipe-action">
|
||||
{crafted
|
||||
? <span className="quest-badge active">
|
||||
{"✅ Crafted"}
|
||||
</span>
|
||||
: <button
|
||||
className="craft-button"
|
||||
disabled={
|
||||
!affordable || isPending || pendingRecipeId !== null
|
||||
}
|
||||
onClick={handleCraftClick}
|
||||
type="button"
|
||||
>
|
||||
{isPending
|
||||
? "Crafting..."
|
||||
: "⚗️ Craft"}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export { GoddessCraftingPanel };
|
||||
Reference in New Issue
Block a user