@tastic/shell 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +154 -2
- package/assets/sounds/notification.wav +0 -0
- package/assets/sounds/select.wav +0 -0
- package/dist/index.d.mts +487 -0
- package/dist/index.d.ts +487 -0
- package/dist/index.js +1636 -0
- package/dist/index.mjs +1562 -0
- package/dist/notification-CGGFPYK7.wav +0 -0
- package/dist/select-MLBNNA4N.wav +0 -0
- package/package.json +163 -4
- package/src/PackContext.tsx +22 -0
- package/src/RootLayout.tsx +186 -0
- package/src/components/Fonts.tsx +41 -0
- package/src/components/GuideHost.tsx +104 -0
- package/src/components/Providers.tsx +86 -0
- package/src/components/SeatTwoPanel.tsx +132 -0
- package/src/components/SettingsDialog.tsx +42 -0
- package/src/components/Theme.tsx +75 -0
- package/src/components/UpdateDialog.tsx +13 -0
- package/src/components/imageGameIcon.tsx +13 -0
- package/src/gameStats.tsx +90 -0
- package/src/hooks/useActiveRound.ts +16 -0
- package/src/hooks/useAppSettings.ts +19 -0
- package/src/hooks/useFrozenWhileHidden.ts +33 -0
- package/src/hooks/useLastActiveGame.tsx +73 -0
- package/src/hooks/useOpponent.ts +14 -0
- package/src/hooks/useProfiles.tsx +134 -0
- package/src/hooks/useSeatColors.ts +148 -0
- package/src/index.ts +39 -0
- package/src/navigation.ts +11 -0
- package/src/playerSettings.ts +112 -0
- package/src/redux/appSettingsSlice.ts +34 -0
- package/src/redux/createPackStore.ts +78 -0
- package/src/redux/gameGuideSlice.ts +39 -0
- package/src/redux/lastActiveGameSlice.ts +12 -0
- package/src/redux/liveplaySlice.ts +34 -0
- package/src/redux/onProfileRemoved.ts +25 -0
- package/src/redux/opponentSlice.ts +21 -0
- package/src/redux/profileSelectionSlice.ts +17 -0
- package/src/redux/seatColorsSlice.ts +24 -0
- package/src/redux/shellState.ts +59 -0
- package/src/screens/GameRoute.tsx +48 -0
- package/src/screens/HomeScreen.tsx +378 -0
- package/src/screens/LoadoutScreen.tsx +271 -0
- package/src/screens/ProfilesRoute.tsx +14 -0
- package/src/screens/StatsScreen.tsx +133 -0
- package/src/screens/homeLayout.ts +129 -0
- package/src/splashGate.ts +3 -0
- package/src/types.ts +216 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1562 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/PackContext.tsx
|
|
9
|
+
import { createContext, useContext } from "react";
|
|
10
|
+
import { jsx } from "react/jsx-runtime";
|
|
11
|
+
var PackContext = createContext(null);
|
|
12
|
+
function PackProvider({ pack, children }) {
|
|
13
|
+
return /* @__PURE__ */ jsx(PackContext.Provider, { value: pack, children });
|
|
14
|
+
}
|
|
15
|
+
function usePack() {
|
|
16
|
+
const pack = useContext(PackContext);
|
|
17
|
+
if (!pack) throw new Error("usePack must be used within a PackProvider");
|
|
18
|
+
return pack;
|
|
19
|
+
}
|
|
20
|
+
function usePackGame(id) {
|
|
21
|
+
return usePack().games.find((g) => g.id === id);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/RootLayout.tsx
|
|
25
|
+
import { useAutoPaperTheme } from "@rific/auto-paper";
|
|
26
|
+
import { useHapticSettings } from "@rific/feedback-press";
|
|
27
|
+
import { Toaster, ToastProvider } from "@rific/toaster";
|
|
28
|
+
import { OrientationProvider, useRotationAwareStatusBar, useThemedRootBackground } from "@tastic/core";
|
|
29
|
+
import { RotatedFrame } from "@tastic/hud";
|
|
30
|
+
import * as Haptics from "expo-haptics";
|
|
31
|
+
import { Stack } from "expo-router";
|
|
32
|
+
import { DeviceMotion } from "expo-sensors";
|
|
33
|
+
import { useCallback as useCallback6 } from "react";
|
|
34
|
+
import { StyleSheet } from "react-native";
|
|
35
|
+
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
|
36
|
+
import * as RNPaper2 from "react-native-paper";
|
|
37
|
+
import { SafeAreaProvider } from "react-native-safe-area-context";
|
|
38
|
+
|
|
39
|
+
// src/components/Fonts.tsx
|
|
40
|
+
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
|
41
|
+
import Constants from "expo-constants";
|
|
42
|
+
import { useFonts } from "expo-font";
|
|
43
|
+
import Head from "expo-router/head";
|
|
44
|
+
import { Platform } from "react-native";
|
|
45
|
+
|
|
46
|
+
// src/splashGate.ts
|
|
47
|
+
import { createGate } from "@rific/splash-gate";
|
|
48
|
+
var { markReady: markSplashReady, useReady: useSplashReady, pendingGates: pendingSplashGates, Gate: SplashGate } = createGate(["theme", "fonts", "profiles"]);
|
|
49
|
+
|
|
50
|
+
// src/components/Fonts.tsx
|
|
51
|
+
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
52
|
+
function Fonts({ children }) {
|
|
53
|
+
const pack = usePack();
|
|
54
|
+
const [loaded, error] = useFonts({ ...pack.fonts, ...MaterialCommunityIcons.font });
|
|
55
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
56
|
+
Platform.OS === "web" && /* @__PURE__ */ jsx2(Head, { children: /* @__PURE__ */ jsx2("title", { children: Constants.expoConfig?.name }) }),
|
|
57
|
+
/* @__PURE__ */ jsx2(SplashGate, { gate: "fonts", ready: loaded || !!error, children })
|
|
58
|
+
] });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/components/GuideHost.tsx
|
|
62
|
+
import { useIsTouchPrimaryDevice, useRotation } from "@tastic/core";
|
|
63
|
+
import { GuideProvider, useGuide } from "@tastic/hud/guide";
|
|
64
|
+
import { createContext as createContext2, useCallback, useContext as useContext2, useEffect, useMemo, useRef, useState } from "react";
|
|
65
|
+
import { shallowEqual, useDispatch, useSelector, useStore } from "react-redux";
|
|
66
|
+
|
|
67
|
+
// src/types.ts
|
|
68
|
+
function roundModeFor(seatTwo) {
|
|
69
|
+
return seatTwo === "person" ? "twoPlayer" : seatTwo === "nobody" ? "solo" : "vsCpu";
|
|
70
|
+
}
|
|
71
|
+
function seatTwoFor(saved, choices) {
|
|
72
|
+
if (!choices) return "nobody";
|
|
73
|
+
return choices.includes(saved) ? saved : "cpu";
|
|
74
|
+
}
|
|
75
|
+
function humanSeatsFor(mode) {
|
|
76
|
+
return mode === "twoPlayer" ? [1, 2] : [1];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/components/GuideHost.tsx
|
|
80
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
81
|
+
var GameGuideContext = createContext2(null);
|
|
82
|
+
function GuideHost({ children }) {
|
|
83
|
+
const pack = usePack();
|
|
84
|
+
const [gameId, setGameId] = useState(pack.games[0].id);
|
|
85
|
+
const game = pack.games.find((g) => g.id === gameId) ?? pack.games[0];
|
|
86
|
+
const rotation = useRotation();
|
|
87
|
+
const touch = useIsTouchPrimaryDevice();
|
|
88
|
+
const seatTwo = useSelector((state) => state.opponent.seatTwo);
|
|
89
|
+
const input = useSelector((state) => game.guide.selectInput(state), shallowEqual);
|
|
90
|
+
const seenVersion = useSelector((state) => game.guide.selectVersionSeen(state));
|
|
91
|
+
const dispatch = useDispatch();
|
|
92
|
+
const mode = roundModeFor(seatTwoFor(seatTwo, game.seatTwo));
|
|
93
|
+
const steps = useMemo(() => game.guide.getSteps(input, { touch, mode }), [game, input, touch, mode]);
|
|
94
|
+
const onSeen = useCallback((version) => dispatch(game.guide.markSeen(version)), [dispatch, game]);
|
|
95
|
+
return /* @__PURE__ */ jsx3(GuideProvider, { steps, currentVersion: game.guide.version, seenVersion, onSeen, rotation, children: /* @__PURE__ */ jsx3(GameGuideBridge, { onPick: setGameId, children }) });
|
|
96
|
+
}
|
|
97
|
+
function GameGuideBridge({ onPick, children }) {
|
|
98
|
+
const pack = usePack();
|
|
99
|
+
const store = useStore();
|
|
100
|
+
const { isOpen, open } = useGuide();
|
|
101
|
+
const pendingNext = useRef(null);
|
|
102
|
+
const wasOpen = useRef(false);
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (wasOpen.current && !isOpen && pendingNext.current) {
|
|
105
|
+
const next = pendingNext.current;
|
|
106
|
+
pendingNext.current = null;
|
|
107
|
+
next();
|
|
108
|
+
}
|
|
109
|
+
wasOpen.current = isOpen;
|
|
110
|
+
}, [isOpen]);
|
|
111
|
+
const openFor = useCallback(
|
|
112
|
+
(gameId) => {
|
|
113
|
+
onPick(gameId);
|
|
114
|
+
open();
|
|
115
|
+
},
|
|
116
|
+
[onPick, open]
|
|
117
|
+
);
|
|
118
|
+
const openFirstTime = useCallback(
|
|
119
|
+
(gameId, next) => {
|
|
120
|
+
const game = pack.games.find((g) => g.id === gameId);
|
|
121
|
+
if (!game || game.guide.selectVersionSeen(store.getState()) >= game.guide.version) {
|
|
122
|
+
next();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
pendingNext.current = next;
|
|
126
|
+
openFor(gameId);
|
|
127
|
+
},
|
|
128
|
+
[pack, store, openFor]
|
|
129
|
+
);
|
|
130
|
+
const value = useMemo(() => ({ openFirstTime, openFor }), [openFirstTime, openFor]);
|
|
131
|
+
return /* @__PURE__ */ jsx3(GameGuideContext.Provider, { value, children });
|
|
132
|
+
}
|
|
133
|
+
function useGameGuide() {
|
|
134
|
+
const ctx = useContext2(GameGuideContext);
|
|
135
|
+
if (!ctx) throw new Error("useGameGuide must be used within a GuideHost");
|
|
136
|
+
return ctx;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/components/Providers.tsx
|
|
140
|
+
import { keepAwakeActions, KeepAwakeProvider } from "@rific/core";
|
|
141
|
+
import { FeedbackPressProvider, hapticActions, soundActions, useFeedbackBridgeProps } from "@rific/feedback-press";
|
|
142
|
+
import { useAudioPool } from "@rific/feedback-press/audio";
|
|
143
|
+
import { useEdgeGestureGuard } from "@tastic/edge-guard";
|
|
144
|
+
import * as ExpoKeepAwake from "expo-keep-awake";
|
|
145
|
+
import { useCallback as useCallback2 } from "react";
|
|
146
|
+
import * as RNPaper from "react-native-paper";
|
|
147
|
+
import { Provider as ReduxProvider, useDispatch as useDispatch2, useSelector as useSelector2 } from "react-redux";
|
|
148
|
+
import { PersistGate } from "redux-persist/integration/react";
|
|
149
|
+
import { Fragment as Fragment2, jsx as jsx4 } from "react/jsx-runtime";
|
|
150
|
+
var FeedbackBridge = ({ children }) => {
|
|
151
|
+
const haptic = useSelector2((state) => state.haptic);
|
|
152
|
+
const sound = useSelector2((state) => state.sound);
|
|
153
|
+
const dispatch = useDispatch2();
|
|
154
|
+
const onChange = useCallback2((next) => dispatch(hapticActions.initialize(next)), [dispatch]);
|
|
155
|
+
const onSoundChange = useCallback2((next) => dispatch(soundActions.initialize(next)), [dispatch]);
|
|
156
|
+
const playSelection = useAudioPool(__require("./select-MLBNNA4N.wav"));
|
|
157
|
+
const playNotification = useAudioPool(__require("./notification-CGGFPYK7.wav"));
|
|
158
|
+
const bridgeProps = useFeedbackBridgeProps({ initialValue: haptic, onChange, soundInitialValue: sound, onSoundChange, sound: { selection: playSelection, notification: playNotification } });
|
|
159
|
+
return /* @__PURE__ */ jsx4(FeedbackPressProvider, { ...bridgeProps, paper: RNPaper, children });
|
|
160
|
+
};
|
|
161
|
+
var EdgeGuardBridge = ({ children }) => {
|
|
162
|
+
const deferBottomEdgeGestures = useSelector2((state) => state.appSettings.deferBottomEdgeGestures);
|
|
163
|
+
const activelyPlaying = useSelector2((state) => state.liveplay.activelyPlaying);
|
|
164
|
+
useEdgeGestureGuard(deferBottomEdgeGestures && activelyPlaying);
|
|
165
|
+
return /* @__PURE__ */ jsx4(Fragment2, { children });
|
|
166
|
+
};
|
|
167
|
+
var KeepAwakeBridge = ({ children }) => {
|
|
168
|
+
const keepAwake = useSelector2((state) => state.keepAwake);
|
|
169
|
+
const dispatch = useDispatch2();
|
|
170
|
+
const onChange = useCallback2((settings) => dispatch(keepAwakeActions.initialize(settings)), [dispatch]);
|
|
171
|
+
return /* @__PURE__ */ jsx4(KeepAwakeProvider, { expoKeepAwake: ExpoKeepAwake, initialValue: keepAwake, onChange, children });
|
|
172
|
+
};
|
|
173
|
+
function Providers({ store, persistor, children }) {
|
|
174
|
+
return /* @__PURE__ */ jsx4(ReduxProvider, { store, children: /* @__PURE__ */ jsx4(PersistGate, { persistor, children: /* @__PURE__ */ jsx4(EdgeGuardBridge, { children: /* @__PURE__ */ jsx4(FeedbackBridge, { children: /* @__PURE__ */ jsx4(KeepAwakeBridge, { children }) }) }) }) });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/components/Theme.tsx
|
|
178
|
+
import MaskedView from "@react-native-masked-view/masked-view";
|
|
179
|
+
import { Provider as AutoPaperProvider, themeActions, useThemeBridgeProps } from "@rific/auto-paper";
|
|
180
|
+
import * as ExpoBlur from "expo-blur";
|
|
181
|
+
import * as SplashScreen from "expo-splash-screen";
|
|
182
|
+
import { useCallback as useCallback3 } from "react";
|
|
183
|
+
import Reanimated from "react-native-reanimated";
|
|
184
|
+
import { shallowEqual as shallowEqual2, useDispatch as useDispatch3, useSelector as useSelector3 } from "react-redux";
|
|
185
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
186
|
+
SplashScreen.preventAutoHideAsync();
|
|
187
|
+
SplashScreen.setOptions({ fade: true, duration: 400 });
|
|
188
|
+
function Theme({ children }) {
|
|
189
|
+
const { fontFamily } = usePack();
|
|
190
|
+
const settings = useSelector3((state) => state.theme, shallowEqual2);
|
|
191
|
+
const dispatch = useDispatch3();
|
|
192
|
+
const onReady = useCallback3(() => markSplashReady("theme"), []);
|
|
193
|
+
const onChange = useCallback3((next) => dispatch(themeActions.initialize(next)), [dispatch]);
|
|
194
|
+
const bridgeProps = useThemeBridgeProps({ initialValue: settings, onChange, onReady });
|
|
195
|
+
return /* @__PURE__ */ jsx5(AutoPaperProvider, { ...bridgeProps, expoBlur: ExpoBlur, maskedView: MaskedView, statusBar: false, reanimated: Reanimated, fontFamily, children });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/components/UpdateDialog.tsx
|
|
199
|
+
import { useUpdateErrorToast } from "@rific/toaster";
|
|
200
|
+
import { useRotation as useRotation2 } from "@tastic/core";
|
|
201
|
+
import { UpdateDialog as SharedUpdateDialog } from "@tastic/hud";
|
|
202
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
203
|
+
function UpdateDialog() {
|
|
204
|
+
const onError = useUpdateErrorToast();
|
|
205
|
+
const rotation = useRotation2();
|
|
206
|
+
return /* @__PURE__ */ jsx6(SharedUpdateDialog, { onError, rotation });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/hooks/useAppSettings.ts
|
|
210
|
+
import { useCallback as useCallback4 } from "react";
|
|
211
|
+
import { useDispatch as useDispatch4, useSelector as useSelector4 } from "react-redux";
|
|
212
|
+
|
|
213
|
+
// src/redux/appSettingsSlice.ts
|
|
214
|
+
import { createSettingsSlice } from "@rific/core";
|
|
215
|
+
var DEFAULT_APP_SETTINGS = {
|
|
216
|
+
lockOrientation: false,
|
|
217
|
+
deferBottomEdgeGestures: false
|
|
218
|
+
};
|
|
219
|
+
var { actions, reducer } = createSettingsSlice("appSettings", {
|
|
220
|
+
initialState: DEFAULT_APP_SETTINGS,
|
|
221
|
+
initializeMode: "merge",
|
|
222
|
+
fieldSetters: []
|
|
223
|
+
});
|
|
224
|
+
var appSettingsActions = actions;
|
|
225
|
+
var appSettingsSlice_default = reducer;
|
|
226
|
+
|
|
227
|
+
// src/hooks/useAppSettings.ts
|
|
228
|
+
function useAppSettings() {
|
|
229
|
+
const settings = useSelector4((state) => state.appSettings);
|
|
230
|
+
const dispatch = useDispatch4();
|
|
231
|
+
const setSettings = useCallback4(
|
|
232
|
+
(patch) => {
|
|
233
|
+
dispatch(appSettingsActions.initialize(patch));
|
|
234
|
+
},
|
|
235
|
+
[dispatch]
|
|
236
|
+
);
|
|
237
|
+
return { settings, setSettings };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/hooks/useLastActiveGame.tsx
|
|
241
|
+
import { createContext as createContext3, useContext as useContext3, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
|
|
242
|
+
import { useDispatch as useDispatch5, useStore as useStore2 } from "react-redux";
|
|
243
|
+
|
|
244
|
+
// src/redux/lastActiveGameSlice.ts
|
|
245
|
+
import { createSettingsSlice as createSettingsSlice2 } from "@rific/core";
|
|
246
|
+
var { actions: actions2, reducer: reducer2 } = createSettingsSlice2("lastActiveGame", { initialState: { gameId: null } });
|
|
247
|
+
var lastActiveGameActions = actions2;
|
|
248
|
+
var lastActiveGameSlice_default = reducer2;
|
|
249
|
+
|
|
250
|
+
// src/hooks/useLastActiveGame.tsx
|
|
251
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
252
|
+
var LastActiveGameContext = createContext3(null);
|
|
253
|
+
function LastActiveGameProvider({ children }) {
|
|
254
|
+
const store = useStore2();
|
|
255
|
+
const dispatch = useDispatch5();
|
|
256
|
+
const [initial] = useState2(() => store.getState().lastActiveGame?.gameId ?? null);
|
|
257
|
+
const pendingResume = useRef2(initial);
|
|
258
|
+
const persisted = useRef2(initial);
|
|
259
|
+
const value = useMemo2(
|
|
260
|
+
() => ({
|
|
261
|
+
consumeResumeGame: () => {
|
|
262
|
+
const game = pendingResume.current;
|
|
263
|
+
pendingResume.current = null;
|
|
264
|
+
return game;
|
|
265
|
+
},
|
|
266
|
+
setLastActiveGame: (gameId) => {
|
|
267
|
+
pendingResume.current = null;
|
|
268
|
+
if (persisted.current === gameId) return;
|
|
269
|
+
persisted.current = gameId;
|
|
270
|
+
dispatch(lastActiveGameActions.setGameId(gameId));
|
|
271
|
+
},
|
|
272
|
+
// Called from the play route's unmount cleanup (screens/GameRoute.tsx) - it fires on the play
|
|
273
|
+
// screen's own back, hardware back, or a browser back, but not on mere app backgrounding, where
|
|
274
|
+
// the screen stays mounted. That's exactly "explicitly back out."
|
|
275
|
+
clearLastActiveGame: () => {
|
|
276
|
+
pendingResume.current = null;
|
|
277
|
+
if (persisted.current === null) return;
|
|
278
|
+
persisted.current = null;
|
|
279
|
+
dispatch(lastActiveGameActions.setGameId(null));
|
|
280
|
+
}
|
|
281
|
+
}),
|
|
282
|
+
[dispatch]
|
|
283
|
+
);
|
|
284
|
+
return /* @__PURE__ */ jsx7(LastActiveGameContext.Provider, { value, children });
|
|
285
|
+
}
|
|
286
|
+
function useLastActiveGame() {
|
|
287
|
+
const ctx = useContext3(LastActiveGameContext);
|
|
288
|
+
if (!ctx) throw new Error("useLastActiveGame must be used within a LastActiveGameProvider");
|
|
289
|
+
return ctx;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/hooks/useProfiles.tsx
|
|
293
|
+
import { createProfileRecord, profilesActions, resolveInitialProfiles, useSharedProfilesSync } from "@tastic/profile";
|
|
294
|
+
import { createContext as createContext4, useCallback as useCallback5, useContext as useContext4, useEffect as useEffect2, useState as useState3 } from "react";
|
|
295
|
+
import { useDispatch as useDispatch6, useSelector as useSelector5 } from "react-redux";
|
|
296
|
+
|
|
297
|
+
// src/redux/profileSelectionSlice.ts
|
|
298
|
+
import { createProfileSelectionSlice } from "@tastic/profile";
|
|
299
|
+
var defaultProfileSelectionState = { 1: null, 2: null };
|
|
300
|
+
var { actions: actions3, reducer: reducer3 } = createProfileSelectionSlice("pack", defaultProfileSelectionState);
|
|
301
|
+
var profileSelectionActions = actions3;
|
|
302
|
+
var profileSelectionSlice_default = reducer3;
|
|
303
|
+
|
|
304
|
+
// src/hooks/useProfiles.tsx
|
|
305
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
306
|
+
var SHARED_GROUP_ID = "group.com.infinitetoken.tastic";
|
|
307
|
+
var ProfilesContext = createContext4(null);
|
|
308
|
+
function ProfilesProvider({ children }) {
|
|
309
|
+
const profiles = useSelector5((state) => state.profiles);
|
|
310
|
+
const lastSelected = useSelector5((state) => state.profileSelection);
|
|
311
|
+
const dispatch = useDispatch6();
|
|
312
|
+
const [loaded, setLoaded] = useState3(false);
|
|
313
|
+
useEffect2(() => {
|
|
314
|
+
let cancelled = false;
|
|
315
|
+
resolveInitialProfiles(SHARED_GROUP_ID, profiles).then((resolved) => {
|
|
316
|
+
if (cancelled) return;
|
|
317
|
+
dispatch(profilesActions.setAll(resolved));
|
|
318
|
+
dispatch(profileSelectionActions.pruneToRoster(resolved.map((p) => p.id)));
|
|
319
|
+
setLoaded(true);
|
|
320
|
+
});
|
|
321
|
+
return () => {
|
|
322
|
+
cancelled = true;
|
|
323
|
+
};
|
|
324
|
+
}, []);
|
|
325
|
+
const { syncToShared } = useSharedProfilesSync({
|
|
326
|
+
groupId: SHARED_GROUP_ID,
|
|
327
|
+
onRemoteChange: useCallback5(
|
|
328
|
+
(remote) => {
|
|
329
|
+
dispatch(profilesActions.setAll(remote));
|
|
330
|
+
dispatch(profileSelectionActions.pruneToRoster(remote.map((p) => p.id)));
|
|
331
|
+
},
|
|
332
|
+
[dispatch]
|
|
333
|
+
)
|
|
334
|
+
});
|
|
335
|
+
const createProfile = useCallback5(
|
|
336
|
+
(input) => {
|
|
337
|
+
const record = createProfileRecord(input);
|
|
338
|
+
dispatch(profilesActions.add(record));
|
|
339
|
+
syncToShared([...profiles, record]);
|
|
340
|
+
return record;
|
|
341
|
+
},
|
|
342
|
+
[profiles, dispatch, syncToShared]
|
|
343
|
+
);
|
|
344
|
+
const updateProfile = useCallback5(
|
|
345
|
+
(id, patch) => {
|
|
346
|
+
dispatch(profilesActions.update({ id, patch }));
|
|
347
|
+
syncToShared(profiles.map((p) => p.id === id ? { ...p, ...patch, updatedAt: Date.now() } : p));
|
|
348
|
+
},
|
|
349
|
+
[profiles, dispatch, syncToShared]
|
|
350
|
+
);
|
|
351
|
+
const deleteProfile = useCallback5(
|
|
352
|
+
(id) => {
|
|
353
|
+
dispatch(profilesActions.remove(id));
|
|
354
|
+
syncToShared(profiles.filter((p) => p.id !== id));
|
|
355
|
+
dispatch(profileSelectionActions.clearProfile(id));
|
|
356
|
+
},
|
|
357
|
+
[profiles, dispatch, syncToShared]
|
|
358
|
+
);
|
|
359
|
+
const selectProfile = useCallback5(
|
|
360
|
+
(seat, profileId) => {
|
|
361
|
+
dispatch(profileSelectionActions.select({ profileId, seat }));
|
|
362
|
+
},
|
|
363
|
+
[dispatch]
|
|
364
|
+
);
|
|
365
|
+
return /* @__PURE__ */ jsx8(ProfilesContext.Provider, { value: { profiles, lastSelected, loaded, createProfile, updateProfile, deleteProfile, selectProfile }, children });
|
|
366
|
+
}
|
|
367
|
+
function useProfiles() {
|
|
368
|
+
const ctx = useContext4(ProfilesContext);
|
|
369
|
+
if (!ctx) throw new Error("useProfiles must be used within a ProfilesProvider");
|
|
370
|
+
return ctx;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/RootLayout.tsx
|
|
374
|
+
import { jsx as jsx9, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
375
|
+
var PLAY_OPTIONS = { statusBarHidden: true };
|
|
376
|
+
function AppStack() {
|
|
377
|
+
const { dark } = useAutoPaperTheme();
|
|
378
|
+
const rootBackground = useThemedRootBackground(dark, false);
|
|
379
|
+
const statusBar = useRotationAwareStatusBar();
|
|
380
|
+
return /* @__PURE__ */ jsxs2(Stack, { screenOptions: { ...rootBackground, ...statusBar }, children: [
|
|
381
|
+
/* @__PURE__ */ jsx9(Stack.Screen, { name: "index" }),
|
|
382
|
+
/* @__PURE__ */ jsx9(Stack.Screen, { name: "play/[game]", options: PLAY_OPTIONS })
|
|
383
|
+
] });
|
|
384
|
+
}
|
|
385
|
+
function OrientationBridge({ children }) {
|
|
386
|
+
const { settings, setSettings } = useAppSettings();
|
|
387
|
+
return /* @__PURE__ */ jsx9(OrientationProvider, { deviceMotion: DeviceMotion, freezeWhileLocked: true, lockInitialValue: { locked: settings.lockOrientation, rotation: settings.lockedRotation }, onLockChange: (next) => setSettings({ lockOrientation: next.locked, lockedRotation: next.rotation }), children });
|
|
388
|
+
}
|
|
389
|
+
function GatedApp({ children }) {
|
|
390
|
+
const { loaded } = useProfiles();
|
|
391
|
+
return /* @__PURE__ */ jsx9(SplashGate, { gate: "profiles", ready: loaded, children });
|
|
392
|
+
}
|
|
393
|
+
function HapticGatedToastProvider({ children }) {
|
|
394
|
+
const { settings } = useHapticSettings();
|
|
395
|
+
return /* @__PURE__ */ jsx9(ToastProvider, { haptics: settings.vibrate ? Haptics : void 0, paper: RNPaper2, children });
|
|
396
|
+
}
|
|
397
|
+
function RotatingToaster() {
|
|
398
|
+
const renderFrame = useCallback6(
|
|
399
|
+
(toasts) => /* @__PURE__ */ jsx9(RotatedFrame, { rotatedSafeArea: true, passThrough: true, children: toasts }),
|
|
400
|
+
[]
|
|
401
|
+
);
|
|
402
|
+
return /* @__PURE__ */ jsx9(Toaster, { historyButton: null, clearButton: null, limit: 3, renderFrame });
|
|
403
|
+
}
|
|
404
|
+
function GameProviders({ games, children }) {
|
|
405
|
+
return games.reduceRight((inner, game) => /* @__PURE__ */ jsx9(game.Provider, { children: inner }, game.id), children);
|
|
406
|
+
}
|
|
407
|
+
function RootLayout({ pack, store, persistor }) {
|
|
408
|
+
return /* @__PURE__ */ jsx9(GestureHandlerRootView, { style: styles.root, children: /* @__PURE__ */ jsx9(SafeAreaProvider, { children: /* @__PURE__ */ jsx9(PackProvider, { pack, children: /* @__PURE__ */ jsx9(Fonts, { children: /* @__PURE__ */ jsx9(Providers, { store, persistor, children: /* @__PURE__ */ jsx9(Theme, { children: /* @__PURE__ */ jsx9(ProfilesProvider, { children: /* @__PURE__ */ jsx9(LastActiveGameProvider, { children: /* @__PURE__ */ jsx9(GameProviders, { games: pack.games, children: /* @__PURE__ */ jsx9(GatedApp, { children: /* @__PURE__ */ jsx9(OrientationBridge, { children: /* @__PURE__ */ jsxs2(HapticGatedToastProvider, { children: [
|
|
409
|
+
/* @__PURE__ */ jsx9(GuideHost, { children: /* @__PURE__ */ jsx9(AppStack, {}) }),
|
|
410
|
+
/* @__PURE__ */ jsx9(RotatingToaster, {}),
|
|
411
|
+
/* @__PURE__ */ jsx9(UpdateDialog, {})
|
|
412
|
+
] }) }) }) }) }) }) }) }) }) }) }) });
|
|
413
|
+
}
|
|
414
|
+
var styles = StyleSheet.create({
|
|
415
|
+
root: { flex: 1 }
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// src/screens/GameRoute.tsx
|
|
419
|
+
import { Redirect, useLocalSearchParams } from "expo-router";
|
|
420
|
+
import { useEffect as useEffect5 } from "react";
|
|
421
|
+
|
|
422
|
+
// src/screens/LoadoutScreen.tsx
|
|
423
|
+
import { defaultColors, useAutoPaperTheme as useAutoPaperTheme3 } from "@rific/auto-paper";
|
|
424
|
+
import { FakeLandscapeView, getViewRotation, rotateInsets, useOrientationState } from "@tastic/core";
|
|
425
|
+
import { CornerActionButtons, PlayerSetupPanel, PressAwayOverlay, ReadyButton as ReadyButton2, SharedActionBand, usePopoverHost } from "@tastic/hud";
|
|
426
|
+
import { DualZoneLayout, needsSharedNeutralZone, useDualZoneLayout } from "@tastic/split-screen";
|
|
427
|
+
import { router as router2, useFocusEffect as useFocusEffect2 } from "expo-router";
|
|
428
|
+
import { useCallback as useCallback9, useEffect as useEffect4, useMemo as useMemo4, useRef as useRef4, useState as useState4 } from "react";
|
|
429
|
+
import { Platform as Platform2, StyleSheet as StyleSheet3, View as View2 } from "react-native";
|
|
430
|
+
import Animated from "react-native-reanimated";
|
|
431
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
432
|
+
import { useDispatch as useDispatch9 } from "react-redux";
|
|
433
|
+
|
|
434
|
+
// src/components/SeatTwoPanel.tsx
|
|
435
|
+
import { ReadyButton, useZoneClampedAlign } from "@tastic/hud";
|
|
436
|
+
import { getOptionPickerContentSize, OptionPicker } from "@tastic/profile";
|
|
437
|
+
import { useMemo as useMemo3 } from "react";
|
|
438
|
+
import { StyleSheet as StyleSheet2, View } from "react-native";
|
|
439
|
+
import { jsx as jsx10, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
440
|
+
var CPU_PREFIX = "cpu:";
|
|
441
|
+
var PROFILE_PREFIX = "profile:";
|
|
442
|
+
var NOBODY = "nobody";
|
|
443
|
+
var GUEST = "guest";
|
|
444
|
+
var GUEST_ICON = "account-outline";
|
|
445
|
+
var NOBODY_ICON = "account-off-outline";
|
|
446
|
+
var CPU_ICON = "robot";
|
|
447
|
+
function getSeatTwoPopoverId(idPrefix) {
|
|
448
|
+
return `${idPrefix}-seat`;
|
|
449
|
+
}
|
|
450
|
+
function SeatTwoPanel({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDifficulty, profiles, selectedProfileId, onProfileSelect, color, onColorChange, swatches, takenColor, allowSwapTaken, colorCaption, dark, secondPicker, secondPickerId, ready, onToggleReady, showReadyButton = true }) {
|
|
451
|
+
const isPerson = seatTwo === "person";
|
|
452
|
+
const selectedProfile = isPerson ? profiles.find((p) => p.id === selectedProfileId) ?? null : null;
|
|
453
|
+
const cpuOptions = cpuDifficulty?.options;
|
|
454
|
+
const options = useMemo3(() => {
|
|
455
|
+
const list = [];
|
|
456
|
+
if (choices.includes("cpu")) list.push(...cpuOptions ? cpuOptions.map((o) => ({ value: `${CPU_PREFIX}${o.value}`, label: o.label, icon: o.icon })) : [{ value: CPU_PREFIX, label: "CPU", icon: CPU_ICON }]);
|
|
457
|
+
if (choices.includes("nobody")) list.push({ value: NOBODY, label: "Nobody", icon: NOBODY_ICON });
|
|
458
|
+
if (choices.includes("person")) {
|
|
459
|
+
list.push({ value: GUEST, label: "Player 2", icon: GUEST_ICON });
|
|
460
|
+
list.push(...[...profiles].sort((a, b) => a.name.localeCompare(b.name)).map((p) => ({ value: `${PROFILE_PREFIX}${p.id}`, label: p.name, icon: "account", color: p.color })));
|
|
461
|
+
}
|
|
462
|
+
return list;
|
|
463
|
+
}, [choices, cpuOptions, profiles]);
|
|
464
|
+
const value = seatTwo === "cpu" ? `${CPU_PREFIX}${cpuDifficulty?.value ?? ""}` : seatTwo === "nobody" ? NOBODY : selectedProfile ? `${PROFILE_PREFIX}${selectedProfile.id}` : GUEST;
|
|
465
|
+
const icon = seatTwo === "cpu" ? CPU_ICON : seatTwo === "nobody" ? NOBODY_ICON : selectedProfile ? "account" : GUEST_ICON;
|
|
466
|
+
const handleChange = (next) => {
|
|
467
|
+
if (next.startsWith(CPU_PREFIX)) {
|
|
468
|
+
cpuDifficulty?.onChange(next.slice(CPU_PREFIX.length));
|
|
469
|
+
onSeatTwoChange("cpu");
|
|
470
|
+
} else if (next === NOBODY) {
|
|
471
|
+
onSeatTwoChange("nobody");
|
|
472
|
+
} else {
|
|
473
|
+
onProfileSelect(next.startsWith(PROFILE_PREFIX) ? next.slice(PROFILE_PREFIX.length) : null);
|
|
474
|
+
onSeatTwoChange("person");
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
const colorSection = seatTwo === "nobody" ? void 0 : { value: color, onChange: onColorChange, swatches, takenValue: takenColor, allowSwapTaken, caption: selectedProfile ? colorCaption : void 0 };
|
|
478
|
+
const pickerId = getSeatTwoPopoverId(idPrefix);
|
|
479
|
+
const size = getOptionPickerContentSize(options.length, colorSection);
|
|
480
|
+
const align = useZoneClampedAlign(host.openId === pickerId, size.width, size.height);
|
|
481
|
+
const ownPopoverOpen = host.openId?.startsWith(`${idPrefix}-`) ?? false;
|
|
482
|
+
const pickerRowOpen = secondPickerId !== void 0 && host.openId === secondPickerId;
|
|
483
|
+
return /* @__PURE__ */ jsxs3(View, { style: [styles2.panel, ownPopoverOpen && styles2.panelOpen], children: [
|
|
484
|
+
/* @__PURE__ */ jsx10(OptionPicker, { id: pickerId, host, options, value, onChange: handleChange, color, dark, icon, colorSection, alignOverride: align }),
|
|
485
|
+
isPerson && secondPicker && /* @__PURE__ */ jsx10(View, { style: [styles2.pickerRow, pickerRowOpen && styles2.pickerRowOpen], children: secondPicker }),
|
|
486
|
+
isPerson && showReadyButton && onToggleReady && /* @__PURE__ */ jsx10(ReadyButton, { color, ready: ready ?? false, onToggleReady })
|
|
487
|
+
] });
|
|
488
|
+
}
|
|
489
|
+
var styles2 = StyleSheet2.create({
|
|
490
|
+
panel: {
|
|
491
|
+
alignItems: "center",
|
|
492
|
+
gap: 12
|
|
493
|
+
},
|
|
494
|
+
// A popover escaping this panel needs the panel itself elevated to paint above a later sibling.
|
|
495
|
+
panelOpen: {
|
|
496
|
+
zIndex: 100
|
|
497
|
+
},
|
|
498
|
+
pickerRow: {
|
|
499
|
+
alignItems: "center",
|
|
500
|
+
flexDirection: "row",
|
|
501
|
+
gap: 12
|
|
502
|
+
},
|
|
503
|
+
// Lifts the controls picker's popover above the Ready button below it (each View is its own
|
|
504
|
+
// stacking context on the web).
|
|
505
|
+
pickerRowOpen: {
|
|
506
|
+
zIndex: 100
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// src/components/SettingsDialog.tsx
|
|
511
|
+
import { useUpdateErrorToast as useUpdateErrorToast2 } from "@rific/toaster";
|
|
512
|
+
import { BaseSettingsDialog } from "@tastic/hud";
|
|
513
|
+
import { jsx as jsx11, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
514
|
+
function SettingsDialog({ visible, onDismiss, rotation = 0, onShowHowToPlay, children }) {
|
|
515
|
+
const onUpdateError = useUpdateErrorToast2();
|
|
516
|
+
const { settings, setSettings } = useAppSettings();
|
|
517
|
+
const { otaVersion, SettingsSections } = usePack();
|
|
518
|
+
return /* @__PURE__ */ jsxs4(BaseSettingsDialog, { visible, onDismiss, rotation, version: otaVersion, lockOrientation: settings.lockOrientation, onLockOrientationChange: (value) => setSettings({ lockOrientation: value }), deferBottomEdgeGestures: settings.deferBottomEdgeGestures, onDeferBottomEdgeGestures: (value) => setSettings({ deferBottomEdgeGestures: value }), onUpdateError, onShowHowToPlay, children: [
|
|
519
|
+
children,
|
|
520
|
+
SettingsSections && /* @__PURE__ */ jsx11(SettingsSections, {})
|
|
521
|
+
] });
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// src/hooks/useOpponent.ts
|
|
525
|
+
import { useCallback as useCallback7 } from "react";
|
|
526
|
+
import { useDispatch as useDispatch7, useSelector as useSelector6 } from "react-redux";
|
|
527
|
+
|
|
528
|
+
// src/redux/opponentSlice.ts
|
|
529
|
+
import { createSettingsSlice as createSettingsSlice3 } from "@rific/core";
|
|
530
|
+
var DEFAULT_OPPONENT = { seatTwo: "cpu" };
|
|
531
|
+
var { actions: actions4, reducer: reducer4 } = createSettingsSlice3("opponent", {
|
|
532
|
+
initialState: DEFAULT_OPPONENT
|
|
533
|
+
});
|
|
534
|
+
var opponentActions = actions4;
|
|
535
|
+
var opponentSlice_default = reducer4;
|
|
536
|
+
|
|
537
|
+
// src/hooks/useOpponent.ts
|
|
538
|
+
function useOpponent() {
|
|
539
|
+
const seatTwo = useSelector6((state) => state.opponent.seatTwo);
|
|
540
|
+
const dispatch = useDispatch7();
|
|
541
|
+
const setSeatTwo = useCallback7((next) => dispatch(opponentActions.setSeatTwo(next)), [dispatch]);
|
|
542
|
+
return { seatTwo, setSeatTwo };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// src/hooks/useSeatColors.ts
|
|
546
|
+
import { getThirdColor, useAutoPaperTheme as useAutoPaperTheme2, useThemeSettings } from "@rific/auto-paper";
|
|
547
|
+
import { avoidSeatColorClash, profileSeatColor } from "@tastic/profile";
|
|
548
|
+
import { useFocusEffect } from "expo-router";
|
|
549
|
+
import { useCallback as useCallback8, useEffect as useEffect3, useRef as useRef3 } from "react";
|
|
550
|
+
import { useDispatch as useDispatch8, useSelector as useSelector7 } from "react-redux";
|
|
551
|
+
|
|
552
|
+
// src/redux/seatColorsSlice.ts
|
|
553
|
+
import { createSeatColorsSlice } from "@tastic/profile";
|
|
554
|
+
function defaultSeatColorsState(seatColors) {
|
|
555
|
+
return { lastGuestColor: { 1: seatColors[1], 2: seatColors[2] }, lastCpuColor: seatColors[2], profileColors: {} };
|
|
556
|
+
}
|
|
557
|
+
function createSeatColorsReducer(seatColors) {
|
|
558
|
+
return createSeatColorsSlice("pack", defaultSeatColorsState(seatColors)).reducer;
|
|
559
|
+
}
|
|
560
|
+
var seatColorsActions = createSeatColorsSlice("pack", defaultSeatColorsState({ 1: "", 2: "" })).actions;
|
|
561
|
+
|
|
562
|
+
// src/hooks/useSeatColors.ts
|
|
563
|
+
function useSeatColors(p2IsHuman) {
|
|
564
|
+
const { colors: themeColors } = useAutoPaperTheme2();
|
|
565
|
+
const { set: setThemeColor } = useThemeSettings();
|
|
566
|
+
const p1Color = themeColors.primary;
|
|
567
|
+
const p2Color = themeColors.secondary;
|
|
568
|
+
const { profiles, lastSelected } = useProfiles();
|
|
569
|
+
const { seatColors } = usePack();
|
|
570
|
+
const seatDefaultColors = [seatColors[1], seatColors[2]];
|
|
571
|
+
const dispatch = useDispatch8();
|
|
572
|
+
const guestColors = useSelector7((state) => state.seatColors.lastGuestColor);
|
|
573
|
+
const cpuColor = useSelector7((state) => state.seatColors.lastCpuColor);
|
|
574
|
+
const profileColors = useSelector7((state) => state.seatColors.profileColors);
|
|
575
|
+
const p1Profile = profiles.find((p) => p.id === lastSelected[1]) ?? null;
|
|
576
|
+
const p2Profile = p2IsHuman ? profiles.find((p) => p.id === lastSelected[2]) ?? null : null;
|
|
577
|
+
const p1ProfileRef = useRef3(p1Profile);
|
|
578
|
+
const p2ProfileRef = useRef3(p2Profile);
|
|
579
|
+
const guestColorsRef = useRef3(guestColors);
|
|
580
|
+
const cpuColorRef = useRef3(cpuColor);
|
|
581
|
+
const profileColorsRef = useRef3(profileColors);
|
|
582
|
+
useEffect3(() => {
|
|
583
|
+
p1ProfileRef.current = p1Profile;
|
|
584
|
+
}, [p1Profile]);
|
|
585
|
+
useEffect3(() => {
|
|
586
|
+
p2ProfileRef.current = p2Profile;
|
|
587
|
+
}, [p2Profile]);
|
|
588
|
+
useEffect3(() => {
|
|
589
|
+
guestColorsRef.current = guestColors;
|
|
590
|
+
}, [guestColors]);
|
|
591
|
+
useEffect3(() => {
|
|
592
|
+
cpuColorRef.current = cpuColor;
|
|
593
|
+
}, [cpuColor]);
|
|
594
|
+
useEffect3(() => {
|
|
595
|
+
profileColorsRef.current = profileColors;
|
|
596
|
+
}, [profileColors]);
|
|
597
|
+
useEffect3(() => {
|
|
598
|
+
const nextP1 = p1Profile ? profileSeatColor(p1Profile, profileColors) : guestColors[1];
|
|
599
|
+
const nextP2 = avoidSeatColorClash(nextP1, p2Profile ? profileSeatColor(p2Profile, profileColors) : p2IsHuman ? guestColors[2] : cpuColor, seatDefaultColors);
|
|
600
|
+
if (nextP1.toLowerCase() === p1Color.toLowerCase() && nextP2.toLowerCase() === p2Color.toLowerCase()) return;
|
|
601
|
+
setThemeColor({ color: { primary: nextP1, secondary: nextP2, tertiary: getThirdColor(nextP1, nextP2) } });
|
|
602
|
+
}, [lastSelected[1], lastSelected[2], p2IsHuman, guestColors, cpuColor, profileColors, profiles.length]);
|
|
603
|
+
useFocusEffect(
|
|
604
|
+
useCallback8(() => {
|
|
605
|
+
const guest = guestColorsRef.current;
|
|
606
|
+
const cpu = cpuColorRef.current;
|
|
607
|
+
const p1Prof = p1ProfileRef.current;
|
|
608
|
+
const p2Prof = p2ProfileRef.current;
|
|
609
|
+
const picks = profileColorsRef.current;
|
|
610
|
+
const nextP1 = p1Prof ? profileSeatColor(p1Prof, picks) : guest[1];
|
|
611
|
+
const nextP2 = avoidSeatColorClash(nextP1, p2Prof ? profileSeatColor(p2Prof, picks) : p2IsHuman ? guest[2] : cpu, seatDefaultColors);
|
|
612
|
+
setThemeColor({ color: { primary: nextP1, secondary: nextP2, tertiary: getThirdColor(nextP1, nextP2) } });
|
|
613
|
+
}, [lastSelected[1], lastSelected[2], p2IsHuman, setThemeColor])
|
|
614
|
+
);
|
|
615
|
+
const persistSeatColor = useCallback8(
|
|
616
|
+
(seat, hex) => {
|
|
617
|
+
const profile = seat === 1 ? p1Profile : p2Profile;
|
|
618
|
+
if (profile) {
|
|
619
|
+
dispatch(seatColorsActions.setProfileColor({ profile, color: hex }));
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
if (seat === 2 && !p2IsHuman) {
|
|
623
|
+
dispatch(seatColorsActions.setLastCpuColor(hex));
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
dispatch(seatColorsActions.setLastGuestColor({ seat, color: hex }));
|
|
627
|
+
},
|
|
628
|
+
[p1Profile, p2Profile, p2IsHuman, dispatch]
|
|
629
|
+
);
|
|
630
|
+
const handleP1ColorChange = useCallback8(
|
|
631
|
+
(hex) => {
|
|
632
|
+
const swapped = hex.toLowerCase() === p2Color.toLowerCase();
|
|
633
|
+
const nextP2 = swapped ? p1Color : p2Color;
|
|
634
|
+
setThemeColor({ color: { primary: hex, secondary: nextP2, tertiary: getThirdColor(hex, nextP2) } });
|
|
635
|
+
persistSeatColor(1, hex);
|
|
636
|
+
if (swapped) persistSeatColor(2, nextP2);
|
|
637
|
+
},
|
|
638
|
+
[setThemeColor, p1Color, p2Color, persistSeatColor]
|
|
639
|
+
);
|
|
640
|
+
const handleP2ColorChange = useCallback8(
|
|
641
|
+
(hex) => {
|
|
642
|
+
const swapped = hex.toLowerCase() === p1Color.toLowerCase();
|
|
643
|
+
const nextP1 = swapped ? p2Color : p1Color;
|
|
644
|
+
setThemeColor({ color: { primary: nextP1, secondary: hex, tertiary: getThirdColor(nextP1, hex) } });
|
|
645
|
+
persistSeatColor(2, hex);
|
|
646
|
+
if (swapped) persistSeatColor(1, nextP1);
|
|
647
|
+
},
|
|
648
|
+
[setThemeColor, p1Color, p2Color, persistSeatColor]
|
|
649
|
+
);
|
|
650
|
+
return { p1Color, p2Color, handleP1ColorChange, handleP2ColorChange };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// src/navigation.ts
|
|
654
|
+
import { configureNavigation, safeBack } from "@rific/core";
|
|
655
|
+
import { router } from "expo-router";
|
|
656
|
+
configureNavigation({ router });
|
|
657
|
+
|
|
658
|
+
// src/redux/liveplaySlice.ts
|
|
659
|
+
import { createSettingsSlice as createSettingsSlice4 } from "@rific/core";
|
|
660
|
+
var { actions: actions5, reducer: reducer5 } = createSettingsSlice4("liveplay", {
|
|
661
|
+
initialState: { activelyPlaying: false, activeRound: null }
|
|
662
|
+
});
|
|
663
|
+
var liveplayActions = actions5;
|
|
664
|
+
var liveplaySlice_default = reducer5;
|
|
665
|
+
|
|
666
|
+
// src/screens/LoadoutScreen.tsx
|
|
667
|
+
import { jsx as jsx12, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
668
|
+
var PANEL_SWAP_FADE_MS = 180;
|
|
669
|
+
function LoadoutScreen({ game }) {
|
|
670
|
+
const pack = usePack();
|
|
671
|
+
const { seatTwo: savedSeatTwo, setSeatTwo } = useOpponent();
|
|
672
|
+
const seatTwo = seatTwoFor(savedSeatTwo, game.seatTwo);
|
|
673
|
+
const mode = roundModeFor(seatTwo);
|
|
674
|
+
const humanSeats = useMemo4(() => humanSeatsFor(mode), [mode]);
|
|
675
|
+
const p2IsHuman = mode === "twoPlayer";
|
|
676
|
+
const p2Exists = mode !== "solo";
|
|
677
|
+
const loadout = game.useLoadout(mode);
|
|
678
|
+
const { openFor } = useGameGuide();
|
|
679
|
+
const { profiles, lastSelected, selectProfile } = useProfiles();
|
|
680
|
+
const { orientationMode, p1OnRight, upsideDown, resolved: p1OnRightResolved } = useOrientationState();
|
|
681
|
+
const fixedSeats = p2IsHuman && (!!game.twoPlayerSeats?.faceToFaceOnWeb || Platform2.OS !== "web");
|
|
682
|
+
const fixedUpsideDown = !!game.twoPlayerSeats?.followUpsideDown && orientationMode === "faceToFace" && upsideDown;
|
|
683
|
+
const { panelLayout, panelFadeStyle } = useDualZoneLayout(fixedSeats ? "faceToFace" : orientationMode, fixedSeats ? true : p1OnRight, fixedSeats || p1OnRightResolved, fixedSeats ? fixedUpsideDown : upsideDown, PANEL_SWAP_FADE_MS);
|
|
684
|
+
const [settingsOpen, setSettingsOpen] = useState4(false);
|
|
685
|
+
const rotation = getViewRotation(panelLayout.orientationMode, panelLayout.p1OnRight, panelLayout.upsideDown);
|
|
686
|
+
const insets = rotateInsets(useSafeAreaInsets(), rotation);
|
|
687
|
+
const { colors: themeColors, dark } = useAutoPaperTheme3();
|
|
688
|
+
const { p1Color, p2Color, handleP1ColorChange, handleP2ColorChange } = useSeatColors(p2IsHuman);
|
|
689
|
+
const dispatch = useDispatch9();
|
|
690
|
+
const handleP1ProfileSelect = useCallback9((profile) => selectProfile(1, profile?.id ?? null), [selectProfile]);
|
|
691
|
+
const handleP2ProfileSelect = useCallback9((profileId) => selectProfile(2, profileId), [selectProfile]);
|
|
692
|
+
const [ready, setReady] = useState4({ 1: false, 2: false });
|
|
693
|
+
useFocusEffect2(
|
|
694
|
+
useCallback9(() => {
|
|
695
|
+
setReady({ 1: false, 2: false });
|
|
696
|
+
}, [setReady])
|
|
697
|
+
);
|
|
698
|
+
const changeSeatTwo = useCallback9(
|
|
699
|
+
(next) => {
|
|
700
|
+
setReady({ 1: false, 2: false });
|
|
701
|
+
setSeatTwo(next);
|
|
702
|
+
},
|
|
703
|
+
[setSeatTwo, setReady]
|
|
704
|
+
);
|
|
705
|
+
const roundSettingsRef = useRef4(loadout.roundSettings);
|
|
706
|
+
useEffect4(() => {
|
|
707
|
+
roundSettingsRef.current = loadout.roundSettings;
|
|
708
|
+
}, [loadout.roundSettings]);
|
|
709
|
+
useEffect4(() => {
|
|
710
|
+
if (!humanSeats.every((s) => ready[s])) return;
|
|
711
|
+
dispatch(liveplayActions.setActiveRound({ game: game.id, mode, settings: roundSettingsRef.current }));
|
|
712
|
+
router2.push({ pathname: "/play/[game]", params: { game: game.id } });
|
|
713
|
+
}, [ready, humanSeats, game.id, mode, dispatch]);
|
|
714
|
+
const bg = dark ? "#000000" : "#FFFFFF";
|
|
715
|
+
const fg = dark ? "#FFFFFF" : "#000000";
|
|
716
|
+
const fgMuted = dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)";
|
|
717
|
+
const isPortrait = panelLayout.orientationMode === "faceToFace";
|
|
718
|
+
const showMetaInSharedBand = needsSharedNeutralZone(panelLayout.orientationMode, humanSeats.length);
|
|
719
|
+
const p2ZoneStyle = p2IsHuman ? isPortrait ? styles3.p2ZoneTop : panelLayout.p1OnRight ? styles3.p2ZoneLeft : styles3.p2ZoneRight : null;
|
|
720
|
+
const showReadyButton = p2IsHuman;
|
|
721
|
+
const controlsHost = usePopoverHost();
|
|
722
|
+
const p2PanelHost = usePopoverHost();
|
|
723
|
+
const p2Host = p2IsHuman ? p2PanelHost : controlsHost;
|
|
724
|
+
const anyPlayerPickerOpen = !!(controlsHost.openId?.startsWith("p1-") || controlsHost.openId?.startsWith("p2-"));
|
|
725
|
+
const optionsRow = loadout.options({
|
|
726
|
+
host: controlsHost,
|
|
727
|
+
rotation,
|
|
728
|
+
accentColor: themeColors.tertiary,
|
|
729
|
+
mutedColor: fgMuted,
|
|
730
|
+
onAccentColor: themeColors.onTertiary,
|
|
731
|
+
dark,
|
|
732
|
+
// Left out (the row's own actions unrendered) whenever the shared band below carries them.
|
|
733
|
+
onRandomize: showMetaInSharedBand ? void 0 : loadout.randomize,
|
|
734
|
+
onReset: showMetaInSharedBand ? void 0 : loadout.reset
|
|
735
|
+
});
|
|
736
|
+
const optionsPopoverOpen = controlsHost.openId !== null && loadout.optionIds.includes(controlsHost.openId);
|
|
737
|
+
const sharedControls = showMetaInSharedBand ? /* @__PURE__ */ jsx12(SharedActionBand, { onBack: safeBack, onSettings: () => setSettingsOpen(true), onRandomize: loadout.randomize, onReset: loadout.reset, fg, popoverOpen: optionsPopoverOpen, children: optionsRow }) : optionsRow;
|
|
738
|
+
const controlLook = { accentColor: themeColors.tertiary, mutedColor: fgMuted, dark, rotation };
|
|
739
|
+
const p1Control = loadout.seatControl(1, controlsHost, { ...controlLook, seatColor: p1Color });
|
|
740
|
+
const p1Panel = /* @__PURE__ */ jsx12(PlayerSetupPanel, { idPrefix: "p1", host: controlsHost, color: p1Color, onColorChange: handleP1ColorChange, swatches: defaultColors, takenColor: p2Exists ? p2Color : void 0, allowSwapTaken: mode === "vsCpu", colorCaption: pack.colorCaption, isHuman: true, ready: ready[1], onToggleReady: () => setReady((r) => ({ ...r, 1: !r[1] })), dark, showReadyButton, secondPicker: p1Control, secondPickerId: p1Control ? "p1-control" : void 0, profiles, selectedProfileId: lastSelected[1], takenProfileId: p2IsHuman ? lastSelected[2] : null, guestLabel: "P1", onProfileSelect: handleP1ProfileSelect, onManageProfiles: () => router2.push("/profiles") });
|
|
741
|
+
const p2Control = p2IsHuman ? loadout.seatControl(2, p2Host, { ...controlLook, seatColor: p2Color }) : void 0;
|
|
742
|
+
const p2Panel = game.seatTwo && /* @__PURE__ */ jsx12(SeatTwoPanel, { idPrefix: "p2", host: p2Host, choices: game.seatTwo, seatTwo, onSeatTwoChange: changeSeatTwo, cpuDifficulty: loadout.cpuDifficulty, profiles, selectedProfileId: lastSelected[2], onProfileSelect: handleP2ProfileSelect, color: p2Color, onColorChange: handleP2ColorChange, swatches: defaultColors, takenColor: p1Color, allowSwapTaken: mode === "vsCpu", colorCaption: pack.colorCaption, dark, secondPicker: p2Control, secondPickerId: p2Control ? "p2-control" : void 0, ready: ready[2], onToggleReady: () => setReady((r) => ({ ...r, 2: !r[2] })), showReadyButton });
|
|
743
|
+
return /* @__PURE__ */ jsxs5(View2, { style: [styles3.container, { backgroundColor: bg }], children: [
|
|
744
|
+
/* @__PURE__ */ jsxs5(FakeLandscapeView, { orientationMode: panelLayout.orientationMode, p1OnRight: panelLayout.p1OnRight, upsideDown: panelLayout.upsideDown, style: styles3.rotatable, children: [
|
|
745
|
+
/* @__PURE__ */ jsx12(PressAwayOverlay, { active: controlsHost.openId !== null, onPress: controlsHost.close }),
|
|
746
|
+
p2ZoneStyle && /* @__PURE__ */ jsx12(PressAwayOverlay, { active: controlsHost.openId !== null || p2Host.openId !== null, onPress: p2Host.close, style: p2ZoneStyle }),
|
|
747
|
+
!showMetaInSharedBand && /* @__PURE__ */ jsx12(CornerActionButtons, { onBack: safeBack, onSettings: () => setSettingsOpen(true), fg, insets }),
|
|
748
|
+
p2IsHuman ? /* @__PURE__ */ jsx12(
|
|
749
|
+
DualZoneLayout,
|
|
750
|
+
{
|
|
751
|
+
panelLayout,
|
|
752
|
+
panelFadeStyle,
|
|
753
|
+
p1: p1Panel,
|
|
754
|
+
p2: p2Panel,
|
|
755
|
+
shared: sharedControls,
|
|
756
|
+
p1Elevated: controlsHost.openId?.startsWith("p1-") ?? false,
|
|
757
|
+
p2Elevated: p2Host.openId !== null,
|
|
758
|
+
sharedElevated: optionsPopoverOpen
|
|
759
|
+
}
|
|
760
|
+
) : /* @__PURE__ */ jsxs5(View2, { style: styles3.stackedZone, children: [
|
|
761
|
+
sharedControls,
|
|
762
|
+
/* @__PURE__ */ jsxs5(Animated.View, { style: [styles3.playersRow, anyPlayerPickerOpen && styles3.playersRowOpen, panelFadeStyle], children: [
|
|
763
|
+
!isPortrait && panelLayout.p1OnRight ? p2Panel : p1Panel,
|
|
764
|
+
!isPortrait && panelLayout.p1OnRight ? p1Panel : p2Panel
|
|
765
|
+
] }),
|
|
766
|
+
/* @__PURE__ */ jsx12(ReadyButton2, { color: p1Color, ready: ready[1], onToggleReady: () => setReady((r) => ({ ...r, 1: !r[1] })), style: anyPlayerPickerOpen && styles3.readyButtonHidden })
|
|
767
|
+
] })
|
|
768
|
+
] }),
|
|
769
|
+
/* @__PURE__ */ jsx12(SettingsDialog, { visible: settingsOpen, onDismiss: () => setSettingsOpen(false), rotation, onShowHowToPlay: () => openFor(game.id) })
|
|
770
|
+
] });
|
|
771
|
+
}
|
|
772
|
+
var styles3 = StyleSheet3.create({
|
|
773
|
+
container: {
|
|
774
|
+
flex: 1
|
|
775
|
+
},
|
|
776
|
+
p2ZoneLeft: {
|
|
777
|
+
bottom: 0,
|
|
778
|
+
left: 0,
|
|
779
|
+
position: "absolute",
|
|
780
|
+
top: 0,
|
|
781
|
+
width: "50%"
|
|
782
|
+
},
|
|
783
|
+
p2ZoneRight: {
|
|
784
|
+
bottom: 0,
|
|
785
|
+
position: "absolute",
|
|
786
|
+
right: 0,
|
|
787
|
+
top: 0,
|
|
788
|
+
width: "50%"
|
|
789
|
+
},
|
|
790
|
+
p2ZoneTop: {
|
|
791
|
+
height: "50%",
|
|
792
|
+
left: 0,
|
|
793
|
+
position: "absolute",
|
|
794
|
+
right: 0,
|
|
795
|
+
top: 0
|
|
796
|
+
},
|
|
797
|
+
// The one-human row: seat 1's panel and seat 2's button read as one evenly spaced row of buttons,
|
|
798
|
+
// at PlayerSetupPanel's own pickerRow gap (12).
|
|
799
|
+
playersRow: {
|
|
800
|
+
flexDirection: "row",
|
|
801
|
+
gap: 12
|
|
802
|
+
},
|
|
803
|
+
playersRowOpen: {
|
|
804
|
+
zIndex: 100
|
|
805
|
+
},
|
|
806
|
+
// See anyPlayerPickerOpen: opacity 0 (not display:'none') keeps the button's layout space, and
|
|
807
|
+
// pointerEvents:'none' keeps the sliver an open menu doesn't cover from being a hidden tap target.
|
|
808
|
+
readyButtonHidden: {
|
|
809
|
+
opacity: 0,
|
|
810
|
+
pointerEvents: "none"
|
|
811
|
+
},
|
|
812
|
+
// A real full-bleed box for FakeLandscapeView to size its absolutely-positioned children against.
|
|
813
|
+
rotatable: {
|
|
814
|
+
alignItems: "center",
|
|
815
|
+
flex: 1,
|
|
816
|
+
gap: 32,
|
|
817
|
+
justifyContent: "center",
|
|
818
|
+
paddingHorizontal: 16
|
|
819
|
+
},
|
|
820
|
+
stackedZone: {
|
|
821
|
+
alignItems: "center",
|
|
822
|
+
gap: 28
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
// src/screens/GameRoute.tsx
|
|
827
|
+
import { jsx as jsx13 } from "react/jsx-runtime";
|
|
828
|
+
function LoadoutRoute() {
|
|
829
|
+
const { game: id } = useLocalSearchParams();
|
|
830
|
+
const game = usePackGame(id);
|
|
831
|
+
if (!game) return /* @__PURE__ */ jsx13(Redirect, { href: "/" });
|
|
832
|
+
if (!hasLoadout(game)) return /* @__PURE__ */ jsx13(Redirect, { href: { pathname: "/play/[game]", params: { game: game.id } } });
|
|
833
|
+
return /* @__PURE__ */ jsx13(LoadoutScreen, { game }, game.id);
|
|
834
|
+
}
|
|
835
|
+
function hasLoadout(game) {
|
|
836
|
+
return !!game.useLoadout;
|
|
837
|
+
}
|
|
838
|
+
function PlayRoute() {
|
|
839
|
+
const { game: id } = useLocalSearchParams();
|
|
840
|
+
const game = usePackGame(id);
|
|
841
|
+
useResumePoint(game?.resumable ? game.id : null);
|
|
842
|
+
if (!game) return /* @__PURE__ */ jsx13(Redirect, { href: "/" });
|
|
843
|
+
const { PlayScreen } = game;
|
|
844
|
+
return /* @__PURE__ */ jsx13(PlayScreen, {}, game.id);
|
|
845
|
+
}
|
|
846
|
+
function useResumePoint(gameId) {
|
|
847
|
+
const { setLastActiveGame, clearLastActiveGame } = useLastActiveGame();
|
|
848
|
+
useEffect5(() => {
|
|
849
|
+
if (!gameId) return;
|
|
850
|
+
setLastActiveGame(gameId);
|
|
851
|
+
return clearLastActiveGame;
|
|
852
|
+
}, [gameId, setLastActiveGame, clearLastActiveGame]);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// src/screens/HomeScreen.tsx
|
|
856
|
+
import { defaultColors as defaultColors2, useAutoPaperTheme as useAutoPaperTheme4 } from "@rific/auto-paper";
|
|
857
|
+
import { IconButton, Pressable } from "@rific/feedback-press";
|
|
858
|
+
import { FakeLandscapeView as FakeLandscapeView2, useRotation as useRotation4 } from "@tastic/core";
|
|
859
|
+
import { PressAwayOverlay as PressAwayOverlay2, usePopoverHost as usePopoverHost2, useWellColors, WELL_SHADOW } from "@tastic/hud";
|
|
860
|
+
import { ProfilePicker } from "@tastic/profile";
|
|
861
|
+
import { Redirect as Redirect2, router as router3, useIsFocused } from "expo-router";
|
|
862
|
+
import { useCallback as useCallback10, useEffect as useEffect6, useState as useState6 } from "react";
|
|
863
|
+
import { AppState, ScrollView, StyleSheet as StyleSheet4, View as View3 } from "react-native";
|
|
864
|
+
import { Icon, Text } from "react-native-paper";
|
|
865
|
+
|
|
866
|
+
// src/hooks/useFrozenWhileHidden.ts
|
|
867
|
+
import { useState as useState5 } from "react";
|
|
868
|
+
function useFrozenWhileHidden(value, hidden) {
|
|
869
|
+
const [prevHidden, setPrevHidden] = useState5(hidden);
|
|
870
|
+
const [prevValue, setPrevValue] = useState5(value);
|
|
871
|
+
const [frozen, setFrozen] = useState5(value);
|
|
872
|
+
if (hidden !== prevHidden) {
|
|
873
|
+
setPrevHidden(hidden);
|
|
874
|
+
if (hidden) setFrozen(prevValue);
|
|
875
|
+
}
|
|
876
|
+
if (value !== prevValue) {
|
|
877
|
+
setPrevValue(value);
|
|
878
|
+
}
|
|
879
|
+
return hidden ? frozen : value;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// src/screens/homeLayout.ts
|
|
883
|
+
import { clamp, rotateInsets as rotateInsets2, useRotatedWindowDimensions, useRotation as useRotation3 } from "@tastic/core";
|
|
884
|
+
import { useSafeAreaInsets as useSafeAreaInsets2 } from "react-native-safe-area-context";
|
|
885
|
+
var PREVIEW_ASPECT = 360 / 200;
|
|
886
|
+
var ROW_PREVIEW_WIDTH = 92;
|
|
887
|
+
var HOME_WIDE_EDGE_PADDING = 16;
|
|
888
|
+
var HOME_WIDE_PANE_GAP = 16;
|
|
889
|
+
var HOME_WIDE_RIGHT_GUTTER = 56;
|
|
890
|
+
var HOME_WIDE_MAX_CONTENT_WIDTH = 1100;
|
|
891
|
+
var HOME_LEFT_PANE_FRACTION = 0.38;
|
|
892
|
+
var HOME_LEFT_PANE_MIN = 260;
|
|
893
|
+
var HOME_LEFT_PANE_MAX = 340;
|
|
894
|
+
var HOME_GRID_GAP = 12;
|
|
895
|
+
var HOME_MIN_TILE_WIDTH = 140;
|
|
896
|
+
var HOME_MAX_COLUMNS = 3;
|
|
897
|
+
var HOME_TILE_PADDING = 10;
|
|
898
|
+
var HOME_TOP_BAND = 56;
|
|
899
|
+
var HOME_LEFT_PANE_PICKER_HEIGHT = 44;
|
|
900
|
+
var HOME_LEFT_PANE_PICKER_GAP = 12;
|
|
901
|
+
var HOME_LEFT_PANE_BOTTOM_PADDING = 16;
|
|
902
|
+
var HOME_HERO_SIDE_ROOM = 16;
|
|
903
|
+
function computeHomeLayout(windowWidth, windowHeight, insets) {
|
|
904
|
+
const isWide = windowWidth > windowHeight;
|
|
905
|
+
if (!isWide) {
|
|
906
|
+
return { isWide, windowWidth, windowHeight, insets, paddingLeft: 0, paddingRight: 0, leftPaneWidth: 0, rightPaneWidth: 0, columns: 1, tileWidth: 0, previewWidth: 0, heroFit: void 0 };
|
|
907
|
+
}
|
|
908
|
+
const usable = windowWidth - insets.left - insets.right;
|
|
909
|
+
const inner = Math.max(0, usable - HOME_WIDE_EDGE_PADDING - HOME_WIDE_RIGHT_GUTTER);
|
|
910
|
+
const content = Math.min(inner, HOME_WIDE_MAX_CONTENT_WIDTH);
|
|
911
|
+
const slack = (inner - content) / 2;
|
|
912
|
+
const leftPaneWidth = Math.min(clamp(Math.round(HOME_LEFT_PANE_FRACTION * content), HOME_LEFT_PANE_MIN, HOME_LEFT_PANE_MAX), Math.max(0, content - HOME_WIDE_PANE_GAP - HOME_MIN_TILE_WIDTH));
|
|
913
|
+
const rightPaneWidth = Math.max(0, content - HOME_WIDE_PANE_GAP - leftPaneWidth);
|
|
914
|
+
const columns = clamp(Math.floor((rightPaneWidth + HOME_GRID_GAP) / (HOME_MIN_TILE_WIDTH + HOME_GRID_GAP)), 1, HOME_MAX_COLUMNS);
|
|
915
|
+
const tileWidth = Math.max(0, Math.floor((rightPaneWidth - (columns - 1) * HOME_GRID_GAP) / columns));
|
|
916
|
+
const previewWidth = Math.max(0, tileWidth - 2 * HOME_TILE_PADDING);
|
|
917
|
+
const heroFit = {
|
|
918
|
+
width: Math.max(0, leftPaneWidth - HOME_HERO_SIDE_ROOM),
|
|
919
|
+
height: Math.max(0, windowHeight - insets.top - insets.bottom - HOME_TOP_BAND - HOME_LEFT_PANE_PICKER_GAP - HOME_LEFT_PANE_PICKER_HEIGHT - HOME_LEFT_PANE_BOTTOM_PADDING)
|
|
920
|
+
};
|
|
921
|
+
return {
|
|
922
|
+
isWide,
|
|
923
|
+
windowWidth,
|
|
924
|
+
windowHeight,
|
|
925
|
+
insets,
|
|
926
|
+
paddingLeft: insets.left + HOME_WIDE_EDGE_PADDING + slack,
|
|
927
|
+
paddingRight: insets.right + HOME_WIDE_RIGHT_GUTTER + slack,
|
|
928
|
+
leftPaneWidth,
|
|
929
|
+
rightPaneWidth,
|
|
930
|
+
columns,
|
|
931
|
+
tileWidth,
|
|
932
|
+
previewWidth,
|
|
933
|
+
heroFit
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
function useHomeLayout() {
|
|
937
|
+
const { width, height } = useRotatedWindowDimensions();
|
|
938
|
+
const rotation = useRotation3();
|
|
939
|
+
const insets = rotateInsets2(useSafeAreaInsets2(), rotation);
|
|
940
|
+
return computeHomeLayout(width, height, insets);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// src/screens/HomeScreen.tsx
|
|
944
|
+
import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
945
|
+
function HomeScreen() {
|
|
946
|
+
const pack = usePack();
|
|
947
|
+
const { consumeResumeGame } = useLastActiveGame();
|
|
948
|
+
const [resumeGame] = useState6(() => {
|
|
949
|
+
const id = consumeResumeGame();
|
|
950
|
+
return pack.games.find((g) => g.id === id && g.resumable) ?? null;
|
|
951
|
+
});
|
|
952
|
+
if (resumeGame) return /* @__PURE__ */ jsx14(Redirect2, { href: { pathname: "/play/[game]", params: { game: resumeGame.id } } });
|
|
953
|
+
return /* @__PURE__ */ jsx14(HomeMenu, {});
|
|
954
|
+
}
|
|
955
|
+
function HomeMenu() {
|
|
956
|
+
const pack = usePack();
|
|
957
|
+
const rotation = useRotation4();
|
|
958
|
+
const layout = useHomeLayout();
|
|
959
|
+
const { insets } = layout;
|
|
960
|
+
const { dark } = useAutoPaperTheme4();
|
|
961
|
+
const { openFirstTime, openFor } = useGameGuide();
|
|
962
|
+
const [settingsOpen, setSettingsOpen] = useState6(false);
|
|
963
|
+
const isFocused = useIsFocused();
|
|
964
|
+
const [appState, setAppState] = useState6(AppState.currentState);
|
|
965
|
+
useEffect6(() => {
|
|
966
|
+
const subscription = AppState.addEventListener("change", setAppState);
|
|
967
|
+
return () => subscription.remove();
|
|
968
|
+
}, []);
|
|
969
|
+
const heroActive = isFocused && appState === "active";
|
|
970
|
+
const { profiles, lastSelected, selectProfile } = useProfiles();
|
|
971
|
+
const { seatTwo } = useOpponent();
|
|
972
|
+
const { p1Color, p2Color, handleP1ColorChange } = useSeatColors(seatTwo === "person");
|
|
973
|
+
const pickerHost = usePopoverHost2();
|
|
974
|
+
const selectedProfile = profiles.find((p) => p.id === lastSelected[1]) ?? null;
|
|
975
|
+
const tiled = pack.games.some((game) => game.Preview);
|
|
976
|
+
const isWide = useFrozenWhileHidden(tiled && layout.isWide, !isFocused);
|
|
977
|
+
const [previewsRevealed, setPreviewsRevealed] = useState6(false);
|
|
978
|
+
useEffect6(() => {
|
|
979
|
+
if (!tiled) return;
|
|
980
|
+
const id = requestAnimationFrame(() => setPreviewsRevealed(true));
|
|
981
|
+
return () => cancelAnimationFrame(id);
|
|
982
|
+
}, [tiled]);
|
|
983
|
+
const closePicker = pickerHost.close;
|
|
984
|
+
useEffect6(() => {
|
|
985
|
+
closePicker();
|
|
986
|
+
}, [isWide, closePicker]);
|
|
987
|
+
const bg = dark ? "#000000" : "#FFFFFF";
|
|
988
|
+
const fg = dark ? "#FFFFFF" : "#000000";
|
|
989
|
+
const openGame = useCallback10((game) => openFirstTime(game.id, () => router3.push(game.useLoadout ? { pathname: "/loadout/[game]", params: { game: game.id } } : { pathname: "/play/[game]", params: { game: game.id } })), [openFirstTime]);
|
|
990
|
+
const picker = /* @__PURE__ */ jsx14(ProfilePicker, { idPrefix: "home-p1", host: pickerHost, profiles, selectedId: lastSelected[1], color: p1Color, dark, guestLabel: "P1", onSelect: (profile) => selectProfile(1, profile?.id ?? null), onManage: () => router3.push("/profiles"), colorSection: { value: p1Color, onChange: handleP1ColorChange, swatches: defaultColors2, caption: selectedProfile ? pack.colorCaption : void 0 } });
|
|
991
|
+
return /* @__PURE__ */ jsxs6(View3, { style: [styles4.container, { backgroundColor: bg }], children: [
|
|
992
|
+
/* @__PURE__ */ jsxs6(FakeLandscapeView2, { style: styles4.rotatable, children: [
|
|
993
|
+
/* @__PURE__ */ jsx14(PressAwayOverlay2, { active: pickerHost.openId !== null, onPress: pickerHost.close }),
|
|
994
|
+
isWide ? /* @__PURE__ */ jsxs6(Fragment3, { children: [
|
|
995
|
+
/* @__PURE__ */ jsx14(ScrollView, { style: [styles4.wideRightPane, { left: layout.paddingLeft + layout.leftPaneWidth + HOME_WIDE_PANE_GAP }], contentContainerStyle: { paddingTop: 16 + insets.top, paddingBottom: 16 + insets.bottom, paddingRight: layout.paddingRight }, children: /* @__PURE__ */ jsx14(View3, { style: [styles4.grid, { width: layout.rightPaneWidth }], children: pack.games.map((game) => /* @__PURE__ */ jsx14(GameTile, { game, width: layout.tileWidth, previewWidth: layout.previewWidth, previewRevealed: previewsRevealed, dark, p1Color, p2Color, onPress: () => openGame(game), onRules: () => openFor(game.id) }, game.id)) }) }),
|
|
996
|
+
/* @__PURE__ */ jsxs6(View3, { style: [styles4.wideLeftPane, { left: layout.paddingLeft, width: layout.leftPaneWidth, paddingTop: HOME_TOP_BAND + insets.top, paddingBottom: 16 + insets.bottom }, pickerHost.openId !== null && styles4.pickerOpen], children: [
|
|
997
|
+
/* @__PURE__ */ jsx14(pack.HeroTitle, { words: pack.wordmark, color: fg, p1Color, p2Color, active: heroActive, fit: layout.heroFit }),
|
|
998
|
+
/* @__PURE__ */ jsx14(View3, { style: styles4.widePicker, children: picker })
|
|
999
|
+
] })
|
|
1000
|
+
] }) : /* @__PURE__ */ jsxs6(ScrollView, { style: styles4.scroll, contentContainerStyle: [styles4.content, { paddingTop: 56 + insets.top, paddingBottom: 24 + insets.bottom, paddingLeft: 16 + insets.left, paddingRight: 16 + insets.right }], children: [
|
|
1001
|
+
/* @__PURE__ */ jsx14(pack.HeroTitle, { words: pack.wordmark, color: fg, p1Color, p2Color, active: heroActive }),
|
|
1002
|
+
/* @__PURE__ */ jsx14(View3, { style: [styles4.picker, pickerHost.openId !== null && styles4.pickerOpen], children: picker }),
|
|
1003
|
+
/* @__PURE__ */ jsx14(View3, { style: styles4.list, children: pack.games.map((game) => /* @__PURE__ */ jsx14(GameRow, { game, dark, p1Color, p2Color, previewRevealed: previewsRevealed, onPress: () => openGame(game), onRules: () => openFor(game.id) }, game.id)) })
|
|
1004
|
+
] }),
|
|
1005
|
+
/* @__PURE__ */ jsx14(IconButton, { icon: "trophy", iconColor: fg, size: 24, style: [styles4.corner, { top: 8 + insets.top, left: 8 + insets.left }], onPress: () => router3.push("/stats"), accessibilityLabel: "Stats & Achievements" }),
|
|
1006
|
+
/* @__PURE__ */ jsx14(IconButton, { icon: "cog", iconColor: fg, size: 24, style: [styles4.corner, { top: 8 + insets.top, right: 8 + insets.right }], onPress: () => setSettingsOpen(true), accessibilityLabel: "Settings" })
|
|
1007
|
+
] }),
|
|
1008
|
+
/* @__PURE__ */ jsx14(SettingsDialog, { visible: settingsOpen, onDismiss: () => setSettingsOpen(false), rotation })
|
|
1009
|
+
] });
|
|
1010
|
+
}
|
|
1011
|
+
function GameRow({ game, dark, p1Color, p2Color, previewRevealed, onPress, onRules }) {
|
|
1012
|
+
const well = useWellColors();
|
|
1013
|
+
const GameIcon = game.icon;
|
|
1014
|
+
return (
|
|
1015
|
+
// The rules button sits beside the row's own button, laid over its end, not inside it: a button
|
|
1016
|
+
// inside a button is invalid on the web.
|
|
1017
|
+
/* @__PURE__ */ jsxs6(View3, { children: [
|
|
1018
|
+
/* @__PURE__ */ jsxs6(Pressable, { onPress, style: [styles4.row, { backgroundColor: well.bg }], accessibilityRole: "button", accessibilityLabel: game.title, children: [
|
|
1019
|
+
game.Preview ? /* @__PURE__ */ jsx14(PreviewBox, { game, width: ROW_PREVIEW_WIDTH, revealed: previewRevealed, dark, p1Color, p2Color }) : /* @__PURE__ */ jsx14(View3, { style: [styles4.rowIcon, { backgroundColor: dark ? ICON_DARK_BG : ICON_LIGHT_BG }], children: /* @__PURE__ */ jsx14(GameIcon, { size: ROW_ICON_SIZE, p1Color, p2Color, dark }) }),
|
|
1020
|
+
/* @__PURE__ */ jsxs6(View3, { style: styles4.rowText, children: [
|
|
1021
|
+
/* @__PURE__ */ jsx14(Text, { variant: "titleLarge", style: [styles4.rowTitle, { color: well.fg }], numberOfLines: 1, children: game.title }),
|
|
1022
|
+
/* @__PURE__ */ jsx14(Text, { variant: "bodyMedium", style: { color: well.fgMuted }, numberOfLines: 2, children: game.blurb })
|
|
1023
|
+
] })
|
|
1024
|
+
] }),
|
|
1025
|
+
/* @__PURE__ */ jsx14(Pressable, { onPress: onRules, style: styles4.rulesButton, hitSlop: 12, accessibilityRole: "button", accessibilityLabel: `${game.title} rules`, children: /* @__PURE__ */ jsx14(Icon, { source: "information-outline", size: 22, color: well.fgMuted }) })
|
|
1026
|
+
] })
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
function GameTile({ game, width, previewWidth, dark, p1Color, p2Color, previewRevealed, onPress, onRules }) {
|
|
1030
|
+
const well = useWellColors();
|
|
1031
|
+
return /* @__PURE__ */ jsxs6(View3, { style: { width }, children: [
|
|
1032
|
+
/* @__PURE__ */ jsxs6(Pressable, { onPress, style: [styles4.tile, { backgroundColor: well.bg }], accessibilityRole: "button", accessibilityLabel: game.title, children: [
|
|
1033
|
+
/* @__PURE__ */ jsx14(PreviewBox, { game, width: previewWidth, revealed: previewRevealed, dark, p1Color, p2Color }),
|
|
1034
|
+
/* @__PURE__ */ jsx14(Text, { variant: "titleMedium", style: [styles4.rowTitle, { color: well.fg }], numberOfLines: 1, children: game.title }),
|
|
1035
|
+
/* @__PURE__ */ jsx14(Text, { variant: "bodySmall", style: { color: well.fgMuted }, numberOfLines: 3, children: game.blurb })
|
|
1036
|
+
] }),
|
|
1037
|
+
/* @__PURE__ */ jsx14(Pressable, { onPress: onRules, style: styles4.tileRulesButton, hitSlop: 12, accessibilityRole: "button", accessibilityLabel: `${game.title} rules`, children: /* @__PURE__ */ jsx14(Icon, { source: "information-outline", size: 20, color: "#FFFFFF" }) })
|
|
1038
|
+
] });
|
|
1039
|
+
}
|
|
1040
|
+
function PreviewBox({ game, width, revealed, dark, p1Color, p2Color }) {
|
|
1041
|
+
const height = Math.round(width / PREVIEW_ASPECT);
|
|
1042
|
+
const { Preview, icon: GameIcon } = game;
|
|
1043
|
+
const iconSize = Math.min(height, ROW_ICON_SIZE);
|
|
1044
|
+
return /* @__PURE__ */ jsx14(View3, { style: [styles4.preview, { width, height, backgroundColor: dark ? ICON_DARK_BG : ICON_LIGHT_BG }], children: Preview ? revealed && /* @__PURE__ */ jsx14(Preview, { width, height, p1Color, p2Color, dark }) : /* @__PURE__ */ jsx14(View3, { style: [styles4.previewIcon, { width: iconSize, height: iconSize, borderRadius: iconSize * 0.22 }], children: /* @__PURE__ */ jsx14(GameIcon, { size: iconSize, p1Color, p2Color, dark }) }) });
|
|
1045
|
+
}
|
|
1046
|
+
var ROW_ICON_SIZE = 56;
|
|
1047
|
+
var ICON_LIGHT_BG = "#FAFAFA";
|
|
1048
|
+
var ICON_DARK_BG = "#050505";
|
|
1049
|
+
var RULES_BUTTON_SPACE = 44;
|
|
1050
|
+
var styles4 = StyleSheet4.create({
|
|
1051
|
+
container: {
|
|
1052
|
+
flex: 1
|
|
1053
|
+
},
|
|
1054
|
+
content: {
|
|
1055
|
+
alignItems: "center",
|
|
1056
|
+
gap: 24
|
|
1057
|
+
},
|
|
1058
|
+
corner: {
|
|
1059
|
+
position: "absolute"
|
|
1060
|
+
},
|
|
1061
|
+
// The wide layout's tiles: wrapped rows of tiles the layout sizes to fill the pane's width.
|
|
1062
|
+
grid: {
|
|
1063
|
+
flexDirection: "row",
|
|
1064
|
+
flexWrap: "wrap",
|
|
1065
|
+
gap: HOME_GRID_GAP
|
|
1066
|
+
},
|
|
1067
|
+
// Solitaire's list: one column, never wider than a phone's, centred on a wide screen.
|
|
1068
|
+
list: {
|
|
1069
|
+
gap: 14,
|
|
1070
|
+
maxWidth: 480,
|
|
1071
|
+
width: "100%"
|
|
1072
|
+
},
|
|
1073
|
+
picker: {
|
|
1074
|
+
alignItems: "center"
|
|
1075
|
+
},
|
|
1076
|
+
pickerOpen: {
|
|
1077
|
+
zIndex: 100
|
|
1078
|
+
},
|
|
1079
|
+
// The preview's own rounding, clipping whatever it draws to the box.
|
|
1080
|
+
preview: {
|
|
1081
|
+
alignItems: "center",
|
|
1082
|
+
borderRadius: 8,
|
|
1083
|
+
justifyContent: "center",
|
|
1084
|
+
overflow: "hidden"
|
|
1085
|
+
},
|
|
1086
|
+
previewIcon: {
|
|
1087
|
+
overflow: "hidden"
|
|
1088
|
+
},
|
|
1089
|
+
rotatable: {
|
|
1090
|
+
flex: 1
|
|
1091
|
+
},
|
|
1092
|
+
// A well (@tastic/hud's wellSurface): the theme's surface on MD3's level-1 shadow, the same surface
|
|
1093
|
+
// as Solitaire's game rows, the stats screen's sections and every dialog.
|
|
1094
|
+
row: {
|
|
1095
|
+
alignItems: "center",
|
|
1096
|
+
borderRadius: 12,
|
|
1097
|
+
boxShadow: WELL_SHADOW,
|
|
1098
|
+
flexDirection: "row",
|
|
1099
|
+
gap: 14,
|
|
1100
|
+
padding: 14,
|
|
1101
|
+
paddingRight: RULES_BUTTON_SPACE
|
|
1102
|
+
},
|
|
1103
|
+
rowIcon: {
|
|
1104
|
+
borderRadius: ROW_ICON_SIZE * 0.22,
|
|
1105
|
+
height: ROW_ICON_SIZE,
|
|
1106
|
+
overflow: "hidden",
|
|
1107
|
+
width: ROW_ICON_SIZE
|
|
1108
|
+
},
|
|
1109
|
+
rowText: {
|
|
1110
|
+
flex: 1,
|
|
1111
|
+
gap: 2
|
|
1112
|
+
},
|
|
1113
|
+
rowTitle: {
|
|
1114
|
+
fontWeight: "bold"
|
|
1115
|
+
},
|
|
1116
|
+
rulesButton: {
|
|
1117
|
+
bottom: 0,
|
|
1118
|
+
justifyContent: "center",
|
|
1119
|
+
position: "absolute",
|
|
1120
|
+
right: 14,
|
|
1121
|
+
top: 0
|
|
1122
|
+
},
|
|
1123
|
+
scroll: {
|
|
1124
|
+
flex: 1
|
|
1125
|
+
},
|
|
1126
|
+
// The same well as a row, turned on its side.
|
|
1127
|
+
tile: {
|
|
1128
|
+
borderRadius: 12,
|
|
1129
|
+
boxShadow: WELL_SHADOW,
|
|
1130
|
+
gap: 6,
|
|
1131
|
+
padding: HOME_TILE_PADDING
|
|
1132
|
+
},
|
|
1133
|
+
// A small translucent disc over the preview's top-right corner, so the white icon reads on whatever
|
|
1134
|
+
// the preview draws. hitSlop (on the Pressable) grows its touch area.
|
|
1135
|
+
tileRulesButton: {
|
|
1136
|
+
alignItems: "center",
|
|
1137
|
+
backgroundColor: "rgba(0, 0, 0, 0.35)",
|
|
1138
|
+
borderRadius: 14,
|
|
1139
|
+
height: 28,
|
|
1140
|
+
justifyContent: "center",
|
|
1141
|
+
position: "absolute",
|
|
1142
|
+
right: HOME_TILE_PADDING + 4,
|
|
1143
|
+
top: HOME_TILE_PADDING + 4,
|
|
1144
|
+
width: 28
|
|
1145
|
+
},
|
|
1146
|
+
// The two panes are absolutely positioned siblings of the press-away overlay, each placed by the
|
|
1147
|
+
// offsets homeLayout.ts computes.
|
|
1148
|
+
wideLeftPane: {
|
|
1149
|
+
alignItems: "center",
|
|
1150
|
+
bottom: 0,
|
|
1151
|
+
justifyContent: "center",
|
|
1152
|
+
pointerEvents: "box-none",
|
|
1153
|
+
position: "absolute",
|
|
1154
|
+
top: 0
|
|
1155
|
+
},
|
|
1156
|
+
widePicker: {
|
|
1157
|
+
marginTop: HOME_LEFT_PANE_PICKER_GAP
|
|
1158
|
+
},
|
|
1159
|
+
wideRightPane: {
|
|
1160
|
+
bottom: 0,
|
|
1161
|
+
position: "absolute",
|
|
1162
|
+
right: 0,
|
|
1163
|
+
top: 0
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
// src/screens/ProfilesRoute.tsx
|
|
1168
|
+
import { defaultColors as defaultColors3 } from "@rific/auto-paper";
|
|
1169
|
+
import { ProfilesScreen } from "@tastic/profile";
|
|
1170
|
+
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
1171
|
+
function ProfilesRoute() {
|
|
1172
|
+
const { profiles, createProfile, updateProfile, deleteProfile } = useProfiles();
|
|
1173
|
+
return /* @__PURE__ */ jsx15(ProfilesScreen, { profiles, defaultColor: defaultColors3[0].value, onCreate: (patch) => createProfile(patch), onSave: (id, patch) => updateProfile(id, patch), onDelete: deleteProfile, onBack: safeBack });
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// src/screens/StatsScreen.tsx
|
|
1177
|
+
import { useAutoPaperTheme as useAutoPaperTheme5 } from "@rific/auto-paper";
|
|
1178
|
+
import { FakeLandscapeView as FakeLandscapeView3, rotateInsets as rotateInsets3, useRotation as useRotation5 } from "@tastic/core";
|
|
1179
|
+
import { BaseStatsScreen, PressAwayOverlay as PressAwayOverlay3, usePopoverHost as usePopoverHost3 } from "@tastic/hud";
|
|
1180
|
+
import { OptionPicker as OptionPicker2 } from "@tastic/profile";
|
|
1181
|
+
import { useMemo as useMemo5, useState as useState7 } from "react";
|
|
1182
|
+
import { StyleSheet as StyleSheet5, View as View4 } from "react-native";
|
|
1183
|
+
import { SegmentedButtons, Text as Text2 } from "react-native-paper";
|
|
1184
|
+
import { useSafeAreaInsets as useSafeAreaInsets3 } from "react-native-safe-area-context";
|
|
1185
|
+
import { useDispatch as useDispatch10 } from "react-redux";
|
|
1186
|
+
import { jsx as jsx16, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1187
|
+
var TABS = [
|
|
1188
|
+
{ value: "stats", label: "Stats", icon: "chart-box-outline" },
|
|
1189
|
+
{ value: "achievements", label: "Achievements", icon: "trophy-outline" }
|
|
1190
|
+
];
|
|
1191
|
+
var EVERYONE = "__everyone__";
|
|
1192
|
+
var ALL_GAMES = "__all__";
|
|
1193
|
+
var PLAYER_PICKER_ID = "stats-player";
|
|
1194
|
+
var GAME_PICKER_ID = "stats-game";
|
|
1195
|
+
function StatsScreen() {
|
|
1196
|
+
const pack = usePack();
|
|
1197
|
+
const { dark, colors } = useAutoPaperTheme5();
|
|
1198
|
+
const rotation = useRotation5();
|
|
1199
|
+
const insets = rotateInsets3(useSafeAreaInsets3(), rotation);
|
|
1200
|
+
const { profiles } = useProfiles();
|
|
1201
|
+
const dispatch = useDispatch10();
|
|
1202
|
+
const pickerHost = usePopoverHost3();
|
|
1203
|
+
const [tab, setTab] = useState7("stats");
|
|
1204
|
+
const [gameId, setGameId] = useState7(ALL_GAMES);
|
|
1205
|
+
const [selectedProfileId, setSelectedProfileId] = useState7(null);
|
|
1206
|
+
const profileId = selectedProfileId && profiles.some((p) => p.id === selectedProfileId) ? selectedProfileId : null;
|
|
1207
|
+
const games = gameId === ALL_GAMES ? pack.games : pack.games.filter((g) => g.id === gameId);
|
|
1208
|
+
const playerOptions = useMemo5(() => [{ value: EVERYONE, label: "Everyone", icon: "account-group" }, ...profiles.map((p) => ({ value: p.id, label: p.name, icon: "account", color: p.color }))], [profiles]);
|
|
1209
|
+
const gameOptions = useMemo5(() => [{ value: ALL_GAMES, label: "All Games", icon: "view-grid-outline" }, ...pack.games.map((g) => ({ value: g.id, label: g.title }))], [pack.games]);
|
|
1210
|
+
const resetScope = games.length === 1 ? games[0].title : "every game";
|
|
1211
|
+
const onReset = () => games.forEach((g) => dispatch(g.resetStats()));
|
|
1212
|
+
return /* @__PURE__ */ jsx16(FakeLandscapeView3, { style: styles5.rotatable, children: /* @__PURE__ */ jsxs7(BaseStatsScreen, { title: tab === "stats" ? "Stats" : "Achievements", onBack: safeBack, insets, onReset, resetLabel: games.length === 1 ? `Reset ${resetScope} Stats` : "Reset All Stats", resetConfirmTitle: "Reset Stats?", resetConfirmBody: `Erases every player's stats and achievements in ${resetScope}. This cannot be undone.`, rotation, children: [
|
|
1213
|
+
/* @__PURE__ */ jsx16(PressAwayOverlay3, { active: pickerHost.openId !== null, onPress: pickerHost.close, style: styles5.pressAway }),
|
|
1214
|
+
/* @__PURE__ */ jsx16(SegmentedButtons, { value: tab, onValueChange: (value) => setTab(value), buttons: TABS, style: styles5.tabs }),
|
|
1215
|
+
/* @__PURE__ */ jsxs7(View4, { style: [styles5.filters, pickerHost.openId !== null && styles5.filtersOpen], children: [
|
|
1216
|
+
profiles.length > 0 && /* @__PURE__ */ jsx16(OptionPicker2, { id: PLAYER_PICKER_ID, host: pickerHost, options: playerOptions, value: profileId ?? EVERYONE, onChange: (value) => setSelectedProfileId(value === EVERYONE ? null : value), color: colors.primary, dark, icon: profileId ? "account" : "account-group" }),
|
|
1217
|
+
/* @__PURE__ */ jsx16(OptionPicker2, { id: GAME_PICKER_ID, host: pickerHost, options: gameOptions, value: gameId, onChange: setGameId, color: colors.primary, dark, icon: "view-grid-outline" })
|
|
1218
|
+
] }),
|
|
1219
|
+
tab === "stats" && pack.StatsSections && /* @__PURE__ */ jsx16(pack.StatsSections, { profileId, gameId: gameId === ALL_GAMES ? null : gameId }),
|
|
1220
|
+
games.map((game) => /* @__PURE__ */ jsx16(GameStats, { game, tab, profileId, showTitle: games.length > 1 }, game.id))
|
|
1221
|
+
] }) });
|
|
1222
|
+
}
|
|
1223
|
+
function GameStats({ game, tab, profileId, showTitle }) {
|
|
1224
|
+
const { colors } = useAutoPaperTheme5();
|
|
1225
|
+
const { StatsSections, AchievementList } = game;
|
|
1226
|
+
return /* @__PURE__ */ jsxs7(View4, { style: styles5.game, children: [
|
|
1227
|
+
showTitle && /* @__PURE__ */ jsx16(Text2, { variant: "titleLarge", style: [styles5.gameTitle, { color: colors.onBackground }], children: game.title }),
|
|
1228
|
+
tab === "stats" ? /* @__PURE__ */ jsx16(StatsSections, { profileId }) : /* @__PURE__ */ jsx16(AchievementList, { profileId })
|
|
1229
|
+
] });
|
|
1230
|
+
}
|
|
1231
|
+
var styles5 = StyleSheet5.create({
|
|
1232
|
+
filters: {
|
|
1233
|
+
flexDirection: "row",
|
|
1234
|
+
flexWrap: "wrap",
|
|
1235
|
+
gap: 12,
|
|
1236
|
+
justifyContent: "center"
|
|
1237
|
+
},
|
|
1238
|
+
filtersOpen: {
|
|
1239
|
+
zIndex: 100
|
|
1240
|
+
},
|
|
1241
|
+
game: {
|
|
1242
|
+
gap: 16
|
|
1243
|
+
},
|
|
1244
|
+
gameTitle: {
|
|
1245
|
+
fontWeight: "bold",
|
|
1246
|
+
marginTop: 8
|
|
1247
|
+
},
|
|
1248
|
+
pressAway: {
|
|
1249
|
+
zIndex: 50
|
|
1250
|
+
},
|
|
1251
|
+
// A real full-bleed box for FakeLandscapeView to size its turned content against.
|
|
1252
|
+
rotatable: {
|
|
1253
|
+
flex: 1
|
|
1254
|
+
},
|
|
1255
|
+
// SegmentedButtons' own rounding.
|
|
1256
|
+
tabs: {
|
|
1257
|
+
borderRadius: 20
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
// src/components/imageGameIcon.tsx
|
|
1262
|
+
import { Image } from "react-native";
|
|
1263
|
+
import { jsx as jsx17 } from "react/jsx-runtime";
|
|
1264
|
+
function imageGameIcon(images) {
|
|
1265
|
+
return function ImageGameIcon({ size, dark }) {
|
|
1266
|
+
return /* @__PURE__ */ jsx17(Image, { source: dark ? images.dark : images.light, style: { height: size, width: size }, accessibilityIgnoresInvertColors: true });
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// src/hooks/useActiveRound.ts
|
|
1271
|
+
import { useSelector as useSelector8 } from "react-redux";
|
|
1272
|
+
function useActiveRound(game, defaults) {
|
|
1273
|
+
const round = useSelector8((state) => state.liveplay.activeRound);
|
|
1274
|
+
const seated = !!usePackGame(game)?.seatTwo;
|
|
1275
|
+
if (!round || round.game !== game) return { mode: seated ? "vsCpu" : "solo", settings: defaults };
|
|
1276
|
+
return { mode: round.mode, settings: { ...defaults, ...round.settings } };
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// src/redux/createPackStore.ts
|
|
1280
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
1281
|
+
import { combineReducers, configureStore } from "@reduxjs/toolkit";
|
|
1282
|
+
import { FLUSH, PAUSE, PERSIST, persistReducer, persistStore, PURGE, REGISTER, REHYDRATE } from "redux-persist";
|
|
1283
|
+
|
|
1284
|
+
// src/redux/shellState.ts
|
|
1285
|
+
import { createThemeReducer, getThirdColor as getThirdColor2 } from "@rific/auto-paper";
|
|
1286
|
+
import { keepAwakeReducer } from "@rific/core";
|
|
1287
|
+
import { defaultSoundSettings, hapticReducer, soundReducer } from "@rific/feedback-press";
|
|
1288
|
+
import { profilesReducer } from "@tastic/profile";
|
|
1289
|
+
var initialSoundSettings = { ...defaultSoundSettings, enabled: !__DEV__ };
|
|
1290
|
+
var appSoundReducer = (state = initialSoundSettings, action) => soundReducer(state, action);
|
|
1291
|
+
function createShellReducers({ seatColors, appearance = "system" }) {
|
|
1292
|
+
return {
|
|
1293
|
+
// Explicit primary/secondary triad (rather than a single seed expanded via harmony) so the pack's
|
|
1294
|
+
// identity is pinned exactly instead of drifting with whatever hue math a harmony offset would
|
|
1295
|
+
// produce. Primary/secondary ARE player 1/player 2's own default colors (PackDefinition.seatColors),
|
|
1296
|
+
// so the theme and the default guest colors (seatColorsSlice.ts) can never drift apart. Tertiary is
|
|
1297
|
+
// derived, not picked, via getThirdColor: the hue maximally distant from both (see
|
|
1298
|
+
// @rific/auto-paper's README "Explicit triad" section). The appearance is the pack's ('system',
|
|
1299
|
+
// createThemeReducer's own default, when it sets none).
|
|
1300
|
+
theme: createThemeReducer({ appearance, color: { primary: seatColors[1], secondary: seatColors[2], tertiary: getThirdColor2(seatColors[1], seatColors[2]) } }),
|
|
1301
|
+
haptic: hapticReducer,
|
|
1302
|
+
sound: appSoundReducer,
|
|
1303
|
+
// @rific/core's Keep Awake setting (see Providers.tsx's KeepAwakeBridge), under the namespace its
|
|
1304
|
+
// REHYDRATE handling expects.
|
|
1305
|
+
keepAwake: keepAwakeReducer,
|
|
1306
|
+
// The shared App Group roster (see @tastic/profile's resolveInitialProfiles/
|
|
1307
|
+
// useSharedProfilesSync, wired in hooks/useProfiles.tsx) is layered on top of this as a separate
|
|
1308
|
+
// sync target - this slice persisting normally via redux-persist is what gives it local-fallback
|
|
1309
|
+
// behavior where the shared store isn't available, with no separate AsyncStorage path.
|
|
1310
|
+
profiles: profilesReducer,
|
|
1311
|
+
profileSelection: profileSelectionSlice_default,
|
|
1312
|
+
// The loadout's "remember this color for next time" memory (guest, CPU, and each profile's pick
|
|
1313
|
+
// in this pack) - separate from `theme` above, which always reflects whatever's live right now.
|
|
1314
|
+
seatColors: createSeatColorsReducer(seatColors),
|
|
1315
|
+
appSettings: appSettingsSlice_default,
|
|
1316
|
+
opponent: opponentSlice_default,
|
|
1317
|
+
// Transient per-round state, the one key blacklisted from persistence (see liveplaySlice.ts).
|
|
1318
|
+
liveplay: liveplaySlice_default
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// src/redux/createPackStore.ts
|
|
1323
|
+
var hasError = (action) => typeof action === "object" && action !== null && "error" in action && Boolean(action.error);
|
|
1324
|
+
var errorMiddleware = () => (next) => (action) => {
|
|
1325
|
+
if (!hasError(action)) return next(action);
|
|
1326
|
+
return action;
|
|
1327
|
+
};
|
|
1328
|
+
function createPackStore(pack, { keyPrefix, migrate }) {
|
|
1329
|
+
const gameReducers = Object.assign({}, ...pack.games.map((game) => game.reducers));
|
|
1330
|
+
const resumeReducers = pack.games.some((game) => game.resumable) ? { lastActiveGame: lastActiveGameSlice_default } : {};
|
|
1331
|
+
const rootReducer = combineReducers({ ...createShellReducers(pack), ...resumeReducers, ...pack.reducers, ...gameReducers });
|
|
1332
|
+
const persistedReducer = persistReducer(
|
|
1333
|
+
{
|
|
1334
|
+
key: "root",
|
|
1335
|
+
keyPrefix,
|
|
1336
|
+
storage: AsyncStorage,
|
|
1337
|
+
// redux-persist defaults `timeout` to 5000ms: a failsafe setTimeout scheduled on every PERSIST
|
|
1338
|
+
// dispatch to force-resolve rehydrate if storage never responds. It's never cleared once
|
|
1339
|
+
// rehydrate resolves normally (only guarded by an internal `_sealed` flag), so it sits as a
|
|
1340
|
+
// pending timer for up to 5s after every store creation - under Jest that's a real open handle
|
|
1341
|
+
// ("A worker process has failed to exit gracefully"). Disabling it (falsy timeout skips the
|
|
1342
|
+
// setTimeout call entirely) is redux-persist's own documented way to opt out; AsyncStorage reads
|
|
1343
|
+
// failing to ever resolve at all isn't a failure mode worth a 5s failsafe for.
|
|
1344
|
+
timeout: 0,
|
|
1345
|
+
// liveplay is per-round state that must never survive a relaunch (see liveplaySlice.ts).
|
|
1346
|
+
blacklist: ["liveplay"],
|
|
1347
|
+
migrate
|
|
1348
|
+
},
|
|
1349
|
+
rootReducer
|
|
1350
|
+
);
|
|
1351
|
+
const store = configureStore({
|
|
1352
|
+
middleware: (getDefaultMiddleware) => {
|
|
1353
|
+
const defaultMiddleware = getDefaultMiddleware({
|
|
1354
|
+
immutableCheck: false,
|
|
1355
|
+
serializableCheck: {
|
|
1356
|
+
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
return defaultMiddleware.concat(errorMiddleware);
|
|
1360
|
+
},
|
|
1361
|
+
reducer: persistedReducer
|
|
1362
|
+
});
|
|
1363
|
+
const persistor = persistStore(store);
|
|
1364
|
+
return { store, persistor };
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// src/redux/gameGuideSlice.ts
|
|
1368
|
+
var REHYDRATE2 = "persist/REHYDRATE";
|
|
1369
|
+
function createGameGuideSlice(mountKey) {
|
|
1370
|
+
const MARK_SEEN = `${mountKey}/markSeen`;
|
|
1371
|
+
const initialState = { versionSeen: 0 };
|
|
1372
|
+
function markSeen(version) {
|
|
1373
|
+
return { type: MARK_SEEN, payload: version };
|
|
1374
|
+
}
|
|
1375
|
+
function reducer6(state = initialState, action) {
|
|
1376
|
+
if (action.type === REHYDRATE2) {
|
|
1377
|
+
const persisted = action.payload?.[mountKey];
|
|
1378
|
+
return typeof persisted?.versionSeen === "number" ? { versionSeen: persisted.versionSeen } : state;
|
|
1379
|
+
}
|
|
1380
|
+
if (action.type === MARK_SEEN && typeof action.payload === "number") {
|
|
1381
|
+
return { versionSeen: Math.max(state.versionSeen, action.payload) };
|
|
1382
|
+
}
|
|
1383
|
+
return state;
|
|
1384
|
+
}
|
|
1385
|
+
function selectVersionSeen(rootState) {
|
|
1386
|
+
return rootState[mountKey]?.versionSeen ?? 0;
|
|
1387
|
+
}
|
|
1388
|
+
return { actions: { markSeen }, reducer: reducer6, selectVersionSeen };
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// src/redux/onProfileRemoved.ts
|
|
1392
|
+
import { removeProfileUnlocks } from "@tastic/achievements";
|
|
1393
|
+
import { profilesActions as profilesActions2 } from "@tastic/profile";
|
|
1394
|
+
function onProfileRemoved(reducer6, forget) {
|
|
1395
|
+
return (state, action) => {
|
|
1396
|
+
const next = reducer6(state, action);
|
|
1397
|
+
return profilesActions2.remove.match(action) ? forget(next, action.payload) : next;
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
function forgetProfileAchievements(dropStats) {
|
|
1401
|
+
return (state, profileId) => {
|
|
1402
|
+
const stats = dropStats(state.stats, profileId);
|
|
1403
|
+
const unlocked = removeProfileUnlocks(state.unlocked, profileId);
|
|
1404
|
+
return stats === state.stats && unlocked === state.unlocked ? state : { ...state, stats, unlocked };
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// src/gameStats.tsx
|
|
1409
|
+
import { createAchievementsSlice, mapUnlocksBySeat, useAchievements } from "@tastic/achievements";
|
|
1410
|
+
import { createContext as createContext5, useCallback as useCallback11, useContext as useContext5, useMemo as useMemo6 } from "react";
|
|
1411
|
+
import { jsx as jsx18 } from "react/jsx-runtime";
|
|
1412
|
+
var SEATS = [1, 2];
|
|
1413
|
+
function createGameStats({ key, defaultStats, isValidStats, catalog, getProfileStatsView }) {
|
|
1414
|
+
const { actions: actions6, reducer: sliceReducer } = createAchievementsSlice({
|
|
1415
|
+
key,
|
|
1416
|
+
defaultStats,
|
|
1417
|
+
isValidStats,
|
|
1418
|
+
// Backfills `profiles` onto stats saved without it.
|
|
1419
|
+
migrateStats: (stored) => ({ ...stored, profiles: stored.profiles ?? {} })
|
|
1420
|
+
});
|
|
1421
|
+
const dropProfileStats = (stats, profileId) => {
|
|
1422
|
+
if (!(profileId in stats.profiles)) return stats;
|
|
1423
|
+
const { [profileId]: _removed, ...rest } = stats.profiles;
|
|
1424
|
+
return { ...stats, profiles: rest };
|
|
1425
|
+
};
|
|
1426
|
+
const reducer6 = onProfileRemoved(sliceReducer, forgetProfileAchievements(dropProfileStats));
|
|
1427
|
+
const profileViews = (stats) => Object.fromEntries(Object.entries(stats.profiles).map(([profileId, bucket]) => [profileId, getProfileStatsView(bucket)]));
|
|
1428
|
+
const Context = createContext5(null);
|
|
1429
|
+
function Provider({ children }) {
|
|
1430
|
+
const { stats, unlockedAchievements, recordOutcome, resetAll } = useAchievements({
|
|
1431
|
+
catalog,
|
|
1432
|
+
select: (state) => state[key],
|
|
1433
|
+
actions: actions6,
|
|
1434
|
+
profileViews
|
|
1435
|
+
});
|
|
1436
|
+
const record = useCallback11(
|
|
1437
|
+
(update, profileIds) => {
|
|
1438
|
+
const result = recordOutcome(update);
|
|
1439
|
+
return { device: result.device, profiles: mapUnlocksBySeat(SEATS, profileIds, result.profiles) };
|
|
1440
|
+
},
|
|
1441
|
+
[recordOutcome]
|
|
1442
|
+
);
|
|
1443
|
+
const value = useMemo6(() => ({ stats, unlockedAchievements, record, resetAll }), [stats, unlockedAchievements, record, resetAll]);
|
|
1444
|
+
return /* @__PURE__ */ jsx18(Context.Provider, { value, children });
|
|
1445
|
+
}
|
|
1446
|
+
function useStats() {
|
|
1447
|
+
const ctx = useContext5(Context);
|
|
1448
|
+
if (!ctx) throw new Error(`The ${key} stats are read within their game's Provider`);
|
|
1449
|
+
return ctx;
|
|
1450
|
+
}
|
|
1451
|
+
return { reducer: reducer6, actions: actions6, Provider, useStats };
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// src/playerSettings.ts
|
|
1455
|
+
import { useCallback as useCallback12, useMemo as useMemo7 } from "react";
|
|
1456
|
+
import { useDispatch as useDispatch11, useSelector as useSelector9 } from "react-redux";
|
|
1457
|
+
var SEATS2 = [1, 2];
|
|
1458
|
+
function seatPlayerKey(selection, seat) {
|
|
1459
|
+
return selection[seat] ?? `guest:${seat}`;
|
|
1460
|
+
}
|
|
1461
|
+
function createPlayerSettings({ id, defaults, distinctControls }) {
|
|
1462
|
+
const settingsKey = `${id}Settings`;
|
|
1463
|
+
const controlsKey = `${id}Controls`;
|
|
1464
|
+
const PATCH = `${settingsKey}/patch`;
|
|
1465
|
+
const SET_CONTROL = `${controlsKey}/set`;
|
|
1466
|
+
const patch = (player, changes) => ({ type: PATCH, payload: { player, changes } });
|
|
1467
|
+
const setControl = (player, control) => ({ type: SET_CONTROL, payload: { player, control } });
|
|
1468
|
+
function forget(state, profileId) {
|
|
1469
|
+
if (!(profileId in state)) return state;
|
|
1470
|
+
const { [profileId]: _removed, ...rest } = state;
|
|
1471
|
+
return rest;
|
|
1472
|
+
}
|
|
1473
|
+
const settingsReducer = onProfileRemoved((state = {}, action) => {
|
|
1474
|
+
if (action.type !== PATCH) return state;
|
|
1475
|
+
const { player, changes } = action.payload;
|
|
1476
|
+
return { ...state, [player]: { ...state[player], ...changes } };
|
|
1477
|
+
}, forget);
|
|
1478
|
+
const controlsReducer = onProfileRemoved((state = {}, action) => {
|
|
1479
|
+
if (action.type !== SET_CONTROL) return state;
|
|
1480
|
+
const { player, control } = action.payload;
|
|
1481
|
+
return state[player] === control ? state : { ...state, [player]: control };
|
|
1482
|
+
}, forget);
|
|
1483
|
+
function compose(saved, controls, selection, seatTwoIsPerson) {
|
|
1484
|
+
const control1 = controls?.[seatPlayerKey(selection, 1)] ?? defaults.controlScheme[1];
|
|
1485
|
+
let control2 = (seatTwoIsPerson ? controls?.[seatPlayerKey(selection, 2)] : void 0) ?? defaults.controlScheme[2];
|
|
1486
|
+
if (distinctControls && control2 === control1) control2 = distinctControls.find((c) => c !== control1) ?? control2;
|
|
1487
|
+
return { ...defaults, ...saved?.[seatPlayerKey(selection, 1)], controlScheme: { 1: control1, 2: control2 } };
|
|
1488
|
+
}
|
|
1489
|
+
function select(state) {
|
|
1490
|
+
const root = state;
|
|
1491
|
+
return compose(root[settingsKey], root[controlsKey], state.profileSelection, state.opponent.seatTwo === "person");
|
|
1492
|
+
}
|
|
1493
|
+
function useSettings() {
|
|
1494
|
+
const selection = useSelector9((state) => state.profileSelection);
|
|
1495
|
+
const seatTwoIsPerson = useSelector9((state) => state.opponent.seatTwo === "person");
|
|
1496
|
+
const saved = useSelector9((state) => state[settingsKey]);
|
|
1497
|
+
const controls = useSelector9((state) => state[controlsKey]);
|
|
1498
|
+
const dispatch = useDispatch11();
|
|
1499
|
+
const settings = useMemo7(() => compose(saved, controls, selection, seatTwoIsPerson), [saved, controls, selection, seatTwoIsPerson]);
|
|
1500
|
+
const setSettings = useCallback12(
|
|
1501
|
+
(next) => {
|
|
1502
|
+
const { controlScheme, ...changes } = next;
|
|
1503
|
+
if (Object.keys(changes).length > 0) dispatch(patch(seatPlayerKey(selection, 1), changes));
|
|
1504
|
+
if (!controlScheme) return;
|
|
1505
|
+
for (const seat of SEATS2) {
|
|
1506
|
+
if (seat === 2 && !seatTwoIsPerson) continue;
|
|
1507
|
+
if (controlScheme[seat] !== settings.controlScheme[seat]) dispatch(setControl(seatPlayerKey(selection, seat), controlScheme[seat]));
|
|
1508
|
+
}
|
|
1509
|
+
},
|
|
1510
|
+
[dispatch, selection, seatTwoIsPerson, settings.controlScheme]
|
|
1511
|
+
);
|
|
1512
|
+
return { settings, setSettings };
|
|
1513
|
+
}
|
|
1514
|
+
return { reducers: { [settingsKey]: settingsReducer, [controlsKey]: controlsReducer }, settingsReducer, controlsReducer, actions: { patch, setControl }, compose, select, useSettings };
|
|
1515
|
+
}
|
|
1516
|
+
export {
|
|
1517
|
+
DEFAULT_APP_SETTINGS,
|
|
1518
|
+
DEFAULT_OPPONENT,
|
|
1519
|
+
HomeScreen,
|
|
1520
|
+
LoadoutRoute,
|
|
1521
|
+
PREVIEW_ASPECT,
|
|
1522
|
+
PackProvider,
|
|
1523
|
+
PlayRoute,
|
|
1524
|
+
ProfilesProvider,
|
|
1525
|
+
ProfilesRoute,
|
|
1526
|
+
RootLayout,
|
|
1527
|
+
SettingsDialog,
|
|
1528
|
+
StatsScreen,
|
|
1529
|
+
appSettingsActions,
|
|
1530
|
+
appSettingsSlice_default as appSettingsReducer,
|
|
1531
|
+
createGameGuideSlice,
|
|
1532
|
+
createGameStats,
|
|
1533
|
+
createPackStore,
|
|
1534
|
+
createPlayerSettings,
|
|
1535
|
+
createSeatColorsReducer,
|
|
1536
|
+
createShellReducers,
|
|
1537
|
+
defaultProfileSelectionState,
|
|
1538
|
+
defaultSeatColorsState,
|
|
1539
|
+
forgetProfileAchievements,
|
|
1540
|
+
humanSeatsFor,
|
|
1541
|
+
imageGameIcon,
|
|
1542
|
+
liveplayActions,
|
|
1543
|
+
liveplaySlice_default as liveplayReducer,
|
|
1544
|
+
onProfileRemoved,
|
|
1545
|
+
opponentActions,
|
|
1546
|
+
opponentSlice_default as opponentReducer,
|
|
1547
|
+
profileSelectionActions,
|
|
1548
|
+
profileSelectionSlice_default as profileSelectionReducer,
|
|
1549
|
+
roundModeFor,
|
|
1550
|
+
safeBack,
|
|
1551
|
+
seatColorsActions,
|
|
1552
|
+
seatPlayerKey,
|
|
1553
|
+
seatTwoFor,
|
|
1554
|
+
useActiveRound,
|
|
1555
|
+
useAppSettings,
|
|
1556
|
+
useGameGuide,
|
|
1557
|
+
useOpponent,
|
|
1558
|
+
usePack,
|
|
1559
|
+
usePackGame,
|
|
1560
|
+
useProfiles,
|
|
1561
|
+
useSeatColors
|
|
1562
|
+
};
|