649 lines
18 KiB
Go
649 lines
18 KiB
Go
package nip
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/expr-lang/expr"
|
|
"github.com/expr-lang/expr/vm"
|
|
"github.com/hectorgimenez/d2go/pkg/data"
|
|
"github.com/hectorgimenez/d2go/pkg/data/item"
|
|
"github.com/hectorgimenez/d2go/pkg/data/stat"
|
|
)
|
|
|
|
const (
|
|
RuleResultFullMatch RuleResult = 1
|
|
RuleResultPartial RuleResult = 2
|
|
RuleResultNoMatch RuleResult = 3
|
|
)
|
|
|
|
var (
|
|
fixedPropsRegexp = regexp.MustCompile(`(\[(type|quality|class|name|flag|color|prefix|suffix)]\s*(<=|<|>|>=|!=|==)\s*([a-zA-Z0-9]+))`)
|
|
statsRegexp = regexp.MustCompile(`\[(.*?)]`)
|
|
maxQtyRegexp = regexp.MustCompile(`(\[maxquantity]\s*(<=|<|>|>=|!=|==)\s*([0-9]+))`)
|
|
tierRegexp = regexp.MustCompile(`(\[tier]\s*(<=|<|>|>=|!=|==)\s*([0-9]+))`)
|
|
mercTierRegexp = regexp.MustCompile(`(\[merctier]\s*(<=|<|>|>=|!=|==)\s*([0-9]+))`)
|
|
whitespaceRegexp = regexp.MustCompile(`\s+`)
|
|
)
|
|
|
|
type Rule struct {
|
|
RawLine string // Original line, don't use it for evaluation
|
|
Filename string
|
|
LineNumber int
|
|
Enabled bool
|
|
maxQuantity int
|
|
tier float64
|
|
mercTier float64
|
|
stage1 *vm.Program
|
|
stage2 *vm.Program
|
|
requiredStats []string
|
|
skillTabSum bool
|
|
classSkillSum bool
|
|
resistSum bool
|
|
nonResistStat bool
|
|
}
|
|
|
|
type RuleResult int
|
|
type Rules []Rule
|
|
|
|
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
|
|
}
|
|
|
|
func (r Rules) EvaluateAllIgnoreTiers(it data.Item) (Rule, RuleResult) {
|
|
bestMatch := RuleResultNoMatch
|
|
bestMatchingRule := Rule{}
|
|
for _, rule := range r {
|
|
if rule.Enabled {
|
|
if rule.tier > 0 || rule.mercTier > 0 {
|
|
continue
|
|
}
|
|
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
|
|
}
|
|
|
|
func (r Rules) EvaluateTiers(it data.Item, tierRulesIndexes []int) (Rule, Rule) {
|
|
highestTierRule := Rule{}
|
|
highestMercTierRule := Rule{}
|
|
for _, ruleIndex := range tierRulesIndexes {
|
|
if ruleIndex < len(r) {
|
|
rule := r[ruleIndex]
|
|
if rule.Enabled {
|
|
result, err := rule.Evaluate(it)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if result == RuleResultFullMatch || result == RuleResultPartial {
|
|
if rule.tier > highestTierRule.tier {
|
|
highestTierRule = rule
|
|
}
|
|
if rule.mercTier > highestMercTierRule.mercTier {
|
|
highestMercTierRule = rule
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return highestTierRule, highestMercTierRule
|
|
}
|
|
|
|
var fixedPropsList = map[string]int{"type": 0, "quality": 0, "class": 0, "name": 0, "flag": 0, "color": 0, "prefix": 0, "suffix": 0}
|
|
|
|
// flagBit* are the bits Evaluate sets on the runtime `flag` value AND the
|
|
// rewriter (replaceStringPropertiesInStage1) recognises. flagAllBits is the
|
|
// disjunction of every supported flag bit: the rewriter enumerates all subsets
|
|
// of flagAllBits when expanding bit tests for the `==`/`!=` operators (expr-lang
|
|
// has no bitwise operators). To add a new flag bit, update Evaluate to set it,
|
|
// add a case in the rewriter's name->bit switch, AND fold it into flagAllBits
|
|
// so the enumeration sees it.
|
|
const (
|
|
flagBitEthereal = 0x400000
|
|
flagBitRuneword = 0x4000000
|
|
flagAllBits = flagBitEthereal | flagBitRuneword
|
|
)
|
|
|
|
var (
|
|
maxClassSkillsLayer = maxAliasLayer(stat.AddClassSkills)
|
|
maxSkillTabLayer = maxAliasLayer(stat.AddSkillTab)
|
|
)
|
|
|
|
func NewRule(rawRule string, filename string, lineNumber int) (Rule, error) {
|
|
rule := sanitizeLine(rawRule)
|
|
|
|
// 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], "")
|
|
}
|
|
|
|
// Try to get the tier value and purge it from the rule, we can not evaluate it yet
|
|
tier := 0.0
|
|
for _, prop := range tierRegexp.FindAllStringSubmatch(rule, -1) {
|
|
parsedTier, err := strconv.Atoi(prop[3])
|
|
if err != nil {
|
|
return Rule{}, fmt.Errorf("error parsing tier value %s: %w", prop[3], err)
|
|
}
|
|
tier = float64(parsedTier)
|
|
rule = strings.ReplaceAll(rule, prop[0], "")
|
|
}
|
|
|
|
// Try to get the merctier value and purge it from the rule, we can not evaluate it yet
|
|
mercTier := 0.0
|
|
for _, prop := range mercTierRegexp.FindAllStringSubmatch(rule, -1) {
|
|
parsedMercTier, err := strconv.Atoi(prop[3])
|
|
if err != nil {
|
|
return Rule{}, fmt.Errorf("error parsing merctier value %s: %w", prop[3], err)
|
|
}
|
|
mercTier = float64(parsedMercTier)
|
|
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
|
|
}
|
|
|
|
r := Rule{
|
|
RawLine: rawRule,
|
|
Filename: filename,
|
|
LineNumber: lineNumber,
|
|
Enabled: true,
|
|
maxQuantity: maxQuantity,
|
|
tier: tier,
|
|
mercTier: mercTier,
|
|
}
|
|
|
|
parts := strings.SplitN(rule, "#", 3)
|
|
|
|
if len(parts) > 0 {
|
|
stage1 := strings.TrimSpace(parts[0])
|
|
if stage1 != "" {
|
|
line, err := replaceStringPropertiesInStage1(stage1)
|
|
if err != nil {
|
|
return Rule{}, err
|
|
}
|
|
|
|
line = strings.ReplaceAll(line, "[", "")
|
|
line = strings.ReplaceAll(line, "]", "")
|
|
program, err := expr.Compile(line, expr.Env(fixedPropsList))
|
|
if err != nil {
|
|
return Rule{}, fmt.Errorf("error compiling rule stage1: %w", err)
|
|
}
|
|
r.stage1 = program
|
|
}
|
|
}
|
|
|
|
if len(parts) > 1 {
|
|
stage2 := strings.TrimSpace(parts[1])
|
|
if stage2 != "" {
|
|
// Extract stats before removing brackets for compilation
|
|
r.requiredStats = getRequiredStatsForRule(stage2)
|
|
stage2Source := ""
|
|
rawParts := strings.SplitN(rawRule, "#", 3)
|
|
if len(rawParts) > 1 {
|
|
stage2Source = strings.ToLower(rawParts[1])
|
|
}
|
|
r.skillTabSum = strings.Contains(stage2Source, "[itemaddskilltab]")
|
|
r.classSkillSum = strings.Contains(stage2Source, "[itemaddclassskills]")
|
|
if strings.Contains(stage2Source, "resist") {
|
|
r.resistSum = strings.Contains(stage2Source, "+") || strings.Contains(stage2Source, "-") ||
|
|
(strings.Contains(stage2Source, "(") && strings.Contains(stage2Source, ")"))
|
|
}
|
|
|
|
statsMap := make(map[string]int)
|
|
for _, prop := range r.requiredStats {
|
|
statsMap[prop] = 0
|
|
if !strings.Contains(prop, "resist") {
|
|
r.nonResistStat = true
|
|
}
|
|
}
|
|
|
|
// Normalize whitespace around operators in parenthesized expressions
|
|
stage2 = normalizeParenthesizedExpressions(stage2)
|
|
|
|
// Remove brackets for compilation
|
|
compileReady := strings.ReplaceAll(stage2, "[", "")
|
|
compileReady = strings.ReplaceAll(compileReady, "]", "")
|
|
|
|
program, err := expr.Compile(compileReady, expr.Env(statsMap))
|
|
if err != nil {
|
|
return Rule{}, fmt.Errorf("error compiling rule stage2: %w, expression: %s", err, compileReady)
|
|
}
|
|
r.stage2 = program
|
|
}
|
|
}
|
|
|
|
return r, nil
|
|
}
|
|
|
|
func normalizeParenthesizedExpressions(expr string) string {
|
|
// Normalize common operators
|
|
expr = strings.ReplaceAll(expr, "||", " || ")
|
|
expr = strings.ReplaceAll(expr, "&&", " && ")
|
|
expr = strings.ReplaceAll(expr, "==", " == ")
|
|
expr = strings.ReplaceAll(expr, "!=", " != ")
|
|
expr = strings.ReplaceAll(expr, ">=", " >= ")
|
|
expr = strings.ReplaceAll(expr, "<=", " <= ")
|
|
|
|
// Fix extra spaces
|
|
expr = whitespaceRegexp.ReplaceAllString(expr, " ")
|
|
|
|
// Normalize parentheses spacing
|
|
expr = strings.ReplaceAll(expr, "( ", "(")
|
|
expr = strings.ReplaceAll(expr, " )", ")")
|
|
|
|
return expr
|
|
}
|
|
|
|
func (r Rule) Evaluate(it data.Item) (RuleResult, error) {
|
|
// Stage 1: Basic properties evaluation
|
|
desc := it.Desc()
|
|
/*
|
|
// 0x400000 (eth) | 0x4000000 (runeword) kolbot style. Bit values are
|
|
// shared with the rewriter (replaceStringPropertiesInStage1) via the
|
|
// package-level flagBit* constants so the rewriter's enumeration of
|
|
// possible flag-states stays in lock-step with the values written
|
|
// here — adding a third bit requires editing both this writer AND
|
|
// the rewriter, but the constants make the dependency discoverable.
|
|
*/
|
|
currentFlag := 0
|
|
if it.Ethereal {
|
|
currentFlag |= flagBitEthereal
|
|
}
|
|
if it.IsRuneword {
|
|
currentFlag |= flagBitRuneword
|
|
}
|
|
stage1Props := map[string]int{
|
|
"type": desc.GetType().ID,
|
|
"quality": int(it.Quality),
|
|
"class": int(desc.Tier()),
|
|
"name": it.ID,
|
|
"flag": currentFlag,
|
|
}
|
|
if it.Affixes.Rare.Prefix != 0 {
|
|
stage1Props["prefix"] = int(it.Affixes.Rare.Prefix)
|
|
}
|
|
for _, prefix := range it.Affixes.Magic.Prefixes {
|
|
if prefix != 0 {
|
|
stage1Props["prefix"] = int(prefix)
|
|
break
|
|
}
|
|
}
|
|
if it.Affixes.Rare.Suffix != 0 {
|
|
stage1Props["suffix"] = int(it.Affixes.Rare.Suffix)
|
|
}
|
|
for _, suffix := range it.Affixes.Magic.Suffixes {
|
|
if suffix != 0 {
|
|
stage1Props["suffix"] = int(suffix)
|
|
break
|
|
}
|
|
}
|
|
|
|
// Check if stage1 exists before evaluating
|
|
if r.stage1 == nil {
|
|
return RuleResultNoMatch, fmt.Errorf("stage1 program is nil")
|
|
}
|
|
|
|
// Let's evaluate first stage
|
|
stage1Result, err := expr.Run(r.stage1, stage1Props)
|
|
if err != nil {
|
|
return RuleResultNoMatch, fmt.Errorf("error evaluating rule stage1: %w", err)
|
|
}
|
|
|
|
// If stage1 does not match, we can stop here, nothing else to match
|
|
if !stage1Result.(bool) {
|
|
return RuleResultNoMatch, nil
|
|
}
|
|
|
|
// If we have no stage2 (no stat requirements), allow full match even for unidentified items
|
|
if r.stage2 == nil {
|
|
return RuleResultFullMatch, nil
|
|
}
|
|
|
|
// From here on we have stat requirements - return partial match for unidentified items
|
|
if !it.Identified {
|
|
return RuleResultPartial, nil
|
|
}
|
|
|
|
stage2Props := make(map[string]int, len(r.requiredStats))
|
|
|
|
// Special handling for skill tabs
|
|
if r.skillTabSum {
|
|
stage2Props["itemaddskilltab"] = evaluateSkillTabSum(it)
|
|
}
|
|
|
|
// Special handling for class skills
|
|
if r.classSkillSum {
|
|
stage2Props["itemaddclassskills"] = evaluateClassSkillsSum(it)
|
|
}
|
|
|
|
// Handle resist sums - check if item has any resist stats (including negative values)
|
|
hasAnyResist := false
|
|
|
|
if r.resistSum {
|
|
// Check if the item has any resist stats at all (including negative values like sunders)
|
|
for _, statName := range r.requiredStats {
|
|
if !strings.Contains(statName, "resist") {
|
|
continue
|
|
}
|
|
statData, found := statAliases[statName]
|
|
if !found {
|
|
continue
|
|
}
|
|
layer := 0
|
|
if len(statData) > 1 {
|
|
layer = statData[1]
|
|
}
|
|
|
|
// Check if stat exists at all, regardless of value (including negative)
|
|
if _, found := it.FindStat(stat.ID(statData[0]), layer); found {
|
|
hasAnyResist = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Evaluate each required stat
|
|
for _, statName := range r.requiredStats {
|
|
// Skip stats we've already handled
|
|
if statName == "itemaddskilltab" || statName == "itemaddclassskills" {
|
|
continue
|
|
}
|
|
statData, found := statAliases[statName]
|
|
if !found {
|
|
return RuleResultNoMatch, fmt.Errorf("property %s is not valid or not supported", statName)
|
|
}
|
|
|
|
layer := 0
|
|
if len(statData) > 1 {
|
|
layer = statData[1]
|
|
}
|
|
|
|
// Use the FindStat method which handles both Stats and BaseStats
|
|
statFound := false
|
|
var statValue int
|
|
|
|
isChanceToCast := false
|
|
targetID := stat.ID(statData[0])
|
|
|
|
// Special cases, those encode skill ID and level in the layer
|
|
switch targetID {
|
|
case stat.SkillOnAttack, stat.SkillOnKill, stat.SkillOnDeath, stat.SkillOnHit, stat.SkillOnLevelUp, stat.SkillOnGetHit, stat.ItemChargedSkill:
|
|
isChanceToCast = true
|
|
}
|
|
|
|
if isChanceToCast && (layer == 1 || layer == 2) {
|
|
var foundStat *stat.Data
|
|
for _, s := range it.Stats {
|
|
if s.ID == targetID {
|
|
foundStat = &s
|
|
break
|
|
}
|
|
}
|
|
|
|
if foundStat == nil {
|
|
for _, s := range it.BaseStats {
|
|
if s.ID == targetID {
|
|
foundStat = &s
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if foundStat != nil {
|
|
statFound = true
|
|
skillID := foundStat.Layer >> 6
|
|
level := foundStat.Layer & 0x3F
|
|
|
|
if layer == 1 {
|
|
statValue = skillID
|
|
} else {
|
|
statValue = level
|
|
}
|
|
}
|
|
} else {
|
|
if itemStat, found := it.FindStat(stat.ID(statData[0]), layer); found {
|
|
statValue = itemStat.Value
|
|
statFound = true
|
|
}
|
|
}
|
|
|
|
// Special handling for stats not found
|
|
if !statFound {
|
|
isResistStat := strings.Contains(statName, "resist")
|
|
// Special case: Pure resist-sum rules (like sunders) should fail early if no resists exist
|
|
// But mixed rules (resists OR other stats) should continue to check other conditions
|
|
if isResistStat && r.resistSum && !hasAnyResist && !r.nonResistStat {
|
|
// This is a pure resist rule and item has no resists - can't match
|
|
return RuleResultNoMatch, nil
|
|
}
|
|
// For all other cases, default missing stats to 0 and let expression evaluate
|
|
stage2Props[statName] = 0
|
|
} else {
|
|
stage2Props[statName] = statValue
|
|
}
|
|
}
|
|
|
|
res, err := expr.Run(r.stage2, stage2Props)
|
|
if err != nil {
|
|
return RuleResultNoMatch, fmt.Errorf("error evaluating rule stage2: %w", err)
|
|
}
|
|
|
|
// 100% rule match, we can return here
|
|
if res.(bool) {
|
|
return RuleResultFullMatch, nil
|
|
}
|
|
|
|
return RuleResultNoMatch, nil
|
|
}
|
|
|
|
func replaceStringPropertiesInStage1(stage1 string) (string, error) {
|
|
baseProperties := fixedPropsRegexp.FindAllStringSubmatch(stage1, -1)
|
|
for _, prop := range baseProperties {
|
|
replaceWith := ""
|
|
switch prop[2] {
|
|
case "type":
|
|
replaceWith = strings.ReplaceAll(prop[0], prop[4], fmt.Sprintf("%d", item.ItemTypes[typeAliases[prop[4]]].ID))
|
|
case "quality":
|
|
replaceWith = strings.ReplaceAll(prop[0], prop[4], fmt.Sprintf("%d", qualityAliases[prop[4]]))
|
|
case "class":
|
|
replaceWith = strings.ReplaceAll(prop[0], prop[4], fmt.Sprintf("%d", classAliases[prop[4]]))
|
|
case "name":
|
|
replaceWith = strings.ReplaceAll(prop[0], prop[4], fmt.Sprintf("%d", item.GetIDByName(prop[4])))
|
|
case "flag":
|
|
// [flag] is a bitmask at runtime (ethereal | runeword), so equality must be
|
|
// a bit test, not a scalar compare. Previously "[flag] == ethereal" rewrote
|
|
// to "flag == 0x400000", which evaluated FALSE on an ethereal runeword (whose
|
|
// flag is 0x4400000 == eth|rw). expr-lang has no bitwise operators, so we
|
|
// enumerate the two states the chosen bit can produce. flagAllBits below
|
|
// MUST stay in sync with the writer in Evaluate — every bit Evaluate can
|
|
// set must appear here, otherwise the negation form may miss valid items.
|
|
name := strings.ToLower(prop[4])
|
|
var bit int
|
|
switch name {
|
|
case "runeword":
|
|
bit = flagBitRuneword
|
|
case "ethereal":
|
|
bit = flagBitEthereal
|
|
default:
|
|
bit = 1
|
|
}
|
|
switch {
|
|
case bit == 1:
|
|
// Unknown flag name — preserve legacy behaviour (replace value with 1).
|
|
replaceWith = strings.ReplaceAll(prop[0], prop[4], "1")
|
|
case prop[3] == "==":
|
|
switch bit {
|
|
case flagBitRuneword:
|
|
replaceWith = fmt.Sprintf("(flag == %d || flag == %d)", flagBitRuneword, flagBitRuneword|flagBitEthereal)
|
|
case flagBitEthereal:
|
|
replaceWith = fmt.Sprintf("(flag == %d || flag == %d)", flagBitEthereal, flagBitRuneword|flagBitEthereal)
|
|
}
|
|
|
|
case prop[3] == "!=":
|
|
switch bit {
|
|
case flagBitRuneword:
|
|
replaceWith = fmt.Sprintf("(flag == 0 || flag == %d)", flagBitEthereal)
|
|
case flagBitEthereal:
|
|
replaceWith = fmt.Sprintf("(flag == 0 || flag == %d)", flagBitRuneword)
|
|
}
|
|
default:
|
|
// Comparison operators other than ==/!= are nonsensical for a bitmask
|
|
// but kept as scalar compares so exotic legacy rules don't suddenly fail.
|
|
replaceWith = strings.ReplaceAll(prop[0], prop[4], fmt.Sprintf("%d", bit))
|
|
}
|
|
case "prefix", "suffix":
|
|
// Handle prefix/suffix IDs
|
|
replaceWith = strings.ReplaceAll(prop[0], prop[4], prop[4])
|
|
case "color":
|
|
// TODO: Not supported yet
|
|
return "", fmt.Errorf("property %s is not supported yet", prop[2])
|
|
}
|
|
|
|
if replaceWith != "" {
|
|
stage1 = strings.ReplaceAll(stage1, prop[0], replaceWith)
|
|
}
|
|
}
|
|
|
|
return stage1, nil
|
|
}
|
|
|
|
func getRequiredStatsForRule(line string) []string {
|
|
statsList := make([]string, 0)
|
|
statsFound := make(map[string]bool)
|
|
|
|
for _, statName := range statsRegexp.FindAllStringSubmatch(line, -1) {
|
|
if !statsFound[statName[1]] {
|
|
statsList = append(statsList, statName[1])
|
|
statsFound[statName[1]] = true
|
|
}
|
|
}
|
|
return statsList
|
|
}
|
|
|
|
// ValidateStats checks that all required stats in stage2 are valid aliases.
|
|
func (r Rule) ValidateStats() error {
|
|
for _, statName := range r.requiredStats {
|
|
if _, found := statAliases[statName]; !found {
|
|
return fmt.Errorf("property %s is not valid or not supported", statName)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func evaluateClassSkillsSum(it data.Item) int {
|
|
return evaluatePositiveStatSum(it, stat.AddClassSkills, maxClassSkillsLayer)
|
|
}
|
|
|
|
func evaluateSkillTabSum(it data.Item) int {
|
|
return evaluatePositiveStatSum(it, stat.AddSkillTab, maxSkillTabLayer)
|
|
}
|
|
|
|
const aggregateLayerFastPathLimit = 64
|
|
|
|
func evaluatePositiveStatSum(it data.Item, statID stat.ID, maxLayer int) int {
|
|
// Current aggregate aliases fit in a small stack bitmap; retain the general
|
|
// lookup path if additional layers later exceed that bound.
|
|
if maxLayer >= aggregateLayerFastPathLimit {
|
|
total := 0
|
|
for layer := 0; layer <= maxLayer; layer++ {
|
|
if itemStat, found := it.FindStat(statID, layer); found && itemStat.Value > 0 {
|
|
total += itemStat.Value
|
|
}
|
|
}
|
|
return total
|
|
}
|
|
|
|
var foundLayers [aggregateLayerFastPathLimit]bool
|
|
total := 0
|
|
for _, itemStat := range it.Stats {
|
|
if itemStat.ID != statID || itemStat.Layer < 0 || itemStat.Layer > maxLayer || foundLayers[itemStat.Layer] {
|
|
continue
|
|
}
|
|
foundLayers[itemStat.Layer] = true
|
|
if itemStat.Value > 0 {
|
|
total += itemStat.Value
|
|
}
|
|
}
|
|
for _, itemStat := range it.BaseStats {
|
|
if itemStat.ID != statID || itemStat.Layer < 0 || itemStat.Layer > maxLayer || foundLayers[itemStat.Layer] {
|
|
continue
|
|
}
|
|
foundLayers[itemStat.Layer] = true
|
|
if itemStat.Value > 0 {
|
|
total += itemStat.Value
|
|
}
|
|
}
|
|
return total
|
|
}
|
|
|
|
func maxAliasLayer(statID stat.ID) int {
|
|
maxLayer := 0
|
|
for _, statData := range statAliases {
|
|
if len(statData) == 0 || stat.ID(statData[0]) != statID {
|
|
continue
|
|
}
|
|
layer := 0
|
|
if len(statData) > 1 {
|
|
layer = statData[1]
|
|
}
|
|
if layer > maxLayer {
|
|
maxLayer = layer
|
|
}
|
|
}
|
|
return maxLayer
|
|
}
|
|
|
|
// 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) Tier() float64 {
|
|
return r.tier
|
|
}
|
|
|
|
func (r Rule) MercTier() float64 {
|
|
return r.mercTier
|
|
}
|