@tastic/shell 0.0.0-stage → 0.1.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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -2
  3. package/assets/sounds/notification.wav +0 -0
  4. package/assets/sounds/select.wav +0 -0
  5. package/dist/index.d.mts +487 -0
  6. package/dist/index.d.ts +487 -0
  7. package/dist/index.js +1636 -0
  8. package/dist/index.mjs +1562 -0
  9. package/dist/notification-CGGFPYK7.wav +0 -0
  10. package/dist/select-MLBNNA4N.wav +0 -0
  11. package/package.json +163 -4
  12. package/src/PackContext.tsx +22 -0
  13. package/src/RootLayout.tsx +186 -0
  14. package/src/components/Fonts.tsx +41 -0
  15. package/src/components/GuideHost.tsx +104 -0
  16. package/src/components/Providers.tsx +86 -0
  17. package/src/components/SeatTwoPanel.tsx +132 -0
  18. package/src/components/SettingsDialog.tsx +42 -0
  19. package/src/components/Theme.tsx +75 -0
  20. package/src/components/UpdateDialog.tsx +13 -0
  21. package/src/components/imageGameIcon.tsx +13 -0
  22. package/src/gameStats.tsx +90 -0
  23. package/src/hooks/useActiveRound.ts +16 -0
  24. package/src/hooks/useAppSettings.ts +19 -0
  25. package/src/hooks/useFrozenWhileHidden.ts +33 -0
  26. package/src/hooks/useLastActiveGame.tsx +73 -0
  27. package/src/hooks/useOpponent.ts +14 -0
  28. package/src/hooks/useProfiles.tsx +134 -0
  29. package/src/hooks/useSeatColors.ts +148 -0
  30. package/src/index.ts +39 -0
  31. package/src/navigation.ts +11 -0
  32. package/src/playerSettings.ts +112 -0
  33. package/src/redux/appSettingsSlice.ts +34 -0
  34. package/src/redux/createPackStore.ts +78 -0
  35. package/src/redux/gameGuideSlice.ts +39 -0
  36. package/src/redux/lastActiveGameSlice.ts +12 -0
  37. package/src/redux/liveplaySlice.ts +34 -0
  38. package/src/redux/onProfileRemoved.ts +25 -0
  39. package/src/redux/opponentSlice.ts +21 -0
  40. package/src/redux/profileSelectionSlice.ts +17 -0
  41. package/src/redux/seatColorsSlice.ts +24 -0
  42. package/src/redux/shellState.ts +59 -0
  43. package/src/screens/GameRoute.tsx +48 -0
  44. package/src/screens/HomeScreen.tsx +378 -0
  45. package/src/screens/LoadoutScreen.tsx +271 -0
  46. package/src/screens/ProfilesRoute.tsx +14 -0
  47. package/src/screens/StatsScreen.tsx +133 -0
  48. package/src/screens/homeLayout.ts +129 -0
  49. package/src/splashGate.ts +3 -0
  50. package/src/types.ts +216 -0
package/src/types.ts ADDED
@@ -0,0 +1,216 @@
1
+ import type { ThemeAppearance } from '@rific/auto-paper'
2
+ import type { ViewRotation } from '@tastic/core'
3
+ import type { MenuOption, PopoverHost } from '@tastic/hud'
4
+ import type { GuideStep } from '@tastic/hud/guide'
5
+ import type { FontSource } from 'expo-font'
6
+ import type { ComponentType, ReactNode } from 'react'
7
+ import type { Action, Reducer } from 'redux'
8
+
9
+ import type { ShellState } from './redux/shellState'
10
+
11
+ // The two seats every game in a pack shares: seat 1 is whoever picked a player on Home, seat 2 is
12
+ // the CPU, a second person, or (only in a game with a solo mode) nobody at all.
13
+ export type Seat = 1 | 2
14
+
15
+ // Who sits in seat 2. One value for the whole pack (shell/redux/opponentSlice.ts), so the same two
16
+ // people stay set up from one game to the next.
17
+ export type SeatTwo = 'cpu' | 'person' | 'nobody'
18
+
19
+ // What a round is, from seat 2: a CPU opponent, two people on one device, or one player alone.
20
+ export type RoundMode = 'vsCpu' | 'twoPlayer' | 'solo'
21
+
22
+ export function roundModeFor(seatTwo: SeatTwo): RoundMode {
23
+ return seatTwo === 'person' ? 'twoPlayer' : seatTwo === 'nobody' ? 'solo' : 'vsCpu'
24
+ }
25
+
26
+ // Seat 2's saved choice in one game (GameDefinition.seatTwo): a saved 'nobody' reads as the CPU in a
27
+ // game without a solo mode, and so does anything else the game doesn't offer. A game with no seat 2
28
+ // at all (no seatTwo) is always nobody, so it plays solo.
29
+ export function seatTwoFor(saved: SeatTwo, choices: readonly SeatTwo[] | undefined): SeatTwo {
30
+ if (!choices) return 'nobody'
31
+ return choices.includes(saved) ? saved : 'cpu'
32
+ }
33
+
34
+ // The players who take part in a round and have to press Ready: a CPU and nobody don't.
35
+ export function humanSeatsFor(mode: RoundMode): Seat[] {
36
+ return mode === 'twoPlayer' ? [1, 2] : [1]
37
+ }
38
+
39
+ // What the loadout's match-options row is handed (see GameLoadout.options): the popover host it
40
+ // shares with seat 1's panel, the screen's rotation, and the colors the row draws with.
41
+ export interface LoadoutOptionsProps {
42
+ host: PopoverHost
43
+ rotation: ViewRotation
44
+ accentColor: string
45
+ mutedColor: string
46
+ onAccentColor: string
47
+ dark: boolean
48
+ // Undefined while the loadout folds randomize/reset into its shared action band instead.
49
+ onRandomize?: () => void
50
+ onReset?: () => void
51
+ }
52
+
53
+ // The colors a seat's controls picker draws with: the loadout's tertiary accent, or the seat's own.
54
+ export interface SeatControlLook {
55
+ accentColor: string
56
+ seatColor: string
57
+ mutedColor: string
58
+ dark: boolean
59
+ rotation: ViewRotation
60
+ }
61
+
62
+ // One game's part of the shared loadout (shell/screens/LoadoutScreen.tsx), built by the game's
63
+ // own useLoadout hook from its own saved settings.
64
+ export interface GameLoadout {
65
+ // The settings snapshot committed as the round's when every human player readies up.
66
+ roundSettings: object
67
+ // The match-options row (board, rules, puck): the game's own controls, drawn under the shared
68
+ // popover host so its menus open, elevate and close like every other loadout menu.
69
+ options: (props: LoadoutOptionsProps) => ReactNode
70
+ // The popover ids the options row opens, so the loadout can elevate the row while one is open.
71
+ optionIds: readonly string[]
72
+ randomize: () => void
73
+ reset: () => void
74
+ // A human seat's controls picker (shot style, keys), or undefined when the game has none.
75
+ seatControl: (seat: Seat, host: PopoverHost, look: SeatControlLook) => ReactNode | undefined
76
+ // Seat 2's CPU difficulty, saved per game since each game's CPU plays differently. Left out by a
77
+ // game with no seat 2 (GameDefinition.seatTwo), or whose CPU has no levels: seat 2's menu then
78
+ // offers one plain CPU.
79
+ cpuDifficulty?: { value: string; options: MenuOption<string>[]; onChange: (value: string) => void }
80
+ }
81
+
82
+ // What a game's How to Play cards are built from: the live settings (through the game's own
83
+ // selector, so the guide host never has to know their shape), how seat 2 is set up, and whether
84
+ // the device is touch-first.
85
+ export interface GameGuide {
86
+ version: number
87
+ selectVersionSeen: (state: ShellState) => number
88
+ markSeen: (version: number) => Action
89
+ // Returns a value compared with shallowEqual, so keep it a flat object of the settings read.
90
+ selectInput: (state: ShellState) => object
91
+ getSteps: (input: object, context: { touch: boolean; mode: RoundMode }) => GuideStep[]
92
+ }
93
+
94
+ // A game's icon on Home, drawn in the seats' colors (seat 1's from Home's button, seat 2's from
95
+ // whoever sits there), so it matches the players about to play, and in the theme where a part of it
96
+ // follows the board's own colors. An icon drawn as a light and dark pair of pictures instead is
97
+ // imageGameIcon's (components/imageGameIcon.tsx).
98
+ export interface GameIconProps {
99
+ size: number
100
+ p1Color: string
101
+ p2Color: string
102
+ dark: boolean
103
+ }
104
+
105
+ // A game's preview on Home (GameDefinition.Preview), drawn in a box of this size: what a deal or a
106
+ // board of the game looks like, in place of its icon. Every preview box has the same shape
107
+ // (PREVIEW_ASPECT in screens/homeLayout.ts); the seats' colors and the theme as for the icon.
108
+ export interface GamePreviewProps {
109
+ width: number
110
+ height: number
111
+ p1Color: string
112
+ p2Color: string
113
+ dark: boolean
114
+ }
115
+
116
+ export interface GameDefinition {
117
+ id: string
118
+ title: string
119
+ // One line under the tile's name on Home.
120
+ blurb: string
121
+ icon: ComponentType<GameIconProps>
122
+ // Drawn on Home in place of the icon, a frame after Home first paints (a preview is far more to
123
+ // draw than an icon). While any game in the pack has one, Home lays its games out as tiles: rows
124
+ // with the preview at their start, and a grid beside the hero on a wide screen. A game without one
125
+ // keeps its icon there. What it draws is the game's own.
126
+ Preview?: ComponentType<GamePreviewProps>
127
+ // Seat 2's choices in this game, in menu order. 'nobody' only where the game has a solo mode.
128
+ // Left out by a game for one player only: its loadout has no seat 2 button and every round is solo.
129
+ seatTwo?: readonly SeatTwo[]
130
+ // How the 2 Player loadout seats its players, to match where the game puts them: always face
131
+ // to face (or only off the web, where a desktop window seats them side by side), and whether an
132
+ // upside-down hold swaps who is at the bottom. Only read in 2 Player, so a game without 'person'
133
+ // among its seatTwo choices can leave it out.
134
+ twoPlayerSeats?: { faceToFaceOnWeb: boolean; followUpsideDown: boolean }
135
+ // The store keys this game mounts (settings, achievements, guide), merged into the pack's store.
136
+ reducers: Record<string, Reducer>
137
+ // Mounted at the app root for the game's lifetime (its stats and achievements context).
138
+ Provider: ComponentType<{ children: ReactNode }>
139
+ // Left out by a game with nothing to set up before it starts: Home opens its play screen
140
+ // straight away (after its How to Play the first time), and its rule variants are rows in its
141
+ // play screen's own Settings (SettingsDialog's children).
142
+ useLoadout?: (mode: RoundMode) => GameLoadout
143
+ PlayScreen: ComponentType
144
+ // A cold launch drops the player back into this game's play screen if that's where they left the
145
+ // app (they didn't back out of it first). For a game whose play screen saves and restores its own
146
+ // board: the shell only remembers which game it was (redux/lastActiveGameSlice.ts). A match that
147
+ // isn't saved (liveplay) shouldn't opt in, or a relaunch would open a fresh one.
148
+ resumable?: boolean
149
+ // The game's stats sections and its achievement list for the stats screen, for one profile
150
+ // (null is Everyone).
151
+ StatsSections: ComponentType<{ profileId: string | null }>
152
+ AchievementList: ComponentType<{ profileId: string | null }>
153
+ // Clears the game's stats and achievements (the stats screen's Reset).
154
+ resetStats: () => Action
155
+ guide: GameGuide
156
+ }
157
+
158
+ // What Home hands the pack's hero (PackDefinition.HeroTitle).
159
+ export interface HeroTitleProps {
160
+ // The pack's wordmark (see PackDefinition.wordmark).
161
+ words: readonly [string, string]
162
+ color: string
163
+ // The live theme's primary/secondary: whatever the players actually picked, not a fixed pair, so
164
+ // an accent in each player's color matches their real in-game color.
165
+ p1Color: string
166
+ p2Color: string
167
+ // False while another screen covers Home or the app is in the background: a hero that animates
168
+ // forever pauses then.
169
+ active: boolean
170
+ // The box the hero has to fit in, when Home gives it one: beside the game grid on a wide screen
171
+ // (a pack whose games have previews, see GameDefinition.Preview), where the hero shares a narrow
172
+ // pane with seat 1's button. Undefined is the full width of a phone held upright, its natural size.
173
+ fit?: { width: number; height: number }
174
+ }
175
+
176
+ // What the stats screen hands a pack's own section (PackDefinition.StatsSections): its two filters,
177
+ // the player (null is Everyone) and the game (null is All Games).
178
+ export interface PackStatsSectionsProps {
179
+ profileId: string | null
180
+ gameId: string | null
181
+ }
182
+
183
+ export interface PackDefinition {
184
+ name: string
185
+ // The Home wordmark's two lines.
186
+ wordmark: readonly [string, string]
187
+ // A seat's color pick for a profile is kept for this pack only; the seat button says so.
188
+ colorCaption: string
189
+ games: readonly GameDefinition[]
190
+ // Player 1's and player 2's own default colors. They seed the theme's primary and secondary (the
191
+ // tertiary is derived from them, see shellState.ts) and the guest and CPU colors a fresh install
192
+ // starts with (seatColorsSlice.ts), so a fresh install's live theme and its remembered colors agree.
193
+ // Also what @tastic/profile's avoidSeatColorClash moves seat 2 to when both seats land on one color.
194
+ seatColors: Record<Seat, string>
195
+ // The theme's appearance on a fresh install ('system' when unset). Still changeable in Settings.
196
+ appearance?: ThemeAppearance
197
+ // Home's wordmark, drawn by the pack: the shell only says where and in which colors.
198
+ HeroTitle: ComponentType<HeroTitleProps>
199
+ // Fonts the pack's own art needs (its hero's display face), loaded beside the icon font behind
200
+ // the splash screen's 'fonts' gate (components/Fonts.tsx).
201
+ fonts?: Record<string, FontSource>
202
+ // The typeface every Paper text variant is set in (auto-paper's Provider). Unset is Paper's own.
203
+ fontFamily?: string
204
+ // The version label in Settings: src/constants/release.ts's otaVersion, which the OTA tooling bumps.
205
+ otaVersion: number
206
+ // The pack's own Settings sections (how cards look, how play is helped along), in every Settings
207
+ // dialog under the shell's own rows and any a play screen adds. Their values live in `reducers`.
208
+ SettingsSections?: ComponentType
209
+ // Store keys of the pack's own, beside the shell's and each game's (GameDefinition.reducers): what
210
+ // SettingsSections saves, or anything else that belongs to every game. Never a key the shell or a
211
+ // game already mounts.
212
+ reducers?: Record<string, Reducer>
213
+ // A section of the pack's own at the top of the stats screen's Stats tab, across its games (a
214
+ // calendar of play), above each game's own StatsSections. It follows the screen's two filters.
215
+ StatsSections?: ComponentType<PackStatsSectionsProps>
216
+ }