Skip to content

Instantly share code, notes, and snippets.

@LrWm3
Created December 25, 2025 17:06
Show Gist options
  • Select an option

  • Save LrWm3/e4cfe4ea595ea672b1c288a7f65eaeee to your computer and use it in GitHub Desktop.

Select an option

Save LrWm3/e4cfe4ea595ea672b1c288a7f65eaeee to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Fauxlatro</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
:root {
--card-w: 80px; --card-h: 115px;
--joker-w: 75px; --joker-h: 105px;
--bg: #0b0d14;
--accent-blue: #4d94ff; --accent-red: #ff4d4d; --accent-gold: #f1c40f;
--hand-bg: #1a1d2e;
--suit-spade: #2c3e50; --suit-heart: #e74c3c; --suit-club: #3498db; --suit-diamond: #e67e22;
}
@media (min-width: 640px) { :root { --card-w: 105px; --card-h: 150px; --joker-w: 95px; --joker-h: 130px; } }
body {
background-color: var(--bg); color: white; height: 100vh; margin: 0;
display: flex; flex-direction: column; font-family: 'Courier New', Courier, monospace;
touch-action: none; user-select: none; overflow: hidden;
}
* { scrollbar-width: none; -ms-overflow-style: none; }
*::-webkit-scrollbar { display: none; }
.crt-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.05) 50%),
linear-gradient(90deg, rgba(255, 0, 0, 0.02), rgba(0, 255, 0, 0.01), rgba(0, 0, 255, 0.03));
background-size: 100% 4px, 4px 100%; pointer-events: none; z-index: 10000;
}
.header-stats { position: fixed; top: 10px; left: 10px; z-index: 7000; display: flex; flex-direction: column; gap: 8px; width: 160px; }
.ui-panel { background: rgba(0,0,0,0.9); padding: 6px 12px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1); backdrop-filter: blur(8px); }
.info-btn {
position: fixed; top: 10px; right: 10px; z-index: 7000;
background: rgba(0,0,0,0.8); border: 1px solid rgba(255,255,255,0.2);
border-radius: 50%; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;
font-weight: bold; cursor: pointer; transition: background 0.2s;
}
.info-btn:hover { background: var(--hand-bg); border-color: var(--accent-blue); }
.joker-area { position: fixed; top: 10px; left: 180px; right: 50px; height: calc(var(--joker-h) + 15px); border: 2px dashed rgba(255,255,255,0.1); border-radius: 12px; z-index: 6000; background: rgba(0,0,0,0.4); }
.card, .joker-card, .planet-card, .pack-card {
width: var(--card-w); height: var(--card-h); background: #fff; border-radius: 8px; position: absolute;
display: flex; flex-direction: column; justify-content: space-between; padding: 10px;
box-shadow: 0 4px 12px rgba(0,0,0,0.8); cursor: pointer; color: #000;
transition: left 0.5s cubic-bezier(0.19, 1, 0.22, 1), top 0.5s cubic-bezier(0.19, 1, 0.22, 1), transform 0.4s cubic-bezier(0.19, 1, 0.22, 1), width 0.4s, height 0.4s, opacity 0.3s;
transform: translate(-50%, -50%);
}
.joker-card { width: var(--joker-w); height: var(--joker-h); }
.planet-card { width: var(--joker-w); height: var(--joker-h); background: #2a1a3e; color: #fff; border: 2px solid #6b4dff; }
.pack-card { width: var(--joker-w); height: var(--joker-h); background: #1a3e2a; color: #fff; border: 2px solid #4dff88; }
.inspected { border: 3px solid var(--accent-gold) !important; box-shadow: 0 0 25px var(--accent-gold) !important; transform: translate(-50%, -50%) scale(1.15) !important; z-index: 900 !important; }
.selected { border: 3px solid var(--accent-blue); box-shadow: 0 0 20px var(--accent-blue); z-index: 100; }
.dragging { position: fixed !important; z-index: 9999 !important; transition: none !important; opacity: 0.9; pointer-events: none; }
.placeholder { background: rgba(255,255,255,0.05); border: 2px dashed rgba(255,255,255,0.2); box-shadow: none; visibility: visible; }
.placeholder * { visibility: hidden; }
.not-scoring { opacity: 0.25; filter: grayscale(0.9); transform: translate(-50%, -30%) scale(0.8) !important; }
.rank-label { font-weight: 900; font-size: 1.5rem; line-height: 1; pointer-events: none; }
.suit-icon { align-self: flex-end; font-size: 1.8rem; line-height: 1; pointer-events: none; }
.suit-club { color: var(--suit-club); } .suit-heart { color: var(--suit-heart); } .suit-diamond { color: var(--suit-diamond); } .suit-spade { color: var(--suit-spade); }
.game-viewport { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; padding-bottom: 20px; }
.played-zone { width: 100%; height: 160px; position: relative; margin-bottom: 20px; pointer-events: none; }
.hand-zone { width: 100%; max-width: 1000px; height: 180px; position: relative; margin: 0 auto; }
.footer-panel { background: rgba(0,0,0,0.95); border-top: 2px solid #1a1d2e; padding: 15px; z-index: 2000; }
.big-btn { height: 65px; min-width: 130px; border-radius: 8px; font-weight: 900; text-transform: uppercase; border: 3px solid rgba(255,255,255,0.1); cursor: pointer; }
.btn-play { background: #1a2a4e; color: var(--accent-blue); }
.btn-discard { background: #3e1a1a; color: var(--accent-red); }
.overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.95); display: none; flex-direction: column; align-items: center; justify-content: center; z-index: 5000; }
.shop-overlay { background: #06070a; z-index: 4000; padding: 20px; justify-content: center; overflow: hidden; }
.shop-title-container { margin-bottom: 20px; transform: rotate(-3deg); z-index: 10; }
.shop-title { font-size: 3.5rem; font-weight: 900; color: #fff; text-shadow: 0 0 10px var(--accent-blue), 0 0 20px var(--accent-blue); font-style: italic; letter-spacing: -3px; line-height: 1; }
.shop-section-label { text-transform: uppercase; font-size: 10px; font-weight: 900; color: var(--accent-gold); letter-spacing: 4px; margin-bottom: 8px; opacity: 0.6; }
.shop-row { position: relative; width: 100%; height: 140px; margin-bottom: 30px; display: flex; justify-content: center; }
.price-tag { position: absolute; bottom: -25px; left: 50%; transform: translateX(-50%); color: var(--accent-gold); font-weight: 900; font-size: 14px; pointer-events: none; }
.inspect-panel {
position: fixed; top: 180px; right: 20px; width: 240px; background: var(--hand-bg); border: 2px solid var(--accent-gold);
border-radius: 8px; padding: 15px; z-index: 8000; display: none; box-shadow: 0 10px 30px rgba(0,0,0,0.8);
}
@keyframes jiggle { 0%, 100% { transform: translate(-50%, -50%) scale(1); } 50% { transform: translate(-50%, -65%) scale(1.15); } }
@keyframes pack-shake { 0%, 100% { transform: translate(-50%, -50%) rotate(0deg) scale(1.5); } 25% { transform: translate(-52%, -50%) rotate(-5deg) scale(1.6); } 75% { transform: translate(-48%, -50%) rotate(5deg) scale(1.6); } }
.trigger-jiggle { animation: jiggle 0.3s ease-out; z-index: 3000 !important; box-shadow: 0 0 40px #fff; }
.pack-opening-anim { animation: pack-shake 0.1s infinite; z-index: 9000 !important; }
.fly-away { transform: translate(150vw, -100vh) rotate(90deg) scale(0.2) !important; opacity: 0; pointer-events: none; }
</style>
</head>
<body onclick="handleGlobalClick(event)">
<div class="crt-overlay"></div>
<button class="info-btn" onclick="toggleInfo(true)">?</button>
<div class="header-stats">
<div class="ui-panel">
<p class="text-[8px] uppercase opacity-50 font-bold">Ante <span id="ante-num">1/8</span></p>
<p class="text-xs font-black text-white uppercase tracking-tighter" id="blind-name">Small Blind</p>
</div>
<div class="ui-panel"><p class="text-[8px] uppercase opacity-50 font-bold">Money</p><p class="text-lg font-black text-yellow-400" id="money-display">$10</p></div>
<div class="ui-panel"><p class="text-[8px] uppercase opacity-50 font-bold">Round Score</p><div class="flex items-baseline gap-1"><p class="text-lg font-black text-blue-400" id="total-score">0</p><p class="text-[10px] opacity-40 font-bold">/ <span id="target-score">300</span></p></div></div>
</div>
<div class="joker-area" id="joker-inventory-list"></div>
<div class="inspect-panel" id="inspect-panel">
<h3 class="text-accent-gold font-black uppercase text-sm id-name" id="inspect-name">NAME</h3>
<p class="text-[10px] text-gray-300 leading-tight mb-4" id="inspect-desc">DESCRIPTION.</p>
<div id="inspect-actions">
<button class="w-full bg-red-900 border border-red-500 text-white font-black py-2 rounded text-[10px] uppercase" id="sell-btn">Sell</button>
<p class="text-[8px] text-white/30 text-center mt-2 uppercase italic" id="buy-hint">Drag into slot to purchase</p>
</div>
</div>
<div class="fixed top-[180px] left-1/2 -translate-x-1/2 z-[1500] text-center pointer-events-none">
<div class="ui-panel border-blue-500/50 min-w-[280px]" id="hand-box">
<p class="text-sm font-black uppercase tracking-widest" id="hand-name">Select Cards</p>
<p class="text-lg font-black mt-1" id="hand-score-val"></p>
</div>
</div>
<div class="game-viewport">
<div class="played-zone" id="played-list"></div>
<div class="hand-zone" id="hand-list"></div>
</div>
<div class="footer-panel">
<div class="flex justify-center gap-3 mb-4 flex-wrap">
<button class="big-btn btn-play" id="play-btn" onclick="playHandSequence()">Play <span class="block text-[10px] opacity-60" id="play-sub">4 Left</span></button>
<button class="big-btn btn-discard" id="discard-btn" onclick="discardSelected()">Discard <span class="block text-[10px] opacity-60" id="discard-sub">3 Left</span></button>
<button class="big-btn bg-zinc-800 text-white min-w-[160px]" id="sort-toggle-btn" onclick="toggleSort()">Sort By Suit</button>
</div>
<div class="flex justify-between items-center max-w-[800px] mx-auto text-[10px] font-bold uppercase text-white/30">
<div id="sel-count">0/5 Selected</div>
<button onclick="resetGame()" class="hover:text-white underline">[ Reset Run ]</button>
<div id="deck-info">Deck: 52</div>
</div>
</div>
<!-- Shop Modal -->
<div id="shop-overlay" class="overlay shop-overlay">
<div class="shop-title-container"><h1 class="shop-title italic uppercase">The Shop</h1></div>
<div class="w-full max-w-2xl px-4">
<div class="flex flex-col items-center">
<div class="w-full">
<div class="shop-section-label text-center">Jokers</div>
<div class="shop-row" id="shop-list-jokers"></div>
</div>
<div class="w-full">
<div class="shop-section-label text-center">Packs</div>
<div class="shop-row" id="shop-list-packs"></div>
</div>
</div>
</div>
<div class="mt-6 flex gap-4">
<button class="bg-gray-800 border border-yellow-600 text-yellow-500 px-8 py-3 rounded font-black text-xs uppercase" onclick="rerollShop()">Reroll ($5)</button>
<button class="bg-blue-600 px-12 py-3 rounded font-black text-xs uppercase shadow-lg shadow-blue-900/50" onclick="startNextRound()">Next Round</button>
</div>
</div>
<!-- Pack Selection Modal -->
<div id="pack-overlay" class="overlay" style="background: rgba(0,0,0,0.98); z-index: 8500;">
<div id="pack-selection-container" class="w-full h-full flex flex-col items-center justify-center">
<h2 class="text-4xl font-black italic uppercase text-accent-gold mb-2" id="pack-name-display">PACK NAME</h2>
<p class="text-white opacity-50 mb-12 uppercase tracking-widest text-xs" id="pack-picks-display">Picks Left: 1</p>
<div class="w-full max-w-3xl h-[200px] relative" id="pack-item-list"></div>
<button class="mt-24 border border-white/20 px-8 py-3 text-[10px] uppercase font-black hover:bg-white/10" id="skip-pack-btn" onclick="closePack()">Skip Pack</button>
</div>
</div>
<!-- Info Overlay -->
<div id="info-overlay" class="overlay">
<div class="bg-black border border-white/20 p-8 rounded-lg max-w-md text-center shadow-2xl">
<h2 class="text-3xl font-black italic uppercase text-blue-400 mb-4">About Fauxlatro</h2>
<p class="text-sm text-gray-300 mb-2">Inspired by the masterpiece</p>
<p class="text-xl font-black text-white uppercase tracking-widest mb-4">BALATRO</p>
<p class="text-[10px] text-gray-500 uppercase mb-8">All rights reserved to LocalThunk and Playstack.</p>
<a href="https://www.playbalatro.com" target="_blank" class="block w-full bg-blue-600 text-white font-black py-4 rounded uppercase mb-4">Visit Official Site</a>
<button class="text-xs text-gray-400 underline uppercase" onclick="toggleInfo(false)">Close</button>
</div>
</div>
<!-- Win Overlay -->
<div id="win-overlay" class="overlay">
<h1 class="text-5xl font-black text-blue-400 uppercase italic">Round Won</h1>
<div class="bg-white/5 border border-white/10 p-8 rounded my-8 min-w-[320px]">
<div class="flex justify-between mb-2"><span>Base Reward:</span><span class="text-yellow-400">$4</span></div>
<div class="flex justify-between mb-2"><span>Hands Left:</span><span class="text-yellow-400" id="win-bonus">$0</span></div>
<div class="flex justify-between mb-2"><span>Joker Bonus:</span><span class="text-yellow-400" id="joker-gold-bonus">$0</span></div>
<div class="flex justify-between border-b border-white/10 pb-2 mb-2"><span>Interest:</span><span class="text-yellow-400" id="interest-bonus">$0</span></div>
<div class="flex justify-between pt-2 font-black text-2xl"><span>Total Gained:</span><span class="text-yellow-400" id="win-total">$0</span></div>
</div>
<button class="bg-blue-500 text-black font-black px-12 py-4 rounded uppercase" onclick="goToShop()">Enter Shop</button>
</div>
<!-- Lose Overlay -->
<div id="lose-overlay" class="overlay">
<h1 class="text-7xl font-black text-red-600 italic uppercase">Defeated</h1>
<button class="bg-red-600 text-white font-black px-12 py-4 rounded mt-10 uppercase" onclick="resetGame()">New Run</button>
</div>
<!-- Victory Overlay -->
<div id="victory-overlay" class="overlay">
<h1 class="text-7xl font-black text-yellow-400 italic uppercase text-center">Victory!<br><span class="text-3xl">Ante 8 Defeated</span></h1>
<button class="bg-yellow-400 text-black font-black px-12 py-4 rounded mt-10 uppercase" onclick="resetGame()">New Run</button>
</div>
<script>
// --- CONSTANTS ---
const ANTE_BASE_SCORES = [300, 800, 2000, 5000, 11000, 20000, 35000, 50000];
const BLIND_MODS = [ { name: "Small Blind", mult: 1.0 }, { name: "Big Blind", mult: 1.5 }, { name: "Boss Blind", mult: 2.0 } ];
const HAND_TYPES = {
'Straight Flush': { emoji: '🍉', baseChips: 100, baseMult: 8, chipMod: 40, multMod: 4 },
'Four of a Kind': { emoji: '🍑', baseChips: 70, baseMult: 6, chipMod: 30, multMod: 3 },
'Full House': { emoji: '🍐', baseChips: 40, baseMult: 4, chipMod: 25, multMod: 2 },
'Flush': { emoji: '🍊', baseChips: 35, baseMult: 4, chipMod: 15, multMod: 2 },
'Straight': { emoji: '🍓', baseChips: 30, baseMult: 4, chipMod: 30, multMod: 3 },
'Three of a Kind': { emoji: '🍋', baseChips: 30, baseMult: 3, chipMod: 20, multMod: 2 },
'Two Pair': { emoji: '🍇', baseChips: 20, baseMult: 2, chipMod: 20, multMod: 1 },
'Pair': { emoji: '🍌', baseChips: 10, baseMult: 2, chipMod: 15, multMod: 1 },
'High Card': { emoji: '🍒', baseChips: 5, baseMult: 1, chipMod: 10, multMod: 1 }
};
const JOKER_POOL = [
{ id: 1, name: 'Basic', emoji: '🃏', price: 2, mult: 4, chips: 0, xMult: 1, trigger: 'all', desc: "+4 Mult on every hand." },
{ id: 2, name: 'Chiller', emoji: '❄️', price: 2, mult: 0, chips: 20, xMult: 1, trigger: 'all', desc: "+20 Chips on every hand." },
{ id: 3, name: 'Pair Buddy', emoji: '👯', price: 4, mult: 12, chips: 0, xMult: 1, trigger: 'Pair', desc: "+12 Mult if hand contains a Pair." },
{ id: 4, name: 'Flush Burn', emoji: '🔥', price: 6, mult: 0, chips: 0, xMult: 1.5, trigger: 'Flush', desc: "x1.5 Mult if hand contains a Flush." },
{ id: 5, name: 'Archer', emoji: '🏹', price: 4, mult: 0, chips: 60, xMult: 1, trigger: 'Straight', desc: "+60 Chips if hand contains a Straight." },
{ id: 6, name: 'Odd Todd', emoji: '🔢', price: 4, mult: 0, chips: 30, xMult: 1, trigger: 'odd', desc: "+30 Chips for scoring Odd ranks." },
{ id: 7, name: 'Even Steven', emoji: '🔟', price: 4, mult: 4, chips: 0, xMult: 1, trigger: 'even', desc: "+4 Mult for scoring Even ranks." },
{ id: 8, name: 'The Trio', emoji: '3️⃣', price: 8, mult: 0, chips: 0, xMult: 3, trigger: 'Three of a Kind', desc: "x3 Mult if hand contains 3 of a Kind." },
{ id: 9, name: 'Golden Joker', emoji: '💰', price: 6, mult: 0, chips: 0, xMult: 1, trigger: 'win', desc: "Earn $3 extra when you win a round." },
{ id: 10, name: 'Blue Joker', emoji: '💙', price: 5, mult: 0, chips: 0, xMult: 1, trigger: 'deck', desc: "+2 Chips per card remaining in Deck." },
{ id: 11, name: 'Lusty', emoji: '❤️', price: 4, mult: 4, chips: 0, xMult: 1, trigger: 'suit_2', desc: "+4 Mult for scoring Hearts." },
{ id: 12, name: 'Greedy', emoji: '💎', price: 4, mult: 4, chips: 0, xMult: 1, trigger: 'suit_1', desc: "+4 Mult for scoring Diamonds." },
{ id: 13, name: 'Wrathful', emoji: '♠️', price: 4, mult: 4, chips: 0, xMult: 1, trigger: 'suit_0', desc: "+4 Mult for scoring Spades." },
{ id: 14, name: 'Gluttonous', emoji: '♣️', price: 4, mult: 4, chips: 0, xMult: 1, trigger: 'suit_3', desc: "+4 Mult for scoring Clubs." },
{ id: 15, name: 'Recycler', emoji: '♻️', price: 5, mult: 0, chips: 0, xMult: 1, trigger: 'discard_cash', desc: "Earn $1 for every Discard used." },
{ id: 16, name: 'Eke Joker', emoji: '👑', price: 8, mult: 0, chips: 0, xMult: 3, trigger: 'all', desc: "x3 Mult. Hail to the King." },
{ id: 17, name: 'Tater Joker', emoji: '🐶', price: 8, mult: 0, chips: 0, xMult: 3, trigger: 'all', desc: "x3 Mult. Good boy!" },
{ id: 18, name: 'Banana Joker', emoji: '🍌', price: 4, mult: 15, chips: 0, xMult: 1, trigger: 'all', desc: "+15 Mult. Potassium power!" }
];
const PACK_POOL_DEF = [
{ type: 'fruit', size: 'Small', price: 4, show: 3, pick: 1, count: 8 },
{ type: 'fruit', size: 'Medium', price: 6, show: 5, pick: 1, count: 8 },
{ type: 'fruit', size: 'Large', price: 8, show: 5, pick: 1, count: 4 },
{ type: 'joker', size: 'Small', price: 8, show: 2, pick: 1, count: 4 },
{ type: 'joker', size: 'Medium', price: 10, show: 4, pick: 1, count: 4 },
{ type: 'joker', size: 'Large', price: 12, show: 5, pick: 2, count: 2 },
];
// --- GLOBAL STATE ---
let packDeck = [], planetDeck = [], deck = [];
let totalScore = 0, money = 10, plays = 4, discards = 3;
let anteIndex = 0, blindIndex = 0;
let selectedOrder = [], inventory = [], shopJokers = [], shopPacks = [], isBusy = false, sortMode = 'rank';
let handLevels = Object.keys(HAND_TYPES).reduce((acc, t) => ({...acc, [t]: 1}), {});
let inspectedIdx = -1, inspectedSource = null;
let activePack = null, packPicksLeft = 0, packSelectionItems = [];
// --- CORE FUNCTIONS ---
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const toggleInfo = (show) => document.getElementById('info-overlay').style.display = show ? 'flex' : 'none';
function initDecks() {
const SUITS = [{s:'♣',c:'suit-club',v:3}, {s:'♥',c:'suit-heart',v:2}, {s:'♦',c:'suit-diamond',v:1}, {s:'♠',c:'suit-spade',v:0}];
const RANKS = [{n:'2',v:2},{n:'3',v:3},{n:'4',v:4},{n:'5',v:5},{n:'6',v:6},{n:'7',v:7},{n:'8',v:8},{n:'9',v:9},{n:'10',v:10},{n:'J',v:10,val:11},{n:'Q',v:10,val:12},{n:'K',v:10,val:13},{n:'A',v:11,val:14}];
deck = [];
SUITS.forEach(s => RANKS.forEach(r => deck.push({ rank: r.n, chips: r.v, sortVal: r.val || r.v, suit: s.s, suitCss: s.c, suitVal: s.v })));
for (let i = deck.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i+1)); [deck[i], deck[j]] = [deck[j], deck[i]]; }
packDeck = [];
PACK_POOL_DEF.forEach((p, idx) => { for(let i=0; i<p.count; i++) packDeck.push({...p, id: `pack-${idx}-${i}`}); });
planetDeck = Object.keys(HAND_TYPES).map(name => ({ name, emoji: HAND_TYPES[name].emoji }));
shuffleArray(planetDeck);
}
function shuffleArray(arr) { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } }
function drawToFull() {
const list = document.getElementById('hand-list');
const currentCount = list.querySelectorAll('.card:not(.dragging):not(.fly-away)').length;
const needed = 8 - currentCount;
for (let i = 0; i < Math.min(needed, deck.length); i++) {
setTimeout(() => {
const d = deck.pop();
const el = document.createElement('div');
el.className = 'card'; el.dataset.rankSort = d.sortVal; el.dataset.rankChips = d.chips; el.dataset.suitSort = d.suitVal;
el.dataset.suitCss = d.suitCss; el.dataset.visualIndex = 1000 + i;
el.innerHTML = `<div class="rank-label ${d.suitCss}">${d.rank}</div><div class="suit-icon ${d.suitCss}">${d.suit}</div>`;
el.onpointerdown = (e) => onEntityDown(e, 'hand-list', 'hand');
list.appendChild(el);
applySort(); updateUI();
}, i * 80);
}
}
function updateSpacings(id) {
const list = document.getElementById(id);
if (!list) return;
const items = [...list.querySelectorAll('.card:not(.dragging):not(.fly-away), .joker-card:not(.dragging), .planet-card:not(.dragging), .pack-card:not(.dragging)')];
if (id === 'hand-list') items.sort((a, b) => parseFloat(a.dataset.visualIndex) - parseFloat(b.dataset.visualIndex));
items.forEach((item, i) => {
item.style.left = ((i + 1) / (items.length + 1)) * 100 + "%";
item.style.top = "50%";
const isSelected = item.classList.contains('selected');
const yOffset = isSelected ? '-45px' : '0px';
item.style.transform = `translate(-50%, calc(-50% + ${yOffset}))`;
item.style.zIndex = isSelected ? 200 + i : i;
});
}
function getTargetScore() { return ANTE_BASE_SCORES[anteIndex] * BLIND_MODS[blindIndex].mult; }
function handContains(handName, triggerName) {
if (triggerName === 'all') return true;
const h = { 'High Card': ['High Card', 'Pair', 'Two Pair', 'Three of a Kind', 'Straight', 'Flush', 'Full House', 'Four of a Kind', 'Straight Flush'], 'Pair': ['Pair', 'Two Pair', 'Three of a Kind', 'Full House', 'Four of a Kind'], 'Two Pair': ['Two Pair', 'Full House'], 'Three of a Kind': ['Three of a Kind', 'Full House', 'Four of a Kind'], 'Straight': ['Straight', 'Straight Flush'], 'Flush': ['Flush', 'Straight Flush'], 'Full House': ['Full House'], 'Four of a Kind': ['Four of a Kind'], 'Straight Flush': ['Straight Flush'] };
return h[triggerName]?.includes(handName) || false;
}
function evaluateHand() {
const handCards = [...document.querySelectorAll('#hand-list .selected')].sort((a,b) => a.offsetLeft - b.offsetLeft);
if (!handCards.length) return { name: "Select Cards", chips: 0, mult: 0, lvl: 1, scoring: [] };
const sortedByRank = [...handCards].sort((a,b) => parseInt(a.dataset.rankSort) - parseInt(b.dataset.rankSort));
const ranks = sortedByRank.map(c => parseInt(c.dataset.rankSort));
const suits = sortedByRank.map(c => parseInt(c.dataset.suitSort));
const counts = {}; ranks.forEach(r => counts[r] = (counts[r]||0)+1);
const freq = Object.entries(counts).sort((a,b)=>b[1]-a[1]);
const isFlush = new Set(suits).size === 1 && handCards.length === 5;
let isStr = false;
if (handCards.length === 5) {
const uniq = [...new Set(ranks)];
if (uniq.length === 5 && (ranks[4]-ranks[0] === 4)) isStr = true;
if (uniq.length === 5 && ranks[4] === 14 && ranks[0] === 2 && ranks[3] === 5) isStr = true;
}
let name = "High Card", scoringElements = [];
if (isStr && isFlush) { name = "Straight Flush"; scoringElements = handCards; }
else if (freq[0][1] === 4) { name = "Four of a Kind"; scoringElements = handCards.filter(c => c.dataset.rankSort == freq[0][0]); }
else if (freq[0][1] === 3 && freq[1]?.[1] === 2) { name = "Full House"; scoringElements = handCards; }
else if (isFlush) { name = "Flush"; scoringElements = handCards; }
else if (isStr) { name = "Straight"; scoringElements = handCards; }
else if (freq[0][1] === 3) { name = "Three of a Kind"; scoringElements = handCards.filter(c => c.dataset.rankSort == freq[0][0]); }
else if (freq[0][1] === 2 && freq[1]?.[1] === 2) { name = "Two Pair"; scoringElements = handCards.filter(c => c.dataset.rankSort == freq[0][0] || c.dataset.rankSort == freq[1][0]); }
else if (freq[0][1] === 2) { name = "Pair"; scoringElements = handCards.filter(c => c.dataset.rankSort == freq[0][0]); }
else { name = "High Card"; scoringElements = [sortedByRank[sortedByRank.length-1]]; }
const lvl = handLevels[name]; const d = HAND_TYPES[name];
return { name, lvl, chips: d.baseChips + (lvl-1)*d.chipMod, mult: d.baseMult + (lvl-1)*d.multMod, scoring: scoringElements };
}
function updatePreview(n, c, m) {
const nEl = document.getElementById('hand-name');
const sEl = document.getElementById('hand-score-val');
if (HAND_TYPES[n]) {
const lvl = handLevels[n] || 1;
nEl.innerText = `${HAND_TYPES[n].emoji} ${n} Lvl.${lvl}`;
sEl.innerHTML = `<span style="color:#4d94ff">${c}</span> x <span style="color:#ff4d4d">${m}</span>`;
} else { nEl.innerText = n; sEl.innerHTML = ""; }
}
async function playHandSequence() {
if (isBusy) return;
const evalResult = evaluateHand();
if (evalResult.name === "Select Cards") return;
isBusy = true; plays--; deselectEverything();
const pZone = document.getElementById('played-list');
const hZone = document.getElementById('hand-list');
const jokerEls = document.querySelectorAll('#joker-inventory-list .joker-card');
const handCards = [...hZone.querySelectorAll('.card')].filter(el => el.classList.contains('selected')).sort((a,b) => a.offsetLeft - b.offsetLeft);
const pRect = pZone.getBoundingClientRect();
for(let c of handCards) {
const cR = c.getBoundingClientRect();
c.style.left = (cR.left - pRect.left + cR.width/2) + "px";
c.style.top = (cR.top - pRect.top + cR.height/2) + "px";
c.classList.remove('selected'); pZone.appendChild(c);
}
pZone.offsetHeight;
handCards.forEach((c, i) => { c.style.left = ((i+1)/(handCards.length+1)*100)+"%"; c.style.top = "50%"; });
await sleep(600);
let currentChips = evalResult.chips, currentMult = evalResult.mult;
updatePreview(evalResult.name, currentChips, currentMult);
for (const card of handCards) {
if (!evalResult.scoring.includes(card)) {
card.classList.add('not-scoring');
await sleep(150);
continue;
}
card.classList.add('trigger-jiggle');
const rankVal = parseInt(card.dataset.rankSort);
currentChips += parseInt(card.dataset.rankChips);
for (let i=0; i<inventory.length; i++) {
const j = inventory[i];
let triggered = (j.trigger === 'odd' && [3,5,7,9,14].includes(rankVal)) ||
(j.trigger === 'even' && [2,4,6,8,10].includes(rankVal)) ||
(j.trigger === `suit_${card.dataset.suitSort}`);
if (triggered) {
if (jokerEls[i]) jokerEls[i].classList.add('trigger-jiggle');
currentChips += j.chips; currentMult += j.mult;
updatePreview(evalResult.name, currentChips, Math.floor(currentMult));
}
}
updatePreview(evalResult.name, currentChips, Math.floor(currentMult));
await sleep(400);
card.classList.remove('trigger-jiggle');
jokerEls.forEach(el => el.classList.remove('trigger-jiggle'));
}
for (let i = 0; i < inventory.length; i++) {
const j = inventory[i];
if (handContains(evalResult.name, j.trigger) || j.trigger === 'deck') {
if (jokerEls[i]) jokerEls[i].classList.add('trigger-jiggle');
if (j.trigger === 'deck') currentChips += (deck.length * 2);
else { currentChips += j.chips; currentMult += j.mult; }
currentMult *= j.xMult;
updatePreview(evalResult.name, currentChips, Math.floor(currentMult));
await sleep(500);
if (jokerEls[i]) jokerEls[i].classList.remove('trigger-jiggle');
}
}
totalScore += (currentChips * Math.floor(currentMult)); updateUI();
await sleep(300);
handCards.forEach(c => c.classList.add('fly-away'));
await sleep(600);
handCards.forEach(c => c.remove());
isBusy = false;
if (totalScore >= getTargetScore()) showWin();
else if (plays <= 0) document.getElementById('lose-overlay').style.display = 'flex';
drawToFull();
}
function updateUI() {
const evalResult = evaluateHand();
const nEl = document.getElementById('hand-name');
const sEl = document.getElementById('hand-score-val');
if (HAND_TYPES[evalResult.name]) { nEl.innerText = `${HAND_TYPES[evalResult.name].emoji} ${evalResult.name} Lvl.${evalResult.lvl}`; sEl.innerHTML = `<span style="color:#4d94ff">${evalResult.chips}</span> x <span style="color:#ff4d4d">${evalResult.mult}</span>`; } else { nEl.innerText = evalResult.name; sEl.innerHTML = ""; }
document.getElementById('money-display').innerText = `$${money}`;
document.getElementById('total-score').innerText = totalScore.toLocaleString();
document.getElementById('target-score').innerText = Math.floor(getTargetScore()).toLocaleString();
document.getElementById('ante-num').innerText = `${anteIndex + 1}/8`;
document.getElementById('blind-name').innerText = BLIND_MODS[blindIndex].name;
document.getElementById('play-sub').innerText = `${plays} Left`;
document.getElementById('discard-sub').innerText = `${discards} Left`;
document.getElementById('sel-count').innerText = `${document.querySelectorAll('#hand-list .selected').length}/5 SELECTED`;
document.getElementById('deck-info').innerText = `Deck: ${deck.length}`;
updateSpacings('hand-list');
}
// --- SHOP & INTERACTION ---
function rerollShop(isFree = false) {
if (!isFree) { if (money < 5) return; money -= 5; }
const jokerPool = JOKER_POOL.filter(j => !inventory.find(inv => inv.id === j.id));
shopJokers = [];
for (let i = 0; i < 2; i++) if (jokerPool.length) {
const randIdx = Math.floor(Math.random() * jokerPool.length);
shopJokers.push(jokerPool.splice(randIdx, 1)[0]);
}
shopPacks = [];
const tempPackDeck = [...packDeck];
for (let i = 0; i < 2; i++) if (tempPackDeck.length) {
const randIdx = Math.floor(Math.random() * tempPackDeck.length);
shopPacks.push(tempPackDeck.splice(randIdx, 1)[0]);
}
renderShop(); updateUI();
}
function renderShop() {
const jCont = document.getElementById('shop-list-jokers');
const pCont = document.getElementById('shop-list-packs');
jCont.innerHTML = ''; pCont.innerHTML = '';
shopJokers.forEach((j, i) => {
const div = document.createElement('div'); div.className = 'joker-card'; div.dataset.idx = i;
div.innerHTML = `<span class="text-3xl">${j.emoji}</span><span class="text-[9px] font-black uppercase text-center mt-2">${j.name}</span><div class="price-tag">$${j.price}</div>`;
div.onpointerdown = (e) => onEntityDown(e, 'shop-list-jokers', 'shop_joker');
jCont.appendChild(div);
});
shopPacks.forEach((p, i) => {
const div = document.createElement('div'); div.className = 'pack-card'; div.dataset.idx = i;
div.innerHTML = `<span class="text-xs font-black uppercase text-center">${p.size} ${p.type} Pack</span><div class="price-tag">$${p.price}</div>`;
div.onpointerdown = (e) => onEntityDown(e, 'shop-list-packs', 'shop_pack');
pCont.appendChild(div);
});
updateSpacings('shop-list-jokers'); updateSpacings('shop-list-packs');
}
async function purchasePack(idx) {
const pack = shopPacks[idx]; if (money < pack.price) return;
money -= pack.price; packDeck = packDeck.filter(p => p.id !== pack.id); shopPacks.splice(idx, 1);
const shopEl = document.querySelectorAll('#shop-list-packs .pack-card')[idx];
shopEl.classList.add('pack-opening-anim'); shopEl.style.left = "50vw"; shopEl.style.top = "50vh";
await sleep(1000); openPack(pack);
}
function openPack(pack) {
activePack = pack; packPicksLeft = pack.pick; packSelectionItems = [];
for (let i = 0; i < pack.show; i++) {
if (pack.type === 'fruit') { const p = planetDeck.shift(); packSelectionItems.push({ type: 'planet', ...p }); planetDeck.push(p); }
else { const pool = JOKER_POOL.filter(j => !inventory.find(inv => inv.id === j.id)); packSelectionItems.push({ type: 'joker', ...pool[Math.floor(Math.random()*pool.length)] }); }
}
updatePackSelectionUI();
}
function updatePackSelectionUI() {
const listEl = document.getElementById('pack-item-list'); listEl.innerHTML = '';
document.getElementById('pack-name-display').innerText = `${activePack.size} ${activePack.type.toUpperCase()} PACK`;
document.getElementById('pack-picks-display').innerText = `Picks Left: ${packPicksLeft}`;
document.getElementById('pack-overlay').style.display = 'flex';
packSelectionItems.forEach((item, i) => {
const div = document.createElement('div'); div.className = item.type === 'joker' ? 'joker-card' : 'planet-card'; div.dataset.idx = i;
div.innerHTML = `<span class="text-3xl">${item.emoji}</span><span class="text-[9px] font-black uppercase text-center mt-2">${item.name}</span>`;
div.onpointerdown = (e) => onEntityDown(e, 'pack-item-list', 'pack_selection');
listEl.appendChild(div);
});
updateSpacings('pack-item-list');
}
function closePack() { activePack = null; document.getElementById('pack-overlay').style.display = 'none'; renderShop(); }
function onEntityDown(e, containerId, source) {
if (isBusy) return;
dTarget = e.currentTarget; dStart = { x: e.clientX, y: e.clientY }; dActive = false;
const onMove = (me) => {
const dist = Math.hypot(me.clientX - dStart.x, me.clientY - dStart.y);
if (!dActive && dist > 8) {
dActive = true; dPlaceholder = dTarget.cloneNode(true); dPlaceholder.classList.add('placeholder');
dTarget.classList.add('dragging'); dTarget.parentNode.insertBefore(dPlaceholder, dTarget);
document.body.appendChild(dTarget);
}
if (dActive) {
dTarget.style.left = me.clientX + 'px'; dTarget.style.top = me.clientY + 'px';
const listId = (source === 'hand') ? 'hand-list' : 'joker-inventory-list';
const list = document.getElementById(listId);
const after = getAfterElement(list, me.clientX);
if (after) list.insertBefore(dPlaceholder, after); else list.appendChild(dPlaceholder);
updateSpacings(listId);
}
};
const onUp = (ue) => {
window.removeEventListener('pointermove', onMove); window.removeEventListener('pointerup', onUp);
if (!dActive) {
const idx = parseInt(dTarget.dataset.idx);
if (source === 'hand') toggleSelection(dTarget);
else if (source === 'shop_pack') purchasePack(idx);
else if (source === 'inventory') inspectEntity(idx, 'inventory');
else if (source === 'shop_joker') inspectEntity(idx, 'shop_joker');
else if (source === 'pack_selection') {
const item = packSelectionItems[idx];
if (item.type === 'planet') { handLevels[item.name]++; packPicksLeft--; if (packPicksLeft <= 0) closePack(); else { packSelectionItems.splice(idx, 1); updatePackSelectionUI(); } }
else inspectEntity(idx, 'pack_selection');
}
} else {
const invRect = document.getElementById('joker-inventory-list').getBoundingClientRect();
const droppedInInv = ue.clientY < invRect.bottom + 50 && ue.clientY > invRect.top - 50;
const idx = parseInt(dTarget.dataset.idx);
if (source === 'shop_joker' && droppedInInv && inventory.length < 5) {
const data = shopJokers[idx]; if (money >= data.price) { money -= data.price; inventory.push(data); shopJokers.splice(idx, 1); deselectEverything(); }
} else if (source === 'pack_selection' && droppedInInv && inventory.length < 5) {
const data = packSelectionItems[idx]; if (data.type === 'joker') { inventory.push(data); packPicksLeft--; if (packPicksLeft <= 0) closePack(); else { packSelectionItems.splice(idx, 1); updatePackSelectionUI(); } }
} else if (source === 'inventory' || source === 'hand') {
const listId = (source === 'hand') ? 'hand-list' : 'joker-inventory-list';
const list = document.getElementById(listId); list.replaceChild(dTarget, dPlaceholder);
dTarget.classList.remove('dragging'); dTarget.style.cssText = "";
if (source === 'inventory') inventory = [...list.querySelectorAll('.joker-card')].map(el => inventory[el.dataset.idx]);
if (source === 'hand') [...list.querySelectorAll('.card')].forEach((c, i) => c.dataset.visualIndex = i);
}
dTarget?.remove(); dPlaceholder?.remove();
}
dTarget = null; dPlaceholder = null; renderInventory(); renderShop(); updateUI();
};
window.addEventListener('pointermove', onMove); window.addEventListener('pointerup', onUp);
}
function getAfterElement(list, x) {
const cs = [...list.querySelectorAll('.card:not(.dragging):not(.placeholder), .joker-card:not(.dragging):not(.placeholder)')];
return cs.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const offset = x - (box.left + box.width / 2);
if (offset < 0 && offset > closest.offset) return { offset, element: child }; else return closest;
}, { offset: Number.NEGATIVE_INFINITY }).element;
}
function renderInventory() {
const cont = document.getElementById('joker-inventory-list'); cont.innerHTML = '';
inventory.forEach((j, i) => {
const div = document.createElement('div'); div.className = 'joker-card' + (inspectedIdx === i && inspectedSource === 'inventory' ? ' inspected' : ''); div.dataset.idx = i;
div.innerHTML = `<span class="text-3xl">${j.emoji}</span><span class="text-[9px] font-black uppercase text-center mt-2">${j.name}</span>`;
div.onpointerdown = (e) => onEntityDown(e, 'joker-inventory-list', 'inventory');
cont.appendChild(div);
});
updateSpacings('joker-inventory-list');
}
function inspectEntity(idx, source) {
let data = source === 'inventory' ? inventory[idx] : (source === 'shop_joker' ? shopJokers[idx] : packSelectionItems[idx]);
inspectedIdx = idx; inspectedSource = source;
document.getElementById('inspect-name').innerText = data.name;
document.getElementById('inspect-desc').innerText = data.desc || `Power up ${data.name}.`;
const sBtn = document.getElementById('sell-btn');
if (source === 'inventory') { const pr = Math.floor(data.price/2); sBtn.innerText = `Sell ($${pr})`; sBtn.onclick = () => { money += pr; inventory.splice(idx, 1); deselectEverything(); updateUI(); }; sBtn.classList.remove('hidden'); } else sBtn.classList.add('hidden');
document.getElementById('inspect-panel').style.display = 'block';
}
function deselectEverything() { inspectedIdx = -1; inspectedSource = null; document.getElementById('inspect-panel').style.display = 'none'; renderInventory(); renderShop(); }
function handleGlobalClick(e) { if (!e.target.closest('.joker-card') && !e.target.closest('.planet-card') && !e.target.closest('.pack-card') && !e.target.closest('#inspect-panel') && !e.target.closest('.info-btn')) deselectEverything(); }
function toggleSelection(el) { if (el.classList.contains('selected')) el.classList.remove('selected'); else if (document.querySelectorAll('#hand-list .selected').length < 5) el.classList.add('selected'); updateUI(); }
function applySort() { const list = document.getElementById('hand-list'); const items = [...list.querySelectorAll('.card:not(.dragging):not(.fly-away)')]; items.sort((a,b) => { const rA = parseInt(a.dataset.rankSort), rB = parseInt(b.dataset.rankSort), sA = parseInt(a.dataset.suitSort), sB = parseInt(b.dataset.suitSort); return sortMode === 'rank' ? (rA !== rB ? rB - rA : sB - sA) : (sA !== sB ? sB - sA : rB - rA); }); items.forEach((item, i) => item.dataset.visualIndex = i); updateSpacings('hand-list'); }
function toggleSort() { sortMode = (sortMode === 'rank' ? 'suit' : 'rank'); document.getElementById('sort-toggle-btn').innerText = `Sort By ${sortMode === 'rank' ? 'Suit' : 'Rank'}`; applySort(); }
function discardSelected() { if (isBusy || discards <= 0) return; const sel = [...document.querySelectorAll('#hand-list .selected')]; if (!sel.length) return; discards--; inventory.forEach(j => { if (j.trigger === 'discard_cash') money += 1; }); sel.forEach(c => { c.classList.add('fly-away'); setTimeout(()=>c.remove(), 600); }); setTimeout(drawToFull, 300); updateUI(); }
function showWin() { let jB = 0; inventory.forEach(j => { if (j.trigger === 'win') jB += 3; }); const base = 4, interest = Math.min(Math.floor(money / 5), 5); const totalGained = base + plays + jB + interest; money += totalGained; document.getElementById('win-bonus').innerText = `$${plays}`; document.getElementById('joker-gold-bonus').innerText = `$${jB}`; document.getElementById('interest-bonus').innerText = `$${interest}`; document.getElementById('win-total').innerText = `$${totalGained}`; document.getElementById('win-overlay').style.display = 'flex'; updateUI(); }
function goToShop() {
document.getElementById('win-overlay').style.display = 'none';
document.getElementById('shop-overlay').style.display = 'flex';
rerollShop(true);
}
function startNextRound() {
blindIndex++; if (blindIndex > 2) { blindIndex = 0; anteIndex++; }
if (anteIndex >= 8) { document.getElementById('victory-overlay').style.display = 'flex'; return; }
document.getElementById('shop-overlay').style.display = 'none'; document.getElementById('hand-list').innerHTML = '';
totalScore = 0; plays = 4; discards = 3; initDecks(); drawToFull(); updateUI();
}
function resetGame() { location.reload(); }
window.onload = () => { initDecks(); drawToFull(); renderInventory(); updateUI(); };
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment