Data are taken from the Ocean Fishing Spreadsheet managed by S’yahn Tia. To report errors, please visit the Fisherman’s Horizon Discord or message Lulu Pillow@Adamantoise or Pillowfication#0538.

Bite times are periodically fetched from Teamcraft and cleaned up with the following process:

  1. For each of the 14 fishing spots (non spectral and spectral) and for each of the 15 baits (mooches included), fetch all reports at the specified fishing spot with the specified bait.
  2. For each fish-bait combination, calculate the bite time range from the reports.
    1. If there are fewer than 10 total reports, do not calculate the bite time range. (This tends to remove reports where fish are caught with the baits that should be impossible, or blue fish where too few reports are recorded)
    2. Otherwise, remove the top 5% and the bottom 5% of the reports. The minimum and maximum of the remaining reports is used as the bite time range. (This tends to remove outliers, like Godsbed taking 18 hours to catch)
  3. For each fish, calculate the bite time range for all baits, excluding Versatile Lure.

The bite times shown on the Ocean Fishing page are the bite time ranges for all baits. The bite time ranges for all baits can currently be found at bite-times.csv.

All my data and the code I used are available on GitHub.


Ocean Fishing voyages follow a specific pattern best seen using Japan Standard Time (JST). Voyages leave every 2 hours on odd hours (at 1:00, 3:00, …, 23:00). The destination always cycles between the 4 destinations in the following order:

The 4 destinations will all be set to arrive at Day, then repeated to arrive at Sunset, then repeated to arrive at Night. There are 12 routes that I label as

and they follow the sequence

However, the first voyage of every day (at 1:00 JST) will skip a route. So there might be the schedule

Since there are 12 routes and 12 voyages a day, the route that is skipped will cycle through all 12 routes in 12 days. The full pattern of routes is 144 routes long.

const PATTERN = [
  'BD', 'TD', 'ND', 'RD', 'BS', 'TS', 'NS', 'RS', 'BN', 'TN', 'NN', 'RN',
  'TD', 'ND', 'RD', 'BS', 'TS', 'NS', 'RS', 'BN', 'TN', 'NN', 'RN', 'BD',
  'ND', 'RD', 'BS', 'TS', 'NS', 'RS', 'BN', 'TN', 'NN', 'RN', 'BD', 'TD',
  'RD', 'BS', 'TS', 'NS', 'RS', 'BN', 'TN', 'NN', 'RN', 'BD', 'TD', 'ND',
  'BS', 'TS', 'NS', 'RS', 'BN', 'TN', 'NN', 'RN', 'BD', 'TD', 'ND', 'RD',
  'TS', 'NS', 'RS', 'BN', 'TN', 'NN', 'RN', 'BD', 'TD', 'ND', 'RD', 'BS',
  'NS', 'RS', 'BN', 'TN', 'NN', 'RN', 'BD', 'TD', 'ND', 'RD', 'BS', 'TS',
  'RS', 'BN', 'TN', 'NN', 'RN', 'BD', 'TD', 'ND', 'RD', 'BS', 'TS', 'NS',
  'BN', 'TN', 'NN', 'RN', 'BD', 'TD', 'ND', 'RD', 'BS', 'TS', 'NS', 'RS',
  'TN', 'NN', 'RN', 'BD', 'TD', 'ND', 'RD', 'BS', 'TS', 'NS', 'RS', 'BN',
  'NN', 'RN', 'BD', 'TD', 'ND', 'RD', 'BS', 'TS', 'NS', 'RS', 'BN', 'TN',
  'RN', 'BD', 'TD', 'ND', 'RD', 'BS', 'TS', 'NS', 'RS', 'BN', 'TN', 'NN'

To figure out the route at a given time, we need to first establish some epoch as the first voyage and determine where in PATTERN that voyage lies. All other routes will be calculated relative to that epoch. Fortunately, JST is UTC+09:00, which means a voyage lands on the Unix epoch. As it turns out, this voyage is index 88 in PATTERN. Altogether,

const TWO_HOURS = 2 * 60 * 60 * 1000
const OFFSET = 88

 * Returns the route of the ongoing/most recent voyage.
function getRoute (date: Date) {
  // Get the number of voyages since 00:00:00 UTC, 1 January 1970
  const voyageNumber = Math.floor(date.getTime() / TWO_HOURS)

  // Get where it lies in the pattern
  const route = PATTERN[(OFFSET + voyageNumber) % PATTERN.length]

  return route