generated from nhcarrigan/template
397169e3dc
Add an expansion preview system so the Goddess and Vampire panels render their full content (zones, bosses, thralls, achievements, etc.) even when the expansion has not been unlocked, with all interactive elements visually disabled. - API /load now returns expansionPreview alongside game state, populated from initialGoddessState() and initialVampireState() — never part of the saved blob - LoadResponse type updated with expansionPreview field - gameContext exposes goddessPreview and vampirePreview, stored in separate state vars that never touch stateReference so saves are never polluted - gameLayout applies expansion-preview CSS class when viewing a locked expansion with preview data available, and shows the coming-soon banner - All 22 expansion panels updated to use state.vampire ?? vampirePreview and state.goddess ?? goddessPreview for display - CSS disables all buttons/inputs/selects inside .expansion-preview - apotheosis service patched to never auto-initialise goddess state — expansion remains locked until explicitly released
265 lines
9.1 KiB
TypeScript
265 lines
9.1 KiB
TypeScript
/**
|
||
* @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 */
|
||
/* eslint-disable complexity -- Expansion preview fallback adds necessary branching */
|
||
|
||
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, goddessPreview } = 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.goddess ?? goddessPreview;
|
||
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 };
|