/****
* 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