Open Source Adventures: Episode 57: Extending BATTLETECH Weapon app
Time to improve the app. Here are the main features for this episode:
- show stability damage
- include stability damage and minimum range in the form
- show weapon type
- show indirect fire indicator - with 🚀 emoji
- clean up display a bit
Slider.svelte
I added extra property step
to the component. In Svelte to mark property as optional you need to give it a default value, even if it's undefined
. Not doing this just results in a warning.
<script>
export let label, min, max, value, format, step = undefined
let id = Math.random().toString(36).slice(2)
</script>
<label for={id}>{label}:</label>
<input type="range" {min} {max} {step} bind:value id={id} />
<span>{format(value)}</span>
Form.svelte
Form now has three extra sliders, and uses step
for them and also for heat compensation.
Arguably now that I have this functionality, I might want to switch 0-100 sliders to 0-1 range with 0.01 step. Right now it's a mix of both.
<script>
import Slider from "./Slider.svelte"
export let ammoRounds
export let heatPercentage
export let doubleHeatSinksPercentage
export let rangeAtLeast
export let damageValue
export let stabDamageValue
</script>
<form>
<Slider label="Ammo for how many rounds" bind:value={ammoRounds} min={1} max={30} format={(v) => `${v}`}/>
<Slider label="Heat to compensate for" bind:value={heatPercentage} min={0} max={100} step={5} format={(v) => `${v}%`}/>
<Slider label="How many double heat sinks" bind:value={doubleHeatSinksPercentage} min={0} max={100} format={(v) => `${v}%`}/>
<Slider label="Normal damage value" bind:value={damageValue} min={0} max={1} step={0.01} format={(v) => `${v}`}/>
<Slider label="Stability damage value" bind:value={stabDamageValue} min={0} max={1} step={0.01} format={(v) => `${v}`}/>
<Slider label="Range at least" bind:value={rangeAtLeast} min={90} max={720} step={30} format={(v) => `${v}m`}/>
</form>
<style>
form {
display: grid;
grid-template-columns: auto auto auto;
margin-bottom: 1em;
}
</style>
App.svelte
All the extra functionality didn't really complicate the code too much, but at some point we might want to refactor things out.
I moved rounding to two decimal digits functionality into Row
component, as doing it here caused some minor rounding issues, and SRM2/SRM4/SRM6 had slightly different values due to too much rounding.
<script>
import {sortBy} from "lodash"
import data from "./data.json"
import Form from "./Form.svelte"
import Headers from "./Headers.svelte"
import Row from "./Row.svelte"
let ammoRounds = 10
let heatPercentage = 80
let doubleHeatSinksPercentage = 0
let rangeAtLeast = 90
let damageValue = 1.0
let stabDamageValue = 0.5
$: heatSinkingPerTon = 3.0 + 3.0 * doubleHeatSinksPercentage / 100
$: costPerHeat = (heatPercentage / 100) / heatSinkingPerTon
let sortedData
$: {
for (let row of data) {
row.value = row.shots * (row.baseDamage * damageValue + row.baseStabDamage * stabDamageValue)
row.ammoWeight = ammoRounds * row.ammoTonnagePerShot
row.cost = row.tonnage + row.ammoWeight + row.heat * costPerHeat
row.ratio = row.value / row.cost
row.id = Math.random().toString(36).slice(2)
}
sortedData = sortBy(data, [(x) => -x.ratio, (x) => x.name])
}
</script>
<h1>BATTLETECH Weapons Data</h1>
<Form bind:ammoRounds bind:heatPercentage bind:doubleHeatSinksPercentage bind:rangeAtLeast bind:damageValue bind:stabDamageValue />
<table>
<Headers />
{#each sortedData as row (row.id)}
{#if row.maxRange >= rangeAtLeast}
<Row data={row} />
{/if}
{/each}
</table>
<style>
:global(body) {
margin: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
}
table :global(tr):nth-child(even) {
background-color: #f2f2f2;
}
table :global(tr):nth-child(odd) {
background-color: #e0e0e0;
}
</style>
Headers.svelte
Nothing too exciting about this one:
<tr>
<th>Name</th>
<th>Bonus</th>
<th>Type</th>
<th>Damage</th>
<th>Stab Damage</th>
<th>Heat</th>
<th>Weight</th>
<th>Ammo Weight</th>
<th>Range</th>
<th>Value</th>
<th>Cost</th>
<th>Ratio</th>
</tr>
Row.svelte
There's some more funcitonality here. Perhaps typeSymbol
should be moved to the data exporter, I didn't notice that Support weapons are marked as AntiPersonnel
in game files.
I'm blanking zero fields to improve readability, but not the weight field for MG++.
<script>
export let data
let round100 = (v) => Math.round(v * 100) / 100
let {baseName, bonus, category, baseStabDamage, heat, shots, baseDamage, tonnage, maxRange, value, cost, ratio, ammoWeight, indirectFire} = data
let damage, stabDamage
if (shots == 1) {
damage = baseDamage
stabDamage = baseStabDamage
} else {
damage = `${shots}x${baseDamage}`
stabDamage = `${shots}x${baseStabDamage}`
}
if (heat == 0) {
heat = ""
}
if (baseStabDamage == 0) {
stabDamage = ""
}
if (ammoWeight == 0) {
ammoWeight = ""
}
let typeSymbol = (category == "AntiPersonnel") ? "S" : category.substring(0, 1)
</script>
<tr>
<td>{baseName}</td>
<td>{bonus}</td>
<td>{typeSymbol}</td>
<td>{damage}</td>
<td>{stabDamage}</td>
<td>{heat}</td>
<td>{tonnage}</td>
<td>{round100(ammoWeight)}</td>
<td>
{maxRange}m
{#if indirectFire}
🚀
{/if}
</td>
<td>{round100(value)}</td>
<td>{round100(cost)}</td>
<td>{round100(ratio)}</td>
</tr>
Story so far
I deployed this on GitHub Pages, you can see it here.
Coming next
I think the app is pretty good, so in the next episode I'll move on to something else.