<?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: Eugene Kozlovsky</title>
    <description>The latest articles on DEV Community by Eugene Kozlovsky (@koztechie).</description>
    <link>https://dev.to/koztechie</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%2F3340118%2Fb7a95fb0-4366-48e6-a83d-3a589891c52e.jpg</url>
      <title>DEV Community: Eugene Kozlovsky</title>
      <link>https://dev.to/koztechie</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/koztechie"/>
    <language>en</language>
    <item>
      <title>Building NICHLYST: The Architecture of Afterlife — Designing Post-Final UI Decay in CSS</title>
      <dc:creator>Eugene Kozlovsky</dc:creator>
      <pubDate>Wed, 23 Sep 2026 22:35:52 +0000</pubDate>
      <link>https://dev.to/koztechie/building-nichlyst-the-architecture-of-afterlife-designing-post-final-ui-decay-in-css-1cd3</link>
      <guid>https://dev.to/koztechie/building-nichlyst-the-architecture-of-afterlife-designing-post-final-ui-decay-in-css-1cd3</guid>
      <description>&lt;p&gt;Most video games treat the user interface as immortal chrome. The HUD sits above the world, pristine and unaffected by whatever apocalypse unfolds beneath it. The interface is the one thing that never dies.&lt;/p&gt;

&lt;p&gt;NICHLYST rejects that premise entirely.&lt;/p&gt;

&lt;p&gt;In NICHLYST, you spend forty days cataloguing the final archive of a dead language in a damp basement. The interface is not a sterile heads-up display. It is paper. It is ink on aging fiber. Over forty days of neglect, humidity, and moral erosion, it rots.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Immortal UI Fallacy
&lt;/h2&gt;

&lt;p&gt;Every game maintains a quiet contract with the player: the interface is sacred ground. Even in survival horror, menus remain immaculate.&lt;/p&gt;

&lt;p&gt;NICHLYST breaks that contract. You are an archivist in a basement. The walls sweat. The paper records you handle are already decaying. Why should the interface be exempt from the same entropy that consumes everything else?&lt;/p&gt;

&lt;p&gt;By Day 16, when the Postscript phase begins and the player confronts the weight of their choices, the UI degrades. Not through dramatic effects, but through the slow processes that govern real material decay. Ink fades. Borders fray. Text drifts off its baseline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Eight Stages of Decomposition
&lt;/h2&gt;

&lt;p&gt;The decay system is organized into eight discrete levels, indexed 0 through 7. Each level corresponds to a range of game days and a specific phase of material deterioration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 0 (Days 1-15, Pristine):&lt;/strong&gt; Full ink contrast using &lt;code&gt;#EDE6D8&lt;/code&gt; on the near-black base. Solid 1px borders at &lt;code&gt;rgba(125, 114, 99, 0.3)&lt;/code&gt;. WCAG AAA contrast ratio of 8.4:1. The interface as the archivist first encounters it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 1 (Days 16-18, Subtle):&lt;/strong&gt; A slight desaturation filter (&lt;code&gt;saturate(0.95)&lt;/code&gt;) cools the palette. Secondary elements drop to 90% opacity. The change is barely perceptible, which is precisely the point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 2 (Days 19-21, Noticeable):&lt;/strong&gt; Desaturation deepens to &lt;code&gt;saturate(0.88)&lt;/code&gt;. A text-shadow appears on the visitor name, simulating ink bleed. Resource icons dim to 90%. Day counter and mood text fall to 85%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 3 (Days 22-24, Significant):&lt;/strong&gt; Saturation drops to &lt;code&gt;saturate(0.8)&lt;/code&gt;. A solid border appears on the screen container. Mood text receives a hairline blur (&lt;code&gt;filter: blur(0.3px)&lt;/code&gt;). Choice buttons maintain &lt;code&gt;opacity: 0.95&lt;/code&gt; and &lt;code&gt;filter: none&lt;/code&gt; -- interactive elements must never be degraded beyond usability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 4 (Days 25-28, Deep):&lt;/strong&gt; The Hidden Postscript begins, and with it paper grain. An SVG &lt;code&gt;feTurbulence&lt;/code&gt; filter is injected as a &lt;code&gt;::before&lt;/code&gt; pseudo-element, rendering fractal noise at &lt;code&gt;baseFrequency: 0.9&lt;/code&gt; with four octaves, desaturated to grayscale via &lt;code&gt;feColorMatrix&lt;/code&gt;, composited at 3% opacity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 5 (Days 29-32, Profound):&lt;/strong&gt; Saturation falls to &lt;code&gt;saturate(0.6)&lt;/code&gt;. The content column narrows to &lt;code&gt;max-width: 440px&lt;/code&gt;. Letter-spacing increases by 0.3px globally. Mood text shifts from opacity reduction to color substitution: &lt;code&gt;var(--c-3)&lt;/code&gt; instead of &lt;code&gt;var(--c-5)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 6 (Days 33-36, Ash):&lt;/strong&gt; Saturation drops to &lt;code&gt;saturate(0.45)&lt;/code&gt;. Background shifts from &lt;code&gt;var(--c-1)&lt;/code&gt; to &lt;code&gt;var(--c-2)&lt;/code&gt;. Visitor names shift to &lt;code&gt;var(--c-4)&lt;/code&gt;. Choice buttons gain a glitch overlay on hover: "ZHYVY" (LIVE) flickers via a stepped keyframe animation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 7 (Days 37-40, Dissolution):&lt;/strong&gt; Terminal afterlife rot. Saturation reaches &lt;code&gt;saturate(0.3)&lt;/code&gt;. Horizontal scan-lines overlay the viewport via &lt;code&gt;repeating-linear-gradient&lt;/code&gt; at 2% opacity. Screen borders pulse in an 8-second cycle. Choice buttons overlay "DYKHAI" (BREATHE) on hover. On Day 40, the interface reaches terminal archival collapse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Engineering Decay Inside an Immutable 5-Color Palette
&lt;/h2&gt;

&lt;p&gt;The hardest constraint was not the decay system itself. It was the color palette.&lt;/p&gt;

&lt;p&gt;NICHLYST enforces a strict 5-color palette at build time. The script &lt;code&gt;scripts/check_colors.sh&lt;/code&gt; scans every CSS, JS, and HTML file, extracts all hex color values, and compares them against a whitelist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#1A1C1C  #2D3131  #7D7263  #BCB2A1  #EDE6D8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any unauthorized color -- blood-red warning hues, rusty oranges, sepia browns -- triggers an immediate commit rejection. The palette is the game's visual identity. We cannot break it to make rot look more dramatic.&lt;/p&gt;

&lt;p&gt;So how do you simulate decomposition when you cannot add new colors? The answer is CSS math.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Opacity Reduction:&lt;/strong&gt; Early levels reduce opacity on secondary elements. Level 1 drops day counters to 90%. Level 3 drops mood text to 80%. This is safe because the underlying tokens already yield 12.5:1 contrast (WCAG AAA), so even at 80% opacity the effective contrast remains well above 4.5:1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Color-Shifting via Token Substitution:&lt;/strong&gt; At Level 5, instead of reducing opacity further, the system swaps token references. Mood text shifts from &lt;code&gt;var(--c-5)&lt;/code&gt; to &lt;code&gt;var(--c-3)&lt;/code&gt;. No new colors are introduced. The system reuses existing palette members to simulate ink dilution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dynamic Contrast Enforcement:&lt;/strong&gt; The &lt;code&gt;UIDecayManager&lt;/code&gt; includes a WCAG contrast guard that activates at Level 5 and above. It calculates effective luminance of desaturated text against the background:&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;textRgb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_hexToRgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#EDE6D8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// --c-5&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bgRgb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_hexToRgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#1A1C1C&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// --c-1&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;grayPoint&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;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mf"&gt;0.299&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;textRgb&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="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.587&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;textRgb&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="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.114&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;textRgb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&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;desatText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;textRgb&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;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;satFilter&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;grayPoint&lt;/span&gt; &lt;span class="o"&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;satFilter&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;ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_contrastRatio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;desatText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bgRgb&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the ratio drops below 4.5:1, a brightness boost is injected as &lt;code&gt;--nl-decay-text-safe&lt;/code&gt;, applied via &lt;code&gt;!important&lt;/code&gt; override on all text elements. The decay becomes aggressive. The text remains readable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SVG feTurbulence for Paper Grain:&lt;/strong&gt; At Level 4, fractal noise is injected as an inline SVG data URI in a &lt;code&gt;::before&lt;/code&gt; pseudo-element using &lt;code&gt;feTurbulence&lt;/code&gt; with &lt;code&gt;baseFrequency="0.9"&lt;/code&gt; and &lt;code&gt;numOctaves="4"&lt;/code&gt;. At Level 7, scan lines appear via &lt;code&gt;::after&lt;/code&gt; using &lt;code&gt;repeating-linear-gradient&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Letter-Spacing Drift:&lt;/strong&gt; Level 5 introduces &lt;code&gt;letter-spacing: 0.3px&lt;/code&gt; globally, simulating ink spreading on damp paper. The characters drift apart, as if the fibers holding them in place are relaxing.&lt;/p&gt;

&lt;p&gt;The complete CSS cascade for representative decay transitions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.decay-3&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;saturate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.decay-3&lt;/span&gt; &lt;span class="nc"&gt;.ds-screen&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;125&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;114&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.decay-3&lt;/span&gt; &lt;span class="nc"&gt;.ds-mood-text&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blur&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.3px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.decay-5&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;saturate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nl"&gt;letter-spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.3px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.decay-5&lt;/span&gt; &lt;span class="nc"&gt;.ds-mood-text&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--c-3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nl"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.decay-5&lt;/span&gt; &lt;span class="nc"&gt;.ds-day-counter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--c-3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.decay-7&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;saturate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--c-1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.decay-7&lt;/span&gt; &lt;span class="nc"&gt;.ds-screen&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;125&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;114&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;borderFlicker&lt;/span&gt; &lt;span class="m"&gt;8s&lt;/span&gt; &lt;span class="n"&gt;ease-in-out&lt;/span&gt; &lt;span class="n"&gt;infinite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.decay-7&lt;/span&gt; &lt;span class="nc"&gt;.ds-body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;text-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;237&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;230&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;216&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.15&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 property references existing palette tokens. The decay is real, but the palette is untouched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mitigating Mobile WebView GPU Choke
&lt;/h2&gt;

&lt;p&gt;The original implementation chained CSS filters: &lt;code&gt;filter: blur(1px) drop-shadow(0 0 4px rgba(0,0,0,0.3))&lt;/code&gt;. On desktop, trivial. On a budget Android WebView, catastrophic.&lt;/p&gt;

&lt;p&gt;The Redmi Note 9 Pro thermal-throttles within 90 seconds when chaining multiple CSS filter primitives. The GPU cannot rasterize &lt;code&gt;blur()&lt;/code&gt; and &lt;code&gt;drop-shadow()&lt;/code&gt; simultaneously at 60fps on a Snapdragon 720G. The frame rate collapses to 15fps.&lt;/p&gt;

&lt;p&gt;The architectural trade-off: replace dynamic CSS filters with static SVG noise tiles and pre-calculated opacity steps.&lt;/p&gt;

&lt;p&gt;The paper grain at Level 4 is an inline SVG data URI rendered once as a &lt;code&gt;::before&lt;/code&gt; pseudo-element with &lt;code&gt;pointer-events: none&lt;/code&gt;. The browser composites it as a single texture layer. Scan lines at Level 7 use &lt;code&gt;repeating-linear-gradient&lt;/code&gt;, a static paint operation the GPU caches immediately.&lt;/p&gt;

&lt;p&gt;On mobile, the &lt;code&gt;@media (max-width: 480px)&lt;/code&gt; block reduces noise overlay opacity from 3% to 2% and trims letter-spacing from 0.3px to 0.2px, preventing the compositor from accumulating state across frames.&lt;/p&gt;

&lt;p&gt;Result: 60fps locked on the Redmi Note 9 Pro, no thermal throttling on an AMD A4 laptop. The decay feels heavy. The rendering is lightweight.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Somatic Impact of Visual Erosion
&lt;/h2&gt;

&lt;p&gt;The decay system is not decorative. It is narrative machinery.&lt;/p&gt;

&lt;p&gt;When the "CONTINUE" button on Day 35 overlays "BREATHE" in a flickering glitch, that is not visual polish. That is the interface speaking in a voice that is no longer the archivist's own. The paper is speaking. The archive is speaking. And what it says is: keep going, even though everything is dissolving.&lt;/p&gt;

&lt;p&gt;Watching resource bars fade toward illegibility creates psychological tension faster than exposition. The player does not need to be told their situation is deteriorating. They can see it in the menu. The ink is running. The borders are fraying. The interface they trusted is becoming unreliable.&lt;/p&gt;

&lt;p&gt;The UI is not separate from the story. It is a character in the story. And like every character in NICHLYST, it is subject to the same entropy, the same forty days, the same inevitable collapse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Countdown to Production
&lt;/h2&gt;

&lt;p&gt;With the September 30 Devpost deadline approaching, the decay system represents the final major technical milestone for the visual layer. The 14-day Closed Testing window on Google Play is underway, and the CSS architecture described here runs in production builds.&lt;/p&gt;

&lt;p&gt;Every decay level has been tested against the 5-color palette constraint. Every opacity value verified against WCAG AA 4.5:1 contrast minimums. Every animation respects &lt;code&gt;prefers-reduced-motion&lt;/code&gt;. The system is not a prototype. It is a production-ready framework built on CSS custom properties, SVG filters, and mathematical constraints.&lt;/p&gt;

</description>
      <category>shipaton</category>
      <category>buildinpublic</category>
      <category>gamedev</category>
      <category>css</category>
    </item>
    <item>
      <title>Building NICHLYST: Eight Waves, One Canon — How 40 Days of Script Survived a Strict State Machine</title>
      <dc:creator>Eugene Kozlovsky</dc:creator>
      <pubDate>Mon, 21 Sep 2026 19:55:09 +0000</pubDate>
      <link>https://dev.to/koztechie/building-nichlyst-eight-waves-one-canon-how-40-days-of-script-survived-a-strict-state-machine-4gn3</link>
      <guid>https://dev.to/koztechie/building-nichlyst-eight-waves-one-canon-how-40-days-of-script-survived-a-strict-state-machine-4gn3</guid>
      <description>&lt;p&gt;It is Monday, September 21, 2026. Day 11 of the 14-day Google Play closed testing anabiosis. The clock runs on Google's terms, not mine. Outside the window in Kyiv, the air siren has just ended its third cycle of the morning. Inside, on an aging AMD A4 laptop with 4 GB of RAM, the terminal holds its breath. The bundle compiled. The state machine holds. There is nothing to add today, and that restraint is the only reason the system still works.&lt;/p&gt;

&lt;p&gt;This devlog is about the architecture that kept NICHLYST from consuming itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Branching Trap
&lt;/h2&gt;

&lt;p&gt;Narrative games with branching paths face a problem that most builders underestimate until it is too late. You write a day. You give the player two choices. Each choice sets a flag. The next day reads that flag and branches again. After ten days, you have 1,024 possible states. After twenty days, the number exceeds one million. After forty days with 130 flags, six moral axes, and 33 distinct endings, you are no longer writing a story. You are constructing a state-space explosion that will crush any naive implementation.&lt;/p&gt;

&lt;p&gt;The naive implementation is always the same: a plain JavaScript object mutated from everywhere. A &lt;code&gt;gameState&lt;/code&gt; property on a global &lt;code&gt;window&lt;/code&gt; object. Flags set with &lt;code&gt;gameState.flags.whatever = true&lt;/code&gt;. Resources modified with &lt;code&gt;gameState.endurance += 10&lt;/code&gt;. Every UI component reads and writes to the same object. The first time something goes wrong, you spend four hours chasing a race condition between the save system and the choice renderer. The second time, you lose a player's progression. The third time, you delete the project.&lt;/p&gt;

&lt;p&gt;NICHLYST has 40 days, 33 endings, 130 boolean flags, and six moral axes (truth, mercy, preservation, complicity, memory, survival). The game data alone weighs 230 kilobytes of JSON. The exponential explosion of possible states is not theoretical. It is the daily engineering reality.&lt;/p&gt;

&lt;p&gt;The solution was not cleverer writing. It was a strict, ruthless state machine.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Eight Waves of Canon
&lt;/h2&gt;

&lt;p&gt;Before describing the machine, it helps to understand what the machine protects. The 40 days are not a flat sequence. They are partitioned into eight thematic waves, each with its own mechanical identity and narrative contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wave 1: The Cold Awakening (Days 1-3).&lt;/strong&gt; The bunker. The first visitor. The player learns to listen, to record, to choose between silence and speech. Mechanical introduction: resource gauges, flag system, the weight of early decisions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wave 2: The First Ruptures (Days 4-7).&lt;/strong&gt; The network stirs. Taras brings diesel. Khoma brings bread. Solomiia brings chalk. Each visitor tests the player's instinct: protect the archive or protect the person. Trust begins to fracture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wave 3: The Tightening Net (Days 8-12).&lt;/strong&gt; Surveillance escalates. The Directorate tightens its grip. Choices become binary: comply or resist, at increasing cost. The player's endurance drains faster. Hope flickers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wave 4: The Great Raid (Days 13-15).&lt;/strong&gt; The climax of the primary arc. The raid, the evacuation, the silence. The player faces the transmission choice: broadcast the archive into the open air, or seal it in an iron crypt. The 33 endings branch from this threshold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wave 5: The Silence of Ash (Days 16-20).&lt;/strong&gt; The postscript begins. The world above has stopped. The player sorts through burnt paper, frozen salt, severed telephone wires. The choices shrink to single sentences. Frost on iron. Chalk dust. An empty chair.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wave 6: The Postscript Residue (Days 21-23).&lt;/strong&gt; Phantom flora. Paper mice. Documents that curl and mutate in the damp. The archive begins to develop a life of its own, independent of the archivist's intent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wave 7: The Hallucinatory Drift (Days 25-33).&lt;/strong&gt; The hidden postscript. Reality bends. Memory echoes surface. The player navigates a space between remembering and forgetting, where the moral ledger exerts its final pressure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wave 8: The Afterlife Reckoning (Days 34-40).&lt;/strong&gt; The archive afterlife. What survives. What decomposes. What the world remembers, and what it does not. The final endings: transmission success, transmission failure, burial preserved, burial lost, mercy destruction, haunted destruction, echo, ashes.&lt;/p&gt;

&lt;p&gt;Each wave has a distinct emotional register and mechanical behavior. The state machine does not care about themes. It cares about invariants. The eight waves exist so that the machine always knows which rules apply.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Iron Container: Engineering the State Machine
&lt;/h2&gt;

&lt;p&gt;The state manager lives in &lt;code&gt;www/js/state_manager.js&lt;/code&gt;. It is 777 lines of JavaScript that enforce a single principle: no external code may mutate game state directly.&lt;/p&gt;

&lt;p&gt;Here is the core of 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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StateManager&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;gameData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;archive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;currentDayId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;gamePhase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;main&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;moralLedger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;truth&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;mercy&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;preservation&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;complicity&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;memory&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;survival&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;get&lt;/span&gt; &lt;span class="nf"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&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="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&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="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;flags&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;moralLedger&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&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="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;moralLedger&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;modifyResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delta&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;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;gameData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;global_resources&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&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;config&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start&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="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&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="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;min&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="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delta&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;autoSave&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;getStateSnapshot&lt;/span&gt;&lt;span class="p"&gt;()&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="na"&gt;resources&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="nx"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;flags&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="nx"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;archive&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="nx"&gt;archive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;currentDayId&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="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;currentDayId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;moralLedger&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="nx"&gt;moralLedger&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;Every getter returns a frozen copy. Every mutation goes through &lt;code&gt;modifyResource&lt;/code&gt; or &lt;code&gt;applyDeltas&lt;/code&gt;, both of which clamp values between &lt;code&gt;config.min&lt;/code&gt; and &lt;code&gt;config.max&lt;/code&gt; (0 and 100 for most resources). The &lt;code&gt;#private&lt;/code&gt; fields mean that if some UI component tries &lt;code&gt;state.resources.endurance = 999&lt;/code&gt;, it simply fails. The property does not exist on the frozen copy. The internal &lt;code&gt;#resources&lt;/code&gt; object is unreachable from outside the class.&lt;/p&gt;

&lt;p&gt;This is not fancy engineering. It is defensive engineering. The distinction matters when you are building on a laptop that freezes if you open too many Chrome tabs.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;applyChoice&lt;/code&gt; method is the central pipeline. A choice arrives. Base deltas are applied and clamped. Conditional effects fire if the relevant flag is set. Flags are written. The moral ledger updates. Fatigue accumulates. Attention restores. Testimony corruption tracks. Weather effects apply. Surveillance stress penalizes endurance above 70. Codex fragments unlock. Memory echoes surface. Delayed notifications schedule. Screen readers announce the change log. At the end of the pipeline, &lt;code&gt;getStateSnapshot()&lt;/code&gt; hands the UI a frozen, immutable snapshot. The UI never touches the internal state. It receives a photograph, not a live feed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Antifragility in Practice
&lt;/h2&gt;

&lt;p&gt;The rule of graceful degradation was simple: if a system is missing, skip it. If a flag is undefined, treat it as false. If a resource key does not exist in the schema, ignore the delta silently. If the save file is corrupted, clamp resources to valid bounds and set &lt;code&gt;#tampered = true&lt;/code&gt; rather than crashing.&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="nf"&gt;modifyResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delta&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;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;gameData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;global_resources&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&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;config&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;// Unknown key: silent skip&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start&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="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&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="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;min&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="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delta&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;autoSave&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;The &lt;code&gt;restoreFromData&lt;/code&gt; method validates every field on load. Missing fields get defaults. Out-of-range values get clamped. The &lt;code&gt;validateResources&lt;/code&gt; helper returns a &lt;code&gt;corrected&lt;/code&gt; object alongside a &lt;code&gt;valid&lt;/code&gt; boolean. If the save was tampered with (detected via HMAC-SHA256 signature in SaveManager), the system logs a warning to Sentry, sets the tamper flag, and continues. The player never sees the corruption. The game continues to function. This is antifragility: the system does not merely survive disorder, it absorbs disorder and carries on.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Final Countdown
&lt;/h2&gt;

&lt;p&gt;September 24 approaches. The 14-day closed testing window ends. Nine days remain until the final Devpost submission. The state machine holds. The 40 days compile into a single 230-kilobyte JSON bundle. The 33 endings remain intact. The 130 flags initialize cleanly on every fresh session.&lt;/p&gt;

&lt;p&gt;There is a peculiar discipline in building something that must not change. The urge to add one more feature, one more flag, one more ending is constant. The correct response is always the same: no. The architecture is the architecture. The eight waves are the eight waves. The iron container holds what it was built to hold.&lt;/p&gt;

&lt;p&gt;Outside, the sirens will sound again. The AMD A4 will spin its fan. The terminal will display its output. The state machine will clamp its values between 0 and 100, freeze its snapshots, and hand them to a UI that never touches the source. This is how a solo builder in wartime Kyiv keeps forty days of branching narrative from collapsing into chaos.&lt;/p&gt;

&lt;p&gt;One wave at a time. One invariant at a time. One frozen snapshot at a time.&lt;/p&gt;

</description>
      <category>shipaton</category>
      <category>buildinpublic</category>
      <category>gamedev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building NICHLYST: The 14-Day Clock, the "Web App" Illusion, and Hardening the Archive</title>
      <dc:creator>Eugene Kozlovsky</dc:creator>
      <pubDate>Sun, 13 Sep 2026 14:04:23 +0000</pubDate>
      <link>https://dev.to/koztechie/building-nichlyst-the-14-day-clock-the-web-app-illusion-and-hardening-the-archive-4a6c</link>
      <guid>https://dev.to/koztechie/building-nichlyst-the-14-day-clock-the-web-app-illusion-and-hardening-the-archive-4a6c</guid>
      <description>&lt;p&gt;On September 10th, at 10:19 AM, the system finally turned green.&lt;/p&gt;

&lt;h2&gt;
  
  
  12 Testers Google Play
&lt;/h2&gt;

&lt;p&gt;Twelve confirmed, opt-in testers. The threshold is met.&lt;/p&gt;

&lt;p&gt;The 14-day Google Play closed testing clock is officially locked.&lt;/p&gt;

&lt;p&gt;For the next two weeks, NICHLYST sits in a state of anabiosis. My role has shifted from "Builder" to "Watcher." But as any builder who has ever shipped knows, the moment you stop building is the moment you start seeing the cracks.&lt;/p&gt;

&lt;p&gt;This week was the week of the "Web App" realization—a harsh feedback loop that transformed the project from a browser experiment into a tactile, native experience. When debating the native app vs web app architecture early on, I chose the web for speed. But the reality of shipping a Capacitor mobile app forced me to deeply reconsider the native app web app comparison. Under the hood, it relies on a native app WebView, but the user’s hand is the ultimate critic.&lt;/p&gt;




&lt;h3&gt;
  
  
  The "Web App" Illusion
&lt;/h3&gt;

&lt;p&gt;A few days ago, I sat with the build on my Redmi Note 9 Pro. I was looking for glitches, for paywall bugs, for layout breaks. Instead, I saw a reflection of my own haste. Good mobile app UX design demands more than just functional code; it demands physical empathy.&lt;/p&gt;

&lt;p&gt;The game felt like a website wrapped in a thin layer of Android glue. The scrolling lacked inertia, the buttons felt like generic text inputs, and the total absence of tactile feedback made the bunker feel... hollow. In a game about the "slow weight of testimony," the interface felt weightless.&lt;/p&gt;

&lt;p&gt;When you build with Capacitor, the browser is your engine, but the user’s hand is the ultimate critic. If the scrolling doesn't have momentum, or if the system doesn't acknowledge a tap with a haptic tick, the user subconsciously knows they are looking at a document, not a world.&lt;/p&gt;

&lt;p&gt;I spent 48 hours purging "web-isms":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Kinetic Scrolling &amp;amp; Notch Awareness:&lt;/strong&gt; I replaced browser-default scrolling with CSS momentum and reclaimed the pixels stolen by the Android camera cutout. The UI is no longer "pushed" by the hardware; it flows around it.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Preventing overscroll bounce on body for native feel */&lt;/span&gt;
&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;overscroll-behavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;-webkit-user-select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;touch-action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;manipulation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Kinetic scrolling on individual scroll containers */&lt;/span&gt;
&lt;span class="nc"&gt;.scroll-container&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;-webkit-overflow-scrolling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Safe area inset padding for notch awareness (injected via NativeBridge) */&lt;/span&gt;
&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--nl-safe-area-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;safe-area-inset-top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--nl-safe-area-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;safe-area-inset-bottom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0px&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;h2&gt;
  
  
  Capacitor Haptics
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tactile Haptics: When the Interface Feels Real&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tactile Feedback:&lt;/strong&gt; If you are looking for a quick Capacitor plugin tutorial, the setup is straightforward. I integrated the &lt;code&gt;@capacitor/haptics&lt;/code&gt; plugin. Every choice button now triggers a light pulse; every raid event or consequence trigger results in a distinct, medium haptic impact.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It is a small set of changes, but the difference between "viewing a site" and "holding an archive" is exactly the sum of these details.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mobile Game Security
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Security as a Feature: Hardening the Archive&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the first devlog, I wrote about an antifragile architecture. But when evaluating mobile game security, antifragility means nothing if the system is insecure. If your monetization is a bypassable &lt;code&gt;localStorage&lt;/code&gt; flag, you haven't built a business; you've built a suggestion.&lt;/p&gt;

&lt;p&gt;I audited the security of the payment flow this week. The results were sobering.&lt;/p&gt;

&lt;p&gt;I had hardcoded the RevenueCat public keys and relied on weak integrity checks for save files. During the Shipaton, your work is not just being reviewed by judges; it is being probed by the entire community.&lt;/p&gt;

&lt;p&gt;I implemented two critical hardening layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;djb2 Checksum Integrity:&lt;/strong&gt; Every save file now contains a djb2 checksum. If a user tries to modify their &lt;code&gt;memory_integrity&lt;/code&gt; or &lt;code&gt;guilt&lt;/code&gt; stats in the JSON, the checksum mismatch triggers a safe recovery protocol (backup restore) rather than a game-breaking crash.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime Security:&lt;/strong&gt; I stripped out the placeholder keys and implemented a strict build-time injection via &lt;code&gt;sed&lt;/code&gt; in the Codemagic CI pipeline. The debug harness now checks for a native platform and a &lt;code&gt;__DEV_MODE__&lt;/code&gt; flag before initializing. If you are running a production build, the debug triggers are inert—they exist in the source but are gated by runtime checks that never pass on a signed APK.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Shipaton is about shipping, but it is also about shipping &lt;em&gt;securely&lt;/em&gt;. If the paywall can be bypassed by opening the Chrome DevTools, it isn't "shipped," it's "leaked."&lt;/p&gt;




&lt;h3&gt;
  
  
  The 14-Day Clock and the Silence
&lt;/h3&gt;

&lt;p&gt;The silence of the Google Play closed testing period is psychological warfare.&lt;/p&gt;

&lt;p&gt;I have 15 testers now, well above the minimum Google Play testing requirements. I am waiting for the algorithm to tick over. In the meantime, I have stopped trying to "play" the game. I have stopped trying to find new features. The roadmap for Days 16 through 40 is written. The narrative is heavy. The engine is stable.&lt;/p&gt;

&lt;p&gt;I am not optimizing for "more" anymore. I am optimizing for "deep."&lt;/p&gt;

&lt;p&gt;For the next two weeks, the only work that matters is the polish of the remaining chapters. I am refining the weight of the testimonies—making sure that when a courier enters your bunker, you feel the cold air behind him.&lt;/p&gt;

&lt;p&gt;My hands are steadier now. The medication adjustment is helping, and the rhythm of the building process has become a cage I am happy to live in. We don't have control over the city outside the window, but we have control over the bunker inside the phone.&lt;/p&gt;

&lt;p&gt;The Archive demands a key. We are just waiting for the door to open.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow this devlog for the final sprint of the #BuildInPublic journey toward the RevenueCat Shipaton 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>shipaton</category>
      <category>buildinpublic</category>
      <category>gamedev</category>
      <category>capacitor</category>
    </item>
    <item>
      <title>Building NICHLYST: Explosions, Medication, and Rewriting 15 Days as a Novel You Can Fail</title>
      <dc:creator>Eugene Kozlovsky</dc:creator>
      <pubDate>Sun, 06 Sep 2026 07:58:32 +0000</pubDate>
      <link>https://dev.to/koztechie/building-nichlyst-explosions-medication-and-rewriting-15-days-as-a-novel-you-can-fail-583g</link>
      <guid>https://dev.to/koztechie/building-nichlyst-explosions-medication-and-rewriting-15-days-as-a-novel-you-can-fail-583g</guid>
      <description>&lt;p&gt;On Saturday evening, August 29th, I was lying on my sofa after eating way too much watermelon. I was taking a short break from writing the narrative core of my game, NICHLYST, pushing the limits of narrative game design for the RevenueCat Shipaton 2026.&lt;/p&gt;

&lt;p&gt;Then the shockwave hit.&lt;/p&gt;

&lt;p&gt;It sounded like heavy crushed stone slamming against sheet iron. The protective netting we installed to keep our cat from falling out of the window was blown a meter across the balcony to the door. I threw on my sneakers, grabbed my walking cane, tossed a piece of chewing gum in my mouth, and quickly messaged my family that the blast was so loud I nearly crapped my pants. Then I ran downstairs to see if our building was hit and if anyone needed help.&lt;/p&gt;

&lt;p&gt;It turned out to be falling debris from a downed "Shahed" attack drone about 300 meters away. If the drone itself had hit the building, the consequences would have been much worse, and the windows in my apartment would have undoubtedly shattered.&lt;/p&gt;

&lt;p&gt;When the initial adrenaline faded, my hands started shaking uncontrollably. You do not design survival mechanics when physical survival is being decided 300 meters from your desk. You just shake. I took a tranquilizer—the kind I keep strictly for situations when I feel a panic attack imminent or during severe stress—went back up to my apartment, sat down at my old AMD A4 PC, and opened VS Code.&lt;/p&gt;

&lt;p&gt;On Wednesday, September 2nd, I had my regular online appointment with my psychiatrist. The verdict was recorded clinically: &lt;em&gt;"State without significant changes. Changing Sertraline to Fluoxetine."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;But the code has to ship. The 14-day Google Play closed testing rule does not care about my neurochemistry or the craters in my neighborhood.&lt;/p&gt;

&lt;p&gt;Here is how the architecture and narrative of NICHLYST evolved this week through the fog of war, community feedback, and medication changes.&lt;/p&gt;




&lt;h3&gt;
  
  
  Filling the Void: Writing a Novel You Can Fail
&lt;/h3&gt;

&lt;p&gt;In my last devlog, I shared the architectural shift of throwing away the 1-sentence script and refactoring the UI to support 4-phase literary chapters (Awakening → Approach → Encounter → Dilemma).&lt;/p&gt;

&lt;p&gt;But building the container is easy. Filling it with prose that justifies a $4.99 premium unlock—the entire indie game monetization strategy for NICHLYST—is brutal.&lt;/p&gt;

&lt;p&gt;This entire weekend was dedicated to writing the Prologue and Acts I through IV (Days 1–15). Because this is a game without 3D models or combat mechanics, the pacing has to rely entirely on syntax.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On &lt;strong&gt;Day 12 (The Raid)&lt;/strong&gt;, I wrote the text using fragmented, staccato sentences to simulate a panic-induced adrenaline spike: &lt;em&gt;"Impact. The concrete shudders. Dust. Tactical flashlights slice the darkness like strobes."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;On &lt;strong&gt;Day 13 (The Aftermath)&lt;/strong&gt;, the syntax completely changes. The sentences become long, heavy, and meditative, reflecting absolute, deafening silence after the violence has passed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The RevenueCat Paywall Threshold
&lt;/h3&gt;

&lt;p&gt;The monetization flow was also deeply integrated into this narrative pacing. The $4.99 paywall now appears precisely at the end of Day 3, not as a jarring commercial banner, but as an in-lore narrative threshold: &lt;em&gt;"The Archive demands a key. Without a key, the blast door remains shut."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It’s not an interruption; it’s a pause before the descent.&lt;/p&gt;




&lt;h3&gt;
  
  
  Diegetic Accessibility: Subtitles That Whisper
&lt;/h3&gt;

&lt;p&gt;A brilliant piece of feedback came from a community discussion on accessibility (a11y). Someone asked: &lt;em&gt;How do deaf players, or people playing on mobile with the sound muted, experience an atmospheric audio-horror game?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My initial thought was to add generic tags like &lt;code&gt;[wind blows]&lt;/code&gt; or &lt;code&gt;[silence]&lt;/code&gt;. But that breaks the magic realism of the world.&lt;/p&gt;

&lt;p&gt;Instead, I built a diegetic subtitle engine in &lt;code&gt;AudioManager.js&lt;/code&gt; using the aria-live polite pattern for screen readers. The subtitles do not describe the sound; they describe the psychological weight of the room.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instead of &lt;code&gt;[silence]&lt;/code&gt;, the subtitle reads: &lt;em&gt;"[The room holds its breath and quietly laughs]"&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Instead of &lt;code&gt;[distant noise]&lt;/code&gt;, it reads: &lt;em&gt;"[A fragile violin is choked by the distant roar of turbines]"&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- AudioManager.js: Diegetic subtitle container --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;
  &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"diegetic-subtitles"&lt;/span&gt;
  &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;
  &lt;span class="na"&gt;aria-live=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;
  &lt;span class="na"&gt;aria-atomic=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;
  &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"whisper-layer"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the Archivist's mental state is compromised (e.g., the &lt;code&gt;codeine_faith&lt;/code&gt; state where clarity is artificially high but endurance is dead), the JavaScript dynamically stretches the typography of the subtitles, injecting slow ellipses and spaced letters (&lt;code&gt;"...k-n-o-c-k... a-t... t-h-e... d-o-o-r..."&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Accessibility became a core narrative mechanic.&lt;/p&gt;




&lt;h3&gt;
  
  
  Somatic UI: When the Interface Feels Pain
&lt;/h3&gt;

&lt;p&gt;To further separate the game from mere "stat management," I implemented somatic micro-gestures directly into &lt;code&gt;DayScreen.js&lt;/code&gt; and the CSS.&lt;/p&gt;

&lt;p&gt;If your &lt;code&gt;endurance&lt;/code&gt; drops below 10, the choice buttons do not appear. You are physically too weak to make a decision. Instead, a single, violently trembling button renders: &lt;strong&gt;[Bandage hands to stop them trembling...]&lt;/strong&gt;. You have to click it, watch the 1-second CSS animation stabilize, and only then do the real narrative choices reveal themselves.&lt;/p&gt;

&lt;p&gt;If your &lt;code&gt;guilt&lt;/code&gt; exceeds 80, the morning rendering of the day screen is accompanied by a 1-second overlay of muddy rust-sepia (&lt;code&gt;.ds-pain-sun&lt;/code&gt;). The interface itself degrades with your psyche.&lt;/p&gt;




&lt;h3&gt;
  
  
  Antifragility: Breaking the JSON Monolith
&lt;/h3&gt;

&lt;p&gt;On the engineering side, I realized I had built a massive single point of failure.&lt;/p&gt;

&lt;p&gt;NICHLYST runs entirely on a master file: &lt;code&gt;game_data.json&lt;/code&gt;. It holds 40 days, 31 endings, all the codex fragments, and 116 persistent flags. It was over 4,000 lines long. One missing comma on Day 39, and &lt;code&gt;JSON.parse&lt;/code&gt; would fail on boot, crashing the game on Day 1.&lt;/p&gt;

&lt;p&gt;That is fragile.&lt;/p&gt;

&lt;p&gt;So, I wrote a Node.js workflow to decouple the data. I split the narrative into modular files: &lt;code&gt;prologue.json&lt;/code&gt;, &lt;code&gt;act_1.json&lt;/code&gt;, &lt;code&gt;postscript.json&lt;/code&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="c1"&gt;// A fragment of the bundler script that ensures dual redundancy&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;manifest&lt;/span&gt; &lt;span class="o"&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;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;www/data/manifest.json&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;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// The script compiles the isolated acts back into the master file at build time.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, the engine's loader attempts to fetch the modular manifest first. If a network request drops or a module is corrupted, it gracefully falls back to the bundled monolith—a core principle of graceful degradation JavaScript in production environments.&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;// manifest-loader.js: Promise.allSettled pattern for module resilience&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;moduleUrls&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;www/data/prologue.json&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;www/data/act_1.json&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;www/data/act_2.json&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;www/data/postscript.json&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;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allSettled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;moduleUrls&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;url&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&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="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="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loadedModules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fulfilled&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="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&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="c1"&gt;// Fallback to bundled monolith if critical modules failed&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;loadedModules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;moduleUrls&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="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;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Falling back to bundled game_data.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;www/data/game_data.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;The game survives.&lt;/p&gt;




&lt;h2&gt;
  
  
  Google Play Closed Testing: The 14-day Clock is Locked
&lt;/h2&gt;

&lt;p&gt;I finally completed the formal verification of my Google Play Developer account.&lt;/p&gt;

&lt;p&gt;The verdict: &lt;strong&gt;The 14-day closed testing rule is mandatory.&lt;/strong&gt; There is no bypass.&lt;/p&gt;

&lt;p&gt;To meet the Shipaton deadline on September 28th, I must launch the closed test track no later than Tuesday, September 8th. The timer does not start when you upload the APK. It starts when the testers physically click &lt;em&gt;Opt-In&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Google Play 12 Testers Requirement
&lt;/h3&gt;

&lt;p&gt;The good news? The Shipaton organizers recently confirmed that the threshold to pass isn't 20 testers—it's 12+ active opt-ins. I already have 15 volunteers from the community. My task for the next 72 hours is to finish writing the Postscript chapters, integrate Sentry for error tracking, and hit the "Start Test" button.&lt;/p&gt;

&lt;p&gt;My hands still shake sometimes. The explosions haven't stopped. The medication is adjusting. But the engine is stable, the narrative is heavy, and the architecture is antifragile.&lt;/p&gt;

&lt;p&gt;I'll see you in the Archive.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow this devlog for the final sprint of the #BuildInPublic journey toward the RevenueCat Shipaton 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>shipaton</category>
      <category>gamedev</category>
      <category>indiedev</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Building NICHLYST: How to Code a Survival Engine When You Are Failing to Survive</title>
      <dc:creator>Eugene Kozlovsky</dc:creator>
      <pubDate>Sat, 29 Aug 2026 15:58:32 +0000</pubDate>
      <link>https://dev.to/koztechie/building-nichlyst-how-to-code-a-survival-engine-when-you-are-failing-to-survive-54nl</link>
      <guid>https://dev.to/koztechie/building-nichlyst-how-to-code-a-survival-engine-when-you-are-failing-to-survive-54nl</guid>
      <description>&lt;h1&gt;
  
  
  Building NICHLYST: How to Code a Survival Engine When You Are Failing to Survive
&lt;/h1&gt;

&lt;p&gt;I track everything. It is an occupational habit of a systems architect. You cannot fix what you do not measure — not a failing build, not a leaking color, and not developer burnout. And if you are reading this while grinding through the RevenueCat Shipaton 2026 yourself, you already know that the hardest metric to log honestly is your own state.&lt;/p&gt;

&lt;p&gt;So, let me share some metrics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clinical Baseline: A PHQ-9 Depression Score of 21
&lt;/h3&gt;

&lt;p&gt;On May 11, 2026, my clinical assessment scores were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PHQ-9 (Depression): 21.&lt;/strong&gt; Severe. Immediate professional intervention required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GAD-7 (Anxiety): 11.&lt;/strong&gt; Moderate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On August 22, 2026, in the middle of the RevenueCat Shipaton, I took the assessment again.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PHQ-9: 21.&lt;/strong&gt; No improvement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GAD-7: 16.&lt;/strong&gt; High anxiety. Daily functioning severely impaired.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have never read a GAD-7 anxiety assessment, 16 sits deep in the high-anxiety band — the zone where "daily functioning severely impaired" stops being a clinical phrase and becomes your actual schedule.&lt;/p&gt;

&lt;p&gt;While I was typing the very first lines of this post, a massive explosion went off, loud enough to make my ears pop. About an hour later, the local news feeds brought the context: an attack drone had been shot down over a park roughly two and a half kilometers from my house. According to the updates, the falling debris killed a two-year-old child and injured two adults.&lt;/p&gt;

&lt;p&gt;I am developing a narrative game about survival, the fragility of life, and human behavior under immense pressure. But here, in Kyiv, these are not abstract game mechanics or dramatic tropes to be monetized. They are the immediate, absurd, and brutal reality outside my window.&lt;/p&gt;

&lt;p&gt;I am exhausted. The clinical scores haven't moved in months. I sleep in the middle of the day because my nervous system simply shuts down. I am looking at this hackathon as a final, desperate push to build something sustainable.&lt;/p&gt;

&lt;p&gt;But here is the thing about the antifragile development plan I wrote about in my first devlog: &lt;strong&gt;it does not require me to be okay.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The code does not care about my PHQ-9 score. The system is designed to absorb the collapse of its creator and keep moving forward.&lt;/p&gt;

&lt;p&gt;Here is exactly how NICHLYST advanced technically and narratively over the last grueling week, while its developer was barely holding on.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Git Gatekeeper: A Git Pre-commit Hook Enforcing a 5-color Dictatorship
&lt;/h2&gt;

&lt;p&gt;NICHLYST has a strict, uncompromising visual identity. It is built exclusively on a &lt;strong&gt;limited color palette&lt;/strong&gt; of five: &lt;code&gt;#232626&lt;/code&gt; (Pitch Dark), &lt;code&gt;#3E403F&lt;/code&gt; (Slate), &lt;code&gt;#736758&lt;/code&gt; (Sepia), &lt;code&gt;#A69B8D&lt;/code&gt; (Ash), and &lt;code&gt;#BFB3A4&lt;/code&gt; (Bone).&lt;/p&gt;

&lt;p&gt;I spent hours auditing the entire codebase via terminal (&lt;code&gt;grep -rnE "#([0-9a-fA-F]{3,8})\b"&lt;/code&gt;) to eradicate every stray &lt;code&gt;#FFFFFF&lt;/code&gt; or &lt;code&gt;#000000&lt;/code&gt; from CSS, inline SVGs, and Android &lt;code&gt;capacitor.config.ts&lt;/code&gt;. I even wrote a Python script to batch-process all graphic assets through a mathematical gradient map, ensuring absolute compliance.&lt;/p&gt;

&lt;p&gt;But human beings make mistakes. Exhausted human beings make a lot of mistakes.&lt;/p&gt;

&lt;p&gt;So, I took the human out of the loop. I wrote a &lt;code&gt;pre-commit&lt;/code&gt; Git hook (&lt;code&gt;check_colors.sh&lt;/code&gt;) that acts as an Anti-Color-Leak Gatekeeper, physically verifying every staged file against our strict 5-color whitelist before allowing a commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# ==============================================================================&lt;/span&gt;
&lt;span class="c"&gt;# NICHLYST — Anti-Color-Leak Gatekeeper (Pre-commit Hook)&lt;/span&gt;
&lt;span class="c"&gt;# Strictly allowed 5-color palette (case-insensitive, 6 or 8-digit hex):&lt;/span&gt;
&lt;span class="c"&gt;# #232626, #3E403F, #736758, #A69B8D, #BFB3A4&lt;/span&gt;
&lt;span class="c"&gt;# ==============================================================================&lt;/span&gt;

&lt;span class="nv"&gt;ALLOWED_PATTERN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"^#((232626|3e403f|736758|a69b8d|bfb3a4)([0-9a-fA-F]{2})?)$"&lt;/span&gt;

&lt;span class="c"&gt;# Determine files to check: staged in Git, or all www/ files on manual execution&lt;/span&gt;
&lt;span class="nv"&gt;FILES_TO_CHECK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git diff &lt;span class="nt"&gt;--cached&lt;/span&gt; &lt;span class="nt"&gt;--name-only&lt;/span&gt; &lt;span class="nt"&gt;--diff-filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ACM 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FILES_TO_CHECK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;FILES_TO_CHECK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find www &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="se"&gt;\(&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.css"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.js"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.html"&lt;/span&gt; &lt;span class="se"&gt;\)&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;EXIT_CODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="nv"&gt;FOUND_ERRORS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0

&lt;span class="k"&gt;for &lt;/span&gt;file &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;$FILES_TO_CHECK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c"&gt;# Skip binary assets, fonts, audio files, and the check script itself&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ &lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;png|webp|jpg|jpeg|svg|ico|ttf|woff|woff2|mp3|ogg|wav&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"scripts/check_colors.sh"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    continue
  fi&lt;/span&gt;

  &lt;span class="c"&gt;# Find all 3, 6, and 8-character HEX colors&lt;/span&gt;
  &lt;span class="nv"&gt;HEX_MATCHES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-onE&lt;/span&gt; &lt;span class="s2"&gt;"#[0-9a-fA-F]{3,8}&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HEX_MATCHES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; match&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
      &lt;/span&gt;&lt;span class="nv"&gt;LINE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$match&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;: &lt;span class="nt"&gt;-f1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
      &lt;span class="nv"&gt;HEX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$match&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;: &lt;span class="nt"&gt;-f2&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
      &lt;span class="nv"&gt;HEX_LOWER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HEX&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'[:upper:]'&lt;/span&gt; &lt;span class="s1"&gt;'[:lower:]'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

      &lt;span class="c"&gt;# Validate against the allowed palette whitelist&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HEX_LOWER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qE&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ALLOWED_PATTERN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"🚨 [STOP-LOSS] Unauthorized color '&lt;/span&gt;&lt;span class="nv"&gt;$HEX&lt;/span&gt;&lt;span class="s2"&gt;' in &lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$LINE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        &lt;span class="nv"&gt;EXIT_CODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
        &lt;span class="o"&gt;((&lt;/span&gt;FOUND_ERRORS++&lt;span class="o"&gt;))&lt;/span&gt;
      &lt;span class="k"&gt;fi
    done&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HEX_MATCHES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;fi
done

if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$EXIT_CODE&lt;/span&gt; &lt;span class="nt"&gt;-ne&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"❌ COMMIT REJECTED! Found &lt;/span&gt;&lt;span class="nv"&gt;$FOUND_ERRORS&lt;/span&gt;&lt;span class="s2"&gt; unauthorized color values."&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"   NICHLYST strictly allows only 5 colors: #232626, #3E403F, #736758, #A69B8D, #BFB3A4."&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✅ [COLOR-GUARD] All colors comply with the official NICHLYST palette."&lt;/span&gt;
&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I am tired and accidentally style a button with &lt;code&gt;#FFF&lt;/code&gt;, the system physically rejects the commit. The system enforces the aesthetic when my brain cannot.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Narrative Rewrite: Narrative Game Design — Why I Threw Away the Script a Month Before Release
&lt;/h2&gt;

&lt;p&gt;A few days ago, I booted up the local server, clicked through the first three days, and felt nothing.&lt;/p&gt;

&lt;p&gt;The engine worked perfectly. The resources depleted. The RevenueCat SDK integration was flawless (the paywall triggers exactly after Day 3). But the text was dry.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Khoma arrives. He has evidence. Do you take it?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you are going to charge $4.99 for a narrative game — my entire indie game monetization strategy is that one honest purchase, no dark patterns — the text must react to the player's conscience. It cannot be a telegram. The player must feel like they are reading an elite, heavy book where they are the protagonist.&lt;/p&gt;

&lt;p&gt;So, I rewrote the JSON schema and refactored the UI (&lt;code&gt;DayScreen.js&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Instead of a single string, the engine now accepts a &lt;code&gt;chapter_narrative&lt;/code&gt; array, breaking every day into four distinct phases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Awakening:&lt;/strong&gt; The somatic feeling of the basement. Cold concrete, heavy lungs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approach:&lt;/strong&gt; The footsteps outside the blast door. The anxiety before the knock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encounter:&lt;/strong&gt; The dialogue and the evidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dilemma:&lt;/strong&gt; The moral trap where there are no good options.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I updated the CSS to handle this like true typography. I added an isolated scrolling container, a cascading fade-in animation, and a drop-cap (&lt;code&gt;::first-letter&lt;/code&gt;) to the opening paragraph so the text feels tactile and deliberate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* ─── Narrative Scroll Area (multi-paragraph chapter_narrative) ── */&lt;/span&gt;
&lt;span class="nc"&gt;.nl-narrative-scroll-area&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;overflow-y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;max-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100vh&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="m"&gt;290px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;padding-right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.25rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;scrollbar-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;thin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;scrollbar-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--c-3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--c-1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;-webkit-overflow-scrolling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.nl-narrative-scroll-area&lt;/span&gt;&lt;span class="nd"&gt;::-webkit-scrollbar&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.nl-narrative-scroll-area&lt;/span&gt;&lt;span class="nd"&gt;::-webkit-scrollbar-track&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--c-1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.nl-narrative-scroll-area&lt;/span&gt;&lt;span class="nd"&gt;::-webkit-scrollbar-thumb&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--c-3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* ─── Narrative Paragraph (book typography) ──────────────────── */&lt;/span&gt;
&lt;span class="nc"&gt;.nl-narrative-paragraph&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--font-serif&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;"Georgia"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;serif&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.05rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--c-5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.25rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;justify&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;nlParagraphFadeIn&lt;/span&gt; &lt;span class="m"&gt;0.35s&lt;/span&gt; &lt;span class="n"&gt;ease&lt;/span&gt; &lt;span class="n"&gt;forwards&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.nl-narrative-paragraph&lt;/span&gt;&lt;span class="nd"&gt;:nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;animation-delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.05s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.nl-narrative-paragraph&lt;/span&gt;&lt;span class="nd"&gt;:nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;2&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;animation-delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.15s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.nl-narrative-paragraph&lt;/span&gt;&lt;span class="nd"&gt;:nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;3&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;animation-delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.25s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.nl-narrative-paragraph&lt;/span&gt;&lt;span class="nd"&gt;:nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;4&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;animation-delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.35s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.nl-narrative-paragraph&lt;/span&gt;&lt;span class="nd"&gt;:nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;n&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="err"&gt;5&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;animation-delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.45s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Drop cap on the opening paragraph */&lt;/span&gt;
&lt;span class="nc"&gt;.nl-narrative-paragraph&lt;/span&gt;&lt;span class="nd"&gt;:first-of-type::first-letter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2.6rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;left&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin-right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.45rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.15rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--font-serif&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;"Georgia"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;serif&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--c-5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;nlParagraphFadeIn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;to&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&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;The engine now supports deep, multi-paragraph storytelling while gracefully falling back to old single-string descriptions for days I haven't rewritten yet.&lt;/p&gt;




&lt;h3&gt;
  
  
  Expanding the Dictionary of Despair
&lt;/h3&gt;

&lt;p&gt;To ensure the new writing hits the exact psychological notes required, I expanded the game's internal &lt;code&gt;game_data.json&lt;/code&gt; dictionaries. I added new tone keywords for the AI-assisted generation prompts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Wave 4:&lt;/em&gt; &lt;code&gt;chemical apathy&lt;/code&gt;, &lt;code&gt;cognitive dissonance&lt;/code&gt;, &lt;code&gt;claustrophobic giant&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Wave 6:&lt;/em&gt; &lt;code&gt;telegraphic despair&lt;/code&gt;, &lt;code&gt;spatial erasure&lt;/code&gt;, &lt;code&gt;terminal apathy&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Wave 8:&lt;/em&gt; &lt;code&gt;somatic overflow&lt;/code&gt;, &lt;code&gt;phantom flora&lt;/code&gt;, &lt;code&gt;sensory dissonance&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also added a new piece of deep lore to the Codex: &lt;strong&gt;Directive 44&lt;/strong&gt;. In the world of NICHLYST, the totalitarian Directorate has officially banned "vocal warmth" and empathy in speech as signs of psychological instability, punishable by ration forfeiture. This justifies the dry, brutal tone of the game's UI.&lt;/p&gt;




&lt;h3&gt;
  
  
  Securing the Perimeter (Via Negativa)
&lt;/h3&gt;

&lt;p&gt;Marketing is not just about posting; it is about squatting. I spent one evening locking down the digital perimeter.&lt;/p&gt;

&lt;p&gt;I registered the GitHub Organization, secured the domain &lt;code&gt;koztechie.pp.ua&lt;/code&gt; (and deployed a blazing-fast Eleventy static hub onto Netlify), claimed &lt;code&gt;@nichlyst&lt;/code&gt; on X, Instagram, TikTok, Bluesky, Threads, and Itch.io.&lt;/p&gt;

&lt;p&gt;I also had "Create a subreddit" on my to-do list. But I looked at Reddit's current corporate ecosystem, the CAPTCHAs, the shadowbans, and the decay of the open web.&lt;/p&gt;

&lt;p&gt;Aaron Swartz didn't fight for a closed corporate silo. I crossed the task off. NICHLYST is anti-Reddit. We rely on the open web, RSS, Dev.to, HackerNoon, and direct code. This is Taleb’s &lt;em&gt;Via Negativa&lt;/em&gt; in action: removing the fragile elements to make the whole system stronger.&lt;/p&gt;




&lt;h3&gt;
  
  
  The 20-Tester Wall Remains
&lt;/h3&gt;

&lt;p&gt;The game is structurally complete. The monetization is wired. The narrative is being rewritten into something profoundly heavy.&lt;/p&gt;

&lt;p&gt;But Google Play’s bureaucratic wall still stands. I need 20 active testers to keep the app installed for 14 days, or I cannot launch on time for the Shipaton.&lt;/p&gt;

&lt;p&gt;I currently have 15. I need &lt;strong&gt;5 more&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you are an Android user and want to help a developer who is pouring the last drops of his nervous system into this engine, please fill out this form. No heavy QA needed. Just accept the invite, install, and hold it on your phone.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://forms.gle/4hJkWsmuyKp3Tgtr8" rel="noopener noreferrer"&gt;https://forms.gle/4hJkWsmuyKp3Tgtr8&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I am starting my day with a selective serotonin reuptake inhibitor (SSRI) and eight other pills or capsules differing in shape, color, size, and purpose, just to keep my body and nervous system functioning. But the engine breathes. The infrastructure holds.&lt;/p&gt;

&lt;p&gt;The antifragile plan works. I'll see you at the finish line.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow this devlog for the #BuildInPublic journey toward the RevenueCat Shipaton 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>shipaton</category>
      <category>buildinpublic</category>
      <category>gamedev</category>
      <category>indiedev</category>
    </item>
    <item>
      <title>Building NICHLYST: Two Mugs, a Black Screen, and the Weekend the Engine Woke Up</title>
      <dc:creator>Eugene Kozlovsky</dc:creator>
      <pubDate>Sun, 16 Aug 2026 13:46:07 +0000</pubDate>
      <link>https://dev.to/koztechie/building-nichlyst-two-mugs-a-black-screen-and-the-weekend-the-engine-woke-up-d05</link>
      <guid>https://dev.to/koztechie/building-nichlyst-two-mugs-a-black-screen-and-the-weekend-the-engine-woke-up-d05</guid>
      <description>&lt;p&gt;A friend came by last Thursday and handed me a mug she had brought from Bulgaria. On the mug was a Kama Sutra position so acrobatic that gravity itself seemed ready to file a complaint. "So that there would be love," she said. It felt strangely appropriate: the thing on my desk that week was a half-asleep HTML5 game engine, and it was going to need exactly that kind of stubborn love to wake up.&lt;/p&gt;

&lt;p&gt;I already own a hackathon mug. It says: "I participated in the world's largest hackathon and all I got was this mug." And what is written there is the honest truth.&lt;/p&gt;

&lt;p&gt;That mug is my main one now. Every day I brew Assam in it, plus a soluble chicory-and-barley drink, and I sit with it through work and through my own projects. The project I received it for last summer no longer exists. I chose not to develop it, for reasons that deserve their own separate silence. But the mug stayed — as a reminder of what I was able to build once, and as a quiet, stubborn motivation to do it again.&lt;/p&gt;

&lt;p&gt;The new mug won for pure practicality. Six hundred milliliters. When you spend your days holding other people's infrastructure together and your evenings turning a vision into prompts, you are always drinking something, and 600 ml is the right amount of continuity.&lt;/p&gt;

&lt;p&gt;Projects die. Mugs survive. Objects outlive the work that made them. I keep thinking about that while I build a game about keeping records after everything else has burned.&lt;/p&gt;

&lt;p&gt;By the morning of August 16th, the plan had quietly eaten steps 49 through 86. Let me explain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Audio Phase: ElevenLabs Sound Effects
&lt;/h2&gt;

&lt;p&gt;The second devlog ended on a promise: the audio phase, then the engine. The audio came together first. I wrote the key voiceover lines and generated the voice — and then, without planning to, made a discovery: ElevenLabs also generates sound effects astonishingly well. The ambient layers and the small diegetic motifs of the game were born there too. I did not expect a voice tool to hand me wind, hum, and a door that creaks without moving. I normalized everything on my Xiaomi, committed, and backed the folder up before I allowed myself to feel satisfaction.&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%2F7cerk225q5abkz79pjqk.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%2F7cerk225q5abkz79pjqk.jpg" alt="VS Code workspace of the NICHLYST HTML5 game: AI-generated audio folders (ambient layers, sound motifs, English voiceover), audio_manifest.json, and codemagic.yaml with the Capacitor Android debug APK build steps — a free CI/CD pipeline for solo developers." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Vibe Coding Tools 2026: OpenCode vs Antigravity CLI
&lt;/h3&gt;

&lt;p&gt;The engine needed a different kind of help. Until recently I vibe coded with Antigravity CLI, and it was extremely convenient — exactly as long as my paid Google subscription lasted. The moment the subscription ended, the limits became laughable. Less than thirty minutes of vibe coding, and the tank was empty. So I switched to OpenCode. Even unpaid, its limits are enough to actually work, and OpenCode vibe coding quickly became my default rhythm: open the terminal, describe the intent, review the diff, commit. On Saturday I opened it, and something clicked.&lt;/p&gt;

&lt;p&gt;If you are compiling your own shortlist of the best vibe coding tools 2026, here is my field data from one weekend: Antigravity CLI is brilliant while the quota lasts, and OpenCode is the tool that keeps a solo dev walking when the quota dies. The winner of any comparison is not the smartest model — it is the one whose limits let you finish an evening session.&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%2Fjf70rpjecc01ycjijzso.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%2Fjf70rpjecc01ycjijzso.jpg" alt="OpenCode terminal splash screen — the free vibe coding CLI running the MiMo V2.5 model, used to build the NICHLYST game engine after Antigravity CLI subscription limits ran out." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Engine Wakes Up
&lt;/h3&gt;

&lt;p&gt;What followed is hard to describe as a sequence, because it stopped being one. The plan's steps blurred into a single long evening and then another. A state manager that reads the frozen JSON and loads its initial truths from it. A persistence layer — autosave after every meaningful event, because a mobile game that loses your guilt when the OS kills the process is not a game, it's a scam. A renderer for the day screen. A pipeline that resolves choices and their delayed consequences. A parser that reads condition strings and picks an ending from the data. Transitions between narrative layers. A UI that slowly decays after the finale, the way the design document always promised it should.&lt;/p&gt;

&lt;p&gt;Then the wiring: backgrounds, portraits, audio — everything the art pipeline had produced, connected to the machine. Every subsystem arrived with a checklist and at least one test scenario, because on a weak PC with a weak margin for error, "it seems to work" is not a status. Commits landed before 23:00. The 23:00 rule from the first devlog is still alive; some rules survive precisely because they are boring.&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%2F222keqw88l6st7gnbiey.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%2F222keqw88l6st7gnbiey.jpg" alt="Git commit history of the NICHLYST weekend sprint: steps 52–58 — six sound motifs, consolidated normalized audio, MARKETING_PROOF.md, and the StateManager class design that woke the engine up." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thirty-eight game development milestones closed in one weekend. Three of them had to be closed twice. That ratio is the honest metric of solo development.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Black Screen
&lt;/h3&gt;

&lt;p&gt;Because here is what happened the first time I wired the art: the game launched and showed me a header, two buttons, and a black void where the narrative was supposed to be.&lt;/p&gt;

&lt;p&gt;Diagnosis first, guess-fix never. The render pipeline was interrupting itself the moment an asset manifest request failed. And the manifest was a lie I had written: it expected file names I had invented, while the disk held file names the generator had invented. Two fictions that had never met.&lt;/p&gt;

&lt;p&gt;The fix became a contract. The narrative renders even if every optional subsystem dies. Every fetch gets a catch. And the manifest is no longer written by hand — it is regenerated from the disk, because the disk is the only source of truth; anything the script can't recognize goes to a quarantine list, never into the game by guesswork. When I reloaded and heard the knock — cautious, irregular, exactly as designed — I sat still for a while. The JSON was no longer a document. It was a place.&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%2Favcxykb0qdzhameajrjp.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%2Favcxykb0qdzhameajrjp.jpg" alt="JavaScript asset manifest code for the NICHLYST HTML5 engine: a function mapping filenames to phase keys and returning relative paths — the disk as the single source of truth after the black screen bug fix." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Monday Is Coming
&lt;/h3&gt;

&lt;p&gt;On Monday, rehabilitation begins. Three to four hours a day, including the road. I don't yet know the exact slot, which means the schedule has to absorb an unknown shape — again. My sessions with the psychologist are also coming to an end; one is left, and I caught myself grieving in advance for a structure that taught me to stand without structures.&lt;/p&gt;

&lt;p&gt;This is what the war years do to a plan. Not always with sirens. Sometimes just with the slow accumulation of appointments, procedures, endings, and beginnings, while the city stays hot and the air stays loud enough to be a baseline. You don't get heroic days. You get days. The antifragile plan from the first devlog was written for exactly this: the schedule bends, it does not break. Rehabilitation goes into the calendar. The 23:00 rule stays. The mug travels to every desk I sit at.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Tester Wall: Google Play 20 Testers, Almost Down
&lt;/h3&gt;

&lt;p&gt;Since the second devlog, the tester form has grown to 14 emails. I need 6 more. That's it. That is the whole distance between me and the Google Play production track.&lt;/p&gt;

&lt;p&gt;If you are reading this, have an Android device, and two minutes to spare:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://forms.gle/4hJkWsmuyKp3Tgtr8" rel="noopener noreferrer"&gt;https://forms.gle/4hJkWsmuyKp3Tgtr8&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Accept the invite later, install the app, keep it for 14 days. No deep QA. Just your presence to satisfy the algorithm.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Comes Next
&lt;/h3&gt;

&lt;p&gt;The engine breathes. Next is money and time: the RevenueCat SDK and the paywall, the push-notification architecture — the diegetic knocks that arrive hours after a choice — the demo video under two minutes, and a Codemagic build my testers can hold.&lt;/p&gt;

&lt;p&gt;NICHLYST is coming. And now it remembers.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow this devlog for the #BuildInPublic journey.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>shipaton</category>
      <category>indiedev</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Building NICHLYST: The Weight of 40 Days, 29 Endings, and a Style Anchor</title>
      <dc:creator>Eugene Kozlovsky</dc:creator>
      <pubDate>Thu, 13 Aug 2026 21:01:28 +0000</pubDate>
      <link>https://dev.to/koztechie/building-nichlyst-the-weight-of-40-days-29-endings-and-a-style-anchor-2o0f</link>
      <guid>https://dev.to/koztechie/building-nichlyst-the-weight-of-40-days-29-endings-and-a-style-anchor-2o0f</guid>
      <description>&lt;p&gt;Three days ago, I published the first devlog for the RevenueCat Shipaton 2026. Diving deep into the heavy, emotional trenches of narrative game design, I had a 128-step plan, a blank APK, and 3 Google Play testers. Today, I have 40 days of structured narrative content, 29 endings, a full art pipeline, and a very familiar tightness in my chest.&lt;/p&gt;

&lt;p&gt;Let me explain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Content Freeze
&lt;/h2&gt;

&lt;p&gt;Between August 10th and August 13th, I completed a massive chunk of the antifragile plan. The most critical milestone was the content freeze.&lt;/p&gt;

&lt;p&gt;What does "frozen" mean? It means NICHLYST now has a definitive shape. Unless a miracle happens and I find myself far ahead of schedule with time to consider expanding the lore, the scope is now locked. The JSON file that drives the entire game has been committed, tagged, and backed up to Google Drive. &lt;/p&gt;

&lt;p&gt;Here is what that structure looks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;40 days&lt;/strong&gt; of narrative content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;29 endings&lt;/strong&gt; (main and hidden).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple resource counters&lt;/strong&gt; (some visible to the player to manage survival, others running silently under the hood, tracking psychological and moral degradation).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Over 100 global flags&lt;/strong&gt; tracking every choice the player makes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of relying on a rigid game design document template, I wrote the initial script as a markdown document, then structured it directly into JSON. The English translations were finalized alongside the Ukrainian originals, as the hackathon requires all submission materials in English.&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%2Ffxiy1jzs18ebf847qlwf.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%2Ffxiy1jzs18ebf847qlwf.jpg" alt="game_data.json in VS Code: the JSON game data structure driving NICHLYST — version 4.0 " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Moral Audit
&lt;/h2&gt;

&lt;p&gt;Before freezing the content, I ran a moral complexity audit on the entire game. My design formula is simple: &lt;strong&gt;every choice must pit the value of testimony against the cost of survival&lt;/strong&gt;. No binary good/evil. No easy outs.&lt;/p&gt;

&lt;p&gt;The audit checked every single day for six criteria: no binary good/evil, testimony vs. survival tension, consequence weight, emotional cost, no easy outs, and narrative coherence.&lt;/p&gt;

&lt;p&gt;The audit found 39 out of 40 days passing all six checks. One day failed.&lt;/p&gt;

&lt;p&gt;It was a breathing space between two heavy acts. The problem? Both choices had zero mechanical consequences. No resource deltas. No flags set. The player could experience the day as cost-free.&lt;/p&gt;

&lt;p&gt;That is a violation of the core design philosophy. In NICHLYST, nothing is free. So I fixed it: accepting help now costs a psychological resource (the moral weight of compromise), and refusing help costs a social resource (the cost of pride). The fix was committed, and the content was frozen.&lt;/p&gt;

&lt;p&gt;The audit also identified some of the game's strongest moral scenes — moments where the player must decide whose memory gets preserved, whose identity gets erased, and whether to warm the paper archive or the living people in the freezing basement. These are not fun choices. They are not supposed to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Art Pipeline: Style Anchor and Privacy
&lt;/h2&gt;

&lt;p&gt;With content frozen, I moved to art generation. The design document specifies a "muted gouache and ink-wash illustration" style — desaturated, painterly, Eastern-European post-industrial. This is the style anchor.&lt;/p&gt;

&lt;p&gt;I am using a single AI model for this: &lt;strong&gt;MAI-Image-2.5-Pro&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;I rejected other tools because their creations are public by default. I am not publishing art before the game ships. Privacy is a stop-loss. &lt;/p&gt;

&lt;p&gt;I generated a series of test images to validate the style anchor. Then I generated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Character sheets and portraits for the Archivist and all visitors.&lt;/li&gt;
&lt;li&gt;Base background locations for the underground node and the besieged city.&lt;/li&gt;
&lt;li&gt;Postscript locations reflecting the slow decay after the finale.&lt;/li&gt;
&lt;li&gt;Flat papercut UI icons for the visible resources.&lt;/li&gt;
&lt;li&gt;The 1024×1024 app icon.&lt;/li&gt;
&lt;/ul&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%2Fd7v9obty0hgbmbh13mmb.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%2Fd7v9obty0hgbmbh13mmb.jpg" alt="Muted gouache and ink-wash character sheet for the Archivist in NICHLYST — the AI-generated style anchor: worn winter coat and rust scarf, journal desk in a besieged-city archive, plus turnaround views and collar detail studies." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every single image was manually audited against a negative checklist: no real flags, no military insignia, no celebrity likenesses, no readable text, no brand logos. One image initially had a pattern too close to real military gear. I regenerated it with explicit instructions for nondescript, worn winter gear without any insignia, and passed the audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 23:00 Rule Met Reality
&lt;/h2&gt;

&lt;p&gt;In my first devlog, I wrote about the 23:00 hard stop. I said an antifragile plan must absorb bad days.&lt;/p&gt;

&lt;p&gt;I had bad days.&lt;/p&gt;

&lt;p&gt;War breaks a person. Even a civilian one. It shatters the psyche. But paradoxically, it also builds a strange kind of callus. You start perceiving things not as acutely as you might have years ago. Explosions become background noise. But the internal pressure remains.&lt;/p&gt;

&lt;p&gt;On August 10th, I felt the pre-panic state creeping in. A tightness in the chest, shallow breathing. I knew exactly what it was — I've been treating GAD for over a year. It didn't escalate into a full attack, but the physical toll was there. Sitting at my desk, trying to write JSON while my nervous system was bracing for an impact that wasn't coming.&lt;/p&gt;

&lt;p&gt;On August 11th, Russian ballistic missiles hit Kyiv again. The explosions were loud. But I didn't panic. I just kept working. There is a strange detachment that develops — you hear the booms, you acknowledge the danger, and you return to the code. The anxiety that day wasn't about the missiles; it was the quiet, creeping dread of an exhausted mind.&lt;/p&gt;

&lt;p&gt;None of these days were perfectly productive hackathon days. The plan bent.&lt;/p&gt;

&lt;p&gt;But the plan did not break.&lt;/p&gt;

&lt;p&gt;On the morning of August 13th, I sat down, opened VS Code, and committed the final content freeze. The JSON was valid. The art was generated. The moral audit was complete. The structure was intact.&lt;/p&gt;

&lt;p&gt;The antifragile plan absorbed days of anxiety and war, and the project still shipped its milestones on schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tester Update: Surviving the Google Play 20 Testers Requirement
&lt;/h2&gt;

&lt;p&gt;In my first post, I asked for help with this exact bureaucracy. &lt;/p&gt;

&lt;p&gt;I now have 6 emails. &lt;/p&gt;

&lt;p&gt;It is progress, but the math is still unforgiving. I need 20 testers to bypass the 14-day closed testing wall before I can publish to production.&lt;/p&gt;

&lt;p&gt;So, I am putting this directly into this devlog again. If you are reading this, have an Android device, and 2 minutes to spare, please help a fellow builder bypass this bureaucratic wall.&lt;br&gt;
👉 &lt;a href="https://forms.gle/4hJkWsmuyKp3Tgtr8" rel="noopener noreferrer"&gt;https://forms.gle/4hJkWsmuyKp3Tgtr8&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You will just need to accept an invite from Google Play later, download the app, and keep it installed for 14 days. No deep QA required. Just your presence to satisfy the algorithm.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;The content is frozen. The art is generated (resizing and compression are deferred to pre-APK build — another stop-loss decision to avoid perfectionism). &lt;/p&gt;

&lt;p&gt;Next up: Phase 4 (Audio) and Phase 5 (Engineering — the game engine in HTML5/JS, reading from the JSON). The hardest part is still ahead. Writing a JavaScript state manager that tracks all these counters, flags, and ending conditions — all running inside a Capacitor Android wrapper on a phone.&lt;/p&gt;

&lt;p&gt;But the foundation is no longer fragile. It is concrete that was poured during a heatwave, under missile alerts, through the fog of GAD.&lt;/p&gt;

&lt;p&gt;NICHLYST is coming. And the content is frozen.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow this devlog for the #BuildInPublic journey.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>shipaton</category>
      <category>indiedev</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Building NICHLYST: An Antifragile Approach to Surviving a Hackathon (and Reality)</title>
      <dc:creator>Eugene Kozlovsky</dc:creator>
      <pubDate>Sun, 09 Aug 2026 21:30:57 +0000</pubDate>
      <link>https://dev.to/koztechie/building-nichlyst-an-antifragile-approach-to-surviving-a-hackathon-and-reality-2ehn</link>
      <guid>https://dev.to/koztechie/building-nichlyst-an-antifragile-approach-to-surviving-a-hackathon-and-reality-2ehn</guid>
      <description>&lt;p&gt;I’m 36. From Kyiv.&lt;/p&gt;

&lt;p&gt;Right now, my city is suffocating under a heatwave. At night, the air raid sirens wail, and the psychological weight of a war with no end in sight presses down on every decision. I am typing this on an AMD A4-4020 with 8GB of RAM — a machine so weak it chokes on modern web browsers. If you are reading this while also grinding through the RevenueCat Shipaton 2026, you already know the unique pressure of shipping under extreme constraints.&lt;/p&gt;

&lt;p&gt;I am not a traditional developer. I am a practitioner of vibe coding with AI. To me, this isn't just a buzzword; it's a philosophy of orchestrating AI. It’s the ability to translate a feeling, an idea, or a complex vision into a precise prompt for a machine. It’s about bringing a concept to life, rather than getting bogged down in the syntax of writing code from scratch. By day, I work as a webmaster and system architect, navigating a full-time job and a complex medical regimen.&lt;/p&gt;

&lt;p&gt;Yet, I am entering the hackathon. Alone. Against teams with M2 Macs and budgets.&lt;/p&gt;

&lt;p&gt;Why? Because I don't just want to build an app. I want to prove that constraints don't kill projects. Fragility does.&lt;/p&gt;

&lt;p&gt;This is the story of NICHLYST — a slow narrative survival-archive sim for Android — and the 128-step antifragile plan I wrote to ensure it actually ships by September 28th.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Concept: Survival as a Gameplay Loop
&lt;/h3&gt;

&lt;p&gt;In NICHLYST, you are an archivist in a besieged city. You don't fight. You record. Every day, someone knocks on your door with a story. You must choose: record the full truth and risk your safety, or edit it to survive. The consequences don't arrive immediately — they arrive days later, via a real-time push notification, forcing you to live with the anxiety of your choices.&lt;/p&gt;

&lt;p&gt;It’s a game about documentation, truth, and the civilian experience of conflict.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 0: The Pre-Mortem
&lt;/h3&gt;

&lt;p&gt;Most hackathon projects die because developers start coding on day one. I started by writing a 128-step plan, but before writing a single line of code, I forced myself to do a pre-mortem.&lt;/p&gt;

&lt;p&gt;I listed the 10 most likely ways NICHLYST would fail. Here they are, raw and unfiltered:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;My weak local PC freezing during an Android build.&lt;/li&gt;
&lt;li&gt;Solo development bottlenecks.&lt;/li&gt;
&lt;li&gt;Missed evenings due to fatigue or schedule conflicts.&lt;/li&gt;
&lt;li&gt;Google Play App Review delays.&lt;/li&gt;
&lt;li&gt;AI art style mismatch or inconsistency.&lt;/li&gt;
&lt;li&gt;RevenueCat sandbox failure or SDK integration issues.&lt;/li&gt;
&lt;li&gt;Missing the hard deadline entirely due to scope creep.&lt;/li&gt;
&lt;li&gt;Psychological burnout.&lt;/li&gt;
&lt;li&gt;Catastrophic loss of content or code.&lt;/li&gt;
&lt;li&gt;Conflict with an existing 32-day evening schedule I must maintain.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then, I applied Nassim Taleb's Antifragility framework. I didn't just want to mitigate these risks; I wanted the project to &lt;em&gt;benefit&lt;/em&gt; from the stress.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Barbell Strategy:&lt;/strong&gt; I allocated 90% of my time to the core, guaranteed MVP. The other 10% is strictly time-boxed for bonus categories. If you are constantly researching how to avoid scope creep, the secret is that the "middle ground" — medium-risk, medium-reward tasks — must be entirely eliminated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop-Loss Dates:&lt;/strong&gt; I hardcoded triggers into my plan. If the AI art pipeline isn't done by August 26th, I don't argue with myself. I automatically cut the art scope to a stark, 1-bit CSS visual filter and move on. Emotion has no place in the schedule.&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%2F3h32uhxr4he69mekvelr.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%2F3h32uhxr4he69mekvelr.jpg" alt="STOP_LOSS_DATES.md in VS Code: the antifragile stop-loss contract for the NICHLYST hackathon project — automatic scope-cut triggers, the August 26, 2026 art deadline, a 1-bit CSS visual fallback, and the ordered MVP cut list." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Developer Burnout Prevention
&lt;/h3&gt;

&lt;p&gt;To survive a solo hackathon, I rely on a strict checklist to protect my mental bandwidth:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hard stop at 23:00.&lt;/li&gt;
&lt;li&gt;Sleep treated as a strategic investment.&lt;/li&gt;
&lt;li&gt;Zero guilt on low-energy days.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The 23:00 Rule and the Reality of Mental Health
&lt;/h3&gt;

&lt;p&gt;My plan includes a strict rule: work ceases at 23:00. A tired solo dev creates more bugs than they fix. Sleep is a strategic investment.&lt;/p&gt;

&lt;p&gt;But that’s the theory. The reality is messier.&lt;/p&gt;

&lt;p&gt;Sometimes external factors — late shifts at my day job, or the air defense activity in Kyiv — make the 23:00 deadline impossible. And then there are the internal factors. I live with GAD and depression. There are days when there are no missiles, no extra work, but my brain simply refuses to cooperate. The energy tank is empty. On those days, the only achievable victory is making it to the shower and getting into bed.&lt;/p&gt;

&lt;p&gt;An antifragile plan can’t be a rigid, brittle clockwork. It must absorb these bad days. If I miss an evening due to a depressive episode, the Stop-Loss dates protect the project. The schedule bends, but it doesn't break. It accommodates the reality of being human.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: Infrastructure and the Art of Being Broke
&lt;/h3&gt;

&lt;p&gt;I operate on a strict, near-zero budget. My monthly income is about $700 USD, and rent consumes half of it. The only financial investment I made was paying the $25 Google Play Console registration fee — a serious amount of money for me right now.&lt;/p&gt;

&lt;p&gt;The Shipaton sponsors heavily promoted Replit for cloud builds. Replit even offers a 40% discount for hackathon participants. But even with the discount, a Replit Pro subscription is financially impossible for me.&lt;/p&gt;

&lt;p&gt;So, I engineered a workaround. I set up a local development environment on my Xubuntu 26.04 LTS machine using VS Code. For the heavy lifting — compiling the Capacitor Android APK — I connected my GitHub repo to &lt;strong&gt;Codemagic's free tier&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The setup wasn't painless. Capacitor v7 requires Java 21, but Codemagic defaulted to an older version. My first cloud build failed with &lt;code&gt;error: invalid source release: 21&lt;/code&gt;. A quick edit to the &lt;code&gt;codemagic.yaml&lt;/code&gt; file fixed the environment variables, and the build went green.&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%2F153xyd7z8hdpp7xprmi9.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%2F153xyd7z8hdpp7xprmi9.jpg" alt="Finished Codemagic CI/CD workflow " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When I sideloaded that blank, debug APK onto my Xiaomi Redmi Note 9 Pro and saw it launch... it wasn't just a blank screen. It was proof that a pipeline running from a $0 local machine to a $0 cloud builder to a physical device actually worked. The cycle was closed before I wrote a single line of game logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Google Play 20-Tester Bureaucracy
&lt;/h3&gt;

&lt;p&gt;Then came the infrastructure hurdle. Google Play now mandates a 14-day closed testing track with 20 active testers before a new personal account can publish to production.&lt;/p&gt;

&lt;p&gt;This is a bureaucratic nightmare for a solo dev with a ticking clock. If I wait until the game is done to start this timer, I will miss the deadline. So, I built a Google Form and posted it in the Shipaton Discord, my social channels, and sent it to colleagues.&lt;/p&gt;

&lt;p&gt;The result? Humbling.&lt;br&gt;
I got 3 emails from colleagues — my most loyal audience. From the Discord community? Zero. After 24 hours, the form remains agonizingly empty.&lt;/p&gt;

&lt;p&gt;I try not to succumb to panic. I try to filter out the noise, both from the outside world and my own anxiety. But the math is unforgiving: I need 20 testers, and I have 3.&lt;/p&gt;

&lt;p&gt;So, I am putting this directly into this devlog. If you are a fellow builder figuring out how to get 20 testers for Google Play, I see your struggle. But right now, I need your help to bypass this bureaucratic wall. If you have an Android device and 2 minutes to spare, please take action:&lt;br&gt;
👉 &lt;a href="https://forms.gle/4hJkWsmuyKp3Tgtr8" rel="noopener noreferrer"&gt;https://forms.gle/4hJkWsmuyKp3Tgtr8&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You will just need to accept an invite from Google Play later, download the app, and keep it installed for 14 days. No deep QA required. Just your presence to satisfy the algorithm.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Unseen Labor
&lt;/h3&gt;

&lt;p&gt;Right now, NICHLYST is just a 128-step plan, a handful of &lt;code&gt;.md&lt;/code&gt; files tracking risks and deadlines, a GitHub repository, and a &lt;code&gt;codemagic.yaml&lt;/code&gt; configuration. No art, no story code, no RevenueCat paywalls yet.&lt;/p&gt;

&lt;p&gt;But the foundation is poured. It is not fragile.&lt;/p&gt;

&lt;p&gt;The day before yesterday, after a 10-hour shift at my day job and helping my wife with her transcription work, I had zero energy left. I didn't code. I didn't force it. I followed the rule: I updated my &lt;code&gt;PLAN_STATE.md&lt;/code&gt; log with one sentence, did a &lt;code&gt;git commit&lt;/code&gt;, and went to sleep.&lt;/p&gt;

&lt;p&gt;In a hackathon, survival isn't about sprinting. It's about building a system that allows you to keep walking, even when the ground is shaking.&lt;/p&gt;

&lt;p&gt;NICHLYST is coming. And it will ship on time.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow this devlog for the #BuildInPublic journey.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>shipaton</category>
      <category>indiedev</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>One person, one project, one month: Building Svitlogics from Kyiv</title>
      <dc:creator>Eugene Kozlovsky</dc:creator>
      <pubDate>Sat, 12 Jul 2025 00:13:56 +0000</pubDate>
      <link>https://dev.to/koztechie/one-person-one-project-one-month-building-svitlogics-from-kyiv-318k</link>
      <guid>https://dev.to/koztechie/one-person-one-project-one-month-building-svitlogics-from-kyiv-318k</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/wlh"&gt;World's Largest Hackathon Writing Challenge&lt;/a&gt;: Building with Bolt.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I'm 34. From Kyiv. A city that has learned to live by the sound of sirens.&lt;br&gt;
Because of my health, office life is impossible for me. So my home became my studio.&lt;br&gt;
I’m not a professional programmer. I know some Python, that's it. My real strength is something else. A process known as "vibe coding"—the ability to translate a feeling, an idea, a vision into a clear prompt for a machine.&lt;/p&gt;

&lt;p&gt;When I saw the announcement for the World’s Largest Hackathon presented by Bolt, my first thought was crushing.&lt;br&gt;
&lt;em&gt;Who am I to compete?&lt;/em&gt;&lt;br&gt;
A 34-year-old guy from Ukraine. Not a developer. Alone. Against 70,000 participants. Against entire teams. It felt absurd.&lt;/p&gt;

&lt;p&gt;I almost gave up on the idea.&lt;br&gt;
But a day before registration closed, I asked myself: what did I really have to lose? Time? I waste it doomscrolling anyway. A prize? The odds were tiny.&lt;br&gt;
But the potential gain was incredible. Experience. And most importantly, a chance to build something that matters.&lt;/p&gt;

&lt;p&gt;That idea was already living in my head. &lt;strong&gt;&lt;a href="https://svitlogics.com/" rel="noopener noreferrer"&gt;Svitlogics&lt;/a&gt;&lt;/strong&gt;.&lt;br&gt;
The name combines the Ukrainian word "світло" (transliterated as svitlo, meaning "light") and "logics" for reason.&lt;br&gt;
This isn’t just a tool. It's my personal mission. Because I believe we must bring light into the information chaos. And while this is a global issue, here in Ukraine, the ability to see clearly has become an acute survival skill.&lt;/p&gt;

&lt;p&gt;On May 28th, I registered. On the 31st, I began.&lt;br&gt;
My first prompt was simple. Very simple. "Create the full structure for a web application. The design should have a brutalist vibe. The app's purpose is to analyze text." Then I hit "Enhance prompt" and let the AI work.&lt;/p&gt;

&lt;p&gt;The result was… magic.&lt;br&gt;
It wasn't what I envisioned, but it wasn't entirely wrong. Bolt.new had created a functional text analyzer, but one that focused on word counts and character metrics, not the nuanced analysis of manipulation I had in mind. But I didn't feel disappointment. I felt euphoria.&lt;br&gt;
That feeling when you see a first, wobbly, but incredibly important step.&lt;br&gt;
The foundation was laid. It was imperfect. It was raw. But it existed.&lt;br&gt;
The first light of Svitlogics had been switched on.&lt;/p&gt;

&lt;p&gt;Onwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let me start with the questions…
&lt;/h2&gt;

&lt;p&gt;… The ones people ask.&lt;/p&gt;

&lt;p&gt;"Why does it look so... raw?"&lt;br&gt;
"Where are the shadows? The rounded corners?"&lt;br&gt;
"Isn't it a bit... primitive?"&lt;/p&gt;

&lt;p&gt;My answer is simple. And it’s always the same.&lt;br&gt;
This is not a mistake. It is a choice. It is a philosophy.&lt;/p&gt;

&lt;p&gt;I call it "Pure Minimalist-Brutalist."&lt;br&gt;
Because I believe form must serve substance. Not hide it.&lt;br&gt;
Svitlogics exists to cut through the noise. Its design must do the same. No exceptions.&lt;/p&gt;

&lt;p&gt;Modern UI uses soft shadows to create depth. It uses gentle gradients to feel "friendly."&lt;br&gt;
This is persuasion.&lt;br&gt;
It’s the same toolkit manipulators use, just with visuals instead of words.&lt;br&gt;
I refused to fall into that trap. For a tool that dissects lies, a "friendly" interface would be the biggest lie of all.&lt;/p&gt;

&lt;p&gt;So. No shadows. No gradients. No visual tricks.&lt;br&gt;
Because they are noise. Because they distract. Because they are not honest.&lt;/p&gt;

&lt;p&gt;The font is IBM Plex Mono. For one reason: absolute clarity. It sets a serious, analytical tone. It signals that this is a tool for thinking.&lt;br&gt;
The palette is even stricter. Black and white. For pure contrast.&lt;br&gt;
And one single color. Blue.&lt;br&gt;
It’s not for branding. It’s for function. It has only one job: to say, “You can act on this” or “This is a key result.”&lt;br&gt;
One color. One purpose.&lt;/p&gt;

&lt;p&gt;This design is not about being "beautiful."&lt;br&gt;
It is about being honest.&lt;br&gt;
And for a tool designed to fight for clarity, honesty is the only thing that matters.&lt;/p&gt;

&lt;p&gt;Unapologetically.&lt;/p&gt;

&lt;h2&gt;
  
  
  If the design is its soul…
&lt;/h2&gt;

&lt;p&gt;…the system prompt is its brain.&lt;br&gt;
It’s what turns a general AI into a specialized tool. And building it was the hardest part of this journey.&lt;/p&gt;

&lt;p&gt;My first approach was naive.&lt;br&gt;
I thought I could write one "universal" prompt for both English and Ukrainian.&lt;br&gt;
I fed it an English text. The result was… promising.&lt;br&gt;
Then I fed it a Ukrainian text.&lt;br&gt;
A total mess.&lt;br&gt;
The AI missed the cultural context. The scores were erratic. The justifications felt soull-ess.&lt;/p&gt;

&lt;p&gt;This wasn't a turning point; it was a confirmation of what I already knew, especially after Russia's full-scale invasion: language is a weapon. The way manipulative narratives are constructed in English-speaking media can be vastly different from the techniques used in texts aimed at a Ukrainian audience. A universal tool would be a blunt instrument where a scalpel was needed.&lt;br&gt;
So I threw out the universal idea. And started over.&lt;br&gt;
Two separate prompts. One for each language. Double the work. But it was the only way.&lt;/p&gt;

&lt;p&gt;The process became a relentless loop.&lt;br&gt;
Prompt. Test. Analyze. Refine. Repeat.&lt;/p&gt;

&lt;p&gt;First, the data. I realized my initial test texts were too simple.&lt;br&gt;
So I spent hours building a richer dataset. Corporate press releases. Political speeches. Opinion pieces. Even 20th-century propaganda.&lt;br&gt;
Then I did something more. I used another AI to create synthetic data. To generate perfect examples of disinformation, with every manipulation I wanted to teach Svitlogics to see. It was like a targeted training course for my AI.&lt;/p&gt;

&lt;p&gt;Then, the real grind began.&lt;br&gt;
I’d run a test. Svitlogics would miss a manipulation.&lt;br&gt;
Why?&lt;br&gt;
My prompt was too vague. "Look for logical fallacies" isn't enough.&lt;br&gt;
So I’d refine it.&lt;br&gt;
“Search for a 'Straw Man' argument. A 'Straw Man' is when you misrepresent someone's position to make it easier to attack. For example…”&lt;br&gt;
Then I’d run the test again. A small victory. It found the Straw Man. But it missed the Appeal to Emotion.&lt;br&gt;
Back to the prompt.&lt;br&gt;
Another iteration. Another test.&lt;br&gt;
For hours. Days. Living in this cycle.&lt;/p&gt;

&lt;p&gt;Every success was a jolt of pure ecstasy. Every failure, a command to think clearer. To instruct better.&lt;br&gt;
This is the unseen labor behind Svitlogics.&lt;br&gt;
It's not just an API call. It's a carefully calibrated instrument.&lt;br&gt;
And the prompts are its secret sauce. The result of dozens of hours of obsession.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cascade eureka
&lt;/h2&gt;

&lt;p&gt;At first, Svitlogics was fragile. It relied on a single AI model.&lt;br&gt;
A choice born from pragmatism. I had a zero-dollar budget. It was free and generous.&lt;br&gt;
For a while, it worked.&lt;/p&gt;

&lt;p&gt;Then, during an intense testing session, it broke.&lt;br&gt;
429 Too Many Requests.&lt;br&gt;
I had hit the limit. My workflow, my focus — shattered.&lt;br&gt;
It was more than an annoyance. It was a fundamental flaw. A single point of failure.&lt;br&gt;
A tool for clarity cannot be this unreliable.&lt;/p&gt;

&lt;p&gt;The dilemma was classic. A smarter model with stricter limits? Or a dumber one that was always available?&lt;br&gt;
Neither was good enough. It needed to be smart &lt;em&gt;and&lt;/em&gt; resilient.&lt;/p&gt;

&lt;p&gt;The answer came to me while staring at the Google AI documentation.&lt;br&gt;
I didn't have to choose one model. I could use them all.&lt;br&gt;
The idea was a cascade. A sequential fallback system.&lt;br&gt;
It would try the best model first. If that failed, it wouldn't show an error. It would seamlessly try the next one. And the next.&lt;br&gt;
From a fragile pillar to a resilient, multi-layered structure.&lt;/p&gt;

&lt;p&gt;I built a priority list of seven models.&lt;br&gt;
The smartest and fastest at the top. The high-volume "workhorses" in the middle. The most limited one at the very end, defining the baseline for the whole app.&lt;br&gt;
This became the central nervous system.&lt;/p&gt;

&lt;p&gt;I chose to build this logic entirely on the client-side. In the browser.&lt;br&gt;
Simpler for a solo dev. Faster to deploy.&lt;br&gt;
I rewrote my API service into a recursive function.&lt;/p&gt;

&lt;p&gt;Here’s how it works.&lt;br&gt;
The system starts with the highest-priority model. It makes the API call.&lt;br&gt;
Success? Great. The job is done. The user gets their analysis.&lt;br&gt;
But if the API returns an error 429—meaning the model's quota is hit—there's no panic.&lt;br&gt;
The function simply calls itself again, this time targeting the next model down the cascade.&lt;br&gt;
This loop continues until it either gets a successful response or has tried all available models.&lt;br&gt;
To the user, it just works.&lt;/p&gt;

&lt;p&gt;Under the hood, it’s an elegant dance between seven different AI brains.&lt;br&gt;
Orchestrated by a few lines of JavaScript code running in the browser.&lt;br&gt;
Ensuring the light of Svitlogics never goes out. Even when some of its bulbs flicker.&lt;/p&gt;

&lt;h2&gt;
  
  
  My workflow: A symphony of two AIs
&lt;/h2&gt;

&lt;p&gt;I am not a programmer. Not a traditional one.&lt;br&gt;
This is the most important context.&lt;br&gt;
Svitlogics exists because of a new way to create. A process known as "vibe coding."&lt;br&gt;
It’s not about writing code from scratch. It's about having a clear vision and orchestrating AI to bring it to life.&lt;br&gt;
The project was built by a trio. Me, as the director. And two AI partners.&lt;/p&gt;

&lt;p&gt;Gemini was my strategic consultant. Bolt.new was my tireless engineer.&lt;br&gt;
A constant, fluid dance between them.&lt;/p&gt;

&lt;p&gt;I used Gemini as my architect and ultimate debugger. Before any big feature, I'd have a conversation with it. "I need to build a model cascade. What are the pitfalls?" It gave me blueprints. When Bolt's code broke, I didn't just ask Gemini to "fix it." I gave it the full context. The component. The prompt. The exact error. And it would find the root cause in seconds.&lt;/p&gt;

&lt;p&gt;Bolt.new was my implementation team. It built the project structure in minutes. My daily flow was simple: discuss logic with Gemini, refine details in Bolt's Discussion Mode, then execute with Build Mode. My job was to be the lead engineer. To review. To refine.&lt;br&gt;
This synergy is best illustrated by the story of when Svitlogics got "sick."&lt;/p&gt;

&lt;p&gt;It happened on June 8th. Until then, my confidence was soaring. The plan was simple: add a language switcher to the UI. I asked my strategist, Gemini. It recommended a solid, battle-tested library.&lt;br&gt;
But then, out of simple curiosity, I asked my engineer, Bolt, for its opinion. It suggested a different, lighter library. And I made a critical error. I deviated from my tested workflow and chose Bolt's suggestion.&lt;/p&gt;

&lt;p&gt;I gave it the prompt. "Install the library."&lt;br&gt;
And the machine I had come to trust turned against me.&lt;br&gt;
The application collapsed. A complete, catastrophic failure.&lt;/p&gt;

&lt;p&gt;My confidence evaporated. Frustration set in.&lt;br&gt;
I spent hours in a frantic loop. Feed error messages to Gemini. Translate its advice into prompts for Bolt. Each attempt was a gamble, burning my precious token allotment. The afternoon bled into evening. I was getting nowhere.&lt;/p&gt;

&lt;p&gt;Then, the air raid sirens began to wail in Kyiv.&lt;br&gt;
The dissonance was surreal. Wrestling with a broken app—a problem entirely of my own making—while the very real threat of missiles loomed outside. It was a stark reminder of priorities.&lt;/p&gt;

&lt;p&gt;In that moment, I had to make a decision. Not as a developer chasing perfection, but as a project manager with a hard deadline.&lt;br&gt;
I could keep fighting this losing battle. Keep burning time and tokens on a feature that wasn't core to the mission.&lt;br&gt;
Or I could retreat.&lt;/p&gt;

&lt;p&gt;I chose to retreat.&lt;br&gt;
The next day, my first command to Bolt was not to fix. It was to remove.&lt;br&gt;
"Delete the library. Remove every file. Every line of code. Revert everything."&lt;br&gt;
It felt like a small defeat. But it was the most mature decision I could make. I realized my desire for "one more feature" had endangered the entire project.&lt;/p&gt;

&lt;p&gt;A buggy app with a language switcher is infinitely worse than a stable, single-language app that does its job perfectly.&lt;br&gt;
This taught me the most valuable lesson of this hackathon.&lt;br&gt;
Ruthless prioritization.&lt;br&gt;
A great project is defined not just by what you add, but by what you wisely choose to remove.&lt;br&gt;
Sometimes, the most powerful move is not to write more code.&lt;br&gt;
It's to decisively delete it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building under fire: Finding focus amidst chaos
&lt;/h2&gt;

&lt;p&gt;This isn't just about code. It's about the human context.&lt;br&gt;
To build a hackathon project solo, with a full-time job, is a stretch.&lt;br&gt;
To do it from Kyiv, during a war, is to find focus in the heart of a storm.&lt;/p&gt;

&lt;p&gt;There were many nights like that. Nights that started not with code, but with the wail of air raid sirens. Nights that ended not with a successful commit, but with the grimly familiar routine: running to the hallway, waiting for the thud of our air defense systems at work, and hoping for the silence that follows.&lt;br&gt;
The next morning, you wake up a wreck. Utterly sleep-deprived.&lt;br&gt;
For me, sleep isn't a luxury; it's a pillar of stability. Its absence is a trigger for my depressive disorder. On those days, the energy tank is empty. Zero code gets written. Zero progress is made on Svitlogics.&lt;/p&gt;

&lt;p&gt;This is the part of the story often left out of triumphant tech narratives. The grind. The exhaustion. The many days where you produce nothing at all.&lt;br&gt;
And it was on one of those days, running on fumes, that I realized my perfectionism had become a trap.&lt;br&gt;
I was obsessed with making the system prompt "perfect." But the list of other essential tasks was a mountain.&lt;br&gt;
I was getting bogged down. Watering a single tree, while a whole ecosystem needed attention.&lt;/p&gt;

&lt;p&gt;So I made a strategic decision.&lt;br&gt;
I split my time into two tracks.&lt;br&gt;
&lt;strong&gt;Track 1: The Engineer.&lt;/strong&gt; Deep, focused work on the AI brain. Reserved for the days when I had strength.&lt;br&gt;
&lt;strong&gt;Track 2: The Product Manager.&lt;/strong&gt; Everything else. UI fixes, writing content, project planning. Less cognitively demanding tasks for the days of exhaustion.&lt;br&gt;
It was a way to keep moving forward. Always.&lt;/p&gt;

&lt;p&gt;This was a turning point. It felt empowering.&lt;br&gt;
It taught me that discipline isn't about forcing yourself to work when you have no energy left.&lt;br&gt;
It’s about being smart with the energy you have.&lt;br&gt;
On some days, progress is a complex new feature. On others, it’s fixing a single bug.&lt;br&gt;
Both are valid. Both are steps forward.&lt;/p&gt;

&lt;p&gt;This journey has been a lesson in resilience.&lt;br&gt;
The drive to create something meaningful is a powerful anchor in a sea of chaos.&lt;br&gt;
And the quiet act of writing a line of code, of building a tool for clarity while the world outside explodes, feels like a small act of defiance.&lt;br&gt;
An act of hope.&lt;br&gt;
And that is the most powerful motivator of all.&lt;/p&gt;




&lt;p&gt;34 days. What started as a last-minute decision has become one of the most rewarding experiences of my life.&lt;br&gt;
Svitlogics is now a real, working application. But for me, it is so much more.&lt;/p&gt;

&lt;p&gt;I came into this not as a programmer, but as a "vibe coder."&lt;br&gt;
I had a vision, a deep "why." But I believed my lack of traditional skills was a barrier.&lt;br&gt;
I was wrong.&lt;br&gt;
This journey proved to me that tools like Bolt.new are powerful equalizers. They shatter the walls between an idea and its execution. The future of building is here. And it’s incredibly empowering.&lt;/p&gt;

&lt;p&gt;Svitlogics itself is a project with a clear philosophy.&lt;br&gt;
It's not a judge that gives you answers. It's a scalpel to help you ask better questions.&lt;br&gt;
Its mission is to empower your own critical thinking. This belief is in every part of it—from its honest, brutalist design to its core analysis. Because the most effective way to fight disinformation is not by creating a new source of truth, but by fostering more critical minds.&lt;/p&gt;

&lt;p&gt;Building this from Kyiv gave it a meaning I never expected.&lt;br&gt;
The sounds of war are not a footnote to this story. They are the ink it was written with.&lt;br&gt;
It transformed a technical exercise into a personal act of resistance.&lt;/p&gt;

&lt;p&gt;I don't know if I will win. But I know I have already won the most important prize.&lt;br&gt;
The experience of building something I believe in, from zero.&lt;br&gt;
The lesson of managing a project, facing failure, and finding focus in chaos.&lt;br&gt;
The proof that my personal challenges do not define me.&lt;/p&gt;

&lt;p&gt;Whether it wins an award or not, Svitlogics exists. It works. It was built with purpose.&lt;br&gt;
For a solo developer from Kyiv, that is everything.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>wlhchallenge</category>
      <category>bolt</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
