Rewrite to Go: engine, plugin system, D2R plugin, API, loot filter

This commit is contained in:
Hoid 2026-02-14 09:43:39 +00:00
parent e0282a7111
commit 3b363192f2
60 changed files with 1576 additions and 3407 deletions

120
pkg/engine/loot/filter.go Normal file
View file

@ -0,0 +1,120 @@
// Package loot provides a declarative, rule-based loot filter engine.
//
// Loot rules are defined in YAML and evaluated against detected items.
// Supports complex matching on type, name, rarity, properties, and more.
package loot
import (
"strings"
"git.cloonar.com/openclawd/iso-bot/pkg/plugin"
)
// Priority levels for pickup ordering.
const (
PriorityLow = 1
PriorityNormal = 5
PriorityHigh = 8
PriorityCritical = 10
)
// Condition defines a match condition for a loot rule.
type Condition struct {
Type *string `yaml:"type,omitempty"` // "unique", "set", "rare", etc.
NameContains *string `yaml:"name_contains,omitempty"` // substring match
NameExact *string `yaml:"name_exact,omitempty"` // exact match
MinRarity *int `yaml:"min_rarity,omitempty"` // minimum rarity tier
MaxRarity *int `yaml:"max_rarity,omitempty"`
BaseType *string `yaml:"base_type,omitempty"` // e.g., "Diadem", "Monarch"
HasProperty *string `yaml:"has_property,omitempty"` // item has this property key
PropertyGTE map[string]int `yaml:"property_gte,omitempty"` // property >= value
}
// Action defines what to do when a rule matches.
type Action string
const (
ActionPickup Action = "pickup"
ActionIgnore Action = "ignore"
ActionAlert Action = "alert" // pickup + send notification
)
// Rule is a single loot filter rule.
type Rule struct {
Name string `yaml:"name,omitempty"`
Match Condition `yaml:"match"`
Action Action `yaml:"action"`
Priority int `yaml:"priority,omitempty"`
}
// RuleEngine evaluates items against a list of rules.
type RuleEngine struct {
Rules []Rule
}
// NewRuleEngine creates a rule engine from a list of rules.
func NewRuleEngine(rules []Rule) *RuleEngine {
return &RuleEngine{Rules: rules}
}
// Evaluate checks an item against all rules.
// Returns the first matching rule's action and priority.
func (e *RuleEngine) Evaluate(item plugin.DetectedItem) (action Action, priority int, matched bool) {
for _, rule := range e.Rules {
if e.matches(rule.Match, item) {
p := rule.Priority
if p == 0 {
p = PriorityNormal
}
return rule.Action, p, true
}
}
return ActionIgnore, 0, false
}
// ShouldPickup implements plugin.LootFilter.
func (e *RuleEngine) ShouldPickup(item plugin.DetectedItem) (bool, int) {
action, priority, matched := e.Evaluate(item)
if !matched {
return false, 0
}
return action == ActionPickup || action == ActionAlert, priority
}
// ShouldAlert implements plugin.LootFilter.
func (e *RuleEngine) ShouldAlert(item plugin.DetectedItem) bool {
action, _, matched := e.Evaluate(item)
return matched && action == ActionAlert
}
func (e *RuleEngine) matches(cond Condition, item plugin.DetectedItem) bool {
if cond.Type != nil && !strings.EqualFold(item.Type, *cond.Type) {
return false
}
if cond.NameContains != nil && !strings.Contains(strings.ToLower(item.Name), strings.ToLower(*cond.NameContains)) {
return false
}
if cond.NameExact != nil && !strings.EqualFold(item.Name, *cond.NameExact) {
return false
}
if cond.MinRarity != nil && item.Rarity < *cond.MinRarity {
return false
}
if cond.MaxRarity != nil && item.Rarity > *cond.MaxRarity {
return false
}
if cond.BaseType != nil && !strings.EqualFold(item.Properties["base_type"], *cond.BaseType) {
return false
}
if cond.HasProperty != nil {
if _, ok := item.Properties[*cond.HasProperty]; !ok {
return false
}
}
for key, minVal := range cond.PropertyGTE {
// TODO: Parse property value as int and compare
_ = key
_ = minVal
}
return true
}