import API from "../../api.js"; import TableBasePage from "../base_table.js"; import DOM from "../../dom.js"; import Events from "../../lib/events.js"; export default class PageMtgGame extends TableBasePage { static hash = hashPageMtgGame; static attrIdRowObject = attrGameId; callSaveTableContent = API.saveGame; constructor(router) { super(router); } initialize() { this.sharedInitialize(); this.hookupTcgGame(); } hookupFilters() { // this.sharedHookupFilters(); } loadRowTable(rowJson) { return; } getJsonRow(row) { return; } initialiseRowNew(tbody, row) { } postInitialiseRowNewCallback(tbody) { } hookupTableMain() { super.hookupTableMain(); } hookupTcgGame() { this.initGamePage(); let pageHeading = document.querySelector('.container.company-name .tcg-title.company-name'); pageHeading.innerText = `MTG Game #${gameId}`; } initGamePage() { // Load existing game state from API or show setup PageMtgGame.updatePlayerSetup(); if (typeof gameId !== 'undefined' && gameId) { this.loadGameFromServer(); } /* else { PageMtgGame.updatePlayerSetup(); } */ PageMtgGame.hookupResetButton(); PageMtgGame.hookupPlayerCountInput(); this.hookupStartGameButton(); /* this.hookupCommanderDeathIncrementButtons(); this.hookupEliminateCommanderButtons(); this.hookupPlayerLifeIncrementButtons(); this.hookupCommanderDamageIncrementButtons(); */ } static hookupResetButton() { const resetGameButton = document.querySelector('header.game-header .header-right .btn-tcg.btn-tcg-secondary'); if (resetGameButton) { resetGameButton.addEventListener('click', PageMtgGame.resetGame); } } static hookupPlayerCountInput() { const playerCountInput = document.getElementById('playerCount'); if (playerCountInput) { playerCountInput.addEventListener('change', PageMtgGame.updatePlayerSetup); } } hookupStartGameButton() { const startGameButton = document.querySelector('.setup-section .setup-actions .btn-tcg'); if (startGameButton) { startGameButton.addEventListener('click', () => { this.startGame(); }); } } /* hookupCommanderDeathIncrementButtons() { const commanderDeathIncremementButtons = document.querySelectorAll('#players-grid .player-card .commander-deaths .death-btn'); if (commanderDeathIncremementButtons) { commanderDeathIncremementButtons.forEach((button) => { button.addEventListener('click', PageMtgGame.changeCommanderDeaths); }); } } hookupEliminateCommanderButtons() { const eliminateCommanderButtons = document.querySelector('#players-grid .player-card .eliminate-btn'); if (eliminateCommanderButtons) { eliminateCommanderButtons.forEach((button) => { button.addEventListener('click', PageMtgGame.toggleEliminate); }); } } hookupPlayerLifeIncrementButtons() { const playerLifeIncrementButtons = document.querySelector('#players-grid .player-card .eliminate-btn'); if (playerLifeIncrementButtons) { playerLifeIncrementButtons.forEach((button) => { button.addEventListener('click', PageMtgGame.changeLife); }); } } hookupCommanderDamageIncrementButtons() { const commanderDamageIncrementButtons = document.querySelector('#players-grid .player-card .eliminate-btn'); if (commanderDamageIncrementButtons) { commanderDamageIncrementButtons.forEach((button) => { button.addEventListener('click', PageMtgGame.changeCommanderDamage); }); } } */ async loadGameFromServer() { console.log("loading game from server"); try { // Fetch players, rounds, and damage records from API const [playersResponse, roundsResponse, damageResponse] = await Promise.all([ API.getGamePlayers(gameId) , API.getGameRounds(gameId) , API.getGameDamageRecords(gameId) ]); console.log({ playersResponse, damageResponse }); let setupSection = document.getElementById('setupSection'); let gameSection = document.getElementById('gameSection'); setupSection.classList.remove('hidden'); gameSection.classList.add('hidden'); if (playersResponse.status !== 'success') { console.error('Failed to load players:', playersResponse.message); return; } const savedPlayers = playersResponse.data || []; const savedRounds = roundsResponse.status === 'success' ? (roundsResponse.data || []) : []; const savedDamageRecords = damageResponse.status === 'success' ? (damageResponse.data || []) : []; players = savedPlayers; rounds = savedRounds; damageRecords = savedDamageRecords; if (savedPlayers.length === 0) { // No players yet, show setup section return; } // Hide setup, show game setupSection.classList.add('hidden'); gameSection.classList.remove('hidden'); console.log({ savedPlayers, damageRecords }); // Render players to DOM this.renderPlayers(); } catch (error) { console.error('Error loading game from server:', error); } } renderPlayers() { const grid = document.getElementById('playersGrid'); grid.innerHTML = ''; // Build a damage lookup: { playerId: { fromPlayerId: damageAmount } } /* const damageLookup = {}; damageRecords.forEach(damage => { if (!damageLookup[damage.player_id]) { damageLookup[damage.player_id] = {}; } if (damage.received_from_commander_player_id) { damageLookup[damage.player_id][damage.received_from_commander_player_id] = damage.health_change || 0; } }); */ const latestRoundId = PageMtgGame.getLatestRoundId(); players.forEach((player, index) => { // Build display name: prefer user_name + deck_name, fallback to player name const playerId = player[attrPlayerId]; let displayName = PageMtgGame.makePlayerDisplayName(playerId, index); let damagePlayerPairs = [...players, { [attrPlayerId]: null }]; let maxCommanderDamageReceived = 0; damagePlayerPairs.forEach(damagePlayerPair => { const sourceId = damagePlayerPair[attrPlayerId]; const filteredPlayerDamages = damageRecords.filter(damage => ( damage[attrRoundId] == latestRoundId && damage[attrPlayerId] == playerId && damage[attrReceivedFromCommanderPlayerId] == sourceId )); //[playerId] || {}; if (filteredPlayerDamages.length == 0) { damageRecords.push(PageMtgGame.makeDefaultGameRoundPlayerDamage(playerId, sourceId)); } maxCommanderDamageReceived = Math.max( maxCommanderDamageReceived , damageRecords.filter(damage => ( damage[attrPlayerId] == playerId && damage[attrReceivedFromCommanderPlayerId] == sourceId )) .map(damage => damage[flagHealthChange]) .reduce((acc, curr) => acc + curr, 0) ); }); const totalDamage = damageRecords.filter(damage => ( damage[attrPlayerId] == playerId )) .map(damage => damage[flagHealthChange]) .reduce((acc, curr) => acc + curr, 0); let life = startingLife + totalDamage; let isEliminatedByForce = damageRecords.filter(damage => ( damage[attrPlayerId] == playerId )) .map(damage => damage[flagIsEliminated]) .some(Boolean); const isEliminated = ( isEliminatedByForce || !player[flagActive] || life < 1 || maxCommanderDamageReceived >= 21 ); const playerOwnDamage = damageRecords.filter(damage => ( damage[attrPlayerId] == playerId && damage[attrReceivedFromCommanderPlayerId] == null && damage[attrRoundId] == latestRoundId ))[0]; const card = document.createElement('div'); card.className = `player-card ${isEliminated ? 'eliminated' : ''}`; card.style.animationDelay = `${index * 0.1}s`; card.dataset.playerId = playerId; card.dataset.userName = player.user_name || ''; card.dataset.deckName = player.deck_name || ''; card.innerHTML = `
${displayName}
Commander Deaths:
${playerOwnDamage[flagCommanderDeaths]}
${life}
Commander Damage Taken
${PageMtgGame.renderCommanderDamageRows( playerId // playerId , player[attrDeckId] // deckId )}
`; grid.appendChild(card); }); // Hookup all event handlers this.hookupPlayerCardEvents(); } static makeDefaultGameRoundPlayerDamage(playerId, receivedFromCommanderPlayerId) { let roundId = PageMtgGame.getLatestRoundId(); return { [attrDamageId]: -1 - damageRecords.length , [attrRoundId]: roundId , [attrPlayerId]: playerId , [attrReceivedFromCommanderPlayerId]: receivedFromCommanderPlayerId , [flagHealthChange]: 0 , [flagCommanderDeaths]: 0 , [flagActive]: true }; } static getLatestRoundId() { let roundId = -1; if (rounds.length > 0) { let highestRoundDisplayOrder = Math.max(rounds.map(round => { return round[flagDisplayOrder]; })); roundId = rounds.filter(round => round[flagDisplayOrder] == highestRoundDisplayOrder)[0][attrRoundId]; console.log({ "method": "getLatestRoundId", highestRoundDisplayOrder, roundId }); } return roundId; } static makePlayerDisplayName(playerId, index) { if (playerId == null) { return `Player ${index + 1}`; } const player = players.filter(player => player[attrPlayerId] == playerId)[0]; const deckId = player[attrDeckId]; const deck = (deckId == null) ? null : decks.filter(deck => deck[attrDeckId] == deckId)[0]; const user = (playerId == null) ? null : users[player[attrUserId]]; return player[flagName] || `${(user == null) ? 'Error' : user[flagName]} - ${(deck == null) ? 'Error' : deck[flagName]}`; } static renderCommanderDamageRows(playerId) { // const roundId = PageMtgGame.getLatestRoundId(); return players .filter(otherPlayer => otherPlayer[attrPlayerId] !== playerId) .map(otherPlayer => { const sourceId = otherPlayer[attrPlayerId]; let otherPlayerDisplayName = PageMtgGame.makePlayerDisplayName(sourceId); const totalDamage = damageRecords.filter(damage => ( damage[attrPlayerId] == playerId && damage[attrReceivedFromCommanderPlayerId] == sourceId )) .map(damage => -damage[flagHealthChange]) .reduce((acc, curr) => acc + curr, 0); const isLethal = totalDamage >= 21; return `
from ${otherPlayerDisplayName}
${totalDamage}
`; }) .join(''); } hookupPlayerCardEvents() { // Life buttons let lifeButtonSelector = '.life-btn'; Events.hookupEventHandler("click", lifeButtonSelector, (event, button) => { const playerId = button.dataset.playerId; const amount = parseInt(button.dataset.amount); const latestRoundId = PageMtgGame.getLatestRoundId(); const damageIndex = damageRecords.findIndex(damage => ( damage[attrRoundId] == latestRoundId && damage[attrPlayerId] == playerId && damage[attrReceivedFromCommanderPlayerId] == null )); this.changeLife( playerId // playerId , amount // amount , true // updateDamage , damageIndex // damageIndex ); }); // Commander death buttons let commanderDeathButtonSelector = '.death-btn'; Events.hookupEventHandler("click", commanderDeathButtonSelector, (event, button) => { const playerId = button.dataset.playerId; const isMinusButton = button.classList.contains('death-minus'); const amount = (isMinusButton) ? -1 : 1; this.changeCommanderDeaths(playerId, amount); }); // Commander damage buttons let commmanderDamageButtonSelector = '.damage-btn'; Events.hookupEventHandler("click", commmanderDamageButtonSelector, (event, button) => { const playerId = button.dataset.playerId; const sourceId = button.dataset.sourceId; const isMinusButton = button.classList.contains('damage-minus'); const amount = (isMinusButton) ? -1 : 1; this.changeCommanderDamage(playerId, sourceId, amount); }); // Eliminate buttons let eliminatePlayerButtonSelector = '.eliminate-btn'; Events.hookupEventHandler("click", eliminatePlayerButtonSelector, (event, button) => { const playerId = button.dataset.playerId; this.toggleEliminate(playerId); }); } changeLife(playerId, amount, updateDamage = false, damageIndex = null) { const card = document.querySelector(`.player-card[data-player-id="${playerId}"]`); if (!card || card.classList.contains('eliminated')) return; const lifeInput = card.querySelector(`.life-value[data-player-id="${playerId}"]`); const lifeDisplay = card.querySelector(`.life-display[data-player-id="${playerId}"]`); const currentLife = parseInt(lifeInput.value) || 0; const newLife = Math.max(0, currentLife + amount); DOM.setElementAttributeValueCurrent(lifeDisplay, newLife); DOM.isElementDirty(lifeDisplay); lifeInput.value = newLife; lifeDisplay.textContent = newLife; if (updateDamage) { damageRecords[damageIndex][flagHealthChange] += amount; } // PageMtgGame.debouncedSave(); this.updateAndToggleShowButtonsSaveCancel(); } changeCommanderDamage(playerId, sourceId, amount) { const card = document.querySelector(`.player-card[data-player-id="${playerId}"]`); if (!card || card.classList.contains('eliminated')) return; const damageInput = card.querySelector(`.damage-value[data-player-id="${playerId}"][data-source-id="${sourceId}"]`); const damageDisplay = card.querySelector(`.damage-display[data-player-id="${playerId}"][data-source-id="${sourceId}"]`); const currentDamage = parseInt(damageInput.value) || 0; const newDamage = Math.max(0, currentDamage + amount); amount = newDamage - currentDamage; DOM.setElementAttributeValueCurrent(damageDisplay, newDamage); DOM.isElementDirty(damageDisplay); damageInput.value = newDamage; damageDisplay.textContent = newDamage; // Update lethal class if (newDamage >= 21) { damageDisplay.classList.add('lethal'); } else { damageDisplay.classList.remove('lethal'); } const latestRoundId = PageMtgGame.getLatestRoundId(); const damageIndex = damageRecords.findIndex(damageRecord => ( damage[attrRoundId] == latestRoundId && damageRecord[attrPlayerId] == playerId && damageRecord[attrReceivedFromCommanderPlayerId] == sourceId )); damageRecords[damageIndex][flagHealthChange] -= amount; this.changeLife( playerId // playerId , -amount // amount , false // updateDamage , damageIndex // damageIndex ); // PageMtgGame.debouncedSave(); } changeCommanderDeaths(playerId, amount) { const card = document.querySelector(`.player-card[data-player-id="${playerId}"]`); if (!card || card.classList.contains('eliminated')) return; const deathDisplay = card.querySelector(`.death-display[data-player-id="${playerId}"]`); const currentDeaths = parseInt(deathDisplay.textContent) || 0; const newDeaths = Math.max(0, currentDeaths + amount); deathDisplay.textContent = newDeaths; DOM.setElementAttributeValueCurrent(deathDisplay, newDeaths); DOM.isElementDirty(deathDisplay); const latestRoundId = PageMtgGame.getLatestRoundId(); const damageIndex = damageRecords.findIndex(damage => ( damage[attrRoundId] == latestRoundId && damage[attrPlayerId] == playerId && damage[attrReceivedFromCommanderPlayerId] == null )); damageRecords[damageIndex][flagCommanderDeaths] = newDeaths; // PageMtgGame.debouncedSave(); this.updateAndToggleShowButtonsSaveCancel(); } toggleEliminate(playerId) { const card = document.querySelector(`.player-card[data-player-id="${playerId}"]`); if (!card) return; const eliminateBtn = card.querySelector(`.eliminate-btn[data-player-id="${playerId}"]`); const wasEliminated = card.classList.contains('eliminated'); if (wasEliminated) { card.classList.remove('eliminated'); eliminateBtn.textContent = 'Eliminate'; } else { card.classList.add('eliminated'); eliminateBtn.textContent = 'Revive'; } const isEliminated = card.classList.contains('eliminated'); const latestRoundId = PageMtgGame.getLatestRoundId(); const damageIndex = damageRecords.findIndex(damage => ( damage[attrRoundId] == latestRoundId && damage[attrPlayerId] == playerId && damage[attrReceivedFromCommanderPlayerId] == null )); damageRecords[damageIndex][flagIsEliminated] = isEliminated; DOM.setElementAttributeValueCurrent(eliminateBtn, isEliminated); DOM.isElementDirty(eliminateBtn); // PageMtgGame.debouncedSave(); this.updateAndToggleShowButtonsSaveCancel(); } static updatePlayerSetup() { const playerCountInput = document.getElementById('playerCount'); if (!playerCountInput) return; const playerCount = parseInt(playerCountInput.value); const grid = document.getElementById('playerSetupGrid'); if (!grid) return; grid.innerHTML = ''; const wrapperTemplate = document.getElementById(playerSetupWrapperTemplateId); let player, wrapper, wrapperHeading, userDdl, deckDdl, nameInput; for (let i = 0; i < playerCount; i++) { if (i < players.length) { player = players[i]; } else { player = PageMtgGame.makeDefaultGamePlayer(); players.push(player); } wrapper = wrapperTemplate.cloneNode(true); wrapper.removeAttribute("id"); wrapper.setAttribute(flagDisplayOrder, i + 1); wrapper.classList.remove(flagIsCollapsed); wrapperHeading = wrapper.querySelector('label'); wrapperHeading.innerText = 'Player ' + (i + 1); userDdl = wrapper.querySelector('.playerUser select'); DOM.setElementValuesCurrentAndPrevious(userDdl, player[attrUserId]); deckDdl = wrapper.querySelector('.playerDeck select'); DOM.setElementValuesCurrentAndPrevious(deckDdl, player[attrDeckId]); nameInput = wrapper.querySelector('.playerName input'); DOM.setElementValuesCurrentAndPrevious(nameInput, player[flagName]); console.log('player: ', player); grid.appendChild(wrapper); } } static makeDefaultGamePlayer() { return { [attrPlayerId]: -players.length , [attrGameId]: gameId , [attrUserId]: user[attrUserId] , [attrDeckId]: 0 , [flagName]: "" , [flagNotes]: null , [flagDisplayOrder]: players.length , [flagActive]: true }; } async startGame() { const playerCountInput = document.getElementById('playerCount'); if (!playerCountInput) return; const playerCount = parseInt(playerCountInput.value); const playersToSave = []; let playerSetupWrapper, playerId, player, userDdl, userId, deckDdl, deckId, nameInput, name; for (let i = 0; i < playerCount; i++) { playerSetupWrapper = document.querySelector('.player-name-input-wrapper[' + flagDisplayOrder + '="' + (i + 1) + '"]'); userDdl = playerSetupWrapper.querySelector('.playerUser select'); deckDdl = playerSetupWrapper.querySelector('.playerDeck select'); nameInput = playerSetupWrapper.querySelector('.playerName input'); userId = DOM.getElementValueCurrent(userDdl); deckId = DOM.getElementValueCurrent(deckDdl); name = nameInput ? nameInput.value.trim() || `Player ${i + 1}` : `Player ${i + 1}`; playerId = playerSetupWrapper.getAttribute(attrPlayerId); player = players.filter(p => p[attrPlayerId] == playerId)[0]; playersToSave.push({ ...player , [attrGameId]: gameId , [attrUserId]: userId , [attrDeckId]: deckId , [flagName]: name , [flagDisplayOrder]: i + 1 , [flagActive]: true }); } // Save players to server const comment = 'Save players'; const self = this; API.saveGamePlayers(playersToSave, null, comment) .then(data => { if (data[flagStatus] == flagSuccess) { self.leave(); window.location.reload(); } else { console.error('Failed to save players:', data[flagMessage]); PageMtgGame.showError('An error occurred while creating the game'); } }) .catch(error => { console.error('Error creating game:', error); PageMtgGame.showError('An error occurred while creating the game'); }) .finally(() => { }); } static resetGame() { if (confirm('Are you sure you want to start a new game? Current game will be lost.')) { localStorage.removeItem(`mtgGame_${gameId}`); window.location.href = hashPageGames; } } async saveGame() { const gameState = { [flagPlayer]: players , [flagRound]: rounds , [flagDamage]: damageRecords }; /* if (gameState[flagPlayer].length > 0) { localStorage.setItem(`mtgGame_${gameId}`, JSON.stringify(gameState)); PageMtgGame.showSaveIndicator(); } */ const comment = 'Save players'; const self = this; API.saveGameRoundPlayerDamages(rounds, damageRecords, null, comment) .then(data => { if (data[flagStatus] == flagSuccess) { self.leave(); window.location.reload(); } else { console.error('Failed to save players:', data[flagMessage]); PageMtgGame.showError('An error occurred while creating the game'); } }) .catch(error => { console.error('Error creating game:', error); PageMtgGame.showError('An error occurred while creating the game'); }) .finally(() => { }); } /* static debouncedSave() { clearTimeout(PageMtgGame._saveTimeout); PageMtgGame._saveTimeout = setTimeout(() => PageMtgGame.saveGame(), 500); } static showSaveIndicator() { const indicator = document.getElementById('saveIndicator'); if (indicator) { indicator.classList.add('show'); setTimeout(() => { indicator.classList.remove('show'); }, 2000); } } */ saveRecordsTableDirty() { this.saveGame(); } static showError(message) { // Check if there's an overlay error element const errorOverlay = document.getElementById('overlayError'); if (errorOverlay) { const errorLabel = errorOverlay.querySelector('.error-message, #labelError'); if (errorLabel) { errorLabel.textContent = message; } errorOverlay.classList.remove('hidden'); errorOverlay.style.display = 'flex'; } else { // Fallback to alert alert(message); } } leave() { super.leave(); } } // Static timeout reference for debouncing PageMtgGame._saveTimeout = null;