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

View file

@ -583,6 +583,7 @@ button.options {
font-weight: bold; font-weight: bold;
float: left; float: left;
border: none; border: none;
border-radius: 0;
padding: 8px 10px; padding: 8px 10px;
transition: 0.2s; transition: 0.2s;
} }
@ -945,10 +946,11 @@ body button.noicon {
#battleBody > table { #battleBody > table {
padding: .2em .6em .2em .6em; padding: .2em .6em .2em .6em;
border: 1px solid #ccc; border: 1px solid #ccc;
margin: 0 0 .4em 0; margin: .2em 0 .4em 0;
display: block; display: block;
overflow: auto; overflow: auto;
max-height: 34vh; max-height: 34vh;
width: 100%;
} }
#battleBody > table .regiment { #battleBody > table .regiment {
@ -961,6 +963,45 @@ tr.battleCasualties, tr.battleSurvivors {
font-size: .9em; 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 { .drag-trigger {
border-left: 1em solid transparent; border-left: 1em solid transparent;
border-right: 1em solid #000; border-right: 1em solid #000;

View file

@ -2448,34 +2448,62 @@
<div id="battleScreen" class="dialog stable" style="display: none"> <div id="battleScreen" class="dialog stable" style="display: none">
<div id="battleBody"> <div id="battleBody">
<div style="font-size:1.2em; font-weight: bold"> <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"> <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> <meter id="battleMorale_attackers" data-tip="Attackers morale: " min=0 max=100 low=33 high=66 optimum=80 style="padding: .1em"></meter>
<button data-tip="Battle phase. Click to change" class="icon-button-pursue"></button> <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>
<button data-tip="Random factor for attackers. Click to re-roll" style="padding: .1em .2em" class="icon-button-die">12</button> <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>
</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="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"> <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> <meter id="battleMorale_defenders" data-tip="Defenders morale: " min=0 max=100 low=33 high=66 optimum=80 style="padding: .1em"></meter>
<button data-tip="Battle phase. Click to change" class="icon-button-skirmish"></button> <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>
<button data-tip="Random factor for defenders. Click to re-roll" style="padding: .1em .2em" class="icon-button-die">05</button> <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>
</div> </div>
<table id="battleDefenders"><thead><tr></tr></thead><tbody></tbody></table> <table id="battleDefenders"></table>
</div> </div>
<div id="battleBottom"> <div id="battleBottom">
<button id="battleAddRegiment" data-tip="Add regiment to the battle" class="icon-user-plus"></button> <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="battleRoll" data-tip="Roll dice to update random factor" class="icon-die"></button>
<button id="battleNext" data-tip="Calculate and apply phase results" class="icon-play"></button> <button id="battleRun" data-tip="Iterate battle" class="icon-play"></button>
<button id="battleApply" data-tip="Apply battle results" class="icon-check"></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 restore initial troop value" class="icon-cancel"></button> <button id="battleCancel" data-tip="Cancel battle results and close the screen" class="icon-cancel"></button>
</div> </div>
</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="brushesPanel" class="dialog stable" style="display: none">
<div id="brushesButtons" style="display: inline-block"> <div id="brushesButtons" style="display: inline-block">
<button id="brushRaise" data-tip="Raise brush: increase height of cells in radius by Power value"> <button id="brushRaise" data-tip="Raise brush: increase height of cells in radius by Power value">
@ -2859,6 +2887,7 @@
<div id="provincesBottom"> <div id="provincesBottom">
<button id="provincesEditorRefresh" data-tip="Refresh the Editor" class="icon-cw"></button> <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="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="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="provincesChart" data-tip="Show provinces chart" class="icon-chart-area"></button>
<button id="provincesToggleLabels" data-tip="Toggle province labels" class="icon-font"></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"> <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>
<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> <div>Urbanization rate:</div>
<input id="urbanizationOutput" type="range" min=.01 max=5 step=.01 value=1> <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> <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 regiments = nodes.filter(n => n.t).sort((a,b) => b.t - a.t).map((r, i) => {
const u = {}; u[r.u] = r.a; const u = {}; u[r.u] = r.a;
(r.childen||[]).forEach(n => u[n.u] = u[n.u] ? u[n.u] += n.a : n.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 // generate name for regiments
@ -210,9 +210,9 @@
return [ return [
{icon: "⚔️", name:"infantry", rural:.25, urban:.2, crew:1, power:1, type:"melee", separate:0}, {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:"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:"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:"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:"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); 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 // 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; 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); // 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)); const relaxed = chain.filter((v, i) => !(i%relax) || vertices.c[v].some(c => c >= pointsN));
if (relaxed.length < 4) continue; if (relaxed.length < 4) continue;
const points = clipPoly(relaxed.map(v => vertices.p[v]), 1); const points = clipPoly(relaxed.map(v => vertices.p[v]), 1);
const inside = d3.polygonContains(points, grid.points[i]); //const inside = d3.polygonContains(points, grid.points[i]);
chains.push([t, points, inside]); 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) { for (const t of limits) {
const layer = chains.filter(c => c[0] === t); const layer = chains.filter(c => c[0] === t);
let path = layer.map(c => round(lineGen(c[1]))).join(""); 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); 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 "🔮"; if (type === "magical") return "🔮";
else 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"; "use strict";
function showBattleScreen(attacker, defender) { class Battle {
if (customization) return;
closeDialogs(".stable");
const battle = {name:"Battle", attackers:[attacker], defenders:[defender]}; constructor(attacker, defender) {
const battleAttackers = document.getElementById("battleAttackers"); if (customization) return;
const battleDefenders = document.getElementById("battleDefenders"); closeDialogs(".stable");
addHeaders(); customization = 13; // enter customization to avoid unwanted dialog closing
addRegiment(battleAttackers, attacker);
addRegiment(battleDefenders, defender);
$("#battleScreen").dialog({ Battle.prototype.context = this; // store context
title: battle.name, resizable: false, width: fitContent(), close: closeBattleScreen, this.x = defender.x;
position: {my: "center", at: "center", of: "#map"} 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; this.addHeaders();
modules.showBattleScreen = true; this.addRegiment("attackers", attacker);
this.addRegiment("defenders", defender);
this.randomize();
this.calculateStrength("attackers");
this.calculateStrength("defenders");
this.getInitialMorale();
// add listeners $("#battleScreen").dialog({
document.getElementById("battleAddRegiment").addEventListener("click", addSide); title: this.name, resizable: false, width: fitContent(), close: this.closeBattleScreen,
position: {my: "center", at: "center", of: "#map"}
});
function addHeaders() { if (modules.Battle) return;
document.getElementById("battleScreen").querySelectorAll("th").forEach(el => el.remove()); modules.Battle = true;
const attackers = battleAttackers.querySelector("tr");
const defenders = battleDefenders.querySelector("tr"); // add listeners
let headers = "<th></th><th></th>"; 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) { for (const u of options.military) {
const label = capitalize(u.name.replace(/_/g, ' ')); const label = capitalize(u.name.replace(/_/g, ' '));
headers += `<th data-tip="${label}">${u.icon}</th>`; headers += `<th data-tip="${label}">${u.icon}</th>`;
} }
headers += "<th>Total</th>"; headers += "<th data-tip='Total military''>Total</th></tr></thead>";
attackers.insertAdjacentHTML("beforebegin", headers); battleAttackers.innerHTML = battleDefenders.innerHTML = headers;
defenders.insertAdjacentHTML("beforebegin", headers);
} }
function addRegiment(div, regiment) { addRegiment(side, regiment) {
const state = ra(pack.states), supply = rand(1000) + " " + distanceUnitInput.value; 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 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> <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>`; <text x="0" y="1.04em" style="">${regiment.icon}</text></svg>`;
const body = `<tbody id="battle${state.i}-${regiment.i}">`; 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 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}</td>`; let casualties = `<tr class="battleCasualties"><td></td><td>${state.fullName.slice(0, 26)}</td>`;
let survivors = `<tr class="battleSurvivors"><td></td><td>Supply line length: ${supply}</td>`; let survivors = `<tr class="battleSurvivors"><td></td><td>Distance to base: ${distance} ${distanceUnitInput.value}</td>`;
for (const u of options.military) { for (const u of options.military) {
initial += `<td style="width: 2.5em; text-align: center">${regiment.u[u.name]||0}</td>`; 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>`; 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>`; 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>"; div.innerHTML += body + initial + casualties + survivors + "</tbody>";
this[side].regiments.push(regiment);
this[side].distances.push(distance);
} }
function addSide() { addSide() {
const states = pack.states.filter(s => s.i && !s.removed); const body = document.getElementById("regimentSelectorBody");
const stateOptions = states.map(s => `<option value=${s.i}>${s.fullName}</option>`).join(""); const context = Battle.prototype.context;
const regiments = states[0].military.map(r => `<option value=${r.i}>${r.icon} ${r.name} (${r.a})</option>`).join(""); const regiments = pack.states.filter(s => s.military && !s.removed).map(s => s.military).flat();
alertMessage.innerHTML = `<select id="addSideSide" data-tip="Select side"><option>Attackers</option><option>Defenders</option></select> const distance = reg => rn(Math.hypot(context.y-reg.y, context.x-reg.x) * distanceScaleInput.value) + " " + distanceUnitInput.value;
<select id="addSideState" data-tip="Select state">${stateOptions}</select><br> const isAdded = reg => context.defenders.regiments.some(r => r === reg) || context.attackers.regiments.some(r => r === reg);
<select id="addSideRegiment" data-tip="Select regiment">${regiments}</select>`;
$("#alert").dialog({resizable: false, title: "Add regiment to the battle", 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: { buttons: {
Add: function() { "Add to attackers": () => addSideClicked("attackers"),
$(this).dialog("close"); "Add to defenders": () => addSideClicked("defenders"),
const div = document.getElementById("addSideSide").selectedIndex ? battleDefenders : battleAttackers; Cancel: () => $("#regimentSelectorScreen").dialog("close")
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");}
} }
}); });
document.getElementById("addSideState").onchange = function () { applySorting(regimentSelectorHeader);
const state = pack.states.find(s => s.i == this.value); body.addEventListener("click", selectLine);
const regiments = state.military.map(r => `<option value=${r.i}>${r.icon} ${r.name} (${r.a})</option>`).join("");
document.getElementById("addSideRegiment").innerHTML = regiments; 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; 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("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("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("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); 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() { function setConvertColorsNumber() {
prompt(`Please provide a desired number of colors. <br>An actual number depends on color scheme and may vary from desired`, 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; convertColors.value = number;
heightsFromImage(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) // show popup with a list of Patreon supportes (updated manually, to be replaced with API call)
function showSupporters() { 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>"; 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"}}); $("#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("provincesManuallyApply").addEventListener("click", applyProvincesManualAssignent);
document.getElementById("provincesManuallyCancel").addEventListener("click", () => exitProvincesManualAssignment()); document.getElementById("provincesManuallyCancel").addEventListener("click", () => exitProvincesManualAssignment());
document.getElementById("provincesAdd").addEventListener("click", enterAddProvinceMode); document.getElementById("provincesAdd").addEventListener("click", enterAddProvinceMode);
document.getElementById("provincesRecolor").addEventListener("click", recolorProvinces);
body.addEventListener("click", function(ev) { body.addEventListener("click", function(ev) {
if (customization) return; if (customization) return;
@ -808,6 +809,20 @@ function editProvinces() {
if (provincesAdd.classList.contains("pressed")) provincesAdd.classList.remove("pressed"); 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() { function downloadProvincesData() {
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value; 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 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() { function splitRegiment() {
const reg = regiment(), u1 = reg.u; 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 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 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 // create new regiment
const shift = +armies.attr("box-size") * 2; 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 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); newReg.name = Military.getName(newReg, military);
military.push(newReg); military.push(newReg);
Military.generateNote(newReg, pack.states[state]); // add legend Military.generateNote(newReg, pack.states[state]); // add legend
@ -153,10 +153,10 @@ function editRegiment(selector) {
const point = d3.mouse(this); const point = d3.mouse(this);
const cell = findCell(point[0], point[1]); const cell = findCell(point[0], point[1]);
const x = pack.cells.p[cell][0], y = pack.cells.p[cell][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 i = military.length ? last(military).i + 1 : 0;
const n = +(pack.cells.h[cell] < 20); // naval or land 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); reg.name = Military.getName(reg, military);
military.push(reg); military.push(reg);
Military.generateNote(reg, pack.states[state]); // add legend 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); 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;} if (!attacker.a || !defender.a) {tip("Regiment has no troops to battle", false, "error"); return;}
// move attacked to defender // save initial position to temp attribute
const duration = Math.hypot(attacker.x - defender.x, attacker.y - defender.y) * 6; attacker.px = attacker.x, attacker.py = attacker.y;
const x = attacker.x = defender.x; defender.px = defender.x, defender.py = defender.y;
const y = attacker.y = defender.y + 8;
const size = +armies.attr("box-size"); // move attacker to defender
const w = attacker.n ? size * 4 : size * 6; Military.moveRegiment(attacker, defender.x, defender.y-8);
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); // draw battle icon
const attack = d3.transition().delay(duration).duration(800).ease(d3.easeSinInOut).on("end", () => showBattleScreen(attacker, defender)); const attack = d3.transition().delay(300).duration(700).ease(d3.easeSinInOut).on("end", () => new Battle(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
svg.append("text").attr("x", window.innerWidth/2).attr("y", window.innerHeight/2) svg.append("text").attr("x", window.innerWidth/2).attr("y", window.innerHeight/2)
.text("⚔️").attr("font-size", 0).attr("opacity", 1) .text("⚔️").attr("font-size", 0).attr("opacity", 1)
.style("dominant-baseline", "central").style("text-anchor", "middle") .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(); clearMainTip();
$("#regimentEditor").dialog("close"); $("#regimentEditor").dialog("close");

View file

@ -930,5 +930,6 @@ function editStates() {
if (customization === 2) exitStatesManualAssignment("close"); if (customization === 2) exitStatesManualAssignment("close");
if (customization === 3) exitAddStateMode(); if (customization === 3) exitAddStateMode();
debug.selectAll(".highlight").remove(); 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); 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) { function Pint(float) {
return ~~float + +P(float % 1); return ~~float + +P(float % 1);
} }