<?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: Yosvani Noriega</title>
    <description>The latest articles on DEV Community by Yosvani Noriega (@yosvani_noriega_ed748d272).</description>
    <link>https://dev.to/yosvani_noriega_ed748d272</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%2F4046309%2F68ea31e0-f070-4f6f-b223-e10beba11cf8.png</url>
      <title>DEV Community: Yosvani Noriega</title>
      <link>https://dev.to/yosvani_noriega_ed748d272</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yosvani_noriega_ed748d272"/>
    <language>en</language>
    <item>
      <title>How I Built a Full RPG Game in TypeScript — Zero Images, Pure Canvas 2D</title>
      <dc:creator>Yosvani Noriega</dc:creator>
      <pubDate>Sat, 25 Jul 2026 03:55:39 +0000</pubDate>
      <link>https://dev.to/yosvani_noriega_ed748d272/how-i-built-a-full-rpg-game-in-typescript-zero-images-pure-canvas-2d-38em</link>
      <guid>https://dev.to/yosvani_noriega_ed748d272/how-i-built-a-full-rpg-game-in-typescript-zero-images-pure-canvas-2d-38em</guid>
      <description>&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;p&gt;No sprites. No assets. No external images. Just a browser, TypeScript, and the Canvas 2D API. Could I build a playable RPG?&lt;/p&gt;

&lt;p&gt;The answer is yes — and it's live at &lt;a href="https://lazaramolina17-stack.github.io/rpg-game" rel="noopener noreferrer"&gt;lazaramolina17-stack.github.io/rpg-game&lt;/a&gt;. Everything you see is drawn procedurally at runtime: the grass, the water, the characters, the enemies, even the UI. Here's how.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;The game is organised into seven modules, each with a single responsibility:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/web/
├── main.ts        # Game loop, entity management, combat, dialogue
├── graphics.ts    # Procedural vector engine (tiles, sprites, effects)
├── renderer.ts    # Canvas rendering (tilemap, HUD, minimap, particles)
├── gameplay.ts    # RPG systems (XP, inventory, quests, spells, shop)
├── content.ts     # Data definitions (enemies, items, quests)
├── input.ts       # Keyboard input handler
├── touch.ts       # DOM overlay touch controls
└── audio.ts       # Procedural Web Audio SFX + music
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bundler (&lt;code&gt;scripts/build-web.ts&lt;/code&gt;) uses esbuild to compile everything into a single &lt;code&gt;docs/bundle.js&lt;/code&gt; (~69KB minified). GitHub Pages serves &lt;code&gt;docs/index.html&lt;/code&gt;, which loads the bundle.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Procedural Vector Graphics Engine
&lt;/h2&gt;

&lt;p&gt;The most distinctive part of the project: &lt;strong&gt;zero bitmap images&lt;/strong&gt;. All visuals are drawn with Canvas 2D paths, gradients, and shadows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tiles (7 biomes)
&lt;/h3&gt;

&lt;p&gt;Each tile is 32x32 pixels, generated procedurally with a seeded PRNG (Mulberry32) so the same tile always looks the same:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tile&lt;/th&gt;
&lt;th&gt;Technique&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Grass&lt;/td&gt;
&lt;td&gt;Linear gradient floor + quadratic grass blades + radial flower dots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forest&lt;/td&gt;
&lt;td&gt;Darker radial gradient + tree trunk arcs + canopy circles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Water&lt;/td&gt;
&lt;td&gt;Animated sine waves + moving specular highlights + foam edges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stone&lt;/td&gt;
&lt;td&gt;Fractal-like gray patches + cracks (thin strokes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sand&lt;/td&gt;
&lt;td&gt;Warm gradient + speckled noise dots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lava&lt;/td&gt;
&lt;td&gt;Pulsating orange-red gradient + drifting bright embers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snow&lt;/td&gt;
&lt;td&gt;White gradient + translucent blue overlay + snowflake specks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;drawWaterTile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CanvasRenderingContext2D&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rng&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="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t&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;grad&lt;/span&gt; &lt;span class="o"&gt;=&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;createRadialGradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&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;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addColorStop&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#1d4ed8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addColorStop&lt;/span&gt;&lt;span class="p"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#1e40af&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addColorStop&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#1e3a8a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;grad&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;fillRect&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Animated waves&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;x&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;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;32&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;4&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;wy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&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;sin&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="mf"&gt;0.4&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&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;sin&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="mf"&gt;0.7&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`rgba(255,255,255,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="mf"&gt;0.08&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;sin&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="mf"&gt;0.3&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;2.5&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.04&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&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;beginPath&lt;/span&gt;&lt;span class="p"&gt;()&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;arc&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;wy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.5&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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&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;h3&gt;
  
  
  Character Sprites
&lt;/h3&gt;

&lt;p&gt;Every character is built from the same set of geometric primitives — arcs, ellipses, bezier curves, quadratic curves — assembled differently per type:&lt;/p&gt;

&lt;p&gt;![Player visual breakdown — head, cape, torso, legs, sword, shield, all gradients]&lt;/p&gt;

&lt;p&gt;The player sprite uses &lt;strong&gt;no &lt;code&gt;fillRect&lt;/code&gt;&lt;/strong&gt; for its body. Instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cape&lt;/strong&gt;: Bezier curves with sine-wave animation for flowing movement&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Armor&lt;/strong&gt;: Quadratic curve torso with purple gradient (&lt;code&gt;#8b5cf6&lt;/code&gt; → &lt;code&gt;#5b21b6&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skirt&lt;/strong&gt;: Animated scalloped edge with per-vertex wave offset&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sword&lt;/strong&gt;: Rotated rectangle with gradient steel + gem in guard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shield&lt;/strong&gt;: Circle with gradient (&lt;code&gt;#4a4a6a&lt;/code&gt; → &lt;code&gt;#6a6a8a&lt;/code&gt;) + gold border arc&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hair&lt;/strong&gt;: Bezier curves for volume, &lt;code&gt;#fbbf24&lt;/code&gt; (blonde)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enemies are equally detailed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bandit&lt;/strong&gt;: Torn brown cape, eye patch, crooked smile&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skeleton&lt;/strong&gt;: Skull with red glowing eyes, bone lines for ribs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mage&lt;/strong&gt;: Pointed hat with stars, robe glow effect (radial gradient)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Goblin&lt;/strong&gt;: Big green head, triangle ears, large circular nose&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Boss&lt;/strong&gt;: 1.5x scale, crown, spikes on armor, yellow glowing eyes&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. Audio — Fully Procedural
&lt;/h2&gt;

&lt;p&gt;No audio files either. The &lt;code&gt;AudioManager&lt;/code&gt; uses the Web Audio API to synthesise everything:&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="nf"&gt;playAttack&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.3&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="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;playHit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.2&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="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;playLevelup&lt;/span&gt;&lt;span class="p"&gt;(){&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.3&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="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;],[&lt;/span&gt;&lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.3&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="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.1&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 is an array of oscillator parameters: &lt;code&gt;[duration, frequency, volume, detune, ...]&lt;/code&gt;. The ambient music generates a bass drone, chord pads, and a pentatonic melody — all in real time.&lt;/p&gt;

&lt;p&gt;15 distinct SFX: &lt;code&gt;attack&lt;/code&gt;, &lt;code&gt;hit&lt;/code&gt;, &lt;code&gt;enemyHit&lt;/code&gt;, &lt;code&gt;death&lt;/code&gt;, &lt;code&gt;pickup&lt;/code&gt;, &lt;code&gt;levelup&lt;/code&gt;, &lt;code&gt;heal&lt;/code&gt;, &lt;code&gt;fireball&lt;/code&gt;, &lt;code&gt;explosion&lt;/code&gt;, &lt;code&gt;coin&lt;/code&gt;, &lt;code&gt;dialogue&lt;/code&gt;, &lt;code&gt;step&lt;/code&gt;, &lt;code&gt;gameover&lt;/code&gt;, &lt;code&gt;victory&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Touch Controls — DOM Overlay
&lt;/h2&gt;

&lt;p&gt;On mobile (and desktop, since detection is hardcoded), a joystick and action buttons appear as a DOM overlay. Why DOM instead of canvas?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DOM elements handle multi-touch natively without gesture conflicts&lt;/li&gt;
&lt;li&gt;They stay fixed-position regardless of camera scroll&lt;/li&gt;
&lt;li&gt;Opacity is reduced (&lt;code&gt;alpha: 0.8&lt;/code&gt;) so game content is still visible underneath
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────┐          ┌──┐ ┌──┐ ┌──┐ ┌──┐
│  ⬆       │          │⚔ │ │✋ │ │🗺 │ │  │
│⬅ 🟣 ➡   │          └──┘ └──┘ └──┘ └──┘
│  ⬇       │          Items 1-4 action bar
│ Joystick │
└──────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each button sends events to the same &lt;code&gt;Input&lt;/code&gt; system as keyboard, so the game loop is input-agnostic.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. RPG Systems
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Content Manager (&lt;code&gt;content.ts&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Pure data. Defines 5 enemy types, 3 spells, loot tables per enemy (with drop rates), quests with objectives, and shop inventories for Merchant and Blacksmith.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gameplay Manager (&lt;code&gt;gameplay.ts&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Stateless logic layer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;XP/level progression (scaling formula: &lt;code&gt;xpToNext = level * 60 + 40&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;10-slot inventory&lt;/li&gt;
&lt;li&gt;Quest state tracking with completion detection&lt;/li&gt;
&lt;li&gt;Loot generation with weighted random drops&lt;/li&gt;
&lt;li&gt;Spell casting with mana cost validation&lt;/li&gt;
&lt;li&gt;Shop transactions (buying with gold)&lt;/li&gt;
&lt;li&gt;Damage floating text creation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Combat
&lt;/h3&gt;

&lt;p&gt;Right-click or Space to attack. The nearest enemy within range takes damage, knockback, and flashes red. Damage numbers float upward with alpha fade. Player has invincibility frames (500ms) after being hit.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Environment &amp;amp; Effects
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Day/Night Cycle
&lt;/h3&gt;

&lt;p&gt;The background sky transitions through 4 phases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dawn&lt;/strong&gt; (&lt;code&gt;t % 240 &amp;lt; 60&lt;/code&gt;): warm orange-pink gradient&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day&lt;/strong&gt; (&lt;code&gt;t % 240 &amp;lt; 120&lt;/code&gt;): bright blue with sun&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dusk&lt;/strong&gt; (&lt;code&gt;t % 240 &amp;lt; 180&lt;/code&gt;): purple-orange transition&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Night&lt;/strong&gt; (else): dark blue with stars + moon&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sun follows a sine arc across the sky. At night, stars twinkle and a crescent moon appears.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clouds &amp;amp; Mountains
&lt;/h3&gt;

&lt;p&gt;5 cloud layers drift at different speeds (parallax). 3 mountain planes in the background provide depth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fog of War
&lt;/h3&gt;

&lt;p&gt;A radial gradient overlay around the player reveals a limited portion of the map. The edge blurs into darkness.&lt;/p&gt;

&lt;h3&gt;
  
  
  Floating Damage Text
&lt;/h3&gt;

&lt;p&gt;Numbers float upward with decreasing opacity over 1 second. Critical hits are larger and yellow.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Performance
&lt;/h2&gt;

&lt;p&gt;The game runs at 60fps on desktop and a stable 30-50fps on mid-range Android. Key techniques:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tile caching&lt;/strong&gt;: Each unique tile is drawn once to an offscreen canvas, then &lt;code&gt;drawImage&lt;/code&gt;-d to the main canvas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Camera culling&lt;/strong&gt;: Only tiles and entities within the viewport (±1 tile margin) are drawn&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Efficient gradients&lt;/strong&gt;: Gradients are created once per frame, not per tile&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimal GC pressure&lt;/strong&gt;: Object pools for particles and damage texts&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Build &amp;amp; Deployment
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build&lt;/span&gt;
node node_modules/esbuild/bin/esbuild src/web/main.ts &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bundle&lt;/span&gt; &lt;span class="nt"&gt;--minify&lt;/span&gt; &lt;span class="nt"&gt;--outfile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docs/bundle.js &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;esm &lt;span class="nt"&gt;--platform&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;browser

&lt;span class="c"&gt;# Deploy (GitHub Pages)&lt;/span&gt;
git add docs/ &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"build"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub Pages is configured to serve from the &lt;code&gt;docs/&lt;/code&gt; directory on the &lt;code&gt;main&lt;/code&gt; branch. Every push triggers an automatic redeploy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Canvas 2D is powerful&lt;/strong&gt; for vector-style games. You don't need WebGL for this art style.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Procedural generation&lt;/strong&gt; eliminates asset pipelines. Change a color, recompile, and every tile updates instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Touch controls as DOM&lt;/strong&gt; is much more reliable than canvas hit detection for mobile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web Audio API&lt;/strong&gt; can replace entire sound libraries for simple games.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; catches so many game logic bugs at compile time — worth the build step.&lt;/li&gt;
&lt;/ol&gt;




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

&lt;p&gt;Play now: &lt;a href="https://lazaramolina17-stack.github.io/rpg-game" rel="noopener noreferrer"&gt;lazaramolina17-stack.github.io/rpg-game&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source code: &lt;a href="https://github.com/lazaramolina17-stack/rpg-game" rel="noopener noreferrer"&gt;github.com/lazaramolina17-stack/rpg-game&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with TypeScript, Canvas 2D, Web Audio API, esbuild, and GitHub Pages.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>gamedev</category>
      <category>canvas</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
