diff --git a/src/_includes/footer.njk b/src/_includes/footer.njk index 2f1b944e..2567d315 100644 --- a/src/_includes/footer.njk +++ b/src/_includes/footer.njk @@ -77,11 +77,11 @@
diff --git a/src/_includes/header.njk b/src/_includes/header.njk index 17ce047c..9435c5a5 100644 --- a/src/_includes/header.njk +++ b/src/_includes/header.njk @@ -51,7 +51,7 @@
Newbie - XP: 0 + XP: 0 / 45 LVL 0 diff --git a/src/assets/css/style.css b/src/assets/css/style.css index 1b8669a8..ab2374bb 100644 --- a/src/assets/css/style.css +++ b/src/assets/css/style.css @@ -5,10 +5,10 @@ --bg-page: #f0f4f8; --bg-card: #ffffff; --bg-footer: #e2e8f0; - --text-main: #1a202c; + --text-main: #1e293b; /* Default Light */ --text-muted: #4a5568; --border-color: #cbd5e0; - --accent: #2563eb; + --accent: #94a3b8; /* Default */ --accent-light: #eff6ff; --accent-rgb: 37, 99, 235; --danger: #dc2626; @@ -17,9 +17,9 @@ .dark { --bg-page: #05070a; - --bg-card: #0f172a; + --bg-card: #1e293b; --bg-footer: #020617; - --text-main: #f1f5f9; + --text-main: #f8fafc; --text-muted: #94a3b8; --border-color: #1e293b; --accent: #38bdf8; @@ -412,10 +412,6 @@ body[data-level="6"]::after { pointer-events: none; opacity: 0.2; /* Very subtle scanlines */ } -#total-xp-display { - display: inline-block; - transition: transform 0.1s ease; -} /* Added via JS when XP increases */ .xp-pulse { @@ -463,20 +459,86 @@ body[data-level="6"]::after { --glow-color: #ef4444; /* Sith Red */ } -/* Force Shadow ripple for the container */ -.force-glow::after { - content: ""; +/* Override the Tailwind duration-1000 for a better feel */ +#level-progress { + transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; +} + + +/* Force Glow for the Badge */ +@keyframes force-pulse { + 0% { filter: drop-shadow(0 0 2px var(--accent)); transform: scale(1); } + 50% { filter: drop-shadow(0 0 15px var(--accent)); transform: scale(1.1); } + 100% { filter: drop-shadow(0 0 2px var(--accent)); transform: scale(1); } +} + +.force-glow { + animation: force-pulse 1.5s infinite ease-in-out; +} +#level-name { + font-weight: 900; + text-transform: uppercase; + letter-spacing: 0.02em; + + /* Remove any existing filters that cause fuzziness */ + filter: none !important; + transition: color 0.3s ease; +} + +/* Adjust the stroke for Dark/Random themes to keep it crisp */ +.dark #level-name { + text-shadow: 1px 1px 0px rgba(0,0,0,0.5); +} + +#level-badge { + transition: background-color 0.4s ease, transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +/* Ensure the XP numbers are always legible */ +#total-xp-display { + color: var(--text-main); + filter: brightness(1.2); /* Pops slightly more in dark mode */ +} + +/* Progress bar should always have a high-contrast background */ +.xp-bar-container { + background: rgba(0, 0, 0, 0.1); +} +.dark .xp-bar-container { + background: rgba(255, 255, 255, 0.1); +} +.xp-popup { position: absolute; - inset: -10px; - border-radius: 50%; - background: var(--glow-color); - opacity: 0.2; - filter: blur(15px); - z-index: -1; + pointer-events: none; + z-index: 9999; + animation: floatUp 1s ease-out forwards; + color: var(--accent); /* This uses our dynamic level color! */ + font-size: 0.75rem; } -#level-progress { - height: 100%; - width: 0%; /* Initial state */ - transition: width 0.3s ease-in-out; /* This makes it "fill" instead of "jump" */ - box-shadow: 0 0 10px var(--accent); + +@keyframes floatUp { + 0% { + transform: translateY(0); + opacity: 1; + } + 100% { + transform: translateY(-50px); + opacity: 0; + } +} +@keyframes xpFloat { + 0% { + transform: translateY(0) scale(1); + opacity: 1; + } + 100% { + transform: translateY(-40px) scale(1.2); + opacity: 0; + } +} + +.animate-xp-float { + animation: xpFloat 1s ease-out forwards; + /* This makes sure the text is crisp on light/dark themes */ + text-shadow: 0 0 4px rgba(0,0,0,0.3); } diff --git a/src/assets/js/script.js b/src/assets/js/script.js index 3451b7d2..900cf800 100644 --- a/src/assets/js/script.js +++ b/src/assets/js/script.js @@ -242,6 +242,20 @@ let currentLevel = parseInt(localStorage.getItem('userLevel')) || 0; // Load saved XP or start at 0 let currentXP = parseInt(localStorage.getItem('userXP')) || 0; +function getContrastYIQ(hexcolor){ + hexcolor = hexcolor.replace("#", ""); + var r = parseInt(hexcolor.substr(0,2),16); + var g = parseInt(hexcolor.substr(2,2),16); + var b = parseInt(hexcolor.substr(4,2),16); + var yiq = ((r*299)+(g*587)+(b*114))/1000; + return (yiq >= 128) ? 'black' : 'white'; +} + + +function isEggUnlocked(eggId) { + // Returns true if the ID exists in the array, false otherwise + return unlockedEggs.includes(eggId); +} /** * 1. RETRO SOUND ENGINE @@ -326,79 +340,57 @@ let matrixActive = false; let destructInterval; function getRank(lvl) { - // 1. Ensure lvl is a valid number - const searchLvl = parseInt(lvl) || 0; + const numericLevel = Number(lvl) || 0; - // 2. Find the highest rank that is less than or equal to current level - // We slice().reverse() to check the highest levels first - const rank = LEVELS.slice().reverse().find(r => searchLvl >= r.level); + // IMPORTANT: .slice().reverse() creates a temporary reversed list + // so we find the HIGHEST level match first. + const rank = LEVELS.slice().reverse().find(r => numericLevel >= r.level); - // 3. Fallback: If rank is undefined, return the first level (Newbie) if (!rank) { - console.warn(`Rank not found for level ${searchLvl}, defaulting to Level 0.`); + console.warn("Rank not found, defaulting to Newbie"); return LEVELS[0]; } return rank; } -/** - * 3. GAME ENGINE - */ -function updateGameUI() { - // Ensure currentLevel is a valid number - const lvl = parseInt(currentLevel) || 0; - // Check if LEVELS exists yet - if (!LEVELS || LEVELS.length === 0) return; +// Ensure this is in the GLOBAL scope (not hidden inside another function) +window.createFloatingXP = function(e) { + // 1. Create the XP element + const popup = document.createElement('div'); - const rank = getRank(currentLevel); + // 2. Styling (Tailwind classes + Inline for positioning) + popup.className = 'fixed pointer-events-none z-[999] font-black text-sm tracking-tighter animate-xp-float'; + popup.innerText = '+1 XP'; - // If rank is STILL undefined (e.g. LEVELS is empty), stop here - if (!rank) return; - - const xpBar = document.getElementById('level-progress'); - const xpText = document.getElementById('total-xp-display'); + // 3. Get current Rank color for the "Pop" + const rank = getRank(currentLevel); + popup.style.color = rank.color; - if (xpBar) { - const progress = (currentXP / 45) * 100; - xpBar.style.width = `${progress}%`; - } + // 4. Position at mouse (using clientX/Y for fixed positioning) + popup.style.left = `${e.clientX}px`; + popup.style.top = `${e.clientY}px`; - if (xpText) { - xpText.innerText = `${currentXP} / 45`; - } + document.body.appendChild(popup); - const nameLabel = document.getElementById('level-name'); - if (nameLabel) { - nameLabel.innerText = rank.name; - // This is where it was crashing: - nameLabel.style.color = rank.color || "#ffffff"; + // 5. Award XP and update that "Newbie" header + if (typeof addExperience === 'function') { + addExperience(1); } - const badge = document.getElementById('level-badge'); - const numLabel = document.getElementById('level-number'); - - if (badge) { - badge.innerText = rank.emoji; - badge.style.backgroundColor = rank.color; - } - if (numLabel) numLabel.innerText = currentLevel; + // 6. Cleanup + setTimeout(() => popup.remove(), 800); +}; - // Update the Progress Bar - const pb = document.getElementById('level-progress'); - if (pb) { - // Use 45 XP per level as the denominator - const progressPercent = Math.min((currentXP / 45) * 100, 100); - pb.style.width = `${progressPercent}%`; - pb.style.backgroundColor = rank.color; - } - // Sith Theme Auto-Switch (Levels 131-160) - if (lvl >= 131 && lvl <= 160) { - document.documentElement.style.setProperty('--accent', '#ef4444'); - } +// Re-attach listeners to your skill tags +function attachSkillListeners() { + const skillTags = document.querySelectorAll('.skill-tag'); // Use your actual class name + skillTags.forEach(tag => { + // Use 'mouseenter' for a clean single-pop on hover + tag.addEventListener('mouseenter', createXPPopup); + }); } - function unlockEgg(eggId) { if (!unlockedEggs.includes(eggId)) { unlockedEggs.push(eggId); @@ -472,8 +464,6 @@ function applyTheme(theme) { html.style.setProperty('--bg-footer', `hsl(${h}, 40%, 5%)`); // Deepest html.style.setProperty('--text-main', `hsl(${h}, 20%, 95%)`); // Near White html.style.setProperty('--text-muted', `hsl(${h}, 15%, 70%)`); // Softened - html.style.setProperty('--accent', `hsl(${h}, 95%, 70%)`); // Vivid Pop - html.style.setProperty('--accent-light', `hsla(${h}, 95%, 70%, 0.2)`); html.style.setProperty('--border-color', `hsl(${h}, 30%, 20%)`); if (heart) { @@ -506,7 +496,33 @@ function updateThemeIcon(theme) { /** * 5. EASTER EGG LOGIC & TRIGGERS */ +function triggerForceSurge() { + initAudio(); + addExperience(100); // This now handles UI updates, sounds, and bar filling +} + +function triggerMagicXP() { + initAudio(); + addExperience(50); +} + +// Visual Effect for Level 101+ +function triggerForceEffects(lvl) { + const badge = document.getElementById('level-badge'); + if (badge) { + badge.classList.add('force-glow'); + // Remove after 2 seconds unless it's a persistent rank + setTimeout(() => badge.classList.remove('force-glow'), 2000); + } +} + function triggerSecretUnlock(type) { + const eggId = `secret_${type}`; + + // 1. Check if this is a NEW discovery + const isNewUnlock = !unlockedEggs.includes(eggId); + + // 2. Trigger the Visual Effects (Always trigger these) if (type === 'gravity') { activateGravityEffect(); } else if (type === 'matrix') { @@ -514,7 +530,26 @@ function triggerSecretUnlock(type) { } else if (type === 'konami') { activateKonami(); } - unlockEgg(`secret_${type}`); + + // 3. Only process XP and Save if it's the first time + if (isNewUnlock) { + // Update the array and save to localStorage + unlockedEggs.push(eggId); + localStorage.setItem('unlockedEggs', JSON.stringify(unlockedEggs)); + + // Assign XP based on difficulty + if (type === 'konami') { + addExperience(500); // Massive bonus for the long code + } else if (type === 'gravity' || type === 'matrix') { + addExperience(45); // 1 full level + } else { + addExperience(75); // 2 full levels + } + + console.log(`✨ Secret Unlocked: ${eggId}`); + } else { + console.log(`Secret ${eggId} already discovered. No extra XP granted.`); + } } const konamiCode = ['arrowup', 'arrowup', 'arrowdown', 'arrowdown', 'arrowleft', 'arrowright', 'arrowleft', 'arrowright', 'b', 'a']; @@ -816,77 +851,104 @@ document.addEventListener('DOMContentLoaded', () => { /** * 9. ENHANCED XP & SKILL MINING SYSTEM */ +function renderXP(value) { + const pb = document.getElementById('level-progress'); + if (pb) { + // Ensure we don't exceed 100% + const percent = Math.min((value / XP_PER_LEVEL) * 100, 100); + pb.style.width = `${percent}%`; + } +} + async function addExperience(amount) { if (document.getElementById('dev-tools')?.getAttribute('data-lock') === 'true') return; - currentXP += amount; + // 1. Force everything to be a clean number to prevent negative/NaN math + let xpToAdd = Number(amount) || 0; + currentXP = Number(currentXP) || 0; + currentLevel = Number(currentLevel) || 0; - // Check if we have enough XP to level up - while (currentXP >= 45 && currentLevel < 200) { - // 1. Force the bar to 100% manually first - renderXP(45); + currentXP += xpToAdd; - // 2. Wait for the CSS transition to finish (matches your transition speed) - await new Promise(resolve => setTimeout(resolve, 300)); + // 2. Level Up Loop + while (currentXP >= XP_PER_LEVEL && currentLevel < 200) { + // Visual fill to end + renderXP(XP_PER_LEVEL); + await new Promise(r => setTimeout(r, 300)); - // 3. Perform the actual Level Up - currentXP -= 45; + // The Math: Subtract 45 and increment level + currentXP -= XP_PER_LEVEL; currentLevel++; - playSound('levelUp'); - showLevelUpNotification(getRank(currentLevel)); + // Safety: Ensure XP never drops below 0 + currentXP = Math.max(0, currentXP); - // 4. Momentarily disable transitions to reset bar to 0% without sliding back + // Reset bar visually for next level in the loop const pb = document.getElementById('level-progress'); if (pb) { pb.style.transition = 'none'; renderXP(0); - void pb.offsetWidth; // Force a reflow + void pb.offsetWidth; pb.style.transition = 'width 0.3s ease-in-out'; } + + const rank = getRank(currentLevel); + showLevelUpNotification(rank); + playSound('levelUp'); } - // 5. Save and Render final state - localStorage.setItem('userLevel', currentLevel); - localStorage.setItem('userXP', currentXP); + // 3. Save clean numbers back to storage + localStorage.setItem('userLevel', currentLevel.toString()); + localStorage.setItem('userXP', currentXP.toString()); + updateGameUI(); } -// Helper to update just the bar width -function renderXP(value) { - const pb = document.getElementById('level-progress'); - if (pb) { - const percent = Math.min((value / 45) * 100, 100); - pb.style.width = `${percent}%`; +function updateGameUI() { + const lvl = Number(currentLevel) || 0; + const rank = getRank(lvl); + + // Update the Name and its Color + const nameLabel = document.getElementById('level-name'); + if (nameLabel) { + nameLabel.innerText = rank.name; + nameLabel.style.color = rank.color; // This applies the array color + } + + // Update the Badge Background + const badge = document.getElementById('level-badge'); + if (badge) { + badge.innerText = rank.emoji; + badge.style.backgroundColor = rank.color; } + + // Update the Number and XP + if (document.getElementById('level-number')) { + document.getElementById('level-number').innerText = lvl; + } + + if (document.getElementById('total-xp-display')) { + document.getElementById('total-xp-display').innerText = `${currentXP} / ${XP_PER_LEVEL}`; + } + + renderXP(currentXP); } +function initSkillMining() { + // Select all your skill badges/tags + const skillTags = document.querySelectorAll('.skill-tag, .experience-badge'); -function createFloatingXP(event, type = "skill") { - const float = document.createElement('div'); - const levelIndex = unlockedEggs.length; - const rankColor = LEVELS[levelIndex]?.color || '#06b6d4'; - - float.innerText = type === "maintenance" ? "+5 XP (MAINT)" : "+1 XP"; - - float.style.cssText = ` - position: fixed; - left: ${event.clientX}px; - top: ${event.clientY}px; - color: ${rankColor}; - font-family: 'JetBrains Mono', monospace; - font-weight: 900; - font-size: ${type === "maintenance" ? '14px' : '12px'}; - pointer-events: none; - z-index: 10000; - animation: float-up 0.8s ease-out forwards; - `; - - document.body.appendChild(float); - setTimeout(() => float.remove(), 800); + skillTags.forEach(tag => { + // Remove old listeners to prevent double-firing + tag.removeEventListener('mouseenter', createXPPopup); + tag.addEventListener('mouseenter', createXPPopup); + }); } +// Run this on page load +document.addEventListener('DOMContentLoaded', initSkillMining); + function initSkillXP() { const skills = document.querySelectorAll('.skill-item'); skills.forEach(skill => { @@ -925,66 +987,8 @@ function addMaintenanceXP() { } } -/** - * MAGIC XP HANDLER - */ -function triggerMagicXP() { - // 1. Play the high-pitched secret sound - playSound('secret'); - // Check if function exists - if (typeof addExperience === "function") { - addExperience(100); - console.log("Magic XP Injected"); - } else { - console.error("Critical Error: addExperience function not found!"); - } - - // 3. Visual "Magic" Flare on the badge - const badge = document.getElementById('level-badge'); - if (badge) { - badge.style.filter = "drop-shadow(0 0 20px #a855f7) brightness(1.5)"; - badge.animate([ - { transform: 'scale(1) rotate(0deg)' }, - { transform: 'scale(2) rotate(180deg)', offset: 0.5 }, - { transform: 'scale(1) rotate(360deg)' } - ], { - duration: 800, - easing: 'ease-out' - }); - - // Reset filter after animation - setTimeout(() => { - badge.style.filter = "none"; - }, 800); - } - - // 4. Console feedback - console.log("%c ✨ Magic XP Cast! +100 XP added to the void.", "color: #a855f7; font-weight: bold;"); -} -function triggerForceSurge() { - playSound('secret'); - - // 1. Add XP via the engine (which handles the math) - addExperience(100); - - // 2. Show a specific "Force" notification manually if you want - // We get the rank object FIRST to avoid passing a raw number - const currentRank = getRank(currentLevel); - showLevelUpNotification(currentRank); - - // 3. Visuals - const badge = document.getElementById('level-badge'); - if (badge) { - badge.classList.add('force-glow'); - setTimeout(() => badge.classList.remove('force-glow'), 2000); - } -} - -/** - * SYSTEM LEVEL JUMP - */ function jumpToLevel() { const input = document.getElementById('jump-lvl'); diff --git a/src/index.njk b/src/index.njk index 3be6d00f..3ed94564 100644 --- a/src/index.njk +++ b/src/index.njk @@ -35,7 +35,7 @@ layout: false
{% for skill in skills %} - {{ skill }} + {{ skill }} {% endfor %}