import { Images } from '@/constants/images';
import ServiceWallet from '@/services/wallet';
import Service from '@/services/dimension';
import { Ionicons } from '@expo/vector-icons';
import { Image } from 'expo-image';
import { useRouter } from 'expo-router';
import React, { useEffect, useRef, useState } from 'react';
import { Alert, Dimensions, ImageBackground, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import Animated, {
cancelAnimation,
Easing,
useAnimatedStyle,
useSharedValue,
withRepeat,
withSequence,
withTiming
} from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { CatchRuleModal, CatchRuleModalRef } from './components/CatchRuleModal';
import { DollPrizeModal, DollPrizeModalRef } from './components/DollPrizeModal';
import { DollResultModal, DollResultModalRef } from './components/DollResultModal';
import { LackMolibModal, LackMolibModalRef } from './components/LackMolibModal';
import { PressSureModal, PressSureModalRef } from './components/PressSureModal';
import { WinRecordModal, WinRecordModalRef } from './components/WinRecordModal';
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
const BALL_COUNT = 20; // Optimized count
const BALL_SIZE = 66;
const BALL_CONTAINER_WIDTH = 275;
const BALL_CONTAINER_HEIGHT = 278;
// Separate Ball Component for Performance
const Ball = React.memo(({ index }: { index: number }) => {
const x = useSharedValue(Math.random() * (BALL_CONTAINER_WIDTH - BALL_SIZE));
const y = useSharedValue(Math.random() * (BALL_CONTAINER_HEIGHT - BALL_SIZE));
const rotate = useSharedValue(Math.random() * 360);
useEffect(() => {
// Generate a sequence of random moves to simulate "chaos" on UI thread
const xMoves = Array.from({ length: 10 }).map(() =>
withTiming(Math.random() * (BALL_CONTAINER_WIDTH - BALL_SIZE), {
duration: 2000 + Math.random() * 1500,
easing: Easing.linear
})
);
const yMoves = Array.from({ length: 10 }).map(() =>
withTiming(Math.random() * (BALL_CONTAINER_HEIGHT - BALL_SIZE), {
duration: 2000 + Math.random() * 1500,
easing: Easing.linear
})
);
const rMoves = Array.from({ length: 10 }).map(() =>
withTiming(Math.random() * 360, {
duration: 2000 + Math.random() * 1500,
easing: Easing.linear
})
);
// @ts-ignore: spread argument for withSequence
x.value = withRepeat(withSequence(...xMoves), -1, true);
// @ts-ignore
y.value = withRepeat(withSequence(...yMoves), -1, true);
// @ts-ignore
rotate.value = withRepeat(withSequence(...rMoves), -1, true);
return () => {
cancelAnimation(x);
cancelAnimation(y);
cancelAnimation(rotate);
};
}, []);
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{ translateX: x.value },
{ translateY: y.value },
{ rotate: `${rotate.value}deg` } // Reanimated handles string interpolation
]
};
});
return (
);
});
export default function CatchDollScreen() {
const router = useRouter();
const insets = useSafeAreaInsets();
// State
const [molibi, setMolibi] = useState(0);
const [luckWheelGoodsList, setLuckWheelGoodsList] = useState([]);
const [loading, setLoading] = useState(false);
const [lotteryFlag, setLotteryFlag] = useState(false);
// Reanimated Values for interactions
const switchRotateVal = useSharedValue(0);
const dropScale = useSharedValue(0);
const dropOpacity = useSharedValue(0);
// Modals
const ruleRef = useRef(null);
const winRecordRef = useRef(null);
const resultRef = useRef(null);
const prizeRef = useRef(null);
const pressSureRef = useRef(null);
const lackMolibRef = useRef(null);
useEffect(() => {
initData();
}, []);
const initData = async () => {
getMolibi();
getDetail();
};
const getMolibi = async () => {
const res = await ServiceWallet.info('MAGIC_POWER_COIN');
if (res) {
setMolibi(res.balance);
}
};
const getDetail = async () => {
const res = await Service.catchDollDetail();
if (res.code == 0) {
setLuckWheelGoodsList(res.data.luckWheelGoodsList);
} else {
Alert.alert('提示', res.msg);
}
};
const handleBack = () => router.back();
const handlePress1 = () => {
if (lotteryFlag) {
Alert.alert('提示', '请不要重复点击');
return;
}
if (molibi === 0) {
lackMolibRef.current?.show();
} else {
pressSureRef.current?.show(1);
}
};
const handlePress5 = () => {
if (lotteryFlag) {
Alert.alert('提示', '请不要重复点击');
return;
}
if (molibi < 5) {
lackMolibRef.current?.show();
} else {
pressSureRef.current?.show(5);
}
};
const onConfirmPress = (quantity: number) => {
playLottery(quantity);
};
const playLottery = async (quantity: number) => {
setLotteryFlag(true);
const res = await Service.dollLottery({ quantity });
if (res.code == 0) {
getMolibi();
// Animate Switch
switchRotateVal.value = withSequence(
withTiming(90, { duration: 500, easing: Easing.inOut(Easing.ease) }),
withTiming(0, { duration: 500, easing: Easing.inOut(Easing.ease) })
);
setTimeout(() => {
// Animate Drop
dropOpacity.value = 1;
dropScale.value = 1;
dropOpacity.value = withTiming(0, { duration: 2000 });
dropScale.value = withTiming(0, { duration: 2000 });
}, 800);
setTimeout(() => {
resultRef.current?.show(res.data);
setLotteryFlag(false);
}, 2000);
} else {
Alert.alert('提示', res.msg);
setLotteryFlag(false);
}
};
const switchStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${switchRotateVal.value}deg` }]
}));
const dropStyle = useAnimatedStyle(() => ({
opacity: dropOpacity.value,
transform: [{ scale: dropScale.value }]
}));
return (
{/* Header */}
扭蛋机
{/* Fixed Record Button */}
winRecordRef.current?.show()} style={styles.recordBtn}>
中奖记录
{/* Rule Button */}
ruleRef.current?.show()} style={styles.ruleBtn}>
{/* Machine */}
{/* Prizes Scroll */}
{luckWheelGoodsList.map((item, index) => (
))}
{/* Molibi Count & Add */}
源力币:{molibi} 个
router.push('/box' as any)} style={styles.addMolibiBtn}>
{/* Switch */}
{/* Balls */}
{Array.from({ length: BALL_COUNT }).map((_, index) => (
))}
{/* Dropping Ball */}
{/* Opening hole image */}
{/* Buttons */}
{/* Modals */}
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
width: '100%',
height: '100%',
},
header: {
// height: 44, // Removing fixed height to allow dynamic override
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
zIndex: 100,
},
backBtn: {
position: 'absolute',
left: 10,
bottom: 10,
zIndex: 101,
},
title: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
},
scrollContent: {
flexGrow: 1,
paddingBottom: 50,
paddingTop: 40, // Push entire content down to avoid header overlap
},
content: {
width: '100%',
alignItems: 'center',
position: 'relative',
height: 750,
},
fullSize: {
width: '100%',
height: '100%',
},
ruleBtn: {
position: 'absolute',
left: 13,
top: 422,
zIndex: 10,
width: 62,
height: 20,
},
recordBtn: {
position: 'absolute',
right: 0,
top: 200, // Fixed position from top of screen
zIndex: 99,
},
recordBg: {
width: 26,
height: 80, // Increased from 70 to fit 4 chars
justifyContent: 'center',
alignItems: 'center',
paddingTop: 5, // Reduced padding slightly
},
recordText: {
fontSize: 12,
color: '#fff',
fontWeight: 'bold',
width: 12,
textAlign: 'center',
textShadowColor: '#6C3200',
textShadowOffset: { width: 1, height: 1 },
textShadowRadius: 1,
},
machineBox: {
marginTop: 20, // Reverted to 20, spacing handled by scrollContent padding
width: '100%',
alignItems: 'center',
},
machineBg: {
width: '100%',
height: 706,
position: 'relative',
// resizeMode: 'stretch' // Applied in component prop
},
prizesScrollBox: {
width: 250,
alignSelf: 'center',
marginTop: 110,
height: 50,
},
prizeItem: {
width: 46,
height: 46,
borderRadius: 4,
backgroundColor: '#ADAEF6',
borderWidth: 2.5,
borderColor: '#8687E4',
marginRight: 5,
justifyContent: 'center',
alignItems: 'center',
},
prizeImg: {
width: '100%',
height: '100%',
},
molibiBox: {
position: 'absolute',
top: 465,
left: 42,
width: 120,
height: 67,
backgroundColor: '#1E1C5B',
borderRadius: 8,
alignItems: 'center',
paddingTop: 5,
},
molibiText: {
color: '#7982CB',
fontSize: 12,
},
molibiNum: {
color: '#FF8400',
fontSize: 18,
fontWeight: '400',
},
addMolibiBtn: {
width: 105,
height: 30,
marginTop: 5,
},
opening: {
position: 'absolute',
top: 455,
right: 42,
width: 133,
height: 82,
},
switchBox: {
position: 'absolute',
top: 560,
left: 42,
width: 65,
height: 65,
zIndex: 3,
},
switchBoxRight: { // switchBox5
position: 'absolute',
top: 560,
right: 42,
width: 65,
height: 65,
zIndex: 3,
},
ballsContainer: {
position: 'absolute',
top: 125,
left: 43,
width: 275,
height: 278,
zIndex: 9,
overflow: 'hidden',
},
ball: {
width: BALL_SIZE,
height: BALL_SIZE,
position: 'absolute',
left: 0,
top: 0,
},
droppingBall: {
position: 'absolute',
top: 475,
right: 87,
width: 49,
height: 48,
zIndex: 3,
},
playBtn: {
position: 'absolute',
bottom: 90, // Match Vue: 179rpx / 2 = 89.5
zIndex: 10,
width: 73,
height: 50,
},
playBtn1: {
left: 98,
},
playBtn5: {
right: 98,
}
});