Reputation: 35
Finding it hard to wrap my head around how to do this. I'm looking to make a clock that is a fantasy timezone and is calculated based on system time (because it needs to carry on when you are not on the webpage).
Daytime lasts from 7:00 am to 9:59 pm is 200 minutes in real time. 13 sec (real time) is 1 minute. Nighttime lasts from 10:00pm to 6:59 am is 45 minutes in real time. 5 sec (real time) is 1 minute.
I would like to display the time in the fantasy world using html (Example: 5:43PM) but then also show how long in real time minutes until it is day/night in the fantasy world (Example: Night in 32 mins).
I'm thinking I would need to set the system time to 0 with Date.setHours(0,0,0,0)
then work out the difference. The maths is really what is confusing me more than anything.
Upvotes: 0
Views: 114
Reputation: 27282
Before we start with a solution, let's start with some tests -- this will give definition to the eventual solution, and give us confident that we are arriving at the correct solution.
What we are ultimately after is a function; we'll call it realTimeToGameTime
. It takes a Date
object, and returns an object that looks like { day: 0, time: '7:00 am' }
. In your question, you don't mention whether it's important to know what day it is in the game (only what time), but not only does this seem to be a useful addition, you'll see it's important to the solution.
Now that we know what the end result looks like, we can consider some tests. There is some point in both real time and the game time that represents a correspondence between game time and real time. In real time, it can be any point in time we want; I'll call it c
. In game time, we'll arbitrarily call it 7:00 am on day 0. This allows us to construct a timeline that gives us the inputs and outputs for our test cases (in minutes and hours/minutes):
c+0m c+100m c+200m c+215m c+245m c+345m c+445m c+490m
c+0h c+1:40h c+3:20h c+3:35h c+4:05h c+5:45h c+7:25h c+8:10h
---+--------+--------+--------+--------+--------+--------+---------+---
7:00a 2:30p 10:00p 1:00a 7:00a 2:30p 10:00p 7:00a
day 0 day 1 day 2
We'll pick an arbitrary date/time -- midnight, Jan 1, 2017 -- and write our tests:
const y = 2017
const m = 0 // 0=Jan in JS
const d = 1
expect(realTimeToGameTime(new Date(y, m, d, 0, 0))
.toEqual({ day: 0, time: '7:00 am' })
expect(realTimeToGameTime(new Date(y, m, d, 1, 40))
.toEqual({ day: 0, time: '2:30 pm' })
expect(realTimeToGameTime(new Date(y, m, d, 3, 20))
.toEqual({ day: 0, time: '10:00 pm' })
expect(realTimeToGameTime(new Date(y, m, d, 3, 35))
.toEqual({ day: 0, time: '1:00 am' })
expect(realTimeToGameTime(new Date(y, m, d, 4, 50))
.toEqual({ day: 1, time: '7:00 am' })
expect(realTimeToGameTime(new Date(y, m, d, 5, 45))
.toEqual({ day: 1, time: '2:30 pm' })
expect(realTimeToGameTime(new Date(y, m, d, 7, 25))
.toEqual({ day: 1, time: '10:00 pm' })
expect(realTimeToGameTime(new Date(y, m, d, 8, 50))
.toEqual({ day: 2, time: '7:00 am' })
(In this example, I'm using Jest expect assertions; you can tailor to whatever test/assertion framework you like...just make sure it has a way to test value equality on objects.)
Now on to the solution.
When confronted with problems like this -- that can get confusing very quickly, especially when you consider all the implementation details -- it's best to look at a simplified picture that's easier to reason about. The numbers you've chosen make it hard to understand the manipulations required because there are so many details: minutes to hours, JavaScript likes milliseconds, etc, etc. So let's start with a simplified picture that's close to your example, but way easier to think about (don't worry: it will generalize to your specific needs).
Let's say we're only worried about hours, and that daytime in the game (7:00a-9:59p) lasts 3 hours in real time, and nighttime (10:00p-6:59a) lasts 1 hour in real-time. In this simplified model, we only care about hours, and we have a much neater timeline:
c+0h c+1h c+2h c+3h c+4h c+5h c+6h c+7h c+8h
----+-------+------+-------+------+------+-------+-------+------+-----
7:00a 12:00p 5:00p 10:00p 7:00a 12:00p 5:00p 10:00p 7:00a
day 0 day 1 day 2
The problem is easier to keep in our head now, but getting to the final solution still seems daunting. But there's an easy problem embedded in the harder problem: starting at the dawn of a new day in the game world, what time is it in the real game world after after a specific real world duration? Even simpler, let's just restrict this to one day in the game world. Solving that problem is very simple. Before we do that, though, we'll need some inputs. Here are the relevant inputs (I am using "day" to mean a 24-hour period here, distinguishing it from "daytime"):
const gameDaybreak = 7
const gameDayLength = 24
const gameDaytimeLength = 15
const gameNighttimeLength = gameDayLength - gameDaytimeLength
const gameDayLengthInRealTime = 4
const gameDaytimeLengthInRealTime = 3
const gameNighttimeLengthInRealTime =
gameDayLengthInRealTime - gameDaytimeLengthInRealTime
Now we can write our function:
gameTimeSinceDaybreakFromRealDuration(realDuration) {
if(realDuration > gameDayLengthInRealTime)
throw new Error("this function only works within a single game day")
if(realDuration <= gameDaytimeLengthInRealTime) {
return (gameDaybreak + realDuration * gameDaytimeLength /
gameDaytimeLengthInRealTime) % gameDayLength
} else {
return (gameDaybreak + gameDaytimeLength +
(realDuration - gameDaytimeLengthInRealTime) *
gameNighttimeLength / gameNighttimeLengthInRealTime) %
gameDayLength
}
}
There's only one puzzle piece left: how to determine what game day it is, and when that game day started in real hours.
Fortunately, we know each game day consumes a specific amount of real-world time (in our simplified example, 4 hours). So if we have an arbitrary correspondence date, we can simply divide the number of hours from that date by 4, and arrive at the (fractional) game day. We simply take the floor of that to get the integer game days:
const c = new Date(2017, 0, 1) // this can be anything
const realHoursSinceC = (realDate.valueOf() - c.valueOf())/1000/60/60
const gameDayFromRealDate = Math.floor(realHoursSinceC / gameDayLengthInRealTime)
(This function won't be exactly the same in our final implementation since we won't be using hours as our time basis.)
Finally, to find out how many (real) hours we are into the day, we can subtract out the time that represents past game days:
const realHoursIntoGameDay = (realDate.valueOf() - c.valueOf()) -
gameDayFromRealDate * gameDayLengthInRealTime
(We could actually be a little more efficient here with these last two calculations, but this is a little clearer.)
Here's the real magic in what we've just done: we've been looking at everything as hours so it's easier to understand. But everything we've been doing has just been arbitrary points in time and durations in the same units. JavaScript likes milliseconds...so all we have to do is use milliseconds instead of hours, and we have a general solution!
One last tiny detail is we need a formatter to get the game time into a nice format:
function formatGameTime(gameTime) {
const totalMinutes = gameTime/1000/60 // convert from milliseconds to minutes
const hours = Math.floor(totalMinutes/60)
let minutes = (totalMinutes % 60).toFixed(0)
if(minutes.length < 2) minutes = "0" + minutes // get that leading zero!
return hours < 13
? (hours + ':' + minutes + ' am')
: ((hours - 12) + ':' + minutes + ' pm')
}
Now all we have to do is put it all together:
realTimeToGameTime(realTime) {
const gameDayFromRealDate = Math.floor((realTime.valueOf() - c.valueOf() / gameDayLengthInRealTime)
const realTimeIntoGameDay = (realTime.valueOf() - c.valueOf()) - gameDayFromRealDate *
gameDayLengthInRealTime
const gameTime = this.gameTimeSinceDaybreakFromRealDuration(realTimeIntoGameDay)
return {
day: gameDayFromRealDate,
time: this.formatGameTime(gameTime),
}
},
Finally, because I really prefer pure functions -- globals are just no good! We can create a factory to create a specific "time mapper" (note this is using some ES2015 features):
function createTimeMapper(config) {
const {
c,
gameDaybreak,
gameDayLength,
gameDaytimeLength,
gameNighttimeLength,
gameDayLengthInRealTime,
gameDaytimeLengthInRealTime,
gameNighttimeLengthInRealTime,
} = config
return {
realTimeToGameTime(realTime) {
const gameDayFromRealDate = Math.floor((realTime.valueOf() - c.valueOf()) / gameDayLengthInRealTime)
const realTimeIntoGameDay = (realTime.valueOf() - c.valueOf()) - gameDayFromRealDate * gameDayLengthInRealTime
const gameTime = this.gameTimeSinceDaybreakFromRealDuration(realTimeIntoGameDay)
return {
day: gameDayFromRealDate,
time: this.formatGameTime(gameTime),
}
},
gameTimeSinceDaybreakFromRealDuration(realDuration) {
if(realDuration > gameDayLengthInRealTime)
throw new Error("this function only works within a single game day")
if(realDuration <= gameDaytimeLengthInRealTime) {
return (gameDaybreak + realDuration * gameDaytimeLength / gameDaytimeLengthInRealTime) %
gameDayLength
} else {
return (gameDaybreak + gameDaytimeLength +
(realDuration - gameDaytimeLengthInRealTime) * gameNighttimeLength / gameNighttimeLengthInRealTime) %
gameDayLength
}
},
formatGameTime(gameTime) {
const totalMinutes = gameTime/1000/60 // convert from milliseconds to minutes
const hours = Math.floor(totalMinutes/60)
let minutes = (totalMinutes % 60).toFixed(0)
if(minutes.length < 2) minutes = "0" + minutes // get that leading zero!
return hours < 13
? (hours + ':' + minutes + ' am')
: ((hours - 12) + ':' + minutes + ' pm')
},
}
}
And to use it:
const timeMapper = createTimeMapper({
new Date(2017, 0, 1),
gameDaybreak: 7*1000*60*60,
gameDayLength: 24*1000*60*60,
gameDaytimeLength: 15*1000*60*60,
gameNighttimeLength: 9*1000*60*60,
gameDayLengthInRealTime: (245/60)*1000*60*60,
gameDaytimeLengthInRealTime: (200/60)*1000*60*60,
gameNighttimeLengthInRealTime: (45/60)*1000*60*60,
})
Upvotes: 1