<?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: Olu Desire</title>
    <description>The latest articles on DEV Community by Olu Desire (@desire_george_434_ai).</description>
    <link>https://dev.to/desire_george_434_ai</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%2F4078676%2F191816c3-343d-4efc-b471-dafb46725e5b.png</url>
      <title>DEV Community: Olu Desire</title>
      <link>https://dev.to/desire_george_434_ai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/desire_george_434_ai"/>
    <language>en</language>
    <item>
      <title>🕹️ The Reinvention Tax: Why We Ditched Pure Scratch for Epic.js (and You Should Too)</title>
      <dc:creator>Olu Desire</dc:creator>
      <pubDate>Tue, 25 Aug 2026 00:31:39 +0000</pubDate>
      <link>https://dev.to/desire_george_434_ai/the-arcade-shooter-paradox-building-from-scratch-vs-leveraging-epicjs-1opf</link>
      <guid>https://dev.to/desire_george_434_ai/the-arcade-shooter-paradox-building-from-scratch-vs-leveraging-epicjs-1opf</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyeggrf0ho3obylwkagaz.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyeggrf0ho3obylwkagaz.jpg" alt="Image showing the difference between building from scratch and leveraging important assets" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;How we tried to build a whole game engine out of stubbornness, and what the browser had to say about that.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  📖 Introduction: The "We Can Build It" Syndrome
&lt;/h2&gt;

&lt;p&gt;Every developer starts a project with a dangerous amount of confidence. The day we decided to build a custom HTML5 canvas arcade shooter, we walked into the room like champions — "no shaking, we go handle am." No pre-built modules, no shortcut libraries, no dependencies. Just us, a blank browser tab, and a keyboard doing overtime past midnight.&lt;/p&gt;

&lt;p&gt;We genuinely thought hand-coding the physics, the enemy spawn logic, and the bullet arrays would be a calm weekend job. Instead, the CPU started shouting the moment two enemies appeared on screen at once. Frame rate dropped to single digits like it was on strike. Our code turned into spaghetti — and not the good kind you enjoy on a Sunday. It showed us pepper.&lt;/p&gt;

&lt;p&gt;We were busy reinventing the wheel from zero, completely blind to the fact that a solid core engine — &lt;code&gt;epic.js&lt;/code&gt; — was sitting right there, fully able to carry the heavy part of the matter for us.&lt;/p&gt;

&lt;p&gt;This isn't just a "we failed, then we won" story, though. We're going to show you exactly &lt;em&gt;why&lt;/em&gt; the naive version fell apart, line by line, and exactly what replaced it — so you can skip the three days we spent finding this out the hard way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The one-line summary:&lt;/strong&gt; &lt;em&gt;We tried to build an arcade shooter entirely by hand, our browser humbled us in public, and switching to a pooled, delta-time-driven architecture on &lt;code&gt;epic.js&lt;/code&gt; is the thing that actually saved the project.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🪤 Sector 1: Falling Into the "Pure Scratch" Trap
&lt;/h2&gt;

&lt;p&gt;Every developer passes through that phase where using an existing framework feels like cheating. We want total control. We want to write every variable ourselves so we can beat our chest later and say "I built this from the ground, nobody helped me."&lt;/p&gt;

&lt;p&gt;Our early object-tracking system was pure chaos. Instead of a clean entity manager, bullets went straight into a raw, unmanaged array that looped endlessly on the main thread:&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;// Our painful, hand-coded bullet loop&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateBullets&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&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;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;playerBullets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;playerBullets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// manual coordinate tracking&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;playerBullets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;playerBullets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&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;i&lt;/span&gt;&lt;span class="o"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This loop worked fine when there was one lonely bullet on screen, moving like a snail with nowhere to be. But scale that up to a real arcade wave — lasers, particle sparks, three enemy formations descending at once — and the browser will very quickly show you pepper.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧯 Sector 2: Why Our Hand-Coded Architecture Packed Up (The Actual Technical Reason)
&lt;/h2&gt;

&lt;p&gt;It didn't take long for the DIY masterpiece to scatter completely. On a web canvas, performance is not a bonus feature — it's the entire difference between a smooth arcade game and a slideshow with extra steps.&lt;/p&gt;

&lt;p&gt;Here's the part most devlogs skip: &lt;strong&gt;why&lt;/strong&gt; exactly that loop was the problem, not just that it was slow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reason 1 — &lt;code&gt;splice()&lt;/code&gt; inside a loop is quietly expensive.&lt;/strong&gt; Every time &lt;code&gt;splice(i, 1)&lt;/code&gt; removes a bullet, JavaScript has to shift &lt;em&gt;every element after it&lt;/em&gt; one position to the left to close the gap. That's an O(n) operation, happening potentially dozens of times per frame, inside a loop that's already O(n). With 5 bullets on screen, you'd never notice. With 200 bullets, particles, and enemies all doing this every frame, you're paying that shifting cost constantly — this alone was a big chunk of our frame drop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reason 2 — we were creating and destroying objects every frame.&lt;/strong&gt; Every new bullet was &lt;code&gt;{ x, y, ... }&lt;/code&gt; freshly allocated with &lt;code&gt;push()&lt;/code&gt;, then thrown away with &lt;code&gt;splice()&lt;/code&gt; a second later. JavaScript's garbage collector has to clean up after all that churn, and garbage collection pauses are exactly the kind of thing that shows up as a stutter, not a steady slowdown — which is why our frame rate wasn't just low, it was &lt;em&gt;inconsistent&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reason 3 — collision checks were brute-force.&lt;/strong&gt; Every bullet was checked against every enemy, every frame — an O(n × m) comparison with no shortcuts. Ten bullets and ten enemies is 100 checks. Fine. Fifty bullets and thirty enemies is 1,500 checks, every single frame, on the main thread, alongside rendering.&lt;/p&gt;

&lt;p&gt;Stacked together, here's roughly what that did to us on a mid-range laptop:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Active bullets/particles&lt;/th&gt;
&lt;th&gt;Frame rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One bullet on screen&lt;/td&gt;
&lt;td&gt;~1&lt;/td&gt;
&lt;td&gt;60 FPS, smooth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A real wave (lasers + enemies + sparks)&lt;/td&gt;
&lt;td&gt;~180&lt;/td&gt;
&lt;td&gt;6–9 FPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same wave, boss fight&lt;/td&gt;
&lt;td&gt;~350&lt;/td&gt;
&lt;td&gt;Effectively frozen&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Our custom approach failed for three structural reasons on top of the raw math:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;th&gt;What it actually looked like&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU overload&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Every bullet trajectory, boundary check, and collision box was computed manually on the main thread — the browser choked under the weight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tightly coupled code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Game logic, rendering, and input listeners were tangled like cheap earphone wires — touch one variable, three unrelated features scatter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The "reinventing the wheel" tax&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Three full days spent hand-writing collision logic that solid engine frameworks had already solved years ago&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We were so busy trying to prove a point that we forgot the first rule of smart engineering: don't carry heavy load when a trailer is passing free of charge. Ego writes the first draft. Engineering rewrites it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🚦 When to walk away from pure-scratch:&lt;/strong&gt; if your frame rate dips below 30 FPS with fewer than 100 objects on screen, stop optimizing your hand-rolled version and start integrating an engine. That's the reinvention tax ceiling — past that point, you're paying in days what a framework already solved in years.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To be fair to our stubborn selves, though — building it wrong first wasn't pure waste. If your goal is &lt;em&gt;learning&lt;/em&gt; how canvas internals actually work, keep going by hand, that struggle is the whole point. Just don't ship the hand-rolled version to production. Know which one you're doing before you start.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔧 Sector 3: The Actual Fix — Object Pooling and Delta Time
&lt;/h2&gt;

&lt;p&gt;Before we get to &lt;code&gt;epic.js&lt;/code&gt; itself, we want to show the two changes that mattered most — because you can apply both of these in &lt;em&gt;any&lt;/em&gt; canvas game, engine or no engine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 1 — Object pooling (stop creating and destroying bullets)
&lt;/h3&gt;

&lt;p&gt;Instead of constantly allocating new bullet objects and throwing them away, we pre-allocate a fixed pool once, and just recycle inactive ones:&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;// A simple bullet pool — allocate once, reuse forever&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BULLET_POOL_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&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;bulletPool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BULLET_POOL_SIZE&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="na"&gt;x&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="na"&gt;y&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="na"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;spawnBullet&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;y&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;bullet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bulletPool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;active&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;bullet&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="c1"&gt;// pool exhausted, skip spawn this frame&lt;/span&gt;
  &lt;span class="nx"&gt;bullet&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;x&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;bullet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;bullet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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;updateBullets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deltaTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &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;bullet&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;bulletPool&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;bullet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;active&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;bullet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;deltaTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// speed in px/sec, not px/frame&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;bullet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;bullet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;active&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="c1"&gt;// recycle, don't destroy&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;No &lt;code&gt;splice()&lt;/code&gt;. No &lt;code&gt;push()&lt;/code&gt;. No garbage collector chasing after us. We just flip a boolean. This one change took our bullet-heavy scenes from stuttering to steady on the same laptop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 2 — Delta time instead of fixed pixel steps
&lt;/h3&gt;

&lt;p&gt;Our original loop moved bullets by a flat &lt;code&gt;10&lt;/code&gt; pixels every frame — which means speed was secretly tied to frame rate. Drop to 30 FPS, and every bullet moves at &lt;em&gt;half&lt;/em&gt; the speed it should, which makes a slow machine feel even more broken than it is.&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;let&lt;/span&gt; &lt;span class="nx"&gt;lastTime&lt;/span&gt; &lt;span class="o"&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;gameLoop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentTime&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;deltaTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lastTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// seconds since last frame&lt;/span&gt;
  &lt;span class="nx"&gt;lastTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;updateBullets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deltaTime&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gameLoop&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gameLoop&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With delta time, &lt;code&gt;updateBullets&lt;/code&gt; always knows how much &lt;em&gt;real&lt;/em&gt; time passed, so movement stays consistent whether the game is running at 60 FPS on a good laptop or 25 FPS on a tired one. Speed becomes &lt;code&gt;pixels per second&lt;/code&gt;, not &lt;code&gt;pixels per frame&lt;/code&gt; — a small change with a big effect on how "broken" a slow device feels.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where epic.js comes in (and where it honestly doesn't)
&lt;/h3&gt;

&lt;p&gt;Let's be precise here, because we don't want to overclaim what the engine hands you for free. Once our engine version was routed correctly:&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="nx"&gt;versionDropdown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;selectedVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;versionDropdown&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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;selectedVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v4&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="nx"&gt;engineScriptFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://limn-engine-doc.vercel.app/asset/epic.js&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...here's what &lt;code&gt;epic.js&lt;/code&gt; actually gave us, straight from the source:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A component system with built-in collision, so we stopped hand-writing AABB math.&lt;/strong&gt; Every game object is a &lt;code&gt;Component&lt;/code&gt; — you give it a width, height, position, and it comes with &lt;code&gt;.crashWith(otherObject)&lt;/code&gt; for rectangle collision (with rotation support baked in) and &lt;code&gt;.enableCircleCollision()&lt;/code&gt; / &lt;code&gt;.crashWithCircle()&lt;/code&gt; if you'd rather check circles:&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;bullet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yellow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;playerX&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;bullet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speedY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;display&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;bullet&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// later, in your own update loop:&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;bullet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;crashWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;enemy&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bullet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// removes it from the engine's render list for you&lt;/span&gt;
  &lt;span class="nx"&gt;enemy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&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;We didn't have to write &lt;code&gt;crashWith()&lt;/code&gt; ourselves — that was three of our "reinvented the wheel" days, gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automatic viewport culling, which is the real reason the frame rate recovered.&lt;/strong&gt; Here's the part we didn't expect: the engine checks each component's Rect-like &lt;code&gt;x&lt;/code&gt; property — which holds &lt;code&gt;x&lt;/code&gt;, &lt;code&gt;y&lt;/code&gt;, &lt;code&gt;width&lt;/code&gt;, and &lt;code&gt;height&lt;/code&gt; — against the game area bounds before doing any work on it:&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="nx"&gt;TCJSgameGameArea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;crashWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;component&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;component&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="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&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;Off-screen bullets, particles, and enemies still exist in memory, but they stop costing us render and update time the moment they leave the visible area. That single check quietly did more for our frame rate than anything we'd hand-rolled ourselves — it wasn't pooling that saved us, it was the engine simply refusing to do work on things nobody could see.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does &lt;em&gt;not&lt;/em&gt; give you for free: object pooling.&lt;/strong&gt; This is the honest part. Objects go into a global list via &lt;code&gt;display.add()&lt;/code&gt; and come back out via &lt;code&gt;.destroy()&lt;/code&gt;, which does a &lt;code&gt;findIndex&lt;/code&gt; + &lt;code&gt;splice()&lt;/code&gt; under the hood — the exact same pattern we were trying to escape in Sector 1. Even the engine's own particle system creates and destroys particles this way. If you want true pooling — pre-allocating and recycling — that's still on you to build on top of &lt;code&gt;Component&lt;/code&gt;, the same way we showed in the manual fix above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed is still per-frame, not per-second, by default.&lt;/strong&gt; &lt;code&gt;move()&lt;/code&gt; applies &lt;code&gt;this.x += this.speedX&lt;/code&gt; directly — no delta-time scaling built in. &lt;code&gt;display.deltaTime&lt;/code&gt; is exposed for you to use in your own &lt;code&gt;update()&lt;/code&gt; function, but the engine won't apply it for you automatically. So the delta-time discipline from the fix above still matters, even once you're on &lt;code&gt;epic.js&lt;/code&gt; — the engine gives you the tools, not the guarantee.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Being honest about it:&lt;/strong&gt; this wasn't instant magic. Wiring &lt;code&gt;epic.js&lt;/code&gt; into a codebase that had spent three days being hand-rolled and tightly coupled meant untangling our own mess first — pulling rendering logic out of the input listeners it had no business living inside. The framework didn't fix our architecture. It gave us collision handling and free culling. Pooling and delta time discipline were still ours to get right.&lt;/p&gt;

&lt;h3&gt;
  
  
  So when do you actually switch?
&lt;/h3&gt;

&lt;p&gt;We were three days in when we switched. That's not a universal number — here's a rougher rule of thumb than "just use an engine":&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your situation&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Object count under ~50, frame rate is fine&lt;/td&gt;
&lt;td&gt;Stay pure. You don't have a performance problem yet.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object count over ~50, frame rate holds but you're hand-writing AABB/circle collision&lt;/td&gt;
&lt;td&gt;Grab a collision-only library or a &lt;code&gt;crashWith()&lt;/code&gt;-style utility — you don't need a full engine yet.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object count over ~50 &lt;em&gt;and&lt;/em&gt; frame rate is dropping &lt;em&gt;and&lt;/em&gt; you're hand-writing collision&lt;/td&gt;
&lt;td&gt;Grab the engine. You've hit both problems at once — that's the actual signal, not a fixed day count.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You're doing this specifically to learn canvas internals&lt;/td&gt;
&lt;td&gt;Keep going by hand, on purpose — just don't ship that version.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The difference, in numbers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Active bullets/particles&lt;/th&gt;
&lt;th&gt;Frame rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Original — &lt;code&gt;splice()&lt;/code&gt;, no delta time, checking everything on-screen or not&lt;/td&gt;
&lt;td&gt;~180&lt;/td&gt;
&lt;td&gt;6–9 FPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual fix — pooling + delta time, still checking everything&lt;/td&gt;
&lt;td&gt;~180&lt;/td&gt;
&lt;td&gt;48–55 FPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On &lt;code&gt;epic.js&lt;/code&gt; — built-in &lt;code&gt;crashWith()&lt;/code&gt; + automatic viewport culling, our own pooling on top&lt;/td&gt;
&lt;td&gt;~350&lt;/td&gt;
&lt;td&gt;Steady 60 FPS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same laptop, same scene, three different architectures. The lesson wasn't "epic.js is magic" — it's that pooling and delta time recovered most of the performance ourselves, and the engine's free viewport culling plus not having to hand-write collision math is what let us push the object count even higher on top of that.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎓 Sector 4: Hard-Earned Lessons From the Trenches
&lt;/h2&gt;

&lt;p&gt;Some lessons don't come from documentation. They come from watching your frame rate crawl at 1 AM and asking yourself hard questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Under-the-hood mechanics still matter.&lt;/strong&gt; Writing those messy loops by hand taught us why canvas rendering contexts and coordinate grids behave the way they do. You genuinely have to break something manually at least once to understand how it ticks — the pain wasn't wasted, even if the code was.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frameworks aren't cheating.&lt;/strong&gt; Leaning on a solid core asset like &lt;code&gt;epic.js&lt;/code&gt; isn't cutting corners — it's smart architecture. It frees up your mental bandwidth for the part that actually moves the needle: game design and how it feels to play.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Knowing when to pivot is a skill, not a defeat.&lt;/strong&gt; Admitting your hand-coded system has become the bottleneck isn't failure — it's a checkpoint in engineering maturity. The stubborn version of us needed to hear that first.&lt;/p&gt;

&lt;h2&gt;
  
  
  📊 What You've Learned
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;How It Applies&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;splice()&lt;/code&gt; cost&lt;/td&gt;
&lt;td&gt;Removing from the middle of an array shifts every element after it — expensive when it happens every frame&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object pooling&lt;/td&gt;
&lt;td&gt;Recycle objects instead of creating/destroying them — cuts garbage collection stutter dramatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delta time&lt;/td&gt;
&lt;td&gt;Move objects by &lt;code&gt;speed × secondsElapsed&lt;/code&gt;, not a flat number per frame, so speed stays consistent across devices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Viewport culling&lt;/td&gt;
&lt;td&gt;Skipping update/render work on objects outside the visible area can matter more than clever collision math&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read your dependency's source&lt;/td&gt;
&lt;td&gt;We assumed &lt;code&gt;epic.js&lt;/code&gt; did pooling for us — it doesn't. Checking the actual code saved us from shipping on a wrong assumption&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The reinvention tax&lt;/td&gt;
&lt;td&gt;Solving a problem a mature framework already solved costs days you don't get back&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  🚀 Conclusion &amp;amp; What's Next
&lt;/h2&gt;

&lt;p&gt;Building an arcade shooter from scratch taught us humility, patience, and the real value of proper tool selection. Dropping the ego, ending the "pure scratch" marathon, and integrating &lt;code&gt;epic.js&lt;/code&gt; is what saved this project from a very avoidable disaster.&lt;/p&gt;

&lt;p&gt;Here's the receipts — the editor running the real V4 build, and the actual arcade wave rendering steady instead of crawling:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp0jxvcnl6oogoebyt188.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp0jxvcnl6oogoebyt188.png" alt="Image showing the arcade game I made with limn engine" width="800" height="443"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fradneik15rfpge8fdfi7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fradneik15rfpge8fdfi7.png" alt="Image showing the arcade shooting game I made with limn engine" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're currently hand-coding every single piece of your own engine, take a breath. Step back, look at your tools honestly, and let clean architecture carry some of the weight for you. And if you take nothing else from this, at least go add object pooling and delta time to whatever you're building right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔗 Come Say Hi
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🌐 &lt;strong&gt;Docs &amp;amp; assets:&lt;/strong&gt; &lt;a href="https://limn-engine-doc.vercel.app" rel="noopener noreferrer"&gt;Limn Engine Vercel Assets&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;Community &amp;amp; support:&lt;/strong&gt; &lt;a href="https://discord.gg/" rel="noopener noreferrer"&gt;Join our Discord&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📂 &lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/terracodes004/limn-engine-doc" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Your turn:&lt;/strong&gt; what's the thing you rebuilt from scratch before finally admitting a library already did it better? Drop it below. 🎮&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Limn Engine&lt;/strong&gt; — &lt;em&gt;draw your game into existence. No shaking, no scattering, just clean architecture doing the heavy lifting for you.&lt;/em&gt; 🎨✨&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>🛠️ How We Upgraded the Limn Engine Playground Editor for Version 4</title>
      <dc:creator>Olu Desire</dc:creator>
      <pubDate>Mon, 24 Aug 2026 23:09:40 +0000</pubDate>
      <link>https://dev.to/desire_george_434_ai/how-we-upgraded-our-limn-engine-playground-editor-to-support-version-4-3g86</link>
      <guid>https://dev.to/desire_george_434_ai/how-we-upgraded-our-limn-engine-playground-editor-to-support-version-4-3g86</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftc6czqid0sbg0nwtlydk.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftc6czqid0sbg0nwtlydk.jpg" alt="Limn Engine Playground Editor v4 interface showing version dropdown" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One dropdown, one bug, and the afternoon we almost shipped it broken.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  📖 Introduction
&lt;/h2&gt;

&lt;p&gt;The Version 4 launch of Limn Engine had everything — a new particle system, audio management, a slick preview. What it didn't have was a way for the playground to actually find the damn file.&lt;/p&gt;

&lt;p&gt;We'd already built &lt;code&gt;epic.js&lt;/code&gt; — the new core script with particle systems and audio management baked in. That part felt great. That part felt like progress, like something to post about. But none of it mattered if the playground editor couldn't even find it. A developer could select "Version 4" from the menu, hit run, cross their fingers... and the preview would just keep loading the old engine. No error. No warning. Just the wrong file, running like nothing happened. Very smooth criminal behaviour from our own code.&lt;/p&gt;

&lt;p&gt;That's the kind of bug that scares us more than any crash — the one where nothing &lt;em&gt;looks&lt;/em&gt; broken, so you almost sign off on it and go rest.&lt;/p&gt;

&lt;p&gt;This is the story of how we fixed it, what we got wrong the first time, and what's still genuinely unfinished about it — because pretending otherwise would defeat the point of writing this at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The one-line summary:&lt;/strong&gt; &lt;em&gt;We upgraded the Limn Engine playground editor to detect and load Version 4 — and learned the hard way that "it works for us" and "it works" are not the same thing.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🔍 The Problem Nobody Would Notice Until It Was Too Late
&lt;/h2&gt;

&lt;p&gt;The playground editor doesn't ask any server which engine version you want — no config file, no manifest, no backend deciding for you. It simply looks at the page, finds a &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; element, reads whatever text is sitting inside it, and tries to match that text to a script. That's the whole idea. Fast, simple, zero dependencies.&lt;/p&gt;

&lt;p&gt;But that convenience comes with a price. If the scanner doesn't recognize a version string, it doesn't shout — it doesn't crash. It just quietly keeps whatever it last loaded, which is exactly how we almost shipped V4 broken. We selected it, hit run, saw the canvas render... and only after staring longer than we'd like to admit did we realize it was the &lt;em&gt;old&lt;/em&gt; engine playing dress-up as the new one. Nobody would've known unless they checked very carefully.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this taught us:&lt;/strong&gt; the bugs that don't announce themselves are the ones that reach production. We started treating a suspiciously calm preview as a warning, not a relief.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧵 Teaching the Scanner to See V4
&lt;/h2&gt;

&lt;p&gt;Here's the loop that walks the dropdowns on the page — the actual fix, straight from the &lt;code&gt;runn()&lt;/code&gt; function that powers the "Run" button:&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;let&lt;/span&gt; &lt;span class="nx"&gt;engineScriptFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;epic.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;allDropdowns&lt;/span&gt; &lt;span class="o"&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;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;select&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;versionDropdown&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="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;select&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;allDropdowns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Updated search check to recognize V2, V3, and V4&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;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;V2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;V3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;V4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;V&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="nx"&gt;versionDropdown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;break&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;Before we touched it, that condition stopped politely at &lt;code&gt;V3&lt;/code&gt;, like V4 never existed. Every dropdown labeled "Version 4" was invisible to the scanner — not broken, not flagged, just completely skipped.&lt;/p&gt;

&lt;p&gt;Adding &lt;code&gt;V4&lt;/code&gt; to that condition was one line. Finding out it was missing took an entire evening, because our first assumption was that the &lt;em&gt;routing&lt;/em&gt; was wrong — not that the dropdown was never being found in the first place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Future-proofing note we haven't shipped yet:&lt;/strong&gt; hardcoding each version string like this means every new release needs another &lt;code&gt;||&lt;/code&gt; clause. A cleaner version — worth doing before V5 — is a simple array:&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;VERSIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;V2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;V3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;V4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;V5&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;VERSIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;V&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="nx"&gt;versionDropdown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;select&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;Still on the todo list. Mentioning it here so future-us can't pretend we didn't know.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 Routing the Version to the Right File
&lt;/h2&gt;

&lt;p&gt;Once the scanner can actually &lt;em&gt;see&lt;/em&gt; the dropdown, something decides which script file to load:&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="nx"&gt;versionDropdown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;selectedVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;versionDropdown&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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;selectedVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v2&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="nx"&gt;engineScriptFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tcjsgame-v2.js&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="k"&gt;else&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;selectedVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v3&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="nx"&gt;engineScriptFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tcjsgame-v3.js&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="k"&gt;else&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;selectedVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v4&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="nx"&gt;engineScriptFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;epic.js&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;engineScriptFile&lt;/code&gt; starts life as &lt;code&gt;"epic.js"&lt;/code&gt; before this block even runs — so if no dropdown is found at all, or nothing matches, the playground defaults to V4 anyway rather than falling back to an older version. That's not fallback logic. That's a variable we forgot to reset, quietly doing the right thing by pure accident — the coding equivalent of surviving a disaster because you happened to fall asleep in the safest corner of the room.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this taught us:&lt;/strong&gt; sometimes your "fallback behavior" isn't something you designed — it's whatever your default variable happens to be. Worth checking that it's the &lt;em&gt;safe&lt;/em&gt; default, not just &lt;em&gt;a&lt;/em&gt; default, before you accidentally get credit for engineering discipline you didn't actually apply.&lt;/p&gt;

&lt;h2&gt;
  
  
  🖼️ The Part the Article Almost Skipped: What Actually Happens on "Run"
&lt;/h2&gt;

&lt;p&gt;Once the right script file is picked, here's what the rest of &lt;code&gt;runn()&lt;/code&gt; actually does — and it's more interesting than just "load a script tag":&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It checks if you're not even using the engine at all.&lt;/strong&gt; Before any of the version-routing logic runs, the function checks whether your editor code looks like raw HTML:&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;let&lt;/span&gt; &lt;span class="nx"&gt;hasHtmlTags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;a-z&lt;/span&gt;&lt;span class="se"&gt;][\s\S]&lt;/span&gt;&lt;span class="sr"&gt;*&amp;gt;/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userEditorCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;userEditorCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;!DOCTYPE&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;hasHtmlTags&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;iframe&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;iframe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;srcdoc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userEditorCode&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you paste a full HTML document into the editor, the playground doesn't fight you about it. It just shrugs, decides "ah, you don craft your own thing," and runs your raw HTML straight through — like a landlord who stops asking questions the moment rent shows up on time. No engine, no version dropdown, no opinions. We hadn't thought to mention this branch in earlier drafts, but it's real, and a reader who hits it without knowing it's there will absolutely think something is broken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Console output gets piped from the sandboxed iframe back to the editor UI.&lt;/strong&gt; The generated preview document overrides &lt;code&gt;console.log&lt;/code&gt; so that anything the user's game code logs shows up in a console panel in the parent editor, not just buried in the iframe's own devtools:&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;_customLog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;_customLog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&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;joinedArgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arg&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;arg&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;arg&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&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;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parent&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="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;consoleDisplay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parent&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;editor-console-logs&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;consoleDisplay&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;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parent&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;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;joinedArgs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nx"&gt;consoleDisplay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nx"&gt;consoleDisplay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollTop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;consoleDisplay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollHeight&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's genuinely a nice touch for a browser playground. Your &lt;code&gt;console.log&lt;/code&gt; doesn't stay trapped in iframe jail — it climbs the fence, walks over to the parent window, and reports what it saw like a very well-behaved snitch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runtime errors get caught and reported automatically.&lt;/strong&gt; If the user's code throws, the playground catches it and posts the error message to a webhook so we can see what's breaking for real users without waiting for a bug report:&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;userEditorCode&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Engine Runtime Error: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// error message dispatched to an internal alerts channel&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're intentionally not showing the actual webhook wiring here — it's a plain fetch call with a URL baked in, and an earlier draft of this article almost pasted that URL straight into the code block. We caught it before publishing, but only just. Publishing your own alert webhook URL in a public tutorial is the developer equivalent of posting a photo of your house key on Twitter with the caption "come design my kitchen." So: fixing that properly (move it behind a real backend endpoint instead of calling a webhook straight from client-side code) is next on the list, and until it's done, the URL stays out of anything we publish. Worth remembering if you're building something similar: a "just post to Discord" error reporter is fine for a personal project, but the moment the code that contains the URL is public — like, say, in a blog post — the URL is public too.&lt;/p&gt;

&lt;h2&gt;
  
  
  ✅ The One We Actually Went Back and Fixed
&lt;/h2&gt;

&lt;p&gt;Talk is cheap, so before publishing this version, we went and closed the loudest gap: the missing timeout. Here's what actually shipped.&lt;/p&gt;

&lt;p&gt;First, the script tag inside the generated iframe gets an &lt;code&gt;id&lt;/code&gt; and real &lt;code&gt;onload&lt;/code&gt;/&lt;code&gt;onerror&lt;/code&gt; handlers, plus a timeout in case the browser never fires either event:&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;engineScript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./${engineScriptFile}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ENGINE_LOAD_TIMEOUT_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5000&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;engineTimeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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="nf"&gt;reportEngineLoadFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Engine timed out after 5s — check your connection or the script URL.&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="nx"&gt;ENGINE_LOAD_TIMEOUT_MS&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;engineScriptTag&lt;/span&gt; &lt;span class="o"&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;engineScript&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;engineScriptTag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onload&lt;/span&gt; &lt;span class="o"&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="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;engineTimeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nx"&gt;engineScriptTag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&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="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;engineTimeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;reportEngineLoadFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Engine script failed to load — 404, CDN issue, or bad URL.&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reportEngineLoadFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parent&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="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;banner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parent&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;engine-error-banner&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;banner&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;banner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;⚠️ &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="nx"&gt;banner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;block&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="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, &lt;code&gt;runn()&lt;/code&gt; itself resets the banner at the start of every run, so a stale error from three runs ago doesn't sit there haunting someone who already fixed the problem:&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;let&lt;/span&gt; &lt;span class="nx"&gt;errorBanner&lt;/span&gt; &lt;span class="o"&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;engine-error-banner&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;errorBanner&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;errorBanner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;errorBanner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same trick as the console piping from earlier — the sandboxed iframe reaches out to &lt;code&gt;window.parent.document&lt;/code&gt; to update a banner that lives in the actual editor UI, not buried where nobody will see it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is this everything?&lt;/strong&gt; No — the dropdown-renaming gap and the "which engine actually loaded" confirmation are both still open, and we're not going to pretend otherwise twice in one post. But this is the difference between a paragraph promising a fix and a code block that is the fix. One down.&lt;/p&gt;

&lt;h2&gt;
  
  
  🩹 What's Still Actually Missing
&lt;/h2&gt;

&lt;p&gt;Here's the part we got wrong last time we wrote about this: we described hardening we hadn't actually built yet — a load timeout, an error banner, a fallback config object. Only the first two of those existed only as a paragraph back then. We fixed the timeout and banner for real, above. The rest still doesn't exist, and we're not going to pretend it does.&lt;/p&gt;

&lt;p&gt;What the current &lt;code&gt;runn()&lt;/code&gt; function still does &lt;em&gt;not&lt;/em&gt; handle, as far as we can tell from the code itself:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gap&lt;/th&gt;
&lt;th&gt;Current reality&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dropdown label gets renamed without updating the matching logic&lt;/td&gt;
&lt;td&gt;Still pure string matching, no fallback config object. If someone changes "Version 4" to something the scanner doesn't recognize, we're back to square one.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confirming which engine actually loaded&lt;/td&gt;
&lt;td&gt;The console piping shows &lt;em&gt;game&lt;/em&gt; output, not engine version. There's no &lt;code&gt;console.log&lt;/code&gt; confirming "epic.js (v4) loaded" anywhere in the real code — the new banner only fires on &lt;em&gt;failure&lt;/em&gt;, not on success.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One honest caveat: we're describing the script-loading path specifically, not every corner of the editor — there's still some iframe styling elsewhere in &lt;code&gt;runn()&lt;/code&gt; we haven't traced all the way through. If something down that path already touches error handling in a way we missed, consider this table mostly right instead of fully right. We'd rather flag the uncertainty than state it with false confidence.&lt;/p&gt;

&lt;p&gt;Two down from three. All of them are still worth naming, which is a more honest place to leave this post than claiming they're all done.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔁 What If V4 Just... Doesn't Work?
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;epic.js&lt;/code&gt; fails to load or throws early, there's currently no automatic fallback to V3 — the developer has to manually reselect it from the dropdown. Combined with the missing timeout above, a broken V4 load can currently look identical to "the playground is just loading forever," with no clear signal to the user about what's actually wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  🗣️ Honesty About the Trade-offs
&lt;/h2&gt;

&lt;p&gt;String-matching a dropdown label is not how a "serious" IDE would do this. Bigger tools use config manifests, package registries, versioned APIs — systems that don't fall apart just because someone renamed a label without checking the routing logic.&lt;/p&gt;

&lt;p&gt;We know. We built it this way anyway, because Limn Engine is still small enough that a full config system would be more machinery than the actual problem deserves — for now. But we won't pretend the trade-off isn't there. If this editor grows past what it is today, string-matching is the first thing on the list to retire — right alongside the webhook that needs to move server-side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this taught us:&lt;/strong&gt; "good enough for now" is a real engineering decision, not laziness in disguise — as long as you say it out loud instead of hiding it in the commit message. And "we fixed it" should mean the code changed, not just that we wrote a paragraph saying it did.&lt;/p&gt;

&lt;h2&gt;
  
  
  📊 What You've Learned
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;How It Applies&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Silent failure&lt;/td&gt;
&lt;td&gt;A missing condition doesn't crash — it quietly does the wrong thing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default values as accidental fallbacks&lt;/td&gt;
&lt;td&gt;Check whether your variable's default is actually the &lt;em&gt;safe&lt;/em&gt; choice, not just &lt;em&gt;a&lt;/em&gt; choice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets in client-side code&lt;/td&gt;
&lt;td&gt;Anything a browser can fetch, a reader can copy — webhook URLs and API keys don't belong in code that ships to the client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write down what's still broken&lt;/td&gt;
&lt;td&gt;A devlog that only shows the fix and skips the gaps isn't more impressive, it's just less useful&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  🚀 Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The routing fix that shipped was one line of code. Writing honestly about everything &lt;em&gt;around&lt;/em&gt; that line — the parts that still don't work — took a lot longer, and mattered more.&lt;/p&gt;

&lt;p&gt;If you're building anything that reads the DOM to make a decision — a dropdown, a data attribute, a class name — assume it will fail quietly before it ever fails loudly. And if you're going to write about your own fix, resist describing the version of it you meant to build instead of the one that's actually running.&lt;/p&gt;

&lt;p&gt;Draw your game into existence — and make sure your dropdown knows which version to use. We're still working on the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔗 Come Say Hi
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🌐 &lt;strong&gt;Try Limn Engine live (Version 4):&lt;/strong&gt; &lt;a href="https://limn-engine-doc.vercel.app" rel="noopener noreferrer"&gt;limn-engine-doc.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;Source &amp;amp; issues:&lt;/strong&gt; &lt;a href="https://github.com/terracodes004/limn-engine-doc" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;Join the community:&lt;/strong&gt; &lt;a href="https://discord.gg/" rel="noopener noreferrer"&gt;Discord&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Your turn:&lt;/strong&gt; what's your most embarrassing version-mismatch bug? Drop it in the comments — we promise ours isn't the worst one out there. 🎮&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
