Starfinder: Soldier with Magnetar Rifle#
Advanced weapons are available to all martial characters through the Weapon Proficiency general feat. However, with Weapon Proficiency, when you hit level 5 your proficiency with martial weapons increases to expert, but not that with advanced weapons. This is unlike ancestral Weapon Familiarity feats.
The Magnetar Rifle is an advanced weapon (d12 Analog, Automatic, range 60ft, magazine 30, reload 1). The closest match with martial weapons are the substantially worse Machine Gun (d8 Analog, Automatic, range 40ft, magazine 20, reload 2) or Rotolaser (d8 Automatic, Tech, range 30ft, magazine 10~100 depending on level, reload 1)
Crucially, Area Fire and Auto-Fire use one’s class proficiency, not the weapon proficiency - which raises the question of how do the damage profiles for these weapons compare. So a Soldier using a Magnetar Rifle would use their lowered weapon proficiency for Primary Target and simple Strikes, and their full class proficiency for Auto-fire. At level 7+, this also lowers the benefit from Weapon Specialization.
Let’s analyse a full round of firing a rotolaser and compare it with a magnetar rifle. With both weapons, we’ll do Primary target -> Auto-Fire against a single target -> simple Strike.
# Install in JupyterLite
%pip install -q pathfinder2e-stats
import matplotlib as mpl # noqa: F401 # Needed by JupyterLite
import xarray
import pathfinder2e_stats as pf2
Note: you may need to restart the kernel to use updated packages.
martial_atk = pf2.tables.SIMPLE_PC.weapon_attack_bonus.soldier.sum("component")
advanced_atk = (
martial_atk
- pf2.tables.PC.weapon_proficiency.soldier
+ pf2.tables.PC.weapon_proficiency.weapon_proficiency
)
atk_by_level = xarray.concat(
[martial_atk, advanced_atk],
dim="weapon",
)
atk_by_level.coords["weapon"] = ["rotolaser", "magnetar_rifle"]
atk_by_level.display(transpose=True)
| level | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| variable | weapon | ||||||||||||||||||||
| soldier | rotolaser | 6 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 28 | 30 | 31 | 32 | 33 | 34 |
| magnetar_rifle | 6 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 18 | 21 | 22 | 23 | 24 | 26 | 28 | 29 | 30 | 31 | 32 |
level = 5
atk = atk_by_level.sel(level=level)
area_fire_DC = (
pf2.tables.SIMPLE_PC.area_fire_DC.soldier.sum("component").sel(level=level).item()
)
print(f"{area_fire_DC=}")
area_fire_DC=22
weapon_dice = pf2.tables.PC.weapon_dice.improvement.sel(level=level).item()
martial_weapon_specialization = pf2.tables.PC.weapon_specialization.soldier.sel(
level=level
).item()
rotolaser = pf2.armory.starfinder.ranged.rotolaser(
weapon_dice, martial_weapon_specialization
) + pf2.armory.upgrades.auto(level=level)
rotolaser
# This is bespoke to the combination of class and Weapon Proficiency feat,
# so we need to handwrite it.
advanced_weapons_specialization = (
xarray.DataArray(
[0] * 10 + [2] * 10, dims=["level"], coords={"level": range(1, 21)}
)
.sel(level=level)
.item()
)
magnetar_rifle = pf2.armory.starfinder.ranged.magnetar_rifle(
weapon_dice, advanced_weapons_specialization
) + pf2.armory.upgrades.auto(level=level)
magnetar_rifle
enemy = pf2.tables.SIMPLE_NPC.sel(level=level, drop=True)[["AC", "saving_throws", "HP"]]
enemy.display()
| variable | AC | saving_throws | HP |
|---|---|---|---|
| challenge | |||
| Weak | 16 | 6 | 31 |
| Matched | 21 | 12 | 75 |
| Boss | 25 | 18 | 148 |
# both 'weapon' and 'challenge' are what-if analyses - let's compare the same dice rolls
# against progressively harder-to-hit enemies.
pf2.set_config(
check_dependent_dims=("challenge", "weapon"),
damage_dependent_dims=("challenge",),
)
primary_target = pf2.check(atk, DC=enemy.AC)
primary_target = xarray.concat(
[
pf2.damage(primary_target.sel(weapon="rotolaser"), rotolaser),
pf2.damage(primary_target.sel(weapon="magnetar_rifle"), magnetar_rifle),
],
dim="weapon",
join="outer",
fill_value=0,
)
auto_fire = pf2.check(
enemy.saving_throws, DC=area_fire_DC, primary_target=primary_target
)
auto_fire = xarray.concat(
[
pf2.damage(
auto_fire.sel(weapon="rotolaser"),
rotolaser.area_fire(),
),
pf2.damage(
auto_fire.sel(weapon="magnetar_rifle"),
magnetar_rifle.area_fire(),
),
],
dim="weapon",
join="outer",
fill_value=0,
)
# Note: Primary Target does not increase MAP, but Auto-Fire does
third_strike = pf2.check(atk - 5, DC=enemy.AC)
third_strike = xarray.concat(
[
pf2.damage(third_strike.sel(weapon="rotolaser"), rotolaser),
pf2.damage(third_strike.sel(weapon="magnetar_rifle"), magnetar_rifle),
],
dim="weapon",
join="outer",
fill_value=0,
)
full_round = xarray.concat([primary_target, auto_fire, third_strike], dim="action")
full_round["action"] = ["primary_target", "auto_fire", "third_strike"]
Chance to hit#
Note how the saving throw against auto-fire uses the same DC for both weapons, but it is influenced by the outcome of the Primary Target strike.
pf2.outcome_counts(full_round).stack(
row=["action", "outcome"], col=["challenge", "weapon"]
).to_pandas()
| challenge | Weak | Matched | Boss | ||||
|---|---|---|---|---|---|---|---|
| weapon | rotolaser | magnetar_rifle | rotolaser | magnetar_rifle | rotolaser | magnetar_rifle | |
| action | outcome | ||||||
| primary_target | Critical success | 0.44976 | 0.34996 | 0.19948 | 0.09888 | 0.04992 | 0.04992 |
| Success | 0.49971 | 0.49928 | 0.50144 | 0.50140 | 0.44966 | 0.34975 | |
| Failure | 0.00000 | 0.10023 | 0.24855 | 0.34919 | 0.44989 | 0.44957 | |
| Critical failure | 0.05053 | 0.05053 | 0.05053 | 0.05053 | 0.05053 | 0.15076 | |
| auto_fire | Critical success | 0.05018 | 0.05018 | 0.05018 | 0.05018 | 0.34809 | 0.34809 |
| Success | 0.00927 | 0.02831 | 0.14785 | 0.19820 | 0.25138 | 0.30083 | |
| Failure | 0.63947 | 0.62043 | 0.75113 | 0.70078 | 0.34969 | 0.30024 | |
| Critical failure | 0.30108 | 0.30108 | 0.05084 | 0.05084 | 0.05084 | 0.05084 | |
| third_strike | Critical success | 0.19714 | 0.09956 | 0.05050 | 0.05050 | 0.05050 | 0.05050 |
| Success | 0.50125 | 0.49898 | 0.39797 | 0.29790 | 0.19654 | 0.09796 | |
| Failure | 0.25067 | 0.35052 | 0.45020 | 0.45026 | 0.45135 | 0.45008 | |
| Critical failure | 0.05094 | 0.05094 | 0.10133 | 0.20134 | 0.30161 | 0.40146 | |
Mean damage#
total_damage = full_round.total_damage.mean("roll")
total_damage = xarray.concat(
[total_damage, total_damage.sum("action").expand_dims(action=["TOTAL"])],
dim="action",
)
total_damage = total_damage.stack(col=["challenge", "weapon"]).to_pandas()
total_damage
| challenge | Weak | Matched | Boss | |||
|---|---|---|---|---|---|---|
| weapon | rotolaser | magnetar_rifle | rotolaser | magnetar_rifle | rotolaser | magnetar_rifle |
| action | ||||||
| primary_target | 12.59890 | 15.60341 | 8.10443 | 9.09517 | 4.95016 | 5.84906 |
| auto_fire | 11.23759 | 16.10085 | 8.31895 | 11.70024 | 5.14770 | 7.13051 |
| third_strike | 8.06935 | 9.09409 | 4.49300 | 5.19927 | 2.68111 | 2.58713 |
| TOTAL | 31.90584 | 40.79835 | 20.91638 | 25.99468 | 12.77897 | 15.56670 |
Damage distribution#
bins = full_round.total_damage.max().item() + 1
_ = (
full_round.total_damage.stack(col=["challenge", "weapon"])
.sum("action")
.to_pandas()
.hist(bins=bins, sharex=True, figsize=(10, 10))
)