Weapon stats
How a crafted weapon's per-usage stats follow from its pieces, at full precision for game v1.4.8: geometry, grip, handling, speed, damage and the stored stats of each usage. Tier, value and trade price are in Value and price; difficulty, quality and materials in Smithing mechanics; the 12 templates and their example recipes in Weapon templates.
The formulas were recovered from v1.4.6 IL of TaleWorlds.Core.dll and TaleWorlds.CampaignSystem.dll and compared against v1.4.8 IL: no compared method differs, but 38 were compared by header only and 22 are not in the v1.4.8 dumps (see Value and price). The stat code is Crafting.CraftedItemGenerationHelper.CraftingStats in TaleWorlds.Core, a helper rather than a game model, so there is no model for SandBox, StoryMode, NavalDLC or BirthAndDeath to replace. NavalDLC changes only the data: it adds the pieces of naval_crafting_pieces.xml and patches two Native files through XSLT (see Loading). The snippet tables are computed by the Python model in src/bannerlord/crafting/.
The game computes in 32-bit floats wherever it uses float, and integer stats are rounded or floored from those float results, so an exact reproduction needs 32-bit arithmetic. The speed simulation uses doubles, as the game does.
Inputs
Stats, tier, value, difficulty and material costs need only game XML and the formulas on these pages. The constants live in the game DLLs, so a patch can change results without changing XML. Save state enters as parameters:
- Trade price needs the party leader's Trade skill, perks and the town's supply, demand and in-store value.
- Quality probabilities need the hero's Crafting skill and perks.
- Which parts a hero has unlocked is also save state.
Pipeline
Six steps left to right for the one-handed sword example. Step 1: choose one piece for each slot, piece tiers 1, 5, 2, 1. Step 2: the pieces set weight and length, weight 0.9, length 97. Step 3: weight, length and balance set the stats, handling 99, swing speed 106, swing damage 21, thrust speed 97, thrust damage 12. Step 4: stats and piece tiers set the tier score, 1.4704. Step 5: the tier score sets the value, 398. Step 6: value and Trade skill set the price, 130 at Trade 86.
The data flow, file by file:
crafting_pieces.xmlgives the piece values,crafting_templates.xmlthebuild_orderof each slot, andweapon_descriptions.xmleach usage'sweapon_class,WeaponFlagsandAvailablePieces.- Piece values and
build_ordergiveweight,weapon_length,center_of_massandinertia(Geometry). WeaponFlagsselect the grip profile (Grip profile).- Geometry and grip profile give
handlingand swing and thrust speed; the speeds give damage, byweapon_class(Handling, Speed, Damage). - The per-usage stats give
tierf, andtierfgivesvalue(Value and price).
An item has one WeaponComponentData per available usage. All usages share pieces and geometry. They differ by weapon_class and WeaponFlags, which select the formula constants.
How a crafted weapon gets its numbers, with the one-handed sword example at each step.
| Step | What happens | Example |
|---|---|---|
| 1 | Choose one piece for each slot | Piece tiers 1, 5, 2, 1 |
| 2 | The pieces set weight and length | Weight 0.9, length 97 |
| 3 | Weight, length and balance set the stats | Handling 99, swing speed 106, swing damage 21, thrust speed 97, thrust damage 12 |
| 4 | Stats and piece tiers set the tier score | 1.4704 |
| 5 | The tier score sets the value | 398 |
| 6 | Value and Trade skill set the price | 130 at Trade 86 |
| Slot | Piece | Tier |
|---|---|---|
| Blade | wood_blade_1 | 1 |
| Guard | vlandian_noble_guard_3 | 5 |
| Handle | khuzait_grip_7 | 2 |
| Pommel | battania_pommel_6 | 1 |
The tier score 1.4704 rounds to tier 1. It mixes 0.8207 from the stats and 2.445 from the pieces.
Variables
Names follow the XML. Lengths are stored in centimeters and loaded in meters.
crafting_pieces.xml, <CraftingPiece>:
| XML | Loaded value | Default |
|---|---|---|
@piece_type |
Blade, Guard, Handle, Pommel |
required |
@tier |
int | required |
@weight |
kg | 0 |
@length |
0.01·length |
sum of the two distances below |
@distance_to_next_piece |
0.01·value |
length/2 |
@distance_to_previous_piece |
0.01·value |
length/2 |
@center_of_mass |
length·value (fraction of length) |
length·0.5 |
@full_scale |
value == "true"; weight scales with scale³ instead of scale |
true for Guard, Pommel; otherwise false |
@appearance |
float | 0.5 |
BuildData/@piece_offset |
0.01·value |
0 |
BuildData/@previous_piece_offset |
0.01·value |
0 |
BuildData/@next_piece_offset |
0.01·value |
0 |
BladeData/@stack_amount |
int | 1 |
BladeData/Thrust/@damage_factor |
float | 0 (no <Thrust>: 0 damage) |
BladeData/Thrust/@damage_type |
Cut, Pierce, Blunt |
Invalid |
BladeData/Swing/@damage_factor |
float | 0 (no <Swing>: 0 damage) |
BladeData/Swing/@damage_type |
Cut, Pierce, Blunt |
Invalid |
StatContributions/@armor_bonus |
int | 0 |
Materials/Material/@id, @count |
IronOre, Iron1..Iron6, Wood, Charcoal |
none |
inertia has no XML attribute. It is derived at load:
inertia = 0.0833333 × weight × length²
crafting_templates.xml, <CraftingTemplate>: PieceDatas/PieceData/@piece_type, @build_order, WeaponDescriptions/WeaponDescription/@id.
weapon_descriptions.xml, <WeaponDescription>: @weapon_class, WeaponFlags/WeaponFlag/@value, AvailablePieces/AvailablePiece/@id.
Recipes (<CraftedItem>): Pieces/Piece/@id, @Type, @scale_factor (percent, default 100).
Outputs use <Weapon> attribute names from item XMLs: weapon_length (cm), speed_rating (swing speed), thrust_speed, swing_damage, thrust_damage, missile_speed, weapon_balance, accuracy, stack_amount, body_armor. Item XML stores weapon_balance multiplied by 100; here it is 0..1. Thrown weapons store missile damage in thrust_damage. handling, inertia, center_of_mass and sweet_spot are in-game fields without an equivalent crafted-item XML attribute.
Loading
Piece attributes load as follows (CraftingPiece.Deserialize, BladeData.Deserialize):
weightis@weight, or 0.- With
@lengthset:length = 0.01·@length, and both distances arelength/2; the distance attributes are not read. Without it: each distance is0.01·its attribute, andlengthis their sum. center_of_mass = length × @center_of_mass, default fraction 0.5.inertia = 0.0833333 × weight × length², from the loadedweightandlength.full_scaleis@full_scale == "true"when the attribute is present; otherwise true forGuardandPommel.- The three offsets are
0.01·theirBuildDataattribute, or 0. stack_amountisBladeData/@stack_amount, or 1, also for a piece withoutBladeData.- Each of
ThrustandSwinggives adamage_factorand adamage_type; a missing element gives factor 0 and typeInvalid. armor_bonusisStatContributions/@armor_bonus, or 0.- Materials are every
Materialelement's@idand@count.
WeaponFlag values used here: MeleeWeapon (0x1), RangedWeapon (0x2), NotUsableWithOneHand (0x10) and WideGrip (0x40). Other values do not enter these formulas.
Files read, with NavalDLC active:
| File | Content |
|---|---|
Native/ModuleData/crafting_pieces.xml |
Native pieces |
NavalDLC/ModuleData/naval_crafting_pieces.xml |
NavalDLC pieces, added to the Native ones |
Native/ModuleData/weapon_descriptions.xml |
usages |
NavalDLC/ModuleData/XSLT/NavalDLC_Native_WeaponDescriptions.xslt |
adds AvailablePiece ids to Native usages (AvailablePieces) |
Native/ModuleData/crafting_templates.xml |
templates |
NavalDLC/ModuleData/XSLT/NavalDLC_Native_CraftingTemplates.xslt |
adds UsablePiece ids to Native templates (UsablePieces) |
NavalDLC data can redefine a Native piece: a piece id in naval_crafting_pieces.xml that is already in crafting_pieces.xml replaces the Native definition. In v1.4.8 the only such piece is sickle_handle_1, whose BuildData/@piece_offset is 13.7 in Native and 0 in NavalDLC (piece_offset 0.137 and 0), so recipes with that handle get different stats with NavalDLC.
Without NavalDLC only the Native files are read. mp_crafting_pieces.xml replaces crafting_pieces.xml in multiplayer.
Scale
scale = scale_factor × 0.01
scale_factor: the recipe'sPiece/@scale_factor, in percent.
length, center_of_mass, both distances and all three offsets multiply by scale. weight multiplies by scale³ when full_scale is true, which is the default for Guard and Pommel, and by scale otherwise. Only 38 pieces, all blades, set the attribute explicitly. The game skips scaling when scale_factor == 100. A piece's inertia is not scaled (see Quirks).
Below, scaled x means x × scale of the piece's own scale, and scaled_weight the scaled weight.
Geometry
build_order: 0 is the root (handle), positive pieces stack toward the tip, negative toward the pommel end. Slots without a piece are skipped everywhere.
weight = round2(Σ scaled_weight)
The sum runs in double and is rounded to 2 decimals (.NET Math.Round: ties to even). All later formulas use the rounded weight.
weapon_length = trunc(100 × reach)
reach = round2(max(blade_pivot + scaled blade.distance_to_next_piece, widest_piece))
blade_pivot (WeaponDesign.CalculatePivotDistances): start with top = 0, bottom = 0, pivot = 0, then for each slot in the template's PieceDatas order:
- If
build_order = 0:top += piece_offset,bottom −= piece_offset. - If
build_order < 0:bottom += distance_to_next_piece + piece_offset − next_piece_offset. - If
build_order > 0:top += distance_to_previous_piece + piece_offset − previous_piece_offset. - If the piece is the blade:
pivot = top, plus the blade'spiece_offsetwhen itsbuild_orderis 0. - If
build_order ≤ 0:bottom += distance_to_previous_piece − previous_piece_offset. - If
build_order ≥ 0:top += distance_to_next_piece − next_piece_offset.
All values are scaled. blade_pivot is the final pivot.
widest_piece (WeaponDesign.CalculateWeaponLength): start with widest = 0, then for each piece in the order Blade, Guard, Handle, Pommel: if its scaled distance_to_next_piece is greater than widest, set widest to its scaled distance_to_next_piece + piece_offset. The comparison leaves out piece_offset; the stored value includes it.
center_of_mass = moment ÷ weight − (scaled handle.distance_to_previous_piece − scaled handle.piece_offset)
moment (CraftingStats.CalculateCenterOfMass): start with moment = 0, length_up = 0, length_down = 0, then for each slot in PieceDatas order, with L its scaled length, c its scaled center_of_mass:
- If
build_order < 0:moment −= (length_down + L − c) × scaled_weight, thenlength_down += L. - Otherwise:
moment += (length_up + c) × scaled_weight, thenlength_up += L.
inertia = Σ [piece.inertia + scaled_weight × (x + piece.center_of_mass)²]
CraftingStats.CalculateWeaponInertia: x starts at −center_of_mass and, after each piece, advances by the piece's scaled length. The sum walks PieceDatas in XML order, with Pommel last. piece.inertia and piece.center_of_mass are the unscaled loaded values.
inertia_shoulder = inertia + weight × (0.5 + center_of_mass)²
inertia_grip = inertia + weight × center_of_mass²
Grip profile
Only WeaponFlags selects constants; weapon_class does not.
| Profile | Required flags |
|---|---|
OneHanded (One-handed) |
none of the below (also all thrown usages, which lack MeleeWeapon) |
OneHandedWideGrip (One-handed, wide grip) |
MeleeWeapon, WideGrip |
TwoHanded (Two-handed) |
MeleeWeapon, NotUsableWithOneHand |
TwoHandedWideGrip (Two-handed, wide grip) |
MeleeWeapon, NotUsableWithOneHand, WideGrip |
Constants per profile. Two-handed stage values are the base value times the game's multiplier, shown in brackets:
Grip constants for each grip. Swing drag is 3.9 times the grip's drag factor, per unit of reach. Two-handed stage values are the one-handed value times a multiplier.
| Constant | One-handed | One-handed, wide grip | Two-handed | Two-handed, wide grip |
|---|---|---|---|---|
| Handling inputs a and b | 1, 0.7 | 0.4, 1 | 0.5, 0.9 | 0.5, 0.9 |
| Swing drag per unit of reach | 1.17 (3.9 × 0.3) | 3.9 (3.9 × 1) | 1.17 (3.9 × 0.3) | 3.9 (3.9 × 1) |
| Extra swing load | 0 | 0 | 1 | 1.5 |
| Swing stage 2: power and torque | 170, 15 | 170, 15 | 195.5, 19.5 (170 × 1.15, 15 × 1.3) | 195.5, 25.5 (170 × 1.15, 15 × 1.7) |
| Swing stage 3: power and torque | 90, 7 | 90, 7 | 121.5, 16.8 (90 × 1.35, 7 × 2.4) | 117, 28 (90 × 1.3, 7 × 4) |
| Extra thrust load | 0 | 0 | 0.6 | 0.9 |
| Thrust stage 2: power and torque | 170, 24 | 170, 24 | 178.5, 26.4 (170 × 1.05, 24 × 1.1) | 178.5, 28.8 (170 × 1.05, 24 × 1.2) |
| Thrust stage 3: power and torque | 90, 15 | 90, 15 | 108, 28.5 (90 × 1.2, 15 × 1.9) | 108, 31.5 (90 × 1.2, 15 × 2.1) |
| Grip | Usages |
|---|---|
| One-handed | Dagger, Dagger (alternative), Javelin, Mace, One-handed axe, One-handed axe (bastard grip), One-handed axe (bastard grip, alternative), One-handed sword, One-handed sword (bastard grip), One-handed sword (bastard grip, alternative), Throwing axe, Throwing knife, Thrown polearm |
| One-handed, wide grip | One-handed polearm, One-handed polearm (javelin alternative), Two-handed polearm (couchable) |
| Two-handed | Two-handed axe, Two-handed mace, Two-handed sword |
| Two-handed, wide grip | Pike, Two-handed polearm, Two-handed polearm (bracing) |
Table of the 8 grip constants for the 4 grips, one-handed, one-handed wide grip, two-handed, two-handed wide grip. Handling inputs a and b: 1, 0.7; 0.4, 1; 0.5, 0.9; 0.5, 0.9. Swing drag per unit of reach: 1.17; 3.9; 1.17; 3.9. Extra swing load: 0; 0; 1; 1.5. Swing stage 2 power and torque: 170, 15; 170, 15; 195.5, 19.5; 195.5, 25.5. Swing stage 3: 90, 7; 90, 7; 121.5, 16.8; 117, 28. Extra thrust load: 0; 0; 0.6; 0.9. Thrust stage 2: 170, 24; 170, 24; 178.5, 26.4; 178.5, 28.8. Thrust stage 3: 90, 15; 90, 15; 108, 28.5; 108, 31.5.
Handling
handling = round(100 × (1 ÷ (a × inertia_grip + b))^0.55)
a,b: the grip's handling inputs.inertia_grip: from Geometry.round: half to even.
Speed
Each raw speed is a constant divided by 0.33 × Σ t, with t the simulated time of three stages. Every stage accelerates a load through an angle in steps of dt = 0.01 s, starting from v = 0.01, angle = 0, t = 0, and repeats while angle is below the stage angle:
torque = min(power ÷ v, max_torque); swing stages subtractdrag × v.v += dt × torque ÷ load.angle += v × dt.t += dt.
Swing stages run to angle 1.5 with dt the float 0.01 widened to double (0.009999999776482582); thrust stages run to angle 0.6 with the double 0.01.
| Stage | Swing | Thrust |
|---|---|---|
Load L |
inertia_shoulder + 0.9 + extra swing load |
1.8 + weight + 0.2·inertia_grip + extra thrust load |
| 1 | power 200, max torque max(1, 27 − L), load L + 2 |
power 250, max torque 48, load L + 4 |
| 2 | stage 2 power, max torque max(1, stage 2 torque − L), load L + 1 |
stage 2 power, max torque stage 2 torque, load L + 2 |
| 3 | stage 3 power, max torque max(1, stage 3 torque − L), load L + 0.5 |
stage 3 power, max torque stage 3 torque, load L + 0.5 |
- Swing
drag = 3.9 × reach × drag factor, which is the grip's swing drag per unit of reach timesreach. - Extra loads, stage powers and torques: the grip's constants in Grip profile.
raw swing speed = 20.8 ÷ (0.33 × Σ t)
raw thrust speed = 3.85 ÷ (0.33 × Σ t)
The thrust constant is the double 3.8500000000000005. Both results are stored as float.
speed_rating = floor(raw swing speed × 4.5454545)
thrust_speed = floor(raw thrust speed × 11.7647057)
Damage uses the raw speeds, not these integer ratings.
weapon_balance = clamp((raw swing speed × 4.5454545 − 70) ÷ 30, 0, 1), rounded to 2 decimals.
Damage
Swing magnitude is the energy transferred by an inelastic hit at impact point p along the blade, with ω the raw swing speed, then maximized over impact points (CombatStatCalculator):
a = reach × p − center_of_massv = ω × (0.5 + center_of_mass)J = (v + ω × a) ÷ (1/weight + a²/inertia)v' = v − J/weightω' = ω − J × a/inertia- strike(p) = 0.067 × (½·weight·v² + ½·inertia·ω² − ½·weight·v'² − ½·inertia·ω'² + 0.5)
a: lever arm of the impact point from the centre of mass.v,ω: linear and angular speed before the hit;v',ω'after it.J: the impulse.
Impact points: the step is clamp(0.4 ÷ reach, 0, 1). A start point begins at 0.93 and drops by 0.05 while above 0.5. Each start samples 5 points, p = start + i/4 × step for i = 0..4, and stops sampling at the first p ≥ 1. Swing magnitude is the largest strike(p) sampled.
thrust magnitude = 0.125 × ½ × (weight + 2.5) × raw thrust speed²
thrown magnitude = 0.125 × ½ × weight × raw thrust speed²
Thrown thrust omits the 2.5; ThrowingKnife and Javelin damage use this thrown magnitude.
Damage by weapon_class, rounded half to even:
weapon_class |
swing_damage |
thrust_damage |
|---|---|---|
| Melee classes | swing magnitude · Swing@damage_factor |
thrust magnitude · Thrust@damage_factor |
ThrowingAxe |
0 |
2 · swing magnitude · Swing@damage_factor |
ThrowingKnife |
0 |
3.3 · thrown magnitude · Thrust@damage_factor |
Javelin |
0 |
9 · thrown magnitude · Thrust@damage_factor |
The damage factors and types are the blade's. swing_damage_type and thrust_damage_type are the blade's Swing and Thrust damage types.
sweet_spot: the impact point p = 0, 0.01, …, 0.99 with the largest strike(p); on a tie the first one counts (CraftingStats.CalculateSweetSpot).
Per-usage stats
Thrown constants, from CraftingStats.SetWeaponData and CalculateMissileSpeed:
weapon_class |
accuracy |
missile_speed |
|---|---|---|
ThrowingAxe |
93 | floor(3.2 · raw thrust speed) |
ThrowingKnife |
95 | floor(3.9 · raw thrust speed) |
Javelin |
92 | floor(3.6 · raw thrust speed) |
Melee usages have accuracy 0 and missile_speed 0.
A usage is available when every piece of the design is in its AvailablePieces. Available usages keep the template's WeaponDescriptions order.
stack_amount: a thrown usage that is the first available usage gets the blade'sstack_amount; a later thrown usage gets 1; melee usages store 0.TotalInertia:inertia × stack_amountfor thrown usages,inertiaotherwise. The speed and handling formulas never read it.body_armor: the guard'sarmor_bonus, or 0 without a guard.
Quirks
GuardandPommelweight scales withscale³by default, so scaling them changes weight much faster than scaling a handle or a default blade.inertiauses each piece's unscaledinertiaandcenter_of_mass. Scale reaches it only throughscaled_weightand the running position.inertiawalksPieceDatasin XML order, withPommellast, so the pommel is placed past the blade tip.center_of_massplaces it correctly. A heavy pommel raisesinertiafar more than its real position would.handlingdepends oninertia_grip, notinertia, and is a power law.StatContributionshandling_bonus,swing_damage_bonus,swing_speed_bonus,thrust_damage_bonus,thrust_speed_bonusandaccuracy_bonusare loaded, butTaleWorlds.Corenever reads them.- Thrown usages always use
OneHandedconstants: their flags lackMeleeWeapon. - Two-handed polearm (couchable),
TwoHandedPolearm_Couchable, lacksNotUsableWithOneHand, so it usesOneHandedWideGripconstants, unlike the other two-handed polearm usages. - Swing damage uses the blade's
Swingfactor even onThrowingAxe, whose result lands inthrust_damage.
Validation
Compared with game-generated exports in data/data_exporter/ (craftable_item_usages*.csv, craftable_items*.csv), computing from current game XML. Exports predate the current build: recipes containing pieces whose XML changed since are skipped (battania_dagger_guard_9, dagger_blade_10, dagger_blade_11, dagger_blade_12, dagger_blade_13, dagger_blade_14, dagger_blade_6, dagger_blade_6_blunt, empire_dagger_guard_5, mace_head_24, mace_head_37, mace_head_38, sickle_handle_1, spear_blade_29). Blank export cells are not compared.
Export coverage:
| Files | Recipes | Scale |
|---|---|---|
craftable_items.csv, craftable_item_usages.csv |
627,602, from up to 20 random pieces per slot per template | 100 only |
craftable_items_with_scale.csv, craftable_item_usages_with_scale.csv |
1 per template | 90..110 on every slot |
Unscaled rows left after skips use 461 of 954 pieces. 11 of the 12 scaled recipes remain: Throwing Knife uses the changed dagger_blade_11. mace_head_10 and mace_head_34 cover explicit full_scale blades.
Random sample of up to 500 per template and usage:
| Stat | Unscaled | Scaled |
|---|---|---|
weight |
10098/10098 | 8264/8264 |
weapon_length |
10098/10098 | 8264/8264 |
center_of_mass |
10098/10098 | 8264/8264 |
total_inertia |
10098/10098 | 8264/8264 |
handling |
10098/10098 | 8264/8264 |
weapon_balance |
10098/10098 | 8264/8264 |
sweet_spot |
10098/10098 | 8264/8264 |
speed_rating |
5818/5818 | 4441/4441 |
thrust_speed |
5961/5961 | 4882/4882 |
swing_damage |
5818/5818 | 4441/4441 |
thrust_damage |
5961/5961 | 4882/4882 |
thrust_damage (thrown) |
2012/2012 | 941/941 |
missile_speed |
2012/2012 | 941/941 |
accuracy |
10098/10098 | 8264/8264 |
body_armor |
10098/10098 | 8264/8264 |
Tolerances: weight, weapon_balance, sweet_spot ±0.0051; center_of_mass, total_inertia ±0.001; integers exact. Available usages, tier, value and price checks are in Value and price.
Game code
| Formula | Method |
|---|---|
| Piece loading | TaleWorlds.Core.CraftingPiece.Deserialize, BladeData.Deserialize |
| Geometry | WeaponDesign.CalculatePivotDistances, CalculateWeaponLength; CraftingStats.CalculateCenterOfMass, CalculateWeaponInertia |
| Speed, handling, damage | Crafting.CraftedItemGenerationHelper.CraftingStats |
| Impact | TaleWorlds.Core.CombatStatCalculator |
| Usage availability, stack | CraftedItemGenerationHelper.GenerateCraftedItem, WeaponComponentData.Init |
IL decoded from game v1.4.8:
- CraftingPiece.il:
CraftingPiece, includingDeserialize. - BladeData.il:
BladeData, includingDeserialize. - WeaponDesign.il:
WeaponDesign, includingCalculatePivotDistancesandCalculateWeaponLength. - CraftingStats.il: all 22 methods of
CraftingStats: centre of mass, inertia, speed simulation, handling, damage, sweet spot, missile speed andSetWeaponData. - CombatStatCalculator.il: the impact energy.
- CraftedItemGenerationHelper.il:
GenerateCraftedItem. - WeaponComponentData.il:
WeaponComponentData, includingInit.
Changelog
- v1.4.8 (2026-11-11): Noted NavalDLC piece redefinitions.
- v1.4.8 (2026-11-11): Rewritten as deep dive; facts unchanged.
- v1.4.8 (2026-11-10): Moved into the site; content unchanged.