Skip to content

Instantly share code, notes, and snippets.

@stuf
Last active February 9, 2017 09:14
Show Gist options
  • Save stuf/54662336a70695a99e42e514dd481380 to your computer and use it in GitHub Desktop.
Save stuf/54662336a70695a99e42e514dd481380 to your computer and use it in GitHub Desktop.
Kancolle Fleet mission timers in calmm

Simple countdown timers in Kancolle

In Kancolle, we have Fleets, which contain information on the Fleet composition, its name, its state indicating whether it's on a missionm with a timestamp indicating when the Fleet will be back from its mission. A wise guy on Twitter already said that timers can be created with setTimeout (or more specifically setInterval), but since sanity might be scarce nowadays, manually managing these periodic effects is something you rather delegate to somebody/something else.

Because calmm encourages streams for data and application state, we can make a simple React component that will be used to display a constantly updating countdown to a set point in the future.

In these examples, I will be using karet for making React play nice with streams, karet.util and partial.lenses for the rest.

If you're not familiar with lenses, take a look at the tutorial in the partial.lenses library to see how they can be useful for us.

The data

Before we start, we need to establish the structure of the data we will be using. A basic Fleet's data structure is going to look something like this:

{
  id: 1234,
  ...,
  mission: {
    completionTime: Number,
    state: Number,
    ...
  }
}

Creating the basic stuff

Let's first define some views that we'll use to make the countdown magic happen.

We'll start by defining a couple of views into our Fleet, which we'll use to get the Fleet's name and time until completion.

const nameIn = U.view('name');
const completionTimeIn = U.view(['mission', 'completionTime', L.define(0)]);

We also need to create a stream that will emit with some specified interval, so here's a function for creating such a stream with Kefir.

const interval = t => Kefir.interval(t).toProperty(() => {});

That's it. Nothing more needed. Now that we have an interval and view to the data defined, we can create the React component for it. Here's a simple component for such a thing:

const Countdown = ({ targetTime }) => <div>{targetTime}</div>;

Very plain and very simple. We can pass in a stream into targetTime, and use it straight up in the component like any other data in React. Because we're using karet, we can embed streams into components just like your regular data in a React component.

Let's finish it up by making the component actually count down.

Making it tick

Let's put the interval creation function we defined earlier into use.

const tick = interval(500);

const Countdown = ({ targetTime }) =>
  <div>
    {tick.map(() => targetTime - +(new Date()))}
  </div>;

Dandy! Now we have a countdown that recalculates the time until the set time every 0.5 seconds. But wait, let's fix it up a little bit still. The timer will keep on ticking even after it has passed the target time, so let's make it so it doesn't go past zero and start counting on negative time. It's a countdown, not a countup.

karet.util contains most of the functions we're used to in ramda, with the difference of these functions being lifted into working on streams. No if/else necessary here, let's clamp the value of the timer to 0:

const Countdown = ({ targetTime }) =>
  <div>
    {tick.map(() => U.clamp(0, Infinity, (targetTime - +(new Date()))))}
  </div>;

Nice! Nothing more needed here. Now we can put our Fleet component together.

Putting it all together

Now all that's missing is for us to create a component that will display the name and countdown for our fleets.

const FleetCountdown = ({ fleet }) =>
  <div>
    <div>{nameIn(fleet)}</div>
    <Countdown targetTime={completionTimeIn(fleet)} />
  </div>;

That's it for that component. When it's called with a view to a Fleet, or with data for a Fleet, the Countdown component will take the targetTime and initialize an automatically updating countdown towards the given timestamp.

Resulting component

Now we've ended up with something that looks roughly like this:

const nameIn = U.view('name');
const completionTimeIn = U.view(['mission', 'completionTime', L.define(0)]);

const interval = t => Kefir.interval(t).toProperty(() => {});

const Countdown = ({ targetTime }) =>
  <div>
    {tick.map(() => U.clamp(0, Infinity, (targetTime - +(new Date()))))}
  </div>;

const FleetCountdown = ({ fleet }) =>
  <div>
    <div>{nameIn(fleet)}</div>
    <Countdown targetTime={completionTimeIn(fleet)} />
  </div>;

Not bad!

There are some ways we can go from here to organize things a bit better, but that's outside of the scope of this gist. But this should give a good overview of working with streams.


Bonus

A little nicer version of the countdown component that displays the time rounded down to seconds. Just plop in moment.duration in the end if you need some extra niceties.

const tick = (t: number, f: Fn1 = id) => Kefir.interval(t).toProperty(f);
const timeDelta = (t: number): number => t - +(new Date());

const Duration = ({ until, prefix, suffix, interval = 1000, ...props }: *) =>
  <div {...props}>
    {prefix}
    {tick(interval).map(() =>
      U.seq(timeDelta(until),
        U.clamp(0, Infinity),
        U.divide(U.__, 1000),
        U.floor))}
    {suffix}
  </div>;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment