380 lines
11 KiB
JavaScript
380 lines
11 KiB
JavaScript
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
import { Animated, Pressable, StyleSheet, Text, useWindowDimensions, View } from 'react-native';
|
|
import { LinearGradient } from 'expo-linear-gradient';
|
|
import { BlurView } from 'expo-blur';
|
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
import { WS_URL } from '../api';
|
|
import { colors, fonts } from '../theme';
|
|
import CasinoButton from '../components/CasinoButton';
|
|
import Chip from '../components/Chip';
|
|
import DealerArea from '../components/DealerArea';
|
|
import Seat from '../components/Seat';
|
|
import TableBackground from '../components/TableBackground';
|
|
import TableMarkings from '../components/TableMarkings';
|
|
|
|
export default function TableScreen({ token, tableId, user, onLeave }) {
|
|
const [table, setTable] = useState(null);
|
|
const [balance, setBalance] = useState(user.balance);
|
|
const [message, setMessage] = useState('');
|
|
const [betAmount, setBetAmount] = useState(10);
|
|
const [turnSeconds, setTurnSeconds] = useState(null);
|
|
const wsRef = useRef(null);
|
|
const pulse = useRef(new Animated.Value(0)).current;
|
|
const insets = useSafeAreaInsets();
|
|
const { width, height } = useWindowDimensions();
|
|
const isPortrait = height >= width;
|
|
|
|
useEffect(() => {
|
|
const ws = new WebSocket(WS_URL);
|
|
wsRef.current = ws;
|
|
|
|
ws.onopen = () => {
|
|
ws.send(JSON.stringify({ type: 'hello', token }));
|
|
ws.send(JSON.stringify({ type: 'join', tableId }));
|
|
};
|
|
|
|
ws.onmessage = (event) => {
|
|
const payload = JSON.parse(event.data);
|
|
if (payload.type === 'table_state') {
|
|
setTable(payload.table);
|
|
if (payload.table.minBet) {
|
|
setBetAmount((prev) => {
|
|
const clamped = Math.max(payload.table.minBet, Math.min(prev, payload.table.maxBet));
|
|
return clamped;
|
|
});
|
|
}
|
|
}
|
|
if (payload.type === 'balance') {
|
|
setBalance(payload.balance);
|
|
}
|
|
if (payload.type === 'error') {
|
|
setMessage(payload.message);
|
|
setTimeout(() => setMessage(''), 2400);
|
|
}
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
wsRef.current = null;
|
|
};
|
|
|
|
return () => {
|
|
if (ws.readyState === 1) {
|
|
ws.send(JSON.stringify({ type: 'leave' }));
|
|
}
|
|
ws.close();
|
|
};
|
|
}, [tableId, token]);
|
|
|
|
const mySeat = useMemo(() => table?.seats?.find((seat) => seat.isYou), [table]);
|
|
const isMyTurn = table?.phase === 'playing' && table?.currentSeatIndex === mySeat?.index;
|
|
const bettingLocked = ['playing', 'dealer', 'payout'].includes(table?.phase);
|
|
|
|
useEffect(() => {
|
|
if (!table?.turnEndsAt || !isMyTurn) {
|
|
setTurnSeconds(null);
|
|
return;
|
|
}
|
|
|
|
const update = () => {
|
|
const msLeft = Math.max(0, table.turnEndsAt - Date.now());
|
|
setTurnSeconds(Math.ceil(msLeft / 1000));
|
|
};
|
|
|
|
update();
|
|
const interval = setInterval(update, 250);
|
|
return () => clearInterval(interval);
|
|
}, [table?.turnEndsAt, isMyTurn]);
|
|
|
|
useEffect(() => {
|
|
if (!isMyTurn) {
|
|
pulse.stopAnimation();
|
|
pulse.setValue(0);
|
|
return;
|
|
}
|
|
|
|
Animated.loop(
|
|
Animated.sequence([
|
|
Animated.timing(pulse, { toValue: 1, duration: 700, useNativeDriver: true }),
|
|
Animated.timing(pulse, { toValue: 0, duration: 700, useNativeDriver: true })
|
|
])
|
|
).start();
|
|
}, [isMyTurn, pulse]);
|
|
|
|
const pulseStyle = {
|
|
transform: [
|
|
{
|
|
scale: pulse.interpolate({
|
|
inputRange: [0, 1],
|
|
outputRange: [1, 1.04]
|
|
})
|
|
}
|
|
],
|
|
opacity: pulse.interpolate({ inputRange: [0, 1], outputRange: [0.85, 1] })
|
|
};
|
|
|
|
const send = (payload) => {
|
|
if (wsRef.current?.readyState === 1) {
|
|
wsRef.current.send(JSON.stringify(payload));
|
|
}
|
|
};
|
|
|
|
const adjustBet = (delta) => {
|
|
if (!table) {
|
|
return;
|
|
}
|
|
setBetAmount((prev) => {
|
|
const next = prev + delta;
|
|
return Math.max(table.minBet, Math.min(table.maxBet, next));
|
|
});
|
|
};
|
|
|
|
const seats = table?.seats || [];
|
|
|
|
if (isPortrait) {
|
|
return (
|
|
<LinearGradient
|
|
colors={[colors.backgroundTop, colors.backgroundBottom]}
|
|
style={[
|
|
styles.container,
|
|
{
|
|
paddingTop: insets.top + 12,
|
|
paddingBottom: insets.bottom + 12
|
|
}
|
|
]}
|
|
>
|
|
<View style={styles.rotateWrap}>
|
|
<Text style={styles.rotateTitle}>Fordítsd el a telefont</Text>
|
|
<Text style={styles.rotateSubtitle}>A blackjack asztal fekvő nézetben működik jól.</Text>
|
|
</View>
|
|
</LinearGradient>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<LinearGradient
|
|
colors={[colors.backgroundTop, colors.backgroundBottom]}
|
|
style={[
|
|
styles.container,
|
|
{
|
|
paddingTop: insets.top + 12,
|
|
paddingBottom: insets.bottom + 12
|
|
}
|
|
]}
|
|
>
|
|
<View style={styles.topBar}>
|
|
<View>
|
|
<Text style={styles.tableTitle}>Asztal {tableId}</Text>
|
|
<Text style={styles.balance}>Egyenleg: {balance} Ft</Text>
|
|
</View>
|
|
<CasinoButton label="Kilépek" onPress={onLeave} variant="red" />
|
|
</View>
|
|
|
|
<View style={styles.tableWrap}>
|
|
<TableBackground style={styles.tableSurface}>
|
|
<View style={styles.tableInner}>
|
|
<TableMarkings />
|
|
<View style={styles.tableContent}>
|
|
<View style={styles.dealerArea}>
|
|
<DealerArea hand={table?.dealerHand || []} />
|
|
</View>
|
|
<View style={styles.seatLayer}>
|
|
{seats.map((seat) => (
|
|
<View key={seat.index} style={[styles.seatSpot, seatPositions[seat.index]]}>
|
|
<Seat seat={seat} highlight={table.currentSeatIndex === seat.index} />
|
|
</View>
|
|
))}
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.controlsOverlay}>
|
|
<BlurView intensity={28} tint="dark" style={styles.blurPanel}>
|
|
<View style={styles.betRow}>
|
|
<Text style={styles.sectionLabel}>Tét</Text>
|
|
<View style={styles.betControls}>
|
|
<Pressable onPress={() => adjustBet(-10)} style={[styles.betAdjust, bettingLocked && styles.betAdjustDisabled]} disabled={bettingLocked}>
|
|
<Text style={styles.betAdjustText}>-</Text>
|
|
</Pressable>
|
|
<Chip label={`${betAmount}`} color="red" />
|
|
<Pressable onPress={() => adjustBet(10)} style={[styles.betAdjust, bettingLocked && styles.betAdjustDisabled]} disabled={bettingLocked}>
|
|
<Text style={styles.betAdjustText}>+</Text>
|
|
</Pressable>
|
|
</View>
|
|
<CasinoButton label="Tét" onPress={() => send({ type: 'bet', amount: betAmount })} variant="gold" disabled={bettingLocked} />
|
|
<CasinoButton label="Kész" onPress={() => send({ type: 'ready' })} variant="green" disabled={bettingLocked} />
|
|
</View>
|
|
</BlurView>
|
|
|
|
<BlurView intensity={28} tint="dark" style={styles.blurPanel}>
|
|
{isMyTurn && turnSeconds !== null ? (
|
|
<Text style={styles.timer}>Idő: {turnSeconds} mp</Text>
|
|
) : null}
|
|
<Animated.View style={[styles.actionRow, isMyTurn && pulseStyle]}>
|
|
<CasinoButton label="Hit" onPress={() => send({ type: 'action', action: 'hit' })} variant="gold" disabled={!isMyTurn} />
|
|
<CasinoButton label="Stand" onPress={() => send({ type: 'action', action: 'stand' })} variant="gold" disabled={!isMyTurn} />
|
|
<CasinoButton label="Double" onPress={() => send({ type: 'action', action: 'double' })} variant="gold" disabled={!isMyTurn} />
|
|
</Animated.View>
|
|
</BlurView>
|
|
</View>
|
|
|
|
{message ? <Text style={styles.message}>{message}</Text> : null}
|
|
</View>
|
|
</TableBackground>
|
|
</View>
|
|
</LinearGradient>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
paddingHorizontal: 12
|
|
},
|
|
topBar: {
|
|
paddingHorizontal: 6,
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center'
|
|
},
|
|
tableTitle: {
|
|
color: colors.goldBright,
|
|
fontSize: 20,
|
|
fontFamily: fonts.display,
|
|
letterSpacing: 2
|
|
},
|
|
balance: {
|
|
color: colors.muted,
|
|
marginTop: 4,
|
|
fontFamily: fonts.mono
|
|
},
|
|
tableWrap: {
|
|
flex: 1,
|
|
justifyContent: 'flex-start',
|
|
alignItems: 'center',
|
|
marginTop: 8,
|
|
marginBottom: 8,
|
|
width: '100%'
|
|
},
|
|
tableSurface: {
|
|
flex: 1,
|
|
width: '100%',
|
|
padding: 0
|
|
},
|
|
tableInner: {
|
|
flex: 1,
|
|
position: 'relative'
|
|
},
|
|
dealerArea: {
|
|
alignItems: 'center',
|
|
paddingTop: 8
|
|
},
|
|
tableContent: {
|
|
flex: 1,
|
|
justifyContent: 'space-between',
|
|
paddingBottom: 8
|
|
},
|
|
seatLayer: {
|
|
flex: 1,
|
|
position: 'relative'
|
|
},
|
|
seatSpot: {
|
|
position: 'absolute',
|
|
width: 96,
|
|
transform: [{ translateX: -48 }]
|
|
},
|
|
controlsOverlay: {
|
|
position: 'absolute',
|
|
left: 12,
|
|
right: 12,
|
|
bottom: 14,
|
|
gap: 10
|
|
},
|
|
blurPanel: {
|
|
borderRadius: 18,
|
|
paddingVertical: 10,
|
|
paddingHorizontal: 12,
|
|
backgroundColor: 'rgba(10, 18, 14, 0.45)',
|
|
borderWidth: 1,
|
|
borderColor: 'rgba(255,255,255,0.12)',
|
|
overflow: 'hidden'
|
|
},
|
|
betRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
gap: 8
|
|
},
|
|
betControls: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 6
|
|
},
|
|
betAdjust: {
|
|
width: 32,
|
|
height: 32,
|
|
borderRadius: 16,
|
|
backgroundColor: 'rgba(255,255,255,0.12)',
|
|
alignItems: 'center',
|
|
justifyContent: 'center'
|
|
},
|
|
betAdjustText: {
|
|
color: colors.text,
|
|
fontSize: 18,
|
|
fontWeight: '700'
|
|
},
|
|
betAdjustDisabled: {
|
|
opacity: 0.4
|
|
},
|
|
actionRow: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
gap: 10
|
|
},
|
|
sectionLabel: {
|
|
color: colors.text,
|
|
fontSize: 14,
|
|
fontFamily: fonts.body
|
|
},
|
|
message: {
|
|
color: colors.goldBright,
|
|
textAlign: 'center',
|
|
marginTop: 8,
|
|
fontFamily: fonts.body
|
|
},
|
|
timer: {
|
|
color: colors.goldBright,
|
|
textAlign: 'center',
|
|
marginBottom: 6,
|
|
fontFamily: fonts.mono
|
|
},
|
|
rotateWrap: {
|
|
flex: 1,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
paddingHorizontal: 24
|
|
},
|
|
rotateTitle: {
|
|
color: colors.goldBright,
|
|
fontFamily: fonts.display,
|
|
fontSize: 24,
|
|
letterSpacing: 2,
|
|
textAlign: 'center'
|
|
},
|
|
rotateSubtitle: {
|
|
color: colors.muted,
|
|
fontFamily: fonts.body,
|
|
fontSize: 14,
|
|
marginTop: 12,
|
|
textAlign: 'center'
|
|
}
|
|
});
|
|
|
|
const seatPositions = {
|
|
0: { left: '8%', top: '62%' },
|
|
1: { left: '22%', top: '55%' },
|
|
2: { left: '36%', top: '50%' },
|
|
3: { left: '50%', top: '48%' },
|
|
4: { left: '64%', top: '50%' },
|
|
5: { left: '78%', top: '55%' },
|
|
6: { left: '92%', top: '62%' }
|
|
};
|