sunete
This commit is contained in:
@@ -20,8 +20,7 @@ const PROBLEMS = [
|
||||
{ label: "C", text: "x = 20, y = 16", correct: false },
|
||||
{ label: "D", text: "x = 30, y = 6", correct: false }
|
||||
],
|
||||
marker: { top: "13%", left: "15%" },
|
||||
overlay: { top: "0%", left: "3%", width: "24%", height: "19%" }
|
||||
marker: { top: "18%", left: "16%" }
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -34,8 +33,7 @@ const PROBLEMS = [
|
||||
{ label: "C", text: "p = 10, c = 10", correct: false },
|
||||
{ label: "D", text: "p = 16, c = 4", correct: false }
|
||||
],
|
||||
marker: { top: "50%", left: "12%" },
|
||||
overlay: { top: "33%", left: "1%", width: "22%", height: "17%" }
|
||||
marker: { top: "46%", left: "12%" }
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
@@ -48,8 +46,7 @@ const PROBLEMS = [
|
||||
{ label: "C", text: "h = 12, s = 10", correct: false },
|
||||
{ label: "D", text: "h = 11, s = 11", correct: false }
|
||||
],
|
||||
marker: { top: "12%", left: "65%" },
|
||||
overlay: { top: "1%", left: "52%", width: "32%", height: "20%" }
|
||||
marker: { top: "22%", left: "72%" }
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
@@ -62,8 +59,7 @@ const PROBLEMS = [
|
||||
{ label: "C", text: "m = 18, c = 6", correct: false },
|
||||
{ label: "D", text: "m = 14, c = 10", correct: false }
|
||||
],
|
||||
marker: { top: "72%", left: "22%" },
|
||||
overlay: { top: "57%", left: "8%", width: "25%", height: "22%" }
|
||||
marker: { top: "72%", left: "24%" }
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
@@ -76,8 +72,7 @@ const PROBLEMS = [
|
||||
{ label: "C", text: "p = 14, c = 8", correct: false },
|
||||
{ label: "D", text: "p = 18, c = 4", correct: false }
|
||||
],
|
||||
marker: { top: "58%", left: "48%" },
|
||||
overlay: { top: "41%", left: "34%", width: "26%", height: "20%" }
|
||||
marker: { top: "58%", left: "60%" }
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
@@ -90,8 +85,7 @@ const PROBLEMS = [
|
||||
{ label: "C", text: "m = 15, n = 15", correct: false },
|
||||
{ label: "D", text: "m = 22, n = 8", correct: false }
|
||||
],
|
||||
marker: { top: "28%", left: "80%" },
|
||||
overlay: { top: "16%", left: "60%", width: "32%", height: "38%" }
|
||||
marker: { top: "50%", left: "88%" }
|
||||
}
|
||||
];
|
||||
|
||||
@@ -147,7 +141,6 @@ const TOTAL_HEARTS = 10;
|
||||
--heart-empty: #bdc3c7;
|
||||
--star-gold: #f1c40f;
|
||||
--star-locked: #95a5a6;
|
||||
--overlay-bg: rgba(244,228,193,0.80);
|
||||
--success-green: #4caf50;
|
||||
--backdrop: rgba(0,0,0,0.5);
|
||||
}
|
||||
@@ -210,12 +203,20 @@ html, body {
|
||||
.star {
|
||||
font-size: clamp(22px, 3vmin, 36px);
|
||||
color: var(--star-locked);
|
||||
transition: color 0.5s, transform 0.5s;
|
||||
transition: color 0.5s, transform 0.5s, filter 0.5s;
|
||||
}
|
||||
.star.solved {
|
||||
color: var(--star-gold);
|
||||
transform: scale(1.2);
|
||||
filter: drop-shadow(0 0 6px rgba(241,196,15,0.6));
|
||||
animation: starPop 0.8s ease-out;
|
||||
}
|
||||
@keyframes starPop {
|
||||
0% { transform: scale(1); }
|
||||
20% { transform: scale(2.2) rotate(-15deg); }
|
||||
40% { transform: scale(1.8) rotate(10deg); }
|
||||
60% { transform: scale(2.0) rotate(-5deg); }
|
||||
80% { transform: scale(1.3) rotate(2deg); }
|
||||
100% { transform: scale(1.2) rotate(0deg); }
|
||||
}
|
||||
.hearts { display: flex; gap: 0.5vmin; }
|
||||
.heart {
|
||||
@@ -227,40 +228,15 @@ html, body {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
OVERLAYS (cover text areas)
|
||||
============================================================ */
|
||||
.overlay {
|
||||
position: absolute;
|
||||
background: var(--overlay-bg);
|
||||
border: 1px dashed rgba(139,115,85,0.4);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
|
||||
z-index: 5;
|
||||
}
|
||||
.overlay .lock-icon {
|
||||
font-size: clamp(20px, 2.5vmin, 36px);
|
||||
opacity: 0.4;
|
||||
user-select: none;
|
||||
}
|
||||
.overlay.revealed {
|
||||
opacity: 0;
|
||||
transform: scale(1.02);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
MARKERS
|
||||
============================================================ */
|
||||
.marker {
|
||||
position: absolute;
|
||||
width: 3.5vmin; height: 3.5vmin;
|
||||
min-width: 30px; min-height: 30px;
|
||||
border: 2px solid rgba(139,115,85,0.6);
|
||||
background: rgba(244,228,193,0.4);
|
||||
width: 5vmin; height: 5vmin;
|
||||
min-width: 40px; min-height: 40px;
|
||||
border: 3px solid #8b7355;
|
||||
background: #f4e4c1;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
@@ -269,14 +245,12 @@ html, body {
|
||||
transform: translate(-50%, -50%);
|
||||
transition: all 0.2s;
|
||||
z-index: 6;
|
||||
/* Larger hitbox via padding */
|
||||
padding: 1vmin;
|
||||
margin: -1vmin;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
}
|
||||
.marker:hover:not(.solved) {
|
||||
background: rgba(244,228,193,0.7);
|
||||
transform: translate(-50%, -50%) scale(1.1);
|
||||
box-shadow: 0 0 8px rgba(139,115,85,0.4);
|
||||
background: #ffe0a0;
|
||||
transform: translate(-50%, -50%) scale(1.15);
|
||||
box-shadow: 0 4px 12px rgba(139,115,85,0.5);
|
||||
}
|
||||
.marker:active:not(.solved) {
|
||||
transform: translate(-50%, -50%) scale(0.95);
|
||||
@@ -293,9 +267,8 @@ html, body {
|
||||
font-weight: 700;
|
||||
}
|
||||
.marker .marker-num {
|
||||
font: 700 clamp(10px, 1vmin, 14px) 'Nunito', sans-serif;
|
||||
font: 700 clamp(14px, 1.5vmin, 20px) 'Nunito', sans-serif;
|
||||
color: var(--text-dark);
|
||||
opacity: 0.7;
|
||||
}
|
||||
.marker.solved .marker-num { display: none; }
|
||||
|
||||
@@ -602,6 +575,24 @@ html, body {
|
||||
font: 400 clamp(16px, 1.6vmin, 22px) / 1.5 'Nunito', sans-serif;
|
||||
color: var(--text-body);
|
||||
}
|
||||
.victory .vic-buttons {
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
justify-content: center;
|
||||
margin-top: 1.2em;
|
||||
}
|
||||
.vic-btn {
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 0.7em 2em;
|
||||
font: 700 clamp(16px, 1.6vmin, 22px) 'Nunito', sans-serif;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 3px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
.vic-btn:hover { filter: brightness(1.1); transform: translateY(-2px); }
|
||||
.vic-btn-map { background: var(--btn-blue); color: white; }
|
||||
.vic-btn-restart { background: var(--success-green); color: white; }
|
||||
|
||||
/* ============================================================
|
||||
CONFETTI CANVAS
|
||||
@@ -685,6 +676,10 @@ html, body {
|
||||
<div class="vic-stars" id="vicStars"></div>
|
||||
<div class="vic-bonus" id="vicBonus"></div>
|
||||
<div class="vic-text">Ați descoperit toate secretele<br>drumului lui Nică prin Humulești!</div>
|
||||
<div class="vic-buttons">
|
||||
<button class="vic-btn vic-btn-map" id="vicCloseBtn">Vezi harta</button>
|
||||
<button class="vic-btn vic-btn-restart" id="vicRestartBtn">Începe din nou</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<canvas id="confetti-canvas"></canvas>
|
||||
@@ -705,6 +700,48 @@ html, body {
|
||||
disabledOptions: {}
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// SOUND EFFECTS (Web Audio API — no external files)
|
||||
// ============================================================
|
||||
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
|
||||
function playTone(freq, type, duration, volume) {
|
||||
const osc = audioCtx.createOscillator();
|
||||
const gain = audioCtx.createGain();
|
||||
osc.type = type;
|
||||
osc.frequency.value = freq;
|
||||
gain.gain.setValueAtTime(volume, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration);
|
||||
osc.connect(gain);
|
||||
gain.connect(audioCtx.destination);
|
||||
osc.start();
|
||||
osc.stop(audioCtx.currentTime + duration);
|
||||
}
|
||||
|
||||
function sfxCorrect() {
|
||||
playTone(523, "sine", 0.15, 0.3);
|
||||
setTimeout(() => playTone(659, "sine", 0.15, 0.3), 100);
|
||||
setTimeout(() => playTone(784, "sine", 0.25, 0.3), 200);
|
||||
}
|
||||
|
||||
function sfxWrong() {
|
||||
playTone(200, "square", 0.2, 0.2);
|
||||
setTimeout(() => playTone(160, "square", 0.3, 0.2), 150);
|
||||
}
|
||||
|
||||
function sfxHeartLost() {
|
||||
playTone(440, "sine", 0.1, 0.2);
|
||||
setTimeout(() => playTone(370, "sine", 0.15, 0.2), 80);
|
||||
setTimeout(() => playTone(300, "sine", 0.25, 0.15), 160);
|
||||
}
|
||||
|
||||
function sfxStar() {
|
||||
const notes = [784, 988, 1175, 1319];
|
||||
notes.forEach((f, i) => {
|
||||
setTimeout(() => playTone(f, "sine", 0.2, 0.25), i * 80);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// DOM REFS
|
||||
// ============================================================
|
||||
@@ -733,6 +770,8 @@ html, body {
|
||||
const victoryEl = $("victory");
|
||||
const vicStars = $("vicStars");
|
||||
const vicBonus = $("vicBonus");
|
||||
const vicCloseBtn = $("vicCloseBtn");
|
||||
const vicRestartBtn = $("vicRestartBtn");
|
||||
|
||||
// ============================================================
|
||||
// INIT UI
|
||||
@@ -749,19 +788,8 @@ html, body {
|
||||
}
|
||||
// Hearts
|
||||
renderHearts();
|
||||
// Overlays & markers
|
||||
// Markers
|
||||
PROBLEMS.forEach(p => {
|
||||
// Overlay
|
||||
const ov = document.createElement("div");
|
||||
ov.className = "overlay";
|
||||
ov.id = "overlay-" + p.id;
|
||||
ov.style.top = p.overlay.top;
|
||||
ov.style.left = p.overlay.left;
|
||||
ov.style.width = p.overlay.width;
|
||||
ov.style.height = p.overlay.height;
|
||||
ov.innerHTML = '<span class="lock-icon">🔒</span>';
|
||||
mapWrapper.appendChild(ov);
|
||||
|
||||
// Marker
|
||||
const mk = document.createElement("div");
|
||||
mk.className = "marker";
|
||||
@@ -815,13 +843,26 @@ html, body {
|
||||
feedbackWrong.style.display = "none";
|
||||
optionsArea.style.display = "grid";
|
||||
|
||||
// Shuffle options order (once per problem, persist for reopens)
|
||||
if (!state.shuffledOptions) state.shuffledOptions = {};
|
||||
if (!state.shuffledOptions[p.id]) {
|
||||
const indices = p.options.map((_, i) => i);
|
||||
for (let i = indices.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[indices[i], indices[j]] = [indices[j], indices[i]];
|
||||
}
|
||||
state.shuffledOptions[p.id] = indices;
|
||||
}
|
||||
const order = state.shuffledOptions[p.id];
|
||||
const labels = ["A", "B", "C", "D"];
|
||||
const disabled = state.disabledOptions[p.id] || new Set();
|
||||
p.options.forEach((opt, i) => {
|
||||
order.forEach((origIdx, displayIdx) => {
|
||||
const opt = p.options[origIdx];
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "option-btn";
|
||||
btn.textContent = opt.label + ") " + opt.text;
|
||||
btn.disabled = disabled.has(i);
|
||||
btn.addEventListener("click", () => checkAnswer(p.id, i));
|
||||
btn.textContent = labels[displayIdx] + ") " + opt.text;
|
||||
btn.disabled = disabled.has(origIdx);
|
||||
btn.addEventListener("click", () => checkAnswer(p.id, origIdx));
|
||||
optionsArea.appendChild(btn);
|
||||
});
|
||||
|
||||
@@ -847,38 +888,42 @@ html, body {
|
||||
|
||||
if (opt.correct) {
|
||||
// Correct!
|
||||
sfxCorrect();
|
||||
optionsArea.style.display = "none";
|
||||
feedbackBravo.style.display = "block";
|
||||
feedbackWrong.style.display = "none";
|
||||
feedbackEl.classList.add("visible");
|
||||
|
||||
state.solved.add(problemId);
|
||||
updateStar(PROBLEMS.findIndex(x => x.id === problemId));
|
||||
|
||||
// Reveal overlay
|
||||
const ov = $("overlay-" + problemId);
|
||||
if (ov) ov.classList.add("revealed");
|
||||
const starIdx = PROBLEMS.findIndex(x => x.id === problemId);
|
||||
|
||||
// Mark marker solved
|
||||
const mk = $("marker-" + problemId);
|
||||
if (mk) mk.classList.add("solved");
|
||||
|
||||
// Close after 2s
|
||||
// Close after 2s, THEN animate star so it's visible
|
||||
setTimeout(() => {
|
||||
closeModal();
|
||||
if (state.solved.size === PROBLEMS.length) {
|
||||
showVictory();
|
||||
}
|
||||
setTimeout(() => {
|
||||
sfxStar();
|
||||
updateStar(starIdx);
|
||||
if (state.solved.size === PROBLEMS.length) {
|
||||
setTimeout(showVictory, 1000);
|
||||
}
|
||||
}, 300);
|
||||
}, 2000);
|
||||
} else {
|
||||
// Wrong
|
||||
sfxWrong();
|
||||
setTimeout(sfxHeartLost, 400);
|
||||
loseHeart();
|
||||
if (!state.disabledOptions[problemId]) state.disabledOptions[problemId] = new Set();
|
||||
state.disabledOptions[problemId].add(optionIdx);
|
||||
|
||||
// Disable the button
|
||||
// Disable the button (find display index from original index)
|
||||
const btns = optionsArea.querySelectorAll(".option-btn");
|
||||
if (btns[optionIdx]) btns[optionIdx].disabled = true;
|
||||
const displayIdx = state.shuffledOptions[problemId].indexOf(optionIdx);
|
||||
if (displayIdx >= 0 && btns[displayIdx]) btns[displayIdx].disabled = true;
|
||||
|
||||
// Shake
|
||||
modal.classList.remove("shake");
|
||||
@@ -929,6 +974,7 @@ html, body {
|
||||
if (state.hintUsed.has(state.currentProblem.id)) return;
|
||||
|
||||
state.hintUsed.add(state.currentProblem.id);
|
||||
sfxHeartLost();
|
||||
loseHeart();
|
||||
hintText.classList.add("shown");
|
||||
hintBtn.classList.add("used");
|
||||
@@ -952,23 +998,24 @@ html, body {
|
||||
gameOver.classList.add("visible");
|
||||
}
|
||||
|
||||
restartBtn.addEventListener("click", () => {
|
||||
// Reset everything
|
||||
function resetGame() {
|
||||
state.hearts = TOTAL_HEARTS;
|
||||
state.solved.clear();
|
||||
state.currentProblem = null;
|
||||
state.modalLocked = false;
|
||||
state.hintUsed.clear();
|
||||
state.disabledOptions = {};
|
||||
state.shuffledOptions = {};
|
||||
|
||||
renderHearts();
|
||||
starsEl.querySelectorAll(".star").forEach(s => s.classList.remove("solved"));
|
||||
document.querySelectorAll(".overlay").forEach(o => o.classList.remove("revealed"));
|
||||
document.querySelectorAll(".marker").forEach(m => m.classList.remove("solved"));
|
||||
|
||||
gameOver.classList.remove("visible");
|
||||
victoryEl.classList.remove("visible");
|
||||
});
|
||||
}
|
||||
|
||||
restartBtn.addEventListener("click", resetGame);
|
||||
|
||||
// ============================================================
|
||||
// VICTORY
|
||||
@@ -980,6 +1027,12 @@ html, body {
|
||||
startConfetti();
|
||||
}
|
||||
|
||||
vicCloseBtn.addEventListener("click", () => {
|
||||
victoryEl.classList.remove("visible");
|
||||
});
|
||||
|
||||
vicRestartBtn.addEventListener("click", resetGame);
|
||||
|
||||
// ============================================================
|
||||
// CONFETTI (100 particles, 3s burst)
|
||||
// ============================================================
|
||||
|
||||
Reference in New Issue
Block a user