DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

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> 
Enter fullscreen mode Exit fullscreen mode

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> 
Enter fullscreen mode Exit fullscreen mode

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> 
Enter fullscreen mode Exit fullscreen mode

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> 
Enter fullscreen mode Exit fullscreen mode

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> 
Enter fullscreen mode Exit fullscreen mode

Story so far

All the code is on GitHub.

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.

Top comments (0)