-
-
diff --git a/AnnoCalculator.js b/AnnoCalculator.js
index 3682a5c..a48bf3b 100644
--- a/AnnoCalculator.js
+++ b/AnnoCalculator.js
@@ -1,17 +1,71 @@
-products = new Map();
-assetsMap = new Map();
+let versionCalculator = "v1.0";
+let ACCURACY = 0.01;
+let EPSILON = 0.0000001;
+let ALL_ISLANDS = "All Islands";
+
+
+
view = {
- populationLevels: [],
- factories: [],
- categories: [],
- workforce: [],
- buildingMaterialsNeeds: [],
settings: {
- language: ko.observable(navigator.language.startsWith("de") ? "german" : "english")
+ language: ko.observable("english")
},
texts: {}
-}
+};
+
+for (var code in languageCodes)
+ if (navigator.language.startsWith(code))
+ view.settings.language(languageCodes[code]);
+
+class Storage {
+ constructor(key) {
+ this.key = key;
+ var text = localStorage.getItem(key);
+ this.json = text ? JSON.parse(text) : {};
+
+ this.length = 0;
+ for (var attr in this.json)
+ this.length = this.length + 1;
+ }
+
+ setItem(itemKey, value) {
+ if (this.json[itemKey] == null)
+ this.length = this.length + 1;
+
+ this.json[itemKey] = value;
+ this.save();
+ }
+
+ getItem(itemKey) {
+ return this.json[itemKey];
+ }
+
+ removeItem(itemKey) {
+ if (this.json[itemKey] != null)
+ this.length = this.length - 1;
+
+ delete this.json[itemKey];
+ this.save();
+ }
+ key(index) {
+ var i = 0;
+ for (let attr in this.json)
+ if (i++ == index)
+ return attr;
+
+ return null;
+ }
+
+ clear() {
+ this.json = {}
+ this.save();
+ this.length = 0;
+ }
+
+ save() {
+ localStorage.setItem(this.key, JSON.stringify(this.json, null, 4));
+ }
+}
class NamedElement {
constructor(config) {
@@ -25,324 +79,2527 @@ class NamedElement {
text = this.locaText["english"];
return text ? text : config.name;
- })
+ });
+
+ if (this.iconPath && params && params.icons)
+ this.icon = params.icons[this.iconPath];
+ }
+}
+
+class Region extends NamedElement { }
+class Session extends NamedElement {
+ constructor(config, assetsMap) {
+ super(config);
+
+ this.region = assetsMap.get(config.region);
+ }
+}
+
+class Option extends NamedElement {
+ constructor(config) {
+ super(config);
+ this.checked = ko.observable(false);
+ this.visible = !!config;
+ }
+}
+
+class Island {
+ constructor(params, localStorage, session) {
+ if (localStorage instanceof Storage) {
+ this.name = ko.observable(localStorage.key);
+ this.isAllIslands = function () { return false; };
+ } else {
+ this.name = ko.computed(() => view.texts.allIslands.name());
+ this.isAllIslands = function () { return true; };
+ }
+ this.storage = localStorage;
+ var isNew = !localStorage.length;
+
+ this.session = session || this.storage.getItem("session");
+ this.session = this.session instanceof Session ? this.session : view.assetsMap.get(this.session);
+ this.region = this.session ? this.session.region : null;
+
+ this.storage.setItem("session", this.session ? this.session.guid : null);
+
+ var assetsMap = new Map();
+ for (var key of view.assetsMap.keys())
+ assetsMap.set(key, view.assetsMap.get(key));
+
+ this.sessionExtendedName = ko.pureComputed(() => {
+ if (!this.session)
+ return this.name();
+
+ return `${this.session.name()} - ${this.name()}`;
+ });
+
+ this.populationLevels = [];
+ this.residenceBuildings = [];
+ this.consumers = [];
+ this.factories = [];
+ this.categories = [];
+ this.workforce = [];
+ this.buildingMaterialsNeeds = [];
+ this.multiFactoryProducts = [];
+ this.items = [];
+ this.replaceInputItems = [];
+ this.extraGoodItems = [];
+ this.allGoodConsumptionUpgrades = new GoodConsumptionUpgradeIslandList();
+
+ for (let workforce of params.workforce) {
+ let w = new Workforce(workforce, assetsMap);
+ assetsMap.set(w.guid, w);
+ this.workforce.push(w);
+ }
+
+ for (let consumer of (params.powerPlants || [])) {
+ //if (this.region && this.region.guid != consumer.region)
+ // continue;
+
+ let f = new Consumer(consumer, assetsMap, this);
+ assetsMap.set(f.guid, f);
+ this.consumers.push(f);
+
+ if (localStorage) {
+ {
+ let id = f.guid + ".existingBuildings";
+ if (localStorage.getItem(id) != null)
+ f.existingBuildings(parseInt(localStorage.getItem(id)));
+
+ f.existingBuildings.subscribe(val => localStorage.setItem(id, val));
+ }
+ }
+ }
+
+ for (let consumer of (params.modules || [])) {
+ let f = new Module(consumer, assetsMap);
+ assetsMap.set(f.guid, f);
+ this.consumers.push(f);
+ }
+
+ if (!this.region || this.region.guid === 5000000) {
+ for (let buff of (params.palaceBuffs || [])) {
+ let f = new PalaceBuff(buff, assetsMap);
+ assetsMap.set(f.guid, f);
+ }
+ }
+
+ for (let factory of params.factories) {
+ let f = new Factory(factory, assetsMap, this);
+ assetsMap.set(f.guid, f);
+ this.consumers.push(f);
+ this.factories.push(f);
+
+ if (localStorage) {
+ if (f.moduleChecked) { // set moduleChecked before boost, otherwise boost would be increased
+ let id = f.guid + ".module.checked";
+ if (localStorage.getItem(id) != null)
+ f.moduleChecked(parseInt(localStorage.getItem(id)));
+
+ f.moduleChecked.subscribe(val => localStorage.setItem(id, val ? 1 : 0));
+ }
+
+ if (f.palaceBuff) {
+ let id = f.guid + ".palaceBuff.checked";
+ if (localStorage.getItem(id) != null)
+ f.palaceBuffChecked(parseInt(localStorage.getItem(id)));
+
+ f.palaceBuffChecked.subscribe(val => localStorage.setItem(id, val ? 1 : 0));
+ }
+
+ {
+ let id = f.guid + ".percentBoost";
+ if (localStorage.getItem(id) != null)
+ f.percentBoost(parseInt(localStorage.getItem(id)));
+
+ f.percentBoost.subscribe(val => {
+ val = parseInt(val);
+
+ if (val == null || !isFinite(val) || isNaN(val)) {
+ f.percentBoost(parseInt(localStorage.getItem(id)) || 100);
+ return;
+ }
+ localStorage.setItem(id, val);
+ });
+ }
+
+ {
+ let id = f.guid + ".existingBuildings";
+ if (localStorage.getItem(id) != null)
+ f.existingBuildings(parseInt(localStorage.getItem(id)));
+
+ f.existingBuildings.subscribe(val => localStorage.setItem(id, val));
+ }
+ }
+ }
+ let products = [];
+ for (let product of params.products) {
+ if (product.producers && product.producers.length) {
+ let p = new Product(product, assetsMap);
+
+ products.push(p);
+ assetsMap.set(p.guid, p);
+
+ if (p.factories.length > 1)
+ this.multiFactoryProducts.push(p);
+
+ if (localStorage) {
+ {
+ let id = p.guid + ".percentBoost";
+ if (localStorage.getItem(id) != null) {
+ let b = parseInt(localStorage.getItem(id))
+ p.factories.forEach(f => f.percentBoost(b));
+ localStorage.removeItem(id);
+ }
+ }
+
+
+ {
+ let id = p.guid + ".fixedFactory";
+ if (localStorage.getItem(id) != null)
+ p.fixedFactory(assetsMap.get(parseInt(localStorage.getItem(id))));
+ p.fixedFactory.subscribe(
+ f => f ? localStorage.setItem(id, f.guid) : localStorage.removeItem(id));
+ }
+ }
+
+ if (isNew && p.guid == 1010240)
+ p.fixedFactory(assetsMap.get(1010318));
+ }
+ }
+
+ for (let item of (params.items || [])) {
+ let i = new Item(item, assetsMap, this.region);
+ if (!i.factories.length)
+ continue; // Affects no factories in this region
+
+ assetsMap.set(i.guid, i);
+ this.items.push(i);
+
+ if (i.replacements)
+ this.replaceInputItems.push(i);
+
+ if (i.additionalOutputs)
+ this.extraGoodItems.push(i);
+
+ if (localStorage) {
+ let oldId = i.guid + ".checked";
+ var oldChecked = false;
+ if (localStorage.getItem(oldId) != null)
+ oldChecked = parseInt(localStorage.getItem(oldId));
+
+ for (var equip of i.equipments) {
+ let id = `${equip.factory.guid}[${i.guid}].checked`;
+
+ if (oldChecked)
+ equip.checked(true);
+
+ if (localStorage.getItem(id) != null)
+ equip.checked(parseInt(localStorage.getItem(id)));
+
+ equip.checked.subscribe(val => localStorage.setItem(id, val ? 1 : 0));
+ }
+
+ localStorage.removeItem(oldId);
+ }
+ }
+
+ this.extraGoodItems.sort((a, b) => a.name() > b.name());
+ view.settings.language.subscribe(() => {
+ this.extraGoodItems.sort((a, b) => a.name() > b.name());
+ });
+
+ // must be set after items so that extraDemand is correctly handled
+ this.consumers.forEach(f => f.referenceProducts(assetsMap));
+
+ // setup demands induced by modules
+ for (let factory of params.factories) {
+ let f = assetsMap.get(factory.guid);
+ if (f && f.module)
+ f.moduleDemand = new Demand({ guid: f.module.getInputs()[0].Product, region: f.region }, assetsMap);
+ }
+
+
+ for (var building of (params.residenceBuildings || [])) {
+ var b = new ResidenceBuilding(building, assetsMap);
+ assetsMap.set(b.guid, b);
+ this.residenceBuildings.push(b);
+ }
+
+ for (let level of params.populationLevels) {
+
+ let l = new PopulationLevel(level, assetsMap);
+ assetsMap.set(l.guid, l);
+ this.populationLevels.push(l);
+
+ if (localStorage) {
+ {
+ let id = l.guid + ".amount";
+ if (localStorage.getItem(id) != null)
+ l.amount(parseInt(localStorage.getItem(id)));
+
+ l.amount.subscribe(val => {
+ val = parseInt(val);
+
+ if (val == null || !isFinite(val) || isNaN(val)) {
+ l.amount(parseInt(localStorage.getItem(id)) || 0);
+ return;
+ }
+ localStorage.setItem(id, val);
+ });
+ }
+ {
+ let id = l.guid + ".existingBuildings";
+ if (localStorage.getItem(id) != null)
+ l.existingBuildings(parseInt(localStorage.getItem(id)));
+
+ l.existingBuildings.subscribe(val => localStorage.setItem(id, val))
+ }
+ } else {
+ l.amount.subscribe(val => {
+ if (val == null || !isFinite(val) || isNaN(val)) {
+ l.amount(0);
+ return;
+ }
+ });
+ }
+
+ for (let n of l.needs) {
+ if (localStorage) {
+ {
+ let id = `${l.guid}[${n.guid}].checked`;
+ if (localStorage.getItem(id) != null)
+ n.checked(parseInt(localStorage.getItem(id)))
+
+ n.checked.subscribe(val => localStorage.setItem(id, val ? 1 : 0));
+
+ }
+
+ {
+ let id = `${l.guid}[${n.guid}].percentBoost`;
+ if (localStorage.getItem(id) != null)
+ n.percentBoost(parseFloat(localStorage.getItem(id)));
+
+ n.percentBoost.subscribe(val => {
+ val = parseFloat(val);
+
+ if (val == null || !isFinite(val) || isNaN(val)) {
+ n.percentBoost(parseFloat(localStorage.getItem(id)) || 100);
+ return;
+ }
+ localStorage.setItem(id, val);
+ });
+ }
+
+ } else {
+ n.percentBoost.subscribe(val => {
+ if (val == null || !isFinite(val) || isNaN(val)) {
+ n.percentBoost(100);
+ return;
+ }
+ });
+ }
+
+ }
+ }
+
+ for (var category of params.productFilter) {
+ let c = new ProductCategory(category, assetsMap);
+ assetsMap.set(c.guid, c);
+ this.categories.push(c);
+ }
+
+ for (let powerPlant of (params.powerPlants || [])) {
+ var pl = assetsMap.get(powerPlant.guid);
+ if (!pl)
+ continue; // power plant not constructable in this region
+
+ this.categories[1].consumers.push(pl);
+ pl.editable(true);
+ var pr = pl.getInputs()[0].product;
+ let n = new PowerPlantNeed({ guid: pr.guid, factory: pl, product: pr }, assetsMap);
+ pl.existingBuildings.subscribe(() => n.updateAmount());
+ n.updateAmount();
+ }
+
+ for (let p of this.categories[1].products) {
+ if (p)
+ for (let b of p.factories) {
+ if (b) {
+ b.editable(true);
+ let n = new BuildingMaterialsNeed({ guid: p.guid, factory: b, product: p }, assetsMap);
+ b.boost.subscribe(() => n.updateAmount());
+ b.existingBuildings.subscribe(() => n.updateAmount());
+ b.amount.subscribe(() => n.updateAmount());
+ b.extraAmount.subscribe(() => n.updateAmount());
+ if (b.palaceBuff)
+ b.palaceBuffChecked.subscribe(() => n.updateAmount());
+ this.buildingMaterialsNeeds.push(n);
+
+ if (localStorage) {
+ let oldId = b.guid + ".buildings";
+ let id = b.guid + ".existingBuildings"
+ if (localStorage.getItem(id) != null || localStorage.getItem(oldId))
+ b.existingBuildings(parseInt(localStorage.getItem(id) || localStorage.getItem(oldId)));
+
+ b.existingBuildings.subscribe(val => localStorage.setItem(id, val));
+ }
+
+ n.updateAmount();
+ }
+ }
+ }
+
+ for (let upgrade of (params.goodConsumptionUpgrades || [])) {
+ let u = new GoodConsumptionUpgrade(upgrade, assetsMap, this.populationLevels);
+ if (!u.populationLevels.length)
+ continue;
+
+ assetsMap.set(u.guid, u);
+ this.allGoodConsumptionUpgrades.upgrades.push(u);
+
+ if (localStorage) {
+ let id = u.guid + ".checked";
+ if (localStorage.getItem(id) != null)
+ u.checked(parseInt(localStorage.getItem(id)));
+
+ u.checked.subscribe(val => localStorage.setItem(id, val ? 1 : 0));
+ }
+ }
+
+ for (let level of this.populationLevels)
+ for (let need of level.needs) {
+ this.allGoodConsumptionUpgrades.lists.push(need.goodConsumptionUpgradeList);
+ }
+
+
+ // negative extra amount must be set after the demands of the population are generated
+ // otherwise it would be set to zero
+ for (let f of this.factories) {
+
+ if (localStorage) {
+ {
+ let id = f.guid + ".extraAmount";
+ if (localStorage.getItem(id) != null) {
+ f.extraAmount(parseFloat(localStorage.getItem(id)));
+ }
+
+ f.extraAmount.subscribe(val => {
+ val = parseFloat(val);
+
+ if (val == null || !isFinite(val) || isNaN(val)) {
+ f.extraAmount(parseFloat(localStorage.getItem(id)) || 0);
+ return;
+ }
+ localStorage.setItem(id, val);
+ });
+ }
+
+ {
+ let id = f.guid + ".extraGoodProductionList.checked";
+ if (localStorage.getItem(id) != null) {
+ f.extraGoodProductionList.checked(parseInt(localStorage.getItem(id)));
+ }
+
+ f.extraGoodProductionList.checked.subscribe(val => localStorage.setItem(id, val ? 1 : 0))
+ }
+
+
+ } else {
+ f.extraAmount.subscribe(val => {
+ if (val == null || !isFinite(val) || isNaN(val)) {
+ f.extraAmount(0);
+ }
+ });
+ }
+ }
+
+ // force update once all pending notifications are processed
+ setTimeout(() => { this.buildingMaterialsNeeds.forEach(b => b.updateAmount()) }, 1000);
+
+ this.workforce = this.workforce.filter(w => w.demands.length);
+
+ this.assetsMap = assetsMap;
+ this.products = products;
+
+
+ this.top2Population = ko.computed(() => {
+ var useHouses = view.settings.existingBuildingsInput.checked();
+ var comp = useHouses
+ ? (a, b) => b.existingBuildings() - a.existingBuildings()
+ : (a, b) => b.amount() - a.amount();
+
+ return [...this.populationLevels].sort(comp).slice(0, 2).filter(l => useHouses ? l.existingBuildings() : l.amount());
+ });
+
+ this.top5Factories = ko.computed(() => {
+ var useBuildings = view.settings.missingBuildingsHighlight.checked();
+ var comp = useBuildings
+ ? (a, b) => b.existingBuildings() - a.existingBuildings()
+ : (a, b) => b.buildings() - a.buildings();
+
+ return [...this.factories].sort(comp).slice(0, 5).filter(f => useBuildings ? f.existingBuildings() : f.buildings());
+ });
+ }
+
+ reset() {
+ {
+ var deletedRoutes = view.tradeManager.routes().filter(r => r.to === this || r.from === this);
+ deletedRoutes.forEach(r => view.tradeManager.remove(r));
+ }
+
+ {
+ var deletedRoutes = view.tradeManager.npcRoutes().filter(r => r.to === this);
+ deletedRoutes.forEach(r => view.tradeManager.remove(r));
+ }
+
+ this.assetsMap.forEach(a => {
+ if (a instanceof Option)
+ a.checked(false);
+ if (a instanceof Product)
+ a.fixedFactory(null);
+ if (a instanceof Consumer)
+ a.existingBuildings(0);
+ if (a instanceof Factory) {
+ if (a.moduleChecked)
+ a.moduleChecked(false);
+ if (a.palaceBuffChecked)
+ a.palaceBuffChecked(false);
+ a.percentBoost(100);
+ a.extraAmount(0);
+ a.extraGoodProductionList.checked(true);
+ }
+
+ if (a instanceof PopulationLevel) {
+ a.existingBuildings(0);
+ a.amount(0);
+ }
+ if (a instanceof Item) {
+ a.checked(false);
+ for (var i of a.equipments)
+ i.checked(false);
+ }
+
+ if (a.guid == 1010240)
+ a.fixedFactory(this.assetsMap.get(1010318));
+ });
+
+ this.populationLevels.forEach(l => l.needs.forEach(n => {
+ if (n.checked)
+ n.checked(true);
+ if (n.percentBoost)
+ n.percentBoost(100);
+ }));
+ }
+}
+
+class Consumer extends NamedElement {
+ constructor(config, assetsMap, island) {
+ super(config);
+
+ this.island = island;
+
+ if (config.region)
+ this.region = assetsMap.get(config.region);
+
+ this.amount = ko.observable(0);
+ this.boost = ko.observable(1);
+
+ this.editable = ko.observable(false);
+
+ this.demands = new Set();
+ this.buildings = ko.computed(() => Math.max(0, parseFloat(this.amount())) / this.tpmin);
+ this.existingBuildings = createIntInput(0);
+ this.items = [];
+
+ this.outputAmount = ko.computed(() => this.amount());
+
+ this.workforceDemand = this.getWorkforceDemand(assetsMap);
+ this.existingBuildings.subscribe(val => this.workforceDemand.updateAmount(Math.max(val, this.buildings())));
+ this.buildings.subscribe(val => this.workforceDemand.updateAmount(Math.max(val, this.buildings())));
+
+ this.tradeList = new TradeList(island, this);
+ }
+
+ getInputs() {
+ return this.inputs || [];
+ }
+
+
+ referenceProducts(assetsMap) {
+ this.getInputs().forEach(i => i.product = assetsMap.get(i.Product));
+ }
+
+
+ getWorkforceDemand(assetsMap) {
+ for (let m of this.maintenances || []) {
+ let a = assetsMap.get(m.Product);
+ if (a instanceof Workforce)
+ return new WorkforceDemand($.extend({ factory: this, workforce: a }, m), assetsMap);
+ }
+ return { updateAmount: () => { } };
+ }
+
+ getRegionExtendedName() {
+ if (!this.region || !this.product || this.product.factories.length <= 1)
+ return this.name;
+
+ return `${this.name()} (${this.region.name()})`;
+ }
+
+ getIcon() {
+ return this.icon;
+ }
+
+ updateAmount() {
+ var sum = 0;
+ this.demands.forEach(d => {
+ var a = d.amount();
+ // if (a <= -ACCURACY || a > 0)
+ sum += a;
+ });
+
+ if (this.extraDemand && sum + this.extraDemand.amount() < -ACCURACY) {
+ if (sum < 0) {
+ this.extraDemand.updateAmount(0);
+ this.amount(0);
+ } else {
+
+ this.extraDemand.updateAmount(-sum);
+ }
+ }
+ else {
+ var val = Math.max(0, sum);
+ if (val < 1e-16)
+ val = 0;
+ this.amount(val);
+ }
+
+ }
+
+
+ add(demand) {
+ this.demands.add(demand);
+ this.updateAmount();
+ }
+
+ remove(demand) {
+ this.demands.delete(demand);
+ this.updateAmount();
+ }
+
+}
+
+class Module extends Consumer {
+ constructor(config, assetsMap) {
+ super(config, assetsMap);
+ this.checked = ko.observable(false);
+ this.visible = !!config;
+ }
+}
+
+class PalaceBuff extends NamedElement {
+ constructor(config, assetsMap) {
+ super(config, assetsMap);
}
}
-class Option extends NamedElement {
- constructor(config) {
- super(config);
- this.checked = ko.observable(false);
- this.visible = !!config;
+class Factory extends Consumer {
+ constructor(config, assetsMap, island) {
+ super(config, assetsMap, island);
+
+ this.extraAmount = createFloatInput(0);
+ this.extraGoodProductionList = new ExtraGoodProductionList(this);
+
+ this.percentBoost = createIntInput(100);
+ this.boost = ko.computed(() => parseInt(this.percentBoost()) / 100);
+
+ if (this.module) {
+ this.module = assetsMap.get(this.module);
+ this.moduleChecked = ko.observable(false);
+ this.moduleChecked.subscribe(checked => {
+ if (checked)
+ this.percentBoost(parseInt(this.percentBoost()) + this.module.productivityUpgrade);
+ else {
+ var val = Math.max(1, parseInt(this.percentBoost()) - this.module.productivityUpgrade);
+ this.percentBoost(val);
+ }
+ });
+ //moduleDemand created in island constructor after referencing products
+ }
+
+ if (config.palaceBuff) {
+ this.palaceBuff = assetsMap.get(config.palaceBuff);
+ this.palaceBuffChecked = ko.observable(false);
+ }
+
+ this.extraGoodFactor = ko.computed(() => {
+ var factor = 1;
+ if (this.module && this.moduleChecked() && this.module.additionalOutputCycle)
+ factor += 1 / this.module.additionalOutputCycle;
+
+ if (this.palaceBuff && this.palaceBuffChecked())
+ factor += 1 / this.palaceBuff.additionalOutputCycle;
+
+ if (this.extraGoodProductionList && this.extraGoodProductionList.selfEffecting && this.extraGoodProductionList.checked())
+ for (var e of this.extraGoodProductionList.selfEffecting())
+ if (e.item.checked())
+ factor += (e.Amount || 1) / e.additionalOutputCycle;
+
+ return factor;
+ });
+
+ this.requiredOutputAmount = ko.computed(() => {
+ var amount = Math.max(0, parseFloat(this.amount() + parseFloat(this.extraAmount())));
+ return amount / this.extraGoodFactor();
+ });
+
+ this.producedOutputAmount = ko.computed(() => {
+ return parseInt(this.existingBuildings()) * this.boost() * this.tpmin;
+ });
+
+ this.outputAmount = ko.computed(() => Math.max(this.requiredOutputAmount(), this.producedOutputAmount()));
+
+ this.buildings = ko.computed(() => {
+ var buildings = this.requiredOutputAmount() / this.tpmin / this.boost();
+
+ if (this.moduleDemand)
+ if (this.moduleChecked())
+ this.moduleDemand.updateAmount(Math.max(Math.ceil(buildings), this.existingBuildings()) * this.module.tpmin);
+ else
+ this.moduleDemand.updateAmount(0);
+ return buildings;
+ });
+
+ this.buildings.subscribe(val => this.workforceDemand.updateAmount(Math.max(val, this.buildings())));
+
+ this.computedExtraAmount = ko.computed(() => {
+ return (this.extraGoodProductionList.checked() ? - this.extraGoodProductionList.amount() : 0) + this.tradeList.amount();
+ });
+
+ this.computedExtraAmount.subscribe(() => {
+ if (view.settings.autoApplyExtraNeed.checked())
+ setTimeout(() => this.updateExtraGoods(), 10);
+ });
+
+ this.amount.subscribe(() => {
+ if (view.settings.autoApplyExtraNeed.checked() && this.computedExtraAmount() < 0 && this.computedExtraAmount() + ACCURACY < this.extraAmount())
+ setTimeout(() => this.updateExtraGoods(), 10);
+ });
+
+ this.overProduction = ko.computed(() => {
+ var val = 0;
+
+ if (view.settings.missingBuildingsHighlight.checked())
+ var val = this.existingBuildings() * this.boost() * this.tpmin * this.extraGoodFactor();
+
+ return val - this.amount() - this.extraAmount();
+ });
+
+ this.visible = ko.computed(() => {
+ if (Math.abs(this.amount()) > EPSILON || Math.abs(this.extraAmount()) > EPSILON || this.existingBuildings() > 0 || !this.island.isAllIslands() && Math.abs(this.extraGoodProductionList.amount()) > EPSILON || Math.abs(this.tradeList.amount()) > EPSILON)
+ return true;
+
+ if (this.region && this.island.region && this.region != this.island.region)
+ return false;
+
+ if (view.settings.showAllConstructableFactories.checked())
+ return true;
+
+ if (this.editable()) {
+ if (this.region && this.island.region)
+ return this.region === this.island.region;
+
+ if (!this.region || this.region.guid === 5000000)
+ return true;
+
+ return false;
+ }
+
+ return false;
+ });
+ }
+
+
+ getOutputs() {
+ return this.outputs || [];
+ }
+
+ referenceProducts(assetsMap) {
+ super.referenceProducts(assetsMap);
+ this.getOutputs().forEach(i => i.product = assetsMap.get(i.Product));
+
+ this.product = this.getProduct();
+ if (!this.icon)
+ this.icon = this.product.icon;
+
+ this.extraDemand = new FactoryDemand({ factory: this, guid: this.product.guid }, assetsMap);
+ this.extraAmount.subscribe(val => {
+ val = parseFloat(val);
+ if (!isFinite(val) || val == null) {
+ this.extraAmount(0);
+ return;
+ }
+
+ let amount = parseFloat(this.amount());
+ if (val < -Math.ceil(amount * 100) / 100)
+ this.extraAmount(- Math.ceil(amount * 100) / 100);
+ else
+ this.extraDemand.updateAmount(Math.max(val, -amount));
+ });
+ this.extraDemand.updateAmount(parseFloat(this.extraAmount()));
+ }
+
+ getProduct() {
+ return this.getOutputs()[0] ? this.getOutputs()[0].product : null;
+ }
+
+ getIcon() {
+ return this.getProduct() ? this.getProduct().icon : super.getIcon();
+ }
+
+ incrementBuildings() {
+ if (this.buildings() <= 0 || parseInt(this.percentBoost()) <= 1)
+ return;
+
+ var minBuildings = Math.ceil(this.buildings() * parseInt(this.percentBoost()) / (parseInt(this.percentBoost()) - 1));
+ let nextBoost = Math.ceil(parseInt(this.percentBoost()) * this.buildings() / minBuildings)
+ this.percentBoost(Math.min(nextBoost, parseInt(this.percentBoost()) - 1));
+ }
+
+ decrementBuildings() {
+ let currentBuildings = Math.ceil(this.buildings() * 100) / 100;
+ var nextBuildings = Math.floor(currentBuildings);
+ if (nextBuildings <= 0)
+ return;
+
+ if (currentBuildings - nextBuildings < 0.01)
+ nextBuildings = Math.floor(nextBuildings - 0.01);
+ var nextBoost = Math.ceil(100 * this.boost() * this.buildings() / nextBuildings);
+ if (nextBoost - parseInt(this.percentBoost()) < 1)
+ nextBoost = parseInt(this.percentBoost()) + 1;
+ this.percentBoost(nextBoost);
+ }
+
+ incrementPercentBoost() {
+ this.percentBoost(parseInt(this.percentBoost()) + 1);
+ }
+
+ decrementPercentBoost() {
+ this.percentBoost(parseInt(this.percentBoost()) - 1);
+ }
+
+ updateExtraGoods(depth) {
+ var val = this.computedExtraAmount();
+ var amount = this.amount();
+ if (val < -Math.ceil(amount * 100) / 100)
+ val = - Math.ceil(amount * 100) / 100;
+
+ if (Math.abs(val - this.extraAmount()) < ACCURACY)
+ return;
+
+ this.extraAmount(val);
+
+ if (depth > 0)
+ for (var route of this.tradeList.routes()) {
+ route.getOppositeFactory(this).updateExtraGoods(depth - 1);
+ }
+ }
+
+ applyConfigGlobally() {
+ for (var isl of view.islands()) {
+ var other = isl.assetsMap.get(this.guid);
+
+ for (var i = 0; i < this.items.length; i++)
+ other.items[i].checked(this.items[i].checked());
+
+ other.percentBoost(this.percentBoost());
+
+ if (this.moduleChecked)
+ other.moduleChecked(this.moduleChecked());
+
+ if (this.palaceBuffChecked)
+ other.palaceBuffChecked(this.palaceBuffChecked());
+ }
+ }
+}
+
+class Product extends NamedElement {
+ constructor(config, assetsMap) {
+ super(config);
+
+
+ this.amount = ko.observable(0);
+
+ this.factories = this.producers.map(p => assetsMap.get(p));
+ this.fixedFactory = ko.observable(null);
+
+ if (this.producers) {
+ this.amount = ko.computed(() => this.factories.map(f => f.amount()).reduce((a, b) => a + b));
+ }
+ }
+}
+
+class Demand extends NamedElement {
+ constructor(config, assetsMap) {
+ super(config);
+
+ this.amount = ko.observable(0);
+
+
+ this.product = assetsMap.get(this.guid);
+ if (!this.product)
+ throw `No Product ${this.guid}`;
+ this.factory = ko.observable(config.factory);
+
+ if (this.product) {
+ this.updateFixedProductFactory(this.product.fixedFactory());
+ this.product.fixedFactory.subscribe(f => this.updateFixedProductFactory(f));
+
+ this.inputAmount = ko.computed(() => {
+ var amount = parseFloat(this.amount());
+
+ var factor = 1;
+
+ if (this.factory() && this.factory().extraGoodFactor)
+ factor = this.factory().extraGoodFactor();
+
+ return amount / factor;
+
+ });
+
+ if (this.consumer)
+ this.consumer.factory.subscribe(() => this.updateFixedProductFactory(this.product.fixedFactory()));
+
+ if (this.product.differentFactoryInputs) {
+ this.demands = [new FactoryDemandSwitch(this, assetsMap)];
+ this.inputAmount.subscribe(val => this.demands[0].updateAmount(val));
+ }
+ else
+ this.demands = this.factory().getInputs().map(input => {
+ var d;
+ let items = this.factory().items.filter(item => item.replacements && item.replacements.has(input.Product));
+ if (items.length)
+ d = new ItemDemandSwitch(this, input, items, assetsMap);
+ else
+ d = new Demand({ guid: input.Product, consumer: this }, assetsMap);
+
+ this.inputAmount.subscribe(val => d.updateAmount(val * input.Amount));
+
+ return d;
+ });
+
+
+ this.amount.subscribe(val => {
+ this.factory().updateAmount();
+ });
+
+ this.buildings = ko.computed(() => {
+ var factory = this.factory();
+ var buildings = Math.max(0, this.inputAmount()) / factory.tpmin / factory.boost();
+
+ return buildings;
+ });
+ }
+ }
+
+ updateFixedProductFactory(f) {
+ if (f == null) {
+ if (this.consumer || this.region) { // find factory in the same region as consumer
+ let region = this.region || this.consumer.factory().region;
+ if (region) {
+ for (let fac of this.product.factories) {
+ if (fac.region === region) {
+ f = fac;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if (f == null) // region based approach not successful
+ f = this.product.factories[0];
+
+ if (f != this.factory()) {
+ if (this.factory())
+ this.factory().remove(this);
+
+ this.factory(f);
+ f.add(this);
+ }
+ }
+
+ updateAmount(amount) {
+ this.amount(amount);
+ }
+}
+
+class ItemDemandSwitch {
+ constructor(consumer, input, items, assetsMap) {
+ this.items = items;
+
+ this.demands = [ // use array index to toggle
+ new Demand({ guid: input.Product, consumer: consumer }, assetsMap),
+ new Demand({ guid: items[0].replacements.get(input.Product), consumer: consumer }, assetsMap)
+ ];
+ this.amount = 0;
+
+ this.items.forEach(item => item.checked.subscribe(() => this.updateAmount(this.amount)));
+ }
+
+ updateAmount(amount) {
+ this.amount = amount;
+ this.demands.forEach((d, idx) => {
+ let checked = this.items.map(item => item.checked()).reduce((a, b) => a || b);
+ d.updateAmount(checked == idx ? amount : 0)
+ });
+ }
+
+}
+
+class FactoryDemandSwitch {
+ constructor(consumer, assetsMap) {
+ this.consumer = consumer;
+ this.factory = this.consumer.factory();
+
+ this.demands = [];
+ this.demandsMap = new Map();
+
+ for (var factory of consumer.product.factories) {
+ var factoryDemands = [];
+ for (var input of factory.getInputs()) {
+
+ var d;
+ let items = factory.items.filter(item => item.replacements && item.replacements.has(input.Product));
+ if (items.length)
+ d = new ItemDemandSwitch(consumer, input, items, assetsMap);
+ else
+ d = new Demand({ guid: input.Product, consumer: consumer }, assetsMap);
+
+ factoryDemands.push(d);
+ this.demands.push(d);
+ }
+
+ this.demandsMap.set(factory, factoryDemands);
+
+ }
+
+ this.amount = 0;
+
+ consumer.factory.subscribe(factory => this.updateAmount(this.amount));
+ }
+
+ updateAmount(amount) {
+ this.amount = amount;
+ var factory = this.consumer.factory();
+
+ if (factory.module && factory.moduleChecked() && factory.module.additionalOutputCycle)
+ amount *= factory.module.additionalOutputCycle / (factory.module.additionalOutputCycle + 1);
+
+ if (factory != this.factory) {
+ for (var d of this.demandsMap.get(this.factory)) {
+ d.updateAmount(0);
+ }
+ }
+
+ this.factory = factory;
+
+
+ for (var d of this.demandsMap.get(factory)) {
+ d.updateAmount(amount);
+ }
+
+ }
+
+}
+
+class FactoryDemand extends Demand {
+ constructor(config, assetsMap) {
+ super(config, assetsMap);
+ this.factory(config.factory);
+ }
+
+ updateFixedProductFactory() {
+ }
+}
+
+class Need extends Demand {
+ constructor(config, assetsMap) {
+ super(config, assetsMap);
+ this.allDemands = [];
+
+ let treeTraversal = node => {
+ if (node instanceof Demand && !(node instanceof Need))
+ this.allDemands.push(node);
+ (node.demands || []).forEach(treeTraversal);
+ }
+ treeTraversal(this);
+ }
+
+}
+
+class PopulationNeed extends Need {
+ constructor(config, assetsMap) {
+ super(config, assetsMap);
+
+ this.residents = 0;
+ this.goodConsumptionUpgradeList = new GoodConsumptionUpgradeList(this);
+
+ this.percentBoost = createFloatInput(100);
+ this.percentBoost.subscribe(val => {
+ val = parseFloat(val);
+ if (val <= 0)
+ this.percentBoost(1);
+ })
+ this.boost = ko.computed(() => parseInt(this.percentBoost()) / 100);
+ this.boost.subscribe(() => this.updateAmount(this.residents));
+
+ this.checked = ko.observable(true);
+ this.banned = ko.computed(() => {
+ var checked = this.checked();
+ return !checked;
+ })
+ this.optionalAmount = ko.observable(0);
+
+ this.banned.subscribe(banned => {
+ if (banned)
+ this.amount(0);
+ else
+ this.amount(this.optionalAmount());
+ });
+ }
+
+ updateAmount(residents) {
+ this.residents = residents;
+ this.optionalAmount(this.tpmin * residents * this.boost());
+ if (!this.banned())
+ this.amount(this.optionalAmount());
+ }
+
+ incrementPercentBoost() {
+ this.percentBoost(parseInt(this.percentBoost()) + 1);
+ }
+
+ decrementPercentBoost() {
+ this.percentBoost(parseInt(this.percentBoost()) - 1);
+ }
+}
+
+class BuildingMaterialsNeed extends Need {
+ constructor(config, assetsMap) {
+ super(config, assetsMap);
+
+ this.product = config.product;
+ this.factory(config.factory);
+
+ this.factory().add(this);
+ }
+
+ updateAmount() {
+ var otherDemand = 0;
+ this.factory().demands.forEach(d => otherDemand += d == this ? 0 : d.amount());
+
+ var existingBuildingsOutput =
+ this.factory().existingBuildings() * this.factory().tpmin * this.factory().boost();
+
+ if (this.factory().palaceBuff && this.factory().palaceBuffChecked())
+ existingBuildingsOutput *= 1 + 1 / this.factory().palaceBuff.additionalOutputCycle;
+
+ var overProduction = existingBuildingsOutput - otherDemand;
+ this.amount(Math.max(0, overProduction - EPSILON));
+ }
+
+ updateFixedProductFactory() { }
+}
+
+class PowerPlantNeed extends Need {
+ constructor(config, assetsMap) {
+ super(config, assetsMap);
+
+ this.factory(config.factory);
+ this.factory().add(this);
+ }
+
+ updateAmount() {
+ this.amount(this.factory().existingBuildings() * this.factory().tpmin);
+ }
+
+ updateFixedProductFactory() { }
+}
+
+class ResidenceBuilding extends NamedElement {
+ constructor(config, assetsMap) {
+ super(config);
+ }
+}
+
+class PopulationLevel extends NamedElement {
+ constructor(config, assetsMap) {
+ super(config);
+
+ this.hotkey = ko.observable(null);
+ this.amount = createIntInput(0);
+ this.existingBuildings = createIntInput(0);
+ this.noOptionalNeeds = ko.observable(false);
+ this.needs = [];
+ this.region = assetsMap.get(config.region);
+
+ config.needs.forEach(n => {
+ if (n.tpmin > 0 && assetsMap.get(n.guid))
+ this.needs.push(new PopulationNeed(n, assetsMap));
+ });
+
+ this.amount.subscribe(val => {
+ if (val < 0)
+ this.amount(0);
+ else if (!view.settings.existingBuildingsInput.checked())
+ this.needs.forEach(n => n.updateAmount(parseInt(val)))
+ });
+ this.existingBuildings.subscribe(val => {
+ if (view.settings.existingBuildingsInput.checked())
+ this.needs.forEach(n => n.updateAmount(parseInt(val * config.fullHouse)))
+ });
+ view.settings.existingBuildingsInput.checked.subscribe(enabled => {
+ if (enabled)
+ this.existingBuildings(Math.max(this.existingBuildings(),
+ Math.ceil(parseInt(this.amount()) / config.fullHouse)));
+ else
+ this.amount(Math.max(this.amount(), parseInt(this.existingBuildings()) / (config.fullHouse - 10)));
+ });
+
+ if (this.residence) {
+ this.residence = assetsMap.get(this.residence);
+ this.residence.populationLevel = this;
+ }
+ }
+
+ incrementAmount() {
+ this.amount(parseFloat(this.amount()) + 1);
+ }
+
+ decrementAmount() {
+ this.amount(parseFloat(this.amount()) - 1);
+ }
+}
+
+
+
+class ProductCategory extends NamedElement {
+ constructor(config, assetsMap) {
+ super(config);
+ this.products = config.products.map(p => assetsMap.get(p)).filter(p => p != null);
+ this.consumers = [];
+ }
+}
+
+class Workforce extends NamedElement {
+ constructor(config, assetsMap) {
+ super(config);
+ this.amount = ko.observable(0);
+ this.demands = [];
+ }
+
+ updateAmount() {
+ var sum = 0;
+ this.demands.forEach(d => sum += d.amount());
+ this.amount(sum);
+ }
+
+ add(demand) {
+ this.demands.push(demand);
+ }
+}
+
+class WorkforceDemand extends NamedElement {
+ constructor(config, assetsMap) {
+ super(config);
+ this.amount = ko.observable(0);
+ this.workforce.add(this);
+ this.amount.subscribe(val => this.workforce.updateAmount());
+ }
+
+ updateAmount(buildings) {
+ this.amount(Math.ceil(buildings) * this.Amount);
+ }
+}
+
+class Item extends NamedElement {
+ constructor(config, assetsMap, region) {
+ super(config);
+
+ if (this.replaceInputs) {
+ this.replacements = new Map();
+ this.replacementArray = [];
+
+
+ this.replaceInputs.forEach(r => {
+ this.replacementArray.push({
+ old: assetsMap.get(r.OldInput),
+ new: assetsMap.get(r.NewInput)
+ });
+ this.replacements.set(r.OldInput, r.NewInput);
+ });
+ }
+
+ if (this.additionalOutputs) {
+ this.extraGoods = this.additionalOutputs.map(p => assetsMap.get(p.Product));
+ }
+
+ this.factories = this.factories.map(f => assetsMap.get(f));
+ this.equipments =
+ this.factories.map(f => new EquippedItem({ item: this, factory: f, locaText: this.locaText }, assetsMap));
+
+ this.checked = ko.pureComputed({
+ read: () => {
+ for (var eq of this.equipments)
+ if (!eq.checked())
+ return false;
+
+ return true;
+ },
+ write: (checked) => {
+ this.equipments.forEach(e => e.checked(checked));
+ }
+
+ });
+
+ this.visible = ko.computed(() => {
+ if (!view.island || !view.island())
+ return true;
+
+ var region = view.island().region;
+ if (!region)
+ return true;
+
+ for (var f of this.factories)
+ if (f.region === region)
+ return true;
+
+ return false;
+ });
+ }
+}
+
+class EquippedItem extends Option {
+ constructor(config, assetsMap) {
+ super(config);
+
+ this.replacements = config.item.replacements;
+ this.replacementArray = config.item.replacementArray;
+
+ if (config.item.additionalOutputs) {
+ this.extraGoods = config.item.additionalOutputs.map(cfg => {
+ var config = $.extend(true, {}, cfg, { item: this, factory: this.factory });
+ return new ExtraGoodProduction(config, assetsMap);
+ })
+ }
+
+ this.factory.items.push(this);
+ }
+}
+
+class ExtraGoodProduction {
+ constructor(config, assetsMap) {
+ this.item = config.item;
+ this.factory = config.factory;
+
+ this.product = assetsMap.get(config.Product);
+ this.additionalOutputCycle = config.AdditionalOutputCycle;
+ this.Amount = config.Amount;
+
+ this.amount = ko.computed(() => !!this.item.checked() * config.Amount * this.factory.outputAmount() / this.additionalOutputCycle);
+
+ for (var f of this.product.factories) {
+ f.extraGoodProductionList.entries.push(this);
+
+ if (f == this.factory)
+ f.extraGoodProductionList.selfEffecting.push(this);
+ }
+ }
+}
+
+class ExtraGoodProductionList {
+ constructor(factory) {
+ this.factory = factory;
+
+ this.checked = ko.observable(true);
+ this.selfEffecting = ko.observableArray();
+
+ this.entries = ko.observableArray();
+ this.nonZero = ko.computed(() => {
+ return this.entries().filter(i => i.amount());
+ });
+ this.amount = ko.computed(() => {
+ var total = 0;
+ for (var i of (this.entries() || []))
+ if (this.selfEffecting.indexOf(i) == -1) // self effects considered in factory.extraGoodFactor
+ total += i.amount();
+
+ return total;
+ });
+ this.amountWithSelf = ko.computed(() => {
+ var total = 0;
+ for (var i of (this.entries() || []))
+ total += i.amount();
+
+ return total;
+ })
+ }
+}
+
+class GoodConsumptionUpgrade extends Option {
+ constructor(config, assetsMap, levels) {
+ super(config, assetsMap);
+
+ this.entries = [];
+ this.entriesMap = new Map();
+ this.populationLevels = config.populationLevels.map(l => assetsMap.get(l)).filter(l => !!l);
+ if (!this.populationLevels.length)
+ return;
+
+ this.populationLevelsSet = new Set(this.populationLevels);
+
+ for (var entry of config.goodConsumptionUpgrade) {
+ if (entry.AmountInPercent <= -100)
+ continue;
+
+ this.entries.push(new GoodConsumptionUpgradeEntry($.extend({ upgrade: this }, entry), assetsMap));
+ this.entriesMap.set(entry.ProvidedNeed, this.entries[this.entries.length - 1]);
+ }
+
+ for (var level of levels) {
+ if (!this.populationLevelsSet.has(level))
+ continue;
+
+ for (var need of level.needs) {
+ var entry = this.entriesMap.get(need.product.guid);
+ if (entry)
+ need.goodConsumptionUpgradeList.add(entry);
+ }
+ }
+
+ this.visible = ko.computed(() => {
+ if (!view.island || !view.island())
+ return true;
+
+ var region = view.island().region;
+ if (!region)
+ return true;
+
+ for (var l of this.populationLevels)
+ if (l.region === region)
+ return true;
+
+ return false;
+ });
+ }
+}
+
+class NewspaperNeedConsumption {
+ constructor() {
+ this.selectedEffects = ko.observableArray();
+ this.allEffects = [];
+ this.amount = ko.observable(100);
+ this.selectedBuff = ko.observable(0);
+ this.selectableBuffs = ko.observableArray();
+
+ this.updateBuff();
+
+ this.selectedEffects.subscribe(() => this.updateBuff());
+
+ this.selectedEffects.subscribe(() => {
+ if (this.selectedEffects().length > 3)
+ this.selectedEffects.splice(0, 1)[0].checked(false);
+ });
+
+ this.amount = ko.computed(() => {
+ var sum = 0;
+ for (var effect of this.selectedEffects()) {
+ sum += Math.ceil(effect.amount * (1 + parseInt(this.selectedBuff()) / 100));
+ }
+
+ return sum;
+ });
+ }
+
+ add(effect) {
+ this.allEffects.push(effect);
+ effect.checked.subscribe(checked => {
+ var idx = this.selectedEffects.indexOf(effect);
+ if (checked && idx != -1 || !checked && idx == -1)
+ return;
+
+ if (checked)
+ this.selectedEffects.push(effect);
+ else
+ this.selectedEffects.remove(effect);
+ });
+ }
+
+ updateBuff() {
+ var influenceCosts = 0;
+ for (var effect of this.selectedEffects()) {
+ influenceCosts += effect.influenceCosts;
+ }
+
+ var threeSelected = this.selectedEffects().length >= 3;
+ var selectedBuff = this.selectedBuff();
+
+ this.selectableBuffs.removeAll();
+ if (influenceCosts < 50)
+ this.selectableBuffs.push(0);
+ if (influenceCosts < 150 && (!threeSelected || !this.selectableBuffs().length))
+ this.selectableBuffs.push(7);
+ if (influenceCosts < 300 && (!threeSelected || !this.selectableBuffs().length))
+ this.selectableBuffs.push(15);
+ if (!threeSelected || !this.selectableBuffs().length)
+ this.selectableBuffs.push(25);
+
+ if (this.selectableBuffs.indexOf(selectedBuff) == -1)
+ this.selectedBuff(this.selectableBuffs()[0]);
+ else
+ this.selectedBuff(selectedBuff);
+ }
+
+ apply() {
+ for (var island of view.islands()) {
+ island.allGoodConsumptionUpgrades.apply();
+ }
+ }
+}
+
+class NewspaperNeedConsumptionEntry extends Option {
+ constructor(config) {
+ super(config);
+
+ this.amount = config.articleEffects[0].ArticleValue;
+ }
+}
+
+class GoodConsumptionUpgradeEntry {
+ constructor(config, assetsMap) {
+ this.upgrade = config.upgrade;
+ this.product = assetsMap.get(config.ProvidedNeed);
+ this.amount = config.AmountInPercent;
+ }
+}
+
+class GoodConsumptionUpgradeList {
+ constructor(need) {
+ this.upgrades = [];
+ this.amount = ko.observable(100);
+ this.need = need;
+
+ this.updateAmount();
+ view.newspaperConsumption.amount.subscribe(() => this.updateAmount());
+ }
+
+ add(upgrade) {
+ this.upgrades.push(upgrade);
+ upgrade.upgrade.checked.subscribe(() => this.updateAmount());
+ }
+
+ updateAmount() {
+ var factor = (100 + view.newspaperConsumption.amount()) / 100;
+
+ var remainingSupply = 100;
+ for (var entry of this.upgrades) {
+ if (entry.upgrade.checked())
+ remainingSupply += entry.amount;
+ }
+
+ this.amount(remainingSupply * (100 + view.newspaperConsumption.amount()) / 100);
+ }
+
+ apply() {
+ this.need.percentBoost(this.amount());
+ }
+}
+
+class GoodConsumptionUpgradeIslandList {
+ constructor() {
+ this.lists = [];
+ this.upgrades = [];
+ }
+
+ apply() {
+ for (var list of this.lists) {
+ list.apply();
+ }
+ }
+}
+
+class TradeRoute {
+ constructor(config) {
+ $.extend(this, config);
+
+ this.amount = createFloatInput(0);
+ this.amount(config.amount);
+ }
+
+ getOpposite(list) {
+ if (list.island == this.from)
+ return this.to;
+ else
+ return this.from;
+ }
+
+ getOppositeFactory(factory) {
+ if (this.fromFactory == factory)
+ return this.toFactory;
+ else
+ return this.fromFactory;
+ }
+
+ isExport(list) {
+ return list.island == this.from;
+ }
+
+ delete() {
+ view.tradeManager.remove(this);
+ }
+}
+
+class NPCTrader extends NamedElement {
+ constructor(config) {
+ super(config);
+ }
+}
+
+class NPCTradeRoute {
+ constructor(config) {
+ $.extend(this, config);
+
+ this.amount = this.ProductionPerMinute;
+ this.checked = ko.observable(false);
+ this.checked.subscribe(checked => {
+ if (view.tradeManager) {
+ if (checked)
+ view.tradeManager.npcRoutes.push(this);
+ else
+ view.tradeManager.npcRoutes.remove(this);
+ }
+ });
+ }
+}
+
+class TradeList {
+ constructor(island, factory) {
+ this.island = island;
+ this.factory = factory;
+
+ this.routes = ko.observableArray();
+ if (this.factory.outputs) {
+ var traders = view.productsToTraders.get(this.factory.outputs[0].Product);
+ if (traders)
+ this.npcRoutes = traders.map(t => new NPCTradeRoute($.extend({}, t, { to: island, toFactory: factory })));
+ }
+
+ this.amount = ko.computed(() => {
+ var amount = 0;
+
+ for (var route of (this.npcRoutes || [])) {
+ amount -= route.checked() ? route.amount : 0;
+ }
+
+ for (var route of this.routes()) {
+ amount += (route.isExport(this) ? 1 : -1) * route.amount();
+ }
+
+ return amount;
+ });
+
+ // interface elements to create a new route
+ this.unusedIslands = ko.observableArray();
+ this.selectedIsland = ko.observable();
+ this.export = ko.observable(false);
+ this.newAmount = ko.observable(0);
+ }
+
+ canCreate() {
+ return this.selectedIsland() && !this.selectedIsland().isAllIslands() && this.newAmount();
+ }
+
+ create() {
+ if (!this.canCreate())
+ return;
+
+ var otherFactory;
+ for (var f of this.selectedIsland().factories)
+ if (f.guid == this.factory.guid) {
+ otherFactory = f;
+ break;
+ }
+
+ if (!otherFactory)
+ return;
+
+ if (this.export()) {
+ var route = new TradeRoute({
+ from: this.island,
+ to: this.selectedIsland(),
+ fromFactory: this.factory,
+ toFactory: otherFactory,
+ amount: this.newAmount()
+ });
+ } else {
+ var route = new TradeRoute({
+ to: this.island,
+ from: this.selectedIsland(),
+ toFactory: this.factory,
+ fromFactory: otherFactory,
+ amount: this.newAmount()
+ });
+ }
+
+ this.routes.push(route);
+ this.unusedIslands.remove(this.selectedIsland());
+ otherFactory.tradeList.routes.push(route);
+
+ view.tradeManager.add(route);
+ }
+
+ onShow() {
+ var usedIslands = new Set(this.routes().flatMap(r => [r.from, r.to]));
+ var islands = view.islands().slice(1).filter(i => !usedIslands.has(i) && i != this.island);
+ islands.sort((a, b) => {
+ var sIdxA = view.sessions.indexOf(a.session);
+ var sIdxB = view.sessions.indexOf(b.session);
+
+ if (sIdxA == sIdxB) {
+ return a.name() > b.name();
+ } else {
+ return sIdxA > sIdxB;
+ }
+ });
+ var overProduction = this.factory.overProduction();
+ if (overProduction == 0)
+ overProduction = -this.factory.computedExtraAmount();
+ this.export(overProduction > 0);
+ this.newAmount(Math.abs(overProduction));
+
+ this.unusedIslands(islands);
+ }
+}
+
+class TradeManager {
+ constructor() {
+ this.key = "tradeRoutes";
+ this.npcKey = "npcTradeRoutes";
+ this.npcRoutes = ko.observableArray();
+ this.routes = ko.observableArray();
+
+ view.selectedFactory.subscribe(f => {
+ if (!(f instanceof Factory))
+ return;
+
+ if (f.tradeList)
+ f.tradeList.onShow();
+ });
+
+
+
+ if (localStorage) {
+ // trade routes
+ var islands = new Map();
+ for (var i of view.islands())
+ if (!i.isAllIslands())
+ islands.set(i.name(), i);
+
+ var resolve = name => name == ALL_ISLANDS ? view.islandManager.allIslands : islands.get(name);
+
+ var text = localStorage.getItem(this.key);
+ var json = text ? JSON.parse(text) : [];
+ for (var r of json) {
+ var config = {
+ from: resolve(r.from),
+ to: resolve(r.to),
+ amount: parseFloat(r.amount)
+ };
+
+ if (!config.from || !config.to)
+ continue;
+
+ config.fromFactory = config.from.assetsMap.get(r.factory);
+ config.toFactory = config.to.assetsMap.get(r.factory);
+
+ if (!config.fromFactory || !config.toFactory)
+ continue;
+
+ var route = new TradeRoute(config);
+ this.routes.push(route);
+ config.fromFactory.tradeList.routes.push(route);
+ config.toFactory.tradeList.routes.push(route);
+ }
+
+
+ this.persistenceSubscription = ko.computed(() => {
+ var json = [];
+
+ for (var r of this.routes()) {
+ json.push({
+ from: r.from.isAllIslands() ? ALL_ISLANDS : r.from.name(),
+ to: r.to.isAllIslands() ? ALL_ISLANDS : r.to.name(),
+ factory: r.fromFactory.guid,
+ amount: r.amount()
+ });
+ }
+
+ localStorage.setItem(this.key, JSON.stringify(json, null, 4));
+
+ return json;
+ });
+
+ // npc trade routes
+ text = localStorage.getItem(this.npcKey);
+ json = text ? JSON.parse(text) : [];
+ for (var r of json) {
+ var to = resolve(r.to);
+
+ if (!to)
+ continue;
+
+ var factory = to.assetsMap.get(r.factory);
+ if (!factory)
+ continue;
+
+ factory.tradeList.npcRoutes.forEach(froute => {
+ if (froute.trader.guid === r.trader) {
+ froute.checked(true);
+ this.add(froute);
+ }
+ });
+ }
+
+
+ this.npcPersistenceSubscription = ko.computed(() => {
+ var json = [];
+
+ for (var r of this.npcRoutes()) {
+ json.push({
+ trader: r.trader.guid,
+ to: r.to.isAllIslands() ? ALL_ISLANDS : r.to.name(),
+ factory: r.toFactory.guid
+ });
+ }
+
+ localStorage.setItem(this.npcKey, JSON.stringify(json, null, 4));
+
+ return json;
+ });
+ }
+ }
+
+ add(route) {
+ if (route instanceof NPCTradeRoute)
+ this.npcRoutes.push(route);
+ else
+ this.routes.push(route);
+ }
+
+ remove(route) {
+ if (route instanceof NPCTradeRoute) {
+ this.npcRoutes.remove(route);
+ route.checked(false);
+ return;
+ }
+
+ route.fromFactory.tradeList.routes.remove(route);
+ route.toFactory.tradeList.routes.remove(route);
+ this.routes.remove(route);
+
+ route.toFactory.tradeList.unusedIslands.unshift(route.from);
+ route.fromFactory.tradeList.unusedIslands.unshift(route.to);
+ }
+
+ islandDeleted(island) {
+ {
+ var deletedRoutes = this.routes().filter(r => r.to === island || r.from === island);
+ deletedRoutes.forEach(r => this.remove(r));
+ }
+
+ {
+ var deletedRoutes = this.npcRoutes().filter(r => r.to === island);
+ deletedRoutes.forEach(r => this.remove(r));
+ }
+ }
+}
+
+class ProductionChainView {
+ constructor() {
+ this.factoryToDemands = new Map();
+ this.demands = ko.computed(() => {
+ var chain = [];
+ let traverse = d => {
+ if (d.factory && d.amount) {
+ var a = ko.isObservable(d.amount) ? parseFloat(d.amount()) : parseFloat(d.amount);
+ var f = ko.isObservable(d.factory) ? d.factory() : d.factory;
+ if (Math.abs(a) < ACCURACY)
+ return;
+
+
+ if (!this.factoryToDemands.has(f)) {
+ var demandAggregate = {
+ amount: a,
+ factory: f,
+ demands: [d]
+ }
+
+ this.factoryToDemands.set(f, demandAggregate);
+ chain.push(demandAggregate);
+ } else {
+ var aggregate = this.factoryToDemands.get(f);
+ aggregate.amount += a;
+ aggregate.demands.push(d);
+ }
+
+ }
+
+ for (var e of d.demands)
+ traverse(e);
+ }
+
+ this.factoryToDemands.clear();
+ for (var d of view.selectedFactory().demands) {
+ traverse(d);
+ }
+
+ if (view.selectedFactory().extraDemand)
+ traverse(view.selectedFactory().extraDemand);
+
+ for (var c of chain) {
+ var factor = 1;
+
+ if (c.factory.extraGoodFactor)
+ factor = c.factory.extraGoodFactor();
+
+ var inputAmount = c.amount / factor;
+ c.buildings = Math.max(0, inputAmount) / c.factory.tpmin / c.factory.boost();
+ }
+
+ return chain;
+ });
+ }
+}
+
+class PopulationReader {
+
+ constructor() {
+ this.url = 'http://localhost:8000/AnnoServer/Population';
+ this.notificationShown = false;
+ this.currentVersion;
+ this.recentVersion;
+
+ // only ping the server when the website is run locally
+ if (isLocal()) {
+ console.log('waiting for responses from ' + this.url);
+ this.requestInterval = setInterval(this.handleResponse.bind(this), 1000);
+
+ $.getJSON("https://api.github.com/repos/NiHoel/Anno1404UXEnhancer/releases/latest").done((release) => {
+ this.recentVersion = release.tag_name;
+ this.checkVersion();
+ });
+ }
+ }
+
+ async handleResponse() {
+ var url_with_params = this.url + "?" +
+ jQuery.param({
+ lang: view.settings.language(),
+ // optimalProductivity: view.settings.optimalProductivity.checked()
+ });
+
+ try {
+ const response = await fetch(url_with_params);
+ const json = await response.json(); //extract JSON from the http response
+
+ if (!json)
+ return;
+
+ if (json.version) {
+ this.currentVersion = json.version;
+ this.checkVersion();
+ }
+
+ if (view.settings.proposeIslandNames.checked()) {
+ for (var isl of (json.islands || [])) {
+ view.islandManager.registerName(isl.name, view.assetsMap.get(isl.session));
+ }
+ }
+
+ var island = null;
+ if (json.islandName) {
+ island = view.islandManager.getByName(json.islandName);
+ }
+
+ if (!island)
+ return;
+
+ if (view.settings.updateSelectedIslandOnly.checked() && island != view.island())
+ return;
+
+
+ for (let key in json) {
+ let asset = island.assetsMap.get(parseInt(key));
+ if (asset instanceof PopulationLevel) {
+ if (json[key].amount != null && view.settings.populationLevelAmount.checked()) {
+ asset.amount(json[key].amount);
+ }
+ if (json[key].existingBuildings != null && view.settings.populationLevelExistingBuildings.checked()) {
+ asset.existingBuildings(json[key].existingBuildings);
+ }
+ } else if (asset instanceof Consumer) {
+ if (json[key].existingBuildings != null && view.settings.factoryExistingBuildings.checked())
+ asset.existingBuildings(parseInt(json[key].existingBuildings));
+ if (json[key].percentBoost != null && view.settings.factoryPercentBoost.checked())
+ asset.percentBoost(parseInt(json[key].percentBoost));
+ } else if (asset instanceof ResidenceBuilding) {
+ if (json[key].existingBuildings != null && view.settings.populationLevelExistingBuildings.checked())
+ asset.populationLevel.existingBuildings(json[key].existingBuildings);
+ }
+ }
+
+ } catch (e) {
+ }
+ }
+
+ checkVersion() {
+ if (!this.notificationShown && this.recentVersion && this.currentVersion && this.recentVersion !== this.currentVersion) {
+ this.notificationShown = true;
+ $.notify({
+ // options
+ message: view.texts.serverUpdate.name()
+ }, {
+ // settings
+ type: 'warning',
+ placement: { align: 'center' }
+ });
+ }
+ }
+
+
+}
+
+class IslandManager {
+ constructor(params) {
+ let islandKey = "islandName";
+ let islandsKey = "islandNames";
+
+ this.islandNameInput = ko.observable();
+ this.sessionInput = ko.observable(view.sessions[0]);
+ this.params = params;
+ this.islandCandidates = ko.observableArray();
+ this.unusedNames = new Set();
+ this.serverNamesMap = new Map();
+
+ this.showIslandOnCreation = new Option({
+ name: "Show Island on Creation",
+ locaText: texts.showIslandOnCreation
+ });
+ this.showIslandOnCreation.checked(true);
+
+ var islandNames = [];
+ if (localStorage && localStorage.getItem(islandsKey))
+ islandNames = JSON.parse(localStorage.getItem(islandsKey))
+
+ var islandName = localStorage.getItem(islandKey);
+ view.islands = ko.observableArray();
+ view.island = ko.observable();
+
+ view.island.subscribe(isl => window.document.title = isl.name());
+
+ for (var name of islandNames) {
+ var island = new Island(params, new Storage(name));
+ view.islands.push(island);
+ this.serverNamesMap.set(island.name(), island);
+
+ if (name == islandName)
+ view.island(island);
+ }
+
+ this.sortIslands();
+
+ var allIslands = new Island(params, localStorage);
+ this.allIslands = allIslands;
+ view.islands.unshift(allIslands);
+ this.serverNamesMap.set(allIslands.name(), allIslands);
+ if (!view.island())
+ view.island(allIslands);
+
+
+
+ if (localStorage) {
+ view.islands.subscribe(islands => {
+ let islandNames = JSON.stringify(islands.filter(i => !i.isAllIslands()).map(i => i.name()));
+ localStorage.setItem(islandsKey, islandNames);
+ });
+
+ view.island.subscribe(island => {
+ localStorage.setItem(islandKey, island.name());
+ });
+
+ }
+
+ this.islandExists = ko.computed(() => {
+ var name = this.islandNameInput();
+ if (!name || name == ALL_ISLANDS || name == view.texts.allIslands.name())
+ return true;
+
+ return this.serverNamesMap.has(name) && this.serverNamesMap.get(name).name() == name;
+ });
+ }
+
+ create(name, session) {
+ if (name == null) {
+ if (this.islandExists())
+ return;
+
+ name = this.islandNameInput();
+ }
+
+ if (this.serverNamesMap.has(name) && this.serverNamesMap.get(name).name() == name)
+ return;
+
+ var island = new Island(this.params, new Storage(name), session);
+ view.islands.push(island);
+ this.sortIslands();
+
+ if (this.showIslandOnCreation.checked())
+ view.island(island);
+
+ this.serverNamesMap.set(name, island);
+ var removedCandidates = this.islandCandidates.remove(i => !isNaN(this.compareNames(i.name, name)));
+ for (var c of removedCandidates) {
+ this.unusedNames.delete(c.name);
+ this.serverNamesMap.set(c.name, island);
+ }
+
+ if (name == this.islandNameInput())
+ this.islandNameInput(null);
}
-}
-class Factory extends NamedElement {
+ delete(island) {
+ if (island == null)
+ island = view.island();
- getInputs() {
- return this.inputs || [];
- }
+ if (island.name() == ALL_ISLANDS || island.isAllIslands())
+ return;
- getOutputs() {
- return this.outputs || [];
- }
+ if (view.island() == island)
+ view.island(view.islands()[0]);
- referenceProducts() {
- this.getInputs().forEach(i => i.product = assetsMap.get(i.Product));
- this.getOutputs().forEach(i => i.product = assetsMap.get(i.Product));
+ if (view.tradeManager) {
+ view.tradeManager.islandDeleted(island);
+ }
+
+ view.islands.remove(island);
+ if (localStorage)
+ localStorage.removeItem(island.name());
+
+ for (var entry of this.serverNamesMap.entries()) {
+ if (entry[1] == island)
+ this.serverNamesMap.set(entry[0], null);
+ }
+
+ this.serverNamesMap.delete(island.name());
+ this.unusedNames.add(island.name());
+ this.islandCandidates.push({ name: island.name(), session: island.session });
+ this.sortUnusedNames();
}
- getProduct() {
- return this.getOutputs()[0].product;
+ deleteCandidate(candidate) {
+ this.unusedNames.delete(candidate.name);
+ this.islandCandidates.remove(candidate);
}
- getWorkforceDemand() {
- for (let m of this.maintenances) {
- let a = assetsMap.get(m.Product);
- if (a instanceof Workforce)
- return new WorkforceDemand($.extend({ factory: this, workforce: a }, m));
- }
+ getByName(name) {
+ return name == ALL_ISLANDS ? this.allIslands : this.serverNamesMap.get(name);
}
-}
-class Product extends NamedElement {
- constructor(config) {
- super(config);
+ registerName(name, session) {
+ if (name == ALL_ISLANDS || this.serverNamesMap.has(name))
+ return;
+ if (this.unusedNames.has(name))
+ return;
- this.amount = ko.observable(0);
- this.percentBoost = ko.observable(100);
- this.boost = ko.computed(() => this.percentBoost() / 100);
- this.demands = [];
- if (this.producer) {
- this.factory = assetsMap.get(this.producer);
- let factoryTpmin = this.factory.tpmin;
- this.buildings = ko.computed(() => this.amount() / factoryTpmin / this.boost());
- this.workforceDemand = this.factory.getWorkforceDemand();
- this.buildings.subscribe(val => this.workforceDemand.updateAmount(val));
+ var island = null;
+ var bestMatch = 0;
+
+ for (var isl of view.islands()) {
+ var match = this.compareNames(isl.name(), name);
+ if (!isNaN(match) && match > bestMatch) {
+ island = isl;
+ bestMatch = match;
+ }
}
- }
- updateAmount() {
- var sum = 0;
- this.demands.forEach(d => sum += d.amount());
- this.amount(sum);
- }
+ if (island) {
+ this.serverNamesMap.set(name, island);
+ var removedCandidates = this.islandCandidates.remove(i => i.name === name);
+ for (var c of removedCandidates)
+ this.unusedNames.delete(c.name);
+ return;
+ }
- getInputs() {
- if (!this.producer) return [];
- return assetsMap.get(this.producer).getInputs();
+ this.islandCandidates.push({ name: name, session: session });
+ this.unusedNames.add(name);
+ this.sortUnusedNames();
}
- getOutputs() {
- if (!this.producer) return [];
- return assetsMap.get(this.producer).getOutputs();
- }
+ compareNames(name1, name2) {
+ var totalLength = Math.max(name1.length, name2.length);
+ var minLcsLength = totalLength - Math.round(-0.677 + 1.51 * Math.log(totalLength));
+ var lcsLength = this.lcsLength(name1, name2);
- add(demand) {
- this.demands.push(demand);
+ if (lcsLength >= minLcsLength)
+ return lcsLength / totalLength;
+ else
+ return NaN;
}
- incrementBuildings() {
- if (this.buildings() <= 0 || this.percentBoost() <= 1)
- return;
+ sortIslands() {
+ view.islands.sort((a, b) => {
+ if (a.isAllIslands() || a.name() == ALL_ISLANDS)
+ return false;
+ else if (b.isAllIslands() || b.name() == ALL_ISLANDS)
+ return true;
+
+ var sIdxA = view.sessions.indexOf(a.session);
+ var sIdxB = view.sessions.indexOf(b.session);
- var minBuildings = Math.ceil(this.buildings() * this.percentBoost() / (this.percentBoost() - 1));
- let nextBoost = Math.ceil(this.percentBoost() * this.buildings() / minBuildings)
- this.percentBoost(Math.min(nextBoost, this.percentBoost() - 1));
+ if (sIdxA == sIdxB) {
+ return a.name() - b.name();
+ } else {
+ return sIdxA - sIdxB;
+ }
+ });
}
- decrementBuildings() {
- let nextBuildings = Math.floor(this.buildings());
- if (nextBuildings <= 0)
- return;
+ sortUnusedNames() {
+ this.islandCandidates.sort((a, b) => {
+ var sIdxA = view.sessions.indexOf(a.session);
+ var sIdxB = view.sessions.indexOf(b.session);
- if (this.buildings() - nextBuildings < 0.01)
- nextBuildings = Math.floor(nextBuildings - 0.01);
- var nextBoost = Math.ceil(100 * this.boost() * this.buildings() / nextBuildings);
- if (nextBoost - this.percentBoost() < 1)
- nextBoost = this.percentBoost() + 1;
- this.percentBoost(nextBoost);
+ if (sIdxA == sIdxB) {
+ return a.name - b.name;
+ } else {
+ return sIdxA - sIdxB;
+ }
+ });
}
- incrementPercentBoost() {
- this.percentBoost(this.percentBoost() + 1);
+ // Function to find length of Longest Common Subsequence of substring
+ // X[0..m-1] and Y[0..n-1]
+ // From https://www.techiedelight.com/longest-common-subsequence/
+ lcsLength(X, Y) {
+ var m = X.length, n = Y.length;
+
+ // lookup table stores solution to already computed sub-problems
+ // i.e. lookup[i][j] stores the length of LCS of substring
+ // X[0..i-1] and Y[0..j-1]
+ var lookup = [];
+ for (var i = 0; i <= m; i++)
+ lookup.push(new Array(n + 1).fill(0));
+
+ // fill the lookup table in bottom-up manner
+ for (var i = 1; i <= m; i++) {
+ for (var j = 1; j <= n; j++) {
+ // if current character of X and Y matches
+ if (X[i - 1] == Y[j - 1])
+ lookup[i][j] = lookup[i - 1][j - 1] + 1;
+
+ // else if current character of X and Y don't match
+ else
+ lookup[i][j] = Math.max(lookup[i - 1][j], lookup[i][j - 1]);
+ }
+ }
+
+ // LCS will be last entry in the lookup table
+ return lookup[m][n];
}
+}
- decrementPercentBoost() {
- this.percentBoost(this.percentBoost() - 1);
+class DarkMode {
+ constructor() {
+ this.checked = ko.observable(false);
+
+ this.classAdditions = {
+ "body": "bg-dark",
+ //".ui-fieldset legend, body": "text-light",
+ //".form-control": "text-light bg-dark bg-darker",
+ //".custom-select": "text-light bg-dark bg-darker",
+ //".input-group-text, .modal-content": "bg-dark text-light",
+ //".btn-default": "btn-dark btn-outline-light",
+ //".btn-light": "btn-dark",
+ //".ui-fchain-item": "bg-dark",
+ //".card": "bg-dark"
+ };
+
+ this.checked.subscribe(() => this.apply());
+
+ if (localStorage) {
+ let id = "darkMode.checked";
+ if (localStorage.getItem(id) != null)
+ this.checked(parseInt(localStorage.getItem(id)));
+
+ this.checked.subscribe(val => localStorage.setItem(id, val ? 1 : 0));
+ }
}
- incrementAmount() {
- this.amount(Math.round(this.amount() * 10 + 1) / 10);
+ toggle() {
+ this.checked(!this.checked());
}
- decrementAmount() {
- this.amount(Math.round(this.amount() * 10 - 1) / 10);
+ apply() {
+ if (this.checked())
+ Object.keys(this.classAdditions).forEach((key) => $(key).addClass(this.classAdditions[key]));
+ else
+ Object.keys(this.classAdditions).reverse()
+ .forEach((key) => $(key).removeClass(this.classAdditions[key]));
}
}
-class Demand extends NamedElement {
- constructor(config) {
- super(config);
+class Template {
+ constructor(asset, parentInstance, attributeName, index) {
- this.amount = ko.observable(0);
- this.product = assetsMap.get(this.guid);
- if (this.product) {
- this.product.add(this);
- this.demands = this.product.getInputs().map(input => {
+ this.attributeName = attributeName;
+ this.index = index;
- let d = new Demand({ guid: input.Product });
- this.amount.subscribe(val => d.updateAmount(val * input.Amount));
- return d;
- });
+ this.name = asset.name;
+ this.guid = asset.guid;
+ this.getRegionExtendedName = asset.getRegionExtendedName;
+ this.editable = asset.editable;
+ this.region = asset.region;
+ this.hotkey = asset.hotkey;
+ this.templates = [];
+ this.parentInstance = ko.observable(parentInstance);
- this.amount.subscribe(val => {
- this.product.updateAmount();
- });
+ this.instance = ko.computed(() => {
+ var p = this.parentInstance();
+
+ var inst = p[this.attributeName][this.index];
- if (this.product.producer) {
+ this.templates.forEach(t => t.parentInstance(inst));
+
+ return inst;
+ });
- let factoryTpmin = assetsMap.get(this.product.producer).tpmin;
- this.buildings = ko.computed(() => this.amount() / factoryTpmin / this.product.boost());
+ for (var attr in asset) {
+ var val = asset[attr];
+
+ if (val instanceof Array) {
+ this[attr] = val.map((a, index) => {
+ if (Template.prototype.applicable(asset)) {
+ var t = new Template(a, this.instance(), attr, index);
+ this.templates.push(t);
+ return t;
+ } else
+ return a;
+ });
}
+ else if (!ko.isObservable(val) && !ko.isComputed(val) && asset.hasOwnProperty(attr))
+ this[attr] = val;
}
+
}
- updateAmount(amount) {
- this.amount(amount);
+ applicable(asset) {
+ return asset instanceof PopulationLevel ||
+ asset instanceof Workforce ||
+ asset instanceof ProductCategory ||
+ asset instanceof Product ||
+ asset instanceof Factory ||
+ asset instanceof Demand;
}
}
-class Need extends Demand {
- constructor(config) {
- super(config);
- this.allDemands = [];
- if (this.happiness) {
- this.optionalAmount = ko.observable(0);
- view.settings.noOptionalNeeds.checked.subscribe(checked => {
- if (checked)
- this.amount(0);
- else
- this.amount(this.optionalAmount());
- })
- }
-
- let treeTraversal = node => {
- this.allDemands.push(node);
- (node.demands || []).forEach(treeTraversal);
+function init() {
+ view.darkMode = new DarkMode();
+
+ // set up options
+ view.settings.options = [];
+ for (let attr in options) {
+ let o = new Option(options[attr]);
+ o.id = attr;
+ view.settings[attr] = o;
+ view.settings.options.push(o);
+
+ if (localStorage) {
+ let id = "settings." + attr;
+ if (localStorage.getItem(id) != null)
+ o.checked(parseInt(localStorage.getItem(id)));
+
+ o.checked.subscribe(val => localStorage.setItem(id, val ? 1 : 0));
}
- treeTraversal(this);
}
- updateAmount(inhabitants) {
- if (this.optionalAmount) {
- this.optionalAmount(this.tpmin * inhabitants)
- if (!view.settings.noOptionalNeeds.checked())
- this.amount(this.tpmin * inhabitants);
- } else {
- this.amount(this.tpmin * inhabitants);
+ view.settings.languages = params.languages;
+
+ view.settings.serverOptions = [];
+ for (let attr in serverOptions) {
+ let o = new Option(serverOptions[attr]);
+ o.id = attr;
+ if (attr != "optimalProductivity")
+ o.checked(true);
+ view.settings[attr] = o;
+ view.settings.serverOptions.push(o);
+
+ if (localStorage) {
+ let id = "serverSettings." + attr;
+ if (localStorage.getItem(id) != null)
+ o.checked(parseInt(localStorage.getItem(id)));
+
+ o.checked.subscribe(val => localStorage.setItem(id, val ? 1 : 0));
}
}
-}
+ view.assetsMap = new Map();
-class BuildingMaterialsNeed extends Need {
- updateAmount(buildings) {
- let factory = assetsMap.get(this.product.producer);
- this.amount(buildings * factory.tpmin * this.product.boost());
+ view.regions = [];
+ for (let region of (params.regions || [])) {
+ let r = new Region(region, view.assetsMap);
+ view.assetsMap.set(r.guid, r);
+ view.regions.push(r);
}
-}
-class PopulationLevel extends NamedElement {
- constructor(config) {
- super(config);
- this.amount = ko.observable(0);
- this.noOptionalNeeds = ko.observable(false);
- this.needs = [];
- config.needs.forEach(n => {
- if (n.tpmin > 0)
- this.needs.push(new Need(n));
- });
- this.amount.subscribe(val => this.needs.forEach(n => n.updateAmount(val)));
+ view.sessions = [];
+ for (let session of (params.sessions || [])) {
+ let s = new Session(session, view.assetsMap);
+ view.assetsMap.set(s.guid, s);
+ view.sessions.push(s);
}
- incrementAmount() {
- this.amount(this.amount() + 1);
- }
- decrementAmount() {
- this.amount(this.amount() - 1);
- }
-}
-class ProductCategory extends NamedElement {
- constructor(config) {
- super(config);
- this.products = config.products.map(p => assetsMap.get(p));
- }
-}
+ // set up newspaper
+ view.newspaperConsumption = new NewspaperNeedConsumption();
+ if (localStorage) {
+ let id = "newspaperPropagandaBuff";
+ if (localStorage.getItem(id) != null)
+ view.newspaperConsumption.selectedBuff(localStorage.getItem(id));
-class Workforce extends NamedElement {
- constructor(config) {
- super(config);
- this.amount = ko.observable(0);
- this.demands = [];
+ view.newspaperConsumption.selectedBuff.subscribe(val => localStorage.setItem(id, val));
}
- updateAmount() {
- var sum = 0;
- this.demands.forEach(d => sum += d.amount());
- this.amount(sum);
- }
+ for (var e of (params.newspaper || [])) {
+ var effect = new NewspaperNeedConsumptionEntry(e);
+ view.newspaperConsumption.add(effect);
- add(demand) {
- this.demands.push(demand);
- }
-}
+ if (localStorage) {
+ let id = effect.guid + ".checked";
+ if (localStorage.getItem(id) != null)
+ effect.checked(parseInt(localStorage.getItem(id)));
-class WorkforceDemand extends NamedElement {
- constructor(config) {
- super(config);
- this.amount = ko.observable(0);
- this.workforce.add(this);
- this.amount.subscribe(val => this.workforce.updateAmount());
+ effect.checked.subscribe(val => localStorage.setItem(id, val ? 1 : 0));
+ }
}
- updateAmount(buildings) {
- this.amount(Math.ceil(buildings) * this.Amount);
+ // set up NPC traders
+ view.productsToTraders = new Map();
+ for (var t of (params.traders || [])) {
+ var trader = new NPCTrader(t);
+
+ for (var r of t.goodsProduction) {
+ var route = $.extend({}, r, { trader: trader });
+ if (view.productsToTraders.has(r.Good))
+ view.productsToTraders.get(r.Good).push(route);
+ else
+ view.productsToTraders.set(r.Good, [route]);
+ }
}
-}
+ // set up island management
+ view.islandManager = new IslandManager(params);
+ if (localStorage) {
+ let id = "language";
+ if (localStorage.getItem(id))
+ view.settings.language(localStorage.getItem(id));
-function init() {
- for (attr in texts) {
- view.texts[attr] = new NamedElement({ name: attr, locaText: texts[attr] });
+ view.settings.language.subscribe(val => localStorage.setItem(id, val));
}
- for (attr in options) {
- view.settings[attr] = new Option(options[attr]);
+ // set up modal dialogs
+ view.selectedFactory = ko.observable(view.island().factories[0]);
+ view.selectedGoodConsumptionUpgradeList =
+ ko.observable(view.island().populationLevels[0].needs[0].goodConsumptionUpgradeList);
+ view.productionChain = new ProductionChainView();
+
+ view.tradeManager = new TradeManager();
+
+ var allIslands = view.islandManager.allIslands;
+ var selectedIsland = view.island();
+ var templates = [];
+ var arrayToTemplate = (name) => allIslands[name].map((asset, index) => {
+ var t = new Template(asset, selectedIsland, name, index);
+ templates.push(t);
+ return t;
+ });
+
+ view.island.subscribe(i => templates.forEach(t => t.parentInstance(i)));
+
+ view.template = {
+ populationLevels: arrayToTemplate("populationLevels"),
+ categories: arrayToTemplate("categories"),
+ consumers: arrayToTemplate("consumers"),
+ buildingMaterialsNeeds: arrayToTemplate("buildingMaterialsNeeds"),
+ workforce: arrayToTemplate("workforce")
}
- view.settings.languages = params.languages;
+ ko.applyBindings(view, $(document.body)[0]);
- for (workforce of params.workforce) {
- let w = new Workforce(workforce)
- assetsMap.set(w.guid, w);
- view.workforce.push(w);
- }
+ view.island().name.subscribe(val => { window.document.title = val; });
- for (factory of params.factories) {
- let f = new Factory(factory)
- assetsMap.set(f.guid, f);
- view.factories.push(f);
- }
+ // set up key bindings
+ var keyBindings = ko.computed(() => {
+ var bindings = new Map();
- let products = [];
- for (product of params.products) {
- if (product.producer) {
- let p = new Product(product);
+ var language = view.settings.language();
+ if (language == 'chinese' || language == 'korean' || language == 'japanese' || language == 'taiwanese') {
+ language = 'english';
+ }
+
+ for (var l of view.island().populationLevels) {
+ var name = l.locaText[language];
- products.push(p);
- assetsMap.set(p.guid, p);
+ for (var c of name.toLowerCase()) {
+ if (!bindings.has(c)) {
+ bindings.set(c, $(`.ui-tier-unit-name[tier-unit-guid=${l.guid}] ~ .input .input-group input`));
+ l.hotkey(c);
+ break;
+ }
+ }
}
- }
- view.factories.forEach(f => f.referenceProducts());
+ return bindings;
+ });
+ $(document).on("keydown", (evt) => {
+ if (evt.altKey || evt.ctrlKey || evt.shiftKey)
+ return true;
- for (level of params.populationLevels) {
- let l = new PopulationLevel(level)
- assetsMap.set(l.guid, l);
- view.populationLevels.push(l);
- }
+ if (evt.target.tagName === 'INPUT' && evt.target.type === "text")
+ return true;
- for (category of params.productFilter) {
- let c = new ProductCategory(category);
- assetsMap.set(c.guid, c);
- view.categories.push(c);
- }
+ var focused = false;
+ var bindings = keyBindings();
+ if (bindings.has(evt.key)) {
+ focused = true;
+ bindings.get(evt.key).focus().select();
+ }
- for (let b of view.categories[1].products) {
-// if (b.guid != 1010224) {
- if (b && b.demands.length == 0) {
- b.editable = true;
- let n = new BuildingMaterialsNeed({ guid: b.guid });
- b.buildings = ko.observable(0);
- b.buildings.subscribe(val => {
- if (!(typeof val === 'number'))
- val = parseFloat(val);
- n.updateAmount(val);
- });
- b.boost.subscribe(() => n.updateAmount(b.buildings()));
- view.buildingMaterialsNeeds.push(n);
+ if (evt.target.tagName === 'INPUT' && !isNaN(parseInt(evt.key)) || focused) {
+ let isDigit = evt.key >= "0" && evt.key <= "9";
+ return ['ArrowUp', 'ArrowDown', 'Backspace', 'Delete'].includes(evt.key) || isDigit || evt.key === "." || evt.key === ",";
}
- }
+ });
- ko.applyBindings(view, $(document.body)[0]);
+
+ // listen for the server providing the population count
+ window.reader = new PopulationReader();
}
function removeSpaces(string) {
@@ -351,98 +2608,289 @@ function removeSpaces(string) {
return string.replace(/\W/g, "");
}
-$(document).ready(function () {
- if (window.params == null)
- $('#params-dialog').modal("show");
- else
- init();
+var formater = new Intl.NumberFormat(navigator.language || "en").format;
+function formatNumber(num) {
+ var rounded = Math.ceil(100 * parseFloat(num)) / 100;
+ if (Math.abs(rounded) < EPSILON)
+ rounded = 0;
+ return formater(rounded);
+}
- $('#params-dialog').on('hide.bs.modal', () => {
- try {
- window.params = JSON.parse($('textarea#input-params').val());
- init();
- } catch (e) {
- console.log(e);
- $('#params-dialog').modal("show");
- }
- })
-})
+function getStep(id) {
+ return parseFloat($('#' + id).attr('step') || 1);
+}
-texts = {
- inhabitants: {
- english: "Inhabitants",
- german: "Bevölkerung"
- },
- workforce: {
- english: "Required Workforce",
- german: "Benötigte Arbeitskraft"
- },
- productionBoost: {
- english: "Production Boost",
- german: "Produktionsboost"
- },
- requiredNumberOfBuildings: {
- english: "Required Number of Buildings",
- german: "Benötigte Anzahl an Gebäuden"
- },
- tonsPerMinute: {
- english: "Production in Tons per Minute",
- german: "Produktion in Tonnen pro Minute"
- },
- language: {
- english: "Language",
- german: "Sprache"
- },
- settings: {
- english: "Settings",
- german: "Einstellungen"
- },
- help: {
- english: "Help",
- german: "Hilfe"
- },
- helpContent: {
- german:
- `Verwendung: Trage die aktuellen oder angestrebten Einwohner pro Stufe in die oberste Reihe ein. Die Produktionsketten aktualisieren sich automatisch sobald man die Eingabe verlässt. Es werden nur diejenigen Fabriken angezeigt, die benötigt werden.
+function getMin(id) {
+ return parseFloat($('#' + id).attr('min') || -Infinity);
+}
+
+function getMax(id) {
+ return parseFloat($('#' + id).attr('max') || Infinity);
+}
+
+ko.components.register('number-input-increment', {
+ template:
+ `
+
+
+
`
+});
+
+function formatPercentage(number) {
+ var str = window.formatNumber(Math.ceil(10 * parseFloat(number)) / 10) + ' %';
+ if (number > 0)
+ str = '+' + str;
+
+ return str;
+}
+
+function createIntInput(init) {
+ var obs = ko.observable(init);
+ obs.subscribe(val => {
+ var num = parseInt(val);
-In der darunterliegenden Reihe wird die Arbeitskraft angezeigt, die benötigt wird, um alle Gebäude zu betreiben (jeweils auf die nächste ganze Fabrik gerundet).
+ if (typeof num == "number" && isFinite(num) && val != num)
+ obs(num);
+ else if (typeof num != "number" || !isFinite(num))
+ obs(init);
+ });
-Danach folgen zwei große Abschnitte, die sich wiederum in Unterabschnitte unterteilen. Der erste gibt einen Überblick über alle benötigten Gebäude, sortiert nach dem produzierten Warentyp. Der zweite schlüsselt die einzelnen Produktionsketten nach Bevölkerungsstufen auf. Jeder der Abschnitte kann durch einen Klick auf die Überschrift zusammengeklappt werden.
+ return obs;
+}
+
+function createFloatInput(init) {
+ var obs = ko.observable(init);
+ obs.subscribe(val => {
+ var num = parseFloat(val);
+
+ if (typeof num == "number" && isFinite(num) && val != num)
+ obs(num);
+ else if (typeof num != "number" || !isFinite(num))
+ obs(init);
+ });
+
+ return obs;
+}
+
+function factoryReset() {
+ if (localStorage)
+ localStorage.clear();
+
+ location.reload();
+}
-In jeder Kachel wird der Name der Fabrik, das Icon der hergestellten Ware, der Boost für den Gebäudetyp, die Anzahl der benötigten Gebäude und die Produktionsrate in Tonnen pro Minute angezeigt. Die Anzahl der Gebäude wird mit zwei Nachkommastellen angezeigt, um die Höhe der Überkapazitäten direkt ablesen zu können. Daneben befinden sich zwei Buttons. Diese versuchen den Boost so einzustellen, dass alle Gebäude des Typs bestmöglich ausgelastet sind und dabei ein Gebäude mehr (+) bzw. eines weniger (-) benötigt wird.
+function isLocal() {
+ return window.location.protocol == 'file:' || /localhost|127\.0\.0\.1/.test(window.location.host.replace);
+}
-Da Baumaterialien sich Zwischenmaterialien mit Konsumgütern teilen sind sie (im Gegensatz zu Warenrechnern früherer Annos) mit aufgeführt, um so den Verbrauch von Minen besser planen zu können. Es muss die Anzahl der Endbetriebe per Hand eingegeben werden.
+function exportConfig() {
+ var saveData = (function () {
+ var a = document.createElement("a");
+ document.body.appendChild(a);
+ a.style = "display: none";
+ return function (data, fileName) {
+ var blob = new Blob([JSON.stringify(data, null, 4)], { type: "text/json" }),
+ url = window.URL.createObjectURL(blob);
+ a.href = url;
+ a.download = fileName;
+ a.click();
+ window.URL.revokeObjectURL(url);
+ };
+ }());
+
+ saveData(localStorage, ("Anno1404CalculatorConfig") + ".json");
+}
-Über das Zahnrad am rechten oberen Bildschirmrand gelangt man zu den Einstellungen. Dort können die Sprache ausgewählt, der Warenrechner heruntergeladen und Einstellungen für die Warenberechnung getroffen werden.`,
- english:
- `Usage: Enter the current or desired number of inhabitants per level into the top most row. The production chains will update automatically when one leaves the input field. Only the required factories are displayed.
+function batchImports(source, destinations, factories) {
+ if (!(source instanceof Island))
+ source = view.islandManager.getByName(source);
-The row below displays the workforce that is required to run all buildings (rounded towards the next complete factory).
+ for (var dest of destinations) {
+ if (!(dest instanceof Island))
+ dest = view.islandManager.getByName(dest);
-Afterwards two big sections follow that are subdivided into smaller sections. The first one gives an overview of the required buildings sorted by the type of good that is produced. The second one lists the individual production chains for each population level. Clicking the heading collapses each section.
+ for (var f of factories) {
+ var list = dest.assetsMap.get(f).tradeList;
-Each card displays the name of the factory, the icon of the produced good, the boost for the given type of building, the number of required buildings, and the production rate in tons per minute. The number of buildings has two decimal places to directly show the amount of overcapacities. There are two buttons next to it. Those try to adjust the boost such that all buildings operate at full capacity and one more (+) or one building less (-) is required.
+ if (list.factory.overProduction() > -ACCURACY)
+ continue;
-Since construction materials share intermediate products with consumables they are explicitly listed (unlike in calculators for previous Annos) to better plan the production of mines. The number of factories must be entered manually.
+ list.onShow();
+ if (list.unusedIslands.indexOf(source) == -1)
+ continue;
-When clicking on the cog wheel in the upper right corner of the screen the settings dialog opens. There, one can chose the language, download the calculator and chose options affecting the calculation.`
+ list.selectedIsland(source);
+ list.export(false);
+ list.newAmount(Math.abs(list.factory.overProduction()));
+ list.create();
+ }
}
}
-options = {
- "noOptionalNeeds": {
- "name": "Do not produce luxury goods",
- "locaText": {
- "english": "Do not produce luxury goods",
- "german": "Keine Luxusgüter produzieren"
+function batchExports(sources, destination, factories) {
+ if (!(destination instanceof Island))
+ destination = view.islandManager.getByName(destination);
+
+ for (var src of sources) {
+ if (!(src instanceof Island))
+ src = view.islandManager.getByName(src);
+
+ for (var f of factories) {
+ var list = src.assetsMap.get(f).tradeList;
+
+ if (list.factory.overProduction() < ACCURACY)
+ continue;
+
+ list.onShow();
+ if (list.unusedIslands.indexOf(destination) == -1)
+ continue;
+
+ list.selectedIsland(destination);
+ list.export(true);
+ list.newAmount(Math.abs(list.factory.overProduction()));
+ list.create();
}
- },
- "decimalsForBuildings": {
- "name": "Show number of buildings with decimals",
- "locaText": {
- "english": "Show number of buildings with decimals",
- "german": "Zeige Nachkommastellen bei der Gebäudeanzahl"
+ }
+}
+
+function checkAndShowNotifications() {
+ $.getJSON("https://api.github.com/repos/NiHoel/Anno1404Calculator/releases/latest").done((release) => {
+ $('#download-calculator-button').attr("href", release.zipball_url);
+
+ if (isLocal()) {
+ if (release.tag_name !== versionCalculator) {
+ $.notify({
+ // options
+ message: view.texts.calculatorUpdate.name()
+ }, {
+ // settings
+ type: 'warning',
+ placement: { align: 'center' }
+ });
+ }
}
- },
-}
\ No newline at end of file
+ if (localStorage) {
+ if (localStorage.getItem("versionCalculator") != versionCalculator) {
+ if (view.texts.newFeature.name() && view.texts.newFeature.name().length)
+ $.notify({
+ // options
+ message: view.texts.newFeature.name()
+ }, {
+ // settings
+ type: 'success',
+ placement: { align: 'center' },
+ timer: 60000
+ });
+ }
+
+ localStorage.setItem("versionCalculator", versionCalculator);
+ }
+
+ });
+}
+
+function installImportConfigListener() {
+ if (localStorage) {
+ $('#config-selector').on('change', event => {
+ event.preventDefault();
+ if (!event.target.files || !event.target.files[0])
+ return;
+
+ let file = event.target.files[0];
+ console.log(file);
+ var fileReader = new FileReader();
+
+ fileReader.onload = function (ev) {
+ let text = ev.target.result || ev.currentTarget.result;
+
+ try {
+ let config = JSON.parse(text);
+
+ if (localStorage) {
+
+ if (config.islandName && config.islandName != "Anno 1404 Calculator" &&
+ !config.islandNames && !config[config.islandName]) {
+ // import old, one island save
+ delete config.versionCalculator;
+ delete config.versionServer;
+
+ view.islandManager.islandNameInput(config.islandName);
+ view.islandManager.create();
+ var island = view.islands().filter(i => i.name() == config.islandName)[0];
+ island.storage.json = config;
+ island.storage.save();
+ localStorage.setItem("islandName", config.islandName);
+ } else {
+ localStorage.clear();
+ for (var a in config)
+ localStorage.setItem(a, config[a]);
+ localStorage.setItem("versionCalculator", versionCalculator);
+
+ if (!config.islandNames) { // old save, restore islands
+ for (var island of view.islands()) {
+ if (!island.isAllIslands())
+ island.storage.save();
+ }
+ let islandNames = JSON.stringify(view.islands().filter(i => !i.isAllIslands()).map(i => i.name()));
+ localStorage.setItem("islandNames", islandNames);
+ }
+ }
+ location.reload();
+
+ } else {
+ console.error("No local storage accessible to write result into.");
+ }
+
+ } catch (e) {
+ console.error(e);
+ }
+ };
+ fileReader.onerror = function (err) {
+ console.error(err);
+ };
+
+ fileReader.readAsText(file);
+ });
+ }
+}
+
+$(document).ready(function () {
+ // parse the parameters
+ for (let attr in texts) {
+ view.texts[attr] = new NamedElement({ name: attr, locaText: texts[attr] });
+ }
+
+ var firstStart = localStorage && !localStorage.getItem("versionCalculator");
+ if (firstStart)
+ localStorage.setItem("hideProductionBoost", 0);
+
+ // check version of calculator - display update and new feature notification
+ checkAndShowNotifications();
+
+ //update links of download buttons
+ $.getJSON("https://api.github.com/repos/NiHoel/Anno1404UXEnhancer/releases/latest").done((release) => {
+ $('#download-calculator-server-button').attr("href", release.assets[0].browser_download_url);
+ });
+
+ installImportConfigListener();
+
+
+ //load parameters
+ if (window.params == null)
+ $('#params-dialog').modal("show");
+ else
+ init();
+
+ $('#params-dialog').on('hide.bs.modal', () => {
+ try {
+ window.params = JSON.parse($('textarea#input-params').val());
+ init();
+ } catch (e) {
+ console.log(e);
+ $('#params-dialog').modal("show");
+ }
+ });
+
+ $('[data-toggle="popover"]').popover();
+})
diff --git a/CalculatorExtractionScreenshot.png b/CalculatorExtractionScreenshot.png
new file mode 100644
index 0000000..55d890c
Binary files /dev/null and b/CalculatorExtractionScreenshot.png differ
diff --git a/CalculatorScreenshot.png b/CalculatorScreenshot.png
new file mode 100644
index 0000000..5b4f46c
Binary files /dev/null and b/CalculatorScreenshot.png differ
diff --git a/README.md b/README.md
index 3532405..6ee82c6 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,14 @@
-# Anno1800Calculator
+# Anno 1404 Calculator
+
+[![Tutorial](CalculatorScreenshot.png?raw=true "Calculator Screenshot")](https://youtu.be/4ZJYZ5GBc60)
+
+* A calculator for the computer game [Anno 1404](https://store.ubi.com/upc/de/anno-1404-history-edition/5e29c6565cdf9a03ec037ae7.html) to compute the required production depending on the population
+* [YouTube-Tutorial](https://youtu.be/4ZJYZ5GBc60)
+* To use the calculator go to the following website: https://nihoel.github.io/Anno1404Calculator/
+* To use it offline, download, unzip and open index.html with a browser: https://github.com/NiHoel/Anno1404Calculator/archive/v1.0.zip
+
+An application to read population and factory count from the game and enter it into the calculator.
+* Download link of UXEnhancer: https://github.com/NiHoel/Anno1404UXEnhancer
+* [YouTube-Tutorial](https://youtu.be/k4WmgEIkp4s)
+* License: MIT
+* Author: Nico Höllerich
diff --git a/bootstrap-notify.min.js b/bootstrap-notify.min.js
new file mode 100644
index 0000000..f5ad385
--- /dev/null
+++ b/bootstrap-notify.min.js
@@ -0,0 +1,2 @@
+/* Project: Bootstrap Growl = v3.1.3 | Description: Turns standard Bootstrap alerts into "Growl-like" notifications. | Author: Mouse0270 aka Robert McIntosh | License: MIT License | Website: https://github.com/mouse0270/bootstrap-growl */
+!function(t){"function"==typeof define&&define.amd?define(["jquery"],t):t("object"==typeof exports?require("jquery"):jQuery)}(function(t){function e(e,i,n){var i={content:{message:"object"==typeof i?i.message:i,title:i.title?i.title:"",icon:i.icon?i.icon:"",url:i.url?i.url:"#",target:i.target?i.target:"-"}};n=t.extend(!0,{},i,n),this.settings=t.extend(!0,{},s,n),this._defaults=s,"-"==this.settings.content.target&&(this.settings.content.target=this.settings.url_target),this.animations={start:"webkitAnimationStart oanimationstart MSAnimationStart animationstart",end:"webkitAnimationEnd oanimationend MSAnimationEnd animationend"},"number"==typeof this.settings.offset&&(this.settings.offset={x:this.settings.offset,y:this.settings.offset}),this.init()}var s={element:"body",position:null,type:"info",allow_dismiss:!0,newest_on_top:!1,showProgressbar:!1,placement:{from:"top",align:"right"},offset:20,spacing:10,z_index:1031,delay:5e3,timer:1e3,url_target:"_blank",mouse_over:null,animate:{enter:"animated fadeInDown",exit:"animated fadeOutUp"},onShow:null,onShown:null,onClose:null,onClosed:null,icon_type:"class",template:'
{1}{2}
'};String.format=function(){for(var t=arguments[0],e=1;e .progress-bar').removeClass("progress-bar-"+t.settings.type),t.settings.type=i[e],this.$ele.addClass("alert-"+i[e]).find('[data-notify="progressbar"] > .progress-bar').addClass("progress-bar-"+i[e]);break;case"icon":var n=this.$ele.find('[data-notify="icon"]');"class"==t.settings.icon_type.toLowerCase()?n.removeClass(t.settings.content.icon).addClass(i[e]):(n.is("img")||n.find("img"),n.attr("src",i[e]));break;case"progress":var a=t.settings.delay-t.settings.delay*(i[e]/100);this.$ele.data("notify-delay",a),this.$ele.find('[data-notify="progressbar"] > div').attr("aria-valuenow",i[e]).css("width",i[e]+"%");break;case"url":this.$ele.find('[data-notify="url"]').attr("href",i[e]);break;case"target":this.$ele.find('[data-notify="url"]').attr("target",i[e]);break;default:this.$ele.find('[data-notify="'+e+'"]').html(i[e])}var o=this.$ele.outerHeight()+parseInt(t.settings.spacing)+parseInt(t.settings.offset.y);t.reposition(o)},close:function(){t.close()}}},buildNotify:function(){var e=this.settings.content;this.$ele=t(String.format(this.settings.template,this.settings.type,e.title,e.message,e.url,e.target)),this.$ele.attr("data-notify-position",this.settings.placement.from+"-"+this.settings.placement.align),this.settings.allow_dismiss||this.$ele.find('[data-notify="dismiss"]').css("display","none"),(this.settings.delay<=0&&!this.settings.showProgressbar||!this.settings.showProgressbar)&&this.$ele.find('[data-notify="progressbar"]').remove()},setIcon:function(){"class"==this.settings.icon_type.toLowerCase()?this.$ele.find('[data-notify="icon"]').addClass(this.settings.content.icon):this.$ele.find('[data-notify="icon"]').is("img")?this.$ele.find('[data-notify="icon"]').attr("src",this.settings.content.icon):this.$ele.find('[data-notify="icon"]').append('')},styleURL:function(){this.$ele.find('[data-notify="url"]').css({backgroundImage:"url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)",height:"100%",left:"0px",position:"absolute",top:"0px",width:"100%",zIndex:this.settings.z_index+1}),this.$ele.find('[data-notify="dismiss"]').css({position:"absolute",right:"10px",top:"5px",zIndex:this.settings.z_index+2})},placement:function(){var e=this,s=this.settings.offset.y,i={display:"inline-block",margin:"0px auto",position:this.settings.position?this.settings.position:"body"===this.settings.element?"fixed":"absolute",transition:"all .5s ease-in-out",zIndex:this.settings.z_index},n=!1,a=this.settings;switch(t('[data-notify-position="'+this.settings.placement.from+"-"+this.settings.placement.align+'"]:not([data-closing="true"])').each(function(){return s=Math.max(s,parseInt(t(this).css(a.placement.from))+parseInt(t(this).outerHeight())+parseInt(a.spacing))}),1==this.settings.newest_on_top&&(s=this.settings.offset.y),i[this.settings.placement.from]=s+"px",this.settings.placement.align){case"left":case"right":i[this.settings.placement.align]=this.settings.offset.x+"px";break;case"center":i.left=0,i.right=0}this.$ele.css(i).addClass(this.settings.animate.enter),t.each(Array("webkit","moz","o","ms",""),function(t,s){e.$ele[0].style[s+"AnimationIterationCount"]=1}),t(this.settings.element).append(this.$ele),1==this.settings.newest_on_top&&(s=parseInt(s)+parseInt(this.settings.spacing)+this.$ele.outerHeight(),this.reposition(s)),t.isFunction(e.settings.onShow)&&e.settings.onShow.call(this.$ele),this.$ele.one(this.animations.start,function(){n=!0}).one(this.animations.end,function(){t.isFunction(e.settings.onShown)&&e.settings.onShown.call(this)}),setTimeout(function(){n||t.isFunction(e.settings.onShown)&&e.settings.onShown.call(this)},600)},bind:function(){var e=this;if(this.$ele.find('[data-notify="dismiss"]').on("click",function(){e.close()}),this.$ele.mouseover(function(){t(this).data("data-hover","true")}).mouseout(function(){t(this).data("data-hover","false")}),this.$ele.data("data-hover","false"),this.settings.delay>0){e.$ele.data("notify-delay",e.settings.delay);var s=setInterval(function(){var t=parseInt(e.$ele.data("notify-delay"))-e.settings.timer;if("false"===e.$ele.data("data-hover")&&"pause"==e.settings.mouse_over||"pause"!=e.settings.mouse_over){var i=(e.settings.delay-t)/e.settings.delay*100;e.$ele.data("notify-delay",t),e.$ele.find('[data-notify="progressbar"] > div').attr("aria-valuenow",i).css("width",i+"%")}t<=-e.settings.timer&&(clearInterval(s),e.close())},e.settings.timer)}},close:function(){var e=this,s=parseInt(this.$ele.css(this.settings.placement.from)),i=!1;this.$ele.data("closing","true").addClass(this.settings.animate.exit),e.reposition(s),t.isFunction(e.settings.onClose)&&e.settings.onClose.call(this.$ele),this.$ele.one(this.animations.start,function(){i=!0}).one(this.animations.end,function(){t(this).remove(),t.isFunction(e.settings.onClosed)&&e.settings.onClosed.call(this)}),setTimeout(function(){i||(e.$ele.remove(),e.settings.onClosed&&e.settings.onClosed(e.$ele))},600)},reposition:function(e){var s=this,i='[data-notify-position="'+this.settings.placement.from+"-"+this.settings.placement.align+'"]:not([data-closing="true"])',n=this.$ele.nextAll(i);1==this.settings.newest_on_top&&(n=this.$ele.prevAll(i)),n.each(function(){t(this).css(s.settings.placement.from,e),e=parseInt(e)+parseInt(s.settings.spacing)+t(this).outerHeight()})}}),t.notify=function(t,s){var i=new e(this,t,s);return i.notify},t.notifyDefaults=function(e){return s=t.extend(!0,{},s,e)},t.notifyClose=function(e){"undefined"==typeof e||"all"==e?t("[data-notify]").find('[data-notify="dismiss"]').trigger("click"):t('[data-notify-position="'+e+'"]').find('[data-notify="dismiss"]').trigger("click")}});
\ No newline at end of file
diff --git a/i18n.js b/i18n.js
new file mode 100644
index 0000000..811ac7a
--- /dev/null
+++ b/i18n.js
@@ -0,0 +1,731 @@
+var languageCodes = {
+ 'en': 'english',
+ 'de': 'german',
+ 'fr': 'french',
+ 'ru': 'russian',
+ 'ko': 'korean',
+ 'ja': 'japanese',
+ 'zh': 'chinese',
+ 'it': 'italien',
+ 'es': 'spanish',
+ 'pl': 'polish'
+}
+
+
+texts = {
+ allIslands: {
+ "french": "Toutes les îles",
+ "english": "All Islands",
+ "italian": "Tutte le isole",
+ "chinese": "所有岛屿",
+ "spanish": "Todas las islas",
+ "japanese": "すべての島",
+ "taiwanese": "所有島嶼",
+ "polish": "Wszystkie wyspy",
+ "german": "Alle Inseln",
+ "korean": "모든 섬",
+ "russian": "Все острова"
+ },
+ residents: {
+ "french": "Résidents",
+ "english": "Residents",
+ "italian": "Residenti",
+ "chinese": "居民",
+ "spanish": "Residentes",
+ "japanese": "住民",
+ "taiwanese": "居民",
+ "polish": "Mieszkańcy",
+ "german": "Einwohner",
+ "korean": "주민",
+ "russian": "Жители"
+ },
+ workforce: {
+ english: "Required Workforce",
+ german: "Benötigte Arbeitskraft",
+ korean: "필요한 인력"
+ },
+ productionBoost: {
+ "french": "Productivité",
+ "brazilian": "Production",
+ "english": "Productivity",
+ "portuguese": "Production",
+ "italian": "Produzione",
+ "chinese": "生产力",
+ "spanish": "Productividad",
+ "japanese": "生産性",
+ "taiwanese": "生產力",
+ "polish": "Wydajność",
+ "german": "Produktivität",
+ "korean": "생산성",
+ "russian": "Производительность"
+ },
+ reset: {
+ "english": "Reset",
+ "french": "Réinitialiser",
+ "german": "Zurücksetzen",
+ "korean": "재설정",
+ "portuguese": "Reset",
+ "brazilian": "Reset",
+ "taiwanese": "重設",
+ "chinese": "重设",
+ "spanish": "Reiniciar",
+ "italian": "Azzera",
+ "russian": "Сбросить",
+ "polish": "Wyzeruj",
+ "japanese": "リセット"
+ },
+ itemsEquipped: {
+ "english": "Items Equipped",
+ "chinese": "已装备物品",
+ "taiwanese": "已裝備物品",
+ "italian": "Oggetti in uso",
+ "spanish": "Objetos equipados",
+ "german": "Ausgerüstete Items",
+ "polish": "Przedmioty w użyciu",
+ "french": "Objets en stock",
+ "korean": "배치한 아이템",
+ "japanese": "装備したアイテム",
+ "russian": "Используемые предметы"
+ },
+ productionBuildings: {
+ "english": "Production Buildings",
+ "chinese": "生产建筑",
+ "taiwanese": "生產建築",
+ "italian": "Edifici produttivi",
+ "spanish": "Edificios de producción",
+ "german": "Produktionsgebäude",
+ "polish": "Budynki produkcyjne",
+ "french": "Bâtiments de production",
+ "korean": "생산 건물",
+ "japanese": "生産施設",
+ "russian": "Производственные здания"
+ },
+ extraGoods: {
+ "english": "Extra Goods",
+ "chinese": "额外货物",
+ "taiwanese": "額外貨物",
+ "italian": "Merci aggiuntive",
+ "spanish": "Bienes extra",
+ "german": "Zusatzwaren",
+ "polish": "Dodatkowe towary",
+ "french": "Marchandises supplémentaires",
+ "korean": "추가 물품",
+ "japanese": "追加品物",
+ "russian": "Дополнительные товары"
+ },
+ total: {
+ "english": "Total",
+ "chinese": "总计",
+ "taiwanese": "總計",
+ "italian": "Totale",
+ "spanish": "Total",
+ "german": "Gesamt",
+ "polish": "Razem",
+ "french": "Total",
+ "korean": "합계",
+ "japanese": "合計",
+ "russian": "Всего"
+ },
+ "all": {
+ "english": "All",
+ "chinese": "全部",
+ "taiwanese": "全部",
+ "italian": "Tutti",
+ "spanish": "Todo",
+ "german": "Alle",
+ "polish": "Wszystko",
+ "french": "Toutes",
+ "korean": "모두",
+ "japanese": "すべて",
+ "russian": "Все"
+ },
+ "effect": {
+ "english": "Effect",
+ "chinese": "效果",
+ "taiwanese": "效果",
+ "italian": "Effetto",
+ "spanish": "Efecto",
+ "german": "Effekte",
+ "polish": "Efekt",
+ "french": "Effet",
+ "korean": "효과",
+ "japanese": "効果",
+ "russian": "Эффект"
+ },
+ "needConsumption": {
+ "english": "Need Consumption",
+ "chinese": "需求消耗程度",
+ "taiwanese": "需求消耗程度",
+ "italian": "Bisogno di consumo",
+ "spanish": "Consumo de necesidad",
+ "german": "Verbrauch der Bedürfnisse",
+ "polish": "Potrzebna konsumpcja",
+ "french": "Consommation du bien",
+ "korean": "물품 요구량",
+ "japanese": "需要の消費",
+ "russian": "Потребление"
+ },
+ "newspaper": {
+ "english": "Newspaper",
+ "chinese": "报纸",
+ "taiwanese": "報紙",
+ "italian": "Giornale",
+ "spanish": "Periódico",
+ "german": "Zeitung",
+ "polish": "Gazeta",
+ "french": "Journal",
+ "korean": "신문",
+ "japanese": "新聞",
+ "russian": "Газета"
+ },
+ "newspaperEffectiveness": {
+ "english": "Newspaper Effectiveness",
+ "chinese": "报纸效用",
+ "taiwanese": "報紙效用",
+ "italian": "Efficacia giornale",
+ "spanish": "Efectividad del periódico",
+ "german": "Effektivität der Zeitung",
+ "polish": "Skuteczność gazety",
+ "french": "Efficacité du journal",
+ "korean": "신문 효과",
+ "japanese": "新聞の効力",
+ "russian": "Эффективность газеты"
+ },
+ "reducedNeeds": {
+ "english": "Reduced Needs",
+ "guid": 21387,
+ "chinese": "减少需求物",
+ "taiwanese": "減少需求物",
+ "italian": "Bisogni ridotti",
+ "spanish": "Necesidades reducidas",
+ "german": "Bedürfnis-Malus",
+ "polish": "Zredukowane potrzeby",
+ "french": "Besoins réduits",
+ "korean": "요구량 감소",
+ "japanese": "減少した需要",
+ "russian": "Сниженные потребности"
+ },
+ "islands": {
+ "english": "Islands",
+ "chinese": "岛屿",
+ "taiwanese": "島嶼",
+ "italian": "Isole",
+ "spanish": "Islas",
+ "german": "Inseln",
+ "polish": "Wyspy",
+ "french": "Îles",
+ "korean": "섬",
+ "japanese": "島",
+ "russian": "Острова"
+ },
+ "apply": {
+ "english": "Apply",
+ "chinese": "应用",
+ "taiwanese": "套用",
+ "italian": "Applica",
+ "spanish": "Aplicar",
+ "german": "Anwenden",
+ "polish": "Zastosuj",
+ "french": "Appliquer",
+ "korean": "적용",
+ "japanese": "適用",
+ "russian": "Применить"
+ },
+ "world": {
+ "english": "The World",
+ "chinese": "世界",
+ "taiwanese": "世界",
+ "italian": "Il mondo",
+ "spanish": "El mundo",
+ "german": "Die Welt",
+ "polish": "Świat",
+ "french": "Le Monde",
+ "korean": "세계",
+ "japanese": "世界",
+ "russian": "Мир"
+ },
+ "deleteAll": {
+ "english": "Delete All",
+ "chinese": "删除全部",
+ "taiwanese": "刪除全部",
+ "italian": "Cancella tutto",
+ "spanish": "Borrar todo",
+ "german": "Alles löschen",
+ "polish": "Skasuj wszystko",
+ "french": "Supprimer tout",
+ "korean": "모두 삭제",
+ "japanese": "すべて削除する",
+ "russian": "Удалить все"
+ },
+ "consumption": {
+ "english": "Consumption",
+ "chinese": "消耗",
+ "taiwanese": "消耗",
+ "italian": "Consumo",
+ "spanish": "Consumo",
+ "german": "Verbrauch",
+ "polish": "Konsumpcja",
+ "french": "Consommation",
+ "korean": "소비량",
+ "japanese": "消費",
+ "russian": "Потребление"
+ },
+ requiredNumberOfBuildings: {
+ english: "Required Number of Buildings",
+ german: "Benötigte Anzahl an Gebäuden",
+ korean: "필요한 건물 수"
+ },
+ existingNumberOfBuildings: {
+ english: "Existing Number of Buildings",
+ german: "Vorhandene Anzahl an Gebäuden",
+ korean: "현재 건물 수"
+ },
+ existingNumberOfBuildingsIs: {
+ english: "Is:",
+ german: "Ist:",
+ korean: "현재:"
+ },
+ requiredNumberOfBuildings: {
+ english: "Required:",
+ german: "Benötigt:",
+ korean: "필요:"
+ },
+ requiredNumberOfBuildingsDescription: {
+ english: "Required number of buildings to produce consumer products",
+ german: "Benötigte Gebäudeanzahl zur Produktion von Verbrauchsgütern",
+ korean: "소비재 생산에 필요한 건물 수"
+ },
+ tonsPerMinute: {
+ "english": "Tons per minute (t/min)",
+ "chinese": "每分钟吨数(吨/分钟)",
+ "taiwanese": "每分鐘噸數(噸/分鐘)",
+ "italian": "Tonnellate al minuto (t/min)",
+ "spanish": "Toneladas por minuto (t/min)",
+ "german": "Tonnen pro Minute (t/min)",
+ "polish": "Tony na minutę (t/min)",
+ "french": "Tonnes par minute (t/min)",
+ "korean": "톤/분(1분당 톤 수)",
+ "japanese": "トン毎分 (トン/分)",
+ "russian": "Тонн в минуту (т./мин.)"
+ },
+ outputAmount: {
+ english: "Plain building output without extra goods",
+ german: "Reiner Gebäudeoutput ohne Zusatzwaren",
+ },
+ extraNeed: {
+ english: "Extra Demand",
+ german: "Zusatzbedarf"
+ },
+ showIslandOnCreation: {
+ english: "After creating a new island display it",
+ german: "Nach dem Erstellen einer neuen Insel diese anzeigen"
+ },
+ applyGlobally: {
+ english: "Apply Globally",
+ german: "Global anwenden"
+ },
+ overproduction: {
+ english: "Overproduction",
+ german: "Überproduktion"
+ },
+ importDeficit: {
+ english: "Import deficit",
+ german: "Defizit importieren"
+ },
+ exportOverproduction: {
+ english: "Export overproduction",
+ german: "Überproduktion exportieren"
+ },
+ language: {
+ "english": "Text Language",
+ "chinese": "文本语言",
+ "taiwanese": "文字語言",
+ "italian": "Lingua testo",
+ "spanish": "Idioma del texto",
+ "german": "Textsprache",
+ "polish": "Język napisów",
+ "french": "Langue des textes",
+ "korean": "텍스트 언어",
+ "japanese": "テキスト言語",
+ "russian": "Язык текста"
+ },
+ islandName: {
+ english: "New island name",
+ german: "Neuer Inselname",
+ korean: "새로운 섬 이름"
+ },
+ selectedIsland: {
+ english: "Selected Island",
+ german: "Ausgewählte Insel",
+ korean: "선택된 섬"
+ },
+ settings: {
+ english: "Settings",
+ german: "Einstellungen",
+ korean: "설정"
+ },
+ help: {
+ english: "Help",
+ german: "Hilfe",
+ korean: "도움말"
+ },
+ chooseFactories: {
+ english: "Modify Production Chains",
+ german: "Modifiziere Produktionsketten",
+ korean: "생산 체인 수정"
+ },
+ noFixedFactory: {
+ english: "Automatic: same region as consumer",
+ german: "Automatisch: gleichen Region wie Verbraucher",
+ korean: "자동 : 소비자와 동일한 지역"
+ },
+ consumptionModifier: {
+ english: "Modify the percental amount of consumption for this tier and product",
+ german: "Verändere die prozentuale Verbrauchsmenge für diese Ware und Bevölkerungsstufe",
+ korean: "이 계층 및 제품의 사용량(백분율)을 수정하십시요"
+ },
+ download: {
+ english: "Downloads",
+ german: "Downloads",
+ korean: "다운로드"
+ },
+ downloadConfig: {
+ english: "Import / Export configuration.",
+ german: "Konfiguration importieren / exportieren.",
+ korean: "설정 가져오기 / 내보내기"
+ },
+ downloadCalculator: {
+ english: "Download the calculator (source code of this website) to run it locally. To do so, extract the archive and double click index.html.",
+ german: "Lade den Warenrechner (Quellcode dieser Seite) herunter, um ihn lokal auszuführen. Zum Ausführen, extrahiere das Archiv und doppelklicke auf index.html.",
+ korean: "Anno 계산기 (이 웹 사이트의 소스 코드)를 다운로드 하여 로컬로 실행 하십시오. 압축을 풀고 index.html 실행 하십시오."
+ },
+ downloadCalculatorServer: {
+ english: `Download a standalone executable that reads the current population count while playing the game. Usage:
+1. Download server application and calculator (using the source code from above).
+2. Start Anno 1404 with the graphics setting "Windowed Full Screen".
+3. Start server (Server.exe) and open downloaded calculator (index.html) - make sure that Anno does not get minimized.
+4. Open the population overview or the reeve's book to update the values in the calculator.
+
+ See the following link for more information: `,
+ german: `Lade eine ausführbare Datei herunter, die beim Spielen die aktuellen Bevölkerungszahlen erfasst. Verwendung:
+1. Lade die Serveranwendung und den Warenrechner (siehe obiger Quellcode) herunter.
+2. Starte Anno 1404 mit der Graphikeinstellung "Vollbild im Fenstermodus".
+3. Führe den Server (Server.exe) aus und öffne den heruntergeladenen Warenrechner (index.html) - stelle sicher, dass Anno nicht minimiert wird.
+4. Öffne die Bevölkerungsübersicht oder das Vogtbuch, um die Werte im Warenrechner zu aktualisieren.
+
+Siehe folgenden Link für weitere Informationen: `,
+ korean: `게임을 하는 동안 현재 인구 수를 읽는 실행 파일을 다운로드 하십시오. 방법:
+1. 서버 프로그램 및 계산기를 다운로드 하십시오. (위의 소스 코드 사용).
+2. Anno 1404을 실행 하십시오.
+3. 서버 (Server.exe)를 실행하고 다운로드한 Anno 1404 계산기 (index.html)를 엽니다.
+4. 인구 통계 (모든 섬 또는 일부 섬)를 펼쳐서 열거나 통계 화면 (금융, 생산, 인구)을 열어 계산기의 값을 업데이트하십시오.
+ 자세한 내용은 다음 링크를 참조하십시오: `
+ },
+ serverUpdate: {
+ english: "A new server version is available. Click the download button.",
+ german: "Eine neue Serverversion ist verfügbar. Klicke auf den Downloadbutton.",
+ korean: "새로운 서버 버전을 사용할 수 있습니다. 다운로드 버튼을 클릭하십시오."
+ },
+ calculatorUpdate: {
+ english: "A new calculator version is available. Click the download button.",
+ german: "Eine neue Version des Warenrechners ist verfügbar. Klicke auf den Downloadbutton.",
+ korean: "새로운 Anno 1404 계산기 버전이 제공됩니다. 다운로드 버튼을 클릭하십시오."
+ },
+ newFeature: {
+ english: "NEU: Zusatzwarenverwaltung, Zeitungseffekt und Handelsrouten. Alles drei muss erst über die Einstellungen aktiviert werden. Über das neue Fabrikkonfigurationsmenü können Routen erstellt, Items ausgerüstet und Zusatzwaren angewendet werden. Siehe die Hilfe für weitere Informationen.",
+ german: "NEW: Extra goods management, newspaper effects and trade routes. All three features must be activated in the settings. From the new factory configuration dialog one can create routes, equip items, and apply extra goods. See the help for more information.",
+ },
+ helpContent: {
+ german:
+ `
Verwendung und Aufbau
+
Trage die aktuellen oder angestrebten Einwohner pro Stufe in die oberste Reihe ein. Die Produktionsketten aktualisieren sich automatisch, sobald man die Eingabe verlässt. Es werden nur diejenigen Fabriken angezeigt, die benötigt werden. In den Einstellungen kann zwischen der Eingabe von Einwohnerzahl und Häusern umgeschaltet werden. Im Fall von Einwohnern geht der Rechner davon aus, dass sämtliche Häuser voll sind.
+
Der Buchstabe in eckigen Klammern vor dem Bevölkerungsnamen ist der Hotkey zum Fokussieren des Eingabefeldes. Die Anzahl dort kann ebenfalls durch Drücken der Pfeiltasten erhöht und verringert werden.
+
Danach folgen zwei große Abschnitte, die sich wiederum in Unterabschnitte unterteilen. Der erste gibt einen Überblick über alle benötigten Gebäude, sortiert nach dem produzierten Warentyp. Der zweite schlüsselt die einzelnen Produktionsketten nach Bevölkerungsstufen auf. Jeder der Abschnitte kann durch einen Klick auf die Überschrift zusammengeklappt werden. Durch das Abwählen des Kontrollkästchens wird das entsprechende Bedürfnis gesperrt.
+
In jeder Kachel wird der Name der Fabrik, das Icon der hergestellten Ware, der Boost für den Gebäudetyp, die Anzahl der benötigten Gebäude und die Produktionsrate in Tonnen pro Minute angezeigt. Die Anzahl der Gebäude wird, wenn aktiviert, mit zwei Nachkommastellen angezeigt, um die Höhe der Überkapazitäten direkt ablesen zu können. Daneben befinden sich zwei Buttons
+
+
+. Diese versuchen den Boost so einzustellen, dass alle Gebäude des Typs bestmöglich ausgelastet sind und dabei ein Gebäude mehr (+) bzw. eines weniger (-) benötigt wird.
+
Da Baumaterialien sich Zwischenmaterialien mit Konsumgütern teilen sind sie (im Gegensatz zu Warenrechnern früherer Annos) mit aufgeführt, um so den Verbrauch von Minen besser planen zu können. Es muss die Anzahl der Endbetriebe per Hand eingegeben werden.
+
+
Globale Einstellungen
+
+
+
+
+
+
+
Die Buttons rechts in der Navigationsleiste dienen zur Verwaltung des Warenrechners. Sie schalten in den Dark-Mode um, öffnen das Einstellungsmenü, zeigen die Hilfe oder öffnen den Download-Dialog. In den Einstellungen kann die Sprache ausgewählt und die Menge der dargestellten Informationen angepasst werden. Im Downloadbereich kann die Konfiguration (Einstellungen, Inseln, Boosts, Gebäude, ...) importiert und exportiert werden. Außerdem können dieser Rechner sowie eine zusätzliche Serveranwendung heruntergeladen werden. Mit der Serveranwendung lassen sich die Bevölkerungszahlen, vorhandenen Gebäude, Inseln und Produktivitäten automatisch aus dem Spiel auslesen.
+
+
Produktionsketten
+
+
+
+
In diesem Dialog kann ausgewählt werden, von welcher Fabrik eine Ware hergestellt werden soll, falls es mehrere Möglichkeiten gibt.
+
+
Inselverwaltung und Handelsrouten
+
Selected Island
+
+
Als erstes muss über das Zahnrad die Inselverwaltung geöffnet werden. Dort können dann neue Inseln erstellt werden. Wer den Serveranwendung verwendet, erhält dort Vorschläge für Inseln (basieren darauf, welche Inselnamen der Server im Statistikmenü gesehen hat). Mit dem Erstellen der ersten Insel werden in der Mitte der Navigationsleiste neue Bedienelemente angezeigt: Wechseln der Insel, Inselverwaltung öffnen und Handelsroutenmenü öffnen. Der Button Alles löschen setzt den Warenrechner auf Werkseinstellungen zurück.
+
+
+
Um Handelsrouten verwenden zu können, muss zunächst die entsprechende Einstellung aktiviert werden. Mit der Option werden in den Fabrikkacheln zwei zusätzliche Informationen angezeigt: Ein Eingabefeld, das den Zusatzbedarf darstellt, welcher sich aus Importen / Exporten von Handelsrouten zusammensetzt. Außerdem wird unter dem Strich der (benötigte) Output der Fabrik angezeigt (was im Ausgangslager der Fabrik erzeugt wird). Darin berücksichtigt sind der Bedarf auf der aktuellen Insel sowie Im- und Exporte.
+
Mit der Einstellung automatisch anwenden wird der Zusatzbedarf bei Änderung der Werte von Handelsrouten angepasst. Dabei ist allerdings Vorsicht geboten, da es in seltenen Fällen zu Endlosschleifen beim Updaten kommen kann. Dies kann sich dadurch ausdrücken, dass der Warenrechner nur sehr langsam reagiert oder Elemente flackern. In solchen Fällen empfiehlt es sich, das automatische Anwenden kurzzeitig zu deaktivieren. Sollte das Problem fortbestehen, exportieren Sie bitte die Konfiguration des Warenrechners, eröffnen ein Issue auf GitHub (siehe unten) und fügen die Konfiguration an.
+
7 t/min
5 t/min
+
+
+
Das Erstellen von Handelsrouten erfolgt über den Konfigurationsdialog einer Fabrik, die diese Ware normalerweise herstellt. Handelsrouten gibt es in zwei Ausführungen. Zum einen können Waren der Händler eingekauft werden. Durch Auswählen des Kästchens neben dem Händler wird die Route erstellt. Die zweite Möglichkeit ist ein Warentransfer zwischen Inseln. Wie bei Zusatzwaren werden dafür der Bedarf auf der einen Seiter erhöht und auf der anderen erniedrigt. Öffnet man den Dialog, wird die Überproduktion direkt in das Eingabefeld zum Erstellen einer neuen Handelsroute übernommen. Ändern sich Produktion oder Bedarf nachträglich, so werden neben geeigneten Handelsrouten Buttons angezeigt, um die Differenz zu übernehmen. Stellt man von Anfang an einen höheren Warentransfer ein als die Zielinsel verbraucht und ist automatisch anwenden aktiviert, so wird der (negative) Zusatzbedarf angepasst, wenn sich der Verbrauch auf der Zielinsel erhöht. Ein im Eingabefeld weist daraufhin, dass die Quellinsel nicht genug produziert, um die Route vollständig zu bedienen.
+
+
+
Das Handelsroutenmenü enthält eine Übersicht über alle Handelsrouten, in der Reihenfolge der Erstellung. Dort können Handelsrouten außerdem gelöscht und die Transportmenge angepasst werden.
+Es gilt zu beachten, dass Routen an Fabriken gekoppelt sind. Dies bedeutet, dass der Import von derjenigen Fabrik erfolgen muss, von der es auf der anderen Insel produziert wird. Hierfür muss auf der importierenden Insel der Bedarf der richtigen Fabrik zugeordnet werden. Dies lässt sich über
+
+
+
+ in der Navigationsleiste einstellen. Andernfalls kann es z.B. passieren, dass vorhandene Gebäude bei Kohleminen eingetragen sind, der Bedarf aber bei Köhlereien anfällt. Grundsätzlich lässt sich schwer abbilden, wenn dieselbe Ware von verschiedenen Fabriktypen hergestellt wird. In solchen Fällen ist es empfehlenswert, sich im Warenrechner nur auf einen Fabriktyp zu beschränken und die Produktion der anderen per künstlicher Handelsroute von einer künstlichen Insel zu simulieren.
+
+
+
Enter the current or desired number of residents per level into the topmost row. The production chains will update automatically when one leaves the input field. Only the required factories are displayed. In the settings one can switch between population and houses count. In case of population count the calculator assumes that houses are full.
+
The letter in square brackets before the resident's name is the hotkey to focus the input field. There, one can use the arrow keys to inc-/decrement the number.
+
Afterwards two big sections follow that are subdivided into smaller sections. The first one gives an overview of the required buildings sorted by the type of good that is produced. The second one lists the individual production chains for each population level. Clicking the heading collapses each section. Deselecting the checkbox leads to locking the need.
+
Each card displays the name of the factory, the icon of the produced good, the boost for the given type of building, the number of required buildings, and the production rate in tons per minute. The number of buildings has two decimal places to directly show the amount of overcapacities. There are two buttons next to it.
+
+
+ Those try to adjust the boost such that all buildings operate at full capacity and one more (+) or one building less (-) is required.
+
Since construction materials share intermediate products with consumables they are explicitly listed (unlike in calculators for previous Annos) to better plan the production of mines. The number of factories must be entered manually.
+
+
Global Settings
+
+
+
+
+
+
+
The buttons on the right of the navigation bar serve the purpose of managing the calculator. They toggle dark mode, open settings, show the help or open the download dialog. The language and the amount of displayed information can be adjusted in the settings. In the download area one can import and export the configuration (settings, islands, boost, buildings, ...). Moreover, this calculator and an additional server application can be downloaded. The server application reads population count, constructed buildings, islands, and productivity automatically from the game.
+
+
Production Chains
+
+
+
In this dialog one can choose which product should be produced by which factory, in case several factories produce the same product.
+
+
Island and Trade Route Management
+
Selected Island
+
+
First, one must open the island management dialog by clicking the cogwheel. One can create new islands there. When using the server application suggestions for new islands get listed (based on those island names the server has seen on the statistics screen). After creating the first island three new control elements show up in the center of the navigation bar: Switch island, open island management, and open trade route management. New islands are associated with a session. The session influences which population levels, factories, items and need consumption effects show up. The button Delete All resets the calculator to its initial state.
+
+
+
Enable the setting trade routes to create and manage them. The option adds two pieces of information to the factory tiles: An input field that displays the extra demand which is put together form imports and exports. The value below the line shows the (required) factory output (what is generated in the factory output storage). This includes the demand on the island, imports, and exports. If the setting automatically apply is active and the values for trade route imports/exports change, the extra demand is automatically adjusted. But be careful when using this setting. In rare cases the calculator might caught up in an infinite loop to update the values. Effects are that the calculator responds slowly or elements flicker. In these cases, deactivate the setting briefly. If problems persist, please export the config, open an issue on GitHub (see below) and attach the config.
+
7 t/min
5 t/min
+
+
+
Trade routes are created from the factory configuration dialog of a factory that normally produces this product. There are two kinds of trade routes. The first kind are routes to purchase goods from a trader. Selecting the checkbox next to the trader creates such a route. The second kind are routes to transfer goods between islands. Like for extra goods, the extra demand is increased on one side and decreased on the other. When opening the factory configuration dialog, the calculator enters the overproduction into the amount input field for a new trade route. When production or island demand change, buttons show up next to suitable trade routes that allow to add the difference. Let us assume that one imports more goods than consumed on an island and that automatically apply is turned on. If the consumption on the island increases the extra demand will be updated automatically. A on an input field signals that the source island does not produce enough to fully supply the trade route.
+
+
+
The trade route menu contains an overview of all trade routes, listed in the order of creation. One can delete trade routes and adjust their load there.
+Please note that routes are attached to factories. This means that an import must be configured on the factory that produces the good on the source island. The demand must therefore be associated with the correct factory on the importing island. The settings can be changed via
+
+
+
+ in the navigation bar. Otherwise it may happen that for instance existing coal mines produce sufficient goods, but the demand is associated with charcoal kilns. It is not possible to produce one input good for one factory by different other factories. One must stick with one type of factory and simulate the production of other factories by artificial trade routes from artificial islands.
+
+
+