/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
highScore: 0
});
/****
* Classes
****/
// Achievement popup class
var AchievementPopup = Container.expand(function () {
var self = Container.call(this);
var txt = new Text2('', {
size: 120,
fill: 0xFFD700
});
txt.anchor.set(0.5, 0.5);
self.addChild(txt);
self.visible = false;
self.show = function (message, color) {
txt.setText(message);
if (txt && txt.style) {
txt.style.fill = color || 0xFFD700;
}
self.x = 2048 / 2;
self.y = 2732 / 2 - 300;
self.visible = true;
self.alpha = 1;
tween(self, {
alpha: 0
}, {
duration: 1200,
onFinish: function onFinish() {
self.visible = false;
}
});
};
return self;
});
// Dog class
var Dog = Container.expand(function () {
var self = Container.call(this);
self.dogAsset = null;
self.laughAsset = null;
self.currentDogId = null;
// Helper to set dog asset based on selectedDogId
self.setDogAssets = function () {
// Remove old assets if any
if (self.dogAsset && self.dogAsset.parent) self.removeChild(self.dogAsset);
if (self.laughAsset && self.laughAsset.parent) self.removeChild(self.laughAsset);
self.dogAsset = null;
self.laughAsset = null;
var dogOpt = null;
for (var i = 0; i < DOG_OPTIONS.length; i++) {
if (DOG_OPTIONS[i].id === selectedDogId) {
dogOpt = DOG_OPTIONS[i];
break;
}
}
if (!dogOpt) dogOpt = DOG_OPTIONS[0];
self.currentDogId = dogOpt.id;
self.dogAsset = self.attachAsset(dogOpt.asset, {
anchorX: 0.5,
anchorY: 1
});
self.dogAsset.visible = false;
// Laugh asset will be created on demand in laugh()
};
self.setDogAssets();
self.visible = false;
// Show dog at (x, y)
self.show = function (x, y) {
// If dog changed, update assets
if (self.currentDogId !== selectedDogId) {
self.setDogAssets();
}
self.x = x;
self.y = y;
self.visible = true;
if (self.dogAsset) self.dogAsset.visible = true;
if (self.laughAsset) self.laughAsset.visible = false;
// Optionally, you could add a little "pop up" animation here
// For example, tween the dog from below the screen to y
var startY = 2732 + (self.dogAsset ? self.dogAsset.height : 180);
self.y = startY;
tween(self, {
y: y
}, {
duration: 350,
easing: tween.cubicOut
});
};
// Hide dog
self.hide = function () {
var endY = 2732 + (self.dogAsset ? self.dogAsset.height : 180);
tween(self, {
y: endY
}, {
duration: 350,
easing: tween.cubicIn,
onFinish: function onFinish() {
self.visible = false;
if (self.dogAsset) self.dogAsset.visible = false;
}
});
};
// Show laugh animation
self.laugh = function (x, y) {
// If dog changed, update assets
if (self.currentDogId !== selectedDogId) {
self.setDogAssets();
}
self.x = x;
self.y = y;
self.visible = true;
if (self.dogAsset) self.dogAsset.visible = false;
// Find laugh asset id for this dog
var dogOpt = null;
for (var i = 0; i < DOG_OPTIONS.length; i++) {
if (DOG_OPTIONS[i].id === selectedDogId) {
dogOpt = DOG_OPTIONS[i];
break;
}
}
if (!dogOpt) dogOpt = DOG_OPTIONS[0];
var laughId = dogOpt.laugh || "dog_laugh";
if (!self.laughAsset || self.laughAsset._assetId !== laughId) {
if (self.laughAsset && self.laughAsset.parent) self.removeChild(self.laughAsset);
self.laughAsset = self.attachAsset(laughId, {
anchorX: 0.5,
anchorY: 1
});
self.laughAsset._assetId = laughId;
}
self.laughAsset.visible = true;
// Play laugh sound (use default for all)
LK.getSound('dog_laugh').play();
// Hide after 1.2s
LK.setTimeout(function () {
self.laughAsset.visible = false;
self.hide();
}, 1200);
};
return self;
});
// Duck class
var Duck = Container.expand(function () {
var self = Container.call(this);
// Determine duck type based on level and DUCK_TYPE_CONFIG
var duckType = 'normal';
var levelIdx = Math.max(0, Math.min(level - 1, DUCK_TYPE_CONFIG.length - 1));
var config = DUCK_TYPE_CONFIG[levelIdx];
var r = Math.random();
if (r < config.golden) {
duckType = 'golden';
} else if (r < config.golden + config.armored) {
duckType = 'armored';
} else if (r < config.golden + config.armored + config.mini) {
duckType = 'mini';
} else if (Math.random() < 0.25) {
duckType = 'fast';
}
self.type = duckType;
// Asset selection
var duckAsset = null;
if (duckType === 'golden') {
duckAsset = self.attachAsset('duck_golden', {
anchorX: 0.5,
anchorY: 0.5
});
duckAsset.tint = 0xFFD700;
} else if (duckType === 'armored') {
duckAsset = self.attachAsset('duck_armored', {
anchorX: 0.5,
anchorY: 0.5
});
duckAsset.tint = 0x888888;
} else if (duckType === 'mini') {
duckAsset = self.attachAsset('duck_mini', {
anchorX: 0.5,
anchorY: 0.5
});
} else {
// normal or fast
var duckColors = ['duck_green', 'duck_blue', 'duck_red'];
var colorIdx = Math.floor(Math.random() * duckColors.length);
duckAsset = self.attachAsset(duckColors[colorIdx], {
anchorX: 0.5,
anchorY: 0.5
});
}
// Set initial position and movement
self.speed = 6 + Math.random() * 4; // Will be set by level
self.angle = 0; // radians, will be set by level
self.alive = true;
self.hit = false;
self.escaped = false;
self.armor = duckType === 'armored' ? 2 : 1; // 2 hits for armored
self.flashTween = null;
// For animation
self.flyTween = null;
// --- Hitbox properties for all ducks (mini ducks are smaller, fast/golden slightly larger) ---
self.getHitboxRadius = function () {
var baseRadius = Math.max(duckAsset.width, duckAsset.height) * 0.5;
if (self.type === 'mini') {
return baseRadius * 0.55;
}
if (self.type === 'fast' || self.type === 'golden' || self.speed >= 6) {
return baseRadius * 0.7;
}
return baseRadius * 0.55;
};
// Head hitbox (top 1/3 of duck)
self.isHeadshot = function (x, y) {
var dx = self.x - x;
var dy = self.y - y;
var dist = Math.sqrt(dx * dx + dy * dy);
var headRadius = duckAsset.height * 0.18;
var headCenterY = self.y - duckAsset.height * 0.22;
var headDist = Math.sqrt((self.x - x) * (self.x - x) + (headCenterY - y) * (headCenterY - y));
return headDist < headRadius;
};
// Called every tick
self.update = function () {
if (!self.alive || self.hit) return;
self.x += Math.cos(self.angle) * self.speed;
self.y += Math.sin(self.angle) * self.speed;
if (self.x < -100 || self.x > 2048 + 100 || self.y < -100 || self.y > 2732 + 100) {
self.escaped = true;
self.alive = false;
}
};
// Animate duck falling when hit
self.fall = function (_onFinish) {
self.alive = false;
tween(self, {
rotation: Math.PI * 1.5,
y: self.y + 400
}, {
duration: 700,
easing: tween.cubicIn,
onFinish: function onFinish() {
if (_onFinish) _onFinish();
}
});
};
// Armored duck: flash red on first hit
self.flashRed = function () {
if (self.flashTween) {
self.flashTween.cancel();
}
var origTint = duckAsset.tint;
duckAsset.tint = 0xff2222;
self.flashTween = tween(duckAsset, {}, {
duration: 200,
onFinish: function onFinish() {
duckAsset.tint = 0x888888;
self.flashTween = null;
}
});
};
return self;
});
// Power-up collectible class
var PowerupCollectible = Container.expand(function () {
var self = Container.call(this);
self.type = null;
self.icon = null;
self.radius = 60;
self.speedY = -2 - Math.random() * 2;
self.lifetime = 0;
self.maxLifetime = 400; // ~6 seconds
self.init = function (ptype, x, y) {
self.type = ptype;
var iconId = ptype === 'doubleBarrel' ? 'powerup_double' : ptype === 'slowTime' ? 'powerup_slow' : 'powerup_auto';
self.icon = self.attachAsset(iconId, {
anchorX: 0.5,
anchorY: 0.5
});
self.x = x;
self.y = y;
self.icon.scaleX = self.icon.scaleY = 1.1;
self.visible = true;
self.alpha = 0.92;
};
self.update = function () {
self.y += self.speedY;
self.lifetime++;
// Fade out near end of life
if (self.lifetime > self.maxLifetime - 60) {
self.alpha = Math.max(0, (self.maxLifetime - self.lifetime) / 60);
}
// Remove if out of bounds or expired
if (self.y < 100 || self.lifetime > self.maxLifetime) {
if (self.parent) self.parent.removeChild(self);
if (typeof powerupCollectibles !== "undefined") {
var idx = powerupCollectibles.indexOf(self);
if (idx >= 0) powerupCollectibles.splice(idx, 1);
}
}
};
// Check if tap is within radius
self.isHit = function (x, y) {
var dx = self.x - x;
var dy = self.y - y;
return dx * dx + dy * dy < self.radius * self.radius;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x87ceeb // sky blue
});
/****
* Game Code
****/
// Use red as base, smaller
// Use blue as base, gray tint
// Use red as base, gold tint
// --- Duck type probabilities and point values ---
var DUCK_TYPE_CONFIG = [
// Level 1
{
golden: 0.04,
// 4%
armored: 0.0,
mini: 0.0
},
// Level 2
{
golden: 0.06,
armored: 0.08,
mini: 0.04
},
// Level 3
{
golden: 0.08,
armored: 0.13,
mini: 0.08
},
// Level 4
{
golden: 0.10,
armored: 0.18,
mini: 0.13
},
// Level 5+
{
golden: 0.13,
armored: 0.22,
mini: 0.18
}];
var DUCK_TYPE_POINTS = {
normal: 10,
golden: 50,
armored: 20,
mini: 25
};
// --- Power-up collectibles array ---
// Ducks: 3 colors, 1 dog, 1 bullet, 1 background, 1 crosshair, 1 "laugh" dog
// sky blue
// Sounds
// Music
// Power-up icons and retro overlay
var powerupCollectibles = [];
// Spawn a power-up collectible at a random position
function spawnPowerupCollectible(ptype) {
var px = 200 + Math.random() * (2048 - 400);
var py = 2732 - 200 - Math.random() * 800;
var p = new PowerupCollectible();
p.init(ptype, px, py);
powerupCollectibles.push(p);
game.addChild(p);
}
// Activate a power-up
function activatePowerup(ptype) {
if (powerupActive[ptype]) return;
powerupActive[ptype] = true;
var duration = 0;
if (ptype === 'doubleBarrel') duration = 7;
if (ptype === 'slowTime') duration = 5;
if (ptype === 'autoAim') duration = 3;
powerupTimers[ptype] = duration;
showPowerupIcon(ptype, duration);
// Apply effect
if (ptype === 'slowTime') {
// Slow all ducks
for (var i = 0; i < ducks.length; i++) {
ducks[i].speed = ducks[i].speed * 0.35;
}
// Add retro overlay
if (!slowTimeEffectOverlay) {
slowTimeEffectOverlay = LK.getAsset('retro_overlay', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
});
slowTimeEffectOverlay.alpha = 0.22;
game.addChild(slowTimeEffectOverlay);
}
}
if (ptype === 'autoAim') {
// No immediate effect, handled in shooting logic
}
if (ptype === 'doubleBarrel') {
// No immediate effect, handled in shooting logic
}
// Set timeout to deactivate
if (powerupTimeouts[ptype]) LK.clearTimeout(powerupTimeouts[ptype]);
powerupTimeouts[ptype] = LK.setTimeout(function () {
powerupActive[ptype] = false;
powerupTimers[ptype] = 0;
hidePowerupIcon(ptype);
if (ptype === 'slowTime') {
// Restore duck speed
for (var i = 0; i < ducks.length; i++) {
ducks[i].speed = ducks[i].speed / 0.35;
}
// Remove overlay
if (slowTimeEffectOverlay && slowTimeEffectOverlay.parent) {
slowTimeEffectOverlay.parent.removeChild(slowTimeEffectOverlay);
slowTimeEffectOverlay = null;
}
}
}, duration * 1000);
}
// Show power-up icon in GUI
function showPowerupIcon(ptype, duration) {
var iconId = ptype === 'doubleBarrel' ? 'powerup_double' : ptype === 'slowTime' ? 'powerup_slow' : 'powerup_auto';
var icon = LK.getAsset(iconId, {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0
});
icon.scaleX = icon.scaleY = 1.1;
var timerTxt = new Text2(duration + "s", {
size: 60,
fill: 0xffffff
});
timerTxt.anchor.set(0.5, 0.5);
icon.addChild(timerTxt);
powerupIcons[ptype] = icon;
// Place icons in top left, but not in 0-100px (menu area)
var idx = ptype === 'doubleBarrel' ? 0 : ptype === 'slowTime' ? 1 : 2;
icon.x = 120 + idx * 120;
icon.y = 120;
LK.gui.top.addChild(icon);
icon._timerTxt = timerTxt;
}
// Hide power-up icon
function hidePowerupIcon(ptype) {
if (powerupIcons[ptype] && powerupIcons[ptype].parent) {
powerupIcons[ptype].parent.removeChild(powerupIcons[ptype]);
powerupIcons[ptype] = null;
}
}
// Game state variables
var ducks = [];
var bullets = [];
var level = 1;
var ducksPerLevel = 5;
var ducksToHit = 0;
var ducksHit = 0;
var ducksEscaped = 0;
var maxBullets = 3;
var bulletsLeft = 3;
var timeLimit = 15; // seconds
var timeLeft = 15;
var gameActive = false;
var waveActive = false;
// Per-mode high score storage
var highScoreClassic = storage.highScoreClassic || 0;
var highScoreTimeAttack = storage.highScoreTimeAttack || 0;
var highScoreEndless = storage.highScoreEndless || 0;
var highScore = 0; // Will be set per mode in showMainMenu/startLevel
var score = 0;
var dog = null;
var crosshair = null;
var timerInterval = null;
var levelText = null;
var scoreText = null;
var timerText = null;
var bulletIcons = [];
var waveResultTimeout = null;
// Track missed shots for warning
var missedShots = 0;
var missedWarningShown = false;
// --- Power-up State ---
var powerupActive = {
doubleBarrel: false,
slowTime: false,
autoAim: false
};
var powerupTimers = {
doubleBarrel: 0,
slowTime: 0,
autoAim: 0
};
var powerupIcons = {
doubleBarrel: null,
slowTime: null,
autoAim: null
};
var powerupTimeouts = {
doubleBarrel: null,
slowTime: null,
autoAim: null
};
// For slow time effect
var slowTimeEffectOverlay = null;
// --- Game Mode State ---
var GAME_MODE_CLASSIC = "classic";
var GAME_MODE_TIMEATTACK = "timeattack";
var GAME_MODE_ENDLESS = "endless";
var gameMode = null; // Set by menu
var mainMenuContainer = null;
var endlessMissStreak = 0;
var endlessGameOver = false;
// --- Dog Selection State ---
var DOG_OPTIONS = [{
id: "dodo",
name: "Dodo",
asset: "dog",
laugh: "dog_laugh"
}, {
id: "kaiser",
name: "Kaiser",
asset: "dog_dalmatian",
laugh: "laugh_d"
}, {
id: "maki",
name: "Maki",
asset: "dog_labrador",
laugh: "laugh_l"
}];
var selectedDogId = "dodo"; // default dog
// Background
var bg = LK.getAsset('bg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
});
game.addChild(bg);
// --- Main Menu UI ---
function showMainMenu() {
// Remove previous menu if present
if (mainMenuContainer && mainMenuContainer.parent) {
mainMenuContainer.parent.removeChild(mainMenuContainer);
}
mainMenuContainer = new Container();
// Dim background
var menuBg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 16,
scaleY: 16,
x: 2048 / 2,
y: 2732 / 2
});
menuBg.alpha = 0.85;
mainMenuContainer.addChild(menuBg);
// Title
var title = new Text2("What The Duck", {
size: 220,
fill: 0xFFD700,
font: "'PressStart2P','GillSans-Bold',Impact,'Arial Black',Tahoma"
});
title.anchor.set(0.5, 0);
title.x = 2048 / 2;
title.y = 400;
mainMenuContainer.addChild(title);
// Classic Mode Button
var btnClassic = new Text2("Classic Mode", {
size: 120,
fill: 0xFFFFFF,
font: "'PressStart2P','GillSans-Bold',Impact,'Arial Black',Tahoma"
});
btnClassic.anchor.set(0.5, 0.5);
btnClassic.x = 2048 / 2;
btnClassic.y = 900;
mainMenuContainer.addChild(btnClassic);
// Time Attack Button
var btnTime = new Text2("Time Attack", {
size: 120,
fill: 0xFFFFFF,
font: "'PressStart2P','GillSans-Bold',Impact,'Arial Black',Tahoma"
});
btnTime.anchor.set(0.5, 0.5);
btnTime.x = 2048 / 2;
btnTime.y = 1100;
mainMenuContainer.addChild(btnTime);
// Endless Mode Button
var btnEndless = new Text2("Endless Mode", {
size: 120,
fill: 0xFFFFFF,
font: "'PressStart2P','GillSans-Bold',Impact,'Arial Black',Tahoma"
});
btnEndless.anchor.set(0.5, 0.5);
btnEndless.x = 2048 / 2;
btnEndless.y = 1300;
mainMenuContainer.addChild(btnEndless);
// Rules Button
var btnRules = new Text2("Rules", {
size: 100,
fill: 0xFFFFFF,
font: "'PressStart2P','GillSans-Bold',Impact,'Arial Black',Tahoma"
});
btnRules.anchor.set(0.5, 0.5);
btnRules.x = 2048 / 2;
btnRules.y = 1450;
mainMenuContainer.addChild(btnRules);
// Instructions
var info = new Text2("Choose a mode to start!", {
size: 80,
fill: 0xFFFFFF,
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
info.anchor.set(0.5, 0.5);
info.x = 2048 / 2;
info.y = 1550;
mainMenuContainer.addChild(info);
// --- Rules Popup ---
var rulesPopup = null;
function showRulesPopup() {
if (rulesPopup && rulesPopup.parent) rulesPopup.parent.removeChild(rulesPopup);
rulesPopup = new Container();
var popupBg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 10,
scaleY: 10,
x: 2048 / 2,
y: 2732 / 2
});
popupBg.alpha = 0.96;
rulesPopup.addChild(popupBg);
var rulesTitle = new Text2("Game Rules", {
size: 120,
fill: 0xFFD700,
font: "'PressStart2P','GillSans-Bold',Impact,'Arial Black',Tahoma"
});
rulesTitle.anchor.set(0.5, 0);
rulesTitle.x = 2048 / 2;
rulesTitle.y = 400;
rulesPopup.addChild(rulesTitle);
var rulesText = new Text2("Classic Mode:\n" + "- Hit enough ducks with limited bullets to advance.\n" + "- Each level has more ducks and bullets.\n\n" + "Time Attack:\n" + "- 30 seconds to hit as many ducks as possible.\n" + "- Unlimited bullets.\n\n" + "Endless Mode:\n" + "- Ducks spawn forever, unlimited bullets.\n" + "- Game ends if you miss 3 times in a row.\n\n" + "Duck Types:\n" + "- Golden Duck: Fast, +50 points.\n" + "- Armored Duck: Needs 2 hits.\n" + "- Mini Fast Duck: Small, fast, +25 points.\n\n" + "Power-ups:\n" + "- Double Barrel: Hit 2 ducks at once.\n" + "- Slow Time: Ducks move slowly for 5s.\n" + "- Auto-Aim: Instantly hit nearest duck for 3s.\n\n" + "Tap power-up icons to collect. Good luck!", {
size: 60,
fill: 0xFF2222,
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma",
wordWrap: true,
wordWrapWidth: 1600,
align: "left"
});
rulesText.anchor.set(0.5, 0);
rulesText.x = 2048 / 2;
rulesText.y = 550;
rulesPopup.addChild(rulesText);
var btnClose = new Text2("Close", {
size: 100,
fill: 0xFFD700,
font: "'PressStart2P','GillSans-Bold',Impact,'Arial Black',Tahoma"
});
btnClose.anchor.set(0.5, 0.5);
btnClose.x = 2048 / 2;
btnClose.y = 2300;
btnClose.interactive = true;
btnClose.down = function () {
if (rulesPopup.parent) rulesPopup.parent.removeChild(rulesPopup);
};
rulesPopup.addChild(btnClose);
game.addChild(rulesPopup);
}
// Button handlers
// --- Dog Select Screen ---
function showDogSelectScreen() {
// Remove previous menu if present
if (mainMenuContainer && mainMenuContainer.parent) {
mainMenuContainer.parent.removeChild(mainMenuContainer);
}
mainMenuContainer = new Container();
// Dim background
var menuBg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 16,
scaleY: 16,
x: 2048 / 2,
y: 2732 / 2
});
menuBg.alpha = 0.85;
mainMenuContainer.addChild(menuBg);
// Title
var title = new Text2("Select Your Dog", {
size: 180,
fill: 0xFFD700,
font: "'PressStart2P','GillSans-Bold',Impact,'Arial Black',Tahoma"
});
title.anchor.set(0.5, 0);
title.x = 2048 / 2;
title.y = 400;
mainMenuContainer.addChild(title);
// Dog options
var spacing = 600;
var startX = 2048 / 2 - spacing;
var y = 1100;
for (var i = 0; i < DOG_OPTIONS.length; i++) {
(function (i) {
var dogOpt = DOG_OPTIONS[i];
var asset = LK.getAsset(dogOpt.asset, {
anchorX: 0.5,
anchorY: 1,
x: startX + i * spacing,
y: y
});
asset.scaleX = asset.scaleY = 1.1;
mainMenuContainer.addChild(asset);
var nameTxt = new Text2(dogOpt.name, {
size: 100,
fill: 0xFFFFFF,
font: "'PressStart2P','GillSans-Bold',Impact,'Arial Black',Tahoma"
});
nameTxt.anchor.set(0.5, 0);
nameTxt.x = asset.x;
nameTxt.y = y + 30;
mainMenuContainer.addChild(nameTxt);
// Make asset interactive
asset.interactive = true;
asset.down = function () {
selectedDogId = dogOpt.id;
if (mainMenuContainer.parent) mainMenuContainer.parent.removeChild(mainMenuContainer);
startLevel();
};
nameTxt.interactive = true;
nameTxt.down = asset.down;
})(i);
}
// Back button
var btnBack = new Text2("Back", {
size: 90,
fill: 0xFFD700,
font: "'PressStart2P','GillSans-Bold',Impact,'Arial Black',Tahoma"
});
btnBack.anchor.set(0.5, 0.5);
btnBack.x = 2048 / 2;
btnBack.y = 2100;
btnBack.interactive = true;
btnBack.down = function () {
if (mainMenuContainer.parent) mainMenuContainer.parent.removeChild(mainMenuContainer);
showMainMenu();
};
mainMenuContainer.addChild(btnBack);
game.addChild(mainMenuContainer);
}
// Mode buttons now show dog select screen
btnClassic.interactive = true;
btnClassic.down = function () {
gameMode = GAME_MODE_CLASSIC;
showDogSelectScreen();
};
btnTime.interactive = true;
btnTime.down = function () {
gameMode = GAME_MODE_TIMEATTACK;
showDogSelectScreen();
};
btnEndless.interactive = true;
btnEndless.down = function () {
gameMode = GAME_MODE_ENDLESS;
showDogSelectScreen();
};
btnRules.interactive = true;
btnRules.down = function () {
showRulesPopup();
};
// Add to game
game.addChild(mainMenuContainer);
}
// Show menu on load
showMainMenu();
// Dog
dog = new Dog();
game.addChild(dog);
// Achievement popup (always on top)
var achievementPopup = new AchievementPopup();
game.addChild(achievementPopup);
// Crosshair
crosshair = LK.getAsset('crosshair', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366
});
crosshair.visible = false;
game.addChild(crosshair);
// GUI: Level, Score, Timer, Bullets
levelText = new Text2('Level 1', {
size: 90,
fill: 0xFFF000
});
levelText.anchor.set(1, 0); // right align to match high score
scoreText = new Text2('Score: 0', {
size: 90,
fill: 0xFFFFFF
});
scoreText.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreText);
timerText = new Text2('Time: 15', {
size: 90,
fill: 0xFFFFFF
});
timerText.anchor.set(0.5, 0);
LK.gui.top.addChild(timerText);
// High Score UI
// Set highScore based on mode (default to classic)
if (gameMode === GAME_MODE_TIMEATTACK) {
highScore = highScoreTimeAttack;
} else if (gameMode === GAME_MODE_ENDLESS) {
highScore = highScoreEndless;
} else {
highScore = highScoreClassic;
}
var highScoreText = new Text2('High: ' + highScore, {
size: 70,
fill: 0xFFD700
});
highScoreText.anchor.set(1, 0);
// Place high score in top right
LK.gui.topRight.addChild(highScoreText);
// Place level text directly under high score text in top right
LK.gui.topRight.addChild(levelText);
// Position GUI
// Position GUI
scoreText.x = 2048 / 2;
timerText.x = 2048 * 3 / 4;
highScoreText.x = 0;
highScoreText.y = 0;
levelText.x = 0;
levelText.y = highScoreText.height + 10; // 10px gap below high score
// Responsive UI: adjust font size and icon size for mobile
function resizeUI() {
var w = LK.width || 2048;
var h = LK.height || 2732;
var scale = Math.min(w / 2048, h / 2732);
if (scoreText && scoreText.style) scoreText.style.size = 90 * scale;
if (timerText && timerText.style) timerText.style.size = 90 * scale;
if (highScoreText && highScoreText.style) highScoreText.style.size = 70 * scale;
if (levelText && levelText.style) levelText.style.size = 90 * scale;
for (var i = 0; i < bulletIcons.length; i++) {
bulletIcons[i].scaleX = bulletIcons[i].scaleY = scale;
}
}
LK.on('resize', resizeUI);
resizeUI();
// Bullets GUI (now under the level text in the top right)
// Show both bullet icons (bottom) and a text counter (top right under level)
var bulletCountText = null;
var bulletLabelText = null;
function updateBulletIcons() {
// Remove old icons
for (var i = 0; i < bulletIcons.length; i++) {
if (bulletIcons[i].parent) bulletIcons[i].parent.removeChild(bulletIcons[i]);
}
bulletIcons = [];
// Draw new icons (up to 10 for visual clarity, then show text)
var maxIcons = Math.min(10, bulletsLeft);
// Center the icons under the screen
var iconSpacing = 60;
var totalWidth = (maxIcons - 1) * iconSpacing;
var startX = 2048 / 2 - totalWidth / 2;
for (var i = 0; i < maxIcons; i++) {
var icon = LK.getAsset('bullet_icon', {
anchorX: 0.5,
anchorY: 0.5,
x: startX + i * iconSpacing,
y: 2732 - 120
});
LK.gui.bottom.addChild(icon);
bulletIcons.push(icon);
}
// Remove old text if present
if (bulletCountText && bulletCountText.parent) {
bulletCountText.parent.removeChild(bulletCountText);
}
if (bulletLabelText && bulletLabelText.parent) {
bulletLabelText.parent.removeChild(bulletLabelText);
}
// Add "Bullets" label above the count, now in top right under level
bulletLabelText = new Text2("Bullets", {
size: 60,
fill: 0xFFF000,
font: "'PressStart2P','GillSans-Bold',Impact,'Arial Black',Tahoma"
});
bulletLabelText.anchor.set(1, 0); // right align, top anchor
bulletLabelText.x = 0;
bulletLabelText.y = levelText.y + levelText.height + 10;
LK.gui.topRight.addChild(bulletLabelText);
// Show bullet count as text (always, for clarity)
bulletCountText = new Text2("" + bulletsLeft, {
size: 90,
fill: 0xFFF000,
font: "'PressStart2P','GillSans-Bold',Impact,'Arial Black',Tahoma"
});
bulletCountText.anchor.set(1, 0); // right align, top anchor
bulletCountText.x = 0;
bulletCountText.y = bulletLabelText.y + bulletLabelText.height + 2;
LK.gui.topRight.addChild(bulletCountText);
}
// Start a new level
function startLevel() {
ducks = [];
bullets = [];
ducksHit = 0;
ducksEscaped = 0;
missedShots = 0;
missedWarningShown = false;
endlessMissStreak = 0;
endlessGameOver = false;
// --- Mode-specific setup ---
if (gameMode === GAME_MODE_CLASSIC || !gameMode) {
// Set ducksPerLevel and ducksToHit per level as per requirements
if (level === 1) {
ducksPerLevel = 6;
ducksToHit = 3;
} else if (level === 2) {
ducksPerLevel = 12;
ducksToHit = 6;
} else if (level === 3) {
ducksPerLevel = 20;
ducksToHit = 12;
} else if (level === 4) {
ducksPerLevel = 30;
ducksToHit = 18;
} else if (level === 5) {
ducksPerLevel = 40;
ducksToHit = 20;
} else {
ducksPerLevel = Math.min(25, 15 + (level - 5) * 2);
ducksToHit = Math.ceil(ducksPerLevel * 0.7);
}
// Set bulletsLeft per level: 10 for level 1, +10 bullets for every other level
if (level === 1) {
bulletsLeft = 10;
} else if (level === 2) {
bulletsLeft = 15 + 10;
} else if (level === 3) {
bulletsLeft = 20 + 10;
} else if (level === 4) {
bulletsLeft = 30 + 10;
} else if (level === 5) {
bulletsLeft = 32 + 10;
} else {
bulletsLeft = 32 + 2 * (level - 5) + 10;
}
maxBullets = bulletsLeft;
timeLimit = Math.max(8, 15 - (level - 1));
timeLeft = timeLimit;
levelText.setText('Level ' + level);
} else if (gameMode === GAME_MODE_TIMEATTACK) {
// 30 seconds, infinite ducks, infinite bullets, score = ducks hit
ducksPerLevel = 99999;
ducksToHit = 0;
bulletsLeft = 99999;
maxBullets = bulletsLeft;
timeLimit = 30;
timeLeft = 30;
levelText.setText('Time Attack');
} else if (gameMode === GAME_MODE_ENDLESS) {
// Infinite ducks, infinite bullets, game ends on 3 misses in a row
ducksPerLevel = 99999;
ducksToHit = 0;
bulletsLeft = 99999;
maxBullets = bulletsLeft;
timeLimit = 99999;
timeLeft = 99999;
levelText.setText('Endless');
}
// Set highScore for current mode
if (gameMode === GAME_MODE_TIMEATTACK) {
highScore = highScoreTimeAttack;
} else if (gameMode === GAME_MODE_ENDLESS) {
highScore = highScoreEndless;
} else {
highScore = highScoreClassic;
}
highScoreText.setText('High: ' + highScore);
gameActive = true;
waveActive = true;
updateBulletIcons();
scoreText.setText('Score: ' + score);
timerText.setText('Time: ' + timeLeft);
// Duck spawn schedule and speed per level/mode
var duckSpawnTimer = null;
var ducksSpawned = 0;
var ducksAtOnce = 1;
var duckSpawnInterval = 3000;
var duckSpeedMin = 2;
var duckSpeedMax = 3.5;
if (gameMode === GAME_MODE_CLASSIC || !gameMode) {
if (level === 1) {
ducksAtOnce = 1;
duckSpawnInterval = 3000;
duckSpeedMin = 2;
duckSpeedMax = 3.5;
} else if (level === 2) {
ducksAtOnce = 1;
duckSpawnInterval = 2000;
duckSpeedMin = 3.5;
duckSpeedMax = 5;
} else if (level === 3) {
ducksAtOnce = 2;
duckSpawnInterval = 1800;
duckSpeedMin = 4.2;
duckSpeedMax = 5.8;
} else if (level === 4) {
ducksAtOnce = 3;
duckSpawnInterval = 1500;
duckSpeedMin = 5.5;
duckSpeedMax = 7.2;
} else if (level === 5) {
ducksAtOnce = 4;
duckSpawnInterval = 1200;
duckSpeedMin = 6.5;
duckSpeedMax = 8.5;
} else {
ducksAtOnce = 5;
duckSpawnInterval = 1000;
duckSpeedMin = 7.5 + (level - 5) * 0.5;
duckSpeedMax = 9.5 + (level - 5) * 0.7;
}
} else if (gameMode === GAME_MODE_TIMEATTACK) {
ducksAtOnce = 2;
duckSpawnInterval = 900;
duckSpeedMin = 4.5;
duckSpeedMax = 7.5;
} else if (gameMode === GAME_MODE_ENDLESS) {
ducksAtOnce = 2;
duckSpawnInterval = 900;
duckSpeedMin = 4.5;
duckSpeedMax = 7.5;
}
// Clear any previous duck spawn timer
if (typeof duckSpawnTimer !== "undefined" && duckSpawnTimer) {
LK.clearInterval(duckSpawnTimer);
duckSpawnTimer = null;
}
ducksSpawned = 0;
// Function to spawn ducks and power-ups
function spawnDucks() {
if (!gameActive || !waveActive) return;
if ((gameMode === GAME_MODE_CLASSIC || !gameMode) && ducksSpawned >= ducksPerLevel) {
if (duckSpawnTimer) {
LK.clearInterval(duckSpawnTimer);
duckSpawnTimer = null;
}
return;
}
var ducksThisWave = gameMode === GAME_MODE_CLASSIC || !gameMode ? Math.min(ducksAtOnce, ducksPerLevel - ducksSpawned) : ducksAtOnce;
for (var d = 0; d < ducksThisWave; d++) {
var duck = new Duck();
// Randomize start edge: 0=left, 1=right, 2=bottom
var edge = Math.floor(Math.random() * 3);
var startX, startY, angle;
if (edge === 0) {
// left
startX = -60;
startY = 400 + Math.random() * (2732 - 800);
angle = Math.random() * Math.PI / 3 - Math.PI / 6; // -30 to +30 deg
} else if (edge === 1) {
// right
startX = 2048 + 60;
startY = 400 + Math.random() * (2732 - 800);
angle = Math.PI + (Math.random() * Math.PI / 3 - Math.PI / 6); // 150 to 210 deg
} else {
// bottom
startX = 200 + Math.random() * (2048 - 400);
startY = 2732 + 60;
angle = -Math.PI / 2 + (Math.random() * Math.PI / 4 - Math.PI / 8); // -67 to -23 deg
}
duck.x = startX;
duck.y = startY;
duck.angle = angle;
// Set duck speed and size based on type
if (duck.type === 'golden') {
duck.speed = duckSpeedMax + 2 + Math.random() * 1.5;
} else if (duck.type === 'armored') {
duck.speed = duckSpeedMin + Math.random() * (duckSpeedMax - duckSpeedMin) * 0.7;
} else if (duck.type === 'mini') {
duck.speed = duckSpeedMax + 1 + Math.random() * 1.2;
duck.scaleX = duck.scaleY = 0.6 + Math.random() * 0.2;
} else if (duck.type === 'fast') {
duck.speed = duckSpeedMax + 0.7 + Math.random() * 0.7;
} else {
duck.speed = duckSpeedMin + Math.random() * (duckSpeedMax - duckSpeedMin);
}
ducks.push(duck);
// Always add ducks above background and below crosshair/dog/UI
game.addChild(duck);
// Move dog and crosshair to top of display list if present
if (dog && dog.parent) {
dog.parent.removeChild(dog);
game.addChild(dog);
}
if (crosshair && crosshair.parent) {
crosshair.parent.removeChild(crosshair);
game.addChild(crosshair);
}
LK.getSound('duck_fly').play();
ducksSpawned++;
}
// --- Power-up spawn logic ---
// Power-ups can appear randomly (1 in 7 chance per spawn, but not if already active)
if (Math.random() < 1 / 7) {
var availablePowerups = [];
if (!powerupActive.doubleBarrel) availablePowerups.push('doubleBarrel');
if (!powerupActive.slowTime) availablePowerups.push('slowTime');
if (!powerupActive.autoAim) availablePowerups.push('autoAim');
if (availablePowerups.length > 0) {
var which = availablePowerups[Math.floor(Math.random() * availablePowerups.length)];
spawnPowerupCollectible(which);
}
}
}
spawnDucks(); // Spawn first batch immediately
if ((gameMode === GAME_MODE_CLASSIC || !gameMode) && ducksPerLevel > ducksAtOnce) {
duckSpawnTimer = LK.setInterval(spawnDucks, duckSpawnInterval);
} else if (gameMode === GAME_MODE_TIMEATTACK || gameMode === GAME_MODE_ENDLESS) {
duckSpawnTimer = LK.setInterval(spawnDucks, duckSpawnInterval);
}
// Start timer
if (timerInterval) LK.clearInterval(timerInterval);
timerInterval = LK.setInterval(function () {
if (!gameActive) return;
if (gameMode === GAME_MODE_TIMEATTACK) {
timeLeft--;
timerText.setText('Time: ' + timeLeft);
if (timeLeft <= 0) {
endWave();
}
} else if (gameMode === GAME_MODE_CLASSIC || !gameMode) {
timeLeft--;
timerText.setText('Time: ' + timeLeft);
if (timeLeft <= 0) {
endWave();
}
} else if (gameMode === GAME_MODE_ENDLESS) {
// No timer, but show "∞" or running time
timerText.setText('Endless');
}
}, 1000);
}
// End wave: check results, show dog if needed, advance or game over
function endWave() {
if (!waveActive) return;
waveActive = false;
gameActive = false;
if (timerInterval) LK.clearInterval(timerInterval);
// Remove remaining ducks
for (var i = 0; i < ducks.length; i++) {
if (ducks[i].parent) ducks[i].parent.removeChild(ducks[i]);
}
ducks = [];
// --- Classic Mode ---
if (gameMode === GAME_MODE_CLASSIC || !gameMode) {
// Show dog if not enough ducks hit
if (ducksHit < ducksToHit) {
dog.laugh(2048 / 2, 2732 - 100);
LK.effects.flashScreen(0xff0000, 800);
// Show final score on Game Over
LK.setTimeout(function () {
var finalScoreText = new Text2('Final Score: ' + score, {
size: 120,
fill: 0xFFFF00
});
finalScoreText.anchor.set(0.5, 0.5);
finalScoreText.x = 2048 / 2;
finalScoreText.y = 2732 / 2;
game.addChild(finalScoreText);
LK.setTimeout(function () {
if (finalScoreText.parent) finalScoreText.parent.removeChild(finalScoreText);
LK.showGameOver();
}, 900);
}, 400);
// Game over after laugh
waveResultTimeout = LK.setTimeout(function () {
// handled above
}, 1300);
} else {
// Advance to next level after short pause
LK.effects.flashObject(levelText, 0x00ff00, 700);
LK.setTimeout(function () {
var finalScoreText = new Text2('Score: ' + score, {
size: 120,
fill: 0x00FF00
});
finalScoreText.anchor.set(0.5, 0.5);
finalScoreText.x = 2048 / 2;
finalScoreText.y = 2732 / 2;
game.addChild(finalScoreText);
LK.setTimeout(function () {
if (finalScoreText.parent) finalScoreText.parent.removeChild(finalScoreText);
level++;
startLevel();
}, 700);
}, 200);
}
}
// --- Time Attack Mode ---
else if (gameMode === GAME_MODE_TIMEATTACK) {
// Show final score and return to menu
LK.effects.flashScreen(0x00ffcc, 800);
LK.setTimeout(function () {
var finalScoreText = new Text2('Ducks Hit: ' + ducksHit, {
size: 120,
fill: 0x00FFCC
});
finalScoreText.anchor.set(0.5, 0.5);
finalScoreText.x = 2048 / 2;
finalScoreText.y = 2732 / 2;
game.addChild(finalScoreText);
LK.setTimeout(function () {
if (finalScoreText.parent) finalScoreText.parent.removeChild(finalScoreText);
showMainMenu();
}, 1400);
}, 400);
}
// --- Endless Mode ---
else if (gameMode === GAME_MODE_ENDLESS) {
endlessGameOver = true;
LK.effects.flashScreen(0xff2222, 800);
LK.setTimeout(function () {
var finalScoreText = new Text2('Ducks Hit: ' + ducksHit, {
size: 120,
fill: 0xFF2222
});
finalScoreText.anchor.set(0.5, 0.5);
finalScoreText.x = 2048 / 2;
finalScoreText.y = 2732 / 2;
game.addChild(finalScoreText);
LK.setTimeout(function () {
if (finalScoreText.parent) finalScoreText.parent.removeChild(finalScoreText);
showMainMenu();
}, 1400);
}, 400);
}
}
// Handle tap/click to shoot
game.down = function (x, y, obj) {
if (!gameActive || !waveActive) return;
// --- Power-up collectible check ---
for (var i = powerupCollectibles.length - 1; i >= 0; i--) {
var p = powerupCollectibles[i];
if (p.isHit(x, y)) {
activatePowerup(p.type);
if (p.parent) p.parent.removeChild(p);
powerupCollectibles.splice(i, 1);
LK.effects.flashObject(p, 0xffffff, 300);
LK.getSound('hit').play();
return;
}
}
// --- Bullets check ---
if ((gameMode === GAME_MODE_CLASSIC || !gameMode) && bulletsLeft <= 0) {
LK.getSound('miss').play();
LK.effects.flashScreen(0x888888, 200);
// Show reload animation (flash bullet icons red)
for (var i = 0; i < bulletIcons.length; i++) {
LK.effects.flashObject(bulletIcons[i], 0xff2222, 300);
}
// Optionally play reload sound if available
// LK.getSound('reload').play();
return;
}
// Only decrement bullets and allow shooting if bulletsLeft > 0 (classic)
if (gameMode === GAME_MODE_CLASSIC || !gameMode) {
bulletsLeft--;
updateBulletIcons();
if (bulletCountText && bulletCountText.setText) {
bulletCountText.setText("Bullets: " + bulletsLeft);
}
// If bullets reach zero after this shot, trigger game over immediately
if (bulletsLeft === 0) {
LK.setTimeout(function () {
LK.showGameOver();
}, 400); // short delay to allow last shot/crosshair to animate
return;
}
}
LK.getSound('shoot').play();
// Show crosshair at tap
crosshair.x = x;
crosshair.y = y;
crosshair.visible = true;
tween(crosshair, {
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
crosshair.visible = false;
crosshair.alpha = 1;
}
});
// --- Power-up shooting logic ---
// Auto-Aim: instantly hit nearest duck
if (powerupActive.autoAim) {
var nearestDuck = null;
var minDist = 99999;
for (var i = 0; i < ducks.length; i++) {
var duck = ducks[i];
if (!duck.alive || duck.hit) continue;
var dx = duck.x - x;
var dy = duck.y - y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < minDist) {
minDist = dist;
nearestDuck = duck;
}
}
if (nearestDuck) {
// Hit the duck
nearestDuck.hit = true;
nearestDuck.fall(function () {
if (nearestDuck.parent) nearestDuck.parent.removeChild(nearestDuck);
if (dog) {
dog.show(nearestDuck.x, 2732 - 100);
LK.getSound('dog_laugh').play();
LK.setTimeout(function () {
dog.hide();
}, 2000);
}
});
ducksHit++;
var duckScore = 10;
var achievementMsg = '';
var achievementColor = 0xFFD700;
if (typeof nearestDuck.type !== "undefined" && nearestDuck.type === 'golden') {
duckScore = 50;
achievementMsg = 'Golden Duck! +50';
achievementColor = 0xFFD700;
LK.effects.flashObject(nearestDuck, 0xffe066, 400);
} else {
LK.effects.flashObject(nearestDuck, 0xffff00, 300);
}
// Headshot bonus (auto-aim never headshots)
score += duckScore;
LK.getSound('hit').play();
if (typeof achievementPopup !== "undefined" && achievementMsg) {
achievementPopup.show(achievementMsg, achievementColor);
}
if (ducksHit === 3 && typeof achievementPopup !== "undefined") {
achievementPopup.show('Triple Hit!', 0x00FFCC);
}
if (ducksHit === ducksToHit && typeof achievementPopup !== "undefined") {
achievementPopup.show('Sharp Shooter!', 0x00FF00);
}
if (score > highScore) {
highScore = score;
storage.highScore = highScore;
if (typeof highScoreText !== "undefined") {
highScoreText.setText('High: ' + highScore);
}
}
scoreText.setText('Score: ' + score);
if (score > highScore) {
highScore = score;
storage.highScore = highScore;
if (typeof highScoreText !== "undefined") {
highScoreText.setText('High: ' + highScore);
}
}
if (gameMode === GAME_MODE_ENDLESS) {
endlessMissStreak = 0;
}
// End wave check
var aliveDucks = 0;
for (var i = 0; i < ducks.length; i++) {
if (ducks[i].alive) aliveDucks++;
}
if ((gameMode === GAME_MODE_CLASSIC || !gameMode) && (aliveDucks === 0 && bulletsLeft === 0 || aliveDucks === 0 && typeof duckSpawnTimer !== "undefined" && (!duckSpawnTimer || ducks.length === 0))) {
endWave();
}
return;
}
}
// Double Barrel: shoot a wide spread, hit up to 2 ducks if close
if (powerupActive.doubleBarrel) {
var hitDucks = [];
for (var i = 0; i < ducks.length; i++) {
var duck = ducks[i];
if (!duck.alive || duck.hit) continue;
var dx = duck.x - x;
var dy = duck.y - y;
var dist = Math.sqrt(dx * dx + dy * dy);
var hitRadius = typeof duck.getHitboxRadius === "function" ? duck.getHitboxRadius() * 1.5 : 120;
if (dist < hitRadius) {
hitDucks.push({
duck: duck,
dist: dist
});
}
}
// Sort by distance, hit up to 2 closest
hitDucks.sort(function (a, b) {
return a.dist - b.dist;
});
hitDucks = hitDucks.slice(0, 2);
if (hitDucks.length > 0) {
for (var h = 0; h < hitDucks.length; h++) {
var hitDuck = hitDucks[h].duck;
hitDuck.hit = true;
hitDuck.fall(function () {
if (hitDuck.parent) hitDuck.parent.removeChild(hitDuck);
if (dog) {
dog.show(hitDuck.x, 2732 - 100);
LK.getSound('dog_laugh').play();
LK.setTimeout(function () {
dog.hide();
}, 2000);
}
});
ducksHit++;
var duckScore = 10;
var achievementMsg = '';
var achievementColor = 0xFFD700;
if (typeof hitDuck.type !== "undefined" && hitDuck.type === 'golden') {
duckScore = 50;
achievementMsg = 'Golden Duck! +50';
achievementColor = 0xFFD700;
LK.effects.flashObject(hitDuck, 0xffe066, 400);
} else {
LK.effects.flashObject(hitDuck, 0xffff00, 300);
}
// Headshot bonus
var isHeadshot = typeof hitDuck.isHeadshot === "function" && hitDuck.isHeadshot(x, y);
if (isHeadshot) {
duckScore += 5;
achievementMsg = 'Headshot! +' + duckScore;
achievementColor = 0xFF2222;
LK.effects.flashObject(hitDuck, 0xff2222, 400);
}
score += duckScore;
LK.getSound('hit').play();
if (typeof achievementPopup !== "undefined" && achievementMsg) {
achievementPopup.show(achievementMsg, achievementColor);
}
if (ducksHit === 3 && typeof achievementPopup !== "undefined") {
achievementPopup.show('Triple Hit!', 0x00FFCC);
}
if (ducksHit === ducksToHit && typeof achievementPopup !== "undefined") {
achievementPopup.show('Sharp Shooter!', 0x00FF00);
}
if (score > highScore) {
highScore = score;
storage.highScore = highScore;
if (typeof highScoreText !== "undefined") {
highScoreText.setText('High: ' + highScore);
}
}
scoreText.setText('Score: ' + score);
if (score > highScore) {
highScore = score;
storage.highScore = highScore;
if (typeof highScoreText !== "undefined") {
highScoreText.setText('High: ' + highScore);
}
}
if (gameMode === GAME_MODE_ENDLESS) {
endlessMissStreak = 0;
}
}
// End wave check
var aliveDucks = 0;
for (var i = 0; i < ducks.length; i++) {
if (ducks[i].alive) aliveDucks++;
}
if ((gameMode === GAME_MODE_CLASSIC || !gameMode) && (aliveDucks === 0 && bulletsLeft === 0 || aliveDucks === 0 && typeof duckSpawnTimer !== "undefined" && (!duckSpawnTimer || ducks.length === 0))) {
endWave();
}
return;
}
}
// --- Normal shooting logic ---
// Check if a duck is hit (closest duck under tap, not already hit)
var hitDuck = null;
var minDist = 99999;
for (var i = 0; i < ducks.length; i++) {
var duck = ducks[i];
// Only allow hitting ducks that are alive and not hit yet
if (!duck.alive || duck.hit) continue;
var dx = duck.x - x;
var dy = duck.y - y;
var hitRadius = typeof duck.getHitboxRadius === "function" ? duck.getHitboxRadius() : 80;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < hitRadius && dist < minDist) {
minDist = dist;
hitDuck = duck;
}
}
if (hitDuck) {
// Armored duck: needs 2 hits
if (hitDuck.type === 'armored' && hitDuck.armor > 1) {
hitDuck.armor--;
hitDuck.flashRed();
LK.effects.flashObject(hitDuck, 0xff2222, 200);
LK.getSound('hit').play();
// No score yet, must hit again
return;
}
// Mark duck as hit and start fall animation
hitDuck.hit = true;
hitDuck.fall(function () {
if (hitDuck.parent) hitDuck.parent.removeChild(hitDuck);
if (dog) {
dog.show(hitDuck.x, 2732 - 100);
LK.getSound('dog_laugh').play();
LK.setTimeout(function () {
dog.hide();
}, 2000);
}
});
ducksHit++;
// Score system: use DUCK_TYPE_POINTS
if (typeof score === "undefined") score = 0;
var duckScore = DUCK_TYPE_POINTS[hitDuck.type] || 10;
var achievementMsg = '';
var achievementColor = 0xFFD700;
if (hitDuck.type === 'golden') {
achievementMsg = 'Golden Duck! +50';
achievementColor = 0xFFD700;
LK.effects.flashObject(hitDuck, 0xffe066, 400);
} else if (hitDuck.type === 'armored') {
achievementMsg = 'Armored Duck! +20';
achievementColor = 0x888888;
LK.effects.flashObject(hitDuck, 0xcccccc, 400);
} else if (hitDuck.type === 'mini') {
achievementMsg = 'Mini Fast Duck! +25';
achievementColor = 0x00e6ff;
LK.effects.flashObject(hitDuck, 0x00e6ff, 350);
} else {
LK.effects.flashObject(hitDuck, 0xffff00, 300);
}
// Headshot bonus
var isHeadshot = typeof hitDuck.isHeadshot === "function" && hitDuck.isHeadshot(x, y);
if (isHeadshot) {
duckScore += 5;
achievementMsg = 'Headshot! +' + duckScore;
achievementColor = 0xFF2222;
LK.effects.flashObject(hitDuck, 0xff2222, 400);
}
score += duckScore;
LK.getSound('hit').play();
// Show achievement popup if any
if (typeof achievementPopup !== "undefined" && achievementMsg) {
achievementPopup.show(achievementMsg, achievementColor);
}
// Triple hit achievement (3 ducks in 1 level)
if (ducksHit === 3 && typeof achievementPopup !== "undefined") {
achievementPopup.show('Triple Hit!', 0x00FFCC);
}
// Sharp shooter (all ducks hit)
if (ducksHit === ducksToHit && typeof achievementPopup !== "undefined") {
achievementPopup.show('Sharp Shooter!', 0x00FF00);
}
// Update high score if needed
if (score > highScore) {
highScore = score;
if (gameMode === GAME_MODE_TIMEATTACK) {
highScoreTimeAttack = highScore;
storage.highScoreTimeAttack = highScore;
} else if (gameMode === GAME_MODE_ENDLESS) {
highScoreEndless = highScore;
storage.highScoreEndless = highScore;
} else {
highScoreClassic = highScore;
storage.highScoreClassic = highScore;
}
if (typeof highScoreText !== "undefined") {
highScoreText.setText('High: ' + highScore);
}
}
scoreText.setText('Score: ' + score);
if (score > highScore) {
highScore = score;
storage.highScore = highScore;
if (typeof highScoreText !== "undefined") {
highScoreText.setText('High: ' + highScore);
}
}
// Endless mode: reset miss streak
if (gameMode === GAME_MODE_ENDLESS) {
endlessMissStreak = 0;
}
} else {
// Miss: do nothing to ducks, just play miss sound and flash
LK.getSound('miss').play();
LK.effects.flashScreen(0x888888, 120);
// Track missed shots and show warning after 20 misses
missedShots++;
if (missedShots >= 20 && !missedWarningShown) {
missedWarningShown = true;
if (typeof achievementPopup !== "undefined") {
achievementPopup.show("Careful! 20 missed shots!", 0xFF2222);
}
}
// Endless mode: increment miss streak, end game if 3 in a row
if (gameMode === GAME_MODE_ENDLESS) {
endlessMissStreak++;
if (endlessMissStreak >= 3) {
endWave();
return;
}
}
}
// If all ducks are gone or out of bullets, end wave (classic only)
var aliveDucks = 0;
for (var i = 0; i < ducks.length; i++) {
if (ducks[i].alive) aliveDucks++;
}
// Only end wave if all ducks are gone AND no more ducks will spawn, or if out of bullets
if ((gameMode === GAME_MODE_CLASSIC || !gameMode) && (aliveDucks === 0 && bulletsLeft === 0 || aliveDucks === 0 && typeof duckSpawnTimer !== "undefined" && (!duckSpawnTimer || ducks.length === 0))) {
endWave();
}
};
// Move handler for crosshair (optional, for desktop)
game.move = function (x, y, obj) {
// Optionally show crosshair following finger/mouse
// crosshair.x = x;
// crosshair.y = y;
};
// Main update loop
game.update = function () {
if (!gameActive || !waveActive) return;
// Update ducks
for (var i = ducks.length - 1; i >= 0; i--) {
var duck = ducks[i];
duck.update();
if (duck.escaped && !duck.hit) {
ducksEscaped++;
if (duck.parent) duck.parent.removeChild(duck);
ducks.splice(i, 1);
// Endless mode: treat escaped duck as a miss
if (gameMode === GAME_MODE_ENDLESS) {
endlessMissStreak++;
if (endlessMissStreak >= 3) {
endWave();
return;
}
}
}
}
// --- Power-up collectibles update ---
for (var i = powerupCollectibles.length - 1; i >= 0; i--) {
powerupCollectibles[i].update();
}
// --- Power-up timers/icons update ---
for (var ptype in powerupActive) {
if (powerupActive[ptype] && powerupTimers[ptype] > 0) {
// Decrement timer (per second, but update is 60fps)
if (LK.ticks % 60 === 0) {
powerupTimers[ptype]--;
if (powerupIcons[ptype] && powerupIcons[ptype]._timerTxt) {
powerupIcons[ptype]._timerTxt.setText(powerupTimers[ptype] + "s");
}
}
}
}
// If all ducks gone, end wave (classic only)
var aliveDucks = 0;
for (var i = 0; i < ducks.length; i++) {
if (ducks[i].alive) aliveDucks++;
}
if ((gameMode === GAME_MODE_CLASSIC || !gameMode) && aliveDucks === 0 && typeof duckSpawnTimer !== "undefined" && (!duckSpawnTimer || ducks.length === 0)) {
endWave();
}
};
// On game over, reset everything
LK.on('gameover', function () {
if (timerInterval) LK.clearInterval(timerInterval);
if (waveResultTimeout) LK.clearTimeout(waveResultTimeout);
if (typeof duckSpawnTimer !== "undefined" && duckSpawnTimer) {
LK.clearInterval(duckSpawnTimer);
duckSpawnTimer = null;
}
gameActive = false;
waveActive = false;
level = 1;
ducks = [];
bullets = [];
ducksHit = 0;
ducksEscaped = 0;
// Reset bullets to 10 on gameover for level 1
bulletsLeft = 10;
score = 0;
updateBulletIcons();
levelText.setText('Level 1');
scoreText.setText('Score: ' + score);
// Reset highScore to current mode's high score on gameover
if (gameMode === GAME_MODE_TIMEATTACK) {
highScore = highScoreTimeAttack;
} else if (gameMode === GAME_MODE_ENDLESS) {
highScore = highScoreEndless;
} else {
highScore = highScoreClassic;
}
if (typeof highScoreText !== "undefined") {
highScoreText.setText('High: ' + highScore);
}
timerText.setText('Time: 15');
if (dog) dog.hide();
// Show main menu after short delay
LK.setTimeout(function () {
showMainMenu();
}, 1200);
});
// On you win (if you want to add a win condition, e.g. after level 10)
LK.on('youwin', function () {
if (timerInterval) LK.clearInterval(timerInterval);
if (waveResultTimeout) LK.clearTimeout(waveResultTimeout);
if (typeof duckSpawnTimer !== "undefined" && duckSpawnTimer) {
LK.clearInterval(duckSpawnTimer);
duckSpawnTimer = null;
}
gameActive = false;
waveActive = false;
// Show win, then reset
LK.setTimeout(function () {
level = 1;
ducks = [];
bullets = [];
ducksHit = 0;
ducksEscaped = 0;
bulletsLeft = 10;
score = 0;
updateBulletIcons();
levelText.setText('Level 1');
scoreText.setText('Score: ' + score);
// Reset highScore to current mode's high score on youwin
if (gameMode === GAME_MODE_TIMEATTACK) {
highScore = highScoreTimeAttack;
} else if (gameMode === GAME_MODE_ENDLESS) {
highScore = highScoreEndless;
} else {
highScore = highScoreClassic;
}
timerText.setText('Time: 15');
if (dog) dog.hide();
showMainMenu();
}, 1800);
});
// Start music
LK.playMusic('bgm', {
fade: {
start: 0,
end: 0.5,
duration: 1200
}
});
// Start first level
// (Removed: now handled by showMainMenu) /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
highScore: 0
});
/****
* Classes
****/
// Achievement popup class
var AchievementPopup = Container.expand(function () {
var self = Container.call(this);
var txt = new Text2('', {
size: 120,
fill: 0xFFD700
});
txt.anchor.set(0.5, 0.5);
self.addChild(txt);
self.visible = false;
self.show = function (message, color) {
txt.setText(message);
if (txt && txt.style) {
txt.style.fill = color || 0xFFD700;
}
self.x = 2048 / 2;
self.y = 2732 / 2 - 300;
self.visible = true;
self.alpha = 1;
tween(self, {
alpha: 0
}, {
duration: 1200,
onFinish: function onFinish() {
self.visible = false;
}
});
};
return self;
});
// Dog class
var Dog = Container.expand(function () {
var self = Container.call(this);
self.dogAsset = null;
self.laughAsset = null;
self.currentDogId = null;
// Helper to set dog asset based on selectedDogId
self.setDogAssets = function () {
// Remove old assets if any
if (self.dogAsset && self.dogAsset.parent) self.removeChild(self.dogAsset);
if (self.laughAsset && self.laughAsset.parent) self.removeChild(self.laughAsset);
self.dogAsset = null;
self.laughAsset = null;
var dogOpt = null;
for (var i = 0; i < DOG_OPTIONS.length; i++) {
if (DOG_OPTIONS[i].id === selectedDogId) {
dogOpt = DOG_OPTIONS[i];
break;
}
}
if (!dogOpt) dogOpt = DOG_OPTIONS[0];
self.currentDogId = dogOpt.id;
self.dogAsset = self.attachAsset(dogOpt.asset, {
anchorX: 0.5,
anchorY: 1
});
self.dogAsset.visible = false;
// Laugh asset will be created on demand in laugh()
};
self.setDogAssets();
self.visible = false;
// Show dog at (x, y)
self.show = function (x, y) {
// If dog changed, update assets
if (self.currentDogId !== selectedDogId) {
self.setDogAssets();
}
self.x = x;
self.y = y;
self.visible = true;
if (self.dogAsset) self.dogAsset.visible = true;
if (self.laughAsset) self.laughAsset.visible = false;
// Optionally, you could add a little "pop up" animation here
// For example, tween the dog from below the screen to y
var startY = 2732 + (self.dogAsset ? self.dogAsset.height : 180);
self.y = startY;
tween(self, {
y: y
}, {
duration: 350,
easing: tween.cubicOut
});
};
// Hide dog
self.hide = function () {
var endY = 2732 + (self.dogAsset ? self.dogAsset.height : 180);
tween(self, {
y: endY
}, {
duration: 350,
easing: tween.cubicIn,
onFinish: function onFinish() {
self.visible = false;
if (self.dogAsset) self.dogAsset.visible = false;
}
});
};
// Show laugh animation
self.laugh = function (x, y) {
// If dog changed, update assets
if (self.currentDogId !== selectedDogId) {
self.setDogAssets();
}
self.x = x;
self.y = y;
self.visible = true;
if (self.dogAsset) self.dogAsset.visible = false;
// Find laugh asset id for this dog
var dogOpt = null;
for (var i = 0; i < DOG_OPTIONS.length; i++) {
if (DOG_OPTIONS[i].id === selectedDogId) {
dogOpt = DOG_OPTIONS[i];
break;
}
}
if (!dogOpt) dogOpt = DOG_OPTIONS[0];
var laughId = dogOpt.laugh || "dog_laugh";
if (!self.laughAsset || self.laughAsset._assetId !== laughId) {
if (self.laughAsset && self.laughAsset.parent) self.removeChild(self.laughAsset);
self.laughAsset = self.attachAsset(laughId, {
anchorX: 0.5,
anchorY: 1
});
self.laughAsset._assetId = laughId;
}
self.laughAsset.visible = true;
// Play laugh sound (use default for all)
LK.getSound('dog_laugh').play();
// Hide after 1.2s
LK.setTimeout(function () {
self.laughAsset.visible = false;
self.hide();
}, 1200);
};
return self;
});
// Duck class
var Duck = Container.expand(function () {
var self = Container.call(this);
// Determine duck type based on level and DUCK_TYPE_CONFIG
var duckType = 'normal';
var levelIdx = Math.max(0, Math.min(level - 1, DUCK_TYPE_CONFIG.length - 1));
var config = DUCK_TYPE_CONFIG[levelIdx];
var r = Math.random();
if (r < config.golden) {
duckType = 'golden';
} else if (r < config.golden + config.armored) {
duckType = 'armored';
} else if (r < config.golden + config.armored + config.mini) {
duckType = 'mini';
} else if (Math.random() < 0.25) {
duckType = 'fast';
}
self.type = duckType;
// Asset selection
var duckAsset = null;
if (duckType === 'golden') {
duckAsset = self.attachAsset('duck_golden', {
anchorX: 0.5,
anchorY: 0.5
});
duckAsset.tint = 0xFFD700;
} else if (duckType === 'armored') {
duckAsset = self.attachAsset('duck_armored', {
anchorX: 0.5,
anchorY: 0.5
});
duckAsset.tint = 0x888888;
} else if (duckType === 'mini') {
duckAsset = self.attachAsset('duck_mini', {
anchorX: 0.5,
anchorY: 0.5
});
} else {
// normal or fast
var duckColors = ['duck_green', 'duck_blue', 'duck_red'];
var colorIdx = Math.floor(Math.random() * duckColors.length);
duckAsset = self.attachAsset(duckColors[colorIdx], {
anchorX: 0.5,
anchorY: 0.5
});
}
// Set initial position and movement
self.speed = 6 + Math.random() * 4; // Will be set by level
self.angle = 0; // radians, will be set by level
self.alive = true;
self.hit = false;
self.escaped = false;
self.armor = duckType === 'armored' ? 2 : 1; // 2 hits for armored
self.flashTween = null;
// For animation
self.flyTween = null;
// --- Hitbox properties for all ducks (mini ducks are smaller, fast/golden slightly larger) ---
self.getHitboxRadius = function () {
var baseRadius = Math.max(duckAsset.width, duckAsset.height) * 0.5;
if (self.type === 'mini') {
return baseRadius * 0.55;
}
if (self.type === 'fast' || self.type === 'golden' || self.speed >= 6) {
return baseRadius * 0.7;
}
return baseRadius * 0.55;
};
// Head hitbox (top 1/3 of duck)
self.isHeadshot = function (x, y) {
var dx = self.x - x;
var dy = self.y - y;
var dist = Math.sqrt(dx * dx + dy * dy);
var headRadius = duckAsset.height * 0.18;
var headCenterY = self.y - duckAsset.height * 0.22;
var headDist = Math.sqrt((self.x - x) * (self.x - x) + (headCenterY - y) * (headCenterY - y));
return headDist < headRadius;
};
// Called every tick
self.update = function () {
if (!self.alive || self.hit) return;
self.x += Math.cos(self.angle) * self.speed;
self.y += Math.sin(self.angle) * self.speed;
if (self.x < -100 || self.x > 2048 + 100 || self.y < -100 || self.y > 2732 + 100) {
self.escaped = true;
self.alive = false;
}
};
// Animate duck falling when hit
self.fall = function (_onFinish) {
self.alive = false;
tween(self, {
rotation: Math.PI * 1.5,
y: self.y + 400
}, {
duration: 700,
easing: tween.cubicIn,
onFinish: function onFinish() {
if (_onFinish) _onFinish();
}
});
};
// Armored duck: flash red on first hit
self.flashRed = function () {
if (self.flashTween) {
self.flashTween.cancel();
}
var origTint = duckAsset.tint;
duckAsset.tint = 0xff2222;
self.flashTween = tween(duckAsset, {}, {
duration: 200,
onFinish: function onFinish() {
duckAsset.tint = 0x888888;
self.flashTween = null;
}
});
};
return self;
});
// Power-up collectible class
var PowerupCollectible = Container.expand(function () {
var self = Container.call(this);
self.type = null;
self.icon = null;
self.radius = 60;
self.speedY = -2 - Math.random() * 2;
self.lifetime = 0;
self.maxLifetime = 400; // ~6 seconds
self.init = function (ptype, x, y) {
self.type = ptype;
var iconId = ptype === 'doubleBarrel' ? 'powerup_double' : ptype === 'slowTime' ? 'powerup_slow' : 'powerup_auto';
self.icon = self.attachAsset(iconId, {
anchorX: 0.5,
anchorY: 0.5
});
self.x = x;
self.y = y;
self.icon.scaleX = self.icon.scaleY = 1.1;
self.visible = true;
self.alpha = 0.92;
};
self.update = function () {
self.y += self.speedY;
self.lifetime++;
// Fade out near end of life
if (self.lifetime > self.maxLifetime - 60) {
self.alpha = Math.max(0, (self.maxLifetime - self.lifetime) / 60);
}
// Remove if out of bounds or expired
if (self.y < 100 || self.lifetime > self.maxLifetime) {
if (self.parent) self.parent.removeChild(self);
if (typeof powerupCollectibles !== "undefined") {
var idx = powerupCollectibles.indexOf(self);
if (idx >= 0) powerupCollectibles.splice(idx, 1);
}
}
};
// Check if tap is within radius
self.isHit = function (x, y) {
var dx = self.x - x;
var dy = self.y - y;
return dx * dx + dy * dy < self.radius * self.radius;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x87ceeb // sky blue
});
/****
* Game Code
****/
// Use red as base, smaller
// Use blue as base, gray tint
// Use red as base, gold tint
// --- Duck type probabilities and point values ---
var DUCK_TYPE_CONFIG = [
// Level 1
{
golden: 0.04,
// 4%
armored: 0.0,
mini: 0.0
},
// Level 2
{
golden: 0.06,
armored: 0.08,
mini: 0.04
},
// Level 3
{
golden: 0.08,
armored: 0.13,
mini: 0.08
},
// Level 4
{
golden: 0.10,
armored: 0.18,
mini: 0.13
},
// Level 5+
{
golden: 0.13,
armored: 0.22,
mini: 0.18
}];
var DUCK_TYPE_POINTS = {
normal: 10,
golden: 50,
armored: 20,
mini: 25
};
// --- Power-up collectibles array ---
// Ducks: 3 colors, 1 dog, 1 bullet, 1 background, 1 crosshair, 1 "laugh" dog
// sky blue
// Sounds
// Music
// Power-up icons and retro overlay
var powerupCollectibles = [];
// Spawn a power-up collectible at a random position
function spawnPowerupCollectible(ptype) {
var px = 200 + Math.random() * (2048 - 400);
var py = 2732 - 200 - Math.random() * 800;
var p = new PowerupCollectible();
p.init(ptype, px, py);
powerupCollectibles.push(p);
game.addChild(p);
}
// Activate a power-up
function activatePowerup(ptype) {
if (powerupActive[ptype]) return;
powerupActive[ptype] = true;
var duration = 0;
if (ptype === 'doubleBarrel') duration = 7;
if (ptype === 'slowTime') duration = 5;
if (ptype === 'autoAim') duration = 3;
powerupTimers[ptype] = duration;
showPowerupIcon(ptype, duration);
// Apply effect
if (ptype === 'slowTime') {
// Slow all ducks
for (var i = 0; i < ducks.length; i++) {
ducks[i].speed = ducks[i].speed * 0.35;
}
// Add retro overlay
if (!slowTimeEffectOverlay) {
slowTimeEffectOverlay = LK.getAsset('retro_overlay', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
});
slowTimeEffectOverlay.alpha = 0.22;
game.addChild(slowTimeEffectOverlay);
}
}
if (ptype === 'autoAim') {
// No immediate effect, handled in shooting logic
}
if (ptype === 'doubleBarrel') {
// No immediate effect, handled in shooting logic
}
// Set timeout to deactivate
if (powerupTimeouts[ptype]) LK.clearTimeout(powerupTimeouts[ptype]);
powerupTimeouts[ptype] = LK.setTimeout(function () {
powerupActive[ptype] = false;
powerupTimers[ptype] = 0;
hidePowerupIcon(ptype);
if (ptype === 'slowTime') {
// Restore duck speed
for (var i = 0; i < ducks.length; i++) {
ducks[i].speed = ducks[i].speed / 0.35;
}
// Remove overlay
if (slowTimeEffectOverlay && slowTimeEffectOverlay.parent) {
slowTimeEffectOverlay.parent.removeChild(slowTimeEffectOverlay);
slowTimeEffectOverlay = null;
}
}
}, duration * 1000);
}
// Show power-up icon in GUI
function showPowerupIcon(ptype, duration) {
var iconId = ptype === 'doubleBarrel' ? 'powerup_double' : ptype === 'slowTime' ? 'powerup_slow' : 'powerup_auto';
var icon = LK.getAsset(iconId, {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0
});
icon.scaleX = icon.scaleY = 1.1;
var timerTxt = new Text2(duration + "s", {
size: 60,
fill: 0xffffff
});
timerTxt.anchor.set(0.5, 0.5);
icon.addChild(timerTxt);
powerupIcons[ptype] = icon;
// Place icons in top left, but not in 0-100px (menu area)
var idx = ptype === 'doubleBarrel' ? 0 : ptype === 'slowTime' ? 1 : 2;
icon.x = 120 + idx * 120;
icon.y = 120;
LK.gui.top.addChild(icon);
icon._timerTxt = timerTxt;
}
// Hide power-up icon
function hidePowerupIcon(ptype) {
if (powerupIcons[ptype] && powerupIcons[ptype].parent) {
powerupIcons[ptype].parent.removeChild(powerupIcons[ptype]);
powerupIcons[ptype] = null;
}
}
// Game state variables
var ducks = [];
var bullets = [];
var level = 1;
var ducksPerLevel = 5;
var ducksToHit = 0;
var ducksHit = 0;
var ducksEscaped = 0;
var maxBullets = 3;
var bulletsLeft = 3;
var timeLimit = 15; // seconds
var timeLeft = 15;
var gameActive = false;
var waveActive = false;
// Per-mode high score storage
var highScoreClassic = storage.highScoreClassic || 0;
var highScoreTimeAttack = storage.highScoreTimeAttack || 0;
var highScoreEndless = storage.highScoreEndless || 0;
var highScore = 0; // Will be set per mode in showMainMenu/startLevel
var score = 0;
var dog = null;
var crosshair = null;
var timerInterval = null;
var levelText = null;
var scoreText = null;
var timerText = null;
var bulletIcons = [];
var waveResultTimeout = null;
// Track missed shots for warning
var missedShots = 0;
var missedWarningShown = false;
// --- Power-up State ---
var powerupActive = {
doubleBarrel: false,
slowTime: false,
autoAim: false
};
var powerupTimers = {
doubleBarrel: 0,
slowTime: 0,
autoAim: 0
};
var powerupIcons = {
doubleBarrel: null,
slowTime: null,
autoAim: null
};
var powerupTimeouts = {
doubleBarrel: null,
slowTime: null,
autoAim: null
};
// For slow time effect
var slowTimeEffectOverlay = null;
// --- Game Mode State ---
var GAME_MODE_CLASSIC = "classic";
var GAME_MODE_TIMEATTACK = "timeattack";
var GAME_MODE_ENDLESS = "endless";
var gameMode = null; // Set by menu
var mainMenuContainer = null;
var endlessMissStreak = 0;
var endlessGameOver = false;
// --- Dog Selection State ---
var DOG_OPTIONS = [{
id: "dodo",
name: "Dodo",
asset: "dog",
laugh: "dog_laugh"
}, {
id: "kaiser",
name: "Kaiser",
asset: "dog_dalmatian",
laugh: "laugh_d"
}, {
id: "maki",
name: "Maki",
asset: "dog_labrador",
laugh: "laugh_l"
}];
var selectedDogId = "dodo"; // default dog
// Background
var bg = LK.getAsset('bg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
});
game.addChild(bg);
// --- Main Menu UI ---
function showMainMenu() {
// Remove previous menu if present
if (mainMenuContainer && mainMenuContainer.parent) {
mainMenuContainer.parent.removeChild(mainMenuContainer);
}
mainMenuContainer = new Container();
// Dim background
var menuBg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 16,
scaleY: 16,
x: 2048 / 2,
y: 2732 / 2
});
menuBg.alpha = 0.85;
mainMenuContainer.addChild(menuBg);
// Title
var title = new Text2("What The Duck", {
size: 220,
fill: 0xFFD700,
font: "'PressStart2P','GillSans-Bold',Impact,'Arial Black',Tahoma"
});
title.anchor.set(0.5, 0);
title.x = 2048 / 2;
title.y = 400;
mainMenuContainer.addChild(title);
// Classic Mode Button
var btnClassic = new Text2("Classic Mode", {
size: 120,
fill: 0xFFFFFF,
font: "'PressStart2P','GillSans-Bold',Impact,'Arial Black',Tahoma"
});
btnClassic.anchor.set(0.5, 0.5);
btnClassic.x = 2048 / 2;
btnClassic.y = 900;
mainMenuContainer.addChild(btnClassic);
// Time Attack Button
var btnTime = new Text2("Time Attack", {
size: 120,
fill: 0xFFFFFF,
font: "'PressStart2P','GillSans-Bold',Impact,'Arial Black',Tahoma"
});
btnTime.anchor.set(0.5, 0.5);
btnTime.x = 2048 / 2;
btnTime.y = 1100;
mainMenuContainer.addChild(btnTime);
// Endless Mode Button
var btnEndless = new Text2("Endless Mode", {
size: 120,
fill: 0xFFFFFF,
font: "'PressStart2P','GillSans-Bold',Impact,'Arial Black',Tahoma"
});
btnEndless.anchor.set(0.5, 0.5);
btnEndless.x = 2048 / 2;
btnEndless.y = 1300;
mainMenuContainer.addChild(btnEndless);
// Rules Button
var btnRules = new Text2("Rules", {
size: 100,
fill: 0xFFFFFF,
font: "'PressStart2P','GillSans-Bold',Impact,'Arial Black',Tahoma"
});
btnRules.anchor.set(0.5, 0.5);
btnRules.x = 2048 / 2;
btnRules.y = 1450;
mainMenuContainer.addChild(btnRules);
// Instructions
var info = new Text2("Choose a mode to start!", {
size: 80,
fill: 0xFFFFFF,
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
info.anchor.set(0.5, 0.5);
info.x = 2048 / 2;
info.y = 1550;
mainMenuContainer.addChild(info);
// --- Rules Popup ---
var rulesPopup = null;
function showRulesPopup() {
if (rulesPopup && rulesPopup.parent) rulesPopup.parent.removeChild(rulesPopup);
rulesPopup = new Container();
var popupBg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 10,
scaleY: 10,
x: 2048 / 2,
y: 2732 / 2
});
popupBg.alpha = 0.96;
rulesPopup.addChild(popupBg);
var rulesTitle = new Text2("Game Rules", {
size: 120,
fill: 0xFFD700,
font: "'PressStart2P','GillSans-Bold',Impact,'Arial Black',Tahoma"
});
rulesTitle.anchor.set(0.5, 0);
rulesTitle.x = 2048 / 2;
rulesTitle.y = 400;
rulesPopup.addChild(rulesTitle);
var rulesText = new Text2("Classic Mode:\n" + "- Hit enough ducks with limited bullets to advance.\n" + "- Each level has more ducks and bullets.\n\n" + "Time Attack:\n" + "- 30 seconds to hit as many ducks as possible.\n" + "- Unlimited bullets.\n\n" + "Endless Mode:\n" + "- Ducks spawn forever, unlimited bullets.\n" + "- Game ends if you miss 3 times in a row.\n\n" + "Duck Types:\n" + "- Golden Duck: Fast, +50 points.\n" + "- Armored Duck: Needs 2 hits.\n" + "- Mini Fast Duck: Small, fast, +25 points.\n\n" + "Power-ups:\n" + "- Double Barrel: Hit 2 ducks at once.\n" + "- Slow Time: Ducks move slowly for 5s.\n" + "- Auto-Aim: Instantly hit nearest duck for 3s.\n\n" + "Tap power-up icons to collect. Good luck!", {
size: 60,
fill: 0xFF2222,
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma",
wordWrap: true,
wordWrapWidth: 1600,
align: "left"
});
rulesText.anchor.set(0.5, 0);
rulesText.x = 2048 / 2;
rulesText.y = 550;
rulesPopup.addChild(rulesText);
var btnClose = new Text2("Close", {
size: 100,
fill: 0xFFD700,
font: "'PressStart2P','GillSans-Bold',Impact,'Arial Black',Tahoma"
});
btnClose.anchor.set(0.5, 0.5);
btnClose.x = 2048 / 2;
btnClose.y = 2300;
btnClose.interactive = true;
btnClose.down = function () {
if (rulesPopup.parent) rulesPopup.parent.removeChild(rulesPopup);
};
rulesPopup.addChild(btnClose);
game.addChild(rulesPopup);
}
// Button handlers
// --- Dog Select Screen ---
function showDogSelectScreen() {
// Remove previous menu if present
if (mainMenuContainer && mainMenuContainer.parent) {
mainMenuContainer.parent.removeChild(mainMenuContainer);
}
mainMenuContainer = new Container();
// Dim background
var menuBg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 16,
scaleY: 16,
x: 2048 / 2,
y: 2732 / 2
});
menuBg.alpha = 0.85;
mainMenuContainer.addChild(menuBg);
// Title
var title = new Text2("Select Your Dog", {
size: 180,
fill: 0xFFD700,
font: "'PressStart2P','GillSans-Bold',Impact,'Arial Black',Tahoma"
});
title.anchor.set(0.5, 0);
title.x = 2048 / 2;
title.y = 400;
mainMenuContainer.addChild(title);
// Dog options
var spacing = 600;
var startX = 2048 / 2 - spacing;
var y = 1100;
for (var i = 0; i < DOG_OPTIONS.length; i++) {
(function (i) {
var dogOpt = DOG_OPTIONS[i];
var asset = LK.getAsset(dogOpt.asset, {
anchorX: 0.5,
anchorY: 1,
x: startX + i * spacing,
y: y
});
asset.scaleX = asset.scaleY = 1.1;
mainMenuContainer.addChild(asset);
var nameTxt = new Text2(dogOpt.name, {
size: 100,
fill: 0xFFFFFF,
font: "'PressStart2P','GillSans-Bold',Impact,'Arial Black',Tahoma"
});
nameTxt.anchor.set(0.5, 0);
nameTxt.x = asset.x;
nameTxt.y = y + 30;
mainMenuContainer.addChild(nameTxt);
// Make asset interactive
asset.interactive = true;
asset.down = function () {
selectedDogId = dogOpt.id;
if (mainMenuContainer.parent) mainMenuContainer.parent.removeChild(mainMenuContainer);
startLevel();
};
nameTxt.interactive = true;
nameTxt.down = asset.down;
})(i);
}
// Back button
var btnBack = new Text2("Back", {
size: 90,
fill: 0xFFD700,
font: "'PressStart2P','GillSans-Bold',Impact,'Arial Black',Tahoma"
});
btnBack.anchor.set(0.5, 0.5);
btnBack.x = 2048 / 2;
btnBack.y = 2100;
btnBack.interactive = true;
btnBack.down = function () {
if (mainMenuContainer.parent) mainMenuContainer.parent.removeChild(mainMenuContainer);
showMainMenu();
};
mainMenuContainer.addChild(btnBack);
game.addChild(mainMenuContainer);
}
// Mode buttons now show dog select screen
btnClassic.interactive = true;
btnClassic.down = function () {
gameMode = GAME_MODE_CLASSIC;
showDogSelectScreen();
};
btnTime.interactive = true;
btnTime.down = function () {
gameMode = GAME_MODE_TIMEATTACK;
showDogSelectScreen();
};
btnEndless.interactive = true;
btnEndless.down = function () {
gameMode = GAME_MODE_ENDLESS;
showDogSelectScreen();
};
btnRules.interactive = true;
btnRules.down = function () {
showRulesPopup();
};
// Add to game
game.addChild(mainMenuContainer);
}
// Show menu on load
showMainMenu();
// Dog
dog = new Dog();
game.addChild(dog);
// Achievement popup (always on top)
var achievementPopup = new AchievementPopup();
game.addChild(achievementPopup);
// Crosshair
crosshair = LK.getAsset('crosshair', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366
});
crosshair.visible = false;
game.addChild(crosshair);
// GUI: Level, Score, Timer, Bullets
levelText = new Text2('Level 1', {
size: 90,
fill: 0xFFF000
});
levelText.anchor.set(1, 0); // right align to match high score
scoreText = new Text2('Score: 0', {
size: 90,
fill: 0xFFFFFF
});
scoreText.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreText);
timerText = new Text2('Time: 15', {
size: 90,
fill: 0xFFFFFF
});
timerText.anchor.set(0.5, 0);
LK.gui.top.addChild(timerText);
// High Score UI
// Set highScore based on mode (default to classic)
if (gameMode === GAME_MODE_TIMEATTACK) {
highScore = highScoreTimeAttack;
} else if (gameMode === GAME_MODE_ENDLESS) {
highScore = highScoreEndless;
} else {
highScore = highScoreClassic;
}
var highScoreText = new Text2('High: ' + highScore, {
size: 70,
fill: 0xFFD700
});
highScoreText.anchor.set(1, 0);
// Place high score in top right
LK.gui.topRight.addChild(highScoreText);
// Place level text directly under high score text in top right
LK.gui.topRight.addChild(levelText);
// Position GUI
// Position GUI
scoreText.x = 2048 / 2;
timerText.x = 2048 * 3 / 4;
highScoreText.x = 0;
highScoreText.y = 0;
levelText.x = 0;
levelText.y = highScoreText.height + 10; // 10px gap below high score
// Responsive UI: adjust font size and icon size for mobile
function resizeUI() {
var w = LK.width || 2048;
var h = LK.height || 2732;
var scale = Math.min(w / 2048, h / 2732);
if (scoreText && scoreText.style) scoreText.style.size = 90 * scale;
if (timerText && timerText.style) timerText.style.size = 90 * scale;
if (highScoreText && highScoreText.style) highScoreText.style.size = 70 * scale;
if (levelText && levelText.style) levelText.style.size = 90 * scale;
for (var i = 0; i < bulletIcons.length; i++) {
bulletIcons[i].scaleX = bulletIcons[i].scaleY = scale;
}
}
LK.on('resize', resizeUI);
resizeUI();
// Bullets GUI (now under the level text in the top right)
// Show both bullet icons (bottom) and a text counter (top right under level)
var bulletCountText = null;
var bulletLabelText = null;
function updateBulletIcons() {
// Remove old icons
for (var i = 0; i < bulletIcons.length; i++) {
if (bulletIcons[i].parent) bulletIcons[i].parent.removeChild(bulletIcons[i]);
}
bulletIcons = [];
// Draw new icons (up to 10 for visual clarity, then show text)
var maxIcons = Math.min(10, bulletsLeft);
// Center the icons under the screen
var iconSpacing = 60;
var totalWidth = (maxIcons - 1) * iconSpacing;
var startX = 2048 / 2 - totalWidth / 2;
for (var i = 0; i < maxIcons; i++) {
var icon = LK.getAsset('bullet_icon', {
anchorX: 0.5,
anchorY: 0.5,
x: startX + i * iconSpacing,
y: 2732 - 120
});
LK.gui.bottom.addChild(icon);
bulletIcons.push(icon);
}
// Remove old text if present
if (bulletCountText && bulletCountText.parent) {
bulletCountText.parent.removeChild(bulletCountText);
}
if (bulletLabelText && bulletLabelText.parent) {
bulletLabelText.parent.removeChild(bulletLabelText);
}
// Add "Bullets" label above the count, now in top right under level
bulletLabelText = new Text2("Bullets", {
size: 60,
fill: 0xFFF000,
font: "'PressStart2P','GillSans-Bold',Impact,'Arial Black',Tahoma"
});
bulletLabelText.anchor.set(1, 0); // right align, top anchor
bulletLabelText.x = 0;
bulletLabelText.y = levelText.y + levelText.height + 10;
LK.gui.topRight.addChild(bulletLabelText);
// Show bullet count as text (always, for clarity)
bulletCountText = new Text2("" + bulletsLeft, {
size: 90,
fill: 0xFFF000,
font: "'PressStart2P','GillSans-Bold',Impact,'Arial Black',Tahoma"
});
bulletCountText.anchor.set(1, 0); // right align, top anchor
bulletCountText.x = 0;
bulletCountText.y = bulletLabelText.y + bulletLabelText.height + 2;
LK.gui.topRight.addChild(bulletCountText);
}
// Start a new level
function startLevel() {
ducks = [];
bullets = [];
ducksHit = 0;
ducksEscaped = 0;
missedShots = 0;
missedWarningShown = false;
endlessMissStreak = 0;
endlessGameOver = false;
// --- Mode-specific setup ---
if (gameMode === GAME_MODE_CLASSIC || !gameMode) {
// Set ducksPerLevel and ducksToHit per level as per requirements
if (level === 1) {
ducksPerLevel = 6;
ducksToHit = 3;
} else if (level === 2) {
ducksPerLevel = 12;
ducksToHit = 6;
} else if (level === 3) {
ducksPerLevel = 20;
ducksToHit = 12;
} else if (level === 4) {
ducksPerLevel = 30;
ducksToHit = 18;
} else if (level === 5) {
ducksPerLevel = 40;
ducksToHit = 20;
} else {
ducksPerLevel = Math.min(25, 15 + (level - 5) * 2);
ducksToHit = Math.ceil(ducksPerLevel * 0.7);
}
// Set bulletsLeft per level: 10 for level 1, +10 bullets for every other level
if (level === 1) {
bulletsLeft = 10;
} else if (level === 2) {
bulletsLeft = 15 + 10;
} else if (level === 3) {
bulletsLeft = 20 + 10;
} else if (level === 4) {
bulletsLeft = 30 + 10;
} else if (level === 5) {
bulletsLeft = 32 + 10;
} else {
bulletsLeft = 32 + 2 * (level - 5) + 10;
}
maxBullets = bulletsLeft;
timeLimit = Math.max(8, 15 - (level - 1));
timeLeft = timeLimit;
levelText.setText('Level ' + level);
} else if (gameMode === GAME_MODE_TIMEATTACK) {
// 30 seconds, infinite ducks, infinite bullets, score = ducks hit
ducksPerLevel = 99999;
ducksToHit = 0;
bulletsLeft = 99999;
maxBullets = bulletsLeft;
timeLimit = 30;
timeLeft = 30;
levelText.setText('Time Attack');
} else if (gameMode === GAME_MODE_ENDLESS) {
// Infinite ducks, infinite bullets, game ends on 3 misses in a row
ducksPerLevel = 99999;
ducksToHit = 0;
bulletsLeft = 99999;
maxBullets = bulletsLeft;
timeLimit = 99999;
timeLeft = 99999;
levelText.setText('Endless');
}
// Set highScore for current mode
if (gameMode === GAME_MODE_TIMEATTACK) {
highScore = highScoreTimeAttack;
} else if (gameMode === GAME_MODE_ENDLESS) {
highScore = highScoreEndless;
} else {
highScore = highScoreClassic;
}
highScoreText.setText('High: ' + highScore);
gameActive = true;
waveActive = true;
updateBulletIcons();
scoreText.setText('Score: ' + score);
timerText.setText('Time: ' + timeLeft);
// Duck spawn schedule and speed per level/mode
var duckSpawnTimer = null;
var ducksSpawned = 0;
var ducksAtOnce = 1;
var duckSpawnInterval = 3000;
var duckSpeedMin = 2;
var duckSpeedMax = 3.5;
if (gameMode === GAME_MODE_CLASSIC || !gameMode) {
if (level === 1) {
ducksAtOnce = 1;
duckSpawnInterval = 3000;
duckSpeedMin = 2;
duckSpeedMax = 3.5;
} else if (level === 2) {
ducksAtOnce = 1;
duckSpawnInterval = 2000;
duckSpeedMin = 3.5;
duckSpeedMax = 5;
} else if (level === 3) {
ducksAtOnce = 2;
duckSpawnInterval = 1800;
duckSpeedMin = 4.2;
duckSpeedMax = 5.8;
} else if (level === 4) {
ducksAtOnce = 3;
duckSpawnInterval = 1500;
duckSpeedMin = 5.5;
duckSpeedMax = 7.2;
} else if (level === 5) {
ducksAtOnce = 4;
duckSpawnInterval = 1200;
duckSpeedMin = 6.5;
duckSpeedMax = 8.5;
} else {
ducksAtOnce = 5;
duckSpawnInterval = 1000;
duckSpeedMin = 7.5 + (level - 5) * 0.5;
duckSpeedMax = 9.5 + (level - 5) * 0.7;
}
} else if (gameMode === GAME_MODE_TIMEATTACK) {
ducksAtOnce = 2;
duckSpawnInterval = 900;
duckSpeedMin = 4.5;
duckSpeedMax = 7.5;
} else if (gameMode === GAME_MODE_ENDLESS) {
ducksAtOnce = 2;
duckSpawnInterval = 900;
duckSpeedMin = 4.5;
duckSpeedMax = 7.5;
}
// Clear any previous duck spawn timer
if (typeof duckSpawnTimer !== "undefined" && duckSpawnTimer) {
LK.clearInterval(duckSpawnTimer);
duckSpawnTimer = null;
}
ducksSpawned = 0;
// Function to spawn ducks and power-ups
function spawnDucks() {
if (!gameActive || !waveActive) return;
if ((gameMode === GAME_MODE_CLASSIC || !gameMode) && ducksSpawned >= ducksPerLevel) {
if (duckSpawnTimer) {
LK.clearInterval(duckSpawnTimer);
duckSpawnTimer = null;
}
return;
}
var ducksThisWave = gameMode === GAME_MODE_CLASSIC || !gameMode ? Math.min(ducksAtOnce, ducksPerLevel - ducksSpawned) : ducksAtOnce;
for (var d = 0; d < ducksThisWave; d++) {
var duck = new Duck();
// Randomize start edge: 0=left, 1=right, 2=bottom
var edge = Math.floor(Math.random() * 3);
var startX, startY, angle;
if (edge === 0) {
// left
startX = -60;
startY = 400 + Math.random() * (2732 - 800);
angle = Math.random() * Math.PI / 3 - Math.PI / 6; // -30 to +30 deg
} else if (edge === 1) {
// right
startX = 2048 + 60;
startY = 400 + Math.random() * (2732 - 800);
angle = Math.PI + (Math.random() * Math.PI / 3 - Math.PI / 6); // 150 to 210 deg
} else {
// bottom
startX = 200 + Math.random() * (2048 - 400);
startY = 2732 + 60;
angle = -Math.PI / 2 + (Math.random() * Math.PI / 4 - Math.PI / 8); // -67 to -23 deg
}
duck.x = startX;
duck.y = startY;
duck.angle = angle;
// Set duck speed and size based on type
if (duck.type === 'golden') {
duck.speed = duckSpeedMax + 2 + Math.random() * 1.5;
} else if (duck.type === 'armored') {
duck.speed = duckSpeedMin + Math.random() * (duckSpeedMax - duckSpeedMin) * 0.7;
} else if (duck.type === 'mini') {
duck.speed = duckSpeedMax + 1 + Math.random() * 1.2;
duck.scaleX = duck.scaleY = 0.6 + Math.random() * 0.2;
} else if (duck.type === 'fast') {
duck.speed = duckSpeedMax + 0.7 + Math.random() * 0.7;
} else {
duck.speed = duckSpeedMin + Math.random() * (duckSpeedMax - duckSpeedMin);
}
ducks.push(duck);
// Always add ducks above background and below crosshair/dog/UI
game.addChild(duck);
// Move dog and crosshair to top of display list if present
if (dog && dog.parent) {
dog.parent.removeChild(dog);
game.addChild(dog);
}
if (crosshair && crosshair.parent) {
crosshair.parent.removeChild(crosshair);
game.addChild(crosshair);
}
LK.getSound('duck_fly').play();
ducksSpawned++;
}
// --- Power-up spawn logic ---
// Power-ups can appear randomly (1 in 7 chance per spawn, but not if already active)
if (Math.random() < 1 / 7) {
var availablePowerups = [];
if (!powerupActive.doubleBarrel) availablePowerups.push('doubleBarrel');
if (!powerupActive.slowTime) availablePowerups.push('slowTime');
if (!powerupActive.autoAim) availablePowerups.push('autoAim');
if (availablePowerups.length > 0) {
var which = availablePowerups[Math.floor(Math.random() * availablePowerups.length)];
spawnPowerupCollectible(which);
}
}
}
spawnDucks(); // Spawn first batch immediately
if ((gameMode === GAME_MODE_CLASSIC || !gameMode) && ducksPerLevel > ducksAtOnce) {
duckSpawnTimer = LK.setInterval(spawnDucks, duckSpawnInterval);
} else if (gameMode === GAME_MODE_TIMEATTACK || gameMode === GAME_MODE_ENDLESS) {
duckSpawnTimer = LK.setInterval(spawnDucks, duckSpawnInterval);
}
// Start timer
if (timerInterval) LK.clearInterval(timerInterval);
timerInterval = LK.setInterval(function () {
if (!gameActive) return;
if (gameMode === GAME_MODE_TIMEATTACK) {
timeLeft--;
timerText.setText('Time: ' + timeLeft);
if (timeLeft <= 0) {
endWave();
}
} else if (gameMode === GAME_MODE_CLASSIC || !gameMode) {
timeLeft--;
timerText.setText('Time: ' + timeLeft);
if (timeLeft <= 0) {
endWave();
}
} else if (gameMode === GAME_MODE_ENDLESS) {
// No timer, but show "∞" or running time
timerText.setText('Endless');
}
}, 1000);
}
// End wave: check results, show dog if needed, advance or game over
function endWave() {
if (!waveActive) return;
waveActive = false;
gameActive = false;
if (timerInterval) LK.clearInterval(timerInterval);
// Remove remaining ducks
for (var i = 0; i < ducks.length; i++) {
if (ducks[i].parent) ducks[i].parent.removeChild(ducks[i]);
}
ducks = [];
// --- Classic Mode ---
if (gameMode === GAME_MODE_CLASSIC || !gameMode) {
// Show dog if not enough ducks hit
if (ducksHit < ducksToHit) {
dog.laugh(2048 / 2, 2732 - 100);
LK.effects.flashScreen(0xff0000, 800);
// Show final score on Game Over
LK.setTimeout(function () {
var finalScoreText = new Text2('Final Score: ' + score, {
size: 120,
fill: 0xFFFF00
});
finalScoreText.anchor.set(0.5, 0.5);
finalScoreText.x = 2048 / 2;
finalScoreText.y = 2732 / 2;
game.addChild(finalScoreText);
LK.setTimeout(function () {
if (finalScoreText.parent) finalScoreText.parent.removeChild(finalScoreText);
LK.showGameOver();
}, 900);
}, 400);
// Game over after laugh
waveResultTimeout = LK.setTimeout(function () {
// handled above
}, 1300);
} else {
// Advance to next level after short pause
LK.effects.flashObject(levelText, 0x00ff00, 700);
LK.setTimeout(function () {
var finalScoreText = new Text2('Score: ' + score, {
size: 120,
fill: 0x00FF00
});
finalScoreText.anchor.set(0.5, 0.5);
finalScoreText.x = 2048 / 2;
finalScoreText.y = 2732 / 2;
game.addChild(finalScoreText);
LK.setTimeout(function () {
if (finalScoreText.parent) finalScoreText.parent.removeChild(finalScoreText);
level++;
startLevel();
}, 700);
}, 200);
}
}
// --- Time Attack Mode ---
else if (gameMode === GAME_MODE_TIMEATTACK) {
// Show final score and return to menu
LK.effects.flashScreen(0x00ffcc, 800);
LK.setTimeout(function () {
var finalScoreText = new Text2('Ducks Hit: ' + ducksHit, {
size: 120,
fill: 0x00FFCC
});
finalScoreText.anchor.set(0.5, 0.5);
finalScoreText.x = 2048 / 2;
finalScoreText.y = 2732 / 2;
game.addChild(finalScoreText);
LK.setTimeout(function () {
if (finalScoreText.parent) finalScoreText.parent.removeChild(finalScoreText);
showMainMenu();
}, 1400);
}, 400);
}
// --- Endless Mode ---
else if (gameMode === GAME_MODE_ENDLESS) {
endlessGameOver = true;
LK.effects.flashScreen(0xff2222, 800);
LK.setTimeout(function () {
var finalScoreText = new Text2('Ducks Hit: ' + ducksHit, {
size: 120,
fill: 0xFF2222
});
finalScoreText.anchor.set(0.5, 0.5);
finalScoreText.x = 2048 / 2;
finalScoreText.y = 2732 / 2;
game.addChild(finalScoreText);
LK.setTimeout(function () {
if (finalScoreText.parent) finalScoreText.parent.removeChild(finalScoreText);
showMainMenu();
}, 1400);
}, 400);
}
}
// Handle tap/click to shoot
game.down = function (x, y, obj) {
if (!gameActive || !waveActive) return;
// --- Power-up collectible check ---
for (var i = powerupCollectibles.length - 1; i >= 0; i--) {
var p = powerupCollectibles[i];
if (p.isHit(x, y)) {
activatePowerup(p.type);
if (p.parent) p.parent.removeChild(p);
powerupCollectibles.splice(i, 1);
LK.effects.flashObject(p, 0xffffff, 300);
LK.getSound('hit').play();
return;
}
}
// --- Bullets check ---
if ((gameMode === GAME_MODE_CLASSIC || !gameMode) && bulletsLeft <= 0) {
LK.getSound('miss').play();
LK.effects.flashScreen(0x888888, 200);
// Show reload animation (flash bullet icons red)
for (var i = 0; i < bulletIcons.length; i++) {
LK.effects.flashObject(bulletIcons[i], 0xff2222, 300);
}
// Optionally play reload sound if available
// LK.getSound('reload').play();
return;
}
// Only decrement bullets and allow shooting if bulletsLeft > 0 (classic)
if (gameMode === GAME_MODE_CLASSIC || !gameMode) {
bulletsLeft--;
updateBulletIcons();
if (bulletCountText && bulletCountText.setText) {
bulletCountText.setText("Bullets: " + bulletsLeft);
}
// If bullets reach zero after this shot, trigger game over immediately
if (bulletsLeft === 0) {
LK.setTimeout(function () {
LK.showGameOver();
}, 400); // short delay to allow last shot/crosshair to animate
return;
}
}
LK.getSound('shoot').play();
// Show crosshair at tap
crosshair.x = x;
crosshair.y = y;
crosshair.visible = true;
tween(crosshair, {
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
crosshair.visible = false;
crosshair.alpha = 1;
}
});
// --- Power-up shooting logic ---
// Auto-Aim: instantly hit nearest duck
if (powerupActive.autoAim) {
var nearestDuck = null;
var minDist = 99999;
for (var i = 0; i < ducks.length; i++) {
var duck = ducks[i];
if (!duck.alive || duck.hit) continue;
var dx = duck.x - x;
var dy = duck.y - y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < minDist) {
minDist = dist;
nearestDuck = duck;
}
}
if (nearestDuck) {
// Hit the duck
nearestDuck.hit = true;
nearestDuck.fall(function () {
if (nearestDuck.parent) nearestDuck.parent.removeChild(nearestDuck);
if (dog) {
dog.show(nearestDuck.x, 2732 - 100);
LK.getSound('dog_laugh').play();
LK.setTimeout(function () {
dog.hide();
}, 2000);
}
});
ducksHit++;
var duckScore = 10;
var achievementMsg = '';
var achievementColor = 0xFFD700;
if (typeof nearestDuck.type !== "undefined" && nearestDuck.type === 'golden') {
duckScore = 50;
achievementMsg = 'Golden Duck! +50';
achievementColor = 0xFFD700;
LK.effects.flashObject(nearestDuck, 0xffe066, 400);
} else {
LK.effects.flashObject(nearestDuck, 0xffff00, 300);
}
// Headshot bonus (auto-aim never headshots)
score += duckScore;
LK.getSound('hit').play();
if (typeof achievementPopup !== "undefined" && achievementMsg) {
achievementPopup.show(achievementMsg, achievementColor);
}
if (ducksHit === 3 && typeof achievementPopup !== "undefined") {
achievementPopup.show('Triple Hit!', 0x00FFCC);
}
if (ducksHit === ducksToHit && typeof achievementPopup !== "undefined") {
achievementPopup.show('Sharp Shooter!', 0x00FF00);
}
if (score > highScore) {
highScore = score;
storage.highScore = highScore;
if (typeof highScoreText !== "undefined") {
highScoreText.setText('High: ' + highScore);
}
}
scoreText.setText('Score: ' + score);
if (score > highScore) {
highScore = score;
storage.highScore = highScore;
if (typeof highScoreText !== "undefined") {
highScoreText.setText('High: ' + highScore);
}
}
if (gameMode === GAME_MODE_ENDLESS) {
endlessMissStreak = 0;
}
// End wave check
var aliveDucks = 0;
for (var i = 0; i < ducks.length; i++) {
if (ducks[i].alive) aliveDucks++;
}
if ((gameMode === GAME_MODE_CLASSIC || !gameMode) && (aliveDucks === 0 && bulletsLeft === 0 || aliveDucks === 0 && typeof duckSpawnTimer !== "undefined" && (!duckSpawnTimer || ducks.length === 0))) {
endWave();
}
return;
}
}
// Double Barrel: shoot a wide spread, hit up to 2 ducks if close
if (powerupActive.doubleBarrel) {
var hitDucks = [];
for (var i = 0; i < ducks.length; i++) {
var duck = ducks[i];
if (!duck.alive || duck.hit) continue;
var dx = duck.x - x;
var dy = duck.y - y;
var dist = Math.sqrt(dx * dx + dy * dy);
var hitRadius = typeof duck.getHitboxRadius === "function" ? duck.getHitboxRadius() * 1.5 : 120;
if (dist < hitRadius) {
hitDucks.push({
duck: duck,
dist: dist
});
}
}
// Sort by distance, hit up to 2 closest
hitDucks.sort(function (a, b) {
return a.dist - b.dist;
});
hitDucks = hitDucks.slice(0, 2);
if (hitDucks.length > 0) {
for (var h = 0; h < hitDucks.length; h++) {
var hitDuck = hitDucks[h].duck;
hitDuck.hit = true;
hitDuck.fall(function () {
if (hitDuck.parent) hitDuck.parent.removeChild(hitDuck);
if (dog) {
dog.show(hitDuck.x, 2732 - 100);
LK.getSound('dog_laugh').play();
LK.setTimeout(function () {
dog.hide();
}, 2000);
}
});
ducksHit++;
var duckScore = 10;
var achievementMsg = '';
var achievementColor = 0xFFD700;
if (typeof hitDuck.type !== "undefined" && hitDuck.type === 'golden') {
duckScore = 50;
achievementMsg = 'Golden Duck! +50';
achievementColor = 0xFFD700;
LK.effects.flashObject(hitDuck, 0xffe066, 400);
} else {
LK.effects.flashObject(hitDuck, 0xffff00, 300);
}
// Headshot bonus
var isHeadshot = typeof hitDuck.isHeadshot === "function" && hitDuck.isHeadshot(x, y);
if (isHeadshot) {
duckScore += 5;
achievementMsg = 'Headshot! +' + duckScore;
achievementColor = 0xFF2222;
LK.effects.flashObject(hitDuck, 0xff2222, 400);
}
score += duckScore;
LK.getSound('hit').play();
if (typeof achievementPopup !== "undefined" && achievementMsg) {
achievementPopup.show(achievementMsg, achievementColor);
}
if (ducksHit === 3 && typeof achievementPopup !== "undefined") {
achievementPopup.show('Triple Hit!', 0x00FFCC);
}
if (ducksHit === ducksToHit && typeof achievementPopup !== "undefined") {
achievementPopup.show('Sharp Shooter!', 0x00FF00);
}
if (score > highScore) {
highScore = score;
storage.highScore = highScore;
if (typeof highScoreText !== "undefined") {
highScoreText.setText('High: ' + highScore);
}
}
scoreText.setText('Score: ' + score);
if (score > highScore) {
highScore = score;
storage.highScore = highScore;
if (typeof highScoreText !== "undefined") {
highScoreText.setText('High: ' + highScore);
}
}
if (gameMode === GAME_MODE_ENDLESS) {
endlessMissStreak = 0;
}
}
// End wave check
var aliveDucks = 0;
for (var i = 0; i < ducks.length; i++) {
if (ducks[i].alive) aliveDucks++;
}
if ((gameMode === GAME_MODE_CLASSIC || !gameMode) && (aliveDucks === 0 && bulletsLeft === 0 || aliveDucks === 0 && typeof duckSpawnTimer !== "undefined" && (!duckSpawnTimer || ducks.length === 0))) {
endWave();
}
return;
}
}
// --- Normal shooting logic ---
// Check if a duck is hit (closest duck under tap, not already hit)
var hitDuck = null;
var minDist = 99999;
for (var i = 0; i < ducks.length; i++) {
var duck = ducks[i];
// Only allow hitting ducks that are alive and not hit yet
if (!duck.alive || duck.hit) continue;
var dx = duck.x - x;
var dy = duck.y - y;
var hitRadius = typeof duck.getHitboxRadius === "function" ? duck.getHitboxRadius() : 80;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < hitRadius && dist < minDist) {
minDist = dist;
hitDuck = duck;
}
}
if (hitDuck) {
// Armored duck: needs 2 hits
if (hitDuck.type === 'armored' && hitDuck.armor > 1) {
hitDuck.armor--;
hitDuck.flashRed();
LK.effects.flashObject(hitDuck, 0xff2222, 200);
LK.getSound('hit').play();
// No score yet, must hit again
return;
}
// Mark duck as hit and start fall animation
hitDuck.hit = true;
hitDuck.fall(function () {
if (hitDuck.parent) hitDuck.parent.removeChild(hitDuck);
if (dog) {
dog.show(hitDuck.x, 2732 - 100);
LK.getSound('dog_laugh').play();
LK.setTimeout(function () {
dog.hide();
}, 2000);
}
});
ducksHit++;
// Score system: use DUCK_TYPE_POINTS
if (typeof score === "undefined") score = 0;
var duckScore = DUCK_TYPE_POINTS[hitDuck.type] || 10;
var achievementMsg = '';
var achievementColor = 0xFFD700;
if (hitDuck.type === 'golden') {
achievementMsg = 'Golden Duck! +50';
achievementColor = 0xFFD700;
LK.effects.flashObject(hitDuck, 0xffe066, 400);
} else if (hitDuck.type === 'armored') {
achievementMsg = 'Armored Duck! +20';
achievementColor = 0x888888;
LK.effects.flashObject(hitDuck, 0xcccccc, 400);
} else if (hitDuck.type === 'mini') {
achievementMsg = 'Mini Fast Duck! +25';
achievementColor = 0x00e6ff;
LK.effects.flashObject(hitDuck, 0x00e6ff, 350);
} else {
LK.effects.flashObject(hitDuck, 0xffff00, 300);
}
// Headshot bonus
var isHeadshot = typeof hitDuck.isHeadshot === "function" && hitDuck.isHeadshot(x, y);
if (isHeadshot) {
duckScore += 5;
achievementMsg = 'Headshot! +' + duckScore;
achievementColor = 0xFF2222;
LK.effects.flashObject(hitDuck, 0xff2222, 400);
}
score += duckScore;
LK.getSound('hit').play();
// Show achievement popup if any
if (typeof achievementPopup !== "undefined" && achievementMsg) {
achievementPopup.show(achievementMsg, achievementColor);
}
// Triple hit achievement (3 ducks in 1 level)
if (ducksHit === 3 && typeof achievementPopup !== "undefined") {
achievementPopup.show('Triple Hit!', 0x00FFCC);
}
// Sharp shooter (all ducks hit)
if (ducksHit === ducksToHit && typeof achievementPopup !== "undefined") {
achievementPopup.show('Sharp Shooter!', 0x00FF00);
}
// Update high score if needed
if (score > highScore) {
highScore = score;
if (gameMode === GAME_MODE_TIMEATTACK) {
highScoreTimeAttack = highScore;
storage.highScoreTimeAttack = highScore;
} else if (gameMode === GAME_MODE_ENDLESS) {
highScoreEndless = highScore;
storage.highScoreEndless = highScore;
} else {
highScoreClassic = highScore;
storage.highScoreClassic = highScore;
}
if (typeof highScoreText !== "undefined") {
highScoreText.setText('High: ' + highScore);
}
}
scoreText.setText('Score: ' + score);
if (score > highScore) {
highScore = score;
storage.highScore = highScore;
if (typeof highScoreText !== "undefined") {
highScoreText.setText('High: ' + highScore);
}
}
// Endless mode: reset miss streak
if (gameMode === GAME_MODE_ENDLESS) {
endlessMissStreak = 0;
}
} else {
// Miss: do nothing to ducks, just play miss sound and flash
LK.getSound('miss').play();
LK.effects.flashScreen(0x888888, 120);
// Track missed shots and show warning after 20 misses
missedShots++;
if (missedShots >= 20 && !missedWarningShown) {
missedWarningShown = true;
if (typeof achievementPopup !== "undefined") {
achievementPopup.show("Careful! 20 missed shots!", 0xFF2222);
}
}
// Endless mode: increment miss streak, end game if 3 in a row
if (gameMode === GAME_MODE_ENDLESS) {
endlessMissStreak++;
if (endlessMissStreak >= 3) {
endWave();
return;
}
}
}
// If all ducks are gone or out of bullets, end wave (classic only)
var aliveDucks = 0;
for (var i = 0; i < ducks.length; i++) {
if (ducks[i].alive) aliveDucks++;
}
// Only end wave if all ducks are gone AND no more ducks will spawn, or if out of bullets
if ((gameMode === GAME_MODE_CLASSIC || !gameMode) && (aliveDucks === 0 && bulletsLeft === 0 || aliveDucks === 0 && typeof duckSpawnTimer !== "undefined" && (!duckSpawnTimer || ducks.length === 0))) {
endWave();
}
};
// Move handler for crosshair (optional, for desktop)
game.move = function (x, y, obj) {
// Optionally show crosshair following finger/mouse
// crosshair.x = x;
// crosshair.y = y;
};
// Main update loop
game.update = function () {
if (!gameActive || !waveActive) return;
// Update ducks
for (var i = ducks.length - 1; i >= 0; i--) {
var duck = ducks[i];
duck.update();
if (duck.escaped && !duck.hit) {
ducksEscaped++;
if (duck.parent) duck.parent.removeChild(duck);
ducks.splice(i, 1);
// Endless mode: treat escaped duck as a miss
if (gameMode === GAME_MODE_ENDLESS) {
endlessMissStreak++;
if (endlessMissStreak >= 3) {
endWave();
return;
}
}
}
}
// --- Power-up collectibles update ---
for (var i = powerupCollectibles.length - 1; i >= 0; i--) {
powerupCollectibles[i].update();
}
// --- Power-up timers/icons update ---
for (var ptype in powerupActive) {
if (powerupActive[ptype] && powerupTimers[ptype] > 0) {
// Decrement timer (per second, but update is 60fps)
if (LK.ticks % 60 === 0) {
powerupTimers[ptype]--;
if (powerupIcons[ptype] && powerupIcons[ptype]._timerTxt) {
powerupIcons[ptype]._timerTxt.setText(powerupTimers[ptype] + "s");
}
}
}
}
// If all ducks gone, end wave (classic only)
var aliveDucks = 0;
for (var i = 0; i < ducks.length; i++) {
if (ducks[i].alive) aliveDucks++;
}
if ((gameMode === GAME_MODE_CLASSIC || !gameMode) && aliveDucks === 0 && typeof duckSpawnTimer !== "undefined" && (!duckSpawnTimer || ducks.length === 0)) {
endWave();
}
};
// On game over, reset everything
LK.on('gameover', function () {
if (timerInterval) LK.clearInterval(timerInterval);
if (waveResultTimeout) LK.clearTimeout(waveResultTimeout);
if (typeof duckSpawnTimer !== "undefined" && duckSpawnTimer) {
LK.clearInterval(duckSpawnTimer);
duckSpawnTimer = null;
}
gameActive = false;
waveActive = false;
level = 1;
ducks = [];
bullets = [];
ducksHit = 0;
ducksEscaped = 0;
// Reset bullets to 10 on gameover for level 1
bulletsLeft = 10;
score = 0;
updateBulletIcons();
levelText.setText('Level 1');
scoreText.setText('Score: ' + score);
// Reset highScore to current mode's high score on gameover
if (gameMode === GAME_MODE_TIMEATTACK) {
highScore = highScoreTimeAttack;
} else if (gameMode === GAME_MODE_ENDLESS) {
highScore = highScoreEndless;
} else {
highScore = highScoreClassic;
}
if (typeof highScoreText !== "undefined") {
highScoreText.setText('High: ' + highScore);
}
timerText.setText('Time: 15');
if (dog) dog.hide();
// Show main menu after short delay
LK.setTimeout(function () {
showMainMenu();
}, 1200);
});
// On you win (if you want to add a win condition, e.g. after level 10)
LK.on('youwin', function () {
if (timerInterval) LK.clearInterval(timerInterval);
if (waveResultTimeout) LK.clearTimeout(waveResultTimeout);
if (typeof duckSpawnTimer !== "undefined" && duckSpawnTimer) {
LK.clearInterval(duckSpawnTimer);
duckSpawnTimer = null;
}
gameActive = false;
waveActive = false;
// Show win, then reset
LK.setTimeout(function () {
level = 1;
ducks = [];
bullets = [];
ducksHit = 0;
ducksEscaped = 0;
bulletsLeft = 10;
score = 0;
updateBulletIcons();
levelText.setText('Level 1');
scoreText.setText('Score: ' + score);
// Reset highScore to current mode's high score on youwin
if (gameMode === GAME_MODE_TIMEATTACK) {
highScore = highScoreTimeAttack;
} else if (gameMode === GAME_MODE_ENDLESS) {
highScore = highScoreEndless;
} else {
highScore = highScoreClassic;
}
timerText.setText('Time: 15');
if (dog) dog.hide();
showMainMenu();
}, 1800);
});
// Start music
LK.playMusic('bgm', {
fade: {
start: 0,
end: 0.5,
duration: 1200
}
});
// Start first level
// (Removed: now handled by showMainMenu)
bullet. In-Game asset. 2d. High contrast. No shadows
crosshair. In-Game asset. 2d. High contrast. No shadows
pixart jungle. In-Game asset. 2d. High contrast. No shadows
pixart hunting brown dog with black ears and white mouth holding a gun. In-Game asset. 2d. High contrast. No shadows
pixart brown hunting dog with black ears and white mouth laughing. In-Game asset. 2d. High contrast. No shadows
pixart blue duck flying. In-Game asset. 2d. High contrast. No shadows
pixart green duck flying. In-Game asset. 2d. High contrast. No shadows
pixart red duck flying. In-Game asset. 2d. High contrast. No shadows
pigeon flying. In-Game asset. 2d. High contrast. No shadows
black wall. In-Game asset. 2d. High contrast. No shadows
empty beer glass. In-Game asset. 2d. High contrast. No shadows
pixart sand watch. In-Game asset. 2d. High contrast. No shadows
dalmation dog holding a gun. In-Game asset. 2d. High contrast. No shadows. hunting
labrador dog hold a hunt gun. In-Game asset. 2d. High contrast. No shadows
dalmatian dog laugh. In-Game asset. 2d. High contrast. No shadows
labrador laugh. In-Game asset. 2d. High contrast. No shadows