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, } });