Code edit (1 edits merged)
Please save this source code
User prompt
1. Newspaper should fly in an arch, not a straight line Right now the Newspaper.update() just adds velocity + gravity. That makes it fall but doesn’t give a true arc. We can make it look more natural by: Applying initial velocity from slingshot. Letting gravity curve the path downward. Adding perspective scaling (we already did). Your Newspaper.update already has gravity (self.velocityY += self.gravity). That gives a parabola, but because of the launch angle calculation, it might look flat. 👉 We’ll tweak launch so it always has a negative vertical velocity (so it starts by going “up” and then curves down): function shootNewspaper(power, angle) { var newspaper = new Newspaper(); newspaper.x = 1024; // launch origin newspaper.y = 2200; var radians = angle * Math.PI / 180; newspaper.velocityX = Math.cos(radians) * power; newspaper.velocityY = Math.sin(radians) * power * -1; // invert to go up first newspapers.push(newspaper); game.addChild(newspaper); LK.getSound('paperThrow').play(); } ✅ Now every throw starts by flying upward and then arcs down due to gravity, instead of straight flat throws. 2. Sun arc should stay in the “sky zone” Right now the sun baseline is too low (sunBaseY = 1400), meaning it overlaps the mailbox zone. Let’s move it higher, so it travels above. Mailbox zone ends ~ at y = 1200. We want the sun baseline higher, e.g. ~ y = 600. We also adjust sunArcHeight so it doesn’t clip offscreen.Update in game.update: sun.x = sunStartX + (sunEndX - sunStartX) * timeProgress; sun.y = sunBaseY - Math.sin(timeProgress * Math.PI) * sunArcHeight; 3. Mailboxes layered correctly with house Currently you game.addChild(mailbox) after house, so depending on spawn order, mailboxes may appear behind or on top. We want: Houses in the back. Mailboxes always in front of the house, but still behind the newspaper. 👉 After creating both, explicitly set the mailbox’s index: function createMailbox() { if (!currentHouse) return; var mailbox = new Mailbox(); mailbox.x = 400 + Math.random() * 1200; mailbox.y = mailboxZoneY + (Math.random() * 200 - 100); // Flip horizontally mailbox.scaleX = (Math.random() < 0.5) ? -1 : 1; mailboxes.push(mailbox); game.addChild(mailbox); // Ensure mailbox is drawn in front of house game.setChildIndex(mailbox, game.getChildIndex(currentHouse) + 1); return mailbox; } } ✅ Mailbox always visually sits “in the yard” in front of the house, never hidden or floating above everything. 📌 Summary of fixes Newspaper now always arcs (launch upward, gravity pulls down). Sun travels across the sky zone, never clipping mailbox area or screen edge. Mailboxes always layered in front of the house, never hidden behind or incorrectly layered. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
// Draw sun trajectory arc var sunPath = new Container(); var pathGraphics = sunPath.attachAsset('trajectory', { width: 4, height: 4, color: 0xffff00, anchorX: 0.5, anchorY: 0.5 }); game.addChild(sunPath); 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(); 3. Replace the old sun movement code in game.update sun.x = sunStartX + (sunEndX - sunStartX) * timeProgress; sun.y = sunBaseY - Math.sin(timeProgress * Math.PI) * sunArcHeight; ✅ With this edit: The sun now follows a realistic parabolic arc. A faint dotted golden trajectory line is visible from sunrise → sunset. Players see how much time is left by watching the sun move across its track. 1. Mailbox random position + flip Right now, createMailbox() positions the mailbox close to the house with only a little randomness. Let’s widen that randomization so it can appear anywhere in the mailbox zone, and add a 50% chance to flip horizontally. function createMailbox() { if (!currentHouse) return; var mailbox = new Mailbox(); // Place randomly within mailbox zone bounds mailbox.x = 400 + Math.random() * 1200; // stays in safe horizontal range 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); return mailbox; } 2. Newspaper shrinking as it travels The newspaper is currently a static size. Let’s scale it based on how far it has traveled from its starting Y position — smaller as it rises, then stabilizing. Inside the Newspaper.update function, add: self.startY = self.startY || self.y; // record starting Y once // scale shrinks as it moves upward var travel = self.startY - self.y; // how high it has gone var scaleFactor = Math.max(0.3, 1 - travel / 2000); graphics.scaleX = graphics.scaleY = scaleFactor; ✅ This gives the illusion of depth: the farther “away” the newspaper goes, the smaller it looks. Minimum 30% so it doesn’t disappear. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
1. Define the sun’s arc We’ll treat the sun’s movement as a parametric equation over timeProgress (0 → 1): X moves left → right across the screen. Y follows a smooth arch (using a sine curve). // Sun path parameters var sunStartX = 100; // left side var sunEndX = 1948; // right side var sunBaseY = 1400; // baseline (ground level-ish) var sunArcHeight = 600; // how high the sun climbs ✅ This makes the sun rise in the east, reach peak at noon, and set in the west. 2. Draw the arc line once at game start We’ll create a path with small points and render it as a curve for the sun to “ride.” ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
👉 The simplest fix is to flip deltaX before computing the angle: // In game.up var deltaX = aimStartX - x; // flip X so left/right feels natural var deltaY = aimStartY - y; // keep inverted Y pull aimPower = Math.sqrt(deltaX * deltaX + deltaY * deltaY) * 0.1; aimAngle = Math.atan2(deltaY, deltaX) * 180 / Math.PI; And the same adjustment in updateTrajectory: var deltaX = startX - endX; // flip X for preview var deltaY = startY - endY; // inverted Y var power = Math.sqrt(deltaX * deltaX + deltaY * deltaY) * 0.1; var angle = Math.atan2(deltaY, deltaX); 2. End turn if the player misses Right now turns only end on a timer if no newspapers are active. We want: one throw per turn → if it misses, turn ends. // Inside game.update, after checking mailbox collisions if (newspaper.active) { // Newspaper went off screen without hitting if (newspaper.x > 2048 || newspaper.y > 2732) { newspaper.active = false; // End the turn if no hit occurred removeHouse(currentHouse, function () { currentHouse = null; // Clear mailboxes for this turn for (var i = mailboxes.length - 1; i >= 0; i--) { mailboxes[i].destroy(); mailboxes.splice(i, 1); } }); } } And after a successful hit, we also end the turn: if (!mailbox.hit && newspaper.intersects(mailbox)) { var distance = Math.sqrt(Math.pow(newspaper.x - mailbox.x, 2) + Math.pow(newspaper.y - mailbox.y, 2)); var score = calculateScore(distance); pennies += score; pennyText.setText('Pennies: ' + pennies); mailbox.hit = true; newspaper.active = false; // Flash + sound LK.effects.flashObject(mailbox, 0x00ff00, 500); LK.getSound('mailboxHit').play(); // End the turn after scoring removeHouse(currentHouse, function () { currentHouse = null; for (var i = mailboxes.length - 1; i >= 0; i--) { mailboxes[i].destroy(); mailboxes.splice(i, 1); } }); break; } 3. Loop summary with your rules Each turn starts with a house sliding in and a new mailbox placed. Player pulls down to aim, directions feel natural. They get one throw per turn. If they hit, they get 1–5 pennies and the turn ends. If they miss, they get 0 and the turn ends. Game continues until: 650 pennies → win Sun finishes arc → lose ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
✅ Fix → remove the auto‐scroll from House.update and instead tween houses in/out as discussed. 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; }); ensure house graphic is spawning in front of the mailbox zone ✅ Fix → remove the scrollSpeed from mailbox update, and pin them relative to the house when created: 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; }); function createMailbox(house) { var mailbox = new Mailbox(); mailbox.x = house.x + 200; // offset to right of house mailbox.y = mailboxZoneY + 250; // centered in zone mailboxes.push(mailbox); game.addChild(mailbox); return mailbox; } Invert slingshot controls var deltaX = x - aimStartX; var deltaY = aimStartY - y; // invert Y pull aimPower = Math.sqrt(deltaX * deltaX + deltaY * deltaY) * 0.1; aimAngle = Math.atan2(deltaY, deltaX) * 180 / Math.PI; And in updateTrajectory: var deltaX = endX - startX; var deltaY = startY - endY; // invert Y var power = Math.sqrt(deltaX * deltaX + deltaY * deltaY) * 0.1; var angle = Math.atan2(deltaY, deltaX); ✅ With these edits: Houses will spawn and stay centered in the mailbox zone. Mailboxes will be attached near the house, not flying away. Slingshot now feels like a real slingshot (pull down to launch up). ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
1. Define new zone shapes in the Assets section: LK.init.shape('streetZone', {width: 2048, height: 700, color: 0x228B22, shape:'box'}); // green LK.init.shape('mailboxZone', {width: 2048, height: 500, color: 0xFF8C00, shape:'box'}); // orange 2. Update your zone creation code: // Street zone background var streetZone = game.addChild(LK.getAsset('streetZone', { anchorX: 0.5, anchorY: 0.5 })); streetZone.x = 1024; streetZone.y = streetZoneY; // Mailbox zone background var mailboxZone = game.addChild(LK.getAsset('mailboxZone', { anchorX: 0.5, anchorY: 0.5 })); mailboxZone.x = 1024; mailboxZone.y = mailboxZoneY; This will: Give the street zone its own proper green rectangle. Give the mailbox zone its own proper orange rectangle. Free up the trajectory asset so it only shows up as actual trajectory dots instead of giant background blocks. center sling shot at bottom half of screen in "street zone" sounds more like a turn-based cycle: At the start of a turn → one house slides in from the right edge into the middle of the mailbox zone. During the turn → the house stays centered. Mailbox spawns relative to it (still randomized vertically if you want). At the end of the turn → the house slides out to the left, disappears, and a new house comes in from the right. That means we’ll change createHouse() and add a house cycle manager instead of spawning houses randomly. Step 1 – Update createHouse 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); // Tween it into center of mailbox zone tween.to(house, { x: 1024 }, 1000, "easeOutQuad"); return house; } Step 2 – Add removeHouse for end-of-turn function removeHouse(house, onComplete) { tween.to(house, { x: -200 }, 1000, "easeInQuad", function () { house.destroy(); houses.splice(houses.indexOf(house), 1); if (onComplete) onComplete(); }); } Step 3 – Manage house cycle per turn // End of turn logic if (houses.length > 0) { var currentHouse = houses[0]; removeHouse(currentHouse, function () { // After old house slides out, create a new one createHouse(); }); } else { // No house yet, just spawn one createHouse(); } Step 4 – Remove house spawning from update loop ✅ With these edits: At start of game → one house slides in and centers in mailbox zone. At end of each turn → it slides left off-screen, then the next house slides in from the right. You control exactly one house at a time (no clutter, no overlapping). ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
📰 Newspaper placement & visibility Start newspaper at bottom-center of screen (instead of mothman’s position). Give enough room below for pull-back. Make sure it always attaches its graphics so it’s visible during flight. 👻 Hide Mothman Don’t render the mothman during gameplay. Keep logic intact if needed, but remove/hide his ellipse. 🏠 House duplication fix The extra “street zone” green rectangle was reusing the house shape as a placeholder background — we’ll replace that with a simple colored rectangle so it doesn’t double as a house. Limit houses to spawn only once per turn. /**** * Zones (fixed) ****/ // Street zone (green bar at bottom, no house duplication) var streetZone = new Graphics(); streetZone.beginFill(0x228B22); // green streetZone.drawRect(0, 0, 2048, 700); streetZone.endFill(); streetZone.x = 0; streetZone.y = 1800; game.addChild(streetZone); // Mailbox zone (orange band in middle) var mailboxZone = new Graphics(); mailboxZone.beginFill(0xFF8C00); // orange mailboxZone.drawRect(0, 0, 2048, 500); mailboxZone.endFill(); mailboxZone.x = 0; mailboxZone.y = 1200; game.addChild(mailboxZone); /**** * House & Mailbox Placement ****/ function createHouse() { var house = new House(); house.x = 2100; house.y = 1000; // fixed: only one row of houses houses.push(house); game.addChild(house); } function createMailbox() { var mailbox = new Mailbox(); mailbox.x = 2100; mailbox.y = 1200 + (Math.random() * 200 - 100); // stays in orange band mailboxes.push(mailbox); game.addChild(mailbox); } /**** * Slingshot + Newspaper ****/ // Hide mothman (no sprite during gameplay) mothman.visible = false; // Newspaper starts at bottom center function shootNewspaper(power, angle) { var newspaper = new Newspaper(); newspaper.x = 1024; // center of screen newspaper.y = 2200; // bottom, with room to pull back var radians = angle * Math.PI / 180; newspaper.velocityX = Math.cos(radians) * power; newspaper.velocityY = Math.sin(radians) * power; newspapers.push(newspaper); game.addChild(newspaper); // ensure added after zones so visible LK.getSound('paperThrow').play(); } ✅ Fix Summary Newspaper now launches from bottom center and is always visible. Mothman hidden. Street zone no longer reuses house sprite → prevents duplicate houses. House only spawns once per turn in the correct zone.
User prompt
/**** * 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('house', { width: 2048, height: 700, color: 0x228B22, // green anchorX: 0.5, anchorY: 0.5 })); streetZone.x = 1024; streetZone.y = streetZoneY; // Mailbox zone background (orange) var mailboxZone = game.addChild(LK.getAsset('house', { width: 2048, height: 500, color: 0xFF8C00, // orange anchorX: 0.5, anchorY: 0.5 })); mailboxZone.x = 1024; mailboxZone.y = mailboxZoneY; /**** * House and Mailbox Placement ****/ function createHouse() { var house = new House(); house.x = 2100; house.y = houseZoneY + 200; // align house to top of mailbox zone houses.push(house); game.addChild(house); } function createMailbox() { var mailbox = new Mailbox(); mailbox.x = 2100; // Randomize Y position only within mailbox zone mailbox.y = mailboxZoneY + (Math.random() * 200 - 100); mailboxes.push(mailbox); game.addChild(mailbox); } /**** * Slingshot + Aiming ****/ // Place slingshot in street zone slingshot = game.addChild(new Container()); var slingshotGraphics = slingshot.attachAsset('slingshot', { anchorX: 0.5, anchorY: 0.5 }); slingshot.x = 200; slingshot.y = streetZoneY + 100; // Slingshot bands (for stretch feedback) var bandLeft = new Graphics(); var bandRight = new Graphics(); game.addChild(bandLeft); game.addChild(bandRight); function updateSlingshotBands(endX, endY) { bandLeft.clear(); bandRight.clear(); bandLeft.lineStyle(6, 0x654321); bandRight.lineStyle(6, 0x654321); bandLeft.moveTo(slingshot.x - 20, slingshot.y - 20); bandLeft.lineTo(endX, endY); bandRight.moveTo(slingshot.x + 20, slingshot.y - 20); bandRight.lineTo(endX, endY); } // Shooting logic game.down = function (x, y, obj) { if (y > streetZoneY - 200) { // only allow aiming in street zone isAiming = true; aimStartX = x; aimStartY = y; } }; game.move = function (x, y, obj) { if (isAiming) { updateTrajectory(mothman.x, mothman.y, x, y); updateSlingshotBands(x, y); } }; game.up = function (x, y, obj) { if (isAiming) { var deltaX = x - aimStartX; var deltaY = y - aimStartY; aimPower = Math.sqrt(deltaX * deltaX + deltaY * deltaY) * 0.1; aimAngle = Math.atan2(deltaY, deltaX) * 180 / Math.PI; if (aimPower > 2) { shootNewspaper(aimPower, aimAngle); } isAiming = false; // Clear trajectory + bands for (var i = 0; i < trajectoryDots.length; i++) { trajectoryDots[i].destroy(); } trajectoryDots = []; bandLeft.clear(); bandRight.clear(); } }; ✅ What this update does: Creates green street zone and orange mailbox zone like your image. Houses sit above the mailbox zone, and mailboxes spawn randomized within the orange zone. Slingshot is fixed in the street zone. Player can click + drag to stretch (shows rubber bands) and see a trajectory preview, then release to fire.
Code edit (1 edits merged)
Please save this source code
User prompt
Mothman's Pumpkin Spice Paper Route
Initial prompt
Game Concept: A 2D side-scrolling aiming game where the player controls Mothman delivering newspapers. The goal is to earn 650 pennies ($6.50) to buy a Pumpkin Spice Latte. Core Gameplay Loop (Turn Structure): Scene Setup: The game background scrolls from right to left, simulating Mothman moving down a suburban street. A new house appears each turn. A mailbox is randomly placed somewhere on the lawn area in front of the house. Player Action: The player uses a slingshot mechanic located at the bottom half of the screen. The player pulls back to set the power and angle of the newspaper throw, then releases to launch it. Result: If the newspaper hits the mailbox, the player earns 1–5 pennies depending on accuracy (5 pennies for a perfect shot, 1 penny for barely hitting). If the player misses, no pennies are earned, and the turn ends. Progression: The game immediately scrolls to the next house for another turn. This continues until either: The player collects 650 pennies (win condition). Or the time runs out (lose condition). Win / Lose Conditions: Win: Player collects 650 pennies before time runs out. Lose: The in-game day ends (time limit reached) without earning 650 pennies. Time System: The time limit is one full day. At the start of the level, the sun rises on the left side of the screen. As turns progress, the sun moves in an arc across the sky. When the sun sets on the right side of the screen, the day ends and the game is over. Additional Notes: Perspective: Player’s view is from street level, looking toward houses as they pass by. Currency Representation: Each penny is counted individually; total displayed on the screen. Difficulty Scaling (optional): Mailbox positions can become smaller or more randomized as time goes on.
/**** * 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();
}
};