diff --git a/index.html b/index.html index c3efcd13..9be8fea0 100644 --- a/index.html +++ b/index.html @@ -2508,8 +2508,8 @@ - - + + diff --git a/modules/military-generator.js b/modules/military-generator.js index 009b4b0d..f0821400 100644 --- a/modules/military-generator.js +++ b/modules/military-generator.js @@ -304,7 +304,7 @@ const station = base ? `${r.name} is ${r.n ? "based" : "stationed"} in ${base}. ` : ""; const composition = r.a ? Object.keys(r.u).map(t => `— ${t}: ${r.u[t]}`).join("\r\n") : null; - const troops = composition ? `\r\n\r\nRegiment composition:\r\n${composition}.` : ""; + const troops = composition ? `\r\n\r\nRegiment composition in ${options.year} ${options.eraShort}:\r\n${composition}.` : ""; const campaign = s.campaigns ? ra(s.campaigns) : null; const year = campaign ? rand(campaign.start, campaign.end) : gauss(options.year-100, 150, 1, options.year-6); diff --git a/modules/ui/battle-screen.js b/modules/ui/battle-screen.js index de9882f2..70375f3e 100644 --- a/modules/ui/battle-screen.js +++ b/modules/ui/battle-screen.js @@ -11,8 +11,8 @@ class Battle { this.y = defender.y; this.name = this.getBattleName(); this.iteration = 0; - this.attackers = {regiments:[], distances:[], morale:100}; - this.defenders = {regiments:[], distances:[], morale:100}; + this.attackers = {regiments:[], distances:[], morale:100, casualties:0}; + this.defenders = {regiments:[], distances:[], morale:100, casualties:0}; this.addHeaders(); this.addRegiment("attackers", attacker); @@ -260,6 +260,8 @@ class Battle { this.calculateCasualties("attackers", casualtiesA); this.calculateCasualties("defenders", casualtiesD); + this.attackers.casualties += casualtiesA; + this.defenders.casualties += casualtiesD; // change morale this.attackers.morale = Math.max(this.attackers.morale - casualtiesA * 100, 0); @@ -326,12 +328,79 @@ class Battle { } applyResults() { - this.attackers.regiments.concat(this.defenders.regiments).forEach(r => { + const battleName = this.name; + const maxCasualties = Math.max(this.attackers.casualties, this.attackers.casualties); + const relativeCasualties = this.defenders.casualties / (this.attackers.casualties + this.attackers.casualties); + const battleStatus = getBattleStatus(relativeCasualties, maxCasualties); + function getBattleStatus(relative, max) { + if (isNaN(relative)) return ["standoff", "standoff"]; // if no casualties at all + if (max < .05) return ["minor skirmishes", "minor skirmishes"]; + if (relative > 95) return ["attackers flawless victory", "disorderly retreat of defenders"]; + if (relative > .7) return ["attackers decisive victory", "defenders disastrous defeat"]; + if (relative > .6) return ["attackers victory", "defenders defeat"]; + if (relative > .4) return ["stalemate", "stalemate"]; + if (relative > .3) return ["attackers defeat", "defenders victory"]; + if (relative > 0.5) return ["attackers disastrous defeat", "decisive victory of defenders"]; + if (relative >= 0) return ["attackers disorderly retreat", "flawless victory of defenders"]; + return ["stalemate", "stalemate"]; // exception + } + + this.attackers.regiments.forEach(r => applyResultForSide(r, "attackers")); + this.defenders.regiments.forEach(r => applyResultForSide(r, "defenders")); + + function applyResultForSide(r, side) { + const id = "regiment" + r.state + "-" + r.i; + + // add result to regiment note + const note = notes.find(n => n.id === id); + if (note) { + const status = side === "attackers" ? battleStatus[0] : battleStatus[1]; + const losses = r.a ? Math.abs(d3.sum(Object.values(r.casualties))) / r.a : 1; + const regStatus = + losses === 1 ? "is destroyed" : + losses > .8 ? "is almost completely destroyed" : + losses > .5 ? "suffered terrible losses" : + losses > .3 ? "suffered severe losses" : + losses > .2 ? "suffered heavy losses" : + losses > .05 ? "suffered significant losses" : + losses > 0 ? "suffered unsignificant losses" : + "left the battle without loss"; + const casualties = Object.keys(r.casualties).map(t => r.casualties[t] ? `${Math.abs(r.casualties[t])} ${t}` : null).filter(c => c).join(", "); + const casualtiesText = casualties ? " Casualties: " + casualties : ""; + const legend = `\r\n\r\n${battleName} (${options.year} ${options.eraShort}): ${status}. The regiment ${regStatus}.${casualtiesText}`; + note.legend += legend; + } + 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); - }); + armies.select(`g#${id} > text`).text(Military.getTotal(r)); // update reg box + Military.moveRegiment(r, r.x + rand(20) - 10, r.y + rand(20) - 10); + } + + // append battlefield marker + void function addMarkerSymbol() { + if (svg.select("#defs-markers").select("#marker_battlefield").size()) return; + const symbol = svg.select("#defs-markers").append("symbol").attr("id", "marker_battlefield").attr("viewBox", "0 0 30 30"); + symbol.append("path").attr("d", "M6,19 l9,10 L24,19").attr("fill", "#000000").attr("stroke", "none"); + symbol.append("circle").attr("cx", 15).attr("cy", 15).attr("r", 10).attr("fill", "#ffffff").attr("stroke", "#000000").attr("stroke-width", 1); + symbol.append("text").attr("x", "50%").attr("y", "52%").attr("fill", "#000000").attr("stroke", "#3200ff").attr("stroke-width", 0) + .attr("font-size", "12px").attr("dominant-baseline", "central").text("⚔️"); + }() + + const getSide = (regs, n) => regs.length > 1 ? + `${n ? "regiments" : "forces"} of ${[... new Set(regs.map(r => pack.states[r.state].name))].join(", ")}` : + getAdjective(pack.states[regs[0].state].name) + " " + regs[0].name; + const getLosses = casualties => Math.min(rn(casualties * 100), 100); + + const legend = `${this.name} took place in ${options.year} ${options.eraShort}. It was fought between ${getSide(this.attackers.regiments, 1)} and ${getSide(this.defenders.regiments, 0)}. The battle ended in ${battleStatus[+P(.7)]}. + \r\nAttackers losses: ${getLosses(this.attackers.casualties)}%, defenders losses: ${getLosses(this.defenders.casualties)}%`; + const id = getNextId("markerElement"); + notes.push({id, name:this.name, legend}); + + markers.append("use").attr("id", id) + .attr("xlink:href", "#marker_battlefield").attr("data-id", "#marker_battlefield") + .attr("data-x", this.x).attr("data-y", this.y).attr("x", this.x - 15).attr("y", this.y - 30) + .attr("data-size", 1).attr("width", 30).attr("height", 30); $("#battleScreen").dialog("destroy"); this.cleanData();