1818# title-bar .dot {width : 8px ;height : 8px ;background : # D4A030 ;border-radius : 50% ;animation : blink 1.6s ease-in-out infinite}
1919@keyframes blink{0% , 100%{opacity : 1 }50% {opacity : 0.3 }}
2020# hint {position : absolute;bottom : 24px ;left : 50% ;transform : translateX (-50% );font-family : 'Silkscreen' , monospace;font-size : 11px ;color : # D4A030 ;opacity : 0.6 ;pointer-events : none;z-index : 10 ;text-shadow : 1px 1px 0px # 6B4820 ;animation : fadeHint 4s ease-in-out infinite}
21+ # hint .status-hidden {visibility : hidden}
2122@keyframes fadeHint{0% , 100%{opacity : 0.6 }50% {opacity : 0.2 }}
2223# controls {position : absolute;bottom : 20px ;left : 20px ;pointer-events : none;z-index : 10 ;font-family : 'Silkscreen' , monospace;font-size : 9px ;color : # 8A9870 ;line-height : 1.8 ;text-shadow : 1px 1px 0px # 1a1510 ;opacity : 0.7 }
2324# controls .key {display : inline-block;background : rgba (90 , 136 , 72 , 0.15 );border : 1px solid rgba (90 , 136 , 72 , 0.3 );border-radius : 3px ;padding : 1px 5px ;font-family : 'Press Start 2P' , monospace;font-size : 7px ;color : # A8C088 ;margin-right : 4px ;vertical-align : middle}
24- # coords {position : absolute;bottom : 24px ;right : 24 px ;font-family : 'Press Start 2P' , monospace;font-size : 8px ;color : # 5A8848 ;pointer-events : none;z-index : 10 }
25+ # coords {position : absolute;bottom : 24px ;right : 76 px ;font-family : 'Press Start 2P' , monospace;font-size : 8px ;color : # 5A8848 ;pointer-events : none;z-index : 10 }
2526# emote {position : absolute;pointer-events : none;z-index : 15 ;font-size : 24px ;transition : transform 0.15s ease-out, opacity 0.15s ;transform : scale (0 );opacity : 0 }
2627# emote .show {transform : scale (1 );opacity : 1 }
2728# action-button {position : absolute;right : max (20px , env (safe-area-inset-right));bottom : max (20px , env (safe-area-inset-bottom));z-index : 20 ;display : none;min-width : 72px ;height : 52px ;padding : 0 12px ;border : 2px solid # D4A030 ;border-radius : 4px ;background : rgba (10 , 10 , 10 , 0.88 );color : # E8C040 ;font : 700 12px 'Silkscreen' , monospace;cursor : pointer;box-shadow : 0 0 18px rgba (212 , 160 , 48 , 0.2 )}
6465 # sound-button {right : max (104px , calc (104px + env (safe-area-inset-right)));bottom : max (28px , calc (28px + env (safe-area-inset-bottom)))}
6566 # status {bottom : max (84px , calc (64px + env (safe-area-inset-bottom)));font-size : 10px }
6667}
68+ @media (hover : none), (pointer : coarse){
69+ # action-button {display : block}
70+ }
6771@media (prefers-reduced-motion : reduce){
6872 # title-bar .dot , # hint {animation : none}
6973 # emote , # status {transition : none}
@@ -260,9 +264,10 @@ <h2 id="journal-title">SIGNAL LOG</h2>
260264 const tx = player . tileX + d . dx , ty = player . tileY + d . dy ;
261265 player . facing = dir ;
262266
263- // Check NPC on target tile
267+ // Interaction targets reserve their tile so the player stops facing them.
264268 const npcOnTile = isTileOccupiedByNPC ( tx , ty , null ) ;
265- if ( npcOnTile || ! isWalkable ( tx , ty ) ) {
269+ const fragmentOnTile = getUncollectedFragmentAt ( tx , ty ) ;
270+ if ( npcOnTile || fragmentOnTile || ! isWalkable ( tx , ty ) ) {
266271 // BLOCKED
267272 player . state = 'BLOCKED' ;
268273 player . bumpDir = dir ;
@@ -858,7 +863,8 @@ <h2 id="journal-title">SIGNAL LOG</h2>
858863// PATHFINDING
859864// ══════════════════════════════════════════════════════
860865function findPath ( sx , sy , ex , ey ) {
861- return findWorldPath ( sx , sy , ex , ey , isWalkable , { allowNearest : false } ) ;
866+ const isPlayerWalkable = ( x , y ) => isWalkable ( x , y ) && ! getUncollectedFragmentAt ( x , y ) ;
867+ return findWorldPath ( sx , sy , ex , ey , isPlayerWalkable , { allowNearest : false } ) ;
862868}
863869
864870// ══════════════════════════════════════════════════════
@@ -879,6 +885,12 @@ <h2 id="journal-title">SIGNAL LOG</h2>
879885
880886 // Check if clicking adjacent NPC while idle
881887 if ( player . state === 'IDLE' ) {
888+ const fragment = getUncollectedFragmentAt ( tileX , tileY ) ;
889+ if ( fragment && Math . abs ( tileX - player . tileX ) + Math . abs ( tileY - player . tileY ) === 1 ) {
890+ player . facing = dirToward ( player . tileX , player . tileY , tileX , tileY ) ;
891+ collectFragment ( fragment ) ;
892+ return ;
893+ }
882894 const adj = isTileOccupiedByNPC ( tileX , tileY , null ) ;
883895 if ( adj && Math . abs ( tileX - player . tileX ) + Math . abs ( tileY - player . tileY ) === 1 ) {
884896 // Trigger interaction
@@ -909,7 +921,13 @@ <h2 id="journal-title">SIGNAL LOG</h2>
909921
910922// ── WASD Keyboard Movement ──
911923const keysDown = { } ;
924+ function shouldHandleGameKey ( target ) {
925+ return ! target || typeof target . closest !== 'function' ||
926+ ! target . closest ( 'button, dialog, input, select, textarea, a[href]' ) ;
927+ }
928+
912929window . addEventListener ( 'keydown' , ( e ) => {
930+ if ( ! shouldHandleGameKey ( e . target ) ) return ;
913931 const k = e . key . toLowerCase ( ) ;
914932 if ( [ 'w' , 'a' , 's' , 'd' , 'arrowup' , 'arrowdown' , 'arrowleft' , 'arrowright' , ' ' , 'enter' ] . includes ( k ) ) e . preventDefault ( ) ;
915933 keysDown [ k ] = true ;
@@ -948,6 +966,12 @@ <h2 id="journal-title">SIGNAL LOG</h2>
948966 ) ) || null ;
949967}
950968
969+ function getUncollectedFragmentAt ( x , y ) {
970+ return signalFragments . find ( fragment => (
971+ fragment . discovered && ! fragment . collected && fragment . x === x && fragment . y === y
972+ ) ) || null ;
973+ }
974+
951975function updateQuestUI ( ) {
952976 const collected = signalFragments . filter ( fragment => fragment . collected ) . length ;
953977 questEl . textContent = quest . complete ? `FACTORY ONLINE · ${ dailyChallenge . code } ` : `SIGNALS ${ collected } /${ signalFragments . length } · ${ dailyChallenge . code } ` ;
@@ -1085,6 +1109,7 @@ <h2 id="journal-title">SIGNAL LOG</h2>
10851109function showStatus ( message , duration = 1.5 ) {
10861110 statusEl . textContent = message ;
10871111 statusEl . classList . add ( 'show' ) ;
1112+ hintEl . classList . add ( 'status-hidden' ) ;
10881113 statusTimer = duration ;
10891114}
10901115
@@ -1115,8 +1140,7 @@ <h2 id="journal-title">SIGNAL LOG</h2>
11151140 }
11161141 scanPulse = {
11171142 x : player . tileX , y : player . tileY ,
1118- time : Date . now ( ) ,
1119- radius : 0 ,
1143+ elapsed : 0 ,
11201144 maxRadius : 6 * TS , // 6 tiles radius
11211145 duration : 800 , // ms
11221146 } ;
@@ -1130,6 +1154,31 @@ <h2 id="journal-title">SIGNAL LOG</h2>
11301154 return true ;
11311155}
11321156
1157+ function updateScanPulse ( dt ) {
1158+ if ( ! scanPulse ) return ;
1159+ const previousRadius = ( scanPulse . elapsed / scanPulse . duration ) * scanPulse . maxRadius ;
1160+ scanPulse . elapsed = Math . min ( scanPulse . duration , scanPulse . elapsed + dt * 1000 ) ;
1161+ const radius = ( scanPulse . elapsed / scanPulse . duration ) * scanPulse . maxRadius ;
1162+
1163+ for ( const fragment of signalFragments ) {
1164+ if ( fragment . discovered || fragment . collected ) continue ;
1165+ const distance = Math . hypot (
1166+ ( fragment . x - scanPulse . x ) * TS ,
1167+ ( fragment . y - scanPulse . y ) * TS ,
1168+ ) ;
1169+ if ( distance >= previousRadius && distance <= radius ) {
1170+ fragment . discovered = true ;
1171+ persistProgress ( ) ;
1172+ updateJournal ( ) ;
1173+ showStatus ( `SIGNAL FOUND: ${ fragment . name } ` , 2.5 ) ;
1174+ feedback . play ( 'discover' ) ;
1175+ feedback . haptic ( [ 15 , 20 , 15 ] ) ;
1176+ }
1177+ }
1178+
1179+ if ( scanPulse . elapsed >= scanPulse . duration ) scanPulse = null ;
1180+ }
1181+
11331182// ══════════════════════════════════════════════════════
11341183// GAME LOOP
11351184// ══════════════════════════════════════════════════════
@@ -1138,9 +1187,13 @@ <h2 id="journal-title">SIGNAL LOG</h2>
11381187function update ( dt ) {
11391188 // Scan cooldown
11401189 if ( scanCooldownTimer > 0 ) scanCooldownTimer -= dt ;
1190+ updateScanPulse ( dt ) ;
11411191 if ( statusTimer > 0 ) {
11421192 statusTimer -= dt ;
1143- if ( statusTimer <= 0 ) statusEl . classList . remove ( 'show' ) ;
1193+ if ( statusTimer <= 0 ) {
1194+ statusEl . classList . remove ( 'show' ) ;
1195+ hintEl . classList . remove ( 'status-hidden' ) ;
1196+ }
11441197 }
11451198
11461199 // ── Player State Machine ──
@@ -1349,10 +1402,7 @@ <h2 id="journal-title">SIGNAL LOG</h2>
13491402
13501403 // Scan pulse effect
13511404 if ( scanPulse ) {
1352- const age = ( Date . now ( ) - scanPulse . time ) / scanPulse . duration ;
1353- if ( age >= 1 ) {
1354- scanPulse = null ;
1355- } else {
1405+ const age = scanPulse . elapsed / scanPulse . duration ;
13561406 const cx = scanPulse . x * TS - camX + TS / 2 ;
13571407 const cy = scanPulse . y * TS - camY + TS / 2 ;
13581408 const radius = age * scanPulse . maxRadius ;
@@ -1393,21 +1443,6 @@ <h2 id="journal-title">SIGNAL LOG</h2>
13931443 rememberNPC ( npc ) ;
13941444 }
13951445 }
1396- for ( const fragment of signalFragments ) {
1397- if ( fragment . discovered || fragment . collected ) continue ;
1398- const fragmentX = fragment . x * TS - camX + TS / 2 ;
1399- const fragmentY = fragment . y * TS - camY + TS / 2 ;
1400- const distance = Math . hypot ( fragmentX - cx , fragmentY - cy ) ;
1401- if ( distance < radius && distance > radius - TS * 2 ) {
1402- fragment . discovered = true ;
1403- persistProgress ( ) ;
1404- updateJournal ( ) ;
1405- showStatus ( `SIGNAL FOUND: ${ fragment . name } ` , 2.5 ) ;
1406- feedback . play ( 'discover' ) ;
1407- feedback . haptic ( [ 15 , 20 , 15 ] ) ;
1408- }
1409- }
1410- }
14111446 }
14121447
14131448 // Discovered signal fragments
0 commit comments