┌─ NIX ──────────────────────────────────────────────────────────────────────┐
│ { │
│ 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; │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ NIX ────────────────────────────────┐
│ { │
│ 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; │
│ } │
└──────────────────────────────────────────────┘