Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions src/unitStats/dpsCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ type CustomizableUnit = {
faction: string; // from folder structure races\[factionName]
weapon_member: WeaponMember[]; // set of weapons + amount
def_weapon_member: WeaponMember[];
unit_type: string; // folder Infantry | vehicles | team_weapons | buildings
unit_type: SbpsType["unitType"]; // folder Infantry | vehicles | team_weapons | buildings
help_text: string; // sbpextensions\squad_ui_ext\race_list\race_data\info\help_text
icon_name: string; // sbpextensions\squad_ui_ext\race_list\race_data\info\icon_name
faction_icon: string;
Expand Down Expand Up @@ -228,14 +228,13 @@ export const mapCustomizableUnit = (
custUnit.def_damage_type = (custUnit.weapon_member[0] as any).parent || null;

// Armor
const ebpsUnit = ebps.find((unit) => unit.id == custUnit.def_member);
const def_ebps = ebps.find((unit) => unit.id == custUnit.def_member);
custUnit.armor =
ebpsUnit?.health.armorLayout?.armor || ebpsUnit?.health.armorLayout?.frontArmor || 1;
def_ebps?.health.armorLayout?.armor || def_ebps?.health.armorLayout?.frontArmor || 1;
if (custUnit.unit_type == "vehicles")
custUnit.armor = ebpsUnit?.health.armorLayout.frontArmor || 1;
custUnit.armor = def_ebps?.health.armorLayout.frontArmor || 1;

// squad hitpoints
const def_ebps = ebps.find((unit) => unit.id == custUnit.def_member);
if (def_ebps) {
custUnit.ebps_default = def_ebps;
custUnit.hitpoints = def_ebps.health.hitpoints;
Expand Down
5 changes: 3 additions & 2 deletions src/unitStats/mappingSbps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ type SbpsType = {
ui: SquadUiData;
/** Found at `squad_upgrade_ext.upgrades`. List of instance references. */
upgrades: string[];
unitType: string;
/** The parent tag within the source JSON. */
unitType: "aircraft" | "emplacements" | "infantry" | "team_weapons" | "vehicles";
/** The `squad_population_ext` holds the base popcap and upkeep per pop per
* minute costs, which will be stacked with the ebps. */
populationExt: {
Expand Down Expand Up @@ -104,7 +105,7 @@ const mapSbpsData = (
screenName: filename,
path: internalSlash(jsonPath),
faction: jsonPath.split("/")[1] ?? jsonPath,
unitType: parent,
unitType: parent as SbpsType["unitType"],
loadout: [],
ui: {
iconName: "",
Expand Down
25 changes: 12 additions & 13 deletions src/unitStats/mappingWeapon.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { resolveLocstring } from "./locstring";
import { traverseTree } from "./unitStatsLib";
import config from "../../config";
import { WeaponBagSchema, WeaponCategory, WeaponClass } from "./types";
import { TargetUnitTypeMultipliers, WeaponBagSchema, WeaponCategory, WeaponClass } from "./types";

const WeaponPatchData: Record<string, Record<string, WeaponType[]>> = {}; // Modified to store by locale and patch

Expand Down Expand Up @@ -145,7 +145,7 @@ type WeaponType = {
};

type TargetType = {
unit_type: string;
unit_type: TargetUnitTypeMultipliers["unit_type"];
dmg_modifier: number;
accuracy_multiplier: number;
penetration_multiplier: number;
Expand Down Expand Up @@ -325,20 +325,19 @@ const mapWeaponData = (
},
};

if (weapon_bag.target_type_table)
for (const target_types of weapon_bag.target_type_table) {
if (weapon_bag.target_type_table && weapon_bag.target_type_table.length > 0) {
for (const { target_unit_type_multipliers } of weapon_bag.target_type_table) {
if (!target_unit_type_multipliers) continue;
const wm = target_unit_type_multipliers.weapon_multipliers ?? {};
weaponData.weapon_bag.target_type_table.push({
unit_type: target_types.target_unit_type_multipliers?.unit_type || "",
dmg_modifier: target_types.target_unit_type_multipliers?.base_damage_modifier || 0,
accuracy_multiplier:
target_types.target_unit_type_multipliers?.weapon_multiplier?.accuracy_multiplier || 1,
penetration_multiplier:
target_types.target_unit_type_multipliers?.weapon_multiplier?.penetration_multiplier ||
1,
damage_multiplier:
target_types.target_unit_type_multiplier?.weapon_multipliers?.damage_multiplier || 1,
unit_type: target_unit_type_multipliers.unit_type,
dmg_modifier: target_unit_type_multipliers.base_damage_modifier ?? 0,
accuracy_multiplier: wm.accuracy_multiplier ?? 1,
penetration_multiplier: wm.penetration_multiplier ?? 1,
damage_multiplier: wm.damage_multiplier ?? 1,
});
}
}

return weaponData;
};
Expand Down
19 changes: 18 additions & 1 deletion src/unitStats/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export type WeaponBag = {
weapon_class: (typeof WeaponClass)[number];
fx_aoe_munition_name: string;
speech_code: string;
target_type_table: any[];
target_type_table: TargetTypeTableItemSchema[];
cover_table: CoverTable;
fx_piercing_munition_name: string;
charge_option: TemplateReferenceParentSchema;
Expand All @@ -115,6 +115,23 @@ export type WeaponBag = {
conditional_table: any[];
};

type TargetTypeTableItemSchema = {
target_unit_type_multipliers: TargetUnitTypeMultipliers;
};

export type TargetUnitTypeMultipliers = {
unit_type: "destructible_object" | "bunker" | "infantry" | "building";
weapon_multipliers: WeaponMultipliers;
base_damage_modifier: number;
};

type WeaponMultipliers = {
accuracy_multiplier: number;
damage_multiplier: number;
penetration_multiplier: number;
suppression_multiplier: number;
};

export type Accuracy = {
near: number;
far: number;
Expand Down
43 changes: 24 additions & 19 deletions src/unitStats/weaponLib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,28 +216,33 @@ const getSingleWeaponDPS = (
// check how many units will fit into the area
// const unitPerAreaApproximation = Math.min(Math.max(scatter_area / Math.pow(width,2),1),units)

let type_damage_mp = 1;
//if (target_unit && target_unit.weapon_member)
for (const modifier of weapon_bag.target_type_table) {
if (modifier.unit_type && target_unit && target_unit.unit_type)
type_damage_mp *= modifier.damage_multiplier;

// Some weapons like AT Guns or Bazookas deal no damage to infantry .
// At the moment it is not clear, which attribute exactly causes the zero damage effect.
// However, having the infantry in the target table is a good indicator to build a
// workaround.
if (
modifier.unit_type == "infantry" &&
(!target_unit || target_unit.unit_type == "infantry")
)
return 0;
/** These two applies towards target types. */
let target_damage_mp = 1;
let target_accuracy_mp = 1;
let target_damage_bonus = 0;

/** Find the correct `target_unit.unit_type` within the weapons target type
* list `unit_type`. */
const targetUnitTypes = target_unit?.ebps_default?.unitTypes || [];

for (const weaponTargetType of weapon_bag.target_type_table) {
const foundTargetType = targetUnitTypes.find((ut) => ut === weaponTargetType.unit_type);

if (foundTargetType) {
target_damage_bonus += weaponTargetType.dmg_modifier;
target_damage_mp *= weaponTargetType.damage_multiplier;
target_accuracy_mp *= weaponTargetType.accuracy_multiplier;
}
}
Comment on lines +228 to 236
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add safety check for target_type_table and clarify accumulation logic.

The code accesses weapon_bag.target_type_table without verifying it exists or is iterable, which could cause a runtime error if undefined.

Additionally, the accumulation logic using += for target_damage_bonus and *= for multipliers implies that if multiple weapon target types match multiple unit types on the target, the bonuses and multipliers will stack. Typically, you'd expect only one target type to match. If stacking is intentional (e.g., a unit has both "infantry" and "heavy_armor" types and the weapon has entries for both), please add a comment explaining this behavior. Otherwise, consider using early return or break after the first match.

Apply this diff to add a safety check:

     const targetUnitTypes = target_unit?.ebps_default?.unitTypes || [];
 
-    for (const weaponTargetType of weapon_bag.target_type_table) {
+    for (const weaponTargetType of weapon_bag.target_type_table || []) {
       const foundTargetType = targetUnitTypes.find((ut) => ut === weaponTargetType.unit_type);
 
       if (foundTargetType) {
         target_damage_bonus += weaponTargetType.dmg_modifier;
         target_damage_mp *= weaponTargetType.damage_multiplier;
         target_accuracy_mp *= weaponTargetType.accuracy_multiplier;
       }
     }

If only one match is intended, consider adding a break:

       if (foundTargetType) {
         target_damage_bonus += weaponTargetType.dmg_modifier;
         target_damage_mp *= weaponTargetType.damage_multiplier;
         target_accuracy_mp *= weaponTargetType.accuracy_multiplier;
+        break; // Only apply the first matching target type
       }
🤖 Prompt for AI Agents
In src/unitStats/weaponLib.ts around lines 228-236, add a safety check that
weapon_bag.target_type_table exists and is iterable before iterating (e.g., skip
the loop if it's falsy or not an array) to avoid runtime errors; also clarify
accumulation intent by either breaking out of the loop after the first matching
target type (if only one match is expected) or, if stacking multiple matches is
intentional, add a concise comment above the loop explaining that dmg_modifier
and multipliers should accumulate across multiple matching target types so the
use of += and *= is deliberate.


// new
const aoe_damage_multi = aoe_damage * type_damage_mp * aoePenetrationChance * memberHit;
/** Compute the combined AOE damage multipliers. */
const aoe_damage_multi =
aoe_damage * target_damage_mp * target_accuracy_mp * aoePenetrationChance * memberHit;

// new
aoeDamageCombines = aoe_damage_multi;
/** Compute the final AOE damage combined with the base damage modifier (if
* applies). */
// aoeDamageCombines = aoe_damage_multi;
aoeDamageCombines = target_damage_bonus + aoe_damage_multi;
Comment on lines +228 to +245
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for TargetTypeTableItemSchema and target_type_table definitions

echo "=== Searching for TargetTypeTableItemSchema ==="
rg -n "TargetTypeTableItemSchema" --type ts --type js -B5 -A10 || true

echo -e "\n=== Searching for target_type_table ==="
rg -n "target_type_table" --type ts --type js -B3 -A3 || true

echo -e "\n=== Searching for WeaponBag type definition ==="
rg -n "WeaponBag" --type ts --type js -B2 -A10 | head -100 || true

Repository: cohstats/coh3-stats

Length of output: 11304


🏁 Script executed:

#!/bin/bash
# Get the WeaponMultipliers definition and check the broader context in weaponLib.ts

echo "=== WeaponMultipliers type definition ==="
rg -n "type WeaponMultipliers" --type ts -A10 src/unitStats/types.ts

echo -e "\n=== Full context around the problem area in weaponLib.ts ==="
sed -n '200,260p' src/unitStats/weaponLib.ts | cat -n

echo -e "\n=== Check TargetType in mappingWeapon.ts ==="
rg -n "^type TargetType" --type ts -A10 src/unitStats/mappingWeapon.ts

Repository: cohstats/coh3-stats

Length of output: 3425


🏁 Script executed:

#!/bin/bash
# Find the function containing the problem code and check its signature

echo "=== Search for function containing lines 228-245 ==="
sed -n '150,260p' src/unitStats/weaponLib.ts | cat -n

echo -e "\n=== Check imports in weaponLib.ts to understand types ==="
head -30 src/unitStats/weaponLib.ts | cat -n

Repository: cohstats/coh3-stats

Length of output: 5885


🏁 Script executed:

#!/bin/bash
# Find WeaponStatsType definition

echo "=== WeaponStatsType definition ==="
rg -n "^type WeaponStatsType|^export type WeaponStatsType" --type ts -A50 src/unitStats/mappingWeapon.ts | head -100

Repository: cohstats/coh3-stats

Length of output: 1656


🏁 Script executed:

#!/bin/bash
# Get more of the WeaponStatsType definition, particularly the weapon_bag section

echo "=== Full WeaponStatsType definition focusing on weapon_bag ==="
rg -n "weapon_bag:" --type ts -B5 -A20 src/unitStats/mappingWeapon.ts

echo -e "\n=== Lines around target_type_table in WeaponStatsType ==="
rg -n "target_type_table" --type ts -B3 -A3 src/unitStats/mappingWeapon.ts

Repository: cohstats/coh3-stats

Length of output: 4539


🏁 Script executed:

#!/bin/bash
# Check the complete context around AOE damage calculations and direct hit damage to verify the second set of concerns

echo "=== Check direct hit damage calculation and how it relates to target-type multipliers ==="
sed -n '100,200p' src/unitStats/weaponLib.ts | cat -n

echo -e "\n=== Check the finalDamage calculation ==="
rg -n "finalDamage|finalHitChance|finalAccuracy" --type ts -B2 -A2 src/unitStats/weaponLib.ts | head -60

Repository: cohstats/coh3-stats

Length of output: 5114


Clarify target-type multiplier application and target_damage_bonus scaling

A couple of design clarifications needed:

  • The schema properties (unit_type, dmg_modifier, damage_multiplier, accuracy_multiplier) are correctly accessed at the top level—they have already been flattened by the mapping function in mappingWeapon.ts before reaching this code.

  • target_damage_bonus is added after all other AoE factors as aoeDamageCombines = target_damage_bonus + aoe_damage_multi. This means:

    • It is not scaled by aoePenetrationChance or memberHit.
    • It contributes to DPS even when aoe_damage_multi is zero (e.g., when aoe_outer_radius is 0).
      If dmg_modifier is intended as "extra damage per effective AoE application", consider folding it into the same chain as aoe_damage rather than adding it unscaled.
  • Currently the target-type multipliers only affect the AoE term (aoeDamageCombines), not the direct-hit term finalHitChance * finalDamage. If target-type multipliers should influence all damage against that target (not just splash), propagate the multipliers into finalDamage / finalAccuracy as well. If AoE-only is deliberate (e.g., to model AT-gun AoE vs infantry), a code comment explaining that choice would help future readers.

🤖 Prompt for AI Agents
In src/unitStats/weaponLib.ts around lines 228-245, the code adds
target_damage_bonus after AoE multipliers which leaves it unscaled by
aoePenetrationChance/memberHit and by the target multipliers; to fix this, fold
the dmg_modifier into the AoE chain by applying the same multipliers (multiply
target_damage_bonus by target_damage_mp * target_accuracy_mp *
aoePenetrationChance * memberHit and add that into aoe_damage_multi instead of
adding it raw), and if target-type multipliers should also affect direct hits,
apply target_damage_mp to finalDamage and target_accuracy_mp to finalAccuracy
(or document intentionally AoE-only behavior if you do not propagate them).

}

/* Combined accuracy */
Expand Down