@tastic/hud 0.14.2 → 0.15.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/README.md +110 -79
- package/dist/chrome/index.d.mts +239 -0
- package/dist/chrome/index.d.ts +239 -0
- package/dist/chrome/index.js +772 -0
- package/dist/chrome/index.mjs +44 -0
- package/dist/chunk-OTMMST5M.mjs +645 -0
- package/dist/guide/index.js +3 -3
- package/dist/guide/index.mjs +3 -3
- package/dist/index.d.mts +21 -252
- package/dist/index.d.ts +21 -252
- package/dist/index.js +20 -20
- package/dist/index.mjs +186 -791
- package/package.json +8 -1
- package/src/AchievementCatalogSection.tsx +1 -1
- package/src/AchievementRow.tsx +9 -9
- package/src/BaseSettingsDialog.tsx +31 -31
- package/src/BaseStatsScreen.tsx +13 -13
- package/src/ConfirmDialog.tsx +2 -2
- package/src/ControlSchemePicker.tsx +9 -9
- package/src/CornerActionButtons.tsx +6 -6
- package/src/CornerStatusBadges.tsx +8 -8
- package/src/InlineColorPicker.tsx +30 -30
- package/src/LabeledDropdown.tsx +8 -8
- package/src/PlayerSetupPanel.tsx +25 -25
- package/src/PopoverBody.tsx +16 -16
- package/src/PressAwayOverlay.tsx +2 -2
- package/src/ReadyButton.tsx +3 -3
- package/src/SectionedDropdown.tsx +31 -31
- package/src/SharedActionBand.tsx +6 -6
- package/src/SkiaGate.tsx +5 -5
- package/src/SkiaGate.web.tsx +2 -2
- package/src/StaggeredWord.tsx +9 -9
- package/src/StatRow.tsx +3 -3
- package/src/StatSection.tsx +3 -3
- package/src/TriggerGauge.tsx +54 -54
- package/src/TriggerGaugeHost.tsx +2 -2
- package/src/chrome/index.ts +22 -0
- package/src/fonts.ts +1 -1
- package/src/guide/DirectionKeysHint.tsx +3 -3
- package/src/guide/GuideProvider.tsx +7 -7
- package/src/guide/HowToPlayDialog.tsx +7 -7
- package/src/guide/SeatDiagram.tsx +3 -3
- package/src/guide/SwipeHint.tsx +4 -4
- package/src/guide/guideSlice.ts +7 -7
- package/src/guide/index.ts +4 -3
- package/src/guide/types.ts +1 -1
- package/src/index.ts +1 -1
- package/src/loadSkiaWeb.ts +2 -2
- package/src/loadSkiaWeb.web.ts +2 -2
- package/src/overlayCard.ts +3 -3
- package/src/useAutoAlign.ts +23 -23
- package/src/usePopoverHost.ts +1 -1
- package/src/useZoneClampedAlign.ts +23 -23
package/src/TriggerGauge.tsx
CHANGED
|
@@ -5,79 +5,79 @@ import { StyleSheet, View } from 'react-native'
|
|
|
5
5
|
import { Easing, useDerivedValue, useSharedValue, withTiming } from 'react-native-reanimated'
|
|
6
6
|
|
|
7
7
|
export interface TriggerGaugeProps {
|
|
8
|
-
// One dash per option
|
|
8
|
+
// One dash per option - a single-select section's arc (grid size, speed, trail speed, CPU
|
|
9
9
|
// difficulty) lights exactly one dash at the selected index; a multi-select section's (powerups)
|
|
10
|
-
// lights one dash per enabled option, independently. Fewer than 2 renders nothing
|
|
10
|
+
// lights one dash per enabled option, independently. Fewer than 2 renders nothing - a one-option
|
|
11
11
|
// "choice" has nothing for an arc to distinguish.
|
|
12
12
|
segments: number
|
|
13
13
|
litIndices: number[]
|
|
14
14
|
size: number
|
|
15
15
|
accentColor: string
|
|
16
16
|
mutedColor: string
|
|
17
|
-
// Each dash's own arc length in px (at this gauge's radius)
|
|
18
|
-
// covers. Omit to auto-size (see DASH_GAP_PX below) so it reads as a true dashed line
|
|
17
|
+
// Each dash's own arc length in px (at this gauge's radius) - how much of the arc it visually
|
|
18
|
+
// covers. Omit to auto-size (see DASH_GAP_PX below) so it reads as a true dashed line - each dash
|
|
19
19
|
// fills its own slot along the arc minus a small constant gap, regardless of segment count. Pass
|
|
20
20
|
// an explicit px value to override that with a fixed size instead (won't auto-adjust for segment
|
|
21
21
|
// count, so a large fixed value can visually merge adjacent dashes on a high-segment gauge like
|
|
22
|
-
// powerups
|
|
22
|
+
// powerups - unlike the old straight-bar version, an oversized arc here just gets long, not
|
|
23
23
|
// distorted, since it's a real curve rather than an approximation of one).
|
|
24
24
|
dashWidth?: number
|
|
25
25
|
// Where the arc begins, in degrees clockwise from 12 o'clock (matching Skia's own angle
|
|
26
|
-
// convention once shifted
|
|
26
|
+
// convention once shifted - see the -90° conversion below). Traversal from startDeg to endDeg is
|
|
27
27
|
// always clockwise, so which of the two possible arcs (the short way or the long way around) you
|
|
28
|
-
// get depends on how far clockwise endDeg sits from startDeg
|
|
29
|
-
// direction. Defaults to 6 o'clock
|
|
28
|
+
// get depends on how far clockwise endDeg sits from startDeg - swap the two values to flip
|
|
29
|
+
// direction. Defaults to 6 o'clock - see endDeg for why that default is a full circle rather than
|
|
30
30
|
// any particular arc.
|
|
31
31
|
startDeg?: number
|
|
32
|
-
// Where the arc ends
|
|
33
|
-
// value if you passed one, otherwise 6 o'clock), not some other fixed clock position
|
|
32
|
+
// Where the arc ends - see startDeg. Defaults to whatever startDeg resolved to (its own explicit
|
|
33
|
+
// value if you passed one, otherwise 6 o'clock), not some other fixed clock position - so passing
|
|
34
34
|
// only startDeg still gets you a full circle, just seamed wherever you started it, rather than an
|
|
35
35
|
// arbitrary partial arc between your startDeg and an unrelated default endDeg. Pass a distinct
|
|
36
|
-
// endDeg to actually clamp the sweep and carve out a gap
|
|
36
|
+
// endDeg to actually clamp the sweep and carve out a gap - e.g. 210 (7 o'clock) with endDeg 150
|
|
37
37
|
// (5 o'clock) is a 300° arc, the long way over the top through 12, leaving a 60° gap under the
|
|
38
38
|
// bottom through 6. Whenever the span is carved down from a full circle like that, it's divided
|
|
39
|
-
// into exactly `segments` equal shares (see the render below)
|
|
39
|
+
// into exactly `segments` equal shares (see the render below) - a 2-way split gets two equal
|
|
40
40
|
// shares meeting at the sweep's midpoint, a 3-way split gets three, and so on for any segment
|
|
41
|
-
// count
|
|
41
|
+
// count - every option gets a true, equal fraction of the visible arc. Segment order follows the
|
|
42
42
|
// same clockwise sweep, so index 0 sits nearest startDeg and the last index nearest endDeg.
|
|
43
43
|
endDeg?: number
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// Stroke width for an unlit dash
|
|
46
|
+
// Stroke width for an unlit dash - thin, so it reads as a faint outline rather than competing with
|
|
47
47
|
// the lit one for attention.
|
|
48
48
|
const UNLIT_THICKNESS = 3
|
|
49
|
-
// Stroke width for a lit dash
|
|
49
|
+
// Stroke width for a lit dash - noticeably thicker than UNLIT_THICKNESS, so the active option looks
|
|
50
50
|
// like it's actually "filled in" rather than just recolored.
|
|
51
51
|
const LIT_THICKNESS = 7
|
|
52
|
-
// Gap between consecutive dashes when auto-sizing (dashWidth omitted)
|
|
52
|
+
// Gap between consecutive dashes when auto-sizing (dashWidth omitted) - constant regardless of
|
|
53
53
|
// segment count, which is what keeps the arc reading as one dashed line instead of a solid merged
|
|
54
|
-
// ring (many segments)
|
|
54
|
+
// ring (many segments) - sized generously so unlit and lit dashes read as clearly separate marks
|
|
55
55
|
// rather than blending into one ring at a glance.
|
|
56
56
|
const DASH_GAP_PX = 9
|
|
57
|
-
// Gap between the trigger's own icon circle and the arc
|
|
57
|
+
// Gap between the trigger's own icon circle and the arc - small, so the dashes sit close in around
|
|
58
58
|
// the icon rather than floating well outside its silhouette.
|
|
59
59
|
const ARC_GAP = 2
|
|
60
|
-
// Baseline for startDeg when the caller passes neither prop at all
|
|
60
|
+
// Baseline for startDeg when the caller passes neither prop at all - arbitrary in principle (any
|
|
61
61
|
// value gives a full circle once endDeg defaults to match it, see the component signature below),
|
|
62
62
|
// picked as 6 o'clock so the seam sits tucked underneath the trigger rather than across its top.
|
|
63
63
|
const DEFAULT_START_DEG = 180 // 6 o'clock
|
|
64
64
|
// Floor so a many-segment gauge (or a very small dashWidth override) never collapses a dash's own
|
|
65
65
|
// sweep down to something too thin to register as a stroke.
|
|
66
66
|
const MIN_DASH_ANGLE_DEG = 4
|
|
67
|
-
// One-shot "gauge waking up" reveal on mount
|
|
67
|
+
// One-shot "gauge waking up" reveal on mount - each dash sweeps in from nothing rather than popping
|
|
68
68
|
// straight into existence, staggered by index so the reveal visibly travels along the arc instead of
|
|
69
69
|
// every dash appearing at once. Purely cosmetic, and runs once per mount only (see the effect below).
|
|
70
70
|
const MOUNT_DURATION_MS = 450
|
|
71
71
|
const MOUNT_STAGGER_MS = 40
|
|
72
|
-
// Selection-change transition
|
|
72
|
+
// Selection-change transition - separate from the one-shot mount reveal above, and re-triggered
|
|
73
73
|
// every time litIndices actually changes after that. Quick and snappy rather than a slow drift, so
|
|
74
74
|
// it reads as "the gauge just updated" without making the picker feel sluggish to use.
|
|
75
75
|
const SELECTION_TRANSITION_DURATION_MS = 260
|
|
76
76
|
|
|
77
|
-
// Arc of dashes drawn around a SectionedDropdown trigger, purely decorative-informational
|
|
77
|
+
// Arc of dashes drawn around a SectionedDropdown trigger, purely decorative-informational - the
|
|
78
78
|
// trigger's own icon stays exactly as it is (see SectionedDropdown's triggerIcon), this just frames
|
|
79
79
|
// it with an at-a-glance read of *which* option is active without opening the popover. Each dash is
|
|
80
|
-
// a real Skia arc segment (Path.addArc), not a straight bar approximating one
|
|
80
|
+
// a real Skia arc segment (Path.addArc), not a straight bar approximating one - a straight bar wide
|
|
81
81
|
// enough to look bold started visibly crossing its neighbors on a sparse (e.g. 3-option) gauge,
|
|
82
82
|
// since a chord that long diverges a lot from the circle it's meant to trace; an actual arc stays on
|
|
83
83
|
// the circle at any width. Skia measures angles clockwise from 3 o'clock, so a center angle here
|
|
@@ -85,66 +85,66 @@ const SELECTION_TRANSITION_DURATION_MS = 260
|
|
|
85
85
|
// addArc.
|
|
86
86
|
export function TriggerGauge({ segments, litIndices, size, accentColor, mutedColor, dashWidth, startDeg = DEFAULT_START_DEG, endDeg = startDeg }: TriggerGaugeProps) {
|
|
87
87
|
// Every hook below runs unconditionally, ahead of the segments<2 bail-out further down (rules of
|
|
88
|
-
// hooks)
|
|
88
|
+
// hooks) - harmless when it fires, since the resulting paths just never get read.
|
|
89
89
|
const totalDurationMs = MOUNT_DURATION_MS + Math.max(0, segments - 1) * MOUNT_STAGGER_MS
|
|
90
90
|
const mountProgress = useSharedValue(0)
|
|
91
91
|
useLayoutEffect(() => {
|
|
92
92
|
mountProgress.value = withTiming(1, { duration: totalDurationMs, easing: Easing.out(Easing.cubic) })
|
|
93
|
-
// Runs once per mount only
|
|
93
|
+
// Runs once per mount only - a "the gauge just appeared" cue, not tied to which option is
|
|
94
94
|
// selected, so this deliberately ignores changes to props that follow.
|
|
95
95
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
96
96
|
}, [])
|
|
97
97
|
|
|
98
|
-
// Clockwise distance from startDeg to endDeg
|
|
98
|
+
// Clockwise distance from startDeg to endDeg - e.g. 210 (7 o'clock) to 150 (5 o'clock) is 300°,
|
|
99
99
|
// the long way over the top, not the 60° short way under the bottom that a naive `endDeg -
|
|
100
100
|
// startDeg` would give as a negative number whenever endDeg is numerically smaller. Divided evenly
|
|
101
|
-
// across every segment (not every *gap* between segments
|
|
101
|
+
// across every segment (not every *gap* between segments - see step below) so each option gets a
|
|
102
102
|
// true equal fraction of the visible arc regardless of segment count, which is what actually keeps
|
|
103
103
|
// a 2-segment gauge from reading differently proportioned than a 3- or 4-segment one. The %360
|
|
104
104
|
// double-mod can't distinguish "0° apart" from "360° apart" (both reduce to 0), and a 0° sweep is
|
|
105
|
-
// never a useful gauge
|
|
105
|
+
// never a useful gauge - every dash would collapse onto the same point - so a 0 result is treated
|
|
106
106
|
// as the far more likely intent, a full unbroken circle, rather than a degenerate one.
|
|
107
107
|
const rawSweepDeg = (((endDeg - startDeg) % 360) + 360) % 360
|
|
108
108
|
const totalSweepDeg = rawSweepDeg === 0 ? 360 : rawSweepDeg
|
|
109
|
-
// Each segment's own angular share of totalSweepDeg
|
|
109
|
+
// Each segment's own angular share of totalSweepDeg - doubles as both "this dash's width before
|
|
110
110
|
// the gap is subtracted" (see autoDashAngleDeg below) and "the spacing between consecutive
|
|
111
111
|
// centers" (see centerDeg), since a set of equal, contiguous shares naturally has both properties
|
|
112
112
|
// at once.
|
|
113
113
|
const step = segments > 0 ? totalSweepDeg / segments : 0
|
|
114
114
|
// Segment i's own center sits half a share clockwise from startDeg, then a further whole share per
|
|
115
|
-
// preceding index
|
|
115
|
+
// preceding index - i.e. the middle of the i-th equal slice, not at either of its edges. Segment
|
|
116
116
|
// 0 is the first slice *after* startDeg (never sits exactly on startDeg itself), and the last
|
|
117
|
-
// segment's slice ends exactly on endDeg. Index increases clockwise
|
|
118
|
-
// top
|
|
117
|
+
// segment's slice ends exactly on endDeg. Index increases clockwise - left to right across the
|
|
118
|
+
// top - matching normal reading order.
|
|
119
119
|
const centerDeg = useCallback((i: number) => startDeg + (i + 0.5) * step, [startDeg, step])
|
|
120
120
|
// A single-select trigger (grid size, speed, trail speed, CPU difficulty) always has exactly one
|
|
121
|
-
// lit index
|
|
121
|
+
// lit index - a multi-select one (powerups) can have any count, including exactly 1, so this is a
|
|
122
122
|
// structural property of which *kind* of section this gauge belongs to, not something that flips
|
|
123
123
|
// render to render in practice.
|
|
124
124
|
const singleSelect = litIndices.length === 1
|
|
125
125
|
|
|
126
|
-
// The lit arc's own center angle, for the singleSelect case
|
|
126
|
+
// The lit arc's own center angle, for the singleSelect case - tracked continuously rather than
|
|
127
127
|
// computed fresh from a "from index" + 0..1 progress pair, specifically so a rapid follow-up
|
|
128
128
|
// selection can *retarget* an already-in-flight rotation instead of snapping to a fixed start
|
|
129
129
|
// angle first. withTiming does this automatically when you assign a new target to a shared value
|
|
130
130
|
// that's already mid-animation: it interpolates from wherever the value actually is right now, not
|
|
131
131
|
// from the animation's original start point. Resetting the value before restarting the animation
|
|
132
|
-
// (this file's own previous approach) throws that away
|
|
132
|
+
// (this file's own previous approach) throws that away - every follow-up transition would snap the
|
|
133
133
|
// arc to the *previous* transition's fixed departure angle before rotating on, which read as a
|
|
134
134
|
// glitch/teleport on anything but the very first change. Initial value is set directly, before any
|
|
135
|
-
// animation exists, so the very first mount doesn't rotate in from nowhere
|
|
135
|
+
// animation exists, so the very first mount doesn't rotate in from nowhere - see the mount reveal
|
|
136
136
|
// (sweep width, not angle) above for that.
|
|
137
137
|
const angleDeg = useSharedValue(centerDeg(litIndices[0] ?? 0))
|
|
138
138
|
|
|
139
|
-
// Selection-change transition state, used only for the multi-select (non-singleSelect) case now
|
|
139
|
+
// Selection-change transition state, used only for the multi-select (non-singleSelect) case now -
|
|
140
140
|
// singleSelect handles its own transition via angleDeg above. `transitionFrom` is whatever was lit
|
|
141
|
-
// just before the most recent change
|
|
141
|
+
// just before the most recent change - detected by comparing litIndicesKey against lastKeyRef,
|
|
142
142
|
// which trails one change behind (see lastIndicesRef.current below). This lives in
|
|
143
|
-
// useLayoutEffect rather than a plain useEffect specifically so it fires
|
|
144
|
-
// setTransitionFrom during it triggers a synchronous re-render, *resolves*
|
|
143
|
+
// useLayoutEffect rather than a plain useEffect specifically so it fires - and, since
|
|
144
|
+
// setTransitionFrom during it triggers a synchronous re-render, *resolves* - before the browser
|
|
145
145
|
// ever paints the frame it was scheduled from: a plain useEffect can run after paint, letting React
|
|
146
146
|
// commit one real frame with the new litIndices against the still-stale transitionFrom/
|
|
147
|
-
// selectionProgress first, which reads as a flash. Refs, not state, for the tracking itself
|
|
147
|
+
// selectionProgress first, which reads as a flash. Refs, not state, for the tracking itself -
|
|
148
148
|
// react-hooks/refs correctly flags reading or writing a ref during the render body (a discarded/
|
|
149
149
|
// replayed render can leave a ref mutated without ever committing, corrupting this tracking in a
|
|
150
150
|
// way that reproduces intermittently rather than every time), so this all needs to live inside an
|
|
@@ -173,7 +173,7 @@ export function TriggerGauge({ segments, litIndices, size, accentColor, mutedCol
|
|
|
173
173
|
|
|
174
174
|
const radius = size / 2 + ARC_GAP
|
|
175
175
|
// step is always >= 0 now (totalSweepDeg's double-mod guarantees it, unlike the old signed
|
|
176
|
-
// sweepDeg this replaced), so this no longer needs Math.abs
|
|
176
|
+
// sweepDeg this replaced), so this no longer needs Math.abs - kept as its own name rather than
|
|
177
177
|
// reusing `step` directly since this one specifically means "before the gap is subtracted."
|
|
178
178
|
const slotDeg = step
|
|
179
179
|
// A px gap converted to degrees at this radius (arc length = radius * angleInRadians), so the gap
|
|
@@ -182,7 +182,7 @@ export function TriggerGauge({ segments, litIndices, size, accentColor, mutedCol
|
|
|
182
182
|
// No upper clamp: each dash fills its own equal slot minus the constant gap, whatever that share
|
|
183
183
|
// works out to be, so a 2-segment gauge tiles as fully as a 4-segment one instead of stopping at
|
|
184
184
|
// some fixed width and leaving the rest of a large share visibly empty (that mismatch is exactly
|
|
185
|
-
// what a fixed ceiling used to cause
|
|
185
|
+
// what a fixed ceiling used to cause - same segment count, same full circle, but only some of
|
|
186
186
|
// them read as "divided all the way around" and others didn't).
|
|
187
187
|
const autoDashAngleDeg = Math.max(MIN_DASH_ANGLE_DEG, slotDeg - gapDeg)
|
|
188
188
|
const dashAngleDeg = dashWidth != null ? Math.max(MIN_DASH_ANGLE_DEG, (dashWidth / radius) * (180 / Math.PI)) : autoDashAngleDeg
|
|
@@ -193,11 +193,11 @@ export function TriggerGauge({ segments, litIndices, size, accentColor, mutedCol
|
|
|
193
193
|
const canvasSize = diameter + LIT_THICKNESS
|
|
194
194
|
const oval = Skia.XYWHRect(LIT_THICKNESS / 2, LIT_THICKNESS / 2, diameter, diameter)
|
|
195
195
|
|
|
196
|
-
// One combined path per style group (lit vs unlit) rather than one path per dash
|
|
196
|
+
// One combined path per style group (lit vs unlit) rather than one path per dash - a Path strokes
|
|
197
197
|
// as a single color/width, so dashes sharing a style share a path; this also keeps the number of
|
|
198
198
|
// useDerivedValue calls fixed at two regardless of segment count, which a per-dash hook would
|
|
199
199
|
// violate (hook counts can't vary across renders). Always reflects the FINAL/target litIndices,
|
|
200
|
-
// dim
|
|
200
|
+
// dim - including whatever's mid-"leaving" in litPath below, so the dim base is already sitting
|
|
201
201
|
// there the instant a lit dash starts shrinking off of it, rather than popping in only once the
|
|
202
202
|
// shrink finishes.
|
|
203
203
|
const unlitPath = useDerivedValue(() => {
|
|
@@ -212,13 +212,13 @@ export function TriggerGauge({ segments, litIndices, size, accentColor, mutedCol
|
|
|
212
212
|
}
|
|
213
213
|
const sweep = dashAngleDeg * local
|
|
214
214
|
if (sweep <= 0) continue
|
|
215
|
-
// Recomputed inline (not calling the outer centerDeg helper)
|
|
215
|
+
// Recomputed inline (not calling the outer centerDeg helper) - this runs inside a
|
|
216
216
|
// useDerivedValue worklet, and the helper above is a plain JS closure, not itself workletized.
|
|
217
217
|
const dashCenterDeg = startDeg + (i + 0.5) * step
|
|
218
218
|
path.addArc(oval, dashCenterDeg - 90 - sweep / 2, sweep)
|
|
219
219
|
}
|
|
220
220
|
return path
|
|
221
|
-
// litIndicesKey, not litIndices
|
|
221
|
+
// litIndicesKey, not litIndices - the array itself is a brand-new reference on every render
|
|
222
222
|
// (including ones triggered by a totally unrelated sibling gauge or settings field), which would
|
|
223
223
|
// otherwise force this worklet to rebuild on every such render. The stable string key only
|
|
224
224
|
// actually changes when the *content* does, so an unrelated re-render leaves the already-running
|
|
@@ -227,11 +227,11 @@ export function TriggerGauge({ segments, litIndices, size, accentColor, mutedCol
|
|
|
227
227
|
|
|
228
228
|
const litPath = useDerivedValue(() => {
|
|
229
229
|
const path = Skia.Path.Make()
|
|
230
|
-
// The common case
|
|
230
|
+
// The common case - a single-select trigger swapping from one tier to another - is just one
|
|
231
231
|
// full-width arc sitting at angleDeg's current value, which useLayoutEffect above retargets
|
|
232
232
|
// (never resets) on every change; this alone gives a real rotating handoff, including smoothly
|
|
233
233
|
// redirecting mid-rotation if another change interrupts it. Only reached once the mount reveal
|
|
234
|
-
// is done
|
|
234
|
+
// is done - before that, the sweep-width stagger below owns the reveal instead.
|
|
235
235
|
if (mountProgress.value >= 1 && singleSelect) {
|
|
236
236
|
path.addArc(oval, angleDeg.value - 90 - dashAngleDeg / 2, dashAngleDeg)
|
|
237
237
|
return path
|
|
@@ -248,21 +248,21 @@ export function TriggerGauge({ segments, litIndices, size, accentColor, mutedCol
|
|
|
248
248
|
const endFrac = startFrac + MOUNT_DURATION_MS / totalDurationMs
|
|
249
249
|
local = clamp((mountProgress.value - startFrac) / (endFrac - startFrac), 0, 1)
|
|
250
250
|
} else if (isLitNow === wasLit) {
|
|
251
|
-
local = 1 // steady
|
|
251
|
+
local = 1 // steady - lit before and after (or the mount-only branch above already handled unlit)
|
|
252
252
|
} else if (isLitNow) {
|
|
253
|
-
local = selectionProgress.value // entering
|
|
253
|
+
local = selectionProgress.value // entering - grows in
|
|
254
254
|
} else {
|
|
255
|
-
local = 1 - selectionProgress.value // leaving
|
|
255
|
+
local = 1 - selectionProgress.value // leaving - shrinks away
|
|
256
256
|
}
|
|
257
257
|
const sweep = dashAngleDeg * local
|
|
258
258
|
if (sweep <= 0) continue
|
|
259
|
-
// Recomputed inline (not calling the outer centerDeg helper)
|
|
259
|
+
// Recomputed inline (not calling the outer centerDeg helper) - this runs inside a
|
|
260
260
|
// useDerivedValue worklet, and the helper above is a plain JS closure, not itself workletized.
|
|
261
261
|
const dashCenterDeg = startDeg + (i + 0.5) * step
|
|
262
262
|
path.addArc(oval, dashCenterDeg - 90 - sweep / 2, sweep)
|
|
263
263
|
}
|
|
264
264
|
return path
|
|
265
|
-
// litIndicesKey/transitionFromKey, not the arrays themselves
|
|
265
|
+
// litIndicesKey/transitionFromKey, not the arrays themselves - see unlitPath's identical comment.
|
|
266
266
|
}, [segments, litIndicesKey, transitionFromKey, singleSelect, step, startDeg, dashAngleDeg, totalDurationMs, oval.x, oval.y, oval.width, oval.height])
|
|
267
267
|
|
|
268
268
|
if (segments < 2) return null
|
package/src/TriggerGaugeHost.tsx
CHANGED
|
@@ -2,12 +2,12 @@ import { SkiaGate } from './SkiaGate'
|
|
|
2
2
|
import { TriggerGaugeProps } from './TriggerGauge'
|
|
3
3
|
|
|
4
4
|
// TriggerGauge.tsx's `Skia` import binds to global.CanvasKit at module-evaluation time, which on web
|
|
5
|
-
// only exists once CanvasKit's WASM fetch resolves
|
|
5
|
+
// only exists once CanvasKit's WASM fetch resolves - a plain `import { TriggerGauge } from
|
|
6
6
|
// './TriggerGauge'` at the top of this file would already have evaluated that binding, against a
|
|
7
7
|
// not-yet-loaded, undefined CanvasKit, before anything in a consuming app renders at all, since
|
|
8
8
|
// bundlers evaluate the whole static import graph up front. This file therefore never imports
|
|
9
9
|
// TriggerGauge itself except as a type (erased at compile time, carrying no runtime module
|
|
10
|
-
// reference)
|
|
10
|
+
// reference) - getTriggerGauge below, hoisted to module scope so its identity stays stable across
|
|
11
11
|
// renders (see SkiaGate's own comment on why that matters), is what actually defers the real import
|
|
12
12
|
// to runtime. No explicit loadSkiaWeb() call here on web: SkiaGate.web.tsx (WithSkiaWeb) already
|
|
13
13
|
// gates its own mount behind CanvasKit being ready, and on native there's nothing to wait on at all.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// The fleet-chrome entry point (`@tastic/hud/chrome`): the shared dialogs, stats screen pieces, corner
|
|
2
|
+
// buttons and style tokens, reachable without the main barrel, for an app that has none of its heavy
|
|
3
|
+
// peers. Hangman is the reason it exists: it has @tastic/core, @rific/updater and the Paper stack, but
|
|
4
|
+
// no Skia, no @tastic/profile and no @tastic/split-screen. Everything reachable from this file must stay
|
|
5
|
+
// free of those three and of the main barrel; @tastic/achievements may appear only as `import type`.
|
|
6
|
+
// AchievementUnlockList is left out on purpose because it value-imports @tastic/profile's ProfileChip.
|
|
7
|
+
// src/__tests__/chromeImports.test.ts walks the real import graph from here and enforces all of it.
|
|
8
|
+
export { AchievementCatalogSection } from '../AchievementCatalogSection'
|
|
9
|
+
export { AchievementRow, LOCKED_BADGE_COLOR } from '../AchievementRow'
|
|
10
|
+
export { ActivityStatSection } from '../ActivityStatSection'
|
|
11
|
+
export { BaseSettingsDialog, type BaseSettingsDialogProps } from '../BaseSettingsDialog'
|
|
12
|
+
export { BaseStatsScreen } from '../BaseStatsScreen'
|
|
13
|
+
export { ConfirmDialog, type ConfirmDialogProps } from '../ConfirmDialog'
|
|
14
|
+
export { ContentGutter } from '../ContentGutter'
|
|
15
|
+
export { CornerActionButtons } from '../CornerActionButtons'
|
|
16
|
+
export { MONO_FONT } from '../fonts'
|
|
17
|
+
export { getOverlayCardColors, overlayActionStyles, type OverlayCardColors, overlayStyles } from '../overlayCard'
|
|
18
|
+
export { RotatedFrame, type RotatedFrameProps } from '../RotatedFrame'
|
|
19
|
+
export { getStaggeredWordDuration, StaggeredWord, type StaggeredWordProps } from '../StaggeredWord'
|
|
20
|
+
export { StatRow } from '../StatRow'
|
|
21
|
+
export { StatSection } from '../StatSection'
|
|
22
|
+
export { UpdateDialog, type UpdateDialogProps } from '../UpdateDialog'
|
package/src/fonts.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Platform } from 'react-native'
|
|
2
2
|
|
|
3
|
-
// System monospace
|
|
3
|
+
// System monospace - every component here defaults its labels to this unless a consumer passes
|
|
4
4
|
// its own `labelFontFamily`, so a game with its own branded/registered font can override it
|
|
5
5
|
// without this package needing to know that font exists.
|
|
6
6
|
export const MONO_FONT = Platform.select({ ios: 'Menlo', android: 'monospace', default: 'monospace' })
|
|
@@ -12,7 +12,7 @@ export interface DirectionKeyLabels {
|
|
|
12
12
|
export type DirectionKeysAxis = 'both' | 'vertical'
|
|
13
13
|
|
|
14
14
|
export interface DirectionKeysHintProps {
|
|
15
|
-
// What's printed on each cap
|
|
15
|
+
// What's printed on each cap - e.g. { up: 'W', left: 'A', down: 'S', right: 'D' }. Omit for the
|
|
16
16
|
// arrow keys themselves, drawn as arrow icons (so does any single cap left out). Pass whatever the
|
|
17
17
|
// seat's own key scheme actually is, not a guess: the fleet games let each seat pick WASD, arrows,
|
|
18
18
|
// IJKL and so on.
|
|
@@ -20,7 +20,7 @@ export interface DirectionKeysHintProps {
|
|
|
20
20
|
// 'both' (default) draws the four-key inverted T. 'vertical' draws only up above down, for a game
|
|
21
21
|
// whose controls only ever read those two (Pong's paddles).
|
|
22
22
|
axis?: DirectionKeysAxis
|
|
23
|
-
// Defaults to the theme's own primary
|
|
23
|
+
// Defaults to the theme's own primary - i.e. player 1's seat color in the fleet games.
|
|
24
24
|
color?: string
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -42,7 +42,7 @@ function KeyCap({ label, icon, color }: { label?: string; icon: string; color: s
|
|
|
42
42
|
)
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
// Key caps in the inverted-T every keyboard player already knows (or just up over down, see `axis`)
|
|
45
|
+
// Key caps in the inverted-T every keyboard player already knows (or just up over down, see `axis`) -
|
|
46
46
|
// the desktop counterpart to SwipeHint for the "how do I steer" card, since a desktop browser steers by
|
|
47
47
|
// key, not by swipe. Plain Views, no animation, so there's nothing for reduced motion to switch off.
|
|
48
48
|
export function DirectionKeysHint({ labels, axis = 'both', color }: DirectionKeysHintProps) {
|
|
@@ -5,19 +5,19 @@ import type { GuideRotation, GuideStep } from './types'
|
|
|
5
5
|
|
|
6
6
|
interface GuideContextValue {
|
|
7
7
|
isOpen: boolean
|
|
8
|
-
// Opens the guide unconditionally
|
|
8
|
+
// Opens the guide unconditionally - the Settings "How to play" row's whole job. Never persists
|
|
9
9
|
// anything on its own: only finishing or skipping does, and only while the player hasn't already
|
|
10
10
|
// seen this version.
|
|
11
11
|
open: () => void
|
|
12
12
|
close: () => void
|
|
13
|
-
// Opens it once per session, and only if this player hasn't seen `currentVersion` yet
|
|
13
|
+
// Opens it once per session, and only if this player hasn't seen `currentVersion` yet - see
|
|
14
14
|
// useAutoShowGuide.
|
|
15
15
|
requestAutoShow: () => void
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const noop = () => {}
|
|
19
19
|
|
|
20
|
-
// A default that does nothing rather than throwing when there's no provider above
|
|
20
|
+
// A default that does nothing rather than throwing when there's no provider above - an app's own
|
|
21
21
|
// existing tests render its Settings dialog or Home screen without one, and a "How to play" row that
|
|
22
22
|
// silently does nothing in that harness is a far better failure than every such test breaking.
|
|
23
23
|
const GuideContext = createContext<GuideContextValue>({ isOpen: false, open: noop, close: noop, requestAutoShow: noop })
|
|
@@ -35,13 +35,13 @@ export interface GuideProviderProps {
|
|
|
35
35
|
children?: ReactNode
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// Mounted once at the app root, next to <UpdateDialog />
|
|
39
|
-
// app-level dialog" seat
|
|
38
|
+
// Mounted once at the app root, next to <UpdateDialog /> - the fleet's existing "self-presenting
|
|
39
|
+
// app-level dialog" seat - so any screen can open it through useGuide() without prop-drilling, and
|
|
40
40
|
// the Portal it renders sits above every route. Inside PersistGate, so `seenVersion` is already the
|
|
41
41
|
// rehydrated value by the time this first renders.
|
|
42
42
|
export function GuideProvider({ steps, currentVersion, seenVersion, onSeen, rotation, children }: GuideProviderProps) {
|
|
43
43
|
const [isOpen, setIsOpen] = useState(false)
|
|
44
|
-
// Once per session, not once per Home mount
|
|
44
|
+
// Once per session, not once per Home mount - Home remounts every time the player backs out of a
|
|
45
45
|
// match, and skipping shouldn't be something they have to do again a minute later just because the
|
|
46
46
|
// persisted flag hadn't been written back through the store yet.
|
|
47
47
|
const autoShownRef = useRef(false)
|
|
@@ -74,7 +74,7 @@ export function useGuide(): GuideContextValue {
|
|
|
74
74
|
return useContext(GuideContext)
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
// Call this from the Home screen and nowhere else
|
|
77
|
+
// Call this from the Home screen and nowhere else - that placement, not any route check in here, is
|
|
78
78
|
// what keeps it from ever firing over a live match or a two-seat loadout. Home is the one screen
|
|
79
79
|
// with a single reader, before any seating, so a face-to-face two-player phone doesn't need per-seat
|
|
80
80
|
// rotated copies of the card. `enabled` lets a caller hold it back (e.g. while a shared-link import
|
|
@@ -46,7 +46,7 @@ export interface HowToPlayDialogProps {
|
|
|
46
46
|
steps: GuideStep[]
|
|
47
47
|
// Last card's primary button.
|
|
48
48
|
onFinish: () => void
|
|
49
|
-
// Skip (first card only
|
|
49
|
+
// Skip (first card only - see the action row), and Android's hardware back button - both mean "I don't need this", so a caller persisting
|
|
50
50
|
// "seen" should treat them exactly like onFinish.
|
|
51
51
|
onSkip: () => void
|
|
52
52
|
// The app's live fake-landscape rotation (see GuideRotation). Portal content escapes any rotated
|
|
@@ -147,8 +147,8 @@ function HowToPlayCard({ steps, onFinish, onSkip, rotation = 0 }: CardProps) {
|
|
|
147
147
|
)}
|
|
148
148
|
|
|
149
149
|
{/* ConfirmDialog's exact row (see overlayActionStyles): outlined secondary on the left,
|
|
150
|
-
contained primary on the right. Skip and Back share the one secondary slot
|
|
151
|
-
full-size buttons don't fit side by side at the row's 128 minimum
|
|
150
|
+
contained primary on the right. Skip and Back share the one secondary slot - three
|
|
151
|
+
full-size buttons don't fit side by side at the row's 128 minimum - so Skip is only
|
|
152
152
|
offered on the first card; after that the way out is Next to the end (a short flow) or
|
|
153
153
|
Android's hardware back, which skips from anywhere. */}
|
|
154
154
|
<View style={overlayActionStyles.actions}>
|
|
@@ -167,15 +167,15 @@ function HowToPlayCard({ steps, onFinish, onSkip, rotation = 0 }: CardProps) {
|
|
|
167
167
|
)
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
// A short, skippable, paged "how to play" card
|
|
170
|
+
// A short, skippable, paged "how to play" card - the one shared shape every fleet game's intro takes,
|
|
171
171
|
// built on the same retro overlay-card look as ConfirmDialog (see overlayCard.ts) so it reads as
|
|
172
172
|
// part of the same family. Controlled and presentational: whether it's visible, and what happens on
|
|
173
|
-
// finish/skip (persisting "seen", mostly), is the caller's
|
|
173
|
+
// finish/skip (persisting "seen", mostly), is the caller's - see GuideProvider for the wiring nearly
|
|
174
174
|
// every app wants. Buttons are the primary way through; the paging ScrollView underneath is what
|
|
175
175
|
// makes a swipe work too, with no gesture-handler dependency.
|
|
176
176
|
//
|
|
177
177
|
// Split into an outer visibility gate and an inner card so the card's own page index is created
|
|
178
|
-
// fresh every time it opens
|
|
178
|
+
// fresh every time it opens - a replay from Settings always starts on the first card, with no
|
|
179
179
|
// effect needed to reset it.
|
|
180
180
|
export function HowToPlayDialog({ visible, steps, ...rest }: HowToPlayDialogProps) {
|
|
181
181
|
if (!visible || steps.length === 0) return null
|
|
@@ -193,7 +193,7 @@ const styles = StyleSheet.create({
|
|
|
193
193
|
body: {
|
|
194
194
|
textAlign: 'center'
|
|
195
195
|
},
|
|
196
|
-
// Additive on top of overlayCard's own shared `card` shape
|
|
196
|
+
// Additive on top of overlayCard's own shared `card` shape - its default 32px padding is tighter
|
|
197
197
|
// here (a paged card wants more width for its art) and the shared 16px gap is right.
|
|
198
198
|
card: {
|
|
199
199
|
padding: CARD_PADDING
|
|
@@ -3,7 +3,7 @@ import { StyleSheet, View } from 'react-native'
|
|
|
3
3
|
import { Text } from 'react-native-paper'
|
|
4
4
|
|
|
5
5
|
export interface SeatDiagramProps {
|
|
6
|
-
// Default to the theme's own secondary/primary
|
|
6
|
+
// Default to the theme's own secondary/primary - in the fleet games those ARE the two players'
|
|
7
7
|
// live seat colors, so the picture always matches whatever colors they've picked.
|
|
8
8
|
topColor?: string
|
|
9
9
|
bottomColor?: string
|
|
@@ -17,7 +17,7 @@ const DEVICE_HEIGHT = 120
|
|
|
17
17
|
// "Two players, one phone": a portrait phone split into a top half and a bottom half, each tinted in
|
|
18
18
|
// its own seat color, with the top label upside down the way that player actually reads it. The
|
|
19
19
|
// seating card every face-to-face game (LightCycles, Snake, AirHockey, BoxHockey, Pong) needs, drawn
|
|
20
|
-
// once. Plain Views, no animation
|
|
20
|
+
// once. Plain Views, no animation - nothing here needs reduced-motion handling.
|
|
21
21
|
export function SeatDiagram({ topColor, bottomColor, topLabel = 'P2', bottomLabel = 'P1' }: SeatDiagramProps) {
|
|
22
22
|
const { colors } = useAutoPaperTheme()
|
|
23
23
|
const top = topColor ?? colors.secondary
|
|
@@ -64,7 +64,7 @@ const styles = StyleSheet.create({
|
|
|
64
64
|
label: {
|
|
65
65
|
fontWeight: 'bold'
|
|
66
66
|
},
|
|
67
|
-
// A wash of the seat color behind its own label rather than an rgba() fill
|
|
67
|
+
// A wash of the seat color behind its own label rather than an rgba() fill - the theme's colors
|
|
68
68
|
// aren't guaranteed to be 6-digit hex, so an opacity layer is the only alpha that always works.
|
|
69
69
|
tint: {
|
|
70
70
|
opacity: 0.2
|
package/src/guide/SwipeHint.tsx
CHANGED
|
@@ -13,7 +13,7 @@ export interface SwipeHintProps {
|
|
|
13
13
|
// plays by dragging). The mouse variant also relabels itself "Drag" for screen readers, since a
|
|
14
14
|
// desktop player should never be told to swipe.
|
|
15
15
|
pointer?: SwipePointer
|
|
16
|
-
// Defaults to the theme's own primary
|
|
16
|
+
// Defaults to the theme's own primary - i.e. the player's own seat color in the fleet games.
|
|
17
17
|
color?: string
|
|
18
18
|
// Length of the dotted path in points, arrowhead excluded.
|
|
19
19
|
length?: number
|
|
@@ -28,7 +28,7 @@ const PAUSE_MS = 500
|
|
|
28
28
|
const ARROW_ICON: Record<SwipeDirection, string> = { up: 'arrow-up', down: 'arrow-down', left: 'arrow-left', right: 'arrow-right' }
|
|
29
29
|
|
|
30
30
|
// A finger-sized ring (or, with pointer='mouse', a mouse) sweeping along a dotted path toward an
|
|
31
|
-
// arrowhead, on a loop
|
|
31
|
+
// arrowhead, on a loop - the "swipe this way" illustration the arcade games (LightCycles, Snake, Pong,
|
|
32
32
|
// AirHockey, BoxHockey) each need for their first card, so it lives here once rather than being
|
|
33
33
|
// redrawn per app. The mouse variant is the same card on desktop web for a seat that plays by
|
|
34
34
|
// dragging; Snake and AirHockey each drew their own before it existed. Plain Views plus Reanimated:
|
|
@@ -40,7 +40,7 @@ export function SwipeHint({ direction = 'right', pointer = 'finger', color, leng
|
|
|
40
40
|
const reducedMotion = useReducedMotion()
|
|
41
41
|
const progress = useSharedValue(0.5)
|
|
42
42
|
const horizontal = direction === 'left' || direction === 'right'
|
|
43
|
-
// Which way the finger travels along its own axis
|
|
43
|
+
// Which way the finger travels along its own axis - left/up run against the positive x/y axes.
|
|
44
44
|
const sign = direction === 'right' || direction === 'down' ? 1 : -1
|
|
45
45
|
|
|
46
46
|
useEffect(() => {
|
|
@@ -94,7 +94,7 @@ const styles = StyleSheet.create({
|
|
|
94
94
|
justifyContent: 'space-between'
|
|
95
95
|
},
|
|
96
96
|
// Centered on the whole path (not on the dots row alone) by the container's own alignItems/
|
|
97
|
-
// justifyContent, then pushed along the axis by its own animated translate
|
|
97
|
+
// justifyContent, then pushed along the axis by its own animated translate - absolute so it never
|
|
98
98
|
// takes space from the dots and arrowhead it's drawn over.
|
|
99
99
|
finger: {
|
|
100
100
|
borderRadius: FINGER_SIZE / 2,
|
package/src/guide/guideSlice.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
// redux-persist's own REHYDRATE action-type constant, inlined as a literal rather than taking a
|
|
2
|
-
// dependency on the package itself
|
|
2
|
+
// dependency on the package itself - same reasoning as @tastic/profile's createSeatColorsSlice, whose
|
|
3
3
|
// factory shape this follows: this file has no opinion on whether the consuming app uses
|
|
4
4
|
// redux-persist at all, and the string is a long-stable part of its public contract.
|
|
5
5
|
const REHYDRATE = 'persist/REHYDRATE'
|
|
6
6
|
|
|
7
7
|
export interface GuideState {
|
|
8
|
-
// The highest guide version this player has finished or skipped
|
|
8
|
+
// The highest guide version this player has finished or skipped - 0 means "never". Compared
|
|
9
9
|
// against the app's own `currentVersion`, so bumping that later re-prompts everyone (only worth
|
|
10
10
|
// doing for a genuine controls/rules change, not a copy tweak).
|
|
11
11
|
versionSeen: number
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
// A `type` alias, not an `interface`, on purpose: redux's UnknownAction has a string index signature,
|
|
15
|
-
// and only an object-literal type alias (never an interface) is implicitly assignable to one
|
|
15
|
+
// and only an object-literal type alias (never an interface) is implicitly assignable to one - with an
|
|
16
16
|
// interface here `configureStore` rejects the reducer at the consuming app's own typecheck.
|
|
17
17
|
export type GuideAction = {
|
|
18
18
|
type: string
|
|
@@ -24,7 +24,7 @@ export type GuideAction = {
|
|
|
24
24
|
export interface GuideSliceOptions {
|
|
25
25
|
// What a player who has already seen (or been grandfathered past) the guide is stamped with.
|
|
26
26
|
currentVersion: number
|
|
27
|
-
// The key the reducer is mounted under in the app's own combineReducers({...})
|
|
27
|
+
// The key the reducer is mounted under in the app's own combineReducers({...}) - redux-persist's
|
|
28
28
|
// REHYDRATE payload is keyed by it, and a reducer has no way to see its own mount key.
|
|
29
29
|
mountKey?: string
|
|
30
30
|
// The persistConfig `key` of the store this reducer lives in. A REHYDRATE for any OTHER persisted
|
|
@@ -40,7 +40,7 @@ export interface GuideSlice {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// The persisted "has this player seen the how-to-play flow" flag, as its own slice rather than a
|
|
43
|
-
// field on any app's settings blob
|
|
43
|
+
// field on any app's settings blob - every fleet app already has a redux-persist root store behind a
|
|
44
44
|
// PersistGate, and a dedicated key survives 'Reset stats'/'Reset match settings' (neither touches
|
|
45
45
|
// Redux) without needing any of the four strict per-app settings validators to learn a new field.
|
|
46
46
|
//
|
|
@@ -49,7 +49,7 @@ export interface GuideSlice {
|
|
|
49
49
|
// fresh install redux-persist's getStoredState finds nothing under the persistConfig key and
|
|
50
50
|
// REHYDRATE arrives with `payload === undefined`; an existing player has a payload object that
|
|
51
51
|
// simply predates this slice and so has no `[mountKey]` entry. The first shows the guide; the
|
|
52
|
-
// second is grandfathered
|
|
52
|
+
// second is grandfathered - stamped as already having seen `currentVersion` - so nobody who has been
|
|
53
53
|
// playing gets a "how to play" screen after an over-the-air update. Both can still replay it from
|
|
54
54
|
// Settings.
|
|
55
55
|
//
|
|
@@ -68,7 +68,7 @@ export function createGuideSlice({ currentVersion, mountKey = 'guide', persistKe
|
|
|
68
68
|
if (action.type === REHYDRATE) {
|
|
69
69
|
if (action.key !== undefined && action.key !== persistKey) return state
|
|
70
70
|
const payload = action.payload as Record<string, Partial<GuideState> | undefined> | undefined
|
|
71
|
-
// Nothing persisted at all (fresh install)
|
|
71
|
+
// Nothing persisted at all (fresh install) - or the read failed, which shows the guide once
|
|
72
72
|
// more rather than silently skipping it.
|
|
73
73
|
if (!payload) return state
|
|
74
74
|
const persisted = payload[mountKey]
|
package/src/guide/index.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
// The how-to-play entry point (`@tastic/hud/guide`)
|
|
1
|
+
// The how-to-play entry point (`@tastic/hud/guide`) - a standalone build reachable without ever
|
|
2
2
|
// touching the main barrel, the same reason `@tastic/hud/skia-gate` is. This file and everything it
|
|
3
3
|
// imports must stay free of @tastic/core, @tastic/profile, @tastic/split-screen, @tastic/achievements,
|
|
4
4
|
// @rific/updater and Skia: those are optional peers of this package (see package.json's
|
|
5
|
-
// peerDependenciesMeta) precisely so an app
|
|
6
|
-
//
|
|
5
|
+
// peerDependenciesMeta) precisely so an app with none of them can install @tastic/hud and use only
|
|
6
|
+
// this. src/__tests__/guideImports.test.ts enforces it. (src/chrome/index.ts is the other light entry,
|
|
7
|
+
// for an app that has @tastic/core and @rific/updater but not the rest; Hangman is that app.)
|
|
7
8
|
export { type DirectionKeyLabels, type DirectionKeysAxis, DirectionKeysHint, type DirectionKeysHintProps } from './DirectionKeysHint'
|
|
8
9
|
export { GuideProvider, type GuideProviderProps, useAutoShowGuide, useGuide } from './GuideProvider'
|
|
9
10
|
export { createGuideSlice, createReturningPlayerMigrate, type GuideAction, type GuidePersistedState, type GuideSlice, type GuideSliceOptions, type GuideState, type GuideStorageReader } from './guideSlice'
|
package/src/guide/types.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ReactNode } from 'react'
|
|
|
2
2
|
|
|
3
3
|
// One card of a game's how-to-play flow. Content is app-local data, not something this package
|
|
4
4
|
// authors: only the app knows its own controls, its own key schemes on desktop web, its own win
|
|
5
|
-
// condition. `art` is whatever illustrates the idea
|
|
5
|
+
// condition. `art` is whatever illustrates the idea - a `SwipeHint`/`SeatDiagram` from this same
|
|
6
6
|
// entry point, or anything the app draws itself (plain Views + Reanimated keeps it renderer-free).
|
|
7
7
|
export interface GuideStep {
|
|
8
8
|
title: string
|
package/src/index.ts
CHANGED
|
@@ -28,7 +28,7 @@ export { StatSection } from './StatSection'
|
|
|
28
28
|
// TriggerGauge itself (the raw Skia-based component) is deliberately NOT exported here, even
|
|
29
29
|
// though a native-only consumer could safely skip TriggerGaugeHost's SkiaGate indirection. A
|
|
30
30
|
// value re-export forces the bundler to fold TriggerGauge.tsx's own `Skia` import into a chunk
|
|
31
|
-
// that this entry point imports eagerly at the top level
|
|
31
|
+
// that this entry point imports eagerly at the top level - since that chunk is then shared with
|
|
32
32
|
// TriggerGaugeHost's own dynamic import too, it defeats TriggerGaugeHost's deferred loading for
|
|
33
33
|
// every consumer, on every platform, not just the ones reaching for the raw component.
|
|
34
34
|
// TriggerGaugeHost is the only supported way to render this.
|