<?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: Sky Shard</title>
    <description>The latest articles on DEV Community by Sky Shard (@skyshardday).</description>
    <link>https://dev.to/skyshardday</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%2F4110611%2F0acaa07f-a68c-473c-bd13-f5b7b15329ca.png</url>
      <title>DEV Community: Sky Shard</title>
      <link>https://dev.to/skyshardday</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/skyshardday"/>
    <language>en</language>
    <item>
      <title>A Recurring Event Is Not a Repeating Timestamp: What Building a Game Timer Taught Me About Time</title>
      <dc:creator>Sky Shard</dc:creator>
      <pubDate>Sat, 05 Sep 2026 05:07:33 +0000</pubDate>
      <link>https://dev.to/skyshardday/a-recurring-event-is-not-a-repeating-timestamp-what-building-a-game-timer-taught-me-about-time-37n5</link>
      <guid>https://dev.to/skyshardday/a-recurring-event-is-not-a-repeating-timestamp-what-building-a-game-timer-taught-me-about-time-37n5</guid>
      <description>&lt;p&gt;&lt;em&gt;Sky: Children of the Light&lt;/em&gt; drops a crystal shard into one of five realms on a repeating&lt;br&gt;
schedule. Players want to know one thing: &lt;strong&gt;is there a shard right now, where, and how&lt;br&gt;
long do I have.&lt;/strong&gt; Building &lt;a href="https://skyshard.day/" rel="noopener noreferrer"&gt;a live tracker for that&lt;/a&gt; looks like a&lt;br&gt;
&lt;code&gt;setInterval&lt;/code&gt; and some arithmetic. It is not, and the reasons are the same reasons every&lt;br&gt;
recurring-event feature you will ever ship is harder than it looks.&lt;/p&gt;
&lt;h2&gt;
  
  
  The bug you will write first
&lt;/h2&gt;

&lt;p&gt;Here is the intuitive implementation, and it is wrong:&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;CYCLE_HOURS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&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;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lastEruption&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;CYCLE_HOURS&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding 8 hours in milliseconds computes an &lt;em&gt;absolute&lt;/em&gt; interval. But an in-game schedule&lt;br&gt;
is almost always defined in a &lt;em&gt;civil&lt;/em&gt; timezone — "every day at 09:00 and 17:00 in the&lt;br&gt;
game's reference zone." Those are different things, and they diverge exactly twice a year.&lt;/p&gt;

&lt;p&gt;On a DST transition day, one civil day is 23 or 25 hours long. Your millisecond&lt;br&gt;
arithmetic sails straight through and lands an hour off. Every user in the affected zone&lt;br&gt;
sees the wrong countdown, on the one day when a wrong countdown is most confusing.&lt;/p&gt;

&lt;p&gt;The fix is to do the arithmetic in the reference zone's civil calendar and convert to an&lt;br&gt;
instant afterwards — not the other way around. In practice:&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;// wrong: absolute offset, ignores civil calendar&lt;/span&gt;
&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;

&lt;span class="c1"&gt;// right: civil-day arithmetic, then resolve to an instant&lt;/span&gt;
&lt;span class="nx"&gt;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ZonedDateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({...}).&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you cannot use &lt;code&gt;Temporal&lt;/code&gt; yet, &lt;code&gt;Intl.DateTimeFormat&lt;/code&gt; with an explicit &lt;code&gt;timeZone&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;formatToParts&lt;/code&gt; will get you the reference zone's wall-clock fields, which is enough to&lt;br&gt;
do the same thing by hand.&lt;/p&gt;
&lt;h2&gt;
  
  
  Three clocks, and only one of them is trustworthy
&lt;/h2&gt;

&lt;p&gt;A countdown has more clocks in it than you expect:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The game's schedule clock&lt;/strong&gt; — fixed, defined by the developers, in one zone&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The user's device clock&lt;/strong&gt; — arbitrary, and frequently wrong&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The user's timezone setting&lt;/strong&gt; — arbitrary, and sometimes deliberately fake&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Number 2 is the one that ruins you. A meaningful fraction of devices have clocks off by&lt;br&gt;
minutes; some are off by hours or years. If the countdown is pure client arithmetic from&lt;br&gt;
&lt;code&gt;Date.now()&lt;/code&gt;, those users see a timer that is confidently, silently wrong.&lt;/p&gt;

&lt;p&gt;You have two honest options: fetch a server timestamp once and compute an offset, or&lt;br&gt;
accept the drift and be explicit that the timer follows the device clock. What you should&lt;br&gt;
not do is compute from the device clock and present it as authoritative.&lt;/p&gt;

&lt;p&gt;Timezone (number 3) is a different problem: it is not wrong, it is just &lt;em&gt;theirs&lt;/em&gt;. The&lt;br&gt;
schedule is defined in the reference zone; the display belongs in the user's. Keep both,&lt;br&gt;
and never store the converted value — store the instant, convert at render time.&lt;/p&gt;
&lt;h2&gt;
  
  
  "Today" is a question with a bad answer
&lt;/h2&gt;

&lt;p&gt;A tracker naturally wants to say "today's shard is in Hidden Forest." But &lt;em&gt;today&lt;/em&gt; for whom?&lt;/p&gt;

&lt;p&gt;If the schedule rolls over at midnight in the reference zone, a player in a zone many&lt;br&gt;
hours ahead is looking at "today's" shard while it is still yesterday there — or the&lt;br&gt;
reverse. Both are correct statements about different days.&lt;/p&gt;

&lt;p&gt;Three approaches, each with a real cost:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reference-zone day    consistent for everyone, confusing for the user
Local day             matches the user's intuition, hard to discuss in a group
Next N events         no "day" concept at all, no ambiguity, less scannable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The third is underrated. "The next four eruptions, with countdowns" sidesteps the whole&lt;br&gt;
question and happens to be what the player actually wants — they are not planning a&lt;br&gt;
calendar week, they are deciding whether to log in now. A day-based view is the&lt;br&gt;
navigational fallback, not the primary answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Countdown rendering is its own trap
&lt;/h2&gt;

&lt;p&gt;Once the math is right, the display can still be wrong in ways that feel like bugs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;setInterval(fn, 1000)&lt;/code&gt; drifts.&lt;/strong&gt; Timers fire late, the page throttles in a
background tab, and after twenty minutes your seconds column is visibly behind. Compute
the remaining time from a timestamp every tick rather than decrementing a counter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backgrounded tabs freeze.&lt;/strong&gt; Mobile browsers suspend timers aggressively. Recompute on
&lt;code&gt;visibilitychange&lt;/code&gt;, or a user who switches apps and comes back sees a stale countdown
that has "stopped."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sub-second rounding lies.&lt;/strong&gt; If you &lt;code&gt;Math.floor&lt;/code&gt; the seconds, the timer sits on "1"
for a moment and then jumps to "expired" — reading as a skipped second. Round rather
than floor, or render at the boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are hard. All of them get reported as "the timer is broken" if you skip&lt;br&gt;
them, and each one is invisible in a five-minute test.&lt;/p&gt;

&lt;h2&gt;
  
  
  What survives from all of this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Do recurring arithmetic on civil calendars in the schedule's own timezone, then resolve
to instants. Never add fixed millisecond offsets across day boundaries.&lt;/li&gt;
&lt;li&gt;Treat the device clock as untrusted input, and say so if you are relying on it.&lt;/li&gt;
&lt;li&gt;Prefer "the next N events" over "today's event." It avoids a genuine ambiguity rather
than picking a side.&lt;/li&gt;
&lt;li&gt;Recompute the countdown from a timestamp on every tick and on &lt;code&gt;visibilitychange&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://skyshard.day/" rel="noopener noreferrer"&gt;shard tracker&lt;/a&gt; exists because doing this correctly once is&lt;br&gt;
worth more than every player doing the timezone conversion in their head every day. That&lt;br&gt;
is basically the whole value proposition of every schedule tool ever built — someone has&lt;br&gt;
to eat the complexity, and it should not be the person who just wants to know whether to&lt;br&gt;
log in.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>gamedev</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
