'use strict'; class Battle { constructor(attacker, defender) { if (customization) return; closeDialogs('.stable'); customization = 13; // enter customization to avoid unwanted dialog closing Battle.prototype.context = this; // store context this.iteration = 0; this.x = defender.x; this.y = defender.y; this.cell = findCell(this.x, this.y); this.attackers = {regiments: [], distances: [], morale: 100, casualties: 0, power: 0}; this.defenders = {regiments: [], distances: [], morale: 100, casualties: 0, power: 0}; this.addHeaders(); this.addRegiment('attackers', attacker); this.addRegiment('defenders', defender); this.place = this.definePlace(); this.defineType(); this.name = this.defineName(); this.randomize(); this.calculateStrength('attackers'); this.calculateStrength('defenders'); this.getInitialMorale(); $('#battleScreen').dialog({ title: this.name, resizable: false, width: fitContent(), position: {my: 'center', at: 'center', of: '#map'}, close: () => Battle.prototype.context.cancelResults() }); if (modules.Battle) return; modules.Battle = true; // add listeners document.getElementById('battleType').addEventListener('click', (ev) => this.toggleChange(ev)); document.getElementById('battleType').nextElementSibling.addEventListener('click', (ev) => Battle.prototype.context.changeType(ev)); document.getElementById('battleNameShow').addEventListener('click', () => Battle.prototype.context.showNameSection()); document.getElementById('battleNamePlace').addEventListener('change', (ev) => (Battle.prototype.context.place = ev.target.value)); document.getElementById('battleNameFull').addEventListener('change', (ev) => Battle.prototype.context.changeName(ev)); document.getElementById('battleNameCulture').addEventListener('click', () => Battle.prototype.context.generateName('culture')); document.getElementById('battleNameRandom').addEventListener('click', () => Battle.prototype.context.generateName('random')); document.getElementById('battleNameHide').addEventListener('click', this.hideNameSection); 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('battleWiki').addEventListener('click', () => wiki('Battle-Simulator')); document.getElementById('battlePhase_attackers').addEventListener('click', (ev) => this.toggleChange(ev)); document.getElementById('battlePhase_attackers').nextElementSibling.addEventListener('click', (ev) => Battle.prototype.context.changePhase(ev, 'attackers')); document.getElementById('battlePhase_defenders').addEventListener('click', (ev) => this.toggleChange(ev)); 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')); } defineType() { const attacker = this.attackers.regiments[0]; const defender = this.defenders.regiments[0]; const getType = () => { const typesA = Object.keys(attacker.u).map((name) => options.military.find((u) => u.name === name).type); const typesD = Object.keys(defender.u).map((name) => options.military.find((u) => u.name === name).type); if (attacker.n && defender.n) return 'naval'; // attacker and defender are navals if (typesA.every((t) => t === 'aviation') && typesD.every((t) => t === 'aviation')) return 'air'; // if attackers and defender have only aviation units if (attacker.n && !defender.n && typesA.some((t) => t !== 'naval')) return 'landing'; // if attacked is naval with non-naval units and defender is not naval if (!defender.n && pack.burgs[pack.cells.burg[this.cell]].walls) return 'siege'; // defender is in walled town if (P(0.1) && [5, 6, 7, 8, 9, 12].includes(pack.cells.biome[this.cell])) return 'ambush'; // 20% if defenders are in forest or marshes return 'field'; }; this.type = getType(); this.setType(); } setType() { document.getElementById('battleType').className = 'icon-button-' + this.type; const sideSpecific = document.getElementById('battlePhases_' + this.type + '_attackers'); const attackers = sideSpecific ? sideSpecific.content : document.getElementById('battlePhases_' + this.type).content; const defenders = sideSpecific ? document.getElementById('battlePhases_' + this.type + '_defenders').content : attackers; document.getElementById('battlePhase_attackers').nextElementSibling.innerHTML = ''; document.getElementById('battlePhase_defenders').nextElementSibling.innerHTML = ''; document.getElementById('battlePhase_attackers').nextElementSibling.append(attackers.cloneNode(true)); document.getElementById('battlePhase_defenders').nextElementSibling.append(defenders.cloneNode(true)); } definePlace() { const cells = pack.cells, i = this.cell; const burg = cells.burg[i] ? pack.burgs[cells.burg[i]].name : null; const getRiver = (i) => { const river = pack.rivers.find((r) => r.i === i); return river.name + ' ' + river.type; }; const river = !burg && cells.r[i] ? getRiver(cells.r[i]) : null; const proper = burg || river ? null : Names.getCulture(cells.culture[this.cell]); return burg ? burg : river ? river : proper; } defineName() { if (this.type === 'field') return 'Battle of ' + this.place; if (this.type === 'naval') return 'Naval Battle of ' + this.place; if (this.type === 'siege') return 'Siege of ' + this.place; if (this.type === 'ambush') return this.place + ' Ambush'; if (this.type === 'landing') return this.place + ' Landing'; if (this.type === 'air') return `${this.place} ${P(0.8) ? 'Air Battle' : 'Dogfight'}`; } getTypeName() { if (this.type === 'field') return 'field battle'; if (this.type === 'naval') return 'naval battle'; if (this.type === 'siege') return 'siege'; if (this.type === 'ambush') return 'ambush'; if (this.type === 'landing') return 'landing'; if (this.type === 'air') return 'battle'; } addHeaders() { let headers = ''; for (const u of options.military) { const label = capitalize(u.name.replace(/_/g, ' ')); headers += `${u.icon}`; } headers += "Total"; battleAttackers.innerHTML = battleDefenders.innerHTML = headers; } 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 = ` ${regiment.icon}`; const body = ``; let initial = `${icon}${regiment.name.slice(0, 24)}`; let casualties = `${state.fullName.slice(0, 26)}`; let survivors = `Distance to base: ${distance} ${distanceUnitInput.value}`; for (const u of options.military) { initial += `${regiment.u[u.name] || 0}`; casualties += `0`; survivors += `${regiment.u[u.name] || 0}`; } initial += `${regiment.a || 0}`; casualties += `0`; survivors += `${regiment.a || 0}`; const div = side === 'attackers' ? battleAttackers : battleDefenders; div.innerHTML += body + initial + casualties + survivors + ''; this[side].regiments.push(regiment); this[side].distances.push(distance); } 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 `
${s.name.slice(0, 11)}
${r.icon}
${r.name.slice(0, 24)}
${r.a}
${dist}
`; }) .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 to attackers': () => addSideClicked('attackers'), 'Add to defenders': () => addSideClicked('defenders'), Cancel: () => $('#regimentSelectorScreen').dialog('close') } }); 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); } } showNameSection() { document.querySelectorAll('#battleBottom > button').forEach((el) => (el.style.display = 'none')); document.getElementById('battleNameSection').style.display = 'inline-block'; document.getElementById('battleNamePlace').value = this.place; document.getElementById('battleNameFull').value = this.name; } hideNameSection() { document.querySelectorAll('#battleBottom > button').forEach((el) => (el.style.display = 'inline-block')); document.getElementById('battleNameSection').style.display = 'none'; } changeName(ev) { this.name = ev.target.value; $('#battleScreen').dialog({title: this.name}); } generateName(type) { const place = type === 'culture' ? Names.getCulture(pack.cells.culture[this.cell], null, null, '') : Names.getBase(rand(nameBases.length - 1)); document.getElementById('battleNamePlace').value = this.place = place; document.getElementById('battleNameFull').value = this.name = this.defineName(); $('#battleScreen').dialog({title: this.name}); } 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 = { // field battle phases skirmish: {melee: 0.2, ranged: 2.4, mounted: 0.1, machinery: 3, naval: 1, armored: 0.2, aviation: 1.8, magical: 1.8}, // ranged excel melee: {melee: 2, ranged: 1.2, mounted: 1.5, machinery: 0.5, naval: 0.2, armored: 2, aviation: 0.8, magical: 0.8}, // melee excel pursue: {melee: 1, ranged: 1, mounted: 4, machinery: 0.05, naval: 1, armored: 1, aviation: 1.5, magical: 0.6}, // mounted excel retreat: {melee: 0.1, ranged: 0.01, mounted: 0.5, machinery: 0.01, naval: 0.2, armored: 0.1, aviation: 0.8, magical: 0.05}, // reduced // naval battle phases shelling: {melee: 0, ranged: 0.2, mounted: 0, machinery: 2, naval: 2, armored: 0, aviation: 0.1, magical: 0.5}, // naval and machinery excel boarding: {melee: 1, ranged: 0.5, mounted: 0.5, machinery: 0, naval: 0.5, armored: 0.4, aviation: 0, magical: 0.2}, // melee excel chase: {melee: 0, ranged: 0.15, mounted: 0, machinery: 1, naval: 1, armored: 0, aviation: 0.15, magical: 0.5}, // reduced withdrawal: {melee: 0, ranged: 0.02, mounted: 0, machinery: 0.5, naval: 0.1, armored: 0, aviation: 0.1, magical: 0.3}, // reduced // siege phases blockade: {melee: 0.25, ranged: 0.25, mounted: 0.2, machinery: 0.5, naval: 0.2, armored: 0.1, aviation: 0.25, magical: 0.25}, // no active actions sheltering: {melee: 0.3, ranged: 0.5, mounted: 0.2, machinery: 0.5, naval: 0.2, armored: 0.1, aviation: 0.25, magical: 0.25}, // no active actions sortie: {melee: 2, ranged: 0.5, mounted: 1.2, machinery: 0.2, naval: 0.1, armored: 0.5, aviation: 1, magical: 1}, // melee excel bombardment: {melee: 0.2, ranged: 0.5, mounted: 0.2, machinery: 3, naval: 1, armored: 0.5, aviation: 1, magical: 1}, // machinery excel storming: {melee: 1, ranged: 0.6, mounted: 0.5, machinery: 1, naval: 0.1, armored: 0.1, aviation: 0.5, magical: 0.5}, // melee excel defense: {melee: 2, ranged: 3, mounted: 1, machinery: 1, naval: 0.1, armored: 1, aviation: 0.5, magical: 1}, // ranged excel looting: {melee: 1.6, ranged: 1.6, mounted: 0.5, machinery: 0.2, naval: 0.02, armored: 0.2, aviation: 0.1, magical: 0.3}, // melee excel surrendering: {melee: 0.1, ranged: 0.1, mounted: 0.05, machinery: 0.01, naval: 0.01, armored: 0.02, aviation: 0.01, magical: 0.03}, // reduced // ambush phases surprise: {melee: 2, ranged: 2.4, mounted: 1, machinery: 1, naval: 1, armored: 1, aviation: 0.8, magical: 1.2}, // increased shock: {melee: 0.5, ranged: 0.5, mounted: 0.5, machinery: 0.4, naval: 0.3, armored: 0.1, aviation: 0.4, magical: 0.5}, // reduced // langing phases landing: {melee: 0.8, ranged: 0.6, mounted: 0.6, machinery: 0.5, naval: 0.5, armored: 0.5, aviation: 0.5, magical: 0.6}, // reduced flee: {melee: 0.1, ranged: 0.01, mounted: 0.5, machinery: 0.01, naval: 0.5, armored: 0.1, aviation: 0.2, magical: 0.05}, // reduced waiting: {melee: 0.05, ranged: 0.5, mounted: 0.05, machinery: 0.5, naval: 2, armored: 0.05, aviation: 0.5, magical: 0.5}, // reduced // air battle phases maneuvering: {melee: 0, ranged: 0.1, mounted: 0, machinery: 0.2, naval: 0, armored: 0, aviation: 1, magical: 0.2}, // aviation dogfight: {melee: 0, ranged: 0.1, mounted: 0, machinery: 0.1, naval: 0, armored: 0, aviation: 2, magical: 0.1} // aviation }; const forces = this.getJoinedForces(this[side].regiments); const phase = this[side].phase; const adjuster = Math.max(populationRate / 10, 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) => minmax(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 i = this.iteration; const morale = [this.attackers.morale, this.defenders.morale]; const powerRatio = this.attackers.power / this.defenders.power; const getFieldBattlePhase = () => { const prev = [this.attackers.phase || 'skirmish', this.defenders.phase || 'skirmish']; // previous phase // chance if moral < 25 if (P(1 - morale[0] / 25)) return ['retreat', 'pursue']; if (P(1 - morale[1] / 25)) return ['pursue', 'retreat']; // skirmish phase continuation depends on ranged forces number 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; // ranged units if (P(ranged) || P(0.8 - i / 10)) return ['skirmish', 'skirmish']; } return ['melee', 'melee']; // default option }; const getNavalBattlePhase = () => { const prev = [this.attackers.phase || 'shelling', this.defenders.phase || 'shelling']; // previous phase if (prev[0] === 'withdrawal') return ['withdrawal', 'chase']; if (prev[0] === 'chase') return ['chase', 'withdrawal']; // withdrawal phase when power imbalanced if (!prev[0] === 'boarding') { if (powerRatio < 0.5 || (P(this.attackers.casualties) && powerRatio < 1)) return ['withdrawal', 'chase']; if (powerRatio > 2 || (P(this.defenders.casualties) && powerRatio > 1)) return ['chase', 'withdrawal']; } // boarding phase can start from 2nd iteration if (prev[0] === 'boarding' || P(i / 10 - 0.1)) return ['boarding', 'boarding']; return ['shelling', 'shelling']; // default option }; const getSiegePhase = () => { const prev = [this.attackers.phase || 'blockade', this.defenders.phase || 'sheltering']; // previous phase let phase = ['blockade', 'sheltering']; // default phase if (prev[0] === 'retreat' || prev[0] === 'looting') return prev; if (P(1 - morale[0] / 30) && powerRatio < 1) return ['retreat', 'pursue']; // attackers retreat chance if moral < 30 if (P(1 - morale[1] / 15)) return ['looting', 'surrendering']; // defenders surrendering chance if moral < 15 if (P((powerRatio - 1) / 2)) return ['storming', 'defense']; // start storm if (prev[0] !== 'storming') { const machinery = options.military.filter((u) => u.type === 'machinery').map((u) => u.name); // machinery units const attackers = this.getJoinedForces(this.attackers.regiments); const machineryA = d3.sum(machinery.map((u) => attackers[u])); if (i && machineryA && P(0.9)) phase[0] = 'bombardment'; const defenders = this.getJoinedForces(this.defenders.regiments); const machineryD = d3.sum(machinery.map((u) => defenders[u])); if (machineryD && P(0.9)) phase[1] = 'bombardment'; if (i && prev[1] !== 'sortie' && machineryD < machineryA && P(0.25) && P(morale[1] / 70)) phase[1] = 'sortie'; // defenders sortie } return phase; }; const getAmbushPhase = () => { const prev = [this.attackers.phase || 'shock', this.defenders.phase || 'surprise']; // previous phase if (prev[1] === 'surprise' && P(1 - (powerRatio * i) / 5)) return ['shock', 'surprise']; // chance if moral < 25 if (P(1 - morale[0] / 25)) return ['retreat', 'pursue']; if (P(1 - morale[1] / 25)) return ['pursue', 'retreat']; return ['melee', 'melee']; // default option }; const getLandingPhase = () => { const prev = [this.attackers.phase || 'landing', this.defenders.phase || 'defense']; // previous phase if (prev[1] === 'waiting') return ['flee', 'waiting']; if (prev[1] === 'pursue') return ['flee', P(0.3) ? 'pursue' : 'waiting']; if (prev[1] === 'retreat') return ['pursue', 'retreat']; if (prev[0] === 'landing') { const attackers = P(i / 2) ? 'melee' : 'landing'; const defenders = i ? prev[1] : P(0.5) ? 'defense' : 'shock'; return [attackers, defenders]; } if (P(1 - morale[0] / 40)) return ['flee', 'pursue']; // chance if moral < 40 if (P(1 - morale[1] / 25)) return ['pursue', 'retreat']; // chance if moral < 25 return ['melee', 'melee']; // default option }; const getAirBattlePhase = () => { const prev = [this.attackers.phase || 'maneuvering', this.defenders.phase || 'maneuvering']; // previous phase // chance if moral < 25 if (P(1 - morale[0] / 25)) return ['retreat', 'pursue']; if (P(1 - morale[1] / 25)) return ['pursue', 'retreat']; if (prev[0] === 'maneuvering' && P(1 - i / 10)) return ['maneuvering', 'maneuvering']; return ['dogfight', 'dogfight']; // default option }; const phase = (function (type) { switch (type) { case 'field': return getFieldBattlePhase(); case 'naval': return getNavalBattlePhase(); case 'siege': return getSiegePhase(); case 'ambush': return getAmbushPhase(); case 'landing': return getLandingPhase(); case 'air': return getAirBattlePhase(); default: getFieldBattlePhase(); } })(this.type); this.attackers.phase = phase[0]; this.defenders.phase = phase[1]; const buttonA = document.getElementById('battlePhase_attackers'); buttonA.className = 'icon-button-' + this.attackers.phase; buttonA.dataset.tip = buttonA.nextElementSibling.querySelector("[data-phase='" + phase[0] + "']").dataset.tip; const buttonD = document.getElementById('battlePhase_defenders'); buttonD.className = 'icon-button-' + this.defenders.phase; buttonD.dataset.tip = buttonD.nextElementSibling.querySelector("[data-phase='" + phase[1] + "']").dataset.tip; } 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 + 0.4); const defense = this.defenders.power * (this.defenders.die / 10 + 0.4); // casualties modifier for phase const phase = { skirmish: 0.1, melee: 0.2, pursue: 0.3, retreat: 0.3, boarding: 0.2, shelling: 0.1, chase: 0.03, withdrawal: 0.03, blockade: 0, sheltering: 0, sortie: 0.1, bombardment: 0.05, storming: 0.2, defense: 0.2, looting: 0.5, surrendering: 0.5, surprise: 0.3, shock: 0.3, landing: 0.3, flee: 0, waiting: 0, maneuvering: 0.1, dogfight: 0.2 }; const casualties = Math.random() * Math.max(phase[this.attackers.phase], phase[this.defenders.phase]); // total casualties, ~10% per iteration const casualtiesA = (casualties * defense) / (attack + defense); // attackers casualties, ~5% per iteration const casualtiesD = (casualties * attack) / (attack + defense); // defenders casualties, ~5% per iteration 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 - 1, 0); this.defenders.morale = Math.max(this.defenders.morale - casualtiesD * 100 - 1, 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 = 0.8 + Math.random() * 0.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); } toggleChange(ev) { ev.stopPropagation(); const button = ev.target; const div = button.nextElementSibling; const hideSection = function () { button.style.opacity = 1; div.style.display = 'none'; }; if (div.style.display === 'block') { hideSection(); return; } button.style.opacity = 0.5; div.style.display = 'block'; document.getElementsByTagName('body')[0].addEventListener('click', hideSection, {once: true}); } changeType(ev) { if (ev.target.tagName !== 'BUTTON') return; this.type = ev.target.dataset.type; this.setType(); this.selectPhase(); this.calculateStrength('attackers'); this.calculateStrength('defenders'); this.name = this.defineName(); $('#battleScreen').dialog({title: this.name}); } 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; button.dataset.tip = ev.target.dataset.tip; this.calculateStrength(side); } applyResults() { 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 < 0.05) return ['minor skirmishes', 'minor skirmishes']; if (relative > 95) return ['attackers flawless victory', 'disorderly retreat of defenders']; if (relative > 0.7) return ['attackers decisive victory', 'defenders disastrous defeat']; if (relative > 0.6) return ['attackers victory', 'defenders defeat']; if (relative > 0.4) return ['stalemate', 'stalemate']; if (relative > 0.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 > 0.8 ? 'is almost completely destroyed' : losses > 0.5 ? 'suffered terrible losses' : losses > 0.3 ? 'suffered severe losses' : losses > 0.2 ? 'suffered heavy losses' : losses > 0.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); const casualtiesText = casualties.length ? ' Casualties: ' + list(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#${id} > text`).text(Military.getTotal(r)); // update reg box } const i = last(pack.markers)?.i + 1 || 0; { // append battlefield marker const marker = {i, x: this.x, y: this.y, cell: this.cell, icon: '⚔️', type: 'battlefields', dy: 52}; pack.markers.push(marker); const markerHTML = drawMarker(marker); document.getElementById('markers').insertAdjacentHTML('beforeend', markerHTML); } const getSide = (regs, n) => regs.length > 1 ? `${n ? 'regiments' : 'forces'} of ${list([...new Set(regs.map((r) => pack.states[r.state].name))])}` : getAdjective(pack.states[regs[0].state].name) + ' ' + regs[0].name; const getLosses = (casualties) => Math.min(rn(casualties * 100), 100); const status = battleStatus[+P(0.7)]; const result = `The ${this.getTypeName(this.type)} ended in ${status}`; 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 )}. ${result}. \r\nAttackers losses: ${getLosses(this.attackers.casualties)}%, defenders losses: ${getLosses(this.defenders.casualties)}%`; notes.push({id: `marker${i}`, name: this.name, legend}); tip(`${this.name} is over. ${result}`, true, 'success', 4000); $('#battleScreen').dialog('destroy'); this.cleanData(); } 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'); this.cleanData(); } cleanData() { battleAttackers.innerHTML = battleDefenders.innerHTML = ''; // clean DOM customization = 0; // exit edit mode // clean temp data this.attackers.regiments.concat(this.defenders.regiments).forEach((r) => { delete r.px; delete r.py; delete r.casualties; delete r.survivors; }); delete Battle.prototype.context; } }