commit d49d608e102371a5f0a18cbdc04a7b9c940b90ae Author: Nathan Steel Date: Thu Dec 12 20:04:08 2024 +0000 First Commit 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(); +}