283 lines
8.1 KiB
JavaScript
283 lines
8.1 KiB
JavaScript
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
import { Animated, Pressable, StyleSheet, Text, View } from 'react-native';
|
|
import { LinearGradient } from 'expo-linear-gradient';
|
|
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';
|
|
|
|
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;
|
|
|
|
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));
|
|
});
|
|
};
|
|
|
|
return (
|
|
<LinearGradient colors={[colors.backgroundTop, colors.backgroundBottom]} style={styles.container}>
|
|
<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>
|
|
|
|
<TableBackground>
|
|
<View style={styles.dealerArea}>
|
|
<DealerArea hand={table?.dealerHand || []} />
|
|
</View>
|
|
|
|
<View style={styles.seatsLayer}>
|
|
{table?.seats?.map((seat) => {
|
|
const position = seatPositions[seat.index];
|
|
if (!position) {
|
|
return null;
|
|
}
|
|
return (
|
|
<View key={seat.index} style={[styles.seatPosition, position]}>
|
|
<Seat seat={seat} highlight={table.currentSeatIndex === seat.index} />
|
|
</View>
|
|
);
|
|
})}
|
|
</View>
|
|
</TableBackground>
|
|
|
|
<View style={styles.controls}>
|
|
<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>
|
|
|
|
{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>
|
|
{message ? <Text style={styles.message}>{message}</Text> : null}
|
|
</View>
|
|
</LinearGradient>
|
|
);
|
|
}
|
|
|
|
const seatPositions = {
|
|
0: { bottom: 10, left: '38%' },
|
|
1: { bottom: 20, left: '8%' },
|
|
2: { bottom: 20, right: '8%' },
|
|
3: { top: '52%', left: 0 },
|
|
4: { top: '52%', right: 0 },
|
|
5: { top: '22%', left: '6%' },
|
|
6: { top: '22%', right: '6%' }
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
paddingTop: 40
|
|
},
|
|
topBar: {
|
|
paddingHorizontal: 20,
|
|
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
|
|
},
|
|
dealerArea: {
|
|
alignItems: 'center'
|
|
},
|
|
seatsLayer: {
|
|
flex: 1,
|
|
position: 'relative'
|
|
},
|
|
seatPosition: {
|
|
position: 'absolute',
|
|
width: 140
|
|
},
|
|
controls: {
|
|
paddingHorizontal: 20,
|
|
paddingBottom: 24
|
|
},
|
|
betRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
gap: 8,
|
|
marginBottom: 12
|
|
},
|
|
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: 10,
|
|
fontFamily: fonts.body
|
|
},
|
|
timer: {
|
|
color: colors.goldBright,
|
|
textAlign: 'center',
|
|
marginBottom: 8,
|
|
fontFamily: fonts.mono
|
|
}
|
|
});
|