352 lines
9.3 KiB
JavaScript
352 lines
9.3 KiB
JavaScript
// Global variables
|
|
let targetWords = []
|
|
let dictionary = []
|
|
let targetWord = ""
|
|
|
|
const WORD_LENGTH = 5
|
|
const FLIP_ANIMATION_DURATION = 500
|
|
const DANCE_ANIMATION_DURATION = 500
|
|
|
|
const keyboard = document.querySelector("[data-keyboard]")
|
|
const alertContainer = document.querySelector("[data-alert-container]")
|
|
const guessGrid = document.querySelector("[data-guess-grid]")
|
|
|
|
// Initialize the game by loading JSON data
|
|
async function initializeGame() {
|
|
try {
|
|
// Load both JSON files
|
|
const [targetWordsResponse, dictionaryResponse] = await Promise.all([
|
|
fetch('targetWords.json'),
|
|
fetch('dictionary.json')
|
|
])
|
|
|
|
// Check if responses are ok
|
|
if (!targetWordsResponse.ok) {
|
|
throw new Error(`Failed to load targetWords.json: ${targetWordsResponse.status}`)
|
|
}
|
|
if (!dictionaryResponse.ok) {
|
|
throw new Error(`Failed to load dictionary.json: ${dictionaryResponse.status}`)
|
|
}
|
|
|
|
// Parse JSON data
|
|
targetWords = await targetWordsResponse.json()
|
|
dictionary = await dictionaryResponse.json()
|
|
|
|
// Validate data
|
|
if (!Array.isArray(targetWords) || targetWords.length === 0) {
|
|
throw new Error('Target words must be a non-empty array')
|
|
}
|
|
if (!Array.isArray(dictionary) || dictionary.length === 0) {
|
|
throw new Error('Dictionary must be a non-empty array')
|
|
}
|
|
|
|
// Select random target word
|
|
targetWord = targetWords[Math.floor(Math.random() * targetWords.length)]
|
|
|
|
// Start the game
|
|
startInteraction()
|
|
|
|
console.log('Game initialized successfully!')
|
|
console.log(`Target word selected from ${targetWords.length} possible words`)
|
|
console.log(`Dictionary loaded with ${dictionary.length} words`)
|
|
|
|
} catch (error) {
|
|
console.error('Failed to initialize game:', error)
|
|
showAlert(`Error loading game data: ${error.message}`, 5000)
|
|
}
|
|
}
|
|
|
|
// Alternative initialization with date-based word selection (uncomment if needed)
|
|
/*
|
|
async function initializeGameWithDate() {
|
|
try {
|
|
const [targetWordsResponse, dictionaryResponse] = await Promise.all([
|
|
fetch('targetWords.json'),
|
|
fetch('dictionary.json')
|
|
])
|
|
|
|
if (!targetWordsResponse.ok || !dictionaryResponse.ok) {
|
|
throw new Error('Failed to load game data files')
|
|
}
|
|
|
|
targetWords = await targetWordsResponse.json()
|
|
dictionary = await dictionaryResponse.json()
|
|
|
|
// Date-based word selection
|
|
const offsetFromDate = new Date(2022, 0, 1)
|
|
const msOffset = Date.now() - offsetFromDate
|
|
const dayOffset = Math.floor(msOffset / 1000 / 60 / 60 / 24)
|
|
targetWord = targetWords[dayOffset % targetWords.length]
|
|
|
|
startInteraction()
|
|
|
|
} catch (error) {
|
|
console.error('Failed to initialize game:', error)
|
|
showAlert(`Error loading game data: ${error.message}`, 5000)
|
|
}
|
|
}
|
|
*/
|
|
|
|
function startInteraction() {
|
|
document.addEventListener("click", handleMouseClick)
|
|
document.addEventListener("keydown", handleKeyPress)
|
|
}
|
|
|
|
function stopInteraction() {
|
|
document.removeEventListener("click", handleMouseClick)
|
|
document.removeEventListener("keydown", handleKeyPress)
|
|
}
|
|
|
|
function handleMouseClick(e) {
|
|
if (e.target.matches("[data-key]")) {
|
|
pressKey(e.target.dataset.key)
|
|
return
|
|
}
|
|
|
|
if (e.target.matches("[data-enter]")) {
|
|
submitGuess()
|
|
return
|
|
}
|
|
|
|
if (e.target.matches("[data-delete]")) {
|
|
deleteKey()
|
|
return
|
|
}
|
|
}
|
|
|
|
function handleKeyPress(e) {
|
|
if (e.key === "Enter") {
|
|
submitGuess()
|
|
return
|
|
}
|
|
|
|
if (e.key === "Backspace" || e.key === "Delete") {
|
|
deleteKey()
|
|
return
|
|
}
|
|
|
|
if (e.key.match(/^[a-z]$/)) {
|
|
pressKey(e.key)
|
|
return
|
|
}
|
|
}
|
|
|
|
function pressKey(key) {
|
|
const activeTiles = getActiveTiles()
|
|
if (activeTiles.length >= WORD_LENGTH) return
|
|
const nextTile = guessGrid.querySelector(":not([data-letter])")
|
|
nextTile.dataset.letter = key.toLowerCase()
|
|
nextTile.textContent = key
|
|
nextTile.dataset.state = "active"
|
|
}
|
|
|
|
function deleteKey() {
|
|
const activeTiles = getActiveTiles()
|
|
const lastTile = activeTiles[activeTiles.length - 1]
|
|
if (lastTile == null) return
|
|
lastTile.textContent = ""
|
|
delete lastTile.dataset.state
|
|
delete lastTile.dataset.letter
|
|
}
|
|
|
|
function submitGuess() {
|
|
const activeTiles = [...getActiveTiles()]
|
|
if (activeTiles.length !== WORD_LENGTH) {
|
|
showAlert("Not enough letters, Sara")
|
|
shakeTiles(activeTiles)
|
|
return
|
|
}
|
|
|
|
const guess = activeTiles.reduce((word, tile) => {
|
|
return word + tile.dataset.letter
|
|
}, "")
|
|
|
|
if (!dictionary.includes(guess)) {
|
|
showAlert("Not in dictionary")
|
|
shakeTiles(activeTiles)
|
|
return
|
|
}
|
|
|
|
stopInteraction()
|
|
activeTiles.forEach((...params) => flipTile(...params, guess))
|
|
}
|
|
|
|
function flipTile(tile, index, array, guess) {
|
|
const letter = tile.dataset.letter
|
|
const key = keyboard.querySelector(`[data-key="${letter}"i]`)
|
|
setTimeout(() => {
|
|
tile.classList.add("flip")
|
|
}, (index * FLIP_ANIMATION_DURATION) / 2)
|
|
|
|
tile.addEventListener(
|
|
"transitionend",
|
|
() => {
|
|
tile.classList.remove("flip")
|
|
if (targetWord[index] === letter) {
|
|
tile.dataset.state = "correct"
|
|
key.classList.add("correct")
|
|
} else if (targetWord.includes(letter)) {
|
|
tile.dataset.state = "wrong-position"
|
|
key.classList.add("wrong-position")
|
|
} else {
|
|
tile.dataset.state = "wrong"
|
|
key.classList.add("wrong")
|
|
}
|
|
|
|
if (index === array.length - 1) {
|
|
tile.addEventListener(
|
|
"transitionend",
|
|
() => {
|
|
startInteraction()
|
|
checkWinLose(guess, array)
|
|
},
|
|
{ once: true }
|
|
)
|
|
}
|
|
},
|
|
{ once: true }
|
|
)
|
|
}
|
|
|
|
function getActiveTiles() {
|
|
return guessGrid.querySelectorAll('[data-state="active"]')
|
|
}
|
|
|
|
function showAlert(message, duration = 1000) {
|
|
const alert = document.createElement("div")
|
|
alert.textContent = message
|
|
alert.classList.add("alert")
|
|
alertContainer.prepend(alert)
|
|
if (duration == null) return
|
|
|
|
setTimeout(() => {
|
|
alert.classList.add("hide")
|
|
alert.addEventListener("transitionend", () => {
|
|
alert.remove()
|
|
})
|
|
}, duration)
|
|
}
|
|
|
|
function shakeTiles(tiles) {
|
|
tiles.forEach(tile => {
|
|
tile.classList.add("shake")
|
|
tile.addEventListener(
|
|
"animationend",
|
|
() => {
|
|
tile.classList.remove("shake")
|
|
},
|
|
{ once: true }
|
|
)
|
|
})
|
|
}
|
|
|
|
function checkWinLose(guess, tiles) {
|
|
if (guess === targetWord) {
|
|
showAlert("Congratulations Sara, You win!", 5000)
|
|
danceTiles(tiles)
|
|
celebrateWithConfetti()
|
|
stopInteraction()
|
|
return
|
|
}
|
|
|
|
const remainingTiles = guessGrid.querySelectorAll(":not([data-letter])")
|
|
if (remainingTiles.length === 0) {
|
|
showAlert(targetWord.toUpperCase(), null)
|
|
stopInteraction()
|
|
}
|
|
}
|
|
// Confetti celebration function
|
|
function celebrateWithConfetti() {
|
|
// Initial burst from the center
|
|
confetti({
|
|
particleCount: 100,
|
|
spread: 70,
|
|
origin: { y: 0.6 }
|
|
})
|
|
|
|
// Side cannons effect
|
|
setTimeout(() => {
|
|
confetti({
|
|
particleCount: 50,
|
|
angle: 60,
|
|
spread: 55,
|
|
origin: { x: 0 }
|
|
})
|
|
confetti({
|
|
particleCount: 50,
|
|
angle: 120,
|
|
spread: 55,
|
|
origin: { x: 1 }
|
|
})
|
|
}, 200)
|
|
|
|
// Stars effect
|
|
setTimeout(() => {
|
|
confetti({
|
|
particleCount: 30,
|
|
spread: 360,
|
|
startVelocity: 30,
|
|
decay: 0.9,
|
|
scalar: 1.2,
|
|
shapes: ['star'],
|
|
colors: ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffeaa7']
|
|
})
|
|
}, 400)
|
|
|
|
// Final cascade
|
|
setTimeout(() => {
|
|
confetti({
|
|
particleCount: 200,
|
|
spread: 100,
|
|
origin: { y: 0.4 },
|
|
colors: ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffeaa7', '#dda0dd']
|
|
})
|
|
}, 600)
|
|
}
|
|
function danceTiles(tiles) {
|
|
tiles.forEach((tile, index) => {
|
|
setTimeout(() => {
|
|
tile.classList.add("dance")
|
|
tile.addEventListener(
|
|
"animationend",
|
|
() => {
|
|
tile.classList.remove("dance")
|
|
},
|
|
{ once: true }
|
|
)
|
|
}, (index * DANCE_ANIMATION_DURATION) / 5)
|
|
})
|
|
}
|
|
|
|
/*==================== DARK LIGHT THEME ====================*/
|
|
const themeButton = document.getElementById('theme-button')
|
|
const darkTheme = 'dark-theme'
|
|
const iconTheme = 'bx-sun'
|
|
|
|
// Previously selected topic (if user selected)
|
|
const selectedTheme = localStorage.getItem('selected-theme')
|
|
const selectedIcon = localStorage.getItem('selected-icon')
|
|
|
|
// We obtain the current theme that the interface has by validating the dark-theme class
|
|
const getCurrentTheme = () => document.body.classList.contains(darkTheme) ? 'dark' : 'light'
|
|
const getCurrentIcon = () => themeButton.classList.contains(iconTheme) ? 'bx-moon' : 'bx-sun'
|
|
|
|
// We validate if the user previously chose a topic
|
|
if (selectedTheme) {
|
|
// If the validation is fulfilled, we ask what the issue was to know if we activated or deactivated the dark
|
|
document.body.classList[selectedTheme === 'dark' ? 'add' : 'remove'](darkTheme)
|
|
themeButton.classList[selectedIcon === 'bx-moon' ? 'add' : 'remove'](iconTheme)
|
|
}
|
|
|
|
// Activate / deactivate the theme manually with the button
|
|
themeButton.addEventListener('click', () => {
|
|
// Add or remove the dark / icon theme
|
|
document.body.classList.toggle(darkTheme)
|
|
themeButton.classList.toggle(iconTheme)
|
|
// We save the theme and the current icon that the user chose
|
|
localStorage.setItem('selected-theme', getCurrentTheme())
|
|
localStorage.setItem('selected-icon', getCurrentIcon())
|
|
})
|
|
|
|
// Initialize the game when the page loads
|
|
document.addEventListener('DOMContentLoaded', initializeGame) |