Files
Werewolf/client/src/modules/game_state/states/InProgress.js
2023-04-02 00:00:33 -04:00

577 lines
26 KiB
JavaScript

import { toast } from '../../front_end_components/Toast.js';
import { globals } from '../../../config/globals.js';
import { HTMLFragments } from '../../front_end_components/HTMLFragments.js';
import { Confirmation } from '../../front_end_components/Confirmation.js';
import { ModalManager } from '../../front_end_components/ModalManager.js';
import { GameTimerManager } from '../../timer/GameTimerManager.js';
import { SharedStateUtil } from './shared/SharedStateUtil.js';
export class InProgress {
constructor (containerId, stateBucket, socket) {
this.stateBucket = stateBucket;
this.socket = socket;
this.container = document.getElementById(containerId);
this.killPlayerHandlers = {};
this.revealRoleHandlers = {};
this.transferModHandlers = {};
}
setUserView (userType) {
switch (userType) {
case globals.USER_TYPES.PLAYER:
this.container.innerHTML = HTMLFragments.PLAYER_GAME_VIEW;
this.renderPlayerView();
break;
case globals.USER_TYPES.KILLED_PLAYER:
this.container.innerHTML = HTMLFragments.PLAYER_GAME_VIEW;
this.renderPlayerView(true);
break;
case globals.USER_TYPES.MODERATOR:
document.getElementById('transfer-mod-prompt').innerHTML = HTMLFragments.TRANSFER_MOD_MODAL;
this.container.innerHTML = HTMLFragments.MODERATOR_GAME_VIEW;
this.renderModeratorView();
break;
case globals.USER_TYPES.TEMPORARY_MODERATOR:
document.getElementById('transfer-mod-prompt').innerHTML = HTMLFragments.TRANSFER_MOD_MODAL;
this.container.innerHTML = HTMLFragments.TEMP_MOD_GAME_VIEW;
this.renderTempModView();
break;
case globals.USER_TYPES.SPECTATOR:
this.container.innerHTML = HTMLFragments.SPECTATOR_GAME_VIEW;
this.renderSpectatorView();
break;
default:
break;
}
if (this.stateBucket.currentGameState.timerParams) {
this.socket.emit(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
globals.EVENT_IDS.GET_TIME_REMAINING,
this.stateBucket.currentGameState.accessCode
);
setTimeout(() => {
if (this.socket.hasListeners(globals.EVENT_IDS.GET_TIME_REMAINING)) {
document.getElementById('game-timer').innerText = 'Timer not found.';
document.getElementById('game-timer').classList.add('timer-error');
}
}, 15000);
} else {
document.querySelector('#game-timer')?.remove();
document.querySelector('#timer-container-moderator')?.remove();
document.querySelector('label[for="game-timer"]')?.remove();
}
const spectatorCount = this.container.querySelector('#spectator-count');
const spectatorHandler = (e) => {
if (e.type === 'click' || e.code === 'Enter') {
Confirmation(SharedStateUtil.buildSpectatorList(this.stateBucket.currentGameState.people
.filter(p => p.userType === globals.USER_TYPES.SPECTATOR)), null, true);
}
};
if (spectatorCount) {
SharedStateUtil.setNumberOfSpectators(
this.stateBucket.currentGameState.people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR).length,
spectatorCount
);
spectatorCount?.addEventListener('click', spectatorHandler);
spectatorCount?.addEventListener('keyup', spectatorHandler);
}
}
renderPlayerView (isKilled = false) {
if (isKilled) {
const clientUserType = document.getElementById('client-user-type');
if (clientUserType) {
clientUserType.innerText = globals.USER_TYPES.KILLED_PLAYER + ' \uD83D\uDC80';
}
}
renderPlayerRole(this.stateBucket.currentGameState);
displayCurrentModerator(this.stateBucket.currentGameState.people
.find((person) => person.userType === globals.USER_TYPES.MODERATOR
|| person.userType === globals.USER_TYPES.TEMPORARY_MODERATOR));
this.renderPlayersWithNoRoleInformationUnlessRevealed(false);
}
renderPlayersWithNoRoleInformationUnlessRevealed (tempMod = false) {
if (tempMod) {
this.removePlayerListEventListeners();
}
document.querySelectorAll('.game-player').forEach((el) => el.remove());
/* TODO: UX issue - it's easier to parse visually when players are sorted this way,
but shifting players around when they are killed or revealed is bad UX for the moderator. */
// sortPeopleByStatus(this.stateBucket.currentGameState.people);
const modType = tempMod
? this.stateBucket.currentGameState.people.find(person =>
person.id === this.stateBucket.currentGameState.currentModeratorId).userType
: null;
this.renderGroupOfPlayers(
this.stateBucket.currentGameState.people.filter(
p => (p.userType !== globals.USER_TYPES.MODERATOR && p.userType !== globals.USER_TYPES.SPECTATOR)
|| p.killed
),
this.killPlayerHandlers,
this.revealRoleHandlers,
this.stateBucket.currentGameState.accessCode,
null,
modType,
this.socket
);
document.getElementById('players-alive-label').innerText =
'Players: ' + this.stateBucket.currentGameState.people.filter((person) => !person.out).length + ' / ' +
this.stateBucket.currentGameState.gameSize + ' Alive';
}
removePlayerListEventListeners (removeEl = true) {
document.querySelectorAll('.game-player').forEach((el) => {
const pointer = el.dataset.pointer;
if (pointer && this.killPlayerHandlers[pointer]) {
el.removeEventListener('click', this.killPlayerHandlers[pointer]);
delete this.killPlayerHandlers[pointer];
}
if (pointer && this.revealRoleHandlers[pointer]) {
el.removeEventListener('click', this.revealRoleHandlers[pointer]);
delete this.revealRoleHandlers[pointer];
}
if (removeEl) {
el.remove();
}
});
}
renderModeratorView () {
createEndGamePromptComponent(this.socket, this.stateBucket);
const modTransferButton = document.getElementById('mod-transfer-button');
modTransferButton.addEventListener(
'click', () => {
this.displayAvailableModerators();
ModalManager.displayModal(
'transfer-mod-modal',
'transfer-mod-modal-background',
'close-mod-transfer-modal-button'
);
}
);
this.renderPlayersWithRoleAndAlignmentInfo();
}
renderTempModView () {
createEndGamePromptComponent(this.socket, this.stateBucket);
renderPlayerRole(this.stateBucket.currentGameState);
this.renderPlayersWithNoRoleInformationUnlessRevealed(true);
}
renderSpectatorView () {
displayCurrentModerator(this.stateBucket.currentGameState.people
.find((person) => person.userType === globals.USER_TYPES.MODERATOR
|| person.userType === globals.USER_TYPES.TEMPORARY_MODERATOR));
this.renderPlayersWithNoRoleInformationUnlessRevealed();
}
setSocketHandlers () {
this.socket.on(globals.EVENT_IDS.KILL_PLAYER, (killedPlayer) => {
this.stateBucket.currentGameState.people = this.stateBucket.currentGameState.people
.map(person => person.id === killedPlayer.id ? killedPlayer : person);
if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) {
toast(killedPlayer.name + ' killed.', 'success', true, true, 'medium');
this.renderPlayersWithRoleAndAlignmentInfo(this.stateBucket.currentGameState.status === globals.STATUS.ENDED);
} else {
if (killedPlayer.id === this.stateBucket.currentGameState.client.id) {
const clientUserType = document.getElementById('client-user-type');
if (clientUserType) {
clientUserType.innerText = globals.USER_TYPES.KILLED_PLAYER + ' \uD83D\uDC80';
}
this.updatePlayerCardToKilledState();
toast('You have been killed!', 'warning', true, true, 'medium');
} else {
toast(killedPlayer.name + ' was killed!', 'warning', true, true, 'medium');
if (killedPlayer.userType === globals.USER_TYPES.MODERATOR) {
displayCurrentModerator(killedPlayer);
}
}
if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
this.removePlayerListEventListeners(false);
} else {
this.renderPlayersWithNoRoleInformationUnlessRevealed(false);
}
}
});
this.socket.on(globals.EVENT_IDS.REVEAL_PLAYER, (revealData) => {
const revealedPerson = this.stateBucket.currentGameState.people.find((person) => person.id === revealData.id);
if (revealedPerson) {
revealedPerson.revealed = true;
revealedPerson.gameRole = revealData.gameRole;
revealedPerson.alignment = revealData.alignment;
if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) {
if (revealedPerson.id === this.stateBucket.currentGameState.client.id) {
toast('You revealed your role.', 'success', true, true, 'medium');
} else {
toast(revealedPerson.name + ' revealed.', 'success', true, true, 'medium');
}
this.renderPlayersWithRoleAndAlignmentInfo(this.stateBucket.currentGameState.status === globals.STATUS.ENDED);
} else {
if (revealedPerson.id === this.stateBucket.currentGameState.client.id) {
toast('Your role has been revealed!', 'warning', true, true, 'medium');
} else {
toast(revealedPerson.name + ' was revealed as a ' + revealedPerson.gameRole + '!', 'warning', true, true, 'medium');
}
if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
this.renderPlayersWithNoRoleInformationUnlessRevealed(true);
} else {
this.renderPlayersWithNoRoleInformationUnlessRevealed(false);
}
}
}
});
this.socket.on(globals.EVENT_IDS.ADD_SPECTATOR, (spectator) => {
this.stateBucket.currentGameState.people.push(spectator);
SharedStateUtil.setNumberOfSpectators(
this.stateBucket.currentGameState.people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR).length,
document.getElementById('spectator-count')
);
if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR
|| this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
this.displayAvailableModerators();
}
});
if (this.stateBucket.currentGameState.timerParams) {
if (this.stateBucket.timerWorker) {
this.stateBucket.timerWorker.terminate();
this.stateBucket.timerWorker = null;
}
this.stateBucket.timerWorker = new Worker(new URL('../../timer/Timer.js', import.meta.url));
const gameTimerManager = new GameTimerManager(this.stateBucket, this.socket);
gameTimerManager.attachTimerSocketListeners(this.socket, this.stateBucket.timerWorker);
}
}
renderPlayersWithRoleAndAlignmentInfo () {
removeExistingPlayerElements(this.killPlayerHandlers, this.revealRoleHandlers);
this.stateBucket.currentGameState.people.sort((a, b) => {
return a.name >= b.name ? 1 : -1;
});
const teamGood = this.stateBucket.currentGameState.people.filter(
(p) => p.alignment === globals.ALIGNMENT.GOOD
&& ((p.userType !== globals.USER_TYPES.MODERATOR && p.userType !== globals.USER_TYPES.SPECTATOR)
|| p.killed)
);
const teamEvil = this.stateBucket.currentGameState.people.filter((p) => p.alignment === globals.ALIGNMENT.EVIL
&& ((p.userType !== globals.USER_TYPES.MODERATOR && p.userType !== globals.USER_TYPES.SPECTATOR)
|| p.killed)
);
this.renderGroupOfPlayers(
teamEvil,
this.killPlayerHandlers,
this.revealRoleHandlers,
this.stateBucket.currentGameState.accessCode,
globals.ALIGNMENT.EVIL,
this.stateBucket.currentGameState.people.find(person =>
person.id === this.stateBucket.currentGameState.currentModeratorId).userType,
this.socket
);
this.renderGroupOfPlayers(
teamGood,
this.killPlayerHandlers,
this.revealRoleHandlers,
this.stateBucket.currentGameState.accessCode,
globals.ALIGNMENT.GOOD,
this.stateBucket.currentGameState.people.find(person =>
person.id === this.stateBucket.currentGameState.currentModeratorId).userType,
this.socket
);
document.getElementById('players-alive-label').innerText =
'Players: ' + this.stateBucket.currentGameState.people.filter((person) => !person.out).length + ' / ' +
this.stateBucket.currentGameState.gameSize + ' Alive';
}
renderGroupOfPlayers (
people,
killPlayerHandlers,
revealRoleHandlers,
accessCode = null,
alignment = null,
moderatorType,
socket = null
) {
for (const player of people) {
const playerEl = document.createElement('div');
playerEl.classList.add('game-player');
// add a reference to the player's id for each corresponding element in the list
if (moderatorType) {
playerEl.dataset.pointer = player.id;
playerEl.innerHTML = HTMLFragments.MODERATOR_PLAYER;
} else {
playerEl.innerHTML = HTMLFragments.GAME_PLAYER;
}
playerEl.querySelector('.game-player-name').innerText = player.name;
const roleElement = playerEl.querySelector('.game-player-role');
// Add role/alignment indicators if necessary
if (moderatorType === globals.USER_TYPES.MODERATOR || player.revealed) {
if (alignment === null) {
roleElement.classList.add(player.alignment);
} else {
roleElement.classList.add(alignment);
}
roleElement.innerText = player.gameRole;
} else {
roleElement.innerText = 'Role Unknown';
}
// Change element based on player's in/out status
if (player.out) {
playerEl.classList.add('killed');
if (moderatorType) {
playerEl.querySelector('.kill-player-button')?.remove();
insertPlaceholderButton(playerEl, false, 'killed');
}
} else if (!player.out && moderatorType) {
killPlayerHandlers[player.id] = () => {
Confirmation('Kill \'' + player.name + '\'?', () => {
if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
socket.emit(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, globals.EVENT_IDS.ASSIGN_DEDICATED_MOD, accessCode, { personId: player.id });
} else {
socket.emit(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, globals.EVENT_IDS.KILL_PLAYER, accessCode, { personId: player.id });
}
});
};
playerEl.querySelector('.kill-player-button').addEventListener('click', killPlayerHandlers[player.id]);
}
// change element based on player's revealed/unrevealed status
if (player.revealed) {
if (moderatorType) {
playerEl.querySelector('.reveal-role-button')?.remove();
insertPlaceholderButton(playerEl, true, 'revealed');
}
} else if (!player.revealed && moderatorType) {
revealRoleHandlers[player.id] = () => {
Confirmation('Reveal \'' + player.name + '\'?', () => {
socket.emit(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, globals.EVENT_IDS.REVEAL_PLAYER, accessCode, { personId: player.id });
});
};
playerEl.querySelector('.reveal-role-button').addEventListener('click', revealRoleHandlers[player.id]);
}
const playerListContainerId = moderatorType === globals.USER_TYPES.MODERATOR
? 'player-list-moderator-team-' + alignment
: 'game-player-list';
document.getElementById(playerListContainerId).appendChild(playerEl);
}
}
displayAvailableModerators () {
document.getElementById('transfer-mod-modal-content').innerText = '';
document.querySelectorAll('.potential-moderator').forEach((el) => {
const pointer = el.dataset.pointer;
if (pointer && this.transferModHandlers[pointer]) {
el.removeEventListener('click', this.transferModHandlers[pointer]);
delete this.transferModHandlers[pointer];
}
el.remove();
});
renderPotentialMods(
this.stateBucket.currentGameState,
this.stateBucket.currentGameState.people,
this.transferModHandlers,
this.socket
);
if (document.querySelectorAll('.potential-moderator').length === 0) {
document.getElementById('transfer-mod-modal-content').innerText =
'There is nobody available to transfer to. Only spectators or killed players (who are not bots) can be mods.';
}
}
updatePlayerCardToKilledState () {
document.querySelector('#role-image').classList.add('killed-card');
document.getElementById('role-image').setAttribute(
'src',
'../images/tombstone.png'
);
}
}
function renderPlayerRole (gameState) {
const name = document.querySelector('#role-name');
name.innerText = gameState.client.gameRole;
if (gameState.client.alignment === globals.ALIGNMENT.GOOD) {
document.getElementById('game-role').classList.add('game-role-good');
name.classList.add('good');
} else {
document.getElementById('game-role').classList.add('game-role-evil');
name.classList.add('evil');
}
name.setAttribute('title', gameState.client.gameRole);
if (gameState.client.out) {
document.querySelector('#role-image').classList.add('killed-card');
document.getElementById('role-image').setAttribute(
'src',
'../images/tombstone.png'
);
} else {
if (gameState.client.gameRole.toLowerCase() === 'villager') {
document.getElementById('role-image').setAttribute(
'src',
'../images/roles/Villager' + Math.ceil(Math.random() * 2) + '.png'
);
} else {
if (gameState.client.customRole) {
document.getElementById('role-image').setAttribute(
'src',
'../images/roles/custom-role.svg'
);
} else {
document.getElementById('role-image').setAttribute(
'src',
'../images/roles/' + gameState.client.gameRole.replaceAll(' ', '') + '.png'
);
}
}
}
document.querySelector('#role-description').innerText = gameState.client.gameRoleDescription;
const roleBackHandler = (e) => {
if (e.type === 'dblclick' || e.code === 'Enter') {
document.getElementById('game-role').style.display = 'flex';
document.getElementById('game-role-back').style.display = 'none';
}
};
const roleFrontHandler = (e) => {
if (e.type === 'dblclick' || e.code === 'Enter') {
document.getElementById('game-role-back').style.display = 'flex';
document.getElementById('game-role').style.display = 'none';
}
};
document.getElementById('game-role-back').addEventListener('dblclick', roleBackHandler);
document.getElementById('game-role-back').addEventListener('keyup', roleBackHandler);
document.getElementById('game-role').addEventListener('dblclick', roleFrontHandler);
document.getElementById('game-role').addEventListener('keyup', roleFrontHandler);
}
function removeExistingPlayerElements (killPlayerHandlers, revealRoleHandlers) {
document.querySelectorAll('.game-player').forEach((el) => {
const pointer = el.dataset.pointer;
if (pointer && killPlayerHandlers[pointer]) {
el.removeEventListener('click', killPlayerHandlers[pointer]);
delete killPlayerHandlers[pointer];
}
if (pointer && revealRoleHandlers[pointer]) {
el.removeEventListener('click', revealRoleHandlers[pointer]);
delete revealRoleHandlers[pointer];
}
el.remove();
});
}
function displayCurrentModerator (moderator) {
document.getElementById('current-moderator-name').innerText = moderator.name;
document.getElementById('current-moderator-type').innerText = moderator.userType + globals.USER_TYPE_ICONS[moderator.userType];
}
function createEndGamePromptComponent (socket, stateBucket) {
if (document.querySelector('#game-control-prompt') === null) {
const div = document.createElement('div');
div.innerHTML = HTMLFragments.GAME_CONTROL_PROMPT;
div.querySelector('#end-game-button').addEventListener('click', (e) => {
e.preventDefault();
Confirmation('End the game?', () => {
socket.emit(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
globals.EVENT_IDS.END_GAME,
stateBucket.currentGameState.accessCode,
null,
() => {
document.querySelector('#game-control-prompt')?.remove();
}
);
});
});
div.querySelector('#game-control-prompt').prepend(SharedStateUtil.createRestartButton(stateBucket));
document.getElementById('game-content').appendChild(div);
}
}
function insertPlaceholderButton (container, append, type) {
const button = document.createElement('div');
button.classList.add('placeholder-button');
if (type === 'killed') {
button.innerText = 'Killed';
} else {
button.innerText = 'Revealed';
}
if (append) {
container.querySelector('.player-action-buttons').appendChild(button);
} else {
container.querySelector('.player-action-buttons').prepend(button);
}
}
function renderPotentialMods (gameState, group, transferModHandlers, socket) {
const modalContent = document.getElementById('transfer-mod-modal-content');
for (const member of group) {
if ((member.userType === globals.USER_TYPES.KILLED_PLAYER || member.userType === globals.USER_TYPES.SPECTATOR) && !(member.id === gameState.client.id)) {
const container = document.createElement('div');
container.classList.add('potential-moderator');
container.setAttribute('tabindex', '0');
container.dataset.pointer = member.id;
container.innerHTML =
'<div class=\'potential-mod-name\'></div>' +
'<div>' + member.userType + ' ' + globals.USER_TYPE_ICONS[member.userType] + ' </div>';
container.querySelector('.potential-mod-name').innerText = member.name;
transferModHandlers[member.id] = (e) => {
if (e.type === 'click' || e.code === 'Enter') {
ModalManager.dispelModal('transfer-mod-modal', 'transfer-mod-modal-background');
Confirmation('Transfer moderator powers to \'' + member.name + '\'?', () => {
const transferPrompt = document.getElementById('transfer-mod-prompt');
if (transferPrompt !== null) {
transferPrompt.innerHTML = '';
}
socket.timeout(5000).emit(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
globals.EVENT_IDS.TRANSFER_MODERATOR,
gameState.accessCode,
{ personId: member.id },
(err) => {
if (err) {
console.error(err);
socket.emit(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
globals.EVENT_IDS.FETCH_GAME_STATE,
gameState.accessCode,
{ personId: gameState.client.cookie },
(gameState) => {
SharedStateUtil.gameStateAckFn(gameState, socket);
}
);
}
}
);
});
}
};
container.addEventListener('click', transferModHandlers[member.id]);
container.addEventListener('keyup', transferModHandlers[member.id]);
modalContent.appendChild(container);
}
}
}