@tastic/shell 0.1.1 → 0.2.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.
- package/README.md +71 -12
- package/dist/index.d.mts +86 -14
- package/dist/index.d.ts +86 -14
- package/dist/index.js +337 -165
- package/dist/index.mjs +327 -156
- package/package.json +9 -9
- package/src/PackContext.tsx +2 -2
- package/src/RootLayout.tsx +110 -28
- package/src/components/GuideHost.tsx +62 -23
- package/src/components/Providers.tsx +17 -7
- package/src/components/SeatTwoPanel.tsx +2 -1
- package/src/components/SettingsDialog.tsx +6 -4
- package/src/components/UpdateDialog.tsx +1 -1
- package/src/hooks/useSeatColors.ts +54 -27
- package/src/index.ts +1 -1
- package/src/redux/createPackStore.ts +16 -6
- package/src/redux/shellState.ts +4 -2
- package/src/routes.ts +10 -0
- package/src/screens/GameRoute.tsx +15 -7
- package/src/screens/HomeScreen.tsx +145 -76
- package/src/screens/LoadoutScreen.tsx +13 -7
- package/src/screens/ProfilesRoute.tsx +26 -1
- package/src/screens/StatsScreen.tsx +138 -56
- package/src/types.ts +143 -10
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getThirdColor, useAutoPaperTheme, useThemeSettings } from '@rific/auto-paper'
|
|
2
|
-
import { avoidSeatColorClash, profileSeatColor } from '@tastic/profile'
|
|
1
|
+
import { getThirdColor, type ThemeSettings, useAutoPaperTheme, useThemeSettings } from '@rific/auto-paper'
|
|
2
|
+
import { avoidSeatColorClash, type Profile, profileSeatColor } from '@tastic/profile'
|
|
3
3
|
import { useFocusEffect } from 'expo-router'
|
|
4
4
|
import { useCallback, useEffect, useRef } from 'react'
|
|
5
5
|
import { useDispatch, useSelector } from 'react-redux'
|
|
@@ -11,19 +11,34 @@ import type { ShellState } from '../redux/shellState'
|
|
|
11
11
|
import type { Seat } from '../types'
|
|
12
12
|
import { useProfiles } from './useProfiles'
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
//
|
|
14
|
+
type ThemeColors = Pick<ThemeSettings, 'color' | 'harmony'>
|
|
15
|
+
|
|
16
|
+
// Whether a theme already has the colors (and harmony) a seat change would set: skipped, so a re-run
|
|
17
|
+
// with nothing new writes nothing.
|
|
18
|
+
function sameTheme(current: ThemeColors, next: Partial<ThemeColors>): boolean {
|
|
19
|
+
const a = current.color
|
|
20
|
+
const b = next.color
|
|
21
|
+
const sameColor = typeof a === 'string' || typeof b === 'string' ? typeof a === typeof b && String(a).toLowerCase() === String(b).toLowerCase() : !!a && !!b && a.primary.toLowerCase() === b.primary.toLowerCase() && a.secondary.toLowerCase() === b.secondary.toLowerCase()
|
|
22
|
+
return sameColor && (next.harmony === undefined || next.harmony === current.harmony)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Both seats' live colors and the handlers that change them, for Home's who's-playing button and the
|
|
26
|
+
// loadout's seat buttons. `p2IsHuman` says whether seat 2 is a person (a guest or a profile) or the
|
|
27
|
+
// CPU, which decides where seat 2's color comes from.
|
|
28
|
+
//
|
|
29
|
+
// The theme follows the seats. By default it's a triad of the two seat colors, and each seat's live
|
|
30
|
+
// color is read back from it (its primary and secondary). A pack with its own seatTheme (a muted seed
|
|
31
|
+
// from seat 1's color, say) gets that instead, applied again when dark mode flips, and each seat's
|
|
32
|
+
// color comes from where it's remembered (the seatColors key), since the theme no longer holds the
|
|
33
|
+
// raw colors: seat 2 keeps its own color rather than the harmony's.
|
|
17
34
|
export function useSeatColors(p2IsHuman: boolean) {
|
|
18
|
-
const { colors: themeColors } = useAutoPaperTheme()
|
|
19
|
-
const { set: setThemeColor } = useThemeSettings()
|
|
20
|
-
const p1Color = themeColors.primary
|
|
21
|
-
const p2Color = themeColors.secondary
|
|
35
|
+
const { dark, colors: themeColors } = useAutoPaperTheme()
|
|
36
|
+
const { settings: themeSettings, set: setThemeColor } = useThemeSettings()
|
|
22
37
|
const { profiles, lastSelected } = useProfiles()
|
|
23
38
|
// What @tastic/profile's avoidSeatColorClash moves seat 2 to when both seats land on one color: a
|
|
24
39
|
// profile picked for one seat can be saved as the other seat's color, with no swap step behind it.
|
|
25
40
|
// The pack's own seat colors, which never change while it runs.
|
|
26
|
-
const { seatColors } = usePack()
|
|
41
|
+
const { seatColors, seatTheme } = usePack()
|
|
27
42
|
const seatDefaultColors = [seatColors[1], seatColors[2]] as const
|
|
28
43
|
|
|
29
44
|
// Typed explicitly because @tastic/profile's action creators return a plain `{ payload, type }`
|
|
@@ -44,6 +59,20 @@ export function useSeatColors(p2IsHuman: boolean) {
|
|
|
44
59
|
const p1Profile = profiles.find((p) => p.id === lastSelected[1]) ?? null
|
|
45
60
|
const p2Profile = p2IsHuman ? (profiles.find((p) => p.id === lastSelected[2]) ?? null) : null
|
|
46
61
|
|
|
62
|
+
// Where each seat's color is remembered: a profile's pick in this pack (or its own color), the CPU's,
|
|
63
|
+
// or a guest's. Seat 2 steps aside when both seats land on one color.
|
|
64
|
+
const rememberedColors = (p1Prof: Profile | null, p2Prof: Profile | null, picks: typeof profileColors, guest: typeof guestColors, cpu: string) => {
|
|
65
|
+
const p1 = p1Prof ? profileSeatColor(p1Prof, picks) : guest[1]
|
|
66
|
+
const p2 = avoidSeatColorClash(p1, p2Prof ? profileSeatColor(p2Prof, picks) : p2IsHuman ? guest[2] : cpu, seatDefaultColors)
|
|
67
|
+
return { p1, p2 }
|
|
68
|
+
}
|
|
69
|
+
const remembered = rememberedColors(p1Profile, p2Profile, profileColors, guestColors, cpuColor)
|
|
70
|
+
const p1Color = seatTheme ? remembered.p1 : themeColors.primary
|
|
71
|
+
const p2Color = seatTheme ? remembered.p2 : themeColors.secondary
|
|
72
|
+
|
|
73
|
+
// The theme two seat colors make: the pack's own, or the triad.
|
|
74
|
+
const themeFor = useCallback((p1: string, p2: string): Partial<ThemeColors> => (seatTheme ? seatTheme({ 1: p1, 2: p2 }, dark) : { color: { primary: p1, secondary: p2, tertiary: getThirdColor(p1, p2) } }), [seatTheme, dark])
|
|
75
|
+
|
|
47
76
|
// Mirrored for the focus effect below, so it can read each seat's current profile and remembered
|
|
48
77
|
// colors without depending on them (profiles is a new array on every useProfiles() render, and
|
|
49
78
|
// useFocusEffect re-runs whenever its callback's identity changes, even while still focused).
|
|
@@ -52,6 +81,7 @@ export function useSeatColors(p2IsHuman: boolean) {
|
|
|
52
81
|
const guestColorsRef = useRef(guestColors)
|
|
53
82
|
const cpuColorRef = useRef(cpuColor)
|
|
54
83
|
const profileColorsRef = useRef(profileColors)
|
|
84
|
+
const themeForRef = useRef(themeFor)
|
|
55
85
|
useEffect(() => {
|
|
56
86
|
p1ProfileRef.current = p1Profile
|
|
57
87
|
}, [p1Profile])
|
|
@@ -67,19 +97,22 @@ export function useSeatColors(p2IsHuman: boolean) {
|
|
|
67
97
|
useEffect(() => {
|
|
68
98
|
profileColorsRef.current = profileColors
|
|
69
99
|
}, [profileColors])
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
themeForRef.current = themeFor
|
|
102
|
+
}, [themeFor])
|
|
70
103
|
|
|
71
104
|
// Keeps each seat's live theme color matching what it's supposed to be right now: on mount, and
|
|
72
105
|
// when a seat's profile changes (a pick, the swap when a seat picks the other seat's profile, or a
|
|
73
106
|
// deletion from Manage), when seat 2 changes between the CPU and a person, when a profile's pick
|
|
74
107
|
// changes, and when the roster resolves. profiles itself is read, not depended on (a new array on
|
|
75
108
|
// every render); profiles.length stands in as a coarse "profiles finished loading" signal.
|
|
109
|
+
// With a seatTheme, dark mode flipping is a reason too: the theme it makes depends on it.
|
|
76
110
|
useEffect(() => {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
setThemeColor({ color: { primary: nextP1, secondary: nextP2, tertiary: getThirdColor(nextP1, nextP2) } })
|
|
111
|
+
const next = themeFor(remembered.p1, remembered.p2)
|
|
112
|
+
if (sameTheme(themeSettings, next)) return
|
|
113
|
+
setThemeColor(next)
|
|
81
114
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
82
|
-
}, [lastSelected[1], lastSelected[2], p2IsHuman, guestColors, cpuColor, profileColors, profiles.length])
|
|
115
|
+
}, [lastSelected[1], lastSelected[2], p2IsHuman, guestColors, cpuColor, profileColors, profiles.length, dark])
|
|
83
116
|
|
|
84
117
|
// The loadout pushes the play screen and Profiles on top of itself and comes back via
|
|
85
118
|
// router.back()/safeBack() - a pop that reveals this same, still-mounted screen. On every focus
|
|
@@ -89,14 +122,8 @@ export function useSeatColors(p2IsHuman: boolean) {
|
|
|
89
122
|
// everything else is read through the refs above.
|
|
90
123
|
useFocusEffect(
|
|
91
124
|
useCallback(() => {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
const p1Prof = p1ProfileRef.current
|
|
95
|
-
const p2Prof = p2ProfileRef.current
|
|
96
|
-
const picks = profileColorsRef.current
|
|
97
|
-
const nextP1 = p1Prof ? profileSeatColor(p1Prof, picks) : guest[1]
|
|
98
|
-
const nextP2 = avoidSeatColorClash(nextP1, p2Prof ? profileSeatColor(p2Prof, picks) : p2IsHuman ? guest[2] : cpu, seatDefaultColors)
|
|
99
|
-
setThemeColor({ color: { primary: nextP1, secondary: nextP2, tertiary: getThirdColor(nextP1, nextP2) } })
|
|
125
|
+
const next = rememberedColors(p1ProfileRef.current, p2ProfileRef.current, profileColorsRef.current, guestColorsRef.current, cpuColorRef.current)
|
|
126
|
+
setThemeColor(themeForRef.current(next.p1, next.p2))
|
|
100
127
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
101
128
|
}, [lastSelected[1], lastSelected[2], p2IsHuman, setThemeColor])
|
|
102
129
|
)
|
|
@@ -127,21 +154,21 @@ export function useSeatColors(p2IsHuman: boolean) {
|
|
|
127
154
|
(hex: string) => {
|
|
128
155
|
const swapped = hex.toLowerCase() === p2Color.toLowerCase()
|
|
129
156
|
const nextP2 = swapped ? p1Color : p2Color
|
|
130
|
-
setThemeColor(
|
|
157
|
+
setThemeColor(themeFor(hex, nextP2))
|
|
131
158
|
persistSeatColor(1, hex)
|
|
132
159
|
if (swapped) persistSeatColor(2, nextP2)
|
|
133
160
|
},
|
|
134
|
-
[setThemeColor, p1Color, p2Color, persistSeatColor]
|
|
161
|
+
[setThemeColor, themeFor, p1Color, p2Color, persistSeatColor]
|
|
135
162
|
)
|
|
136
163
|
const handleP2ColorChange = useCallback(
|
|
137
164
|
(hex: string) => {
|
|
138
165
|
const swapped = hex.toLowerCase() === p1Color.toLowerCase()
|
|
139
166
|
const nextP1 = swapped ? p2Color : p1Color
|
|
140
|
-
setThemeColor(
|
|
167
|
+
setThemeColor(themeFor(nextP1, hex))
|
|
141
168
|
persistSeatColor(2, hex)
|
|
142
169
|
if (swapped) persistSeatColor(1, nextP1)
|
|
143
170
|
},
|
|
144
|
-
[setThemeColor, p1Color, p2Color, persistSeatColor]
|
|
171
|
+
[setThemeColor, themeFor, p1Color, p2Color, persistSeatColor]
|
|
145
172
|
)
|
|
146
173
|
|
|
147
174
|
return { p1Color, p2Color, handleP1ColorChange, handleP2ColorChange }
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// and store, and every screen, game and shared piece reads the pack through usePack().
|
|
3
3
|
export { PackProvider, usePack, usePackGame } from './PackContext'
|
|
4
4
|
export { RootLayout } from './RootLayout'
|
|
5
|
-
export { type GameDefinition, type GameGuide, type GameIconProps, type GameLoadout, type GamePreviewProps, type HeroTitleProps, humanSeatsFor, type LoadoutOptionsProps, type PackDefinition, type PackStatsSectionsProps, type RoundMode, roundModeFor, type Seat, type SeatControlLook, type SeatTwo, seatTwoFor } from './types'
|
|
5
|
+
export { type FeedbackSounds, type GameDefinition, type GameGuide, type GameIconProps, type GameLoadout, type GamePreviewProps, type GameRulesProps, GUEST_PLAYER_ID, type GuidelessGameDefinition, type HeroTitleProps, humanSeatsFor, type LoadoutOptionsProps, type PackDefinition, type PackGameDefinition, type PackGuide, type PackStatsReset, type PackStatsSectionsProps, type PackStatsTab, type PickerAlign, type RoundMode, roundModeFor, type Seat, type SeatControlLook, type SeatTwo, seatTwoFor, type StackScreenOptions } from './types'
|
|
6
6
|
|
|
7
7
|
// The screens behind an app's routes (src/app/ stays in the app: expo-router only reads routes there).
|
|
8
8
|
export { LoadoutRoute, PlayRoute } from './screens/GameRoute'
|
|
@@ -2,7 +2,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
|
2
2
|
import { combineReducers, configureStore, type Middleware, type Reducer } from '@reduxjs/toolkit'
|
|
3
3
|
import { FLUSH, PAUSE, PERSIST, type PersistedState, persistReducer, persistStore, PURGE, REGISTER, REHYDRATE } from 'redux-persist'
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type { PackDefinition, PackGameDefinition } from '../types'
|
|
6
6
|
import lastActiveGame from './lastActiveGameSlice'
|
|
7
7
|
import { createShellReducers } from './shellState'
|
|
8
8
|
|
|
@@ -22,16 +22,25 @@ export interface PackStoreConfig {
|
|
|
22
22
|
keyPrefix: string
|
|
23
23
|
// redux-persist's migrate, run on the saved store before it rehydrates (see pack.ts).
|
|
24
24
|
migrate?: (state: PersistedState, version: number) => Promise<PersistedState>
|
|
25
|
+
// Keys left out of the root entry, beside liveplay: keys that persist themselves (a key whose reducer
|
|
26
|
+
// is its own persistReducer, saving each game's board to an entry of its own), so they aren't written
|
|
27
|
+
// into the root entry too.
|
|
28
|
+
blacklist?: readonly string[]
|
|
29
|
+
// State paths the dev serializability check skips (keys too big to walk on every action, such as
|
|
30
|
+
// saved boards).
|
|
31
|
+
serializableIgnoredPaths?: readonly string[]
|
|
32
|
+
// The pack's own middleware, after the shell's.
|
|
33
|
+
middleware?: readonly Middleware[]
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
// What the store is built from: the pack's games (each mounts its own keys, GameDefinition.reducers,
|
|
28
37
|
// and whether one is resumable decides the resume key), the pack's own keys (PackDefinition.reducers),
|
|
29
38
|
// and the seat colors and appearance the shell's own keys start from.
|
|
30
|
-
export type PackStoreSource = Pick<PackDefinition, 'seatColors' | 'appearance' | 'reducers'> & { games: readonly Pick<
|
|
39
|
+
export type PackStoreSource = Pick<PackDefinition, 'seatColors' | 'appearance' | 'seatTheme' | 'reducers'> & { games: readonly Pick<PackGameDefinition, 'reducers' | 'resumable'>[] }
|
|
31
40
|
|
|
32
41
|
// The pack's one Redux store, persisted with redux-persist: the shell's keys (shellState.ts), the
|
|
33
42
|
// pack's own, and every game's own.
|
|
34
|
-
export function createPackStore(pack: PackStoreSource, { keyPrefix, migrate }: PackStoreConfig) {
|
|
43
|
+
export function createPackStore(pack: PackStoreSource, { keyPrefix, migrate, blacklist = [], serializableIgnoredPaths = [], middleware = [] }: PackStoreConfig) {
|
|
35
44
|
const gameReducers: Record<string, Reducer> = Object.assign({}, ...pack.games.map((game) => game.reducers))
|
|
36
45
|
// Which play screen the player was last on, for a cold launch to resume (GameDefinition.resumable).
|
|
37
46
|
// Only in a pack with a resumable game, so a pack without one saves exactly what it did before.
|
|
@@ -52,7 +61,7 @@ export function createPackStore(pack: PackStoreSource, { keyPrefix, migrate }: P
|
|
|
52
61
|
// failing to ever resolve at all isn't a failure mode worth a 5s failsafe for.
|
|
53
62
|
timeout: 0,
|
|
54
63
|
// liveplay is per-round state that must never survive a relaunch (see liveplaySlice.ts).
|
|
55
|
-
blacklist: ['liveplay'],
|
|
64
|
+
blacklist: ['liveplay', ...blacklist],
|
|
56
65
|
migrate
|
|
57
66
|
},
|
|
58
67
|
rootReducer
|
|
@@ -63,10 +72,11 @@ export function createPackStore(pack: PackStoreSource, { keyPrefix, migrate }: P
|
|
|
63
72
|
const defaultMiddleware = getDefaultMiddleware({
|
|
64
73
|
immutableCheck: false,
|
|
65
74
|
serializableCheck: {
|
|
66
|
-
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
|
|
75
|
+
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
|
|
76
|
+
ignoredPaths: [...serializableIgnoredPaths]
|
|
67
77
|
}
|
|
68
78
|
})
|
|
69
|
-
return defaultMiddleware.concat(errorMiddleware)
|
|
79
|
+
return defaultMiddleware.concat(errorMiddleware, ...middleware)
|
|
70
80
|
},
|
|
71
81
|
reducer: persistedReducer
|
|
72
82
|
})
|
package/src/redux/shellState.ts
CHANGED
|
@@ -21,7 +21,7 @@ const appSoundReducer = (state: SoundSettings = initialSoundSettings, action: {
|
|
|
21
21
|
|
|
22
22
|
// The store keys every pack has, whatever its games, built from the pack's own seat colors and
|
|
23
23
|
// appearance. Each game adds its own keys beside these (see GameDefinition.reducers).
|
|
24
|
-
export function createShellReducers({ seatColors, appearance = 'system' }: Pick<PackDefinition, 'seatColors' | 'appearance'>) {
|
|
24
|
+
export function createShellReducers({ seatColors, appearance = 'system', seatTheme }: Pick<PackDefinition, 'seatColors' | 'appearance' | 'seatTheme'>) {
|
|
25
25
|
return {
|
|
26
26
|
// Explicit primary/secondary triad (rather than a single seed expanded via harmony) so the pack's
|
|
27
27
|
// identity is pinned exactly instead of drifting with whatever hue math a harmony offset would
|
|
@@ -30,7 +30,9 @@ export function createShellReducers({ seatColors, appearance = 'system' }: Pick<
|
|
|
30
30
|
// derived, not picked, via getThirdColor: the hue maximally distant from both (see
|
|
31
31
|
// @rific/auto-paper's README "Explicit triad" section). The appearance is the pack's ('system',
|
|
32
32
|
// createThemeReducer's own default, when it sets none).
|
|
33
|
-
|
|
33
|
+
// A pack with its own seatTheme starts from what seat 1's default color makes of it instead (dark
|
|
34
|
+
// only when the pack's appearance is; hooks/useSeatColors.ts re-applies it with the live mode).
|
|
35
|
+
theme: createThemeReducer(seatTheme ? { appearance, ...seatTheme(seatColors, appearance === 'dark') } : { appearance, color: { primary: seatColors[1], secondary: seatColors[2], tertiary: getThirdColor(seatColors[1], seatColors[2]) } }),
|
|
34
36
|
haptic: hapticReducer,
|
|
35
37
|
sound: appSoundReducer,
|
|
36
38
|
// @rific/core's Keep Awake setting (see Providers.tsx's KeepAwakeBridge), under the namespace its
|
package/src/routes.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Href } from 'expo-router'
|
|
2
|
+
|
|
3
|
+
// The shell's own routes, the ones every pack's src/app/ has (play/[game], loadout/[game], stats,
|
|
4
|
+
// profiles). An app with expo-router's typed routes type-checks this source against its own route tree
|
|
5
|
+
// (its tsc reads the package's src/), and an app moving onto the shell (Solitaire) has these routes only
|
|
6
|
+
// part of the way through. So they're handed to expo-router as plain Hrefs, not checked against it.
|
|
7
|
+
export const playHref = (game: string) => ({ pathname: '/play/[game]', params: { game } }) as unknown as Href
|
|
8
|
+
export const loadoutHref = (game: string) => ({ pathname: '/loadout/[game]', params: { game } }) as unknown as Href
|
|
9
|
+
export const STATS_HREF = '/stats' as unknown as Href
|
|
10
|
+
export const PROFILES_HREF = '/profiles' as unknown as Href
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Redirect, useLocalSearchParams } from 'expo-router'
|
|
2
2
|
import { useEffect } from 'react'
|
|
3
|
+
import { AppState } from 'react-native'
|
|
3
4
|
|
|
4
5
|
import { useLastActiveGame } from '../hooks/useLastActiveGame'
|
|
5
6
|
import { usePackGame } from '../PackContext'
|
|
6
|
-
import
|
|
7
|
+
import { playHref } from '../routes'
|
|
8
|
+
import type { PackGameDefinition } from '../types'
|
|
7
9
|
import { type LoadoutGame, LoadoutScreen } from './LoadoutScreen'
|
|
8
10
|
|
|
9
11
|
// The game routes, loadout/[game] and play/[game]: a game's loadout and its play screen, by id. An id
|
|
@@ -17,11 +19,11 @@ export function LoadoutRoute() {
|
|
|
17
19
|
const game = usePackGame(id)
|
|
18
20
|
if (!game) return <Redirect href='/' />
|
|
19
21
|
// A game with nothing to set up has no loadout (GameDefinition.useLoadout): a link to one plays it.
|
|
20
|
-
if (!hasLoadout(game)) return <Redirect href={
|
|
22
|
+
if (!hasLoadout(game)) return <Redirect href={playHref(game.id)} />
|
|
21
23
|
return <LoadoutScreen key={game.id} game={game} />
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
function hasLoadout(game:
|
|
26
|
+
function hasLoadout(game: PackGameDefinition): game is LoadoutGame {
|
|
25
27
|
return !!game.useLoadout
|
|
26
28
|
}
|
|
27
29
|
|
|
@@ -35,14 +37,20 @@ export function PlayRoute() {
|
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
// A resumable game's play screen (GameDefinition.resumable) is where a cold launch comes back to:
|
|
38
|
-
// remembered as it mounts, and forgotten when
|
|
39
|
-
//
|
|
40
|
-
// so it stays remembered.
|
|
40
|
+
// remembered as it mounts, and forgotten when the player backs out of it (its own back, hardware back
|
|
41
|
+
// or a browser back), which unmounts it while the app is in use. Backgrounding the app leaves the
|
|
42
|
+
// screen mounted, so it stays remembered. An unmount with the app NOT active keeps it too: when iOS
|
|
43
|
+
// terminates an app it hasn't suspended yet (swiped away from the app switcher soon after leaving
|
|
44
|
+
// it), React Native's renderer stops every surface (RCTSurfacePresenter's will-terminate handler),
|
|
45
|
+
// which unmounts the whole tree, and clearing then sent every such relaunch to Home. Runs once per
|
|
46
|
+
// play screen: the context's functions never change.
|
|
41
47
|
function useResumePoint(gameId: string | null) {
|
|
42
48
|
const { setLastActiveGame, clearLastActiveGame } = useLastActiveGame()
|
|
43
49
|
useEffect(() => {
|
|
44
50
|
if (!gameId) return
|
|
45
51
|
setLastActiveGame(gameId)
|
|
46
|
-
return
|
|
52
|
+
return () => {
|
|
53
|
+
if (AppState.currentState === 'active') clearLastActiveGame()
|
|
54
|
+
}
|
|
47
55
|
}, [gameId, setLastActiveGame, clearLastActiveGame])
|
|
48
56
|
}
|