/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Arrow Button class
var ArrowButton = Container.expand(function () {
var self = Container.call(this);
self.direction = 'left'; // 'left', 'right', 'up', 'down'
self.arrowAsset = null;
self.setDirection = function (dir) {
self.direction = dir;
if (self.arrowAsset) {
self.removeChild(self.arrowAsset);
}
var assetId = 'arrow_' + dir;
self.arrowAsset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Add a simple arrow "icon" using Text2
var arrowChar = {
'left': '←',
'right': '→',
'up': '↑',
'down': '↓'
}[dir];
if (self.arrowText) {
self.removeChild(self.arrowText);
}
self.arrowText = new Text2(arrowChar, {
size: 120,
fill: 0x222222
});
self.arrowText.anchor.set(0.5, 0.5);
self.addChild(self.arrowText);
};
return self;
});
// Cat class
var Cat = Container.expand(function () {
var self = Container.call(this);
self.lane = 1; // start in lane 1 (from 0-3)
self.catAsset = self.attachAsset('cat', {
anchorX: 0.5,
anchorY: 0.5
});
self.setLane = function (lane) {
self.lane = lane;
self.x = laneCenters[lane];
};
return self;
});
// Note class
var Note = Container.expand(function () {
var self = Container.call(this);
self.color = 'red'; // default, will be set on spawn
self.lane = 0; // 0-3
self.hit = false;
self.setColor = function (color) {
self.color = color;
if (self.noteAsset) {
self.removeChild(self.noteAsset);
}
var assetId = 'note_' + color;
self.noteAsset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
};
self.update = function () {
self.y += noteSpeed;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181818
});
/****
* Game Code
****/
// Music (looping, for rhythm)
// Sound effects
// Arrow buttons
// Lane backgrounds (for visual feedback)
// Note shapes: four colors
// Cat character: cute box, default color yellow
// Lane setup
var laneCount = 4;
var laneWidth = 400;
var laneSpacing = 32;
var laneColors = ['red', 'blue', 'green', 'yellow'];
var laneCenters = [];
var laneLeft = (2048 - (laneCount * laneWidth + (laneCount - 1) * laneSpacing)) / 2;
for (var i = 0; i < laneCount; i++) {
laneCenters[i] = laneLeft + i * (laneWidth + laneSpacing) + laneWidth / 2;
}
// Lane backgrounds
var laneBGs = [];
for (var i = 0; i < laneCount; i++) {
var assetId = 'lane_' + laneColors[i];
var laneBG = LK.getAsset(assetId, {
anchorX: 0.5,
anchorY: 0,
x: laneCenters[i],
y: 0,
width: laneWidth,
height: 2732
});
laneBG.interactive = true;
laneBG.buttonMode = true;
laneBG.laneIndex = i;
laneBG.down = function (x, y, obj) {
// Move cat to this lane if not already there
if (cat.lane !== this.laneIndex) {
cat.setLane(this.laneIndex);
}
};
game.addChild(laneBG);
laneBGs.push(laneBG);
}
// Cat setup
var cat = new Cat();
cat.setLane(1);
cat.y = 2732 - 350;
game.addChild(cat);
// Arrow buttons (bottom of screen, left/right/up/down)
var arrowButtons = [];
var arrowButtonSize = 180;
var arrowButtonY = 2732 - 120;
var arrowButtonSpacing = 220;
var arrowButtonXStart = 2048 / 2 - arrowButtonSpacing * 1.5;
var arrowDirs = ['left', 'right', 'up', 'down'];
for (var i = 0; i < 4; i++) {
var btn = new ArrowButton();
btn.setDirection(arrowDirs[i]);
btn.x = arrowButtonXStart + i * arrowButtonSpacing;
btn.y = arrowButtonY;
btn.direction = arrowDirs[i];
btn.interactive = true;
btn.buttonMode = true;
game.addChild(btn); // Place arrow buttons in the main game area so they are always visible
arrowButtons.push(btn);
}
// Color change buttons removed (no longer needed)
// Score display
var scoreTxt = new Text2('0', {
size: 120,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Combo display
var comboTxt = new Text2('', {
size: 80,
fill: 0xFFE066
});
comboTxt.anchor.set(0.5, 0);
comboTxt.y = 130;
LK.gui.top.addChild(comboTxt);
// Speed selection buttons
var speedOptions = [{
label: "0.5x",
speed: 0.5,
interval: 72
}, {
label: "1x",
speed: 1,
interval: 36
}, {
label: "2x",
speed: 2,
interval: 18
}];
var speedBtnY = 320;
var speedBtnSpacing = 320;
var speedBtnXStart = 2048 / 2 - speedBtnSpacing;
var speedBtns = [];
var selectedSpeedIdx = 1; // default 1x
function setGameSpeed(idx) {
selectedSpeedIdx = idx;
for (var i = 0; i < speedBtns.length; i++) {
speedBtns[i].alpha = i === idx ? 1 : 0.5;
}
noteSpeed = 18 * speedOptions[idx].speed;
noteSpawnInterval = speedOptions[idx].interval;
}
for (var i = 0; i < speedOptions.length; i++) {
var btn = new Container();
var bg = LK.getAsset('arrow_up', {
anchorX: 0.5,
anchorY: 0.5
});
bg.width = 220;
bg.height = 120;
btn.addChild(bg);
var txt = new Text2(speedOptions[i].label, {
size: 60,
fill: "#222"
});
txt.anchor.set(0.5, 0.5);
btn.addChild(txt);
btn.x = speedBtnXStart + i * speedBtnSpacing;
btn.y = speedBtnY;
btn.interactive = true;
btn.buttonMode = true;
(function (idx) {
btn.down = function (x, y, obj) {
setGameSpeed(idx);
};
})(i);
btn.alpha = i === selectedSpeedIdx ? 1 : 0.5;
game.addChild(btn);
speedBtns.push(btn);
}
setGameSpeed(selectedSpeedIdx);
// Notes array
var notes = [];
var noteSpeed = 18; // will be set by speed selection
var noteSpawnInterval = 36; // will be set by speed selection
var noteTimer = 0;
var noteColors = ['red', 'blue', 'green', 'yellow'];
// Rhythm pattern (random for now, can be improved)
function spawnNote() {
var lane = Math.floor(Math.random() * 4);
var color = noteColors[Math.floor(Math.random() * 4)];
var note = new Note();
note.setColor(color);
note.lane = lane;
note.x = laneCenters[lane];
note.y = -100;
notes.push(note);
game.addChild(note);
}
// Hit window (distance from cat.y to note.y for a "hit")
var hitWindow = 120;
// Score/combo
var score = 0;
var combo = 0;
var maxCombo = 0;
// Input handling
function handleArrowButton(dir) {
var newLane = cat.lane;
if (dir === 'left') {
newLane = Math.max(0, cat.lane - 1);
} else if (dir === 'right') {
newLane = Math.min(laneCount - 1, cat.lane + 1);
} else if (dir === 'up') {
// No vertical movement, but can add jump effect
tween(cat, {
y: cat.y - 60
}, {
duration: 80,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(cat, {
y: 2732 - 350
}, {
duration: 120,
easing: tween.easeIn
});
}
});
return;
} else if (dir === 'down') {
// No vertical movement, but can add crouch effect
tween(cat, {
y: cat.y + 40
}, {
duration: 80,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(cat, {
y: 2732 - 350
}, {
duration: 120,
easing: tween.easeIn
});
}
});
return;
}
if (newLane !== cat.lane) {
cat.setLane(newLane);
}
}
// Color change input handler and event attachment removed (no longer needed)
for (var i = 0; i < arrowButtons.length; i++) {
(function (btn) {
btn.down = function (x, y, obj) {
handleArrowButton(btn.direction);
};
})(arrowButtons[i]);
}
// Attach input events to color buttons
// Touch input: allow swipe left/right to move cat, tap on color buttons to change color
var touchStartX = null;
var touchStartY = null;
game.down = function (x, y, obj) {
touchStartX = x;
touchStartY = y;
};
game.up = function (x, y, obj) {
if (touchStartX !== null && Math.abs(x - touchStartX) > 80 && Math.abs(x - touchStartX) > Math.abs(y - touchStartY)) {
// Horizontal swipe
if (x < touchStartX) {
handleArrowButton('left');
} else {
handleArrowButton('right');
}
}
touchStartX = null;
touchStartY = null;
};
// Main update loop
game.update = function () {
// Spawn notes
if (LK.ticks % noteSpawnInterval === 0) {
spawnNote();
}
// Update notes
for (var i = notes.length - 1; i >= 0; i--) {
var note = notes[i];
note.update();
// Check for hit
if (!note.hit && Math.abs(note.y - cat.y) < hitWindow && note.lane === cat.lane) {
// Hit!
note.hit = true;
score += 1;
combo += 1;
if (combo > maxCombo) maxCombo = combo;
scoreTxt.setText(score);
comboTxt.setText(combo > 1 ? combo + ' combo!' : '');
LK.getSound('note_hit').play();
LK.effects.flashObject(cat, 0xffffff, 120);
// Animate note
tween(note, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 180,
onFinish: function onFinish() {
note.destroy();
}
});
notes.splice(i, 1);
continue;
}
// Missed note (passed cat)
if (!note.hit && note.y > cat.y + hitWindow) {
note.hit = true;
combo = 0;
comboTxt.setText('');
LK.getSound('note_miss').play();
LK.effects.flashScreen(0xff4b4b, 200);
// Animate note
tween(note, {
alpha: 0
}, {
duration: 120,
onFinish: function onFinish() {
note.destroy();
}
});
notes.splice(i, 1);
continue;
}
// Remove notes off screen
if (note.y > 2732 + 200) {
note.destroy();
notes.splice(i, 1);
}
}
// Infinite mode: no win condition
};
// Start music
LK.playMusic('bgmusic', {
fade: {
start: 0,
end: 0.7,
duration: 1200
}
}); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Arrow Button class
var ArrowButton = Container.expand(function () {
var self = Container.call(this);
self.direction = 'left'; // 'left', 'right', 'up', 'down'
self.arrowAsset = null;
self.setDirection = function (dir) {
self.direction = dir;
if (self.arrowAsset) {
self.removeChild(self.arrowAsset);
}
var assetId = 'arrow_' + dir;
self.arrowAsset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Add a simple arrow "icon" using Text2
var arrowChar = {
'left': '←',
'right': '→',
'up': '↑',
'down': '↓'
}[dir];
if (self.arrowText) {
self.removeChild(self.arrowText);
}
self.arrowText = new Text2(arrowChar, {
size: 120,
fill: 0x222222
});
self.arrowText.anchor.set(0.5, 0.5);
self.addChild(self.arrowText);
};
return self;
});
// Cat class
var Cat = Container.expand(function () {
var self = Container.call(this);
self.lane = 1; // start in lane 1 (from 0-3)
self.catAsset = self.attachAsset('cat', {
anchorX: 0.5,
anchorY: 0.5
});
self.setLane = function (lane) {
self.lane = lane;
self.x = laneCenters[lane];
};
return self;
});
// Note class
var Note = Container.expand(function () {
var self = Container.call(this);
self.color = 'red'; // default, will be set on spawn
self.lane = 0; // 0-3
self.hit = false;
self.setColor = function (color) {
self.color = color;
if (self.noteAsset) {
self.removeChild(self.noteAsset);
}
var assetId = 'note_' + color;
self.noteAsset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
};
self.update = function () {
self.y += noteSpeed;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181818
});
/****
* Game Code
****/
// Music (looping, for rhythm)
// Sound effects
// Arrow buttons
// Lane backgrounds (for visual feedback)
// Note shapes: four colors
// Cat character: cute box, default color yellow
// Lane setup
var laneCount = 4;
var laneWidth = 400;
var laneSpacing = 32;
var laneColors = ['red', 'blue', 'green', 'yellow'];
var laneCenters = [];
var laneLeft = (2048 - (laneCount * laneWidth + (laneCount - 1) * laneSpacing)) / 2;
for (var i = 0; i < laneCount; i++) {
laneCenters[i] = laneLeft + i * (laneWidth + laneSpacing) + laneWidth / 2;
}
// Lane backgrounds
var laneBGs = [];
for (var i = 0; i < laneCount; i++) {
var assetId = 'lane_' + laneColors[i];
var laneBG = LK.getAsset(assetId, {
anchorX: 0.5,
anchorY: 0,
x: laneCenters[i],
y: 0,
width: laneWidth,
height: 2732
});
laneBG.interactive = true;
laneBG.buttonMode = true;
laneBG.laneIndex = i;
laneBG.down = function (x, y, obj) {
// Move cat to this lane if not already there
if (cat.lane !== this.laneIndex) {
cat.setLane(this.laneIndex);
}
};
game.addChild(laneBG);
laneBGs.push(laneBG);
}
// Cat setup
var cat = new Cat();
cat.setLane(1);
cat.y = 2732 - 350;
game.addChild(cat);
// Arrow buttons (bottom of screen, left/right/up/down)
var arrowButtons = [];
var arrowButtonSize = 180;
var arrowButtonY = 2732 - 120;
var arrowButtonSpacing = 220;
var arrowButtonXStart = 2048 / 2 - arrowButtonSpacing * 1.5;
var arrowDirs = ['left', 'right', 'up', 'down'];
for (var i = 0; i < 4; i++) {
var btn = new ArrowButton();
btn.setDirection(arrowDirs[i]);
btn.x = arrowButtonXStart + i * arrowButtonSpacing;
btn.y = arrowButtonY;
btn.direction = arrowDirs[i];
btn.interactive = true;
btn.buttonMode = true;
game.addChild(btn); // Place arrow buttons in the main game area so they are always visible
arrowButtons.push(btn);
}
// Color change buttons removed (no longer needed)
// Score display
var scoreTxt = new Text2('0', {
size: 120,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Combo display
var comboTxt = new Text2('', {
size: 80,
fill: 0xFFE066
});
comboTxt.anchor.set(0.5, 0);
comboTxt.y = 130;
LK.gui.top.addChild(comboTxt);
// Speed selection buttons
var speedOptions = [{
label: "0.5x",
speed: 0.5,
interval: 72
}, {
label: "1x",
speed: 1,
interval: 36
}, {
label: "2x",
speed: 2,
interval: 18
}];
var speedBtnY = 320;
var speedBtnSpacing = 320;
var speedBtnXStart = 2048 / 2 - speedBtnSpacing;
var speedBtns = [];
var selectedSpeedIdx = 1; // default 1x
function setGameSpeed(idx) {
selectedSpeedIdx = idx;
for (var i = 0; i < speedBtns.length; i++) {
speedBtns[i].alpha = i === idx ? 1 : 0.5;
}
noteSpeed = 18 * speedOptions[idx].speed;
noteSpawnInterval = speedOptions[idx].interval;
}
for (var i = 0; i < speedOptions.length; i++) {
var btn = new Container();
var bg = LK.getAsset('arrow_up', {
anchorX: 0.5,
anchorY: 0.5
});
bg.width = 220;
bg.height = 120;
btn.addChild(bg);
var txt = new Text2(speedOptions[i].label, {
size: 60,
fill: "#222"
});
txt.anchor.set(0.5, 0.5);
btn.addChild(txt);
btn.x = speedBtnXStart + i * speedBtnSpacing;
btn.y = speedBtnY;
btn.interactive = true;
btn.buttonMode = true;
(function (idx) {
btn.down = function (x, y, obj) {
setGameSpeed(idx);
};
})(i);
btn.alpha = i === selectedSpeedIdx ? 1 : 0.5;
game.addChild(btn);
speedBtns.push(btn);
}
setGameSpeed(selectedSpeedIdx);
// Notes array
var notes = [];
var noteSpeed = 18; // will be set by speed selection
var noteSpawnInterval = 36; // will be set by speed selection
var noteTimer = 0;
var noteColors = ['red', 'blue', 'green', 'yellow'];
// Rhythm pattern (random for now, can be improved)
function spawnNote() {
var lane = Math.floor(Math.random() * 4);
var color = noteColors[Math.floor(Math.random() * 4)];
var note = new Note();
note.setColor(color);
note.lane = lane;
note.x = laneCenters[lane];
note.y = -100;
notes.push(note);
game.addChild(note);
}
// Hit window (distance from cat.y to note.y for a "hit")
var hitWindow = 120;
// Score/combo
var score = 0;
var combo = 0;
var maxCombo = 0;
// Input handling
function handleArrowButton(dir) {
var newLane = cat.lane;
if (dir === 'left') {
newLane = Math.max(0, cat.lane - 1);
} else if (dir === 'right') {
newLane = Math.min(laneCount - 1, cat.lane + 1);
} else if (dir === 'up') {
// No vertical movement, but can add jump effect
tween(cat, {
y: cat.y - 60
}, {
duration: 80,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(cat, {
y: 2732 - 350
}, {
duration: 120,
easing: tween.easeIn
});
}
});
return;
} else if (dir === 'down') {
// No vertical movement, but can add crouch effect
tween(cat, {
y: cat.y + 40
}, {
duration: 80,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(cat, {
y: 2732 - 350
}, {
duration: 120,
easing: tween.easeIn
});
}
});
return;
}
if (newLane !== cat.lane) {
cat.setLane(newLane);
}
}
// Color change input handler and event attachment removed (no longer needed)
for (var i = 0; i < arrowButtons.length; i++) {
(function (btn) {
btn.down = function (x, y, obj) {
handleArrowButton(btn.direction);
};
})(arrowButtons[i]);
}
// Attach input events to color buttons
// Touch input: allow swipe left/right to move cat, tap on color buttons to change color
var touchStartX = null;
var touchStartY = null;
game.down = function (x, y, obj) {
touchStartX = x;
touchStartY = y;
};
game.up = function (x, y, obj) {
if (touchStartX !== null && Math.abs(x - touchStartX) > 80 && Math.abs(x - touchStartX) > Math.abs(y - touchStartY)) {
// Horizontal swipe
if (x < touchStartX) {
handleArrowButton('left');
} else {
handleArrowButton('right');
}
}
touchStartX = null;
touchStartY = null;
};
// Main update loop
game.update = function () {
// Spawn notes
if (LK.ticks % noteSpawnInterval === 0) {
spawnNote();
}
// Update notes
for (var i = notes.length - 1; i >= 0; i--) {
var note = notes[i];
note.update();
// Check for hit
if (!note.hit && Math.abs(note.y - cat.y) < hitWindow && note.lane === cat.lane) {
// Hit!
note.hit = true;
score += 1;
combo += 1;
if (combo > maxCombo) maxCombo = combo;
scoreTxt.setText(score);
comboTxt.setText(combo > 1 ? combo + ' combo!' : '');
LK.getSound('note_hit').play();
LK.effects.flashObject(cat, 0xffffff, 120);
// Animate note
tween(note, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 180,
onFinish: function onFinish() {
note.destroy();
}
});
notes.splice(i, 1);
continue;
}
// Missed note (passed cat)
if (!note.hit && note.y > cat.y + hitWindow) {
note.hit = true;
combo = 0;
comboTxt.setText('');
LK.getSound('note_miss').play();
LK.effects.flashScreen(0xff4b4b, 200);
// Animate note
tween(note, {
alpha: 0
}, {
duration: 120,
onFinish: function onFinish() {
note.destroy();
}
});
notes.splice(i, 1);
continue;
}
// Remove notes off screen
if (note.y > 2732 + 200) {
note.destroy();
notes.splice(i, 1);
}
}
// Infinite mode: no win condition
};
// Start music
LK.playMusic('bgmusic', {
fade: {
start: 0,
end: 0.7,
duration: 1200
}
});