Skip to content

Commit b53a2a6

Browse files
Add eRepublik air damage calculator (#68)
- add standalone eRepublik air damage calculator tool following repository template - implement responsive form for energy, rank, weapon, protector level, boosters, and bonuses - display live damage totals with breakdown of calculation factors ------ [Codex Task](https://chatgpt.com/codex/tasks/task_e_693c41d4ad248325baba8a44c5176e4c)
1 parent 75df87e commit b53a2a6

2 files changed

Lines changed: 285 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Calculate expected air battle damage in eRepublik with inputs for rank, energy, weapons, boosters, protector level, and relevant bonuses.
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>eRepublik Air Damage Calculator</title>
7+
<link rel="stylesheet" href="styles.css">
8+
<style>
9+
:root {
10+
--gap: clamp(0.75rem, 2vw, 1.25rem);
11+
}
12+
13+
body {
14+
max-width: 960px;
15+
margin: 0 auto;
16+
padding: 24px 20px 48px;
17+
}
18+
19+
.page-header {
20+
display: flex;
21+
flex-direction: column;
22+
gap: 0.5rem;
23+
}
24+
25+
.site-link {
26+
font-weight: 600;
27+
color: var(--foreground-subtle);
28+
text-decoration: none;
29+
}
30+
31+
.site-link:hover,
32+
.site-link:focus-visible {
33+
color: var(--foreground);
34+
}
35+
36+
main {
37+
display: grid;
38+
gap: 1.5rem;
39+
}
40+
41+
.tool-card {
42+
padding: clamp(1.25rem, 3vw, 2rem);
43+
}
44+
45+
.result-display {
46+
display: flex;
47+
flex-direction: column;
48+
gap: 0.5rem;
49+
}
50+
51+
.result-value {
52+
font-size: clamp(2rem, 4vw, 2.75rem);
53+
font-weight: 700;
54+
letter-spacing: 0.01em;
55+
}
56+
57+
form {
58+
display: grid;
59+
gap: var(--gap);
60+
}
61+
62+
.inputs-grid {
63+
display: grid;
64+
gap: var(--gap);
65+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
66+
}
67+
68+
.checkbox-row {
69+
display: grid;
70+
gap: 0.5rem;
71+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
72+
}
73+
74+
.helper-text {
75+
color: var(--foreground-subtle);
76+
margin: 0;
77+
}
78+
79+
@media (max-width: 720px) {
80+
body {
81+
padding: 20px 16px 40px;
82+
}
83+
}
84+
</style>
85+
</head>
86+
<body>
87+
<header class="page-header">
88+
<a class="site-link" href="https://tools.mathspp.com/" aria-label="Back to tools.mathspp.com">← tools.mathspp.com</a>
89+
<h1>eRepublik Air Damage Calculator</h1>
90+
<p class="lead">Compute expected air battle damage using rank, weapons, boosters, and bonuses.</p>
91+
</header>
92+
93+
<main>
94+
<section class="surface tool-card" aria-live="polite">
95+
<div class="result-display">
96+
<p class="helper-text">Final damage</p>
97+
<div class="result-value" id="final-damage">0</div>
98+
<p class="helper-text" id="breakdown"></p>
99+
</div>
100+
</section>
101+
102+
<section class="surface tool-card">
103+
<form id="calculator" aria-label="Air damage calculator">
104+
<div class="inputs-grid">
105+
<div class="form-group">
106+
<label for="energy">Total energy to spend</label>
107+
<input id="energy" name="energy" type="number" min="0" step="1" value="10" required>
108+
</div>
109+
110+
<div class="form-group">
111+
<label for="rank">Aircraft rank</label>
112+
<select id="rank" name="rank"></select>
113+
</div>
114+
115+
<div class="form-group">
116+
<label for="weapon">Weapon</label>
117+
<select id="weapon" name="weapon">
118+
<option value="none" selected>No weapon</option>
119+
<option value="q1">Q1 weapon</option>
120+
<option value="q2">Q2 weapon</option>
121+
<option value="q3">Q3 weapon</option>
122+
<option value="q4">Q4 weapon</option>
123+
<option value="q5">Q5 weapon</option>
124+
<option value="stinger">Stinger</option>
125+
</select>
126+
</div>
127+
128+
<div class="form-group">
129+
<label for="protector">Protector level (0–50)</label>
130+
<input id="protector" name="protector" type="number" min="0" max="50" step="1" value="0" required>
131+
</div>
132+
133+
<div class="form-group">
134+
<label for="booster">Damage booster</label>
135+
<select id="booster" name="booster">
136+
<option value="0" selected>0%</option>
137+
<option value="20">20%</option>
138+
<option value="50">50%</option>
139+
<option value="100">100%</option>
140+
</select>
141+
</div>
142+
</div>
143+
144+
<div class="checkbox-row">
145+
<label class="checkbox">
146+
<input type="checkbox" id="natural-enemy" name="natural-enemy">
147+
<span>Fighting against natural enemy</span>
148+
</label>
149+
<label class="checkbox">
150+
<input type="checkbox" id="level-100" name="level-100">
151+
<span>Player is level 100+</span>
152+
</label>
153+
</div>
154+
</form>
155+
</section>
156+
</main>
157+
158+
<footer class="page-footer">
159+
<p>Built with ❤️, 🤖, and 🐍, by <a href="https://mathspp.com/">Rodrigo Girão Serrão</a></p>
160+
</footer>
161+
162+
<script>
163+
(function () {
164+
const ranksAscending = [
165+
'Airman',
166+
'Airman First Class',
167+
'Sergeant',
168+
'Staff Sergeant',
169+
'Technical Sergeant',
170+
'Lieutenant',
171+
'Captain',
172+
'Major',
173+
'Commander',
174+
'Lt Colonel',
175+
'Colonel',
176+
'Chief Colonel',
177+
'Air Commodore',
178+
'Air Vice Marshal',
179+
'Air Marshal',
180+
'Air Chief Marshal',
181+
'Air General',
182+
'Air Force Marshal',
183+
'Sky Captain'
184+
];
185+
186+
const rankSelect = document.getElementById('rank');
187+
ranksAscending
188+
.slice()
189+
.reverse()
190+
.forEach((label, reverseIndex) => {
191+
const option = document.createElement('option');
192+
const rankIndex = ranksAscending.length - 1 - reverseIndex;
193+
option.value = String(rankIndex);
194+
option.textContent = label;
195+
if (label === 'Air Commodore') {
196+
option.selected = true;
197+
}
198+
rankSelect.appendChild(option);
199+
});
200+
201+
const energyInput = document.getElementById('energy');
202+
const weaponSelect = document.getElementById('weapon');
203+
const protectorInput = document.getElementById('protector');
204+
const boosterSelect = document.getElementById('booster');
205+
const naturalEnemyCheckbox = document.getElementById('natural-enemy');
206+
const level100Checkbox = document.getElementById('level-100');
207+
const finalDamage = document.getElementById('final-damage');
208+
const breakdown = document.getElementById('breakdown');
209+
210+
const weaponMultipliers = {
211+
none: 0,
212+
q1: 1 / 5,
213+
q2: 2 / 5,
214+
q3: 3 / 5,
215+
q4: 4 / 5,
216+
q5: 5 / 5,
217+
stinger: 1
218+
};
219+
220+
const formatNumber = (value) => {
221+
return Number.isFinite(value) ? value.toLocaleString(undefined, { maximumFractionDigits: 2 }) : '0';
222+
};
223+
224+
const clampNumber = (value, min, max) => {
225+
if (Number.isNaN(value)) return min;
226+
if (typeof max === 'number') {
227+
return Math.min(Math.max(value, min), max);
228+
}
229+
return Math.max(value, min);
230+
};
231+
232+
function calculateDamage() {
233+
const energy = clampNumber(parseInt(energyInput.value, 10), 0);
234+
if (energyInput.value === '') energyInput.value = energy;
235+
236+
const protectorLevel = clampNumber(parseInt(protectorInput.value, 10), 0, 50);
237+
if (protectorInput.value === '') protectorInput.value = protectorLevel;
238+
protectorInput.value = protectorLevel;
239+
240+
const weapon = weaponSelect.value;
241+
const boosterPercent = Number(boosterSelect.value) || 0;
242+
const isStinger = weapon === 'stinger';
243+
const rankIndex = isStinger ? 0 : Number(rankSelect.value) || 0;
244+
245+
const baseDamage = isStinger ? 1000 : 10;
246+
const energyFactor = energy / 10;
247+
const rankMultiplier = isStinger ? 1 : 1 + rankIndex / 5;
248+
const weaponMultiplier = weaponMultipliers[weapon] ?? 0;
249+
const protectorMultiplier = 1 + protectorLevel / 100;
250+
const boosterMultiplier = 1 + boosterPercent / 100;
251+
const naturalEnemyMultiplier = naturalEnemyCheckbox.checked ? 1.1 : 1;
252+
const level100Multiplier = level100Checkbox.checked ? 1.1 : 1;
253+
254+
const result = energyFactor
255+
* baseDamage
256+
* rankMultiplier
257+
* weaponMultiplier
258+
* protectorMultiplier
259+
* boosterMultiplier
260+
* naturalEnemyMultiplier
261+
* level100Multiplier;
262+
263+
finalDamage.textContent = formatNumber(result);
264+
265+
const parts = [
266+
`${formatNumber(energyFactor)} energy chunks`,
267+
`${formatNumber(baseDamage)} base damage`,
268+
`rank ×${formatNumber(rankMultiplier)}`,
269+
`${weapon === 'stinger' ? 'stinger damage' : 'weapon'} ×${formatNumber(weaponMultiplier)}`,
270+
`protector ×${formatNumber(protectorMultiplier)}`,
271+
`booster ×${formatNumber(boosterMultiplier)}`,
272+
naturalEnemyCheckbox.checked ? 'natural enemy ×1.1' : 'no natural enemy',
273+
level100Checkbox.checked ? 'level 100+ ×1.1' : 'below level 100'
274+
];
275+
276+
breakdown.textContent = parts.join(' • ');
277+
}
278+
279+
document.getElementById('calculator').addEventListener('input', calculateDamage);
280+
calculateDamage();
281+
})();
282+
</script>
283+
</body>
284+
</html>

0 commit comments

Comments
 (0)