@johnmorrisdotca/kyogijo-stadiums 0.5.0 → 0.6.1

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.
Files changed (193) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/NOTICE.md +179 -3
  3. package/README.md +43 -11
  4. package/dist/arenas-entry.d.ts +4 -0
  5. package/dist/arenas-entry.js +3 -0
  6. package/dist/arenas.d.ts +125 -0
  7. package/dist/arenas.data.d.ts +2 -0
  8. package/dist/arenas.data.js +3763 -0
  9. package/dist/arenas.js +72 -0
  10. package/dist/atlas-entry.d.ts +3 -1
  11. package/dist/atlas-entry.js +3 -2
  12. package/dist/atlas.d.ts +37 -19
  13. package/dist/atlas.js +92 -7
  14. package/dist/background.js +2 -1
  15. package/dist/budo/describe.d.ts +13 -0
  16. package/dist/budo/describe.js +120 -0
  17. package/dist/budo/dojo.api.d.ts +64 -0
  18. package/dist/budo/dojo.api.js +46 -0
  19. package/dist/budo/dojo.d.ts +72 -0
  20. package/dist/budo/dojo.js +300 -0
  21. package/dist/budo/judo.d.ts +61 -0
  22. package/dist/budo/judo.js +109 -0
  23. package/dist/budo/karate.d.ts +29 -0
  24. package/dist/budo/karate.js +80 -0
  25. package/dist/budo/kendo.d.ts +18 -0
  26. package/dist/budo/kendo.js +44 -0
  27. package/dist/budo/kimarite.d.ts +19 -0
  28. package/dist/budo/kimarite.js +33 -0
  29. package/dist/budo/kit.d.ts +16 -0
  30. package/dist/budo/kit.js +24 -0
  31. package/dist/budo/kyudo.d.ts +28 -0
  32. package/dist/budo/kyudo.js +66 -0
  33. package/dist/budo/looks.d.ts +28 -0
  34. package/dist/budo/looks.js +58 -0
  35. package/dist/budo/parts.d.ts +32 -0
  36. package/dist/budo/parts.js +10 -0
  37. package/dist/budo/plan.d.ts +64 -0
  38. package/dist/budo/plan.js +139 -0
  39. package/dist/budo/room.d.ts +133 -0
  40. package/dist/budo/room.js +111 -0
  41. package/dist/budo/sumo.d.ts +31 -0
  42. package/dist/budo/sumo.js +71 -0
  43. package/dist/budo/tatami.d.ts +55 -0
  44. package/dist/budo/tatami.js +156 -0
  45. package/dist/budo/types.d.ts +192 -0
  46. package/dist/budo/types.js +2 -0
  47. package/dist/budo-entry.d.ts +22 -0
  48. package/dist/budo-entry.js +13 -0
  49. package/dist/budo.board.d.ts +28 -0
  50. package/dist/budo.board.js +81 -0
  51. package/dist/budo.score.d.ts +18 -0
  52. package/dist/budo.score.js +52 -0
  53. package/dist/budo.style.d.ts +6 -0
  54. package/dist/budo.style.js +67 -0
  55. package/dist/charts.baseball.js +1 -1
  56. package/dist/charts.js +4 -4
  57. package/dist/charts.soccer.js +3 -3
  58. package/dist/charts.types.d.ts +10 -0
  59. package/dist/colour.d.ts +4 -4
  60. package/dist/colour.js +4 -4
  61. package/dist/crowd.d.ts +11 -1
  62. package/dist/crowd.draw.js +15 -9
  63. package/dist/crowd.js +17 -2
  64. package/dist/draw.js +156 -31
  65. package/dist/element-entry.d.ts +2 -0
  66. package/dist/element-entry.js +1 -0
  67. package/dist/element.d.ts +2 -2
  68. package/dist/element.js +2 -1
  69. package/dist/emblems-source.d.ts +50 -0
  70. package/dist/emblems-source.js +56 -0
  71. package/dist/eras.d.ts +3 -1
  72. package/dist/eras.js +8 -3
  73. package/dist/features.js +73 -0
  74. package/dist/field.data.d.ts +16 -0
  75. package/dist/field.data.js +50 -0
  76. package/dist/fields.kit.d.ts +2 -2
  77. package/dist/fields.types.d.ts +9 -3
  78. package/dist/formations.js +18 -7
  79. package/dist/formations.sports.d.ts +8 -0
  80. package/dist/formations.sports.js +251 -0
  81. package/dist/frames.providers.js +9 -1
  82. package/dist/halls.d.ts +65 -0
  83. package/dist/halls.js +122 -0
  84. package/dist/heat.js +17 -10
  85. package/dist/heat.zones.d.ts +4 -2
  86. package/dist/heat.zones.js +55 -3
  87. package/dist/homeside.js +7 -0
  88. package/dist/index.d.ts +15 -3
  89. package/dist/index.js +8 -1
  90. package/dist/layers-entry.d.ts +2 -1
  91. package/dist/layers-entry.js +1 -0
  92. package/dist/layers.d.ts +2 -1
  93. package/dist/layers.env.d.ts +10 -1
  94. package/dist/layers.env.js +33 -3
  95. package/dist/layers.js +30 -11
  96. package/dist/layers.types.d.ts +9 -2
  97. package/dist/leagues/budo.d.ts +196 -0
  98. package/dist/leagues/budo.js +105 -0
  99. package/dist/leagues/hockey.js +41 -19
  100. package/dist/leagues/sources.js +21 -0
  101. package/dist/leagues/types.d.ts +1 -1
  102. package/dist/leagues.d.ts +4 -1
  103. package/dist/leagues.js +102 -2
  104. package/dist/lineup.codes.d.ts +23 -0
  105. package/dist/lineup.codes.js +122 -0
  106. package/dist/logo.d.ts +25 -0
  107. package/dist/logo.js +47 -0
  108. package/dist/outline.d.ts +1 -1
  109. package/dist/outline.js +17 -3
  110. package/dist/pageant.js +7 -3
  111. package/dist/pointer-entry.d.ts +2 -0
  112. package/dist/pointer-entry.js +2 -0
  113. package/dist/pointer.d.ts +113 -0
  114. package/dist/pointer.js +151 -0
  115. package/dist/pool.d.ts +7 -7
  116. package/dist/pool.figures.d.ts +57 -55
  117. package/dist/pool.figures.js +59 -55
  118. package/dist/pool.js +7 -7
  119. package/dist/pool.labels.js +2 -1
  120. package/dist/ranges.names.js +6 -0
  121. package/dist/road.draw.js +3 -2
  122. package/dist/road.js +2 -1
  123. package/dist/road.types.d.ts +3 -0
  124. package/dist/rules.d.ts +138 -0
  125. package/dist/rules.js +511 -0
  126. package/dist/rules.types.d.ts +4 -0
  127. package/dist/rules.types.js +64 -0
  128. package/dist/score.d.ts +3 -0
  129. package/dist/score.js +23 -1
  130. package/dist/situation.d.ts +135 -0
  131. package/dist/situation.draw.d.ts +34 -0
  132. package/dist/situation.draw.js +202 -0
  133. package/dist/situation.js +337 -0
  134. package/dist/situation.style.d.ts +2 -0
  135. package/dist/situation.style.js +45 -0
  136. package/dist/situation.types.d.ts +77 -0
  137. package/dist/situation.types.js +3 -0
  138. package/dist/situation.widgets.d.ts +39 -0
  139. package/dist/situation.widgets.js +129 -0
  140. package/dist/strings.d.ts +175 -1
  141. package/dist/strings.js +364 -20
  142. package/dist/style.js +9 -2
  143. package/dist/surfaces/artistic.d.ts +3 -3
  144. package/dist/surfaces/artistic.js +3 -3
  145. package/dist/surfaces/baseball.js +2 -0
  146. package/dist/surfaces/basketball.js +1 -1
  147. package/dist/surfaces/budo.d.ts +6 -0
  148. package/dist/surfaces/budo.js +104 -0
  149. package/dist/surfaces/dive.d.ts +3 -3
  150. package/dist/surfaces/dive.js +3 -3
  151. package/dist/surfaces/football.js +7 -0
  152. package/dist/surfaces/hockey.js +3 -2
  153. package/dist/surfaces/polo.d.ts +4 -4
  154. package/dist/surfaces/polo.js +5 -5
  155. package/dist/surfaces/pool.d.ts +1 -1
  156. package/dist/surfaces/pool.js +3 -3
  157. package/dist/surfaces/surface.d.ts +32 -0
  158. package/dist/surfaces/venue.d.ts +1 -1
  159. package/dist/surfaces/venue.js +1 -1
  160. package/dist/target-entry.d.ts +2 -0
  161. package/dist/target-entry.js +2 -0
  162. package/dist/target.d.ts +200 -0
  163. package/dist/target.js +525 -0
  164. package/dist/team.common.d.ts +1 -1
  165. package/dist/team.common.js +3 -3
  166. package/dist/team.formation.d.ts +3 -1
  167. package/dist/team.formation.js +3 -3
  168. package/dist/team.layers.d.ts +2 -2
  169. package/dist/team.layers.js +4 -1
  170. package/dist/team.lineup.d.ts +27 -0
  171. package/dist/team.lineup.js +207 -0
  172. package/dist/team.network.js +6 -5
  173. package/dist/team.shape.js +5 -5
  174. package/dist/team.types.d.ts +40 -1
  175. package/dist/tenants.data.js +848 -0
  176. package/dist/turn-entry.d.ts +2 -2
  177. package/dist/turn-entry.js +1 -1
  178. package/dist/turn.camera.d.ts +16 -3
  179. package/dist/turn.camera.js +53 -7
  180. package/dist/turn.dom.js +11 -9
  181. package/dist/turn.kit.d.ts +2 -2
  182. package/dist/turn.style.js +4 -3
  183. package/dist/types.d.ts +43 -7
  184. package/dist/units.js +7 -0
  185. package/dist/uprights.budo.d.ts +17 -0
  186. package/dist/uprights.budo.js +87 -0
  187. package/dist/uprights.js +10 -1
  188. package/dist/uprights.pools.js +4 -4
  189. package/dist/version.d.ts +1 -1
  190. package/dist/version.js +1 -1
  191. package/dist/zones-entry.d.ts +2 -0
  192. package/dist/zones-entry.js +2 -0
  193. package/package.json +39 -3
@@ -0,0 +1,337 @@
1
+ // A game's situation read from the options: hockey's skaters and shootout, baseball's bases, outs and count, football's down, distance and drives. Everything is decided here (which
2
+ // team attacks, which end, what the words say), so that the drawing (`situation.draw.ts`, `situation.badge.ts`) and the description do not decide it twice. A value that is not what
3
+ // it should be (a fourth down on the CFL's field, 7 skaters, a clock that is not a time) is left out, as a value from a page's markup is anywhere else; nothing here throws.
4
+ import { attackingIn } from "./frames.js";
5
+ import { KYOGIJO_STRINGS, fill } from "./strings.js";
6
+ /** The most rounds of a shootout that are drawn. */
7
+ export const SHOOTOUT_MAX = 12;
8
+ /** The rounds that are regular: the rounds after them are sudden death. */
9
+ export const SHOOTOUT_REGULAR = 3;
10
+ /** The most drives drawn in a drive chart. */
11
+ export const DRIVES_MAX = 24;
12
+ /** The red zone: the yards before the goal line the offence attacks. */
13
+ export const RED_ZONE_YARDS = 20;
14
+ /** The yards to a first down. */
15
+ export const FIRST_DOWN_YARDS = 10;
16
+ /** The short code each drive result is written with at the head of its arrow. */
17
+ export const DRIVE_CODES = Object.freeze({
18
+ touchdown: "TD",
19
+ "field-goal": "FG",
20
+ "missed-field-goal": "MFG",
21
+ punt: "PUNT",
22
+ turnover: "TO",
23
+ interception: "INT",
24
+ fumble: "FUM",
25
+ downs: "DOWNS",
26
+ safety: "SAF",
27
+ "end-of-half": "END",
28
+ });
29
+ const RESULTS = Object.keys(DRIVE_CODES);
30
+ /** The hockey leagues whose rulebook calls the shootout the game winning shots (GWS): the IIHF's and the Olympic rink's. The Japanese leagues play to the IIHF's rules, but none is a league of its own here. */
31
+ const GWS_LEAGUES = ["iihf", "olympic"];
32
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
33
+ const flip = (side) => (side === "left" ? "right" : "left");
34
+ const other = (team) => (team === "home" ? "away" : "home");
35
+ /** A whole number from `lo` to `hi`, else `undefined`. */
36
+ function whole(value, lo, hi) {
37
+ return typeof value === "number" && Number.isInteger(value) && value >= lo && value <= hi ? value : undefined;
38
+ }
39
+ const team = (value) => (value === "home" || value === "away" ? value : undefined);
40
+ /** The end a team attacks: the one the `direction` options say (in the period asked for), else the one opposite the team's own side. */
41
+ function headingOf(role, ctx) {
42
+ const own = (role === "home") === (ctx.homeSide === "left") ? "left" : "right";
43
+ const d = ctx.direction;
44
+ if (d === undefined)
45
+ return flip(own);
46
+ const named = d[role] === "left" || d[role] === "right" ? d[role] : d[other(role)] === "left" || d[other(role)] === "right" ? flip(d[other(role)]) : undefined;
47
+ if (named === undefined)
48
+ return flip(own);
49
+ if (d.period === undefined)
50
+ return named;
51
+ try {
52
+ return attackingIn(ctx.sport, d.period, named, d.changedAfter);
53
+ }
54
+ catch {
55
+ return named;
56
+ }
57
+ }
58
+ /** `m:ss`, `m:ss.s`, or a whole number of seconds written as `m:ss`; anything else is left out. */
59
+ export function clockText(value) {
60
+ if (typeof value === "number") {
61
+ if (!Number.isFinite(value) || value < 0 || value >= 6000)
62
+ return undefined;
63
+ const s = Math.floor(value);
64
+ return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
65
+ }
66
+ return typeof value === "string" && /^\d{1,2}:[0-5]\d(?:\.\d)?$/.test(value.trim()) ? value.trim() : undefined;
67
+ }
68
+ function readHockey(s, ctx) {
69
+ const away = whole(s.awaySkaters, 3, 6);
70
+ const home = whole(s.homeSkaters, 3, 6);
71
+ const clock = clockText(s.clock);
72
+ const more = away !== undefined && home !== undefined && away !== home ? (away > home ? "away" : "home") : undefined;
73
+ const pulled = s.emptyNet === true ? more : team(s.emptyNet);
74
+ const delayed = team(s.delayedPenalty);
75
+ const rounds = isObject(s.shootout) && Array.isArray(s.shootout.rounds) ? (s.shootout.rounds) : undefined;
76
+ const base = { kind: "hockey", away, home, powerPlay: false, gws: ctx.league !== undefined && GWS_LEAGUES.includes(ctx.league) };
77
+ if (rounds !== undefined) {
78
+ const read = rounds.slice(0, SHOOTOUT_MAX).map((round) => {
79
+ const pair = Array.isArray(round) ? round : [];
80
+ return [typeof pair[0] === "boolean" ? pair[0] : null, typeof pair[1] === "boolean" ? pair[1] : null];
81
+ });
82
+ while (read.length > 0 && read[read.length - 1][0] === null && read[read.length - 1][1] === null)
83
+ read.pop();
84
+ if (read.length === 0)
85
+ return undefined;
86
+ const goals = [read.filter((r) => r[0] === true).length, read.filter((r) => r[1] === true).length];
87
+ return { ...base, shootout: { rounds: read, goals, sudden: Math.max(0, read.length - SHOOTOUT_REGULAR) } };
88
+ }
89
+ // The team that attacks: the one with the goalkeeper pulled, else the one with more skaters, else the one that has the puck in a delayed penalty.
90
+ const attacker = pulled ?? more ?? (delayed === undefined ? undefined : other(delayed));
91
+ if (away === undefined && home === undefined && pulled === undefined && delayed === undefined && clock === undefined)
92
+ return undefined;
93
+ // Five against five and nothing else to say: there is nothing to draw.
94
+ if (away === 5 && home === 5 && pulled === undefined && delayed === undefined && clock === undefined)
95
+ return undefined;
96
+ const toward = s.toward === "left" || s.toward === "right" ? s.toward : attacker === undefined ? undefined : headingOf(attacker, ctx);
97
+ const strength = away !== undefined && home !== undefined ? (attacker === "home" ? [home, away] : attacker === "away" ? [away, home] : [away, home]) : undefined;
98
+ return { ...base, attacker, toward, strength, powerPlay: more !== undefined, emptyNet: pulled, delayed, clock };
99
+ }
100
+ function readBaseball(s) {
101
+ const runners = Array.isArray(s.bases) && s.bases.length >= 3 ? [Boolean(s.bases[0]), Boolean(s.bases[1]), Boolean(s.bases[2])] : undefined;
102
+ const read = {
103
+ kind: "baseball",
104
+ bases: runners,
105
+ outs: whole(s.outs, 0, 3),
106
+ inning: whole(s.inning, 1, 99),
107
+ half: s.half === "top" || s.half === "bottom" ? s.half : undefined,
108
+ balls: whole(s.balls, 0, 3),
109
+ strikes: whole(s.strikes, 0, 2),
110
+ };
111
+ return Object.values(read).some((value, i) => i > 0 && value !== undefined) ? read : undefined;
112
+ }
113
+ function readFootball(s, ctx) {
114
+ const yards = ctx.football?.yards ?? 100;
115
+ const downs = ctx.football?.downs ?? 4;
116
+ const number = (value, lo, hi) => (typeof value === "number" && Number.isFinite(value) && value >= lo && value <= hi ? Math.round(value * 10) / 10 : undefined);
117
+ const possession = team(s.possession);
118
+ const ballOn = number(s.ballOn, 0, yards);
119
+ const toGo = s.toGo === "goal" ? "goal" : number(s.toGo, 0.1, yards);
120
+ const down = whole(s.down, 1, downs);
121
+ const offence = possession ?? "home";
122
+ const toward = s.toward === "left" || s.toward === "right" ? s.toward : headingOf(offence, ctx);
123
+ // The red zone: the ball is in it unless the options say it is not; `redZone: true` marks it with no ball.
124
+ const inZone = ballOn !== undefined && ballOn >= yards - RED_ZONE_YARDS;
125
+ const redZone = s.redZone === true || (s.redZone !== false && inZone);
126
+ const drive = [];
127
+ if (Array.isArray(s.drive)) {
128
+ for (const leg of s.drive.slice(0, DRIVES_MAX)) {
129
+ if (!isObject(leg))
130
+ continue;
131
+ const from = number(leg.from, 0, yards);
132
+ const to = number(leg.to, 0, yards);
133
+ if (from === undefined || to === undefined)
134
+ continue;
135
+ const who = team(leg.team) ?? offence;
136
+ // A team heads for the end the offence heads for, or for the other.
137
+ drive.push({ team: who, from, to, result: RESULTS.includes(leg.result) ? leg.result : undefined, toward: who === offence ? toward : flip(toward) });
138
+ }
139
+ }
140
+ const read = { kind: "football", yards, downs, ballOn, toGo, down, possession, toward, redZone, drive };
141
+ return ballOn === undefined && toGo === undefined && down === undefined && possession === undefined && !redZone && drive.length === 0 ? undefined : read;
142
+ }
143
+ /**
144
+ * The situation a drawing's options ask for, decided: which team attacks and which end, what is lit. `undefined` when the options have none, when the sport has no situation (only
145
+ * hockey, baseball and football do), or when nothing in it is a value the sport can have.
146
+ */
147
+ export function readSituation(situation, ctx) {
148
+ if (!isObject(situation))
149
+ return undefined;
150
+ const s = situation;
151
+ switch (ctx.sport) {
152
+ case "nhl":
153
+ return readHockey(s, ctx);
154
+ case "mlb":
155
+ return readBaseball(s);
156
+ case "nfl":
157
+ case "cfl":
158
+ case "ifaf":
159
+ return readFootball(s, ctx);
160
+ default:
161
+ return undefined;
162
+ }
163
+ }
164
+ // ---- Words. ----
165
+ const words = (language) => KYOGIJO_STRINGS[language].situation;
166
+ /** The ordinal of an inning in English (1st, 2nd, 3rd, 11th); in Japanese the number. */
167
+ function nth(n, language) {
168
+ if (language === "ja")
169
+ return String(n);
170
+ const tens = n % 100;
171
+ if (tens >= 11 && tens <= 13)
172
+ return `${n}th`;
173
+ return `${n}${["th", "st", "nd", "rd"][n % 10 < 4 ? n % 10 : 0]}`;
174
+ }
175
+ /** A yard line as the tag writes it: `OWN 35`, `OPP 12`, or the middle of the field. */
176
+ export function yardLineText(ballOn, yards, language) {
177
+ const w = words(language);
178
+ const n = Math.round(ballOn);
179
+ const mid = yards / 2;
180
+ if (ballOn === mid)
181
+ return String(mid);
182
+ return ballOn < mid ? fill(w.tagOwn, { n }) : fill(w.tagOpp, { n: Math.round(yards - ballOn) });
183
+ }
184
+ /** A yard line as a sentence writes it: `own 35`, `opponent's 12`, `midfield`. */
185
+ function yardPlace(ballOn, yards, language) {
186
+ const w = words(language);
187
+ const mid = yards / 2;
188
+ if (ballOn === mid)
189
+ return w.placeMid;
190
+ return ballOn < mid ? fill(w.placeOwn, { n: Math.round(ballOn) }) : fill(w.placeOpp, { n: Math.round(yards - ballOn) });
191
+ }
192
+ /** The down and distance as the tag writes it (`3rd & 7`, `1st & Goal`), or `undefined` when there is no down. */
193
+ export function downTag(read, language) {
194
+ if (read.down === undefined)
195
+ return undefined;
196
+ const w = words(language);
197
+ const ord = w.downs[read.down - 1];
198
+ // The goal is nearer than the first-down line: asked for, or the distance reaches the goal line.
199
+ const goal = read.toGo === "goal" || (read.ballOn !== undefined && typeof read.toGo === "number" && read.ballOn + read.toGo >= read.yards);
200
+ if (goal)
201
+ return fill(w.tagGoal, { ord });
202
+ return typeof read.toGo === "number" ? fill(w.tag, { ord, n: formatYards(read.toGo) }) : ord;
203
+ }
204
+ const formatYards = (n) => (Number.isInteger(n) ? String(n) : n.toFixed(1));
205
+ /** The baseball situation as a short label: `Top 7 · 2 out · 3-2`. */
206
+ function baseballTag(read, language) {
207
+ const w = words(language);
208
+ const parts = [];
209
+ if (read.inning !== undefined)
210
+ parts.push(language === "ja" ? `${read.inning}回${read.half === undefined ? "" : w[read.half]}` : `${read.half === undefined ? "" : `${w[read.half]} `}${read.inning}`);
211
+ else if (read.half !== undefined)
212
+ parts.push(w[read.half]);
213
+ if (read.outs !== undefined)
214
+ parts.push(language === "ja" ? (read.outs === 0 ? "ノーアウト" : `${read.outs}アウト`) : `${read.outs} out`);
215
+ if (read.balls !== undefined && read.strikes !== undefined)
216
+ parts.push(`${read.balls}-${read.strikes}`);
217
+ return parts.join(" · ");
218
+ }
219
+ /** The strength of a hockey situation on the pill: `5v4 PP`, `6v5 EN`, `4v4`, `DEL PEN`, with the clock after it; `undefined` when there is nothing to say (5 against 5). */
220
+ export function hockeyPill(read, language) {
221
+ const w = words(language);
222
+ if (read.shootout !== undefined)
223
+ return `${read.gws ? w.gws : w.shootout} ${read.shootout.goals[0]}–${read.shootout.goals[1]}`;
224
+ const parts = [];
225
+ const strength = read.strength;
226
+ const plain = strength !== undefined && strength[0] === 5 && strength[1] === 5;
227
+ if (strength !== undefined && !(plain && read.emptyNet === undefined && read.delayed === undefined && read.clock === undefined))
228
+ parts.push(`${strength[0]}v${strength[1]}`);
229
+ if (read.powerPlay)
230
+ parts.push(w.pp);
231
+ if (read.emptyNet !== undefined)
232
+ parts.push(w.en);
233
+ if (read.delayed !== undefined)
234
+ parts.push(w.delayed);
235
+ if (read.clock !== undefined)
236
+ parts.push(read.clock);
237
+ return parts.length === 0 ? undefined : parts.join(" ");
238
+ }
239
+ /**
240
+ * The short label of a situation as the drawing writes it: hockey's `5v4 PP 1:42`, baseball's `Top 7 · 2 out · 3-2`, football's `3rd & 7`. `undefined` when there is none to write.
241
+ * @example
242
+ * ```ts
243
+ * import { situationText } from "@johnmorrisdotca/kyogijo-stadiums";
244
+ * console.log(situationText({ sport: "nhl", situation: { awaySkaters: 5, homeSkaters: 4, clock: "1:42" } }));
245
+ * console.log(situationText({ sport: "mlb", situation: { inning: 7, half: "top", outs: 2, balls: 3, strikes: 2 } }));
246
+ * console.log(situationText({ sport: "nfl", situation: { ballOn: 88, down: 3, toGo: 7 } }));
247
+ * // 5v4 PP 1:42
248
+ * // Top 7 · 2 out · 3-2
249
+ * // 3rd & 7 · OPP 12
250
+ * ```
251
+ */
252
+ export function situationText(options, language = options.language === "ja" ? "ja" : "en") {
253
+ const read = readSituation(options.situation, contextOf(options));
254
+ return read === undefined ? undefined : tagOf(read, language);
255
+ }
256
+ /** The reading context of options that name no teams: the sport's own football field, the side the home team stands on by default. */
257
+ function contextOf(options) {
258
+ const football = options.sport === "cfl" ? { yards: 110, downs: 3 } : options.sport === "nfl" || options.sport === "ifaf" ? { yards: 100, downs: 4 } : undefined;
259
+ return { sport: options.sport, homeSide: options.homeSide ?? "right", direction: options.direction, football, league: options.league };
260
+ }
261
+ /** The short label of a reading, in a language. */
262
+ export function tagOf(read, language) {
263
+ if (read.kind === "hockey")
264
+ return hockeyPill(read, language);
265
+ if (read.kind === "baseball") {
266
+ const tag = baseballTag(read, language);
267
+ return tag === "" ? undefined : tag;
268
+ }
269
+ const down = downTag(read, language);
270
+ const where = read.ballOn === undefined ? undefined : yardLineText(read.ballOn, read.yards, language);
271
+ const parts = [down, where].filter((part) => part !== undefined);
272
+ return parts.length === 0 ? undefined : parts.join(" · ");
273
+ }
274
+ /** The sentences a description adds for a situation, each beginning with a capital. */
275
+ export function situationSentences(read, language, names) {
276
+ return sentencesOf(read, language, names).map((line) => line.charAt(0).toLocaleUpperCase("en") + line.slice(1));
277
+ }
278
+ function sentencesOf(read, language, names) {
279
+ const w = words(language);
280
+ const out = [];
281
+ const name = (t) => names?.[t] ?? w[t];
282
+ if (read.kind === "hockey") {
283
+ if (read.shootout !== undefined) {
284
+ const { rounds, goals, sudden } = read.shootout;
285
+ out.push(fill(read.gws ? w.gwsScore : w.shootoutScore, { n: rounds.length, away: name("away"), awayGoals: goals[0], home: name("home"), homeGoals: goals[1] }));
286
+ if (sudden > 0)
287
+ out.push(w.suddenDeath);
288
+ return out;
289
+ }
290
+ const [a, b] = read.strength ?? [0, 0];
291
+ if (read.strength !== undefined && read.powerPlay && read.attacker !== undefined)
292
+ out.push(fill(w.powerPlay, { team: name(read.attacker), a, b }));
293
+ else if (read.strength !== undefined && !(a === 5 && b === 5))
294
+ out.push(fill(w.strength, { a, b }));
295
+ if (read.clock !== undefined)
296
+ out.push(fill(w.penaltyTime, { clock: read.clock }));
297
+ if (read.emptyNet !== undefined)
298
+ out.push(fill(w.emptyNet, { team: name(read.emptyNet) }));
299
+ if (read.delayed !== undefined)
300
+ out.push(fill(w.delayedPenalty, { team: name(read.delayed) }));
301
+ return out;
302
+ }
303
+ if (read.kind === "baseball") {
304
+ const half = read.half === undefined ? undefined : w[read.half];
305
+ if (read.inning !== undefined && half !== undefined)
306
+ out.push(fill(w.inning, { half, n: read.inning, nth: nth(read.inning, language) }));
307
+ else if (read.inning !== undefined)
308
+ out.push(fill(w.inningOnly, { n: read.inning, nth: nth(read.inning, language) }));
309
+ else if (half !== undefined)
310
+ out.push(fill(w.halfOnly, { half }));
311
+ if (read.outs !== undefined)
312
+ out.push(read.outs === 0 ? w.noOuts : read.outs === 1 ? w.oneOut : fill(w.outs, { n: read.outs }));
313
+ if (read.bases !== undefined) {
314
+ const on = read.bases.flatMap((runner, i) => (runner ? [w.bases[i]] : []));
315
+ out.push(on.length === 0 ? w.basesEmpty : on.length === 3 ? w.basesLoaded : fill(w.runnersOn, { list: on.join(w.and) }));
316
+ }
317
+ if (read.balls !== undefined && read.strikes !== undefined)
318
+ out.push(fill(w.count, { b: read.balls, s: read.strikes }));
319
+ return out;
320
+ }
321
+ if (read.possession !== undefined)
322
+ out.push(fill(w.possession, { team: name(read.possession) }));
323
+ if (read.down !== undefined) {
324
+ const ord = w.downs[read.down - 1];
325
+ const goal = read.toGo === "goal" || (read.ballOn !== undefined && typeof read.toGo === "number" && read.ballOn + read.toGo >= read.yards);
326
+ out.push(goal ? fill(w.downGoal, { ord }) : typeof read.toGo === "number" ? fill(w.downDistance, { ord, n: formatYards(read.toGo) }) : fill(w.down, { ord }));
327
+ }
328
+ if (read.ballOn !== undefined)
329
+ out.push(read.ballOn === read.yards / 2 ? w.ballMid : read.ballOn < read.yards / 2 ? fill(w.ballOwn, { n: Math.round(read.ballOn) }) : fill(w.ballOpp, { n: Math.round(read.yards - read.ballOn) }));
330
+ if (read.redZone)
331
+ out.push(w.redZone);
332
+ for (const leg of read.drive) {
333
+ const at = { team: name(leg.team), from: yardPlace(leg.from, read.yards, language), to: yardPlace(leg.to, read.yards, language) };
334
+ out.push(leg.result === undefined ? fill(w.driveNoResult, at) : fill(w.driveLine, { ...at, result: w.results[leg.result] }));
335
+ }
336
+ return out;
337
+ }
@@ -0,0 +1,2 @@
1
+ /** The situation's colours and shapes. */
2
+ export declare const KYOGIJO_SITUATION_STYLE: string;
@@ -0,0 +1,45 @@
1
+ // A situation's rules: the pill, the scorebug and the tag on the score panel's colours, the lit bases and the attacking zone in amber (or the attacking team's colour), football's blue
2
+ // line of scrimmage, yellow first-down line and red zone, and the colours of a drive's arrows. The custom properties (`--ky-sit-on`, `--ky-sit-los`, `--ky-sit-fd`, `--ky-sit-red`) are
3
+ // the ones a page can set; the dark ones are the dark theme's. A drawing with a situation writes these in; `KYOGIJO_STYLE` has them for a page that draws with `style: "none"`.
4
+ /** The situation's colours and shapes. */
5
+ export const KYOGIJO_SITUATION_STYLE = `
6
+ :where(.kyogijo){--ky-sit-on:#f2b705;--ky-sit-los:#1f6fe0;--ky-sit-fd:#f5d90a;--ky-sit-red:#d6322a;--ky-sit-halo:rgba(8,12,16,.72);--ky-sit-td:#2f9e5b;--ky-sit-fg:#7bbf3a;--ky-sit-lose:#d6453d;--ky-sit-neutral:#9aa4ab}
7
+ .kyogijo .ky-sit-bg{fill:var(--ky-plate)}
8
+ .kyogijo .ky-badge .ky-sit-bg{stroke:var(--ky-plate-ink);stroke-opacity:.35;stroke-width:1}
9
+ .kyogijo .ky-sit-t,.kyogijo .ky-sit-lab,.kyogijo .ky-sit-sdw{fill:var(--ky-plate-ink)}
10
+ .kyogijo .ky-sit-sub,.kyogijo .ky-sit-sdw{opacity:.82}
11
+ .kyogijo .ky-sit-lab{font-weight:700;opacity:.9}
12
+ .kyogijo .ky-sit-dot-left{fill:var(--ky-left)}
13
+ .kyogijo .ky-sit-dot-right{fill:var(--ky-right)}
14
+ .kyogijo .ky-sit-dot{stroke:var(--ky-plate-ink);stroke-opacity:.8}
15
+ .kyogijo .ky-sit-zone{fill:var(--ky-sit-on);fill-opacity:.2}
16
+ .kyogijo .ky-sit-zone-left{fill:var(--ky-left);fill-opacity:.2}
17
+ .kyogijo .ky-sit-zone-right{fill:var(--ky-right);fill-opacity:.2}
18
+ .kyogijo .ky-sit-net{fill:none;stroke:var(--ky-sit-lose);stroke-width:.7;stroke-dasharray:1.6 1}
19
+ .kyogijo .ky-sit-goal{fill:var(--ky-sit-on)}
20
+ .kyogijo .ky-sit-miss{fill:none;stroke:var(--ky-plate-ink);stroke-opacity:.85}
21
+ .kyogijo .ky-sit-wait{fill:var(--ky-plate-ink);fill-opacity:.35}
22
+ .kyogijo .ky-sit-sd{stroke-opacity:1}
23
+ .kyogijo .ky-sit-goal.ky-sit-sd{stroke:var(--ky-plate-ink)}
24
+ .kyogijo .ky-sit-split{stroke:var(--ky-plate-ink);stroke-opacity:.4;stroke-dasharray:.5 .5}
25
+ .kyogijo .ky-sit-dia{stroke:var(--ky-plate-ink);stroke-opacity:.55}
26
+ .kyogijo .ky-sit-base{fill:var(--ky-plate-ink);fill-opacity:.24}
27
+ .kyogijo .ky-sit-base.ky-sit-on{fill:var(--ky-sit-on);fill-opacity:1;stroke:var(--ky-plate-ink)}
28
+ .kyogijo .ky-sit-out{fill:var(--ky-plate-ink);fill-opacity:.24}
29
+ .kyogijo .ky-sit-out.ky-sit-on{fill:var(--ky-sit-lose);fill-opacity:1}
30
+ .kyogijo .ky-sit-arrow{fill:var(--ky-plate-ink)}
31
+ .kyogijo .ky-sit-lit{fill:var(--ky-sit-on);stroke:var(--ky-line);stroke-width:.7;stroke-linejoin:round;fill-opacity:.95}
32
+ .kyogijo .ky-sit-red{fill:var(--ky-sit-red);fill-opacity:.24}
33
+ .kyogijo .ky-sit-los-halo,.kyogijo .ky-sit-fd-halo,.kyogijo .ky-sit-dr-halo{stroke:var(--ky-sit-halo);stroke-width:3;stroke-linecap:round}
34
+ .kyogijo .ky-sit-dr-halo{stroke-width:2.8}
35
+ .kyogijo .ky-sit-los{stroke:var(--ky-sit-los);stroke-width:1.5}
36
+ .kyogijo .ky-sit-fd{stroke:var(--ky-sit-fd);stroke-width:1.5}
37
+ .kyogijo .ky-sit-dr{stroke:var(--ky-sit-neutral);stroke-width:1.3;stroke-linecap:round}
38
+ .kyogijo .ky-sit-dot-s,.kyogijo .ky-sit-dh{fill:var(--ky-sit-neutral);stroke:var(--ky-sit-halo);stroke-width:.4}
39
+ .kyogijo .ky-sit-d-touchdown{stroke:var(--ky-sit-td);fill:var(--ky-sit-td)}
40
+ .kyogijo .ky-sit-d-field-goal{stroke:var(--ky-sit-fg);fill:var(--ky-sit-fg)}
41
+ .kyogijo .ky-sit-d-missed-field-goal,.kyogijo .ky-sit-d-downs{stroke:#e8862a;fill:#e8862a}
42
+ .kyogijo .ky-sit-d-turnover,.kyogijo .ky-sit-d-interception,.kyogijo .ky-sit-d-fumble{stroke:var(--ky-sit-lose);fill:var(--ky-sit-lose)}
43
+ .kyogijo .ky-sit-dl{fill:var(--ky-line);stroke:var(--ky-sit-halo);stroke-width:.3em;paint-order:stroke;stroke-linejoin:round}
44
+ @media (prefers-color-scheme:dark){:where(html:not([data-theme=light]) .kyogijo:not([data-theme=light])){--ky-sit-on:#ffc933;--ky-sit-los:#4b95ff;--ky-sit-red:#ff6a5e}}
45
+ :where(html[data-theme=dark] .kyogijo,.kyogijo[data-theme=dark]){--ky-sit-on:#ffc933;--ky-sit-los:#4b95ff;--ky-sit-red:#ff6a5e}`.trim();
@@ -0,0 +1,77 @@
1
+ /** A team of the game: the home team or the visitors. */
2
+ export type SituationTeam = "home" | "away";
3
+ /** One shootout attempt: `true` a goal (●), `false` a miss or a save (○), `null` or left out one not yet taken. */
4
+ export type ShootoutAttempt = boolean | null | undefined;
5
+ /** One round of a shootout: the visitors' attempt and then the home team's, as the NHL's shootout is shot (the visitors shoot first). */
6
+ export type ShootoutRound = readonly [away: ShootoutAttempt, home: ShootoutAttempt];
7
+ /**
8
+ * Hockey's situation. The skaters are the players on the ice without the goalkeeper, 3 to 6 (a goalkeeper pulled for an extra attacker is a team with 6). A strength that is 5 against 5
9
+ * with nothing else to say draws nothing. See `docs/situation.md`.
10
+ */
11
+ export interface HockeySituation {
12
+ /** The visitors' skaters on the ice, a whole number from 3 to 6. */
13
+ awaySkaters?: number;
14
+ /** The home team's skaters on the ice. */
15
+ homeSkaters?: number;
16
+ /** The time left on the penalty (or the power play), as `m:ss` or `m:ss.s`, or a whole number of seconds. Written beside the strength. */
17
+ clock?: string | number;
18
+ /** A net with no goalkeeper in it: `"home"` or `"away"` for the team that has pulled its goalkeeper, or `true` for the team with more skaters than the other. */
19
+ emptyNet?: boolean | SituationTeam;
20
+ /** A penalty called and not yet taken (the play goes on until the penalised team touches the puck): the team that will be short, `"home"` or `"away"`. The other team has the puck, and is the one that attacks. */
21
+ delayedPenalty?: SituationTeam;
22
+ /** The shootout: one entry for each round, `[away, home]`. The first three rounds are the regular ones; the rounds after the third are sudden death. */
23
+ shootout?: {
24
+ rounds: readonly ShootoutRound[];
25
+ };
26
+ /** The end the attacking team is heading for, as the surface is drawn wide. Without it, read from `direction`, else the end opposite the attacking team's own side. */
27
+ toward?: "left" | "right";
28
+ }
29
+ /** Baseball's situation (softball and NPB too). */
30
+ export interface BaseballSituation {
31
+ /** The runners: `[first, second, third]`, `true` for a base with a runner on it. */
32
+ bases?: readonly [boolean, boolean, boolean] | readonly boolean[];
33
+ /** The outs, 0 to 3. */
34
+ outs?: number;
35
+ /** The inning, 1 or more. */
36
+ inning?: number;
37
+ /** `top` (the visitors bat) or `bottom` (the home team bats). */
38
+ half?: "top" | "bottom";
39
+ /** The count: balls 0 to 3 (four is a walk and is not a count) and strikes 0 to 2. */
40
+ balls?: number;
41
+ strikes?: number;
42
+ }
43
+ /** How a drive of a football game ended. */
44
+ export type DriveResult = "touchdown" | "field-goal" | "missed-field-goal" | "punt" | "turnover" | "interception" | "fumble" | "downs" | "safety" | "end-of-half";
45
+ /**
46
+ * One drive of a drive chart: where it started and where it ended, in yards from the team's own goal line (0 the goal line it defends, 100 the one it attacks; 110 on the CFL's field),
47
+ * and how it ended.
48
+ */
49
+ export interface DriveLeg {
50
+ /** The team that had the ball. The team in possession when missing. */
51
+ team?: SituationTeam;
52
+ /** Yards from the team's own goal line where it began. */
53
+ from: number;
54
+ /** Yards from the team's own goal line where it ended. */
55
+ to: number;
56
+ /** How it ended. */
57
+ result?: DriveResult;
58
+ }
59
+ /** Football's situation (NFL, CFL, IFAF and the leagues of other rulebooks). The CFL has three downs and a field of 110 yards with end zones of 20. */
60
+ export interface FootballSituation {
61
+ /** The ball, in yards from the offence's own goal line: 35 is its own 35, 88 the opponent's 12. 0 to 100 (0 to 110 on the CFL's field). */
62
+ ballOn?: number;
63
+ /** The yards to a first down, or `"goal"` when the goal line is nearer than the first-down line. */
64
+ toGo?: number | "goal";
65
+ /** The down, 1 to 4 (1 to 3 on the CFL's field). */
66
+ down?: number;
67
+ /** The team with the ball. */
68
+ possession?: SituationTeam;
69
+ /** Tint the red zone, the last 20 yards before the goal line the offence attacks. By default when the ball is in it; `false` leaves it out. */
70
+ redZone?: boolean;
71
+ /** A drive chart: the drives as arrows across the field, one to a lane. */
72
+ drive?: readonly DriveLeg[];
73
+ /** The end zone the offence is heading for, as the surface is drawn wide. Without it, read from `direction`, else the end opposite the offence's own side. */
74
+ toward?: "left" | "right";
75
+ }
76
+ /** A game's situation: the sport decides which of the three it is. */
77
+ export type Situation = HockeySituation | BaseballSituation | FootballSituation;
@@ -0,0 +1,3 @@
1
+ // What a game's situation is, as the options give it: hockey's skaters and shootout, baseball's bases, outs and count, football's down, distance and drives.
2
+ // The types only; `situation.ts` reads them and `situation.draw.ts` draws them.
3
+ export {};
@@ -0,0 +1,39 @@
1
+ /** A widget: its markup, centred on the origin, and the box it fills. */
2
+ export interface Widget {
3
+ markup: string;
4
+ w: number;
5
+ h: number;
6
+ }
7
+ /** A triangle pointing at `angle` degrees (0 right, 90 down) centred on a point, `r` from its middle to its tip. */
8
+ export declare const triangle: (cx: number, cy: number, r: number, angle: number, cls: string) => string;
9
+ /** The pill: a line of text on a rounded plate, with a dot of the benefiting team's colour at its head when `dot` names its side. */
10
+ export declare function pillWidget(label: string, size: number, dot?: "left" | "right"): Widget;
11
+ /** A football tag: the down and distance, with the yard line beneath it and an arrow for the way the offence is going (`angle`, 0 right, 90 down, 180 left, -90 up). */
12
+ export declare function tagWidget(main: string, sub: string | undefined, size: number, angle?: number): Widget;
13
+ /** What a scorebug shows; a part left out is not drawn. */
14
+ export interface Scorebug {
15
+ bases?: readonly [boolean, boolean, boolean];
16
+ outs?: number;
17
+ inning?: number;
18
+ half?: "top" | "bottom";
19
+ balls?: number;
20
+ strikes?: number;
21
+ }
22
+ /**
23
+ * The classic scorebug: a diamond whose occupied bases are lit, the outs as three dots under it, the inning with an arrow for its half, and the count. `size` is a letter's height;
24
+ * the diamond is 3.4 of them across. `bg` draws the plate behind it (a badge has its own).
25
+ */
26
+ export declare function scorebugWidget(s: Scorebug, size: number, bg: boolean): Widget;
27
+ /** What a shootout strip is given: the rounds as `[away, home]`, with `null` for an attempt not yet taken, the rounds after the third being sudden death, and the two rows' labels. */
28
+ export interface ShootoutStrip {
29
+ rounds: readonly (readonly [boolean | null, boolean | null])[];
30
+ goals: readonly [number, number];
31
+ sudden: number;
32
+ labels: readonly [string, string];
33
+ /** The two rows' sides on the drawing, for the colour of their labels. */
34
+ sides?: readonly ["left" | "right", "left" | "right"];
35
+ /** The short word over the sudden-death rounds. */
36
+ suddenWord: string;
37
+ }
38
+ /** The shootout as two rows of dots, a goal filled (●) and a miss hollow (○), the rounds not yet taken a faint speck, with each team's goals at the end. */
39
+ export declare function shootoutWidget(s: ShootoutStrip, size: number, bg: boolean): Widget;
@@ -0,0 +1,129 @@
1
+ // The small pictures a situation is told with, each drawn upright in a frame of its own whose origin is its middle: the pill of hockey's strength, the scorebug of baseball (the diamond
2
+ // with its runners, the outs, the inning and the count), the tag of football's down and distance, and the strip of a hockey shootout. `size` is the height of a letter in the frame's
3
+ // units, so a widget is as big on a surface (feet) as it is in a badge (pixels) when it is given the right size. The drawing places them (`situation.draw.ts`, `score.ts`).
4
+ import { el, esc, num } from "./format.js";
5
+ import { textWidth as measure } from "./text.js";
6
+ /** The width of a text in ems, with room to spare for a face wider than the estimate. */
7
+ const textWidth = (value) => measure(value) * 1.12;
8
+ const text = (cls, x, y, size, value, anchor = "middle") => el("text", { class: `ky-text ${cls}`, x, y: y + size * 0.36, "font-size": size, "text-anchor": anchor }, esc(value));
9
+ /** A rounded plate behind a widget. */
10
+ const plate = (w, h, cls = "ky-sit-bg") => el("rect", { class: cls, x: -w / 2, y: -h / 2, width: w, height: h, rx: Math.min(h / 2, h * 0.34) });
11
+ /** A triangle pointing at `angle` degrees (0 right, 90 down) centred on a point, `r` from its middle to its tip. */
12
+ export const triangle = (cx, cy, r, angle, cls) => {
13
+ const a = (angle * Math.PI) / 180;
14
+ const pts = [0, 140, 220].map((k) => {
15
+ const t = a + (k * Math.PI) / 180;
16
+ const rr = k === 0 ? r : r * 0.8;
17
+ return `${num(cx + rr * Math.cos(t))} ${num(cy + rr * Math.sin(t))}`;
18
+ });
19
+ return el("path", { class: cls, d: `M${pts.join("L")}Z` });
20
+ };
21
+ /** The pill: a line of text on a rounded plate, with a dot of the benefiting team's colour at its head when `dot` names its side. */
22
+ export function pillWidget(label, size, dot) {
23
+ const h = size * 1.6;
24
+ const dotW = dot === undefined ? 0 : size * 1.15;
25
+ const w = textWidth(label) * size + size * 1.4 + dotW;
26
+ let m = plate(w, h);
27
+ if (dot !== undefined)
28
+ m += el("circle", { class: `ky-sit-dot ky-sit-dot-${dot}`, cx: -w / 2 + size * 0.95, cy: 0, r: size * 0.4, "stroke-width": size * 0.14 });
29
+ m += text("ky-sit-t", dotW / 2, 0, size, label);
30
+ return { markup: el("g", { class: "ky-sit-pill" }, m), w, h };
31
+ }
32
+ /** A football tag: the down and distance, with the yard line beneath it and an arrow for the way the offence is going (`angle`, 0 right, 90 down, 180 left, -90 up). */
33
+ export function tagWidget(main, sub, size, angle) {
34
+ const subSize = size * 0.72;
35
+ const arrow = angle === undefined ? 0 : size * 1.2;
36
+ const lead = arrow * 1.35;
37
+ const textW = Math.max(textWidth(main) * size, sub === undefined ? 0 : textWidth(sub) * subSize);
38
+ const w = textW + size * 1.7 + arrow * 1.35;
39
+ const h = sub === undefined ? size * 1.6 : size * 2.5;
40
+ let m = plate(w, h);
41
+ const cx = lead / 2;
42
+ if (angle !== undefined)
43
+ m += triangle(-w / 2 + size * 1.1, 0, size * 0.52, angle, "ky-sit-arrow");
44
+ m += text("ky-sit-t", cx, sub === undefined ? 0 : -size * 0.42, size, main);
45
+ if (sub !== undefined)
46
+ m += text("ky-sit-t ky-sit-sub", cx, size * 0.82, subSize, sub);
47
+ return { markup: el("g", { class: "ky-sit-tag" }, m), w, h };
48
+ }
49
+ /**
50
+ * The classic scorebug: a diamond whose occupied bases are lit, the outs as three dots under it, the inning with an arrow for its half, and the count. `size` is a letter's height;
51
+ * the diamond is 3.4 of them across. `bg` draws the plate behind it (a badge has its own).
52
+ */
53
+ export function scorebugWidget(s, size, bg) {
54
+ const u = size;
55
+ const right = s.inning !== undefined || (s.balls !== undefined && s.strikes !== undefined);
56
+ const left = 3.5 * u;
57
+ const w = left + (right ? 3.3 * u : 0) + 0.8 * u;
58
+ const h = 3.9 * u;
59
+ const x0 = -w / 2 + 0.4 * u + left / 2;
60
+ const cy = s.outs === undefined ? 0 : -0.5 * u;
61
+ let m = bg ? plate(w, h) : "";
62
+ // The diamond: a square turned a quarter, a base at each corner, home at the foot.
63
+ const r = 0.98 * u;
64
+ const base = (dx, dy, on, name) => {
65
+ const k = 0.5 * u;
66
+ return el("path", { class: `ky-sit-base ky-sit-b-${name}${on ? " ky-sit-on" : ""}`, "stroke-width": u * 0.1, d: `M${num(x0 + dx)} ${num(cy + dy - k)}L${num(x0 + dx + k)} ${num(cy + dy)}L${num(x0 + dx)} ${num(cy + dy + k)}L${num(x0 + dx - k)} ${num(cy + dy)}Z` });
67
+ };
68
+ m += el("path", { class: "ky-sit-dia", d: `M${num(x0)} ${num(cy - r)}L${num(x0 + r)} ${num(cy)}L${num(x0)} ${num(cy + r)}L${num(x0 - r)} ${num(cy)}Z`, fill: "none", "stroke-width": u * 0.12 });
69
+ m += base(r, 0, s.bases?.[0] === true, "1") + base(0, -r, s.bases?.[1] === true, "2") + base(-r, 0, s.bases?.[2] === true, "3");
70
+ if (s.outs !== undefined) {
71
+ for (let i = 0; i < 3; i += 1)
72
+ m += el("circle", { class: `ky-sit-out${i < s.outs ? " ky-sit-on" : ""}`, cx: x0 + (i - 1) * 1.05 * u, cy: cy + r + 0.95 * u, r: 0.34 * u });
73
+ }
74
+ if (right) {
75
+ const x1 = -w / 2 + 0.4 * u + left + 0.3 * u + 1.35 * u;
76
+ const rows = s.inning !== undefined && s.balls !== undefined && s.strikes !== undefined ? 2 : 1;
77
+ const y = (row) => (rows === 1 ? 0 : (row === 0 ? -1 : 1) * 0.95 * u);
78
+ if (s.inning !== undefined) {
79
+ const iy = y(0);
80
+ if (s.half !== undefined)
81
+ m += triangle(x1 - 0.95 * u, iy, 0.5 * u, s.half === "top" ? -90 : 90, "ky-sit-arrow");
82
+ m += text("ky-sit-t", x1 + (s.half === undefined ? 0 : 0.35 * u), iy, 1.25 * u, String(s.inning));
83
+ }
84
+ if (s.balls !== undefined && s.strikes !== undefined)
85
+ m += text("ky-sit-t ky-sit-count", x1, y(1), 1.2 * u, `${s.balls}-${s.strikes}`);
86
+ }
87
+ return { markup: el("g", { class: "ky-sit-bug" }, m), w, h };
88
+ }
89
+ /** The shootout as two rows of dots, a goal filled (●) and a miss hollow (○), the rounds not yet taken a faint speck, with each team's goals at the end. */
90
+ export function shootoutWidget(s, size, bg) {
91
+ const u = size;
92
+ const columns = Math.max(3, s.rounds.length);
93
+ const regular = Math.min(columns, 3);
94
+ const sd = columns - regular;
95
+ const pitch = 2 * u;
96
+ const label = Math.max(3.2 * u, ...s.labels.map((l) => textWidth(l) * 1.05 * u + 0.6 * u));
97
+ const gap = sd > 0 ? 1.1 * u : 0;
98
+ const total = 2.2 * u;
99
+ const top = sd > 0 ? 1.3 * u : 0;
100
+ const w = 0.8 * u + label + columns * pitch + gap + total + 0.4 * u;
101
+ const h = top + 5.2 * u;
102
+ let m = bg ? plate(w, h) : "";
103
+ const left = -w / 2 + 0.8 * u;
104
+ const y0 = -h / 2 + top + 1.55 * u;
105
+ const colX = (i) => left + label + pitch * i + pitch / 2 + (i >= regular ? gap : 0);
106
+ for (let row = 0; row < 2; row += 1) {
107
+ const y = y0 + row * 2.1 * u;
108
+ const side = s.sides?.[row];
109
+ m += text(`ky-sit-lab${side === undefined ? "" : ` ky-sit-lab-${side}`}`, left, y, 1.1 * u, s.labels[row], "start");
110
+ for (let i = 0; i < columns; i += 1) {
111
+ const shot = s.rounds[i]?.[row] ?? null;
112
+ const x = colX(i);
113
+ const sudden = i >= regular;
114
+ if (shot === true)
115
+ m += el("circle", { class: `ky-sit-goal${sudden ? " ky-sit-sd" : ""}`, cx: x, cy: y, r: 0.66 * u, "stroke-width": sudden ? 0.14 * u : undefined });
116
+ else if (shot === false)
117
+ m += el("circle", { class: `ky-sit-miss${sudden ? " ky-sit-sd" : ""}`, cx: x, cy: y, r: 0.58 * u, "stroke-width": 0.2 * u });
118
+ else
119
+ m += el("circle", { class: "ky-sit-wait", cx: x, cy: y, r: 0.2 * u });
120
+ }
121
+ m += text("ky-sit-t ky-sit-total", w / 2 - 0.5 * u - total / 2, y, 1.5 * u, String(s.goals[row]));
122
+ }
123
+ if (sd > 0) {
124
+ const x = colX(regular) - pitch / 2 - gap / 2;
125
+ m += el("path", { class: "ky-sit-split", d: `M${num(x)} ${num(-h / 2 + top + 0.2 * u)}V${num(h / 2 - 0.2 * u)}`, "stroke-width": 0.14 * u });
126
+ m += text("ky-sit-sdw", (colX(regular) + colX(columns - 1)) / 2, -h / 2 + 0.75 * u, 0.9 * u, s.suddenWord);
127
+ }
128
+ return { markup: el("g", { class: "ky-sit-strip" }, m), w, h };
129
+ }