{ "cells": [ { "cell_type": "markdown", "id": "c1cba98e-c242-47c9-aae2-76168c1db7e5", "metadata": {}, "source": [ "# Starfinder: Soldier with Magnetar Rifle\n", "\n", "Advanced weapons are available to all martial characters through the {srd_feats}`Weapon Proficiency <911-weapon-proficiency>` general feat.\n", "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.\n", "\n", "The {srd_weapons}`Magnetar Rifle <68-magnetar-rifle>` is an advanced weapon (d12 Analog, Automatic, range 60ft, magazine 30, reload 1).\n", "The closest match with martial weapons are the substantially worse {srd_weapons}`Machine Gun <55-machine-gun>` (d8 Analog, Automatic, range 40ft, magazine 20, reload 2) or {srd_weapons}`Rotolaser <58-rotolaser>` (d8 Automatic, Tech, range 30ft, magazine 10~100 depending on level, reload 1)\n", "\n", "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.\n", "So a {srd_classes}`Soldier<5-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.\n", "\n", "Let's analyse a full round of firing a rotolaser and compare it with a magnetar rifle.\n", "With both weapons, we'll do Primary target -> Auto-Fire against a single target -> simple Strike." ] }, { "cell_type": "code", "execution_count": null, "id": "9fa9a497-5cd9-4998-aeb5-90825a1aa4e4", "metadata": {}, "outputs": [], "source": [ "# Install in jupyterlite\n", "%pip install -q pathfinder2e-stats\n", "\n", "import xarray\n", "\n", "import pathfinder2e_stats as pf2" ] }, { "cell_type": "code", "execution_count": null, "id": "d7efd826-98ce-46bc-95ab-d7e6a8166d13", "metadata": {}, "outputs": [], "source": [ "martial_atk = pf2.tables.SIMPLE_PC.weapon_attack_bonus.soldier.sum(\"component\")\n", "advanced_atk = (\n", " martial_atk\n", " - pf2.tables.PC.weapon_proficiency.soldier\n", " + pf2.tables.PC.weapon_proficiency.weapon_proficiency\n", ")\n", "\n", "atk_by_level = xarray.concat(\n", " [martial_atk, advanced_atk],\n", " dim=\"weapon\",\n", ")\n", "atk_by_level.coords[\"weapon\"] = [\"rotolaser\", \"magnetar_rifle\"]\n", "atk_by_level.display(transpose=True)" ] }, { "cell_type": "code", "execution_count": null, "id": "eac30c05-2fdc-4fda-abc5-75b62fb34591", "metadata": {}, "outputs": [], "source": [ "level = 5\n", "atk = atk_by_level.sel(level=level)\n", "area_fire_DC = (\n", " pf2.tables.SIMPLE_PC.area_fire_DC.soldier.sum(\"component\").sel(level=level).item()\n", ")\n", "print(f\"{area_fire_DC=}\")" ] }, { "cell_type": "code", "execution_count": null, "id": "a4b380b0-3573-4d9d-b7e3-fc26dfc84c1f", "metadata": {}, "outputs": [], "source": [ "weapon_dice = pf2.tables.PC.weapon_dice.improvement.sel(level=level).item()\n", "martial_weapon_specialization = pf2.tables.PC.weapon_specialization.soldier.sel(\n", " level=level\n", ").item()\n", "rotolaser = pf2.armory.starfinder.ranged.rotolaser(\n", " weapon_dice, martial_weapon_specialization\n", ")\n", "\n", "rotolaser" ] }, { "cell_type": "code", "execution_count": null, "id": "6158157f-0921-4764-a96d-617364e4747a", "metadata": {}, "outputs": [], "source": [ "# This is bespoke to the combination of class and Weapon Proficiency feat,\n", "# so we need to handwrite it.\n", "advanced_weapons_specialization = (\n", " xarray.DataArray([0] * 11 + [2] * 9, dims=[\"level\"], coords={\"level\": range(1, 21)})\n", " .sel(level=level)\n", " .item()\n", ")\n", "magnetar_rifle = pf2.armory.starfinder.ranged.magnetar_rifle(\n", " weapon_dice, advanced_weapons_specialization\n", ")\n", "\n", "magnetar_rifle" ] }, { "cell_type": "code", "execution_count": null, "id": "0d2a01d3-f4ac-4c19-9a2b-a92cf9a8afe9", "metadata": {}, "outputs": [], "source": [ "enemy = pf2.tables.SIMPLE_NPC.sel(level=level, drop=True)[[\"AC\", \"saving_throws\", \"HP\"]]\n", "enemy.display()" ] }, { "cell_type": "code", "execution_count": null, "id": "dac5ca8d-9a9f-43c6-b61c-3966338d9225", "metadata": {}, "outputs": [], "source": [ "# both 'weapon' and 'challenge' are what-if analyses - let's compare the same dice rolls\n", "# against progressively harder-to-hit enemies.\n", "pf2.set_config(\n", " check_dependent_dims=(\"challenge\", \"weapon\"),\n", " damage_dependent_dims=(\"challenge\",),\n", ")" ] }, { "cell_type": "code", "execution_count": null, "id": "034a1c49-ae10-4710-9b41-b67d9e31031d", "metadata": {}, "outputs": [], "source": [ "primary_target = pf2.check(atk, DC=enemy.AC)\n", "primary_target = xarray.concat(\n", " [\n", " pf2.damage(primary_target.sel(weapon=\"rotolaser\"), rotolaser),\n", " pf2.damage(primary_target.sel(weapon=\"magnetar_rifle\"), magnetar_rifle),\n", " ],\n", " dim=\"weapon\",\n", " join=\"outer\",\n", " fill_value=0,\n", ")" ] }, { "cell_type": "code", "execution_count": null, "id": "9b7d078b-291c-4592-9567-7dff47324f98", "metadata": {}, "outputs": [], "source": [ "auto_fire = pf2.check(\n", " enemy.saving_throws, DC=area_fire_DC, primary_target=primary_target\n", ")\n", "\n", "auto_fire = xarray.concat(\n", " [\n", " pf2.damage(\n", " auto_fire.sel(weapon=\"rotolaser\"),\n", " rotolaser.area_fire(),\n", " ),\n", " pf2.damage(\n", " auto_fire.sel(weapon=\"magnetar_rifle\"),\n", " magnetar_rifle.area_fire(),\n", " ),\n", " ],\n", " dim=\"weapon\",\n", " join=\"outer\",\n", " fill_value=0,\n", ")" ] }, { "cell_type": "code", "execution_count": null, "id": "b1598f88-e02d-4dda-b406-2ea30616b003", "metadata": {}, "outputs": [], "source": [ "# Note: Primary Target does not increase MAP, but Auto-Fire does\n", "third_strike = pf2.check(atk - 5, DC=enemy.AC)\n", "third_strike = xarray.concat(\n", " [\n", " pf2.damage(third_strike.sel(weapon=\"rotolaser\"), rotolaser),\n", " pf2.damage(third_strike.sel(weapon=\"magnetar_rifle\"), magnetar_rifle),\n", " ],\n", " dim=\"weapon\",\n", " join=\"outer\",\n", " fill_value=0,\n", ")" ] }, { "cell_type": "code", "execution_count": null, "id": "cc2291f6-5be5-4f6a-b6ad-dfa2abde1565", "metadata": {}, "outputs": [], "source": [ "full_round = xarray.concat([primary_target, auto_fire, third_strike], dim=\"action\")\n", "full_round[\"action\"] = [\"primary_target\", \"auto_fire\", \"third_strike\"]" ] }, { "cell_type": "markdown", "id": "7e054ed8-3f60-45ef-bdf4-7b6b972a92d6", "metadata": {}, "source": [ "## Chance to hit\n", "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." ] }, { "cell_type": "code", "execution_count": null, "id": "838148fb-1663-4684-b12d-f38d0b941eab", "metadata": {}, "outputs": [], "source": [ "pf2.outcome_counts(full_round).stack(\n", " row=[\"action\", \"outcome\"], col=[\"challenge\", \"weapon\"]\n", ").to_pandas()" ] }, { "cell_type": "markdown", "id": "64b8ac45-d135-493e-987a-224ba7802d1c", "metadata": {}, "source": [ "## Mean damage" ] }, { "cell_type": "code", "execution_count": null, "id": "cafa521d-c952-4060-8e2e-16f1598cc2f6", "metadata": {}, "outputs": [], "source": [ "total_damage = full_round.total_damage.mean(\"roll\")\n", "total_damage = xarray.concat(\n", " [total_damage, total_damage.sum(\"action\").expand_dims(action=[\"TOTAL\"])],\n", " dim=\"action\",\n", ")\n", "total_damage = total_damage.stack(col=[\"challenge\", \"weapon\"]).to_pandas()\n", "total_damage" ] }, { "cell_type": "markdown", "id": "deed8321-d018-4725-88d7-edb8698cc784", "metadata": {}, "source": [ "## Damage distribution" ] }, { "cell_type": "code", "execution_count": null, "id": "3550df9c-093e-4c36-8464-764ca5969540", "metadata": {}, "outputs": [], "source": [ "bins = full_round.total_damage.max().item() + 1\n", "_ = (\n", " full_round.total_damage.stack(col=[\"challenge\", \"weapon\"])\n", " .sum(\"action\")\n", " .to_pandas()\n", " .hist(bins=bins, sharex=True, figsize=(10, 10))\n", ")" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.7" } }, "nbformat": 4, "nbformat_minor": 5 }