1154 lines
28 KiB
Go
1154 lines
28 KiB
Go
package nip
|
|
|
|
import (
|
|
"sort"
|
|
"strconv"
|
|
"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{
|
|
ID: 618,
|
|
Name: "SmAlLCharM",
|
|
Quality: item.QualityMagic,
|
|
Identified: true,
|
|
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{
|
|
ID: 373,
|
|
Name: "mageplate",
|
|
Quality: item.QualitySuperior,
|
|
Identified: true,
|
|
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{
|
|
ID: 373,
|
|
Name: "mageplate",
|
|
Identified: true,
|
|
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{
|
|
ID: 373,
|
|
Name: "mageplate",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SingleSkill, Value: 3, Layer: 55},
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
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{
|
|
ID: 373,
|
|
Identified: false,
|
|
Name: "mageplate",
|
|
Quality: item.QualityMagic,
|
|
},
|
|
},
|
|
want: RuleResultPartial,
|
|
},
|
|
{
|
|
name: "Basic rule without stats or maxquantity",
|
|
fields: fields{
|
|
RawLine: "[type] == assassinclaw && [class] == exceptional && [quality] == magic # #",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 187,
|
|
Identified: false,
|
|
Name: "GreaterTalons",
|
|
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.EnhancedDefense, Value: 15},
|
|
{ID: stat.Defense, Value: 301},
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "Basic rule for a white superior item with enhanceddamage",
|
|
fields: fields{
|
|
RawLine: "[type] == sword # [enhanceddamage] >= 15 #",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
Identified: true,
|
|
ID: 234,
|
|
Name: "colossusblade",
|
|
Quality: item.QualitySuperior,
|
|
Stats: []stat.Data{
|
|
{ID: stat.EnhancedDamage, Value: 15},
|
|
{ID: stat.MinDamage, Value: 28},
|
|
{ID: stat.MaxDamage, Value: 74},
|
|
{ID: stat.TwoHandedMinDamage, Value: 66},
|
|
{ID: stat.TwoHandedMaxDamage, Value: 132},
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "Magic Giant Sword with +3 Warcries and 6% mana leech",
|
|
fields: fields{
|
|
RawLine: "[name] == giantsword && [quality] == magic # [warcriesskilltab] >= 3 && ([coldmindam] >= 4 || [firemindam] >= 9 || [lightmaxdam] >= 40 || [poisonmindam] >= 10 || [poisondamage] >= 10 || [maxdamage] >= 10 || [mindamage] >= 5 || [strength] >= 15 || [hpregen] >= 5 || [maxhp] >= 30 || [itemknockback] >= 1 || [lifeleech] >= 6 || [manaleech] >= 6 || [itemreqpercent] == -30 || [itemskillonattack] >= 1 || [itemchargedskill] >= 1)",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
Identified: true,
|
|
ID: 35,
|
|
Name: "GiantSword",
|
|
Quality: item.QualityMagic,
|
|
Stats: []stat.Data{
|
|
{ID: stat.AddSkillTab, Value: 3, Layer: 34},
|
|
{ID: stat.ManaSteal, Value: 6},
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "Ensure [itemskillonstriking] returns error, not supported yet",
|
|
fields: fields{RawLine: "[quality] == magic # [itemskillonstriking] >= 1", Enabled: true},
|
|
args: args{item: data.Item{Identified: true, ID: 516, Name: "healingpotion", Quality: item.QualityMagic}},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "Magic Flail with itemskillonhit",
|
|
fields: fields{
|
|
RawLine: "[name] == flail && [quality] == magic && [flag] != ethereal # [itemskillonhit] == 48",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
Identified: true,
|
|
ID: 21,
|
|
Name: "Flail",
|
|
Quality: item.QualityMagic,
|
|
Ethereal: false,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnHit, Value: 10, Layer: 3075},
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "Magic Giant Sword with itemskillonhit",
|
|
fields: fields{
|
|
RawLine: "[name] == giantsword && [quality] == magic # [itemskillonhit] >= 1",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
Identified: true,
|
|
ID: 35,
|
|
Name: "GiantSword",
|
|
Quality: item.QualityMagic,
|
|
Ethereal: false,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnHit, Value: 10, Layer: 3075},
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "Magic Flail with itemskillonhitlevel",
|
|
fields: fields{
|
|
RawLine: "[name] == flail && [quality] == magic && [flag] != ethereal # [itemskillonhitlevel] >= 3",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
Identified: true,
|
|
ID: 21,
|
|
Name: "Flail",
|
|
Quality: item.QualityMagic,
|
|
Ethereal: false,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnHit, Value: 10, Layer: 3075},
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "SkillOnAttack: Fire Ball (47) Level 5 (match)",
|
|
fields: fields{
|
|
RawLine: "[name] == flail # [itemskillonattack] == 47 && [itemskillonattacklevel] == 5",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 21,
|
|
Name: "Flail",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnAttack, Value: 10, Layer: 3013}, // (47 << 6) | 5
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "SkillOnAttack: Fire Ball (47) Level 5 (match distinct level condition)",
|
|
fields: fields{
|
|
RawLine: "[name] == flail # [itemskillonattacklevel] >= 4",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 21,
|
|
Name: "Flail",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnAttack, Value: 10, Layer: 3013}, // (47 << 6) | 5
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "SkillOnAttack: Fire Ball (47) Level 5 (match distinct OR condition)",
|
|
fields: fields{
|
|
RawLine: "[name] == flail # [itemskillonattack] == 47 || [itemskillonattack] == 50",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 21,
|
|
Name: "Flail",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnAttack, Value: 10, Layer: 3013}, // (47 << 6) | 5
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "SkillOnAttack: Fire Ball (47) Level 5 (no match)",
|
|
fields: fields{
|
|
RawLine: "[name] == flail # [itemskillonattack] == 48",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 21,
|
|
Name: "Flail",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnAttack, Value: 10, Layer: 3013},
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultNoMatch,
|
|
},
|
|
{
|
|
name: "SkillOnKill: Chain Lightning (53) Level 12 (match)",
|
|
fields: fields{
|
|
RawLine: "[name] == flail # [itemskillonkill] == 53 && [itemskillonkilllevel] == 12",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 21,
|
|
Name: "Flail",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnKill, Value: 10, Layer: 3404}, // (53 << 6) | 12
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "SkillOnKill: Chain Lightning (53) Level 12 (match distinct level condition)",
|
|
fields: fields{
|
|
RawLine: "[name] == flail # [itemskillonkilllevel] >= 10",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 21,
|
|
Name: "Flail",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnKill, Value: 10, Layer: 3404}, // (53 << 6) | 12
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "SkillOnKill: Chain Lightning (53) Level 12 (match distinct OR condition)",
|
|
fields: fields{
|
|
RawLine: "[name] == flail # [itemskillonkill] == 53 || [itemskillonkill] == 60",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 21,
|
|
Name: "Flail",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnKill, Value: 10, Layer: 3404}, // (53 << 6) | 12
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "SkillOnKill: Chain Lightning (53) Level 12 (no match)",
|
|
fields: fields{
|
|
RawLine: "[name] == flail # [itemskillonkill] == 54",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 21,
|
|
Name: "Flail",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnKill, Value: 10, Layer: 3404}, // (53 << 6) | 12
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultNoMatch,
|
|
},
|
|
{
|
|
name: "SkillOnDeath: Shiver Armor (50) Level 3 (match)",
|
|
fields: fields{
|
|
RawLine: "[name] == mageplate # [itemskillondeath] == 50 && [itemskillondeathlevel] == 3",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 373,
|
|
Name: "MagePlate",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnDeath, Value: 100, Layer: 3203}, // (50 << 6) | 3
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "SkillOnDeath: Shiver Armor (50) Level 3 (match distinct level condition)",
|
|
fields: fields{
|
|
RawLine: "[name] == mageplate # [itemskillondeathlevel] >= 2",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 373,
|
|
Name: "MagePlate",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnDeath, Value: 100, Layer: 3203}, // (50 << 6) | 3
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "SkillOnDeath: Shiver Armor (50) Level 3 (match distinct OR condition)",
|
|
fields: fields{
|
|
RawLine: "[name] == mageplate # [itemskillondeath] == 50 || [itemskillondeath] == 55",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 373,
|
|
Name: "MagePlate",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnDeath, Value: 100, Layer: 3203}, // (50 << 6) | 3
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "SkillOnDeath: Shiver Armor (50) Level 3 (no match)",
|
|
fields: fields{
|
|
RawLine: "[name] == mageplate # [itemskillondeath] == 51",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 373,
|
|
Name: "MagePlate",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnDeath, Value: 100, Layer: 3203}, // (50 << 6) | 3
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultNoMatch,
|
|
},
|
|
{
|
|
name: "SkillOnHit: Amplify Damage (66) Level 1 (match)",
|
|
fields: fields{
|
|
RawLine: "[name] == flail # [itemskillonhit] == 66 && [itemskillonhitlevel] == 1",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 21,
|
|
Name: "Flail",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnHit, Value: 5, Layer: 4225}, // (66 << 6) | 1
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "SkillOnHit: Amplify Damage (66) Level 1 (match distinct level condition)",
|
|
fields: fields{
|
|
RawLine: "[name] == flail # [itemskillonhitlevel] >= 1",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 21,
|
|
Name: "Flail",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnHit, Value: 5, Layer: 4225}, // (66 << 6) | 1
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "SkillOnHit: Amplify Damage (66) Level 1 (match distinct OR condition)",
|
|
fields: fields{
|
|
RawLine: "[name] == flail # [itemskillonhit] == 66 || [itemskillonhit] == 70",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 21,
|
|
Name: "Flail",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnHit, Value: 5, Layer: 4225}, // (66 << 6) | 1
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "SkillOnHit: Amplify Damage (66) Level 1 (no match)",
|
|
fields: fields{
|
|
RawLine: "[name] == flail # [itemskillonhit] == 67",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 21,
|
|
Name: "Flail",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnHit, Value: 5, Layer: 4225}, // (66 << 6) | 1
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultNoMatch,
|
|
},
|
|
{
|
|
name: "SkillOnLevelUp: Blizzard (59) Level 7 (match)",
|
|
fields: fields{
|
|
RawLine: "[name] == mageplate # [itemskillonlevelup] == 59 && [itemskillonleveluplevel] >= 5",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 373,
|
|
Name: "MagePlate",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnLevelUp, Value: 10, Layer: 3783}, // (59 << 6) | 7
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "SkillOnLevelUp: Blizzard (59) Level 7 (match distinct OR condition)",
|
|
fields: fields{
|
|
RawLine: "[name] == mageplate # [itemskillonlevelup] == 59 || [itemskillonlevelup] == 60",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 373,
|
|
Name: "MagePlate",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnLevelUp, Value: 10, Layer: 3783}, // (59 << 6) | 7
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "SkillOnLevelUp: Blizzard (59) Level 7 (no match)",
|
|
fields: fields{
|
|
RawLine: "[name] == mageplate # [itemskillonlevelup] == 60",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 373,
|
|
Name: "MagePlate",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnLevelUp, Value: 10, Layer: 3783}, // (59 << 6) | 7
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultNoMatch,
|
|
},
|
|
{
|
|
name: "SkillOnGetHit: Nova (48) Level 9 (match)",
|
|
fields: fields{
|
|
RawLine: "[name] == mageplate # [itemskillongethit] == 48 && [itemskillongethitlevel] == 9",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 373,
|
|
Name: "MagePlate",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnGetHit, Value: 10, Layer: 3081}, // (48 << 6) | 9
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "SkillOnGetHit: Nova (48) Level 9 (match distinct level condition)",
|
|
fields: fields{
|
|
RawLine: "[name] == mageplate # [itemskillongethitlevel] >= 5",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 373,
|
|
Name: "MagePlate",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnGetHit, Value: 10, Layer: 3081}, // (48 << 6) | 9
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "SkillOnGetHit: Nova (48) Level 9 (match distinct OR condition)",
|
|
fields: fields{
|
|
RawLine: "[name] == mageplate # [itemskillongethit] == 48 || [itemskillongethit] == 50",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 373,
|
|
Name: "MagePlate",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnGetHit, Value: 10, Layer: 3081}, // (48 << 6) | 9
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "SkillOnGetHit: Nova (48) Level 9 (no match)",
|
|
fields: fields{
|
|
RawLine: "[name] == mageplate # [itemskillongethit] == 49",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 373,
|
|
Name: "MagePlate",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.SkillOnGetHit, Value: 10, Layer: 3081}, // (48 << 6) | 9
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultNoMatch,
|
|
},
|
|
{
|
|
name: "ChargedSkill: Teleport (54) Level 1 (match)",
|
|
fields: fields{
|
|
RawLine: "[name] == flail # [itemchargedskill] == 54 && [itemchargedskilllevel] == 1",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 21,
|
|
Name: "Flail",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.ItemChargedSkill, Value: 20, Layer: 3457}, // (54 << 6) | 1
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "ChargedSkill: Teleport (54) Level 1 (match distinct level condition)",
|
|
fields: fields{
|
|
RawLine: "[name] == flail # [itemchargedskilllevel] >= 1",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 21,
|
|
Name: "Flail",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.ItemChargedSkill, Value: 20, Layer: 3457}, // (54 << 6) | 1
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "ChargedSkill: Teleport (54) Level 1 (match distinct OR condition)",
|
|
fields: fields{
|
|
RawLine: "[name] == flail # [itemchargedskill] == 54 || [itemchargedskill] == 60",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 21,
|
|
Name: "Flail",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.ItemChargedSkill, Value: 20, Layer: 3457}, // (54 << 6) | 1
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "ChargedSkill: Teleport (54) Level 1 (no match - different skill)",
|
|
fields: fields{
|
|
RawLine: "[name] == flail # [itemchargedskill] == 55",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 21,
|
|
Name: "Flail",
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: stat.ItemChargedSkill, Value: 20, Layer: 3457}, // (54 << 6) | 1
|
|
},
|
|
},
|
|
},
|
|
want: RuleResultNoMatch,
|
|
},
|
|
// Regression tests for the ethereal+runeword [flag] bitmask interaction.
|
|
// Previously "[flag] == ethereal" rewrote to a scalar compare and missed
|
|
// ethereal runewords (flag = eth | rw). The rewriter now enumerates both
|
|
// states the chosen bit can produce.
|
|
{
|
|
name: "Flag: == ethereal matches a plain ethereal item",
|
|
fields: fields{
|
|
RawLine: "[type] == armor && [flag] == ethereal",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 373,
|
|
Name: "mageplate",
|
|
Identified: true,
|
|
Ethereal: true,
|
|
IsRuneword: false,
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "Flag: == ethereal also matches an ethereal RUNEWORD (regression: was NoMatch)",
|
|
fields: fields{
|
|
RawLine: "[type] == armor && [flag] == ethereal",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 373,
|
|
Name: "mageplate",
|
|
Identified: true,
|
|
Ethereal: true,
|
|
IsRuneword: true,
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "Flag: != ethereal correctly excludes ethereal runeword",
|
|
fields: fields{
|
|
RawLine: "[type] == armor && [flag] != ethereal",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 373,
|
|
Name: "mageplate",
|
|
Identified: true,
|
|
Ethereal: true,
|
|
IsRuneword: true,
|
|
},
|
|
},
|
|
want: RuleResultNoMatch,
|
|
},
|
|
{
|
|
name: "Flag: != ethereal matches a non-ethereal runeword",
|
|
fields: fields{
|
|
RawLine: "[type] == armor && [flag] != ethereal",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 373,
|
|
Name: "mageplate",
|
|
Identified: true,
|
|
Ethereal: false,
|
|
IsRuneword: true,
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "Flag: == runeword matches an ethereal runeword",
|
|
fields: fields{
|
|
RawLine: "[type] == armor && [flag] == runeword",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 373,
|
|
Name: "mageplate",
|
|
Identified: true,
|
|
Ethereal: true,
|
|
IsRuneword: true,
|
|
},
|
|
},
|
|
want: RuleResultFullMatch,
|
|
},
|
|
{
|
|
name: "Flag: != runeword excludes ethereal runeword",
|
|
fields: fields{
|
|
RawLine: "[type] == armor && [flag] != runeword",
|
|
Enabled: true,
|
|
},
|
|
args: args{
|
|
item: data.Item{
|
|
ID: 373,
|
|
Name: "mageplate",
|
|
Identified: true,
|
|
Ethereal: true,
|
|
IsRuneword: true,
|
|
},
|
|
},
|
|
want: RuleResultNoMatch,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
r, err := NewRule(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: "Ensure [color] returns error, not supported yet",
|
|
args: args{
|
|
rawRule: "[type] == armor && [color] == 1000 && [quality] == magic",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := NewRule(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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAggregateSkillRulesIncludeHighestAliasLayers(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
rule string
|
|
stat stat.ID
|
|
layer int
|
|
}{
|
|
{
|
|
name: "class skills include warlock layer",
|
|
rule: "[quality] >= normal # [itemaddclassskills] >= 3",
|
|
stat: stat.AddClassSkills,
|
|
layer: 7,
|
|
},
|
|
{
|
|
name: "skill tabs include demon layer",
|
|
rule: "[quality] >= normal # [itemaddskilltab] >= 3",
|
|
stat: stat.AddSkillTab,
|
|
layer: 56,
|
|
},
|
|
{
|
|
name: "skill tabs include eldritch layer",
|
|
rule: "[quality] >= normal # [itemaddskilltab] >= 3",
|
|
stat: stat.AddSkillTab,
|
|
layer: 57,
|
|
},
|
|
{
|
|
name: "skill tabs include chaos layer",
|
|
rule: "[quality] >= normal # [itemaddskilltab] >= 3",
|
|
stat: stat.AddSkillTab,
|
|
layer: 58,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
rule, err := NewRule(tt.rule, "test.nip", 1)
|
|
require.NoError(t, err)
|
|
|
|
result, err := rule.Evaluate(data.Item{
|
|
ID: 516,
|
|
Name: "healingpotion",
|
|
Quality: item.QualityMagic,
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: tt.stat, Layer: tt.layer, Value: 3},
|
|
},
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, RuleResultFullMatch, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWarlockSkillAliasesReadExactLayers(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
rule string
|
|
stat stat.ID
|
|
layer int
|
|
}{
|
|
{
|
|
name: "warlock class skills",
|
|
rule: "[quality] >= normal # [warlockskills] >= 3",
|
|
stat: stat.AddClassSkills,
|
|
layer: 7,
|
|
},
|
|
{
|
|
name: "itemadd warlock class skills",
|
|
rule: "[quality] >= normal # [itemaddwarlockskills] >= 3",
|
|
stat: stat.AddClassSkills,
|
|
layer: 7,
|
|
},
|
|
{
|
|
name: "warlock chaos skill tab",
|
|
rule: "[quality] >= normal # [chaosskilltab] >= 3",
|
|
stat: stat.AddSkillTab,
|
|
layer: 58,
|
|
},
|
|
{
|
|
name: "itemadd warlock chaos skill tab",
|
|
rule: "[quality] >= normal # [itemaddchaosskilltab] >= 3",
|
|
stat: stat.AddSkillTab,
|
|
layer: 58,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
rule, err := NewRule(tt.rule, "test.nip", 1)
|
|
require.NoError(t, err)
|
|
|
|
result, err := rule.Evaluate(data.Item{
|
|
ID: 516,
|
|
Name: "healingpotion",
|
|
Quality: item.QualityMagic,
|
|
Identified: true,
|
|
Stats: []stat.Data{
|
|
{ID: tt.stat, Layer: tt.layer, Value: 3},
|
|
},
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, RuleResultFullMatch, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAllResUsesLowestElementalResistance(t *testing.T) {
|
|
rule, err := NewRule("[quality] >= normal # [allres] >= 20", "test.nip", 1)
|
|
require.NoError(t, err)
|
|
|
|
result, err := rule.Evaluate(data.Item{
|
|
ID: 516,
|
|
Name: "healingpotion",
|
|
Quality: item.QualityMagic,
|
|
Identified: true,
|
|
Stats: stat.Stats{
|
|
{ID: stat.FireResist, Value: 25},
|
|
{ID: stat.LightningResist, Value: 20},
|
|
{ID: stat.ColdResist, Value: 30},
|
|
{ID: stat.PoisonResist, Value: 35},
|
|
},
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, RuleResultFullMatch, result)
|
|
}
|
|
|
|
func TestAllStatAliasesReadFromItemStats(t *testing.T) {
|
|
aliases := make([]string, 0, len(statAliases))
|
|
for alias := range statAliases {
|
|
aliases = append(aliases, alias)
|
|
}
|
|
sort.Strings(aliases)
|
|
|
|
for _, alias := range aliases {
|
|
t.Run(alias, func(t *testing.T) {
|
|
if alias == "allres" {
|
|
itemToCheck := data.Item{
|
|
ID: 516,
|
|
Name: "healingpotion",
|
|
Quality: item.QualityMagic,
|
|
Identified: true,
|
|
Stats: stat.Stats{
|
|
{ID: stat.FireResist, Value: 321},
|
|
{ID: stat.LightningResist, Value: 321},
|
|
{ID: stat.ColdResist, Value: 321},
|
|
{ID: stat.PoisonResist, Value: 321},
|
|
},
|
|
}
|
|
requireRuleFullMatch(t, alias, 321, itemToCheck)
|
|
return
|
|
}
|
|
|
|
statData := statAliases[alias]
|
|
require.NotEmpty(t, statData)
|
|
|
|
statID := stat.ID(statData[0])
|
|
layer := 0
|
|
if len(statData) > 1 {
|
|
layer = statData[1]
|
|
}
|
|
|
|
statsItem, expected := itemWithStat(statID, layer, false)
|
|
requireRuleFullMatch(t, alias, expected, statsItem)
|
|
|
|
baseStatsItem, expected := itemWithStat(statID, layer, true)
|
|
requireRuleFullMatch(t, alias, expected, baseStatsItem)
|
|
})
|
|
}
|
|
}
|
|
|
|
func itemWithStat(statID stat.ID, layer int, base bool) (data.Item, int) {
|
|
expected := 321
|
|
statLayer := layer
|
|
statValue := expected
|
|
|
|
switch statID {
|
|
case stat.SkillOnAttack, stat.SkillOnKill, stat.SkillOnDeath, stat.SkillOnHit, stat.SkillOnLevelUp, stat.SkillOnGetHit, stat.ItemChargedSkill:
|
|
if layer == 1 || layer == 2 {
|
|
skillID := 47
|
|
level := 13
|
|
statLayer = skillID<<6 | level
|
|
statValue = 5
|
|
if layer == 1 {
|
|
expected = skillID
|
|
} else {
|
|
expected = level
|
|
}
|
|
}
|
|
}
|
|
|
|
itemToCheck := data.Item{
|
|
ID: 516,
|
|
Name: "healingpotion",
|
|
Quality: item.QualityMagic,
|
|
Identified: true,
|
|
}
|
|
statToCheck := stat.Data{ID: statID, Layer: statLayer, Value: statValue}
|
|
if base {
|
|
itemToCheck.BaseStats = stat.Stats{statToCheck}
|
|
} else {
|
|
itemToCheck.Stats = stat.Stats{statToCheck}
|
|
}
|
|
|
|
return itemToCheck, expected
|
|
}
|
|
|
|
func requireRuleFullMatch(t *testing.T, alias string, expected int, itemToCheck data.Item) {
|
|
t.Helper()
|
|
|
|
rule, err := NewRule("[quality] >= normal # ["+alias+"] == "+strconv.Itoa(expected), "test.nip", 1)
|
|
require.NoError(t, err)
|
|
require.NoError(t, rule.ValidateStats())
|
|
|
|
result, err := rule.Evaluate(itemToCheck)
|
|
require.NoError(t, err)
|
|
require.Equal(t, RuleResultFullMatch, result)
|
|
}
|
|
|
|
func BenchmarkEvaluate(b *testing.B) {
|
|
it := data.Item{
|
|
ID: 0,
|
|
Name: "Axe",
|
|
Quality: item.QualitySuperior,
|
|
}
|
|
|
|
rule, err := NewRule(
|
|
"[type] == amulet && [quality] == crafted # ([shapeshiftingskilltab] >= 2 || [elementalskilltab] >= 2 || [druidsummoningskilltab] >= 2) && [fcr] >= 10 && ([strength]+[maxhp]+[maxmana] >= 60 || [dexterity]+[maxhp]+[maxmana] >= 60 || [strength]+[dexterity]+[maxhp] >= 50 || [strength]+[dexterity]+[maxmana] >= 55)",
|
|
"test",
|
|
1,
|
|
)
|
|
require.NoError(b, err)
|
|
|
|
for n := 0; n < b.N; n++ {
|
|
rule.Evaluate(it)
|
|
}
|
|
}
|