/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var House = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('house', {
anchorX: 0.5,
anchorY: 1.0
});
self.active = true;
// Remove scrolling from update, handled by tweens
self.update = function () {};
return self;
});
var Mailbox = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('mailbox', {
anchorX: 0.5,
anchorY: 1.0
});
self.hit = false;
self.active = true;
self.update = function () {}; // no auto-scrolling
return self;
});
var Newspaper = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('newspaper', {
anchorX: 0.5,
anchorY: 0.5
});
// Physics properties
self.vx = 0;
self.vy = 0;
self.gravity = 0.4;
self.active = true;
// Store starting position for scaling math
self.startY = null;
self.update = function () {
if (!self.active) {
return;
}
// Apply velocity
self.x += self.vx;
self.y += self.vy;
// Apply gravity
self.vy += self.gravity;
// Record starting Y on first update
if (self.startY === null) {
self.startY = self.y;
}
// Scale down as it rises (depth effect)
var travel = self.startY - self.y;
var scaleFactor = Math.max(0.2, 1 - travel / 1500);
graphics.scaleX = graphics.scaleY = scaleFactor;
// Cleanup when it goes "into the distance"
if (self.y < mailboxZoneY - 300 || self.y > 2732 || self.x < -200 || self.x > 2300) {
self.active = false;
// Trigger turn end if this was the last shot
if (currentHouse && !isHouseTransitioning) {
removeHouse(currentHouse, function () {
currentHouse = null;
hasThrownThisTurn = false; // reset throw flag
// Clear leftover mailboxes
for (var k = mailboxes.length - 1; k >= 0; k--) {
mailboxes[k].destroy();
mailboxes.splice(k, 1);
}
});
}
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x87ceeb
});
/****
* Game Code
****/
// green
// orange
game.setBackgroundColor(0x87ceeb);
// Start background music immediately when game loads
LK.playMusic('MothmanBoogie');
/****
* Color Interpolation Helper
****/
function lerpColor(c1, c2, t) {
var r1 = c1 >> 16 & 0xff,
g1 = c1 >> 8 & 0xff,
b1 = c1 & 0xff;
var r2 = c2 >> 16 & 0xff,
g2 = c2 >> 8 & 0xff,
b2 = c2 & 0xff;
var r = Math.round(r1 + (r2 - r1) * t);
var g = Math.round(g1 + (g2 - g1) * t);
var b = Math.round(b1 + (b2 - b1) * t);
return r << 16 | g << 8 | b;
}
/****
* Sun Path Parameters
****/
// Sun path parameters
var sunStartX = 100; // left side
var sunEndX = 1948; // right side
var sunBaseY = 600; // baseline (sky zone level)
var sunArcHeight = 400; // how high the sun climbs
/****
* Zone Setup
****/
// Define Y positions for zones (adjust as needed to match your reference image)
var streetZoneY = 2000; // Green bottom zone where slingshot sits
var mailboxZoneY = 1200; // Orange middle zone where mailbox spawns
var houseZoneY = 1000; // Houses aligned just above mailbox zone
// Ground reference (for houses)
var groundY = 2400;
/****
* Visual Zone Placeholders
****/
// Street zone background (green)
var streetZone = game.addChild(LK.getAsset('streetZone', {
anchorX: 0.5,
anchorY: 0.5
}));
streetZone.x = 1024;
streetZone.y = streetZoneY;
// Mailbox zone background (orange)
var mailboxZone = game.addChild(LK.getAsset('mailboxZone', {
anchorX: 0.5,
anchorY: 0.5
}));
mailboxZone.x = 1024;
mailboxZone.y = mailboxZoneY;
// Treeline container to move with houses
var treelineContainer = new Container();
// compute how many tiles we need to cover the screen plus one extra on each side
var tileWidth = LK.getAsset('treeline', {}).width;
var screenWidth = 2048; // your game width
var sideBuffers = 2; // two extra tiles on left side, one on right
var tileCount = Math.ceil(screenWidth / tileWidth) + sideBuffers * 2 + 1; // +1 for additional right side duplicate
var treelines = [];
for (var i = 0; i < tileCount; i++) {
var tile = treelineContainer.addChild(LK.getAsset('treeline', {
anchorX: 0,
anchorY: 1
}));
// position so that the first tile starts at –tileWidth
tile.x = (i - sideBuffers) * tileWidth;
tile.y = 0;
treelines.push(tile);
}
// lock the whole strip at the correct vertical position
treelineContainer.x = 0;
treelineContainer.y = mailboxZoneY - 350;
// Add treeline container before other game elements to render behind them
game.addChild(treelineContainer);
// Parallax scrolling parameters
var baseSpeed = 5; // Base movement speed for reference
var treeParallaxFactor = 0.2; // Trees move at 20% of house speed for depth effect
// Update function for treeline infinite scroll
function updateTreeline(speed) {
for (var i = 0; i < treelines.length; i++) {
var t = treelines[i];
t.x -= speed;
// Snap positions to integers to prevent floating-point drift
t.x = Math.round(t.x);
}
// After moving, check for wrap and re-align all treelines to be flush
// Find the leftmost treeline
var leftMostIndex = 0;
var leftMostX = treelines[0].x;
for (var i = 1; i < treelines.length; i++) {
if (treelines[i].x < leftMostX) {
leftMostX = treelines[i].x;
leftMostIndex = i;
}
}
// Now, starting from leftmost, set each treeline's x so they are flush
var startX = treelines[leftMostIndex].x;
for (var i = 0; i < treelines.length; i++) {
var idx = (leftMostIndex + i) % treelines.length;
if (i === 0) {
treelines[idx].x = startX;
} else {
treelines[idx].x = treelines[(idx - 1 + treelines.length) % treelines.length].x + tileWidth;
}
}
// If any treeline is now off the left edge, move it to the right of the last one
for (var i = 0; i < treelines.length; i++) {
var t = treelines[i];
if (t.x + tileWidth < 0) {
// Find the rightmost treeline
var rightMost = Math.max.apply(Math, treelines.map(function (tr) {
return tr.x;
}));
t.x = rightMost + tileWidth - 1; // subtract 1px to overlap
}
}
}
// Game variables
var mothman;
var sun;
var slingshot;
var newspapers = [];
var mailboxes = [];
var houses = [];
var trajectoryDots = [];
// Create one reusable graphics line for the trajectory
var trajectoryLine = new Container();
// Pull-back origin indicator will be created after slingshot for proper layering
var pullOriginIndicator;
var pennies = 0;
var targetPennies = 650;
var gameTime = 0;
var maxGameTime = 18000; // 5 minutes at 60fps
var isAiming = false;
var aimStartX = 0;
var aimStartY = 0;
var aimPower = 0;
var aimAngle = 0;
var hasThrownThisTurn = false;
// Create UI
var paycheckText = new Text2('Paycheck: $0.00', {
size: 65,
fill: 0x006400,
font: "'Comic Sans MS', cursive"
});
paycheckText.anchor.set(0.5, 0);
paycheckText.y = 100;
LK.gui.top.addChild(paycheckText);
function updatePaycheck() {
paycheckText.setText('Paycheck: $' + (pennies / 100).toFixed(2));
}
// Power bar background + fill
var powerBarBG = LK.getAsset('powerBarBGShape', {
anchorX: 0.5,
anchorY: 0.5
});
powerBarBG.x = 1024;
powerBarBG.y = 200; // top center instead of bottom
LK.gui.top.addChild(powerBarBG);
var powerBarFill = LK.getAsset('powerBarFillShape', {
width: 0,
// start empty
anchorX: 0,
anchorY: 0.5
});
powerBarFill.x = powerBarBG.x - 150;
powerBarFill.y = powerBarBG.y;
LK.gui.top.addChild(powerBarFill);
// Create mothman
mothman = game.addChild(new Container());
var mothmanGraphics = mothman.attachAsset('mothman', {
anchorX: 0.5,
anchorY: 0.5
});
mothman.x = 200;
mothman.y = 400;
// Hide mothman (no sprite during gameplay)
mothman.visible = false;
// Create pull origin indicator after mothman to ensure proper layering in front
pullOriginIndicator = LK.getAsset('aimOriginShape', {
anchorX: 0.5,
anchorY: 0.5
});
// Position the aim origin indicator at slingshot position, moved up by 100 pixels
pullOriginIndicator.x = 1024;
pullOriginIndicator.y = 2000; // streetZoneY (2000) + 100 - 100 = 2000
pullOriginIndicator.visible = true; // Always visible
// Add aiming visuals after mothman to ensure they render on top
game.addChild(trajectoryLine);
game.addChild(pullOriginIndicator);
// Create sun
sun = game.addChild(new Container());
var sunGraphics = sun.attachAsset('sun', {
anchorX: 0.5,
anchorY: 0.5
});
sun.x = sunStartX;
sun.y = sunBaseY - sunArcHeight;
/****
* Draw Sun Arc Path
****/
// Draw sun trajectory arc with dotted path
function drawSunPath() {
for (var t = 0; t <= 1; t += 0.05) {
var x = sunStartX + (sunEndX - sunStartX) * t;
var y = sunBaseY - Math.sin(t * Math.PI) * sunArcHeight;
var dot = game.addChild(LK.getAsset('trajectory', {
width: 6,
height: 6,
color: 0xffff66,
// pale yellow
anchorX: 0.5,
anchorY: 0.5
}));
dot.x = x;
dot.y = y;
dot.alpha = 0.3; // semi-transparent
}
}
drawSunPath();
// Create slingshot area - positioned in street zone center
slingshot = game.addChild(new Container());
var slingshotGraphics = slingshot.attachAsset('slingshot', {
anchorX: 0.5,
anchorY: 0.5
});
slingshot.x = 1024; // center of screen
slingshot.y = streetZoneY + 100;
// Slingshot bands (for stretch feedback)
var bandLeft = new Container();
var bandRight = new Container();
game.addChild(bandLeft);
game.addChild(bandRight);
// Ground level
var groundY = 2400;
// Turn-based house cycle variables
var currentHouse = null;
var isHouseTransitioning = false;
var turnComplete = true;
var prevHouseX = 1024; // Track previous house X position for treeline scroll
function ensureMailboxForTurn() {
// Clear leftover mailboxes
for (var k = mailboxes.length - 1; k >= 0; k--) {
mailboxes[k].destroy();
mailboxes.splice(k, 1);
}
createMailbox(); // always create (no RNG)
}
function createHouse() {
var house = new House();
// Start just off-screen to the right
house.x = 2200;
house.y = mailboxZoneY + 250; // roughly center mailbox zone
houses.push(house);
game.addChild(house);
currentHouse = house;
isHouseTransitioning = true;
turnComplete = false;
hasThrownThisTurn = false;
// Tween it into center of mailbox zone
tween(house, {
x: 1024
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
isHouseTransitioning = false;
ensureMailboxForTurn(); // mailbox priority
}
});
return house;
}
function removeHouse(house, onComplete) {
if (!house) {
return;
}
isHouseTransitioning = true;
tween(house, {
x: -200
}, {
duration: 1000,
easing: tween.easeIn,
onFinish: function onFinish() {
house.destroy();
var index = houses.indexOf(house);
if (index !== -1) {
houses.splice(index, 1);
}
isHouseTransitioning = false;
turnComplete = true;
if (onComplete) {
onComplete();
}
}
});
}
function createMailbox() {
if (!currentHouse) {
return;
}
var mailbox = new Mailbox();
// Calculate house position and width to avoid overlap
var houseX = currentHouse.x;
var houseWidth = 900; // house asset width
var houseLeftEdge = houseX - houseWidth * 0.5;
var houseRightEdge = houseX + houseWidth * 0.5;
// Generate random position that doesn't overlap with house
var mailboxX;
var attempts = 0;
do {
mailboxX = 400 + Math.random() * 1200; // stays in safe horizontal range
attempts++;
// Safety check to prevent infinite loop
if (attempts > 50) {
// If we can't find a spot, place it far from house
if (houseX > 1024) {
mailboxX = 500; // place on left side
} else {
mailboxX = 1500; // place on right side
}
break;
}
} while (mailboxX >= houseLeftEdge - 100 && mailboxX <= houseRightEdge + 100);
mailbox.x = mailboxX;
mailbox.y = mailboxZoneY + (Math.random() * 200 - 100);
// Random horizontal flip
if (Math.random() < 0.5) {
mailbox.scaleX = -1;
} else {
mailbox.scaleX = 1;
}
mailboxes.push(mailbox);
game.addChild(mailbox);
// Ensure mailbox is drawn in front of house
game.setChildIndex(mailbox, game.getChildIndex(currentHouse) + 1);
return mailbox;
}
function shootNewspaper(power, angle) {
var newspaper = new Newspaper();
newspaper.x = 1024; // slingshot center
newspaper.y = 2200; // bottom
// Use power + angle for velocity
newspaper.vx = Math.cos(angle) * power;
newspaper.vy = Math.sin(angle) * power;
newspapers.push(newspaper);
game.addChild(newspaper);
LK.getSound('Paperthrow').play();
}
function updateSlingshotBands(endX, endY) {
bandLeft.removeChildren();
bandRight.removeChildren();
// Don't draw visible bands - keep containers for positioning only
}
function calculateScore(distance) {
if (distance < 15) {
return 5;
} // Perfect hit
if (distance < 25) {
return 4;
} // Great hit
if (distance < 35) {
return 3;
} // Good hit
if (distance < 45) {
return 2;
} // OK hit
return 1; // Glancing hit
}
function payoutFor(distancePx) {
if (distancePx <= 25) return {
cents: 25,
label: '+25¢ Perfect!'
};
if (distancePx <= 60) return {
cents: 15,
label: '+15¢ Very close'
};
if (distancePx <= 120) return {
cents: 5,
label: '+5¢ Close'
};
return null;
}
function updateTrajectory(originX, originY, pullX, pullY) {
// Clear any previous drawing
trajectoryLine.removeChildren();
if (!isAiming) return;
// Launch vector = opposite of pull
var dx = originX - pullX;
var dy = originY - pullY;
var power = Math.sqrt(dx * dx + dy * dy) * 0.1;
var angle = Math.atan2(dy, dx);
// Launch velocity
var vX = Math.cos(angle) * power;
var vY = Math.sin(angle) * power;
// Choose a projection distance (how long the line should be)
var projectionTime = 80; // tweak for how far line extends
var g = 0.3;
// Predict end point of the line
var endX = originX + vX * projectionTime;
var endY = originY + vY * projectionTime - 0.5 * g * projectionTime * projectionTime;
// Create line graphic
var lineGraphic = trajectoryLine.addChild(LK.getAsset('trajectory', {
width: Math.sqrt((endX - originX) * (endX - originX) + (endY - originY) * (endY - originY)),
height: 4,
color: 0x00ff00,
anchorX: 0,
anchorY: 0.5
}));
lineGraphic.x = originX;
lineGraphic.y = originY;
lineGraphic.rotation = Math.atan2(endY - originY, endX - originX);
lineGraphic.alpha = 0.7;
}
// Game input handlers - natural slingshot controls
game.down = function (x, y, obj) {
// Only allow aiming if player clicks/touches near the slingshot
if (y > streetZoneY - 200) {
isAiming = true;
aimStartX = x;
aimStartY = y;
// Pull origin indicator is already positioned and visible
// No need to reposition it during aiming
}
};
game.move = function (x, y, obj) {
if (isAiming) {
// Draw a preview trajectory
updateTrajectory(1024, 2000, x, y);
// Compute pull distance for power
var dx = 1024 - x;
var dy = 2200 - y;
var pullDistance = Math.sqrt(dx * dx + dy * dy);
var maxPull = 600; // clamp so bar doesn't overflow
var percent = Math.min(1, pullDistance / maxPull);
// Dynamic color based on power level
var color = 0x00ff00; // green
if (percent > 0.66) {
color = 0xff0000; // red
} else if (percent > 0.33) {
color = 0xffff00; // yellow
}
// Re-create the fill shape with new color and width
powerBarFill.destroy();
powerBarFill = LK.getAsset('powerBarFillShape', {
width: percent * 300,
color: color,
anchorX: 0,
anchorY: 0.5
});
powerBarFill.x = powerBarBG.x - 150;
powerBarFill.y = powerBarBG.y;
LK.gui.top.addChild(powerBarFill);
}
};
game.up = function (x, y, obj) {
if (!isAiming || hasThrownThisTurn) return;
var originX = 1024,
originY = 2000; // slingshot base moved up by 100 pixels
var pullX = x - originX;
var pullY = y - originY;
// Launch vector = opposite of pull
var launchX = -pullX;
var launchY = -pullY;
// Power is proportional to pull distance
var pullDistance = Math.sqrt(launchX * launchX + launchY * launchY);
var powerScale = 0.15; // tweak this number to control speed
var power = pullDistance * powerScale;
// Angle from pull-back vector
var angle = Math.atan2(launchY, launchX);
if (power > 2) {
shootNewspaper(power, angle);
hasThrownThisTurn = true;
}
// Reset aiming visuals
isAiming = false;
trajectoryLine.removeChildren();
// Pull origin indicator stays visible
powerBarFill.width = 0;
};
game.update = function () {
gameTime++;
// Update sun position (moves across sky as timer)
var timeProgress = gameTime / maxGameTime;
sun.x = sunStartX + (sunEndX - sunStartX) * timeProgress;
sun.y = sunBaseY - Math.sin(timeProgress * Math.PI) * sunArcHeight;
// Sky color progression with smooth transitions
var skyColors = [{
t: 0.0,
color: 0xE6E6FA
},
// Lavender
{
t: 0.2,
color: 0xADD8E6
},
// Light Blue
{
t: 0.4,
color: 0x87CEEB
},
// Bright Blue
{
t: 0.7,
color: 0xFFA500
},
// Orange
{
t: 1.0,
color: 0x4B0082
} // Deep Purple
];
// Find the two colors to interpolate between
for (var i = 0; i < skyColors.length - 1; i++) {
var c1 = skyColors[i];
var c2 = skyColors[i + 1];
if (timeProgress >= c1.t && timeProgress <= c2.t) {
var localT = (timeProgress - c1.t) / (c2.t - c1.t);
var blended = lerpColor(c1.color, c2.color, localT);
game.setBackgroundColor(blended);
break;
}
}
// Turn-based house cycle management
if (turnComplete && !isHouseTransitioning && !currentHouse) {
createHouse(); // mailbox is guaranteed in onFinish
}
// Watchdog: if for any reason a mailbox isn't present, create one.
if (currentHouse && !isHouseTransitioning && mailboxes.length === 0) {
ensureMailboxForTurn();
}
// Update newspapers
for (var i = newspapers.length - 1; i >= 0; i--) {
var newspaper = newspapers[i];
if (!newspaper.active) {
newspaper.destroy();
newspapers.splice(i, 1);
continue;
}
// Check if newspaper went off screen without hitting (miss)
if (newspaper.active && (newspaper.x > 2048 || newspaper.y > 2732)) {
newspaper.active = false;
// End the turn if no hit occurred
if (currentHouse && !isHouseTransitioning) {
removeHouse(currentHouse, function () {
currentHouse = null;
hasThrownThisTurn = false; // reset throw flag
// Clear mailboxes for this turn
for (var k = mailboxes.length - 1; k >= 0; k--) {
mailboxes[k].destroy();
mailboxes.splice(k, 1);
}
});
}
continue;
}
// Check mailbox collisions
for (var j = 0; j < mailboxes.length; j++) {
var m = mailboxes[j];
if (m.hit) continue;
// enlarge hitbox with tolerance for better hit detection
var gfx = m.children && m.children[0] ? m.children[0] : m;
var halfW = (gfx.width || 100) * 0.5;
var fullH = gfx.height || 100;
var left = m.x - halfW;
var right = m.x + halfW;
var top = m.y - fullH;
var bottom = m.y;
// loosen bounds by 20px in all directions
var tol = 20;
var insideX = newspaper.x >= left - tol && newspaper.x <= right + tol;
var insideY = newspaper.y >= top - tol && newspaper.y <= bottom + tol;
if (!insideX || !insideY) continue;
// compute accuracy off the mailbox "mouth"
var mouthY = m.y - fullH * 0.6;
var dx = newspaper.x - m.x;
var dy = newspaper.y - mouthY;
var dist = Math.sqrt(dx * dx + dy * dy);
// if payoutFor returns null (too far), still give a glancing hit
var res = payoutFor(dist) || {
cents: 5,
label: '+5¢ Close enough'
};
pennies += res.cents;
updatePaycheck();
m.hit = true;
newspaper.active = false;
// floating green payout text that fades away (large and bold)
var _float = new Text2(res.label, {
size: 72,
fill: 0x00aa00,
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
_float.anchor.set(0.5, 1);
_float.x = m.x;
_float.y = top - 10;
game.addChild(_float);
tween(_float, {
y: _float.y - 80,
alpha: 0
}, {
duration: 800,
onFinish: function onFinish() {
_float.destroy();
}
});
LK.effects.flashObject(m, 0x00ff00, 500);
LK.getSound('MailboxHit').play();
// end turn after scoring (keep existing cleanup behavior)
if (currentHouse && !isHouseTransitioning) {
removeHouse(currentHouse, function () {
currentHouse = null;
hasThrownThisTurn = false; // reset throw flag
for (var k = mailboxes.length - 1; k >= 0; k--) {
mailboxes[k].destroy();
mailboxes.splice(k, 1);
}
});
}
break;
}
}
// Update and clean up mailboxes
for (var i = mailboxes.length - 1; i >= 0; i--) {
var mailbox = mailboxes[i];
if (!mailbox.active) {
mailbox.destroy();
mailboxes.splice(i, 1);
}
}
// Update and clean up houses
for (var i = houses.length - 1; i >= 0; i--) {
var house = houses[i];
if (!house.active) {
house.destroy();
houses.splice(i, 1);
}
}
// Check win condition
if (pennies >= targetPennies) {
LK.showYouWin();
}
// Infinite-scroll treeline every frame with parallax effect
if (currentHouse) {
var houseSpeed = prevHouseX - currentHouse.x;
// Apply parallax factor to create depth - trees move slower than houses
var parallaxSpeed = houseSpeed * treeParallaxFactor;
updateTreeline(parallaxSpeed);
prevHouseX = currentHouse.x;
}
// Ensure treeline is always behind mailbox zone
game.setChildIndex(treelineContainer, game.getChildIndex(mailboxZone) - 1);
// Force aiming visuals to always render on top
game.setChildIndex(trajectoryLine, game.children.length - 1);
game.setChildIndex(pullOriginIndicator, game.children.length - 2);
// Check lose condition
if (gameTime >= maxGameTime) {
LK.showGameOver();
}
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var House = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('house', {
anchorX: 0.5,
anchorY: 1.0
});
self.active = true;
// Remove scrolling from update, handled by tweens
self.update = function () {};
return self;
});
var Mailbox = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('mailbox', {
anchorX: 0.5,
anchorY: 1.0
});
self.hit = false;
self.active = true;
self.update = function () {}; // no auto-scrolling
return self;
});
var Newspaper = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('newspaper', {
anchorX: 0.5,
anchorY: 0.5
});
// Physics properties
self.vx = 0;
self.vy = 0;
self.gravity = 0.4;
self.active = true;
// Store starting position for scaling math
self.startY = null;
self.update = function () {
if (!self.active) {
return;
}
// Apply velocity
self.x += self.vx;
self.y += self.vy;
// Apply gravity
self.vy += self.gravity;
// Record starting Y on first update
if (self.startY === null) {
self.startY = self.y;
}
// Scale down as it rises (depth effect)
var travel = self.startY - self.y;
var scaleFactor = Math.max(0.2, 1 - travel / 1500);
graphics.scaleX = graphics.scaleY = scaleFactor;
// Cleanup when it goes "into the distance"
if (self.y < mailboxZoneY - 300 || self.y > 2732 || self.x < -200 || self.x > 2300) {
self.active = false;
// Trigger turn end if this was the last shot
if (currentHouse && !isHouseTransitioning) {
removeHouse(currentHouse, function () {
currentHouse = null;
hasThrownThisTurn = false; // reset throw flag
// Clear leftover mailboxes
for (var k = mailboxes.length - 1; k >= 0; k--) {
mailboxes[k].destroy();
mailboxes.splice(k, 1);
}
});
}
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x87ceeb
});
/****
* Game Code
****/
// green
// orange
game.setBackgroundColor(0x87ceeb);
// Start background music immediately when game loads
LK.playMusic('MothmanBoogie');
/****
* Color Interpolation Helper
****/
function lerpColor(c1, c2, t) {
var r1 = c1 >> 16 & 0xff,
g1 = c1 >> 8 & 0xff,
b1 = c1 & 0xff;
var r2 = c2 >> 16 & 0xff,
g2 = c2 >> 8 & 0xff,
b2 = c2 & 0xff;
var r = Math.round(r1 + (r2 - r1) * t);
var g = Math.round(g1 + (g2 - g1) * t);
var b = Math.round(b1 + (b2 - b1) * t);
return r << 16 | g << 8 | b;
}
/****
* Sun Path Parameters
****/
// Sun path parameters
var sunStartX = 100; // left side
var sunEndX = 1948; // right side
var sunBaseY = 600; // baseline (sky zone level)
var sunArcHeight = 400; // how high the sun climbs
/****
* Zone Setup
****/
// Define Y positions for zones (adjust as needed to match your reference image)
var streetZoneY = 2000; // Green bottom zone where slingshot sits
var mailboxZoneY = 1200; // Orange middle zone where mailbox spawns
var houseZoneY = 1000; // Houses aligned just above mailbox zone
// Ground reference (for houses)
var groundY = 2400;
/****
* Visual Zone Placeholders
****/
// Street zone background (green)
var streetZone = game.addChild(LK.getAsset('streetZone', {
anchorX: 0.5,
anchorY: 0.5
}));
streetZone.x = 1024;
streetZone.y = streetZoneY;
// Mailbox zone background (orange)
var mailboxZone = game.addChild(LK.getAsset('mailboxZone', {
anchorX: 0.5,
anchorY: 0.5
}));
mailboxZone.x = 1024;
mailboxZone.y = mailboxZoneY;
// Treeline container to move with houses
var treelineContainer = new Container();
// compute how many tiles we need to cover the screen plus one extra on each side
var tileWidth = LK.getAsset('treeline', {}).width;
var screenWidth = 2048; // your game width
var sideBuffers = 2; // two extra tiles on left side, one on right
var tileCount = Math.ceil(screenWidth / tileWidth) + sideBuffers * 2 + 1; // +1 for additional right side duplicate
var treelines = [];
for (var i = 0; i < tileCount; i++) {
var tile = treelineContainer.addChild(LK.getAsset('treeline', {
anchorX: 0,
anchorY: 1
}));
// position so that the first tile starts at –tileWidth
tile.x = (i - sideBuffers) * tileWidth;
tile.y = 0;
treelines.push(tile);
}
// lock the whole strip at the correct vertical position
treelineContainer.x = 0;
treelineContainer.y = mailboxZoneY - 350;
// Add treeline container before other game elements to render behind them
game.addChild(treelineContainer);
// Parallax scrolling parameters
var baseSpeed = 5; // Base movement speed for reference
var treeParallaxFactor = 0.2; // Trees move at 20% of house speed for depth effect
// Update function for treeline infinite scroll
function updateTreeline(speed) {
for (var i = 0; i < treelines.length; i++) {
var t = treelines[i];
t.x -= speed;
// Snap positions to integers to prevent floating-point drift
t.x = Math.round(t.x);
}
// After moving, check for wrap and re-align all treelines to be flush
// Find the leftmost treeline
var leftMostIndex = 0;
var leftMostX = treelines[0].x;
for (var i = 1; i < treelines.length; i++) {
if (treelines[i].x < leftMostX) {
leftMostX = treelines[i].x;
leftMostIndex = i;
}
}
// Now, starting from leftmost, set each treeline's x so they are flush
var startX = treelines[leftMostIndex].x;
for (var i = 0; i < treelines.length; i++) {
var idx = (leftMostIndex + i) % treelines.length;
if (i === 0) {
treelines[idx].x = startX;
} else {
treelines[idx].x = treelines[(idx - 1 + treelines.length) % treelines.length].x + tileWidth;
}
}
// If any treeline is now off the left edge, move it to the right of the last one
for (var i = 0; i < treelines.length; i++) {
var t = treelines[i];
if (t.x + tileWidth < 0) {
// Find the rightmost treeline
var rightMost = Math.max.apply(Math, treelines.map(function (tr) {
return tr.x;
}));
t.x = rightMost + tileWidth - 1; // subtract 1px to overlap
}
}
}
// Game variables
var mothman;
var sun;
var slingshot;
var newspapers = [];
var mailboxes = [];
var houses = [];
var trajectoryDots = [];
// Create one reusable graphics line for the trajectory
var trajectoryLine = new Container();
// Pull-back origin indicator will be created after slingshot for proper layering
var pullOriginIndicator;
var pennies = 0;
var targetPennies = 650;
var gameTime = 0;
var maxGameTime = 18000; // 5 minutes at 60fps
var isAiming = false;
var aimStartX = 0;
var aimStartY = 0;
var aimPower = 0;
var aimAngle = 0;
var hasThrownThisTurn = false;
// Create UI
var paycheckText = new Text2('Paycheck: $0.00', {
size: 65,
fill: 0x006400,
font: "'Comic Sans MS', cursive"
});
paycheckText.anchor.set(0.5, 0);
paycheckText.y = 100;
LK.gui.top.addChild(paycheckText);
function updatePaycheck() {
paycheckText.setText('Paycheck: $' + (pennies / 100).toFixed(2));
}
// Power bar background + fill
var powerBarBG = LK.getAsset('powerBarBGShape', {
anchorX: 0.5,
anchorY: 0.5
});
powerBarBG.x = 1024;
powerBarBG.y = 200; // top center instead of bottom
LK.gui.top.addChild(powerBarBG);
var powerBarFill = LK.getAsset('powerBarFillShape', {
width: 0,
// start empty
anchorX: 0,
anchorY: 0.5
});
powerBarFill.x = powerBarBG.x - 150;
powerBarFill.y = powerBarBG.y;
LK.gui.top.addChild(powerBarFill);
// Create mothman
mothman = game.addChild(new Container());
var mothmanGraphics = mothman.attachAsset('mothman', {
anchorX: 0.5,
anchorY: 0.5
});
mothman.x = 200;
mothman.y = 400;
// Hide mothman (no sprite during gameplay)
mothman.visible = false;
// Create pull origin indicator after mothman to ensure proper layering in front
pullOriginIndicator = LK.getAsset('aimOriginShape', {
anchorX: 0.5,
anchorY: 0.5
});
// Position the aim origin indicator at slingshot position, moved up by 100 pixels
pullOriginIndicator.x = 1024;
pullOriginIndicator.y = 2000; // streetZoneY (2000) + 100 - 100 = 2000
pullOriginIndicator.visible = true; // Always visible
// Add aiming visuals after mothman to ensure they render on top
game.addChild(trajectoryLine);
game.addChild(pullOriginIndicator);
// Create sun
sun = game.addChild(new Container());
var sunGraphics = sun.attachAsset('sun', {
anchorX: 0.5,
anchorY: 0.5
});
sun.x = sunStartX;
sun.y = sunBaseY - sunArcHeight;
/****
* Draw Sun Arc Path
****/
// Draw sun trajectory arc with dotted path
function drawSunPath() {
for (var t = 0; t <= 1; t += 0.05) {
var x = sunStartX + (sunEndX - sunStartX) * t;
var y = sunBaseY - Math.sin(t * Math.PI) * sunArcHeight;
var dot = game.addChild(LK.getAsset('trajectory', {
width: 6,
height: 6,
color: 0xffff66,
// pale yellow
anchorX: 0.5,
anchorY: 0.5
}));
dot.x = x;
dot.y = y;
dot.alpha = 0.3; // semi-transparent
}
}
drawSunPath();
// Create slingshot area - positioned in street zone center
slingshot = game.addChild(new Container());
var slingshotGraphics = slingshot.attachAsset('slingshot', {
anchorX: 0.5,
anchorY: 0.5
});
slingshot.x = 1024; // center of screen
slingshot.y = streetZoneY + 100;
// Slingshot bands (for stretch feedback)
var bandLeft = new Container();
var bandRight = new Container();
game.addChild(bandLeft);
game.addChild(bandRight);
// Ground level
var groundY = 2400;
// Turn-based house cycle variables
var currentHouse = null;
var isHouseTransitioning = false;
var turnComplete = true;
var prevHouseX = 1024; // Track previous house X position for treeline scroll
function ensureMailboxForTurn() {
// Clear leftover mailboxes
for (var k = mailboxes.length - 1; k >= 0; k--) {
mailboxes[k].destroy();
mailboxes.splice(k, 1);
}
createMailbox(); // always create (no RNG)
}
function createHouse() {
var house = new House();
// Start just off-screen to the right
house.x = 2200;
house.y = mailboxZoneY + 250; // roughly center mailbox zone
houses.push(house);
game.addChild(house);
currentHouse = house;
isHouseTransitioning = true;
turnComplete = false;
hasThrownThisTurn = false;
// Tween it into center of mailbox zone
tween(house, {
x: 1024
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
isHouseTransitioning = false;
ensureMailboxForTurn(); // mailbox priority
}
});
return house;
}
function removeHouse(house, onComplete) {
if (!house) {
return;
}
isHouseTransitioning = true;
tween(house, {
x: -200
}, {
duration: 1000,
easing: tween.easeIn,
onFinish: function onFinish() {
house.destroy();
var index = houses.indexOf(house);
if (index !== -1) {
houses.splice(index, 1);
}
isHouseTransitioning = false;
turnComplete = true;
if (onComplete) {
onComplete();
}
}
});
}
function createMailbox() {
if (!currentHouse) {
return;
}
var mailbox = new Mailbox();
// Calculate house position and width to avoid overlap
var houseX = currentHouse.x;
var houseWidth = 900; // house asset width
var houseLeftEdge = houseX - houseWidth * 0.5;
var houseRightEdge = houseX + houseWidth * 0.5;
// Generate random position that doesn't overlap with house
var mailboxX;
var attempts = 0;
do {
mailboxX = 400 + Math.random() * 1200; // stays in safe horizontal range
attempts++;
// Safety check to prevent infinite loop
if (attempts > 50) {
// If we can't find a spot, place it far from house
if (houseX > 1024) {
mailboxX = 500; // place on left side
} else {
mailboxX = 1500; // place on right side
}
break;
}
} while (mailboxX >= houseLeftEdge - 100 && mailboxX <= houseRightEdge + 100);
mailbox.x = mailboxX;
mailbox.y = mailboxZoneY + (Math.random() * 200 - 100);
// Random horizontal flip
if (Math.random() < 0.5) {
mailbox.scaleX = -1;
} else {
mailbox.scaleX = 1;
}
mailboxes.push(mailbox);
game.addChild(mailbox);
// Ensure mailbox is drawn in front of house
game.setChildIndex(mailbox, game.getChildIndex(currentHouse) + 1);
return mailbox;
}
function shootNewspaper(power, angle) {
var newspaper = new Newspaper();
newspaper.x = 1024; // slingshot center
newspaper.y = 2200; // bottom
// Use power + angle for velocity
newspaper.vx = Math.cos(angle) * power;
newspaper.vy = Math.sin(angle) * power;
newspapers.push(newspaper);
game.addChild(newspaper);
LK.getSound('Paperthrow').play();
}
function updateSlingshotBands(endX, endY) {
bandLeft.removeChildren();
bandRight.removeChildren();
// Don't draw visible bands - keep containers for positioning only
}
function calculateScore(distance) {
if (distance < 15) {
return 5;
} // Perfect hit
if (distance < 25) {
return 4;
} // Great hit
if (distance < 35) {
return 3;
} // Good hit
if (distance < 45) {
return 2;
} // OK hit
return 1; // Glancing hit
}
function payoutFor(distancePx) {
if (distancePx <= 25) return {
cents: 25,
label: '+25¢ Perfect!'
};
if (distancePx <= 60) return {
cents: 15,
label: '+15¢ Very close'
};
if (distancePx <= 120) return {
cents: 5,
label: '+5¢ Close'
};
return null;
}
function updateTrajectory(originX, originY, pullX, pullY) {
// Clear any previous drawing
trajectoryLine.removeChildren();
if (!isAiming) return;
// Launch vector = opposite of pull
var dx = originX - pullX;
var dy = originY - pullY;
var power = Math.sqrt(dx * dx + dy * dy) * 0.1;
var angle = Math.atan2(dy, dx);
// Launch velocity
var vX = Math.cos(angle) * power;
var vY = Math.sin(angle) * power;
// Choose a projection distance (how long the line should be)
var projectionTime = 80; // tweak for how far line extends
var g = 0.3;
// Predict end point of the line
var endX = originX + vX * projectionTime;
var endY = originY + vY * projectionTime - 0.5 * g * projectionTime * projectionTime;
// Create line graphic
var lineGraphic = trajectoryLine.addChild(LK.getAsset('trajectory', {
width: Math.sqrt((endX - originX) * (endX - originX) + (endY - originY) * (endY - originY)),
height: 4,
color: 0x00ff00,
anchorX: 0,
anchorY: 0.5
}));
lineGraphic.x = originX;
lineGraphic.y = originY;
lineGraphic.rotation = Math.atan2(endY - originY, endX - originX);
lineGraphic.alpha = 0.7;
}
// Game input handlers - natural slingshot controls
game.down = function (x, y, obj) {
// Only allow aiming if player clicks/touches near the slingshot
if (y > streetZoneY - 200) {
isAiming = true;
aimStartX = x;
aimStartY = y;
// Pull origin indicator is already positioned and visible
// No need to reposition it during aiming
}
};
game.move = function (x, y, obj) {
if (isAiming) {
// Draw a preview trajectory
updateTrajectory(1024, 2000, x, y);
// Compute pull distance for power
var dx = 1024 - x;
var dy = 2200 - y;
var pullDistance = Math.sqrt(dx * dx + dy * dy);
var maxPull = 600; // clamp so bar doesn't overflow
var percent = Math.min(1, pullDistance / maxPull);
// Dynamic color based on power level
var color = 0x00ff00; // green
if (percent > 0.66) {
color = 0xff0000; // red
} else if (percent > 0.33) {
color = 0xffff00; // yellow
}
// Re-create the fill shape with new color and width
powerBarFill.destroy();
powerBarFill = LK.getAsset('powerBarFillShape', {
width: percent * 300,
color: color,
anchorX: 0,
anchorY: 0.5
});
powerBarFill.x = powerBarBG.x - 150;
powerBarFill.y = powerBarBG.y;
LK.gui.top.addChild(powerBarFill);
}
};
game.up = function (x, y, obj) {
if (!isAiming || hasThrownThisTurn) return;
var originX = 1024,
originY = 2000; // slingshot base moved up by 100 pixels
var pullX = x - originX;
var pullY = y - originY;
// Launch vector = opposite of pull
var launchX = -pullX;
var launchY = -pullY;
// Power is proportional to pull distance
var pullDistance = Math.sqrt(launchX * launchX + launchY * launchY);
var powerScale = 0.15; // tweak this number to control speed
var power = pullDistance * powerScale;
// Angle from pull-back vector
var angle = Math.atan2(launchY, launchX);
if (power > 2) {
shootNewspaper(power, angle);
hasThrownThisTurn = true;
}
// Reset aiming visuals
isAiming = false;
trajectoryLine.removeChildren();
// Pull origin indicator stays visible
powerBarFill.width = 0;
};
game.update = function () {
gameTime++;
// Update sun position (moves across sky as timer)
var timeProgress = gameTime / maxGameTime;
sun.x = sunStartX + (sunEndX - sunStartX) * timeProgress;
sun.y = sunBaseY - Math.sin(timeProgress * Math.PI) * sunArcHeight;
// Sky color progression with smooth transitions
var skyColors = [{
t: 0.0,
color: 0xE6E6FA
},
// Lavender
{
t: 0.2,
color: 0xADD8E6
},
// Light Blue
{
t: 0.4,
color: 0x87CEEB
},
// Bright Blue
{
t: 0.7,
color: 0xFFA500
},
// Orange
{
t: 1.0,
color: 0x4B0082
} // Deep Purple
];
// Find the two colors to interpolate between
for (var i = 0; i < skyColors.length - 1; i++) {
var c1 = skyColors[i];
var c2 = skyColors[i + 1];
if (timeProgress >= c1.t && timeProgress <= c2.t) {
var localT = (timeProgress - c1.t) / (c2.t - c1.t);
var blended = lerpColor(c1.color, c2.color, localT);
game.setBackgroundColor(blended);
break;
}
}
// Turn-based house cycle management
if (turnComplete && !isHouseTransitioning && !currentHouse) {
createHouse(); // mailbox is guaranteed in onFinish
}
// Watchdog: if for any reason a mailbox isn't present, create one.
if (currentHouse && !isHouseTransitioning && mailboxes.length === 0) {
ensureMailboxForTurn();
}
// Update newspapers
for (var i = newspapers.length - 1; i >= 0; i--) {
var newspaper = newspapers[i];
if (!newspaper.active) {
newspaper.destroy();
newspapers.splice(i, 1);
continue;
}
// Check if newspaper went off screen without hitting (miss)
if (newspaper.active && (newspaper.x > 2048 || newspaper.y > 2732)) {
newspaper.active = false;
// End the turn if no hit occurred
if (currentHouse && !isHouseTransitioning) {
removeHouse(currentHouse, function () {
currentHouse = null;
hasThrownThisTurn = false; // reset throw flag
// Clear mailboxes for this turn
for (var k = mailboxes.length - 1; k >= 0; k--) {
mailboxes[k].destroy();
mailboxes.splice(k, 1);
}
});
}
continue;
}
// Check mailbox collisions
for (var j = 0; j < mailboxes.length; j++) {
var m = mailboxes[j];
if (m.hit) continue;
// enlarge hitbox with tolerance for better hit detection
var gfx = m.children && m.children[0] ? m.children[0] : m;
var halfW = (gfx.width || 100) * 0.5;
var fullH = gfx.height || 100;
var left = m.x - halfW;
var right = m.x + halfW;
var top = m.y - fullH;
var bottom = m.y;
// loosen bounds by 20px in all directions
var tol = 20;
var insideX = newspaper.x >= left - tol && newspaper.x <= right + tol;
var insideY = newspaper.y >= top - tol && newspaper.y <= bottom + tol;
if (!insideX || !insideY) continue;
// compute accuracy off the mailbox "mouth"
var mouthY = m.y - fullH * 0.6;
var dx = newspaper.x - m.x;
var dy = newspaper.y - mouthY;
var dist = Math.sqrt(dx * dx + dy * dy);
// if payoutFor returns null (too far), still give a glancing hit
var res = payoutFor(dist) || {
cents: 5,
label: '+5¢ Close enough'
};
pennies += res.cents;
updatePaycheck();
m.hit = true;
newspaper.active = false;
// floating green payout text that fades away (large and bold)
var _float = new Text2(res.label, {
size: 72,
fill: 0x00aa00,
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
_float.anchor.set(0.5, 1);
_float.x = m.x;
_float.y = top - 10;
game.addChild(_float);
tween(_float, {
y: _float.y - 80,
alpha: 0
}, {
duration: 800,
onFinish: function onFinish() {
_float.destroy();
}
});
LK.effects.flashObject(m, 0x00ff00, 500);
LK.getSound('MailboxHit').play();
// end turn after scoring (keep existing cleanup behavior)
if (currentHouse && !isHouseTransitioning) {
removeHouse(currentHouse, function () {
currentHouse = null;
hasThrownThisTurn = false; // reset throw flag
for (var k = mailboxes.length - 1; k >= 0; k--) {
mailboxes[k].destroy();
mailboxes.splice(k, 1);
}
});
}
break;
}
}
// Update and clean up mailboxes
for (var i = mailboxes.length - 1; i >= 0; i--) {
var mailbox = mailboxes[i];
if (!mailbox.active) {
mailbox.destroy();
mailboxes.splice(i, 1);
}
}
// Update and clean up houses
for (var i = houses.length - 1; i >= 0; i--) {
var house = houses[i];
if (!house.active) {
house.destroy();
houses.splice(i, 1);
}
}
// Check win condition
if (pennies >= targetPennies) {
LK.showYouWin();
}
// Infinite-scroll treeline every frame with parallax effect
if (currentHouse) {
var houseSpeed = prevHouseX - currentHouse.x;
// Apply parallax factor to create depth - trees move slower than houses
var parallaxSpeed = houseSpeed * treeParallaxFactor;
updateTreeline(parallaxSpeed);
prevHouseX = currentHouse.x;
}
// Ensure treeline is always behind mailbox zone
game.setChildIndex(treelineContainer, game.getChildIndex(mailboxZone) - 1);
// Force aiming visuals to always render on top
game.setChildIndex(trajectoryLine, game.children.length - 1);
game.setChildIndex(pullOriginIndicator, game.children.length - 2);
// Check lose condition
if (gameTime >= maxGameTime) {
LK.showGameOver();
}
};