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:
2026-04-13 18:38:27 -07:00
committed by Naomi Carrigan
parent 96d6759661
commit 91c9f52daf
21 changed files with 7093 additions and 108 deletions
@@ -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 };