|
const $ = s => document.querySelector(s); |
|
const $$ = s => Array.from(document.querySelectorAll(s)); |
|
|
|
const randFrom = arr => arr[Math.floor(Math.random() * arr.length)] |
|
|
|
const elementFactory = (tag, props={}, attrs={}, children=[], custom=(elem) => undefined) => { |
|
const elem = document.createElement(tag) |
|
Object.entries({ attrs, props }).forEach(([type, obj]) => |
|
Object.entries(obj).forEach(([key, value]) => type === 'attrs' |
|
? elem.setAttribute(key, value) |
|
: elem[key] = value |
|
)); |
|
|
|
elem.appendChild(children.reduce((frag, child) => { |
|
frag.appendChild(child); |
|
return frag; |
|
}, document.createDocumentFragment())); |
|
custom(elem); |
|
return elem; |
|
} |
|
|
|
const WORD_BANK = [ |
|
['Mountain', 'Everest'], |
|
['Rascal Two', 'Yours truly!'], |
|
['DevWars', '#1 Game show'], |
|
['Mambadev', 'Best Dev'], |
|
['Android', '#1 Mobile OS'], |
|
['VSCode', '#1 Editor'], |
|
['GitHub', 'Code Collab'], |
|
['CSS', 'Make things pretty!'], |
|
['JavaScript', 'Make things functional!'], |
|
['Java', 'Not JavaScript'], |
|
['COVID-19', 'Destroyer of Mankind'], |
|
['1969', 'Internet Birth-Year'], |
|
['Hangman', 'This'] |
|
].map(([word, clue]) => [word.toUpperCase(), clue]); |
|
const MAX_LIVES = 5; |
|
|
|
|
|
let playedWords = []; |
|
let currentWC |
|
let hitLetters = []; |
|
let consecWins = 0; |
|
|
|
|
|
let indentRows = window.innerWidth > 450 |
|
|
|
const KEYBOARD_ROW_CHARS = [ |
|
'1234567890-', |
|
'qwertyuiop', |
|
'asdfghjkl', |
|
'zxcvbnm' |
|
] |
|
$('#keyboard').appendChild(KEYBOARD_ROW_CHARS.reduce((frag, row, index) => { |
|
frag.appendChild(row.split('').map(char => char.toUpperCase()).reduce((rowFrag, char) => { |
|
rowFrag.append(elementFactory('button', { className: 'key', textContent: char, onclick: e => handleKeyClick(e.target, char) })) |
|
return rowFrag; |
|
}, elementFactory('span', { className: 'row' }, { style: indentRows ? `margin-left: ${index * 1}em` : ''}))); |
|
return frag |
|
}, document.createDocumentFragment())); |
|
|
|
const start = () => { |
|
if (WORD_BANK.length == playedWords.length) playedWords = []; |
|
|
|
$$('.key').forEach(key => key.classList.remove('right', 'wrong') || key.removeAttribute('disabled')); |
|
|
|
while (true){ |
|
var [word, clue] = randFrom(WORD_BANK); |
|
if (!playedWords.includes(word)) break; |
|
} |
|
playedWords.push(word); |
|
currentWC = { word, clue }; |
|
|
|
const clueHolder = $('#clue'); |
|
clueHolder.dataset.clue = clue; |
|
clueHolder.textContent = '?\u20DD'; |
|
|
|
const wordHolder = $('#word'); |
|
wordHolder.innerHTML = '' |
|
wordHolder.appendChild(word.split('').reduce((frag, char) => { |
|
frag.appendChild(elementFactory('span', { className: 'letter' + (char === ' ' ? ' space' : ''), innerHTML: ' ' }, {}, [], letter => letter.dataset.char = char)) |
|
return frag; |
|
}, document.createDocumentFragment())); |
|
if (!lives.lives) lives.changeLives(+1000); |
|
} |
|
|
|
const clicky = new Audio('https://www.fesliyanstudios.com/play-mp3/642') |
|
const handleKeyClick = async (key, char) => { |
|
if (key.disabled) return; |
|
key.disabled = true; |
|
|
|
//clicky.currentTime = 0.375; |
|
clicky.play() |
|
|
|
const hit = currentWC.word.includes(char.toUpperCase()) |
|
key.classList.add(hit ? 'right' : 'wrong'); |
|
if (hit){ |
|
hitLetters.push(char); |
|
const letters = $$('.letter') |
|
letters.filter(letter => letter.dataset.char == char).map(letter => letter.textContent = char || letter) |
|
const visibleLetters = letters.filter(letter => letter.textContent == letter.dataset.char || letter.dataset.char === ' ') |
|
if (visibleLetters.length != currentWC.word.length) return; |
|
await showMessage(`You won the round! |
|
The word was "${currentWC.word}"`, true) |
|
consecWins += 1 |
|
if (consecWins === 3) await showMessage('You won the whole game!', true) || lives.changeLives(+1000); |
|
start(); |
|
} |
|
else if (lives.changeLives(-1) == 0){ |
|
await showMessage(`You lost! |
|
The word was "${currentWC.word}"`, true); |
|
consecWins = 0; |
|
start(); |
|
} |
|
} |
|
|
|
$('#clue').addEventListener('click', e => { |
|
const clue = e.target; |
|
if (!clue.dataset.clue) return; |
|
clue.textContent = clue.dataset.clue == clue.textContent ? '?\u20DD' : clue.dataset.clue |
|
}) |
|
|
|
|
|
|
|
const waitFor = check => new Promise(async r => { |
|
while (!check()){ |
|
await new Promise(rt => setTimeout(rt, 250)); |
|
} |
|
r() |
|
}) |
|
|
|
const showMessage = (text, blocking=false) => { |
|
const bin = $('#recycle-bin'); |
|
bin.classList.add('message'); |
|
bin.dataset.messageText = text |
|
const fade = blocking && $('#fade'); |
|
if (blocking) fade.classList.add('active'); |
|
bin.addEventListener('click', hideMessage.bind(null, bin, fade)) |
|
return waitFor(() => !bin.classList.contains('message')).then(() => console.log('done')) |
|
} |
|
|
|
const messageIsShowing = () => $('#recycle-bin').classList.contains('message'); |
|
|
|
const hideMessage = (bin, fade) => { |
|
bin.classList.remove('message'); |
|
bin.removeEventListener('click', hideMessage); |
|
if (fade) fade.classList.remove('active') |
|
} |
|
|
|
|
|
window.addEventListener('resize', () => { |
|
indentRows = window.innerWidth > 450; |
|
$$('.row').forEach((row, index) => row.style = indentRows ? `margin-left: ${index * 1}em` : ''); |
|
lives.updateDisplay(); |
|
}) |
|
|
|
window.addEventListener('keyup', e => { |
|
const { key } = e; |
|
|
|
if (messageIsShowing()){ |
|
if (['Enter', 'Escape'].includes(key)) $('#recycle-bin').click(); |
|
return; |
|
} |
|
|
|
if (KEYBOARD_ROW_CHARS.some(row => row.includes(key.toLowerCase()))){ |
|
const elem = document.evaluate(`//*[@class='key' and text()='${key.toUpperCase()}']`, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue |
|
if (!elem || elem.disabled) return; |
|
elem.click(); |
|
} |
|
|
|
if (key === '?') $('#clue').click(); |
|
}); |
|
|
|
|
|
|
|
class LifeCounter{ |
|
constructor(maximum){ |
|
this.sys32 = $('#system32'); |
|
this.recy = $('#recycle-bin') |
|
|
|
this.lives = maximum; |
|
this.maximum = maximum; |
|
|
|
this.updateDisplay(); |
|
} |
|
|
|
setPercentage(percent){ |
|
this.sys32.style.left = (80 * percent * 0.01 + 5) + 'vw'; |
|
this.sys32.style.transform = percent == 100 ? 'scale(0) rotate(360deg)' : ''; |
|
} |
|
|
|
updateDisplay(){ |
|
this.sys32.dataset.text = '❤️'.repeat(this.lives) |
|
this.recy.dataset.text = 'Lives' |
|
this.setPercentage(100 - (this.lives / this.maximum * 100)) |
|
} |
|
|
|
changeLives(change){ |
|
this.lives += change; |
|
if (this.lives < 0) this.lives = 0; |
|
if (this.lives > this.maximum) this.lives = this.maximum |
|
this.updateDisplay(); |
|
return this.lives |
|
} |
|
} |
|
|
|
const lives = new LifeCounter(MAX_LIVES); |
|
|
|
|
|
start(); |