🎰 Gerador de Horários Pagantes - PGSoft





return { total, wins, jackpot, jackpotSymbol: cand }; } /* ========= highlight + popup ========= */ function highlightWins(wins){ clearLines(); if(!wins.length) return; const cells = Array.from(boardEl.children); wins.forEach((w, idx) => { const points = w.line.map(i => { const c = cells[i]; c.classList.add('win'); c.classList.add('pulse'); return computeCenterPerc(c); }); drawPolyline(points); }); } /* show popup breakdown */ function showWinPopup(wins, jackpotObj){ // wins array and jackpotObj { jackpot:boolean, jackpotSymbol, totalBefore } // Build breakdown per line, sum; if jackpot apply x20 const modal = document.createElement('div'); modal.className = 'modal-backdrop'; const box = document.createElement('div'); box.className = 'modal'; const h = document.createElement('h3'); h.textContent = 'Ganho!'; box.appendChild(h); const breakdown = document.createElement('div'); breakdown.className = 'breakdown'; if(!wins.length){ const r = document.createElement('div'); r.className='row'; r.textContent = 'Nenhuma linha vencedora'; breakdown.appendChild(r); } else { wins.forEach((w,i)=>{ const row = document.createElement('div'); row.className='row'; const left = document.createElement('div'); left.textContent = `Linha ${i+1} — ${w.id}`; const right = document.createElement('div'); right.textContent = fmtMoney(w.pay); row.appendChild(left); row.appendChild(right); breakdown.appendChild(row); }); } box.appendChild(breakdown); // compute sums and jackpot multiplier let sum = wins.reduce((a,b)=>a + (b.pay||0), 0); let final = sum; if(jackpotObj && jackpotObj.jackpot){ final = Math.round(final * 20); const info = document.createElement('div'); info.style.marginTop='8px'; info.style.textAlign='center'; info.style.color='var(--accent)'; info.textContent = `Jackpot! Todos os símbolos combinam (${jackpotObj.jackpotSymbol}) — multiplicador x20 aplicado`; box.appendChild(info); } const totalDiv = document.createElement('div'); totalDiv.className = 'total'; totalDiv.textContent = `Total: ${fmtMoney(final)}`; box.appendChild(totalDiv); // close button const ok = document.createElement('button'); ok.className='btn primary'; ok.style.marginTop='12px'; ok.textContent='Fechar'; ok.addEventListener('click', ()=> { modal.remove(); }); box.style.display='flex'; box.style.flexDirection='column'; box.style.alignItems='stretch'; box.appendChild(ok); modal.appendChild(box); modalRoot.innerHTML = ''; modalRoot.appendChild(modal); } /* ========= SPIN FLOW (smooth, no flicker) ========= */ let spinToken = 0; function spinOnce(forced=false){ if(state.spinning) return; if(state.balance < state.bet && !forced){ showMarq('Saldo insuficiente'); return; } state.spinning = true; spinBtn.disabled = true; forceBtn.disabled = true; state.balance -= state.bet; state.lastWin = 0; render(); fxSpin(); const cells = boardEl.children; const ticksMax = 12; let tick = 0; const token = ++spinToken; function frame(){ if(token !== spinToken) return; tick++; for(let i=0;is.id==='moneybag'); state.grid[3]=coin; state.grid[4]=coin; state.grid[5]=coin; } else { for(let i=0;i spinOnce(false), 420); } /* ========= MASCOT: soltar "cartinha" ========= */ function animateMascotCard(){ const bRect = boardEl.getBoundingClientRect(); const mRect = document.getElementById('mascot').getBoundingClientRect(); const card = document.createElement('div'); card.style.position = 'fixed'; card.style.left = (mRect.left + mRect.width*0.5)+'px'; card.style.top = (mRect.top + mRect.height*0.6)+'px'; card.style.width='64px'; card.style.height='92px'; card.style.borderRadius='8px'; card.style.background='linear-gradient(180deg,#fff7eb,#fff1d8)'; card.style.boxShadow='0 16px 40px rgba(0,0,0,.45)'; card.style.display='flex'; card.style.alignItems='center'; card.style.justifyContent='center'; card.style.fontWeight='900'; card.style.color='#2b1400'; card.style.zIndex='9999'; card.textContent='🎴'; document.body.appendChild(card); const targetX = bRect.left + bRect.width * 0.5; const targetY = bRect.top + bRect.height * 0.34; const duration = 640; const start = performance.now(); const startX = mRect.left + mRect.width*0.5; const startY = mRect.top + mRect.height*0.6; function go(now){ const t = Math.min(1,(now-start)/duration); const e = 1 - (1-t)*(1-t); const curX = startX + (targetX - startX) * e; const curY = startY + (targetY - startY) * e - Math.sin(e*Math.PI)*40; card.style.left = (curX - 32)+'px'; card.style.top = (curY - 46)+'px'; card.style.transform = `rotate(${e*360}deg) scale(${1 + 0.06 * Math.sin(e*Math.PI*2)})`; if(t < 1) requestAnimationFrame(go); else { card.style.transition = 'transform .28s ease, opacity .28s ease'; card.style.transform = 'scale(0.2) translateY(-20px)'; card.style.opacity='0'; setTimeout(()=> card.remove(), 320); } } requestAnimationFrame(go); } /* ========= AUXILIARES ========= */ function showMarq(txt, t=1400){ marq.textContent = txt; marq.classList.add('show'); setTimeout(()=> marq.classList.remove('show'), t); } /* ========= CONTROLES ========= */ spinBtn.addEventListener('click', ()=>{ if(!audioCtx) ensureAudio(); if(state.music) startBG(); spinOnce(false); }); autoBtn.addEventListener('click', ()=>{ state.autoplay = !state.autoplay; autoBtn.textContent = state.autoplay ? 'Auto: ON' : 'Auto'; if(state.autoplay) spinOnce(false); }); betBtn.addEventListener('click', ()=>{ const opts=[2,5,10,20,50,100]; let idx = opts.indexOf(state.bet); idx=(idx+1)%opts.length; state.bet = opts[idx]; render(); }); musicBtn.addEventListener('click', ()=>{ state.music = !state.music; musicBtn.textContent = state.music? 'Música: ON' : 'Música: OFF'; if(state.music) startBG(); else stopBG(); }); resetBtn.addEventListener('click', ()=>{ state.balance=1000; state.bet=10; state.lastWin=0; state.autoplay=false; state.music=true; storage.set('ft_balance', state.balance); storage.set('ft_bet', state.bet); storage.set('ft_lastWin', state.lastWin); storage.set('ft_music', state.music); initBoard(); showMarq('Jogo reiniciado'); }); forceBtn.addEventListener('click', ()=>{ if(state.spinning) return; spinOnce(true); }); /* ========= INIT ========= */ function initBoard(){ boardEl.innerHTML = ''; for(let i=0;i { const el = document.createElement('div'); el.className='cell'; el.dataset.i=i; const img = document.createElement('img'); img.alt=''; img.draggable=false; el.appendChild(img); return el; })()); for(let i=0;i { clearTimeout(resizeTimer); resizeTimer = setTimeout(()=> { const anyWin = boardEl.querySelectorAll('.cell.win').length > 0; if(anyWin){ clearLines(); const { wins } = evaluateGrid(1); highlightWins(wins); } }, 120); }); /* ========= Créditos / Aviso ========= - Paytable values and images loaded from the page you indicated. - Verifique licenças/direitos antes de uso comercial. - Este arquivo é uma réplica inspirada; não redistribui assets proprietários sem autorização. */ ingClientRect(); const parent = boardEl.getBoundingClientRect(); const cx = ((rect.left + rect.width/2) - parent.left) / parent.width * 100; const cy = ((rect.top + rect.height/2) - parent.top) / parent.height * 100; return [cx, cy]; } function drawLine(points){ const ns = "http://www.w3.org/2000/svg"; const poly = document.createElementNS(ns, 'polyline'); poly.setAttribute('points', points.map(p=>p.join(',')).join(' ')); poly.setAttribute('fill','none'); poly.setAttribute('stroke','#ffd94d'); poly.setAttribute('stroke-width','1.8'); poly.setAttribute('stroke-linecap','round'); poly.setAttribute('stroke-linejoin','round'); poly.style.filter = 'drop-shadow(0 6px 12px rgba(255,200,60,0.12))'; svgEl.appendChild(poly); const len = poly.getTotalLength(); poly.style.strokeDasharray = len; poly.style.strokeDashoffset = len; requestAnimationFrame(()=> { poly.style.transition='stroke-dashoffset .48s ease-out'; poly.style.strokeDashoffset = '0'; }); return poly; } function evaluateGrid(mult=1){ const wins = []; let total = 0; for(const line of paylines){ const syms = line.map(i=>state.grid[i]); // anchor: primeiro não-wild let anchor = null; for(const s of syms) if(!s.wild){ anchor = s.id; break; } if(!anchor) anchor = 'tiger'; // checa compatibilidade const ok = syms.every(s => s.id === anchor || s.wild); if(ok){ const symObj = SYMBOLS.find(s => s.id === anchor); const pay = Math.round(symObj.pay * (state.bet/10) * mult); total += pay; wins.push({line, id: anchor, pay}); } } return {total, wins}; } function highlightWins(wins){ clearLines(); if(!wins.length) return; const cells = Array.from(boardEl.children); wins.forEach((w, idx) => { const pts = w.line.map(i => { const c = cells[i]; c.classList.add('win'); c.classList.add('pulse'); return computeCellCenterPerc(c); }); drawLine(pts); }); } /* ========== fluxo de spin (suave sem flicker) ========== */ let spinToken = 0; function spinOnce(forced=false){ if(state.spinning) return; if(state.balance < state.bet && !forced){ showMarq('Saldo insuficiente'); return; } state.spinning = true; spinBtn.disabled = true; forceBtn.disabled = true; state.balance -= state.bet; state.lastWin = 0; render(); fxSpin(); const cells = boardEl.children; const ticksMax = 12; let tick = 0; const token = ++spinToken; function frame(){ if(token !== spinToken) return; tick++; for(let i=0;is.id==='moneybag'); state.grid[3]=coin; state.grid[4]=coin; state.grid[5]=coin; } else { for(let i=0;i 0){ highlightWins(wins); animateMascotCard(); fxWin(); showMarq('Você ganhou ' + fmtMoney(total), 2200); } else { fxStop(); } state.spinning = false; spinBtn.disabled = false; forceBtn.disabled = false; if(state.autoplay) setTimeout(()=> spinOnce(false), 420); } /* ========== Mascot solta cartinha (animação) ========== */ function animateMascotCard(){ const boardRect = boardEl.getBoundingClientRect(); const mascotRect = document.getElementById('mascot').getBoundingClientRect(); const card = document.createElement('div'); card.style.position = 'fixed'; card.style.left = (mascotRect.left + mascotRect.width*0.5) + 'px'; card.style.top = (mascotRect.top + mascotRect.height*0.6) + 'px'; card.style.width = '64px'; card.style.height='92px'; card.style.borderRadius='8px'; card.style.background = 'linear-gradient(180deg,#fff7eb,#fff1d8)'; card.style.boxShadow='0 16px 40px rgba(0,0,0,.45)'; card.style.display='flex'; card.style.alignItems='center'; card.style.justifyContent='center'; card.style.fontWeight='900'; card.style.color='#2b1400'; card.style.zIndex=9999; card.textContent='🎴'; document.body.appendChild(card); const targetX = boardRect.left + boardRect.width * 0.5; const targetY = boardRect.top + boardRect.height * 0.34; const duration = 640; const start = performance.now(); const startX = mascotRect.left + mascotRect.width*0.5; const startY = mascotRect.top + mascotRect.height*0.6; function go(now){ const t = Math.min(1,(now-start)/duration); const e = 1 - (1-t)*(1-t); const curX = startX + (targetX - startX) * e; const curY = startY + (targetY - startY) * e - Math.sin(e*Math.PI)*40; card.style.left = (curX - 32)+'px'; card.style.top=(curY - 46)+'px'; card.style.transform = `rotate(${e*360}deg) scale(${1 + 0.06 * Math.sin(e*Math.PI*2)})`; if(t<1) requestAnimationFrame(go); else { card.style.transition='transform .28s ease, opacity .28s ease'; card.style.transform='scale(0.2) translateY(-20px)'; card.style.opacity='0'; setTimeout(()=> card.remove(),320); } } requestAnimationFrame(go); } /* ========== helpers ========== */ function showMarq(txt, t=1400){ marq.textContent = txt; marq.classList.add('show'); setTimeout(()=> marq.classList.remove('show'), t); } /* ========== controles ========== */ spinBtn.addEventListener('click', ()=>{ if(!audioCtx) ensureAudio(); if(state.music) startBG(); spinOnce(false); }); autoBtn.addEventListener('click', ()=>{ state.autoplay = !state.autoplay; autoBtn.textContent = state.autoplay? 'Auto: ON' : 'Auto'; if(state.autoplay) spinOnce(false); }); betBtn.addEventListener('click', ()=>{ const opts=[2,5,10,20,50,100]; let idx = opts.indexOf(state.bet); idx=(idx+1)%opts.length; state.bet = opts[idx]; render(); }); musicBtn.addEventListener('click', ()=>{ state.music = !state.music; musicBtn.textContent = state.music? 'Música: ON' : 'Música: OFF'; if(state.music) startBG(); else stopBG(); }); resetBtn.addEventListener('click', ()=>{ state.balance=1000; state.bet=10; state.lastWin=0; state.autoplay=false; state.music=true; storage.set('ft_balance', state.balance); storage.set('ft_bet', state.bet); storage.set('ft_lastWin', state.lastWin); storage.set('ft_music', state.music); initBoard(); showMarq('Jogo reiniciado'); }); forceBtn.addEventListener('click', ()=>{ if(state.spinning) return; spinOnce(true); }); /* ========== init ========= */ function initBoard(){ boardEl.innerHTML=''; for(let i=0;i { const anyWin = boardEl.querySelectorAll('.cell.win').length > 0; if(anyWin){ clearLines(); const {wins} = evaluateGrid(1); highlightWins(wins); } }); /* ========== créditos / aviso legal ========= */ /* Paytable values used (copiados do artigo indicado): - Laranja: 3× por linha - Fogos de artifício (sino): 5× por linha - Envelopes vermelhos: 8× por linha - Sacos de dinheiro: 10× por linha - Amuleto dourado: 25× por linha - Tigre (WILD): 250× por linha Imagens carregadas diretamente do artigo: https://tiger-fortune-online.com.br/wp-content/uploads/2024/06/fortune-tiger-Laranja.png https://tiger-fortune-online.com.br/wp-content/uploads/2024/06/Fortune-Tiger-Fogos-de-artificio.png https://tiger-fortune-online.com.br/wp-content/uploads/2024/06/fortune-tiger-Envelopes-vermelhos.png https://tiger-fortune-online.com.br/wp-content/uploads/2024/06/fortune-tiger-Sacos-de-dinheiro.png https://tiger-fortune-online.com.br/wp-content/uploads/2024/06/fortune-tiger-Amuletos-dourados.png https://tiger-fortune-online.com.br/wp-content/uploads/2024/06/fortune-tiger-wild.png AVISO: confirme licenças antes de uso comercial. Este é um clone inspirado; não contém ou distribui ativos proprietários da PG Soft (use apenas se autorizado). */ if(t<1) requestAnimationFrame(go); else { card.style.transition='transform .28s ease, opacity .28s ease'; card.style.transform='scale(0.2) translateY(-20px)'; card.style.opacity='0'; setTimeout(()=> card.remove(),320); } } requestAnimationFrame(go); } /* helper marquee */ function showMarq(txt,t=1400){ marq.textContent = txt; marq.classList.add('show'); setTimeout(()=> marq.classList.remove('show'), t); } /* ============================ CONTROLS ============================ */ spinBtn.addEventListener('click', ()=>{ if(!audioCtx) ensureAudio(); if(state.music) startBG(); spinOnce(false); }); autoBtn.addEventListener('click', ()=>{ state.autoplay = !state.autoplay; autoBtn.textContent = state.autoplay? 'Auto: ON':'Auto'; if(state.autoplay) spinOnce(false); }); betBtn.addEventListener('click', ()=>{ const opts=[2,5,10,20,50,100]; let idx=opts.indexOf(state.bet); idx=(idx+1)%opts.length; state.bet=opts[idx]; render(); }); musicBtn.addEventListener('click', ()=>{ state.music = !state.music; musicBtn.textContent = state.music? 'Música: ON':'Música: OFF'; if(state.music) startBG(); else stopBG(); }); resetBtn.addEventListener('click', ()=>{ state.balance=1000; state.bet=10; state.lastWin=0; state.autoplay=false; state.music=true; storage.set('ft_balance', state.balance); storage.set('ft_bet', state.bet); storage.set('ft_lastWin', state.lastWin); storage.set('ft_music', state.music); initBoard(); showMarq('Jogo reiniciado'); }); forceBtn.addEventListener('click', ()=>{ if(state.spinning) return; spinOnce(true); }); /* ============================ INIT ============================ */ function initBoard(){ boardEl.innerHTML=''; for(let i=0;i{ const anyWin=boardEl.querySelectorAll('.cell.win').length>0; if(anyWin){ clearLines(); const {wins} = evaluateGrid(1); highlightWins(wins); } }); by only replacing src when different) if(img.src !== sym.img) img.src = sym.img; img.alt = sym.id; cells[i].classList.remove('win','pulse'); } balanceEl.textContent = fmtMoney(state.balance); betEl.textContent = state.bet; lastWinEl.textContent = fmtMoney(state.lastWin); storage.set('ft_balance', state.balance); storage.set('ft_bet', state.bet); storage.set('ft_lastWin', state.lastWin); storage.set('ft_music', state.music); } /* helper: pick weighted */ function pick(){ return BAG[Math.floor(Math.random()*BAG.length)]; } /* =========================== Game logic: spin, evaluate, highlight =========================== */ function clearLines(){ svgEl.innerHTML = ''; } function evaluateGrid(mult=1){ const wins = []; let total = 0; for(const line of paylines){ const syms = line.map(i => state.grid[i]); // find anchor (first non-wild) let anchor = null; for(const s of syms) if(!s.wild){ anchor = s.id; break; } if(!anchor){ anchor = 'tiger'; // all wilds -> treat as tiger jackpot } // check compatibility (each is anchor or wild) const ok = syms.every(s => s.id === anchor || s.wild); if(ok){ const symObj = SYMBOLS.find(s => s.id === anchor); const pay = Math.round(symObj.pay * (state.bet/10) * mult); total += pay; wins.push({line, id:anchor, pay}); } } return { total, wins }; } /* draw connecting line between centers (points as [x,y] in %, relative to board) */ function drawLine(points){ const ns = "http://www.w3.org/2000/svg"; const poly = document.createElementNS(ns, 'polyline'); const pts = points.map(p => p.join(',')).join(' '); poly.setAttribute('points', pts); poly.setAttribute('fill','none'); poly.setAttribute('stroke', '#ffd94d'); poly.setAttribute('stroke-width', '1.8'); poly.setAttribute('stroke-linecap','round'); poly.setAttribute('stroke-linejoin','round'); poly.style.filter = 'drop-shadow(0 6px 12px rgba(255,200,60,0.12))'; svgEl.appendChild(poly); const len = poly.getTotalLength(); poly.style.strokeDasharray = len; poly.style.strokeDashoffset = len; // animate requestAnimationFrame(()=> { poly.style.transition='stroke-dashoffset .48s ease-out'; poly.style.strokeDashoffset = '0'; }); return poly; } function computeCellCenterPerc(el){ const rect = el.getBoundingClientRect(); const parent = boardEl.getBoundingClientRect(); const cx = ((rect.left + rect.width/2) - parent.left) / parent.width * 100; const cy = ((rect.top + rect.height/2) - parent.top) / parent.height * 100; return [cx, cy]; } function highlightWins(wins){ clearLines(); if(!wins.length) return; const cells = Array.from(boardEl.children); wins.forEach((w, idx)=>{ const pts = w.line.map(i => { const c = cells[i]; c.classList.add('win'); // small pulsing of image c.classList.add('pulse'); // compute center return computeCellCenterPerc(c); }); drawLine(pts); }); } /* SPIN flow: animated, smooth, no blank */ let spinAnimToken = 0; function spinOnce(forced=false){ if(state.spinning) return; if(state.balance < state.bet && !forced){ showMarq('Saldo insuficiente'); return; } state.spinning = true; spinBtn.disabled = true; forceBtn.disabled = true; state.balance -= state.bet; state.lastWin = 0; render(); fxSpin(); const cells = boardEl.children; const ticksMax = 12; let tick = 0; const token = ++spinAnimToken; // shuffle animation using requestAnimationFrame for fluidity function step(){ if(token !== spinAnimToken) return; tick++; // for each cell, set a temporary random symbol but do not set src to empty (prevents flash) for(let i=0;is.id==='coin'); state.grid[4] = SYMBOLS.find(s=>s.id==='coin'); state.grid[5] = SYMBOLS.find(s=>s.id==='coin'); // other cells random for(let i=0;i 0){ animateMascotCard(); } // award state.balance += total; render(); if(total > 0){ highlightWins(wins); fxWin(); showMarq('Você ganhou ' + fmtMoney(total)); } else { fxStop(); } // end spin state state.spinning = false; spinBtn.disabled = false; forceBtn.disabled = false; if(state.autoplay){ setTimeout(()=> spinOnce(false), 420); } } /* =========================== Mascot animation (tiger solta uma cartinha) =========================== */ function animateMascotCard(){ const boardRect = boardEl.getBoundingClientRect(); const mascotRect = document.getElementById('mascot').getBoundingClientRect(); // Create floating card element const card = document.createElement('div'); card.style.position = 'fixed'; card.style.left = (mascotRect.left + mascotRect.width*0.5) + 'px'; card.style.top = (mascotRect.top + mascotRect.height*0.6) + 'px'; card.style.width = '64px'; card.style.height = '92px'; card.style.borderRadius = '8px'; card.style.background = 'linear-gradient(180deg,#fff7eb,#fff1d8)'; card.style.boxShadow = '0 16px 40px rgba(0,0,0,.45)'; card.style.display = 'flex'; card.style.alignItems = 'center'; card.style.justifyContent = 'center'; card.style.fontWeight = '900'; card.style.color = '#2b1400'; card.style.zIndex = 9999; card.textContent = '🎴'; document.body.appendChild(card); // target center of board const targetX = boardRect.left + boardRect.width * 0.5; const targetY = boardRect.top + boardRect.height * 0.34; const duration = 640; const start = performance.now(); const startX = mascotRect.left + mascotRect.width*0.5; const startY = mascotRect.top + mascotRect.height*0.6; function animate(now){ const t = Math.min(1, (now - start) / duration); // ease out quad const e = 1 - (1 - t) * (1 - t); const curX = startX + (targetX - startX) * e; const curY = startY + (targetY - startY) * e - Math.sin(e * Math.PI) * 40; card.style.left = (curX - 32) + 'px'; card.style.top = (curY - 46) + 'px'; card.style.transform = `rotate(${e * 360}deg) scale(${1 + 0.06 * Math.sin(e*Math.PI*2)})`; if(t < 1) requestAnimationFrame(animate); else { // disappear with small burst card.style.transition = 'transform .28s ease, opacity .28s ease'; card.style.transform = 'scale(0.2) translateY(-20px)'; card.style.opacity = '0'; setTimeout(()=> card.remove(), 320); } } requestAnimationFrame(animate); } /* =========================== helpers: marquee and show =========================== */ function showMarq(txt, t=1500){ marq.textContent = txt; marq.classList.add('show'); setTimeout(()=> marq.classList.remove('show'), t); } /* =========================== controls hooks =========================== */ spinBtn.addEventListener('click', ()=> { // unlock audio if necessary if(!audioCtx) ensureAudio(); if(state.music) startBG(); spinOnce(false); }); autoBtn.addEventListener('click', ()=> { state.autoplay = !state.autoplay; autoBtn.textContent = state.autoplay ? 'Auto: ON' : 'Auto'; if(state.autoplay) spinOnce(false); }); betBtn.addEventListener('click', ()=> { const opts = [2,5,10,20,50,100]; let idx = opts.indexOf(state.bet); idx = (idx + 1) % opts.length; state.bet = opts[idx]; render(); }); musicBtn.addEventListener('click', ()=> { state.music = !state.music; musicBtn.textContent = state.music ? 'Música: ON' : 'Música: OFF'; if(state.music) startBG(); else stopBG(); }); resetBtn.addEventListener('click', ()=> { state.balance = 1000; state.bet = 10; state.lastWin = 0; state.autoplay = false; state.music = true; storage.set('ft_balance', state.balance); storage.set('ft_bet', state.bet); storage.set('ft_lastWin', state.lastWin); storage.set('ft_music', state.music); initBoard(); showMarq('Jogo reiniciado'); }); forceBtn.addEventListener('click', ()=> { // Force a win for test — produce center line triple if(state.spinning) return; // set non-center random then center triple for(let i=0;is.id==='coin'); state.grid[3] = coin; state.grid[4] = coin; state.grid[5] = coin; finalizeForced(); }); function finalizeForced(){ // immediate show with highlight render(); const { total, wins } = evaluateGrid(1); state.lastWin = total; state.balance += total; render(); highlightWins(wins); animateMascotCard(); fxWin(); showMarq('Teste: ganho ' + fmtMoney(total)); } /* =========================== init =========================== */ function init(){ // preload images Object.values(ASSETS).forEach(u => { const i = new Image(); i.src = u; }); // draw mascot small float let phase = 0; function floatMascot(){ phase += 0.03; const y = Math.sin(phase) * 4; const el = document.getElementById('mascot'); if(el) el.style.transform = `translateY(${y}px)`; requestAnimationFrame(floatMascot); } requestAnimationFrame(floatMascot); // init board initBoard(); // initial HUD update render(); if(state.music) { // don't auto-start sound until user interacts (handled by pointerdown) musicBtn.textContent = 'Música: ON'; } else musicBtn.textContent = 'Música: OFF'; // hide force button by default for normal users (but keep it) // forceBtn.style.display = 'none'; } init(); /* recompute lines on resize so polyline positions remain correct */ window.addEventListener('resize', ()=> { // if wins highlighted, recompute (re-evaluate and re-highlight) const anyWin = boardEl.querySelectorAll('.cell.win').length > 0; if(anyWin){ clearLines(); const { wins } = evaluateGrid(1); highlightWins(wins); } }); bj = SYMBOLS.find(s=>s.id===candidate); const pay = Math.round((symObj.pay) * (state.bet/10) * multi); if(pay>0){ total += pay; wins.push({ line, id:candidate, pay }); } } } return { total, wins }; } /* highlight wins + draw connecting lines */ function highlightWins(wins){ const cells = Array.from(board.children); clearLines(); if(!wins.length) return; wins.forEach((w, idx)=>{ // add class to each cell const coords = w.line.map(i => { const el = cells[i]; el.classList.add('win'); // compute center relative coordinates (0..100) const r = el.getBoundingClientRect(); const parentRect = board.getBoundingClientRect(); const cx = ((r.left + r.width/2) - parentRect.left) / parentRect.width * 100; const cy = ((r.top + r.height/2) - parentRect.top) / parentRect.height * 100; return [cx, cy]; }); // draw polyline through coords in SVG drawPolyline(coords, idx); }); } /* draw polyline on SVG overlay (coords in 0..100) */ function drawPolyline(points, idx){ const ns = "http://www.w3.org/2000/svg"; const poly = document.createElementNS(ns, 'polyline'); poly.setAttribute('points', points.map(p=>p.join(',')).join(' ')); poly.setAttribute('fill','none'); poly.setAttribute('stroke','#ffd94d'); poly.setAttribute('stroke-width', Math.max(0.8, 2 - idx*0.2)); poly.setAttribute('stroke-linecap','round'); poly.setAttribute('stroke-linejoin','round'); poly.setAttribute('stroke-opacity','0.95'); poly.style.filter = 'drop-shadow(0 6px 12px rgba(255,200,60,0.12))'; linesSVG.appendChild(poly); // animate stroke-dashoffset for a draw effect const length = poly.getTotalLength(); poly.style.strokeDasharray = length; poly.style.strokeDashoffset = length; poly.getBoundingClientRect(); // force layout poly.style.transition = 'stroke-dashoffset 550ms ease-out'; setTimeout(()=> poly.style.strokeDashoffset = '0', 50); } /* clear existing lines */ function clearLines(){ linesSVG.innerHTML = ''; } /* ========================= SPIN SEQUENCE ========================= */ function spinOnce(){ if(state.spinning) return; if(state.balance < state.bet){ showMarquee('Saldo insuficiente'); return; } state.spinning = true; state.balance -= state.bet; state.lastWin = 0; render(); playSpinSound(); // quick shuffle animation const cells = board.querySelectorAll('.cell img'); let ticks = 0; const anim = setInterval(()=>{ ticks++; for(let i=0;i 10){ clearInterval(anim); finalizeSpin(); } }, 48); function finalizeSpin(){ // set final result for(let i=0;i<9;i++) state.grid[i] = randomSymbol(); // small chance of "Tiger Luck" multiplier let multi = 1; if(Math.random() < 0.20){ multi = Math.floor(2 + Math.random()*8); // 2..9 showMarquee('Sorte do Tigre x' + multi); } // evaluate const { total, wins } = evaluateGrid(multi); state.lastWin = total; state.balance += total; render(); if(total > 0){ highlightWins(wins); playWinFanfare(); showMarquee('Você ganhou ' + total.toLocaleString()); } else { playStopSound(); } state.spinning = false; if(state.autoplay) setTimeout(()=> spinOnce(), 420); } } /* ========================= UI helpers ========================= */ function showMarquee(txt, t=1400){ marquee.textContent = txt; marquee.classList.add('show'); setTimeout(()=> marquee.classList.remove('show'), t); } /* ========================= Controls ========================= */ spinBtn.addEventListener('click', ()=> { // unlock audio if needed if(!audioCtx) ensureAudio(); if(state.musicOn && !bgOsc) startBackgroundMusic(); spinOnce(); }); autoBtn.addEventListener('click', ()=> { state.autoplay = !state.autoplay; autoBtn.textContent = state.autoplay ? 'AUTO ON' : 'AUTO'; if(state.autoplay) spinOnce(); }); betBtn.addEventListener('click', ()=> { const options = [2,5,10,20,50,100]; let idx = options.indexOf(state.bet); idx = (idx + 1) % options.length; state.bet = options[idx]; render(); }); musicBtn.addEventListener('click', ()=> { state.musicOn = !state.musicOn; musicBtn.textContent = state.musicOn ? 'Música: ON' : 'Música: OFF'; if(state.musicOn) startBackgroundMusic(); else stopBackgroundMusic(); }); resetBtn.addEventListener('click', ()=> { state.balance = 1000; state.bet = 10; state.lastWin = 0; state.autoplay=false; autoBtn.textContent = 'AUTO'; render(); clearLines(); }); /* ========================= INIT ========================= */ initBoard(); /* small accessibility: resize SVG overlay when layout changes */ window.addEventListener('resize', ()=> { // if there are highlighted wins, recompute lines positions const presentWins = board.querySelectorAll('.cell.win').length; if(presentWins){ // recompute by re-evaluating last grid (no multiplier known) - simple approach: re-evaluate and re-highlight const {wins} = evaluateGrid(1); clearLines(); highlightWins(wins); } }); (c === COLS-1){ // terminou tudo setTimeout(()=>{ document.body.classList.remove('spinning'); state.spinning = false; const {winAmount, wins} = evaluateWin(result); state.lastWin = winAmount; state.balance += winAmount; updateHUD(); highlightWins(wins); if(winAmount>0){ audio.win(); toast(`Você ganhou ${fmt(winAmount)}!`) } if(state.autoplay){ requestAnimationFrame(()=> setTimeout(spinOnce, state.turbo? 250 : 700)); } }, state.turbo? 60 : 250); } }, state.turbo ? (c*120) : (250 + c*420)); } } function evaluateWin(grid){ // SCATTER paga em qualquer lugar (3+) let scatterCount = 0; for(let r=0;rs.id==='S'); let scatterWin = 0, scatterPositions=[]; if(scatterCount>=3){ const idx = scatterCount===3?0: scatterCount===4?1:2; scatterWin = scatterSym.pay[idx] * state.betPerLine; // pago uma vez (fixo) // guarda posições para destaque for(let r=0;r{ // coleta símbolos da linha (com possibilidade de wild substituir) // Determina melhor símbolo “âncora” considerando wilds. // Pegamos primeiro símbolo não-wild na sequência inicial. let bestSymId = null; const seq = []; for(let col=0; cols.id===bestSymId); if(sym.scatter) return; // scatter não é avaliado por linha // comprimento da sequência (inclui wilds) let count = 0; const hitPositions = []; for(let col=0; col=3){ const idx = (count===3?0: count===4?1:2); const linePay = sym.pay[idx] * state.betPerLine; if(linePay>0){ wins.push({line: lineIdx+1, symbol: bestSymId, count, amount: linePay, positions: hitPositions}); lineWinSum += linePay; } } }); const totalWin = lineWinSum + scatterWin; if(scatterWin>0){ wins.push({line:'Scatter', symbol:'S', count:scatterCount, amount: scatterWin, positions: scatterPositions}); } return {winAmount: totalWin, wins}; } function highlightWins(wins){ // limpar el.grid.querySelectorAll('.cell').forEach(c=> c.classList.remove('win')); if(!wins || !wins.length) return; for(const w of wins){ for(const [r,c] of w.positions){ const cell = el.grid.children[c].children[r]; cell.classList.add('win'); // brilho passando cell.style.setProperty('--glow', 1); setTimeout(()=> cell.style.removeProperty('--glow'), 1200); } } } /* ========= CONTROLES ========= */ el.spinBtn.addEventListener('click', spinOnce); el.autoBtn.addEventListener('click', ()=>{ if(state.autoplay) return; state.autoplay = true; el.autoBtn.disabled = true; el.stopAutoBtn.disabled = false; spinOnce(); }); el.stopAutoBtn.addEventListener('click', ()=>{ state.autoplay = false; el.autoBtn.disabled = false; el.stopAutoBtn.disabled = true; toast('Autoplay parado.'); }); el.betMinus.addEventListener('click', ()=>{ if(state.spinning) return; state.betPerLine = Math.max(0.10, +(state.betPerLine - 0.10).toFixed(2)); updateHUD(); }); el.betPlus.addEventListener('click', ()=>{ if(state.spinning) return; state.betPerLine = Math.min(100, +(state.betPerLine + 0.10).toFixed(2)); updateHUD(); }); el.turboBtn.addEventListener('click', ()=>{ state.turbo = !state.turbo; el.turboBtn.textContent = `Turbo: ${state.turbo? 'ON':'OFF'}`; toast(state.turbo? 'Turbo ativado' : 'Turbo desativado'); }); el.addBalanceBtn.addEventListener('click', ()=>{ state.balance += 1000; updateHUD(); toast('Saldo DEMO +1000'); }); el.rulesBtn.addEventListener('click', ()=>{ const msg = [ 'REGRAS:', '• 10 linhas fixas, paga da esquerda para a direita.', '• 3+ símbolos iguais (Wild substitui) iniciando no 1º carretel.', '• Scatter 🎟️ paga em qualquer lugar (3+).', '• Aposta total = aposta por linha × 10.', '• Este é um jogo DEMO (sem dinheiro real).' ].join('\n'); alert(msg); }); el.clearWinsBtn.addEventListener('click', ()=> highlightWins([]) ); /* ========= INIT ========= */ createEmptyGrid(); updateHUD(); // draw inicial for(const cell of el.grid.querySelectorAll('.cell')){ putSymbol(cell, pickSymbol()); } // desbloquear áudio no 1º clique/touch window.addEventListener('pointerdown',()=> audio.ensure(), {once:true});