Skip to content

Seasons and events

tilttek/seasonal-events

Schedules

A SeasonSchedule says when a season comes, in its timezone:

SeasonSchedule::once('America/Toronto', '2027-03-20', '2027-04-03');            // once, both days included
SeasonSchedule::yearly('America/Toronto', '2026-12-15', 23);                     // each year from the 15th of December
SeasonSchedule::yearly('America/Toronto', '2026-11-26', 4, 'FREQ=YEARLY;BYMONTH=11;BYDAY=4TH'); // the fourth Thursday of November

A yearly season starts on the dates its rule gives from its first date, which the rule starts from: FREQ=YEARLY alone comes back on the same date each year, INTERVAL=2 every other year, COUNT and UNTIL end it. A season lasts 1 to 183 days, half a year, so that one time of a season never meets its next. A season on the 29th of February comes back on the next 29th of February.

Its days start at midnight on the wall clock of its timezone, across changes of time: a day of 23 or 25 hours is one day. Each time a season comes is a Period, from the start of its first day, included, to the start of the day after its last, excluded. Its code is the date of its first day, 2026-12-15: it stays the same for that time whatever the clock says, so that a quest done or an item earned in it is told apart from the next year.

MethodAnswers
periodAt($moment)The time of the season a moment falls in, or null.
nextPeriod($after)The first time that starts after a moment, or null when it never comes again.
periods($from, $through, $limit)The times that start between two dates, at most so many.

Seasons

A Season has a stable identifier — the same from one year to the next —, its schedule, what it brings while it lasts, and its events:

new Season('winter-festival', $schedule, [
    Content::quest('quest:winter-hunt'),
    Content::item('item:snow-hat'),
    Content::decoration('decoration:snowflakes'),
], $events);

A Content is a kind — ContentKind::Quest, Item or Decoration — and a reference the application or the package it belongs to knows. A season brings at most 200 contents and holds at most 50 events.

Events

A SeasonalEvent runs inside a season, from one of its days for a number of days — to the season's end by default — and adds its own contents while it runs:

new SeasonalEvent('gift-week', fromDay: 8, days: 7, contents: [Content::item('item:wrapped-die')], multiplier: new Multiplier('points', 200), announceDays: 7);

An event starts and ends at midnight in the season's timezone, and never runs past the season's end. Its identifier is unique in its season.

Announcements and multipliers

An event is announced announceDays days before it starts, 0 to 90: from then, SeasonalEvents::announced() lists it, so that a site shows what is coming. A Multiplier multiplies the gains of a ledger while its event runs, in percent: 150 adds half, 200 doubles, 101 to 500. An event with a multiplier is announced at least one day ahead: the members know of it before it begins, and nothing multiplies by surprise. Multipliers of events that meet never add up: the highest applies. Multiplier::apply() rounds down, so that a gain never grows by a fraction of a point.

Limits

Every value is checked when it is built, and a refusal throws InvalidSeason with the reason. Identifiers are 1 to 64 lower-case letters, digits, _, . or -, starting with a letter or a digit.