OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
 
master @ 540 LINES
 
[ HISTORY ]  [ UP ]
 

{
  lib,
  pkgs,
  classes,
  buffs,
  debuffs,
  inputs,
  ...
}: let
  # TODO: use writers.writePython3Bin
  inherit (lib.sim.simulation) mkSim;
  inherit (lib.sim) itemDatabase shellUtils;
  inherit (lib) length listToAttrs hasAttr flatten mapAttrsToList;

  # extract playable races from class definitions
  getPlayableRaces = className:
    if lib.hasAttr className classes && hasAttr "playableRaces" classes.${classNam
e}
    then classes.${className}.playableRaces
    else throw "playableRaces not defined for class ${className}. Please add playa
bleRaces = [...] to nix/classes/${className}/default.nix";

  # categorize specs by role
  # TODO: tanks
  getAllDPSSpecs = classes: template: phase: let
    dpsSpecs = {
      death_knight = ["frost" "unholy"];
      druid = ["balance" "feral"];
      hunter = ["beast_mastery" "marksmanship" "survival"];
      mage = ["arcane" "fire" "frost"];
      monk = ["windwalker"];
      paladin = ["retribution"];
      priest = ["shadow"];
      rogue = ["assassination" "combat" "subtlety"];
      shaman = ["elemental" "enhancement"];
      warlock = ["affliction" "demonology" "destruction"];
      warrior = ["arms" "fury"];
    };

    # extract specs that exist in classes and have the template structure
    validSpecs = flatten (mapAttrsToList (
        className: specNames:
          lib.filter (spec: spec != null) (map (
              specName:
                if
                  hasAttr className classes
                  && hasAttr specName classes.${className}
                  && hasAttr "defaultRace" classes.${className}.${specName}
                  && hasAttr "template" classes.${className}.${specName}
                then let
                  inherit (classes.${className}.${specName}) defaultRace;
                  spec = classes.${className}.${specName};
                in
                  if
                    hasAttr defaultRace spec.template
                    && hasAttr phase spec.template.${defaultRace}
                    && hasAttr "raid" spec.template.${defaultRace}.${phase}
                    && hasAttr template spec.template.${defaultRace}.${phase}.raid
                  then {
                    inherit className specName;
                    config = spec.template.${defaultRace}.${phase}.raid.${template
};
                  }
                  else null
                else null
            )
            specNames)
      )
      dpsSpecs);
  in
    validSpecs;

  getRaceConfigs = classes: className: specName: template: phase: encounterType: l
et
    availableRaces = getPlayableRaces className;
    baseSpec = classes.${className}.${specName};
  in
    map (raceName: {
      inherit className specName raceName;
      config =
        if
          # validate template exists
          hasAttr "template" baseSpec
          && hasAttr raceName baseSpec.template
          && hasAttr phase baseSpec.template.${raceName}
          && hasAttr encounterType baseSpec.template.${raceName}.${phase}
          && hasAttr template baseSpec.template.${raceName}.${phase}.${encounterTy
pe}
        then baseSpec.template.${raceName}.${phase}.${encounterType}.${template}
        else throw "Template ${template} not found for ${className}/${specName}/${
raceName} at ${phase}.${encounterType}";
    })
    availableRaces;

  mkRaceComparison = {
    class,
    spec,
    encounter,
    iterations ? 10000,
    phase ? "p1",
    encounterType ? "raid",
    targetCount ? "single",
    duration ? "long",
    template ? "singleTarget",
    wowsimsCommit ? inputs.wowsims-upstream.shortRev,
  }: let
    raceConfigs = getRaceConfigs classes class spec template phase encounterType;

    # create individual simulation derivations for each race
    simDerivations = listToAttrs (map (raceConfig: {
        name = "${raceConfig.className}-${raceConfig.specName}-${raceConfig.raceNa
me}";
        value = let
          # pre enrich the equipment, consumables, glyphs, and talents for this ra
ce config
          enrichedEquipment = itemDatabase.enrichEquipment raceConfig.config.equip
ment;
          enrichedConsumables = itemDatabase.enrichConsumables raceConfig.config.c
onsumables;
          enrichedGlyphs = itemDatabase.enrichGlyphs raceConfig.config.class raceC
onfig.config.glyphs;
          enrichedTalents = itemDatabase.enrichTalents raceConfig.config.class rac
eConfig.config.talentsString;

          # warrior and shaman should be included in the their respective buff cou
nt, not in addition to
          classSpecificBuffs =
            if raceConfig.config.class == "ClassWarrior"
            then buffs.full // {skullBannerCount = 1;}
            else if raceConfig.config.class == "ClassShaman"
            then buffs.full // {stormlashTotemCount = 3;}
            else buffs.full;

          simInput = mkSim {
            inherit iterations;
            player = raceConfig.config;
            buffs = classSpecificBuffs;
            debuffs = debuffs.full;
            inherit encounter;
          };
        in
          pkgs.runCommand "race-sim-${raceConfig.className}-${raceConfig.specName}
-${raceConfig.raceName}" {
            buildInputs = [pkgs.jq];
            nativeBuildInputs = [inputs.wowsims.packages.${pkgs.system}.wowsimcli]
;
          } ''
            # Write enriched JSON data to files using cat with EOF to handle any q
uotes safely
            cat > enriched_equipment.json << 'EQUIPMENT_EOF'
              ${builtins.toJSON enrichedEquipment}
            EQUIPMENT_EOF

            cat > consumables.json << 'CONSUMABLES_EOF'
              ${builtins.toJSON enrichedConsumables}
            CONSUMABLES_EOF

            cat > glyphs.json << 'GLYPHS_EOF'
              ${builtins.toJSON enrichedGlyphs}
            GLYPHS_EOF

            cat > talents.json << 'TALENTS_EOF'
              ${builtins.toJSON enrichedTalents}
            TALENTS_EOF

            cat > input.json << 'INPUT_EOF'
              ${simInput}
            INPUT_EOF

            echo "Running ${raceConfig.className}/${raceConfig.specName}/${raceCon
fig.raceName} simulation..."
            if wowsimcli sim --infile input.json --outfile output.json; then
              # extract dps statistics
              avgDps=$(jq -r '.raidMetrics.dps.avg // 0' output.json)
              maxDps=$(jq -r '.raidMetrics.dps.max // 0' output.json)
              minDps=$(jq -r '.raidMetrics.dps.min // 0' output.json)
              stdevDps=$(jq -r '.raidMetrics.dps.stdev // 0' output.json)

              # Generate wowsim link from input file
              echo "Generating wowsim link..."
              simLink=$(wowsimcli encodelink input.json || echo "")

              # create final result with enriched data
              jq -n \
                --arg raceName "${raceConfig.raceName}" \
                --arg avgDps "$avgDps" \
                --arg maxDps "$maxDps" \
                --arg minDps "$minDps" \
                --arg stdevDps "$stdevDps" \
                --arg simLink "$simLink" \
                --slurpfile equipment enriched_equipment.json \
                --slurpfile consumables consumables.json \
                --arg talentsString "${raceConfig.config.talentsString}" \
                --slurpfile talents talents.json \
                --slurpfile glyphs glyphs.json \
                --arg race "${raceConfig.config.race}" \
                --arg class "${raceConfig.config.class}" \
                --arg profession1 "${raceConfig.config.profession1}" \
                --arg profession2 "${raceConfig.config.profession2}" \
                '{
                  race: $raceName,
                  dps: ($avgDps | tonumber),
                  max: ($maxDps | tonumber),
                  min: ($minDps | tonumber),
                  stdev: ($stdevDps | tonumber),
                  loadout: {
                    consumables: $consumables[0],
                    talentsString: $talentsString,
                    talents: $talents[0],
                    glyphs: $glyphs[0],
                    equipment: $equipment[0],
                    race: $race,
                    class: $class,
                    profession1: $profession1,
                    profession2: $profession2,
                    simLink: $simLink
                  }
                }' > $out
              else
                echo "Simulation failed for ${raceConfig.className}/${raceConfig.s
pecName}/${raceConfig.raceName}"
                exit 1
              fi
          '';
      })
      raceConfigs);

    structuredOutput = "${class}_${spec}_race_${phase}_${encounterType}_${targetCo
unt}_${duration}";

    aggregationScript = pkgs.writeShellApplication {
      name = "${structuredOutput}-aggregator";
      text = ''
        ${shellUtils.parseArgsAndEnv}

        echo "Aggregating race comparison results for: ${class}/${spec}"
        echo "Races simulated: ${toString (lib.length raceConfigs)}"

        result=$(jq -n '{}')

        ${lib.concatMapStringsSep "\n" (raceConfig: ''
            # ${raceConfig.raceName} results
            raceData=$(cat ${simDerivations."${raceConfig.className}-${raceConfig.
specName}-${raceConfig.raceName}"})

            result=$(echo "$result" | jq \
              --argjson race "$raceData" \
              --arg raceName "${raceConfig.raceName}" \
              '.[$raceName] = {
                dps: $race.dps,
                max: $race.max,
                min: $race.min,
                stdev: $race.stdev,
                loadout: $race.loadout
              }')
          '')
          raceConfigs}

        finalResult=$(echo "$result" | jq \
          --arg class "${class}" \
          --arg spec "${spec}" \
          --arg encounter "${phase}_${encounterType}_${targetCount}_${duration}" \
          --arg timestamp "$(date -Iseconds)" \
          --arg iterations "${toString iterations}" \
          --arg encounterDuration "${toString encounter.duration}" \
          --arg encounterVariation "${toString encounter.durationVariation}" \
          --arg targetCount "${toString (lib.length encounter.targets)}" \
          --arg wowsimsCommit "${wowsimsCommit}" \
          --argjson raidBuffs '${builtins.toJSON buffs.full}' \
          '{
            metadata: {
              spec: $spec,
              class: $class,
              encounter: $encounter,
              timestamp: $timestamp,
              iterations: ($iterations | tonumber),
              encounterDuration: ($encounterDuration | tonumber),
              encounterVariation: ($encounterVariation | tonumber),
              targetCount: ($targetCount | tonumber),
              wowsimsCommit: $wowsimsCommit,
              raidBuffs: $raidBuffs
            },
            results: .
          }')

        ${shellUtils.conditionalOutput {
          inherit structuredOutput;
          webSetupCode = shellUtils.setupComparisonDirs {
            comparisonType = "race";
            class = "${class}";
            spec = "${spec}";
          };
          webPath = "$comparison_dir";
          webMessage = "Copied to";
        }}

        echo ""
        echo "Race DPS Rankings for ${class}/${spec}:"
        echo "======================================="
        echo "$finalResult" | jq -r '
          .results | to_entries[] |
          select(.value.dps != null and .value.dps > 0) |
          "\(.key): \(.value.dps | floor) DPS"
        ' | sort -k2 -nr

        # Show any failed races
        failed_races=$(echo "$finalResult" | jq -r '
          .results | to_entries[] |
          select(.value.dps == null or .value.dps <= 0) |
          .key
        ')

        if [[ -n "$failed_races" ]]; then
          echo ""
          echo "Failed races (no valid DPS data):"
          echo "$failed_races"
        fi

      '';
      runtimeInputs = [pkgs.jq pkgs.coreutils];
    };
  in {
    simulations = simDerivations;

    script = aggregationScript;

    metadata = {
      output = structuredOutput;
      inherit class spec iterations phase encounterType targetCount duration;
      raceCount = lib.length raceConfigs;
      races = map (r: r.raceName) raceConfigs;
    };
  };

  mkMassSim = {
    specs ? "dps",
    encounter,
    iterations ? 10000,
    phase ? "p1",
    encounterType ? "raid",
    targetCount ? "single",
    duration ? "long",
    template ? "singleTarget",
    wowsimsCommit ? inputs.wowsims-upstream.shortRev,
  }: let
    # get the list of specs based on the specs parameter
    specConfigs =
      if specs == "dps"
      then getAllDPSSpecs classes template phase
      else if builtins.isList specs
      then specs
      else throw "specs must be 'dps' or a list of spec configurations";

    # create individual simulation derivations for each spec
    simDerivations = lib.listToAttrs (map (spec: {
        name = "${spec.className}-${spec.specName}";
        value = let
          # Pre-enrich the equipment, consumables, glyphs, and talents for this sp
ec
          enrichedEquipment = itemDatabase.enrichEquipment spec.config.equipment;
          enrichedConsumables = itemDatabase.enrichConsumables spec.config.consuma
bles;
          enrichedGlyphs = itemDatabase.enrichGlyphs spec.config.class spec.config
.glyphs;
          enrichedTalents = itemDatabase.enrichTalents spec.config.class spec.conf
ig.talentsString;

          # Create class-specific buff adjustments
          classSpecificBuffs =
            if spec.config.class == "ClassWarrior"
            then buffs.full // {skullBannerCount = 1;}
            else if spec.config.class == "ClassShaman"
            then buffs.full // {stormlashTotemCount = 3;}
            else buffs.full;

          simInput = mkSim {
            inherit iterations;
            player = spec.config;
            buffs = classSpecificBuffs;
            debuffs = debuffs.full;
            inherit encounter;
          };
        in
          pkgs.runCommand "sim-${spec.className}-${spec.specName}" {
            buildInputs = [pkgs.jq];
            nativeBuildInputs = [inputs.wowsims.packages.${pkgs.system}.wowsimcli]
;
          } ''
            cat > enriched_equipment.json << 'EQUIPMENT_EOF'
              ${builtins.toJSON enrichedEquipment}
            EQUIPMENT_EOF

            cat > consumables.json << 'CONSUMABLES_EOF'
              ${builtins.toJSON enrichedConsumables}
            CONSUMABLES_EOF

            cat > glyphs.json << 'GLYPHS_EOF'
              ${builtins.toJSON enrichedGlyphs}
            GLYPHS_EOF

            cat > talents.json << 'TALENTS_EOF'
              ${builtins.toJSON enrichedTalents}
            TALENTS_EOF

            cat > input.json << 'INPUT_EOF'
              ${simInput}
            INPUT_EOF

            echo "Running ${spec.className}/${spec.specName} simulation..."
            if wowsimcli sim --infile input.json --outfile output.json; then

              avgDps=$(jq -r '.raidMetrics.dps.avg // 0' output.json)
              maxDps=$(jq -r '.raidMetrics.dps.max // 0' output.json)
              minDps=$(jq -r '.raidMetrics.dps.min // 0' output.json)
              stdevDps=$(jq -r '.raidMetrics.dps.stdev // 0' output.json)

              # Generate wowsim link from input file
              echo "Generating wowsim link..."
              simLink=$(wowsimcli encodelink input.json || echo "")

              # create final result with all DPS statistics and enriched data
              jq -n \
                --arg className "${spec.className}" \
                --arg specName "${spec.specName}" \
                --arg avgDps "$avgDps" \
                --arg maxDps "$maxDps" \
                --arg minDps "$minDps" \
                --arg stdevDps "$stdevDps" \
                --arg simLink "$simLink" \
                --slurpfile equipment enriched_equipment.json \
                --slurpfile consumables consumables.json \
                --arg talentsString "${spec.config.talentsString}" \
                --slurpfile talents talents.json \
                --slurpfile glyphs glyphs.json \
                --arg race "${spec.config.race}" \
                --arg class "${spec.config.class}" \
                --arg profession1 "${spec.config.profession1}" \
                --arg profession2 "${spec.config.profession2}" \
                '{
                  className: $className,
                  specName: $specName,
                  dps: ($avgDps | tonumber),
                  max: ($maxDps | tonumber),
                  min: ($minDps | tonumber),
                  stdev: ($stdevDps | tonumber),
                  loadout: {
                    consumables: $consumables[0],
                    talentsString: $talentsString,
                    talents: $talents[0],
                    glyphs: $glyphs[0],
                    equipment: $equipment[0],
                    race: $race,
                    class: $class,
                    profession1: $profession1,
                    profession2: $profession2,
                    simLink: $simLink
                  }
                }' > $out
            else
              echo "Simulation failed for ${spec.className}/${spec.specName}"
              exit 1
            fi
          '';
      })
      specConfigs);

    # generate structured output filename: <type>_<phase>_<encounter-type>_<target
-count>_<duration>
    structuredOutput = "${specs}_${phase}_${encounterType}_${targetCount}_${durati
on}";

    # aggregation script that combines all results
    aggregationScript = pkgs.writeShellApplication {
      name = "${structuredOutput}-aggregator";
      text = ''
        set -euo pipefail

        ${shellUtils.parseArgsAndEnv}

        echo "Aggregating mass simulation results for: ${structuredOutput}"
        echo "Specs simulated: ${toString (lib.length specConfigs)}"

        # create base structure
        result=$(jq -n '{}')

        ${lib.concatMapStringsSep "\n" (spec: ''
            # Add ${spec.className}/${spec.specName} results
            specData=$(cat ${simDerivations."${spec.className}-${spec.specName}"})

            result=$(echo "$result" | jq \
              --argjson spec "$specData" \
              --arg className "${spec.className}" \
              --arg specName "${spec.specName}" \
              '.[$className][$specName] = {
                dps: $spec.dps,
                max: $spec.max,
                min: $spec.min,
                stdev: $spec.stdev,
                loadout: $spec.loadout
              }')
          '')
          specConfigs}

        # Create final output with metadata including encounter information
        finalResult=$(echo "$result" | jq \
          --arg output "${structuredOutput}" \
          --arg timestamp "$(date -Iseconds)" \
          --arg iterations "${toString iterations}" \
          --arg specCount "${toString (lib.length specConfigs)}" \
          --arg encounterDuration "${toString encounter.duration}" \
          --arg encounterVariation "${toString encounter.durationVariation}" \
          --arg targetCount "${toString (lib.length encounter.targets)}" \
          --arg wowsimsCommit "${wowsimsCommit}" \
          --argjson raidBuffs '${builtins.toJSON buffs.full}' \
          '{
            metadata: {
              name: $output,
              timestamp: $timestamp,
              iterations: ($iterations | tonumber),
              specCount: ($specCount | tonumber),
              encounterDuration: ($encounterDuration | tonumber),
              encounterVariation: ($encounterVariation | tonumber),
              targetCount: ($targetCount | tonumber),
              wowsimsCommit: $wowsimsCommit,
              raidBuffs: $raidBuffs
            },
            results: .
          }')

        ${shellUtils.conditionalOutput {
          inherit structuredOutput;
          webSetupCode = shellUtils.setupRankingsDirs;
          webPath = "$rankings_dir";
          webMessage = "Copied to";
        }}

        echo ""
        echo "Top DPS Rankings:"
        echo "=================="
        echo "$finalResult" | jq -r '
          .results | to_entries[] as $class |
          $class.key as $className |
          $class.value | to_entries[] as $spec |
          "\($className)/\($spec.key): \($spec.value.dps | floor) DPS"
        ' | sort -k2 -nr | head -10

      '';
      runtimeInputs = [pkgs.jq pkgs.coreutils];
    };
  in {
    # individual simulation derivations (for debugging)
    simulations = simDerivations;

    # main aggregation script
    script = aggregationScript;

    metadata = {
      output = structuredOutput;
      inherit iterations phase encounterType targetCount duration;
      specCount = length specConfigs;
      specs = map (s: "${s.className}/${s.specName}") specConfigs;
    };
  };
in {
  inherit mkMassSim getAllDPSSpecs mkRaceComparison getRaceConfigs getPlayableRace
s;
}

{
  lib,
  pkgs,
  classes,
  buffs,
  debuffs,
  inputs,
  ...
}: let
  # TODO: use writers.writePython3Bin
  inherit (lib.sim.simulation) mkSim;
  inherit (lib.sim) itemDatabase shellUtils;
  inherit (lib) length listToAttrs hasAttr f
latten mapAttrsToList;

  # extract playable races from class defini
tions
  getPlayableRaces = className:
    if lib.hasAttr className classes && hasA
ttr "playableRaces" classes.${className}
    then classes.${className}.playableRaces
    else throw "playableRaces not defined fo
r class ${className}. Please add playableRac
es = [...] to nix/classes/${className}/defau
lt.nix";

  # categorize specs by role
  # TODO: tanks
  getAllDPSSpecs = classes: template: phase:
 let
    dpsSpecs = {
      death_knight = ["frost" "unholy"];
      druid = ["balance" "feral"];
      hunter = ["beast_mastery" "marksmanshi
p" "survival"];
      mage = ["arcane" "fire" "frost"];
      monk = ["windwalker"];
      paladin = ["retribution"];
      priest = ["shadow"];
      rogue = ["assassination" "combat" "sub
tlety"];
      shaman = ["elemental" "enhancement"];
      warlock = ["affliction" "demonology" "
destruction"];
      warrior = ["arms" "fury"];
    };

    # extract specs that exist in classes an
d have the template structure
    validSpecs = flatten (mapAttrsToList (
        className: specNames:
          lib.filter (spec: spec != null) (m
ap (
              specName:
                if
                  hasAttr className classes
                  && hasAttr specName classe
s.${className}
                  && hasAttr "defaultRace" c
lasses.${className}.${specName}
                  && hasAttr "template" clas
ses.${className}.${specName}
                then let
                  inherit (classes.${classNa
me}.${specName}) defaultRace;
                  spec = classes.${className
}.${specName};
                in
                  if
                    hasAttr defaultRace spec
.template
                    && hasAttr phase spec.te
mplate.${defaultRace}
                    && hasAttr "raid" spec.t
emplate.${defaultRace}.${phase}
                    && hasAttr template spec
.template.${defaultRace}.${phase}.raid
                  then {
                    inherit className specNa
me;
                    config = spec.template.$
{defaultRace}.${phase}.raid.${template};
                  }
                  else null
                else null
            )
            specNames)
      )
      dpsSpecs);
  in
    validSpecs;

  getRaceConfigs = classes: className: specN
ame: template: phase: encounterType: let
    availableRaces = getPlayableRaces classN
ame;
    baseSpec = classes.${className}.${specNa
me};
  in
    map (raceName: {
      inherit className specName raceName;
      config =
        if
          # validate template exists
          hasAttr "template" baseSpec
          && hasAttr raceName baseSpec.templ
ate
          && hasAttr phase baseSpec.template
.${raceName}
          && hasAttr encounterType baseSpec.
template.${raceName}.${phase}
          && hasAttr template baseSpec.templ
ate.${raceName}.${phase}.${encounterType}
        then baseSpec.template.${raceName}.$
{phase}.${encounterType}.${template}
        else throw "Template ${template} not
 found for ${className}/${specName}/${raceNa
me} at ${phase}.${encounterType}";
    })
    availableRaces;

  mkRaceComparison = {
    class,
    spec,
    encounter,
    iterations ? 10000,
    phase ? "p1",
    encounterType ? "raid",
    targetCount ? "single",
    duration ? "long",
    template ? "singleTarget",
    wowsimsCommit ? inputs.wowsims-upstream.
shortRev,
  }: let
    raceConfigs = getRaceConfigs classes cla
ss spec template phase encounterType;

    # create individual simulation derivatio
ns for each race
    simDerivations = listToAttrs (map (raceC
onfig: {
        name = "${raceConfig.className}-${ra
ceConfig.specName}-${raceConfig.raceName}";
        value = let
          # pre enrich the equipment, consum
ables, glyphs, and talents for this race con
fig
          enrichedEquipment = itemDatabase.e
nrichEquipment raceConfig.config.equipment;
          enrichedConsumables = itemDatabase
.enrichConsumables raceConfig.config.consuma
bles;
          enrichedGlyphs = itemDatabase.enri
chGlyphs raceConfig.config.class raceConfig.
config.glyphs;
          enrichedTalents = itemDatabase.enr
ichTalents raceConfig.config.class raceConfi
g.config.talentsString;

          # warrior and shaman should be inc
luded in the their respective buff count, no
t in addition to
          classSpecificBuffs =
            if raceConfig.config.class == "C
lassWarrior"
            then buffs.full // {skullBannerC
ount = 1;}
            else if raceConfig.config.class 
== "ClassShaman"
            then buffs.full // {stormlashTot
emCount = 3;}
            else buffs.full;

          simInput = mkSim {
            inherit iterations;
            player = raceConfig.config;
            buffs = classSpecificBuffs;
            debuffs = debuffs.full;
            inherit encounter;
          };
        in
          pkgs.runCommand "race-sim-${raceCo
nfig.className}-${raceConfig.specName}-${rac
eConfig.raceName}" {
            buildInputs = [pkgs.jq];
            nativeBuildInputs = [inputs.wows
ims.packages.${pkgs.system}.wowsimcli];
          } ''
            # Write enriched JSON data to fi
les using cat with EOF to handle any quotes 
safely
            cat > enriched_equipment.json <<
 'EQUIPMENT_EOF'
              ${builtins.toJSON enrichedEqui
pment}
            EQUIPMENT_EOF

            cat > consumables.json << 'CONSU
MABLES_EOF'
              ${builtins.toJSON enrichedCons
umables}
            CONSUMABLES_EOF

            cat > glyphs.json << 'GLYPHS_EOF
'
              ${builtins.toJSON enrichedGlyp
hs}
            GLYPHS_EOF

            cat > talents.json << 'TALENTS_E
OF'
              ${builtins.toJSON enrichedTale
nts}
            TALENTS_EOF

            cat > input.json << 'INPUT_EOF'
              ${simInput}
            INPUT_EOF

            echo "Running ${raceConfig.class
Name}/${raceConfig.specName}/${raceConfig.ra
ceName} simulation..."
            if wowsimcli sim --infile input.
json --outfile output.json; then
              # extract dps statistics
              avgDps=$(jq -r '.raidMetrics.d
ps.avg // 0' output.json)
              maxDps=$(jq -r '.raidMetrics.d
ps.max // 0' output.json)
              minDps=$(jq -r '.raidMetrics.d
ps.min // 0' output.json)
              stdevDps=$(jq -r '.raidMetrics
.dps.stdev // 0' output.json)

              # Generate wowsim link from in
put file
              echo "Generating wowsim link..
."
              simLink=$(wowsimcli encodelink
 input.json || echo "")

              # create final result with enr
iched data
              jq -n \
                --arg raceName "${raceConfig
.raceName}" \
                --arg avgDps "$avgDps" \
                --arg maxDps "$maxDps" \
                --arg minDps "$minDps" \
                --arg stdevDps "$stdevDps" \
                --arg simLink "$simLink" \
                --slurpfile equipment enrich
ed_equipment.json \
                --slurpfile consumables cons
umables.json \
                --arg talentsString "${raceC
onfig.config.talentsString}" \
                --slurpfile talents talents.
json \
                --slurpfile glyphs glyphs.js
on \
                --arg race "${raceConfig.con
fig.race}" \
                --arg class "${raceConfig.co
nfig.class}" \
                --arg profession1 "${raceCon
fig.config.profession1}" \
                --arg profession2 "${raceCon
fig.config.profession2}" \
                '{
                  race: $raceName,
                  dps: ($avgDps | tonumber),
                  max: ($maxDps | tonumber),
                  min: ($minDps | tonumber),
                  stdev: ($stdevDps | tonumb
er),
                  loadout: {
                    consumables: $consumable
s[0],
                    talentsString: $talentsS
tring,
                    talents: $talents[0],
                    glyphs: $glyphs[0],
                    equipment: $equipment[0]
,
                    race: $race,
                    class: $class,
                    profession1: $profession
1,
                    profession2: $profession
2,
                    simLink: $simLink
                  }
                }' > $out
              else
                echo "Simulation failed for 
${raceConfig.className}/${raceConfig.specNam
e}/${raceConfig.raceName}"
                exit 1
              fi
          '';
      })
      raceConfigs);

    structuredOutput = "${class}_${spec}_rac
e_${phase}_${encounterType}_${targetCount}_$
{duration}";

    aggregationScript = pkgs.writeShellAppli
cation {
      name = "${structuredOutput}-aggregator
";
      text = ''
        ${shellUtils.parseArgsAndEnv}

        echo "Aggregating race comparison re
sults for: ${class}/${spec}"
        echo "Races simulated: ${toString (l
ib.length raceConfigs)}"

        result=$(jq -n '{}')

        ${lib.concatMapStringsSep "\n" (race
Config: ''
            # ${raceConfig.raceName} results
            raceData=$(cat ${simDerivations.
"${raceConfig.className}-${raceConfig.specNa
me}-${raceConfig.raceName}"})

            result=$(echo "$result" | jq \
              --argjson race "$raceData" \
              --arg raceName "${raceConfig.r
aceName}" \
              '.[$raceName] = {
                dps: $race.dps,
                max: $race.max,
                min: $race.min,
                stdev: $race.stdev,
                loadout: $race.loadout
              }')
          '')
          raceConfigs}

        finalResult=$(echo "$result" | jq \
          --arg class "${class}" \
          --arg spec "${spec}" \
          --arg encounter "${phase}_${encoun
terType}_${targetCount}_${duration}" \
          --arg timestamp "$(date -Iseconds)
" \
          --arg iterations "${toString itera
tions}" \
          --arg encounterDuration "${toStrin
g encounter.duration}" \
          --arg encounterVariation "${toStri
ng encounter.durationVariation}" \
          --arg targetCount "${toString (lib
.length encounter.targets)}" \
          --arg wowsimsCommit "${wowsimsComm
it}" \
          --argjson raidBuffs '${builtins.to
JSON buffs.full}' \
          '{
            metadata: {
              spec: $spec,
              class: $class,
              encounter: $encounter,
              timestamp: $timestamp,
              iterations: ($iterations | ton
umber),
              encounterDuration: ($encounter
Duration | tonumber),
              encounterVariation: ($encounte
rVariation | tonumber),
              targetCount: ($targetCount | t
onumber),
              wowsimsCommit: $wowsimsCommit,
              raidBuffs: $raidBuffs
            },
            results: .
          }')

        ${shellUtils.conditionalOutput {
          inherit structuredOutput;
          webSetupCode = shellUtils.setupCom
parisonDirs {
            comparisonType = "race";
            class = "${class}";
            spec = "${spec}";
          };
          webPath = "$comparison_dir";
          webMessage = "Copied to";
        }}

        echo ""
        echo "Race DPS Rankings for ${class}
/${spec}:"
        echo "==============================
========="
        echo "$finalResult" | jq -r '
          .results | to_entries[] |
          select(.value.dps != null and .val
ue.dps > 0) |
          "\(.key): \(.value.dps | floor) DP
S"
        ' | sort -k2 -nr

        # Show any failed races
        failed_races=$(echo "$finalResult" |
 jq -r '
          .results | to_entries[] |
          select(.value.dps == null or .valu
e.dps <= 0) |
          .key
        ')

        if [[ -n "$failed_races" ]]; then
          echo ""
          echo "Failed races (no valid DPS d
ata):"
          echo "$failed_races"
        fi

      '';
      runtimeInputs = [pkgs.jq pkgs.coreutil
s];
    };
  in {
    simulations = simDerivations;

    script = aggregationScript;

    metadata = {
      output = structuredOutput;
      inherit class spec iterations phase en
counterType targetCount duration;
      raceCount = lib.length raceConfigs;
      races = map (r: r.raceName) raceConfig
s;
    };
  };

  mkMassSim = {
    specs ? "dps",
    encounter,
    iterations ? 10000,
    phase ? "p1",
    encounterType ? "raid",
    targetCount ? "single",
    duration ? "long",
    template ? "singleTarget",
    wowsimsCommit ? inputs.wowsims-upstream.
shortRev,
  }: let
    # get the list of specs based on the spe
cs parameter
    specConfigs =
      if specs == "dps"
      then getAllDPSSpecs classes template p
hase
      else if builtins.isList specs
      then specs
      else throw "specs must be 'dps' or a l
ist of spec configurations";

    # create individual simulation derivatio
ns for each spec
    simDerivations = lib.listToAttrs (map (s
pec: {
        name = "${spec.className}-${spec.spe
cName}";
        value = let
          # Pre-enrich the equipment, consum
ables, glyphs, and talents for this spec
          enrichedEquipment = itemDatabase.e
nrichEquipment spec.config.equipment;
          enrichedConsumables = itemDatabase
.enrichConsumables spec.config.consumables;
          enrichedGlyphs = itemDatabase.enri
chGlyphs spec.config.class spec.config.glyph
s;
          enrichedTalents = itemDatabase.enr
ichTalents spec.config.class spec.config.tal
entsString;

          # Create class-specific buff adjus
tments
          classSpecificBuffs =
            if spec.config.class == "ClassWa
rrior"
            then buffs.full // {skullBannerC
ount = 1;}
            else if spec.config.class == "Cl
assShaman"
            then buffs.full // {stormlashTot
emCount = 3;}
            else buffs.full;

          simInput = mkSim {
            inherit iterations;
            player = spec.config;
            buffs = classSpecificBuffs;
            debuffs = debuffs.full;
            inherit encounter;
          };
        in
          pkgs.runCommand "sim-${spec.classN
ame}-${spec.specName}" {
            buildInputs = [pkgs.jq];
            nativeBuildInputs = [inputs.wows
ims.packages.${pkgs.system}.wowsimcli];
          } ''
            cat > enriched_equipment.json <<
 'EQUIPMENT_EOF'
              ${builtins.toJSON enrichedEqui
pment}
            EQUIPMENT_EOF

            cat > consumables.json << 'CONSU
MABLES_EOF'
              ${builtins.toJSON enrichedCons
umables}
            CONSUMABLES_EOF

            cat > glyphs.json << 'GLYPHS_EOF
'
              ${builtins.toJSON enrichedGlyp
hs}
            GLYPHS_EOF

            cat > talents.json << 'TALENTS_E
OF'
              ${builtins.toJSON enrichedTale
nts}
            TALENTS_EOF

            cat > input.json << 'INPUT_EOF'
              ${simInput}
            INPUT_EOF

            echo "Running ${spec.className}/
${spec.specName} simulation..."
            if wowsimcli sim --infile input.
json --outfile output.json; then

              avgDps=$(jq -r '.raidMetrics.d
ps.avg // 0' output.json)
              maxDps=$(jq -r '.raidMetrics.d
ps.max // 0' output.json)
              minDps=$(jq -r '.raidMetrics.d
ps.min // 0' output.json)
              stdevDps=$(jq -r '.raidMetrics
.dps.stdev // 0' output.json)

              # Generate wowsim link from in
put file
              echo "Generating wowsim link..
."
              simLink=$(wowsimcli encodelink
 input.json || echo "")

              # create final result with all
 DPS statistics and enriched data
              jq -n \
                --arg className "${spec.clas
sName}" \
                --arg specName "${spec.specN
ame}" \
                --arg avgDps "$avgDps" \
                --arg maxDps "$maxDps" \
                --arg minDps "$minDps" \
                --arg stdevDps "$stdevDps" \
                --arg simLink "$simLink" \
                --slurpfile equipment enrich
ed_equipment.json \
                --slurpfile consumables cons
umables.json \
                --arg talentsString "${spec.
config.talentsString}" \
                --slurpfile talents talents.
json \
                --slurpfile glyphs glyphs.js
on \
                --arg race "${spec.config.ra
ce}" \
                --arg class "${spec.config.c
lass}" \
                --arg profession1 "${spec.co
nfig.profession1}" \
                --arg profession2 "${spec.co
nfig.profession2}" \
                '{
                  className: $className,
                  specName: $specName,
                  dps: ($avgDps | tonumber),
                  max: ($maxDps | tonumber),
                  min: ($minDps | tonumber),
                  stdev: ($stdevDps | tonumb
er),
                  loadout: {
                    consumables: $consumable
s[0],
                    talentsString: $talentsS
tring,
                    talents: $talents[0],
                    glyphs: $glyphs[0],
                    equipment: $equipment[0]
,
                    race: $race,
                    class: $class,
                    profession1: $profession
1,
                    profession2: $profession
2,
                    simLink: $simLink
                  }
                }' > $out
            else
              echo "Simulation failed for ${
spec.className}/${spec.specName}"
              exit 1
            fi
          '';
      })
      specConfigs);

    # generate structured output filename: <
type>_<phase>_<encounter-type>_<target-count
>_<duration>
    structuredOutput = "${specs}_${phase}_${
encounterType}_${targetCount}_${duration}";

    # aggregation script that combines all r
esults
    aggregationScript = pkgs.writeShellAppli
cation {
      name = "${structuredOutput}-aggregator
";
      text = ''
        set -euo pipefail

        ${shellUtils.parseArgsAndEnv}

        echo "Aggregating mass simulation re
sults for: ${structuredOutput}"
        echo "Specs simulated: ${toString (l
ib.length specConfigs)}"

        # create base structure
        result=$(jq -n '{}')

        ${lib.concatMapStringsSep "\n" (spec
: ''
            # Add ${spec.className}/${spec.s
pecName} results
            specData=$(cat ${simDerivations.
"${spec.className}-${spec.specName}"})

            result=$(echo "$result" | jq \
              --argjson spec "$specData" \
              --arg className "${spec.classN
ame}" \
              --arg specName "${spec.specNam
e}" \
              '.[$className][$specName] = {
                dps: $spec.dps,
                max: $spec.max,
                min: $spec.min,
                stdev: $spec.stdev,
                loadout: $spec.loadout
              }')
          '')
          specConfigs}

        # Create final output with metadata 
including encounter information
        finalResult=$(echo "$result" | jq \
          --arg output "${structuredOutput}"
 \
          --arg timestamp "$(date -Iseconds)
" \
          --arg iterations "${toString itera
tions}" \
          --arg specCount "${toString (lib.l
ength specConfigs)}" \
          --arg encounterDuration "${toStrin
g encounter.duration}" \
          --arg encounterVariation "${toStri
ng encounter.durationVariation}" \
          --arg targetCount "${toString (lib
.length encounter.targets)}" \
          --arg wowsimsCommit "${wowsimsComm
it}" \
          --argjson raidBuffs '${builtins.to
JSON buffs.full}' \
          '{
            metadata: {
              name: $output,
              timestamp: $timestamp,
              iterations: ($iterations | ton
umber),
              specCount: ($specCount | tonum
ber),
              encounterDuration: ($encounter
Duration | tonumber),
              encounterVariation: ($encounte
rVariation | tonumber),
              targetCount: ($targetCount | t
onumber),
              wowsimsCommit: $wowsimsCommit,
              raidBuffs: $raidBuffs
            },
            results: .
          }')

        ${shellUtils.conditionalOutput {
          inherit structuredOutput;
          webSetupCode = shellUtils.setupRan
kingsDirs;
          webPath = "$rankings_dir";
          webMessage = "Copied to";
        }}

        echo ""
        echo "Top DPS Rankings:"
        echo "=================="
        echo "$finalResult" | jq -r '
          .results | to_entries[] as $class 
|
          $class.key as $className |
          $class.value | to_entries[] as $sp
ec |
          "\($className)/\($spec.key): \($sp
ec.value.dps | floor) DPS"
        ' | sort -k2 -nr | head -10

      '';
      runtimeInputs = [pkgs.jq pkgs.coreutil
s];
    };
  in {
    # individual simulation derivations (for
 debugging)
    simulations = simDerivations;

    # main aggregation script
    script = aggregationScript;

    metadata = {
      output = structuredOutput;
      inherit iterations phase encounterType
 targetCount duration;
      specCount = length specConfigs;
      specs = map (s: "${s.className}/${s.sp
ecName}") specConfigs;
    };
  };
in {
  inherit mkMassSim getAllDPSSpecs mkRaceCom
parison getRaceConfigs getPlayableRaces;
}
 
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET