view mrjunejune/src/public/dog-game.js @ 216:e82b80b24012 default tip

[MrJuneJune] Make webp translate background job.
author June Park <parkjune1995@gmail.com>
date Sat, 28 Feb 2026 21:04:43 -0800
parents 84826b3c655b
children
line wrap: on
line source

// Dog frames
const imagesSrc = [
  "/public/sprite_shiba0.png",
  "/public/sprite_shiba1.png",
  "/public/sprite_shiba2.png",
  "/public/sprite_shiba3.png",
];
const images =imagesSrc.map((src, index) => {
  const img = new Image();
  img.src = src;
  img.onload = () => {
    if (index == imagesSrc.length-1)
      startAnimation();
  };
  return img
});

// Load treat image
const treatImage = new Image();
treatImage.src = "/public/dog-treat.png";

// Load star images for background
let starImages = [
  { src: "/public/start-large.png", img: new Image() },
  { src: "/public/start-small-1.png", img: new Image() },
  { src: "/public/start-small-2.png", img: new Image() },
  { src: "/public/start-small-3.png", img: new Image() }
];
starImages.forEach(star => star.img.src = star.src);

const background = document.getElementById('background');
const bgCtx = background.getContext("2d");
const ctx = game.getContext("2d");
const uiCtx = gameUI.getContext("2d");


// Dog initial position and movement
const initialDogVx = 2;
let x = Math.random() * (game.width);
let y = Math.random() * (game.height);
let vx = initialDogVx; 
let vy = 0; 
const gravity = 0.5;
const jumpForce = -12;
let isFlipped = false;

// Random jump interval
// TODO: let's do it by frame?
let nextJump = Math.random() * 2000 + 1000;

// Treat system
const originalTreatSize = 30;
let treatSize = originalTreatSize;
const treatMargin = 20;
const reachDistance = 50;
let treatRainActive = false;
let treats = []; 
let lastTreatSpawn = 0;
const treatFallSpeed = 0.25;
const treatGravity = 0.13;

// Background stars
let stars = [];
function createStars() {
  stars = [];
  const starCount = window.innerWidth * 0.05;

  for (let i = 0; i < starCount; i++) {
    const starType = starImages[Math.floor(Math.random() * starImages.length)];
    const scale = 0.5 + Math.random() * 0.8; // Random scale

    stars.push({
      x: Math.random() * background.width,
      y: Math.random() * background.height,
      vx: (Math.random() - 0.5) * 0.5,
      vy: (Math.random() - 0.5) * 0.5,
      rotation: Math.random() * Math.PI * 2,
      rotationSpeed: (Math.random() - 0.5) * 0.02,
      image: starType.img,
      size: starType.img.width,
      scale: scale,
      opacity: 0.3 + Math.random() * 0.4 // Random opacity
    });
  }
}

function animateBackground() {
  bgCtx.clearRect(0, 0, background.width, background.height);

  stars.forEach(star => {
    // Update position
    star.x += star.vx;
    star.y += star.vy;
    star.rotation += star.rotationSpeed;

    // Wrap around edges
    if (star.x < -100) star.x = background.width + 100;
    if (star.x > background.width + 100) star.x = -100;
    if (star.y < -100) star.y = background.height + 100;
    if (star.y > background.height + 100) star.y = -100;

    // Draw star
    bgCtx.save();
    bgCtx.globalAlpha = star.opacity;
    bgCtx.translate(star.x, star.y);
    bgCtx.rotate(star.rotation);

    const size = star.size * star.scale;
    bgCtx.drawImage(star.image, -size / 2, -size / 2, size, size);

    bgCtx.restore();
  });

  requestAnimationFrame(animateBackground);
}

function getMousePosition(event) {
  const rect = game.getBoundingClientRect();
  const x = event.clientX - rect.left;
  const y = event.clientY - rect.top;
  return { x: x, y: y };
}

// UI canvas click - toggle treat rain
gameUI.addEventListener('click', (e) => {
  treatRainActive = !treatRainActive;
  if (treatRainActive)
    game.classList.add('active');
  else
    game.classList.remove('active');
});

game.addEventListener('click', (e) => {
  if (treatRainActive) {
    const mousePos = getMousePosition(e);
    treats.push({ x: mousePos.x, y: mousePos.y, vy: treatFallSpeed });
  }
});

function startAnimation() {
  requestAnimationFrame(animate);
}


let index = 0;
let last = 0;
const frameDuration = 120; // ms per frame
function animate(ts) {
  if (ts - last > frameDuration) {
    index = (index + 1) % images.length;
    last = ts;
  }

  // Get image dimensions
  const imgWidth = images[0].width;
  const imgHeight = images[0].height;

  // Update falling treats
  treats.forEach(treat => {
    treat.vy += treatGravity;
    treat.y += treat.vy;
  });
  treats = treats.filter(treat => treat.y < game.height + treatSize);

  // Find closest treat for dog to chase
  let targetTreat = null;
  let closestDist = Infinity;
  treats.forEach(treat => {
    const dx = treat.x + treatSize / 2 - (x + imgWidth / 2);
    const dy = treat.y + treatSize / 2 - (y + imgHeight / 2);
    const dist = Math.sqrt(dx * dx + dy * dy);

    if (dist < closestDist) {
      closestDist = dist;
      targetTreat = treat;
    }
  });

  // Dog behavior
  if (targetTreat) {
    const dx = targetTreat.x + treatSize / 2 - (x + imgWidth / 2);
    const dy = targetTreat.y + treatSize / 2 - (y + imgHeight / 2);
    const distance = Math.sqrt(dx * dx + dy * dy);

    // Dog eating
    if (distance < reachDistance)
    {
      treats = treats.filter(t => t !== targetTreat);
      vx = initialDogVx;
    }
    // Looking for it
    else
    {
      const speed = initialDogVx * 1.5;
      vx = dx > 0 ? speed : -speed;

      // Update flip direction
      if ((dx > 0 && isFlipped) || (dx < 0 && !isFlipped))
      {
        isFlipped = !isFlipped;
      }

      // Jump if treat is above and dog is on ground
      const onGround = y + imgHeight >= game.height - 5;
      if (dy < -50 && onGround && Math.abs(vy) < 1)
      {
        vy = jumpForce;
      }
    }
  }

  // --- Entity updates --- // 
  vy += gravity;
  x += vx;
  y += vy;

  // Bounce off walls
  if (x <= 0 || x + imgWidth >= game.width)
  {
    if (!targetTreat)
    {
      vx = -vx;
      isFlipped = !isFlipped;
    }
    x = x <= 0 ? 0 : game.width - imgWidth;
  }

  // Bounce off floor
  if (y + imgHeight >= game.height)
  {
    y = game.height - imgHeight;
    vy = -vy * 0.7; 
    if (Math.abs(vy) < 2) vy = 0;
  }

  // Random jumps (only when not chasing treat)
  if (!targetTreat && Date.now() > nextJump && Math.abs(vy) < 1)
  {
    vy = jumpForce;
    nextJump = Date.now() + 1000 + Math.random() * 3000;
  }

  // --- Draw entity --- // 
  ctx.clearRect(0, 0, game.width, game.height);
  ctx.save();

  if (isFlipped) {
    ctx.translate(x + imgWidth, y);
    ctx.scale(-1, 1);
    ctx.drawImage(images[index], 0, 0);
  } else {
    ctx.drawImage(images[index], x, y);
  }

  ctx.restore();
  treats.forEach(treat => {
    ctx.drawImage(treatImage, treat.x, treat.y, treatSize, treatSize);
  });

  drawUI();
  // repeat
  requestAnimationFrame(animate);
}

// Draw UI elements (treat toggle button)
function drawUI() {
  uiCtx.drawImage(treatImage, 0, 0, treatSize, treatSize);
}

// --- Window functions --- //
function resizeCanvas() {
  background.width = window.innerWidth;
  background.height = window.innerHeight;
  game.width = window.innerWidth;
  game.height = window.innerHeight;

  // resize treats
  treatSize = window.innerWidth < 700 ? originalTreatSize * 0.6 : originalTreatSize;

  // Size UI canvas to fit treat button + text
  gameUI.width = treatSize + 10;
  gameUI.height = treatSize + 25;

  createStars(); // Recreate stars on resize
  drawUI(); // Redraw UI after resize
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);

window.onload = () => {
  resizeCanvas();     // ensure canvas has real size
  createStars();      // stars need correct canvas size
  animateBackground(); // safe to start background animation
};