${this.name} + +
`) + .addHook(() => this.text = $(`#${textID}`)) + .addHook(() => this.input = $(`#${inputID}`)) + .addHook(() => this.input.click(() => { + const deactivateAllInputs = () => { + $('.hotkeyInput').removeClass('hotkeyInputActive'); + $('.hotkeyInput').off('keydown'); + $('.hotkeyInput').off('blur'); + }; + deactivateAllInputs(); + + // Start listening for a key, and disable if they click outside of the input + this.input.focus(); + this.input.addClass('hotkeyInputActive'); + this.input.keydown((e) => { + this.value = keyManager.getKeyCode(e); + this.__refreshUI(); + onChangeCallback(); + }); + this.input.blur(() => deactivateAllInputs()); + })) + .addHook(() => this.__refreshUI()); + } +} + +class HotkeyGroup { + // Parameter containsGroups determines whether or not this HotkeyGroup contains only HotkeyGroups + // A value of true means that it does, and a value of false means it contains only HotkeySettings + constructor(name, containsGroups, description = '', id = null) { + this.name = name; + this.id = id ?? name; + this.containsGroups = containsGroups; + this.description = description; + + this.children = {}; + } + + addChild(c, insertAtBeginning) { + assert(!(c.id in this.children)); + if (this.containsGroups) { + assert(c instanceof HotkeyGroup); + } else { + assert(c instanceof HotkeySetting); + } + + if (insertAtBeginning) { + this.children = { [c.id]: c, ...this.children }; + } else { + this.children[c.id] = c; + } + return this; + } + + getHotkeyValue(id) { + assert(!this.containsGroups); + assert(id in this.children); + return this.children[id].value; + } + + forEach(c) { + assert(!this.containsGroup); + let i = 0; + for (let id in this.children) { + c(this.children[id], i++); + } + } + + // Returns the HotkeySetting or HotkeyGroup corresponding to the traversal down the tree defined by + // the provided list of group and setting IDs + __getHotkeyGroup(ids) { + assert(ids.length > 0); + assert(ids[0] in this.children); + + if (ids.length == 1) { + return this.children[ids[0]]; + } else { + return this.children[ids[0]].getChild(ids.slice(1)); + } + } + + __loadFromObject(obj, prefix = '') { + Object.values(this.children).forEach((c) => c.__loadFromObject(obj, `${prefix}${this.id}`)); + } + + __saveToObject(obj, prefix = '') { + Object.values(this.children).forEach((c) => c.__saveToObject(obj, `${prefix}${this.id}`)); + } + + __reset(prefix = '') { + Object.values(this.children).forEach((c) => c.__reset(`${prefix}${this.id}`)); + } + + __exportHotkeys() { + let hotkeyString = localStorage.getItem(LOCAL_STORAGE_KEY_NAME_HOTKEYS); + const blob = new Blob([hotkeyString], { encoding: 'UTF-8', type: 'text/plain;charset=utf-8' }); + saveAs(blob, 'lwg_hotkeys.file'); + } + + __importHotkeys(hotkeysClass) { + fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.click(); + + fileInput.onchange = () => { + const file = fileInput.files[0]; + if (file) { + const reader = new FileReader(); + reader.addEventListener( + 'load', + () => { + let hotkeysFile = reader.result; + localStorage.setItem(LOCAL_STORAGE_KEY_NAME_HOTKEYS, hotkeysFile); + hotkeysClass.init(); + }, + false, + ); + + reader.readAsText(file); + } + }; + } + + __generateUI(onChangeCallback, depth = 0) { + assert(depth <= 2); + const headingClass = { + 0: 'invis', + 1: 'hotkeyHeadline', + 2: 'hotkeySectionTitle', + }; + + // Closure variable that keeps track of whether or not the children are visible + let isCollapsed = depth > 0; // Root element is not collapsed + + const headingID = uniqueID(); + const collapseButtonID = uniqueID(); + const collapsibleDivID = uniqueID(); + const builder = new HTMLBuilder() + .add(` +'); + builder.add(`${getFormattedTime()} `); + if (el) { + builder.add(network.getClanLink(el)); + } + builder.add(network.getPlayerLink(el ? el : { name: sender, authLevel: AUTH_LEVEL.GUEST }, false)); + builder.add(`: ${escapedMsg}`); + builder.add('
'); + + addToChatWindow(builder); + } +} + +function pauseGame() { + if (!network_game) { + game_paused = !game_paused; + interface_.addMessage(game_paused ? 'Game paused' : 'Game unpaused', 'yellow', imgs.attentionmarkYellow); + soundManager.playSound(SOUND.POSITIVE); + worker.postMessage({ what: 'setPause', val: game_paused }); + } else { + network.send('request-pause'); + } +} + +function typeNameIsUnique(name) { + var found = false; + + _.each(lists.types, function(type) { + if (type.name == name) { + found = true; + } + }); + + return !found; +} + +function graphicNameIsUnique(name) { + var found = false; + + _.each(lists.imgs, function(type) { + if (type && type.name == name) { + found = true; + } + }); + + return !found; +} + +function getImageFunctionName(func) { + var returnVal = ''; + + _.each(lists.imgs, function(imgFunc, key) { + if (func == imgFunc) { + returnVal = key; + } + }); + + return returnVal; +} + +function arraysAreSame(a1, a2, values) { + if (a1 == a2) { + return true; + } + + if (!a2) { + a2 = []; + } + + if (!a1) { + a1 = []; + } + + if (Object.prototype.toString.call(a1) != Object.prototype.toString.call(a2)) { + return false; + } + + if (Object.prototype.toString.call(a1) === '[object Array]' && a1.length == a2.length) // both are array, compare elemnt by element + { + for (var j = 0; j < a1.length; j++) { + if (a1[j] != ((values && (typeof a2[j] == 'string') && values[a2[j]]) ? values[a2[j]] : a2[j])) { + return false; + } + } + + return true; + } + + return false; +} + +function objectsAreSame(o1, o2, values) { + var isSame = true; + + if (o1 == o2 || !o1 && !o2) { + return true; + } + + if ((o1 && !o2) || (!o1 && o2)) { + return false; + } + + _.each(o1, function(val, key) { + if (((values && (typeof val == 'string') && values[val]) ? values[val] : val) != ((values && (typeof o2[key] == 'string') && values[o2[key]]) ? values[o2[key]] : o2[key])) { + isSame = false; + } + }); + + _.each(o2, function(val, key) { + if (((values && (typeof val == 'string') && values[val]) ? values[val] : val) != ((values && (typeof o1[key] == 'string') && values[o1[key]]) ? values[o1[key]] : o1[key])) { + isSame = false; + } + }); + + return isSame; +} + +function getBuildingImgNameFromImgObject(o) { + var returnVal = null; + + _.each(building_imgs, function(val, key) { + if (o == val) { + returnVal = key; + } + }); + + return returnVal; +} + +function isObject(o) { + return typeof o === 'object' && o !== null; +} + +function copyObject(o) { + var o2 = {}; + _.each(o, function(val, key) { + o2[key] = val; + }); + return o2; +} + +function setReplaySpeed(index) { + replaySpeedIndex = Math.max(Math.min(index, game_speeds.length - 1), 0); + TICK_TIME = game_speeds[replaySpeedIndex].tick_time; + $('#replayShowSpeed').html(game_speeds[replaySpeedIndex].caption); + worker.postMessage({ what: 'setTickTime', tickTime: TICK_TIME }); +} + +function getRainTimeFromSeed(seed) { + var rng = new RNG(seed); + var rainTime = []; + var t_ = rng.getNr(-1, 15); + var lastPoint = t_; + var start = true; + + while (t_ < 300) { + t_ += start ? rng.getNr(1, 7) : rng.getNr(2, 16); + + if (start) { + rainTime.push({ start: lastPoint, end: t_ }); + } + + start = !start; + lastPoint = t_; + } + + return rainTime; +} + +function RNG(seed) { + this.m = 0x80000000; // 2**31; + this.a = 1103515245; + this.c = 12345; + this.state = seed ? seed : Math.floor(Math.random() * (this.m - 1)); +} + +RNG.prototype.getNr = function(min, max) { + this.state = (this.a * this.state + this.c) % this.m; + + var rangeSize = max - min; + var randomUnder1 = this.state / this.m; + return min + randomUnder1 * rangeSize; +}; + +const lcgVolume = LocalConfig.registerValue('lcgVolume', DEFAULT_VOLUME); +function littlechatgame(log_) { + var lcg_canvas = document.createElement('canvas'); + var lcg_context = lcg_canvas.getContext('2d'); + lcg_canvas.width = $('#lobbyChatTextArea').width() - 20; + lcg_canvas.height = 90; + + lcg = log_; + lcg.currentPhase = -1; + lcg.army = []; + lcg.startTime = Date.now() + 2000; + lcg.canvas = lcg_canvas; + lcg.ticksCounter = -1; + lcg.actualPhase = -1; + + game = new Game(); + game.loadMap({ + 'name': 'unnamed', + 'x': 30, + 'y': 10, + 'units': [], + 'buildings': [], + 'tiles': [], + 'defaultTiles': ['Ground n 6'], + 'unitData': + { + 'dragon': + { + 'imageScale': 0.7, + 'height': 1.5, + 'projectileSpeed': 12, + }, + + 'ballista': + { + 'imageScale': 0.8, + }, + + 'catapult': + { + 'projectileSpeed': 12, + 'imageScale': 0.8, + }, + + 'fireball': + { + 'projectileSpeed': 9, + // }, + + // "gyrocraft": + // { + // "imageScale": 0.1, + // "height": 1.5 + }, + }, + }, null, null, null, false, true); + + lcg_context.font = 'bold 32px LCDSOlid'; + drawText(lcg_context, log_.players[0], game.players[1].getColor(), 'bold 32px LCDSolid', 10, 30); + var w1 = lcg_context.measureText(log_.players[0]).width; + drawText(lcg_context, 'vs', 'white', 'bold 32px LCDSolid', 10 + w1 + 10, 30); + var w2 = lcg_context.measureText('vs').width; + drawText(lcg_context, log_.players[1], game.players[2].getColor(), 'bold 32px LCDSolid', 10 + w1 + w2 + 20, 30); + + soundManager.playSound(SOUND.BATTLE_FANFARE, lcgVolume.get()); + + // vision + for (var x = 0; x < 30; x++) { + for (var y = 0; y < 10; y++) { + PLAYING_PLAYER.team.mask[x][y] = 2; + } + } + + var players = [ + new Player('pl1', CONTROLLER.COMPUTER, 1), + new Player('pl2', CONTROLLER.COMPUTER, 2), + ]; + + var pos = 3; + + for (var i = 0; i < log_.armies[0].length; i++) { + lcg.army.push(new Unit({ + x: pos++, + y: 3.2, + type: log_.armies[0][i] == 'Gatlinggun' ? 'Gatling Gun'.toUnitType() : log_.armies[0][i].toUnitType(), + owner: players[0], + })); + } + + for (var i = log_.armies[1].length - 1; i >= 0; i--) { + lcg.army.push(new Unit({ + x: pos++, + y: 3.2, + type: log_.armies[1][i] == 'Gatlinggun' ? 'Gatling Gun'.toUnitType() : log_.armies[1][i].toUnitType(), + owner: players[1], + })); + } + + addToChatWindow(lcg_canvas); + + clearInterval(lcg_interval); + + lcg_interval = setInterval(function() { + var timeNow = Date.now(); + + if (game_state != GAME.LOBBY) { + clearInterval(lcg_interval); + return; + } + + if (lcg.startTime > timeNow) { + return; + } + + var age = timeNow - lcg.startTime; + var phaseAge = age % 1500; + var phaseStartTime = timeNow - phaseAge; + var phaseAgeInTicks = Math.floor(phaseAge / 50); + var phase = Math.floor(age / 1500); + ticksCounter = Math.floor(age / 1000 * 20); + percentageOfCurrentTickPassed = (age % 50) / 50; + var phaseType = lcg.actualPhase % 3; + tickDiff = 0; + + // if not die phase, set units that have been thrown to target pos, because it might not have happened correctly, if the screen was inactive during throwing + if (phaseType != 1) { + for (var i = 0; i < lcg.army.length; i++) { + if (lcg.army[i].setToPos) { + lcg.army[i].pos = lcg.army[i].setToPos; + lcg.army[i].lastTicksPosition = lcg.army[i].setToPos; + lcg.army[i].drawPos = lcg.army[i].setToPos; + delete lcg.army[i].setToPos; + } + } + } + + // update + if (phase != lcg.currentPhase) { + lcg.currentPhase = phase; + + for (var i = 0; i < lcg.army.length; i++) { + lcg.army[i].order = lists.types.stop; + lcg.army[i].lastAttackingTick = -999; + lcg.army[i].lastTicksPosition = lcg.army[i].pos; + } + + var arr = []; + while (arr.length == 0) { + lcg.actualPhase++; + arr = lcg.replay[lcg.actualPhase]; + + if (!arr) // battle over + { + if (lcg.hasBeenDrawn) { + clearInterval(lcg_interval); + lcg_context.font = 'bold 32px LCDSOlid'; + + if (lcg.winner == 0) { + drawText(lcg_context, 'draw', 'white', 'bold 32px LCDSolid', 200, 60); + } else { + drawText(lcg_context, lcg.players[lcg.winner - 1] + ' wins', game.players[lcg.winner].getColor(), 'bold 32px LCDSolid', 200, 60); + } + + return; + } else { + arr = [0]; + } + } + } + + phaseType = lcg.actualPhase % 3; + + if (phaseType == 0) // attack phase + { + for (var i = 0; i < arr.length; i++) { + if (arr[i].indexOf && arr[i].indexOf('A') >= 0) // attack + { + var split = arr[i].split('A'); + + var u1 = game.getUnitById(split[0]); + var u2 = game.getUnitById(split[1]); + + u1.targetUnit = u2; + u1.order = lists.types.attack; + u1.lastAttackingTick = ticksCounter; + u1.tickOfLastWeaponFired = ticksCounter + u1.type.weaponDelay; + } else if (arr[i].indexOf && arr[i].indexOf('S') >= 0) // smash + { + var split = arr[i].split('S'); + + var u1 = game.getUnitById(split[0]); + var u2 = game.getUnitById(split[1]); + + u1.targetUnit = u2; + u1.order = lists.types.smash; + u1.lastAttackingTick = ticksCounter; + u1.tickOfLastWeaponFired = ticksCounter + u1.type.weaponDelay; + } else if (arr[i].indexOf && arr[i].indexOf('H') >= 0) // heal + { + var split = arr[i].split('H'); + + var u1 = game.getUnitById(split[0]); + var u2 = game.getUnitById(split[1]); + + u1.targetUnit = u2; + u1.order = lists.types.heal; + u1.lastAttackingTick = ticksCounter; + u1.tickOfLastWeaponFired = ticksCounter + u1.type.weaponDelay; + } else if (arr[i].indexOf && arr[i].indexOf('F') >= 0) // fireball + { + var split = arr[i].split('F'); + + var u1 = game.getUnitById(split[0]); + var u2 = game.getUnitById(split[1]); + + u1.targetUnit = u2; + u1.order = lists.types.attack; + u1.lastAttackingTick = ticksCounter; + u1.tickOfLastWeaponFired = ticksCounter + u1.type.weaponDelay; + + new Flamestrike({ + from: u1, + to: new Field(parseInt(split[2]), u1.pos.py, true), + speed: 3, + noFinalBlow: true, + scale: 1, + }); + + soundManager.playSound(SOUND.FIREBALL, lcgVolume.get()); + } + } + } + + if (phaseType == 1) // die phase + { + for (var i = 0; i < arr.length; i++) { + if (arr[i].indexOf && arr[i].indexOf('X') >= 0) // get smashed + { + var u1 = game.getUnitById(arr[i].split('X')[0]); + + u1.isThrowedUntil = ticksCounter + 15; + u1.throwStart = ticksCounter; + u1.throwFrom = u1.pos.getCopy(); + u1.throwTo = new Field(parseInt(arr[i].split('X')[1]), u1.pos.py, true); + u1.lastTicksPosition = u1.throwTo.getCopy(); + u1.setToPos = u1.throwTo.getCopy(); + u1.targetPos_ = u1.throwTo.getCopy(); + u1.target = u1.throwTo.getCopy(); + } else if (arr[i].indexOf && arr[i].indexOf('Z') >= 0) // flash + { + soundManager.playSound(SOUND.WARP, lcgVolume.get()); + var u1 = game.getUnitById(arr[i].split('Z')[0]); + + u1.targetPos_ = new Field(parseInt(arr[i].split('Z')[1]), u1.pos.py, true); + u1.setToPos = u1.targetPos_.getCopy(); + u1.originPos_ = u1.pos.getCopy(); + u1.target = u1.targetPos_.getCopy(); + u1.moveStartTime_ = phaseStartTime; + u1.order = lists.types.TELEPORT; + } else // die + { + var u1 = game.getUnitById(arr[i]); + + u1.isAlive = false; + u1.throwStart = ticksCounter; + u1.isThrowedUntil = ticksCounter + 25; + u1.throwFrom = u1.pos; + u1.throwTo = u1.pos; + + if (u1.type.deathSound) { + soundManager.playSound(u1.type.deathSound, lcgVolume.get()); + } + } + } + } + + if (phaseType == 2) // move phase + { + for (var i = 0; i < arr.length; i++) { + if (arr[i].indexOf && arr[i].indexOf('M') >= 0) { + var u1 = game.getUnitById(arr[i].split('M')[0]); + + u1.targetPos_ = new Field(arr[i].split('M')[1], u1.pos.py, true); + u1.originPos_ = u1.pos.getCopy(); + u1.target = u1.targetPos_; + u1.moveStartTime_ = phaseStartTime; + u1.order = lists.types.move; + } + } + } + } + + if (lcg.ticksCounter != ticksCounter) { + lcg.ticksCounter = ticksCounter; + tickDiff = 1; + + for (var i = 0; i < lcg.army.length; i++) { + var u = lcg.army[i]; + + if (phaseType == 0) // attack phase + { + if ((u.order == lists.types.attack || u.order == lists.types.smash) && phaseAgeInTicks < u.type.weaponCooldown - 5) { + u.hitCycle++; + u.lastAttackingTick = ticksCounter; + + if (u.tickOfLastWeaponFired == ticksCounter) { + if (u.type.attackEffect) // create projectile (if ranged) + { + startEffect(u.type.attackEffect, { + from: u, + to: u.targetUnit, + speed: u.type.projectileSpeed / 2, + }); + } + + if (u.type.meleeHitSound) { + soundManager.playSound(u.type.meleeHitSound, lcgVolume.get()); + } + + if (u.type.attackLaunchSound) { + soundManager.playSound(u.type.attackLaunchSound, lcgVolume.get()); + } + + if (u.targetUnit.type.painSound) { + soundManager.playSound(u.targetUnit.type.painSound, lcgVolume.get()); + } + } + } + + if (u.tickOfLastWeaponFired == ticksCounter && u.order == lists.types.heal) { + startEffect('heal', { + originPos: u.pos, + from: u.targetUnit, + }); + + soundManager.playSound(SOUND.HEAL, lcgVolume.get()); + } + } + + if (u.targetPos_ && u.pos.distanceTo2(u.targetPos_) > 0.03) { + if (u.order == lists.types.TELEPORT) { + u.lastTicksPosition = u.pos; + u.pos = u.originPos_.addNormalizedVector(u.targetPos_, (Math.min(timeNow - u.moveStartTime_, 1) / 1) * u.originPos_.distanceTo2(u.targetPos_)); + } else { + u.lastTicksPosition = u.pos; + u.pos = u.originPos_.addNormalizedVector(u.targetPos_, (Math.min(timeNow - u.moveStartTime_, 1500) / 1500) * u.originPos_.distanceTo2(u.targetPos_)); + } + } else { + u.lastTicksPosition = u.pos; + } + } + } + + lcg.canvas.style.width = '95%'; + lcg.canvas.width = lcg.canvas.width; + SCALE_FACTOR = 1.5; + FIELD_SIZE = 16 * SCALE_FACTOR; + c = lcg.canvas.getContext('2d'); + lcg.hasBeenDrawn = true; + + // draw + for (var i = 0; i < lcg.army.length; i++) { + lcg.army[i].updateDrawPosition(); + lcg.army[i].draw(); + } + + // draw effectz + var objs = game.objectsToDraw.slice(); + for (var i = 0; i < objs.length; i++) { + if (objs[i].isEffect) { + objs[i].draw(0, 0, 0, 0, lcgVolume.get()); + + if (objs[i].isExpired()) { + if (objs[i].to && objs[i].to.type && objs[i].to.type.painSound) { + soundManager.playSound(objs[i].to.type.painSound, lcgVolume.get()); + } + + if (objs[i].to && objs[i].to.type && objs[i].to.type.painSound2) { + soundManager.playSound(objs[i].to.type.painSound2, lcgVolume.get()); + } + + if (objs[i].constructor == LaunchedRock) { + soundManager.playSound(SOUND.CATA_IMPACT, lcgVolume.get()); + } + + game.objectsToDraw.erease(objs[i]); + } + } + } + + var ageFactor = Math.min(age / 1000, 1); + lcg_context.font = 'bold ' + (31 - ageFactor * 13) + 'px LCDSOlid'; + drawText(lcg_context, log_.players[0], game.players[1].getColor(), 'bold ' + (31 - ageFactor * 13) + 'px LCDSolid', 10, 30 - ageFactor * 9); + var w1 = lcg_context.measureText(log_.players[0]).width; + drawText(lcg_context, 'vs', 'white', 'bold ' + (31 - ageFactor * 13) + 'px LCDSolid', 10 + w1 + (10 - ageFactor * 4), 30 - ageFactor * 9); + var w2 = lcg_context.measureText('vs').width; + drawText(lcg_context, log_.players[1], game.players[2].getColor(), 'bold ' + (31 - ageFactor * 13) + 'px LCDSolid', 10 + w1 + w2 + (20 - ageFactor * 8), 30 - ageFactor * 9); + + c = originalC; + }, 10); +}; + +function escapeHtml(text) { + return text.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); +}; + +// Subsitute chat messages +function kappa(text, permissions) { + // replace ish + if ((new RegExp('\u5350|\u534D', 'g')).test(text)) { + return ''; + } + + permissions = permissions ? hex32ToBin(permissions) : null; + + // console.dir(permissions); + var em = emotes.concat(_emotes2); + + // ? + for (var i = 0; i < em.length; i++) { + if (!em[i].dbPos || (permissions && em[i].dbPos <= permissions.length && permissions.substr(permissions.length - em[i].dbPos, 1) == '1')) { + text = text.replace(new RegExp(em[i].text, 'g'), '#<<#<>#<' + i + '#<<#<>#'); + } + } + + // Own name highlighting + text = text.replace(new RegExp(networkPlayerName, 'gi'), '' + networkPlayerName + ''); + + // emotes + for (var i = 0; i < em.length; i++) { + // console.dir(em[i]); + if (!em[i].dbPos || (permissions && em[i].dbPos <= permissions.length && permissions.substr(permissions.length - em[i].dbPos, 1) == '1')) { + text = text.replace(new RegExp('#<<#<>#<' + i + '#<<#<>#', 'g'), ''); + } + } + + return text; +}; + +function soundFaceOut() { + if (soundManager.buildingClickSound[0] && soundManager.buildingClickSound[0].fadeOut) { + var newVolume = soundManager.buildingClickSound[0].sound.volume - soundManager.buildingClickSound[0].maxVolume / 11; + + if (newVolume <= 0) { + soundManager.buildingClickSound[0].sound.pause(); + soundManager.buildingClickSound[0].sound.volume = 0; + soundManager.buildingClickSound[0].sound.currentTime = 0; + soundManager.buildingClickSound.splice(0, 1); + } else { + soundManager.buildingClickSound[0].sound.volume = newVolume; + setTimeout(soundFaceOut, 50); + } + } +} + +function levelUp(splitMsg) { + var xp1 = getXPRequiredForLvl(splitMsg[1]); + var xp2 = getXPRequiredForLvl(parseInt(splitMsg[1]) + 1); + + const builder = new HTMLBuilder(); + + builder.add('' + a.name + '
' + msg + '
'); + $('#bingMessageWindow').fadeIn(1000); + + setTimeout(function() { + $('#bingMessageWindow').fadeOut(1000); + }, 4000); +}; + +function editPersonalText() { + var b = $('#profileTextButton'); + + if (b.html() == 'edit') { + $('#personalTextDiv').html(''); + b.html('save'); + } else { + network.send('update-profile-text<<$' + $('#personalTextTextArea')[0].value); + $('#personalTextDiv').html(escapeHtml($('#personalTextTextArea')[0].value).replace(/(?:\r\n|\r|\n)/g, 'Info
'); + infoBuilder.add('') + .addHook(() => $('#spectatorDropdown').change(() => this.refreshSpectatorTab($('#spectatorDropdown').val()))) + .appendInto('#spectatorDropdowns'); + + // Dropdown which allows the spectator to see a specific player's vision + const visionBuilder = new HTMLBuilder(); + visionBuilder.add('Vision
'); + visionBuilder.add('') + .addHook(() => $('#visionDropdown').change(() => this.refreshVision($('#visionDropdown').val()))) + .appendInto('#spectatorDropdowns'); + + const follow_hotkey = interface_.specVisionHotkeys.getHotkeyValue('toggle_follow_camera'); + keyManager.registerListener(follow_hotkey, 'GAME_VISION', () => { + const isSpectator = PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR; + if (this.visionSetting > 0) { + const player = this.players[this.visionSetting]; + if (player.cameraWidth == 0 || player.cameraHeight == 0) { + return; + } + this.followVision = isSpectator && !this.followVision; + } + }); + }; + Hotkeys.onHotkeysChanged('GAME_SPEC_INTERFACE', refreshInterface); + refreshInterface(); +}; + +Game.prototype.updateGlobalVars = function(o) { + this.globalVars = o ? o : {}; + this.setModded(this.globalVars.isModded ?? false); +}; + +Game.prototype.automine = function() { + if (!this.globalVars.automine) { + return; + } + + const workers = this.units.filter((u) => u.type == lists.types.worker && u.owner == PLAYING_PLAYER); + this.issueOrderToUnits(workers, lists.types.mine); +}; + +Game.prototype.setModded = function(value) { + this.globalVars.isModded = value; + if (!value) { + delete this.globalVars.maxSupply; + delete this.globalVars.startGold; + delete this.globalVars.mineDist; + } +}; + +Game.prototype.__getGlobalVar = function(key, min, max, defaultVal) { + const val = parseInt(this.globalVars?.[key]); + if (isNaN(val)) { + return defaultVal; + } + return Math.min(max, Math.max(min, val)); +}; + +Game.prototype.getMineDistance = function() { + return this.__getGlobalVar('mineDist', 0, 100, MINE_DIST); +}; + +Game.prototype.getMaxSupply = function() { + return this.__getGlobalVar('maxSupply', 0, 200, MAX_SUPPLY); +}; + +Game.prototype.getStartGold = function() { + return this.__getGlobalVar('startGold', 0, 999999, START_GOLD); +}; + +Game.prototype.getFieldFromPos = function(x, y, lowerMod) { + // if no coords passed, use current mouse pos as default + if (!x && !y) { + x = (keyManager.x + game.cameraX) / FIELD_SIZE; + y = (keyManager.y + game.cameraY) / FIELD_SIZE; + } + + var bestDist = 99999; + var bestField = null; + for (var i = MAX_CLIFF_LEVEL * CLIFF_HEIGHT; i > 0; i -= 0.1) { + var f = new Field(x, y + i, true); + var dist = Math.abs(i - (lowerMod ? this.getHMValue3X(f) : this.getHMValue3(f)) * CLIFF_HEIGHT); + if (dist < 0.2) { + return f; + } + + if (dist < bestDist) { + bestDist = dist; + bestField = f; + } + } + + if (bestField) { + return bestField; + } + + return new Field(x, y, true); +}; + +// höhe, auf der Einheiten gezeichnet werden +Game.prototype.getHMValue3X = function(pos) { + if (this.fields[pos.x] && this.fields[pos.x][pos.y]) { + var f = this.fields[pos.x][pos.y]; + + if (!f.rampEnds) { + return f.hm2 ? f.hm2 : 0; + } + + var dist = f.rampEnds.low.distanceTo2(f.rampEnds.high); + return f.hm2 + Math.min(Math.max((pos.distanceTo2(f.rampEnds.low) - 0.1) * 1.1, 0) / dist, 1); + } + + return 0; +}; + +// höhe, auf der Einheiten gezeichnet werden +Game.prototype.getHMValue3 = function(pos) { + if (this.fields[pos.x] && this.fields[pos.x][pos.y]) { + var f = this.fields[pos.x][pos.y]; + + if (!f.rampEnds) { + return f.hm4 ? f.hm4 : 0; + } + + var dist = f.rampEnds.low.distanceTo2(f.rampEnds.high); + return f.hm2 + Math.min(Math.max((pos.distanceTo2(f.rampEnds.low) - 0.1) * 1.1, 0) / dist, 1); + } + + return 0; +}; + +Game.prototype.getHMValue4 = function(x, y) { + return (this.fields[x] && this.fields[x][y]) ? this.fields[x][y].hm4 : 0; +}; + +Game.prototype.getHMValue2 = function(x, y) { + return (this.fields[x] && this.fields[x][y]) ? this.fields[x][y].hm2 : 0; +}; + +Game.prototype.getHMValue = function(x, y) { + return (this.fields[x] && this.fields[x][y]) ? this.fields[x][y].hm : 0; +}; + +Game.prototype.setHMValue = function(x, y, val, noHistory) { + if (this.fields[x] && this.fields[x][y]) { + if (game_state == GAME.EDITOR && noHistory != true) { + var heightMapChange = { + x: x, + y: y, + newHeight: val, + oldHeight: this.fields[x][y].hm, + }; + editor.clipboard.history.addObject(heightMapChange, 'ChangeHeight'); + } + this.fields[x][y].hm = val; + } +}; + +Game.prototype.makeCliffsArray = function() { + var hm = this.data.heightmap ? this.data.heightmap : ''; + var i = 0; + + for (var x = 1; x <= this.x; x++) { + for (var y = 1; y <= this.y; y++) { + if (hm.length > i) { + var val = hm.slice(i, i + 1); + + if (parseInt(val) == val) { + if (val > MAX_CLIFF_LEVEL) { + val = MAX_CLIFF_LEVEL; + } + + if (val < 0) { + val = 0; + } + + val = parseInt(val); + } + + this.fields[x][y].hm = val; + } else { + this.fields[x][y].hm = 0; + } + + i++; + } + } +}; + +// make cliffs (in an area from x1:y1 to x2:y2; default: whole map) +Game.prototype.makeCliffs = function(x1_, y1_, x2_, y2_) { + x1_ = Math.min(Math.max(x1_ ? x1_ : 1, 1), this.x); + y1_ = Math.min(Math.max(y1_ ? y1_ : 1, 1), this.y); + x2_ = Math.min(Math.max(x2_ ? x2_ : this.x, 1), this.x); + y2_ = Math.min(Math.max(y2_ ? y2_ : this.y, 1), this.y); + + // kill all existing cliff Tiles + for (var i = 0; i < this.blockingTiles.length; i++) { + var tile = this.blockingTiles[i]; + if (tile.type.isCliff && tile.x >= x1_ && tile.x <= x2_ && tile.y >= y1_ && tile.y <= y2_) { + this.blockingTiles[i].switchBlockingTotal(false); + this.blockingTiles.splice(i, 1); + i--; + } + } + + // higher level, if single low grounded cliff fields, cuz those look stupido + for (var x = x1_; x <= x2_; x++) { + for (var y = y1_; y <= y2_; y++) { + var h = this.fields[x][y].hm; + + if (parseInt(h) == h) { + var countHigherNBs = 0; + + for (var i = 0; i < 8; i++) { + var h2 = this.getHMValue(x + nbCoords[i].x, y + nbCoords[i].y); + + if (parseInt(h2) == h2 && h2 > h) { + countHigherNBs++; + } + } + + // if all but one (or all) nbs are higher, make this field, higher, too + if (countHigherNBs >= 6) { + this.fields[x][y].hm++; + } else { + for (var i = 0; i < reversePairs.length; i++) { + var h1_ = this.getHMValue(x + reversePairs[i][0].x, y + reversePairs[i][0].y); + var h2_ = this.getHMValue(x + reversePairs[i][1].x, y + reversePairs[i][1].y); + + if (parseInt(h1_) == h1_ && h1_ > h && parseInt(h2_) == h2_ && h2_ > h) { + this.fields[x][y].hm++; + i = reversePairs.length; + } + } + } + } + } + } + + for (var x = x1_; x <= x2_; x++) { + for (var y = y1_; y <= y2_; y++) { + var h = this.fields[x][y].hm; + + this.fields[x][y].hm2 = h; + this.fields[x][y].hm4 = h; + this.fields[x][y].rampEnds = null; + + // get height levels of all nbs + var nbs = []; + for (var i = 0; i < 8; i++) { + var h2 = this.getHMValue(x + nbCoords[i].x, y + nbCoords[i].y); + + if (parseInt(h2) != h2) { + h2 = h; + } + + nbs.push(h2 - h); + } + + // find according cliff + var cliffTilePlaced = false; + for (var i = 0; i < cliffTable.length; i++) { + var match = true; + var arr = cliffTable[i].arr; + for (var k = 0; k < 8; k++) { + if (!(arr[k] == '*' || (arr[k] == 0 && nbs[k] <= 0) || (arr[k] == 1 && nbs[k] == 1))) { + match = false; + } + } + + if (match) { + new Tile({ + x: x, + y: y, + type: this.theme.cliffs[cliffTable[i].cliffIndex], + }); + cliffTilePlaced = true; + this.fields[x][y].hm4++; + } + } + + if (!cliffTilePlaced) { + var atLeastOneHigherGroundExists = false; + for (var k = 0; k < 8; k++) { + if (nbs[k] > 0) { + atLeastOneHigherGroundExists = true; + } + } + + if (atLeastOneHigherGroundExists && h == parseInt(h)) { + if (this.getHMValue(x, y + 1) <= h) { + new Tile({ + x: x, + y: y, + type: this.theme.cliffs[0], + }); + } else { + new Tile({ + x: x, + y: y, + type: this.theme.cliffs[12], + }); + } + + this.fields[x][y].hm4++; + } + } + } + } + + + // Ramps + var usedPoints = []; + + for (var x = x1_; x <= x2_; x++) { + for (var y = y1_; y <= y2_; y++) { + var h = this.fields[x][y].hm; + + if (h == 'N' || h == 'S' || h == 'E' || h == 'W') { + var ramp = getRampTypeFromCode(h); + + // determine height level of this ramp + var x2 = x; + var y2 = y; + + while (this.getHMValue(x2, y2) == h) { + x2 += ramp.vec.x; + y2 += ramp.vec.y; + } + + this.fields[x][y].hm2 = this.getHMValue(x2, y2); + + // search and set lowest and highest point + x2 = x; + y2 = y; + + while (this.getHMValue(x2, y2) == h) { + x2 += ramp.vec.x; + y2 += ramp.vec.y; + } + + var x3 = x; + var y3 = y; + + while (this.getHMValue(x3, y3) == h) { + x3 -= ramp.vec.x; + y3 -= ramp.vec.y; + } + + this.fields[x][y].rampEnds = { + low: new Field(x2, y2).add3(-ramp.vec.x / 2, -ramp.vec.y / 2), + high: new Field(x3, y3).add3(ramp.vec.x / 2, ramp.vec.y / 2), + }; + + // sides + for (var i = 0; i < ramp.cliffs.length; i++) { + var cliff = ramp.cliffs[i]; + + // search point1 + x2 = x; + y2 = y; + + while (this.getHMValue(x2 + cliff.x, y2) == h) { + x2 += cliff.x; + } + + while (this.getHMValue(x2, y2 + cliff.y) == h) { + y2 += cliff.y; + } + + if (cliff.addX) { + x2 += cliff.addX; + } + + if (cliff.addY) { + y2 += cliff.addY; + } + + var hash = x2 + y2 * 10000; + if (usedPoints.contains(hash)) { + i = ramp.cliffs.length; + continue; + } else { + usedPoints.push(hash); + if (!this.fieldIsBlocked(x2, y2)) { + new Tile({ + x: x2, + y: y2, + type: cliff.cliff, + }); + } + } + } + + // ground texture + + // move draw pointer to initial position + x2 = x; + y2 = y; + + while (this.getHMValue(x2 + ramp.texture.initX, y2) == h) { + x2 += ramp.texture.initX; + } + + while (this.getHMValue(x2, y2 + ramp.texture.initY) == h) { + y2 += ramp.texture.initY; + } + + // draw + while (this.getHMValue(x2 + ramp.texture.loopX * 2, y2 + ramp.texture.loopY * 2) == h) { + new Tile({ + x: x2 + ramp.texture.drawX, + y: y2 + ramp.texture.drawY - this.getHMValue2(x2, y2) * CLIFF_HEIGHT, + type: ramp.texture.tiles[Math.floor(Math.random() * ramp.texture.tiles.length)], + }); + + x2 += ramp.texture.loopX; + y2 += ramp.texture.loopY; + } + } + } + } +}; +Game.prototype.enableChat = function(val) { + this.chat_muted = val; // !this.chat_muted; + // Pop up + interface_.addMessage('Chat ' + (this.chat_muted ? 'muted.' : 'unmuted.'), 'yellow', imgs.attentionmarkYellow); +}; + +// kill ramp at position x, y +Game.prototype.killRamp = function(x, y) { + var h = this.getHMValue(x, y); + + if (h == parseInt(h)) { + return; + } + + var fields = [this.fields[x][y]]; + var fields2 = []; + + while (fields.length > 0) { + var f = fields.pop(); + fields2.push(f); + + for (var i = 0; i < nbCoords.length; i++) { + var nb = this.fields[f.x + nbCoords[i].x] ? this.fields[f.x + nbCoords[i].x][f.y + nbCoords[i].y] : null; + if (nb && !fields.contains(nb) && !fields2.contains(nb) && this.getHMValue(nb.x, nb.y) == h) { + fields.push(nb); + } + } + } + + for (var i = 0; i < fields2.length; i++) { + // kill side cliffs + if (!this.blockArray[fields2[i].x][fields2[i].y]) { + for (var k = 0; k < this.blockingTiles.length; k++) { + if (this.blockingTiles[k].includesField(fields2[i].x, fields2[i].y)) { + this.blockingTiles[k].switchBlockingTotal(false); + this.blockingTiles.splice(k, 1); // kill the object + } + } + } + + // kill ground tiles + for (var k = 0; k < this.groundTiles2.length; k++) { + if (this.groundTiles2[k].type.isCliff) { + this.groundTiles2.splice(k, 1); + } + } + + fields2[i].hm = fields2[i].hm2; + } + + // refresh the pre drawn blocking tiles canvasses + this.generateTilesCanvasses(); + + // re sort tiles + this.sortTiles(); + + // refresh minimap canvas, we might have killed a tile + this.minimap.refreshTilesCanvas(); + + // redraw ground tiles canvas + this.generateGroundTextureCanvas(); +}; + +// sort tiles by y coord to bring them in the right order when drawing +Game.prototype.sortTiles = function() { + this.objectsToDraw = _.sortBy(this.tilesCashes.concat(this.units, this.buildings), function(obj) { + return obj.getYDrawingOffset(); + }); +}; + +Game.prototype.generateTilesCanvasses = function() { + for (var y = 0; y <= this.y; y++) { + this.refreshBlockingTilesCanvas(y); + } +}; + +Game.prototype.refreshBlockingTilesCanvas = function(y) { + var tiles = []; + var maxHeight = 1; + + // add all tiles that have this y coordinate + for (var i = 0; i < this.blockingTiles.length; i++) { + var tile = this.blockingTiles[i]; + + if (tile.y + tile.type.sizeY - 1 == y) { + tiles.push(tile); + var hm = this.getHMValue2(tile.x, tile.y); + maxHeight = Math.max(maxHeight, tile.type.img.img.h + (hm ? hm : 0) * CLIFF_HEIGHT * 16); + } + } + + // sort the tiles by their exact y offset + tiles = _.sortBy(tiles, function(tile) { + return tile.randomOffsetY; + }); + + // create canvas and set parameters + var canvas = document.createElement('canvas'); + canvas.width = (this.x + 1) * FIELD_SIZE / SCALE_FACTOR; + canvas.height = maxHeight; + canvas.y_ = y; + + canvas.getYDrawingOffset = function() { + return this.y_; + }; + + // define draw function + canvas.draw = function(x1, x2, y1, y2) { + c.drawImage(this, -x1 * FIELD_SIZE, (this.y_ - y1) * FIELD_SIZE - this.height * SCALE_FACTOR, this.width * SCALE_FACTOR, this.height * SCALE_FACTOR); + }; + + canvas.isInBox = function(x1, y1, x2, y2) { + return y1 < this.y_ && y2 > this.y_ - 1 && this.height > 1; + }; + + canvas.isInBoxVisible = canvas.isInBox; + + var ctx = canvas.getContext('2d'); + + // draw the tiles on the canvas + for (var i = 0; i < tiles.length; i++) { + var tile = tiles[i]; + + var img = (game_state == GAME.EDITOR && tile.type.img.imgEditor) ? tile.type.img.imgEditor : tile.type.img.img; + var x_ = (tile.pos.px + tile.randomOffsetX) * 16 - img.w / 2; + var y_ = maxHeight - img.h + (tile.randomOffsetY - this.getHMValue2(tile.x, tile.y) * CLIFF_HEIGHT) * 16; + ctx.drawImage(tile.type.img.file[0], img.x, img.y, img.w, img.h, Math.floor(x_), Math.floor(y_), img.w, img.h); + } + + this.tilesCashes[y] = canvas; +}; + +Game.prototype.refreshSpectatorTab = function(index) { + if (isNaN(index)) { + index = $('#spectatorDropdown')[0].selectedIndex; + } + + interface_.showInfoDiv = index > 0; + if (index == 0) { + return; + } + + var fieldName = this.specFieldNames[index]; + + // draw + $('#spectatorDiv').html(''); + + var x = 0; + var y = 0; + + for (var i = 1; i < this.players.length; i++) { + if (this.players[i] && this.players[i].controller != CONTROLLER.SPECTATOR && playerColors[i - 1]) { + var p = this.players[i]; + + x = 20; + + var arr = playerColors[i - 1][4]; + $('#spectatorDiv')[0].innerHTML += ''; + + if (fieldName) { + _.each(p[fieldName], function(count, type_id_string) { + var type = lists.types[type_id_string]; + + var count_ = 0; + var bar = ''; + + if (isNaN(count)) { + var perc = 0; + + if (!count.from) { + var buildTicksLeft = _.sortBy(count.to, function(el) { + return el.buildTicksLeft; + })[0].buildTicksLeft; + perc = (type.getValue('buildTime', p) - buildTicksLeft) / type.getValue('buildTime', p); + } else { + var soonestFinish = _.sortBy(count.to, function(el) { + return el; + })[0]; + var soonestStart = _.sortBy(count.from, function(el) { + return el; + })[0]; + perc = (ticksCounter - soonestStart) / (soonestFinish - soonestStart); + } + + bar = ''; + count_ = count.to.length; + } else { + count_ = count; + } + + var img = ((type.img && type.img.getDataURLFile) ? type.img : type.image).getDataURLFile(game.players[i].number); + + var rnd = 'div_' + parseInt(Math.random() * 999999999); + + var str = '' + count_ + '
' + bar + 'Current: ' + (p.currentMinedGold * 6) + ' Total: ' + p.minedGold + ' Lost: ' + p.goldLost + '
' + Math.floor(p.apm / (ticksCounter / 1200)) + '
' + escapedMsg + '
'; +}; + +// refresh the neighbours of a field, gets called everytime a building gets destroyed or created +Game.prototype.refreshNBSOfField = function(field) { + var nbs = []; + + for (var x = field.x - 1; x <= field.x + 1; x++) { + for (var y = field.y - 1; y <= field.y + 1; y++) { + if ((x != field.x || y != field.y) && x >= 0 && y >= 0 && x <= this.x && y <= this.y && this.blockArray[x][y] && (x == field.x || y == field.y || (this.blockArray[field.x][y] && this.blockArray[x][field.y]))) { + nbs.push(this.fields[x][y]); + } + } + } + + field.nbs = nbs; +}; + +// refresh the neighbours of a field, gets called everytime a building gets destroyed or created (2x2) +Game.prototype.refreshNBSOfField2x2 = function(field) { + var nbs = []; + + for (var x = field.x - 1; x <= field.x + 1; x++) { + for (var y = field.y - 1; y <= field.y + 1; y++) { + if ((x != field.x || y != field.y) && x >= 0 && y >= 0 && x <= this.x - 1 && y <= this.y - 1 && this.fieldIsFree2x2(x, y) && (x == field.x || y == field.y || (this.fieldIsFree2x2(field.x, y) && this.fieldIsFree2x2(x, field.y)))) { + nbs.push(this.fields2x2[x][y]); + } + } + } + + field.nbs = nbs; +}; + +Game.prototype.fieldIsFree2x2 = function(x, y) { + return this.blockArray[x][y] && this.blockArray[x + 1][y] && this.blockArray[x][y + 1] && this.blockArray[x + 1][y + 1]; +}; + +Game.prototype.generateGroundTextureCanvas = function() { + // set size depending on map size + this.groundTilesCanvas.width = this.defaultTilesCanvas.width; + this.groundTilesCanvas.height = this.defaultTilesCanvas.height; + + var textures = []; + var nonTextures = []; + + for (var i = 0; i < this.groundTiles2.length; i++) { + if (this.groundTiles2[i].type.isTexture) { + textures.push(this.groundTiles2[i]); + } else { + nonTextures.push(this.groundTiles2[i]); + } + } + + var tiles = textures.concat(nonTextures); + + var ctx = this.groundTilesCanvas.getContext('2d'); + + ctx.drawImage(this.defaultTilesCanvas, 0, 0); + + for (var i = 0; i < tiles.length; i++) { + var img = tiles[i].type.img.img; + ctx.drawImage(tiles[i].type.img.file[0], img.x, img.y, img.w, img.h, Math.floor(tiles[i].drawPos.px * 16 - img.w / 2), Math.floor((tiles[i].drawPos.py + 2) * 16 - img.h / 2), img.w, img.h); + } + + ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'; + ctx.fillRect(0, 0, this.groundTilesCanvas.width, 32); +}; + +Game.prototype.getHeightmapString = function() { + var str = ''; + for (var x = 1; x <= this.x; x++) { + for (var y = 1; y <= this.y; y++) { + str += this.fields[x][y].hm.toString(); + } + } + return str; +}; + +// returns a json file which includes the whole map data including all tiles and units +Game.prototype.export_ = function(withCliffs) { + var unitData = {}; + + // Checks if the provided field in type t is the same as the field in the provided basic type, so that non-frozen maps + // can skip saving that field's value + // TODO: rewrite this to be less incomprehensible (originally was a massive set of nested ternary statements) + const isFieldSameAsBasicType = (t, field, basicType) => { + // Figure out the default value that the basic object will have + // TODO: this logic is likely duplicated somewhere when the basic type is actually loaded into the game + // Find out where it's duplicated and use that instead + let basicValue; + if (field.name in basicType) { + if (field.values && typeof basicType[field.name] == 'string' && basicType[field.name] in field.values) { + basicValue = field.values[basicType[field.name]]; + } else { + basicValue = basicType[field.name]; + } + } else { + basicValue = field.default_; + } + + // The field not being defined means it will take the default value + return (!(field.name in t)) || t[field.name] == basicValue; + }; + + _.each(lists.types, (t1) => { + // If this is a frozen map, there is no basic type that we should diff off of + const basicType = this.globalVars.isFrozen ? null : t1.getBasicType(); + const fields = t1.getDataFields(); + + if (!basicType) { + unitData[t1.id_string] = { + isUnit: t1.isUnit, + isBuilding: t1.isBuilding, + isUpgrade: t1.isUpgrade, + isCommand: t1.isCommand, + isModifier: t1.isModifier, + }; + } + + _.each(fields, (field) => { + if (field.type == 'complex') { + // TODO: this is copied from the loop below. Merge the code for these two + if (!t1[field.name]) { + return; + } + + if (!this.globalVars.isFrozen && mapEditorData.fieldIsDefault(field, t1)) { + return; + } + + if (!unitData[t1.id_string]) { + unitData[t1.id_string] = {}; + } + if (!unitData[t1.id_string][field.name]) { + unitData[t1.id_string][field.name] = {}; + } + + field.values.forEach((value) => { + unitData[t1.id_string][field.name][value.name] = t1[field.name][value.name]; + }); + } else if (field.isArray) { + if (basicType && arraysAreSame(t1[field.name], basicType[field.name] ?? field.default2_, field.values)) { + return; + } + + if (!unitData[t1.id_string]) { + unitData[t1.id_string] = {}; + } + + var arr = []; + + if (t1[field.name]) { + for (var j = 0; j < t1[field.name].length; j++) { + if (isObject(t1[field.name][j]) && t1[field.name][j].id_string) { + arr[j] = t1[field.name][j].id_string; + } else if (isObject(t1[field.name][j]) && t1[field.name][j].isTargetRequirement) { + arr[j] = t1[field.name][j].funcName; + } else { + arr[j] = t1[field.name][j]; + } + } + } + + unitData[t1.id_string][field.name] = arr; + } else if (field.type == 'commands') { + if (basicType && objectsAreSame(t1[field.name], basicType[field.name], lists.types)) { + return; + } + + if (!unitData[t1.id_string]) { + unitData[t1.id_string] = {}; + } + + var obj = {}; + + _.each(t1[field.name], function(val, key) { + if (!val) { + throw Error(`Issue with command (${val}, ${key})`); + } + obj[key] = val.id_string; + }); + + unitData[t1.id_string][field.name] = obj; + } else { + if (basicType && isFieldSameAsBasicType(t1, field, basicType)) { + return; + } + + if (!unitData[t1.id_string]) { + unitData[t1.id_string] = {}; + } + + var dataToWrite = t1[field.name] ?? field.default_; + + if (isObject(dataToWrite)) { + if (getBuildingImgNameFromImgObject(dataToWrite)) // building img + { + dataToWrite = getBuildingImgNameFromImgObject(dataToWrite); + } else if (dataToWrite.name && dataToWrite.file) // unit img + { + dataToWrite = dataToWrite.id_string; + } else if (dataToWrite.id_string) // other type + { + dataToWrite = dataToWrite.id_string; + } + } else if (_.isFunction(dataToWrite)) { + dataToWrite = getImageFunctionName(dataToWrite); + } + + unitData[t1.id_string][field.name] = dataToWrite; + } + }); + }); + + var graphics = {}; + for (key in customImgs) { + if (!unit_imgs[key] && !building_imgs[key] && key != 'buildingSheet' && key != 'tileSheet' && key != 'miscSheet') { + graphics[key] = customImgs[key][0].src; + } + } + + var graphicObjects = {}; + _.each(lists.imgs, (t1) => { + if (!t1) { + return; + } + + const basicType = this.globalVars.isFrozen ? null : t1.getBasicType(); + const fields = t1.getDataFields(); + + _.each(fields, (field) => { + if (field.type == 'complex') { + if (!t1[field.name]) { + return; + } + + if (!this.globalVars.isFrozen && mapEditorData.fieldIsDefault(field, t1)) { + return; + } + + if (!graphicObjects[t1.id_string]) { + graphicObjects[t1.id_string] = {}; + } + if (!graphicObjects[t1.id_string][field.name]) { + graphicObjects[t1.id_string][field.name] = {}; + } + + field.values.forEach((value) => { + graphicObjects[t1.id_string][field.name][value.name] = t1[field.name][value.name]; + }); + } else if (field.name == 'file') { + if (basicType && t1[field.name] == basicType[field.name]) { + return; + } + + if (!graphicObjects[t1.id_string]) { + graphicObjects[t1.id_string] = {}; + } + + for (key_ in customImgs) { + if (customImgs[key_] == t1[field.name]) { + graphicObjects[t1.id_string][field.name] = key_; + } + } + } else { + if (basicType && isFieldSameAsBasicType(t1, field, basicType)) { + return; + } + + if (!graphicObjects[t1.id_string]) { + graphicObjects[t1.id_string] = {}; + } + + // TODO: this is copy-pasted from above. Honestly, both of these loops should use the same code + var dataToWrite = t1[field.name] ?? field.default_; + + if (isObject(dataToWrite)) { + if (getBuildingImgNameFromImgObject(dataToWrite)) // building img + { + dataToWrite = getBuildingImgNameFromImgObject(dataToWrite); + } else if (dataToWrite.name && dataToWrite.file) // unit img + { + dataToWrite = dataToWrite.name; + } else if (dataToWrite.id_string) // other type + { + dataToWrite = dataToWrite.id_string; + } + } else if (_.isFunction(dataToWrite)) { + dataToWrite = getImageFunctionName(dataToWrite); + } + + graphicObjects[t1.id_string][field.name] = dataToWrite; + } + }); + }); + + var data = { + name: this.name, + x: this.x, + y: this.y, + tiles: [], + groundTiles: [], + units: [], + buildings: [], + defaultTiles: this.data.defaultTiles, + heightmap: this.getHeightmapString(), + unitData: {}, + description: this.description, + globalVars: this.globalVars, + players: MapEditorSettings.getPlayerSettings(), + graphics: {}, + graphicObjects: {}, + }; + + if (this.globalVars.isModded || this.globalVars.isFrozen) { + data.unitData = unitData; + data.graphics = graphics; + data.graphicObjects = graphicObjects; + } + + var tiles = this.groundTiles2.concat(this.blockingTiles); + for (var i = 0; i < tiles.length; i++) { + if (!tiles[i].type.isCliff || withCliffs) // dont save default tiles (= ground textures), they are generated randomly at each mapload and dont save cliff tiles, they are created from heightmap + { + if (tiles[i].type.ignoreGrid) { + data.tiles.push({ + x: tiles[i].pos.px, + y: tiles[i].pos.py, + type: tiles[i].type.name, + }); + } else { + data.tiles.push({ + x: tiles[i].x, + y: tiles[i].y, + type: tiles[i].type.name, + }); + } + } + } + + for (var i = 0; i < this.units.length; i++) { + var u = this.units[i]; + var o = { + x: u.pos.px, + y: u.pos.py, + type: u.type.name, + owner: u.owner.number, + waypoint: this.getWaypointExportObject(u.waypoint), + }; + data.units.push(o); + } + + for (var i = 0; i < this.buildings.length; i++) { + var u = this.buildings[i]; + var o = { + x: u.x, + y: u.y, + type: u.type.name, + owner: u.owner.number, + waypoint: this.getWaypointExportObject(u.waypoint), + }; + data.buildings.push(o); + } + + return data; +}; + +Game.prototype.getWaypointExportObject = function(wp) { + if (wp) { + var arr = []; + for (var i = 0; i < wp.length; i++) { + if (wp[i].px) { + arr.push(wp[i].px, wp[i].py); + } + } + + return arr; + } +}; + +Game.prototype.getUnitById = function(id) { + return this.unitList[id]; +}; + +Game.prototype.getNextBuildingOfType = function(pos, owner, onlyFinished, filter) { + var shortestDistance = 999999; + var building = null; + + for (var i = 0; i < this.buildings.length; i++) { + var b = this.buildings[i]; + if ((!filter || b.type[filter]) && pos.distanceTo2(b.pos) < shortestDistance && (!owner || owner == b.owner) && (!onlyFinished || !b.isUnderConstruction)) { + building = b; + shortestDistance = pos.distanceTo2(b.pos); + } + } + + return building; +}; + +Game.prototype.unitMatchesFilters = function(unit, command) { + if (unit && unit.type) { + for (var k = 0; k < command.targetRequiremementsArray.length; k++) { + var met = false; + + for (var i = 0; i < command.targetRequiremementsArray[k].length; i++) { + if (command.targetRequiremementsArray[k][i].func(unit)) { + met = true; + } + } + + if (!met) { + return false; + } + } + } + + return true; +}; + +// get unit at specific position (usually to check if where we clicked is a unit and which one) or for hover effect +Game.prototype.getUnitAtPosition = function(x, y, filters) { + var bestUnit = null; + var lowestDistance = 99999; + var clickedField = new Field(x, y, true); + + for (var i = 0; i < this.units.length; i++) { + var unit = this.units[i]; + var dist = unit.pos.distanceTo2(clickedField); + + if ((game_state == GAME.EDITOR || PLAYING_PLAYER.team.canSeeUnitInvisible(unit) || PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR) && unit.isInBox(x - CLICK_TOLERANCE, y - CLICK_TOLERANCE + unit.type.selectionOffsetY, x + CLICK_TOLERANCE, y + CLICK_TOLERANCE + 0.4 + unit.type.selectionOffsetY) && lowestDistance > dist && PLAYING_PLAYER.team.canSeeUnit(unit)) { + if (!filters || this.unitMatchesFilters(unit, filters)) { + lowestDistance = dist; + bestUnit = unit; + } + } + } + + if (bestUnit) { + return bestUnit; + } + + lowestDistance = 99999; + for (var i = 0; i < this.buildings2.length; i++) { + var building = this.buildings2[i]; + var dist = building.pos.distanceTo2(clickedField); + + var selectionOffsetY = building.type.selectionOffsetY || 0; + if (building.seenBy[PLAYING_PLAYER.team.number] && building.isInBox(x - CLICK_TOLERANCE, y - CLICK_TOLERANCE + selectionOffsetY, x + CLICK_TOLERANCE, y + CLICK_TOLERANCE + selectionOffsetY + 0.4) && lowestDistance > dist) { + lowestDistance = dist; + bestUnit = building; + } + } + return bestUnit; +}; + +// return unit-array based on the coordinates of a box drawn (select all units of this type in the screen region if selectAll) +Game.prototype.getSelection = function(x1, y1, x2, y2, selectAll) { + var units = []; + + // if click (no box), then look for nearest valiable unit and select this one + if (x2 - x1 <= 0.05 && y2 - y1 <= 0.05) { + var newUnit = this.getUnitAtPosition(x1, y1); + + if (newUnit && newUnit.owner == PLAYING_PLAYER && (selectAll || (this.timeOfLastSelection + 500 >= timestamp && /* game.selectedUnits.length == 1 &&*/ newUnit == game.selectedUnits[0]))) { + var unitsAndBuildings = this.units.concat(this.buildings); + for (var i = 0; i < unitsAndBuildings.length; i++) { + var u = unitsAndBuildings[i]; + if ((game_state == GAME.EDITOR || PLAYING_PLAYER.team.canSeeUnitInvisible(u) || PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR) && u.isInBox(game.cameraX / FIELD_SIZE, game.cameraY / FIELD_SIZE, (WIDTH + game.cameraX) / FIELD_SIZE, (HEIGHT - INTERFACE_HEIGHT + game.cameraY) / FIELD_SIZE) && u.type == newUnit.type && u.owner == PLAYING_PLAYER) { + units.push(u); + } + } + + return units; + } + + return newUnit ? [newUnit] : []; + } + + var countEnemyUnitsSelected = 0; // also buildings + var countOwnUnitsSelected = 0; // also buildings + var countOwnBildingsSelected = 0; + var unitsAndBuildings = this.units.concat(this.buildings); + var selected_type_id_strings = {}; + + for (var i = 0; i < unitsAndBuildings.length; i++) { + var u = unitsAndBuildings[i]; + if ((game_state == GAME.EDITOR || PLAYING_PLAYER.team.canSeeUnitInvisible(u) || PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR) && u.isInBox(x1, y1 + u.type.selectionOffsetY, x2, y2 + u.type.selectionOffsetY)) { + if (selectAll) { + selected_type_id_strings[u.type.id_string] = true; + } else { + units.push(u); + countEnemyUnitsSelected += u.owner != PLAYING_PLAYER ? 1 : 0; + countOwnUnitsSelected += u.owner == PLAYING_PLAYER ? 1 : 0; + countOwnBildingsSelected += (u.owner == PLAYING_PLAYER && u.type.isBuilding) ? 1 : 0; + } + } + } + + if (selectAll) { + for (var i = 0; i < unitsAndBuildings.length; i++) { + var u = unitsAndBuildings[i]; + if (selected_type_id_strings[u.type.id_string] && ((game_state == GAME.EDITOR || PLAYING_PLAYER.team.canSeeUnitInvisible(u) || PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR) && u.isInBox(game.cameraX / FIELD_SIZE, game.cameraY / FIELD_SIZE, (WIDTH + game.cameraX) / FIELD_SIZE, (HEIGHT - INTERFACE_HEIGHT + game.cameraY) / FIELD_SIZE) && u.owner == PLAYING_PLAYER)) { + units.push(u); + countEnemyUnitsSelected += u.owner != PLAYING_PLAYER ? 1 : 0; + countOwnUnitsSelected += u.owner == PLAYING_PLAYER ? 1 : 0; + countOwnBildingsSelected += (u.owner == PLAYING_PLAYER && u.type.isBuilding) ? 1 : 0; + } + } + } + + // if were in the editor, were allowed to select multiple enemy units and multiple whatever, so return here and dont perform the deselect stuff + if (game_state == GAME.EDITOR || PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR) { + return units; + } + + // if own and enemy units selected, unselect all enemy units + if (countOwnUnitsSelected > 0 && countEnemyUnitsSelected > 0) { + for (var i = 0; i < units.length; i++) { + if (units[i].owner != PLAYING_PLAYER) { + units.splice(i, 1); + i--; + } + } + } + + // if no own but more than 1 enemy units selected, unselect all but 1 enemy units + if (countOwnUnitsSelected == 0 && countEnemyUnitsSelected > 0) { + units = [units[0]]; + } + + // if only buildings selected, unselect all but 1 + if (units.length > 1 && countOwnBildingsSelected == units.length) { + units.length = 1; + } + + // if buildings but also units selected, unselect all buildings + if (countOwnBildingsSelected > 0 && countOwnUnitsSelected - countOwnBildingsSelected > 0) { + for (var i = 0; i < units.length; i++) { + if (units[i].type.isBuilding) { + units.splice(i, 1); + i--; + } + } + } + + return units; +}; + +// adding new units to an existing selection, removing duplicates and not allowing buildings & units or units from several players +Game.prototype.addUnitsToSelection = function(selection, newUnits) { + // if no new units, nothing to do + if (newUnits.length == 0) { + return; + } + + // if no existing units, just replace selected units with the new units + if (selection.length == 0) { + selection = newUnits; + return; + } + + if (newUnits[0].owner == PLAYING_PLAYER && selection[0].owner == PLAYING_PLAYER && (selection[0].type.isBuilding && newUnits[0].type.isBuilding) || (!selection[0].type.isBuilding && !newUnits[0].type.isBuilding)) { + var newUnitsContainUnselectedUnits = false; + + for (var i = 0; i < newUnits.length; i++) { + if (!selection.contains(newUnits[i])) { + newUnitsContainUnselectedUnits = true; + } + } + + for (var i = 0; i < newUnits.length; i++) { + if (selection.contains(newUnits[i])) { + if (!newUnitsContainUnselectedUnits) { + selection.erease(newUnits[i]); + } + } else { + selection.push(newUnits[i]); + } + } + } +}; + +// check if the current selected units belong to the playing player +Game.prototype.humanUnitsSelected = function() { + return this.selectedUnits.length > 0 && this.selectedUnits[0].owner == PLAYING_PLAYER; +}; + +Game.prototype.getGameStateCheckSum = function() { + var checksum = 0; + + for (var i = 0; i < this.units.length; i++) { + checksum += this.units[i].pos.px + this.units[i].pos.py; + } + + return checksum; +}; + +Game.prototype.fieldIsBlocked = function(x, y) { + return !(x > 0 && x <= this.x && y > 0 && y <= this.y && this.blockArray[x][y]); +}; + +Game.prototype.fieldIsBlockedForBuilding = function(x, y) { + return !(x > 0 && x <= this.x && y > 0 && y <= this.y && this.blockArray[x][y] && this.fields[x][y].hm == parseInt(this.fields[x][y].hm)); +}; + +Game.prototype.fieldIsBlockedFlying = function(x, y) { + return !(x > 0 && x <= this.x && y > 2 && y <= this.y); +}; + +// get the center of a group of units +Game.prototype.getCenterOfUnits = function(units) { + var x = 0; + var y = 0; + + for (var i = 0; i < units.length; i++) { + x += units[i].pos.px; + y += units[i].pos.py; + } + + return new Field(x / units.length, y / units.length, true); +}; + +// return true, if at least one of the selected units can perform a specific order; only the units matching 'type' are checked +Game.prototype.selectedUnitsCanPerformOrder = function(order, type) { + // loop through all units, and check if one of the highest prio units can perform the order + var units = this.selectedUnits; + for (var i = 0; i < units.length; i++) { + if (units[i].type == type) { + if (units[i].type.commands[order.id_string] && (!units[i].type.isBuilding || !units[i].isUnderConstruction)) { + return true; + } + + if (order.type == COMMAND.CANCEL && units[i].type.isBuilding && (units[i].isUnderConstruction || units[i].queue[0])) { + return true; + } + } + } + + return false; +}; + +Game.prototype.playerHasIdleWorkers = function(player) { + for (var i = 0; i < this.units.length; i++) { + if (this.units[i].owner == player && this.units[i].type == lists.types.worker && (!this.units[i].order || this.units[i].order.type == COMMAND.IDLE)) { + return true; + } + } + return false; +}; + +Game.prototype.getOrderArray = function(units, order, target, shift, autoCast, on, learn) { + // make array with target and order + var o; + + if (learn) { + o = ['learn', order.id]; + } else if (autoCast) { + o = ['ac', order.id, on]; + } else if (!target) { + o = ['instant', order.id, shift]; + } else if (target.isField) { + o = ['field', order.id, shift, target.px, target.py]; + } else if (order.targetIsInt) { + o = ['int', order.id, shift, parseInt(target)]; + } else { + o = ['unit', order.id, shift, target.id]; + } + + // add all the selected units + for (var i = 0; i < units.length; i++) { + o.push(units[i].id); + } + + return o; +}; + +Game.prototype.getUnitsWithRdyCooldown = function(units, cmd) { + // Negation specifically used instead of <= to account for case where addends are NaN + return units.filter((u) => + !(u.lastTickAbilityUsed[cmd.id] + cmd.getValue('cooldown2', u) > ticksCounter)); +}; + +// store an order +Game.prototype.issueOrderToUnits = function(units, order, target, shift, autoCast, on, learn) { + if (!units || units.length == 0) { + return; + } + + if (learn) { + if (network_game) { + outgoingOrders.push(this.getOrderArray(units, order, target, shift, autoCast, on, true)); + } else { + worker.postMessage({ + what: 'order', + msg: [units[0].owner.number].concat(this.getOrderArray(units, order, target, shift, autoCast, on, true)), + }); + } + + return; + } + + // check for cooldown + if (order.cooldown2) { + units = this.getUnitsWithRdyCooldown(units, order); + } + + if (units.length == 0) { + interface_.addMessage('Cooldown not ready', 'red', imgs.attentionmark); + soundManager.playSound(SOUND.NEGATIVE); + return; + } + + // if attacking invincible unit (Goldmine) and drawing and playingplayer, create msg and sound + if (order.type == COMMAND.ATTACK && target.getValue('isInvincible')) { + // if drawing, show alert + if (units[0].owner == PLAYING_PLAYER) { + interface_.addMessage('This ' + (target.type.isBuilding ? 'building' : 'unit') + ' is invincible.', 'red', imgs.attentionmark); + soundManager.playSound(SOUND.NEGATIVE); + } + + return; + } + + // check for target requirements + var requirement_text = !autoCast ? units[0].owner.getCommandRequirementText(order, units, target) : null; + + if (requirement_text) { + // if drawing, show alert + if (units[0].owner == PLAYING_PLAYER) { + interface_.addMessage(requirement_text, 'red', imgs.attentionmark); + soundManager.playSound(SOUND.NEGATIVE); + } + + return; + } + + if (network_game) { + outgoingOrders.push(this.getOrderArray(units, order, target, shift, autoCast, on)); + } else { + worker.postMessage({ + what: 'order', + msg: [units[0].owner.number].concat(this.getOrderArray(units, order, target, shift, autoCast, on)), + }); + } +}; + +Game.prototype.getReplayName = function() { + var str = ''; + + for (var i = 0; i < this.players.length; i++) { + if (this.players[i] && this.players[i].originalTeam.number > 0) { + str += this.players[i].name + '_v_'; + } + } + + if (str.length > 2) { + str = str.substring(0, str.length - 2); + } + + str += 'on_' + this.data.name; + + return str; +}; + +Game.prototype.selectionContainsCargoUnits = function() { + for (var i = 0; i < this.selectedUnits.length; i++) { + if (this.selectedUnits[i].type.cargoSpace) { + return true; + } + } + return false; +}; + +Game.prototype.selectionContainsWorkers = function() { + for (var i = 0; i < this.selectedUnits.length; i++) { + if (this.selectedUnits[i].type == lists.types.worker) { + return true; + } + } + return false; +}; + +Game.prototype.addObject = function(o) { + if (o.type && o.type.isUnit) { + this.units.push(o); + } else if (o.type && o.type.isBuilding && !o.isDummy) { + this.buildings.push(o); + this.buildings2.push(o); + } + + if (!o.isDummy) { + this.unitList[o.id] = o; + } + + this.addToObjectsToDraw(o); +}; + +Game.prototype.addToObjectsToDraw = function(o) { + for (var i = 0; i < this.objectsToDraw.length; i++) { + if (this.objectsToDraw[i].getYDrawingOffset() > o.getYDrawingOffset()) { + this.objectsToDraw.splice(i, 0, o); + return; + } + } + this.objectsToDraw.push(o); +}; + +Game.prototype.cacheCameraUpdate = function() { + if (PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR) { + return; + } + + outgoingCameraUpdate = { + x: this.cameraX, + y: this.cameraY, + width: WIDTH, + height: HEIGHT, + fieldSize: FIELD_SIZE, + }; +}; + +Game.prototype.setCameraX = function(x) { + this.cameraX = x; + + // if camera is out of bounds, bring it back + this.cameraX = Math.floor(Math.min(this.cameraX, this.x * FIELD_SIZE - WIDTH)); + this.cameraX = Math.floor(Math.max(this.cameraX, 0)); + + this.cacheCameraUpdate(); +}; + +Game.prototype.setCameraY = function(y) { + this.cameraY = y; + + // if camera is out of bounds, bring it back + this.cameraY = Math.floor(Math.min(this.cameraY, (this.y - this.getHMValue(1, this.y) * CLIFF_HEIGHT) * FIELD_SIZE - HEIGHT + (game_state == GAME.EDITOR ? MINIMAP_HEIGHT : INTERFACE_HEIGHT))); + this.cameraY = Math.floor(Math.max(this.cameraY, /* -FIELD_SIZE * 2*/ 0)); + + this.cacheCameraUpdate(); +}; + +Game.prototype.draw = function() { + // limit the max zoom, users can archive with their browser zoom + var maxViewSize = 1200 / 2; + + if (HEIGHT / SCALE_FACTOR > maxViewSize) { + var middle_x = (game.cameraX + WIDTH / 2) / FIELD_SIZE; + var middle_y = (game.cameraY + HEIGHT / 2) / FIELD_SIZE; + + setScaleFactor(HEIGHT / maxViewSize); + FIELD_SIZE = 16 * SCALE_FACTOR; + + this.setCameraX(middle_x * FIELD_SIZE - WIDTH / 2); + this.setCameraY(middle_y * FIELD_SIZE - HEIGHT / 2); + } + + // limit the max zoom, users can archive with their browser zoom + if (HEIGHT > 1200) { + var max_scale = HEIGHT / maxViewSize; + + if (SCALE_FACTOR < max_scale) { + setScaleFactor(max_scale); + FIELD_SIZE = 16 * SCALE_FACTOR; + } + } + + // mute chat + if (keyManager.keys[KEY.M] && this.lastMuteToggle + 10 <= ticksCounter) { + this.enableChat(!this.chat_muted); + this.lastMuteToggle = ticksCounter; + } + + // Cameras for all players + if (PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR) { + if (incomingCameraUpdates[ticksCounter]) { + // Camera updates has format {player_nr: {x, y, width, height}, ...} + for (const player_nr in incomingCameraUpdates[ticksCounter]) { + const update = incomingCameraUpdates[ticksCounter][player_nr]; + this.players[player_nr].cameraX = update.x; + this.players[player_nr].cameraY = update.y; + this.players[player_nr].cameraWidth = update.width; + this.players[player_nr].cameraHeight = update.height; + this.players[player_nr].fieldSize = update.fieldSize; + } + } + } + + var oldCameraX = this.cameraX; + var oldCameraY = this.cameraY; + + // Scrolling + if (this.followVision && this.visionSetting > 0) { + const player = this.players[this.visionSetting]; + const targetX = player.cameraX + player.cameraWidth / 2 - WIDTH / 2; + const targetY = player.cameraY + player.cameraHeight / 2 - HEIGHT / 2; + FIELD_SIZE = player.fieldSize; + this.setCameraX(targetX); + this.setCameraY(targetY); + } else if (!game_paused || PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR) { + const mouseScrollEnabled = timeDiff < 1000 && ( + document.fullScreen || + document.mozFullScreen || + document.webkitIsFullScreen || + interface_.mouseScrollWhenWindowed.get() + ); + + const scrollSpeed = interface_.scrollSpeed.get(); + if (keyManager.keys[KEY.DOWN] || keyManager.y >= HEIGHT - SCROLL_RANGE && mouseScrollEnabled) { + this.setCameraY(this.cameraY + timeDiff * scrollSpeed / 1000); + } else if (keyManager.keys[KEY.UP] || keyManager.y <= 25 && mouseScrollEnabled) { + this.setCameraY(this.cameraY - timeDiff * scrollSpeed / 1000); + } + + if (keyManager.keys[KEY.LEFT] || keyManager.x <= SCROLL_RANGE && mouseScrollEnabled) { + this.setCameraX(this.cameraX - timeDiff * scrollSpeed / 1000); + } else if (keyManager.keys[KEY.RIGHT] || keyManager.x >= WIDTH - SCROLL_RANGE && mouseScrollEnabled) { + this.setCameraX(this.cameraX + timeDiff * scrollSpeed / 1000); + } + + // Minimap click change screen + if (keyManager.minimapScroll && keyManager.x < MINIMAP_WIDTH && keyManager.y > HEIGHT - MINIMAP_HEIGHT) { + var field = this.minimap.getFieldFromClick(keyManager.x, keyManager.y); + + this.setCameraX(Math.floor(field.px * FIELD_SIZE - WIDTH / 2)); + this.setCameraY(Math.floor(field.py * FIELD_SIZE - HEIGHT / 2)); + } + + // clicking portrait scrolling (following unit) + var unitNfoX = (WIDTH - 780) / 2; + if (this.selectedUnits.length == 1 && (keyManager.leftMouse && keyManager.x > unitNfoX && keyManager.y > HEIGHT - INTERFACE_HEIGHT + 20 && keyManager.x < unitNfoX + 140)) { + this.setCameraX(this.selectedUnits[0].drawPos.px * FIELD_SIZE - WIDTH / 2); + this.setCameraY(this.selectedUnits[0].drawPos.py * FIELD_SIZE - HEIGHT / 2); + } + + if (keyManager.keys[KEY.SPACE] && this.selectedUnits.length > 0) { + var location = this.getCenterOfUnits(this.selectedUnits); + this.setCameraX(location.px * FIELD_SIZE - WIDTH / 2); + this.setCameraY(location.py * FIELD_SIZE - HEIGHT / 2); + } + } + + if (keyManager.drawBox && (oldCameraX != this.cameraX || oldCameraY != this.cameraY)) { + keyManager.startX -= this.cameraX - oldCameraX; + keyManager.startY -= this.cameraY - oldCameraY; + } + + // unselected units + if (game_state == GAME.PLAYING) { + for (var i = 0; i < this.selectedUnits.length; i++) { + if (!PLAYING_PLAYER.team.canSeeUnit(this.selectedUnits[i], true) || (!(PLAYING_PLAYER.team.canSeeUnitInvisible(this.selectedUnits[i]) || PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR))) { + this.selectedUnits.splice(i, 1); + i--; + } + } + } + + var unitsBuildings = this.buildings2.concat(this.units); + + var hoverUnit = null; + + // clear screen + c.fillStyle = 'black'; + c.fillRect(0, 0, WIDTH, HEIGHT); + + // draw GroundTiles canvas + var drawW = Math.min(WIDTH, this.groundTilesCanvas.width * SCALE_FACTOR); + var drawH = Math.min(HEIGHT - (game_state == GAME.EDITOR ? MINIMAP_HEIGHT : INTERFACE_HEIGHT), this.groundTilesCanvas.height * SCALE_FACTOR); + c.drawImage(this.groundTilesCanvas, this.cameraX / SCALE_FACTOR, (this.cameraY + 2 * FIELD_SIZE) / SCALE_FACTOR, drawW / SCALE_FACTOR, drawH / SCALE_FACTOR, 0, 0, drawW, drawH); + + // calculate gameplay screen coords + var x1 = this.cameraX / FIELD_SIZE; + var y1 = this.cameraY / FIELD_SIZE; + var x2 = (this.cameraX + WIDTH) / FIELD_SIZE; + var y2 = (this.cameraY + HEIGHT) / FIELD_SIZE; + + // editor grid + if (game_state == GAME.EDITOR && $('#showGridCheckbox')[0].checked) { + c.lineWidth = 1; + c.strokeStyle = 'rgba(' + this.theme.line_red + ', ' + this.theme.line_green + ', ' + this.theme.line_blue + ', 0.7)'; + c.beginPath(); + + var y_ = Math.ceil(y1) * FIELD_SIZE - this.cameraY + 0.5; + + while (y_ < HEIGHT - INTERFACE_HEIGHT) { + c.moveTo(0, y_); + c.lineTo(WIDTH, y_); + + y_ += FIELD_SIZE; + } + + var x_ = Math.ceil(x1) * FIELD_SIZE - this.cameraX + 0.5; + + while (x_ < WIDTH) { + c.moveTo(x_, 0); + c.lineTo(x_, HEIGHT - INTERFACE_HEIGHT); + + x_ += FIELD_SIZE; + } + + c.stroke(); + c.closePath(); + + // make middle lines red + if (this.x / 2 > x1 && this.x / 2 < x2 && this.x % 2 == 0) { + var x2_ = (this.x / 2) * FIELD_SIZE - this.cameraX + 0.5; + + c.strokeStyle = 'rgba(255, 0, 0, 0.9)'; + c.beginPath(); + c.moveTo(x2_, 0); + c.lineTo(x2_, HEIGHT - INTERFACE_HEIGHT); + c.stroke(); + c.closePath(); + } + + // make middle lines red + if (this.y / 2 > y1 && this.y / 2 < y2 && this.y % 2 == 0) { + var y2_ = (this.y / 2) * FIELD_SIZE - this.cameraY + 0.5; + + c.strokeStyle = 'rgba(255, 0, 0, 0.9)'; + c.beginPath(); + c.moveTo(0, y2_); + c.lineTo(WIDTH, y2_); + c.stroke(); + c.closePath(); + } + } + + // calculate exact drawing positions (interpolate between real positions) + for (var i = 0; i < this.units.length; i++) { + this.units[i].updateDrawPosition(); + } + + // mark selected units + for (var i = 0; i < this.selectedUnits.length; i++) { + this.selectedUnits[i].lastSelectionTime = timestamp; + } + + // mark hover units + if (keyManager.drawBox) { + var x1n = (Math.min(keyManager.x, keyManager.startX) + this.cameraX) / FIELD_SIZE; + var x2n = (Math.max(keyManager.x, keyManager.startX) + this.cameraX) / FIELD_SIZE; + var y1n = (Math.min(keyManager.y, keyManager.startY) + this.cameraY) / FIELD_SIZE; + var y2n = (Math.max(keyManager.y, keyManager.startY) + this.cameraY) / FIELD_SIZE; + + for (var i = 0; i < unitsBuildings.length; i++) { + var u = unitsBuildings[i]; + + if (u.isAlive && u.isInBox(x1n, y1n + u.type.selectionOffsetY, x2n, y2n + u.type.selectionOffsetY) && PLAYING_PLAYER.team.canSeeUnit(u, true) && (PLAYING_PLAYER.team.canSeeUnitInvisible(u) || PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR)) { + u.lastHoverTime = timestamp; + } + } + } + + // mark curser hovering unit + else if (!keyManager.command || keyManager.command.targetIsUnit) { + hoverUnit = this.getUnitAtPosition((keyManager.x + this.cameraX) / FIELD_SIZE, (keyManager.y + this.cameraY) / FIELD_SIZE); + hoverUnit = (hoverUnit && PLAYING_PLAYER.team.canSeeUnit(hoverUnit, true)) ? hoverUnit : null; + + if (hoverUnit && (PLAYING_PLAYER.team.canSeeUnitInvisible(hoverUnit) || PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR)) { + hoverUnit.lastHoverTime = timestamp; + } + } + + + // selection circcles of tiles (if editor) + if (game_state == GAME.EDITOR) { + for (var i = 0; i < this.selectedUnits.length; i++) { + if (this.selectedUnits[i].type.isTile) { + drawCircle(this.selectedUnits[i].drawPos.px * FIELD_SIZE - this.cameraX, (this.selectedUnits[i].drawPos.py + this.selectedUnits[i].getValue('circleOffset')) * FIELD_SIZE - this.cameraY, this.selectedUnits[i].getValue('circleSize') * FIELD_SIZE, this.selectedUnits[i].owner.getAllyColor()); + } + } + } + + // draw all objects + var objectsToDraw = this.objectsToDraw.slice(); + for (var i = 0; i < objectsToDraw.length; i++) { + var u = objectsToDraw[i]; + + if (u.isInBoxVisible(x1, y1, x2, y2)) { + // selection circles or blinking circles + if (u.lastSelectionTime == timestamp && u.isThrowedUntil < ticksCounter) { + drawCircle(u.drawPos.px * FIELD_SIZE - this.cameraX, (u.drawPos.py + u.getValue('circleOffset')) * FIELD_SIZE - this.cameraY, u.getValue('circleSize') * FIELD_SIZE, u.owner.getAllyColor()); + } + + // hover circles + if (u.isThrowedUntil < ticksCounter && ((u.lastBlinkStart && u.lastBlinkStart + 1000 > timestamp && (timestamp - u.lastBlinkStart) % 200 < 100) || (u.lastHoverTime == timestamp && (!u.lastBlinkStart || u.lastBlinkStart + 1000 <= timestamp)))) { + drawCircle(u.drawPos.px * FIELD_SIZE - this.cameraX, (u.drawPos.py + u.getValue('circleOffset')) * FIELD_SIZE - this.cameraY, u.getValue('circleSize') * FIELD_SIZE, u.owner.getAllyColor(), u.owner.getAllyColor(0.4)); + } + + u.draw(x1, x2, y1, y2); + } + + if (u.isEffect && u.isExpired()) { + this.objectsToDraw.erease(u); + } + } + + // draw health bars + for (var i = 0; i < unitsBuildings.length; i++) { + var unit = unitsBuildings[i]; + + if (PLAYING_PLAYER.team.canSeeUnit(unit, true) && (PLAYING_PLAYER.team.canSeeUnitInvisible(unit) || PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR) && unit.isAlive && unit.isInBoxVisible(x1, y1, x2, y2)) { + var x = (unit.drawPos.px - unit.type.healthbarWidth / 2) * FIELD_SIZE - game.cameraX; + var y = (unit.drawPos.py - unit.type.healthbarOffset) * FIELD_SIZE - game.cameraY; + + if (!unit.type.isInvincible && (interface_.showFullHPBars.get() || unit.hp < unit.getValue('hp'))) { + unit.drawHealthbar(x, y, unit.type.healthbarWidth * FIELD_SIZE, 0.125 * FIELD_SIZE, SCALE_FACTOR / 2); + y -= FIELD_SIZE * 0.16; + } + + if (unit.type.mana) { + unit.drawManabar(x, y, unit.type.healthbarWidth * FIELD_SIZE, 0.125 * FIELD_SIZE, SCALE_FACTOR / 2); + y -= FIELD_SIZE * 0.16; + } + + if (unit.type.lifetime) { + unit.drawLifetimebar(x, y, unit.type.healthbarWidth * FIELD_SIZE, 0.125 * FIELD_SIZE, SCALE_FACTOR / 2); + y -= FIELD_SIZE * 0.16; + } + + if (unit.owner == PLAYING_PLAYER && unit.cargo && unit.cargo.length > 0) { + unit.drawLoadbar(x, y, unit.type.healthbarWidth * FIELD_SIZE, 0.125 * FIELD_SIZE, SCALE_FACTOR / 2); + y -= FIELD_SIZE * 0.16; + } + + if ((unit.owner == PLAYING_PLAYER || PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR) && unit.queue[0]) { + unit.drawProgressBar(x, y, unit.type.healthbarWidth * FIELD_SIZE, 0.125 * FIELD_SIZE, SCALE_FACTOR / 2); + } + } + + // draw workload + if (unit.gold && unit.type.startGold && unit.countWorkingWorkers > 0) { + var workload = unit.getWorkload(); + + drawText(c, 'Workload', 'white', 'bold ' + (SCALE_FACTOR * 6) + 'px LCDSolid', unit.drawPos.px * FIELD_SIZE - this.cameraX, (unit.drawPos.py - unit.type.healthbarOffset * 0.3) * FIELD_SIZE - this.cameraY, 999, 'center', null, 'rgba(0, 0, 0, 0.5)', null, (SCALE_FACTOR * 6)); + drawText(c, workload + ' %', 'white', 'bold ' + (SCALE_FACTOR * 6) + 'px LCDSolid', unit.drawPos.px * FIELD_SIZE - this.cameraX, (unit.drawPos.py - unit.type.healthbarOffset * 0.1) * FIELD_SIZE - this.cameraY, 999, 'center', null, 'rgba(0, 0, 0, 0.5)', null, (SCALE_FACTOR * 6)); + + c.strokeStyle = 'white'; + c.fillStyle = 'white'; + c.strokeRect(unit.drawPos.px * FIELD_SIZE - this.cameraX - FIELD_SIZE * 1.1, (unit.drawPos.py + unit.type.healthbarOffset * 0.0) * FIELD_SIZE - this.cameraY, FIELD_SIZE * 2.2, FIELD_SIZE * 0.3); + c.fillRect(unit.drawPos.px * FIELD_SIZE - this.cameraX - FIELD_SIZE * 1.05, (unit.drawPos.py + unit.type.healthbarOffset * 0.0 + 0.05) * FIELD_SIZE - this.cameraY, FIELD_SIZE * 2.1 * (workload / 100), FIELD_SIZE * 0.2); + } + } + + // draw waypoints + if (this.humanUnitsSelected() || PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR || game_state == GAME.EDITOR) { + c.lineWidth = 2; + + for (var i = 0; i < this.selectedUnits.length; i++) { + var unit = this.selectedUnits[i]; + + if (unit.waypoint && (unit.type.isBuilding || unit.queue[0])) { + for (var k = 0; k < unit.waypoint.length; k++) { + c.strokeStyle = 'rgba(' + this.theme.line_red + ', ' + this.theme.line_green + ', ' + this.theme.line_blue + ', ' + (1 - ((timestamp / 1000) % 0.8) * 0.8) + ')'; + + var point0 = (k > 0 ? (unit.waypoint[k - 1].pos ? unit.waypoint[k - 1].pos : unit.waypoint[k - 1]) : unit.pos); + point0 = point0.add3(0, -this.getHMValue3(point0) * CLIFF_HEIGHT); + + var point = unit.waypoint[k].pos ? unit.waypoint[k].pos : unit.waypoint[k]; + point = point.add3(0, -this.getHMValue3(point) * CLIFF_HEIGHT); + + c.beginPath(); + c.moveTo(point0.px * FIELD_SIZE - game.cameraX, (point0.py + (k == 0 ? (unit.getValue('circleOffset') / 2) : -Y_OFFSET)) * FIELD_SIZE - game.cameraY); + c.lineTo(point.px * FIELD_SIZE - game.cameraX, (point.py - Y_OFFSET) * FIELD_SIZE - game.cameraY); + c.stroke(); + + // circle effect + if (k == (unit.waypoint.length - 1) && ticksCounter % 8 == 0 && unit.lastTickCircleEffect != ticksCounter) { + new GroundOrder({ + from: point.add3(0, -Y_OFFSET), + }); + unit.lastTickCircleEffect = ticksCounter; + } + } + } + + if (unit.targetsQueue && unit.targetsQueue.length > 0 && unit.targetUnit) { + c.strokeStyle = 'rgba(164, 0, 0, ' + (1 - ((timestamp / 1000) % 0.8) * 0.8) + ')'; + + var targetsArray = [unit.targetUnit.drawPos]; + for (var k = 0; k < unit.targetsQueue.length; k++) { + targetsArray.push(unit.targetsQueue[k].drawPos); + } + + c.beginPath(); + c.moveTo(unit.drawPos.px * FIELD_SIZE - game.cameraX, (unit.drawPos.py + unit.getValue('circleOffset') / 2) * FIELD_SIZE - game.cameraY); + for (var k = 0; k < targetsArray.length; k++) { + c.lineTo(targetsArray[k].px * FIELD_SIZE - game.cameraX, (targetsArray[k].py + Y_OFFSET) * FIELD_SIZE - game.cameraY); + } + c.stroke(); + } + } + } + + // queued paths + if (this.humanUnitsSelected() && this.selectedUnits[0].type.isUnit) { + c.strokeStyle = 'rgba(' + this.theme.line_red + ', ' + this.theme.line_green + ', ' + this.theme.line_blue + ', 0.5)'; + + for (var i = 0; i < this.selectedUnits.length; i++) { + if (this.selectedUnits[i].queueOrder && this.selectedUnits[i].queueOrder.length > 0) { + var unit = this.selectedUnits[i]; + + var targetsArray = [unit.drawPos]; + if (unit.hasPath()) { + targetsArray.push(unit.path.add3(0, -this.getHMValue3(unit.path) * CLIFF_HEIGHT)); + } + + for (var k = 0; k < unit.queueOrder.length; k++) { + var target = unit.queueTarget[k]; + + if (target && target.isField) { + targetsArray.push(target.add3(0, -this.getHMValue3(target) * CLIFF_HEIGHT)); + } + + if (target && target.pos) { + targetsArray.push(target.drawPos); + } + } + + for (var k = 1; k < targetsArray.length; k++) { + c.beginPath(); + c.moveTo(targetsArray[k - 1].px * FIELD_SIZE - game.cameraX, (targetsArray[k - 1].py) * FIELD_SIZE - game.cameraY); + c.lineTo(targetsArray[k].px * FIELD_SIZE - game.cameraX, (targetsArray[k].py) * FIELD_SIZE - game.cameraY); + c.stroke(); + } + + // circle effect + if (ticksCounter % 8 == 0 && unit.lastTickCircleEffect != ticksCounter) { + new GroundOrder({ + from: targetsArray[targetsArray.length - 1].add3(0, 0), + }); + unit.lastTickCircleEffect = ticksCounter; + } + } + } + } + + // map pings + for (var i = 0; i < this.minimap.mapPings.length; i++) { + var ping = this.minimap.mapPings[i]; + + var age = Date.now() - ping.time; + + if (age > 7000) // ping is too old, kill it + { + this.minimap.mapPings.splice(i, 1); + i--; + } else { + var drawX = (ping.field.x - 0.5) * FIELD_SIZE - this.cameraX; + var drawY = (ping.field.y - 0.5) * FIELD_SIZE - this.cameraY; + + c.strokeStyle = 'rgba(255, 255, 0, ' + (age < 6000 ? 0.9 : Math.max((7000 - age) / 1000, 0)) + ')'; + c.lineWidth = 1.5 * SCALE_FACTOR; + + + var radius = FIELD_SIZE * 0.3 * Math.max(Math.sin(Math.PI * age / 250) + 1.5) / 2.5; + var radius2 = FIELD_SIZE * 0.3 * Math.max(Math.sin(Math.PI * age / 250 + 3.14 / 2) + 1.5) / 2.5; + + c.beginPath(); + c.strokeRect(drawX - radius, drawY - radius, 2 * radius, 2 * radius); + c.strokeRect(drawX - radius2, drawY - radius2, 2 * radius2, 2 * radius2); + c.stroke(); + + + var angles = [0, 90, 180, 270]; + + var sin = angles.map(function(s) { + return Math.sin(s / 180 * Math.PI + age / 5000 * Math.PI * 3); + }); + var cos = angles.map(function(s) { + return Math.cos(s / 180 * Math.PI + age / 5000 * Math.PI * 3); + }); + + var xs = [FIELD_SIZE, FIELD_SIZE, FIELD_SIZE / 2]; + var ys = [-FIELD_SIZE / 3, FIELD_SIZE / 3, 0]; + + for (var j = 0; j < 4; j++) { + c.beginPath(); + + c.moveTo(drawX + xs[0] * cos[j] - ys[0] * sin[j], drawY + ys[0] * cos[j] + xs[0] * sin[j]); + c.lineTo(drawX + xs[1] * cos[j] - ys[1] * sin[j], drawY + ys[1] * cos[j] + xs[1] * sin[j]); + c.lineTo(drawX + xs[2] * cos[j] - ys[2] * sin[j], drawY + ys[2] * cos[j] + xs[2] * sin[j]); + + c.closePath(); + c.stroke(); + } + } + } + + if (game_state == GAME.PLAYING) { + this.env.draw(); + this.rain.draw(); + } + + // draw fog + c.drawImage(game_state == GAME.PLAYING ? this.minimap.screenCanvas : this.minimap.editorCanvas, this.cameraX / SCALE_FACTOR, this.cameraY / SCALE_FACTOR, drawW / SCALE_FACTOR, drawH / SCALE_FACTOR, 0, 0, drawW, drawH); + + // if cursor is hovering a unit, draw the units owners name (if not playing player) + if (hoverUnit && ((hoverUnit.owner != PLAYING_PLAYER && hoverUnit.owner.number > 0) || (hoverUnit.type.hoverText && hoverUnit.type.hoverText.length > 0))) { + var y = hoverUnit.drawPos.py * FIELD_SIZE - this.cameraY; + y += hoverUnit.type.isUnit ? FIELD_SIZE : ((hoverUnit.type.sizeY / 2 + 0.7) * FIELD_SIZE); + + var text = (hoverUnit.type.hoverText && hoverUnit.type.hoverText.length > 0) ? hoverUnit.type.hoverText : hoverUnit.owner.name; + + if (game_state == GAME.EDITOR) { + text = hoverUnit.type.name + ' (x: ' + (parseInt(hoverUnit.pos.px * 100) / 100) + ', y: ' + (parseInt(hoverUnit.pos.py * 100) / 100) + ')'; + } + + drawText(c, text, 'white', 'bold ' + (4 * 6) + 'px LCDSolid', hoverUnit.drawPos.px * FIELD_SIZE - this.cameraX, y, 300, 'center', 1, 'rgba(0, 0, 0, 0.5)', null, (4 * 6)); + } + + // if game paused, draw black screen with opac + if (game_paused) { + c.fillStyle = 'rgba(0, 0, 0, 0.5)'; + c.fillRect(0, 0, WIDTH, HEIGHT); + } + + // print path + if (path2Print) { + for (var i = 1; i < path2Print.length; i++) { + c.beginPath(); + c.moveTo(path2Print[i - 1].px * FIELD_SIZE - game.cameraX, (path2Print[i - 1].py) * FIELD_SIZE - game.cameraY); + c.lineTo(path2Print[i].px * FIELD_SIZE - game.cameraX, (path2Print[i].py) * FIELD_SIZE - game.cameraY); + c.stroke(); + } + } + + /* + for(var x = parseInt(x1); x < x2; x++) + for(var y = parseInt(y1); y < y2; y++) + { + drawText(c, x + ":" + y, "white", 12, (x - x1) * FIELD_SIZE, (y - y1) * FIELD_SIZE); + drawText(c, "hm: " + this.fields[x][y].hm, "white", 12, (x - x1) * FIELD_SIZE, (y - y1) * FIELD_SIZE + 16); + drawText(c, "hm2: " + this.fields[x][y].hm2, "white", 12, (x - x1) * FIELD_SIZE, (y - y1) * FIELD_SIZE + 32); + drawText(c, "hm4: " + this.fields[x][y].hm4, "white", 12, (x - x1) * FIELD_SIZE, (y - y1) * FIELD_SIZE + 48); + } + */ + + // draw minimap + this.minimap.draw(); +}; + + +// pos has influence on the volume; if a pos is given, the distance from the screen to pos is checked, and the volume is lower, the higher the distance is +Game.prototype.getVolumeModifier = function(pos) { + var volume = 1; + + var left = game.cameraX / FIELD_SIZE; + var top = game.cameraY / FIELD_SIZE; + var right = (game.cameraX + WIDTH) / FIELD_SIZE; + var bottom = (game.cameraY + HEIGHT - INTERFACE_HEIGHT) / FIELD_SIZE; + + var distX = 0; + if (pos.px < left || pos.px > right) { + distX = Math.min(Math.abs(pos.px - left), Math.abs(pos.px - right)); + } + + var distY = 0; + if (pos.py < top || pos.py > bottom) { + distY = Math.min(Math.abs(pos.py - top), Math.abs(pos.py - bottom)); + } + + var dist = Math.sqrt(distX * distX + distY * distY); + + if (dist > 0) { + volume = 1 - dist / 5; + } + + // make sounds a little bit lower, when zoomed out + volume *= Math.min(SCALE_FACTOR / 15 + 0.6, 1); + + return volume; +}; + +//IDLEWORKERINDEX is shared between interface and keymanager +IDLEWORKERINDEX = 0; +// input manager, manages all the inputs +// TODO: rename this to InputManager to better reflect what it does +function KeyManager() { + this.reset(); + + // Register hotkeys that the KeyManager is responsible for + this.interfaceHotkeys = new HotkeyGroup('Interface Hotkeys') + .addChild(new HotkeySetting('Queue actions', KEY.SHIFT, 'queue')) + .addChild(new HotkeySetting('Select all idle workers', KEY.PERIOD, 'selectallidle')) + .addChild(new HotkeySetting('Select and go to next idle worker', KEY.COMMA, 'selectnextidle')) + .addChild(new HotkeySetting('Select all idle army units', KEY.SEMICOLON, 'selectallidlearmy')) + .addChild(new HotkeySetting('Select all army units', KEY.QUOTE, 'selectallarmy')) + .addChild(new HotkeySetting('Select all units of matching type', KEY.CTRL, 'selectall')) + .addChild(new HotkeySetting('Add / remove units from selection', KEY.SHIFT, 'toggleselection')) + .addChild(new HotkeySetting('Toggle chat between all and allies', KEY.SHIFT, 'togglechat')) + .addChild(new HotkeySetting('Zoom in', KEY.O, 'zoomin')) + .addChild(new HotkeySetting('Zoom out', KEY.L, 'zoomout')) + .addChild(new HotkeySetting('Map ping', KEY.K, 'ping')); + Hotkeys.registerHotkeyGroup(this.interfaceHotkeys, true); + + this.controlGroupHotkeys = new HotkeyGroup('Control Groups', false, 'Set with [set] + [key], add units with [add] + [key], and select with [key]'); + this.controlGroupHotkeys.addChild(new HotkeySetting('Set control group modifier [set]', KEY.CTRL, 'set')); + this.controlGroupHotkeys.addChild(new HotkeySetting('Add to control group modifier [add]', KEY.SHIFT, 'add')); + this.controlGroupHotkeys.addChild(new HotkeySetting('Create control group and take away units modifier [set]', KEY.ALT, 'removeandcreate')); + this.controlGroupHotkeys.addChild(new HotkeySetting('Add to control group and take away units modifier [add]', KEY.CAPSLOCK, 'removeandadd')); + for (let i = 1; i <= 10; i++) { + const key = i == 10 ? 0 : i; + this.controlGroupHotkeys.addChild(new HotkeySetting(`CTRL Group ${i}`, KEY[`NUM${key}`]).setData('num', i)); + } + for (let i = 11; i <= 20; i++) { + const key = i - 11; + this.controlGroupHotkeys.addChild(new HotkeySetting(`CTRL Group ${i}`, KEY[`NUMPAD${key}`]).setData('num', i)); + } + Hotkeys.registerHotkeyGroup(this.controlGroupHotkeys); + + this.cameraHotkeys = new HotkeyGroup('Camera Locations', false, 'Set with [set] + [key], and move to location with [key]'); + this.cameraHotkeys.addChild(new HotkeySetting('Set camera location modifier [set]', KEY.CTRL, 'set')); + for (let i = 1; i <= 6; i++) { + this.cameraHotkeys.addChild(new HotkeySetting(`Camera Location ${i}`, KEY[`F${i}`]).setData('num', i)); + } + Hotkeys.registerHotkeyGroup(this.cameraHotkeys); + + // Register relevant configuration values + this.mmScrollInvert = LocalConfig.registerValue('mm_scroll_invert', false); +}; + +KeyManager.prototype.reset = function() { + this.keys = new Array(300); // stores which keys are pressed atm (for active button effect) (index = asci code, true for button is pressed) + this.drawBox = false; // use to check if selection box has to be drawn + this.leftMouse = false; // is leftmouse is pressed down atm, for button pressed graphic effect + this.middleMouse = false; // if true, activate map scrolling + this.minimapScroll = false; // if clicking in the minimap, set this true as long as leftmouse is true + this.timeOfLastKeyPressed = 0; // to check for double press + this.lastKeyPressed = 0; + this.commandCardWhenPressStart = 0; + + this.cursor = Cursors.DEFAULT; + this.setCursor(this.cursor); + this.x = 0; // x mouse pos + this.y = 0; // y mouse pos + this.startX = 0; // when box is drawn, store start X + this.startY = 0; // when box is drawn, store start Y + this.command = null; // active command + + this.controlGroups = new Array(11); + this.cameraLocations = new Array(11); + for (let i = 0; i < this.controlGroups.length; i++) { + this.controlGroups[i] = []; + } + + this.listeners = {}; +}; + +KeyManager.prototype.draw = function() { + if (this.drawBox && (game_state != GAME.EDITOR || !editor.dragging)) // Leftmouse is pressed, so we draw a box + { + c.fillStyle = 'rgba(150, 190, 255, 0.4)'; + c.fillRect(this.x, this.y, this.startX - this.x, this.startY - this.y); + c.strokeStyle = 'rgba(0, 50, 133, 0.8)'; + c.strokeRect(this.x, this.y, this.startX - this.x, this.startY - this.y); + } + + // Aoe cursor + if (this.command && this.command.useAoeCursor && game.selectedUnits[0]) { + var radius = this.command.getValue('aoeRadius', game.selectedUnits[0]) * FIELD_SIZE; + drawCircle(this.x, this.y, radius, null, 'rgba(' + game.theme.line_red + ', ' + game.theme.line_green + ', ' + game.theme.line_blue + ', 0.4)', 0.8); + } + + // call editor click function every frame (as if we would click all the time, because editor can draw multiple stuff, when mouse is clicked) + if (game_state == GAME.EDITOR && this.leftMouse) { + editor.click(this.x, this.y, false, 1); + } +}; + +// Registers a key listener under a non-unique ID +KeyManager.prototype.registerListener = function(key, id, listener) { + if (!this.listeners[key]) { + this.listeners[key] = {}; + } + this.listeners[key][id] = listener; +}; + +// Removes all listeners with the provided ID +KeyManager.prototype.removeListener = function(id) { + for (const key in this.listeners) { + delete this.listeners[key]?.[id]; + } +}; + +KeyManager.prototype.callListeners = function(key) { + if (this.listeners[key]) { + for (let id in this.listeners[key]) { + this.listeners[key][id](); + } + } +}; + +KeyManager.prototype.interfaceHotkeyPressed = function(name) { + return this.keys[this.interfaceHotkeys.getHotkeyValue(name)]; +}; + +KeyManager.prototype.changeUnitSelection = function(oldType) { + if ((game.selectedUnits[0] && game.selectedUnits[0].type == oldType) || game_state != GAME.PLAYING) { + return; + } + + if (oldType && oldType.clickSound) { + while (soundManager.buildingClickSound.length > 1) { + soundManager.buildingClickSound[0].sound.pause(); + soundManager.buildingClickSound[0].sound.volume = 0; + soundManager.buildingClickSound[0].sound.currentTime = 0; + soundManager.buildingClickSound.splice(0, 1); + } + + if (soundManager.buildingClickSound[0]) { + soundManager.buildingClickSound[0].fadeOut = true; + setTimeout(soundFaceOut, 1); + } + } + + if (game.selectedUnits[0] && game.selectedUnits[0].owner == PLAYING_PLAYER && game.selectedUnits[0].type.clickSound) { + soundManager.playSound( + game.selectedUnits[0].isUnderConstruction ? SOUND.BUILD : game.selectedUnits[0].type.clickSound, + 0.5, + game.selectedUnits[0].isUnderConstruction ? 0.5 : game.selectedUnits[0].type.clickSoundVolume, + true, + ); + } +}; + +// replace the cursor with tho normal one +KeyManager.prototype.resetCommand = function() { + this.command = null; + this.setCursor(Cursors.DEFAULT); +}; + +KeyManager.prototype.setCursor = function(cursor) { + // TODO: might want to store this in interface + this.cursor = cursor; + document.body.style.cursor = `url(${CursorFiles[this.cursor]}), auto`; +}; + +KeyManager.prototype.getCursor = function() { + return this.cursor; +}; + +// a command is started +KeyManager.prototype.order = function(cmd, learn) { + if (learn) { + game.issueOrderToUnits(game.selectedUnits, cmd, null, this.interfaceHotkeyPressed('queue'), false, null, true); + return; + } + + if (!this.interfaceHotkeyPressed('queue')) { + this.resetCommand(); + } + + if (cmd.type == COMMAND.SWITCH_CC) { + interface_.commandCard = cmd.targetCC; + } + + // instant order: execute them + else if (cmd.isInstant) { + game.issueOrderToUnits(game.selectedUnits, cmd, null, this.interfaceHotkeyPressed('queue')); + } else if (game.selectedUnitsCanPerformOrder(cmd, interface_.unitTypeWithCurrentTabPrio)) { + if (cmd.cursor && cmd.cursor in CursorFiles) { + this.setCursor(cmd.cursor); + } + + if (cmd.type == COMMAND.MAKEBUILDING && PLAYING_PLAYER.gold < cmd.unitType.getValue('cost', PLAYING_PLAYER)) { + interface_.addMessage('not enough gold (' + Math.ceil(cmd.unitType.getValue('cost', PLAYING_PLAYER) - PLAYING_PLAYER.gold) + ' missing)', 'red', imgs.attentionmark); + soundManager.playSound(SOUND.NEGATIVE); + } else if (cmd.type == COMMAND.MAKEBUILDING && cmd.unitType.supply && cmd.unitType.supply > PLAYING_PLAYER.maxSupply - PLAYING_PLAYER.supply) { + interface_.addMessage('not enough supply', 'red', imgs.attentionmark); + soundManager.playSound(SOUND.NEGATIVE); + } else { + this.command = cmd; + } + } +}; + +KeyManager.prototype.getKeyCode = function(e) { + return e.which || e.keyCode; +}; + +// create object +var keyManager = new KeyManager(); + +document.onkeyup = function(e) { + keyManager.keys[keyManager.getKeyCode(e)] = false; + return false; +}; + +document.onkeydown = function(e) { + if (!e.repeat) { + keyManager.commandCardWhenPressStart = interface_.commandCard; + } + + var key = keyManager.getKeyCode(e); + + if (key == KEY.F8) { + toggleFullscreen(document.documentElement); + } + + // if theres ui windows open, return because no delegation to the game + if (document.activeElement && ((document.activeElement.nodeName == 'INPUT' && document.activeElement.type == 'text') || document.activeElement.nodeName == 'TEXTAREA') && document.activeElement.style.visibility != 'hidden' && document.activeElement.offsetParent !== null) { + return true; + } + + keyManager.callListeners(key); + uimanager.onKey(key); + + // if were not ingame, return, because no delegation to the game + if (game_state != GAME.PLAYING && game_state != GAME.EDITOR) { + return true; + } + + keyManager.keys[key] = true; + + // zoom via keys + if ((game_state == GAME.PLAYING || game_state == GAME.EDITOR) && key == keyManager.interfaceHotkeys.getHotkeyValue('zoomin')) { + zoom(120); + } + + if ((game_state == GAME.PLAYING || game_state == GAME.EDITOR) && key == keyManager.interfaceHotkeys.getHotkeyValue('zoomout')) { + zoom(-120); + } + + if (game_state == GAME.EDITOR) { + editor.keyPressed(key); + return false; + } + + // map ping + if (game_state == GAME.PLAYING && key == keyManager.interfaceHotkeys.getHotkeyValue('ping')) { + var f = null; + + if (keyManager.x < MINIMAP_WIDTH && keyManager.y > HEIGHT - MINIMAP_HEIGHT) // if click in minimap + { + f = game.minimap.getFieldFromClick(keyManager.x, keyManager.y); + } else if (keyManager.y < HEIGHT - INTERFACE_HEIGHT) // if click in main screen map + { + f = game.getFieldFromPos(); + } + + if (f) { + if (network_game) { + network.send('map-ping<<$' + f.x + '<<$' + f.y); + } else if (game.minimap.mapPings.length < 3) { + game.minimap.mapPings.push({ field: f, time: Date.now() }); + soundManager.playSound(SOUND.BING2); + } + } + } + + // open ingame chat input and set all/allies dropdown + if (game_state == GAME.PLAYING && key == KEY.ENTER && !uimanager.ingameInput.active) { + uimanager.ingameInput.active = true; + + // check, if playing player has allies + var playingPlayerHasAllies = false; + for (var i = 1; i < game.players.length; i++) { + if (game.players[i] && game.players[i] != PLAYING_PLAYER && !game.players[i].isEnemyOfPlayer(PLAYING_PLAYER) && game.players[i].controller != CONTROLLER.SPECTATOR && game.players[i].isAlive) { + playingPlayerHasAllies = true; + } + } + + // if is spectator, then always send to all + if (PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR) { + playingPlayerHasAllies = false; + } + + // if shift pressed, set the opposite + var setDropdown = keyManager.interfaceHotkeyPressed('togglechat') ? !playingPlayerHasAllies : playingPlayerHasAllies; + + $('#ingameChatDropdown')[0].selectedIndex = setDropdown ? 1 : 0; + } + + // tell interface, a key has been pressed + if (interface_ && keyManager.commandCardWhenPressStart == interface_.commandCard) { + interface_.keyPressed(key); + } + + // pause + if (key == KEY.PAUSE && game_state == GAME.PLAYING) { + pauseGame(); + if (game_paused) { + $('#replayPlayButton').text('||').css({ 'font-size': '10px' }); + } else { + $('#replayPlayButton').text('>').css({ 'font-size': '' }); + } + } + + // if idle worker hotkey stuff + if (game_state == GAME.PLAYING && game.playerHasIdleWorkers(PLAYING_PLAYER)) { + if (key == keyManager.interfaceHotkeys.getHotkeyValue('selectallidle')) { + var workers = []; + for (var i = 0; i < game.units.length; i++) { + if (game.units[i].owner == PLAYING_PLAYER && game.units[i].type == lists.types.worker && game.units[i].order && game.units[i].order.type == COMMAND.IDLE) { + workers.push(game.units[i]); + } + } + if (workers.length > 0) { + game.selectedUnits = workers; + } + } + else if (key == keyManager.interfaceHotkeys.getHotkeyValue('selectnextidle')) { + var workers = []; + for (var i = 0; i < game.units.length; i++) { + if (game.units[i].owner == PLAYING_PLAYER && game.units[i].type == lists.types.worker && game.units[i].order && game.units[i].order.type == COMMAND.IDLE) { + workers.push(game.units[i]); + } + } + if (workers.length > 0) { + IDLEWORKERINDEX = (IDLEWORKERINDEX + 1) % workers.length; + var worker = workers[IDLEWORKERINDEX]; + game.selectedUnits = [worker]; + game.setCameraX(worker.pos.px * FIELD_SIZE - WIDTH / 2); + game.setCameraY(worker.pos.py * FIELD_SIZE - HEIGHT / 2); + } + } + } + + // if select all army hotkey stuff + if (game_state == GAME.PLAYING && key == keyManager.interfaceHotkeys.getHotkeyValue('selectallarmy')) { + var armyunits = []; + for (var i = 0; i < game.units.length; i++) { + if (game.units[i].owner == PLAYING_PLAYER && game.units[i].type != lists.types.worker && !game.units[i].type.isBuilding) { + armyunits.push(game.units[i]); + } + } + if (armyunits.length > 0) { + game.selectedUnits = armyunits; + } + } + + if (game_state == GAME.PLAYING && key == keyManager.interfaceHotkeys.getHotkeyValue('selectallidlearmy')) { + var idlearmyunits = []; + for (var i = 0; i < game.units.length; i++) { + if (game.units[i].owner == PLAYING_PLAYER && game.units[i].type != lists.types.worker && !game.units[i].type.isBuilding && game.units[i].order && game.units[i].order.type == COMMAND.IDLE) { + idlearmyunits.push(game.units[i]); + } + } + if (idlearmyunits.length > 0) { + game.selectedUnits = idlearmyunits; + } + } + + // if camera location hotkey stuff + let cameraHotkey = 0; + keyManager.cameraHotkeys.forEach((hotkey, i) => { + if (key == hotkey.value) { + cameraHotkey = hotkey.getData('num'); + } + }); + + if (game_state == GAME.PLAYING && cameraHotkey > 0) { + const nr = cameraHotkey; + + if (nr < keyManager.cameraLocations.length) { + // if modifier key pressed, set camera location + if (keyManager.keys[keyManager.cameraHotkeys.getHotkeyValue('set')]) { + keyManager.cameraLocations[nr] = new Field((game.cameraX + WIDTH / 2) / FIELD_SIZE, (game.cameraY + HEIGHT / 2) / FIELD_SIZE, true); + } + + // else, go to camera location + else if (keyManager.cameraLocations[nr]) { + game.setCameraX(keyManager.cameraLocations[nr].px * FIELD_SIZE - WIDTH / 2); + game.setCameraY(keyManager.cameraLocations[nr].py * FIELD_SIZE - HEIGHT / 2); + } + } + } + + // if ctrl group stuff + let ctrlGroupKey = 0; + keyManager.controlGroupHotkeys.forEach((hotkey, i) => { + if (key == hotkey.value) { + ctrlGroupKey = hotkey.getData('num'); + } + }); + + if (game_state == GAME.PLAYING && ctrlGroupKey > 0) { + const nr = ctrlGroupKey; + + // create new ctrl grp + if (keyManager.keys[keyManager.controlGroupHotkeys.getHotkeyValue('set')] && game.humanUnitsSelected()) { + keyManager.controlGroups[nr] = game.selectedUnits; + } + + // add units to ctrl grp + else if (keyManager.keys[keyManager.controlGroupHotkeys.getHotkeyValue('add')] && game.humanUnitsSelected()) { + for (var i = 0; i < game.selectedUnits.length; i++) { + var contains = false; + for (var k = 0; k < keyManager.controlGroups[nr].length; k++) { + if (game.selectedUnits[i] == keyManager.controlGroups[nr][k]) { + contains = true; + } + } + + if (!contains && game.selectedUnits[i].owner == PLAYING_PLAYER && (keyManager.controlGroups[nr].length == 0 || keyManager.controlGroups[nr][0].type.isBuilding == game.selectedUnits[i].type.isBuilding)) { + keyManager.controlGroups[nr].push(game.selectedUnits[i]); + } + } + } + + // remove units from ctrl grps and create to ctrl grp + else if (keyManager.keys[keyManager.controlGroupHotkeys.getHotkeyValue('removeandcreate')] && game.humanUnitsSelected()) { + for (let i = 0; i < game.selectedUnits.length; i++) { + if (game.selectedUnits[i].owner == PLAYING_PLAYER) { + for (let j = 1; j <= 10; j++) { + var unitsToRemove = []; + for (let k = 0; k < keyManager.controlGroups[j].length; k++) { + if (game.selectedUnits[i] == keyManager.controlGroups[j][k]) { + unitsToRemove.push(k) + } + } + for (const k of unitsToRemove) { + keyManager.controlGroups[j].splice(k, 1); + } + } + } + } + keyManager.controlGroups[nr] = game.selectedUnits; + } + + // remove units from ctrl grps and add to ctrl grp + else if (keyManager.keys[keyManager.controlGroupHotkeys.getHotkeyValue('removeandadd')] && game.humanUnitsSelected()) { + for (let i = 0; i < game.selectedUnits.length; i++) { + if (game.selectedUnits[i].owner == PLAYING_PLAYER && (keyManager.controlGroups[nr].length == 0 || keyManager.controlGroups[nr][0].type.isBuilding == game.selectedUnits[i].type.isBuilding)) { + var contains = false; + for (let j = 1; j <= 10; j++) { + var unitsToRemove = []; + for (let k = 0; k < keyManager.controlGroups[j].length; k++) { + if (game.selectedUnits[i] == keyManager.controlGroups[j][k]) { + if (j != nr) { + unitsToRemove.push(k) + } + else { + contains = true; + } + } + } + for (const k of unitsToRemove) { + keyManager.controlGroups[j].splice(k, 1); + } + } + if (!contains) { + keyManager.controlGroups[nr].push(game.selectedUnits[i]); + } + } + } + } + + // select ctrl grp + else if (keyManager.controlGroups[nr].length > 0) { + var oldType = game.selectedUnits[0] ? game.selectedUnits[0].type : null; + game.selectedUnits = keyManager.controlGroups[nr].slice(); + + interface_.unitsTab = 0; + + keyManager.resetCommand(); + + // deselct units, that are currently "not active" (= means, they are in buildings for example) + for (var i = 0; i < game.selectedUnits.length; i++) { + if (!game.selectedUnits[i].isActive) { + game.selectedUnits.splice(i, 1); + i--; + } + } + + interface_.commandCard = 0; // reset command card (in case we were in the building submenu) + interface_.unitTypeWithCurrentTabPrio = null; // reset command card prio + + // if double pressed this ctrl grp, jump camera there + if (keyManager.lastKeyPressed == key && keyManager.timeOfLastKeyPressed + 350 > timestamp && game.selectedUnits.length > 0) { + var point = game.getCenterOfUnits(game.selectedUnits); + + var nextUnit = null; + var closestDistance = 9999; + for (var i = 0; i < game.selectedUnits.length; i++) { + var distance = point.distanceTo2(game.selectedUnits[i].pos); + if (distance < closestDistance) { + closestDistance = distance; + nextUnit = game.selectedUnits[i]; + } + } + + game.setCameraX(nextUnit.pos.px * FIELD_SIZE - WIDTH / 2); + game.setCameraY(nextUnit.pos.py * FIELD_SIZE - HEIGHT / 2); + } + + keyManager.changeUnitSelection(oldType); + } + } + + keyManager.timeOfLastKeyPressed = timestamp; + keyManager.lastKeyPressed = key; + + return false; +}; + +// IE9, Chrome, Safari, Opera +canvas.addEventListener('mousewheel', MouseWheelHandler, false); + +// Firefox +canvas.addEventListener('DOMMouseScroll', MouseWheelHandler, false); + +function MouseWheelHandler(e) { + // set zoom level + if (game_state == GAME.PLAYING || game_state == GAME.EDITOR) { + var e = window.event || e; + zoom(e.wheelDelta || -e.detail); + } +}; + +function zoom(direction) { + // middle of camera + var middle_x = (game.cameraX + WIDTH / 2) / FIELD_SIZE; + var middle_y = (game.cameraY + HEIGHT / 2) / FIELD_SIZE; + + if (direction > 0) { + setScaleFactor(SCALE_FACTOR + 1); + } else { + setScaleFactor(SCALE_FACTOR - 1); + } + + FIELD_SIZE = 16 * SCALE_FACTOR; + + // adjust camera + game.setCameraX(middle_x * FIELD_SIZE - WIDTH / 2); + game.setCameraY(middle_y * FIELD_SIZE - HEIGHT / 2); +} + +canvas.onmousedown = function(e) { + if (game_paused && PLAYING_PLAYER.controller != CONTROLLER.SPECTATOR) { + return; + } + + // leftmouse + if (keyManager.getKeyCode(e) == 1) { + keyManager.leftMouse = true; + } + + // if click in minimap + if (keyManager.x < MINIMAP_WIDTH && keyManager.y > HEIGHT - MINIMAP_HEIGHT && (game_state == GAME.PLAYING || game_state == GAME.EDITOR)) { + var clickedPos = game.minimap.getFieldFromClick(keyManager.x, keyManager.y); + + // leftmouse + if (keyManager.getKeyCode(e) == 1) { + // if no command is active, set minimap scroll enabled + if (!keyManager.command) { + keyManager.minimapScroll = true; + } + + // if attack order has been issued + else if (keyManager.command.type == COMMAND.ATTACK && game.humanUnitsSelected()) { + game.issueOrderToUnits(game.selectedUnits, lists.types.amove, clickedPos, keyManager.interfaceHotkeyPressed('queue')); + new GroundOrder({ from: clickedPos.add3(0, Y_OFFSET) }); + keyManager.resetCommand(); + } + } + + // rightmouse & no command active, order move command + else if (keyManager.getKeyCode(e) == 3 && !keyManager.command && game.humanUnitsSelected()) { + game.issueOrderToUnits(game.selectedUnits, lists.types.move, clickedPos, keyManager.interfaceHotkeyPressed('queue')); + new GroundOrder({ from: clickedPos.add3(0, Y_OFFSET) }); + } + + // return false; + return; + } + + // if editor, delegate there + if (game_state == GAME.EDITOR) { + editor.click(keyManager.x, keyManager.y, true, keyManager.getKeyCode(e)); + } + + // if left click in interface and hit something (returns true), return + if (!keyManager.command && keyManager.getKeyCode(e) == 1 && game_state == GAME.PLAYING && interface_.leftClick(keyManager.x, keyManager.y)) { + return; + } + + // if right click in interface and hit something (returns true), return + if (!keyManager.command && keyManager.getKeyCode(e) == 3 && game_state == GAME.PLAYING && interface_.rightClick(keyManager.x, keyManager.y)) { + return; + } + + // if click in screen map + if ((keyManager.y < HEIGHT - INTERFACE_HEIGHT || keyManager.command) && (game_state == GAME.PLAYING || game_state == GAME.EDITOR)) { + var field = game.getFieldFromPos(); + + // activate map scrolling + if (keyManager.getKeyCode(e) == 2) { + keyManager.middleMouse = true; + } + + // leftmouse + else if (keyManager.getKeyCode(e) == 1) { + // if no active command && if editor, then only when no button selected + if ((game_state == GAME.PLAYING && !keyManager.command) || (game_state == GAME.EDITOR && !editor.selectedItemType && editor.terrainModifier == 0)) { + keyManager.drawBox = true; + keyManager.startX = keyManager.x; + keyManager.startY = keyManager.y; + } + + // attack order has been issued + else if (keyManager.command && keyManager.command.type == COMMAND.ATTACK && game.humanUnitsSelected()) { + var targetUnit = (interface_ && interface_.currentHoverUnit) ? interface_.currentHoverUnit : game.getUnitAtPosition((keyManager.x + game.cameraX) / FIELD_SIZE, (keyManager.y + game.cameraY) / FIELD_SIZE); + + // AMove + if (!targetUnit || targetUnit.getValue('noShow')) { + game.issueOrderToUnits(game.selectedUnits, lists.types.amove, field.add3(0, -Y_OFFSET), keyManager.interfaceHotkeyPressed('queue')); + new GroundOrder({ from: new Field((keyManager.x + game.cameraX) / FIELD_SIZE, (keyManager.y + game.cameraY + 3) / FIELD_SIZE, true) }); + } + + // Attack specific unit order + else { + game.issueOrderToUnits(game.selectedUnits, lists.types.attack, targetUnit, keyManager.interfaceHotkeyPressed('queue')); + targetUnit.blink(); + } + + keyManager.resetCommand(); + } + + // make building order has been issued + else if (keyManager.command && keyManager.command.type == COMMAND.MAKEBUILDING && game.humanUnitsSelected()) { + // get building type and field + var building = keyManager.command.unitType; + var field = building.getFieldFromMousePos(); + + // if building has space, issue order + if (building.couldBePlacedAt(field, true)) { + var cost = PLAYING_PLAYER.getCostOfNextInstanceForBuilding(building); + + // player has enough gold to make building + if (PLAYING_PLAYER.gold >= cost) { + if (!building.supply || building.supply <= PLAYING_PLAYER.maxSupply - PLAYING_PLAYER.supply) { + game.issueOrderToUnits(game.selectedUnits, keyManager.command, field, keyManager.interfaceHotkeyPressed('queue')); + + if (!keyManager.interfaceHotkeyPressed('queue')) { + keyManager.resetCommand(); + } + + soundManager.playSound(SOUND.PLACE); + } else { + interface_.addMessage('not enough supply', 'red', imgs.attentionmark); + soundManager.playSound(SOUND.NEGATIVE); + } + } else { + interface_.addMessage('not enough gold (' + Math.ceil(cost - PLAYING_PLAYER.gold) + ' missing)', 'red', imgs.attentionmark); + soundManager.playSound(SOUND.NEGATIVE); + } + } + + // if no space for building at this position, display warning msg and play sound + else { + interface_.addMessage('no space', 'red', imgs.attentionmark); + soundManager.playSound(SOUND.NEGATIVE); + } + } + + // if unload + else if (keyManager.command && keyManager.command.type == COMMAND.UNLOAD && game.humanUnitsSelected()) { + var targetUnit = (interface_ && interface_.currentHoverUnit) ? interface_.currentHoverUnit : game.getUnitAtPosition((keyManager.x + game.cameraX) / FIELD_SIZE, (keyManager.y + game.cameraY) / FIELD_SIZE); + game.issueOrderToUnits(game.selectedUnits, keyManager.command, (targetUnit && game.selectedUnits.contains(targetUnit)) ? targetUnit : field, keyManager.interfaceHotkeyPressed('queue')); + if (!targetUnit || !game.selectedUnits.contains(targetUnit)) { + new GroundOrder({ from: new Field((keyManager.x + game.cameraX) / FIELD_SIZE, (keyManager.y + game.cameraY + 3) / FIELD_SIZE, true) }); + } + keyManager.resetCommand(); + } + + // if target is point + else if (keyManager.command && (keyManager.command.targetIsPoint || keyManager.command.type == COMMAND.TELEPORT) && game.humanUnitsSelected()) { + game.issueOrderToUnits(game.selectedUnits, keyManager.command, field, keyManager.interfaceHotkeyPressed('queue')); + new GroundOrder({ from: new Field((keyManager.x + game.cameraX) / FIELD_SIZE, (keyManager.y + game.cameraY + 3) / FIELD_SIZE, true) }); + keyManager.resetCommand(); + } else if (keyManager.command && keyManager.command.targetIsUnit && game.humanUnitsSelected()) { + var targetUnit = (interface_ && interface_.currentHoverUnit) ? interface_.currentHoverUnit : game.getUnitAtPosition((keyManager.x + game.cameraX) / FIELD_SIZE, (keyManager.y + game.cameraY) / FIELD_SIZE, keyManager.command.targetRequiremementsArray.length > 0 ? keyManager.command : null); + + if (targetUnit && targetUnit.getValue('noShow')) { + targetUnit = null; + } + + if (targetUnit && keyManager.command.type == COMMAND.LOAD && targetUnit.owner != game.selectedUnits[0].owner) { + interface_.addMessage('can only target own units', 'red', imgs.attentionmark); + soundManager.playSound(SOUND.NEGATIVE); + } else if (targetUnit) { + game.issueOrderToUnits(game.selectedUnits, keyManager.command, targetUnit, keyManager.interfaceHotkeyPressed('queue')); + targetUnit.blink(); + keyManager.resetCommand(); + } else { + interface_.addMessage('must target unit', 'red', imgs.attentionmark); + soundManager.playSound(SOUND.NEGATIVE); + } + } + } + + // rightmouse (and were ingame; rightmouse only works when ingame) + if (keyManager.getKeyCode(e) == 3 && game_state == GAME.PLAYING) { + e.preventDefault(); + e.stopPropagation(); + // if no command has been clicked -> move / attack / waypoint + if (!keyManager.command && game && game.humanUnitsSelected()) { + var targetUnit = game.getUnitAtPosition((keyManager.x + game.cameraX) / FIELD_SIZE, (keyManager.y + game.cameraY) / FIELD_SIZE); + + if (targetUnit && targetUnit.getValue('noShow')) { + targetUnit = null; + } + + // if the unit is a gold mine + if (targetUnit && targetUnit.owner.controller == CONTROLLER.NONE && targetUnit.type.isBuilding && targetUnit.type.startGold) { + game.issueOrderToUnits(game.selectedUnits, lists.types.moveto, targetUnit, keyManager.interfaceHotkeyPressed('queue')); + targetUnit.blink(); + } + + // if theres an enemy unit at this point, attack + else if (targetUnit && targetUnit.owner.isEnemyOfPlayer(PLAYING_PLAYER)) { + game.issueOrderToUnits(game.selectedUnits, lists.types.attack, targetUnit, keyManager.interfaceHotkeyPressed('queue')); + targetUnit.blink(); + } + + // if theres a friendly or neutral unit, moveto or mine + else if (targetUnit && !targetUnit.owner.isEnemyOfPlayer(PLAYING_PLAYER) && ((game.selectedUnits[0].type.isBuilding || targetUnit.type.isBuilding) || (game.selectionContainsCargoUnits() && targetUnit.type.cargoUse) || (targetUnit.type.isMechanical && game.selectionContainsWorkers()) || targetUnit.canLoad())) { + game.issueOrderToUnits(game.selectedUnits, lists.types.moveto, targetUnit, keyManager.interfaceHotkeyPressed('queue')); + targetUnit.blink(); + } + + // if theres no enemy unit => move / waypoint + else { + game.issueOrderToUnits(game.selectedUnits, lists.types.move, field.add3(0, -Y_OFFSET), keyManager.interfaceHotkeyPressed('queue')); + new GroundOrder({ from: new Field((keyManager.x + game.cameraX) / FIELD_SIZE, (keyManager.y + game.cameraY + 3) / FIELD_SIZE, true) }); + } + } + } + } + + // rightmouse + if (keyManager.getKeyCode(e) == 3) { + keyManager.resetCommand(); + } + + return false; +}; +document.addEventListener('mousedown', function(e) { + if (e.button == 2) { + e.preventDefault(); + } +}, false); +// this prevents the context menu when shift right clicking (at least for some browsers) +document.onclick = function(e) { + var b = keyManager.getKeyCode(e); + + if ((b == 2 || b == 3) && (game_state == GAME.PLAYING || game_state == GAME.EDITOR)) { + e.preventDefault(); + e.stopPropagation(); + return false; + } +}; + +canvas.onmouseup = function(e) { + if (keyManager.getKeyCode(e) == 1) // leftmouse + { + // if we were drawing a box + if (keyManager.drawBox && ((game_state == GAME.PLAYING && !keyManager.command) || (game_state == GAME.EDITOR && !editor.dragging && !editor.selectedItemType && editor.terrainModifier == 0))) { + var oldType = game.selectedUnits[0] ? game.selectedUnits[0].type : null; + + var x1 = (keyManager.x + game.cameraX) / FIELD_SIZE; + var y1 = (keyManager.y + game.cameraY) / FIELD_SIZE; + var x2 = (keyManager.startX + game.cameraX) / FIELD_SIZE; + var y2 = (keyManager.startY + game.cameraY) / FIELD_SIZE; + + var x1n = Math.min(x1, x2); + var x2n = Math.max(x1, x2); + var y1n = Math.min(y1, y2); + var y2n = Math.max(y1, y2); + + var units = game.getSelection(x1n, y1n, x2n, y2n, keyManager.interfaceHotkeyPressed('selectall')); + + // deselect units that we cant see + for (var i = 0; i < units.length; i++) { + if (!PLAYING_PLAYER.team.canSeeUnit(units[i], true)) { + units.splice(i, 1); + i--; + } + } + + // if were in editor and no units selected, check for selected tiles + if (units.length == 0 && game_state == GAME.EDITOR) { + for (var i = 0; i < game.blockingTiles.length; i++) { + if (game.blockingTiles[i].isInBox(x1n, y1n, x2n, y2n)) { + units.push(game.blockingTiles[i]); + } + } + + // deselect cliffs + for (var i = 0; i < units.length; i++) { + if (units[i].type.isCliff) { + units.splice(i, 1); + i--; + } + } + + // if we still selected nothing, check if we selected ground tiles + if (units.length == 0) { + for (var i = 0; i < game.groundTiles2.length; i++) { + if (!game.groundTiles2[i].type.isCliff && game.groundTiles2[i].isInBox(x1n, y1n, x2n, y2n)) { + units.push(game.groundTiles2[i]); + } + } + } + } + + // Add / remove units instead of replacing them + if (keyManager.interfaceHotkeyPressed('toggleselection')) { + game.addUnitsToSelection(game.selectedUnits, units); + } + + // if we selected something, replace the old selection with the new one + else if (units.length > 0) { + game.selectedUnits = units; + game.timeOfLastSelection = timestamp; + interface_.commandCard = 0; // reset command card (in case we were in the building submenu) + interface_.unitTypeWithCurrentTabPrio = null; // reset command card prio + interface_.unitsTab = 0; + } + + // if editor and something is selected, set player dropdown box to owning player + if (game.selectedUnits.length > 0 && game_state == GAME.EDITOR) { + $('#playerDropdown')[0].selectedIndex = game.selectedUnits[0].owner.number; + editor.player = game.selectedUnits[0].owner.number; + } + + // sort selected units + game.selectedUnits = _.sortBy(game.selectedUnits, function(o) { + return -o.type.tabPriority; + }); + + keyManager.changeUnitSelection(oldType); + } + + keyManager.leftMouse = false; + keyManager.drawBox = false; + keyManager.minimapScroll = false; + } + + // middleMouse + else if (keyManager.getKeyCode(e) == 2) { + keyManager.middleMouse = false; + } + + return false; +}; + +// when mouse is moved, store position +document.onmousemove = function(e) { + // Calculate pageX/Y if missing and clientX/Y available + if (e.pageX == null && e.clientX != null) { + var doc = document.documentElement; + var body = document.body; + e.pageX = e.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); + e.pageY = e.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); + } + + const clamp = (val, min, max) => { + return val < min ? min : (val > max ? max : val); + }; + const isPointerLocked = interface_.isPointerLocked(); + let x = isPointerLocked ? clamp(keyManager.x + e.movementX, 0, WIDTH) : e.pageX; + let y = isPointerLocked ? clamp(keyManager.y + e.movementY, 0, HEIGHT) : e.pageY; + + + // if middlemouse is pressed, scroll + if (keyManager.middleMouse && game && (!game_paused || PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR)) { + game.setCameraX(game.cameraX + (keyManager.x - x) * 1.5 * (keyManager.mmScrollInvert.get() ? -1 : 1)); + game.setCameraY(game.cameraY + (keyManager.y - y) * 1.5 * (keyManager.mmScrollInvert.get() ? -1 : 1)); + } + + keyManager.x = x; + keyManager.y = y; + + // fix shift bug that sometimes happens (special thanks to pox) + if (!e) { + e = document.event; + } + + if (!e.shiftKey) { + keyManager.keys[KEY.SHIFT] = false; + } +}; + +function Interface() { + Initialization.onDocumentReady(() => this.init()); + + this.messages = []; // active chat messages are stores here + + this.commandCard = 0; // command card 0 => normal commands; command card 1 => buildings, ... + + this.unitTypeWithCurrentTabPrio = null; // prio of the command card that is currently displayed (if more than 1 unit type is selected, only the cc of one unit type can be displayd, can be tabbed with tab key by user) + + // create all the command buttons + this.buttons = []; + + // Preload all the cursor images for pointer lock + this.fakeCursors = {}; + for (const key in CursorFiles) { + const url = CursorFiles[key]; + const img = document.createElement('img'); + document.body.insertBefore(img, canvas); + $(img).addClass('fakeCursor').hide(); + img.src = CursorFiles[key]; + this.fakeCursors[key] = img; + } + this.pointerLockEnabled = LocalConfig.registerValue('pointer_lock_enabled', false); + + // Key events are queued up in here every time draw() is called, and they + // are checked at the end. + // + // Map from ("left", "right" or "hover") -> array of {x, y, w, h, callback} + // which defines what happens when the region defined by (x, y, w, h) is + // left clicked, right clicked, and hovered over respectively + this.keyEvents = { 'left': [], 'right': [], 'hover': [] }; + + this.currentHoverUnit = null; + + this.idleWorkersButtonIsActive = false; + + this.timeOflastGoldMsg = -9999; + + this.unitsTab = 0; + + this.showInfoDiv = false; + + // Register interface configuration values + this.mouseScrollWhenWindowed = LocalConfig.registerValue('mouse_scroll_on', true); + this.scrollSpeed = LocalConfig.registerValue('scroll_speed', 2000); + this.showFullHPBars = LocalConfig.registerValue('show_full_hp_bars', true); + this.noRain = LocalConfig.registerValue('no_rain', true); + this.noGuestDM = LocalConfig.registerValue('no_guest_dms', false); + this.lastChosenAI = LocalConfig.registerValue('last_ai_chosen', 'Default'); + + // Register interface hotkeys + // Must remain in sync with Game.specFieldNames + const spectatorHotkeys = new HotkeyGroup('Spectator Hotkeys', true); + + this.specVisionHotkeys = new HotkeyGroup('Vision'); + this.specVisionHotkeys + .addChild(new HotkeySetting('Toggle following player camera', KEY.BACKTICK, 'toggle_follow_camera')) + .addChild(new HotkeySetting('Set vision all', KEY.NUM0, 0)); + for (let i = 0; i < MAX_PLAYERS; i++) { + this.specVisionHotkeys.addChild(new HotkeySetting(`Set vision to player ${i + 1}`, KEY[`NUM${i + 1}`], i + 1)); + } + + this.specGeneralHotkeys = new HotkeyGroup('General') + .addChild(new HotkeySetting('Units', KEY.Q)) + .addChild(new HotkeySetting('Buildings', KEY.W)) + .addChild(new HotkeySetting('Upgrades', KEY.E)) + .addChild(new HotkeySetting('Production', KEY.R)) + .addChild(new HotkeySetting('Income', KEY.T)) + .addChild(new HotkeySetting('APM', KEY.Z)) + .addChild(new HotkeySetting('Units lost', KEY.U)); + + spectatorHotkeys.addChild(this.specGeneralHotkeys).addChild(this.specVisionHotkeys); + Hotkeys.registerHotkeyGroup(spectatorHotkeys); +}; + +Interface.prototype.init = function() { + const showSpectatorInterface = () => + game_state == GAME.PLAYING && PLAYING_PLAYER && PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR; + + const infoDiv = new UIElement('div', 'spectatorDiv', () => showSpectatorInterface() && this.showInfoDiv); + const spectatorDropdowns = new UIElement('div', 'spectatorDropdowns', showSpectatorInterface); + UIManagerSingleton.registerUIElement(infoDiv); + UIManagerSingleton.registerUIElement(spectatorDropdowns); + + canvas.addEventListener('click', async () => { + if (this.pointerLockEnabled.get()) { + try { + await canvas.requestPointerLock({ unadjustedMovement: true }); + } catch (e) { + await canvas.requestPointerLock(); + } + } + }); +}; + +Interface.prototype.isPointerLocked = function() { + return document.pointerLockElement === canvas; +}; + +Interface.prototype.setPointerLockEnabled = function(enabled) { + if (!enabled && this.isPointerLocked()) { + document.exitPointerLock(); + } + this.pointerLockEnabled.set(enabled); +}; + +// add a message +Interface.prototype.addMessage = function(msg, font, img, whiteShadow) { + if (msg.slice(0, 15) == 'not enough gold' || msg.slice(0, 15) == 'This spell need') { + if (this.timeOflastGoldMsg + 3000 < timestamp) { + this.timeOflastGoldMsg = timestamp; + } else { + return; + } + } + + this.messages.splice(0, 0, { + msg: msg, + font: font, + img: img ? img.img : null, + creationTime: timestamp, + shadow: whiteShadow ? 'white' : 'black', + }); +}; + +// add a chat message +Interface.prototype.chatMsg = function(msg, noSound) { + var str = msg.split(': '); + + var color = 'white'; + var playerName = str[0].indexOf(' [') >= 0 ? (str[0].split(' ['))[0] : str[0]; + var str1 = /^\[.*\]/.test(str[1]) ? str[1].substr(str[1].indexOf('[')+1) : str[1]; + for (var i = 0; i < game.players.length; i++) { + if (game.players[i] && game.players[i].name == playerName && game.players[i].controller != CONTROLLER.SPECTATOR && playerColors[game.players[i].number - 1]) { + color = game.players[i].number == 5 ? 'gray' : game.players[i].getColor(); + } + } + + if (str1 === 'showfps' || str1 === '/showfps') { + show_fps = !show_fps; + } else if (str[1] == 'showunitdetails' && !network_game) { + show_unit_details = !show_unit_details; + } + + this.addMessage(msg, color, null, color == 'black'); + + if (!noSound) { + soundManager.playSound(SOUND.POSITIVE); + } +}; + +Interface.prototype.checkKeyEvents = function(type) { + let retval = false; + for (e of this.keyEvents[type]) { + if (keyManager.x >= e.x && keyManager.y >= e.y && + keyManager.x < e.x + e.w && keyManager.y < e.y + e.h) { + e.callback(); + retval = true; + break; + } + } + // Clear the events to be replenished on the next iteration + this.keyEvents[type] = []; + return retval; +}; + +Interface.prototype.draw = function() { + // chat msg width + var width = Math.max(WIDTH - 500, 200); + + var offset = -1; + + // set font so we can measure the text correctly + c.font = 'bold 24px LCDSolid'; + + // messages + for (var i = 0; i < this.messages.length; i++) { + var msg = this.messages[i]; + + var age = timestamp - msg.creationTime; + + // if more than 10 msges or time expired, kill msg + if (i > 10 || age > 10000) { + this.messages.splice(i, 1); + i--; + } + + // draw msg + else { + // measure text + var textWidth = c.measureText(msg.msg).width; + var predictedAmountOfLines = Math.ceil(textWidth / width); + offset += predictedAmountOfLines * 28; + + // calculate alpha (if older than a ) + var alpha = age < 8000 ? 1 : Math.max(10000 - age, 0.001) / 2000; + + // if msg contains an icon, draw it + if (msg.img) { + c.globalAlpha = alpha; + c.drawImage(miscSheet[0], msg.img.x, msg.img.y, msg.img.w, msg.img.h, 334 - msg.img.w * 2, HEIGHT - INTERFACE_HEIGHT - 70 - offset, msg.img.w * 2, msg.img.h * 2); + c.globalAlpha = 1; + } + + // draw text + drawText(c, ' ' + msg.msg, msg.font, 'bold 24px LCDSolid', 300 + (msg.img ? msg.img.w * 2 + 6 : 0), HEIGHT - INTERFACE_HEIGHT - 40 - offset, width, 'left', alpha, 'rgba(0, 0, 0, 0.5)', msg.shadow, 24); + } + } + + if (game_state == GAME.PLAYING) { + // draw background + c.fillStyle = '#4E4A4E'; + c.fillRect(MINIMAP_WIDTH, HEIGHT - INTERFACE_HEIGHT, WIDTH - MINIMAP_WIDTH, INTERFACE_HEIGHT); + c.lineWidth = 4; + c.strokeStyle = '#757161'; + c.strokeRect(MINIMAP_WIDTH + 2, HEIGHT - INTERFACE_HEIGHT + 2, WIDTH - MINIMAP_WIDTH, INTERFACE_HEIGHT); + + // small black line below the top boarder + c.lineWidth = 1; + c.strokeStyle = 'rgba(0, 0, 0, 0.8)'; + c.beginPath(); + c.moveTo(MINIMAP_WIDTH + 4, HEIGHT - INTERFACE_HEIGHT + 4.5); + c.lineTo(WIDTH, HEIGHT - INTERFACE_HEIGHT + 4.5); + c.stroke(); + } + + var unitNfoX = (WIDTH - 780) / 2; + + this.currentHoverUnit = null; + + var unit0 = game.selectedUnits[0] ? game.selectedUnits[0] : null; + + // imgs + if (game_state == GAME.PLAYING) { + c.drawImage(miscSheet[0], imgs.interfaceRight.img.x, imgs.interfaceRight.img.y, imgs.interfaceRight.img.w, imgs.interfaceRight.img.h, WIDTH - 510, 0, imgs.interfaceRight.img.w * 2, imgs.interfaceRight.img.h * 2); + } + + c.drawImage(miscSheet[0], imgs.interfaceLeft.img.x, imgs.interfaceLeft.img.y, imgs.interfaceLeft.img.w, imgs.interfaceLeft.img.h, -346, 0, imgs.interfaceRight.img.w * 2, imgs.interfaceRight.img.h * 2); + + if (game_state == GAME.PLAYING) { + c.drawImage(miscSheet[0], imgs.interfaceButtonDiv.img.x, imgs.interfaceButtonDiv.img.y, imgs.interfaceButtonDiv.img.w, imgs.interfaceButtonDiv.img.h, WIDTH - 390, HEIGHT - 170, imgs.interfaceButtonDiv.img.w * 2, imgs.interfaceButtonDiv.img.h * 2); + } + + c.drawImage(miscSheet[0], imgs.interfaceMapBorder.img.x, imgs.interfaceMapBorder.img.y, imgs.interfaceMapBorder.img.w, imgs.interfaceMapBorder.img.h, -16, HEIGHT - 200, imgs.interfaceMapBorder.img.w * 2, imgs.interfaceMapBorder.img.h * 2); + + if (game_state == GAME.PLAYING) { + c.drawImage(miscSheet[0], imgs.interfaceUnitInfo.img.x, imgs.interfaceUnitInfo.img.y, imgs.interfaceUnitInfo.img.w, imgs.interfaceUnitInfo.img.h, unitNfoX, HEIGHT - 152, imgs.interfaceUnitInfo.img.w * 2, imgs.interfaceUnitInfo.img.h * 2); + } + + + var units = game.selectedUnits; + + // unit data / stats, if only 1 unit is selected + if (units.length == 1) { + var u = units[0]; + var isInvincible = u.getValue('isInvincible'); + + this.drawUnitInfo(u); + + var countBars = (!isInvincible ? 1 : 0) + (u.type.mana ? 1 : 0) + (u.type.experienceLevels && u.type.experienceLevels.length > 0 ? 1 : 0); + var x = countBars == 3 ? HEIGHT - 136 : HEIGHT - 130; + var step = countBars == 3 ? 26 : 30; + + // hp bar (if not invincible) + if (!isInvincible) { + u.drawHealthbar(unitNfoX + 350 - 70, x, 180, 20); + drawText(c, Math.floor(u.hp) + ' / ' + u.getValue('hp'), 'black', 'bold 16px LCDSolid', unitNfoX + 370, x + 16, 200, 'center', 1, null, 'white'); + } + + // mana bar (if has mana) + if (u.type.mana) { + x += step; + u.drawManabar(unitNfoX + 350 - 70, x, 180, 20); + drawText(c, Math.floor(u.mana) + ' / ' + u.getValue('mana'), 'black', 'bold 16px LCDSolid', unitNfoX + 370, x + 16, 200, 'center', 1, null, 'white'); + } + + // exp bar + if (u.type.experienceLevels && u.type.experienceLevels.length > 0) { + x += step; + u.drawExpbar(unitNfoX + 350 - 70, x, 180, 20); + drawText(c, 'Level ' + u.level + ' (' + Math.floor(Math.min(u.exp, u.type.experienceLevels[u.type.experienceLevels.length - 1])) + ' / ' + u.getXP4NextLevel() + ' exp)', 'black', 'bold 16px LCDSolid', unitNfoX + 370, x + 16, 200, 'center', 1, null, 'white'); + } + + const boxesStartPosX = unitNfoX + 188; + const boxesPosY = HEIGHT - 94; + const boxesWidth = 64; + const boxesHeight = 64; + const numberOfBoxes = 5; + + // buildings queue + if ((game.humanUnitsSelected() || PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR) && u.queue[0]) { + c.lineWidth = 2; + c.strokeStyle = 'white'; + + for (let i = 0; i < numberOfBoxes; i++) { + const boxX = boxesStartPosX + i * 70; + c.strokeRect(boxesStartPosX + i * 70, boxesPosY, boxesWidth, boxesHeight); + + if (u.queue[i]) { + var img = u.queue[i].getTitleImage(u.owner); + var scale = 64 / Math.max(img.w, img.h); + const x = boxesStartPosX + i * 70 + 32 - img.w * scale / 2; + const y = boxesPosY + 32 - img.h * scale / 2; + const w = img.w * scale; + const h = img.h * scale; + c.drawImage(img.file, img.x, img.y, img.w, img.h, x, y, w, h); + + if (PLAYING_PLAYER && + PLAYING_PLAYER.controller != CONTROLLER.SPECTATOR && + u.owner == PLAYING_PLAYER) { + this.keyEvents['hover'].push({ + x: boxX, y: boxesPosY, w: boxesWidth, h: boxesHeight, + callback: () => { + c.fillStyle = 'rgba(255, 255, 255, 0.5)'; + c.fillRect(boxX, boxesPosY, boxesWidth, boxesHeight); + }, + }); + this.keyEvents['left'].push({ + x: boxX, y: boxesPosY, w: boxesWidth, h: boxesHeight, + callback: () => { + game.issueOrderToUnits([u], lists.types.cancel, /* target=*/i + 1); // adding 1 because if sending 0, it gets converted to null somewhere, and therefore can't differenciate between cancelling first in queue by clicking or cancelling last in queue by default + }, + }); + } + } else { + drawText(c, (i + 1).toString(), 'white', 'bold 38px LCDSolid', unitNfoX + 212 + i * 70, boxesPosY + 48); + } + } + + // building unit bar + var progress = u.queueStarted ? u.currentBuildTime - (u.queueFinish - ticksCounter) : 0; + var total = u.currentBuildTime; + this.drawConstructionProgress(boxesStartPosX, boxesPosY + 72, 344, 14, progress / 20, total / 20); + } + + // under construction + else if (u.isUnderConstruction && (game.humanUnitsSelected() || PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR)) { + var dots = ''; + for (var i = 200; i < timestamp % 1000; i += 200) { + dots += '.'; + } + drawText(c, 'Constructing' + dots, 'white', 'bold 38px LCDSolid', unitNfoX + 190, HEIGHT - 56); + + var progress = u.type.getValue('buildTime', unit0.owner) - u.buildTicksLeft; + var total = u.type.getValue('buildTime', unit0.owner); + this.drawConstructionProgress(boxesStartPosX, boxesPosY + 54, 344, 14, progress / 20, total / 20); + } + + // else if unit has cargo + else if (u.cargo && u.cargo.length > 0 && (u.owner == PLAYING_PLAYER || PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR)) { + c.fillStyle = 'rgba(255, 255, 255, 0.2)'; + + var spc = Math.min(u.type.cargoSpace, 10); + c.fillRect(unitNfoX + 249, HEIGHT - 98, Math.ceil(spc / 2) * 47, spc == 1 ? 48 : 98); + + var sortedCargo = _.sortBy(u.cargo, function(e) { + return -e.type.cargoUse; + }); + + c.strokeStyle = 'white'; + c.lineWidth = 2; + c.fillStyle = 'rgba(255, 255, 255, 0.5)'; + + var cargo = 0; + + for (var i = 0; i < sortedCargo.length && i < 10; i++) { + var type = sortedCargo[i].type; + + if (cargo + type.cargoUse <= 10) { + var x = unitNfoX + 254 + Math.floor(cargo / 2) * 46; + var y = HEIGHT - 94 + (cargo % 2) * 46; + var w = Math.ceil((type.cargoUse - 0.5) / 2) * 40; + var h = type.cargoUse <= 1 ? 40 : 80; + + c.strokeRect(x, y, w, h); + var img = type.getTitleImage(u.owner); + var scale = Math.max(w / img.w, h / img.h); + c.drawImage(img.file, img.x, img.y, img.w, img.h, x + w / 2 - img.w * scale / 2, y + h / 2 - img.h * scale / 2, img.w * scale, img.h * scale); + + // hover + if (keyManager.x > x && keyManager.y > y && keyManager.x < x + w && keyManager.y < y + h) { + c.fillRect(x, y, w, h); + } + + cargo += type.cargoUse; + } else { + i = sortedCargo.length; + } + } + } + + // display attributes + else { + // if damage not 0, display damage value and range + if (u.type.dmg > 0 && !u.isUnderConstruction) { + c.font = 'bold 20px LCDSolid'; + + // dmg + var bonusDamage = u.owner.getValueModifier('dmg', u.type) + (u.modifierMods['dmg'] ? u.modifierMods['dmg'] : 0); + var basicText = 'Damage: ' + u.type.dmg; + drawText(c, basicText, 'white', '16px LCDSolid', unitNfoX + 170, HEIGHT - 60); + var pointer = unitNfoX + 170 + c.measureText(basicText).width + 6; + + if (bonusDamage) { + var text = ' (' + (bonusDamage > 0 ? '+' : '') + bonusDamage + ')'; + drawText(c, text, bonusDamage > 0 ? '#34DA34' : '#FF0000', '16px LCDSolid', pointer, HEIGHT - 60); + pointer += c.measureText(text).width + 6; + } + + var basicDmg = u.getValue('dmg'); + var modifiersObj = {}; + if (u.type.dmgModifierAttributes) { + for (var i = 0; i < u.type.dmgModifierAttributes.length; i++) { + var att = u.type.dmgModifierAttributes[i]; + + if (u.type.dmgModifierMultiplier && u.type.dmgModifierMultiplier[i]) { + modifiersObj[att] = modifiersObj[att] ? ((modifiersObj[att] + basicDmg) * (u.type.dmgModifierMultiplier[i] - 1)) : ((u.type.dmgModifierMultiplier[i] - 1) * basicDmg); + } + + if (u.type.dmgModifierAddition && u.type.dmgModifierAddition[i]) { + modifiersObj[att] = modifiersObj[att] ? (modifiersObj[att] + u.type.dmgModifierAddition[i]) : u.type.dmgModifierAddition[i]; + } + } + } + + var modifierStr = ''; + _.each(modifiersObj, function(val, att) { + if (val) { + var text = ' (' + (val > 0 ? '+' : '') + val + ' vs ' + (att.startsWith('is') ? att.slice(2) : att) + ')'; + drawText(c, text, val > 0 ? '#34DA34' : '#FF0000', '16px LCDSolid', pointer, HEIGHT - 60); + pointer += c.measureText(text).width + 8; + } + }); + + + // attack speed + var atkSpeed = (Math.round(u.getValue('weaponCooldown') / 20 * 100) / 100); + drawText(c, 'Attack speed: ' + atkSpeed, 'white', '16px LCDSolid', unitNfoX + 170, HEIGHT - 42); + + // armor penetration + if (u.type.armorPenetration > 0) { + var armorPenetrationText = 'Armor Penetration: ' + u.type.armorPenetration; + drawText(c, armorPenetrationText, 'white', '16px LCDSolid', unitNfoX + 170, HEIGHT - 24); + } + + // range + var bonusRange = u.owner.getValueModifier('range', u.type) + (u.modifierMods['range'] ? u.modifierMods['range'] : 0); + basicText = 'Range: ' + (u.type.range > 1 ? u.type.range : 'Melee') + (u.type.minRange > 0 ? ' (min: ' + u.type.minRange + ')' : ''); + drawText(c, basicText, 'white', '16px LCDSolid', unitNfoX + 170, HEIGHT - 78); + if (bonusRange) { + drawText(c, ' (' + (bonusRange > 0 ? '+' : '') + bonusRange + ')', bonusRange > 0 ? '#34DA34' : '#FF0000', '16px LCDSolid', unitNfoX + 170 + c.measureText(basicText).width + 6, HEIGHT - 78); + } + + // kills + drawText(c, 'Kills: ' + u.kills, 'white', '16px LCDSolid', unitNfoX + 170, HEIGHT - 6); + + // dmg hover + if (keyManager.x > unitNfoX + 170 && keyManager.y > HEIGHT - 76 && keyManager.x < unitNfoX + 280 && keyManager.y < HEIGHT - 60) { + this.drawHoverBox(); + drawText(c, 'This is the damage this unit deals everytime it hits another unit.', 'white', '18px LCDSolid', WIDTH - 340, HEIGHT - INTERFACE_HEIGHT - 270, 310, null, null, null, null, 18); + } + + // range hover + if (keyManager.x > unitNfoX + 170 && keyManager.y > HEIGHT - 94 && keyManager.x < unitNfoX + 280 && keyManager.y < HEIGHT - 78) { + this.drawHoverBox(); + drawText(c, 'This is the range this unit can shoot.', 'white', '18px LCDSolid', WIDTH - 340, HEIGHT - INTERFACE_HEIGHT - 270, 310, null, null, null, null, 18); + } + + // attack speed hover + if (keyManager.x > unitNfoX + 170 && keyManager.y > HEIGHT - 58 && keyManager.x < unitNfoX + 280 && keyManager.y < HEIGHT - 42) { + this.drawHoverBox(); + drawText(c, 'This unit attacks once every ' + atkSpeed + ' seconds.', 'white', '18px LCDSolid', WIDTH - 340, HEIGHT - INTERFACE_HEIGHT - 270, 310, null, null, null, null, 18); + } + + // armor penetration hover + if (keyManager.x > unitNfoX + 170 && keyManager.y > HEIGHT - 40 && keyManager.x < unitNfoX + 280 && keyManager.y < HEIGHT - 24) { + if (u.type.armorPenetration > 0) { + this.drawHoverBox(); + drawText(c, 'This unit ignores up to ' + u.type.armorPenetration + ' armor when attacking.', 'white', '18px LCDSolid', WIDTH - 340, HEIGHT - INTERFACE_HEIGHT - 270, 310, null, null, null, null, 18); + } + } + } + + // stati + var stati = ''; + + if (u.type.flying) { + stati += 'flying, '; + } + + if (u.type.isBiological) { + stati += 'biological, '; + } + + if (u.type.isMechanical) { + stati += 'mechanical, '; + } + + if (u.getValue('hasDetection')) { + stati += 'detection, '; + } + + if (u.type.isUndead) { + stati += 'undead, '; + } + + drawText(c, stati.slice(0, stati.length - 2), 'white', '16px LCDSolid', unitNfoX + 340, HEIGHT - 24); + + // modifiers + if (u.modifiers.length > 0) { + var k = 0; + for (var i = 0; i < u.modifiers.length && k < 5; i++) { + if (u.modifiers[i].modifier.image) { + var img = u.modifiers[i].modifier.getTitleImage(); + c.drawImage(img.file, img.x, img.y, img.w, img.h, unitNfoX + 554, HEIGHT - INTERFACE_HEIGHT + 37 + k * 22, 20, 20); + + // hover + if (keyManager.x > unitNfoX + 554 && keyManager.y > HEIGHT - INTERFACE_HEIGHT + 37 + k * 22 && keyManager.x < unitNfoX + 554 + 20 && keyManager.y < HEIGHT - INTERFACE_HEIGHT + 37 + k * 22 + 20) { + this.drawHoverBox(); + + var duration = (u.modifiers[i].removeAt && u.modifiers[i].removeAt > ticksCounter) ? (Math.ceil((u.modifiers[i].removeAt - ticksCounter) / 20) + ' sec left') : null; + + drawText(c, u.modifiers[i].modifier.name, 'yellow', '22px LCDSolid', WIDTH - 340, HEIGHT - INTERFACE_HEIGHT - 270, 310); + + if (duration) { + drawText(c, duration, 'grey', '16px LCDSolid', WIDTH - 340, HEIGHT - INTERFACE_HEIGHT - 248, 310); + } + + var str = interpreteString(u.modifiers[i].modifier.description, u).split('#BR'); + for (var j = 0; j < str.length; j++) { + drawText(c, str[j], 'white', '18px LCDSolid', WIDTH - 340, HEIGHT - INTERFACE_HEIGHT - 246 + (duration ? 28 : 0) + j * 22, 310, null, null, null, null, 18); + } + } + + k++; + } + } + } + + // if has gold (= is Mine), display remaining gold amount + if (u.type.startGold) { + drawText(c, 'Gold remaining: ' + u.gold, u.gold ? 'white' : 'red', '20px LCDSolid', unitNfoX + 170, HEIGHT - 60); + } + + // armor + if (!isInvincible) { + var bonusArmor = u.owner.getValueModifier('armor', u.type) + (u.modifierMods['armor'] ? u.modifierMods['armor'] : 0); + c.font = 'bold 20px LCDSolid'; + var basicText = 'Armor: ' + u.type.armor; + drawText(c, basicText, 'white', '16px LCDSolid', unitNfoX + 170, HEIGHT - 96); + if (bonusArmor) { + drawText(c, ' (' + (bonusArmor > 0 ? '+' : '') + bonusArmor + ')', bonusArmor > 0 ? '#34DA34' : '#FF0000', '16px LCDSolid', unitNfoX + 160 + c.measureText(basicText).width + 16, HEIGHT - 96); + } + + // armor hover + if (keyManager.x > unitNfoX + 160 && keyManager.y > HEIGHT - 112 && keyManager.x < unitNfoX + 280 && keyManager.y < HEIGHT - 96) { + this.drawHoverBox(); + drawText(c, 'This is the units\' armor. 1 armor reduces the incoming damage by 1.', 'white', '18px LCDSolid', WIDTH - 340, HEIGHT - INTERFACE_HEIGHT - 270, 310, null, null, null, null, 18); + } + } + + // speed + if (u.getValue('movementSpeed')) { + var bonusSpeed = Math.round((u.owner.getValueModifier('movementSpeed', u.type) + (u.modifierMods['movementSpeed'] ? u.modifierMods['movementSpeed'] : 0)) * 20 * 100) / 100; + c.font = 'bold 20px LCDSolid'; + var basicText = 'Speed: ' + (Math.round(u.type.movementSpeed * 20 * 100) / 100); + drawText(c, basicText, 'white', '16px LCDSolid', unitNfoX + 170, HEIGHT - 114); + if (bonusSpeed) { + drawText(c, ' (' + (bonusSpeed > 0 ? '+' : '') + bonusSpeed + ')', bonusSpeed > 0 ? '#34DA34' : '#FF0000', '16px LCDSolid', unitNfoX + 160 + c.measureText(basicText).width + 16, HEIGHT - 114); + } + + // speed hover + if (keyManager.x > unitNfoX + 160 && keyManager.y > HEIGHT - 130 && keyManager.x < unitNfoX + 280 && keyManager.y < HEIGHT - 114) { + this.drawHoverBox(); + drawText(c, 'This is the units\' movement speed.', 'white', '18px LCDSolid', WIDTH - 340, HEIGHT - INTERFACE_HEIGHT - 270, 310, null, null, null, null, 18); + } + } + } + + // description hover + if (keyManager.x > unitNfoX && keyManager.y > HEIGHT - 140 && keyManager.x < unitNfoX + 150) { + this.drawHoverBox(); + drawText(c, u.type.description, 'white', '18px LCDSolid', WIDTH - 340, HEIGHT - INTERFACE_HEIGHT - 270, 310, null, null, null, null, 18); + } + } + + // if more than 1 units selected + else if (units.length > 1) { + var x_ = unitNfoX + 160; + var y_ = HEIGHT - 142; + + for (var i = this.unitsTab * 27; i < units.length; i++) { + var u = units[i]; + + // active + if (u.type == this.unitTypeWithCurrentTabPrio) { + c.fillStyle = 'rgba(255, 255, 255, 0.2)'; + c.fillRect(x_ + 1.5, y_ + 1.5, 37, 40); + } + + // unit img + var img = u.type.getTitleImage(u.owner); + var scale = Math.min(40 / img.w, 40 / img.h); + var x = (40 - img.w * scale) / 2; + var y = (40 - img.h * scale) / 2; + + c.drawImage(img.file, img.x, img.y, img.w, img.h, x_ + x, y_ + y, img.w * scale, img.h * scale); + + // hp bar (if not invincible) + if (!isInvincible) { + u.drawHealthbar(x_ + 2, y_ + 39, 36, 4); + } + + // mana bar (if has mana) + if (u.type.mana) { + u.drawManabar(x_ + 2, y_ + 32, 36, 4); + } + + // small building queue + if ((u.owner == PLAYING_PLAYER || PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR) && u.queue && u.queue[0]) { + c.strokeStyle = 'white'; + c.lineWidth = 1; + + for (var k = 0; k < BUILDING_QUEUE_LEN; k++) { + c.strokeRect(x_ + 2.5 + k * 7, y_ + 28.5, 7, 7); + c.fillStyle = u.queue[k] ? 'rgba(0, 160, 230, 1)' : 'rgba(255, 255, 255, 0.5)'; + c.fillRect(x_ + k * 7 + 3.5, y_ + 29.5, 5, 5); + } + } + + // hover + if (keyManager.x > x_ && keyManager.y > y_ && keyManager.x < x_ + 40 && keyManager.y < y_ + 40) { + this.drawHoverBox(); + drawText(c, u.type.description, 'white', '18px LCDSolid', WIDTH - 340, HEIGHT - INTERFACE_HEIGHT - 270, 310, null, null, null, null, 18); + + this.drawUnitInfo(u); + + // box + c.strokeStyle = 'white'; + c.lineWidth = 1; + c.strokeRect(x_ - 0.5, y_ - 0.5, 41, 44); + + this.currentHoverUnit = u; + } + + x_ += 44; + if (x_ > unitNfoX + 540) { + x_ = unitNfoX + 160; + y_ += 44; + + if (y_ > HEIGHT - 20) { + break; + } + } + } + + if (units.length > 27) { + for (var i = 0; i * 27 < units.length && i <= 4; i++) { + var _x = unitNfoX + 594; + var _y = HEIGHT - 139 + i * 24; + + // box + c.strokeStyle = 'white'; + c.fillStyle = (keyManager.x > _x && keyManager.y > _y && keyManager.x < _x + 20 && keyManager.y < _y + 20) ? 'rgba(255, 255, 255, 0.8)' : 'rgba(255, 255, 255, 0.5)'; + c.lineWidth = 2; + c.strokeRect(_x, _y, 20, 20); + c.fillRect(_x, _y, 20, 20); + + drawText(c, i + 1, 'white', 'bold 18px LCDSolid', unitNfoX + 606, HEIGHT - 122 + i * 24, 20, 'center'); + } + } + } + + + // draw building @cursorpos, if ordering building placement right now + var placedBuilding = keyManager.command && keyManager.command.type == COMMAND.MAKEBUILDING && keyManager.command.unitType; + if (placedBuilding && placedBuilding.isBuilding) { + var field = placedBuilding.getFieldFromMousePos(); + var hm = game.getHMValue4(field.x, field.y); + + c.globalAlpha = 0.6; + placedBuilding.draw(field.x, field.y - hm * CLIFF_HEIGHT); + c.globalAlpha = 1; + var gap = FIELD_SIZE / 24; + + // draw red box with alpha over the image when blocked, otherwise white alpha, for every grid field the building covers + for (x = field.x - 2; x < field.x + placedBuilding.size + 2; x++) { + for (y = field.y - 2; y < field.y + placedBuilding.size + 2; y++) { + var f = new Field(x, y); + var distanceAllowed = true; + var nextGoldmine = game.getNextBuildingOfType(f, null, false, 'startGold'); + var nextCC = game.getNextBuildingOfType(f, null, false, 'takesGold'); + + if (placedBuilding.takesGold && nextGoldmine && nextGoldmine.pos.distanceTo2(f) < game.getMineDistance()) { + distanceAllowed = false; + } + + if (placedBuilding.startGold && nextCC && nextCC.pos.distanceTo2(field) < game.getMineDistance()) { + distanceAllowed = false; + } + + if (PLAYING_PLAYER.team.fieldIsBlocked(f.x, f.y) || !distanceAllowed) { + c.fillStyle = 'rgba(200, 0, 0, 0.25)'; + } else if (x >= field.x && x < field.x + placedBuilding.size && y >= field.y && y < field.y + placedBuilding.size) { + c.fillStyle = 'rgba(255, 255, 255, 0.45)'; + } else { + c.fillStyle = 'rgba(122, 255, 122, 0.2)'; + } + + c.fillRect((f.x - 1) * FIELD_SIZE - game.cameraX + gap / 2, (f.y - 1 - hm * CLIFF_HEIGHT) * FIELD_SIZE - game.cameraY + gap / 2, FIELD_SIZE - gap, FIELD_SIZE - gap); + } + } + } + + var goldDisplay = ''; + var supplyDisplay = ''; + var maxSupplyDisplay = ''; + + if (PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR) { + if (unit0 && unit0.owner.number > 0) { + goldDisplay = Math.floor(unit0.owner.gold); + supplyDisplay = Math.floor(unit0.owner.supply); + maxSupplyDisplay = Math.floor(unit0.owner.maxSupply); + } else { + goldDisplay = ''; + supplyDisplay = ''; + maxSupplyDisplay = ''; + } + } else { + goldDisplay = Math.floor(PLAYING_PLAYER.gold); + supplyDisplay = Math.floor(PLAYING_PLAYER.supply); + maxSupplyDisplay = Math.floor(PLAYING_PLAYER.maxSupply); + } + + if (game_state == GAME.PLAYING) { + drawText(c, goldDisplay, 'white', 'bold 24px LCDSolid', WIDTH - 264, 35); + + var supplyFontColor; + if (PLAYING_PLAYER.supply < PLAYING_PLAYER.maxSupply - 2|| PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR) { + supplyFontColor = 'white'; + } else if (PLAYING_PLAYER.supply < PLAYING_PLAYER.maxSupply) { + supplyFontColor = 'orange'; + } else { + supplyFontColor = 'red'; + } + + // supply + drawText(c, supplyDisplay + ' / ' + maxSupplyDisplay, supplyFontColor, 'bold 24px LCDSolid', WIDTH - 120, 35); + + // supply hover info + if (keyManager.x > WIDTH - 168 && keyManager.y < 47) { + this.drawHoverBox(); + drawText(c, 'This is your supply count. The left number is your current supply ( = how many units you have). The right number is your maximum supply. If you reach it, you can\'t spawn any more units. You can build Houses or Castles to increase your max supply count up to ' + game.getMaxSupply() + '.', 'white', '18px LCDSolid', WIDTH - 340, HEIGHT - INTERFACE_HEIGHT - 270, 310, null, null, null, null, 18); + } + + // timer + var sec_total = Math.floor(ticksCounter * 50 / 1000); + var min = Math.floor(sec_total / 60); + var sec = sec_total % 60; + sec = sec < 10 ? '0' + sec : sec; + drawText(c, min + ':' + sec, 'white', 'bold 24px LCDSolid', WIDTH - 442, 35); + + // idle workers button + if (game.playerHasIdleWorkers(PLAYING_PLAYER)) { + this.idleWorkersButtonIsActive = true; + + c.fillStyle = 'rgba(155, 155, 155, 0.6)'; + c.fillRect(20, HEIGHT - MINIMAP_HEIGHT - 93, 70, 70); + + var img = lists.types.worker.getTitleImage(PLAYING_PLAYER); + c.drawImage(img.file, img.x, img.y, img.w, img.h, 10, HEIGHT - MINIMAP_HEIGHT - 103, 90, 90); + + // hover + if (keyManager.x > 20 && keyManager.x < 90 && keyManager.y > HEIGHT - MINIMAP_HEIGHT - 93 && keyManager.y < HEIGHT - MINIMAP_HEIGHT - 23) { + c.fillRect(20, HEIGHT - MINIMAP_HEIGHT - 93, 70, 70); + this.drawHoverBox(); + drawText(c, 'You have idle workers. Click this button to select one of them.', 'white', '18px LCDSolid', WIDTH - 340, HEIGHT - INTERFACE_HEIGHT - 270, 310, null, null, null, null, 18); + } + } else { + this.idleWorkersButtonIsActive = false; + } + } + + // draw buttons + if (game.humanUnitsSelected()) { + // check if tab prio is ok + var currentTabPrioIsFine = false; + for (var i = 0; i < units.length; i++) { + if (units[i].type == this.unitTypeWithCurrentTabPrio) { + currentTabPrioIsFine = true; + } + } + + // if prio has to be re-set + if (!currentTabPrioIsFine) { + this.unitTypeWithCurrentTabPrio = units[0].type; + for (var i = 1; i < units.length; i++) { + if (units[i].type.tabPriority > this.unitTypeWithCurrentTabPrio.tabPriority) { + this.unitTypeWithCurrentTabPrio = units[i].type; + } + } + } + + for (var i = 0; i < this.buttons.length; i++) { + if (this.buttons[i].isVisible(this.unitTypeWithCurrentTabPrio)) { + this.buttons[i].draw(keyManager.x, keyManager.y); + } + } + } + + // fps + if (show_fps) { + drawText(c, `${Math.round(fps)} fps`, 'white', '30px LCDSolid', 20, HEIGHT - INTERFACE_HEIGHT - 114); + if (network_game) { + const pingText = `ping: ${Math.round(network.pings.reduce((a, c) => a + c, 0) / network.pings.length)}`; + drawText(c, pingText, 'white', '30px LCDSolid', 20, HEIGHT - INTERFACE_HEIGHT - 144); + } + } + + // spectator stuff: show supply and gold + if (PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR) { + // supply icon + c.drawImage(miscSheet[0], imgs.supply.img.x, imgs.supply.img.y, imgs.supply.img.w, imgs.supply.img.h, WIDTH - 120, HEIGHT - 150, imgs.supply.img.w * 2, imgs.supply.img.h * 2); + + // gold icon + c.drawImage(miscSheet[0], imgs.gold.img.x, imgs.gold.img.y, imgs.gold.img.w, imgs.gold.img.h, WIDTH - 60, HEIGHT - 150, imgs.gold.img.w * 2, imgs.gold.img.h * 2); + + var nr = 0; + + for (var i = 0; i < game.players.length && nr < 2; i++) { + if (game.players[i]) { + var p = game.players[i]; + + if (p.controller != CONTROLLER.SPECTATOR && p.controller != CONTROLLER.NONE) { + // rect to indicate player nr + var arr = playerColors[p.number - 1][3]; + c.fillStyle = 'rgb(' + arr[0] + ', ' + arr[1] + ', ' + arr[2] + ')'; + c.fillRect(WIDTH - 354, HEIGHT - 96 + nr * 40, 342, 30); + + // name + drawText(c, p.name, 'white', '20px LCDSolid', WIDTH - 340, HEIGHT - 74 + nr * 40, /* w=*/200, /* align=*/undefined, /* alpha=*/undefined, /* fillStyle=*/undefined, /* shadowStyle=*/undefined, /* height=*/undefined, /* overflow=*/'ellipses'); + + // supply + drawText(c, p.supply + ' / ' + p.maxSupply, 'white', '20px LCDSolid', WIDTH - 130, HEIGHT - 74 + nr * 40); + + // gold + drawText(c, Math.floor(p.gold), 'white', '20px LCDSolid', WIDTH - 60, HEIGHT - 74 + nr * 40); + + nr++; + } + } + } + } + + this.checkKeyEvents('hover'); +}; + +Interface.prototype.drawUnitInfo = function(unit) { + // img + var img = unit.type.getTitleImage(unit.owner); + var scale = Math.min(INTERFACE_UNIT_IMG_SIZE / img.w, INTERFACE_UNIT_IMG_SIZE / img.h); + var x = (INTERFACE_UNIT_IMG_SIZE - img.w * scale) / 2; + var y = (INTERFACE_UNIT_IMG_SIZE - img.h * scale) / 2; + c.drawImage(img.file, img.x, img.y, img.w, img.h, (WIDTH - 780) / 2 + 78 - img.w * scale / 2, HEIGHT - 140 + y, img.w * scale, img.h * scale); + + // name + var name = (unit.type.name == 'Archer' && unit.owner.getUpgradeLevel(lists.types.upgspeed) >= 2) ? 'Ranger' : unit.type.name; // special: if archer and speed upg at least 3, name is "Ranger" + var fontSize = Math.max(Math.min(30 - (name.length - 6) * 3, 30), 14); + drawText(c, name, 'white', 'bold ' + fontSize + 'px LCDSolid', (WIDTH - 783) / 2 + 78, HEIGHT - 14, 150, 'center', null, null, null, fontSize); +}; + +// draw a box at the bottom right (above the interface), where the button hover text for command buttons is placed in +Interface.prototype.drawHoverBox = function() { + // rect + c.fillStyle = '#4E4A4E'; + c.lineWidth = 4; + c.strokeStyle = '#757161'; + c.fillRect(WIDTH - 350, HEIGHT - INTERFACE_HEIGHT - 300, 342, 292); + c.strokeRect(WIDTH - 350, HEIGHT - INTERFACE_HEIGHT - 300, 342, 292); + + // small black line below the top boarder + c.lineWidth = 1; + c.strokeStyle = 'rgba(0, 0, 0, 0.8)'; + c.beginPath(); + c.moveTo(WIDTH - 348, HEIGHT - INTERFACE_HEIGHT - 297.5); + c.lineTo(WIDTH - 10, HEIGHT - INTERFACE_HEIGHT - 297.5); + c.stroke(); + + // small black line right the left border + c.strokeStyle = 'rgba(0, 0, 0, 0.4)'; + c.beginPath(); + c.moveTo(WIDTH - 347.5, HEIGHT - INTERFACE_HEIGHT - 297.5); + c.lineTo(WIDTH - 347.5, HEIGHT - INTERFACE_HEIGHT - 110); + c.stroke(); +}; + +Interface.prototype.rightClick = function(x, y) { + for (var i = 0; i < this.buttons.length; i++) { + var b = this.buttons[i]; + + if (b.contains(x, y) && b.isVisible(this.unitTypeWithCurrentTabPrio) && b.command.hasAutocast) { + if (game.humanUnitsSelected()) { + soundManager.playSound(SOUND.INGAMECLICK); + game.issueOrderToUnits(game.selectedUnits, b.command, null, null, true, !game.selectedUnits[0].autocast.contains(b.command.id)); + } + + return true; + } + } + + return this.checkKeyEvents('right'); +}; + +// gets called on left click; check if a button was clicked, do stuff and return true, if yes +Interface.prototype.leftClick = function(x, y) { + // check, if a unit was clicked + if (this.currentHoverUnit) { + // Select all units of that type + if (keyManager.interfaceHotkeyPressed('selectall')) { + // Remove all units of that type, since they are by definition already selected + if (keyManager.interfaceHotkeyPressed('toggleselection')) { + for (var i = 0; i < game.selectedUnits.length; i++) { + if (game.selectedUnits[i].type == this.currentHoverUnit.type) { + game.selectedUnits.splice(i, 1); + i--; + } + } + } + + // if no shift pressed, select all units of that type + else { + var units = []; + for (var i = 0; i < game.selectedUnits.length; i++) { + if (game.selectedUnits[i].type == this.currentHoverUnit.type) { + units.push(game.selectedUnits[i]); + } + } + game.selectedUnits = units; + this.unitsTab = 0; + } + } + + // Remove the unit from the selection + else if (keyManager.interfaceHotkeyPressed('toggleselection')) { + game.selectedUnits.erease(this.currentHoverUnit); + } + + // if nothing pressed, just select the clicked unit + else { + game.selectedUnits = [this.currentHoverUnit]; + this.unitsTab = 0; + } + + soundManager.playSound(SOUND.INGAMECLICK); + + while (this.unitsTab * 27 + 1 > game.selectedUnits.length && this.unitsTab > 0) { + this.unitsTab--; + } + } else if (game.selectedUnits.length > 27) { + for (var i = 0; i * 27 < game.selectedUnits.length && i <= 4; i++) { + var _x = (WIDTH - 780) / 2 + 594; + var _y = HEIGHT - 139 + i * 24; + + if (keyManager.x > _x && keyManager.y > _y && keyManager.x < _x + 20 && keyManager.y < _y + 20) { + this.unitsTab = i; + soundManager.playSound(SOUND.INGAMECLICK); + } + } + } + + for (var i = 0; i < this.buttons.length; i++) { + var b = this.buttons[i]; + + if (b.contains(x, y) && b.isVisible(this.unitTypeWithCurrentTabPrio)) { + var requirement_text = PLAYING_PLAYER.getCommandRequirementText(b.command, game.selectedUnits, null, b.learn); + + // if requirements of the corresponding command are met + if (!requirement_text) { + keyManager.order(b.command, b.learn); + soundManager.playSound(SOUND.INGAMECLICK, 0.7); + return true; + } else { + soundManager.playSound(SOUND.NEGATIVE, 0.6); + this.addMessage(requirement_text, 'red', imgs.attentionmark); + return true; + } + } + } + + // idleworkers button is active and has been clicked + if (this.idleWorkersButtonIsActive && keyManager.x > 20 && keyManager.x < 90 && keyManager.y > HEIGHT - MINIMAP_HEIGHT - 93 && keyManager.y < HEIGHT - MINIMAP_HEIGHT - 23) { + c.fillStyle = 'rgba(155, 155, 155, 0.5)'; + c.fillRect(20, HEIGHT - MINIMAP_HEIGHT - 93, 70, 70); + + // get all idle workers + var workers = []; + for (var i = 0; i < game.units.length; i++) { + if (game.units[i].owner == PLAYING_PLAYER && game.units[i].type == lists.types.worker && game.units[i].order && game.units[i].order.type == COMMAND.IDLE) { + workers.push(game.units[i]); + } + } + + if (workers.length > 0) { + // Select all workers + if (keyManager.interfaceHotkeyPressed('selectall')) { + game.selectedUnits = workers; + this.unitsTab = 0; + + var centerPosition = game.getCenterOfUnits(workers); + + // jump the camera to the worker + game.setCameraX(centerPosition.px * FIELD_SIZE - WIDTH / 2); + game.setCameraY(centerPosition.py * FIELD_SIZE - HEIGHT / 2); + } + + // else, only select one worker + else { + // increase the index and select the worker + IDLEWORKERINDEX = (IDLEWORKERINDEX + 1) % workers.length; + var worker = workers[IDLEWORKERINDEX]; + game.selectedUnits = [worker]; + this.unitsTab = 0; + + // jump the camera to the worker + game.setCameraX(worker.pos.px * FIELD_SIZE - WIDTH / 2); + game.setCameraY(worker.pos.py * FIELD_SIZE - HEIGHT / 2); + } + } + + // play sound + soundManager.playSound(SOUND.CLICK2); + + return true; + } + + var unitNfoX = (WIDTH - 780) / 2; + + // if unit has cargo + if (game.selectedUnits && game.selectedUnits.length == 1 && game.selectedUnits[0].cargo && game.selectedUnits[0].cargo.length > 0 && game.selectedUnits[0].owner == PLAYING_PLAYER) { + var sortedCargo = _.sortBy(game.selectedUnits[0].cargo, function(e) { + return -e.type.cargoUse; + }); + + var cargo = 0; + + for (var i = 0; i < sortedCargo.length && i < 10; i++) { + if (cargo + sortedCargo[i].type.cargoUse <= 10) { + var x = unitNfoX + 254 + Math.floor(cargo / 2) * 46; + var y = HEIGHT - 94 + (cargo % 2) * 46; + var w = Math.ceil((sortedCargo[i].type.cargoUse - 0.5) / 2) * 40; + var h = sortedCargo[i].type.cargoUse <= 1 ? 40 : 80; + + if (keyManager.x > x && keyManager.y > y && keyManager.x < x + w && keyManager.y < y + h) { + for (var k = 0; k < game.selectedUnits[0].cargo.length; k++) { + if (game.selectedUnits[0].cargo[k] == sortedCargo[i]) { + game.issueOrderToUnits(game.selectedUnits, lists.types.directunload, k); + soundManager.playSound(SOUND.CLICK2); + return true; + } + } + } + + cargo += sortedCargo[i].type.cargoUse; + } else { + i = sortedCargo.length; + } + } + } + + return this.checkKeyEvents('left'); +}; + +// gets called on key pressed +Interface.prototype.keyPressed = function(key) { + if (key == KEY.F10) { + if ($('#optionsWindow')[0].style.display == 'none') { + fadeIn($('#optionsWindow')); + } else { + fadeOut($('#optionsWindow')); + } + } + + if (key == KEY.F9) { + if ($('#chatHistoryWindow')[0].style.display == 'none') { + fadeIn($('#chatHistoryWindow')); + } else { + fadeOut($('#chatHistoryWindow')); + } + } + + // set current tab prio to the next lower one + if (key == KEY.TAB && this.unitTypeWithCurrentTabPrio && game.humanUnitsSelected()) { + var currentPrio = this.unitTypeWithCurrentTabPrio.tabPriority; + + var units = game.selectedUnits; + + var newType = null; + for (var i = 0; i < game.selectedUnits.length; i++) { + if (units[i].type.tabPriority < currentPrio && (!newType || units[i].type.tabPriority > newType.typPrio)) { + newType = units[i].type; + } + } + + // the current prio is the lowest, so new use now the highest + if (!newType) { + newType = units[0]; + for (var i = 1; i < units.length; i++) { + if (units[i].type.tabPriority > newType.typPrio) { + newType = units[i].type; + } + } + } + + if (newType) { + this.unitTypeWithCurrentTabPrio = newType; + } + } + + // check if a command button has this key as a hotkey, and press it in case + for (var i = 0; i < this.buttons.length; i++) { + var cmd = this.buttons[i].command; + + if (cmd.hotkey == key && this.buttons[i].isVisible(this.unitTypeWithCurrentTabPrio) && game.humanUnitsSelected()) { + // if requirement met + var requirement_text = PLAYING_PLAYER.getCommandRequirementText(cmd, game.selectedUnits); + + if (!requirement_text) { + keyManager.order(cmd); + soundManager.playSound(SOUND.INGAMECLICK, 0.7); + } else { + soundManager.playSound(SOUND.NEGATIVE, 0.6); + this.addMessage(requirement_text, 'red', imgs.attentionmark); + } + } + } +}; + +Interface.prototype.drawConstructionProgress = function(x, y, w, h, progress, total) { + var percentage = progress/total; + drawBar(x, y, w, h, Math.min(percentage, 1), 'rgba(0, 160, 230, 1)'); + + var progressText = progress ? `${Math.round(progress)} / ${Math.round(total)}` : ''; + drawText(c, progressText, 'black', '16px LCDSolid', x + w/2 - 18, y + h - 2); +}; + +Interface.prototype.updateFakeCursor = function() { + // Hide them all by default to begin with + for (const key in this.fakeCursors) { + $(this.fakeCursors[key]).hide(); + } + + if (game_state != GAME.PLAYING) { + return; + } + + const cursor = keyManager.getCursor(); + const element = $(this.fakeCursors[cursor]); + + if (this.isPointerLocked()) { + element.show(); + element.css({ left: keyManager.x, top: keyManager.y }); + } +}; + +// represents an ingame command button (attack, make unit, ...) +function Button(command, learn) { + this.init(command, learn); +}; + +Button.prototype.init = function(command, learn) { + this.command = command; // the command, this buttons refers to + this.learn = learn; + this.refresh(); +}; + +Button.prototype.refresh = function() { + if (this.learn) { + this.x = (5 - this.command.learnInterfacePosX) * 72 + 4; // x distance to the right screen border + this.y = (2 - this.command.learnInterfacePosY) * 72 + 4; // x distance to the right screen border + this.hotkey = this.command.learnHotkey; + } else { + this.x = (5 - this.command.interfacePosX) * 72 + 4; // x distance to the right screen border + this.y = (2 - this.command.interfacePosY) * 72 + 4; // x distance to the right screen border + this.hotkey = this.command.hotkey; + } +}; + +Button.prototype.getCC = function() { + return this.command.getValue(this.learn ? 'learnCommandCard' : 'commandCard', game.selectedUnits[0]); +}; + +Button.prototype.draw = function(mouseX, mouseY) { + // if learn and all levels learned, dont draw shit + if (this.learn) { + var canLearn = false; + for (var i = 0; i < game.selectedUnits.length; i++) { + if (game.selectedUnits[i].type.commands[this.command.id_string] && game.selectedUnits[i].abilityLevels[this.command.id] < this.command.requiredLevels.length) { + canLearn = true; + } + } + + if (!canLearn) { + return; + } + } + + // check lvl + var maxLvl = 0; + if (this.command.requiredLevels && this.command.requiredLevels > 0) { + for (var i = 0; i < game.selectedUnits.length; i++) { + if (game.selectedUnits[i].abilityLevels) { + maxLvl = Math.max(game.selectedUnits[i].abilityLevels[this.command.id], maxLvl); + } + } + + if (maxLvl == 0 && !this.learn) { + return; + } + } + + // check if the button is pressed / active + var pressed = (keyManager.command == this.command || ((keyManager.keys[this.hotkey] || (this.contains(mouseX, mouseY) && keyManager.leftMouse)) && keyManager.commandCardWhenPressStart == this.getCC())); + + // calculate total width / height of the image + var d = imgs.button.img.w; + + // draw border img + var borderImg = pressed ? imgs.button2.img : imgs.button.img; + c.drawImage(miscSheet[0], borderImg.x, borderImg.y, borderImg.w, borderImg.h, WIDTH - this.x, HEIGHT - this.y, borderImg.w, borderImg.h); + + // check if requirement is met; if not, use greyscaled image + var requirement_text = PLAYING_PLAYER.getCommandRequirementText(this.command, game.selectedUnits, null, this.learn); + var img = this.command.getTitleImage(requirement_text ? (MAX_PLAYERS + 1) : PLAYING_PLAYER.number); + + // draw button img itself + var scale = Math.min((d * 0.95) / img.w, (d * 0.95) / img.h); + scale = scale > 1 ? Math.floor(scale) : scale; + var x = Math.floor(WIDTH - this.x + (pressed ? 1 : 0) + (d - img.w * scale) / 2); + var y = Math.floor(HEIGHT - this.y + (pressed ? 1 : 0) + (d - img.h * scale) / 2); + + c.drawImage(img.file, img.x, img.y, img.w, img.h, x, y, img.w * scale, img.h * scale); + + // if autocast is on + if (!this.learn && this.command.hasAutocast && game.selectedUnits[0].autocast.contains(this.command.id)) { + drawText(c, 'auto', 'yellow', 'bold 22px LCDSolid', x + (pressed ? 4 : 3), y + (pressed ? 50 : 49)); + } + + // draw hotkey + drawText(c, '[' + getKeyName(this.hotkey) + ']', 'white', 'bold 20px LCDSolid', WIDTH - this.x + (pressed ? 4 : 3), HEIGHT - this.y + (pressed ? 22 : 21)); + + // if cooldowning, draw cooldown + if (!this.learn && this.command.cooldown2) { + // get shortest cooldown + var shortestCD = 9999999; + for (var i = 0; i < game.selectedUnits.length; i++) { + if (game.selectedUnits[i].type == interface_.unitTypeWithCurrentTabPrio) { + shortestCD = Math.min(game.selectedUnits[i].lastTickAbilityUsed[this.command.id] + this.command.getValue('cooldown2', game.selectedUnits[i]) - ticksCounter, shortestCD); + } + } + + if (shortestCD < 9999999 && shortestCD > 0) { + c.fillStyle = 'rgba(0, 0, 0, 0.55)'; + c.fillRect(pressed ? WIDTH - this.x + SCALE_FACTOR : WIDTH - this.x, pressed ? HEIGHT - this.y + SCALE_FACTOR : HEIGHT - this.y, d, d); + drawText(c, Math.ceil(shortestCD / 20), 'white', 'bold 20px LCDSolid', WIDTH - this.x + (pressed ? 4 : 3) + 30, HEIGHT - this.y + (pressed ? 22 : 21) + 20, 60, 'center'); + } + } + + // if hover + if (this.contains(mouseX, mouseY)) { + // draw button hover effect + c.fillStyle = 'rgba(255, 255, 255, 0.15)'; + c.fillRect(pressed ? WIDTH - this.x + SCALE_FACTOR : WIDTH - this.x, pressed ? HEIGHT - this.y + SCALE_FACTOR : HEIGHT - this.y, d, d); + + interface_.drawHoverBox(); + + // draw command name + drawText(c, this.command.name, 'white', 'bold 26px LCDSolid', WIDTH - 340, HEIGHT - INTERFACE_HEIGHT - 270); + + // draw command description + var offset = 0; + var desc = interpreteString(this.command.description, game.selectedUnits[0]).split('#BR'); + for (var i = 0; i < desc.length; i++) { + offset += 24 * drawText(c, desc[i], 'yellow', '16px LCDSolid', WIDTH - 340, HEIGHT - INTERFACE_HEIGHT - 234 + offset, 320, null, null, null, null, 16); + } + + // mana cost + var unit = null; + for (var i = 0; i < game.selectedUnits.length; i++) { + if (game.selectedUnits[i].type == interface_.unitTypeWithCurrentTabPrio) { + unit = game.selectedUnits[i]; + } + } + + if (unit && this.command.getValue('manaCost', unit)) { + offset += 28 + 28 * drawText(c, 'Mana Cost: ' + this.command.getValue('manaCost', unit), 'rgba(200, 0, 200, 1)', '20px LCDSolid', WIDTH - 340, HEIGHT - INTERFACE_HEIGHT - 234 + offset, 320); + } + + // draw condition (if not met) + if (requirement_text) { + offset += 28 * drawText(c, requirement_text, 'red', '20px LCDSolid', WIDTH - 340, HEIGHT - INTERFACE_HEIGHT - 234 + offset, 320); + } + + // draw lvl + if (maxLvl > 0 || this.learn) { + offset += 28 * drawText(c, 'Level: ' + maxLvl, 'white', '20px LCDSolid', WIDTH - 340, HEIGHT - INTERFACE_HEIGHT - 234 + offset, 320); + } + } +}; + +// returns, if a button is visible; argument is the current selected unit type +Button.prototype.isVisible = function(unitType) { + if (unitType && game.selectedUnitsCanPerformOrder(this.command, unitType) && interface_.commandCard == this.getCC()) { + if (this.command.upgrade && this.command.upgrade.maxLevel && (PLAYING_PLAYER.getUpgradeLevel(this.command.upgrade) + PLAYING_PLAYER.upgradeCountInResearch(this.command.upgrade)) >= this.command.upgrade.maxLevel) { + return false; + } + + if (this.command.type == COMMAND.BUILDING_UPGRADE && this.command.improvedBuilding) { + for (var i = 0; i < game.selectedUnits.length; i++) { + if (!game.selectedUnits[i].hasInQueue(this.command.improvedBuilding)) { + return true; + } + } + return false; + } + + return true; + } + + return false; +}; + +// checks if button contains the specified coordinates +Button.prototype.contains = function(x, y) { + return x >= WIDTH - this.x && y >= HEIGHT - this.y && x <= WIDTH - this.x + imgs.button.img.w && y <= HEIGHT - this.y + imgs.button.img.h; +}; + +// does the flying white-yellow-green showish-looking stuff +function Enviroment() { + this.dots = []; + this.img = null; + this.countDots = 190; + this.pixel = document.createElement('canvas'); + this.pixel.width = 1; + this.pixel.height = 1; +}; + +Enviroment.prototype.draw = function() { + for (var i = 0; i < this.dots.length; i++) { + if (!this.dots[i].draw()) { + this.dots.splice(i, 1); + } + } + + while (this.dots.length < this.countDots / SCALE_FACTOR) { + this.dots.push(new Dot(Math.random() * WIDTH, Math.random() * HEIGHT, this.alpha)); + } +}; + +Enviroment.prototype.setFromTheme = function(theme) { + this.pixel.getContext('2d').fillStyle = theme.particleColor; + this.pixel.getContext('2d').fillRect(0, 0, 1, 1); + this.countDots = theme.countDots; +}; + + +function Dot(x, y, alpha) { + this.x = x; + this.y = y; + this.vx = 4; + this.vy = 4; + this.timeToLive = 3 + Math.random() * 3; + this.size = 1; + this.alpha = alpha; +}; + +Dot.prototype.draw = function() { + if (!game_paused) { + this.alpha = Math.max(Math.min(this.alpha + Math.random() * 0.02 - 0.01, env.alpha + 0.15), env.alpha - 0.15); + this.vx += (Math.random() - 0.5); + this.vy += (Math.random() - 0.5); + this.x += this.vx * gameTimeDiff * 10; + this.y += this.vy * gameTimeDiff * 10; + this.timeToLive -= gameTimeDiff; + } + + var camX = game ? game.cameraX : 0; + var camY = game ? game.cameraY : 0; + + while (this.x > camX + WIDTH) { + this.x -= WIDTH; + } + + while (this.x < camX) { + this.x += WIDTH; + } + + while (this.y > camY + HEIGHT) { + this.y -= HEIGHT; + } + + while (this.y < camY) { + this.y += HEIGHT; + } + + // draw + c.globalAlpha = Math.max(Math.min(this.timeToLive, 1), 0); + c.drawImage(env.pixel, this.x - camX, this.y - camY, this.size * SCALE_FACTOR, this.size * SCALE_FACTOR); + c.globalAlpha = 1; + return this.timeToLive > 0; +}; + +// does the flying white-yellow-green showish-looking stuff +function Rain() { + this.drops = []; + this.img = null; + this.countDrops = 500; + + this.middleX = 0; + this.middleY = 0; + + this.lastRainSoundStart = -999; +}; + +Rain.prototype.draw = function() { + if (interface_.noRain.get()) { + musicManager.rain[0].volume = 0; + musicManager.rain[1].volume = 0; + return; + } + var time = ticksCounter / 20 / 60; + + var period = null; + // Each tick, work out if we're in a rain period + for (var i = 0; i < game.rainTime.length; i++) { + if (game.rainTime[i].start < time && game.rainTime[i].end > time) { + period = game.rainTime[i]; + i = game.rainTime.length; + } + } + + // If we're not in a rain period, disable the sound and exit + if (!period) { + musicManager.rain[0].volume = 0; + musicManager.rain[1].volume = 0; + return; + } + + var dropCountDensity = 1; + + if (time < period.start + 0.1) { + dropCountDensity = (time - period.start) / 0.1; + } else if (time > period.end - 0.1) { + dropCountDensity = (period.end - time) / 0.1; + } + + // TODO: don't directly set variables in MusicManager + musicManager.rain[0].volume = 0.6 * soundManager.volume.get() * dropCountDensity; + musicManager.rain[1].volume = 0.6 * soundManager.volume.get() * dropCountDensity; + + if (this.lastRainSoundStart + 2000 < timestamp && (musicManager.rain[0].currentTime <= 0 || musicManager.rain[0].currentTime >= musicManager.rain[0].duration - 0.1) && (musicManager.rain[1].currentTime <= 0 || musicManager.rain[1].currentTime >= musicManager.rain[1].duration - 0.1)) { + if (musicManager.rain[0].currentTime >= musicManager.rain[0].duration - 0.1) { + musicManager.rain[1].play(); + } else { + musicManager.rain[0].play(); + } + + this.lastRainSoundStart = timestamp; + } + + c.strokeStyle = 'rgba(255, 255, 255, 1)'; + c.beginPath(); + + var vert = (WIDTH > (HEIGHT * 2) ? WIDTH : (HEIGHT * 2)); + this.countDrops = Math.floor(WIDTH * 0.5 * dropCountDensity); + vert *= 3 / SCALE_FACTOR; + var hori = vert / 2; + + this.middleX = game.cameraX + WIDTH * 0.4; + this.middleY = game.cameraY + hori * 0.3; + + for (var i = 0; i < this.drops.length; i++) { + if (!this.drops[i].draw(this)) { + this.drops.splice(i, 1); + } + } + + c.globalAlpha = 1; + + while (this.drops.length < this.countDrops / SCALE_FACTOR) { + this.drops.push(new Drop()); + } +}; + + +function Drop() { + this.x = Math.random() * WIDTH; + this.y = Math.random() * WIDTH; + this.z = 1 + Math.random(); +}; + +Drop.prototype.draw = function(rain) { + if (!game_paused) { + this.z -= gameTimeDiff * 1.0; + } + + while (this.x > game.cameraX + WIDTH) { + this.x -= WIDTH; + } + + while (this.x < game.cameraX) { + this.x += WIDTH; + } + + while (this.y > game.cameraY + HEIGHT) { + this.y -= HEIGHT; + } + + while (this.y < game.cameraY) { + this.y += HEIGHT; + } + + // draw + if (this.z > 0) { + var z1 = this.z * SCALE_FACTOR / 3; + + var topX = this.x + (this.x - rain.middleX) * (0.7 - SCALE_FACTOR / 20); + var topY = this.y + (this.y - rain.middleY) * 0.8; + var z2 = z1 + 0.004 * SCALE_FACTOR; + + var x1 = (this.x + (topX - this.x) * z1) - game.cameraX; + var y1 = (this.y + (topY - this.y) * z1) - game.cameraY - z1 * HEIGHT; + + if (y1 > -10 && y1 < (HEIGHT - INTERFACE_HEIGHT) && x1 > -10 && x1 < (WIDTH + 10)) { + c.lineWidth = 1 + SCALE_FACTOR * 0.25 + SCALE_FACTOR * 0.25 * Math.min(this.z, 1); + c.globalAlpha = 0.4 * Math.max(Math.min(z1 * 3, 1), 0.01); + + c.beginPath(); + c.moveTo(x1, y1); + c.lineTo((this.x + (topX - this.x) * z2) - game.cameraX, (this.y + (topY - this.y) * z2) - game.cameraY - z2 * HEIGHT); + c.stroke(); + } + + return true; + } + + return false; +}; + +// the minimap at the bottom left of the screen +function Minimap(game, x, y) { + if (!game) { + throw 'Game is not defined or null. Game must be defined when creating Minimap!'; + } + + this.game = game; + this.x = x; + this.y = y; + + this.mapPings = []; + + this.x_scale = MINIMAP_WIDTH / game.x; + this.y_scale = MINIMAP_HEIGHT / game.y; + + // create additional canvas for fog + this.canvas = document.createElement('canvas'); + this.canvas.width = MINIMAP_WIDTH; + this.canvas.height = MINIMAP_HEIGHT; + + // create additional canvas for screen fog + this.screenCanvas = document.createElement('canvas'); + this.screenCanvas.width = (game.x + DEAD_MAP_SPACE) * FIELD_SIZE / SCALE_FACTOR; + this.screenCanvas.height = (game.y + DEAD_MAP_SPACE) * FIELD_SIZE / SCALE_FACTOR; + + // editor screen fog + this.editorCanvas = document.createElement('canvas'); + this.editorCanvas.width = (game.x + DEAD_MAP_SPACE) * FIELD_SIZE / SCALE_FACTOR; + this.editorCanvas.height = (game.y + DEAD_MAP_SPACE) * FIELD_SIZE / SCALE_FACTOR; + + // darker border + this.screenCanvas.getContext('2d').fillStyle = 'rgba(0, 0, 0, 0.85)'; + this.screenCanvas.getContext('2d').fillRect(0, this.screenCanvas.height - (DEAD_MAP_SPACE + game.getHMValue2(1, 1) * CLIFF_HEIGHT) * 16, this.screenCanvas.width, DEAD_MAP_SPACE * 16); // bottom + this.screenCanvas.getContext('2d').fillRect(this.screenCanvas.width - DEAD_MAP_SPACE * 16, 0, DEAD_MAP_SPACE * 16, this.screenCanvas.height - (DEAD_MAP_SPACE + game.getHMValue2(1, 1) * CLIFF_HEIGHT) * 16); // right + + // darker border screen fog + this.editorCanvas.getContext('2d').fillStyle = 'rgba(0, 0, 0, 0.8)'; + this.editorCanvas.getContext('2d').fillRect(0, this.editorCanvas.height - DEAD_MAP_SPACE * 16, this.editorCanvas.width, DEAD_MAP_SPACE * 16); // bottom + this.editorCanvas.getContext('2d').fillRect(this.editorCanvas.width - DEAD_MAP_SPACE * 16, 0, DEAD_MAP_SPACE * 16, this.editorCanvas.height - DEAD_MAP_SPACE * 16); // right + + // create additional canvas for tiles (groundtiles and doodads) + this.canvasTiles = document.createElement('canvas'); + this.canvasTiles.width = MINIMAP_WIDTH; + this.canvasTiles.height = MINIMAP_HEIGHT; + + // create additional canvas for default groundtiles + this.groundTiles = document.createElement('canvas'); + this.groundTiles.width = MINIMAP_WIDTH; + this.groundTiles.height = MINIMAP_HEIGHT; + + // attack ping stuff + this.tickOfLastAttackPing = -999; + this.positionOfLastAttackPing = null; +}; + +Minimap.prototype.refreshTilesCanvas = function() { + var ctx = this.canvasTiles.getContext('2d'); + + ctx.drawImage(this.groundTiles, 0, 0); + + var tiles = this.game.groundTiles2.concat(this.game.blockingTiles); + + for (var i = 1; i < tiles.length; i++) { + if (!tiles[i].type.ignoreGrid || true) { + ctx.fillStyle = tiles[i].type.minimapColor; + ctx.fillRect(Math.floor((tiles[i].x - 1) * this.x_scale), Math.floor((tiles[i].y - 1) * this.y_scale), Math.ceil(this.x_scale * tiles[i].type.sizeX), Math.ceil(this.y_scale * tiles[i].type.sizeY)); + } + } +}; + +// gets called when the player gets attacked, so he can display warning stuff +Minimap.prototype.attackPingAt = function(pos) { + // if last msg is more than 10 sec ago and msg is not in screen + if (this.tickOfLastAttackPing + 200 < ticksCounter && !(pos.px > (game.cameraX / FIELD_SIZE) && pos.px < ((game.cameraX + WIDTH) / FIELD_SIZE) && pos.py > (game.cameraY / FIELD_SIZE) && pos.py < ((game.cameraY + HEIGHT) / FIELD_SIZE))) { + this.tickOfLastAttackPing = ticksCounter; + this.positionOfLastAttackPing = pos; + + // message + interface_.addMessage('We are under attack', 'red', imgs.underAttack); + soundManager.playSound(SOUND.UNDER_ATTACK); + } +}; + +Minimap.prototype.getFieldFromClick = function(x, y) { + return new Field(x / this.x_scale + 1, (y - HEIGHT + MINIMAP_HEIGHT) / this.y_scale); +}; + +Minimap.prototype.drawCameraBorder = function(y_start, x, y, width, height, field_size, color) { + c.lineWidth = 2; + c.strokeStyle = color; + + var x1 = this.x_scale * x / field_size; + x1 -= x1 % this.x_scale; + var y1 = this.y_scale * y / field_size; + y1 -= y1 % this.y_scale; + var x2 = this.x_scale * (x + width) / field_size; + x2 -= x2 % this.x_scale; + var y2 = this.y_scale * (y + height) / field_size; + y2 -= y2 % this.y_scale; + + c.strokeRect(x1 + this.x, Math.max(y1, 0) + y_start, Math.min(x2 - x1, MINIMAP_WIDTH), Math.min(y2 - y1, MINIMAP_HEIGHT)); +}; + +Minimap.prototype.draw = function() { + var y_start = this.y < 0 ? HEIGHT + this.y : this.y; + + // draw tiles image + c.drawImage(this.canvasTiles, this.x, y_start, MINIMAP_WIDTH, MINIMAP_HEIGHT); + + // buildings + for (var i = 0; i < this.game.buildings2.length; i++) { + if (this.game.buildings2[i].seenBy[PLAYING_PLAYER.team.number]) { + var x = this.x_scale * (this.game.buildings2[i].x - 1); + x -= x % this.x_scale; + var y = this.y_scale * (this.game.buildings2[i].y - 1); + y -= y % this.y_scale; + c.fillStyle = (this.game.buildings2[i].owner.controller == CONTROLLER.NONE && !this.game.buildings2[i].type.startGold) ? 'rgba(77, 166, 174, 0.9)' : this.game.buildings2[i].owner.getAllyColor(); + c.fillRect(x + this.x, y + y_start, this.x_scale * this.game.buildings2[i].type.size, this.y_scale * this.game.buildings2[i].type.size); + } + } + + // units + for (var i = 0; i < this.game.units.length; i++) { + if (PLAYING_PLAYER.team.canSeeUnit(this.game.units[i]) && PLAYING_PLAYER.team.canSeeUnitInvisible(this.game.units[i])) { + var x = this.x_scale * (this.game.units[i].pos.x - 1); + x -= x % this.x_scale; + var y = this.y_scale * (this.game.units[i].pos.y - 1); + y -= y % this.y_scale; + c.fillStyle = this.game.units[i].owner.getAllyColor(); + c.fillRect(x + this.x, y + y_start, this.x_scale * 1.5, this.y_scale * 1.5); + } + } + + // draw fog canvas + if (game_state == GAME.PLAYING) { + c.drawImage(this.canvas, this.x, y_start, MINIMAP_WIDTH, MINIMAP_HEIGHT); + } + + // draw attack ping + if (this.tickOfLastAttackPing + 100 > ticksCounter && this.positionOfLastAttackPing) { + // calculate coords of pos on minimap + var x = this.positionOfLastAttackPing.px * this.x_scale; + var y = this.positionOfLastAttackPing.py * this.y_scale + y_start; + var age = ticksCounter - this.tickOfLastAttackPing; + + // set drawing attributes + c.strokeStyle = 'red'; + c.lineWidth = 2; + + // draw horizontal line + c.beginPath(); + c.moveTo(0, y); + c.lineTo(MINIMAP_WIDTH, y); + c.stroke(); + + // draw vertical line + c.beginPath(); + c.moveTo(x, y_start); + c.lineTo(x, WIDTH); + c.stroke(); + + var offset = Math.max(50 - age * 4, 7); + c.strokeRect(Math.max(x - offset, 0), Math.max(y - offset, y_start), Math.min(offset * 2, MINIMAP_WIDTH - (x - offset)), Math.min(offset * 2, HEIGHT - (y - offset))); + + if (age > 10) { + c.fillStyle = 'red'; + c.globalAlpha = 0.8 - (age / 10) % 0.8; + c.fillRect(Math.max(x - 6, 0), Math.max(y - 6, y_start), Math.min(6 * 2, MINIMAP_WIDTH - (x - 6)), Math.min(6 * 2, HEIGHT - (y - 6))); + c.globalAlpha = 1; + } + } + + // player pings + for (var i = 0; i < this.mapPings.length; i++) { + var age = Date.now() - this.mapPings[i].time; + + if (age > 7000) // ping is too old, kill it + { + this.mapPings.splice(i, 1); + i--; + } else { + var ageAlpha = age > 5000 ? ((7000 - age) / 2000) : 1; + + c.globalAlpha = ageAlpha; + + var x = this.mapPings[i].field.x * this.x_scale; + var y = this.mapPings[i].field.y * this.y_scale + y_start; + + c.strokeStyle = 'yellow'; + c.lineWidth = 2; + c.beginPath(); + c.moveTo(0, y); + c.lineTo(MINIMAP_WIDTH, y); + c.stroke(); + c.beginPath(); + c.moveTo(x, y_start); + c.lineTo(x, HEIGHT); + c.stroke(); + var d = Math.max(50 - age / 15, 7); + c.strokeRect(Math.max(x - d, 0), Math.max(y - d, y_start), Math.min(d * 2, MINIMAP_WIDTH - (x - d)), Math.min(d * 2, HEIGHT - (y - d))); + + if (age > 10) { + c.fillStyle = 'yellow'; + c.globalAlpha = (0.8 - (age / 600) % 0.8) * ageAlpha; + c.fillRect(Math.max(x - 6, 0), Math.max(y - 6, y_start), Math.min(6 * 2, MINIMAP_WIDTH - (x - 6)), Math.min(6 * 2, HEIGHT - (y - 6))); + c.globalAlpha = 1; + } + } + } + + // screen borders + const ourBorderColor = game.followVision ? 'rgb(120, 120, 120)' : 'white'; + this.drawCameraBorder( + y_start, game.cameraX, game.cameraY, WIDTH, HEIGHT, FIELD_SIZE, ourBorderColor, + ); + if (PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR) { + for (const player of game.players) { + if (!player) { + continue; + } + if (game.visionSetting != 0 && game.visionSetting != player.number) { + continue; + } + this.drawCameraBorder( + y_start, player.cameraX, player.cameraY, player.cameraWidth, + player.cameraHeight, player.fieldSize, player.getColor()); + } + } + + + if (game_state == GAME.EDITOR) { + c.drawImage(miscSheet[0], imgs.interfaceMapBorder.img.x, imgs.interfaceMapBorder.img.y, imgs.interfaceMapBorder.img.w, imgs.interfaceMapBorder.img.h, -16, HEIGHT - 200, imgs.interfaceMapBorder.img.w * 2, imgs.interfaceMapBorder.img.h * 2); + } +}; + +// true: fog, false: no fog +Minimap.prototype.setFog = function(x, y, fog) { + var alpha = (game.globalVars && game.globalVars.useDarkMask) ? darkFogMaskAlpha[fog] : fogMaskAlpha[fog]; + + var h = this.game.getHMValue2(x, y); + + var x_ = (x - 1) * 16; + var y_ = (y - 1 - h * CLIFF_HEIGHT) * 16; + + var h_ = 16; + + if (this.game.getHMValue2(x, y - 1) > h) { + y_ -= CLIFF_HEIGHT * 16; + h_ += CLIFF_HEIGHT * 16; + } + + for (var i = 0; i < CLIFF_HEIGHT; i++) { + if (this.game.getHMValue2(x, y + 1 + i) > h) { + h_ = Math.max(h_ - (CLIFF_HEIGHT - i) * 16, 0); + } + } + + if (alpha == 0) { + this.canvas.getContext('2d').clearRect(Math.floor((x - 1) * this.x_scale), Math.floor((y - 1) * this.y_scale), Math.ceil(this.x_scale), Math.ceil(this.y_scale)); + + this.screenCanvas.getContext('2d').clearRect(x_, y_, 16, h_); + } else { + this.canvas.getContext('2d').fillStyle = 'rgba(0, 0, 0, ' + alpha + ')'; + this.canvas.getContext('2d').clearRect(Math.floor((x - 1) * this.x_scale), Math.floor((y - 1) * this.y_scale), Math.ceil(this.x_scale), Math.ceil(this.y_scale)); + this.canvas.getContext('2d').fillRect(Math.floor((x - 1) * this.x_scale), Math.floor((y - 1) * this.y_scale), Math.ceil(this.x_scale), Math.ceil(this.y_scale)); + + this.screenCanvas.getContext('2d').fillStyle = 'rgba(0, 0, 0, ' + alpha + ')'; + this.screenCanvas.getContext('2d').clearRect(x_, y_, 16, h_); + this.screenCanvas.getContext('2d').fillRect(x_, y_, 16, h_); + } +}; + +function MapEditor(width = 64, height = 64, themeName = 'Grass', defaultHeight = 0) { + game = new Game(); + + let hm = ''; + for (let x = 1; x <= width; x++) { + for (let y = 1; y <= height; y++) { + hm += defaultHeight; + } + } + + const theme = getThemeByName(themeName); + var map = { + name: 'unnamed', + x: width, + y: height, + units: [], + buildings: [], + tiles: [], + theme: themeName, + defaultTiles: theme.defaultTiles, + heightmap: hm, + }; + + game.loadMap(map, null, null, null, true); + worker.postMessage({ + what: 'start-game', + map: map, + network_game: false, + game_state: game_state, + networkPlayerName: networkPlayerName, + }); + + this.selectedItemType = null; // type of the selected unit / building / doodad (when clicked on a button in the interface) + this.terrainModifier = 0; + this.player = 1; // current selected player + this.dragging = false; + this.almostDragging = false; + this.draggStartPos = null; + this.draggingUnitsOriginalpositions = []; + this.startHeight = 0; + this.lastClickedField = null; + this.randomTree = false; + + this.MirroringMode = Object.freeze({ + NONE: 0, + DIAGONAL: 1, + HORIZONTAL: 2, + VERTICAL: 3, + FOURWAYS: 4, + }); + this.updateMirroringMode(this.MirroringMode.NONE); + + this.createButtons(); + + // clipboard for history, copy and paste + this.clipboard = new MapEditorClipboard(); +}; + +MapEditor.prototype.createButtons = function() { + this.trees = { + fullList: [], + // 1_1 : [] + // 2_2 : [] + // 3_3 : [] + }; + + // ICONS (i could not use lists... 'lists' are empty, lists.buildingTypes for example are empty too..) + this.unitsIcon = game.unitTypes[0]; // soldier + this.buildingIcon = game.buildingTypes[4]; // house + this.treeIcon = tileTypes[0]; // tree 1 + this.tileIcon = tileTypes[30]; // stone 4 + this.decorationIcon = tileTypes[69]; // grass 22 + + // dividing tiles into blocking and non-blocking + var blocking = []; + var nonblocking = []; + // building tree lists for the random trees as well + + for (var i = 0; i < tileTypes.length; i ++) { + if (tileTypes[i].blocking && !tileTypes[i].isTree) // if its not a tree + { + blocking.push(tileTypes[i]); + } else if (tileTypes[i].isTree) { + this.trees.fullList.push(tileTypes[i]); + + var listName = tileTypes[i].sizeX + '_' + tileTypes[i].sizeY; + + if (!this.trees[listName]) { + this.trees[listName] = []; + } + + this.trees[listName].push(tileTypes[i]); + + tileTypes[i].randomTree = true; // flag + } else { + nonblocking.push(tileTypes[i]); + } + } + + // create ui + var types = [game.unitTypes, game.buildingTypes, this.trees.fullList, blocking, nonblocking]; + var typesDescription = ['Units', 'Buildings', 'Trees', 'Tiles', 'Decoration']; + var icons = [this.unitsIcon, this.buildingIcon, this.treeIcon, this.tileIcon, this.decorationIcon]; + + $('#typesWindow').remove(); + var typesWindows = document.createElement('div'); + typesWindows.style.cssText = 'position: absolute; left: 10px; top: 2px; height: 64px; right: 450px;'; + typesWindows.id = 'typesWindow'; + $('#mapEditorInterface').append(typesWindows); + + for (var i = 0; i < types.length; i++) { + $('#mapEditorTypeButtons' + i).remove(); + var win = document.createElement('div'); + win.id = 'mapEditorTypeButtons' + i; + win.className = 'editorTypeClass'; + win.style.cssText = 'position: absolute; left: 10px; top: ' + (1 * 64 + 6) + 'px; height: 128px; right: 500px; overflow: auto;'; + $('#mapEditorInterface').append(win); + if (i!=0) { + $(win).hide(); + } + + // creating the types buttons + var firstElement = types[i][0]; + var typeButton = document.createElement('button'); + typeButton.id = 'editorTypeButton_' + i; + typeButton.className = 'editorTypeButton'; + typeButton.title = typesDescription[i]; + if (i!=0) { + $(typeButton).css('background-color', '#99cccc'); + } else { + $(typeButton).css('background-color', '#ccffcc'); + $(typeButton).addClass('TabSelected'); + } + + var img = icons[i].getTitleImage(); + var w = img.w; + var h = img.h; + + if (w > h) { + h = 60 * (h / w); + w = 60; + } else { + w = 60 * (w / h); + h = 60; + } + + var w2 = img.file.width * (w / img.w); + var h2 = img.file.height * (h / img.h); + var x = img.x * (w2 / img.file.width); + var y = img.y * (h2 / img.file.height); + + typeButton.innerHTML = ''; + $(typesWindow).append(typeButton); + typeButton.onclick = function() { + $('.TabSelected').css('background-color', '#99cccc'); + $('.editorTypeButton').removeClass('TabSelected'); + $(this).addClass('TabSelected'); + $(this).css('background-color', '#ccffcc'); + var id = this.id.split('_')[1]; + $('.editorTypeClass').hide(); + $('#mapEditorTypeButtons'+id).show(); + editor.selectedItemType = undefined; + editor.randomTree = false; + game.selectedUnits = []; + soundManager.playSound(SOUND.CLICK); + }; + + for (var k = 0; k < types[i].length; k++) { + if (!types[i][k].isDefault && types[i][k].getTitleImage) // not generate buttons for default (= default ground textures), they are created randomly at map load and can not be placed manually + { + var img = types[i][k].getTitleImage(); + var button = document.createElement('button'); + win.appendChild(button); + button.id = 'editorTypeButton_' + i + '_' + k; + button.className = 'editorTypeButton'; + button.title = types[i][k].name + (types[i][k].description ? ' - ' + types[i][k].description : ''); + button.type_ = types[i][k]; + + var w = img.w; + var h = img.h; + if (w > h) { + h = 60 * (h / w); + w = 60; + } else { + w = 60 * (w / h); + h = 60; + } + + var w2 = img.file.width * (w / img.w); + var h2 = img.file.height * (h / img.h); + var x = img.x * (w2 / img.file.width); + var y = img.y * (h2 / img.file.height); + button.innerHTML = ''; + + button.onclick = function() { + editor.randomTree = false; + editor.selectedItemType = this.type_; + game.selectedUnits = []; + soundManager.playSound(SOUND.CLICK); + }; + } + } + } + + for (var treeIndex in this.trees) { + if (treeIndex != 'fullList') { + var tree = this.trees[treeIndex][0]; + var treeSize = tree.sizeX; + var treeIconIndex = 0; + var button = document.createElement('button'); + if (treeSize == 1) { + treeIconIndex = 0; + } else if (treeSize == 2) { + treeIconIndex = 5; + } else if (treeSize == 3) { + treeIconIndex = 7; + } + button.randomTree = true; + button.title = 'Places a ' + treeSize + 'x' + treeSize + ' random tree!'; + button.className = 'editorTypeButton'; + button.randomTreeSize = treeSize; + button.treeIconIndex = treeIconIndex; + button.type_ = tree; + + $('#mapEditorTypeButtons2').append(button); + + var img = tree.getTitleImage(); + var w = img.w; + var h = img.h; + + if (w > h) { + h = 60 * (h / w); + w = 60; + } else { + w = 60 * (w / h); + h = 60; + } + var w2 = img.file.width * (w / img.w); + var h2 = img.file.height * (h / img.h); + var x = img.x * (w2 / img.file.width); + var y = img.y * (h2 / img.file.height); + button.innerHTML = 'Random ' + treeSize + 'x' + treeSize + ' tree'; + + button.onclick = function() { + editor.randomTree = true; + editor.selectedItemType = this.type_; + game.selectedUnits = []; + soundManager.playSound(SOUND.CLICK); + }; + } + } +}; + +MapEditor.prototype.reload = function(map = game.export_(false)) { + game = new Game(); + game.loadMap(map, null, null, null, true); + worker.postMessage({ what: 'start-game', editorLoad: true, game_state: GAME.EDITOR, map, players: null }); +}; + +// remove a unity/entity/tile/building (MapObject) from all context +// accepts a single entity or an array +MapEditor.prototype.removeObjects = function(mapObjects, saveHistory) { + var units = []; + if (mapObjects.constructor === Array) { + for (var i = 0; i < mapObjects.length; i++) { + var u = mapObjects[i]; + + if (saveHistory) { + editor.clipboard.history.addObject(u, 'DeleteMapObject'); + } + + if (u.id) { + units.push({ hasId: true, id: u.id }); + } else { + units.push({ hasId: false, x: u.pos.px, y: u.pos.py, type: u.type.name }); + } + + if (u.type.isBuilding) { + game.buildings.erease(u); + game.buildings2.erease(u); + } else if (u.type.isUnit) { + game.units.erease(u); + } else if (u.type.isGround) { + game.groundTiles2.erease(u); + } else if (u.type.blocking) { + game.blockingTiles.erease(u); + } + + // unblock fields in game block array (if building or tile and non-blocking) + if ((u.type.isBuilding || u.type.isTile) && !u.type.ignoreGrid) { + u.switchBlockingTotal(false); + } + } + worker.postMessage({ what: 'deleteUnitEditor', units: units }); + } else { + var u = mapObjects; + if (u.id) { + units.push({ hasId: true, id: u.id }); + } else { + units.push({ hasId: false, x: u.pos.px, y: u.pos.py, type: u.type.name }); + } + + if (u.type.isBuilding) { + game.buildings.erease(u); + game.buildings2.erease(u); + } else if (u.type.isUnit) { + game.units.erease(u); + } else if (u.type.isGround) { + game.groundTiles2.erease(u); + } else if (u.type.blocking) { + game.blockingTiles.erease(u); + } + + // unblock fields in game block array (if building or tile and non-blocking) + if ((u.type.isBuilding || u.type.isTile) && !u.type.ignoreGrid) { + u.switchBlockingTotal(false); + } + worker.postMessage({ what: 'deleteUnitEditor', units: units }); + } +}; + +// gets called every frame, newClick = true, when onmousedown +MapEditor.prototype.click = function(x, y, newClick, code) { + if (y < HEIGHT - 212) // if click in map + { + const mouseGameX = (x + game.cameraX) / FIELD_SIZE; + const mouseGameY = (y + game.cameraY) / FIELD_SIZE; + // TODO: Make field into const + var field = game.getFieldFromPos(mouseGameX, mouseGameY, this.terrainModifier == 0 || this.selectedItemType); + + if (code == 1) // if left click + { + const fieldDiagonal = field.getExactCopy().mirror(game.x, game.y, true, true); + const fieldHorizontal = field.getExactCopy().mirror(game.x, game.y, true, false); + const fieldVertical = field.getExactCopy().mirror(game.x, game.y, false, true); + + let fields; + switch (this.mirroring) { + case this.MirroringMode.NONE: + fields = [field]; + break; + case this.MirroringMode.DIAGONAL: + fields = [field, fieldDiagonal]; + break; + case this.MirroringMode.HORIZONTAL: + fields = [field, fieldHorizontal]; + break; + case this.MirroringMode.VERTICAL: + fields = [field, fieldVertical]; + break; + case this.MirroringMode.FOURWAYS: + fields = [field, fieldDiagonal, fieldHorizontal, fieldVertical]; + break; + } + + // get field + var blocked = false; + + if (this.clipboard.copyclipboard) { + this.clipboard.copyclipboard = null; + return; // wont need to waste + } + + var tree = undefined; + if (this.randomTree && this.selectedItemType) { + var listName = this.selectedItemType.sizeX+'_'+this.selectedItemType.sizeY; + tree = this.trees[listName][Math.floor(Math.random()*this.trees[listName].length)]; + } + + fields.forEach((element) => { + var msg = { + what: 'editorClick', + x: element.px, + y: element.py, + type: this.selectedItemType ? this.selectedItemType.name : null, + playerIndex: this.player, + heightMod: this.terrainModifier, + startHeight: this.startHeight, + newClick: newClick, + }; + + if (tree) { + msg.type = tree.name; + } + worker.postMessage(msg); + }); + + // if we clicked on a unit, that is selected, enable dragging + if (newClick) { + var clickedUnit = game.getUnitAtPosition((x + game.cameraX) / FIELD_SIZE, (y + game.cameraY) / FIELD_SIZE); + this.almostDragging = clickedUnit && clickedUnit.type.isUnit && game.selectedUnits.contains(clickedUnit); + this.dragging = false; + this.draggStartPos = new Field(x, y, true); + + // save all original positions + this.draggingUnitsOriginalpositions = []; + for (var i = 0; i < game.selectedUnits.length; i++) { + this.draggingUnitsOriginalpositions.push(game.selectedUnits[i].pos.getCopy()); + } + } + + if (this.almostDragging && (x != this.draggStartPos.px || y != this.draggStartPos.py)) { + this.almostDragging = false; + this.dragging = true; + } + + if (this.dragging) { + var offsetp = new Field((x - this.draggStartPos.x) / FIELD_SIZE, (y - this.draggStartPos.y) / FIELD_SIZE, true); + var offset = new Field(Math.floor((x - this.draggStartPos.x) / FIELD_SIZE), Math.floor((y - this.draggStartPos.y) / FIELD_SIZE)); + + for (var i = 0; i < game.selectedUnits.length; i++) { + var unit = game.selectedUnits[i]; + if (unit.type.isUnit) { + var field = unit.type.getNextFreePositionFrom(this.draggingUnitsOriginalpositions[i].add(offsetp)); + worker.postMessage({ what: 'changeUnitPos', x: field.px, y: field.py, id: unit.id }); + } + } + } + } else if (code == 3) // right click (set waypoint, if possible) + { + for (var i = 0; i < game.selectedUnits.length; i++) { + if (game.selectedUnits[i].type.canHaveWaypoint) { + var u = game.selectedUnits[i]; + + if (keyManager.interfaceHotkeyPressed('queue')) { + if (u.waypoint.length < 19) { + u.waypoint.push(field); + } + } else { + u.waypoint = [field]; + } + + var arr = []; + for (var k = 0; k < u.waypoint.length; k++) { + arr.push(u.waypoint.px, u.waypoint.py); + } + + worker.postMessage({ what: 'setWP', id: game.selectedUnits[i].id, waypoint: arr }); + } + } + } + } +}; + +function higherTerrain() { + editor.selectedItemType = null; + editor.terrainModifier = 1; +}; + +function lowerTerrain() { + editor.selectedItemType = null; + editor.terrainModifier = -1; +}; + +function addRamp() { + editor.selectedItemType = null; + editor.terrainModifier = 0.5; +}; + +function killRamp() { + editor.selectedItemType = null; + editor.terrainModifier = -0.5; +}; + +MapEditor.prototype.updateMirroringMode = function(mode) { + if (!isNaN(mode)) { + this.mirroring = mode; + } else { + this.mirroring = (this.mirroring + 1) % Object.keys(this.MirroringMode).length; + } + + const mirrorButtonData = { + [this.MirroringMode.NONE]: { + image: '../play/imgs/mirror-none.png', + title: `Mirroring mode is not enabled + + Click to mirror map object placement diagonally`, + }, + [this.MirroringMode.DIAGONAL]: { + image: '../play/imgs/mirror-diagonal.png', + title: `Mirroring diagonally + + Click to mirror map object placement horizontally`, + }, + [this.MirroringMode.HORIZONTAL]: { + image: '../play/imgs/mirror-horizontal.png', + title: `Mirroring horizontally + + Click to mirror map object placement vertically`, + }, + [this.MirroringMode.VERTICAL]: { + image: '../play/imgs/mirror-vertical.png', + title: `Mirroring vertically + + Click to mirror map object placement four ways`, + }, + [this.MirroringMode.FOURWAYS]: { + image: '../play/imgs/mirror-4way.png', + title: `Mirroring four ways + + Click to disable map mirroring`, + }, + }; + + $('#mirrorButton').css('background', `url(${mirrorButtonData[this.mirroring].image})`); + $('#mirrorButton').css('background-size', '100% 100%'); + $('#mirrorButton').tooltip('option', 'content', mirrorButtonData[this.mirroring].title); +}; + +MapEditor.prototype.keyPressed = function(key) { + switch (key) { + case KEY.DELETE: + case KEY.BACKSPACE: + case KEY.E: + this.removeObjects(game.selectedUnits, true); + break; + // case KEY.U: this.clipboard.history.undo(); break; + // case KEY.Q: this.clipboard.history.debug(); break; + case KEY.A: killRamp(); break; + case KEY.S: addRamp(); break; + case KEY.D: higherTerrain(); break; + case KEY.F: lowerTerrain(); break; + case KEY.Q: testMap(); break; + case KEY.G: this.updateMirroringMode(); break; + default: return; + } + + soundManager.playSound(SOUND.CLICK); +}; + +MapEditor.prototype.draw = function() { + game.draw(); + + // if im with some copy on my clipboard + // disabled for now, this will aways be null for now + + if (this.clipboard.copyclipboard) { + /* + var selected = this.clipboard.copyclipboard.units; + var relativeUnit = this.clipboard.copyclipboard.relativeUnit; // this will be used to coordinates to other guys + var references = this.clipboard.copyclipboard.reference; + + var ct = selected.length; // recursive while loop, way faster then for loops for JS. good for rendering + while(ct--) { + + var thisUnit = selected[ct].type; + var field = thisUnit.getFieldFromMousePos(); + var hm = game.getHMValue4(field.x, field.y); + + var difX = references[ct].x; + var difY = references[ct].y; + // if its a building or a doodle + if(thisUnit.isBuilding || thisUnit.isTile) + { + c.globalAlpha = 0.5; + thisUnit.draw(thisUnit.ignoreGrid ? keyManager.x + game.cameraX : field.x, thisUnit.ignoreGrid ? keyManager.y + game.cameraY : field.y - hm * CLIFF_HEIGHT); + c.globalAlpha = 1; + // its a unit + + + } + else + { + c.globalAlpha = 0.5; + thisUnit.draw(keyManager.x + game.cameraX + difX, keyManager.y + game.cameraY + difY); + c.globalAlpha = 1; + } + + vtester called this + i will remove when implemented + + } + */ + } + // draw current Item @ mouse pos, if we have selected a type + else if (this.selectedItemType && keyManager.y < HEIGHT - 212) { + // if its a building or doodad + if (this.selectedItemType.isBuilding || this.selectedItemType.isTile) { + var field = this.selectedItemType.getFieldFromMousePos(); + var hm = game.getHMValue4(field.x, field.y); + + // draw + c.globalAlpha = 0.5; + this.selectedItemType.draw(this.selectedItemType.ignoreGrid ? keyManager.x + game.cameraX : field.x, this.selectedItemType.ignoreGrid ? keyManager.y + game.cameraY : field.y - hm * CLIFF_HEIGHT); + c.globalAlpha = 1; + + // draw red box with alpha over the image when blocked, otherwise white alpha, for every grid field the building covers + if (!this.selectedItemType.ignoreGrid) { + for (x = field.x; x < field.x + this.selectedItemType.sizeX; x++) { + for (y = field.y; y < field.y + this.selectedItemType.sizeY; y++) { + var f = new Field(x, y); + var distanceAllowed = true; + var nextGoldmine = game.getNextBuildingOfType(f, null, false, 'startGold'); + var nextCC = game.getNextBuildingOfType(f, null, false, 'takesGold'); + + if (this.selectedItemType.takesGold && nextGoldmine && nextGoldmine.pos.distanceTo2(f) < game.getMineDistance()) { + distanceAllowed = false; + } + + if (this.selectedItemType.startGold && nextCC && nextCC.pos.distanceTo2(f) < game.getMineDistance()) { + distanceAllowed = false; + } + + c.fillStyle = (game.fieldIsBlockedForBuilding(f.x, f.y) || !distanceAllowed) ? 'rgba(200, 0, 0, 0.25)' : 'rgba(' + game.theme.line_red + ', ' + game.theme.line_green + ', ' + game.theme.line_blue + ', 0.3)'; + c.fillRect((f.x - 1) * FIELD_SIZE - game.cameraX, (f.y - 1 - hm * CLIFF_HEIGHT) * FIELD_SIZE - game.cameraY, FIELD_SIZE, FIELD_SIZE); + } + } + } + } + + // if its a unit + else { + c.globalAlpha = 0.5; + this.selectedItemType.draw(keyManager.x + game.cameraX, keyManager.y + game.cameraY); + c.globalAlpha = 1; + } + } + // terrain editor transparent tile to show where its gonna be built + else if (this.terrainModifier != 0) { + var f = 'Tree 1'.toUnitType().getFieldFromMousePos(); + + c.fillStyle = 'rgba(' + game.theme.line_red + ', ' + game.theme.line_green + ', ' + game.theme.line_blue + ', 0.3)'; + c.fillRect((f.x - 1) * FIELD_SIZE - game.cameraX, (f.y - 1 - game.getHMValue4(f.x, f.y) * CLIFF_HEIGHT) * FIELD_SIZE - game.cameraY, FIELD_SIZE, FIELD_SIZE); + } + + // refresh mouse cursor pos display + var field = game.getFieldFromPos(); + $('#cursorPosDiv').html('X: ' + (Math.round(field.px * 100) / 100) + 'You got ' + splitMsg[1] + ' gold
Opponent found, starting game in
') + .add(`${counter}
`) + .insertInto('#ladderWindow'); + + soundManager.playSound(SOUND.LADDER_START); + const decrementCounterAndStart = () => { + $('#ladderStartCounter').html(counter--); + if (counter > 0) { + setTimeout(decrementCounterAndStart, 1000); + return; + } + + $('#ladderWindow').hide(); + + uimanager.showLoadingScreen(map, players); + network.send('load-ladder-map'); + + setTimeout(() => { + game = new Game(); + game.loadMap(map, players, aiRandomizer); + game_state = GAME.PLAYING; + mapData = ''; + worker.postMessage({ what: 'start-game', map, players, aiCommit: AIManager.getAICommit(), aiRandomizer, network_game, game_state, networkPlayerName }); + + // Send initial order cycle(s) + for (let i = 0; i < TICKS_DELAY - 1; i++) { + network.send(JSON.stringify({ tick: i, orders: [] })); + } + }, 50); + }; + decrementCounterAndStart(); + }; + } else if (splitMsg[0] == 'bing-msg' && splitMsg.length >= 2) { + bingMsg(splitMsg[1]); + } else if (splitMsg[0] == 'lcg-battle') { + littlechatgame(JSON.parse(splitMsg[1])); + } else if (splitMsg[0] == 'lcg-rank') { + var p = document.createElement('p'); + + var time = document.createElement('span'); + time.className = 'time'; + time.innerHTML = getFormattedTime() + ' '; + + var span = document.createElement('span'); + + span.innerHTML = 'Server: Littlechatgame record for ' + splitMsg[1] + ': ' + splitMsg[2] + ' wins / ' + splitMsg[3] + ' losses. Rank: ' + getRankCode(splitMsg[4]) + ' (Global ranking #' + splitMsg[5] + ')'; + + p.appendChild(time); + p.appendChild(span); + + addToChatWindow(p); + } else if (splitMsg[0] == 'lcg-top') { + var p = document.createElement('p'); + + var time = document.createElement('span'); + time.className = 'time'; + time.innerHTML = getFormattedTime() + ' '; + + var span = document.createElement('span'); + + span.innerHTML = 'Server: Littlechatgame top ranked players:'; + + for (var i = 1; i < splitMsg.length; i += 2) { + span.innerHTML += '${i + 1}.
`) + .addHook(() => this.slotNumbers.push($(`#${ID}`))); + } + builder.add('Players
') + .add('') // Host only, Multiplayer only + .addHook(() => $('#inviteButton').click(() => this.__showInviteWindow())) + .add('') // Host only + .addHook(() => $('#addCpuButton').click(() => this.__addCPU())) + .add('Spectators
') + .add('Friends
'); + + // Add buttons for all online friends + for (const name of FriendsList.onlineFriends()) { + builder.add(generateInviteButton(name, true)); + } + + builder.add('You have no online friends.
'); + } else { + builder.add('Create an account to add friends and invite them to lobbies.
'); + } + + builder.insertInto('#inviteWindowContents'); + }; + + PlayersList.onChange(refreshInviteWindow); + FriendsList.onChange(refreshInviteWindow); + }; + + LobbyPlayerManager_.prototype.__initNetworkListeners = function() { + // GAME.LOBBY when creating a new game, and GAME.SKIRMISH when changing map + network.registerListener([GAME.LOBBY, GAME.SKIRMISH], 'singleplayer-game-created', (splitMsg) => { + network.onMapFile = (map) => this.__joinLobby(false, 0, map, splitMsg[1], true); + }); + + network.registerListener(GAME.LOBBY, 'game-created', (splitMsg) => { + network.onMapFile = (map) => this.__joinLobby(true, splitMsg[1], map, splitMsg[3], true); + }); + + network.registerListener(GAME.LOBBY, 'game-joined', (splitMsg) => { + network.onMapFile = (map) => this.__joinLobby(true, splitMsg[1], map, splitMsg[4], false); + }); + + network.registerListener(GAME.LOBBY, 'you-are-new-host', () => this.isHost = true); + + network.registerListener(GAME.LOBBY, 'change-map-file', (splitMsg) => { + network.onMapFile = (map) => { + addChatMsg('Server', `Host changed map to ${splitMsg[1]}`); + this.__setMapFromServer(map, splitMsg[2]); + }; + }); + + network.registerListener(GAME.LOBBY, 'youve-been-kicked', () => { + this.active = false; + game_state = GAME.LOBBY; + setChatFocus(true); + }); + + // Message sent to all players whenever a multiplayer lobby changes + network.registerListener(GAME.LOBBY, 'infostring', (splitMsg) => { + const serverPlayers = JSON.parse(splitMsg[1]).players; + + const oldPlayersArr = this.__getPlayersInLobby(); + this.__refreshPlayersFromServer(serverPlayers); + const newPlayersArr = this.__getPlayersInLobby(); + + // Print a message for each joined / left player if we didn't join just now + if (oldPlayersArr.length > 0) { + const oldPlayers = new Multiset(oldPlayersArr); + const newPlayers = new Multiset(newPlayersArr); + + oldPlayers.forEach((p) => { + for (let i = 0; i < oldPlayers.count(p) - newPlayers.count(p); i++) { + addChatMsg('Server', `Player ${p} left`); + } + }); + + newPlayers.forEach((p) => { + for (let i = 0; i < newPlayers.count(p) - oldPlayers.count(p); i++) { + addChatMsg('Server', `Player ${p} joined`); + } + }); + } + soundManager.playSound(SOUND.CLICK); + }); + + network.registerListener(GAME.LOBBY, 'start-game', () => this.active = false); + }; + + LobbyPlayerManager_.prototype.__getAllowedAIs = function() { + let ai_names = ['Default'].concat(this.isModded ? [] : ['Random AI']); + ai_names = ai_names.concat(AIManager.getNames( + /* includeRegularAI=*/!this.isModded, + /* includeCustomAI=*/!this.isMultiplayer, + )); + return ai_names; + }; + + // Generates the interactive bar that is inserted into the slot with the given position given a + // reference to the corresponding contents object in this.slotContents + LobbyPlayerManager_.prototype.__generateSlotBar = function(position, contents) { + const barID = uniqueID('bar'); + const teamButtonID = uniqueID('team'); + const builder = new HTMLBuilder(); + builder.add(`Everyone is on the same team!
(${duration})
`) + + .add(` `) + .addHook(() => $(`#${watchBtnID}`).click(addClickSound(() => { + replayFile = JSON.parse(replay.replay); + network.send(`get-map-for-replay<<$${replayFile.map}`); + fadeOut($('#replaysListWindow')); + }))) + + .add(` `) + .addHook(() => $(`#${saveBtnID}`).click(addClickSound(() => { + const blob = new Blob([replay.replay], { type: 'text/plain;charset=utf-8' }); + saveAs(blob, replay.name + '.json'); + }))) + + .add(``) + .addHook(() => $(`#${deleteBtnID}`).click(addClickSound(() => { + this.replays.remove((r) => r.id == replay.id); + this.updateLocalStorage(); + this.refreshContents(); + }))) + .add('No saved replays
'); + } + + builder.insertInto('#replaysListContent'); + }; + + ReplaysWindow_.prototype.saveReplay = function() { + // Insert the replay into the beginning of the list and save + this.replays.unshift({ + id: Math.floor(performance.now() * 1000), + name: game.getReplayName(), + replay: game.getReplayFile(), + }); + this.updateLocalStorage(); + }; + + ReplaysWindow_.prototype.updateLocalStorage = function() { + // Loop while we are unable to save to localStorage and there are still old replays to remove + do { + try { + const raw = Compression.compressToString(JSON.stringify(this.replays)); + localStorage.setItem('Replays', raw); + return; + } catch (e) { + // Try removing the oldest replay + console.log('killing replay'); + this.replays.pop(); + } + } while (this.replays.length > 0); + + // Something is wrong with their browser's localStorage + }; + + return new ReplaysWindow_(); +})(); + +var elements = []; + +const UIManagerSingleton = (() => { + function UIManager_() { + // Map from ID to a UI elements (UIElement or UIWindow) + this.registry = {}; + } + + UIManager_.prototype.registerUIElement = function(uielement) { + this.registry[uielement.id] = uielement; + }; + + UIManager_.prototype.getUIElement = function(id) { + return this.registry[id]; + }; + + UIManager_.prototype.draw = function() { + for (let i in this.registry) { + this.registry[i].refreshVisibility(); + } + }; + + + return new UIManager_(); +})(); // UIManagerSingleton + +// [DEPRECATE] Use the singleton UIManagerSingleton instead and remove this +// manages all the HTML UIElements; constructor creates everything +function UIManager() { + const tipMessages = [ + 'Try and get up to 10 workers per castle and gold mine for a high gold income.', + 'Building a second castle near a different gold mine early in the game can double your gold income and give you a big advantage.', + 'Scouting is key! Try and find out what your opponent is doing early game using a worker, wolf, bird, or raider.', + 'Your workers get less efficient after four workers on a mine, so spread them evenly on each gold mine to increase income.', + 'Refresh to get your tips!', + 'Remember to spend your gold! Gold doesn\'t earn interest sitting in your bank and would be more useful as buildings or units.', + 'Start building houses and castles before you get supply blocked, or else you will have to wait for them to complete.', + 'Invisible units can be slightly seen with your eyes, but that doesn\'t mean your units can see them.', + 'Invisible units killing your stuff? You can get detection with Birds by researching Bird Detection, with Airships by researching Airship Telescope Extension, or with Watchtowers by upgrading them.', + 'Mechanical units can be repaired with multiple workers at the same time, but buildings can only be repaired by one worker at a time.', + 'You can customize your hotkeys in the menu.', + 'You can change the volume levels in the menu.', + 'Check out LittleChatGame by typing \'/lcg help\' in the chat.', + 'Play 5 ranked games to be placed in a division.', + 'A handful of replays of your most recent games are automatically saved for you. View them using the Replays button.', + 'Did you just have a particularly amazing game? Save a replay of it by pressing \'Save Replay\' on the score screen.', + 'Are you interested in creating your own maps or mods? Check out the Editor.', + 'Join a clan and meet new friends! You can also create a clan if you want.', + ]; + + var tipsDiv = new UIElement('div', 'tipsDiv', function() { + return game_state == GAME.LOBBY || game_state == GAME.LOGIN || game_state == GAME.REGISTER || game_state == GAME.RECOVERY; + }); + $('#tipsDiv').html('Tip: ' + _.sample(tipMessages)); + + /* + * Win / Loss Window + */ + this.winLossWindow = new UIWindow('winLossWindow', function() { + return game && game.gameHasEnded && game_state == GAME.PLAYING; + }, true, 'Game Statistics', true); + this.winLossWindow.addScrollableSubDiv('winLossTextArea'); + + var quitLogo = document.createElement('img'); + quitLogo.id = 'quitLogo'; + quitLogo.src = 'imgs/Logo.png'; + quitLogo.className = 'quitLogo'; + $('#winLossWindow').append(quitLogo); + + // Quit Game Button + $('#winLossWindow').append(this.createButton('winLossWindowQuitButton', 'Quit', () => exitGame())); + + /* + * chat history window + */ + var chatHistoryWindow = new UIWindow('chatHistoryWindow', function() { + return true; + }, true, 'Chat History'); + chatHistoryWindow.addScrollableSubDiv('chatHistorytextContainer'); + + + /* + * Lobby div (invis, holds all teh elements) + */ + var lobbyDiv = new UIElement('div', 'lobbyDiv', () => network.connected && game_state == GAME.LOBBY && !LobbyPlayerManager.active); + + /* + * lobby chat window + */ + var lobbyChatWindow = new UIWindow('lobbyChatWindow', null, false, 'Chat'); + lobbyChatWindow.addScrollableSubDiv('lobbyChatTextArea'); + $('#lobbyDiv').append(lobbyChatWindow.domElement); + + // TODO: move these to dedicated files for lobby chat and game lobby + /* + * lobby chat input + */ + new HTMLBuilder() + .add('Stats for ' + types_[i].name + ''; + + for (var k = 0; k < fields_.length; k++) { + if (types_[i][fields_[k]]) { + var val = Object.prototype.toString.call(types_[i][fields_[k]]) === '[object Array]' ? types_[i][fields_[k]][0] : types_[i][fields_[k]]; + + if (val) { + for (var j = 0; j < fieldTypes.length; j++) { + if (fieldTypes[j].name == fields_[k] && fieldTypes[j].displayScale) { + val *= fieldTypes[j].displayScale; + j = fieldTypes.length; + } + } + + // round val to two decimal places + val = Math.round(val * 100) / 100; + str += '
' + fields_[k] + ': ' + val + '
'; + } + } + } + + div.innerHTML = str; + addToChatWindow(div); + + found = true; + } + } + + if (!found) { + var p = document.createElement('p'); + p.innerHTML = 'Sorry, ' + name + ' not found'; + addToChatWindow(p); + } + } else if (value == '/help' || value == '/man' || value == '/info') { + var div = document.createElement('div'); + var str = 'Chat commands:'; + str += '
[/ping] send a ping command to the server and see how good your connection is
'; + str += '[/stats] get stats for a specific unit, building, ability or upgrade (for example: \'/stats soldier\')
'; + str += '[/ignore] ignore a player (for example \'/ignore player123\')
'; + str += '[/unignore] unignore a player (for example \'/unignore player123\')
'; + str += '[/ignorelist] show your current ignorelist
'; + str += '[/lcg help] show info about Littlechatgame, a small game you can play in the lobby chat
'; + div.innerHTML = str; + addToChatWindow(div); + } else if (value == '/ignorelist') { + addChatMsg('Server', 'Your ignore list: ' + AccountInfo.ignores.join(', ')); + } else if (value.substr(0, 8) == '/ignore ') { + network.send('ignore<<$' + value.split(' ')[1]); + } else if (value.substr(0, 10) == '/unignore ') { + network.send('unignore<<$' + value.split(' ')[1]); + } else if (value == '/lcg help' || value == '/lcg man' || value == '/lcg info' || value == '/lch help' || value == '/lch man' || value == '/lch info') { + var div = document.createElement('div'); + var str = 'Littlechatgame is a game taking place in lobby chat. Send an army of unit-emotes to fight an opponent. Comands:
[/lcg fight]'; + str += ' Send your army into battle. Type \'/lcg fight\' followed by the emotes you want to send, for example \'/lcg fight Catapult Archer Soldier Soldier\'. '; + str += 'Valid emotes / units: Soldier, Archer, Wolf, Mage, Priest, Werewolf, Catapult, Ballista, Dragon, Worker. '; + str += 'You can only send emotes that you have unlocked. The emotes you type first will stand in the back and the emotes you type last will stand in the front in battle. Your army can consist of max 16 supply of units. '; + str += '
[/lcg stats] Get Littlechatgame related stats for a player. Example: \'/lcg stats player123\'
'; + str += '[/lcg top] Get a list of the top ranked lcg players
'; + str += '[/lcg units] Get information on all the available units / emotes
'; + div.innerHTML = str; + addToChatWindow(div); + } else if (value == '/modstuff' && (AccountInfo.isMod || AccountInfo.isAdmin)) { + var div = document.createElement('div'); + var str = 'Mod commands:
'; + str += '[/killgame] kill a running game (for example when an offensive name is used). Type \'/killgame\' followed by the index of the game (beginning with 0 !!!), for example \'/killgame 2\' would kill the 3rd game in the list. It is possible to kill multiple games at once by typing for example \'/killgame 0 3 4\'
'; + if (AccountInfo.isMod2 || AccountInfo.isAdmin) { + str += '[/reward] give gold to a player. Type \'/reward\' followed by the name of the player and then the amount of gold to reward, for example \'/reward mage 2000\'. Rewards can not be higher than 10000. Player will see popup notification (if he is online).
'; + } + str += '[/killmap] delete a map (for example when using offensive content or name). Type \'/killmap\' followed by the name of the map, for example \'/killmap la petite (deleted maps are not entirely deleted, theres still a backup stored, so no worries if you misstype or something, it can always be restored)
'; + str += '[/plinfo] shows some information about a player. Works on guests, too (for example \'/plinfo player123\')
'; + str += '[/unban] unbans a player account
'; + str += '[/setwelcomemessage] (ADMIN) sets the welcome message to whatever follows the command
'; + div.innerHTML = str; + addToChatWindow(div); + } else if (value.startsWith('/setwelcomemessage')) { + const msg = value.split(' ').slice(1).join(' '); + const msgObject = { message: msg }; + network.send(`set-welcome-message<<$${JSON.stringify(msgObject)}`); + } else { + network.send('chat<<$' + value); + } + + $(input).val(''); + } + }; + + $('#lobbyChatInput').keydown((e) => { + if (keyManager.getKeyCode(e) == KEY.ENTER && network.connected) { + this.chatFunction($('#lobbyChatInput')); + } + }); + + /* + * Options Window + */ + var optionsWindow = new UIWindow('optionsWindow', function() { + return true; + }, true, 'Options', true, function(key) { + if (key == KEY.N && (game_state == GAME.PLAYING || game_state == GAME.EDITOR)) { + $('#optionsQuitButton')[0].click(); + } + + if (key == KEY.ESC) { + fadeOut($('#optionsWindow')); + soundManager.playSound(SOUND.CLICK); + } + }); + + // Buttons at the top + var optionsButtonsDiv = document.createElement('div'); + optionsButtonsDiv.id = 'optionsButtonsDiv'; + $('#optionsWindow').append(optionsButtonsDiv); + + // Toggle options + var optionsChecklistDiv = document.createElement('div'); + optionsChecklistDiv.id = 'optionsChecklistDiv'; + $('#optionsWindow').append(optionsChecklistDiv); + + // Fullscreen Button + $('#optionsButtonsDiv').append(this.createButton('optionsFullscreenButton', 'Fullscreen [F8]', function() { + toggleFullscreen(document.documentElement); + })); + + + // sound volume label + var soundVolumeLabel = document.createElement('p'); + soundVolumeLabel.id = 'soundVolumeLabel'; + soundVolumeLabel.innerHTML = 'Sound Effects Volume'; + $('#optionsChecklistDiv').append(soundVolumeLabel); + + // sound volume slider + var optionsSoundButton = document.createElement('div'); + optionsSoundButton.id = 'optionsSoundButton'; + $('#soundVolumeLabel').append(optionsSoundButton); + $('#optionsSoundButton').slider({ slide: (event, ui) => soundManager.volume.set(ui.value / 100) }); + $('#optionsSoundButton').slider('value', soundManager.volume.get() * 100); + + // music volume label + var musicVolumeLabel = document.createElement('p'); + musicVolumeLabel.id = 'musicVolumeLabel'; + musicVolumeLabel.innerHTML = 'Music Volume'; + $('#optionsChecklistDiv').append(musicVolumeLabel); + + // music volume slider + var optionsMusicButton = document.createElement('div'); + optionsMusicButton.id = 'optionsMusicButton'; + $('#musicVolumeLabel').append(optionsMusicButton); + $('#optionsMusicButton').slider({ slide: (event, ui) => musicManager.volume.set(ui.value / 100) }); + $('#optionsMusicButton').slider('value', musicManager.volume.get() * 100); + + // LCG volume label + const lcgVolumeLabel = document.createElement('p'); + lcgVolumeLabel.id = 'lcgVolumeLabel'; + lcgVolumeLabel.innerHTML = 'LittleChatGame Volume'; + lcgVolumeLabel.title = 'Set the percentage of the total Sound Effects Volume that Littlechatgame sounds will play at.'; + $('#optionsChecklistDiv').append(lcgVolumeLabel); + + // LCG volume slider + const lcgVolumeSlider = document.createElement('div'); + lcgVolumeSlider.id = 'lcgVolumeSlider'; + $('#lcgVolumeLabel').append(lcgVolumeSlider); + $('#lcgVolumeSlider').slider({ slide: (event, ui) => lcgVolume.set(ui.value / 100) }); + Initialization.onDocumentReady(() => $('#lcgVolumeSlider').slider('value', lcgVolume.get() * 100)); + + // scroll speed label + var scrollSpeedLabel = document.createElement('p'); + scrollSpeedLabel.id = 'scrollSpeedLabel'; + scrollSpeedLabel.innerHTML = 'Scroll Speed'; + $('#optionsChecklistDiv').append(scrollSpeedLabel); + + // scroll speed slider + var scrollSpeedButton = document.createElement('div'); + scrollSpeedButton.id = 'scrollSpeedButton'; + $('#scrollSpeedLabel').append(scrollSpeedButton); + $('#scrollSpeedButton').slider({ slide: (event, ui) => interface_.scrollSpeed.set(ui.value * 50) }); + Initialization.onDocumentReady(() => $('#scrollSpeedButton').slider('value', interface_.scrollSpeed.get() / 50)); + + // Setting for when messages can pop up + const popupMsgsBuilder = new HTMLBuilder(); + { + const selectID = uniqueID(); + popupMsgsBuilder + .add('Message pop-ups') + .add(`
') + .addHook(() => $(`#${selectID}`).val(Chats.popupChats.get())) + .addHook(() => $(`#${selectID}`).change(() => Chats.popupChats.set($(`#${selectID}`).val()))) + .appendInto('#optionsChecklistDiv'); + } + + // Mouse scroll on off Checkbox + var scrollLabel = document.createElement('p'); + scrollLabel.id = 'scrollLabel'; + scrollLabel.innerHTML = 'Enable mouse scroll in non fullscreen mode'; + scrollLabel.title = 'When this option is deactivated, the screen will not scroll when you move the cursor to the border of the screen when you are not in fullscreen mode.'; + $('#optionsChecklistDiv').append(scrollLabel); + + var scrollCheckbox = document.createElement('input'); + scrollCheckbox.id = 'scrollCheckbox'; + scrollCheckbox.type = 'checkbox'; + scrollCheckbox.onclick = addClickSound(() => interface_.mouseScrollWhenWindowed.set(scrollCheckbox.checked)); + Initialization.onDocumentReady(() => scrollCheckbox.checked = interface_.mouseScrollWhenWindowed.get()); + $('#scrollLabel').append(scrollCheckbox); + + // Pointer lock + var pointerLockLabel = document.createElement('p'); + pointerLockLabel.id = 'pointerLockLabel'; + pointerLockLabel.innerHTML = 'Lock mouse pointer to screen ingame'; + pointerLockLabel.title = 'The mouse pointer will stay locked inside the game screen after the first click. This may cause the cursor to be slightly laggy.'; + $('#optionsChecklistDiv').append(pointerLockLabel); + + var pointerLockCheckbox = document.createElement('input'); + pointerLockCheckbox.id = 'pointerLockCheckbox'; + pointerLockCheckbox.type = 'checkbox'; + pointerLockCheckbox.onclick = addClickSound(() => { + interface_.setPointerLockEnabled(pointerLockCheckbox.checked); + }); + Initialization.onDocumentReady(() => { + pointerLockCheckbox.checked = interface_.pointerLockEnabled.get(); + }); + $('#pointerLockLabel').append(pointerLockCheckbox); + + // Middle Mouse invert scroll + var mmLabel = document.createElement('p'); + mmLabel.id = 'mmLabel'; + mmLabel.innerHTML = 'Invert middle mouse button scrolling'; + mmLabel.title = 'Invert the middle mouse scrolling (you can press and hold middle mouse button ingame to scroll).'; + $('#optionsChecklistDiv').append(mmLabel); + + var mmCheckbox = document.createElement('input'); + mmCheckbox.id = 'mmCheckbox'; + mmCheckbox.type = 'checkbox'; + mmCheckbox.onclick = addClickSound(() => keyManager.mmScrollInvert.set(mmCheckbox.checked)); + Initialization.onDocumentReady(() => mmCheckbox.checked = keyManager.mmScrollInvert.get()); + $('#mmLabel').append(mmCheckbox); + + // HP bars only when not full hp + var hpBarsLabel = document.createElement('p'); + hpBarsLabel.id = 'hpBarsLabel'; + hpBarsLabel.innerHTML = 'Show HP bars when full HP'; + hpBarsLabel.title = 'Show a unit or building\'s HP bars when it has full HP.'; + $('#optionsChecklistDiv').append(hpBarsLabel); + + var hpBarsCheckbox = document.createElement('input'); + hpBarsCheckbox.id = 'hpBarsCheckbox'; + hpBarsCheckbox.type = 'checkbox'; + hpBarsCheckbox.onclick = addClickSound(() => interface_.showFullHPBars.set(hpBarsCheckbox.checked)); + Initialization.onDocumentReady(() => hpBarsCheckbox.checked = interface_.showFullHPBars.get()); + $('#hpBarsLabel').append(hpBarsCheckbox); + + // no main manu music + var noMainMenuMusicLabel = document.createElement('p'); + noMainMenuMusicLabel.id = 'noMainMenuMusicLabel'; + noMainMenuMusicLabel.innerHTML = 'No menu / lobby music'; + noMainMenuMusicLabel.title = 'Don\'t play music in the menus / lobby.'; + $('#optionsChecklistDiv').append(noMainMenuMusicLabel); + + var noMainMenuMusicCheckbox = document.createElement('input'); + noMainMenuMusicCheckbox.id = 'noMainMenuMusicCheckbox'; + noMainMenuMusicCheckbox.type = 'checkbox'; + noMainMenuMusicCheckbox.onclick = addClickSound(() => musicManager.noMainMenuMusic.set(noMainMenuMusicCheckbox.checked)); + noMainMenuMusicCheckbox.checked = musicManager.noMainMenuMusic.get(); + $('#noMainMenuMusicLabel').append(noMainMenuMusicCheckbox); + + // no rain + var noRainLabel = document.createElement('p'); + noRainLabel.id = 'noRainLabel'; + noRainLabel.innerHTML = 'No rain effects'; + noRainLabel.title = 'Disable in-game rain effects'; + $('#optionsChecklistDiv').append(noRainLabel); + + var noRainCheckbox = document.createElement('input'); + noRainCheckbox.id = 'noRainCheckbox'; + noRainCheckbox.type = 'checkbox'; + noRainCheckbox.onclick = addClickSound(() => interface_.noRain.set(noRainCheckbox.checked)); + Initialization.onDocumentReady(() => noRainCheckbox.checked = interface_.noRain.get()); + $('#noRainLabel').append(noRainCheckbox); + + // no guest direct messages + var noGuestDMLabel = document.createElement('p'); + noGuestDMLabel.id = 'noGuestDMLabel'; + noGuestDMLabel.innerHTML = 'No direct messages from guests'; + noGuestDMLabel.title = 'Disable direct messages from guests'; + $('#optionsChecklistDiv').append(noGuestDMLabel); + + var noGuestDMCheckbox = document.createElement('input'); + noGuestDMCheckbox.id = 'noGuestDMCheckbox'; + noGuestDMCheckbox.type = 'checkbox'; + noGuestDMCheckbox.onclick = addClickSound(() => interface_.noGuestDM.set(noGuestDMCheckbox.checked)); + Initialization.onDocumentReady(() => noGuestDMCheckbox.checked = interface_.noGuestDM.get()); + $('#noGuestDMLabel').append(noGuestDMCheckbox); + + // open custom hotkeys window + $('#optionsButtonsDiv').append(this.createButton('hotkeyWindowButton', 'Hotkeys', function() { + Hotkeys.showWindow(); + fadeOut($('#optionsWindow')); + soundManager.playSound(SOUND.CLICK); + })); + + // open FAQ window + $('#optionsButtonsDiv').append(this.createButton('openFaqButton', 'F.A.Q.', function() { + fadeIn($('#faqWindow')); + soundManager.playSound(SOUND.CLICK); + })); + + // open window to load custom AI + $('#optionsButtonsDiv').append(this.createButton('loadAIButton', 'Load custom AI', function() { + // create new input and simulate a click on it and set function (we have to make a new one, so onchange works when the same replay is loaded 2 times in a row) + fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.click(); + + fileInput.onchange = function() { + var file = fileInput.files[0]; + if (file) { + var reader = new FileReader(); + reader.onload = (e) => { + AIManager.sendCustomAIToWorker(e.target.result); + }; + reader.readAsBinaryString(file); + } + }; + + soundManager.playSound(SOUND.CLICK); + })); + $('#loadAIButton')[0].title = 'Load custom AI file (you can write your own AI and load it into the game. For more information on that, check the blog)'; + + this.optionsPauseButton = new UIElement('button', 'optionsPauseButton', function() { + return game_state == GAME.PLAYING; + }); + this.optionsPauseButton.domElement.innerHTML = 'Pause [Pause]'; + $('#optionsButtonsDiv').append(this.optionsPauseButton.domElement); + this.optionsPauseButton.domElement.onclick = function() { + pauseGame(); + }; + // + this.quitGameButton = new UIElement('button', 'optionsQuitButton', function() { + return game_state == GAME.PLAYING || game_state == GAME.EDITOR; + }); + this.quitGameButton.domElement.innerHTML = 'Quit [N]'; + $('#optionsWindow').append(this.quitGameButton.domElement); + this.quitGameButton.domElement.onclick = () => { + fadeOut($('#optionsWindow')); + + // Exiting the editor into the lobby + if (game_state == GAME.EDITOR) { + game_state = GAME.LOBBY; + soundManager.playSound(SOUND.CLICK); + network.send('leave-game'); + worker.postMessage({ what: 'end-game' }); + } else { + exitGame(); + } + }; + + /* + * Queries Window + */ + var queriesWindow = new UIWindow('queriesWindow', function() { + return true; + }, true, 'Friends', true); + queriesWindow.addScrollableSubDiv('chatListSubDiv'); + queriesWindow.addScrollableSubDiv('friendsSubdiv'); + queriesWindow.addSubDiv('addFriendSubDiv'); + + var p = document.createElement('p'); + p.innerHTML = '» Messages'; + p.id = 'queriesTitle'; + $('#queriesWindow').append(p); + + // button daddy + var buttonDaddy = document.createElement('div'); + buttonDaddy.id = 'buttonDaddy'; + $('#lobbyDiv').append(buttonDaddy); + + // Create Multi Button + $('#buttonDaddy').append(this.createButton('lobbyCreateButton', 'Play', function() { + MapSelection.openWindow(MapSelection.SelectionAction.MULTIPLAYER); + })); + $('#lobbyCreateButton').prop('title', 'Click here to create a new multiplayer game. Alternatively you can join an existing game by simply clicking it (you find them in the games window on the left).'); + + // Create Single Button + $('#buttonDaddy').append(this.createButton('singleplayerButton', 'Play vs CPU', function() { + MapSelection.openWindow(MapSelection.SelectionAction.SINGLEPLAYER); + })); + $('#singleplayerButton').prop('title', 'Click here to create a new singleplayer game and play vs computer opponents.'); + + // Ladder Button + $('#buttonDaddy').append(this.createButton('ladderButton', 'Ranked Match', function() { + network.send(JSON.stringify({ message: 'get-ladder-maps' })); + soundManager.playSound(SOUND.CLICK); + })); + $('#ladderButton')[0].title = 'Play a ranked 1v1 match via auto matchmaking.'; + + // Map Editor Button + $('#buttonDaddy').append(this.createButton('mapEditorButton', 'Editor', function() { + network.send('cancel-ladder'); + fadeOut($('#ladderWindow')); + + game_state = GAME.EDITOR; + editor = new MapEditor(); + soundManager.playSound(SOUND.CLICK); + network.send('leave-lobby'); + })); + $('#mapEditorButton')[0].title = 'The editor is a powerful tool that allows you to create your own maps and mods.'; + + // Replay Button + $('#buttonDaddy').append(this.createButton('replayButton', 'Replays', addClickSound(() => { + ReplaysWindow.openWindow(); + }))); + $('#replayButton')[0].title = 'View replays of your recently played games and load replays that you saved on your computer.'; + + // Create Tutorial Button + if (!hideTutorials.get()) { + var tutorialButtonSpan = document.createElement('span'); + tutorialButtonSpan.id = 'tutorialButtonSpan'; + $('#buttonDaddy').append(tutorialButtonSpan); + + $('#tutorialButtonSpan').append(this.createButton('tutorialButton', 'Tutorials', () => { + MapSelection.openWindow(MapSelection.SelectionAction.TUTORIAL); + })); + $('#tutorialButton').prop('title', 'Here you can play several tutorials, that will introduce you to the game\'s basic features'); + + $('#tutorialButtonSpan').append(this.createButton('killTutorialButtonButton', 'x', addClickSound(() => { + displayConfirmPrompt( + 'Hide this button? You will still be able to play the tutorials by clicking \'Play vs CPU\' and searching for \'tutorial\'', + killTutorialButton, + () => {}, + ); + }))); + + $('#killTutorialButtonButton')[0].title = 'Hide this button and dont show it again. You will still be able to play the tutorials by clicking \'Play vs CPU\' and searching for \'tutorial\'.'; + } + + /* + * ingame Chat Input + */ + this.ingameInput = new UIElement('input', 'ingameChatInput', function() { + return game_state == GAME.PLAYING && this.active; + }); + $('#ingameChatInput')[0].type = 'text'; + $('#ingameChatInput')[0].onkeydown = function(e) { + if (keyManager.getKeyCode(e) == KEY.ENTER && uimanager.ingameInput.active) { + if (this.value.length > 0) { + if (network_game && network && network.socket) { + const chatCommand = game.commands.find((c) => c.chat_str == this.value); + if (chatCommand) { + game.issueOrderToUnits(game.selectedUnits, chatCommand); + } else if (this.value.substr(0, 8) == '/ignore ') { + network.send('ignore<<$' + this.value.split(' ')[1]); + } else if (this.value.substr(0, 10) == '/unignore ') { + network.send('unignore<<$' + this.value.split(' ')[1]); + } else { + if (this.value == '/ping') { + timeOfLastPingSent = Date.now(); + } + + network.send('chati<<$' + $('#ingameChatDropdown')[0].value + '<<$' + this.value); + } + } else { + var msg = PLAYING_PLAYER.name + ': ' + this.value; + interface_.chatMsg(msg); + game.addChatMsgToLog(msg); + } + + this.value = ''; + } + + uimanager.ingameInput.active = false; + } + + // tab between ally chat and all chat + else if (keyManager.getKeyCode(e) == KEY.TAB) { + $('#ingameChatDropdown')[0].selectedIndex = $('#ingameChatDropdown')[0].selectedIndex == 0 ? 1 : 0; + return false; + } + + // close input on ESC + else if (keyManager.getKeyCode(e) == KEY.ESC) { + uimanager.ingameInput.active = false; + } + }; + + // allies / all dropdown for ingame chat + var ingameChatDropdown = new UIElement('select', 'ingameChatDropdown', function() { + return game_state == GAME.PLAYING && uimanager.ingameInput.active; + }); + $('#ingameChatDropdown').html(''); + + + /* + * Info Window & Info Window 2 + */ + this.infoWindow = new UIWindow('infoWindow', function() { + return true; + }, true, null, true); + this.infoWindow.addScrollableSubDiv('infoWindowTextArea'); + + var infoWindow2 = new UIWindow('infoWindow2', function() { + return true; + }, true, null, true); + infoWindow2.addScrollableSubDiv('infoWindowTextArea2'); + + + /* + * Ladder Window + */ + var ladderWindow = new UIWindow('ladderWindow', function() { + return true; + }, false, null, true); + $('#ladderWindow')[0].style.display = 'none'; + + + /* + * Replay control panel + */ + const replayControlWindow = new UIWindow('replayControlWindow', function() { + return game_state == GAME.PLAYING && game.replay_mode; + }, false, null, false, function(key) { + if (key == KEY.PLUS) { + $('#replayPlusButton')[0].click(); + } else if (key == KEY.MINUS) { + $('#replayMinusButton')[0].click(); + } + }); + replayControlWindow.blocksCanvas = false; + + const replayPlusButton = this.createButton('replayPlusButton', '+', function() { + setReplaySpeed(replaySpeedIndex + 1); + soundManager.playSound(SOUND.CLICK); + }); + + const replayMinusButton = this.createButton('replayMinusButton', '-', function() { + setReplaySpeed(replaySpeedIndex - 1); + soundManager.playSound(SOUND.CLICK); + }); + + const replayShowSpeed = document.createElement('p'); + replayShowSpeed.id = 'replayShowSpeed'; + replayShowSpeed.innerHTML = '1x'; + + const replayPlayButton = this.createButton('replayPlayButton', '>', function() { + pauseGame(); + soundManager.playSound(SOUND.CLICK); + + if (game_paused) { + $('#replayPlayButton').text('||').css({ 'font-size': '10px' }); + } else { + $('#replayPlayButton').text('>').css({ 'font-size': '' }); + } + }); + + $('#replayControlWindow').append(replayPlusButton); + $('#replayControlWindow').append(replayMinusButton); + $('#replayControlWindow').append(replayShowSpeed); + $('#replayControlWindow').append(replayPlayButton); + + + /* + * Map Editor New Map Window + */ + var newMapWindow = new UIWindow('newMapWindow', function() { + return true; + }, true, 'New Map', true); + newMapWindow.addScrollableSubDiv('newMapWindowSubdiv'); + + const newMapBuilder = new HTMLBuilder(); + newMapBuilder + .add('Quick Links
'; + + var linksMenu2 = document.createElement('div'); + linksMenu2.innerHTML += ''; + linksMenu2.innerHTML += ''; + linksMenu2.innerHTML += ''; + linksMenu2.innerHTML += ''; + linksMenu2.innerHTML += ''; + linksMenu2.innerHTML += ''; + + linksMenu.domElement.appendChild(linksMenu2); + + // legalities + const legalities = new UIElement('div', 'legalities', () => game_state == GAME.LOGIN || game_state == GAME.REGISTER || game_state == GAME.LOBBY || game_state == GAME.RECOVERY); + new HTMLBuilder() + .add('Imprint | ').addHook(() => $('#imprintLink').click(showImprint)) + .add('Terms & Conditions | ').addHook(() => $('#agbLink').click(showAGB)) + .add('Privacy Policy').addHook(() => $('#dseLink').click(showDSE)) + .insertInto('#legalities'); + + // loading window + var loadingWindow = document.createElement('div'); + loadingWindow.id = 'loadingWindow'; + document.body.appendChild(loadingWindow); + + // loading text + var loadingText = document.createElement('p'); + loadingWindow.appendChild(loadingText); + loadingText.id = 'loadingText'; + loadingText.innerHTML = 'loading...'; + + // map preview img + var loadingScreenMapImg = document.createElement('img'); + loadingScreenMapImg.id = 'loadingScreenMapImg'; + loadingWindow.appendChild(loadingScreenMapImg); + + // map name + var mapScreenName = document.createElement('p'); + mapScreenName.id = 'mapScreenName'; + loadingWindow.appendChild(mapScreenName); + + // players display + var playersDisplay = document.createElement('p'); + playersDisplay.id = 'playersDisplay'; + loadingWindow.appendChild(playersDisplay); + + // dark screen div + var darkScreenDiv = document.createElement('div'); + darkScreenDiv.id = 'darkScreenDiv'; + document.body.appendChild(darkScreenDiv); + + // put elements in array + elements.push( + this.winLossWindow, + lobbyDiv, + this.ingameInput, + ingameChatDropdown, + mapEditorInterface, + ingameMenuButton, + ingameChatHistoryButton, + replayControlWindow, + this.quitGameButton, + this.optionsPauseButton, + versionNumber, + tipsDiv, + linksMenu, + clansButton, + friendsButton, + achivementsButton, + emotesButton, + legalities, + faqContainer, + ); + + this.onKeyElements = [ + replayControlWindow, + optionsWindow, + ]; + + + // add hover sound events to all buttons + $('button').mouseenter(function() { + soundManager.playSound(SOUND.ZIP, 0.3); + }); + + + Initialization.onUIGenerated(() => { + // make windows draggable (with jquery ui) + $('.draggable').draggable({ + drag: onDrag, + cancel: 'p, input, select, textarea, button, #personalTextDiv, .nodrag', + }); + }); +}; + +// is called every frame, checks for all elements if they should be drawn and saves the number of active elements (that block canvas input) +UIManager.prototype.draw = function() { + for (let i = 0; i < elements.length; i++) { + elements[i].refreshVisibility(); + } +}; + +// is calles onkey; calls onkey on all active ui elements +UIManager.prototype.onKey = function(key) { + for (var i = 0; i < this.onKeyElements.length; i++) { + if (this.onKeyElements[i].domElement.style.display == 'inline' || (this.onKeyElements[i].domElement.style.display != 'none' && this.onKeyElements[i].domElement.style.visibility == 'visible')) { + this.onKeyElements[i].onKey(key); + } + } +}; + +// create a new html button +UIManager.prototype.createButton = function(id, caption, onclick) { + var button = document.createElement('button'); + + if (id) { + button.id = id; + } + + button.innerHTML = caption; + button.onclick = onclick; + + return button; +}; + +UIManager.prototype.showLoadingScreen = function(map, players) { + if (map && map.img) { + $('#loadingScreenMapImg')[0].src = map.img; + $('#loadingScreenMapImg')[0].style.display = 'inline'; + } else { + $('#loadingScreenMapImg')[0].style.display = 'none'; + } + + if (map && map.name) { + $('#mapScreenName')[0].innerHTML = map.name; + $('#mapScreenName')[0].style.display = 'inline'; + } else { + $('#mapScreenName')[0].style.display = 'none'; + } + + + // show players + if (players) { + var str = ''; + + for (var i = 0; i < players.length; i++) { + if (players[i].controller != CONTROLLER.SPECTATOR) { + if (players[i].clan) { + str += '[' + players[i].clan + '] '; + } + + str += players[i].name; + + if (players[i + 1] && players[i + 1].team == players[i].team && players[i + 1].controller != CONTROLLER.SPECTATOR) { + str += ' & '; + } else if (players[i + 1] && players[i + 1].team != players[i].team && players[i + 1].controller != CONTROLLER.SPECTATOR) { + str += ' vs '; + } + } + } + + $('#playersDisplay')[0].innerHTML = str; + $('#playersDisplay')[0].style.display = 'inline'; + } else { + $('#playersDisplay')[0].style.display = 'none'; + } + + $('#loadingWindow')[0].style.display = 'inline'; +}; + +function exitGame() { + worker.postMessage({ what: 'end-game' }); + game.end(); + document.exitPointerLock(); + + // Exiting from a test map into the editor + if (game_state != GAME.EDITOR && mapData) { + // Show loading screen + uimanager.showLoadingScreen(mapData); + keyManager.resetCommand(); + + setTimeout(() => { + game_state = GAME.EDITOR; + game = new Game(); + game.loadMap(mapData, null, null, null, true); + worker.postMessage({ what: 'start-game', editorLoad: true, map: mapData, players: null, network_game: network_game, game_state: game_state, networkPlayerName: networkPlayerName }); + }, 50); + } + + // Exiting from a regular game to the lobby + else if (game_state == GAME.PLAYING || game_state == GAME.SKIRMISH) { + keyManager.resetCommand(); + + setChatFocus(true); + StatsWindow.showGameStatistics(game); + network.send('leave-game'); + + game_state = GAME.LOBBY; + network_game = false; + + setTimeout(() => showAchievement(), 2000); + } +} + +function MapEditorData() { + $('#mapEditorData').remove(); + $('#dataAddListWindow').remove(); + $('#dataNewObjectWindow').remove(); + + this.window = new UIWindow('mapEditorData', function() { + return game_state == GAME.EDITOR; + }, true, 'Data', true); + elements.push(this.window); + this.window.addScrollableSubDiv('mapEditorDataSubDiv'); + + + // make windows draggable (with jquery ui) + $('#mapEditorData').draggable({ + drag: onDrag, + cancel: 'p, input, select, textarea, button, #personalTextDiv, .nodrag', + }); + + + // create riders + var riders = document.createElement('div'); + riders.id = 'dataRiders'; + + riders.appendChild(uimanager.createButton('dataUnitsButton', 'Units', function() { + mapEditorData.switchRider(0); + soundManager.playSound(SOUND.CLICK); + })); + + riders.appendChild(uimanager.createButton('dataBuildingsButton', 'Buildings', function() { + mapEditorData.switchRider(1); + soundManager.playSound(SOUND.CLICK); + })); + + riders.appendChild(uimanager.createButton('dataCommandsButton', 'Abilities', function() { + mapEditorData.switchRider(2); + soundManager.playSound(SOUND.CLICK); + })); + + riders.appendChild(uimanager.createButton('dataUpgradesButton', 'Upgrades', function() { + mapEditorData.switchRider(3); + soundManager.playSound(SOUND.CLICK); + })); + + riders.appendChild(uimanager.createButton('dataModifiersButton', 'Modifiers', function() { + mapEditorData.switchRider(4); + soundManager.playSound(SOUND.CLICK); + })); + + riders.appendChild(uimanager.createButton('dataGraphicsButton', 'Graphics', function() { + mapEditorData.switchRider(5); + soundManager.playSound(SOUND.CLICK); + })); + + $('#mapEditorDataSubDiv').append(riders); + + this.types = [ + game.unitTypes, + game.buildingTypes, + game.commands, + game.upgrades, + game.modifiers, + game.graphics, + ]; + + this.fields = [ + unit_fields, + building_fields, + ability_fields, + upgrade_fields, + modifiers_fields, + imgs_fields, + ]; + + + this.type = null; + this.listIndex = 0; + this.switchRider(0); + + + // create types list + var typesList = document.createElement('div'); + typesList.id = 'typesList'; + $('#mapEditorDataSubDiv').append(typesList); + + // create fields table + var fieldsTable = document.createElement('div'); + fieldsTable.id = 'dataFieldsTable'; + $('#mapEditorDataSubDiv').append(fieldsTable); + + // create new object window + var dataNewObjectWindow = new UIWindow('dataNewObjectWindow', function() { + return true; + }, true, 'New Object', true); + var dataNewObjectWindowSubdiv = dataNewObjectWindow.addScrollableSubDiv('dataNewObjectWindowSubdiv'); + dataNewObjectWindowSubdiv.innerHTML = '${game.commands[index].name} `) + .add(`
`) + .addHook(() => $(`#${ID}`).click(() => deleteMapEditorDataElement(ID))) + .appendInto('.d_data_commands_div'); + + fadeOut($('#dataAddListWindow')); + this.saveUnit(); +}; + +MapEditorData.prototype.switchRider = function(type) { + if (type < 0) { + type = 0; + } + + if (type > this.types.length - 1) { + type = this.types.length - 1; + } + + $('#typesList').html(''); + + this.listIndex = type; + this.type = null; + + var types = _.sortBy(this.types[type], function(type) { + return type.name; + }); + + for (var i = 0; i < types.length; i++) { + if (!types[i].isCommand || _.contains(EDITOR_COMMANDS, types[i].type)) { + var div = document.createElement('div'); + div.textContent = types[i].name; + div.title = 'ID String: ' + types[i].id_string; + div.className = 'dataTypeListDiv'; + div.index_ = types[i].id_string; + + if (!this.type) { + this.type = types[i]; + div.className = 'dataTypeListDivActive'; + } + + div.onclick = function() { + mapEditorData.saveUnit(true); + // reset active class + soundManager.playSound(SOUND.CLICK); + var children = $('#typesList').children(); + for (var k = 0; k < children.length; k++) { + children[k].className = 'dataTypeListDiv'; + } + mapEditorData.type = mapEditorData.types[mapEditorData.listIndex] == game.graphics ? lists.imgs[this.index_] : lists.types[this.index_]; + mapEditorData.refreshFieldsTable(mapEditorData.type); + this.className = 'dataTypeListDivActive'; + }; + $('#typesList').append(div); + } + } + + this.refreshFieldsTable(this.type); +}; + +function deleteMapEditorDataElement(id) { + const el = document.getElementById(id); + el.parentNode.parentNode.removeChild(el.parentNode); + mapEditorData.saveUnit(); + soundManager.playSound(SOUND.CLICK); +}; + +MapEditorData.prototype.fieldIsDefault = function(field, type) { + var value = type[field.name]; + var originValue = type.getBasicType ? type.getBasicType()[field.name] : ''; + + if (typeof value === 'undefined') { + value = field.isArray ? field.default2_ : field.default_; + } + + if (typeof originValue === 'undefined') { + originValue = field.isArray ? field.default2_ : field.default_; + } + + if (field.type == 'complex') { + for (var field in value) { + if (Object.prototype.toString.call(value[field]) === '[object Array]' && value[field].length == 0) { + delete value[field]; + } + } + + for (var field in originValue) { + if (Object.prototype.toString.call(originValue[field]) === '[object Array]' && originValue[field].length == 0) { + delete originValue[field]; + } + } + + value = JSON.stringify(value); + originValue = JSON.stringify(originValue); + } else if (field.isArray) { + if (value && value.length > 0 && value[0].id_string) { + value = value.slice(); + for (var i = 0; i < value.length; i++) { + value[i] = value[i].id_string; + } + } + + return arraysAreSame(value, originValue); + } else if (field.type == 'commands') { + var arr1 = []; + var arr2 = []; + + _.each(value, function(val, key) { + arr1.push(key); + }); + + _.each(originValue, function(val, key) { + arr2.push(key); + }); + + return arraysAreSame(arr1, arr2); + } else if (field.name == 'img' && isObject(value)) { + originValue = lists.imgs[originValue]; + } else if (field.name == 'unitImg' && isObject(value)) { + value = value.id_string; + } else if ((field.name == 'getTitleImage' || field.name == 'image') && value && {}.toString.call(value) === '[object Function]') { + value = value(); + originValue = lists.imgs[originValue] ? lists.imgs[originValue]() : originValue; + } else if (isObject(value) && typeof originValue == 'string') { + value = value.id_string; + } else if (field.type == 'float') { + value = Math.floor(value * 1000000) / 1000000; + originValue = Math.floor(originValue * 1000000) / 1000000; + } + + return value == originValue; +}; + +// Shows an animated preview image for the provided Graphic type in the div given by divID +// Action defines which animation is shown (idle, walk, die, etc) and defaults to "img" (which not all graphics have) +MapEditorData.prototype.showPreviewImg = function(type, divID, action = 'img', angle = 0) { + // Quit if the requested animation does not exist + if (!type[action]) { + return; + } + + const frameWidth = type[action].frameWidth; + const imgWidth = type[action].w; + const imgHeight = type[action].h; + const frameHeight = type._angles ? imgHeight / type._angles : imgHeight; + const imgOffsetX = type[action].x; + const imgOffsetY = type[action].y; + + const previewObj = getPreviewImgObj(type.id_string); + $(`#${divID}`).attr('style', previewObj.css); + + this.previewImages.push({ + element: `#${divID}`, + x: imgOffsetX * previewObj.scale, + y: (imgOffsetY + (angle * frameHeight)) * previewObj.scale, + step: frameWidth * previewObj.scale, + frames: (imgWidth / frameWidth) || 1, + currentStep: 0, + currentX: 0, + }); +}; + +function getPreviewImgObj(img_key) { + var img = lists.imgs[img_key] ? lists.imgs[img_key].getTitleImage(1) : null; + var file = null; + + if (!img) { + return { css: '', scale: 0 }; + } + + if (img.file.toDataURL) { + file = img.file.toDataURL(); + } else if (img.file.src) { + file = img.file.src; + } else { + return { css: '', scale: 0 }; + } + + var w = img.w || 60; + var h = img.h || 60; + var w2 = img.file.width * (w / img.w); + var h2 = img.file.height * (h / img.h); + + if (w > h) { + h = 60 * (h / w); + w = 60; + } else { + w = 60 * (w / h); + h = 60; + } + + var scale = w / (img.w || 60); + var x = img.x * scale; + var y = img.y * scale; + + return { + css: 'background: url(' + file + '); background-position: ' + -x + 'px ' + -y + 'px; background-size: ' + (img.file.width * scale) + 'px ' + (img.file.height * scale) + 'px; width: ' + w + 'px; height: ' + h + 'px; display: inline-block', + scale, + }; +} + +/** For a given type, create an element in main_data_table for each field it has. */ +MapEditorData.prototype.refreshFieldsTable = function(type) { + // The properties this type(Building|Command|Unit|etc) has (Name|isBuilding|cost|etc) + var fields = this.fields[this.listIndex]; + + const builder = new HTMLBuilder(); + builder.add(''); + builder.add((startGroup ? startGroup : field.name) + (field.isArray ? ' (list)' : '') + ' | ');
+
+ for (var j = 0; j < vals.length; j++) {
+ builder.add('');
+
+ while (startI == i || (startGroup && fields[i] && fields[i].group == startGroup)) {
+ var vals2 = fields[i].isArray ? (Object.prototype.toString.call(type[fields[i].name]) === '[object Array]' ? type[fields[i].name] : []) : [type[fields[i].name]];
+ builder
+ .add((fields[i].subName ? ('' + fields[i].subName + ': ') : '') + '')
+ .add(this.getFieldHTMLCode(fields[i], vals2[j], j, type))
+ .add(' ');
+ i++;
+ }
+
+ // Allow user to remove the field
+ if (field.isArray) {
+ const ID = uniqueID();
+ builder.add(` `); + builder.addHook(() => $(`#${ID}`).click(() => deleteMapEditorDataElement(ID))); + } + + i = startI; + } + + while (startGroup && fields[i + 1] && fields[i + 1].group == startGroup) { + i++; + } + + if (field.isArray) { + const ID = uniqueID(); + builder.add(``); + builder.addHook(() => $(`#${ID}`).click(() => mapEditorData.addField_(startI, i))); + } + + builder.add(' |
${val.name}
`) + .addHook(() => $(`#${ID}`).click(() => deleteMapEditorDataElement(ID))); + }); + + const addBtnID = uniqueID(); + builder + .add(`» Player Settings
'); + this.__playerSettingsSpec.forEach((c) => builder.add(` | ${c.name} | `)); + builder.add('') + // Set the value to be the value in this.__playerSettings and save and refresh the UI when it's changed + .addHook(() => $(`#${ID}`).val(this.__playerSettings[playerIndex][column.key])) + .addHook(() => $(`#${ID}`).change(() => { + const value = $(`#${ID}`).val(); + if (!column.validate || column.validate(value, playerIndex)) { + this.__playerSettings[playerIndex][column.key] = value; + } + + this.__updatedPlayerSettings(); + })); + }; + + for (let i = 0; i < MAX_PLAYERS; i++) { + builder.add(` |
Player ${i + 1} | `); + this.__playerSettingsSpec.forEach((c) => addTD(c, i)); + builder.add('
No Messages
'); + }; + + Chats_.prototype.__initNetworkListeners = function() { + network.registerListener(null, 'query', (splitMsg) => { + const rawMsg = splitMsg[7]; + const sender = { + name: splitMsg[3], + emojiPermissions: splitMsg[1], + authLevel: splitMsg[5], + isPremium: splitMsg[6], + }; + const conversationPartner = splitMsg[2]; + const msgStatus = splitMsg[4]; + + if (AccountInfo.ignores.contains(conversationPartner.toLowerCase()) && conversationPartner == sender.name) { + if (msgStatus == 0) { + network.send('query-ignore<<$' + splitMsg[2]); + } + return; + } + + if (interface_.noGuestDM.get() && conversationPartner == sender.name && sender.authLevel == 1) { + if (msgStatus == 0) { + network.send('query-ignore-guests<<$' + splitMsg[2]); + } + return; + } + + this.addMessage(conversationPartner, rawMsg, sender, msgStatus); + }); + }; + + Chats_.prototype.__chatsWindowOpen = function() { + return $('#queriesWindow')[0].style.display != 'none'; + }; + + Chats_.prototype.__getChatWindow = function(name) { + // Check if the chat window already exists and return if so + if (this.__chatWindows[name]) { + return this.__chatWindows[name]; + } + + // Otherwise, create a new chat window + const chatWindow = new ChatWindow(name); + this.__chatWindows[name] = chatWindow; + + // Create a chat button for this window + const chatButtonID = uniqueID('chatButton'); + generateButton(chatButtonID, 'queryButtonLink', null, addClickSound(() => this.openChatFor(name)), name) + .appendInto('#chatListSubDiv'); + this.__chatButtons[name] = $(`#${chatButtonID}`); + + // Remove the message saying there are no chats + $('#noMessagesP').css('display', 'none'); + + return chatWindow; + }; + + Chats_.prototype.openChatFor = function(name, focus) { + this.__getChatWindow(name).show(); + if (focus) { + this.focusChat(name); + } + + // Remove blinking because the chat has now been opened + this.__chatButtons[name].removeClass('blinkBorderRed'); + + // Check if there are any remaining pending chats + const chatsPending = Object.values(this.__chatWindows).some((chatWindow) => chatWindow.pending); + + // If there are no pending chats, remove the blinking from the friends button + if (!chatsPending) { + $('#friendsButton').removeClass('blinkBGRed'); + } + }; + + Chats_.prototype.addMessage = function(name, rawMsg, sender, msgStatus = '0') { + const chatWindow = this.__getChatWindow(name); + chatWindow.addMessage(rawMsg, sender, msgStatus); + + const neverOpen = this.popupChats.get() == this.POPUP_CHATS.NEVER; + const dontOpenBecauseIngame = this.popupChats.get() == this.POPUP_CHATS.NOT_INGAME && game_state == GAME.PLAYING; + + const shouldOpenChat = !neverOpen && !dontOpenBecauseIngame; + + if (shouldOpenChat) { + // Play a different sound based on whether or not the chat window was already open + soundManager.playSound(chatWindow.hidden ? SOUND.BING : SOUND.ZIP3, 0.8); + this.openChatFor(name); + } else if (chatWindow.hidden) { + // If the chats window is not visible, blink the icon + if (!this.__chatsWindowOpen()) { + $('#friendsButton').addClass('blinkBGRed'); + } + + // Make the chat button blink as well + this.__chatButtons[name].addClass('blinkBorderRed'); + } + }; + + Chats_.prototype.focusChat = function(name) { + for (let i in this.__chatWindows) { + if (i == name) { + this.__chatWindows[i].sendToFront(); + this.__chatWindows[i].focusInput(); + } else { + this.__chatWindows[i].sendToBack(); + } + } + }; + + return new Chats_(); +})(); + +function ChatWindow(name) { + this.name = name; + this.minimized = false; + this.hidden = true; // The chat window will not be visible when created + this.pending = false; // Whether or not there are messages that haven't been seen yet + + // Construct the chat window itself + const windowID = uniqueID('chat'); + const windowLeft = Math.floor(Math.random() * 100 + 100) + 'px'; + const windowTop = Math.floor(Math.random() * 100 + 100) + 'px'; + new HTMLBuilder() + .add(``) + .appendInto(document.body); + + this.window = $(`#${windowID}`); + this.window.css('display', 'none'); + + // Make the chat window draggable + this.window.draggable({ + drag: onDrag, + cancel: 'p, input', + }); + + // Make the chat window close when the ESC button is pressed + this.window.keydown((e) => { + if (keyManager.getKeyCode(e) == KEY.ESC) { + soundManager.playSound(SOUND.CLICK); + this.hide(); + } + }); + + // Construct the contents of the chat window + const textAreaID = uniqueID('chatText'); + const closeButtonID = uniqueID('closeButton'); + const minimizeButtonID = uniqueID('minimizeButton'); + const chatInputID = uniqueID('chatInput'); + const banButtonID = uniqueID('ban'); + + new HTMLBuilder() + .add(`'); + + // Print different messages depending on the msgStatus + if (msgStatus == '2') { + builder.add('This player does not exist.'); + } else if (msgStatus == '9') { + builder.add('This player ignores you.'); + } else if (msgStatus == '8') { + builder.add('This player does not accept messages from guests.'); + } else if (msgStatus == '3') { + builder.add('You\'re sending messages too quickly.'); + } else if (msgStatus == '4') { + // Retry until the game is loaded into GamesList + await retry(() => { + if (sender.name == AccountInfo.playerName) { + builder.add(`You sent an invitation to join ${GamesList.games[rawMsg].map}`); + } else { + const ID = uniqueID(); + const [gameID, inviteID] = rawMsg.split(','); + builder + .add(` + ${sender.name} is inviting you to join + + ${GamesList.games[gameID].map} + + + `) + .addHook(() => $(`#${ID}`).click(() => GamesList.joinGame(gameID, inviteID))); + } + }, 500, 4000); + } else { + // The message was sent successfully + const isOffline = msgStatus == '1'; + + // Print the name of the person who sent the message + if (sender.name.length > 0) { + builder.add(PlayersList.getPlayerLink(escapeHtml(sender.name), true, false, sender.authLevel)); + } + + // Print the message itself, after cleaning it up and inserting emojis + const msg = rawMsg ? kappa(escapeHtml(rawMsg), sender.emojiPermissions) : ''; + builder.add(': ').add(msg); + + // If the user sent a message to an offline user, tell them the message will be delivered + if (isOffline) { + const offlineMsg = ' (This player is offline. The message will be delivered when they come online.)'; + builder.add(`${offlineMsg}`); + } + } + + builder.add('
'); + builder.appendInto(this.textArea); + this.textArea.scrollTop(this.textArea.prop('scrollHeight')); +}; + +ChatWindow.prototype.sendToBack = function() { + this.window.css('z-index', 101); +}; + +ChatWindow.prototype.sendToFront = function() { + this.window.css('z-index', topZIndex++); +}; + +ChatWindow.prototype.focusInput = function() { + setTimeout(() => this.chatInput.focus(), 50); +}; + +ChatWindow.prototype.show = function() { + this.pending = false; + this.hidden = false; + fadeIn(this.window); +}; + +ChatWindow.prototype.hide = function() { + this.hidden = true; + fadeOut(this.window); +}; + +// Destroys the window, making this class unusable afterwards +ChatWindow.prototype.kill = function() { + this.window.remove(); +}; + +function Effect() { + this.isEffect = true; +}; + +Effect.prototype.getYDrawingOffset = function() { + return this.pos.py; +}; + +// return true if building / tile is inside a drawn box on the screen +Effect.prototype.isInBox = function(x1, y1, x2, y2) { + return this.borderRight >= x1 && this.borderBottom >= y1 && this.borderLeft <= x2 && this.borderTop <= y2 + (this.height ? this.height : 0); +}; + +Effect.prototype.isInBoxVisible = Effect.prototype.isInBox; + +Effect.prototype.isExpired = function() { + return ticksCounter > this.tickOfDeath; +}; + +Effect.prototype.attach = function(attachToUnit) { + if (attachToUnit && this.unit) { + this.unit.effectsToDraw.push(this); + } + + game.addToObjectsToDraw(this); +}; + + +Arrow.prototype = new Effect(); +function Arrow(data) { + if (!data.to) { + data.to = data.from; + } + + this.from = data.from.drawPos.getCopy(); + var targetPos = data.to.type ? data.to.drawPos.add3(0, -data.to.type.height) : data.to; + this.pos = this.from; + this.to = data.to; + this.dist = this.from.distanceTo2(targetPos); + this.len = (data.from && data.from.type) ? data.from.type.projectileLen : 0.2; + this.tickStart = ticksCounter; + this.lifeTime = this.dist / data.speed * 20; + this.height = (data.from && data.from.type) ? data.from.type.projectileStartHeight : 0.25; + this.tickOfDeath = ticksCounter + this.lifeTime; + + this.borderLeft = Math.min(this.from.px, targetPos.px); + this.borderRight = Math.max(this.from.px, targetPos.px); + this.borderTop = Math.min(this.from.py, targetPos.py); + this.borderBottom = Math.max(this.from.py, targetPos.py); + + this.attach(data.attachToUnit); + + this.modId = data.modId; +}; + +Arrow.prototype.draw = function() { + var targetPos = this.to.type ? this.to.drawPos.add3(0, -this.to.type.height) : this.to; + + this.dist = this.from.distanceTo2(targetPos); + + var percentageDone = (ticksCounter + percentageOfCurrentTickPassed - this.tickStart) / (this.lifeTime + 0.75); + + var maxHeight = Math.max(this.dist - 1.5, 0) / 6; + var stg = Math.max(Math.min((-(percentageDone * this.dist) + this.dist / 2) * maxHeight * 0.05, this.len / 2), -this.len / 2); + height = (1 - percentageDone) * this.height - Math.pow((2 / this.dist) * (percentageDone * this.dist) - 1, 2) * maxHeight + maxHeight; + + var len = Math.sqrt(this.len * this.len - stg * stg); + + var pos1 = this.from.addNormalizedVector(targetPos, percentageDone * this.dist - len / 2); + var pos2 = this.from.addNormalizedVector(targetPos, percentageDone * this.dist + len / 2); + + this.pos = pos1; + + if (PLAYING_PLAYER.team.canSeeField(this.pos.x, this.pos.y)) { + c.lineWidth = 0.75 * SCALE_FACTOR; + c.strokeStyle = (game.theme && game.theme.arrowColor) ? game.theme.arrowColor : '#111111'; + c.beginPath(); + c.moveTo(pos1.px * FIELD_SIZE - game.cameraX, (pos1.py - height) * FIELD_SIZE - game.cameraY); + c.lineTo(pos2.px * FIELD_SIZE - game.cameraX, (pos2.py - height - stg) * FIELD_SIZE - game.cameraY); + c.stroke(); + c.closePath(); + } +}; + + +CustomProjectile.prototype = new Effect(); +function CustomProjectile(data) { + var targetPos = data.to.type ? data.to.drawPos.add3(0, -data.to.type.height) : data.to.add3(0, -game.getHMValue2(data.to.x, data.to.y) * CLIFF_HEIGHT); + + this.from = data.from.drawPos.getCopy(); + this.pos = this.from; + this.to = data.to; + this.dist = this.from.distanceTo2(targetPos); + this.tickStart = ticksCounter; + this.lifeTime = this.dist / data.speed * 20; + this.height = data.from.type.projectileStartHeight; + this.tickOfDeath = ticksCounter + this.lifeTime; + this.playerNumber = (data.from.owner && data.from.owner.number) ? data.from.owner.number : 0; + this.scale = data.scale ? data.scale : 1; + + this.borderLeft = Math.min(this.from.px, targetPos.px); + this.borderRight = Math.max(this.from.px, targetPos.px); + this.borderTop = Math.min(this.from.py, targetPos.py); + this.borderBottom = Math.max(this.from.py, targetPos.py); + + this.addExplosion = data.addExplosion; + + var angle = this.from.getAngleTo(targetPos); + this.direction = ''; + + angle += angle < -Math.PI ? Math.PI * 2 : 0; + angle -= angle > Math.PI ? Math.PI * 2 : 0; + + if (angle >= Math.PI * 3 / 8 && angle <= Math.PI * 5 / 8) { + this.direction = 's'; + } else if (angle <= -Math.PI * 3 / 8 && angle >= -Math.PI * 5 / 8) { + this.direction = 'n'; + } else if (angle >= Math.PI * 7 / 8 || angle <= -Math.PI * 7 / 8) { + this.direction = 'w'; + } else if ((angle <= Math.PI * 1 / 8 && angle >= 0) || (angle >= -Math.PI * 1 / 8 && angle <= 0)) { + this.direction = 'e'; + } else if (angle <= Math.PI * 7 / 8 && angle >= Math.PI * 5 / 8) { + this.direction = 'sw'; + } else if (angle <= Math.PI * 3 / 8 && angle >= Math.PI * 1 / 8) { + this.direction = 'se'; + } else if (angle >= -Math.PI * 7 / 8 && angle <= -Math.PI * 5 / 8) { + this.direction = 'nw'; + } else { + this.direction = 'ne'; + } + + this.attach(data.attachToUnit); + + this.modId = data.modId; + + this.imgs = [imgs.fire1.img, imgs.fire2.img, imgs.fire3.img, imgs.fire4.img, imgs.fire5.img]; +}; + +CustomProjectile.prototype.draw = function() { + var targetPos = this.to.type ? this.to.drawPos.add3(0, -this.to.type.height) : this.to.add3(0, -game.getHMValue2(this.to.x, this.to.y) * CLIFF_HEIGHT); + + this.dist = this.from.distanceTo2(targetPos); + + var percentageDone = (ticksCounter + percentageOfCurrentTickPassed - this.tickStart) / (this.lifeTime + 0.75); + + var maxHeight = Math.max(this.dist - 1.5, 0) / 6; + height = ((1 - percentageDone) * this.height - Math.pow((2 / this.dist) * (percentageDone * this.dist) - 1, 2) * maxHeight + maxHeight) * 0.6; + + this.pos = this.from.addNormalizedVector(targetPos, percentageDone * this.dist); + var img = arrowImg[this.direction][percentageDone > 0.5 ? 1 : 0]; + + var scale = SCALE_FACTOR * this.scale; + + if (PLAYING_PLAYER.team.canSeeField(this.pos.x, this.pos.y)) { + c.drawImage(buildingSheet[this.playerNumber], img.x, img.y, img.w, img.h, this.pos.px * FIELD_SIZE - game.cameraX - img.w * scale / 2, (this.pos.py - height) * FIELD_SIZE - game.cameraY - img.h * scale / 2, img.w * scale, img.h * scale); + } + + if (!this.addExplosion || !this.isExpired()) { + return; + } + + soundManager.playSound(SOUND.FLAK, game.getVolumeModifier(targetPos)); + + // crate explosion + for (var i = 0; i < 10; i++) { + new Sprite({ + from: targetPos.add2(Math.random() * Math.PI * 2, Math.random() * 1.5).add3(0, 6.5), + img: this.imgs[Math.floor(Math.random() * this.imgs.length)], + scaleFunction: function(age) { + return (1.5 / (age + 0.25) + age - 4) * (-1) + this.r1; + }, + age: 0.6 + Math.random() * 0.5, + r1: 1.0 + Math.random() / 2, + height: 6.5, + }); + } + + // crate zmoke + for (var i = 0; i < 6; i++) { + new Sprite({ + from: targetPos.add2(Math.random() * Math.PI * 2, Math.random() * 1.5).add3(0, 6.5), + img: lists.imgs.dust1, + scaleFunction: function(age) { + return (1.5 / (age + 0.25) + age - 4) * (-1) + this.r1; + }, + age: 1.6 + Math.random() * 0.5, + r1: 1.0 + Math.random() / 2, + alphaFunction: function(age) { + return 0.4; + }, + height: 6.5, + }); + } +}; + + +DragonAttack.prototype = new Effect(); +function DragonAttack(data) { + var targetPos = data.to.type ? data.to.drawPos.getCopy() : data.to; + + this.from = data.from.drawPos.addNormalizedVector(targetPos, data.from.type.size / 3); + this.pos = this.from; + this.to = data.to; + this.dist = this.from.distanceTo2(targetPos); + this.tickStart = ticksCounter; + this.lifeTime = this.dist / (data.speed ? data.speed : 6) * 20; + this.actualLifeTime = this.lifeTime; + this.height = data.from.type ? data.from.type.height : 0; + this.targetHeight = (this.to.type && this.to.type.isUnit) ? this.to.type.height : (game.getHMValue2(this.to.x, this.to.y) * CLIFF_HEIGHT + 0.3); + this.scale = data.scale ? data.scale : 1; + + // if were shooting a building, make the "hit" a little bit earlier (when the projectile hit the wall of the building) + if (this.to.type && this.to.type.isBuilding) { + var angle = this.from.getAngleTo(this.to.pos); + var size = this.to.type.size / 2; + + var distMinimization = Math.min( + Math.sqrt( + Math.pow(Math.tan(angle) * size, 2) + Math.pow(size, 2), + ), + Math.sqrt( + Math.pow((1 / Math.tan(angle)) * size, 2) + Math.pow(size, 2), + ), + ); + + this.actualLifeTime = (this.dist - distMinimization - 0.4) / (data.speed ? data.speed : 6) * 20; + } + + this.tickOfDeath = ticksCounter + this.actualLifeTime; + + this.borderLeft = Math.min(this.from.px, targetPos.px); + this.borderRight = Math.max(this.from.px, targetPos.px); + this.borderTop = Math.min(this.from.py, targetPos.py); + this.borderBottom = Math.max(this.from.py, targetPos.py); + + this.attach(data.attachToUnit); + + this.modId = data.modId; + + this.imgs = [imgs.fire1.img, imgs.fire2.img, imgs.fire3.img, imgs.fire4.img]; +}; + +DragonAttack.prototype.draw = function() { + var targetPos = this.to.type ? this.to.drawPos.getCopy() : this.to; + + this.dist = this.from.distanceTo2(targetPos); + + var percentageDone = (ticksCounter + percentageOfCurrentTickPassed - this.tickStart) / (this.lifeTime + 1); + var height = this.height + (this.targetHeight - this.height) * percentageDone; + this.pos = this.from.addNormalizedVector(targetPos, percentageDone * this.from.distanceTo2(targetPos)); + + if (PLAYING_PLAYER.team.canSeeField(this.pos.x, this.pos.y)) { + var scale = SCALE_FACTOR * this.scale; + var drawX = this.pos.px * FIELD_SIZE - (imgs.fire5.img.w / 2) * scale - game.cameraX; + var drawY = (this.pos.py - height) * FIELD_SIZE - (imgs.fire5.img.h / 2) * scale - game.cameraY; + c.drawImage(miscSheet[0], imgs.fire5.img.x, imgs.fire5.img.y, imgs.fire5.img.w, imgs.fire5.img.h, drawX, drawY, imgs.fire5.img.w * scale, imgs.fire5.img.h * scale); + } + + // randomly create flames + for (var i = 0; i < tickDiff; i++) { + new Sprite({ + from: this.pos.add2(Math.random() * Math.PI * 2, 0.15), + img: this.imgs[Math.floor(Math.random() * this.imgs.length)], + scaleFunction: function(age) { + return (age + 1) * this.scale_; + }, + age: Math.random() + 0.5, + height: height, + scale_: this.scale, + }); + } + + // randomly create smoke / dust + for (var i = 0; i < tickDiff; i++) { + if (Math.random() > 0.5) { + new Dust({ from: this.pos.add2(Math.random() * Math.PI * 2, 0.15), scale: 1 + Math.random(), ageScale: 2 + Math.random(), height: height }); + } + } + + if (!this.isExpired()) { + return; + } + + var pos = (this.to.type && this.to.type.isBuilding) ? this.pos.add3(0, 0.5) : targetPos.add3(0, 0.5); + + if (this.to.type && this.to.type.flying) { + pos = pos.add3(0, 3); + height += 3; + } + + // create impact flames + for (var i = 0; i < 4; i++) { + new Sprite({ + from: pos.add2(Math.random() * Math.PI * 2, Math.random() * 0.75), + img: this.imgs[Math.floor(Math.random() * this.imgs.length)], + scaleFunction: function(age) { + return (1 / (age + 0.25) + age - 4) * (-1) * this.scale_; + }, + age: 1 + Math.random() * 2, + height: height, + scale_: this.scale, + }); + } + + // create impact smoke + for (var i = 0; i < 2; i++) { + new Dust({ from: pos.add2(Math.random() * Math.PI * 2, Math.random() * 1), scale: 1.5 + Math.random(), ageScale: 2 + Math.random(), height: height }); + } + + // create soot + if (this.to.type && this.to.type.isUnit && !this.to.type.flying) { + pos = pos.add3(0, -0.5).add2(Math.random() * Math.PI * 2, Math.random() * 0.25); + + game.groundTilesCanvas.getContext('2d').globalAlpha = 0.35; + game.groundTilesCanvas.getContext('2d').drawImage(miscSheet[0], imgs.soot.img.x, imgs.soot.img.y, imgs.soot.img.w, imgs.soot.img.h, pos.px * FIELD_SIZE / SCALE_FACTOR - imgs.soot.img.w / 4, (pos.py + 2) * FIELD_SIZE / SCALE_FACTOR - imgs.soot.img.h / 4, imgs.soot.img.w / 2, imgs.soot.img.h / 2); + game.groundTilesCanvas.getContext('2d').globalAlpha = 1; + } +}; + + +Dust.prototype = new Effect(); +function Dust(data) { + _.extend(this, data); + + this.height = this.height ? this.height : 0; + this.scale = this.scale ? this.scale : 1; + + this.pos = this.from; + + this.x = this.pos.px; + this.y = this.pos.py - this.height; + + this.particle = { + x: Math.random() / 3 - 0.166, + y: Math.random() / 20 - 1 / 40, + vz: this.vz ? this.vz : Math.random() / 3 + 0.166, + alpha: 0.75, + img: Math.random() > 0.5 ? imgs.dust1.img : imgs.dust2.img, + scale: Math.random() * 4 + 2, + tickOfCreation: ticksCounter, + timeToLive: (Math.random() * 650 + 800) * (this.ageScale ? this.ageScale : 1), + }; + + this.tickOfDeath = ticksCounter + this.particle.timeToLive / 50; + + this.borderLeft = this.pos.px; + this.borderRight = this.pos.px; + this.borderTop = this.pos.py; + this.borderBottom = this.pos.py; + + this.attach(data.attachToUnit); + + this.modId = data.modId; +}; + +Dust.prototype.draw = function() { + var age = ((ticksCounter + percentageOfCurrentTickPassed) - this.particle.tickOfCreation) * 50; + var percentageDone = age / this.particle.timeToLive; + + var y = this.particle.vz * (age / 1000) * this.scale; + var scale = this.particle.scale * (percentageDone * 0.5 + 0.5) * this.scale * (SCALE_FACTOR / 4); + + if (this.yFunction) { + y += this.yFunction(age / 1000) * 0.75; + } + + var x = 0; + if (this.xFunction) { + x = this.xFunction(age / 1000); + } + + if (PLAYING_PLAYER.team.canSeeField(this.pos.x, this.pos.y)) { + c.globalAlpha = Math.max(this.particle.alpha * (1 - percentageDone), 0); + c.drawImage(miscSheet[0], this.particle.img.x, this.particle.img.y, this.particle.img.w, this.particle.img.h, (this.x + this.particle.x + x) * FIELD_SIZE - game.cameraX - this.particle.img.w * scale / 2, (this.y + this.particle.y - y) * FIELD_SIZE - game.cameraY - this.particle.img.w * scale / 2, this.particle.img.w * scale, this.particle.img.h * scale); + c.globalAlpha = 1; + } +}; + + +Flame.prototype = new Effect(); +function Flame(data) { + this.unit = (data.from && data.from.type) ? data.from : null; + + this.pos = this.unit ? this.unit.drawPos : data.from; + this.tickOfDeath = ticksCounter + (Math.random() * 3 + 0.5) * 20; + + this.borderLeft = this.pos.px - 0.5; + this.borderRight = this.pos.px + 0.5; + this.borderTop = this.pos.py + 1.5; + this.borderBottom = this.pos.py - 0.5; + + this.attach(data.attachToUnit); + + this.modId = data.modId; + + this.imgs = [imgs.fire1.img, imgs.fire2.img, imgs.fire3.img, imgs.fire4.img]; +}; + +Flame.prototype.draw = function() { + if (this.unit) { + this.pos = this.unit.drawPos; + } + + for (var i = 0; i < tickDiff; i++) { + if (Math.random() < 0.06) { + new Sprite({ + from: this.pos.add2(Math.random() * Math.PI * 2, 0.1), + img: this.imgs[Math.floor(Math.random() * this.imgs.length)], + scaleFunction: function(age) { + return 1 + age; + }, + alphaFunction: function(age) { + return 0.5; + }, + }); + } + } + + for (var i = 0; i < tickDiff; i++) { + if (Math.random() < 0.1) { + new Dust({ from: this.pos, scale: 1 + Math.random(), ageScale: 2 + Math.random() }); + } + } +}; + + +Flamestrike.prototype = new Effect(); +function Flamestrike(data) { + var targetPos = data.to.type ? data.to.pos.getCopy() : data.to; + + this.from = data.from.pos.getCopy(); + this.pos = this.from; + this.to = data.to; + this.dist = this.from.distanceTo2(targetPos); + this.tickStart = ticksCounter; + this.lifeTime = this.dist / (data.speed ? data.speed : 7) * 20; + this.tickOfDeath = ticksCounter + this.lifeTime; + this.noFinalBlow = data.noFinalBlow; + this.scale = data.scale ? data.scale : 1; + this.lastTimeSoot = ticksCounter; + this.startHeight = data.from.type.height; + this.endHeight = data.to.type ? data.to.type.height : 0.3; + + this.borderLeft = Math.min(this.from.px, targetPos.px); + this.borderRight = Math.max(this.from.px, targetPos.px); + this.borderTop = Math.min(this.from.py, targetPos.py); + this.borderBottom = Math.max(this.from.py, targetPos.py); + + this.attach(data.attachToUnit); + + this.modId = data.modId; + + this.imgs = [imgs.fire1.img, imgs.fire2.img, imgs.fire3.img, imgs.fire4.img, imgs.fire5.img]; +}; + +Flamestrike.prototype.draw = function() { + var targetPos = this.to.type ? this.to.pos.getCopy() : this.to; + this.dist = this.from.distanceTo2(targetPos); + + this.vec = this.from.vectorTo(targetPos).normalize(0.7); + + var percentageDone = (ticksCounter + percentageOfCurrentTickPassed - this.tickStart) / (this.lifeTime + 1); + this.pos = this.from.addNormalizedVector(targetPos, percentageDone * this.dist); + + var height = this.startHeight * (1 - percentageDone) + this.endHeight * percentageDone; + + var hm = game.getHMValue3(this.pos) * CLIFF_HEIGHT; + + var drawX = (this.pos.px + Math.random() * 0.1 - 0.05) * FIELD_SIZE - (imgs.fire5.img.w / 2) * SCALE_FACTOR * this.scale * (Math.random() * 0.2 + 0.9) - game.cameraX; + var drawY = ((this.pos.py + Math.random() * 0.1 - 0.05 - height - hm) + Math.sin((ticksCounter + percentageOfCurrentTickPassed) * 0.4) * this.scale * 0.04) * FIELD_SIZE - (imgs.fire5.img.h / 2) * SCALE_FACTOR * this.scale * (Math.random() * 0.2 + 0.9) - game.cameraY; + + if (this.noFinalBlow && this.tickOfDeath - 10 < ticksCounter) { + c.globalAlpha = Math.max((this.tickOfDeath - ticksCounter) / 10, 0); + } + + if (PLAYING_PLAYER.team.canSeeField(this.pos.x, this.pos.y)) { + if (this.noFinalBlow) { + var oldGlobalAlpha = c.globalAlpha; + c.globalAlpha *= 0.15; + var scale = SCALE_FACTOR * this.scale * 1.5 * (Math.random() * 0.2 + 0.9); + var drawX2 = (this.pos.px + Math.random() * 0.1 - 0.05) * FIELD_SIZE - imgs.lightGround.img.w / 2 * scale - game.cameraX; + var drawY2 = (this.pos.py + 0.5 + Math.random() * 0.1 - 0.05 - height - hm) * FIELD_SIZE - imgs.lightGround.img.h / 2 * scale - game.cameraY; + c.drawImage(miscSheet[0], imgs.lightGround.img.x, imgs.lightGround.img.y, imgs.lightGround.img.w, imgs.lightGround.img.h, drawX2, drawY2, imgs.lightGround.img.w * scale, imgs.lightGround.img.h * scale); + c.globalAlpha = oldGlobalAlpha; + } + + c.drawImage(miscSheet[0], imgs.fire5.img.x, imgs.fire5.img.y, imgs.fire5.img.w, imgs.fire5.img.h, drawX, drawY, imgs.fire5.img.w * SCALE_FACTOR * this.scale, imgs.fire5.img.h * SCALE_FACTOR * this.scale); + + // soot + if (this.noFinalBlow && this.lastTimeSoot < ticksCounter && height < 1) { + var ctx = game.groundTilesCanvas.getContext('2d'); + ctx.globalAlpha = 0.05; + ctx.drawImage(miscSheet[0], imgs.soot2.img.x, imgs.soot2.img.y, imgs.soot2.img.w, imgs.soot2.img.h, this.pos.px * FIELD_SIZE / SCALE_FACTOR - imgs.soot2.img.w / 2, (this.pos.py - hm + 2) * FIELD_SIZE / SCALE_FACTOR - imgs.soot2.img.h / 2, imgs.soot2.img.w, imgs.soot2.img.h); + this.lastTimeSoot = ticksCounter; + ctx.globalAlpha = 1; + } + } + + if (this.noFinalBlow && this.tickOfDeath - 10 < ticksCounter) { + c.globalAlpha = 1; + } + + // randomly create flames + for (var i = 0; i < tickDiff; i++) { + new Sprite({ + from: this.pos.add2(Math.random() * Math.PI * 2, 0.15).add3(0, -hm), + img: this.imgs[Math.floor(Math.random() * 4)], + scaleFunction: function(age) { + return (age + 1) * this.var1; + }, + var1: Math.sqrt(this.scale), + rX: Math.random() - 0.5 + this.vec.px, + rY: Math.random() - 0.5 + this.vec.py, + alphaFunction: function(age) { + return 1; + }, + xFunction: function(age) { + return this.rX * Math.sqrt(age); + }, + yFunction: function(age) { + return this.rY * Math.sqrt(age); + }, + height: height, + }); + } + + // randomly create smoke / dust + for (var i = 0; i < tickDiff; i++) { + if (Math.random() > 0.5) { + new Dust({ from: this.pos.add2(Math.random() * Math.PI * 2, 0.15).add3(0, -hm), scale: 1 + Math.random(), ageScale: 2 + Math.random(), height: height }); + } + } + + if (!this.isExpired() || this.noFinalBlow) { + return; + } + + // impact + new Sprite({ + from: this.pos.add2(Math.random() * Math.PI * 2, Math.random() / 2).add3(0, -hm), + img: this.imgs[Math.floor(Math.random() * this.imgs.length)], + scaleFunction: function(age) { + return (1 / (age + 0.25) + age - 4) * (-1) + 2; + }, + age: 2, + height: height, + }); + + // create impact flames + for (var i = 0; i < 13; i++) { + new Sprite({ + from: this.pos.add2(Math.random() * Math.PI * 2, Math.random() * 1.75).add3(0, -hm), + img: this.imgs[Math.floor(Math.random() * this.imgs.length)], + scaleFunction: function(age) { + return (1 / (age + 0.25) + age - 4) * (-1) + this.r1; + }, + age: 1 + Math.random() * 2, + r1: Math.random(), + height: height, + }); + } + + // create impact zmoke + for (var i = 0; i < 7; i++) { + new Dust({ from: this.pos.add2(Math.random() * Math.PI * 2, Math.random() * 2).add3(0, -hm), scale: 1.5 + Math.random(), ageScale: 2 + Math.random(), height: height }); + } + + // create soot + if (height < 1) { + game.groundTilesCanvas.getContext('2d').globalAlpha = 0.5; + game.groundTilesCanvas.getContext('2d').drawImage(miscSheet[0], imgs.soot.img.x, imgs.soot.img.y, imgs.soot.img.w, imgs.soot.img.h, this.pos.px * FIELD_SIZE / SCALE_FACTOR - imgs.soot.img.w / 2, (this.pos.py - hm) * FIELD_SIZE / SCALE_FACTOR - imgs.soot.img.h / 2, imgs.soot.img.w, imgs.soot.img.h); + game.groundTilesCanvas.getContext('2d').globalAlpha = 1; + + // create flames + for (var i = 0; i < 2; i++) { + new Flame({ from: this.pos.add2(Math.random() * Math.PI * 2, Math.random() * 2).add3(0, -hm) }); + } + } +}; + + +GroundOrder.prototype = new Effect(); +function GroundOrder(data) { + this.pos = data.from; + this.time = timestamp; + this.tickOfDeath = ticksCounter + 6; + + this.borderLeft = this.pos.px; + this.borderRight = this.pos.px; + this.borderTop = this.pos.py; + this.borderBottom = this.pos.py; + + this.attach(data.attachToUnit); + + this.modId = data.modId; +}; + +GroundOrder.prototype.draw = function() { + var age = timestamp - this.time; // effect age in sec + var randomYOffsetPixels = (age * 0.05) % 4 - 2; + + c.strokeStyle = 'rgba(' + game.theme.line_red + ', ' + game.theme.line_green + ', ' + game.theme.line_blue + ', ' + (1 - age / 400) + ')'; + c.lineWidth = 2; + + // draw "ring" + c.beginPath(); + c.ellipse(this.pos.px * FIELD_SIZE - game.cameraX, this.pos.py * FIELD_SIZE - game.cameraY + randomYOffsetPixels, Math.abs(age / 10), Math.abs(age / 10) * 0.8, 0, 2 * Math.PI, false); + c.stroke(); + + // draw point + c.beginPath(); + c.ellipse(this.pos.px * FIELD_SIZE - game.cameraX, this.pos.py * FIELD_SIZE - game.cameraY + randomYOffsetPixels, 1, 0.8, 0, 2 * Math.PI, false); + c.stroke(); +}; + + +PlasmaShield.prototype = new Effect(); +function PlasmaShield(data) { + this.pos = data.from.type ? data.from.drawPos : data.from; + this.radius = (data.from.type ? data.from.type.size : 1) * 0.85; + this.to = data.to; + this.time = timestamp; + this.tickOfDeath = ticksCounter + 10; + + this.borderLeft = this.pos.px; + this.borderRight = this.pos.px; + this.borderTop = this.pos.py; + this.borderBottom = this.pos.py; + + this.attach(data.attachToUnit); + + this.modId = data.modId; +}; + +PlasmaShield.prototype.draw = function() { + var age = timestamp - this.time; // effect age in sec + var randomYOffsetPixels = (age * 0.05) % 4 - 2; + var angle = this.pos.getAngleTo(this.to); + + c.strokeStyle = 'rgba(' + game.theme.line_red + ', ' + game.theme.line_green + ', ' + game.theme.line_blue + ', ' + ((1 - age / 400) * 0.6) + ')'; + c.lineWidth = 1.5 * SCALE_FACTOR; + + c.beginPath(); + c.ellipse(this.pos.px * FIELD_SIZE - game.cameraX, (this.pos.py - 0.2) * FIELD_SIZE - game.cameraY + randomYOffsetPixels, this.radius * FIELD_SIZE, this.radius * FIELD_SIZE * 0.8, angle - 0.5, angle + 0.5, false); + c.ellipse(this.pos.px * FIELD_SIZE - game.cameraX, (this.pos.py - 0.3) * FIELD_SIZE - game.cameraY + randomYOffsetPixels, this.radius * FIELD_SIZE, this.radius * FIELD_SIZE * 0.8, angle - 0.5, angle + 0.5, false); + c.ellipse(this.pos.px * FIELD_SIZE - game.cameraX, (this.pos.py - 0.4) * FIELD_SIZE - game.cameraY + randomYOffsetPixels, this.radius * FIELD_SIZE, this.radius * FIELD_SIZE * 0.8, angle - 0.5, angle + 0.5, false); + c.stroke(); +}; + + +FloatingText.prototype = new Effect(); +function FloatingText(data) { + this.unit = data.from.type ? data.from : null; + + this.pos = this.unit ? this.unit.drawPos.add3(0, -u.type.size - u.type.height + 1.5 + (u.type.isBuilding ? 2 : 0)) : data.from; + this.tickOfCreation = ticksCounter; + this.tickOfDeath = ticksCounter + (data.duration ? data.duration : 40); + this.size = (data.scale ? data.scale : 20) / 3; + this.content = data.content; + this.color = data.color ? data.color : 'white'; + + this.borderLeft = this.pos.px - 2; + this.borderRight = this.pos.px + 2; + this.borderTop = this.pos.py + 2; + this.borderBottom = this.pos.py - 1; + + this.attach(data.attachToUnit); + + this.modId = data.modId; +}; + +FloatingText.prototype.draw = function() { + if (this.tickOfDeath <= ticksCounter) { + return false; + } + + var age = ticksCounter + percentageOfCurrentTickPassed - this.tickOfCreation; + + var alpha = (this.tickOfDeath - ticksCounter < 10) ? Math.max(c.globalAlpha = (this.tickOfDeath - (ticksCounter + percentageOfCurrentTickPassed)) / 10, 0) : 1; + + drawText(c, this.content, this.color, 'bold ' + (this.size * SCALE_FACTOR) + 'px LCDSolid', this.pos.px * FIELD_SIZE - game.cameraX, (this.pos.py - 0 - age / 40) * FIELD_SIZE - game.cameraY, 200, 'center', alpha); +}; + + +Heal.prototype = new Effect(); +function Heal(data) { + this.unit = data.from.type ? data.from : null; + + this.pos = this.unit ? this.unit.drawPos.add3(0, 0.3 - this.unit.type.height) : data.from; + this.x = this.pos.px; + this.y = this.pos.py; + this.tickOfCreation = ticksCounter; + this.size = data.scale ? data.scale : 1; + this.duration = data.duration ? data.duration : ((data.originPos.distanceTo2(this.pos) / (data.speed ? data.speed : 7)) * 20); + if (!data.duration && !data.speed) { + this.duration = 0; + } + this.tickOfDeath = ticksCounter + this.duration + 24; + this.borderLeft = this.pos.px; + this.borderRight = this.pos.px; + this.borderTop = this.pos.py; + this.borderBottom = this.pos.py; + + this.attach(data.attachToUnit); + + this.modId = data.modId; + + this.imgs = [imgs.heal2.img, imgs.heal3.img, imgs.heal4.img]; +}; + +Heal.prototype.draw = function() { + if (this.tickOfCreation + this.duration > ticksCounter) { + return; + } + + if (this.unit) { + this.pos = this.unit.drawPos.add3(0, 0.3 - this.unit.type.height); + } + + var age = ((ticksCounter + percentageOfCurrentTickPassed) - (this.tickOfCreation + this.duration)) * 50; + + if (age < 1000) { + for (var i = 0; i < tickDiff; i++) { + new Sprite({ + from: this.pos.add2(Math.random() * Math.PI * 2, Math.random() * 0.4 * this.size).add3(0, 6.6), + img: this.imgs[Math.floor(Math.random() * this.imgs.length)], + scaleFunction: function(age) { + return ((1 / ((age * 5) + 0.25) + (age * 5) - 4) * (-0.7) + 0.6) * this.var1 * 1.15; + }, + age: 0.6 + Math.random() * 0.5, + var1: this.size, + zFunction: function(age) { + return -age * 1 - 0.5; + }, + height: 6.5, + }); + } + } + + // draw circle + c.globalAlpha = Math.max(1 - age / 1200, 0.01); + drawCircle(this.x * FIELD_SIZE - game.cameraX, this.y * FIELD_SIZE - game.cameraY - 0.5, (age / 1400) * FIELD_SIZE * this.size, '#9DE9A4', false, false, 1.5); + + // draw circle 2 + var age2 = Math.max(0.01, age - 400); + c.globalAlpha = Math.max(1 - age2 / 800, 0.01); + drawCircle(this.x * FIELD_SIZE - game.cameraX, this.y * FIELD_SIZE - game.cameraY - 0.5, (age2 / 1200) * FIELD_SIZE * this.size, 'white', false, false, 1.5); + + c.globalAlpha = 1; +}; + + +LaunchedRock.prototype = new Effect(); +function LaunchedRock(data) { + var targetPos = data.to.type ? data.to.drawPos.getCopy() : data.to; + + this.speed = data.speed ? data.speed : 7; + this.from = data.from.drawPos.getCopy().add3(0, 0.3); + this.pos = data.from; + this.to = data.to; + this.dist = this.from.distanceTo2(targetPos); + this.tickStart = ticksCounter; + this.lifeTime = this.dist / this.speed / 50 * 1000; + this.actualLifeTime = (this.dist - 0.05) / this.speed * 20; + this.startHeight = data.from.type.height; + this.endHeight = data.to.type ? data.to.type.height : 0; + this.scale = data.scale ? data.scale : 1; + + // if were shooting a building, make the "hit" a little bit earlier (when the rock hit the wall of the building) + if (data.to.type && data.to.type.isBuilding) { + this.endHeight -= 1; + + var angle = this.from.getAngleTo(targetPos); + var size = data.to.type.size / 2; + + var distMinimization = Math.min( + Math.sqrt( + Math.pow(Math.tan(angle) * size, 2) + Math.pow(size, 2), + ), + Math.sqrt( + Math.pow((1 / Math.tan(angle)) * size, 2) + Math.pow(size, 2), + ), + ); + + this.actualLifeTime = (this.dist - distMinimization) / this.speed * 20; + } + + this.tickOfDeath = ticksCounter + this.actualLifeTime; + + this.borderLeft = Math.min(this.from.px, targetPos.px); + this.borderRight = Math.max(this.from.px, targetPos.px); + this.borderTop = Math.min(this.from.py, targetPos.py); + this.borderBottom = Math.max(this.from.py, targetPos.py); + + this.attach(data.attachToUnit); + + this.modId = data.modId; + + this.imgs = [imgs.flyingRock1.img, imgs.flyingRock2.img, imgs.flyingRock3.img, imgs.flyingRock4.img, imgs.flyingRock5.img, imgs.flyingRock6.img, imgs.flyingRock7.img, imgs.flyingRock8.img]; +}; + +LaunchedRock.prototype.draw = function(x1, x2, y1, y2, volume) { + var targetPos = this.to.type ? this.to.drawPos.getCopy() : this.to; + + this.dist = this.from.distanceTo2(targetPos); + + var percentageDone = (ticksCounter + percentageOfCurrentTickPassed - this.tickStart) / (this.lifeTime + 1); + this.pos = this.from.addNormalizedVector(targetPos, percentageDone * this.dist); + + var height = this.startHeight * (1 - percentageDone) + this.endHeight * percentageDone; + + var img = this.imgs[Math.floor(this.tickStart + ticksCounter / 2) % this.imgs.length]; + + // calculate additional heigh + var y = Math.pow((percentageDone * 16.5 - 6 - this.dist / 10) / (9.6 + Math.pow(0.11 * (this.dist - 9), 2) - (3 * this.dist) / 9), 2) - 0.7 - this.dist / 6; + + var drawPos = this.pos.add3(0, y); + + var scale = this.scale * SCALE_FACTOR; + + var drawX = drawPos.px * FIELD_SIZE - (img.w / 2) * scale - game.cameraX; + var drawY = (drawPos.py - height) * FIELD_SIZE - (img.h / 2) * scale - game.cameraY; + if (PLAYING_PLAYER.team.canSeeField(this.pos.x, this.pos.y)) { + c.drawImage(miscSheet[0], img.x, img.y, img.w, img.h, drawX, drawY, img.w * scale, img.h * scale); + } + + var flyVec = this.from.vectorTo(this.to).normalize(0.5); + + // randomly create particle images + for (var i = 0; i < tickDiff; i++) { + if (Math.random() > 0.5) { + new Sprite({ + from: drawPos.add2(Math.random() * Math.PI * 2, 0.1), + img: imgs.particle.img, + scaleFunction: function(age) { + return ((this.r1 + 1.4) - age) * this.scale_; + }, + age: 1.5 + Math.random() * 0.7, + r1: Math.random(), + rX: flyVec.px, + rY: flyVec.py, + zFunction: function(age) { + return Math.pow(age * 1.15, 2.5) * 0.5; + }, + xFunction: function(age) { + return this.rX * Math.sqrt(age); + }, + yFunction: function(age) { + return this.rY * Math.sqrt(age); + }, + height: height, + scale_: this.scale, + }); + } + } + + if (!this.isExpired()) { + return; + } + + // play sound + if (PLAYING_PLAYER.team.canSeeField(this.pos.x, this.pos.y)) { + soundManager.playSound(SOUND.CATA_IMPACT, game.getVolumeModifier(this.pos)); + } + + // create impact dust + for (var i = 0; i < 5; i++) { + new Dust({ from: this.pos.add2(Math.random() * Math.PI * 2, Math.random() * 0.6), scale: Math.random() * 1.5 + 0.5, height: height - y }); + } + + // create impact sprites / particles + for (var i = 0; i < 12; i++) { + new Sprite({ + from: this.pos.add2(Math.random() * Math.PI * 2, Math.random() * 0.3), + img: imgs.particle.img, + scaleFunction: function(age) { + return 2 * this.scale_; + }, + age: 0.5 + Math.random(), + r1: Math.random() * 0.4, + r2: -y, + rX: Math.random() * 3.5 - 1.75, + rY: Math.random() * 3.5 - 1.75, + zFunction: function(age) { + return Math.min(Math.pow(age * 1.6 - 0.8 + this.r1, 2) - 0.7 - this.r2, 0); + }, + xFunction: function(age) { + return this.rX * Math.sqrt(age); + }, + yFunction: function(age) { + return this.rY * Math.sqrt(age); + }, + height: height, + scale_: this.scale, + }); + } +}; + + +Mageattack.prototype = new Effect(); +function Mageattack(data) { + var targetField = data.to.isField ? data.to : data.to.drawPos; + + this.from = data.from.drawPos.getCopy().add3(0, -0.1 - (data.from.type ? data.from.type.projectileStartHeight : 0)); + this.pos = data.from.drawPos.getCopy(); + this.to = data.to.isField ? data.to.getCopy() : data.to; + this.dist = this.from.distanceTo2(targetField); + this.tickStart = ticksCounter; + this.lifeTime = this.dist / (data.speed ? data.speed : 10) / 50 * 1000; + this.tickOfDeath = ticksCounter + this.lifeTime; + this.startHeight = data.from.type.height; + this.endHeight = this.to.type ? this.to.type.height : (game.getHMValue2(this.to.x, this.to.y) * CLIFF_HEIGHT + 0.3); + this.scale = data.scale ? data.scale : 1; + + this.borderLeft = Math.min(this.from.px, targetField.px); + this.borderRight = Math.max(this.from.px, targetField.px); + this.borderTop = Math.min(this.from.py, targetField.py); + this.borderBottom = Math.max(this.from.py, targetField.py); + + this.attach(data.attachToUnit); + + this.modId = data.modId; + + this.imgs = [imgs.mageAttack1.img, imgs.mageAttack2.img, imgs.mageAttack3.img, imgs.mageAttack4.img]; +}; + +Mageattack.prototype.draw = function() { + var targetField = this.to.isField ? this.to : this.to.drawPos.getCopy(); + + this.dist = this.from.distanceTo2(targetField); + + var percentageDone = (ticksCounter + percentageOfCurrentTickPassed - this.tickStart) / (this.lifeTime + 1); + this.pos = this.from.addNormalizedVector(targetField, percentageDone * this.dist); + + var height = this.startHeight * (1 - percentageDone) + this.endHeight * percentageDone; + + var scale = SCALE_FACTOR + SCALE_FACTOR * (Math.random() * 0.4 + 0.8) * 0.55; + var drawX = (this.pos.px + Math.random() * 0.15 - 0.075) * FIELD_SIZE - (imgs.mageAttack4.img.w / 2) * scale - game.cameraX; + var drawY = (this.pos.py - height + 0.3 + Math.random() * 0.15 - 0.075) * FIELD_SIZE - (imgs.mageAttack4.img.h / 2) * scale - game.cameraY; + if (PLAYING_PLAYER.team.canSeeField(this.pos.x, this.pos.y)) { + c.drawImage(miscSheet[0], imgs.mageAttack4.img.x, imgs.mageAttack4.img.y, imgs.mageAttack4.img.w, imgs.mageAttack4.img.h, drawX, drawY, imgs.mageAttack4.img.w * scale, imgs.mageAttack4.img.h * scale); + } + + var flyVec = this.from.vectorTo(targetField).normalize(0.5); + + // randomly create magic stuff + for (var i = 0; i < tickDiff; i++) { + new Sprite({ + from: this.pos.add2(Math.random() * Math.PI * 2, 0.1), + img: this.imgs[Math.floor(Math.random() * this.imgs.length)], + scaleFunction: function(age) { + return 1 - age; + }, + age: 0.7, + varX: flyVec.px, + varY: flyVec.py, + zFunction: function(age) { + return Math.pow(age * 1.5, 2) * 0.33 + 0.2; + }, + xFunction: function(age) { + return this.varX * Math.sqrt(age); + }, + yFunction: function(age) { + return this.varY * Math.sqrt(age); + }, + height: height, + }); + } + + if (!this.isExpired()) { + return; + } + + // impact + new Sprite({ + from: targetField.add2(Math.random() * Math.PI * 2, Math.random() / 2).add3(0, 0.9), + img: this.imgs[Math.floor(Math.random() * this.imgs.length)], + scaleFunction: function(age) { + return (1 / (age + 0.25) + age - 4) * (-1); + }, + age: 1, + zFunction: function() { + return -0.9; + }, + height: height, + }); + + // create impact magic stuff + for (var i = 0; i < 3; i++) { + new Sprite({ + from: targetField.add2(Math.random() * Math.PI * 2, Math.random()* 0.3).add3(0, 0.9), + img: this.imgs[Math.floor(Math.random() * this.imgs.length)], + scaleFunction: function(age) { + return (1 / (age + 0.25) + age - 4) * (-0.5); + }, + age: 0.5 + Math.random() * 0.5, + zFunction: function() { + return -0.9; + }, + height: height, + }); + } +}; + + +Shockwave.prototype = new Effect(); +function Shockwave(data) { + this.pos = data.from; + this.tickOfCreation = ticksCounter; + this.duration = data.duration ? data.duration : ((data.originPos.distanceTo2(this.pos) / data.speed) * 20); + this.durationInMS = this.duration * 50; + this.tickOfDeath = ticksCounter + this.duration + 40; + + this.borderLeft = this.pos.px - 2; + this.borderRight = this.pos.px + 2; + this.borderTop = this.pos.py - 2; + this.borderBottom = this.pos.py + 2; + + this.circleDuration = 900; + this.circleOffsets = [0, 300, 600]; + + this.attach(data.attachToUnit); + + this.modId = data.modId; +}; + +Shockwave.prototype.draw = function() { + var age = ((ticksCounter + percentageOfCurrentTickPassed) - this.tickOfCreation) * 50; + + if (age < this.durationInMS) { + for (var i = 0; i < tickDiff * 1; i++) { + new Sprite({ + from: this.pos.add2(Math.random() * Math.PI * 2, Math.random() * 1.5), + img: imgs.whitePixel.img, + scaleFunction: function(age) { + return this.var1; + }, + age: 3 + Math.random() * 1, + var1: 1 + Math.random(), + var2: Math.random() * 2 - 1, + var3: Math.random() * 2 - 1, + ageLeft: (this.durationInMS - age) / (this.durationInMS / 2), + startHeight: Math.random() * 0.5 + 1.5, + alphaFunction: function(age) { + return Math.min(0.7, age); + }, + zFunction: function(age) { + return (age < this.ageLeft) ? Math.min(-this.startHeight + age * 1, 0) : ((-this.startHeight + this.ageLeft * 1) - (-1 / ((age - this.ageLeft) + 0.5) + 2 - (age - this.ageLeft) * 1.5)); + }, + xFunction: function(age) { + return (age < this.ageLeft) ? 0 : ((-1 / ((age - this.ageLeft) / 2 + 0.2) + 5) * this.var2); + }, + yFunction: function(age) { + return (age < this.ageLeft) ? 0 : ((-1 / ((age - this.ageLeft) / 2 + 0.2) + 5) * this.var3); + }, + }); + } + + for (var i = 0; i < tickDiff * 2; i++) { + new Sprite({ + from: this.pos.add2(Math.random() * Math.PI * 2, Math.random() * 0.5), + img: imgs.whitePixel.img, + scaleFunction: function(age) { + return this.var1; + }, + age: 3 + Math.random() * 1, + var1: 1 + Math.random(), + var2: Math.random() * 2 - 1, + var3: Math.random() * 2 - 1, + ageLeft: (this.durationInMS - age) / (this.durationInMS / 2), + alphaFunction: function(age) { + return Math.min(0.7, age); + }, + zFunction: function(age) { + return (age < this.ageLeft) ? 0 : -(-1 / ((age - this.ageLeft) + 0.5) + 2 - (age - this.ageLeft) * 1.5); + }, + xFunction: function(age) { + return (age < this.ageLeft) ? ((this.ageLeft - age) * this.var2 * 3) : ((-1 / ((age - this.ageLeft) / 2 + 0.2) + 5) * this.var2); + }, + yFunction: function(age) { + return (age < this.ageLeft) ? ((this.ageLeft - age) * this.var3 * 3) : ((-1 / ((age - this.ageLeft) / 2 + 0.2) + 5) * this.var3); + }, + }); + } + + // circles + for (var i = 0; i < this.circleOffsets.length; i++) { + if (age > this.circleOffsets[i] && age < (this.durationInMS - ((this.durationInMS - this.circleOffsets[i]) % this.circleDuration))) { + var state = (this.circleDuration - (age - this.circleOffsets[i]) % this.circleDuration) / this.circleDuration; + + c.globalAlpha = (1 - state) * 0.2; + drawCircle(this.pos.px * FIELD_SIZE - game.cameraX, this.pos.py * FIELD_SIZE - game.cameraY - 0.5, state * FIELD_SIZE * 3, 'white', 'white'); + } + } + + c.globalAlpha = 1; + } + + if (age > this.durationInMS) { + if (age < this.durationInMS * 1.4) { + var state = (age - this.durationInMS) / (this.durationInMS * 0.4); + c.globalAlpha = (1 - state) * 0.3; + drawCircle(this.pos.px * FIELD_SIZE - game.cameraX, this.pos.py * FIELD_SIZE - game.cameraY - 0.5, state * FIELD_SIZE * 5, 'white', 'white'); + c.globalAlpha = 1; + } + } +}; + + +Spell.prototype = new Effect(); +function Spell(data) { + this.pos = data.from; + this.x = this.pos.px; + this.y = this.pos.py; + this.tickOfCreation = ticksCounter; + this.tickOfDeath = ticksCounter + 20; + this.scale = data.scale ? data.scale : 1; + this.density = data.density ? data.density : 1; + + soundManager.playSound(SOUND.SPELL, game.getVolumeModifier(this.pos)); + + this.borderLeft = this.x; + this.borderRight = this.x; + this.borderTop = this.y; + this.borderBottom = this.y; + + this.attach(data.attachToUnit); + + this.modId = data.modId; +}; + +Spell.prototype.draw = function() { + var age = ((ticksCounter + percentageOfCurrentTickPassed) - this.tickOfCreation) * 50; + + for (var i = 0; i < tickDiff * this.density; i++) { + if (Math.random() < 0.75) { + new Sprite({ + from: new Field(this.x, this.y + 0.5, true).add2(Math.random() * Math.PI * 2, Math.random() * 0.4 * this.scale), + img: imgs.heal1.img, + scaleFunction: function(age) { + return ((1 / ((age * 5) + 0.25) + (age * 5) - 4) * (-0.6) + this.var1 * 0.4) * this.var2; + }, + age: 0.6 + Math.random() * 0.5, + var1: Math.random() + 0.5, + var2: this.scale, + zFunction: function(age) { + return -age * 1 - 0.5 * this.var2; + }, + }); + } + } +}; + + +Sprite.prototype = new Effect(); +function Sprite(data) { + _.extend(this, data); + + this.pos = data.from; + this.basePos = this.pos.getCopy(); + + this.tickOfBirth = ticksCounter; + + this.ticksToLive = this.age ? this.age * 20 : (Math.random() + 1) * 20; + this.timeToLive = this.ticksToLive / 20; + this.tickOfDeath = ticksCounter + this.ticksToLive; + + this.alphaFunction = this.alphaFunction ? this.alphaFunction : function() { + return 1; + }; + this.zFunction = this.zFunction ? this.zFunction : function(age) { + return -age * 0.5; + }; + + this.height = this.height ? this.height : 0; + + this.borderLeft = this.pos.px; + this.borderRight = this.pos.px; + this.borderTop = this.pos.py; + this.borderBottom = this.pos.py; + + this.attach(data.attachToUnit); + + this.modId = data.modId; +}; + +Sprite.prototype.draw = function() { + var age = (ticksCounter + percentageOfCurrentTickPassed - this.tickOfBirth) / 20; + + alpha = age < this.timeToLive * 0.5 ? 1 : Math.max(1 - (age - this.timeToLive / 2) / (this.timeToLive / 2), 0); + alpha *= this.alphaFunction(age); + + var scale = Math.max(this.scaleFunction(age) * SCALE_FACTOR, 0); + + var z = this.zFunction(age); + + var x = this.basePos.px; + if (this.xFunction) { + x += this.xFunction(age); + } + + var y = this.basePos.py; + if (this.yFunction) { + y += this.yFunction(age) * 0.75; + } + + this.pos = new Field(x, y, true); + + var drawX = this.pos.px * FIELD_SIZE - (this.img.w / 2) * scale - game.cameraX; + var drawY = (this.pos.py + z - this.height) * FIELD_SIZE - (this.img.h / 2) * scale - game.cameraY; + + if (PLAYING_PLAYER.team.canSeeField(this.pos.x, this.pos.y)) { + c.globalAlpha = alpha; + c.drawImage(miscSheet[0], this.img.x, this.img.y, this.img.w, this.img.h, drawX, drawY, this.img.w * scale, this.img.h * scale); + c.globalAlpha = 1; + } +}; + + +Aura.prototype = new Effect(); +function Aura(data) { + this.unit = (data.from && data.from.type) ? data.from : null; + this.pos = this.unit ? this.unit.drawPos : data.from; + this.realPos = this.pos; + this.tickOfCreation = ticksCounter; + this.tickOfDeath = (data.duration ? data.duration + ticksCounter + 40 : 99999999); + this.radius = data.scale ? data.scale : 3; + this.countParticles = data.density ? data.density : 20; + + this.borderLeft = this.pos.px - this.radius; + this.borderRight = this.pos.px + this.radius; + this.borderTop = this.pos.py - this.radius; + this.borderBottom = this.pos.py + this.radius; + + this.particles = []; + + this.attach(data.attachToUnit); + + this.modId = data.modId; + + this.drawPos = this.pos; + this.pos = this.pos.add3(0, 2); + + const toRGBAString = (color, alpha) => `rgba(${color.red}, ${color.green}, ${color.blue}, ${alpha ?? color.alpha})`; + const defaultAuraColor = list_modifiers_fields.auraColor.default_; + + this.circleColor = toRGBAString(data.auraColor ?? defaultAuraColor); + this.particleColor1 = toRGBAString(data.auraColor ?? defaultAuraColor, 0); + this.particleColor2 = toRGBAString(data.auraColor ?? defaultAuraColor, 0.7); + this.mode = data.mode ? data.mode : 1; + + // create vision offset circle array + this.visionOffsetArray = visionOffsets[Math.max(0, Math.min(visionOffsets.length - 1, parseInt(this.radius)))]; +}; + +Aura.prototype.draw = function() { + var age = ((ticksCounter + percentageOfCurrentTickPassed) - this.tickOfCreation) * 50; + + if (this.unit) { + this.drawPos = this.unit.drawPos; + this.pos = this.drawPos.add3(0, 2); + this.realPos = this.unit.pos; + + this.borderLeft = this.pos.px - this.radius; + this.borderRight = this.pos.px + this.radius; + this.borderTop = this.pos.py - this.radius; + this.borderBottom = this.pos.py + this.radius; + } + + var canSeeThis = false; + for (var i = 0; i < this.visionOffsetArray.length; i++) { + if (PLAYING_PLAYER.team.canSeeField(this.realPos.x + this.visionOffsetArray[i][0], this.realPos.y + this.visionOffsetArray[i][1])) { + canSeeThis = true; + i = this.visionOffsetArray.length; + } + } + + if (!canSeeThis && !PLAYING_PLAYER.team.canSeeField(this.realPos.x, this.realPos.y)) { + return; + } + + // circle + if (ticksCounter + 40 > this.tickOfDeath) { + c.globalAlpha = Math.max((this.tickOfDeath - ticksCounter) / 40, 0); + } + + drawCircle(this.drawPos.px * FIELD_SIZE - game.cameraX, this.drawPos.py * FIELD_SIZE - game.cameraY, this.radius * FIELD_SIZE, null, this.circleColor, 0.85); + + drawCircle(this.drawPos.px * FIELD_SIZE - game.cameraX, this.drawPos.py * FIELD_SIZE - game.cameraY, this.radius * FIELD_SIZE, 'rgba(200, 200, 255, 0.2)', null, 0.85, 1); + + c.globalAlpha = 1; + + while (this.particles.length < this.countParticles && ticksCounter + 40 < this.tickOfDeath) { + this.particles.push({ + pos: this.drawPos.add(new Field(0, 0, true).add2(Math.random() * Math.PI * 2, this.mode == 1 ? (Math.random() * this.radius) : (this.radius + 1)).mul(1, this.mode == 1 ? 0.6 : 0.85)), + tickOfCreation: ticksCounter + percentageOfCurrentTickPassed, + tickOfDeath: ticksCounter + percentageOfCurrentTickPassed + Math.random() * 40 + 20, + speed: (Math.random() * 0.5 + 0.35) / 1000, + }); + } + + c.lineWidth = FIELD_SIZE / 16; + + if (this.mode == 1) { + for (var i = 0; i < this.particles.length; i++) { + var particle = this.particles[i]; + + if (particle.tickOfDeath <= ticksCounter + percentageOfCurrentTickPassed) { + this.particles.splice(i, 1); + i--; + } else if (PLAYING_PLAYER.team.canSeeField(particle.pos.x, particle.pos.y)) { + var age2 = (ticksCounter + percentageOfCurrentTickPassed - particle.tickOfCreation) * 50; + + var drawX = 0; + var drawY = 0; + var drawX2 = 0; + + var drawY1 = 0; + var drawY2 = 0; + + if (this.mode == 1) { + drawX = particle.pos.px * FIELD_SIZE - game.cameraX; + drawX2 = drawX; + drawY = (particle.pos.py - 1) * FIELD_SIZE - game.cameraY; + + drawY1 = drawY - particle.speed * age2 * FIELD_SIZE; + drawY2 = drawY - (particle.speed * age2 * 1.5 - 2.4) * FIELD_SIZE; + } else if (this.mode == 2) { + var speed = (age2 / 1000) * (particle.pos.distanceTo2(this.drawPos) - 1) / ((particle.tickOfDeath - particle.tickOfCreation) / 20); + + var f1 = particle.pos.addNormalizedVector(this.drawPos, speed); + var f2 = particle.pos.addNormalizedVector(this.drawPos, speed + 1); + + + drawX = f1.px * FIELD_SIZE - game.cameraX; + drawY1 = f1.py * FIELD_SIZE - game.cameraY; + + drawX2 = f2.px * FIELD_SIZE - game.cameraX; + drawY2 = f2.py * FIELD_SIZE - game.cameraY; + } + + if (age2 < 500) { + c.globalAlpha = age2 / 500; + } + + if (particle.tickOfDeath - ticksCounter < 10) { + c.globalAlpha = (particle.tickOfDeath - ticksCounter) / 10; + } + + var grad = c.createLinearGradient(drawX2, drawY2, drawX, drawY1); + grad.addColorStop(0, this.particleColor1); + grad.addColorStop(0.5, this.particleColor2); + grad.addColorStop(1, this.particleColor1); + c.strokeStyle = grad; + + c.beginPath(); + c.moveTo(drawX, drawY1); + c.lineTo(drawX2, drawY2); + c.stroke(); + + c.globalAlpha = 1; + } + } + } else if (this.mode == 2) { + var gABase = 1; + if (ticksCounter + 40 > this.tickOfDeath) { + gABase = Math.max((this.tickOfDeath - ticksCounter) / 40, 0); + } + + for (var i = this.radius - ((ticksCounter + percentageOfCurrentTickPassed) / 30) % 1; i > 0; i -= 1) { + c.globalAlpha = (i > this.radius - 1) ? ((this.radius - i) * 1 * gABase) : gABase; + + drawCircle(this.drawPos.px * FIELD_SIZE - game.cameraX, (this.drawPos.py - Math.pow(this.radius - i, 0.5) * 0.7) * FIELD_SIZE - game.cameraY, i * FIELD_SIZE, null, this.circleColor, 0.85); + } + + c.globalAlpha = 1; + } +}; + +Sound.prototype = new Effect(); +function Sound(data) { + this.unit = (data.from && data.from.type) ? data.from : null; + this.pos = this.unit ? this.unit.drawPos : data.from; + this.tickOfCreation = ticksCounter; + this.tickOfDeath = (data.duration ? data.duration + ticksCounter + 40 : 99999999); + this.timeOFLastSound = -99999; + this.soundDuration = data.soundDuration; + this.sound = data.sound; + this.volume = data.volume; + + this.borderLeft = this.pos.px - 5; + this.borderRight = this.pos.px + 5; + this.borderTop = this.pos.py - 5; + this.borderBottom = this.pos.py + 5; + + this.attach(data.attachToUnit); + + this.modId = data.modId; +}; + +Sound.prototype.draw = function() { + if (this.timeOFLastSound + this.soundDuration <= timestamp && ticksCounter + 40 < this.tickOfDeath) { + this.timeOFLastSound = timestamp; + soundManager.playSound(this.sound, this.unit ? game.getVolumeModifier(this.unit.drawPos) * this.volume : game.getVolumeModifier(this.pos) * this.volume); + } +}; + + +function startEffect(typeName, data) { + if (typeName == 'arrow') { + return new Arrow(data); + } + + if (typeName == 'ballista') { + return new CustomProjectile(data); + } + + if (typeName == 'dragonAttack') { + return new DragonAttack(data); + } + + if (typeName == 'flame') { + return new Flame(data); + } + + if (typeName == 'flamestrike') { + return new Flamestrike(data); + } + + if (typeName == 'floatingText') { + return new FloatingText(data); + } + + if (typeName == 'heal') { + return new Heal(data); + } + + if (typeName == 'launchedRock') { + return new LaunchedRock(data); + } + + if (typeName == 'mageAttack') { + return new Mageattack(data); + } + + if (typeName == 'shockwave') { + return new Shockwave(data); + } + + if (typeName == 'spell') { + return new Spell(data); + } + + if (typeName == 'sprite') { + return new Sprite(data); + } + + if (typeName == 'aura') { + return new Aura(data); + } + + if (typeName == 'sound') { + return new Sound(data); + } +} + +// Error tracking sentry https://sentry.io/ +// Only run when the value sentry_dsn has been set in environment-config.json +if ( '@@sentry_dsn' != '@' + '@sentry_dsn') { + const Sentry = require('@sentry/browser'); + const { Integrations } = require('@sentry/tracing'); + Sentry.init({ + dsn: '@@sentry_dsn', + integrations: [new Integrations.BrowserTracing()], + + // Set tracesSampleRate to 1.0 to capture 100% + // of transactions for performance monitoring. + // We recommend adjusting this value in production + tracesSampleRate: 0.25, + }); +} + +// set size and resize +resize(); +window.onresize = resize; + +// Fill screen black +c.fillStyle = 'black'; +c.fillRect(0, 0, WIDTH, HEIGHT); + +// check for ie and dont run game if ie +if (window.navigator.userAgent.indexOf('MSIE ') > 0 || window.navigator.userAgent.indexOf('Trident/') > 0) { + alert('Internet Explorer is currently not supported for Littlewargame. Please use a different browser.'); + throw 'IE used'; +} + +// custom jquery ui tooltip styling +$(document).tooltip({ + show: { effect: 'fade', duration: 200 }, + hide: { effect: 'fade', duration: 100 }, + track: false, +}); + +// version +const version = '5.0.0'; + +// Ladder Season +let ladderSeasonId; + +// Webworker +var worker = new Worker(`Worker-${version}.js`); +console.log('Version: ' + version); + +worker.onerror = function(e) { + var e_msg = e.message + ' @' + e.filename + ':' + e.lineno + ':' + e.colno; + // var e_stack_trace = e.stack; + + displayInfoMsg(new HTMLBuilder() + .add(`Critical error: ${e_msg}`) + // .add(`save password
'); + + loginWindowBuilder.add(generateButton('loginWindowLoginButton', null, null, () => this.__formLogin(), 'Login')); + + // Only allow the player to go back if they are a guest, because otherwise they have been logged out + const backToLobbyButton = new UIElement('button', 'loginWindowBackButton', () => this.loginState == this.LoginStates.GUEST); + $('#loginWindowBackButton').text('Back to Lobby').click(() => game_state = GAME.LOBBY); + UIManagerSingleton.registerUIElement(backToLobbyButton); + + loginWindowBuilder.addDOM(backToLobbyButton.domElement); + + loginWindowBuilder.add(generateButton( + 'loginWindowGuestButton', + null, + 'Login as a guest. You will be able to use most features, so this is perfectly fine for trying out the game.', + addClickSound(() => network.send('login-guest<<$dummy-string')), + + 'Play as a guest', + )); + + loginWindowBuilder.add(generateButton('loginWindowCreateAccount', null, null, addClickSound(() => game_state = GAME.REGISTER), 'Create account')); + loginWindowBuilder.add(generateButton('recoverPWButton', null, null, addClickSound(() => game_state = GAME.RECOVERY), 'Forgot password?')); + + loginWindowBuilder.insertInto(`#${loginWindow.id}`); + UIManagerSingleton.registerUIElement(loginWindow); + }; + + Login_.prototype.__initRecoveryWindow = function() { + const recoveryWindow = new UIWindow('recoveryWindow', () => game_state == GAME.RECOVERY, false, 'Forgot Password'); + + const recoveryWindowBuilder = new HTMLBuilder() + .add('') + .add('') + .add('') + .add('') + .add('
New password
') + .add(''); + + recoveryWindowBuilder.add(generateButton('recoveryWindowCreate', null, null, addClickSound(() => { + const email = $('#recoveryWindowEmail').val(); + const password = $('#recoveryWindowPW').val(); + if (email && password) { + network.send('email-recover<<$' + email + '<<$' + password); + $('#recoveryWindowState').text('Waiting for server...'); + } else { + $('#recoveryWindowState').text('You must fill in all the fields'); + } + }), 'Recover')); + + recoveryWindowBuilder.add(generateButton('recoveryWindowBack', null, null, addClickSound(() => game_state = GAME.LOGIN), 'Back')); + + recoveryWindowBuilder.insertInto(`#${recoveryWindow.id}`); + UIManagerSingleton.registerUIElement(recoveryWindow); + }; + + Login_.prototype.__initRegisterWindow = function() { + const registerWindow = new UIWindow('registerWindow', function() { + return game_state == GAME.REGISTER; + }, false, 'Register'); + + const termsAndConditionsLinkID = uniqueID('termsAndConditions'); + const registerWindowBuilder = new HTMLBuilder() + .add('Create new account
') + .add('Name
Password
') + .add('First Name
Last Name
') + .add('${escapeHtml(game.name)} [${game.numPlayers}/${game.maxPlayers}] `);
+ if (game.running) {
+ gameBuilder.add('running ');
+ }
+ if (game.private) {
+ gameBuilder.add('');
+ }
+ gameBuilder
+ .addHook(() => el = $(`#${ID}`))
+ .addHook(() => el.mouseover(addZipSound(() => el[0].style.backgroundColor = 'rgba(255, 255, 255, 0.4)')))
+ .addHook(() => el.mouseout(addZipSound(() => el[0].style.backgroundColor = 'rgba(0, 0, 0, 0)')))
+ .addHook(() => el.click(addClickSound(() => this.joinGame(gameID))))
+ .appendInto('#gamesWindowTextArea');
+ }
+ };
+
+ return new GamesList_();
+})();
+
+const Clans = (() => {
+ function Clans_() {
+ Initialization.onDocumentReady(() => this.init());
+ }
+
+ Clans_.prototype.init = function() {
+ this.__initNetworkListeners();
+ };
+
+ Clans_.prototype.__initNetworkListeners = function() {
+ network.registerListener(null, 'clan-update', (splitMsg) => {
+ this.setClan(splitMsg[1], splitMsg[2], splitMsg[3], splitMsg[4], splitMsg[5], splitMsg[6]);
+ AccountInfo.clan = splitMsg[1];
+ });
+
+ network.registerListener(null, 'clan-wall', (splitMsg) => this.setClanWall(splitMsg));
+
+ network.registerListener(null, 'clan-update-noclan', (splitMsg) => {
+ this.setNoClan(splitMsg[1]);
+ AccountInfo.clan = splitMsg[1];
+ });
+
+ network.registerListener(null, 'clan-info', (splitMsg) => {
+ this.showClanInfo(splitMsg[1], splitMsg[2], splitMsg[3], splitMsg[4]);
+ });
+
+ network.registerListener(null, 'clans-list', (splitMsg) => {
+ const oldSearchTerm = typeof ($('#clanSearchInput').val()) == 'undefined' ? '' : $('#clanSearchInput').val();
+
+ const builder = new HTMLBuilder();
+
+ const page = parseInt(splitMsg[1]) + 1;
+ const totalPages = parseInt(splitMsg[2]);
+ const clans = [];
+
+ for (let i = 3; i < splitMsg.length - 1; i += 3) {
+ clans.push({
+ tag: escapeHtml(splitMsg[i]),
+ name: escapeHtml(splitMsg[i + 1]),
+ members: escapeHtml(splitMsg[i + 2]),
+ });
+ }
+
+ builder
+ .add('')
+ .add('
')
+ .add('
Tag | ') + .add('Name | ') + .add('Members |
[${clan.tag}] | `) + .add(`${clan.name} | `) + .add(`${clan.members} |