/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// --- Arena class ---
// Provides a background and groundY property for fighter placement
var Arena = Container.expand(function () {
var self = Container.call(this);
// Add arena background image, anchored at top-left
var bg = self.attachAsset('arena_bg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
});
// Set groundY to a reasonable value for fighter placement
// Place ground near the bottom of the visible area, but above the very bottom
self.groundY = 2048 + 300; // 2048 is the base height, +300 for a bit above the bottom
return self;
});
// --- Attack class ---
// Represents a regular or special attack hitbox, with owner and damage
var Attack = Container.expand(function () {
var self = Container.call(this);
// Arguments: owner (Fighter), isSpecial (bool)
self.owner = arguments[0] || null;
self.isSpecial = !!arguments[1];
self.hit = false;
self.destroyed = false;
// Damage values
self.damage = self.isSpecial ? 24 : 8;
// Visual
var assetId = self.isSpecial ? 'special_attack' : 'attack';
var sprite = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0
});
// Set size for hitbox (use asset size)
self.width = sprite.width;
self.height = sprite.height;
// Set owner after creation if needed
self.setOwner = function (fighter) {
self.owner = fighter;
};
// Update: attacks last for a short time, then destroy
self.lifetime = self.isSpecial ? 30 : 18; // special lasts longer
self.update = function () {
if (self.destroyed) return;
self.lifetime--;
if (self.lifetime <= 0) {
self.destroy();
}
};
self.destroy = function () {
if (self.destroyed) return;
self.destroyed = true;
if (self.parent && self.parent.removeChild) {
self.parent.removeChild(self);
}
};
return self;
});
// --- Fighter class ---
// Handles player and AI fighter logic, health, attacks, movement, and health bar
var Fighter = Container.expand(function () {
var self = Container.call(this);
// --- Properties ---
self.maxHealth = 100;
self.health = self.maxHealth;
self.moveSpeed = 6; // Slower movement
self.jumpStrength = 32; // Lower jump
self.gravity = 3; // Slower fall
self.isJumping = false;
self.isAttacking = false;
self.isSpecialReady = true;
self.specialCooldown = 600; // 10 seconds at 60fps (slower special)
self.specialTimer = 0;
self.immobile = false;
self.isFacingRight = true;
self.attackCooldown = 120; // 2 seconds at 60fps (slower attack rate)
self.attackTimer = 0;
self.lastJumpPressed = false;
// --- Dodge mechanic ---
self.isDodging = false;
self.dodgeDuration = 30; // 0.5s at 60fps (slower dodge)
self.dodgeTimer = 0;
self.dodgeSpeed = 15; // Slower dodge movement
self.dodgeInvuln = false;
self.lastDodgePressed = false;
self.dodgeCooldown = 240; // 4s cooldown (slower dodge cooldown)
self.dodgeCooldownTimer = 0;
// Start dodge if not already dodging or on 3s cooldown
self.dodge = function () {
if (self.isDodging || self.immobile || self.dodgeCooldownTimer > 0 || self.isJumping) return;
self.isDodging = true;
self.dodgeTimer = self.dodgeDuration;
self.dodgeInvuln = true;
self.dodgeCooldownTimer = self.dodgeCooldown;
// Play dodge sound when dodge happens
var dodgeSound = LK.getSound('dodge');
if (dodgeSound) dodgeSound.play();
// Visual feedback: flash fighter blue for dodge duration
LK.effects.flashObject(self, 0x00aaff, self.dodgeDuration * 16);
};
// --- Visuals ---
// Use a different asset for player1 (controlled fighter)
var assetId = typeof self.isPlayer1 !== "undefined" && self.isPlayer1 === true ? 'fighter1' : 'fighter';
var sprite = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 1.0,
x: 0,
y: 0
});
// Health bar container
self.healthBar = new Container();
// Health bar outline (drawn behind the background)
var healthBarOutline = new Container();
var outlineThickness = 6;
var outlineW = 300 + outlineThickness * 2;
var outlineH = 50 + outlineThickness * 2;
var outlineRect = LK.getAsset('healthbar_bg', {
anchorX: 0,
anchorY: 0.5,
x: -outlineThickness,
y: 0,
width: outlineW,
height: outlineH,
color: 0xffffff // white outline
});
healthBarOutline.addChild(outlineRect);
self.healthBar.addChild(healthBarOutline);
// Health bar background
var healthBarBg = LK.getAsset('healthbar_bg', {
anchorX: 0,
anchorY: 0.5,
x: 0,
y: 0
});
self.healthBar.addChild(healthBarBg);
// Health bar foreground
var healthBarFg = LK.getAsset('healthbar_fg', {
anchorX: 0,
anchorY: 0.5,
x: 0,
y: 0
});
self.healthBar.addChild(healthBarFg);
self.healthBar.width = 300;
self.healthBar.height = 50;
self.healthBarFg = healthBarFg;
self.updateHealthBar = function () {
var pct = Math.max(0, self.health / self.maxHealth);
self.healthBarFg.width = 300 * pct;
};
self.updateHealthBar();
// --- Methods ---
self.setFacing = function (right) {
self.isFacingRight = right;
sprite.scaleX = right ? 1 : -1;
};
self.jump = function () {
if (!self.isJumping && !self.immobile) {
self.isJumping = true;
self.vy = -self.jumpStrength;
}
};
self.attack = function () {
if (self.isAttacking || self.immobile || self.attackTimer > 0) return;
self.isAttacking = true;
self.attackTimer = self.attackCooldown;
// Create attack object
var atk = new Attack(self, false);
atk.x = self.x + (self.isFacingRight ? 120 : -120);
atk.y = self.y - 200;
attacks.push(atk);
game.addChild(atk);
// Play regatt sound for regular attack with randomized volume
var regattSound = LK.getSound('regatt');
regattSound.volume = 0.2 + Math.random() * 0.8; // random volume between 0.2 and 1.0
regattSound.play();
// End attack after short delay
LK.setTimeout(function () {
self.isAttacking = false;
}, 300);
};
self.special = function () {
if (!self.isSpecialReady || self.immobile) return;
self.isSpecialReady = false;
self.specialTimer = self.specialCooldown;
// Create special attack object
var atk = new Attack(self, true);
atk.x = self.x + (self.isFacingRight ? 180 : -180);
atk.y = self.y - 200;
attacks.push(atk);
game.addChild(atk);
// Play spatt sound for special attack
var spattSound = LK.getSound('spatt');
spattSound.play();
};
self.takeDamage = function (amount, dir, knockback) {
if (self.immobile) return;
self.health -= amount;
self.updateHealthBar();
// Flash health bar foreground red for 200ms when taking damage
LK.effects.flashObject(self.healthBarFg, 0xff0000, 200);
// --- Blood particle effect on damage ---
for (var i = 0; i < 8; i++) {
var blood = LK.getAsset('blood_particle', {
anchorX: 0.5,
anchorY: 0.5,
x: self.x + (Math.random() - 0.5) * 80,
y: self.y - (self.height || 400) + (Math.random() - 0.5) * 40,
width: 30 + Math.random() * 20,
height: 18 + Math.random() * 10
});
blood.alpha = 0.7 + Math.random() * 0.3;
if (self.parent && self.parent.addChild) self.parent.addChild(blood);
// Animate blood: random direction, fade out, fall down
var dx = (Math.random() - 0.5) * 120;
var dy = 80 + Math.random() * 120;
tween(blood, {
x: blood.x + dx,
y: blood.y + dy,
alpha: 0
}, {
duration: 120 + Math.random() * 60,
// much faster animation
onFinish: function (b) {
return function () {
if (b && b.destroy) b.destroy();
};
}(blood)
});
}
// Knockback
self.x += (dir || 1) * (knockback || 32);
// Clamp to arena
self.x = Math.max(120, Math.min(2048 - 120, self.x));
};
self.update = function () {
// Dodge cooldown
if (self.dodgeCooldownTimer > 0) {
self.dodgeCooldownTimer--;
}
// Dodge logic
if (self.isDodging) {
// Move quickly in facing direction
var dir = self.isFacingRight ? 1 : -1;
self.x += dir * self.dodgeSpeed;
// Clamp to arena
self.x = Math.max(120, Math.min(2048 - 120, self.x));
self.dodgeTimer--;
if (self.dodgeTimer <= 0) {
self.isDodging = false;
self.dodgeInvuln = false;
}
}
// Attack cooldown
if (self.attackTimer > 0) {
self.attackTimer--;
}
// Special cooldown
if (!self.isSpecialReady) {
if (self.specialTimer > 0) {
self.specialTimer--;
}
if (self.specialTimer <= 0) {
self.isSpecialReady = true;
}
}
// Jumping physics
if (self.isJumping) {
if (typeof self.vy === "undefined") self.vy = 0;
// Track lastY for landing detection
if (typeof self.lastY === "undefined") self.lastY = self.y;
self.y += self.vy;
self.vy += self.gravity;
// Land on ground (detect landing this frame)
if (self.lastY < arena.groundY && self.y >= arena.groundY) {
self.y = arena.groundY;
self.isJumping = false;
self.vy = 0;
// Play mov sound when landing from a jump
var movSound = LK.getSound('mov');
if (movSound) movSound.play();
}
self.lastY = self.y;
}
};
// --- Throw mechanic ---
self.throwObject = function () {
if (self.immobile || self.isDodging || self.isAttacking || self.isJumping) return;
// Only allow one projectile at a time per fighter (optional, can remove for spam)
if (typeof self.lastProjectileTick !== "undefined" && LK.ticks - self.lastProjectileTick < 30) return;
if (typeof throwablesLeft !== "undefined" && throwablesLeft > 0) {
throwablesLeft--;
if (typeof throwablesText !== "undefined" && throwablesText.setText) {
throwablesText.setText('Throwables: ' + throwablesLeft);
}
}
self.lastProjectileTick = LK.ticks;
var proj = new Projectile(self, self.isFacingRight);
proj.x = self.x + (self.isFacingRight ? 120 : -120);
proj.y = self.y - 180;
// Play throw sound when projectile is spawned
var throwSound = LK.getSound('throw');
if (throwSound) throwSound.play();
attacks.push(proj);
game.addChild(proj);
};
return self;
});
// --- Projectile class ---
// Represents a thrown projectile (e.g. shuriken, fireball)
var Projectile = Container.expand(function () {
var self = Container.call(this);
// Arguments: owner (Fighter), isRight (bool)
self.owner = arguments[0] || null;
self.isRight = !!arguments[1];
self.hit = false;
self.destroyed = false;
self.damage = 6;
self.speed = 16; // Slower projectile
self.lifetime = 90; // Lasts longer (1.5 seconds)
// Visual
var sprite = self.attachAsset('btn_throw', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0
});
self.width = sprite.width;
self.height = sprite.height;
// Set owner after creation if needed
self.setOwner = function (fighter) {
self.owner = fighter;
};
self.update = function () {
if (self.destroyed) return;
// --- Trail effect: spawn faded throw asset at current position ---
// Only spawn trail every 2 frames for performance
if (typeof self._trailTick === "undefined") self._trailTick = 0;
self._trailTick++;
if (self._trailTick % 2 === 0) {
var trail = LK.getAsset('btn_throw', {
anchorX: 0.5,
anchorY: 0.5,
x: self.x,
y: self.y,
scaleX: 0.7,
scaleY: 0.7
});
trail.alpha = 0.35;
if (self.parent && self.parent.addChild) self.parent.addChild(trail);
// Fade out and destroy after 180ms
tween(trail, {
alpha: 0
}, {
duration: 180,
onFinish: function onFinish() {
if (trail && trail.destroy) trail.destroy();
}
});
}
// Move in direction
self.x += self.isRight ? self.speed : -self.speed;
self.lifetime--;
// Remove if out of bounds or expired
if (self.x < -200 || self.x > 2048 + 200 || self.lifetime <= 0) {
self.destroy();
}
};
self.destroy = function () {
if (self.destroyed) return;
self.destroyed = true;
if (self.parent && self.parent.removeChild) {
self.parent.removeChild(self);
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// --- Main Menu Overlay ---
// Play background music at game start with reduced volume
LK.playMusic('music', {
volume: 0.3
});
var mainMenuOverlay = new Container();
mainMenuOverlay.visible = true; // Ensure menu is visible at game start
// Main menu background image (separate from arena)
var menuBgImg = LK.getAsset('arena_bg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 2732
});
mainMenuOverlay.addChild(menuBgImg);
// Semi-transparent overlay for menu text readability
var menuBg = LK.getAsset('healthbar_bg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 2732,
color: 0x000000
});
menuBg.alpha = 0.7;
mainMenuOverlay.addChild(menuBg);
// Game title
var titleText = new Text2('FIGHTER DUEL', {
size: 180,
fill: "#fff",
stroke: 0x000000,
strokeThickness: 16
});
titleText.anchor.set(0.5, 0);
titleText.x = 2048 / 2;
titleText.y = 420;
mainMenuOverlay.addChild(titleText);
// Subtitle
var subtitleText = new Text2('Touch to Start', {
size: 90,
fill: 0xFFE066,
stroke: 0x000000,
strokeThickness: 8
});
subtitleText.anchor.set(0.5, 0);
subtitleText.x = 2048 / 2;
subtitleText.y = 700;
mainMenuOverlay.addChild(subtitleText);
// Simple instructions
var instrText = new Text2('Move, Jump, Attack, Dodge, Throw!\nFirst to 2 wins!', {
size: 60,
fill: "#fff",
stroke: 0x000000,
strokeThickness: 6
});
instrText.anchor.set(0.5, 0);
instrText.x = 2048 / 2;
instrText.y = 900;
mainMenuOverlay.addChild(instrText);
// Add overlay to game
game.addChild(mainMenuOverlay);
// Block game start until menu is dismissed
var gameStarted = false;
// Hide all gameplay UI elements until menu is dismissed
function setGameplayUIVisible(visible) {
// Hide or show all gameplay UI elements
if (typeof fighter1 !== "undefined" && fighter1 && fighter1.healthBar) {
fighter1.healthBar.visible = visible;
}
if (typeof fighter2 !== "undefined" && fighter2 && fighter2.healthBar) {
fighter2.healthBar.visible = visible;
}
if (typeof comboText1 !== "undefined") comboText1.visible = false;
if (typeof comboText2 !== "undefined") comboText2.visible = false;
if (typeof timerText !== "undefined") timerText.visible = visible;
if (typeof throwablesText !== "undefined") throwablesText.visible = visible;
if (typeof spcCooldownOverlay !== "undefined") spcCooldownOverlay.visible = false;
if (typeof spcCooldownText !== "undefined") spcCooldownText.visible = false;
if (typeof spcCooldownCounter !== "undefined") spcCooldownCounter.visible = false;
if (typeof dodgeCooldownOverlay !== "undefined") dodgeCooldownOverlay.visible = false;
if (typeof dodgeCooldownText !== "undefined") dodgeCooldownText.visible = false;
if (typeof dodgeCooldownCounter !== "undefined") dodgeCooldownCounter.visible = false;
if (typeof btnLeft !== "undefined") btnLeft.visible = visible;
if (typeof btnRight !== "undefined") btnRight.visible = visible;
if (typeof btnAtk !== "undefined") btnAtk.visible = visible;
if (typeof btnJump !== "undefined") btnJump.visible = visible;
if (typeof btnSpc !== "undefined") btnSpc.visible = visible;
if (typeof btnThrow !== "undefined") btnThrow.visible = visible;
if (typeof btnDodge !== "undefined") btnDodge.visible = visible;
if (typeof arrowAbovePlayer1 !== "undefined") arrowAbovePlayer1.visible = visible;
// Hide ground tiles
if (typeof groundTiles !== "undefined") {
for (var i = 0; i < groundTiles.length; i++) {
if (groundTiles[i]) groundTiles[i].visible = visible;
}
}
// Do NOT hide arena or fighters here; they should always remain visible except for main menu/game over
// Do NOT hide ground, healthbars, buttons, or any gameplay elements during countdown; only hide for main menu/game over
}
// Hide gameplay UI at start
setGameplayUIVisible(false);
// Dismiss menu on any touch/click
mainMenuOverlay.down = function () {
mainMenuOverlay.visible = false;
// Do NOT hide any gameplay elements (ground, healthbars, buttons, etc.) during countdown; only hide overlays for main menu/game over
// All gameplay elements remain visible during countdown
setGameplayUIVisible(true); // Show all gameplay UI elements during countdown
// Countdown overlay
var countdownOverlay = new Container();
countdownOverlay.visible = true;
// Large countdown text
var countdownText = new Text2('3', {
size: 320,
fill: "#fff",
stroke: 0x000000,
strokeThickness: 18
});
countdownText.anchor.set(0.5, 0.5);
countdownText.x = 2048 / 2;
countdownText.y = 1200;
countdownOverlay.addChild(countdownText);
game.addChild(countdownOverlay);
var countdownValue = 3;
countdownText.setText(countdownValue);
// Countdown logic
var countdownInterval = LK.setInterval(function () {
countdownValue--;
if (countdownValue > 0) {
countdownText.setText(countdownValue);
} else if (countdownValue === 0) {
countdownText.setText("FIGHT!");
// Animate FIGHT! (scale up and fade out)
countdownText.scaleX = countdownText.scaleY = 1.0;
countdownText.alpha = 1.0;
tween(countdownText, {
scaleX: 1.4,
scaleY: 1.4,
alpha: 0
}, {
duration: 700,
onFinish: function onFinish() {
countdownOverlay.visible = false;
if (countdownOverlay.parent) countdownOverlay.parent.removeChild(countdownOverlay);
setGameplayUIVisible(true);
gameStarted = true;
}
});
LK.clearInterval(countdownInterval);
}
}, 900);
};
// --- Arena setup ---
//Note game dimensions are 2048x2732
//Minimalistic tween library which should be used for animations over time, including tinting / colouring an object, scaling, rotating, or changing any game object property.
// Unique asset for player1
var arena = new Arena();
arena.x = 0;
arena.y = 0;
game.addChild(arena);
// --- Fixed ground platform (visual) ---
var groundHeight = 60;
var groundTileWidth = LK.getAsset('ground', {
anchorX: 0,
anchorY: 0
}).width;
var groundY = arena.groundY - 100; // Move ground even further down by reducing the offset (was -300)
var groundTiles = [];
var numGroundTiles = Math.ceil(2048 / groundTileWidth); // Only enough to fill the screen
for (var i = 0; i < numGroundTiles; i++) {
var groundTile = LK.getAsset('ground', {
anchorX: 0,
anchorY: 0,
x: i * groundTileWidth,
y: groundY
});
game.addChild(groundTile);
groundTiles.push(groundTile);
}
// No infinite scrolling, ground is fixed and does not move
// --- Fighters ---
var fighter1 = new Fighter();
fighter1.isPlayer1 = true; // Mark as player1 for asset selection
fighter1.lastJumpPressed = false;
fighter1.lastHitTime = -9999; // Track last time this fighter hit the other
fighter1.comboCount = 0; // Current combo count
var fighter2 = new Fighter();
fighter2.lastHitTime = -9999;
fighter2.comboCount = 0;
// Place fighters on opposite sides
fighter1.x = 600;
fighter1.y = arena.groundY;
fighter1.setFacing(true);
fighter2.x = 2048 - 600;
fighter2.y = arena.groundY;
fighter2.setFacing(false);
game.addChild(fighter1);
game.addChild(fighter2);
// --- Arrow above controlled player (fighter1) ---
var arrowAbovePlayer1 = LK.getAsset('btn_jump', {
anchorX: 0.5,
anchorY: 1.0,
x: 0,
y: 0,
scaleX: 0.7,
scaleY: 0.7
});
arrowAbovePlayer1.rotation = Math.PI; // Point downwards
game.addChild(arrowAbovePlayer1);
// --- Health bar GUI ---
// Removed number healthbars (Text2 healthBar1 and healthBar2)
// Move the box healthbars (fighter healthBar containers) to a more visible, symmetric place below the timer at the top center
// Remove from fighter containers and add to GUI, position symmetrically below timer
fighter1.removeChild(fighter1.healthBar);
fighter2.removeChild(fighter2.healthBar);
// Place both healthbars below the timer, offset horizontally from center
fighter1.healthBar.x = -550;
fighter1.healthBar.y = 170;
fighter2.healthBar.x = 250;
fighter2.healthBar.y = 170;
LK.gui.top.addChild(fighter1.healthBar);
LK.gui.top.addChild(fighter2.healthBar);
// --- Round win counters (displayed on each side) ---
var winsText1 = new Text2('0', {
size: 120,
fill: "#fff",
stroke: 0x000000,
strokeThickness: 12
});
winsText1.anchor.set(1, 0); // right aligned, top
// Bring win counters closer together so they can be seen
winsText1.x = fighter1.healthBar.x + 180; // closer to center
winsText1.y = fighter1.healthBar.y + 240; // moved even further down below health bar
var winsText2 = new Text2('0', {
size: 120,
fill: "#fff",
stroke: 0x000000,
strokeThickness: 12
});
winsText2.anchor.set(0, 0); // left aligned, top
winsText2.x = fighter2.healthBar.x + 120; // closer to center
winsText2.y = fighter2.healthBar.y + 240; // moved even further down below health bar
LK.gui.top.addChild(winsText1);
LK.gui.top.addChild(winsText2);
// --- Combo counters under health bars ---
var comboText1 = new Text2('Combo: 0', {
size: 48,
fill: 0xFF0000,
stroke: 0xffffff,
strokeThickness: 8
});
comboText1.anchor.set(0.5, 0);
comboText1.x = fighter1.healthBar.x + 150; // center under health bar (300px wide)
comboText1.y = fighter1.healthBar.y + 60; // just below health bar
var comboText2 = new Text2('Combo: 0', {
size: 48,
fill: 0xFF0000,
stroke: 0xffffff,
strokeThickness: 8
});
comboText2.anchor.set(0.5, 0);
comboText2.x = fighter2.healthBar.x + 150;
comboText2.y = fighter2.healthBar.y + 60;
LK.gui.top.addChild(comboText1);
LK.gui.top.addChild(comboText2);
// Combo timer variables (in frames, 1.5s = 90 frames)
var comboTimer1 = 0;
var comboTimer2 = 0;
// --- Round and timer ---
var round = 1;
var maxRounds = 3;
var wins1 = 0;
var wins2 = 0;
var roundTime = 60 * 30; // 30 seconds at 60fps
var roundTimer = roundTime;
var timerText = new Text2('30', {
size: 80,
fill: "#fff"
});
timerText.anchor.set(0.5, 0);
LK.gui.top.addChild(timerText);
// --- Touch controls (mobile-friendly) ---
var leftPressed = false,
rightPressed = false,
atkPressed = false,
jumpPressed = false,
spcPressed = false,
throwPressed = false,
dodgePressed = false; // New for dodge
// Special attack cooldown overlay for button
var spcCooldownOverlay = LK.getAsset('healthbar_bg', {
anchorX: 0.5,
anchorY: 0.5,
width: 180,
height: 180,
color: 0x000000
});
spcCooldownOverlay.alpha = 0.5;
spcCooldownOverlay.visible = false;
// Move overlay to the special button's position
spcCooldownOverlay.x = 2048 - 950;
spcCooldownOverlay.y = 2450;
game.addChild(spcCooldownOverlay);
var spcCooldownText = new Text2('3', {
size: 90,
fill: "#fff"
});
spcCooldownText.anchor.set(0.5, 0.5);
// Move text to the special button's position
spcCooldownText.x = 2048 - 950;
spcCooldownText.y = 2450;
spcCooldownText.visible = false;
game.addChild(spcCooldownText);
// Special attack cooldown counter above the special attack button
var spcCooldownCounter = new Text2('', {
size: 60,
fill: "#fff"
});
spcCooldownCounter.anchor.set(0.5, 1.0);
// Place above the special button (btnSpc)
spcCooldownCounter.x = 2048 - 950;
spcCooldownCounter.y = 2450 - 120;
spcCooldownCounter.visible = false;
game.addChild(spcCooldownCounter);
// --- Dodge cooldown overlay and timer (like special) ---
// --- Dodge cooldown overlay and timer (like special) ---
// (Positioning is set after btnDodge is defined below)
var dodgeCooldownOverlay = LK.getAsset('healthbar_bg', {
anchorX: 0.5,
anchorY: 0.5,
width: 180,
height: 180,
color: 0x000000
});
dodgeCooldownOverlay.alpha = 0.5;
dodgeCooldownOverlay.visible = false;
game.addChild(dodgeCooldownOverlay);
var dodgeCooldownText = new Text2('3', {
size: 90,
fill: "#fff"
});
dodgeCooldownText.anchor.set(0.5, 0.5);
dodgeCooldownText.visible = false;
game.addChild(dodgeCooldownText);
var dodgeCooldownCounter = new Text2('', {
size: 60,
fill: "#fff"
});
dodgeCooldownCounter.anchor.set(0.5, 1.0);
dodgeCooldownCounter.visible = false;
game.addChild(dodgeCooldownCounter);
// Control buttons (simple rectangles for now)
var btnLeft = LK.getAsset('btn_left', {
anchorX: 0.5,
anchorY: 0.5,
x: 200,
y: 2600,
scaleX: 3.5,
scaleY: 3.5
});
var btnRight = LK.getAsset('btn_right', {
anchorX: 0.5,
anchorY: 0.5,
x: 500,
y: 2600,
scaleX: 3.5,
scaleY: 3.5
});
var btnAtk = LK.getAsset('btn_atk', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 - 650,
// moved farther left
y: 2600,
scaleX: 3.5,
scaleY: 3.5
});
var btnJump = LK.getAsset('btn_jump', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 - 200,
y: 2600,
scaleX: 3.5,
scaleY: 3.5
});
var btnSpc = LK.getAsset('btn_spc', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 - 950,
// moved farther left
y: 2450,
scaleX: 3.5,
scaleY: 3.5
});
// Add dodge button (reuse btn_right asset, place above the throw button, lower opacity)
var btnDodge = LK.getAsset('btn_right', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 - 200,
y: 1880,
// moved farther up
scaleX: 3.5,
scaleY: 3.5
});
btnDodge.alpha = 0.7;
// Add throw button (use unique btn_throw asset, place visually below the dodge button)
var btnThrow = LK.getAsset('btn_throw', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 - 200,
y: 2200,
// moved farther up
scaleX: 3.5,
scaleY: 3.5
});
game.addChild(btnLeft);
game.addChild(btnRight);
game.addChild(btnAtk);
game.addChild(btnJump);
game.addChild(btnSpc);
game.addChild(btnThrow);
game.addChild(btnDodge);
// Set dodge cooldown overlay and text positions now that btnDodge is defined
dodgeCooldownOverlay.x = btnDodge.x;
dodgeCooldownOverlay.y = btnDodge.y;
dodgeCooldownText.x = btnDodge.x;
dodgeCooldownText.y = btnDodge.y;
dodgeCooldownCounter.x = btnDodge.x;
dodgeCooldownCounter.y = btnDodge.y - 120;
// Button event helpers
btnLeft.down = function () {
leftPressed = true;
};
btnLeft.up = function () {
leftPressed = false;
};
btnRight.down = function () {
rightPressed = true;
};
btnRight.up = function () {
rightPressed = false;
};
btnAtk.down = function () {
atkPressed = true;
};
btnAtk.up = function () {
atkPressed = false;
};
btnJump.down = function () {
jumpPressed = true;
};
btnJump.up = function () {
jumpPressed = false;
};
btnSpc.down = function () {
spcPressed = true;
};
btnSpc.up = function () {
spcPressed = false;
};
btnThrow.down = function () {
throwPressed = true;
};
btnThrow.up = function () {
throwPressed = false;
};
btnDodge.down = function () {
dodgePressed = true;
};
btnDodge.up = function () {
dodgePressed = false;
};
// --- Attacks array ---
var attacks = [];
// --- Throwables counter and display ---
var throwablesLeft = 5;
var throwablesText = new Text2('Throwables: 5', {
size: 60,
fill: "#fff",
stroke: 0x000000,
strokeThickness: 8
});
throwablesText.anchor.set(0, 0); // Top left, but not in the 100x100 reserved area
throwablesText.x = 120;
throwablesText.y = 30;
LK.gui.top.addChild(throwablesText);
// --- Game update loop ---
game.update = function () {
// --- Main menu block: If not started, skip all gameplay logic ---
if (typeof gameStarted !== "undefined" && !gameStarted) {
// Animate subtitle (blink)
if (typeof subtitleText !== "undefined") {
subtitleText.alpha = 0.5 + 0.5 * Math.sin(LK.ticks / 20);
}
return;
}
// --- Timer ---
if (roundTimer > 0) {
roundTimer--;
timerText.setText(Math.ceil(roundTimer / 60));
}
// --- Update arrow position above fighter1 ---
if (typeof arrowAbovePlayer1 !== "undefined" && typeof fighter1 !== "undefined") {
arrowAbovePlayer1.x = fighter1.x;
// Place arrow 30px above the top of the fighter sprite (closer to player)
arrowAbovePlayer1.y = fighter1.y - (fighter1.height || 400) - 30;
}
// --- Ground is fixed, no update needed ---
// --- Player 1 controls (left side) ---
if (!fighter1.immobile) {
if (typeof fighter1.isMoving === "undefined") fighter1.isMoving = false;
if (typeof fighter1.lastMoving === "undefined") fighter1.lastMoving = false;
fighter1.isMoving = leftPressed || rightPressed;
// Removed mov sound on movement for player
if (leftPressed) {
fighter1.x = Math.max(120, fighter1.x - fighter1.moveSpeed);
fighter1.setFacing(false);
}
if (rightPressed) {
fighter1.x = Math.min(2048 - 120, fighter1.x + fighter1.moveSpeed);
fighter1.setFacing(true);
}
fighter1.lastMoving = fighter1.isMoving;
// Only trigger jump on the frame the button is pressed (rising edge)
if (jumpPressed && !fighter1.lastJumpPressed) {
fighter1.jump();
}
// Only trigger dodge on the frame the button is pressed (rising edge)
if (dodgePressed && !fighter1.lastDodgePressed) {
fighter1.dodge();
}
if (atkPressed) {
fighter1.attack();
}
if (throwPressed && typeof fighter1.throwObject === "function") {
if (throwablesLeft > 0) {
fighter1.throwObject();
}
}
if (spcPressed) {
if (fighter1.isSpecialReady) {
fighter1.special();
}
}
}
fighter1.lastJumpPressed = jumpPressed;
fighter1.lastDodgePressed = dodgePressed;
// --- Special attack cooldown overlay update ---
if (!fighter1.isSpecialReady) {
spcCooldownOverlay.visible = true;
spcCooldownText.visible = true;
var secondsLeft = Math.ceil(fighter1.specialTimer / 60);
spcCooldownText.setText(secondsLeft);
// Show and update the counter above the special button
spcCooldownCounter.visible = true;
spcCooldownCounter.setText(secondsLeft + "s");
} else {
spcCooldownOverlay.visible = false;
spcCooldownText.visible = false;
spcCooldownCounter.visible = false;
}
// --- Dodge cooldown overlay update ---
if (fighter1.dodgeCooldownTimer > 0) {
dodgeCooldownOverlay.visible = true;
dodgeCooldownText.visible = true;
var dodgeSecondsLeft = Math.ceil(fighter1.dodgeCooldownTimer / 60);
dodgeCooldownText.setText(dodgeSecondsLeft);
dodgeCooldownCounter.visible = true;
dodgeCooldownCounter.setText(dodgeSecondsLeft + "s");
} else {
dodgeCooldownOverlay.visible = false;
dodgeCooldownText.visible = false;
dodgeCooldownCounter.visible = false;
}
// --- AI for fighter2 (improved logic: better approach, attack, dodge, and use abilities) ---
if (!fighter2.immobile) {
var dx = fighter1.x - fighter2.x;
var dy = fighter1.y - fighter2.y;
var dist = Math.sqrt(dx * dx + dy * dy);
// --- Dodge logic: more reactive and predictive ---
var shouldDodge = false;
// Dodge if player is attacking and close, or if a projectile is incoming
var incomingProjectile = false;
for (var i = 0; i < attacks.length; i++) {
var atk = attacks[i];
if (atk instanceof Projectile && atk.owner === fighter1 && !atk.hit && !atk.destroyed) {
// Predict if projectile will hit soon (within 80px horizontally and 200px vertically)
if (Math.abs(atk.x - fighter2.x) < 80 && Math.abs(atk.y - fighter2.y) < 200) {
incomingProjectile = true;
break;
}
}
}
// If player is attacking and close, or projectile incoming, or sometimes randomly
if (!fighter2.isDodging && fighter2.dodgeCooldownTimer === 0 && !fighter2.isJumping && (Math.abs(dx) < 350 && fighter1.isAttacking && Math.random() < 0.35 || incomingProjectile && Math.random() < 0.7 || Math.random() < 0.008)) {
shouldDodge = true;
}
if (shouldDodge) {
fighter2.dodge();
}
// --- Move toward player with smarter spacing ---
if (typeof fighter2.isMoving === "undefined") fighter2.isMoving = false;
if (typeof fighter2.lastMoving === "undefined") fighter2.lastMoving = false;
fighter2.isMoving = false;
// Try to keep optimal distance (attack range)
var optimalMin = 140,
optimalMax = 220;
if (Math.abs(dx) > optimalMax) {
// Approach player
if (dx < 0) {
fighter2.x -= fighter2.moveSpeed * 0.9;
fighter2.setFacing(false);
fighter2.isMoving = true;
} else {
fighter2.x += fighter2.moveSpeed * 0.9;
fighter2.setFacing(true);
fighter2.isMoving = true;
}
} else if (Math.abs(dx) < optimalMin) {
// Back away to avoid being too close
if (dx < 0) {
fighter2.x += fighter2.moveSpeed * 0.7;
fighter2.setFacing(true);
fighter2.isMoving = true;
} else {
fighter2.x -= fighter2.moveSpeed * 0.7;
fighter2.setFacing(false);
fighter2.isMoving = true;
}
}
// --- Attack logic: attack if in range and not on cooldown ---
if (Math.abs(dx) >= optimalMin && Math.abs(dx) <= optimalMax && !fighter2.isAttacking && !fighter2.isDodging && Math.random() < 0.22) {
fighter2.attack();
}
// --- Jump logic: jump if player jumps, or to avoid projectiles, or randomly ---
if (!fighter2.isJumping && (fighter1.isJumping && Math.abs(dx) < 300 && Math.random() < 0.25 || incomingProjectile && Math.random() < 0.25 || Math.random() < 0.006)) {
fighter2.jump();
}
// --- Special attack: use if ready and player is in range, or randomly ---
if (fighter2.isSpecialReady && Math.abs(dx) < 350 && Math.random() < 0.08) {
fighter2.special();
}
// --- Throw object if available and player is at mid/long range ---
if (typeof fighter2.throwablesLeft === "undefined") {
fighter2.throwablesLeft = 5;
}
if (typeof fighter2.lastProjectileTick === "undefined" || LK.ticks - fighter2.lastProjectileTick >= 30) {
if (fighter2.throwablesLeft > 0 && Math.abs(dx) > 400 && Math.abs(dx) < 1200 && Math.random() < 0.13) {
fighter2.throwablesLeft--;
fighter2.lastProjectileTick = LK.ticks;
var proj = new Projectile(fighter2, fighter2.isFacingRight);
proj.x = fighter2.x + (fighter2.isFacingRight ? 120 : -120);
proj.y = fighter2.y - 180;
attacks.push(proj);
game.addChild(proj);
}
}
fighter2.lastMoving = fighter2.isMoving;
}
// --- Update fighters ---
fighter1.update();
fighter2.update();
// --- Update attacks ---
for (var i = attacks.length - 1; i >= 0; i--) {
var atk = attacks[i];
// Store last position for possible future use (e.g. for projectiles)
if (typeof atk.lastX === "undefined") atk.lastX = atk.x;
if (typeof atk.lastY === "undefined") atk.lastY = atk.y;
atk.update();
// Remove if destroyed
if (atk.destroyed) {
attacks.splice(i, 1);
continue;
}
// Set owner if not set
if (!atk.owner) {
atk.setOwner(atk.isSpecial ? atk.x < 2048 / 2 ? fighter1 : fighter2 : fighter1);
}
// Handle collision with the other fighter
var target = atk.owner === fighter1 ? fighter2 : fighter1;
// If target is dodging and invulnerable, skip hit
if (target.dodgeInvuln) {
atk.lastX = atk.x;
atk.lastY = atk.y;
continue;
}
// Only trigger on the exact frame of collision (rising edge)
var wasIntersecting = atk.lastWasIntersecting || false;
var isIntersecting = atk.intersects(target);
if (!wasIntersecting && isIntersecting && !atk.hit) {
// Combo logic: check if this hit is within 1.5 seconds (90 frames) of the last hit by this attacker
var nowTick = LK.ticks || 0;
if (typeof atk.owner.lastHitTime === "undefined") atk.owner.lastHitTime = -9999;
if (typeof atk.owner.comboCount === "undefined") atk.owner.comboCount = 0;
var prevCombo = atk.owner.comboCount;
if (nowTick - atk.owner.lastHitTime <= 90) {
atk.owner.comboCount++;
// Start combo timer for this fighter
if (atk.owner === fighter1 && atk.owner.comboCount > prevCombo) {
comboTimer1 = 90;
}
if (atk.owner === fighter2 && atk.owner.comboCount > prevCombo) {
comboTimer2 = 90;
}
// Impact effect on combo text when combo increases
if (atk.owner === fighter1 && atk.owner.comboCount > prevCombo) {
comboText1.scaleX = comboText1.scaleY = 1.3;
// Impact scale
tween(comboText1, {
scaleX: 1,
scaleY: 1
}, {
duration: 180
});
// Shake effect
var shakeTimes = 10;
var shakeMagnitude = 18;
var shakeDuration = 180;
var shakeStep = Math.floor(shakeDuration / shakeTimes);
var origX = comboText1.x;
var origY = comboText1.y;
for (var s = 0; s < shakeTimes; s++) {
(function (s) {
LK.setTimeout(function () {
// Alternate shake direction
var dx = (s % 2 === 0 ? 1 : -1) * shakeMagnitude * (1 - s / shakeTimes);
var dy = (s % 2 === 0 ? -1 : 1) * shakeMagnitude * 0.5 * (1 - s / shakeTimes);
comboText1.x = origX + dx;
comboText1.y = origY + dy;
// Restore at end
if (s === shakeTimes - 1) {
LK.setTimeout(function () {
comboText1.x = origX;
comboText1.y = origY;
}, shakeStep);
}
}, s * shakeStep);
})(s);
}
} else if (atk.owner === fighter2 && atk.owner.comboCount > prevCombo) {
comboText2.scaleX = comboText2.scaleY = 1.3;
tween(comboText2, {
scaleX: 1,
scaleY: 1
}, {
duration: 180
});
// Shake effect
var shakeTimes2 = 10;
var shakeMagnitude2 = 18;
var shakeDuration2 = 180;
var shakeStep2 = Math.floor(shakeDuration2 / shakeTimes2);
var origX2 = comboText2.x;
var origY2 = comboText2.y;
for (var s2 = 0; s2 < shakeTimes2; s2++) {
(function (s2) {
LK.setTimeout(function () {
var dx2 = (s2 % 2 === 0 ? 1 : -1) * shakeMagnitude2 * (1 - s2 / shakeTimes2);
var dy2 = (s2 % 2 === 0 ? -1 : 1) * shakeMagnitude2 * 0.5 * (1 - s2 / shakeTimes2);
comboText2.x = origX2 + dx2;
comboText2.y = origY2 + dy2;
if (s2 === shakeTimes2 - 1) {
LK.setTimeout(function () {
comboText2.x = origX2;
comboText2.y = origY2;
}, shakeStep2);
}
}, s2 * shakeStep2);
})(s2);
}
}
} else {
atk.owner.comboCount = 1;
// Start combo timer for this fighter
if (atk.owner === fighter1 && atk.owner.comboCount > prevCombo) {
comboTimer1 = 90;
}
if (atk.owner === fighter2 && atk.owner.comboCount > prevCombo) {
comboTimer2 = 90;
}
// Impact effect on combo text when combo increases from 0
if (atk.owner === fighter1 && atk.owner.comboCount > prevCombo) {
comboText1.scaleX = comboText1.scaleY = 1.3;
tween(comboText1, {
scaleX: 1,
scaleY: 1
}, {
duration: 180
});
// Shake effect
var shakeTimes = 10;
var shakeMagnitude = 18;
var shakeDuration = 180;
var shakeStep = Math.floor(shakeDuration / shakeTimes);
var origX = comboText1.x;
var origY = comboText1.y;
for (var s = 0; s < shakeTimes; s++) {
(function (s) {
LK.setTimeout(function () {
var dx = (s % 2 === 0 ? 1 : -1) * shakeMagnitude * (1 - s / shakeTimes);
var dy = (s % 2 === 0 ? -1 : 1) * shakeMagnitude * 0.5 * (1 - s / shakeTimes);
comboText1.x = origX + dx;
comboText1.y = origY + dy;
if (s === shakeTimes - 1) {
LK.setTimeout(function () {
comboText1.x = origX;
comboText1.y = origY;
}, shakeStep);
}
}, s * shakeStep);
})(s);
}
} else if (atk.owner === fighter2 && atk.owner.comboCount > prevCombo) {
comboText2.scaleX = comboText2.scaleY = 1.3;
tween(comboText2, {
scaleX: 1,
scaleY: 1
}, {
duration: 180
});
// Shake effect
var shakeTimes2 = 10;
var shakeMagnitude2 = 18;
var shakeDuration2 = 180;
var shakeStep2 = Math.floor(shakeDuration2 / shakeTimes2);
var origX2 = comboText2.x;
var origY2 = comboText2.y;
for (var s2 = 0; s2 < shakeTimes2; s2++) {
(function (s2) {
LK.setTimeout(function () {
var dx2 = (s2 % 2 === 0 ? 1 : -1) * shakeMagnitude2 * (1 - s2 / shakeTimes2);
var dy2 = (s2 % 2 === 0 ? -1 : 1) * shakeMagnitude2 * 0.5 * (1 - s2 / shakeTimes2);
comboText2.x = origX2 + dx2;
comboText2.y = origY2 + dy2;
if (s2 === shakeTimes2 - 1) {
LK.setTimeout(function () {
comboText2.x = origX2;
comboText2.y = origY2;
}, shakeStep2);
}
}, s2 * shakeStep2);
})(s2);
}
}
}
atk.owner.lastHitTime = nowTick;
// Knockback direction: +1 if attacker is facing right, -1 if left
var knockbackDir = atk.owner && atk.owner.isFacingRight ? 1 : -1;
// Knockback strength: special attacks knock back SIGNIFICANTLY more (much stronger effect)
var knockbackStrength = atk.isSpecial ? 480 : 64;
target.takeDamage(atk.damage, knockbackDir, knockbackStrength);
// If hit by a special attack, make target immobile for 1 second (60 frames)
if (atk.isSpecial) {
target.immobile = true;
LK.setTimeout(function () {
target.immobile = false;
}, 1000);
}
atk.hit = true;
atk.destroy();
// Flash on hit
LK.effects.flashObject(target, 0xff0000, 200);
// Show a visible attack effect at the hit location, rotated to match the attack direction
var hitEffect;
if (typeof Projectile !== "undefined" && atk instanceof Projectile) {
// Unique projectile hit effect: use btn_throw asset, smaller, quick fade
hitEffect = LK.getAsset('btn_throw', {
anchorX: 0.5,
anchorY: 0.5,
x: target.x,
y: target.y - 200
});
hitEffect.rotation = 0;
game.addChild(hitEffect);
hitEffect.scaleX = hitEffect.scaleY = 1.0;
hitEffect.alpha = 1;
tween(hitEffect, {
scaleX: 1.7,
scaleY: 1.7,
alpha: 0
}, {
duration: 220,
onFinish: function onFinish() {
if (hitEffect && hitEffect.destroy) hitEffect.destroy();
}
});
} else {
// Regular or special attack effect
hitEffect = LK.getAsset(atk.isSpecial ? 'special_attack' : 'attack', {
anchorX: 0.5,
anchorY: 0.5,
x: target.x,
y: target.y - 200
});
// Rotate the effect to match the direction the attacker is facing
if (atk.owner && atk.owner.isFacingRight === false) {
hitEffect.rotation = Math.PI; // Face left
} else {
hitEffect.rotation = 0; // Face right (default)
}
game.addChild(hitEffect);
// Animate the effect: scale up and fade out, then destroy
hitEffect.scaleX = hitEffect.scaleY = 1.2;
hitEffect.alpha = 1;
tween(hitEffect, {
scaleX: 2.0,
scaleY: 2.0,
alpha: 0
}, {
duration: 350,
onFinish: function onFinish() {
if (hitEffect && hitEffect.destroy) hitEffect.destroy();
}
});
}
}
// Update last intersection state for next frame
atk.lastWasIntersecting = isIntersecting;
atk.lastX = atk.x;
atk.lastY = atk.y;
}
// --- Health bar updates ---
// Removed number healthbars update
// --- Combo reset logic: reset combo if 1.5 seconds (90 frames) have passed since last hit ---
var nowTick = LK.ticks || 0;
if (nowTick - fighter1.lastHitTime > 90) {
fighter1.comboCount = 0;
}
if (nowTick - fighter2.lastHitTime > 90) {
fighter2.comboCount = 0;
}
// Decrement combo timers
if (comboTimer1 > 0) comboTimer1--;
if (comboTimer2 > 0) comboTimer2--;
// Update combo counters under health bars, only show if timer is active
if (comboTimer1 > 0 && fighter1.comboCount > 0) {
comboText1.visible = true;
comboText1.setText(('COMBO: ' + (fighter1.comboCount || 0) + '!').toUpperCase());
} else {
comboText1.visible = false;
}
if (comboTimer2 > 0 && fighter2.comboCount > 0) {
comboText2.visible = true;
comboText2.setText(('COMBO: ' + (fighter2.comboCount || 0) + '!').toUpperCase());
} else {
comboText2.visible = false;
}
// --- Win/lose/round logic ---
var roundOver = false;
var winner = null;
if (fighter1.health <= 0) {
wins2++;
roundOver = true;
winner = 2;
} else if (fighter2.health <= 0) {
wins1++;
roundOver = true;
winner = 1;
} else if (roundTimer <= 0) {
if (fighter1.health > fighter2.health) {
wins1++;
winner = 1;
} else if (fighter2.health > fighter1.health) {
wins2++;
winner = 2;
} else {
// Draw, no one gets a win
}
roundOver = true;
}
if (roundOver) {
// Update round win counters
if (typeof winsText1 !== "undefined") winsText1.setText(wins1 + '');
if (typeof winsText2 !== "undefined") winsText2.setText(wins2 + '');
if (wins1 >= 2) {
LK.showYouWin();
return;
} else if (wins2 >= 2) {
LK.showGameOver();
return;
}
// Next round
round++;
roundTimer = roundTime;
fighter1.health = fighter1.maxHealth;
fighter2.health = fighter2.maxHealth;
fighter1.updateHealthBar();
fighter2.updateHealthBar();
fighter1.x = 600;
fighter2.x = 2048 - 600;
fighter1.setFacing(true);
fighter2.setFacing(false);
// Reset throwables for both fighters
throwablesLeft = 5;
if (typeof throwablesText !== "undefined" && throwablesText.setText) {
throwablesText.setText('Throwables: ' + throwablesLeft);
}
fighter2.throwablesLeft = 5;
// Remove all attacks
for (var j = attacks.length - 1; j >= 0; j--) {
if (attacks[j].destroy) attacks[j].destroy();
}
attacks = [];
// --- Start countdown overlay for new round ---
setGameplayUIVisible(true); // Show all gameplay UI elements during countdown
// Countdown overlay
var countdownOverlay = new Container();
countdownOverlay.visible = true;
// Large countdown text
var countdownText = new Text2('3', {
size: 320,
fill: "#fff",
stroke: 0x000000,
strokeThickness: 18
});
countdownText.anchor.set(0.5, 0.5);
countdownText.x = 2048 / 2;
countdownText.y = 1200;
countdownOverlay.addChild(countdownText);
game.addChild(countdownOverlay);
var countdownValue = 3;
countdownText.setText(countdownValue);
// Pause gameplay during countdown
gameStarted = false;
var countdownInterval = LK.setInterval(function () {
countdownValue--;
if (countdownValue > 0) {
countdownText.setText(countdownValue);
} else if (countdownValue === 0) {
countdownText.setText("FIGHT!");
// Animate FIGHT! (scale up and fade out)
countdownText.scaleX = countdownText.scaleY = 1.0;
countdownText.alpha = 1.0;
tween(countdownText, {
scaleX: 1.4,
scaleY: 1.4,
alpha: 0
}, {
duration: 700,
onFinish: function onFinish() {
countdownOverlay.visible = false;
if (countdownOverlay.parent) countdownOverlay.parent.removeChild(countdownOverlay);
setGameplayUIVisible(true);
gameStarted = true;
}
});
LK.clearInterval(countdownInterval);
}
}, 900);
}
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// --- Arena class ---
// Provides a background and groundY property for fighter placement
var Arena = Container.expand(function () {
var self = Container.call(this);
// Add arena background image, anchored at top-left
var bg = self.attachAsset('arena_bg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
});
// Set groundY to a reasonable value for fighter placement
// Place ground near the bottom of the visible area, but above the very bottom
self.groundY = 2048 + 300; // 2048 is the base height, +300 for a bit above the bottom
return self;
});
// --- Attack class ---
// Represents a regular or special attack hitbox, with owner and damage
var Attack = Container.expand(function () {
var self = Container.call(this);
// Arguments: owner (Fighter), isSpecial (bool)
self.owner = arguments[0] || null;
self.isSpecial = !!arguments[1];
self.hit = false;
self.destroyed = false;
// Damage values
self.damage = self.isSpecial ? 24 : 8;
// Visual
var assetId = self.isSpecial ? 'special_attack' : 'attack';
var sprite = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0
});
// Set size for hitbox (use asset size)
self.width = sprite.width;
self.height = sprite.height;
// Set owner after creation if needed
self.setOwner = function (fighter) {
self.owner = fighter;
};
// Update: attacks last for a short time, then destroy
self.lifetime = self.isSpecial ? 30 : 18; // special lasts longer
self.update = function () {
if (self.destroyed) return;
self.lifetime--;
if (self.lifetime <= 0) {
self.destroy();
}
};
self.destroy = function () {
if (self.destroyed) return;
self.destroyed = true;
if (self.parent && self.parent.removeChild) {
self.parent.removeChild(self);
}
};
return self;
});
// --- Fighter class ---
// Handles player and AI fighter logic, health, attacks, movement, and health bar
var Fighter = Container.expand(function () {
var self = Container.call(this);
// --- Properties ---
self.maxHealth = 100;
self.health = self.maxHealth;
self.moveSpeed = 6; // Slower movement
self.jumpStrength = 32; // Lower jump
self.gravity = 3; // Slower fall
self.isJumping = false;
self.isAttacking = false;
self.isSpecialReady = true;
self.specialCooldown = 600; // 10 seconds at 60fps (slower special)
self.specialTimer = 0;
self.immobile = false;
self.isFacingRight = true;
self.attackCooldown = 120; // 2 seconds at 60fps (slower attack rate)
self.attackTimer = 0;
self.lastJumpPressed = false;
// --- Dodge mechanic ---
self.isDodging = false;
self.dodgeDuration = 30; // 0.5s at 60fps (slower dodge)
self.dodgeTimer = 0;
self.dodgeSpeed = 15; // Slower dodge movement
self.dodgeInvuln = false;
self.lastDodgePressed = false;
self.dodgeCooldown = 240; // 4s cooldown (slower dodge cooldown)
self.dodgeCooldownTimer = 0;
// Start dodge if not already dodging or on 3s cooldown
self.dodge = function () {
if (self.isDodging || self.immobile || self.dodgeCooldownTimer > 0 || self.isJumping) return;
self.isDodging = true;
self.dodgeTimer = self.dodgeDuration;
self.dodgeInvuln = true;
self.dodgeCooldownTimer = self.dodgeCooldown;
// Play dodge sound when dodge happens
var dodgeSound = LK.getSound('dodge');
if (dodgeSound) dodgeSound.play();
// Visual feedback: flash fighter blue for dodge duration
LK.effects.flashObject(self, 0x00aaff, self.dodgeDuration * 16);
};
// --- Visuals ---
// Use a different asset for player1 (controlled fighter)
var assetId = typeof self.isPlayer1 !== "undefined" && self.isPlayer1 === true ? 'fighter1' : 'fighter';
var sprite = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 1.0,
x: 0,
y: 0
});
// Health bar container
self.healthBar = new Container();
// Health bar outline (drawn behind the background)
var healthBarOutline = new Container();
var outlineThickness = 6;
var outlineW = 300 + outlineThickness * 2;
var outlineH = 50 + outlineThickness * 2;
var outlineRect = LK.getAsset('healthbar_bg', {
anchorX: 0,
anchorY: 0.5,
x: -outlineThickness,
y: 0,
width: outlineW,
height: outlineH,
color: 0xffffff // white outline
});
healthBarOutline.addChild(outlineRect);
self.healthBar.addChild(healthBarOutline);
// Health bar background
var healthBarBg = LK.getAsset('healthbar_bg', {
anchorX: 0,
anchorY: 0.5,
x: 0,
y: 0
});
self.healthBar.addChild(healthBarBg);
// Health bar foreground
var healthBarFg = LK.getAsset('healthbar_fg', {
anchorX: 0,
anchorY: 0.5,
x: 0,
y: 0
});
self.healthBar.addChild(healthBarFg);
self.healthBar.width = 300;
self.healthBar.height = 50;
self.healthBarFg = healthBarFg;
self.updateHealthBar = function () {
var pct = Math.max(0, self.health / self.maxHealth);
self.healthBarFg.width = 300 * pct;
};
self.updateHealthBar();
// --- Methods ---
self.setFacing = function (right) {
self.isFacingRight = right;
sprite.scaleX = right ? 1 : -1;
};
self.jump = function () {
if (!self.isJumping && !self.immobile) {
self.isJumping = true;
self.vy = -self.jumpStrength;
}
};
self.attack = function () {
if (self.isAttacking || self.immobile || self.attackTimer > 0) return;
self.isAttacking = true;
self.attackTimer = self.attackCooldown;
// Create attack object
var atk = new Attack(self, false);
atk.x = self.x + (self.isFacingRight ? 120 : -120);
atk.y = self.y - 200;
attacks.push(atk);
game.addChild(atk);
// Play regatt sound for regular attack with randomized volume
var regattSound = LK.getSound('regatt');
regattSound.volume = 0.2 + Math.random() * 0.8; // random volume between 0.2 and 1.0
regattSound.play();
// End attack after short delay
LK.setTimeout(function () {
self.isAttacking = false;
}, 300);
};
self.special = function () {
if (!self.isSpecialReady || self.immobile) return;
self.isSpecialReady = false;
self.specialTimer = self.specialCooldown;
// Create special attack object
var atk = new Attack(self, true);
atk.x = self.x + (self.isFacingRight ? 180 : -180);
atk.y = self.y - 200;
attacks.push(atk);
game.addChild(atk);
// Play spatt sound for special attack
var spattSound = LK.getSound('spatt');
spattSound.play();
};
self.takeDamage = function (amount, dir, knockback) {
if (self.immobile) return;
self.health -= amount;
self.updateHealthBar();
// Flash health bar foreground red for 200ms when taking damage
LK.effects.flashObject(self.healthBarFg, 0xff0000, 200);
// --- Blood particle effect on damage ---
for (var i = 0; i < 8; i++) {
var blood = LK.getAsset('blood_particle', {
anchorX: 0.5,
anchorY: 0.5,
x: self.x + (Math.random() - 0.5) * 80,
y: self.y - (self.height || 400) + (Math.random() - 0.5) * 40,
width: 30 + Math.random() * 20,
height: 18 + Math.random() * 10
});
blood.alpha = 0.7 + Math.random() * 0.3;
if (self.parent && self.parent.addChild) self.parent.addChild(blood);
// Animate blood: random direction, fade out, fall down
var dx = (Math.random() - 0.5) * 120;
var dy = 80 + Math.random() * 120;
tween(blood, {
x: blood.x + dx,
y: blood.y + dy,
alpha: 0
}, {
duration: 120 + Math.random() * 60,
// much faster animation
onFinish: function (b) {
return function () {
if (b && b.destroy) b.destroy();
};
}(blood)
});
}
// Knockback
self.x += (dir || 1) * (knockback || 32);
// Clamp to arena
self.x = Math.max(120, Math.min(2048 - 120, self.x));
};
self.update = function () {
// Dodge cooldown
if (self.dodgeCooldownTimer > 0) {
self.dodgeCooldownTimer--;
}
// Dodge logic
if (self.isDodging) {
// Move quickly in facing direction
var dir = self.isFacingRight ? 1 : -1;
self.x += dir * self.dodgeSpeed;
// Clamp to arena
self.x = Math.max(120, Math.min(2048 - 120, self.x));
self.dodgeTimer--;
if (self.dodgeTimer <= 0) {
self.isDodging = false;
self.dodgeInvuln = false;
}
}
// Attack cooldown
if (self.attackTimer > 0) {
self.attackTimer--;
}
// Special cooldown
if (!self.isSpecialReady) {
if (self.specialTimer > 0) {
self.specialTimer--;
}
if (self.specialTimer <= 0) {
self.isSpecialReady = true;
}
}
// Jumping physics
if (self.isJumping) {
if (typeof self.vy === "undefined") self.vy = 0;
// Track lastY for landing detection
if (typeof self.lastY === "undefined") self.lastY = self.y;
self.y += self.vy;
self.vy += self.gravity;
// Land on ground (detect landing this frame)
if (self.lastY < arena.groundY && self.y >= arena.groundY) {
self.y = arena.groundY;
self.isJumping = false;
self.vy = 0;
// Play mov sound when landing from a jump
var movSound = LK.getSound('mov');
if (movSound) movSound.play();
}
self.lastY = self.y;
}
};
// --- Throw mechanic ---
self.throwObject = function () {
if (self.immobile || self.isDodging || self.isAttacking || self.isJumping) return;
// Only allow one projectile at a time per fighter (optional, can remove for spam)
if (typeof self.lastProjectileTick !== "undefined" && LK.ticks - self.lastProjectileTick < 30) return;
if (typeof throwablesLeft !== "undefined" && throwablesLeft > 0) {
throwablesLeft--;
if (typeof throwablesText !== "undefined" && throwablesText.setText) {
throwablesText.setText('Throwables: ' + throwablesLeft);
}
}
self.lastProjectileTick = LK.ticks;
var proj = new Projectile(self, self.isFacingRight);
proj.x = self.x + (self.isFacingRight ? 120 : -120);
proj.y = self.y - 180;
// Play throw sound when projectile is spawned
var throwSound = LK.getSound('throw');
if (throwSound) throwSound.play();
attacks.push(proj);
game.addChild(proj);
};
return self;
});
// --- Projectile class ---
// Represents a thrown projectile (e.g. shuriken, fireball)
var Projectile = Container.expand(function () {
var self = Container.call(this);
// Arguments: owner (Fighter), isRight (bool)
self.owner = arguments[0] || null;
self.isRight = !!arguments[1];
self.hit = false;
self.destroyed = false;
self.damage = 6;
self.speed = 16; // Slower projectile
self.lifetime = 90; // Lasts longer (1.5 seconds)
// Visual
var sprite = self.attachAsset('btn_throw', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0
});
self.width = sprite.width;
self.height = sprite.height;
// Set owner after creation if needed
self.setOwner = function (fighter) {
self.owner = fighter;
};
self.update = function () {
if (self.destroyed) return;
// --- Trail effect: spawn faded throw asset at current position ---
// Only spawn trail every 2 frames for performance
if (typeof self._trailTick === "undefined") self._trailTick = 0;
self._trailTick++;
if (self._trailTick % 2 === 0) {
var trail = LK.getAsset('btn_throw', {
anchorX: 0.5,
anchorY: 0.5,
x: self.x,
y: self.y,
scaleX: 0.7,
scaleY: 0.7
});
trail.alpha = 0.35;
if (self.parent && self.parent.addChild) self.parent.addChild(trail);
// Fade out and destroy after 180ms
tween(trail, {
alpha: 0
}, {
duration: 180,
onFinish: function onFinish() {
if (trail && trail.destroy) trail.destroy();
}
});
}
// Move in direction
self.x += self.isRight ? self.speed : -self.speed;
self.lifetime--;
// Remove if out of bounds or expired
if (self.x < -200 || self.x > 2048 + 200 || self.lifetime <= 0) {
self.destroy();
}
};
self.destroy = function () {
if (self.destroyed) return;
self.destroyed = true;
if (self.parent && self.parent.removeChild) {
self.parent.removeChild(self);
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// --- Main Menu Overlay ---
// Play background music at game start with reduced volume
LK.playMusic('music', {
volume: 0.3
});
var mainMenuOverlay = new Container();
mainMenuOverlay.visible = true; // Ensure menu is visible at game start
// Main menu background image (separate from arena)
var menuBgImg = LK.getAsset('arena_bg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 2732
});
mainMenuOverlay.addChild(menuBgImg);
// Semi-transparent overlay for menu text readability
var menuBg = LK.getAsset('healthbar_bg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 2732,
color: 0x000000
});
menuBg.alpha = 0.7;
mainMenuOverlay.addChild(menuBg);
// Game title
var titleText = new Text2('FIGHTER DUEL', {
size: 180,
fill: "#fff",
stroke: 0x000000,
strokeThickness: 16
});
titleText.anchor.set(0.5, 0);
titleText.x = 2048 / 2;
titleText.y = 420;
mainMenuOverlay.addChild(titleText);
// Subtitle
var subtitleText = new Text2('Touch to Start', {
size: 90,
fill: 0xFFE066,
stroke: 0x000000,
strokeThickness: 8
});
subtitleText.anchor.set(0.5, 0);
subtitleText.x = 2048 / 2;
subtitleText.y = 700;
mainMenuOverlay.addChild(subtitleText);
// Simple instructions
var instrText = new Text2('Move, Jump, Attack, Dodge, Throw!\nFirst to 2 wins!', {
size: 60,
fill: "#fff",
stroke: 0x000000,
strokeThickness: 6
});
instrText.anchor.set(0.5, 0);
instrText.x = 2048 / 2;
instrText.y = 900;
mainMenuOverlay.addChild(instrText);
// Add overlay to game
game.addChild(mainMenuOverlay);
// Block game start until menu is dismissed
var gameStarted = false;
// Hide all gameplay UI elements until menu is dismissed
function setGameplayUIVisible(visible) {
// Hide or show all gameplay UI elements
if (typeof fighter1 !== "undefined" && fighter1 && fighter1.healthBar) {
fighter1.healthBar.visible = visible;
}
if (typeof fighter2 !== "undefined" && fighter2 && fighter2.healthBar) {
fighter2.healthBar.visible = visible;
}
if (typeof comboText1 !== "undefined") comboText1.visible = false;
if (typeof comboText2 !== "undefined") comboText2.visible = false;
if (typeof timerText !== "undefined") timerText.visible = visible;
if (typeof throwablesText !== "undefined") throwablesText.visible = visible;
if (typeof spcCooldownOverlay !== "undefined") spcCooldownOverlay.visible = false;
if (typeof spcCooldownText !== "undefined") spcCooldownText.visible = false;
if (typeof spcCooldownCounter !== "undefined") spcCooldownCounter.visible = false;
if (typeof dodgeCooldownOverlay !== "undefined") dodgeCooldownOverlay.visible = false;
if (typeof dodgeCooldownText !== "undefined") dodgeCooldownText.visible = false;
if (typeof dodgeCooldownCounter !== "undefined") dodgeCooldownCounter.visible = false;
if (typeof btnLeft !== "undefined") btnLeft.visible = visible;
if (typeof btnRight !== "undefined") btnRight.visible = visible;
if (typeof btnAtk !== "undefined") btnAtk.visible = visible;
if (typeof btnJump !== "undefined") btnJump.visible = visible;
if (typeof btnSpc !== "undefined") btnSpc.visible = visible;
if (typeof btnThrow !== "undefined") btnThrow.visible = visible;
if (typeof btnDodge !== "undefined") btnDodge.visible = visible;
if (typeof arrowAbovePlayer1 !== "undefined") arrowAbovePlayer1.visible = visible;
// Hide ground tiles
if (typeof groundTiles !== "undefined") {
for (var i = 0; i < groundTiles.length; i++) {
if (groundTiles[i]) groundTiles[i].visible = visible;
}
}
// Do NOT hide arena or fighters here; they should always remain visible except for main menu/game over
// Do NOT hide ground, healthbars, buttons, or any gameplay elements during countdown; only hide for main menu/game over
}
// Hide gameplay UI at start
setGameplayUIVisible(false);
// Dismiss menu on any touch/click
mainMenuOverlay.down = function () {
mainMenuOverlay.visible = false;
// Do NOT hide any gameplay elements (ground, healthbars, buttons, etc.) during countdown; only hide overlays for main menu/game over
// All gameplay elements remain visible during countdown
setGameplayUIVisible(true); // Show all gameplay UI elements during countdown
// Countdown overlay
var countdownOverlay = new Container();
countdownOverlay.visible = true;
// Large countdown text
var countdownText = new Text2('3', {
size: 320,
fill: "#fff",
stroke: 0x000000,
strokeThickness: 18
});
countdownText.anchor.set(0.5, 0.5);
countdownText.x = 2048 / 2;
countdownText.y = 1200;
countdownOverlay.addChild(countdownText);
game.addChild(countdownOverlay);
var countdownValue = 3;
countdownText.setText(countdownValue);
// Countdown logic
var countdownInterval = LK.setInterval(function () {
countdownValue--;
if (countdownValue > 0) {
countdownText.setText(countdownValue);
} else if (countdownValue === 0) {
countdownText.setText("FIGHT!");
// Animate FIGHT! (scale up and fade out)
countdownText.scaleX = countdownText.scaleY = 1.0;
countdownText.alpha = 1.0;
tween(countdownText, {
scaleX: 1.4,
scaleY: 1.4,
alpha: 0
}, {
duration: 700,
onFinish: function onFinish() {
countdownOverlay.visible = false;
if (countdownOverlay.parent) countdownOverlay.parent.removeChild(countdownOverlay);
setGameplayUIVisible(true);
gameStarted = true;
}
});
LK.clearInterval(countdownInterval);
}
}, 900);
};
// --- Arena setup ---
//Note game dimensions are 2048x2732
//Minimalistic tween library which should be used for animations over time, including tinting / colouring an object, scaling, rotating, or changing any game object property.
// Unique asset for player1
var arena = new Arena();
arena.x = 0;
arena.y = 0;
game.addChild(arena);
// --- Fixed ground platform (visual) ---
var groundHeight = 60;
var groundTileWidth = LK.getAsset('ground', {
anchorX: 0,
anchorY: 0
}).width;
var groundY = arena.groundY - 100; // Move ground even further down by reducing the offset (was -300)
var groundTiles = [];
var numGroundTiles = Math.ceil(2048 / groundTileWidth); // Only enough to fill the screen
for (var i = 0; i < numGroundTiles; i++) {
var groundTile = LK.getAsset('ground', {
anchorX: 0,
anchorY: 0,
x: i * groundTileWidth,
y: groundY
});
game.addChild(groundTile);
groundTiles.push(groundTile);
}
// No infinite scrolling, ground is fixed and does not move
// --- Fighters ---
var fighter1 = new Fighter();
fighter1.isPlayer1 = true; // Mark as player1 for asset selection
fighter1.lastJumpPressed = false;
fighter1.lastHitTime = -9999; // Track last time this fighter hit the other
fighter1.comboCount = 0; // Current combo count
var fighter2 = new Fighter();
fighter2.lastHitTime = -9999;
fighter2.comboCount = 0;
// Place fighters on opposite sides
fighter1.x = 600;
fighter1.y = arena.groundY;
fighter1.setFacing(true);
fighter2.x = 2048 - 600;
fighter2.y = arena.groundY;
fighter2.setFacing(false);
game.addChild(fighter1);
game.addChild(fighter2);
// --- Arrow above controlled player (fighter1) ---
var arrowAbovePlayer1 = LK.getAsset('btn_jump', {
anchorX: 0.5,
anchorY: 1.0,
x: 0,
y: 0,
scaleX: 0.7,
scaleY: 0.7
});
arrowAbovePlayer1.rotation = Math.PI; // Point downwards
game.addChild(arrowAbovePlayer1);
// --- Health bar GUI ---
// Removed number healthbars (Text2 healthBar1 and healthBar2)
// Move the box healthbars (fighter healthBar containers) to a more visible, symmetric place below the timer at the top center
// Remove from fighter containers and add to GUI, position symmetrically below timer
fighter1.removeChild(fighter1.healthBar);
fighter2.removeChild(fighter2.healthBar);
// Place both healthbars below the timer, offset horizontally from center
fighter1.healthBar.x = -550;
fighter1.healthBar.y = 170;
fighter2.healthBar.x = 250;
fighter2.healthBar.y = 170;
LK.gui.top.addChild(fighter1.healthBar);
LK.gui.top.addChild(fighter2.healthBar);
// --- Round win counters (displayed on each side) ---
var winsText1 = new Text2('0', {
size: 120,
fill: "#fff",
stroke: 0x000000,
strokeThickness: 12
});
winsText1.anchor.set(1, 0); // right aligned, top
// Bring win counters closer together so they can be seen
winsText1.x = fighter1.healthBar.x + 180; // closer to center
winsText1.y = fighter1.healthBar.y + 240; // moved even further down below health bar
var winsText2 = new Text2('0', {
size: 120,
fill: "#fff",
stroke: 0x000000,
strokeThickness: 12
});
winsText2.anchor.set(0, 0); // left aligned, top
winsText2.x = fighter2.healthBar.x + 120; // closer to center
winsText2.y = fighter2.healthBar.y + 240; // moved even further down below health bar
LK.gui.top.addChild(winsText1);
LK.gui.top.addChild(winsText2);
// --- Combo counters under health bars ---
var comboText1 = new Text2('Combo: 0', {
size: 48,
fill: 0xFF0000,
stroke: 0xffffff,
strokeThickness: 8
});
comboText1.anchor.set(0.5, 0);
comboText1.x = fighter1.healthBar.x + 150; // center under health bar (300px wide)
comboText1.y = fighter1.healthBar.y + 60; // just below health bar
var comboText2 = new Text2('Combo: 0', {
size: 48,
fill: 0xFF0000,
stroke: 0xffffff,
strokeThickness: 8
});
comboText2.anchor.set(0.5, 0);
comboText2.x = fighter2.healthBar.x + 150;
comboText2.y = fighter2.healthBar.y + 60;
LK.gui.top.addChild(comboText1);
LK.gui.top.addChild(comboText2);
// Combo timer variables (in frames, 1.5s = 90 frames)
var comboTimer1 = 0;
var comboTimer2 = 0;
// --- Round and timer ---
var round = 1;
var maxRounds = 3;
var wins1 = 0;
var wins2 = 0;
var roundTime = 60 * 30; // 30 seconds at 60fps
var roundTimer = roundTime;
var timerText = new Text2('30', {
size: 80,
fill: "#fff"
});
timerText.anchor.set(0.5, 0);
LK.gui.top.addChild(timerText);
// --- Touch controls (mobile-friendly) ---
var leftPressed = false,
rightPressed = false,
atkPressed = false,
jumpPressed = false,
spcPressed = false,
throwPressed = false,
dodgePressed = false; // New for dodge
// Special attack cooldown overlay for button
var spcCooldownOverlay = LK.getAsset('healthbar_bg', {
anchorX: 0.5,
anchorY: 0.5,
width: 180,
height: 180,
color: 0x000000
});
spcCooldownOverlay.alpha = 0.5;
spcCooldownOverlay.visible = false;
// Move overlay to the special button's position
spcCooldownOverlay.x = 2048 - 950;
spcCooldownOverlay.y = 2450;
game.addChild(spcCooldownOverlay);
var spcCooldownText = new Text2('3', {
size: 90,
fill: "#fff"
});
spcCooldownText.anchor.set(0.5, 0.5);
// Move text to the special button's position
spcCooldownText.x = 2048 - 950;
spcCooldownText.y = 2450;
spcCooldownText.visible = false;
game.addChild(spcCooldownText);
// Special attack cooldown counter above the special attack button
var spcCooldownCounter = new Text2('', {
size: 60,
fill: "#fff"
});
spcCooldownCounter.anchor.set(0.5, 1.0);
// Place above the special button (btnSpc)
spcCooldownCounter.x = 2048 - 950;
spcCooldownCounter.y = 2450 - 120;
spcCooldownCounter.visible = false;
game.addChild(spcCooldownCounter);
// --- Dodge cooldown overlay and timer (like special) ---
// --- Dodge cooldown overlay and timer (like special) ---
// (Positioning is set after btnDodge is defined below)
var dodgeCooldownOverlay = LK.getAsset('healthbar_bg', {
anchorX: 0.5,
anchorY: 0.5,
width: 180,
height: 180,
color: 0x000000
});
dodgeCooldownOverlay.alpha = 0.5;
dodgeCooldownOverlay.visible = false;
game.addChild(dodgeCooldownOverlay);
var dodgeCooldownText = new Text2('3', {
size: 90,
fill: "#fff"
});
dodgeCooldownText.anchor.set(0.5, 0.5);
dodgeCooldownText.visible = false;
game.addChild(dodgeCooldownText);
var dodgeCooldownCounter = new Text2('', {
size: 60,
fill: "#fff"
});
dodgeCooldownCounter.anchor.set(0.5, 1.0);
dodgeCooldownCounter.visible = false;
game.addChild(dodgeCooldownCounter);
// Control buttons (simple rectangles for now)
var btnLeft = LK.getAsset('btn_left', {
anchorX: 0.5,
anchorY: 0.5,
x: 200,
y: 2600,
scaleX: 3.5,
scaleY: 3.5
});
var btnRight = LK.getAsset('btn_right', {
anchorX: 0.5,
anchorY: 0.5,
x: 500,
y: 2600,
scaleX: 3.5,
scaleY: 3.5
});
var btnAtk = LK.getAsset('btn_atk', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 - 650,
// moved farther left
y: 2600,
scaleX: 3.5,
scaleY: 3.5
});
var btnJump = LK.getAsset('btn_jump', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 - 200,
y: 2600,
scaleX: 3.5,
scaleY: 3.5
});
var btnSpc = LK.getAsset('btn_spc', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 - 950,
// moved farther left
y: 2450,
scaleX: 3.5,
scaleY: 3.5
});
// Add dodge button (reuse btn_right asset, place above the throw button, lower opacity)
var btnDodge = LK.getAsset('btn_right', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 - 200,
y: 1880,
// moved farther up
scaleX: 3.5,
scaleY: 3.5
});
btnDodge.alpha = 0.7;
// Add throw button (use unique btn_throw asset, place visually below the dodge button)
var btnThrow = LK.getAsset('btn_throw', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 - 200,
y: 2200,
// moved farther up
scaleX: 3.5,
scaleY: 3.5
});
game.addChild(btnLeft);
game.addChild(btnRight);
game.addChild(btnAtk);
game.addChild(btnJump);
game.addChild(btnSpc);
game.addChild(btnThrow);
game.addChild(btnDodge);
// Set dodge cooldown overlay and text positions now that btnDodge is defined
dodgeCooldownOverlay.x = btnDodge.x;
dodgeCooldownOverlay.y = btnDodge.y;
dodgeCooldownText.x = btnDodge.x;
dodgeCooldownText.y = btnDodge.y;
dodgeCooldownCounter.x = btnDodge.x;
dodgeCooldownCounter.y = btnDodge.y - 120;
// Button event helpers
btnLeft.down = function () {
leftPressed = true;
};
btnLeft.up = function () {
leftPressed = false;
};
btnRight.down = function () {
rightPressed = true;
};
btnRight.up = function () {
rightPressed = false;
};
btnAtk.down = function () {
atkPressed = true;
};
btnAtk.up = function () {
atkPressed = false;
};
btnJump.down = function () {
jumpPressed = true;
};
btnJump.up = function () {
jumpPressed = false;
};
btnSpc.down = function () {
spcPressed = true;
};
btnSpc.up = function () {
spcPressed = false;
};
btnThrow.down = function () {
throwPressed = true;
};
btnThrow.up = function () {
throwPressed = false;
};
btnDodge.down = function () {
dodgePressed = true;
};
btnDodge.up = function () {
dodgePressed = false;
};
// --- Attacks array ---
var attacks = [];
// --- Throwables counter and display ---
var throwablesLeft = 5;
var throwablesText = new Text2('Throwables: 5', {
size: 60,
fill: "#fff",
stroke: 0x000000,
strokeThickness: 8
});
throwablesText.anchor.set(0, 0); // Top left, but not in the 100x100 reserved area
throwablesText.x = 120;
throwablesText.y = 30;
LK.gui.top.addChild(throwablesText);
// --- Game update loop ---
game.update = function () {
// --- Main menu block: If not started, skip all gameplay logic ---
if (typeof gameStarted !== "undefined" && !gameStarted) {
// Animate subtitle (blink)
if (typeof subtitleText !== "undefined") {
subtitleText.alpha = 0.5 + 0.5 * Math.sin(LK.ticks / 20);
}
return;
}
// --- Timer ---
if (roundTimer > 0) {
roundTimer--;
timerText.setText(Math.ceil(roundTimer / 60));
}
// --- Update arrow position above fighter1 ---
if (typeof arrowAbovePlayer1 !== "undefined" && typeof fighter1 !== "undefined") {
arrowAbovePlayer1.x = fighter1.x;
// Place arrow 30px above the top of the fighter sprite (closer to player)
arrowAbovePlayer1.y = fighter1.y - (fighter1.height || 400) - 30;
}
// --- Ground is fixed, no update needed ---
// --- Player 1 controls (left side) ---
if (!fighter1.immobile) {
if (typeof fighter1.isMoving === "undefined") fighter1.isMoving = false;
if (typeof fighter1.lastMoving === "undefined") fighter1.lastMoving = false;
fighter1.isMoving = leftPressed || rightPressed;
// Removed mov sound on movement for player
if (leftPressed) {
fighter1.x = Math.max(120, fighter1.x - fighter1.moveSpeed);
fighter1.setFacing(false);
}
if (rightPressed) {
fighter1.x = Math.min(2048 - 120, fighter1.x + fighter1.moveSpeed);
fighter1.setFacing(true);
}
fighter1.lastMoving = fighter1.isMoving;
// Only trigger jump on the frame the button is pressed (rising edge)
if (jumpPressed && !fighter1.lastJumpPressed) {
fighter1.jump();
}
// Only trigger dodge on the frame the button is pressed (rising edge)
if (dodgePressed && !fighter1.lastDodgePressed) {
fighter1.dodge();
}
if (atkPressed) {
fighter1.attack();
}
if (throwPressed && typeof fighter1.throwObject === "function") {
if (throwablesLeft > 0) {
fighter1.throwObject();
}
}
if (spcPressed) {
if (fighter1.isSpecialReady) {
fighter1.special();
}
}
}
fighter1.lastJumpPressed = jumpPressed;
fighter1.lastDodgePressed = dodgePressed;
// --- Special attack cooldown overlay update ---
if (!fighter1.isSpecialReady) {
spcCooldownOverlay.visible = true;
spcCooldownText.visible = true;
var secondsLeft = Math.ceil(fighter1.specialTimer / 60);
spcCooldownText.setText(secondsLeft);
// Show and update the counter above the special button
spcCooldownCounter.visible = true;
spcCooldownCounter.setText(secondsLeft + "s");
} else {
spcCooldownOverlay.visible = false;
spcCooldownText.visible = false;
spcCooldownCounter.visible = false;
}
// --- Dodge cooldown overlay update ---
if (fighter1.dodgeCooldownTimer > 0) {
dodgeCooldownOverlay.visible = true;
dodgeCooldownText.visible = true;
var dodgeSecondsLeft = Math.ceil(fighter1.dodgeCooldownTimer / 60);
dodgeCooldownText.setText(dodgeSecondsLeft);
dodgeCooldownCounter.visible = true;
dodgeCooldownCounter.setText(dodgeSecondsLeft + "s");
} else {
dodgeCooldownOverlay.visible = false;
dodgeCooldownText.visible = false;
dodgeCooldownCounter.visible = false;
}
// --- AI for fighter2 (improved logic: better approach, attack, dodge, and use abilities) ---
if (!fighter2.immobile) {
var dx = fighter1.x - fighter2.x;
var dy = fighter1.y - fighter2.y;
var dist = Math.sqrt(dx * dx + dy * dy);
// --- Dodge logic: more reactive and predictive ---
var shouldDodge = false;
// Dodge if player is attacking and close, or if a projectile is incoming
var incomingProjectile = false;
for (var i = 0; i < attacks.length; i++) {
var atk = attacks[i];
if (atk instanceof Projectile && atk.owner === fighter1 && !atk.hit && !atk.destroyed) {
// Predict if projectile will hit soon (within 80px horizontally and 200px vertically)
if (Math.abs(atk.x - fighter2.x) < 80 && Math.abs(atk.y - fighter2.y) < 200) {
incomingProjectile = true;
break;
}
}
}
// If player is attacking and close, or projectile incoming, or sometimes randomly
if (!fighter2.isDodging && fighter2.dodgeCooldownTimer === 0 && !fighter2.isJumping && (Math.abs(dx) < 350 && fighter1.isAttacking && Math.random() < 0.35 || incomingProjectile && Math.random() < 0.7 || Math.random() < 0.008)) {
shouldDodge = true;
}
if (shouldDodge) {
fighter2.dodge();
}
// --- Move toward player with smarter spacing ---
if (typeof fighter2.isMoving === "undefined") fighter2.isMoving = false;
if (typeof fighter2.lastMoving === "undefined") fighter2.lastMoving = false;
fighter2.isMoving = false;
// Try to keep optimal distance (attack range)
var optimalMin = 140,
optimalMax = 220;
if (Math.abs(dx) > optimalMax) {
// Approach player
if (dx < 0) {
fighter2.x -= fighter2.moveSpeed * 0.9;
fighter2.setFacing(false);
fighter2.isMoving = true;
} else {
fighter2.x += fighter2.moveSpeed * 0.9;
fighter2.setFacing(true);
fighter2.isMoving = true;
}
} else if (Math.abs(dx) < optimalMin) {
// Back away to avoid being too close
if (dx < 0) {
fighter2.x += fighter2.moveSpeed * 0.7;
fighter2.setFacing(true);
fighter2.isMoving = true;
} else {
fighter2.x -= fighter2.moveSpeed * 0.7;
fighter2.setFacing(false);
fighter2.isMoving = true;
}
}
// --- Attack logic: attack if in range and not on cooldown ---
if (Math.abs(dx) >= optimalMin && Math.abs(dx) <= optimalMax && !fighter2.isAttacking && !fighter2.isDodging && Math.random() < 0.22) {
fighter2.attack();
}
// --- Jump logic: jump if player jumps, or to avoid projectiles, or randomly ---
if (!fighter2.isJumping && (fighter1.isJumping && Math.abs(dx) < 300 && Math.random() < 0.25 || incomingProjectile && Math.random() < 0.25 || Math.random() < 0.006)) {
fighter2.jump();
}
// --- Special attack: use if ready and player is in range, or randomly ---
if (fighter2.isSpecialReady && Math.abs(dx) < 350 && Math.random() < 0.08) {
fighter2.special();
}
// --- Throw object if available and player is at mid/long range ---
if (typeof fighter2.throwablesLeft === "undefined") {
fighter2.throwablesLeft = 5;
}
if (typeof fighter2.lastProjectileTick === "undefined" || LK.ticks - fighter2.lastProjectileTick >= 30) {
if (fighter2.throwablesLeft > 0 && Math.abs(dx) > 400 && Math.abs(dx) < 1200 && Math.random() < 0.13) {
fighter2.throwablesLeft--;
fighter2.lastProjectileTick = LK.ticks;
var proj = new Projectile(fighter2, fighter2.isFacingRight);
proj.x = fighter2.x + (fighter2.isFacingRight ? 120 : -120);
proj.y = fighter2.y - 180;
attacks.push(proj);
game.addChild(proj);
}
}
fighter2.lastMoving = fighter2.isMoving;
}
// --- Update fighters ---
fighter1.update();
fighter2.update();
// --- Update attacks ---
for (var i = attacks.length - 1; i >= 0; i--) {
var atk = attacks[i];
// Store last position for possible future use (e.g. for projectiles)
if (typeof atk.lastX === "undefined") atk.lastX = atk.x;
if (typeof atk.lastY === "undefined") atk.lastY = atk.y;
atk.update();
// Remove if destroyed
if (atk.destroyed) {
attacks.splice(i, 1);
continue;
}
// Set owner if not set
if (!atk.owner) {
atk.setOwner(atk.isSpecial ? atk.x < 2048 / 2 ? fighter1 : fighter2 : fighter1);
}
// Handle collision with the other fighter
var target = atk.owner === fighter1 ? fighter2 : fighter1;
// If target is dodging and invulnerable, skip hit
if (target.dodgeInvuln) {
atk.lastX = atk.x;
atk.lastY = atk.y;
continue;
}
// Only trigger on the exact frame of collision (rising edge)
var wasIntersecting = atk.lastWasIntersecting || false;
var isIntersecting = atk.intersects(target);
if (!wasIntersecting && isIntersecting && !atk.hit) {
// Combo logic: check if this hit is within 1.5 seconds (90 frames) of the last hit by this attacker
var nowTick = LK.ticks || 0;
if (typeof atk.owner.lastHitTime === "undefined") atk.owner.lastHitTime = -9999;
if (typeof atk.owner.comboCount === "undefined") atk.owner.comboCount = 0;
var prevCombo = atk.owner.comboCount;
if (nowTick - atk.owner.lastHitTime <= 90) {
atk.owner.comboCount++;
// Start combo timer for this fighter
if (atk.owner === fighter1 && atk.owner.comboCount > prevCombo) {
comboTimer1 = 90;
}
if (atk.owner === fighter2 && atk.owner.comboCount > prevCombo) {
comboTimer2 = 90;
}
// Impact effect on combo text when combo increases
if (atk.owner === fighter1 && atk.owner.comboCount > prevCombo) {
comboText1.scaleX = comboText1.scaleY = 1.3;
// Impact scale
tween(comboText1, {
scaleX: 1,
scaleY: 1
}, {
duration: 180
});
// Shake effect
var shakeTimes = 10;
var shakeMagnitude = 18;
var shakeDuration = 180;
var shakeStep = Math.floor(shakeDuration / shakeTimes);
var origX = comboText1.x;
var origY = comboText1.y;
for (var s = 0; s < shakeTimes; s++) {
(function (s) {
LK.setTimeout(function () {
// Alternate shake direction
var dx = (s % 2 === 0 ? 1 : -1) * shakeMagnitude * (1 - s / shakeTimes);
var dy = (s % 2 === 0 ? -1 : 1) * shakeMagnitude * 0.5 * (1 - s / shakeTimes);
comboText1.x = origX + dx;
comboText1.y = origY + dy;
// Restore at end
if (s === shakeTimes - 1) {
LK.setTimeout(function () {
comboText1.x = origX;
comboText1.y = origY;
}, shakeStep);
}
}, s * shakeStep);
})(s);
}
} else if (atk.owner === fighter2 && atk.owner.comboCount > prevCombo) {
comboText2.scaleX = comboText2.scaleY = 1.3;
tween(comboText2, {
scaleX: 1,
scaleY: 1
}, {
duration: 180
});
// Shake effect
var shakeTimes2 = 10;
var shakeMagnitude2 = 18;
var shakeDuration2 = 180;
var shakeStep2 = Math.floor(shakeDuration2 / shakeTimes2);
var origX2 = comboText2.x;
var origY2 = comboText2.y;
for (var s2 = 0; s2 < shakeTimes2; s2++) {
(function (s2) {
LK.setTimeout(function () {
var dx2 = (s2 % 2 === 0 ? 1 : -1) * shakeMagnitude2 * (1 - s2 / shakeTimes2);
var dy2 = (s2 % 2 === 0 ? -1 : 1) * shakeMagnitude2 * 0.5 * (1 - s2 / shakeTimes2);
comboText2.x = origX2 + dx2;
comboText2.y = origY2 + dy2;
if (s2 === shakeTimes2 - 1) {
LK.setTimeout(function () {
comboText2.x = origX2;
comboText2.y = origY2;
}, shakeStep2);
}
}, s2 * shakeStep2);
})(s2);
}
}
} else {
atk.owner.comboCount = 1;
// Start combo timer for this fighter
if (atk.owner === fighter1 && atk.owner.comboCount > prevCombo) {
comboTimer1 = 90;
}
if (atk.owner === fighter2 && atk.owner.comboCount > prevCombo) {
comboTimer2 = 90;
}
// Impact effect on combo text when combo increases from 0
if (atk.owner === fighter1 && atk.owner.comboCount > prevCombo) {
comboText1.scaleX = comboText1.scaleY = 1.3;
tween(comboText1, {
scaleX: 1,
scaleY: 1
}, {
duration: 180
});
// Shake effect
var shakeTimes = 10;
var shakeMagnitude = 18;
var shakeDuration = 180;
var shakeStep = Math.floor(shakeDuration / shakeTimes);
var origX = comboText1.x;
var origY = comboText1.y;
for (var s = 0; s < shakeTimes; s++) {
(function (s) {
LK.setTimeout(function () {
var dx = (s % 2 === 0 ? 1 : -1) * shakeMagnitude * (1 - s / shakeTimes);
var dy = (s % 2 === 0 ? -1 : 1) * shakeMagnitude * 0.5 * (1 - s / shakeTimes);
comboText1.x = origX + dx;
comboText1.y = origY + dy;
if (s === shakeTimes - 1) {
LK.setTimeout(function () {
comboText1.x = origX;
comboText1.y = origY;
}, shakeStep);
}
}, s * shakeStep);
})(s);
}
} else if (atk.owner === fighter2 && atk.owner.comboCount > prevCombo) {
comboText2.scaleX = comboText2.scaleY = 1.3;
tween(comboText2, {
scaleX: 1,
scaleY: 1
}, {
duration: 180
});
// Shake effect
var shakeTimes2 = 10;
var shakeMagnitude2 = 18;
var shakeDuration2 = 180;
var shakeStep2 = Math.floor(shakeDuration2 / shakeTimes2);
var origX2 = comboText2.x;
var origY2 = comboText2.y;
for (var s2 = 0; s2 < shakeTimes2; s2++) {
(function (s2) {
LK.setTimeout(function () {
var dx2 = (s2 % 2 === 0 ? 1 : -1) * shakeMagnitude2 * (1 - s2 / shakeTimes2);
var dy2 = (s2 % 2 === 0 ? -1 : 1) * shakeMagnitude2 * 0.5 * (1 - s2 / shakeTimes2);
comboText2.x = origX2 + dx2;
comboText2.y = origY2 + dy2;
if (s2 === shakeTimes2 - 1) {
LK.setTimeout(function () {
comboText2.x = origX2;
comboText2.y = origY2;
}, shakeStep2);
}
}, s2 * shakeStep2);
})(s2);
}
}
}
atk.owner.lastHitTime = nowTick;
// Knockback direction: +1 if attacker is facing right, -1 if left
var knockbackDir = atk.owner && atk.owner.isFacingRight ? 1 : -1;
// Knockback strength: special attacks knock back SIGNIFICANTLY more (much stronger effect)
var knockbackStrength = atk.isSpecial ? 480 : 64;
target.takeDamage(atk.damage, knockbackDir, knockbackStrength);
// If hit by a special attack, make target immobile for 1 second (60 frames)
if (atk.isSpecial) {
target.immobile = true;
LK.setTimeout(function () {
target.immobile = false;
}, 1000);
}
atk.hit = true;
atk.destroy();
// Flash on hit
LK.effects.flashObject(target, 0xff0000, 200);
// Show a visible attack effect at the hit location, rotated to match the attack direction
var hitEffect;
if (typeof Projectile !== "undefined" && atk instanceof Projectile) {
// Unique projectile hit effect: use btn_throw asset, smaller, quick fade
hitEffect = LK.getAsset('btn_throw', {
anchorX: 0.5,
anchorY: 0.5,
x: target.x,
y: target.y - 200
});
hitEffect.rotation = 0;
game.addChild(hitEffect);
hitEffect.scaleX = hitEffect.scaleY = 1.0;
hitEffect.alpha = 1;
tween(hitEffect, {
scaleX: 1.7,
scaleY: 1.7,
alpha: 0
}, {
duration: 220,
onFinish: function onFinish() {
if (hitEffect && hitEffect.destroy) hitEffect.destroy();
}
});
} else {
// Regular or special attack effect
hitEffect = LK.getAsset(atk.isSpecial ? 'special_attack' : 'attack', {
anchorX: 0.5,
anchorY: 0.5,
x: target.x,
y: target.y - 200
});
// Rotate the effect to match the direction the attacker is facing
if (atk.owner && atk.owner.isFacingRight === false) {
hitEffect.rotation = Math.PI; // Face left
} else {
hitEffect.rotation = 0; // Face right (default)
}
game.addChild(hitEffect);
// Animate the effect: scale up and fade out, then destroy
hitEffect.scaleX = hitEffect.scaleY = 1.2;
hitEffect.alpha = 1;
tween(hitEffect, {
scaleX: 2.0,
scaleY: 2.0,
alpha: 0
}, {
duration: 350,
onFinish: function onFinish() {
if (hitEffect && hitEffect.destroy) hitEffect.destroy();
}
});
}
}
// Update last intersection state for next frame
atk.lastWasIntersecting = isIntersecting;
atk.lastX = atk.x;
atk.lastY = atk.y;
}
// --- Health bar updates ---
// Removed number healthbars update
// --- Combo reset logic: reset combo if 1.5 seconds (90 frames) have passed since last hit ---
var nowTick = LK.ticks || 0;
if (nowTick - fighter1.lastHitTime > 90) {
fighter1.comboCount = 0;
}
if (nowTick - fighter2.lastHitTime > 90) {
fighter2.comboCount = 0;
}
// Decrement combo timers
if (comboTimer1 > 0) comboTimer1--;
if (comboTimer2 > 0) comboTimer2--;
// Update combo counters under health bars, only show if timer is active
if (comboTimer1 > 0 && fighter1.comboCount > 0) {
comboText1.visible = true;
comboText1.setText(('COMBO: ' + (fighter1.comboCount || 0) + '!').toUpperCase());
} else {
comboText1.visible = false;
}
if (comboTimer2 > 0 && fighter2.comboCount > 0) {
comboText2.visible = true;
comboText2.setText(('COMBO: ' + (fighter2.comboCount || 0) + '!').toUpperCase());
} else {
comboText2.visible = false;
}
// --- Win/lose/round logic ---
var roundOver = false;
var winner = null;
if (fighter1.health <= 0) {
wins2++;
roundOver = true;
winner = 2;
} else if (fighter2.health <= 0) {
wins1++;
roundOver = true;
winner = 1;
} else if (roundTimer <= 0) {
if (fighter1.health > fighter2.health) {
wins1++;
winner = 1;
} else if (fighter2.health > fighter1.health) {
wins2++;
winner = 2;
} else {
// Draw, no one gets a win
}
roundOver = true;
}
if (roundOver) {
// Update round win counters
if (typeof winsText1 !== "undefined") winsText1.setText(wins1 + '');
if (typeof winsText2 !== "undefined") winsText2.setText(wins2 + '');
if (wins1 >= 2) {
LK.showYouWin();
return;
} else if (wins2 >= 2) {
LK.showGameOver();
return;
}
// Next round
round++;
roundTimer = roundTime;
fighter1.health = fighter1.maxHealth;
fighter2.health = fighter2.maxHealth;
fighter1.updateHealthBar();
fighter2.updateHealthBar();
fighter1.x = 600;
fighter2.x = 2048 - 600;
fighter1.setFacing(true);
fighter2.setFacing(false);
// Reset throwables for both fighters
throwablesLeft = 5;
if (typeof throwablesText !== "undefined" && throwablesText.setText) {
throwablesText.setText('Throwables: ' + throwablesLeft);
}
fighter2.throwablesLeft = 5;
// Remove all attacks
for (var j = attacks.length - 1; j >= 0; j--) {
if (attacks[j].destroy) attacks[j].destroy();
}
attacks = [];
// --- Start countdown overlay for new round ---
setGameplayUIVisible(true); // Show all gameplay UI elements during countdown
// Countdown overlay
var countdownOverlay = new Container();
countdownOverlay.visible = true;
// Large countdown text
var countdownText = new Text2('3', {
size: 320,
fill: "#fff",
stroke: 0x000000,
strokeThickness: 18
});
countdownText.anchor.set(0.5, 0.5);
countdownText.x = 2048 / 2;
countdownText.y = 1200;
countdownOverlay.addChild(countdownText);
game.addChild(countdownOverlay);
var countdownValue = 3;
countdownText.setText(countdownValue);
// Pause gameplay during countdown
gameStarted = false;
var countdownInterval = LK.setInterval(function () {
countdownValue--;
if (countdownValue > 0) {
countdownText.setText(countdownValue);
} else if (countdownValue === 0) {
countdownText.setText("FIGHT!");
// Animate FIGHT! (scale up and fade out)
countdownText.scaleX = countdownText.scaleY = 1.0;
countdownText.alpha = 1.0;
tween(countdownText, {
scaleX: 1.4,
scaleY: 1.4,
alpha: 0
}, {
duration: 700,
onFinish: function onFinish() {
countdownOverlay.visible = false;
if (countdownOverlay.parent) countdownOverlay.parent.removeChild(countdownOverlay);
setGameplayUIVisible(true);
gameStarted = true;
}
});
LK.clearInterval(countdownInterval);
}
}, 900);
}
};