window.onload = function () { /* My programming practice notes: * every major feature as part of updating the game (such as towers, enemies) are handeled in a separate 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') let ctx = canvas.getContext('2d') let fps = 0 let fpsDraw = 0 let fpsCount = 0 // mouse let mX = 0 let mY = 0 let mXr = 0 let mYr = 0 let Game = { state: 2, enemies: [], enemySpawnList: [], towers: [], particles: [], selltext: [], map: null, health: 100, money: 100, wave: 0, waveTimer: 0, tower: 'simple', towerSel: null, debug: false, sellRatio: .8 } /** speed - movement speed multiplier - higher: faster node - always 1 health - health of the enemy reward - money earned when killed frequency - milliseconds to spawn in icon - currently color of the enemy */ let Enemies = { basic: { speed: 10, node: 1, health: 50, reward: 10, frequency: 40, icon: '#f00' }, speedy: { speed: 20, node: 1, health: 60, reward: 15, frequency: 35, icon: '#f11' }, tough: { speed: 5, node: 1, health: 80, reward: 20, frequency: 40, icon: '#f40' } } let Towers = { simple: { range: 5, // range in tiles damage: 15, // damage to deal to enemies when hit rate: 20, // rate of fire, higher - slower name: 'Simple', // name of the tower description: 'Medium rate and damage', speed: 30, // bullet speed, higher - faster cost: 50, // cost to place 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 but low damage', speed: 30, cost: 250, icon: '#303', bullet: 1 }, sticky: { range: 3, damage: 10, rate: 30, name: 'Sticky', description: 'Slow down enemies by damage', speed: 50, cost: 500, 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 } } let Maps = { width: 20, // Width of the map height: 20, // Height of the map tile: 32, // Tile size in pixels (each coordinate is multiplied by this number in rendering) first: { // map rendering data tiles: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], // enemy follow path pathgen: [ {x: 1, y: 2, end: false}, {x: 14, y: 2, end: false}, {x: 14, y: 6, end: false}, {x: 8, y: 6, end: false}, {x: 8, y: 10, end: false}, {x: 17, y: 10, end: false}, {x: 17, y: 17, end: false}, {x: 5, y: 17, end: false}, {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 }] } ] } } let Components = {} class Component { constructor (x, y) { this.visible = true this.elements = [] this.x = x this.y = y } 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 { constructor (text, textColor, color, x, y, w, h, fn) { super(x, y) this.w = w this.h = h this.fn = () => { fn.apply(this, []) } this.text = text this.textColor = textColor 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) ctx.fillStyle = this.textColor let txtMeasure = ctx.measureText(this.text) let tx = this.x + (this.w / 2 - txtMeasure.width / 2) let ty = this.y + (this.h / 2) * 1.2 ctx.fillText(this.text, tx, ty) if (this.disabled) { 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.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) { this.disabled = typeof disabled === 'boolean' ? disabled : !this.disabled } } class TowerButton extends ButtonComponent { constructor (tower, i) { super() this.active = false this.w = 50 this.h = 50 this.x = (Maps.width * Maps.tile) + (i % 4) * (this.w + 4) + 4 this.y = 80 + Math.floor(i / 4) * (this.h + 4) this.tower = tower this.towerObj = Towers[this.tower] this.costText = '$' + this.towerObj.cost this.text = this.towerObj.name this.textColor = '#fff' this.color = '#995522' this.fn = this.select } 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) } ctx.font = '14px Helvetica' ctx.fillStyle = this.color ctx.fillRect(this.x, this.y, this.w, this.h) ctx.fillStyle = this.textColor let txtMeasure = ctx.measureText(this.text) let tx = this.x + (this.w / 2 - txtMeasure.width / 2) let ty = this.y + (this.h / 2) * 1 ctx.fillText(this.text, tx, ty) ctx.font = '10px Helvetica' if (Game.money >= this.towerObj.cost) { ctx.fillStyle = '#0f0' } else { ctx.fillStyle = '#f11' } txtMeasure = ctx.measureText(this.costText) tx = this.x + (this.w / 2 - txtMeasure.width / 2) ty = this.y + (this.h / 2) * 1.6 ctx.fillText(this.costText, tx, ty) if (this.disabled) { 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.15)' ctx.fillRect(this.x, this.y, this.w, this.h) } this.elDraw() } } 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) } } } // 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++ 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 }) } } } function getTileIn (map, x, y) { let tile = x + y * Maps.width return map[tile] } function updateGameState (gst) { if (Game.state !== -1 && Game.health <= 0) { Game.health = 0 Game.state = -1 Game.towerSel = null } if (Game.state === 2 && gst === 1) { nextWave() } if (gst === 2 && Game.state === 1) { Game.waveTimer = 0 } if (gst != null) { Game.state = gst } Components.wave.disabled = (Game.state !== 2) } function updateEnemyMovements () { for (let i in Game.enemies) { let enemy = Game.enemies[i] let enemyTrackTarget = Game.map.pathgen[enemy.node] if (enemyTrackTarget) { let tx = enemyTrackTarget.x let ty = enemyTrackTarget.y let vexLen = Math.sqrt(Math.pow(tx - enemy.x, 2) + Math.pow(ty - enemy.y, 2)) let velX = (tx - enemy.x) / Math.abs(vexLen) let velY = (ty - enemy.y) / Math.abs(vexLen) enemy.velocity = {x: velX, y: velY, dist: Math.abs(vexLen)} } if (enemy.velocity.dist > 0.1) { 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 } else if (enemyTrackTarget.end === true) { Game.enemies.splice(i, 1) Game.health -= Math.floor(enemy.dmg / 2) if (Game.health < 0) { Game.health = 0 } } } } if (Game.state === 1 && Game.enemies.length === 0 && Game.enemySpawnList.length === 0) { updateGameState(2) } } function towerFire (tower) { let enemiesProxima = [] let target = null for (let i in Game.enemies) { let enemy = Game.enemies[i] let proxi = Math.abs(Math.sqrt(Math.pow(enemy.x - tower.x, 2) + Math.pow(enemy.y - tower.y, 2))) if (proxi > tower.range) continue enemiesProxima.push(Object.assign({dist: proxi}, enemy)) } if (!enemiesProxima.length) return enemiesProxima.sort((a, b) => { return a.dist - b.dist }) if (tower.setting === 1) { target = enemiesProxima[0] } else { 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, type: tower.bullet || 1, life: 30 }) } function tickTowers () { for (let i in Game.towers) { let tower = Game.towers[i] // Tick towers tower.tick++ tower.tick %= tower.rate // fire if (tower.tick === 0) { towerFire(tower) } } } function tickParticles () { for (let i in Game.particles) { let parti = Game.particles[i] parti.x += parti.velX * 0.01 * parti.speed parti.y += parti.velY * 0.01 * parti.speed parti.life-- if (parti.life <= 0) { Game.particles.splice(i, 1) continue } for (let j in Game.enemies) { let enemy = Game.enemies[j] 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 if (parti.type === 1) { enemy.dmg -= parti.dmg 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 Game.particles.splice(i, 1) } } } } // 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 } txt.y -= 0.05 } } function selectTower (x, y) { let tower = getTowerAt(x, y) Game.towerSel = tower } 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) { for (let i in Game.towers) { let tower = Game.towers[i] if (tower.x === x && tower.y === y) return tower } return null } function canPlaceTowerAt (x, y) { let tileAt = getTileIn(Game.map.tiles, x, y) if (tileAt !== 0) return false // Do not overlap towers if (getTowerAt(x, y) !== null) return false // Prevent towers from being placed right next to each-other let can = true for (let j in Game.towers) { if (can === false) break let tower = Game.towers[j] // tower placement restriction visualization for (let i = 0; i < 4; i++) { if (can === false) break 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 (ax === x && ay === y) can = false } } return can } function placeTower (tower, x, y) { if (tower.cost > Game.money && !Game.debug) return // no money if (!canPlaceTowerAt(x, y)) return if (!Game.debug) { Game.money -= tower.cost } Game.towers.push(Object.assign({ x: x, y: y, tick: tower.rate, 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 } // 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.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) { ctx.fillStyle = '#aaf' } else if (tile === 3) { ctx.fillStyle = '#f3a' } else { draw_tile = false } 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) } } // 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.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 margin = .25 //A ratio of the width of a tile. .25 margins with 32 px tiles leave a 8 px margin on all sides, with the body being 16px x 16px let enemy = Game.enemies[i] let rx = (enemy.x + margin) * mt let ry = (enemy.y + margin) * mt let w = mt * (1 - margin * 2) ctx.fillStyle = enemy.icon ctx.fillRect(rx, ry, w, w) // health bars let hx = rx - 6 let hy = ry - 12 ctx.fillStyle = '#f00' ctx.fillRect(hx, hy, 16 + 12, 5) 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 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) ctx.fillText('Wave: ' + Game.wave, 645, 25) ctx.fillText('Health: ' + Game.health, 645, 45) ctx.fillText('Money: ' + Game.money, 645, 65) // 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() let now let fpsRes = 50 function gameLoop () { update() render() // Update FPS let cfps = 1000 / ((now = new Date) - lastTime) if (now != lastTime) { fps += (cfps - fps) / fpsRes lastTime = now } requestAnimationFrame(gameLoop) } function initialize () { Game.map = Maps.first // 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 (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) } }) canvas.addEventListener('contextmenu', (e) => { if (Game.state === 2 && mX < Maps.width && mY < Maps.height && sellTower(mX, mY)) { e.preventDefault() } }) canvas.addEventListener('mousemove', (e) => { if (e.changedTouches) { let touch = e.changedTouches[0] if (touch) { e.pageX = touch.pageX e.pageY = touch.pageY } } if (e.pageX || e.pageY) { mXr = e.pageX mYr = e.pageY } else { mXr = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft mYr = e.clientY + document.body.scrollTop + document.documentElement.scrollTop } mXr -= canvas.offsetLeft mYr -= canvas.offsetTop mX = Math.floor(mXr / Maps.tile) mY = Math.floor(mYr / Maps.tile) }) initialize() }