feat: enhance Card, CasinoButton, Chip, DealerArea, and TableScreen components; add animation, size adjustments, and improved UI elements for better user experience
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Animated, StyleSheet, Text, View } from 'react-native';
|
||||
import { colors, fonts } from '../theme';
|
||||
|
||||
const suitSymbols = {
|
||||
@@ -15,12 +16,70 @@ const suitColors = {
|
||||
D: colors.red
|
||||
};
|
||||
|
||||
export default function Card({ rank, suit, hidden }) {
|
||||
export default function Card({ rank, suit, hidden, size = 'normal', animate = true, delay = 0 }) {
|
||||
const scaleMap = { tiny: 0.78, small: 0.92, normal: 1.06, large: 1.18 };
|
||||
const scale = scaleMap[size] ?? 1;
|
||||
const appear = useRef(new Animated.Value(animate ? 0 : 1)).current;
|
||||
|
||||
useEffect(() => {
|
||||
if (!animate) {
|
||||
return;
|
||||
}
|
||||
Animated.timing(appear, {
|
||||
toValue: 1,
|
||||
duration: 260,
|
||||
delay,
|
||||
useNativeDriver: true
|
||||
}).start();
|
||||
}, [animate, appear, delay]);
|
||||
|
||||
const animatedStyle = animate
|
||||
? {
|
||||
opacity: appear,
|
||||
transform: [
|
||||
{
|
||||
translateY: appear.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [-12 * scale, 0]
|
||||
})
|
||||
},
|
||||
{
|
||||
scale: appear.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0.96, 1]
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
: null;
|
||||
|
||||
const cardStyle = [
|
||||
styles.card,
|
||||
{
|
||||
width: 54 * scale,
|
||||
height: 78 * scale,
|
||||
borderRadius: 8 * scale,
|
||||
padding: 6 * scale,
|
||||
marginRight: 6 * scale
|
||||
}
|
||||
];
|
||||
const cornerStyle = [styles.corner, { fontSize: 12 * scale, lineHeight: 13 * scale }];
|
||||
const centerStyle = [styles.center, { fontSize: 24 * scale, lineHeight: 26 * scale }];
|
||||
const backPatternStyle = [
|
||||
styles.backPattern,
|
||||
{
|
||||
width: 32 * scale,
|
||||
height: 46 * scale,
|
||||
borderRadius: 6 * scale,
|
||||
borderWidth: Math.max(1, Math.round(2 * scale))
|
||||
}
|
||||
];
|
||||
|
||||
if (hidden || rank === 'X') {
|
||||
return (
|
||||
<View style={[styles.card, styles.cardBack]}>
|
||||
<View style={styles.backPattern} />
|
||||
</View>
|
||||
<Animated.View style={[cardStyle, styles.cardBack, animatedStyle]}>
|
||||
<View style={backPatternStyle} />
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,29 +87,29 @@ export default function Card({ rank, suit, hidden }) {
|
||||
const color = suitColors[suit] || colors.text;
|
||||
|
||||
return (
|
||||
<View style={styles.card}>
|
||||
<Text style={[styles.corner, { color }]}>{rank}</Text>
|
||||
<Text style={[styles.corner, { color }]}>{symbol}</Text>
|
||||
<Text style={[styles.center, { color }]}>{symbol}</Text>
|
||||
<Animated.View style={[cardStyle, animatedStyle]}>
|
||||
<Text style={[cornerStyle, { color }]}>{rank}</Text>
|
||||
<Text style={[cornerStyle, { color }]}>{symbol}</Text>
|
||||
<Text style={[centerStyle, { color }]}>{symbol}</Text>
|
||||
<View style={styles.cornerBottom}>
|
||||
<Text style={[styles.corner, { color }]}>{rank}</Text>
|
||||
<Text style={[styles.corner, { color }]}>{symbol}</Text>
|
||||
<Text style={[cornerStyle, { color }]}>{rank}</Text>
|
||||
<Text style={[cornerStyle, { color }]}>{symbol}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
width: 54,
|
||||
height: 78,
|
||||
borderRadius: 8,
|
||||
backgroundColor: '#fdf8f0',
|
||||
padding: 6,
|
||||
marginRight: 6,
|
||||
borderWidth: 1,
|
||||
borderColor: '#d2c1a4',
|
||||
justifyContent: 'space-between'
|
||||
justifyContent: 'space-between',
|
||||
overflow: 'hidden',
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.18,
|
||||
shadowRadius: 6,
|
||||
shadowOffset: { width: 0, height: 3 }
|
||||
},
|
||||
cardBack: {
|
||||
backgroundColor: '#152d52',
|
||||
@@ -59,21 +118,17 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center'
|
||||
},
|
||||
backPattern: {
|
||||
width: 32,
|
||||
height: 46,
|
||||
borderRadius: 6,
|
||||
borderWidth: 2,
|
||||
borderColor: 'rgba(255,255,255,0.4)'
|
||||
},
|
||||
corner: {
|
||||
fontSize: 12,
|
||||
fontFamily: fonts.body,
|
||||
fontWeight: '700'
|
||||
fontWeight: '700',
|
||||
includeFontPadding: false
|
||||
},
|
||||
center: {
|
||||
fontSize: 24,
|
||||
fontFamily: fonts.display,
|
||||
textAlign: 'center'
|
||||
textAlign: 'center',
|
||||
includeFontPadding: false
|
||||
},
|
||||
cornerBottom: {
|
||||
transform: [{ rotate: '180deg' }]
|
||||
|
||||
@@ -42,8 +42,8 @@ const styles = StyleSheet.create({
|
||||
borderColor: 'rgba(255,255,255,0.2)'
|
||||
},
|
||||
buttonSmall: {
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 16
|
||||
paddingVertical: 9,
|
||||
paddingHorizontal: 18
|
||||
},
|
||||
inner: {
|
||||
alignItems: 'center'
|
||||
@@ -56,7 +56,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
textSmall: {
|
||||
fontSize: 13,
|
||||
letterSpacing: 0.8
|
||||
letterSpacing: 0.7
|
||||
},
|
||||
disabled: {
|
||||
opacity: 0.5
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function Chip({ label, color = 'blue', size = 'default' }) {
|
||||
const isSmall = size === 'small';
|
||||
return (
|
||||
<View style={[styles.chip, isSmall && styles.chipSmall, { backgroundColor: chipColors[color] || colors.chipBlue }]}>
|
||||
<View style={styles.inner}>
|
||||
<View style={[styles.inner, isSmall && styles.innerSmall]}>
|
||||
<Text style={[styles.text, isSmall && styles.textSmall]}>{label}</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -20,9 +20,9 @@ export default function Chip({ label, color = 'blue', size = 'default' }) {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
chip: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 28,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 3,
|
||||
@@ -34,21 +34,27 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 20
|
||||
},
|
||||
inner: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
borderWidth: 2,
|
||||
borderColor: 'rgba(255,255,255,0.7)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
innerSmall: {
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 11,
|
||||
borderWidth: 1.5
|
||||
},
|
||||
text: {
|
||||
color: '#f7f2e6',
|
||||
fontWeight: '700',
|
||||
fontSize: 12,
|
||||
fontSize: 13,
|
||||
fontFamily: fonts.mono
|
||||
},
|
||||
textSmall: {
|
||||
fontSize: 11
|
||||
fontSize: 10
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,13 +2,20 @@ import { StyleSheet, Text, View } from 'react-native';
|
||||
import Card from './Card';
|
||||
import { colors, fonts } from '../theme';
|
||||
|
||||
export default function DealerArea({ hand }) {
|
||||
export default function DealerArea({ hand, cardSize = 'small', delayBase = 0 }) {
|
||||
return (
|
||||
<View style={styles.wrapper}>
|
||||
<Text style={styles.label}>Osztó</Text>
|
||||
<View style={styles.hand}>
|
||||
{hand.map((card, idx) => (
|
||||
<Card key={`${card.rank}-${card.suit}-${idx}`} rank={card.rank} suit={card.suit} hidden={card.hidden} />
|
||||
<Card
|
||||
key={`${card.rank}-${card.suit}-${idx}`}
|
||||
rank={card.rank}
|
||||
suit={card.suit}
|
||||
hidden={card.hidden}
|
||||
size={cardSize}
|
||||
delay={delayBase + idx * 120}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
@@ -23,7 +30,7 @@ const styles = StyleSheet.create({
|
||||
color: colors.goldBright,
|
||||
letterSpacing: 2,
|
||||
textTransform: 'uppercase',
|
||||
fontSize: 12,
|
||||
fontSize: 13,
|
||||
fontFamily: fonts.body
|
||||
},
|
||||
hand: {
|
||||
|
||||
@@ -5,12 +5,46 @@ import { BlurView } from 'expo-blur';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { WS_URL } from '../api';
|
||||
import { colors, fonts } from '../theme';
|
||||
import Card from '../components/Card';
|
||||
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';
|
||||
|
||||
const emptySeat = (index) => ({
|
||||
index,
|
||||
userId: null,
|
||||
username: '',
|
||||
bet: 0,
|
||||
hand: [],
|
||||
result: null,
|
||||
isYou: false
|
||||
});
|
||||
|
||||
const getHandTotal = (hand) => {
|
||||
if (!hand || hand.length === 0) {
|
||||
return null;
|
||||
}
|
||||
let total = 0;
|
||||
let aces = 0;
|
||||
for (const card of hand) {
|
||||
if (!card || card.rank === 'X') {
|
||||
return null;
|
||||
}
|
||||
if (card.rank === 'A') {
|
||||
total += 11;
|
||||
aces += 1;
|
||||
} else if (['J', 'Q', 'K'].includes(card.rank)) {
|
||||
total += 10;
|
||||
} else {
|
||||
total += Number(card.rank);
|
||||
}
|
||||
}
|
||||
while (total > 21 && aces > 0) {
|
||||
total -= 10;
|
||||
aces -= 1;
|
||||
}
|
||||
return total;
|
||||
};
|
||||
|
||||
export default function TableScreen({ token, tableId, user, onLeave }) {
|
||||
const [table, setTable] = useState(null);
|
||||
@@ -18,9 +52,14 @@ export default function TableScreen({ token, tableId, user, onLeave }) {
|
||||
const [message, setMessage] = useState('');
|
||||
const [betAmount, setBetAmount] = useState(10);
|
||||
const [turnSeconds, setTurnSeconds] = useState(null);
|
||||
const [roundSeconds, setRoundSeconds] = useState(null);
|
||||
const [controlsVisible, setControlsVisible] = useState(true);
|
||||
const [seatLayout, setSeatLayout] = useState(null);
|
||||
const [winOverlay, setWinOverlay] = useState(null);
|
||||
const [confettiKey, setConfettiKey] = useState(0);
|
||||
const wsRef = useRef(null);
|
||||
const hideTimerRef = useRef(null);
|
||||
const lastWinRoundRef = useRef(null);
|
||||
const pulse = useRef(new Animated.Value(0)).current;
|
||||
const insets = useSafeAreaInsets();
|
||||
const { width, height } = useWindowDimensions();
|
||||
@@ -70,6 +109,8 @@ export default function TableScreen({ token, tableId, user, onLeave }) {
|
||||
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);
|
||||
const showBetControls = !bettingLocked;
|
||||
const showActionControls = table?.phase === 'playing';
|
||||
|
||||
const scheduleHide = useCallback(() => {
|
||||
if (hideTimerRef.current) {
|
||||
@@ -113,6 +154,22 @@ export default function TableScreen({ token, tableId, user, onLeave }) {
|
||||
return () => clearInterval(interval);
|
||||
}, [table?.turnEndsAt, isMyTurn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!table?.roundStartsAt || table?.phase !== 'betting') {
|
||||
setRoundSeconds(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const update = () => {
|
||||
const msLeft = Math.max(0, table.roundStartsAt - Date.now());
|
||||
setRoundSeconds(Math.ceil(msLeft / 1000));
|
||||
};
|
||||
|
||||
update();
|
||||
const interval = setInterval(update, 250);
|
||||
return () => clearInterval(interval);
|
||||
}, [table?.roundStartsAt, table?.phase]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMyTurn) {
|
||||
pulse.stopAnimation();
|
||||
@@ -128,6 +185,24 @@ export default function TableScreen({ token, tableId, user, onLeave }) {
|
||||
).start();
|
||||
}, [isMyTurn, pulse]);
|
||||
|
||||
useEffect(() => {
|
||||
if (table?.phase !== 'payout' || !mySeat?.result) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastWinRoundRef.current === table.roundId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (['win', 'blackjack'].includes(mySeat.result.outcome)) {
|
||||
lastWinRoundRef.current = table.roundId;
|
||||
setWinOverlay({ payout: mySeat.result.payout, outcome: mySeat.result.outcome });
|
||||
setConfettiKey((prev) => prev + 1);
|
||||
const timer = setTimeout(() => setWinOverlay(null), 2600);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [table?.phase, table?.roundId, mySeat?.result]);
|
||||
|
||||
const pulseStyle = {
|
||||
transform: [
|
||||
{
|
||||
@@ -159,29 +234,65 @@ export default function TableScreen({ token, tableId, user, onLeave }) {
|
||||
};
|
||||
|
||||
const seats = table?.seats || [];
|
||||
const activePlayers = seats.filter((seat) => seat.userId).length;
|
||||
const showRoundTimer = roundSeconds !== null && table?.phase === 'betting' && activePlayers > 1;
|
||||
|
||||
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 style={styles.rotateActions}>
|
||||
<CasinoButton label="Vissza" onPress={onLeave} variant="red" />
|
||||
</View>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
);
|
||||
}
|
||||
const uiScale = useMemo(() => {
|
||||
const base = isPortrait ? Math.min(width / 390, height / 844) : Math.min(width / 844, height / 390);
|
||||
return Math.max(0.88, Math.min(base, 1.15));
|
||||
}, [width, height, isPortrait]);
|
||||
|
||||
const seatMetrics = useMemo(() => {
|
||||
if (!seatLayout) {
|
||||
return {
|
||||
width: Math.round(150 * uiScale),
|
||||
height: Math.round(110 * uiScale)
|
||||
};
|
||||
}
|
||||
const baseWidth = seatLayout.width * (isPortrait ? 0.24 : 0.18);
|
||||
const clamped = Math.max(120, Math.min(baseWidth, 190));
|
||||
const widthValue = Math.round(clamped * uiScale);
|
||||
return {
|
||||
width: widthValue,
|
||||
height: Math.round(widthValue * 0.78)
|
||||
};
|
||||
}, [seatLayout, isPortrait, uiScale]);
|
||||
|
||||
const seatAnchors = useMemo(() => {
|
||||
if (isPortrait) {
|
||||
return [
|
||||
{ x: 0.12, y: 0.32 },
|
||||
{ x: 0.88, y: 0.32 },
|
||||
{ x: 0.12, y: 0.58 },
|
||||
{ x: 0.88, y: 0.58 }
|
||||
];
|
||||
}
|
||||
return [
|
||||
{ x: 0.2, y: 0.26 },
|
||||
{ x: 0.8, y: 0.26 },
|
||||
{ x: 0.2, y: 0.64 },
|
||||
{ x: 0.8, y: 0.64 }
|
||||
];
|
||||
}, [isPortrait]);
|
||||
|
||||
const seatSlots = useMemo(() => {
|
||||
return seatAnchors.map((_, idx) => seats[idx] ?? emptySeat(idx));
|
||||
}, [seatAnchors, seats]);
|
||||
|
||||
const seatPositions = useMemo(() => {
|
||||
if (!seatLayout) {
|
||||
return [];
|
||||
}
|
||||
return seatAnchors.map((anchor) => ({
|
||||
left: seatLayout.width * anchor.x - seatMetrics.width / 2,
|
||||
top: seatLayout.height * anchor.y - seatMetrics.height / 2
|
||||
}));
|
||||
}, [seatLayout, seatAnchors, seatMetrics]);
|
||||
|
||||
const seatCardSize = uiScale > 1.02 ? 'normal' : 'small';
|
||||
const playerCardSize = uiScale > 1 ? 'large' : 'normal';
|
||||
const myTotal = getHandTotal(mySeat?.hand);
|
||||
const dealerDelayBase = seatSlots.length * 140;
|
||||
|
||||
return (
|
||||
<LinearGradient
|
||||
@@ -189,78 +300,283 @@ export default function TableScreen({ token, tableId, user, onLeave }) {
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
paddingTop: insets.top + 12,
|
||||
paddingBottom: insets.bottom + 12
|
||||
paddingTop: insets.top + 10,
|
||||
paddingBottom: insets.bottom + 10
|
||||
}
|
||||
]}
|
||||
>
|
||||
<View style={styles.topBar}>
|
||||
<View>
|
||||
<Text style={styles.tableTitle}>Asztal {tableId}</Text>
|
||||
<Text style={styles.balance}>Egyenleg: {balance} Ft</Text>
|
||||
<Text style={styles.balance}>{balance} Ft</Text>
|
||||
</View>
|
||||
<CasinoButton label="Kilépek" onPress={onLeave} variant="red" />
|
||||
<CasinoButton label="Kilép" onPress={onLeave} variant="red" size="small" />
|
||||
</View>
|
||||
|
||||
<View style={styles.tableWrap}>
|
||||
<TableBackground style={styles.tableSurface} showRing={false}>
|
||||
<View style={styles.tableInner}>
|
||||
<TableMarkings />
|
||||
<View style={styles.tableContent}>
|
||||
<View style={styles.tableSurface} onTouchStart={showControls}>
|
||||
<LinearGradient colors={[colors.tableFeltDark, colors.tableFelt]} style={styles.tableGradient}>
|
||||
<View style={styles.tableGlow} />
|
||||
<View style={[styles.tableArc, styles.tableArcTop, { top: 20 * uiScale, height: 200 * uiScale }]} />
|
||||
<View style={[styles.tableArc, styles.tableArcMid, { top: 140 * uiScale, height: 240 * uiScale }]} />
|
||||
<View style={[styles.tableArc, styles.tableArcBottom, { bottom: 220 * uiScale, height: 260 * uiScale }]} />
|
||||
|
||||
<View style={[styles.tableContent, { paddingTop: 6 * uiScale, paddingBottom: 10 * uiScale }]}
|
||||
>
|
||||
<View style={styles.dealerArea}>
|
||||
<DealerArea hand={table?.dealerHand || []} />
|
||||
<DealerArea hand={table?.dealerHand || []} cardSize={seatCardSize} delayBase={dealerDelayBase} />
|
||||
</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>
|
||||
|
||||
{controlsVisible ? (
|
||||
<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" size="small" />
|
||||
<Pressable onPress={() => adjustBet(10)} style={[styles.betAdjust, bettingLocked && styles.betAdjustDisabled]} disabled={bettingLocked}>
|
||||
<Text style={styles.betAdjustText}>+</Text>
|
||||
</Pressable>
|
||||
<View style={styles.seatLayer} onLayout={(event) => setSeatLayout(event.nativeEvent.layout)}>
|
||||
{seatSlots.map((seat, idx) => {
|
||||
const pos = seatPositions[idx];
|
||||
if (!pos) {
|
||||
return null;
|
||||
}
|
||||
const highlight = seat.userId && table?.currentSeatIndex === seat.index;
|
||||
return (
|
||||
<View
|
||||
key={seat.index}
|
||||
style={[
|
||||
styles.seatSpot,
|
||||
{
|
||||
width: seatMetrics.width,
|
||||
minHeight: seatMetrics.height
|
||||
},
|
||||
pos
|
||||
]}
|
||||
>
|
||||
<PortraitSeat
|
||||
seat={seat}
|
||||
highlight={highlight}
|
||||
cardSize={seatCardSize}
|
||||
scale={uiScale}
|
||||
dealIndex={idx}
|
||||
/>
|
||||
</View>
|
||||
<CasinoButton label="Tét" onPress={() => send({ type: 'bet', amount: betAmount })} variant="gold" size="small" disabled={bettingLocked} />
|
||||
<CasinoButton label="Kész" onPress={() => send({ type: 'ready' })} variant="green" size="small" 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" size="small" disabled={!isMyTurn} />
|
||||
<CasinoButton label="Stand" onPress={() => send({ type: 'action', action: 'stand' })} variant="gold" size="small" disabled={!isMyTurn} />
|
||||
<CasinoButton label="Double" onPress={() => send({ type: 'action', action: 'double' })} variant="gold" size="small" disabled={!isMyTurn} />
|
||||
</Animated.View>
|
||||
</BlurView>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
) : (
|
||||
<Pressable style={styles.controlsReveal} onPress={showControls} />
|
||||
)}
|
||||
|
||||
{message ? <Text style={styles.message}>{message}</Text> : null}
|
||||
</View>
|
||||
</TableBackground>
|
||||
<View style={styles.playerArea}>
|
||||
<View style={styles.playerHand}>
|
||||
{(mySeat?.hand || []).map((card, idx) => (
|
||||
<View key={`${card.rank}-${card.suit}-${idx}`} style={[styles.playerCard, { marginLeft: idx === 0 ? 0 : -12 * uiScale }]}
|
||||
>
|
||||
<Card
|
||||
rank={card.rank}
|
||||
suit={card.suit}
|
||||
hidden={card.hidden}
|
||||
size={playerCardSize}
|
||||
delay={(mySeat?.index ?? 0) * 140 + idx * 120}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
{myTotal !== null ? (
|
||||
<Text style={styles.playerTotal}>Összeg: {myTotal}</Text>
|
||||
) : null}
|
||||
{mySeat ? (
|
||||
<Text style={styles.playerBet}>Tét: {mySeat.bet || betAmount} Ft</Text>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{controlsVisible ? (
|
||||
<View style={styles.controlsArea}>
|
||||
{showRoundTimer ? (
|
||||
<Text style={styles.roundTimer}>Kezdés: {roundSeconds} mp</Text>
|
||||
) : null}
|
||||
|
||||
{showBetControls ? (
|
||||
<BlurView intensity={22} tint="dark" style={styles.controlsPanel}>
|
||||
<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" size="small" disabled={bettingLocked} />
|
||||
<CasinoButton label="Kész" onPress={() => send({ type: 'ready' })} variant="green" size="small" disabled={bettingLocked} />
|
||||
</View>
|
||||
</BlurView>
|
||||
) : null}
|
||||
|
||||
{showActionControls ? (
|
||||
<View style={styles.actionWrap}>
|
||||
<View style={[styles.actionHint, isMyTurn && styles.actionHintActive]}>
|
||||
<Text style={styles.actionHintText}>{isMyTurn ? 'HIT' : 'Várakozás'}</Text>
|
||||
{isMyTurn && turnSeconds !== null ? (
|
||||
<Text style={styles.turnTimer}>{turnSeconds} mp</Text>
|
||||
) : null}
|
||||
</View>
|
||||
<Animated.View style={[styles.actionRow, isMyTurn && pulseStyle]}>
|
||||
<CasinoButton label="Hit" onPress={() => send({ type: 'action', action: 'hit' })} variant="gold" size="small" disabled={!isMyTurn} />
|
||||
<CasinoButton label="Stand" onPress={() => send({ type: 'action', action: 'stand' })} variant="gold" size="small" disabled={!isMyTurn} />
|
||||
<CasinoButton label="Double" onPress={() => send({ type: 'action', action: 'double' })} variant="gold" size="small" disabled={!isMyTurn} />
|
||||
</Animated.View>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
) : (
|
||||
<Pressable style={styles.controlsReveal} onPress={showControls} />
|
||||
)}
|
||||
|
||||
{message ? <Text style={styles.message}>{message}</Text> : null}
|
||||
</View>
|
||||
</LinearGradient>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{winOverlay ? (
|
||||
<View pointerEvents="none" style={styles.winOverlay}>
|
||||
<ConfettiBurst trigger={confettiKey} width={width} height={height} />
|
||||
<View style={styles.winCard}>
|
||||
<Text style={styles.winTitle}>Nyertél!</Text>
|
||||
<Text style={styles.winAmount}>+{winOverlay.payout} Ft</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
</LinearGradient>
|
||||
);
|
||||
}
|
||||
|
||||
function PortraitSeat({ seat, highlight, cardSize, scale, dealIndex }) {
|
||||
const isEmpty = !seat.userId;
|
||||
const hand = seat.hand || [];
|
||||
const total = getHandTotal(hand);
|
||||
const label = seat.userId ? (seat.isYou ? 'ÉN' : seat.username || 'Játékos') : 'Üres hely';
|
||||
|
||||
return (
|
||||
<View style={[styles.seatCard, highlight && styles.seatCardActive]}>
|
||||
{highlight ? <View style={styles.seatGlow} /> : null}
|
||||
<View style={styles.avatarWrap}>
|
||||
<View style={styles.avatarRing}>
|
||||
<View style={styles.avatarHead} />
|
||||
<View style={styles.avatarBody} />
|
||||
</View>
|
||||
</View>
|
||||
<Text style={[styles.seatName, seat.isYou && styles.seatNameYou]} numberOfLines={1}>
|
||||
{label}
|
||||
</Text>
|
||||
{hand.length > 0 ? (
|
||||
<View style={styles.seatHand}>
|
||||
{hand.map((card, idx) => (
|
||||
<View
|
||||
key={`${card.rank}-${card.suit}-${idx}`}
|
||||
style={[
|
||||
styles.seatCardStack,
|
||||
{
|
||||
marginLeft: idx === 0 ? 0 : -14 * scale,
|
||||
transform: [{ rotate: `${idx === 0 ? 0 : idx * -3}deg` }]
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Card
|
||||
rank={card.rank}
|
||||
suit={card.suit}
|
||||
hidden={card.hidden}
|
||||
size={cardSize}
|
||||
delay={dealIndex * 140 + idx * 120}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
{total !== null ? <Text style={styles.seatTotal}>Összeg: {total}</Text> : null}
|
||||
<View style={[styles.betPill, isEmpty && styles.betPillEmpty]}>
|
||||
<Text style={styles.betPillText}>{seat.bet > 0 ? `${seat.bet} Ft` : 'TÉT'}</Text>
|
||||
</View>
|
||||
{seat.result?.outcome === 'blackjack' ? (
|
||||
<View style={styles.blackjackTag}>
|
||||
<Text style={styles.blackjackText}>BLACKJACK!</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function ConfettiBurst({ trigger, width, height }) {
|
||||
const pieces = useMemo(() => {
|
||||
const palette = ['#f7d488', '#d94a3d', '#39c377', '#2f7dd3', '#f2f1e8'];
|
||||
return Array.from({ length: 22 }, (_, idx) => {
|
||||
return {
|
||||
key: `${trigger}-${idx}`,
|
||||
x: Math.random() * Math.max(0, width - 24),
|
||||
delay: idx * 70,
|
||||
rotate: Math.random() * 180,
|
||||
drift: (Math.random() - 0.5) * 80,
|
||||
size: 6 + Math.random() * 6,
|
||||
color: palette[idx % palette.length],
|
||||
anim: new Animated.Value(0)
|
||||
};
|
||||
});
|
||||
}, [trigger, width]);
|
||||
|
||||
useEffect(() => {
|
||||
const animations = pieces.map((piece) =>
|
||||
Animated.timing(piece.anim, {
|
||||
toValue: 1,
|
||||
duration: 1600,
|
||||
delay: piece.delay,
|
||||
useNativeDriver: true
|
||||
})
|
||||
);
|
||||
Animated.parallel(animations).start();
|
||||
}, [pieces]);
|
||||
|
||||
return (
|
||||
<View pointerEvents="none" style={styles.confettiLayer}>
|
||||
{pieces.map((piece) => (
|
||||
<Animated.View
|
||||
key={piece.key}
|
||||
style={[
|
||||
styles.confettiPiece,
|
||||
{
|
||||
backgroundColor: piece.color,
|
||||
width: piece.size,
|
||||
height: piece.size * 1.6,
|
||||
left: piece.x,
|
||||
transform: [
|
||||
{
|
||||
translateY: piece.anim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [-20, height + 20]
|
||||
})
|
||||
},
|
||||
{
|
||||
translateX: piece.anim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, piece.drift]
|
||||
})
|
||||
},
|
||||
{
|
||||
rotate: piece.anim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [`${piece.rotate}deg`, `${piece.rotate + 120}deg`]
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
@@ -274,64 +590,215 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
tableTitle: {
|
||||
color: colors.goldBright,
|
||||
fontSize: 20,
|
||||
fontSize: 22,
|
||||
fontFamily: fonts.display,
|
||||
letterSpacing: 2
|
||||
letterSpacing: 1
|
||||
},
|
||||
balance: {
|
||||
color: colors.muted,
|
||||
marginTop: 4,
|
||||
marginTop: 2,
|
||||
fontFamily: fonts.mono
|
||||
},
|
||||
tableWrap: {
|
||||
flex: 1,
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
marginTop: 8,
|
||||
marginTop: 10,
|
||||
marginBottom: 8,
|
||||
width: '100%'
|
||||
},
|
||||
tableSurface: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
padding: 0
|
||||
borderRadius: 26,
|
||||
overflow: 'hidden'
|
||||
},
|
||||
tableInner: {
|
||||
flex: 1,
|
||||
position: 'relative'
|
||||
tableGradient: {
|
||||
flex: 1
|
||||
},
|
||||
dealerArea: {
|
||||
alignItems: 'center',
|
||||
paddingTop: 8
|
||||
tableGlow: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(255,255,255,0.02)'
|
||||
},
|
||||
tableArc: {
|
||||
position: 'absolute',
|
||||
left: '10%',
|
||||
right: '10%',
|
||||
borderRadius: 999,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.12)'
|
||||
},
|
||||
tableArcTop: {
|
||||
top: 26
|
||||
},
|
||||
tableArcMid: {
|
||||
top: 130,
|
||||
borderColor: 'rgba(255,255,255,0.08)'
|
||||
},
|
||||
tableArcBottom: {
|
||||
bottom: 200,
|
||||
borderColor: 'rgba(255,255,255,0.1)'
|
||||
},
|
||||
tableContent: {
|
||||
flex: 1,
|
||||
justifyContent: 'space-between',
|
||||
paddingBottom: 140
|
||||
paddingHorizontal: 16
|
||||
},
|
||||
dealerArea: {
|
||||
alignItems: 'center',
|
||||
marginTop: 6
|
||||
},
|
||||
seatLayer: {
|
||||
flex: 1,
|
||||
position: 'relative'
|
||||
position: 'relative',
|
||||
marginTop: 6,
|
||||
marginBottom: 6
|
||||
},
|
||||
seatSpot: {
|
||||
position: 'absolute'
|
||||
},
|
||||
seatCard: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start'
|
||||
},
|
||||
seatCardActive: {
|
||||
shadowColor: colors.goldBright,
|
||||
shadowOpacity: 0.6,
|
||||
shadowRadius: 12
|
||||
},
|
||||
seatGlow: {
|
||||
position: 'absolute',
|
||||
width: 90,
|
||||
transform: [{ translateX: -45 }]
|
||||
top: -8,
|
||||
left: -8,
|
||||
right: -8,
|
||||
bottom: -8,
|
||||
borderRadius: 999,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,214,138,0.6)'
|
||||
},
|
||||
controlsOverlay: {
|
||||
position: 'absolute',
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 10,
|
||||
gap: 10
|
||||
avatarWrap: {
|
||||
marginTop: 2,
|
||||
marginBottom: 4
|
||||
},
|
||||
controlsReveal: {
|
||||
...StyleSheet.absoluteFillObject
|
||||
avatarRing: {
|
||||
width: 38,
|
||||
height: 38,
|
||||
borderRadius: 19,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.35)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.25)'
|
||||
},
|
||||
blurPanel: {
|
||||
borderRadius: 18,
|
||||
paddingVertical: 8,
|
||||
avatarHead: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.3)',
|
||||
marginBottom: 2
|
||||
},
|
||||
avatarBody: {
|
||||
width: 18,
|
||||
height: 10,
|
||||
borderRadius: 6,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.25)'
|
||||
},
|
||||
seatName: {
|
||||
color: colors.text,
|
||||
fontFamily: fonts.body,
|
||||
fontSize: 12,
|
||||
marginBottom: 2
|
||||
},
|
||||
seatNameYou: {
|
||||
fontWeight: '800',
|
||||
color: colors.goldBright
|
||||
},
|
||||
seatHand: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: 2
|
||||
},
|
||||
seatCardStack: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
seatTotal: {
|
||||
marginTop: 3,
|
||||
color: colors.muted,
|
||||
fontSize: 11,
|
||||
fontFamily: fonts.mono
|
||||
},
|
||||
betPill: {
|
||||
marginTop: 4,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.goldBright,
|
||||
backgroundColor: 'rgba(0,0,0,0.25)'
|
||||
},
|
||||
betPillEmpty: {
|
||||
borderColor: 'rgba(255,255,255,0.18)'
|
||||
},
|
||||
betPillText: {
|
||||
color: colors.goldBright,
|
||||
fontFamily: fonts.mono,
|
||||
fontSize: 12
|
||||
},
|
||||
blackjackTag: {
|
||||
marginTop: 4,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 8,
|
||||
backgroundColor: 'rgba(0,0,0,0.45)',
|
||||
borderWidth: 1,
|
||||
borderColor: colors.gold
|
||||
},
|
||||
blackjackText: {
|
||||
color: colors.goldBright,
|
||||
fontSize: 10,
|
||||
fontFamily: fonts.body
|
||||
},
|
||||
playerArea: {
|
||||
alignItems: 'center',
|
||||
marginTop: 4,
|
||||
marginBottom: 4
|
||||
},
|
||||
playerHand: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
},
|
||||
playerCard: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
playerTotal: {
|
||||
marginTop: 6,
|
||||
color: colors.muted,
|
||||
fontFamily: fonts.mono,
|
||||
fontSize: 12
|
||||
},
|
||||
playerBet: {
|
||||
marginTop: 2,
|
||||
color: colors.goldBright,
|
||||
fontFamily: fonts.body,
|
||||
fontSize: 14
|
||||
},
|
||||
controlsArea: {
|
||||
marginTop: 4,
|
||||
gap: 12,
|
||||
paddingBottom: 6
|
||||
},
|
||||
roundTimer: {
|
||||
textAlign: 'center',
|
||||
color: colors.goldBright,
|
||||
fontFamily: fonts.mono,
|
||||
fontSize: 12
|
||||
},
|
||||
controlsPanel: {
|
||||
borderRadius: 18,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
backgroundColor: 'rgba(10, 18, 14, 0.45)',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.12)',
|
||||
@@ -341,17 +808,18 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 8
|
||||
gap: 10,
|
||||
flexWrap: 'wrap'
|
||||
},
|
||||
betControls: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6
|
||||
gap: 8
|
||||
},
|
||||
betAdjust: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
backgroundColor: 'rgba(255,255,255,0.12)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
@@ -364,6 +832,38 @@ const styles = StyleSheet.create({
|
||||
betAdjustDisabled: {
|
||||
opacity: 0.4
|
||||
},
|
||||
actionWrap: {
|
||||
alignItems: 'center',
|
||||
gap: 10
|
||||
},
|
||||
actionHint: {
|
||||
minWidth: '80%',
|
||||
borderRadius: 18,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.2)',
|
||||
backgroundColor: 'rgba(0,0,0,0.25)',
|
||||
alignItems: 'center'
|
||||
},
|
||||
actionHintActive: {
|
||||
borderColor: colors.goldBright,
|
||||
shadowColor: colors.goldBright,
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 10
|
||||
},
|
||||
actionHintText: {
|
||||
color: colors.goldBright,
|
||||
fontFamily: fonts.display,
|
||||
fontSize: 16,
|
||||
letterSpacing: 2
|
||||
},
|
||||
turnTimer: {
|
||||
marginTop: 4,
|
||||
color: colors.muted,
|
||||
fontFamily: fonts.mono,
|
||||
fontSize: 12
|
||||
},
|
||||
actionRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
@@ -377,46 +877,45 @@ const styles = StyleSheet.create({
|
||||
message: {
|
||||
color: colors.goldBright,
|
||||
textAlign: 'center',
|
||||
marginTop: 8,
|
||||
marginTop: 4,
|
||||
fontFamily: fonts.body
|
||||
},
|
||||
timer: {
|
||||
color: colors.goldBright,
|
||||
textAlign: 'center',
|
||||
marginBottom: 4,
|
||||
fontFamily: fonts.mono
|
||||
controlsReveal: {
|
||||
...StyleSheet.absoluteFillObject
|
||||
},
|
||||
rotateWrap: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
winOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 24
|
||||
alignItems: 'center'
|
||||
},
|
||||
rotateTitle: {
|
||||
winCard: {
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 16,
|
||||
borderRadius: 18,
|
||||
backgroundColor: 'rgba(8, 18, 12, 0.85)',
|
||||
borderWidth: 1,
|
||||
borderColor: colors.goldBright
|
||||
},
|
||||
winTitle: {
|
||||
color: colors.goldBright,
|
||||
fontFamily: fonts.display,
|
||||
fontSize: 24,
|
||||
letterSpacing: 2,
|
||||
fontSize: 22,
|
||||
textAlign: 'center'
|
||||
},
|
||||
rotateSubtitle: {
|
||||
color: colors.muted,
|
||||
fontFamily: fonts.body,
|
||||
fontSize: 14,
|
||||
marginTop: 12,
|
||||
winAmount: {
|
||||
marginTop: 6,
|
||||
color: colors.text,
|
||||
fontFamily: fonts.mono,
|
||||
fontSize: 18,
|
||||
textAlign: 'center'
|
||||
},
|
||||
rotateActions: {
|
||||
marginTop: 20
|
||||
confettiLayer: {
|
||||
...StyleSheet.absoluteFillObject
|
||||
},
|
||||
confettiPiece: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
borderRadius: 3,
|
||||
opacity: 0.9
|
||||
}
|
||||
});
|
||||
|
||||
const seatPositions = {
|
||||
0: { left: '8%', top: '52%' },
|
||||
1: { left: '22%', top: '45%' },
|
||||
2: { left: '36%', top: '40%' },
|
||||
3: { left: '50%', top: '38%' },
|
||||
4: { left: '64%', top: '40%' },
|
||||
5: { left: '78%', top: '45%' },
|
||||
6: { left: '92%', top: '52%' }
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user