/**** * 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);
}
};