User prompt
Increase hold particle size
User prompt
Give notes a scale pulse to the beat after they reach full scale from scaling in. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Add a slight sine wave to stars upwards travel. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Make it faster. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Make the star particle alpha pulse faster ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Add a slight alpha pulse to the stars to simulate twinkling. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Star background should be moving already when the game starts.
User prompt
Increase their size by another 30%
User prompt
Increase background particle average size by 20%
User prompt
Background particles should be different sizes and speeds and travel upwards to make a moving star field effect.
User prompt
Make background particles that create a multicolored neon star field effect with different sized parallax. Traveling from bottom to top.
Code edit (1 edits merged)
Please save this source code
User prompt
Currently all tap notes are showing up as red, make sure left zone notes are blue.
User prompt
Left zone notes should be blue, not red.
User prompt
Zone notes will be color coded now. Left zone Blue, right zone red. Synced tap notes will be pink and synced hold notes will be purple.
User prompt
Remove tap and hold pattern type. Notes should not spawn beside hold notes other than sync hold notes.
User prompt
Add more space between tap notes in complex song patterns.
User prompt
Reduce amount of hold notes.
User prompt
Change note hit detection to use the entire size of the note core (good) and to award more points (perfect) for the center portion. Add large pop up text to inform the player of their accuracy.
User prompt
Add a 5% safety zone on the top and sides and a 10% safety zone on the bottom of the screen where notes do not spawn. Also take this into consideration for hold note ends.
User prompt
Synced and synced hold notes should be the same color
User prompt
Remove the win scenario.
User prompt
Reduce note density in dense patterns and lean more towards syncing notes and holds between sides overall
User prompt
Make the length between the start and end of hold notes shorter overall and make sure that the placement of the starting note does not place the end off screen.
User prompt
Make the space between hold notes shorter overall and make sure the end does not go off the screen. Smarter placement of hold note starts.
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var BackgroundParticle = Container.expand(function () {
var self = Container.call(this);
self.particleGraphics = null;
self.speed = 0;
self.initialScale = 1;
self.baseAlpha = 0.1; // Default base alpha, instance specific value set in init
self.initialX = 0; // For sine wave horizontal movement
self.sineAmplitude = 0; // Amplitude of the sine wave
self.sineFrequency = 0; // Frequency of the sine wave
// Method to handle the twinkling animation
self._startTwinkleAnimation = function () {
// Stop any existing alpha tween on this particle to ensure only one runs.
tween.stop(self, {
alpha: true
});
var currentAlpha = self.alpha;
// Calculate pulse range: +/- 40% of baseAlpha for a noticeable but subtle twinkle.
var pulseDelta = self.baseAlpha * 0.4;
var targetMinAlpha = Math.max(0.05, self.baseAlpha - pulseDelta); // Ensure minimum visibility
var targetMaxAlpha = Math.min(1.0, self.baseAlpha + pulseDelta); // Cap at 1.0
// If baseAlpha is very low or high, the pulse range might be tiny.
// Try to ensure a minimum pulse range, otherwise, don't twinkle if too small.
if (targetMaxAlpha - targetMinAlpha < 0.05) {
if (self.baseAlpha < 0.15) {
// If base is very low, try to allow a slightly larger upward pulse
targetMaxAlpha = Math.min(1.0, self.baseAlpha + 0.1);
} else if (self.baseAlpha > 0.85) {
// If base is very high, allow a slightly larger downward pulse
targetMinAlpha = Math.max(0.05, self.baseAlpha - 0.1);
}
// If the range is still too small, it's better not to twinkle this particle.
if (targetMaxAlpha - targetMinAlpha < 0.05) {
return;
}
}
var nextTargetAlpha;
// Determine the next target alpha to create an alternating pulse:
// If current alpha is at base, or closer to max (or at max), tween to min.
// Otherwise (closer to min, or at min), tween to max.
if (self.alpha === self.baseAlpha || Math.abs(currentAlpha - targetMaxAlpha) < Math.abs(currentAlpha - targetMinAlpha)) {
nextTargetAlpha = targetMinAlpha;
} else {
nextTargetAlpha = targetMaxAlpha;
}
var pulseDuration = 300 + Math.random() * 400; // Random duration: 0.3s to 0.7s (much faster pulse)
tween(self, {
alpha: nextTargetAlpha
}, {
duration: pulseDuration,
easing: tween.easeInOut,
// Smooth in-out easing for a gentle pulse
onFinish: function onFinish() {
// Recursively call to continue the twinkle effect.
// If the particle is re-initialized, self.init() will call tween.stop(),
// effectively breaking this chain and starting a new one.
if (self && self._startTwinkleAnimation) {
self._startTwinkleAnimation();
}
}
});
};
self.init = function () {
// Stop any existing tweens when particle is recycled/reinitialized.
// This is crucial to prevent multiple tweens running on a recycled particle.
tween.stop(self, {
alpha: true
});
// Random size (for parallax effect)
var scale = 0.312 + Math.random() * 1.248; // Varying sizes from 0.312x to 1.56x
self.initialScale = scale;
self.scale.set(scale);
// Random neon-like color
var colors = [0xff00ff, 0x00ffff, 0xffff00, 0xff0000, 0x00ff00, 0x0000ff, 0xffa500, 0xda70d6];
var color = colors[Math.floor(Math.random() * colors.length)];
// Particle graphics
if (self.particleGraphics) {
self.particleGraphics.destroy();
}
self.particleGraphics = self.attachAsset('starParticle', {
anchorX: 0.5,
anchorY: 0.5
});
self.particleGraphics.tint = color;
// Random speed (slower particles appear further away)
self.speed = (0.5 + Math.random() * 1.5) * scale; // Speed influenced by scale for parallax
// Random initial X position
self.x = Math.random() * GAME_WIDTH;
self.initialX = self.x; // Store the initial X for sine wave calculation
// Sine wave parameters - randomize for variety
self.sineAmplitude = 20 + Math.random() * 60; // Amplitude: 20px to 80px horizontal sway
self.sineFrequency = 0.0015 + Math.random() * 0.003; // Frequency: determines waviness
// Start just below the screen
self.y = GAME_HEIGHT + Math.random() * 100 + self.particleGraphics.height * scale;
// Alpha and Twinkle logic
self.baseAlpha = 0.1 + Math.random() * 0.7; // Set instance-specific base transparency
self.alpha = self.baseAlpha; // Initialize current alpha to baseAlpha
self._startTwinkleAnimation(); // Start the twinkling animation
};
self.updateParticle = function () {
self.y -= self.speed;
// Apply sine wave to x position
// yProgress increases as the particle moves up (self.y decreases from GAME_HEIGHT to 0)
// This makes the sine wave phase dependent on the particle's vertical position.
var yProgress = GAME_HEIGHT - self.y;
var sineOffset = self.sineAmplitude * Math.sin(self.sineFrequency * yProgress);
self.x = self.initialX + sineOffset;
// Reset if particle moves off the top of the screen
if (self.y < -self.particleGraphics.height * self.initialScale) {
self.init(); // Re-initialize to recycle the particle
self.y = GAME_HEIGHT + Math.random() * 50 + self.particleGraphics.height * self.initialScale; // Ensure it's off-screen at bottom
}
};
// Initialize when created
self.init();
return self;
});
var Note = Container.expand(function (noteData, spawnTime) {
var self = Container.call(this);
// Note properties
self.noteData = noteData;
self.type = noteData.type; // 'tap', 'hold', 'flick'
self.targetX = noteData.x;
self.targetY = noteData.y;
self.hitTime = noteData.time;
self.duration = noteData.duration || 0; // For hold notes
// self.flickTarget = noteData.flickTarget || null; // Removed
self.spawnTime = spawnTime;
self.zone = noteData.x < GAME_WIDTH / 2 ? 'left' : 'right'; // Determine zone
// Determine note color based on type and zone, and if it's a sync note
if (noteData.color && noteData.color === SYNC_NOTE_COLOR) {
// This note was flagged as a sync note by SongGenerator
if (self.type === 'tap') {
self.color = COLOR_SYNC_TAP;
} else if (self.type === 'hold') {
self.color = COLOR_SYNC_HOLD;
} else {
// Fallback for unexpected sync note types (should not occur with current patterns)
self.color = self.zone === 'left' ? COLOR_LEFT_ZONE : COLOR_RIGHT_ZONE;
}
} else {
// Standard note, color by zone
self.color = self.zone === 'left' ? COLOR_LEFT_ZONE : COLOR_RIGHT_ZONE;
}
// self.requiredFlickDirection = noteData.flickDirection || null; // Removed
// State
self.active = true;
self.isHit = false;
self.isMissed = false;
self.isSpawning = true;
self.isHolding = false; // True if player is currently holding this note
self.holdSuccessfullyCompleted = false; // True if hold was completed successfully
self.holdParticles = []; // Stores active connection particles for this hold note
self.lastParticleEmissionTime = 0; // For throttling particle emission
self.PARTICLE_EMISSION_INTERVAL = 50; // Emit particles every 50ms during hold
// self.isHolding = false; // For hold notes // Duplicate line, already declared above
self.holdStarted = false;
self.beatPulseInterval = null; // For managing the beat pulse animation
// Visual components
var assetName = self.type === 'hold' ? 'holdNoteCore' : 'noteCore'; // Removed flickNoteCore
self.noteGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
self.noteGraphics.tint = self.color;
// --- Beat Pulse Animation Methods ---
self._startBeatPulseAnimation = function () {
// Check conditions at the moment of attempted execution
// Ensures the note should be pulsing (active, not hit/missed, not spawning)
if (!self || !self.active || self.isHit || self.isMissed || self.isSpawning) {
tween.stop(self, {
scaleX: true,
scaleY: true
}); // Stop any scale tweens on self
return;
}
// Stop any potentially conflicting scale tweens on 'self' before starting a new one.
tween.stop(self, {
scaleX: true,
scaleY: true
});
tween(self, {
scaleX: 1.15,
// Pulse out to 115%
scaleY: 1.15
}, {
duration: BEAT_DURATION_MS * 0.2,
// Pulse out duration (e.g., 20% of a beat)
easing: tween.easeOut,
onFinish: function onFinish() {
// Check conditions again before tweening back, in case state changed during pulse-out
if (self && self.active && !self.isHit && !self.isMissed && !self.isSpawning) {
tween(self, {
scaleX: 1.0,
// Pulse back to normal scale
scaleY: 1.0
}, {
duration: BEAT_DURATION_MS * 0.2,
// Pulse in duration
easing: tween.easeIn
});
}
// If state changed (e.g., note was hit/missed), other effects will handle the scale.
}
});
};
self._clearBeatPulseLoop = function () {
if (self.beatPulseInterval) {
LK.clearInterval(self.beatPulseInterval);
self.beatPulseInterval = null;
}
// Stop any ongoing beat pulse scale animation on 'self'.
tween.stop(self, {
scaleX: true,
scaleY: true
});
// It's generally safer to let specific effect methods (hit, miss, complete, fail)
// handle the final scale of the note, rather than resetting to 1.0 here,
// as that might conflict with their animations.
};
self._initiateBeatPulseLoop = function () {
self._clearBeatPulseLoop(); // Clear any existing interval before starting a new one.
// Proceed only if the note is in a state where it should be pulsing.
if (self && self.active && !self.isHit && !self.isMissed && !self.isSpawning) {
self._startBeatPulseAnimation(); // Start the first pulse immediately.
self.beatPulseInterval = LK.setInterval(function () {
// Continuously check conditions inside the interval callback.
if (self && self.active && !self.isHit && !self.isMissed && !self.isSpawning) {
self._startBeatPulseAnimation();
} else {
// If conditions are no longer met (e.g., note became inactive, hit, or missed),
// clear the interval. This acts as a self-cleanup mechanism for the interval.
self._clearBeatPulseLoop();
}
}, BEAT_DURATION_MS);
}
};
// --- End Beat Pulse Animation Methods ---
// self.flickDirectionIndicator logic removed
self.glowRing = self.attachAsset('glowRing', {
anchorX: 0.5,
anchorY: 0.5
});
self.glowRing.tint = self.color;
self.glowRing.alpha = 0.3;
// Position
self.x = self.targetX;
self.y = self.targetY;
// Start invisible and scale up
self.alpha = 0;
self.scaleX = 0.1;
self.scaleY = 0.1;
// Define createHoldTrail method first
self.createHoldTrail = function () {
// Determine scanner direction at hit time
var cycleTime = self.hitTime % SCANNER_CYCLE_DURATION;
var scannerMovingUp = cycleTime < SCANNER_HALF_CYCLE;
var direction = scannerMovingUp ? -1 : 1; // -1 for up, 1 for down
// Calculate initial trail length based on original duration
var originalRequestedDuration = self.duration;
var maxPossibleTrailPixelLength = originalRequestedDuration / SCANNER_HALF_CYCLE * PLAY_AREA_HEIGHT;
var actualTrailPixelLength = maxPossibleTrailPixelLength;
// Cap trail length to stay within SCANNER_Y_MIN and SCANNER_Y_MAX
if (direction === -1) {
// Trail goes upwards from the note
// End note Y would be self.y - actualTrailPixelLength. It must be >= SCANNER_Y_MIN.
if (self.y - actualTrailPixelLength < SCANNER_Y_MIN) {
actualTrailPixelLength = self.y - SCANNER_Y_MIN;
}
} else {
// Trail goes downwards from the note
// End note Y would be self.y + actualTrailPixelLength. It must be <= SCANNER_Y_MAX.
if (self.y + actualTrailPixelLength > SCANNER_Y_MAX) {
actualTrailPixelLength = SCANNER_Y_MAX - self.y;
}
}
actualTrailPixelLength = Math.max(0, actualTrailPixelLength); // Ensure length is not negative
// If the trail length was capped, adjust self.duration to match the visual representation.
// Avoid division by zero if critical constants are invalid.
if (actualTrailPixelLength < maxPossibleTrailPixelLength) {
if (PLAY_AREA_HEIGHT > 0 && SCANNER_HALF_CYCLE > 0) {
self.duration = actualTrailPixelLength / PLAY_AREA_HEIGHT * SCANNER_HALF_CYCLE;
} else if (actualTrailPixelLength === 0) {
// If trail length is zero due to invalid constants or extreme capping, set duration to 0.
self.duration = 0;
}
// else, retain original duration if recalculation is not possible but length wasn't zeroed.
}
self.duration = Math.max(0, self.duration); // Final check to ensure duration is non-negative.
// Create the main connecting trail line
var trailLine = self.attachAsset('holdTrail', {
anchorX: 0.5,
anchorY: 0 // Anchor at the top of the trail asset
});
trailLine.tint = self.color;
trailLine.alpha = 0.6;
trailLine.width = 20; // Set a visible width for the trail line
trailLine.height = actualTrailPixelLength;
if (direction === -1) {
// Trail goes upwards from the note
trailLine.y = -actualTrailPixelLength;
} else {
// Trail goes downwards from the note
trailLine.y = 0; // AnchorY is 0, so top of trailLine is at note's y
}
self.holdTrails.push(trailLine);
// Add end indicator note, positioned relative to the start note
self.endNote = self.attachAsset('noteCore', {
anchorX: 0.5,
anchorY: 0.5
});
self.endNote.tint = self.color;
self.endNote.alpha = 0.8;
self.endNote.scaleX = 0.7;
self.endNote.scaleY = 0.7;
self.endNote.y = actualTrailPixelLength * direction;
// Add a subtle glow to the end note, positioned with the end note
self.endGlow = self.attachAsset('glowRing', {
anchorX: 0.5,
anchorY: 0.5
});
self.endGlow.tint = self.color;
self.endGlow.alpha = 0.3;
self.endGlow.scaleX = 0.5;
self.endGlow.scaleY = 0.5;
self.endGlow.y = actualTrailPixelLength * direction;
};
// Hold trail for hold notes
self.holdTrails = [];
if (self.type === 'hold' && self.duration > 0) {
self.createHoldTrail();
}
self.spawnIn = function () {
self.isSpawning = true;
tween(self, {
alpha: 1,
scaleX: 1,
scaleY: 1
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
self.isSpawning = false;
self._initiateBeatPulseLoop(); // Start the beat pulse animation
}
});
};
self.showHitEffect = function () {
self._clearBeatPulseLoop(); // Stop beat pulse
// Primarily for TAP notes now
LK.getSound('hitSound').play();
self.explodeIntoParticles();
// For tap notes, there's no endNote or endGlow to clean typically.
// This method makes the note inactive.
tween(self, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
self.active = false;
self._clearBeatPulseLoop(); // Ensure cleared as note becomes inactive
}
});
};
self.startHoldEffect = function () {
self._clearBeatPulseLoop(); // Stop beat pulse as interaction begins
// Called for HOLD note initial interaction
LK.getSound('hitSound').play();
// Visual cue for hold start, e.g., a slight pulse. Note remains active.
tween(self.noteGraphics, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 100,
onFinish: function onFinish() {
tween(self.noteGraphics, {
scaleX: 1,
scaleY: 1
}, {
duration: 100
});
}
});
// EndNote and EndGlow MUST remain visible. Note itself is not deactivated here.
};
// self.showFlickEffect method removed.
// self.createFlickTrail method is now obsolete and removed.
self.showMissEffect = function () {
self._clearBeatPulseLoop(); // Stop beat pulse
LK.getSound('missSound').play();
// Flick direction indicator cleanup removed
tween(self.noteGraphics, {
tint: 0xff0000
}, {
duration: 150,
onFinish: function onFinish() {
tween(self, {
alpha: 0.3
}, {
duration: 300,
onFinish: function onFinish() {
self.active = false;
self._clearBeatPulseLoop(); // Ensure cleared as note becomes inactive
}
});
}
});
};
self.explodeIntoParticles = function () {
// Flick direction indicator cleanup removed
var particleCount = 12;
for (var i = 0; i < particleCount; i++) {
var particle = game.attachAsset('particle', {
anchorX: 0.5,
anchorY: 0.5
});
particle.tint = self.color;
particle.x = self.x;
particle.y = self.y;
var angle = i / particleCount * Math.PI * 2;
var speed = 150 + Math.random() * 100;
var targetX = self.x + Math.cos(angle) * speed;
var targetY = self.y + Math.sin(angle) * speed;
tween(particle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: 0.3,
scaleY: 0.3
}, {
duration: 600,
easing: tween.easeOut,
onFinish: function onFinish() {
if (particle && particle.destroy) {
particle.destroy();
}
}
});
}
};
self.updateHoldEffects = function (scannerCurrentY) {
if (self.type === 'hold' && self.isHolding && self.active && !self.holdSuccessfullyCompleted && !self.isMissed) {
var currentTime = Date.now();
if (currentTime - self.lastParticleEmissionTime > self.PARTICLE_EMISSION_INTERVAL) {
self.lastParticleEmissionTime = currentTime;
var trailStartY = self.y;
var trailEndY = self.y + (self.endNote ? self.endNote.y : 0); // endNote.y is relative
var scannerIsOverTrail = self.endNote && scannerCurrentY >= Math.min(trailStartY, trailEndY) && scannerCurrentY <= Math.max(trailStartY, trailEndY);
if (scannerIsOverTrail) {
var particle = game.attachAsset('holdConnectionParticle', {
anchorX: 0.5,
anchorY: 0.5
});
particle.x = self.x;
particle.y = scannerCurrentY;
particle.tint = self.color;
particle.alpha = 0.8;
self.holdParticles.push(particle);
tween(particle, {
alpha: 0,
scaleX: 0.2,
scaleY: 0.2
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (particle && particle.destroy) {
particle.destroy();
}
var index = self.holdParticles.indexOf(particle);
if (index > -1) {
self.holdParticles.splice(index, 1);
}
}
});
}
}
}
};
self.cleanupHoldVisuals = function () {
self.holdParticles.forEach(function (p) {
if (p && p.destroy) {
p.destroy();
}
});
self.holdParticles = [];
if (self.endNote && self.endNote.destroy) {
self.endNote.destroy();
}
if (self.endGlow && self.endGlow.destroy) {
self.endGlow.destroy();
}
self.endNote = null;
self.endGlow = null;
self.holdTrails.forEach(function (trail) {
if (trail && trail.destroy) {
trail.destroy();
}
});
self.holdTrails = [];
};
self.completeHold = function () {
if (self.holdSuccessfullyCompleted || self.isMissed) {
return;
}
self._clearBeatPulseLoop(); // Stop beat pulse
self.holdSuccessfullyCompleted = true;
self.isHolding = false;
self.explodeIntoParticles(); // Main note explosion
self.cleanupHoldVisuals(); // Clean up trail, end note, particles
// The main note (self) will become inactive due to explodeIntoParticles tween or explicitly here
tween(self, {
alpha: 0
}, {
duration: 100,
delay: 100,
onFinish: function onFinish() {
self.active = false;
self._clearBeatPulseLoop(); // Ensure cleared as note becomes inactive
}
});
};
self.failHold = function () {
if (self.isMissed || self.holdSuccessfullyCompleted) {
return;
}
self._clearBeatPulseLoop(); // Stop beat pulse
self.isMissed = true;
self.isHolding = false;
self.showMissEffect(); // Main note shows miss effect
self.cleanupHoldVisuals(); // Clean up trail, end note, particles
// self.active will be set to false by showMissEffect's tween's onFinish
};
return self;
});
var Scanner = Container.expand(function () {
var self = Container.call(this);
self.glow = self.attachAsset('scannerGlow', {
anchorX: 0,
anchorY: 0.5
});
self.glow.alpha = 0.2;
self.line = self.attachAsset('scannerLine', {
anchorX: 0,
anchorY: 0.5
});
self.x = 0;
self.isMovingUp = true; // Start moving up
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Game Constants
0;
var GAME_WIDTH = 2048;
var GAME_HEIGHT = 2732;
// Safety Zone Percentages
var SAFETY_MARGIN_TOP_PERCENT = 0.05; // 5% from the top
var SAFETY_MARGIN_BOTTOM_PERCENT = 0.15; // 10% from the bottom
var SAFETY_MARGIN_SIDES_PERCENT = 0.10; // 5% from the sides
var BPM = 120;
var BEAT_DURATION_MS = 60 / BPM * 1000;
var SCANNER_Y_MIN = GAME_HEIGHT * SAFETY_MARGIN_TOP_PERCENT;
var SCANNER_Y_MAX = GAME_HEIGHT * (1 - SAFETY_MARGIN_BOTTOM_PERCENT);
var PLAY_AREA_HEIGHT = SCANNER_Y_MAX - SCANNER_Y_MIN;
// var HIT_TOLERANCE_PX = 80; // Replaced by note-size based detection
var NOTE_SPAWN_AHEAD_MS = 800; // Reduced to 800ms to prevent mass spawning
// New Hit Accuracy and Popup Constants
var PERFECT_HIT_RATIO = 0.4; // 40% of the note core height (center portion) for "perfect"
var POINTS_PERFECT_TAP = 30;
var POINTS_GOOD_TAP = 15;
var POINTS_PERFECT_HOLD_START = 10; // Initial points for starting a hold perfectly
var POINTS_GOOD_HOLD_START = 5; // Initial points for starting a hold well
var ACCURACY_POPUP_DURATION = 750; // ms for the popup to be visible
var ACCURACY_POPUP_Y_OFFSET = -100; // Initial Y offset from note center for the popup
var ACCURACY_POPUP_FONT_SIZE = 80;
var PERFECT_POPUP_COLOR = 0x00FF00; // Green for "Perfect"
var GOOD_POPUP_COLOR = 0xFFFF00; // Yellow for "Good"
var SCANNER_CYCLE_DURATION = 4000;
var SCANNER_HALF_CYCLE = SCANNER_CYCLE_DURATION / 2;
var HIT_TOLERANCE_PX_FOR_HOLD_END = 100; // Tolerance for releasing hold note at the end
var HOLD_END_GRACE_PERIOD_MS = 250; // Grace period for hold note release time vs scanner position
// var MIN_FLICK_DISTANCE = 75; // Removed
// Game State Variables
var activeHoldNotes = {}; // Stores the currently active hold note, keyed by 'left' or 'right' zone
// var activeFlickPrimer = null; // Removed
var scoreTxt;
var countdownTxt;
var scanner;
var scannerIsMovingUp = true;
var gameStartTime;
var gameStarted = false;
var notes = [];
var currentSongData;
var spawnedNotes = []; // Using array instead of Set
var leftZone, rightZone;
var isDragging = false;
var dragStartX, dragStartY;
var currentTrail = [];
var backgroundParticles = [];
var NUM_BACKGROUND_PARTICLES = 100;
// Note Color Definitions
var COLOR_LEFT_ZONE = 0x0077FF; // Blue for left zone notes
var COLOR_RIGHT_ZONE = 0xFF3333; // Red for right zone notes
var COLOR_SYNC_TAP = 0xFF69B4; // Pink for synced tap notes (HotPink)
var COLOR_SYNC_HOLD = 0x9370DB; // Purple for synced hold notes (MediumPurple)
// SYNC_NOTE_COLOR is used by SongGenerator as a FLAG to indicate a note is part of a sync pattern.
// The Note class will then apply the final COLOR_SYNC_TAP or COLOR_SYNC_HOLD.
var SYNC_NOTE_COLOR = 0xFFA500; // This specific value signals a sync note to the Note class.
// Song Pattern System
// Pattern Templates - reusable note patterns
var PatternTemplates = {
// Basic patterns
tapLeft: function tapLeft(startTime) {
return [{
time: startTime,
type: 'tap',
zone: 'left'
}];
},
tapRight: function tapRight(startTime) {
return [{
time: startTime,
type: 'tap',
zone: 'right'
}];
},
syncTap: function syncTap(startTime) {
return [{
time: startTime,
type: 'tap',
zone: 'left'
}, {
time: startTime,
type: 'tap',
zone: 'right'
}];
},
// Alternating patterns
leftRightTaps: function leftRightTaps(startTime, spacing) {
spacing = spacing || 500; // Default to beat spacing
return [{
time: startTime,
type: 'tap',
zone: 'left'
}, {
time: startTime + spacing,
type: 'tap',
zone: 'right'
}];
},
rightLeftTaps: function rightLeftTaps(startTime, spacing) {
spacing = spacing || 500;
return [{
time: startTime,
type: 'tap',
zone: 'right'
}, {
time: startTime + spacing,
type: 'tap',
zone: 'left'
}];
},
// Hold patterns
holdLeft: function holdLeft(startTime, duration) {
duration = duration || 1000;
return [{
time: startTime,
type: 'hold',
zone: 'left',
duration: duration
}];
},
holdRight: function holdRight(startTime, duration) {
duration = duration || 1000;
return [{
time: startTime,
type: 'hold',
zone: 'right',
duration: duration
}];
},
syncHold: function syncHold(startTime, duration) {
duration = duration || 1000;
return [{
time: startTime,
type: 'hold',
zone: 'left',
duration: duration
}, {
time: startTime,
type: 'hold',
zone: 'right',
duration: duration
}];
},
// Complex patterns
tripletTaps: function tripletTaps(startTime, zone, spacing) {
spacing = spacing || 167; // Triplet timing
return [{
time: startTime,
type: 'tap',
zone: zone
}, {
time: startTime + spacing,
type: 'tap',
zone: zone
}, {
time: startTime + spacing * 2,
type: 'tap',
zone: zone
}];
},
alternatingTriplets: function alternatingTriplets(startTime, spacing) {
spacing = spacing || 167;
return [{
time: startTime,
type: 'tap',
zone: 'left'
}, {
time: startTime + spacing,
type: 'tap',
zone: 'right'
}, {
time: startTime + spacing * 2,
type: 'tap',
zone: 'left'
}];
},
buildUp: function buildUp(startTime) {
return [{
time: startTime,
type: 'tap',
zone: 'left'
}, {
time: startTime + 250,
type: 'tap',
zone: 'right'
}, {
time: startTime + 500,
type: 'tap',
zone: 'left'
}, {
time: startTime + 625,
type: 'tap',
zone: 'right'
}, {
time: startTime + 750,
type: 'tap',
zone: 'left'
}, {
time: startTime + 875,
type: 'tap',
zone: 'right'
}];
}
};
var SongGenerator = {
generateSong: function generateSong(config) {
var notes = [];
var totalLength = config.totalLength || 202136;
var startTime = config.startDelay || 2000;
notes = this.generateZoneAwareSong(startTime, totalLength);
notes.sort(function (a, b) {
return a.time - b.time;
}); // Ensure notes are sorted
return notes;
},
generateZoneAwareSong: function generateZoneAwareSong(startTime, songTotalLength) {
var notes = [];
var currentTime = startTime;
// Track when each zone will be free
var zoneFreeTime = {
left: startTime,
right: startTime
};
// Minimum spacing between notes (human reaction time)
var MIN_SAME_ZONE_SPACING = 400; // 400ms minimum in same zone
var MIN_DIFFERENT_ZONE_SPACING = 200; // 200ms minimum between different zones
var MIN_SYNC_SPACING = 1000; // Increased to 1000ms for more spacing between sync events
var lastSyncTime = startTime - MIN_SYNC_SPACING; // Adjusted initialization
// Define sections with different complexities
var sections = [{
start: 0,
end: 16000,
complexity: 'simple'
}, {
start: 16000,
end: 48000,
complexity: 'medium'
}, {
start: 48000,
end: 72000,
complexity: 'medium'
},
// Original: chorus1, intensity 0.8
{
start: 72000,
end: 104000,
complexity: 'medium'
}, {
start: 104000,
end: 128000,
complexity: 'complex'
},
// Original: chorus2, intensity 0.9
{
start: 128000,
end: 160000,
complexity: 'medium'
}, {
start: 160000,
end: 184000,
complexity: 'complex'
},
// Original: finalchorus, intensity 1.0
{
start: 184000,
end: songTotalLength,
complexity: 'simple'
} // Ensure it goes to the end
];
for (var s = 0; s < sections.length; s++) {
var section = sections[s];
var sectionStartTime = startTime + section.start;
var sectionEndTime = startTime + section.end;
if (sectionEndTime > startTime + songTotalLength) {
sectionEndTime = startTime + songTotalLength;
}
if (sectionStartTime >= sectionEndTime) {
continue;
}
currentTime = Math.max(currentTime, sectionStartTime);
while (currentTime < sectionEndTime - 1000) {
// Leave 1s buffer at end of section
var pattern = this.selectPattern(section.complexity, zoneFreeTime, currentTime, lastSyncTime);
var patternResult = this.placePattern(pattern, currentTime, zoneFreeTime, MIN_SAME_ZONE_SPACING, MIN_DIFFERENT_ZONE_SPACING, sectionEndTime);
if (patternResult.notes.length > 0) {
var allNotesFit = true;
for (var ni = 0; ni < patternResult.notes.length; ni++) {
if (patternResult.notes[ni].time + (patternResult.notes[ni].duration || 0) > sectionEndTime - 200) {
// Check end time
allNotesFit = false;
break;
}
}
if (allNotesFit) {
notes = notes.concat(patternResult.notes);
currentTime = patternResult.nextTime;
if (pattern.type === 'sync' || pattern.type === 'syncHold') {
// Also update for syncHold
lastSyncTime = patternResult.notes[0].time; // Time of the sync notes themselves
}
} else {
currentTime += 300; // Advance time if pattern didn't fit
}
} else {
currentTime += 300; // If pattern couldn't be placed, advance time
}
if (currentTime >= sectionEndTime - 1000) {
break;
}
}
}
return notes;
},
selectPattern: function selectPattern(complexity, zoneFreeTime, currentTime, lastSyncTime) {
var patterns = [];
var MIN_SYNC_SPACING_FOR_SELECTION = 1000; // Consistent with SongGenerator.generateZoneAwareSong
if (complexity === 'simple') {
patterns = [{
type: 'single',
zone: 'left',
duration: 800
},
// Longer footprint
{
type: 'single',
zone: 'right',
duration: 800
},
// Longer footprint
{
type: 'rest',
duration: 1200
}, {
type: 'rest',
duration: 1200
} // More rests
];
if (currentTime - lastSyncTime >= MIN_SYNC_SPACING_FOR_SELECTION + 200) {
// Sync less frequent
patterns.push({
type: 'sync',
duration: 1000
});
}
} else if (complexity === 'medium') {
patterns = [
// { type: 'hold', zone: 'left', holdDuration: 1000, duration: 700 }, // Reduced //{5M} //{5N} //{5O}
{
type: 'hold',
zone: 'right',
holdDuration: 1000,
duration: 700
}, {
type: 'single',
zone: 'left',
duration: 600
}, {
type: 'single',
zone: 'right',
duration: 600
}, {
type: 'single',
zone: 'left',
duration: 600
},
// Added more single taps
{
type: 'single',
zone: 'right',
duration: 600
},
// Added more single taps
{
type: 'alternating',
duration: 750
},
// Footprint for 2 taps ~300ms apart
{
type: 'rest',
duration: 1000
}];
if (currentTime - lastSyncTime >= MIN_SYNC_SPACING_FOR_SELECTION) {
patterns.push({
type: 'sync',
duration: 800
});
patterns.push({
type: 'sync',
duration: 800
}); // Increase likelihood
// patterns.push({ type: 'syncHold', holdDuration: 1000, duration: 800 }); // Removed syncHold //{6i} //{6j} //{6k}
}
} else {
// complex
patterns = [
// { type: 'hold', zone: 'left', holdDuration: 1200, duration: 800 }, // Reduced //{6p} //{6q} //{6r}
{
type: 'hold',
zone: 'right',
holdDuration: 1200,
duration: 800
},
// { type: 'holdWithTaps', zone: 'left', holdDuration: 1200, duration: 1000 }, // Reduced //{6y} //{6z} //{6A}
{
type: 'alternating',
duration: 800 // Increased from 600
}, {
type: 'alternating',
duration: 800 // Increased from 600
},
// Added more alternating
// Slightly larger footprint for alternating taps
{
type: 'single',
zone: 'left',
duration: 700 // Increased from 500
},
// Slightly larger footprint for single taps
{
type: 'single',
zone: 'right',
duration: 700 // Increased from 500
}, {
type: 'single',
zone: 'left',
duration: 700 // Increased from 500
},
// Added more single
{
type: 'single',
zone: 'right',
duration: 700 // Increased from 500
} // Added more single
];
if (currentTime - lastSyncTime >= MIN_SYNC_SPACING_FOR_SELECTION - 200) {
// Allow syncs a bit closer
patterns.push({
type: 'sync',
duration: 600
});
patterns.push({
type: 'sync',
duration: 600
}); // Increase likelihood
// patterns.push({ type: 'syncHold', holdDuration: 1200, duration: 900 }); // Reduced one syncHold //{74} //{75} //{76} //{77}
patterns.push({
type: 'syncHold',
holdDuration: 1200,
duration: 900
}); // Increase likelihood
}
}
return patterns[Math.floor(Math.random() * patterns.length)];
},
placePattern: function placePattern(pattern, requestedTime, zoneFreeTime, minSameZoneSpacing, minDiffZoneSpacing, sectionEndTime) {
var notes = [];
var earliestTime = requestedTime;
var nextTimeAdvance = pattern.duration || 500; // Default advancement
if (pattern.type === 'single') {
earliestTime = Math.max(requestedTime, zoneFreeTime[pattern.zone]);
if (earliestTime + (pattern.duration || 0) < sectionEndTime) {
notes.push({
time: earliestTime,
type: 'tap',
zone: pattern.zone
});
zoneFreeTime[pattern.zone] = earliestTime + minSameZoneSpacing;
return {
notes: notes,
nextTime: earliestTime + nextTimeAdvance
};
}
} else if (pattern.type === 'sync') {
earliestTime = Math.max(requestedTime, zoneFreeTime.left, zoneFreeTime.right);
if (earliestTime + (pattern.duration || 0) < sectionEndTime) {
notes.push({
time: earliestTime,
type: 'tap',
zone: 'left',
color: SYNC_NOTE_COLOR
});
notes.push({
time: earliestTime,
type: 'tap',
zone: 'right',
color: SYNC_NOTE_COLOR
});
zoneFreeTime.left = earliestTime + minSameZoneSpacing;
zoneFreeTime.right = earliestTime + minSameZoneSpacing;
return {
notes: notes,
nextTime: earliestTime + nextTimeAdvance
};
}
} else if (pattern.type === 'alternating') {
// Attempt Left then Right
var firstZone = Math.random() < 0.5 ? 'left' : 'right';
var secondZone = firstZone === 'left' ? 'right' : 'left';
var t1 = Math.max(requestedTime, zoneFreeTime[firstZone]);
var t2 = Math.max(t1 + minDiffZoneSpacing, zoneFreeTime[secondZone]);
if (t2 + (pattern.duration || 0) - (t2 - t1) < sectionEndTime) {
// check if second note fits
notes.push({
time: t1,
type: 'tap',
zone: firstZone
});
notes.push({
time: t2,
type: 'tap',
zone: secondZone
});
zoneFreeTime[firstZone] = t1 + minSameZoneSpacing;
zoneFreeTime[secondZone] = t2 + minSameZoneSpacing;
return {
notes: notes,
nextTime: t1 + nextTimeAdvance // Use pattern's duration for next time, (nextTimeAdvance is pattern.duration)
};
}
} else if (pattern.type === 'syncHold') {
earliestTime = Math.max(requestedTime, zoneFreeTime.left, zoneFreeTime.right);
var holdDuration = pattern.holdDuration || 1200; // Default if not specified
if (earliestTime + holdDuration < sectionEndTime) {
notes.push({
time: earliestTime,
type: 'hold',
zone: 'left',
duration: holdDuration,
color: SYNC_NOTE_COLOR
});
notes.push({
time: earliestTime,
type: 'hold',
zone: 'right',
duration: holdDuration,
color: SYNC_NOTE_COLOR
});
zoneFreeTime.left = earliestTime + holdDuration + 200; // Buffer after hold
zoneFreeTime.right = earliestTime + holdDuration + 200; // Buffer after hold
return {
notes: notes,
nextTime: earliestTime + (pattern.duration || 800) // Use pattern's defined footprint
};
}
} else if (pattern.type === 'hold') {
earliestTime = Math.max(requestedTime, zoneFreeTime[pattern.zone]);
var holdDuration = pattern.holdDuration || 1500;
if (earliestTime + holdDuration < sectionEndTime) {
notes.push({
time: earliestTime,
type: 'hold',
zone: pattern.zone,
duration: holdDuration
});
var otherZone = pattern.zone === 'left' ? 'right' : 'left';
zoneFreeTime[pattern.zone] = earliestTime + holdDuration + 200; // Buffer after hold in its own zone
// Block the other zone for the duration of this hold note, plus minDiffZoneSpacing buffer.
// This prevents other notes from spawning in the adjacent lane during the hold.
zoneFreeTime[otherZone] = Math.max(zoneFreeTime[otherZone], earliestTime + holdDuration + minDiffZoneSpacing);
return {
notes: notes,
nextTime: earliestTime + nextTimeAdvance
}; // Pattern's own duration logic
}
} else if (pattern.type === 'rest') {
return {
notes: [],
nextTime: requestedTime + (pattern.duration || 500)
};
}
return {
notes: [],
nextTime: requestedTime + 100
}; // Failed to place, small advance
}
};
// Simpler song database
var SongDatabase = {
'gameMusic': {
bpm: 120,
scannerCycleDuration: 4000,
totalLength: 202136,
notes: null
}
};
// Generate with the new system
SongDatabase['gameMusic'].notes = SongGenerator.generateSong({
startDelay: 2000,
totalLength: SongDatabase['gameMusic'].totalLength
});
var defaultSongData = SongDatabase['gameMusic'];
// Helper function to add new songs easily
function addSong(songId, config) {
SongDatabase[songId] = {
bpm: config.bpm || 120,
scannerCycleDuration: config.scannerCycleDuration || 4000,
totalLength: config.totalLength,
notes: config.customNotes || SongGenerator.generateSong(config)
};
}
// Example of adding a new song:
/*
addSong('newSong', {
bpm: 140,
totalLength: 180000, // 3 minutes
startDelay: 1500,
verseCount: 2,
verseLength: 30000
});
*/
function calculateNotePosition(noteTime) {
// Calculate where scanner will be when note should be hit
var cycleTime = noteTime % SCANNER_CYCLE_DURATION;
var y;
if (cycleTime < SCANNER_HALF_CYCLE) {
// First half: moving up from bottom to top
var progress = cycleTime / SCANNER_HALF_CYCLE;
y = SCANNER_Y_MAX - progress * PLAY_AREA_HEIGHT;
} else {
// Second half: moving down from top to bottom
var progress = (cycleTime - SCANNER_HALF_CYCLE) / SCANNER_HALF_CYCLE;
y = SCANNER_Y_MIN + progress * PLAY_AREA_HEIGHT;
}
return y;
}
function calculateZoneX(zone) {
// Calculate random X position within the specified zone
var sideMargin = GAME_WIDTH * SAFETY_MARGIN_SIDES_PERCENT;
// Calculate the available width for random placement within a zone, considering margins from both the screen edge and the center line.
var randomSpan = GAME_WIDTH / 2 - 2 * sideMargin;
// Ensure randomSpan is not negative, which could happen if margins are very large.
// This would effectively mean notes spawn exactly on the margin line if randomSpan is 0.
if (randomSpan < 0) {
randomSpan = 0;
}
if (zone === 'left') {
// Spawn between left_edge_margin and (center_line - center_margin)
return sideMargin + Math.random() * randomSpan;
} else {
// zone === 'right'
// Spawn between (center_line + center_margin) and (right_edge - right_edge_margin)
return GAME_WIDTH / 2 + sideMargin + Math.random() * randomSpan;
}
}
function createZoneIndicators() {
// Left zone indicator
leftZone = game.attachAsset('zoneIndicator', {
anchorX: 0,
anchorY: 0
});
leftZone.x = 0;
leftZone.y = 0;
leftZone.width = GAME_WIDTH / 2;
leftZone.alpha = 0.1;
leftZone.tint = 0x00ffff;
// Right zone indicator
rightZone = game.attachAsset('zoneIndicator', {
anchorX: 0,
anchorY: 0
});
rightZone.x = GAME_WIDTH / 2;
rightZone.y = 0;
rightZone.width = GAME_WIDTH / 2;
rightZone.alpha = 0.1;
rightZone.tint = 0xff00ff;
}
function showAccuracyPopup(text, x, y, color) {
var popup = new Text2(text, {
size: ACCURACY_POPUP_FONT_SIZE,
fill: color,
stroke: 0x000000,
// Black stroke for better visibility
strokeThickness: 4
});
popup.anchor.set(0.5, 0.5);
popup.x = x;
popup.y = y + ACCURACY_POPUP_Y_OFFSET;
popup.alpha = 0; // Start invisible
game.addChild(popup);
// Animation: Fade in and move up slightly, then continue moving up and fade out.
var initialY = popup.y;
tween(popup, {
alpha: 1,
y: initialY - 30
}, {
// Fade in and initial move
duration: ACCURACY_POPUP_DURATION * 0.25,
easing: tween.easeOut,
onFinish: function onFinish() {
// Second part of animation: fade out while continuing to move up
tween(popup, {
alpha: 0,
y: initialY - 130
}, {
// Target Y is further up from original initialY
duration: ACCURACY_POPUP_DURATION * 0.75,
easing: tween.easeIn,
onFinish: function onFinish() {
if (popup && popup.destroy) {
popup.destroy();
}
}
});
}
});
}
// function createFlickResultNote is removed as it's no longer used.
function startCountdown() {
countdownTxt = new Text2('3', {
size: 200,
fill: 0xFFFFFF
});
countdownTxt.anchor.set(0.5, 0.5);
countdownTxt.x = GAME_WIDTH / 2;
countdownTxt.y = GAME_HEIGHT / 2;
game.addChild(countdownTxt);
var count = 3;
var countdownInterval = LK.setInterval(function () {
count--;
if (count > 0) {
countdownTxt.setText(count.toString());
} else if (count === 0) {
countdownTxt.setText('GO!');
} else {
LK.clearInterval(countdownInterval);
if (countdownTxt && countdownTxt.destroy) {
countdownTxt.destroy();
}
startGame();
}
}, 1000);
}
function startGame() {
gameStarted = true;
gameStartTime = Date.now();
moveScanner();
LK.playMusic('gameMusic', {
loop: true
});
// updateHealthDisplay function is removed.
}
function moveScanner() {
if (!scanner || !gameStarted) {
return;
}
tween.stop(scanner);
var targetY = scannerIsMovingUp ? SCANNER_Y_MIN : SCANNER_Y_MAX;
tween(scanner, {
y: targetY
}, {
duration: SCANNER_CYCLE_DURATION / 2,
easing: tween.linear,
onFinish: function onFinish() {
scannerIsMovingUp = !scannerIsMovingUp;
moveScanner();
}
});
}
function spawnNotesForCurrentTime(currentTime) {
if (!currentSongData) {
return;
}
currentSongData.notes.forEach(function (noteData, index) {
var noteKey = index + '_' + noteData.time;
// Check if already spawned
var alreadySpawned = false;
for (var i = 0; i < spawnedNotes.length; i++) {
if (spawnedNotes[i] === noteKey) {
alreadySpawned = true;
break;
}
}
// Only spawn if note time is approaching and not yet spawned
if (!alreadySpawned && currentTime >= noteData.time - NOTE_SPAWN_AHEAD_MS && currentTime <= noteData.time - NOTE_SPAWN_AHEAD_MS + 100) {
// Narrow spawn window
// Calculate proper position
var calculatedY = calculateNotePosition(noteData.time);
var calculatedX = calculateZoneX(noteData.zone);
// Create note data with calculated positions
var processedNoteData = {
time: noteData.time,
type: noteData.type,
x: calculatedX,
y: calculatedY,
color: noteData.color,
// Pass the color from song data (e.g. SYNC_NOTE_COLOR flag or undefined)
// The Note class constructor will determine the final actual color.
duration: noteData.duration
// flickDirection: noteData.flickDirection // Removed
};
// Flick target logic is removed
var note = new Note(processedNoteData, currentTime);
notes.push(note);
game.addChild(note);
note.spawnIn();
spawnedNotes.push(noteKey);
}
});
}
function setupGame() {
LK.setScore(0);
// playerHealth = INITIAL_PLAYER_HEALTH; // playerHealth assignment removed
gameStarted = false;
// Score Text
if (scoreTxt && scoreTxt.destroy) {
scoreTxt.destroy();
}
scoreTxt = new Text2('0', {
size: 100,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
scoreTxt.setText(LK.getScore());
// Health Display removed
// Zone indicators
createZoneIndicators();
// Scanner - start at bottom
if (scanner && scanner.destroy) {
scanner.destroy();
}
scanner = new Scanner();
scanner.y = SCANNER_Y_MAX; // Start at bottom
game.addChild(scanner);
// Clear previous notes
notes.forEach(function (note) {
if (note && note.destroy) {
note.destroy();
}
});
notes = [];
spawnedNotes = [];
// Load song data
currentSongData = defaultSongData;
BPM = currentSongData.bpm;
BEAT_DURATION_MS = 60 / BPM * 1000;
scannerIsMovingUp = true; // Will move up first
// Start countdown instead of immediate game start
startCountdown();
// Initialize background particles
if (backgroundParticles.length === 0) {
// Only create if they don't exist
for (var i = 0; i < NUM_BACKGROUND_PARTICLES; i++) {
var particle = new BackgroundParticle();
// Stagger initial Y positions more for a fuller effect from the start
particle.y = Math.random() * GAME_HEIGHT;
backgroundParticles.push(particle);
game.addChildAt(particle, 0); // Add particles behind other game elements
}
} else {
// If they exist, re-initialize their positions
backgroundParticles.forEach(function (p) {
p.init();
p.y = Math.random() * GAME_HEIGHT; // Spread them out
});
}
}
function checkZoneHit(x, y) {
var hitZone = x < GAME_WIDTH / 2 ? 'left' : 'right';
var currentTime = Date.now() - gameStartTime;
for (var i = notes.length - 1; i >= 0; i--) {
var note = notes[i];
if (!note || !note.active || note.isHit || note.isSpawning || !note.noteGraphics) {
continue;
}
// Check if note is in the same zone
if (note.zone === hitZone) {
var noteCoreHeight = note.noteGraphics.height; // Assumes scale is 1 for active notes
var noteCenterY = note.y;
var scannerY = scanner.y;
// Define hit windows based on note core size
var perfectHitRadius = noteCoreHeight * PERFECT_HIT_RATIO / 2;
var goodHitRadius = noteCoreHeight / 2;
var perfectHitTop = noteCenterY - perfectHitRadius;
var perfectHitBottom = noteCenterY + perfectHitRadius;
var goodHitTop = noteCenterY - goodHitRadius;
var goodHitBottom = noteCenterY + goodHitRadius;
var accuracyText = null;
var hitPoints = 0;
var popupColor = 0xFFFFFF;
if (scannerY >= perfectHitTop && scannerY <= perfectHitBottom) {
accuracyText = "PERFECT";
popupColor = PERFECT_POPUP_COLOR;
if (note.type === 'tap') {
hitPoints = POINTS_PERFECT_TAP;
} else if (note.type === 'hold') {
hitPoints = POINTS_PERFECT_HOLD_START;
}
} else if (scannerY >= goodHitTop && scannerY <= goodHitBottom) {
accuracyText = "GOOD";
popupColor = GOOD_POPUP_COLOR;
if (note.type === 'tap') {
hitPoints = POINTS_GOOD_TAP;
} else if (note.type === 'hold') {
hitPoints = POINTS_GOOD_HOLD_START;
}
}
if (accuracyText) {
note.isHit = true; // Mark as judged for timing.
showAccuracyPopup(accuracyText, note.x, note.y, popupColor);
if (note.type === 'tap') {
note.showHitEffect(); // Makes note inactive
} else if (note.type === 'hold') {
note.startHoldEffect(); // Prepares note for holding
}
if (hitPoints > 0) {
LK.setScore(LK.getScore() + hitPoints);
scoreTxt.setText(LK.getScore());
}
break; //{4u} // Found and processed a note for this hit
}
}
}
}
// Initial setup call
setupGame();
game.down = function (x, y, obj) {
if (!gameStarted) {
return;
} //{4L} // playerHealth check removed
checkZoneHit(x, y); // This will set note.isHit for taps and call startHoldEffect for holds.
var hitZoneForDown = x < GAME_WIDTH / 2 ? 'left' : 'right';
// Logic for starting a hold
// activeFlickPrimer = null; // Removed
for (var i = 0; i < notes.length; i++) {
var note = notes[i];
if (!note.active || note.isSpawning || note.holdSuccessfullyCompleted || note.isMissed) {
continue;
}
if (note.zone === hitZoneForDown && note.isHit) {
// note.isHit is now set by checkZoneHit if it was a "Good" or "Perfect" hit.
// checkZoneHit also calls startHoldEffect and awards initial points.
if (note.type === 'hold' && !note.isHolding) {
note.isHolding = true;
activeHoldNotes[note.zone] = note;
// A flick note should not also be a hold note. // Comment remains for context, but flick logic is gone.
} //{4I} // This closing brace now corresponds to 'if (note.type === 'hold' ...)'
}
}
isDragging = true; // Still useful for game.up to know a drag sequence occurred
dragStartX = x; // General drag start, flick primer uses its own copy
dragStartY = y;
// currentTrail related lines removed as trails are gone.
};
game.move = function (x, y, obj) {
if (!isDragging || !gameStarted) {
return;
}
// Particle trail logic removed. This function can be empty or used for other drag-move effects if needed later.
};
game.up = function (x, y, obj) {
if (!gameStarted) {
return;
} // Allow processing even if not dragging for safety, but check gameStarted
var releasedZone = x < GAME_WIDTH / 2 ? 'left' : 'right';
// Flick logic (activeFlickPrimer related) removed entirely.
// Now, directly handle hold note release.
var heldNote = activeHoldNotes[releasedZone];
if (heldNote && heldNote.isHolding) {
heldNote.isHolding = false; // Stop particle effects etc.
delete activeHoldNotes[releasedZone];
var currentTimeMs = Date.now() - gameStartTime;
var noteExpectedEndTimeMs = heldNote.hitTime + heldNote.duration;
var absoluteEndNoteY = heldNote.y + (heldNote.endNote ? heldNote.endNote.y : 0); // endNote.y is relative
// Determine scanner direction at hit time for this note (needed for correct end check if trail goes up)
var hitTimeCycle = heldNote.hitTime % SCANNER_CYCLE_DURATION;
var noteScannerMovingUp = hitTimeCycle < SCANNER_HALF_CYCLE;
var trailDirection = noteScannerMovingUp ? -1 : 1;
var scannerAtEnd;
if (trailDirection === -1) {
// Trail goes upwards
scannerAtEnd = scanner.y <= absoluteEndNoteY + HIT_TOLERANCE_PX_FOR_HOLD_END && scanner.y >= absoluteEndNoteY - HIT_TOLERANCE_PX_FOR_HOLD_END / 2; // Scanner near or slightly past the end upwardly
} else {
// Trail goes downwards
scannerAtEnd = scanner.y >= absoluteEndNoteY - HIT_TOLERANCE_PX_FOR_HOLD_END && scanner.y <= absoluteEndNoteY + HIT_TOLERANCE_PX_FOR_HOLD_END / 2; // Scanner near or slightly past the end downwardly
}
var timeReachedOrPassedEnd = currentTimeMs >= noteExpectedEndTimeMs - HOLD_END_GRACE_PERIOD_MS;
var timeNotTooLate = currentTimeMs <= noteExpectedEndTimeMs + HOLD_END_GRACE_PERIOD_MS + 200; // Not held excessively long
if (scannerAtEnd && timeReachedOrPassedEnd && timeNotTooLate) {
heldNote.completeHold();
LK.setScore(LK.getScore() + 50); // More points for a successful hold
scoreTxt.setText(LK.getScore());
} else {
heldNote.failHold();
// playerHealth--;//{5x} // Health decrement removed
// updateHealthDisplay();//{5y} // Health display update removed
}
}
// This bracket no longer closes an "else" block, but the "if (heldNote && heldNote.isHolding)" block.
isDragging = false; // Reset general dragging flag.
// currentTrail cleanup is removed as it's no longer created.
};
game.update = function () {
// Update background particles immediately, even during countdown
backgroundParticles.forEach(function (particle) {
particle.updateParticle();
});
if (!gameStarted) {
return;
} // {5C} // playerHealth check removed
var currentTime = Date.now() - gameStartTime;
spawnNotesForCurrentTime(currentTime);
for (var i = notes.length - 1; i >= 0; i--) {
var note = notes[i];
if (!note) {
notes.splice(i, 1);
continue;
}
// Update hold effects if note is being held
if (note.type === 'hold' && note.isHolding && note.active && !note.holdSuccessfullyCompleted && !note.isMissed) {
note.updateHoldEffects(scanner.y);
// Fail-safe for holds that are held too long past their end time
var currentTimeMsForUpdate = Date.now() - gameStartTime;
var noteExpectedEndTimeMsForUpdate = note.hitTime + note.duration;
if (currentTimeMsForUpdate > noteExpectedEndTimeMsForUpdate + HOLD_END_GRACE_PERIOD_MS + 500) {
// 500ms buffer
if (activeHoldNotes[note.zone] === note) {
// Still registered as actively held
note.failHold();
// playerHealth--;//{5I} // Health decrement removed
// updateHealthDisplay();//{5J} // Health display update removed
delete activeHoldNotes[note.zone];
}
}
}
// Check for missed notes
if (note.active && !note.isHit && !note.isMissed && !note.isSpawning) {
var timeDiff = currentTime - note.hitTime;
var scannerPassed = false;
if (scannerIsMovingUp && scanner.y < note.y - 50) {
scannerPassed = true;
} else if (!scannerIsMovingUp && scanner.y > note.y + 50) {
scannerPassed = true;
}
if (scannerPassed && timeDiff > 300) {
note.isMissed = true;
note.showMissEffect();
// playerHealth--;//{5Q} // Health decrement removed
// updateHealthDisplay();//{5R} // Health display update removed
// if (playerHealth <= 0) {//{5S} // Game over check removed
// return;
// }
}
}
// Cleanup inactive notes
if (!note.active) {
notes.splice(i, 1);
if (game.children.indexOf(note) > -1) {
game.removeChild(note);
}
if (note.destroy) {
note.destroy();
}
}
}
}; ===================================================================
--- original.js
+++ change.js
The word 'Pulsar' in a glowing neon SVG in futuristic font. The word is half blue on the left and half red on the right. In-Game asset. 2d. High contrast. No shadows
Remove the background.
A thin expanding ring with energy distortion ``` - Outer ring: 4-6 pixels thick, bright cyan (#00FFFF) - Inner ring: 2-3 pixels thick, white (#FFFFFF) - Ring thickness: Tapers from thick to thin as it expands - Transparency: Ring itself at 80% opacity - Background: Completely transparent - Edge treatment: Soft anti-aliased edges, slight glow effect - Optional: Subtle "energy crackle" texture within the ring. In-Game asset. 2d. High contrast. No shadows
Soft, lingering light effect ``` - Center: Warm orange (#FF6600) at 40% opacity - Middle: Yellow (#FFAA00) at 25% opacity - Edge: Transparent - Shape: Perfect circle with very soft, wide falloff. In-Game asset. 2d. High contrast. No shadows