NIP parser v2 (#17)
This commit is contained in:
789
pkg/nip/aliases.go
Normal file
789
pkg/nip/aliases.go
Normal file
@@ -0,0 +1,789 @@
|
||||
package nip
|
||||
|
||||
var typeAliases = map[string]int{
|
||||
"shield": 2,
|
||||
"armor": 3,
|
||||
"gold": 4,
|
||||
"bowquiver": 5,
|
||||
"crossbowquiver": 6,
|
||||
"playerbodypart": 7,
|
||||
"herb": 8,
|
||||
"potion": 9,
|
||||
"ring": 10,
|
||||
"elixir": 11,
|
||||
"amulet": 12,
|
||||
"charm": 13,
|
||||
"notused1": 14,
|
||||
"boots": 15,
|
||||
"gloves": 16,
|
||||
"notused2": 17,
|
||||
"book": 18,
|
||||
"belt": 19,
|
||||
"gem": 20,
|
||||
"torch": 21,
|
||||
"scroll": 22,
|
||||
"notused3": 23,
|
||||
"scepter": 24,
|
||||
"wand": 25,
|
||||
"staff": 26,
|
||||
"bow": 27,
|
||||
"axe": 28,
|
||||
"club": 29,
|
||||
"sword": 30,
|
||||
"hammer": 31,
|
||||
"knife": 32,
|
||||
"spear": 33,
|
||||
"polearm": 34,
|
||||
"crossbow": 35,
|
||||
"mace": 36,
|
||||
"helm": 37,
|
||||
"missilepotion": 38,
|
||||
"quest": 39,
|
||||
"bodypart": 40,
|
||||
"key": 41,
|
||||
"throwingknife": 42,
|
||||
"throwingaxe": 43,
|
||||
"javelin": 44,
|
||||
"weapon": 45,
|
||||
"meleeweapon": 46,
|
||||
"missileweapon": 47,
|
||||
"thrownweapon": 48,
|
||||
"comboweapon": 49,
|
||||
"anyarmor": 50,
|
||||
"anyshield": 51,
|
||||
"miscellaneous": 52,
|
||||
"socketfiller": 53,
|
||||
"secondhand": 54,
|
||||
"stavesandrods": 55,
|
||||
"missile": 56,
|
||||
"blunt": 57,
|
||||
"jewel": 58,
|
||||
"classspecific": 59,
|
||||
"amazonitem": 60,
|
||||
"barbarianitem": 61,
|
||||
"necromanceritem": 62,
|
||||
"paladinitem": 63,
|
||||
"sorceressitem": 64,
|
||||
"assassinitem": 65,
|
||||
"druiditem": 66,
|
||||
"handtohand": 67,
|
||||
"orb": 68,
|
||||
"voodooheads": 69,
|
||||
"auricshields": 70,
|
||||
"primalhelm": 71,
|
||||
"pelt": 72,
|
||||
"cloak": 73,
|
||||
"rune": 74,
|
||||
"circlet": 75,
|
||||
"healingpotion": 76,
|
||||
"manapotion": 77,
|
||||
"rejuvpotion": 78,
|
||||
"staminapotion": 79,
|
||||
"antidotepotion": 80,
|
||||
"thawingpotion": 81,
|
||||
"smallcharm": 82,
|
||||
"mediumcharm": 83,
|
||||
"largecharm": 84,
|
||||
"amazonbow": 85,
|
||||
"amazonspear": 86,
|
||||
"amazonjavelin": 87,
|
||||
"assassinclaw": 88,
|
||||
"magicbowquiv": 89,
|
||||
"magicxbowquiv": 90,
|
||||
"chippedgem": 91,
|
||||
"flawedgem": 92,
|
||||
"standardgem": 93,
|
||||
"flawlessgem": 94,
|
||||
"perfectgem": 95,
|
||||
"amethyst": 96,
|
||||
"diamond": 97,
|
||||
"emerald": 98,
|
||||
"ruby": 99,
|
||||
"sapphire": 100,
|
||||
"topaz": 101,
|
||||
"skull": 102,
|
||||
}
|
||||
|
||||
var qualityAliases = map[string]int{
|
||||
"lowquality": 1,
|
||||
"normal": 2,
|
||||
"superior": 3,
|
||||
"magic": 4,
|
||||
"set": 5,
|
||||
"rare": 6,
|
||||
"unique": 7,
|
||||
"crafted": 8,
|
||||
}
|
||||
|
||||
var classAliases = map[string]int{
|
||||
"normal": 0,
|
||||
"exceptional": 1,
|
||||
"elite": 2,
|
||||
}
|
||||
|
||||
var statAliases = map[string][]int{
|
||||
"strength": {0},
|
||||
"energy": {1},
|
||||
"dexterity": {2},
|
||||
"vitality": {3},
|
||||
"statpts": {4},
|
||||
"newskills": {5},
|
||||
"hitpoints": {6},
|
||||
"maxhp": {7},
|
||||
"mana": {8},
|
||||
"maxmana": {9},
|
||||
"stamina": {10},
|
||||
"maxstamina": {11},
|
||||
"level": {12},
|
||||
"experience": {13},
|
||||
"gold": {14},
|
||||
"goldbank": {15},
|
||||
"itemarmorpercent": {16, 0}, "enhanceddefense": {16, 0},
|
||||
"itemmaxdamagepercent": {17, 0},
|
||||
"itemmindamagepercent": {18, 0}, "enhanceddamage": {18, 0},
|
||||
"tohit": {19},
|
||||
"toblock": {20},
|
||||
"plusmindamage": {21, 1},
|
||||
"mindamage": {21},
|
||||
"plusmaxdamage": {22, 1},
|
||||
"maxdamage": {22},
|
||||
"secondarymindamage": {23},
|
||||
"secondarymaxdamage": {24},
|
||||
"damagepercent": {25},
|
||||
"manarecovery": {26},
|
||||
"manarecoverybonus": {27},
|
||||
"staminarecoverybonus": {28},
|
||||
"lastexp": {29},
|
||||
"nextexp": {30},
|
||||
|
||||
"armorclass": {31}, "defense": {31},
|
||||
"plusdefense": {31, 0},
|
||||
|
||||
"armorclassvsmissile": {32},
|
||||
"armorclassvshth": {33},
|
||||
"normaldamagereduction": {34},
|
||||
"magicdamagereduction": {35},
|
||||
"damageresist": {36},
|
||||
"magicresist": {37},
|
||||
"maxmagicresist": {38},
|
||||
"fireresist": {39},
|
||||
"maxfireresist": {40},
|
||||
"lightresist": {41},
|
||||
"maxlightresist": {42},
|
||||
"coldresist": {43},
|
||||
"maxcoldresist": {44},
|
||||
"poisonresist": {45},
|
||||
"maxpoisonresist": {46},
|
||||
"damageaura": {47},
|
||||
"firemindam": {48},
|
||||
"firemaxdam": {49},
|
||||
"lightmindam": {50},
|
||||
"lightmaxdam": {51},
|
||||
"magicmindam": {52},
|
||||
"magicmaxdam": {53},
|
||||
"coldmindam": {54},
|
||||
"coldmaxdam": {55},
|
||||
"coldlength": {56},
|
||||
"poisondamage": {57, 1},
|
||||
"poisonmindam": {57},
|
||||
"poisonmaxdam": {58},
|
||||
"poisonlength": {59},
|
||||
"lifedrainmindam": {60}, "lifeleech": {60},
|
||||
"lifedrainmaxdam": {61},
|
||||
"manadrainmindam": {62}, "manaleech": {62},
|
||||
"manadrainmaxdam": {63},
|
||||
"stamdrainmindam": {64},
|
||||
"stamdrainmaxdam": {65},
|
||||
"stunlength": {66},
|
||||
"velocitypercent": {67},
|
||||
"attackrate": {68},
|
||||
"otheranimrate": {69},
|
||||
"quantity": {70},
|
||||
"value": {71},
|
||||
"durability": {72},
|
||||
"maxdurability": {73},
|
||||
"hpregen": {74},
|
||||
"itemmaxdurabilitypercent": {75},
|
||||
"itemmaxhppercent": {76},
|
||||
"itemmaxmanapercent": {77},
|
||||
"itemattackertakesdamage": {78},
|
||||
"itemgoldbonus": {79},
|
||||
"itemmagicbonus": {80},
|
||||
"itemknockback": {81},
|
||||
"itemtimeduration": {82},
|
||||
|
||||
"itemaddclassskills": {83},
|
||||
"itemaddamazonskills": {83, 0}, "amazonskills": {83, 0},
|
||||
"itemaddsorceressskills": {83, 1}, "sorceressskills": {83, 1},
|
||||
"itemaddnecromancerskills": {83, 2}, "necromancerskills": {83, 2},
|
||||
"itemaddpaladinskills": {83, 3}, "paladinskills": {83, 3},
|
||||
"itemaddbarbarianskills": {83, 4}, "barbarianskills": {83, 4},
|
||||
"itemadddruidskills": {83, 5}, "druidskills": {83, 5},
|
||||
"itemaddassassinskills": {83, 6}, "assassinskills": {83, 6},
|
||||
|
||||
"unsentparam1": {84},
|
||||
"itemaddexperience": {85},
|
||||
"itemhealafterkill": {86},
|
||||
"itemreducedprices": {87},
|
||||
"itemdoubleherbduration": {88},
|
||||
"itemlightradius": {89},
|
||||
"itemlightcolor": {90},
|
||||
"itemreqpercent": {91},
|
||||
"itemlevelreq": {92},
|
||||
"itemfasterattackrate": {93}, "ias": {93},
|
||||
"itemlevelreqpct": {94},
|
||||
"lastblockframe": {95},
|
||||
"itemfastermovevelocity": {96}, "frw": {96},
|
||||
|
||||
// oskill
|
||||
"itemnonclassskill": {97},
|
||||
// Amazon
|
||||
"plusskillcriticalstrike": {97, 9},
|
||||
"plusskillguidedarrow": {97, 22},
|
||||
// Sorceress
|
||||
"plusskillteleport": {97, 54},
|
||||
// Barbarian
|
||||
"plusskillbattleorders": {97, 149},
|
||||
"plusskillbattlecommand": {97, 155},
|
||||
"plusskillbattlecry": {97, 146},
|
||||
// Druid
|
||||
"plusskillwerewolf": {97, 223},
|
||||
"plusskillshapeshifting": {97, 224}, "plusskilllycanthropy": {97, 224},
|
||||
"plusskillsummonspiritwolf": {97, 227},
|
||||
"plusskillferalrage": {97, 232},
|
||||
|
||||
"state": {98},
|
||||
"itemfastergethitrate": {99}, "fhr": {99},
|
||||
"monsterplayercount": {100},
|
||||
"skillpoisonoverridelength": {101},
|
||||
"itemfasterblockrate": {102}, "fbr": {102},
|
||||
"skillbypassundead": {103},
|
||||
"skillbypassdemons": {104},
|
||||
"itemfastercastrate": {105}, "fcr": {105},
|
||||
"skillbypassbeasts": {106},
|
||||
|
||||
"itemsingleskill": {107},
|
||||
// Amazon skills
|
||||
"skillmagicarrow": {107, 6},
|
||||
"skillfirearrow": {107, 7},
|
||||
"skillinnersight": {107, 8},
|
||||
"skillcriticalstrike": {107, 9},
|
||||
"skilljab": {107, 10},
|
||||
"skillcoldarrow": {107, 11},
|
||||
"skillmultipleshot": {107, 12},
|
||||
"skilldodge": {107, 13},
|
||||
"skillpowerstrike": {107, 14},
|
||||
"skillpoisonjavelin": {107, 15},
|
||||
"skillexplodingarrow": {107, 16},
|
||||
"skillslowmissiles": {107, 17},
|
||||
"skillavoid": {107, 18},
|
||||
"skillimpale": {107, 19},
|
||||
"skilllightningbolt": {107, 20},
|
||||
"skillicearrow": {107, 21},
|
||||
"skillguidedarrow": {107, 22},
|
||||
"skillpenetrate": {107, 23},
|
||||
"skillchargedstrike": {107, 24},
|
||||
"skillplaguejavelin": {107, 25},
|
||||
"skillstrafe": {107, 26},
|
||||
"skillimmolationarrow": {107, 27},
|
||||
"skilldecoy": {107, 28},
|
||||
"skillevade": {107, 29},
|
||||
"skillfend": {107, 30},
|
||||
"skillfreezingarrow": {107, 31},
|
||||
"skillvalkyrie": {107, 32},
|
||||
"skillpierce": {107, 33},
|
||||
"skilllightningstrike": {107, 34},
|
||||
"skilllightningfury": {107, 35},
|
||||
// Sorceress skills
|
||||
"skillfirebolt": {107, 36},
|
||||
"skillwarmth": {107, 37},
|
||||
"skillchargedbolt": {107, 38},
|
||||
"skillicebolt": {107, 39},
|
||||
"skillfrozenarmor": {107, 40},
|
||||
"skillinferno": {107, 41},
|
||||
"skillstaticfield": {107, 42},
|
||||
"skilltelekinesis": {107, 43},
|
||||
"skillfrostnova": {107, 44},
|
||||
"skilliceblast": {107, 45},
|
||||
"skillblaze": {107, 46},
|
||||
"skillfireball": {107, 47},
|
||||
"skillnova": {107, 48},
|
||||
"skilllightning": {107, 49},
|
||||
"skillshiverarmor": {107, 50},
|
||||
"skillfirewall": {107, 51},
|
||||
"skillenchant": {107, 52},
|
||||
"skillchainlightning": {107, 53},
|
||||
"skillteleport": {107, 54},
|
||||
"skillglacialspike": {107, 55},
|
||||
"skillmeteor": {107, 56},
|
||||
"skillthunderstorm": {107, 57},
|
||||
"skillenergyshield": {107, 58},
|
||||
"skillblizzard": {107, 59},
|
||||
"skillchillingarmor": {107, 60},
|
||||
"skillfiremastery": {107, 61},
|
||||
"skillhydra": {107, 62},
|
||||
"skilllightningmastery": {107, 63},
|
||||
"skillfrozenorb": {107, 64},
|
||||
"skillcoldmastery": {107, 65},
|
||||
// Necromancer skills
|
||||
"skillamplifydamage": {107, 66},
|
||||
"skillteeth": {107, 67},
|
||||
"skillbonearmor": {107, 68},
|
||||
"skillskeletonmastery": {107, 69},
|
||||
"skillraiseskeleton": {107, 70},
|
||||
"skilldimvision": {107, 71},
|
||||
"skillweaken": {107, 72},
|
||||
"skillpoisondagger": {107, 73},
|
||||
"skillcorpseexplosion": {107, 74},
|
||||
"skillclaygolem": {107, 75},
|
||||
"skillironmaiden": {107, 76},
|
||||
"skillterror": {107, 77},
|
||||
"skillbonewall": {107, 78},
|
||||
"skillgolemmastery": {107, 79},
|
||||
"skillskeletalmage": {107, 80},
|
||||
"skillconfuse": {107, 81},
|
||||
"skilllifetap": {107, 82},
|
||||
"skillpoisonexplosion": {107, 83},
|
||||
"skillbonespear": {107, 84},
|
||||
"skillbloodgolem": {107, 85},
|
||||
"skillattract": {107, 86},
|
||||
"skilldecrepify": {107, 87},
|
||||
"skillboneprison": {107, 88},
|
||||
"skillsummonresist": {107, 89},
|
||||
"skillirongolem": {107, 90},
|
||||
"skilllowerresist": {107, 91},
|
||||
"skillpoisonnova": {107, 92},
|
||||
"skillbonespirit": {107, 93},
|
||||
"skillfiregolem": {107, 94},
|
||||
"skillrevive": {107, 95},
|
||||
// Paladin skills
|
||||
"skillsacrifice": {107, 96},
|
||||
"skillsmite": {107, 97},
|
||||
"skillmight": {107, 98},
|
||||
"skillprayer": {107, 99},
|
||||
"skillresistfire": {107, 100},
|
||||
"skillholybolt": {107, 101},
|
||||
"skillholyfire": {107, 102},
|
||||
"skillthorns": {107, 103},
|
||||
"skilldefiance": {107, 104},
|
||||
"skillresistcold": {107, 105},
|
||||
"skillzeal": {107, 106},
|
||||
"skillcharge": {107, 107},
|
||||
"skillblessedaim": {107, 108},
|
||||
"skillcleansing": {107, 109},
|
||||
"skillresistlightning": {107, 110},
|
||||
"skillvengeance": {107, 111},
|
||||
"skillblessedhammer": {107, 112},
|
||||
"skillconcentration": {107, 113},
|
||||
"skillholyfreeze": {107, 114},
|
||||
"skillvigor": {107, 115},
|
||||
"skillconversion": {107, 116},
|
||||
"skillholyshield": {107, 117},
|
||||
"skillholyshock": {107, 118},
|
||||
"skillsanctuary": {107, 119},
|
||||
"skillmeditation": {107, 120},
|
||||
"skillfistoftheheavens": {107, 121},
|
||||
"skillfanaticism": {107, 122},
|
||||
"skillconviction": {107, 123},
|
||||
"skillredemption": {107, 124},
|
||||
"skillsalvation": {107, 125},
|
||||
// Barbarian skills
|
||||
"skillbash": {107, 126},
|
||||
"skillswordmastery": {107, 127},
|
||||
"skillaxemastery": {107, 128},
|
||||
"skillmacemastery": {107, 129},
|
||||
"skillhowl": {107, 130},
|
||||
"skillfindpotion": {107, 131},
|
||||
"skillleap": {107, 132},
|
||||
"skilldoubleswing": {107, 133},
|
||||
"skillpolearmmastery": {107, 134},
|
||||
"skillthrowingmastery": {107, 135},
|
||||
"skillspearmastery": {107, 136},
|
||||
"skilltaunt": {107, 137},
|
||||
"skillshout": {107, 138},
|
||||
"skillstun": {107, 139},
|
||||
"skilldoublethrow": {107, 140},
|
||||
"skillincreasedstamina": {107, 141},
|
||||
"skillfinditem": {107, 142},
|
||||
"skillleapattack": {107, 143},
|
||||
"skillconcentrate": {107, 144},
|
||||
"skillironskin": {107, 145},
|
||||
"skillbattlecry": {107, 146},
|
||||
"skillfrenzy": {107, 147},
|
||||
"skillincreasedspeed": {107, 148},
|
||||
"skillbattleorders": {107, 149},
|
||||
"skillgrimward": {107, 150},
|
||||
"skillwhirlwind": {107, 151},
|
||||
"skillberserk": {107, 152},
|
||||
"skillnaturalresistance": {107, 153},
|
||||
"skillwarcry": {107, 154},
|
||||
"skillbattlecommand": {107, 155},
|
||||
// Druid skills
|
||||
"skillraven": {107, 221},
|
||||
"skillpoisoncreeper": {107, 222},
|
||||
"skillwerewolf": {107, 223},
|
||||
"skilllycanthropy": {107, 224},
|
||||
"skillfirestorm": {107, 225},
|
||||
"skilloaksage": {107, 226},
|
||||
"skillsummonspiritwolf": {107, 227},
|
||||
"skillwerebear": {107, 228},
|
||||
"skillmoltenboulder": {107, 229},
|
||||
"skillarcticblast": {107, 230},
|
||||
"skillcarrionvine": {107, 231},
|
||||
"skillferalrage": {107, 232},
|
||||
"skillmaul": {107, 233},
|
||||
"skillfissure": {107, 234},
|
||||
"skillcyclonearmor": {107, 235},
|
||||
"skillheartofwolverine": {107, 236},
|
||||
"skillsummondirewolf": {107, 237},
|
||||
"skillrabies": {107, 238},
|
||||
"skillfireclaws": {107, 239},
|
||||
"skilltwister": {107, 240},
|
||||
"skillsolarcreeper": {107, 241},
|
||||
"skillhunger": {107, 242},
|
||||
"skillshockwave": {107, 243},
|
||||
"skillvolcano": {107, 244},
|
||||
"skilltornado": {107, 245},
|
||||
"skillspiritofbarbs": {107, 246},
|
||||
"skillsummongrizzly": {107, 247},
|
||||
"skillfury": {107, 248},
|
||||
"skillarmageddon": {107, 249},
|
||||
"skillhurricane": {107, 250},
|
||||
// Assassin skills
|
||||
"skillfireblast": {107, 251},
|
||||
"skillclawmastery": {107, 252},
|
||||
"skillpsychichammer": {107, 253},
|
||||
"skilltigerstrike": {107, 254},
|
||||
"skilldragontalon": {107, 255},
|
||||
"skillshockweb": {107, 256},
|
||||
"skillbladesentinel": {107, 257},
|
||||
"skillburstofspeed": {107, 258},
|
||||
"skillfistsoffire": {107, 259},
|
||||
"skilldragonclaw": {107, 260},
|
||||
"skillchargedboltsentry": {107, 261},
|
||||
"skillwakeoffire": {107, 262},
|
||||
"skillweaponblock": {107, 263},
|
||||
"skillcloakofshadows": {107, 264},
|
||||
"skillcobrastrike": {107, 265},
|
||||
"skillbladefury": {107, 266},
|
||||
"skillfade": {107, 267},
|
||||
"skillshadowwarrior": {107, 268},
|
||||
"skillclawsofthunder": {107, 269},
|
||||
"skilldragontail": {107, 270},
|
||||
"skilllightningsentry": {107, 271},
|
||||
"skillwakeofinferno": {107, 272},
|
||||
"skillmindblast": {107, 273},
|
||||
"skillbladesofice": {107, 274},
|
||||
"skilldragonflight": {107, 275},
|
||||
"skilldeathsentry": {107, 276},
|
||||
"skillbladeshield": {107, 277},
|
||||
"skillvenom": {107, 278},
|
||||
"skillshadowmaster": {107, 279},
|
||||
"skillphoenixstrike": {107, 280},
|
||||
|
||||
"itemrestinpeace": {108},
|
||||
"curseresistance": {109},
|
||||
"itempoisonlengthresist": {110},
|
||||
"itemnormaldamage": {111},
|
||||
"itemhowl": {112},
|
||||
"itemstupidity": {113},
|
||||
"itemdamagetomana": {114},
|
||||
"itemignoretargetac": {115},
|
||||
"itemfractionaltargetac": {116},
|
||||
"itempreventheal": {117},
|
||||
"itemhalffreezeduration": {118},
|
||||
"itemtohitpercent": {119},
|
||||
"itemdamagetargetac": {120},
|
||||
"itemdemondamagepercent": {121},
|
||||
"itemundeaddamagepercent": {122},
|
||||
"itemdemontohit": {123},
|
||||
"itemundeadtohit": {124},
|
||||
"itemthrowable": {125},
|
||||
"itemelemskill": {126},
|
||||
"itemallskills": {127},
|
||||
"itemattackertakeslightdamage": {128},
|
||||
"ironmaidenlevel": {129},
|
||||
"lifetaplevel": {130},
|
||||
"thornspercent": {131},
|
||||
"bonearmor": {132},
|
||||
"bonearmormax": {133},
|
||||
"itemfreeze": {134},
|
||||
"itemopenwounds": {135},
|
||||
"itemcrushingblow": {136},
|
||||
"itemkickdamage": {137},
|
||||
"itemmanaafterkill": {138},
|
||||
"itemhealafterdemonkill": {139},
|
||||
"itemextrablood": {140},
|
||||
"itemdeadlystrike": {141},
|
||||
"itemabsorbfirepercent": {142},
|
||||
"itemabsorbfire": {143},
|
||||
"itemabsorblightpercent": {144},
|
||||
"itemabsorblight": {145},
|
||||
"itemabsorbmagicpercent": {146},
|
||||
"itemabsorbmagic": {147},
|
||||
"itemabsorbcoldpercent": {148},
|
||||
"itemabsorbcold": {149},
|
||||
"itemslow": {150},
|
||||
|
||||
"itemaura": {151},
|
||||
"mightaura": {151, 98},
|
||||
"holyfireaura": {151, 102},
|
||||
"thornsaura": {151, 103},
|
||||
"defianceaura": {151, 104},
|
||||
"concentrationaura": {151, 113},
|
||||
"holyfreezeaura": {151, 114},
|
||||
"vigoraura": {151, 115},
|
||||
"holyshockaura": {151, 118},
|
||||
"sanctuaryaura": {151, 119},
|
||||
"meditationaura": {151, 120},
|
||||
"fanaticismaura": {151, 122},
|
||||
"convictionaura": {151, 123},
|
||||
"redemptionaura": {151, 124},
|
||||
|
||||
"itemindestructible": {152},
|
||||
"itemcannotbefrozen": {153},
|
||||
"itemstaminadrainpct": {154},
|
||||
"itemreanimate": {155},
|
||||
"itempierce": {156},
|
||||
"itemmagicarrow": {157},
|
||||
"itemexplosivearrow": {158},
|
||||
"itemthrowmindamage": {159},
|
||||
"itemthrowmaxdamage": {160},
|
||||
"itemskillhandofathena": {161},
|
||||
"itemskillstaminapercent": {162},
|
||||
"itemskillpassivestaminapercent": {163},
|
||||
"itemskillconcentration": {164},
|
||||
"itemskillenchant": {165},
|
||||
"itemskillpierce": {166},
|
||||
"itemskillconviction": {167},
|
||||
"itemskillchillingarmor": {168},
|
||||
"itemskillfrenzy": {169},
|
||||
"itemskilldecrepify": {170},
|
||||
"itemskillarmorpercent": {171},
|
||||
"alignment": {172},
|
||||
"target0": {173},
|
||||
"target1": {174},
|
||||
"goldlost": {175},
|
||||
"conversionlevel": {176},
|
||||
"conversionmaxhp": {177},
|
||||
"unitdooverlay": {178},
|
||||
"attackvsmontype": {179},
|
||||
"damagevsmontype": {180},
|
||||
"fade": {181},
|
||||
"armoroverridepercent": {182},
|
||||
"unused183": {183},
|
||||
"unused184": {184},
|
||||
"unused185": {185},
|
||||
"unused186": {186},
|
||||
"unused187": {187},
|
||||
|
||||
"itemaddskilltab": {188},
|
||||
"itemaddbowandcrossbowskilltab": {188, 0}, "bowandcrossbowskilltab": {188, 0},
|
||||
"itemaddpassiveandmagicskilltab": {188, 1}, "passiveandmagicskilltab": {188, 1},
|
||||
"itemaddjavelinandspearskilltab": {188, 2}, "javelinandspearskilltab": {188, 2},
|
||||
"itemaddfireskilltab": {188, 8}, "fireskilltab": {188, 8},
|
||||
"itemaddlightningskilltab": {188, 9}, "lightningskilltab": {188, 9},
|
||||
"itemaddcoldskilltab": {188, 10}, "coldskilltab": {188, 10},
|
||||
"itemaddcursesskilltab": {188, 16}, "cursesskilltab": {188, 16},
|
||||
"itemaddpoisonandboneskilltab": {188, 17}, "poisonandboneskilltab": {188, 17},
|
||||
"itemaddnecromancersummoningskilltab": {188, 18}, "necromancersummoningskilltab": {188, 18},
|
||||
"itemaddpalicombatskilltab": {188, 24}, "palicombatskilltab": {188, 24},
|
||||
"itemaddoffensiveaurasskilltab": {188, 25}, "offensiveaurasskilltab": {188, 25},
|
||||
"itemadddefensiveaurasskilltab": {188, 26}, "defensiveaurasskilltab": {188, 26},
|
||||
"itemaddbarbcombatskilltab": {188, 32}, "barbcombatskilltab": {188, 32},
|
||||
"itemaddmasteriesskilltab": {188, 33}, "masteriesskilltab": {188, 33},
|
||||
"itemaddwarcriesskilltab": {188, 34}, "warcriesskilltab": {188, 34},
|
||||
"itemadddruidsummoningskilltab": {188, 40}, "druidsummoningskilltab": {188, 40},
|
||||
"itemaddshapeshiftingskilltab": {188, 41}, "shapeshiftingskilltab": {188, 41},
|
||||
"itemaddelementalskilltab": {188, 42}, "elementalskilltab": {188, 42},
|
||||
"itemaddtrapsskilltab": {188, 48}, "trapsskilltab": {188, 48},
|
||||
"itemaddshadowdisciplinesskilltab": {188, 49}, "shadowdisciplinesskilltab": {188, 49},
|
||||
"itemaddmartialartsskilltab": {188, 50}, "martialartsskilltab": {188, 50},
|
||||
|
||||
"unused189": {189},
|
||||
"unused190": {190},
|
||||
"unused191": {191},
|
||||
"unused192": {192},
|
||||
"unused193": {193},
|
||||
"itemnumsockets": {194}, "sockets": {194},
|
||||
"itemskillonattack": {195, 1},
|
||||
"itemskillonattacklevel": {195, 2},
|
||||
"itemskillonkill": {196, 1},
|
||||
"itemskillonkilllevel": {196, 2},
|
||||
"itemskillondeath": {197, 1},
|
||||
"itemskillondeathlevel": {197, 2},
|
||||
|
||||
"itemskillonhit": {198, 1},
|
||||
"itemskillonhitlevel": {198, 2},
|
||||
"amplifydamageonhit": {198, 4225},
|
||||
|
||||
"itemskillonlevelup": {199, 1},
|
||||
"itemskillonleveluplevel": {199, 2},
|
||||
"unused200": {200},
|
||||
"itemskillongethit": {201, 1},
|
||||
"itemskillongethitlevel": {201, 2},
|
||||
"unused202": {202},
|
||||
"unused203": {203},
|
||||
|
||||
"itemchargedskill": {204, 1},
|
||||
"itemchargedskilllevel": {204, 2},
|
||||
"teleportcharges": {204, 3461},
|
||||
|
||||
"unused204": {205},
|
||||
"unused205": {206},
|
||||
"unused206": {207},
|
||||
"unused207": {208},
|
||||
"unused208": {209},
|
||||
"unused209": {210},
|
||||
"unused210": {211},
|
||||
"unused211": {212},
|
||||
"unused212": {213},
|
||||
"itemarmorperlevel": {214},
|
||||
"itemarmorpercentperlevel": {215},
|
||||
"itemhpperlevel": {216},
|
||||
"itemmanaperlevel": {217},
|
||||
"itemmaxdamageperlevel": {218},
|
||||
"itemmaxdamagepercentperlevel": {219},
|
||||
"itemstrengthperlevel": {220},
|
||||
"itemdexterityperlevel": {221},
|
||||
"itemenergyperlevel": {222},
|
||||
"itemvitalityperlevel": {223},
|
||||
"itemtohitperlevel": {224},
|
||||
"itemtohitpercentperlevel": {225},
|
||||
"itemcolddamagemaxperlevel": {226},
|
||||
"itemfiredamagemaxperlevel": {227},
|
||||
"itemltngdamagemaxperlevel": {228},
|
||||
"itempoisdamagemaxperlevel": {229},
|
||||
"itemresistcoldperlevel": {230},
|
||||
"itemresistfireperlevel": {231},
|
||||
"itemresistltngperlevel": {232},
|
||||
"itemresistpoisperlevel": {233},
|
||||
"itemabsorbcoldperlevel": {234},
|
||||
"itemabsorbfireperlevel": {235},
|
||||
"itemabsorbltngperlevel": {236},
|
||||
"itemabsorbpoisperlevel": {237},
|
||||
"itemthornsperlevel": {238},
|
||||
"itemfindgoldperlevel": {239},
|
||||
"itemfindmagicperlevel": {240},
|
||||
"itemregenstaminaperlevel": {241},
|
||||
"itemstaminaperlevel": {242},
|
||||
"itemdamagedemonperlevel": {243},
|
||||
"itemdamageundeadperlevel": {244},
|
||||
"itemtohitdemonperlevel": {245},
|
||||
"itemtohitundeadperlevel": {246},
|
||||
"itemcrushingblowperlevel": {247},
|
||||
"itemopenwoundsperlevel": {248},
|
||||
"itemkickdamageperlevel": {249},
|
||||
"itemdeadlystrikeperlevel": {250},
|
||||
"itemfindgemsperlevel": {251},
|
||||
"itemreplenishdurability": {252},
|
||||
"itemreplenishquantity": {253},
|
||||
"itemextrastack": {254},
|
||||
"itemfinditem": {255},
|
||||
"itemslashdamage": {256},
|
||||
"itemslashdamagepercent": {257},
|
||||
"itemcrushdamage": {258},
|
||||
"itemcrushdamagepercent": {259},
|
||||
"itemthrustdamage": {260},
|
||||
"itemthrustdamagepercent": {261},
|
||||
"itemabsorbslash": {262},
|
||||
"itemabsorbcrush": {263},
|
||||
"itemabsorbthrust": {264},
|
||||
"itemabsorbslashpercent": {265},
|
||||
"itemabsorbcrushpercent": {266},
|
||||
"itemabsorbthrustpercent": {267},
|
||||
"itemarmorbytime": {268},
|
||||
"itemarmorpercentbytime": {269},
|
||||
"itemhpbytime": {270},
|
||||
"itemmanabytime": {271},
|
||||
"itemmaxdamagebytime": {272},
|
||||
"itemmaxdamagepercentbytime": {273},
|
||||
"itemstrengthbytime": {274},
|
||||
"itemdexteritybytime": {275},
|
||||
"itemenergybytime": {276},
|
||||
"itemvitalitybytime": {277},
|
||||
"itemtohitbytime": {278},
|
||||
"itemtohitpercentbytime": {279},
|
||||
"itemcolddamagemaxbytime": {280},
|
||||
"itemfiredamagemaxbytime": {281},
|
||||
"itemltngdamagemaxbytime": {282},
|
||||
"itempoisdamagemaxbytime": {283},
|
||||
"itemresistcoldbytime": {284},
|
||||
"itemresistfirebytime": {285},
|
||||
"itemresistltngbytime": {286},
|
||||
"itemresistpoisbytime": {287},
|
||||
"itemabsorbcoldbytime": {288},
|
||||
"itemabsorbfirebytime": {289},
|
||||
"itemabsorbltngbytime": {290},
|
||||
"itemabsorbpoisbytime": {291},
|
||||
"itemfindgoldbytime": {292},
|
||||
"itemfindmagicbytime": {293},
|
||||
"itemregenstaminabytime": {294},
|
||||
"itemstaminabytime": {295},
|
||||
"itemdamagedemonbytime": {296},
|
||||
"itemdamageundeadbytime": {297},
|
||||
"itemtohitdemonbytime": {298},
|
||||
"itemtohitundeadbytime": {299},
|
||||
"itemcrushingblowbytime": {300},
|
||||
"itemopenwoundsbytime": {301},
|
||||
"itemkickdamagebytime": {302},
|
||||
"itemdeadlystrikebytime": {303},
|
||||
"itemfindgemsbytime": {304},
|
||||
"itempiercecold": {305},
|
||||
"itempiercefire": {306},
|
||||
"itempierceltng": {307},
|
||||
"itempiercepois": {308},
|
||||
"itemdamagevsmonster": {309},
|
||||
"itemdamagepercentvsmonster": {310},
|
||||
"itemtohitvsmonster": {311},
|
||||
"itemtohitpercentvsmonster": {312},
|
||||
"itemacvsmonster": {313},
|
||||
"itemacpercentvsmonster": {314},
|
||||
"firelength": {315},
|
||||
"burningmin": {316},
|
||||
"burningmax": {317},
|
||||
"progressivedamage": {318},
|
||||
"progressivesteal": {319},
|
||||
"progressiveother": {320},
|
||||
"progressivefire": {321},
|
||||
"progressivecold": {322},
|
||||
"progressivelightning": {323},
|
||||
"itemextracharges": {324},
|
||||
"progressivetohit": {325},
|
||||
"poisoncount": {326},
|
||||
"damageframerate": {327},
|
||||
"pierceidx": {328},
|
||||
"passivefiremastery": {329},
|
||||
"passiveltngmastery": {330},
|
||||
"passivecoldmastery": {331},
|
||||
"passivepoismastery": {332},
|
||||
"passivefirepierce": {333},
|
||||
"passiveltngpierce": {334},
|
||||
"passivecoldpierce": {335},
|
||||
"passivepoispierce": {336},
|
||||
"passivecriticalstrike": {337},
|
||||
"passivedodge": {338},
|
||||
"passiveavoid": {339},
|
||||
"passiveevade": {340},
|
||||
"passivewarmth": {341},
|
||||
"passivemasterymeleeth": {342},
|
||||
"passivemasterymeleedmg": {343},
|
||||
"passivemasterymeleecrit": {344},
|
||||
"passivemasterythrowth": {345},
|
||||
"passivemasterythrowdmg": {346},
|
||||
"passivemasterythrowcrit": {347},
|
||||
"passiveweaponblock": {348},
|
||||
"passivesummonresist": {349},
|
||||
"modifierlistskill": {350},
|
||||
"modifierlistlevel": {351},
|
||||
"lastsenthppct": {352},
|
||||
"sourceunittype": {353},
|
||||
"sourceunitid": {354},
|
||||
"shortparam1": {355},
|
||||
"questitemdifficulty": {356},
|
||||
"passivemagmastery": {357},
|
||||
"passivemagpierce": {358},
|
||||
|
||||
// Doesnt really exists, but is calculated in getStatEx
|
||||
"allres": {555},
|
||||
}
|
||||
8
pkg/nip/errors.go
Normal file
8
pkg/nip/errors.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package nip
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// ErrEmptyRule is returned when a rule is empty
|
||||
ErrEmptyRule = errors.New("empty rule")
|
||||
)
|
||||
@@ -6,9 +6,12 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hectorgimenez/d2go/pkg/data"
|
||||
"github.com/hectorgimenez/d2go/pkg/data/item"
|
||||
)
|
||||
|
||||
func ReadDir(path string) ([]Rule, error) {
|
||||
func ReadDir(path string) (Rules, error) {
|
||||
files, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -31,7 +34,7 @@ func ReadDir(path string) ([]Rule, error) {
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func ParseNIPFile(filePath string) ([]Rule, error) {
|
||||
func ParseNIPFile(filePath string) (Rules, error) {
|
||||
fileReader, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -43,18 +46,44 @@ func ParseNIPFile(filePath string) ([]Rule, error) {
|
||||
|
||||
rules := make([]Rule, 0)
|
||||
lineNumber := 0
|
||||
|
||||
dummyItem := data.Item{
|
||||
ID: 516,
|
||||
Name: "healingpotion",
|
||||
Quality: item.QualityNormal,
|
||||
}
|
||||
|
||||
for fileScanner.Scan() {
|
||||
lineNumber++
|
||||
rule, err := ParseLine(fileScanner.Text(), filePath, lineNumber)
|
||||
if errors.Is(err, errEmptyLine) {
|
||||
rule, err := New(fileScanner.Text(), filePath, lineNumber)
|
||||
if errors.Is(err, ErrEmptyRule) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading %s file at line %d: %w", filePath, lineNumber, err)
|
||||
}
|
||||
|
||||
// We evaluate all the rules at startup to ensure no format errors, if there is a format error we will throw it now instead of during runtime
|
||||
_, err = rule.Evaluate(dummyItem)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error testing rule on [%s:%d]: %w", filePath, lineNumber, err)
|
||||
}
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func sanitizeLine(rawLine string) string {
|
||||
l := strings.Split(rawLine, "//")
|
||||
line := strings.TrimSpace(l[0])
|
||||
line = strings.Join(strings.Fields(line), " ")
|
||||
line = strings.ReplaceAll(line, "'", "")
|
||||
|
||||
// Fix possible wrong formatted lines
|
||||
line = strings.ReplaceAll(line, "=>", ">=")
|
||||
line = strings.ReplaceAll(line, "=<", "<=")
|
||||
line = strings.TrimSpace(strings.Trim(line, "&&"))
|
||||
|
||||
return strings.ToLower(line)
|
||||
}
|
||||
|
||||
15
pkg/nip/file_reader_test.go
Normal file
15
pkg/nip/file_reader_test.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package nip
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseNIPFile(t *testing.T) {
|
||||
path := "mock/rare.nip"
|
||||
|
||||
rules, err := ParseNIPFile(path)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, rules)
|
||||
}
|
||||
21
pkg/nip/ignored_properties.go
Normal file
21
pkg/nip/ignored_properties.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package nip
|
||||
|
||||
// These stats are currently NOT supported, rules can not contain them otherwise it will return an error
|
||||
var notSupportedStats = []string{
|
||||
"plusmindamage",
|
||||
"mindamage",
|
||||
"plusmaxdamage",
|
||||
"maxdamage",
|
||||
"enhanceddamage",
|
||||
"itemarmorpercent",
|
||||
"itemmindamagepercent",
|
||||
"itemslashdamage",
|
||||
"itemslashdamagepercent",
|
||||
"itemcrushdamage",
|
||||
"itemcrushdamagepercent",
|
||||
"itemthrustdamage",
|
||||
"itemthrustdamagepercent",
|
||||
"secondarymindamage",
|
||||
"secondarymaxdamage",
|
||||
"damagepercent",
|
||||
}
|
||||
103
pkg/nip/mock/rare.nip
Normal file
103
pkg/nip/mock/rare.nip
Normal file
@@ -0,0 +1,103 @@
|
||||
// rare jewels
|
||||
[type] == jewel
|
||||
|
||||
// rare rings
|
||||
[type] == ring && [quality] == rare # [fcr] >= 10 && [strength] + [dexterity] + [maxhp] + [maxmana] + [fireresist] + [lightresist] + [coldresist] + [poisonresist] >= 50
|
||||
|
||||
// rare amulets
|
||||
[type] == amulet && [quality] == rare # [itemaddclassskills] >= 2
|
||||
|
||||
// rare circlets
|
||||
[type] == circlet && [quality] == rare && [flag] != ethereal # [itemaddclassskills] >= 2 && [dexterity]+[strength] >= 10 && ([itemtohitpercentperlevel] >= 1 || [fhr]+[frw] >= 10 || [maxhp]+[maxmana] >= 10 ||[coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 10)
|
||||
[type] == circlet && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && [itemaddclassskills] >= 2 && [dexterity]+[strength] >= 10 && ([itemtohitpercentperlevel] >= 1 || [fhr]+[frw] >= 10 || [maxhp]+[maxmana] >= 10 ||[coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 10)
|
||||
|
||||
|
||||
[type] == circlet && [quality] == rare && [flag] != ethereal # [amazonskills] >= 2 && [frw] >= 10 && ([dexterity]+[strength] >= 10 || [maxhp] >= 20) && (([itemtohitpercentperlevel] >= 1) || ([dexterity]+[strength] >= 20) || ([maxhp] >= 10) || ([fhr] >= 10) || ([fcr] >= 20) || ([coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 10))
|
||||
[type] == circlet && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && [amazonskills] >= 2 && [frw] >= 10 && ([dexterity]+[strength] >= 10 || [maxhp] >= 20) && (([itemtohitpercentperlevel] >= 1) || ([dexterity]+[strength] >= 20) || ([maxhp] >= 10) || ([fhr] >= 10) || ([fcr] >= 10) || ([coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 10))
|
||||
|
||||
|
||||
[type] == circlet && [quality] == rare && [flag] != ethereal # [paladinskills] >= 2
|
||||
[type] == circlet && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && [paladinskills] >= 2
|
||||
|
||||
[type] == circlet && [quality] == rare && [flag] != ethereal # [necromancerskills] >= 2
|
||||
[type] == circlet && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && [necromancerskills] >= 2
|
||||
|
||||
[type] == circlet && [quality] == rare && [flag] != ethereal # [itemgoldbonus] >= 40 && [sockets] >= 2
|
||||
[type] == circlet && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && [itemgoldbonus] >= 40 && [sockets] >= 2
|
||||
|
||||
// rare sorceress orbs
|
||||
[type] == orb && [quality] == rare # [fcr] >= 20 && [sorceressskills]+[skilllightning] >= 5
|
||||
[type] == orb && [quality] == rare # [fcr] >= 20 && [sorceressskills]+[skillfrozenorb]+[skillblizzard] >= 5
|
||||
[type] == orb && [quality] == rare # [fcr] >= 20 && [sorceressskills]+[skillfireball]+[skillhydra] >= 5
|
||||
|
||||
// rare barb helms
|
||||
[type] == primalhelm && [quality] == rare # [barbarianskills]+[skillbattleorders] >= 5
|
||||
[type] == primalhelm && [quality] == rare # [itemtohitpercentperlevel] >= 1 || [sockets] == 2
|
||||
[type] == primalhelm && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && ([itemtohitpercentperlevel] >= 1 || [sockets] == 2)
|
||||
|
||||
// rare wands
|
||||
[type] == wand && [quality] == rare # [necromancerskills]+[poisonandboneskilltab]+[skillbonespear] +[skillpoisonnova] + [skillrevive] >= 4
|
||||
|
||||
// rare necro shields
|
||||
[type] == voodooheads && [quality] == rare # [necromancerskills] == 2 && ([skillbonespear] >= 3 || [skillpoisonnova] >= 3) && ([sockets] >= 2 || ([fbr] >= 30 && ([maxhp] >= 20 || [fhr] >= 17)))
|
||||
[type] == voodooheads && [quality] == rare # [necromancerskills]+[skillbonespear]+[skillpoisonnova]+[skillrevive]+[skilllowerresist]+[skilldecrepify]+[poisonandboneskilltab]+[necromancersummoningskilltab]+[skillbonespirit] >= 5
|
||||
|
||||
// rare helm
|
||||
[type] == helm && [quality] == rare && [flag] != ethereal # [itemtohitpercentperlevel] >= 1 && [maxhp] >= 39 && [sockets] == 2 && [fhr] >= 10
|
||||
[type] == helm && [quality] == rare && [flag] == ethereal # [itemtohitpercentperlevel] >= 1 && [maxhp] >= 35 && [sockets] == 2 && ([fhr] >= 10 || [itemtohitpercent] >= 5 ) && [itemreplenishdurability] >=1
|
||||
[type] == helm && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && ([itemtohitpercentperlevel] >= 1 || [sockets] == 2)
|
||||
|
||||
// rare armor
|
||||
//([name] == archonplate || [name] == mageplate || [name] == lightplate || [name] == gothicplate || [name] == ancientarmor || [name] == wirefleece)
|
||||
//[type] == armor && [quality] == rare && [flag] != ethereal # [sockets] == 2 && [maxhp] >= 55 && ([dexterity] >= 9 || [strength] >= 17) && (([coldresist]+[fireresist]+[lightresist] >= 20) || [fhr] >= 17 || [hpregen] >= 4)
|
||||
[type] == armor && [quality] == rare && [flag] == ethereal # [sockets] == 2 && [maxhp] >= 50 && [dexterity]+[strength] >= 7 && [itemreplenishdurability] >= 1
|
||||
[type] == armor && [quality] == rare && [flag] == ethereal # [sockets] == 2 && [itemreplenishdurability] >=1
|
||||
[type] == armor && [quality] == rare && [flag] == ethereal # [sockets] == 2 && [itemreplenishdurability] >=1 && ([maxhp] >= 20 || [dexterity]+[strength] >= 7 || [hpregen] >= 4)
|
||||
|
||||
// rare shields
|
||||
[type] == shield && [quality] == rare && [flag] == ethereal # [sockets] == 2 && [itemreplenishdurability] >=1
|
||||
[type] == shield && [quality] == rare && [flag] != ethereal # [fhr] >= 17 && [sockets] == 2 && [fbr] >= 30 && ([maxhp] >= 30 || [coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 40)
|
||||
[type] == shield && [quality] == rare && [flag] == ethereal # [fhr] >= 17 && [sockets] == 2 && [fbr] >= 30 && ([maxhp] >= 30 || [coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 40) && [itemreplenishdurability] >= 1
|
||||
[type] == shield && [quality] == rare && [flag] != ethereal # [maxhp] >= 30 && [coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 40 && [sockets] == 2 && [fbr] >= 30
|
||||
[type] == shield && [quality] == rare && [flag] == ethereal # [maxhp] >= 30 && [coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 40 && [sockets] == 2 && [fbr] >= 30 && [itemreplenishdurability] >= 1
|
||||
|
||||
// rare gloves
|
||||
[type] == gloves && [quality] == rare && [flag] == ethereal # [ias] >= 20 && ([bowandcrossbowskilltab] == 2 || [javelinandspearskilltab] == 2) && [itemreplenishdurability] >=1
|
||||
[type] == gloves && [quality] == rare && [flag] != ethereal # [ias] >= 20 && ([bowandcrossbowskilltab] == 2 || [javelinandspearskilltab] == 2)
|
||||
|
||||
[type] == gloves && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && ([enhanceddefense] >= 200 || [ias] >= 20 || ([dexterity]+[strength] >= 10) || [hpregen] >= 5 || [passiveandmagicskilltab] == 2 || [javelinandspearskilltab] == 2 || ([coldresist]+[fireresist]+[lightresist] >= 20))
|
||||
[type] == gloves && [quality] == rare && [flag] != ethereal # [dexterity]+[strength] >= 25 && [coldresist]+[fireresist]+[lightresist] >= 55
|
||||
[type] == gloves && [quality] == rare && [flag] != ethereal # [ias] == 20 && ([dexterity] >= 15 || [strength] >= 15) && ([bowandcrossbowskilltab] == 2 || [javelinandspearskilltab] == 2) && [coldresist]+[fireresist]+[lightresist] >= 25
|
||||
//[type] == gloves && [quality] == rare && [flag] != ethereal # [ias] == 20 && [dexterity]+[strength] >= 20 && (([coldresist]+[fireresist]+[lightresist] >= 35) || ([coldresist]+[fireresist] >= 40) || ([coldresist]+[lightresist] >= 40) || ([fireresist]+[lightresist] >= 40))
|
||||
[type] == gloves && [quality] == rare && [flag] != ethereal # [ias] == 20 && [dexterity]+[strength] >= 20 && ([bowandcrossbowskilltab] == 2 || [javelinandspearskilltab] == 2)
|
||||
[type] == gloves && [quality] == rare && [flag] == ethereal # [dexterity]+[strength] >= 25 && [coldresist]+[fireresist]+[lightresist] >= 60 && [itemreplenishdurability] >= 1
|
||||
[type] == gloves && [quality] == rare && [flag] == ethereal # [ias] == 20 && ([dexterity] >= 15 || [strength] >= 15) && ([bowandcrossbowskilltab] == 2 || [javelinandspearskilltab] == 2) && [coldresist]+[fireresist]+[lightresist] >= 25 && [itemreplenishdurability] >= 1
|
||||
//[type] == gloves && [quality] == rare && [flag] == ethereal # [ias] == 20 && [dexterity]+[strength] >= 20 && (([coldresist]+[fireresist]+[lightresist] >= 35) || ([coldresist]+[fireresist] >= 40) || ([coldresist]+[lightresist] >= 40) || ([fireresist]+[lightresist] >= 40)) && [itemreplenishdurability] >= 1
|
||||
[type] == gloves && [quality] == rare && [flag] == ethereal # [ias] == 20 && [dexterity]+[strength] >= 20 && ([bowandcrossbowskilltab] == 2 || [javelinandspearskilltab] == 2) && [itemreplenishdurability] >= 1
|
||||
|
||||
// rare boots
|
||||
[type] == boots && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && ([enhanceddefense] >= 200 || [fhr] >= 10 || [dexterity] >= 7 || [hpregen] >= 5 || [frw] >= 30 || ([coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 60))
|
||||
[type] == boots && [quality] == rare && [flag] != ethereal # [coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 120
|
||||
[type] == boots && [quality] == rare && [flag] != ethereal # [coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 105 && ([fhr] >= 10 || [frw] >= 30 || [dexterity] >= 7 || [hpregen] >= 5)
|
||||
//[type] == boots && [quality] == rare && [flag] != ethereal # [dexterity] >= 7 && ([frw] >= 30 || [hpregen] >= 5) && (([coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 90) || ([coldresist]+[fireresist] >= 79) || ([coldresist]+[lightresist] >= 79) || ([lightresist]+[fireresist] >= 79))
|
||||
//[type] == boots && [quality] == rare && [flag] != ethereal # [frw] >= 30 && ([fhr] >= 10 || [hpregen] >= 5) && (([coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 90) || ([coldresist]+[fireresist] >= 79) || ([coldresist]+[lightresist] >= 79) || ([lightresist]+[fireresist] >= 79))
|
||||
//[type] == boots && [quality] == rare && [flag] != ethereal # [fhr] >= 10 && ([dexterity] >= 7 || [hpregen] >= 5) && (([coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 90) || ([coldresist]+[fireresist] >= 79) || ([coldresist]+[lightresist] >= 79) || ([lightresist]+[fireresist] >= 79))
|
||||
[type] == boots && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && [coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 120
|
||||
[type] == boots && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && [coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 105 && ([fhr] >= 10 || [frw] >= 30 || [dexterity] >= 7 || [hpregen] >= 5)
|
||||
//[type] == boots && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && [dexterity] >= 7 && ([frw] >= 30 || [hpregen] >= 5) && (([coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 90) || ([coldresist]+[fireresist] >= 79) || ([coldresist]+[lightresist] >= 79) || ([lightresist]+[fireresist] >= 79))
|
||||
//[type] == boots && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && [frw] >= 30 && ([fhr] >= 10 || [hpregen] >= 5) && (([coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 90) || ([coldresist]+[fireresist] >= 79) || ([coldresist]+[lightresist] >= 79) || ([lightresist]+[fireresist] >= 79))
|
||||
//[type] == boots && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && [fhr] >= 10 && ([dexterity] >= 7 || [hpregen] >= 5) && (([coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 90) || ([coldresist]+[fireresist] >= 79) || ([coldresist]+[lightresist] >= 79) || ([lightresist]+[fireresist] >= 79))
|
||||
|
||||
// rare belts
|
||||
[type] == belt && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && [fhr] >= 1 && [coldresist]+[fireresist]+[lightresist] >= 25
|
||||
[type] == belt && [quality] == rare && [flag] != ethereal # [fhr] == 24 && [strength] >= 25 && [maxhp] >= 45 && [coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 22
|
||||
[type] == belt && [quality] == rare && [flag] != ethereal # [fhr] == 24 && [strength] >= 30 && [maxhp] >= 60
|
||||
[type] == belt && [quality] == rare && [flag] == ethereal # [fhr] == 24 && [strength] >= 25 && [maxhp] >= 45 && [coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 22 && [itemreplenishdurability] >= 1
|
||||
[type] == belt && [quality] == rare && [flag] == ethereal # [fhr] == 24 && [strength] >= 29 && [maxhp] >= 55 && [itemreplenishdurability] >= 1
|
||||
|
||||
// scepters
|
||||
[type] == scepter && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >= 1 && [skillfistoftheheavens] + [paladinskills] + [palicombatskilltab] + [sockets] + [skillblessedhammer] + [skillconcentration]+[skillholyshield] >= 5
|
||||
[type] == scepter && [quality] == rare && [flag] != ethereal # [skillfistoftheheavens] + [paladinskills] + [palicombatskilltab] + [sockets] + [skillblessedhammer] + [skillconcentration]+[skillholyshield] >= 5
|
||||
|
||||
// polearms
|
||||
[type] == polearm && [quality] == rare && [flag] == ethereal # [itemtohitperlevel] >= 1
|
||||
@@ -1,175 +0,0 @@
|
||||
package nip
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
errEmptyLine = errors.New("empty line")
|
||||
operandRegex = regexp.MustCompile(`(\|{2}|\&{2})`)
|
||||
comparableRegex = regexp.MustCompile(`(\={2}|\<\=|\>\=|\>|\<|\!\=)`)
|
||||
propertyNameRegex = regexp.MustCompile(`\[(.*)\]`)
|
||||
)
|
||||
|
||||
func ParseLine(line string, filename string, linenum int) (Rule, error) {
|
||||
line = lineCleanup(line)
|
||||
if line == "" {
|
||||
return Rule{}, errEmptyLine
|
||||
}
|
||||
|
||||
lineSplit := strings.Split(line, "#")
|
||||
properties, err := buildGroups(lineSplit[0])
|
||||
if err != nil {
|
||||
return Rule{}, err
|
||||
}
|
||||
|
||||
var stats []Group
|
||||
if len(lineSplit) > 1 && strings.TrimSpace(lineSplit[1]) != "" {
|
||||
stats, err = buildGroups(lineSplit[1])
|
||||
if err != nil {
|
||||
return Rule{}, err
|
||||
}
|
||||
}
|
||||
|
||||
var maxQuantity []Group
|
||||
if len(lineSplit) > 2 && strings.TrimSpace(lineSplit[2]) != "" {
|
||||
maxQuantity, err = buildGroups(lineSplit[2])
|
||||
if err != nil {
|
||||
return Rule{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return Rule{
|
||||
Properties: properties,
|
||||
Stats: stats,
|
||||
MaxQuantity: maxQuantity,
|
||||
Filename: filename,
|
||||
LineNumber: linenum,
|
||||
Enabled: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func lineCleanup(line string) string {
|
||||
l := strings.Split(line, "//")
|
||||
line = strings.TrimSpace(l[0])
|
||||
|
||||
return strings.ToLower(line)
|
||||
}
|
||||
|
||||
func buildGroups(block string) ([]Group, error) {
|
||||
operands := operandRegex.FindAllString(block, -1)
|
||||
groupLines := operandRegex.Split(block, -1)
|
||||
|
||||
operands, groupLines = reGroupParentheses(operands, groupLines)
|
||||
|
||||
if len(operands) != len(groupLines)-1 {
|
||||
return nil, errors.New("syntax error")
|
||||
}
|
||||
|
||||
groups := make([]Group, 0)
|
||||
for i, groupLine := range groupLines {
|
||||
cmp, err := buildGroupComparables(groupLine)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
group := Group{Comparable: cmp}
|
||||
if i < len(operands) {
|
||||
group.Operand = Operand(operands[i])
|
||||
}
|
||||
|
||||
groups = append(groups, group)
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func reGroupParentheses(operands, groups []string) ([]string, []string) {
|
||||
cleanOperands := make([]string, 0)
|
||||
cleanGroups := make([]string, 0)
|
||||
|
||||
inGroup := false
|
||||
currentGroupContent := ""
|
||||
for i, group := range groups {
|
||||
group = strings.TrimSpace(group)
|
||||
if strings.Contains(group, "(") && strings.Contains(group, ")") {
|
||||
group = strings.ReplaceAll(group, "(", "")
|
||||
group = strings.ReplaceAll(group, ")", "")
|
||||
cleanGroups = append(cleanGroups, group)
|
||||
if i < len(operands) {
|
||||
cleanOperands = append(cleanOperands, operands[i])
|
||||
}
|
||||
continue
|
||||
}
|
||||
if strings.Contains(group, "(") || (inGroup && !strings.Contains(group, ")")) {
|
||||
inGroup = true
|
||||
currentGroupContent += strings.ReplaceAll(group, "(", "") + operands[i]
|
||||
continue
|
||||
}
|
||||
if strings.Contains(group, ")") {
|
||||
inGroup = false
|
||||
currentGroupContent += strings.ReplaceAll(group, ")", "")
|
||||
cleanGroups = append(cleanGroups, currentGroupContent)
|
||||
currentGroupContent = ""
|
||||
if i < len(operands) {
|
||||
cleanOperands = append(cleanOperands, operands[i])
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !inGroup {
|
||||
cleanGroups = append(cleanGroups, group)
|
||||
if i < len(operands) {
|
||||
cleanOperands = append(cleanOperands, operands[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cleanOperands, cleanGroups
|
||||
}
|
||||
|
||||
func buildGroupComparables(group string) ([]Comparable, error) {
|
||||
operands := operandRegex.FindAllString(group, -1)
|
||||
properties := operandRegex.Split(group, -1)
|
||||
if len(properties)-1 != len(operands) {
|
||||
return nil, errors.New("invalid NIP block")
|
||||
}
|
||||
|
||||
comparables := make([]Comparable, 0)
|
||||
for i, prop := range properties {
|
||||
comparisonSymbol := comparableRegex.FindString(prop)
|
||||
values := comparableRegex.Split(prop, -1)
|
||||
if len(values) != 2 {
|
||||
return nil, errors.New("invalid NIP line")
|
||||
}
|
||||
|
||||
aggregatedProperties := strings.Split(values[0], "+")
|
||||
for propNum, prop := range aggregatedProperties {
|
||||
propertyNameGr := propertyNameRegex.FindStringSubmatch(prop)
|
||||
propertyName := strings.TrimSpace(propertyNameGr[1])
|
||||
|
||||
cmp := Comparable{
|
||||
Keyword: propertyName,
|
||||
Comparison: Operand(strings.TrimSpace(comparisonSymbol)),
|
||||
}
|
||||
if len(aggregatedProperties) > 0 && propNum < len(aggregatedProperties)-1 {
|
||||
cmp.Operand = OperandOr
|
||||
} else if i < len(operands) {
|
||||
cmp.Operand = Operand(operands[i])
|
||||
}
|
||||
|
||||
value := strings.TrimSpace(values[1])
|
||||
if val, err := strconv.Atoi(value); err == nil {
|
||||
cmp.ValueInt = val
|
||||
} else {
|
||||
cmp.ValueString = value
|
||||
}
|
||||
|
||||
comparables = append(comparables, cmp)
|
||||
}
|
||||
}
|
||||
|
||||
return comparables, nil
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
package nip
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const nipLine = "[type] == boots && [quality] == rare # [frw] >= 10 && [fireresist] >= 10 && ([lightresist]+[coldresist] >= 10 && [dexterity] >= 1 && [fireresist]+[poisonresist] >= 10) // this is a comment"
|
||||
|
||||
func Test_parseLine(t *testing.T) {
|
||||
rules, err := ParseLine(nipLine, "dummy", 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := Rule{
|
||||
Properties: []Group{
|
||||
{
|
||||
Comparable: []Comparable{
|
||||
{
|
||||
Keyword: "type",
|
||||
Comparison: OperandEqual,
|
||||
ValueString: "boots",
|
||||
},
|
||||
},
|
||||
Operand: OperandAnd,
|
||||
},
|
||||
{
|
||||
Comparable: []Comparable{
|
||||
{
|
||||
Keyword: "quality",
|
||||
Comparison: OperandEqual,
|
||||
ValueString: "rare",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Stats: []Group{
|
||||
{
|
||||
Comparable: []Comparable{
|
||||
{
|
||||
Keyword: "frw",
|
||||
Comparison: OperandGreaterOrEqualTo,
|
||||
ValueInt: 10,
|
||||
},
|
||||
},
|
||||
Operand: OperandAnd,
|
||||
},
|
||||
{
|
||||
Comparable: []Comparable{
|
||||
{
|
||||
Keyword: "fireresist",
|
||||
Comparison: OperandGreaterOrEqualTo,
|
||||
ValueInt: 10,
|
||||
},
|
||||
},
|
||||
Operand: OperandAnd,
|
||||
},
|
||||
{
|
||||
Comparable: []Comparable{
|
||||
{
|
||||
Keyword: "lightresist",
|
||||
Comparison: OperandGreaterOrEqualTo,
|
||||
ValueInt: 10,
|
||||
Operand: OperandOr,
|
||||
},
|
||||
{
|
||||
Keyword: "coldresist",
|
||||
Comparison: OperandGreaterOrEqualTo,
|
||||
ValueInt: 10,
|
||||
Operand: OperandAnd,
|
||||
},
|
||||
{
|
||||
Keyword: "dexterity",
|
||||
Comparison: OperandGreaterOrEqualTo,
|
||||
ValueInt: 1,
|
||||
Operand: OperandAnd,
|
||||
},
|
||||
{
|
||||
Keyword: "fireresist",
|
||||
Comparison: OperandGreaterOrEqualTo,
|
||||
ValueInt: 10,
|
||||
Operand: OperandOr,
|
||||
},
|
||||
{
|
||||
Keyword: "poisonresist",
|
||||
Comparison: OperandGreaterOrEqualTo,
|
||||
ValueInt: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, rules)
|
||||
}
|
||||
@@ -1,28 +1,14 @@
|
||||
package nip
|
||||
|
||||
const (
|
||||
// Properties
|
||||
PropertyType = "type"
|
||||
PropertyName = "name"
|
||||
PropertyClass = "class"
|
||||
PropertyQuality = "quality"
|
||||
PropertyFlag = "flag"
|
||||
PropertyLevel = "level"
|
||||
PropertyPrefix = "prefix"
|
||||
PropertySuffix = "suffix"
|
||||
import "github.com/hectorgimenez/d2go/pkg/data/item"
|
||||
|
||||
// Types
|
||||
TypeNormal = "normal"
|
||||
TypeExceptional = "exceptional"
|
||||
TypeElite = "elite"
|
||||
|
||||
// Quality
|
||||
QualityLowQuality = "lowquality"
|
||||
QualityNormal = "normal"
|
||||
QualitySuperior = "superior"
|
||||
QualityMagic = "magic"
|
||||
QualitySet = "set"
|
||||
QualityRare = "rare"
|
||||
QualityUnique = "unique"
|
||||
QualityCrafted = "crafted"
|
||||
)
|
||||
var quality = map[string]item.Quality{
|
||||
"LowQuality": item.QualityLowQuality,
|
||||
"Normal": item.QualityNormal,
|
||||
"Superior": item.QualitySuperior,
|
||||
"Magic": item.QualityMagic,
|
||||
"Set": item.QualitySet,
|
||||
"Rare": item.QualityRare,
|
||||
"Unique": item.QualityUnique,
|
||||
"Crafted": item.QualityCrafted,
|
||||
}
|
||||
|
||||
233
pkg/nip/rule.go
233
pkg/nip/rule.go
@@ -1,38 +1,223 @@
|
||||
package nip
|
||||
|
||||
const (
|
||||
OperandEqual Operand = "=="
|
||||
OperandGreaterThan Operand = ">"
|
||||
OperandGreaterOrEqualTo Operand = ">="
|
||||
OperandLessThan Operand = "<"
|
||||
OperandLessThanOrEqualTo Operand = "<="
|
||||
OperandNotEqualTo Operand = "!="
|
||||
OperandAnd Operand = "&&"
|
||||
OperandOr Operand = "||"
|
||||
OperandNone Operand = ""
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/expr-lang/expr"
|
||||
"github.com/hectorgimenez/d2go/pkg/data"
|
||||
"github.com/hectorgimenez/d2go/pkg/data/item"
|
||||
"github.com/hectorgimenez/d2go/pkg/data/stat"
|
||||
)
|
||||
|
||||
var (
|
||||
fixedPropsRegexp = regexp.MustCompile(`(\[(type|quality|class|name|flag|color)]\s*(<=|<|>|>=|!=|==)\s*([a-zA-Z]+))`)
|
||||
statsRegexp = regexp.MustCompile(`\[(.*?)]`)
|
||||
maxQtyRegexp = regexp.MustCompile(`(\[maxquantity]\s*(<=|<|>|>=|!=|==)\s*([0-9]+))`)
|
||||
)
|
||||
|
||||
type Rule struct {
|
||||
Properties []Group
|
||||
Stats []Group
|
||||
MaxQuantity []Group
|
||||
RawLine string // Original line, don't use it for evaluation
|
||||
Stages [3]string
|
||||
Filename string
|
||||
LineNumber int
|
||||
Enabled bool
|
||||
maxQuantity int
|
||||
}
|
||||
|
||||
type Keyword string
|
||||
type Operand string
|
||||
type Rules []Rule
|
||||
|
||||
type Comparable struct {
|
||||
Keyword string
|
||||
Comparison Operand
|
||||
ValueInt int
|
||||
ValueString string
|
||||
Operand
|
||||
type RuleResult int
|
||||
|
||||
var (
|
||||
RuleResultFullMatch RuleResult = 1
|
||||
RuleResultPartial RuleResult = 2
|
||||
RuleResultNoMatch RuleResult = 3
|
||||
)
|
||||
|
||||
func (r Rules) EvaluateAll(it data.Item) (Rule, RuleResult) {
|
||||
bestMatch := RuleResultNoMatch
|
||||
bestMatchingRule := Rule{}
|
||||
for _, rule := range r {
|
||||
if rule.Enabled {
|
||||
result, err := rule.Evaluate(it)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if result == RuleResultFullMatch {
|
||||
return rule, result
|
||||
}
|
||||
if result == RuleResultPartial {
|
||||
bestMatch = result
|
||||
bestMatchingRule = rule
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestMatchingRule, bestMatch
|
||||
}
|
||||
|
||||
type Group struct {
|
||||
Comparable []Comparable
|
||||
Operand
|
||||
func New(rawRule string, filename string, lineNumber int) (Rule, error) {
|
||||
rule := sanitizeLine(rawRule)
|
||||
|
||||
// Check for not supported stats
|
||||
for _, prop := range statsRegexp.FindAllStringSubmatch(rule, -1) {
|
||||
if slices.Contains(notSupportedStats, prop[1]) {
|
||||
return Rule{}, fmt.Errorf("property %s is not supported, please remove it", prop[1])
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get the maxquantity value and purge it from the rule, we can not evaluate it
|
||||
maxQuantity := 0
|
||||
for _, prop := range maxQtyRegexp.FindAllStringSubmatch(rule, -1) {
|
||||
mxQty, err := strconv.Atoi(prop[3])
|
||||
if err != nil {
|
||||
return Rule{}, fmt.Errorf("error parsing maxquantity value %s: %w", prop[3], err)
|
||||
}
|
||||
maxQuantity = mxQty
|
||||
rule = strings.ReplaceAll(rule, prop[0], "")
|
||||
}
|
||||
|
||||
// Sanitize again, just in case we messed up the rule while parsing maxquantity
|
||||
rule = sanitizeLine(rule)
|
||||
if rule == "" {
|
||||
return Rule{}, ErrEmptyRule
|
||||
}
|
||||
|
||||
stages := [3]string{}
|
||||
for i, stg := range strings.Split(rule, "#") {
|
||||
stages[i] = strings.TrimSpace(stg)
|
||||
}
|
||||
|
||||
return Rule{
|
||||
RawLine: rawRule,
|
||||
Stages: stages,
|
||||
Filename: filename,
|
||||
LineNumber: lineNumber,
|
||||
Enabled: true,
|
||||
maxQuantity: maxQuantity,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r Rule) Evaluate(it data.Item) (RuleResult, error) {
|
||||
stage1 := r.Stages[0]
|
||||
baseProperties := fixedPropsRegexp.FindAllStringSubmatch(stage1, -1)
|
||||
for _, prop := range baseProperties {
|
||||
switch prop[2] {
|
||||
case "type":
|
||||
partialReplace := strings.ReplaceAll(prop[0], fmt.Sprintf("[%s]", prop[2]), fmt.Sprintf("%d", typeAliases[it.TypeAsString()]))
|
||||
partialReplace = strings.ReplaceAll(partialReplace, prop[4], fmt.Sprintf("%d", typeAliases[prop[4]]))
|
||||
stage1 = strings.ReplaceAll(stage1, prop[0], partialReplace)
|
||||
case "quality":
|
||||
partialReplace := strings.ReplaceAll(prop[0], fmt.Sprintf("[%s]", prop[2]), fmt.Sprintf("%d", it.Quality))
|
||||
partialReplace = strings.ReplaceAll(partialReplace, prop[4], fmt.Sprintf("%d", qualityAliases[prop[4]]))
|
||||
stage1 = strings.ReplaceAll(stage1, prop[0], partialReplace)
|
||||
case "class":
|
||||
partialReplace := strings.ReplaceAll(prop[0], fmt.Sprintf("[%s]", prop[2]), fmt.Sprintf("%d", it.Desc().Tier()))
|
||||
partialReplace = strings.ReplaceAll(partialReplace, prop[4], fmt.Sprintf("%d", classAliases[prop[4]]))
|
||||
stage1 = strings.ReplaceAll(stage1, prop[0], partialReplace)
|
||||
case "name":
|
||||
partialReplace := strings.ReplaceAll(prop[0], fmt.Sprintf("[%s]", prop[2]), fmt.Sprintf("%d", item.GetIDByName(string(it.Name))))
|
||||
partialReplace = strings.ReplaceAll(partialReplace, prop[4], fmt.Sprintf("%d", item.GetIDByName(prop[4])))
|
||||
stage1 = strings.ReplaceAll(stage1, prop[0], partialReplace)
|
||||
case "flag":
|
||||
partialReplace := strings.ReplaceAll(prop[0], fmt.Sprintf("[%s]", prop[2]), fmt.Sprintf("%d", map[bool]int{true: 1, false: 0}[it.Ethereal]))
|
||||
partialReplace = strings.ReplaceAll(partialReplace, prop[4], fmt.Sprintf("%d", 1))
|
||||
stage1 = strings.ReplaceAll(stage1, prop[0], partialReplace)
|
||||
case "color":
|
||||
// TODO: Not supported yet
|
||||
}
|
||||
}
|
||||
|
||||
// Let's evaluate first stage
|
||||
stage1Result, err := expr.Eval(stage1, nil)
|
||||
if err != nil {
|
||||
return RuleResultNoMatch, fmt.Errorf("error evaluating rule: %w", err)
|
||||
}
|
||||
|
||||
// Let's go with other stats now
|
||||
// TODO: properties are missing (enhanceddefense, enhanceddamage, etc)
|
||||
stage2 := r.Stages[1]
|
||||
stage2Result := true
|
||||
if stage2 != "" {
|
||||
for _, statName := range statsRegexp.FindAllStringSubmatch(stage2, -1) {
|
||||
statData, found := statAliases[statName[1]]
|
||||
if !found {
|
||||
return RuleResultNoMatch, fmt.Errorf("property %s is not valid or not supported", statName[1])
|
||||
}
|
||||
|
||||
// Exception for enhanceddefense, is not accurate
|
||||
if statName[0] == "[enhanceddefense]" {
|
||||
enhancedDefense := r.calculateEnhancedDefense(it)
|
||||
stage2 = strings.ReplaceAll(stage2, statName[0], fmt.Sprintf("%d", enhancedDefense))
|
||||
continue
|
||||
}
|
||||
|
||||
layer := 0
|
||||
if len(statData) > 1 {
|
||||
layer = statData[1]
|
||||
}
|
||||
itemStat, _ := it.FindStat(stat.ID(statData[0]), layer)
|
||||
// By default, value will be 0 is stat is not found, it's okay for evaluation purposes.
|
||||
stage2 = strings.ReplaceAll(stage2, statName[0], fmt.Sprintf("%d", itemStat.Value))
|
||||
}
|
||||
|
||||
res, err := expr.Eval(stage2, nil)
|
||||
if err != nil {
|
||||
return RuleResultNoMatch, fmt.Errorf("error evaluating rule: %w", err)
|
||||
}
|
||||
stage2Result = res.(bool)
|
||||
}
|
||||
|
||||
// 100% rule match, we can return here
|
||||
if stage1Result.(bool) && stage2Result {
|
||||
return RuleResultFullMatch, nil
|
||||
}
|
||||
|
||||
// If Stage 1 matches and the item is NOT identified, let's return a partial match, we can not be 100% sure if all the stats are matching
|
||||
if stage1Result.(bool) && !it.Identified {
|
||||
return RuleResultPartial, nil
|
||||
}
|
||||
|
||||
return RuleResultNoMatch, nil
|
||||
}
|
||||
|
||||
// MaxQuantity returns the maximum quantity of items that character can have, 0 means no limit
|
||||
func (r Rule) MaxQuantity() int {
|
||||
return r.maxQuantity
|
||||
}
|
||||
|
||||
func (r Rule) calculateEnhancedDefense(i data.Item) int {
|
||||
defenseStat, found := i.FindStat(stat.Defense, 0)
|
||||
|
||||
// This is special case for weapons and other stuff without base Defense and with EnhancedDefense, we can return it directly
|
||||
// since EnhancedDefense it's a stat.
|
||||
if !found {
|
||||
if edStat, edStatFound := i.FindStat(stat.EnhancedDefense, 0); edStatFound {
|
||||
return edStat.Value
|
||||
}
|
||||
}
|
||||
|
||||
// Make it only work for white base items
|
||||
if i.Quality != item.QualitySuperior {
|
||||
return 0
|
||||
}
|
||||
|
||||
if i.TypeAsString() != "armor" {
|
||||
return 0
|
||||
}
|
||||
|
||||
itemTypeMaxDefense := i.Desc().MaxDefense
|
||||
|
||||
if defenseStat.Value <= itemTypeMaxDefense {
|
||||
return 0
|
||||
}
|
||||
|
||||
res := float64(defenseStat.Value-itemTypeMaxDefense) / float64(itemTypeMaxDefense) * 100
|
||||
|
||||
return int(math.Ceil(res))
|
||||
}
|
||||
|
||||
213
pkg/nip/rule_test.go
Normal file
213
pkg/nip/rule_test.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package nip
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hectorgimenez/d2go/pkg/data"
|
||||
"github.com/hectorgimenez/d2go/pkg/data/item"
|
||||
"github.com/hectorgimenez/d2go/pkg/data/stat"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRule_Evaluate(t *testing.T) {
|
||||
type fields struct {
|
||||
RawLine string
|
||||
Filename string
|
||||
LineNumber int
|
||||
Enabled bool
|
||||
}
|
||||
type args struct {
|
||||
item data.Item
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want RuleResult
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Basic rule with posion dmg, ethereal is not specified as a condition so it should be ignored",
|
||||
fields: fields{
|
||||
RawLine: "[name] == smallcharm && [quality] == magic # (([poisonlength]*25)*[poisonmaxdam])/256 >= 123",
|
||||
Filename: "test.nip",
|
||||
LineNumber: 1,
|
||||
Enabled: true,
|
||||
},
|
||||
args: args{
|
||||
item: data.Item{
|
||||
Name: "SmAlLCharM",
|
||||
Quality: item.QualityMagic,
|
||||
Ethereal: true,
|
||||
Stats: []stat.Data{
|
||||
{ID: stat.PoisonLength, Value: 20},
|
||||
{ID: stat.PoisonMaxDamage, Value: 100},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: RuleResultFullMatch,
|
||||
},
|
||||
{
|
||||
name: "Complex rule with flags",
|
||||
fields: fields{
|
||||
RawLine: "[type] == armor && [quality] <= superior && [flag] != ethereal # ([itemmaxdurabilitypercent] == 0 || [itemmaxdurabilitypercent] == 15) && ([sockets] == 0 || [sockets] == 3 || [sockets] == 4)",
|
||||
Filename: "test.nip",
|
||||
LineNumber: 1,
|
||||
Enabled: true,
|
||||
},
|
||||
args: args{
|
||||
item: data.Item{
|
||||
Name: "mageplate",
|
||||
Quality: item.QualitySuperior,
|
||||
Ethereal: false,
|
||||
Stats: []stat.Data{
|
||||
{ID: stat.MaxDurabilityPercent, Value: 15},
|
||||
{ID: stat.NumSockets, Value: 4},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: RuleResultFullMatch,
|
||||
},
|
||||
{
|
||||
name: "Armor with +3 Sorc skills",
|
||||
fields: fields{
|
||||
RawLine: "[type] == armor # [sorceressskills] >= 3",
|
||||
Enabled: true,
|
||||
},
|
||||
args: args{
|
||||
item: data.Item{
|
||||
Name: "mageplate",
|
||||
Stats: []stat.Data{
|
||||
{ID: stat.AddClassSkills, Value: 3, Layer: 1},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: RuleResultFullMatch,
|
||||
},
|
||||
{
|
||||
name: "Armor with +3 Glacial Spike",
|
||||
fields: fields{
|
||||
RawLine: "[type] == armor # [skillglacialspike] >= 3",
|
||||
Enabled: true,
|
||||
},
|
||||
args: args{
|
||||
item: data.Item{
|
||||
Name: "mageplate",
|
||||
Stats: []stat.Data{
|
||||
{ID: stat.SingleSkill, Value: 3, Layer: 55},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: RuleResultFullMatch,
|
||||
},
|
||||
{
|
||||
name: "Ensure [color] returns error, not supported yet",
|
||||
fields: fields{
|
||||
RawLine: "[type] == armor && [color] == 1000 && [quality] == magic",
|
||||
Enabled: true,
|
||||
},
|
||||
args: args{
|
||||
item: data.Item{
|
||||
Name: "mageplate",
|
||||
Quality: item.QualityMagic,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Unid item matching base stats should return partial match",
|
||||
fields: fields{
|
||||
RawLine: "[type] == armor && [quality] == magic # [defense] == 200",
|
||||
Enabled: true,
|
||||
},
|
||||
args: args{
|
||||
item: data.Item{
|
||||
Identified: false,
|
||||
Name: "mageplate",
|
||||
Quality: item.QualityMagic,
|
||||
},
|
||||
},
|
||||
want: RuleResultPartial,
|
||||
},
|
||||
{
|
||||
name: "Basic rule without stats or maxquantity",
|
||||
fields: fields{
|
||||
RawLine: "[type] == armor && [quality] == magic # #",
|
||||
Enabled: true,
|
||||
},
|
||||
args: args{
|
||||
item: data.Item{
|
||||
Identified: false,
|
||||
Name: "mageplate",
|
||||
Quality: item.QualityMagic,
|
||||
},
|
||||
},
|
||||
want: RuleResultFullMatch,
|
||||
},
|
||||
{
|
||||
name: "Basic rule for a white superior item with enhanceddefense",
|
||||
fields: fields{
|
||||
RawLine: "[type] == armor && [quality] == superior # [enhanceddefense] >= 15 #",
|
||||
Enabled: true,
|
||||
},
|
||||
args: args{
|
||||
item: data.Item{
|
||||
Identified: true,
|
||||
ID: 373,
|
||||
Name: "mageplate",
|
||||
Quality: item.QualitySuperior,
|
||||
Stats: []stat.Data{
|
||||
{ID: stat.Defense, Value: 301},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: RuleResultFullMatch,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r, err := New(tt.fields.RawLine, tt.fields.Filename, tt.fields.LineNumber)
|
||||
require.NoError(t, err)
|
||||
got, err := r.Evaluate(tt.args.item)
|
||||
if !tt.wantErr {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.want, got)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
type args struct {
|
||||
rawRule string
|
||||
filename string
|
||||
lineNumber int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want Rule
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Enhanced Damage should throw an error",
|
||||
args: args{
|
||||
rawRule: "[type] == armor # [enhanceddamage] >= 15",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := New(tt.args.rawRule, tt.args.filename, tt.args.lineNumber)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user