about summary refs log blame commit diff stats
path: root/js/game-frame/game.js
blob: cc463fe7f376f760f065f9457c9232c81b71853d (plain) (tree)
1
2
3
4
5
6
7
8
                   


                                                                       



                                                     























                                                                                                                                
 


 

 
                    
                                                               
                





                                                                    
                                                               
  
 
                                           
                      


                                     





                                                 
 


                                                                 

 
 
 


      
                     
             











                                                                                                                                                                                                                                                                                                                                                                                                        

  


 


        





                                        

 





                                                  

                                                 

  























                                                                                       






                                                            






















                                                                                                                          
 
 


 
                                                   



                                                                               


                                                                                          











                                                                                  


                     


                                                    
 




                                                                                       


                                 
 

                                                                                             
 





                                                                                               






                                                                                      

                                                                                    
     






                                                     

                         
 
 




                                                  
 
                                                       




                                                                 















                                                                                                         


                                              


                                                          







                                
                                   
                                              
                                                                
       

     



                                                                                                                     
     



                                        
 
 
 

 

 


                      




 






                                           

               
                            

                       

               
                            

                       

               
                            

                        

               
                            
              














                                                                             























                                                          





                                               
                                                                
                                                                        


                      
   
 
// Setup the canvas
// This'll be where the game is drawn
// Everything below is configured in relation to the canvas' size
// The canvas is 512px wide and 512px high, which makes for easier math
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
canvas.width = 512;
canvas.height = 512;
const startingX = 0;
const startingY = 0;





// Misc. helpers
const camera = {
  x: startingX,
  y: startingY,
  width: canvas.width,
  height: canvas.height
};

const cons = ["b", "c", "d", "f", "g", "h", "j", "k", "l", "m", "n", "p", "r", "s", "t", "v", "w", "z", "ch", "sh", "zh", "th"];
const vow = ["a", "e", "i", "o", "u", "y", "ee", "ai", "ae", "au"];
const rpick = t => t[Math.floor(Math.random() * t.length)];
const syl = () => rpick(cons) + rpick(vow);
const word = () => {
  const syllables = Array(Math.floor(Math.random() * 3) + 1).fill(null).map(() => syl()).join('');
  return Math.random() > 0.2 ? syllables + rpick(cons) : syllables;
};
const speak = numberOfWords => Array(numberOfWords).fill(null).map(() => word()).join(' ');






// Create the player
// This object tracks all information about the player chracter
const player = {
    x: startingX,       // X coordinate on the canvas
    y: startingY,       // Y coordinate on the canvas
    width: 8,           // Width of the player
    height: 8,          // Height of the player
    step: 8,            // How many pixels the player moves per step
    color: 'blue',      // Color of the player
    spriteUrl: "chickadee.svg"  // Sprite of the player, if any
};

// Player inventory and inventory mangement
player.inventory = [];
player.collectItem = function(item) {
  this.inventory.push(item);
}
player.dropItem = function(item) {
  const itemIndex = this.inventory.indexOf(item);
  if (itemIndex > -1) {
    this.inventory.splice(itemIndex, 1);
  }
}

// If you wanna have a sprite, you need to create an image object
const playerSprite = new Image();
playerSprite.src = player.spriteUrl;






// Map
const TILE_SIZE = 64;
const map = [
    [{ walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }],
    [{ walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }],
    [{ walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }],
    [{ walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }],
    [{ walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }],
    [{ walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }],
    [{ walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }],
    [{ walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }],
    [{ walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }],
    [{ walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }],
    [{ walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }],
    [{ walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }]
];






// Items
function Item(name, x, y, type, color) {
  this.name = name;
  this.x = x;
  this.y = y;
  this.type = type;
  this.color = color;
}

// Create some bespoke items
let items = [
  new Item('helmet', 16, 16, 'clothes', 'red'),
  new Item('banana', 24, 128, 'food', 'yellow'),
  new Item('bucket', 128, 256, 'object', 'green'),
  new Item('big key', 216, 216, 'key', 'cyan'),
  new Item('small key', 456, 256, 'key', 'cyan'),
  new Item('bike', 0, 48, 'bike', 'orange'),
];

// Populate the world with really very weird objects
// FIXME: update this so that items aren't always placed within the 0x0 space of a tile
const appendRandomItems = () => {
  const getRandomColor = () => '#' + Math.floor(Math.random() * 16777215).toString(16);
  const getRandomName = () => speak(Math.floor(Math.random() * 3) + 1);
  const getRandomCoordinates = () => {
    let x, y;
    do {
      x = Math.floor(Math.random() * (map[0].length - 2)) + 1;
      y = Math.floor(Math.random() * (map.length - 2)) + 1;
    } while (!map[y][x].walkable);
    return { x, y };
  };
  const createRandomItem = () => {
    const { x, y } = getRandomCoordinates();
    const name = getRandomName();
    const color = getRandomColor();
    return new Item(name, x * TILE_SIZE, y * TILE_SIZE, 'mystery', color);
  };
  const numberOfItems = Math.floor(Math.random() * 39);
  const randomItems = Array.from({ length: numberOfItems }, createRandomItem);
  items.push(...randomItems);
};

function checkForSpecialItems() {
  if (player.inventory.find(item => item.type === 'bike')) {
    player.step = 12;
  } else {
    player.step = 8;
  }
}





// Define the Sign class
function Sign(x, y, width, height, message) {
  this.x = x;
  this.y = y;
  this.width = width;
  this.height = height;
  this.message = message;
}

// Create some signs
let signs = [
  new Sign(34, 38, player.width, player.height, speak(3).toUpperCase()),
  new Sign(28, 64, player.width, player.height, 'Welcome to the game!'),
  new Sign(128, 128, player.width, player.height, 'You cannot pass through walls!'),
  new Sign(24, 212, player.width, player.height, 'You can collect items!'),
  new Sign(428, 712, player.width, player.height, 'This sign has very long text that will wrap around to the next line!'),
  new Sign(28, 712, player.width, player.height, 'This sign is very far away.'),
];





// Display the player's stats, useful for debugging
function displayStats() {
  document.getElementById("stats").innerHTML = "";
  for (let stat in player) {
    if (typeof player[stat] === "string" || typeof player[stat] === "number") {
      if (player[stat] !== player.spriteUrl) {
        document.getElementById("stats").innerHTML += stat + ": " + player[stat] + "<br>";
      }
    }
  }
  document.getElementById("stats").innerHTML += "Inventory: ";
  for (let i = 0; i < player.inventory.length; i++) {
    document.getElementById("stats").innerHTML += player.inventory[i].name + ", ";
  }
}






// Game loop
function gameLoop() {
  // Update the camera position to follow the player
  camera.x = player.x - camera.width / 2;
  camera.y = player.y - camera.height / 2;

  // Clamp the camera position to the map boundaries
  camera.x = Math.max(0, Math.min(map[0].length * TILE_SIZE - camera.width, camera.x));
  camera.y = Math.max(0, Math.min(map.length * TILE_SIZE - camera.height, camera.y));

  // Draw the map
  map.forEach((row, y) => {
    row.forEach((tile, x) => {
      ctx.fillStyle = tile.color;

      // Draw the tile
      ctx.fillRect(x * TILE_SIZE - camera.x, y * TILE_SIZE - camera.y, TILE_SIZE, TILE_SIZE);

      // Draw the grid
      ctx.strokeStyle = 'white';
      ctx.lineWidth = 1;
      ctx.strokeRect(x * TILE_SIZE - camera.x, y * TILE_SIZE - camera.y, TILE_SIZE, TILE_SIZE);
    });
  });

  // Draw the player
  ctx.fillStyle = player.color;
  ctx.fillRect(player.x - camera.x, player.y - camera.y, player.width, player.height);

  // Draw items and check for collisions
  items.forEach(item => {
    ctx.fillStyle = item.color;
    ctx.fillRect(item.x - camera.x, item.y - camera.y, player.width, player.height);
  });
  items = items.filter(item => {
    if (player.x === item.x && player.y === item.y) {
      player.collectItem(item);
      return false; // Remove the item from the map
    }
    return true;
  });

  checkForSpecialItems();


  // Draw signs and check for collisions
  signs.forEach(sign => {
    const dx = player.x - sign.x;
    const dy = player.y - sign.y;
    const distance = Math.sqrt(dx * dx + dy * dy);

    if (distance < player.width / 2 + sign.width / 2) {
      document.getElementById("dialog").innerHTML = sign.message;

      ctx.fillStyle = 'black';
      ctx.font = '22px Serif';

      // Calculate the starting position of the text
      const lineHeight = 24;

      // Calculate the starting position of the text within the sign
      const textWidth = ctx.measureText(sign.message).width;
      const textHeight = lineHeight;
      const startX = sign.x - camera.x + (sign.width - textWidth) / 2;
      const startYWithinSign = sign.y - camera.y + (sign.height - textHeight) / 2;

      // Calculate the final position of the text within the canvas area
      const padding = 12; // Set the desired padding value
      const finalX = Math.max(padding, Math.min(canvas.width - textWidth - padding, startX));
      const finalY = Math.max(padding, Math.min(canvas.height - textHeight - padding, startYWithinSign));

      // Wrap the text to multiple lines if it exceeds the canvas width
      const words = sign.message.split(' ');
      let line = '';
      let lines = [];
      for (let i = 0; i < words.length; i++) {
        const testLine = line + words[i] + ' ';
        const testWidth = ctx.measureText(testLine).width;
        if (testWidth > canvas.width - padding * 2) {
          lines.push(line);
          line = words[i] + ' ';
        } else {
          line = testLine;
        }
      }
      lines.push(line);

      // Draw each line of the text
      for (let i = 0; i < lines.length; i++) {
        ctx.fillText(lines[i], finalX, finalY + i * lineHeight);
      }
    }

    ctx.fillStyle = 'brown';
    ctx.beginPath();
    ctx.arc(sign.x - camera.x + sign.width / 2, sign.y - camera.y + sign.height / 2, sign.width / 2, 0, Math.PI * 2);
    ctx.fill();
  });

  // Call the game loop again next frame
  requestAnimationFrame(gameLoop);
  displayStats();
}






// Start the game loop
gameLoop();






// Handle user input
window.addEventListener('keydown', (e) => {
    let newX = player.x;
    let newY = player.y;
  
    switch (e.key) {
      case 'ArrowUp':
      case 'w':
      case 'k':
        newY -= player.step;
        break;
      case 'ArrowDown':
      case 's':
      case 'j':
        newY += player.step;
        break;
      case 'ArrowLeft':
      case 'a':
      case 'h':
        newX -= player.step;
        break;
      case 'ArrowRight':
      case 'd':
      case 'l':
        newX += player.step;
        break;
      case 'z':
      case 'n':
        player.inventory.forEach(item => {
          player.dropItem(item);
          items.push(item);
        });
        break;
      case 'x':
      case 'm':
        player.color = '#' + Math.floor(Math.random()*16777215).toString(16);
        break;
      case 'q':
      case 'p':
        player.color = 'blue';
        break;
    // Add this code where you handle the 'i' key press
    case 'i':
      // Create a menu element
      const menu = document.createElement('div');
      menu.style.position = 'fixed';
      menu.style.left = '10px';
      menu.style.top = '10px';
      menu.style.backgroundColor = 'white';
      menu.style.padding = '10px';
      document.body.appendChild(menu);

      // Add each inventory item to the menu
      player.inventory.forEach((item) => {
        const itemElement = document.createElement('div');
        itemElement.textContent = item.type;
        itemElement.style.cursor = 'pointer';
        itemElement.addEventListener('click', () => {
          player.dropItem(item);
          items.push(item);
          menu.remove(); // Remove the entire menu
        });
        menu.appendChild(itemElement);
      });
      break;
    }
  
    // Calculate the tile coordinates
    const tileX = Math.floor(newX / TILE_SIZE);
    const tileY = Math.floor(newY / TILE_SIZE);
  
    // Only update the player's position if the tile is walkable
    if (map[tileY] && map[tileY][tileX] && map[tileY][tileX].walkable) {
      player.x = newX;
      player.y = newY;
    }
});