A continuation of Janady13/Jennys-Drumste / drum-teacher-pro.html. The complete source identity, every recorded occurrence, and linked equations are on its first page.
Source transcript · continued
Download complete source text ↓2,033 lines · 69.1 KB
points = 100;
gameState.perfectNotes++;
} else if (closestDist < greatWindow) {
judgment = 'great';
points = 75;
gameState.greatNotes++;
} else {
judgment = 'good';
points = 50;
gameState.goodNotes++;
}
gameState.hitNotes++;
gameState.combo++;
gameState.maxCombo = Math.max(gameState.maxCombo, gameState.combo);
// Multiplier logic
gameState.multiplierProgress += 10;
if (gameState.multiplierProgress >= 100) {
gameState.multiplierProgress = 0;
gameState.multiplier = Math.min(8, gameState.multiplier + 1);
}
// Star power build-up
if (!gameState.starPowerActive) {
gameState.starPower = Math.min(100, gameState.starPower + 2);
}
// Score calculation
const multiplier = gameState.starPowerActive ? gameState.multiplier * 2 : gameState.multiplier;
gameState.score += points * multiplier;
// Crowd meter
gameState.crowdMeter = Math.min(100, gameState.crowdMeter + 2);
return judgment;
}
return null;
}
function handleMiss(note) {
if (note.hit || note.missed) return;
note.missed = true;
gameState.missedNotes++;
gameState.combo = 0;
gameState.multiplier = 1;
gameState.multiplierProgress = 0;
// Crowd meter decrease
if (!gameState.isPracticeMode) {
gameState.crowdMeter = Math.max(0, gameState.crowdMeter - 5);
}
// Show miss judgment
showJudgment(note.x, canvas.offsetHeight - 150, 'miss');
updateUI();
}
// ═══════════════════════════════════════════════════════════════════════════════
// VISUAL EFFECTS
// ═══════════════════════════════════════════════════════════════════════════════
function createHitEffect(drumType, judgment) {
const container = document.getElementById('effectsContainer');
const lane = lanePositions[drumType];
const y = canvas.offsetHeight - 110;
if (judgment) {
// Explosion effect
const explosion = document.createElement('div');
explosion.className = 'explosion';
explosion.style.left = (lane - 75) + 'px';
explosion.style.bottom = '70px';
explosion.style.background = `radial-gradient(circle, ${drumColors[drumType]} 0%, transparent 70%)`;
container.appendChild(explosion);
setTimeout(() => explosion.remove(), 400);
// Judgment text
showJudgment(lane, y, judgment);
}
}
function showJudgment(x, y, judgment) {
const container = document.getElementById('effectsContainer');
const text = document.createElement('div');
text.className = `hit-judgment ${judgment}`;
text.textContent = judgment.toUpperCase();
text.style.left = (x - 50) + 'px';
text.style.bottom = '150px';
container.appendChild(text);
setTimeout(() => text.remove(), 600);
}
// ═══════════════════════════════════════════════════════════════════════════════
// GAME LOOP
// ═══════════════════════════════════════════════════════════════════════════════
function togglePlay() {
if (gameState.isPlaying) {
stopGame();
} else {
startCountdown();
}
}
function startCountdown() {
const overlay = document.getElementById('countdownOverlay');
const number = document.getElementById('countdownNumber');
overlay.classList.add('active');
let count = 3;
number.textContent = count;
const interval = setInterval(() => {
count--;
if (count > 0) {
number.textContent = count;
number.style.animation = 'none';
number.offsetHeight;
number.style.animation = 'countdownPulse 1s ease-out';
} else {
clearInterval(interval);
overlay.classList.remove('active');
startGame();
}
}, 1000);
}
function startGame() {
gameState.isPlaying = true;
document.getElementById('playBtn').classList.add('active');
document.getElementById('playBtn').textContent = 'PAUSE';
lastTime = performance.now();
gameState.songPosition = 0;
activeNotes = [];
gameLoop();
generateNotes();
}
function stopGame() {
gameState.isPlaying = false;
document.getElementById('playBtn').classList.remove('active');
document.getElementById('playBtn').textContent = 'PLAY';
if (animationId) {
cancelAnimationFrame(animationId);
}
}
function resetGame() {
gameState.score = 0;
gameState.combo = 0;
gameState.maxCombo = 0;
gameState.multiplier = 1;
gameState.multiplierProgress = 0;
gameState.starPower = 0;
gameState.starPowerActive = false;
gameState.accuracy = 100;
gameState.totalNotes = 0;
gameState.hitNotes = 0;
gameState.perfectNotes = 0;
gameState.greatNotes = 0;
gameState.goodNotes = 0;
gameState.missedNotes = 0;
gameState.crowdMeter = 50;
gameState.songPosition = 0;
activeNotes = [];
updateUI();
}
function restartSong() {
stopGame();
resetGame();
startCountdown();
}
function gameLoop(currentTime = 0) {
if (!gameState.isPlaying) return;
const deltaTime = currentTime - lastTime;
lastTime = currentTime;
// Update song position
gameState.songPosition += deltaTime;
// Update notes
updateNotes(deltaTime);
// Render
render();
// Update UI
updateUI();
// Check for song end or game over
if (!gameState.isPracticeMode && gameState.crowdMeter <= 0) {
endGame(false);
return;
}
animationId = requestAnimationFrame(gameLoop);
}
function generateNotes() {
const lesson = lessons[gameState.currentLesson];
const pattern = lesson.pattern;
const beatDuration = 60000 / gameState.bpm / 4; // 16th note duration
let noteIndex = 0;
const patternLength = 16;
const totalBeats = 64; // Play pattern 4 times
for (let beat = 0; beat < totalBeats; beat++) {
const patternBeat = beat % patternLength;
for (const [drum, drumPattern] of Object.entries(pattern)) {
if (drumPattern[patternBeat] === '1') {
const noteTime = beat * beatDuration;
scheduleNote(drum, noteTime);
gameState.totalNotes++;
}
}
}
}
function scheduleNote(drum, time) {
const travelTime = (canvas.offsetHeight / gameState.noteSpeed) * 1000;
setTimeout(() => {
if (gameState.isPlaying) {
activeNotes.push({
drum,
y: 0,
x: lanePositions[drum],
hit: false,
missed: false
});
}
}, Math.max(0, time - travelTime));
}
function updateNotes(deltaTime) {
const speed = gameState.noteSpeed * (deltaTime / 1000);
const hitLine = canvas.offsetHeight - 110;
const missLine = canvas.offsetHeight - 50;
for (let i = activeNotes.length - 1; i >= 0; i--) {
const note = activeNotes[i];
note.y += speed;
// Check for miss
if (note.y > missLine && !note.hit && !note.missed) {
handleMiss(note);
}
// Remove off-screen notes
if (note.y > canvas.offsetHeight + 50) {
activeNotes.splice(i, 1);
}
}
}
// ═══════════════════════════════════════════════════════════════════════════════
// RENDERING
// ═══════════════════════════════════════════════════════════════════════════════
function render() {
const width = canvas.offsetWidth;
const height = canvas.offsetHeight;
ctx.clearRect(0, 0, width, height);
// Draw lane lines
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
ctx.lineWidth = 1;
lanes.forEach(lane => {
const x = lanePositions[lane];
if (x) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height - 80);
ctx.stroke();
}
});
// Draw hit line
const hitLineY = height - 110;
ctx.strokeStyle = gameState.starPowerActive ? 'rgba(255, 200, 0, 0.8)' : 'rgba(0, 255, 204, 0.5)';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(0, hitLineY);
ctx.lineTo(width, hitLineY);
ctx.stroke();
// Draw notes
activeNotes.forEach(note => {
if (!note.hit) {
drawNote(note);
}
});
}
function drawNote(note) {
const x = note.x;
const y = note.y;
const color = drumColors[note.drum];
// Note glow
ctx.shadowBlur = 20;
ctx.shadowColor = color;
// Note body
ctx.fillStyle = color;
ctx.beginPath();
if (note.drum === 'kick') {
// Kick is circular
ctx.arc(x, y, 25, 0, Math.PI * 2);
} else if (note.drum === 'crash' || note.drum === 'ride' || note.drum === 'hihat') {
// Cymbals are diamonds
ctx.moveTo(x, y - 20);
ctx.lineTo(x + 20, y);
ctx.lineTo(x, y + 20);
ctx.lineTo(x - 20, y);
} else {
// Toms/snare are rounded rectangles
ctx.roundRect(x - 20, y - 15, 40, 30, 8);
}
ctx.fill();
ctx.shadowBlur = 0;
// Star power notes glow extra
if (gameState.starPowerActive) {
ctx.strokeStyle = '#ffcc00';
ctx.lineWidth = 3;
ctx.stroke();
}
}
// ═══════════════════════════════════════════════════════════════════════════════
// STAR POWER
// ═══════════════════════════════════════════════════════════════════════════════
function activateStarPower() {
if (gameState.starPower >= 50 && !gameState.starPowerActive) {
gameState.starPowerActive = true;
document.getElementById('starPowerFill').classList.add('active');
document.body.style.background = 'linear-gradient(180deg, #1a0a00 0%, #0a0a15 100%)';
// Star power drains over time
const drainInterval = setInterval(() => {
if (!gameState.isPlaying || !gameState.starPowerActive) {
clearInterval(drainInterval);
return;
}
gameState.starPower -= 1;
if (gameState.starPower <= 0) {
gameState.starPowerActive = false;
document.getElementById('starPowerFill').classList.remove('active');
document.body.style.background = 'var(--dark)';
clearInterval(drainInterval);
}
updateUI();
}, 100);
}
}
// ═══════════════════════════════════════════════════════════════════════════════
// UI UPDATES
// ═══════════════════════════════════════════════════════════════════════════════
function updateUI() {
// Score
document.getElementById('scoreDisplay').textContent = gameState.score.toLocaleString();
// Combo
const comboEl = document.getElementById('comboDisplay');
comboEl.textContent = gameState.combo;
comboEl.classList.add('bump');
setTimeout(() => comboEl.classList.remove('bump'), 100);
// Multiplier
document.getElementById('multiplierDisplay').textContent = gameState.multiplier + 'x';
document.getElementById('multiplierFill').style.width = gameState.multiplierProgress + '%';
// Star Power
document.getElementById('starPowerFill').style.width = gameState.starPower + '%';
document.getElementById('starPowerText').textContent = Math.round(gameState.starPower) + '%';
// Accuracy
const total = gameState.hitNotes + gameState.missedNotes;
gameState.accuracy = total > 0 ? Math.round((gameState.hitNotes / total) * 100) : 100;
const accEl = document.getElementById('accuracyDisplay');
accEl.textContent = gameState.accuracy + '%';
accEl.className = 'accuracy-value ' + getAccuracyClass(gameState.accuracy);
// Crowd meter
const crowdFill = document.getElementById('crowdFill');
crowdFill.style.width = gameState.crowdMeter + '%';
crowdFill.className = 'crowd-fill ' + getCrowdClass(gameState.crowdMeter);
// Streak flames
const flames = document.getElementById('streakFlames');
flames.classList.toggle('active', gameState.combo >= 10);
}
function getAccuracyClass(acc) {
if (acc >= 95) return 'perfect';
if (acc >= 85) return 'great';
if (acc >= 70) return 'good';
return 'poor';
}
function getCrowdClass(meter) {
if (meter >= 75) return 'great';
if (meter >= 50) return 'good';
if (meter >= 25) return 'warning';
return 'danger';
}
// ═══════════════════════════════════════════════════════════════════════════════
// GAME END
// ═══════════════════════════════════════════════════════════════════════════════
function endGame(completed = true) {
stopGame();
// Show results
document.getElementById('finalScore').textContent = gameState.score.toLocaleString();
document.getElementById('finalAccuracy').textContent = gameState.accuracy + '%';
document.getElementById('finalCombo').textContent = gameState.maxCombo;
document.getElementById('finalStreak').textContent = gameState.hitNotes;
// Star rating
const stars = Math.min(5, Math.floor(gameState.accuracy / 20));
const starEls = document.querySelectorAll('#starRating .star');
starEls.forEach((star, i) => {
star.classList.toggle('filled', i < stars);
});
document.getElementById('gameOverOverlay').classList.add('active');
// Process through SEFERIM
if (window.SEFERIM) {
window.SEFERIM.processPatternComplete(
lessons[gameState.currentLesson].name,
gameState.accuracy,
gameState.maxCombo,
gameState.bpm
);
window.SEFERIM.save();
}
}
function closeResults() {
document.getElementById('gameOverOverlay').classList.remove('active');
resetGame();
}
// ═══════════════════════════════════════════════════════════════════════════════
// SETTINGS
// ═══════════════════════════════════════════════════════════════════════════════
document.getElementById('tempoSlider').addEventListener('input', (e) => {
gameState.bpm = parseInt(e.target.value);
document.getElementById('tempoValue').textContent = gameState.bpm;
document.getElementById('bpmValue').textContent = gameState.bpm;
});
function togglePracticeMode() {
gameState.isPracticeMode = !gameState.isPracticeMode;
document.getElementById('practiceModeToggle').classList.toggle('active', gameState.isPracticeMode);
}
// ═══════════════════════════════════════════════════════════════════════════════
// SEFERIM INTEGRATION
// ═══════════════════════════════════════════════════════════════════════════════
function initSeferim() {
const container = document.getElementById('familiesMini');
container.innerHTML = '';
for (let i = 0; i < 16; i++) {
const bar = document.createElement('div');
bar.className = 'family-bar';
bar.id = `fam-${i}`;
bar.innerHTML = '<div class="family-bar-fill" style="height: 0%"></div>';
container.appendChild(bar);
}
// Check for SEFERIM
const checkInterval = setInterval(() => {
if (window.SEFERIM) {
clearInterval(checkInterval);
updateSeferimUI();
setInterval(updateSeferimUI, 2000);
}
}, 100);
}
function updateSeferimUI() {
if (!window.SEFERIM) return;
const status = window.SEFERIM.getStatus();
document.getElementById('seferimPhi').textContent = status.PhiGlobal.toFixed(2);
document.getElementById('seferimK').textContent = status.K.toLocaleString();
document.getElementById('seferimTick').textContent = status.tick.toLocaleString();
document.getElementById('seferimLevel').textContent = status.difficulty.level;
status.families.forEach((fam, i) => {
const bar = document.getElementById(`fam-${i}`);
if (bar) {
bar.querySelector('.family-bar-fill').style.height = (fam.coherence * 100) + '%';
bar.classList.toggle('active', fam.pressure > 0.3);
}
});
}
</script>
</body>
</html>