diff --git a/index.js b/index.js index a6c6668..68dc3d5 100644 --- a/index.js +++ b/index.js @@ -6,6 +6,9 @@ window.onload = function () { * I use Object.assign to copy objects in order to eliminate references when spawning enemies and to get the ability to modify them individually * Components such as buttons or selections within the canvas are classes + * if, function and else have spaces between both '(' and '{' + `if (thing) {}` not `if(thing){}` + * Keep all variables local to their scope (in other words, use `let` instead of `var`) */ let canvas = document.getElementById('canvas') @@ -24,16 +27,19 @@ window.onload = function () { let Game = { state: 2, enemies: [], + enemySpawnList: [], towers: [], particles: [], + selltext: [], map: null, health: 100, money: 100, - enemySpawn: 0, - pace: 1, wave: 0, waveTimer: 0, - tower: 'simple' + tower: 'simple', + towerSel: null, + debug: false, + sellRatio: .8 } /** @@ -50,7 +56,7 @@ window.onload = function () { node: 1, health: 50, reward: 10, - frequency: 1000, + frequency: 40, icon: '#f00' }, speedy: { @@ -58,15 +64,15 @@ window.onload = function () { node: 1, health: 60, reward: 15, - frequency: 500, + frequency: 35, icon: '#f11' }, tough: { speed: 5, node: 1, - health: 100, + health: 80, reward: 20, - frequency: 1000, + frequency: 40, icon: '#f40' } } @@ -77,30 +83,44 @@ window.onload = function () { damage: 15, // damage to deal to enemies when hit rate: 20, // rate of fire, higher - slower name: 'Simple', // name of the tower - description: 'Basic tower', + description: 'Medium rate and damage', speed: 30, // bullet speed, higher - faster cost: 50, // cost to place - icon: '#333' // currently color + icon: '#333', // currently color + bullet: 1 // The type of bullet. 1: damage, 2: slow down by damage, 3: instant kill }, rapid: { range: 3, damage: 5, rate: 5, name: 'Rapid', - description: 'Rapid-firing tower', + description: 'Rapid-firing but low damage', speed: 30, cost: 250, - icon: '#303' + icon: '#303', + bullet: 1 }, - snipe: { - range: 5, - damage: 150, - rate: 100, - name: 'Sniper', - description: 'Slow but powerful shots', + sticky: { + range: 3, + damage: 10, + rate: 30, + name: 'Sticky', + description: 'Slow down enemies by damage', speed: 50, cost: 500, - icon: '#4f3' + icon: '#e27c06', + bullet: 2 + }, + snipe: { + range: 10, + damage: 1500, + rate: 100, + name: 'Sniper', + description: 'Slow firing but always kills', + speed: 50, + cost: 1000, + icon: '#4f3', + bullet: 1 } } @@ -145,6 +165,74 @@ window.onload = function () { {x: 5, y: 12, end: false}, {x: 1, y: 12, end: false}, {x: 1, y: 17, end: true}, + ], + waves: [ + { + type: 'recurring', + waveLow: 0, + waveHigh: 10, + oneAfterAnother: false, + enemies: [{ + type: 'basic', + count: 5, + inclCount: true, + inclHealth: true + }] + }, + { + type: 'recurring', + waveLow: 10, + waveHigh: 15, + oneAfterAnother: false, + enemies: [{ + type: 'basic', + count: 5, + inclCount: true, + inclHealth: true + }, + { + type: 'speedy', + count: 10, + inclCount: true, + inclHealth: true + }] + }, + { + type: 'recurring', + waveLow: 15, + oneAfterAnother: false, + enemies: [{ + type: 'basic', + count: 5, + inclCount: true, + inclHealth: true + }, + { + type: 'speedy', + count: 10, + inclCount: true, + inclHealth: true + }] + }, + { + type: 'once-every', + every: 5, + oneAfterAnother: false, + enemies: [{ + type: 'tough', + count: 5, + inclCount: true, + inclHealth: true + }] + }, + { + type: 'once', + wave: 3, + enemies: [{ + type: 'tough', + count: 2 + }] + } ] }, generate: function(targetLength) { @@ -261,12 +349,115 @@ window.onload = function () { class Component { constructor (x, y) { + this.visible = true + this.elements = [] this.x = x this.y = y } - draw () {} - update() {} + elDraw() { + for (let i in this.elements) { + let elem = this.elements[i] + if (elem instanceof Component) { + elem.draw() + } + } + } + + elUpdate() { + for (let i in this.elements) { + let elem = this.elements[i] + if (elem instanceof Component) { + elem.update() + } + } + } + + addElement (el) { + if (!(el instanceof Component)) return + this.elements.push(el) + } + + draw () { + this.elDraw() + } + + update() { + this.elUpdate() + } + } + + class Tooltip extends Component { + constructor () { + super(0, 0) + this.font = '20px Helvetica' + this.text = '' + this.w = 0 + this.h = 24 + this.components = [] + this.visible = false + } + + static assign (tooltip, component, text) { + tooltip.addComponent(component, text) + } + + setText (str) { + if (this.text === str) return + this.text = str + if (this.font) ctx.font = this.font + this.w = ctx.measureText(this.text).width + this.h + } + + draw () { + if (!this.visible) return + if (this.text === '') return + if (this.font) ctx.font = this.font + + let aX = this.x + let aY = this.y + + if (aX + this.w > canvas.width) { + aX -= this.w + 5 + } + + ctx.fillStyle = 'rgba(255, 255, 255, 0.5)' + ctx.fillRect(aX, aY, this.w, this.h) + + ctx.fillStyle = '#000' + ctx.fillText(this.text, aX + this.h / 2, aY + this.h / 2 + 5) + } + + setPosition (x, y) { + this.x = x + this.y = y + } + + update () { + if (this.components.length) { + let cmps = false + for (let i in this.components) { + let cmp = this.components[i] + if (mXr > cmp.x && mYr > cmp.y && + mXr < cmp.x + cmp.w && mYr < cmp.y + cmp.h) { + this.setPosition(mXr, mYr) + this.setText(cmp.text) + cmps = true + } + } + this.visible = cmps + } + } + + addComponent (component, text) { + this.components.push({ + x: component.x, + y: component.y, + w: component.w, + h: component.h, + text: text + }) + } } class ButtonComponent extends Component { @@ -282,9 +473,11 @@ window.onload = function () { this.color = color this.disabled = false this.hovered = false + this.font = '20px Helvetica' } draw () { + if (!this.visible) return if (this.font) ctx.font = this.font ctx.fillStyle = this.color ctx.fillRect(this.x, this.y, this.w, this.h) @@ -299,9 +492,19 @@ window.onload = function () { ctx.fillStyle = 'rgba(0, 0, 0, 0.45)' ctx.fillRect(this.x, this.y, this.w, this.h) } else if (this.hovered) { - ctx.fillStyle = 'rgba(255, 255, 255, 0.25)' + ctx.fillStyle = 'rgba(255, 255, 255, 0.15)' ctx.fillRect(this.x, this.y, this.w, this.h) } + this.elDraw() + } + + update () { + if (!this.visible || this.disabled) return + if (mXr > this.x && mYr > this.y && mXr < this.x + this.w && mYr < this.y + this.h) { + this.hovered = true + } else if (this.hovered) { + this.hovered = false + } } setDisabled (disable) { @@ -324,14 +527,25 @@ window.onload = function () { this.textColor = '#fff' this.color = '#995522' this.fn = this.select - this.font = '14px Helvetica' } select () { Game.tower = this.tower } + addTooltip () { + Tooltip.assign(Components.tooltip, this, this.towerObj.description) + } + + update () { + super.update() + this.disabled = this.towerObj.cost > Game.money && !Game.debug + this.active = Game.tower === this.tower + this.elUpdate() + } + draw () { + if (!this.visible) return if (this.active) { ctx.fillStyle = '#afa' ctx.fillRect(this.x - 2, this.y - 2, this.w + 4, this.h + 4) @@ -362,56 +576,191 @@ window.onload = function () { ctx.fillStyle = 'rgba(0, 0, 0, 0.45)' ctx.fillRect(this.x, this.y, this.w, this.h) } else if (this.hovered) { - ctx.fillStyle = 'rgba(255, 255, 255, 0.25)' + ctx.fillStyle = 'rgba(255, 255, 255, 0.15)' ctx.fillRect(this.x, this.y, this.w, this.h) } + this.elDraw() } } - function clickBtn () { - for (let i in Components) { - let btn = Components[i] - if (!(btn instanceof ButtonComponent)) continue - if (btn.disabled) continue - if (mXr > btn.x && mYr > btn.y && mXr < btn.x + btn.w && mYr < btn.y + btn.h) { - btn.fn() + class InfoDialog extends Component { + constructor () { + super() + this.x = 0 + this.y = (Maps.height - 5) * Maps.tile + this.w = Maps.width * Maps.tile + this.h = 5 * Maps.tile + this.createButton() + } + + createButton () { + let btn = new ButtonComponent('Sell Tower', '#fff', '#f11', 490, 590, 140, 40, () => { + if (Game.towerSel) { + sellTower(Game.towerSel.x, Game.towerSel.y) + } + }) + + btn.update = function () { + this.visible = Game.towerSel !== null + if (!this.visible) return + if (mXr > this.x && mYr > this.y && mXr < this.x + this.w && mYr < this.y + this.h) { + this.hovered = true + } else if (this.hovered) { + this.hovered = false + } + } + + this.addElement(btn) + } + + draw () { + if (Game.towerSel) { + let ts = Game.towerSel + + ctx.fillStyle = 'rgba(0, 0, 0, 0.45)' + ctx.fillRect(this.x, this.y, this.w, this.h) + + ctx.fillStyle = '#fff' + ctx.font = '25px Helvetica' + ctx.fillText(ts.name + ' Tower', 5, this.y + 25) + + ctx.font = '15px Helvetica' + ctx.fillText(ts.description, 5, this.y + 42) + + ctx.fillText('Range: ' + ts.range + ' tiles', 5, this.y + 70) + ctx.fillText('Damage: ' + ts.damage + ' HP', 5, this.y + 85) + ctx.fillText('Fire Rate: ' + ts.rate, 5, this.y + 100) + ctx.fillText('Kills: ' + ts.killcount, 5, this.y + 115) + ctx.fillText('Fired ' + ts.fires + ' times', 5, this.y + 130) + } + this.elDraw() + } + } + + function clickBtn (cmp) { + let click = false + let compList = cmp != null && cmp instanceof Component ? cmp.elements : null + if (cmp == null && compList == null) { + compList = Components + } + + for (let i in compList) { + let btn = compList[i] + + if (!(btn instanceof ButtonComponent)) { + // Loop through sub-components of components + if (btn.elements.length) { + click = clickBtn(btn) + } else { + continue + } + } else { + // Click the button if its in bounds, visible and not disabled + if (btn.disabled || !btn.visible) continue + if (mXr > btn.x && mYr > btn.y && mXr < btn.x + btn.w && mYr < btn.y + btn.h && btn) { + btn.fn() + click = true + } + } + } + return click + } + + function updateComponents (cmp) { + // Determine which object of components to update + let compList = cmp != null && cmp instanceof Component ? cmp.elements : null + if (cmp == null && compList == null) { + compList = Components + } + + for (let i in compList) { + let component = compList[i] + component.update() + + if (component.elements.length) { + updateComponents(component) } } } - // Use this function to spawn enemies depending on round + // Total enemy spawn count is used to determine that the round is over + // Local (in-function) determines how many there are left to spawn as ordered by the function call + function addEnemies (enemies, type, specs) { + let path = Game.map.pathgen[0] + let enemy = Enemies[type] + + // Copy the enemy and add x and y coordinates + let enemyCopy = Object.assign({ + x: path.x, + y: path.y + }, enemy) + + // Modify the enemy according to wave settings + if (specs.healthIncrease) { + enemyCopy.health += specs.healthIncrease + } + + if (specs.speedIncrease) { + enemyCopy.speed += specs.speedIncrease + } + + enemyCopy.dmg = enemyCopy.health + + // Insert them into the spawn queue + for (let i = 0; i < enemies; i++) { + let spawnTime = enemyCopy.frequency * i + (specs.multiply ? (specs.multiply * (enemies * enemyCopy.frequency)) : 0) + if (Game.debug) { + console.log('added %s to spawn at %d', type, spawnTime) + } + + Game.enemySpawnList.push(Object.assign({ + time: spawnTime + }, enemyCopy)) + } + } + function nextWave () { Game.wave++ - if (Game.wave < 5) { - addEnemies(10 + Game.wave, Enemies.basic) - } else { - addEnemies(10 + Game.wave, Enemies.speedy) + for (let i in Game.map.waves) { + let wv = Game.map.waves[i] + let eSpawn = false + if (wv.type === 'once-every' && Game.wave % wv.every === 0) { + eSpawn = true + } else if (wv.type === 'once' && Game.wave === wv.wave) { + eSpawn = true + } else if (wv.type === 'recurring' && Game.wave >= wv.waveLow && (wv.waveHigh ? Game.wave < wv.waveHigh : true)) { + eSpawn = true + } + + if (!eSpawn) continue + for (let i in wv.enemies) { + let e = wv.enemies[i] + let eCount = e.count || 5 + let eHealthIncl = 0 + let multiply = wv.oneAfterAnother != null ? wv.oneAfterAnother : false + + if (e.inclCount === true) { + eCount += Game.wave + } + + if (e.baseHealth) { + eHealthIncl = e.baseHealth + } + + if (e.inclHealth === true) { + eHealthIncl = Game.wave * 5 + if (eHealthIncl > 500) { + eHealthIncl = 500 + } + } + + addEnemies(eCount, e.type, { + healthIncrease: eHealthIncl, + multiply: multiply ? parseInt(i) : false + }) + } } - - if (Game.wave > 10) { - addEnemies(Game.wave - 5, Enemies.tough) - } - - if (Game.wave % 5 === 0) { - addEnemies(Game.wave / 5, Enemies.tough) - } - } - - // Use this function to modify the enemies spawned each round - function waveEnemyModifer (enemy, round) { - // Reduce the time between enemy spawns - let fr = enemy.frequency - 2 * round - if (fr < 100) { - fr = 100 - } - - enemy.frequency = fr - - // Increase enemy health - enemy.health += round * 5 - - return enemy } function getTileIn (map, x, y) { @@ -423,6 +772,7 @@ window.onload = function () { if (Game.state !== -1 && Game.health <= 0) { Game.health = 0 Game.state = -1 + Game.towerSel = null } if (Game.state === 2 && gst === 1) { @@ -454,8 +804,8 @@ window.onload = function () { } if (enemy.velocity.dist > 0.1) { - enemy.x += (enemy.velocity.x * 0.01) * enemy.speed * Game.pace - enemy.y += (enemy.velocity.y * 0.01) * enemy.speed * Game.pace + enemy.x += (enemy.velocity.x * 0.01) * enemy.speed + enemy.y += (enemy.velocity.y * 0.01) * enemy.speed } else { if (Game.map.pathgen[enemy.node + 1]) { enemy.node += 1 @@ -469,7 +819,7 @@ window.onload = function () { } } - if (Game.state === 1 && Game.enemies.length === 0 && Game.enemySpawn === 0) { + if (Game.state === 1 && Game.enemies.length === 0 && Game.enemySpawnList.length === 0) { updateGameState(2) } } @@ -497,14 +847,18 @@ window.onload = function () { target = enemiesProxima[enemiesProxima.length - 1] } + tower.fires++ + Game.particles.push({ x: tower.x, y: tower.y, + tower: {x: tower.x, y: tower.y}, velX: (target.x - tower.x) / target.dist * 1.24, velY: (target.y - tower.y) / target.dist * 1.24, dmg: tower.damage, speed: tower.speed, - life: 100 + type: tower.bullet || 1, + life: 30 }) } @@ -541,11 +895,23 @@ window.onload = function () { if (parti.x >= enemy.x - 0.25 && parti.y >= enemy.y - 0.25 && parti.x <= enemy.x + 0.5 && parti.y <= enemy.y + 0.5) { // damage enemy - enemy.dmg -= parti.dmg + if (parti.type === 1) { + enemy.dmg -= parti.dmg - if (enemy.dmg <= 0) { - Game.enemies.splice(j, 1) - Game.money += 10 + if (enemy.dmg <= 0) { + Game.enemies.splice(j, 1) + Game.money += 10 + + let tower = getTowerAt(parti.tower.x, parti.tower.y) + if (tower) { + tower.killcount++ + } + } + } else if (parti.type === 2) { + enemy.speed -= parti.dmg + if (enemy.speed < 2) { + enemy.speed = 2 + } } // remove particle @@ -555,31 +921,37 @@ window.onload = function () { } } - // Total enemy spawn count is used to determine that the round is over - // Local (in-function) determines how many there are left to spawn as ordered by the function call - function addEnemies (cnt, type) { - Game.enemySpawn += cnt // Total amount of enemies to spawn - let enemies = cnt // Local amount of enemies to spawn + // Render text telling the amount of money received when selling a tower + // Disappears after 30 game ticks + function tickSellText () { + for (let i in Game.selltext) { + let txt = Game.selltext[i] + txt.tick++ + txt.tick %= 30 + if (txt.tick === 0) { + Game.selltext.splice(i, 1) + continue + } - let path = Game.map.pathgen[0] - // Copy the enemy and add x and y coordinates - let enemyCopy = Object.assign({ - x: path.x, - y: path.y - }, type) + txt.y -= 0.05 + } + } - // Modify the enemy according to wave settings - enemyCopy = waveEnemyModifer(enemyCopy, Game.wave) - enemyCopy.dmg = enemyCopy.health + function selectTower (x, y) { + let tower = getTowerAt(x, y) + Game.towerSel = tower + } - // Copy the enemy at an interval and spawn it - let ect = setInterval(() => { - if (enemies === 0) return clearInterval(ect) - Game.enemySpawn-- // Reduce total spawn count - enemies-- // Reduce local spawn count - - Game.enemies.push(Object.assign({}, enemyCopy)) - }, enemyCopy.frequency) + function spawnQueue () { + if (Game.enemySpawnList.length) { + for (let i in Game.enemySpawnList) { + let ef = Game.enemySpawnList[i] + if (ef.time < Game.waveTimer) { + Game.enemies.push(ef) + Game.enemySpawnList.splice(i, 1) + } + } + } } function getTowerAt (x, y) { @@ -600,9 +972,9 @@ window.onload = function () { // Prevent towers from being placed right next to each-other let can = true - for (let i in Game.towers) { + for (let j in Game.towers) { if (can === false) break - let tower = Game.towers[i] + let tower = Game.towers[j] // tower placement restriction visualization for (let i = 0; i < 4; i++) { @@ -628,62 +1000,96 @@ window.onload = function () { } function placeTower (tower, x, y) { - if (tower.cost > Game.money) return // no money + if (tower.cost > Game.money && !Game.debug) return // no money if (!canPlaceTowerAt(x, y)) return - Game.money -= tower.cost + if (!Game.debug) { + Game.money -= tower.cost + } + Game.towers.push(Object.assign({ x: x, y: y, tick: tower.rate, - setting: 1 + setting: 1, + fires: 0, + killcount: 0 }, tower)) } + function sellTower (x, y) { + let tower = getTowerAt(x, y) + if (tower) { + let amount = tower.cost * Game.sellRatio + Game.money += amount + Game.selltext.push({ + x: x, + y: y, + amount: amount, + tick: 0 + }) + + if (Game.towerSel && Game.towerSel.x === x && Game.towerSel.y === y) { + Game.towerSel = null + } + + return Game.towers.splice(Game.towers.indexOf(tower), 1) + } else { + return null + } + } + function update (dt) { + // Update FPS count for drawing (Don't render a new number every frame, it changes too fast) fpsCount++ fpsCount %= 20 if (fpsCount === 0) { fpsDraw = fps } - tickTowers() - updateEnemyMovements() - tickParticles() - - for (let i in Components) { - let btn = Components[i] - if (btn instanceof ButtonComponent) { - if (mXr > btn.x && mYr > btn.y && mXr < btn.x + btn.w && mYr < btn.y + btn.h) { - btn.hovered = true - } else if (btn.hovered) { - btn.hovered = false - } - - if (btn instanceof TowerButton) { - btn.disabled = btn.towerObj.cost > Game.money - btn.active = Game.tower === i - } - } + // Only tick towers when the game is in the play state + if (Game.state === 1) { + tickTowers() } + // Move enemies + updateEnemyMovements() + + // Move bullets + tickParticles() + + // Move sell texts + tickSellText() + + // Update all components (eg buttons) + updateComponents() + + // Set the state updateGameState() + + // Increment game clock if (Game.state === 1) { Game.waveTimer++ } + + spawnQueue() } + let lastRenderTime = Date.now() function render () { let mt = Maps.tile - ctx.clearRect(0, 0, canvas.width, canvas.height) + ctx.fillStyle = '#0fa' + ctx.fillRect(0, 0, canvas.width, canvas.height) + // Draw the map for (let i in Game.map.tiles) { let tile = Game.map.tiles[i] let index = parseInt(i) let y = Math.floor(index / Maps.width) let x = Math.floor(index % Maps.height) - + let draw_tile = true + if (tile === 1) { ctx.fillStyle = '#fdd' } else if (tile === 2) { @@ -691,46 +1097,43 @@ window.onload = function () { } else if (tile === 3) { ctx.fillStyle = '#f3a' } else { - ctx.fillStyle = '#0fa' + draw_tile = false } - ctx.fillRect(x * mt, y * mt, mt, mt) + if (draw_tile) { + ctx.fillRect(x * mt, y * mt, mt, mt) + } + + // Draw obstructed tiles + if (Game.state === 2 && tile === 0 && !canPlaceTowerAt(x, y)) { + ctx.fillStyle = '#738c5d' + ctx.fillRect(x * mt, y * mt, mt, mt) + } } -/* - for (let i in Game.map.pathgen) { - let node = Game.map.pathgen[i] - ctx.fillStyle = '#00f' - ctx.fillRect((node.x * mt) + mt / 3, (node.y * mt) + mt / 3, 8, 8) + + // Show the enemy movement path + if (Game.debug) { + for (let i in Game.map.pathgen) { + let node = Game.map.pathgen[i] + ctx.fillStyle = '#00f' + ctx.fillRect((node.x * mt) + mt / 3, (node.y * mt) + mt / 3, 8, 8) + } } -*/ + + // Draw towers for (let i in Game.towers) { let tower = Game.towers[i] ctx.fillStyle = tower.icon ctx.fillRect(tower.x * mt + 2, tower.y * mt + 2, 28, 28) - if (Game.state === 2 && Game.tower) { - // tower placement restriction visualization - for (let i = 0; i < 4; i++) { - let ax = tower.x - let ay = tower.y - if (i == 0) { - ax -= 1 - } else if (i == 1) { - ax += 1 - } else if (i == 2) { - ay -= 1 - } else if (i == 3) { - ay += 1 - } - - if (ax < 0 || ay < 0 || ay > Maps.height || ax > Maps.width) continue - if (getTileIn(Game.map.tiles, ax, ay) !== 0) continue - ctx.fillStyle = 'rgba(255, 0, 0, 0.45)' - ctx.fillRect(ax * mt, ay * mt, mt, mt) - } + if (Game.debug) { + ctx.fillStyle = '#f11' + ctx.font = '10px Helvetica' + ctx.fillText(tower.tick, tower.x * mt + 10, tower.y * mt + 25) } } + // Draw enemies for (let i in Game.enemies) { let enemy = Game.enemies[i] let rx = (enemy.x * mt) + mt / 8 @@ -746,33 +1149,61 @@ window.onload = function () { ctx.fillStyle = '#0f0' ctx.fillRect(hx, hy, (16 + 12) * enemy.dmg / enemy.health, 5) + + if (Game.debug) { + ctx.fillStyle = '#511' + ctx.font = '10px Helvetica' + ctx.fillText(enemy.dmg, hx + 10, hy + 25) + } } + // Draw bullets for (let i in Game.particles) { let tower = Game.particles[i] ctx.fillStyle = '#f33' ctx.fillRect(tower.x * mt + mt / 16, tower.y * mt + mt / 16, 8, 8) } - // tower placement - if (Game.state === 2 && Game.tower && mX < Maps.width && mY < Maps.height) { - // tower range visualization - let towerData = Towers[Game.tower] - if (towerData.cost <= Game.money && canPlaceTowerAt (mX, mY)) { - let towerData = Towers[Game.tower] - ctx.strokeStyle = '#ddd' - ctx.fillStyle = 'rgba(200, 200, 200, 0.25)' - ctx.beginPath() - ctx.arc(mX * mt + mt / 2, mY * mt + mt / 2, towerData.range * mt, 0, 2 * Math.PI) - ctx.stroke() - ctx.fill() - ctx.closePath() - } + // Tower range visualization + let towerData = Towers[Game.tower] + let vX = null + let vY = null + + // Render the currently selected tower's range if present + if (Game.towerSel) { + towerData = Game.towerSel + vX = towerData.x + vY = towerData.y + } else if (towerData != null && towerData.cost <= Game.money && canPlaceTowerAt(mX, mY) && + mX < Maps.width && mY < Maps.height && Game.state === 2) { + vX = mX + vY = mY } + // Render range + if (vX != null && vY != null && towerData) { + ctx.strokeStyle = '#ddd' + ctx.fillStyle = 'rgba(200, 200, 200, 0.25)' + ctx.beginPath() + ctx.arc(vX * mt + mt / 2, vY * mt + mt / 2, towerData.range * mt, 0, 2 * Math.PI) + ctx.stroke() + ctx.fill() + ctx.closePath() + } + + // Render sell text + for (let i in Game.selltext) { + let txt = Game.selltext[i] + ctx.font = '12px Helvetica' + ctx.fillStyle = '#0a0' + ctx.fillText('+ $' + txt.amount, txt.x * mt, txt.y * mt) + } + + // Render sidebar background ctx.fillStyle = '#996633' ctx.fillRect(640, 0, 240, 640) + // Render sidebar text ctx.font = '20px Helvetica' ctx.fillStyle = '#fff' ctx.fillText('FPS: ' + fpsDraw.toFixed(2), 0, 20) @@ -780,21 +1211,36 @@ window.onload = function () { ctx.fillText('Health: ' + Game.health, 645, 45) ctx.fillText('Money: ' + Game.money, 645, 65) - for (let i in Components) { - let btn = Components[i] - btn.draw() - } - + // Game Over text if (Game.state === -1) { ctx.font = '80px Helvetica' ctx.fillStyle = '#f00' ctx.fillText('Game Over', 100, canvas.height / 2 - 80 / 2) } + // Draw mouse cursor if (mX < Maps.width && mY < Maps.height) { ctx.fillStyle = 'rgba(255, 0, 0, 0.24)' ctx.fillRect(mX * mt, mY * mt, mt, mt) } + + // Draw all components + for (let i in Components) { + let cmp = Components[i] + if (!(cmp) instanceof Component) continue + cmp.draw() + } + + if (Game.debug) { + ctx.fillStyle = '#f11' + ctx.font = '10px Helvetica' + ctx.fillText('enemy queue length ' + Game.enemySpawnList.length, 5, 580) + ctx.fillText('enemy count ' + Game.enemies.length, 5, 590) + ctx.fillText('tower count ' + Game.towers.length, 5, 600) + ctx.fillText('particle count ' + Game.particles.length, 5, 610) + ctx.fillText('render tick ms ' + (Date.now() - lastRenderTime), 5, 620) + lastRenderTime = Date.now() + } } let lastTime = Date.now() @@ -805,6 +1251,7 @@ window.onload = function () { update() render() + // Update FPS let cfps = 1000 / ((now = new Date) - lastTime) if (now != lastTime) { fps += (cfps - fps) / fpsRes @@ -817,25 +1264,55 @@ window.onload = function () { function initialize () { Game.map = Maps.generate() + // Next wave button Components.wave = new ButtonComponent('Next Wave', '#fff', '#11f', 650, 570, 200, 60, () => { updateGameState(1) }) + // Tower information box + Components.info = new InfoDialog() + + // Add buy buttons to every tower let index = 0 for (let i in Towers) { Components[i] = new TowerButton(i, index) index++ } + // Tooltip + Components.tooltip = new Tooltip() + for (let i in Towers) { + let cmp = Components[i] + if (!cmp) continue + Components.tooltip.addComponent(cmp, cmp.towerObj.description) + } + + // Start the game gameLoop() } canvas.addEventListener('click', (e) => { - if (Game.state === 2 && mX < Maps.width && mY < Maps.height && Game.tower) { + if (clickBtn()) return + if (mX < Maps.width && mY < Maps.height) { + // Select a tower if present + if (getTowerAt(mX, mY)) { + return selectTower(mX, mY) + } else if (Game.towerSel) { + Game.towerSel = null + return + } + + // Place a tower + if (!Game.tower || Game.state !== 2) return placeTower(Towers[Game.tower], mX, mY) } - - clickBtn() + }) + + canvas.addEventListener('contextmenu', (e) => { + if (Game.state === 2 && mX < Maps.width && mY < Maps.height && + sellTower(mX, mY)) { + e.preventDefault() + } }) canvas.addEventListener('mousemove', (e) => {