@@ -4,7 +4,7 @@ import {storeToRefs} from 'pinia';
44import ModalOverlay from ' components/ModalOverlay.vue' ;
55
66import {useLoginStore , useUserStatsStore , useUserPreferencesStore , useCellHistoryStore , useProofreadingBackendStore , CellHistoryEntry } from ' ../store' ;
7- import {BADGE_DEFINITIONS , BadgeDefinition } from ' ../widgets/badge_definitions' ;
7+ import {BADGE_DEFINITIONS , BUILDING_BADGES , EXPLORATION_BADGES , BadgeDefinition , BadgeTrack , statKeyForTrack } from ' ../widgets/badge_definitions' ;
88import {BADGE_IMAGE_MAP } from ' ../widgets/badge_images' ;
99import {DEMO_USERS , DEMO_COMMUNITY_EDITS_WEEK , DEMO_COMMUNITY_EDITS_MONTH } from ' ../data/demo-users' ;
1010
@@ -272,27 +272,47 @@ function getBadgeUrl(imageKey: string): string {
272272 return BADGE_IMAGE_MAP [imageKey ] ?? ' ' ;
273273}
274274
275- function isBadgeEarned(editThreshold : number ): boolean {
276- if (editThreshold === 0 ) return false ;
277- return (stats .value .editsAllTime ?? 0 ) >= editThreshold ;
275+ /** Get the player's current count for a given track. */
276+ function statForTrack(track : BadgeTrack ): number {
277+ return track === ' building'
278+ ? (stats .value .editsAllTime ?? 0 )
279+ : (stats .value .cellsSubmitted ?? 0 );
280+ }
281+
282+ function isBadgeEarned(badge : BadgeDefinition ): boolean {
283+ if (badge .threshold === 0 ) return false ;
284+ return statForTrack (badge .track ) >= badge .threshold ;
278285}
279286
280287function onBadgeClick(badge : BadgeDefinition ) {
281- if (! isBadgeEarned (badge . editThreshold )) return ;
288+ if (! isBadgeEarned (badge )) return ;
282289 // Toggle: click same badge again to return to cells canvas
283290 selectedBadge .value = selectedBadge .value ?.id === badge .id ? null : badge ;
284291}
285292
286- // ── Achievement countdown ─────────────────────────────────────────────────────
293+ /** Label for the threshold in badge detail. */
294+ function thresholdLabel(badge : BadgeDefinition ): string {
295+ return badge .track === ' building' ? ' edits' : ' cells completed' ;
296+ }
297+
298+ // ── Achievement countdown (shows next badge across both tracks) ──────────────
287299const nextAchievement = computed (() => {
288- const allTime = stats .value .editsAllTime ?? 0 ;
289- const sorted = [... BADGE_DEFINITIONS ].filter (b => b .editThreshold > 0 ).sort ((a , b ) => a .editThreshold - b .editThreshold );
290- const next = sorted .find (b => allTime < b .editThreshold );
291- if (! next ) return null ;
292- const prev = sorted [sorted .indexOf (next ) - 1 ]?.editThreshold ?? 0 ;
293- const remaining = next .editThreshold - allTime ;
294- const pct = Math .min (100 , Math .round (((allTime - prev ) / (next .editThreshold - prev )) * 100 ));
295- return { name: next .name , threshold: next .editThreshold , remaining , pct };
300+ // Find next unearned badge from each track, show whichever is closer
301+ function nextFor(badges : BadgeDefinition [], current : number ) {
302+ const sorted = [... badges ].filter (b => b .threshold > 0 ).sort ((a , b ) => a .threshold - b .threshold );
303+ const next = sorted .find (b => current < b .threshold );
304+ if (! next ) return null ;
305+ const prev = sorted [sorted .indexOf (next ) - 1 ]?.threshold ?? 0 ;
306+ const remaining = next .threshold - current ;
307+ const pct = Math .min (100 , Math .round (((current - prev ) / (next .threshold - prev )) * 100 ));
308+ return { name: next .name , threshold: next .threshold , remaining , pct , track: next .track as BadgeTrack };
309+ }
310+ const building = nextFor (BUILDING_BADGES , stats .value .editsAllTime ?? 0 );
311+ const exploration = nextFor (EXPLORATION_BADGES , stats .value .cellsSubmitted ?? 0 );
312+ // Prefer whichever has fewer remaining (closer to unlock)
313+ if (! building ) return exploration ;
314+ if (! exploration ) return building ;
315+ return building .remaining <= exploration .remaining ? building : exploration ;
296316});
297317
298318// ── Current dataset helper (for filtering cells) ────────────────────────────
@@ -571,7 +591,7 @@ const emit = defineEmits({hide: null, 'open-settings': null});
571591 <div class =" nge-profile-section-label nge-profile-section-label--green" >▌ Next Achievement</div >
572592 <div class =" nge-profile-countdown-row" >
573593 <div class =" nge-profile-countdown-name" >{{ nextAchievement.name }}</div >
574- <div class =" nge-profile-countdown-remaining" >{{ nextAchievement.remaining.toLocaleString() }} edits to go</div >
594+ <div class =" nge-profile-countdown-remaining" >{{ nextAchievement.remaining.toLocaleString() }} {{ nextAchievement.track === 'building' ? ' edits' : 'cells' }} to go</div >
575595 </div >
576596 <div class =" nge-profile-countdown-track" >
577597 <div class =" nge-profile-countdown-fill" :style =" { width: nextAchievement.pct + '%' }" ></div >
@@ -583,27 +603,65 @@ const emit = defineEmits({hide: null, 'open-settings': null});
583603 </div >
584604 </div >
585605
586- <!-- Badges -->
606+ <!-- Building Badges (edits) -->
587607 <div class =" nge-profile-section nge-profile-section--badges" >
588- <div class =" nge-profile-section-label" >▌ Badges</div >
589- <div class =" nge-profile-badges-hint" v-if =" selectedBadge" >
608+ <div class =" nge-profile-section-label" style = " color : #ffd08a ; " >▌ Building Badges < span style = " font-size : 0.75 em ; opacity : 0.6 ; " >(edits)</ span > </div >
609+ <div class =" nge-profile-badges-hint" v-if =" selectedBadge?.track === 'building' " >
590610 badge detail on the right · click again to dismiss
591611 </div >
592612 <div class =" nge-profile-badges-grid" >
593613 <div
594- v-for =" badge in BADGE_DEFINITIONS "
614+ v-for =" badge in BUILDING_BADGES "
595615 :key =" badge.id"
596616 class =" nge-profile-badge"
597617 :class =" {
598- 'nge-profile-badge--locked': !isBadgeEarned(badge.editThreshold ),
618+ 'nge-profile-badge--locked': !isBadgeEarned(badge),
599619 'nge-profile-badge--selected': selectedBadge?.id === badge.id,
600620 }"
601- :title =" isBadgeEarned(badge.editThreshold )
621+ :title =" isBadgeEarned(badge)
602622 ? badge.name + ' — click to see detail'
603623 : '??? — keep editing to unlock!'"
604624 @click =" onBadgeClick(badge)"
605625 >
606- <template v-if =" isBadgeEarned (badge .editThreshold )" >
626+ <template v-if =" isBadgeEarned (badge )" >
627+ <div class =" nge-profile-badge-img" >
628+ <img :src =" getBadgeUrl(badge.imageKey)" :alt =" badge.name" class =" nge-profile-badge-icon" />
629+ </div >
630+ <div class =" nge-profile-badge-name" >{{ badge.name }}</div >
631+ </template >
632+ <template v-else >
633+ <div class =" nge-profile-badge-img" >
634+ <div class =" nge-profile-badge-mystery" >
635+ <span class =" nge-profile-badge-mystery-q" >?</span >
636+ </div >
637+ </div >
638+ <div class =" nge-profile-badge-name nge-profile-badge-name--locked" >???</div >
639+ </template >
640+ </div >
641+ </div >
642+ </div >
643+
644+ <!-- Exploration Badges (cells completed) -->
645+ <div class =" nge-profile-section nge-profile-section--badges" >
646+ <div class =" nge-profile-section-label" style =" color : #90fff2 ;" >▌ Exploration Badges <span style =" font-size : 0.75em ; opacity : 0.6 ;" >(cells)</span ></div >
647+ <div class =" nge-profile-badges-hint" v-if =" selectedBadge?.track === 'exploration'" >
648+ badge detail on the right · click again to dismiss
649+ </div >
650+ <div class =" nge-profile-badges-grid" >
651+ <div
652+ v-for =" badge in EXPLORATION_BADGES"
653+ :key =" badge.id"
654+ class =" nge-profile-badge"
655+ :class =" {
656+ 'nge-profile-badge--locked': !isBadgeEarned(badge),
657+ 'nge-profile-badge--selected': selectedBadge?.id === badge.id,
658+ }"
659+ :title =" isBadgeEarned(badge)
660+ ? badge.name + ' — click to see detail'
661+ : '??? — complete more cells to unlock!'"
662+ @click =" onBadgeClick(badge)"
663+ >
664+ <template v-if =" isBadgeEarned (badge )" >
607665 <div class =" nge-profile-badge-img" >
608666 <img :src =" getBadgeUrl(badge.imageKey)" :alt =" badge.name" class =" nge-profile-badge-icon" />
609667 </div >
@@ -708,7 +766,7 @@ const emit = defineEmits({hide: null, 'open-settings': null});
708766 <div class =" nge-profile-viz-badge-desc" >{{ selectedBadge.description }}</div >
709767 <div class =" nge-profile-viz-badge-threshold" >
710768 Unlocked at<br >
711- <strong >{{ selectedBadge.editThreshold .toLocaleString() }}</strong > edits
769+ <strong >{{ selectedBadge.threshold .toLocaleString() }}</strong > {{ thresholdLabel(selectedBadge) }}
712770 </div >
713771 <button class =" nge-profile-viz-badge-back" @click =" selectedBadge = null" >
714772 ← Back to cells map
0 commit comments