946 lines
41 KiB
JavaScript
946 lines
41 KiB
JavaScript
|
|
|
|
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
|
|
const latestRoundId = PageMtgGame.getLatestRoundId();
|
|
const latestRound = rounds.filter(round => round[attrRoundId] == latestRoundId)[0];
|
|
const roundDisplayOrderLabel = PageMtgGame.getRoundDisplayOrderLabel();
|
|
DOM.setElementValuesCurrentAndPrevious(roundDisplayOrderLabel, latestRound[flagDisplayOrder]);
|
|
|
|
this.renderPlayers();
|
|
|
|
} catch (error) {
|
|
console.error('Error loading game from server:', error);
|
|
}
|
|
}
|
|
static getRoundDisplayOrderLabel() {
|
|
return document.querySelector([
|
|
'#gameSection'
|
|
, ' > .'
|
|
, flagRow
|
|
, '.'
|
|
, flagRound
|
|
, ' > .'
|
|
, flagRow
|
|
, '.'
|
|
, flagRound
|
|
, ' > .'
|
|
, flagRound
|
|
, '.'
|
|
, flagDisplayOrder
|
|
, ' > span.'
|
|
, flagRound
|
|
, '.'
|
|
, flagDisplayOrder
|
|
].join(''));
|
|
}
|
|
|
|
renderPlayers() {
|
|
const grid = document.getElementById('playersGrid');
|
|
grid.innerHTML = '';
|
|
|
|
// let activeRoundId = PageMtgGame.getActiveRoundId();
|
|
const roundDisplayOrderLabel = PageMtgGame.getRoundDisplayOrderLabel();
|
|
const currentRoundDisplayOrder = parseInt(DOM.getElementValueCurrent(roundDisplayOrderLabel));
|
|
let activeRound = rounds.filter(round => round[flagDisplayOrder] == currentRoundDisplayOrder)[0];
|
|
if (activeRound == null) {
|
|
activeRound = PageMtgGame.makeDefaultGameRound(currentRoundDisplayOrder);
|
|
rounds.push(activeRound);
|
|
}
|
|
DOM.setElementValueCurrent(roundDisplayOrderLabel, activeRound[flagDisplayOrder]);
|
|
|
|
const previousRoundIds = rounds.filter(round => round[flagDisplayOrder] <= currentRoundDisplayOrder)
|
|
.map(round => round[attrRoundId]);
|
|
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] == activeRound[attrRoundId]
|
|
// previousRoundIds.includes(damage[attrRoundId])
|
|
&& 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
|
|
&& damage[attrReceivedFromCommanderPlayerId] != null
|
|
&& previousRoundIds.includes(damage[attrRoundId])
|
|
))
|
|
.map(damage => damage[flagLifeLoss])
|
|
.reduce((a, b) => a + b, 0)
|
|
);
|
|
});
|
|
|
|
const totalDamage = damageRecords.filter(damage => (
|
|
damage[attrPlayerId] == playerId
|
|
&& previousRoundIds.includes(damage[attrRoundId])
|
|
))
|
|
.map(damage => damage[flagLifeLoss] - damage[flagLifeGain])
|
|
.reduce((a, b) => a + b, 0);
|
|
let life = startingLife - totalDamage;
|
|
|
|
let isEliminatedByForce = damageRecords.filter(damage => (
|
|
damage[attrPlayerId] == playerId
|
|
&& previousRoundIds.includes(damage[attrRoundId])
|
|
))
|
|
.map(damage => damage[flagIsEliminated])
|
|
.some(Boolean);
|
|
console.log("renderPlayers");
|
|
console.log({isEliminatedByForce, player, life, maxCommanderDamageReceived});
|
|
const isEliminated = (
|
|
isEliminatedByForce
|
|
|| !player[flagActive]
|
|
|| life < 1
|
|
|| maxCommanderDamageReceived >= 21
|
|
);
|
|
|
|
const totalCommanderDeaths = damageRecords.filter(damage => (
|
|
damage[attrPlayerId] == playerId
|
|
&& damage[attrReceivedFromCommanderPlayerId] == null
|
|
&& damage[attrRoundId] == activeRound[attrRoundId]
|
|
))
|
|
.map(damage => damage[flagCommanderDeaths])
|
|
.reduce((a, b) => a + b, 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 = `
|
|
<div class="player-header">
|
|
<div class="player-info">
|
|
<div class="player-name">${displayName}</div>
|
|
<div class="commander-deaths">
|
|
<span>Commander Deaths:</span>
|
|
<div class="death-counter">
|
|
<button class="death-btn death-minus" data-player-id="${playerId}">−</button>
|
|
<span class="death-display" data-player-id="${playerId}" ${attrValuePrevious}="${totalCommanderDeaths}">${totalCommanderDeaths}</span>
|
|
<button class="death-btn death-plus" data-player-id="${playerId}">+</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button class="eliminate-btn" data-player-id="${playerId}" ${attrValuePrevious}="${isEliminated}">
|
|
${isEliminated ? 'Revive' : 'Eliminate'}
|
|
</button>
|
|
</div>
|
|
|
|
<div class="life-total">
|
|
<input type="hidden" class="life-value" data-player-id="${playerId}" value="${life}">
|
|
<div class="life-display" data-player-id="${playerId}" ${attrValuePrevious}="${life}">${life}</div>
|
|
<label>Gain</label>
|
|
<div class="life-gain-controls">
|
|
<button class="life-gain-btn" data-player-id="${playerId}" data-amount="-5">-5</button>
|
|
<button class="life-gain-btn" data-player-id="${playerId}" data-amount="-1">-1</button>
|
|
<button class="life-gain-btn" data-player-id="${playerId}" data-amount="1">+1</button>
|
|
<button class="life-gain-btn" data-player-id="${playerId}" data-amount="5">+5</button>
|
|
</div>
|
|
<label>Loss</label>
|
|
<div class="life-loss-controls">
|
|
<button class="life-loss-btn" data-player-id="${playerId}" data-amount="-5">-5</button>
|
|
<button class="life-loss-btn" data-player-id="${playerId}" data-amount="-1">-1</button>
|
|
<button class="life-loss-btn" data-player-id="${playerId}" data-amount="1">+1</button>
|
|
<button class="life-loss-btn" data-player-id="${playerId}" data-amount="5">+5</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="commander-damage-section">
|
|
<div class="section-title">Commander Damage Taken</div>
|
|
<div class="damage-grid" data-player-id="${playerId}">
|
|
${PageMtgGame.renderCommanderDamageRows(
|
|
playerId // playerId
|
|
, player[attrDeckId] // deckId
|
|
)}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
grid.appendChild(card);
|
|
});
|
|
|
|
this.reorderPlayerCards();
|
|
|
|
PageMtgGame.renderCommanderDamageLog();
|
|
|
|
// Hookup all event handlers
|
|
this.hookupGameRoundEvents();
|
|
this.hookupPlayerCardEvents();
|
|
}
|
|
static renderCommanderDamageLog() {
|
|
const currentRoundDisplayOrder = PageMtgGame.getActiveRoundDisplayOrder();
|
|
|
|
const damageTableBody = document.querySelector('.' + flagDamageLog + '.' + flagContainer + ' table tbody');
|
|
damageTableBody.innerHTML = '';
|
|
|
|
const previousRoundIds = rounds.filter(round => round[flagDisplayOrder] <= currentRoundDisplayOrder)
|
|
.map(round => round[attrRoundId]);
|
|
let newTableBodyHtml = '';
|
|
damageRecords.forEach((damage) => {
|
|
if (
|
|
damage[flagActive]
|
|
&& (
|
|
damage[flagCommanderDeaths] > 0
|
|
|| damage[flagLifeGain] != 0
|
|
|| damage[flagLifeLoss] != 0
|
|
|| damage[flagIsEliminated]
|
|
)
|
|
// && rounds.filter(r => r[attrRoundId] == damage[attrRoundId])[0][flagDisplayOrder] <= currentRoundDisplayOrder
|
|
&& previousRoundIds.includes(damage[attrRoundId])
|
|
) {
|
|
let round = rounds.filter(r => r[attrRoundId] == damage[attrRoundId])[0];
|
|
let player = players.filter(p => p[attrPlayerId] == damage[attrPlayerId])[0];
|
|
let receivedFromPlayer = (damage[attrReceivedFromCommanderPlayerId] == null) ? { [flagName]: ''} : players.filter(p => p[attrPlayerId] == damage[attrReceivedFromCommanderPlayerId])[0];
|
|
newTableBodyHtml += `
|
|
<tr ${attrDamageId}="${damage[attrDamageId]}">
|
|
<td class="${attrRoundId}">${round[flagDisplayOrder]}</td>
|
|
<td class="${attrPlayerId}">${player[flagName]}</td>
|
|
<td class="${attrReceivedFromCommanderPlayerId}">${receivedFromPlayer[flagName]}</td>
|
|
<td class="${flagLifeGain}">${damage[flagLifeGain]}</td>
|
|
<td class="${flagLifeLoss}">${damage[flagLifeLoss]}</td>
|
|
<td class="${flagCommanderDeaths}">${damage[flagCommanderDeaths]}</td>
|
|
<td class="${flagIsEliminated}">${damage[flagIsEliminated]}</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
});
|
|
damageTableBody.innerHTML = newTableBodyHtml;
|
|
}
|
|
static makeDefaultGameRoundPlayerDamage(playerId, receivedFromCommanderPlayerId) {
|
|
let roundId = PageMtgGame.getActiveRoundId();
|
|
return {
|
|
[attrDamageId]: -1 - damageRecords.length
|
|
, [attrRoundId]: roundId
|
|
, [attrPlayerId]: playerId
|
|
, [attrReceivedFromCommanderPlayerId]: receivedFromCommanderPlayerId
|
|
, [flagLifeGain]: 0
|
|
, [flagLifeLoss]: 0
|
|
, [flagCommanderDeaths]: 0
|
|
, [flagIsEliminated]: false
|
|
, [flagActive]: true
|
|
};
|
|
}
|
|
static getLatestRoundId() {
|
|
let roundId = -1;
|
|
if (rounds.length > 0) {
|
|
const highestRoundDisplayOrder = rounds.map(round => { return round[flagDisplayOrder]; })
|
|
.reduce((acc, cur) => Math.max(acc, cur), 0);
|
|
const filteredRounds = rounds.filter(round => round[flagDisplayOrder] == highestRoundDisplayOrder);
|
|
if (filteredRounds.length > 0) {
|
|
roundId = filteredRounds[0][attrRoundId];
|
|
}
|
|
console.log({ "method": "getLatestRoundId", highestRoundDisplayOrder, filteredRounds, roundId });
|
|
}
|
|
return roundId;
|
|
}
|
|
static getActiveRoundDisplayOrder() {
|
|
const roundDisplayOrderLabel = PageMtgGame.getRoundDisplayOrderLabel();
|
|
return parseInt(DOM.getElementValueCurrent(roundDisplayOrderLabel));
|
|
}
|
|
static getActiveRoundId() {
|
|
const currentRoundDisplayOrder = PageMtgGame.getActiveRoundDisplayOrder();
|
|
let roundId = 0;
|
|
if (rounds.length > 0) {
|
|
let filteredRounds = rounds.filter(round => round[flagDisplayOrder] == currentRoundDisplayOrder);
|
|
if (filteredRounds.length > 0) roundId = filteredRounds[0][attrRoundId];
|
|
console.log({ "method": "getActiveRoundId", filteredRounds, 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 activeRoundDisplayOrder = PageMtgGame.getActiveRoundDisplayOrder();
|
|
const previousRoundIds = rounds.filter(round => round[flagDisplayOrder] <= activeRoundDisplayOrder)
|
|
.map(round => round[attrRoundId]);
|
|
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
|
|
// && damage[attrRoundId] == roundId
|
|
&& previousRoundIds.includes(damage[attrRoundId])
|
|
))
|
|
.map(damage => damage[flagLifeLoss])
|
|
.reduce((acc, curr) => acc + curr, 0);
|
|
const isLethal = (totalDamage >= 21);
|
|
|
|
return `
|
|
<div class="damage-row" data-player-id="${playerId}" data-source-id="${sourceId}">
|
|
<span class="damage-source">from ${otherPlayerDisplayName}</span>
|
|
<div class="damage-controls">
|
|
<button class="damage-btn damage-minus" data-player-id="${playerId}" data-source-id="${sourceId}">−</button>
|
|
<input type="hidden" class="damage-value" data-player-id="${playerId}" data-source-id="${sourceId}" value="${totalDamage}">
|
|
<span class="damage-display ${isLethal ? 'lethal' : ''}" data-player-id="${playerId}" data-source-id="${sourceId}" ${attrValuePrevious}="${totalDamage}">${totalDamage}</span>
|
|
<button class="damage-btn damage-plus" data-player-id="${playerId}" data-source-id="${sourceId}">+</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
})
|
|
.join('');
|
|
}
|
|
|
|
hookupGameRoundEvents() {
|
|
let incrementRoundButtonSelector = '#gameSection .' + flagRow + '.' + flagRound + ' button.' + flagRoundDisplayOrderButton;
|
|
Events.hookupEventHandler("click", incrementRoundButtonSelector, (event, button) => {
|
|
const amount = button.classList.contains(flagRoundDisplayOrderPlus) ? 1 : -1;
|
|
const roundDisplayOrderButtonContainer = button.parentElement;
|
|
const roundDisplayOrderLabel = roundDisplayOrderButtonContainer.querySelector('span.' + flagRound + '.' + flagDisplayOrder);
|
|
const currentRoundDisplayOrder = parseInt(DOM.getElementValueCurrent(roundDisplayOrderLabel));
|
|
const newDisplayOrder = currentRoundDisplayOrder + amount;
|
|
DOM.setElementValueCurrent(roundDisplayOrderLabel, newDisplayOrder);
|
|
DOM.isElementDirty(roundDisplayOrderLabel);
|
|
this.updateAndToggleShowButtonsSaveCancel();
|
|
this.renderPlayers();
|
|
});
|
|
}
|
|
static makeDefaultGameRound(displayOrder) {
|
|
const newDisplayOrder = (displayOrder != null) ? displayOrder : 1 + Math.max(rounds.map(round => round[flagDisplayOrder]));
|
|
return {
|
|
[attrRoundId]: -newDisplayOrder
|
|
, [attrGameId]: gameId
|
|
, [flagNotes]: null
|
|
, [flagDisplayOrder]: newDisplayOrder
|
|
, [flagActive]: true
|
|
};
|
|
}
|
|
hookupPlayerCardEvents() {
|
|
// Life gain buttons
|
|
let lifeGainButtonSelector = '.life-gain-btn';
|
|
Events.hookupEventHandler("click", lifeGainButtonSelector, (event, button) => {
|
|
const playerId = parseInt(button.dataset.playerId);
|
|
const amount = parseInt(button.dataset.amount);
|
|
const activeRoundId = PageMtgGame.getActiveRoundId();
|
|
const damageIndex = damageRecords.findIndex(damage => (
|
|
damage[attrRoundId] == activeRoundId
|
|
&& damage[attrPlayerId] == playerId
|
|
&& damage[attrReceivedFromCommanderPlayerId] == null
|
|
));
|
|
this.changeLife(
|
|
playerId // playerId
|
|
, amount // amount
|
|
, true // isLifeGainNotLoss
|
|
, true // updateDamage
|
|
, damageIndex // damageIndex
|
|
);
|
|
});
|
|
|
|
// Life loss buttons
|
|
let lifeLossButtonSelector = '.life-loss-btn';
|
|
Events.hookupEventHandler("click", lifeLossButtonSelector, (event, button) => {
|
|
const playerId = parseInt(button.dataset.playerId);
|
|
const amount = parseInt(button.dataset.amount);
|
|
const activeRoundId = PageMtgGame.getActiveRoundId();
|
|
const damageIndex = damageRecords.findIndex(damage => (
|
|
damage[attrRoundId] == activeRoundId
|
|
&& damage[attrPlayerId] == playerId
|
|
&& damage[attrReceivedFromCommanderPlayerId] == null
|
|
));
|
|
this.changeLife(
|
|
playerId // playerId
|
|
, amount // amount
|
|
, false // isLifeGainNotLoss
|
|
, true // updateDamage
|
|
, damageIndex // damageIndex
|
|
);
|
|
});
|
|
|
|
// Commander death buttons
|
|
let commanderDeathButtonSelector = '.death-btn';
|
|
Events.hookupEventHandler("click", commanderDeathButtonSelector, (event, button) => {
|
|
const playerId = parseInt(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 = parseInt(button.dataset.playerId);
|
|
const sourceId = parseInt(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 = parseInt(button.dataset.playerId);
|
|
this.toggleEliminate(playerId);
|
|
});
|
|
}
|
|
|
|
changeLife(playerId, amount, isLifeGainNotLoss = false, 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 = currentLife + amount * ((isLifeGainNotLoss) ? 1 : -1);
|
|
|
|
DOM.setElementAttributeValueCurrent(lifeDisplay, newLife);
|
|
DOM.isElementDirty(lifeDisplay);
|
|
lifeInput.value = newLife;
|
|
lifeDisplay.textContent = newLife;
|
|
|
|
if (updateDamage) {
|
|
let fieldFlag = (isLifeGainNotLoss) ? flagLifeGain : flagLifeLoss;
|
|
damageRecords[damageIndex][fieldFlag] += amount;
|
|
}
|
|
|
|
PageMtgGame.renderCommanderDamageLog();
|
|
|
|
// 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 activeRoundId = PageMtgGame.getActiveRoundId();
|
|
const damageIndex = damageRecords.findIndex(damageRecord => (
|
|
damageRecord[attrRoundId] == activeRoundId
|
|
&& damageRecord[attrPlayerId] == playerId
|
|
&& damageRecord[attrReceivedFromCommanderPlayerId] == sourceId
|
|
));
|
|
damageRecords[damageIndex][flagLifeLoss] += amount;
|
|
|
|
let isLifeGainNotLoss = false;
|
|
this.changeLife(
|
|
playerId // playerId
|
|
, -amount // amount
|
|
, isLifeGainNotLoss // isLifeGainNotLoss
|
|
, 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 activeRoundId = PageMtgGame.getActiveRoundId();
|
|
const damageIndex = damageRecords.findIndex(damage => (
|
|
damage[attrRoundId] == activeRoundId
|
|
&& damage[attrPlayerId] == playerId
|
|
&& damage[attrReceivedFromCommanderPlayerId] == null
|
|
));
|
|
damageRecords[damageIndex][flagCommanderDeaths] = newDeaths;
|
|
|
|
PageMtgGame.renderCommanderDamageLog();
|
|
|
|
// 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 activeRoundId = PageMtgGame.getActiveRoundId();
|
|
const damageIndex = damageRecords.findIndex(damage => (
|
|
damage[attrRoundId] == activeRoundId
|
|
&& damage[attrPlayerId] == playerId
|
|
&& damage[attrReceivedFromCommanderPlayerId] == null
|
|
));
|
|
damageRecords[damageIndex][flagIsEliminated] = isEliminated;
|
|
|
|
DOM.setElementAttributeValueCurrent(eliminateBtn, isEliminated);
|
|
DOM.isElementDirty(eliminateBtn);
|
|
|
|
this.reorderPlayerCards();
|
|
PageMtgGame.renderCommanderDamageLog();
|
|
|
|
// PageMtgGame.debouncedSave();
|
|
this.updateAndToggleShowButtonsSaveCancel();
|
|
}
|
|
reorderPlayerCards() {
|
|
let playerGrid = document.getElementById('playersGrid');
|
|
let currentPlayerCards = playerGrid.querySelectorAll('.player-card');
|
|
let newPlayerCards = [];
|
|
let playerCardMetas = [];
|
|
currentPlayerCards.forEach((playerCard, index) => {
|
|
newPlayerCards.push(playerCard.cloneNode(true));
|
|
playerCardMetas.push({
|
|
[flagIsEliminated]: playerCard.classList.contains(flagIsEliminated)
|
|
, [attrPlayerId]: playerCard.dataset["playerId"]
|
|
, [flagDisplayOrder]: index
|
|
});
|
|
});
|
|
let activeRoundDisplayOrder = PageMtgGame.getActiveRoundDisplayOrder();
|
|
let newPlayerGridInnerHTML = '';
|
|
let playerIdA, playerIdB, isEliminatedAsIntA, isEliminatedAsIntB, playerA, playerB, indexPlayerCard;
|
|
playerCardMetas.sort((a, b) => {
|
|
playerIdA = a[attrPlayerId];
|
|
playerIdB = b[attrPlayerId];
|
|
isEliminatedAsIntA = PageMtgGame.isPlayerEliminated(playerIdA, activeRoundDisplayOrder) ? 1 : 0;
|
|
isEliminatedAsIntB = PageMtgGame.isPlayerEliminated(playerIdB, activeRoundDisplayOrder) ? 1 : 0;
|
|
playerA = players.filter(p => p[attrPlayerId] == playerIdA)[0];
|
|
playerB = players.filter(p => p[attrPlayerId] == playerIdB)[0];
|
|
return (
|
|
players.length * isEliminatedAsIntA
|
|
+ playerA[flagDisplayOrder]
|
|
) - (
|
|
players.length * isEliminatedAsIntB
|
|
+ playerB[flagDisplayOrder]
|
|
);
|
|
}).forEach((playerCardMeta) => {
|
|
indexPlayerCard = playerCardMeta[flagDisplayOrder];
|
|
newPlayerGridInnerHTML += newPlayerCards[indexPlayerCard].outerHTML;
|
|
});
|
|
|
|
playerGrid.innerHTML = newPlayerGridInnerHTML;
|
|
|
|
playerGrid.querySelectorAll('.' + flagInitialised).forEach((initialisedElement) => {
|
|
initialisedElement.classList.remove(flagInitialised);
|
|
});
|
|
|
|
this.hookupPlayerCardEvents();
|
|
}
|
|
static isPlayerEliminated(playerId, roundDisplayOrder = null) {
|
|
if (roundDisplayOrder == null) roundDisplayOrder = PageMtgGame.getActiveRoundDisplayOrder();
|
|
const filteredRoundIds = rounds.filter(round => round[flagDisplayOrder] <= roundDisplayOrder)
|
|
.map(round => round[attrRoundId]);
|
|
let hasDamageWithIsEliminated = damageRecords.filter(damage => (
|
|
// damage[attrRoundId] <= roundDisplayOrder
|
|
filteredRoundIds.includes(damage[attrRoundId])
|
|
&& damage[attrPlayerId] == playerId
|
|
&& damage[flagIsEliminated]
|
|
)).length > 0;
|
|
let damageFromOtherPlayers = {};
|
|
let otherPlayerId;
|
|
damageRecords.filter(damage => (
|
|
// damage[attrRoundId] <= roundId
|
|
filteredRoundIds.includes(damage[attrRoundId])
|
|
&& damage[attrPlayerId] == playerId
|
|
&& damage[attrReceivedFromCommanderPlayerId] != null
|
|
))
|
|
.forEach((damage) => {
|
|
otherPlayerId = damage[attrReceivedFromCommanderPlayerId];
|
|
damageFromOtherPlayers[otherPlayerId] =
|
|
damage[flagLifeLoss]
|
|
+ ((damageFromOtherPlayers[otherPlayerId] == null) ? 0 : damageFromOtherPlayers[otherPlayerId]);
|
|
});
|
|
let maxDamageFromOtherCommander = Object.keys(damageFromOtherPlayers)
|
|
.map((playerId) => damageFromOtherPlayers[playerId])
|
|
.reduce((acc, cur) => Math.max(acc, cur), 0);
|
|
let totalDamageTaken = damageRecords.filter(damage => (
|
|
// damage[attrRoundId] <= roundId
|
|
filteredRoundIds.includes(damage[attrRoundId])
|
|
&& damage[attrPlayerId] == playerId
|
|
))
|
|
.map((damage) => damage[flagLifeLoss] - damage[flagLifeGain])
|
|
.reduce((a, b) => a + b, 0);
|
|
console.log({ roundDisplayOrder, filteredRoundIds, hasDamageWithIsEliminated, maxDamageFromOtherCommander, totalDamageTaken });
|
|
return (
|
|
hasDamageWithIsEliminated
|
|
|| maxDamageFromOtherCommander >= 21
|
|
|| totalDamageTaken >= startingLife
|
|
);
|
|
}
|
|
|
|
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() || null : null; // `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 player damage';
|
|
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 player damages:', data[flagMessage]);
|
|
PageMtgGame.showError('An error occurred while saving player damages');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error saving player damages:', error);
|
|
PageMtgGame.showError('An error occurred while saving player damages');
|
|
})
|
|
.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;
|