This commit is contained in:
Azgaar 2020-05-05 02:00:40 +03:00
parent 5304306044
commit beb2d0ad7c
14 changed files with 469 additions and 108 deletions

View file

@ -250,7 +250,6 @@
.icon-smooth:before {font-weight: bold;content:'';}
.icon-disrupt:before {font-weight: bold;content:'⥄';}
.icon-if:before {font-style: italic; font-weight: bold;content:'if';}
/* .icon-coa:before {content: '⚜'; font-size: 1.1em; margin: -2px;} */
.icon-coa:before {content:'\f3ed'; font-size: .9em; color: #999;} /* '' */
.icon-half:before {font-weight: bold;content:'½';}
.icon-curve:before {content: 'C';}
@ -265,11 +264,9 @@
font-family: monospace;
}
.icon-die:before {content:'🎲';}
.icon-button-plus:before {content:''; padding-right: .4em;}
.icon-button-die:before {content:'🎲'; padding-right: .4em;}
.icon-button-power:before {content:'💪'; padding-right: .6em;}
.icon-button-skirmish:before {content:'🎯'; padding-right: .4em;}
.icon-button-melee:before {content:'🗡'; padding-right: .4em;}
.icon-button-melee:before {content:'⚔️'; padding-right: .4em;}
.icon-button-pursue:before {content:'🐎'; padding-right: .4em;}
.icon-button-retreat:before {content:'🏳️'; padding-right: .4em;}

View file

@ -583,6 +583,7 @@ button.options {
font-weight: bold;
float: left;
border: none;
border-radius: 0;
padding: 8px 10px;
transition: 0.2s;
}
@ -945,10 +946,11 @@ body button.noicon {
#battleBody > table {
padding: .2em .6em .2em .6em;
border: 1px solid #ccc;
margin: 0 0 .4em 0;
margin: .2em 0 .4em 0;
display: block;
overflow: auto;
max-height: 34vh;
width: 100%;
}
#battleBody > table .regiment {
@ -961,6 +963,45 @@ tr.battleCasualties, tr.battleSurvivors {
font-size: .9em;
}
#battleBody div.battlePhases {
position: absolute;
background-color: #fff;
}
#battleBody div.battlePhases > button {
width: 3.2em;
display: block;
margin: .2em 0;
}
div#regimentSelectorBody {
max-height: 50vh;
font-size: .9em;
}
div#regimentSelectorBody > div {
padding: .1em;
border: 1px solid #fff;
}
div#regimentSelectorBody > div:hover {
border: 1px solid #ccc;
}
div#regimentSelectorBody > div.selected {
border: 1px solid #b28585;
}
div#regimentSelectorBody > div.inactive {
background-color: #eee;
color: #aaa;
}
div#regimentSelectorBody > div > div {
display: inline-block;
pointer-events: none;
}
.drag-trigger {
border-left: 1em solid transparent;
border-right: 1em solid #000;

View file

@ -2448,34 +2448,62 @@
<div id="battleScreen" class="dialog stable" style="display: none">
<div id="battleBody">
<div style="font-size:1.2em; font-weight: bold">
<div style="width: 1.2em; display: inline-block"></div><span>Attackers</span>
<span>Attackers</span>
<div style="float: right; font-size: .7em">
<span data-tip="Attackers power considering phase and randon factor" style="padding: .2em" class="icon-button-power">43</span>
<button data-tip="Battle phase. Click to change" class="icon-button-pursue"></button>
<button data-tip="Random factor for attackers. Click to re-roll" style="padding: .1em .2em" class="icon-button-die">12</button>
<meter id="battleMorale_attackers" data-tip="Attackers morale: " min=0 max=100 low=33 high=66 optimum=80 style="padding: .1em"></meter>
<div id="battlePower_attackers" data-tip="Attackers strength during this phase" style="width: 3.2em; display: inline-block; text-align: center" class="icon-button-power"></div>
<div style="display: inline-block;">
<button id="battlePhase_attackers" data-tip="Battle phase. Click to change" class="icon-button-pursue" style="width: 3.2em"></button>
<div class="battlePhases" style="display: none">
<button data-tip="Skirmish phase. Ranged units excel" data-phase="skirmish" class="icon-button-skirmish"></button>
<button data-tip="Melee phase. Melee units excel" data-phase="melee" class="icon-button-melee"></button>
<button data-tip="Pursue phase. Mounted units excel" data-phase="pursue" class="icon-button-pursue"></button>
<button data-tip="Retreat phase. All units strength reduced" data-phase="retreat" class="icon-button-retreat"></button>
</div>
</div>
<button id="battleDie_attackers" data-tip="Random factor for attackers. Click to re-roll" style="padding: .1em .2em; width: 3.2em" class="icon-button-die"></button>
</div>
</div>
<table id="battleAttackers"><thead><tr></tr></thead><tbody></tbody></table>
<table id="battleAttackers"></table>
<div style="font-size:1.2em; font-weight: bold">
<div style="width: 1.2em; display: inline-block">🛡</div><span>Defenders</span>
<span></span>Defenders</span>
<div style="float: right; font-size: .7em">
<span data-tip="Defenders power considering phase and randon factor" style="padding: .2em" class="icon-button-power">65</span>
<button data-tip="Battle phase. Click to change" class="icon-button-skirmish"></button>
<button data-tip="Random factor for defenders. Click to re-roll" style="padding: .1em .2em" class="icon-button-die">05</button>
<meter id="battleMorale_defenders" data-tip="Defenders morale: " min=0 max=100 low=33 high=66 optimum=80 style="padding: .1em"></meter>
<div id="battlePower_defenders" data-tip="Defenders strength during this phase" style="width: 3.2em; display: inline-block; text-align: center" class="icon-button-power"></div>
<div style="display: inline-block;">
<button id="battlePhase_defenders" data-tip="Battle phase. Click to change" class="icon-button-pursue" style="width: 3.2em"></button>
<div class="battlePhases" style="display: none">
<button data-tip="Skirmish phase. Ranged units excel" data-phase="skirmish" class="icon-button-skirmish"></button>
<button data-tip="Melee phase. Melee units excel" data-phase="melee" class="icon-button-melee"></button>
<button data-tip="Pursue phase. Mounted units excel" data-phase="pursue" class="icon-button-pursue"></button>
<button data-tip="Retreat phase. All units strength reduced" data-phase="retreat" class="icon-button-retreat"></button>
</div>
</div>
<button id="battleDie_defenders" data-tip="Random factor for defenders. Click to re-roll" style="padding: .1em .2em; width: 3.2em" class="icon-button-die"></button>
</div>
</div>
<table id="battleDefenders"><thead><tr></tr></thead><tbody></tbody></table>
<table id="battleDefenders"></table>
</div>
<div id="battleBottom">
<button id="battleAddRegiment" data-tip="Add regiment to the battle" class="icon-user-plus"></button>
<button id="battleRoll" data-tip="Roll dice to randomize sides power and battle phase" class="icon-die"></button>
<button id="battleNext" data-tip="Calculate and apply phase results" class="icon-play"></button>
<button id="battleApply" data-tip="Apply battle results" class="icon-check"></button>
<button id="battleCancel" data-tip="Cancel battle results and restore initial troop value" class="icon-cancel"></button>
<button id="battleRoll" data-tip="Roll dice to update random factor" class="icon-die"></button>
<button id="battleRun" data-tip="Iterate battle" class="icon-play"></button>
<button id="battleApply" data-tip="Apply battle results and close the screen" class="icon-check"></button>
<button id="battleCancel" data-tip="Cancel battle results and close the screen" class="icon-cancel"></button>
</div>
</div>
<div id="regimentSelectorScreen" class="dialog" style="display: none">
<div id="regimentSelectorHeader" class="header">
<div style="left: 1.2em;" data-tip="Click to sort by state name" class="sortable alphabetically" data-sortby="state">State&nbsp;</div>
<div style="left: 9.2em;" data-tip="Click to sort by regiment name" class="sortable alphabetically" data-sortby="regiment">Regiment&nbsp;</div>
<div style="left: 22.4em;" data-tip="Click to sort by total military forces" class="sortable" data-sortby="total">Total&nbsp;</div>
<div style="left: 28em;" data-tip="Click to sort by distance to the battlefield" class="sortable icon-sort-number-up" data-sortby="distance">Distance&nbsp;</div>
</div>
<div id="regimentSelectorBody"></div>
</div>
<div id="brushesPanel" class="dialog stable" style="display: none">
<div id="brushesButtons" style="display: inline-block">
<button id="brushRaise" data-tip="Raise brush: increase height of cells in radius by Power value">
@ -2859,6 +2887,7 @@
<div id="provincesBottom">
<button id="provincesEditorRefresh" data-tip="Refresh the Editor" class="icon-cw"></button>
<button id="provincesEditStyle" data-tip="Edit provinces style in Style Editor" class="icon-adjust"></button>
<button id="provincesRecolor" data-tip="Recolor listed provinces based on state color" class="icon-paint-roller"></button>
<button id="provincesPercentage" data-tip="Toggle percentage / absolute values views" class="icon-percent"></button>
<button id="provincesChart" data-tip="Show provinces chart" class="icon-chart-area"></button>
<button id="provincesToggleLabels" data-tip="Toggle province labels" class="icon-font"></button>
@ -3247,7 +3276,7 @@
<input id="populationRate" data-stored="populationRate" type="number" min=10 max=9990 step=10 value=1000 data-value=1000 style="width:4.5em">
</div>
<div data-tip="Set ubranization rate: burgs population relative to all population">
<div data-tip="Set urbranization rate: burgs population relative to all population">
<div>Urbanization rate:</div>
<input id="urbanizationOutput" type="range" min=.01 max=5 step=.01 value=1>
<input id="urbanization" data-stored="urbanization" type="number" min=.01 max=5 step=.01 value=1 data-value=1>

View file

@ -190,7 +190,7 @@
const regiments = nodes.filter(n => n.t).sort((a,b) => b.t - a.t).map((r, i) => {
const u = {}; u[r.u] = r.a;
(r.childen||[]).forEach(n => u[n.u] = u[n.u] ? u[n.u] += n.a : n.a);
return {i, a:r.t, cell:r.cell, x:r.x, y:r.y, bx:r.x, by:r.y, u, n:r.n, name};
return {i, a:r.t, cell:r.cell, x:r.x, y:r.y, bx:r.x, by:r.y, u, n:r.n, name, state: s.i};
});
// generate name for regiments
@ -210,9 +210,9 @@
return [
{icon: "⚔️", name:"infantry", rural:.25, urban:.2, crew:1, power:1, type:"melee", separate:0},
{icon: "🏹", name:"archers", rural:.12, urban:.2, crew:1, power:1, type:"ranged", separate:0},
{icon: "🐴", name:"cavalry", rural:.12, urban:.03, crew:3, power:4, type:"mounted", separate:0},
{icon: "💣", name:"artillery", rural:0, urban:.03, crew:8, power:12, type:"machinery", separate:0},
{icon: "🌊", name:"fleet", rural:0, urban:.015, crew:100, power:50, type:"naval", separate:1}
{icon: "🐴", name:"cavalry", rural:.12, urban:.03, crew:2, power:2, type:"mounted", separate:0},
{icon: "💣", name:"artillery", rural:0, urban:.03, crew:8, power:12, type:"machinery", separate:0},
{icon: "🌊", name:"fleet", rural:0, urban:.015, crew:100, power:50, type:"naval", separate:1}
];
}
@ -256,6 +256,26 @@
g.append("text").attr("class", "regimentIcon").attr("x", x1-size).attr("y", reg.y).text(reg.icon);
}
// move one regiment to another
const moveRegiment = function(reg, x, y) {
const el = armies.select("g#army"+reg.state).select("g#regiment"+reg.state+"-"+reg.i);
if (!el.size()) return;
const duration = Math.hypot(reg.x - x, reg.y - y) * 8;
reg.x = x; reg.y = y;
const size = +armies.attr("box-size");
const w = reg.n ? size * 4 : size * 6;
const h = size * 2;
const x1 = x => rn(x - w / 2, 2);
const y1 = y => rn(y - size, 2);
const move = d3.transition().duration(duration).ease(d3.easeSinInOut);
el.select("rect").transition(move).attr("x", x1(x)).attr("y", y1(y));
el.select("text").transition(move).attr("x", x).attr("y", y);
el.selectAll("rect:nth-of-type(2)").transition(move).attr("x", x1(x)-h).attr("y", y1(y));
el.select(".regimentIcon").transition(move).attr("x", x1(x)-size).attr("y", y);
}
// utilize si function to make regiment total text fit regiment box
const getTotal = reg => reg.a > (reg.n ? 999 : 99999) ? si(reg.a) : reg.a;
@ -304,6 +324,6 @@
// note.legend = note.legend.replace(oldComposition, newComposition);
// }
return {generate, getDefaultOptions, getName, generateNote, drawRegiments, drawRegiment, getTotal, getEmblem};
return {generate, getDefaultOptions, getName, generateNote, drawRegiments, drawRegiment, moveRegiment, getTotal, getEmblem};
})));

View file

@ -33,15 +33,15 @@
const relaxed = chain.filter((v, i) => !(i%relax) || vertices.c[v].some(c => c >= pointsN));
if (relaxed.length < 4) continue;
const points = clipPoly(relaxed.map(v => vertices.p[v]), 1);
const inside = d3.polygonContains(points, grid.points[i]);
chains.push([t, points, inside]);
//const inside = d3.polygonContains(points, grid.points[i]);
chains.push([t, points]); //chains.push([t, points, inside]);
}
const bbox = `M0,0h${graphWidth}v${graphHeight}h${-graphWidth}Z`;
//const bbox = `M0,0h${graphWidth}v${graphHeight}h${-graphWidth}Z`;
for (const t of limits) {
const layer = chains.filter(c => c[0] === t);
let path = layer.map(c => round(lineGen(c[1]))).join("");
if (layer.every(c => !c[2])) path = bbox + path; // add outer ring if all segments are outside (works not for all cases)
//if (layer.every(c => !c[2])) path = bbox + path; // add outer ring if all segments are outside (works not for all cases)
if (path) oceanLayers.append("path").attr("d", path).attr("fill", "#ecf2f9").style("opacity", opacity);
}

View file

@ -1018,6 +1018,9 @@ function parseLoadedData(data) {
if (type === "magical") return "🔮";
else return "⚔️";
}
// 1.4 added state reference for regiments
pack.states.filter(s => s.military).forEach(s => s.military.forEach(r => r.state = s.i));
}
}()

View file

@ -1,53 +1,83 @@
"use strict";
function showBattleScreen(attacker, defender) {
if (customization) return;
closeDialogs(".stable");
class Battle {
const battle = {name:"Battle", attackers:[attacker], defenders:[defender]};
const battleAttackers = document.getElementById("battleAttackers");
const battleDefenders = document.getElementById("battleDefenders");
addHeaders();
addRegiment(battleAttackers, attacker);
addRegiment(battleDefenders, defender);
constructor(attacker, defender) {
if (customization) return;
closeDialogs(".stable");
customization = 13; // enter customization to avoid unwanted dialog closing
$("#battleScreen").dialog({
title: battle.name, resizable: false, width: fitContent(), close: closeBattleScreen,
position: {my: "center", at: "center", of: "#map"}
});
Battle.prototype.context = this; // store context
this.x = defender.x;
this.y = defender.y;
this.name = this.getBattleName();
this.iteration = 0;
this.attackers = {regiments:[], distances:[], morale:100};
this.defenders = {regiments:[], distances:[], morale:100};
if (modules.showBattleScreen) return;
modules.showBattleScreen = true;
this.addHeaders();
this.addRegiment("attackers", attacker);
this.addRegiment("defenders", defender);
this.randomize();
this.calculateStrength("attackers");
this.calculateStrength("defenders");
this.getInitialMorale();
// add listeners
document.getElementById("battleAddRegiment").addEventListener("click", addSide);
$("#battleScreen").dialog({
title: this.name, resizable: false, width: fitContent(), close: this.closeBattleScreen,
position: {my: "center", at: "center", of: "#map"}
});
function addHeaders() {
document.getElementById("battleScreen").querySelectorAll("th").forEach(el => el.remove());
const attackers = battleAttackers.querySelector("tr");
const defenders = battleDefenders.querySelector("tr");
let headers = "<th></th><th></th>";
if (modules.Battle) return;
modules.Battle = true;
// add listeners
document.getElementById("battleAddRegiment").addEventListener("click", this.addSide);
document.getElementById("battleRoll").addEventListener("click", () => Battle.prototype.context.randomize());
document.getElementById("battleRun").addEventListener("click", () => Battle.prototype.context.run());
document.getElementById("battleApply").addEventListener("click", () => Battle.prototype.context.applyResults());
document.getElementById("battleCancel").addEventListener("click", () => Battle.prototype.context.cancelResults());
document.getElementById("battlePhase_attackers").addEventListener("click", ev => this.toggleChangePhase(ev, "attackers"));
document.getElementById("battlePhase_attackers").nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changePhase(ev, "attackers"));
document.getElementById("battlePhase_defenders").addEventListener("click", ev => this.toggleChangePhase(ev, "defenders"));
document.getElementById("battlePhase_defenders").nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changePhase(ev, "defenders"));
document.getElementById("battleDie_attackers").addEventListener("click", () => Battle.prototype.context.rollDie("attackers"));
document.getElementById("battleDie_defenders").addEventListener("click", () => Battle.prototype.context.rollDie("defenders"));
}
getBattleName() {
const cell = findCell(this.x, this.y);
const burg = pack.cells.burg[cell] ? pack.burgs[pack.cells.burg[cell]].name : null;
return burg ? burg + " Battle" : Names.getCulture(pack.cells.culture[cell]) + " Battle"
}
addHeaders() {
let headers = "<thead><tr><th></th><th></th>";
for (const u of options.military) {
const label = capitalize(u.name.replace(/_/g, ' '));
headers += `<th data-tip="${label}">${u.icon}</th>`;
}
headers += "<th>Total</th>";
attackers.insertAdjacentHTML("beforebegin", headers);
defenders.insertAdjacentHTML("beforebegin", headers);
headers += "<th data-tip='Total military''>Total</th></tr></thead>";
battleAttackers.innerHTML = battleDefenders.innerHTML = headers;
}
function addRegiment(div, regiment) {
const state = ra(pack.states), supply = rand(1000) + " " + distanceUnitInput.value;
addRegiment(side, regiment) {
regiment.casualties = Object.keys(regiment.u).reduce((a,b) => (a[b]=0,a), {});
regiment.survivors = Object.assign({}, regiment.u);
const state = pack.states[regiment.state];
const distance = Math.hypot(this.y-regiment.by, this.x-regiment.bx) * distanceScaleInput.value | 0; // distance between regiment and its base
const color = state.color[0] === "#" ? state.color : "#999";
const icon = `<svg width="1.4em" height="1.4em" style="margin-bottom: -.6em;">
const icon = `<svg width="1.4em" height="1.4em" style="margin-bottom: -.6em">
<rect x="0" y="0" width="100%" height="100%" fill="${color}" class="fillRect"></rect>
<text x="0" y="1.04em" style="">${regiment.icon}</text></svg>`;
const body = `<tbody id="battle${state.i}-${regiment.i}">`;
let initial = `<tr class="battleInitial"><td>${icon}</td><td class="regiment">${regiment.name.slice(0,25)}</td>`;
let casualties = `<tr class="battleCasualties"><td></td><td>${state.fullName}</td>`;
let survivors = `<tr class="battleSurvivors"><td></td><td>Supply line length: ${supply}</td>`;
let initial = `<tr class="battleInitial"><td>${icon}</td><td class="regiment">${regiment.name.slice(0, 24)}</td>`;
let casualties = `<tr class="battleCasualties"><td></td><td>${state.fullName.slice(0, 26)}</td>`;
let survivors = `<tr class="battleSurvivors"><td></td><td>Distance to base: ${distance} ${distanceUnitInput.value}</td>`;
for (const u of options.military) {
initial += `<td style="width: 2.5em; text-align: center">${regiment.u[u.name]||0}</td>`;
@ -59,38 +89,274 @@ function showBattleScreen(attacker, defender) {
casualties += `<td style="width: 2.5em; text-align: center; color: red">0</td></tr>`;
survivors += `<td style="width: 2.5em; text-align: center; color: green">${regiment.a||0}</td></tr>`;
const div = side === "attackers" ? battleAttackers : battleDefenders;
div.innerHTML += body + initial + casualties + survivors + "</tbody>";
this[side].regiments.push(regiment);
this[side].distances.push(distance);
}
function addSide() {
const states = pack.states.filter(s => s.i && !s.removed);
const stateOptions = states.map(s => `<option value=${s.i}>${s.fullName}</option>`).join("");
const regiments = states[0].military.map(r => `<option value=${r.i}>${r.icon} ${r.name} (${r.a})</option>`).join("");
alertMessage.innerHTML = `<select id="addSideSide" data-tip="Select side"><option>Attackers</option><option>Defenders</option></select>
<select id="addSideState" data-tip="Select state">${stateOptions}</select><br>
<select id="addSideRegiment" data-tip="Select regiment">${regiments}</select>`;
$("#alert").dialog({resizable: false, title: "Add regiment to the battle",
addSide() {
const body = document.getElementById("regimentSelectorBody");
const context = Battle.prototype.context;
const regiments = pack.states.filter(s => s.military && !s.removed).map(s => s.military).flat();
const distance = reg => rn(Math.hypot(context.y-reg.y, context.x-reg.x) * distanceScaleInput.value) + " " + distanceUnitInput.value;
const isAdded = reg => context.defenders.regiments.some(r => r === reg) || context.attackers.regiments.some(r => r === reg);
body.innerHTML = regiments.map(r => {
const s = pack.states[r.state], added = isAdded(r), dist = added ? "0 " + distanceUnitInput.value : distance(r);
return `<div ${added ? "class='inactive'" : ""} data-s=${s.i} data-i=${r.i} data-state=${s.name} data-regiment=${r.name}
data-total=${r.a} data-distance=${dist} data-tip="Click to select regiment">
<svg width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" class="fillRect"></svg>
<div style="width:6em">${s.name.slice(0, 11)}</div>
<div style="width:1.2em">${r.icon}</div>
<div style="width:13em">${r.name.slice(0, 24)}</div>
<div style="width:4em">${r.a}</div>
<div style="width:4em">${dist}</div>
</div>`;
}).join("");
$("#regimentSelectorScreen").dialog({
resizable: false, width: fitContent(), title: "Add regiment to the battle",
position: {my: "left center", at: "right+10 center", of: "#battleScreen"}, close: addSideClosed,
buttons: {
Add: function() {
$(this).dialog("close");
const div = document.getElementById("addSideSide").selectedIndex ? battleDefenders : battleAttackers;
const state = pack.states.find(s => s.i == document.getElementById("addSideState").value);
const regiment = state.military.find(r => r.i == document.getElementById("addSideRegiment").value);
addRegiment(div, regiment);
},
Cancel: function() {$(this).dialog("close");}
"Add to attackers": () => addSideClicked("attackers"),
"Add to defenders": () => addSideClicked("defenders"),
Cancel: () => $("#regimentSelectorScreen").dialog("close")
}
});
document.getElementById("addSideState").onchange = function () {
const state = pack.states.find(s => s.i == this.value);
const regiments = state.military.map(r => `<option value=${r.i}>${r.icon} ${r.name} (${r.a})</option>`).join("");
document.getElementById("addSideRegiment").innerHTML = regiments;
applySorting(regimentSelectorHeader);
body.addEventListener("click", selectLine);
function selectLine(ev) {
if (ev.target.className === "inactive") {tip("Regiment is already in the battle", false, "error"); return};
ev.target.classList.toggle("selected");
}
function addSideClicked(side) {
const selected = body.querySelectorAll(".selected");
if (!selected.length) {tip("Please select a regiment first", false, "error"); return}
$("#regimentSelectorScreen").dialog("close");
selected.forEach(line => {
const state = pack.states[line.dataset.s];
const regiment = state.military.find(r => r.i == +line.dataset.i);
Battle.prototype.addRegiment.call(context, side, regiment);
Battle.prototype.calculateStrength.call(context, side);
Battle.prototype.getInitialMorale.call(context);
// move regiment
const defenders = context.defenders.regiments, attackers = context.attackers.regiments;
const shift = side === "attackers" ? attackers.length * -8 : (defenders.length-1) * 8;
regiment.px = regiment.x;
regiment.py = regiment.y;
Military.moveRegiment(regiment, defenders[0].x, defenders[0].y + shift);
});
}
function addSideClosed() {
body.innerHTML = "";
body.removeEventListener("click", selectLine);
}
}
function closeBattleScreen() {
getJoinedForces(regiments) {
return regiments.reduce((a, b) => {
for (let k in b.survivors) {
if (!b.survivors.hasOwnProperty(k)) continue;
a[k] = (a[k] || 0) + b.survivors[k];
}
return a;
}, {});
}
calculateStrength(side) {
const scheme = {
"skirmish": {"melee":.2, "ranged":2.4, "mounted":.1, "machinery":3, "naval":1, "armored":.2, "aviation":1.8, "magical":1.8}, // ranged excel
"melee": {"melee":2, "ranged":1.2, "mounted":1.5, "machinery":.5, "naval":.2, "armored":2, "aviation":.8, "magical":.8}, // melee excel
"pursue": {"melee":1, "ranged":1, "mounted":4, "machinery":.05, "naval":1, "armored":1, "aviation":1.5, "magical":.6}, // mounted excel
"retreat": {"melee":.1, "ranged":.01, "mounted":.5, "machinery":.01, "naval":.2, "armored":.1, "aviation":.8, "magical":.05} // mounted excel
};
const forces = this.getJoinedForces(this[side].regiments);
const phase = this[side].phase;
const adjuster = populationRate.value / 10; // population adjuster, by default 100
this[side].power = d3.sum(options.military.map(u => (forces[u.name] || 0) * u.power * scheme[phase][u.type])) / adjuster;
const UIvalue = this[side].power ? Math.max(this[side].power|0, 1) : 0;
document.getElementById("battlePower_"+side).innerHTML = UIvalue;
}
getInitialMorale() {
const powerFee = diff => Math.min(Math.max(100 - diff ** 1.5 * 10 + 10, 50), 100);
const distanceFee = dist => Math.min(d3.mean(dist) / 50, 15);
const powerDiff = this.defenders.power / this.attackers.power;
this.attackers.morale = powerFee(powerDiff) - distanceFee(this.attackers.distances);
this.defenders.morale = powerFee(1 / powerDiff) - distanceFee(this.defenders.distances);
this.updateMorale("attackers");
this.updateMorale("defenders");
}
updateMorale(side) {
const morale = document.getElementById("battleMorale_"+side);
morale.dataset.tip = morale.dataset.tip.replace(morale.value, "");
morale.value = this[side].morale | 0;
morale.dataset.tip += morale.value;
}
randomize() {
this.rollDie("attackers");
this.rollDie("defenders");
this.selectPhase();
this.calculateStrength("attackers");
this.calculateStrength("defenders");
}
rollDie(side) {
const el = document.getElementById("battleDie_"+side);
const prev = +el.innerHTML;
do {el.innerHTML = rand(1, 6)} while (el.innerHTML === prev)
this[side].die = +el.innerHTML;
}
selectPhase() {
const phase = this.getPhase();
this.attackers.phase = phase[0];
this.defenders.phase = phase[1];
document.getElementById("battlePhase_attackers").className = "icon-button-" + this.attackers.phase;
document.getElementById("battlePhase_defenders").className = "icon-button-" + this.defenders.phase;
}
getPhase() {
const i = this.iteration;
const prev = [this.attackers.phase || "skirmish", this.defenders.phase || "skirmish"]; // previous phase
const morale = [this.attackers.morale, this.defenders.morale];
if (P(1 - morale[0] / 25)) return ["retreat", "pursue"];
if (P(1 - morale[1] / 25)) return ["pursue", "retreat"];
if (prev[0] === "skirmish" && prev[1] === "skirmish") {
const forces = this.getJoinedForces(this.attackers.regiments.concat(this.defenders.regiments));
const total = d3.sum(Object.values(forces)); // total forces
const ranged = d3.sum(options.military.filter(u => u.type === "ranged").map(u => u.name).map(u => forces[u])) / total;
if (ranged && (P(ranged) || P(.8-i/10))) return ["skirmish", "skirmish"];
}
return ["melee", "melee"]; // default option
}
run() {
// validations
if (!this.attackers.power) {tip("Attackers army destroyed", false, "warn"); return}
if (!this.defenders.power) {tip("Defenders army destroyed", false, "warn"); return}
// calculate casualties
const attack = this.attackers.power * (this.attackers.die / 10 + .4);
const defence = this.defenders.power * (this.defenders.die / 10 + .4);
const phase = {"skirmish":.1, "melee":.2, "pursue":.3, "retreat":.3}; // casualties modifier for phase
const casualties = Math.random() * phase[this.attackers.phase]; // total casualties, ~10% per iteration
const casualtiesA = casualties * defence / (attack + defence); // attackers casualties, ~5% per iteration
const casualtiesD = casualties * attack / (attack + defence); // defenders casualties, ~5% per iteration
this.calculateCasualties("attackers", casualtiesA);
this.calculateCasualties("defenders", casualtiesD);
// change morale
this.attackers.morale = Math.max(this.attackers.morale - casualtiesA * 100, 0);
this.defenders.morale = Math.max(this.defenders.morale - casualtiesD * 100, 0);
// update table values
this.updateTable("attackers");
this.updateTable("defenders");
// prepare for next iteration
this.iteration += 1;
this.selectPhase();
this.calculateStrength("attackers");
this.calculateStrength("defenders");
}
calculateCasualties(side, casualties) {
for (const r of this[side].regiments) {
for (const unit in r.u) {
const rand = .8 + Math.random() * .4;
const died = Math.min(Pint(r.u[unit] * casualties * rand), r.survivors[unit]);
r.casualties[unit] -= died;
r.survivors[unit] -= died;
}
}
}
updateTable(side) {
for (const r of this[side].regiments) {
const tbody = document.getElementById("battle" + r.state + "-" + r.i);
const battleCasualties = tbody.querySelector(".battleCasualties");
const battleSurvivors = tbody.querySelector(".battleSurvivors");
let index = 3; // index to find table element easily
for (const u of options.military) {
battleCasualties.querySelector(`td:nth-child(${index})`).innerHTML = r.casualties[u.name] || 0;
battleSurvivors.querySelector(`td:nth-child(${index})`).innerHTML = r.survivors[u.name] || 0;
index++;
}
battleCasualties.querySelector(`td:nth-child(${index})`).innerHTML = d3.sum(Object.values(r.casualties));
battleSurvivors.querySelector(`td:nth-child(${index})`).innerHTML = d3.sum(Object.values(r.survivors));
}
this.updateMorale(side);
}
toggleChangePhase(ev, side) {
ev.stopPropagation();
const button = document.getElementById("battlePhase_"+side);
const div = button.nextElementSibling;
button.style.opacity = .5;
div.style.display = "block";
const hideSection = function() {button.style.opacity = 1; div.style.display = "none"}
document.getElementsByTagName("body")[0].addEventListener("click", hideSection, {once: true});
}
changePhase(ev, side) {
if (ev.target.tagName !== "BUTTON") return;
const phase = this[side].phase = ev.target.dataset.phase;
const button = document.getElementById("battlePhase_"+side);
button.className = "icon-button-" + phase;
this.calculateStrength(side);
}
applyResults() {
this.attackers.regiments.concat(this.defenders.regiments).forEach(r => {
r.u = Object.assign({}, r.survivors);
r.a = d3.sum(Object.values(r.u)); // reg total
armies.select(`g#regiment${r.state}-${r.i} > text`).text(Military.getTotal(r)); // update reg box
Military.moveRegiment(r, r.x + rand(30) - 15, r.y + rand(30) - 15);
});
$("#battleScreen").dialog("close");
}
cancelResults() {
// move regiments back to initial positions
this.attackers.regiments.concat(this.defenders.regiments).forEach(r => {
Military.moveRegiment(r, r.px, r.py);
});
$("#battleScreen").dialog("close");
}
closeBattleScreen() {
battleAttackers.innerHTML = battleDefenders.innerHTML = ""; // clean DOM
customization = 0; // exit edit mode
// clean temp data
const context = Battle.prototype.context;
context.attackers.regiments.concat(context.defenders.regiments).forEach(r => {
delete r.px;
delete r.py;
delete r.casualties;
delete r.survivors;
});
delete Battle.prototype.context;
}
}

View file

@ -377,7 +377,7 @@ function createPicker() {
const height = bbox.height + 9;
picker.insert("rect", ":first-child").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height).attr("fill", "#ffffff").attr("stroke", "#5d4651").on("mousemove", pos);
picker.insert("text", ":first-child").attr("x", 291).attr("y", -11).attr("id", "pickerCloseText").text("");
picker.insert("text", ":first-child").attr("x", 291).attr("y", -10).attr("id", "pickerCloseText").text("");
picker.insert("rect", ":first-child").attr("x", 288).attr("y", -21).attr("id", "pickerCloseRect").attr("width", 14).attr("height", 14).on("mousemove", cl).on("click", closePicker);
picker.insert("text", ":first-child").attr("x", 12).attr("y", -10).attr("id", "pickerLabel").text("Color Picker").on("mousemove", pos);
picker.insert("rect", ":first-child").attr("x", 0).attr("y", -30).attr("width", width).attr("height", 30).attr("id", "pickerHeader").on("mousemove", pos);

View file

@ -1167,7 +1167,7 @@ function editHeightmap() {
function setConvertColorsNumber() {
prompt(`Please provide a desired number of colors. <br>An actual number depends on color scheme and may vary from desired`,
{default:convertColors.value, step:1, min:3, max:255}, number => {
{default:+convertColors.value, step:1, min:3, max:255}, number => {
convertColors.value = number;
heightsFromImage(number);
});

View file

@ -71,7 +71,7 @@ document.getElementById("options").querySelector("div.tab").addEventListener("cl
// show popup with a list of Patreon supportes (updated manually, to be replaced with API call)
function showSupporters() {
const supporters = "Aaron Meyer, Ahmad Amerih, AstralJacks, aymeric, Billy Dean Goehring, Branndon Edwards, Chase Mayers, Curt Flood, cyninge, Dino Princip, E.M. White, es, Fondue, Fritjof Olsson, Gatsu, Johan Fröberg, Jonathan Moore, Joseph Miranda, Kate, KC138, Luke Nelson, Markus Finster, Massimo Vella, Mikey, Nathan Mitchell, Paavi1, Pat, Ryan Westcott, Sasquatch, Shawn Spencer, Sizz_TV, Timothée CALLET, UTG community, Vlad Tomash, Wil Sisney, William Merriott, Xariun, Gun Metal Games, Scott Marner, Spencer Sherman, Valerii Matskevych, Alloyed Clavicle, Stewart Walsh, Ruthlyn Mollett (Javan), Benjamin Mair-Pratt, Diagonath, Alexander Thomas, Ashley Wilson-Savoury, William Henry, Preston Brooks, JOSHUA QUALTIERI, Hilton Williams, Katharina Haase, Hisham Bedri, Ian arless, Karnat, Bird, Kevin, Jessica Thomas, Steve Hyatt, Logicspren, Alfred García, Jonathan Killstring, John Ackley, Invad3r233, Norbert Žigmund, Jennifer, PoliticsBuff, _gfx_, Maggie, Connor McMartin, Jared McDaris, BlastWind, Franc Casanova Ferrer, Dead & Devil, Michael Carmody, Valerie Elise, naikibens220, Jordon Phillips, William Pucs, The Dungeon Masters, Brady R Rathbun, J, Shadow, Matthew Tiffany, Huw Williams, Joseph Hamilton, FlippantFeline, Tamashi Toh, kms, Stephen Herron, MidnightMoon, Whakomatic x, Barished, Aaron bateson, Brice Moss, Diklyquill, PatronUser, Michael Greiner, Steven Bennett, Jacob Harrington, Miguel C., Reya C., Giant Monster Games, Noirbard, Brian Drennen, Ben Craigie, Alex Smolin, Endwords";
const supporters = "Aaron Meyer, Ahmad Amerih, AstralJacks, aymeric, Billy Dean Goehring, Branndon Edwards, Chase Mayers, Curt Flood, cyninge, Dino Princip, E.M. White, es, Fondue, Fritjof Olsson, Gatsu, Johan Fröberg, Jonathan Moore, Joseph Miranda, Kate, KC138, Luke Nelson, Markus Finster, Massimo Vella, Mikey, Nathan Mitchell, Paavi1, Pat, Ryan Westcott, Sasquatch, Shawn Spencer, Sizz_TV, Timothée CALLET, UTG community, Vlad Tomash, Wil Sisney, William Merriott, Xariun, Gun Metal Games, Scott Marner, Spencer Sherman, Valerii Matskevych, Alloyed Clavicle, Stewart Walsh, Ruthlyn Mollett (Javan), Benjamin Mair-Pratt, Diagonath, Alexander Thomas, Ashley Wilson-Savoury, William Henry, Preston Brooks, JOSHUA QUALTIERI, Hilton Williams, Katharina Haase, Hisham Bedri, Ian arless, Karnat, Bird, Kevin, Jessica Thomas, Steve Hyatt, Logicspren, Alfred García, Jonathan Killstring, John Ackley, Invad3r233, Norbert Žigmund, Jennifer, PoliticsBuff, _gfx_, Maggie, Connor McMartin, Jared McDaris, BlastWind, Franc Casanova Ferrer, Dead & Devil, Michael Carmody, Valerie Elise, naikibens220, Jordon Phillips, William Pucs, The Dungeon Masters, Brady R Rathbun, J, Shadow, Matthew Tiffany, Huw Williams, Joseph Hamilton, FlippantFeline, Tamashi Toh, kms, Stephen Herron, MidnightMoon, Whakomatic x, Barished, Aaron bateson, Brice Moss, Diklyquill, PatronUser, Michael Greiner, Steven Bennett, Jacob Harrington, Miguel C., Reya C., Giant Monster Games, Noirbard, Brian Drennen, Ben Craigie, Alex Smolin, Endwords, Joshua E Goodwin, SirTobit , Allen S. Rout, Allen Bull Bear, Pippa Mitchell, R K, G0atfather, Ryan Lege, Caner Oleas Pekgönenç, Bradley Edwards, Tertiary , Austin Miller, Jesse Holmes";
alertMessage.innerHTML = "<ul style='column-count: 3; column-gap: 2em'>" + supporters.split(", ").sort().map(n => `<li>${n}</li>`).join("") + "</ul>";
$("#alert").dialog({resizable: false, title: "Patreon Supporters", width: "30vw", position: {my: "center", at: "center", of: "svg"}});
}

View file

@ -31,6 +31,7 @@ function editProvinces() {
document.getElementById("provincesManuallyApply").addEventListener("click", applyProvincesManualAssignent);
document.getElementById("provincesManuallyCancel").addEventListener("click", () => exitProvincesManualAssignment());
document.getElementById("provincesAdd").addEventListener("click", enterAddProvinceMode);
document.getElementById("provincesRecolor").addEventListener("click", recolorProvinces);
body.addEventListener("click", function(ev) {
if (customization) return;
@ -808,6 +809,20 @@ function editProvinces() {
if (provincesAdd.classList.contains("pressed")) provincesAdd.classList.remove("pressed");
}
function recolorProvinces() {
const state = +document.getElementById("provincesFilterState").value;
pack.provinces.forEach(p => {
if (!p || p.removed) return;
if (state !== -1 && p.state !== state) return;
const stateColor = pack.states[p.state].color;
const rndColor = getRandomColor();
p.color = stateColor[0] === "#" ? d3.color(d3.interpolate(stateColor, rndColor)(.2)).hex() : rndColor;
});
if (!layerIsOn("toggleProvinces")) toggleProvinces(); else drawProvinces();
}
function downloadProvincesData() {
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
let data = "Id,Province,Form,State,Color,Capital,Area "+unit+",Total Population,Rural Population,Urban Population\n"; // headers

View file

@ -113,7 +113,7 @@ function editRegiment(selector) {
function splitRegiment() {
const reg = regiment(), u1 = reg.u;
const state = elSelected.dataset.state, military = pack.states[state].military;
const state = +elSelected.dataset.state, military = pack.states[state].military;
const i = last(military).i + 1, u2 = Object.assign({}, u1); // u clone
Object.keys(u2).forEach(u => u2[u] = Math.floor(u2[u]/2)); // halved new reg
@ -129,7 +129,7 @@ function editRegiment(selector) {
// create new regiment
const shift = +armies.attr("box-size") * 2;
const y = function(x, y) {do {y+=shift} while (military.find(r => r.x === x && r.y === y)); return y;}
const newReg = {a, cell:reg.cell, i, n:reg.n, u:u2, x:reg.x, y:y(reg.x, reg.y), bx:reg.bx, by:reg.by, icon: reg.icon};
const newReg = {a, cell:reg.cell, i, n:reg.n, u:u2, x:reg.x, y:y(reg.x, reg.y), bx:reg.bx, by:reg.by, state, icon: reg.icon};
newReg.name = Military.getName(newReg, military);
military.push(newReg);
Military.generateNote(newReg, pack.states[state]); // add legend
@ -153,10 +153,10 @@ function editRegiment(selector) {
const point = d3.mouse(this);
const cell = findCell(point[0], point[1]);
const x = pack.cells.p[cell][0], y = pack.cells.p[cell][1];
const state = elSelected.dataset.state, military = pack.states[state].military;
const state = +elSelected.dataset.state, military = pack.states[state].military;
const i = military.length ? last(military).i + 1 : 0;
const n = +(pack.cells.h[cell] < 20); // naval or land
const reg = {a:0, cell, i, n, u:{}, x, y, bx:x, by:y, icon:"🛡️"};
const reg = {a:0, cell, i, n, u:{}, x, y, bx:x, by:y, state, icon:"🛡️"};
reg.name = Military.getName(reg, military);
military.push(reg);
Military.generateNote(reg, pack.states[state]); // add legend
@ -190,30 +190,19 @@ function editRegiment(selector) {
const defender = pack.states[regSelected.dataset.state].military.find(r => r.i == regSelected.dataset.id);
if (!attacker.a || !defender.a) {tip("Regiment has no troops to battle", false, "error"); return;}
// move attacked to defender
const duration = Math.hypot(attacker.x - defender.x, attacker.y - defender.y) * 6;
const x = attacker.x = defender.x;
const y = attacker.y = defender.y + 8;
// save initial position to temp attribute
attacker.px = attacker.x, attacker.py = attacker.y;
defender.px = defender.x, defender.py = defender.y;
const size = +armies.attr("box-size");
const w = attacker.n ? size * 4 : size * 6;
const h = size * 2;
const x1 = x => rn(x - w / 2, 2);
const y1 = y => rn(y - size, 2);
// move attacker to defender
Military.moveRegiment(attacker, defender.x, defender.y-8);
const move = d3.transition().duration(duration).ease(d3.easeSinInOut);
const attack = d3.transition().delay(duration).duration(800).ease(d3.easeSinInOut).on("end", () => showBattleScreen(attacker, defender));
d3.select(elSelected.querySelector("rect")).transition(move).attr("x", x1(x)).attr("y", y1(y));
d3.select(elSelected.querySelector("text")).transition(move).attr("x", x).attr("y", y);
d3.select(elSelected.querySelectorAll("rect")[1]).transition(move).attr("x", x1(x)-h).attr("y", y1(y));
d3.select(elSelected.querySelector(".regimentIcon")).transition(move).attr("x", x1(x)-size).attr("y", y);
// battle icon
// draw battle icon
const attack = d3.transition().delay(300).duration(700).ease(d3.easeSinInOut).on("end", () => new Battle(attacker, defender));
svg.append("text").attr("x", window.innerWidth/2).attr("y", window.innerHeight/2)
.text("⚔️").attr("font-size", 0).attr("opacity", 1)
.style("dominant-baseline", "central").style("text-anchor", "middle")
.transition(attack).attr("font-size", 1000).attr("opacity", 0).remove();
.transition(attack).attr("font-size", 1000).attr("opacity", .2).remove();
clearMainTip();
$("#regimentEditor").dialog("close");

View file

@ -930,5 +930,6 @@ function editStates() {
if (customization === 2) exitStatesManualAssignment("close");
if (customization === 3) exitAddStateMode();
debug.selectAll(".highlight").remove();
body.innerHTML = "";
}
}

View file

@ -232,7 +232,7 @@ function gauss(expected = 100, deviation = 30, min = 0, max = 300, round = 0) {
return rn(Math.max(Math.min(d3.randomNormal(expected, deviation)(), max), min), round);
}
// get integer from float as floor + P(fractional)
/** This is a description of the foo function. */
function Pint(float) {
return ~~float + +P(float % 1);
}