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