Mercurial
comparison mrjunejune/src/public/dog-game.js @ 212:84826b3c655b
[MrJuneJune] Forgot to add assets.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sun, 15 Feb 2026 21:38:36 -0800 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 211:a6d8d32a0261 | 212:84826b3c655b |
|---|---|
| 1 // Dog frames | |
| 2 const imagesSrc = [ | |
| 3 "/public/sprite_shiba0.png", | |
| 4 "/public/sprite_shiba1.png", | |
| 5 "/public/sprite_shiba2.png", | |
| 6 "/public/sprite_shiba3.png", | |
| 7 ]; | |
| 8 const images =imagesSrc.map((src, index) => { | |
| 9 const img = new Image(); | |
| 10 img.src = src; | |
| 11 img.onload = () => { | |
| 12 if (index == imagesSrc.length-1) | |
| 13 startAnimation(); | |
| 14 }; | |
| 15 return img | |
| 16 }); | |
| 17 | |
| 18 // Load treat image | |
| 19 const treatImage = new Image(); | |
| 20 treatImage.src = "/public/dog-treat.png"; | |
| 21 | |
| 22 // Load star images for background | |
| 23 let starImages = [ | |
| 24 { src: "/public/start-large.png", img: new Image() }, | |
| 25 { src: "/public/start-small-1.png", img: new Image() }, | |
| 26 { src: "/public/start-small-2.png", img: new Image() }, | |
| 27 { src: "/public/start-small-3.png", img: new Image() } | |
| 28 ]; | |
| 29 starImages.forEach(star => star.img.src = star.src); | |
| 30 | |
| 31 const background = document.getElementById('background'); | |
| 32 const bgCtx = background.getContext("2d"); | |
| 33 const ctx = game.getContext("2d"); | |
| 34 const uiCtx = gameUI.getContext("2d"); | |
| 35 | |
| 36 | |
| 37 // Dog initial position and movement | |
| 38 const initialDogVx = 2; | |
| 39 let x = Math.random() * (game.width); | |
| 40 let y = Math.random() * (game.height); | |
| 41 let vx = initialDogVx; | |
| 42 let vy = 0; | |
| 43 const gravity = 0.5; | |
| 44 const jumpForce = -12; | |
| 45 let isFlipped = false; | |
| 46 | |
| 47 // Random jump interval | |
| 48 // TODO: let's do it by frame? | |
| 49 let nextJump = Math.random() * 2000 + 1000; | |
| 50 | |
| 51 // Treat system | |
| 52 const originalTreatSize = 30; | |
| 53 let treatSize = originalTreatSize; | |
| 54 const treatMargin = 20; | |
| 55 const reachDistance = 50; | |
| 56 let treatRainActive = false; | |
| 57 let treats = []; | |
| 58 let lastTreatSpawn = 0; | |
| 59 const treatFallSpeed = 0.25; | |
| 60 const treatGravity = 0.13; | |
| 61 | |
| 62 // Background stars | |
| 63 let stars = []; | |
| 64 function createStars() { | |
| 65 stars = []; | |
| 66 const starCount = window.innerWidth * 0.05; | |
| 67 | |
| 68 for (let i = 0; i < starCount; i++) { | |
| 69 const starType = starImages[Math.floor(Math.random() * starImages.length)]; | |
| 70 const scale = 0.5 + Math.random() * 0.8; // Random scale | |
| 71 | |
| 72 stars.push({ | |
| 73 x: Math.random() * background.width, | |
| 74 y: Math.random() * background.height, | |
| 75 vx: (Math.random() - 0.5) * 0.5, | |
| 76 vy: (Math.random() - 0.5) * 0.5, | |
| 77 rotation: Math.random() * Math.PI * 2, | |
| 78 rotationSpeed: (Math.random() - 0.5) * 0.02, | |
| 79 image: starType.img, | |
| 80 size: starType.img.width, | |
| 81 scale: scale, | |
| 82 opacity: 0.3 + Math.random() * 0.4 // Random opacity | |
| 83 }); | |
| 84 } | |
| 85 } | |
| 86 | |
| 87 function animateBackground() { | |
| 88 bgCtx.clearRect(0, 0, background.width, background.height); | |
| 89 | |
| 90 stars.forEach(star => { | |
| 91 // Update position | |
| 92 star.x += star.vx; | |
| 93 star.y += star.vy; | |
| 94 star.rotation += star.rotationSpeed; | |
| 95 | |
| 96 // Wrap around edges | |
| 97 if (star.x < -100) star.x = background.width + 100; | |
| 98 if (star.x > background.width + 100) star.x = -100; | |
| 99 if (star.y < -100) star.y = background.height + 100; | |
| 100 if (star.y > background.height + 100) star.y = -100; | |
| 101 | |
| 102 // Draw star | |
| 103 bgCtx.save(); | |
| 104 bgCtx.globalAlpha = star.opacity; | |
| 105 bgCtx.translate(star.x, star.y); | |
| 106 bgCtx.rotate(star.rotation); | |
| 107 | |
| 108 const size = star.size * star.scale; | |
| 109 bgCtx.drawImage(star.image, -size / 2, -size / 2, size, size); | |
| 110 | |
| 111 bgCtx.restore(); | |
| 112 }); | |
| 113 | |
| 114 requestAnimationFrame(animateBackground); | |
| 115 } | |
| 116 | |
| 117 function getMousePosition(event) { | |
| 118 const rect = game.getBoundingClientRect(); | |
| 119 const x = event.clientX - rect.left; | |
| 120 const y = event.clientY - rect.top; | |
| 121 return { x: x, y: y }; | |
| 122 } | |
| 123 | |
| 124 // UI canvas click - toggle treat rain | |
| 125 gameUI.addEventListener('click', (e) => { | |
| 126 treatRainActive = !treatRainActive; | |
| 127 if (treatRainActive) | |
| 128 game.classList.add('active'); | |
| 129 else | |
| 130 game.classList.remove('active'); | |
| 131 }); | |
| 132 | |
| 133 game.addEventListener('click', (e) => { | |
| 134 if (treatRainActive) { | |
| 135 const mousePos = getMousePosition(e); | |
| 136 treats.push({ x: mousePos.x, y: mousePos.y, vy: treatFallSpeed }); | |
| 137 } | |
| 138 }); | |
| 139 | |
| 140 function startAnimation() { | |
| 141 requestAnimationFrame(animate); | |
| 142 } | |
| 143 | |
| 144 | |
| 145 let index = 0; | |
| 146 let last = 0; | |
| 147 const frameDuration = 120; // ms per frame | |
| 148 function animate(ts) { | |
| 149 if (ts - last > frameDuration) { | |
| 150 index = (index + 1) % images.length; | |
| 151 last = ts; | |
| 152 } | |
| 153 | |
| 154 // Get image dimensions | |
| 155 const imgWidth = images[0].width; | |
| 156 const imgHeight = images[0].height; | |
| 157 | |
| 158 // Update falling treats | |
| 159 treats.forEach(treat => { | |
| 160 treat.vy += treatGravity; | |
| 161 treat.y += treat.vy; | |
| 162 }); | |
| 163 treats = treats.filter(treat => treat.y < game.height + treatSize); | |
| 164 | |
| 165 // Find closest treat for dog to chase | |
| 166 let targetTreat = null; | |
| 167 let closestDist = Infinity; | |
| 168 treats.forEach(treat => { | |
| 169 const dx = treat.x + treatSize / 2 - (x + imgWidth / 2); | |
| 170 const dy = treat.y + treatSize / 2 - (y + imgHeight / 2); | |
| 171 const dist = Math.sqrt(dx * dx + dy * dy); | |
| 172 | |
| 173 if (dist < closestDist) { | |
| 174 closestDist = dist; | |
| 175 targetTreat = treat; | |
| 176 } | |
| 177 }); | |
| 178 | |
| 179 // Dog behavior | |
| 180 if (targetTreat) { | |
| 181 const dx = targetTreat.x + treatSize / 2 - (x + imgWidth / 2); | |
| 182 const dy = targetTreat.y + treatSize / 2 - (y + imgHeight / 2); | |
| 183 const distance = Math.sqrt(dx * dx + dy * dy); | |
| 184 | |
| 185 // Dog eating | |
| 186 if (distance < reachDistance) | |
| 187 { | |
| 188 treats = treats.filter(t => t !== targetTreat); | |
| 189 vx = initialDogVx; | |
| 190 } | |
| 191 // Looking for it | |
| 192 else | |
| 193 { | |
| 194 const speed = initialDogVx * 1.5; | |
| 195 vx = dx > 0 ? speed : -speed; | |
| 196 | |
| 197 // Update flip direction | |
| 198 if ((dx > 0 && isFlipped) || (dx < 0 && !isFlipped)) | |
| 199 { | |
| 200 isFlipped = !isFlipped; | |
| 201 } | |
| 202 | |
| 203 // Jump if treat is above and dog is on ground | |
| 204 const onGround = y + imgHeight >= game.height - 5; | |
| 205 if (dy < -50 && onGround && Math.abs(vy) < 1) | |
| 206 { | |
| 207 vy = jumpForce; | |
| 208 } | |
| 209 } | |
| 210 } | |
| 211 | |
| 212 // --- Entity updates --- // | |
| 213 vy += gravity; | |
| 214 x += vx; | |
| 215 y += vy; | |
| 216 | |
| 217 // Bounce off walls | |
| 218 if (x <= 0 || x + imgWidth >= game.width) | |
| 219 { | |
| 220 if (!targetTreat) | |
| 221 { | |
| 222 vx = -vx; | |
| 223 isFlipped = !isFlipped; | |
| 224 } | |
| 225 x = x <= 0 ? 0 : game.width - imgWidth; | |
| 226 } | |
| 227 | |
| 228 // Bounce off floor | |
| 229 if (y + imgHeight >= game.height) | |
| 230 { | |
| 231 y = game.height - imgHeight; | |
| 232 vy = -vy * 0.7; | |
| 233 if (Math.abs(vy) < 2) vy = 0; | |
| 234 } | |
| 235 | |
| 236 // Random jumps (only when not chasing treat) | |
| 237 if (!targetTreat && Date.now() > nextJump && Math.abs(vy) < 1) | |
| 238 { | |
| 239 vy = jumpForce; | |
| 240 nextJump = Date.now() + 1000 + Math.random() * 3000; | |
| 241 } | |
| 242 | |
| 243 // --- Draw entity --- // | |
| 244 ctx.clearRect(0, 0, game.width, game.height); | |
| 245 ctx.save(); | |
| 246 | |
| 247 if (isFlipped) { | |
| 248 ctx.translate(x + imgWidth, y); | |
| 249 ctx.scale(-1, 1); | |
| 250 ctx.drawImage(images[index], 0, 0); | |
| 251 } else { | |
| 252 ctx.drawImage(images[index], x, y); | |
| 253 } | |
| 254 | |
| 255 ctx.restore(); | |
| 256 treats.forEach(treat => { | |
| 257 ctx.drawImage(treatImage, treat.x, treat.y, treatSize, treatSize); | |
| 258 }); | |
| 259 | |
| 260 drawUI(); | |
| 261 // repeat | |
| 262 requestAnimationFrame(animate); | |
| 263 } | |
| 264 | |
| 265 // Draw UI elements (treat toggle button) | |
| 266 function drawUI() { | |
| 267 uiCtx.drawImage(treatImage, 0, 0, treatSize, treatSize); | |
| 268 } | |
| 269 | |
| 270 // --- Window functions --- // | |
| 271 function resizeCanvas() { | |
| 272 background.width = window.innerWidth; | |
| 273 background.height = window.innerHeight; | |
| 274 game.width = window.innerWidth; | |
| 275 game.height = window.innerHeight; | |
| 276 | |
| 277 // resize treats | |
| 278 treatSize = window.innerWidth < 700 ? originalTreatSize * 0.6 : originalTreatSize; | |
| 279 | |
| 280 // Size UI canvas to fit treat button + text | |
| 281 gameUI.width = treatSize + 10; | |
| 282 gameUI.height = treatSize + 25; | |
| 283 | |
| 284 createStars(); // Recreate stars on resize | |
| 285 drawUI(); // Redraw UI after resize | |
| 286 } | |
| 287 resizeCanvas(); | |
| 288 window.addEventListener('resize', resizeCanvas); | |
| 289 | |
| 290 window.onload = () => { | |
| 291 resizeCanvas(); // ensure canvas has real size | |
| 292 createStars(); // stars need correct canvas size | |
| 293 animateBackground(); // safe to start background animation | |
| 294 }; | |
| 295 |