gubble
This commit is contained in:
parent
a4626ada30
commit
db0673b77f
6 changed files with 154 additions and 37 deletions
|
@ -2,9 +2,9 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>💸</text></svg>">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<title>🎰 Glückspiel fetzt 🎲💵</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
61
src/App.tsx
61
src/App.tsx
|
@ -2,6 +2,8 @@ import React, { useState, useCallback, useMemo } from 'react';
|
|||
import Card from './components/Card';
|
||||
import TierCard from './components/TierCard';
|
||||
import HistoryList from './components/HistoryList';
|
||||
import type { Upgrades } from './components/GubbleStore';
|
||||
import GubbleStore from './components/GubbleStore';
|
||||
|
||||
// --- Scratch Card Game Types and State ---
|
||||
export type Tier = {
|
||||
|
@ -19,15 +21,18 @@ const TIERS: Tier[] = [
|
|||
{ name: 'Platinum', unlockPrice: 30, buyPrice: 20, gubblePointChance: 0.1 },
|
||||
{ name: 'Diamond', unlockPrice: 75, buyPrice: 50, gubblePointChance: 0.2 },
|
||||
{ name: 'Master', unlockPrice: 750, buyPrice: 500, gubblePointChance: 0.2 },
|
||||
{ name: 'Legend', unlockPrice: 3000, buyPrice: 2000, gubblePointChance: 0.3 },
|
||||
{ name: 'Ultimate', unlockPrice: 7500, buyPrice: 5000, gubblePointChance: 0.3 },
|
||||
{ name: 'Champion', unlockPrice: 15000, buyPrice: 10000, gubblePointChance: 0.4 },
|
||||
{ name: 'Legend', unlockPrice: 10000, buyPrice: 8000, gubblePointChance: 0.3 },
|
||||
{ name: 'Ultimate', unlockPrice: 100000, buyPrice: 80000, gubblePointChance: 0.3 },
|
||||
{ name: 'Champion', unlockPrice: 5000000, buyPrice: 4000000, gubblePointChance: 0.4 },
|
||||
];
|
||||
|
||||
const START_MONEY = 6;
|
||||
const START_UNLOCKED: string[] = [];
|
||||
|
||||
function App() {
|
||||
// --- Game State ---
|
||||
const [money, setMoney] = useState(5); // User starts with $20
|
||||
const [unlockedTiers, setUnlockedTiers] = useState<string[]>([]); // Unlocked tier names
|
||||
const [money, setMoney] = useState(START_MONEY);
|
||||
const [unlockedTiers, setUnlockedTiers] = useState<string[]>(START_UNLOCKED);
|
||||
const [gubblePoints, setGubblePoints] = useState(0);
|
||||
const [card, setCard] = useState<{
|
||||
winningNumbers: number[];
|
||||
|
@ -40,6 +45,10 @@ function App() {
|
|||
winningNumbers: number[];
|
||||
fields: number[];
|
||||
}[]>([]);
|
||||
// --- Gubble Store State ---
|
||||
const [isStoreOpen, setIsStoreOpen] = useState(false);
|
||||
const [upgrades, setUpgrades] = useState<Upgrades>({ evenDouble: false, oddDouble: false, allTripple: false, gubbleDouble: false });
|
||||
const [upgradeBought, setUpgradeBought] = useState(false);
|
||||
|
||||
// --- Tier Unlock Logic ---
|
||||
const handleUnlockTier = useCallback((tier: Tier) => {
|
||||
|
@ -72,9 +81,28 @@ function App() {
|
|||
});
|
||||
}, [unlockedTiers]);
|
||||
|
||||
// --- Gubble Store Logic ---
|
||||
const handleBuyUpgrade = useCallback((upgrade: keyof Upgrades, cost: number) => {
|
||||
setGubblePoints(gp => {
|
||||
if (gp < cost) return gp;
|
||||
setUpgrades(u => ({ ...u, [upgrade]: true }));
|
||||
setUpgradeBought(true);
|
||||
return gp - cost;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleStoreClose = useCallback(() => {
|
||||
setIsStoreOpen(false);
|
||||
if (upgradeBought) {
|
||||
setMoney(START_MONEY);
|
||||
setUnlockedTiers(START_UNLOCKED);
|
||||
setCard(null);
|
||||
setHistory([]);
|
||||
setUpgradeBought(false);
|
||||
}
|
||||
}, [upgradeBought]);
|
||||
|
||||
// --- Update history winnings after all fields scratched ---
|
||||
// This effect only runs when card changes
|
||||
// Use useCallback for setHistory logic
|
||||
const updateHistoryWinnings = useCallback(() => {
|
||||
if (!card) return;
|
||||
const allScratched = card.fields.every(f => f.scratched);
|
||||
|
@ -90,8 +118,6 @@ function App() {
|
|||
}
|
||||
}, [card]);
|
||||
|
||||
// Only run when card changes
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
React.useEffect(updateHistoryWinnings, [card]);
|
||||
|
||||
// --- Memoized winnings for current card ---
|
||||
|
@ -103,6 +129,20 @@ function App() {
|
|||
<h1 className="text-3xl font-bold text-blue-600 mb-2">Scratch Card Game</h1>
|
||||
<div className="mb-4 text-lg">Money: <span className="font-mono font-bold">${money}</span></div>
|
||||
<div className="mb-4 text-lg">Gubble Points: <span className="font-mono font-bold">{gubblePoints}</span></div>
|
||||
<button
|
||||
className="mb-4 px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white font-bold rounded disabled:opacity-50"
|
||||
disabled={gubblePoints < 1}
|
||||
onClick={() => setIsStoreOpen(true)}
|
||||
>
|
||||
Open Gubble Point Store
|
||||
</button>
|
||||
<GubbleStore
|
||||
isOpen={isStoreOpen}
|
||||
onClose={handleStoreClose}
|
||||
gubblePoints={gubblePoints}
|
||||
upgrades={upgrades}
|
||||
onBuyUpgrade={handleBuyUpgrade}
|
||||
/>
|
||||
<div className="flex flex-wrap gap-6 mb-8 justify-center">
|
||||
{TIERS.map((tier) => (
|
||||
<TierCard
|
||||
|
@ -112,6 +152,7 @@ function App() {
|
|||
money={money}
|
||||
onUnlock={handleUnlockTier}
|
||||
onBuy={handleBuyCard}
|
||||
gubblePointChance={tier.gubblePointChance}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -121,7 +162,7 @@ function App() {
|
|||
<span className="font-semibold">Winnings this card: </span>
|
||||
${currentWinnings}
|
||||
</div>
|
||||
<Card card={card} setCard={setCard} setMoney={setMoney} setGubblePoints={setGubblePoints} />
|
||||
<Card card={card} setCard={setCard} setMoney={setMoney} setGubblePoints={setGubblePoints} upgrades={upgrades} />
|
||||
</>
|
||||
)}
|
||||
<HistoryList history={history} />
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import type { Tier } from '../App';
|
||||
import type { Upgrades } from './GubbleStore';
|
||||
|
||||
type CardProps = {
|
||||
card: {
|
||||
|
@ -14,9 +15,10 @@ type CardProps = {
|
|||
} | null>>;
|
||||
setMoney: React.Dispatch<React.SetStateAction<number>>;
|
||||
setGubblePoints: React.Dispatch<React.SetStateAction<number>>;
|
||||
upgrades?: Upgrades;
|
||||
};
|
||||
|
||||
const Card: React.FC<CardProps> = ({ card, setCard, setMoney, setGubblePoints }) => {
|
||||
const Card: React.FC<CardProps> = ({ card, setCard, setMoney, setGubblePoints, upgrades }) => {
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow p-6 flex flex-col items-center mb-6 w-full max-w-md">
|
||||
<div className="mb-2 text-lg font-semibold text-gray-700">{card.tier?.name} Scratch Card</div>
|
||||
|
@ -36,16 +38,28 @@ const Card: React.FC<CardProps> = ({ card, setCard, setMoney, setGubblePoints })
|
|||
disabled={field.scratched}
|
||||
onMouseMove={() => {
|
||||
if (field.scratched) return;
|
||||
// Gubble point gain (with upgrade)
|
||||
if (Math.random() < (card.tier?.gubblePointChance ?? 0)) {
|
||||
setGubblePoints((g) => g + 1);
|
||||
setGubblePoints((g) => g + (upgrades?.gubbleDouble ? 2 : 1));
|
||||
return;
|
||||
}
|
||||
// Determine win amount
|
||||
let won = null;
|
||||
if (card.winningNumbers.includes(field.value)) {
|
||||
won = 0;
|
||||
if (idx < 2) won = Math.ceil((card.tier?.buyPrice ?? 0) * 0.5);
|
||||
else if (idx < 4) won = Math.ceil((card.tier?.buyPrice ?? 0) * 0.8);
|
||||
else won = Math.ceil((card.tier?.buyPrice ?? 0) * 1.2);
|
||||
// Apply upgrades
|
||||
if (upgrades?.allTripple) {
|
||||
won = won * 2;
|
||||
}
|
||||
if (upgrades?.evenDouble && field.value % 2 === 0) {
|
||||
won = won * 2;
|
||||
}
|
||||
if (upgrades?.oddDouble && field.value % 2 === 1) {
|
||||
won = won * 2;
|
||||
}
|
||||
}
|
||||
setCard((prev) => {
|
||||
if (!prev) return prev;
|
||||
|
|
57
src/components/GubbleStore.tsx
Normal file
57
src/components/GubbleStore.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import React from 'react';
|
||||
|
||||
type Upgrades = {
|
||||
evenDouble: boolean;
|
||||
oddDouble: boolean;
|
||||
allTripple: boolean;
|
||||
gubbleDouble: boolean;
|
||||
};
|
||||
|
||||
type GubbleStoreProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
gubblePoints: number;
|
||||
upgrades: Upgrades;
|
||||
onBuyUpgrade: (upgrade: keyof Upgrades, cost: number) => void;
|
||||
};
|
||||
|
||||
const UPGRADE_LIST: { key: keyof Upgrades; label: string; cost: number; description: string }[] = [
|
||||
{ key: 'evenDouble', label: 'Double Even Winnings', cost: 30, description: 'Double winnings from even numbers.' },
|
||||
{ key: 'oddDouble', label: 'Double Odd Winnings', cost: 60, description: 'Double winnings from odd numbers.' },
|
||||
{ key: 'allTripple', label: 'Tripple All Winnings', cost: 150, description: 'Tripple all winnings.' },
|
||||
{ key: 'gubbleDouble', label: 'Double Gubble Point Gains', cost: 50, description: 'Double gubble point gains.' },
|
||||
];
|
||||
|
||||
const GubbleStore: React.FC<GubbleStoreProps> = ({ isOpen, onClose, gubblePoints, upgrades, onBuyUpgrade }) => {
|
||||
if (!isOpen) return null;
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-40 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-lg p-8 w-full max-w-md relative">
|
||||
<button className="absolute top-2 right-2 text-gray-500 hover:text-gray-700 text-2xl" onClick={onClose}>×</button>
|
||||
<h2 className="text-2xl font-bold mb-4 text-blue-700">Gubble Point Store</h2>
|
||||
<div className="mb-4">Gubble Points: <span className="font-mono font-bold">{gubblePoints}</span></div>
|
||||
<ul className="space-y-4">
|
||||
{UPGRADE_LIST.map(upg => (
|
||||
<li key={upg.key} className="flex flex-col sm:flex-row sm:items-center sm:justify-between bg-gray-50 rounded p-3 border">
|
||||
<div>
|
||||
<div className="font-semibold text-lg">{upg.label}</div>
|
||||
<div className="text-gray-500 text-sm mb-1">{upg.description}</div>
|
||||
<div className="text-gray-700 text-sm">Cost: <span className="font-mono">{upg.cost}</span></div>
|
||||
</div>
|
||||
<button
|
||||
className={`mt-2 sm:mt-0 px-4 py-1 rounded font-bold text-white ${upgrades[upg.key] ? 'bg-gray-400 cursor-not-allowed' : gubblePoints < upg.cost ? 'bg-yellow-200 cursor-not-allowed' : 'bg-blue-500 hover:bg-blue-600'}`}
|
||||
disabled={upgrades[upg.key] || gubblePoints < upg.cost}
|
||||
onClick={() => onBuyUpgrade(upg.key, upg.cost)}
|
||||
>
|
||||
{upgrades[upg.key] ? 'Bought' : 'Buy'}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export type { Upgrades };
|
||||
export default GubbleStore;
|
|
@ -30,7 +30,16 @@ const HistoryList: React.FC<HistoryListProps> = React.memo(({ history }) => {
|
|||
</div>
|
||||
<div className="flex gap-1 mt-2 sm:mt-0">
|
||||
{h.fields.map((n, j) => (
|
||||
<span key={j} className="w-5 h-5 flex items-center justify-center rounded bg-gray-100 text-gray-700 text-xs border border-gray-300">{n}</span>
|
||||
<span
|
||||
key={j}
|
||||
className={`w-5 h-5 flex items-center justify-center rounded text-xs border ${
|
||||
h.winningNumbers.includes(n)
|
||||
? 'bg-green-100 text-green-700 border-green-300'
|
||||
: 'bg-gray-100 text-gray-700 border-gray-300'
|
||||
}`}
|
||||
>
|
||||
{n}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
@ -5,34 +5,30 @@ type TierCardProps = {
|
|||
tier: Tier;
|
||||
unlocked: boolean;
|
||||
money: number;
|
||||
gubblePointChance: number;
|
||||
onUnlock: (tier: Tier) => void;
|
||||
onBuy: (tier: Tier) => void;
|
||||
};
|
||||
|
||||
const TierCard: React.FC<TierCardProps> = React.memo(({ tier, unlocked, money, onUnlock, onBuy }) => {
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow p-4 flex flex-col items-center w-40">
|
||||
<div className="text-xl font-semibold mb-1">{tier.name}</div>
|
||||
<div className="text-gray-500 text-sm mb-2">Unlock: ${tier.unlockPrice}</div>
|
||||
<div className="text-gray-500 text-sm mb-2">Buy: ${tier.buyPrice}</div>
|
||||
{!unlocked ? (
|
||||
<button
|
||||
className="bg-yellow-400 hover:bg-yellow-500 text-white font-bold px-4 py-1 rounded mb-1 disabled:opacity-50"
|
||||
disabled={money < tier.unlockPrice}
|
||||
onClick={() => onUnlock(tier)}
|
||||
className={`rounded-lg shadow p-2 flex flex-col items-center w-32 font-bold text-white mb-1 disabled:opacity-50 ${
|
||||
!unlocked
|
||||
? 'bg-yellow-400 hover:bg-yellow-500'
|
||||
: 'bg-green-500 hover:bg-green-600'
|
||||
}`}
|
||||
disabled={!unlocked ? money < tier.unlockPrice : money < tier.buyPrice}
|
||||
onClick={() => (!unlocked ? onUnlock(tier) : onBuy(tier))}
|
||||
>
|
||||
Unlock
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="bg-green-500 hover:bg-green-600 text-white font-bold px-4 py-1 rounded mb-1 disabled:opacity-50"
|
||||
disabled={money < tier.buyPrice}
|
||||
onClick={() => onBuy(tier)}
|
||||
>
|
||||
Buy Card
|
||||
</button>
|
||||
)}
|
||||
<div className="text-lg font-semibold mb-1">{tier.name}</div>
|
||||
<div className="text-gray-500 text-xs mb-1">Unlock: ${tier.unlockPrice}</div>
|
||||
<div className="text-gray-500 text-xs mb-1">Buy: ${tier.buyPrice}</div>
|
||||
<div className="text-gray-500 text-xs mb-1">Point Chance: {tier.gubblePointChance * 100}%</div>
|
||||
<div>
|
||||
{!unlocked ? 'Unlock' : 'Buy Card'}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue