@johnmorrisdotca/kyogijo-stadiums 0.60.1 → 0.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +4 -2
- package/catalogue.json +1 -1
- package/dist/catalogue.data.js +1 -1
- package/dist/replay-entry.d.ts +7 -3
- package/dist/replay-entry.js +3 -1
- package/dist/replay.board.d.ts +75 -4
- package/dist/replay.board.js +113 -13
- package/dist/replay.d.ts +1 -1
- package/dist/replay.deadtime.d.ts +58 -0
- package/dist/replay.deadtime.js +169 -0
- package/dist/replay.hockey.d.ts +143 -0
- package/dist/replay.hockey.js +276 -0
- package/dist/replay.js +12 -6
- package/dist/replay.types.d.ts +41 -0
- package/dist/sound-entry.d.ts +8 -0
- package/dist/sound-entry.js +6 -0
- package/dist/sound.dom.d.ts +113 -0
- package/dist/sound.dom.js +508 -0
- package/dist/sound.mix.d.ts +158 -0
- package/dist/sound.mix.js +234 -0
- package/dist/sound.sets.d.ts +70 -0
- package/dist/sound.sets.js +85 -0
- package/dist/sound.synth.d.ts +38 -0
- package/dist/sound.synth.js +303 -0
- package/dist/sound.words.d.ts +16 -0
- package/dist/sound.words.js +21 -0
- package/dist/strings.core.d.ts +1 -0
- package/dist/strings.en.js +1 -1
- package/dist/strings.ja.js +1 -1
- package/dist/turn.replay.board.d.ts +1 -1
- package/dist/turn.replay.board.js +20 -3
- package/dist/venue.boards.d.ts +6 -0
- package/dist/venue.boards.js +20 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,25 @@ All notable changes to this project are written here. The format follows
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.62.0] - 2026-11-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Penalties, power plays and the penalty box (KYOGIJO-STADIUMS-48)** ([penalties](./docs/replay.md#penalties-and-the-power-play)): a penalty in a replay is a stoppage with the face-off in the offending side's end; the penalized player skates to his side's penalty box, across the ice from the benches, and sits there for the penalty's time on the game clock while his side plays a skater short (two for overlapping penalties, never below three skaters), then comes back into the play. A timeline has `penalties` now; the board over the arena shows each side's strength and each penalty's clock (the time left in the short side's block of colour, PP in the other's), and `replayBoard` gives `strength` and `penalties`. One rule set, in `/replay`: `hockeyPenaltyEnds` (a minor 2 minutes, a double minor 4, a major 5, a misconduct 10 with a substitute; a power-play goal ends a minor but not a major; coincidental penalties keep the strength even; a third penalty waits), `hockeyPenaltyKind` (the NHL play-by-play's kinds), `hockeyPenaltyBox` (a stand-in for the rink's own boxes), `penaltiesAt` and `replayGameTime`. The example game has its seven penalties (`examples/games/canada-japan.json`, 1.1 MB). Players on the bench are not drawn.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **Stoppages play like hockey (KYOGIJO-STADIUMS-45)** ([stoppages](./docs/replay.md#stoppages-the-clock-stops-the-players-line-up)): after a goal the example game no longer moves the puck from the net to centre ice as if it were passed. A timeline has `stoppages` now: in each the game clock stands still (the board over the arena shows the same time from the goal to the drop) while the replay's own clock moves on, the puck is dead (it stays where play stopped, in the net after a goal, then is out of sight until the drop), and the players glide to the face-off's line-up at the spot the rules give, a line going off by its bench; the clock runs again at the drop. One rule set for hockey, in `/replay`: `hockeyFaceoffDot` (centre after a goal and at a period's start, the end-zone spot after a save or a frozen puck, the offending side's end after icing or a penalty, the neutral-zone spot after offside, the nearest spot in the zone otherwise), `hockeyFormation` (the line-up of Rule 76), `HOCKEY_DEAD_TIME` and `HOCKEY_RULES` (the speeds and seconds), `hockeyStopOf` (the NHL play-by-play's words), and, for any sport, `addDeadTime`, `replayGameClock`, `replayStoppages`, `stoppageAt` and `restartsBetween`. A `faceoff` event is said at each drop and draws no mark. A trail begins again after a frame its object is not in. `examples/games/canada-japan.json` is made again with the rules: 66 stoppages, about fifteen minutes, 1.1 MB (was 1.3 MB). The other sports' dead time is planned in the docs, not built.
|
|
19
|
+
|
|
20
|
+
## [0.61.0] - 2026-11-03
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **Sound: the crowd of each kind of venue, at a level (KYOGIJO-STADIUMS-36)** ([sound](./docs/sound.md)): `@johnmorrisdotca/kyogijo-stadiums/sound` has `createKyogijoSound`, a surface's crowd made in the browser with Web Audio (the building's air, people talking, applause and the crowd's voice, and the pool's water), at five levels from quiet to a roar, heard in the room its venue makes (its roof and its seats: an arena rings, an open stadium does not), as large as the crowd in it and drier from above in 2D. An index of sound sets (`SOUND_BEDS`, `SOUND_SETS`, `SOUND_OF_SPORT`) gives every surface a bed for its kind of venue (arena, open stadium, ballpark, court hall, pool, dojo, ring, track) and the levels it offers (a kyudo hall only two); a surface with none has no sound. Off until turned on: nothing is fetched, made or decoded before, one audio context serves the page, and nothing plays or decodes while it is off, muted, hidden or out of sight. `soundRoom`, `soundMix` and `levelFor` are the rules, pure.
|
|
25
|
+
- The demo: a Sound button, a volume slider and the crowd's levels as buttons, kept in the address (`sound=on`, `volume`, `level`), on the real arenas (venues.html), under the surface in Try it (the levels of each surface's venue, none with the crowd off) and over the arena at the top of the page. Its recordings (`demo/sounds/`) are CC0 or in the public domain, each listed with its source, author and licence in [sound credits](./docs/sound-credits.md), made by `pnpm sounds:fetch` and `pnpm sounds` (loudness matched, seamless loops, Opus with AAC); the most a sport loads is under 110 kB. They are not in the npm package.
|
|
26
|
+
- **Sound: a game's cues from its replay (KYOGIJO-STADIUMS-36)** ([game cues](./docs/sound.md#game-cues)): `sound.follow(controller, { periods, scoreAt })` plays each sport's cues as the replay goes by them (hockey's goal horn and the crowd's swell, a whistle at a stoppage, the buzzer at a period's end, a ring's bell, a starting gun, an "ooh" at a near miss), none on a seek or while paused, and under `level: "auto"` the crowd follows the game (`excitementAt`: loud late in a close game, a roar after a goal). `cueKindOf` reads a timeline's own words (`touchdown`, `try`, `ippon`, `foul`...); `cuesOf` and `cuesOfChange` are the rules, pure. The demo's game section and every Play a game (the arena at the top of the page, Try it and the real arenas) have the sound, with Follow the game among the levels; `play-game.js` tells a page of the replay it puts on the view (`onReplay`).
|
|
27
|
+
- **Sound: live events (KYOGIJO-STADIUMS-36)** ([live events](./docs/sound.md#live-events)): `sound.cue({ kind, team })` plays an event's cues as it happens, and `sound.game(moment)` gives the game's state to the level under `auto`; a goal lifts it to a roar for six seconds. The demo's live score has the sound, and pushes each goal and the game's end as they come.
|
|
28
|
+
|
|
10
29
|
## [0.60.1] - 2026-11-03
|
|
11
30
|
|
|
12
31
|
### Fixed
|
package/README.md
CHANGED
|
@@ -159,7 +159,7 @@ Real buildings drawn in the outline OpenStreetMap maps for them, each with a pla
|
|
|
159
159
|
<td align="center" valign="top">
|
|
160
160
|
<picture>
|
|
161
161
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/replay-game-3d-desk-dark.webp">
|
|
162
|
-
<img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/replay-game-3d-desk-light.webp" alt="A made-up game of Canada and Japan replayed on the rink in 3D from the main camera: the players standing up as discs with their numbers, red circles and blue squares, the score hung over centre ice, and a
|
|
162
|
+
<img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/replay-game-3d-desk-light.webp" alt="A made-up game of Canada and Japan replayed on the rink in 3D from the main camera: the players standing up as discs with their numbers, red circles and blue squares, the score hung over centre ice, and a goal's arrow into the net." width="250">
|
|
163
163
|
</picture>
|
|
164
164
|
<br><em>A made-up game, replayed in 3D</em>
|
|
165
165
|
</td>
|
|
@@ -182,7 +182,7 @@ Real buildings drawn in the outline OpenStreetMap maps for them, each with a pla
|
|
|
182
182
|
|
|
183
183
|
A camera can follow the play, flat or in 3D: it closes in where the action is and pulls back as the players spread, or moves with the puck, while the view goes on turning ([following the play](./docs/replay.md#following-the-play)).
|
|
184
184
|
|
|
185
|
-
The scoreboard hung over an arena's ice follows the replay: its score, period and game clock come from the same timeline and clock as the controls ([the scoreboard over the arena](./docs/replay.md#the-scoreboard-over-the-arena-follows-the-game)).
|
|
185
|
+
The scoreboard hung over an arena's ice follows the replay: its score, period and game clock come from the same timeline and clock as the controls ([the scoreboard over the arena](./docs/replay.md#the-scoreboard-over-the-arena-follows-the-game)). In a stoppage its clock stands still while the players glide to the face-off's line-up and the dead puck is out of sight, and it runs again at the drop ([stoppages](./docs/replay.md#stoppages-the-clock-stops-the-players-line-up)). A penalized player sits in his box for his time while his side plays short, and the board shows the power play and each penalty's clock ([penalties](./docs/replay.md#penalties-and-the-power-play)).
|
|
186
186
|
|
|
187
187
|
**Surfaces** ([the surfaces](./docs/surfaces.md), [score and layout](./docs/score.md))
|
|
188
188
|
|
|
@@ -260,6 +260,7 @@ console.log(svg.startsWith("<svg"), svg.includes("USA 3, Canada 2"));
|
|
|
260
260
|
- **Bring your own race feed**: a race plays from a feed you choose, read by your own browser from an address, pasted or from a file, and followed while it is live: kyogijo's open race timeline or OpenF1-style lap rows, with our made-up sample feed first. [The race feed](./docs/race-feed.md), [Where to find a feed](./docs/where-to-find-a-feed.md).
|
|
261
261
|
- **Tournaments**: several surfaces on one floor, who judges where, boards laid over a surface. [Layouts](./docs/multi.md), [judging](./docs/judging.md), [boards](./docs/boards.md).
|
|
262
262
|
- **Data on a surface**: points, heat maps, shot and pass charts, team shapes and a replay, flat or in 3D with the players standing on the ice, in 48 providers' coordinate frames. [Layers](./docs/layers.md), [frames](./docs/frames.md), [replay](./docs/replay.md), [an example game](./docs/example-games.md).
|
|
263
|
+
- **Sound**: the crowd of each kind of venue at a level from quiet to a roar, heard in the room its venue makes, and its game's sounds (the goal horn, the whistle, the buzzer, the bell) from a replay or live events; made in the browser, off until turned on. [Sound](./docs/sound.md).
|
|
263
264
|
- **Looks and flags**: eight named looks, a flag or crest on each half, trophies and crests from Dougu Sports, flags from Hata, light and dark. [Theming](./docs/theming.md), [the pageant](./docs/pageant.md).
|
|
264
265
|
- **Four ways in**: a function that returns SVG text, a helper that draws into a container, a custom element, a frame for pages with no script; renderers for a canvas, d3, Observable Plot, GeoJSON and PNG. [Entry points](./docs/entry-points.md), [renderers](./docs/renderers.md).
|
|
265
266
|
- **Accessible and small**: each drawing is an `img` with a title and a description, in English or Japanese; no dependencies, no network requests, nothing stored.
|
|
@@ -275,6 +276,7 @@ console.log(svg.startsWith("<svg"), svg.includes("USA 3, Canada 2"));
|
|
|
275
276
|
| `./circuits` | Race tracks as OpenStreetMap maps them, in 2D and 3D, and a simulated race on them with its clock and timing board ([demo](https://johnmorrisdotca.github.io/kyogijo-stadiums/circuits.html?race=1)). |
|
|
276
277
|
| `./multi`, `./judging`, `./layouts` | Several surfaces for one event ([demo](https://johnmorrisdotca.github.io/kyogijo-stadiums/halls.html)); who judges where; small multiples and backgrounds. |
|
|
277
278
|
| `./layers`, `./charts`, `./frames`, `./zones`, `./pointer`, `./replay` | Data layers, charts, the providers' frames, zones by the rules, a click as a place, tracking data played on a surface. |
|
|
279
|
+
| `./sound` | A surface's crowd and its game's sounds, made in the browser with Web Audio ([demo](https://johnmorrisdotca.github.io/kyogijo-stadiums/venues.html?sound=on)). |
|
|
278
280
|
| `./athletics`, `./budo`, `./skating` | The athletics stadium, the budō (areas, dohyō, the dojo builder) and the skating rinks. |
|
|
279
281
|
| `./scene`, `./canvas`, `./d3`, `./plot`, `./geojson`, `./png` | The drawing as data, on a canvas, in d3, as an Observable Plot mark, as GeoJSON, as a PNG. |
|
|
280
282
|
| `./mount`, `./react`, `./element`, `./element/define` | Draw into a container; React props; the `<kyogijo-field>` element and its registration. |
|
package/catalogue.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"generatedAt": "2026-11-03",
|
|
4
4
|
"package": {
|
|
5
5
|
"name": "@johnmorrisdotca/kyogijo-stadiums",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.62.0"
|
|
7
7
|
},
|
|
8
8
|
"address": "https://johnmorrisdotca.github.io/kyogijo-stadiums/catalogue.json",
|
|
9
9
|
"site": "https://johnmorrisdotca.github.io/kyogijo-stadiums/",
|
package/dist/catalogue.data.js
CHANGED
|
@@ -3,7 +3,7 @@ export const CATALOGUE_DATA = {
|
|
|
3
3
|
"generatedAt": "2026-11-03",
|
|
4
4
|
"package": {
|
|
5
5
|
"name": "@johnmorrisdotca/kyogijo-stadiums",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.62.0"
|
|
7
7
|
},
|
|
8
8
|
"address": "https://johnmorrisdotca.github.io/kyogijo-stadiums/catalogue.json",
|
|
9
9
|
"site": "https://johnmorrisdotca.github.io/kyogijo-stadiums/",
|
package/dist/replay-entry.d.ts
CHANGED
|
@@ -2,8 +2,12 @@ import "./full.ts";
|
|
|
2
2
|
import "./venues.all.ts";
|
|
3
3
|
export { activeEvents, describeEvent, drawFrame, eventsBetween, interpolate, possession, replayClock, replayEvents, replayInfo, replayLayers, replaySnapshot, replaySummary, replayTable, replayTimes, trails } from "./replay.ts";
|
|
4
4
|
export type { ReplaySnapshot } from "./replay.ts";
|
|
5
|
-
export { boardClock, replayBoard, replayPeriods } from "./replay.board.ts";
|
|
6
|
-
export type { ReplayBoardState } from "./replay.board.ts";
|
|
5
|
+
export { boardClock, penaltiesAt, replayBoard, replayGameClock, replayGameTime, replayPenalties, replayPeriods } from "./replay.board.ts";
|
|
6
|
+
export type { ReplayBoardState, ReplayGameClock, ServedPenalty } from "./replay.board.ts";
|
|
7
|
+
export { addDeadTime, deadSeconds, replayStoppages, restartsBetween, stoppageAt } from "./replay.deadtime.ts";
|
|
8
|
+
export type { DeadStop, DeadTimeRules } from "./replay.deadtime.ts";
|
|
9
|
+
export { HOCKEY_BLUE_LINE, HOCKEY_DEAD_TIME, HOCKEY_DOTS, HOCKEY_PENALTY_MINUTES, HOCKEY_RULES, hockeyFaceoffDot, hockeyFormation, hockeyPenaltyBox, hockeyPenaltyEnds, hockeyPenaltyKind, hockeyStopOf, isHockeyDot, } from "./replay.hockey.ts";
|
|
10
|
+
export type { HockeyCall, HockeyPenaltyKind, HockeyRole, HockeyServed, HockeyStop, HockeyStopAt } from "./replay.hockey.ts";
|
|
7
11
|
export { alongPool, distanceAt, progressOf, raceTimeline } from "./pool.replay.ts";
|
|
8
12
|
export type { Progress, RaceReplayOptions } from "./pool.replay.ts";
|
|
9
13
|
export { formatTime, parseTime } from "./pool.race.ts";
|
|
@@ -11,4 +15,4 @@ export { REPLAY_SPEEDS, REPLAY_SPEED_RANGE, createReplay } from "./replay.contro
|
|
|
11
15
|
export { FOLLOW_LIMITS, FOLLOW_MODES, clampShot, followCamera, followMode, followShot, followStop, followWindow, frameOf, springStep, zoomFor } from "./replay.follow.ts";
|
|
12
16
|
export type { FollowBox, FollowCamera, FollowInput, FollowLimits, FollowMode, FollowShot } from "./replay.follow.ts";
|
|
13
17
|
export type { ReplayController } from "./replay.control.ts";
|
|
14
|
-
export type { ActiveEvent, Possession, ReplayChange, ReplayEvent, ReplayEventKind, ReplayFrame, ReplayLook, ReplayObject, ReplayPeriod, ReplayReason, ReplaySettings, ReplayState, ReplayTeam, ReplayTimeline, ReplayTrail, } from "./replay.types.ts";
|
|
18
|
+
export type { ActiveEvent, Possession, ReplayChange, ReplayEvent, ReplayEventKind, ReplayFrame, ReplayLook, ReplayObject, ReplayPenalty, ReplayPeriod, ReplayReason, ReplaySettings, ReplayState, ReplayStoppage, ReplayTeam, ReplayTimeline, ReplayTrail, } from "./replay.types.ts";
|
package/dist/replay-entry.js
CHANGED
|
@@ -4,7 +4,9 @@ import "./full.js";
|
|
|
4
4
|
// The real venues of the NHL's arenas, as this entry has always had them (the lean district reads the ones it shows).
|
|
5
5
|
import "./venues.all.js";
|
|
6
6
|
export { activeEvents, describeEvent, drawFrame, eventsBetween, interpolate, possession, replayClock, replayEvents, replayInfo, replayLayers, replaySnapshot, replaySummary, replayTable, replayTimes, trails } from "./replay.js";
|
|
7
|
-
export { boardClock, replayBoard, replayPeriods } from "./replay.board.js";
|
|
7
|
+
export { boardClock, penaltiesAt, replayBoard, replayGameClock, replayGameTime, replayPenalties, replayPeriods } from "./replay.board.js";
|
|
8
|
+
export { addDeadTime, deadSeconds, replayStoppages, restartsBetween, stoppageAt } from "./replay.deadtime.js";
|
|
9
|
+
export { HOCKEY_BLUE_LINE, HOCKEY_DEAD_TIME, HOCKEY_DOTS, HOCKEY_PENALTY_MINUTES, HOCKEY_RULES, hockeyFaceoffDot, hockeyFormation, hockeyPenaltyBox, hockeyPenaltyEnds, hockeyPenaltyKind, hockeyStopOf, isHockeyDot, } from "./replay.hockey.js";
|
|
8
10
|
export { alongPool, distanceAt, progressOf, raceTimeline } from "./pool.replay.js";
|
|
9
11
|
export { formatTime, parseTime } from "./pool.race.js";
|
|
10
12
|
export { REPLAY_SPEEDS, REPLAY_SPEED_RANGE, createReplay } from "./replay.control.js";
|
package/dist/replay.board.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ReplayPeriod, ReplayTimeline } from "./replay.types.ts";
|
|
1
|
+
import type { ReplayPenalty, ReplayPeriod, ReplayStoppage, ReplayTimeline } from "./replay.types.ts";
|
|
2
2
|
/** The board at a time. */
|
|
3
3
|
export interface ReplayBoardState {
|
|
4
4
|
/** The goals so far: the events of kind `goal` at or before the time, by `team`. */
|
|
@@ -7,6 +7,14 @@ export interface ReplayBoardState {
|
|
|
7
7
|
period?: number;
|
|
8
8
|
/** The game clock as it is written, `19:20`: the time left in the period, or the time gone when the timeline counts `up`. Whole seconds, so it changes once a second of game time at most. */
|
|
9
9
|
clock: string;
|
|
10
|
+
/** Only when the timeline has penalties: the skaters each side has on the ice (`[5, 4]` is a power play for the home side). */
|
|
11
|
+
strength?: [number, number];
|
|
12
|
+
/** Only when the timeline has penalties: each penalty being served at the time, by its side, with its time left as the board writes it (`1:42`), those that make the side play short first. */
|
|
13
|
+
penalties?: {
|
|
14
|
+
team: "home" | "away";
|
|
15
|
+
left: string;
|
|
16
|
+
short: boolean;
|
|
17
|
+
}[];
|
|
10
18
|
}
|
|
11
19
|
/** Seconds on a board's clock, `19:20`. A board has two digits of minutes, so more than 99:59 is shown as 99:59. */
|
|
12
20
|
export declare function boardClock(seconds: number): string;
|
|
@@ -14,10 +22,73 @@ export declare function boardClock(seconds: number): string;
|
|
|
14
22
|
export declare function replayPeriods(timeline: ReplayTimeline): readonly (ReplayPeriod & {
|
|
15
23
|
number?: number;
|
|
16
24
|
})[];
|
|
25
|
+
/** The game clock at a time on the replay's clock. */
|
|
26
|
+
export interface ReplayGameClock {
|
|
27
|
+
/** The period's number, or `undefined` when the timeline has no periods. */
|
|
28
|
+
period?: number;
|
|
29
|
+
/** Seconds of the period played on the game clock, and seconds left of it. */
|
|
30
|
+
played: number;
|
|
31
|
+
left: number;
|
|
32
|
+
/** `false` in a stoppage (and before a period has begun or after it has ended): the game clock is standing still. */
|
|
33
|
+
running: boolean;
|
|
34
|
+
/** The stoppage the time is in, when it is in one. */
|
|
35
|
+
stoppage?: ReplayStoppage;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The game clock at a time `t` on the replay's clock: the period it is in (the last that has begun; the first, before any has) and how much of it has been played. The game clock runs only while play is
|
|
39
|
+
* on: it stands still in each of the timeline's `stoppages`, while the replay's clock moves on through it. A period's `length` is spread over the seconds of it that are not in a stoppage, so a game
|
|
40
|
+
* re-timed to minutes reads like the real one. Pure.
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* import { replayGameClock } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
44
|
+
* const timeline = {
|
|
45
|
+
* frames: [{ t: 0, objects: [] }, { t: 70, objects: [] }],
|
|
46
|
+
* periods: [{ start: 0, end: 70, length: 1200 }],
|
|
47
|
+
* stoppages: [{ start: 30, end: 40, reason: "goal" }],
|
|
48
|
+
* };
|
|
49
|
+
* const at = (t: number) => { const c = replayGameClock(timeline, t); return `${c.played} ${c.running}`; };
|
|
50
|
+
* console.log(at(15), at(30), at(35), at(40), at(55));
|
|
51
|
+
* // 300 true 600 false 600 false 600 true 900 true
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export declare function replayGameClock(timeline: ReplayTimeline, t: number): ReplayGameClock;
|
|
55
|
+
/**
|
|
56
|
+
* The game time at `t` on the replay's clock: the seconds of game clock played since the first period began (the lengths of the periods before, and what has been played of this one). It stands still in a
|
|
57
|
+
* stoppage, as the game clock does: a penalty's two minutes are two minutes of it.
|
|
58
|
+
*/
|
|
59
|
+
export declare function replayGameTime(timeline: ReplayTimeline, t: number): number;
|
|
60
|
+
export declare function replayPenalties(timeline: ReplayTimeline): readonly ReplayPenalty[];
|
|
61
|
+
/** A penalty being served at a time, and its game seconds left. */
|
|
62
|
+
export interface ServedPenalty {
|
|
63
|
+
penalty: ReplayPenalty;
|
|
64
|
+
left: number;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The penalties being served at `t` (the player is in the box: from the call to his `end`) with the game seconds left of each, and each side's skaters on the ice: five, less one for each penalty that
|
|
68
|
+
* makes it play short, never fewer than three. A penalty's time left is the game clock from now to its `end` (it runs only while the game clock does), or, with no end or one a power-play goal
|
|
69
|
+
* brought early (`endedBy: "goal"`), its minutes from the drop after the call. Pure.
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* import { penaltiesAt } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
73
|
+
* const timeline = {
|
|
74
|
+
* frames: [{ t: 0, objects: [] }, { t: 60, objects: [] }],
|
|
75
|
+
* periods: [{ start: 0, end: 60, length: 1200 }],
|
|
76
|
+
* penalties: [{ team: "away" as const, by: "a4", start: 10, end: 16, minutes: 2 }],
|
|
77
|
+
* };
|
|
78
|
+
* const at = penaltiesAt(timeline, 12);
|
|
79
|
+
* console.log(at.strength, at.served.map((s) => `${s.penalty.by} ${s.left}`), penaltiesAt(timeline, 16).strength);
|
|
80
|
+
* // [ 5, 4 ] [ 'a4 80' ] [ 5, 5 ]
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export declare function penaltiesAt(timeline: ReplayTimeline, t: number): {
|
|
84
|
+
served: ServedPenalty[];
|
|
85
|
+
strength: [number, number];
|
|
86
|
+
};
|
|
17
87
|
/**
|
|
18
|
-
* The scoreboard of a replay at a time `t`, in seconds on the timeline's clock. The score is the goals at or before it; the period
|
|
19
|
-
*
|
|
20
|
-
* seconds however long it lasts on the timeline, so a game re-timed to minutes can read like the real one.
|
|
88
|
+
* The scoreboard of a replay at a time `t`, in seconds on the timeline's clock. The score is the goals at or before it; the period and the clock are the game clock's (`replayGameClock`): the clock runs
|
|
89
|
+
* through the period from its full length to nothing, or from nothing up when the timeline counts `up`, and stands still in a stoppage. A period with a `length` of its own runs its clock through that many
|
|
90
|
+
* seconds however long it lasts on the timeline, so a game re-timed to minutes can read like the real one. A timeline with `penalties` adds each side's `strength` and the penalties being served,
|
|
91
|
+
* with their time left (`penaltiesAt`).
|
|
21
92
|
* @example
|
|
22
93
|
* ```ts
|
|
23
94
|
* import { replayBoard } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
package/dist/replay.board.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// What a scoreboard shows of a replayed game at a time: the score, the period and the game clock, worked out from the same timeline the replay plays (its goals and its periods). Pure: a timeline and a time
|
|
2
2
|
// give the same board every time, so a seek, a step and a period's button all land on the right figures at once, and a replay that goes back finds the score as it was. The 3D view draws it on the scoreboard
|
|
3
3
|
// hung over the surface (`turn.replay.ts`); a page that draws its own board reads it the same way.
|
|
4
|
+
import { deadSeconds, stoppageAt } from "./replay.deadtime.js";
|
|
4
5
|
import { replayEvents, replayInfo } from "./replay.js";
|
|
5
6
|
const isNum = (value) => typeof value === "number" && Number.isFinite(value);
|
|
6
7
|
const two = (n) => String(n).padStart(2, "0");
|
|
@@ -19,9 +20,106 @@ export function replayPeriods(timeline) {
|
|
|
19
20
|
return [...given].sort((a, b) => a.start - b.start).map((p, i) => ({ ...p, number: isNum(p.number) ? p.number : i + 1 }));
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
22
|
-
* The
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* The game clock at a time `t` on the replay's clock: the period it is in (the last that has begun; the first, before any has) and how much of it has been played. The game clock runs only while play is
|
|
24
|
+
* on: it stands still in each of the timeline's `stoppages`, while the replay's clock moves on through it. A period's `length` is spread over the seconds of it that are not in a stoppage, so a game
|
|
25
|
+
* re-timed to minutes reads like the real one. Pure.
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { replayGameClock } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
29
|
+
* const timeline = {
|
|
30
|
+
* frames: [{ t: 0, objects: [] }, { t: 70, objects: [] }],
|
|
31
|
+
* periods: [{ start: 0, end: 70, length: 1200 }],
|
|
32
|
+
* stoppages: [{ start: 30, end: 40, reason: "goal" }],
|
|
33
|
+
* };
|
|
34
|
+
* const at = (t: number) => { const c = replayGameClock(timeline, t); return `${c.played} ${c.running}`; };
|
|
35
|
+
* console.log(at(15), at(30), at(35), at(40), at(55));
|
|
36
|
+
* // 300 true 600 false 600 false 600 true 900 true
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function replayGameClock(timeline, t) {
|
|
40
|
+
const periods = replayPeriods(timeline);
|
|
41
|
+
let at = periods[0];
|
|
42
|
+
for (const p of periods)
|
|
43
|
+
if (p.start <= t)
|
|
44
|
+
at = p;
|
|
45
|
+
const live = at.end - at.start - deadSeconds(timeline, at.start, at.end);
|
|
46
|
+
const into = Math.min(at.end, Math.max(at.start, t));
|
|
47
|
+
const gone = into - at.start - deadSeconds(timeline, at.start, into);
|
|
48
|
+
const share = live > 0 ? Math.min(1, Math.max(0, gone / live)) : 0;
|
|
49
|
+
const length = isNum(at.length) && at.length > 0 ? at.length : live;
|
|
50
|
+
const stoppage = stoppageAt(timeline, t);
|
|
51
|
+
const played = share * length;
|
|
52
|
+
const running = stoppage === undefined && t >= at.start && t < at.end;
|
|
53
|
+
return { ...(at.number === undefined ? {} : { period: at.number }), played, left: length - played, running, ...(stoppage === undefined ? {} : { stoppage }) };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The game time at `t` on the replay's clock: the seconds of game clock played since the first period began (the lengths of the periods before, and what has been played of this one). It stands still in a
|
|
57
|
+
* stoppage, as the game clock does: a penalty's two minutes are two minutes of it.
|
|
58
|
+
*/
|
|
59
|
+
export function replayGameTime(timeline, t) {
|
|
60
|
+
const periods = replayPeriods(timeline);
|
|
61
|
+
let before = 0;
|
|
62
|
+
for (const p of periods) {
|
|
63
|
+
if (p.end > t)
|
|
64
|
+
break;
|
|
65
|
+
const next = periods[periods.indexOf(p) + 1];
|
|
66
|
+
if (next === undefined || next.start > t)
|
|
67
|
+
break;
|
|
68
|
+
before += isNum(p.length) && p.length > 0 ? p.length : p.end - p.start - deadSeconds(timeline, p.start, p.end);
|
|
69
|
+
}
|
|
70
|
+
return before + replayGameClock(timeline, t).played;
|
|
71
|
+
}
|
|
72
|
+
/** The penalties of a timeline, each checked: a side, a start and minutes that are numbers. Kept for the next call. */
|
|
73
|
+
const PENALTIES = new WeakMap();
|
|
74
|
+
export function replayPenalties(timeline) {
|
|
75
|
+
const hit = PENALTIES.get(timeline);
|
|
76
|
+
if (hit !== undefined)
|
|
77
|
+
return hit;
|
|
78
|
+
const given = Array.isArray(timeline.penalties) ? timeline.penalties : [];
|
|
79
|
+
const out = given.filter((p) => typeof p === "object" && p !== null && (p.team === "home" || p.team === "away") && isNum(p.start) && isNum(p.minutes) && p.minutes > 0 && (p.end === undefined || (isNum(p.end) && p.end >= p.start))).map((p) => ({ ...p }));
|
|
80
|
+
PENALTIES.set(timeline, out);
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The penalties being served at `t` (the player is in the box: from the call to his `end`) with the game seconds left of each, and each side's skaters on the ice: five, less one for each penalty that
|
|
85
|
+
* makes it play short, never fewer than three. A penalty's time left is the game clock from now to its `end` (it runs only while the game clock does), or, with no end or one a power-play goal
|
|
86
|
+
* brought early (`endedBy: "goal"`), its minutes from the drop after the call. Pure.
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* import { penaltiesAt } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
90
|
+
* const timeline = {
|
|
91
|
+
* frames: [{ t: 0, objects: [] }, { t: 60, objects: [] }],
|
|
92
|
+
* periods: [{ start: 0, end: 60, length: 1200 }],
|
|
93
|
+
* penalties: [{ team: "away" as const, by: "a4", start: 10, end: 16, minutes: 2 }],
|
|
94
|
+
* };
|
|
95
|
+
* const at = penaltiesAt(timeline, 12);
|
|
96
|
+
* console.log(at.strength, at.served.map((s) => `${s.penalty.by} ${s.left}`), penaltiesAt(timeline, 16).strength);
|
|
97
|
+
* // [ 5, 4 ] [ 'a4 80' ] [ 5, 5 ]
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export function penaltiesAt(timeline, t) {
|
|
101
|
+
const served = [];
|
|
102
|
+
const short = { home: 0, away: 0 };
|
|
103
|
+
const now = replayGameTime(timeline, t);
|
|
104
|
+
for (const p of replayPenalties(timeline)) {
|
|
105
|
+
if (p.start > t || (p.end !== undefined && p.end <= t))
|
|
106
|
+
continue;
|
|
107
|
+
const full = p.minutes * 60;
|
|
108
|
+
// Its time left: to its end on the game clock (a penalty that waited for a side two short begins its count later), or, with no end or one a power-play goal brought early, its minutes from the call.
|
|
109
|
+
const left = p.end !== undefined && p.endedBy !== "goal" ? Math.min(full, replayGameTime(timeline, p.end) - now) : full - (now - replayGameTime(timeline, p.start));
|
|
110
|
+
if (left <= 0 && p.end === undefined)
|
|
111
|
+
continue;
|
|
112
|
+
served.push({ penalty: p, left: Math.max(0, left) });
|
|
113
|
+
if (p.short !== false)
|
|
114
|
+
short[p.team] += 1;
|
|
115
|
+
}
|
|
116
|
+
return { served, strength: [Math.max(3, 5 - short.home), Math.max(3, 5 - short.away)] };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The scoreboard of a replay at a time `t`, in seconds on the timeline's clock. The score is the goals at or before it; the period and the clock are the game clock's (`replayGameClock`): the clock runs
|
|
120
|
+
* through the period from its full length to nothing, or from nothing up when the timeline counts `up`, and stands still in a stoppage. A period with a `length` of its own runs its clock through that many
|
|
121
|
+
* seconds however long it lasts on the timeline, so a game re-timed to minutes can read like the real one. A timeline with `penalties` adds each side's `strength` and the penalties being served,
|
|
122
|
+
* with their time left (`penaltiesAt`).
|
|
25
123
|
* @example
|
|
26
124
|
* ```ts
|
|
27
125
|
* import { replayBoard } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
@@ -48,15 +146,17 @@ export function replayBoard(timeline, t) {
|
|
|
48
146
|
if (e.kind === "goal" && (e.team === "home" || e.team === "away"))
|
|
49
147
|
score[e.team === "home" ? 0 : 1] += 1;
|
|
50
148
|
}
|
|
51
|
-
const
|
|
52
|
-
let at = periods[0];
|
|
53
|
-
for (const p of periods)
|
|
54
|
-
if (p.start <= t)
|
|
55
|
-
at = p;
|
|
56
|
-
const span = at.end - at.start;
|
|
57
|
-
const share = span > 0 ? Math.min(1, Math.max(0, (t - at.start) / span)) : 0;
|
|
58
|
-
const length = isNum(at.length) && at.length > 0 ? at.length : span;
|
|
149
|
+
const game = replayGameClock(timeline, t);
|
|
59
150
|
// Down: the time left, rounded up, so that the full length shows until the period has moved and nothing shows only at its end. Up: the whole seconds gone.
|
|
60
|
-
const clock = timeline.clock === "up" ? boardClock(
|
|
61
|
-
|
|
151
|
+
const clock = timeline.clock === "up" ? boardClock(game.played) : boardClock(Math.ceil(game.left - 1e-9));
|
|
152
|
+
const board = game.period === undefined ? { score, clock } : { score, period: game.period, clock };
|
|
153
|
+
if (replayPenalties(timeline).length > 0) {
|
|
154
|
+
const { served, strength } = penaltiesAt(timeline, t);
|
|
155
|
+
board.strength = strength;
|
|
156
|
+
board.penalties = served
|
|
157
|
+
.map((s) => ({ team: s.penalty.team, left: boardClock(Math.ceil(s.left - 1e-9)), short: s.penalty.short !== false, seconds: s.left }))
|
|
158
|
+
.sort((a, b) => Number(b.short) - Number(a.short) || a.seconds - b.seconds)
|
|
159
|
+
.map(({ team, left, short }) => ({ team, left, short }));
|
|
160
|
+
}
|
|
161
|
+
return board;
|
|
62
162
|
}
|
package/dist/replay.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export declare function interpolate(timeline: ReplayTimeline, t: number, options
|
|
|
44
44
|
}): ReplayObject[];
|
|
45
45
|
/**
|
|
46
46
|
* The path each object took over the last `seconds` before `t`, oldest place first and ending at its place at `t`: the frames in between, and the places at the two ends.
|
|
47
|
-
* Only objects that are there at `t` have a trail. `teams` and `ids` choose whose; all by default.
|
|
47
|
+
* Only objects that are there at `t` have a trail, and it begins after the last frame the object was not in (a dead puck out of sight). `teams` and `ids` choose whose; all by default.
|
|
48
48
|
* @example
|
|
49
49
|
* ```ts
|
|
50
50
|
* import { trails } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ReplayObject, ReplayStoppage, ReplayTimeline } from "./replay.types.ts";
|
|
2
|
+
type P = readonly [number, number];
|
|
3
|
+
/**
|
|
4
|
+
* The stoppages of a timeline, in order, each checked: a `start` and an `end` that are numbers, the end not before the start. Ones that overlap are joined. Read once and kept.
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { replayStoppages } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
8
|
+
* const frames = [{ t: 0, objects: [] }];
|
|
9
|
+
* console.log(replayStoppages({ frames, stoppages: [{ start: 9, end: 12 }, { start: 2, end: 5, reason: "goal" }, { start: 4, end: 6 }, { start: 8, end: 7 }] }));
|
|
10
|
+
* // [ { start: 2, end: 6, reason: 'goal' }, { start: 9, end: 12 } ]
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export declare function replayStoppages(timeline: ReplayTimeline): readonly ReplayStoppage[];
|
|
14
|
+
/** The stoppage a time is in (from its start, up to but not at its end, when play starts again), or `undefined` while play is on. */
|
|
15
|
+
export declare function stoppageAt(timeline: ReplayTimeline, t: number): ReplayStoppage | undefined;
|
|
16
|
+
/** The seconds of a span of the timeline's clock that were dead: the parts of it in stoppages. */
|
|
17
|
+
export declare function deadSeconds(timeline: ReplayTimeline, from: number, to: number): number;
|
|
18
|
+
/**
|
|
19
|
+
* The restarts that went by while the clock moved from `from` to `to` (`(from, to]`): the stoppages whose end, the drop of the puck, is in it. What a page plays the drop's sound on, as it plays the
|
|
20
|
+
* horn on a goal of `eventsBetween`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function restartsBetween(timeline: ReplayTimeline, from: number, to: number): readonly ReplayStoppage[];
|
|
23
|
+
/** How a sport's dead time goes: the speed the players go at and how long each part lasts, by the reason for the stoppage. */
|
|
24
|
+
export interface DeadTimeRules {
|
|
25
|
+
/** The fastest a player goes to his place, in the timeline's units a second. */
|
|
26
|
+
speed: number;
|
|
27
|
+
/** Seconds after play stops before anyone heads for the restart: a goal is celebrated, a whistle is a moment. */
|
|
28
|
+
pause(reason: string): number;
|
|
29
|
+
/** Seconds after play stops that the dead ball or puck is still seen where it stopped; then it is out of sight until the restart. */
|
|
30
|
+
shown(reason: string): number;
|
|
31
|
+
/** The shortest time the players take to get to their places, and how long they stand in them before the restart. */
|
|
32
|
+
least: number;
|
|
33
|
+
hold: number;
|
|
34
|
+
/** Where a player who comes off goes, and one who comes on comes from (the bench's gate), or `undefined` for none: then they only go and come at the restart. */
|
|
35
|
+
bench?(object: ReplayObject): P | undefined;
|
|
36
|
+
/** Is it the ball or the puck: dead in the stoppage, out of sight until the restart. */
|
|
37
|
+
dead(object: ReplayObject): boolean;
|
|
38
|
+
}
|
|
39
|
+
/** A stoppage to write into a timeline: play stops at `t`; the timeline's frames from `until` on (the players in their places, the puck on the dot) are the restart. */
|
|
40
|
+
export interface DeadStop {
|
|
41
|
+
t: number;
|
|
42
|
+
/** The time on the timeline given of the restart's frame: `t` and `until` are moved apart by the dead time, what was between them is left out, and the instant between them becomes part of the stoppage. 0.04 seconds after `t` by default. */
|
|
43
|
+
until?: number;
|
|
44
|
+
reason: string;
|
|
45
|
+
/** Where play starts again. */
|
|
46
|
+
x?: number;
|
|
47
|
+
y?: number;
|
|
48
|
+
team?: "home" | "away";
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Write stoppages into a timeline. The timeline given has each stoppage as an instant: the play up to `t`, then at `until` the restart (the players in their places, the puck on the dot). Each is
|
|
52
|
+
* opened into the dead time the rules give: the puck stays where it stopped for a moment and is then out of sight, the players stand a moment (a goal is celebrated) and then go to their places at the
|
|
53
|
+
* rules' speed or slower (all getting there together, the farthest at the rules' speed), a player going off goes to the bench and the one coming on comes from it, and they stand in their places a
|
|
54
|
+
* moment before the restart. Everything after is moved on by the dead time; the periods with it (each keeps its length on the game clock), the events, the stoppages already there and the
|
|
55
|
+
* penalties. Pure.
|
|
56
|
+
*/
|
|
57
|
+
export declare function addDeadTime(timeline: ReplayTimeline, stops: readonly DeadStop[], rules: DeadTimeRules): ReplayTimeline;
|
|
58
|
+
export {};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Dead time: the stoppages of a replay. A stoppage stops the game clock (the board's) while the replay's own clock moves on; in it the ball or the puck is dead (it stays where play stopped,
|
|
2
|
+
// then is out of sight until the restart) and the players go to their places for the restart at a speed a player would. The functions here are pure and know no sport: a sport's rule set
|
|
3
|
+
// (`replay.hockey.ts` for the rink) says where play starts again, how long each part of the dead time lasts and how fast the players go, and `addDeadTime` writes the stoppages into a timeline.
|
|
4
|
+
import { interpolate } from "./replay.js";
|
|
5
|
+
const isNum = (value) => typeof value === "number" && Number.isFinite(value);
|
|
6
|
+
const CLEAN = new WeakMap();
|
|
7
|
+
/**
|
|
8
|
+
* The stoppages of a timeline, in order, each checked: a `start` and an `end` that are numbers, the end not before the start. Ones that overlap are joined. Read once and kept.
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { replayStoppages } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
12
|
+
* const frames = [{ t: 0, objects: [] }];
|
|
13
|
+
* console.log(replayStoppages({ frames, stoppages: [{ start: 9, end: 12 }, { start: 2, end: 5, reason: "goal" }, { start: 4, end: 6 }, { start: 8, end: 7 }] }));
|
|
14
|
+
* // [ { start: 2, end: 6, reason: 'goal' }, { start: 9, end: 12 } ]
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function replayStoppages(timeline) {
|
|
18
|
+
const hit = CLEAN.get(timeline);
|
|
19
|
+
if (hit !== undefined)
|
|
20
|
+
return hit;
|
|
21
|
+
const given = Array.isArray(timeline.stoppages) ? timeline.stoppages : [];
|
|
22
|
+
const sorted = given
|
|
23
|
+
.filter((s) => typeof s === "object" && s !== null && isNum(s.start) && isNum(s.end) && s.end >= s.start)
|
|
24
|
+
.map((s) => ({ ...s }))
|
|
25
|
+
.sort((a, b) => a.start - b.start);
|
|
26
|
+
const out = [];
|
|
27
|
+
for (const s of sorted) {
|
|
28
|
+
const last = out[out.length - 1];
|
|
29
|
+
if (last !== undefined && s.start <= last.end)
|
|
30
|
+
last.end = Math.max(last.end, s.end);
|
|
31
|
+
else
|
|
32
|
+
out.push(s);
|
|
33
|
+
}
|
|
34
|
+
CLEAN.set(timeline, out);
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
/** The stoppage a time is in (from its start, up to but not at its end, when play starts again), or `undefined` while play is on. */
|
|
38
|
+
export function stoppageAt(timeline, t) {
|
|
39
|
+
for (const s of replayStoppages(timeline)) {
|
|
40
|
+
if (s.start > t)
|
|
41
|
+
return undefined;
|
|
42
|
+
if (t < s.end)
|
|
43
|
+
return s;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
/** The seconds of a span of the timeline's clock that were dead: the parts of it in stoppages. */
|
|
48
|
+
export function deadSeconds(timeline, from, to) {
|
|
49
|
+
let dead = 0;
|
|
50
|
+
for (const s of replayStoppages(timeline)) {
|
|
51
|
+
if (s.start >= to)
|
|
52
|
+
break;
|
|
53
|
+
dead += Math.max(0, Math.min(to, s.end) - Math.max(from, s.start));
|
|
54
|
+
}
|
|
55
|
+
return dead;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The restarts that went by while the clock moved from `from` to `to` (`(from, to]`): the stoppages whose end, the drop of the puck, is in it. What a page plays the drop's sound on, as it plays the
|
|
59
|
+
* horn on a goal of `eventsBetween`.
|
|
60
|
+
*/
|
|
61
|
+
export function restartsBetween(timeline, from, to) {
|
|
62
|
+
return replayStoppages(timeline).filter((s) => s.end > from && s.end <= to);
|
|
63
|
+
}
|
|
64
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
65
|
+
const round1 = (n) => Math.round(n * 10) / 10;
|
|
66
|
+
const ceil2 = (n) => Math.ceil(n * 100 - 1e-6) / 100;
|
|
67
|
+
const dist = (a, b) => Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
68
|
+
const placeOn = (path, t) => {
|
|
69
|
+
if (t < path.from || t >= path.until)
|
|
70
|
+
return undefined;
|
|
71
|
+
let at = path.legs[0].a;
|
|
72
|
+
for (const leg of path.legs) {
|
|
73
|
+
if (t < leg.t0)
|
|
74
|
+
break;
|
|
75
|
+
if (t >= leg.t1)
|
|
76
|
+
at = leg.b;
|
|
77
|
+
else {
|
|
78
|
+
const f = leg.t1 > leg.t0 ? (t - leg.t0) / (leg.t1 - leg.t0) : 1;
|
|
79
|
+
return [leg.a[0] + (leg.b[0] - leg.a[0]) * f, leg.a[1] + (leg.b[1] - leg.a[1]) * f];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return at;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Write stoppages into a timeline. The timeline given has each stoppage as an instant: the play up to `t`, then at `until` the restart (the players in their places, the puck on the dot). Each is
|
|
86
|
+
* opened into the dead time the rules give: the puck stays where it stopped for a moment and is then out of sight, the players stand a moment (a goal is celebrated) and then go to their places at the
|
|
87
|
+
* rules' speed or slower (all getting there together, the farthest at the rules' speed), a player going off goes to the bench and the one coming on comes from it, and they stand in their places a
|
|
88
|
+
* moment before the restart. Everything after is moved on by the dead time; the periods with it (each keeps its length on the game clock), the events, the stoppages already there and the
|
|
89
|
+
* penalties. Pure.
|
|
90
|
+
*/
|
|
91
|
+
export function addDeadTime(timeline, stops, rules) {
|
|
92
|
+
// Each period's length on the game clock, before the dead time stretches it: a period that had none keeps the live seconds it had.
|
|
93
|
+
const periods = timeline.periods?.map((p) => ({ ...p, length: isNum(p.length) ? p.length : p.end - p.start - deadSeconds(timeline, p.start, p.end) }));
|
|
94
|
+
let frames = [...timeline.frames];
|
|
95
|
+
let events = [...(timeline.events ?? [])];
|
|
96
|
+
let stoppages = [...replayStoppages(timeline)];
|
|
97
|
+
// The last first, so that a stoppage does not move the ones before it.
|
|
98
|
+
const order = [...stops].filter((s) => isNum(s.t)).sort((a, b) => b.t - a.t);
|
|
99
|
+
let pers = periods;
|
|
100
|
+
let penalties = timeline.penalties?.map((p) => ({ ...p }));
|
|
101
|
+
for (const stop of order) {
|
|
102
|
+
const t0 = stop.t;
|
|
103
|
+
const u = isNum(stop.until) && stop.until > t0 ? stop.until : t0 + 0.04;
|
|
104
|
+
const view = { ...timeline, frames, events: [] };
|
|
105
|
+
const at = new Map(interpolate(view, t0).map((o) => [o.id, o]));
|
|
106
|
+
const next = new Map(interpolate(view, u).map((o) => [o.id, o]));
|
|
107
|
+
const pause = round2(Math.max(0, rules.pause(stop.reason)));
|
|
108
|
+
const shown = round2(Math.max(0, rules.shown(stop.reason)));
|
|
109
|
+
// The players: who stays on, who goes off and who comes on, and how far each goes.
|
|
110
|
+
const stay = [];
|
|
111
|
+
const off = [];
|
|
112
|
+
const on = [];
|
|
113
|
+
for (const [id, o] of at) {
|
|
114
|
+
if (rules.dead(o))
|
|
115
|
+
continue;
|
|
116
|
+
const b = next.get(id);
|
|
117
|
+
if (b !== undefined)
|
|
118
|
+
stay.push([o, [o.x, o.y], [b.x, b.y]]);
|
|
119
|
+
else
|
|
120
|
+
off.push([o, [o.x, o.y], rules.bench?.(o)]);
|
|
121
|
+
}
|
|
122
|
+
for (const [id, o] of next)
|
|
123
|
+
if (!rules.dead(o) && !at.has(id))
|
|
124
|
+
on.push([o, rules.bench?.(o), [o.x, o.y]]);
|
|
125
|
+
const leave = Math.max(0, ...off.map(([, a, b]) => (b === undefined ? 0 : dist(a, b) / rules.speed)));
|
|
126
|
+
const swap = off.length > 0 || on.length > 0 ? ceil2(pause + leave) : pause;
|
|
127
|
+
const come = Math.max(0, ...on.map(([, a, b]) => (a === undefined ? 0 : dist(a, b) / rules.speed)));
|
|
128
|
+
const skate = Math.max(...stay.map(([, a, b]) => dist(a, b) / rules.speed), 0);
|
|
129
|
+
const arrive = ceil2(Math.max(pause + Math.max(rules.least, skate), on.length > 0 ? round2(swap + 0.04) + come : 0));
|
|
130
|
+
const total = round2(Math.max(arrive + rules.hold, shown + 1));
|
|
131
|
+
// The paths.
|
|
132
|
+
const paths = [];
|
|
133
|
+
for (const [object, a, b] of stay)
|
|
134
|
+
paths.push({ object, from: 0, until: Infinity, legs: [{ t0: 0, t1: pause, a, b: a }, { t0: pause, t1: arrive, a, b }] });
|
|
135
|
+
for (const [object, a, b] of off)
|
|
136
|
+
paths.push({ object, from: 0, until: b === undefined ? swap : round2(swap + 0.04), legs: [{ t0: 0, t1: pause, a, b: a }, { t0: pause, t1: swap, a, b: b ?? a }] });
|
|
137
|
+
for (const [object, a, b] of on)
|
|
138
|
+
paths.push({ object, from: a === undefined ? arrive : round2(swap + 0.04), until: Infinity, legs: [{ t0: round2(swap + 0.04), t1: arrive, a: a ?? b, b }] });
|
|
139
|
+
const deadOnes = [...at.values()].filter((o) => rules.dead(o));
|
|
140
|
+
const changes = off.length > 0 || on.length > 0 ? [swap, round2(swap + 0.04)] : [];
|
|
141
|
+
const moments = [...new Set([0, pause, shown, ...changes, arrive].map(round2))].filter((m) => m >= 0 && m < total).sort((a, b) => a - b);
|
|
142
|
+
const inserted = moments.map((m) => {
|
|
143
|
+
const objects = [];
|
|
144
|
+
for (const path of paths) {
|
|
145
|
+
const p = placeOn(path, m);
|
|
146
|
+
if (p !== undefined)
|
|
147
|
+
objects.push({ ...path.object, x: round1(p[0]), y: round1(p[1]) });
|
|
148
|
+
}
|
|
149
|
+
// The dead puck: where it stopped, and out of sight from `shown` on.
|
|
150
|
+
if (m < shown)
|
|
151
|
+
for (const o of deadOnes)
|
|
152
|
+
objects.push({ ...o });
|
|
153
|
+
return { t: round2(t0 + m), objects };
|
|
154
|
+
});
|
|
155
|
+
// The restart: the frame given at `until`, and everything after it, moved on.
|
|
156
|
+
const shift = round2(t0 + total - u);
|
|
157
|
+
const move = (t) => (t >= u ? round2(t + shift) : t > t0 ? t0 : t);
|
|
158
|
+
const restart = { t: round2(t0 + total), objects: [...next.values()] };
|
|
159
|
+
frames = [...frames.filter((f) => f.t < t0), ...inserted, restart, ...frames.filter((f) => f.t > u).map((f) => ({ ...f, t: move(f.t) }))];
|
|
160
|
+
events = events.map((e) => ({ ...e, t: move(e.t), ...(isNum(e.until) ? { until: move(e.until) } : {}) }));
|
|
161
|
+
stoppages = [
|
|
162
|
+
...stoppages.map((s) => ({ ...s, start: move(s.start), end: move(s.end) })),
|
|
163
|
+
{ start: t0, end: round2(t0 + total), reason: stop.reason, ...(isNum(stop.x) && isNum(stop.y) ? { x: stop.x, y: stop.y } : {}), ...(stop.team === undefined ? {} : { team: stop.team }) },
|
|
164
|
+
].sort((a, b) => a.start - b.start);
|
|
165
|
+
pers = pers?.map((p) => ({ ...p, start: move(p.start), end: move(p.end) }));
|
|
166
|
+
penalties = penalties?.map((p) => ({ ...p, start: move(p.start), ...(isNum(p.end) ? { end: move(p.end) } : {}) }));
|
|
167
|
+
}
|
|
168
|
+
return { ...timeline, frames, events: events.sort((a, b) => a.t - b.t), stoppages, ...(pers === undefined ? {} : { periods: pers }), ...(penalties === undefined ? {} : { penalties }) };
|
|
169
|
+
}
|