22 MISSION · client interactivity. Progressive, reduced-motion-safe.
33 ================================================================= */
44
5- import { mascotCompanion } from '../data/site' ;
6- import { createMascotGame } from './game' ;
5+ export { } ;
76
87const reduceMotion = window . matchMedia ( '(prefers-reduced-motion: reduce)' ) . matches ;
98const hashSettleDelays = [ 80 , 350 , 1000 , 1800 , 2800 ] as const ;
@@ -53,6 +52,8 @@ type MissionSoundName =
5352 | 'firework'
5453 | 'restart' ;
5554
55+ type MascotGame = { launch : ( ) => void } ;
56+
5657/* ---------- Scroll reveal ---------- */
5758function initReveal ( ) {
5859 const items = document . querySelectorAll < HTMLElement > ( '[data-reveal]' ) ;
@@ -478,7 +479,6 @@ function initAnalyticsScrollTracking() {
478479 }
479480 } ;
480481
481- requestUpdate ( ) ;
482482 window . addEventListener ( 'scroll' , requestUpdate , { passive : true } ) ;
483483 window . addEventListener ( 'resize' , requestUpdate , { passive : true } ) ;
484484 analyticsScrollCleanup = ( ) => {
@@ -526,16 +526,18 @@ function initMagnetic() {
526526 if ( reduceMotion ) return ;
527527 if ( ! window . matchMedia ( '(hover: hover) and (pointer: fine)' ) . matches ) return ;
528528 document . querySelectorAll < HTMLElement > ( '.hero__cta .btn, .final-cta__cta .btn' ) . forEach ( ( el ) => {
529- let rect = el . getBoundingClientRect ( ) ;
529+ let rect : DOMRect | null = null ;
530530 el . addEventListener ( 'pointerenter' , ( ) => {
531531 rect = el . getBoundingClientRect ( ) ;
532532 } ) ;
533533 el . addEventListener ( 'mousemove' , ( e ) => {
534+ rect ??= el . getBoundingClientRect ( ) ;
534535 const x = e . clientX - ( rect . left + rect . width / 2 ) ;
535536 const y = e . clientY - ( rect . top + rect . height / 2 ) ;
536537 el . style . transform = `translate(${ x * 0.25 } px, ${ y * 0.32 } px)` ;
537538 } ) ;
538539 el . addEventListener ( 'mouseleave' , ( ) => {
540+ rect = null ;
539541 el . style . transform = '' ;
540542 } ) ;
541543 } ) ;
@@ -972,6 +974,18 @@ function initMascot() {
972974 // the sprite is decorative (pointer-events:none) — make it clickable
973975 mascot . style . pointerEvents = 'auto' ;
974976 mascot . style . cursor = 'pointer' ;
977+ const firstQuip = mascot . dataset . firstQuip ?? '' ;
978+ const ctaQuip = mascot . dataset . ctaQuip ?? '' ;
979+ const idleQuips = ( ( ) => {
980+ try {
981+ const parsed = JSON . parse ( mascot . dataset . idleQuips ?? '[]' ) ;
982+ return Array . isArray ( parsed )
983+ ? parsed . filter ( ( item ) : item is string => typeof item === 'string' )
984+ : [ ] ;
985+ } catch {
986+ return [ ] ;
987+ }
988+ } ) ( ) ;
975989
976990 const canRoam =
977991 ! reduceMotion &&
@@ -981,7 +995,7 @@ function initMascot() {
981995 ! navigator . webdriver ||
982996 window . __missionMascotRoamingTest === true ||
983997 window . __missionMascotQuipTest === true ;
984- const autoQuips = [ mascotCompanion . firstQuip , ... mascotCompanion . idleQuips ] . filter ( Boolean ) ;
998+ const autoQuips = [ firstQuip , ...idleQuips ] . filter ( Boolean ) ;
985999 if ( navigator . webdriver ) window . __missionMascotQuips = autoQuips ;
9861000 const speechHoldMs = window . __missionMascotQuipTest === true ? 1000 : 5600 ;
9871001 const firstQuipDelayMs = window . __missionMascotQuipTest === true ? 120 : 900 ;
@@ -1056,12 +1070,29 @@ function initMascot() {
10561070 window . setTimeout ( ( ) => mascot . classList . remove ( 'is-hop' ) , 500 ) ;
10571071 }
10581072 } ;
1059- // The game DOM is built lazily on first launch() — never on load — so it can
1060- // never surface in screenshots/tests unless a real user clicks the mascot.
1061- const game = createMascotGame ( ) ;
1073+ // The game code and DOM are built lazily on first launch() — never on load.
1074+ let game : MascotGame | null = null ;
1075+ let gameLoad : Promise < MascotGame > | null = null ;
1076+ const getGame = ( ) => {
1077+ if ( game ) return Promise . resolve ( game ) ;
1078+ gameLoad ??= import ( './game' )
1079+ . then ( ( { createMascotGame } ) => {
1080+ game = createMascotGame ( ) ;
1081+ return game ;
1082+ } )
1083+ . catch ( ( error ) => {
1084+ gameLoad = null ;
1085+ throw error ;
1086+ } ) ;
1087+ return gameLoad ;
1088+ } ;
10621089 const onMascotClick = ( ) => {
10631090 triggerHop ( ) ;
1064- game . launch ( ) ;
1091+ void getGame ( )
1092+ . then ( ( loadedGame ) => loadedGame . launch ( ) )
1093+ . catch ( ( ) => {
1094+ /* keep the page usable if the optional game chunk fails */
1095+ } ) ;
10651096 } ;
10661097 mascot . addEventListener ( 'click' , onMascotClick ) ;
10671098
@@ -1095,7 +1126,8 @@ function initMascot() {
10951126 syncSurface ( ) ;
10961127 } ) ;
10971128 } ;
1098- syncSurface ( ) ;
1129+ mascot . dataset . overInvert = 'false' ;
1130+ queueSurfaceSync ( ) ;
10991131 window . addEventListener ( 'scroll' , queueSurfaceSync , { passive : true } ) ;
11001132 window . addEventListener ( 'resize' , queueSurfaceSync , { passive : true } ) ;
11011133
@@ -1147,7 +1179,7 @@ function initMascot() {
11471179 } ;
11481180 const onCtaEnter = ( ) => {
11491181 triggerHop ( ) ;
1150- if ( Math . random ( ) < 0.5 ) speak ( mascotCompanion . ctaQuip ) ;
1182+ if ( Math . random ( ) < 0.5 ) speak ( ctaQuip ) ;
11511183 } ;
11521184
11531185 window . addEventListener ( 'pointermove' , onPointerMove , { passive : true } ) ;
0 commit comments