// To whomever is reading this code in the future
// It is organized pretty badly and I'm sorry for the pain you have reading it.
application = (function() {
var calc = exports.calculator;
var im = exports.imitator;
var BattleSide = {
Attacker: 'attacker',
Defender: 'defender',
};
var unitDisplayData = createUnitsDisplayData();
var canvasWidth = 600;
var canvasHeight = 400;
var lastResult = null;
var app = {
init: function() {
recreateFleetCounters();
createCanvases();
$("#compute-on-the-fly").on('change', computeOnTheFlyChanged);
$("#battle-type-place").find('button').on('click', changeBattleType);
$('#canvas-size-selector').on('change', selectedSizeChanged);
$("#sides-options").find('input').on('change', optionChanged);
$("#sides-options").find('select').on('change', optionChanged);
$("#shared-options").find('select').on('change', optionChanged);
$("#clear-counters-defender").on('click', clear);
$("#clear-counters-attacker").on('click', clear);
$("#force-compute").on('click', recompute);
$(window).bind('beforeunload', onBeforeUnload);
restoreState("all");
markParticipatingUnits();
recompute();
}
};
function recreateFleetCounters() {
var container = $("#fleet-counters");
container.find(".unit-selector").remove();
for (var typeName in calc.UnitType) {
if (calc.UnitType.hasOwnProperty(typeName)) {
var unitType = calc.UnitType[typeName];
var display = unitDisplayData[unitType];
var unitBlock = $("
")
.attr("data-unit-type", unitType)
.append(createDamagedDropdown(BattleSide.Attacker, unitType))
.append(createCounter(BattleSide.Attacker, unitType, "battle-values", display.battleValue))
.append(createCounter(BattleSide.Attacker, unitType, "units"))
.append($(" ")
.text(display.displayName))
.append(createCounter(BattleSide.Defender, unitType, "units"))
.append(createCounter(BattleSide.Defender, unitType, "battle-values", display.battleValue))
.append(createDamagedDropdown(BattleSide.Defender, unitType));
container.append(unitBlock);
}
}
container.find('div[data-unit-type="pds"] div[data-battle-side="defender"] button').on('click', markParticipatingUnits);
container.find('div[data-unit-type="ground"] div[data-battle-side="attacker"] button').on('click', markParticipatingUnits);
container.find('div[data-unit-type="mech"] div[data-battle-side="attacker"] button').on('click', markParticipatingUnits);
function createDamagedDropdown(battleSide, unitType) {
var result = $("");
if (calc.units[unitType].isDamageable || unitType === calc.UnitType.Cruiser || unitType === calc.UnitType.Carrier) {
result.addClass("damaged");
result.attr("data-battle-side", battleSide);
var select = $("
")
.attr("data-counter-type", "damaged")
.attr("data-unit-type", unitType)
.on('change', attemptOnTheFlyRecompute);
result.append(select);
for (var i = 0; i <= calc.units[unitType].supply; i++) {
select.append($("" + i + " "));
}
}
return result;
}
function createCounter(battleSide, unitType, counterType, defaultBattleValue) {
var result = $("
")
.attr("data-battle-side", battleSide);
var isBattleValue = counterType === "battle-values";
result
.append($("
")
.append($("")
.append($("")
.attr("type", "button")
.text("-")
.click(getModifyCounterHandler(-1, isBattleValue, defaultBattleValue))))
.append($(" ")
.attr("type", isBattleValue ? "text" : "number")
.attr("data-unit-type", unitType)
.attr("data-counter-type", counterType)
.attr("min", isBattleValue ? null : "0")
.attr("readonly", isBattleValue ? "readonly" : null)
.attr("value", defaultBattleValue || 0)
.on('change', attemptOnTheFlyRecompute))
.append($("")
.append($("")
.attr("type", "button")
.text("+")
.click(getModifyCounterHandler(+1, isBattleValue, defaultBattleValue))
)
)
);
return result;
}
}
function markParticipatingUnits() {
$("#title-dreadnought").text(getDreadnoughtTitle());
var battleType = currentBattleType();
var options = getOptions();
var attackerFleet = getFleet(BattleSide.Attacker, options.attacker);
var defenderFleet = getFleet(BattleSide.Defender, options.defender);
$("#fleet-counters").find(".unit-selector").each(function(_, element) {
var unitType = $(element).attr("data-unit-type");
var attackerParticipates = unitDisplayData[unitType].availableFor(battleType, BattleSide.Attacker, options, defenderFleet, attackerFleet);
var defenderParticipates = unitDisplayData[unitType].availableFor(battleType, BattleSide.Defender, options, defenderFleet, attackerFleet);
$(element).removeClass("attacker-participates");
$(element).removeClass("defender-participates");
if (attackerParticipates)
$(element).addClass("attacker-participates");
if (defenderParticipates)
$(element).addClass("defender-participates");
var attackerDamageable = calc.units[unitType].isDamageable ||
options.attacker.enhancedArmor && unitType === calc.UnitType.Cruiser ||
options.attacker.advancedCarriers && unitType === calc.UnitType.Carrier;
var defenderDamageable = calc.units[unitType].isDamageable ||
options.defender.enhancedArmor && unitType === calc.UnitType.Cruiser ||
options.defender.advancedCarriers && unitType === calc.UnitType.Carrier;
$(element).removeClass("attacker-damageable");
$(element).removeClass("defender-damageable");
if (attackerDamageable)
$(element).addClass("attacker-damageable");
if (defenderDamageable)
$(element).addClass("defender-damageable");
})
}
function createCanvases() {
var createCanvas = function(id, zIndex) {
return $(" ")
.css("position", "absolute")
.css("z-index", zIndex);
};
$("#result-area").empty()
.append(createCanvas('chart-area', 1))
.append(createCanvas('chart-area-overlay', 0))
.css("width", canvasWidth + "px")
.css("height", canvasHeight + "px");
}
function currentBattleType() {
return $("#battle-type-place").find('button.active').attr("data-battle-type");
}
function recompute() {
var options = getOptions();
var fleet1 = getFleet(BattleSide.Attacker, options.attacker);
var fleet2 = getFleet(BattleSide.Defender, options.defender);
var battleType = currentBattleType();
var duraniumArmor = options.attacker.duraniumArmor || options.defender.duraniumArmor;
if (duraniumArmor)
lastResult = im.estimateProbabilities(fleet1, fleet2, battleType, options);
else
lastResult = calc.computeProbabilities(fleet1, fleet2, battleType, options);
displayLastResult();
}
function displayLastResult() {
if (lastResult)
displayDistribution(lastResult);
}
function changeBattleType() {
$(this).parent().find('button.active').removeClass('active');
$(this).addClass('active');
markParticipatingUnits();
attemptOnTheFlyRecompute();
}
function computeOnTheFlyChanged() {
var onTheFlyEnabled = $("#compute-on-the-fly").is(':checked');
$("#force-compute").prop('disabled', onTheFlyEnabled); //mutually exclusive with 'compute on the fly option'
attemptOnTheFlyRecompute();
}
function optionChanged() {
markParticipatingUnits();
attemptOnTheFlyRecompute();
}
function attemptOnTheFlyRecompute() {
persistState();
var onTheFlyEnabled = $("#compute-on-the-fly").is(':checked');
if (onTheFlyEnabled)
recompute();
}
function clear(e) {
var restoreSide = null;
if (e.currentTarget.attributes.dataBattleSide.value === "attacker") {
restoreSide = "defender";
} else if (e.currentTarget.attributes.dataBattleSide.value === "defender") {
restoreSide = "attacker";
}
recreateFleetCounters();
restoreState(restoreSide);
markParticipatingUnits();
attemptOnTheFlyRecompute();
e.stopPropagation();
}
function selectedSizeChanged() {
var selection = $('#canvas-size-selector').val();
switch (selection) {
case "600x400":
canvasWidth = 600;
canvasHeight = 400;
break;
case "900x600":
canvasWidth = 900;
canvasHeight = 600;
break;
case "1200x800":
canvasWidth = 1200;
canvasHeight = 800;
break;
case "1600x1200":
canvasWidth = 1600;
canvasHeight = 1200;
break;
}
createCanvases();
displayLastResult();
}
function displayDistribution(calcRes) {
var distribution = calcRes.distribution;
var labels = [];
var data = [];
var dataLabels = [];
var from = Math.min(-8, distribution.min());
var to = Math.max(8, distribution.max());
for (var i = from; i <= to; ++i) {
labels.push(getLabel(i, calcRes.attacker, calcRes.defender));
if (i === 0) {
var drawOrDeadlockProbability = distribution.at(0) + (distribution.deadlock || 0);
data.push(drawOrDeadlockProbability * 100);
dataLabels.push(Math.round(drawOrDeadlockProbability * 100).toString() + "%");
} else {
data.push(distribution.at(i) * 100);
dataLabels.push(Math.round(distribution.downTo(i) * 100).toString() + "%");
}
}
RGraph.clear(document.getElementById("chart-area"));
RGraph.clear(document.getElementById("chart-area-overlay"));
RGraph.ObjectRegistry.Clear();
var line = new RGraph.Line('chart-area', data)
.Set('labels', labels)
.Set('chart.background.grid.vlines', true)
.Set('chart.background.grid.autofit.numvlines', 1)
.Set('chart.filled', true)
.Set('chart.tickmarks', 'circle')
.Set('chart.numxticks', 0)
.Set('chart.ymax', _.max(data) * 1.08)
.Set('chart.colors', ['rgba(200,200,256,0.7)']);
if (to - from < 20)
line.Set('chart.labels.ingraph', dataLabels);
else
line.Set('chart.tooltips', dataLabels);
line.Draw();
// draw total percentages
var selector = function(index) { return distribution.at(index); };
var summator = function(memo, num) { return memo + num; };
var sumRange = function(range) { return _.reduce(_.map(range, selector), summator, 0); };
var attacherWinProbability = sumRange(_.range(distribution.min(), 0));
var defenderWinProbability = sumRange(_.range(1, distribution.max() + 1));
var canvas = $("#chart-area-overlay").get(0);
var context = canvas.getContext("2d");
context.font = "bold 100px Arial";
context.fillStyle = "rgba(256, 100, 100, 0.5)";
context.fillText(Math.round(attacherWinProbability * 100) + "%", canvasWidth / 12, 3 * canvasHeight / 4);
context.fillStyle = "rgba(100, 100, 256, 0.5)";
context.fillText(Math.round(defenderWinProbability * 100) + "%", 7 * canvasWidth / 12, 3 * canvasHeight / 4);
}
function getLabel(i, attacker, defender) {
if (i === 0)
return "=";
if (i < 0) {
i = -i;
if (i <= attacker.length)
return attacker[i - 1];
else
return "";
}
else {
if (i <= defender.length)
return defender[i - 1];
else
return "";
}
}
function getFleet(battleSide, options) {
var fleetDamaged = getCounters(battleSide, "damaged");
var fleetCounters = getCounters(battleSide, "units");
var fleetBattleValues = getCounters(battleSide, "battle-values");
var fleetDiceModifiers = getDiceModifiers(battleSide);
var fleetModifiers = {};
for (var typeName in fleetBattleValues)
if (fleetBattleValues.hasOwnProperty(typeName))
fleetModifiers[typeName] = unitDisplayData[typeName].battleValue - fleetBattleValues[typeName];
return calc.defaultSort(calc.expandFleet(fleetCounters, fleetDamaged, fleetModifiers, fleetDiceModifiers, options), options.gravitonNegator);
}
function getOptions() {
return {
attacker: getSideOptions(BattleSide.Attacker),
defender: getSideOptions(BattleSide.Defender)
};
}
function addSharedOptions(result) {
var shared = $("#shared-options").find("select");
shared.each(function() {
var select = $(this);
var selected = select[0].selectedOptions[0].value;
result[selected] = true;
});
}
function getSideOptions(battleSide) {
var inputs = $("#sides-options").find("input[data-battle-side='" + battleSide + "']");
var result = {admiral: getAdmiral(battleSide)};
inputs.each(function() {
var input = $(this);
var option = input.attr("data-option");
result[option] = input.is(':checked');
});
addSharedOptions(result);
return result;
function getAdmiral(battleSide) {
var inputs = $("#sides-options").find("select[data-battle-side='" + battleSide + "']");
var admiral = "none";
if (inputs.length > 0)
admiral = inputs[0].value;
return admiral;
}
}
function createUnitsDisplayData() {
var result = {};
result[calc.UnitType.WarSun] = {
displayName: "War Sun (3\uD83C\uDFB2)",
availableFor: function(battleType, battleSide) {
return battleType === calc.BattleType.Space ||
battleSide === BattleSide.Attacker;
},
};
var displayNameDreadnought = getDreadnoughtTitle();
result[calc.UnitType.Dreadnought] = {
displayName: displayNameDreadnought,
availableFor: function(battleType, battleSide, options, defenderFleet, attackerFleet) {
return battleType === calc.BattleType.Space ||
(
battleSide === BattleSide.Attacker && _.any(attackerFleet, function(unit) {
return unit.type === calc.UnitType.Ground || unit.type === calc.UnitType.Mech;
}) &&
(options.attacker.gravitonNegator || _.every(defenderFleet, function(unit) {
return unit.type !== calc.UnitType.PDS;
}))
);
},
};
result[calc.UnitType.Cruiser] = {
displayName: "Cruiser",
unavailable: {
in: calc.BattleType.Ground,
},
availableFor: function(battleType) {
return battleType === calc.BattleType.Space;
},
};
result[calc.UnitType.Destroyer] = {
displayName: "Destroyer",
availableFor: function(battleType) {
return battleType === calc.BattleType.Space;
},
};
result[calc.UnitType.Carrier] = {
displayName: "Carrier",
availableFor: function(battleType) {
return battleType === calc.BattleType.Space;
},
};
result[calc.UnitType.Fighter] = {
displayName: "Fighter",
availableFor: function(battleType, battleSide, options) {
return battleType === calc.BattleType.Space ||
battleSide === BattleSide.Attacker && options.attacker.gravitonNegator;
},
};
result[calc.UnitType.PDS] = {
displayName: "PDS",
availableFor: function(battleType, battleSide) {
return battleType === calc.BattleType.Space ||
battleSide === BattleSide.Defender;
},
};
result[calc.UnitType.Ground] = {
displayName: "Ground Force",
availableFor: function(battleType) {
return battleType === calc.BattleType.Ground;
},
};
result[calc.UnitType.Mech] = {
displayName: "Mech",
availableFor: function(battleType) {
return battleType === calc.BattleType.Ground;
},
};
for (var typeName in result)
if (result.hasOwnProperty(typeName))
result[typeName].battleValue = calc.units[typeName].dmgDice;
return result;
}
function getDreadnoughtTitle() {
var dice = getDiceModifiers(BattleSide.Attacker)[calc.UnitType.Dreadnought];
var displayNameDreadnought;
if (dice === 1) {
displayNameDreadnought = "Dreadnought";
}
else {
displayNameDreadnought = "Drdnght(" + dice + "\uD83C\uDFB2)"; // abbreviated as full word doesn't fit
}
return displayNameDreadnought;
}
function getCounters(battleSide, counterType) {
var controlType = counterType === "damaged" ? "select" : "input";
var inputs = $("#fleet-counters")
.find("[data-battle-side='" + battleSide + "']")
.find(controlType + "[data-counter-type='" + counterType + "']");
var result = {};
inputs.each(function() {
var input = $(this);
var unitType = input.attr("data-unit-type");
result[unitType] = input.val();
});
return result;
}
function getDiceModifiers(battleSide) {
var result = {};
var options = getOptions();
if (options.attacker.twoDiceDread && battleSide == BattleSide.Attacker) {
result[calc.UnitType.Dreadnought] = 2;
}
else if (options.defender.twoDiceDread && battleSide == BattleSide.Defender) {
result[calc.UnitType.Dreadnought] = 2;
}
else {
result[calc.UnitType.Dreadnought] = 1;
}
return result;
}
function getModifyCounterHandler(change, isBattleValue, defaultBattleValue) {
return function() {
var input = $(this).parent().siblings("input[type!='button']");
var val = Number(input.val()) + change;
if (!isBattleValue && val < 0)
val = 0;
if (isBattleValue) {
if (val < 1)
val = 1;
else if (val > 10)
val = 10;
if (val < defaultBattleValue)
input.removeClass("negative").addClass("positive");
else if (val > defaultBattleValue)
input.removeClass("positive").addClass("negative");
else
input.removeClass("negative").removeClass("positive");
}
input.val(val);
attemptOnTheFlyRecompute();
};
}
function onBeforeUnload() {
var stateSaved = persistState();
if (!stateSaved)
return 'This action will clear all entered values.';
}
function persistState() {
if (localStorage) {
var state = {
battleType: currentBattleType(),
options: getOptions(),
attackerCounters: getCounters(BattleSide.Attacker, "units"),
attackerBattleValues: getCounters(BattleSide.Attacker, "battle-values"),
attackerDamaged: getCounters(BattleSide.Attacker, "damaged"),
defenderCounters: getCounters(BattleSide.Defender, "units"),
defenderBattleValues: getCounters(BattleSide.Defender, "battle-values"),
defenderDamaged: getCounters(BattleSide.Defender, "damaged"),
};
localStorage.setItem("selectionState", JSON.stringify(state));
return true;
}
return false;
}
function restoreState(battleSide) {
if (!localStorage) return;
var state = localStorage.getItem("selectionState");
if (!state) return;
state = JSON.parse(state);
$("#battle-type-place").find('button.active').removeClass('active');
$("#battle-type-place").find('button[data-battle-type="' + state.battleType + '"]').addClass('active');
if (battleSide == "all" || battleSide == BattleSide.Attacker) {
restoreOptions(state.options.attacker, BattleSide.Attacker);
restoreAdmirals(state.options.attacker, BattleSide.Attacker);
restoreCounters(state.attackerCounters, BattleSide.Attacker, "units");
restoreCounters(state.attackerBattleValues, BattleSide.Attacker, "battle-values");
restoreCounters(state.attackerDamaged, BattleSide.Attacker, "damaged");
}
if (battleSide == "all" || battleSide == BattleSide.Defender) {
restoreAdmirals(state.options.defender, BattleSide.Defender);
restoreOptions(state.options.defender, BattleSide.Defender);
restoreCounters(state.defenderCounters, BattleSide.Defender, "units");
restoreCounters(state.defenderBattleValues, BattleSide.Defender, "battle-values");
restoreCounters(state.defenderDamaged, BattleSide.Defender, "damaged");
}
colorBattleValues();
selectedSizeChanged();
return;
function restoreAdmirals(options, battleSide) {
var inputs = $("#sides-options").find("select[data-battle-side='" + battleSide + "']");
inputs.each(function() {
var input = $(this);
input.val(options["admiral"]);
});
}
function restoreOptions(options, battleSide) {
var inputs = $("#sides-options").find("input[data-battle-side='" + battleSide + "']");
inputs.each(function() {
var input = $(this);
var option = input.attr("data-option");
input.prop('checked', options[option]);
});
// shared will be set twice, but who cares
var shared = $("#shared-options").find("select");
shared.each(function () {
var select = $(this);
select.find("option").each(function () {
var option = $(this);
if (options[option.val()])
select.val(option.val());
});
});
}
function restoreCounters(counters, battleSide, counterType) {
if (!counters)
return;
var controlType = counterType === "damaged" ? "select" : "input";
var inputs = $("#fleet-counters")
.find("[data-battle-side='" + battleSide + "']")
.find(controlType + "[data-counter-type='" + counterType + "']");
inputs.each(function() {
var input = $(this);
var unitType = input.attr("data-unit-type");
input.val(counters[unitType]);
});
}
function colorBattleValues() {
for (var typeName in calc.UnitType) {
if (calc.UnitType.hasOwnProperty(typeName)) {
colorBattleValue(BattleSide.Attacker, typeName);
colorBattleValue(BattleSide.Defender, typeName);
}
}
return;
function colorBattleValue (battleSide, unitTypeName) {
var unitType = calc.UnitType[unitTypeName];
var display = unitDisplayData[unitType];
var unitValueInput = $(".unit-selector [data-battle-side='" + battleSide + "'] [data-counter-type='battle-values'][data-unit-type='" + unitType + "']");
var val = parseInt(unitValueInput.val());
if (val < display.battleValue)
unitValueInput.removeClass("negative").addClass("positive");
else if (val > display.battleValue)
unitValueInput.removeClass("positive").addClass("negative");
else
unitValueInput.removeClass("negative").removeClass("positive");
}
}
}
return app;
})();