From d49d608e102371a5f0a18cbdc04a7b9c940b90ae Mon Sep 17 00:00:00 2001 From: Nathan Steel Date: Thu, 12 Dec 2024 20:04:08 +0000 Subject: [PATCH] First Commit --- README.md | 2 + components.js | 14 +++ entities.js | 3 + global.js | 9 ++ helpers.js | 32 ++++++ index.html | 33 ++++++ interaction.js | 187 ++++++++++++++++++++++++++++++++ main.js | 279 ++++++++++++++++++++++++++++++++++++++++++++++++ shapes.js | 96 +++++++++++++++++ spriteCanvas.js | 62 +++++++++++ 10 files changed, 717 insertions(+) create mode 100644 README.md create mode 100644 components.js create mode 100644 entities.js create mode 100644 global.js create mode 100644 helpers.js create mode 100644 index.html create mode 100644 interaction.js create mode 100644 main.js create mode 100644 shapes.js create mode 100644 spriteCanvas.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..dadeac1 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# Entities + diff --git a/components.js b/components.js new file mode 100644 index 0000000..546b6ae --- /dev/null +++ b/components.js @@ -0,0 +1,14 @@ +position = {} +size = {} + +team = { + good : {} + ,bad : {} + ,allied : {} +} + +direction = {} + +zindex = {} + +speed = {} diff --git a/entities.js b/entities.js new file mode 100644 index 0000000..8e95c41 --- /dev/null +++ b/entities.js @@ -0,0 +1,3 @@ +entities = {}; +entityCount = 0; +entityLive = 0; // To keep track of how many active entities there are (just a count) diff --git a/global.js b/global.js new file mode 100644 index 0000000..17d8292 --- /dev/null +++ b/global.js @@ -0,0 +1,9 @@ +const GAME_VERSION = 'v0.0'; + +const ctx = canvas.getContext('2d'); +let canvasLeft = canvas.offsetLeft + canvas.clientLeft; +let canvasTop = canvas.offsetTop + canvas.clientTop; + +ctx.font = "12px Arial"; +canvas.style.backgroundColor = 'rgb(143 153 150)'; +ctx.fillStyle = '#000'; diff --git a/helpers.js b/helpers.js new file mode 100644 index 0000000..eff45cf --- /dev/null +++ b/helpers.js @@ -0,0 +1,32 @@ +function printText(text, positionX, positionY, alignment = 'left', baseline = 'alphabetic', style = 'normal', weight = 'normal', size = '10', font = 'Arial', colour = '#000', strokeStyle = false, lineWidth = false, strokeOnly = false){ + + // Save the styling, and content already on the canvas + ctx.save(); + + // Do the alterations and print the text + ctx.textAlign = alignment; + ctx.textBaseline = baseline; + + // Set the font styling + ctx.font = style+' '+weight+' '+size+'pt'+' '+font; + //ctx.font-style = fontStyle; // normal, italic, oblique + ctx.fillStyle = colour; + + if(strokeStyle && lineWidth){ + // Set the stroke styling + ctx.strokeStyle = strokeStyle; + ctx.lineWidth = lineWidth; + + // Add the stroke (first, before fill) as it looks better + ctx.strokeText(text, positionX, positionY); + } + + if(!strokeOnly){ + // Actually add the text + ctx.fillText(text, positionX, positionY); + } + + // Restore the prior existing canvas content + ctx.restore(); + +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..ae2f659 --- /dev/null +++ b/index.html @@ -0,0 +1,33 @@ + + + + + + Entities + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + diff --git a/interaction.js b/interaction.js new file mode 100644 index 0000000..3c802da --- /dev/null +++ b/interaction.js @@ -0,0 +1,187 @@ +// Which interaction occurred for logic checks +const INTERACTION = { + 'left_click' : 0 + , 0 : 'left_click' + ,'right_click' : 1 + , 1 : 'right_click' +}; + +// Mouse data +const MOUSE = { + down : false + ,x : 0 + ,y : 0 + ,button : [0,0,0] // Left, Middle, Right +}; + +// Keypress data +const KEYPRESS = { + w : 0 + ,a : 0 + ,s : 0 + ,d : 0 + ,shift : 0 +} + + +// MOUSE +// https://stackoverflow.com/a/29957988 +canvas.addEventListener('mousedown', function(event) { + MOUSE.button[event.button] = 1; // Button down bool + MOUSE.down = true; +}); +canvas.addEventListener('mouseup', function(event) { + MOUSE.button[event.button] = 0; // Button not down bool + MOUSE.down = false; +}); +canvas.addEventListener('mouseout', function(event) { + MOUSE.down = false; +}); +canvas.addEventListener('mousemove', function(event) { + MOUSE.x = event.pageX - canvasLeft; + MOUSE.y = event.pageY - canvasTop; +}); + + +// Left click +canvas.addEventListener('click', function(event) { + + entityInteractionCheck( + event.pageX - canvasLeft, + event.pageY - canvasTop, + INTERACTION.left_click + ); + +}); + +// Right click +canvas.addEventListener('contextmenu', function(event) { + + event.preventDefault(); + + entityInteractionCheck( + event.pageX - canvasLeft, + event.pageY - canvasTop, + INTERACTION.right_click + ); + +}); + + +// Check if something has been clicked (via position) +// Allows to pass an entity OR an area to check +function clickableCheck(cursorX,cursorY,entity = null, area = null){ + + if(entity == null && area == null){ return false; } + if(entity != null){ + // Set area to size/pos of entity + area = { + 'x': position[entity].x, + 'y': position[entity].y, + 'width': size[entity].width, + 'height': size[entity].height + } + } + + // Collision detection between clicked offset and clickableItems + // https://stackoverflow.com/a/9880302 + if( + cursorY > area['y'] && cursorY < area['y'] + area['height'] + && cursorX > area['x'] && cursorX < area['x'] + area['width'] + ) + { + return true; + } + + return false; + +} + + +// KEYBOARD +window.addEventListener("keydown", onKeyDown, false); +window.addEventListener("keyup", onKeyUp, false); + +function onKeyDown(event) { + + if(event.shiftKey){ KEYPRESS.shift = true; } + + var keyCode = event.keyCode; + switch (keyCode) { + case 87: + KEYPRESS.w = true; + break; + case 65: + KEYPRESS.a = true; + break; + case 83: + KEYPRESS.s = true; + break; + case 68: + KEYPRESS.d = true; + break; + + } +} + +function onKeyUp(event) { + + if(event.shiftKey){ KEYPRESS.shift = false; } + + var keyCode = event.keyCode; + switch (keyCode) { + case 87: + KEYPRESS.w = false; + break; + case 65: + KEYPRESS.a = false; + break; + case 83: + KEYPRESS.s = false; + break; + case 68: + KEYPRESS.d = false; + break; + + } +} + + +// HELPERS (Should be for all) +function entityInteractionCheck(x,y,interaction){ + + // Loop the entities that are interactable + // I.e. Have size, and positions + + console.log('Interaction: '+INTERACTION[interaction]); + console.log('Positions; X: '+x+' Y: '+y); + + + for (const [entity] of Object.entries(size)) { + + // If there's no position (or size), skip + if(position[entity] === undefined){ continue; } + + // If the X/Y of cursor isn't within the shape's bounds can't interact + if(!clickableCheck(x,y,entity)){ + continue; + } + + console.log('Entity: '+entity); + console.log('Entities Total live: '+entityLive); + console.log('Entities Total overall: '+entityCount); + + switch (interaction) { + case INTERACTION.left_click: + + break; + + default: + break; + } + + + } + +} + diff --git a/main.js b/main.js new file mode 100644 index 0000000..ea7ff3f --- /dev/null +++ b/main.js @@ -0,0 +1,279 @@ +// https://stackoverflow.com/questions/25612452/html5-canvas-game-loop-delta-time-calculations +// TODO: Need to fully figure out a 60FPS and also display FPS on top right +let game = { + fps : 60 // ?? + ,time : 0.0 + ,start : Date.now() + ,frameDuration : 1000 / 60 // Target FPS 60? + ,lagOffset : 0 + ,player : false +} + +startGame(); + +function startGame(){ + + // Create player + createPlayerEntity(canvas.width/2, canvas.height/2); + + // Start game loop + gameLoop(); + +} + +function gameLoop(){ + + // Check player exists (not dead/undefined by OOB) + if(entities[game.player] === undefined || game.player === false){ + console.log('gome'); + return false; + } + + setTimeout(() => { + requestAnimationFrame(gameLoop, canvas); + }, game.frameDuration); + + // Calcuate the time that has elapsed since the last frame + let current = Date.now(); + let elapsed = current - game.start; + game.start = current; + game.lagOffset += elapsed; + + // Update the frame if the lag counter is greater than or + // equal to the frame duration + while (game.lagOffset >= game.frameDuration){ + + gameUpdate(); + game.lagOffset -= game.frameDuration; + + } + + // Calculate the lag offset and use it to render the sprites + // var lagOffset = Math.round(game.lagOffset / game.frameDuration); + // drawEntities(); // May need to pass lagOffset here to attempt to correct positions, etc. + drawGame(); + + // Print the game version above everything + printText(GAME_VERSION, + 20, + canvas.height - 20, + 'left', 'alphabetic', 'normal', 'bold', '10', 'Arial', '#00000050' + ); + + printText(elapsed, + canvas.width - 20 - (ctx.measureText(elapsed).width), + 20 + 10, // + 10 is pt font-size to get in right pos + 'left', 'alphabetic', 'normal', 'bold', '10', 'Arial', '#00000090' + ); + +} + +// All the game update logic +function gameUpdate(){ + + calculatePlayerMovement(); + calculateDirectionToPlayer(); + + updateMovement(); + updateOOB(); + + if(MOUSE.down){ + createBulkEntities(MOUSE.x, MOUSE.y); + } + + +} + +function calculatePlayerMovement(){ + + let speedNow = speed[game.player]; + if(KEYPRESS.shift){ speedNow = speedNow * 5; } + + if(KEYPRESS.w){ position[game.player].y -= (1*speedNow); } + if(KEYPRESS.s){ position[game.player].y += (1*speedNow); } + + if(KEYPRESS.a){ position[game.player].x -= (1*speedNow); } + if(KEYPRESS.d){ position[game.player].x += (1*speedNow); } + +} + +function updateMovement(){ + // Use the direction currently, but likely 'velocity' in the future + for (const [entity] of Object.entries(direction)) { + if(position[entity] === undefined){ continue; } + + if(direction[entity].x !== undefined){ + if(direction[entity].x == 1){ + position[entity].x += (1 * speed[entity]); + } + else if(direction[entity].x == -1){ + position[entity].x -= (1 * speed[entity]); + } + } + + if(direction[entity].y !== undefined){ + if(direction[entity].y == 1){ + position[entity].y += (1 * speed[entity]); + } + else if(direction[entity].y == -1){ + position[entity].y -= (1 * speed[entity]); + } + } + + } +} +function calculateDirectionToPlayer(){ + + // Enemies will W key towards the player as a hoard + for (const [entity] of Object.entries(position)) { + + if(entity == game.player){ continue; } + + if(position[game.player].x > position[entity].x){ direction[entity].x = 1; } + else{ direction[entity].x = -1; } + + if(position[game.player].y > position[entity].y){ direction[entity].y = 1; } + else{ direction[entity].y = -1; } + + } + +} + +function updateOOB(){ + for (const [entity] of Object.entries(position)) { + // Remove the entity if it goes out of bounds + if( + position[entity].y >= canvas.height-50 || position[entity].y <= 50 + || + position[entity].x >= canvas.width-50 || position[entity].x <= 50 + ){ + deleteEntity(entity); + } + } +} +function deleteEntity(entity){ + + // Remove from each component + // Definitely the most inefficient and janky way, but it does it + if(position[entity] !== null){ delete position[entity]; } + if(size[entity] !== null){ delete size[entity]; } + if(team.good[entity] !== null){ delete team.good[entity]; } + if(team.bad[entity] !== null){ delete team.bad[entity]; } + if(team.allied[entity] !== null){ delete team.allied[entity]; } + if(direction[entity] !== null){ delete direction[entity]; } + + // Finally remove the entity itself + delete entities[entity]; + entityLive--; // To keep track of how many active entities there are (just a count) + +} + + +function drawGame(){ + + // Reset/clear all prior draws + ctx.clearRect(0, 0, canvas.width, canvas.height); + + drawEntities(); + + // Draw 'temp' hitbox area (to visualise entity removal) + drawRectangle(50,50,canvas.width-100,canvas.height-100,null,'#FFF'); + +} + +// Draw the game's entities +function drawEntities(){ + + // Draw all entities (with size/position) + for (const entity in size) { + + // If there's no position (or size), skip + if(position[entity] === undefined){ continue; } + + // TEMP z-index draw to ensure player, etc. are drawn atop enemies (for now) + // 0,1,2 TODO: WHY ISN'T IT WOKRIINNNGG + for(let i = 0; i < 2; i++){ + + // JANK IF. Change to defensive when I can be bothered + if((zindex[entity] === undefined && i === 0) || zindex[entity] === i){ + + // console.log(zindex[entity]); + + if(team.bad[entity] !== undefined){ + // Draw pixels from imageCanvas + drawEnemy(entity); + } + if(team.allied[entity] !== undefined){ + // Draw pixels from imageCanvas + drawAlly(entity); + } + + // Player, temp + if(team.good[entity] !== undefined){ + // Draw pixels from imageCanvas + drawPlayer(entity); + } + + } + + } + + } + +} + + +function createTestEntity(x,y){ + + entityCount++; + entityLive++; // To keep track of how many active entities there are (just a count) + entities[entityCount] = entityCount; + + position[entityCount] = {x: x, y: y}; + size[entityCount] = {width: 16, height: 16}; + + if(MOUSE.button[0]){ + team.bad[entityCount] = true; + } + else if(MOUSE.button[1]){ + team.allied[entityCount] = true; + } + else if(MOUSE.button[2]){ + // + } + + direction[entityCount] = {x: 0, y: 0}; // 0,1 left,right/up,down + zindex[entityCount] = 0; + speed[entityCount] = .75; + +} +function createBulkEntities(x,y){ + let amount = 90; + for(let i = 0; i < amount; i++){ + createTestEntity( + x+(Math.floor(Math.random() * 45)+1) + ,y+Math.floor(Math.random() * 35)+1 + ); + } +} + + +function createPlayerEntity(x,y){ + + entityCount++; + game.player = entityCount; // Set game.player to the player entity. TEMP + + entityLive++; // To keep track of how many active entities there are (just a count) + entities[entityCount] = entityCount; + + position[entityCount] = {x: x, y: y}; + size[entityCount] = {width: 16, height: 16}; + + team.good[entityCount] = true; + + direction[entityCount] = {x: 0, y: 0}; // 0,1 left,right/up,down + + zindex[entityCount] = 1; + speed[entityCount] = 1.6; + +} diff --git a/shapes.js b/shapes.js new file mode 100644 index 0000000..5629aac --- /dev/null +++ b/shapes.js @@ -0,0 +1,96 @@ +let defaultFillStyle = '#FF0'; +let shapeDebug = true; +let shapeDebugColour = '#FF00FF'; +let lineWidth = 1; + +// Don't think I'll need/want this too often now +// keeping for UI elements that will draw less regular +// but no longer for entities +function drawRectangle(x,y,w,h,colour = false, strokeColour = false){ + + if(!colour && colour !== null){ colour = defaultFillStyle; } + ctx.fillStyle = colour; + + if(colour !== null){ + ctx.fillRect(x,y,w,h); + } + + ctx.strokeStyle = strokeColour; + ctx.lineWidth = lineWidth; + ctx.strokeRect(x,y,w,h); + + if(shapeDebug && !strokeColour){ + ctx.strokeStyle = shapeDebugColour; + ctx.lineWidth = lineWidth; + ctx.strokeRect(x,y,w,h); + } + +} + +function drawCircle(x,y,w,colour = false, strokeColour = false){ + + if(!colour){ colour = defaultFillStyle; } + ctx.fillStyle = colour; + + ctx.beginPath(); + ctx.arc(x,y+(w/2), w/2, 0, 2 * Math.PI); // y+(w/2) to align like rect as circle draws from centre + ctx.fill(); + + if(shapeDebug){ + ctx.strokeStyle = shapeDebugColour; + ctx.lineWidth = lineWidth; + ctx.stroke(); + } + + ctx.closePath(); + +} + +function drawSemiCircle(x,y,w,colour = false, strokeColour = false){ + + if(!colour){ colour = defaultFillStyle; } + ctx.fillStyle = colour; + + ctx.beginPath(); + ctx.arc(x,y+(w/2), w/2, Math.PI, 0); + ctx.fill(); + + if(shapeDebug){ + ctx.strokeStyle = shapeDebugColour; + ctx.lineWidth = lineWidth; + ctx.stroke(); + } + + ctx.closePath(); + +} + + + +// Shape references from image canvas to be blitted to +function drawEnemy(entity){ + ctx.drawImage(imageCanvas, + // source x,y,w,h + 0,0,16,16 + // x,y,w,h to be drawn onto the (visibile/game) canvas + ,position[entity].x,position[entity].y,size[entity].width,size[entity].height + ); +} + +function drawAlly(entity){ + ctx.drawImage(imageCanvas, + // source x,y,w,h + 16,0,16,16 + // x,y,w,h to be drawn onto the (visibile/game) canvas + ,position[entity].x,position[entity].y,size[entity].width,size[entity].height + ); +} + +function drawPlayer(entity){ + ctx.drawImage(imageCanvas, + // source x,y,w,h + 32,0,16,16 + // x,y,w,h to be drawn onto the (visibile/game) canvas + ,position[entity].x,position[entity].y,size[entity].width,size[entity].height + ); +} \ No newline at end of file diff --git a/spriteCanvas.js b/spriteCanvas.js new file mode 100644 index 0000000..ec3a3de --- /dev/null +++ b/spriteCanvas.js @@ -0,0 +1,62 @@ +// WIP: Draw to another canvas then reference that shape +// takes waay less resource +// Draw image from 'temp canvas' to "0,0" on primary canvas +// Will do this to draw each sprite onto the temp canvas, then reference +// the canvas for 'pixel manipulation' or whatnot, see how it goes +imageCanvas = document.createElement("canvas"), +ictx = imageCanvas.getContext("2d"); + +document.getElementById('canvasRule').parentNode.insertBefore(imageCanvas, document.getElementById('canvasRule')); + +// set the canvas to the size of the image(s) with gap, smaller = better +sprites = 3; + +imageCanvas.width = 16*sprites; +imageCanvas.height = 16; + +addEnemySprite(); +addAlliedSprite(); +addPlayerSprite(); + +// Doing all the sprites, etc as 16*16 with 0 gap between +// essentially just a spritesheet on the canvas +function addEnemySprite(){ + ictx.fillStyle = '#FF0'; + ictx.fillRect(0,0,16,16); + + ictx.strokeStyle = '#FF00FF'; + ictx.lineWidth = 1; + ictx.strokeRect(0,0,16,16); +} + +function addAlliedSprite(){ + + ictx.fillStyle = '#0EF'; + + ictx.beginPath(); + // awkward cirlce maffs + ictx.arc(16+(16/2),16/2, 15/2, 0, 2 * Math.PI); // y+(w/2) to align like rect as circle draws from centre + ictx.fill(); + + ictx.strokeStyle = '#FF00FF'; + ictx.lineWidth = 1; + ictx.stroke(); + + ictx.closePath(); +} + +function addPlayerSprite(){ + + ictx.fillStyle = '#5ce65c'; + + ictx.beginPath(); + // awkward cirlce maffs + ictx.arc(32+(16/2),16/2, 15/2, 0, 2 * Math.PI); // y+(w/2) to align like rect as circle draws from centre + ictx.fill(); + + ictx.strokeStyle = '#FF00FF'; + ictx.lineWidth = 1; + ictx.stroke(); + + ictx.closePath(); +}