From 70dac57bf91b40c83aa2974d7aa266ccd37d75d2 Mon Sep 17 00:00:00 2001 From: Ivan Martell Date: Wed, 24 Jul 2024 23:11:41 +0900 Subject: [PATCH] Can now override js to add custom functionality. Mainly hotkeys. --- LittleWarGameClient/AddOns.js | 20 +- LittleWarGameClient/Form1.Designer.cs | 3 +- LittleWarGameClient/Form1.cs | 28 +- .../LittleWarGameClient.csproj | 6 +- LittleWarGameClient/VersionHandler.cs | 11 +- LittleWarGameClient/override/lwg-5.0.0.js | 69583 ++++++++++++++++ 6 files changed, 69631 insertions(+), 20 deletions(-) create mode 100644 LittleWarGameClient/override/lwg-5.0.0.js diff --git a/LittleWarGameClient/AddOns.js b/LittleWarGameClient/AddOns.js index c712927..7e8e493 100644 --- a/LittleWarGameClient/AddOns.js +++ b/LittleWarGameClient/AddOns.js @@ -49,14 +49,14 @@ ); }, - fakeClick: function (anchorObj) { - if (anchorObj.click) { - anchorObj.click() + fakeClick: function (element) { + if (element.click) { + element.click() } else if (document.createEvent) { var event = new MouseEvent('click', { 'view': window }); - anchorObj.dispatchEvent(evt); + element.dispatchEvent(evt); } }, @@ -148,6 +148,8 @@ addons.init = { function(clientVersion, mouseLock, volume) { this.handleConnectionError(); this.addExitButton(); + this.resizeInfoWindow(); + this.moveLoadingText(); this.changeQuitButtonText(); this.addVolumeSlider(volume); this.addClientVersion(clientVersion); @@ -161,6 +163,16 @@ addons.init = { this.jsInitComplete(); }, + moveLoadingText: function () { + var loadingText = document.getElementById("loadingText"); + loadingText.style.cssText = "top: 50px;"; + }, + + resizeInfoWindow: function () { + var infoWindow = document.getElementById("playerInfoWindow"); + infoWindow.style.cssText += "top: 50px; height: 580px;"; + }, + handleConnectionError: function () { var connectionErrorWindow = document.getElementById("NoConnectionWindow"); if (connectionErrorWindow != null) { diff --git a/LittleWarGameClient/Form1.Designer.cs b/LittleWarGameClient/Form1.Designer.cs index 893254e..fd18e24 100644 --- a/LittleWarGameClient/Form1.Designer.cs +++ b/LittleWarGameClient/Form1.Designer.cs @@ -104,7 +104,8 @@ private void InitializeComponent() mainImage.ErrorImage = null; mainImage.Image = Properties.Resources.soldier; mainImage.InitialImage = null; - mainImage.Location = new Point(582, 120); + mainImage.Location = new Point(582, 70); + mainImage.Margin = new Padding(0); mainImage.Name = "mainImage"; mainImage.Size = new Size(100, 100); mainImage.SizeMode = PictureBoxSizeMode.StretchImage; diff --git a/LittleWarGameClient/Form1.cs b/LittleWarGameClient/Form1.cs index afde17c..8c63a46 100644 --- a/LittleWarGameClient/Form1.cs +++ b/LittleWarGameClient/Form1.cs @@ -15,6 +15,7 @@ namespace LittleWarGameClient { internal partial class Form1 : Form { + private const string baseUrl = @"https://littlewargame.com/play"; private readonly Settings settings; private readonly Fullscreen fullScreen; private readonly KeyboardHandler kbHandler; @@ -44,7 +45,7 @@ private async void InitWebView() var path = Path.GetDirectoryName(System.Windows.Forms.Application.ExecutablePath); CoreWebView2Environment env = await CoreWebView2Environment.CreateAsync(null, Path.Join(path, "data"), new CoreWebView2EnvironmentOptions()); await webView.EnsureCoreWebView2Async(env); - webView.Source = new Uri("https://littlewargame.com/play", UriKind.Absolute); + webView.Source = new Uri(baseUrl, UriKind.Absolute); webView.CoreWebView2.Profile.DefaultDownloadFolderPath = Path.Join(path, "downloads"); webView.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false; webView.CoreWebView2.Settings.AreBrowserAcceleratorKeysEnabled = false; @@ -53,12 +54,10 @@ private async void InitWebView() private void webView_NavigationCompleted(object sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationCompletedEventArgs e) { - var addOnJS = System.IO.File.ReadAllText("AddOns.js"); - webView.CoreWebView2.ExecuteScriptAsync(addOnJS); + var addonJS = System.IO.File.ReadAllText("addons.js"); + webView.CoreWebView2.ExecuteScriptAsync(addonJS); ElementMessage.CallJSFunc(webView, "init.function", $"\"{vHandler.CurrentVersion}\", {settings.GetMouseLock().ToString().ToLower()}, {settings.GetVolume()}"); kbHandler.InitHotkeyNames(settings); - gameHasLoaded = true; - ResizeGameWindows(); } private void webView_WebMessageReceived(object sender, Microsoft.Web.WebView2.Core.CoreWebView2WebMessageReceivedEventArgs e) @@ -84,6 +83,8 @@ private void webView_WebMessageReceived(object sender, Microsoft.Web.WebView2.Co CaptureCursor(); break; case ButtonType.InitComplete: + gameHasLoaded = true; + ResizeGameWindows(); loadingPanel.Visible = false; loadingTimer.Enabled = false; loadingText.Text = "Reconnecting"; @@ -139,6 +140,7 @@ private void Form1_ResizeEnd(object sender, EventArgs e) private void Form1_Resize(object sender, EventArgs e) { + mainImage.Top = this.Height / 4; CaptureCursor(); ResizeGameWindows(); } @@ -175,8 +177,24 @@ private void loadingTimer_Tick(object sender, EventArgs e) private void webView_NavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs e) { + webView.CoreWebView2.AddWebResourceRequestedFilter("*", CoreWebView2WebResourceContext.Script); + webView.CoreWebView2.WebResourceRequested += + delegate (object? sender, CoreWebView2WebResourceRequestedEventArgs args) + { + if (args.Request.Uri == $"{baseUrl}/js/lwg-5.0.0.js") + { + try + { + FileStream fs = File.Open("override/lwg-5.0.0.js", FileMode.Open); + CoreWebView2WebResourceResponse response = webView.CoreWebView2.Environment.CreateWebResourceResponse(fs, 200, "OK", "Content-Type: text/javascript"); + args.Response = response; + } + catch { } + } + }; loadingPanel.Visible = true; loadingTimer.Enabled = true; + gameHasLoaded = false; } } } diff --git a/LittleWarGameClient/LittleWarGameClient.csproj b/LittleWarGameClient/LittleWarGameClient.csproj index 4691c93..d550dbc 100644 --- a/LittleWarGameClient/LittleWarGameClient.csproj +++ b/LittleWarGameClient/LittleWarGameClient.csproj @@ -23,13 +23,17 @@ + - + PreserveNewest + + PreserveNewest + diff --git a/LittleWarGameClient/VersionHandler.cs b/LittleWarGameClient/VersionHandler.cs index 4a83409..9d9adb2 100644 --- a/LittleWarGameClient/VersionHandler.cs +++ b/LittleWarGameClient/VersionHandler.cs @@ -84,25 +84,18 @@ private async void TryGetLatestVersionAsync() private async Task GetLatestGitHubVersion() { var client = new GitHubClient(new ProductHeaderValue("LWGClient")); - IReadOnlyList releases = await client.Repository.Release.GetAll("ivanpmartell", "LittleWarGameClient"); - return new Version(releases[0].TagName.Substring(1)); + var release = await client.Repository.Release.GetLatest("ivanpmartell", "LittleWarGameClient"); + return new Version(release.TagName.Substring(1)); } private async Task TimeoutAfter(Task task, TimeSpan timeout) { - // We need to be able to cancel the "timeout" task, so create a token source using var cts = new CancellationTokenSource(); - // Create the timeout task (don't await it) var timeoutTask = Task.Delay(timeout, cts.Token); - - // Run the task and timeout in parallel, return the Task that completes first var completedTask = await Task.WhenAny(task, timeoutTask).ConfigureAwait(false); - if (completedTask == task) { - // Cancel the "timeout" task so we don't leak a Timer cts.Cancel(); - // await the task to bubble up any errors etc return await task.ConfigureAwait(false); } else diff --git a/LittleWarGameClient/override/lwg-5.0.0.js b/LittleWarGameClient/override/lwg-5.0.0.js new file mode 100644 index 0000000..3dceaaa --- /dev/null +++ b/LittleWarGameClient/override/lwg-5.0.0.js @@ -0,0 +1,69583 @@ +(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i { + let counter = 0; + return (prefix) => (prefix ? prefix : 'id') + counter++; +})(); + +function HTMLBuilder() { + this.html = ''; + this.hooks = []; +} + +HTMLBuilder.prototype.add = function(content) { + if (typeof content === 'string') { + this.html += content; + } else if (typeof content === 'object' && 'html' in content && 'hooks' in content) { + this.html += content.html; + this.addHooks(content.hooks); + } else { + throw 'Input must be an HTML string or an HTMLBuilder-like object'; + } + return this; +}; + +// Polls the function getValue at a specified for updated values and updates the HTML in the DOM +// - getValue may return either a string or an HTMLBuilder, but either must be complete valid HTML (no unmatched tags) +// - period specifies the time between updates in milliseconds (default 100 ms) +HTMLBuilder.prototype.addReactive = function(getValue, period = 100) { + const ID = uniqueID('reactive'); + this.html += ``; + this.addHook(() => { + (function update() { + const el = document.getElementById(ID); + if (el) { + const value = getValue(); + + if (value instanceof HTMLBuilder) { + value.insertInto(el); + } else { + el.innerHTML = value; + } + + setTimeout(update, period); + } + })(); + }); + return this; +}; + +HTMLBuilder.prototype.addDOM = function(domElement) { + const ID = uniqueID('DOM'); + this.html += ``; + this.addHook(() => $(`#${ID}`).html(domElement)); + return this; +}; + +HTMLBuilder.prototype.addHook = function(hook) { + this.hooks.push(hook); + return this; +}; + +HTMLBuilder.prototype.addHooks = function(hooks) { + this.hooks = this.hooks.concat(hooks); + return this; +}; + +HTMLBuilder.prototype.insertInto = function(selector) { + $(selector).html(this.html); + this.hooks.forEach((hook) => hook()); +}; + +HTMLBuilder.prototype.appendInto = function(selector) { + $(selector).append(this.html); + this.hooks.forEach((hook) => hook()); +}; + +HTMLBuilder.prototype.appendAfter = function(selector) { + $(selector).after(this.html); + this.hooks.forEach((hook) => hook()); +}; + +HTMLBuilder.prototype.appendBefore = function(selector) { + $(selector).before(this.html); + this.hooks.forEach((hook) => hook()); +}; + +HTMLBuilder.prototype.print = function() { + console.log(this.html); + console.log(this.hooks); + return this; +}; + +module.exports.HTMLBuilder = HTMLBuilder; +module.exports.uniqueID = uniqueID; + +},{}],3:[function(require,module,exports){ +class LocalConfigValue { + constructor(key, configuration, defaultValue = undefined) { + this.key = key; + this.configuration = configuration; + this.onChangeListeners = []; + + this.value = this.configuration.__get(this.key, defaultValue); + } + + get(defaultValue = undefined) { + return this.value ?? defaultValue; + } + + set(value) { + this.configuration.__set(this.key, value); + this.value = value; + this.onChangeListeners.forEach((c) => c(value)); + } + + onChange(cb) { + this.onChangeListeners.push(cb); + } +}; + +const LOCAL_STORAGE_KEY_NAME_CONFIG = 'configuration'; + +function LocalConfig() { + if (!localStorage.getItem(LOCAL_STORAGE_KEY_NAME_CONFIG)) { + localStorage.setItem(LOCAL_STORAGE_KEY_NAME_CONFIG, JSON.stringify({})); + } +} + +LocalConfig.prototype.__getAndSet = function(cb) { + const obj = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY_NAME_CONFIG)); + cb(obj); + localStorage.setItem(LOCAL_STORAGE_KEY_NAME_CONFIG, JSON.stringify(obj)); +}; + +// Gets the config value for the given key, and returns the defaultValue if specified +// Additionally sets the config value to the defaultValue if provided +LocalConfig.prototype.__get = function(key, defaultValue = undefined) { + let value; + this.__getAndSet((obj) => { + if (!(key in obj) && defaultValue != undefined) { + obj[key] = defaultValue; + } + value = obj[key]; + }); + return value; +}; + +LocalConfig.prototype.__set = function(key, value) { + this.__getAndSet((obj) => obj[key] = value); +}; + +LocalConfig.prototype.registerValue = function(key, defaultValue = undefined) { + return new LocalConfigValue(key, this, defaultValue); +}; + +module.exports = new LocalConfig(); + +},{}],4:[function(require,module,exports){ +// plays and manages all the sounds +const LocalConfig = require('./LocalConfig.js'); +const SOUND = require('./data/Sound.js'); + + +var DEFAULT_VOLUME = 0.20; + +function SoundManager() { + this.volume = LocalConfig.registerValue('sound_volume', DEFAULT_VOLUME); + + this.sounds = []; + + this.sounds[SOUND.FLESH] = this.loadSound([ + 'sounds/hit05.ogg', + 'sounds/hit08.ogg', + 'sounds/hit13.ogg', + 'sounds/hit14.ogg', + ]); + + this.sounds[SOUND.LADDER_START] = this.loadSound([ + 'sounds/ladder-start.ogg', + ]); + + this.sounds[SOUND.ARCHIVEMENT] = this.loadSound([ + 'sounds/archivement.ogg', + ]); + + this.sounds[SOUND.ARCHIVEMENT2] = this.loadSound([ + 'sounds/archivement2.ogg', + ]); + + this.sounds[SOUND.ARCHIVEMENT3] = this.loadSound([ + 'sounds/archivement3.ogg', + ]); + + this.sounds[SOUND.WARP] = this.loadSound([ + 'sounds/warp-totem.ogg', + 'sounds/warp-totem.ogg', + ]); + + this.sounds[SOUND.OPEN_WINDOW] = this.loadSound([ + 'sounds/zip-new.ogg', + ]); + + this.sounds[SOUND.PAIN] = this.loadSound([ + 'sounds/pain1.ogg', + 'sounds/pain2.ogg', + 'sounds/pain5.ogg', + 'sounds/pain1.ogg', + ]); + + this.sounds[SOUND.WOLF_PAIN] = this.loadSound([ + 'sounds/wolf-pain.ogg', + 'sounds/wolf-pain-2.ogg', + 'sounds/wolf-pain.ogg', + 'sounds/wolf-pain-2.ogg', + ]); + + this.sounds[SOUND.BIRD_SPAWN] = this.loadSound([ + 'sounds/bird1.ogg', + ]); + + this.sounds[SOUND.BIRD_YES] = this.loadSound([ + 'sounds/bird2.ogg', + ]); + + this.sounds[SOUND.BIRD_DEATH] = this.loadSound([ + 'sounds/bird3.ogg', + ]); + + this.sounds[SOUND.BIRD_SLAM] = this.loadSound([ + 'sounds/bird-slam.ogg', + ]); + + this.sounds[SOUND.DEATH] = this.loadSound([ + 'sounds/die1.ogg', + 'sounds/die2.ogg', + 'sounds/die1.ogg', + ]); + + this.sounds[SOUND.WOLF_DEATH] = this.loadSound([ + 'sounds/wolf-die.ogg', + 'sounds/wolf-die-2.ogg', + ]); + + this.sounds[SOUND.WOLF_HIT] = this.loadSound([ + 'sounds/wolf-hit.ogg', + 'sounds/wolf-hit-2.ogg', + 'sounds/wolf-hit.ogg', + ]); + + this.sounds[SOUND.SWING] = this.loadSound([ + 'sounds/swing.ogg', + 'sounds/swing2.ogg', + 'sounds/swing4.ogg', + ]); + + this.sounds[SOUND.READY] = this.loadSound([ + 'sounds/voices/ready1.ogg', + 'sounds/voices/ready2.ogg', + 'sounds/voices/ready3.ogg', + 'sounds/voices/ready4.ogg', + 'sounds/voices/ready5.ogg', + 'sounds/voices/ready7.ogg', + 'sounds/voices/ready8.ogg', + ]); + + this.sounds[SOUND.YES] = this.loadSound([ + 'sounds/voices/yes1.ogg', + 'sounds/voices/yes2.ogg', + 'sounds/voices/yes3.ogg', + 'sounds/voices/yes4.ogg', + 'sounds/voices/yes5.ogg', + ]); + + this.sounds[SOUND.WOLF_YES] = this.loadSound([ + 'sounds/voices/wolf-yes.ogg', + 'sounds/voices/wolf-yes-2.ogg', + ]); + + this.sounds[SOUND.WOLF_READY] = this.loadSound([ + 'sounds/voices/wolf-spawn.ogg', + ]); + + this.sounds[SOUND.SWORD] = this.loadSound([ + 'sounds/sword.ogg', + 'sounds/sword.ogg', + 'sounds/sword.ogg', + 'sounds/sword.ogg', + ]); + + this.sounds[SOUND.PLACE] = this.loadSound([ + 'sounds/clic02.ogg', + ]); + + this.sounds[SOUND.NEGATIVE] = this.loadSound([ + 'sounds/negative_2.ogg', + ]); + + this.sounds[SOUND.POSITIVE] = this.loadSound([ + 'sounds/misc_menu.ogg', + ]); + + this.sounds[SOUND.CLICK] = this.loadSound([ + 'sounds/click1.ogg', + ]); + + this.sounds[SOUND.CLICK2] = this.loadSound([ + 'sounds/click.ogg', + ]); + + this.sounds[SOUND.BUILD] = this.loadSound([ + 'sounds/build.ogg', + ]); + + this.sounds[SOUND.BUILDING_DEATH] = this.loadSound([ + 'sounds/building_destroy.ogg', + 'sounds/building_destroy.ogg', + 'sounds/building_destroy.ogg', + ]); + + this.sounds[SOUND.REPAIR] = this.loadSound([ + 'sounds/hammer1.ogg', + 'sounds/hammer2.ogg', + 'sounds/hammer3.ogg', + 'sounds/hammer1.ogg', + 'sounds/hammer2.ogg', + ]); + + this.sounds[SOUND.INGAMECLICK] = this.loadSound([ + 'sounds/mouseclick1.ogg', + ]); + + this.sounds[SOUND.BUILDING_FINISHED] = this.loadSound([ + 'sounds/building_finished.ogg', + 'sounds/building_finished.ogg', + ]); + + this.sounds[SOUND.UNDER_ATTACK] = this.loadSound([ + 'sounds/under-attack.ogg', + ]); + + this.sounds[SOUND.VICTORY] = this.loadSound([ + 'sounds/yeah.ogg', + ]); + + this.sounds[SOUND.DEFEAT] = this.loadSound([ + 'sounds/defeat.ogg', + ]); + + this.sounds[SOUND.FLAMESTRIKE_LAUNCH] = this.loadSound([ + 'sounds/flamestrike-throw.ogg', + 'sounds/flamestrike-throw.ogg', + 'sounds/flamestrike-throw.ogg', + ]); + + this.sounds[SOUND.FLAMESTRIKE_IMPACT] = this.loadSound([ + 'sounds/flamestrike-impact.ogg', + 'sounds/flamestrike-impact.ogg', + 'sounds/flamestrike-impact.ogg', + ]); + + this.sounds[SOUND.MAGE_IMPACT] = this.loadSound([ + 'sounds/mage-impact.ogg', + 'sounds/mage-impact.ogg', + 'sounds/mage-impact.ogg', + ]); + + this.sounds[SOUND.MAGE_ATTACK] = this.loadSound([ + 'sounds/mage-attack.ogg', + 'sounds/mage-attack.ogg', + 'sounds/mage-attack.ogg', + ]); + + this.sounds[SOUND.GUN] = this.loadSound([ + 'sounds/gun.ogg', + 'sounds/gun.ogg', + ]); + + this.sounds[SOUND.CATA_HIT] = this.loadSound([ + 'sounds/cata-hit.ogg', + 'sounds/cata-hit.ogg', + ]); + + this.sounds[SOUND.CATA_LAUNCH] = this.loadSound([ + 'sounds/catapult-launch.ogg', + 'sounds/catapult-launch-2.ogg', + 'sounds/catapult-launch.ogg', + 'sounds/catapult-launch-2.ogg', + ]); + + this.sounds[SOUND.CATA_IMPACT] = this.loadSound([ + 'sounds/catapult-impact.ogg', + 'sounds/catapult-impact-2.ogg', + 'sounds/catapult-impact-3.ogg', + 'sounds/catapult-impact-4.ogg', + ]); + + this.sounds[SOUND.BUILDING_PAIN] = this.loadSound([ + 'sounds/building-hit.ogg', + 'sounds/building-hit-3.ogg', + 'sounds/building-hit.ogg', + 'sounds/building-hit-3.ogg', + ]); + + this.sounds[SOUND.CATA_DEATH] = this.loadSound([ + 'sounds/catapult_death.ogg', + 'sounds/catapult_death.ogg', + ]); + + this.sounds[SOUND.GAME_START] = this.loadSound([ + 'sounds/gamestart.ogg', + ]); + + this.sounds[SOUND.HEAL] = this.loadSound([ + 'sounds/heal.ogg', + 'sounds/heal.ogg', + ]); + + this.sounds[SOUND.BING] = this.loadSound([ + 'sounds/bing2.ogg', + ]); + + this.sounds[SOUND.BING2] = this.loadSound([ + 'sounds/bing.ogg', + ]); + + this.sounds[SOUND.SWITCH] = this.loadSound([ + 'sounds/switch.ogg', + ]); + + this.sounds[SOUND.ZIP] = this.loadSound([ + 'sounds/zip.ogg', + ]); + + this.sounds[SOUND.ZIP3] = this.loadSound([ + 'sounds/zip3.ogg', + ]); + + this.sounds[SOUND.SPELL] = this.loadSound([ + 'sounds/spell.ogg', + 'sounds/spell.ogg', + ]); + + this.sounds[SOUND.DRAGON_SPAWN] = this.loadSound([ + 'sounds/dragon_spawn.ogg', + 'sounds/dragon_spawn.ogg', + ]); + + this.sounds[SOUND.DRAGON_DEATH] = this.loadSound([ + 'sounds/dragon_death.ogg', + 'sounds/dragon_death.ogg', + ]); + + this.sounds[SOUND.DRAGON_YES] = this.loadSound([ + 'sounds/dragon_yes_1.ogg', + 'sounds/dragon_yes_2.ogg', + 'sounds/dragon_yes_3.ogg', + ]); + + this.sounds[SOUND.FALL] = this.loadSound([ + 'sounds/fall.ogg', + 'sounds/fall.ogg', + ]); + + this.sounds[SOUND.DRAGON_FIRE] = this.loadSound([ + 'sounds/dragon_fire_1.ogg', + 'sounds/dragon_fire_2.ogg', + 'sounds/dragon_fire_1.ogg', + 'sounds/dragon_fire_2.ogg', + ]); + + this.sounds[SOUND.MINE] = this.loadSound([ + 'sounds/mine1.ogg', + 'sounds/mine2.ogg', + 'sounds/mine3.ogg', + 'sounds/mine1.ogg', + 'sounds/mine2.ogg', + ]); + + this.sounds[SOUND.ROUNDHOUSE] = this.loadSound([ + 'sounds/roundhouse.ogg', + 'sounds/roundhouse.ogg', + ]); + + this.sounds[SOUND.BIGHIT] = this.loadSound([ + 'sounds/big-hit.ogg', + 'sounds/big-hit-2.ogg', + 'sounds/big-hit.ogg', + 'sounds/big-hit-2.ogg', + ]); + + this.sounds[SOUND.STRONG_HIT] = this.loadSound([ + 'sounds/strong-hit.ogg', + 'sounds/strong-hit-2.ogg', + ]); + + this.sounds[SOUND.BEAST_READY] = this.loadSound([ + 'sounds/beast-ready.ogg', + ]); + + this.sounds[SOUND.BEAST_YES] = this.loadSound([ + 'sounds/beast-yes.ogg', + 'sounds/beast-yes-2.ogg', + ]); + + this.sounds[SOUND.BEAST_DIE] = this.loadSound([ + 'sounds/beast-die.ogg', + ]); + + this.sounds[SOUND.FLAK] = this.loadSound([ + 'sounds/flak1.ogg', + 'sounds/flak2.ogg', + 'sounds/flak3.ogg', + ]); + + this.sounds[SOUND.SHOCKWAVE] = this.loadSound([ + 'sounds/shockwave.ogg', + 'sounds/shockwave.ogg', + ]); + + this.sounds[SOUND.FIREBALL] = this.loadSound([ + 'sounds/fireball.ogg', + 'sounds/fireball.ogg', + ]); + + this.sounds[SOUND.SKELETON_PAIN] = this.loadSound([ + 'sounds/skeleton-hit.ogg', + 'sounds/skeleton-hit.ogg', + ]); + + this.sounds[SOUND.SKELETON_YES] = this.loadSound([ + 'sounds/skeleton1.ogg', + 'sounds/skeleton3.ogg', + ]); + + this.sounds[SOUND.SKELETON_SPAWN] = this.loadSound([ + 'sounds/skeleton2.ogg', + 'sounds/skeleton2.ogg', + ]); + + this.sounds[SOUND.AURA_HEAL] = this.loadSound([ + 'sounds/heal-aura.ogg', + 'sounds/heal-aura.ogg', + ]); + + this.sounds[SOUND.PLASMA_SHIELD] = this.loadSound([ + 'sounds/plasma-shield.ogg', + 'sounds/plasma-shield2.ogg', + 'sounds/plasma-shield3.ogg', + ]); + + this.sounds[SOUND.BATTLE_FANFARE] = this.loadSound([ + 'sounds/battle-fanfare.ogg', + ]); + + this.sounds[SOUND.A_WS] = this.loadSound([ + 'sounds/buildings/advanced_Workshop.ogg', + ]); + + this.sounds[SOUND.CC] = this.loadSound([ + 'sounds/buildings/cc.ogg', + ]); + + this.sounds[SOUND.CHURCH] = this.loadSound([ + 'sounds/buildings/church.ogg', + ]); + + this.sounds[SOUND.DRAGONS_LAIR] = this.loadSound([ + 'sounds/buildings/dragons_lair.ogg', + ]); + + this.sounds[SOUND.FORGE] = this.loadSound([ + 'sounds/buildings/forge.ogg', + ]); + + this.sounds[SOUND.HOUSE] = this.loadSound([ + 'sounds/buildings/house.ogg', + ]); + + this.sounds[SOUND.LAB] = this.loadSound([ + 'sounds/buildings/lab.ogg', + ]); + + this.sounds[SOUND.MAGES_GUILD] = this.loadSound([ + 'sounds/buildings/magesguild.ogg', + ]); + + this.sounds[SOUND.RAX] = this.loadSound([ + 'sounds/buildings/rax.ogg', + ]); + + this.sounds[SOUND.WW_DEN] = this.loadSound([ + 'sounds/buildings/ww_den.ogg', + ]); + + this.sounds[SOUND.W_DEN] = this.loadSound([ + 'sounds/buildings/w_den.ogg', + ]); + + this.sounds[SOUND.WORKSHOP] = this.loadSound([ + 'sounds/buildings/workshop.ogg', + ]); + + this.sounds[SOUND.MILL] = this.loadSound([ + 'sounds/buildings/mill.ogg', + ]); + + this.sounds[SOUND.SNAKE_CHARMER] = this.loadSound([ + 'sounds/buildings/snake_charmer.ogg', + ]); + + this.sounds[SOUND.ARMORY] = this.loadSound([ + 'sounds/buildings/armory.ogg', + ]); + + this.sounds[SOUND.AIRSHIP] = this.loadSound([ + 'sounds/voices/airship.ogg', + ]); + + this.sounds[SOUND.ARCHER_READY] = this.loadSound([ + 'sounds/voices/archer_ready.ogg', + ]); + + this.sounds[SOUND.ARCHER] = this.loadSound([ + 'sounds/voices/archer1.ogg', + 'sounds/voices/archer2.ogg', + 'sounds/voices/archer3.ogg', + 'sounds/voices/archer2.ogg', + 'sounds/voices/archer1.ogg', + ]); + + this.sounds[SOUND.BALLISTA] = this.loadSound([ + 'sounds/voices/ballista.ogg', + ]); + + this.sounds[SOUND.CALTROP] = this.loadSound([ + 'sounds/voices/caltrop.ogg', + ]); + + this.sounds[SOUND.CATAPULT] = this.loadSound([ + 'sounds/voices/catapult.ogg', + ]); + + this.sounds[SOUND.GATLING_GUN] = this.loadSound([ + 'sounds/voices/gatlinggun.ogg', + ]); + + this.sounds[SOUND.GYROCRAFT_READY] = this.loadSound([ + 'sounds/voices/gyro_ready1.ogg', + 'sounds/voices/gyro_ready2.ogg', + ]); + + this.sounds[SOUND.MAGE_READY] = this.loadSound([ + 'sounds/voices/mage_ready.ogg', + ]); + + this.sounds[SOUND.MAGE] = this.loadSound([ + 'sounds/voices/mage1.ogg', + 'sounds/voices/mage2.ogg', + 'sounds/voices/mage3.ogg', + 'sounds/voices/mage2.ogg', + 'sounds/voices/mage1.ogg', + ]); + + this.sounds[SOUND.PRIEST] = this.loadSound([ + 'sounds/voices/priest1.ogg', + 'sounds/voices/priest2.ogg', + 'sounds/voices/priest3.ogg', + 'sounds/voices/priest1.ogg', + 'sounds/voices/priest3.ogg', + ]); + + this.sounds[SOUND.RAIDER] = this.loadSound([ + 'sounds/voices/raider1.ogg', + 'sounds/voices/raider2.ogg', + 'sounds/voices/raider3.ogg', + 'sounds/voices/raider2.ogg', + 'sounds/voices/raider1.ogg', + ]); + + this.sounds[SOUND.SOLDIER] = this.loadSound([ + 'sounds/voices/soldier1.ogg', + 'sounds/voices/soldier2.ogg', + 'sounds/voices/soldier3.ogg', + 'sounds/voices/soldier2.ogg', + 'sounds/voices/soldier1.ogg', + ]); + + this.sounds[SOUND.GYROCRAFT_YES] = this.loadSound([ + 'sounds/voices/gyro_yes1.ogg', + 'sounds/voices/gyro_yes2.ogg', + 'sounds/voices/gyro_yes3.ogg', + 'sounds/voices/gyro_yes4.ogg', + ]); + + this.sounds[SOUND.GYROCRAFT_PAIN] = this.loadSound([ + 'sounds/voices/gyro_hurt1.ogg', + 'sounds/voices/gyro_hurt2.ogg', + ]); + + this.buildingClickSound = []; +}; + +SoundManager.prototype.loadSound = function(files) { + var target = []; + + for (var i = 0; i < files.length; i++) { + target.push(new Audio(files[i])); + } + + return target; +}; + + +SoundManager.prototype.playSound = function(sound, volume = 1, isUnitClickSound = false) { + // if sound is disabled (by the user in options) return + if (!(this.volume.get() > 0) || !this.sounds[sound]) { + return; + } + + if (volume <= 0) { + return; + } + + var s = this.sounds[sound]; + var readySounds = []; + + // find sound(s), which are / is ready + for (var i = 0; i < s.length; i++) { + if (s[i].currentTime >= s[i].duration || s[i].currentTime == 0 || !s[i].currentTime) { + readySounds.push(s[i]); + } + } + + // all sounds still in use, return + if (readySounds.length == 0) { + return; + } + + // random one of the ready sounds + var soundToPlay = readySounds[Math.floor(Math.random() * readySounds.length)]; + + // play sound + // soundToPlay.load(); + soundToPlay.loop = false; + // soundToPlay.currentTime = 0; + soundToPlay.play(); + soundToPlay.volume = volume * this.volume.get(); + + if (isUnitClickSound) { + this.buildingClickSound.push({ + sound: soundToPlay, + maxVolume: soundToPlay.volume, + }); + } +}; + +module.exports = new SoundManager(); + +},{"./LocalConfig.js":3,"./data/Sound.js":6}],5:[function(require,module,exports){ +soundManager = require('./SoundManager.js'); +const SOUND = require('./data/Sound.js'); + +// a html div, that has children +function UIWindow(id, condition, closeable, title, draggable, onKey, onClose) { + this.id = id; + + this.domElement = document.createElement('div'); + this.domElement.id = id; + this.domElement.className = 'ingameWindow' + (draggable ? ' draggable' : ''); + this.draggable = draggable; + if (condition) { + this.domElement.style.display = 'none'; + this.active = true; // some elements are closable; it not closable, this is always true + this.condition = condition; // decides if this should be drawn + // TODO: MOVE THIS.CONDITION OUT OF HERE - CURRENTLY BREAKS EVERYTHING + } + document.body.appendChild(this.domElement); + + this.onKey = onKey; + + // if this is true, the windiw has a "x" button for closing in the corner + if (closeable) { + this.active = false; + this.wasActiveLastFrame = false; + + // create close button + var closeButton = document.createElement('button'); + closeButton.className = 'closeButton'; + closeButton.innerHTML = 'X'; + this.closeButton = closeButton; + closeButton.parent_ = this; + closeButton.onclick = function() { + soundManager.playSound(SOUND.CLICK); + this.parent_.active = false; + closeButton.parent_.fadeOut($(this.parent_.domElement)); + if (onClose) { + onClose(); + } + + if (this.parent_.domElement.id == 'infoWindow2') { + setTimeout(showAchievement, 1000); // TODO: Bug found. showAchievement is undefined. No read access to showAchievement or storedAchievements + } + }; + + // make it close on escape + this.domElement.parent_ = this; + this.domElement.onkeydown = function(e) { + if (keyManager.getKeyCode(e) == KEY.ESC) { + soundManager.playSound(SOUND.CLICK); + this.parent_.active = false; + closeButton.parent_.fadeOut($(this.closeButton.domElement)); + + if (this.parent_.domElement.id == 'infoWindow2') { + setTimeout(showAchievement, 1000); + } // TODO: showAchievement is undefined. No read access to showAchievement or storedAchievements + } + }; + + // add it to the window + this.domElement.appendChild(closeButton); + } + + // if a title is set + if (title) { + var title_ = document.createElement('h2'); + title_.innerHTML = '» ' + title; + title_.className = 'windowTitle'; + this.title = title_; + this.domElement.appendChild(title_); + } + + // head rider + this.head_rider = document.createElement('div'); + this.head_rider.className = 'head_rider'; + this.domElement.appendChild(this.head_rider); + + this.blocksCanvas = true; + // elements.push(this); + // TODO: ADD THIS - currently divs default to visible so this causes issues. +}; + +// [DEPRECATE] Use setTitleText + setTitleStyle + setRider instead +UIWindow.prototype.setTitle = function(newTitle, head_rider) { + this.head_rider.innerHTML = head_rider ? head_rider : ''; + + var h2s = this.domElement.getElementsByTagName('h2'); + + if (h2s.length == 0) { + return; + } + + h2s[0].innerHTML = newTitle; +}; + +UIWindow.prototype.setTitleText = function(title) { + if (this.title) { + this.title.textContent = title; + } +}; + +UIWindow.prototype.setTitleTitle = function(title) { + if (this.title) { + this.title.title = title; + } +}; + +UIWindow.prototype.setTitleStyle = function(style) { + if (!this.title) { + return; + } + this.title.className = style; +}; + +UIWindow.prototype.setHeadRider = function(riderBuilder) { + riderBuilder.insertInto(this.head_rider); +}; + +UIWindow.prototype.setRider = function(riderBuilder) { + riderBuilder.insertInto('#riderDiv'); +}; + +UIWindow.prototype.setCloseButtonStyle = function(style) { + this.closeButton.className = style; +}; + +UIWindow.prototype.setBackgroundStyle = function(style) { + this.domElement.className = style; + this.closeButton.className = style; +}; + +// adds a scrollable subdiv to the parent div, which covers most of the div, usually used for text content (chat window for example) +UIWindow.prototype.addScrollableSubDiv = function(id) { + var textArea = document.createElement('div'); + textArea.className = 'textContainer'; + textArea.id = id; + this.domElement.appendChild(textArea); + return textArea; +}; + +UIWindow.prototype.addSubDiv = function(id) { + const div = document.createElement('div'); + div.id = id; + this.domElement.appendChild(div); + return div; +}; + +// called every frame; checks if this element should be drawn and then does so +UIWindow.prototype.refreshVisibility = function() { + if (this.active && this.condition()) { + if (this.wasActiveLastFrame) { + return this.blocksCanvas; + } + + this.wasActiveLastFrame = true; + this.domElement.style.display = 'inline'; + this.domElement.style.opacity = 1; + + // if the window contains an input, set focus on it + var inputs = this.domElement.getElementsByTagName('input'); + if (inputs[0]) { + inputs[0].focus(); + var val = inputs[0].value; + inputs[0].value = '-'; + inputs[0].value = val; + } + return this.blocksCanvas; + } + + if (!this.wasActiveLastFrame) { + return false; + } + + this.wasActiveLastFrame = false; + this.domElement.style.display = 'none'; + return false; +}; + +// Duplicated in functions.js for other elements +UIWindow.prototype.fadeOut = function(jQueryElement) { + if (jQueryElement[0].style.display == 'none') { + return; + } + + jQueryElement[0].style.display = 'inline'; + + jQueryElement.css({ + opacity: 1, + }).animate({ + opacity: 0, + top: (parseInt(jQueryElement.css('top')) + 30) + 'px', + }, 200, function() { + jQueryElement.css({ + display: 'none', + top: (parseInt(jQueryElement.css('top')) - 30) + 'px', + }); + }); + + if (jQueryElement.attr('id') == 'infoWindow2') { + this.fadeOut($('#darkScreenDiv')); + } +}; + +module.exports = UIWindow; + + +},{"./SoundManager.js":4,"./data/Sound.js":6}],6:[function(require,module,exports){ + +const SOUND = Object.freeze({ + NONE: 0, + PAIN: 1, + DEATH: 2, + SWING: 3, + PLACE: 4, + POSITIVE: 5, + NEGATIVE: 6, + CLICK: 7, + CLICK2: 8, + GUN: 9, + SWORD: 10, + BUILD: 11, + BUILDING_DEATH: 12, + READY: 13, + YES: 14, + INGAMECLICK: 15, + BUILDING_FINISHED: 16, + UNDER_ATTACK: 17, + VICTORY: 18, + DEFEAT: 19, + FLAMESTRIKE_LAUNCH: 20, + FLAMESTRIKE_IMPACT: 21, + MAGE_ATTACK: 22, + MAGE_IMPACT: 23, + CATA_HIT: 24, + CATA_LAUNCH: 25, + CATA_IMPACT: 26, + BUILDING_PAIN: 27, + REPAIR: 28, + CATA_DEATH: 29, + GAME_START: 30, + HEAL: 31, + BING: 32, + SWITCH: 33, + ZIP: 34, + ZIP3: 35, + SPELL: 36, + DRAGON_YES: 37, + DRAGON_SPAWN: 38, + DRAGON_DEATH: 39, + FALL: 40, + DRAGON_FIRE: 41, + MINE: 42, + WOLF_YES: 43, + WOLF_READY: 44, + WOLF_DEATH: 45, + WOLF_HIT: 46, + WOLF_PAIN: 47, + LADDER_START: 48, + ROUNDHOUSE: 49, + BIGHIT: 50, + STRONG_HIT: 51, + BEAST_DIE: 52, + BEAST_YES: 53, + BEAST_READY: 54, + FLESH: 55, + FLAK: 56, + SHOCKWAVE: 57, + FIREBALL: 58, + SKELETON_SPAWN: 59, + SKELETON_YES: 60, + SKELETON_PAIN: 61, + BING2: 62, + ARCHIVEMENT: 63, + ARCHIVEMENT2: 64, + ARCHIVEMENT3: 65, + AURA_HEAL: 66, + PLASMA_SHIELD: 67, + WARP: 68, + BIRD_SPAWN: 69, + BIRD_YES: 70, + BIRD_DEATH: 71, + BIRD_SLAM: 72, + BATTLE_FANFARE: 73, + A_WS: 74, + CC: 75, + CHURCH: 76, + DRAGONS_LAIR: 77, + FORGE: 78, + HOUSE: 79, + LAB: 80, + MAGES_GUILD: 81, + RAX: 82, + WW_DEN: 83, + W_DEN: 84, + WORKSHOP: 85, + MILL: 86, + SNAKE_CHARMER: 87, + ARMORY: 88, + AIRSHIP: 89, + ARCHER_READY: 90, + ARCHER: 91, + BALLISTA: 92, + CALTROP: 93, + CATAPULT: 94, + GATLING_GUN: 95, + GYROCRAFT_READY: 96, + MAGE_READY: 97, + MAGE: 98, + PRIEST: 99, + RAIDER: 100, + SOLDIER: 101, + GYROCRAFT_YES: 102, + GYROCRAFT_PAIN: 103, +}); + +module.exports = SOUND; + +},{}],7:[function(require,module,exports){ +(function (Buffer){(function (){ +// TODO: deduplicate this with server when/if we merge server and client repos + +const Compression = (() => { + const zlib = require('minizlib'); + + function compress(str) { + return new zlib.Deflate().end(str).read(); + } + + function decompress(bytes) { + try { + return new zlib.Inflate().end(bytes).read().toString(); + } catch (e) { + return null; + } + } + + function buf2str(buf) { + return buf.toString('binary'); + } + + function str2buf(str) { + return Buffer.from(str, 'binary'); + } + + return { + compress, + decompress, + buf2str, + str2buf, + compressToString: (str) => buf2str(compress(str)), + decompressFromString: (str) => decompress(str2buf(str)), + }; +})(); + +const Initialization = (() => { + const Stage = Object.freeze({ + NO_DEPENDENCY: 0, + UI_GENERATED: 1, + RESOURCES_LOADED: 2, + INITIALIZATION_COMPLETE: 3, + }); + + const callbacks = {}; + let totalResources = 0; + let pendingResources = 0; + let currentStage = Stage.NO_DEPENDENCY; + + function __registerCallback(callback, stage) { + if (!(stage in callbacks)) { + callbacks[stage] = []; + } + + callbacks[stage].push(callback); + } + + function onDocumentReady(callback) { + __registerCallback(callback, Stage.NO_DEPENDENCY); + } + + function onUIGenerated(callback) { + __registerCallback(callback, Stage.UI_GENERATED); + } + + function onResourcesLoaded(callback) { + __registerCallback(callback, Stage.RESOURCES_LOADED); + } + + function onInitializationComplete(callback) { + __registerCallback(callback, Stage.INITIALIZATION_COMPLETE); + } + + async function runCallbacks(stage) { + currentStage = stage; + if (!callbacks[stage]) { + return; + } + await Promise.all(callbacks[stage].map((cb) => cb())); + } + + function addPendingResource() { + ++totalResources; + ++pendingResources; + } + + function loadedPendingResource() { + --pendingResources; + + // display % loaded + const percent = (totalResources - pendingResources) / totalResources; + c.fillRect(WIDTH / 2 - 296, HEIGHT - 146, 592 * percent, 42); + + if (pendingResources == 0) { + // setTimeout used to prevent out of order execution, since + // callbacks from previous stages can trigger pendingResources to + // hit 0 + setTimeout(async () => { + await runCallbacks(Stage.RESOURCES_LOADED); + await runCallbacks(Stage.INITIALIZATION_COMPLETE); + }); + } + } + + function getStage() { + return currentStage; + } + + $(document).ready(() => { + // jQuery cannot handle async callbacks unless we upgrade the version, + // but this causes a tooltip visual bug that is out of the scope of this + // change + // TODO: upgrade the jQuery version + runCallbacks(Stage.NO_DEPENDENCY).then(() => runCallbacks(Stage.UI_GENERATED)); + }); + + return { onDocumentReady, onUIGenerated, onResourcesLoaded, onInitializationComplete, Stage, addPendingResource, loadedPendingResource, getStage }; +})(); + +const AIManager = (() => { + const names = {}; + const customNames = new Set(); + const manifests = {}; + let aiCommit; + + // Register a pending resource for the manifest and commit + Initialization.addPendingResource(); + + async function sendAIToWorker(commit, name, code, isCustom) { + try { + await WorkerClient.call('load-ai', { + commit: commit, + name: name, + code: code, + }); + } catch (e) { + displayInfoMsg(`Error parsing AI file: ${e}`); + return false; + } + + if (isCustom) { + interface_.lastChosenAI.set(name); + displayInfoMsg(`AI has been loaded as "${name}". To play it, start a singleplayer game and select the AI for one or more of the CPU players.`); + + customNames.add(name); + } else { + if (!(commit in names)) { + names[commit] = new Set(); + } + names[commit].add(name); + } + return true; + } + + async function loadManifest(commit) { + if (commit in manifests) { + return manifests[commit]; + } + manifests[commit] = JSON.parse(await httpGet(`https://raw.githubusercontent.com/littlewargame/customai/${commit}/manifest.json`)); + return manifests[commit]; + } + + // @brief Loads the AI from the given commit with the given name. Leaving name + // unspecified will load all AIs, as will "Random AI". + async function loadAI(commit, name = undefined) { + const manifest = await loadManifest(commit); + if (!name || name == 'Random AI') { + // Just load 'em all! + const results = await Promise.all(manifest.map((ai) => loadAI(commit, ai.name))); + return results.every(Boolean); + } + + const ai = manifest.find((ai) => ai.name == name); + if (!ai) { + displayInfoMsg(`Failed to find AI named ${name} in commit ${commit}`); + return false; + } + if (commit in names && names[commit].has(name)) { + return true; + } + let code; + try { + code = await httpGet(`https://raw.githubusercontent.com/littlewargame/customai/${commit}/${ai.src}`); + } catch (e) { + return false; + } + return await sendAIToWorker(commit, name, code, /* isCustom=*/false); + } + + async function init() { + try { + aiCommit = await httpGet('https://sockets.littlewargame.com:8084/ai_commit'); + await WorkerClient.call('set-ai-commit', { aiCommit: aiCommit }); + + const manifest = await loadManifest(aiCommit); + await Promise.all(manifest.map(async (ai) => { + Initialization.addPendingResource(); + const code = await httpGet(`https://raw.githubusercontent.com/littlewargame/customai/${aiCommit}/${ai.src}`); + await sendAIToWorker(aiCommit, ai.name, code, /* isCustom=*/false); + Initialization.loadedPendingResource(); + })); + } catch (e) { + console.log(`Failed to load AIs: ${e}`); + return; + } + + // We got the manifest and commit + Initialization.loadedPendingResource(); + } + + function getAICommit() { + return aiCommit; + } + + function getNames(includeRegularAI, includeCustomAI, commit = undefined) { + if (!commit) { + commit = aiCommit; + } + const regularAIs = includeRegularAI ? [...names[commit]] : []; + const customAIs = includeCustomAI ? [...customNames] : []; + return regularAIs.concat(customAIs); + } + + // Called when a user uploads an AI file in the main thread + async function sendCustomAIToWorker(code) { + const aiNum = customNames.size + 1; + const name = `Custom AI ${aiNum}`; + sendAIToWorker('', name, code, /* isCustom=*/true); + } + + Initialization.onDocumentReady(init); + + return { getAICommit, getNames, loadAI, sendCustomAIToWorker }; +})(); + +const Changelog = (() => { + function Changelog_() { + Initialization.onDocumentReady(() => this.init()); + this.__html = null; + } + + Changelog_.prototype.init = function() { + $.ajax('Changelog.html').then((data) => this.__html = data); + + new HTMLBuilder() + .add(` +
+
+
+
+
+ `) + .addHook(() => $('#changelogDiv').click(function(e) { + if ($(this).is(e.target)) { + fadeOut($('#changelogDiv')); + } + })) + .appendInto(document.body); + }; + + // Inserts a unit or building image into the provided element + // The is expected to have property data-name with the id_string of the graphic object + Changelog_.prototype.__insertUnitImage = function(el) { + const isUnit = $(el).attr('data-type') == 'unit'; + const size = isUnit ? 180 : 80; + const imgKey = isUnit ? 'idle' : 'img'; + + const img = unit_imgs[$(el).attr('data-name')]; + const file = img.file[1]; + + // TODO: this is copy-pasted in many places and should be centralized + let w = img[imgKey].frameWidth; + let h = img[imgKey].h / img._angles; + if (w > h) { + h = size * (h / w); + w = size; + } else { + w = size * (w / h); + h = size; + } + const w2 = file.width * (w / img[imgKey].frameWidth); + const h2 = file.height * (h * img._angles / img[imgKey].h); + const x = img[imgKey].x * (w2 / file.width); + const y = img[imgKey].y * (h2 / file.height); + + $(el).html(` +
+ +
+ `); + }; + + Changelog_.prototype.tryShow = function(showImmediately = false) { + if (!showImmediately) { + return; + } + + const show = () => { + // Wait for the HTML to load if it hasn't loaded + if (!this.__html) { + setTimeout(show, 500); + return; + } + + $('#changelogContents').html(this.__html); + + // Insert all unit images + const self = this; + $('.infoImage').each(function() { + self.__insertUnitImage(this); + }); + + // Add links + $('.changelogLink').each(function() { + const targetID = $(this).attr('data-target'); + $(this).click(() => setTimeout(() => { + document.getElementById(targetID).scrollIntoView({ behavior: 'smooth' }); + $('.jumpTarget').removeClass('highlightedFromJump'); + $(`#${targetID}`).addClass('highlightedFromJump'); + }, 0)); + }); + + fadeIn($('#changelogDiv')); + }; + setTimeout(show, showImmediately ? 0 : 1000); + }; + + return new Changelog_(); +})(); + +// Uncomment when making private and add Initialization +// const UIWindow = require('./UIWindow.js'); +// const assert = require('./Assert.js'); +// const { generateButton } = require('./functions-new.js'); +// const HTMLBuilder = require('./HTMLBuilder.js'); + +const LOCAL_STORAGE_KEY_NAME_HOTKEYS = 'hotkeys'; + +class HotkeySetting { + constructor(name, defaultValue, id = null) { + this.name = name; + this.id = id ?? name; + this.defaultValue = defaultValue; + this.data = {}; + } + + setData(key, value) { + this.data[key] = value; + return this; + } + + getData(key) { + return this.data[key]; + } + + __getObjectKey(prefix) { + return `${prefix}${this.id}`; + } + + __loadFromObject(obj, prefix) { + this.value = obj[this.__getObjectKey(prefix)] ?? this.defaultValue; + if (this.input) { + this.input.val(getKeyName(this.value)); + } + } + + __saveToObject(obj, prefix) { + if (this.value != this.defaultValue) { + obj[this.__getObjectKey(prefix)] = this.value; + } + } + + __reset(prefix) { + this.value = this.defaultValue; + this.__refreshUI(); + } + + __refreshUI() { + assert(this.input && this.text); + this.input.val(getKeyName(this.value)); + this.input.blur(); + this.input.removeClass('hotkeyInputActive'); + this.text.toggleClass('yellowfont', this.value != this.defaultValue); + } + + __generateUI(onChangeCallback) { + const textID = uniqueID(); + const inputID = uniqueID(); + + return new HTMLBuilder() + .add(` +

${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(` +
+ ${this.name} +
+ `) + .addHook(() => $(`#${collapseButtonID}`).click(() => { + if (isCollapsed) { + $(`#${collapsibleDivID}`).removeClass('invis'); + $(`#${collapseButtonID}`).text('-'); + isCollapsed = false; + } else { + $(`#${collapsibleDivID}`).addClass('invis'); + $(`#${collapseButtonID}`).text('+'); + isCollapsed = true; + } + })); + + builder.add(`
`); + if (this.description) { + builder.add(`
${this.description}
`); + } + + Object.values(this.children).forEach((c) => builder.add(c.__generateUI(onChangeCallback, depth + 1))); + builder.add('
'); + + if (isCollapsed) { + builder.addHook(() => $(`#${collapsibleDivID}`).addClass('invis')); + } + + return builder; + } +} + +// To add a custom hotkey, use the function registerHotkeyGroup or modify an existing HotkeyGroup +// To get the hotkey value, use getHotkeyValue from the parent HotkeyGroup, +// and getHotkeyGroup to get the group by name if necessary +// Hotkey values should only be read from within a onHotkeysChanged callback, +// which is guaranteed to be called on page load and whenever hotkeys are changed +const Hotkeys = (() => { + function Hotkeys_() { + Initialization.onDocumentReady(() => this.init()); + this.__hotkeys = new HotkeyGroup('', true); + this.__onChangedListeners = {}; + + // Initialize the localStorage entry to an empty object if it does not exist + if (!localStorage.getItem(LOCAL_STORAGE_KEY_NAME_HOTKEYS)) { + localStorage.setItem(LOCAL_STORAGE_KEY_NAME_HOTKEYS, Compression.compressToString('{}')); + } + } + + Hotkeys_.prototype.registerHotkeyGroup = function(group, insertAtBeginning) { + this.__hotkeys.addChild(group, insertAtBeginning); + }; + + Hotkeys_.prototype.onHotkeysChanged = function(id, callback) { + this.__onChangedListeners[id] = callback; + }; + + Hotkeys_.prototype.removeHotkeysChangedListener = function(id) { + delete this.__onChangedListeners?.[id]; + }; + + // Returns the HotkeyGroup corresponding to the provided depth-first traversal down the tree + // The traversal must be composed of the IDs of each HotkeyGroup, and must be non-empty + // Asserts on failure + Hotkeys_.prototype.getHotkeyGroup = function(ids) { + return this.__hotkeys.__getHotkeyGroup(ids); + }; + + Hotkeys_.prototype.init = function() { + // Load the hotkeys from localStorage and notify all listeners + let obj = {}; + try { + const data = Compression.decompressFromString(localStorage.getItem(LOCAL_STORAGE_KEY_NAME_HOTKEYS)); + if (data) { + obj = JSON.parse(data); + } + } catch (e) {} + this.__hotkeys.__loadFromObject(obj); + for (const id in this.__onChangedListeners) { + this.__onChangedListeners[id](); + } + + this.__initUI(); + }; + + Hotkeys_.prototype.__initUI = function() { + this.window = new UIWindow('customHotkeysWindow', () => true, true, 'Hotkeys', true); + this.window.addScrollableSubDiv('customHotkeysWindowSubdiv'); + + const onChangeCallback = () => { + const obj = {}; + this.__hotkeys.__saveToObject(obj); + localStorage.setItem(LOCAL_STORAGE_KEY_NAME_HOTKEYS, Compression.compressToString(JSON.stringify(obj))); + for (const id in this.__onChangedListeners) { + this.__onChangedListeners[id](); + } + }; + + if ($('#resetHotkeysButton').length == 0) { + generateButton('resetHotkeysButton', null, null, addClickSound(() => { + const ID = uniqueID(); + displayInfoMsg(new HTMLBuilder() + .add('Reset all hotkeys to default?

') + .add(generateButton(null, null, null, addClickSound(() => { + this.__hotkeys.__reset(); + $('#infoWindow').hide(); + onChangeCallback(); + }), 'reset'))); + }), 'Reset').appendInto('#customHotkeysWindow'); + } + + + if ($('#exportHotkeysButton').length == 0) { + generateButton('exportHotkeysButton', null, null, addClickSound(() => { + this.__hotkeys.__exportHotkeys(); + }), 'Export').appendInto('#customHotkeysWindow'); + } + + + if ($('#importHotkeysButton').length == 0) { + generateButton('importHotkeysButton', null, null, addClickSound(() => { + this.__hotkeys.__importHotkeys(this); + }), 'Import').appendInto('#customHotkeysWindow'); + } + + this.__hotkeys.__generateUI(onChangeCallback, 0).insertInto('#customHotkeysWindowSubdiv'); + }; + + Hotkeys_.prototype.showWindow = function() { + fadeIn($(this.window.domElement)); + }; + + return new Hotkeys_(); +})(); + + +// const Hotkeys = new Hotkeys(); + +// module.exports.HotkeyGroup = HotkeyGroup; +// module.exports.HotkeySetting = HotkeySetting; +// module.exports.Hotkeys = Hotkeys_; + +var emotes = /* emotesstart*/ +[ + { + 'name': 'Kappa', + 'img': 'kappa.png', + 'free': true, + 'text': 'Kappa', + 'type': 'emotes', + }, + + { + 'name': 'Apple', + 'img': 'apple.png', + 'free': true, + 'text': 'Apple', + 'type': 'emotes', + }, + + { + 'name': 'GG', + 'img': 'gg.png', + 'free': true, + 'text': 'GG', + 'type': 'emotes', + }, + + { + 'name': 'Nr. 1', + 'img': 'nr1.png', + 'playerLvl': 2, + 'text': '#1', + 'type': 'emotes', + 'dbPos': 1, + 'artNr': 'e0001', + }, + + { + 'name': 'OP', + 'img': 'op.png', + 'playerLvl': 6, + 'text': 'OP', + 'type': 'emotes', + 'dbPos': 2, + 'artNr': 'e0002', + }, + + { + 'name': 'Jbs pls fix', + 'img': 'jbsplsfix.png', + 'price': 0.79, + 'text': 'JbsPlsFix', + 'type': 'emotes', + 'dbPos': 3, + 'artNr': 'e0003', + }, + + { + 'name': 'Doge', + 'img': 'doge.png', + 'gold': 800, + 'text': 'Doge', + 'type': 'emotes', + 'dbPos': 4, + 'artNr': 'e0004', + }, + + { + 'name': 'Nyan Cat', + 'img': 'nyancat.png', + 'gold': 800, + 'text': 'Nyancat', + 'type': 'emotes', + 'dbPos': 5, + 'artNr': 'e0005', + }, + + { + 'name': 'WTF?', + 'img': 'wtf.png', + 'gold': 1000, + 'text': 'WTF', + 'type': 'emotes', + 'dbPos': 6, + 'artNr': 'e0006', + }, + + { + 'name': 'You Dont Say', + 'img': 'youdontsay.png', + 'price': 0.79, + 'text': 'YouDontSay', + 'type': 'emotes', + 'dbPos': 8, + 'artNr': 'e0008', + }, + + { + 'name': 'Fuuuuuuu', + 'img': 'fuu.png', + 'gold': 1500, + 'text': 'FU', + 'type': 'emotes', + 'dbPos': 9, + 'artNr': 'e0009', + }, + + { + 'name': 'Pie', + 'img': 'pie.png', + 'free': true, + 'text': 'pielons', + 'type': 'emotes', + }, + + { + 'name': 'Soldier', + 'img': 'soldier.png', + 'free': true, + 'text': 'Soldier', + 'type': 'emotes', + }, + + { + 'name': 'Skeleton', + 'img': 'skeleton.png', + 'playerLvl': 9, + 'text': 'Skeleton', + 'type': 'emotes', + 'dbPos': 10, + 'artNr': 'e0010', + }, + + { + 'name': 'Priest', + 'img': 'priest.gif', + 'price': 0.79, + 'text': 'Priest', + 'type': 'emotes', + 'dbPos': 11, + 'artNr': 'e0011', + }, + + { + 'name': 'Wolf', + 'img': 'wolf.png', + 'free': true, + 'text': 'Wolf', + 'type': 'emotes', + }, + + { + 'name': 'ItanoCircus', + 'img': 'itano.png', + 'text': 'ItanoCircus', + 'dbPos': 12, + 'price': 0.79, + 'artNr': 'e0012', + 'type': 'emotes', + }, + + { + 'name': 'Dirty Itano Money', + 'img': 'dirtyitanomoney.png', + 'text': 'DirtyItanoMoney', + 'price': 1.09, + 'type': 'emotes', + 'dbPos': 13, + 'artNr': 'e0013', + }, + + { + 'name': 'Worker', + 'img': 'worker.gif', + 'text': 'Worker', + 'playerLvl': 13, + 'artNr': 'e0014', + 'type': 'emotes', + 'dbPos': 14, + }, + + { + 'name': 'Ballista', + 'img': 'ballista.png', + 'text': 'Ballista', + 'gold': 1500, + 'artNr': 'e0015', + 'type': 'emotes', + 'dbPos': 15, + }, + + { + 'name': 'Archer', + 'img': 'archer.gif', + 'text': 'Archer', + 'gold': 1000, + 'artNr': 'e0016', + 'type': 'emotes', + 'dbPos': 16, + }, + + { + 'name': 'Rifleman', + 'img': 'rifleman.gif', + 'text': 'Rifleman', + 'gold': 1500, + 'artNr': 'e0017', + 'type': 'emotes', + 'dbPos': 17, + }, + + { + 'name': 'Catapult', + 'img': 'catapult.png', + 'text': 'Catapult', + 'price': 0.79, + 'artNr': 'e0018', + 'type': 'emotes', + 'dbPos': 18, + }, + + { + 'name': 'Mage', + 'img': 'mage.gif', + 'text': 'Mage', + 'price': 0.89, + 'artNr': 'e0019', + 'type': 'emotes', + 'dbPos': 19, + }, + + { + 'name': 'Airship', + 'img': 'airship.gif', + 'text': 'Airship', + 'playerLvl': 17, + 'artNr': 'e0020', + 'type': 'emotes', + 'dbPos': 20, + }, + + { + 'name': 'Dragon', + 'img': 'dragon.gif', + 'text': 'Dragon', + 'playerLvl': 20, + 'artNr': 'e0021', + 'type': 'emotes', + 'dbPos': 21, + }, + + { + 'name': 'Werewolf', + 'img': 'werewolf.gif', + 'text': 'Werewolf', + 'gold': 2000, + 'artNr': 'e0022', + 'type': 'emotes', + 'dbPos': 22, + }, + + { + 'name': '5th Division', + 'img': 'div5.png', + 'text': 'Div5', + 'div': 1, + 'type': 'emotes', + 'dbPos': 23, + 'artNr': 'e0023', + }, + + { + 'name': '4th Division', + 'img': 'div4.png', + 'text': 'Div4', + 'div': 2, + 'type': 'emotes', + 'dbPos': 24, + 'artNr': 'e0024', + }, + + { + 'name': '3rd Division', + 'img': 'div3.png', + 'text': 'Div3', + 'div': 3, + 'type': 'emotes', + 'dbPos': 25, + 'artNr': 'e0025', + }, + + { + 'name': '2nd Division', + 'img': 'div2.png', + 'text': 'Div2', + 'div': 4, + 'type': 'emotes', + 'dbPos': 26, + 'artNr': 'e0026', + }, + + { + 'name': '1st Division', + 'img': 'div1.png', + 'text': 'Div1', + 'div': 5, + 'type': 'emotes', + 'dbPos': 27, + 'artNr': 'e0027', + }, + + { + 'name': 'Gamma Division', + 'img': 'divgamma.png', + 'text': 'DivGamma', + 'div': 6, + 'type': 'emotes', + 'dbPos': 30, + 'artNr': 'e0030', + }, + + { + 'name': 'Beta Division', + 'img': 'divbeta.png', + 'text': 'DivBeta', + 'div': 7, + 'type': 'emotes', + 'dbPos': 29, + 'artNr': 'e0029', + }, + + { + 'name': 'Alpha Division', + 'img': 'divalpha.png', + 'text': 'DivAlpha', + 'div': 8, + 'type': 'emotes', + 'dbPos': 28, + 'artNr': 'e0028', + }, + + { + 'name': 'O RLY ?', + 'img': 'orly.jpg', + 'text': 'Orly', + 'price': 0.79, + 'type': 'emotes', + 'dbPos': 31, + 'artNr': 'e0031', + }, + + { + 'name': '#REKT', + 'img': 'rekt.png', + 'text': '#REKT', + 'price': 0.79, + 'type': 'emotes', + 'dbPos': 32, + 'artNr': 'e0032', + }, + + { + 'name': 'Star', + 'img': 'premium.png', + 'text': 'Star', + 'type': 'emotes', + 'special': 'requires_premium', + 'requirementText': 'Requires premium account', + 'requirementTitle': 'Get a premium account to unlock this emote', + 'dbPos': 33, + 'artNr': 'e0033', + }, + + { + 'name': 'Not Bad', + 'img': 'not-bad.png', + 'text': 'notbad', + 'playerLvl': 8, + 'type': 'emotes', + 'dbPos': 34, + 'artNr': 'e0034', + }, + + { + 'name': 'XZibit', + 'img': 'xzibit.png', + 'text': 'xzibit', + 'gold': 800, + 'type': 'emotes', + 'dbPos': 35, + 'artNr': 'e0035', + }, + + { + 'name': 'Castle', + 'img': 'castle.png', + 'text': 'Castle', + 'gold': 1500, + 'type': 'emotes', + 'dbPos': 36, + 'artNr': 'e0036', + }, + + { + 'name': 'Barracks', + 'img': 'rax.png', + 'text': 'Barracks', + 'gold': 1100, + 'type': 'emotes', + 'dbPos': 37, + 'artNr': 'e0037', + }, + + { + 'name': 'Tower', + 'img': 'tower.png', + 'text': 'Tower', + 'gold': 1000, + 'type': 'emotes', + 'dbPos': 38, + 'artNr': 'e0038', + }, + + { + 'name': 'House', + 'img': 'house.png', + 'text': 'House', + 'gold': 900, + 'type': 'emotes', + 'dbPos': 39, + 'artNr': 'e0039', + }, + + { + 'name': 'Goldmine', + 'img': 'mine.png', + 'text': 'Goldmine', + 'gold': 1100, + 'type': 'emotes', + 'dbPos': 40, + 'artNr': 'e0040', + }, + + { + 'name': 'Magesguild', + 'img': 'guild.png', + 'text': 'Guild', + 'gold': 1400, + 'type': 'emotes', + 'dbPos': 41, + 'artNr': 'e0041', + }, + + { + 'name': 'Workshop', + 'img': 'workshop.png', + 'text': 'Workshop', + 'gold': 1400, + 'type': 'emotes', + 'dbPos': 42, + 'artNr': 'e0042', + }, + + { + 'name': 'Forge', + 'img': 'forge.png', + 'text': 'Forge', + 'gold': 1200, + 'type': 'emotes', + 'dbPos': 43, + 'artNr': 'e0043', + }, + + { + 'name': 'Fortress', + 'img': 'fort.png', + 'text': 'Fortress', + 'gold': 1600, + 'type': 'emotes', + 'dbPos': 44, + 'artNr': 'e0044', + }, + + { + 'name': 'Dragonslair', + 'img': 'dragonslair.png', + 'text': 'Lair', + 'gold': 1400, + 'type': 'emotes', + 'dbPos': 45, + 'artNr': 'e0045', + }, + + { + 'name': 'Wolvesden', + 'img': 'wolfden.png', + 'text': 'Wolvesden', + 'gold': 1100, + 'type': 'emotes', + 'dbPos': 46, + 'artNr': 'e0046', + }, + + { + 'name': 'Animal Testing Lab', + 'img': 'lab.png', + 'text': 'Animaltestinglab', + 'gold': 1200, + 'type': 'emotes', + 'dbPos': 47, + 'artNr': 'e0047', + }, + + { + 'name': 'Advanced Workshop', + 'img': 'adv-workshop.png', + 'text': 'Advancedworkshop', + 'gold': 1400, + 'type': 'emotes', + 'dbPos': 48, + 'artNr': 'e0048', + }, + + { + 'name': 'Werewolves Den', + 'img': 'wwden.png', + 'text': 'Werewolvesden', + 'gold': 1800, + 'type': 'emotes', + 'dbPos': 49, + 'artNr': 'e0049', + }, + + { + 'name': 'Church', + 'img': 'church.png', + 'text': 'Church', + 'gold': 1400, + 'type': 'emotes', + 'dbPos': 50, + 'artNr': 'e0050', + }, + + { + 'name': 'Basketball', + 'img': 'basketball.png', + 'gold': 1500, + 'text': 'Basketball', + 'type': 'emotes', + 'dbPos': 51, + 'artNr': 'e0051', + }, + + { + 'name': 'Frog', + 'img': 'frog.png', + 'playerLvl': 26, + 'text': 'Frog', + 'type': 'emotes', + 'dbPos': 52, + 'artNr': 'e0052', + }, + + { + 'name': 'Machinegun', + 'img': 'machinegun.png', + 'special': 'requires_premium', + 'requirementText': 'Requires premium account', + 'requirementTitle': 'Get a premium account to unlock this emote', + 'text': 'Machinegun', + 'type': 'emotes', + 'dbPos': 53, + 'artNr': 'e0053', + }, + + { + 'name': 'Cookie', + 'img': 'cookie.png', + 'special': 'requires_premium', + 'requirementText': 'Requires premium account', + 'requirementTitle': 'Get a premium account to unlock this emote', + 'text': 'Cookie', + 'type': 'emotes', + 'dbPos': 54, + 'artNr': 'e0054', + }, + + { + 'name': 'Gatling Gun', + 'img': 'gatlinggun.gif', + 'gold': 1000, + 'text': 'Gatlinggun', + 'type': 'emotes', + 'dbPos': 55, + 'artNr': 'e0055', + }, + + { + 'name': 'Snake', + 'img': 'snake.gif', + 'gold': 1000, + 'text': 'Snake', + 'type': 'emotes', + 'dbPos': 56, + 'artNr': 'e0056', + }, + + { + 'name': 'Raider', + 'img': 'raider.gif', + 'gold': 1000, + 'text': 'Raider', + 'type': 'emotes', + 'dbPos': 57, + 'artNr': 'e0057', + }, + + { + 'name': 'Gyrocraft', + 'img': 'gyro.gif', + 'gold': 1000, + 'text': 'Gyrocraft', + 'type': 'emotes', + 'dbPos': 58, + 'artNr': 'e0058', + }, + + // { + // "name": "pumpkin", + // "img": "pumpkin.png", + // "free": true, + // "text": "pumpkin", + // "type": "emotes" + // }, + + // { + // "name": "spookyhead", + // "img": "pumpkin_soldier.png", + // "free": true, + // "text": "spookyhead", + // "type": "emotes" + // }, + + // { + // "name": "skeleton", + // "img": "skeleton.png", + // "free": true, + // "text": "skeleton", + // "type": "emotes" + // } + +]/* emotesend*/; + +var skins = /* skinsstart*/ +[ + + { + 'name': 'Redhead Worker', + 'img': 'workerS1', + 'unit_id_string': 'worker', + 'dbPos': 1, + 'gold': 2000, + 'artNr': 's0001', + 'type': 'skins', + }, + + { + 'name': 'Blonde Worker', + 'img': 'workerS2', + 'unit_id_string': 'worker', + 'dbPos': 2, + 'gold': 2000, + 'artNr': 's0002', + 'type': 'skins', + }, + + { + 'name': 'Soldier 2', + 'img': 'soldierS1', + 'unit_id_string': 'soldier', + 'dbPos': 3, + 'price': 1.39, + 'artNr': 's0003', + 'type': 'skins', + }, + + { + 'name': 'Viking Archer', + 'img': 'archerS1', + 'unit_id_string': 'archer', + 'dbPos': 4, + 'playerLvl': 14, + 'artNr': 's0004', + 'type': 'skins', + }, + + { + 'name': 'Soldier 3', + 'img': 'soldierS2', + 'unit_id_string': 'soldier', + 'dbPos': 5, + 'price': 1.39, + 'artNr': 's0005', + 'type': 'skins', + }, + + { + 'name': 'British Mage', + 'img': 'mageS1', + 'unit_id_string': 'mage', + 'dbPos': 6, + 'price': 1.19, + 'artNr': 's0006', + 'type': 'skins', + }, + + { + 'name': 'Christmas Worker', + 'img': 'worker_christmas', + 'unit_id_string': 'worker', + // "dbPos": 7, + 'free': true, + 'artNr': 's0007', + 'type': 'skins', + }, + +]/* skinsend*/; + +var dances = /* dancesstart*/ +[ + + { + 'name': 'Worker 01', + 'unit_id_string': 'worker', + 'dbPos': 1, + 'artNr': 'd0001', + 'animName': 'dance1', + 'price': 0.99, + 'type': 'dances', + 'chat_str': '/dance', + 'img': 'worker', + }, + + { + 'name': 'Soldier 01', + 'unit_id_string': 'soldier', + 'dbPos': 2, + 'gold': 1500, + 'artNr': 'd0002', + 'animName': 'dance1', + 'type': 'dances', + 'chat_str': '/dance', + 'img': 'soldier', + }, + + { + 'name': 'Soldier 02', + 'unit_id_string': 'soldier', + 'dbPos': 3, + 'price': 0.99, + 'artNr': 'd0003', + 'animName': 'dance2', + 'type': 'dances', + 'chat_str': '/dance2', + 'img': 'soldier', + }, + +]/* dancesend*/; + +var achivements = /* achivementsstart*/ +[ + + { + 'name': 'The First Step', + 'a_type': 'ladderwins', + 'type': 'achivements', + 'text': 'Win 1 ladder game', + 'count': 1, + 'dbPos': 1, + 'reward': 10, + 'img': 'ladderwins1.png', + }, + + { + 'name': 'Ranked Up!', + 'a_type': 'ladderwins', + 'type': 'achivements', + 'text': 'Win 5 ladder games', + 'count': 5, + 'dbPos': 2, + 'reward': 20, + 'img': 'ladderwins5.png', + }, + + { + 'name': 'Ladder Hiker', + 'a_type': 'ladderwins', + 'type': 'achivements', + 'text': 'Win 10 ladder games', + 'count': 10, + 'dbPos': 3, + 'reward': 35, + 'img': 'ladderwins10.png', + }, + + { + 'name': 'Matchmaking Battler', + 'a_type': 'ladderwins', + 'type': 'achivements', + 'text': 'Win 20 ladder games', + 'count': 20, + 'dbPos': 4, + 'reward': 70, + 'img': 'ladderwins20.png', + }, + + { + 'name': 'The Steep Climb', + 'a_type': 'ladderwins', + 'type': 'achivements', + 'text': 'Win 50 ladder games', + 'count': 50, + 'dbPos': 5, + 'reward': 150, + 'img': 'ladderwins50.png', + }, + + { + 'name': 'Contender', + 'a_type': 'ladderwins', + 'type': 'achivements', + 'text': 'Win 100 ladder games', + 'count': 100, + 'dbPos': 6, + 'reward': 250, + 'img': 'ladderwins100.png', + }, + + { + 'name': 'Matchmaking Maestro', + 'a_type': 'ladderwins', + 'type': 'achivements', + 'text': 'Win 200 ladder games', + 'count': 200, + 'dbPos': 7, + 'reward': 450, + 'img': 'ladderwins200.png', + }, + + { + 'name': 'Ladder Hero', + 'a_type': 'ladderwins', + 'type': 'achivements', + 'text': 'Win 500 ladder games', + 'count': 500, + 'dbPos': 8, + 'reward': 1000, + 'img': 'ladderwins500.png', + }, + + { + 'name': 'Chief Of Ladder', + 'a_type': 'ladderwins', + 'type': 'achivements', + 'text': 'Win 1000 ladder games', + 'count': 1000, + 'dbPos': 9, + 'reward': 1700, + 'img': 'ladderwins1000.png', + }, + + { + 'name': 'Ladder King', + 'a_type': 'ladderwins', + 'type': 'achivements', + 'text': 'Win 2000 ladder games', + 'count': 2000, + 'dbPos': 10, + 'reward': 3000, + 'img': 'ladderwins2000.png', + }, + + { + 'name': 'Ladder Boss', + 'a_type': 'ladderwins', + 'type': 'achivements', + 'text': 'Win 5000 ladder games', + 'count': 5000, + 'dbPos': 11, + 'reward': 5000, + 'img': 'ladderwins5000.png', + }, + + { + 'name': 'Ladder God', + 'a_type': 'ladderwins', + 'type': 'achivements', + 'text': 'Win 10000 ladder games', + 'count': 10000, + 'dbPos': 12, + 'reward': 10000, + 'img': 'ladderwins10000.png', + }, + + // laddergames + + { + 'name': 'Ladder Player', + 'a_type': 'laddergames', + 'type': 'achivements', + 'text': 'Play 5 ladder games', + 'count': 5, + 'dbPos': 13, + 'reward': 25, + 'img': 'laddergames5.png', + }, + + { + 'name': 'Ladder Rookie', + 'a_type': 'laddergames', + 'type': 'achivements', + 'text': 'Play 10 ladder games', + 'count': 10, + 'dbPos': 14, + 'reward': 40, + 'img': 'laddergames10.png', + }, + + { + 'name': 'Better Ladder Player', + 'a_type': 'laddergames', + 'type': 'achivements', + 'text': 'Play 20 ladder games', + 'count': 20, + 'dbPos': 15, + 'reward': 70, + 'img': 'laddergames20.png', + }, + + { + 'name': 'Experienced Matchmaker', + 'a_type': 'laddergames', + 'type': 'achivements', + 'text': 'Play 50 ladder games', + 'count': 50, + 'dbPos': 16, + 'reward': 150, + 'img': 'laddergames50.png', + }, + + { + 'name': 'Ladder Champ', + 'a_type': 'laddergames', + 'type': 'achivements', + 'text': 'Play 100 ladder games', + 'count': 100, + 'dbPos': 17, + 'reward': 250, + 'img': 'laddergames100.png', + }, + + { + 'name': 'Durable Fighter', + 'a_type': 'laddergames', + 'type': 'achivements', + 'text': 'Play 200 ladder games', + 'count': 200, + 'dbPos': 18, + 'reward': 450, + 'img': 'laddergames200.png', + }, + + { + 'name': 'Competitive Spirit', + 'a_type': 'laddergames', + 'type': 'achivements', + 'text': 'Play 500 ladder games', + 'count': 500, + 'dbPos': 19, + 'reward': 1000, + 'img': 'laddergames500.png', + }, + + { + 'name': 'One Grand', + 'a_type': 'laddergames', + 'type': 'achivements', + 'text': 'Play 1000 ladder games', + 'count': 1000, + 'dbPos': 20, + 'reward': 1750, + 'img': 'laddergames1000.png', + }, + + { + 'name': 'Unstoppable', + 'a_type': 'laddergames', + 'type': 'achivements', + 'text': 'Play 2000 ladder games', + 'count': 2000, + 'dbPos': 21, + 'reward': 3000, + 'img': 'laddergames2000.png', + }, + + { + 'name': 'Matchmaking Specialist', + 'a_type': 'laddergames', + 'type': 'achivements', + 'text': 'Play 5000 ladder games', + 'count': 5000, + 'dbPos': 22, + 'reward': 5000, + 'img': 'laddergames5000.png', + }, + + { + 'name': 'Ladder Warrior', + 'a_type': 'laddergames', + 'type': 'achivements', + 'text': 'Play 10000 ladder games', + 'count': 10000, + 'dbPos': 23, + 'reward': 10000, + 'img': 'laddergames10000.png', + }, + + // games + + { + 'name': 'Beginner', + 'a_type': 'games', + 'type': 'achivements', + 'text': 'Play 5 multiplayer games', + 'count': 5, + 'dbPos': 24, + 'reward': 25, + 'img': 'games5.png', + }, + + { + 'name': 'One Step Further', + 'a_type': 'games', + 'type': 'achivements', + 'text': 'Play 10 multiplayer games', + 'count': 10, + 'dbPos': 25, + 'reward': 40, + 'img': 'games10.png', + }, + + { + 'name': 'Fighter', + 'a_type': 'games', + 'type': 'achivements', + 'text': 'Play 20 multiplayer games', + 'count': 20, + 'dbPos': 26, + 'reward': 70, + 'img': 'games20.png', + }, + + { + 'name': 'Soldier', + 'a_type': 'games', + 'type': 'achivements', + 'text': 'Play 50 multiplayer games', + 'count': 50, + 'dbPos': 27, + 'reward': 150, + 'img': 'games50.png', + }, + + { + 'name': 'Experienced Player', + 'a_type': 'games', + 'type': 'achivements', + 'text': 'Play 100 multiplayer games', + 'count': 100, + 'dbPos': 28, + 'reward': 250, + 'img': 'games100.png', + }, + + { + 'name': 'Littlewargame Expert', + 'a_type': 'games', + 'type': 'achivements', + 'text': 'Play 200 multiplayer games', + 'count': 200, + 'dbPos': 29, + 'reward': 450, + 'img': 'games200.png', + }, + + { + 'name': 'Littlewargame Specialist', + 'a_type': 'games', + 'type': 'achivements', + 'text': 'Play 500 multiplayer games', + 'count': 500, + 'dbPos': 30, + 'reward': 1000, + 'img': 'games500.png', + }, + + { + 'name': 'Master Of Littlewargame', + 'a_type': 'games', + 'type': 'achivements', + 'text': 'Play 1000 multiplayer games', + 'count': 1000, + 'dbPos': 31, + 'reward': 1750, + 'img': 'games1000.png', + }, + + { + 'name': 'Living Legend', + 'a_type': 'games', + 'type': 'achivements', + 'text': 'Play 2000 multiplayer games', + 'count': 2000, + 'dbPos': 32, + 'reward': 3000, + 'img': 'games2000.png', + }, + + { + 'name': 'Based God', + 'a_type': 'games', + 'type': 'achivements', + 'text': 'Play 5000 multiplayer games', + 'count': 5000, + 'dbPos': 33, + 'reward': 5000, + 'img': 'games5000.png', + }, + + { + 'name': 'It\'s Over 9000!!!', + 'a_type': 'games', + 'type': 'achivements', + 'text': 'Play over 9000 multiplayer games', + 'count': 9001, + 'dbPos': 34, + 'reward': 10000, + 'img': 'games9001.png', + }, + + { + 'name': 'Holiday Spirit', + 'a_type': 'games', + 'type': 'achivements', + 'text': 'Play a game during the christmas event', + 'expires': 1580564544072, // New year's day + 'count': 1, + 'dbPos': 35, + 'img': 'christmas.png', + }, + +]/* achivementsend*/; + +var goldPacks = /* goldstart*/ +[ + + { + 'name': 'Gold Pack 1000', + 'artNr': 'g0001', + 'type': 'gold', + 'price': 0.99, + 'reward': 1000, + }, + + { + 'name': 'Gold Pack 2000', + 'artNr': 'g0002', + 'type': 'gold', + 'price': 1.89, + 'reward': 2000, + }, + + { + 'name': 'Gold Pack 5000', + 'artNr': 'g0003', + 'type': 'gold', + 'price': 3.99, + 'reward': 5000, + }, + + { + 'name': 'Gold Pack 10000', + 'artNr': 'g0004', + 'type': 'gold', + 'price': 7.49, + 'reward': 10000, + }, + +]/* goldend*/; + +var specials = /* specialsstart*/ +[ + + { + 'name': 'Premium Account', + 'artNr': 'x0001', + 'type': 'special', + 'type_2': 'premium', + 'dbPos': 1, + 'gold': 3500, + 'img': 'premium.png', + }, + + { + 'name': 'Treasure Chest', + 'artNr': 'x0002', + 'type': 'special', + 'type_2': 'treasure', + 'dbPos': 2, + 'gold': 1000, + 'img': 'chest.png', + }, + +]/* specialsend*/; + + +if (typeof exports !== 'undefined') { + exports.emotes = emotes; + exports.skins = skins; + exports.dances = dances; + exports.achivements = achivements; + exports.goldPacks = goldPacks; + exports.specials = specials; +} + +var _emotes2 = /* emotesstart*/ +[ + + { + 'name': 'Teddy', + 'img': 'teddy.png', + 'text': 'Teddy', + 'type': 'emotes', + 'hidd': true, + 'dbPos': 55, + 'artNr': 'e0055', + }, + + { + 'name': 'Minigun', + 'img': 'minigun.png', + 'text': 'Minigun', + 'type': 'emotes', + 'hidd': true, + 'dbPos': 56, + 'artNr': 'e0056', + }, + + { + 'name': 'Cheese', + 'img': 'cheese.png', + 'text': 'Cheese', + 'type': 'emotes', + 'hidd': true, + 'dbPos': 57, + 'artNr': 'e0057', + }, + + { + 'name': 'Violin', + 'img': 'violin.png', + 'text': 'Violin', + 'type': 'emotes', + 'hidd': true, + 'dbPos': 58, + 'artNr': 'e0058', + }, + +]/* emotesend*/; + +if (typeof exports !== 'undefined') { + exports.emotes = _emotes2; +} + + +var browserWindow; +try { + browserWindow = window; +} catch (e) {}; // In worker thread before window load +var KEY = Object.freeze({ + UP: 38, + DOWN: 40, + LEFT: 37, + RIGHT: 39, + A: 65, + B: 66, + C: 67, + D: 68, + E: 69, + F: 70, + G: 71, + H: 72, + I: 73, + J: 74, + K: 75, + L: 76, + M: 77, + N: 78, + O: 79, + P: 80, + Q: 81, + R: 82, + S: 83, + T: 84, + U: 85, + V: 86, + W: 87, + X: 88, + Y: 89, + Z: 90, + SHIFT: 16, + CTRL: 17, + ALT: 18, + NUM1: 49, + NUM2: 50, + NUM3: 51, + NUM4: 52, + NUM5: 53, + NUM6: 54, + NUM7: 55, + NUM8: 56, + NUM9: 57, + NUM0: 48, + NUMPAD0: 96, + NUMPAD1: 97, + NUMPAD2: 98, + NUMPAD3: 99, + NUMPAD4: 100, + NUMPAD5: 101, + NUMPAD6: 102, + NUMPAD7: 103, + NUMPAD8: 104, + NUMPAD9: 105, + CIRCUMFLEX: (browserWindow && browserWindow.chrome) ? 220 : 160, + ENTER: 13, + BACKSPACE: 8, + DELETE: 46, + PAUSE: 19, + F1: 112, + F2: 113, + F3: 114, + F4: 115, + F5: 116, + F6: 117, + F7: 118, + F8: 119, + F9: 120, + F10: 121, + ESC: 27, + TAB: 9, + PLUS: 107, + MINUS: 109, + SPACE: 32, + CAPSLOCK: 20, + BACKTICK: 192, + COMMA: 188, + PERIOD: 190, + OPENBRACKETS: 219, + CLOSEBRACKETS: 221, + SEMICOLON: 186, + QUOTE: 222 +}); + +const Cursors = Object.freeze({ + DEFAULT: 0, + BLANK: 1, + HEAL: 2, + ATTACK: 3, +}); + +const CursorFiles = Object.freeze({ + [Cursors.DEFAULT]: 'imgs/cursor.cur', + [Cursors.BLANK]: 'imgs/blank-pixel.cur', + [Cursors.HEAL]: 'imgs/cursor-heal.cur', + [Cursors.ATTACK]: 'imgs/cursor-attack.cur', +}); + +var CLIFF_HEIGHT = 1.5; +var MIN_MAP_SIZE = 24; +var MAX_MAP_SIZE = 256; +var MAX_CLIFF_LEVEL = 2; +var MAX_PLAYERS = 6; +var BUILDING_QUEUE_LEN = 5; +var BUILDING_START_HP_PERCENTAGE = 0.2; +var MAX_SUPPLY = 150; +var MINE_DIST = 7; +var START_GOLD = 50; +var START_WORKERS = 7; +var TICK_TIME = 50; + +var customImgs = {}; +var possibleAngleCounts = { 'None': 0, '1': 1, '4': 4, '8': 8 }; + +function toInt(i) { + var i2 = parseInt(i); + return isNaN(i2) ? 0 : i2; +} + +function checkField(field, val, unscaled) { + if (field.type == 'selection') { + return val; + } + + if (field.type == 'bool') { + if (val) { + return true; + } + return false; + } + + if (field.type == 'integer') { + val = parseInt(val); + + if (isNaN(val)) { + val = 0; + } + + if (val > field.max_val) { + val = field.max_val; + } else if (val < field.min_val) { + val = field.min_val; + } + + return val; + } + + if (field.type == 'float') { + val = parseFloat(val); + + if (isNaN(val)) { + val = 0; + } + + var max_val = field.max_val; + var min_val = field.min_val; + + if (unscaled && field.displayScale) { + max_val /= field.displayScale; + min_val /= field.displayScale; + } + + if (val > max_val) { + val = max_val; + } else if (val < min_val) { + val = min_val; + } + + return val; + } + + if (field.type == 'string') { + if (!val) { + val = ''; + } + + if (val.length > field.max_len) { + val = val.slice(0, field.max_len); + } else if (val.length < field.min_len) { + for (var i = 0; i < field.min_len - val.length; i++) { + val += '_'; + } + } + + return val; + } +} + +function getRampTypeFromCode(code) { + var ramps = (game.theme && game.theme.ramps) ? game.theme.ramps : ramps; + + for (var i = 0; i < ramps.length; i++) { + if (ramps[i].code == code) { + return ramps[i]; + } + } + + return ramps[0]; +} + +function getThemeByName(name) { + for (var i = 0; i < mapThemes.length; i++) { + if (mapThemes[i].name == name) { + return mapThemes[i]; + } + } +} + +// TODO: move all data structure extensions / definitions elsewhere +Array.prototype.contains = function(value) { + for (var i = 0; i < this.length; i++) { + if (this[i] == value) { + return true; + } + } + return false; +}; + +Array.prototype.erease = function(element) { + for (var i = 0; i < this.length; i++) { + if (this[i] == element) { + this.splice(i, 1); + return true; + } + } + return false; +}; + +Array.prototype.remove = function(predicate) { + for (let i = 0; i < this.length; i++) { + if (predicate(this[i])) { + this.splice(i, 1); + return true; + } + } + return false; +}; + +Array.prototype.last = function() { + if (this.length == 0) { + return undefined; + } + return this[this.length - 1]; +}; + +Array.prototype.count = function(predicate) { + let n = 0; + for (let i = 0; i < this.length; i++) { + if (predicate(this[i], i)) { + n++; + } + } + return n; +}; + +const tryGet = function(obj, key, defaultValue = undefined) { + if (!(key in obj) && defaultValue) { + obj[key] = defaultValue; + } + return obj[key]; +}; + +String.prototype.toUnitType = function() { + var types_ = game ? game.unitTypes.concat(game.buildingTypes, tileTypes, cliffs, cliffs_winter, egypt_cliffs, grave_cliffs, ramp_tiles, ramp_tiles_egypt, ramp_tiles_grave, game.upgrades) : basicUnitTypes.concat(basicBuildingTypes, tileTypes, cliffs, cliffs_winter, ramp_tiles, basicUpgrades); + + for (var i = 0; i < types_.length; i++) { + if (this == types_[i].name) { + return types_[i]; + } + } + + return null; +}; + +function Multiset(arr) { + this.data = {}; + if (arr) { + arr.forEach((i) => this.add(i)); + } +} + +Multiset.prototype.count = function(i) { + return this.data[i] || 0; +}; + +Multiset.prototype.add = function(i) { + this.data[i] = this.count(i) + 1; +}; + +Multiset.prototype.forEach = function(callback) { + for (let i in this.data) { + callback(i); + } +}; + +var COMMAND = Object.freeze({ + MAKEUNIT: 1, + UNIVERSAL: 2, + MAKEBUILDING: 3, + IDLE: 7, + HOLDPOSITION: 8, + ATTACK: 9, + CANCEL: 10, + MOVE: 11, + MOVETO: 12, + MINE: 13, + REPAIR: 14, + AMOVE: 15, + SWITCH_CC: 22, + UPGRADE: 23, + BUILDING_UPGRADE: 25, + LOAD: 26, + UNLOAD: 27, + UNLOAD2: 28, + ATTACK_GROUND: 29, + TELEPORT: 30, + DAMAGING_PROJECTILE: 31, + DANCE: 32, +}); + +var EDITOR_COMMANDS = Object.freeze({ + MAKEUNIT: 1, + MAKEBUILDING: 3, + IDLE: 7, + HOLDPOSITION: 8, + ATTACK: 9, + CANCEL: 10, + MOVE: 11, + REPAIR: 14, + UNIVERSAL: 2, + SWITCH_CC: 22, + UPGRADE: 23, + BUILDING_UPGRADE: 25, + ATTACK_GROUND: 29, + TELEPORT: 30, +}); + +var ability_type_fields = {}; + +ability_type_fields[COMMAND.MAKEUNIT] = +[ + 'name', + 'type', + 'unitType', + 'hotkey', + 'commandCard', + 'interfacePosX', + 'interfacePosY', + 'image', + 'description', + 'requirementType', + 'requirementLevel', + 'requirementText', + 'manaCost', + 'hasAutocast', + 'autocastDefault', + 'requiredLevels', + 'learnCommandCard', + 'learnInterfacePosX', + 'learnInterfacePosY', + 'learnHotkey', +]; + +ability_type_fields[COMMAND.MAKEBUILDING] = +[ + 'name', + 'type', + 'unitType', + 'hotkey', + 'commandCard', + 'interfacePosX', + 'interfacePosY', + 'image', + 'description', + 'requirementType', + 'requirementLevel', + 'requirementText', + 'manaCost', + 'range', + 'requiredLevels', + 'learnCommandCard', + 'learnInterfacePosX', + 'learnInterfacePosY', + 'learnHotkey', +]; + +ability_type_fields[COMMAND.IDLE] = +[ + 'name', + 'type', + 'hotkey', + 'commandCard', + 'interfacePosX', + 'interfacePosY', + 'image', + 'description', + 'requirementType', + 'requirementLevel', + 'requirementText', + 'manaCost', +]; + +ability_type_fields[COMMAND.REPAIR] = +[ + 'name', + 'type', + 'hotkey', + 'commandCard', + 'interfacePosX', + 'interfacePosY', + 'image', + 'description', + 'targetRequirements1', + 'targetRequirements2', + 'targetRequirements3', + 'requirementType', + 'requirementLevel', + 'requirementText', + 'manaCost', + 'range', + 'hasAutocast', + 'autocastConditions', + 'cursor', +]; + +ability_type_fields[COMMAND.HOLDPOSITION] = +[ + 'name', + 'type', + 'hotkey', + 'commandCard', + 'interfacePosX', + 'interfacePosY', + 'image', + 'description', + 'requirementType', + 'requirementLevel', + 'requirementText', + 'manaCost', +]; + +ability_type_fields[COMMAND.ATTACK] = +[ + 'name', + 'type', + 'hotkey', + 'commandCard', + 'interfacePosX', + 'interfacePosY', + 'image', + 'description', + 'requirementType', + 'requirementLevel', + 'requirementText', + 'manaCost', + 'cursor', +]; + +ability_type_fields[COMMAND.CANCEL] = +[ + 'name', + 'type', + 'hotkey', + 'commandCard', + 'interfacePosX', + 'interfacePosY', + 'image', + 'description', + 'requirementType', + 'requirementLevel', + 'requirementText', +]; + +ability_type_fields[COMMAND.MOVE] = +[ + 'name', + 'type', + 'hotkey', + 'commandCard', + 'interfacePosX', + 'interfacePosY', + 'image', + 'description', + 'requirementType', + 'requirementLevel', + 'requirementText', +]; + +ability_type_fields[COMMAND.TELEPORT] = +[ + 'name', + 'type', + 'hotkey', + 'commandCard', + 'interfacePosX', + 'interfacePosY', + 'image', + 'attackEffect', + 'description', + 'requirementType', + 'requirementLevel', + 'requirementText', + 'launchSound', + 'manaCost', + 'goldCost', + 'effectScale', + 'castingDelay', + 'cooldown', + 'cooldown2', + 'range', + 'minRange', + 'animationName', + 'requiresVision', + 'requiredLevels', + 'modifiersSelf', + 'learnInterfacePosX', + 'learnInterfacePosY', + 'learnCommandCard', + 'learnHotkey', + 'attackEffectInit', + 'cursor', +]; + +ability_type_fields[COMMAND.UNIVERSAL] = +[ + 'name', + 'type', + 'hotkey', + 'targetIsPoint', + 'targetIsUnit', + 'isInstant', + 'isChanneled', + 'playLaunchSoundOnce', + 'useAoeCursor', + 'commandCard', + 'interfacePosX', + 'interfacePosY', + 'image', + 'attackEffect', + 'description', + 'requirementType', + 'requirementLevel', + 'requirementText', + 'targetRequirements1', + 'targetRequirements2', + 'targetRequirements3', + 'launchSound', + 'manaCost', + 'goldCost', + 'aoeRadius', + 'damage', + 'projectileDamage', + 'projectileAoeRadius', + 'maximizeRangeWhenCasting', + 'hitsFriendly', + 'hitsEnemy', + 'hitsSelf', + 'targetFilters', + 'targetFiltersExclude', + 'effectScale', + 'projectileSpeed', + 'duration', + 'castingDelay', + 'cooldown', + 'cooldown2', + 'range', + 'minRange', + 'bounceDistMin', + 'bounceDistMax', + 'bouncePower', + 'animationName', + 'causesFlameDeath', + 'modifiers', + 'summonedUnits', + 'summonsUseWaypoint', + 'summonsWaypointAMove', + 'ignoreSupplyCheck', + 'requiresVision', + 'requiredLevels', + 'modifiersSelf', + 'learnInterfacePosX', + 'learnInterfacePosY', + 'learnCommandCard', + 'learnHotkey', + 'hasAutocast', + 'autocastDefault', + 'autocastConditions', + 'attackEffectInit', + 'cursor', +]; + +ability_type_fields[COMMAND.SWITCH_CC] = +[ + 'name', + 'type', + 'hotkey', + 'commandCard', + 'interfacePosX', + 'interfacePosY', + 'image', + 'description', + 'requirementType', + 'requirementLevel', + 'requirementText', + 'targetCC', +]; + +ability_type_fields[COMMAND.UPGRADE] = +[ + 'name', + 'type', + 'hotkey', + 'commandCard', + 'interfacePosX', + 'interfacePosY', + 'image', + 'description', + 'requirementType', + 'requirementLevel', + 'requirementText', + 'upgrade', + 'manaCost', + 'hasAutocast', + 'autocastDefault', +]; + +ability_type_fields[COMMAND.BUILDING_UPGRADE] = +[ + 'name', + 'type', + 'hotkey', + 'commandCard', + 'interfacePosX', + 'interfacePosY', + 'image', + 'description', + 'requirementType', + 'requirementLevel', + 'requirementText', + 'improvedBuilding', + 'manaCost', +]; + +ability_type_fields[COMMAND.ATTACK_GROUND] = +[ + 'name', + 'type', + 'hotkey', + 'useAoeCursor', + 'commandCard', + 'interfacePosX', + 'interfacePosY', + 'image', + 'description', + 'requirementType', + 'requirementLevel', + 'requirementText', + 'manaCost', + 'cursor', +]; + +var commandTypeDescriptions = { + MAKEUNIT: 'This command will produce / train a unit. Set a unit type to determine which unit will be produced.', + MAKEBUILDING: 'This command will construct a building. Set a unit type to determine which building will be constructed', + UNIVERSAL: 'This is a universal type that you want to pick for most spells (damagespells, heal, ...). It uses most of the fields like dmg, aoeRadius, range, ...', + SWITCH_CC: 'This command will switch the command menu. This is used for example to go to the submenu for making buildings. Set the targetCC field to determine which command menu to go.', + UPGRADE: 'This command will research an upgrade.', + BUILDING_UPGRADE: 'This command will upgrade this building / unit to another one set the improvedBuilding field to determine which building / unit it will be morphed into.', + ATTACK_GROUND: 'This command will order the unit to attack the target position.', +}; + + +var graphic_type_fields = {}; + + +var BUILDING_STATE = Object.freeze({ + NORMAL: 1, + UNDER_CONSTRUCTION: 2, + BUSY: 3, + BUSY_DAMAGED: 4, + DAMAGED: 5, + UPGRADING: 6, + UPGRADING_DAMAGED: 7, + EMPTY: 8, + DEAD: 0, +}); + +var CONTROLLER = Object.freeze({ + HUMAN: 0, + COMPUTER: 1, + NONE: 2, + REMOTE: 3, + SPECTATOR: 4, +}); + + +var checkAngles = [[1, 0], [3, 1], [1, 1], [1, 3], [0, 1], [-1, 3], [-1, 1], [-3, 1], [-1, 0], [-3, -1], [-1, -1], [-1, -3], [0, -1], [1, -3], [1, -1], [3, -1]]; +var angleOffsets = [[0.5, 0.86], [0.86, 0.5], [1, 0], [0.86, -0.5], [0.5, -0.86], [0, -1], [-0.5, -0.86], [-0.86, -0.5], [-1, 0], [-0.86, 0.5], [-0.5, 0.86], [0, 1]]; + + +var targetRequirements = { + isHuman: { + func: (target) => target.type.isHuman, + text: 'Target needs to be a human unit.', + isTargetRequirement: true, + funcName: 'isHuman', + }, + + isBiological: { + func: (target) => target.type.isBiological, + text: 'Target needs to be a biological unit.', + isTargetRequirement: true, + funcName: 'isBiological', + }, + + isMechanical: { + func: (target) => target.type.isMechanical, + text: 'Target needs to be a mechanical unit.', + isTargetRequirement: true, + funcName: 'isMechanical', + }, + + isUndead: { + func: (target) => target.type.isUndead, + text: 'Target needs to be an undead unit.', + isTargetRequirement: true, + funcName: 'isUndead', + }, + + isBuilding: { + func: (target) => target.type.isBuilding, + text: 'Target needs to be a building.', + isTargetRequirement: true, + funcName: 'isBuilding', + }, + + isUnit: { + func: (target) => target.type.isUnit, + text: 'Target needs to be a unit.', + isTargetRequirement: true, + funcName: 'isUnit', + }, + + isFlying: { + func: (target) => target.type.flying, + text: 'Target needs to be flying.', + isTargetRequirement: true, + funcName: 'isFlying', + }, + + isGround: { + func: (target) => !target.type.flying, + text: 'Target needs to be on the ground.', + isTargetRequirement: true, + funcName: 'isGround', + }, + + notFullHp: { + func: (target) => target.hp < target.type.hp, + text: 'Target has full life.', + isTargetRequirement: true, + funcName: 'notFullHp', + }, +}; + + +var targetFilters1 = { + + flying: 'flying', + isBiological: 'isBiological', + isMechanical: 'isMechanical', + isUndead: 'isUndead', + uniqueAndHeroic: 'uniqueAndHeroic', + isBeast: 'isBeast', + isHuman: 'isHuman', + isInvisible: 'isInvisible', + +}; + +const IS_WORKER = false; + +// canvas +var canvas = document.getElementById('canvas'); +var c = canvas.getContext('2d'); +var originalC = c; + +// dummy canvas +var canvas2 = document.createElement('canvas'); +var c2 = canvas2.getContext('2d'); + +// LocalConfig +const LocalConfig = require('./LocalConfig.js'); + +// Assert +const assert = require('./Assert.js'); + +// HTMLBuilder +const { HTMLBuilder } = require('./HTMLBuilder.js'); +const { uniqueID } = require('./HTMLBuilder.js'); + +// UIWindow +const UIWindow = require('./UIWindow.js'); + +// SoundManager +soundManager = require('./SoundManager.js'); + +const scaleFactorConfig = LocalConfig.registerValue('scale_factor', 3); +var SCALE_FACTOR; + +function setScaleFactor(value) { + value = Math.max(2, Math.min(10, value)); + scaleFactorConfig.set(value); + SCALE_FACTOR = value; +} +setScaleFactor(scaleFactorConfig.get()); + +var WIDTH = window.innerWidth; +var HEIGHT = window.innerHeight; +var SCROLL_RANGE = 10; // in which distance to the border the cursor starts scrolling +var FIELD_SIZE = 16 * SCALE_FACTOR; +var INTERFACE_HEIGHT = 176; +var MINIMAP_WIDTH = 192; +var MINIMAP_HEIGHT = 192; +var Y_OFFSET = 1 / 8; +var CLICK_TOLERANCE = 0.25; // click tolerance when selecting a unit, in fields +var TICKS_DELAY = 6; // delay in multiplayer game. Too low delay can lead to slow running game if connection is bad; is dynamically changed while playing +var MAX_DELAY = 20; +var MIN_DELAY = 2; +var PLAYING_PLAYER = null; +var DEAD_MAP_SPACE = 36; +var DEFAULT_VOLUME = 0.20; // default volume percentage for game sounds, music and littlechatgame + +// var SERVER_ADRESS = "ws://jbs.hercules.uberspace.de:64978"; +// var SERVER_ADRESS = "ws://108.61.78.37:8083"; +// var SERVER_ADRESS = "wss://us1.littlewargame.com:8083"; +// var SERVER_ADRESS = "wss://us1-dev.littlewargame.com:8083"; +// var SERVER_ADRESS = 'ws://localhost:8083'; + +// The server and special event placeholders will be filled out by the gulp build +var SERVER_ADRESS = 'wss://sockets.littlewargame.com:9000'; +const SPECIAL_EVENT = '@@special_event'; +const MICROTRANSACTIONSENABLED = false; + +var IS_LOGIC = false; +var COMMAND_BUTTON_SIZE = 72; +var INTERFACE_UNIT_IMG_SIZE = 100; // px +const GAME_VERSION = '5.0.0'; + +// Global Variables +var timestamp = performance.now(); +var timeDiff = 0; +var gameTimeDiff = 0; +var timeOfLastUpdate = 0; +var show_fps = false; +var frameTimes = []; // to calculate fps from weighted average of last three frame times +var mspfCap = 25; // "ms per frame cap" = inverse fps cap: 25ms per frame = 40 frames per second (1000/x), varies adaptively between 25 and 40 for more consistent performance under high load +var show_unit_details = true; +var fps = 0; +var ticksCounter = 0; +var percentageOfCurrentTickPassed = 0; +var game_paused = false; +var tickDiff = 0; +var lastFramesTick = 0; +var fileInput = ''; +var replayFile = ''; +var replaySpeedIndex = 1; +var tickTimes = [0, 0, 0, 0, 0]; +var clan = null; +var mapData = null; +var lastSentTick = -1; +var storedAchievements = []; +var npf = 'This anonymous survey gives you some free gold.'; +var lcg = {}; +var lcg_interval = null; +var path2Print = null; +var printPathUntil = 0; +var srvState = ''; +var tileImgs = {}; + +// network variables +var network_game = false; +var ladder_game = false; +var incomingOrders = {}; +var outgoingOrders = {}; +var playerLefts = {}; +var incomingCameraUpdates = {}; +var outgoingCameraUpdate = {}; +var timeOfLastPingSent = 0; + +// strings +const goldDescription = 'Use gold to unlock new emotes, skins or dances. You can get gold from achievements or levelling up.'; + +function getKeyName(key) { + let keyName; + for (let i in KEY) { + if (KEY[i] == key) { + keyName = i; + break; + } + } + + if (keyName.indexOf('NUM') == 0) { + return keyName.substring(3); + } else if (keyName == 'CIRCUMFLEX') { + return '^'; + } else if (keyName == 'BACKTICK') { + return '`'; + } else { + return keyName; + } +} + +// League Names +var leagueNames = [ + '5th Division', + '4th Division', + '3rd Division', + '2nd Division', + '1st Division', + 'Gamma Division', + 'Beta Division', + 'Alpha Division', +]; + +var imgs = { + + dust1: { x: 155, y: 0, w: 7, h: 7 }, + dust2: { x: 155, y: 7, w: 7, h: 7 }, + + particle: { x: 155, y: 14, w: 1, h: 1 }, + hammer: { x: 278, y: 0, w: 22, h: 22 }, + research: { x: 145, y: 44, w: 17, h: 17 }, + attentionmark: { x: 32, y: 0, w: 19, h: 18 }, + attentionmarkYellow: { x: 51, y: 0, w: 19, h: 18 }, + underAttack: { x: 201, y: 102, w: 18, h: 20 }, + stop: { x: 3, y: 141, w: 26, h: 30 }, + holdposition: { x: 109, y: 92, w: 32, h: 32 }, + attack: { x: 2, y: 3, w: 27, h: 27 }, + cancel: { x: 139, y: 0, w: 16, h: 16 }, + flamestrike: { x: 101, y: 69, w: 32, h: 23 }, + heal: { x: 73, y: 80, w: 18, h: 19 }, + button: { x: 70, y: 0, w: 69, h: 69 }, + button2: { x: 0, y: 69, w: 69, h: 69 }, + soot: { x: 141, y: 80, w: 60, h: 60 }, + attackUpg: { x: 31, y: 146, w: 24, h: 24 }, + armorUpg: { x: 55, y: 146, w: 24, h: 24 }, + interfaceLeft: { x: 0, y: 202, w: 260, h: 29 }, + interfaceRight: { x: 0, y: 172, w: 260, h: 29 }, + interfaceMapBorder: { x: 0, y: 321, w: 108, h: 109 }, + interfaceButtonDiv: { x: 0, y: 232, w: 205, h: 88 }, + interfaceUnitInfo: { x: 0, y: 431, w: 293, h: 76 }, + dragonAttUpg: { x: 103, y: 146, w: 24, h: 24 }, + dragonDefUpg: { x: 128, y: 146, w: 24, h: 24 }, + towerUpg: { x: 269, y: 35, w: 32, h: 32 }, + unload: { x: 268, y: 108, w: 32, h: 32 }, + load: { x: 268, y: 71, w: 32, h: 32 }, + repair: { x: 139, y: 16, w: 23, h: 23 }, + groundAttack: { x: 229, y: 32, w: 32, h: 32 }, + smash: { x: 184, y: 153, w: 16, h: 16 }, + speedUpg: { x: 79, y: 146, w: 24, h: 24 }, + beastSpeedUpg: { x: 155, y: 146, w: 24, h: 24 }, + mechAttUpg: { x: 207, y: 146, w: 24, h: 24 }, + mechDefUpg: { x: 232, y: 146, w: 24, h: 24 }, + mechSpeedUpg: { x: 257, y: 146, w: 24, h: 24 }, + flakUpg: { x: 238, y: 121, w: 24, h: 24 }, + lightGround: { x: 187, y: 62, w: 29, h: 26 }, + soot2: { x: 221, y: 70, w: 34, h: 32 }, + shockwave: { x: 209, y: 234, w: 24, h: 24 }, + gold: { x: 261, y: 172, w: 20, h: 19 }, + supply: { x: 284, y: 172, w: 11, h: 19 }, + beastRangeUpg: { x: 210, y: 260, w: 23, h: 21 }, + rangeUpg: { x: 210, y: 284, w: 23, h: 21 }, + mechRangeUpg: { x: 237, y: 235, w: 25, h: 22 }, + invisibility: { x: 73, y: 123, w: 19, h: 21 }, + telescope: { x: 237, y: 259, w: 17, h: 21 }, + eye: { x: 266, y: 195, w: 11, h: 9 }, + slowfield: { x: 282, y: 195, w: 18, h: 12 }, + leaf: { x: 280, y: 208, w: 20, h: 20 }, + cake: { x: 279, y: 233, w: 21, h: 22 }, + book: { x: 259, y: 259, w: 17, h: 21 }, + tree: { x: 280, y: 258, w: 20, h: 22 }, + back: { x: 236, y: 283, w: 19, h: 22 }, + bow: { x: 258, y: 283, w: 20, h: 20 }, + spell_1: { x: 282, y: 284, w: 16, h: 20 }, + spell_2: { x: 110, y: 382, w: 22, h: 22 }, + gun: { x: 135, y: 382, w: 21, h: 24 }, + teleport: { x: 159, y: 382, w: 23, h: 22 }, + whitePixel: { x: 74, y: 112, w: 1, h: 1 }, + + fire1: { x: 69, y: 73, w: 3, h: 3 }, + fire2: { x: 73, y: 72, w: 4, h: 4 }, + fire3: { x: 78, y: 71, w: 5, h: 5 }, + fire4: { x: 84, y: 70, w: 7, h: 7 }, + fire5: { x: 91, y: 69, w: 9, h: 9 }, + + heal1: { x: 74, y: 103, w: 1, h: 1 }, + heal2: { x: 76, y: 103, w: 1, h: 1 }, + heal3: { x: 78, y: 103, w: 1, h: 1 }, + heal4: { x: 80, y: 103, w: 3, h: 3 }, + + mageAttack1: { x: 133, y: 70, w: 2, h: 2 }, + mageAttack2: { x: 137, y: 70, w: 2, h: 2 }, + mageAttack3: { x: 141, y: 70, w: 4, h: 4 }, + mageAttack4: { x: 146, y: 70, w: 4, h: 4 }, + + flyingRock1: { x: 0, y: 31, w: 9, h: 8 }, + flyingRock2: { x: 9, y: 31, w: 9, h: 9 }, + flyingRock3: { x: 18, y: 31, w: 8, h: 8 }, + flyingRock4: { x: 26, y: 31, w: 8, h: 9 }, + flyingRock5: { x: 34, y: 31, w: 8, h: 8 }, + flyingRock6: { x: 42, y: 31, w: 9, h: 9 }, + flyingRock7: { x: 51, y: 31, w: 9, h: 8 }, + flyingRock8: { x: 60, y: 31, w: 9, h: 9 }, + +}; + +var arrowImg = { + s: [ + { x: 74, y: 1065, w: 10, h: 12 }, + { x: 74, y: 1089, w: 10, h: 19 }, + ], + + w: [ + { x: 87, y: 1063, w: 25, h: 15 }, + { x: 86, y: 1092, w: 26, h: 9 }, + ], + + e: [ + { x: 116, y: 1063, w: 25, h: 15 }, + { x: 116, y: 1092, w: 26, h: 9 }, + ], + + n: [ + { x: 145, y: 1055, w: 8, h: 26 }, + { x: 145, y: 1086, w: 8, h: 20 }, + ], + + sw: [ + { x: 159, y: 1065, w: 25, h: 10 }, + { x: 160, y: 1086, w: 21, h: 20 }, + ], + + se: [ + { x: 190, y: 1065, w: 25, h: 10 }, + { x: 194, y: 1086, w: 21, h: 20 }, + ], + + nw: [ + { x: 218, y: 1061, w: 16, h: 21 }, + { x: 217, y: 1091, w: 21, h: 20 }, + ], + + ne: [ + { x: 240, y: 2061, w: 16, h: 21 }, + { x: 239, y: 1091, w: 21, h: 20 }, + ], +}; + +var lists = { + types: {}, + imgs: { none: null }, + upgrades: { none: null }, + unitTypes: { none: null }, + buildingTypes: { none: null }, + buildingsUpgrades: {}, + modifiers: {}, + commands: {}, +}; + +TileType.prototype = new MapObjectType(); +function TileType(data) { + _.extend(this, data); + + /* + // if img is an array (= animated tile), load all the images, else only load one img + if(Object.prototype.toString.call(data.img) === '[object Array]') + { + for(var i = 0; i < data.img.length; i++) + this.img[i] = loadImage(data.img[i]); + } + else + this.img = loadImage(data.img); + */ + + this.minimapColor = '#ffffff'; // will be calculated correctly, when img is loaded + this.isTile = true; + + this.circleOffset = 0.16; + this.circleSize = this.sizeX * 0.75; +}; + +TileType.prototype.replaceReferences = function() { + if (this.img && lists.imgs[this.name]) { + this.img = lists.imgs[this.name]; + } +}; + +// loads an image and returns the image object +function loadImage(imgFile) { + try { + Initialization.addPendingResource(); + + var img = new Image(); + + img.onload = () => { + if (img && (!img.complete || !(img.width > 0))) { + img.src = ''; + img.src = img.srcCpy; + return; + } + + Initialization.loadedPendingResource(); + }; + img.src = imgFile; + img.srcCpy = imgFile; // in case we must load img again, store the path + + return img; + } catch (error) { + console.error(`Failed to load image from ${imgFile}`); + } +}; + +function initCustomImgsObj() { + for (key in customImgs) { + delete customImgs[key]; + } + + for (key in unit_imgs) { + customImgs[key] = unit_imgs[key].file; + } + + customImgs['buildingSheet'] = buildingSheet; + customImgs['tileSheet'] = tileSheet; + customImgs['miscSheet'] = miscSheet; +}; + +// gets called when all the images are loaded, creates the color specific unit images from the original ones +function createColorTransformedUnitImages() { + var animNames = ['idle', 'walk', 'walkGold', 'attack', 'die', 'special1', 'dance1', 'dance2']; + + // units + _.each(unit_imgs, function(img) { + var img_ = img.idle ? img.idle : img.walk; + + img.file = [img.file].concat(ImageTransformer.replaceColors(img.file, searchColors, playerColors)); + img.file = img.file.concat(ImageTransformer.getGreyScaledImage(img.file[0])); + + // creating non existing images by linking them to walk img + for (var i = 0; i < animNames.length; i++) { + if (!img[animNames[i]]) { + img[animNames[i]] = img.walk; + } + } + }); + + buildingSheet = [buildingSheet].concat(ImageTransformer.replaceColors(buildingSheet, searchColors, playerColors)); + buildingSheet = buildingSheet.concat(ImageTransformer.getGreyScaledImage(buildingSheet[0])); + + miscSheet = [miscSheet].concat(ImageTransformer.replaceColors(miscSheet, searchColors, playerColors)); + miscSheet = miscSheet.concat(ImageTransformer.getGreyScaledImage(miscSheet[0])); + + tileSheet = [tileSheet].concat(ImageTransformer.replaceColors(tileSheet, searchColors, playerColors)); + tileSheet = tileSheet.concat(ImageTransformer.getGreyScaledImage(tileSheet[0])); +}; + +Initialization.onResourcesLoaded(() => { + // load replace colors + var len = searchColors.length; + var colorsImg = ImageTransformer.getImgFromSheet(tileSheet, { x: 196, y: 0, w: len, h: 1 }); + var colorsImgData = colorsImg.getContext('2d').getImageData(0, 0, colorsImg.width, colorsImg.height).data; + for (var i = 0; i < len; i++) { + searchColors[i] = [colorsImgData[i * 4], colorsImgData[i * 4 + 1], colorsImgData[i * 4 + 2]]; + } + + // create images for units for the other players by transforming colors from original image + createColorTransformedUnitImages(); + + // load tile imgs from spritesheet + var tiles = tileTypes.concat(cliffs, cliffs_winter, egypt_cliffs, grave_cliffs, ramp_tiles, ramp_tiles_egypt, ramp_tiles_grave); + for (var i = 0; i < tiles.length; i++) { + tiles[i].img.frameWidth = tiles[i].img.w; + tiles[i].img = { img: tiles[i].img, file: tileSheet, name: tiles[i].name }; + + tiles[i].minimapColor = tiles[i].minimap_color ? tiles[i].minimap_color : ImageTransformer.getAverageColor(tiles[i].img); + + if (tiles[i].imgEditor) { + tiles[i].img.imgEditor = tiles[i].imgEditor; + } + + tileImgs[tiles[i].name] = tiles[i].img; + } + + for (var i = 0; i < tileTypes.length; i++) { + building_imgs[tileTypes[i].name] = tileTypes[i].img; + tileTypes[i].img = tileTypes[i].name; + } + + for (key in building_imgs) { + building_imgs[key].name = key; + } + + for (key in imgs) { + imgs[key].frameWidth = 0; + imgs[key] = { + img: imgs[key], + name: key, + id_string: key, + file: miscSheet, + }; + } + + initCustomImgsObj(); + + $('#loadingSoldier').remove(); + // Login after this? +}); + + +var tileSheet = loadImage('imgs/tileSheet.png'); +var miscSheet = loadImage('imgs/miscSheet.png'); +var buildingSheet = (SPECIAL_EVENT == 'christmas') ? loadImage('imgs/buildingSheet_christmas.png') : loadImage('imgs/buildingSheet_new.png'); + +var unit_imgs = { + 'soldier': { + file: loadImage('imgs/units/soldier.png'), + _angles: 8, + idle: { x: 0, y: 0, w: 64, h: 256, frameWidth: 32 }, + walk: { x: 64, y: 0, w: 256, h: 256, frameWidth: 32 }, + die: { x: 672, y: 0, w: 256, h: 256, frameWidth: 32 }, + attack: { x: 320, y: 0, w: 352, h: 256, frameWidth: 32 }, + dance1: { x: 0, y: 256, w: 992, h: 32, frameWidth: 32 }, + dance2: { x: 0, y: 285, w: 256, h: 32, frameWidth: 32 }, + name: 'soldier', + }, + + 'soldierS1': { + file: loadImage('imgs/units/soldierS1.png'), + _angles: 8, + idle: { x: 0, y: 0, w: 64, h: 256, frameWidth: 32 }, + walk: { x: 64, y: 0, w: 256, h: 256, frameWidth: 32 }, + die: { x: 672, y: 0, w: 256, h: 256, frameWidth: 32 }, + attack: { x: 320, y: 0, w: 352, h: 256, frameWidth: 32 }, + dance1: { x: 0, y: 256, w: 992, h: 32, frameWidth: 32 }, + dance2: { x: 0, y: 285, w: 256, h: 32, frameWidth: 32 }, + name: 'soldierS1', + isSkin: true, + }, + + 'soldierS2': { + file: loadImage('imgs/units/soldierS2.png'), + _angles: 8, + idle: { x: 0, y: 0, w: 64, h: 256, frameWidth: 32 }, + walk: { x: 64, y: 0, w: 256, h: 256, frameWidth: 32 }, + die: { x: 672, y: 0, w: 256, h: 256, frameWidth: 32 }, + attack: { x: 320, y: 0, w: 352, h: 256, frameWidth: 32 }, + dance1: { x: 0, y: 256, w: 992, h: 32, frameWidth: 32 }, + dance2: { x: 0, y: 285, w: 256, h: 32, frameWidth: 32 }, + name: 'soldierS2', + isSkin: true, + }, + + 'skeleton': { + file: loadImage('imgs/units/skeletton.png'), + _angles: 4, + idle: { x: 0, y: 0, w: 72, h: 80, frameWidth: 18 }, + walk: { x: 75, y: 0, w: 144, h: 80, frameWidth: 18 }, + die: { x: 0, y: 87, w: 144, h: 80, frameWidth: 18 }, + attack: { x: 148, y: 87, w: 160, h: 80, frameWidth: 20 }, + special1: { x: 219, y: 0, w: 90, h: 80, frameWidth: 18 }, + name: 'skeleton', + }, + + 'archer': { + file: loadImage('imgs/units/archer.png'), + _angles: 8, + idle: { x: 0, y: 0, w: 64, h: 256, frameWidth: 32 }, + walk: { x: 64, y: 0, w: 256, h: 256, frameWidth: 32 }, + die: { x: 704, y: 0, w: 256, h: 256, frameWidth: 32 }, + attack: { x: 320, y: 0, w: 384, h: 256, frameWidth: 32 }, + name: 'archer', + }, + + 'archerS1': { + file: loadImage('imgs/units/archerS1.png'), + _angles: 8, + idle: { x: 0, y: 0, w: 64, h: 256, frameWidth: 32 }, + walk: { x: 64, y: 0, w: 256, h: 256, frameWidth: 32 }, + die: { x: 704, y: 0, w: 256, h: 256, frameWidth: 32 }, + attack: { x: 320, y: 0, w: 384, h: 256, frameWidth: 32 }, + name: 'archerS1', + isSkin: true, + }, + + 'rifleman': { + file: loadImage('imgs/units/rifleman_old.png'), + _angles: 4, + idle: { x: 0, y: 0, w: 44, h: 80, frameWidth: 22 }, + walk: { x: 51, y: 0, w: 176, h: 80, frameWidth: 22 }, + die: { x: 0, y: 87, w: 176, h: 80, frameWidth: 22 }, + attack: { x: 187, y: 87, w: 132, h: 80, frameWidth: 22 }, + name: 'rifleman', + }, + + 'worker': { + file: (SPECIAL_EVENT == 'christmas') ? loadImage('imgs/units/worker_christmas.png'): loadImage('imgs/units/worker.png'), + _angles: 8, + idle: { x: 0, y: 0, w: 64, h: 256, frameWidth: 32 }, + walk: { x: 64, y: 0, w: 256, h: 256, frameWidth: 32 }, + walkGold: { x: 320, y: 0, w: 256, h: 256, frameWidth: 32 }, + die: { x: 768, y: 0, w: 256, h: 256, frameWidth: 32 }, + attack: { x: 576, y: 0, w: 192, h: 256, frameWidth: 32 }, + dance1: { x: 0, y: 256, w: 864, h: 32, frameWidth: 18 }, + name: 'worker', + }, + + 'worker_christmas': { + file: loadImage('imgs/units/worker_christmas.png'), + _angles: 8, + idle: { x: 0, y: 0, w: 64, h: 256, frameWidth: 32 }, + walk: { x: 64, y: 0, w: 256, h: 256, frameWidth: 32 }, + walkGold: { x: 320, y: 0, w: 256, h: 256, frameWidth: 32 }, + die: { x: 768, y: 0, w: 256, h: 256, frameWidth: 32 }, + attack: { x: 576, y: 0, w: 192, h: 256, frameWidth: 32 }, + dance1: { x: 0, y: 256, w: 864, h: 32, frameWidth: 18 }, + name: 'worker christmas', + }, + + 'workerS1': { + file: loadImage('imgs/units/workerS1.png'), + _angles: 8, + idle: { x: 0, y: 0, w: 64, h: 256, frameWidth: 32 }, + walk: { x: 64, y: 0, w: 256, h: 256, frameWidth: 32 }, + walkGold: { x: 320, y: 0, w: 256, h: 256, frameWidth: 32 }, + die: { x: 768, y: 0, w: 256, h: 256, frameWidth: 32 }, + attack: { x: 576, y: 0, w: 192, h: 256, frameWidth: 32 }, + dance1: { x: 0, y: 256, w: 864, h: 32, frameWidth: 18 }, + name: 'workerS1', + isSkin: true, + }, + + 'workerS2': { + file: loadImage('imgs/units/workerS2.png'), + _angles: 8, + idle: { x: 0, y: 0, w: 64, h: 256, frameWidth: 32 }, + walk: { x: 64, y: 0, w: 256, h: 256, frameWidth: 32 }, + walkGold: { x: 320, y: 0, w: 256, h: 256, frameWidth: 32 }, + die: { x: 768, y: 0, w: 256, h: 256, frameWidth: 32 }, + attack: { x: 576, y: 0, w: 192, h: 256, frameWidth: 32 }, + dance1: { x: 0, y: 256, w: 864, h: 32, frameWidth: 18 }, + name: 'workerS2', + isSkin: true, + }, + + 'mage': { + file: loadImage('imgs/units/mage.png'), + _angles: 8, + idle: { x: 0, y: 0, w: 64, h: 256, frameWidth: 32 }, + walk: { x: 64, y: 0, w: 256, h: 256, frameWidth: 32 }, + die: { x: 608, y: 0, w: 256, h: 256, frameWidth: 32 }, + attack: { x: 320, y: 0, w: 288, h: 256, frameWidth: 32 }, + name: 'mage', + }, + + 'mageS1': { + file: loadImage('imgs/units/mageS1.png'), + _angles: 8, + idle: { x: 0, y: 0, w: 64, h: 256, frameWidth: 32 }, + walk: { x: 64, y: 0, w: 256, h: 256, frameWidth: 32 }, + die: { x: 608, y: 0, w: 256, h: 256, frameWidth: 32 }, + attack: { x: 320, y: 0, w: 288, h: 256, frameWidth: 32 }, + name: 'mageS1', + isSkin: true, + }, + + 'priest': { + file: loadImage('imgs/units/priest.png'), + _angles: 8, + idle: { x: 0, y: 0, w: 96, h: 256, frameWidth: 32 }, + walk: { x: 96, y: 0, w: 256, h: 256, frameWidth: 32 }, + die: { x: 544, y: 0, w: 256, h: 256, frameWidth: 32 }, + attack: { x: 352, y: 0, w: 192, h: 256, frameWidth: 32 }, + name: 'priest', + }, + + 'catapult': { + file: loadImage('imgs/units/catapult.png'), + _angles: 8, + idle: { x: 0, y: 0, w: 40, h: 264, frameWidth: 40 }, + walk: { x: 43, y: 0, w: 160, h: 264, frameWidth: 40 }, + die: { x: 233, y: 0, w: 357, h: 264, frameWidth: 51 }, + attack: { x: 0, y: 279, w: 722, h: 328, frameWidth: 38 }, + name: 'catapult', + }, + + 'dragon': { + file: loadImage('imgs/units/dragon.png'), + _angles: 8, + walk: { x: 0, y: 0, w: 720, h: 960, frameWidth: 120 }, + die: { x: 720, y: 0, w: 1920, h: 960, frameWidth: 120 }, + name: 'dragon', + }, + + 'wolf': { + file: loadImage('imgs/units/wolf.png'), + _angles: 4, + idle: { x: 0, y: 0, w: 60, h: 120, frameWidth: 30 }, + walk: { x: 121, y: 0, w: 180, h: 120, frameWidth: 30 }, + die: { x: 181, y: 121, w: 150, h: 120, frameWidth: 30 }, + attack: { x: 0, y: 120, w: 180, h: 120, frameWidth: 30 }, + name: 'wolf', + }, + + 'bird': { + file: loadImage('imgs/units/bird.png'), + _angles: 8, + walk: { x: 0, y: 0, w: 192, h: 572, frameWidth: 32 }, + die: { x: 192, y: 0, w: 256, h: 572, frameWidth: 32 }, + name: 'bird', + }, + + 'airship': { + file: loadImage('imgs/units/zeppelin.png'), + _angles: 8, + walk: { x: 0, y: 0, w: 800, h: 1120, frameWidth: 100 }, + die: { x: 800, y: 0, w: 800, h: 1120, frameWidth: 100 }, + name: 'airship', + }, + + 'werewolf': { + file: loadImage('imgs/units/beast.png'), + _angles: 4, + idle: { x: 623, y: 824, w: 267, h: 232, frameWidth: 89 }, + walk: { x: 0, y: 296, w: 712, h: 232, frameWidth: 89 }, + die: { x: 0, y: 824, w: 623, h: 232, frameWidth: 89 }, + attack: { x: 0, y: 528, w: 623, h: 296, frameWidth: 89 }, + special1: { x: 0, y: 0, w: 712, h: 296, frameWidth: 89 }, + name: 'werewolf', + }, + + 'ballista': { + file: loadImage('imgs/units/ballista.png'), + _angles: 8, + idle: { x: 0, y: 0, w: 66, h: 416, frameWidth: 66 }, + walk: { x: 0, y: 0, w: 264, h: 416, frameWidth: 66 }, + die: { x: 264, y: 0, w: 330, h: 416, frameWidth: 66 }, + attack: { x: 594, y: 0, w: 660, h: 416, frameWidth: 66 }, + name: 'ballista', + }, + + 'totem': { + file: loadImage('imgs/units/totem.png'), + _angles: 1, + idle: { x: 0, y: 0, w: 88, h: 30, frameWidth: 22 }, + walk: { x: 0, y: 0, w: 88, h: 30, frameWidth: 22 }, + die: { x: 0, y: 30, w: 324, h: 47, frameWidth: 54 }, + special1: { x: 88, y: 0, w: 154, h: 30, frameWidth: 22 }, + name: 'totem', + }, + 'raider': { + 'name': 'imgRaider', + 'file': loadImage('imgs/units/raider.png'), + '_angles': 4, + 'idle': { + 'x': 0, + 'y': 0, + 'w': 64, + 'h': 128, + 'frameWidth': 32, + }, + 'walk': { + 'x': 64, + 'y': 0, + 'w': 192, + 'h': 128, + 'frameWidth': 32, + }, + 'walkGold': { + 'x': 64, + 'y': 0, + 'w': 192, + 'h': 128, + 'frameWidth': 32, + }, + 'die': { + 'x': 256, + 'y': 0, + 'w': 160, + 'h': 128, + 'frameWidth': 32, + }, + 'attack': { + 'x': 416, + 'y': 0, + 'w': 672, + 'h': 128, + 'frameWidth': 32, + }, + 'special1': { + 'x': 416, + 'y': 0, + 'w': 224, + 'h': 128, + 'frameWidth': 32, + }, + }, + 'snake': { + 'name': 'imgSnake', + 'file': loadImage('imgs/units/snake.png'), + '_angles': 4, + 'idle': { + 'x': 1472, + 'y': 0, + 'w': 128, + 'h': 384, + 'frameWidth': 64, + }, + 'walk': { + 'x': 0, + 'y': 0, + 'w': 384, + 'h': 384, + 'frameWidth': 64, + }, + 'walkGold': { + 'x': 0, + 'y': 0, + 'w': 384, + 'h': 384, + 'frameWidth': 64, + }, + 'die': { + 'x': 384, + 'y': 0, + 'w': 448, + 'h': 384, + 'frameWidth': 64, + }, + 'attack': { + 'x': 382, + 'y': 0, + 'w': 640, + 'h': 384, + 'frameWidth': 64, + }, + 'special1': { + 'x': 382, + 'y': 0, + 'w': 640, + 'h': 384, + 'frameWidth': 64, + }, + }, + 'knight': { + 'name': 'imgKnight', + 'file': loadImage('imgs/units/knight.png'), + '_angles': 8, + 'idle': { + 'x': 0, + 'y': 0, + 'w': 58, + 'h': 520, + 'frameWidth': 58, + }, + 'walk': { + 'x': 58, + 'y': 0, + 'w': 408, + 'h': 520, + 'frameWidth': 58, + }, + 'walkGold': { + 'x': 58, + 'y': 0, + 'w': 408, + 'h': 520, + 'frameWidth': 58, + }, + 'die': { + 'x': 408, + 'y': 520, + 'w': 58, + 'h': 520, + 'frameWidth': 58, + }, + 'attack': { + 'x': 0, + 'y': 520, + 'w': 348, + 'h': 520, + 'frameWidth': 58, + }, + 'special1': { + 'x': 348, + 'y': 520, + 'w': 58, + 'h': 520, + 'frameWidth': 58, + }, + }, + 'gatling_gun': { + 'name': 'imgCannon', + 'file': loadImage('imgs/units/gatling_gun.png'), + '_angles': 8, + 'idle': { + 'x': 0, + 'y': 0, + 'w': 48, + 'h': 384, + 'frameWidth': 48, + }, + 'img': { + 'x': 0, + 'y': 0, + 'w': 48, + 'h': 384, + 'frameWidth': 48, + }, + 'walk': { + 'x': 0, + 'y': 0, + 'w': 192, + 'h': 384, + 'frameWidth': 48, + }, + 'walkGold': { + 'x': 0, + 'y': 0, + 'w': 192, + 'h': 384, + 'frameWidth': 48, + }, + 'die': { + 'x': 384, + 'y': 0, + 'w': 288, + 'h': 384, + 'frameWidth': 48, + }, + 'attack': { + 'x': 192, + 'y': 0, + 'w': 192, + 'h': 384, + 'frameWidth': 48, + }, + 'special1': { + 'x': 192, + 'y': 0, + 'w': 192, + 'h': 384, + 'frameWidth': 48, + }, + }, + 'shroud': { + 'name': 'imgShroud', + 'file': loadImage('imgs/shroud.png'), + '_angles': 1, + 'idle': { + 'x': 0, + 'y': 0, + 'w': 88, + 'h': 88, + 'frameWidth': 88, + }, + 'walk': { + 'x': 0, + 'y': 0, + 'w': 352, + 'h': 88, + 'frameWidth': 88, + }, + 'die': { + 'x': 0, + 'y': 0, + 'w': 88, + 'h': 88, + 'frameWidth': 88, + }, + }, + 'buildings_snake': { + 'name': 'imgSnakeDen', + 'file': loadImage('imgs/buildings_snake.png'), + '_angles': 1, + 'idle': { + 'x': 80, + 'y': 80, + 'w': 80, + 'h': 80, + 'frameWidth': 80, + }, + 'img': { + 'x': 80, + 'y': 80, + 'w': 80, + 'h': 80, + 'frameWidth': 80, + }, + 'constructionImg': { + 'x': 0, + 'y': 80, + 'w': 80, + 'h': 80, + 'frameWidth': 80, + }, + 'damagedImg': { + 'x': 160, + 'y': 80, + 'w': 80, + 'h': 80, + 'frameWidth': 80, + }, + 'busyImgs': { + 'x': 80, + 'y': 80, + 'w': 80, + 'h': 80, + 'frameWidth': 80, + }, + 'busyDamagedImgs': { + 'x': 160, + 'y': 80, + 'w': 80, + 'h': 80, + 'frameWidth': 80, + }, + 'upgradeImg': { + 'x': 80, + 'y': 80, + 'w': 80, + 'h': 80, + 'frameWidth': 80, + }, + 'upgradeImgDamaged': { + 'x': 160, + 'y': 80, + 'w': 80, + 'h': 80, + 'frameWidth': 80, + }, + }, + 'armory': { + 'name': 'imgArmory', + 'file': loadImage('imgs/buildings_snake.png'), + '_angles': 1, + 'idle': { + 'x': 0, + 'y': 0, + 'w': 64, + 'h': 80, + 'frameWidth': 64, + }, + 'img': { + 'x': 0, + 'y': 0, + 'w': 64, + 'h': 80, + 'frameWidth': 64, + }, + 'constructionImg': { + 'x': 128, + 'y': 0, + 'w': 64, + 'h': 80, + 'frameWidth': 64, + }, + 'damagedImg': { + 'x': 64, + 'y': 0, + 'w': 64, + 'h': 80, + 'frameWidth': 64, + }, + 'busyImgs': { + 'x': 0, + 'y': 0, + 'w': 64, + 'h': 80, + 'frameWidth': 64, + }, + 'busyDamagedImgs': { + 'x': 64, + 'y': 0, + 'w': 64, + 'h': 80, + 'frameWidth': 64, + }, + 'upgradeImg': { + 'x': 0, + 'y': 0, + 'w': 46, + 'h': 80, + 'frameWidth': 64, + }, + 'upgradeImgDamaged': { + 'x': 64, + 'y': 0, + 'w': 64, + 'h': 80, + 'frameWidth': 64, + }, + }, + 'caltropg': { + 'name': 'zCaltrop', + 'file': loadImage('imgs/totem.png'), + '_angles': 1, + 'idle': { + 'x': 0, + 'y': 0, + 'w': 22, + 'h': 30, + 'frameWidth': 22, + }, + 'walk': { + 'x': 0, + 'y': 0, + 'w': 88, + 'h': 30, + 'frameWidth': 22, + }, + 'walkGold': { + 'x': 0, + 'y': 0, + 'w': 88, + 'h': 30, + 'frameWidth': 22, + }, + 'die': { + 'x': 0, + 'y': 30, + 'w': 324, + 'h': 47, + 'frameWidth': 54, + }, + 'attack': { + 'x': 0, + 'y': 0, + 'w': 88, + 'h': 30, + 'frameWidth': 22, + }, + }, + 'gyrocopter': { + 'name': 'Gyrocraft', + 'file': loadImage('imgs/units/gyrocopter.png'), + '_angles': 8, + 'idle': { + 'x': 0, + 'y': 0, + 'w': 576, + 'h': 648, + 'frameWidth': 48, + }, + 'walk': { + 'x': 0, + 'y': 0, + 'w': 576, + 'h': 648, + 'frameWidth': 48, + }, + 'walkGold': { + 'x': 0, + 'y': 0, + 'w': 600, + 'h': 520, + 'frameWidth': 48, + }, + 'die': { + 'x': 576, + 'y': 0, + 'w': 960, + 'h': 648, + 'frameWidth': 48, + }, + 'attack': { + 'x': 0, + 'y': 0, + 'w': 576, + 'h': 648, + 'frameWidth': 48, + }, + 'special1': { + 'x': 0, + 'y': 0, + 'w': 600, + 'h': 520, + 'frameWidth': 100, + }, + }, + 'mill': { + 'name': 'mill', + 'file': loadImage('imgs/mill.png'), + '_angles': 1, + 'idle': { + 'x': 0, + 'y': 20, + 'w': 320, + 'h': 80, + 'frameWidth': 80, + 'frames': [ + 0, + 0, + 0, + 1, + 1, + 1, + 2, + 2, + 2, + ], + }, + 'img': { + 'x': 0, + 'y': 20, + 'w': 80, + 'h': 80, + 'frameWidth': 80, + }, + 'constructionImg': { + 'x': 640, + 'y': 20, + 'w': 80, + 'h': 80, + 'frameWidth': 80, + }, + 'damagedImg': { + 'x': 320, + 'y': 20, + 'w': 80, + 'h': 80, + 'frameWidth': 80, + }, + 'busyImgs': { + 'x': 0, + 'y': 20, + 'w': 320, + 'h': 80, + 'frameWidth': 80, + 'frames': [ + 0, + 0, + 0, + 1, + 1, + 1, + 2, + 2, + 2, + 3, + 3, + 3, + ], + }, + 'busyDamagedImgs': { + 'x': 320, + 'y': 20, + 'w': 320, + 'h': 80, + 'frameWidth': 80, + 'frames': [ + 0, + 0, + 0, + 1, + 1, + 1, + 2, + 2, + 2, + 3, + 3, + 3, + ], + }, + }, +}; + +// For some reason putting sound in config.js is not early enough +const SOUND = require('./data/Sound.js'); + +var buildingData = [ + { + name: 'Castle', + id_string: 'castle', + hp: 2250, + supplyProvided: 10, + size: 4, + weaponCooldown: 1 * 20, + weaponDelay: 1 * 20, + dmg: 0, + armor: 1, + range: 0, + vision: 10, + circleSize: 2.65, + circleOffset: 0.625, + commands: { + trainworker: 'trainworker', + upgradetofortress: 'upgradetofortress', + trainbird: 'trainbird', + bird_detection_cmd: 'bird_detection_cmd', + }, + buildTime: 54 * 20, + cost: 350, + healthbarOffset: 3.25, + healthbarWidth: 2.5, + img: 'castle', + description: 'The Castle is your main building. It can train Workers and is used to return gathered gold.', + timeToMine: 2, // how many ticks workers stay to get / deliver gold + projectileLen: 0.22, + tabPriority: 16, + takesGold: true, + preventsReveal: true, + goldPerDelivery: 0, + clickSound: SOUND.CC, + clickSoundVolume: 1, + canHaveWaypoint: true, + }, + { + name: 'Barracks', + id_string: 'barracks', + hp: 1200, + size: 3, + weaponCooldown: 1 * 20, + weaponDelay: 1 * 20, + dmg: 0, + armor: 1, + range: 0, + vision: 8, + circleSize: 2.4, + circleOffset: 0.25, + commands: { + trainsoldier: 'trainsoldier', + trainarcher: 'trainarcher', + train_raider: 'train_raider', + trainmage: 'trainmage', + }, + buildTime: 40 * 20, + cost: 125, + healthbarOffset: 3.7, + healthbarWidth: 2.0, + img: 'barracks', + description: 'The Barracks can train Soldiers, Archers, Mages and Raiders.', + tabPriority: 12, + preventsReveal: false, + clickSound: SOUND.RAX, + clickSoundVolume: 0.8, + canHaveWaypoint: true, + }, + { + name: 'Watchtower', + id_string: 'watchtower', + hp: 500, + size: 2, + weaponCooldown: 1.6 * 20, + weaponDelay: 0.3 * 20, + dmg: 30, + armor: 1, + range: 7, + vision: 12, + projectileSpeed: 14, + projectileStartHeight: 1.75, + attackLaunchSound: SOUND.SWING, + circleSize: 1.45, + circleOffset: 0.312, + commands: { + stop: 'stop', + attack: 'attack', + researchdetection: 'researchdetection', + }, + buildTime: 40 * 20, + cost: 130, + costIncrease: 5, + costIncreaseGroup: [ + 'watchtower', + ], + healthbarOffset: 3.69, + healthbarWidth: 1.56, + img: 'watchtower', + description: 'The Watchtower is a defensive structure, which shoots arrows at enemy units in range.', + projectileLen: 0.22, + tabPriority: 2, + attackEffect: 'arrow', + canAttackFlying: true, + hasDetection: false, + preventsReveal: false, + attackPrio: 10, + }, + { + name: 'Watchtower (detection)', + id_string: 'watchtower2', + hp: 450, + size: 2, + weaponCooldown: 1.6 * 20, + weaponDelay: 0.3 * 20, + dmg: 30, + armor: 1, + range: 7, + vision: 12, + projectileSpeed: 14, + projectileStartHeight: 1.75, + attackLaunchSound: SOUND.SWING, + circleSize: 1.45, + circleOffset: 0.312, + commands: { + stop: 'stop', + attack: 'attack', + }, + buildTime: 30 * 20, + cost: 25, + healthbarOffset: 3.69, + healthbarWidth: 1.56, + img: 'watchtower', + description: 'The Watchtower is a defensive structure, which shoots arrows at enemy units in range. Watchtowers get more expensive the more you have. Watchtowers have detection, that means they can see enemy invisible units.', + projectileLen: 0.22, + tabPriority: 2, + attackEffect: 'arrow', + canAttackFlying: true, + hasDetection: true, + preventsReveal: false, + attackPrio: 10, + }, + { + name: 'House', + id_string: 'house', + hp: 900, + supplyProvided: 10, + size: 3, + weaponCooldown: 1.8 * 20, + weaponDelay: 1 * 20, + dmg: 0, + armor: 1, + range: 0, + vision: 8, + circleSize: 2.4, + circleOffset: 0.25, + commands: {}, + buildTime: 25 * 20, + cost: 100, + healthbarOffset: 3.4, + healthbarWidth: 2.1, + img: 'house', + description: 'A House increases your maximum population by 10. You require a house to construct Barracks, Wolf Dens, and Workshops', + tabPriority: 1, + preventsReveal: false, + clickSound: SOUND.HOUSE, + clickSoundVolume: 1, + }, + { + name: 'Goldmine', + id_string: 'goldmine', + hp: 40000, + size: 3, + weaponCooldown: 1 * 20, + weaponDelay: 1 * 20, + dmg: 0, + armor: 10, + range: 0, + vision: 0, + circleSize: 2.12, + circleOffset: 0.17, + commands: {}, + buildTime: 30 * 20, + cost: 250, + healthbarOffset: 2.15, + healthbarWidth: 2.03, + img: 'mine', + description: 'The Goldmine contains gold for players to gather using Workers. Too many Workers on one mine leads to inefficient mining.', + isInvincible: true, + startGold: 6000, + maxWorkers: 7, + miningEfficiencyCoefficient: 0.5, + minMiningRate: 0.25, + timeToMine: 32, + alwaysNeutral: true, + goldPerDelivery: 5, + preventsReveal: false, + }, + { + name: 'Mages Guild', + id_string: 'magesguild', + hp: 950, + size: 3, + weaponCooldown: 1 * 20, + weaponDelay: 1 * 20, + dmg: 0, + armor: 1, + range: 0, + vision: 8, + circleSize: 2.1, + circleOffset: 0.2, + commands: { + researchfireball: 'researchfireball', + }, + buildTime: 54 * 20, + cost: 150, + healthbarOffset: 3.45, + healthbarWidth: 2.04, + img: 'mages_guild', + description: 'The Mages Guild allows you to train Mages from the barracks and research fireball. Mages are strong, but vulnerable units.', + tabPriority: 3, + preventsReveal: false, + clickSound: SOUND.MAGES_GUILD, + clickSoundVolume: 0.9, + canHaveWaypoint: true, + }, + { + name: 'Workshop', + id_string: 'workshop', + hp: 1300, + size: 4, + weaponCooldown: 1 * 20, + weaponDelay: 1 * 20, + dmg: 0, + armor: 1, + range: 0, + vision: 8, + circleSize: 3, + circleOffset: 0.4, + commands: { + constructcatapult: 'constructcatapult', + construct_gatling: 'construct_gatling', + spokedwheel_cmd: 'spokedwheel_cmd', + }, + buildTime: 55 * 20, + cost: 125, + healthbarOffset: 3.6, + healthbarWidth: 2.7, + img: 'workshop', + busySmokeEffectLocationX: 0.5, + busySmokeEffectLocationY: 3, + busySmokeEffectLocationZ: 5.7, + description: 'The Workshop allows you to build Catapults, Gatling Guns and research upgrades.', + tabPriority: 10, + preventsReveal: false, + clickSound: SOUND.WORKSHOP, + clickSoundVolume: 1, + canHaveWaypoint: true, + }, + { + name: 'Forge', + id_string: 'forge', + hp: 800, + size: 4, + weaponCooldown: 1 * 20, + weaponDelay: 1 * 20, + dmg: 0, + armor: 1, + range: 0, + vision: 8, + circleSize: 3, + circleOffset: 0.2, + commands: { + attackupgrade: 'attackupgrade', + armorupgrade: 'armorupgrade', + mechattackupgrade: 'mechattackupgrade', + mecharmorupgrade: 'mecharmorupgrade', + }, + buildTime: 50 * 20, + cost: 150, + healthbarOffset: 3, + healthbarWidth: 2.7, + img: 'forge', + busySmokeEffectLocationX: -1.3, + busySmokeEffectLocationY: 3, + busySmokeEffectLocationZ: 5.0, + description: 'The Forge provides you with the ability to research armor and damage upgrades for human or mechanical units', + tabPriority: 6, + preventsReveal: false, + clickSound: SOUND.FORGE, + clickSoundVolume: 0.75, + }, + { + name: 'Start Location', + id_string: 'startlocation', + hp: 400, + size: 4, + weaponCooldown: 1 * 20, + supplyProvided: 10, + weaponDelay: 1 * 20, + dmg: 0, + armor: 1, + range: 0, + vision: 8, + circleSize: 3, + circleOffset: 0.2, + commands: {}, + buildTime: 55 * 20, + cost: 150, + healthbarOffset: 3, + healthbarWidth: 2.7, + img: 'start_location', + description: 'The Start Location marks where a player starts. When the game begins, a Castle and 6 Workers will be created at this position.', + tabPriority: 1, + isInvincible: true, + limit: 1, + takesGold: true, + preventsReveal: true, + }, + { + name: 'Fortress', + id_string: 'fortress', + hp: 2900, + supplyProvided: 10, + size: 4, + weaponCooldown: 2 * 20, + weaponDelay: 1 * 20, + dmg: 0, + armor: 2, + range: 0, + projectileSpeed: 14, + projectileStartHeight: 1.75, + attackLaunchSound: SOUND.SWING, + vision: 10, + circleSize: 2.65, + circleOffset: 0.625, + commands: { + trainworker: 'trainworker', + trainbird: 'trainbird', + bird_detection_cmd: 'bird_detection_cmd', + }, + buildTime: 72 * 20, + cost: 100, + healthbarOffset: 4.25, + healthbarWidth: 2.5, + img: 'fortress', + description: 'The Fortress is an advanced version of the Castle. It allows you to build a Dragons Lair.', + timeToMine: 2, // how many ticks workers stay to get / deliver gold + projectileLen: 0.22, + tabPriority: 16, + attackEffect: 'arrow', + takesGold: true, + preventsReveal: true, + clickSound: SOUND.CC, + clickSoundVolume: 1, + canHaveWaypoint: true, + }, + { + name: 'Dragons Lair', + id_string: 'dragonslair', + hp: 950, + size: 3, + weaponCooldown: 1 * 20, + weaponDelay: 1 * 20, + dmg: 0, + armor: 1, + range: 0, + vision: 8, + circleSize: 2.5, + circleOffset: 0.15, + commands: { + traindragon: 'traindragon', + }, + buildTime: 72 * 20, + cost: 200, + healthbarOffset: 4.65, + healthbarWidth: 2.5, + img: 'dragons_lair', + description: 'The Dragons Lair allows you to train Dragons.', + tabPriority: 13, + preventsReveal: false, + clickSound: SOUND.DRAGONS_LAIR, + clickSoundVolume: 0.85, + canHaveWaypoint: true, + }, + { + name: 'Wolves Den', + id_string: 'wolvesden', + hp: 950, + size: 3, + weaponCooldown: 1 * 20, + weaponDelay: 1 * 20, + dmg: 0, + armor: 1, + range: 0, + vision: 8, + circleSize: 2.3, + circleOffset: 0.125, + commands: { + trainwolf: 'trainwolf', + train_snake: 'train_snake', + upgradetowerewolvesden: 'upgradetowerewolvesden', + }, + buildTime: 40 * 20, + cost: 100, + healthbarOffset: 2.95, + healthbarWidth: 2.5, + img: 'wolves_den', + description: 'The Wolves Den allows you to train Wolves and Snakes.', + tabPriority: 14, + preventsReveal: false, + clickSound: SOUND.W_DEN, + clickSoundVolume: 1, + canHaveWaypoint: true, + }, + { + name: 'Animal Testing Lab', + id_string: 'animaltestinglab', + hp: 900, + size: 4, + weaponCooldown: 1 * 20, + weaponDelay: 1 * 20, + dmg: 0, + armor: 1, + range: 0, + vision: 8, + circleSize: 3.4, + circleOffset: 0.125, + commands: { + beastattackupgrade: 'beastattackupgrade', + beastdefenseupgrade: 'beastdefenseupgrade', + }, + buildTime: 54 * 20, + cost: 150, + healthbarOffset: 3.75, + healthbarWidth: 3.1, + img: 'animal_testing_lab', + description: 'The Animal Testing Lab allows you to research upgrades for your beasts.', + tabPriority: 7, + preventsReveal: false, + clickSound: SOUND.LAB, + clickSoundVolume: 1, + }, + { + name: 'Advanced Workshop', + id_string: 'advancedworkshop', + hp: 1050, + size: 4, + weaponCooldown: 1 * 20, + weaponDelay: 1 * 20, + dmg: 0, + armor: 1, + range: 0, + vision: 8, + circleSize: 3.4, + circleOffset: 0.125, + commands: { + constructballista: 'constructballista', + constructairship: 'constructairship', + ballistaexplosives: 'ballistaexplosives', + researchtelescope: 'researchtelescope', + }, + buildTime: 63 * 20, + cost: 140, + healthbarOffset: 3.75, + healthbarWidth: 3.1, + img: 'adv_workshop', + description: 'The Advanced Workshop allows you to build Airships and Ballistas.', + tabPriority: 9, + preventsReveal: false, + clickSound: SOUND.A_WS, + clickSoundVolume: 1, + canHaveWaypoint: true, + }, + { + name: 'Werewolves Den', + id_string: 'werewolvesden', + hp: 1200, + size: 3, + weaponCooldown: 1 * 20, + weaponDelay: 1 * 20, + dmg: 0, + armor: 2, + range: 0, + vision: 8, + circleSize: 2.3, + circleOffset: 0.125, + commands: { + trainwolf: 'trainwolf', + train_snake: 'train_snake', + trainwerewolf: 'trainwerewolf', + }, + buildTime: 65 * 20, + cost: 225, + healthbarOffset: 3.95, + healthbarWidth: 2.5, + img: 'werewolves_den', + description: 'The Werewolves Den allows you to train Wolves, Snakes and Werewolves.', + tabPriority: 15, + preventsReveal: false, + clickSound: SOUND.WW_DEN, + clickSoundVolume: 1, + canHaveWaypoint: true, + }, + { + name: 'Church', + id_string: 'church', + hp: 1100, + size: 4, + weaponCooldown: 1 * 20, + weaponDelay: 1 * 20, + dmg: 0, + armor: 1, + range: 0, + vision: 8, + circleSize: 3.3, + circleOffset: 0.125, + commands: { + researchinvisibility: 'researchinvisibility', + trainpriest: 'trainpriest', + }, + buildTime: 54 * 20, + cost: 175, + healthbarOffset: 4.7, + healthbarWidth: 2.5, + img: 'church', + description: 'The Church allows you to train Priests and research spells. Priests are strong, but vulnerable units.', + tabPriority: 11, + preventsReveal: false, + clickSound: SOUND.CHURCH, + clickSoundVolume: 1, + canHaveWaypoint: true, + }, + { + name: 'Snake Charmer', + id_string: 'snakecharmer', + hp: 880, + supplyProvided: 0, + size: 2, + weaponCooldown: 1 * 20, + weaponDelay: 1 * 20, + dmg: 0, + armor: 1, + range: 0, + vision: 8, + circleSize: 1.5, + imageScale: 0.7, + circleOffset: 0.1, + buildTime: 40 * 20, + commands: { + research_sprint: 'research_sprint', + }, + cost: 100, + healthbarOffset: 1.9, + healthbarWidth: 1.4, + selectionOffsetY: 0, + img: 'buildings_snake', + description: 'Construction of this building allows you to build Snakes in the Wolf Den and unlock Wolf Sprint.', + timeToMine: 2, // how many ticks workers stay to get / deliver gold + projectileLen: 0.22, + tabPriority: 5, + takesGold: false, + preventsReveal: false, + clickSound: SOUND.SNAKE_CHARMER, + clickSoundVolume: 1, + canHaveWaypoint: false, + }, + { + name: 'Mill', + id_string: 'mill', + hp: 1050, + mana: 0, + startMana: 0, + hpRegenerationRate: 0 / 20, + manaRegenerationRate: 0 / 20, + armor: 1, + supply: 0, + supplyProvided: 0, + weaponCooldown: 1 * 20, + weaponDelay: 1 * 20, + dmg: 0, + dmgModifierAttributes: [], + dmgModifierAddition: [], + dmgModifierMultiplier: [], + lifesteal: 0, + armorPenetration: 0, + percDmg: 0, + dmgCap: 1, + range: 0, + minRange: -999, + aoeRadius: 0, + bouncePower: 0, + bounceDistMin: 0, + bounceDistMax: 0, + attackPrio: 5, + vision: 8, + projectileSpeed: 8, + projectileLen: 0.2, + attackLaunchSound: 0, + attackEffect: null, + projectileStartHeight: 0, + circleSize: 3.4, + imageScale: 1, + circleOffset: 0.125, + buildTime: 63 * 20, + cost: 140, + costIncrease: 150, + costIncreaseGroup: [], + healthbarOffset: 3.3, + healthbarWidth: 3.1, + selectionOffsetY: 0, + img: 'mill', + description: 'The Mill allows you to build Gyrocrafts.', + tabPriority: 8, + drawOffsetY: 6, + size: 4, + repairRate: 0 / 20, + startGold: 0, + timeToMine: 20, + miningEfficiencyCoefficient: 0.5, + minMiningRate: 0.1, + maxWorkers: 6, + limit: 0, + visionHeightBonus: 0, + commands: { + construct_gyrocraft: 'construct_gyrocraft', + }, + lifetime: 0, + goldReward: 0, + maxUnitsToRepair: 1, + modifiers: [], + modifiersSelf: [], + spawnModifiers: [], + deathSound: 12, + clickSound: SOUND.MILL, + clickSoundVolume: 1, + hoverText: '', + canHaveWaypoint: true, + causesFlameDeath: false, + hitscan: false, + maximizeRangeWhenShooting: false, + hitsFriendly: true, + hitsEnemy: true, + canAttackGround: true, + canAttackFlying: false, + isHeatSeeking: true, + ignoreEnemyHitscan: false, + controllable: true, + hasDetection: false, + expOnlyFromOwnKills: false, + alliesGetExperience: false, + alliesGetGold: false, + isReflectingProjectiles: false, + isBlockingProjectiles: false, + takeDamageOnBlock: false, + isInvincible: false, + isInvisible: false, + alwaysNeutral: false, + takesGold: false, + spawnWithAMove: false, + preventsReveal: false, + preventsLoss: true, + isMechanical: false, + isUndead: false, + isBiological: false, + isBeast: false, + isHuman: false, + noShow: false, + }, + { + name: 'Armory', + id_string: 'armory', + hp: 880, + mana: 0, + startMana: 0, + hpRegenerationRate: 0 / 20, + manaRegenerationRate: 0 / 20, + armor: 1, + supply: 0, + supplyProvided: 0, + weaponCooldown: 1.8 * 20, + weaponDelay: 1 * 20, + dmg: 0, + dmgModifierAttributes: [], + dmgModifierAddition: [], + dmgModifierMultiplier: [], + lifesteal: 0, + armorPenetration: 0, + percDmg: 0, + dmgCap: 1, + range: 0.2, + minRange: -999, + aoeRadius: 0, + bouncePower: 0, + bounceDistMin: 0, + bounceDistMax: 0, + attackPrio: 5, + vision: 8, + projectileSpeed: 8, + projectileLen: 0.2, + attackLaunchSound: 0, + attackEffect: null, + projectileStartHeight: 0, + circleSize: 2.2, + imageScale: 1, + circleOffset: 0.35, + buildTime: 50 * 20, + cost: 150, + costIncrease: 150, + costIncreaseGroup: [], + healthbarOffset: 2.8, + healthbarWidth: 2.1, + selectionOffsetY: 0, + img: 'armory', + description: 'The Armory researches abilities for your Barracks units.', + tabPriority: 4, + drawOffsetY: 6, + size: 3, + repairRate: 0 / 20, + startGold: 0, + timeToMine: 20, + miningEfficiencyCoefficient: 0.5, + minMiningRate: 0.1, + maxWorkers: 6, + limit: 0, + visionHeightBonus: 0, + commands: { + rangeupgrade: 'rangeupgrade', + researchraidershroudupgrade: 'researchraidershroudupgrade', + }, + lifetime: 0, + goldReward: 0, + maxUnitsToRepair: 1, + modifiers: [], + modifiersSelf: [], + spawnModifiers: [], + deathSound: 12, + clickSound: SOUND.ARMORY, + clickSoundVolume: 1, + hoverText: '', + canHaveWaypoint: false, + causesFlameDeath: false, + hitscan: false, + maximizeRangeWhenShooting: false, + hitsFriendly: true, + hitsEnemy: true, + canAttackGround: true, + canAttackFlying: false, + isHeatSeeking: true, + ignoreEnemyHitscan: false, + controllable: true, + hasDetection: false, + expOnlyFromOwnKills: false, + alliesGetExperience: false, + alliesGetGold: false, + isReflectingProjectiles: false, + isBlockingProjectiles: false, + takeDamageOnBlock: false, + isInvincible: false, + isInvisible: false, + alwaysNeutral: false, + takesGold: false, + spawnWithAMove: false, + preventsReveal: false, + preventsLoss: true, + isMechanical: false, + isUndead: false, + isBiological: false, + isBeast: false, + isHuman: false, + noShow: false, + }, +]; + +var commands = [ + { + type: COMMAND.MAKEUNIT, + unitType: 'worker', + isInstant: true, + image: 'worker', + hotkey: KEY.Q, + name: 'Train Worker', + id_string: 'trainworker', + description: 'Cost: getField(worker.cost) Gold#BRSupply: getField(worker.supply)#BRDuration: getField(worker.buildTime) sec#BRgetField(worker.description)', + interfacePosX: 0, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.MAKEUNIT, + unitType: 'soldier', + isInstant: true, + image: 'soldier', + hotkey: KEY.Q, + name: 'Train Soldier', + id_string: 'trainsoldier', + description: 'Cost: getField(soldier.cost) Gold#BRSupply: getField(soldier.supply)#BRDuration: getField(soldier.buildTime) sec#BRgetField(soldier.description)', + interfacePosX: 0, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.MAKEUNIT, + unitType: 'archer', + isInstant: true, + image: 'archer', + hotkey: KEY.W, + name: 'Train Archer', + id_string: 'trainarcher', + description: 'Cost: getField(archer.cost) Gold#BRSupply: getField(archer.supply)#BRDuration: getField(archer.buildTime) sec#BRgetField(archer.description)', + interfacePosX: 1, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.MAKEBUILDING, + unitType: 'castle', + image: 'castle', + hotkey: KEY.A, + name: 'Build Castle', + id_string: 'buildcastle', + description: 'Cost: getField(castle.cost) Gold#BRDuration: getField(castle.buildTime) sec#BRgetField(castle.description)', + targetIsPoint: true, + commandCard: 1, + interfacePosX: 0, + interfacePosY: 1, + }, + + { + type: COMMAND.MAKEBUILDING, + unitType: 'barracks', + image: 'barracks', + hotkey: KEY.Q, + name: 'Build Barracks', + id_string: 'buildbarracks', + requirementType: ['house'], + requirementLevel: [1], + requirementText: ['Requires a House'], + description: 'Cost: getField(barracks.cost) Gold#BRDuration: getField(barracks.buildTime) sec#BRgetField(barracks.description)', + targetIsPoint: true, + commandCard: 2, + interfacePosX: 0, + interfacePosY: 0, + }, + + { + type: COMMAND.MAKEBUILDING, + unitType: 'watchtower', + image: 'watchtower', + hotkey: KEY.W, + name: 'Build Watchtower', + id_string: 'buildwatchtower', + description: 'Cost: getField(watchtower.cost) Gold#BRDuration: getField(watchtower.buildTime) sec#BRgetField(watchtower.description)', + targetIsPoint: true, + commandCard: 1, + interfacePosX: 1, + interfacePosY: 0, + }, + + { + type: COMMAND.MAKEBUILDING, + unitType: 'house', + image: 'house', + hotkey: KEY.E, + name: 'Build House', + id_string: 'buildhouse', + description: 'Cost: getField(house.cost) Gold#BRDuration: getField(house.buildTime) sec#BRgetField(house.description)', + targetIsPoint: true, + commandCard: 1, + interfacePosX: 2, + interfacePosY: 0, + }, + + { + type: COMMAND.IDLE, + isInstant: true, + image: 'stop', + hotkey: KEY.S, + name: 'Stop', + id_string: 'stop', + description: 'Stop and cancel any existing orders.', + interfacePosX: 1, + interfacePosY: 1, + commandCard: 0, + }, + + { + type: COMMAND.HOLDPOSITION, + isInstant: true, + image: 'holdposition', + hotkey: KEY.D, + name: 'Hold Position', + id_string: 'holdposition', + description: 'The unit will stop and not move whatsoever.', + interfacePosX: 2, + interfacePosY: 1, + commandCard: 0, + }, + + { + type: COMMAND.ATTACK, + image: 'attack', + hotkey: KEY.A, + name: 'Attack', + id_string: 'attack', + description: 'Order attack an enemy unit to attack it directly or order attack at a point to order your unit(s) to move there and attacking any enemies on its way.', + targetIsUnit: true, + interfacePosX: 0, + interfacePosY: 1, + commandCard: 0, + cursor: Cursors.ATTACK, + }, + + { + type: COMMAND.CANCEL, + isInstant: true, + targetIsInt: true, + image: 'cancel', + hotkey: KEY.R, + name: 'Cancel', + id_string: 'cancel', + description: 'Cancel the current order.', + interfacePosX: 4, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.MOVE, + name: 'Move', + id_string: 'move', + targetIsPoint: true, + }, + + { + type: COMMAND.MOVETO, + name: 'Moveto', + id_string: 'moveto', + targetIsUnit: true, + range: [0.2], + }, + + { + type: COMMAND.MINE, + name: 'Mine', + id_string: 'mine', + targetIsUnit: true, + range: [0.2], + }, + + { + type: COMMAND.REPAIR, + name: 'Repair', + id_string: 'repair', + image: 'repair', + description: 'Repair a building or a mechanical unit.', + targetIsUnit: true, + range: [0], + hotkey: KEY.F, + interfacePosX: 3, + interfacePosY: 1, + commandCard: 0, + targetRequirements1: [targetRequirements.isBuilding, targetRequirements.isMechanical], + hasAutocast: true, + autocastConditions: 'hp < type.hp - 1 && owner == this.owner && isUnderConstruction != 1', + }, + + { + type: COMMAND.AMOVE, + name: 'AMove', + id_string: 'amove', + targetIsPoint: true, + }, + + { + type: COMMAND.MAKEBUILDING, + name: 'Makebuilding', + id_string: 'makebuilding', + }, + + { + type: COMMAND.MAKEUNIT, + unitType: 'mage', + isInstant: true, + image: 'mage', + hotkey: KEY.A, + name: 'Train Mage', + id_string: 'trainmage', + description: 'Cost: getField(mage.cost) Gold#BRSupply: getField(mage.supply)#BRDuration: getField(mage.buildTime) sec#BRgetField(mage.description) Train a mage, requires a mage\'s guild.', + requirementText: ['Requires a Mages Guild'], + requirementType: ['magesguild'], + requirementLevel: [1], + interfacePosX: 0, + interfacePosY: 1, + commandCard: 0, + }, + + { + type: COMMAND.MAKEUNIT, + unitType: 'priest', + id_string: 'trainpriest', + isInstant: true, + image: 'priest', + hotkey: KEY.A, + name: 'Train Priest', + id_string: 'trainpriest', + description: 'Cost: getField(priest.cost) Gold#BRSupply: getField(priest.supply)#BRDuration: getField(priest.buildTime) sec#BRgetField(priest.description)', + interfacePosX: 0, + interfacePosY: 1, + commandCard: 0, + }, + + { + type: COMMAND.MAKEBUILDING, + unitType: 'magesguild', + image: 'mages_guild', + hotkey: KEY.E, + name: 'Build Mages Guild', + id_string: 'buildmagesguild', + description: 'Cost: getField(magesguild.cost) Gold#BRDuration: getField(magesguild.buildTime) sec#BRgetField(magesguild.description)', + requirementText: ['Requires a Barracks'], + requirementType: ['barracks'], + requirementLevel: [1], + targetIsPoint: true, + commandCard: 2, + interfacePosX: 2, + interfacePosY: 0, + }, + + { + type: COMMAND.UNIVERSAL, + image: 'flamestrike', + hotkey: KEY.Q, + name: 'Flamestrike', + id_string: 'flamestrike', + targetIsPoint: true, + manaCost: [50], + aoeRadius: [1.75], + damage: [40], + projectileSpeed: [6], + duration: 0, + range: [6], + description: 'Casts a flamestrike at a target location, dealing getField(shockwave.damage) damage to nearby units and throwing them back.', + interfacePosX: 2, + interfacePosY: 1, + commandCard: 0, + cursor: Cursors.BLANK, + useAoeCursor: true, + castingDelay: 0.05 * 20, + cooldown: 0.3 * 20, + launchSound: SOUND.FLAMESTRIKE_LAUNCH, + attackEffect: 'flamestrike', + bounceDistMax: 6, + bounceDistMin: 3, + bouncePower: 1.5, + }, + + { + type: COMMAND.MAKEUNIT, + unitType: 'catapult', + isInstant: true, + image: 'catapult', + hotkey: KEY.Q, + name: 'Construct Catapult', + id_string: 'constructcatapult', + description: 'Cost: getField(catapult.cost) Gold#BRSupply: getField(catapult.supply)#BRDuration: getField(catapult.buildTime) sec#BRgetField(catapult.description)', + interfacePosX: 0, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.MAKEBUILDING, + unitType: 'workshop', + image: 'workshop', + hotkey: KEY.Q, + name: 'Build Workshop', + id_string: 'buildworkshop', + description: 'Cost: getField(workshop.cost) Gold#BRDuration: getField(workshop.buildTime) sec#BRgetField(workshop.description)', + requirementText: ['Requires a House'], + requirementType: ['house'], + requirementLevel: [1], + targetIsPoint: true, + interfacePosX: 0, + interfacePosY: 0, + commandCard: 4, + }, + + { + type: COMMAND.MAKEBUILDING, + unitType: 'forge', + image: 'forge', + hotkey: KEY.R, + name: 'Build Forge', + id_string: 'buildforge', + description: 'Cost: getField(forge.cost) Gold#BRDuration: getField(forge.buildTime) sec#BRgetField(forge.description)', + targetIsPoint: true, + interfacePosX: 3, + interfacePosY: 0, + commandCard: 1, + }, + + { + type: COMMAND.SWITCH_CC, + image: 'hammer', + hotkey: KEY.Q, + name: 'Basic Buildings', + id_string: 'basic_buildings', + description: 'Construct basic buildings', + interfacePosX: 0, + interfacePosY: 0, + commandCard: 0, + targetCC: 1, + }, + + { + type: COMMAND.SWITCH_CC, + image: 'soldier', + hotkey: KEY.W, + name: 'Human Buildings', + id_string: 'human_buildings', + description: 'Construct buildings to train and upgrade human units', + interfacePosX: 1, + interfacePosY: 0, + commandCard: 0, + targetCC: 2, + }, + + { + type: COMMAND.SWITCH_CC, + image: 'wolf', + hotkey: KEY.E, + name: 'Beast Buildings', + id_string: 'beast_buildings', + description: 'Construct buildings to train and upgrade beast units', + interfacePosX: 2, + interfacePosY: 0, + commandCard: 0, + targetCC: 3, + }, + + { + type: COMMAND.SWITCH_CC, + image: 'catapult', + hotkey: KEY.R, + name: 'Mechancial Buildings', + id_string: 'mech_buildings', + description: 'Construct buildings to train and upgrade mechanical units', + interfacePosX: 3, + interfacePosY: 0, + commandCard: 0, + targetCC: 4, + }, + // No longer used in the base game but needed for backwards compatability for mods + { + type: COMMAND.SWITCH_CC, + image: 'hammer', + hotkey: KEY.Q, + name: 'Buildings', + id_string: 'buildings', + description: 'Construct Buildings', + interfacePosX: 0, + interfacePosY: 0, + commandCard: 0, + targetCC: 1, + }, + // No longer used in the base game but needed for backwards compatability for mods + { + type: COMMAND.SWITCH_CC, + image: 'hammer', + hotkey: KEY.W, + name: 'Buildings', + id_string: 'buildings2', + description: 'Construct additional Buildings', + interfacePosX: 1, + interfacePosY: 0, + commandCard: 0, + targetCC: 2, + }, + + { + type: COMMAND.UPGRADE, + upgrade: 'upgattack', + isInstant: true, + image: 'attackUpg', + hotkey: KEY.Q, + name: 'Attack Upgrade', + id_string: 'attackupgrade', + description: 'Level add(getUpgradeLevel(upgattack), upgradeCountInResearch(upgattack), 1)#BRCost: getField(upgattack.cost) Gold#BRDuration: getField(upgattack.buildTime) sec#BRgetField(upgattack.description)', + interfacePosX: 0, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.UPGRADE, + upgrade: 'upgarmor', + isInstant: true, + image: 'armorUpg', + hotkey: KEY.W, + name: 'Armor Upgrade', + id_string: 'armorupgrade', + description: 'Level add(getUpgradeLevel(upgarmor), upgradeCountInResearch(upgarmor), 1)#BRCost: getField(upgarmor.cost) Gold#BRDuration: getField(upgarmor.buildTime) sec#BRgetField(upgarmor.description)', + interfacePosX: 1, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.UNIVERSAL, + image: 'heal', + hotkey: KEY.W, + name: 'Heal', + id_string: 'heal', + targetRequirements1: [targetRequirements.isBiological], + targetIsUnit: true, + attackEffect: 'heal', + manaCost: [25], + range: [8], + projectileSpeed: [0], + damage: [-150], + description: 'Heals a target unit by mul(getField(heal.damage), -1) HP', + interfacePosX: 1, + interfacePosY: 1, + commandCard: 0, + castingDelay: 0.05 * 20, + cooldown: 0.4 * 20, + launchSound: SOUND.HEAL, + cursor: Cursors.HEAL, + }, + + { + type: COMMAND.UPGRADE, + upgrade: 'upgfireball', + isInstant: true, + image: 'flamestrike', + hotkey: KEY.Q, + name: 'Research Fireball', + id_string: 'researchfireball', + description: 'Cost: getField(upgfireball.cost) Gold#BRDuration: getField(upgfireball.buildTime) sec#BRgetField(upgfireball.description)', + interfacePosX: 0, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.UPGRADE, + upgrade: 'upgslowfield', + isInstant: true, + image: 'slowfield', + hotkey: KEY.W, + name: 'Research Slow Field', + id_string: 'researchslowfield', + description: 'Cost: getField(upgslowfield.cost) Gold#BRDuration: getField(upgslowfield.buildTime) sec#BRgetField(upgslowfield.description)', + interfacePosX: 1, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.UPGRADE, + upgrade: 'upgheal', + isInstant: true, + image: 'heal', + hotkey: KEY.W, + name: 'Research Heal', + id_string: 'researchheal', + description: 'Cost: getField(upgheal.cost) Gold#BRDuration: getField(upgheal.buildTime) sec#BRgetField(upgheal.description)', + interfacePosX: 1, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.BUILDING_UPGRADE, + isInstant: true, + image: 'fortress', + hotkey: KEY.A, + improvedBuilding: 'fortress', + name: 'Upgrade To Fortress', + id_string: 'upgradetofortress', + description: 'Cost: getField(fortress.cost) Gold#BRDuration: getField(fortress.buildTime) sec#BRgetField(fortress.description)', + interfacePosX: 0, + interfacePosY: 1, + commandCard: 0, + }, + + { + type: COMMAND.BUILDING_UPGRADE, + isInstant: true, + image: 'eye', + hotkey: KEY.Q, + improvedBuilding: 'watchtower2', + name: 'Research Detection', + id_string: 'researchdetection', + description: 'Cost: getField(watchtower2.cost) Gold#BRDuration: getField(watchtower2.buildTime) sec#BRgetField(watchtower2.description)', + interfacePosX: 0, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.MAKEBUILDING, + unitType: 'dragonslair', + image: 'dragons_lair', + hotkey: KEY.W, + name: 'Build Dragons Lair', + id_string: 'builddragonslair', + description: 'Cost: getField(dragonslair.cost) Gold#BRDuration: getField(dragonslair.buildTime) sec#BRgetField(dragonslair.description)', + requirementText: ['Requires a Fortress'], + requirementType: ['fortress'], + requirementLevel: [1, 1], + targetIsPoint: true, + commandCard: 3, + interfacePosX: 1, + interfacePosY: 0, + }, + + { + type: COMMAND.MAKEUNIT, + unitType: 'dragon', + isInstant: true, + image: 'dragon', + hotkey: KEY.Q, + name: 'Train Dragon', + id_string: 'traindragon', + description: 'Cost: getField(dragon.cost) Gold#BRSupply: getField(dragon.supply)#BRDuration: getField(dragon.buildTime) sec#BRgetField(dragon.description)', + interfacePosX: 0, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.UPGRADE, + upgrade: 'upgbeastattack', + isInstant: true, + image: 'dragonAttUpg', + hotkey: KEY.Q, + name: 'Beast Attack Upgrade', + id_string: 'beastattackupgrade', + description: 'Level add(getUpgradeLevel(upgbeastattack), upgradeCountInResearch(upgbeastattack), 1)#BRCost: getField(upgbeastattack.cost) Gold#BRDuration: getField(upgbeastattack.buildTime) sec#BRgetField(upgbeastattack.description)', + interfacePosX: 0, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.UPGRADE, + upgrade: 'upgbeastdefense', + isInstant: true, + image: 'dragonDefUpg', + hotkey: KEY.W, + name: 'Beast Defense Upgrade', + id_string: 'beastdefenseupgrade', + description: 'Level add(getUpgradeLevel(upgbeastdefense), upgradeCountInResearch(upgbeastdefense), 1)#BRCost: getField(upgbeastdefense.cost) Gold#BRDuration: getField(upgbeastdefense.buildTime) sec#BRgetField(upgbeastdefense.description)', + interfacePosX: 1, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.MAKEUNIT, + unitType: 'wolf', + isInstant: true, + image: 'wolf', + hotkey: KEY.Q, + name: 'Train Wolf', + id_string: 'trainwolf', + description: 'Cost: getField(wolf.cost) Gold#BRSupply: getField(wolf.supply)#BRDuration: getField(wolf.buildTime) sec#BRgetField(wolf.description)', + interfacePosX: 0, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.MAKEBUILDING, + unitType: 'wolvesden', + image: 'wolves_den', + hotkey: KEY.Q, + name: 'Build Wolves Den', + id_string: 'buildwolvesden', + description: 'Cost: getField(wolvesden.cost) Gold#BRDuration: getField(wolvesden.buildTime) sec#BRgetField(wolvesden.description)', + requirementText: ['Requires a House'], + requirementType: ['house'], + requirementLevel: [1], + targetIsPoint: true, + commandCard: 3, + interfacePosX: 0, + interfacePosY: 0, + }, + + { + type: COMMAND.MAKEBUILDING, + unitType: 'animaltestinglab', + image: 'animal_testing_lab', + hotkey: KEY.R, + name: 'Build Animal Testing Lab', + id_string: 'buildanimaltestinglab', + description: 'Cost: getField(animaltestinglab.cost) Gold#BRDuration: getField(animaltestinglab.buildTime) sec#BRgetField(animaltestinglab.description)', + targetIsPoint: true, + commandCard: 3, + interfacePosX: 3, + interfacePosY: 0, + }, + + { + type: COMMAND.LOAD, + image: 'load', + hotkey: KEY.Q, + name: 'Load in', + id_string: 'loadin', + description: 'Load units in', + targetIsUnit: true, + commandCard: 0, + interfacePosX: 0, + interfacePosY: 0, + }, + + { + type: COMMAND.UNLOAD, + image: 'unload', + hotkey: KEY.W, + name: 'Unload', + id_string: 'unload', + description: 'Unload units', + targetIsPoint: true, + commandCard: 0, + interfacePosX: 1, + interfacePosY: 0, + }, + + { + type: COMMAND.MAKEBUILDING, + unitType: 'advancedworkshop', + image: 'adv_workshop', + hotkey: KEY.W, + name: 'Build Advanced Workshop', + id_string: 'buildadvancedworkshop', + description: 'Cost: getField(advancedworkshop.cost) Gold#BRDuration: getField(advancedworkshop.buildTime) sec#BRgetField(advancedworkshop.description)', + targetIsPoint: true, + commandCard: 4, + interfacePosX: 1, + interfacePosY: 0, + }, + + { + type: COMMAND.MAKEBUILDING, + unitType: 'advancedworkshop', + image: 'adv_workshop', + hotkey: KEY.S, + name: 'Test building', + id_string: 'buildadvancedworkshop2', + description: 'Cost: getField(advancedworkshop.cost) Gold#BRDuration: getField(advancedworkshop.buildTime) sec#BRgetField(advancedworkshop.description)', + targetIsPoint: true, + commandCard: 4, + interfacePosX: 1, + interfacePosY: 1, + }, + + { + type: COMMAND.MAKEUNIT, + unitType: 'airship', + isInstant: true, + image: 'airship', + hotkey: KEY.Q, + name: 'Construct Airship', + id_string: 'constructairship', + description: 'Cost: getField(airship.cost) Gold#BRSupply: getField(airship.supply)#BRDuration: getField(airship.buildTime) sec#BRgetField(airship.description)', + interfacePosX: 0, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.UPGRADE, + upgrade: 'upgtower', + isInstant: true, + image: 'towerUpg', + hotkey: KEY.W, + name: 'Tower Upgrade', + id_string: 'towerupgrade', + description: 'Cost: getField(upgtower.cost) Gold#BRDuration: getField(upgtower.buildTime) sec#BRgetField(upgtower.description)', + interfacePosX: 1, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.UNLOAD2, + name: 'Direct Unload', + id_string: 'directunload', + targetIsInt: true, + }, + + { + type: COMMAND.ATTACK_GROUND, + hotkey: KEY.Q, + name: 'Attack Ground', + id_string: 'attackground', + targetIsPoint: true, + image: 'groundAttack', + interfacePosX: 0, + interfacePosY: 0, + commandCard: 0, + description: 'Fire at a certain point', + cursor: Cursors.BLANK, + useAoeCursor: true, + aoeRadius: [0.45], + }, + + { + type: COMMAND.UNIVERSAL, + name: 'Smash', + id_string: 'smash', + image: 'smash', + hotkey: KEY.Q, + isInstant: true, + interfacePosX: 0, + interfacePosY: 0, + commandCard: 0, + description: 'Perform a round house hit, dealing damage and smashing back small nearby units (only hits enemy units)', + aoeRadius: [3], + damage: [40], + hitsFriendly: false, + cooldown2: 15 * 20, + animationName: 'special1', + castingDelay: 0.75 * 20, + cooldown: 1.8 * 20, + hitsSelf: false, + launchSound: SOUND.ROUNDHOUSE, + soundPerHit: SOUND.BIGHIT, + bounceDistMax: 6.5, + bounceDistMin: 4, + bouncePower: 1.5, + attackEffect: 'smoke', + targetFiltersExclude: ['flying'], + }, + + { + type: COMMAND.UPGRADE, + upgrade: 'upgspeed', + isInstant: true, + image: 'speedUpg', + hotkey: KEY.E, + name: 'Speed Upgrade', + id_string: 'speedupgrade', + description: 'Level add(getUpgradeLevel(upgspeed), upgradeCountInResearch(upgspeed), 1)#BRCost: getField(upgspeed.cost) Gold#BRDuration: getField(upgspeed.buildTime) sec#BRgetField(upgspeed.description)', + interfacePosX: 2, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.UPGRADE, + upgrade: 'upgbeastspeed', + isInstant: true, + image: 'beastSpeedUpg', + hotkey: KEY.E, + name: 'Beast Speed Upgrade', + id_string: 'beastspeedupgrade', + description: 'Level add(getUpgradeLevel(upgbeastspeed), upgradeCountInResearch(upgbeastspeed), 1)#BRCost: getField(upgbeastspeed.cost) Gold#BRDuration: getField(upgbeastspeed.buildTime) sec#BRgetField(upgbeastspeed.description)', + interfacePosX: 2, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.BUILDING_UPGRADE, + isInstant: true, + image: 'werewolves_den', + hotkey: KEY.A, + improvedBuilding: 'werewolvesden', + name: 'Upgrade To Werewolves Den', + id_string: 'upgradetowerewolvesden', + description: 'Cost: getField(werewolvesden.cost) Gold#BRDuration: getField(werewolvesden.buildTime) sec#BRgetField(werewolvesden.description)', + interfacePosX: 0, + interfacePosY: 1, + commandCard: 0, + }, + + { + type: COMMAND.MAKEUNIT, + unitType: 'werewolf', + isInstant: true, + image: 'werewolf', + hotkey: KEY.E, + name: 'Train Werewolf', + id_string: 'trainwerewolf', + description: 'Cost: getField(werewolf.cost) Gold#BRSupply: getField(werewolf.supply)#BRDuration: getField(werewolf.buildTime) sec#BRgetField(werewolf.description)', + interfacePosX: 2, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.MAKEUNIT, + unitType: 'ballista', + isInstant: true, + image: 'ballista', + hotkey: KEY.W, + name: 'Construct Ballista', + id_string: 'constructballista', + description: 'Cost: getField(ballista.cost) Gold#BRSupply: getField(ballista.supply)#BRDuration: getField(ballista.buildTime) sec#BRgetField(ballista.description)', + interfacePosX: 1, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.UPGRADE, + upgrade: 'upgmechattack', + isInstant: true, + image: 'mechAttUpg', + hotkey: KEY.A, + name: 'Mech Attack Upgrade', + id_string: 'mechattackupgrade', + description: 'Level add(getUpgradeLevel(upgmechattack), upgradeCountInResearch(upgmechattack), 1)#BRCost: getField(upgmechattack.cost) Gold#BRDuration: getField(upgmechattack.buildTime) sec#BRgetField(upgmechattack.description)', + interfacePosX: 0, + interfacePosY: 1, + commandCard: 0, + }, + + { + type: COMMAND.UPGRADE, + upgrade: 'upgmechdefense', + isInstant: true, + image: 'mechDefUpg', + hotkey: KEY.S, + name: 'Mech Armor Upgrade', + id_string: 'mecharmorupgrade', + description: 'Level add(getUpgradeLevel(upgmechdefense), upgradeCountInResearch(upgmechdefense), 1)#BRCost: getField(upgmechdefense.cost) Gold#BRDuration: getField(upgmechdefense.buildTime) sec#BRgetField(upgmechdefense.description)', + interfacePosX: 1, + interfacePosY: 1, + commandCard: 0, + }, + + { + type: COMMAND.UPGRADE, + upgrade: 'upgmechspeed', + isInstant: true, + image: 'mechSpeedUpg', + hotkey: KEY.D, + name: 'Mech Speed Upgrade', + id_string: 'mechspeedupgrade', + description: 'Level add(getUpgradeLevel(upgmechspeed), upgradeCountInResearch(upgmechspeed), 1)#BRCost: getField(upgmechspeed.cost) Gold#BRDuration: getField(upgmechspeed.buildTime) sec#BRgetField(upgmechspeed.description)', + interfacePosX: 2, + interfacePosY: 1, + commandCard: 0, + }, + + { + type: COMMAND.UPGRADE, + upgrade: 'upgballistaexplosives', + isInstant: true, + image: 'flakUpg', + hotkey: KEY.S, + name: 'Ballista Black Powder', + id_string: 'ballistaexplosives', + description: 'Cost: getField(upgballistaexplosives.cost) Gold#BRDuration: getField(upgballistaexplosives.buildTime) sec#BRgetField(upgballistaexplosives.description) Allows ballistas to use AOE shot and adds splash damage to ballistas attacks.', + interfacePosX: 1, + interfacePosY: 1, + commandCard: 0, + }, + + { + type: COMMAND.UNIVERSAL, + name: 'Fireball', + id_string: 'fireball', + requirementText: ['This spell needs to be researched first'], + requirementType: ['upgfireball'], + requirementLevel: [1], + description: 'Casts a fireball at a target location, dealing damage to all enemy units in the way.', + targetIsPoint: true, + manaCost: [55], + image: 'flamestrike', + attackEffect: 'flamestrike', + hotkey: KEY.Q, + projectileAoeRadius: [1.0], + projectileDamage: [5], + projectileSpeed: [6.6], + maximizeRangeWhenCasting: true, + hitsFriendly: false, + hitsSelf: false, + range: [13], + effectScale: 2, + interfacePosX: 0, + interfacePosY: 0, + commandCard: 0, + castingDelay: 0.05 * 20, + cooldown: 0.3 * 20, + cooldown2: 0 * 20, + launchSound: SOUND.FLAMESTRIKE_LAUNCH, + causesFlameDeath: true, + cursor: Cursors.ATTACK, + targetFiltersExclude: ['flying'], + }, + + { + type: COMMAND.UPGRADE, + upgrade: 'upgshockwave', + isInstant: true, + image: 'shockwave', + hotkey: KEY.E, + name: 'Research Shockwave', + id_string: 'researchshockwave', + description: 'Cost: getField(upgshockwave.cost) Gold#BRDuration: getField(upgshockwave.buildTime) sec#BRgetField(upgshockwave.description)', + interfacePosX: 2, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.SWITCH_CC, + image: 'cancel', + hotkey: KEY.G, + name: 'Back', + id_string: 'back_basic', + description: 'Back to main menu', + interfacePosX: 4, + interfacePosY: 1, + commandCard: 1, + targetCC: 0, + }, + + { + type: COMMAND.SWITCH_CC, + image: 'cancel', + hotkey: KEY.G, + name: 'Back', + id_string: 'back_humans', + description: 'Back to main menu', + interfacePosX: 4, + interfacePosY: 1, + commandCard: 2, + targetCC: 0, + }, + + { + type: COMMAND.SWITCH_CC, + image: 'cancel', + hotkey: KEY.G, + name: 'Back', + id_string: 'back_beast', + description: 'Back to main menu', + interfacePosX: 4, + interfacePosY: 1, + commandCard: 3, + targetCC: 0, + }, + + { + type: COMMAND.SWITCH_CC, + image: 'cancel', + hotkey: KEY.G, + name: 'Back', + id_string: 'back_mech', + description: 'Back to main menu', + interfacePosX: 4, + interfacePosY: 1, + commandCard: 4, + targetCC: 0, + }, + + // No longer used in the base game but needed for backwards compatability for mods + { + type: COMMAND.SWITCH_CC, + image: 'cancel', + hotkey: KEY.G, + name: 'Back', + id_string: 'back', + description: 'Back to main menu', + interfacePosX: 4, + interfacePosY: 1, + commandCard: 1, + targetCC: 0, + }, + + // No longer used in the base game but needed for backwards compatability for mods + { + type: COMMAND.SWITCH_CC, + image: 'cancel', + hotkey: KEY.G, + name: 'Back', + id_string: 'back2', + description: 'Back to main menu', + interfacePosX: 4, + interfacePosY: 1, + commandCard: 2, + targetCC: 0, + }, + { + type: COMMAND.SWITCH_CC, + image: 'cancel', + hotkey: KEY.G, + name: 'Back', + id_string: 'back3', + description: 'Back to main menu', + interfacePosX: 4, + interfacePosY: 1, + commandCard: 3, + targetCC: 0, + }, + + { + type: COMMAND.SWITCH_CC, + image: 'cancel', + hotkey: KEY.G, + name: 'Back', + id_string: 'back4', + description: 'Back to main menu', + interfacePosX: 4, + interfacePosY: 1, + commandCard: 4, + targetCC: 0, + }, + + { + type: COMMAND.UPGRADE, + upgrade: 'upgrange', + isInstant: true, + image: 'rangeUpg', + hotkey: KEY.W, + name: 'Research Archer Range', + id_string: 'rangeupgrade', + description: 'Level add(getUpgradeLevel(upgrange), upgradeCountInResearch(upgrange), 1)#BRCost: getField(upgrange.cost) Gold#BRDuration: getField(upgrange.buildTime) sec#BRgetField(upgrange.description)', + interfacePosX: 1, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.UPGRADE, + upgrade: 'upgbeastrange', + isInstant: true, + image: 'beastRangeUpg', + hotkey: KEY.T, + name: 'Beast Range Upgrade', + id_string: 'beastrangeupgrade', + description: 'Level add(getUpgradeLevel(upgbeastrange), upgradeCountInResearch(upgbeastrange), 1)#BRCost: getField(upgbeastrange.cost) Gold#BRDuration: getField(upgbeastrange.buildTime) sec#BRgetField(upgbeastrange.description)', + interfacePosX: 3, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.UPGRADE, + upgrade: 'upgmechrange', + isInstant: true, + image: 'mechRangeUpg', + hotkey: KEY.F, + name: 'Mech Range Upgrade', + id_string: 'mechrangeupgrade', + description: 'Level add(getUpgradeLevel(upgmechrange), upgradeCountInResearch(upgmechrange), 1)#BRCost: getField(upgmechrange.cost) Gold#BRDuration: getField(upgmechrange.buildTime) sec#BRgetField(upgmechrange.description)', + interfacePosX: 3, + interfacePosY: 1, + commandCard: 0, + }, + + { + type: COMMAND.UNIVERSAL, + name: 'Dmg Buff', + id_string: 'dmgbuffability', + image: 'worker', + hotkey: KEY.Q, + targetIsPoint: true, + interfacePosX: 0, + interfacePosY: 1, + commandCard: 0, + range: [8], + description: 'Gives a dmg buff', + aoeRadius: [3.5], + cooldown2: 10 * 20, + castingDelay: 1 * 20, + cooldown: 1.8 * 20, + launchSound: SOUND.ROUNDHOUSE, + modifiers: ['dmgbuff'], + }, + + { + type: COMMAND.UNIVERSAL, + name: 'Invisibility', + id_string: 'invisibilityspell', + image: 'invisibility', + hotkey: KEY.Q, + targetIsUnit: true, + hitsEnemy: false, + interfacePosX: 0, + interfacePosY: 0, + commandCard: 0, + range: [9], + description: 'Makes target unit or building invisible and gives a small damage buff', + castingDelay: 0.05 * 20, + cooldown: 0.5 * 20, + manaCost: [50], + requirementText: ['This spell needs to be researched first'], + requirementType: ['upginvis'], + requirementLevel: [1], + launchSound: SOUND.ROUNDHOUSE, + modifiers: ['invisibility'], + attackEffect: 'smoke', + }, + + { + type: COMMAND.UNIVERSAL, + name: 'Summon Skeleton', + id_string: 'summonskeleton', + image: 'skeleton', + hotkey: KEY.W, + targetIsPoint: true, + interfacePosX: 1, + interfacePosY: 1, + commandCard: 0, + range: [6], + description: 'Summons a skeleton that has a limited lifetime', + castingDelay: 0.05 * 20, + cooldown: 0.5 * 20, + manaCost: [50], + requirementText: ['This spell needs to be researched first'], + requirementType: ['upgskeleton'], + requirementLevel: [1], + launchSound: SOUND.ROUNDHOUSE, + summonedUnits: ['skeleton'], + attackEffect: 'smoke', + requiresVision: true, + }, + + { + type: COMMAND.MAKEBUILDING, + unitType: 'church', + image: 'church', + hotkey: KEY.A, + name: 'Build Church', + id_string: 'buildchurch', + description: 'Cost: getField(church.cost) Gold#BRDuration: getField(church.buildTime) sec#BRgetField(church.description)', + targetIsPoint: true, + commandCard: 2, + interfacePosX: 0, + interfacePosY: 1, + }, + + { + type: COMMAND.UPGRADE, + upgrade: 'upginvis', + id_string: 'researchinvisibility', + isInstant: true, + image: 'invisibility', + hotkey: KEY.Q, + name: 'Research Invisibility', + id_string: 'researchinvisibility', + description: 'Cost: getField(upginvis.cost) Gold#BRDuration: getField(upginvis.buildTime) sec#BRgetField(upginvis.description)', + interfacePosX: 0, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.UPGRADE, + upgrade: 'upgskeleton', + isInstant: true, + image: 'skeleton', + hotkey: KEY.W, + name: 'Research Summon Skeleton', + id_string: 'researchsummonskeleton', + description: 'Cost: getField(upgskeleton.cost) Gold#BRDuration: getField(upgskeleton.buildTime) sec#BRgetField(upgskeleton.description)', + interfacePosX: 1, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.UPGRADE, + upgrade: 'upghealingward', + id_string: 'researchsummonhealingward', + isInstant: true, + image: 'heal', + hotkey: KEY.W, + name: 'Research Summon Healing Ward', + id_string: 'researchsummonhealingward', + description: 'Cooldown: getField(summonhealingward.cooldown2)#BRSummons a healing ward that heals nearby allied units.', + interfacePosX: 1, + interfacePosY: 0, + cooldown2: 0.5 * 20, + commandCard: 0, + }, + + { + type: COMMAND.UPGRADE, + upgrade: 'upgtelescope', + isInstant: true, + image: 'telescope', + hotkey: KEY.A, + name: 'Airship Telescope Extension', + id_string: 'researchtelescope', + description: 'Cost: getField(upgtelescope.cost) Gold#BRDuration: getField(upgtelescope.buildTime) sec#BRgetField(upgtelescope.description)', + interfacePosX: 0, + interfacePosY: 1, + commandCard: 0, + }, + + { + type: COMMAND.DANCE, + isInstant: true, + name: 'Dance', + id_string: 'dance', + dance_img: 'dance1', + chat_str: '/dance', + hide: true, + }, + + { + type: COMMAND.DANCE, + isInstant: true, + name: 'Dance2', + id_string: 'dance2', + dance_img: 'dance2', + chat_str: '/dance2', + hide: true, + }, + + { + type: COMMAND.UNIVERSAL, + name: 'Summon Healing Ward', + id_string: 'summonhealingward', + image: 'heal', + hotkey: KEY.W, + targetIsPoint: true, + interfacePosX: 1, + interfacePosY: 0, + commandCard: 0, + range: [6], + description: 'Summons a healing ward that heals nearby allied non-undead units and damages enemy undead units', + castingDelay: 0.05 * 20, + cooldown: 0.5 * 20, + cooldown2: 10 * 20, + manaCost: [25], + requirementText: [], + requirementType: [], + requirementLevel: [1], + launchSound: SOUND.WARP, + summonedUnits: ['healingward'], + attackEffect: 'smoke', + aoeRadius: [2.75], + useAoeCursor: true, + requiresVision: true, + }, + + { + type: COMMAND.UNIVERSAL, + name: 'Slow Field', + id_string: 'summonslowfield', + image: 'slowfield', + hotkey: KEY.W, + targetIsPoint: true, + interfacePosX: 1, + interfacePosY: 0, + commandCard: 0, + range: [7], + description: 'Summons a slow field that slows nearby ground units.', + castingDelay: 0.05 * 20, + cooldown: 0.5 * 2, + manaCost: [80], + launchSound: SOUND.WARP, + summonedUnits: ['slowingfield'], + attackEffect: 'smoke', + useAoeCursor: true, + aoeRadius: [3.5], + requiresVision: true, + }, + + { + type: COMMAND.MAKEUNIT, + unitType: 'bird', + isInstant: true, + image: 'bird', + hotkey: KEY.W, + name: 'Train Bird', + id_string: 'trainbird', + description: 'Cost: getField(bird.cost) Gold#BRSupply: getField(bird.supply)#BRDuration: getField(bird.buildTime) sec#BRgetField(bird.description)', + requirementText: ['Requires a House'], + requirementType: ['house'], + requirementLevel: [1], + interfacePosX: 1, + interfacePosY: 0, + commandCard: 0, + }, + + { + type: COMMAND.UNIVERSAL, + name: 'Fire', + id_string: 'fire', + image: 'heal', + hotkey: KEY.Q, + learnHotkey: KEY.W, + targetIsUnit: true, + interfacePosX: 0, + interfacePosY: 1, + commandCard: 0, + learnCommandCard: 0, + learnInterfacePosX: 1, + learnInterfacePosY: 1, + requiredLevels: [1, 3, 5], + range: [2, 6, 10], + damage: [20, 100, 800], + description: 'Summons a healing ward that heals nearby allied non-undead units and damages enemy undead units', + castingDelay: 0.05 * 20, + cooldown: 0.5 * 20, + manaCost: [10, 20, 30], + launchSound: SOUND.WARP, + attackEffect: 'smoke', + requiresVision: true, + }, + + { + type: COMMAND.TELEPORT, + name: 'Teleport', + id_string: 'teleport', + image: 'teleport', + hotkey: KEY.Q, + interfacePosX: 0, + interfacePosY: 1, + commandCard: 0, + range: [7], + description: 'Teleports this unit over a short distance', + castingDelay: 0.05 * 20, + cooldown: 0.5 * 20, + manaCost: [0], + launchSound: SOUND.WARP, + attackEffect: 'smoke', + requiresVision: true, + }, + { + type: COMMAND.MAKEUNIT, + name: 'Train Snake', + id_string: 'train_snake', + isCommand: true, + unitType: 'snake', + hotkey: KEY.W, + targetIsPoint: false, + targetIsUnit: false, + isInstant: true, + isChanneled: false, + playLaunchSoundOnce: false, + useAoeCursor: false, + commandCard: 0, + interfacePosX: 1, + interfacePosY: 0, + requiredLevels: [], + learnCommandCard: 0, + learnInterfacePosX: 0, + learnInterfacePosY: 0, + learnHotkey: 81, + image: 'snake', + attackEffectInit: 'spell', + description: 'Cost: getField(snake.cost) Gold#BRSupply: getField(snake.supply)#BRDuration: getField(snake.buildTime) sec#BRgetField(snake.description)', + requirementType: [ + 'snakecharmer', + ], + requirementLevel: [ + 1, + ], + requirementText: [ + 'Requires a Snake Charmer', + ], + targetRequirements1: [], + targetRequirements2: [], + targetRequirements3: [], + manaCost: [ + 0, + ], + goldCost: 0, + aoeRadius: [ + 0, + ], + damage: [ + 0, + ], + projectileDamage: [ + 0, + ], + projectileAoeRadius: [ + 0, + ], + maximizeRangeWhenCasting: false, + hitsFriendly: true, + hitsEnemy: true, + hitsSelf: true, + targetFilters: [], + targetFiltersExclude: [], + effectScale: 1, + hasAutocast: false, + autocastDefault: false, + autocastConditions: '', + projectileSpeed: [ + 8, + ], + duration: 0, + castingDelay: 0 * 20, + cooldown: 0 * 20, + cooldown2: 0 * 20, + range: [ + 0, + ], + minRange: [-999], + bounceDistMin: 0, + bounceDistMax: 0, + bouncePower: 0, + targetCC: 0, + animationName: '', + causesFlameDeath: false, + modifiers: [], + modifiersSelf: [], + summonedUnits: [], + summonsUseWaypoint: false, + summonsWaypointAMove: false, + ignoreSupplyCheck: false, + requiresVision: false, + }, + + { + type: COMMAND.TELEPORT, + isCommand: true, + name: 'Flash', + id_string: 'Flash', + hotkey: 81, + targetIsPoint: false, + targetIsUnit: false, + isInstant: false, + isChanneled: false, + playLaunchSoundOnce: false, + useAoeCursor: false, + commandCard: 0, + interfacePosX: 0, + interfacePosY: 0, + requiredLevels: [], + learnCommandCard: 0, + learnInterfacePosX: 0, + learnInterfacePosY: 0, + learnHotkey: 81, + image: 'teleport', + attackEffectInit: 'spell', + attackEffect: 'smoke', + description: 'Teleports the Raider over a short distance (4)', + requirementType: [], + requirementLevel: [], + requirementText: [], + targetRequirements1: [], + targetRequirements2: [], + targetRequirements3: [], + launchSound: 68, + manaCost: [ + 25, + ], + goldCost: 0, + aoeRadius: [ + 0, + ], + damage: [ + 0, + ], + projectileDamage: [ + 0, + ], + projectileAoeRadius: [ + 0, + ], + maximizeRangeWhenCasting: false, + hitsFriendly: true, + hitsEnemy: true, + hitsSelf: true, + targetFilters: [], + targetFiltersExclude: [], + effectScale: 1, + hasAutocast: false, + autocastDefault: false, + autocastConditions: '', + projectileSpeed: [ + 8, + ], + duration: 0, + castingDelay: 0 * 20, + cooldown: 0 * 20, + cooldown2: 0 * 20, + range: [ + 4, + ], + minRange: [-999], + bounceDistMin: 0, + bounceDistMax: 0, + bouncePower: 0, + targetCC: 0, + animationName: '', + causesFlameDeath: false, + modifiers: [], + modifiersSelf: [ + 'raiderflashmana', + ], + summonedUnits: [], + summonsUseWaypoint: false, + summonsWaypointAMove: false, + ignoreSupplyCheck: false, + requiresVision: true, + }, + { + type: COMMAND.UPGRADE, + name: 'Research Shroud', + id_string: 'researchraidershroudupgrade', + isCommand: true, + hotkey: KEY.Q, + targetIsPoint: false, + targetIsUnit: false, + isInstant: true, + isChanneled: false, + playLaunchSoundOnce: false, + useAoeCursor: false, + commandCard: 0, + interfacePosX: 0, + interfacePosY: 0, + requiredLevels: [], + learnCommandCard: 0, + learnInterfacePosX: 0, + learnInterfacePosY: 0, + image: 'shroud', + attackEffectInit: 'spell', + description: 'Cost: getField(raidershroudupgrade.cost) Gold#BRDuration: getField(raidershroudupgrade.buildTime) sec#BRgetField(raidershroudupgrade.description)', + requirementType: [], + requirementLevel: [], + requirementText: [], + targetRequirements1: [], + targetRequirements2: [], + targetRequirements3: [], + upgrade: 'raidershroudupgrade', + manaCost: [ + 0, + ], + goldCost: 0, + aoeRadius: [ + 0, + ], + damage: [ + 0, + ], + projectileDamage: [ + 0, + ], + projectileAoeRadius: [ + 0, + ], + maximizeRangeWhenCasting: false, + hitsFriendly: true, + hitsEnemy: true, + hitsSelf: true, + targetFilters: [], + targetFiltersExclude: [ + 'flying', + ], + effectScale: 1, + hasAutocast: false, + autocastDefault: false, + autocastConditions: '', + projectileSpeed: [ + 8, + ], + duration: 0, + castingDelay: 0 * 20, + cooldown: 0 * 20, + cooldown2: 0 * 20, + range: [ + 0, + ], + minRange: [-999], + bounceDistMin: 0, + bounceDistMax: 0, + bouncePower: 0, + targetCC: 0, + animationName: '', + causesFlameDeath: false, + modifiers: [], + modifiersSelf: [], + summonedUnits: [], + summonsUseWaypoint: false, + summonsWaypointAMove: false, + ignoreSupplyCheck: false, + requiresVision: false, + }, + { + type: COMMAND.UNIVERSAL, + name: 'Shroud', + id_string: 'Shroud', + isCommand: true, + hotkey: 87, + targetIsPoint: true, + targetIsUnit: false, + isInstant: false, + isChanneled: false, + playLaunchSoundOnce: false, + useAoeCursor: true, + commandCard: 0, + interfacePosX: 1, + interfacePosY: 0, + requiredLevels: [], + learnCommandCard: 0, + learnInterfacePosX: 0, + learnInterfacePosY: 0, + learnHotkey: 81, + image: 'shroud', + attackEffectInit: 'spell', + attackEffect: 'smoke', + description: 'Creates a shroud that reduces the range of all units under it to melee for 15 seconds.', + requirementType: [ + 'raidershroudupgrade', + ], + requirementLevel: [ + 1, + ], + requirementText: [ + 'Shroud must be researched.', + ], + targetRequirements1: [], + targetRequirements2: [], + targetRequirements3: [], + launchSound: 68, + manaCost: [ + 30, + ], + goldCost: 0, + aoeRadius: [ + 2.7, + ], + damage: [ + 0, + ], + projectileDamage: [ + 0, + ], + projectileAoeRadius: [ + 0, + ], + maximizeRangeWhenCasting: false, + hitsFriendly: false, + hitsEnemy: true, + hitsSelf: true, + targetFilters: [], + targetFiltersExclude: [], + effectScale: 1, + hasAutocast: false, + autocastDefault: false, + autocastConditions: '', + projectileSpeed: [ + 8, + ], + duration: 0, + castingDelay: 0.05 * 20, + cooldown: 0.05 * 20, + cooldown2: 0 * 20, + range: [ + 6, + ], + minRange: [-999], + bounceDistMin: 0, + bounceDistMax: 0, + bouncePower: 0, + targetCC: 0, + animationName: '', + causesFlameDeath: false, + modifiers: [], + modifiersSelf: [], + summonedUnits: [ + 'shroudfield', + ], + summonsUseWaypoint: false, + summonsWaypointAMove: false, + ignoreSupplyCheck: false, + requiresVision: true, + }, + { + type: COMMAND.MAKEUNIT, + name: 'Train Raider', + id_string: 'train_raider', + isCommand: true, + image: 'raider', + unitType: 'raider', + hotkey: 69, + targetIsPoint: false, + targetIsUnit: false, + isInstant: true, + isChanneled: false, + playLaunchSoundOnce: false, + useAoeCursor: false, + commandCard: 0, + interfacePosX: 2, + interfacePosY: 0, + requiredLevels: [], + learnCommandCard: 0, + learnInterfacePosX: 0, + learnInterfacePosY: 0, + learnHotkey: 81, + attackEffectInit: 'spell', + description: 'Cost: getField(raider.cost) Gold#BRSupply: getField(raider.supply)#BRDuration: getField(raider.buildTime) sec#BRgetField(raider.description)', + requirementType: [], + requirementLevel: [], + requirementText: [], + targetRequirements1: [], + targetRequirements2: [], + targetRequirements3: [], + manaCost: [ + 0, + ], + goldCost: 0, + aoeRadius: [ + 0, + ], + damage: [ + 0, + ], + projectileDamage: [ + 0, + ], + projectileAoeRadius: [ + 0, + ], + maximizeRangeWhenCasting: false, + hitsFriendly: true, + hitsEnemy: true, + hitsSelf: true, + targetFilters: [], + targetFiltersExclude: [], + effectScale: 1, + hasAutocast: false, + autocastDefault: false, + autocastConditions: '', + projectileSpeed: [ + 8, + ], + duration: 0, + castingDelay: 0 * 20, + cooldown: 0 * 20, + cooldown2: 0 * 20, + range: [ + 0, + ], + minRange: [-999], + bounceDistMin: 0, + bounceDistMax: 0, + bouncePower: 0, + targetCC: 0, + animationName: '', + causesFlameDeath: false, + modifiers: [], + modifiersSelf: [], + summonedUnits: [], + summonsUseWaypoint: false, + summonsWaypointAMove: false, + ignoreSupplyCheck: false, + requiresVision: false, + }, + + { + type: COMMAND.MAKEBUILDING, + unitType: 'snakecharmer', + image: 'buildings_snake', + hotkey: KEY.A, + name: 'Build Snake Charmer', + id_string: 'buildsnakecharmer', + description: 'Cost: getField(snakecharmer.cost) Gold#BRDuration: getField(snakecharmer.buildTime) sec#BRgetField(snakecharmer.description)', + requirementText: ['Requires a Wolves Den'], + requirementType: ['wolvesden'], + requirementLevel: [1], + targetIsPoint: true, + commandCard: 3, + interfacePosX: 0, + interfacePosY: 1, + }, + + { + type: COMMAND.MAKEBUILDING, + name: 'Build Armory', + id_string: 'buildarmory', + isCommand: true, + unitType: 'armory', + hotkey: KEY.R, + targetIsPoint: true, + targetIsUnit: false, + isInstant: false, + isChanneled: false, + playLaunchSoundOnce: false, + useAoeCursor: false, + commandCard: 2, + interfacePosX: 3, + interfacePosY: 0, + requiredLevels: [], + learnCommandCard: 0, + learnInterfacePosX: 0, + learnInterfacePosY: 0, + learnHotkey: 81, + image: 'armory', + attackEffectInit: 'spell', + description: 'Cost: getField(armory.cost) Gold#BRDuration: getField(armory.buildTime) sec#BRgetField(armory.description)', + requirementType: [ + 'barracks', + ], + requirementLevel: [ + 1, + ], + requirementText: [ + 'Requires a Barracks', + ], + targetRequirements1: [], + targetRequirements2: [], + targetRequirements3: [], + manaCost: [ + 0, + ], + goldCost: 0, + aoeRadius: [ + 0, + ], + damage: [ + 0, + ], + projectileDamage: [ + 0, + ], + projectileAoeRadius: [ + 0, + ], + maximizeRangeWhenCasting: false, + hitsFriendly: true, + hitsEnemy: true, + hitsSelf: true, + targetFilters: [], + targetFiltersExclude: [], + effectScale: 1, + hasAutocast: false, + autocastDefault: false, + autocastConditions: '', + projectileSpeed: [ + 8, + ], + duration: 0, + castingDelay: 0 * 20, + cooldown: 0 * 20, + cooldown2: 0 * 20, + range: [ + 0, + ], + minRange: [-999], + bounceDistMin: 0, + bounceDistMax: 0, + bouncePower: 0, + targetCC: 0, + animationName: '', + causesFlameDeath: false, + modifiers: [], + modifiersSelf: [], + summonedUnits: [], + summonsUseWaypoint: false, + summonsWaypointAMove: false, + ignoreSupplyCheck: false, + requiresVision: false, + }, + { + type: COMMAND.MAKEUNIT, + name: 'Construct Gatling Gun', + id_string: 'construct_gatling', + isCommand: true, + image: 'gatling_gun', + unitType: 'gatlinggun', + hotkey: 87, + targetIsPoint: false, + targetIsUnit: false, + isInstant: true, + isChanneled: false, + playLaunchSoundOnce: false, + useAoeCursor: false, + commandCard: 0, + interfacePosX: 1, + interfacePosY: 0, + requiredLevels: [], + learnCommandCard: 0, + learnInterfacePosX: 0, + learnInterfacePosY: 0, + learnHotkey: 81, + attackEffectInit: 'spell', + description: 'Cost: getField(gatlinggun.cost) Gold#BRSupply: getField(gatlinggun.supply)#BRDuration: getField(gatlinggun.buildTime) sec#BRgetField(gatlinggun.description)', + requirementType: [], + requirementLevel: [], + requirementText: [], + targetRequirements1: [], + targetRequirements2: [], + targetRequirements3: [], + manaCost: [ + 0, + ], + goldCost: 0, + aoeRadius: [ + 0, + ], + damage: [ + 0, + ], + projectileDamage: [ + 0, + ], + projectileAoeRadius: [ + 0, + ], + maximizeRangeWhenCasting: false, + hitsFriendly: true, + hitsEnemy: true, + hitsSelf: true, + targetFilters: [], + targetFiltersExclude: [], + effectScale: 1, + hasAutocast: false, + autocastDefault: false, + autocastConditions: '', + projectileSpeed: [ + 8, + ], + duration: 0, + castingDelay: 0 * 20, + cooldown: 0 * 20, + cooldown2: 0 * 20, + range: [ + 0, + ], + minRange: [-999], + bounceDistMin: 0, + bounceDistMax: 0, + bouncePower: 0, + targetCC: 0, + animationName: '', + causesFlameDeath: false, + modifiers: [], + modifiersSelf: [], + summonedUnits: [], + summonsUseWaypoint: false, + summonsWaypointAMove: false, + ignoreSupplyCheck: false, + requiresVision: false, + }, + { + type: COMMAND.UNIVERSAL, + name: 'Drop Caltrops', + id_string: 'dropcaltrops', + isCommand: true, + unitType: 'worker', + hotkey: 81, + targetIsPoint: false, + targetIsUnit: false, + isInstant: true, + isChanneled: false, + playLaunchSoundOnce: false, + useAoeCursor: false, + commandCard: 0, + interfacePosX: 0, + interfacePosY: 0, + requiredLevels: [], + learnCommandCard: 0, + learnInterfacePosX: 0, + learnInterfacePosY: 0, + learnHotkey: 81, + image: 'caltropg', + attackEffectInit: 'spell', + attackEffect: null, + description: 'Cost: getField(dropcaltrops.goldCost)#BRCooldown: getField(dropcaltrops.cooldown2)#BRDrops a caltrop on the ground.', + requirementType: [ + 'advancedworkshop', + ], + requirementLevel: [ + 1, + ], + requirementText: [ + 'Requires an Advanced Workshop', + ], + targetRequirements1: [], + targetRequirements2: [], + targetRequirements3: [], + launchSound: SOUND.CALTROP, + manaCost: [ + 10, + ], + goldCost: 5, + aoeRadius: [ + 0, + ], + damage: [ + 0, + ], + projectileDamage: [ + 0, + ], + projectileAoeRadius: [ + 0, + ], + maximizeRangeWhenCasting: false, + hitsFriendly: true, + hitsEnemy: true, + hitsSelf: true, + targetFilters: [], + targetFiltersExclude: [], + effectScale: 1, + hasAutocast: false, + autocastDefault: false, + autocastConditions: '', + projectileSpeed: [ + 8, + ], + duration: 0, + castingDelay: 0 * 20, + cooldown: 0 * 20, + cooldown2: 6 * 20, + range: [ + 0, + ], + minRange: [-999], + bounceDistMin: 0, + bounceDistMax: 0, + bouncePower: 0, + targetCC: 0, + animationName: '', + causesFlameDeath: false, + modifiers: [], + modifiersSelf: [ + 'speedbuffgat', + ], + summonedUnits: [ + 'caltrop', + ], + summonsUseWaypoint: false, + summonsWaypointAMove: false, + ignoreSupplyCheck: false, + requiresVision: false, + }, + { + type: COMMAND.UPGRADE, + name: 'Research Spoked Wheel', + id_string: 'spokedwheel_cmd', + isCommand: true, + hotkey: 68, + targetIsPoint: false, + targetIsUnit: false, + isInstant: true, + isChanneled: false, + playLaunchSoundOnce: false, + useAoeCursor: false, + commandCard: 0, + interfacePosX: 0, + interfacePosY: 1, + requiredLevels: [], + learnCommandCard: 0, + learnInterfacePosX: 0, + learnInterfacePosY: 0, + image: 'mechSpeedUpg', + attackEffectInit: 'spell', + description: 'Cost: getField(spokedwheel_upgrade.cost) Gold#BRDuration: getField(spokedwheel_upgrade.buildTime) sec#BRgetField(spokedwheel_upgrade.description)', + requirementType: [], + requirementLevel: [], + requirementText: [], + targetRequirements1: [], + targetRequirements2: [], + targetRequirements3: [], + upgrade: 'spokedwheel_upgrade', + manaCost: [ + 0, + ], + goldCost: 0, + aoeRadius: [ + 0, + ], + damage: [ + 0, + ], + projectileDamage: [ + 0, + ], + projectileAoeRadius: [ + 0, + ], + maximizeRangeWhenCasting: false, + hitsFriendly: true, + hitsEnemy: true, + hitsSelf: true, + targetFilters: [], + targetFiltersExclude: [], + effectScale: 1, + hasAutocast: false, + autocastDefault: false, + autocastConditions: '', + projectileSpeed: [ + 8, + ], + duration: 0, + castingDelay: 0 * 20, + cooldown: 0 * 20, + cooldown2: 0 * 20, + range: [ + 0, + ], + minRange: [-999], + bounceDistMin: 0, + bounceDistMax: 0, + bouncePower: 0, + targetCC: 0, + animationName: '', + causesFlameDeath: false, + modifiers: [], + modifiersSelf: [], + summonedUnits: [], + summonsUseWaypoint: false, + summonsWaypointAMove: false, + ignoreSupplyCheck: false, + requiresVision: false, + }, + { + type: COMMAND.UPGRADE, + name: 'Research Bird Detection', + id_string: 'bird_detection_cmd', + isCommand: true, + hotkey: 83, + targetIsPoint: false, + targetIsUnit: false, + isInstant: true, + isChanneled: false, + playLaunchSoundOnce: false, + useAoeCursor: false, + commandCard: 0, + interfacePosX: 1, + interfacePosY: 1, + requiredLevels: [], + learnCommandCard: 0, + learnInterfacePosX: 0, + learnInterfacePosY: 0, + image: 'eye', + attackEffectInit: 'spell', + description: 'Cost: getField(birddetection.cost) Gold#BRDuration: getField(birddetection.buildTime) sec#BRgetField(birddetection.description)', + requirementType: [ + 'house', + ], + requirementLevel: [ + 1, + ], + requirementText: [ + 'Requires a House', + ], + targetRequirements1: [], + targetRequirements2: [], + targetRequirements3: [], + upgrade: 'birddetection', + manaCost: [ + 0, + ], + goldCost: 0, + aoeRadius: [ + 0, + ], + damage: [ + 0, + ], + projectileDamage: [ + 0, + ], + projectileAoeRadius: [ + 0, + ], + maximizeRangeWhenCasting: false, + hitsFriendly: true, + hitsEnemy: true, + hitsSelf: true, + targetFilters: [], + targetFiltersExclude: [], + effectScale: 1, + hasAutocast: false, + autocastDefault: false, + autocastConditions: '', + projectileSpeed: [ + 8, + ], + duration: 0, + castingDelay: 0 * 20, + cooldown: 0 * 20, + cooldown2: 0 * 20, + range: [ + 0, + ], + minRange: [-999], + bounceDistMin: 0, + bounceDistMax: 0, + bouncePower: 0, + targetCC: 0, + animationName: '', + causesFlameDeath: false, + modifiers: [], + modifiersSelf: [], + summonedUnits: [], + summonsUseWaypoint: false, + summonsWaypointAMove: false, + ignoreSupplyCheck: false, + requiresVision: false, + }, + { + type: COMMAND.MAKEUNIT, + name: 'Construct Gyrocraft', + id_string: 'construct_gyrocraft', + isCommand: true, + image: 'gyrocopter', + unitType: 'gyrocraft', + hotkey: KEY.Q, + targetIsPoint: false, + targetIsUnit: false, + isInstant: true, + isChanneled: false, + playLaunchSoundOnce: false, + useAoeCursor: false, + commandCard: 0, + interfacePosX: 0, + interfacePosY: 0, + requiredLevels: [], + learnCommandCard: 0, + learnInterfacePosX: 0, + learnInterfacePosY: 0, + learnHotkey: 69, + attackEffectInit: 'spell', + description: 'Cost: getField(gyrocraft.cost) Gold#BRSupply: getField(gyrocraft.supply)#BRDuration: getField(gyrocraft.buildTime) sec#BRgetField(gyrocraft.description)', + requirementType: [], + requirementLevel: [], + requirementText: [], + targetRequirements1: [], + targetRequirements2: [], + targetRequirements3: [], + manaCost: [ + 0, + ], + goldCost: 0, + aoeRadius: [ + 0, + ], + damage: [ + 0, + ], + projectileDamage: [ + 0, + ], + projectileAoeRadius: [ + 0, + ], + maximizeRangeWhenCasting: false, + hitsFriendly: true, + hitsEnemy: true, + hitsSelf: true, + targetFilters: [], + targetFiltersExclude: [], + effectScale: 1, + hasAutocast: false, + autocastDefault: false, + autocastConditions: '', + projectileSpeed: [ + 8, + ], + duration: 0, + castingDelay: 0 * 20, + cooldown: 0 * 20, + cooldown2: 0 * 20, + range: [ + 0, + ], + minRange: [-999], + bounceDistMin: 0, + bounceDistMax: 0, + bouncePower: 0, + targetCC: 0, + animationName: '', + causesFlameDeath: false, + modifiers: [], + modifiersSelf: [], + summonedUnits: [], + summonsUseWaypoint: false, + summonsWaypointAMove: false, + ignoreSupplyCheck: false, + requiresVision: false, + }, + { + type: COMMAND.MAKEBUILDING, + name: 'Build Mill', + id_string: 'buildmill', + isCommand: true, + unitType: 'mill', + hotkey: KEY.E, + targetIsPoint: true, + targetIsUnit: false, + isInstant: false, + isChanneled: false, + playLaunchSoundOnce: false, + useAoeCursor: false, + commandCard: 4, + interfacePosX: 2, + interfacePosY: 0, + requiredLevels: [], + learnCommandCard: 0, + learnInterfacePosX: 0, + learnInterfacePosY: 0, + learnHotkey: 83, + image: 'mill', + attackEffectInit: 'spell', + description: 'Cost: getField(mill.cost) Gold#BRDuration: getField(mill.buildTime) sec#BRgetField(mill.description)', + requirementType: [ + 'workshop', + ], + requirementLevel: [ + 1, + ], + requirementText: [ + 'Requires a workshop', + ], + targetRequirements1: [], + targetRequirements2: [], + targetRequirements3: [], + manaCost: [ + 0, + ], + goldCost: 0, + aoeRadius: [ + 0, + ], + damage: [ + 0, + ], + projectileDamage: [ + 0, + ], + projectileAoeRadius: [ + 0, + ], + maximizeRangeWhenCasting: false, + hitsFriendly: true, + hitsEnemy: true, + hitsSelf: true, + targetFilters: [], + targetFiltersExclude: [], + effectScale: 1, + hasAutocast: false, + autocastDefault: false, + autocastConditions: '', + projectileSpeed: [ + 8, + ], + duration: 0, + castingDelay: 0 * 20, + cooldown: 0 * 20, + cooldown2: 0 * 20, + range: [ + 0, + ], + minRange: [-999], + bounceDistMin: 0, + bounceDistMax: 0, + bouncePower: 0, + targetCC: 0, + animationName: '', + causesFlameDeath: false, + modifiers: [], + modifiersSelf: [], + summonedUnits: [], + summonsUseWaypoint: false, + summonsWaypointAMove: false, + ignoreSupplyCheck: false, + requiresVision: false, + }, + { + type: COMMAND.UNIVERSAL, + name: 'Sprint', + id_string: 'sprint', + isCommand: true, + hotkey: 81, + targetIsPoint: false, + targetIsUnit: false, + isInstant: true, + isChanneled: false, + playLaunchSoundOnce: false, + useAoeCursor: false, + commandCard: 0, + interfacePosX: 0, + interfacePosY: 0, + requiredLevels: [], + learnCommandCard: 0, + learnInterfacePosX: 0, + learnInterfacePosY: 0, + learnHotkey: 81, + image: 'beastSpeedUpg', + attackEffectInit: 'smoke', + attackEffect: null, + description: 'Cooldown: getField(sprint.cooldown2)#BRThe wolf sprints, temporaily running faster until getting tired and running slower.', + requirementType: [ + 'sprint_upgrade', + ], + requirementLevel: [ + 1, + ], + requirementText: [ + 'Research in the snake charmer.', + ], + targetRequirements1: [], + targetRequirements2: [], + targetRequirements3: [], + launchSound: 49, + manaCost: [ + 0, + ], + goldCost: 0, + aoeRadius: [ + 0, + ], + damage: [ + 0, + ], + projectileDamage: [ + 0, + ], + projectileAoeRadius: [ + 0, + ], + maximizeRangeWhenCasting: false, + hitsFriendly: false, + hitsEnemy: false, + hitsSelf: true, + targetFilters: [], + targetFiltersExclude: [], + effectScale: 1, + hasAutocast: false, + autocastDefault: false, + autocastConditions: '', + projectileSpeed: [ + 0, + ], + duration: 0, + castingDelay: 0 * 20, + cooldown: 0 * 20, + cooldown2: 40 * 20, + range: [ + 0, + ], + minRange: [-999], + bounceDistMin: 0, + bounceDistMax: 0, + bouncePower: 0, + targetCC: 0, + animationName: '', + causesFlameDeath: false, + modifiers: [ + 'speeddebuff', + ], + modifiersSelf: [ + 'speedbuff', + 'speeddebuff', + ], + summonedUnits: [], + summonsUseWaypoint: false, + summonsWaypointAMove: false, + ignoreSupplyCheck: false, + requiresVision: false, + }, + { + type: COMMAND.UPGRADE, + name: 'Research Wolf Sprint', + id_string: 'research_sprint', + isCommand: true, + hotkey: 81, + targetIsPoint: false, + targetIsUnit: false, + isInstant: true, + isChanneled: false, + playLaunchSoundOnce: false, + useAoeCursor: false, + commandCard: 0, + interfacePosX: 0, + interfacePosY: 0, + requiredLevels: [], + learnCommandCard: 0, + learnInterfacePosX: 0, + learnInterfacePosY: 0, + image: 'beastSpeedUpg', + attackEffectInit: 'spell', + description: 'Cost: getField(sprint_upgrade.cost) Gold#BRDuration: getField(sprint_upgrade.buildTime) sec#BRgetField(sprint_upgrade.description)', + requirementType: [ + 'fortress', + ], + requirementLevel: [ + 1, + ], + requirementText: [ + 'Requires a fortress.', + ], + targetRequirements1: [], + targetRequirements2: [], + targetRequirements3: [], + upgrade: 'sprint_upgrade', + manaCost: [ + 0, + ], + goldCost: 0, + aoeRadius: [ + 0, + ], + damage: [ + 0, + ], + projectileDamage: [ + 0, + ], + projectileAoeRadius: [ + 0, + ], + maximizeRangeWhenCasting: false, + hitsFriendly: true, + hitsEnemy: true, + hitsSelf: true, + targetFilters: [], + targetFiltersExclude: [], + effectScale: 1, + hasAutocast: false, + autocastDefault: false, + autocastConditions: '', + projectileSpeed: [ + 8, + ], + duration: 0, + castingDelay: 0 * 20, + cooldown: 0 * 20, + cooldown2: 0 * 20, + range: [ + 0, + ], + minRange: [-999], + bounceDistMin: 0, + bounceDistMax: 0, + bouncePower: 0, + targetCC: 0, + animationName: '', + causesFlameDeath: false, + modifiers: [], + modifiersSelf: [], + summonedUnits: [], + summonsUseWaypoint: false, + summonsWaypointAMove: false, + ignoreSupplyCheck: false, + requiresVision: false, + }, + { + type: COMMAND.UNIVERSAL, + name: 'Explosive Shot', + id_string: 'explosiveshot', + isCommand: true, + hotkey: KEY.Q, + targetIsPoint: false, + targetIsUnit: true, + isInstant: false, + isChanneled: false, + playLaunchSoundOnce: false, + useAoeCursor: false, + commandCard: 0, + interfacePosX: 0, + interfacePosY: 0, + requiredLevels: [], + learnCommandCard: 0, + learnInterfacePosX: 0, + learnInterfacePosY: 0, + learnHotkey: 81, + image: 'flakUpg', + attackEffectInit: 'spell', + attackEffect: 'flamestrike', + description: 'Cooldown: getField(aoeshot.cooldown2)#BRDamage: getField(aoeshot.damage)#BRSelect a flying target to shot a projectile that deals AOE damage.', + requirementType: [ + 'upgballistaexplosives', + ], + requirementLevel: [ + 1, + ], + requirementText: [ + 'Requires Ballista Black Powder research', + ], + targetRequirements1: [ + 'isFlying', + ], + targetRequirements2: [], + targetRequirements3: [], + launchSound: 20, + manaCost: [ + 0, + ], + goldCost: 0, + aoeRadius: [ + 1.5, + ], + damage: [ + 45, + ], + projectileDamage: [ + 0, + ], + projectileAoeRadius: [ + 0, + ], + maximizeRangeWhenCasting: false, + hitsFriendly: true, + hitsEnemy: true, + hitsSelf: false, + targetFilters: [ + 'flying', + ], + targetFiltersExclude: [], + effectScale: 1, + hasAutocast: false, + autocastDefault: false, + autocastConditions: '', + projectileSpeed: [ + 8, + ], + duration: 0, + castingDelay: 0 * 20, + cooldown: 0 * 20, + cooldown2: 10 * 20, + range: [ + 8, + ], + minRange: [-999], + bounceDistMin: 0, + bounceDistMax: 0, + bouncePower: 0, + targetCC: 0, + animationName: '', + causesFlameDeath: false, + modifiers: [], + modifiersSelf: [], + summonedUnits: [], + summonsUseWaypoint: false, + summonsWaypointAMove: false, + ignoreSupplyCheck: false, + requiresVision: false, + }, +]; + +var RampTilesEgyptData = [ + + new TileType({ + name: 'South West', + img: { x: 582, y: 422, w: 22, h: 76 }, + sizeX: 1, + sizeY: 3, + blocking: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'South East', + img: { x: 626, y: 422, w: 24, h: 76 }, + sizeX: 1, + sizeY: 3, + blocking: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'South Floor', + img: { x: 603, y: 420, w: 16, h: 78 }, + sizeX: 1, + sizeY: 3, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'South Floor 2', + img: { x: 608, y: 341, w: 16, h: 78 }, + sizeX: 1, + sizeY: 3, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'South Floor 3', + img: { x: 625, y: 341, w: 16, h: 78 }, + sizeX: 1, + sizeY: 3, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'West Top', + img: { x: 653, y: 445, w: 54, h: 47 }, + sizeX: 3, + sizeY: 1, + blocking: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'West Bottom', + img: { x: 712, y: 448, w: 52, h: 47 }, + sizeX: 3, + sizeY: 1, + blocking: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'West Floor', + img: { x: 655, y: 393, w: 55, h: 52 }, + sizeX: 3, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'West Floor 2', + img: { x: 711, y: 393, w: 55, h: 52 }, + sizeX: 3, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'West Floor 3', + img: { x: 655, y: 327, w: 55, h: 52 }, + sizeX: 3, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'East Top', + img: { x: 730, y: 275, w: 54, h: 45 }, + sizeX: 3, + sizeY: 1, + blocking: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'East Bottom', + img: { x: 673, y: 279, w: 52, h: 43 }, + sizeX: 3, + sizeY: 1, + blocking: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'East Floor', + img: { x: 722, y: 327, w: 50, h: 53 }, + sizeX: 3, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'East Floor 2', + img: { x: 672, y: 221, w: 50, h: 53 }, + sizeX: 3, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'East Floor 3', + img: { x: 727, y: 221, w: 50, h: 53 }, + sizeX: 3, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'North Left', + img: { x: 460, y: 359, w: 23, h: 51 }, + sizeX: 1, + sizeY: 3, + blocking: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'North Right', + img: { x: 492, y: 359, w: 21, h: 51 }, + sizeX: 1, + sizeY: 3, + blocking: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'North Floor', + img: { x: 513, y: 360, w: 16, h: 23 }, + sizeX: 1, + sizeY: 3, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'North Floor 2', + img: { x: 513, y: 386, w: 16, h: 23 }, + sizeX: 1, + sizeY: 3, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'North Floor 3', + img: { x: 530, y: 386, w: 16, h: 23 }, + sizeX: 1, + sizeY: 3, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + +]; + +/** + * Dependency on DataCore for SOUND + */ +var UnitData = [ + { + name: 'Worker', + id_string: 'worker', + hp: 120, + dmg: 8, + repairRate: 12 / 20, + repairIneffiency: 0.25, + isHuman: true, + supply: 1, + movementSpeed: 2.6 / 20, + weaponCooldown: 1 * 20, + weaponDelay: 0.3 * 20, + armor: 0, + range: 0.2, + size: 0.85, + vision: 8, + repairCost: 2.5 / 20, + circleSize: 0.43, + circleOffset: 0.125, + commands: { + stop: 'stop', + holdposition: 'holdposition', + attack: 'attack', + move: 'move', + moveto: 'moveto', + amove: 'amove', + mine: 'mine', + repair: 'repair', + buildcastle: 'buildcastle', + buildbarracks: 'buildbarracks', + buildwatchtower: 'buildwatchtower', + buildhouse: 'buildhouse', + buildmagesguild: 'buildmagesguild', + buildworkshop: 'buildworkshop', + buildforge: 'buildforge', + mech_buildings: 'mech_buildings', + builddragonslair: 'builddragonslair', + buildwolvesden: 'buildwolvesden', + buildanimaltestinglab: 'buildanimaltestinglab', + buildadvancedworkshop: 'buildadvancedworkshop', + buildchurch: 'buildchurch', + buildmill: 'buildmill', + buildarmory: 'buildarmory', + buildsnakecharmer: 'buildsnakecharmer', + basic_buildings: 'basic_buildings', + human_buildings: 'human_buildings', + beast_buildings: 'beast_buildings', + back_basic: 'back_basic', + back_humans: 'back_humans', + back_beast: 'back_beast', + back_mech: 'back_mech', + }, + buildTime: 27 * 20, + cost: 50, + healthbarOffset: 0.95, + healthbarWidth: 0.7, + img: 'worker', + description: 'Workers gather gold and construct buildings. They can also fight, but are not very good at it.', + tabPriority: 10, + drawOffsetY: 12.5, + meleeHitSound: 28, + meleeHitVolume: 0.6, + painSound: 1, + painSoundVolume: 0.4, + painSound2: 55, + painSoundVolume2: 1, + deathSound: 2, + isBiological: true, + cargoUse: 1, + isPassive: true, + bodyPower: 0.8, + }, + { + name: 'Soldier', + id_string: 'soldier', + hp: 220, + armor: 2, + movementSpeed: 2.5875 / 20, + dmg: 18, + tabPriority: 6, + isHuman: true, + supply: 2, + weaponCooldown: 1.15 * 20, + weaponDelay: 0.25 * 20, + armor: 1, + range: 0.2, + size: 0.95, + vision: 8, + circleSize: 0.43, + circleOffset: 0, + commands: { + stop: 'stop', + holdposition: 'holdposition', + attack: 'attack', + move: 'move', + moveto: 'moveto', + amove: 'amove', + }, + buildTime: 25 * 20, + cost: 80, + healthbarOffset: 1.1, + healthbarWidth: 0.7, + img: 'soldier', + description: 'Soldiers are basic melee combat units.', + tabPriority: 7, + drawOffsetY: 10, + meleeHitSound: 10, + meleeHitVolume: 1, + painSound: 1, + painSoundVolume: 0.4, + painSound2: 55, + painSoundVolume2: 1, + yesSound: 101, + yesSoundVolume: 1, + readySound: 101, + readySoundVolume: 1, + deathSound: 2, + isBiological: true, + cargoUse: 2, + }, + { + name: 'Archer', + id_string: 'archer', + hp: 160, + movementSpeed: 2.52 / 20, + dmg: 12, + buildTime: 22 * 20, + isHuman: true, + supply: 2, + weaponCooldown: 1.4 * 20, + weaponDelay: 0.1 * 20, + armor: 0, + range: 5, + size: 0.95, + vision: 8, + projectileSpeed: 12, + projectileStartHeight: 0.25, + attackLaunchSound: 3, + circleSize: 0.43, + circleOffset: 0, + commands: { + stop: 'stop', + holdposition: 'holdposition', + attack: 'attack', + move: 'move', + moveto: 'moveto', + amove: 'amove', + }, + cost: 80, + healthbarOffset: 1.45, + healthbarWidth: 0.7, + img: 'archer', + description: 'Archers are weaker than Soldiers, but can shoot over distance.', + projectileLen: 0.2, + tabPriority: 6, + drawOffsetY: 10, + painSound: 1, + painSoundVolume: 0.4, + painSound2: 55, + painSoundVolume2: 1, + yesSound: 91, + yesSoundVolume: 1, + readySound: 90, + readySoundVolume: 1, + deathSound: 2, + bodyPower: 0.8, + isBiological: true, + canAttackFlying: true, + cargoUse: 2, + attackEffect: 'arrow', + }, + { + name: 'Raider', + id_string: 'raider', + hp: 165, + startHp: 0, + mana: 50, + startMana: 25, + hpRegenerationRate: 1 / 20, + manaRegenerationRate: 0.9375 / 20, + armor: 0, + supply: 2, + supplyProvided: 0, + movementSpeed: 3.2 / 20, + weaponCooldown: 2.79 * 20, + weaponDelay: 0.25 * 20, + dmg: 30, + dmgModifierAttributes: [ + 'isBiological', + 'isMechanical', + ], + dmgModifierAddition: [ + 14, + 16, + ], + dmgModifierMultiplier: [ + 1, + 1, + ], + lifesteal: 0, + armorPenetration: 0, + percDmg: 0, + dmgCap: 1, + bouncePower: 0, + bounceDistMin: 0, + bounceDistMax: 0, + range: 0.2, + minRange: -999, + aoeRadius: 0, + attackPrio: 10, + size: 0.95, + imageScale: 1, + vision: 8, + repairRate: 0 / 20, + projectileSpeed: 8, + projectileLen: 0.2, + attackLaunchSound: 0, + circleSize: 0.55, + circleOffset: 0, + buildTime: 32 * 20, + cost: 110, + healthbarOffset: 1.1, + healthbarWidth: 0.7, + selectionOffsetY: 0, + img: 'raider', + description: 'Raiders are fast melee combat units with several abilities.', + experienceLevels: [], + experience: 0, + modifiersPerLevel: [], + experienceRange: 9, + tabPriority: 13, + drawOffsetY: 10, + attackEffect: null, + painSound: 1, + painSoundVolume: 0.4, + painSound2: 55, + painSoundVolume2: 1, + deathSound: 2, + yesSound: 100, + yesSoundVolume: 0.9, + readySound: 100, + readySoundVolume: 0.9, + bodyPower: 0.8, + dustCreationChance: 1 / 20, + visionHeightBonus: 0, + animSpeed: 1.5, + oscillationAmplitude: 0, + height: 0.3, + acceleration: 0, + angularVelocity: 0, + commands: { + stop: 'stop', + holdposition: 'holdposition', + attack: 'attack', + move: 'move', + moveto: 'moveto', + amove: 'amove', + Flash: 'Flash', + Shroud: 'Shroud', + }, + cargoUse: 2, + cargoSpace: 0, + projectileStartHeight: 0, + power: 0, + lifetime: 0, + goldReward: 0, + limit: 0, + modifiers: [], + modifiersSelf: [ + 'killflash', + ], + spawnModifiers: [ + 'raiderfix', + ], + hoverText: '', + canHaveWaypoint: false, + isPassive: false, + causesFlameDeath: false, + shootingReveals: false, + shootWhileMoving: false, + hitscan: false, + maximizeRangeWhenShooting: false, + hitsFriendly: false, + hitsEnemy: true, + canAttackGround: true, + canAttackFlying: false, + isHeatSeeking: true, + ignoreEnemyHitscan: false, + controllable: true, + hasDetection: false, + expOnlyFromOwnKills: false, + alliesGetExperience: false, + alliesGetGold: false, + isReflectingProjectiles: false, + isBlockingProjectiles: false, + takeDamageOnBlock: false, + preventsLoss: false, + flying: false, + uniqueAndHeroic: false, + isMechanical: false, + isUndead: false, + isBiological: true, + isBeast: false, + isHuman: false, + noShow: false, + noCollision: false, + isInvisible: false, + isInvincible: false, + spawnWithAMove: false, + }, + { + name: 'Mage', + id_string: 'mage', + hp: 160, + supply: 3, + mana: 100, + startMana: 50, + manaRegenerationRate: 1 / 20, + movementSpeed: 2.3 / 20, + weaponCooldown: 2 * 20, + weaponDelay: 0.2 * 20, + dmg: 10, + armor: 0, + range: 5, + size: 0.95, + vision: 8, + projectileSpeed: 10, + projectileStartHeight: 0.5, + attackLaunchSound: 22, + circleSize: 0.43, + circleOffset: 0.125, + commands: { + stop: 'stop', + holdposition: 'holdposition', + attack: 'attack', + move: 'move', + moveto: 'moveto', + amove: 'amove', + summonslowfield: 'summonslowfield', + fireball: 'fireball', + }, + buildTime: 35 * 20, + cost: 125, + healthbarOffset: 1.25, + healthbarWidth: 0.7, + img: 'mage', + description: 'Mages can cast several spells.', + tabPriority: 15, + drawOffsetY: 12, + painSound: 1, + painSoundVolume: 0.4, + painSound2: 55, + painSoundVolume2: 1, + yesSound: 98, + yesSoundVolume: 1, + readySound: 97, + readySoundVolume: 1, + deathSound: 2, + isBiological: true, + canAttackFlying: true, + cargoUse: 2, + bodyPower: 0.8, + attackEffect: 'mageAttack', + isHuman: true, + }, + { + name: 'Priest', + id_string: 'priest', + hp: 160, + supply: 3, + mana: 100, + startMana: 50, + manaRegenerationRate: 0.8 / 20, + movementSpeed: 2.3 / 20, + weaponCooldown: 2 * 20, + weaponDelay: 0.2 * 20, + dmg: 10, + armor: 0, + range: 5, + size: 0.95, + vision: 8, + projectileSpeed: 10, + attackLaunchSound: 22, + circleSize: 0.43, + circleOffset: -0.05, + commands: { + stop: 'stop', + holdposition: 'holdposition', + attack: 'attack', + move: 'move', + moveto: 'moveto', + amove: 'amove', + summonhealingward: 'summonhealingward', + invisibilityspell: 'invisibilityspell', + }, + buildTime: 30 * 20, + cost: 100, + healthbarOffset: 1.2, + healthbarWidth: 0.7, + img: 'priest', + description: 'Priests can cast several spells.', + tabPriority: 14, + drawOffsetY: 9, + painSound: 1, + painSoundVolume: 0.4, + painSound2: 55, + painSoundVolume2: 1, + deathSound: 2, + yesSound: 99, + yesSoundVolume: 1, + readySound: 99, + readySoundVolume: 1, + isBiological: true, + canAttackFlying: true, + cargoUse: 2, + bodyPower: 0.8, + attackEffect: 'mageAttack', + isHuman: true, + }, + { + name: 'Wolf', + id_string: 'wolf', + hp: 160, + hpRegenerationRate: 1 / 20, + supply: 2, + movementSpeed: 3.45 / 20, + weaponCooldown: 1.15 * 20, + weaponDelay: 0.25 * 20, + dmg: 10, + armor: 0, + range: 0.2, + size: 0.85, + imageScale: 0.89, + vision: 8, + circleSize: 0.47, + circleOffset: -0.1, + spawnWithAMove: true, + commands: { + stop: 'stop', + holdposition: 'holdposition', + attack: 'attack', + move: 'move', + moveto: 'moveto', + amove: 'amove', + sprint: 'sprint', + }, + buildTime: 17 * 20, + cost: 45, + healthbarOffset: 1.4, + healthbarWidth: 0.7, + img: 'wolf', + description: 'Wolves are fast melee units.', + tabPriority: 6, + drawOffsetY: 7, + meleeHitSound: 46, + meleeHitVolume: 0.7, + painSound: 47, + painSoundVolume: 0.8, + painSound2: 55, + painSoundVolume2: 1, + yesSound: 43, + yesSoundVolume: 1, + deathSound: 45, + readySound: 44, + readySoundVolume: 1, + isBiological: true, + bodyPower: 0.5, + cargoUse: 1, + isBeast: true, + lifesteal: 0.01, + }, + { + name: 'Bird', + id_string: 'bird', + hp: 75, + hpRegenerationRate: 0.5 / 20, + supply: 1, + movementSpeed: 3 / 20, + armor: 0, + size: 0.95, + vision: 10, + circleSize: 0.6, + circleOffset: -2.9, + commands: { + stop: 'stop', + holdposition: 'holdposition', + move: 'move', + moveto: 'moveto', + }, + buildTime: 20 * 20, + cost: 75, + healthbarOffset: 3.8, + healthbarWidth: 0.8, + selectionOffsetY: 3, + img: 'bird', + description: 'Birds are fast flying units that can be used for scouting.', + tabPriority: 4, + drawOffsetY: 10, + painSound: 55, + painSoundVolume: 0.7, + deathSound: 71, + yesSound: 70, + yesSoundVolume: 1, + readySound: 69, + readySoundVolume: 1, + dustCreationChance: -1 / 20, + flying: true, + isBiological: true, + isBeast: true, + visionHeightBonus: 1, + animSpeed: 3, + oscillationAmplitude: 0.05, + height: 3.3, + acceleration: 0.05, + angularVelocity: 0.2, + removeAfterDeadAnimation: true, + deathAnimationSpeed: 0.5, + bodyPower: 0.25, + slamSound: 72, + }, + { + name: 'Snake', + id_string: 'snake', + hp: 90, + startHp: 0, + mana: 0, + startMana: 0, + hpRegenerationRate: 0.5 / 20, + armor: 0, + supply: 2, + supplyProvided: 0, + movementSpeed: 2.99 / 20, + weaponCooldown: 0.67 * 20, + weaponDelay: 0.15 * 20, + dmg: 7, + dmgModifierAttributes: [ + 'flying', + ], + dmgModifierAddition: [-1.5], + dmgModifierMultiplier: [ + 1, + ], + lifesteal: 0, + armorPenetration: 0, + percDmg: 0, + dmgCap: 1, + bouncePower: 0, + bounceDistMin: 0, + bounceDistMax: 0, + range: 3, + minRange: -999, + aoeRadius: 0, + attackPrio: 10, + size: 0.95, + imageScale: 0.55, + vision: 8, + repairRate: 0 / 20, + projectileSpeed: 8, + projectileLen: 0.2, + attackLaunchSound: 22, + circleSize: 0.43, + circleOffset: 0.125, + buildTime: 19 * 20, + cost: 50, + healthbarOffset: 1, + healthbarWidth: 0.7, + selectionOffsetY: 0, + img: 'snake', + description: 'Snakes are vulnerable but quick ranged beasts.', + experienceLevels: [], + experience: 0, + modifiersPerLevel: [], + experienceRange: 9, + tabPriority: 6, + drawOffsetY: 45, + attackEffect: 'mageAttack', + painSound: 61, + painSoundVolume: 0.5, + painSound2: 55, + painSoundVolume2: 1, + deathSound: 59, + yesSound: 60, + yesSoundVolume: 0.8, + readySound: 59, + readySoundVolume: 0.9, + bodyPower: 0.8, + dustCreationChance: 1 / 20, + visionHeightBonus: 0, + animSpeed: 1, + oscillationAmplitude: 0, + height: 0.3, + acceleration: 0, + angularVelocity: 0, + commands: { + stop: 'stop', + holdposition: 'holdposition', + attack: 'attack', + move: 'move', + moveto: 'moveto', + amove: 'amove', + }, + cargoUse: 1, + cargoSpace: 0, + projectileStartHeight: 0, + power: 0, + lifetime: 0, + goldReward: 0, + limit: 0, + modifiers: [], + modifiersSelf: [], + spawnModifiers: [], + hoverText: '', + canHaveWaypoint: false, + isPassive: false, + causesFlameDeath: false, + shootingReveals: false, + shootWhileMoving: false, + hitscan: false, + maximizeRangeWhenShooting: false, + hitsFriendly: true, + hitsEnemy: true, + canAttackGround: true, + canAttackFlying: true, + isHeatSeeking: true, + ignoreEnemyHitscan: false, + controllable: true, + hasDetection: false, + expOnlyFromOwnKills: false, + alliesGetExperience: false, + alliesGetGold: false, + isReflectingProjectiles: false, + isBlockingProjectiles: false, + takeDamageOnBlock: false, + preventsLoss: false, + flying: false, + uniqueAndHeroic: false, + isMechanical: false, + isUndead: false, + isBiological: true, + isBeast: true, + isHuman: false, + noShow: false, + noCollision: false, + isInvisible: false, + isInvincible: false, + spawnWithAMove: false, + }, + { + name: 'Dragon', + id_string: 'dragon', + hp: 265, + hpRegenerationRate: 0.8 / 20, + supply: 4, + movementSpeed: 3.52 / 20, + weaponCooldown: 1.65 * 20, + weaponDelay: 0.05 * 20, + dmg: 14, + armor: 1, + range: 4.5, + aoeRadius: 0.2, + size: 2.9, + vision: 10, + projectileSpeed: 6, + attackLaunchSound: 41, + circleSize: 1.8, + circleOffset: -3, + commands: { + stop: 'stop', + holdposition: 'holdposition', + attack: 'attack', + move: 'move', + moveto: 'moveto', + amove: 'amove', + }, + buildTime: 30 * 20, + cost: 145, + healthbarOffset: 5.5, + healthbarWidth: 1.6, + selectionOffsetY: 3, + img: 'dragon', + description: 'Dragons are big, flying units.', + tabPriority: 6, + drawOffsetY: 34, + painSound: 55, + painSoundVolume: 1, + deathSound: 39, + yesSound: 37, + yesSoundVolume: 1, + readySound: 38, + readySoundVolume: 1, + dustCreationChance: -1 / 20, + flying: true, + isBiological: true, + isBeast: true, + visionHeightBonus: 2, + animSpeed: 3, + projectileStartHeight: 3, + oscillationAmplitude: 0.15, + canAttackFlying: true, + height: 3.3, + acceleration: 0.03, + angularVelocity: 0.0, + removeAfterDeadAnimation: true, + shootingReveals: true, + causesFlameDeath: true, + attackEffect: 'dragonAttack', + }, + { + name: 'Werewolf', + id_string: 'werewolf', + hp: 440, + supply: 6, + hpRegenerationRate: 1.8 / 20, + movementSpeed: 2.52 / 20, + weaponCooldown: 2 * 20, + weaponDelay: 0 * 20, + dmg: 40, + armor: 4, + range: 1.0, + size: 1.75, + imageScale: 0.9, + vision: 8, + circleSize: 0.93, + circleOffset: 0, + commands: { + stop: 'stop', + holdposition: 'holdposition', + attack: 'attack', + move: 'move', + moveto: 'moveto', + amove: 'amove', + smash: 'smash', + }, + buildTime: 40 * 20, + cost: 225, + healthbarOffset: 2.6, + healthbarWidth: 1, + img: 'werewolf', + description: 'Werewolves are very strong melee units.', + tabPriority: 6, + drawOffsetY: 21, + painSound: 47, + painSoundVolume: 0.5, + painSound2: 55, + painSoundVolume2: 1, + meleeHitSound: 51, + meleeHitVolume: 1, + yesSound: 53, + yesSoundVolume: 1, + deathSound: 52, + readySound: 54, + readySoundVolume: 1, + isBiological: true, + isBeast: true, + cargoUse: 4, + animSpeed: 3, + bodyPower: 2.5, + deathAnimationSpeed: 0.2, + power: 2, + }, + { + name: 'Gatling Gun', + id_string: 'gatlinggun', + hp: 175, + startHp: 0, + mana: 30, + startMana: 10, + hpRegenerationRate: 0 / 20, + manaRegenerationRate: 0.13 / 20, + armor: 0, + supply: 3, + supplyProvided: 0, + movementSpeed: 2.9 / 20, + weaponCooldown: 1.07 * 20, + weaponDelay: 0.05 * 20, + dmg: 13, + dmgModifierAttributes: [], + dmgModifierAddition: [], + dmgModifierMultiplier: [], + lifesteal: 0, + armorPenetration: 5, + percDmg: 0, + dmgCap: 1, + bouncePower: 0, + bounceDistMin: 0, + bounceDistMax: 0, + range: 3, + minRange: -999, + aoeRadius: 0, + attackPrio: 10, + size: 1.01, + imageScale: 0.9, + vision: 8, + repairRate: 0 / 20, + projectileSpeed: 14, + projectileLen: 0.4, + attackLaunchSound: 25, + circleSize: 0.7, + circleOffset: 0, + buildTime: 26 * 20, + cost: 95, + healthbarOffset: 1.1, + healthbarWidth: 1, + selectionOffsetY: 0, + img: 'gatling_gun', + description: 'Gatling guns are quick and strong mechanical units', + experienceLevels: [], + experience: 0, + modifiersPerLevel: [], + experienceRange: 9, + tabPriority: 7, + drawOffsetY: 19, + attackEffect: 'arrow', + painSound: 24, + painSoundVolume: 1, + painSound2: 0, + painSoundVolume2: 1, + deathSound: 29, + yesSound: 95, + yesSoundVolume: 0.9, + readySound: 95, + readySoundVolume: 0.9, + bodyPower: 2, + dustCreationChance: 2 / 20, + visionHeightBonus: 0, + animSpeed: 1.5, + oscillationAmplitude: 0, + height: 0.3, + acceleration: 0, + angularVelocity: 0, + commands: { + stop: 'stop', + holdposition: 'holdposition', + attack: 'attack', + move: 'move', + moveto: 'moveto', + amove: 'amove', + dropcaltrops: 'dropcaltrops', + }, + cargoUse: 2, + cargoSpace: 0, + projectileStartHeight: 0.1, + power: 2, + lifetime: 0, + goldReward: 0, + limit: 0, + modifiers: [], + modifiersSelf: [], + spawnModifiers: [], + hoverText: '', + canHaveWaypoint: false, + isPassive: false, + causesFlameDeath: false, + shootingReveals: false, + shootWhileMoving: false, + hitscan: false, + maximizeRangeWhenShooting: false, + hitsFriendly: true, + hitsEnemy: true, + canAttackGround: true, + canAttackFlying: false, + isHeatSeeking: true, + ignoreEnemyHitscan: false, + controllable: true, + hasDetection: false, + expOnlyFromOwnKills: false, + alliesGetExperience: false, + alliesGetGold: false, + isReflectingProjectiles: false, + isBlockingProjectiles: false, + takeDamageOnBlock: false, + preventsLoss: false, + flying: false, + uniqueAndHeroic: false, + isMechanical: true, + isUndead: false, + isBiological: false, + isBeast: false, + isHuman: false, + noShow: false, + noCollision: false, + isInvisible: false, + isInvincible: false, + spawnWithAMove: false, + }, + { + name: 'Catapult', + id_string: 'catapult', + hp: 220, + supply: 3, + movementSpeed: 2 / 20, + weaponCooldown: 3 * 20, + weaponDelay: 0.65 * 20, + dmg: 58, + armor: 2, + range: 10, + minRange: 4.5, + aoeRadius: 0.5, + size: 1.5, + vision: 8, + projectileSpeed: 8.8, + attackLaunchSound: 25, + bounceDistMin: 2, + bounceDistMax: 4.5, + circleSize: 0.88, + circleOffset: 0, + commands: { + stop: 'stop', + holdposition: 'holdposition', + attack: 'attack', + move: 'move', + moveto: 'moveto', + amove: 'amove', + attackground: 'attackground', + }, + buildTime: 30 * 20, + isHeatSeeking: false, + cost: 125, + healthbarOffset: 1.3, + healthbarWidth: 1, + img: 'catapult', + description: 'Catapults can sling powerful rocks from a great distance, but cannot defend themselves against close-range enemies.', + tabPriority: 9, + drawOffsetY: 13, + painSound: 24, + painSoundVolume: 1, + yesSound: 94, + yesSoundVolume: 1, + readySound: 94, + readySoundVolume: 1, + deathSound: 29, + bodyPower: 2, + power: 2, + isMechanical: true, + dustCreationChance: 2 / 20, + cargoUse: 4, + attackEffect: 'launchedRock', + }, + { + name: 'Airship', + id_string: 'airship', + hp: 220, + supply: 2, + movementSpeed: 3.4 / 20, + weaponCooldown: 1.65 * 20, + weaponDelay: 0.05 * 20, + dmg: 0, + armor: 0, + range: 0, + size: 3.28, + imageScale: 0.8, + vision: 10, + projectileSpeed: 6, + attackLaunchSound: 41, + circleSize: 1.8, + circleOffset: -2.5, + commands: { + stop: 'stop', + holdposition: 'holdposition', + move: 'move', + moveto: 'moveto', + loadin: 'loadin', + unload: 'unload', + directunload: 'directunload', + }, + buildTime: 32 * 20, + cost: 100, + healthbarOffset: 5.3, + healthbarWidth: 1.6, + selectionOffsetY: 3, + img: 'airship', + description: 'Airships can transport your ground units.', + tabPriority: 1, + drawOffsetY: 34, + painSound: 24, + painSoundVolume: 1, + yesSound: 89, + yesSoundVolume: 1, + readySound: 89, + readySoundVolume: 1, + deathSound: 29, + dustCreationChance: -1 / 20, + flying: true, + isMechanical: true, + isBiological: false, + visionHeightBonus: 2, + animSpeed: 2, + oscillationAmplitude: 0.07, + height: 3.3, + acceleration: 0.05, + angularVelocity: 0.0, + removeAfterDeadAnimation: true, + cargoSpace: 8, + deathAnimationSpeed: 0.3, + isPassive: true, + onDamageModifiers: [], + }, + { + name: 'Gyrocraft', + id_string: 'gyrocraft', + hp: 175, + startHp: 0, + mana: 0, + startMana: 0, + hpRegenerationRate: 0 / 20, + manaRegenerationRate: 0.13 / 20, + armor: 0, + supply: 3, + supplyProvided: 0, + movementSpeed: 4 / 20, + weaponCooldown: 1.5 * 20, + weaponDelay: 0.05 * 20, + dmg: 12, + dmgModifierAttributes: [], + dmgModifierAddition: [], + dmgModifierMultiplier: [], + lifesteal: 0, + armorPenetration: 0, + percDmg: 0, + dmgCap: 1, + bouncePower: 0, + bounceDistMin: 0, + bounceDistMax: 0, + range: 2.5, + minRange: -999, + aoeRadius: 0, + attackPrio: 10, + size: 1.65, + imageScale: 0.95, + vision: 9, + repairRate: 0 / 20, + projectileSpeed: 14, + projectileLen: 0.4, + attackLaunchSound: 25, + circleSize: 1, + circleOffset: -1.95, + buildTime: 30 * 20, + cost: 110, + healthbarOffset: 3.6, + healthbarWidth: 1, + selectionOffsetY: 1.7, + img: 'gyrocopter', + description: 'Gyrocrafts are speedy flying mechanical units.', + experienceLevels: [], + experience: 0, + modifiersPerLevel: [], + experienceRange: 9, + tabPriority: 6, + drawOffsetY: 20, + attackEffect: 'arrow', + painSound: 103, + painSoundVolume: 1, + painSound2: 0, + painSoundVolume2: 1, + deathSound: 29, + yesSound: 102, + yesSoundVolume: 0.9, + readySound: 96, + readySoundVolume: 0.9, + bodyPower: 2, + dustCreationChance: 2 / 20, + visionHeightBonus: 2, + animSpeed: 1.3, + oscillationAmplitude: 0.12, + height: 2, + acceleration: 0.06, + angularVelocity: 0, + commands: { + stop: 'stop', + holdposition: 'holdposition', + attack: 'attack', + move: 'move', + moveto: 'moveto', + amove: 'amove', + }, + cargoUse: -1, + cargoSpace: 0, + projectileStartHeight: 4, + power: 2, + lifetime: 0, + goldReward: 0, + limit: 0, + modifiers: [], + modifiersSelf: [], + spawnModifiers: [], + hoverText: '', + canHaveWaypoint: false, + isPassive: false, + causesFlameDeath: false, + shootingReveals: true, + shootWhileMoving: false, + hitscan: false, + maximizeRangeWhenShooting: false, + hitsFriendly: true, + hitsEnemy: true, + canAttackGround: true, + canAttackFlying: true, + isHeatSeeking: true, + ignoreEnemyHitscan: false, + controllable: true, + hasDetection: false, + expOnlyFromOwnKills: false, + alliesGetExperience: false, + alliesGetGold: false, + isReflectingProjectiles: false, + isBlockingProjectiles: false, + takeDamageOnBlock: false, + preventsLoss: false, + flying: true, + uniqueAndHeroic: false, + isMechanical: true, + isUndead: false, + isBiological: false, + isBeast: false, + isHuman: false, + noShow: false, + noCollision: false, + isInvisible: false, + isInvincible: false, + spawnWithAMove: false, + }, + { + name: 'Ballista', + id_string: 'ballista', + hp: 250, + supply: 4, + movementSpeed: 2.1275 / 20, + weaponCooldown: 2.5 * 20, + weaponDelay: 0.3 * 20, + dmg: 45, + armor: 4, + range: 6, + size: 1.7, + vision: 8, + projectileSpeed: 14, + attackLaunchSound: 25, + circleSize: 1.1, + circleOffset: 0, + commands: { + stop: 'stop', + holdposition: 'holdposition', + attack: 'attack', + move: 'move', + moveto: 'moveto', + amove: 'amove', + explosiveshot: 'explosiveshot', + }, + buildTime: 32 * 20, + cost: 150, + healthbarOffset: 1.8, + healthbarWidth: 1, + img: 'ballista', + description: 'Ballistas are slow but strong anti air units.', + tabPriority: 8, + drawOffsetY: 20, + painSound: 24, + painSoundVolume: 1, + yesSound: 92, + yesSoundVolume: 1, + readySound: 92, + readySoundVolume: 1, + deathSound: 29, + bodyPower: 2, + isMechanical: true, + isBiological: false, + dustCreationChance: 2 / 20, + cargoUse: 4, + canAttackFlying: true, + canAttackGround: false, + attackEffect: 'ballista', + projectileStartHeight: 1, + power: 2, + }, + { + name: 'Skeleton', + id_string: 'skeleton', + hp: 230, + supply: 0, + movementSpeed: 2.07 / 20, + weaponCooldown: 1.25 * 20, + weaponDelay: 0.25 * 20, + dmg: 18, + armor: 0, + range: 0.2, + size: 0.95, + vision: 8, + circleSize: 0.43, + circleOffset: 0.125, + commands: { + stop: 'stop', + holdposition: 'holdposition', + attack: 'attack', + move: 'move', + moveto: 'moveto', + amove: 'amove', + }, + buildTime: 23 * 20, + cost: 0, + healthbarOffset: 1, + healthbarWidth: 0.7, + img: 'skeleton', + description: 'Skeletons are basic melee combat units. They are undead, that means they take damage from healing spells.', + tabPriority: 7, + drawOffsetY: 6, + meleeHitSound: 10, + meleeHitVolume: 1, + painSound: 61, + painSoundVolume: 0.5, + painSound2: 55, + painSoundVolume2: 1, + yesSound: 60, + yesSoundVolume: 0.8, + deathSound: 59, + readySound: 59, + cargoUse: 2, + lifetime: 700, + isBiological: false, + isUndead: true, + summonTime: 40, + }, + { + name: 'Healing Ward', + id_string: 'healingward', + hp: 50, + supply: 0, + movementSpeed: 0 / 20, + armor: 2, + size: 0.85, + vision: 7, + circleSize: 0.6, + circleOffset: 0, + commands: {}, + buildTime: 45 * 20, + cost: 0, + healthbarOffset: 1.75, + healthbarWidth: 0.7, + img: 'totem', + description: 'Healing Wards heal nearby allied non-undead units and damage enemy undead units.', + tabPriority: 4, + drawOffsetY: 7.5, + painSound: 24, + painSoundVolume: 1, + deathSound: 29, + dustCreationChance: -1 / 20, + animSpeed: 5, + deathAnimationSpeed: 0.3, + spawnModifiers: [ + 'healaura', + 'healauradmg', + ], + lifetime: 225, + attackPrio: 6, + idleFrames: [ + 0, + 1, + 2, + 3, + 2, + 1, + ], + summonTime: 16, + noMoveWhenHit: true, + power: 2, + isBiological: false, + }, + { + name: 'Slowing Field', + id_string: 'slowingfield', + hp: 999999, + supply: 0, + movementSpeed: 0 / 20, + armor: 2, + size: 0.85, + vision: -1, + circleSize: 0.6, + circleOffset: 0, + commands: {}, + buildTime: 45 * 20, + cost: 0, + healthbarOffset: 1.75, + healthbarWidth: 0.7, + img: 'totem', + description: 'Creates an aura that slows down the movement speed of ground units.', + tabPriority: 2, + drawOffsetY: 7.5, + dustCreationChance: -1 / 20, + animSpeed: 5, + isInvincible: true, + noShow: true, + noCollision: true, + deathAnimationSpeed: 0.3, + spawnModifiers: [ + 'slowfield', + ], + lifetime: 500, + attackPrio: 6, + idleFrames: [ + 0, + 1, + 2, + 3, + 2, + 1, + ], + summonTime: 16, + noMoveWhenHit: true, + power: 2, + }, + { + name: 'Shroud Field', + id_string: 'shroudfield', + hp: 999999, + startHp: 0, + mana: 0, + startMana: 0, + hpRegenerationRate: 0 / 20, + manaRegenerationRate: 0 / 20, + armor: 0, + supply: 0, + supplyProvided: 0, + movementSpeed: 0 / 20, + weaponCooldown: 1 * 20, + weaponDelay: 1 * 20, + dmg: 10, + dmgModifierAttributes: [], + dmgModifierAddition: [], + dmgModifierMultiplier: [], + lifesteal: 0, + armorPenetration: 0, + percDmg: 0, + dmgCap: 1, + bouncePower: 0, + bounceDistMin: 0, + bounceDistMax: 0, + range: 0.2, + minRange: -999, + aoeRadius: 0, + attackPrio: 6, + size: 0.01, + imageScale: 1, + vision: -1, + repairRate: 0 / 20, + projectileSpeed: 8, + projectileLen: 0.2, + attackLaunchSound: 0, + circleSize: 0.02, + circleOffset: 0, + buildTime: 45 * 20, + cost: 0, + healthbarOffset: 0.2, + healthbarWidth: 0.5, + selectionOffsetY: 0, + img: 'shroud', + description: 'Creates an aura that reduces all units under it to melee range.', + experienceLevels: [], + experience: 0, + modifiersPerLevel: [], + experienceRange: 9, + tabPriority: 4, + drawOffsetY: 46, + attackEffect: null, + painSound: 0, + painSoundVolume: 1, + painSound2: 0, + painSoundVolume2: 1, + deathSound: 0, + yesSound: 14, + yesSoundVolume: 0.6, + readySound: 13, + readySoundVolume: 0.9, + bodyPower: 0.8, + dustCreationChance: -1 / 20, + visionHeightBonus: 0, + animSpeed: 6, + oscillationAmplitude: 0, + height: 0.3, + acceleration: 0, + angularVelocity: 0, + commands: {}, + cargoUse: -1, + cargoSpace: 0, + projectileStartHeight: 0, + power: 2, + lifetime: 300, + goldReward: 0, + limit: 0, + modifiers: [], + modifiersSelf: [], + spawnModifiers: [ + 'raidershroudaura', + ], + hoverText: '', + canHaveWaypoint: false, + isPassive: false, + causesFlameDeath: false, + shootingReveals: false, + shootWhileMoving: false, + hitscan: false, + maximizeRangeWhenShooting: false, + hitsFriendly: true, + hitsEnemy: true, + canAttackGround: true, + canAttackFlying: false, + isHeatSeeking: true, + ignoreEnemyHitscan: false, + controllable: false, + hasDetection: false, + expOnlyFromOwnKills: false, + alliesGetExperience: false, + alliesGetGold: false, + isReflectingProjectiles: false, + isBlockingProjectiles: false, + takeDamageOnBlock: false, + preventsLoss: false, + flying: true, + uniqueAndHeroic: false, + isMechanical: false, + isUndead: false, + isBiological: true, + isBeast: false, + isHuman: false, + noShow: false, + noCollision: true, + isInvisible: false, + isInvincible: true, + spawnWithAMove: false, + }, + { + name: 'Caltrop', + id_string: 'caltrop', + hp: 12, + startHp: 0, + mana: 0, + startMana: 0, + hpRegenerationRate: 0 / 20, + manaRegenerationRate: 0 / 20, + armor: 4, + supply: 0, + supplyProvided: 0, + movementSpeed: 0 / 20, + weaponCooldown: 5 * 20, + weaponDelay: 0 * 20, + dmg: 0, + dmgModifierAttributes: [], + dmgModifierAddition: [], + dmgModifierMultiplier: [], + lifesteal: 0, + armorPenetration: 0, + percDmg: 0, + dmgCap: 1, + bouncePower: 0, + bounceDistMin: 0, + bounceDistMax: 0, + range: 0.3, + minRange: -999, + aoeRadius: 0, + attackPrio: 10, + size: 1, + imageScale: 1.0, + vision: 0.1, + repairRate: 0 / 20, + projectileSpeed: 8, + projectileLen: 0.2, + attackLaunchSound: 0, + circleSize: 0.43, + circleOffset: 0, + buildTime: 25 * 20, + cost: 0, + healthbarOffset: 1.1, + healthbarWidth: 0.4, + selectionOffsetY: 0, + img: 'caltropg', + description: 'Caltrops snare enemy units that walk over them, slowing them down.', + experienceLevels: [], + experience: 0, + modifiersPerLevel: [], + experienceRange: 9, + tabPriority: 4, + drawOffsetY: 10, + attackEffect: null, + painSound: 1, + painSoundVolume: 0.4, + painSound2: 55, + painSoundVolume2: 1, + deathSound: 2, + yesSound: 93, + yesSoundVolume: 0.6, + readySound: 93, + readySoundVolume: 0.9, + bodyPower: 0.8, + dustCreationChance: 1 / 20, + visionHeightBonus: 0, + animSpeed: 1.5, + oscillationAmplitude: 0, + height: 0.3, + acceleration: 0, + angularVelocity: 0, + commands: { + stop: 'stop', + attack: 'attack', + amove: 'amove', + }, + cargoUse: -1, + cargoSpace: 0, + projectileStartHeight: 0, + power: 2, + lifetime: 0, + goldReward: 0, + limit: 0, + modifiers: [ + 'trapped', + ], + modifiersSelf: [ + 'revealself', + ], + spawnModifiers: [], + hoverText: '', + canHaveWaypoint: false, + isPassive: false, + causesFlameDeath: false, + shootingReveals: false, + shootWhileMoving: false, + hitscan: false, + maximizeRangeWhenShooting: false, + hitsFriendly: true, + hitsEnemy: true, + canAttackGround: true, + canAttackFlying: false, + isHeatSeeking: true, + ignoreEnemyHitscan: false, + controllable: false, + hasDetection: false, + expOnlyFromOwnKills: false, + alliesGetExperience: false, + alliesGetGold: false, + isReflectingProjectiles: false, + isBlockingProjectiles: false, + takeDamageOnBlock: false, + preventsLoss: false, + flying: false, + uniqueAndHeroic: false, + isMechanical: true, + isUndead: false, + isBiological: false, + isBeast: false, + isHuman: false, + noShow: false, + noCollision: true, + isInvisible: true, + isInvincible: false, + spawnWithAMove: false, + }, +]; + +var CliffsData = [ + + new TileType({ + name: 'North', + img: { x: 2, y: 418, w: 22, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'West', + img: { x: 24, y: 418, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'East', + img: { x: 44, y: 418, w: 16, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'South', + img: { x: 62, y: 418, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(88, 80, 68)', + }), + + new TileType({ + name: 'SW', + img: { x: 85, y: 416, w: 22, h: 42 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'SE', + img: { x: 110, y: 416, w: 22, h: 42 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'NW', + img: { x: 135, y: 418, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'NE', + img: { x: 157, y: 418, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'SW2', + img: { x: 2, y: 460, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'SE2', + img: { x: 25, y: 460, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'NW2', + img: { x: 53, y: 460, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'NE2', + img: { x: 75, y: 460, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'Universal', + img: { x: 372, y: 168, w: 22, h: 46 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + +]; + +var GraveCliffData = [ + + new TileType({ + name: 'North', + img: { x: 390 + 380, y: 418, w: 22, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'West', + img: { x: 409 + 380, y: 413, w: 24, h: 47 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'East', + img: { x: 434 + 380, y: 413, w: 18, h: 47 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'South', + img: { x: 453 + 380, y: 418, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(88, 80, 68)', + }), + + new TileType({ + name: 'SW', + img: { x: 473 + 380, y: 416, w: 22, h: 42 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'SE', + img: { x: 496 + 380, y: 416, w: 25, h: 42 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'NW', + img: { x: 523 + 380, y: 418, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'NE', + img: { x: 543 + 380, y: 418, w: 24, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'SW2', + img: { x: 390 + 380, y: 460, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'SE2', + img: { x: 413 + 380, y: 460, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'NW2', + img: { x: 439 + 380, y: 460, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'NE2', + img: { x: 463 + 380, y: 460, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'Universal', + img: { x: 397 + 380, y: 168, w: 22, h: 46 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + +]; + +var RampTilesGraveData = [ + + new TileType({ + name: 'South West', + img: { x: 582 + 380, y: 422, w: 22, h: 76 }, + sizeX: 1, + sizeY: 3, + blocking: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'South East', + img: { x: 626 + 380, y: 422, w: 24, h: 76 }, + sizeX: 1, + sizeY: 3, + blocking: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'South Floor', + img: { x: 603 + 380, y: 420, w: 16, h: 78 }, + sizeX: 1, + sizeY: 3, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'South Floor 2', + img: { x: 608 + 380, y: 341, w: 16, h: 78 }, + sizeX: 1, + sizeY: 3, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'South Floor 3', + img: { x: 625 + 380, y: 341, w: 16, h: 78 }, + sizeX: 1, + sizeY: 3, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'West Top', + img: { x: 653 + 380, y: 445, w: 54, h: 47 }, + sizeX: 3, + sizeY: 1, + blocking: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'West Bottom', + img: { x: 712 + 380, y: 448, w: 52, h: 47 }, + sizeX: 3, + sizeY: 1, + blocking: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'West Floor', + img: { x: 655 + 380, y: 393, w: 55, h: 52 }, + sizeX: 3, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'West Floor 2', + img: { x: 711 + 380, y: 393, w: 55, h: 52 }, + sizeX: 3, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'West Floor 3', + img: { x: 655 + 380, y: 327, w: 55, h: 52 }, + sizeX: 3, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'East Top', + img: { x: 730 + 380, y: 275, w: 54, h: 45 }, + sizeX: 3, + sizeY: 1, + blocking: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'East Bottom', + img: { x: 673 + 380, y: 279, w: 52, h: 43 }, + sizeX: 3, + sizeY: 1, + blocking: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'East Floor', + img: { x: 722 + 380, y: 327, w: 50, h: 53 }, + sizeX: 3, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'East Floor 2', + img: { x: 672 + 380, y: 221, w: 50, h: 53 }, + sizeX: 3, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'East Floor 3', + img: { x: 727 + 380, y: 221, w: 50, h: 53 }, + sizeX: 3, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'North Left', + img: { x: 460 + 380, y: 359, w: 23, h: 51 }, + sizeX: 1, + sizeY: 3, + blocking: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'North Right', + img: { x: 492 + 380, y: 359, w: 21, h: 51 }, + sizeX: 1, + sizeY: 3, + blocking: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'North Floor', + img: { x: 513 + 380, y: 360, w: 16, h: 23 }, + sizeX: 1, + sizeY: 3, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'North Floor 2', + img: { x: 513 + 380, y: 386, w: 16, h: 23 }, + sizeX: 1, + sizeY: 3, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'North Floor 3', + img: { x: 530 + 380, y: 386, w: 16, h: 23 }, + sizeX: 1, + sizeY: 3, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + +]; + +var UpgradesData = [ + { + isUpgrade: true, + name: 'Research Shroud ', + id_string: 'raidershroudupgrade', + buildTime: 72 * 20, + cost: 100, + maxLevel: 1, + description: 'Allows your Raiders to cast Shroud.', + image: 'soot', + effectsTypes: [], + effectsFields: [], + effectsModifications: [], + effectsModsMultiplier: [], + noParallelResearch: false, + }, + + { + isUpgrade: true, + name: 'Spoked Wheel', + id_string: 'spokedwheel_upgrade', + buildTime: 36 * 20, + cost: 100, + maxLevel: 1, + description: 'Improves the movement speed of the Gatling Gun.', + image: 'mechSpeedUpg', + effectsTypes: [ + 'gatlinggun', + ], + effectsFields: [ + 'movementSpeed', + ], + effectsModifications: [ + 0.15 / 20, + ], + effectsModsMultiplier: [ + 1, + ], + noParallelResearch: false, + }, + + { + isUpgrade: true, + name: 'Bird Detection', + id_string: 'birddetection', + buildTime: 54 * 20, + cost: 100, + maxLevel: 1, + description: 'Allows your Birds to see invisible units.', + image: 'eye', + effectsTypes: [ + 'bird', + ], + effectsFields: [ + 'hasDetection', + ], + effectsModifications: [ + 1, + ], + effectsModsMultiplier: [ + 1, + ], + noParallelResearch: false, + }, + { + isUpgrade: true, + name: 'Research Sprint', + id_string: 'sprint_upgrade', + buildTime: 60 * 20, + cost: 150, + maxLevel: 1, + description: 'Allows your wolves to sprint.', + image: 'beastSpeedUpg', + effectsTypes: [], + effectsFields: [], + effectsModifications: [], + effectsModsMultiplier: [], + noParallelResearch: false, + }, + + { + name: 'Damage', + id_string: 'upgattack', + cost: 150, + buildTime: 90 * 20, + maxLevel: 5, + effectsTypes: [ + 'worker', + 'soldier', + 'archer', + 'mage', + 'priest', + 'raider', + 'upgattack', + 'upgattack', + ], + effectsFields: [ + 'dmg', + 'dmg', + 'dmg', + 'dmg', + 'dmg', + 'dmg', + 'buildTime', + 'cost', + ], + effectsModifications: [ + 1, + 1, + 1, + 1, + 1, + 3, + 20 * 20, + 50, + ], + effectsModsMultiplier: [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + ], + description: 'Increases the attack damage of your human units.', + image: 'attackUpg', + }, + + { + name: 'Armor', + id_string: 'upgarmor', + cost: 150, + buildTime: 90 * 20, + maxLevel: 5, + effectsTypes: [ + 'worker', + 'soldier', + 'archer', + 'mage', + 'priest', + 'raider', + 'upgarmor', + 'upgarmor', + ], + effectsFields: [ + 'armor', + 'armor', + 'armor', + 'armor', + 'armor', + 'armor', + 'buildTime', + 'cost', + ], + effectsModifications: [ + 1, + 1, + 1, + 1, + 1, + 1, + 20 * 20, + 50, + ], + effectsModsMultiplier: [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + ], + description: 'Increases the armor of your human units.', + image: 'armorUpg', + }, + + { + name: 'Research Fireball', + id_string: 'upgfireball', + cost: 100, + buildTime: 80 * 20, + maxLevel: 1, + description: 'Allows your Mages to cast Fireball.', + isUpgrade: true, + effectsTypes: [], + effectsFields: [], + effectsModifications: [], + effectsModsMultiplier: [], + image: 'flamestrike', + }, + + { + name: 'Research Heal', + id_string: 'upgheal', + cost: 100, + buildTime: 72 * 20, + maxLevel: 1, + description: 'Allows your Priests to cast Heal.', + effectsTypes: [ + 'upginvis', + 'upghealingward', + ], + effectsFields: [ + 'cost', + 'cost', + ], + effectsModifications: [ + 200, + 200, + ], + effectsModsMultiplier: [ + 1, + 1, + ], + image: 'heal', + }, + + { + name: 'Beast Attack', + id_string: 'upgbeastattack', + cost: 150, + buildTime: 90 * 20, + maxLevel: 5, + effectsTypes: [ + 'dragon', + 'wolf', + 'werewolf', + 'snake', + 'bird', + 'upgbeastattack', + 'upgbeastattack', + ], + effectsFields: [ + 'dmg', + 'dmg', + 'dmg', + 'dmg', + 'dmg', + 'buildTime', + 'cost', + ], + effectsModifications: [ + 1, + 1, + 2, + 1, + 0, + 20 * 20, + 50, + ], + effectsModsMultiplier: [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + ], + description: 'Increases the attack damage of your beast units.', + image: 'dragonAttUpg', + }, + + { + name: 'Beast Defense', + id_string: 'upgbeastdefense', + cost: 150, + buildTime: 90 * 20, + maxLevel: 5, + effectsTypes: [ + 'dragon', + 'wolf', + 'werewolf', + 'snake', + 'bird', + 'upgbeastdefense', + 'upgbeastdefense', + ], + effectsFields: [ + 'armor', + 'armor', + 'armor', + 'armor', + 'armor', + 'buildTime', + 'cost', + ], + effectsModifications: [ + 1, + 1, + 1, + 1, + 1, + 20 * 20, + 50, + ], + effectsModsMultiplier: [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + ], + description: 'Increases the armor of your beast units.', + image: 'dragonDefUpg', + }, + + { + name: 'Tower Upgrade', + id_string: 'upgtower', + cost: 225, + buildTime: 54 * 20, + maxLevel: 1, + effectsTypes: [ + 'watchtower', + 'watchtower', + 'watchtower', + 'watchtower2', + 'watchtower2', + 'watchtower2', + ], + effectsFields: [ + 'armor', + 'dmg', + 'range', + 'armor', + 'dmg', + 'range', + ], + effectsModifications: [ + 2, + 2, + 1, + 2, + 2, + 1, + ], + effectsModsMultiplier: [ + 1, + 1, + 1, + 1, + 1, + 1, + ], + description: 'Increases the armor, damage and range of your towers.', + image: 'towerUpg', + }, + + { + name: 'Speed', + id_string: 'upgspeed', + cost: 140, + buildTime: 90 * 20, + maxLevel: 5, + effectsTypes: [ + 'soldier', + 'archer', + 'mage', + 'priest', + 'upgspeed', + 'upgspeed', + ], + effectsFields: [ + 'movementSpeed', + 'movementSpeed', + 'movementSpeed', + 'movementSpeed', + 'buildTime', + 'cost', + 'buildTime', + 'cost', + 'buildTime', + 'cost', + 'buildTime', + 'cost', + ], + effectsModifications: [ + 0.25 / 20, + 0.25 / 20, + 0.2 / 20, + 0.2 / 20, + 20 * 20, + 60, + 20 * 20, + 60, + 20 * 20, + 60, + 20 * 20, + 60, + ], + effectsModsMultiplier: [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + ], + description: 'Increases the movement speed of your human units.', + image: 'speedUpg', + }, + + { + name: 'Beast Speed', + id_string: 'upgbeastspeed', + cost: 140, + buildTime: 90 * 20, + maxLevel: 5, + effectsTypes: [ + 'dragon', + 'wolf', + 'werewolf', + 'upgbeastspeed', + 'upgbeastspeed', + ], + effectsFields: [ + 'movementSpeed', + 'movementSpeed', + 'movementSpeed', + 'buildTime', + 'cost', + 'buildTime', + 'cost', + 'buildTime', + 'cost', + 'buildTime', + 'cost', + ], + effectsModifications: [ + 0.3 / 20, + 0.3 / 20, + 0.25 / 20, + 20 * 20, + 60, + 20 * 20, + 60, + 20 * 20, + 60, + 20 * 20, + 60, + ], + effectsModsMultiplier: [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + ], + description: 'Increases the movement speed of your beast units.', + image: 'beastSpeedUpg', + }, + + { + name: 'Mech Attack', + id_string: 'upgmechattack', + cost: 150, + buildTime: 90 * 20, + maxLevel: 5, + effectsTypes: [ + 'catapult', + 'ballista', + 'gatlinggun', + 'gyrocraft', + 'upgmechattack', + 'upgmechattack', + ], + effectsFields: [ + 'dmg', + 'dmg', + 'dmg', + 'dmg', + 'buildTime', + 'cost', + ], + effectsModifications: [ + 3, + 3, + 2, + 1.5, + 20 * 20, + 50, + ], + effectsModsMultiplier: [ + 1, + 1, + 1, + 1, + 1, + 1, + ], + description: 'Increases the attack damage of your mechanical units.', + image: 'mechAttUpg', + }, + + { + name: 'Mech Defense', + id_string: 'upgmechdefense', + cost: 150, + buildTime: 90 * 20, + maxLevel: 5, + effectsTypes: [ + 'catapult', + 'ballista', + 'gatlinggun', + 'gyrocraft', + 'airship', + 'upgmechdefense', + 'upgmechdefense', + ], + effectsFields: [ + 'armor', + 'armor', + 'armor', + 'armor', + 'armor', + 'buildTime', + 'cost', + ], + effectsModifications: [ + 1, + 1.5, + 1, + 1, + 1, + 20 * 20, + 50, + ], + effectsModsMultiplier: [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + ], + description: 'Increases the armor of your mechanical units.', + image: 'mechDefUpg', + }, + + { + name: 'Mech Speed', + id_string: 'upgmechspeed', + cost: 125, + buildTime: 90 * 20, + maxLevel: 5, + effectsTypes: [ + 'catapult', + 'airship', + 'ballista', + 'upgmechspeed', + 'upgmechspeed', + ], + effectsFields: [ + 'movementSpeed', + 'movementSpeed', + 'movementSpeed', + 'buildTime', + 'cost', + 'buildTime', + 'cost', + 'buildTime', + 'cost', + 'buildTime', + 'cost', + ], + effectsModifications: [ + 0.15 / 20, + 0.25 / 20, + 0.15 / 20, + 20 * 20, + 50, + 20 * 20, + 50, + 20 * 20, + 50, + 20 * 20, + 50, + ], + effectsModsMultiplier: [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + ], + description: 'Increases the movement speed of your mechanical units.', + image: 'mechSpeedUpg', + }, + + { + name: 'Ballista Black Powder', + id_string: 'upgballistaexplosives', + cost: 100, + buildTime: 45 * 20, + maxLevel: 1, + effectsTypes: [ + 'ballista', + ], + effectsFields: [ + 'aoeRadius', + ], + effectsModifications: [ + 0.3, + ], + effectsModsMultiplier: [ + 1, + ], + description: 'Adds splash damage to the default Ballista attack and gives them a powerful anti-air ability.', + image: 'flakUpg', + }, + + { + name: 'Research Shockwave', + id_string: 'upgshockwave', + cost: 100, + buildTime: 90 * 20, + maxLevel: 1, + description: 'Allows your Mages to cast Shockwave.', + effectsTypes: [ + 'upgheal', + 'upgfireball', + ], + effectsFields: [ + 'cost', + 'cost', + ], + effectsModifications: [ + 200, + 200, + ], + effectsModsMultiplier: [ + 1, + 1, + ], + image: 'shockwave', + }, + + { + name: 'Mech Range', + id_string: 'upgmechrange', + cost: 125, + buildTime: 90 * 20, + maxLevel: 5, + effectsTypes: [ + 'catapult', + 'ballista', + 'upgmechrange', + 'upgmechrange', + ], + effectsFields: [ + 'range', + 'range', + 'buildTime', + 'cost', + 'buildTime', + 'cost', + 'buildTime', + 'cost', + 'buildTime', + 'cost', + ], + effectsModifications: [ + 1, + 1, + 20 * 20, + 50, + 20 * 20, + 50, + 20 * 20, + 50, + 20 * 20, + 50, + ], + effectsModsMultiplier: [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + ], + description: 'Increases the range of your ranged mechanical units. Has no influence on melee units.', + image: 'mechRangeUpg', + }, + + { + name: 'Archer Range', + id_string: 'upgrange', + cost: 140, + buildTime: 90 * 20, + maxLevel: 1, + effectsTypes: [ + 'archer', + ], + effectsFields: [ + 'range', + ], + effectsModifications: [ + 1, + ], + effectsModsMultiplier: [ + 1, + ], + description: 'Increases the range of your archers.', + image: 'rangeUpg', + }, + + { + name: 'Beast Range', + id_string: 'upgbeastrange', + cost: 140, + buildTime: 90 * 20, + maxLevel: 5, + effectsTypes: [ + 'dragon', + 'upgbeastrange', + 'upgbeastrange', + ], + effectsFields: [ + 'range', + 'buildTime', + 'cost', + 'buildTime', + 'cost', + 'buildTime', + 'cost', + 'buildTime', + 'cost', + ], + effectsModifications: [ + 1, + 20 * 20, + 60, + 20 * 20, + 60, + 20 * 20, + 60, + 20 * 20, + 60, + ], + effectsModsMultiplier: [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + ], + description: 'Increases the range of your ranged beast units. Has no influence on melee units.', + image: 'beastRangeUpg', + }, + + { + name: 'Research Invisibility', + id_string: 'upginvis', + cost: 200, + buildTime: 60 * 20, + maxLevel: 1, + description: 'Allows your Priests to cast Invisibility.', + effectsTypes: [ + 'upgheal', + 'upghealingward', + ], + effectsFields: [ + 'cost', + 'cost', + ], + effectsModifications: [ + 200, + 200, + ], + effectsModsMultiplier: [ + 1, + 1, + ], + image: 'invisibility', + }, + + { + name: 'Research Summon Skeleton', + id_string: 'upgskeleton', + cost: 100, + buildTime: 72 * 20, + maxLevel: 1, + description: 'Allows your Mages to summon skeletons.', + effectsTypes: [ + 'upgslowfield', + 'upgfireball', + ], + effectsFields: [ + 'cost', + 'cost', + ], + effectsModifications: [ + 200, + 200, + ], + effectsModsMultiplier: [ + 1, + 1, + ], + image: 'skeleton', + }, + + { + name: 'Research Summon Healing Ward', + id_string: 'upghealingward', + cost: 100, + buildTime: 72 * 20, + maxLevel: 1, + description: 'Allows your Priests to summon a Healing Ward.', + effectsTypes: [ + 'upgheal', + 'upginvis', + ], + effectsFields: [ + 'cost', + 'cost', + ], + effectsModifications: [ + 200, + 200, + ], + effectsModsMultiplier: [ + 1, + 1, + ], + image: 'heal', + }, + + { + name: 'Research Slow Field', + id_string: 'upgslowfield', + cost: 100, + buildTime: 72 * 20, + maxLevel: 1, + description: 'Allows your Mages to create a Slow Field.', + effectsTypes: [ + 'upgfireball', + 'upgskeleton', + ], + effectsFields: [ + 'cost', + 'cost', + ], + effectsModifications: [ + 200, + 200, + ], + effectsModsMultiplier: [ + 1, + 1, + ], + image: 'slowfield', + }, + + { + name: 'Airship Telescope Extension', + id_string: 'upgtelescope', + cost: 100, + buildTime: 54 * 20, + maxLevel: 1, + description: 'Allows your Airships to see invisible units.', + effectsTypes: [ + 'airship', + ], + effectsFields: [ + 'hasDetection', + ], + effectsModifications: [ + 1, + ], + effectsModsMultiplier: [ + 1, + ], + image: 'telescope', + }, +]; + +var CliffsEgyptData = [ + + new TileType({ + name: 'North', + img: { x: 390, y: 418, w: 22, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'West', + img: { x: 411, y: 413, w: 22, h: 47 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'East', + img: { x: 433, y: 413, w: 16, h: 47 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'South', + img: { x: 450, y: 418, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(88, 80, 68)', + }), + + new TileType({ + name: 'SW', + img: { x: 473, y: 416, w: 22, h: 42 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'SE', + img: { x: 498, y: 416, w: 22, h: 42 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'NW', + img: { x: 523, y: 418, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'NE', + img: { x: 544, y: 418, w: 22, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'SW2', + img: { x: 390, y: 460, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'SE2', + img: { x: 413, y: 460, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'NW2', + img: { x: 439, y: 460, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'NE2', + img: { x: 463, y: 460, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + + new TileType({ + name: 'Universal', + img: { x: 397, y: 168, w: 22, h: 46 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + minimap_color: 'rgb(72, 60, 56)', + }), + +]; + +var Modifiers = [ + + { + name: 'InvisibilityBuff', + id_string: 'invisibility', + image: 'invisibility', + description: 'This unit is invisible#BRLasts getField(invisibility.duration) sec', + duration: 40 * 20, + fields: ['isInvisible', 'dmg'], + modifications: [1, 2], + modificationsMultiplier: [1, 1], + }, + + { + name: 'dmgBuff', + id_string: 'dmgbuff', + image: 'attackUpg', + description: 'This unit gets a dmg buff#BRLasts getField(dmgbuff.duration) sec', + duration: 20 * 20, + fields: ['dmg'], + modifications: [5], + modificationsMultiplier: [1], + }, + + { + name: 'Healed', + id_string: 'healed', + image: 'heal', + description: 'This unit is getting healed', + duration: 2 * 20, + fields: ['hpRegenerationRate'], + modifications: [5 / 20], + modificationsMultiplier: [1], + }, + + { + name: 'Damaged', + id_string: 'healeddmg', + image: 'skeleton', + description: 'This unit is getting damaged', + duration: 2 * 20, + fields: ['hpRegenerationRate'], + modifications: [-4 / 20], + modificationsMultiplier: [1], + }, + + { + name: 'Heal Aura', + id_string: 'healaura', + description: 'Heals nearby alled non-undead units', + duration: 0, + auraModifiers: ['healed'], + auraRange: [1.8], + auraTargetFilters: ['isBiological'], + auraTargetFiltersExclude: ['isUndead'], + auraHitsFriendly: true, + auraHitsEnemy: false, + auraHitsSelf: false, + effects: ['aura'], + auraColor: { red: 150, green: 250, blue: 180, alpha: 0.2 }, + sound: SOUND.AURA_HEAL, + volume: 0.2, + }, + + { + name: 'Heal Aura Dmg', + id_string: 'healauradmg', + description: 'Heals nearby enemy undead units', + duration: 0, + auraModifiers: ['healeddmg'], + auraHitsFriendly: false, + auraHitsEnemy: true, + auraHitsSelf: false, + auraRange: [3], + auraTargetFilters: ['isBiological', 'isUndead'], + }, + + { + name: 'Slow Field', + id_string: 'slowfield', + description: 'Slows nearby enemy units.', + duration: 0, + auraModifiers: ['slowed'], + auraHitsFriendly: false, + auraHitsEnemy: true, + effects: ['aura'], + auraColor: { red: 70, green: 100, blue: 190, alpha: 0.2 }, + particleMode: 2, + density: 40, + auraRange: [4.0], + }, + + { + name: 'Slowed', + id_string: 'slowed', + image: 'slowfield', + description: 'This unit is getting slowed', + duration: 1.1 * 20, + fields: ['movementSpeed'], + modifications: [0], + modificationsMultiplier: [0.5], + }, + + { + name: 'Slowed', + id_string: 'slowed25', + image: 'slowfield', + description: 'This unit is getting slowed by 25%', + duration: 1.1 * 20, + fields: ['movementSpeed'], + modifications: [0], + modificationsMultiplier: [0.75], + }, + + { + 'isModifier': true, + 'name': 'Raider Fix', + 'id_string': 'raiderfix', + 'description': 'This unit is invisible#BRLasts getField(invisibility.duration) sec', + 'image': null, + 'duration': -20, + 'fields': [ + 'weaponCooldown', + ], + 'modifications': [ + -55.599999999999994, + ], + 'modificationsMultiplier': [ + 1, + ], + 'maxStack': 1, + 'auraModifiers': [], + 'auraRange': [], + 'auraHitsFriendly': true, + 'auraHitsAllied': true, + 'auraHitsEnemy': true, + 'auraHitsSelf': true, + 'auraTargetFilters': [], + 'auraTargetFiltersExclude': [], + 'disabledCommands': [], + 'changeUnitImg': false, + 'unitImg': null, + 'changeAttackEffect': false, + 'attackEffect': null, + 'effects': [], + 'sound': 0, + 'volume': 1, + 'killModifiers': [], + }, + { + 'isModifier': true, + 'name': 'Raider Flash', + 'id_string': 'raiderflashmana', + 'description': 'Flash mana cost', + 'image': 'raider', + 'duration': 2, + 'fields': [ + 'manaRegenerationRate', + ], + 'modifications': [ + -24.440625, + ], + 'modificationsMultiplier': [ + 1, + ], + 'maxStack': 1, + 'auraModifiers': [], + 'auraRange': [], + 'auraHitsFriendly': true, + 'auraHitsAllied': true, + 'auraHitsEnemy': true, + 'auraHitsSelf': true, + 'auraTargetFilters': [], + 'auraTargetFiltersExclude': [], + 'disabledCommands': [], + 'changeUnitImg': false, + 'unitImg': null, + 'changeAttackEffect': false, + 'attackEffect': null, + 'effects': [], + 'sound': 0, + 'volume': 1, + 'killModifiers': [], + }, + { + 'isModifier': true, + 'name': 'Rallied', + 'id_string': 'rallied', + 'description': 'This unit has been Rallied; it has increased speed and armor.', + 'image': 'shockwave', + 'duration': 160, + 'fields': [ + 'movementSpeed', + 'armor', + 'imageScale', + ], + 'modifications': [ + 0.025, + 2, + 0, + ], + 'modificationsMultiplier': [ + 1, + 1, + 1.3, + ], + 'maxStack': 1, + 'auraModifiers': [], + 'auraRange': [], + 'auraHitsFriendly': true, + 'auraHitsAllied': true, + 'auraHitsEnemy': true, + 'auraHitsSelf': true, + 'auraTargetFilters': [], + 'auraTargetFiltersExclude': [], + 'disabledCommands': [], + 'changeUnitImg': false, + 'unitImg': null, + 'changeAttackEffect': false, + 'attackEffect': null, + 'effects': [], + 'sound': 0, + 'volume': 1, + 'killModifiers': [], + }, + { + 'isModifier': true, + 'name': 'Trapped', + 'id_string': 'trapped', + 'description': 'This units foot is severly injured', + 'image': 'heal', + 'duration': 5 * 20, + 'fields': [ + 'movementSpeed', + ], + 'modifications': [ + 0, + ], + 'modificationsMultiplier': [ + 0.1, + ], + 'maxStack': 1, + 'auraModifiers': [], + 'auraRange': [], + 'auraHitsFriendly': false, + 'auraHitsAllied': true, + 'auraHitsEnemy': true, + 'auraHitsSelf': true, + 'auraTargetFilters': [], + 'auraTargetFiltersExclude': [], + 'disabledCommands': [], + 'changeUnitImg': false, + 'unitImg': null, + 'changeAttackEffect': false, + 'attackEffect': null, + 'effects': [], + 'sound': 0, + 'volume': 1, + 'killModifiers': [], + }, + { + 'isModifier': true, + 'name': 'Reveal Self', + 'id_string': 'revealself', + 'description': 'This units foot is severly injured', + 'image': null, + 'duration': 40, + 'fields': [ + 'isInvisible', + 'hpRegenerationRate', + 'isInvincible', + ], + 'modifications': [ + -1, + -0.4, + 1, + ], + 'modificationsMultiplier': [ + 1, + 1, + 1, + ], + 'maxStack': 1, + 'auraModifiers': [], + 'auraRange': [], + 'auraHitsFriendly': false, + 'auraHitsAllied': true, + 'auraHitsEnemy': true, + 'auraHitsSelf': true, + 'auraTargetFilters': [], + 'auraTargetFiltersExclude': [], + 'disabledCommands': [], + 'changeUnitImg': false, + 'unitImg': null, + 'changeAttackEffect': false, + 'attackEffect': null, + 'effects': [], + 'sound': 0, + 'volume': 1, + 'killModifiers': [], + }, + { + 'isModifier': true, + 'name': 'Kill Flash', + 'id_string': 'killflash', + 'description': 'Raider cant attack', + 'image': null, + 'duration': 57, + 'fields': [ + 'weaponCooldown', + ], + 'modifications': [ + 55.99999999999999, + ], + 'modificationsMultiplier': [ + 1, + ], + 'maxStack': 1, + 'auraModifiers': [], + 'auraRange': [], + 'auraHitsFriendly': true, + 'auraHitsAllied': true, + 'auraHitsEnemy': true, + 'auraHitsSelf': true, + 'auraTargetFilters': [], + 'auraTargetFiltersExclude': [], + 'disabledCommands': [], + 'changeUnitImg': false, + 'unitImg': null, + 'changeAttackEffect': false, + 'attackEffect': null, + 'effects': [], + 'sound': 0, + 'volume': 1, + 'killModifiers': [ + 'raiderflashmana', + ], + }, + { + 'isModifier': true, + 'name': 'speedBuff', + 'id_string': 'speedbuff', + 'description': 'This unit gets a speed buff#BRLasts getField(dmgbuff.duration) sec', + 'image': 'beastSpeedUpg', + 'duration': 8 * 20, + 'fields': [ + 'movementSpeed', + ], + 'modifications': [ + 0.05, + ], + 'modificationsMultiplier': [ + 1, + ], + 'maxStack': 1, + 'auraModifiers': [], + 'auraRange': [], + 'auraHitsFriendly': true, + 'auraHitsAllied': true, + 'auraHitsEnemy': true, + 'auraHitsSelf': true, + 'auraTargetFilters': [], + 'auraTargetFiltersExclude': [], + 'disabledCommands': [], + 'changeUnitImg': false, + 'unitImg': null, + 'changeAttackEffect': false, + 'attackEffect': null, + 'effects': [], + 'sound': 0, + 'volume': 1, + 'killModifiers': [], + }, + { + 'isModifier': true, + 'name': 'speedDebuff', + 'id_string': 'speeddebuff', + 'description': 'This unit gets a speed decreasef#BRLasts getField(dmgbuff.duration) sec', + 'image': null, + 'duration': 13 * 20, + 'fields': [ + 'movementSpeed', + ], + 'modifications': [ + -0.017499999999999998, + ], + 'modificationsMultiplier': [ + 1, + ], + 'maxStack': 1, + 'auraModifiers': [], + 'auraRange': [], + 'auraHitsFriendly': true, + 'auraHitsAllied': true, + 'auraHitsEnemy': true, + 'auraHitsSelf': true, + 'auraTargetFilters': [], + 'auraTargetFiltersExclude': [], + 'disabledCommands': [], + 'changeUnitImg': false, + 'unitImg': null, + 'changeAttackEffect': false, + 'attackEffect': null, + 'effects': [], + 'sound': 0, + 'volume': 1, + 'killModifiers': [], + }, + { + 'isModifier': true, + 'name': 'Shrouded', + 'id_string': 'raidershrouded', + 'description': 'This unit is under a Shroud, and has reduced range.', + 'image': 'shroud', + 'duration': 22, + 'fields': [ + 'range', + ], + 'modifications': [ + 0.2, + ], + 'modificationsMultiplier': [ + 0, + ], + 'maxStack': 1, + 'auraModifiers': [], + 'auraRange': [], + 'auraHitsFriendly': true, + 'auraHitsAllied': true, + 'auraHitsEnemy': true, + 'auraHitsSelf': true, + 'auraTargetFilters': [], + 'auraTargetFiltersExclude': [], + 'disabledCommands': [], + 'changeUnitImg': false, + 'unitImg': null, + 'changeAttackEffect': false, + 'attackEffect': null, + 'effects': [], + 'sound': 0, + 'volume': 1, + 'killModifiers': [], + }, + { + 'isModifier': true, + 'name': 'Shroud Aura', + 'id_string': 'raidershroudaura', + 'description': 'Shrouds nearby units.', + 'image': null, + 'duration': 0, + 'fields': [], + 'modifications': [], + 'modificationsMultiplier': [], + 'maxStack': 1, + 'auraModifiers': [ + 'raidershrouded', + ], + 'auraRange': [ + 2, + ], + 'auraHitsFriendly': true, + 'auraHitsAllied': true, + 'auraHitsEnemy': true, + 'auraHitsSelf': true, + 'auraTargetFilters': [], + 'auraTargetFiltersExclude': [ + 'isMechanical', + ], + 'disabledCommands': [], + 'changeUnitImg': false, + 'unitImg': null, + 'changeAttackEffect': false, + 'attackEffect': null, + 'effects': [], + 'sound': 0, + 'volume': 1, + 'killModifiers': [], + }, + { + 'isModifier': true, + 'name': 'Speed Buff Gatling', + 'id_string': 'speedbuffgat', + 'description': 'This unit gets a speed buff#BRLasts getField(dmgbuff.duration) sec', + 'image': 'mechSpeedUpg', + 'duration': 30, + 'fields': [ + 'movementSpeed', + ], + 'modifications': [ + 0.0425, + ], + 'modificationsMultiplier': [ + 1, + ], + 'maxStack': 1, + 'auraModifiers': [], + 'auraRange': [], + 'auraHitsFriendly': true, + 'auraHitsAllied': true, + 'auraHitsEnemy': true, + 'auraHitsSelf': true, + 'auraTargetFilters': [], + 'auraTargetFiltersExclude': [], + 'disabledCommands': [], + 'changeUnitImg': false, + 'unitImg': null, + 'changeAttackEffect': false, + 'attackEffect': null, + 'effects': [], + 'sound': 0, + 'volume': 1, + 'killModifiers': [], + }, +]; + +var RampTileData = [ + + new TileType({ + name: 'South West', + img: { x: 194, y: 422, w: 22, h: 76 }, + sizeX: 1, + sizeY: 3, + blocking: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'South East', + img: { x: 238, y: 422, w: 24, h: 76 }, + sizeX: 1, + sizeY: 3, + blocking: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'South Floor', + img: { x: 217, y: 420, w: 16, h: 78 }, + sizeX: 1, + sizeY: 3, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'South Floor 2', + img: { x: 220, y: 341, w: 16, h: 78 }, + sizeX: 1, + sizeY: 3, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'South Floor 3', + img: { x: 237, y: 341, w: 16, h: 78 }, + sizeX: 1, + sizeY: 3, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'West Top', + img: { x: 265, y: 445, w: 54, h: 47 }, + sizeX: 3, + sizeY: 1, + blocking: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'West Bottom', + img: { x: 324, y: 448, w: 52, h: 46 }, + sizeX: 3, + sizeY: 1, + blocking: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'West Floor', + img: { x: 267, y: 393, w: 55, h: 52 }, + sizeX: 3, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'West Floor 2', + img: { x: 323, y: 393, w: 55, h: 52 }, + sizeX: 3, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'West Floor 3', + img: { x: 267, y: 327, w: 55, h: 52 }, + sizeX: 3, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'East Top', + img: { x: 342, y: 275, w: 54, h: 45 }, + sizeX: 3, + sizeY: 1, + blocking: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'East Bottom', + img: { x: 285, y: 279, w: 52, h: 43 }, + sizeX: 3, + sizeY: 1, + blocking: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'East Floor', + img: { x: 334, y: 327, w: 50, h: 53 }, + sizeX: 3, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'East Floor 2', + img: { x: 284, y: 221, w: 50, h: 53 }, + sizeX: 3, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'East Floor 3', + img: { x: 339, y: 221, w: 50, h: 53 }, + sizeX: 3, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'North Left', + img: { x: 72, y: 359, w: 23, h: 51 }, + sizeX: 1, + sizeY: 3, + blocking: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'North Right', + img: { x: 104, y: 359, w: 23, h: 51 }, + sizeX: 1, + sizeY: 3, + blocking: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'North Floor', + img: { x: 127, y: 360, w: 16, h: 23 }, + sizeX: 1, + sizeY: 3, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'North Floor 2', + img: { x: 127, y: 386, w: 16, h: 23 }, + sizeX: 1, + sizeY: 3, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + + new TileType({ + name: 'North Floor 3', + img: { x: 144, y: 386, w: 16, h: 23 }, + sizeX: 1, + sizeY: 3, + blocking: false, + ignoreGrid: true, + isGround: true, + isCliff: true, + isRamp: true, + }), + +]; + +var CliffsWinterData = [ + + new TileType({ + name: 'North', + img: { x: 2, y: 418, w: 22, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + }), + + new TileType({ + name: 'West', + img: { x: 24, y: 418, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + }), + + new TileType({ + name: 'East', + img: { x: 44, y: 418, w: 16, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + }), + + new TileType({ + name: 'South', + img: { x: 98, y: 460, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + }), + + new TileType({ + name: 'SW', + img: { x: 85, y: 416, w: 22, h: 42 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + }), + + new TileType({ + name: 'SE', + img: { x: 110, y: 416, w: 22, h: 42 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + }), + + new TileType({ + name: 'NW', + img: { x: 162, y: 456, w: 16, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + }), + + new TileType({ + name: 'NE', + img: { x: 180, y: 456, w: 16, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + }), + + new TileType({ + name: 'SW2', + img: { x: 2, y: 460, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + }), + + new TileType({ + name: 'SE2', + img: { x: 25, y: 460, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + }), + + new TileType({ + name: 'NW2', + img: { x: 120, y: 460, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + }), + + new TileType({ + name: 'NE2', + img: { x: 142, y: 460, w: 20, h: 40 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + }), + + new TileType({ + name: 'Universal', + img: { x: 347, y: 168, w: 22, h: 46 }, + sizeX: 1, + sizeY: 1, + blocking: true, + isCliff: true, + }), + +]; + +var TileTypes = [ + + new TileType({ + name: 'Tree 1', + img: { x: 164, y: 41, w: 28, h: 50 }, + sizeX: 1, + sizeY: 1, + isTree: true, + blocking: true, + }), + + new TileType({ + name: 'Tree 2', + img: { x: 0, y: 91, w: 28, h: 50 }, + sizeX: 1, + sizeY: 1, + isTree: true, + blocking: true, + }), + + new TileType({ + name: 'Tree 3', + img: { x: 68, y: 91, w: 28, h: 50 }, + sizeX: 1, + sizeY: 1, + blocking: true, + }), + + new TileType({ + name: 'Tree 4', + img: { x: 136, y: 91, w: 28, h: 50 }, + sizeX: 1, + sizeY: 1, + isTree: true, + blocking: true, + }), + + new TileType({ + name: 'Tree 5', + img: { x: 108, y: 207, w: 17, h: 30 }, + sizeX: 1, + sizeY: 1, + isTree: true, + blocking: true, + }), + + new TileType({ + name: 'Tree 6', + img: { x: 132, y: 188, w: 38, h: 50 }, + sizeX: 2, + sizeY: 2, + isTree: true, + blocking: true, + }), + + new TileType({ + name: 'Tree 7', + img: { x: 174, y: 188, w: 35, h: 50 }, + sizeX: 2, + sizeY: 2, + isTree: true, + blocking: true, + }), + + new TileType({ + name: 'Tree 8', + img: { x: 188, y: 244, w: 64, h: 64 }, + sizeX: 3, + sizeY: 3, + isTree: true, + blocking: true, + }), + + new TileType({ + name: 'Tree 9', + img: { x: 3, y: 300, w: 42, h: 50 }, + sizeX: 2, + sizeY: 2, + isTree: true, + blocking: true, + }), + + new TileType({ + name: 'Tree 10', + img: { x: 39, y: 248, w: 52, h: 50 }, + sizeX: 2, + sizeY: 2, + isTree: true, + blocking: true, + }), + + new TileType({ + name: 'Tree 11', + img: { x: 48, y: 301, w: 52, h: 45 }, + sizeX: 2, + sizeY: 2, + blocking: true, + }), + + new TileType({ + name: 'Tree 12', + img: { x: 105, y: 303, w: 42, h: 50 }, + sizeX: 2, + sizeY: 2, + isTree: true, + blocking: true, + }), + + new TileType({ + name: 'Tree 13', + img: { x: 150, y: 313, w: 66, h: 65 }, + sizeX: 3, + sizeY: 3, + isTree: true, + blocking: true, + }), + + new TileType({ + name: 'Cactus', + img: { x: 284, y: 27, w: 18, h: 26 }, + sizeX: 1, + sizeY: 1, + blocking: true, + }), + + new TileType({ + name: 'Cactus 2', + img: { x: 284, y: 55, w: 18, h: 26 }, + sizeX: 1, + sizeY: 1, + blocking: true, + }), + + new TileType({ + name: 'Cactus 3', + img: { x: 362, y: 119, w: 37, h: 45 }, + sizeX: 2, + sizeY: 2, + blocking: true, + }), + + new TileType({ + name: 'Grave Stone 1', + img: { x: 795, y: 275, w: 20, h: 30 }, + sizeX: 1, + sizeY: 1, + blocking: true, + noRandomOffset: true, + }), + + new TileType({ + name: 'Grave Stone 2', + img: { x: 817, y: 276, w: 20, h: 29 }, + sizeX: 1, + sizeY: 1, + blocking: true, + noRandomOffset: true, + }), + + new TileType({ + name: 'Grave Stone 3', + img: { x: 839, y: 276, w: 20, h: 29 }, + sizeX: 1, + sizeY: 1, + blocking: true, + noRandomOffset: true, + }), + + new TileType({ + name: 'Grave Stone 4', + img: { x: 861, y: 286, w: 18, h: 19 }, + sizeX: 1, + sizeY: 1, + blocking: true, + noRandomOffset: true, + }), + + new TileType({ + name: 'Grave Stone 5', + img: { x: 787, y: 306, w: 54, h: 45 }, + sizeX: 3, + sizeY: 1, + blocking: true, + noRandomOffset: true, + }), + + new TileType({ + name: 'Grave Stone 6', + img: { x: 843, y: 322, w: 68, h: 29 }, + sizeX: 4, + sizeY: 1, + blocking: true, + noRandomOffset: true, + }), + + new TileType({ + name: 'Grave Stone 7', + img: { x: 916, y: 317, w: 38, h: 59 }, + sizeX: 2, + sizeY: 1, + blocking: true, + noRandomOffset: true, + }), + + new TileType({ + name: 'Egypt Tile 1', + img: { x: 251, y: 25, w: 28, h: 56 }, + sizeX: 1, + sizeY: 1, + blocking: true, + }), + + new TileType({ + name: 'Palm', + img: { x: 306, y: 23, w: 57, h: 71 }, + sizeX: 1, + sizeY: 1, + isTree: true, + blocking: true, + }), + + new TileType({ + name: 'Tree 14', + img: { x: 45, y: 348, w: 26, h: 25 }, + sizeX: 1, + sizeY: 1, + isTree: true, + blocking: true, + }), + + new TileType({ + name: 'Tree 15', + img: { x: 491, y: 58, w: 23, h: 60 }, + sizeX: 1, + sizeY: 1, + isTree: true, + blocking: true, + }), + + new TileType({ + name: 'Stone 1', + img: { x: 0, y: 35, w: 28, h: 50 }, + sizeX: 1, + sizeY: 1, + blocking: true, + }), + + new TileType({ + name: 'Stone 2', + img: { x: 28, y: 32, w: 40, h: 60 }, + sizeX: 2, + sizeY: 2, + blocking: true, + }), + + new TileType({ + name: 'Stone 3', + img: { x: 68, y: 35, w: 28, h: 50 }, + sizeX: 1, + sizeY: 1, + blocking: true, + }), + + new TileType({ + name: 'Stone 4', + img: { x: 96, y: 35, w: 40, h: 60 }, + sizeX: 2, + sizeY: 2, + blocking: true, + }), + + new TileType({ + name: 'Stone 5', + img: { x: 136, y: 32, w: 28, h: 50 }, + sizeX: 1, + sizeY: 1, + blocking: true, + }), + + new TileType({ + name: 'Stone 10', + img: { x: 44, y: 376, w: 18, h: 20 }, + sizeX: 1, + sizeY: 1, + blocking: true, + }), + + new TileType({ + name: 'Stone 16', + img: { x: 94, y: 252, w: 36, h: 45 }, + sizeX: 2, + sizeY: 2, + blocking: true, + }), + + new TileType({ + name: 'Stone 17', + img: { x: 130, y: 252, w: 40, h: 45 }, + sizeX: 2, + sizeY: 2, + blocking: true, + }), + + new TileType({ + name: 'Stone 18', + img: { x: 567, y: 50, w: 26, h: 31 }, + sizeX: 1, + sizeY: 1, + blocking: true, + }), + + new TileType({ + name: 'Wall', + img: { x: 192, y: 2, w: 60, h: 78 }, + sizeX: 2, + sizeY: 2, + blocking: true, + noRandomOffset: true, + }), + + new TileType({ + name: 'Wall 2', + img: { x: 192, y: 80, w: 60, h: 80 }, + sizeX: 2, + sizeY: 2, + blocking: true, + noRandomOffset: true, + }), + + new TileType({ + name: 'Wall 3', + img: { x: 0, y: 160, w: 60, h: 80 }, + sizeX: 2, + sizeY: 2, + blocking: true, + noRandomOffset: true, + }), + + new TileType({ + name: 'Wall 4', + img: { x: 60, y: 160, w: 28, h: 50 }, + sizeX: 1, + sizeY: 1, + blocking: true, + noRandomOffset: true, + }), + + new TileType({ + name: 'Wall 5', + img: { x: 2, y: 354, w: 40, h: 56 }, + sizeX: 2, + sizeY: 2, + blocking: true, + noRandomOffset: true, + }), + + new TileType({ + name: 'Wall 6', + img: { x: 518, y: 54, w: 21, h: 63 }, + sizeX: 1, + sizeY: 3, + blocking: true, + noRandomOffset: true, + }), + + new TileType({ + name: 'Wall 7', + img: { x: 542, y: 54, w: 21, h: 63 }, + sizeX: 1, + sizeY: 3, + blocking: true, + noRandomOffset: true, + }), + + new TileType({ + name: 'Wall 8', + img: { x: 490, y: 121, w: 53, h: 23 }, + sizeX: 3, + sizeY: 1, + blocking: true, + noRandomOffset: true, + }), + + new TileType({ + name: 'Wall 9', + img: { x: 491, y: 147, w: 37, h: 45 }, + sizeX: 2, + sizeY: 2, + blocking: true, + noRandomOffset: true, + }), + + new TileType({ + name: 'Invisible Pathing Blocker', + img: { x: 875, y: 0, w: 16, h: 16 }, + imgEditor: { x: 858, y: 0, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: true, + }), + + new TileType({ + name: 'Invisible Pathing Blocker 2x2', + img: { x: 892, y: 0, w: 32, h: 32 }, + imgEditor: { x: 858, y: 17, w: 32, h: 32 }, + sizeX: 2, + sizeY: 2, + blocking: true, + }), + + new TileType({ + name: 'Flower 2', + img: { x: 0, y: 0, w: 11, h: 12 }, + sizeX: 1, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + }), + + new TileType({ + name: 'Flower 3', + img: { x: 11, y: 0, w: 7, h: 6 }, + sizeX: 1, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + }), + + new TileType({ + name: 'Flower 4', + img: { x: 18, y: 0, w: 11, h: 12 }, + sizeX: 1, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + }), + + new TileType({ + name: 'Flower 5', + img: { x: 29, y: 0, w: 12, h: 11 }, + sizeX: 1, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + }), + + new TileType({ + name: 'Stone 6', + img: { x: 181, y: 16, w: 6, h: 6 }, + sizeX: 1, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + }), + + new TileType({ + name: 'Stone 7', + img: { x: 164, y: 35, w: 6, h: 6 }, + sizeX: 1, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + }), + + new TileType({ + name: 'Stone 8', + img: { x: 170, y: 35, w: 10, h: 6 }, + sizeX: 1, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + }), + + new TileType({ + name: 'Stone 9', + img: { x: 180, y: 35, w: 10, h: 6 }, + sizeX: 1, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + }), + + new TileType({ + name: 'Grass d 5', + img: { x: 41, y: 0, w: 21, h: 15 }, + sizeX: 1, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + }), + + new TileType({ + name: 'Grass d 6', + img: { x: 62, y: 0, w: 12, h: 10 }, + sizeX: 1, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + }), + + new TileType({ + name: 'Grass d 7', + img: { x: 74, y: 0, w: 6, h: 9 }, + sizeX: 1, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + }), + + new TileType({ + name: 'Grass d 8', + img: { x: 80, y: 0, w: 6, h: 9 }, + sizeX: 1, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + }), + + new TileType({ + name: 'Grass d 9', + img: { x: 86, y: 0, w: 6, h: 9 }, + sizeX: 1, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + }), + + new TileType({ + name: 'Leaf 1', + img: { x: 176, y: 16, w: 5, h: 7 }, + sizeX: 1, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + }), + + new TileType({ + name: 'Leaf 2', + img: { x: 188, y: 0, w: 4, h: 7 }, + sizeX: 1, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + }), + + new TileType({ + name: 'Plant 1', + img: { x: 176, y: 23, w: 13, h: 12 }, + sizeX: 1, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + }), + + new TileType({ + name: 'Wood 1', + img: { x: 88, y: 160, w: 26, h: 15 }, + sizeX: 1, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + }), + + + // Grounds + new TileType({ + name: 'Grass 1', + img: { x: 92, y: 0, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Grass 2', + img: { x: 108, y: 0, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Grass 3', + img: { x: 124, y: 0, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Grass 4', + img: { x: 140, y: 0, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Grass 5', + img: { x: 156, y: 0, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Grass 21', + img: { x: 295, y: 0, w: 17, h: 19 }, + sizeX: 1, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + }), + + new TileType({ + name: 'Grass 22', + img: { x: 252, y: 0, w: 34, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + }), + + new TileType({ + name: 'Water P Big', + img: { x: 622, y: 0, w: 58, h: 47 }, + sizeX: 3, + sizeY: 3, + blocking: false, + ignoreGrid: true, + isGround: true, + }), + + new TileType({ + name: 'Water P Small', + img: { x: 568, y: 83, w: 18, h: 18 }, + sizeX: 1, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + }), + + new TileType({ + name: 'Water P Big Dark', + img: { x: 695, y: 51, w: 58, h: 47 }, + sizeX: 3, + sizeY: 3, + blocking: false, + ignoreGrid: true, + isGround: true, + }), + + new TileType({ + name: 'Water P Small Dark', + img: { x: 568, y: 104, w: 18, h: 18 }, + sizeX: 1, + sizeY: 1, + blocking: false, + ignoreGrid: true, + isGround: true, + }), + + new TileType({ + name: 'Ground n 1', + img: { x: 32, y: 16, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Ground n 2', + img: { x: 48, y: 16, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Ground n 3', + img: { x: 64, y: 16, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Ground n 4', + img: { x: 80, y: 16, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Ground n 5', + img: { x: 96, y: 16, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Ground n 6', + img: { x: 112, y: 16, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Ground n 7', + img: { x: 128, y: 16, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Ground n 8', + img: { x: 144, y: 16, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Ground n 9', + img: { x: 32, y: 16, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + + new TileType({ + name: 'Ground n 10', + img: { x: 160, y: 16, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isGround: true, + ignoreGrid: true, + }), + + + new TileType({ + name: 'Ground e 1', + img: { x: 32, y: 16, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Ground e 2', + img: { x: 48, y: 16, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Ground e 3', + img: { x: 64, y: 16, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Snow 1', + img: { x: 0, y: 245, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Snow 2', + img: { x: 16, y: 245, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Snow 3', + img: { x: 0, y: 261, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Snow 4', + img: { x: 16, y: 261, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Egypt Ground 1', + img: { x: 484, y: 477, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Egypt Ground 2', + img: { x: 500, y: 477, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Egypt Ground 3', + img: { x: 516, y: 477, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Egypt Ground 4', + img: { x: 532, y: 477, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Egypt Ground 5', + img: { x: 548, y: 477, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Egypt Ground 6', + img: { x: 484, y: 477, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Grave Ground 1', + img: { x: 484 + 380, y: 477, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Grave Ground 2', + img: { x: 500 + 380, y: 477 + 4, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Grave Ground 3', + img: { x: 516 + 380, y: 477 + 4, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Grave Ground 4', + img: { x: 532 + 380, y: 477, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Grave Ground 5', + img: { x: 548 + 380, y: 477 + 4, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Grave Ground 6', + img: { x: 484 + 380, y: 477, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isDefault: true, + isGround: true, + }), + + new TileType({ + name: 'Dirt 1', + img: { x: 235, y: 166, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isGround: true, + ignoreGrid: true, + isTexture: true, + }), + + new TileType({ + name: 'Dirt 2', + img: { x: 241, y: 184, w: 10, h: 9 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isGround: true, + ignoreGrid: true, + isTexture: true, + }), + + new TileType({ + name: 'Dirt 3', + img: { x: 240, y: 195, w: 11, h: 10 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isGround: true, + ignoreGrid: true, + isTexture: true, + }), + + new TileType({ + name: 'Dirt 4', + img: { x: 236, y: 206, w: 15, h: 15 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isGround: true, + ignoreGrid: true, + isTexture: true, + }), + + new TileType({ + name: 'Dirt 5', + img: { x: 235, y: 223, w: 16, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isGround: true, + ignoreGrid: true, + isTexture: true, + }), + + new TileType({ + name: 'Dirt 6', + img: { x: 339, y: 0, w: 14, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isGround: true, + ignoreGrid: true, + isTexture: true, + }), + + new TileType({ + name: 'Dirt 7', + img: { x: 355, y: 0, w: 13, h: 17 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isGround: true, + ignoreGrid: true, + isTexture: true, + }), + + new TileType({ + name: 'Dirt 8', + img: { x: 369, y: 0, w: 18, h: 14 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isGround: true, + ignoreGrid: true, + isTexture: true, + }), + + new TileType({ + name: 'Dirt 9', + img: { x: 387, y: 0, w: 19, h: 11 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isGround: true, + ignoreGrid: true, + isTexture: true, + }), + + new TileType({ + name: 'Big Dirt 1', + img: { x: 412, y: 1, w: 54, h: 54 }, + sizeX: 3, + sizeY: 3, + blocking: false, + isGround: true, + ignoreGrid: true, + isTexture: true, + }), + + new TileType({ + name: 'Big Dirt 2', + img: { x: 467, y: 0, w: 55, h: 54 }, + sizeX: 3, + sizeY: 3, + blocking: false, + isGround: true, + ignoreGrid: true, + isTexture: true, + }), + + new TileType({ + name: 'Big Dirt 3', + img: { x: 374, y: 56, w: 54, h: 56 }, + sizeX: 3, + sizeY: 3, + blocking: false, + isGround: true, + ignoreGrid: true, + isTexture: true, + }), + + new TileType({ + name: 'Big Dirt 4', + img: { x: 431, y: 57, w: 54, h: 55 }, + sizeX: 3, + sizeY: 3, + blocking: false, + isGround: true, + ignoreGrid: true, + isTexture: true, + }), + + new TileType({ + name: 'Big Dirt 5', + img: { x: 423, y: 114, w: 64, h: 63 }, + sizeX: 3, + sizeY: 3, + blocking: false, + isGround: true, + ignoreGrid: true, + isTexture: true, + }), + + new TileType({ + name: 'Big Dirt 6', + img: { x: 422, y: 178, w: 64, h: 62 }, + sizeX: 3, + sizeY: 3, + blocking: false, + isGround: true, + ignoreGrid: true, + isTexture: true, + }), + + new TileType({ + name: 'Snow Ground 1', + img: { x: 760, y: 0, w: 71, h: 63 }, + sizeX: 4, + sizeY: 4, + blocking: false, + isGround: true, + ignoreGrid: true, + isTexture: true, + }), + + new TileType({ + name: 'Snow Ground 2', + img: { x: 835, y: 0, w: 20, h: 16 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isGround: true, + ignoreGrid: true, + isTexture: true, + }), + + new TileType({ + name: 'Stone Tile 1', + img: { x: 683, y: 0, w: 14, h: 14 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isGround: true, + ignoreGrid: true, + isTexture: true, + }), + + new TileType({ + name: 'Stone Tile 2', + img: { x: 697, y: 0, w: 14, h: 14 }, + sizeX: 1, + sizeY: 1, + blocking: false, + isGround: true, + ignoreGrid: true, + isTexture: true, + }), + + new TileType({ + name: 'Stone Tile 3', + img: { x: 683, y: 14, w: 30, h: 30 }, + sizeX: 2, + sizeY: 2, + blocking: false, + isGround: true, + ignoreGrid: true, + isTexture: true, + }), + + new TileType({ + name: 'Stone Tile 4', + img: { x: 713, y: 0, w: 46, h: 46 }, + sizeX: 3, + sizeY: 3, + blocking: false, + isGround: true, + ignoreGrid: true, + isTexture: true, + }), + + new TileType({ + name: 'Stone Tile 5', + img: { x: 596, y: 50, w: 94, h: 94 }, + sizeX: 6, + sizeY: 6, + blocking: false, + isGround: true, + ignoreGrid: true, + isTexture: true, + }), + +]; + +// Units +var basicUnitTypes = UnitData; + +// Buildings +var basicBuildingTypes = buildingData; + +// Tiles +var tileTypes = TileTypes; + +// Cliffs +var cliffs = CliffsData; + +// Cliffs Winter +var cliffs_winter = CliffsWinterData; + +var egypt_cliffs = CliffsEgyptData; + +var grave_cliffs = GraveCliffData; + +var ramp_tiles = RampTileData; + +var ramp_tiles_egypt = RampTilesEgyptData; + +var ramp_tiles_grave = RampTilesGraveData; + +// Upgrades +var basicUpgrades = UpgradesData; + +// Commands +var basicCommands = commands; + +// Modifiers +var basicModifiers = Modifiers; + +// Setup hotkeys for basic commands +if (!IS_LOGIC) { + const specialCommands = ['attack', 'stop', 'holdposition', 'cancel']; + + // Map from unit/building name to a Set containing the names of the units/buildings they are grouped with + const groups = {}; + // Map from command ID to the name of one of its users + const commandToUser = {}; + + // Create groups of units and buildings that have commands in common + // Exclude specialCommands and commands that don't have hotkeys + basicCommands + .filter((c) => specialCommands.indexOf(c.id_string) < 0) + .filter((c) => 'hotkey' in c) + .forEach((c) => { + // Check which units / buildings use this command + const usesCommand = (type) => c.id_string in type.commands; + const unitCommandUsers = basicUnitTypes.filter((u) => usesCommand(u)).map((u) => u.name); + const buildingCommandUsers = basicBuildingTypes.filter((u) => usesCommand(u)).map((u) => u.name); + + // Change this code if there is an ability shared between units and buildings + // Currently the hotkey code is structured assuming there are none + assert(unitCommandUsers.length == 0 || buildingCommandUsers.length == 0); + if (unitCommandUsers.length == 0 && buildingCommandUsers.length == 0) { + return; + } + + const users = unitCommandUsers.length > 0 ? unitCommandUsers : buildingCommandUsers; + commandToUser[c.id_string] = users[0]; + + // Create a combined group that merges all of users and their existing groups + let combinedSet = new Set(); + users.forEach((u) => combinedSet = new Set([...combinedSet, ...tryGet(groups, u, new Set([u]))])); + users.forEach((u) => groups[u] = combinedSet); + }); + + // Maps from group name to HotkeyGroup + const unitHotkeyGroups = {}; + const buildingHotkeyGroups = {}; + // Create HotkeyGroups with HotkeySettings and insert them into unitHotkeyGroups and buildingHotkeyGroups + for (const cmdID in commandToUser) { + const command = basicCommands.find((c) => c.id_string == cmdID); + const cmdUser = commandToUser[cmdID]; + const isUnit = !!basicUnitTypes.find((u) => u.name == cmdUser); + + const groupName = Array.from(groups[cmdUser]).sort().join(' / '); + const commands = isUnit ? unitHotkeyGroups : buildingHotkeyGroups; + + if (!commands[groupName]) { + commands[groupName] = new HotkeyGroup(groupName); + } + commands[groupName].addChild(new HotkeySetting(command.name, command.hotkey, cmdID)); + } + + const unitHotkeys = new HotkeyGroup('Units', true); + const buildingHotkeys = new HotkeyGroup('Buildings', true); + + // Insert the groups created earlier into unitHotkeys and buildingHotkeys + for (const [commands, hotkeys] of [[unitHotkeyGroups, unitHotkeys], [buildingHotkeyGroups, buildingHotkeys]]) { + for (const groupName in commands) { + hotkeys.addChild(commands[groupName]); + } + } + + // Add the special commands to their own group + const commonCommandHotkeys = new HotkeyGroup('Common Commands'); + const getCommonCommandHotkeySetting = (id) => { + const command = basicCommands.find((c) => c.id_string == id); + return new HotkeySetting(command.name, command.hotkey, id); + }; + specialCommands.forEach((id) => commonCommandHotkeys.addChild(getCommonCommandHotkeySetting(id))); + + Hotkeys.registerHotkeyGroup(commonCommandHotkeys); + Hotkeys.registerHotkeyGroup(unitHotkeys); + Hotkeys.registerHotkeyGroup(buildingHotkeys); + + // Insert the user's custom hotkeys into the basic commands whenever the hotkeys change + Hotkeys.onHotkeysChanged('COMMANDS', () => { + const setHotkey = (hotkey) => { + const command = basicCommands.find((c) => c.id_string == hotkey.id); + command.hotkey = hotkey.value; + + // Set the hotkey ingame if there is a game running + if (game?.commands) { + const gameCommand = game.commands.find((c) => c.getBasicType() == command); + if (gameCommand) { + gameCommand.updateHotkey(hotkey.value); + } + } + }; + + for (const commands of [unitHotkeyGroups, buildingHotkeyGroups]) { + for (const groupName in commands) { + commands[groupName].forEach((hotkey) => setHotkey(hotkey)); + } + } + commonCommandHotkeys.forEach((hotkey) => setHotkey(hotkey)); + }); +} + +// cliff data +var cliffTable = [ + + { arr: ['*', 1, '*', 0, 0, 0, 0, 0], cliffIndex: 0 }, + { arr: ['*', 0, 0, 1, 0, '*', 0, 0], cliffIndex: 1 }, + { arr: [0, 0, '*', 0, 1, 0, 0, '*'], cliffIndex: 2 }, + { arr: [0, 0, 0, 0, 0, '*', 1, '*'], cliffIndex: 3 }, + { arr: [0, 0, 1, 0, 0, 0, 0, 0], cliffIndex: 4 }, + { arr: [1, 0, 0, 0, 0, 0, 0, 0], cliffIndex: 5 }, + { arr: [0, 0, 0, 0, 0, 0, 0, 1], cliffIndex: 6 }, + { arr: [0, 0, 0, 0, 0, 1, 0, 0], cliffIndex: 7 }, + { arr: ['*', 1, 1, 0, 1, 0, 0, '*'], cliffIndex: 8 }, + { arr: [1, 1, '*', 1, 0, '*', 0, 0], cliffIndex: 9 }, + { arr: ['*', 0, 0, 1, 0, 1, 1, '*'], cliffIndex: 10 }, + { arr: [0, 0, '*', 0, 1, '*', 1, 1], cliffIndex: 11 }, + +]; + +// 8 nb's +var nbCoords = [ + { x: -1, y: -1 }, + { x: 0, y: -1 }, + { x: 1, y: -1 }, + { x: -1, y: 0 }, + { x: 1, y: 0 }, + { x: -1, y: 1 }, + { x: 0, y: 1 }, + { x: 1, y: 1 }, +]; + +// 8 nb's with center field +var nbCoords2 = [ + { x: -1, y: -1 }, + { x: 0, y: -1 }, + { x: 1, y: -1 }, + { x: -1, y: 0 }, + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: -1, y: 1 }, + { x: 0, y: 1 }, + { x: 1, y: 1 }, +]; + +var bucketTable = { + '-1-1': { remove: [{ x: 1, y: -1 }, { x: 1, y: 0 }, { x: 1, y: 1 }, { x: 0, y: 1 }, { x: -1, y: 1 }], add: [{ x: -1, y: 1 }, { x: -1, y: 0 }, { x: -1, y: -1 }, { x: 0, y: -1 }, { x: 1, y: -1 }] }, + '0-1': { remove: [{ x: -1, y: 1 }, { x: 0, y: 1 }, { x: 1, y: 1 }], add: [{ x: -1, y: -1 }, { x: 0, y: -1 }, { x: 1, y: -1 }] }, + '1-1': { remove: [{ x: -1, y: -1 }, { x: -1, y: 0 }, { x: -1, y: 1 }, { x: 0, y: 1 }, { x: 1, y: 1 }], add: [{ x: 1, y: 1 }, { x: 1, y: 0 }, { x: 1, y: -1 }, { x: 0, y: -1 }, { x: -1, y: -1 }] }, + '-10': { remove: [{ x: 1, y: -1 }, { x: 1, y: 0 }, { x: 1, y: 1 }], add: [{ x: -1, y: -1 }, { x: -1, y: 0 }, { x: -1, y: 1 }] }, + '10': { remove: [{ x: -1, y: -1 }, { x: -1, y: 0 }, { x: -1, y: 1 }], add: [{ x: 1, y: -1 }, { x: 1, y: 0 }, { x: 1, y: 1 }] }, + '-11': { remove: [{ x: 1, y: 1 }, { x: 1, y: 0 }, { x: 1, y: -1 }, { x: 0, y: -1 }, { x: -1, y: -1 }], add: [{ x: -1, y: -1 }, { x: -1, y: 0 }, { x: -1, y: 1 }, { x: 0, y: 1 }, { x: 1, y: 1 }] }, + '01': { remove: [{ x: -1, y: -1 }, { x: 0, y: -1 }, { x: 1, y: -1 }], add: [{ x: -1, y: 1 }, { x: 0, y: 1 }, { x: 1, y: 1 }] }, + '11': { remove: [{ x: -1, y: 1 }, { x: -1, y: 0 }, { x: -1, y: -1 }, { x: 0, y: -1 }, { x: 1, y: -1 }], add: [{ x: 1, y: -1 }, { x: 1, y: 0 }, { x: 1, y: 1 }, { x: 0, y: 1 }, { x: -1, y: 1 }] }, +}; + +var reversePairs = [ + [{ x: 0, y: -1 }, { x: 0, y: 1 }], + [{ x: 1, y: 0 }, { x: -1, y: 0 }], +]; + +var rampMaps = [ + { h: [1, 1, 1, 0, 0, 0, 0, 0, 0], rampId: 0, clippingPoint: { x: -1, y: 0 } }, + { h: [0, 0, 1, 0, 0, 1, 0, 0, 1], rampId: 1, clippingPoint: { x: -2, y: -1 } }, + { h: [1, 0, 0, 1, 0, 0, 1, 0, 0], rampId: 2, clippingPoint: { x: 0, y: -1 } }, + { h: [0, 0, 0, 0, 0, 0, 1, 1, 1], rampId: 3, clippingPoint: { x: -1, y: -2 } }, +]; + +/* + * Ramps + * those x, y, addX values and so on work the following: when map is created and ramp field is found, for every cliff there will be + * x incremented by the x value and y by the y value until reached the last field still contained by the ramp, then once addX and addY will be added + * and the field that is reached now will be used to place the cliff tile. + * for the texture: initX and initY will be applied until reached the last field, then the texture will be applied for every loopX and loopY until the last ramp field reached + */ +var ramps = [ + + { + map: [{ x: 0, y: 0, cliff: true }, { x: 1, y: 0, cliff: true }, { x: 2, y: 0, cliff: true }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 0, y: 2 }, { x: 1, y: 2 }, { x: 2, y: 2 }], + vec: { x: 0, y: 1 }, + code: 'S', + cliffs: [{ x: -1, y: -1, cliff: ramp_tiles[0] }, { x: 1, y: -1, cliff: ramp_tiles[1] }], + texture: { tiles: [ramp_tiles[2], ramp_tiles[3], ramp_tiles[4]], initX: -1, initY: -1, loopX: 1, loopY: 0, drawX: 0.5, drawY: -0.3 }, + noRamps: [{ x: -1, y: -1 }, { x: 0, y: -1 }, { x: 1, y: -1 }, { x: 2, y: -1 }, { x: 3, y: -1 }, { x: -1, y: 3 }, { x: 0, y: 3 }, { x: 1, y: 3 }, { x: 2, y: 3 }, { x: 3, y: 3 }], + onlyThisRamp: [{ x: -1, y: 0 }, { x: -1, y: 1 }, { x: -1, y: 2 }, { x: 3, y: 0 }, { x: 3, y: 1 }, { x: 3, y: 2 }], + sameLevel: [{ x: 0, y: 3 }, { x: 1, y: 3 }, { x: 2, y: 3 }], + }, + + { + map: [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 2, y: 0, cliff: true }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1, cliff: true }, { x: 0, y: 2 }, { x: 1, y: 2 }, { x: 2, y: 2, cliff: true }], + vec: { x: -1, y: 0 }, + code: 'W', + cliffs: [{ x: -1, y: -1, cliff: ramp_tiles[5] }, { x: -1, y: 1, cliff: ramp_tiles[6] }], + texture: { tiles: [ramp_tiles[7], ramp_tiles[8], ramp_tiles[9]], initX: -1, initY: -1, loopX: 0, loopY: 1, drawX: 0.3, drawY: -0.2 }, + noRamps: [{ x: -1, y: -1 }, { x: -1, y: 0 }, { x: -1, y: 1 }, { x: -1, y: 2 }, { x: -1, y: 3 }, { x: 3, y: -1 }, { x: 3, y: 0 }, { x: 3, y: 1 }, { x: 3, y: 2 }, { x: 3, y: 3 }], + onlyThisRamp: [{ x: 0, y: -1 }, { x: 1, y: -1 }, { x: 2, y: -1 }, { x: 0, y: 3 }, { x: 1, y: 3 }, { x: 2, y: 3 }], + sameLevel: [{ x: -1, y: 0 }, { x: -1, y: 1 }, { x: -1, y: 2 }], + }, + + { + map: [{ x: 0, y: 0, cliff: true }, { x: 1, y: 0 }, { x: 2, y: 0 }, { x: 0, y: 1, cliff: true }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 0, y: 2, cliff: true }, { x: 1, y: 2 }, { x: 2, y: 2 }], + vec: { x: 1, y: 0 }, + code: 'E', + cliffs: [{ x: -1, y: -1, cliff: ramp_tiles[10] }, { x: -1, y: 1, cliff: ramp_tiles[11] }], + texture: { tiles: [ramp_tiles[12], ramp_tiles[13], ramp_tiles[14]], initX: -1, initY: -1, loopX: 0, loopY: 1, drawX: 0.5, drawY: -0.2 }, + noRamps: [{ x: -1, y: -1 }, { x: -1, y: 0 }, { x: -1, y: 1 }, { x: -1, y: 2 }, { x: -1, y: 3 }, { x: 3, y: -1 }, { x: 3, y: 0 }, { x: 3, y: 1 }, { x: 3, y: 2 }, { x: 3, y: 3 }], + onlyThisRamp: [{ x: 0, y: -1 }, { x: 1, y: -1 }, { x: 2, y: -1 }, { x: 0, y: 3 }, { x: 1, y: 3 }, { x: 2, y: 3 }], + sameLevel: [{ x: 3, y: 0 }, { x: 3, y: 1 }, { x: 3, y: 2 }], + }, + + { + map: [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 2, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 0, y: 2, cliff: true }, { x: 1, y: 2, cliff: true }, { x: 2, y: 2, cliff: true }], + vec: { x: 0, y: -1 }, + code: 'N', + cliffs: [{ x: -1, y: -1, cliff: ramp_tiles[15] }, { x: 1, y: -1, cliff: ramp_tiles[16] }], + texture: { tiles: [ramp_tiles[17], ramp_tiles[18], ramp_tiles[19]], initX: -1, initY: -1, loopX: 1, loopY: 0, drawX: 0.5, drawY: -0.2 }, + noRamps: [{ x: -1, y: -1 }, { x: 0, y: -1 }, { x: 1, y: -1 }, { x: 2, y: -1 }, { x: 3, y: -1 }, { x: -1, y: 3 }, { x: 0, y: 3 }, { x: 1, y: 3 }, { x: 2, y: 3 }, { x: 3, y: 3 }], + onlyThisRamp: [{ x: -1, y: 0 }, { x: -1, y: 1 }, { x: -1, y: 2 }, { x: 3, y: 0 }, { x: 3, y: 1 }, { x: 3, y: 2 }], + sameLevel: [{ x: 0, y: -1 }, { x: 1, y: -1 }, { x: 2, y: -1 }], + + }, + +]; + +var ramps_egypt = [ + + { + map: [{ x: 0, y: 0, cliff: true }, { x: 1, y: 0, cliff: true }, { x: 2, y: 0, cliff: true }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 0, y: 2 }, { x: 1, y: 2 }, { x: 2, y: 2 }], + vec: { x: 0, y: 1 }, + code: 'S', + cliffs: [{ x: -1, y: -1, cliff: ramp_tiles_egypt[0] }, { x: 1, y: -1, cliff: ramp_tiles_egypt[1] }], + texture: { tiles: [ramp_tiles_egypt[2], ramp_tiles_egypt[3], ramp_tiles_egypt[4]], initX: -1, initY: -1, loopX: 1, loopY: 0, drawX: 0.5, drawY: -0.3 }, + noRamps: [{ x: -1, y: -1 }, { x: 0, y: -1 }, { x: 1, y: -1 }, { x: 2, y: -1 }, { x: 3, y: -1 }, { x: -1, y: 3 }, { x: 0, y: 3 }, { x: 1, y: 3 }, { x: 2, y: 3 }, { x: 3, y: 3 }], + onlyThisRamp: [{ x: -1, y: 0 }, { x: -1, y: 1 }, { x: -1, y: 2 }, { x: 3, y: 0 }, { x: 3, y: 1 }, { x: 3, y: 2 }], + sameLevel: [{ x: 0, y: 3 }, { x: 1, y: 3 }, { x: 2, y: 3 }], + }, + + { + map: [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 2, y: 0, cliff: true }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1, cliff: true }, { x: 0, y: 2 }, { x: 1, y: 2 }, { x: 2, y: 2, cliff: true }], + vec: { x: -1, y: 0 }, + code: 'W', + cliffs: [{ x: -1, y: -1, cliff: ramp_tiles_egypt[5] }, { x: -1, y: 1, cliff: ramp_tiles_egypt[6] }], + texture: { tiles: [ramp_tiles_egypt[7], ramp_tiles_egypt[8], ramp_tiles_egypt[9]], initX: -1, initY: -1, loopX: 0, loopY: 1, drawX: 0.3, drawY: -0.2 }, + noRamps: [{ x: -1, y: -1 }, { x: -1, y: 0 }, { x: -1, y: 1 }, { x: -1, y: 2 }, { x: -1, y: 3 }, { x: 3, y: -1 }, { x: 3, y: 0 }, { x: 3, y: 1 }, { x: 3, y: 2 }, { x: 3, y: 3 }], + onlyThisRamp: [{ x: 0, y: -1 }, { x: 1, y: -1 }, { x: 2, y: -1 }, { x: 0, y: 3 }, { x: 1, y: 3 }, { x: 2, y: 3 }], + sameLevel: [{ x: -1, y: 0 }, { x: -1, y: 1 }, { x: -1, y: 2 }], + }, + + { + map: [{ x: 0, y: 0, cliff: true }, { x: 1, y: 0 }, { x: 2, y: 0 }, { x: 0, y: 1, cliff: true }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 0, y: 2, cliff: true }, { x: 1, y: 2 }, { x: 2, y: 2 }], + vec: { x: 1, y: 0 }, + code: 'E', + cliffs: [{ x: -1, y: -1, cliff: ramp_tiles_egypt[10] }, { x: -1, y: 1, cliff: ramp_tiles_egypt[11] }], + texture: { tiles: [ramp_tiles_egypt[12], ramp_tiles_egypt[13], ramp_tiles_egypt[14]], initX: -1, initY: -1, loopX: 0, loopY: 1, drawX: 0.5, drawY: -0.2 }, + noRamps: [{ x: -1, y: -1 }, { x: -1, y: 0 }, { x: -1, y: 1 }, { x: -1, y: 2 }, { x: -1, y: 3 }, { x: 3, y: -1 }, { x: 3, y: 0 }, { x: 3, y: 1 }, { x: 3, y: 2 }, { x: 3, y: 3 }], + onlyThisRamp: [{ x: 0, y: -1 }, { x: 1, y: -1 }, { x: 2, y: -1 }, { x: 0, y: 3 }, { x: 1, y: 3 }, { x: 2, y: 3 }], + sameLevel: [{ x: 3, y: 0 }, { x: 3, y: 1 }, { x: 3, y: 2 }], + }, + + { + map: [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 2, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 0, y: 2, cliff: true }, { x: 1, y: 2, cliff: true }, { x: 2, y: 2, cliff: true }], + vec: { x: 0, y: -1 }, + code: 'N', + cliffs: [{ x: -1, y: -1, cliff: ramp_tiles_egypt[15] }, { x: 1, y: -1, cliff: ramp_tiles_egypt[16] }], + texture: { tiles: [ramp_tiles_egypt[17], ramp_tiles_egypt[18], ramp_tiles_egypt[19]], initX: -1, initY: -1, loopX: 1, loopY: 0, drawX: 0.5, drawY: -0.2 }, + noRamps: [{ x: -1, y: -1 }, { x: 0, y: -1 }, { x: 1, y: -1 }, { x: 2, y: -1 }, { x: 3, y: -1 }, { x: -1, y: 3 }, { x: 0, y: 3 }, { x: 1, y: 3 }, { x: 2, y: 3 }, { x: 3, y: 3 }], + onlyThisRamp: [{ x: -1, y: 0 }, { x: -1, y: 1 }, { x: -1, y: 2 }, { x: 3, y: 0 }, { x: 3, y: 1 }, { x: 3, y: 2 }], + sameLevel: [{ x: 0, y: -1 }, { x: 1, y: -1 }, { x: 2, y: -1 }], + }, + +]; + +var ramps_grave = [ + + { + map: [{ x: 0, y: 0, cliff: true }, { x: 1, y: 0, cliff: true }, { x: 2, y: 0, cliff: true }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 0, y: 2 }, { x: 1, y: 2 }, { x: 2, y: 2 }], + vec: { x: 0, y: 1 }, + code: 'S', + cliffs: [{ x: -1, y: -1, cliff: ramp_tiles_grave[0] }, { x: 1, y: -1, cliff: ramp_tiles_grave[1] }], + texture: { tiles: [ramp_tiles_grave[2], ramp_tiles_grave[3], ramp_tiles_grave[4]], initX: -1, initY: -1, loopX: 1, loopY: 0, drawX: 0.5, drawY: -0.3 }, + noRamps: [{ x: -1, y: -1 }, { x: 0, y: -1 }, { x: 1, y: -1 }, { x: 2, y: -1 }, { x: 3, y: -1 }, { x: -1, y: 3 }, { x: 0, y: 3 }, { x: 1, y: 3 }, { x: 2, y: 3 }, { x: 3, y: 3 }], + onlyThisRamp: [{ x: -1, y: 0 }, { x: -1, y: 1 }, { x: -1, y: 2 }, { x: 3, y: 0 }, { x: 3, y: 1 }, { x: 3, y: 2 }], + sameLevel: [{ x: 0, y: 3 }, { x: 1, y: 3 }, { x: 2, y: 3 }], + }, + + { + map: [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 2, y: 0, cliff: true }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1, cliff: true }, { x: 0, y: 2 }, { x: 1, y: 2 }, { x: 2, y: 2, cliff: true }], + vec: { x: -1, y: 0 }, + code: 'W', + cliffs: [{ x: -1, y: -1, cliff: ramp_tiles_grave[5] }, { x: -1, y: 1, cliff: ramp_tiles_grave[6] }], + texture: { tiles: [ramp_tiles_grave[7], ramp_tiles_grave[8], ramp_tiles_grave[9]], initX: -1, initY: -1, loopX: 0, loopY: 1, drawX: 0.3, drawY: -0.2 }, + noRamps: [{ x: -1, y: -1 }, { x: -1, y: 0 }, { x: -1, y: 1 }, { x: -1, y: 2 }, { x: -1, y: 3 }, { x: 3, y: -1 }, { x: 3, y: 0 }, { x: 3, y: 1 }, { x: 3, y: 2 }, { x: 3, y: 3 }], + onlyThisRamp: [{ x: 0, y: -1 }, { x: 1, y: -1 }, { x: 2, y: -1 }, { x: 0, y: 3 }, { x: 1, y: 3 }, { x: 2, y: 3 }], + sameLevel: [{ x: -1, y: 0 }, { x: -1, y: 1 }, { x: -1, y: 2 }], + }, + + { + map: [{ x: 0, y: 0, cliff: true }, { x: 1, y: 0 }, { x: 2, y: 0 }, { x: 0, y: 1, cliff: true }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 0, y: 2, cliff: true }, { x: 1, y: 2 }, { x: 2, y: 2 }], + vec: { x: 1, y: 0 }, + code: 'E', + cliffs: [{ x: -1, y: -1, cliff: ramp_tiles_grave[10] }, { x: -1, y: 1, cliff: ramp_tiles_grave[11] }], + texture: { tiles: [ramp_tiles_grave[12], ramp_tiles_grave[13], ramp_tiles_grave[14]], initX: -1, initY: -1, loopX: 0, loopY: 1, drawX: 0.5, drawY: -0.2 }, + noRamps: [{ x: -1, y: -1 }, { x: -1, y: 0 }, { x: -1, y: 1 }, { x: -1, y: 2 }, { x: -1, y: 3 }, { x: 3, y: -1 }, { x: 3, y: 0 }, { x: 3, y: 1 }, { x: 3, y: 2 }, { x: 3, y: 3 }], + onlyThisRamp: [{ x: 0, y: -1 }, { x: 1, y: -1 }, { x: 2, y: -1 }, { x: 0, y: 3 }, { x: 1, y: 3 }, { x: 2, y: 3 }], + sameLevel: [{ x: 3, y: 0 }, { x: 3, y: 1 }, { x: 3, y: 2 }], + }, + + { + map: [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 2, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 0, y: 2, cliff: true }, { x: 1, y: 2, cliff: true }, { x: 2, y: 2, cliff: true }], + vec: { x: 0, y: -1 }, + code: 'N', + cliffs: [{ x: -1, y: -1, cliff: ramp_tiles_grave[15] }, { x: 1, y: -1, cliff: ramp_tiles_grave[16] }], + texture: { tiles: [ramp_tiles_grave[17], ramp_tiles_grave[18], ramp_tiles_grave[19]], initX: -1, initY: -1, loopX: 1, loopY: 0, drawX: 0.5, drawY: -0.2 }, + noRamps: [{ x: -1, y: -1 }, { x: 0, y: -1 }, { x: 1, y: -1 }, { x: 2, y: -1 }, { x: 3, y: -1 }, { x: -1, y: 3 }, { x: 0, y: 3 }, { x: 1, y: 3 }, { x: 2, y: 3 }, { x: 3, y: 3 }], + onlyThisRamp: [{ x: -1, y: 0 }, { x: -1, y: 1 }, { x: -1, y: 2 }, { x: 3, y: 0 }, { x: 3, y: 1 }, { x: 3, y: 2 }], + sameLevel: [{ x: 0, y: -1 }, { x: 1, y: -1 }, { x: 2, y: -1 }], + }, + +]; + + +// Map Themes +var mapThemes = [ + + { + name: 'Grass', + defaultTiles: ['Ground n 6', 'Ground n 7', 'Ground n 8', 'Ground n 5', 'Ground n 1', 'Ground n 2', 'Ground n 3', 'Ground n 4'], + particleColor: 'rgba(255, 255, 100, 0.4)', + countDots: 150, + line_red: 255, + line_green: 255, + line_blue: 255, + cliffs: cliffs, + ramps: ramps, + arrowColor: 'white', + }, + + { + name: 'Snow', + defaultTiles: ['Snow 1', 'Snow 2', 'Snow 3', 'Snow 4'], + particleColor: 'rgba(255, 255, 255, 0.8)', + countDots: 200, + alpha: 0.8, + line_red: 0, + line_green: 255, + line_blue: 0, + cliffs: cliffs_winter, + ramps: ramps, + arrowColor: 'black', + }, + + { + name: 'Egypt', + defaultTiles: ['Egypt Ground 1', 'Egypt Ground 2', 'Egypt Ground 3', 'Egypt Ground 4', 'Egypt Ground 5', 'Egypt Ground 6'], + particleColor: 'rgba(255, 255, 100, 0.8)', + countDots: 0, + line_red: 255, + line_green: 255, + line_blue: 255, + cliffs: egypt_cliffs, + ramps: ramps_egypt, + arrowColor: 'white', + }, + + { + name: 'Graveyard', + defaultTiles: ['Grave Ground 1', 'Grave Ground 2', 'Grave Ground 3', 'Grave Ground 4', 'Grave Ground 5', 'Grave Ground 6'], + particleColor: 'rgba(255, 255, 255, 0.8)', + countDots: 100, + line_red: 0, + line_green: 255, + line_blue: 0, + cliffs: grave_cliffs, + ramps: ramps_grave, + arrowColor: 'white', + }, + +]; + + +var list_attack_effects = { + none: null, + arrow: 'arrow', + mageAttack: 'mageAttack', + launchedRock: 'launchedRock', + dragonAttack: 'dragonAttack', + ballistaAttack: 'ballista', + flamestrike: 'flamestrike', + heal: 'heal', + smoke: 'smoke', + aura: 'aura', + spell: 'spell', +}; + + +var unit_fields = [ + + { + name: 'name', + type: 'string', + max_len: 30, + min_len: 1, + description: 'The units name', + default_: 'new_unit', + logic: true, + }, + + { + name: 'hp', + type: 'integer', + min_val: 1, + max_val: 99999999, + description: 'The max amount of hit points this unit has.', + default_: 100, + logic: true, + }, + + { + name: 'startHp', + type: 'integer', + min_val: 0, + max_val: 99999999, + description: 'The amount of hit points this unit has when it spanws. 0 for max.', + default_: 0, + logic: true, + }, + + { + name: 'mana', + type: 'integer', + min_val: 0, + max_val: 99999999, + description: 'The amount of mana this unit has.', + default_: 0, + logic: true, + }, + + { + name: 'startMana', + type: 'integer', + min_val: 0, + max_val: 99999999, + description: 'The amount of starting mana this unit has.', + default_: 0, + logic: true, + }, + + { + name: 'hpRegenerationRate', + type: 'float', + min_val: -99999999, + max_val: 99999999, + description: 'The amount of hit points this unit regenerates per sec.', + default_: 0, + logic: true, + displayScale: 20, + }, + + { + name: 'manaRegenerationRate', + type: 'float', + min_val: -99999999, + max_val: 99999999, + description: 'The amount of mana this unit regenerates per sec.', + default_: 0, + logic: true, + displayScale: 20, + }, + + { + name: 'armor', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The armor this unit has. 1 armor reduces all incoming damage by 1.', + default_: 0, + logic: true, + }, + + { + name: 'supply', + type: 'integer', + min_val: 0, + max_val: 99999999, + description: 'The amount of supply this unit uses.', + default_: 1, + logic: true, + }, + + { + name: 'supplyProvided', + type: 'integer', + min_val: 0, + max_val: 99999999, + description: 'The amount of supply this unit prodives.', + default_: 0, + logic: true, + }, + + { + name: 'movementSpeed', + type: 'float', + min_val: 0, + max_val: 6.0, + description: 'The movement speed of the unit.', + default_: 0.1, + logic: true, + displayScale: 20, + }, + + { + name: 'weaponCooldown', + type: 'float', + min_val: 0.1, + max_val: 99999999, + description: 'The time it takes for this unit to fire again when it just fired.', + default_: 20, + logic: true, + displayScale: 1 / 20, + }, + + { + name: 'weaponDelay', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The time it takes for this unit to actually fire when an attack is initiated.', + default_: 20, + logic: true, + displayScale: 1 / 20, + }, + + { + name: 'dmg', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The damage this unit does with one attack.', + default_: 10, + logic: true, + }, + + { + name: 'dmgModifierAttributes', + type: 'selection', + description: 'The filter that target unit has to meet for the damage modifiers to be applied', + isArray: true, + values: targetFilters1, + default_: 'isHuman', + default2_: [], + logic: true, + group: 'dmgModifiers', + subName: 'filter', + groupDescription: 'The unit can have different damage for different types of units, for example it can do +3 damage vs mechanical units, or it can do +50% vs flying units.', + }, + + { + name: 'dmgModifierAddition', + type: 'float', + min_val: -999999, + max_val: 9999999, + description: 'This value will be added to the damage value, if it meets the filter atribute.', + default_: 0, + default2_: [], + isArray: true, + logic: true, + group: 'dmgModifiers', + subName: 'add', + groupDescription: 'The unit can have different damage for different types of units, for example it can do +3 damage vs mechanical units, or it can do +50% vs flying units.', + }, + + { + name: 'dmgModifierMultiplier', + type: 'float', + min_val: -999999, + max_val: 9999999, + description: 'The damage value will be multiplied with this value, if it meets the filter atributes.', + default_: 1, + default2_: [], + isArray: true, + logic: true, + group: 'dmgModifiers', + subName: 'multiply', + groupDescription: 'The unit can have different damage for different types of units, for example it can do +3 damage vs mechanical units, or it can do +50% vs flying units.', + }, + + { + name: 'lifesteal', + type: 'float', + min_val: -99999999, + max_val: 99999999, + description: 'The % of the damage this unit gets as HP when dealing dmg (0.5 means it gets half the dmg it deals as HP).', + default_: 0, + logic: true, + }, + + { + name: 'armorPenetration', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The amount of armor this unit ignores when dealing damage.', + default_: 0, + logic: true, + }, + + { + name: 'percDmg', + type: 'float', + min_val: -1, + max_val: 1, + description: 'The percentual damage this unit does. For example 0.2 means it does 20% (of target max hp) dmg.', + default_: 0, + logic: true, + }, + + { + name: 'dmgCap', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The min possible value this units damage can be after applying the defenders armor. If you set this to 2 for example, the damage will always be at least 2, even if the defenders armor would reduce to less than 2 normally.', + default_: 1, + logic: true, + }, + + { + name: 'bouncePower', + type: 'float', + min_val: 0.0, + max_val: 99.0, + description: 'If this is set to higher than 0, this unit will smash target units back when attacking them. It only affects units with power less than this bouncePower.', + default_: 0, + logic: true, + }, + + { + name: 'bounceDistMin', + type: 'float', + min_val: 0.0, + max_val: 99.0, + description: 'The min range of how far this unit smashes target units back.', + default_: 0, + logic: true, + }, + + { + name: 'bounceDistMax', + type: 'float', + min_val: 0.0, + max_val: 99.0, + description: 'The max range of how far this unit smashes target units back.', + default_: 0, + logic: true, + }, + + { + name: 'range', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The attack range of this unit.', + default_: 0.2, + logic: true, + }, + + { + name: 'minRange', + type: 'float', + min_val: -999, + max_val: 999, + description: 'The minimum attack range of this unit. Other units close that this can not be attacked. Set to -1 for no min attack range', + default_: -999, + logic: true, + }, + + { + name: 'aoeRadius', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'If this is bigger than 0, the unit will deal aoe damage (= area of effect). That means not only the target unit takes damage, but all units in range of the target unit.', + default_: 0, + logic: true, + }, + + { + name: 'attackPrio', + type: 'float', + min_val: -99999, + max_val: 999999, + description: 'Units with higher attack prios get attacked first.', + default_: 10, + logic: true, + }, + + { + name: 'size', + type: 'float', + min_val: 0, + max_val: 12.0, + description: 'The size of the unit (= diameter). This determines when a unit collides with other units or map objects. Ground units with a size bigger than 2 might cause pathing problems.', + default_: 0.9, + logic: true, + }, + + { + name: 'imageScale', + type: 'float', + min_val: 0.1, + max_val: 10, + description: 'If you put another value than 1, the image will be scaled, so the unit becomes bigger or smaller (only visual, no gameplay effect).', + default_: 1, + }, + + { + name: 'vision', + type: 'float', + min_val: -1, + max_val: 360, // 256 is the max map size, multiplied by the sqrt of 2 (1.414) to get the diagonal + description: 'The vision range of the unit.', + default_: 7, + logic: true, + }, + + { + name: 'repairRate', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The amount of HP this unit restores when repairing (per sec).', + default_: 0, + logic: true, + displayScale: 20, + }, + + { + name: 'repairIneffiency', + type: 'float', + min_val: 0, + max_val: 1, + description: 'Inefficiency per additional repairer e.g if set to 0.25, the second repairer will repair at a rate of 75% of the first, and third of a rate of 56%, etc', + default_: 0, + logic: true, + }, + + { + name: 'repairCost', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The amount of gold it costs when repairing (per sec).', + default_: 0, + logic: true, + displayScale: 20, + }, + + { + name: 'projectileSpeed', + type: 'float', + min_val: 0.02, + max_val: 99999999, + description: 'Only relevant if this is a ranged unit. The speed which the projectile travels.', + default_: 8, + logic: true, + }, + + { + name: 'projectileLen', + type: 'float', + min_val: 0.02, + max_val: 10, + description: 'Only relevant if this is a ranged unit with basic arrow projectiles. The length of the projectile.', + default_: 0.2, + }, + + { + name: 'attackLaunchSound', + type: 'selection', + values: SOUND, + description: 'The sound that plays when this unit attacks.', + default_: SOUND.NONE, + logic: true, + }, + + { + name: 'circleSize', + type: 'float', + min_val: 0.02, + max_val: 20, + description: 'The size of the selection circle of this unit.', + default_: 0.43, + }, + + { + name: 'circleOffset', + type: 'float', + min_val: -99, + max_val: 99, + description: 'The y offset of the cirle of this unit (only a visual thing).', + default_: 0.125, + }, + + { + name: 'buildTime', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The time it takes to build this unit.', + default_: 30, + logic: true, + displayScale: 1 / 20, + }, + + { + name: 'cost', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The amount of gold it costs to build this unit.', + default_: 100, + logic: true, + }, + + { + name: 'healthbarOffset', + type: 'float', + min_val: -99, + max_val: 99, + description: 'The y offset of the health bar (only a visual thing).', + default_: 0.95, + }, + + { + name: 'healthbarWidth', + type: 'float', + min_val: 0.1, + max_val: 99, + description: 'The width of the health bar.', + default_: 0.69, + }, + + { + name: 'selectionOffsetY', + type: 'float', + min_val: -99, + max_val: 99, + description: 'The y offset of where the unit should be selected.', + default_: 0, + }, + + { + name: 'img', + type: 'selection', + values: lists.imgs, + description: 'The unit\'s image.', + default_: lists.imgs.soldier, + special: 'imgPreview', + }, + + { + name: 'description', + type: 'string', + max_len: 300, + min_len: 0, + description: 'The description of the unit.', + default_: '', + }, + + { + name: 'experienceLevels', + type: 'integer', + min_val: 0, + max_val: 99999999, + description: 'The amount of experience this unit needs to reach higher levels. Each value represents a level. The first value is the exp needed to reach lvl 2, the 2nd value the exp to reach lvl 3 and so on.', + default_: 0, + default2_: [], + logic: true, + isArray: true, + }, + + { + name: 'experience', + type: 'integer', + min_val: 0, + max_val: 99999999, + description: 'The amount of experience this unit gives when it gets killed.', + default_: 0, + logic: true, + }, + + { + name: 'modifiersPerLevel', + type: 'selection', + values: lists.modifiers, + description: 'A list of modifiers that will be applied to this unit each time it levels up. For example you can make a modifier, that gives +1 damage and +10 HP and link it here, so the unit will get +1 dmg and +10 HP each time it levels up.', + default_: null, + default2_: [], + isArray: true, + logic: true, + }, + + { + name: 'experienceRange', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The range in which this unit collects experience from enemy dying units.', + default_: 9.0, + logic: true, + }, + + { + name: 'tabPriority', + type: 'float', + min_val: -99, + max_val: 99, + description: 'Buttons of units with higher tab priorities will be displayed first. When selecting two different unit types and both have special abilities, then the buttons of the unit with higher tab priority will be displayed.', + default_: 5, + }, + + { + name: 'drawOffsetY', + type: 'float', + min_val: -99, + max_val: 99, + description: 'Y offset of the unit when drawn.', + default_: 6, + }, + + { + name: 'attackEffect', + type: 'selection', + description: 'The effect, that shows when this unit is attacking (only relevant for ranged units).', + values: list_attack_effects, + default_: null, + logic: true, + }, + + { + name: 'painSound', + type: 'selection', + values: SOUND, + description: 'A number of sounds that will be played when this unit gets hit.', + default_: SOUND.NONE, + logic: true, + }, + + { + name: 'painSoundVolume', + type: 'float', + max_val: 1, + min_val: 0, + description: 'Volume 1 => 100%', + default_: 1, + logic: true, + }, + + { + name: 'painSound2', + type: 'selection', + values: SOUND, + description: 'A 2nd pain sound, in case 2 are needed.', + default_: SOUND.NONE, + logic: true, + }, + + { + name: 'painSoundVolume2', + type: 'float', + max_val: 1, + min_val: 0, + description: 'Volume 1 => 100%', + default_: 1, + logic: true, + }, + + { + name: 'deathSound', + type: 'selection', + values: SOUND, + description: 'The sound that will be played when this unit dies.', + default_: SOUND.NONE, + logic: true, + }, + + { + name: 'yesSound', + type: 'selection', + values: SOUND, + description: 'The sound that will be played when this unit gets an order.', + default_: SOUND.YES, + logic: true, + }, + + + { + name: 'yesSoundVolume', + type: 'float', + max_val: 1, + min_val: 0, + description: 'Volume 1 => 100%', + default_: 0.6, + logic: true, + }, + + { + name: 'readySound', + type: 'selection', + values: SOUND, + description: 'The sound that will be played when this unit spawns.', + default_: SOUND.READY, + logic: true, + }, + + { + name: 'readySoundVolume', + type: 'float', + max_val: 1, + min_val: 0, + description: 'The sound that will be played when this unit spawns.', + default_: 0.9, + logic: true, + }, + + { + name: 'bodyPower', + type: 'float', + min_val: -99, + max_val: 99, + description: 'This represents the power that a target unit will be pushed when killed by this unit. A higher value means a unit killed by this unit will be pushed back very far when killed.', + default_: 0.8, + logic: true, + }, + + { + name: 'dustCreationChance', + type: 'float', + min_val: -20, + max_val: 5, + description: 'The average dust particles this unit creates per sec. Put -1 for no dust creation at all (for example for flying units).', + default_: 0.05, + displayScale: 20, + }, + + { + name: 'visionHeightBonus', + type: 'integer', + min_val: 0, + max_val: 99, + description: 'Usually units can not look at higher grounds. With this value set they can look up X cliff levels. A unit being on height level 1 and having a visionHeightBonus of 1 can see height level 2, but not 3.', + default_: 0, + logic: true, + }, + + { + name: 'animSpeed', + type: 'float', + min_val: 0.1, + max_val: 99, + description: 'The speed in which the animations for this unit will be played.', + default_: 1.5, + }, + + { + name: 'oscillationAmplitude', + type: 'float', + min_val: 0, + max_val: 99, + description: 'The determined how fast a unit is moved up and down (only visual, no gameplay effect). Usually you want this only for flying units. To not have this effect, set to 0', + default_: 0, + }, + + { + name: 'height', + type: 'float', + min_val: -99, + max_val: 99, + description: 'This is only a visual thing. This value will be used when this unit gets shot with a projectile. The projectile will hit at a higher point when the height value is higher.', + default_: 0.3, + }, + + { + name: 'acceleration', + type: 'float', + min_val: 0, + max_val: 99, + description: 'Units with an acceleration dont start moving at their full speed. They also need some time to stop or change directions. Usually this is used for flying units. Set this to 0, if you dont want any acceleration.', + default_: 0, + logic: true, + }, + + { + name: 'angularVelocity', + type: 'float', + min_val: 0, + max_val: 1, + description: 'Units with an angular velocity take curves instead of immediately changing their angle when moving and changing their direction. Set this to 0, if you dont want any acceleration.', + default_: 0, + logic: true, + }, + + { + name: 'commands', + type: 'commands', + isObject: true, + description: 'Here are all the abilites stored, that this unit can execute.', + logic: true, + }, + + { + name: 'cargoUse', + type: 'integer', + min_val: -1, + max_val: 100, + description: 'How much space in a transport unit this unit takes. Cargo space -1 means this unit is not loadable', + default_: -1, + logic: true, + }, + + { + name: 'cargoSpace', + type: 'integer', + min_val: 0, + max_val: 99999, + description: 'How much space this unit can carry.', + default_: 0, + logic: true, + }, + + { + name: 'projectileStartHeight', + type: 'float', + min_val: -99, + max_val: 99, + description: 'The (only visual) height of projectiles that this unit shoots.', + default_: 0, + }, + + { + name: 'power', + type: 'float', + min_val: 0, + max_val: 99, + description: 'The smash effect has a bouncePower value. If that value is higher than this units power value, then it gets smashed back.', + default_: 0, + logic: true, + }, + + { + name: 'lifetime', + type: 'float', + min_val: 0, + max_val: 999999, + description: 'When this is bigger than 0, the unit will only live for a certain amount of time.', + default_: 0, + logic: true, + displayScale: 1 / 20, + }, + + { + name: 'goldReward', + type: 'integer', + min_val: -999999, + max_val: 999999, + description: 'If this is bigger than 0, a player who kills this unit will get gold rewarded according to this value.', + default_: 0, + logic: true, + }, + + { + name: 'limit', + type: 'integer', + min_val: 0, + max_val: 999999, + description: 'Set this to a number bigger than 0 to limit the amount of instances of this building a player can make.', + default_: 0, + logic: true, + }, + + { + name: 'modifiers', + type: 'selection', + isArray: true, + values: lists.modifiers, + description: 'Modifiers this unit applies to other units when attacking them.', + default_: null, + default2_: [], + logic: true, + }, + + { + name: 'modifiersSelf', + type: 'selection', + isArray: true, + values: lists.modifiers, + description: 'Modifiers this unit applies to ITSELF when attacking (other units).', + default_: null, + default2_: [], + logic: true, + }, + + { + name: 'onDamageModifiers', + type: 'selection', + isArray: true, + values: lists.modifiers, + description: 'Modifiers this unit applies to ITSELF when after taking damage', + default_: null, + default2_: [], + logic: true, + }, + + { + name: 'spawnModifiers', + type: 'selection', + isArray: true, + values: lists.modifiers, + description: 'The modifiers this unit gets when it spawns.', + default_: null, + default2_: [], + logic: true, + }, + + { + name: 'hoverText', + type: 'string', + min_len: 0, + max_len: 1000, + description: 'If you enter a text here, it will be displayed instead of the owners\' name when hovering this unit.', + default_: '', + }, + + { + name: 'canHaveWaypoint', + type: 'bool', + description: 'If this is true, this unit can set a waypoint (waypoints are used to tell spawning units where to go, so this usually makes sense if this unit can produce units)', + default_: false, + logic: true, + }, + + { + name: 'isPassive', + type: 'bool', + description: 'Passive units dont attack on their own.', + default_: false, + logic: true, + }, + + { + name: 'causesFlameDeath', + type: 'bool', + description: 'If this is true, then units killed by this unit will burn for some seconds (only visual, no gameplay effect).', + default_: false, + }, + + { + name: 'shootingReveals', + type: 'bool', + description: 'If this is true, then this unit will be visible for a short amount of time for the other player when it attacks.', + default_: false, + logic: true, + }, + + { + name: 'shootWhileMoving', + type: 'bool', + description: 'Determines if this unit can attack while it moves.', + default_: false, + logic: true, + }, + + { + name: 'hitscan', + type: 'bool', + description: 'Units that use hitscan, can not shoot through other units or obstacles, but need to have free space between them and their target in order to be able to shoot it. Their projectiles also can be intercepted if something moves in their way.', + default_: false, + logic: true, + }, + + { + name: 'maximizeRangeWhenShooting', + type: 'bool', + description: 'Always shoot at max range. Only works on not heatseeking units. Makes sense on hitscan units.', + default_: false, + logic: true, + }, + + { + name: 'hitsFriendly', + type: 'bool', + description: 'Determines if this unit attack affects friendly units (only relevant on aoe abilities).', + default_: true, + logic: true, + }, + + { + name: 'hitsEnemy', + type: 'bool', + description: 'Determines if this unit attack affects enemy units (only relevant on aoe abilities).', + default_: true, + logic: true, + }, + + { + name: 'canAttackGround', + type: 'bool', + description: 'Is the unit able to attack ground units ?', + default_: true, + logic: true, + }, + + { + name: 'canAttackFlying', + type: 'bool', + description: 'Determines if this unit can attack flying units.', + default_: false, + logic: true, + }, + + { + name: 'isHeatSeeking', + type: 'bool', + description: 'If this is false, hits by this unit can miss and will hit where the target unit was, what the attack was started.', + default_: true, + logic: true, + }, + + { + name: 'ignoreEnemyHitscan', + type: 'bool', + description: 'If this is true, the unit will be shootable for enemy units, that use hitscan, even if something is in the way. Makes sense for example on air units.', + default_: false, + logic: true, + }, + + { + name: 'controllable', + type: 'bool', + description: 'If this is false, the unit can not be controlled by the player.', + default_: true, + logic: true, + }, + + { + name: 'hasDetection', + type: 'bool', + description: 'If its true, this unit can detect invisible units.', + default_: false, + logic: true, + }, + + { + name: 'expOnlyFromOwnKills', + type: 'bool', + description: 'If true, this unit gets only experience when it actually kills a unit itself, but not when its just in range when an enemy unit is killed by another allied unit.', + default_: false, + logic: true, + }, + + { + name: 'alliesGetExperience', + type: 'bool', + description: 'If true, allied units get experience when killing this unit.', + default_: false, + logic: true, + }, + + { + name: 'alliesGetGold', + type: 'bool', + description: 'If true, allied units get gold reward when killing this unit.', + default_: false, + logic: true, + }, + + { + name: 'isReflectingProjectiles', + type: 'bool', + description: 'If this is true, this unit reflects projectiles that would hit it otherwise. Only affects projectiles that deal damage while flying.', + default_: false, + logic: true, + }, + + { + name: 'isBlockingProjectiles', + type: 'bool', + description: 'If this is true, this unit blocks (kills) projectiles that would hit it otherwise. Only affects projectiles that deal damage while flying.', + default_: false, + logic: true, + }, + + { + name: 'takeDamageOnBlock', + type: 'bool', + description: 'If this unit blocks or reflects a projectile, this field determines if it is taking damage from the projectile.', + default_: false, + logic: true, + }, + + { + name: 'preventsLoss', + type: 'bool', + description: 'If a player has no buildings or units that prevent losing, he will be eliminated.', + default_: false, + logic: true, + }, + + { + name: 'flying', + type: 'bool', + description: 'Make an educated guess.', + default_: false, + logic: true, + }, + + { + name: 'uniqueAndHeroic', + type: 'bool', + description: 'Units that are unique & heroic only exist once and can be revived when they die.', + default_: false, + logic: true, + }, + + { + name: 'isMechanical', + type: 'bool', + description: 'Mechanical units can be repaired and not healed.', + default_: false, + logic: true, + }, + + { + name: 'isUndead', + type: 'bool', + description: 'Undead units take damage from healing spells.', + default_: false, + logic: true, + }, + + { + name: 'isBiological', + type: 'bool', + description: 'Classification for unit types. Does not have any active effects but there can be abilities that only work for certain unit types. For example the \'Heal\' spell only works with biological units.', + default_: true, + logic: true, + }, + + { + name: 'isBeast', + type: 'bool', + description: 'Classification for unit types. Does not have any active effects but there can be abilities that only work for certain unit types. For example the \'Heal\' spell only works with biological units.', + default_: false, + logic: true, + }, + + { + name: 'isHuman', + type: 'bool', + description: 'Classification for unit types. Does not have any active effects but there can be abilities that only work for certain unit types. For example the \'Heal\' spell only works with biological units.', + default_: false, + logic: true, + }, + + { + name: 'noShow', + type: 'bool', + description: 'If this is true, the unit will not be drawn.', + default_: false, + logic: true, + }, + + { + name: 'noCollision', + type: 'bool', + description: 'If this is true, the unit will not collide with anything.', + default_: false, + logic: true, + }, + + { + name: 'isInvisible', + type: 'bool', + description: 'Invisible units can only be attacked when detected.', + default_: false, + logic: true, + }, + + { + name: 'isInvincible', + type: 'bool', + description: 'If this is true, the unit can not be attacked or damaged.', + default_: false, + logic: true, + }, + + { + name: 'spawnWithAMove', + type: 'bool', + description: 'If this is true, units this unit produces will use AMove command when spawning to walk to the waypoint instead of normal move.', + default_: false, + logic: true, + }, + +]; + + +var building_fields = [ + + { + name: 'name', + type: 'string', + max_len: 30, + min_len: 1, + description: 'The buildings name', + default_: 'new_building', + logic: true, + }, + + { + name: 'hp', + type: 'integer', + min_val: 1, + max_val: 99999999, + description: 'The max amount of hit points this building has.', + default_: 500, + logic: true, + }, + + { + name: 'mana', + type: 'integer', + min_val: 0, + max_val: 99999999, + description: 'The amount of mana this unit has.', + default_: 0, + logic: true, + }, + + { + name: 'startMana', + type: 'integer', + min_val: 0, + max_val: 99999999, + description: 'The amount of starting mana this unit has.', + default_: 0, + logic: true, + }, + + { + name: 'hpRegenerationRate', + type: 'float', + min_val: -99999999, + max_val: 99999999, + description: 'The amount of hit points this building regenerates per sec.', + default_: 0, + logic: true, + displayScale: 20, + }, + + { + name: 'manaRegenerationRate', + type: 'float', + min_val: -99999999, + max_val: 99999999, + description: 'The amount of mana this unit regenerates per sec.', + default_: 0, + logic: true, + displayScale: 20, + }, + + { + name: 'armor', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The armor this building has. 1 armor reduces all incoming damage by 1.', + default_: 1, + logic: true, + }, + + { + name: 'supply', + type: 'integer', + min_val: 0, + max_val: 99999999, + description: 'The amount of supply this unit uses.', + default_: 0, + logic: true, + }, + + { + name: 'supplyProvided', + type: 'integer', + min_val: 0, + max_val: 99999999, + description: 'The amount of supply this unit provides.', + default_: 0, + logic: true, + }, + + { + name: 'weaponCooldown', + type: 'float', + min_val: 0.1, + max_val: 99999999, + description: 'The time it takes for this building to fire again when it just fired.', + default_: 20, + logic: true, + displayScale: 1 / 20, + }, + + { + name: 'weaponDelay', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The time it takes for this building to actually fire when an attack is initiated.', + default_: 20, + logic: true, + displayScale: 1 / 20, + }, + + { + name: 'dmg', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The damage this building does with one attack.', + default_: 10, + logic: true, + }, + + { + name: 'dmgModifierAttributes', + type: 'selection', + description: 'The filter that target unit has to meet for the damage modifiers to be applied', + isArray: true, + values: targetFilters1, + default_: 'isHuman', + default2_: [], + logic: true, + group: 'dmgModifiers', + subName: 'filter', + groupDescription: 'The unit can have different damage for different types of units, for example it can do +3 damage vs mechanical units, or it can do +50% vs flying units.', + }, + + { + name: 'dmgModifierAddition', + type: 'float', + min_val: -999999, + max_val: 9999999, + description: 'This value will be added to the damage value, if it meets the filter atribute.', + default_: 0, + default2_: [], + isArray: true, + logic: true, + group: 'dmgModifiers', + subName: 'add', + groupDescription: 'The unit can have different damage for different types of units, for example it can do +3 damage vs mechanical units, or it can do +50% vs flying units.', + }, + + { + name: 'dmgModifierMultiplier', + type: 'float', + min_val: -999999, + max_val: 9999999, + description: 'The damage value will be multiplied with this value, if it meets the filter atributes.', + default_: 1, + default2_: [], + isArray: true, + logic: true, + group: 'dmgModifiers', + subName: 'multiply', + groupDescription: 'The unit can have different damage for different types of units, for example it can do +3 damage vs mechanical units, or it can do +50% vs flying units.', + }, + + { + name: 'lifesteal', + type: 'float', + min_val: -99999999, + max_val: 99999999, + description: 'The % of the damage this unit gets as HP when dealing dmg (0.5 means it gets half the dmg it deals as HP).', + default_: 0, + logic: true, + }, + + { + name: 'armorPenetration', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The amount of armor this unit ignores when dealing damage.', + default_: 0, + logic: true, + }, + + { + name: 'percDmg', + type: 'float', + min_val: -1, + max_val: 1, + description: 'The percentual damage this unit does. For example 0.2 means it does 20% (of target max hp) dmg.', + default_: 0, + logic: true, + }, + + { + name: 'dmgCap', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The min possible value this units damage can be after applying the defenders armor. If you set this to 2 for example, the damage will always be at least 2, even if the defenders armor would reduce to less than 2 normally.', + default_: 1, + logic: true, + }, + + { + name: 'range', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The attack range of this building.', + default_: 0.2, + logic: true, + }, + + { + name: 'minRange', + type: 'float', + min_val: -999, + max_val: 999, + description: 'The minimum attack range of this unit. Other units close that this can not be attacked. Set to -1 for no min attack range', + default_: -999, + logic: true, + }, + + { + name: 'aoeRadius', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'If this is bigger than 0, the building will deal aoe damage (= area of effect). That means not only the target unit takes damage, but all units in range of the target unit.', + default_: 0, + logic: true, + }, + + { + name: 'bouncePower', + type: 'float', + min_val: 0.0, + max_val: 99.0, + description: 'If this is set to higher than 0, this unit will smash target units back when attacking them. It only affects units with power less than this bouncePower.', + default_: 0, + logic: true, + }, + + { + name: 'bounceDistMin', + type: 'float', + min_val: 0.0, + max_val: 99.0, + description: 'The min range of how far this unit smashes target units back.', + default_: 0, + logic: true, + }, + + { + name: 'bounceDistMax', + type: 'float', + min_val: 0.0, + max_val: 99.0, + description: 'The max range of how far this unit smashes target units back.', + default_: 0, + logic: true, + }, + + { + name: 'attackPrio', + type: 'float', + min_val: -99999, + max_val: 999999, + description: 'Units with higher attack prios get attacked first.', + default_: 5, + logic: true, + }, + + { + name: 'vision', + type: 'float', + min_val: -1, + max_val: 20, + description: 'The vision range of the building.', + default_: 7, + logic: true, + }, + + { + name: 'projectileSpeed', + type: 'float', + min_val: 0.02, + max_val: 99999999, + description: 'Only relevant if this is a ranged building. The speed which the projectile travels.', + default_: 8, + logic: true, + }, + + { + name: 'projectileLen', + type: 'float', + min_val: 0.02, + max_val: 10, + description: 'Only relevant if this is a ranged building with basic arrow projectiles. The length of the projectile.', + default_: 0.2, + }, + + { + name: 'attackLaunchSound', + type: 'selection', + values: SOUND, + description: 'The sound that plays when this building fires.', + default_: SOUND.NONE, + logic: true, + }, + + { + name: 'attackEffect', + type: 'selection', + description: 'The effect, that shows when this unit is attacking (only relevant for ranged units).', + values: list_attack_effects, + default_: null, + logic: true, + }, + + { + name: 'projectileStartHeight', + type: 'float', + min_val: -99, + max_val: 99, + description: 'The (only visual) height of projectiles that this unit shoots.', + default_: 0, + }, + + { + name: 'circleSize', + type: 'float', + min_val: 0.02, + max_val: 20, + description: 'The size of the selection circle of this building.', + default_: 2.2, + }, + + { + name: 'imageScale', + type: 'float', + min_val: 0.1, + max_val: 10, + description: 'If you put another value than 1, the image will be scaled, so the building becomes bigger or smaller (only visual, no gameplay effect).', + default_: 1, + }, + + { + name: 'circleOffset', + type: 'float', + min_val: -99, + max_val: 99, + description: 'The y offset of the cirle of this building (only a visual thing).', + default_: 0.125, + }, + + { + name: 'buildTime', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The time it takes to build this building.', + default_: 50, + logic: true, + displayScale: 1 / 20, + }, + + { + name: 'goldPerDelivery', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The amout of gold a player gets per delivery, when mining from this building.', + default_: 5, + logic: true, + }, + + { + name: 'cost', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The amount of gold it costs to build this unit.', + default_: 150, + logic: true, + }, + + { + name: 'costIncrease', + type: 'float', + min_val: -99999999, + max_val: 99999999, + description: 'The amount of gold-cost-increase for this building. So the building gets more expensive the more you have.', + default_: 150, + logic: true, + }, + + { + name: 'costIncreaseGroup', + type: 'selection', + values: lists.unitTypes, + default_: null, + default2_: [], + logic: true, + isArray: true, + description: 'If this building has a costIncrese value, you put here all buildings, that increase this buildings cost.', + }, + + { + name: 'healthbarOffset', + type: 'float', + min_val: -99, + max_val: 99, + description: 'The y offset of the health bar (only a visual thing).', + default_: 0.95, + }, + + { + name: 'healthbarWidth', + type: 'float', + min_val: 0.1, + max_val: 99, + description: 'The width of the health bar.', + default_: 1.7, + }, + + { + name: 'selectionOffsetY', + type: 'float', + min_val: -99, + max_val: 99, + description: 'The y offset of where the building should be selected.', + default_: 0, + }, + + { + name: 'img', + type: 'selection', + values: lists.imgs, + description: 'The buildings image.', + default_: lists.imgs.castle, + special: 'imgPreview', + }, + + { + name: 'description', + type: 'string', + max_len: 300, + min_len: 0, + description: 'The description of the building.', + default_: '', + }, + + { + name: 'tabPriority', + type: 'float', + min_val: -99, + max_val: 99, + description: 'Buttons of building with higher tab priorities will be displayed first. When selecting two different types and both have special abilities, then the buttons of the building with higher tab priority will be displayed.', + default_: 5, + }, + + { + name: 'drawOffsetY', + type: 'float', + min_val: -99, + max_val: 99, + description: 'Y offset of the building when drawn.', + default_: 6, + }, + + { + name: 'size', + type: 'integer', + min_val: 1, + max_val: 5, + description: 'The size of the building (in fields). 3 means the building will be 3x3 fields (squares).', + default_: 1, + logic: true, + }, + + { + name: 'repairRate', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The amount of HP this unit restores when repairing (per sec).', + default_: 0, + logic: true, + displayScale: 20, + }, + + { + name: 'startGold', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'Only relevant for mines. The amount of gold, this building holds.', + default_: 0, + logic: true, + }, + + { + name: 'timeToMine', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'Only relevant for mines or gold-taking buildings. The time it takes for a worker to grab / bring gold.', + default_: 20, + logic: true, + }, + + { + name: 'miningEfficiencyCoefficient', + type: 'float', + min_val: 0, + max_val: 10, + description: 'Only relevant for mines or gold-taking buildings. This determines how fast workers mine after the first one. If this is set to 0.5 for example, a worker only mines at 50% speed when theres already another worker working on this mine. A third worker only works at 25% speed and so on.', + default_: 0.5, + logic: true, + }, + + { + name: 'minMiningRate', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The absolute min rate workers will be mining at this building. The miningEfficiencyCoefficient value will make additional workers mine less effective, but they will never mine slower than minMiningRate.', + default_: 0.1, + logic: true, + }, + + { + name: 'maxWorkers', + type: 'integer', + min_val: 0, + max_val: 99, + description: 'The max amount of workers that can work on this building.', + default_: 6, + logic: true, + }, + + { + name: 'limit', + type: 'integer', + min_val: 0, + max_val: 999999, + description: 'Set this to a number bigger than 0 to limit the amount of instances of this building a player can make.', + default_: 0, + logic: true, + }, + + { + name: 'visionHeightBonus', + type: 'integer', + min_val: 0, + max_val: 99, + description: 'Usually units can not look at higher grounds. With this value set they can look up X cliff levels. A unit being on height level 1 and having a visionHeightBonus of 1 can see height level 2, but not 3.', + default_: 0, + logic: true, + }, + + { + name: 'commands', + type: 'commands', + isObject: true, + description: 'Here are all the abilites stored, that this building can execute.', + logic: true, + }, + + { + name: 'lifetime', + type: 'float', + min_val: 0, + max_val: 999999, + description: 'When this is bigger than 0, the unit will only live for a certain amount of time.', + default_: 0, + logic: true, + displayScale: 1 / 20, + }, + + { + name: 'goldReward', + type: 'integer', + min_val: -999999, + max_val: 999999, + description: 'If this is not 0, a player who kills this unit will get gold rewarded according to this value.', + default_: 0, + logic: true, + }, + + { + name: 'maxUnitsToRepair', + type: 'integer', + min_val: 0, + max_val: 999999, + description: 'Here you can determine, how many unit can repair this unit at a time.', + default_: 1, + logic: true, + }, + + { + name: 'modifiers', + type: 'selection', + isArray: true, + values: lists.modifiers, + description: 'Modifiers this unit applies to other units when attacking them.', + default_: null, + default2_: [], + logic: true, + }, + + { + name: 'modifiersSelf', + type: 'selection', + isArray: true, + values: lists.modifiers, + description: 'Modifiers this unit applies to ITSELF when attacking (other units).', + default_: null, + default2_: [], + logic: true, + }, + + { + name: 'onDamageModifiers', + type: 'selection', + isArray: true, + values: lists.modifiers, + description: 'Modifiers this unit applies to ITSELF when after taking damage', + default_: null, + default2_: [], + logic: true, + }, + + { + name: 'spawnModifiers', + type: 'selection', + isArray: true, + values: lists.modifiers, + description: 'The modifiers this unit gets when it spawns.', + default_: null, + default2_: [], + logic: true, + }, + + { + name: 'deathSound', + type: 'selection', + values: SOUND, + description: 'The sound that gets played when this building is destroyed.', + default_: SOUND.BUILDING_DEATH, + }, + + { + name: 'clickSound', + type: 'selection', + values: SOUND, + description: 'The sound that gets played when this building gets selected.', + default_: null, + }, + + { + name: 'clickSoundVolume', + type: 'float', + min_val: 0, + max_val: 1.0, + description: 'The volume of the sound that gets played when this building gets selected.', + default_: 1.0, + }, + + { + name: 'hoverText', + type: 'string', + min_len: 0, + max_len: 1000, + description: 'If you enter a text here, it will be displayed instead of the owners\' name when hovering this building.', + default_: '', + }, + + { + name: 'canHaveWaypoint', + type: 'bool', + description: 'If this is true, this unit can set a waypoint (waypoints are used to tell spawning units where to go, so this usually makes sense if this unit can produce units)', + default_: false, + logic: true, + }, + + { + name: 'causesFlameDeath', + type: 'bool', + description: 'If this is true, then units killed by this unit will burn for some seconds (only visual, no gameplay effect).', + default_: false, + }, + + { + name: 'hitscan', + type: 'bool', + description: 'Units that use hitscan, can not shoot through other units or obstacles, but need to have free space between them and their target in order to be able to shoot it. Their projectiles also can be intercepted if something moves in their way.', + default_: false, + logic: true, + }, + + { + name: 'maximizeRangeWhenShooting', + type: 'bool', + description: 'Always shoot at max range. Only works on not heatseeking units. Makes sense on hitscan units.', + default_: false, + logic: true, + }, + + { + name: 'hitsFriendly', + type: 'bool', + description: 'Determines if this unit attack affects friendly units (only relevant on aoe abilities).', + default_: true, + logic: true, + }, + + { + name: 'hitsEnemy', + type: 'bool', + description: 'Determines if this unit attack affects enemy units (only relevant on aoe abilities).', + default_: true, + logic: true, + }, + + { + name: 'canAttackGround', + type: 'bool', + description: 'Is the building able to attack ground units ?', + default_: true, + logic: true, + }, + + { + name: 'canAttackFlying', + type: 'bool', + description: 'Determines if this building can attack flying units.', + default_: false, + logic: true, + }, + + { + name: 'isHeatSeeking', + type: 'bool', + description: 'If this is false, hits by this unit can miss and will hit where the target unit was, what the attack was started.', + default_: true, + logic: true, + }, + + { + name: 'ignoreEnemyHitscan', + type: 'bool', + description: 'If this is true, the unit will be shootable for enemy units, that use hitscan, even if something is in the way. Makes sense for example on air units.', + default_: false, + logic: true, + }, + + { + name: 'controllable', + type: 'bool', + description: 'If this is false, the unit can not be controlled by the player.', + default_: true, + logic: true, + }, + + { + name: 'hasDetection', + type: 'bool', + description: 'If its true, this unit can detect invisible units.', + default_: false, + logic: true, + }, + + { + name: 'expOnlyFromOwnKills', + type: 'bool', + description: 'If true, this unit gets only experience when it actually kills a unit itself, but not when its just in range when an enemy unit is killed by another allied unit.', + default_: false, + logic: true, + }, + + { + name: 'alliesGetExperience', + type: 'bool', + description: 'If true, allied units get experience when killing this unit.', + default_: false, + logic: true, + }, + + { + name: 'alliesGetGold', + type: 'bool', + description: 'If true, allied units get gold reward when killing this unit.', + default_: false, + logic: true, + }, + + { + name: 'isReflectingProjectiles', + type: 'bool', + description: 'If this is true, this unit reflects projectiles that would hit it otherwise. Only affects projectiles that deal damage while flying.', + default_: false, + logic: true, + }, + + { + name: 'isBlockingProjectiles', + type: 'bool', + description: 'If this is true, this unit blocks (kills) projectiles that would hit it otherwise. Only affects projectiles that deal damage while flying.', + default_: false, + logic: true, + }, + + { + name: 'takeDamageOnBlock', + type: 'bool', + description: 'If this unit blocks or reflects a projectile, this field determines if it is taking damage from the projectile.', + default_: false, + logic: true, + }, + + { + name: 'isInvincible', + type: 'bool', + description: 'If this is true, the building can not be attacked or damaged.', + default_: false, + logic: true, + }, + + { + name: 'isInvisible', + type: 'bool', + description: 'Invisible units can only be attacked when detected.', + default_: false, + logic: true, + }, + + { + name: 'alwaysNeutral', + type: 'bool', + description: 'Determines if this building is always neutral (can not be owned by a player).', + default_: false, + logic: true, + }, + + { + name: 'takesGold', + type: 'bool', + description: 'Determines if this building can be used to return gold by workers.', + default_: false, + logic: true, + }, + + { + name: 'spawnWithAMove', + type: 'bool', + description: 'If this is true, units this unit produces will use AMove command when spawning to walk to the waypoint instead of normal move.', + default_: false, + logic: true, + }, + + { + name: 'preventsReveal', + type: 'bool', + description: 'If a player has no buildings that prevent revealing, he will be revealed. That means all his buildings become visible to other players.', + default_: true, + logic: true, + }, + + { + name: 'preventsLoss', + type: 'bool', + description: 'If a player has no buildings or units that prevent losing, he will be eliminated.', + default_: true, + logic: true, + }, + + { + name: 'isMechanical', + type: 'bool', + description: 'Mechanical units (and all buildings) can be repaired and not healed.', + default_: false, + logic: true, + }, + + { + name: 'isUndead', + type: 'bool', + description: 'Undead units take damage from healing spells.', + default_: false, + logic: true, + }, + + { + name: 'isBiological', + type: 'bool', + description: 'Classification for unit types. Does not have any active effects but there can be abilities that only work for certain unit types. For example the \'Heal\' spell only works with biological units.', + default_: false, + logic: true, + }, + + { + name: 'isBeast', + type: 'bool', + description: 'Classification for unit types. Does not have any active effects but there can be abilities that only work for certain unit types. For example the \'Heal\' spell only works with biological units.', + default_: false, + logic: true, + }, + + { + name: 'isHuman', + type: 'bool', + description: 'Classification for unit types. Does not have any active effects but there can be abilities that only work for certain unit types. For example the \'Heal\' spell only works with biological units.', + default_: false, + logic: true, + }, + + { + name: 'noShow', + type: 'bool', + description: 'If this is true, the unit will not be drawn.', + default_: false, + logic: true, + }, + +]; + + +var ability_fields = [ + + { + name: 'name', + type: 'string', + max_len: 30, + min_len: 1, + description: 'The abilities name', + default_: 'new_ability', + logic: true, + }, + + { + name: 'type', + type: 'selection', + values: EDITOR_COMMANDS, + all_values: COMMAND, + description: 'The abilities type (determines what the basic function of the ability is)', + default_: EDITOR_COMMANDS.UNIVERSAL, + descriptions: commandTypeDescriptions, + logic: true, + }, + + { + name: 'unitType', + type: 'selection', + values: lists.unitTypes, + description: 'The unit type or building type that this ability produces.', + default_: null, + logic: true, + }, + + { + name: 'hotkey', + type: 'selection', + values: KEY, + description: 'The abilities hotkey', + default_: KEY.Q, + }, + + { + name: 'targetIsPoint', + type: 'bool', + description: 'Determines if the target is a point.', + default_: false, + logic: true, + }, + + { + name: 'targetIsUnit', + type: 'bool', + description: 'Determines if the target is a unit.', + default_: false, + logic: true, + }, + + { + name: 'isInstant', + type: 'bool', + description: 'Determines if this order is instant (no target).', + default_: false, + logic: true, + }, + + { + name: 'isChanneled', + type: 'bool', + description: 'Channeled spells are being cast repediately until the caster is being ordered a different order (or doesnt have enough mana anymore) instead of only once like normal spells.', + default_: false, + logic: true, + }, + + { + name: 'playLaunchSoundOnce', + type: 'bool', + description: 'When this spell is channeled, you can choose to play the launch sound only once while channeling.', + default_: false, + logic: true, + }, + + { + name: 'useAoeCursor', + type: 'bool', + description: 'Determines if the mouse cursor becomes an aoe indicator when searching a target for this ability (only works / makes sense for ranged abilities with aoe damage).', + default_: false, + }, + + { + name: 'cursor', + type: 'selection', + values: Cursors, + all_values: Cursors, + description: 'The cursor displayed when the user selects the ability.', + default_: Cursors.DEFAULT, + }, + + { + name: 'commandCard', + type: 'integer', + min_val: 0, + max_val: 99, + description: 'Which command card the button will be. 0 is the default one, 1 is the 1st sub command card (for example for workers all the make building commands are on sub command cards).', + default_: 0, + }, + + { + name: 'interfacePosX', + type: 'integer', + min_val: 0, + max_val: 4, + description: 'The x pos of the button on the command card (0 is very left, 4 is very right).', + default_: 0, + }, + + { + name: 'interfacePosY', + type: 'integer', + min_val: 0, + max_val: 1, + description: 'The y pos of the button on the command card (0 is top row, 1 is bottom row).', + default_: 0, + }, + + { + name: 'requiredLevels', + type: 'integer', + min_val: 0, + max_val: 999999, + isArray: true, + description: 'If you want the ability to be learnable, you can put here one one more levels, that the unit needs to reach before it can learn this ability (only makes sense if you put the ability on a unit that can have levels of course). Put more values for multi level abilities, the first value will be the unit level required for ability lvl 1, the 2nd value the unit level required for ability lvl 2 and so on ...', + default_: 0, + default2_: [], + logic: true, + }, + + { + name: 'learnCommandCard', + type: 'integer', + min_val: 0, + max_val: 99, + description: 'Which command card the learn button will be. 0 is the default one, 1 is the 1st sub command card (for example for workers all the make building commands are on sub command cards). This is required when this ability has to be learned first, where you want the button for learning it to be.', + default_: 0, + }, + + { + name: 'learnInterfacePosX', + type: 'integer', + min_val: 0, + max_val: 4, + description: 'The x pos of the learn button on the command card (0 is very left, 4 is very right). This is required when this ability has to be learned first, where you want the button for learning it to be.', + default_: 0, + }, + + { + name: 'learnInterfacePosY', + type: 'integer', + min_val: 0, + max_val: 1, + description: 'The y pos of the learn button on the command card (0 is top row, 1 is bottom row). This is required when this ability has to be learned first, where you want the button for learning it to be.', + default_: 0, + }, + + { + name: 'learnHotkey', + type: 'selection', + values: KEY, + description: 'The abilities hotkey', + default_: KEY.Q, + }, + + { + name: 'image', + type: 'selection', + values: lists.imgs, + description: 'The abilities image (that shows on the button)', + default_: lists.imgs.stop, + special: 'imgPreview', + }, + + { + name: 'dance_img', + type: 'selection', + values: ['dance1', 'dance2'], + description: 'The animation played when this dance command is used.', + default_: 'dance1', + }, + + { + name: 'attackEffectInit', + type: 'selection', + values: list_attack_effects, + description: 'The graphic effect that appears when start using this ability.', + default_: list_attack_effects.spell, + logic: true, + }, + + { + name: 'attackEffect', + type: 'selection', + description: 'The graphic effect, that shows when this ability is used.', + values: list_attack_effects, + default_: null, + logic: true, + }, + + { + name: 'description', + type: 'string', + max_len: 500, + min_len: 0, + description: 'The abilities description', + default_: '', + }, + + { + name: 'requirementType', + type: 'selection', + isArray: true, + values: lists.buildingsUpgrades, + description: 'The building or research type for the requirement. Put Barracks here for example, then put 1 for level, that will make this ability require at least 1 barracks.', + default_: lists.buildingsUpgrades.house, + default2_: [], + logic: true, + group: 'requirements', + subName: 'type', + groupDescription: 'Here you can put tech requirements, like for example a specific upgrade or a specific building is required to use this ability.', + }, + + { + name: 'requirementLevel', + type: 'integer', + isArray: true, + min_val: 0, + max_val: 9999, + description: 'The level that the required building / research has to be.', + default_: 1, + default2_: [], + logic: true, + group: 'requirements', + subName: 'level', + groupDescription: 'Here you can put tech requirements, like for example a specific upgrade or a specific building is required to use this ability.', + }, + + { + name: 'requirementText', + type: 'string', + isArray: true, + max_len: 300, + min_len: 0, + description: 'The description for failed requirement mets.', + default_: '', + default2_: [], + logic: true, + group: 'requirements', + subName: 'text', + groupDescription: 'Here you can put tech requirements, like for example a specific upgrade or a specific building is required to use this ability.', + }, + + { + name: 'targetRequirements1', + type: 'selection', + isArray: true, + values: targetRequirements, + description: 'The requirements the target has to fit. If you put multiple requirements into one field, only one of them has to fit.', + default_: targetRequirements.isBiological, + default2_: [], + logic: true, + }, + + { + name: 'targetRequirements2', + type: 'selection', + isArray: true, + values: targetRequirements, + description: 'The requirements the target has to fit. If you put multiple requirements into one field, only one of them has to fit.', + default_: targetRequirements.isBiological, + default2_: [], + logic: true, + }, + + { + name: 'targetRequirements3', + type: 'selection', + isArray: true, + values: targetRequirements, + description: 'The requirements the target has to fit. If you put multiple requirements into one field, only one of them has to fit.', + default_: targetRequirements.isBiological, + default2_: [], + logic: true, + }, + + { + name: 'launchSound', + type: 'selection', + values: SOUND, + description: 'The sound that plays when the ability starts.', + default_: SOUND.NONE, + logic: true, + }, + + { + name: 'upgrade', + type: 'selection', + values: lists.upgrades, + description: 'The upgrade that will be researched.', + default_: null, + logic: true, + }, + + { + name: 'improvedBuilding', + type: 'selection', + values: lists.buildingTypes, + description: 'The building that the building will be upgraded to.', + default_: null, + logic: true, + }, + + { + name: 'manaCost', + type: 'integer', + min_val: -9999, + max_val: 9999, + description: 'The mana cost of this ability. Put multiple values if you want to create a multiple level ability. First value will be level 1, 2nd level 2 and so on ...', + default_: 0, + default2_: [0], + logic: true, + isArray: true, + }, + + { + name: 'goldCost', + type: 'integer', + min_val: -9999, + max_val: 9999, + description: 'The gold cost of this ability.', + default_: 0, + logic: true, + }, + + { + name: 'chat_str', + type: 'string', + max_len: 30, + min_len: 0, + description: 'The chat string that will trigger this ability. An empty string means this ability cannot be triggered by a chat.', + default_: '', + hide: true, + }, + + { + name: 'aoeRadius', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'If this is bigger than 0, the ability will deal aoe damage (= area of effect). That means not only the target unit takes damage, but all units in range of the target unit. Put multiple values if you want to create a multiple level ability. First value will be level 1, 2nd level 2 and so on ...', + default_: 0, + default2_: [0], + logic: true, + isArray: true, + }, + + { + name: 'damage', + type: 'float', + min_val: -99999999, + max_val: 99999999, + description: 'The damage this ability does. Put multiple values if you want to create a multiple level ability. First value will be level 1, 2nd level 2 and so on ...', + default_: 0, + default2_: [0], + logic: true, + isArray: true, + }, + + { + name: 'projectileDamage', + type: 'float', + min_val: -99999999, + max_val: 99999999, + description: 'The damage the projectile does while flying. Damage will be dealt every 1 / 20 sec. So if you put 2 here, the projectile will deal 40 dmg per sec. Put multiple values if you want to create a multiple level ability. First value will be level 1, 2nd level 2 and so on ...', + default_: 0, + default2_: [0], + logic: true, + isArray: true, + }, + + { + name: 'projectileAoeRadius', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The aoe radius in which the projectile does damage while flying. Put multiple values if you want to create a multiple level ability. First value will be level 1, 2nd level 2 and so on ...', + default_: 0, + default2_: [0], + isArray: true, + logic: true, + }, + + { + name: 'maximizeRangeWhenCasting', + type: 'bool', + description: 'If this is true, the spell gets cast at full range even when the target point is closer than max range. You usually want this for spells that have damaging projectiles, because the projectile should fly as long as possible.', + default_: false, + logic: true, + }, + + { + name: 'hitsFriendly', + type: 'bool', + description: 'Determines if this ability affects friendly units (only relevant on aoe abilities).', + default_: true, + logic: true, + }, + + { + name: 'hitsEnemy', + type: 'bool', + description: 'Determines if this ability affects enemy units (only relevant on aoe abilities).', + default_: true, + logic: true, + }, + + { + name: 'hitsSelf', + type: 'bool', + description: 'Determines if this ability affects the casting unit itself (only relevant on aoe abilities).', + default_: true, + logic: true, + }, + + { + name: 'targetFilters', + type: 'string', + isArray: true, + max_len: 50, + min_len: 0, + description: 'Here you can put one or more fields that the target units have to meet to be hit (for exaple flying, isBiological, isMechanical, isUnit, isBuilding, ...).', + default_: '', + default2_: [], + logic: true, + }, + + { + name: 'targetFiltersExclude', + type: 'string', + isArray: true, + max_len: 50, + min_len: 0, + description: 'The same as targetFilters, but these filters will exclude units from being a target instead of including them.', + default_: '', + default2_: [], + logic: true, + }, + + { + name: 'effectScale', + type: 'float', + min_val: 0, + max_val: 99, + description: 'The scaling of the attack effect graphic (only graphic, no gameplay effect).', + default_: 1, + logic: true, + }, + + { + name: 'hasAutocast', + type: 'bool', + description: 'If set to true, this ability can be set to be cast automatically.', + default_: false, + logic: true, + }, + + { + name: 'autocastDefault', + type: 'bool', + description: 'If set to true, autocast is enabled by default (only makes sense, if hasAutocast is enabled, of course).', + default_: false, + logic: true, + }, + + { + name: 'autocastConditions', + realTimeCompile: true, + type: 'string', + max_len: 100, + min_len: 0, + description: 'If this ability has autocast, you can use this to tell the AI what targets to use for autocasting. Write something like hp > 10 and units that have hp bigger than 10 will be targetted. use && to combine multiple conditions with a logical AND, use || to combine multiple conditions with a logical OR. Use type.fieldname to refer to the units basic types fields. If you use hp, its the current units hp, if you use type.hp, its its basic hp. So if you use hp < type.hp for example, you get units that currently have hp less then their full hp. Use this to refer to the casting unit. For example this.owner = owner will only hit units that have the same owner as the casting unit.', + default_: '', + logic: true, + }, + + { + name: 'projectileSpeed', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The speed which the projectile travels. Put multiple values if you want to create a multiple level ability. First value will be level 1, 2nd level 2 and so on ...', + default_: 8, + default2_: [8], + logic: true, + isArray: true, + }, + + { + name: 'duration', + type: 'float', + min_val: 0.0, + max_val: 99999999, + description: 'If projectileSpeed is 0 then you can set a duration instead, that determines how long it takes for the ability to hit.', + default_: 0, + logic: true, + displayScale: 1 / 20, + }, + + { + name: 'castingDelay', + type: 'float', + min_val: 0.0, + max_val: 99999999, + description: 'When a unit starts using this ability there can be a short delay until the ability actually starts kicking in.', + default_: 0, + logic: true, + displayScale: 1 / 20, + }, + + { + name: 'cooldown', + type: 'float', + min_val: 0.0, + max_val: 99999999, + description: 'The time the unit cant do any other things after doing this ability.', + default_: 0, + logic: true, + displayScale: 1 / 20, + }, + + { + name: 'cooldown2', + type: 'float', + min_val: 0.0, + max_val: 99999999, + description: 'The time it takes for this unit to be able to do this order again after it did it.', + default_: 0, + logic: true, + displayScale: 1 / 20, + }, + + { + name: 'range', + type: 'float', + min_val: 0.0, + max_val: 99999999, + description: 'The range of the ability. Put multiple values if you want to create a multiple level ability. First value will be level 1, 2nd level 2 and so on ...', + default_: 0, + default2_: [0], + logic: true, + isArray: true, + }, + + { + name: 'minRange', + type: 'float', + min_val: -999, + max_val: 999, + description: 'The minimum range of this ability. Set to -1 for no min range. Put multiple values if you want to create a multiple level ability. First value will be level 1, 2nd level 2 and so on ...', + default_: -999, + default2_: [-999], + logic: true, + isArray: true, + }, + + { + name: 'bounceDistMin', + type: 'float', + min_val: 0.0, + max_val: 99.0, + description: 'The min range of how far this ability smashes target units back.', + default_: 0, + logic: true, + }, + + { + name: 'bounceDistMax', + type: 'float', + min_val: 0.0, + max_val: 99.0, + description: 'The max range of how far this ability smashes target units back.', + default_: 0, + logic: true, + }, + + { + name: 'bouncePower', + type: 'float', + min_val: 0.0, + max_val: 99.0, + description: 'If this is set to higher than 0, the ability will smash target units back. It only affects units with power less than this bouncePower.', + default_: 0, + logic: true, + }, + + { + name: 'targetCC', + type: 'float', + min_val: 0, + max_val: 99, + description: 'Target Command Card. Determined the number of the command card that will be switched to (0 = basic command card).', + default_: 0, + }, + + { + name: 'animationName', + type: 'string', + max_len: 300, + min_len: 0, + description: 'Name of the animation played when executing this order. The model of the unit must have an ability of this name, otherwise it wont work.', + default_: '', + }, + + { + name: 'causesFlameDeath', + type: 'bool', + description: 'If this is true, then units killed by this ability will burn for some seconds (only visual, no gameplay effect).', + default_: false, + logic: true, + }, + + { + name: 'modifiers', + type: 'selection', + isArray: true, + values: lists.modifiers, + description: 'The modifier this ability applies to target unit(s). Put multiple modifiers if you have a multiple level ability. First modifier will be used at level 1, 2nd at level 2 and so on ...', + default_: null, + default2_: [], + logic: true, + }, + + { + name: 'modifiersSelf', + type: 'selection', + isArray: true, + values: lists.modifiers, + description: 'The modifier this ability applies to the caster. Put multiple modifiers if you have a multiple level ability. First modifier will be used at level 1, 2nd at level 2 and so on ...', + default_: null, + default2_: [], + logic: true, + }, + + { + name: 'summonedUnits', + type: 'selection', + isArray: true, + values: lists.unitTypes, + description: 'Unit, that this ability summons. Put multiple units if you want to create a multiple level ability. First unit will be summoned at level 1, 2nd at level 2 and so on ...', + default_: null, + default2_: [], + logic: true, + }, + + { + name: 'summonsUseWaypoint', + type: 'bool', + description: 'Determines if the summoned unit(s) move to the waypoint of the summoning unit.', + default_: false, + logic: true, + }, + + { + name: 'summonsWaypointAMove', + type: 'bool', + description: 'Determines if the summoned units move to the waypoint with attack-move instead of normal move command.', + default_: false, + logic: true, + }, + + { + name: 'ignoreSupplyCheck', + type: 'bool', + description: 'If this is true, the summonedUnit will be summoned even if theres not enough free supply.', + default_: false, + logic: true, + }, + + { + name: 'requiresVision', + type: 'bool', + description: 'Determines if the casting player needs to have vision at the target point in order to be able to cast this spell.', + default_: false, + logic: true, + }, + +]; + + +var upgrade_fields = [ + + { + name: 'name', + type: 'string', + max_len: 30, + min_len: 1, + description: 'The upgrades name', + default_: 'new_upgrade', + logic: true, + }, + + { + name: 'buildTime', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The time it takes to research this upgrade.', + default_: 30, + logic: true, + displayScale: 1 / 20, + }, + + { + name: 'cost', + type: 'float', + min_val: 0, + max_val: 99999999, + description: 'The amount of gold it costs to research this upgrade.', + default_: 100, + logic: true, + }, + + { + name: 'maxLevel', + type: 'integer', + min_val: 0, + max_val: 9999, + description: 'The amount of levels this upgrade has.', + default_: 1, + logic: true, + }, + + { + name: 'description', + type: 'string', + max_len: 300, + min_len: 0, + description: 'The description of the upgrade.', + default_: '', + }, + + { + name: 'image', + type: 'selection', + values: lists.imgs, + description: 'The upgrades image', + default_: null, + special: 'imgPreview', + }, + + { + name: 'effectsTypes', + type: 'selection', + isArray: true, + values: lists.types, + description: 'The type of the unit / building / upgrade / ability that receives the modification.', + default_: lists.types.soldier, + default2_: [], + logic: true, + group: 'modification', + subName: 'type', + groupDescription: 'Here you can determine which field of which units will be affected by this upgrade and how they will be affected.', + }, + + { + name: 'effectsFields', + type: 'string', + max_len: 300, + min_len: 0, + default_: 'damage', + default2_: [], + isArray: true, + description: 'Which field should be modified.', + logic: true, + group: 'modification', + subName: 'field', + groupDescription: 'Here you can determine which field of which units will be affected by this upgrade and how they will be affected.', + }, + + { + name: 'effectsModifications', + type: 'float', + isArray: true, + min_val: -99999999, + max_val: 99999999, + description: 'How to modify the field. Here you can put a value that will be added.', + default_: 0.0, + default2_: [], + logic: true, + group: 'modification', + subName: 'add', + groupDescription: 'Here you can determine which field of which units will be affected by this upgrade and how they will be affected.', + }, + + { + name: 'effectsModsMultiplier', + type: 'float', + isArray: true, + min_val: -99999999, + max_val: 99999999, + description: 'How to modify the field. Here you can put a value that will be multiplied.', + default_: 1.0, + default2_: [], + logic: true, + group: 'modification', + subName: 'multiply', + groupDescription: 'Here you can determine which field of which units will be affected by this upgrade and how they will be affected.', + }, + + { + name: 'noParallelResearch', + type: 'bool', + description: 'If this is true, the next upgrade level can only be started after the last one is finished. By default, if this is not set, multiple levels can be researched at the same time.', + default_: false, + logic: true, + }, + +]; + + +var modifiers_fields = [ + + { + name: 'name', + type: 'string', + max_len: 30, + min_len: 1, + description: 'The modifiers name', + default_: 'new_modifier', + logic: true, + }, + + { + name: 'description', + type: 'string', + max_len: 300, + min_len: 0, + description: 'The description of the modifier.', + default_: '', + }, + + { + name: 'image', + type: 'selection', + values: lists.imgs, + description: 'The modifiers image', + default_: null, + special: 'imgPreview', + }, + + { + name: 'duration', + type: 'float', + min_val: -99999999, + max_val: 99999999, + description: 'The duration of the modifier. It will be removed after the duration expires. Put 0 or a negative number for endless duration.', + default_: 60, + logic: true, + displayScale: 1 / 20, + }, + + { + name: 'fields', + type: 'string', + max_len: 300, + min_len: 0, + default_: 'damage', + default2_: [], + isArray: true, + description: 'The field that will be modified.', + logic: true, + group: 'modification', + subName: 'field', + groupDescription: 'Here you can determine which field will be affected by this modifier and how.', + }, + + { + name: 'modifications', + type: 'float', + isArray: true, + min_val: -99999999, + max_val: 99999999, + description: 'How the field will be modified. The value in here will be added to the value of the field.', + default_: 0.0, + default2_: [], + logic: true, + group: 'modification', + subName: 'add', + groupDescription: 'Here you can determine which field will be affected by this modifier and how.', + }, + + { + name: 'modificationsMultiplier', + type: 'float', + isArray: true, + min_val: -99999999, + max_val: 99999999, + description: 'How the field will be modified. The value in here will be multiplied with the value of the field.', + default_: 1.0, + default2_: [], + logic: true, + group: 'modification', + subName: 'multiply', + groupDescription: 'Here you can determine which field will be affected by this modifier and how.', + }, + + { + name: 'maxStack', + type: 'integer', + min_val: 0, + max_val: 9999, + description: 'How many instances of this modifier can be on one unit. Put 0 for unlimited instancs.', + default_: 1, + logic: true, + }, + + { + name: 'auraModifiers', + type: 'selection', + isArray: true, + values: lists.modifiers, + description: 'This is the modifier that gets applied. It gets applied every 1.05 sec, so if you want them to be applied constantly, you want to give them a duration of at least something like 1.1 sec.', + default_: null, + default2_: [], + logic: true, + group: 'aura', + subName: 'modifier', + groupDescription: 'Here you can create auras. Put a range and a modifier and the modifier will be applied to all units in range.', + }, + + { + name: 'auraRange', + type: 'float', + isArray: true, + min_val: 0, + max_val: 999, + description: 'Determines the range in which the modifier gets applied.', + default_: 9, + default2_: [], + logic: true, + group: 'aura', + subName: 'range', + groupDescription: 'Here you can create auras. Put a range and a modifier and the modifier will be applied to all units in range.', + }, + + { + name: 'auraHitsFriendly', + type: 'bool', + description: 'Determines if the aura affects the casting players units.', + default_: true, + logic: true, + }, + + { + name: 'auraHitsAllied', + type: 'bool', + description: 'Determines if the aura affects units by allied players.', + default_: true, + logic: true, + }, + + { + name: 'auraHitsEnemy', + type: 'bool', + description: 'Determines if the aura affects enemy units.', + default_: true, + logic: true, + }, + + { + name: 'auraHitsSelf', + type: 'bool', + description: 'Determines if the aura affects the origin unit.', + default_: true, + logic: true, + }, + + { + name: 'auraTargetFilters', + type: 'selection', + isArray: true, + values: targetFilters1, + description: 'Here you can put one or more fields that the target units have to meet to be hit.', + default_: 'isHuman', + default2_: [], + logic: true, + }, + + { + name: 'auraTargetFiltersExclude', + type: 'selection', + isArray: true, + values: targetFilters1, + description: 'The same as targetFilters, but these filters will exclude units from being a target instead of including them.', + default_: 'isHuman', + default2_: [], + logic: true, + }, + + { + name: 'disabledCommands', + type: 'selection', + isArray: true, + values: lists.commands, + description: 'Abilities / commands that get disabled for the target unit while being under the influence of this modifier.', + default_: lists.commands.flamestrike, + default2_: [], + logic: true, + }, + + { + name: 'changeUnitImg', + type: 'bool', + description: 'Determines if this modifier changes the img of the unit.', + default_: false, + }, + + { + name: 'unitImg', + type: 'selection', + values: lists.imgs, + description: 'Changes the units img to this img. Only works, when changeUnitImg is set.', + default_: null, + }, + + { + name: 'changeAttackEffect', + type: 'bool', + description: 'Determines if this modifier changes the attack effect of the unit (only graphic).', + default_: false, + logic: true, + }, + + { + name: 'attackEffect', + type: 'selection', + values: list_attack_effects, + description: 'Changes the units attack effect to this effect. Only works, when changeAttackEffect is set.', + default_: null, + logic: true, + }, + + { + name: 'effects', + type: 'selection', + isArray: true, + values: list_attack_effects, + description: 'Graphic effects that will be displayed on a unit that has this modifier', + default_: null, + default2_: [], + }, + + { + name: 'auraColor', + type: 'complex', + default_: { red: 150, green: 250, blue: 180, alpha: 0.2 }, + values: [ + { name: 'red', type: 'integer', min_val: 0, max_val: 255, description: 'The red component of the color.', default_: 150 }, + { name: 'green', type: 'integer', min_val: 0, max_val: 255, description: 'The green component of the color.', default_: 250 }, + { name: 'blue', type: 'integer', min_val: 0, max_val: 255, description: 'The blue component of the color.', default_: 180 }, + { name: 'alpha', type: 'float', min_val: 0, max_val: 1.0, description: 'The alpha component of the color.', default_: 0.2 }, + ], + description: 'The color of the aura, if the aura effect is enabled.', + }, + + { + name: 'sound', + type: 'selection', + values: SOUND, + description: 'The sound that plays when this modifier is active (will be looped).', + default_: SOUND.NONE, + }, + + { + name: 'volume', + type: 'float', + min_val: 0, + max_val: 1.0, + description: 'The volume at which the sound will be played', + default_: 1.0, + }, + + { + name: 'killModifiers', + type: 'selection', + isArray: true, + values: lists.modifiers, + description: 'Here you can put modifiers that will be removed when this modifier gets applied.', + default_: null, + default2_: [], + logic: true, + }, + +]; + +const frameStartData = +[ + { + name: 'x', + type: 'integer', + min_val: 0, + max_val: 5000, + description: 'The x cordinate of the part of the image where this animation starts.', + default_: 0, + }, + + { + name: 'y', + type: 'integer', + min_val: 0, + max_val: 5000, + description: 'The y cordinate of the part of the image where this animation starts.', + default_: 0, + }, + + { + name: 'w', + type: 'integer', + min_val: 0, + max_val: 5000, + description: 'The width (pixels) of the part of the image that contains this animation.', + default_: 0, + }, + + { + name: 'h', + type: 'integer', + min_val: 0, + max_val: 5000, + description: 'The height (pixels) of the part of the image that contains this animation.', + default_: 0, + }, + + { + name: 'frameWidth', + type: 'integer', + min_val: 0, + max_val: 5000, + description: 'The width (pixels) of one animation frame of this animation. The width (w) divided by the frameWidth equels the number of frames for this animation.', + default_: 0, + }, +]; + +var editableFrameStartData = +[ + ...frameStartData, + { + name: 'frames', + type: 'integer', + isArray: true, + min_val: 0, + max_val: 5000, + description: 'When you have multiple frames, then you can put here in which order they will be played. Each entry will last for 0.05 sec, so if you put 1, 1, 1, 2, 3 for example, then the first frame will be shown for 0.15 sec, then the 2nd frame will be shown for 0.05 sec and then the 3rd frame for 0.05 sec and then it starts all over again.', + default_: 0, + default2_: [], + }, +]; + +var imgs_fields = [ + + { + name: 'name', + type: 'string', + max_len: 30, + min_len: 1, + description: 'The images name', + default_: 'new_img', + }, + + { + name: 'file', + type: 'selection', + values: customImgs, + description: 'The source file', + default_: null, + }, + + { + name: '_angles', + type: 'selection', + values: possibleAngleCounts, + description: '4 angle units have 4 views (top, left, right, down), 8 angle units have 8 views (top, left, right, down, top-left, top-right, down-left, down-right). 1 angle units only have one view.', + default_: 0, + }, + + { + name: 'idle', + type: 'complex', + default_: { x: 0, y: 0, w: 0, h: 0, frameWidth: 0 }, + values: frameStartData, + description: 'The idle animation frames.', + }, + + { + name: 'walk', + type: 'complex', + default_: { x: 0, y: 0, w: 0, h: 0, frameWidth: 0 }, + values: frameStartData, + description: 'The walk animation frames.', + }, + + { + name: 'walkGold', + type: 'complex', + default_: { x: 0, y: 0, w: 0, h: 0, frameWidth: 0 }, + values: frameStartData, + description: 'The walkGold animation frames.', + }, + + { + name: 'die', + type: 'complex', + default_: { x: 0, y: 0, w: 0, h: 0, frameWidth: 0 }, + values: frameStartData, + description: 'The die animation frames.', + }, + + { + name: 'attack', + type: 'complex', + default_: { x: 0, y: 0, w: 0, h: 0, frameWidth: 0 }, + values: frameStartData, + description: 'The attack animation frames.', + }, + + { + name: 'special1', + type: 'complex', + default_: { x: 0, y: 0, w: 0, h: 0, frameWidth: 0 }, + values: frameStartData, + description: 'The special1 animation frames (can be used for casting animation for example)', + }, + + { + name: 'img', + type: 'complex', + default_: { x: 0, y: 0, w: 0, h: 0, frameWidth: 0, frames: [] }, + values: editableFrameStartData, + description: 'The main image.', + }, + { + name: 'constructionImg', + type: 'complex', + default_: { x: 0, y: 0, w: 0, h: 0, frameWidth: 0, frames: [] }, + values: editableFrameStartData, + description: 'The image that is shown while the building is under construction. (only required for building images)', + }, + + { + name: 'damagedImg', + type: 'complex', + default_: { x: 0, y: 0, w: 0, h: 0, frameWidth: 0, frames: [] }, + values: editableFrameStartData, + description: 'The image that is shown when the building is (heavily) damaged. (only required for building images)', + }, + + { + name: 'busyImgs', + type: 'complex', + default_: { x: 0, y: 0, w: 0, h: 0, frameWidth: 0, frames: [] }, + values: editableFrameStartData, + description: 'The image that is shown when the building is busy (training a unit, researching an upgrade). (only required for building images)', + }, + + { + name: 'busyDamagedImgs', + type: 'complex', + default_: { x: 0, y: 0, w: 0, h: 0, frameWidth: 0, frames: [] }, + values: editableFrameStartData, + description: 'The image that is shown when the building is damaged and busy (training a unit, researching an upgrade). (only required for building images)', + }, + + { + name: 'upgradeImg', + type: 'complex', + default_: { x: 0, y: 0, w: 0, h: 0, frameWidth: 0, frames: [] }, + values: editableFrameStartData, + description: 'The image that is shown when the building is upgrading to a different building. (only required for building images)', + }, + + { + name: 'upgradeImgDamaged', + type: 'complex', + default_: { x: 0, y: 0, w: 0, h: 0, frameWidth: 0, frames: [] }, + values: editableFrameStartData, + description: 'The image that is shown when the building is damaged and upgrading to a different building. (only required for building images)', + }, + + { + name: 'imgEmpty', + type: 'complex', + default_: { x: 0, y: 0, w: 0, h: 0, frameWidth: 0, frames: [] }, + values: editableFrameStartData, + description: 'The image that is shown when the building used to have gold but has been mined out. (only required for building images)', + }, + +]; + + +var targetFilters = []; + +for (var i = 0; i < unit_fields.length; i++) { + if (unit_fields[i].logic) { + targetFilters.push(unit_fields[i].name); + } +} + +for (var i = 0; i < building_fields.length; i++) { + if (building_fields[i].logic && !targetFilters.contains(building_fields[i].name)) { + targetFilters.push(building_fields[i].name); + } +} + +targetFilters.push(); + + +var list_unit_fields = {}; +for (var i = 0; i < unit_fields.length; i++) { + list_unit_fields[unit_fields[i].name] = unit_fields[i]; +} + +var list_building_fields = {}; +for (var i = 0; i < building_fields.length; i++) { + list_building_fields[building_fields[i].name] = building_fields[i]; +} + +var list_ability_fields = {}; +for (var i = 0; i < ability_fields.length; i++) { + list_ability_fields[ability_fields[i].name] = ability_fields[i]; +} + +var list_upgrade_fields = {}; +for (var i = 0; i < upgrade_fields.length; i++) { + list_upgrade_fields[upgrade_fields[i].name] = upgrade_fields[i]; +} + +var list_modifiers_fields = {}; +for (var i = 0; i < modifiers_fields.length; i++) { + list_modifiers_fields[modifiers_fields[i].name] = modifiers_fields[i]; +} + +var list_graphic_fields = {}; +for (var i = 0; i < imgs_fields.length; i++) { + list_graphic_fields[imgs_fields[i].name] = imgs_fields[i]; +} + +function Player(name, controller, number, team, clan, ai_name, skins_, dances) { + this.name = name; + this.controller = controller; // Human, remote, cpu, none + this.gold = game ? game.getStartGold() : START_GOLD; + this.number = number; + this.supply = 0; // current supply + this.lastAttackMessage = 0; // for ai + this.team = game ? (game.teams[team] ? game.teams[team] : game.teams[0]) : null; + this.originalTeam = this.team; // gotta be stored, cuz when the game ends, the team of a player changes to 0 (so he gets full vision), but then when the replay is saved, the team needs to be his original team in the replay file + this.ai_name = ai_name; + this.clan = clan; // clan name + this.isRevealed = false; + this.isAlive = true; + + // camera + this.cameraX = 0; + this.cameraY = 0; + this.cameraWidth = 0; + this.cameraHeight = 0; + this.fieldSize = 1; + + // global game variables (supply, tech requirements, ...) + this.maxSupply = 0; + this.buildings = {}; // format: {id_string: count} + this.buildingsUC = {}; // (under construction) format: {id_string: count} + this.pseudoBuildings = {}; // format: {id_string: count} + this.upgrades = {}; // format: {id_string: level} + this.units = {}; // format: {id_string: count} + this.production = {}; // format: {id_string: {count: count, from: startTick, to: finishTick}}; + this.upgradeMods = {}; // all modified fields from upgrades (format: {fieldName: mod} (for example {dmg: 2} for +2 dmg)) + + // statistics + this.unitKills = 0; + this.unitDeaths = 0; + this.buildingKills = 0; + this.buildingDeaths = 0; + this.apm = 0; + this.minedGold = 0; + this.minedGoldAtTicks = [0]; + this.currentMinedGold = 0; + this.unspentGold = 0; + this.goldLost = 0; + + this.lostUnitTypes = {}; + + // skins n dances + // Save the skins and dances as they appear in playerSettings for replays + this.psSkins = skins_ ? JSON.parse(JSON.stringify(skins_)) : {}; + this.psDances = dances; + + try { + skins_ = JSON.parse(skins_); + } catch (e) {} + if (skins_) { + _.each(skins_, function(val, key) { + for (var i = 0; i < skins.length; i++) { + if (val == skins[i].artNr) { + skins_[key] = unit_imgs[skins[i].img] ? unit_imgs[skins[i].img] : null; + } + } + }); + } + this.skins = skins_; +}; + +Player.prototype.getReplayObject = function() { + return { + name: this.name, + controller: this.controller, + team: this.originalTeam.number, + nr: this.number, + ai_name: this.ai_name, + clan: this.clan ? this.clan : '', + skins: this.psSkins, + dances: this.psDances, + }; +}; + +Player.prototype.getColor = function() { + var arr = playerTextColors[this.number] ? playerTextColors[this.number] : playerTextColors[0]; + return 'rgb(' + arr[0] + ', ' + arr[1] + ', ' + arr[2] + ')'; +}; + +Player.prototype.mineGold = function(amount) { + this.minedGold += amount; + this.minedGoldAtTicks[0] += amount; +}; + +Player.prototype.killProduction = function(type, finishTick) { + if (!this.production[type.id_string]) { + return; + } + + if (this.production[type.id_string].to.length <= 1) { + delete this.production[type.id_string]; + return; + } + + for (var i = 0; i < this.production[type.id_string].to.length; i++) { + if (this.production[type.id_string].to[i] == finishTick) { + this.production[type.id_string].to.splice(i, 1); + + if (this.production[type.id_string].from) { + this.production[type.id_string].from.splice(i, 1); + } + + return; + } + } +}; + +Player.prototype.startProduction = function(type, finishTick) { + if (!this.production[type.id_string]) { + this.production[type.id_string] = finishTick.type ? { to: [finishTick] } : { from: [ticksCounter], to: [finishTick] }; + } else { + if (finishTick.type) { + this.production[type.id_string].to.push(finishTick); + } else { + this.production[type.id_string].from.push(ticksCounter); + this.production[type.id_string].to.push(finishTick); + } + } +}; + +Player.prototype.unitDies = function(u) { + this.goldLost += u.type.cost; + + if (!u.type.isUnit) { + return; + } + + this.lostUnitTypes[u.type.id_string] = this.lostUnitTypes[u.type.id_string] ? this.lostUnitTypes[u.type.id_string] + 1 : 1; + + if (this.units[u.type.id_string] && this.units[u.type.id_string] > 1) { + this.units[u.type.id_string]--; + } else { + delete this.units[u.type.id_string]; + } +}; + +Player.prototype.unitSpawns = function(u) { + this.units[u.type.id_string] = this.units[u.type.id_string] ? this.units[u.type.id_string] + 1 : 1; +}; + +// returns the requirement text for a certain command and certain selected units; returns false, if there is no text (= the command can be ordered) +Player.prototype.getCommandRequirementText = function(command, units, target, learn) { + var requirement_text = ''; + + if (learn) { + var canLearn = false; + + for (var i = 0; i < units.length; i++) { + var requiredLevel = command.requiredLevels ? command.requiredLevels[units[i].abilityLevels[command.id]] : -1; + + if (units[i].level >= requiredLevel && units[i].level > units[i].countLearnedAbilities) { + canLearn = true; + } + } + + if (!canLearn) { + requirement_text += ' Higher level required.'; + } + + return requirement_text == '' ? false : requirement_text; + } + + // check for lvl + var lvl = 0; + for (var i = 0; i < units.length; i++) { + lvl += units[i].abilityLevels[command.id]; + } + + if (lvl == 0) { + requirement_text += ' This ability needs to be learned first.'; + } + + // check for mana + if (units) { + var hasMana = false; + for (var i = 0; i < units.length; i++) { + if (units[i].mana >= command.getValue('manaCost', units[i])) { + hasMana = true; + } + } + + if (!hasMana) { + requirement_text += ' Not enough mana.'; + } + } + + // check for gold + if (command.goldCost && units && units[0].owner.gold < command.goldCost) { + requirement_text += ' Not enough gold.'; + } + + // check if not disabled + if (command.type != COMMAND.MOVETO && command.type != COMMAND.MOVE && command.type != COMMAND.CANCEL && command.type != COMMAND.AMOVE && command.type != COMMAND.DANCE) { + var canOrder = false; + for (var i = 0; i < units.length; i++) { + if (units[i].type.commands[command.id_string] && !units[i].disabledCommands[command.id_string]) { + canOrder = true; + } + } + + if (!canOrder) { + requirement_text += ' This ability is disabled.'; + } + } + + // limit + if (command.unitType) { + var limit = command.unitType.getValue('limit', this); + if (limit) { + var count = 0; + + if (this.buildings[command.unitType.id_string]) { + count += this.buildings[command.unitType.id_string]; + } + + if (this.buildingsUC[command.unitType.id_string]) { + count += this.buildingsUC[command.unitType.id_string]; + } + + if (this.pseudoBuildings[command.unitType.id_string]) { + count += this.pseudoBuildings[command.unitType.id_string]; + } + + if (this.units[command.unitType.id_string]) { + count += this.units[command.unitType.id_string]; + } + + if (this.production[command.unitType.id_string]) { + count += this.production[command.unitType.id_string]; + } + + if (count >= limit) { + requirement_text += ' You can only have ' + count + '.'; + } + } + } + + // check for max lvl + if (command.upgrade && this.getUpgradeLevel(command.upgrade) + this.upgradeCountInResearch(command.upgrade) >= command.upgrade.maxLevel) { + requirement_text += ' Max level reached.'; + } + + // if no parallel research allowed and already researching, ... + if (command.upgrade && command.upgrade.noParallelResearch && this.upgradeCountInResearch(command.upgrade) > 0) { + requirement_text += ' Researching at the moment.'; + } + + // check for unique unit make + if (command.type == COMMAND.MAKEUNIT && command.unitType && command.unitType.uniqueAndHeroic && (this.units[command.unitType.id_string] > 0 || this.production[command.unitType.id_string])) { + requirement_text += ' You can only have one.'; + } + + if (command.requirementType) { + for (var i = 0; i < command.requirementType.length; i++) { + var t = command.requirementType[i]; + if (!t) { + throw Error(`Command ${command.id} requirement type ${i} was ${t}!\nFull Object:\n${JSON.stringify(command)}`); + } + if ((t && t.isBuilding && (this.buildings[t.id_string] ? this.buildings[t.id_string] : 0) < command.requirementLevel[i]) || (t.isUpgrade && (this.upgrades[t.id_string] ? this.upgrades[t.id_string] : 0) < command.requirementLevel[i])) { + requirement_text += ' ' + command.requirementText[i] + '.'; + } + } + } + + if (target && target.type) { + for (var k = 0; k < command.targetRequiremementsArray.length; k++) { + var met = false; + var text = ''; + + for (var i = 0; i < command.targetRequiremementsArray[k].length; i++) { + if (command.targetRequiremementsArray[k][i].func(target)) { + met = true; + } else { + text += command.targetRequiremementsArray[k][i].text + ' or '; + } + } + + if (!met) { + requirement_text += text.slice(0, text.length - 4) + ' '; + } + } + } + + return requirement_text == '' ? false : requirement_text; +}; + +Player.prototype.getCostOfNextInstanceForBuilding = function(type) { + if (!type.costIncrease) { + return type.cost; + } + + var cost = type.cost; + if (type.costIncreaseGroup) { + for (var i = 0; i < type.costIncreaseGroup.length; i++) { + cost += (this.buildings[type.costIncreaseGroup[i].id_string] ? this.buildings[type.costIncreaseGroup[i].id_string] : 0) * type.costIncrease; + cost += (this.buildingsUC[type.costIncreaseGroup[i].id_string] ? this.buildingsUC[type.costIncreaseGroup[i].id_string] : 0) * type.costIncrease; + cost += (this.pseudoBuildings[type.costIncreaseGroup[i].id_string] ? this.pseudoBuildings[type.costIncreaseGroup[i].id_string] : 0) * type.costIncrease; + } + } + + return cost; +}; + +// returns a modifier for a given unit field, based on which upgrades this player has researched +Player.prototype.getValueModifier = function(value, type, alsoUnderConstruction) { + var mod = (this.upgradeMods[type.id_string] && this.upgradeMods[type.id_string][value]) ? this.upgradeMods[type.id_string][value] : 0; + + if (alsoUnderConstruction) { + for (var i = 0; i < game.upgrades.length; i++) { + var u = game.upgrades[i]; + var count = this.upgradeCountInResearch(u); + + for (var k = 0; k < u.effectsTypes.length; k++) { + if (u.effectsFields[k] == value && u.effectsTypes[k] == type) { + mod += u.effectsModifications[k] * count; + } + + if (u.effectsModsMultiplier && k in u.effectsModsMultiplier) { + for (var j = 0; j < count; j++) { + mod += type[value] * (u.effectsModsMultiplier[k] - 1); + } + } + } + } + } + + return mod; +}; + +Player.prototype.getUpgradeLevel = function(upg) { + return this.upgrades[upg.id_string] ? this.upgrades[upg.id_string] : 0; +}; + +// used to get the color in which the unit selection circles are drawn +Player.prototype.getAllyColor = function(alpha) { + var alpha_ = alpha ? alpha : 0.9; + + // if the playing player is spectator, return the player color itself instead of the ally color + if (PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR && playerColors[this.number - 1]) { + var arr = playerColors[this.number - 1][4]; + return 'rgba(' + arr[0] + ', ' + arr[1] + ', ' + arr[2] + ', ' + alpha_ + ')'; + } + + // white, if this is the playing player + if (this == PLAYING_PLAYER) { + return 'rgba(' + game.theme.line_red + ', ' + game.theme.line_green + ', ' + game.theme.line_blue + ', ' + alpha_ + ')'; + } + + // if neutral (mine), yellow + if (this.controller == CONTROLLER.NONE) { + return 'rgba(255, 255, 0, ' + alpha_ + ')'; + } + + // red, if its an enemy player + if (this.isEnemyOfPlayer(PLAYING_PLAYER)) { + return 'rgba(224, 0, 0, ' + alpha_ + ')'; + } + + // blue, if allied player + return 'rgba(100, 160, 255, ' + alpha_ + ')'; +}; + +Player.prototype.isEnemyOfPlayer = function(player) { + if ( game.globalVars.isPassiveNeutral) { + return this.team.number != 0 && player.team.number != 0 && this.team != player.team; + } else { + return this.team != player.team; + } +}; + +Player.prototype.toString = function() { + return this.name; +}; + +Player.prototype.upgradeCountInResearch = function(upgrade) { + var c = 0; + + for (var i = 0; i < game.buildings.length; i++) { + var b = game.buildings[i]; + if (b.owner == this) { + for (var k = 0; k < BUILDING_QUEUE_LEN; k++) { + if (b.queue && b.queue[k] == upgrade) { + c++; + } + } + } + } + + return c; +}; + +// parent class for all map objects (units, buildings, doodads / tiles) +function MapObject() { + this.lastBlinkStart = 0; // last time this unit started blinking (when clicked on it); ms gametime + this.isActive = true; // is false, when the unit is in a gold mine for example + this.isAlive = true; + this.targetUnit = null; + this.hitCycle = 0; // when attacking, this gets ++ on every tick, to check when the actual attack happens and to check for cooldown + this.tickOfDeath = 0; // when did the unit die, used for playing dead animation + this.nextTickToCheckForNextEnemyUnit = 0; // we save this, because we dont wanna check every tick, but only every 5 ticks or so + this.provisionalTargetUnit = null; + this.kills = 0; + this.isDetectedUntil = -1; + this.level = 1; + this.countLearnedAbilities = 0; + this.queueFinish = 0; // how many ticks left, until the current producing unit is finished + this.queueStarted = false; + this.currentBuildTime = 0; + this.isThrowedUntil = -1; +}; + +MapObject.prototype.initAutocast = function() { + var arr = []; + + _.each(this.type.commands, function(cmd) { + if (cmd.hasAutocast && cmd.autocastDefault) { + arr.push(cmd.id); + } + }); + + return arr; +}; + +MapObject.prototype.getValue = function(value) { + return this.type[value] + this.owner.getValueModifier(value, this.type) + (this.modifierMods[value] ? this.modifierMods[value] : 0); +}; + +MapObject.prototype.includesField = function(x, y) { + return x >= this.x && y >= this.y && x <= this.x + this.type.sizeX - 1 && y <= this.y + this.type.sizeX - 1; +}; + +// return true if building / tile is inside a drawn box on the screen (coords relative to screen position, so we have to add camera coords) +MapObject.prototype.isInBox = function(x1, y1, x2, y2) { + return this.drawPos.px + this.type.sizeX / 2 >= x1 && this.drawPos.py + this.type.sizeY / 2 >= y1 && this.drawPos.px - this.type.sizeX / 2 <= x2 && this.drawPos.py - this.type.sizeY / 2 <= y2; +}; + +// return true if building / tile is inside a drawn box on the screen (coords relative to screen position, so we have to add camera coords) +MapObject.prototype.isInBoxVisible = function(x1, y1, x2, y2) { + return this.isInBox(x1, y1, x2, y2); +}; + +// remove the unit from the game. Most of the time, when it dies, but also when it enters a building +MapObject.prototype.remove = function() { + this.isActive = false; + + game.units.erease(this); + game.buildings.erease(this); + game.selectedUnits.erease(this); + + if (game.selectedUnits.length == 0) { + keyManager.resetCommand(); + } + + // delete from waypoints and target unitz + var units = game.units.concat(game.buildings); + for (var i = 0; i < units.length; i++) { + if (units[i].waypoint) { + for (var k = 0; k < units[i].waypoint.length; k++) { + if (units[i].waypoint[k] == this) { + units[i].waypoint.splice(k, 1); + k--; + } + } + } + + // delete from units targetUnits + if (units[i].targetUnit == this) { + units[i].targetUnit = null; + } + } + + if (this.type.uniqueAndHeroic) { + game.units4.push(this); + } + + // kill all graphic effects on this unit + for (var k = 0; k < this.effectsToDraw.length; k++) { + this.effectsToDraw[k].tickOfDeath = ticksCounter + 40; + } +}; + +MapObject.prototype.initHPAndMana = function() { + var maxHP = this.getValue('hp'); + var startHP = this.getValue('startHp'); + this.hp = startHP ? startHP : maxHP; + if (this.hp > maxHP) { + this.hp = maxHP; + } + + var maxMana = this.getValue('mana'); + var startMana = this.getValue('startMana'); + this.mana = startMana ? startMana : 0; + if (this.mana > maxMana) { + this.mana = maxMana; + } +}; + +// start blinking the unit circle (when rightclicked) +MapObject.prototype.blink = function() { + this.lastBlinkStart = timestamp; +}; + +MapObject.prototype.canLoad = function() { + return this.type.cargoSpace > 0; +}; + +MapObject.prototype.hasInQueue = function(type) { + if (!this.queue) { + return false; + } + + for (var i = 0; i < this.queue.length; i++) { + if (this.queue[i] == type) { + return true; + } + } + + return false; +}; + +MapObject.prototype.setAutocast = function(order, on) { + if (on) { + if (!this.autocast.contains(order)) { + this.autocast.push(order); + } + } else { + this.autocast.erease(order); + } +}; + +MapObject.prototype.hasModifierWithId = function(id) { + for (var i = 0; i < this.modifiers.length; i++) { + if (this.modifierMods[i].modId == id) { + return true; + } + } + + return false; +}; + +MapObject.prototype.applyModifier = function(mod, originUnit) { + var modId = parseInt(Math.random() * 99999999); + + this.modifiers.push({ + modifier: mod, + originUnit: originUnit, + removeAt: (mod.duration && mod.duration > 0) ? (ticksCounter + mod.getValue('duration', this.owner)) : -1, + modId: modId, + }); + + if (mod.effects) { + for (var i = 0; i < mod.effects.length; i++) { + startEffect(mod.effects[i], { + from: this, + attachToUnit: true, + modId: modId, + scale: (mod.auraRange && mod.auraRange[i]) ? (mod.auraRange[i] + 1) : 0, + auraColor: mod.auraColor, + mode: mod.particleMode, + density: mod.density, + }); + } + } + + if (mod.sound) { + startEffect('sound', { + from: this, + sound: mod.sound, + attachToUnit: true, + volume: mod.volume, + soundDuration: 980, + modId: modId, + }); + } + + this.checkUpgrades(); +}; + +MapObject.prototype.removeModifier = function(index) { + var mod = this.modifiers.splice(index, 1)[0]; + + // kill expired effects + for (var k = 0; k < this.effectsToDraw.length; k++) { + if (this.effectsToDraw[k].tickOfDeath <= ticksCounter) { + this.effectsToDraw.splice(k, 1); + k--; + } + } + + for (var k = 0; k < this.effectsToDraw.length; k++) { + if (this.effectsToDraw[k].modId == mod.modId) { + this.effectsToDraw[k].tickOfDeath = ticksCounter + 40; + } + } + + this.checkUpgrades(); +}; + +MapObject.prototype.checkUpgrades = function() { + this.modifierMods = {}; + this.disabledCommands = {}; + var customImg = null; + + var dataFields = this.type.getDataFields(); + + for (var i = 0; i < this.modifiers.length; i++) { + var mod = this.modifiers[i].modifier; + var dataField = dataFields[field]; + + if (mod.fields) { + for (var k = 0; k < mod.fields.length; k++) { + if (dataFields[mod.fields[k]]) { + var field = mod.fields[k]; + var dataField = dataFields[field]; + + if (dataField.type == 'float' || dataField.type == 'integer' || dataField.type == 'bool') { + if (mod.modificationsMultiplier && k in mod.modificationsMultiplier && (dataField.type == 'float' || dataField.type == 'integer')) { + this.modifierMods[field] = this.modifierMods[field] ? (this.modifierMods[field] + (mod.modificationsMultiplier[k] - 1) * this.type[field]) : ((mod.modificationsMultiplier[k] - 1) * this.type[field]); + } + + if (mod.modifications && mod.modifications[k]) { + this.modifierMods[field] = this.modifierMods[field] ? (this.modifierMods[field] + mod.modifications[k]) : mod.modifications[k]; + } + } + } + } + } + + if (mod.disabledCommands) { + for (var k = 0; k < mod.disabledCommands.length; k++) { + this.disabledCommands[mod.disabledCommands[k].id_string] = mod.disabledCommands[k].id_string; + } + } + + if (mod.unitImg && mod.changeUnitImg && this.type.isUnit) { + customImg = mod.unitImg; + } + }; + + if (customImg) { + this.img = customImg; + } else { + this.refreshImg(); + } + + var thisReference = this; + _.each(this.modifierMods, function(mod, field) { + var newVal = thisReference.getValue(field); + var checkVal = checkField(dataFields[field], newVal, true); + + if (checkVal != newVal) { + thisReference.modifierMods[field] -= newVal - checkVal; + } + }); +}; + +// blocks / unblocks all the containing fields of this building / tile. Usually used before and after searching a path to (the center of) this building. For this, is has to not block the pathfinding. +// Also used on creation or when killed +MapObject.prototype.switchBlocking = function(on, dontRefreshNBs) { + if (this.type.isUnit || !this.x || !this.y) { + return; + } + + // block / unblock fields + for (var x = this.x; x < this.x + this.type.sizeX; x++) { + for (var y = this.y; y < this.y + this.type.sizeY; y++) { + game.blockArray[x][y] = !on; + } + } + + if (dontRefreshNBs) { + return; + } + + // refresh neightbours of blocked / unblocked fields + for (var x = this.x - 1; x < this.x + this.type.sizeX + 1; x++) { + for (var y = this.y - 1; y < this.y + this.type.sizeY + 1; y++) { + game.refreshNBSOfField(game.fields[x][y]); + } + } + + // refresh neightbours of blocked / unblocked fields 2x2 + for (var x = Math.max(this.x - 2, 1); x < Math.min(this.x + this.type.sizeX + 2, game.x); x++) { + for (var y = Math.max(this.y - 2, 1); y < Math.min(this.y + this.type.sizeY + 2, game.y); y++) { + game.refreshNBSOfField2x2(game.fields2x2[x][y]); + } + } +}; + +MapObject.prototype.switchBlockingForTeam = function(on, team) { + if (this.type.isUnit || !this.x || !this.y) { + return; + } + + // block / unblock fields + for (var x = this.x; x < this.x + this.type.sizeX; x++) { + for (var y = this.y; y < this.y + this.type.sizeY; y++) { + team.blockArray[x][y] = !on; + } + } +}; + +MapObject.prototype.switchBlockingTotal = function(on) { + this.switchBlocking(on); + + // switch on blocking for all the teams + for (var i = 0; i < game.teams.length; i++) { + this.switchBlockingForTeam(on, game.teams[i]); + } +}; + +MapObject.prototype.getXP4NextLevel = function() { + if (!this.type.experienceLevels || this.type.experienceLevels.length <= 0) { + return 0; + } + + for (var i = 0; i < this.type.experienceLevels.length; i++) { + if (this.exp < this.type.experienceLevels[i]) { + return this.type.experienceLevels[i]; + } + } + + return this.type.experienceLevels[this.type.experienceLevels.length - 1]; +}; + +MapObject.prototype.getPercOfCurrentLevel = function() { + if (!this.type.experienceLevels || this.type.experienceLevels.length <= 0) { + return 0; + } + + for (var i = 0; i < this.type.experienceLevels.length; i++) { + if (this.exp < this.type.experienceLevels[i]) { + var oldExp = this.type.experienceLevels[i - 1] ? this.type.experienceLevels[i - 1] : 0; + return (this.exp - oldExp) / (this.type.experienceLevels[i] - oldExp); + } + } + + return 1; +}; + +MapObject.prototype.drawExpbar = function(x, y, w, h, linwWidth) { + var lineWidth_ = linwWidth ? linwWidth : 2; + + c.fillStyle = 'white'; + c.fillRect(x - lineWidth_, y - lineWidth_, w + lineWidth_ * 2, h + lineWidth_ * 2); + + c.fillStyle = 'rgba(125, 125, 125, 1)'; + + c.fillRect(x, y, w * this.getPercOfCurrentLevel(), h); +}; + +MapObject.prototype.drawHealthbar = function(x, y, w, h, linwWidth) { + var hpPercentage = this.hp / this.getValue('hp'); + + var lineWidth_ = linwWidth ? linwWidth : 2; + + c.fillStyle = 'white'; + c.fillRect(x - lineWidth_, y - lineWidth_, w + lineWidth_ * 2, h + lineWidth_ * 2); + + if (hpPercentage <= 0.25) { + c.fillStyle = 'rgba(255, 0, 0, 1)'; + } else if (hpPercentage <= 0.5) { + c.fillStyle = 'rgba(255, 125, 0, 1)'; + } else { + c.fillStyle = 'rgba(0, 150, 0, 1)'; + } + + c.fillRect(x, y, w * hpPercentage, h); +}; + +MapObject.prototype.drawManabar = function(x, y, w, h, linwWidth) { + var lineWidth_ = linwWidth ? linwWidth : 2; + + c.fillStyle = 'white'; + c.fillRect(x - lineWidth_, y - lineWidth_, w + lineWidth_ * 2, h + lineWidth_ * 2); + + c.fillStyle = 'rgba(200, 0, 200, 1)'; + + c.fillRect(x, y, w * (this.mana / this.getValue('mana')), h); +}; + +MapObject.prototype.drawLifetimebar = function(x, y, w, h, linwWidth) { + var lineWidth_ = linwWidth ? linwWidth : 2; + + c.fillStyle = 'white'; + c.fillRect(x - lineWidth_, y - lineWidth_, w + lineWidth_ * 2, h + lineWidth_ * 2); + + c.fillStyle = 'rgba(100, 100, 100, 1)'; + + c.fillRect(x, y, w * (this.lifetime / (this.type.lifetime + this.owner.getValueModifier('lifetime', this.type))), h); +}; + +MapObject.prototype.drawLoadbar = function(x, y, w, h, linwWidth) { + var lineWidth_ = linwWidth ? linwWidth : 2; + + c.fillStyle = 'white'; + c.fillRect(x - lineWidth_, y - lineWidth_, w + lineWidth_ * 2, h + lineWidth_ * 2); + + c.fillStyle = 'rgba(35, 35, 35, 1)'; + + var cargoSpace = this.getValue('cargoSpace'); + for (var i = 0; i < cargoSpace; i++) { + c.fillRect(x + (i / cargoSpace) * w + lineWidth_ / 2, y, (1 / cargoSpace) * w - lineWidth_, h); + } + + c.fillStyle = 'rgba(190, 190, 190, 1)'; + + var cargo = 0; + for (var i = 0; i < this.cargo.length; i++) { + c.fillRect(x + (cargo / cargoSpace) * w + lineWidth_ / 2, y, (this.cargo[i].type.cargoUse / cargoSpace) * w - lineWidth_, h); + cargo += this.cargo[i].type.cargoUse; + } + + c.stroke(); +}; + +MapObject.prototype.drawEye = function() { + c.globalAlpha *= 0.8 * ((Math.sin(ticksCounter) + 1) / 2 * 0.4 + 0.8); + + x = this.drawPos.px * FIELD_SIZE - imgs.eye.img.w * SCALE_FACTOR / 2 - game.cameraX; + y = (this.drawPos.py - this.type.healthbarOffset - 0.4) * FIELD_SIZE - game.cameraY - imgs.eye.img.h * ((SCALE_FACTOR + 1.0) / 2.0); + + c.drawImage(miscSheet[0], imgs.eye.img.x, imgs.eye.img.y, imgs.eye.img.w, imgs.eye.img.h, x, y, imgs.eye.img.w * SCALE_FACTOR, imgs.eye.img.h * SCALE_FACTOR); +}; + +// gets the tab prio of this object; higher prio's unit's commands get displayed when several units selected +MapObject.prototype.getTabPriority = function() { + return this.type.tabPriority + (this.isUnderConstruction ? -1 : 0); +}; + +MapObject.prototype.drawProgressBar = function(x, y, w, h, linwWidth) { + var lineWidth_ = linwWidth ? linwWidth : 2; + + var percentage = this.queueStarted ? ((this.currentBuildTime - (this.queueFinish - ticksCounter)) / this.currentBuildTime) : 0; + + drawBar(x, y, w, h, Math.min(percentage, 1), 'rgba(0, 160, 230, 1)', lineWidth_); +}; + +MapObject.prototype.toString = function() { + return this.type.name + ' @' + this.pos.px + ':' + this.pos.py; +}; + +// Class repreesenting a (movable) unit +Unit.prototype = new MapObject(); +function Unit(data) { + this.modifiers = []; + this.modifierMods = {}; + this.disabledCommands = {}; + + // basic attributes + this.pos = new Field(data.x, data.y, true); + this.type = data.type; + this.owner = data.owner; + + this.id = game.global_id++; + + // everything regarding the current order + this.order = lists.types.stop; + this.path = null; // the current path (array of fields) + this.target = null; // the current target; can be a field or a unit + this.carriedGoldAmount = 0; + this.goldMine = null; // the goldmine currently mining from + this.lastAttackingTick = -999; // the last tick, when this unit was attacking + this.tickOfLastWeaponFired = -999; // the last tick, when this unit was exactly doing damage + this.targetLockingUnit = null; // when a unit starts attacking, it locks on a unit (and will continue the current attack cycle, even if this unit moves out of range) + this.vision = this.type.vision; + this.cargo = []; + + // order queue + this.queueOrder = []; + this.queueTarget = []; + + // pushing / blocking + this.blocking = false; // can be pushed by friendly units (false when hold position for example) + this.lastTicksPosition = this.pos.getCopy(); // we need this for example to interpolate between last ticks and this ticks position when drawing (for more smooth looking movement) + this.drawPos = this.pos.getCopy(); // interpolation between last ticks position and this ticks position + + this.setYDrawingOffset(); + game.addObject(this); + + // adding for clipboard undo + if (game_state == GAME.EDITOR && data.noHistory!=true) { + editor.clipboard.history.addObject(this); + } + + this.hitOffsetPos = null; // when this unit is hit, draw pos is slightly offsettet, to demonstate the "power" of the hit + this.hitOffsetTill = 0; // how long to draw the hit modified pos + this.lastTickCircleEffect = 0; // last tick, when the circle blinking effect was started + this.oscillationOffset = Math.floor(Math.random() * 100); + + this.bodyPower = 1; + this.drawingHeight = 0; + this.yDrawingOffset = this.pos.py; + this.lastMiningTick = -999; + this.tickOfLastLoadIn = -1; + + // making units stuff + this.queue = []; // references to the unit types that are in production + this.autocast = this.initAutocast(); // ids of abilities that are on autocast + + this.animationOffset = Math.floor(Math.random() * 100); // random offset for animation, so not all unit have the same animation state at the same time + + this.revealedToTeamUntil = new Array(MAX_PLAYERS); + + this.flameDeath = false; + + if (ticksCounter > 0 && game.lastReadySound + 20 < ticksCounter && this.owner == PLAYING_PLAYER) { + soundManager.playSound(this.type.readySound, game.getVolumeModifier(this.drawPos) * this.type.readySoundVolume); + game.lastReadySound = ticksCounter; + game.lastYesSound = ticksCounter; + } + + this.throwStart = -1; + this.throwFrom = null; + this.throwTo = null; + + if (this.type.lifetime) { + this.lifetime = this.type.lifetime; + } + + this.abilityLevels = []; + this.lastTickAbilityUsed = []; + for (var i = 0; i < game.commands.length; i++) { + this.lastTickAbilityUsed[i] = -9999; + this.abilityLevels[i] = (game.commands[i] && game.commands[i].requiredLevels && game.commands[i].requiredLevels.length > 0) ? 0 : 1; + } + + // add to units for this player + if (this.owner) { + this.owner.unitSpawns(this); + } + + this.level = (this.type.experienceLevels && this.type.experienceLevels.length > 0) ? 1 : 0; + this.exp = 0; + + this.refreshImg(); + + this.direction = Math.floor(Math.random() * this.img._angles); // direction the unit is looking in + + this.effectsToDraw = []; // if this unit has modifiers for example, that make effect, the effects are attached here + + this.initHPAndMana(); +}; + +Unit.prototype.refreshImg = function() { + this.img = (this.owner.skins && this.owner.skins[this.type.id_string]) ? this.owner.skins[this.type.id_string] : this.type.img; +}; + +Unit.prototype.hasPath = function() { + return this.path != null; +}; + +Unit.prototype.distanceTo = function(otherUnit) { + if (!otherUnit) { + return 999999; + } + + return otherUnit.type.isBuilding ? otherUnit.distanceTo(this) : this.pos.distanceTo2(otherUnit.pos) - this.type.radius - otherUnit.type.radius; +}; + +// return true if unit is inside a drawn box; gameplay coordinates +Unit.prototype.isInBox = function(x1, y1, x2, y2) { + return this.drawPos.px + this.type.radius >= x1 && this.drawPos.py + this.type.radius >= y1 && this.drawPos.px - this.type.radius <= x2 && this.drawPos.py - this.type.radius <= y2; +}; + +// return true if unit is inside a drawn box; gameplay coordinates +Unit.prototype.isInBoxVisible = function(x1, y1, x2, y2) { + return this.drawPos.px + this.type.radius >= x1 && this.drawPos.py + this.type.radius >= y1 && this.drawPos.px - this.type.radius <= x2 && this.drawPos.py - this.type.radius <= y2 + this.type.height; +}; + +Unit.prototype.getYDrawingOffset = function() { + return this.yDrawingOffset; +}; + +Unit.prototype.setYDrawingOffset = function() { + this.yDrawingOffset = this.pos.py + this.type.radius - (this.isAlive ? 0 : 3) + (this.type.flying ? 2 : 0); +}; + +// interpolate between last ticks and this ticks position +Unit.prototype.updateDrawPosition = function() { + this.drawPos = this.lastTicksPosition.addNormalizedVector(this.pos, percentageOfCurrentTickPassed * this.pos.distanceTo2(this.lastTicksPosition)); + + if (this.type.flying) { + this.drawingHeight = this.drawingHeight + Math.max(Math.min((game.getHMValue3(this.drawPos) * CLIFF_HEIGHT - this.drawingHeight), gameTimeDiff * 2), -gameTimeDiff * 2); + this.drawPos = this.drawPos.add3(0, -this.drawingHeight); + } else { + this.drawPos = this.drawPos.add3(0, -game.getHMValue3(this.drawPos) * CLIFF_HEIGHT); + } + + if (this.hitOffsetTill > timestamp && this.hitOffsetPos) { + var t = 200 - (this.hitOffsetTill - timestamp); + var mod = t < 50 ? t / 50 : ((150 - (t - 50)) / 150); + + this.drawPos = this.drawPos.addNormalizedVector(this.hitOffsetPos, -0.04 * mod); + } +}; + +// draw; expects screen bounds (ingame coords) +Unit.prototype.draw = function() { + // if ourside if visible bounds or not visible (fogwise), return, because we dont need to draw anything + if (!PLAYING_PLAYER.team.canSeeUnit(this) || this.getValue('noShow')) { + return; + } + + var frame; + var frameWidth; + var scale = this.getValue('imageScale') * SCALE_FACTOR; + var img = this.img.idle; + + if (this.isThrowedUntil > ticksCounter && this.isAlive) { + var percDone = (ticksCounter + percentageOfCurrentTickPassed - this.throwStart) / (this.isThrowedUntil - this.throwStart); + var dist = this.throwFrom.distanceTo2(this.throwTo); + var isDeadForHowLong = ticksCounter - this.throwStart; + img = this.img.die ? this.img.die : this.img.idle; + + if (percDone < 0.5) { + this.drawPos = this.throwFrom.addNormalizedVector(this.throwTo, dist * percDone * 1.5); + this.pos = this.drawPos; + this.drawPos = this.drawPos.add3(0, -game.getHMValue2(this.drawPos.x, this.drawPos.y) * CLIFF_HEIGHT - (-Math.pow(percDone * 4 - 1, 2) + 1) * 0.20 * dist); + } else if (percDone < 0.75) { + this.drawPos = this.throwFrom.addNormalizedVector(this.throwTo, 0.75 * dist + dist * (percDone - 0.5) * 0.66); + this.pos = this.drawPos; + this.drawPos = this.drawPos.add3(0, -game.getHMValue2(this.drawPos.x, this.drawPos.y) * CLIFF_HEIGHT - (-Math.pow(percDone * 8 - 5, 2) + 1) * 0.06 * dist); + } else { + this.drawPos = this.throwFrom.addNormalizedVector(this.throwTo, 0.915 * dist + dist * (percDone - 0.75) * 0.33); + this.pos = this.drawPos; + this.drawPos = this.drawPos.add3(0, -game.getHMValue2(this.drawPos.x, this.drawPos.y) * CLIFF_HEIGHT); + } + + // create smoke effect when hit the ground + if (((percDone > 0.45 && percDone < 0.55) || (percDone > 0.7 && percDone < 0.8)) && tickDiff > 0) { + new Dust({ from: this.drawPos }); + } + + frameWidth = img.frameWidth; + frame = (this.isThrowedUntil - ticksCounter <= 10) ? Math.floor((this.isThrowedUntil - ticksCounter) / 10 * (img.w / frameWidth)) : Math.floor(Math.min(percDone * 2, 0.99) * (img.w / frameWidth)); + + var tileHeight = img.h / this.img._angles; + + // get drawing position + var x = this.drawPos.px * FIELD_SIZE - frameWidth * scale / 2 - game.cameraX; + var y = this.drawPos.py * FIELD_SIZE - (tileHeight - this.getValue('drawOffsetY')) * scale - game.cameraY; + + c.drawImage(this.img.file[this.owner.number], frame * frameWidth + img.x, this.direction * tileHeight + img.y, frameWidth, tileHeight, x, y, frameWidth * scale, tileHeight * scale); + + return; + } + + // if unit is dying, play death animation + if (!this.isAlive) { + var percDone = (ticksCounter + percentageOfCurrentTickPassed - this.throwStart) / (this.isThrowedUntil - this.throwStart); + var dist = this.throwFrom.distanceTo2(this.throwTo); + var isDeadForHowLong = (ticksCounter + percentageOfCurrentTickPassed) - this.throwStart; + img = this.img.die ? this.img.die : this.img.idle; + + if (this.type.removeAfterDeadAnimation && isDeadForHowLong > 100) { + game.objectsToDraw.erease(this); + return; + } + + if (percDone < 0.5) { + this.drawPos = this.throwFrom.addNormalizedVector(this.throwTo, dist * percDone * 1.5); + this.drawPos = this.drawPos.add3(0, -game.getHMValue2(this.drawPos.x, this.drawPos.y) * CLIFF_HEIGHT - (-Math.pow(percDone * 4 - 1, 2) + 1) * 0.20 * dist); + } else if (percDone < 0.75) { + this.drawPos = this.throwFrom.addNormalizedVector(this.throwTo, 0.75 * dist + dist * (percDone - 0.5) * 0.66); + this.drawPos = this.drawPos.add3(0, -game.getHMValue2(this.drawPos.x, this.drawPos.y) * CLIFF_HEIGHT - (-Math.pow(percDone * 8 - 5, 2) + 1) * 0.06 * dist); + } else if (percDone < 1) { + this.drawPos = this.throwFrom.addNormalizedVector(this.throwTo, 0.915 * dist + dist * (percDone - 0.75) * 0.33); + this.drawPos = this.drawPos.add3(0, -game.getHMValue2(this.drawPos.x, this.drawPos.y) * CLIFF_HEIGHT); + } else { + this.drawPos = this.throwTo.add3(0, -game.getHMValue2(this.throwTo.x, this.throwTo.y) * CLIFF_HEIGHT); + } + + this.pos = this.drawPos; + + // create smoke effect when hit the ground + if (((percDone > 0.45 && percDone < 0.55) || (percDone > 0.7 && percDone < 0.8)) && tickDiff > 0) { + new Dust({ from: this.drawPos }); + } + + frameWidth = img.frameWidth; + frame = Math.floor(Math.min(isDeadForHowLong * this.type.deathAnimationSpeed, img.w / frameWidth - 1)); + + var tileHeight = img.h / this.img._angles; + + // get drawing position + var x = this.drawPos.px * FIELD_SIZE - frameWidth * scale / 2 - game.cameraX; + var y = this.drawPos.py * FIELD_SIZE - (tileHeight - this.getValue('drawOffsetY')) * scale - game.cameraY; + + // if dead animation is done playing and this unit type has to be removed after dead animation done playing + if (this.type.removeAfterDeadAnimation && Math.floor(isDeadForHowLong * this.type.deathAnimationSpeed) == img.w / frameWidth && tickDiff) { + // create some random smoke effects + if (this.type.size > 2) { + for (var i = 0; i < 10; i++) { + new Dust({ + from: this.drawPos.add2(Math.random() * Math.PI * 2, Math.random() * this.type.size / 2), + scale: (Math.random() * 1.33 + 0.5) * this.drawPos, + }); + } + } + + // create side smoke effects (big dust clouds that go sideways) + for (var i = 0; i < Math.PI * 2; i += Math.random() * 1.5) { + new Dust({ + from: this.drawPos, + scale: (Math.random() * 1.66 + 0.5) * this.size, + ageScale: 0.66 * this.size, + vz: 0.01, + xFunction: function(age) { + return ((-1) / (age + 0.3) + 3) * this.x_ * this.size_; + }, + yFunction: function(age) { + return ((-1) / (age + 0.3) + 3) * this.y_ * this.size_; + }, + x_: Math.cos(i), + y_: Math.sin(i), + size_: this.size / 3, + }); + } + + // play hit ground sound + soundManager.playSound(this.type.slamSound ? this.type.slamSound : SOUND.FALL, game.getVolumeModifier(this.pos)); + } + + // fading out + if (this.type.removeAfterDeadAnimation) { + c.globalAlpha = Math.min(Math.max(1 + (img.w / frameWidth - isDeadForHowLong * this.type.deathAnimationSpeed) / 16, 0), 1); + } else { + c.globalAlpha = isDeadForHowLong > 35 ? Math.max(55 - isDeadForHowLong, 0) / 20 : 1; + } + + c.drawImage(this.img.file[this.owner.number], frame * frameWidth + img.x, this.direction * tileHeight + img.y, frameWidth, tileHeight, x, y, frameWidth * scale, tileHeight * scale); + + if (this.flameDeath) { + if (c.globalAlpha > 0.5) { + // create random flames + for (var i = 0; i < tickDiff; i++) { + if (Math.random() < 0.07) { + new Sprite({ + from: this.drawPos.add3(0, -0.3).add2(Math.random() * Math.PI * 2, 0.1), + img: imgs['fire' + (Math.floor(Math.random() * 4) + 1)], + scaleFunction: function(age) { + return 1.25 + age * 0.8; + }, + alphaFunction: function(age) { + return 0.5; + }, + }); + } + } + + // create random zmoke + for (var i = 0; i < tickDiff; i++) { + if (Math.random() < 0.1) { + new Dust({ from: this.drawPos, scale: 1 + Math.random(), ageScale: 2 + Math.random() }); + } + } + } + } + + c.globalAlpha = 1; + + // eventually remove unit from the game + if (isDeadForHowLong > 100) { + game.objectsToDraw.erease(this); + } + + return; + } + + var angle = 999; + var isAttacking = (this.lastAttackingTick + 6 >= ticksCounter || (this.order && this.order.type == COMMAND.REPAIR) || this.lastMiningTick + 1 >= ticksCounter) || (this.order && this.order.animationName); + + if (this.order && this.order.type == COMMAND.DANCE) { + img = this.img[this.order.dance_img] ? this.img[this.order.dance_img] : this.img.idle; + frameWidth = img.frameWidth; + frame = Math.floor(ticksCounter / 3) % (img.w / frameWidth); + } else if (this.forcedAnimation && this.img[this.forcedAnimation] && this.forcedAnimationStop >= ticksCounter && this.forcedAnimationStart >= 0) { + img = this.img[this.forcedAnimation]; + frameWidth = img.frameWidth; + angle = 0; + frame = Math.min(Math.max(Math.floor((ticksCounter - this.forcedAnimationStart) / (this.forcedAnimationStop - this.forcedAnimationStart) * (img.w / frameWidth)), 0), img.w / frameWidth - 1); + } + + // if walk + else if ((!this.lastTicksPosition.equals(this.pos) || (this.type.flying && !isAttacking)) && !(this.type.flying && isAttacking)) { + img = (this.carriedGoldAmount && this.img.walkGold) ? this.img.walkGold : (this.img.walk ? this.img.walk : this.img.idle); + frameWidth = img.frameWidth; + + if (!this.lastTicksPosition.equals(this.pos)) { + angle = this.lastTicksPosition.getAngleTo(this.pos); + } + + frame = Math.floor(((ticksCounter + this.animationOffset + this.oscillationOffset) / this.type.animSpeed + (this.hitOffsetTill > timestamp ? 1 : 0)) % (img.w / frameWidth)); + + // dust effect + if (tickDiff > 0 && Math.random() < this.type.dustCreationChance) { + new Dust({ from: this.drawPos.add3(0, 0.2) }); + } + } + + // if attack + else if (isAttacking) { + img = (this.order && this.order.animationName && this.img[this.order.animationName]) ? this.img[this.order.animationName] : (this.img.attack ? this.img.attack : this.img.idle); + + if (this.targetUnit || this.target) { + angle = this.pos.getAngleTo(this.targetUnit ? this.targetUnit.pos : this.target); + } + + frameWidth = img.frameWidth; + + var weaponDelay = (this.order && this.order.castingDelay) ? this.order.getValue('castingDelay', this) : this.type.weaponDelay; + var cooldown = (this.order && this.order.cooldown) ? this.order.cooldown : this.type.weaponCooldown; + + if (this.type.flying) { + frame = Math.floor(((ticksCounter + this.oscillationOffset) / this.type.animSpeed) % (img.w / frameWidth)); + } else if (ticksCounter - this.tickOfLastWeaponFired <= cooldown + 1) // if weapon has been fired recently and is cooldowning now + { + frame = Math.floor(((ticksCounter - this.tickOfLastWeaponFired + weaponDelay) % cooldown) / cooldown * (img.w / frameWidth)); + } else { + frame = Math.floor((this.hitCycle % cooldown) / cooldown * (img.w / frameWidth)); + } + } + + // if idle + else { + frameWidth = img.frameWidth; + frame = this.type.idleFrames ? this.type.idleFrames[Math.min(Math.floor(ticksCounter / 4 + this.pos.py * 123241) % this.type.idleFrames.length, parseInt(img.w / frameWidth))] : (Math.max((Math.floor((ticksCounter + this.animationOffset) / 5) % (img.w / frameWidth + 20)) - 20, 0)); + } + + if (angle != 999) // if a new angle has been calculated, calculate the new direction from it + { + angle += angle < -Math.PI ? Math.PI * 2 : 0; + angle -= angle > Math.PI ? Math.PI * 2 : 0; + + if (this.img._angles == 8) { + if (angle >= Math.PI * 3 / 8 && angle <= Math.PI * 5 / 8) { + this.direction = 0; + } else if (angle <= -Math.PI * 3 / 8 && angle >= -Math.PI * 5 / 8) { + this.direction = 3; + } else if (angle >= Math.PI * 7 / 8 || angle <= -Math.PI * 7 / 8) { + this.direction = 1; + } else if ((angle <= Math.PI * 1 / 8 && angle >= 0) || (angle >= -Math.PI * 1 / 8 && angle <= 0)) { + this.direction = 2; + } else if (angle <= Math.PI * 7 / 8 && angle >= Math.PI * 5 / 8) { + this.direction = 4; + } else if (angle <= Math.PI * 3 / 8 && angle >= Math.PI * 1 / 8) { + this.direction = 5; + } else if (angle >= -Math.PI * 7 / 8 && angle <= -Math.PI * 5 / 8) { + this.direction = 6; + } else { + this.direction = 7; + } + } else if (this.img._angles == 1) { + this.direction = 0; + } else { + if (angle >= Math.PI / 4 && angle <= Math.PI * 3 / 4) { + this.direction = 0; + } else if (angle <= -Math.PI / 4 && angle >= -Math.PI * 3 / 4) { + this.direction = 3; + } else if (angle >= Math.PI * 3 / 4 || angle <= -Math.PI * 3 / 4) { + this.direction = 1; + } else { + this.direction = 2; + } + } + } + + var tileHeight = (this.order && this.order.type == COMMAND.DANCE) ? img.h : (img.h / this.img._angles); + + // get drawing position + var x = this.drawPos.px * FIELD_SIZE - frameWidth / 2 * scale - game.cameraX; + var y = this.drawPos.py * FIELD_SIZE - (tileHeight - this.getValue('drawOffsetY')) * scale - game.cameraY; + + // if flying, calculate oscillation frquency height + if (this.type.flying && this.img.walk) { + var countFrames = this.img.walk.w / this.img.walk.frameWidth; + y += Math.sin(((((ticksCounter + this.oscillationOffset + percentageOfCurrentTickPassed + 3.8 * this.type.animSpeed) / this.type.animSpeed) % countFrames) / countFrames) * Math.PI * 2) * FIELD_SIZE * this.type.oscillationAmplitude; + } + + // invis alpha + if (this.getValue('isInvisible')) { + c.globalAlpha = (this.isDetectedUntil >= ticksCounter || !this.owner.isEnemyOfPlayer(PLAYING_PLAYER)) ? 0.5 : 0.07; + } else { + c.globalAlpha = 1; + } + + if (this.order && this.order.type == COMMAND.DANCE) { + this.direction = 0; + } + + // unit image itself + c.drawImage(this.img.file[this.owner.number], frame * frameWidth + img.x, this.direction * tileHeight + img.y, frameWidth, tileHeight, x, y, frameWidth * scale, tileHeight * scale); + + if (this.getValue('hasDetection')) { + this.drawEye(); + } + + c.globalAlpha = 1; + + //debugs + if(show_unit_details && !network_game) + { + c.fillStyle = "rgba(0, 0, 0, 1)"; + c.fillText(this.order.name, this.drawPos.px * FIELD_SIZE - game.cameraX - FIELD_SIZE / 3, (this.drawPos.py - this.type.healthbarOffset) * FIELD_SIZE - game.cameraY - 12); + + // show detailed path to next target + /*if(this.path) + { + c.strokeStyle = "yellow"; + c.beginPath(); + c.moveTo(this.drawPos.px * FIELD_SIZE - game.cameraX, this.drawPos.py * FIELD_SIZE - game.cameraY); + c.lineTo(this.path.px * FIELD_SIZE - game.cameraX, this.path.py * FIELD_SIZE - game.cameraY); + c.stroke(); + }*/ + } +}; + +Unit.prototype.popQueueFront = function() { + if (this.queue) { + this.owner.killProduction(this.queue[0], this.queueFinish); + + // update queue + for (var k = 0; k < BUILDING_QUEUE_LEN - 1; k++) { + this.queue[k] = this.queue[k + 1]; + } + this.queue[BUILDING_QUEUE_LEN - 1] = null; + + // if more units in queue, set build time according to their build time, else 0 + if (this.queue[0]) { + this.queueFinish = this.queue[0].getValue('buildTime', this.owner) + ticksCounter; + this.currentBuildTime = this.queueFinish - ticksCounter; + // this.owner.startProduction(this.queue[0], this.queueFinish); + } + + this.queueStarted = false; + } +}; + +Building.prototype = new MapObject(); + +// Class for Building, expects a json object with several paramters +function Building(data) { + _.extend(this, data); + + // mines always belong to player 0 (neutral) + if (this.type.alwaysNeutral) { + this.owner = game.players[0]; + } + + this.isUnderConstruction = this.buildFirst ? true : false; + this.buildTicksLeft = 0; // when under construction, when does it finish + this.seenBy = new Array(MAX_PLAYERS + 1); // contains a value, determing in which state this building has been seen by player [index] the last time he saw it + this.seenByAsBuildingType = new Array(MAX_PLAYERS + 1); // buildings can transform, so here the building type is stored in which this building has been seen by player [index] + + this.workload = [0]; + this.workload_total = 0; + this.countWorkingWorkers = 0; + this.lastCountWorkingWorkers = 0; + + this.vision = this.buildFirst ? 3 : this.type.vision; + + // neutral buildings are always seen by all players + if (this.owner.controller == CONTROLLER.NONE && !this.isDummy) { + for (var i = 0; i < this.seenBy.length; i++) { + this.seenBy[i] = BUILDING_STATE.NORMAL; + } + + // switch on blocking for all the teams + for (var i = 0; i < game.teams.length; i++) { + this.switchBlockingForTeam(true, game.teams[i]); + } + } + + // team 0 always sees everything + if (game_state == GAME.EDITOR) { + this.seenBy[0] = BUILDING_STATE.NORMAL; + } + + // making units stuff + this.queue = []; // references to the unit types that are in production + this.autocast = this.initAutocast(); // ids of abilities that are on autocast + + this.abilityLevels = new Array(game.commands.length); + this.lastTickAbilityUsed = new Array(game.commands.length); + for (var i = 0; i < game.commands.length; i++) { + this.lastTickAbilityUsed[i] = -9999; + this.abilityLevels[i] = (game.commands[i] && game.commands[i].requiredLevels && game.commands[i].requiredLevels.length > 0) ? 0 : 1; + } + + this.pos = new Field(this.x - 1 + this.type.size / 2, this.y - 1 + this.type.size / 2, true); // the position of a building (or doodad) = center + this.drawPos = this.pos.add3(0, -game.getHMValue2(this.x, this.y) * CLIFF_HEIGHT); // all map object need a draw pos. Its not gameplay relevant, but only used for drawing. For units this is interpolated between this and last ticks real pos, so the movement is smooth. For buildings and doodads ist just static + + // in case of gold mine, store gold value + if (this.type.startGold) { + this.gold = this.type.startGold; + } + + // dummys are used, when you place a building and the transparent building is drawn while the worker moves to the location + if (!this.isDummy) { + this.switchBlocking(true, this.dontRefreshNBs); // mark the fields in the grid as blocked + this.id = game.global_id++; // ids are used to identify the building in network games + + if (this.type.lifetime) { + this.lifetime = this.type.lifetime; + } + } + + this.yDrawingOffset = this.pos.py + this.type.size / 2; + game.addObject(this); + + // editor clipboard for undo + if (game_state == GAME.EDITOR && data.noHistory != true) { + editor.clipboard.history.addObject(this); + } + + this.modifiers = []; + this.modifierMods = {}; + + // if building is beeing constructed (when made in game and not on map load) + if (this.buildFirst) { + this.buildTicksLeft = this.getValue('buildTime'); + this.hp *= BUILDING_START_HP_PERCENTAGE; + + if (PLAYING_PLAYER.team.canSeeUnit(this)) { + this.massSmoke(); + soundManager.playSound(SOUND.BUILD, game.getVolumeModifier(this.pos) * 0.5); + } + + this.owner.startProduction(this.type, this); + } + + this.borderLeft = this.drawPos.px - this.type.size / 2; + this.borderRight = this.drawPos.px + this.type.size / 2; + this.borderTop = this.drawPos.py - this.type.size / 2; + this.borderBottom = this.drawPos.py + this.type.size / 2; + + this.lastRepairedTick = -1; // if this building is under construction, this is set to the current tick, when the constucting worker (whos technically repairing it), gets updated; all other repairing workers will use repair stats then in stead of construction stats + + this.targetsQueue = []; + this.disabledCommands = {}; + + this.effectsToDraw = []; // if this unit has modifiers for example, that make effect, the effects are attached here + + this.initHPAndMana(); +}; + +Building.prototype.refreshImg = function() { + this.img = this.type.img; +}; + +Building.prototype.massSmoke = function() { + for (var i = 0; i < 12; i++) { + new Dust({ from: this.drawPos.add2(Math.random() * Math.PI * 2, this.type.size / 1.5) }); + } +}; + +// return true if building / tile is inside a drawn box on the screen +Building.prototype.isInBox = function(x1, y1, x2, y2) { + return this.borderRight >= x1 && this.borderBottom >= y1 && this.borderLeft <= x2 && this.borderTop <= y2; +}; + +Building.prototype.getWorkload = function() { + return Math.min(Math.ceil((this.workload_total / 400) * 100), 100); +}; + +// determines the z index when drawing; when building is dying atm, return a lower z index, so the building does not overlap with the explosion effect +Building.prototype.getYDrawingOffset = function() { + return this.yDrawingOffset; +}; + +Building.prototype.draw = function() { + // if ourside of visible bounds, return, because we dont need to draw anything + if ((!this.seenBy[PLAYING_PLAYER.team.number] && !this.isDummy) || this.getValue('noShow')) { + return; + } + + c.globalAlpha = this.isDummy ? 0.4 : 1; + + var state = this.seenBy[PLAYING_PLAYER.team.number]; + var canSeeNow = PLAYING_PLAYER.team.canSeeUnit(this, true); + var img = this.seenByAsBuildingType[PLAYING_PLAYER.team.number] ? this.seenByAsBuildingType[PLAYING_PLAYER.team.number].img : this.type.img; + var scale = this.getValue('imageScale') * SCALE_FACTOR; + var x = 0; + var y = 0; + var w = 0; + var h = 0; + + if (state == BUILDING_STATE.UNDER_CONSTRUCTION && img.constructionImg) { + var frame = img.constructionImg.frame ? Math.min(img.constructionImg.frame[ticksCounter % img.constructionImg.frame.length], parseInt(img.constructionImg.w / img.constructionImg.frameWidth) - 1) : 0; + x = img.constructionImg.x + frame * img.constructionImg.frameWidth; + y = img.constructionImg.y; + w = img.constructionImg.frameWidth; + h = img.constructionImg.h; + } else if (state == BUILDING_STATE.DEAD) { + return; + } else if (state == BUILDING_STATE.BUSY && img.busyImgs) { + var frame = (canSeeNow && img.busyImgs.frames) ? Math.min(img.busyImgs.frames[ticksCounter % img.busyImgs.frames.length], parseInt(img.busyImgs.w / img.busyImgs.frameWidth) - 1) : 0; + x = img.busyImgs.x + frame * img.busyImgs.frameWidth; + y = img.busyImgs.y; + w = img.busyImgs.frameWidth; + h = img.busyImgs.h; + } else if (state == BUILDING_STATE.BUSY_DAMAGED && img.busyDamagedImgs) { + var frame = (canSeeNow && img.busyDamagedImgs.frames) ? Math.min(img.busyDamagedImgs.frames[ticksCounter % img.busyDamagedImgs.frames.length], parseInt(img.busyDamagedImgs.w / img.busyDamagedImgs.frameWidth) - 1) : 0; + x = img.busyDamagedImgs.x + frame * img.busyDamagedImgs.frameWidth; + y = img.busyDamagedImgs.y; + w = img.busyDamagedImgs.frameWidth; + h = img.busyDamagedImgs.h; + } else if (state == BUILDING_STATE.UPGRADING && img.upgradeImg) { + var frame = img.upgradeImg.frames ? Math.min(img.upgradeImg.frames[ticksCounter % img.upgradeImg.frames.length], parseInt(img.upgradeImg.w / img.upgradeImg.frameWidth) - 1) : 0; + x = img.upgradeImg.x + frame * img.upgradeImg.frameWidth; + y = img.upgradeImg.y; + w = img.upgradeImg.frameWidth; + h = img.upgradeImg.h; + } else if (state == BUILDING_STATE.UPGRADING_DAMAGED && img.upgradeImgDamaged) { + var frame = img.upgradeImgDamaged.frames ? Math.min(img.upgradeImgDamaged.frames[ticksCounter % img.upgradeImgDamaged.frames.length], parseInt(img.upgradeImgDamaged.w / img.upgradeImgDamaged.frameWidth) - 1) : 0; + x = img.upgradeImgDamaged.x + frame * img.upgradeImgDamaged.frameWidth; + y = img.upgradeImgDamaged.y; + w = img.upgradeImgDamaged.frameWidth; + h = img.upgradeImgDamaged.h; + } else if (state == BUILDING_STATE.DAMAGED && img.damagedImg) { + var frame = img.damagedImg.frames ? Math.min(img.damagedImg.frames[ticksCounter % img.damagedImg.frames.length], parseInt(img.damagedImg.w / img.damagedImg.frameWidth) - 1) : 0; + x = img.damagedImg.x + frame * img.damagedImg.frameWidth; + y = img.damagedImg.y; + w = img.damagedImg.frameWidth; + h = img.damagedImg.h; + } + + // if mine and empty + else if (state == BUILDING_STATE.EMPTY && img.imgEmpty) { + var frame = img.imgEmpty.frames ? Math.min(img.imgEmpty.frames[ticksCounter % img.imgEmpty.frames.length], parseInt(img.imgEmpty.w / img.imgEmpty.frameWidth) - 1) : 0; + x = img.imgEmpty.x + frame * img.imgEmpty.frameWidth; + y = img.imgEmpty.y; + w = img.imgEmpty.frameWidth; + h = img.imgEmpty.h; + } else // normal + { + var frame = img.img.frames ? Math.min(img.img.frames[ticksCounter % img.img.frames.length], parseInt(img.img.w / img.img.frameWidth) - 1) : 0; + x = img.img.x + frame * img.img.frameWidth; + y = img.img.y; + w = img.img.frameWidth; + h = img.img.h; + } + + if ((state == BUILDING_STATE.BUSY || state == BUILDING_STATE.BUSY_DAMAGED) && canSeeNow && this.type.busySmokeEffectLocationX && tickDiff > 0 && ticksCounter % 10 == 0) { + new Dust({ from: this.drawPos.add3(this.type.busySmokeEffectLocationX, this.type.busySmokeEffectLocationY), scale: 2, ageScale: 2, height: this.type.busySmokeEffectLocationZ }); + } + + var target_x = this.drawPos.px * FIELD_SIZE - w * scale / 2 - game.cameraX; + var target_y = (this.drawPos.py + this.type.size / 2) * FIELD_SIZE - h * scale - game.cameraY; + + // invis alpha + if (this.getValue('isInvisible')) { + c.globalAlpha = (this.isDetectedUntil >= ticksCounter || !this.owner.isEnemyOfPlayer(PLAYING_PLAYER)) ? 0.5 : 0.07; + } + + c.drawImage(img.file[this.owner.number], x, y, w, h, target_x, target_y, w * scale, h * scale); + + if (!this.isDummy && this.getValue('hasDetection') && !this.isUnderConstruction) { + this.drawEye(); + } + + c.globalAlpha = 1; + + if (this.isDummy) { + return; + } + + // create dust effects randomly if under construction + if ((this.lastRepairedTick + 1 >= ticksCounter || state == BUILDING_STATE.UPGRADING || state == BUILDING_STATE.UPGRADING_DAMAGED) && canSeeNow) { + for (var i = 0; i < tickDiff; i++) { + if (Math.random() < 0.15) { + new Dust({ from: this.drawPos.add2(Math.random() * Math.PI * 2, Math.random() * this.type.size / 1.7).add3(0, 3), scale: Math.random() + 1.5, height: 3 }); + } + } + } +}; + +Building.prototype.popQueueFront = function() { + if (this.queue) { + this.owner.killProduction(this.queue[0], this.queueFinish); + + // update queue + for (var k = 0; k < BUILDING_QUEUE_LEN - 1; k++) { + this.queue[k] = this.queue[k + 1]; + } + this.queue[BUILDING_QUEUE_LEN - 1] = null; + + // if more units in queue, set build time according to their build time, else 0 + if (this.queue[0]) { + this.queueFinish = this.queue[0].getValue('buildTime', this.owner) + ticksCounter; + this.currentBuildTime = this.queueFinish - ticksCounter; + // this.owner.startProduction(this.queue[0], this.queueFinish); + } + + this.queueStarted = false; + } +}; + +var searchColors = new Array(11); +// searchColors = new Array(18); + +// Player Colors (the search colors will get replaced with those) +var playerColors = [ + + [ // (player 1, red) + [35, 1, 1], + [66, 1, 1], + [67, 11, 8], + [113, 38, 27], + [138, 50, 37], + [161, 60, 46], + [174, 72, 72], + [90, 24, 24], + [123, 45, 45], + [188, 101, 101], + [125, 65, 65], + [185, 25, 25], + ], + + [ // (player 2, blue) + [1, 5, 35], + [1, 7, 66], + [9, 15, 145], + [38, 45, 194], + [54, 61, 212], + [86, 93, 238], + [72, 84, 174], + [24, 24, 90], + [45, 45, 123], + [101, 101, 188], + [65, 65, 125], + [25, 25, 150], + ], + + [ // (player 3, green) + [1, 35, 1], + [3, 66, 1], + [19, 77, 10], + [36, 116, 23], + [51, 143, 36], + [69, 169, 53], + [86, 174, 72], + [24, 90, 24], + [45, 123, 45], + [101, 188, 101], + [65, 135, 65], + [0, 200, 0], + ], + + [ // (player 4, white) + [30, 30, 30], + [50, 50, 50], + [92, 92, 92], + [187, 187, 187], + [217, 217, 217], + [239, 239, 239], + [225, 225, 225], + [80, 80, 80], + [150, 150, 150], + [200, 200, 200], + [205, 205, 205], + [255, 255, 255], + ], + + [ // (player 5, black) + [1, 1, 1], + [5, 5, 5], + [19, 19, 19], + [36, 36, 36], + [51, 51, 51], + [69, 69, 69], + [86, 86, 86], + [24, 24, 24], + [45, 45, 45], + [101, 101, 101], + [65, 65, 65], + [0, 0, 0], + ], + + [ // (player 6, yellow) + [35, 35, 1], + [66, 66, 1], + [77, 77, 10], + [116, 116, 23], + [143, 143, 36], + [169, 169, 53], + [174, 174, 72], + [90, 90, 24], + [123, 123, 45], + [188, 188, 101], + [135, 135, 65], + [200, 200, 0], + ], +]; + +var playerTextColors = [ + [204, 204, 204], + [255, 100, 100], + [153, 153, 255], + [153, 255, 153], + [255, 255, 255], + [100, 100, 100], + [255, 255, 100], +]; + +var building_imgs = { + + castle: { + img: { x: 0, y: 0, w: 76, h: 92, frameWidth: 76 }, + constructionImg: { x: 80, y: 13, w: 75, h: 78, frameWidth: 76 }, + damagedImg: { x: 158, y: 0, w: 76, h: 92, frameWidth: 76 }, + busyImgs: { x: 235, y: 0, w: 152, h: 92, frameWidth: 76, frames: [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1] }, + busyDamagedImgs: { x: 188, y: 378, w: 152, h: 92, frameWidth: 76, frames: [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1] }, + upgradeImg: { x: 2, y: 596, w: 76, h: 92, frameWidth: 76 }, + upgradeImgDamaged: { x: 80, y: 596, w: 76, h: 92, frameWidth: 76 }, + }, + + barracks: { + img: { x: 539, y: 160, w: 61, h: 79, frameWidth: 61 }, + constructionImg: { x: 417, y: 174, w: 60, h: 64, frameWidth: 60 }, + damagedImg: { x: 478, y: 160, w: 61, h: 79, frameWidth: 61 }, + busyImgs: { x: 478, y: 318, w: 122, h: 79, frameWidth: 61, frames: [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1] }, + busyDamagedImgs: { x: 478, y: 239, w: 122, h: 79, frameWidth: 61, frames: [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1] }, + }, + + watchtower: { + img: { x: 2, y: 190, w: 43, h: 71, frameWidth: 43 }, + constructionImg: { x: 46, y: 216, w: 43, h: 46, frameWidth: 43 }, + damagedImg: { x: 90, y: 191, w: 43, h: 71, frameWidth: 43 }, + }, + + house: { + img: { x: 538, y: 0, w: 62, h: 73, frameWidth: 62 }, + constructionImg: { x: 478, y: 17, w: 58, h: 56, frameWidth: 62 }, + damagedImg: { x: 417, y: 3, w: 59, h: 69, frameWidth: 62 }, + }, + + mine: { + img: { x: 3, y: 265, w: 60, h: 82, frameWidth: 60 }, + imgEmpty: { x: 127, y: 265, w: 60, h: 82, frameWidth: 60 }, + }, + + mages_guild: { + img: { x: 1, y: 353, w: 60, h: 76, frameWidth: 60 }, + constructionImg: { x: 63, y: 369, w: 58, h: 60, frameWidth: 58 }, + damagedImg: { x: 123, y: 354, w: 60, h: 76, frameWidth: 60 }, + busyImgs: { x: 1, y: 434, w: 120, h: 76, frameWidth: 60, frames: [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1] }, + busyDamagedImgs: { x: 1, y: 514, w: 120, h: 76, frameWidth: 60, frames: [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1] }, + }, + + workshop: { + img: { x: 4, y: 94, w: 72, h: 92, frameWidth: 72 }, + constructionImg: { x: 77, y: 101, w: 77, h: 85, frameWidth: 77 }, + damagedImg: { x: 157, y: 93, w: 72, h: 92, frameWidth: 72 }, + busyImgs: { x: 237, y: 93, w: 144, h: 92, frameWidth: 72, frames: [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1] }, + busyDamagedImgs: { x: 196, y: 283, w: 144, h: 92, frameWidth: 72, frames: [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1] }, + }, + + forge: { + img: { x: 548, y: 81, w: 84, h: 76, frameWidth: 84 }, + constructionImg: { x: 482, y: 90, w: 66, h: 69, frameWidth: 66 }, + damagedImg: { x: 395, y: 82, w: 84, h: 76, frameWidth: 84 }, + busyImgs: { x: 431, y: 404, w: 168, h: 76, frameWidth: 84, frames: [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1] }, + busyDamagedImgs: { x: 135, y: 190, w: 168, h: 76, frameWidth: 84, frames: [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1] }, + }, + + start_location: { + img: { x: 351, y: 405, w: 66, h: 55, frameWidth: 66 }, + }, + + fortress: { + img: { x: 128, y: 480, w: 76, h: 110, frameWidth: 76 }, + damagedImg: { x: 204, y: 480, w: 76, h: 110, frameWidth: 76 }, + busyImgs: { x: 280, y: 480, w: 152, h: 110, frameWidth: 76, frames: [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1] }, + busyDamagedImgs: { x: 432, y: 480, w: 152, h: 110, frameWidth: 76, frames: [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1] }, + }, + + dragons_lair: { + img: { x: 228, y: 595, w: 62, h: 93, frameWidth: 62 }, + constructionImg: { x: 169, y: 644, w: 52, h: 44, frameWidth: 52 }, + damagedImg: { x: 295, y: 595, w: 62, h: 93, frameWidth: 62 }, + busyImgs: { x: 363, y: 595, w: 62, h: 93, frameWidth: 62, frames: [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1] }, + busyDamagedImgs: { x: 432, y: 595, w: 62, h: 93, frameWidth: 62, frames: [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1] }, + }, + + wolves_den: { + img: { x: 3, y: 697, w: 65, h: 65, frameWidth: 65 }, + constructionImg: { x: 71, y: 697, w: 65, h: 65, frameWidth: 65 }, + damagedImg: { x: 277, y: 697, w: 65, h: 65, frameWidth: 65 }, + busyImgs: { x: 139, y: 697, w: 130, h: 65, frameWidth: 65, frames: [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1] }, + busyDamagedImgs: { x: 347, y: 697, w: 130, h: 65, frameWidth: 65, frames: [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1] }, + upgradeImg: { x: 417, y: 866, w: 65, h: 72, frameWidth: 65 }, + upgradeImgDamaged: { x: 485, y: 866, w: 65, h: 72, frameWidth: 65 }, + }, + + animal_testing_lab: { + img: { x: 5, y: 770, w: 77, h: 86, frameWidth: 77 }, + constructionImg: { x: 87, y: 777, w: 70, h: 79, frameWidth: 70 }, + damagedImg: { x: 317, y: 770, w: 77, h: 86, frameWidth: 77 }, + busyImgs: { x: 160, y: 770, w: 154, h: 86, frameWidth: 77, frames: [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1] }, + busyDamagedImgs: { x: 395, y: 770, w: 154, h: 86, frameWidth: 77, frames: [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1] }, + }, + + adv_workshop: { + img: { x: 640, y: 2, w: 84, h: 126, frameWidth: 84 }, + constructionImg: { x: 641, y: 265, w: 84, h: 82, frameWidth: 84 }, + damagedImg: { x: 640, y: 133, w: 84, h: 126, frameWidth: 84 }, + busyImgs: { x: 640, y: 2, w: 252, h: 126, frameWidth: 84, frames: [0, 0, 0, 1, 1, 1, 2, 2, 2] }, + busyDamagedImgs: { x: 640, y: 133, w: 252, h: 126, frameWidth: 84, frames: [0, 0, 0, 1, 1, 1, 2, 2, 2] }, + }, + + werewolves_den: { + img: { x: 3, y: 866, w: 65, h: 72, frameWidth: 65 }, + damagedImg: { x: 208, y: 866, w: 65, h: 72, frameWidth: 65 }, + busyImgs: { x: 71, y: 866, w: 130, h: 72, frameWidth: 65, frames: [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1] }, + busyDamagedImgs: { x: 277, y: 866, w: 130, h: 72, frameWidth: 65, frames: [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1] }, + }, + + church: { + img: { x: 4, y: 943, w: 74, h: 103, frameWidth: 74 }, + constructionImg: { x: 472, y: 965, w: 78, h: 81, frameWidth: 78 }, + damagedImg: { x: 82, y: 943, w: 74, h: 103, frameWidth: 74 }, + busyImgs: { x: 159, y: 943, w: 148, h: 103, frameWidth: 74, frames: [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1] }, + busyDamagedImgs: { x: 316, y: 943, w: 148, h: 103, frameWidth: 74, frames: [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1] }, + }, +}; + +const sleep = (t) => new Promise((resolve) => setTimeout(resolve, t)); + +async function retry(f, period, timeout) { + do { + try { + f(); + return; + } catch (e) {} + await sleep(period); + timeout -= period; + } while (timeout > 0); +} + +function is(val) { + return !(val === undefined || val === null); +} + +let topZIndex = 1000; +function fadeIn(jQueryElement) { + if (jQueryElement[0].style.display == 'inline') { + return; + } + + jQueryElement[0].style.display = 'inline'; + jQueryElement[0].style.zIndex = topZIndex++; + + jQueryElement.css({ + opacity: 0, + top: (parseInt(jQueryElement.css('top')) - 30) + 'px', + }).animate({ + opacity: 1, + top: (parseInt(jQueryElement.css('top')) + 30) + 'px', + }, 200); +} + +function fadeOut(jQueryElement) { + if (jQueryElement[0].style.display == 'none') { + return; + } + + jQueryElement[0].style.display = 'inline'; + + jQueryElement.css({ + opacity: 1, + }).animate({ + opacity: 0, + top: (parseInt(jQueryElement.css('top')) + 30) + 'px', + }, 200, function() { + jQueryElement.css({ + display: 'none', + top: (parseInt(jQueryElement.css('top')) - 30) + 'px', + }); + }); + + if (jQueryElement.attr('id') == 'infoWindow2') { + fadeOut($('#darkScreenDiv')); + } +} + +function addClickSound(callback) { + return () => { + callback(); soundManager.playSound(SOUND.CLICK); + }; +} + +function addZipSound(callback) { + return () => { + callback(); soundManager.playSound(SOUND.ZIP, 0.3); + }; +} + +function generateButton(htmlID, htmlClass, title, clickHandler, text) { + const builder = new HTMLBuilder(); + builder.add(' $(`#${htmlID}`).click(clickHandler)); + } + builder.add('>'); + if (text) { + builder.add(text); + } + return builder.add(''); +} + +// Takes either an HTML element or an HTMLBuilder and adds it to the currently active chat window +// which is either the main lobby chat or a game lobby chat +function addToChatWindow(p) { + const chatWindowSelector = LobbyPlayerManager.active ? '#lobbyGameChatTextArea' : '#lobbyChatTextArea'; + + if (p && p.insertInto) { + const chatID = uniqueID('chat'); + $(chatWindowSelector).append(``); + p.insertInto(`#${chatID}`); + if (LobbyPlayerManager.active) { + soundManager.playSound(SOUND.ZIP3, 0.7); + } + } else { + $(chatWindowSelector)[0].appendChild(p); + } + + const chatWindow = $(chatWindowSelector)[0]; + if (chatWindow.scrollTop + chatWindow.offsetHeight >= chatWindow.scrollHeight - 250) { + chatWindow.scrollTop = chatWindow.scrollHeight; + } +} + +// calculate the build time in ticks for all unit types and upgrade types +function calculateTypesTickValues() { + var types_ = game.buildingTypes.concat(game.unitTypes); + for (var i = 0; i < types_.length; i++) { + var t = types_[i]; + t.radius = t.size / 2; + + // if unit + if (t.movementSpeed >= 0) { + // calculate circle offsets depending on the radius (is used to check if a unit collides with a static object) + t.circleOffsets = []; + for (k = 0; k < checkAngles.length; k++) { + var field = new Field(checkAngles[k][0], checkAngles[k][1], true).normalize(t.radius); + t.circleOffsets.push([field.px, field.py]); + } + } + + if (t.size) { + t.sizeX = t.size; + t.sizeY = t.size; + } + } +} + + +var fogMaskAlpha = [ + 0.75, + 0.5, + 0, +]; + +var darkFogMaskAlpha = [ + 1, + 0.6, + 0, +]; + + +AUTH_LEVEL = Object.freeze({ + NONE: 0, + GUEST: 1, + PLAYER: 2, + MOD: 3, + ADMIN: 4, +}); + +var auth_level_css_classes = [ + '', + 'playerLinkGuest', + 'playerLinkRegistered', + 'playerLinkMod', + 'playerLinkAdmin', +]; + +var clan_member_role_names = [ + '', + 'moderator', + 'admin', +]; + +var GAME = Object.freeze({ + LOGIN: 0, + REGISTER: 1, + PLAYING: 3, + SKIRMISH: 4, // AI + EDITOR: 5, + LOBBY: 6, + RECOVERY: 8, + ACCEPT_AGB: 9, +}); + +var game_speeds = [ + { caption: '1/2x', tick_time: 100 }, + { caption: '1x', tick_time: 50 }, + { caption: '2x', tick_time: 25 }, + { caption: '4x', tick_time: 13 }, + { caption: '8x', tick_time: 6 }, + { caption: '16x', tick_time: 3 }, +]; + +var game_state = GAME.LOGIN; + +// TODO: replace this with a non-polling solution +const onGameStateChange = (() => { + const callbacks = []; + let prevGameState = null; + + setInterval(() => { + if (prevGameState != game_state) { + callbacks.forEach((callback) => callback(game_state)); + } + prevGameState = game_state; + }, 100); + + return (callback) => callbacks.push(callback); +})(); + +function addChatMsg(sender, msg, emotes) { + const builder = new HTMLBuilder(); + const el = PlayersList.players[sender]; + const escapedMsg = (sender == 'Server') ? msg : kappa(escapeHtml(msg), emotes); + + if (escapedMsg && escapedMsg.length > 0) { + builder.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('
'); + builder.add('You reached level '); + builder.add(`${splitMsg[1]}

`); + + builder.add('
'); + builder.add(`
`); + builder.add('
'); + builder.add(`${splitMsg[2]} / ${xp2}
`); + if (splitMsg[3] > 0) { + builder.add(`
`); + builder.add(`+${splitMsg[3]} `); + builder.add('
'); + } + + storedAchievements.push({ + builder: builder, + sound: SOUND.ARCHIVEMENT3, + }); + + showAchievement(); + AccountInfo.gold += parseInt(splitMsg[3]); + + for (var i = 4; i < splitMsg.length; i++) { + unlockEmote(splitMsg[i]); + } +}; +// TODO: Review this +function alphabetizePlayers(p_dict) { + // Grabs all players, separates by authLevel/Premium status, then alphabetizes them + + gst_arr = []; + acc_arr = []; + pre_arr = []; + mod_arr = []; + dev_arr = []; + alpha_arr = []; + new_dict = {}; + + /* Separate players by their authLevel and Premium Status: + authLevel 1 = Guest + authLevel 2 = Normal Account + authLevel 2 w/ Premium = Premium + authLevel 3 = Moderator + authLevel 4 = Developer + */ + + for (let auth in p_dict) { + if (p_dict[auth].authLevel == '1') { + gst_arr.push(p_dict[auth].name.toLowerCase()); + } else if ((p_dict[auth].authLevel == '2') && (p_dict[auth].premium != true)) { + acc_arr.push(p_dict[auth].name.toLowerCase()); + } else if ((p_dict[auth].authLevel == '2') && (p_dict[auth].premium == true)) { + pre_arr.push(p_dict[auth].name.toLowerCase()); + } else if (p_dict[auth].authLevel == '3') { + mod_arr.push(p_dict[auth].name.toLowerCase()); + } else if (p_dict[auth].authLevel == '4') { + dev_arr.push(p_dict[auth].name.toLowerCase()); + } + } + + gst_arr.sort(); + acc_arr.sort(); + pre_arr.sort(); + mod_arr.sort(); + dev_arr.sort(); + + var omega_arr = alpha_arr.concat(dev_arr, mod_arr, pre_arr, acc_arr, gst_arr); + + for (let name in omega_arr) { + for (let p in p_dict) { + if (omega_arr[name] == p_dict[p].name.toLowerCase()) { + new_dict[name] = p_dict[p]; + } + } + } + + return new_dict; +} + +function secToDate(sec) { + var str = ''; + + if (sec > 60 * 60 * 24) { + str += parseInt(sec / (60 * 60 * 24)) + ' days, '; + sec = sec % (60 * 60 * 24); + } + + if (sec > 60 * 60) { + str += parseInt(sec / (60 * 60)) + ' hours, '; + sec = sec % (60 * 60); + } + + str += Math.ceil(sec / 60) + ' min'; + + return str; +} + +function unlockEmote(artNr, newPremiumTime) { + var arch_ = emotes.concat(skins, dances, goldPacks, specials, _emotes2); + for (var i = 0; i < arch_.length; i++) { + if (arch_[i].artNr == artNr) { + fadeOut($('#playerInfoWindow')); + + var text = ''; + var img = ''; + var text2 = ''; + + if (arch_[i].type == 'skins') { + var img = unit_imgs[arch_[i].img]; + + var w = img.idle.frameWidth; + var h = img.idle.h / img._angles; + + if (w > h) { + h = 160 * (h / w); + w = 160; + } else { + w = 160 * (w / h); + h = 160; + } + + var w2 = img.file[1].width * (w / img.idle.frameWidth); + var h2 = img.file[1].height * (h / (img.idle.h / img._angles)); + var x = img.idle.x * (w2 / img.file[1].width); + var y = img.idle.y * (h2 / img.file[1].height); + + text = 'Skin ' + arch_[i].name + ' unlocked'; + img = '
'; + text2 = 'Select this skin from the \'Skins and Dances\' menu to use it. Works in multiplayer games only.'; + } else if (arch_[i].type == 'dances') { + var img = unit_imgs[arch_[i].img]; + + var w = img.idle.frameWidth; + var h = img.idle.h / img._angles; + + if (w > h) { + h = 160 * (h / w); + w = 160; + } else { + w = 160 * (w / h); + h = 160; + } + + var w2 = img.file[1].width * (w / img.idle.frameWidth); + var h2 = img.file[1].height * (h / (img.idle.h / img._angles)); + var x = img.idle.x * (w2 / img.file[1].width); + var y = img.idle.y * (h2 / img.file[1].height); + + text = 'Dance ' + arch_[i].name + ' unlocked'; + img = '
'; + text2 = 'In a multiplayer game, select one or more units of this type and type \'' + arch_[i].chat_str + '\' in chat to make them dance.'; + } else if (arch_[i].type == 'emotes') { + text = (arch_[i].hidd ? 'Hidden ' : '') + 'Emote ' + arch_[i].name + ' unlocked'; + img = ''; + text2 = 'Type \'' + arch_[i].text + '\' in lobby chat to use this emote.'; + } else if (arch_[i].type == 'gold') { + text = 'Received ' + arch_[i].reward + ' gold'; + img = ''; + AccountInfo.gold += parseInt(arch_[i].reward); + } else if (arch_[i].type == 'special') { + if (arch_[i].type_2 == 'premium') { + text = 'Premium account unlocked'; + img = ''; + AccountInfo.premiumExpiry = newPremiumTime; + } + } + + storedAchievements.push({ + text: img + '




' + text + '
' + text2 + '
', + sound: SOUND.ARCHIVEMENT, + }); + + showAchievement(); + + return; + } + } +}; + +function unlockAchivement(index) { + var a = achivements[index]; + + if (!a) { + return; + } + + storedAchievements.push({ + text: '


Achievement ' + a.name + ' unlocked
(' + a.text + ')

Reward: ' + a.reward + '
', + sound: SOUND.ARCHIVEMENT, + }); + + AccountInfo.gold += a.reward; + showAchievement(); +}; + +function showAchievement() { + if ($('#infoWindow2').css('display') == 'none' && storedAchievements.length > 0 && game_state != GAME.PLAYING) { + var a = storedAchievements.splice(0, 1)[0]; + // TODO: deprecate text member of storedAchievements + displayInfoMsgDarkBG(a.builder || a.text); + soundManager.playSound(a.sound); + } +}; + +function showAchievementsWindow(ach_str) { + uimanager.playerInfoWindow.setTitle('Achievements'); + + var str = ''; + + for (var i = 0; i < achivements.length; i++) { + var a = achivements[i]; + var rewardStr = a.reward ? `(reward: ${a.reward} gold)` : ''; + str += '
`; + str += '

' + a.name + '

'; + } + + $('#addScrollableSubDivTextArea').html(str); + fadeIn($('#playerInfoWindow')); +}; + + +function checkDBPos(pos, dbEntry) { + return dbEntry.length >= pos && dbEntry.substr(dbEntry.length - pos, 1) == '1'; +}; + +function animateDances(offsets, maxs, xs, ys) { + setTimeout(function() { + if ($('#dance_img_0').is(':visible')) { + for (var i = 0; i < offsets.length; i++) { + var offset = parseFloat($('#dance_img_' + i)[0].style.marginLeft) - offsets[i]; + + if (offset < (-maxs[i] + offsets[i] / 2)) { + offset = 0; + } + + $('#dance_img_' + i)[0].style.marginLeft = (offset - xs[i]) + 'px'; + $('#dance_img_' + i)[0].style.marginTop = -ys[i] + 'px'; + } + + animateDances(offsets, maxs, xs, ys); + } + }, 150); +}; + +function hex32ToBin(s) { + var returnVal = ''; + + for (var i = s.length - 1; i >= 0; i--) { + var char_ = s.substr(i, 1); + var val = !isNaN(char_) ? parseInt(char_) : char_.charCodeAt(0) - 'A'.charCodeAt(0) + 10; + + for (var k = 1; k <= 5; k++) { + var modulu = val % Math.pow(2, k); + returnVal = (modulu == 0 ? '0' : '1') + returnVal; + val -= modulu; + } + } + + return returnVal; +}; + +function showImprint() { + soundManager.playSound(SOUND.CLICK); + $('#addScrollableSubDivTextArea2').html('

littlewargame.com

Owner:
Addicting Games, Inc.
15332 Antioch Street Los Angeles
Suite 200
California 90272
USA
email: chris@addictinggames.com
'); + fadeIn($('#playerInfoWindow2')); + uimanager.playerInfoWindow2.setTitle('Imprint'); +}; + +function getItemFromArtNr(artNr) { + var arr = emotes.concat(skins, dances, goldPacks, specials, _emotes2); + + for (var i = 0; i < arr.length; i++) { + if (arr[i].artNr == artNr) { + return arr[i]; + } + } + + return null; +}; + +function onSubmit(e) { + if (!$('#check1').prop('checked')) { + displayInfoMsg('You have to accept the text'); + e.preventDefault(); + return false; + } + + if (!$('#check2').prop('checked')) { + displayInfoMsg('You have to accept the terms and conditions'); + e.preventDefault(); + return false; + } + + var radioVal = $('input[name="legal_radio"]:checked').val(); + + if (!radioVal) { + displayInfoMsg('You have to select a cancellation policy option'); + e.preventDefault(); + return false; + } + + $('#buy_form_item_number')[0].value = $('#original_value')[0].value + '_' + radioVal; + + setTimeout(function() { + $('#addScrollableSubDivTextArea').html('
Waiting while payment is being handled ...
'); + }, 1000); + + return true; +}; + +function showAGB() { + $.ajax({ + dataType: 'text', + url: 'agb.html', + }).done(function(data) { + soundManager.playSound(SOUND.CLICK); + uimanager.playerInfoWindow2.setTitle('Terms and conditions'); + $('#addScrollableSubDivTextArea2').html('
' + data + '
'); + fadeIn($('#playerInfoWindow2')); + $('#addScrollableSubDivTextArea2')[0].scrollTop = 0; + }); +}; + +function showWRE() { + $.ajax({ + dataType: 'text', + url: 'wre.html', + }).done(function(data) { + soundManager.playSound(SOUND.CLICK); + uimanager.playerInfoWindow2.setTitle('Cancellation policy'); + $('#addScrollableSubDivTextArea2').html('
' + data + '
'); + fadeIn($('#playerInfoWindow2')); + $('#addScrollableSubDivTextArea2')[0].scrollTop = 0; + }); +}; + +function showDSE() { + $.ajax({ + dataType: 'text', + url: 'dse.html', + }).done(function(data) { + soundManager.playSound(SOUND.CLICK); + uimanager.playerInfoWindow2.setTitle('Privacy Policy'); + $('#addScrollableSubDivTextArea2').html('
' + data + '
'); + fadeIn($('#playerInfoWindow2')); + $('#addScrollableSubDivTextArea2')[0].scrollTop = 0; + }); +}; + +// validates if a username is correct +function validatePlayerName(name) { + return name.length > 2 && name.length < 21 && name.match(/[A-Za-z0-9\-\_]*/) == name; +}; + +function killFaqMsg0() { + soundManager.playSound(SOUND.CLICK); + displayConfirmPrompt( + 'Hide this message? You will still be able to read the FAQs by clicking the FAQ button in the options window.', + killFaqMsg, + () => {}, + ); +}; + +// TODO: remove this +const hideFAQ = LocalConfig.registerValue('hide_faq', false); +function killFaqMsg() { + soundManager.playSound(SOUND.CLICK); + $('#faqContainer').remove(); + hideFAQ.set(true); + fadeOut($('#infoWindow')); +}; + +const hideTutorials = LocalConfig.registerValue('hide_tutorials', false); +function killTutorialButton() { + soundManager.playSound(SOUND.CLICK); + $('#tutorialButtonSpan').remove(); + hideTutorials.set(true); +}; + +function testMap() { + mapData = game.export_(false); // save map data, so after ending test game, we can go back to the editor and reload the start map state + mapData.img = getImageFromMap(game.export_(true)); + uimanager.showLoadingScreen(mapData); // show loading screen + + setTimeout(function() { + game_state = GAME.PLAYING; + game = new Game(); + game.loadMap(mapData); + + worker.postMessage({ + what: 'start-game', + fromEditor: true, + map: mapData, + network_game: network_game, + game_state: game_state, + networkPlayerName: networkPlayerName, + aiRandomizer: Math.ceil(Math.random() * 100000), + }); + }, 50); + + game.playingFromEditor = true; +} + +// is called, when a user enters values in the x/y-size inputs of the new map window in the editor, checks if theyre valid and makes then, if not +function checkNewMapInputs() { + var x = parseInt($('#newMapSizeX')[0].value); + var y = parseInt($('#newMapSizeY')[0].value); + + if (!x || !y) { + displayInfoMsg('Please enter valid numbers.'); + if (!x) { + $('#newMapSizeX')[0].value = MIN_MAP_SIZE; + } + if (!y) { + $('#newMapSizeY')[0].value = MIN_MAP_SIZE; + } + return false; + } + if (x < MIN_MAP_SIZE || y < MIN_MAP_SIZE || x > MAX_MAP_SIZE || y > MAX_MAP_SIZE) { + displayInfoMsg(`Invalid map size, must be between ${MIN_MAP_SIZE}x${MIN_MAP_SIZE} and ${MAX_MAP_SIZE}x${MAX_MAP_SIZE}.`); + if (x < MIN_MAP_SIZE) { + $('#newMapSizeX')[0].value = MIN_MAP_SIZE; + } + if (y < MIN_MAP_SIZE) { + $('#newMapSizeY')[0].value = MIN_MAP_SIZE; + } + if (x > MAX_MAP_SIZE) { + $('#newMapSizeX')[0].value = MAX_MAP_SIZE; + } + if (y > MAX_MAP_SIZE) { + $('#newMapSizeY')[0].value = MAX_MAP_SIZE; + } + return false; + } + + $('#newMapSizeX')[0].value = x; + $('#newMapSizeY')[0].value = y; + return true; +}; + +var onDrag = function(e, ui) { + var fiftyPercent = ui.helper.css('marginLeft').replace('px', '') < 0; + + var left = ui.position.left - (fiftyPercent ? (ui.helper.width() / 2) : 0); + + if (left < 10) { + ui.position.left = 10 + (fiftyPercent ? (ui.helper.width() / 2) : 0); + } + + if (ui.position.top < 10) { + ui.position.top = 10; + } + + if (left + ui.helper.width() > WIDTH - 10) { + ui.position.left = WIDTH - 10 - ui.helper.width() / (fiftyPercent ? 2 : 1); + } + + if (ui.position.top + ui.helper.height() > HEIGHT - 10) { + ui.position.top = HEIGHT - 10 - ui.helper.height(); + } +}; + +function drawCircle(x, y, size, color, fillColor, yScale, lineWidth) { + var yScale_ = yScale ? yScale : 0.7; + c.lineWidth = 0.75 * SCALE_FACTOR * (lineWidth ? lineWidth : 1); + c.beginPath(); + c.ellipse(x, y, size, size * yScale_, 0, 2 * Math.PI, false); + + if (color) { + c.strokeStyle = color; + c.stroke(); + } + + if (fillColor) { + c.fillStyle = fillColor; + c.fill(); + } +}; + +function drawBar(x, y, w, h, percentage, color, lineWidth) { + var lineWidth_ = lineWidth ? Math.floor(lineWidth) : 2; + + c.fillStyle = 'white'; + c.fillRect(x - lineWidth_, y - lineWidth_, w + lineWidth_ * 2, h + lineWidth_ * 2); + c.fillStyle = color; + c.fillRect(x, y, w * percentage, h); +}; + +function getPlNfo(player) { + soundManager.playSound(SOUND.CLICK); + network.send(JSON.stringify({ message: 'get-player-info', properties: { username: player } })); +}; + +function getLgNfo(nr) { + soundManager.playSound(SOUND.CLICK); + network.send(JSON.stringify({ message: 'get-league', properties: { leagueId: nr } })); +}; + +function getLeagueLink(rank, addName, scale, noLink) { + const builder = new HTMLBuilder(); + scale = scale || 1; + + // Add link + if (!noLink) { + const linkID = uniqueID('leagueLink'); + builder.add(``); + builder.addHook(() => $(`#${linkID}`).click(() => getLgNfo(rank))); + } + + // Set offset + const offset = -10 * scale; + builder.add(``); + + if (addName) { + builder.add(` ${leagueNames[rank]}`); + } + if (!noLink) { + builder.add(''); + } + + return builder; +}; + +function bingMsg(msg, noSound) { + if (game_state == GAME.PLAYING) { + return; + } + + if (!noSound) { + soundManager.playSound(SOUND.BING2, 0.65); + } + + $('#bingMessageWindow').html('

' + 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, '
')); + b.html('edit'); + } + + soundManager.playSound(SOUND.CLICK); +} + +function importGraphic() { + // 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.readAsDataURL(file); + reader.onload = function(e) { + var i = 1; + var name = 'newimg' + i; + while (customImgs[name]) { + i++; + name = 'newimg' + i; + } + + var newImg = new Image(); + newImg.src = e.currentTarget.result; + + newImg.onload = function() { + customImgs[name] = [newImg].concat(ImageTransformer.replaceColors(newImg, searchColors, playerColors)); + refreshCustomImgs(); + }; + }; + } + }; + + soundManager.playSound(SOUND.CLICK); +}; + +function refreshCustomImgs() { + const builder = new HTMLBuilder(); + + for (key in customImgs) { + if (!unit_imgs[key] && !building_imgs[key] && key != 'buildingSheet' && key != 'tileSheet' && key != 'miscSheet') { + const imgSrc = customImgs[key][0].toDataURL ? customImgs[key][0].toDataURL() : customImgs[key][0].src; + builder.add(`
`); + + const killImgButtonID = uniqueID('killImage'); + builder.add(`
`); + builder.addHook(() => $(`#${killImgButtonID}`).click(() => killCustomImg(key))); + } + } + + builder.insertInto('#customGraphicsDiv'); +}; + +function saveCustomGraphics() { + var divs = $('.customImgPrv').toArray(); + + for (var i = 0; i < divs.length; i++) { + var newName = $(divs[i]).children('input').toArray()[0].value; + var oldName = divs[i].title; + + if (newName != divs[i].title) { + if (newName.length > 20 || newName.length < 3 || newName.match(/[A-Za-z0-9]*/) != newName) { + displayInfoMsg(newName + ' is not a valid name (3 - 20 chars, letters and numbers only)!'); + return; + } + + if (customImgs[newName]) { + displayInfoMsg(newName + ' is already used!'); + return; + } + + customImgs[newName] = customImgs[oldName]; + + delete customImgs[oldName]; + } + } + + fadeOut($('#customGraphicsWindow')); + soundManager.playSound(SOUND.CLICK); +}; + +function killCustomImg(key) { + if (!unit_imgs[key] && !building_imgs[key] && customImgs[key]) { + for (var i = 0; i < game.graphics.length; i++) { + if (game.graphics[i].file == customImgs[key]) { + game.graphics[i].file = miscSheet; + } + } + + delete customImgs[key]; + } + + refreshCustomImgs(); + + soundManager.playSound(SOUND.CLICK); +}; + +function reallyBan(player) { + soundManager.playSound(SOUND.CLICK); + displayInfoMsg(new HTMLBuilder() + .add(`Ban ${player} for how many hours? (0 is permanent)

`) + .add('

') + .add('Reason for ban:

') + .add('') + .addHook(() => $('#confirmBanButton').click(addClickSound(() => { + let confirmed = true; + if ($('#time2Ban').val() == '0') { + confirmed = confirm('Permanently ban this player?'); + } + if (confirmed) { + network.send(`ban<<$${player}<<$${$('#time2Ban').val()}<<$${$('#reason4Ban').val()}`); + } + })))); +}; + +function getPlayerNameArrayFromPlayerSettingsArrayObject(o) { + var arr = []; + for (var i = 0; i < o.length; i++) { + arr.push(o[i].name); + } + return arr; +}; + +function getFormattedTime() { + // get time (hh:mm) + var timeObject = new Date(); + var minutes = timeObject.getMinutes(); + minutes = minutes < 10 ? '0' + minutes : minutes; + return timeObject.getHours() + ':' + minutes; +}; + +// Prints the duration as HH:MM:SS, excluding HH if inapplicable +function getFormattedDuration(millis) { + let seconds = Math.floor(millis / 1000); + const hours = Math.floor(seconds / 3600); + seconds -= hours * 3600; + const minutes = Math.floor(seconds / 60); + seconds -= minutes * 60; + + const paddedHours = String(hours).padStart(2, '0'); + const paddedMinutes = String(minutes).padStart(2, '0'); + const paddedSeconds = String(seconds).padStart(2, '0'); + if (hours > 0) { + return `${paddedHours}:${paddedMinutes}:${paddedSeconds}`; + } else { + return `${paddedMinutes}:${paddedSeconds}`; + } +} + +function toggleFullscreen(element) { + if (element.requestFullScreenWithKeys) { + if (!document.fullScreen) { + element.requestFullScreenWithKeys(); + } else { + document.exitFullScreen(); + } + } + + if (element.requestFullScreen) { + if (!document.fullScreen) { + element.requestFullscreen(); + } else { + document.exitFullScreen(); + } + } else if (element.mozRequestFullScreen) { + if (!document.mozFullScreen) { + element.mozRequestFullScreen(); + } else { + document.mozCancelFullScreen(); + } + } else if (element.webkitRequestFullScreen) { + if (!document.webkitIsFullScreen) { + element.webkitRequestFullScreen(element.ALLOW_KEYBOARD_INPUT); + } else { + document.webkitCancelFullScreen(); + } + } + + resize(); +}; + +function createExplosion(x, y, size) { + var pos = new Field(x, y); + + for (var i = 0; i < 15; i++) { + new Sprite({ + from: pos.add2(Math.random() * Math.PI * 2, Math.random()), + img: imgs.particle.img, + scaleFunction: i < 10 ? function() { + return 4; + } : function() { + return 6; + }, + age: 1.3 + Math.random(), + r1: Math.random() * 0.4, + r2: Math.random() * 5 - 3, + r3: Math.random() * 5 - 3, + zFunction: function(age) { + return Math.min(Math.pow(age * 1.6 - 1.5 + this.r1, 2) - 2.2, 0); + }, + xFunction: function(age) { + return Math.sqrt(age) * this.r2; + }, + yFunction: function(age) { + return Math.sqrt(age) * this.r3; + }, + }); + } + + new Dust({ from: pos, scale: size + Math.random(), ageScale: 2 + Math.random() }); + + for (var i = 0; i <= 2; i++) { + new Dust({ from: pos.add3(Math.random() - 0.5, 0), scale: size + Math.random(), ageScale: 2 + Math.random(), height: Math.random() }); + } + + // create big fire and smoke effects + for (var i = 0; i <= 2; i++) { + setTimeout(function() { + new Sprite({ + from: new Field(x, y, true).add2(Math.random() * Math.PI * 2, Math.random() * 0.5), + img: imgs['fire' + (Math.floor(Math.random() * 4) + 1)], + scaleFunction: function(age) { + return (1 / (age + 0.25) + age - 4) * (-2) + this.r1; + }, + r1: 1 + Math.random() * 2, + zFunction: function(age) { + return -age * 2; + }, + }); + }, i * 317); + + setTimeout(function() { + for (var k = 0; k <= 1; k++) { + new Dust({ from: pos.add3(Math.random() - 0.5, 0), scale: size + Math.random(), ageScale: 2 + Math.random(), height: Math.random() - 0.5 }); + } + }, i * 333); + } + + // create side smoke effects (big dust clouds that go sideways) + for (var i = 0; i < Math.PI * 2; i += Math.random() * 1.5) { + new Dust({ + from: pos, + scale: Math.random() * 7 + 1.5, + ageScale: 2, + vz: 0.01, + xFunction: function(age) { + return ((-1) / (age + 0.3) + 3) * this.x_; + }, + yFunction: function(age) { + return ((-1) / (age + 0.3) + 3) * this.y_; + }, + x_: Math.cos(i), + y_: Math.sin(i), + }); + } + + // create soot + game.groundTilesCanvas.getContext('2d').drawImage(miscSheet[0], imgs.soot.img.x, imgs.soot.img.y, imgs.soot.img.w, imgs.soot.img.h, x * FIELD_SIZE / SCALE_FACTOR - imgs.soot.img.w / 2, (y + 2) * FIELD_SIZE / SCALE_FACTOR - imgs.soot.img.h / 2, imgs.soot.img.w, imgs.soot.img.h); +}; + +function realTimeCompile(htmlElID) { + var jqueryEL = $(`#${htmlElID}`).parent().parent(); + var compile = Command.prototype.compileCondition($(`#${htmlElID}`).val()); + + jqueryEL.prop('title', compile[0] ? 'condition is ok' : compile[1]); + jqueryEL.tooltip({ content: jqueryEL.prop('title') }); + jqueryEL.tooltip('open'); +} + +function getRankCode(text) { + return text.replace(/S/g, ' ') + .replace(/Y/g, ' ') + .replace(/G/g, ' ') + .replace(/R/g, ' '); +} + + +function displayInfoMsg(msg) { + if (typeof msg === 'string') { + msg = escapeHtml(msg); + } + fadeIn($('#infoWindow')); + new HTMLBuilder() + .add('
') + .add(msg) + .add('
') + .insertInto('#infoWindowTextArea'); +}; + +function displayInfoMsgDarkBG(msg, arch_sound) { + fadeIn($('#darkScreenDiv')); + fadeIn($('#infoWindow2')); + if (msg instanceof HTMLBuilder) { + msg.insertInto('#infoWindowTextArea2'); + } else { + $('#infoWindowTextArea2').html(msg); + } + soundManager.playSound(arch_sound ? SOUND.ARCHIVEMENT : SOUND.OPEN_WINDOW, 0.7); +}; + +function displayConfirmPrompt(msg, yesCallback, noCallback) { + // The only way they can exit is by pressing yes or no + $(uimanager.infoWindow.closeButton).hide(); + const restoreCloseButton = () => setTimeout(() => $(uimanager.infoWindow.closeButton).show(), 201); + + const yesButtonID = uniqueID(); + const noButtonID = uniqueID(); + displayInfoMsg(new HTMLBuilder() + .add(`${msg}

`) + .add(` `) + .addHook(() => $(`#${yesButtonID}`).click(() => { + // TODO: jQuery animations completely break if the UI thread gets blocked, so using this instead + // of fadeOut. Figure out how to fix this and add an animation here + $('#infoWindow').hide(); + restoreCloseButton(); + setTimeout(() => yesCallback(), 0); + })) + .addHook(() => $(`#${noButtonID}`).click(() => { + $('#infoWindow').hide(); + restoreCloseButton(); + setTimeout(() => noCallback(), 0); + }))); +} + +function sendFriendRequest() { + network.send('add-friend<<$' + $('#newFriendInput')[0].value); +}; + +function showAllDivisions() { + const builder = new HTMLBuilder(); + + let x = 10; + let y = 10; + + for (let i = 0; i < leagueNames.length; i++) { + builder.add(`
`); + builder.add(getLeagueLink(i, true, 3)); + builder.add('
'); + + y += 100; + if (i == 3) { + y = 10; + x += 270; + } + } + + builder.insertInto('#addScrollableSubDivTextArea'); + + uimanager.playerInfoWindow.setTitleText('Divisions'); + uimanager.playerInfoWindow.setRider(new HTMLBuilder()); + uimanager.playerInfoWindow.setHeadRider(new HTMLBuilder()); + fadeIn($('#playerInfoWindow')); +}; + +function clearCache(extended) { + if (!extended && network_game) { + network.send('cache-clear'); + } else if (extended && network_game) { + network.send('cache-clear-extended'); + } +}; + + +var visionOffsets = []; +var countCircles = 6; + +for (var i = 0; i <= countCircles; i++) { + visionOffsets[i] = []; +} + +for (var x = -7; x <= countCircles + 1; x++) { + for (var y = -7; y <= countCircles + 1; y++) { + var dist = Math.sqrt(x * x + y * y); + + for (var i = 0; i <= countCircles; i++) { + if (dist <= i && dist >= i - 1) { + visionOffsets[i].push([x, y]); + } + } + } +} + +function getPlayerCountFromMap(map) { + var players = []; + var units = map.units.concat(map.buildings); + for (var k = 0; k < units.length; k++) { + if (units[k].owner != 0 && !players.contains(units[k].owner)) { + players.push(units[k].owner); + } + } + + return players.length; +}; + +function getImageFromMap(map) { + var imgSize = 265; + + var newCanvas = document.createElement('canvas'); + newCanvas.width = imgSize; + newCanvas.height = imgSize; + var ctx = newCanvas.getContext('2d'); + + // set scales + var scale_x = imgSize / map.x; + var scale_y = imgSize / map.y; + + // fill map with default tiles background + ctx.fillStyle = map.defaultTiles[0].toUnitType().minimapColor; + ctx.fillRect(0, 0, imgSize, imgSize); + + // draw tiles + for (var k = 0; k < map.tiles.length; k++) { + var type = map.tiles[k].type.toUnitType(); + if (type.blocking) { + ctx.fillStyle = type.minimapColor; + ctx.fillRect((map.tiles[k].x - 1) * scale_x, (map.tiles[k].y - 1) * scale_y, type.sizeX * scale_x, type.sizeY * scale_y); + } + } + + // draw buildings + for (var k = 0; k < map.buildings.length; k++) { + var type = map.buildings[k].type.toUnitType(); + var size = type ? type.size : 3; + + if (map.buildings[k].owner == 0 && map.buildings[k].type.alwaysNeutral) { + ctx.fillStyle = 'yellow'; + } else if (map.buildings[k].owner == 0) { + ctx.fillStyle = '#4DA6AE'; + } else { + var colorArray = playerColors[map.buildings[k].owner - 1][4]; + ctx.fillStyle = 'rgba(' + colorArray[0] + ', ' + colorArray[1] + ', ' + colorArray[2] + ', 1)'; + } + + ctx.fillRect((map.buildings[k].x - 1) * scale_x, (map.buildings[k].y - 1) * scale_y, size * scale_x, size * scale_y); + } + + return newCanvas.toDataURL('image/png'); +}; + +var interpreter = [ + + { + funcName: 'getField', + func: function(s) { + var type = s.replace(/ /g, '').split('.'); + + if (lists.types[type[0]]) { + if (lists.types[type[0]][type[1]] && typeof lists.types[type[0]][type[1]] !== 'number') { + return lists.types[type[0]][type[1]]; + } + + if (lists.types[type[0]].isUpgrade) { + return lists.types[type[0]].getValue(type[1], PLAYING_PLAYER, true) * ((list_upgrade_fields[type[1]] && list_upgrade_fields[type[1]].displayScale) ? list_upgrade_fields[type[1]].displayScale : 1); + } else if (lists.types[type[0]].isUnit) { + return lists.types[type[0]].getValue(type[1], PLAYING_PLAYER) * ((list_unit_fields[type[1]] && list_unit_fields[type[1]].displayScale) ? list_unit_fields[type[1]].displayScale : 1); + } else if (lists.types[type[0]].isBuilding) { + return lists.types[type[0]].getValue(type[1], PLAYING_PLAYER) * ((list_building_fields[type[1]] && list_building_fields[type[1]].displayScale) ? list_building_fields[type[1]].displayScale : 1); + } else if (lists.types[type[0]].isModifier) { + return lists.types[type[0]].getValue(type[1], PLAYING_PLAYER) * ((list_modifiers_fields[type[1]] && list_modifiers_fields[type[1]].displayScale) ? list_modifiers_fields[type[1]].displayScale : 1); + } else // ability + { + return lists.types[type[0]].getValue([type[1]], game.selectedUnits[0]) * ((list_ability_fields[type[1]] && list_ability_fields[type[1]].displayScale) ? list_ability_fields[type[1]].displayScale : 1); + } + } + + return ''; + }, + }, + + { + funcName: 'getUpgradeLevel', + func: function(s) { + var type = lists.types[s.replace(/ /g, '')]; + return type ? PLAYING_PLAYER.getUpgradeLevel(type) : 0; + }, + }, + + { + funcName: 'upgradeCountInResearch', + func: function(s) { + var type = lists.types[s.replace(/ /g, '')]; + return type ? PLAYING_PLAYER.upgradeCountInResearch(type) : 0; + }, + }, + + { + funcName: 'add', + func: function(s) { + var numbers = s.replace(/ /g, '').split(','); + var number = 0; + for (var i = 0; i < numbers.length; i++) { + number += parseInt(numbers[i]); + } + return number; + }, + }, + + { + funcName: 'mul', + func: function(s) { + var numbers = s.replace(/ /g, '').split(','); + var number = 1; + for (var i = 0; i < numbers.length; i++) { + number *= parseInt(numbers[i]); + } + return number; + }, + }, + + { + funcName: 'sub', + func: function(s) { + var numbers = s.replace(/ /g, '').split(','); + var number = parseInt(numbers[0]); + for (var i = 1; i < numbers.length; i++) { + number -= parseInt(numbers[i]); + } + return number; + }, + }, + + { + funcName: 'div', + func: function(s) { + var numbers = s.replace(/ /g, '').split(','); + var number = parseInt(numbers[0]); + for (var i = 1; i < numbers.length; i++) { + number /= parseInt(numbers[i]) ? parseInt(numbers[i]) : 1; + } + return number; + }, + }, + +]; + +function interpreteString(s, unit) { + if (typeof s !== 'string') { + return ''; + } + + for (var i = 0; i < interpreter.length; i++) { + var data = interpreter[i]; + + var s1 = s.indexOf(data.funcName + '('); + var s2 = s.indexOf(')', s1); + + while (s1 >= 0 && s2 >= 0 && s2 > s1) { + var s3 = s.split(data.funcName + '('); + s3[1] = s3.slice(1).join(data.funcName + '('); + var s4 = s3[1].split(')'); + s4[1] = s4.slice(1).join(')'); + + s = s3[0] + data.func(s4[0], unit) + s4[1]; + + s1 = s.indexOf(data.funcName + '('); + s2 = s.indexOf(')', s1); + } + } + + return s; +}; + +// draw wrapped text (returns the number of drawed lines if wrapped) +// if overflow is set to "ellipses" instead of "wrap", it will terminate with +// ... +function drawText(ctx, text, color, size, x, y, w, align, alpha, fillStyle, shadowStyle, height, overflow = 'wrap') { + const ellipses = overflow == 'ellipses' ? '...' : ''; + + var text2 = text; + var w2 = w ? w : 99999; + alpha = alpha ? alpha : 1; + var returnValue = 1; // number of lines we used to draw the text + + ctx.font = size; + ctx.textAlign = align ? align : 'left'; + + // check if text fits in line, if not, recursively call for next line + if (ctx.measureText(text).width > w2) { + var words = text.split(' '); + + var line = words[0]; + var lastFittingLine; + var i = 1; + + while (ctx.measureText(line + ellipses).width <= w2 && i < words.length) { + lastFittingLine = line; + line = line + ' ' + words[i]; + i++; + } + + text2 = lastFittingLine ? lastFittingLine : text; + if (text2 != text) { + text2 += ellipses; + } + + words.splice(0, Math.max(i - 1, 1)); + + if (words.length > 0 && overflow == 'wrap') { + returnValue += drawText(ctx, words.join(' '), color, size, x, y + height + 4, w2, align, alpha, fillStyle, shadowStyle, height); + } + } + + var textWidth = ctx.measureText(text2).width; + + // round + var x2 = Math.floor(x); + var y2 = Math.floor(y); + + // fillrect, if fillstyle parameter passed + if (fillStyle) { + ctx.globalAlpha = alpha; + ctx.fillStyle = fillStyle; + ctx.fillRect(x2 - (align == 'center' ? (textWidth + 6) / 2 : 0), y2 - height * 0.9, textWidth + 6, height * 1.1); + } + + // nu shadow + // ctx.shadowColor = shadowStyle ? shadowStyle : "black"; + // ctx.shadowBlur = 3; + + // draw text + ctx.fillStyle = color; + ctx.fillText(text2, x2, y2); + ctx.globalAlpha = 1; + + // ctx.shadowBlur = 0; + + return returnValue; +}; + +// when window gets resized, this is calld +function resize() { + WIDTH = window.innerWidth; + HEIGHT = window.innerHeight; + canvas.width = WIDTH; + canvas.height = HEIGHT; + + // Settings + c.mozImageSmoothingEnabled = false; + c.imageSmoothingEnabled = false; +}; + +function getFriendlyDate(unixTime) { + if (unixTime == 0) { + return 'before records began'; + } + var d = new Date(unixTime * 1000); + var months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + var year = d.getFullYear(); + var month = months[d.getMonth()]; + var date = d.getDate(); + return month + ' ' + date + ', ' + year; +} + +async function httpGet(url) { + return new Promise((resolve, reject) => { + $.get(url) + .done(resolve) + .fail((jqXHR, textStatus, errorThrown) => reject(JSON.stringify(jqXHR))); + }); +} + +// parent class for map object types (unit types, building typed, tile / doodas types) +function MapObjectType() { + this.projectileLen = 0.2; + this.projectileStartHeight = 0; + this.projectileSpeed = 8; + this.imageScale = 1; + this.canAttackGround = true; + this.isHeatSeeking = true; + this.aoeRadius = 0; + this.isInvisible = false; + this.noShow = false; + this.controllable = true; + this.hasDetection = false; +}; + +MapObjectType.prototype.replaceReferences = function() { + var t = this; + if (this.commands) { + _.each(this.commands, function(c, key) { + if (typeof c == 'string') { + t.commands[key] = lists.types[c]; + } + }); + } + + if (this.img && (typeof this.img == 'string')) { + this.img = lists.imgs[this.img] ? lists.imgs[this.img] : (this.isBuilding ? lists.imgs.castle : lists.imgs.soldier); + } + + if (this.modifiers) { + for (var k = 0; k < this.modifiers.length; k++) { + if (typeof this.modifiers[k] == 'string') { + this.modifiers[k] = lists.types[this.modifiers[k]]; + } + } + } + + if (this.modifiersSelf) { + for (var k = 0; k < this.modifiersSelf.length; k++) { + if (typeof this.modifiersSelf[k] == 'string') { + this.modifiersSelf[k] = lists.types[this.modifiersSelf[k]]; + } + } + } + + if (this.spawnModifiers) { + for (var k = 0; k < this.spawnModifiers.length; k++) { + if (typeof this.spawnModifiers[k] == 'string') { + this.spawnModifiers[k] = lists.types[this.spawnModifiers[k]]; + } + } + } + + if (this.onDamageModifiers) { + for (var k = 0; k < this.onDamageModifiers.length; k++) { + if (typeof this.onDamageModifiers[k] == 'string') { + this.onDamageModifiers[k] = lists.types[this.onDamageModifiers[k]]; + } + } + } + + if (this.modifiersPerLevel) { + for (var k = 0; k < this.modifiersPerLevel.length; k++) { + if (typeof this.modifiersPerLevel[k] == 'string') { + this.modifiersPerLevel[k] = lists.types[this.modifiersPerLevel[k]]; + } + } + } + + if (this.costIncreaseGroup) { + for (var k = 0; k < this.costIncreaseGroup.length; k++) { + if (typeof this.costIncreaseGroup[k] == 'string') { + this.costIncreaseGroup[k] = lists.types[this.costIncreaseGroup[k]]; + } + } + } +}; + +MapObjectType.prototype.getValue = function(value, owner) { + return this[value] + owner.getValueModifier(value, this); +}; + +MapObjectType.prototype.toString = function() { + return this.name; +}; + +// where would this be placed regarding the current mouse cursor pos +MapObjectType.prototype.getFieldFromMousePos = function() { + return game.getFieldFromPos(null, null, !this.isCliff).add3(-this.sizeX / 2 + 0.5, -this.sizeY / 2 + 0.5); +}; + +MapObjectType.prototype.getDataFields = function() { + return this.isUnit ? list_unit_fields : list_building_fields; +}; + +MapObjectType.prototype.draw = function(x, y) { + var img = this.img.img; + var scale = this.getValue('imageScale', PLAYING_PLAYER) * SCALE_FACTOR; + + var x_ = 0; + var y_ = 0; + + // decorations coords are exact pixels + if (this.ignoreGrid) { + x_ = Math.floor((x - (img.w / 2) * SCALE_FACTOR) / SCALE_FACTOR) * SCALE_FACTOR - game.cameraX; + y_ = Math.floor(((y + 3 * SCALE_FACTOR) - (img.h / 2) * SCALE_FACTOR) / SCALE_FACTOR) * SCALE_FACTOR - game.cameraY; + } else { + x_ = (x - 1 + this.sizeX / 2) * FIELD_SIZE - game.cameraX - img.w / 2 * scale; + y_ = (y - 1 + this.sizeY) * FIELD_SIZE - game.cameraY - img.h * scale; + } + + var nr = Math.max(PLAYING_PLAYER.number, 1); + + c.drawImage(this.img.file[this.img.file[nr] ? nr : 0], img.x, img.y, img.w, img.h, x_, y_, img.w * scale, img.h * scale); +}; + +MapObjectType.prototype.couldBePlacedAt = function(field, dontIgnoreHiddenBlocks) { + var checkObj = dontIgnoreHiddenBlocks ? PLAYING_PLAYER.team : game; + var distanceAllowed = true; + + for (var x = field.x; x < field.x + this.sizeX; x++) { + for (var y = field.y; y < field.y + this.sizeY; y++) { + // get the nearest CC, Goldmine and Start Location, because those are not allowed to be placed near eachother + var f = new Field(x, y); + var nextGoldmine = game.getNextBuildingOfType(f, null, false, 'startGold'); + var nextCC = game.getNextBuildingOfType(f, null, false, 'takesGold'); + + if (this.takesGold && nextGoldmine && nextGoldmine.pos.distanceTo2(f) < game.getMineDistance()) { + distanceAllowed = false; + } + + if (this.startGold && nextCC && nextCC.pos.distanceTo2(field) < game.getMineDistance()) { + distanceAllowed = false; + } + + if (checkObj.fieldIsBlockedForBuilding(x, y) || !distanceAllowed) { + return false; + } + } + } + + return true; +}; + +MapObjectType.prototype.getTitleImage = function() { + var img = (game_state == GAME.EDITOR && this.img.imgEditor) ? this.img.imgEditor : this.img.img; + + return { file: this.img.file[0], x: img.x, y: img.y, w: img.w, h: img.h }; +}; + +UnitType.prototype = new MapObjectType(); +function UnitType(data) { + _.extend(this, data); + + // copy arrays (so they dont get only referenced) + var thisRef = this; + _.each(data, function(val, key) { + thisRef[key] = Object.prototype.toString.call(thisRef[key]) === '[object Array]' ? thisRef[key].slice() : thisRef[key]; + }); + + // copy command object + this.commands = copyObject(this.commands); + + this.minimapColor = '#ffffff'; + + this.isUnit = true; + + this.dustCreationChance = this.dustCreationChance ? this.dustCreationChance : 0.05; // if not set, use default which is 0,05 + + this.minRange = this.minRange ? this.minRange : -999; // if no min range is set, set it to -999 (otherwise some calculations would fail) + + this.id = game.unitTypeIdCounter++; + + this.animSpeed = this.animSpeed ? this.animSpeed : 1.5; + + this.oscillationAmplitude = this.oscillationAmplitude ? this.oscillationAmplitude : 0; + + this.yesSound = this.yesSound ? this.yesSound : SOUND.YES; + this.yesSoundVolume = this.yesSoundVolume ? this.yesSoundVolume : 0.6; + this.readySound = this.readySound ? this.readySound : SOUND.READY; + this.readySoundVolume = this.readySoundVolume ? this.readySoundVolume : 0.9; + + this.selectionOffsetY = this.selectionOffsetY ? this.selectionOffsetY : 0; + this.height = this.height ? this.height : 0.3; + this.deathAnimationSpeed = data.deathAnimationSpeed ? data.deathAnimationSpeed : 0.6; +}; + +UnitType.prototype.addToLists = function() { + lists.types[this.id_string] = this; + lists.unitTypes[this.id_string] = this; +}; + +// get the title img of this unit type; if nr is passed, return img of color of player nr, else return of player PLAYING_PLAYER +UnitType.prototype.getTitleImage = function(player) { + try { + player = player ? player : game.players[1]; + + var img = (player && player.skins && player.skins[this.id_string]) ? player.skins[this.id_string] : this.img; + + return { file: img.file[player ? player.number : 1], x: img.idle.x, y: img.idle.y, w: img.idle.frameWidth, h: img.idle.h / img._angles }; + } catch (err) { + console.error(`Failed to find titleImage for ${this.name}. Img "${this.img}".`); + } +}; + +// checks if the unit could stand at a position (without beeing blocked, only checks for buildings and tiles, not units) +UnitType.prototype.couldStandAt = function(pos) { + if (this.flying) { + for (i = 0; i < this.circleOffsets.length; i++) { + if (game.fieldIsBlockedFlying(Math.ceil(pos.px + this.circleOffsets[i][0]), Math.ceil(pos.py + this.circleOffsets[i][1]))) { + return false; + } + } + } else { + for (i = 0; i < this.circleOffsets.length; i++) { + if (game.fieldIsBlocked(Math.ceil(pos.px + this.circleOffsets[i][0]), Math.ceil(pos.py + this.circleOffsets[i][1]))) { + return false; + } + } + } + + return true; +}; + +// find the next free position where a unit of this type could stand (is pos is valid itself, return pos; only check for buildings and tiles, no units) +UnitType.prototype.getNextFreePositionFrom = function(pos) { + var testPos = pos; + var len = 0.02; + var i = 0; + + while (!this.couldStandAt(testPos)) { + testPos = pos.add(new Field(angleOffsets[i][0], angleOffsets[i][1], true).normalize(len)); + len += 0.02; + i = (i + 1) % 12; + } + + return testPos; +}; + +UnitType.prototype.getBasicType = function() { + for (var i = 0; i < basicUnitTypes.length; i++) { + if (basicUnitTypes[i].id_string == this.id_string) { + return basicUnitTypes[i]; + } + } + + return false; +}; + +// not sure if this gets ever called (it does when placing in editor) +UnitType.prototype.draw = function(px, py) { + var img = this.getTitleImage(); + var scale = this.getValue('imageScale', PLAYING_PLAYER) * SCALE_FACTOR; + c.drawImage(img.file, img.x, img.y, img.w, img.h, px - img.w / 2 * scale - game.cameraX, py - img.h / 2 * scale - game.cameraY, img.w * scale, img.h * scale); +}; + +BuildingType.prototype = new MapObjectType(); +function BuildingType(data) { + this.preventsReveal = true; + this.preventsLoss = true; + + this.deathSound = SOUND.BUILDING_DEATH; + + _.extend(this, data); + + // copy arrays (so they dont get only referenced) + var thisRef = this; + _.each(data, function(val, key) { + thisRef[key] = Object.prototype.toString.call(thisRef[key]) === '[object Array]' ? thisRef[key].slice() : thisRef[key]; + }); + + // copy command object + this.commands = copyObject(this.commands); + + this.minRange = this.minRange ? this.minRange : -999; // if no min range is set, set it to -999 (otherwise some calculations would fail) + + this.isBuilding = true; + + this.minimapColor = 'white'; + + this.id = game.buildingTypeIdCounter++; + + this.height = this.height ? this.height : 1; + + this.selectionOffsetY = this.selectionOffsetY ? this.selectionOffsetY : 0; +}; + +BuildingType.prototype.addToLists = function() { + lists.types[this.id_string] = this; + // lists.imgs[this.id_string] = this.img; + lists.unitTypes[this.id_string] = this; + lists.buildingTypes[this.id_string] = this; + lists.buildingsUpgrades[this.id_string] = this; +}; + +BuildingType.prototype.getValue = function(value, owner) { + if (value == 'cost') { + return owner.getCostOfNextInstanceForBuilding(this); + } + + return this[value] + owner.getValueModifier(value, this); +}; + +BuildingType.prototype.getBasicType = function() { + for (var i = 0; i < basicBuildingTypes.length; i++) { + if (basicBuildingTypes[i].id_string == this.id_string) { + return basicBuildingTypes[i]; + } + } + + return false; +}; + +BuildingType.prototype.getTitleImage = function(player) { + player = player ? player : game.players[1]; + + var img = (player && player.skins && player.skins[this.id_string]) ? player.skins[this.id_string] : this.img; + + return { file: img.file[player ? player.number : 1], x: img.img.x, y: img.img.y, w: img.img.frameWidth, h: img.img.h }; +}; + + + +function Command(data) { + this.manaCost = [0]; + this.goldCost = 0; + this.range = [0]; + this.minRange = [-999]; + this.aoeRadius = [0]; + this.damage = [0]; + this.projectileSpeed = [8]; + this.hitsFriendly = true; + this.hitsEnemy = true; + this.hitsSelf = true; + this.effectScale = 1; + this.projectileAoeRadius = [0]; + this.projectileDamage = [0]; + this.modifiers = []; + this.summonedUnits = []; + this.autocastConditions = ''; + this.cooldown = 0; + this.cooldown2 = 0; + this.attackEffectInit = 'spell'; + this.bounceDistMax = 0; + this.bounceDistMin = 0; + + _.extend(this, data); + + // copy arrays (so they dont get only referenced) + var thisRef = this; + _.each(data, function(val, key) { + thisRef[key] = Object.prototype.toString.call(thisRef[key]) === '[object Array]' ? thisRef[key].slice() : thisRef[key]; + }); + + this.isCommand = true; + + this.id = game.global_command_id++; + + if (!IS_LOGIC && this.image) { + this.buttons = [new Button(this)]; + interface_.buttons.push(this.buttons[0]); + + if (this.requiredLevels && this.requiredLevels.length > 0) { + this.buttons.push(new Button(this, true)); + interface_.buttons.push(this.buttons[1]); + } + } +}; + +Command.prototype.updateHotkey = function(hotkey) { + this.hotkey = hotkey; + this.buttons.forEach((b) => b.refresh()); +}; + +Command.prototype.compileCondition0 = function() { + if (this.autocastConditions && this.autocastConditions.length > 0) { + var comp = this.compileCondition(this.autocastConditions); + + if (comp && comp[0]) { + eval('this.autocastCondition = function(u, uthis){return ' + comp[1] + ';};'); + } + } +}; + +Command.prototype.getTitleImage = function(nr) { + return this.image.getTitleImage(nr); +}; + +// returns an 2 len array; 1st element: false on error, true on no error; 2nd element: error msg on error, finished language on no error +Command.prototype.compileCondition = function(str) { + if (!str || str.length == 0) { + return [0, 'no content']; + } + + var lastType = ''; + var word = ''; + var words = []; + + str += ' '; + + for (var i = 0; i < str.length; i++) { + var char_ = str.substr(i, 1); + + var type_ = ''; + + if (char_.match(/[a-zA-Z_]/) == char_) { + type_ = 'char'; + } else if (char_.match(/[0-9]/) == char_) { + type_ = 'numerical'; + } else if (char_.match(/[<>!=]/) == char_) { + type_ = 'compare'; + } else if (char_.match(/[\-+*/]/) == char_) { + type_ = 'arithmetic'; + } else if (char_.match(/[\.]/) == char_) { + type_ = lastType == 'char' ? 'char' : 'numerical'; + } else if (char_.match(/[&|]/) == char_) { + type_ = 'conjunction'; + } else if (char_ == ' ') { + type_ = 'undefined'; + } else { + return [false, 'invalid character: ' + char_]; + } + + if (type_ == lastType || lastType == '') { + word += char_; + } else { + if (lastType != 'undefined') { + if (lastType == 'numerical') { + var word2 = parseFloat(word); + + if (word2.isNaN) { + return [false, word2 + ' is not a number']; + } else { + word = word2; + } + } else if (lastType == 'compare') { + if (word != '<' && word != '>' && word != '<=' && word != '>=' && word != '=' && word != '==' && word != '!=') { + return [false, word + ' is not a valid comparison expression (allowed are: < > <= >= == !=)']; + } + + if (word == '=') { + word = '=='; + } + } else if (lastType == 'arithmetic') { + if (word.length != 1) { + return [false, word + ' is not a valid arithmetic expression (allowed are: + - / *)']; + } + } else if (lastType == 'conjunction') { + if (word != '&&' && word != '||') { + return [false, word + ' is not a valid conjunction (allowed are: && ||)']; + } + } else if (lastType == 'char') { + var countDots = (word.match(/\./g) || []).length > 0; + + if (countDots == 1) { + if (word.substr(0, 5) != 'type.' && word.substr(0, 5) != 'this.' && word.substr(0, 6) != 'owner.') { + return [false, word + ' is not a valid field name']; + } + } else if (countDots > 2) { + return [false, word + ' is not a valid field name']; + } + + if (word != 'true' && word != 'false') { + word = word.substr(0, 5) != 'this.' ? ('u.' + word) : ('u' + word); + } + } + + words.push({ + word: word, + type: (lastType == 'char' || lastType == 'numerical') ? 'expression' : lastType, + }); + } + + word = char_; + } + + lastType = type_; + } + + var stateMachine = { + + start: { + expression: 'exp1', + }, + + exp1: { + arithmetic: 'start', + compare: 'c', + }, + + c: { + expression: 'exp2', + }, + + exp2: { + isFinish: true, + arithmetic: 'c', + conjunction: 'start', + }, + + }; + + var state = stateMachine.start; + var language = ''; + + for (var i = 0; i < words.length; i++) { + language += words[i].word + ' '; + + if (state[words[i].type]) { + state = stateMachine[state[words[i].type]]; + } else { + return [false, 'syntax error; unexpected word: ' + words[i].word]; + } + } + + if (!state.isFinish) { + return [false, 'syntax error; expecting at least one more word']; + } + + return [true, language]; +}; + +Command.prototype.canTargetUnit = function(u) { + for (var k = 0; k < this.targetRequiremementsArray.length; k++) { + var met = false; + + for (var i = 0; i < this.targetRequiremementsArray[k].length; i++) { + if (this.targetRequiremementsArray[k][i].func(u)) { + met = true; + } + } + + if (!met) { + return false; + } + } + + return true; +}; + +Command.prototype.replaceReferences = function() { + if (typeof this.unitType == 'string') { + this.unitType = lists.types[this.unitType]; + } + + if (typeof this.improvedBuilding == 'string') { + this.improvedBuilding = lists.types[this.improvedBuilding]; + } + + if (typeof this.upgrade == 'string') { + this.upgrade = lists.types[this.upgrade]; + } + + if (typeof this.image == 'string') { + this.image = lists.imgs[this.image] ? lists.imgs[this.image] : lists.imgs.attentionmarkYellow; + } + + if (this.modifiers) { + for (var k = 0; k < this.modifiers.length; k++) { + if (typeof this.modifiers[k] == 'string') { + this.modifiers[k] = lists.types[this.modifiers[k]]; + } + } + } + + if (this.modifiersSelf) { + for (var k = 0; k < this.modifiersSelf.length; k++) { + if (typeof this.modifiersSelf[k] == 'string') { + this.modifiersSelf[k] = lists.types[this.modifiersSelf[k]]; + } + } + } + + if (this.summonedUnits) { + for (var k = 0; k < this.summonedUnits.length; k++) { + if (typeof this.summonedUnits[k] == 'string') { + this.summonedUnits[k] = lists.types[this.summonedUnits[k]]; + } + } + } + + if (this.requirementType) { + for (var k = 0; k < this.requirementType.length; k++) { + if (typeof this.requirementType[k] == 'string') { + this.requirementType[k] = lists.types[this.requirementType[k]]; + } + } + } + + this.targetRequiremementsArray = []; + + if (this.targetRequirements1 && this.targetRequirements1.length > 0) { + this.targetRequiremementsArray.push(this.targetRequirements1); + } + + if (this.targetRequirements2 && this.targetRequirements2.length > 0) { + this.targetRequiremementsArray.push(this.targetRequirements2); + } + + if (this.targetRequirements3 && this.targetRequirements3.length > 0) { + this.targetRequiremementsArray.push(this.targetRequirements3); + } + + for (var i = 0; i < this.targetRequiremementsArray.length; i++) { + for (var k = 0; k < this.targetRequiremementsArray[i].length; k++) { + if (typeof this.targetRequiremementsArray[i][k] == 'string') { + this.targetRequiremementsArray[i][k] = targetRequirements[this.targetRequiremementsArray[i][k]]; + } + } + } +}; + +Command.prototype.getValue = function(field, unit) { + if (field == 'summonedUnits' || field == 'modifiers' || field == 'modifiersSelf') { + return this[field][Math.min(this[field].length - 1, unit.abilityLevels[this.id] - 1)]; + } + + return ((unit.abilityLevels && (field == 'manaCost' || field == 'range' || field == 'minRange' || field == 'damage' || field == 'aoeRadius' || field == 'projectileSpeed' || field == 'projectileDamage' || field == 'projectileAoeRadius')) ? + this[field][Math.min(this[field].length - 1, unit.abilityLevels[this.id] - 1)] : this[field]) + (unit.owner ? unit.owner.getValueModifier(field, this) : unit.getValueModifier(field, this)); +}; + +Command.prototype.getDataFields = function() { + return list_ability_fields; +}; + +Command.prototype.addToLists = function() { + lists.types[this.id_string] = this; + lists.commands[this.id_string] = this; +}; + +Command.prototype.getBasicType = function() { + for (var i = 0; i < basicCommands.length; i++) { + if (basicCommands[i].id_string == this.id_string) { + return basicCommands[i]; + } + } + + return false; +}; + +Command.prototype.aoeHitsUnit = function(caster, target) { + if (caster == target) { + return this.hitsSelf; + } + + var isEnemy = caster.owner.isEnemyOfPlayer(target.owner); + + if ((isEnemy && !this.hitsEnemy) || (!isEnemy && !this.hitsFriendly)) { + return false; + } + + if (this.targetFilters) { + for (var i = 0; i < this.targetFilters.length; i++) { + if (!target.type[this.targetFilters[i]]) { + return false; + } + } + } + + if (this.targetFiltersExclude) { + for (var i = 0; i < this.targetFiltersExclude.length; i++) { + if (target.type[this.targetFiltersExclude[i]]) { + return false; + } + } + } + + return true; +}; + +function Upgrade(data) { + _.extend(this, data); + + // copy arrays (so they dont get only referenced) + var thisRef = this; + _.each(data, function(val, key) { + thisRef[key] = Object.prototype.toString.call(thisRef[key]) === '[object Array]' ? thisRef[key].slice() : thisRef[key]; + }); + + this.isUpgrade = true; +}; + +Upgrade.prototype.getValue = function(value, owner, alsoUnderConstruction) { + return (typeof this[value] === 'number') ? (this[value] + owner.getValueModifier(value, this, alsoUnderConstruction)) : this[value]; +}; + +Upgrade.prototype.replaceReferences = function() { + if (this.effectsTypes) { + for (var k = 0; k < this.effectsTypes.length; k++) { + if (typeof this.effectsTypes[k] == 'string') { + this.effectsTypes[k] = lists.types[this.effectsTypes[k]]; + } + } + } + + if (typeof this.image == 'string') { + this.image = lists.imgs[this.image] ? lists.imgs[this.image] : lists.imgs.attentionmarkYellow; + } +}; + +Upgrade.prototype.getDataFields = function() { + return list_upgrade_fields; +}; + +Upgrade.prototype.getTitleImage = function(nr) { + return this.image.getTitleImage(nr); +}; + +Upgrade.prototype.addToLists = function() { + lists.types[this.id_string] = this; + lists.upgrades[this.id_string] = this; + lists.buildingsUpgrades[this.id_string] = this; +}; + +Upgrade.prototype.getBasicType = function() { + for (var i = 0; i < basicUpgrades.length; i++) { + if (basicUpgrades[i].id_string == this.id_string) { + return basicUpgrades[i]; + } + } + + return false; +}; + + +function Modifier(data) { + this.isModifier = true; + + this.maxStack = 1; + this.auraHitsFriendly = true; + this.auraHitsAllied = true; + this.auraHitsEnemy = true; + this.auraHitsSelf = true; + + _.extend(this, data); + + // copy arrays (so they dont get only referenced) + var thisRef = this; + _.each(data, function(val, key) { + thisRef[key] = Object.prototype.toString.call(thisRef[key]) === '[object Array]' ? thisRef[key].slice() : thisRef[key]; + }); +}; + +Modifier.prototype.getDataFields = function() { + return list_modifiers_fields; +}; + +Modifier.prototype.replaceReferences = function() { + if (typeof this.image == 'string') { + this.image = lists.imgs[this.image] ? lists.imgs[this.image] : lists.imgs.attentionmarkYellow; + } + + if (this.unitImg && (typeof this.unitImg == 'string')) { + this.unitImg = lists.imgs[this.unitImg] ? lists.imgs[this.unitImg] : lists.imgs.attentionmarkYellow; + } + + if (this.auraModifiers) { + for (var k = 0; k < this.auraModifiers.length; k++) { + if (typeof this.auraModifiers[k] == 'string') { + this.auraModifiers[k] = lists.types[this.auraModifiers[k]]; + } + } + } + + if (this.killModifiers) { + for (var k = 0; k < this.killModifiers.length; k++) { + if (typeof this.killModifiers[k] == 'string') { + this.killModifiers[k] = lists.types[this.killModifiers[k]]; + } + } + } + + if (this.addCommands) { + for (var k = 0; k < this.addCommands.length; k++) { + if (typeof this.addCommands[k] == 'string') { + this.addCommands[k] = lists.types[this.addCommands[k]]; + } + } + } + + if (this.disabledCommands) { + for (var k = 0; k < this.disabledCommands.length; k++) { + if (typeof this.disabledCommands[k] == 'string') { + this.disabledCommands[k] = lists.types[this.disabledCommands[k]]; + } + } + } +}; + +Modifier.prototype.addToLists = function() { + lists.types[this.id_string] = this; + lists.modifiers[this.id_string] = this; +}; + +Modifier.prototype.getBasicType = function() { + for (var i = 0; i < basicModifiers.length; i++) { + if (basicModifiers[i].id_string == this.id_string) { + return basicModifiers[i]; + } + } + + return false; +}; + +Modifier.prototype.getValue = function(field, owner) { + return this[field] + owner.getValueModifier(field, this); +}; + +Modifier.prototype.getTitleImage = function(nr) { + return this.image.getTitleImage(nr); +}; + +Modifier.prototype.aoeHitsUnit = function(caster, target) { + if (!this.auraHitsSelf && caster == target) { + return false; + } + + var isEnemy = caster.owner.isEnemyOfPlayer(target.owner); + var sameTeam = caster.owner.team == target.owner.team; + + if ((isEnemy && !this.auraHitsEnemy) || (!isEnemy && sameTeam && !this.auraHitsFriendly) || (!isEnemy && !sameTeam && !this.auraHitsAllied)) { + return false; + } + + if (this.auraTargetFilters) { + for (var i = 0; i < this.auraTargetFilters.length; i++) { + if (!target.type[this.auraTargetFilters[i]]) { + return false; + } + } + } + + if (this.auraTargetFiltersExclude) { + for (var i = 0; i < this.auraTargetFiltersExclude.length; i++) { + if (target.type[this.auraTargetFiltersExclude[i]]) { + return false; + } + } + } + + return true; +}; + +function Graphic(data) { + this.isGraphic = true; + this.noLogic = true; + + _.extend(this, data); + + this.dataFiles = []; + + if (!this.id_string) { + this.id_string = this.name; + } + + if (!this.name) { + this.name = this.id_string; + } + + if (!this.img) { + this.img = { x: 0, y: 0, w: 0, h: 0, frameWidth: 0, frames: [] }; + } + + if (!this.idle) { + this.idle = { x: 0, y: 0, w: 0, h: 0, frameWidth: 0, frames: [] }; + } + + if (!this.file) { + this.file = 'buildingSheet'; + } +}; + +Graphic.prototype.getDataURLFile = function(index) { + if (this.dataFiles[index]) { + return this.dataFiles[index]; + } + + if (!this.file[index]) { + return null; + } + + var img = this.getTitleImage(index); + + var canv_ = document.createElement('canvas'); + canv_.width = img.w; + canv_.height = img.h; + canv_.getContext('2d').drawImage(img.file, img.x, img.y, img.w, img.h, 0, 0, img.w, img.h); + + var w = img.w; + var h = img.h; + + if (w > h) { + h = 54 * (h / w); + w = 54; + } else { + w = 54 * (w / h); + h = 54; + } + + var el = document.createElement('img'); + el.src = canv_.toDataURL(); + el.width = w; + el.height = h; + + this.dataFiles[index] = el; + + return this.dataFiles[index]; +}; + +Graphic.prototype.getDataFields = function() { + return list_graphic_fields; +}; + +Graphic.prototype.replaceReferences = function() { + if (this.file && (typeof this.file == 'string')) { + this.file = customImgs[this.file] ? customImgs[this.file] : miscSheet; + } +}; + +Graphic.prototype.addToLists = function() { + lists.imgs[this.id_string] = this; +}; + +Graphic.prototype.getBasicType = function() { + if (unit_imgs[this.id_string]) { + return unit_imgs[this.id_string]; + } + + if (building_imgs[this.id_string]) { + return building_imgs[this.id_string]; + } + + if (imgs[this.id_string]) { + return imgs[this.id_string]; + } + + return false; +}; + +Graphic.prototype.getTitleImage = function(nr) { + if (this._angles) { + return { file: this.file[this.file[nr] ? nr : 0], x: this.idle.x, y: this.idle.y, w: this.idle.frameWidth, h: this.idle.h / this._angles }; + } + + return { file: this.file[this.file[nr] ? nr : 0], x: this.img.x, y: this.img.y, w: this.img.frameWidth ? this.img.frameWidth : this.img.w, h: this.img.h }; +}; + +Tile.prototype = new MapObject(); +function Tile(data) { + _.extend(this, data); + + // random offset for drawing (only on not ground not decoration tiles, do the tiles do appear not so much "in line") + this.randomOffsetX = 0; + this.randomOffsetY = 0; + + if (this.type.ignoreGrid) { + this.pos = new Field(this.x, this.y, true); + } else { + this.pos = new Field(this.x - 1 + this.type.sizeX / 2, this.y - 1 + this.type.sizeY / 2, true); + + // if not ground, add a random offset for drawing, to make it look more naturally + if (this.type.blocking && !this.type.isCliff && !this.type.noRandomOffset) { + this.randomOffsetX = Math.random() * 0.2 - 0.1; + this.randomOffsetY = Math.random() * 0.2 - 0.1; + } + } + + this.drawPos = this.type.isGround ? this.pos : this.pos.add3(0, -game.getHMValue2(Math.floor(this.x), Math.floor(this.y)) * CLIFF_HEIGHT); + this.owner = game.players[0]; // tiles always belong to the neutral player + this.yDrawingOffset = this.type.blocking ? this.drawPos.py + this.randomOffsetY + this.type.sizeY / 2 : this.drawPos.py; + + if (this.type.blocking) { + game.blockingTiles.push(this); + + if (game_state == GAME.EDITOR && this.type.isCliff!=true && data.noHistory != true) { + editor.clipboard.history.addObject(this); + } + + this.switchBlocking(true, data.dontRefreshNBs); + + // switch on for all the teams + for (var i = 0; i < game.teams.length; i++) { + this.switchBlockingForTeam(true, game.teams[i]); + } + } else if (!this.type.isDefault) { + // adding the tile to the history + game.groundTiles2.push(this); + if (game_state == GAME.EDITOR && (this.type.isCliff!=true && data.noHistory != true)) { + editor.clipboard.history.addObject(this); + } + } + // define random offset, if this is an animated tile (img = array), define a random time offset for animation + if (Object.prototype.toString.call(this.type.img) === '[object Array]') { + this.randomOffsetFrame = Math.floor(Math.random() * 1000) % this.type.img.length; + } + + this.modifierMods = {}; +}; + +Tile.prototype.getYDrawingOffset = function() { + return this.yDrawingOffset; +}; + +// draw; expects screen bounds (ingame coords) +/* +Tile.prototype.draw = function(x1, x2, y1, y2) +{ + // if not in screen, return + if(!(this.pos.px + 1 >= x1 && this.pos.py + 1 >= y1 && this.pos.px - 1 <= x2 && this.pos.py <= y2)) + return; + + // check if this.type.img is an array, if yes, its animated and we have to look for the current frame + var img = this.type.img; + if(Object.prototype.toString.call(img) === '[object Array]') + img = this.type.img[(Math.floor(ticksCounter / 6) + this.randomOffsetFrame) % this.type.img.length]; + + var x = (this.pos.px + this.randomOffsetX) * FIELD_SIZE - img.width * SCALE_FACTOR / 2 - game.cameraX; + var y = (this.pos.py + this.randomOffsetY + this.type.sizeY / 2) * FIELD_SIZE - img.height * SCALE_FACTOR - game.cameraY; + + c.drawImage(img, x, y, img.width * SCALE_FACTOR, img.height * SCALE_FACTOR); +}; +*/ + +// represents a field in the grid or an exact position; p_mode = true => exact position, else Field +function Field(x, y, p_mode) { + if (p_mode) { + this.px = x; + this.py = y; + this.x = Math.ceil(x); + this.y = Math.ceil(y); + } else { + this.x = x; + this.y = y; + this.px = x - 0.5; + this.py = y - 0.5; + } + + this.isField = true; +}; + +// get distance to other field, using field values +Field.prototype.distanceTo = function(otherField) { + return otherField ? Math.sqrt(Math.pow(this.x - otherField.x, 2) + Math.pow(this.y - otherField.y, 2)) : 999999; +}; + +// get distance to other field, using exact positions +Field.prototype.distanceTo2 = function(otherField) { + return otherField ? Math.sqrt(Math.pow(this.px - otherField.px, 2) + Math.pow(this.py - otherField.py, 2)) : 999999; +}; + +Field.prototype.isSameGrid = function(otherField) { + return this.x == otherField.x && this.y == otherField.y; +}; + +// get vector from here to other Field +Field.prototype.vectorTo = function(otherField) { + return new Field(otherField.px - this.px, otherField.py - this.py, true); +}; + +// normalize the vector to a set length +Field.prototype.normalize = function(factor) { + var len = Math.sqrt(this.px * this.px + this.py * this.py); + if (len == 0) { + len = 0.001; + } + + this.px *= factor / len; + this.py *= factor / len; + return this; +}; + +Field.prototype.mirror = function(width, height, horizontalInvert, verticalInvert) { + if (horizontalInvert) { + this.px = width - this.px; + this.x = width - this.x; + } + if (verticalInvert) { + this.py = height - this.py; + this.y = height - this.y; + } + return this; +}; + +Field.prototype.getLen = function() { + return Math.sqrt(this.px * this.px + this.py * this.py); +}; + +// adds a vector to this one from x and y values +Field.prototype.add = function(otherField) { + return new Field(this.px + otherField.px, this.py + otherField.py, true); +}; + +Field.prototype.add3 = function(x, y) { + return new Field(this.px + x, this.py + y, true); +}; + +Field.prototype.mul = function(x, y) { + return new Field(this.px * x, this.py * y, true); +}; + +// returns a new field, created from adding a vector from this to otherfield with a fixed length +Field.prototype.addNormalizedVector = function(otherField, len) { + var x = otherField.px - this.px; + var y = otherField.py - this.py; + + var len2 = Math.sqrt(x * x + y * y); + if (len2 == 0) { + len2 = 0.001; + } + + x *= len / len2; + y *= len / len2; + + return new Field(this.px + x, this.py + y, true); +}; + +// adds a vector to this field with a given angle and length and returns the resulting field; a different name might be cool +Field.prototype.add2 = function(angle, len) { + return this.add(new Field(Math.cos(angle), Math.sin(angle), true).normalize(len)); +}; + +// get angle from this point to another point +Field.prototype.getAngleTo = function(otherField) { + var returnValue = Math.atan((otherField.py - this.py) / (otherField.px - this.px)); + returnValue -= otherField.px - this.px < 0 ? Math.PI : 0; + return returnValue; +}; + +Field.prototype.getCopy = function() { + return new Field(this.px, this.py, true); +}; + +// getCopy does not return a correct copy if the Field was initialized with p_mode == False +// TODO: determine if any code actually depends on this inaccuracy. If not, replace getCopy with getExactCopy +Field.prototype.getExactCopy = function() { + const f = new Field(0, 0, true); + f.px = this.px; + f.py = this.py; + f.x = this.x; + f.y = this.y; + return f; +}; + +Field.prototype.equals = function(otherField) { + return this.px == otherField.px && this.py == otherField.py; +}; + +Field.prototype.toString = function() { + return this.px + ':' + this.py; +}; + +// the central class of a game; represents a game and holds arrays of all the objects +function Game() { + this.selectedUnits = []; // The players currently selected units + this.blockArray = []; // false = blocked, true = free + this.timeOfLastSelection = 0; // to check for double click on selection + this.projectiles = []; // arrows, ... + this.fields = []; // one Field() for every grid element is stored here, also containing an array with all its neighbours + this.fields2x2 = []; // Additional grid, we need that for units that are bigger than 1 field + this.buckets2x2 = []; + + this.units = []; // all units + this.units4 = []; // dead heroes; they must be stored in case they get ressurected + this.buildings = []; // all in game existing buildings + this.buildings2 = []; // all existing buildings plus some already dead buildings (if seen by some play who doesnt know the building is dead it still has to be drawn for him) + this.groundTiles2 = []; // ground tiles, that are not default (might be added or removed in editor) + this.blockingTiles = []; // trees, rocks and so on, that block pathing and are bound to the grid + this.objectsToDraw = []; + this.unitList = {}; + + // camera position (top left corner of the screen, in pixels) + this.followVision = false; + this.cameraX = 0; + this.cameraY = 0; + + this.global_id = 1; + this.global_command_id = 0; + this.buildingTypeIdCounter = 0; + this.unitTypeIdCounter = 0; + + this.minimap = null; + this.env = new Enviroment(); + this.rain = new Rain(); + + this.lastYesSound = -999; // tick of last time unit said "yes" sound (reaction to order) + this.lastReadySound = -999; // tick of last time unit said "ready" sound (reaction to order) + this.lastMuteToggle = -999; // tick of last time unit said "ready" sound (reaction to order) + + // create additional canvas for groundtiles + this.groundTilesCanvas = document.createElement('canvas'); + + // create additional canvas for default tiles + this.defaultTilesCanvas = document.createElement('canvas'); + + // canvasses for tiles, we put some tiles together in canvasses, so in game we have less draw calls + this.tilesCashes = []; + + this.gameHasEnded = false; + this.playingPlayerWon = false; + + // set replay mode to false (might have been true if we watched a replay before) + this.replay_mode = false; + this.chat_muted = false; + + this.playingFromEditor = false; + this.chatLog = {}; + this.chat = {}; + + this.globalVars = null; + + this.visionSetting = 0; + + this.specFieldNames = [ + '', + 'units', + 'buildings', + 'upgrades', + 'production', + '', + '', + 'lostUnitTypes', + ]; +}; + +Game.prototype.end = function() { + Hotkeys.removeHotkeysChangedListener('GAME_SPEC_INTERFACE'); + keyManager.removeListener('GAME_SPEC_INFO'); + keyManager.removeListener('GAME_VISION'); + + // TODO: set the scale factor to a valid value in LCG instead of fixing this here + if (this.isLCG) { + setScaleFactor(3); + } +}; + +// load a map +/* + * @param data map file data + * @param playerSetting array of json datas for each player + * @param aiRandomizer random number that comes from the server and determines the style / strategy, that ai players play, (has to be the same for all clients) + * @param ticksCounter in case of replay this is the amount of total ticks, so the game knows when the replay is finished and it has to stop + */ +Game.prototype.loadMap = function(data, playerSettings, aiRandomizer, replayTicksCounter, isEditor, isLCG, chat, rainTime) { + if (!playerSettings) { + playerSettings = []; + + let insertedSelf = false; + + if (isEditor) { + playerSettings.push({ name: networkPlayerName, controller: CONTROLLER.HUMAN, team: 0, isPlayingPlayer: true }); + insertedSelf = true; + } + + for (let i = 0; i < MAX_PLAYERS; i++) { + const team = parseInt(data.players?.[i].team.split(' ')[1]) || i + 1; + const slot = data.players?.[i].slot ?? 'open'; + const isNormalAI = (data.players?.[i].ai ?? 'normal AI') == 'normal AI'; + const common = { nr: i + 1, team: team }; + + if (!insertedSelf && slot == 'open') { + playerSettings.push({ ...common, name: networkPlayerName, controller: CONTROLLER.HUMAN, isPlayingPlayer: true }); + insertedSelf = true; + } else if ((slot == 'open' || slot == 'computer') && isNormalAI) { + playerSettings.push({ ...common, name: 'Computer', controller: CONTROLLER.COMPUTER }); + } + } + + if (!insertedSelf) { + playerSettings.push({ nr: 7, team: 0, name: networkPlayerName, controller: CONTROLLER.SPECTATOR, isPlayingPlayer: true }); + } + } + + this.x = parseInt(data.x); + this.y = parseInt(data.y); + this.name = data.name; + this.description = data.description; + this.data = data; + this.aiRandomizer = aiRandomizer ? aiRandomizer : Math.ceil(Math.random() * 100000); // this is used to determine, which AI-type the cpu players pick + this.replayTicksCounter = replayTicksCounter ? replayTicksCounter : -1; + this.isEditor = !!isEditor; + this.isLCG = !!isLCG; + + // create Minimap + this.minimap = new Minimap(this, 0, -MINIMAP_HEIGHT); + + initCustomImgsObj(); + + // custom graphics + if (data.graphics) { + for (key2_ in data.graphics) { + var newImg = new Image(); + customImgs[key2_] = []; + for (var i = 0; i < MAX_PLAYERS + 2; i++) { + customImgs[key2_][i] = newImg; + } + + newImg.onload = function() { + for (key3_ in customImgs) { + if (customImgs[key3_] && customImgs[key3_][0] == customImgs[key3_][1]) { + var newImgs = ImageTransformer.replaceColors(customImgs[key3_][0], searchColors, playerColors); + for (var i = 0; i < newImgs.length; i++) { + customImgs[key3_][i + 1] = newImgs[i]; + } + customImgs[key3_][MAX_PLAYERS + 1] = ImageTransformer.getGreyScaledImage(customImgs[key3_][0]); + } + } + }; + + newImg.src = data.graphics[key2_]; + } + } + + this.updateGlobalVars(data.globalVars); + + // create teams + this.teams = []; + for (var i = 0; i < MAX_PLAYERS + 1; i++) { + this.teams.push(new Team(i)); + } + + // create players, player 0 is always the neutral player. He owns gold mines for example + this.players = [new Player('Neutral', CONTROLLER.NONE, 0, 0)]; + + // create the active players from playerSettings + var specNr = MAX_PLAYERS + 1; + for (var i = 0; i < playerSettings.length; i++) { + var ps = playerSettings[i]; + + var nr = ps.nr ? ps.nr : i + 1; + if (ps.controller == CONTROLLER.SPECTATOR) { + nr = specNr; + specNr++; + } + + var p = new Player( + ps.name, + ps.controller, + nr, + ps.controller == CONTROLLER.SPECTATOR ? 0 : ps.team, + ps.clan, + ps.ai_name, + ps.skins, + ps.dances, + ); + + if (playerSettings[i].isPlayingPlayer) { + PLAYING_PLAYER = p; + } + + // set controller == remote, if human but not playing player + if (p.controller == CONTROLLER.HUMAN && PLAYING_PLAYER != p) { + p.controller == CONTROLLER.REMOTE; + } + + this.players[nr] = p; + } + + // reset ticksCounter (= Game timer) + ticksCounter = 0; + + // reset TICK_TIME + TICK_TIME = 50; + replaySpeedIndex = 1; + + // reset storage for commands that will be sent and recieved + incomingOrders = {}; + outgoingOrders = []; + playerLefts = {}; + incomingCameraUpdates = {}; + outgoingCameraUpdate = {}; + + // Set default delay + TICKS_DELAY = network_game ? 6 : 2; + + // Reset the keyManager to clear control groups, camera hotkeys, and spectator hotkeys + keyManager.reset(); + + // fill block and Fields Arrays + for (var x = 0; x <= this.x + 1; x++) { + this.fields[x] = []; + this.fields2x2[x] = []; + this.blockArray[x] = []; + for (var y = 0; y <= this.y + 1; y++) { + this.fields[x][y] = new Field(x, y); + this.fields2x2[x][y] = new Field(x, y, true); + this.blockArray[x][y] = true; + if (x < 1 || x > this.x || y < 1 || y > this.y) // if outside borders + { + this.blockArray[x][y] = false; + } + } + } + + // create types + interface_.buttons = []; + + // Clear lists of any old data + _.each(lists, function(listElement, listName) { + _.each(listElement, function(type, key) { + if (key != 'none') { + delete listElement[key]; + } + }); + }); + + // Load all the default graphics and add to lists + this.graphics = []; + + for (key in unit_imgs) { + unit_imgs[key].id_string = key; + this.graphics.push(new Graphic(unit_imgs[key])); + } + + for (key in building_imgs) { + building_imgs[key].id_string = key; + building_imgs[key].file = building_imgs[key].file ? building_imgs[key].file : buildingSheet; + this.graphics.push(new Graphic(building_imgs[key])); + } + + for (key in imgs) { + this.graphics.push(new Graphic(imgs[key])); + } + + this.graphics.forEach((g) => g.addToLists()); + + // Reset all other types and then populate them differently depending on whether or not the map is a mod + this.unitTypes = []; + this.buildingTypes = []; + this.upgrades = []; + this.commands = []; + this.modifiers = []; + + // Populate with default unit data if the map isn't frozen + if (!this.globalVars.isFrozen) { + for (var i = 0; i < basicUnitTypes.length; i++) { + this.unitTypes.push(new UnitType(basicUnitTypes[i])); + } + + for (var i = 0; i < basicBuildingTypes.length; i++) { + this.buildingTypes.push(new BuildingType(basicBuildingTypes[i])); + } + + for (var i = 0; i < basicUpgrades.length; i++) { + this.upgrades.push(new Upgrade(basicUpgrades[i])); + } + + for (var i = 0; i < basicCommands.length; i++) { + this.commands.push(new Command(basicCommands[i])); + } + + for (var i = 0; i < basicModifiers.length; i++) { + this.modifiers.push(new Modifier(basicModifiers[i])); + } + + this.buildingTypes.concat(this.unitTypes, this.commands, this.upgrades, this.modifiers).forEach((t) => t.addToLists()); + } + + // Load custom unit data and graphics if this is a mod (frozen implies modded, so that case should already be included) + if (this.globalVars.isModded) { + var gameRef = this; + + // Load custom unit data + _.each(data.unitData, function(type, typeName) { + // AMove is handled as a special case ability + if (typeName == 'amove') { + return; + } + + if (!lists.types[typeName]) { + let o = null; + if (type.isUnit) { + o = new UnitType(type); + gameRef.unitTypes.push(o); + } else if (type.isBuilding) { + o = new BuildingType(type); + gameRef.buildingTypes.push(o); + } else if (type.isCommand) { + o = new Command(type); + gameRef.commands.push(o); + } else if (type.isUpgrade) { + o = new Upgrade(type); + gameRef.upgrades.push(o); + } else if (type.isModifier) { + o = new Modifier(type); + gameRef.modifiers.push(o); + } else { + assert(false); + } + + if (o) { + o.id_string = typeName; + o.addToLists(); + } else { + throw Error(`Failed to obtain object for typeName ${typeName}\n${JSON.stringify(type)}`); + } + } + + const dataFields = lists.types[typeName].getDataFields(); + + _.each(type, function(val, field) { + if (dataFields[field]) { + if (dataFields[field].isObject || dataFields[field].type == 'complex') { + var obj = {}; + + _.each(val, function(el, key) { + obj[key] = el; + }); + + lists.types[typeName][field] = obj; + } else if (dataFields[field].isArray) { + var arr = []; + + if (Object.prototype.toString.call(val) === '[object Array]') { + for (var i = 0; i < val.length; i++) { + arr.push(checkField(dataFields[field], val[i])); + } + } else { + arr.push(checkField(dataFields[field], val)); + } + + lists.types[typeName][field] = arr; + } else { + lists.types[typeName][field] = checkField(dataFields[field], val); + + if (dataFields[field].type == 'selection' && dataFields[field].all_values && !_.contains(dataFields[field].all_values, lists.types[typeName][field])) { + lists.types[typeName][field] = dataFields[field].default_; + } + } + } + }); + }); + + // Load specifically AMove as a special case + const amove = new Command(basicCommands.find((c) => c.id_string == 'amove')); + amove.addToLists(); + this.commands.push(amove); + + // Load custom graphics data + _.each(data.graphicObjects, function(type, typeName) { + if (!lists.imgs[typeName]) { + var o = new Graphic(type); + o.id_string = typeName; + gameRef.graphics.push(o); + o.addToLists(); + } + + var dataFields = lists.imgs[typeName].getDataFields(); + + _.each(type, function(val, field) { + if (dataFields[field]) { + if (dataFields[field].type == 'complex') { + var obj = {}; + + _.each(val, function(el, key) { + obj[key] = el; + }); + + lists.imgs[typeName][field] = obj; + } else { + lists.imgs[typeName][field] = checkField(dataFields[field], val); + + if (dataFields[field].type == 'selection' && dataFields[field].all_values && !_.contains(dataFields[field].all_values, lists.types[typeName][field])) { + lists.imgs[typeName][field] = dataFields[field].default_; + } + } + } + }); + }); + } + + // refresh all buttons + for (var k = 0; k < interface_.buttons.length; k++) { + interface_.buttons[k].init(interface_.buttons[k].command, interface_.buttons[k].learn); + } + + calculateTypesTickValues(); + + // replacing references + _.each(this.commands.concat(this.upgrades, this.buildingTypes, this.unitTypes, this.modifiers, this.graphics, tileTypes, cliffs, cliffs_winter, egypt_cliffs, grave_cliffs, ramp_tiles, ramp_tiles_egypt, ramp_tiles_grave), function(t) { + t.replaceReferences(); + }); + + mapEditorData = new MapEditorData(); + + if (game_state == GAME.EDITOR && editor) { + editor.createButtons(); + } + + + // change environment particle style depending on theme + var theme = getThemeByName(data.theme); + + if (!theme && this.data.defaultTiles && this.data.defaultTiles[0]) { + for (var i = 0; i < mapThemes.length; i++) { + if (mapThemes[i].defaultTiles.contains(this.data.defaultTiles[0])) { + theme = mapThemes[i]; + } + } + } + + if (!theme) { + theme = mapThemes[0]; + } + + this.theme = theme; + + // generate default ground tiles (by random) + if (this.data.defaultTiles) { + for (var x = 1; x <= this.x + DEAD_MAP_SPACE; x++) { + for (var y = 1; y <= this.y + DEAD_MAP_SPACE; y++) { + new Tile({ + x: x, + y: y, + type: this.data.defaultTiles[Math.floor(Math.random() * this.data.defaultTiles.length)].toUnitType(), + }); + } + } + } + + // make cliffs + this.makeCliffsArray(); + this.makeCliffs(); + + for (var i = 0; i < data.tiles.length; i++) { + new Tile({ + x: data.tiles[i].x, + y: data.tiles[i].y, + type: data.tiles[i].type.toUnitType(), + dontRefreshNBs: true, + }); + } + + // set size depending on map size + this.defaultTilesCanvas.width = (this.x + DEAD_MAP_SPACE) * FIELD_SIZE / SCALE_FACTOR; + this.defaultTilesCanvas.height = (this.y + 2 + DEAD_MAP_SPACE) * FIELD_SIZE / SCALE_FACTOR; + + var ctx = this.defaultTilesCanvas.getContext('2d'); + var ctxM = this.minimap.groundTiles.getContext('2d'); + + // generate default ground tiles (by random) + if (this.data.defaultTiles) { + for (var x = 1; x <= this.x + DEAD_MAP_SPACE; x++) { + for (var y = -1; y <= this.y + DEAD_MAP_SPACE; y++) { + var type = this.data.defaultTiles[Math.floor(Math.random() * this.data.defaultTiles.length)].toUnitType(); + + ctx.drawImage(type.img.file[0], type.img.img.x, type.img.img.y, 16, 16, (x - 1) * 16, (y - 1 + 2) * 16, 16, 16); + + ctxM.fillStyle = type.minimapColor; + ctxM.fillRect(Math.floor((x - 1) * this.minimap.x_scale), Math.floor((y - 1) * this.minimap.y_scale), Math.ceil(this.minimap.x_scale * type.sizeX), Math.ceil(this.minimap.y_scale * type.sizeY)); + + // new Tile({x: x, y: y, type: this.data.defaultTiles[Math.floor(Math.random() * this.data.defaultTiles.length)].toUnitType()}); + } + } + } + + this.generateGroundTextureCanvas(); + + this.generateTilesCanvasses(); + + this.sortTiles(); + + this.reduceDelayOnNextTick = false; + this.increaseDelayOnNextTick = false; + + this.chat = chat ? chat : {}; + + FIELD_SIZE = 16 * SCALE_FACTOR; + + this.minimap.refreshTilesCanvas(); + + // find cc or start location + var cc = null; + for (var k = 0; k < this.buildings.length; k++) { + if (this.buildings[k].type.takesGold && this.buildings[k].owner == PLAYING_PLAYER) { + cc = this.buildings[k]; + } + } + + // clear the stats window of any previous information + StatsWindow.clear(); + + // empty chat history + $('#chatHistorytextContainer').html(''); + + // Initialize the dropdowns for spectators + // Disable them in the editor, in editor testing, LCG, and obviously if the player is not a spectator + if (!this.isEditor && !mapData && !this.isLCG && PLAYING_PLAYER.number > MAX_PLAYERS) { + this.initSpectatorInterface(); + } + + // Load settings if we are in the editor + MapEditorSettings.loadFromMap(data); + + env.setFromTheme(theme); + + this.replay_mode = typeof replayTicksCounter != 'undefined' && replayTicksCounter !== null; + game_paused = false; + show_fps = false; + this.chat_muted = false; + + if (network_game) { + interface_.chatMsg('press [ENTER] to chat', true); + } + + this.rainTime = rainTime ? rainTime : getRainTimeFromSeed(aiRandomizer); +}; + +Game.prototype.initSpectatorInterface = function() { + const refreshInterface = () => { + // Clear any existing key listeners + keyManager.removeListener('GAME_SPEC_INFO'); + keyManager.removeListener('GAME_VISION'); + + $('#spectatorDropdowns').empty(); + + // Dropdown containing information about the game + const infoBuilder = new HTMLBuilder(); + infoBuilder.add('

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 + '
'; + + $('#spectatorDiv')[0].innerHTML += str; + $('#' + rnd)[0].appendChild(img); + + x += 60; + }); + } else if (index == 5) // mined gold + { + $('#spectatorDiv')[0].innerHTML += '

Current: ' + (p.currentMinedGold * 6) + '     Total: ' + p.minedGold + '     Lost: ' + p.goldLost + '

'; + } else if (index == 6) // apm + { + $('#spectatorDiv')[0].innerHTML += '

' + Math.floor(p.apm / (ticksCounter / 1200)) + '

'; + } + + y += 60; + } + } +}; + +Game.prototype.refreshVision = function(playerNr) { + if (playerNr != this.visionSetting) { + this.visionSetting = playerNr; + PLAYING_PLAYER.team = playerNr > 0 ? this.players[playerNr].team : PLAYING_PLAYER.team = this.teams[0]; + worker.postMessage({ + what: 'requestFogMask', + teamIndex: this.players[playerNr].team.number, + }); + } +}; + +Game.prototype.getReplayFile = function() { + // build players info object + var p = []; + for (var i = 1; i < this.players.length; i++) { + if (this.players[i]) { + const player = this.players[i]; + p.push(player.getReplayObject()); + } + } + + // strip ticks without orders + var orders = {}; + for (var i = 0; i <= ticksCounter; i++) { + if (incomingOrders[i] && incomingOrders[i].length > 0) { + orders[i] = incomingOrders[i]; + } + } + + // build and return stringified replay object + return JSON.stringify({ + map: this.name, + mapVersion: this.data.timestamp, + gameVersion: GAME_VERSION, + players: p, + aiRandomizer: this.aiRandomizer, + aiCommit: AIManager.getAICommit(), + ticksCounter: ticksCounter, + orders: orders, + messages: this.chatLog, + playerLefts: playerLefts, + cameraUpdates: incomingCameraUpdates, + }); +}; + +Game.prototype.addChatMsgToLog = function(msg) { + var escapedMsg = escapeHtml(msg); + // if this index already exists, push the msg + if (this.chatLog[ticksCounter]) { + this.chatLog[ticksCounter].push(msg); + } + + // if the index doesnt exist, create it and add the msg + else { + this.chatLog[ticksCounter] = [msg]; + } + console.log(`Chat msg ${msg}`); + // add to chat history window + $('#chatHistorytextContainer')[0].innerHTML += '

' + 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) + '
Y: ' + (Math.round((field.py - 0.2) * 100) / 100)); +}; + +function MapEditorClipboard() { + this.history = new History(); + this.copyclipboard = null; + this.date = new Date(); +} + +// controls history for mapeditor +function History() { + this.actions = []; +} + +// places a Tile object (code/Tile.js) on a history to be removed on control.z later +History.prototype.addObject = function(tileObject, actionType) { + // // Disabled UNDO for height , as its not 100 % + // if(actionType=="ChangeHeight") + // return; + if (actionType==null || actionType == undefined) { + actionType = 'PlaceMapObject'; + } + var historyObject = { + obj: tileObject, + type: actionType, + time: editor.clipboard.date.getTime(), + }; + this.actions.push(historyObject); + if (this.actions.length > 100) { // max history saved + this.actions.shift(); + } +}; + +// disabled, button is commented in UI +// its not fully working yet +MapEditorClipboard.prototype.copy = function() { + if (game.selectedUnits==null || game.selectedUnits==undefined || game.selectedUnits.length==0) { + return; + } + // calculating X Y differences for reference + var differences = []; + for (var x = 0; x < game.selectedUnits.length; x++) { + var unit = game.selectedUnits[x]; + differences.push({ + x: unit.pos.px - game.selectedUnits[0].pos.px, + y: unit.pos.py - game.selectedUnits[0].pos.py, + }); + } + var copy = { + multiSelect: true, + units: game.selectedUnits, + relativeUnit: game.selectedUnits[0], + reference: differences, + }; + this.copyclipboard = copy; +}; + +History.prototype.debug = function() { + console.log('------------'); + for (var x = 0; x < this.actions.length; x++) { + var action = this.actions[x]; + console.log(action); + } +}; + +History.prototype.undo = function() { + if (this.actions.length > 0) { + var action = this.actions[this.actions.length - 1]; + var u = action.obj; + if (action.type==undefined || action.type=='PlaceMapObject') { + editor.removeObjects(u); + // undoing a delete action + } else if (action.type=='DeleteMapObject') { + soundManager.playSound(SOUND.PLACE); + var field = u.pos; + var type = u.type; + var owner = u.owner; + var newObj = null; + if (u.type.isUnit) { + newObj = new Unit({ x: field.px, y: field.py, type: type, owner: owner, noHistory: true }); + } else if (u.type.isBuilding) { + newObj = new Building({ x: field.x, y: field.y, type: type, owner: owner, noHistory: true }); + } else if (u.type.isGround || u.type.blocking) { + newObj = new Tile({ x: field.x, y: field.y, type: type, dontRefreshNBs: false, noHistory: true }); + newObj.pos.px = u.pos.px; + newObj.pos.py = u.pos.py; + if (u.type.isGround) { + var img = u.type.getTitleImage(); + game.groundTilesCanvas.getContext('2d').drawImage(img.file, img.x, img.y, img.w, img.h, Math.floor(u.drawPos.px * 16 - img.width / 2), Math.floor((u.drawPos.py + 2) * 16 - img.height / 2), img.w, img.h); + } else if (u.type.blocking) { + game.refreshBlockingTilesCanvas(field.y); + game.sortTiles(); + } + } + + // searching if this placement is in history + // so the new created object keep tracks of its older action + // with its new instance, so the undo will work as if the instance was the same + for (var x = 0; x < this.actions.length; x++) { + var action = this.actions[x]; + if (action.type=='PlaceMapObject' || action.type==undefined) { + if (action.obj == u) { + this.actions[x] = { + obj: newObj, type: 'PlaceMapObject', + }; + } + } + } + } else if (action.type=='ChangeHeight') { + var change = u; + game.setHMValue(change.x, change.y, change.oldHeight, true); + worker.postMessage('setHMValue$' + change.x + '$' + change.y + '$' + change.oldHeight); + worker.postMessage('cliffSBeenPlaced2$' + change.x + '$' + change.y + '$' + change.oldHeight); + + // a tricky part here + // we are going to look for other HM changes in the past 100ms and undo them too ! + // because each change height, gets called about 5 times + var ct = this.actions.length; + ct -= 1; // removing this current action + var indexesToRemove = []; + while (ct--) { + var older = this.actions[ct]; + if (older.type=='ChangeHeight' && (editor.clipboard.date.getTime() - older.time)<100) { + indexesToRemove.push(ct); + game.setHMValue(change.x, change.y, change.oldHeight, true); + worker.postMessage('setHMValue$' + change.x + '$' + change.y + '$' + change.oldHeight); + worker.postMessage('cliffSBeenPlaced2$' + change.x + '$' + change.y + '$' + change.oldHeight); + } + } + // removing from history the undone HM changes + for (var i = 0; i < indexesToRemove.length; i++) { + this.actions.splice(indexesToRemove[i], 1); + } + game.makeCliffs(change.x - 2, change.y -2, change.x + 2, change.y + 2); + console.log(game.getHMValue(change.x, change.y)); + // worker.postMessage("setHMValue$" + change.x+ "$" + change.y+ "$" + change.oldHeight); + // worker.postMessage("cliffSBeenPlaced2$" + change.x + "$" + change.y + "$" + change.oldHeight); + // game.sortTiles(); + } + var index = this.actions.indexOf(action); + this.actions.splice(index, 1); + } +}; + +// playes music +function MusicManager() { + this.volume = LocalConfig.registerValue('music_volume', DEFAULT_VOLUME); + this.noMainMenuMusic = LocalConfig.registerValue('no_main_menu_music', false); + + soundManager.volume.onChange((volume) => this.setSoundVolume(volume)); + this.volume.onChange((volume) => this.setMusicVolume(volume)); + + this.ingameMusic = [ + new Audio('music/ingame1.ogg'), + new Audio('music/ingame3.ogg'), + new Audio('music/ingame5.ogg'), + new Audio('music/ingame6.ogg'), + new Audio('music/opening.ogg'), + new Audio('music/BrightGameTheme.ogg'), + new Audio('music/TheOldGods.ogg'), + new Audio('music/Vengeance.ogg'), + ]; + + this.defeatMusic = [ + new Audio('music/defeat.ogg'), + ]; + + this.victoryMusic = [ + new Audio('music/victory.ogg'), + ]; + + this.menuMusic = [ + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu.ogg'), + new Audio('music/menu2.ogg'), + ]; + + this.ambient = [ + new Audio('sounds/ambient/ambient1.ogg'), + ]; + + this.rain = [ + new Audio('sounds/ambient/rain.ogg'), + new Audio('sounds/ambient/rain.ogg'), + ]; + + this.currentMusic = null; + this.lastGameState = null; + this.isMusicPlaying = false; + this.bindEvents(); +}; + +MusicManager.prototype.bindEvents = function() { + _.each([this.ingameMusic, this.defeatMusic, this.menuMusic], function(musics) { + for (var i = 0; i < musics.length; i++) { + musics[i].addEventListener('ended', function() { + setTimeout(function() { + musicManager.playMusic(); + }, 3000); + }); + + musics[i].addEventListener('canplaythrough', function() { + if (!musicManager.currentMusic) { + musicManager.playMusic(); + } + }); + + musics[i].volume = 0.5; + musics[i].loop = false; + } + }); + + this.ambient[0].loop = true; +}; + +MusicManager.prototype.setMusicVolume = function(volume) { + _.each([this.ingameMusic, this.defeatMusic, this.menuMusic, this.victoryMusic], function(musics) { + for (var i = 0; i < musics.length; i++) { + musics[i].volume = 0.7 * volume; + } + }); +}; + +MusicManager.prototype.setSoundVolume = function(volume) { + this.ambient[0].volume = 0.4 * volume; +}; + +// start playing a song; checks which song has to be played and plays it; gets called on game start or when music is turned on +MusicManager.prototype.playMusic = function() { + if (!(this.volume.get() > 0)) { + return; + } + + var music = this.getMusicFromGameState(); + + if (!music) { + return; + } + + var rand = Math.floor(Math.random() * 1000) % music.length; + for (var i = 0; i < music.length; i++) { + var musicPiece = music[(rand + i) % music.length]; + if (musicPiece.readyState == 4) { + musicPiece.currentTime = 0; + const playAndRetry = () => { + musicPiece.play() + .then(() => this.isMusicPlaying = true) + .catch(() => { + if (!this.isMusicPlaying) { + setTimeout(playAndRetry, 50); + } + }); + }; + playAndRetry(); + + i = 999; + this.currentMusic = music; + } + } +}; + +// stop playing music (called when music is turned off) +MusicManager.prototype.stopMusic = function() { + _.each([this.ingameMusic, this.defeatMusic, this.victoryMusic, this.menuMusic], function(musics) { + for (var i = 0; i < musics.length; i++) { + musics[i].pause(); + } + }); + + this.currentMusic = null; + this.isMusicPlaying = false; +}; + +// return the array which holds the music that is to be played, depending on the current game state +MusicManager.prototype.getMusicFromGameState = function() { + if (game_state == GAME.SKIRMISH || game_state == GAME.LOBBY || game_state == GAME.LOGIN || game_state == GAME.REGISTER || game_state == GAME.RECOVERY) { + return this.noMainMenuMusic.get() ? null : this.menuMusic; + } + + if (game_state == GAME.EDITOR) { + return null; + } + + if (game_state == GAME.PLAYING && game.gameHasEnded && game.playingPlayerWon) { + return this.victoryMusic; + } + + if (game_state == GAME.PLAYING && game.gameHasEnded && !game.playingPlayerWon) { + return this.defeatMusic; + } + + return this.ingameMusic; +}; + +// is called every frame, doesnt really "draw" anythiing, but checks if music has to be switched +MusicManager.prototype.draw = function() { + // get state of game (regarding the music) + var newMusic = this.getMusicFromGameState(); + + var gameStateHasChangedAndItsIngameMusic = game_state != this.lastGameState && newMusic == this.ingameMusic; + + // ambient sound has to start (we entered a game) + if (game_state == GAME.PLAYING && this.lastGameState != GAME.PLAYING && soundManager.volume.get() > 0) { + this.ambient[0].play(); + } + + // ambient sound has to stop (we left a game) + if (game_state != GAME.PLAYING && this.lastGameState == GAME.PLAYING) { + this.ambient[0].pause(); + this.rain[0].pause(); + this.rain[1].pause(); + } + + // save this game state for next time + this.lastGameState = game_state; + + if (!(this.volume.get() > 0)) { + return; + } + + // if game state has changed, music has to change + if (newMusic != this.currentMusic || gameStateHasChangedAndItsIngameMusic) { + this.stopMusic(); + + if (newMusic) { + this.playMusic(); + } + } +}; + +const musicManager = new MusicManager(); + +function Network() { + this.pings = []; // contains the last ~10 ping values, when a new one comes, the oldest one gets killed + this.EXTRA_DELAY = 2; + + this.onOpenCallbacks = []; + this.onFirstOpenCallbacks = []; + this.listeners = {}; + this.jsonListeners = {}; + + Initialization.onDocumentReady(() => this.reset(true)); +}; + +// Removes all information specific to the current player and reconnects to the server +// Retains all of the callbacks and listeners and runs the onOpen callbacks upon connection +Network.prototype.reset = function(firstConnection = false) { + if (this.socket) { + this.socket.close(); + } + + this.firstConnection = firstConnection; + this.connected = false; + this.onMapFile = () => {}; + + // connect to server and define all the callbacks + try { + this.socket = new WebSocket(SERVER_ADRESS); + this.socket.onmessage = (data) => this.onmessage(data); + this.socket.onopen = () => this.onopen(); + this.socket.onclose = () => this.onclose(); + this.socket.onerror = () => this.onclose(); + } catch (e) { + alert('You browser seems to not support Websockets. Websockets, however, are required to run this game.'); + throw new Error(e); + } +}; + +// Registers a callback to be called whenever a new network connection is established +Network.prototype.registerOnOpen = function(callback) { + this.onOpenCallbacks.push(callback); +}; + +Network.prototype.registerOnFirstOpen = function(callback) { + this.onFirstOpenCallbacks.push(callback); +}; + +// Registers a callback to be called when a message arrives with matching messageType and game_state matches requiredGameState +// requiredGameState may be null, in which case the callback will be called regardless of game_state, or an array +// Multiple listeners should not be registered to run at the same time! +Network.prototype.registerListener = function(requiredGameState, messageType, callback) { + let requiredGameStates; + if (requiredGameState == null) { + requiredGameStates = [-1]; + } else if (Array.isArray(requiredGameState)) { + requiredGameStates = requiredGameState; + } else { + requiredGameStates = [requiredGameState]; + } + + requiredGameStates.forEach((gameState) => { + if (!(gameState in this.listeners)) { + this.listeners[gameState] = {}; + } + + this.listeners[gameState][messageType] = callback; + }); +}; + +// Same as above, but registers a json listener. +// Registers a callback to be called when a message arrives with matching messageType and game_state matches requiredGameState +// requiredGameState may be null, in which case the callback will be called regardless of game_state, or an array +// Multiple listeners should not be registered to run at the same time! +Network.prototype.registerJSONListener = function(requiredGameState, messageType, callback) { + let requiredGameStates; + if (requiredGameState == null) { + requiredGameStates = [-1]; + } else if (Array.isArray(requiredGameState)) { + requiredGameStates = requiredGameState; + } else { + requiredGameStates = [requiredGameState]; + } + + requiredGameStates.forEach((gameState) => { + if (!(gameState in this.jsonListeners)) { + this.jsonListeners[gameState] = {}; + } + + this.jsonListeners[gameState][messageType] = callback; + }); +}; + +// Invokes the registered listener corresponding to the incoming server message +// Returns true if a callback was found and called, and false otherwise +Network.prototype.invokeListener = function(splitMsg) { + const messageType = splitMsg[0]; + + // Check the listeners that don't care about game state, and those that care about the current game state + const indices = [-1, game_state]; + let foundListener = false; + for (let i in indices) { + const index = indices[i]; + + if (this.listeners[index] && messageType in this.listeners[index]) { + this.listeners[index][messageType](splitMsg); + foundListener = true; + } + } + + return foundListener; +}; + +Network.prototype.invokeJSONListener = function(json) { + const messageType = json.message; + const indices = [-1, game_state]; + let foundListener = false; + for (const i in indices) { + const index = indices[i]; + + if (this.jsonListeners[index] && messageType in this.jsonListeners[index]) { + this.jsonListeners[index][messageType](json.properties); + foundListener = true; + return foundListener; + } + } + return foundListener; +}; + +Network.prototype.onopen = function() { + this.connected = true; + this.onOpenCallbacks.forEach((callback) => callback()); + if (this.firstConnection) { + this.onFirstOpenCallbacks.forEach((callback) => callback()); + } + + const expVal = localStorage.exp; + if (expVal) { + this.send('exp-update<<$' + expVal + '<<$' + expVal); + } +}; + +Network.prototype.onclose = function() { + this.connected = false; + this.failedConnection(); +}; + +Network.prototype.onmessage = function(data, flags) { + var msg = data.data; + + var splitMsg = msg.split('<<$'); + // console.log(`msg: ${splitMsg[0]} state ${game_state}`); + + this.invokeListener(splitMsg); + + // server msg + if (splitMsg[0] == 'server-info') { + displayInfoMsg(splitMsg[1]); + } + + if (splitMsg[0] == 'setExp_') { + localStorage.exp = splitMsg[1]; + } else if (splitMsg[0] == 'unlock-emotes') { + unlockEmote(splitMsg[1], splitMsg[2]); + } else if (splitMsg[0] == 'achivement-unlocked') { + unlockAchivement(splitMsg[1]); + } else if (splitMsg[0] == 'level-up') { + levelUp(splitMsg); + } else if (splitMsg[0] == 'custom-map-editor-file') { + const canEditMap = splitMsg[2] == '1'; + if (!canEditMap) { + displayInfoMsg('You can only edit your own maps or unlocked maps!'); + return; + } + + this.onMapFile = (map) => { + map.img = splitMsg[1]; + + uimanager.showLoadingScreen(map); + + setTimeout(() => { + game = new Game(); + game.loadMap(map, null, null, null, true); + worker.postMessage({ what: 'start-game', editorLoad: true, map, network_game, game_state, networkPlayerName }); + }, 50); + }; + } else if (splitMsg[0] == 'custom-map-replay-file') { + this.onMapFile = async (map) => { + if (map.timestamp != replayFile.mapVersion) { + displayInfoMsg( + 'This replay was made with an older map version so it might be buggy.', + ); + } else if (replayFile.gameVersion != GAME_VERSION) { + displayInfoMsg( + `This replay was recorded in version ${replayFile.gameVersion}. ` + + `The current game version is ${GAME_VERSION} so it might be buggy.`, + ); + } + + const hasCustomAI = replayFile.players.some( + (p) => p.ai_name && p.ai_name.startsWith('Custom AI'), + ); + if (hasCustomAI) { + displayInfoMsg( + 'This replay was played with a custom AI. If the same custom AI ' + + 'is not loaded, the replay may be buggy.', + ); + } + + const customAINames = AIManager.getNames(/* includeRegularAI=*/false, + /* includeCustomAI=*/true); + for (const p of replayFile.players) { + if (!p.controller == CONTROLLER.COMPUTER || !p.ai_name) continue; + + // Make sure there is _a_ custom AI loaded in the same slot + if (p.ai_name.startsWith('Custom AI')) { + if (!customAINames.includes(p.ai_name)) { + displayInfoMsg( + 'This replay was played with a custom AI, but there is no ' + + 'custom AI loaded in the same slot.', + ); + return; + } + continue; + } + + // The AI may have to be loaded if it was on a different commit + if (p.ai_name != 'Default') { + const success = await AIManager.loadAI(replayFile.aiCommit, p.ai_name); + if (!success) { + displayInfoMsg(`Failed to load AI with name ${p.ai_name} and commit ${replayFile.aiCommit} in replay file`); + return; + } + } + } + + network.send('cancel-ladder'); + fadeOut($('#ladderWindow')); + + const p = replayFile.players.concat([{ name: networkPlayerName, controller: CONTROLLER.SPECTATOR, team: 0, isPlayingPlayer: true }]); + + map.img = getImageFromMap(map); + uimanager.showLoadingScreen(map, p); + + setTimeout(() => { + game_state = GAME.PLAYING; + + game = new Game(); + game.loadMap(map, p, replayFile.aiRandomizer, replayFile.ticksCounter, false, false, replayFile.messages); + worker.postMessage({ + what: 'start-game', + map: map, + players: p, + network_game: network_game, + game_state: game_state, + networkPlayerName: networkPlayerName, + aiCommit: replayFile.aiCommit, + aiRandomizer: replayFile.aiRandomizer, + ticksCounter: replayFile.ticksCounter, + incomingOrders: replayFile.orders, + playerLefts: replayFile.playerLefts, + }); + + incomingOrders = replayFile.orders; + playerLefts = replayFile.playerLefts; + incomingCameraUpdates = replayFile.cameraUpdates ?? {}; + + mapData = ''; + + $('#replayShowSpeed').html('1x'); + }, 50); + }; + } else if (splitMsg[0] == 'achivements-list') { + showAchievementsWindow(splitMsg[1]); + } else if (splitMsg[0] == 'gold-reward') { + displayInfoMsgDarkBG('

You got ' + splitMsg[1] + ' gold




', true); + } + + // Emotes info + else if (splitMsg[0] == 'emotes-info') { + Microtransactions.showEmotesInfo(splitMsg[1], splitMsg[2]); + } else if (splitMsg[0] == 'skins-info') { + let skinsObj = null; + try { + skinsObj = JSON.parse(splitMsg[2]); + } catch (e) {} + if (!skinsObj) { + skinsObj = {}; + } + + Microtransactions.showSkinsDancesInfo(splitMsg[4], splitMsg[1], skinsObj, splitMsg[3]); + } else if (splitMsg[0] == 'map-file') { + // TODO: this is a bit of a hack, we really should have a better messaging format + let mapData = ''; + for (let i = 1; i < splitMsg.length; i++) { + mapData += splitMsg[i]; + if (i != splitMsg.length - 1) { + mapData += '<<$'; + } + } + + const map = JSON.parse(Compression.decompressFromString(mapData)); + this.onMapFile(map); + } else if (game_state == GAME.EDITOR) { + if (msg == 'map-upload-init') { + var map = game.export_(); + this.send('uploading-map<<$' + JSON.stringify(map) + '<<$' + getImageFromMap(game.export_(true)) + '<<$' + getPlayerCountFromMap(map) + '<<$' + game.description); + } else if (splitMsg[0] == 'personal-maps') { + // clear window + $('#mapWindowSubdiv').html(''); + + // display maps + for (var i = 1; i < splitMsg.length; i++) { + var p = document.createElement('p'); + p.innerHTML = splitMsg[i] + ' '; + + var b = document.createElement('button'); + b.innerHTML = 'X'; + b.i_ = splitMsg[i]; + b.title = 'delete this map'; + b.onclick = function() { + network.send('map-delete-request<<$' + this.i_); + soundManager.playSound(SOUND.CLICK); + }; + + p.appendChild(b); + $('#mapWindowSubdiv').append(p); + } + } else if (msg == 'map-has-been-deleted') { + this.send('request-my-maps'); + } + } else if (game_state == GAME.LOBBY || game_state == GAME.ACCEPT_AGB || game_state == GAME.SKIRMISH) { + if (splitMsg[0] == 'start-game' && splitMsg.length >= 4) { + network.send('cancel-ladder'); + fadeOut($('#ladderWindow')); + + // parse players object + var obj = JSON.parse(splitMsg[3]); + + // set players object + var players = obj.players; + + // set playing player + players[splitMsg[1]].isPlayingPlayer = true; + + // random KI number + var aiRandomizer = splitMsg[2]; + + network_game = true; + ladder_game = false; + + // show loading screen + uimanager.showLoadingScreen(LobbyPlayerManager.map, players); + + setTimeout(function() { + game = new Game(); + game.loadMap(LobbyPlayerManager.map, players, aiRandomizer); + game_state = GAME.PLAYING; + mapData = ''; + worker.postMessage({ + what: 'start-game', + map: LobbyPlayerManager.map, + players: players, + aiCommit: AIManager.getAICommit(), + aiRandomizer: aiRandomizer, + network_game: network_game, + game_state: game_state, + networkPlayerName: networkPlayerName, + }); + + // send initial order cycle(s) + for (var i = 0; i < TICKS_DELAY - 1; i++) { + network.send(JSON.stringify({ 'tick': i, 'orders': [] })); + } + }, 50); + } else if (splitMsg[0] == 'custom-map-ladder-file' && splitMsg.length >= 3) { + $('#ladderWindow')[0].style.display = 'inline'; + + this.onMapFile = (map) => { + map.img = splitMsg[1]; + + const aiRandomizer = splitMsg[3]; + const players = JSON.parse(splitMsg[4]).players; + players[splitMsg[2]].isPlayingPlayer = true; + + network_game = true; + ladder_game = true; + + let counter = 10; + new HTMLBuilder() + .add('

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 += '
' + Math.ceil(i / 2) + '. ' + splitMsg[i] + ' (' + getRankCode(splitMsg[i + 1]) + ')'; + } + + p.appendChild(time); + p.appendChild(span); + + addToChatWindow(p); + } else if (splitMsg[0] == 'chat') { + // split chat msg and add time + var display = true; + + // its a command, (theres a "/" at the beginning, so dont display message, but execute command) + if (splitMsg[3].indexOf('/') === 0) { + display = false; + + // if its a ping command and we are the sending player + if (splitMsg[3] == '/ping' && splitMsg[2] == networkPlayerName) { + display = true; + splitMsg[3] = 'ping: ' + (Date.now() - timeOfLastPingSent) + ' ms'; + } + } + + if (display && !AccountInfo.ignores.contains(splitMsg[2].toLowerCase())) { + addChatMsg(splitMsg[2], splitMsg[3], splitMsg[1]); + } + } + } else if (game_state == GAME.PLAYING && network_game) { + if (splitMsg[0] == 'order-missing') { + var tickToSend = parseInt(splitMsg[1]); + + // if(lastSentTick >= tickToSend) + network.send(JSON.stringify({ tick: tickToSend, orders: [] })); + + // console.log("missing order " + tickToSend + " resent"); + } else if (splitMsg[0] == 'chat') { + if (!AccountInfo.ignores.contains(splitMsg[2].toLowerCase())) { + var msg_ = splitMsg[2] + ': ' + splitMsg[3]; + game.addChatMsgToLog(msg_); + // console.log(`Muted: ${game.chat_muted}`); + if (!game.chat_muted) { + interface_.chatMsg(msg_); + } + } + } else if (splitMsg[0] == 'chat-server') { + soundManager.playSound(SOUND.POSITIVE); + interface_.addMessage(splitMsg[1], 'yellow', imgs.attentionmarkYellow); + } else if (splitMsg[0] == 'ping') { + this.pings.push(Date.now() - timeOfLastPingSent); + if (this.pings.length > 8) { + this.pings.splice(0, 1); + } + + var delayShouldBe = Math.ceil(Math.max.apply(null, this.pings) / TICK_TIME) + this.EXTRA_DELAY + ((PLAYING_PLAYER && PLAYING_PLAYER.controller == CONTROLLER.SPECTATOR) ? 2 : 0); + + if (delayShouldBe > TICKS_DELAY) { + game.increaseDelayOnNextTick = true; + } else if (delayShouldBe < TICKS_DELAY) { + game.reduceDelayOnNextTick = true; + } + } else if (splitMsg[0] == 'waiting-for') { + interface_.addMessage('Waiting for player ' + splitMsg[1], 'yellow', imgs.attentionmarkYellow); + } else if (msg == 'youve-been-kicked') { + game_state = GAME.LOBBY; + + // set focus on input + setChatFocus(true); + + keyManager.resetCommand(); + } else if (splitMsg[0] == 'game-paused') { + game_paused = true; + interface_.addMessage('Game paused by ' + splitMsg[1] + ' (' + splitMsg[2] + ' pauses left)', 'yellow', imgs.attentionmarkYellow); + soundManager.playSound(SOUND.POSITIVE); + worker.postMessage({ what: 'setPause', val: game_paused }); + } else if (splitMsg[0] == 'game-unpaused') { + game_paused = false; + interface_.addMessage('Game unpaused by ' + splitMsg[1], 'yellow', imgs.attentionmarkYellow); + soundManager.playSound(SOUND.POSITIVE); + worker.postMessage({ what: 'setPause', val: game_paused }); + } else if (splitMsg[0] == 'map-ping') { + if (!AccountInfo.ignores.contains(splitMsg[3].toLowerCase())) { + game.minimap.mapPings.push({ field: new Field(parseInt(splitMsg[1]), parseInt(splitMsg[2])), time: Date.now() }); + soundManager.playSound(SOUND.BING2); + } + } + + // orders + else if (splitMsg[0] != 'dummy') { + worker.postMessage({ what: 'orders', msg: msg }); + + try { + var parsedMsg = JSON.parse(msg); + if (parsedMsg.tick !== undefined) { + incomingOrders[parsedMsg.tick] = parsedMsg.orders; + if (parsedMsg.playersLeft) { + playerLefts[parsedMsg.tick] = parsedMsg.playersLeft; + } + if (Object.keys(parsedMsg.cameraUpdates).length > 0) { + incomingCameraUpdates[parsedMsg.tick] = parsedMsg.cameraUpdates; + } + } + } catch (e) { + console.log('main thread error parsing orders msg', msg); + } + } + } + let jsonMessage; + try { + jsonMessage = JSON.parse(msg); + } catch (err) { + // Fail silently + } + + if (jsonMessage && jsonMessage.message) { + this.invokeJSONListener(jsonMessage); + return; + } +}; + +function setChatFocus(show) { + if (show) { + $('#lobbyChatInput')[0].focus(); + } else { + $('#lobbyGameChatInput')[0].focus(); + } +} + +function currencyFormatter(currency) { + for (let i = currency.length -3; i > 0; i-=3) { + currency = currency.slice(0, i) + ',' + currency.slice(i, currency.length); + } + return currency; +} + +Network.prototype.failedConnection = function() { + // (id, condition, closeable, title, draggable, onKey) + var optionsWindow = new UIWindow('NoConnectionWindow', function() { + return true; + }, false, 'Connection Error', false); + optionsWindow.refreshVisibility(); + elements.push(optionsWindow); + console.log('No connection.'); + if (!this.socket) { + console.log('Websocket failed. Does your browser support it?'); + } else if (!this.connected) { + console.log('Websocket success. Could not connect to server. Try again later.'); + } else { + console.log('Websocket Success. Connected. Send Failed.'); + } +}; + +Network.prototype.send = function(data) { + if (this.connected && this.socket) { + this.socket.send(data); + return; + } + this.failedConnection(); +}; + +// TODO: move this somewhere where it better belongs +Network.prototype.getClanLink = function(p) { + const builder = new HTMLBuilder(); + builder.add(''); + if (p.clan && p.clan.length > 0) { + const clanLinkID = uniqueID('clanLink'); + builder.add(`[${p.clan}] `); + + const clan = p.clan; // Required so that link stays valid after p.clan changes + builder.addHook(() => $(`#${clanLinkID}`).click(() => Clans.getClanInfo(clan))); + } + builder.add(''); + return builder; +}; + +// TODO: move this somewhere where it better belongs +Network.prototype.getPlayerLink = function(p, noStyle) { + const builder = new HTMLBuilder(); + if (p.name == 'Server') { + return builder.add('Server'); + } + + const linkID = uniqueID('playerLink'); + const linkClass = noStyle ? + 'underline' : + (p.name != 'Server' ? ((p.premium && p.authLevel < AUTH_LEVEL.MOD) ? 'playerLinkPremium' : auth_level_css_classes[p.authLevel]) : '') + ' playerNameInList'; + + builder.add(`${p.name}`); + + if (p.name != 'Server' && (p.name != networkPlayerName || p.authLevel >= AUTH_LEVEL.PLAYER)) { + if (p.authLevel >= AUTH_LEVEL.PLAYER) { + builder.addHook(() => $(`#${linkID}`).click( + addClickSound(() => network.send(JSON.stringify({ message: 'get-player-info', properties: { username: p.name } }))))); + } else { + builder.addHook(() => $(`#${linkID}`).click(() => Chats.openChatFor(p.name, true))); + } + } + + return builder; +}; + +// just a "static js-class" (HA-HA) that has some image transform methods +function ImageTransformer() { + +}; + +// return a greyscaled version of a given image +ImageTransformer.getGreyScaledImage = function(img) { + var canv = document.createElement('canvas'); + canv.height = img.height; + canv.width = img.width; + if (img.width == 0 || img.height == 0) { + throw Error(`Tried to transform image "${img.name}" (from ${img.src}) that has height or width 0.`); + } + var ctx = canv.getContext('2d'); + + ctx.drawImage(img, 0, 0); + + var imgData = ctx.getImageData(0, 0, canv.width, canv.height); + + for (var i = 0; i < imgData.data.length; i += 4) { + var newValue = (imgData.data[i] + imgData.data[i + 1] + imgData.data[i + 2]) / 3; + imgData.data[i] = newValue; + imgData.data[i + 1] = newValue; + imgData.data[i + 2] = newValue; + } + + ctx.putImageData(imgData, 0, 0); + + return canv; +}; + +// returns a image with some colors replaced, specified by search and replace, which are arrays of color arrays ([[255, 255, 255], [...], ...], ) +ImageTransformer.replaceColors = function(img, search, replace) { + var canv = new Array(replace.length); + var imgData = new Array(replace.length); + + // If we were sent an asset reference by mistake, fix it here. + // if (typeof img == "string") + // { + // img = loadImage(`${img}.png`); + // } + if (img.width == 0 || img.height == 0) { + throw Error(`Tried to transform image "${img.name}" (from ${img.src}) that has height or width 0.`); + } + for (var i = 0; i < replace.length; i++) { + canv[i] = document.createElement('canvas'); + canv[i].height = img.height; + canv[i].width = img.width; + canv[i].getContext('2d').drawImage(img, 0, 0); + imgData[i] = canv[i].getContext('2d').getImageData(0, 0, img.width, img.height); + } + + var data0 = imgData[0].data; + for (var i = 0; i < data0.length; i += 4) { + if (data0[i + 3] > 0) { + for (var k = 0; k < search.length; k++) { + if (data0[i] == search[k][0] && data0[i + 1] == search[k][1] && data0[i + 2] == search[k][2]) { + for (var j = 0; j < replace.length; j++) { + imgData[j].data[i] = replace[j][k][0]; + imgData[j].data[i + 1] = replace[j][k][1]; + imgData[j].data[i + 2] = replace[j][k][2]; + } + } + } + } + } + + for (var i = 0; i < replace.length; i++) { + canv[i].getContext('2d').putImageData(imgData[i], 0, 0); + } + + return canv; +}; + +// returns a image with all colors replaced with white +ImageTransformer.replaceColorsWhite = function(img, color) { + color = color ? color : [255, 255, 255]; + + var canv = document.createElement('canvas'); + canv.height = img.height; + canv.width = img.width; + var ctx = canv.getContext('2d'); + + ctx.drawImage(img, 0, 0); + + var imgData = ctx.getImageData(0, 0, canv.width, canv.height); + + for (var i = 0; i < imgData.data.length; i += 4) { + if (imgData.data[i + 3] > 0) { + imgData.data[i] = color[0]; + imgData.data[i + 1] = color[1]; + imgData.data[i + 2] = color[2]; + } + } + + ctx.putImageData(imgData, 0, 0); + + return canv; +}; + +// return the average color of an image (used to get a color to represent the image on the minimap) +ImageTransformer.getAverageColor = function(img) { + c2.clearRect(0, 0, img.img.w, img.img.h); + c2.drawImage(img.file[0], img.img.x, img.img.y, img.img.w, img.img.h, 0, 0, img.img.w, img.img.h); + var data = c2.getImageData(0, 0, img.img.w, img.img.h).data; + + var p = [0, 0, 0]; + var count = 0.01; + for (var i = 0; i < data.length; i += 4) { + if (data[i + 3] > 0) // if not + { + count++; + for (k = 0; k < 3; k++) { + p[k] += data[i + k]; + } + } + } + + return 'rgba(' + Math.floor(p[0] / count) + ', ' + Math.floor(p[1] / count) + ', ' + Math.floor(p[2] / count) + ', ' + (count / (data.length / 4)) + ')'; +}; + +// returns a canvas based on a spritesheet and coords in the form of {x = 5, y = 8, w = 3, h = 3} +ImageTransformer.getImgFromSheet = function(sheet, coords) { + // create new canvas + var canv = document.createElement('canvas'); + canv.height = coords.h; + canv.width = coords.w; + + // draw image on canvas + canv.getContext('2d').drawImage(sheet, coords.x, coords.y, coords.w, coords.h, 0, 0, coords.w, coords.h); + + return canv; +}; + +// an UIElement, mostly a HTML Element, with some extra features +function UIElement(type, id, condition, killOnHide, onKey) { + this.id = id; + this.domElement = document.createElement(type); + this.domElement.id = id; + + if (killOnHide) { + this.domElement.style.display = condition ? 'none' : 'block'; + } else { + this.domElement.style.visibility = condition ? 'hidden' : 'visible'; + } + + document.body.appendChild(this.domElement); + + this.condition = condition; // function that returns true, if the element should be drawn + this.wasActiveLastFrame = false; + this.blocksCanvas = true; // if this is true and the element is active, clicks do not get delegated to the canvas + this.killOnHide = killOnHide; + + this.onKey = onKey; +}; + +UIElement.prototype.refreshVisibility = function() { + if (!this.condition) { + return; + } + + if (this.condition()) { + if (this.wasActiveLastFrame) { + return this.blocksCanvas; + } + + this.wasActiveLastFrame = true; + + if (this.killOnHide) { + this.domElement.style.display = 'block'; + } else { + this.domElement.style.visibility = 'visible'; + } + + // if its an inout which gets just active in this frame, set focus to it + if (this.domElement.tagName == 'INPUT') { + this.domElement.focus(); + this.domElement.value = this.domElement.value; // re-set value, so cursor is at the end and not at the beginning + } + + return this.blocksCanvas; + } + + if (!this.wasActiveLastFrame) { + return false; + } + + this.wasActiveLastFrame = false; + + if (this.killOnHide) { + this.domElement.style.display = 'none'; + } else { + this.domElement.style.visibility = 'hidden'; + } + + return false; +}; + +const LobbyPlayerManager = (() => { + const dedent = require('dedent'); + + // Terminology: + // Slot - either a player slot or spectator slot that slot bars are dragged into + // Slot bar - the
  • elements that can be dragged between slots + // Index - a 0-based index into slotContents where [0, MAX_PLAYERS) correspond to player slots + // and MAX_PLAYERS, or SPECTATOR_INDEX, corresponds to all the spectator slots together + // Used locally in the client + // Position - a 1-based index into the list of players where [1, MAX_PLAYERS] correspond to player slots + // and MAX_PLAYERS+1, or SPECTATOR_POSITION, to MAX_PLAYERS+MAX_SPECTATORS (inclusive) correspond to each of the spec spots + // Used by the server + // Settings (playerSettings) - the map-specific constraints on what slots are open/closed/CPU, what team they can be, + // and what AI they can have + // Contents - the data describing the player currently in a slot which is the backing data for the slot bars + + const MAX_SPECTATORS = 4; + const SPECTATOR_INDEX = MAX_PLAYERS; // 0-based index for client + const SPECTATOR_POSITION = MAX_PLAYERS + 1; // 1-based index for server + + // The default team assigned to CPUs when they are added + const DEFAULT_CPU_TEAM = 2; + + // Enums used in playerSettings + const SlotType = Object.freeze({ OPEN: 0, CLOSED: 1, COMPUTER: 2 }); + const AIType = Object.freeze({ NORMAL: 0, NONE: 1 }); + + // Enum used in slotContents + const PlayerType = Object.freeze({ PLAYER: 0, CPU: 1 }); + + function LobbyPlayerManager_() { + Initialization.onDocumentReady(() => this.init()); + + this.active = false; + this.map = null; + + let isMultiplayer = false; + Object.defineProperty(this, 'isMultiplayer', { + get: () => isMultiplayer, + set: (value) => { + isMultiplayer = value; + $('#inviteButton').toggle(isHost && isMultiplayer); + }, + }); + // Game ID for multiplayer games + this.gameID = null; + // Invites sent out for multiplayer games (Host only) + this.invites = []; + + let isHost = false; + Object.defineProperty(this, 'isHost', { + get: () => isHost, + set: (value) => { + isHost = value; + $('#changeMapButton').toggle(isHost); + $('#startButton').toggle(isHost); + $('#addCpuButton').toggle(isHost); + $('#inviteButton').toggle(isHost && isMultiplayer); + $('.connectedSortable').sortable(isHost ? 'enable' : 'disable'); + $('.invitePlayerButton').prop('disabled', false); + this.invites = []; + this.__updateSlots(); + }, + }); + + // Array of settings for the 6 player slots + // Each setting has the format {slot: SlotType, team: Int (where 0 represents any team), ai: AIType} + this.playerSettings = []; + + // JQuery references to the player slots'