A continuation of Janady13/Jennys-Drumste / drum-song-mode.html. The complete source identity, every recorded occurrence, and linked equations are on its first page.
Source transcript · continued
Download complete source text ↓2,622 lines · 94.3 KB
while (time < duration) {
pattern.push([time, 'kick']);
pattern.push([time, 'ride']);
pattern.push([time + beatMs * 2, 'snare']);
pattern.push([time + beatMs * 2, 'ride']);
pattern.push([time + beatMs * 4, 'kick']);
pattern.push([time + beatMs * 4, 'ride']);
pattern.push([time + beatMs * 6, 'snare']);
pattern.push([time + beatMs * 6, 'ride']);
time += beatMs * 8;
}
return pattern;
}
}
];
// ═══════════════════════════════════════════════════════════════════════════
// GAME STATE
// ═══════════════════════════════════════════════════════════════════════════
let gameState = {
isPlaying: false,
isPracticeMode: false,
currentSong: null,
notes: [],
score: 0,
combo: 0,
maxStreak: 0,
multiplier: 1,
notesHit: 0,
notesMissed: 0,
totalNotes: 0,
starPower: 0,
starPowerActive: false,
crowdMeter: 50,
lastNoteTime: 0,
gameStartTime: 0,
playbackSpeed: 1
};
// YouTube Player
let player = null;
let isPlayerReady = false;
// Canvas contexts
let highwayCanvas, highwayCtx;
let effectsCanvas, effectsCtx;
// MIDI
let midiAccess = null;
// Tap tempo
let tapTimes = [];
let tapTimeout = null;
// Active effects
let activeEffects = [];
// Animation frame
let animationId = null;
// ═══════════════════════════════════════════════════════════════════════════
// INITIALIZATION
// ═══════════════════════════════════════════════════════════════════════════
function init() {
// Setup canvases
highwayCanvas = document.getElementById('noteHighway');
effectsCanvas = document.getElementById('effectsCanvas');
resizeCanvases();
window.addEventListener('resize', resizeCanvases);
highwayCtx = highwayCanvas.getContext('2d');
effectsCtx = effectsCanvas.getContext('2d');
// Load song library
loadSongLibrary();
// Setup SEFERIM
initSeferim();
// Setup keyboard controls
setupKeyboardControls();
// Setup drum pad clicks
setupDrumPads();
// Setup speed slider
setupSpeedControl();
// Try to connect MIDI
connectMIDI();
// Start render loop
requestAnimationFrame(gameLoop);
}
function resizeCanvases() {
const container = document.querySelector('.highway-container');
highwayCanvas.width = container.clientWidth;
highwayCanvas.height = container.clientHeight;
effectsCanvas.width = window.innerWidth;
effectsCanvas.height = window.innerHeight;
}
// ═══════════════════════════════════════════════════════════════════════════
// YOUTUBE PLAYER
// ═══════════════════════════════════════════════════════════════════════════
function onYouTubeIframeAPIReady() {
player = new YT.Player('youtube-player', {
height: '100%',
width: '100%',
videoId: '',
playerVars: {
'playsinline': 1,
'controls': 0,
'disablekb': 1,
'modestbranding': 1,
'rel': 0
},
events: {
'onReady': onPlayerReady,
'onStateChange': onPlayerStateChange,
'onError': onPlayerError
}
});
}
function onPlayerReady(event) {
isPlayerReady = true;
console.log('YouTube player ready');
document.getElementById('loadingIndicator').classList.remove('active');
}
function onPlayerStateChange(event) {
if (event.data === YT.PlayerState.PLAYING) {
if (!gameState.isPlaying && gameState.notes.length > 0) {
// Sync game start with video
gameState.gameStartTime = performance.now() - (player.getCurrentTime() * 1000);
}
} else if (event.data === YT.PlayerState.ENDED) {
endGame();
}
}
function onPlayerError(event) {
console.error('YouTube player error:', event.data);
alert('Error loading video. Please check the URL and try again.');
document.getElementById('loadingIndicator').classList.remove('active');
}
function loadYouTubeVideo() {
const url = document.getElementById('youtubeUrl').value.trim();
const videoId = extractYouTubeId(url);
if (!videoId) {
alert('Please enter a valid YouTube URL');
return;
}
if (!isPlayerReady) {
alert('YouTube player is still loading. Please wait...');
return;
}
document.getElementById('loadingIndicator').classList.add('active');
// Load the video
player.loadVideoById(videoId);
player.pauseVideo();
// Wait for video to load and get duration
setTimeout(() => {
const duration = player.getDuration() * 1000;
const bpm = parseInt(document.getElementById('bpmInput').value) || 120;
// Generate pattern for the video
generatePatternForVideo(videoId, bpm, duration);
document.getElementById('nowPlayingTitle').textContent = 'Custom Song';
document.getElementById('nowPlayingArtist').textContent = 'YouTube Video';
document.getElementById('loadingIndicator').classList.remove('active');
// Set current song
gameState.currentSong = {
title: 'Custom Song',
artist: 'YouTube',
youtubeId: videoId,
bpm: bpm
};
}, 2000);
}
function extractYouTubeId(url) {
const patterns = [
/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/,
/^([a-zA-Z0-9_-]{11})$/
];
for (const pattern of patterns) {
const match = url.match(pattern);
if (match) return match[1];
}
return null;
}
function generatePatternForVideo(videoId, bpm, duration) {
// Check if we have a predefined pattern
const predefinedSong = SONG_DATABASE.find(s => s.youtubeId === videoId);
if (predefinedSong) {
gameState.notes = predefinedSong.generatePattern(duration).map(([time, drum]) => ({
time: time + (predefinedSong.offset || 0),
drum: drum,
hit: false,
missed: false,
isStarNote: Math.random() < 0.1
}));
} else {
// Generate a basic pattern based on BPM
gameState.notes = generateBasicPattern(bpm, duration);
}
gameState.totalNotes = gameState.notes.length;
updateNotesDisplay();
}
function generateBasicPattern(bpm, duration) {
const pattern = [];
const beatMs = 60000 / bpm;
let time = beatMs * 4; // Start after 4 beats
while (time < duration - 5000) {
// Random pattern variety
const patternType = Math.floor(time / (beatMs * 16)) % 4;
if (patternType === 0) {
// Basic rock beat
pattern.push({ time, drum: 'kick', hit: false, missed: false, isStarNote: Math.random() < 0.08 });
pattern.push({ time, drum: 'hihat', hit: false, missed: false, isStarNote: false });
pattern.push({ time: time + beatMs, drum: 'hihat', hit: false, missed: false, isStarNote: false });
pattern.push({ time: time + beatMs * 2, drum: 'snare', hit: false, missed: false, isStarNote: Math.random() < 0.08 });
pattern.push({ time: time + beatMs * 2, drum: 'hihat', hit: false, missed: false, isStarNote: false });
pattern.push({ time: time + beatMs * 3, drum: 'hihat', hit: false, missed: false, isStarNote: false });
} else if (patternType === 1) {
// Crash on downbeat
pattern.push({ time, drum: 'crash', hit: false, missed: false, isStarNote: Math.random() < 0.1 });
pattern.push({ time, drum: 'kick', hit: false, missed: false, isStarNote: false });
pattern.push({ time: time + beatMs * 2, drum: 'snare', hit: false, missed: false, isStarNote: false });
pattern.push({ time: time + beatMs * 2, drum: 'kick', hit: false, missed: false, isStarNote: false });
for (let i = 0; i < 4; i++) {
pattern.push({ time: time + beatMs * i, drum: 'hihat', hit: false, missed: false, isStarNote: false });
}
} else if (patternType === 2) {
// Tom fills
pattern.push({ time, drum: 'kick', hit: false, missed: false, isStarNote: false });
pattern.push({ time: time + beatMs, drum: 'tom1', hit: false, missed: false, isStarNote: Math.random() < 0.1 });
pattern.push({ time: time + beatMs * 2, drum: 'snare', hit: false, missed: false, isStarNote: false });
pattern.push({ time: time + beatMs * 3, drum: 'tom2', hit: false, missed: false, isStarNote: Math.random() < 0.1 });
} else {
// Ride pattern
pattern.push({ time, drum: 'kick', hit: false, missed: false, isStarNote: false });
pattern.push({ time: time + beatMs * 2, drum: 'snare', hit: false, missed: false, isStarNote: false });
for (let i = 0; i < 4; i++) {
pattern.push({ time: time + beatMs * i, drum: 'ride', hit: false, missed: false, isStarNote: false });
}
}
time += beatMs * 4;
}
// Sort by time
pattern.sort((a, b) => a.time - b.time);
return pattern;
}
// ═══════════════════════════════════════════════════════════════════════════
// SONG LIBRARY
// ═══════════════════════════════════════════════════════════════════════════
function loadSongLibrary() {
const container = document.getElementById('songLibrary');
container.innerHTML = '';
SONG_DATABASE.forEach(song => {
const card = document.createElement('div');
card.className = 'song-card';
card.onclick = () => selectSong(song);
card.innerHTML = `
<div class="song-title">${song.title}</div>
<div class="song-artist">${song.artist}</div>
<div class="song-meta">
<span class="song-bpm">${song.bpm} BPM</span>
<span class="song-difficulty diff-${song.difficulty}">${song.difficulty}</span>
</div>
`;
container.appendChild(card);
});
}
function selectSong(song) {
// Update UI
document.querySelectorAll('.song-card').forEach(c => c.classList.remove('active'));
event.currentTarget.classList.add('active');
// Load video
if (isPlayerReady) {
document.getElementById('loadingIndicator').classList.add('active');
player.loadVideoById(song.youtubeId);
player.pauseVideo();
document.getElementById('bpmInput').value = song.bpm;
setTimeout(() => {
const duration = player.getDuration() * 1000 || 180000;
gameState.notes = song.generatePattern(duration).map(([time, drum]) => ({
time: time + (song.offset || 0),
drum: drum,
hit: false,
missed: false,
isStarNote: Math.random() < 0.1
}));
gameState.totalNotes = gameState.notes.length;
gameState.currentSong = song;
document.getElementById('nowPlayingTitle').textContent = song.title;
document.getElementById('nowPlayingArtist').textContent = song.artist;
document.getElementById('loadingIndicator').classList.remove('active');
updateNotesDisplay();
}, 1500);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// GAME CONTROLS
// ═══════════════════════════════════════════════════════════════════════════
function startGame() {
if (!gameState.currentSong && gameState.notes.length === 0) {
alert('Please select a song or load a YouTube video first!');
return;
}
// Reset game state
resetGameState();
// Show countdown
showCountdown(() => {
gameState.isPlaying = true;
gameState.gameStartTime = performance.now();
// Start video playback with speed
player.setPlaybackRate(gameState.playbackSpeed);
player.seekTo(0);
player.playVideo();
document.getElementById('startBtn').textContent = 'PLAYING...';
document.getElementById('startBtn').disabled = true;
});
}
function resetGameState() {
gameState.score = 0;
gameState.combo = 0;
gameState.maxStreak = 0;
gameState.multiplier = 1;
gameState.notesHit = 0;
gameState.notesMissed = 0;
gameState.starPower = 0;
gameState.starPowerActive = false;
gameState.crowdMeter = 50;
// Reset notes
gameState.notes.forEach(note => {
note.hit = false;
note.missed = false;
});
updateAllDisplays();
}
function showCountdown(callback) {
const overlay = document.getElementById('countdownOverlay');
const numberEl = document.getElementById('countdownNumber');
const textEl = document.getElementById('countdownText');
overlay.classList.add('active');
let count = 3;
textEl.textContent = gameState.currentSong ? gameState.currentSong.title : 'Get Ready!';
const interval = setInterval(() => {
if (count > 0) {
numberEl.textContent = count;
numberEl.style.animation = 'none';
numberEl.offsetHeight; // Trigger reflow
numberEl.style.animation = 'countdownPulse 1s ease-in-out';
count--;
} else {
numberEl.textContent = 'GO!';
numberEl.style.color = '#00ff88';
setTimeout(() => {
overlay.classList.remove('active');
numberEl.style.color = '';
callback();
}, 500);
clearInterval(interval);
}
}, 1000);
}
function endGame() {
gameState.isPlaying = false;
player.pauseVideo();
document.getElementById('startBtn').textContent = 'START';
document.getElementById('startBtn').disabled = false;
// Process with SEFERIM
if (window.SEFERIM && gameState.currentSong) {
const accuracy = gameState.totalNotes > 0 ?
(gameState.notesHit / gameState.totalNotes) * 100 : 0;
window.SEFERIM.processSongLearning(
gameState.currentSong,
{ overall: accuracy },
accuracy
);
}
// Show results
showResults();
}
function showResults() {
const accuracy = gameState.totalNotes > 0 ?
Math.round((gameState.notesHit / gameState.totalNotes) * 100) : 0;
// Calculate stars
let stars = 0;
if (accuracy >= 95) stars = 5;
else if (accuracy >= 85) stars = 4;
else if (accuracy >= 70) stars = 3;
else if (accuracy >= 50) stars = 2;
else if (accuracy >= 30) stars = 1;
document.getElementById('resultsSong').textContent =
gameState.currentSong ? gameState.currentSong.title : 'Custom Song';
document.getElementById('resultsStars').textContent = '★'.repeat(stars) + '☆'.repeat(5 - stars);
document.getElementById('resultScore').textContent = gameState.score.toLocaleString();
document.getElementById('resultAccuracy').textContent = accuracy + '%';
document.getElementById('resultStreak').textContent = gameState.maxStreak;
document.getElementById('resultNotes').textContent = `${gameState.notesHit}/${gameState.totalNotes}`;
document.getElementById('resultsOverlay').classList.add('active');
}
function restartSong() {
document.getElementById('resultsOverlay').classList.remove('active');
startGame();
}
function backToMenu() {
document.getElementById('resultsOverlay').classList.remove('active');
}
function togglePractice() {
const toggle = document.getElementById('practiceToggle');
toggle.classList.toggle('active');
gameState.isPracticeMode = toggle.classList.contains('active');
}
// ═══════════════════════════════════════════════════════════════════════════
// INPUT HANDLING
// ═══════════════════════════════════════════════════════════════════════════
function setupKeyboardControls() {
document.addEventListener('keydown', (e) => {
const drum = KEY_MAP[e.key.toLowerCase()];
if (drum) {
e.preventDefault();
handleDrumHit(drum);
}
// Star power activation
if (e.key === 'Enter' && gameState.starPower >= 50 && !gameState.starPowerActive) {
activateStarPower();
}
});
}
function setupDrumPads() {
document.querySelectorAll('.drum-lane').forEach(pad => {
pad.addEventListener('mousedown', () => {
const drum = pad.dataset.drum;
handleDrumHit(drum);
});
});
}
async function connectMIDI() {
try {
midiAccess = await navigator.requestMIDIAccess();
const inputs = midiAccess.inputs.values();
for (const input of inputs) {
input.onmidimessage = handleMIDIMessage;
}
console.log('MIDI connected');
} catch (err) {
console.log('MIDI not available:', err);
}
}
function handleMIDIMessage(message) {
const [status, note, velocity] = message.data;
if (status >= 144 && status <= 159 && velocity > 0) {
const drum = DRUM_MIDI_MAP[note];
if (drum) {
handleDrumHit(drum);
}
}
}
function handleDrumHit(drum) {
// Visual feedback
const pad = document.querySelector(`.drum-lane[data-drum="${drum}"]`);
if (pad) {
pad.classList.add('hit');
setTimeout(() => pad.classList.remove('hit'), 100);
}
// Create hit effect
createHitEffect(drum);
if (!gameState.isPlaying) return;
// Check for note hit
const currentTime = performance.now() - gameState.gameStartTime;
const adjustedTime = currentTime * gameState.playbackSpeed;
checkNoteHit(drum, adjustedTime);
// Process with SEFERIM
if (window.SEFERIM) {
window.SEFERIM.processDrumHit(drum, 100, adjustedTime, adjustedTime,
gameState.currentSong ? gameState.currentSong.title : 'freeplay');
}
}
function checkNoteHit(drum, currentTime) {
// Find closest unhit note for this drum
let closestNote = null;
let closestDiff = Infinity;
for (const note of gameState.notes) {
if (note.drum !== drum || note.hit || note.missed) continue;
const diff = Math.abs(note.time - currentTime);
if (diff < closestDiff && diff < TIMING.miss) {
closestNote = note;
closestDiff = diff;
}
}
if (closestNote) {
closestNote.hit = true;
// Determine judgment
let judgment, points;
if (closestDiff <= TIMING.perfect) {
judgment = 'PERFECT';
points = 100;
} else if (closestDiff <= TIMING.great) {
judgment = 'GREAT';
points = 75;
} else if (closestDiff <= TIMING.good) {
judgment = 'GOOD';
points = 50;
} else {
judgment = 'OK';
points = 25;
}
// Update stats
gameState.notesHit++;
gameState.combo++;
gameState.maxStreak = Math.max(gameState.maxStreak, gameState.combo);
// Calculate multiplier
if (gameState.combo >= 30) gameState.multiplier = 4;
else if (gameState.combo >= 20) gameState.multiplier = 3;
else if (gameState.combo >= 10) gameState.multiplier = 2;
else gameState.multiplier = 1;
// Star power bonus
const starBonus = gameState.starPowerActive ? 2 : 1;
gameState.score += points * gameState.multiplier * starBonus;
// Star power charge
if (closestNote.isStarNote) {
gameState.starPower = Math.min(100, gameState.starPower + 10);
} else {
gameState.starPower = Math.min(100, gameState.starPower + 2);
}
// Crowd meter
gameState.crowdMeter = Math.min(100, gameState.crowdMeter + 2);
// Show judgment
showJudgment(judgment);
// Create explosion
createExplosion(drum, judgment);
} else {
// Missed hit (wrong timing or wrong drum)
// Don't break combo for phantom hits
}
updateAllDisplays();
}
function missNote(note) {
note.missed = true;
gameState.notesMissed++;
gameState.combo = 0;
gameState.multiplier = 1;
if (!gameState.isPracticeMode) {
gameState.crowdMeter = Math.max(0, gameState.crowdMeter - 5);
if (gameState.crowdMeter <= 0) {
endGame();
}
}
showJudgment('MISS');
updateAllDisplays();
}
// ═══════════════════════════════════════════════════════════════════════════
// STAR POWER
// ═══════════════════════════════════════════════════════════════════════════
function activateStarPower() {
if (gameState.starPower < 50 || gameState.starPowerActive) return;
gameState.starPowerActive = true;
document.getElementById('starPowerFill').classList.add('active');
document.getElementById('starPowerStatus').textContent = 'ACTIVE!';
// Drain star power over time
const drainInterval = setInterval(() => {
gameState.starPower -= 2;
updateStarPowerDisplay();
if (gameState.starPower <= 0) {
gameState.starPowerActive = false;
document.getElementById('starPowerFill').classList.remove('active');
document.getElementById('starPowerStatus').textContent = 'CHARGING';
clearInterval(drainInterval);
}
}, 100);
}
// ═══════════════════════════════════════════════════════════════════════════
// VISUAL EFFECTS
// ═══════════════════════════════════════════════════════════════════════════
function showJudgment(text) {
const display = document.getElementById('judgmentDisplay');
display.textContent = text;
display.className = 'judgment-display show judgment-' + text.toLowerCase();
setTimeout(() => {
display.classList.remove('show');
}, 300);
}
function createHitEffect(drum) {
const pad = document.querySelector(`.drum-lane[data-drum="${drum}"]`);
if (!pad) return;
const rect = pad.getBoundingClientRect();
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
// Add ring effect
for (let i = 0; i < 8; i++) {
activeEffects.push({
type: 'ring',
x: x,
y: y,
radius: 20,
maxRadius: 60 + Math.random() * 30,
color: DRUM_COLORS[drum],
alpha: 1
});
}
}
function createExplosion(drum, judgment) {
const pad = document.querySelector(`.drum-lane[data-drum="${drum}"]`);
if (!pad) return;
const rect = pad.getBoundingClientRect();
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
const particleCount = judgment === 'PERFECT' ? 20 : 10;
for (let i = 0; i < particleCount; i++) {
const angle = (Math.PI * 2 / particleCount) * i;
const speed = 3 + Math.random() * 5;
activeEffects.push({
type: 'particle',
x: x,
y: y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
radius: 3 + Math.random() * 4,
color: DRUM_COLORS[drum],
alpha: 1,
decay: 0.02 + Math.random() * 0.02
});
}
}
function updateEffects() {
effectsCtx.clearRect(0, 0, effectsCanvas.width, effectsCanvas.height);
for (let i = activeEffects.length - 1; i >= 0; i--) {
const effect = activeEffects[i];
if (effect.type === 'ring') {
effect.radius += 3;
effect.alpha -= 0.05;
effectsCtx.beginPath();
effectsCtx.arc(effect.x, effect.y, effect.radius, 0, Math.PI * 2);
effectsCtx.strokeStyle = effect.color + Math.floor(effect.alpha * 255).toString(16).padStart(2, '0');
effectsCtx.lineWidth = 3;
effectsCtx.stroke();
if (effect.alpha <= 0) {
activeEffects.splice(i, 1);
}
} else if (effect.type === 'particle') {
effect.x += effect.vx;
effect.y += effect.vy;
effect.vy += 0.2; // Gravity
effect.alpha -= effect.decay;
effectsCtx.beginPath();
effectsCtx.arc(effect.x, effect.y, effect.radius, 0, Math.PI * 2);
effectsCtx.fillStyle = effect.color + Math.floor(Math.max(0, effect.alpha) * 255).toString(16).padStart(2, '0');
effectsCtx.fill();
if (effect.alpha <= 0) {
activeEffects.splice(i, 1);
}
}
}
}
// ═══════════════════════════════════════════════════════════════════════════
// NOTE HIGHWAY RENDERING
// ═══════════════════════════════════════════════════════════════════════════
function renderHighway() {
const width = highwayCanvas.width;
const height = highwayCanvas.height;
// Clear
highwayCtx.fillStyle = 'rgba(10, 0, 20, 0.3)';
highwayCtx.fillRect(0, 0, width, height);
// Draw lane lines
const laneCount = DRUMS.length;
const laneWidth = width / laneCount;
highwayCtx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
highwayCtx.lineWidth = 1;
for (let i = 1; i < laneCount; i++) {
highwayCtx.beginPath();
highwayCtx.moveTo(i * laneWidth, 0);
highwayCtx.lineTo(i * laneWidth, height);
highwayCtx.stroke();
}
// Draw beat lines
if (gameState.currentSong) {
const bpm = parseInt(document.getElementById('bpmInput').value) || 120;
const beatMs = 60000 / bpm;
const currentTime = gameState.isPlaying ?
(performance.now() - gameState.gameStartTime) * gameState.playbackSpeed : 0;
const lookAhead = 2000; // 2 seconds of notes visible
for (let time = currentTime; time < currentTime + lookAhead; time += beatMs) {
const y = height - ((time - currentTime) / lookAhead * (height - 100));
highwayCtx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
highwayCtx.beginPath();
highwayCtx.moveTo(0, y);
highwayCtx.lineTo(width, y);
highwayCtx.stroke();
}
}
// Draw notes
if (gameState.isPlaying || gameState.notes.length > 0) {
const currentTime = gameState.isPlaying ?
(performance.now() - gameState.gameStartTime) * gameState.playbackSpeed : 0;
const lookAhead = 2000;
const hitZoneY = height - 80;
for (const note of gameState.notes) {
if (note.hit || note.missed) continue;
const timeDiff = note.time - currentTime;
// Check for missed notes
if (timeDiff < -TIMING.miss && gameState.isPlaying) {
missNote(note);
continue;
}
// Only render notes within view
if (timeDiff > lookAhead || timeDiff < -200) continue;
const y = hitZoneY - (timeDiff / lookAhead * (hitZoneY - 50));
const laneIndex = DRUMS.indexOf(note.drum);
const x = laneIndex * laneWidth + laneWidth / 2;
// Note glow
const gradient = highwayCtx.createRadialGradient(x, y, 0, x, y, 25);
gradient.addColorStop(0, DRUM_COLORS[note.drum] + '80');
gradient.addColorStop(1, 'transparent');
highwayCtx.fillStyle = gradient;
highwayCtx.fillRect(x - 30, y - 30, 60, 60);
// Note body
highwayCtx.beginPath();
highwayCtx.arc(x, y, 18, 0, Math.PI * 2);
if (note.isStarNote) {
// Star note - special appearance
highwayCtx.fillStyle = '#00d4ff';
highwayCtx.fill();
highwayCtx.strokeStyle = '#ffffff';
highwayCtx.lineWidth = 3;
highwayCtx.stroke();
// Star symbol
highwayCtx.fillStyle = '#ffffff';
highwayCtx.font = '12px Arial';
highwayCtx.textAlign = 'center';
highwayCtx.textBaseline = 'middle';
highwayCtx.fillText('★', x, y);
} else {
highwayCtx.fillStyle = DRUM_COLORS[note.drum];
highwayCtx.fill();
highwayCtx.strokeStyle = '#ffffff';
highwayCtx.lineWidth = 2;
highwayCtx.stroke();
}
}
}
// Draw hit zone line
highwayCtx.strokeStyle = '#ffffff';
highwayCtx.lineWidth = 2;
highwayCtx.beginPath();
highwayCtx.moveTo(0, height - 80);
highwayCtx.lineTo(width, height - 80);
highwayCtx.stroke();
}
// ═══════════════════════════════════════════════════════════════════════════
// DISPLAY UPDATES
// ═══════════════════════════════════════════════════════════════════════════
function updateAllDisplays() {
updateScoreDisplay();
updateComboDisplay();
updateStarPowerDisplay();
updateCrowdDisplay();
updateNotesDisplay();
updateSeferimDisplay();
}
function updateScoreDisplay() {
document.getElementById('scoreDisplay').textContent = gameState.score.toLocaleString();
document.getElementById('headerScore').textContent = gameState.score.toLocaleString();
document.getElementById('multiplierDisplay').textContent = gameState.multiplier + 'x';
}
function updateComboDisplay() {
document.getElementById('comboDisplay').textContent = gameState.combo;
document.getElementById('headerStreak').textContent = gameState.combo;
document.getElementById('maxStreakDisplay').textContent = gameState.maxStreak;
const accuracy = gameState.notesHit + gameState.notesMissed > 0 ?
Math.round((gameState.notesHit / (gameState.notesHit + gameState.notesMissed)) * 100) : 100;
document.getElementById('accuracyDisplay').textContent = accuracy + '%';
}
function updateStarPowerDisplay() {
const fill = document.getElementById('starPowerFill');
fill.style.width = gameState.starPower + '%';
if (gameState.starPower >= 50 && !gameState.starPowerActive) {
fill.classList.add('ready');
document.getElementById('starPowerStatus').textContent = 'READY!';
} else if (!gameState.starPowerActive) {
fill.classList.remove('ready');
document.getElementById('starPowerStatus').textContent = 'CHARGING';
}
}
function updateCrowdDisplay() {
const fill = document.getElementById('crowdFill');
fill.style.width = gameState.crowdMeter + '%';
const reaction = document.getElementById('crowdReaction');
if (gameState.crowdMeter >= 80) {
fill.style.background = 'linear-gradient(90deg, #00ff88, #00ffcc)';
reaction.textContent = 'AMAZING!';
} else if (gameState.crowdMeter >= 60) {
fill.style.background = 'linear-gradient(90deg, #f7c948, #00ff88)';
reaction.textContent = 'Great!';
} else if (gameState.crowdMeter >= 40) {
fill.style.background = 'linear-gradient(90deg, #ff6b35, #f7c948)';
reaction.textContent = 'Good';
} else if (gameState.crowdMeter >= 20) {
fill.style.background = 'linear-gradient(90deg, #ff006e, #ff6b35)';
reaction.textContent = 'Meh...';
} else {
fill.style.background = 'linear-gradient(90deg, #ff0000, #ff006e)';
reaction.textContent = 'BOO!';
}
}
function updateNotesDisplay() {
document.getElementById('notesHitDisplay').textContent =
`${gameState.notesHit}/${gameState.totalNotes}`;
}
function updateVideoTime() {
if (!player || !isPlayerReady) return;
try {
const current = player.getCurrentTime() || 0;
const duration = player.getDuration() || 0;
const formatTime = (s) => {
const mins = Math.floor(s / 60);
const secs = Math.floor(s % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
document.getElementById('videoTime').textContent =
`${formatTime(current)} / ${formatTime(duration)}`;
} catch (e) {
// Player not ready
}
}
// ═══════════════════════════════════════════════════════════════════════════
// SPEED CONTROL
// ═══════════════════════════════════════════════════════════════════════════
function setupSpeedControl() {
const slider = document.getElementById('speedSlider');
const display = document.getElementById('speedValue');
slider.addEventListener('input', () => {
const speed = slider.value / 100;
gameState.playbackSpeed = speed;
display.textContent = slider.value + '%';
if (player && isPlayerReady && gameState.isPlaying) {
player.setPlaybackRate(speed);
}
});
}
// ═══════════════════════════════════════════════════════════════════════════
// TAP TEMPO
// ═══════════════════════════════════════════════════════════════════════════
function startTapTempo() {
const now = performance.now();
// Clear old taps
if (tapTimeout) clearTimeout(tapTimeout);
tapTimes.push(now);
// Keep only recent taps
if (tapTimes.length > 8) {
tapTimes.shift();
}
// Calculate BPM if we have enough taps
if (tapTimes.length >= 2) {
const intervals = [];
for (let i = 1; i < tapTimes.length; i++) {
intervals.push(tapTimes[i] - tapTimes[i-1]);
}
const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
const bpm = Math.round(60000 / avgInterval);
if (bpm >= 60 && bpm <= 300) {
document.getElementById('bpmInput').value = bpm;
}
}
// Reset after 2 seconds of no taps
tapTimeout = setTimeout(() => {
tapTimes = [];
}, 2000);
}
// ═══════════════════════════════════════════════════════════════════════════
// SEFERIM INTEGRATION
// ═══════════════════════════════════════════════════════════════════════════
function initSeferim() {
const container = document.getElementById('seferimFamilies');
container.innerHTML = '';
for (let i = 0; i < 16; i++) {
const bar = document.createElement('div');
bar.className = 'family-bar';
bar.id = `family-bar-${i}`;
bar.innerHTML = '<div class="family-bar-fill" style="height: 0%"></div>';
container.appendChild(bar);
}
}
function updateSeferimDisplay() {
if (!window.SEFERIM) return;
try {
const status = window.SEFERIM.getStatus();
document.getElementById('seferimPhi').textContent = status.PhiGlobal.toFixed(2);
document.getElementById('seferimK').textContent = status.K.toLocaleString();
document.getElementById('seferimLevel').textContent = status.difficulty.level;
status.families.forEach((family, index) => {
const bar = document.getElementById(`family-bar-${index}`);
if (bar) {
const fill = bar.querySelector('.family-bar-fill');
if (fill) {
fill.style.height = `${family.coherence * 100}%`;
}
if (family.pressure > 0.3) {
bar.classList.add('active');
} else {
bar.classList.remove('active');
}
}
});
} catch (e) {
console.log('SEFERIM not ready');
}
}
// ═══════════════════════════════════════════════════════════════════════════
// GAME LOOP
// ═══════════════════════════════════════════════════════════════════════════
function gameLoop() {
renderHighway();
updateEffects();
updateVideoTime();
animationId = requestAnimationFrame(gameLoop);
}
// ═══════════════════════════════════════════════════════════════════════════
// INITIALIZATION
// ═══════════════════════════════════════════════════════════════════════════
document.addEventListener('DOMContentLoaded', init);
// Wait for SEFERIM
const checkSeferim = setInterval(() => {
if (window.SEFERIM) {
clearInterval(checkSeferim);
updateSeferimDisplay();
setInterval(updateSeferimDisplay, 2000);
console.log('[SEFERIM] Song Mode Integration Active');
}
}, 100);
</script>
</body>
</html>