<?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: Sam Ayoub</title>
    <description>The latest articles on DEV Community by Sam Ayoub (@sam_ayoub_f53707eb9f506b4).</description>
    <link>https://dev.to/sam_ayoub_f53707eb9f506b4</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%2F4137817%2F845bd8da-221f-4ae7-87ec-2af11020da20.png</url>
      <title>DEV Community: Sam Ayoub</title>
      <link>https://dev.to/sam_ayoub_f53707eb9f506b4</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sam_ayoub_f53707eb9f506b4"/>
    <language>en</language>
    <item>
      <title>Building Overheat: Overheat: Match 3 Blast — the match-3 puzzle where every combo heats the board</title>
      <dc:creator>Sam Ayoub</dc:creator>
      <pubDate>Tue, 22 Sep 2026 14:26:14 +0000</pubDate>
      <link>https://dev.to/sam_ayoub_f53707eb9f506b4/building-overheat-overheat-match-3-blast-the-match-3-puzzle-where-every-combo-heats-the-board-5505</link>
      <guid>https://dev.to/sam_ayoub_f53707eb9f506b4/building-overheat-overheat-match-3-blast-the-match-3-puzzle-where-every-combo-heats-the-board-5505</guid>
      <description>&lt;p&gt;Overheat is out now on Web, iOS, Android. This is how it got made: the decisions, the pipeline, and what I would do differently.&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%2Fp2c5l7giu1a757et8u3a.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%2Fp2c5l7giu1a757et8u3a.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;Overheat is the match-3 puzzle where every combo sets the board alight. Swap gem tiles, fill the Heat Meter with combos and specials, then press BURST to blow a cross through the board — for free, without spending a move. 400 levels across 20 worlds that each clear their own way, hot tiles, ice, coal and frozen boards, daily rewards, trophies and a weekly Heat League. See the board flat or tilted into 3D. Every colour is also a shape, so everyone can play. Free, family friendly, works offline. From level 30 the optional Overheat Club removes ads and adds lives, coins and exclusive themes — pay once and keep it forever, or monthly.&lt;/p&gt;

&lt;p&gt;Swap gem tiles, fill the Heat Meter with combos and specials, then press BURST to blow a cross through the board — for free, without spending a move. 400 levels across 20 worlds that each clear their own way, hot tiles, ice, coal and frozen&lt;/p&gt;

&lt;h2&gt;
  
  
  Archetype JSON
&lt;/h2&gt;

&lt;p&gt;One file per model; adding a model is a JSON edit, not a Blender session. Buildings:&lt;/p&gt;

&lt;p&gt;Vehicles use the same shape with base (car | plane | ship | train | bus | truck), scale, palette, cargo, coaches and boolean flags. Archetypes can also be synthesised from the content catalog (core/scripts/lib/), so a new entry in the content JSON gets a model without hand-authoring — the archetype file is then a build artifact you can still edit.&lt;/p&gt;

&lt;p&gt;Determinism is the contract: a variant is keyed (archetype, VARIANT), SEED = seed + VARIANT, and variant row 0 is all-defaults so the base model never moves. High-frequency archetypes (the ones the game repeats hundreds of times) get 8 variants; everything else 3 — spend payload where repetition is visible. Vary independent axes (palette wheel, wall material, roof shape, window rhythm, massing, mirrored plan) and make every wheel length coprime with the variant count, or all axes turn over together and eight variants read as one. The runtime needs no variant table: the model registry counts the ids that actually shipped and walks a variant index down to the highest present&lt;/p&gt;

&lt;h2&gt;
  
  
  Textures — baked in Blender, tiered per platform
&lt;/h2&gt;

&lt;p&gt;Models ship untextured; one atlas set per asset family ships beside them. core/scripts/blender/bake_textures.py builds 16 procedural node-tree materials per family on a 4×4 tile grid and runs two Cycles EMIT bakes (1 sample, no lights — a bake, not a render): base colour, and a packed data map. core/scripts/gen-textures.mjs then derives the tangent-space normal (from the baked height field) and the ORM (R occlusion / G roughness / B metalness / A emissive mask), and encodes one set per quality tier, capped at each tier's textureMax from config/platform-quality.json (high 2048 / medium 1024 / low 512):&lt;/p&gt;

&lt;p&gt;The client fetches exactly one tier (its own), chosen by the quality system — this is the other half of the platform-quality contract: the pipeline caps what each tier can even download.&lt;/p&gt;

&lt;p&gt;UV contract: faces carry TEXCOORD_0 = world-scaled tiling coordinates (metres per repeat, so brick courses align with floor lines) and TEXCOORD_1 = the tile origin, constant per face; the shader samples origin + fract(uv) * 0.25 with textureGrad so the tile wrap never picks a coarse mip. Only designated slots are tintable by the palette; everything else keeps its real albedo. The attributes survive weld/join/simplify/Draco, so all LODs share the atlas.&lt;/p&gt;

&lt;p&gt;KTX2 is a VRAM decision, not a download decision. Measured on an eleven-family set (4096 px albedo class):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;map: WebP download KTX2 download…&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Platform quirks the web code must absorb
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Concern: Web Shell&lt;/li&gt;
&lt;li&gt;Safe areas: CSS env(safe-area-inset-*) shell reports insets via bridge.info/app-info; injected as the same CSS variables, so one layout system covers notches and gesture bars&lt;/li&gt;
&lt;li&gt;Back button: Esc key hardware back → synthetic Esc + a bridge event; at root, second press within 2 s exits&lt;/li&gt;
&lt;li&gt;Audio unlock: no AudioContext before the first pointer/key gesture (autoplay contract) same rule, doubly important: a WebView that grabs the audio session at startup stops the player's podcast before the menu renders&lt;/li&gt;
&lt;li&gt;Keyboard: normal on-screen keyboard resizes the viewport — bottom-docked UI must key off visualViewport, and inputs must scroll into view&lt;/li&gt;
&lt;li&gt;Lifecycle: visibilitychange app-state events; on background keep the socket ~30 s then close; on active reconnect + refetch&lt;/li&gt;
&lt;li&gt;Sign-in: web identity providers provider JS SDKs are blocked inside WebViews — sign-in must go through the games bridge, with guest + recovery key (secure storage) as fallback&lt;/li&gt;
&lt;li&gt;Payments: web checkout redirect store billing via bridge; hide items with no store SKU mapping&lt;/li&gt;
&lt;li&gt;External links: target=_blank an openUrl op — navigation inside the WebView would trap the player&lt;/li&gt;
&lt;li&gt;Version floor: reload prompt store-listing link on the server's minimum-version kick&lt;/li&gt;
&lt;li&gt;Quality: full ladder from config/platform-quality.json platform default starts at medium; same governor&lt;/li&gt;
&lt;li&gt;Orientation: any unlocked; the…&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  CSP and frame ancestors
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Emit a real Content-Security-Policy on every response from the server: default-src 'self',
object-src 'none', base-uri 'self', frame-ancestors 'none', and a connect-src built from
the game/API/public origins plus their wss: forms and the exact payment/identity provider
domains in use. WASM decoders need 'wasm-unsafe-eval'; workers need worker-src 'self' blob:.&lt;/li&gt;
&lt;li&gt;CSP=off disables the header entirely — the documented escape hatch when the live client turns
out to load something the policy has not listed. Better a day without CSP than a broken game.&lt;/li&gt;
&lt;li&gt;FRAME_ANCESTORS= widens frame-ancestors (e.g. for an embedding canvas host) &lt;strong&gt;and
removes X-Frame-Options&lt;/strong&gt; — the two headers contradict each other and the older one wins in
some browsers, so they must be managed as one setting.&lt;/li&gt;
&lt;li&gt;Plus: X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, a
Permissions-Policy denying camera/microphone/geolocation. HSTS is the edge's&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Latest changes
&lt;/h2&gt;

&lt;p&gt;Changelog&lt;/p&gt;

&lt;p&gt;All notable changes to Overheat. Format: Keep a Changelog. Player-facing bullets are written so they can be lifted into store release notes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is next
&lt;/h2&gt;

&lt;p&gt;It is live at &lt;a href="https://overheat.reallexi.io" rel="noopener noreferrer"&gt;https://overheat.reallexi.io&lt;/a&gt;. If you have shipped something similar, I would like to hear what you would have cut.&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>gamedev</category>
      <category>mobile</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
