<?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: dundun sun</title>
    <description>The latest articles on DEV Community by dundun sun (@dundunup).</description>
    <link>https://dev.to/dundunup</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3949859%2Ff91f1c89-5cb2-4554-921f-eafd99a804d1.jpg</url>
      <title>DEV Community: dundun sun</title>
      <link>https://dev.to/dundunup</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dundunup"/>
    <language>en</language>
    <item>
      <title>Building a Game with Zero Game Libraries — The Architecture Behind CursorCamp Sandbox</title>
      <dc:creator>dundun sun</dc:creator>
      <pubDate>Mon, 25 May 2026 03:26:47 +0000</pubDate>
      <link>https://dev.to/dundunup/building-a-game-with-zero-game-libraries-the-architecture-behind-cursorcamp-sandbox-20hn</link>
      <guid>https://dev.to/dundunup/building-a-game-with-zero-game-libraries-the-architecture-behind-cursorcamp-sandbox-20hn</guid>
      <description>&lt;p&gt;You'd think building a browser game requires a stack of dependencies. Phaser, PixiJS, maybe Three.js. A physics engine, an audio library, a state manager.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CursorCamp Sandbox&lt;/strong&gt; does none of that. Zero game engines. Zero physics libraries. Just Next.js, TypeScript, raw Canvas 2D, and inline SVG. Under 6,000 lines of hand-rolled code.&lt;/p&gt;

&lt;p&gt;Here's how the architecture works.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is It?
&lt;/h2&gt;

&lt;p&gt;An interactive fan-made companion for Neal.fun's Cursor Camp. You explore a procedurally-detailed world, collect seashells, kick soccer balls, and share the space with 55 AI campers. Weather changes. Music fades as you move around. Everything runs in the browser at &lt;a href="https://www.cursorcamp-sandbox.com" rel="noopener noreferrer"&gt;cursorcamp-sandbox.com&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack (Spoiler: It's Empty)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"14.2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"react"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^18.3.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"react-dom"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^18.3.0"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Every extra dependency — &lt;code&gt;next-sitemap&lt;/code&gt;, &lt;code&gt;tailwindcss&lt;/code&gt;, &lt;code&gt;postcss&lt;/code&gt;, &lt;code&gt;typescript&lt;/code&gt; — is either build tooling or a sitemap generator. The runtime ships with Next.js and React only.&lt;/p&gt;

&lt;p&gt;No Phaser. No Matter.js. No Howler.js. No Redux. No Framer Motion.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dual-Canvas Rendering Pipeline
&lt;/h2&gt;

&lt;p&gt;The most interesting architectural decision: &lt;strong&gt;two HTML Canvas elements overlaid on top of each other&lt;/strong&gt;, separated by z-index, each responsible for a different set of concerns.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Layer 1 (canvasRef)     → terrain, trees, structures, entities, bots
Layer 2 (cursorCanvasRef) → weather particles, cursor pointers, accessories
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids &lt;code&gt;globalCompositeOperation&lt;/code&gt; tricks and makes the render loop cleaner. Each canvas gets fully cleared and redrawn every frame via &lt;code&gt;requestAnimationFrame&lt;/code&gt;. No dirty-rect tracking, no spatial partitioning — just brute-force, draw-everything, every tick.&lt;/p&gt;

&lt;p&gt;Between the two canvas layers sit &lt;strong&gt;inline React SVG components&lt;/strong&gt; positioned absolutely over the world. The DJ stage, the lake, the gym equipment, the banner — all rendered as SVG elements with CSS animations (rotating spotlights, rippling water). They share the same viewBox coordinate system as the canvas, so positioning is consistent.&lt;/p&gt;

&lt;p&gt;Why SVG for these? Because CSS animations on SVG elements are GPU-accelerated and don't need to be re-drawn in the canvas loop. The DJ's moving-head spotlights rotate with a few lines of CSS &lt;code&gt;@keyframes&lt;/code&gt; instead of recalculating angles in JavaScript every frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  The State Split That Makes It Work
&lt;/h2&gt;

&lt;p&gt;React isn't built for game loops. If you put framerate-critical state into &lt;code&gt;useState&lt;/code&gt;, you'll trigger re-renders 60 times per second and kill performance.&lt;/p&gt;

&lt;p&gt;The solution: &lt;strong&gt;a deliberate split between &lt;code&gt;useRef&lt;/code&gt; and &lt;code&gt;useState&lt;/code&gt;&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Game state → useRef (no re-renders)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;camRef&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Vec2&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;4000&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;1050&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;botsRef&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Bot&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;createBots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;55&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;ballRef&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Ball&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;pos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FIELD_CENTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;vel&lt;/span&gt;&lt;span class="p"&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="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// UI state → useState (triggers re-renders for UI updates)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;shellsCollected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setShellsCollected&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setNotification&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;showJournal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setShowJournal&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The game loop reads and mutates refs 60 times per second without touching React's render cycle. Only UI-relevant state — collected shell counts, notification popups, journal visibility — lives in &lt;code&gt;useState&lt;/code&gt; and triggers the normal React pipeline.&lt;/p&gt;

&lt;p&gt;This is embarrassingly simple compared to ECS (Entity Component System) architectures, but for a single-developer project with ~200 entities, it works perfectly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bot AI State Machine
&lt;/h2&gt;

&lt;p&gt;55 campers wander the map simultaneously. Each one runs a &lt;strong&gt;5-phase movement state machine&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;idle → accel → cruise → decel → overshoot → idle (loop)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;idle&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stands still for 1-4 seconds, fidgets slightly, picks a random destination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;accel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Eases into movement along a quadratic Bezier curve toward the target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;cruise&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Moves at full speed with sub-pixel jitter for natural-looking motion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;decel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Eases out as it approaches the target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;overshoot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;If it passes the target, corrects back with a small counter-movement&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A couple bots also keep an eye on the soccer ball. If the ball is kicked near them, they enter a chase mode and move toward it. When the ball is idle too long, a random nearby bot kicks it.&lt;/p&gt;

&lt;p&gt;The result looks organic — not like pathfinding soldiers, more like actual people wandering a campsite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spatial Proximity Audio
&lt;/h2&gt;

&lt;p&gt;The audio system has no external dependencies. It uses raw &lt;code&gt;HTMLAudioElement&lt;/code&gt; objects with proximity-based volume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateDJProximity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;vol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;djAudio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;volume&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vol&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;Each sound source — DJ booth, campfire, ocean, river, car horn, football — has its own init and update function. The audio elements are created once at startup and their volumes are modulated every frame based on the player's distance to the sound source.&lt;/p&gt;

&lt;p&gt;The DJ system is the most complex: a 4-track playlist that auto-advances on the &lt;code&gt;ended&lt;/code&gt; event with crossfading. Walk toward the DJ booth, the music fades in. Walk away, it fades out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Procedural Terrain, Deterministically
&lt;/h2&gt;

&lt;p&gt;An 8,000 × 5,000 pixel world with oceans, rivers, beaches, roads, soccer fields, running tracks, vegetable gardens, and scattered decorations — all generated at runtime from math.&lt;/p&gt;

&lt;p&gt;The secret weapon is a &lt;code&gt;hash2d&lt;/code&gt; function that takes two integers and returns a deterministic pseudo-random number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hash2d&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="kr"&gt;number&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="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&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;h&lt;/span&gt; &lt;span class="o"&gt;=&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="mi"&gt;374761393&lt;/span&gt; &lt;span class="o"&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;668265263&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1274126177&lt;/span&gt;&lt;span class="p"&gt;)&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;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1274126177&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4294967296&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 flower, grass tuft, bush, and stone is positioned using this hash. The result looks organic but is completely deterministic — every render produces the same layout. No random seed, no saved state, just math.&lt;/p&gt;

&lt;h2&gt;
  
  
  Isometric 2.5D Without a 3D Engine
&lt;/h2&gt;

&lt;p&gt;All SVG components use "fake isometric" rendering: explicit 2D coordinates that simulate depth. Building roofs are parallelograms. Shadows are dark ellipses under entities. The DJ stage platform surface is computed using bilinear interpolation. Everything is top-down isometric drawn in pure 2D — no projection matrices, no 3D transforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  SVG Data URIs as Sprite Sheets
&lt;/h2&gt;

&lt;p&gt;Every game asset — trees, the campfire, houses, the soccer ball, shells — is an &lt;strong&gt;inline SVG stored as a data URI&lt;/strong&gt; in &lt;code&gt;assets.ts&lt;/code&gt;. These are loaded into &lt;code&gt;Image&lt;/code&gt; objects once at startup, then drawn with &lt;code&gt;ctx.drawImage()&lt;/code&gt; in the game loop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TREE_ROUND_SVG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data:image/svg+xml,...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// 2KB of hand-crafted SVG&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;img&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;Image&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;TREE_ROUND_SVG&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Later, in the render loop:&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&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="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This eliminates all external image requests. The entire game loads as a single HTML page with inline assets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Weather That Works
&lt;/h2&gt;

&lt;p&gt;Rain and snow are particle systems running on the cursor canvas layer. Rain drops are angled streaks with wind variation. Snow particles float with multi-frequency wobble. Splash particles spawn when rain hits the "ground" at the bottom of the viewport.&lt;/p&gt;

&lt;p&gt;The weather system transitions on a timer — 50-100 seconds of clear sky, then 25-35 seconds of rain or snow, with a smooth fade transition in between. Wind direction drifts slowly over time using a sine function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Water Physics
&lt;/h2&gt;

&lt;p&gt;Wading into the ocean or river doesn't just change the visual — it changes how you move. Ocean depth uses a quadratic resistance curve: ankle-deep gives light drag, while 200px deep makes you nearly immovable. The river applies a consistent 85% drag plus directional current force. Diving in triggers a splash sound via edge-triggered audio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;The takeaway isn't "you should build games without engines." It's that &lt;strong&gt;the browser platform is incredibly capable on its own&lt;/strong&gt;. Canvas 2D, SVG, CSS animations, and the Web Audio API can handle far more than most developers assume — especially when you're one person building something for the love of it.&lt;/p&gt;

&lt;p&gt;No build pipeline for sprite sheets. No WebGL shader debugging. No physics engine configuration. Just TypeScript, math, and the raw browser.&lt;/p&gt;

&lt;p&gt;If you want to see it in action, it's at &lt;a href="https://www.cursorcamp-sandbox.com" rel="noopener noreferrer"&gt;cursorcamp-sandbox.com&lt;/a&gt;. The source is all there in your browser's dev tools — no minification, no obfuscation, just readable code.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Next.js 14, TypeScript, and precisely zero game libraries.&lt;/em&gt;&lt;/p&gt;

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