From 56664b56d3ae36305eef64b2427e12ad9b2d8d38 Mon Sep 17 00:00:00 2001 From: gahusb Date: Mon, 4 May 2026 04:06:54 +0900 Subject: [PATCH] feat: surface recipe hints on undiscovered codex slots (A-5) Each tier section now shows up to 6 undiscovered silhouettes after the owned cards, sorted by craftable-now > known-ingredients > unknown. Hint is the two-ingredient emoji pair, with "?" when an ingredient has not been found. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/screens/ElementsScreen.tsx | 115 ++++++++++++++++++++-- 1 file changed, 105 insertions(+), 10 deletions(-) diff --git a/src/components/screens/ElementsScreen.tsx b/src/components/screens/ElementsScreen.tsx index 5d7ee28..10856a6 100644 --- a/src/components/screens/ElementsScreen.tsx +++ b/src/components/screens/ElementsScreen.tsx @@ -795,6 +795,14 @@ const undiscoveredNameStyle = css` text-align: center; `; +const undiscoveredHintStyle = (craftable: boolean) => css` + font-size: 9px; + font-weight: 800; + color: ${craftable ? adaptive.blue500 : adaptive.grey400}; + letter-spacing: 0.5px; + margin-top: 2px; +`; + const elementCountStyle = css` font-size: 11px; color: ${adaptive.blue500}; @@ -1115,6 +1123,34 @@ const TIER_LABELS: Record = { type ElementData = (typeof elementsData)[number]; +const elementMap = Object.fromEntries(elementsData.map((el) => [el.id, el])); + +interface RecipeHint { + display: string; + craftableNow: boolean; +} + +function getRecipeHintForElement( + elementId: string, + discoveredIds: string[], + ownedCounts: Record +): RecipeHint | null { + const recipe = recipesData.find((r) => r.result === elementId); + if (!recipe) return null; + const [aId, bId] = recipe.ingredients; + const aDiscovered = discoveredIds.includes(aId); + const bDiscovered = discoveredIds.includes(bId); + const aSym = aDiscovered ? (elementMap[aId]?.emoji ?? '?') : '?'; + const bSym = bDiscovered ? (elementMap[bId]?.emoji ?? '?') : '?'; + const sameElement = aId === bId; + const hasA = (ownedCounts[aId] ?? 0) >= (sameElement ? 2 : 1); + const hasB = sameElement ? hasA : (ownedCounts[bId] ?? 0) >= 1; + return { + display: `${aSym} + ${bSym}`, + craftableNow: aDiscovered && bDiscovered && hasA && hasB, + }; +} + interface ActivityEntry { id: number; text: string; @@ -1267,6 +1303,8 @@ interface ElementCardProps { onSelect: (el: ElementData) => void; isAutomated?: boolean; isTutorialTarget?: boolean; + mood?: Mood; + hint?: RecipeHint | null; } const ElementCard = memo(function ElementCard({ @@ -1277,6 +1315,8 @@ const ElementCard = memo(function ElementCard({ onSelect, isAutomated = true, isTutorialTarget, + mood, + hint, }: ElementCardProps) { const spawnRate = calcSpawnRate(el.id, level); @@ -1284,9 +1324,20 @@ const ElementCard = memo(function ElementCard({ return (
- +
+ +
??? + {hint && ( + {hint.display} + )}
); } @@ -1296,7 +1347,15 @@ const ElementCard = memo(function ElementCard({
๐Ÿ”’
- +
+ +
{el.name}
@@ -1310,7 +1369,9 @@ const ElementCard = memo(function ElementCard({ data-tutorial={isTutorialTarget ? 'elements-first-card' : undefined} >
- +
+ +
{el.name} ร—{count.toLocaleString()} @@ -1478,10 +1539,31 @@ export function ElementsScreen() { .filter((summary): summary is ActiveBoostSummary => summary !== null && summary.remainingMs > 0) .sort((a, b) => a.remainingMs - b.remainingMs); - const tierGroups = [1, 2, 3, 4, 5].map((tier) => ({ - tier, - items: discoveredElementList.filter((el) => el.tier === tier), - })).filter(({ items }) => items.length > 0); + const MAX_UNDISCOVERED_PER_TIER = 6; + + const tierGroups = [1, 2, 3, 4, 5].map((tier) => { + const tierElements = elementsData.filter((el) => el.tier === tier); + const owned = tierElements.filter((el) => discoveredElementIds.includes(el.id)); + const undiscovered = tierElements.filter((el) => !discoveredElementIds.includes(el.id)); + + const undiscoveredWithHints = undiscovered + .map((el) => ({ el, hint: getRecipeHintForElement(el.id, discoveredElementIds, elements) })) + .sort((a, b) => { + const aCraftable = a.hint?.craftableNow ? 1 : 0; + const bCraftable = b.hint?.craftableNow ? 1 : 0; + if (aCraftable !== bCraftable) return bCraftable - aCraftable; + const aHasHint = a.hint ? 1 : 0; + const bHasHint = b.hint ? 1 : 0; + return bHasHint - aHasHint; + }) + .slice(0, MAX_UNDISCOVERED_PER_TIER); + + return { + tier, + owned, + undiscovered: undiscoveredWithHints, + }; + }).filter(({ owned, undiscovered }) => owned.length > 0 || undiscovered.length > 0); // ์ฒซ ๋ฒˆ์งธ obtained ์›์†Œ ID (ํŠœํ† ๋ฆฌ์–ผ ์ŠคํฌํŠธ๋ผ์ดํŠธ์šฉ) const firstObtainedId = discoveredElementList[0]?.id ?? null; @@ -1799,12 +1881,12 @@ export function ElementsScreen() { - {/* ํ‹ฐ์–ด๋ณ„ ๋ณด์œ  ์›์†Œ */} - {tierGroups.map(({ tier, items }) => ( + {/* ํ‹ฐ์–ด๋ณ„ ๋ณด์œ  ์›์†Œ + ๋ฏธ๋ฐœ๊ฒฌ ์Šฌ๋กฏ ํžŒํŠธ */} + {tierGroups.map(({ tier, owned, undiscovered }) => (
{TIER_LABELS[tier]}
- {items.map((el) => ( + {owned.map((el) => ( + ))} + {undiscovered.map(({ el, hint }) => ( + ))}