/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var Anchor = Container.expand(function () { var self = Container.call(this); // Create anchor graphic var anchorGraphics = self.attachAsset('anchor', { anchorX: 0.5, anchorY: 0.5, scaleX: 1, scaleY: 10 }); self.isBlue = true; // true = blue (stop), false = orange (move) self.damage = 6; self.lastX = 0; self.lastY = 0; self.colorChangeTimer = 0; self.colorChangeInterval = 180; // Change color every 3 seconds at 60fps self.speed = 3; self.movingRight = true; // Update anchor color self.updateColor = function () { if (self.isBlue) { anchorGraphics.tint = 0x0066ff; // Blue } else { anchorGraphics.tint = 0xff6600; // Orange } }; self.update = function () { self.lastX = self.x; self.lastY = self.y; // Move horizontally within the box boundaries var anchorHalfWidth = 60; // Half of 120px anchor width var minX = boxX + 8 + anchorHalfWidth; var maxX = boxX + boxWidth - 8 - anchorHalfWidth; if (self.movingRight) { self.x += self.speed; // Move right if (self.x >= maxX) { self.x = maxX; self.movingRight = false; // Switch to moving left } } else { self.x -= self.speed; // Move left if (self.x <= minX) { self.x = minX; self.movingRight = true; // Switch to moving right } } // Keep anchor within vertical bounds of the box var anchorHalfHeight = 600; // Half of 1200px anchor height var minY = boxY + 8 + anchorHalfHeight; var maxY = boxY + boxHeight - 8 - anchorHalfHeight; if (self.y < minY) { self.y = minY; } if (self.y > maxY) { self.y = maxY; } // Change color periodically self.colorChangeTimer++; if (self.colorChangeTimer >= self.colorChangeInterval) { self.isBlue = !self.isBlue; self.updateColor(); self.colorChangeTimer = 0; } }; // Initialize with blue color self.updateColor(); return self; }); var Asgore = Container.expand(function () { var self = Container.call(this); // Create Asgore using proper Asgore image asset var asgoreGraphics = self.attachAsset('asgore', { anchorX: 0.5, anchorY: 0.5, scaleX: 1, scaleY: 1 }); self.attackTimer = 0; self.attackInterval = 240; // Attack every 4 seconds self.blocksLeft = 12; self.isDead = false; self.block = function () { if (self.blocksLeft > 0 && !self.isDead) { self.blocksLeft--; // Flash blue to indicate block tween(asgoreGraphics, { tint: 0x0066ff }, { duration: 100, onFinish: function onFinish() { tween(asgoreGraphics, { tint: 0xffffff }, { duration: 100 }); } }); if (self.blocksLeft <= 0) { self.isDead = true; tween(asgoreGraphics, { alpha: 0.3 }, { duration: 500 }); } return true; } return false; }; return self; }); var BoxBoundary = Container.expand(function () { var self = Container.call(this); // Create four walls for the box var topWall = self.attachAsset('boxBorder', { anchorX: 0, anchorY: 0, width: 1600, height: 8 }); var bottomWall = self.attachAsset('boxBorder', { anchorX: 0, anchorY: 0, width: 1600, height: 8 }); var leftWall = self.attachAsset('boxBorder', { anchorX: 0, anchorY: 0, width: 8, height: 1200 }); var rightWall = self.attachAsset('boxBorder', { anchorX: 0, anchorY: 0, width: 8, height: 1200 }); // Position walls topWall.x = 0; topWall.y = 0; bottomWall.x = 0; bottomWall.y = 1200 - 8; leftWall.x = 0; leftWall.y = 0; rightWall.x = 1600 - 8; rightWall.y = 0; return self; }); var Bullet = Container.expand(function () { var self = Container.call(this); var bulletGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); self.speed = 15; self.directionX = 0; self.directionY = 0; self.lastX = 0; self.lastY = 0; self.setDirection = function (targetX, targetY, startX, startY) { var deltaX = targetX - startX; var deltaY = targetY - startY; var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); if (distance > 0) { self.directionX = deltaX / distance; self.directionY = deltaY / distance; } }; self.update = function () { self.lastX = self.x; self.lastY = self.y; self.x += self.directionX * self.speed; self.y += self.directionY * self.speed; }; return self; }); var Flowey = Container.expand(function () { var self = Container.call(this); // Create Flowey using proper Flowey image asset var floweyGraphics = self.attachAsset('flowey', { anchorX: 0.5, anchorY: 0.5, scaleX: 1, scaleY: 1 }); self.attackTimer = 0; self.attackInterval = 600; // Attack every 10 seconds self.destroysLeft = 999; // Infinite until transformation self.finalFormDestroysLeft = 5; self.isFinalForm = false; self.isDead = false; self.destroyAttack = function () { if (self.isDead) return false; var destroysToUse = self.isFinalForm ? self.finalFormDestroysLeft : self.destroysLeft; if (destroysToUse > 0) { if (self.isFinalForm) { self.finalFormDestroysLeft--; if (self.finalFormDestroysLeft <= 0) { self.isDead = true; tween(floweyGraphics, { alpha: 0.3 }, { duration: 500 }); } } // Flash red to indicate destruction tween(floweyGraphics, { tint: 0xff0000 }, { duration: 100, onFinish: function onFinish() { tween(floweyGraphics, { tint: 0xffffff }, { duration: 100 }); } }); return true; } return false; }; self.transformToFinalForm = function () { if (!self.isFinalForm) { self.isFinalForm = true; self.attackInterval = 300; // Attack more frequently // Transform animation tween(floweyGraphics, { scaleX: 1.5, scaleY: 1.5, tint: 0xff00ff }, { duration: 1000, easing: tween.easeOut }); } }; return self; }); var GasterBeam = Container.expand(function () { var self = Container.call(this); // Create black border first (behind white beam) var beamBorder = self.attachAsset('beamBorder', { anchorX: 0.5, anchorY: 0.5 }); // Create white beam on top var whiteBeam = self.attachAsset('whiteBeam', { anchorX: 0.5, anchorY: 0.5 }); self.damage = 12; // Gaster beam damage self.speed = 4; self.lastX = 0; self.lastY = 0; self.targetX = 0; self.targetY = 0; self.isActive = false; self.fireBeam = function (targetX, targetY) { if (self.isActive) return; self.isActive = true; self.targetX = targetX; self.targetY = targetY; // Calculate direction to target var deltaX = targetX - self.x; var deltaY = targetY - self.y; var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); // Set rotation to point towards target self.rotation = Math.atan2(deltaY, deltaX) + Math.PI / 2; // Add 90 degrees because beam asset points up // Move towards target tween(self, { x: targetX, y: targetY }, { duration: distance / self.speed * 16, // Adjust duration based on distance easing: tween.linear, onFinish: function onFinish() { self.isActive = false; } }); }; self.update = function () { self.lastX = self.x; self.lastY = self.y; }; return self; }); var GasterBlaster = Container.expand(function () { var self = Container.call(this); // Create Gaster Blaster using image asset var blasterGraphics = self.attachAsset('gasterBlaster', { anchorX: 0.5, anchorY: 0.5, scaleX: 1, scaleY: 1 }); self.chargeTime = 60; // 1 second charge time at 60fps self.chargeTimer = 0; self.isCharging = false; self.isReady = false; self.beamDuration = 120; // 2 seconds beam duration self.beamTimer = 0; self.targetX = 0; self.targetY = 0; self.startCharge = function (targetX, targetY) { if (self.isCharging || self.isReady) return; self.isCharging = true; self.targetX = targetX; self.targetY = targetY; self.chargeTimer = 0; // Point towards target during charge var deltaX = targetX - self.x; var deltaY = targetY - self.y; self.rotation = Math.atan2(deltaY, deltaX); // Flash effect during charge tween(blasterGraphics, { alpha: 0.3 }, { duration: 100, easing: tween.linear, onFinish: function onFinish() { tween(blasterGraphics, { alpha: 1 }, { duration: 100, easing: tween.linear }); } }); }; self.fireBeam = function () { if (!self.isReady) return null; var beam = new GasterBeam(); beam.x = self.x; beam.y = self.y; beam.lastX = beam.x; beam.lastY = beam.y; beam.fireBeam(self.targetX, self.targetY); return beam; }; self.update = function () { if (self.isCharging) { self.chargeTimer++; if (self.chargeTimer >= self.chargeTime) { self.isCharging = false; self.isReady = true; self.beamTimer = 0; } } else if (self.isReady) { self.beamTimer++; if (self.beamTimer >= self.beamDuration) { self.isReady = false; } } }; return self; }); var Heart = Container.expand(function () { var self = Container.call(this); var heartGraphics = self.attachAsset('heart', { anchorX: 0.5, anchorY: 0.5 }); var blueHeartGraphics = self.attachAsset('blueHeart', { anchorX: 0.5, anchorY: 0.5, alpha: 0 }); var greenHeartGraphics = self.attachAsset('greenHeart', { anchorX: 0.5, anchorY: 0.5, alpha: 0 }); var yellowHeartGraphics = self.attachAsset('yellowHeart', { anchorX: 0.5, anchorY: 0.5, alpha: 0 }); var shieldGraphics = self.attachAsset('shield', { anchorX: 0.5, anchorY: 0.5, alpha: 0, scaleX: 2.0, scaleY: 2.0 }); self.isBlue = false; self.isGreen = false; self.isYellow = false; self.velocityY = 0; self.gravity = 0.3; self.jumpPower = -18; self.onGround = false; self.groundY = 0; self.shieldX = 0; self.shieldY = 0; self.shieldRadius = 150; // Distance from heart center to shield center self.setBlueMode = function (isBlue) { self.isBlue = isBlue; self.isGreen = false; self.isYellow = false; if (isBlue) { heartGraphics.alpha = 0; blueHeartGraphics.alpha = 1; greenHeartGraphics.alpha = 0; yellowHeartGraphics.alpha = 0; shieldGraphics.alpha = 0; } else { heartGraphics.alpha = 1; blueHeartGraphics.alpha = 0; greenHeartGraphics.alpha = 0; yellowHeartGraphics.alpha = 0; shieldGraphics.alpha = 0; } }; self.setGreenMode = function (isGreen) { self.isGreen = isGreen; self.isBlue = false; self.isYellow = false; if (isGreen) { heartGraphics.alpha = 0; blueHeartGraphics.alpha = 0; greenHeartGraphics.alpha = 1; yellowHeartGraphics.alpha = 0; shieldGraphics.alpha = 1; } else { heartGraphics.alpha = 1; blueHeartGraphics.alpha = 0; greenHeartGraphics.alpha = 0; yellowHeartGraphics.alpha = 0; shieldGraphics.alpha = 0; } }; self.setYellowMode = function (isYellow) { self.isYellow = isYellow; self.isBlue = false; self.isGreen = false; if (isYellow) { heartGraphics.alpha = 0; blueHeartGraphics.alpha = 0; greenHeartGraphics.alpha = 0; yellowHeartGraphics.alpha = 1; shieldGraphics.alpha = 0; } else { heartGraphics.alpha = 1; blueHeartGraphics.alpha = 0; greenHeartGraphics.alpha = 0; yellowHeartGraphics.alpha = 0; shieldGraphics.alpha = 0; } }; self.updateShieldPosition = function (targetX, targetY) { if (self.isGreen) { // Calculate angle from heart to target var deltaX = targetX - self.x; var deltaY = targetY - self.y; var angle = Math.atan2(deltaY, deltaX); // Position shield at fixed radius around heart self.shieldX = Math.cos(angle) * self.shieldRadius; self.shieldY = Math.sin(angle) * self.shieldRadius; // Update shield graphics position shieldGraphics.x = self.shieldX; shieldGraphics.y = self.shieldY; } }; self.getShieldWorldPosition = function () { return { x: self.x + self.shieldX, y: self.y + self.shieldY }; }; return self; }); var Ivy = Container.expand(function () { var self = Container.call(this); // Create ivy graphic - anchor from top to hang down var ivyGraphics = self.attachAsset('ivy', { anchorX: 0.5, anchorY: 0 }); self.damage = 5; self.speed = 3; self.lastX = 0; self.lastY = 0; self.movingRight = true; self.update = function () { self.lastX = self.x; self.lastY = self.y; // Move downward from top to bottom of box self.y += self.speed; // Always move down }; return self; }); var Sans = Container.expand(function () { var self = Container.call(this); // Create Sans using proper Sans image asset var sansGraphics = self.attachAsset('sans', { anchorX: 0.5, anchorY: 0.5, scaleX: 1, scaleY: 1 }); self.attackTimer = 0; self.attackInterval = 720; // Attack every 12 seconds self.greenAttackTimer = 0; self.greenAttackInterval = 10800; // Start at 12:00 timer (180 seconds * 60 fps) self.greenAttackDuration = 7200; // 2 minutes duration from 12:00 to 10:00 (120 seconds * 60 fps) self.isGreenAttack = false; self.yellowAttackTimer = 0; self.yellowAttackInterval = 21600; // Start at 9:00 timer (360 seconds * 60 fps) self.yellowAttackDuration = 7200; // 2 minute duration from 9:00 to 7:00 (120 seconds * 60 fps) self.isYellowAttack = false; self.dodgesLeft = 10; self.isDead = false; self.originalX = 0; self.originalY = 0; self.dodge = function () { if (self.dodgesLeft > 0 && !self.isDead) { self.dodgesLeft--; // Move to random position near original location var dodgeDistance = 100; var angle = Math.random() * Math.PI * 2; var newX = self.originalX + Math.cos(angle) * dodgeDistance; var newY = self.originalY + Math.sin(angle) * dodgeDistance; tween(self, { x: newX, y: newY }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { // Return to original position tween(self, { x: self.originalX, y: self.originalY }, { duration: 300, easing: tween.easeIn }); } }); if (self.dodgesLeft <= 0) { self.isDead = true; tween(sansGraphics, { alpha: 0.3 }, { duration: 500 }); } return true; } return false; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0xf0f8ff }); /**** * Game Code ****/ // Box dimensions var boxX = 224; // Center the 1600px box in 2048px screen var boxY = 766; // Center the 1200px box in 2732px screen var boxWidth = 1600; var boxHeight = 1200; // Create box boundary var boundary = game.addChild(new BoxBoundary()); boundary.x = boxX; boundary.y = boxY; // Create heart character var heart = game.addChild(new Heart()); heart.x = boxX + boxWidth / 2; // Center of box heart.y = boxY + boxHeight / 2; // Center of box // Create the three characters // Sans on the left side (bone thrower) var sans = game.addChild(new Sans()); sans.x = boxX - 150; // Left side of box sans.y = boxY + boxHeight / 2; // Middle height // Asgore in the middle (anchor owner) var asgore = game.addChild(new Asgore()); asgore.x = boxX + boxWidth / 2; // Center of box horizontally asgore.y = boxY - 150; // Above the box // Flowey on the right side (ivy sender) var flowey = game.addChild(new Flowey()); flowey.x = boxX + boxWidth + 150; // Right side of box flowey.y = boxY + boxHeight / 2; // Middle height // Create lives display var livesTxt = new Text2('Lives: 92', { size: 80, fill: 0x000000 }); livesTxt.anchor.set(0.5, 0); LK.gui.top.addChild(livesTxt); // Create timer display var gameTime = 900; // 15 minutes in seconds var timerTxt = new Text2('Time: 15:00', { size: 80, fill: 0x000000 }); timerTxt.anchor.set(1, 0); // Right align LK.gui.topRight.addChild(timerTxt); // Create "THREE LEFT" text in red under timer var threeTxt = new Text2('THREE LEFT', { size: 60, fill: 0xff0000 }); threeTxt.anchor.set(1, 0); // Right align threeTxt.y = 100; // Position below timer LK.gui.topRight.addChild(threeTxt); // Create health button var healthButton = LK.getAsset('healthButton', { anchorX: 0.5, anchorY: 0.5 }); healthButton.x = boxX + boxWidth / 2; // Center horizontally with the box healthButton.y = boxY + boxHeight + 100; // Position below the box (outside) game.addChild(healthButton); // Create health button text var healthButtonText = new Text2('HEALTH +20', { size: 40, fill: 0x000000 }); healthButtonText.anchor.set(0.5, 0.5); healthButtonText.x = healthButton.x; healthButtonText.y = healthButton.y; game.addChild(healthButtonText); // Create yellow heart special button var yellowButton = LK.getAsset('healthButton', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5, scaleY: 1.5 }); yellowButton.x = boxX + boxWidth / 2; // Position directly below health button yellowButton.y = boxY + boxHeight + 200; // Position below the health button yellowButton.tint = 0xffff00; // Yellow color game.addChild(yellowButton); // Create yellow button text var yellowButtonText = new Text2('YELLOW SHOT', { size: 40, fill: 0x000000 }); yellowButtonText.anchor.set(0.5, 0.5); yellowButtonText.x = yellowButton.x; yellowButtonText.y = yellowButton.y; game.addChild(yellowButtonText); // Health button cooldown variables var healthButtonCooldown = 0; var healthButtonMaxCooldown = 1200; // 20 seconds at 60fps var healthButtonEnabled = true; // Store original positions for characters sans.originalX = sans.x; sans.originalY = sans.y; // Dragging variables var isDragging = false; var dragOffsetX = 0; var dragOffsetY = 0; // Constraint function to keep heart within boundaries function constrainHeartPosition() { var heartRadius = 50; // Half of heart width/height // Left boundary if (heart.x - heartRadius < boxX + 8) { heart.x = boxX + 8 + heartRadius; } // Right boundary if (heart.x + heartRadius > boxX + boxWidth - 8) { heart.x = boxX + boxWidth - 8 - heartRadius; } // Top boundary if (heart.y - heartRadius < boxY + 8) { heart.y = boxY + 8 + heartRadius; } // Bottom boundary if (heart.y + heartRadius > boxY + boxHeight - 8) { heart.y = boxY + boxHeight - 8 - heartRadius; } } // Player control variables var targetX = 0; var targetY = 0; var isPlayerControlling = false; // Touch/mouse down event game.down = function (x, y, obj) { // Check if health button was clicked var buttonDistance = Math.sqrt(Math.pow(x - healthButton.x, 2) + Math.pow(y - healthButton.y, 2)); if (buttonDistance < 100 && healthButtonEnabled) { // Increase health by 20 lives += 20; livesTxt.setText('Lives: ' + lives); // Start cooldown healthButtonCooldown = healthButtonMaxCooldown; healthButtonEnabled = false; // Visual feedback - flash button tween(healthButton, { tint: 0xffff00 }, { duration: 200, onFinish: function onFinish() { tween(healthButton, { tint: 0x888888 }, { duration: 200 }); } }); // Update button text to show cooldown healthButtonText.setText('COOLDOWN'); return; // Don't process normal movement } // Check if yellow button was clicked var yellowButtonDistance = Math.sqrt(Math.pow(x - yellowButton.x, 2) + Math.pow(y - yellowButton.y, 2)); if (yellowButtonDistance < 100 && heart.isYellow && bullets.length < maxBullets) { var bullet = new Bullet(); bullet.x = heart.x; bullet.y = heart.y; bullet.lastX = bullet.x; bullet.lastY = bullet.y; // Set direction upward bullet.setDirection(heart.x, heart.y - 100, heart.x, heart.y); bullets.push(bullet); game.addChild(bullet); // Visual feedback - flash yellow button tween(yellowButton, { tint: 0xffffff }, { duration: 100, onFinish: function onFinish() { tween(yellowButton, { tint: 0xffff00 }, { duration: 100 }); } }); return; // Don't process normal movement } // Handle yellow heart shooting if (heart.isYellow && bullets.length < maxBullets) { var bullet = new Bullet(); bullet.x = heart.x; bullet.y = heart.y; bullet.lastX = bullet.x; bullet.lastY = bullet.y; bullet.setDirection(x, y, heart.x, heart.y); bullets.push(bullet); game.addChild(bullet); return; // Don't process normal movement when shooting } isPlayerControlling = true; targetX = x; targetY = y; }; // Touch/mouse move event game.move = function (x, y, obj) { if (isPlayerControlling) { targetX = x; targetY = y; } }; // Touch/mouse up event game.up = function (x, y, obj) { isPlayerControlling = false; }; // Lives system var lives = 92; // Movement variables var heartSpeed = 8; var movementDirection = { x: 1, y: 0.5 }; // Initial movement direction var lastDirectionChangeTime = 0; var directionChangeInterval = 2000; // Change direction every 2 seconds // Gaster Blaster and beam system var gasterBlasters = []; var gasterBeams = []; var maxBlasters = 10; var blasterSpawnTimer = 0; var blasterSpawnInterval = 720; // Spawn every 12 seconds at 60fps var waveIntensity = 1; // Anchor system var anchors = []; var maxAnchors = 2; var anchorSpawnTimer = 0; var anchorSpawnInterval = 240; // Spawn every 4 seconds at 60fps // Ivy system var ivies = []; var maxIvies = 5; var ivySpawnTimer = 0; var ivySpawnInterval = 600; // Spawn every 10 seconds at 60fps // Slow effect system var isSlowed = false; var slowEndTime = 0; var normalHeartSpeed = 8; // Bullet system var bullets = []; var maxBullets = 20; // Function to spawn a new Gaster Blaster at random edge position function spawnGasterBlaster() { if (gasterBlasters.length >= maxBlasters) return; var blaster = new GasterBlaster(); var edge = Math.floor(Math.random() * 4); // 0=top, 1=right, 2=bottom, 3=left // Position blaster at random point on selected edge switch (edge) { case 0: // Top edge blaster.x = boxX + Math.random() * boxWidth; blaster.y = boxY - 100; break; case 1: // Right edge blaster.x = boxX + boxWidth + 100; blaster.y = boxY + Math.random() * boxHeight; break; case 2: // Bottom edge blaster.x = boxX + Math.random() * boxWidth; blaster.y = boxY + boxHeight + 100; break; case 3: // Left edge blaster.x = boxX - 100; blaster.y = boxY + Math.random() * boxHeight; break; } gasterBlasters.push(blaster); game.addChild(blaster); // Start charging to attack the heart blaster.startCharge(heart.x, heart.y); } // Function to spawn a new anchor at random position within the box function spawnAnchor() { if (anchors.length >= maxAnchors) return; var anchor = new Anchor(); // Position anchor within the box boundaries for horizontal movement var anchorHalfWidth = 60; // Half of 120px anchor width var startFromLeft = Math.random() < 0.5; if (startFromLeft) { anchor.x = boxX + 8 + anchorHalfWidth; // Start from left edge inside box anchor.movingRight = true; // Moving right } else { anchor.x = boxX + boxWidth - 8 - anchorHalfWidth; // Start from right edge inside box anchor.movingRight = false; // Moving left } // Random Y position within the box, constrained to keep full anchor height inside var anchorHalfHeight = 600; // Half of 1200px anchor height anchor.y = boxY + anchorHalfHeight + Math.random() * (boxHeight - 2 * anchorHalfHeight); anchor.lastX = anchor.x; anchor.lastY = anchor.y; anchors.push(anchor); game.addChild(anchor); } // Function to spawn a new ivy hanging from top of box like an anchor function spawnIvy() { if (ivies.length >= maxIvies) return; var ivy = new Ivy(); // Position ivy at the very top of box to move downward (anchor point is at top of ivy) ivy.y = boxY + 8; // Start from top edge inside box // Random X position within the box, constrained to keep full ivy width inside var ivyHalfWidth = 60; // Half of 120px ivy width ivy.x = boxX + ivyHalfWidth + Math.random() * (boxWidth - 2 * ivyHalfWidth); ivy.lastX = ivy.x; ivy.lastY = ivy.y; ivies.push(ivy); game.addChild(ivy); } // Game update loop game.update = function () { // Update timer every second (60 ticks = 1 second at 60fps) if (LK.ticks % 60 === 0 && gameTime > 0) { gameTime--; var minutes = Math.floor(gameTime / 60); var seconds = gameTime % 60; var timeString = minutes + ':' + (seconds < 10 ? '0' : '') + seconds; timerTxt.setText('Time: ' + timeString); } // Update health button cooldown if (!healthButtonEnabled) { healthButtonCooldown--; if (healthButtonCooldown <= 0) { healthButtonEnabled = true; healthButtonText.setText('HEALTH +20'); tween(healthButton, { tint: 0x00ff00 }, { duration: 300 }); } else { var cooldownSeconds = Math.ceil(healthButtonCooldown / 60); healthButtonText.setText('COOLDOWN ' + cooldownSeconds + 's'); } } // Check if Sans and Asgore are both dead for Flowey transformation if (sans.isDead && asgore.isDead && !flowey.isFinalForm && !flowey.isDead) { flowey.transformToFinalForm(); // Update ivy system for final form maxIvies = 10; flowey.attackInterval = 300; } // Increase wave intensity over time if (LK.ticks % 600 === 0) { // Every 10 seconds waveIntensity = Math.min(waveIntensity + 0.5, 4); maxBlasters = Math.min(Math.floor(2 + waveIntensity), 10); blasterSpawnInterval = Math.max(Math.floor(720 - waveIntensity * 120), 300); } // Update Sans attack timer and spawn Gaster Blasters (only if alive) if (!sans.isDead) { sans.attackTimer++; sans.greenAttackTimer++; // Check for green heart attack when timer reaches 12:00 (180 seconds elapsed) var elapsedTime = 900 - gameTime; // Calculate elapsed time in seconds var elapsedTicks = elapsedTime * 60; // Convert to ticks if (elapsedTicks >= sans.greenAttackInterval && elapsedTicks < sans.greenAttackInterval + sans.greenAttackDuration && !sans.isGreenAttack) { sans.isGreenAttack = true; heart.setGreenMode(true); // Position heart in center of box and disable movement heart.x = boxX + boxWidth / 2; heart.y = boxY + boxHeight / 2; heart.shieldX = 0; heart.shieldY = 0; } // End green heart attack when timer reaches 10:00 (300 seconds elapsed) if (sans.isGreenAttack && elapsedTicks >= sans.greenAttackInterval + sans.greenAttackDuration) { sans.isGreenAttack = false; heart.setGreenMode(false); } // Check for yellow heart attack when timer reaches 9:00 (360 seconds elapsed) if (elapsedTicks >= sans.yellowAttackInterval && elapsedTicks < sans.yellowAttackInterval + sans.yellowAttackDuration && !sans.isYellowAttack) { sans.isYellowAttack = true; heart.setYellowMode(true); } // End yellow heart attack when timer reaches 7:00 (480 seconds elapsed) if (sans.isYellowAttack && elapsedTicks >= sans.yellowAttackInterval + sans.yellowAttackDuration) { sans.isYellowAttack = false; heart.setYellowMode(false); } if (sans.attackTimer >= sans.attackInterval && !sans.isGreenAttack) { // Sans attack: make heart blue and enable physics heart.setBlueMode(true); heart.velocityY = 0; heart.onGround = false; heart.groundY = boxY + boxHeight - 8 - 50; // Ground level accounting for heart radius // Sans spawns Gaster Blasters var spawnCount = Math.random() < 0.4 ? Math.floor(waveIntensity) : 1; for (var s = 0; s < spawnCount && gasterBlasters.length < maxBlasters; s++) { spawnGasterBlaster(); } sans.attackTimer = 0; } } // Update Asgore attack timer and spawn anchors (only if alive) if (!asgore.isDead) { asgore.attackTimer++; if (asgore.attackTimer >= asgore.attackInterval) { // Asgore sends anchors spawnAnchor(); asgore.attackTimer = 0; } } // Update Flowey attack timer and spawn ivies (only if alive) if (!flowey.isDead) { flowey.attackTimer++; if (flowey.attackTimer >= flowey.attackInterval) { // Flowey sends ivies var spawnCount = flowey.isFinalForm ? 10 : Math.floor(Math.random() * 2) + 2; // Final form sends 10, normal sends 2-3 for (var i = 0; i < spawnCount && ivies.length < maxIvies; i++) { spawnIvy(); } flowey.attackTimer = 0; } } // Handle slow effect expiry if (isSlowed && LK.ticks >= slowEndTime) { isSlowed = false; heartSpeed = normalHeartSpeed; } // Update all Gaster Blasters and spawn beams when ready for (var i = gasterBlasters.length - 1; i >= 0; i--) { var blaster = gasterBlasters[i]; // Check if blaster is ready to fire if (blaster.isReady && blaster.beamTimer === 0) { var beam = blaster.fireBeam(); if (beam) { gasterBeams.push(beam); game.addChild(beam); } } // Remove blasters that have finished their attack cycle if (!blaster.isCharging && !blaster.isReady) { blaster.destroy(); gasterBlasters.splice(i, 1); continue; } } // Update all beams and check for collisions for (var j = gasterBeams.length - 1; j >= 0; j--) { var beam = gasterBeams[j]; var beamBlockedByShield = false; // Check if shield destroyed the beam in green mode if (heart.isGreen) { var shieldPos = heart.getShieldWorldPosition(); var shieldDistance = Math.sqrt(Math.pow(beam.x - shieldPos.x, 2) + Math.pow(beam.y - shieldPos.y, 2)); if (shieldDistance < 100) { beam.destroy(); gasterBeams.splice(j, 1); beamBlockedByShield = true; continue; } } // Check if beam hit the heart if (beam.intersects(heart) && !beamBlockedByShield) { // In green mode, don't take damage as shield should protect if (!heart.isGreen) { // Normal mode - always take damage lives -= beam.damage; livesTxt.setText('Lives: ' + lives); // Flash screen white to indicate beam damage LK.effects.flashScreen(0xffffff, 600); // Check for game over if (lives <= 0) { LK.showGameOver(); return; } } // Remove the beam that hit us beam.destroy(); gasterBeams.splice(j, 1); continue; } // Remove beams that are too far from the play area or inactive var distanceFromCenter = Math.sqrt(Math.pow(beam.x - (boxX + boxWidth / 2), 2) + Math.pow(beam.y - (boxY + boxHeight / 2), 2)); if (distanceFromCenter > 2000 || !beam.isActive) { beam.destroy(); gasterBeams.splice(j, 1); continue; } } // Update all anchors and check for collisions for (var j = anchors.length - 1; j >= 0; j--) { var anchor = anchors[j]; var anchorBlockedByShield = false; // Check if shield destroyed the anchor in green mode if (heart.isGreen) { var shieldPos = heart.getShieldWorldPosition(); var shieldDistance = Math.sqrt(Math.pow(anchor.x - shieldPos.x, 2) + Math.pow(anchor.y - shieldPos.y, 2)); if (shieldDistance < 100) { anchor.destroy(); anchors.splice(j, 1); anchorBlockedByShield = true; continue; } } // Check if anchor hit the heart if (anchor.intersects(heart) && !anchorBlockedByShield) { // In green mode, don't take damage as shield should protect if (!heart.isGreen) { var shouldTakeDamage = false; // Blue anchor: damage if player is moving if (anchor.isBlue && isPlayerControlling) { shouldTakeDamage = true; } // Orange anchor: damage if player is not moving else if (!anchor.isBlue && !isPlayerControlling) { shouldTakeDamage = true; } if (shouldTakeDamage) { // Reduce lives by anchor damage (2) lives -= anchor.damage; livesTxt.setText('Lives: ' + lives); // Flash screen purple to indicate anchor damage LK.effects.flashScreen(0x8800ff, 700); // Check for game over if (lives <= 0) { LK.showGameOver(); return; } } } // Remove the anchor that hit us anchor.destroy(); anchors.splice(j, 1); continue; } // Anchors now stay within box bounds, so no need to remove them for going off screen // They will only be removed when hitting the heart } // Update all ivies and check for collisions for (var k = ivies.length - 1; k >= 0; k--) { var ivy = ivies[k]; var ivyBlockedByShield = false; // Check if shield destroyed the ivy in green mode if (heart.isGreen) { var shieldPos = heart.getShieldWorldPosition(); var shieldDistance = Math.sqrt(Math.pow(ivy.x - shieldPos.x, 2) + Math.pow(ivy.y - shieldPos.y, 2)); if (shieldDistance < 100) { ivy.destroy(); ivies.splice(k, 1); ivyBlockedByShield = true; continue; } } // Check if ivy hit the heart if (ivy.intersects(heart) && !ivyBlockedByShield) { // In green mode, don't take damage as shield should protect if (!heart.isGreen) { // Normal mode - take damage and apply slow effect lives -= ivy.damage; livesTxt.setText('Lives: ' + lives); // Apply slow effect (25% speed reduction) isSlowed = true; heartSpeed = normalHeartSpeed * 0.75; slowEndTime = LK.ticks + 300; // Slow for 5 seconds at 60fps // Flash screen green to indicate ivy damage and slow LK.effects.flashScreen(0x00ff00, 600); // Check for game over if (lives <= 0) { LK.showGameOver(); return; } } // Remove the ivy that hit us ivy.destroy(); ivies.splice(k, 1); continue; } // Remove ivies that have reached the bottom of the box // Since ivy anchor is at top (anchorY: 0), check when ivy top + height reaches bottom if (ivy.y + 800 >= boxY + boxHeight - 8) { ivy.destroy(); ivies.splice(k, 1); continue; } } // Update all bullets and check for collisions with attacks for (var b = bullets.length - 1; b >= 0; b--) { var bullet = bullets[b]; // Check bullet collision with beams for (var beam_i = gasterBeams.length - 1; beam_i >= 0; beam_i--) { var beam = gasterBeams[beam_i]; if (bullet.intersects(beam)) { // Destroy both bullet and beam bullet.destroy(); bullets.splice(b, 1); beam.destroy(); gasterBeams.splice(beam_i, 1); continue; } } // Skip further checks if bullet was destroyed if (b >= bullets.length) continue; // Check bullet collision with anchors for (var anchor_i = anchors.length - 1; anchor_i >= 0; anchor_i--) { var anchor = anchors[anchor_i]; if (bullet.intersects(anchor)) { // Destroy both bullet and anchor bullet.destroy(); bullets.splice(b, 1); anchor.destroy(); anchors.splice(anchor_i, 1); continue; } } // Skip further checks if bullet was destroyed if (b >= bullets.length) continue; // Check bullet collision with ivies for (var ivy_i = ivies.length - 1; ivy_i >= 0; ivy_i--) { var ivy = ivies[ivy_i]; if (bullet.intersects(ivy)) { // Destroy both bullet and ivy bullet.destroy(); bullets.splice(b, 1); ivy.destroy(); ivies.splice(ivy_i, 1); continue; } } // Skip further checks if bullet was destroyed if (b >= bullets.length) continue; // Remove bullets that are too far from play area var bulletDistanceFromCenter = Math.sqrt(Math.pow(bullet.x - (boxX + boxWidth / 2), 2) + Math.pow(bullet.y - (boxY + boxHeight / 2), 2)); if (bulletDistanceFromCenter > 2000) { bullet.destroy(); bullets.splice(b, 1); continue; } } if (isPlayerControlling) { if (heart.isGreen) { // Green heart mode: heart stays in center, only shield moves heart.updateShieldPosition(targetX, targetY); } else if (heart.isYellow) { // Yellow heart mode: normal movement but shooting on click var deltaX = targetX - heart.x; var deltaY = targetY - heart.y; var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); if (distance > 5) { movementDirection.x = deltaX / distance; movementDirection.y = deltaY / distance; heart.x += movementDirection.x * heartSpeed; heart.y += movementDirection.y * heartSpeed; } } else if (heart.isBlue) { // Blue heart physics: left/right movement and jumping var deltaX = targetX - heart.x; // Horizontal movement if (Math.abs(deltaX) > 5) { var horizontalDirection = deltaX > 0 ? 1 : -1; heart.x += horizontalDirection * heartSpeed; } // Jump if touching upward and on ground if (targetY < heart.y - 20 && heart.onGround) { heart.velocityY = heart.jumpPower; heart.onGround = false; } } else { // Normal movement when not blue // Calculate direction toward target var deltaX = targetX - heart.x; var deltaY = targetY - heart.y; var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); // Move toward target if not already there if (distance > 5) { // Normalize direction and apply speed movementDirection.x = deltaX / distance; movementDirection.y = deltaY / distance; heart.x += movementDirection.x * heartSpeed; heart.y += movementDirection.y * heartSpeed; } } } // Apply blue heart physics if (heart.isBlue) { // Apply gravity heart.velocityY += heart.gravity; heart.y += heart.velocityY; // Ground collision if (heart.y >= heart.groundY) { heart.y = heart.groundY; heart.velocityY = 0; heart.onGround = true; } // Reset blue heart after 4 seconds if (sans.attackTimer > 240) { // 4 seconds at 60fps heart.setBlueMode(false); sans.attackTimer = 0; // Reset timer to prevent staying blue } } // Ensure heart stays within bounds each frame constrainHeartPosition(); };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Anchor = Container.expand(function () {
var self = Container.call(this);
// Create anchor graphic
var anchorGraphics = self.attachAsset('anchor', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1,
scaleY: 10
});
self.isBlue = true; // true = blue (stop), false = orange (move)
self.damage = 6;
self.lastX = 0;
self.lastY = 0;
self.colorChangeTimer = 0;
self.colorChangeInterval = 180; // Change color every 3 seconds at 60fps
self.speed = 3;
self.movingRight = true;
// Update anchor color
self.updateColor = function () {
if (self.isBlue) {
anchorGraphics.tint = 0x0066ff; // Blue
} else {
anchorGraphics.tint = 0xff6600; // Orange
}
};
self.update = function () {
self.lastX = self.x;
self.lastY = self.y;
// Move horizontally within the box boundaries
var anchorHalfWidth = 60; // Half of 120px anchor width
var minX = boxX + 8 + anchorHalfWidth;
var maxX = boxX + boxWidth - 8 - anchorHalfWidth;
if (self.movingRight) {
self.x += self.speed; // Move right
if (self.x >= maxX) {
self.x = maxX;
self.movingRight = false; // Switch to moving left
}
} else {
self.x -= self.speed; // Move left
if (self.x <= minX) {
self.x = minX;
self.movingRight = true; // Switch to moving right
}
}
// Keep anchor within vertical bounds of the box
var anchorHalfHeight = 600; // Half of 1200px anchor height
var minY = boxY + 8 + anchorHalfHeight;
var maxY = boxY + boxHeight - 8 - anchorHalfHeight;
if (self.y < minY) {
self.y = minY;
}
if (self.y > maxY) {
self.y = maxY;
}
// Change color periodically
self.colorChangeTimer++;
if (self.colorChangeTimer >= self.colorChangeInterval) {
self.isBlue = !self.isBlue;
self.updateColor();
self.colorChangeTimer = 0;
}
};
// Initialize with blue color
self.updateColor();
return self;
});
var Asgore = Container.expand(function () {
var self = Container.call(this);
// Create Asgore using proper Asgore image asset
var asgoreGraphics = self.attachAsset('asgore', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1,
scaleY: 1
});
self.attackTimer = 0;
self.attackInterval = 240; // Attack every 4 seconds
self.blocksLeft = 12;
self.isDead = false;
self.block = function () {
if (self.blocksLeft > 0 && !self.isDead) {
self.blocksLeft--;
// Flash blue to indicate block
tween(asgoreGraphics, {
tint: 0x0066ff
}, {
duration: 100,
onFinish: function onFinish() {
tween(asgoreGraphics, {
tint: 0xffffff
}, {
duration: 100
});
}
});
if (self.blocksLeft <= 0) {
self.isDead = true;
tween(asgoreGraphics, {
alpha: 0.3
}, {
duration: 500
});
}
return true;
}
return false;
};
return self;
});
var BoxBoundary = Container.expand(function () {
var self = Container.call(this);
// Create four walls for the box
var topWall = self.attachAsset('boxBorder', {
anchorX: 0,
anchorY: 0,
width: 1600,
height: 8
});
var bottomWall = self.attachAsset('boxBorder', {
anchorX: 0,
anchorY: 0,
width: 1600,
height: 8
});
var leftWall = self.attachAsset('boxBorder', {
anchorX: 0,
anchorY: 0,
width: 8,
height: 1200
});
var rightWall = self.attachAsset('boxBorder', {
anchorX: 0,
anchorY: 0,
width: 8,
height: 1200
});
// Position walls
topWall.x = 0;
topWall.y = 0;
bottomWall.x = 0;
bottomWall.y = 1200 - 8;
leftWall.x = 0;
leftWall.y = 0;
rightWall.x = 1600 - 8;
rightWall.y = 0;
return self;
});
var Bullet = Container.expand(function () {
var self = Container.call(this);
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 15;
self.directionX = 0;
self.directionY = 0;
self.lastX = 0;
self.lastY = 0;
self.setDirection = function (targetX, targetY, startX, startY) {
var deltaX = targetX - startX;
var deltaY = targetY - startY;
var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance > 0) {
self.directionX = deltaX / distance;
self.directionY = deltaY / distance;
}
};
self.update = function () {
self.lastX = self.x;
self.lastY = self.y;
self.x += self.directionX * self.speed;
self.y += self.directionY * self.speed;
};
return self;
});
var Flowey = Container.expand(function () {
var self = Container.call(this);
// Create Flowey using proper Flowey image asset
var floweyGraphics = self.attachAsset('flowey', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1,
scaleY: 1
});
self.attackTimer = 0;
self.attackInterval = 600; // Attack every 10 seconds
self.destroysLeft = 999; // Infinite until transformation
self.finalFormDestroysLeft = 5;
self.isFinalForm = false;
self.isDead = false;
self.destroyAttack = function () {
if (self.isDead) return false;
var destroysToUse = self.isFinalForm ? self.finalFormDestroysLeft : self.destroysLeft;
if (destroysToUse > 0) {
if (self.isFinalForm) {
self.finalFormDestroysLeft--;
if (self.finalFormDestroysLeft <= 0) {
self.isDead = true;
tween(floweyGraphics, {
alpha: 0.3
}, {
duration: 500
});
}
}
// Flash red to indicate destruction
tween(floweyGraphics, {
tint: 0xff0000
}, {
duration: 100,
onFinish: function onFinish() {
tween(floweyGraphics, {
tint: 0xffffff
}, {
duration: 100
});
}
});
return true;
}
return false;
};
self.transformToFinalForm = function () {
if (!self.isFinalForm) {
self.isFinalForm = true;
self.attackInterval = 300; // Attack more frequently
// Transform animation
tween(floweyGraphics, {
scaleX: 1.5,
scaleY: 1.5,
tint: 0xff00ff
}, {
duration: 1000,
easing: tween.easeOut
});
}
};
return self;
});
var GasterBeam = Container.expand(function () {
var self = Container.call(this);
// Create black border first (behind white beam)
var beamBorder = self.attachAsset('beamBorder', {
anchorX: 0.5,
anchorY: 0.5
});
// Create white beam on top
var whiteBeam = self.attachAsset('whiteBeam', {
anchorX: 0.5,
anchorY: 0.5
});
self.damage = 12; // Gaster beam damage
self.speed = 4;
self.lastX = 0;
self.lastY = 0;
self.targetX = 0;
self.targetY = 0;
self.isActive = false;
self.fireBeam = function (targetX, targetY) {
if (self.isActive) return;
self.isActive = true;
self.targetX = targetX;
self.targetY = targetY;
// Calculate direction to target
var deltaX = targetX - self.x;
var deltaY = targetY - self.y;
var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// Set rotation to point towards target
self.rotation = Math.atan2(deltaY, deltaX) + Math.PI / 2; // Add 90 degrees because beam asset points up
// Move towards target
tween(self, {
x: targetX,
y: targetY
}, {
duration: distance / self.speed * 16,
// Adjust duration based on distance
easing: tween.linear,
onFinish: function onFinish() {
self.isActive = false;
}
});
};
self.update = function () {
self.lastX = self.x;
self.lastY = self.y;
};
return self;
});
var GasterBlaster = Container.expand(function () {
var self = Container.call(this);
// Create Gaster Blaster using image asset
var blasterGraphics = self.attachAsset('gasterBlaster', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1,
scaleY: 1
});
self.chargeTime = 60; // 1 second charge time at 60fps
self.chargeTimer = 0;
self.isCharging = false;
self.isReady = false;
self.beamDuration = 120; // 2 seconds beam duration
self.beamTimer = 0;
self.targetX = 0;
self.targetY = 0;
self.startCharge = function (targetX, targetY) {
if (self.isCharging || self.isReady) return;
self.isCharging = true;
self.targetX = targetX;
self.targetY = targetY;
self.chargeTimer = 0;
// Point towards target during charge
var deltaX = targetX - self.x;
var deltaY = targetY - self.y;
self.rotation = Math.atan2(deltaY, deltaX);
// Flash effect during charge
tween(blasterGraphics, {
alpha: 0.3
}, {
duration: 100,
easing: tween.linear,
onFinish: function onFinish() {
tween(blasterGraphics, {
alpha: 1
}, {
duration: 100,
easing: tween.linear
});
}
});
};
self.fireBeam = function () {
if (!self.isReady) return null;
var beam = new GasterBeam();
beam.x = self.x;
beam.y = self.y;
beam.lastX = beam.x;
beam.lastY = beam.y;
beam.fireBeam(self.targetX, self.targetY);
return beam;
};
self.update = function () {
if (self.isCharging) {
self.chargeTimer++;
if (self.chargeTimer >= self.chargeTime) {
self.isCharging = false;
self.isReady = true;
self.beamTimer = 0;
}
} else if (self.isReady) {
self.beamTimer++;
if (self.beamTimer >= self.beamDuration) {
self.isReady = false;
}
}
};
return self;
});
var Heart = Container.expand(function () {
var self = Container.call(this);
var heartGraphics = self.attachAsset('heart', {
anchorX: 0.5,
anchorY: 0.5
});
var blueHeartGraphics = self.attachAsset('blueHeart', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0
});
var greenHeartGraphics = self.attachAsset('greenHeart', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0
});
var yellowHeartGraphics = self.attachAsset('yellowHeart', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0
});
var shieldGraphics = self.attachAsset('shield', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0,
scaleX: 2.0,
scaleY: 2.0
});
self.isBlue = false;
self.isGreen = false;
self.isYellow = false;
self.velocityY = 0;
self.gravity = 0.3;
self.jumpPower = -18;
self.onGround = false;
self.groundY = 0;
self.shieldX = 0;
self.shieldY = 0;
self.shieldRadius = 150; // Distance from heart center to shield center
self.setBlueMode = function (isBlue) {
self.isBlue = isBlue;
self.isGreen = false;
self.isYellow = false;
if (isBlue) {
heartGraphics.alpha = 0;
blueHeartGraphics.alpha = 1;
greenHeartGraphics.alpha = 0;
yellowHeartGraphics.alpha = 0;
shieldGraphics.alpha = 0;
} else {
heartGraphics.alpha = 1;
blueHeartGraphics.alpha = 0;
greenHeartGraphics.alpha = 0;
yellowHeartGraphics.alpha = 0;
shieldGraphics.alpha = 0;
}
};
self.setGreenMode = function (isGreen) {
self.isGreen = isGreen;
self.isBlue = false;
self.isYellow = false;
if (isGreen) {
heartGraphics.alpha = 0;
blueHeartGraphics.alpha = 0;
greenHeartGraphics.alpha = 1;
yellowHeartGraphics.alpha = 0;
shieldGraphics.alpha = 1;
} else {
heartGraphics.alpha = 1;
blueHeartGraphics.alpha = 0;
greenHeartGraphics.alpha = 0;
yellowHeartGraphics.alpha = 0;
shieldGraphics.alpha = 0;
}
};
self.setYellowMode = function (isYellow) {
self.isYellow = isYellow;
self.isBlue = false;
self.isGreen = false;
if (isYellow) {
heartGraphics.alpha = 0;
blueHeartGraphics.alpha = 0;
greenHeartGraphics.alpha = 0;
yellowHeartGraphics.alpha = 1;
shieldGraphics.alpha = 0;
} else {
heartGraphics.alpha = 1;
blueHeartGraphics.alpha = 0;
greenHeartGraphics.alpha = 0;
yellowHeartGraphics.alpha = 0;
shieldGraphics.alpha = 0;
}
};
self.updateShieldPosition = function (targetX, targetY) {
if (self.isGreen) {
// Calculate angle from heart to target
var deltaX = targetX - self.x;
var deltaY = targetY - self.y;
var angle = Math.atan2(deltaY, deltaX);
// Position shield at fixed radius around heart
self.shieldX = Math.cos(angle) * self.shieldRadius;
self.shieldY = Math.sin(angle) * self.shieldRadius;
// Update shield graphics position
shieldGraphics.x = self.shieldX;
shieldGraphics.y = self.shieldY;
}
};
self.getShieldWorldPosition = function () {
return {
x: self.x + self.shieldX,
y: self.y + self.shieldY
};
};
return self;
});
var Ivy = Container.expand(function () {
var self = Container.call(this);
// Create ivy graphic - anchor from top to hang down
var ivyGraphics = self.attachAsset('ivy', {
anchorX: 0.5,
anchorY: 0
});
self.damage = 5;
self.speed = 3;
self.lastX = 0;
self.lastY = 0;
self.movingRight = true;
self.update = function () {
self.lastX = self.x;
self.lastY = self.y;
// Move downward from top to bottom of box
self.y += self.speed; // Always move down
};
return self;
});
var Sans = Container.expand(function () {
var self = Container.call(this);
// Create Sans using proper Sans image asset
var sansGraphics = self.attachAsset('sans', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1,
scaleY: 1
});
self.attackTimer = 0;
self.attackInterval = 720; // Attack every 12 seconds
self.greenAttackTimer = 0;
self.greenAttackInterval = 10800; // Start at 12:00 timer (180 seconds * 60 fps)
self.greenAttackDuration = 7200; // 2 minutes duration from 12:00 to 10:00 (120 seconds * 60 fps)
self.isGreenAttack = false;
self.yellowAttackTimer = 0;
self.yellowAttackInterval = 21600; // Start at 9:00 timer (360 seconds * 60 fps)
self.yellowAttackDuration = 7200; // 2 minute duration from 9:00 to 7:00 (120 seconds * 60 fps)
self.isYellowAttack = false;
self.dodgesLeft = 10;
self.isDead = false;
self.originalX = 0;
self.originalY = 0;
self.dodge = function () {
if (self.dodgesLeft > 0 && !self.isDead) {
self.dodgesLeft--;
// Move to random position near original location
var dodgeDistance = 100;
var angle = Math.random() * Math.PI * 2;
var newX = self.originalX + Math.cos(angle) * dodgeDistance;
var newY = self.originalY + Math.sin(angle) * dodgeDistance;
tween(self, {
x: newX,
y: newY
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
// Return to original position
tween(self, {
x: self.originalX,
y: self.originalY
}, {
duration: 300,
easing: tween.easeIn
});
}
});
if (self.dodgesLeft <= 0) {
self.isDead = true;
tween(sansGraphics, {
alpha: 0.3
}, {
duration: 500
});
}
return true;
}
return false;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0xf0f8ff
});
/****
* Game Code
****/
// Box dimensions
var boxX = 224; // Center the 1600px box in 2048px screen
var boxY = 766; // Center the 1200px box in 2732px screen
var boxWidth = 1600;
var boxHeight = 1200;
// Create box boundary
var boundary = game.addChild(new BoxBoundary());
boundary.x = boxX;
boundary.y = boxY;
// Create heart character
var heart = game.addChild(new Heart());
heart.x = boxX + boxWidth / 2; // Center of box
heart.y = boxY + boxHeight / 2; // Center of box
// Create the three characters
// Sans on the left side (bone thrower)
var sans = game.addChild(new Sans());
sans.x = boxX - 150; // Left side of box
sans.y = boxY + boxHeight / 2; // Middle height
// Asgore in the middle (anchor owner)
var asgore = game.addChild(new Asgore());
asgore.x = boxX + boxWidth / 2; // Center of box horizontally
asgore.y = boxY - 150; // Above the box
// Flowey on the right side (ivy sender)
var flowey = game.addChild(new Flowey());
flowey.x = boxX + boxWidth + 150; // Right side of box
flowey.y = boxY + boxHeight / 2; // Middle height
// Create lives display
var livesTxt = new Text2('Lives: 92', {
size: 80,
fill: 0x000000
});
livesTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(livesTxt);
// Create timer display
var gameTime = 900; // 15 minutes in seconds
var timerTxt = new Text2('Time: 15:00', {
size: 80,
fill: 0x000000
});
timerTxt.anchor.set(1, 0); // Right align
LK.gui.topRight.addChild(timerTxt);
// Create "THREE LEFT" text in red under timer
var threeTxt = new Text2('THREE LEFT', {
size: 60,
fill: 0xff0000
});
threeTxt.anchor.set(1, 0); // Right align
threeTxt.y = 100; // Position below timer
LK.gui.topRight.addChild(threeTxt);
// Create health button
var healthButton = LK.getAsset('healthButton', {
anchorX: 0.5,
anchorY: 0.5
});
healthButton.x = boxX + boxWidth / 2; // Center horizontally with the box
healthButton.y = boxY + boxHeight + 100; // Position below the box (outside)
game.addChild(healthButton);
// Create health button text
var healthButtonText = new Text2('HEALTH +20', {
size: 40,
fill: 0x000000
});
healthButtonText.anchor.set(0.5, 0.5);
healthButtonText.x = healthButton.x;
healthButtonText.y = healthButton.y;
game.addChild(healthButtonText);
// Create yellow heart special button
var yellowButton = LK.getAsset('healthButton', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
yellowButton.x = boxX + boxWidth / 2; // Position directly below health button
yellowButton.y = boxY + boxHeight + 200; // Position below the health button
yellowButton.tint = 0xffff00; // Yellow color
game.addChild(yellowButton);
// Create yellow button text
var yellowButtonText = new Text2('YELLOW SHOT', {
size: 40,
fill: 0x000000
});
yellowButtonText.anchor.set(0.5, 0.5);
yellowButtonText.x = yellowButton.x;
yellowButtonText.y = yellowButton.y;
game.addChild(yellowButtonText);
// Health button cooldown variables
var healthButtonCooldown = 0;
var healthButtonMaxCooldown = 1200; // 20 seconds at 60fps
var healthButtonEnabled = true;
// Store original positions for characters
sans.originalX = sans.x;
sans.originalY = sans.y;
// Dragging variables
var isDragging = false;
var dragOffsetX = 0;
var dragOffsetY = 0;
// Constraint function to keep heart within boundaries
function constrainHeartPosition() {
var heartRadius = 50; // Half of heart width/height
// Left boundary
if (heart.x - heartRadius < boxX + 8) {
heart.x = boxX + 8 + heartRadius;
}
// Right boundary
if (heart.x + heartRadius > boxX + boxWidth - 8) {
heart.x = boxX + boxWidth - 8 - heartRadius;
}
// Top boundary
if (heart.y - heartRadius < boxY + 8) {
heart.y = boxY + 8 + heartRadius;
}
// Bottom boundary
if (heart.y + heartRadius > boxY + boxHeight - 8) {
heart.y = boxY + boxHeight - 8 - heartRadius;
}
}
// Player control variables
var targetX = 0;
var targetY = 0;
var isPlayerControlling = false;
// Touch/mouse down event
game.down = function (x, y, obj) {
// Check if health button was clicked
var buttonDistance = Math.sqrt(Math.pow(x - healthButton.x, 2) + Math.pow(y - healthButton.y, 2));
if (buttonDistance < 100 && healthButtonEnabled) {
// Increase health by 20
lives += 20;
livesTxt.setText('Lives: ' + lives);
// Start cooldown
healthButtonCooldown = healthButtonMaxCooldown;
healthButtonEnabled = false;
// Visual feedback - flash button
tween(healthButton, {
tint: 0xffff00
}, {
duration: 200,
onFinish: function onFinish() {
tween(healthButton, {
tint: 0x888888
}, {
duration: 200
});
}
});
// Update button text to show cooldown
healthButtonText.setText('COOLDOWN');
return; // Don't process normal movement
}
// Check if yellow button was clicked
var yellowButtonDistance = Math.sqrt(Math.pow(x - yellowButton.x, 2) + Math.pow(y - yellowButton.y, 2));
if (yellowButtonDistance < 100 && heart.isYellow && bullets.length < maxBullets) {
var bullet = new Bullet();
bullet.x = heart.x;
bullet.y = heart.y;
bullet.lastX = bullet.x;
bullet.lastY = bullet.y;
// Set direction upward
bullet.setDirection(heart.x, heart.y - 100, heart.x, heart.y);
bullets.push(bullet);
game.addChild(bullet);
// Visual feedback - flash yellow button
tween(yellowButton, {
tint: 0xffffff
}, {
duration: 100,
onFinish: function onFinish() {
tween(yellowButton, {
tint: 0xffff00
}, {
duration: 100
});
}
});
return; // Don't process normal movement
}
// Handle yellow heart shooting
if (heart.isYellow && bullets.length < maxBullets) {
var bullet = new Bullet();
bullet.x = heart.x;
bullet.y = heart.y;
bullet.lastX = bullet.x;
bullet.lastY = bullet.y;
bullet.setDirection(x, y, heart.x, heart.y);
bullets.push(bullet);
game.addChild(bullet);
return; // Don't process normal movement when shooting
}
isPlayerControlling = true;
targetX = x;
targetY = y;
};
// Touch/mouse move event
game.move = function (x, y, obj) {
if (isPlayerControlling) {
targetX = x;
targetY = y;
}
};
// Touch/mouse up event
game.up = function (x, y, obj) {
isPlayerControlling = false;
};
// Lives system
var lives = 92;
// Movement variables
var heartSpeed = 8;
var movementDirection = {
x: 1,
y: 0.5
}; // Initial movement direction
var lastDirectionChangeTime = 0;
var directionChangeInterval = 2000; // Change direction every 2 seconds
// Gaster Blaster and beam system
var gasterBlasters = [];
var gasterBeams = [];
var maxBlasters = 10;
var blasterSpawnTimer = 0;
var blasterSpawnInterval = 720; // Spawn every 12 seconds at 60fps
var waveIntensity = 1;
// Anchor system
var anchors = [];
var maxAnchors = 2;
var anchorSpawnTimer = 0;
var anchorSpawnInterval = 240; // Spawn every 4 seconds at 60fps
// Ivy system
var ivies = [];
var maxIvies = 5;
var ivySpawnTimer = 0;
var ivySpawnInterval = 600; // Spawn every 10 seconds at 60fps
// Slow effect system
var isSlowed = false;
var slowEndTime = 0;
var normalHeartSpeed = 8;
// Bullet system
var bullets = [];
var maxBullets = 20;
// Function to spawn a new Gaster Blaster at random edge position
function spawnGasterBlaster() {
if (gasterBlasters.length >= maxBlasters) return;
var blaster = new GasterBlaster();
var edge = Math.floor(Math.random() * 4); // 0=top, 1=right, 2=bottom, 3=left
// Position blaster at random point on selected edge
switch (edge) {
case 0:
// Top edge
blaster.x = boxX + Math.random() * boxWidth;
blaster.y = boxY - 100;
break;
case 1:
// Right edge
blaster.x = boxX + boxWidth + 100;
blaster.y = boxY + Math.random() * boxHeight;
break;
case 2:
// Bottom edge
blaster.x = boxX + Math.random() * boxWidth;
blaster.y = boxY + boxHeight + 100;
break;
case 3:
// Left edge
blaster.x = boxX - 100;
blaster.y = boxY + Math.random() * boxHeight;
break;
}
gasterBlasters.push(blaster);
game.addChild(blaster);
// Start charging to attack the heart
blaster.startCharge(heart.x, heart.y);
}
// Function to spawn a new anchor at random position within the box
function spawnAnchor() {
if (anchors.length >= maxAnchors) return;
var anchor = new Anchor();
// Position anchor within the box boundaries for horizontal movement
var anchorHalfWidth = 60; // Half of 120px anchor width
var startFromLeft = Math.random() < 0.5;
if (startFromLeft) {
anchor.x = boxX + 8 + anchorHalfWidth; // Start from left edge inside box
anchor.movingRight = true; // Moving right
} else {
anchor.x = boxX + boxWidth - 8 - anchorHalfWidth; // Start from right edge inside box
anchor.movingRight = false; // Moving left
}
// Random Y position within the box, constrained to keep full anchor height inside
var anchorHalfHeight = 600; // Half of 1200px anchor height
anchor.y = boxY + anchorHalfHeight + Math.random() * (boxHeight - 2 * anchorHalfHeight);
anchor.lastX = anchor.x;
anchor.lastY = anchor.y;
anchors.push(anchor);
game.addChild(anchor);
}
// Function to spawn a new ivy hanging from top of box like an anchor
function spawnIvy() {
if (ivies.length >= maxIvies) return;
var ivy = new Ivy();
// Position ivy at the very top of box to move downward (anchor point is at top of ivy)
ivy.y = boxY + 8; // Start from top edge inside box
// Random X position within the box, constrained to keep full ivy width inside
var ivyHalfWidth = 60; // Half of 120px ivy width
ivy.x = boxX + ivyHalfWidth + Math.random() * (boxWidth - 2 * ivyHalfWidth);
ivy.lastX = ivy.x;
ivy.lastY = ivy.y;
ivies.push(ivy);
game.addChild(ivy);
}
// Game update loop
game.update = function () {
// Update timer every second (60 ticks = 1 second at 60fps)
if (LK.ticks % 60 === 0 && gameTime > 0) {
gameTime--;
var minutes = Math.floor(gameTime / 60);
var seconds = gameTime % 60;
var timeString = minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
timerTxt.setText('Time: ' + timeString);
}
// Update health button cooldown
if (!healthButtonEnabled) {
healthButtonCooldown--;
if (healthButtonCooldown <= 0) {
healthButtonEnabled = true;
healthButtonText.setText('HEALTH +20');
tween(healthButton, {
tint: 0x00ff00
}, {
duration: 300
});
} else {
var cooldownSeconds = Math.ceil(healthButtonCooldown / 60);
healthButtonText.setText('COOLDOWN ' + cooldownSeconds + 's');
}
}
// Check if Sans and Asgore are both dead for Flowey transformation
if (sans.isDead && asgore.isDead && !flowey.isFinalForm && !flowey.isDead) {
flowey.transformToFinalForm();
// Update ivy system for final form
maxIvies = 10;
flowey.attackInterval = 300;
}
// Increase wave intensity over time
if (LK.ticks % 600 === 0) {
// Every 10 seconds
waveIntensity = Math.min(waveIntensity + 0.5, 4);
maxBlasters = Math.min(Math.floor(2 + waveIntensity), 10);
blasterSpawnInterval = Math.max(Math.floor(720 - waveIntensity * 120), 300);
}
// Update Sans attack timer and spawn Gaster Blasters (only if alive)
if (!sans.isDead) {
sans.attackTimer++;
sans.greenAttackTimer++;
// Check for green heart attack when timer reaches 12:00 (180 seconds elapsed)
var elapsedTime = 900 - gameTime; // Calculate elapsed time in seconds
var elapsedTicks = elapsedTime * 60; // Convert to ticks
if (elapsedTicks >= sans.greenAttackInterval && elapsedTicks < sans.greenAttackInterval + sans.greenAttackDuration && !sans.isGreenAttack) {
sans.isGreenAttack = true;
heart.setGreenMode(true);
// Position heart in center of box and disable movement
heart.x = boxX + boxWidth / 2;
heart.y = boxY + boxHeight / 2;
heart.shieldX = 0;
heart.shieldY = 0;
}
// End green heart attack when timer reaches 10:00 (300 seconds elapsed)
if (sans.isGreenAttack && elapsedTicks >= sans.greenAttackInterval + sans.greenAttackDuration) {
sans.isGreenAttack = false;
heart.setGreenMode(false);
}
// Check for yellow heart attack when timer reaches 9:00 (360 seconds elapsed)
if (elapsedTicks >= sans.yellowAttackInterval && elapsedTicks < sans.yellowAttackInterval + sans.yellowAttackDuration && !sans.isYellowAttack) {
sans.isYellowAttack = true;
heart.setYellowMode(true);
}
// End yellow heart attack when timer reaches 7:00 (480 seconds elapsed)
if (sans.isYellowAttack && elapsedTicks >= sans.yellowAttackInterval + sans.yellowAttackDuration) {
sans.isYellowAttack = false;
heart.setYellowMode(false);
}
if (sans.attackTimer >= sans.attackInterval && !sans.isGreenAttack) {
// Sans attack: make heart blue and enable physics
heart.setBlueMode(true);
heart.velocityY = 0;
heart.onGround = false;
heart.groundY = boxY + boxHeight - 8 - 50; // Ground level accounting for heart radius
// Sans spawns Gaster Blasters
var spawnCount = Math.random() < 0.4 ? Math.floor(waveIntensity) : 1;
for (var s = 0; s < spawnCount && gasterBlasters.length < maxBlasters; s++) {
spawnGasterBlaster();
}
sans.attackTimer = 0;
}
}
// Update Asgore attack timer and spawn anchors (only if alive)
if (!asgore.isDead) {
asgore.attackTimer++;
if (asgore.attackTimer >= asgore.attackInterval) {
// Asgore sends anchors
spawnAnchor();
asgore.attackTimer = 0;
}
}
// Update Flowey attack timer and spawn ivies (only if alive)
if (!flowey.isDead) {
flowey.attackTimer++;
if (flowey.attackTimer >= flowey.attackInterval) {
// Flowey sends ivies
var spawnCount = flowey.isFinalForm ? 10 : Math.floor(Math.random() * 2) + 2; // Final form sends 10, normal sends 2-3
for (var i = 0; i < spawnCount && ivies.length < maxIvies; i++) {
spawnIvy();
}
flowey.attackTimer = 0;
}
}
// Handle slow effect expiry
if (isSlowed && LK.ticks >= slowEndTime) {
isSlowed = false;
heartSpeed = normalHeartSpeed;
}
// Update all Gaster Blasters and spawn beams when ready
for (var i = gasterBlasters.length - 1; i >= 0; i--) {
var blaster = gasterBlasters[i];
// Check if blaster is ready to fire
if (blaster.isReady && blaster.beamTimer === 0) {
var beam = blaster.fireBeam();
if (beam) {
gasterBeams.push(beam);
game.addChild(beam);
}
}
// Remove blasters that have finished their attack cycle
if (!blaster.isCharging && !blaster.isReady) {
blaster.destroy();
gasterBlasters.splice(i, 1);
continue;
}
}
// Update all beams and check for collisions
for (var j = gasterBeams.length - 1; j >= 0; j--) {
var beam = gasterBeams[j];
var beamBlockedByShield = false;
// Check if shield destroyed the beam in green mode
if (heart.isGreen) {
var shieldPos = heart.getShieldWorldPosition();
var shieldDistance = Math.sqrt(Math.pow(beam.x - shieldPos.x, 2) + Math.pow(beam.y - shieldPos.y, 2));
if (shieldDistance < 100) {
beam.destroy();
gasterBeams.splice(j, 1);
beamBlockedByShield = true;
continue;
}
}
// Check if beam hit the heart
if (beam.intersects(heart) && !beamBlockedByShield) {
// In green mode, don't take damage as shield should protect
if (!heart.isGreen) {
// Normal mode - always take damage
lives -= beam.damage;
livesTxt.setText('Lives: ' + lives);
// Flash screen white to indicate beam damage
LK.effects.flashScreen(0xffffff, 600);
// Check for game over
if (lives <= 0) {
LK.showGameOver();
return;
}
}
// Remove the beam that hit us
beam.destroy();
gasterBeams.splice(j, 1);
continue;
}
// Remove beams that are too far from the play area or inactive
var distanceFromCenter = Math.sqrt(Math.pow(beam.x - (boxX + boxWidth / 2), 2) + Math.pow(beam.y - (boxY + boxHeight / 2), 2));
if (distanceFromCenter > 2000 || !beam.isActive) {
beam.destroy();
gasterBeams.splice(j, 1);
continue;
}
}
// Update all anchors and check for collisions
for (var j = anchors.length - 1; j >= 0; j--) {
var anchor = anchors[j];
var anchorBlockedByShield = false;
// Check if shield destroyed the anchor in green mode
if (heart.isGreen) {
var shieldPos = heart.getShieldWorldPosition();
var shieldDistance = Math.sqrt(Math.pow(anchor.x - shieldPos.x, 2) + Math.pow(anchor.y - shieldPos.y, 2));
if (shieldDistance < 100) {
anchor.destroy();
anchors.splice(j, 1);
anchorBlockedByShield = true;
continue;
}
}
// Check if anchor hit the heart
if (anchor.intersects(heart) && !anchorBlockedByShield) {
// In green mode, don't take damage as shield should protect
if (!heart.isGreen) {
var shouldTakeDamage = false;
// Blue anchor: damage if player is moving
if (anchor.isBlue && isPlayerControlling) {
shouldTakeDamage = true;
}
// Orange anchor: damage if player is not moving
else if (!anchor.isBlue && !isPlayerControlling) {
shouldTakeDamage = true;
}
if (shouldTakeDamage) {
// Reduce lives by anchor damage (2)
lives -= anchor.damage;
livesTxt.setText('Lives: ' + lives);
// Flash screen purple to indicate anchor damage
LK.effects.flashScreen(0x8800ff, 700);
// Check for game over
if (lives <= 0) {
LK.showGameOver();
return;
}
}
}
// Remove the anchor that hit us
anchor.destroy();
anchors.splice(j, 1);
continue;
}
// Anchors now stay within box bounds, so no need to remove them for going off screen
// They will only be removed when hitting the heart
}
// Update all ivies and check for collisions
for (var k = ivies.length - 1; k >= 0; k--) {
var ivy = ivies[k];
var ivyBlockedByShield = false;
// Check if shield destroyed the ivy in green mode
if (heart.isGreen) {
var shieldPos = heart.getShieldWorldPosition();
var shieldDistance = Math.sqrt(Math.pow(ivy.x - shieldPos.x, 2) + Math.pow(ivy.y - shieldPos.y, 2));
if (shieldDistance < 100) {
ivy.destroy();
ivies.splice(k, 1);
ivyBlockedByShield = true;
continue;
}
}
// Check if ivy hit the heart
if (ivy.intersects(heart) && !ivyBlockedByShield) {
// In green mode, don't take damage as shield should protect
if (!heart.isGreen) {
// Normal mode - take damage and apply slow effect
lives -= ivy.damage;
livesTxt.setText('Lives: ' + lives);
// Apply slow effect (25% speed reduction)
isSlowed = true;
heartSpeed = normalHeartSpeed * 0.75;
slowEndTime = LK.ticks + 300; // Slow for 5 seconds at 60fps
// Flash screen green to indicate ivy damage and slow
LK.effects.flashScreen(0x00ff00, 600);
// Check for game over
if (lives <= 0) {
LK.showGameOver();
return;
}
}
// Remove the ivy that hit us
ivy.destroy();
ivies.splice(k, 1);
continue;
}
// Remove ivies that have reached the bottom of the box
// Since ivy anchor is at top (anchorY: 0), check when ivy top + height reaches bottom
if (ivy.y + 800 >= boxY + boxHeight - 8) {
ivy.destroy();
ivies.splice(k, 1);
continue;
}
}
// Update all bullets and check for collisions with attacks
for (var b = bullets.length - 1; b >= 0; b--) {
var bullet = bullets[b];
// Check bullet collision with beams
for (var beam_i = gasterBeams.length - 1; beam_i >= 0; beam_i--) {
var beam = gasterBeams[beam_i];
if (bullet.intersects(beam)) {
// Destroy both bullet and beam
bullet.destroy();
bullets.splice(b, 1);
beam.destroy();
gasterBeams.splice(beam_i, 1);
continue;
}
}
// Skip further checks if bullet was destroyed
if (b >= bullets.length) continue;
// Check bullet collision with anchors
for (var anchor_i = anchors.length - 1; anchor_i >= 0; anchor_i--) {
var anchor = anchors[anchor_i];
if (bullet.intersects(anchor)) {
// Destroy both bullet and anchor
bullet.destroy();
bullets.splice(b, 1);
anchor.destroy();
anchors.splice(anchor_i, 1);
continue;
}
}
// Skip further checks if bullet was destroyed
if (b >= bullets.length) continue;
// Check bullet collision with ivies
for (var ivy_i = ivies.length - 1; ivy_i >= 0; ivy_i--) {
var ivy = ivies[ivy_i];
if (bullet.intersects(ivy)) {
// Destroy both bullet and ivy
bullet.destroy();
bullets.splice(b, 1);
ivy.destroy();
ivies.splice(ivy_i, 1);
continue;
}
}
// Skip further checks if bullet was destroyed
if (b >= bullets.length) continue;
// Remove bullets that are too far from play area
var bulletDistanceFromCenter = Math.sqrt(Math.pow(bullet.x - (boxX + boxWidth / 2), 2) + Math.pow(bullet.y - (boxY + boxHeight / 2), 2));
if (bulletDistanceFromCenter > 2000) {
bullet.destroy();
bullets.splice(b, 1);
continue;
}
}
if (isPlayerControlling) {
if (heart.isGreen) {
// Green heart mode: heart stays in center, only shield moves
heart.updateShieldPosition(targetX, targetY);
} else if (heart.isYellow) {
// Yellow heart mode: normal movement but shooting on click
var deltaX = targetX - heart.x;
var deltaY = targetY - heart.y;
var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance > 5) {
movementDirection.x = deltaX / distance;
movementDirection.y = deltaY / distance;
heart.x += movementDirection.x * heartSpeed;
heart.y += movementDirection.y * heartSpeed;
}
} else if (heart.isBlue) {
// Blue heart physics: left/right movement and jumping
var deltaX = targetX - heart.x;
// Horizontal movement
if (Math.abs(deltaX) > 5) {
var horizontalDirection = deltaX > 0 ? 1 : -1;
heart.x += horizontalDirection * heartSpeed;
}
// Jump if touching upward and on ground
if (targetY < heart.y - 20 && heart.onGround) {
heart.velocityY = heart.jumpPower;
heart.onGround = false;
}
} else {
// Normal movement when not blue
// Calculate direction toward target
var deltaX = targetX - heart.x;
var deltaY = targetY - heart.y;
var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// Move toward target if not already there
if (distance > 5) {
// Normalize direction and apply speed
movementDirection.x = deltaX / distance;
movementDirection.y = deltaY / distance;
heart.x += movementDirection.x * heartSpeed;
heart.y += movementDirection.y * heartSpeed;
}
}
}
// Apply blue heart physics
if (heart.isBlue) {
// Apply gravity
heart.velocityY += heart.gravity;
heart.y += heart.velocityY;
// Ground collision
if (heart.y >= heart.groundY) {
heart.y = heart.groundY;
heart.velocityY = 0;
heart.onGround = true;
}
// Reset blue heart after 4 seconds
if (sans.attackTimer > 240) {
// 4 seconds at 60fps
heart.setBlueMode(false);
sans.attackTimer = 0; // Reset timer to prevent staying blue
}
}
// Ensure heart stays within bounds each frame
constrainHeartPosition();
};
Undertale Heart. In-Game asset. 2d. High contrast. No shadows
Undertale bone with black outline. In-Game asset. 2d. High contrast. No shadows
Undertale asgore spear. In-Game asset. 2d. High contrast. No shadows
Ivy. In-Game asset. 2d. High contrast. No shadows
Sans Undertale. In-Game asset. 2d. High contrast. No shadows
Asgore Undertale. In-Game asset. 2d. High contrast. No shadows
Flowey Undertale but with 6 souls (Orange,Green,Yellow,blue,Purple, Light Blue). In-Game asset. 2d. High contrast. No shadows
Flat staring gaster blaster Undertale. In-Game asset. 2d. High contrast. No shadows
Undertale heart but blue. In-Game asset. 2d. High contrast. No shadows
Green Heart Undertale. In-Game asset. 2d. High contrast. No shadows
Yellow heart Undertale. In-Game asset. 2d. High contrast. No shadows