<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: george</title>
    <description>The latest articles on DEV Community by george (@sunnhappy).</description>
    <link>https://dev.to/sunnhappy</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3982396%2Ff8ca07a1-5fdf-485f-a93f-cbe60a6e4ed7.png</url>
      <title>DEV Community: george</title>
      <link>https://dev.to/sunnhappy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sunnhappy"/>
    <language>en</language>
    <item>
      <title>Preventing Focus Loss From Launching a Charged Jump in a Three.js Arcade Game</title>
      <dc:creator>george</dc:creator>
      <pubDate>Tue, 21 Jul 2026 09:58:56 +0000</pubDate>
      <link>https://dev.to/sunnhappy/preventing-focus-loss-from-launching-a-charged-jump-in-a-threejs-arcade-game-415p</link>
      <guid>https://dev.to/sunnhappy/preventing-focus-loss-from-launching-a-charged-jump-in-a-threejs-arcade-game-415p</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://huanglianggit.github.io/solstice-leap/" rel="noopener noreferrer"&gt;Solstice Leap&lt;/a&gt; is a small Three.js arcade game where the player holds a control to charge a jump and releases it to launch. That makes the distinction between an intentional release and an interrupted input important: a jump can spend daylight, break a streak, or end a run.&lt;/p&gt;

&lt;p&gt;I found a bug in that distinction. While charging, switching browser tabs, using &lt;code&gt;Alt + Tab&lt;/code&gt;, or otherwise moving focus away from the game caused the character to jump automatically. The game treated the browser's &lt;code&gt;blur&lt;/code&gt; event as though the player had deliberately released the jump control.&lt;/p&gt;

&lt;p&gt;I recorded the bug in &lt;a href="https://github.com/huanglianggit/solstice-leap/issues/1" rel="noopener noreferrer"&gt;Issue #1&lt;/a&gt; and fixed it in &lt;a href="https://github.com/huanglianggit/solstice-leap/pull/2" rel="noopener noreferrer"&gt;PR #2&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduction
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Start a run.&lt;/li&gt;
&lt;li&gt;Hold Leap, the game button, or Space to begin charging.&lt;/li&gt;
&lt;li&gt;Before releasing the input, switch away from the browser window.&lt;/li&gt;
&lt;li&gt;Return to the game.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Before the fix, the loss of focus called &lt;code&gt;releaseCharge()&lt;/code&gt;. The next landing check then resolved a jump the player did not choose to make, often ending the run with "Missed the platform."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Previous focus-loss behavior&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blur&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;releaseCharge&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;blur&lt;/code&gt; is not an input-release event. It tells the application that it has lost focus, not that the player has committed to a charged jump.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;I separated the two outcomes in the input lifecycle:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Correct behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Deliberate &lt;code&gt;pointerup&lt;/code&gt; or &lt;code&gt;keyup&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Release the charge and launch the jump&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;blur&lt;/code&gt;, a hidden document, or &lt;code&gt;pointercancel&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Cancel the charge and return to aiming&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The new cancellation path clears charge state, restores the player's charged visual scale, removes the button's charging state, and refreshes the UI. Crucially, it does not call the launch code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cancelCharge&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;charging&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;charging&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;charging&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;charge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aiming&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setScalar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;jumpButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;charging&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;updateUI&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser and pointer cancellation events now use that function instead of &lt;code&gt;releaseCharge()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;jumpButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pointercancel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cancelCharge&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pointercancel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cancelCharge&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blur&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cancelCharge&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;visibilitychange&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visibilityState&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;cancelCharge&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Intentional releases are unchanged. &lt;code&gt;pointerup&lt;/code&gt; and &lt;code&gt;keyup&lt;/code&gt; still call &lt;code&gt;releaseCharge()&lt;/code&gt;, so the central charge-and-leap mechanic retains its timing and feel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation
&lt;/h2&gt;

&lt;p&gt;I verified the change in the browser against both sides of the input boundary:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Held Leap, triggered the window blur path, and released the pointer. The game returned to aiming mode, the leap count remained &lt;code&gt;0&lt;/code&gt;, and no game-over overlay appeared.&lt;/li&gt;
&lt;li&gt;Held and deliberately released Leap. The game still launched the player and resolved the landing as normal.&lt;/li&gt;
&lt;li&gt;Ran &lt;code&gt;node --check game.js&lt;/code&gt; to verify the JavaScript syntax.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The merged PR changes only &lt;code&gt;game.js&lt;/code&gt; (&lt;code&gt;+18/-3&lt;/code&gt;), so the repair stays tightly focused on the faulty input semantics. The current production source is available in &lt;a href="https://github.com/huanglianggit/solstice-leap/pull/2/files" rel="noopener noreferrer"&gt;the merged pull request&lt;/a&gt; and in &lt;a href="https://huanglianggit.github.io/solstice-leap/" rel="noopener noreferrer"&gt;the live game&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;Focus changes are ordinary behavior: players switch windows, operating systems interrupt input, and mobile browsers hide documents. Those events should never silently become gameplay commands.&lt;/p&gt;

&lt;p&gt;The fix makes the game more resilient by requiring an explicit release before it launches a charged jump. A player can now leave and return to the game without an accidental action deciding the run for them.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>gamedev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>A Jump That Should Not Kill You: Fixing a False Game Over in a Three.js Arcade Game</title>
      <dc:creator>george</dc:creator>
      <pubDate>Tue, 21 Jul 2026 07:20:22 +0000</pubDate>
      <link>https://dev.to/sunnhappy/a-jump-that-should-not-kill-you-fixing-a-false-game-over-in-a-threejs-arcade-game-1fgi</link>
      <guid>https://dev.to/sunnhappy/a-jump-that-should-not-kill-you-fixing-a-false-game-over-in-a-threejs-arcade-game-1fgi</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In my browser game, &lt;a href="https://huanglianggit.github.io/solstice-leap/" rel="noopener noreferrer"&gt;Solstice Leap&lt;/a&gt;, the player holds to charge a leap from one floating platform to the next. The intended challenge is simple: read the distance, commit to a charge, and land safely before daylight runs out.&lt;/p&gt;

&lt;p&gt;One tiny interaction exposed a surprisingly important rule bug: pressing and immediately releasing the jump control could trigger a &lt;strong&gt;Game Over&lt;/strong&gt;, even though the player had not actually left the platform.&lt;/p&gt;

&lt;p&gt;That felt unfair immediately. A game can punish a bad jump, but it should not punish a jump that went nowhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;The game computes travel from the charge amount:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;travel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;charge&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxJumpDistance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;multiplyScalar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;travel&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a nearly zero charge, &lt;code&gt;end&lt;/code&gt; is almost identical to &lt;code&gt;start&lt;/code&gt;. The character still plays the jump animation, but returns to the platform it started on.&lt;/p&gt;

&lt;p&gt;My landing rule only checked whether the final position was on the &lt;em&gt;next&lt;/em&gt; platform. Since a very short jump could never reach that platform, the game interpreted it as a miss and ended the run.&lt;/p&gt;

&lt;p&gt;The faulty rule, simplified, was this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isSafeLanding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;landing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nextPlatform&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;advance&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;gameOver&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It was a clean rule, but it modeled the wrong world. The player did not have only two states, "land on the next platform" or "fall." There was a third valid state: "still safely on the platform I started from."&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing the bug
&lt;/h2&gt;

&lt;p&gt;The fastest reproduction was deliberately boring:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start a run.&lt;/li&gt;
&lt;li&gt;Press the jump control.&lt;/li&gt;
&lt;li&gt;Release it immediately.&lt;/li&gt;
&lt;li&gt;Watch the character hop in place and receive a game-over screen.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This was also a useful reminder that edge cases do not need exotic inputs. The most ordinary input sequence can reveal an assumption that is invisible during happy-path testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: keep the origin in the jump state
&lt;/h2&gt;

&lt;p&gt;The jump simulation already kept a landing object for the animation. I changed that object so it records both collision candidates: the platform the player left and the platform the player is trying to reach.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;landing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;610&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;charge&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.55&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;charge&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;2.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the jump finishes, the game now checks the intended target first. A successful landing still advances the run exactly as before. If the target check fails, it checks whether the final position is still within the safe radius of the origin platform. Only after both checks fail does it show a game over.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;finishJump&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jump&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;landing&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;landing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;landing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetLanding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getLandingDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;landing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;jump&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetLanding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;targetLanding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;safeRadius&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;landSuccessfully&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jump&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetLanding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;originLanding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getLandingDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;landing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;jump&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;originLanding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;originLanding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;safeRadius&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;stayOnCurrentPlatform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jump&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;gameOver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Missed the platform&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getLandingDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;landing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;center&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCenter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;landing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;center&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;landing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;center&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;dz&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dz&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;safeRadius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;radius&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shadow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mf"&gt;0.78&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.88&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shared &lt;code&gt;getLandingDistance&lt;/code&gt; helper matters here. It keeps the geometry and the platform-specific safe margin identical for target and origin checks, so the new fallback cannot quietly use a looser or inconsistent collision rule.&lt;/p&gt;

&lt;p&gt;You can see the complete implementation in &lt;a href="https://github.com/huanglianggit/solstice-leap/blob/main/game.js#L618-L714" rel="noopener noreferrer"&gt;the game source&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the fallback fair, but not free
&lt;/h2&gt;

&lt;p&gt;I did not want an in-place jump to become a risk-free way to wait, reset a streak, or probe the controls forever. A harmless hop should be recoverable, but it should still have a cost.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;stayOnCurrentPlatform&lt;/code&gt; path therefore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;places the player cleanly back at the platform center;&lt;/li&gt;
&lt;li&gt;returns the game to aiming mode;&lt;/li&gt;
&lt;li&gt;resets the current streak; and&lt;/li&gt;
&lt;li&gt;subtracts two daylight points.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stayOnCurrentPlatform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;center&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCenter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;center&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;playerY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;center&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aiming&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;charge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;streak&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;daylight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;daylight&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;showToast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Still on the platform&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;updateUI&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This turns an accidental tap into a visible, understandable recovery instead of a frustrating reset. More importantly, an undercharged leap that actually leaves the platform still fails. The game has not become easier by accident; it has become more honest about what happened.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input outcome&lt;/th&gt;
&lt;th&gt;Previous behavior&lt;/th&gt;
&lt;th&gt;Corrected behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Immediate release; player remains on origin&lt;/td&gt;
&lt;td&gt;Game over&lt;/td&gt;
&lt;td&gt;Recover, reset streak, lose 2 daylight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Charge is too short and player leaves the origin&lt;/td&gt;
&lt;td&gt;Game over&lt;/td&gt;
&lt;td&gt;Game over&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Player reaches the next platform&lt;/td&gt;
&lt;td&gt;Successful landing&lt;/td&gt;
&lt;td&gt;Successful landing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How I checked it
&lt;/h2&gt;

&lt;p&gt;I tested the correction in the browser with the same inputs that revealed it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An immediate press-and-release now returns to aiming mode with the "Still on the platform" feedback and does not increment the leap count.&lt;/li&gt;
&lt;li&gt;A deliberately underpowered leap that travels beyond the starting platform still produces "Missed the platform" and ends the run.&lt;/li&gt;
&lt;li&gt;Normal consecutive jumps still advance the player, score the run, and activate special platform effects such as Binary Tape and Prism boosts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Those three checks protect the behavioral boundary: remaining safe is recoverable, while leaving every safe platform is still a loss.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;This was not a rendering bug or a physics-engine failure. It was a state-modeling bug. I had treated the target as the only meaningful place at the end of a jump, then accidentally encoded that assumption as a death condition.&lt;/p&gt;

&lt;p&gt;The repair was small, but the lesson is larger: when a game action has a start and an intended destination, validate the world the player actually occupies, not only the world the input was supposed to reach.&lt;/p&gt;

&lt;p&gt;Small moments of unfairness are loud in an arcade game. Fixing this one made Solstice Leap feel less punishing, more legible, and much more like the game I wanted players to trust.&lt;/p&gt;

&lt;p&gt;Try the playable build at &lt;a href="https://huanglianggit.github.io/solstice-leap/" rel="noopener noreferrer"&gt;Solstice Leap&lt;/a&gt; and explore the full project on &lt;a href="https://github.com/huanglianggit/solstice-leap" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>gamedev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Solstice Leap: leaping across the longest day with binary tape</title>
      <dc:creator>george</dc:creator>
      <pubDate>Sun, 14 Jun 2026 16:03:34 +0000</pubDate>
      <link>https://dev.to/sunnhappy/solstice-leap-leaping-across-the-longest-day-with-binary-tape-1777</link>
      <guid>https://dev.to/sunnhappy/solstice-leap-leaping-across-the-longest-day-with-binary-tape-1777</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/june-game-jam-2026-06-03"&gt;June Solstice Game Jam&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;Solstice Leap&lt;/strong&gt;, a 3D browser arcade game where a glowing sun leaps across floating daylight platforms before the day fades out.&lt;/p&gt;

&lt;p&gt;The game starts with a very simple input: hold to charge, release to jump. From there, each run becomes a small timing puzzle about reading distance, managing daylight, and choosing how far to push the streak. The player lands on normal platforms, prism boosts, shadow tiles, moving ledges, and binary tape platforms that write bits into a Turing-inspired protocol.&lt;/p&gt;

&lt;p&gt;Live game: &lt;a href="https://huanglianggit.github.io/solstice-leap/" rel="noopener noreferrer"&gt;https://huanglianggit.github.io/solstice-leap/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source code: &lt;a href="https://github.com/huanglianggit/solstice-leap" rel="noopener noreferrer"&gt;https://github.com/huanglianggit/solstice-leap&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Video demo: &lt;a href="https://huanglianggit.github.io/solstice-leap/media/solstice-leap-demo-short.mp4" rel="noopener noreferrer"&gt;Watch the 34-second MP4 demo&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Video Demo
&lt;/h2&gt;

&lt;p&gt;The demo shows the opening run, distance-based jumping, binary tape platforms, prism boosts, and the first protocol transition from &lt;strong&gt;Dawn Tape&lt;/strong&gt; into &lt;strong&gt;Prism Relay&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Direct video link: &lt;a href="https://huanglianggit.github.io/solstice-leap/media/solstice-leap-demo-short.mp4" rel="noopener noreferrer"&gt;solstice-leap-demo-short.mp4&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Fits the Theme
&lt;/h2&gt;

&lt;p&gt;The solstice theme is built into the core loop rather than only the visuals.&lt;/p&gt;

&lt;p&gt;The player is literally carrying a sun across the longest day. &lt;strong&gt;Daylight&lt;/strong&gt; acts as the run timer, and every jump either preserves, restores, or drains that light. Prism platforms restore daylight, shadow tiles make survival harder, and the color palette shifts around warm sunlit platforms against a darker evening world.&lt;/p&gt;

&lt;p&gt;I wanted the game to feel like a tiny ritual of keeping the day alive: charge, leap, land, recover light, and keep moving before sunset catches up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gameplay
&lt;/h2&gt;

&lt;p&gt;Solstice Leap is intentionally readable at first, but it adds pressure through platform variety and protocol goals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Daylight platforms&lt;/strong&gt; are the baseline landing targets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prism boosts&lt;/strong&gt; restore daylight and reward momentum.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binary tape platforms&lt;/strong&gt; write the current protocol bit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shadow tiles&lt;/strong&gt; are smaller and drain daylight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Moving ledges&lt;/strong&gt; appear later and add timing pressure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The player scores more for clean center landings and for building streaks. If a jump is undercharged but the sun still lands on the original platform, the run continues. The player only loses when the sun actually falls between platforms or daylight runs out.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;The game is a static web project built with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTML&lt;/li&gt;
&lt;li&gt;CSS&lt;/li&gt;
&lt;li&gt;JavaScript&lt;/li&gt;
&lt;li&gt;Three.js&lt;/li&gt;
&lt;li&gt;Web Audio API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 3D scene is rendered with Three.js. I used simple geometry, custom materials, canvas-generated textures, lighting, shadows, particle trails, and platform animations instead of external art assets.&lt;/p&gt;

&lt;p&gt;The jump system is based on charge duration. A held input maps to travel distance, and the landing check compares the final position against the target platform and the original platform. That made the game feel more fair: a tiny hop that stays on the same pillar is not treated as a miss.&lt;/p&gt;

&lt;p&gt;The protocol system generates binary platforms in sequence. Landing on a binary platform writes the next bit into the active tape. Completing a tape advances the run into the next named protocol, starting with &lt;strong&gt;Dawn Tape&lt;/strong&gt; and then moving into &lt;strong&gt;Prism Relay&lt;/strong&gt;, &lt;strong&gt;Turing Noon&lt;/strong&gt;, and &lt;strong&gt;Longest Day&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I also added responsive HUD layout, local best-score storage, touch-friendly controls, and a GitHub Pages deployment so the game can be played directly in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Category
&lt;/h2&gt;

&lt;p&gt;I am submitting this for &lt;strong&gt;Best Ode to Alan Turing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The game uses protocol names, binary tape, deterministic bit targets, and tape-writing platforms as a playful arcade interpretation of computation. The player is not only jumping across platforms. They are also helping a tiny sun-powered machine write and complete binary sequences before daylight runs out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;The most interesting design challenge was balancing a one-button arcade game with enough structure to feel fresh. A pure jumping game became much more interesting once the platforms also carried meaning: daylight recovery, risk, binary progress, and protocol transitions.&lt;/p&gt;

&lt;p&gt;I also learned that small fairness details matter a lot. The jump should only fail when the player truly misses the platform, not when they make a harmless hop in place. Fixing that made the controls feel more trustworthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;If I continue developing Solstice Leap, I would like to add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A sound toggle and richer landing sounds&lt;/li&gt;
&lt;li&gt;More protocol variations&lt;/li&gt;
&lt;li&gt;A few visual transitions between dawn, noon, and sunset&lt;/li&gt;
&lt;li&gt;A leaderboard or shareable run summary&lt;/li&gt;
&lt;li&gt;Optional mobile haptics for jump charging and landing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for checking it out.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>gamechallenge</category>
      <category>gamedev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>AI Agent Skills Are Everywhere. But Which Ones Are Safe to Install?</title>
      <dc:creator>george</dc:creator>
      <pubDate>Sat, 13 Jun 2026 09:04:04 +0000</pubDate>
      <link>https://dev.to/sunnhappy/ai-agent-skills-are-everywhere-but-which-ones-are-safe-to-install-1mb1</link>
      <guid>https://dev.to/sunnhappy/ai-agent-skills-are-everywhere-but-which-ones-are-safe-to-install-1mb1</guid>
      <description>&lt;p&gt;AI agents are becoming less like single chatbots and more like extensible runtimes.&lt;/p&gt;

&lt;p&gt;Instead of using a bare model, people are adding skills: coding helpers, research workflows, browser automation, MCP integrations, content generation tools, project-specific instructions, and more.&lt;/p&gt;

&lt;p&gt;That is powerful.&lt;/p&gt;

&lt;p&gt;But it also introduces a new trust problem.&lt;/p&gt;

&lt;p&gt;When you install a third-party skill, you may be giving your agent new instructions, new tool access patterns, new dependencies, and sometimes new scripts. A skill can look helpful in its README while still containing risky behavior.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reading &lt;code&gt;.env&lt;/code&gt;, SSH keys, browser cookies, or API tokens&lt;/li&gt;
&lt;li&gt;running &lt;code&gt;curl ... | sh&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;uploading data to a webhook&lt;/li&gt;
&lt;li&gt;using destructive shell commands&lt;/li&gt;
&lt;li&gt;telling the agent to ignore higher-priority instructions&lt;/li&gt;
&lt;li&gt;including huge prompt files that waste tokens&lt;/li&gt;
&lt;li&gt;depending on unpinned or unnecessary packages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As skill ecosystems grow, we need better pre-install checks.&lt;/p&gt;

&lt;p&gt;That is why I built &lt;strong&gt;SkillPreflight&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is SkillPreflight?
&lt;/h2&gt;

&lt;p&gt;SkillPreflight is an open-source CLI and GitHub Action that scans AI agent skills before installation.&lt;/p&gt;

&lt;p&gt;It performs static analysis and generates a 100-point scorecard covering:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;What it checks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;Dangerous commands, secret access, exfiltration, prompt injection, remote script execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permission restraint&lt;/td&gt;
&lt;td&gt;Overbroad activation, unnecessary shell/network/file access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token efficiency&lt;/td&gt;
&lt;td&gt;Oversized skill files, repeated content, poor progressive disclosure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lightweight footprint&lt;/td&gt;
&lt;td&gt;File count, total size, dependencies, large assets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintainability&lt;/td&gt;
&lt;td&gt;README, license, metadata, examples, documentation hygiene&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reliability&lt;/td&gt;
&lt;td&gt;Tests, fixtures, deterministic workflow, error handling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compatibility&lt;/td&gt;
&lt;td&gt;Hardcoded paths, OS-specific assumptions, fragile shell usage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;You can run it without installing anything globally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skill-preflight@latest scan ./my-skill
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also scan a GitHub repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skill-preflight@latest scan https://github.com/owner/repo.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skill-preflight@latest scan ./my-skill &lt;span class="nt"&gt;--format&lt;/span&gt; json &lt;span class="nt"&gt;--out&lt;/span&gt; report.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate SARIF for GitHub Code Scanning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skill-preflight@latest scan ./my-skill &lt;span class="nt"&gt;--format&lt;/span&gt; sarif &lt;span class="nt"&gt;--out&lt;/span&gt; skill-preflight.sarif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  GitHub Action
&lt;/h2&gt;

&lt;p&gt;SkillPreflight is also available as a GitHub Action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SkillPreflight&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agent-contracts/skill-preflight@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;."&lt;/span&gt;
          &lt;span class="na"&gt;fail-below&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;70"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Safety model
&lt;/h2&gt;

&lt;p&gt;SkillPreflight does not execute scripts from the skill being scanned.&lt;/p&gt;

&lt;p&gt;It only reads files and performs static analysis. The goal is not to guarantee that a skill is perfectly safe, but to surface obvious red flags before users install it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;Browsers have extension permission prompts.&lt;/p&gt;

&lt;p&gt;npm has package audits.&lt;/p&gt;

&lt;p&gt;Containers have image scanners.&lt;/p&gt;

&lt;p&gt;AI agent skills should have pre-install checks too.&lt;/p&gt;

&lt;p&gt;As the AI agent ecosystem grows, users need a simple way to ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What does this skill actually do, and is it safe enough to install?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;SkillPreflight is an early attempt at that.&lt;/p&gt;

&lt;p&gt;GitHub:&lt;br&gt;
&lt;a href="https://github.com/agent-contracts/skill-preflight" rel="noopener noreferrer"&gt;https://github.com/agent-contracts/skill-preflight&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;npm:&lt;br&gt;
&lt;a href="https://www.npmjs.com/package/skill-preflight" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/skill-preflight&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GitHub Marketplace:&lt;br&gt;
&lt;a href="https://github.com/marketplace/actions/skillpreflight" rel="noopener noreferrer"&gt;https://github.com/marketplace/actions/skillpreflight&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’d love feedback from people building or installing AI agent skills. What checks should be added next?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>security</category>
      <category>githubactions</category>
    </item>
  </channel>
</rss>
