generated from nhcarrigan/template
feat: add exploration and crafting systems
Adds two new game systems: Exploration (scouts collect materials from timed area runs) and Crafting (combine materials into permanent multipliers). Includes 72 exploration areas, 54 materials, 36 recipes, and 108 new Codex lore entries. Removes unused characterName requirement from prestige/transcendence/apotheosis reset flows.
This commit is contained in:
@@ -33,7 +33,7 @@ const HOW_TO_PLAY = [
|
||||
},
|
||||
{
|
||||
title: "⭐ Prestige",
|
||||
body: "When you've progressed far enough, you can prestige to earn runestones — a permanent currency that persists across all runs. Prestige resets your current run but grants a production multiplier that stacks with every prestige. Name your prestige character to commemorate the run!",
|
||||
body: "When you've progressed far enough, you can prestige to earn runestones — a permanent currency that persists across all runs. Prestige resets your current run but grants a production multiplier that stacks with every prestige.",
|
||||
},
|
||||
{
|
||||
title: "🔮 Runestones & Prestige Upgrades",
|
||||
@@ -51,9 +51,17 @@ const HOW_TO_PLAY = [
|
||||
title: "📅 Daily Challenges",
|
||||
body: "Complete daily challenges for bonus rewards including gold, essence, crystals, and runestones. Challenges reset each day and vary in difficulty. Completing all daily challenges gives an extra bonus reward.",
|
||||
},
|
||||
{
|
||||
title: "🗺️ Exploration",
|
||||
body: "Send scouts to explore areas within each zone. Explorations run in real-time and reward gold, essence, and crafting materials when collected. Each area has a set duration — short explorations are faster but longer ones offer rarer finds. A 📖 icon marks areas you've collected from at least once, unlocking a Codex entry.",
|
||||
},
|
||||
{
|
||||
title: "⚗️ Crafting",
|
||||
body: "Use materials gathered from exploration to craft permanent bonuses. Each recipe provides a multiplier to gold income, essence income, click power, or combat power — all of which stack and persist across prestige runs. Check the Crafting tab to see your material inventory and available recipes per zone.",
|
||||
},
|
||||
{
|
||||
title: "📖 Codex",
|
||||
body: "Defeating bosses, completing quests, acquiring equipment, hiring adventurers, purchasing upgrades, unlocking prestige upgrades, and discovering new zones all permanently unlock lore entries in the Codex. A badge appears on the Codex tab and a toast notification pops up each time new lore is discovered. Collect all 364 entries to build a complete picture of the world of Elysium.",
|
||||
body: "Defeating bosses, completing quests, acquiring equipment, hiring adventurers, purchasing upgrades, unlocking prestige upgrades, discovering new zones, collecting from exploration areas, and crafting recipes all permanently unlock lore entries in the Codex. A badge appears on the Codex tab and a toast notification pops up each time new lore is discovered. Collect all 472 entries to build a complete picture of the world of Elysium.",
|
||||
},
|
||||
{
|
||||
title: "☁️ Cloud Saves",
|
||||
|
||||
@@ -6,7 +6,6 @@ const TOTAL_ECHO_UPGRADES = TRANSCENDENCE_UPGRADES.length;
|
||||
|
||||
export const ApotheosisPanel = (): React.JSX.Element => {
|
||||
const { state, apotheosis } = useGame();
|
||||
const [characterName, setCharacterName] = useState("");
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [result, setResult] = useState<number | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -19,11 +18,10 @@ export const ApotheosisPanel = (): React.JSX.Element => {
|
||||
const apotheosisCount = state.apotheosis?.count ?? 0;
|
||||
|
||||
const handleApotheosis = async (): Promise<void> => {
|
||||
if (!characterName.trim()) return;
|
||||
setIsPending(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await apotheosis(characterName.trim());
|
||||
const data = await apotheosis();
|
||||
setResult(data.newApotheosisCount);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Apotheosis failed");
|
||||
@@ -75,18 +73,10 @@ export const ApotheosisPanel = (): React.JSX.Element => {
|
||||
|
||||
{isEligible && (
|
||||
<div className="prestige-form">
|
||||
<p>This action is <strong>permanent and irreversible</strong>. Choose your name for the next cycle:</p>
|
||||
<input
|
||||
disabled={isPending}
|
||||
maxLength={32}
|
||||
onChange={(e) => { setCharacterName(e.target.value); }}
|
||||
placeholder="Character name..."
|
||||
type="text"
|
||||
value={characterName}
|
||||
/>
|
||||
<p>This action is <strong>permanent and irreversible</strong>.</p>
|
||||
<button
|
||||
className="apotheosis-button"
|
||||
disabled={isPending || !characterName.trim()}
|
||||
disabled={isPending}
|
||||
onClick={() => { void handleApotheosis(); }}
|
||||
type="button"
|
||||
>
|
||||
|
||||
@@ -11,6 +11,8 @@ const SOURCE_BADGE: Record<CodexEntry["sourceType"], string> = {
|
||||
upgrade: "🔧",
|
||||
prestige: "🔮",
|
||||
zone: "🗺️",
|
||||
exploration: "🧭",
|
||||
recipe: "⚗️",
|
||||
};
|
||||
|
||||
export const CodexPanel = (): React.JSX.Element => {
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { useState } from "react";
|
||||
import { useGame } from "../../context/GameContext.js";
|
||||
import { MATERIALS } from "../../data/materials.js";
|
||||
import { RECIPES } from "../../data/recipes.js";
|
||||
import { ZoneSelector } from "./ZoneSelector.js";
|
||||
|
||||
const BONUS_LABEL: Record<string, string> = {
|
||||
gold_income: "🪙 Gold Income",
|
||||
essence_income: "✨ Essence Income",
|
||||
click_power: "👆 Click Power",
|
||||
combat_power: "⚔️ Combat Power",
|
||||
};
|
||||
|
||||
export const CraftingPanel = (): React.JSX.Element => {
|
||||
const { state, craftRecipe, formatNumber } = useGame();
|
||||
const [activeZoneId, setActiveZoneId] = useState("verdant_vale");
|
||||
const [pendingRecipeId, setPendingRecipeId] = useState<string | null>(null);
|
||||
|
||||
if (!state) return <section className="panel"><p>Loading...</p></section>;
|
||||
|
||||
const zones = state.zones ?? [];
|
||||
const explorationState = state.exploration;
|
||||
const playerMaterials = explorationState?.materials ?? [];
|
||||
const craftedIds = explorationState?.craftedRecipeIds ?? [];
|
||||
|
||||
const zoneRecipes = RECIPES.filter((r) => r.zoneId === activeZoneId);
|
||||
const zoneMaterials = MATERIALS.filter((m) => m.zoneId === activeZoneId);
|
||||
|
||||
const getQuantity = (materialId: string): number =>
|
||||
playerMaterials.find((m) => m.materialId === materialId)?.quantity ?? 0;
|
||||
|
||||
const canAfford = (recipeId: string): boolean => {
|
||||
const recipe = RECIPES.find((r) => r.id === recipeId);
|
||||
if (!recipe) return false;
|
||||
return recipe.requiredMaterials.every(
|
||||
(req) => getQuantity(req.materialId) >= req.quantity,
|
||||
);
|
||||
};
|
||||
|
||||
const handleCraft = async (recipeId: string): Promise<void> => {
|
||||
setPendingRecipeId(recipeId);
|
||||
try {
|
||||
await craftRecipe(recipeId);
|
||||
} finally {
|
||||
setPendingRecipeId(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="panel crafting-panel">
|
||||
<div className="panel-header">
|
||||
<h2>⚗️ Crafting</h2>
|
||||
</div>
|
||||
|
||||
<ZoneSelector
|
||||
activeZoneId={activeZoneId}
|
||||
zones={zones}
|
||||
onSelectZone={(id) => { setActiveZoneId(id); }}
|
||||
/>
|
||||
|
||||
<div className="crafting-content">
|
||||
<div className="materials-section">
|
||||
<h3>📦 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 key={material.id} className={`material-card rarity-${material.rarity} ${qty === 0 ? "material-empty" : ""}`}>
|
||||
<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>📜 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 = canAfford(recipe.id);
|
||||
const isPending = pendingRecipeId === recipe.id;
|
||||
|
||||
return (
|
||||
<div key={recipe.id} className={`recipe-card ${crafted ? "recipe-crafted" : ""} ${!affordable && !crafted ? "recipe-unaffordable" : ""}`}>
|
||||
<div className="recipe-info">
|
||||
<h4>{recipe.name}</h4>
|
||||
<p className="recipe-description">{recipe.description}</p>
|
||||
<div className="recipe-bonus">
|
||||
<span className="bonus-label">{BONUS_LABEL[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((req) => {
|
||||
const have = getQuantity(req.materialId);
|
||||
const enough = have >= req.quantity;
|
||||
const matName = MATERIALS.find((m) => m.id === req.materialId)?.name ?? req.materialId;
|
||||
return (
|
||||
<span key={req.materialId} className={`req-tag ${enough ? "req-met" : "req-missing"}`}>
|
||||
{matName}: {formatNumber(have)}/{formatNumber(req.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={() => { void handleCraft(recipe.id); }}
|
||||
type="button"
|
||||
>
|
||||
{isPending ? "Crafting..." : "⚗️ Craft"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,172 @@
|
||||
import type { ExploreCollectResponse } from "@elysium/types";
|
||||
import { useState } from "react";
|
||||
import { useGame } from "../../context/GameContext.js";
|
||||
import { EXPLORATION_AREAS } from "../../data/explorations.js";
|
||||
import { ZoneSelector } from "./ZoneSelector.js";
|
||||
|
||||
const formatDuration = (seconds: number): string => {
|
||||
if (seconds >= 86400) {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
|
||||
}
|
||||
if (seconds >= 3600) return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
|
||||
if (seconds >= 60) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
||||
return `${seconds}s`;
|
||||
};
|
||||
|
||||
const timeRemaining = (startedAt: number, durationSeconds: number): number => {
|
||||
const elapsed = (Date.now() - startedAt) / 1000;
|
||||
return Math.max(0, durationSeconds - elapsed);
|
||||
};
|
||||
|
||||
interface CollectResult {
|
||||
areaId: string;
|
||||
response: ExploreCollectResponse;
|
||||
}
|
||||
|
||||
export const ExplorationPanel = (): React.JSX.Element => {
|
||||
const { state, startExploration, collectExploration, formatNumber } = useGame();
|
||||
const [activeZoneId, setActiveZoneId] = useState("verdant_vale");
|
||||
const [pendingAreaId, setPendingAreaId] = useState<string | null>(null);
|
||||
const [lastResult, setLastResult] = useState<CollectResult | null>(null);
|
||||
|
||||
if (!state) return <section className="panel"><p>Loading...</p></section>;
|
||||
|
||||
const zones = state.zones ?? [];
|
||||
const explorationState = state.exploration;
|
||||
|
||||
const zoneAreas = EXPLORATION_AREAS.filter((a) => a.zoneId === activeZoneId);
|
||||
|
||||
const handleStart = async (areaId: string): Promise<void> => {
|
||||
setPendingAreaId(areaId);
|
||||
try {
|
||||
await startExploration(areaId);
|
||||
} finally {
|
||||
setPendingAreaId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCollect = async (areaId: string): Promise<void> => {
|
||||
setPendingAreaId(areaId);
|
||||
try {
|
||||
const result = await collectExploration(areaId);
|
||||
setLastResult({ areaId, response: result });
|
||||
} finally {
|
||||
setPendingAreaId(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="panel exploration-panel">
|
||||
<div className="panel-header">
|
||||
<h2>🗺️ Exploration</h2>
|
||||
</div>
|
||||
|
||||
{lastResult && (
|
||||
<div className="exploration-result">
|
||||
<button
|
||||
className="exploration-result-close"
|
||||
onClick={() => { setLastResult(null); }}
|
||||
type="button"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
{lastResult.response.foundNothing ? (
|
||||
<p className="exploration-nothing">{lastResult.response.nothingMessage}</p>
|
||||
) : (
|
||||
<>
|
||||
{lastResult.response.event && (
|
||||
<p className="exploration-event-text">{lastResult.response.event.text}</p>
|
||||
)}
|
||||
<div className="exploration-rewards">
|
||||
{(lastResult.response.event?.goldChange ?? 0) !== 0 && (
|
||||
<span className={`reward-tag ${(lastResult.response.event?.goldChange ?? 0) > 0 ? "" : "negative"}`}>
|
||||
🪙 {(lastResult.response.event?.goldChange ?? 0) > 0 ? "+" : ""}{formatNumber(lastResult.response.event?.goldChange ?? 0)} gold
|
||||
</span>
|
||||
)}
|
||||
{(lastResult.response.event?.essenceChange ?? 0) > 0 && (
|
||||
<span className="reward-tag">
|
||||
✨ +{formatNumber(lastResult.response.event?.essenceChange ?? 0)} essence
|
||||
</span>
|
||||
)}
|
||||
{lastResult.response.event?.materialGained && (
|
||||
<span className="reward-tag material-tag">
|
||||
📦 +{lastResult.response.event.materialGained.quantity} {lastResult.response.event.materialGained.materialId.replace(/_/g, " ")} (event)
|
||||
</span>
|
||||
)}
|
||||
{lastResult.response.materialsFound.map((m) => (
|
||||
<span key={m.materialId} className="reward-tag material-tag">
|
||||
📦 +{m.quantity} {m.materialId.replace(/_/g, " ")}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ZoneSelector
|
||||
activeZoneId={activeZoneId}
|
||||
zones={zones}
|
||||
onSelectZone={(id) => { setActiveZoneId(id); setLastResult(null); }}
|
||||
/>
|
||||
|
||||
<div className="exploration-list">
|
||||
{zoneAreas.map((area) => {
|
||||
const areaState = explorationState?.areas.find((a) => a.id === area.id);
|
||||
const status = areaState?.status ?? "locked";
|
||||
const startedAt = areaState?.startedAt ?? 0;
|
||||
const isReady = status === "in_progress" && timeRemaining(startedAt, area.durationSeconds) <= 0;
|
||||
const isPending = pendingAreaId === area.id;
|
||||
|
||||
return (
|
||||
<div key={area.id} className={`exploration-card exploration-${status}`}>
|
||||
<div className="exploration-info">
|
||||
<h3>
|
||||
{area.name}
|
||||
{areaState?.completedOnce && <span className="exploration-discovered"> 📖</span>}
|
||||
</h3>
|
||||
<p>{area.description}</p>
|
||||
<span className="exploration-duration">⏱️ {formatDuration(area.durationSeconds)}</span>
|
||||
</div>
|
||||
<div className="exploration-action">
|
||||
{status === "locked" && (
|
||||
<span className="quest-badge locked">🔒 Locked</span>
|
||||
)}
|
||||
{status === "available" && (
|
||||
<button
|
||||
className="start-quest-button"
|
||||
disabled={isPending}
|
||||
onClick={() => { void handleStart(area.id); }}
|
||||
type="button"
|
||||
>
|
||||
{isPending ? "Departing..." : `Explore (${formatDuration(area.durationSeconds)})`}
|
||||
</button>
|
||||
)}
|
||||
{status === "in_progress" && !isReady && (
|
||||
<span className="quest-badge active">
|
||||
⏳ {formatDuration(Math.ceil(timeRemaining(startedAt, area.durationSeconds)))} remaining
|
||||
</span>
|
||||
)}
|
||||
{(status === "in_progress" && isReady) && (
|
||||
<button
|
||||
className="collect-button"
|
||||
disabled={isPending}
|
||||
onClick={() => { void handleCollect(area.id); }}
|
||||
type="button"
|
||||
>
|
||||
{isPending ? "Collecting..." : "📦 Collect Results"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{zoneAreas.length === 0 && (
|
||||
<p className="empty-zone">No exploration areas in this zone.</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -20,8 +20,10 @@ import { QuestPanel } from "./QuestPanel.js";
|
||||
import { StatisticsPanel } from "./StatisticsPanel.js";
|
||||
import { UpgradePanel } from "./UpgradePanel.js";
|
||||
import { DailyChallengePanel } from "./DailyChallengePanel.js";
|
||||
import { ExplorationPanel } from "./ExplorationPanel.js";
|
||||
import { CraftingPanel } from "./CraftingPanel.js";
|
||||
|
||||
type Tab = "adventurers" | "upgrades" | "quests" | "bosses" | "equipment" | "achievements" | "prestige" | "transcendence" | "apotheosis" | "statistics" | "daily" | "codex" | "about";
|
||||
type Tab = "adventurers" | "upgrades" | "quests" | "bosses" | "equipment" | "achievements" | "prestige" | "transcendence" | "apotheosis" | "statistics" | "daily" | "codex" | "about" | "exploration" | "crafting";
|
||||
|
||||
const BASE_TABS: { id: Tab; label: string }[] = [
|
||||
{ id: "adventurers", label: "⚔️ Adventurers" },
|
||||
@@ -33,6 +35,8 @@ const BASE_TABS: { id: Tab; label: string }[] = [
|
||||
{ id: "prestige", label: "⭐ Prestige" },
|
||||
{ id: "transcendence", label: "🌌 Transcendence" },
|
||||
{ id: "apotheosis", label: "✨ Apotheosis" },
|
||||
{ id: "exploration", label: "🗺️ Exploration" },
|
||||
{ id: "crafting", label: "⚗️ Crafting" },
|
||||
{ id: "statistics", label: "📊 Statistics" },
|
||||
{ id: "daily", label: "📅 Daily" },
|
||||
{ id: "codex", label: "📖 Codex" },
|
||||
@@ -120,6 +124,8 @@ export const GameLayout = (): React.JSX.Element => {
|
||||
{activeTab === "prestige" && <PrestigePanel />}
|
||||
{activeTab === "transcendence" && <TranscendencePanel />}
|
||||
{activeTab === "apotheosis" && <ApotheosisPanel />}
|
||||
{activeTab === "exploration" && <ExplorationPanel />}
|
||||
{activeTab === "crafting" && <CraftingPanel />}
|
||||
{activeTab === "statistics" && <StatisticsPanel />}
|
||||
{activeTab === "daily" && <DailyChallengePanel />}
|
||||
{activeTab === "codex" && <CodexPanel />}
|
||||
|
||||
@@ -41,7 +41,6 @@ const CATEGORY_ORDER: PrestigeUpgradeCategory[] = [
|
||||
|
||||
export const PrestigePanel = (): React.JSX.Element => {
|
||||
const { state, reload, formatNumber, buyPrestigeUpgrade, toggleAutoPrestige } = useGame();
|
||||
const [characterName, setCharacterName] = useState("");
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [result, setResult] = useState<{ runestones: number; count: number; milestoneRunestones: number } | null>(null);
|
||||
const [prestigeError, setPrestigeError] = useState<string | null>(null);
|
||||
@@ -61,11 +60,10 @@ export const PrestigePanel = (): React.JSX.Element => {
|
||||
const nextMultiplier = calculateProductionMultiplier(prestigeData.count + 1);
|
||||
|
||||
const handlePrestige = async (): Promise<void> => {
|
||||
if (!characterName.trim()) return;
|
||||
setIsPending(true);
|
||||
setPrestigeError(null);
|
||||
try {
|
||||
const data = await prestige({ characterName: characterName.trim() });
|
||||
const data = await prestige({});
|
||||
setResult({ runestones: data.runestones, count: data.newPrestigeCount, milestoneRunestones: data.milestoneRunestones });
|
||||
await reload();
|
||||
} catch (err) {
|
||||
@@ -156,18 +154,10 @@ export const PrestigePanel = (): React.JSX.Element => {
|
||||
|
||||
{isEligible ? (
|
||||
<div className="prestige-form">
|
||||
<p>You are ready to prestige! Choose your new character name:</p>
|
||||
<input
|
||||
disabled={isPending}
|
||||
maxLength={32}
|
||||
onChange={(e) => { setCharacterName(e.target.value); }}
|
||||
placeholder="Character name..."
|
||||
type="text"
|
||||
value={characterName}
|
||||
/>
|
||||
<p>You are ready to prestige!</p>
|
||||
<button
|
||||
className="prestige-button"
|
||||
disabled={isPending || !characterName.trim()}
|
||||
disabled={isPending}
|
||||
onClick={() => { void handlePrestige(); }}
|
||||
type="button"
|
||||
>
|
||||
|
||||
@@ -24,7 +24,6 @@ const CATEGORY_ORDER: TranscendenceUpgradeCategory[] = [
|
||||
|
||||
export const TranscendencePanel = (): React.JSX.Element => {
|
||||
const { state, formatNumber, transcend, buyEchoUpgrade } = useGame();
|
||||
const [characterName, setCharacterName] = useState("");
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [result, setResult] = useState<{ echoes: number; count: number } | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -41,11 +40,10 @@ export const TranscendencePanel = (): React.JSX.Element => {
|
||||
const transcendenceCount = transcendence?.count ?? 0;
|
||||
|
||||
const handleTranscend = async (): Promise<void> => {
|
||||
if (!characterName.trim()) return;
|
||||
setIsPending(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await transcend(characterName.trim());
|
||||
const data = await transcend();
|
||||
setResult({ echoes: data.echoes, count: data.newTranscendenceCount });
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Transcendence failed");
|
||||
@@ -136,18 +134,9 @@ export const TranscendencePanel = (): React.JSX.Element => {
|
||||
{hasDefeatedFinalBoss && (
|
||||
<div className="prestige-form">
|
||||
<p>You are ready to transcend. This action is <strong>irreversible</strong>.</p>
|
||||
<p>Choose your new character name for the next cycle:</p>
|
||||
<input
|
||||
disabled={isPending}
|
||||
maxLength={32}
|
||||
onChange={(e) => { setCharacterName(e.target.value); }}
|
||||
placeholder="Character name..."
|
||||
type="text"
|
||||
value={characterName}
|
||||
/>
|
||||
<button
|
||||
className="transcendence-button"
|
||||
disabled={isPending || !characterName.trim()}
|
||||
disabled={isPending}
|
||||
onClick={() => { void handleTranscend(); }}
|
||||
type="button"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user