┌─ NIX ──────────────────────────────────────────────────────────────────────┐
│ { │
│ lib, │
│ pkgs, │
│ classes, │
│ buffs, │
│ debuffs, │
│ inputs, │
│ trinket, │
│ ... │
│ }: let │
│ inherit (lib.sim.simulation) mkSim; │
│ inherit (lib.sim) itemDatabase shellUtils; │
│ inherit (lib) hasAttr length listToAttrs imap0 replaceStrings concatMapStringsSe │
│ p; │
│ │
│ # Import our trinket manipulation functions │
│ trinketLib = lib.sim.trinket; │
│ │
│ mkTrinketComparison = { │
│ class, │
│ spec, │
│ encounter, │
│ trinketCategory ? "meleeAgility", # Default to melee agility trinkets │
│ trinketIds ? null, # Optional: provide trinket IDs directly │
│ iterations ? 10000, │
│ phase ? "p1", │
│ encounterType ? "raid", │
│ targetCount ? "single", │
│ duration ? "long", │
│ template ? "singleTarget", │
│ wowsimsCommit ? inputs.wowsims-upstream.shortRev, │
│ }: let │
│ # Get the base spec configuration │
│ baseSpec = classes.${class}.${spec}; │
│ defaultRace = baseSpec.defaultRace; │
│ │
│ # Get the base player configuration │
│ baseConfig = │
│ if │
│ hasAttr "template" baseSpec │
│ && hasAttr defaultRace baseSpec.template │
│ && hasAttr phase baseSpec.template.${defaultRace} │
│ && hasAttr encounterType baseSpec.template.${defaultRace}.${phase} │
│ && hasAttr template baseSpec.template.${defaultRace}.${phase}.${encounterT │
│ ype} │
│ then baseSpec.template.${defaultRace}.${phase}.${encounterType}.${template} │
│ else throw "Template ${template} not found for ${class}/${spec}/${defaultRac │
│ e} at ${phase}.${encounterType}"; │
│ │
│ # Load trinket list - either from direct input or from trinket presets │
│ actualTrinketIds = │
│ if trinketIds != null │
│ then trinketIds │
│ else trinket.presets.p1.${trinketCategory}; │
│ │
│ # Create baseline gearset (no trinkets) │
│ baselineGearset = trinketLib.removeTrinkets baseConfig.equipment; │
│ │
│ # Create baseline player config │
│ baselineConfig = baseConfig // {equipment = baselineGearset;}; │
│ │
│ # Trinket profession requirements mapping │
│ trinketProfessionRequirements = { │
│ "75274" = "Alchemy"; # Zen Alchemist Stone │
│ # Add other profession-specific trinkets here as needed │
│ }; │
│ │
│ # Generate trinket gearsets │
│ trinketGearsets = trinketLib.generateTrinketGearsets baselineGearset actualTri │
│ nketIds; │
│ │
│ # Create trinket configs with gearsets and profession adjustments │
│ trinketConfigs = │
│ imap0 (index: gearset: let │
│ trinketId = builtins.elemAt actualTrinketIds index; │
│ trinketIdStr = toString trinketId; │
│ requiredProfession = trinketProfessionRequirements.${trinketIdStr} or null │
│ ; │
│ │
│ # Modify profession if trinket requires it │
│ configWithProfession = │
│ if requiredProfession != null │
│ then let │
│ # Determine which profession is being replaced │
│ droppedProfession = │
│ if baseConfig.profession2 != requiredProfession │
│ then baseConfig.profession2 │
│ else if baseConfig.profession1 != requiredProfession │
│ then baseConfig.profession1 │
│ else null; │
│ │
│ # Remove enchants from the dropped profession │
│ cleanedGearset = │
│ if droppedProfession != null │
│ then trinketLib.removeProfessionEnchants gearset droppedProfession │
│ else gearset; │
│ in │
│ baseConfig │
│ // { │
│ equipment = cleanedGearset; │
│ profession2 = requiredProfession; │
│ } │
│ else baseConfig // {equipment = gearset;}; │
│ in { │
│ trinketId = trinketId; │
│ config = configWithProfession; │
│ }) │
│ trinketGearsets; │
│ │
│ # Class-specific buff adjustments (copied from mkMassSim) │
│ classSpecificBuffs = │
│ if baseConfig.class == "ClassWarrior" │
│ then buffs.full // {skullBannerCount = 1;} │
│ else if baseConfig.class == "ClassShaman" │
│ then buffs.full // {stormlashTotemCount = 3;} │
│ else buffs.full; │
│ │
│ # Create baseline simulation │
│ baselineSim = let │
│ enrichedEquipment = itemDatabase.enrichEquipment baselineConfig.equipment; │
│ enrichedConsumables = itemDatabase.enrichConsumables baselineConfig.consumab │
│ les; │
│ enrichedGlyphs = itemDatabase.enrichGlyphs baselineConfig.class baselineConf │
│ ig.glyphs; │
│ enrichedTalents = itemDatabase.enrichTalents baselineConfig.class baselineCo │
│ nfig.talentsString; │
│ │
│ simInput = mkSim { │
│ inherit iterations; │
│ player = baselineConfig; │
│ buffs = classSpecificBuffs; │
│ debuffs = debuffs.full; │
│ inherit encounter; │
│ }; │
│ in │
│ pkgs.runCommand "trinket-baseline-${class}-${spec}" { │
│ buildInputs = [pkgs.jq]; │
│ nativeBuildInputs = [inputs.wowsims.packages.${pkgs.system}.wowsimcli]; │
│ } '' │
│ # Write enriched JSON data to files │
│ 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 baseline (no trinkets) simulation for ${class}/${spec}..." │
│ 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 │
│ jq -n \ │
│ --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 "${baselineConfig.talentsString}" \ │
│ --slurpfile talents talents.json \ │
│ --slurpfile glyphs glyphs.json \ │
│ --arg race "${baselineConfig.race}" \ │
│ --arg class "${baselineConfig.class}" \ │
│ --arg profession1 "${baselineConfig.profession1}" \ │
│ --arg profession2 "${baselineConfig.profession2}" \ │
│ '{ │
│ 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 "Baseline simulation failed for ${class}/${spec}" │
│ exit 1 │
│ fi │
│ ''; │
│ │
│ # Create individual trinket simulations │
│ trinketSims = listToAttrs (map (trinketConfig: let │
│ trinketItem = itemDatabase.getItem trinketConfig.trinketId; │
│ trinketName = │
│ if trinketItem != null │
│ then trinketItem.name │
│ else "Unknown_${toString trinketConfig.trinketId}"; │
│ trinketIlvl = │
│ if trinketItem != null && trinketItem ? scalingOptions && trinketItem.sc │
│ alingOptions ? "0" │
│ then toString trinketItem.scalingOptions."0".ilvl │
│ else "0"; │
│ # Clean name for use as attribute key │
│ cleanName = replaceStrings [" " "'" "," "(" ")"] ["_" "" "" "" ""] trinket │
│ Name; │
│ in { │
│ name = "${cleanName}_${trinketIlvl}"; │
│ value = let │
│ enrichedEquipment = itemDatabase.enrichEquipment trinketConfig.config.eq │
│ uipment; │
│ enrichedConsumables = itemDatabase.enrichConsumables trinketConfig.confi │
│ g.consumables; │
│ enrichedGlyphs = itemDatabase.enrichGlyphs trinketConfig.config.class tr │
│ inketConfig.config.glyphs; │
│ enrichedTalents = itemDatabase.enrichTalents trinketConfig.config.class │
│ trinketConfig.config.talentsString; │
│ │
│ simInput = mkSim { │
│ inherit iterations; │
│ player = trinketConfig.config; │
│ buffs = classSpecificBuffs; │
│ debuffs = debuffs.full; │
│ inherit encounter; │
│ }; │
│ in │
│ pkgs.runCommand "trinket-sim-${class}-${spec}-${toString trinketConfig.t │
│ rinketId}" { │
│ buildInputs = [pkgs.jq]; │
│ nativeBuildInputs = [inputs.wowsims.packages.${pkgs.system}.wowsimcli] │
│ ; │
│ } '' │
│ # Write enriched JSON data to files │
│ 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 trinket simulation for ${class}/${spec} with trinket ${t │
│ oString trinketConfig.trinketId}..." │
│ 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 "") │
│ │
│ # Get trinket metadata │
│ trinketName="${trinketName}" │
│ trinketId="${toString trinketConfig.trinketId}" │
│ │
│ # Create final result with trinket metadata │
│ jq -n \ │
│ --arg avgDps "$avgDps" \ │
│ --arg maxDps "$maxDps" \ │
│ --arg minDps "$minDps" \ │
│ --arg stdevDps "$stdevDps" \ │
│ --arg simLink "$simLink" \ │
│ --arg trinketName "$trinketName" \ │
│ --arg trinketId "$trinketId" \ │
│ --slurpfile equipment enriched_equipment.json \ │
│ --slurpfile consumables consumables.json \ │
│ --arg talentsString "${trinketConfig.config.talentsString}" \ │
│ --slurpfile talents talents.json \ │
│ --slurpfile glyphs glyphs.json \ │
│ --arg race "${trinketConfig.config.race}" \ │
│ --arg class "${trinketConfig.config.class}" \ │
│ --arg profession1 "${trinketConfig.config.profession1}" \ │
│ --arg profession2 "${trinketConfig.config.profession2}" \ │
│ '{ │
│ dps: ($avgDps | tonumber), │
│ max: ($maxDps | tonumber), │
│ min: ($minDps | tonumber), │
│ stdev: ($stdevDps | tonumber), │
│ trinket: { │
│ id: ($trinketId | tonumber), │
│ name: $trinketName │
│ }, │
│ 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 "Trinket simulation failed for ${class}/${spec} with trinket ${ │
│ toString trinketConfig.trinketId}" │
│ exit 1 │
│ fi │
│ ''; │
│ }) │
│ trinketConfigs); │
│ │
│ # Generate structured output filename │
│ structuredOutput = "${class}_${spec}_trinket_${phase}_${encounterType}_${targe │
│ tCount}_${duration}"; │
│ │
│ # Aggregation script that combines baseline + all trinket results │
│ aggregationScript = pkgs.writeShellApplication { │
│ name = "${structuredOutput}-aggregator"; │
│ text = '' │
│ ${shellUtils.parseArgsAndEnv} │
│ │
│ echo "Aggregating trinket comparison results for: ${class}/${spec}" │
│ echo "Trinkets tested: ${toString (builtins.length actualTrinketIds)}" │
│ │
│ # Start with baseline result │
│ result=$(jq -n '{}') │
│ │
│ # Add baseline │
│ baselineData=$(cat ${baselineSim}) │
│ result=$(echo "$result" | jq \ │
│ --argjson baseline "$baselineData" \ │
│ '.baseline = $baseline') │
│ │
│ # Add each trinket result │
│ ${concatMapStringsSep "\n" (trinketConfig: let │
│ trinketItem = itemDatabase.getItem trinketConfig.trinketId; │
│ trinketName = │
│ if trinketItem != null │
│ then trinketItem.name │
│ else "Unknown_${toString trinketConfig.trinketId}"; │
│ trinketIlvl = │
│ if trinketItem != null && trinketItem ? scalingOptions && trinketIte │
│ m.scalingOptions ? "0" │
│ then toString trinketItem.scalingOptions."0".ilvl │
│ else "0"; │
│ cleanName = replaceStrings [" " "'" "," "(" ")"] ["_" "" "" "" ""] tri │
│ nketName; │
│ keyName = "${cleanName}_${trinketIlvl}"; │
│ in '' │
│ trinketData=$(cat ${trinketSims.${keyName}}) │
│ result=$(echo "$result" | jq \ │
│ --argjson trinket "$trinketData" \ │
│ --arg keyName "${keyName}" \ │
│ '.[$keyName] = $trinket') │
│ '') │
│ trinketConfigs} │
│ │
│ # Create final output with metadata │
│ finalResult=$(echo "$result" | jq \ │
│ --arg class "${class}" \ │
│ --arg spec "${spec}" \ │
│ --arg comparison "trinket" \ │
│ --arg baseline "no_trinkets" \ │
│ --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 (length encounter.targets)}" \ │
│ --arg wowsimsCommit "${wowsimsCommit}" \ │
│ --arg trinketCategory "${trinketCategory}" \ │
│ --argjson raidBuffs '${builtins.toJSON classSpecificBuffs}' \ │
│ '{ │
│ metadata: { │
│ spec: $spec, │
│ class: $class, │
│ comparison: $comparison, │
│ baseline: $baseline, │
│ encounter: $encounter, │
│ timestamp: $timestamp, │
│ iterations: ($iterations | tonumber), │
│ encounterDuration: ($encounterDuration | tonumber), │
│ encounterVariation: ($encounterVariation | tonumber), │
│ targetCount: ($targetCount | tonumber), │
│ wowsimsCommit: $wowsimsCommit, │
│ trinketCategory: $trinketCategory, │
│ raidBuffs: $raidBuffs │
│ }, │
│ results: . │
│ }') │
│ │
│ ${shellUtils.conditionalOutput { │
│ inherit structuredOutput; │
│ webSetupCode = shellUtils.setupComparisonDirs { │
│ comparisonType = "trinkets"; │
│ class = "${class}"; │
│ spec = "${spec}"; │
│ }; │
│ webPath = "$comparison_dir"; │
│ webMessage = "Copied to"; │
│ }} │
│ │
│ echo "" │
│ echo "Trinket DPS Rankings for ${class}/${spec}:" │
│ echo "=======================================" │
│ │
│ # Show baseline DPS │
│ echo "Baseline (no trinkets): $(echo "$finalResult" | jq -r '.results.base │
│ line.dps | floor') DPS" │
│ echo "" │
│ │
│ # Show trinket rankings sorted by DPS │
│ echo "$finalResult" | jq -r ' │
│ .results | to_entries[] | │
│ select(.key != "baseline" and .value.dps != null and .value.dps > 0) | │
│ "\(.value.trinket.name): \(.value.dps | floor) DPS" │
│ ' | sort -k2 -nr │
│ │
│ ''; │
│ runtimeInputs = [pkgs.jq pkgs.coreutils]; │
│ }; │
│ in { │
│ # Individual simulation derivations │
│ simulations = trinketSims // {baseline = baselineSim;}; │
│ │
│ # Main aggregation script │
│ script = aggregationScript; │
│ │
│ # Metadata │
│ metadata = { │
│ inherit class spec trinketCategory; │
│ output = structuredOutput; │
│ inherit iterations phase encounterType targetCount duration; │
│ trinketCount = length actualTrinketIds; │
│ trinkets = actualTrinketIds; │
│ }; │
│ }; │
│ in { │
│ inherit mkTrinketComparison; │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ NIX ────────────────────────────────┐
│ { │
│ lib, │
│ pkgs, │
│ classes, │
│ buffs, │
│ debuffs, │
│ inputs, │
│ trinket, │
│ ... │
│ }: let │
│ inherit (lib.sim.simulation) mkSim; │
│ inherit (lib.sim) itemDatabase shellUtils; │
│ inherit (lib) hasAttr length listToAttrs i │
│ map0 replaceStrings concatMapStringsSep; │
│ │
│ # Import our trinket manipulation function │
│ s │
│ trinketLib = lib.sim.trinket; │
│ │
│ mkTrinketComparison = { │
│ class, │
│ spec, │
│ encounter, │
│ trinketCategory ? "meleeAgility", # Defa │
│ ult to melee agility trinkets │
│ trinketIds ? null, # Optional: provide t │
│ rinket IDs directly │
│ iterations ? 10000, │
│ phase ? "p1", │
│ encounterType ? "raid", │
│ targetCount ? "single", │
│ duration ? "long", │
│ template ? "singleTarget", │
│ wowsimsCommit ? inputs.wowsims-upstream. │
│ shortRev, │
│ }: let │
│ # Get the base spec configuration │
│ baseSpec = classes.${class}.${spec}; │
│ defaultRace = baseSpec.defaultRace; │
│ │
│ # Get the base player configuration │
│ baseConfig = │
│ if │
│ hasAttr "template" baseSpec │
│ && hasAttr defaultRace baseSpec.temp │
│ late │
│ && hasAttr phase baseSpec.template.$ │
│ {defaultRace} │
│ && hasAttr encounterType baseSpec.te │
│ mplate.${defaultRace}.${phase} │
│ && hasAttr template baseSpec.templat │
│ e.${defaultRace}.${phase}.${encounterType} │
│ then baseSpec.template.${defaultRace}. │
│ ${phase}.${encounterType}.${template} │
│ else throw "Template ${template} not f │
│ ound for ${class}/${spec}/${defaultRace} at │
│ ${phase}.${encounterType}"; │
│ │
│ # Load trinket list - either from direct │
│ input or from trinket presets │
│ actualTrinketIds = │
│ if trinketIds != null │
│ then trinketIds │
│ else trinket.presets.p1.${trinketCateg │
│ ory}; │
│ │
│ # Create baseline gearset (no trinkets) │
│ baselineGearset = trinketLib.removeTrink │
│ ets baseConfig.equipment; │
│ │
│ # Create baseline player config │
│ baselineConfig = baseConfig // {equipmen │
│ t = baselineGearset;}; │
│ │
│ # Trinket profession requirements mappin │
│ g │
│ trinketProfessionRequirements = { │
│ "75274" = "Alchemy"; # Zen Alchemist S │
│ tone │
│ # Add other profession-specific trinke │
│ ts here as needed │
│ }; │
│ │
│ # Generate trinket gearsets │
│ trinketGearsets = trinketLib.generateTri │
│ nketGearsets baselineGearset actualTrinketId │
│ s; │
│ │
│ # Create trinket configs with gearsets a │
│ nd profession adjustments │
│ trinketConfigs = │
│ imap0 (index: gearset: let │
│ trinketId = builtins.elemAt actualTr │
│ inketIds index; │
│ trinketIdStr = toString trinketId; │
│ requiredProfession = trinketProfessi │
│ onRequirements.${trinketIdStr} or null; │
│ │
│ # Modify profession if trinket requi │
│ res it │
│ configWithProfession = │
│ if requiredProfession != null │
│ then let │
│ # Determine which profession is │
│ being replaced │
│ droppedProfession = │
│ if baseConfig.profession2 != r │
│ equiredProfession │
│ then baseConfig.profession2 │
│ else if baseConfig.profession1 │
│ != requiredProfession │
│ then baseConfig.profession1 │
│ else null; │
│ │
│ # Remove enchants from the dropp │
│ ed profession │
│ cleanedGearset = │
│ if droppedProfession != null │
│ then trinketLib.removeProfessi │
│ onEnchants gearset droppedProfession │
│ else gearset; │
│ in │
│ baseConfig │
│ // { │
│ equipment = cleanedGearset; │
│ profession2 = requiredProfessi │
│ on; │
│ } │
│ else baseConfig // {equipment = ge │
│ arset;}; │
│ in { │
│ trinketId = trinketId; │
│ config = configWithProfession; │
│ }) │
│ trinketGearsets; │
│ │
│ # Class-specific buff adjustments (copie │
│ d from mkMassSim) │
│ classSpecificBuffs = │
│ if baseConfig.class == "ClassWarrior" │
│ then buffs.full // {skullBannerCount = │
│ 1;} │
│ else if baseConfig.class == "ClassSham │
│ an" │
│ then buffs.full // {stormlashTotemCoun │
│ t = 3;} │
│ else buffs.full; │
│ │
│ # Create baseline simulation │
│ baselineSim = let │
│ enrichedEquipment = itemDatabase.enric │
│ hEquipment baselineConfig.equipment; │
│ enrichedConsumables = itemDatabase.enr │
│ ichConsumables baselineConfig.consumables; │
│ enrichedGlyphs = itemDatabase.enrichGl │
│ yphs baselineConfig.class baselineConfig.gly │
│ phs; │
│ enrichedTalents = itemDatabase.enrichT │
│ alents baselineConfig.class baselineConfig.t │
│ alentsString; │
│ │
│ simInput = mkSim { │
│ inherit iterations; │
│ player = baselineConfig; │
│ buffs = classSpecificBuffs; │
│ debuffs = debuffs.full; │
│ inherit encounter; │
│ }; │
│ in │
│ pkgs.runCommand "trinket-baseline-${cl │
│ ass}-${spec}" { │
│ buildInputs = [pkgs.jq]; │
│ nativeBuildInputs = [inputs.wowsims. │
│ packages.${pkgs.system}.wowsimcli]; │
│ } '' │
│ # Write enriched JSON data to files │
│ cat > enriched_equipment.json << 'EQ │
│ UIPMENT_EOF' │
│ ${builtins.toJSON enrichedEquipment} │
│ EQUIPMENT_EOF │
│ │
│ cat > consumables.json << 'CONSUMABL │
│ ES_EOF' │
│ ${builtins.toJSON enrichedConsumable │
│ s} │
│ 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 baseline (no trinkets) │
│ simulation for ${class}/${spec}..." │
│ if wowsimcli sim --infile input.json │
│ --outfile output.json; then │
│ # Extract DPS statistics │
│ avgDps=$(jq -r '.raidMetrics.dps.a │
│ vg // 0' output.json) │
│ maxDps=$(jq -r '.raidMetrics.dps.m │
│ ax // 0' output.json) │
│ minDps=$(jq -r '.raidMetrics.dps.m │
│ in // 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 inp │
│ ut.json || echo "") │
│ │
│ # Create final result │
│ jq -n \ │
│ --arg avgDps "$avgDps" \ │
│ --arg maxDps "$maxDps" \ │
│ --arg minDps "$minDps" \ │
│ --arg stdevDps "$stdevDps" \ │
│ --arg simLink "$simLink" \ │
│ --slurpfile equipment enriched_e │
│ quipment.json \ │
│ --slurpfile consumables consumab │
│ les.json \ │
│ --arg talentsString "${baselineC │
│ onfig.talentsString}" \ │
│ --slurpfile talents talents.json │
│ \ │
│ --slurpfile glyphs glyphs.json \ │
│ --arg race "${baselineConfig.rac │
│ e}" \ │
│ --arg class "${baselineConfig.cl │
│ ass}" \ │
│ --arg profession1 "${baselineCon │
│ fig.profession1}" \ │
│ --arg profession2 "${baselineCon │
│ fig.profession2}" \ │
│ '{ │
│ dps: ($avgDps | tonumber), │
│ max: ($maxDps | tonumber), │
│ min: ($minDps | tonumber), │
│ stdev: ($stdevDps | tonumber), │
│ loadout: { │
│ consumables: $consumables[0] │
│ , │
│ talentsString: $talentsStrin │
│ g, │
│ talents: $talents[0], │
│ glyphs: $glyphs[0], │
│ equipment: $equipment[0], │
│ race: $race, │
│ class: $class, │
│ profession1: $profession1, │
│ profession2: $profession2, │
│ simLink: $simLink │
│ } │
│ }' > $out │
│ else │
│ echo "Baseline simulation failed f │
│ or ${class}/${spec}" │
│ exit 1 │
│ fi │
│ ''; │
│ │
│ # Create individual trinket simulations │
│ trinketSims = listToAttrs (map (trinketC │
│ onfig: let │
│ trinketItem = itemDatabase.getItem t │
│ rinketConfig.trinketId; │
│ trinketName = │
│ if trinketItem != null │
│ then trinketItem.name │
│ else "Unknown_${toString trinketCo │
│ nfig.trinketId}"; │
│ trinketIlvl = │
│ if trinketItem != null && trinketI │
│ tem ? scalingOptions && trinketItem.scalingO │
│ ptions ? "0" │
│ then toString trinketItem.scalingO │
│ ptions."0".ilvl │
│ else "0"; │
│ # Clean name for use as attribute ke │
│ y │
│ cleanName = replaceStrings [" " "'" │
│ "," "(" ")"] ["_" "" "" "" ""] trinketName; │
│ in { │
│ name = "${cleanName}_${trinketIlvl}" │
│ ; │
│ value = let │
│ enrichedEquipment = itemDatabase.e │
│ nrichEquipment trinketConfig.config.equipmen │
│ t; │
│ enrichedConsumables = itemDatabase │
│ .enrichConsumables trinketConfig.config.cons │
│ umables; │
│ enrichedGlyphs = itemDatabase.enri │
│ chGlyphs trinketConfig.config.class trinketC │
│ onfig.config.glyphs; │
│ enrichedTalents = itemDatabase.enr │
│ ichTalents trinketConfig.config.class trinke │
│ tConfig.config.talentsString; │
│ │
│ simInput = mkSim { │
│ inherit iterations; │
│ player = trinketConfig.config; │
│ buffs = classSpecificBuffs; │
│ debuffs = debuffs.full; │
│ inherit encounter; │
│ }; │
│ in │
│ pkgs.runCommand "trinket-sim-${cla │
│ ss}-${spec}-${toString trinketConfig.trinket │
│ Id}" { │
│ buildInputs = [pkgs.jq]; │
│ nativeBuildInputs = [inputs.wows │
│ ims.packages.${pkgs.system}.wowsimcli]; │
│ } '' │
│ # Write enriched JSON data to fi │
│ les │
│ cat > enriched_equipment.json << │
│ 'EQUIPMENT_EOF' │
│ ${builtins.toJSON enrichedEquipm │
│ ent} │
│ EQUIPMENT_EOF │
│ │
│ cat > consumables.json << 'CONSU │
│ MABLES_EOF' │
│ ${builtins.toJSON enrichedConsum │
│ ables} │
│ CONSUMABLES_EOF │
│ │
│ cat > glyphs.json << 'GLYPHS_EOF │
│ ' │
│ ${builtins.toJSON enrichedGlyphs │
│ } │
│ GLYPHS_EOF │
│ │
│ cat > talents.json << 'TALENTS_E │
│ OF' │
│ ${builtins.toJSON enrichedTalent │
│ s} │
│ TALENTS_EOF │
│ │
│ cat > input.json << 'INPUT_EOF' │
│ ${simInput} │
│ INPUT_EOF │
│ │
│ echo "Running trinket simulation │
│ for ${class}/${spec} with trinket ${toStrin │
│ g trinketConfig.trinketId}..." │
│ 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 "") │
│ │
│ # Get trinket metadata │
│ trinketName="${trinketName}" │
│ trinketId="${toString trinketC │
│ onfig.trinketId}" │
│ │
│ # Create final result with tri │
│ nket metadata │
│ jq -n \ │
│ --arg avgDps "$avgDps" \ │
│ --arg maxDps "$maxDps" \ │
│ --arg minDps "$minDps" \ │
│ --arg stdevDps "$stdevDps" \ │
│ --arg simLink "$simLink" \ │
│ --arg trinketName "$trinketN │
│ ame" \ │
│ --arg trinketId "$trinketId" │
│ \ │
│ --slurpfile equipment enrich │
│ ed_equipment.json \ │
│ --slurpfile consumables cons │
│ umables.json \ │
│ --arg talentsString "${trink │
│ etConfig.config.talentsString}" \ │
│ --slurpfile talents talents. │
│ json \ │
│ --slurpfile glyphs glyphs.js │
│ on \ │
│ --arg race "${trinketConfig. │
│ config.race}" \ │
│ --arg class "${trinketConfig │
│ .config.class}" \ │
│ --arg profession1 "${trinket │
│ Config.config.profession1}" \ │
│ --arg profession2 "${trinket │
│ Config.config.profession2}" \ │
│ '{ │
│ dps: ($avgDps | tonumber), │
│ max: ($maxDps | tonumber), │
│ min: ($minDps | tonumber), │
│ stdev: ($stdevDps | tonumb │
│ er), │
│ trinket: { │
│ id: ($trinketId | tonumb │
│ er), │
│ name: $trinketName │
│ }, │
│ 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 "Trinket simulation faile │
│ d for ${class}/${spec} with trinket ${toStri │
│ ng trinketConfig.trinketId}" │
│ exit 1 │
│ fi │
│ ''; │
│ }) │
│ trinketConfigs); │
│ │
│ # Generate structured output filename │
│ structuredOutput = "${class}_${spec}_tri │
│ nket_${phase}_${encounterType}_${targetCount │
│ }_${duration}"; │
│ │
│ # Aggregation script that combines basel │
│ ine + all trinket results │
│ aggregationScript = pkgs.writeShellAppli │
│ cation { │
│ name = "${structuredOutput}-aggregator │
│ "; │
│ text = '' │
│ ${shellUtils.parseArgsAndEnv} │
│ │
│ echo "Aggregating trinket comparison │
│ results for: ${class}/${spec}" │
│ echo "Trinkets tested: ${toString (b │
│ uiltins.length actualTrinketIds)}" │
│ │
│ # Start with baseline result │
│ result=$(jq -n '{}') │
│ │
│ # Add baseline │
│ baselineData=$(cat ${baselineSim}) │
│ result=$(echo "$result" | jq \ │
│ --argjson baseline "$baselineData" │
│ \ │
│ '.baseline = $baseline') │
│ │
│ # Add each trinket result │
│ ${concatMapStringsSep "\n" (trinketC │
│ onfig: let │
│ trinketItem = itemDatabase.getIt │
│ em trinketConfig.trinketId; │
│ trinketName = │
│ if trinketItem != null │
│ then trinketItem.name │
│ else "Unknown_${toString trink │
│ etConfig.trinketId}"; │
│ trinketIlvl = │
│ if trinketItem != null && trin │
│ ketItem ? scalingOptions && trinketItem.scal │
│ ingOptions ? "0" │
│ then toString trinketItem.scal │
│ ingOptions."0".ilvl │
│ else "0"; │
│ cleanName = replaceStrings [" " │
│ "'" "," "(" ")"] ["_" "" "" "" ""] trinketNa │
│ me; │
│ keyName = "${cleanName}_${trinke │
│ tIlvl}"; │
│ in '' │
│ trinketData=$(cat ${trinketSims. │
│ ${keyName}}) │
│ result=$(echo "$result" | jq \ │
│ --argjson trinket "$trinketDat │
│ a" \ │
│ --arg keyName "${keyName}" \ │
│ '.[$keyName] = $trinket') │
│ '') │
│ trinketConfigs} │
│ │
│ # Create final output with metadata │
│ finalResult=$(echo "$result" | jq \ │
│ --arg class "${class}" \ │
│ --arg spec "${spec}" \ │
│ --arg comparison "trinket" \ │
│ --arg baseline "no_trinkets" \ │
│ --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 (len │
│ gth encounter.targets)}" \ │
│ --arg wowsimsCommit "${wowsimsComm │
│ it}" \ │
│ --arg trinketCategory "${trinketCa │
│ tegory}" \ │
│ --argjson raidBuffs '${builtins.to │
│ JSON classSpecificBuffs}' \ │
│ '{ │
│ metadata: { │
│ spec: $spec, │
│ class: $class, │
│ comparison: $comparison, │
│ baseline: $baseline, │
│ encounter: $encounter, │
│ timestamp: $timestamp, │
│ iterations: ($iterations | ton │
│ umber), │
│ encounterDuration: ($encounter │
│ Duration | tonumber), │
│ encounterVariation: ($encounte │
│ rVariation | tonumber), │
│ targetCount: ($targetCount | t │
│ onumber), │
│ wowsimsCommit: $wowsimsCommit, │
│ trinketCategory: $trinketCateg │
│ ory, │
│ raidBuffs: $raidBuffs │
│ }, │
│ results: . │
│ }') │
│ │
│ ${shellUtils.conditionalOutput { │
│ inherit structuredOutput; │
│ webSetupCode = shellUtils.setupCom │
│ parisonDirs { │
│ comparisonType = "trinkets"; │
│ class = "${class}"; │
│ spec = "${spec}"; │
│ }; │
│ webPath = "$comparison_dir"; │
│ webMessage = "Copied to"; │
│ }} │
│ │
│ echo "" │
│ echo "Trinket DPS Rankings for ${cla │
│ ss}/${spec}:" │
│ echo "============================== │
│ =========" │
│ │
│ # Show baseline DPS │
│ echo "Baseline (no trinkets): $(echo │
│ "$finalResult" | jq -r '.results.baseline.d │
│ ps | floor') DPS" │
│ echo "" │
│ │
│ # Show trinket rankings sorted by DP │
│ S │
│ echo "$finalResult" | jq -r ' │
│ .results | to_entries[] | │
│ select(.key != "baseline" and .val │
│ ue.dps != null and .value.dps > 0) | │
│ "\(.value.trinket.name): \(.value. │
│ dps | floor) DPS" │
│ ' | sort -k2 -nr │
│ │
│ ''; │
│ runtimeInputs = [pkgs.jq pkgs.coreutil │
│ s]; │
│ }; │
│ in { │
│ # Individual simulation derivations │
│ simulations = trinketSims // {baseline = │
│ baselineSim;}; │
│ │
│ # Main aggregation script │
│ script = aggregationScript; │
│ │
│ # Metadata │
│ metadata = { │
│ inherit class spec trinketCategory; │
│ output = structuredOutput; │
│ inherit iterations phase encounterType │
│ targetCount duration; │
│ trinketCount = length actualTrinketIds │
│ ; │
│ trinkets = actualTrinketIds; │
│ }; │
│ }; │
│ in { │
│ inherit mkTrinketComparison; │
│ } │
└──────────────────────────────────────────────┘