| class Match3Game { |
| constructor() { |
| this.boardRows = 7; |
| this.boardCols = 8; |
| this.numGemTypes = 6; |
| this.board = []; |
| this.selectedCell = null; |
| this.score = 0; |
| this.totalScore = 0; |
| this.moves = 30; |
| this.level = 1; |
| this.levelTargetScore = 1000; |
| this.isProcessing = false; |
| this.dragStartCell = null; |
| this.isDragging = false; |
| this.visualSwapActive = false; |
| this.visualSwapTarget = null; |
| this.mouseX = 0; |
| this.mouseY = 0; |
| this.dragOffsetX = 0; |
| this.dragOffsetY = 0; |
| this.targetOffsetX = 0; |
| this.targetOffsetY = 0; |
| this.currentOffsetX = 0; |
| this.currentOffsetY = 0; |
| this.animationFrameId = null; |
| this.buttonClickCooldown = false; |
| this.renderPending = false; |
| this.lastBoardState = null; |
| |
| |
| this.bestLevel = parseInt(localStorage.getItem('bestLevel') || '1'); |
| this.bestScore = parseInt(localStorage.getItem('bestScore') || '0'); |
| |
| |
| this.soundManager = new SoundManager(); |
| |
| this.init(); |
| } |
| |
| init() { |
| this.setupEventListeners(); |
| this.setupTheme(); |
| this.showHome(); |
| } |
| |
| setupTheme() { |
| |
| const savedTheme = localStorage.getItem('gameTheme') || 'light'; |
| this.applyTheme(savedTheme); |
| |
| |
| const themeSelect = document.getElementById('theme-select'); |
| if (themeSelect) { |
| themeSelect.value = savedTheme; |
| themeSelect.addEventListener('change', (e) => { |
| this.applyTheme(e.target.value); |
| }); |
| } |
| } |
| |
| applyTheme(theme) { |
| const body = document.body; |
| if (theme === 'dark') { |
| body.classList.add('dark-theme'); |
| body.classList.remove('light-theme'); |
| } else { |
| body.classList.add('light-theme'); |
| body.classList.remove('dark-theme'); |
| } |
| localStorage.setItem('gameTheme', theme); |
| } |
| |
| showHome() { |
| document.getElementById('home-page').classList.remove('hidden'); |
| const gameHeader = document.querySelector('.game-header'); |
| const gameBoard = document.getElementById('game-board'); |
| const gameControls = document.querySelector('.game-controls'); |
| |
| if (gameHeader) gameHeader.classList.add('hidden'); |
| if (gameBoard) gameBoard.classList.add('hidden'); |
| if (gameControls) { |
| gameControls.classList.add('hidden'); |
| gameControls.style.visibility = 'hidden'; |
| } |
| |
| |
| if (this.soundManager) { |
| this.soundManager.stopBackgroundMusic(); |
| } |
| |
| this.updateHomeStats(); |
| } |
| |
| showGame() { |
| document.getElementById('home-page').classList.add('hidden'); |
| const gameHeader = document.querySelector('.game-header'); |
| const gameBoard = document.getElementById('game-board'); |
| const gameControls = document.querySelector('.game-controls'); |
| |
| if (gameHeader) gameHeader.classList.remove('hidden'); |
| if (gameBoard) gameBoard.classList.remove('hidden'); |
| if (gameControls) { |
| gameControls.classList.remove('hidden'); |
| gameControls.style.visibility = 'visible'; |
| gameControls.style.display = 'flex'; |
| gameControls.style.opacity = '1'; |
| } |
| this.createBoard(); |
| this.renderBoard(); |
| this.updateUI(); |
| |
| |
| if (this.soundManager) { |
| this.soundManager.startBackgroundMusic(); |
| this.soundManager.updateMusicButton(); |
| } |
| } |
| |
| updateHomeStats() { |
| document.getElementById('best-level').textContent = this.bestLevel; |
| document.getElementById('best-score').textContent = this.bestScore.toLocaleString(); |
| } |
| |
| saveBestStats() { |
| let updated = false; |
| if (this.level > this.bestLevel) { |
| this.bestLevel = this.level; |
| localStorage.setItem('bestLevel', this.bestLevel.toString()); |
| updated = true; |
| } |
| if (this.totalScore > this.bestScore) { |
| this.bestScore = this.totalScore; |
| localStorage.setItem('bestScore', this.bestScore.toString()); |
| updated = true; |
| } |
| |
| if (updated && !document.querySelector('.game-header').classList.contains('hidden')) { |
| |
| } |
| } |
| |
| createBoard() { |
| |
| this.board = []; |
| for (let row = 0; row < this.boardRows; row++) { |
| this.board[row] = []; |
| for (let col = 0; col < this.boardCols; col++) { |
| this.board[row][col] = this.getRandomGem(); |
| } |
| } |
| |
| |
| while (this.findMatches().length > 0) { |
| for (let row = 0; row < this.boardRows; row++) { |
| for (let col = 0; col < this.boardCols; col++) { |
| if (this.isPartOfMatch(row, col)) { |
| this.board[row][col] = this.getRandomGem(); |
| } |
| } |
| } |
| } |
| } |
| |
| getRandomGem() { |
| return Math.floor(Math.random() * this.numGemTypes); |
| } |
| |
| isPartOfMatch(row, col) { |
| const gem = this.board[row][col]; |
| |
| |
| let horizontalCount = 1; |
| for (let c = col - 1; c >= 0 && this.board[row][c] === gem; c--) horizontalCount++; |
| for (let c = col + 1; c < this.boardCols && this.board[row][c] === gem; c++) horizontalCount++; |
| if (horizontalCount >= 3) return true; |
| |
| |
| let verticalCount = 1; |
| for (let r = row - 1; r >= 0 && this.board[r][col] === gem; r--) verticalCount++; |
| for (let r = row + 1; r < this.boardRows && this.board[r][col] === gem; r++) verticalCount++; |
| if (verticalCount >= 3) return true; |
| |
| return false; |
| } |
| |
| renderBoard(forceUpdate = false) { |
| |
| if (this.isProcessing && !forceUpdate) { |
| this.renderPending = true; |
| return; |
| } |
| |
| |
| if (this.isDragging && !forceUpdate) { |
| this.renderPending = true; |
| return; |
| } |
| |
| const boardElement = document.getElementById('game-board'); |
| if (!boardElement) return; |
| |
| |
| const existingCells = boardElement.querySelectorAll('.cell'); |
| const needsFullRender = existingCells.length !== this.boardRows * this.boardCols; |
| |
| if (needsFullRender) { |
| boardElement.innerHTML = ''; |
| } |
| |
| |
| const currentState = JSON.stringify(this.board); |
| const stateChanged = currentState !== this.lastBoardState; |
| |
| |
| if (!needsFullRender && !stateChanged && !forceUpdate) { |
| return; |
| } |
| |
| this.lastBoardState = currentState; |
| |
| |
| for (let row = 0; row < this.boardRows; row++) { |
| for (let col = 0; col < this.boardCols; col++) { |
| let cell = existingCells[row * this.boardCols + col]; |
| |
| if (!cell || needsFullRender) { |
| cell = document.createElement('div'); |
| cell.dataset.row = row; |
| cell.dataset.col = col; |
| |
| |
| cell.addEventListener('click', () => this.handleCellClick(row, col)); |
| |
| |
| cell.addEventListener('mousedown', (e) => this.handleDragStart(e, row, col)); |
| cell.addEventListener('mouseenter', (e) => this.handleDragOver(e, row, col)); |
| |
| |
| cell.addEventListener('selectstart', (e) => e.preventDefault()); |
| cell.addEventListener('dragstart', (e) => e.preventDefault()); |
| |
| boardElement.appendChild(cell); |
| } |
| |
| |
| const gemType = this.board[row][col]; |
| const expectedClass = `cell gem-${gemType}`; |
| const expectedEmoji = this.getGemEmoji(gemType); |
| |
| |
| if (gemType === -1) { |
| |
| if (!cell.classList.contains('empty-cell')) { |
| cell.classList.add('empty-cell'); |
| cell.style.display = 'none'; |
| } |
| continue; |
| } |
| |
| |
| if (cell.classList.contains('empty-cell')) { |
| cell.classList.remove('empty-cell'); |
| cell.style.display = ''; |
| cell.style.opacity = ''; |
| cell.style.visibility = ''; |
| } |
| |
| |
| const currentGemClass = Array.from(cell.classList).find(c => c.startsWith('gem-')); |
| const needsUpdate = needsFullRender || |
| currentGemClass !== `gem-${gemType}` || |
| cell.textContent !== expectedEmoji; |
| |
| if (needsUpdate) { |
| |
| const isMatched = cell.classList.contains('matched'); |
| const isMatchEnlarge = cell.classList.contains('match-enlarge'); |
| const hasActiveAnimation = isMatched || isMatchEnlarge; |
| |
| |
| if (!hasActiveAnimation) { |
| |
| cell.style.display = ''; |
| cell.style.opacity = ''; |
| cell.style.visibility = ''; |
| |
| cell.className = expectedClass; |
| cell.textContent = expectedEmoji; |
| |
| |
| if (!cell.style.transform || cell.style.transform === '') { |
| cell.style.transform = ''; |
| cell.style.transition = ''; |
| } |
| } |
| } |
| } |
| } |
| |
| |
| boardElement.addEventListener('mousemove', (e) => { |
| if (this.isDragging && this.dragStartCell) { |
| |
| this.mouseX = e.clientX; |
| this.mouseY = e.clientY; |
| |
| const startCell = document.querySelector( |
| `[data-row="${this.dragStartCell.row}"][data-col="${this.dragStartCell.col}"]` |
| ); |
| if (startCell) { |
| const rect = startCell.getBoundingClientRect(); |
| |
| this.targetOffsetX = (e.clientX - (rect.left + rect.width / 2)) * 0.6; |
| this.targetOffsetY = (e.clientY - (rect.top + rect.height / 2)) * 0.6; |
| this.dragOffsetX = this.targetOffsetX; |
| this.dragOffsetY = this.targetOffsetY; |
| } |
| |
| const cell = e.target.closest('.cell'); |
| if (cell && cell.dataset.row && cell.dataset.col) { |
| const row = parseInt(cell.dataset.row); |
| const col = parseInt(cell.dataset.col); |
| this.handleDragOver(e, row, col); |
| } |
| } |
| }); |
| |
| |
| document.addEventListener('mouseup', () => this.handleDragEnd()); |
| } |
| |
| getGemEmoji(gemType) { |
| if (gemType === -1) return ''; |
| const emojis = ['🔴', '🔵', '🟡', '🟢', '🟠', '🟣']; |
| return emojis[gemType] || '⚪'; |
| } |
| |
| handleDragStart(e, row, col) { |
| if (this.isProcessing) return; |
| if (this.moves <= 0) return; |
| |
| e.preventDefault(); |
| this.dragStartCell = { row, col }; |
| this.isDragging = true; |
| |
| |
| if (this.soundManager) { |
| this.soundManager.playSwipeSound(); |
| } |
| |
| const cell = document.querySelector(`[data-row="${row}"][data-col="${col}"]`); |
| if (cell) { |
| const rect = cell.getBoundingClientRect(); |
| const boardRect = document.getElementById('game-board').getBoundingClientRect(); |
| |
| |
| this.mouseX = e.clientX; |
| this.mouseY = e.clientY; |
| this.dragOffsetX = 0; |
| this.dragOffsetY = 0; |
| this.targetOffsetX = 0; |
| this.targetOffsetY = 0; |
| this.currentOffsetX = 0; |
| this.currentOffsetY = 0; |
| |
| cell.classList.add('selected', 'dragging'); |
| cell.style.cursor = 'grabbing'; |
| cell.style.transition = 'none'; |
| cell.style.willChange = 'transform'; |
| |
| |
| this.startDragTracking(); |
| } |
| |
| |
| document.getElementById('game-board').classList.add('is-dragging'); |
| } |
| |
| startDragTracking() { |
| if (this.animationFrameId) { |
| cancelAnimationFrame(this.animationFrameId); |
| } |
| |
| const updateDragPosition = () => { |
| if (!this.isDragging || !this.dragStartCell) return; |
| |
| const cell = document.querySelector( |
| `[data-row="${this.dragStartCell.row}"][data-col="${this.dragStartCell.col}"]` |
| ); |
| |
| if (cell) { |
| |
| const smoothingFactor = 0.15; |
| this.currentOffsetX += (this.targetOffsetX - this.currentOffsetX) * smoothingFactor; |
| this.currentOffsetY += (this.targetOffsetY - this.currentOffsetY) * smoothingFactor; |
| |
| |
| const scale = 1.12 + Math.min(Math.abs(this.currentOffsetX) + Math.abs(this.currentOffsetY), 30) / 200; |
| const rotationX = this.currentOffsetY * 0.1; |
| const rotationY = this.currentOffsetX * 0.1; |
| |
| cell.style.transform = `translate(${this.currentOffsetX}px, ${this.currentOffsetY}px) scale(${scale}) translateZ(20px) rotateX(${rotationX}deg) rotateY(${rotationY}deg)`; |
| } |
| |
| this.animationFrameId = requestAnimationFrame(updateDragPosition); |
| }; |
| |
| this.animationFrameId = requestAnimationFrame(updateDragPosition); |
| } |
| |
| handleDragOver(e, row, col) { |
| if (!this.isDragging || !this.dragStartCell) return; |
| if (this.isProcessing) return; |
| |
| e.preventDefault(); |
| |
| |
| if (row === this.dragStartCell.row && col === this.dragStartCell.col) return; |
| |
| |
| const isAdjacent = |
| (Math.abs(this.dragStartCell.row - row) === 1 && this.dragStartCell.col === col) || |
| (Math.abs(this.dragStartCell.col - col) === 1 && this.dragStartCell.row === row); |
| |
| const startCell = document.querySelector( |
| `[data-row="${this.dragStartCell.row}"][data-col="${this.dragStartCell.col}"]` |
| ); |
| |
| |
| if (this.visualSwapActive && this.visualSwapTarget) { |
| if (this.visualSwapTarget.row !== row || this.visualSwapTarget.col !== col) { |
| this.revertVisualSwap(); |
| } |
| } |
| |
| if (isAdjacent) { |
| const targetCell = document.querySelector(`[data-row="${row}"][data-col="${col}"]`); |
| |
| |
| if (!this.visualSwapActive || |
| !this.visualSwapTarget || |
| this.visualSwapTarget.row !== row || |
| this.visualSwapTarget.col !== col) { |
| |
| |
| if (this.visualSwapActive) { |
| this.revertVisualSwap(); |
| } |
| |
| |
| this.performVisualSwap(this.dragStartCell.row, this.dragStartCell.col, row, col); |
| this.visualSwapTarget = { row, col }; |
| } |
| |
| if (targetCell) { |
| targetCell.classList.add('drag-target'); |
| if (startCell) { |
| this.drawConnectionLine(startCell, targetCell); |
| } |
| } |
| } else { |
| |
| if (this.visualSwapActive) { |
| this.revertVisualSwap(); |
| } |
| |
| |
| document.querySelectorAll('.cell').forEach(c => { |
| c.classList.remove('drag-target'); |
| }); |
| |
| |
| this.removeConnectionLine(); |
| } |
| } |
| |
| performVisualSwap(row1, col1, row2, col2) { |
| const cell1 = document.querySelector(`[data-row="${row1}"][data-col="${col1}"]`); |
| const cell2 = document.querySelector(`[data-row="${row2}"][data-col="${col2}"]`); |
| |
| if (!cell1 || !cell2) return; |
| |
| |
| const rect1 = cell1.getBoundingClientRect(); |
| const rect2 = cell2.getBoundingClientRect(); |
| |
| |
| const deltaX = rect2.left - rect1.left; |
| const deltaY = rect2.top - rect1.top; |
| |
| |
| [this.board[row1][col1], this.board[row2][col2]] = |
| [this.board[row2][col2], this.board[row1][col1]]; |
| |
| |
| const gem1 = this.board[row1][col1]; |
| const gem2 = this.board[row2][col2]; |
| |
| |
| cell1.style.transition = 'transform 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94)'; |
| cell2.style.transition = 'transform 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94)'; |
| |
| |
| cell1.style.transform = `translate(${deltaX}px, ${deltaY}px) scale(1.08) translateZ(15px)`; |
| cell2.style.transform = `translate(${-deltaX}px, ${-deltaY}px) scale(1.08) translateZ(15px)`; |
| |
| |
| setTimeout(() => { |
| cell1.className = `cell gem-${gem1}`; |
| cell1.textContent = this.getGemEmoji(gem1); |
| cell1.classList.add('selected', 'dragging'); |
| |
| cell2.className = `cell gem-${gem2}`; |
| cell2.textContent = this.getGemEmoji(gem2); |
| cell2.classList.add('drag-target'); |
| }, 175); |
| |
| |
| setTimeout(() => { |
| cell1.style.transform = ''; |
| cell2.style.transform = ''; |
| }, 350); |
| |
| this.visualSwapActive = true; |
| } |
| |
| revertVisualSwap() { |
| if (!this.visualSwapActive || !this.dragStartCell || !this.visualSwapTarget) return; |
| |
| const cell1 = document.querySelector( |
| `[data-row="${this.dragStartCell.row}"][data-col="${this.dragStartCell.col}"]` |
| ); |
| const cell2 = document.querySelector( |
| `[data-row="${this.visualSwapTarget.row}"][data-col="${this.visualSwapTarget.col}"]` |
| ); |
| |
| if (cell1 && cell2) { |
| |
| cell1.style.transition = 'transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)'; |
| cell2.style.transition = 'transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)'; |
| |
| |
| cell1.style.transform = 'translate(0, 0) scale(1.1) translateZ(15px)'; |
| cell2.style.transform = 'translate(0, 0) scale(1.1) translateZ(15px)'; |
| |
| |
| setTimeout(() => { |
| [this.board[this.dragStartCell.row][this.dragStartCell.col], |
| this.board[this.visualSwapTarget.row][this.visualSwapTarget.col]] = |
| [this.board[this.visualSwapTarget.row][this.visualSwapTarget.col], |
| this.board[this.dragStartCell.row][this.dragStartCell.col]]; |
| |
| |
| const gem1 = this.board[this.dragStartCell.row][this.dragStartCell.col]; |
| const gem2 = this.board[this.visualSwapTarget.row][this.visualSwapTarget.col]; |
| |
| cell1.className = `cell gem-${gem1}`; |
| cell1.textContent = this.getGemEmoji(gem1); |
| cell1.classList.add('selected', 'dragging'); |
| cell1.style.transform = ''; |
| |
| cell2.className = `cell gem-${gem2}`; |
| cell2.textContent = this.getGemEmoji(gem2); |
| cell2.style.transform = ''; |
| |
| |
| requestAnimationFrame(() => { |
| this.renderBoard(); |
| }); |
| }, 300); |
| } |
| |
| this.visualSwapActive = false; |
| this.visualSwapTarget = null; |
| } |
| |
| drawConnectionLine(startCell, targetCell) { |
| |
| this.removeConnectionLine(); |
| |
| const board = document.getElementById('game-board'); |
| const startRect = startCell.getBoundingClientRect(); |
| const targetRect = targetCell.getBoundingClientRect(); |
| const boardRect = board.getBoundingClientRect(); |
| |
| |
| const startX = startRect.left + startRect.width / 2 - boardRect.left; |
| const startY = startRect.top + startRect.height / 2 - boardRect.top; |
| const targetX = targetRect.left + targetRect.width / 2 - boardRect.left; |
| const targetY = targetRect.top + targetRect.height / 2 - boardRect.top; |
| |
| |
| const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); |
| svg.setAttribute('class', 'drag-connection'); |
| svg.style.position = 'absolute'; |
| svg.style.left = '0'; |
| svg.style.top = '0'; |
| svg.style.width = '100%'; |
| svg.style.height = '100%'; |
| svg.style.pointerEvents = 'none'; |
| |
| const line = document.createElementNS('http://www.w3.org/2000/svg', 'line'); |
| line.setAttribute('x1', startX); |
| line.setAttribute('y1', startY); |
| line.setAttribute('x2', targetX); |
| line.setAttribute('y2', targetY); |
| line.setAttribute('stroke', 'rgba(100, 150, 255, 0.9)'); |
| line.setAttribute('stroke-width', '5'); |
| line.setAttribute('stroke-dasharray', '8, 4'); |
| line.setAttribute('marker-end', 'url(#arrowhead)'); |
| |
| |
| const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs'); |
| const marker = document.createElementNS('http://www.w3.org/2000/svg', 'marker'); |
| marker.setAttribute('id', 'arrowhead'); |
| marker.setAttribute('markerWidth', '10'); |
| marker.setAttribute('markerHeight', '10'); |
| marker.setAttribute('refX', '9'); |
| marker.setAttribute('refY', '3'); |
| marker.setAttribute('orient', 'auto'); |
| |
| const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon'); |
| polygon.setAttribute('points', '0 0, 10 3, 0 6'); |
| polygon.setAttribute('fill', 'rgba(100, 150, 255, 0.9)'); |
| |
| marker.appendChild(polygon); |
| defs.appendChild(marker); |
| svg.appendChild(defs); |
| svg.appendChild(line); |
| |
| board.style.position = 'relative'; |
| board.appendChild(svg); |
| } |
| |
| removeConnectionLine() { |
| const existingLine = document.querySelector('.drag-connection'); |
| if (existingLine) { |
| existingLine.remove(); |
| } |
| } |
| |
| handleDragEnd() { |
| if (!this.isDragging || !this.dragStartCell) return; |
| |
| |
| if (this.visualSwapActive && this.visualSwapTarget) { |
| |
| |
| const matches = this.findMatches(); |
| |
| if (matches.length > 0) { |
| |
| this.selectedCell = null; |
| this.updateCellSelection(); |
| if (this.moves > 0) this.moves--; |
| |
| |
| this.animateMatchedCells(matches).then(() => { |
| this.processMatches(); |
| }); |
| } else { |
| |
| this.revertVisualSwap(); |
| |
| } |
| |
| |
| this.visualSwapActive = false; |
| this.visualSwapTarget = null; |
| } else { |
| |
| const targetCell = document.querySelector('.drag-target'); |
| if (targetCell) { |
| const targetRow = parseInt(targetCell.dataset.row); |
| const targetCol = parseInt(targetCell.dataset.col); |
| |
| this.swapCells( |
| this.dragStartCell.row, |
| this.dragStartCell.col, |
| targetRow, |
| targetCol |
| ); |
| } else { |
| |
| if (this.visualSwapActive) { |
| this.revertVisualSwap(); |
| } |
| } |
| } |
| |
| |
| this.removeConnectionLine(); |
| |
| |
| this.isDragging = false; |
| const dragStart = this.dragStartCell; |
| this.dragStartCell = null; |
| |
| |
| if (this.animationFrameId) { |
| cancelAnimationFrame(this.animationFrameId); |
| this.animationFrameId = null; |
| } |
| |
| |
| requestAnimationFrame(() => { |
| document.querySelectorAll('.cell').forEach(c => { |
| |
| if (!c.classList.contains('matched') && !c.classList.contains('match-enlarge')) { |
| c.style.transition = 'transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94), opacity 0.2s ease'; |
| c.style.transform = ''; |
| c.style.willChange = ''; |
| c.classList.remove('selected', 'drag-target', 'dragging'); |
| c.style.cursor = ''; |
| |
| |
| setTimeout(() => { |
| if (!c.classList.contains('matched')) { |
| c.style.transition = ''; |
| } |
| }, 300); |
| } |
| }); |
| }); |
| |
| |
| this.currentOffsetX = 0; |
| this.currentOffsetY = 0; |
| this.targetOffsetX = 0; |
| this.targetOffsetY = 0; |
| |
| |
| document.getElementById('game-board').classList.remove('is-dragging'); |
| } |
| |
| handleCellClick(row, col) { |
| |
| |
| if (this.isProcessing) return; |
| if (this.isDragging) return; |
| |
| |
| this.selectedCell = { row, col }; |
| this.updateCellSelection(); |
| } |
| |
| updateCellSelection() { |
| const cells = document.querySelectorAll('.cell'); |
| cells.forEach(cell => cell.classList.remove('selected')); |
| |
| if (this.selectedCell) { |
| const cell = document.querySelector( |
| `[data-row="${this.selectedCell.row}"][data-col="${this.selectedCell.col}"]` |
| ); |
| if (cell) cell.classList.add('selected'); |
| } |
| } |
| |
| swapCells(row1, col1, row2, col2) { |
| |
| |
| if (!this.visualSwapActive) { |
| |
| [this.board[row1][col1], this.board[row2][col2]] = |
| [this.board[row2][col2], this.board[row1][col1]]; |
| } |
| |
| |
| const matches = this.findMatches(); |
| |
| if (matches.length > 0) { |
| |
| this.selectedCell = null; |
| this.updateCellSelection(); |
| if (this.moves > 0) this.moves--; |
| |
| |
| if (this.soundManager) { |
| this.soundManager.playMatchSound(matches.length); |
| } |
| |
| |
| this.animateMatchedCells(matches).then(() => { |
| this.processMatches(); |
| }); |
| } else { |
| |
| if (this.visualSwapActive) { |
| |
| [this.board[row1][col1], this.board[row2][col2]] = |
| [this.board[row2][col2], this.board[row1][col1]]; |
| } else { |
| |
| [this.board[row1][col1], this.board[row2][col2]] = |
| [this.board[row2][col2], this.board[row1][col1]]; |
| } |
| |
| |
| } |
| } |
| |
| async animateMatchedCells(matches) { |
| |
| |
| await this.sleep(0); |
| } |
| |
| updateCellsAfterMatch() { |
| |
| |
| } |
| |
| findMatches() { |
| const matches = []; |
| const visited = new Set(); |
| |
| |
| for (let row = 0; row < this.boardRows; row++) { |
| let count = 1; |
| let currentGem = this.board[row][0]; |
| |
| for (let col = 1; col < this.boardCols; col++) { |
| if (this.board[row][col] === currentGem) { |
| count++; |
| } else { |
| if (count >= 3) { |
| for (let c = col - count; c < col; c++) { |
| const key = `${row},${c}`; |
| if (!visited.has(key)) { |
| matches.push({ row, col: c }); |
| visited.add(key); |
| } |
| } |
| } |
| count = 1; |
| currentGem = this.board[row][col]; |
| } |
| } |
| |
| |
| if (count >= 3) { |
| for (let c = this.boardCols - count; c < this.boardCols; c++) { |
| const key = `${row},${c}`; |
| if (!visited.has(key)) { |
| matches.push({ row, col: c }); |
| visited.add(key); |
| } |
| } |
| } |
| } |
| |
| |
| for (let col = 0; col < this.boardCols; col++) { |
| let count = 1; |
| let currentGem = this.board[0][col]; |
| |
| for (let row = 1; row < this.boardRows; row++) { |
| if (this.board[row][col] === currentGem) { |
| count++; |
| } else { |
| if (count >= 3) { |
| for (let r = row - count; r < row; r++) { |
| const key = `${r},${col}`; |
| if (!visited.has(key)) { |
| matches.push({ row: r, col }); |
| visited.add(key); |
| } |
| } |
| } |
| count = 1; |
| currentGem = this.board[row][col]; |
| } |
| } |
| |
| |
| if (count >= 3) { |
| for (let r = this.boardRows - count; r < this.boardRows; r++) { |
| const key = `${r},${col}`; |
| if (!visited.has(key)) { |
| matches.push({ row: r, col }); |
| visited.add(key); |
| } |
| } |
| } |
| } |
| |
| return matches; |
| } |
| |
| async processMatches() { |
| this.isProcessing = true; |
| |
| while (true) { |
| const matches = this.findMatches(); |
| |
| if (matches.length === 0) { |
| break; |
| } |
| |
| |
| this.score += matches.length * 10; |
| this.updateUI(); |
| |
| |
| matches.forEach(match => { |
| const cell = document.querySelector( |
| `[data-row="${match.row}"][data-col="${match.col}"]` |
| ); |
| if (cell) { |
| cell.classList.add('matched'); |
| |
| this.createExplosionParticles(cell); |
| } |
| }); |
| |
| |
| await this.sleep(50); |
| |
| |
| document.querySelectorAll('.explosion-particle').forEach(particle => { |
| particle.remove(); |
| }); |
| |
| |
| matches.forEach(match => { |
| this.board[match.row][match.col] = -1; |
| |
| |
| |
| }); |
| |
| |
| await this.applyGravity(); |
| |
| |
| this.fillEmptySpaces(); |
| |
| |
| this.renderBoard(true); |
| |
| |
| await this.sleep(100); |
| } |
| |
| this.isProcessing = false; |
| |
| |
| if (this.renderPending) { |
| this.renderPending = false; |
| this.renderBoard(true); |
| } |
| |
| this.checkGameOver(); |
| } |
| |
| async applyGravity() { |
| for (let col = 0; col < this.boardCols; col++) { |
| let writeIndex = this.boardRows - 1; |
| |
| for (let row = this.boardRows - 1; row >= 0; row--) { |
| if (this.board[row][col] !== -1) { |
| if (writeIndex !== row) { |
| this.board[writeIndex][col] = this.board[row][col]; |
| this.board[row][col] = -1; |
| } |
| writeIndex--; |
| } |
| } |
| } |
| } |
| |
| fillEmptySpaces() { |
| for (let row = 0; row < this.boardRows; row++) { |
| for (let col = 0; col < this.boardCols; col++) { |
| if (this.board[row][col] === -1) { |
| this.board[row][col] = this.getRandomGem(); |
| } |
| } |
| } |
| } |
| |
| checkGameOver() { |
| |
| if (this.score >= this.levelTargetScore) { |
| this.completeLevel(); |
| return; |
| } |
| |
| |
| if (this.moves <= 0 && this.score < this.levelTargetScore) { |
| this.gameOver(); |
| } |
| } |
| |
| gameOver() { |
| |
| this.isProcessing = true; |
| |
| |
| this.saveBestStats(); |
| |
| |
| if (this.soundManager) { |
| this.soundManager.playGameOverSound(); |
| } |
| |
| |
| document.getElementById('final-level').textContent = this.level; |
| document.getElementById('final-score').textContent = this.totalScore.toLocaleString(); |
| document.getElementById('game-over').classList.remove('hidden'); |
| } |
| |
| retryLevel() { |
| |
| document.getElementById('game-over').classList.add('hidden'); |
| |
| |
| this.score = 0; |
| |
| |
| this.moves = this.getLevelMoves(this.level); |
| |
| |
| this.selectedCell = null; |
| this.isProcessing = false; |
| |
| |
| this.createBoard(); |
| this.renderBoard(); |
| this.updateUI(); |
| } |
| |
| completeLevel() { |
| |
| this.isProcessing = true; |
| |
| |
| this.totalScore += this.score; |
| |
| |
| if (this.soundManager) { |
| this.soundManager.playLevelCompleteSound(); |
| } |
| |
| |
| document.getElementById('completed-level').textContent = this.level; |
| document.getElementById('level-score').textContent = this.score; |
| document.getElementById('total-score').textContent = this.totalScore; |
| document.getElementById('level-complete').classList.remove('hidden'); |
| } |
| |
| continueToNextLevel() { |
| |
| document.getElementById('level-complete').classList.add('hidden'); |
| |
| |
| this.level++; |
| |
| |
| this.saveBestStats(); |
| |
| |
| this.levelTargetScore = this.level * 1000; |
| this.moves = this.getLevelMoves(this.level); |
| |
| |
| this.score = 0; |
| |
| |
| this.createBoard(); |
| this.renderBoard(); |
| this.updateUI(); |
| |
| |
| this.isProcessing = false; |
| } |
| |
| goToHome() { |
| |
| this.saveBestStats(); |
| |
| |
| document.getElementById('level-complete').classList.add('hidden'); |
| document.getElementById('game-over').classList.add('hidden'); |
| |
| |
| this.showHome(); |
| } |
| |
| endGame() { |
| |
| this.goToHome(); |
| } |
| |
| getLevelMoves(level) { |
| |
| return level === 1 ? 30 : 25 + (level * 2); |
| } |
| |
| updateUI() { |
| document.getElementById('score').textContent = this.score; |
| document.getElementById('moves').textContent = Math.max(0, this.moves); |
| document.getElementById('level').textContent = this.level; |
| document.getElementById('target-score').textContent = this.levelTargetScore.toLocaleString(); |
| |
| |
| const levelMoves = this.getLevelMoves(this.level); |
| document.getElementById('level-moves').textContent = levelMoves; |
| |
| |
| const progress = Math.min(100, (this.score / this.levelTargetScore) * 100); |
| |
| } |
| |
| setupEventListeners() { |
| |
| const preventRapidClicks = (callback) => { |
| return (e) => { |
| if (this.buttonClickCooldown) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| return; |
| } |
| |
| this.buttonClickCooldown = true; |
| const button = e.currentTarget; |
| |
| |
| if (this.soundManager) { |
| this.soundManager.playClickSound(); |
| } |
| |
| |
| button.style.transform = 'scale(0.95)'; |
| setTimeout(() => { |
| button.style.transform = ''; |
| }, 150); |
| |
| |
| callback(); |
| |
| |
| setTimeout(() => { |
| this.buttonClickCooldown = false; |
| }, 300); |
| }; |
| }; |
| |
| document.getElementById('start-game-btn').addEventListener('click', preventRapidClicks(() => { |
| this.startNewGame(); |
| })); |
| |
| document.getElementById('home-btn').addEventListener('click', preventRapidClicks(() => { |
| this.goToHome(); |
| })); |
| |
| document.getElementById('reset-btn').addEventListener('click', preventRapidClicks(() => { |
| this.resetGame(); |
| })); |
| |
| document.getElementById('retry-btn').addEventListener('click', preventRapidClicks(() => { |
| this.retryLevel(); |
| })); |
| |
| document.getElementById('quit-game-btn').addEventListener('click', preventRapidClicks(() => { |
| this.goToHome(); |
| })); |
| |
| document.getElementById('hint-btn').addEventListener('click', preventRapidClicks(() => { |
| this.showHint(); |
| })); |
| |
| document.getElementById('continue-btn').addEventListener('click', preventRapidClicks(() => { |
| this.continueToNextLevel(); |
| })); |
| |
| document.getElementById('quit-btn').addEventListener('click', preventRapidClicks(() => { |
| this.goToHome(); |
| })); |
| |
| |
| const musicToggleBtn = document.getElementById('music-toggle-btn'); |
| if (musicToggleBtn) { |
| musicToggleBtn.addEventListener('click', (e) => { |
| e.preventDefault(); |
| e.stopPropagation(); |
| if (this.soundManager) { |
| this.soundManager.toggleMusic(); |
| this.soundManager.playClickSound(); |
| } |
| }); |
| } |
| } |
| |
| startNewGame() { |
| this.score = 0; |
| this.totalScore = 0; |
| this.moves = 30; |
| this.level = 1; |
| this.levelTargetScore = 1000; |
| this.selectedCell = null; |
| this.isProcessing = false; |
| this.showGame(); |
| } |
| |
| resetGame() { |
| |
| this.score = 0; |
| this.totalScore = 0; |
| this.moves = 30; |
| this.level = 1; |
| this.levelTargetScore = 1000; |
| this.selectedCell = null; |
| this.isProcessing = false; |
| document.getElementById('game-over').classList.add('hidden'); |
| document.getElementById('level-complete').classList.add('hidden'); |
| this.createBoard(); |
| this.renderBoard(); |
| this.updateUI(); |
| } |
| |
| showHint() { |
| |
| for (let row = 0; row < this.boardRows - 1; row++) { |
| for (let col = 0; col < this.boardCols; col++) { |
| |
| [this.board[row][col], this.board[row + 1][col]] = |
| [this.board[row + 1][col], this.board[row][col]]; |
| |
| if (this.findMatches().length > 0) { |
| |
| [this.board[row][col], this.board[row + 1][col]] = |
| [this.board[row + 1][col], this.board[row][col]]; |
| |
| const cell1 = document.querySelector(`[data-row="${row}"][data-col="${col}"]`); |
| const cell2 = document.querySelector(`[data-row="${row + 1}"][data-col="${col}"]`); |
| |
| if (cell1 && cell2) { |
| cell1.style.boxShadow = '0 0 0 4px #00ff00'; |
| cell2.style.boxShadow = '0 0 0 4px #00ff00'; |
| |
| setTimeout(() => { |
| cell1.style.boxShadow = ''; |
| cell2.style.boxShadow = ''; |
| }, 2000); |
| } |
| return; |
| } |
| |
| [this.board[row][col], this.board[row + 1][col]] = |
| [this.board[row + 1][col], this.board[row][col]]; |
| } |
| } |
| |
| for (let row = 0; row < this.boardRows; row++) { |
| for (let col = 0; col < this.boardCols - 1; col++) { |
| |
| [this.board[row][col], this.board[row][col + 1]] = |
| [this.board[row][col + 1], this.board[row][col]]; |
| |
| if (this.findMatches().length > 0) { |
| |
| [this.board[row][col], this.board[row][col + 1]] = |
| [this.board[row][col + 1], this.board[row][col]]; |
| |
| const cell1 = document.querySelector(`[data-row="${row}"][data-col="${col}"]`); |
| const cell2 = document.querySelector(`[data-row="${row}"][data-col="${col + 1}"]`); |
| |
| if (cell1 && cell2) { |
| cell1.style.boxShadow = '0 0 0 4px #00ff00'; |
| cell2.style.boxShadow = '0 0 0 4px #00ff00'; |
| |
| setTimeout(() => { |
| cell1.style.boxShadow = ''; |
| cell2.style.boxShadow = ''; |
| }, 2000); |
| } |
| return; |
| } |
| |
| [this.board[row][col], this.board[row][col + 1]] = |
| [this.board[row][col + 1], this.board[row][col]]; |
| } |
| } |
| |
| alert('No valid moves found! Shuffling board...'); |
| this.createBoard(); |
| this.renderBoard(); |
| } |
| |
| createExplosionParticles(cell) { |
| const particleEmojis = ['✨', '⭐', '💫', '🌟', '⚡', '💥']; |
| const numParticles = 4 + Math.floor(Math.random() * 3); |
| |
| for (let i = 0; i < numParticles; i++) { |
| const particle = document.createElement('div'); |
| particle.className = 'explosion-particle'; |
| particle.textContent = particleEmojis[Math.floor(Math.random() * particleEmojis.length)]; |
| |
| |
| const angle = (Math.PI * 2 * i) / numParticles + (Math.random() - 0.5) * 0.8; |
| const distance = 20 + Math.random() * 15; |
| const curveAmount = 7 + Math.random() * 10; |
| |
| |
| const x1 = Math.cos(angle) * (distance * 0.3) + (Math.random() - 0.5) * curveAmount; |
| const y1 = Math.sin(angle) * (distance * 0.3) + (Math.random() - 0.5) * curveAmount; |
| |
| const x2 = Math.cos(angle) * (distance * 0.6) + (Math.random() - 0.5) * curveAmount * 1.5; |
| const y2 = Math.sin(angle) * (distance * 0.6) + (Math.random() - 0.5) * curveAmount * 1.5; |
| |
| const x3 = Math.cos(angle) * distance + (Math.random() - 0.5) * curveAmount * 2; |
| const y3 = Math.sin(angle) * distance + (Math.random() - 0.5) * curveAmount * 2; |
| |
| |
| const rotation = (Math.random() > 0.5 ? 1 : -1) * (720 + Math.random() * 720); |
| |
| |
| const scale = 0.05 + Math.random() * 0.03; |
| |
| |
| particle.style.setProperty('--particle-x1', `${x1}px`); |
| particle.style.setProperty('--particle-y1', `${y1}px`); |
| particle.style.setProperty('--particle-x2', `${x2}px`); |
| particle.style.setProperty('--particle-y2', `${y2}px`); |
| particle.style.setProperty('--particle-x3', `${x3}px`); |
| particle.style.setProperty('--particle-y3', `${y3}px`); |
| particle.style.setProperty('--particle-rotate', `${rotation}deg`); |
| particle.style.setProperty('--particle-scale', scale.toString()); |
| |
| |
| particle.style.animationDelay = `${Math.random() * 0.2}s`; |
| |
| cell.appendChild(particle); |
| } |
| } |
| |
| sleep(ms) { |
| return new Promise(resolve => setTimeout(resolve, ms)); |
| } |
| } |
|
|
| |
| class SoundManager { |
| constructor() { |
| this.audioContext = null; |
| this.musicEnabled = localStorage.getItem('musicEnabled') !== 'false'; |
| this.soundsEnabled = localStorage.getItem('soundsEnabled') !== 'false'; |
| this.musicVolume = parseFloat(localStorage.getItem('musicVolume') || '0.5'); |
| this.sfxVolume = parseFloat(localStorage.getItem('sfxVolume') || '0.7'); |
| this.musicOscillator = null; |
| this.musicGainNode = null; |
| this.isMusicPlaying = false; |
| this.initAudioContext(); |
| this.setupVolumeControls(); |
| } |
| |
| setupVolumeControls() { |
| |
| const musicVolumeSlider = document.getElementById('music-volume'); |
| if (musicVolumeSlider) { |
| musicVolumeSlider.value = this.musicVolume * 100; |
| musicVolumeSlider.addEventListener('input', (e) => { |
| this.musicVolume = e.target.value / 100; |
| localStorage.setItem('musicVolume', this.musicVolume); |
| }); |
| } |
| |
| |
| const sfxVolumeSlider = document.getElementById('sfx-volume'); |
| if (sfxVolumeSlider) { |
| sfxVolumeSlider.value = this.sfxVolume * 100; |
| sfxVolumeSlider.addEventListener('input', (e) => { |
| this.sfxVolume = e.target.value / 100; |
| localStorage.setItem('sfxVolume', this.sfxVolume); |
| }); |
| } |
| } |
| |
| initAudioContext() { |
| try { |
| this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
| } catch (e) { |
| console.warn('Web Audio API not supported'); |
| } |
| } |
| |
| |
| startBackgroundMusic() { |
| if (!this.audioContext || !this.musicEnabled || this.isMusicPlaying) return; |
| |
| try { |
| this.isMusicPlaying = true; |
| this.playMusicLoop(); |
| } catch (e) { |
| console.warn('Could not start music:', e); |
| } |
| } |
| |
| playMusicLoop() { |
| if (!this.audioContext || !this.musicEnabled) return; |
| |
| const notes = [261.63, 293.66, 329.63, 349.23, 392.00, 440.00, 493.88, 523.25]; |
| let noteIndex = 0; |
| |
| const playNote = () => { |
| if (!this.musicEnabled || !this.isMusicPlaying) return; |
| |
| const frequency = notes[noteIndex % notes.length]; |
| const oscillator = this.audioContext.createOscillator(); |
| const gainNode = this.audioContext.createGain(); |
| |
| oscillator.connect(gainNode); |
| gainNode.connect(this.audioContext.destination); |
| |
| oscillator.type = 'sine'; |
| oscillator.frequency.value = frequency; |
| |
| const baseGain = this.musicVolume * 0.1; |
| gainNode.gain.setValueAtTime(baseGain, this.audioContext.currentTime); |
| gainNode.gain.exponentialRampToValueAtTime(baseGain * 0.5, this.audioContext.currentTime + 0.3); |
| gainNode.gain.exponentialRampToValueAtTime(baseGain * 0.1, this.audioContext.currentTime + 0.6); |
| |
| oscillator.start(this.audioContext.currentTime); |
| oscillator.stop(this.audioContext.currentTime + 0.6); |
| |
| noteIndex++; |
| |
| setTimeout(() => { |
| if (this.musicEnabled && this.isMusicPlaying) { |
| playNote(); |
| } |
| }, 300); |
| }; |
| |
| playNote(); |
| } |
| |
| stopBackgroundMusic() { |
| this.isMusicPlaying = false; |
| if (this.musicOscillator) { |
| this.musicOscillator.stop(); |
| this.musicOscillator = null; |
| } |
| } |
| |
| toggleMusic() { |
| this.musicEnabled = !this.musicEnabled; |
| localStorage.setItem('musicEnabled', this.musicEnabled); |
| |
| if (this.musicEnabled) { |
| this.startBackgroundMusic(); |
| } else { |
| this.stopBackgroundMusic(); |
| } |
| |
| this.updateMusicButton(); |
| } |
| |
| updateMusicButton() { |
| const btn = document.getElementById('music-toggle-btn'); |
| if (btn) { |
| btn.textContent = this.musicEnabled ? '🔊' : '🔇'; |
| } |
| } |
| |
| |
| playSwipeSound() { |
| if (!this.audioContext || !this.soundsEnabled) return; |
| |
| const oscillator = this.audioContext.createOscillator(); |
| const gainNode = this.audioContext.createGain(); |
| |
| oscillator.connect(gainNode); |
| gainNode.connect(this.audioContext.destination); |
| |
| oscillator.type = 'sine'; |
| oscillator.frequency.setValueAtTime(400, this.audioContext.currentTime); |
| oscillator.frequency.exponentialRampToValueAtTime(200, this.audioContext.currentTime + 0.1); |
| |
| const volume = this.sfxVolume * 0.2; |
| gainNode.gain.setValueAtTime(volume, this.audioContext.currentTime); |
| gainNode.gain.exponentialRampToValueAtTime(volume * 0.05, this.audioContext.currentTime + 0.1); |
| |
| oscillator.start(this.audioContext.currentTime); |
| oscillator.stop(this.audioContext.currentTime + 0.1); |
| } |
| |
| |
| playMatchSound(count = 1) { |
| if (!this.audioContext || !this.soundsEnabled) return; |
| |
| const frequencies = [523.25, 659.25, 783.99]; |
| const delay = 0.05; |
| |
| frequencies.forEach((freq, index) => { |
| setTimeout(() => { |
| const oscillator = this.audioContext.createOscillator(); |
| const gainNode = this.audioContext.createGain(); |
| |
| oscillator.connect(gainNode); |
| gainNode.connect(this.audioContext.destination); |
| |
| oscillator.type = 'sine'; |
| oscillator.frequency.value = freq * (1 + count * 0.1); |
| |
| const volume = this.sfxVolume * 0.15; |
| gainNode.gain.setValueAtTime(volume, this.audioContext.currentTime); |
| gainNode.gain.exponentialRampToValueAtTime(volume * 0.07, this.audioContext.currentTime + 0.3); |
| |
| oscillator.start(this.audioContext.currentTime); |
| oscillator.stop(this.audioContext.currentTime + 0.3); |
| }, index * delay); |
| }); |
| } |
| |
| |
| playLevelCompleteSound() { |
| if (!this.audioContext || !this.soundsEnabled) return; |
| |
| const melody = [523.25, 659.25, 783.99, 1046.50, 783.99, 1046.50]; |
| melody.forEach((freq, index) => { |
| setTimeout(() => { |
| const oscillator = this.audioContext.createOscillator(); |
| const gainNode = this.audioContext.createGain(); |
| |
| oscillator.connect(gainNode); |
| gainNode.connect(this.audioContext.destination); |
| |
| oscillator.type = 'sine'; |
| oscillator.frequency.value = freq; |
| |
| const volume = this.sfxVolume * 0.2; |
| gainNode.gain.setValueAtTime(volume, this.audioContext.currentTime); |
| gainNode.gain.exponentialRampToValueAtTime(volume * 0.05, this.audioContext.currentTime + 0.4); |
| |
| oscillator.start(this.audioContext.currentTime); |
| oscillator.stop(this.audioContext.currentTime + 0.4); |
| }, index * 150); |
| }); |
| } |
| |
| |
| playGameOverSound() { |
| if (!this.audioContext || !this.soundsEnabled) return; |
| |
| const frequencies = [392.00, 349.23, 293.66, 261.63]; |
| frequencies.forEach((freq, index) => { |
| setTimeout(() => { |
| const oscillator = this.audioContext.createOscillator(); |
| const gainNode = this.audioContext.createGain(); |
| |
| oscillator.connect(gainNode); |
| gainNode.connect(this.audioContext.destination); |
| |
| oscillator.type = 'sine'; |
| oscillator.frequency.value = freq; |
| |
| const volume = this.sfxVolume * 0.15; |
| gainNode.gain.setValueAtTime(volume, this.audioContext.currentTime); |
| gainNode.gain.exponentialRampToValueAtTime(volume * 0.07, this.audioContext.currentTime + 0.5); |
| |
| oscillator.start(this.audioContext.currentTime); |
| oscillator.stop(this.audioContext.currentTime + 0.5); |
| }, index * 200); |
| }); |
| } |
| |
| |
| playClickSound() { |
| if (!this.audioContext || !this.soundsEnabled) return; |
| |
| const oscillator = this.audioContext.createOscillator(); |
| const gainNode = this.audioContext.createGain(); |
| |
| oscillator.connect(gainNode); |
| gainNode.connect(this.audioContext.destination); |
| |
| oscillator.type = 'sine'; |
| oscillator.frequency.value = 800; |
| |
| const volume = this.sfxVolume * 0.1; |
| gainNode.gain.setValueAtTime(volume, this.audioContext.currentTime); |
| gainNode.gain.exponentialRampToValueAtTime(volume * 0.1, this.audioContext.currentTime + 0.05); |
| |
| oscillator.start(this.audioContext.currentTime); |
| oscillator.stop(this.audioContext.currentTime + 0.05); |
| } |
| } |
|
|
| |
| let game; |
| window.addEventListener('DOMContentLoaded', () => { |
| game = new Match3Game(); |
| }); |
|
|
|
|