<?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>We Built an Arcade for Our Own Game Engine</title>
      <dc:creator>Olu Desire</dc:creator>
      <pubDate>Tue, 15 Sep 2026 08:06:44 +0000</pubDate>
      <link>https://dev.to/desire_george_434_ai/we-built-an-arcade-for-our-own-game-engine-a83</link>
      <guid>https://dev.to/desire_george_434_ai/we-built-an-arcade-for-our-own-game-engine-a83</guid>
      <description>&lt;h3&gt;
  
  
  Or: how I spent three months using the platform we built at Evolved Tech, noticing what was broken, and refusing to stop
&lt;/h3&gt;




&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/kehinde_owolabi_e2e54567a"&gt;Kehinde Owolabi&lt;/a&gt;&lt;/strong&gt; founded &lt;strong&gt;Evolved Tech&lt;/strong&gt; and built the core: &lt;code&gt;epic.js&lt;/code&gt;, our 2D game engine for the browser. Not Unity. Not Godot. Something small and honest — one file, no npm, no build step. Drop it into an HTML page, write some JavaScript, and you have a game running.&lt;/p&gt;

&lt;p&gt;We called the project &lt;strong&gt;Limn Engine&lt;/strong&gt;. Kehinde owns the engine core. It worked.&lt;/p&gt;

&lt;p&gt;My name is &lt;strong&gt;Desire&lt;/strong&gt;. I'm the co-founder of Evolved Tech. My job was everything around the engine — I built the &lt;strong&gt;editor&lt;/strong&gt; where people actually make games with it, and the &lt;strong&gt;arcade&lt;/strong&gt; where those games live once they're made.&lt;/p&gt;

&lt;p&gt;Here's the situation we found ourselves in: the engine existed. Games could be made. But there was nowhere to put them. No gallery. No way to share. No way for anyone to see what you'd built.&lt;/p&gt;

&lt;p&gt;Kehinde built the engine. I built the editor and the roads.&lt;/p&gt;

&lt;p&gt;So I started building the arcade. Then I kept building — not because anyone asked, but because every time I used our own platform, something felt missing.&lt;/p&gt;

&lt;p&gt;This is the story of how our engine became a full arcade with profiles, leaderboards, comments, replies, follows, weekly winners, an inbox that took two days to stop loading forever, and one screenshot that will haunt Kehinde for the rest of his football-watching life.&lt;/p&gt;

&lt;p&gt;If you've ever added one feature and ended up with fifteen, pull up a chair.&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%2F7sc1u0964240nv3uo5xg.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%2F7sc1u0964240nv3uo5xg.png" alt="Image" width="800" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1 — The Arcade
&lt;/h2&gt;

&lt;p&gt;Picture a game gallery. Grid of cards. Each card shows one game someone made.&lt;/p&gt;

&lt;p&gt;That's the arcade. The problem: &lt;strong&gt;how do you show a game without a screenshot?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Games aren't images, they're JavaScript. There's no browser on the server to render one, and recording video on every publish is way too heavy for a platform where games ship in 30 seconds.&lt;/p&gt;

&lt;p&gt;So I did the obvious thing: I draw the thumbnail from the game's own data.&lt;/p&gt;

&lt;p&gt;Every game saves a small config — the positions and colours of its objects. A dress-up game has five coloured rectangles (hat, head, shirt, pants, shoes). A platformer has a player square and a goal. A shooter has a ship and an enemy.&lt;/p&gt;

&lt;p&gt;At load time, the arcade reads that config and draws the shapes into an SVG. No screenshot needed. No server rendering. Just math.&lt;/p&gt;

&lt;p&gt;I expected it to look terrible. It actually looks... fine. A bit abstract. Like a Mondrian painting, if Mondrian was into 2D side-scrollers.&lt;/p&gt;

&lt;p&gt;I'll take it.&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%2Fthk02cmtnxdo9vhoxn4x.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%2Fthk02cmtnxdo9vhoxn4x.png" alt="Image" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2 — The Leaderboard (Which Nearly Broke Me)
&lt;/h2&gt;

&lt;p&gt;Here's how my brain works. Watch closely.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I should rank the games."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Okay, by what? Plays? Too easy to game — refresh five times, you're #1. Score? Every user writes their own game code, so there's no shared scoring format. Likes? Fine. Simple. One like per person per game.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"But likes should reset weekly, or the same game stays #1 forever."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Okay. Monday, 00:00 UTC. Every week. Automatic.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"But how do people know the leaderboard changed?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now there's a &lt;strong&gt;Weekly Leaderboard card&lt;/strong&gt; in the inbox — top 5 games, like counts, comment counts. Like a game on Tuesday, the creator's inbox updates Wednesday.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"But that's kind of empty if nobody likes anything."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then the card just doesn't render — which, in the platform's early days, meant it almost never did. Because I was the only user, and I wasn't going to like my own games.&lt;/p&gt;

&lt;p&gt;...Reader, I liked my own games. I'm not proud of it. The leaderboard showed.&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%2F0t01ubqeszoez45rjuoa.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%2F0t01ubqeszoez45rjuoa.png" alt="Image" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 3 — Comments with Replies (The &lt;code&gt;prompt()&lt;/code&gt; Mistake)
&lt;/h2&gt;

&lt;p&gt;Comments were easy. I built them in an afternoon.&lt;/p&gt;

&lt;p&gt;Replies were &lt;strong&gt;not&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;My first version used a &lt;code&gt;prompt()&lt;/code&gt; dialog — the built-in browser popup. Ugly, unstylable, covers the whole screen on mobile, looks like it's from 1998. I shipped it anyway because it worked.&lt;/p&gt;

&lt;p&gt;Then I tried it on my tablet.&lt;/p&gt;

&lt;p&gt;Tapped "Reply." Grey box appeared. Typed. Hit OK. Box vanished.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing happened.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Turns out the page had already reloaded from an earlier action, and the reply was posting against a stale game ID. The fix took twenty minutes. The rewrite of the UI took four hours.&lt;/p&gt;

&lt;p&gt;The new version: an inline reply form under every comment — small text box, indented, blue left border. Reply appears underneath, slightly smaller, slightly dimmer, clearly nested. Two levels deep, max — root comments and replies. No more, because three levels of nested comments on a phone screen is a war crime.&lt;/p&gt;

&lt;p&gt;Now it looks like a real comment section. Like Reddit, if Reddit had five users and all of them were me.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Screenshot above: that's Kehinde in one of our test comment threads, confidently typing "Arsenal will win the league this season" like a man who has never once watched Arsenal play in May. Sir, we are testing a **reply feature&lt;/em&gt;&lt;em&gt;, not writing your eulogy. I kept the screenshot in the codebase for QA purposes. I kept it in this article for accountability purposes. History will remember this comment. History will also remember what happened next.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Part 4 — The Username Problem
&lt;/h2&gt;

&lt;p&gt;Worth telling carefully, because it's the kind of bug that teaches you something.&lt;/p&gt;

&lt;p&gt;Until recently, every game shipped with two pieces of creator info: &lt;code&gt;author_id&lt;/code&gt; (a Supabase UUID) and &lt;code&gt;author_name&lt;/code&gt; — the creator's &lt;strong&gt;email prefix&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So if your email was &lt;code&gt;desiregeorge434@gmail.com&lt;/code&gt;, every game you published said "by desiregeorge434." Not your name. Not a handle. Your raw email prefix, sitting there, looking at you.&lt;/p&gt;

&lt;p&gt;So I added usernames. The rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3–20 characters&lt;/li&gt;
&lt;li&gt;Lowercase letters, numbers, underscores only&lt;/li&gt;
&lt;li&gt;Unique across the platform&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Locked forever once set&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last rule matters. If usernames could change, every &lt;code&gt;/u/?u=desire&lt;/code&gt; link would break the moment Desire became &lt;code&gt;@desirethegreat&lt;/code&gt;. Games, followers, old shares — all dead links.&lt;/p&gt;

&lt;p&gt;So: one shot. Pick carefully. Live with it.&lt;/p&gt;

&lt;p&gt;There's a second field — &lt;strong&gt;display name&lt;/strong&gt; — which &lt;em&gt;can&lt;/em&gt; change. You can be &lt;code&gt;@desire&lt;/code&gt; forever while your display name bounces between "Desire" and "Desire the Magnificent" on a whim.&lt;/p&gt;

&lt;p&gt;Two names. One permanent, one fluid. I only know this is the correct design because I got it wrong first.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 5 — The Two-Day Bug (Three Lines of Code)
&lt;/h2&gt;

&lt;p&gt;My favourite story from this whole project.&lt;/p&gt;

&lt;p&gt;For a week, every game showed its author's &lt;strong&gt;email prefix&lt;/strong&gt;. Every profile page showed the actual &lt;strong&gt;username&lt;/strong&gt;. The two systems never spoke to each other.&lt;/p&gt;

&lt;p&gt;Games stored &lt;code&gt;author_id&lt;/code&gt; / &lt;code&gt;author_name&lt;/code&gt;. Profiles stored &lt;code&gt;user_id&lt;/code&gt; / &lt;code&gt;username&lt;/code&gt;. The arcade rendered games by reading &lt;code&gt;author_name&lt;/code&gt; directly — it had no idea a profile system even existed.&lt;/p&gt;

&lt;p&gt;I stared at this for two days. Rewrote the arcade query. Blamed Supabase. Blamed my browser cache. Blamed the phase of the moon.&lt;/p&gt;

&lt;p&gt;Then it clicked: &lt;strong&gt;I needed a lookup.&lt;/strong&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;profileMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author_id&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;profile&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&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;profile&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display_name&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;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lines. Two days. This is what programming is. I've made peace with it.&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%2F50rbp62hyezq42wqmph8.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%2F50rbp62hyezq42wqmph8.png" alt="Image" width="800" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 6 — The Inbox That Wouldn't Load
&lt;/h2&gt;

&lt;p&gt;The inbox should load in two seconds. On my tablet, it loaded in &lt;strong&gt;never&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here was the culprit:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&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;docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_documents&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;engine_updates&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&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="c1"&gt;// ...and four more fetches&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="s1"&gt;Error&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;Every fetch inside one &lt;code&gt;try/catch&lt;/code&gt;. If any single one hangs, the whole chain waits forever — and my data was clocking in at roughly 2 bars and a prayer that week, so they hung &lt;em&gt;constantly&lt;/em&gt;. (Nigerian networks are fine, by the way. It's not the network. It's a man who refuses to buy more data and instead debugs for six hours as a substitute.)&lt;/p&gt;

&lt;p&gt;The fix: every fetch gets its own &lt;code&gt;try/catch&lt;/code&gt; and a hard timeout.&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;withTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;supabase&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="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="nf"&gt;select&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="mi"&gt;4000&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four seconds, then move on. If a fetch fails, that one card just doesn't render — everything else still does.&lt;/p&gt;

&lt;p&gt;The old inbox took 40 seconds to load, when it loaded at all. The new one takes 2. &lt;strong&gt;Twenty times faster, from forty lines of defensive code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Lesson: fast isn't about speed. It's about knowing when to give up.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 7 — The Buttons Nobody Could See
&lt;/h2&gt;

&lt;p&gt;My first on-screen button was &lt;code&gt;rgba(255,255,255,0.18)&lt;/code&gt; — white at 18% opacity. Basically a ghost. Invisible on light backgrounds, barely-there on dark ones, a mistake on all of them.&lt;/p&gt;

&lt;p&gt;Players would tap the screen, something would happen, and they'd have no idea why.&lt;/p&gt;

&lt;p&gt;New buttons:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Button&lt;/th&gt;
&lt;th&gt;Color&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Move&lt;/td&gt;
&lt;td&gt;Dark navy &lt;code&gt;#1a1a2e&lt;/code&gt; + white text&lt;/td&gt;
&lt;td&gt;Solid, unambiguous&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jump&lt;/td&gt;
&lt;td&gt;Bright blue &lt;code&gt;#2563eb&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Clearly an action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fire&lt;/td&gt;
&lt;td&gt;Bright red &lt;code&gt;#dc2626&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Clearly an action&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Plus labels on top: &lt;code&gt;◀&lt;/code&gt; &lt;code&gt;▶&lt;/code&gt; &lt;code&gt;JUMP&lt;/code&gt; &lt;code&gt;FIRE&lt;/code&gt;. No more guessing.&lt;/p&gt;

&lt;p&gt;The runner template has &lt;strong&gt;zero&lt;/strong&gt; buttons — tap anywhere to jump. "The screen is the button." Beautiful, and free.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 8 — Game Over → Play Again
&lt;/h2&gt;

&lt;p&gt;Before: you lose. The screen freezes. Your score just sits there, mocking you. You refresh the page. You lose your score too.&lt;/p&gt;

&lt;p&gt;After: you lose, and a blue &lt;strong&gt;▶ PLAY AGAIN&lt;/strong&gt; button appears center screen. Tap it — score zeroes, timer resets, you're back in, one tap away.&lt;/p&gt;

&lt;p&gt;Fifteen minutes of work. Turned every template from "play once and close the tab" into "play forever." This is the feature that separates a tech demo from an actual game.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 9 — The Features That Were Missing
&lt;/h2&gt;

&lt;p&gt;Every feature below came from actually using my own platform and hitting a wall. Not a roadmap. Not user requests — I had no users yet.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The arcade&lt;/td&gt;
&lt;td&gt;Because nobody could see the games&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thumbnails&lt;/td&gt;
&lt;td&gt;Because blank cards look broken&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Likes&lt;/td&gt;
&lt;td&gt;Because I wanted a signal people were playing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Leaderboard&lt;/td&gt;
&lt;td&gt;Because "top games" beats "all games"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weekly reset&lt;/td&gt;
&lt;td&gt;Because the leaderboard got stale fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comments&lt;/td&gt;
&lt;td&gt;Because I wanted to know what people thought&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replies&lt;/td&gt;
&lt;td&gt;Because one-way comments felt empty&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Usernames&lt;/td&gt;
&lt;td&gt;Because email prefixes should never be public&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Profiles&lt;/td&gt;
&lt;td&gt;Because usernames needed a home&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Follows&lt;/td&gt;
&lt;td&gt;Because "creator" is a real concept&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inbox&lt;/td&gt;
&lt;td&gt;Because notifications make a platform feel alive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Touch buttons&lt;/td&gt;
&lt;td&gt;Because tablets exist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Play Again&lt;/td&gt;
&lt;td&gt;Because the game isn't over until &lt;em&gt;you&lt;/em&gt; say so&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's the real lesson: &lt;strong&gt;you don't design a platform, you use one until the gaps become obvious&lt;/strong&gt; — then you build them, one weekend at a time, because you're the only one who cares they're missing.&lt;/p&gt;

&lt;p&gt;That's not "features nobody asked for." That's the job.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 10 — What I Actually Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You are your first user.&lt;/strong&gt; Every confusing button and broken flow you notice before anyone else does is real. Fix it — that's not scope creep, that's the product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Users don't care about your architecture.&lt;/strong&gt; They care that the button is visible, the game loads fast, and the leaderboard updates. Your state management is invisible to them, on purpose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeouts beat retries.&lt;/strong&gt; Four seconds then move on is a design decision. Waiting forever is a bug wearing a trench coat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small UX wins beat big features.&lt;/strong&gt; A &lt;code&gt;PLAY AGAIN&lt;/code&gt; button beats a new template. A readable button beats a pretty one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usernames must be separate from display names.&lt;/strong&gt; One person, two identities, two lifespans. Respect both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Loading..." past 5 seconds isn't loading. It's broken.&lt;/strong&gt; Fix it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You will spend two days on three lines of code&lt;/strong&gt;, and it will feel like three years, and then you'll commit it, feel like a genius, open your email, and find 47 new messages. That's fine. That's the job too.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Part 11 — The Loop
&lt;/h2&gt;

&lt;p&gt;Nothing is ever done. But right now, the loop works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Make a game → Publish it → Share the link → Watch people play
→ Get likes → Rank on the leaderboard → Check the inbox → Make another
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the thing that has to work before anything else matters. And it does.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 12 — What's Still Coming
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short-term (this month)&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tags &amp;amp; categories&lt;/td&gt;
&lt;td&gt;Filter by puzzle, runner, shooter, dress-up — instead of scrolling everything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Featured banner&lt;/td&gt;
&lt;td&gt;Highlight one game a week on the arcade home page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search by tag&lt;/td&gt;
&lt;td&gt;Pairs with existing search — find all "runner" games at once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real thumbnails&lt;/td&gt;
&lt;td&gt;Render an actual screenshot on publish instead of abstract rectangles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mobile multi-touch&lt;/td&gt;
&lt;td&gt;Move &lt;em&gt;and&lt;/em&gt; shoot at once — an engine-level fix, not just a platform one&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Medium-term (1–2 months)&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Profile badges&lt;/td&gt;
&lt;td&gt;"Won the week of Sep 8" — give people bragging rights&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-category leaderboards&lt;/td&gt;
&lt;td&gt;Top platformer, top runner, top puzzle — rewards variety&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notifications&lt;/td&gt;
&lt;td&gt;"Kofi replied to your comment" — instead of checking the inbox manually&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private sharing&lt;/td&gt;
&lt;td&gt;Publish without listing in the arcade, for WIPs and friends-only games&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Follow feed&lt;/td&gt;
&lt;td&gt;A timeline of games from creators you follow&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Long-term (not sure yet)&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Why I'm hesitating&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Collaborative editing&lt;/td&gt;
&lt;td&gt;Two people, one game, live. Cool in theory, nightmare in practice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Moderation tools&lt;/td&gt;
&lt;td&gt;Right now I delete comments manually via the Supabase dashboard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monetization&lt;/td&gt;
&lt;td&gt;Sounds nice, adds friction to a platform that currently has none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native mobile app&lt;/td&gt;
&lt;td&gt;The web app works fine on tablets — this is a big lift for a small platform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open-sourcing it&lt;/td&gt;
&lt;td&gt;I want to. I need to clean up the 4-second timeouts first&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;How I decide what's next:&lt;/strong&gt; not by writing a roadmap and following it religiously — by opening my own arcade and getting annoyed at something that should exist and doesn't. Every item in Part 9 came from that process. Every item above will too.&lt;/p&gt;

&lt;p&gt;The one thing I'm sure of: &lt;strong&gt;the loop works.&lt;/strong&gt; Everything else is decoration.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's New — At a Glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🎮 &lt;strong&gt;Arcade gallery&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Browse every published game as a card with an auto-generated SVG thumbnail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🏆 &lt;strong&gt;Weekly leaderboard&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Top 5 games ranked by likes, resets every Monday at 00:00 UTC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;💬 &lt;strong&gt;Comments + replies&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Two-level nested comments with inline reply forms (no more &lt;code&gt;prompt()&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🧑 &lt;strong&gt;Usernames + profiles&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Permanent unique handles, editable display names, public &lt;code&gt;/u/?u=&lt;/code&gt; profile pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;👥 &lt;strong&gt;Follows&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Follow creators, see their games from one place&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;📥 &lt;strong&gt;Inbox&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Weekly leaderboard cards, notifications — loads in 2s flat, timeout-protected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🕹️ &lt;strong&gt;Touch controls&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Redesigned high-contrast buttons for mobile play, plus tap-anywhere runners&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔁 &lt;strong&gt;Play Again&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;One-tap restart on every game template — no more refreshing the page&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🕹️ &lt;strong&gt;Arcade:&lt;/strong&gt; &lt;a href="https://limn-engine-doc.vercel.app/arcade" rel="noopener noreferrer"&gt;limn-engine-doc.vercel.app/arcade&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎨 &lt;strong&gt;Build a game:&lt;/strong&gt; &lt;a href="https://limn-engine-doc.vercel.app/editor" rel="noopener noreferrer"&gt;limn-engine-doc.vercel.app/editor&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;⚙️ &lt;strong&gt;Set up a profile:&lt;/strong&gt; &lt;a href="https://limn-engine-doc.vercel.app/settings.html" rel="noopener noreferrer"&gt;limn-engine-doc.vercel.app/settings.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;👤 &lt;strong&gt;Follow me:&lt;/strong&gt; &lt;code&gt;/u/?u=desire&lt;/code&gt; — follower count currently says 1, and I'm 90% sure that 1 is me&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you make a game, send me the link. I'll play it. I'll probably like it. I might even comment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Get Involved / Get Help
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Link&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;💬 Discord&lt;/td&gt;
&lt;td&gt;&lt;a href="https://discord.gg/kVpSmYWXr" rel="noopener noreferrer"&gt;discord.gg/kVpSmYWXr&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🧑‍💻 GitHub&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/terracodes004" rel="noopener noreferrer"&gt;github.com/terracodes004&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;📧 Email&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:evolvedtech004@gmail.com"&gt;evolvedtech004@gmail.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Bug reports, feature ideas, "why is my button rgba(255,255,255,0.18)" — all welcome.&lt;/p&gt;




&lt;h2&gt;
  
  
  Credits
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Limn Engine&lt;/strong&gt; — built by &lt;a href="https://dev.to/kehinde_owolabi_e2e54567a"&gt;Kehinde Owolabi&lt;/a&gt;, founder of &lt;strong&gt;Evolved Tech&lt;/strong&gt;. He built the core engine: one file, no dependencies, no build tools. Every game on this platform runs on his code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The editor, arcade, leaderboard, profiles, comments, inbox, buttons, and everything in Part 12&lt;/strong&gt; — me, Desire, co-founder at Evolved Tech. Built one weekend at a time, on a tablet, while muttering about timeouts — because I kept using our own platform and kept finding things that bothered me.&lt;/p&gt;

&lt;p&gt;If the games run, that's Kehinde. If the buttons work, that's me. If something's broken, it's probably both of us, and we'll fix it together — right after Kehinde finishes explaining, again, why this is finally Arsenal's year.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Draw your game into existence — with a canvas and a dream.&lt;/em&gt; 🎨🎮&lt;/p&gt;

&lt;p&gt;&lt;code&gt;#javascript&lt;/code&gt; &lt;code&gt;#gamedev&lt;/code&gt; &lt;code&gt;#webdev&lt;/code&gt; &lt;code&gt;#showdev&lt;/code&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>gamedev</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Limn Engine Just Got Smarter — Here's What We Added (And Why It Matters)</title>
      <dc:creator>Olu Desire</dc:creator>
      <pubDate>Mon, 07 Sep 2026 18:50:31 +0000</pubDate>
      <link>https://dev.to/desire_george_434_ai/limn-engine-just-got-smarter-heres-what-we-added-and-why-it-matters-2gc1</link>
      <guid>https://dev.to/desire_george_434_ai/limn-engine-just-got-smarter-heres-what-we-added-and-why-it-matters-2gc1</guid>
      <description>&lt;p&gt;The question that started it all: "Why do we have to write the same code in every single game?" So we built Limn Engine — a zero-configuration, browser-based 2D game engine that hits 60 FPS on hardware old enough to vote. It got published on SitePoint and ranked #3 among 2D JavaScript game engines, which we now mention in every conversation, including this one.&lt;/p&gt;

&lt;p&gt;Today's post is about what's new. And to be crystal clear: &lt;strong&gt;all of it is live right now.&lt;/strong&gt; Not "coming soon," not "in beta," not "works on our machine." You can go use every single feature below today, this minute, before you even finish this sentenc—okay you finished it, go use them.&lt;/p&gt;

&lt;p&gt;Fair warning: some of these features exist purely because we got tired of doing something annoying, several times, in a row, forever.&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%2Fl9uae5lx9tq4dsebto6x.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%2Fl9uae5lx9tq4dsebto6x.png" alt="Image showing the playground editor with its sharable url button" width="799" height="454"&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%2F0zueiudeiwmv7kmv2k3w.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%2F0zueiudeiwmv7kmv2k3w.png" alt="Image showing the sign up page of limn Engine" width="800" height="451"&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%2Fqrhtc0w0l3ovtevvy1fg.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%2Fqrhtc0w0l3ovtevvy1fg.png" alt="Image showing the homepage of limn engine" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&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%2Fuk0lg23m66tb8r49h6sp.png" alt="Image showing the inbox page of limn engine" width="800" height="446"&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  📋 The TL;DR (For Those Who Scroll First, Read Later — No Judgment)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;th&gt;Why You'll Care&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🔐 &lt;strong&gt;Google OAuth&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;One-click sign-in via Google, plus email OTP as backup. Powered by Supabase Auth.&lt;/td&gt;
&lt;td&gt;No passwords, no forms, no "forgot password" emails haunting your inbox at 3 AM.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔗 &lt;strong&gt;Shareable Code Snippets&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Write code in the playground, hit Share, get a unique URL.&lt;/td&gt;
&lt;td&gt;Send exact working code to friends instead of dumping 200 lines into Discord like a ransom note.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔍 &lt;strong&gt;Universal Search&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Global search overlay — hit ESC or click the icon.&lt;/td&gt;
&lt;td&gt;Find docs, API refs, tutorials, and community posts instantly. Even we use it now.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;📱 &lt;strong&gt;Touch-Friendly Design&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Fully responsive interface across desktop, tablet, and phone.&lt;/td&gt;
&lt;td&gt;Build your game from the couch. We don't make the rules, we just enable them.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;💬 &lt;strong&gt;Inbox &amp;amp; Messaging&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Built-in messaging system for updates and collaborator chats.&lt;/td&gt;
&lt;td&gt;Stay in the loop without alt-tabbing to five other apps.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Status: all shipped. All live. Go bother them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now, for those of you who like the director's commentary — here's the full story behind each one.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Problems That Kept Us Up At Night
&lt;/h3&gt;

&lt;p&gt;Rule one of building Limn Engine: "Common outcomes should be functions." Edge detection, camera math, collision loops — if we typed it twice, it became &lt;code&gt;move.bound(player)&lt;/code&gt; or &lt;code&gt;camera.shake(5,5)&lt;/code&gt; before we typed it a third time out of spite.&lt;/p&gt;

&lt;p&gt;But the engine's &lt;em&gt;infrastructure&lt;/em&gt; needed the same intervention. Three problems in particular were staging a nightly protest in our brains:&lt;/p&gt;

&lt;p&gt;· &lt;strong&gt;Authentication&lt;/strong&gt; — every game needs users, and building sign-up/login/password-reset from scratch takes weeks we do not have and would rather spend on, frankly, anything else.&lt;br&gt;
· &lt;strong&gt;Collaboration&lt;/strong&gt; — "send us your code" in Discord meant receiving 200 unformatted lines that looked like a ransom note.&lt;br&gt;
· &lt;strong&gt;Discovery&lt;/strong&gt; — finding anything on our own website had become a scavenger hunt with no prize at the end.&lt;/p&gt;

&lt;p&gt;So, in the spirit of turning problems into functions, we fixed them. All three. They're live. We already told you that but it bears repeating because we're proud of ourselves.&lt;/p&gt;




&lt;h3&gt;
  
  
  🔐 Google OAuth: One Click, No Passwords — Live Now
&lt;/h3&gt;

&lt;p&gt;Nobody has ever, in the history of the internet, been excited to "Create an account." You see that button, your soul leaves your body a little, and you close the tab. We know. We've done it to other websites while building the fix for our own.&lt;/p&gt;

&lt;p&gt;So Limn Engine now runs on Google OAuth as the primary login method:&lt;/p&gt;

&lt;p&gt;· &lt;strong&gt;One click, and you're in.&lt;/strong&gt; No forms, no email verification purgatory, no "forgot password" email arriving at 3 AM to remind you of your own incompetence.&lt;br&gt;
· &lt;strong&gt;Instant onboarding&lt;/strong&gt; — straight to building, zero friction, zero excuses left to procrastinate with.&lt;br&gt;
· &lt;strong&gt;We never see your password&lt;/strong&gt;, mostly because we don't want that responsibility. Google can have it. Google seems more organized than us.&lt;/p&gt;

&lt;p&gt;Prefer not to hand Google the keys? We also added email OTP login, because choice is nice and so is not being cornered.&lt;/p&gt;

&lt;p&gt;Under the hood, this runs on Supabase Auth, a real production-grade auth system built by people who are much better at this than we are. We know our strengths, and "building auth from scratch" is not one of them. We have games to make.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The takeaway:&lt;/strong&gt; authentication should never be the reason someone bounces off your engine. Consider that reason eliminated. You're welcome.&lt;/p&gt;




&lt;h3&gt;
  
  
  🔗 Shareable Code Snippets: Collaboration Without The Crime Scene — Live Now
&lt;/h3&gt;

&lt;p&gt;This one's personal.&lt;/p&gt;

&lt;p&gt;Every debugging session in Discord went the same way: "send us your code," followed by 200 lines of unindented JavaScript arriving like evidence at a trial nobody asked for. Our DMs looked less like a support channel and more like a crime scene with syntax errors.&lt;/p&gt;

&lt;p&gt;So now the playground has a Share button that generates one clean URL, sendable anywhere — Discord, WhatsApp, Twitter, email, and yes we did consider carrier pigeon and no it did not make the roadmap.&lt;/p&gt;

&lt;p&gt;What that gets you:&lt;/p&gt;

&lt;p&gt;· &lt;strong&gt;Debugging that doesn't require detective work&lt;/strong&gt; — stuck on a physics bug? Send the link. Your friend sees exactly what you see, and "works on my machine" officially retires.&lt;br&gt;
· &lt;strong&gt;A place to flex&lt;/strong&gt; — nailed a particle effect in 30 seconds? Send the link and let people watch it run live, no copy-paste required.&lt;br&gt;
· &lt;strong&gt;A Discord server that's actually readable&lt;/strong&gt; — no more scrolling past a wall of code to find the one message that matters. Our eyes, and yours, say thank you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The takeaway:&lt;/strong&gt; sharing code is now exactly as easy as sharing a meme, which was frankly always the bar.&lt;/p&gt;




&lt;h3&gt;
  
  
  🔍 Universal Search: Find Anything, Instantly — Live Now
&lt;/h3&gt;

&lt;p&gt;Our website had grown a lot of rooms and forgotten to build any hallways. Docs, API references, tutorials, community posts — all real, all somewhere, none easily findable, including by us. We have genuinely spent 10 minutes hunting for our own documentation, which is a special kind of humbling.&lt;/p&gt;

&lt;p&gt;So we built a global search overlay. Hit ESC, or click the icon, type your question, get your answer. No reloads, no menu spelunking.&lt;/p&gt;

&lt;p&gt;Searchable: API references, documentation, community threads, game templates, tutorials, and everything else we forgot to list here (which the search bar will also find, conveniently).&lt;/p&gt;

&lt;p&gt;It's fast, it's obvious, and it makes Limn Engine feel like a serious tool built by serious people — a costume we are, at this point, fully committed to wearing in public.&lt;/p&gt;




&lt;h3&gt;
  
  
  📱 Touch-Friendly Design: Build Anywhere — Live Now
&lt;/h3&gt;

&lt;p&gt;Games get played on desktops, tablets, and phones, so it never made sense that you could only &lt;em&gt;build&lt;/em&gt; one hunched over a laptop like it's a ritual. We rebuilt the interface to be genuinely touch-responsive, whether you're:&lt;/p&gt;

&lt;p&gt;· Skimming docs on a tablet in bed&lt;br&gt;
· Managing your community from your phone on a lunch break&lt;br&gt;
· Poking at your game on whatever screen happens to be nearby&lt;/p&gt;

&lt;p&gt;No pinch-to-zoom archaeology, no buttons sized for a smaller species of thumb. Just an interface that works wherever you decided to be productive today, including the couch, which we fully endorse.&lt;/p&gt;




&lt;h3&gt;
  
  
  💬 Inbox &amp;amp; Messaging: Stay Connected — Live Now
&lt;/h3&gt;

&lt;p&gt;Building alone is fine. Building with people who occasionally message you back is better. So we added a built-in inbox and messaging system for:&lt;/p&gt;

&lt;p&gt;· Getting updates and announcements, so "what's new" isn't a mystery&lt;br&gt;
· Talking to collaborators without leaving the platform&lt;br&gt;
· Staying plugged into the wider Limn Engine community&lt;/p&gt;

&lt;p&gt;It's a small feature that makes us feel disproportionately like a real company — one with an actual office, not just a room, some laptops, and a shared refusal to give up.&lt;/p&gt;




&lt;h3&gt;
  
  
  🎮 What's Next?
&lt;/h3&gt;

&lt;p&gt;Currently in the workshop (these ones, unlike everything above, are genuinely not out yet — we'd tell you):&lt;/p&gt;

&lt;p&gt;· &lt;strong&gt;TypeScript support&lt;/strong&gt; — for developers who like their errors caught before runtime, understandably&lt;br&gt;
· &lt;strong&gt;An NPM package&lt;/strong&gt; — &lt;code&gt;npm install limn-engine&lt;/code&gt; is coming, we promise, for real this time&lt;br&gt;
· &lt;strong&gt;More game templates&lt;/strong&gt; — for when the blank canvas is scarier than the actual coding&lt;br&gt;
· &lt;strong&gt;Better physics and particles&lt;/strong&gt; — because if something explodes in your game, it should look &lt;em&gt;incredible&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We're building this for the long haul, and we'd like some company.&lt;/p&gt;




&lt;h3&gt;
  
  
  🙌 Join The Community
&lt;/h3&gt;

&lt;p&gt;Already here? You're basically family at this point, and we mean that with zero legal obligation attached.&lt;/p&gt;

&lt;p&gt;New? Come say hi:&lt;/p&gt;

&lt;p&gt;· &lt;strong&gt;#beginner-help&lt;/strong&gt; — no question is too small, we've asked worse&lt;br&gt;
· &lt;strong&gt;#showcase&lt;/strong&gt; — show us your first game, we will cheer, possibly too loudly&lt;br&gt;
· &lt;strong&gt;#engine-dev&lt;/strong&gt; — help build the engine itself, we need every hand we can get&lt;br&gt;
· &lt;strong&gt;#performance&lt;/strong&gt; — help us make this thing run on a toaster, we're only half joking&lt;/p&gt;




&lt;h3&gt;
  
  
  🏁 Final Thoughts
&lt;/h3&gt;

&lt;p&gt;We started with a Toshiba laptop, 4GB of RAM, and one stubborn question: "Why do we have to write the same code in every single game?"&lt;/p&gt;

&lt;p&gt;That question is now a real, live, shipped engine with:&lt;/p&gt;

&lt;p&gt;· Zero configuration, because life is complicated enough already&lt;br&gt;
· 60 FPS on low-end hardware, yes, even that one&lt;br&gt;
· Google OAuth, one click, no passwords, no drama&lt;br&gt;
· Shareable code snippets, because copy-paste had its moment and that moment ended&lt;br&gt;
· Universal search, so nobody has to hunt for their own documentation again (looking at us)&lt;br&gt;
· Touch-friendly design, because the couch is a legitimate workstation&lt;br&gt;
· A community that keeps showing up, which is honestly the part we're proudest of&lt;/p&gt;

&lt;p&gt;Built by a team that refused to give up, mostly out of stubbornness.&lt;/p&gt;




&lt;h3&gt;
  
  
  🔗 Links
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;💬 &lt;strong&gt;Discord&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;a href="https://discord.gg/kVpSmYWXr" rel="noopener noreferrer"&gt;discord.gg/kVpSmYWXr&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;💻 &lt;strong&gt;GitHub&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/terracodes004" rel="noopener noreferrer"&gt;github.com/terracodes004&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✉️ &lt;strong&gt;Support Email&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:evolvedtech004@gmail.com"&gt;evolvedtech004@gmail.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Got questions, bugs, or just want to show off what you built? Drop into the Discord or email us — we read everything, we promise, we're not just saying that like a company that doesn't.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Draw your game into existence — the easy and advanced way.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>productivity</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <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>
