/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// FallingArrow: a single falling arrow
var FallingArrow = Container.expand(function () {
var self = Container.call(this);
// Arrow type: 'up', 'down', 'left', 'right'
self.arrowType = 'up';
self.speed = 8; // Will be set on spawn
// Attach arrow asset, set anchor to center
self.arrowAsset = null;
self.setArrowType = function (type) {
self.arrowType = type;
if (self.arrowAsset) {
self.removeChild(self.arrowAsset);
}
var assetId = '';
if (type === 'up') assetId = 'arrowUp';
if (type === 'down') assetId = 'arrowDown';
if (type === 'left') assetId = 'arrowLeft';
if (type === 'right') assetId = 'arrowRight';
self.arrowAsset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Rotate asset to match direction
if (type === 'up') self.arrowAsset.rotation = 0;
if (type === 'down') self.arrowAsset.rotation = Math.PI;
if (type === 'left') self.arrowAsset.rotation = -Math.PI / 2;
if (type === 'right') self.arrowAsset.rotation = Math.PI / 2;
};
// Called every tick
self.update = function () {
self.y += self.speed;
};
return self;
});
// FeedbackText: floating feedback text (e.g. "Very Good", "Bad", etc)
var FeedbackText = Container.expand(function () {
var self = Container.call(this);
self.textObj = new Text2('', {
size: 120,
fill: "#fff",
font: "Impact"
});
self.textObj.anchor.set(0.5, 0.5);
self.addChild(self.textObj);
self.show = function (msg, color) {
self.textObj.setText(msg);
// Use setStyle to update fill color, as direct assignment may not work in this context
self.textObj.setStyle({
fill: color
});
self.alpha = 1;
// Animate up and fade out
tween(self, {
y: self.y - 120,
alpha: 0
}, {
duration: 700,
easing: tween.cubicOut,
onFinish: function onFinish() {
if (self.parent) self.parent.removeChild(self);
}
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x87ceeb // Sky blue background for a sky effect
});
/****
* Game Code
****/
// Add several semi-transparent white ellipses as clouds
// --- Clouds background ---
// No need to init Text2 assets, just use Text2
// Feedback text colors
// Target zone highlight
// Arrow shapes (up, down, left, right) - colored for clarity
// --- Constants ---
var cloudConfigs = [{
x: 400,
y: 350,
width: 520,
height: 180
}, {
x: 1200,
y: 420,
width: 400,
height: 140
}, {
x: 900,
y: 200,
width: 320,
height: 110
}, {
x: 1700,
y: 300,
width: 380,
height: 120
}, {
x: 600,
y: 700,
width: 600,
height: 200
}, {
x: 1500,
y: 800,
width: 500,
height: 170
}];
for (var i = 0; i < cloudConfigs.length; i++) {
var cfg = cloudConfigs[i];
var cloud = LK.getAsset('cloudShape' + i, {
width: cfg.width,
height: cfg.height,
color: 0xffffff,
shape: 'ellipse',
anchorX: 0.5,
anchorY: 0.5,
x: cfg.x,
y: cfg.y
});
cloud.alpha = 1;
game.addChildAt(cloud, 0); // Add behind all other elements
}
var ARROW_TYPES = ['left', 'down', 'up', 'right'];
var ARROW_COLORS = {
'up': '#4fc3f7',
'down': '#ffb74d',
'left': '#81c784',
'right': '#e57373'
};
var FEEDBACKS = [{
msg: "Very Good",
color: 0x00E676
}, {
msg: "Good",
color: 0xFFD600
}, {
msg: "Bad",
color: 0xFF3D00
}, {
msg: "Disgusting",
color: 0xB71C1C
}];
// --- Layout ---
var GAME_W = 2048,
GAME_H = 2732;
var ARROW_SIZE = 180;
var BUTTON_SIZE = 220;
var BUTTON_MARGIN = 60;
var TARGET_ZONE_HEIGHT = 220;
var TARGET_ZONE_Y = GAME_H - 600;
// --- State ---
var fallingArrows = [];
var arrowSpawnInterval = 60; // ticks between spawns (start slow)
var arrowSpeed = 8; // px per tick (start slow)
var minArrowInterval = 24; // fastest spawn interval
var maxArrowSpeed = 32; // fastest speed
var ticksSinceLastArrow = 0;
var score = 0;
var combo = 0;
var lastArrowTypeTapped = null;
var gameOver = false;
// --- Monster event state ---
var monsterEventActive = false;
var monsterEventStartTick = null;
var monsterEventDuration = 600; // 10 seconds at 60fps
var monsterEventTriggered = false;
var normalArrowSpawnInterval = 60;
var normalArrowSpeed = 8;
// --- UI Elements ---
// Add player's skin image to the left side of the screen
var skinImg = LK.getAsset('Skin', {
anchorX: 0.5,
anchorY: 0.5,
x: 200,
// Move higher: raise the y position by 220px (was 120px)
y: TARGET_ZONE_Y + TARGET_ZONE_HEIGHT / 2 - 220,
width: 320,
height: 320
});
game.addChild(skinImg);
// Target zone highlight
var targetZone = LK.getAsset('targetZone', {
anchorX: 0,
anchorY: 0,
x: 0,
y: TARGET_ZONE_Y,
width: GAME_W,
height: TARGET_ZONE_HEIGHT
});
targetZone.alpha = 0.18;
game.addChild(targetZone);
// Score text
var scoreTxt = new Text2('0', {
size: 120,
fill: "#fff",
font: "Impact"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Feedback text (floating, created as needed)
// --- Arrow Button Controls ---
var buttonY = GAME_H - BUTTON_SIZE / 2 - 60;
var buttonXs = [GAME_W / 2 - (BUTTON_SIZE * 1.5 + BUTTON_MARGIN), GAME_W / 2 - (BUTTON_SIZE / 2 + BUTTON_MARGIN / 2), GAME_W / 2 + (BUTTON_SIZE / 2 + BUTTON_MARGIN / 2), GAME_W / 2 + (BUTTON_SIZE * 1.5 + BUTTON_MARGIN)];
var arrowButtonOrder = ['left', 'down', 'up', 'right'];
var arrowButtons = [];
// Create arrow buttons
for (var i = 0; i < 4; i++) {
(function (i) {
var type = arrowButtonOrder[i];
var btn = new Container();
var assetId = '';
if (type === 'up') assetId = 'arrowUp';
if (type === 'down') assetId = 'arrowDown';
if (type === 'left') assetId = 'arrowLeft';
if (type === 'right') assetId = 'arrowRight';
var arrowAsset = btn.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
width: BUTTON_SIZE - 20,
height: BUTTON_SIZE - 20
});
// Rotate to match direction
if (type === 'up') arrowAsset.rotation = 0;
if (type === 'down') arrowAsset.rotation = Math.PI;
if (type === 'left') arrowAsset.rotation = -Math.PI / 2;
if (type === 'right') arrowAsset.rotation = Math.PI / 2;
btn.x = buttonXs[i];
btn.y = buttonY;
btn.type = type;
btn.interactive = true;
// Add a subtle background highlight
var bg = LK.getAsset('targetZone', {
anchorX: 0.5,
anchorY: 0.5,
width: BUTTON_SIZE,
height: BUTTON_SIZE,
x: 0,
y: 0
});
bg.alpha = 0.10;
btn.addChildAt(bg, 0);
// Touch/click handler
btn.down = function (x, y, obj) {
handleArrowButtonPress(type);
};
game.addChild(btn);
arrowButtons.push(btn);
})(i);
}
// --- Helper: Find the closest arrow in the target zone for a given type ---
function findMatchingArrowInZone(type) {
var best = null;
var bestDist = 99999;
for (var i = 0; i < fallingArrows.length; i++) {
var arr = fallingArrows[i];
if (arr.arrowType !== type) continue;
// Check if in target zone
var arrowY = arr.y;
if (arrowY >= TARGET_ZONE_Y && arrowY <= TARGET_ZONE_Y + TARGET_ZONE_HEIGHT) {
// Closer to center of zone is better
var dist = Math.abs(arrowY - (TARGET_ZONE_Y + TARGET_ZONE_HEIGHT / 2));
if (dist < bestDist) {
best = arr;
bestDist = dist;
}
}
}
return best;
}
// --- Helper: Show feedback text at a position ---
function showFeedback(msg, color, x, y) {
// Determine color: green for good, red for bad
var feedbackColor;
if (msg === "Very Good" || msg === "Good") {
feedbackColor = "#00e676"; // green
} else {
feedbackColor = "#ff1744"; // red
}
var fb = new FeedbackText();
fb.x = x;
fb.y = y;
fb.show(msg, feedbackColor);
game.addChild(fb);
}
// --- Handle arrow button press ---
function handleArrowButtonPress(type) {
if (gameOver) return;
// --- Begin: Temporarily increase KSK music volume for 1s when arrow pressed ---
LK.playMusic('Ksk', {
fade: {
start: 1,
end: 2,
duration: 50
}
});
LK.setTimeout(function () {
LK.playMusic('Ksk', {
fade: {
start: 2,
end: 1,
duration: 200
}
});
}, 1000);
// --- End: music volume boost ---
var arr = findMatchingArrowInZone(type);
if (arr) {
// Good timing!
var centerY = arr.y;
var dist = Math.abs(centerY - (TARGET_ZONE_Y + TARGET_ZONE_HEIGHT / 2));
var feedback, points;
if (dist < 30) {
feedback = FEEDBACKS[0]; // Very Good
points = 3;
} else if (dist < 60) {
feedback = FEEDBACKS[1]; // Good
points = 2;
} else {
feedback = FEEDBACKS[2]; // Bad
points = 1;
}
score += points;
combo += 1;
showFeedback(feedback.msg, feedback.color, arr.x, arr.y - 100);
// Remove arrow
arr.destroy();
for (var i = 0; i < fallingArrows.length; i++) {
if (fallingArrows[i] === arr) {
fallingArrows.splice(i, 1);
break;
}
}
// Animate button
var btn = null;
for (var i = 0; i < arrowButtons.length; i++) {
if (arrowButtons[i].type === type) btn = arrowButtons[i];
}
if (btn) {
tween(btn, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 80,
easing: tween.cubicOut,
onFinish: function onFinish() {
tween(btn, {
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.cubicIn
});
}
});
}
} else {
// No matching arrow in zone: mistake!
combo = 0;
showFeedback(FEEDBACKS[3].msg, FEEDBACKS[3].color, GAME_W / 2, TARGET_ZONE_Y + TARGET_ZONE_HEIGHT / 2);
// Animate all buttons red
for (var i = 0; i < arrowButtons.length; i++) {
var btn = arrowButtons[i];
tween(btn, {
alpha: 0.5
}, {
duration: 80,
onFinish: function onFinish() {
tween(btn, {
alpha: 1
}, {
duration: 120
});
}
});
}
}
scoreTxt.setText(score);
}
// --- Spawn a new falling arrow ---
function spawnArrow() {
var type = ARROW_TYPES[Math.floor(Math.random() * 4)];
var arr = new FallingArrow();
arr.setArrowType(type);
arr.arrowType = type;
arr.speed = arrowSpeed;
var idx = arrowButtonOrder.indexOf(type);
arr.x = buttonXs[idx];
arr.y = -ARROW_SIZE / 2;
fallingArrows.push(arr);
game.addChild(arr);
}
// --- Game update loop ---
game.update = function () {
if (gameOver) return;
// Spawn arrows
ticksSinceLastArrow++;
if (ticksSinceLastArrow >= arrowSpawnInterval) {
if (monsterEventActive) {
// Spawn only 1 arrow during monster event (make it easier)
var arr = new FallingArrow();
var type = ARROW_TYPES[Math.floor(Math.random() * 4)];
arr.setArrowType(type);
arr.arrowType = type;
arr.speed = arrowSpeed;
var idx = arrowButtonOrder.indexOf(type);
arr.x = buttonXs[idx];
arr.y = -ARROW_SIZE / 2;
fallingArrows.push(arr);
game.addChild(arr);
} else {
// Spawn arrows in pairs
spawnArrow();
}
ticksSinceLastArrow = 0;
}
// Update arrows, check for misses
for (var i = fallingArrows.length - 1; i >= 0; i--) {
var arr = fallingArrows[i];
// If arrow passes target zone without being hit
if (arr.y > TARGET_ZONE_Y + TARGET_ZONE_HEIGHT + ARROW_SIZE / 2) {
// Missed!
showFeedback(FEEDBACKS[2].msg, FEEDBACKS[2].color, arr.x, TARGET_ZONE_Y + TARGET_ZONE_HEIGHT / 2);
combo = 0;
arr.destroy();
fallingArrows.splice(i, 1);
continue;
}
}
// --- Monster event logic ---
if (!monsterEventTriggered && LK.ticks >= 1200) {
// 20 seconds * 60fps
monsterEventActive = true;
monsterEventTriggered = true;
monsterEventStartTick = LK.ticks;
// Save normal values
normalArrowSpawnInterval = arrowSpawnInterval;
normalArrowSpeed = arrowSpeed;
// Make arrows slightly faster and a bit harder during monster event
arrowSpawnInterval = 40; // slightly faster than before
arrowSpeed = 10; // slightly faster than before
}
if (monsterEventActive && LK.ticks - monsterEventStartTick >= monsterEventDuration) {
// Revert to normal after 10 seconds
monsterEventActive = false;
arrowSpawnInterval = normalArrowSpawnInterval;
arrowSpeed = normalArrowSpeed;
}
// Gradually increase difficulty (skip if monster event is active)
if (!monsterEventActive && LK.ticks % 300 === 0) {
// every 5 seconds
if (arrowSpawnInterval > minArrowInterval) arrowSpawnInterval -= 2;
if (arrowSpeed < maxArrowSpeed) arrowSpeed += 1;
}
};
// --- Touch handling for game area (ignore, only arrow buttons are interactive) ---
game.down = function (x, y, obj) {
// Do nothing
};
game.move = function (x, y, obj) {
// Do nothing
};
game.up = function (x, y, obj) {
// Do nothing
};
// --- Reset state on game over ---
game.onGameOver = function () {
gameOver = true;
// Remove all arrows
for (var i = 0; i < fallingArrows.length; i++) {
fallingArrows[i].destroy();
}
fallingArrows = [];
monsterEventActive = false;
monsterEventTriggered = false;
monsterEventStartTick = null;
arrowSpawnInterval = normalArrowSpawnInterval;
arrowSpeed = normalArrowSpeed;
};
// --- Score display update ---
scoreTxt.setText(score);
// --- Start the game ---
score = 0;
combo = 0;
arrowSpawnInterval = 60;
arrowSpeed = 8;
normalArrowSpawnInterval = 60;
normalArrowSpeed = 8;
monsterEventActive = false;
monsterEventTriggered = false;
monsterEventStartTick = null;
gameOver = false;
fallingArrows = [];
ticksSinceLastArrow = 0;
// Play 'Ksk' music at game start
LK.playMusic('Ksk'); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// FallingArrow: a single falling arrow
var FallingArrow = Container.expand(function () {
var self = Container.call(this);
// Arrow type: 'up', 'down', 'left', 'right'
self.arrowType = 'up';
self.speed = 8; // Will be set on spawn
// Attach arrow asset, set anchor to center
self.arrowAsset = null;
self.setArrowType = function (type) {
self.arrowType = type;
if (self.arrowAsset) {
self.removeChild(self.arrowAsset);
}
var assetId = '';
if (type === 'up') assetId = 'arrowUp';
if (type === 'down') assetId = 'arrowDown';
if (type === 'left') assetId = 'arrowLeft';
if (type === 'right') assetId = 'arrowRight';
self.arrowAsset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Rotate asset to match direction
if (type === 'up') self.arrowAsset.rotation = 0;
if (type === 'down') self.arrowAsset.rotation = Math.PI;
if (type === 'left') self.arrowAsset.rotation = -Math.PI / 2;
if (type === 'right') self.arrowAsset.rotation = Math.PI / 2;
};
// Called every tick
self.update = function () {
self.y += self.speed;
};
return self;
});
// FeedbackText: floating feedback text (e.g. "Very Good", "Bad", etc)
var FeedbackText = Container.expand(function () {
var self = Container.call(this);
self.textObj = new Text2('', {
size: 120,
fill: "#fff",
font: "Impact"
});
self.textObj.anchor.set(0.5, 0.5);
self.addChild(self.textObj);
self.show = function (msg, color) {
self.textObj.setText(msg);
// Use setStyle to update fill color, as direct assignment may not work in this context
self.textObj.setStyle({
fill: color
});
self.alpha = 1;
// Animate up and fade out
tween(self, {
y: self.y - 120,
alpha: 0
}, {
duration: 700,
easing: tween.cubicOut,
onFinish: function onFinish() {
if (self.parent) self.parent.removeChild(self);
}
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x87ceeb // Sky blue background for a sky effect
});
/****
* Game Code
****/
// Add several semi-transparent white ellipses as clouds
// --- Clouds background ---
// No need to init Text2 assets, just use Text2
// Feedback text colors
// Target zone highlight
// Arrow shapes (up, down, left, right) - colored for clarity
// --- Constants ---
var cloudConfigs = [{
x: 400,
y: 350,
width: 520,
height: 180
}, {
x: 1200,
y: 420,
width: 400,
height: 140
}, {
x: 900,
y: 200,
width: 320,
height: 110
}, {
x: 1700,
y: 300,
width: 380,
height: 120
}, {
x: 600,
y: 700,
width: 600,
height: 200
}, {
x: 1500,
y: 800,
width: 500,
height: 170
}];
for (var i = 0; i < cloudConfigs.length; i++) {
var cfg = cloudConfigs[i];
var cloud = LK.getAsset('cloudShape' + i, {
width: cfg.width,
height: cfg.height,
color: 0xffffff,
shape: 'ellipse',
anchorX: 0.5,
anchorY: 0.5,
x: cfg.x,
y: cfg.y
});
cloud.alpha = 1;
game.addChildAt(cloud, 0); // Add behind all other elements
}
var ARROW_TYPES = ['left', 'down', 'up', 'right'];
var ARROW_COLORS = {
'up': '#4fc3f7',
'down': '#ffb74d',
'left': '#81c784',
'right': '#e57373'
};
var FEEDBACKS = [{
msg: "Very Good",
color: 0x00E676
}, {
msg: "Good",
color: 0xFFD600
}, {
msg: "Bad",
color: 0xFF3D00
}, {
msg: "Disgusting",
color: 0xB71C1C
}];
// --- Layout ---
var GAME_W = 2048,
GAME_H = 2732;
var ARROW_SIZE = 180;
var BUTTON_SIZE = 220;
var BUTTON_MARGIN = 60;
var TARGET_ZONE_HEIGHT = 220;
var TARGET_ZONE_Y = GAME_H - 600;
// --- State ---
var fallingArrows = [];
var arrowSpawnInterval = 60; // ticks between spawns (start slow)
var arrowSpeed = 8; // px per tick (start slow)
var minArrowInterval = 24; // fastest spawn interval
var maxArrowSpeed = 32; // fastest speed
var ticksSinceLastArrow = 0;
var score = 0;
var combo = 0;
var lastArrowTypeTapped = null;
var gameOver = false;
// --- Monster event state ---
var monsterEventActive = false;
var monsterEventStartTick = null;
var monsterEventDuration = 600; // 10 seconds at 60fps
var monsterEventTriggered = false;
var normalArrowSpawnInterval = 60;
var normalArrowSpeed = 8;
// --- UI Elements ---
// Add player's skin image to the left side of the screen
var skinImg = LK.getAsset('Skin', {
anchorX: 0.5,
anchorY: 0.5,
x: 200,
// Move higher: raise the y position by 220px (was 120px)
y: TARGET_ZONE_Y + TARGET_ZONE_HEIGHT / 2 - 220,
width: 320,
height: 320
});
game.addChild(skinImg);
// Target zone highlight
var targetZone = LK.getAsset('targetZone', {
anchorX: 0,
anchorY: 0,
x: 0,
y: TARGET_ZONE_Y,
width: GAME_W,
height: TARGET_ZONE_HEIGHT
});
targetZone.alpha = 0.18;
game.addChild(targetZone);
// Score text
var scoreTxt = new Text2('0', {
size: 120,
fill: "#fff",
font: "Impact"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Feedback text (floating, created as needed)
// --- Arrow Button Controls ---
var buttonY = GAME_H - BUTTON_SIZE / 2 - 60;
var buttonXs = [GAME_W / 2 - (BUTTON_SIZE * 1.5 + BUTTON_MARGIN), GAME_W / 2 - (BUTTON_SIZE / 2 + BUTTON_MARGIN / 2), GAME_W / 2 + (BUTTON_SIZE / 2 + BUTTON_MARGIN / 2), GAME_W / 2 + (BUTTON_SIZE * 1.5 + BUTTON_MARGIN)];
var arrowButtonOrder = ['left', 'down', 'up', 'right'];
var arrowButtons = [];
// Create arrow buttons
for (var i = 0; i < 4; i++) {
(function (i) {
var type = arrowButtonOrder[i];
var btn = new Container();
var assetId = '';
if (type === 'up') assetId = 'arrowUp';
if (type === 'down') assetId = 'arrowDown';
if (type === 'left') assetId = 'arrowLeft';
if (type === 'right') assetId = 'arrowRight';
var arrowAsset = btn.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
width: BUTTON_SIZE - 20,
height: BUTTON_SIZE - 20
});
// Rotate to match direction
if (type === 'up') arrowAsset.rotation = 0;
if (type === 'down') arrowAsset.rotation = Math.PI;
if (type === 'left') arrowAsset.rotation = -Math.PI / 2;
if (type === 'right') arrowAsset.rotation = Math.PI / 2;
btn.x = buttonXs[i];
btn.y = buttonY;
btn.type = type;
btn.interactive = true;
// Add a subtle background highlight
var bg = LK.getAsset('targetZone', {
anchorX: 0.5,
anchorY: 0.5,
width: BUTTON_SIZE,
height: BUTTON_SIZE,
x: 0,
y: 0
});
bg.alpha = 0.10;
btn.addChildAt(bg, 0);
// Touch/click handler
btn.down = function (x, y, obj) {
handleArrowButtonPress(type);
};
game.addChild(btn);
arrowButtons.push(btn);
})(i);
}
// --- Helper: Find the closest arrow in the target zone for a given type ---
function findMatchingArrowInZone(type) {
var best = null;
var bestDist = 99999;
for (var i = 0; i < fallingArrows.length; i++) {
var arr = fallingArrows[i];
if (arr.arrowType !== type) continue;
// Check if in target zone
var arrowY = arr.y;
if (arrowY >= TARGET_ZONE_Y && arrowY <= TARGET_ZONE_Y + TARGET_ZONE_HEIGHT) {
// Closer to center of zone is better
var dist = Math.abs(arrowY - (TARGET_ZONE_Y + TARGET_ZONE_HEIGHT / 2));
if (dist < bestDist) {
best = arr;
bestDist = dist;
}
}
}
return best;
}
// --- Helper: Show feedback text at a position ---
function showFeedback(msg, color, x, y) {
// Determine color: green for good, red for bad
var feedbackColor;
if (msg === "Very Good" || msg === "Good") {
feedbackColor = "#00e676"; // green
} else {
feedbackColor = "#ff1744"; // red
}
var fb = new FeedbackText();
fb.x = x;
fb.y = y;
fb.show(msg, feedbackColor);
game.addChild(fb);
}
// --- Handle arrow button press ---
function handleArrowButtonPress(type) {
if (gameOver) return;
// --- Begin: Temporarily increase KSK music volume for 1s when arrow pressed ---
LK.playMusic('Ksk', {
fade: {
start: 1,
end: 2,
duration: 50
}
});
LK.setTimeout(function () {
LK.playMusic('Ksk', {
fade: {
start: 2,
end: 1,
duration: 200
}
});
}, 1000);
// --- End: music volume boost ---
var arr = findMatchingArrowInZone(type);
if (arr) {
// Good timing!
var centerY = arr.y;
var dist = Math.abs(centerY - (TARGET_ZONE_Y + TARGET_ZONE_HEIGHT / 2));
var feedback, points;
if (dist < 30) {
feedback = FEEDBACKS[0]; // Very Good
points = 3;
} else if (dist < 60) {
feedback = FEEDBACKS[1]; // Good
points = 2;
} else {
feedback = FEEDBACKS[2]; // Bad
points = 1;
}
score += points;
combo += 1;
showFeedback(feedback.msg, feedback.color, arr.x, arr.y - 100);
// Remove arrow
arr.destroy();
for (var i = 0; i < fallingArrows.length; i++) {
if (fallingArrows[i] === arr) {
fallingArrows.splice(i, 1);
break;
}
}
// Animate button
var btn = null;
for (var i = 0; i < arrowButtons.length; i++) {
if (arrowButtons[i].type === type) btn = arrowButtons[i];
}
if (btn) {
tween(btn, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 80,
easing: tween.cubicOut,
onFinish: function onFinish() {
tween(btn, {
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.cubicIn
});
}
});
}
} else {
// No matching arrow in zone: mistake!
combo = 0;
showFeedback(FEEDBACKS[3].msg, FEEDBACKS[3].color, GAME_W / 2, TARGET_ZONE_Y + TARGET_ZONE_HEIGHT / 2);
// Animate all buttons red
for (var i = 0; i < arrowButtons.length; i++) {
var btn = arrowButtons[i];
tween(btn, {
alpha: 0.5
}, {
duration: 80,
onFinish: function onFinish() {
tween(btn, {
alpha: 1
}, {
duration: 120
});
}
});
}
}
scoreTxt.setText(score);
}
// --- Spawn a new falling arrow ---
function spawnArrow() {
var type = ARROW_TYPES[Math.floor(Math.random() * 4)];
var arr = new FallingArrow();
arr.setArrowType(type);
arr.arrowType = type;
arr.speed = arrowSpeed;
var idx = arrowButtonOrder.indexOf(type);
arr.x = buttonXs[idx];
arr.y = -ARROW_SIZE / 2;
fallingArrows.push(arr);
game.addChild(arr);
}
// --- Game update loop ---
game.update = function () {
if (gameOver) return;
// Spawn arrows
ticksSinceLastArrow++;
if (ticksSinceLastArrow >= arrowSpawnInterval) {
if (monsterEventActive) {
// Spawn only 1 arrow during monster event (make it easier)
var arr = new FallingArrow();
var type = ARROW_TYPES[Math.floor(Math.random() * 4)];
arr.setArrowType(type);
arr.arrowType = type;
arr.speed = arrowSpeed;
var idx = arrowButtonOrder.indexOf(type);
arr.x = buttonXs[idx];
arr.y = -ARROW_SIZE / 2;
fallingArrows.push(arr);
game.addChild(arr);
} else {
// Spawn arrows in pairs
spawnArrow();
}
ticksSinceLastArrow = 0;
}
// Update arrows, check for misses
for (var i = fallingArrows.length - 1; i >= 0; i--) {
var arr = fallingArrows[i];
// If arrow passes target zone without being hit
if (arr.y > TARGET_ZONE_Y + TARGET_ZONE_HEIGHT + ARROW_SIZE / 2) {
// Missed!
showFeedback(FEEDBACKS[2].msg, FEEDBACKS[2].color, arr.x, TARGET_ZONE_Y + TARGET_ZONE_HEIGHT / 2);
combo = 0;
arr.destroy();
fallingArrows.splice(i, 1);
continue;
}
}
// --- Monster event logic ---
if (!monsterEventTriggered && LK.ticks >= 1200) {
// 20 seconds * 60fps
monsterEventActive = true;
monsterEventTriggered = true;
monsterEventStartTick = LK.ticks;
// Save normal values
normalArrowSpawnInterval = arrowSpawnInterval;
normalArrowSpeed = arrowSpeed;
// Make arrows slightly faster and a bit harder during monster event
arrowSpawnInterval = 40; // slightly faster than before
arrowSpeed = 10; // slightly faster than before
}
if (monsterEventActive && LK.ticks - monsterEventStartTick >= monsterEventDuration) {
// Revert to normal after 10 seconds
monsterEventActive = false;
arrowSpawnInterval = normalArrowSpawnInterval;
arrowSpeed = normalArrowSpeed;
}
// Gradually increase difficulty (skip if monster event is active)
if (!monsterEventActive && LK.ticks % 300 === 0) {
// every 5 seconds
if (arrowSpawnInterval > minArrowInterval) arrowSpawnInterval -= 2;
if (arrowSpeed < maxArrowSpeed) arrowSpeed += 1;
}
};
// --- Touch handling for game area (ignore, only arrow buttons are interactive) ---
game.down = function (x, y, obj) {
// Do nothing
};
game.move = function (x, y, obj) {
// Do nothing
};
game.up = function (x, y, obj) {
// Do nothing
};
// --- Reset state on game over ---
game.onGameOver = function () {
gameOver = true;
// Remove all arrows
for (var i = 0; i < fallingArrows.length; i++) {
fallingArrows[i].destroy();
}
fallingArrows = [];
monsterEventActive = false;
monsterEventTriggered = false;
monsterEventStartTick = null;
arrowSpawnInterval = normalArrowSpawnInterval;
arrowSpeed = normalArrowSpeed;
};
// --- Score display update ---
scoreTxt.setText(score);
// --- Start the game ---
score = 0;
combo = 0;
arrowSpawnInterval = 60;
arrowSpeed = 8;
normalArrowSpawnInterval = 60;
normalArrowSpeed = 8;
monsterEventActive = false;
monsterEventTriggered = false;
monsterEventStartTick = null;
gameOver = false;
fallingArrows = [];
ticksSinceLastArrow = 0;
// Play 'Ksk' music at game start
LK.playMusic('Ksk');