floating-games-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/package.json +44 -0
  2. package/src/FloatingGames.tsx +25 -0
  3. package/src/assets/fonts/Creepster-Regular.ttf +0 -0
  4. package/src/assets/images/android-icon-background.png +0 -0
  5. package/src/assets/images/android-icon-foreground.png +0 -0
  6. package/src/assets/images/android-icon-monochrome.png +0 -0
  7. package/src/assets/images/blocks-icon.png +0 -0
  8. package/src/assets/images/bright-rubber-puzzles-blue.png +0 -0
  9. package/src/assets/images/close.png +0 -0
  10. package/src/assets/images/eriri.png +0 -0
  11. package/src/assets/images/favicon.png +0 -0
  12. package/src/assets/images/first-play.png +0 -0
  13. package/src/assets/images/game-over.gif +0 -0
  14. package/src/assets/images/game-pad.png +0 -0
  15. package/src/assets/images/game-pad2.png +0 -0
  16. package/src/assets/images/game-page-bg1.png +0 -0
  17. package/src/assets/images/hands-down-bg.png +0 -0
  18. package/src/assets/images/icon.png +0 -0
  19. package/src/assets/images/lets-play.gif +0 -0
  20. package/src/assets/images/lucky-day.gif +0 -0
  21. package/src/assets/images/maths.png +0 -0
  22. package/src/assets/images/not-playing.png +0 -0
  23. package/src/assets/images/number-puzzle-select.png +0 -0
  24. package/src/assets/images/number-puzzle-selector.png +0 -0
  25. package/src/assets/images/pause-clock.png +0 -0
  26. package/src/assets/images/pause-icon.png +0 -0
  27. package/src/assets/images/play-icon.png +0 -0
  28. package/src/assets/images/play-now.gif +0 -0
  29. package/src/assets/images/play-play.png +0 -0
  30. package/src/assets/images/play.png +0 -0
  31. package/src/assets/images/quit-game.png +0 -0
  32. package/src/assets/images/quit-icon.png +0 -0
  33. package/src/assets/images/quit.png +0 -0
  34. package/src/assets/images/review-win-selector.png +0 -0
  35. package/src/assets/images/select-bg.png +0 -0
  36. package/src/assets/images/select-logo.png +0 -0
  37. package/src/assets/images/spin-wheel-select.png +0 -0
  38. package/src/assets/images/spin-wheel-selector.png +0 -0
  39. package/src/assets/images/splash-icon.png +0 -0
  40. package/src/assets/images/start-button.png +0 -0
  41. package/src/assets/images/start-game.png +0 -0
  42. package/src/assets/images/stop-quit.png +0 -0
  43. package/src/assets/images/waiting-for-you.gif +0 -0
  44. package/src/assets/sounds/selector-music.mp3 +0 -0
  45. package/src/assets/sounds/timeup-sound.wav +0 -0
  46. package/src/components/atoms/Amount.tsx +48 -0
  47. package/src/components/atoms/FloatingMenu.tsx +119 -0
  48. package/src/components/atoms/ShimmerHR.tsx +64 -0
  49. package/src/components/atoms/UseAnimatedCounter.tsx +31 -0
  50. package/src/components/games/Chain.tsx +26 -0
  51. package/src/components/games/GameItem.tsx +38 -0
  52. package/src/components/games/GameSelectorScreen.tsx +221 -0
  53. package/src/components/games/GameSplash.tsx +44 -0
  54. package/src/components/games/HangingSign.tsx +75 -0
  55. package/src/components/games/SplashScreen.tsx +69 -0
  56. package/src/components/games/number-puzzle/DragTile.tsx +65 -0
  57. package/src/components/games/number-puzzle/GameScreen.tsx +597 -0
  58. package/src/components/games/number-puzzle/PuzzleSplash.tsx +50 -0
  59. package/src/components/games/number-puzzle/TileComponent.tsx +47 -0
  60. package/src/components/games/review-and-win/EmojiTracker.tsx +96 -0
  61. package/src/components/games/review-and-win/GameScreen.tsx +135 -0
  62. package/src/components/games/review-and-win/Tile.tsx +60 -0
  63. package/src/components/games/spin-wheel/SpinAndWin.tsx +507 -0
  64. package/src/components/payment/CheckoutScreen.tsx +50 -0
  65. package/src/components/payment/FundAccount.tsx +281 -0
  66. package/src/components/payment/Payment.tsx +45 -0
  67. package/src/components/reusables/AppSelect.tsx +70 -0
  68. package/src/components/reusables/CountdownTimer.tsx +116 -0
  69. package/src/components/reusables/FloatingButton.tsx +71 -0
  70. package/src/components/reusables/SoundPlayer.ts +28 -0
  71. package/src/components/utils/shuffle.ts +48 -0
  72. package/src/context/GameContext.tsx +15 -0
  73. package/src/index.tsx +5 -0
  74. package/src/modals/BottomSheetModal.tsx +38 -0
  75. package/src/modals/GameOverModal.tsx +76 -0
  76. package/src/modals/PauseModal.tsx +95 -0
  77. package/src/navigation/GameNavigator.tsx +48 -0
@@ -0,0 +1,281 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import {
3
+ Platform,
4
+ ScrollView,
5
+ StyleSheet,
6
+ Text,
7
+ TextInput,
8
+ TouchableOpacity,
9
+ View,
10
+ } from 'react-native';
11
+
12
+ import Animated, {
13
+ FadeInDown,
14
+ useAnimatedStyle,
15
+ useSharedValue,
16
+ withSpring,
17
+ withTiming,
18
+ } from 'react-native-reanimated';
19
+
20
+ import { Amount } from '../atoms/Amount';
21
+
22
+ const quickAmounts = [1000, 2000, 3000, 4000, 5000, 6000, 7000, 10000];
23
+
24
+ export default function FundAccount({ navigation }: any) {
25
+ const [amount, setAmount] = useState('');
26
+
27
+ const balance = 1267;
28
+
29
+ const isMobileWeb =
30
+ Platform.OS === 'web' &&
31
+ !/Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
32
+
33
+ // ✅ Reanimated shared values
34
+ const button1Width = useSharedValue(1); // 1 = 100%, 0.48 = 48%
35
+ const button2Opacity = useSharedValue(0);
36
+ const button2Scale = useSharedValue(0.8);
37
+
38
+ // ✅ Animate based on amount
39
+ useEffect(() => {
40
+ if (amount) {
41
+ button1Width.value = withTiming(0.48, { duration: 300 });
42
+ button2Opacity.value = withTiming(1, { duration: 250 });
43
+ button2Scale.value = withSpring(1);
44
+ } else {
45
+ button1Width.value = withTiming(1, { duration: 300 });
46
+ button2Opacity.value = withTiming(0, { duration: 200 });
47
+ button2Scale.value = withTiming(0.8);
48
+ }
49
+ }, [amount]);
50
+
51
+ // ✅ Animated styles
52
+ const button1Style = useAnimatedStyle(() => ({
53
+ width: `${button1Width.value * 100}%`,
54
+ }));
55
+
56
+ const button2Style = useAnimatedStyle(() => ({
57
+ opacity: button2Opacity.value,
58
+ transform: [{ scale: button2Scale.value }],
59
+ }));
60
+
61
+ const formatNumber = (value: string) => {
62
+ const cleaned = value.replace(/[^0-9]/g, '');
63
+ if (!cleaned) return '';
64
+ return Number(cleaned).toLocaleString();
65
+ };
66
+
67
+ const clearAmount = () => setAmount('');
68
+
69
+ const handlePressIn = () => {};
70
+ const handlePressOut = () => {
71
+ navigation.replace('GameSelector');
72
+ };
73
+
74
+ return (
75
+ <ScrollView
76
+ showsVerticalScrollIndicator={ false }
77
+ style={ [
78
+ styles.container,
79
+ { paddingHorizontal: isMobileWeb ? 200 : 20 },
80
+ ] }
81
+ >
82
+ { /* Title */ }
83
+ <Animated.Text entering={ FadeInDown.delay(100) } style={ styles.title }>
84
+ Fund Account
85
+ </Animated.Text>
86
+
87
+ { /* Balance */ }
88
+ <Animated.View entering={ FadeInDown.delay(200) } style={ styles.balanceCard }>
89
+ <Text style={ styles.balanceLabel }>Current Balance</Text>
90
+ <Amount value={ balance } fontSize={ 28 } color="#fff" fontWeight="bold" />
91
+ </Animated.View>
92
+
93
+ { /* Input */ }
94
+ <Animated.Text entering={ FadeInDown.delay(300) } style={ styles.label }>
95
+ Enter Amount
96
+ </Animated.Text>
97
+
98
+ <Animated.View entering={ FadeInDown.delay(350) }>
99
+ <View style={ { position: 'relative' } }>
100
+ <TextInput
101
+ style={ styles.input }
102
+ placeholder={ '0' }
103
+ placeholderTextColor={ '#666' }
104
+ keyboardType="number-pad"
105
+ value={ amount ? `₦${formatNumber(amount)}` : '' }
106
+ maxLength={ 10 }
107
+ onChangeText={ (text) => {
108
+ const cleaned = text.replace(/[^0-9]/g, '');
109
+ setAmount(cleaned);
110
+ } }
111
+ />
112
+
113
+ { amount ? (
114
+ <Text onPress={ clearAmount } style={ styles.clear }>
115
+
116
+ </Text>
117
+ ) : null }
118
+ </View>
119
+ </Animated.View>
120
+
121
+ { /* Quick amounts */ }
122
+ <Animated.View
123
+ entering={ FadeInDown.delay(400) }
124
+ style={ styles.quickContainer }
125
+ >
126
+ { quickAmounts.map((amt) => (
127
+ <TouchableOpacity
128
+ key={ amt }
129
+ style={ [
130
+ styles.quickBtn,
131
+ amount === String(amt) && styles.activeQuick,
132
+ ] }
133
+ onPress={ () => setAmount(String(amt)) }
134
+ >
135
+ <Amount value={ amt } fontSize={ 15 } color="#fff" fontWeight="400" />
136
+ </TouchableOpacity>
137
+ )) }
138
+ </Animated.View>
139
+
140
+ { /* Payment */ }
141
+ <Animated.Text entering={ FadeInDown.delay(500) } style={ styles.label }>
142
+ Payment Method
143
+ </Animated.Text>
144
+
145
+ <Animated.View entering={ FadeInDown.delay(550) } style={ styles.paymentCard }>
146
+ <Text style={ styles.paymentText }>💳 NetApps Pay</Text>
147
+ </Animated.View>
148
+
149
+ { /* Buttons */ }
150
+ <Animated.View entering={ FadeInDown.delay(600) }>
151
+ <View style={ [styles.buttonDiv, { marginTop: 20 }] }>
152
+ { /* Button 1 */ }
153
+ <Animated.View style={ button1Style }>
154
+ <TouchableOpacity
155
+ style={ [styles.button, styles.button1] }
156
+ onPressIn={ handlePressIn }
157
+ onPressOut={ handlePressOut }
158
+ >
159
+ <Text style={ styles.buttonText }>Continue to Game</Text>
160
+ </TouchableOpacity>
161
+ </Animated.View>
162
+
163
+ { /* Button 2 */ }
164
+ { amount ? (
165
+ <Animated.View
166
+ style={ [
167
+ styles.payWrapper,
168
+ button2Style,
169
+ ] }
170
+ >
171
+ <TouchableOpacity style={ [styles.button, styles.button2, { flexDirection:'row' }] }>
172
+ <Text style={ [styles.buttonText, { marginEnd: 4 }] }>Pay</Text>
173
+ <Amount
174
+ value={ Number(amount) }
175
+ fontSize={ 15 }
176
+ color="#fff"
177
+ fontWeight="normal"
178
+ />
179
+ </TouchableOpacity>
180
+ </Animated.View>
181
+ ) : null }
182
+ </View>
183
+ </Animated.View>
184
+
185
+ </ScrollView>
186
+ );
187
+ }
188
+ const styles = StyleSheet.create({
189
+ container: {
190
+ flex: 1,
191
+ padding: 20,
192
+ backgroundColor: '#0f172a',
193
+ paddingVertical: 60,
194
+ },
195
+ title: {
196
+ fontSize: 26,
197
+ fontWeight: 'bold',
198
+ color: '#fff',
199
+ marginBottom: 20,
200
+ },
201
+ balanceCard: {
202
+ backgroundColor: '#1e293b',
203
+ padding: 20,
204
+ borderRadius: 6,
205
+ marginBottom: 25,
206
+ },
207
+ balanceLabel: {
208
+ color: '#94a3b8',
209
+ fontSize: 14,
210
+ },
211
+ label: {
212
+ color: '#cbd5f5',
213
+ marginBottom: 8,
214
+ fontSize: 14,
215
+ },
216
+ input: {
217
+ backgroundColor: '#1e293b',
218
+ padding: 15,
219
+ borderRadius: 6,
220
+ color: '#fff',
221
+ fontSize: 18,
222
+ marginBottom: 15,
223
+ },
224
+ clear: {
225
+ position: 'absolute',
226
+ right: 10,
227
+ top: 10,
228
+ padding: 8,
229
+ },
230
+ quickContainer: {
231
+ flexDirection: 'row',
232
+ flexWrap: 'wrap',
233
+ justifyContent: 'space-between',
234
+ marginBottom: 20
235
+ },
236
+ quickBtn: {
237
+ backgroundColor: '#334155',
238
+ paddingVertical: 10,
239
+ paddingHorizontal: 15,
240
+ borderRadius: 6,
241
+ marginVertical: 8,
242
+ fontWeight: '500'
243
+ },
244
+ activeQuick: {
245
+ backgroundColor: '#22c55e',
246
+ },
247
+ paymentCard: {
248
+ backgroundColor: '#1e293b',
249
+ padding: 15,
250
+ borderRadius: 6,
251
+ marginBottom: 30,
252
+ },
253
+ paymentText: {
254
+ color: '#fff',
255
+ fontSize: 16,
256
+ },
257
+ buttonDiv: {
258
+ flexDirection: 'row',
259
+ justifyContent: 'space-between',
260
+ alignItems: 'center',
261
+ },
262
+ button: {
263
+ padding: 16,
264
+ borderRadius: 6,
265
+ alignItems: 'center',
266
+ justifyContent: 'center',
267
+ },
268
+ button1: {
269
+ backgroundColor: '#4c68af',
270
+ },
271
+ button2: {
272
+ backgroundColor: '#22c55e',
273
+ },
274
+ payWrapper: {
275
+ width: '48%',
276
+ },
277
+ buttonText: {
278
+ color: '#fff',
279
+ fontSize: 15,
280
+ },
281
+ });
@@ -0,0 +1,45 @@
1
+ // import type { PaymentConfig } from '@netappsng/react-native-pay';
2
+ // import { presentPayment } from '@netappsng/react-native-pay';
3
+ //
4
+ // function Payment() {
5
+ // const amountNaira = 100;
6
+ // const amountKobo = Math.round(amountNaira * 100);
7
+ //
8
+ // const config: PaymentConfig = {
9
+ // publicKey: 'pk_live_xxxxxxxxxxxxxxxxxxxx',
10
+ // amount: amountKobo, // ₦100.00 = 10000 kobo
11
+ // currency: 'NGN',
12
+ // email: 'john@example.com',
13
+ // fullName: 'John Doe',
14
+ // phoneNumber: '08106720418',
15
+ // narration: 'Dev Test Payment',
16
+ // paymentChannels: ['card', 'transfer', 'ussd', 'payattitude', 'moniflow'],
17
+ // defaultChannel: 'card',
18
+ // address1: 'Customer Address',
19
+ // metadata: { inputAmount: amountNaira, env: 'development' },
20
+ // businessName: 'Demo Store',
21
+ // showTransactionSummary: true,
22
+ // };
23
+ //
24
+ // const cleanup = presentPayment(config, {
25
+ // onSuccess: (payload) => {
26
+ // console.log('Payment successful!', payload.transactionRef);
27
+ // // payload: { status, merchantRef, transactionRef, amount, currency, channel, message, timestamp }
28
+ // },
29
+ // onFailed: (payload) => {
30
+ // console.log('Payment failed:', payload.message);
31
+ // // payload: { status, amount, currency, message, timestamp, errorCode? }
32
+ // },
33
+ // onCancel: () => {
34
+ // console.log('Payment cancelled');
35
+ // },
36
+ // onReady: () => {
37
+ // console.log('Payment UI ready');
38
+ // },
39
+ // });
40
+ //
41
+ // // Call cleanup() to remove event listeners when unmounting
42
+ // // e.g. in useEffect: return cleanup;
43
+ // }
44
+ //
45
+ // export default Payment
@@ -0,0 +1,70 @@
1
+ import React from 'react';
2
+ import DropDownPicker from 'react-native-dropdown-picker';
3
+
4
+ export type Option = {
5
+ label: string;
6
+ value: string | number;
7
+ };
8
+
9
+ type Props = {
10
+ open: boolean;
11
+ value: string | number | null;
12
+ items: Option[];
13
+ setOpen: React.Dispatch<React.SetStateAction<boolean>>;
14
+ setValue: React.Dispatch<React.SetStateAction<any>>;
15
+ setItems?: React.Dispatch<React.SetStateAction<Option[]>>;
16
+ placeholder?: string;
17
+ fontSize?: number;
18
+ };
19
+
20
+ const AppSelect = ({
21
+ open,
22
+ value,
23
+ items,
24
+ setOpen,
25
+ setValue,
26
+ setItems,
27
+ placeholder = 'Select option',
28
+ fontSize = 16,
29
+ }: Props) => {
30
+ return (
31
+ <DropDownPicker
32
+ open={open}
33
+ value={value}
34
+ items={items}
35
+ setOpen={setOpen}
36
+ setValue={setValue}
37
+ setItems={setItems}
38
+ placeholder={placeholder}
39
+ style={{
40
+ borderRadius: 25,
41
+ minHeight: 38,
42
+ width: '100%',
43
+ borderColor: '#72aaf1',
44
+ borderWidth: 3,
45
+ // borderWidth: 0,
46
+ backgroundColor: 'transparent'
47
+ }}
48
+ textStyle={{
49
+ fontSize,
50
+ fontWeight: '500',
51
+ textAlign: 'center',
52
+ color: '#666'
53
+ }}
54
+ listItemLabelStyle={{
55
+ fontSize,
56
+ textAlign: 'left',
57
+ fontWeight: '500',
58
+ color: '#666'
59
+ }}
60
+ dropDownContainerStyle={{
61
+ borderRadius: 8,
62
+ width: '100%',
63
+ borderWidth: 0,
64
+ marginBottom: 1,
65
+ }}
66
+ />
67
+ );
68
+ };
69
+
70
+ export default AppSelect;
@@ -0,0 +1,116 @@
1
+ import React, {
2
+ useEffect,
3
+ useState,
4
+ useImperativeHandle,
5
+ forwardRef,
6
+ } from 'react';
7
+ import {View, Text, StyleSheet, Vibration, Platform} from 'react-native';
8
+ import * as Haptics from 'expo-haptics';
9
+
10
+ type CountdownTimerProps = {
11
+ initialSeconds: number;
12
+ onComplete: () => void;
13
+ color?: string;
14
+ warningThreshold?: number;
15
+ };
16
+
17
+ export type CountdownTimerRef = {
18
+ reset: () => void;
19
+ pause: () => void;
20
+ resume: () => void;
21
+ };
22
+
23
+ const CountdownTimer = forwardRef<CountdownTimerRef, CountdownTimerProps>(
24
+ (
25
+ {initialSeconds, onComplete, color = '#333', warningThreshold = 10},
26
+ ref
27
+ ) => {
28
+ const [seconds, setSeconds] = useState(initialSeconds);
29
+ const [show, setShow] = useState(true);
30
+ const [paused, setPaused] = useState(false); // ✅ NEW
31
+
32
+ // ✅ Expose ALL methods
33
+ useImperativeHandle(ref, () => ({
34
+ reset: () => {
35
+ setSeconds(initialSeconds);
36
+ setPaused(false);
37
+ },
38
+ pause: () => setPaused(true),
39
+ resume: () => setPaused(false),
40
+ }));
41
+
42
+ // ✅ Countdown effect
43
+ useEffect(() => {
44
+ if (paused) return; // 🚀 stop if paused
45
+
46
+ if (seconds <= 0) {
47
+ onComplete();
48
+ return;
49
+ }
50
+
51
+ const interval = setInterval(() => {
52
+ setSeconds(prev => prev - 1);
53
+ }, 1000);
54
+
55
+ return () => clearInterval(interval);
56
+ }, [seconds, paused]);
57
+
58
+ // ✅ Blinking
59
+ useEffect(() => {
60
+ if (seconds <= warningThreshold && seconds > 0 && !paused) {
61
+ const blinkInterval = setInterval(() => {
62
+ setShow(prev => !prev);
63
+ }, 500);
64
+
65
+ return () => clearInterval(blinkInterval);
66
+ } else {
67
+ setShow(true);
68
+ }
69
+ }, [seconds, paused]);
70
+
71
+ // ✅ Move vibration into effect (important fix)
72
+ useEffect(() => {
73
+ const vibrateSoft = async () => {
74
+ if (Platform.OS === 'ios') {
75
+ await Haptics.impactAsync(
76
+ Haptics.ImpactFeedbackStyle.Light
77
+ );
78
+ } else {
79
+ Vibration.vibrate(30);
80
+ }
81
+ };
82
+
83
+ if (seconds <= warningThreshold && seconds > 0 && !paused) {
84
+ vibrateSoft();
85
+ }
86
+ }, [seconds, paused]);
87
+
88
+ const formatTime = (secs: number) => {
89
+ const minutes = Math.floor(secs / 60);
90
+ const remainderSeconds = secs % 60;
91
+ return `${minutes < 10 ? '0' : ''}${minutes}:${
92
+ remainderSeconds < 10 ? '0' : ''
93
+ }${remainderSeconds}`;
94
+ };
95
+
96
+ const textColor =
97
+ seconds <= warningThreshold ? '#f67373' : color;
98
+
99
+ return (
100
+ <View style={styles.container}>
101
+ {show && (
102
+ <Text style={[styles.text, {color: textColor}]}>
103
+ {formatTime(seconds)}
104
+ </Text>
105
+ )}
106
+ </View>
107
+ );
108
+ }
109
+ );
110
+
111
+ export default CountdownTimer;
112
+
113
+ const styles = StyleSheet.create({
114
+ container: {justifyContent: 'center', alignItems: 'center'},
115
+ text: {fontSize: 30, fontWeight: 'bold'},
116
+ });
@@ -0,0 +1,71 @@
1
+ import React, {useEffect} from 'react';
2
+ import Animated, {Easing, useAnimatedStyle, useSharedValue, withRepeat, withTiming,} from 'react-native-reanimated';
3
+
4
+ export type FloatPreset = 'left' | 'right' | 'center' | 'normal';
5
+
6
+ type Props = {
7
+ children: React.ReactNode;
8
+ preset?: FloatPreset;
9
+ };
10
+
11
+ export default function FloatingButton({children, preset = 'center'}: Props) {
12
+ const translateY = useSharedValue(0);
13
+ const translateX = useSharedValue(0);
14
+ const rotate = useSharedValue(0);
15
+
16
+ useEffect(() => {
17
+ let x = 0;
18
+ let r = 0;
19
+
20
+ if (preset === 'left') {
21
+ x = -8;
22
+ r = 1;
23
+ } else if (preset === 'right') {
24
+ x = 18;
25
+ r = 1;
26
+ } else if (preset === 'normal') {
27
+ x = 11;
28
+ r = 0;
29
+ } else {
30
+ x = 34;
31
+ r = 1.5;
32
+ }
33
+
34
+ translateY.value = withRepeat(
35
+ withTiming(-12, {
36
+ duration: 2300 + Math.random() * 600,
37
+ easing: Easing.inOut(Easing.ease),
38
+ }),
39
+ -1,
40
+ true
41
+ );
42
+
43
+ translateX.value = withRepeat(
44
+ withTiming(x, {
45
+ duration: 1700 + Math.random() * 500,
46
+ easing: Easing.inOut(Easing.ease),
47
+ }),
48
+ -1,
49
+ true
50
+ );
51
+
52
+ rotate.value = withRepeat(
53
+ withTiming(r, {
54
+ duration: 1500 + Math.random() * 500,
55
+ easing: Easing.inOut(Easing.ease),
56
+ }),
57
+ -1,
58
+ true
59
+ );
60
+ }, [preset]);
61
+
62
+ const animatedStyle = useAnimatedStyle(() => ({
63
+ transform: [
64
+ {translateY: translateY.value},
65
+ {translateX: translateX.value},
66
+ {rotate: `${rotate.value}deg`},
67
+ ],
68
+ }));
69
+
70
+ return <Animated.View style={animatedStyle}>{children}</Animated.View>;
71
+ }
@@ -0,0 +1,28 @@
1
+ import { useAudioPlayer } from 'expo-audio';
2
+
3
+ /**
4
+ * Reusable hook to play a sound
5
+ * @param asset - require('../assets/sounds/file.wav') or mp3
6
+ * @param initialVolume - number between 0.0 and 1.0
7
+ */
8
+ export function useSoundPlayer(asset: any, initialVolume: number = 0.1) {
9
+ const player = useAudioPlayer(asset);
10
+
11
+ // Set volume (clamp between 0 and 1)
12
+ player.volume = Math.max(0, Math.min(1, initialVolume));
13
+
14
+ const playSound = () => {
15
+ player.seekTo(0);
16
+ player.play();
17
+ };
18
+
19
+ const stopSound = () => {
20
+ player.pause();
21
+ };
22
+
23
+ const setVolume = (volume: number) => {
24
+ player.volume = Math.max(0, Math.min(1, volume));
25
+ };
26
+
27
+ return { playSound, stopSound, setVolume };
28
+ }
@@ -0,0 +1,48 @@
1
+ // utils/shuffle.ts
2
+ export type TileType = "😀" | "🔥" | "⭐";
3
+ export type TileData = {
4
+ id: number;
5
+ type: TileType;
6
+ opened: boolean;
7
+ };
8
+
9
+ const EMOJIS: TileType[] = ["😀","🔥","⭐"];
10
+
11
+ export const createBoard = (): TileData[] => {
12
+
13
+ const tiles: TileType[] = [];
14
+ let remaining = 16;
15
+ const counts: Record<TileType, number> = {"😀":0,"🔥":0,"⭐":0};
16
+
17
+ // Distribute emojis to fill 16 tiles, each emoji 2-6 times but total = 16
18
+ // Make sure at least 2 of each
19
+ let emojiCounts: Record<TileType, number> = {"😀":0,"🔥":0,"⭐":0};
20
+
21
+ // Step 1: assign minimum 2 to each
22
+ EMOJIS.forEach(emoji=>{
23
+ emojiCounts[emoji] = 2;
24
+ remaining -= 2;
25
+ });
26
+
27
+ // Step 2: randomly distribute remaining 10 tiles
28
+ const allEmojis: TileType[] = [];
29
+ EMOJIS.forEach(e=>{ for(let i=0;i<emojiCounts[e];i++) allEmojis.push(e) });
30
+
31
+ while(remaining>0){
32
+ const randIndex = Math.floor(Math.random()*EMOJIS.length);
33
+ const emoji = EMOJIS[randIndex];
34
+ allEmojis.push(emoji);
35
+ emojiCounts[emoji] +=1;
36
+ remaining -=1;
37
+ }
38
+
39
+ // shuffle
40
+ const shuffled = allEmojis.sort(()=> Math.random() - 0.5);
41
+
42
+ return shuffled.map((type,index)=>({
43
+ id:index,
44
+ type,
45
+ opened:false
46
+ }));
47
+
48
+ };
@@ -0,0 +1,15 @@
1
+ import React, { createContext, useContext } from 'react';
2
+
3
+ type GameContextType = { name: string; balance: number };
4
+
5
+ const GameContext = createContext<GameContextType>({ name: '', balance: 0 });
6
+
7
+ export const GameProvider = ({
8
+ name, balance, children
9
+ }: GameContextType & { children: React.ReactNode }) => (
10
+ <GameContext.Provider value={{ name, balance }}>
11
+ {children}
12
+ </GameContext.Provider>
13
+ );
14
+
15
+ export const useGameContext = () => useContext(GameContext);
package/src/index.tsx ADDED
@@ -0,0 +1,5 @@
1
+ export { default as FloatingGames } from './FloatingGames';
2
+ export { useGameContext } from './context/GameContext';
3
+
4
+
5
+ //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