This commit is contained in:
Azgaar 2020-06-05 22:21:32 +03:00
parent e75a12f90d
commit 957bab32c1
6 changed files with 390 additions and 95 deletions

View file

@ -266,7 +266,34 @@
.icon-die:before {content:'🎲';}
.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-skirmish:before {content:'🎯'; padding-right: .4em;}
.icon-button-pursue:before {content:'🐎'; padding-right: .4em;}
.icon-button-retreat:before {content:'🏳️'; padding-right: .4em;}
.icon-button-shelling:before {content:'💣'; padding-right: .4em;}
.icon-button-boarding:before {content:'⚔️'; padding-right: .4em;}
.icon-button-chase:before {content:'⛵'; padding-right: .4em;}
.icon-button-withdrawal:before {content:'🏳️'; padding-right: .4em;}
.icon-button-bombardment:before {content:'💣'; padding-right: .4em;}
.icon-button-blockade:before {content:'⏳'; padding-right: .4em;}
.icon-button-sheltering:before {content:'🔒'; padding-right: .4em;}
.icon-button-sortie:before {content:'🚪'; padding-right: .4em;}
.icon-button-defense:before {content:'🛡️'; padding-right: .4em;}
.icon-button-storming:before {content:'⚔️'; padding-right: .4em;}
.icon-button-looting:before {content:'☠️'; padding-right: .4em;}
.icon-button-surrendering:before {content:'🏳️'; padding-right: .4em;}
.icon-button-surprise:before {content:'⚡'; padding-right: .4em;}
.icon-button-shock:before {content:'💫'; padding-right: .4em;}
.icon-button-maneuvering:before {content:'💢'; padding-right: .4em;}
.icon-button-dogfight:before {content:'🐕'; padding-right: .4em;}
.icon-button-field:before {content:'🗡️'; padding-right: .4em;}
.icon-button-naval:before {content:'🌊'; padding-right: .4em;}
.icon-button-siege:before {content:'🏰'; padding-right: .4em;}
.icon-button-ambush:before {content:'🌳'; padding-right: .4em;}
.icon-button-landing:before {content:'⚓'; padding-right: .4em;}
.icon-button-air:before {content:'💨'; padding-right: .4em;}

View file

@ -969,12 +969,14 @@ tr.battleCasualties, tr.battleSurvivors {
font-size: .9em;
}
#battleBody div.battlePhases {
position: absolute;
background-color: #fff;
#battleBody div.battlePhases,
#battleBottom div.battleTypes {
position: fixed;
background-color: #ffffff30;
}
#battleBody div.battlePhases > button {
#battleBody div.battlePhases > button,
#battleBottom div.battleTypes > button {
width: 3.2em;
display: block;
margin: .2em 0;

View file

@ -2466,19 +2466,81 @@
<div id="battleScreen" class="dialog stable" style="display: none">
<div id="battleBody">
<template id="battlePhases_field">
<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. Units strength reduced" data-phase="retreat" class="icon-button-retreat"></button>
</template>
<template id="battlePhases_naval">
<button data-tip="Shelling phase. Naval artillery bombardment of enemy fleet" data-phase="shelling" class="icon-button-shelling"></button>
<button data-tip="Boarding phase. Melee units go aboard" data-phase="boarding" class="icon-button-boarding"></button>
<button data-tip="Сhase phase. Naval units pursue and rarely shell enemy fleet" data-phase="chase" class="icon-button-chase"></button>
<button data-tip="Withdrawal phase. Naval units try to escape enemy fleet" data-phase="withdrawal" class="icon-button-withdrawal"></button>
</template>
<template id="battlePhases_siege_attackers">
<button data-tip="Blockade phase. Hold the blockade and wait" data-phase="blockade" class="icon-button-blockade"></button>
<button data-tip="Bombardment phase. Attack enemy with machinery units" data-phase="bombardment" class="icon-button-bombardment"></button>
<button data-tip="Storming phase. Storm enemy town. Melee units excel" data-phase="storming" class="icon-button-storming"></button>
<button data-tip="Looting phase. Plunder the town. Units strength increased" data-phase="looting" class="icon-button-looting"></button>
<button data-tip="Retreat phase. Units strength reduced" data-phase="retreat" class="icon-button-retreat"></button>
</template>
<template id="battlePhases_siege_defenders">
<button data-tip="Sheltering phase. Hide behind the walls and wait" data-phase="sheltering" class="icon-button-sheltering"></button>
<button data-tip="Sortie phase. Make a sortie from besieged town. Melee units excel" data-phase="sortie" class="icon-button-sortie"></button>
<button data-tip="Bombardment phase. Attack enemy with machinery units" data-phase="bombardment" class="icon-button-bombardment"></button>
<button data-tip="Defense phase. Ranged and melee units excel" data-phase="defense" class="icon-button-defense"></button>
<button data-tip="Surrendering phase. Give up the defence. Units strength reduced" data-phase="surrendering" class="icon-button-surrendering"></button>
<button data-tip="Pursue phase. Mounted units excel" data-phase="pursue" class="icon-button-pursue"></button>
</template>
<template id="battlePhases_ambush_attackers">
<button data-tip="Shock phase. Units strength reduced" data-phase="shock" class="icon-button-shock"></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. Units strength reduced" data-phase="retreat" class="icon-button-retreat"></button>
</template>
<template id="battlePhases_ambush_defenders">
<button data-tip="Surprice attack phase. Units strength increased, ranged units excel" data-phase="surprise" class="icon-button-surprise"></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. Units strength reduced" data-phase="retreat" class="icon-button-retreat"></button>
</template>
<template id="battlePhases_landing_attackers">
<button data-tip="Landing phase. Units strength reduced" data-phase="skirmish" class="icon-button-landing"></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. Units strength reduced" data-phase="retreat" class="icon-button-retreat"></button>
</template>
<template id="battlePhases_landing_defenders">
<button data-tip="Shock phase. Units strength reduced" data-phase="shock" class="icon-button-shock"></button>
<button data-tip="Defense phase. Units strength increased" data-phase="defense" class="icon-button-defense"></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. Units strength reduced" data-phase="retreat" class="icon-button-retreat"></button>
</template>
<template id="battlePhases_air">
<button data-tip="Maneuvering phase. Units strength reduced" data-phase="maneuvering" class="icon-button-maneuvering"></button>
<button data-tip="Dogfight phase. Units strength increased" data-phase="dogfight" class="icon-button-dogfight"></button>
<button data-tip="Pursue phase. Units strength increased" data-phase="pursue" class="icon-button-pursue"></button>
<button data-tip="Retreat phase. Units strength reduced" data-phase="retreat" class="icon-button-retreat"></button>
</template>
<div style="font-size:1.2em; font-weight: bold">
<span>Attackers</span>
<div style="float: right; font-size: .7em">
<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>
<meter id="battleMorale_attackers" data-tip="Attackers morale: " min=0 max=100 low=33 high=66 optimum=80></meter>
<div id="battlePower_attackers" data-tip="Attackers strength during this phase. Strength defines dealt damage" 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>
<button id="battlePhase_attackers" style="width: 3.2em"></button>
<div class="battlePhases" style="display: none"></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>
@ -2487,16 +2549,11 @@
<div style="font-size:1.2em; font-weight: bold">
<span></span>Defenders</span>
<div style="float: right; font-size: .7em">
<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>
<meter id="battleMorale_defenders" data-tip="Defenders morale: " min=0 max=100 low=33 high=66 optimum=80></meter>
<div id="battlePower_defenders" data-tip="Defenders strength during this phase. Strength defines dealt damage" 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>
<button id="battlePhase_defenders" style="width: 3.2em"></button>
<div class="battlePhases" style="display: none"></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>
@ -2505,6 +2562,25 @@
</div>
<div id="battleBottom">
<button id="battleType" data-tip="Battle type. Click to change"></button>
<div class="battleTypes" style="display: none">
<button data-tip="Field Battle: a standard type of combat" data-type="field" class="icon-button-field"></button>
<button data-tip="Naval Battle: naval units combat" data-type="naval" class="icon-button-naval"></button>
<button data-tip="Siege: burg blockade and storming" data-type="siege" class="icon-button-siege"></button>
<button data-tip="Ambush: surprise attack" data-type="ambush" class="icon-button-ambush"></button>
<button data-tip="Landing: amphibious attack" data-type="landing" class="icon-button-landing"></button>
<button data-tip="Air Battle: maneuring fight of avia units" data-type="air" class="icon-button-air"></button>
</div>
<button id="battleNameShow" data-tip="Set battle name" class="icon-font"></button>
<div id="battleNameSection" style="display: none">
<button id="battleNameHide" data-tip="Hide the battle name section" class="icon-font"></button>
<input id="battleNamePlace" data-tip="Type place name"" style="width: 30%">
<input id="battleNameFull" data-tip="Type full battle name"" style="width: 46%">
<button id="battleNameCulture" data-tip="Generate culture-specific name for place and battle" class="icon-book"></button>
<button id="battleNameRandom" data-tip="Generate random name for place and battle" class="icon-globe"></button>
</div>
<button id="battleAddRegiment" data-tip="Add regiment to the battle" class="icon-user-plus"></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>

View file

@ -7,16 +7,19 @@ class Battle {
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.name = this.getBattleName();
this.iteration = 0;
this.attackers = {regiments:[], distances:[], morale:100, casualties:0};
this.defenders = {regiments:[], distances:[], morale:100, casualties:0};
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");
@ -32,24 +35,81 @@ class Battle {
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", () => this.showNameSection());
document.getElementById("battleNamePlace").addEventListener("change", ev => this.place = ev.target.value);
document.getElementById("battleNameFull").addEventListener("change", ev => this.changeName(ev));
document.getElementById("battleNameCulture").addEventListener("click", () => this.generateName(Names.getCulture(pack.cells.culture[this.cell], null, null, "")));
document.getElementById("battleNameRandom").addEventListener("click", () => this.generateName(Names.getBase(rand(nameBases.length-1))));
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("battlePhase_attackers").addEventListener("click", ev => this.toggleChangePhase(ev, "attackers"));
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.toggleChangePhase(ev, "defenders"));
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"));
}
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"
defineType() {
const attacker = this.attackers.regiments[0];
const defender = this.defenders.regiments[0];
const getType = () => {
if (attacker.n && defender.n) return "naval"; // attacker and defender are navals
if (!defender.n && pack.burgs[pack.cells.burg[this.cell]].walls) return "siege"; // defender is in walled town
if (P(.1) && [5,6,7,8,9,12].includes(pack.cells.biome[this.cell])) return "ambush"; // 20% if defenders are in forest or marshes
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 attacked is naval with non-naval units and defender is not naval
if (attacker.n && !defender.n && typesA.some(t => t !== "naval")) return "landing";
// if attacked and defender have only aviation units
if (typesA.every(t => t === "aviation") && typesD.every(t => t === "aviation")) return "air";
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(.8) ? "Air Battle" : "Dogfight"}`;
}
addHeaders() {
@ -78,17 +138,17 @@ class Battle {
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>`;
let survivors = `<tr class="battleSurvivors"><td></td><td data-tip="Supply line length, affects morale">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>`;
casualties += `<td style="width: 2.5em; text-align: center; color: red">0</td>`;
survivors += `<td style="width: 2.5em; text-align: center; color: green">${regiment.u[u.name]||0}</td>`;
initial += `<td data-tip="Initial forces" style="width: 2.5em; text-align: center">${regiment.u[u.name]||0}</td>`;
casualties += `<td data-tip="Casualties" style="width: 2.5em; text-align: center; color: red">0</td>`;
survivors += `<td data-tip="Survivors" style="width: 2.5em; text-align: center; color: green">${regiment.u[u.name]||0}</td>`;
}
initial += `<td style="width: 2.5em; text-align: center">${regiment.a||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>`;
initial += `<td data-tip="Initial forces" style="width: 2.5em; text-align: center">${regiment.a||0}</td></tr>`;
casualties += `<td data-tip="Casualties" style="width: 2.5em; text-align: center; color: red">0</td></tr>`;
survivors += `<td data-tip="Survivors" 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>";
@ -161,6 +221,30 @@ class Battle {
}
}
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(place) {
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) {
@ -173,10 +257,31 @@ class Battle {
calculateStrength(side) {
const scheme = {
// field battle phases
"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
"retreat": {"melee":.1, "ranged":.01, "mounted":.5, "machinery":.01, "naval":.2, "armored":.1, "aviation":.8, "magical":.05}, // reduced
// naval battle phases
"shelling": {"melee":0, "ranged":.2, "mounted":0, "machinery":2, "naval":2, "armored":0, "aviation":.1, "magical":.5}, // naval and machinery excel
"boarding": {"melee":1, "ranged":.5, "mounted":.5, "machinery":0, "naval":.5, "armored":.4, "aviation":0, "magical":.2}, // melee excel
"chase": {"melee":0, "ranged":.15, "mounted":0, "machinery":1, "naval":1, "armored":0, "aviation":.15, "magical":.5}, // reduced
"withdrawal": {"melee":0, "ranged":.02, "mounted":0, "machinery":.5, "naval":.1, "armored":0, "aviation":.1, "magical":.3}, // reduced
// siege phases
"blockade": {"melee":.25, "ranged":.25, "mounted":.2, "machinery":.5, "naval":.2, "armored":.1, "aviation":.25, "magical":.25}, // no active actions
"sheltering": {"melee":.3, "ranged":.5, "mounted":.2, "machinery":.5, "naval":.2, "armored":.1, "aviation":.25, "magical":.25}, // no active actions
"sortie": {"melee":2, "ranged":.5, "mounted":1.2, "machinery":.2, "naval":.1, "armored":.5, "aviation":1, "magical":1}, // melee excel
"bombardment": {"melee":.2, "ranged":.5, "mounted":.2, "machinery":3, "naval":1, "armored":.5, "aviation":1, "magical":1}, // machinery excel
"storming": {"melee":1, "ranged":.6, "mounted":.5, "machinery":1, "naval":.1, "armored":.1, "aviation":.5, "magical":.5}, // melee excel
"defense": {"melee":2, "ranged":3, "mounted":1, "machinery":1, "naval":.1, "armored":1, "aviation":.5, "magical":1}, // ranged excel
"looting": {"melee":1.6, "ranged":1.6, "mounted":.5, "machinery":.2, "naval":.02, "armored":.2, "aviation":.1, "magical":.3}, // melee excel
"surrendering": {"melee":.1, "ranged":.1, "mounted":.05, "machinery":.01, "naval":.01, "armored":.02, "aviation":.01, "magical":.03}, // reduced
// ambush phases
"surprise": {"melee":2.2, "ranged":3, "mounted":1.5, "machinery":1.3, "naval":1, "armored":1.3, "aviation":1, "magical":2}, // increased
"shock": {"melee":.8, "ranged":.8, "mounted":.6, "machinery":.5, "naval":.5, "armored":.1, "aviation":.5, "magical":.7} // reduced
};
const forces = this.getJoinedForces(this[side].regiments);
@ -215,34 +320,119 @@ class Battle {
rollDie(side) {
const el = document.getElementById("battleDie_"+side);
const prev = +el.innerHTML;
do {el.innerHTML = rand(1, 6)} while (el.innerHTML === prev)
do {el.innerHTML = rand(1, 6)} while (el.innerHTML == prev)
this[side].die = +el.innerHTML;
}
selectPhase() {
const phase = this.getPhase();
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(.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 < .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 - .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(.9)) phase[0] = "bombardment";
const defenders = this.getJoinedForces(this.defenders.regiments);
const machineryD = d3.sum(machinery.map(u => defenders[u]));
if (machineryD && P(.9)) phase[1] = "bombardment";
if (i && prev[1] !== "sortie" && machineryD < machineryA && P(.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 = () => {
// ⚓ landing / 💫 shock OR 🛡️ defense, ⚔️ melee, 🏳️ retreat / 🐎 pursue
}
const getAirBattlePhase = () => {
// 🎯 maneuvering, 🐕 dogfight, 🏳️ retreat / 🐎 pursue
}
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];
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
document.getElementById("battlePhase_attackers").dataset.tip = battleBody.querySelector(".battlePhases > [data-phase='"+phase[0]+"']").dataset.tip;
document.getElementById("battlePhase_defenders").dataset.tip = battleBody.querySelector(".battlePhases > [data-phase='"+phase[1]+"']").dataset.tip;
}
run() {
@ -253,8 +443,16 @@ class Battle {
// 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
// casualties modifier for phase
const phase = {
"skirmish":.1, "melee":.2, "pursue":.3, "retreat":.3,
"boarding":.2, "shelling":.1, "chase":.03, "withdrawal": .03,
"blockade":0, "sheltering":0, "sortie":.1, "bombardment":.05, "storming":.2, "defense":.2, "looting":.5, "surrendering":.5,
"surprise":.3, "shock":.3
};
const casualties = Math.random() * (Math.max(phase[this.attackers.phase], phase[this.defenders.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
@ -264,8 +462,8 @@ class Battle {
this.defenders.casualties += 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);
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");
@ -308,22 +506,37 @@ class Battle {
this.updateMorale(side);
}
toggleChangePhase(ev, side) {
toggleChange(ev) {
ev.stopPropagation();
const button = document.getElementById("battlePhase_"+side);
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 = .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});
}
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);
}

View file

@ -316,35 +316,12 @@ function editBurg(id) {
const p2 = cells.p[cells.haven[i]];
let deg = Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * 180 / Math.PI - 90;
if (deg < 0) deg += 360;
let norm = rn(normalize(deg, 0, 360) * 8) / 4;
if (norm === 2) norm = 0;
switch(norm) {
case 0 : return "&southSea=1";
case 0.25 : return "&southSea=1&westSea=1";
case 0.50 : return "&westSea=1";
case 0.75 : return "&westSea=1&northSea=1";
case 1 : return "&northSea=1";
case 1.25 : return "&northSea=1&eastSea=1";
case 1.5 : return "&eastSea=1";
case 1.75 : return "&eastSea=1&southSea=1";
}
const norm = rn(normalize(deg, 0, 360) * 2, 2); // 0 = south, 0.5 = west, 1 = north, 1.5 = east
return "&sea="+norm;
// debug.selectAll("*").remove();
// pack.burgs.filter(b => b.port).forEach(b => {
// var p1 = pack.cells.p[b.cell];
// var p2 = pack.cells.p[pack.cells.haven[b.cell]];
// var deg = Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * 180 / Math.PI - 90;
// if (deg < 0) deg += 360;
// var norm = rn(normalize(deg, 0, 360) * 8) / 4;
// if (norm === 2) norm = 0;
// debug.append("line").attr("x1", p1[0]).attr("y1", p1[1]).attr("x2", p2[0]).attr("y2", p2[1]).attr("stroke", "red").attr("stroke-width", .2);
// debug.append("circle").attr("cx", b.x).attr("cy", b.y).attr("r", .4);
// debug.append("text").attr("x", b.x+1).attr("y", b.y).attr("font-size", 2).text(rn(norm, 2));
// });
}
const site = "http://fantasycities.watabou.ru/";
const url = `${site}?name=${name}&size=${size}&seed=${s}&hub=${hub}&random=0&continuous=0&river=${river}&coast=${coast}&citadel=${citadel}&plaza=${plaza}&temple=${temple}&walls=${walls}&shantytown=${shanty}${sea}`;
const site = "http://fantasycities.watabou.ru/?random=0&continuous=0";
const url = `${site}&name=${name}&size=${size}&seed=${s}&hub=${hub}&river=${river}&coast=${coast}&citadel=${citadel}&plaza=${plaza}&temple=${temple}&walls=${walls}&shantytown=${shanty}${sea}`;
openURL(url);
}
}

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, 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";
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, Jan Dvořák, Marten F, Erin D. Smale";
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"}});
}