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