3535# quest-reset {position : absolute;top : max (88px , calc (66px + env (safe-area-inset-top)));right : max (22px , env (safe-area-inset-right));z-index : 20 ;display : none;width : 34px ;height : 34px ;border : 1px solid # D4A030 ;border-radius : 4px ;background : rgba (10 , 10 , 10 , .82 );color : # E8C040 ;font : 20px monospace;cursor : pointer}
3636# quest-reset .show {display : block}
3737# quest-reset : focus-visible {outline : 3px solid # E8E0D0 ;outline-offset : 2px }
38+ # share-button {position : absolute;top : max (128px , calc (106px + env (safe-area-inset-top)));right : max (22px , env (safe-area-inset-right));z-index : 20 ;display : none;width : 34px ;height : 34px ;border : 1px solid # D4A030 ;border-radius : 4px ;background : rgba (10 , 10 , 10 , .82 );color : # E8C040 ;font : 18px monospace;cursor : pointer}
39+ # share-button .show {display : block}
40+ # share-button : focus-visible {outline : 3px solid # E8E0D0 ;outline-offset : 2px }
3841# journal-button {position : absolute;top : max (48px , calc (26px + env (safe-area-inset-top)));right : max (22px , env (safe-area-inset-right));z-index : 20 ;width : 42px ;height : 30px ;border : 1px solid # 5A8848 ;border-radius : 4px ;background : rgba (10 , 10 , 10 , .82 );color : # A8C088 ;font : 10px 'Silkscreen' , monospace;cursor : pointer}
3942# journal-button : focus-visible , # journal-close : focus-visible {outline : 3px solid # E8E0D0 ;outline-offset : 2px }
4043# sound-button {position : absolute;right : max (20px , env (safe-area-inset-right));bottom : max (20px , env (safe-area-inset-bottom));z-index : 20 ;width : 38px ;height : 34px ;border : 1px solid # 5A8848 ;border-radius : 4px ;background : rgba (10 , 10 , 10 , .82 );color : # A8C088 ;font : 18px monospace;cursor : pointer}
5760 # quest {top : calc (54px + env (safe-area-inset-top));right : 12px ;font-size : 9px }
5861 # journal-button {top : calc (74px + env (safe-area-inset-top));right : 12px }
5962 # quest-reset {top : calc (112px + env (safe-area-inset-top));right : 12px }
63+ # share-button {top : calc (152px + env (safe-area-inset-top));right : 12px }
6064 # hint {bottom : max (88px , calc (68px + env (safe-area-inset-bottom)));width : 70vw ;font-size : 10px ;text-align : center}
6165 # action-button {display : block}
6266 # sound-button {right : max (104px , calc (104px + env (safe-area-inset-right)));bottom : max (28px , calc (28px + env (safe-area-inset-bottom)))}
8993 < div id ="quest " aria-live ="polite "> SIGNALS 0/3</ div >
9094 < button id ="journal-button " type ="button " aria-label ="Open signal log "> LOG</ button >
9195 < button id ="quest-reset " type ="button " aria-label ="Restart signal quest " title ="Restart signal quest "> ↻</ button >
96+ < button id ="share-button " type ="button " aria-label ="Share completion " title ="Share completion "> ↗</ button >
9297 < button id ="action-button " type ="button " aria-label ="Scan or interact "> SCAN</ button >
9398 < button id ="sound-button " type ="button " aria-label ="Mute sound " title ="Toggle sound "> ♪</ button >
9499 < p class ="sr-only " id ="game-instructions "> Move with WASD, arrow keys, or by selecting a destination. Press Space or Enter, or use the Scan button, to scan nearby residents or interact with the character you face.</ p >
@@ -102,6 +107,7 @@ <h2 id="journal-title">SIGNAL LOG</h2>
102107< script src ="src/game-content.js "> </ script >
103108< script src ="src/progress.js "> </ script >
104109< script src ="src/feedback.js "> </ script >
110+ < script src ="src/engagement.js "> </ script >
105111< script >
106112// ══════════════════════════════════════════════════════
107113// SETUP
@@ -120,6 +126,7 @@ <h2 id="journal-title">SIGNAL LOG</h2>
120126const journalList = document . getElementById ( 'journal-list' ) ;
121127const journalClose = document . getElementById ( 'journal-close' ) ;
122128const soundButton = document . getElementById ( 'sound-button' ) ;
129+ const shareButton = document . getElementById ( 'share-button' ) ;
123130
124131const PX = 3 ;
125132const TILE = 16 ;
@@ -173,15 +180,29 @@ <h2 id="journal-title">SIGNAL LOG</h2>
173180} = LittleAndroidLogic ;
174181const world = createWorldMap ( 77 ) ;
175182const MW = world . width , MH = world . height , map = world . map ;
176- const signalFragments = LittleAndroidContent . SIGNAL_FRAGMENTS . map ( fragment => ( {
177- ...fragment ,
183+ const dailyChallenge = LittleAndroidEngagement . createDailyChallenge (
184+ new Date ( ) ,
185+ LittleAndroidContent . SIGNAL_FRAGMENTS . map ( fragment => fragment . id ) ,
186+ ) ;
187+ const fragmentContent = new Map ( LittleAndroidContent . SIGNAL_FRAGMENTS . map ( fragment => [ fragment . id , fragment ] ) ) ;
188+ const signalFragments = dailyChallenge . order . map ( id => ( {
189+ ...fragmentContent . get ( id ) ,
178190 discovered : false ,
179191 collected : false ,
180192} ) ) ;
181193let progressStorage = null ;
182194try { progressStorage = window . localStorage ; } catch { }
183- const savedProgress = LittleAndroidProgress . loadProgress ( progressStorage ) ;
195+ const loadedProgress = LittleAndroidProgress . loadProgress ( progressStorage ) ;
196+ const savedProgress = LittleAndroidProgress . scopeProgressToChallenge (
197+ loadedProgress ,
198+ dailyChallenge . dateKey ,
199+ ) ;
200+ if ( loadedProgress . challengeDate !== savedProgress . challengeDate ) {
201+ LittleAndroidProgress . saveProgress ( progressStorage , savedProgress ) ;
202+ }
184203const feedback = LittleAndroidFeedback . createFeedbackController ( window , progressStorage ) ;
204+ const metrics = LittleAndroidEngagement . createLocalMetrics ( progressStorage ) ;
205+ metrics . increment ( 'sessions' ) ;
185206let progressGeneration = savedProgress . generation ;
186207for ( const fragment of signalFragments ) {
187208 fragment . discovered = savedProgress . discoveredFragments . includes ( fragment . id ) ;
@@ -274,6 +295,7 @@ <h2 id="journal-title">SIGNAL LOG</h2>
274295 player . moveToX = tx ; player . moveToY = ty ;
275296 player . moveProgress = 0 ;
276297 player . idleTimer = 0 ;
298+ metrics . markOnce ( 'first_move' ) ;
277299}
278300
279301function drawUnit01 ( sx , sy , facing , walkFrame , moving , idleCoreOn , interacting ) {
@@ -660,6 +682,7 @@ <h2 id="journal-title">SIGNAL LOG</h2>
660682 triggerNPCInteraction ( npc ) ;
661683 rememberNPC ( npc ) ;
662684 feedback . play ( 'interact' ) ;
685+ metrics . markOnce ( 'first_interaction' ) ;
663686 const dialogue = LittleAndroidContent . NPC_DIALOGUE [ npc . type ] ;
664687 showStatus ( dialogue ? ( quest . complete ? dialogue . complete : dialogue . active ) : `LINK: ${ npc . scanLabel || npc . type . toUpperCase ( ) } ` , 3.5 ) ;
665688}
@@ -974,13 +997,15 @@ <h2 id="journal-title">SIGNAL LOG</h2>
974997
975998function updateQuestUI ( ) {
976999 const collected = signalFragments . filter ( fragment => fragment . collected ) . length ;
977- questEl . textContent = quest . complete ? ' FACTORY ONLINE' : `SIGNALS ${ collected } /${ signalFragments . length } ` ;
1000+ questEl . textContent = quest . complete ? ` FACTORY ONLINE · ${ dailyChallenge . code } ` : `SIGNALS ${ collected } /${ signalFragments . length } · ${ dailyChallenge . code } ` ;
9781001 questResetButton . classList . toggle ( 'show' , quest . complete ) ;
1002+ shareButton . classList . toggle ( 'show' , quest . complete ) ;
9791003}
9801004
9811005function currentProgress ( ) {
9821006 return {
9831007 version : LittleAndroidProgress . SAVE_VERSION ,
1008+ challengeDate : dailyChallenge . dateKey ,
9841009 generation : progressGeneration ,
9851010 discoveredFragments : signalFragments . filter ( fragment => fragment . discovered ) . map ( fragment => fragment . id ) ,
9861011 collectedFragments : signalFragments . filter ( fragment => fragment . collected ) . map ( fragment => fragment . id ) ,
@@ -990,7 +1015,9 @@ <h2 id="journal-title">SIGNAL LOG</h2>
9901015}
9911016
9921017function persistProgress ( ) {
993- const latest = LittleAndroidProgress . loadProgress ( progressStorage ) ;
1018+ const stored = LittleAndroidProgress . loadProgress ( progressStorage ) ;
1019+ if ( stored . challengeDate && stored . challengeDate !== dailyChallenge . dateKey ) return false ;
1020+ const latest = LittleAndroidProgress . scopeProgressToChallenge ( stored , dailyChallenge . dateKey ) ;
9941021 const merged = LittleAndroidProgress . mergeProgress (
9951022 latest ,
9961023 currentProgress ( ) ,
@@ -1039,6 +1066,7 @@ <h2 id="journal-title">SIGNAL LOG</h2>
10391066 showStatus ( 'FACTORY LINK RESTORED' , 4 ) ;
10401067 feedback . play ( 'complete' ) ;
10411068 feedback . haptic ( [ 25 , 35 , 50 ] ) ;
1069+ metrics . increment ( 'completions' ) ;
10421070 } else {
10431071 showStatus ( `${ fragment . name } RECOVERED · ${ collected } /${ signalFragments . length } ` , 2.5 ) ;
10441072 feedback . play ( 'collect' ) ;
@@ -1051,6 +1079,11 @@ <h2 id="journal-title">SIGNAL LOG</h2>
10511079}
10521080
10531081function resetQuest ( ) {
1082+ const storedProgress = LittleAndroidProgress . loadProgress ( progressStorage ) ;
1083+ if ( storedProgress . challengeDate && storedProgress . challengeDate !== dailyChallenge . dateKey ) {
1084+ showStatus ( 'NEW DAILY CHALLENGE ACTIVE IN ANOTHER TAB' , 3 ) ;
1085+ return false ;
1086+ }
10541087 const occupiedSignalSite = signalFragments . some ( fragment => (
10551088 entityOccupiesTile ( player , fragment . x , fragment . y ) ||
10561089 isTileReserved ( npcs , fragment . x , fragment . y , null )
@@ -1069,12 +1102,13 @@ <h2 id="journal-title">SIGNAL LOG</h2>
10691102 discoveredNPCs . clear ( ) ;
10701103 quest . complete = false ;
10711104 document . body . classList . remove ( 'world-restored' ) ;
1072- const storedGeneration = LittleAndroidProgress . loadProgress ( progressStorage ) . generation ;
1105+ const storedGeneration = storedProgress . generation ;
10731106 progressGeneration = Math . max ( progressGeneration , storedGeneration ) + 1 ;
10741107 LittleAndroidProgress . saveProgress ( progressStorage , currentProgress ( ) ) ;
10751108 updateQuestUI ( ) ;
10761109 updateJournal ( ) ;
10771110 showStatus ( 'SIGNAL SEARCH RESTARTED' , 2 ) ;
1111+ metrics . increment ( 'replays' ) ;
10781112 return true ;
10791113}
10801114
@@ -1123,6 +1157,30 @@ <h2 id="journal-title">SIGNAL LOG</h2>
11231157 if ( ! muted ) feedback . play ( 'interact' ) ;
11241158} ) ;
11251159
1160+ async function shareCompletion ( ) {
1161+ const text = LittleAndroidEngagement . completionText ( dailyChallenge . code ) ;
1162+ metrics . increment ( 'share_intent' ) ;
1163+ if ( window . navigator && typeof window . navigator . share === 'function' ) {
1164+ try {
1165+ await window . navigator . share ( { title : 'Little Android' , text, url : 'https://littleandroid.com' } ) ;
1166+ showStatus ( 'COMPLETION SHARED' , 2 ) ;
1167+ return ;
1168+ } catch {
1169+ showStatus ( 'SHARE CANCELLED' , 2 ) ;
1170+ return ;
1171+ }
1172+ }
1173+ if ( window . navigator && window . navigator . clipboard ) {
1174+ try {
1175+ await window . navigator . clipboard . writeText ( text ) ;
1176+ showStatus ( 'RESULT COPIED' , 2 ) ;
1177+ return ;
1178+ } catch { }
1179+ }
1180+ showStatus ( text , 4 ) ;
1181+ }
1182+ shareButton . addEventListener ( 'click' , shareCompletion ) ;
1183+
11261184let statusTimer = 0 ;
11271185function showStatus ( message , duration = 1.5 ) {
11281186 statusEl . textContent = message ;
@@ -1168,6 +1226,7 @@ <h2 id="journal-title">SIGNAL LOG</h2>
11681226 player . idleTimer = 0 ;
11691227 showStatus ( 'SCANNING...' ) ;
11701228 feedback . play ( 'scan' ) ;
1229+ metrics . markOnce ( 'first_scan' ) ;
11711230 return true ;
11721231}
11731232
0 commit comments