User prompt
make ai better at playin the game
User prompt
reduce the throwable damage
User prompt
increase the size of the buttons for better cotnrols on mobile devices
User prompt
make the particle effects animation speed up significiantly
User prompt
add a different asset for the particle texture
User prompt
make the special attack cooldown 10 secs
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'parent')' in or related to this line: 'if (self.parent && typeof self.parent.toGlobal === "function" && self.position) {' Line Number: 1595
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'parent')' in or related to this line: 'if (self.parent && typeof self.parent.toGlobal === "function" && self.position) {' Line Number: 1595
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'x')' in or related to this line: 'var blood = LK.getAsset('blood_particle', {' Line Number: 1590
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'x')' in or related to this line: 'var blood = LK.getAsset('blood_particle', {' Line Number: 1586
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'x')' in or related to this line: 'countdownOverlay.parent.removeChild(countdownOverlay);' Line Number: 1571
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'x')' in or related to this line: 'var blood = LK.getAsset('blood_particle', {' Line Number: 1575
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'x')' in or related to this line: 'var blood = LK.getAsset('blood_particle', {' Line Number: 1576
User prompt
make the particle texture an asset
User prompt
If a player takes damage, a blood particle effect will appear
User prompt
make the game slow paced
User prompt
move the cooldown timer for the special attack to where the button is located now
User prompt
Move the attack, special attack, throw and dodge keys farther apart
User prompt
make the buttons bigger
User prompt
increase the size of numbers
User prompt
Instead of writing wins, just write numbers
User prompt
Please fix the bug: 'Cannot set properties of undefined (setting 'y')' in or related to this line: 'winsText2.y = fighter2.healthBar.y + 240; // moved even further down below health bar' Line Number: 706
User prompt
move down even more
User prompt
move win counters down
User prompt
bring win counters closer together so they can be seen
/****
* 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);
}
}; ===================================================================
--- original.js
+++ change.js
@@ -971,65 +971,82 @@
dodgeCooldownOverlay.visible = false;
dodgeCooldownText.visible = false;
dodgeCooldownCounter.visible = false;
}
- // --- AI for fighter2 (move toward player, attack, dodge, and throw) ---
+ // --- AI for fighter2 (improved logic: better approach, attack, dodge, and use abilities) ---
if (!fighter2.immobile) {
var dx = fighter1.x - fighter2.x;
- // --- Dodge logic: randomly dodge if player is attacking and close, or sometimes randomly ---
+ var dy = fighter1.y - fighter2.y;
+ var dist = Math.sqrt(dx * dx + dy * dy);
+ // --- Dodge logic: more reactive and predictive ---
var shouldDodge = false;
- // If player is attacking and close, higher chance to dodge
- if (!fighter2.isDodging && fighter2.dodgeCooldownTimer === 0 && !fighter2.isJumping && Math.abs(dx) < 350 && fighter1.isAttacking && Math.random() < 0.18) {
- shouldDodge = true;
+ // 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;
+ }
+ }
}
- // Occasionally dodge randomly
- if (!fighter2.isDodging && fighter2.dodgeCooldownTimer === 0 && !fighter2.isJumping && Math.random() < 0.01) {
+ // 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 ---
- if (Math.abs(dx) > 180) {
- if (typeof fighter2.isMoving === "undefined") fighter2.isMoving = false;
- if (typeof fighter2.lastMoving === "undefined") fighter2.lastMoving = false;
- fighter2.isMoving = false;
- if (Math.abs(dx) > 180) {
- if (dx < 0) {
- fighter2.x -= fighter2.moveSpeed * 0.7;
- fighter2.setFacing(false);
- fighter2.isMoving = true;
- } else {
- fighter2.x += fighter2.moveSpeed * 0.7;
- fighter2.setFacing(true);
- fighter2.isMoving = true;
- }
- // Removed mov sound on movement for AI
- } else if (!fighter2.isAttacking && Math.random() < 0.015) {
- fighter2.attack();
+ // --- 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;
}
- fighter2.lastMoving = fighter2.isMoving;
- } else if (!fighter2.isAttacking && Math.random() < 0.015) {
+ } 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 occasionally ---
- if (!fighter2.isJumping && Math.random() < 0.004) {
+ // --- 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 occasionally ---
- if (fighter2.isSpecialReady && Math.random() < 0.003) {
+ // --- 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 in range ---
- // Give AI its own throwables count (initialize if needed)
+ // --- Throw object if available and player is at mid/long range ---
if (typeof fighter2.throwablesLeft === "undefined") {
fighter2.throwablesLeft = 5;
}
- // Only allow one projectile at a time per fighter (like player)
if (typeof fighter2.lastProjectileTick === "undefined" || LK.ticks - fighter2.lastProjectileTick >= 30) {
- // If player is in range and AI has throwables, throw with some probability
- if (fighter2.throwablesLeft > 0 && Math.abs(dx) > 300 && Math.abs(dx) < 1200 && Math.random() < 0.04) {
- // Use the same throwObject logic, but decrement AI's own throwablesLeft
+ 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);
@@ -1037,8 +1054,9 @@
attacks.push(proj);
game.addChild(proj);
}
}
+ fighter2.lastMoving = fighter2.isMoving;
}
// --- Update fighters ---
fighter1.update();
fighter2.update();