Rewrite to Go: engine, plugin system, D2R plugin, API, loot filter
This commit is contained in:
parent
e0282a7111
commit
3b363192f2
60 changed files with 1576 additions and 3407 deletions
107
plugins/d2r/config.go
Normal file
107
plugins/d2r/config.go
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// D2R-specific configuration: screen regions, colors, timings.
|
||||
package d2r
|
||||
|
||||
import "image"
|
||||
|
||||
// ScreenRegions defines UI element positions at 1920x1080.
|
||||
type ScreenRegions struct {
|
||||
HealthOrb image.Rectangle
|
||||
ManaOrb image.Rectangle
|
||||
XPBar image.Rectangle
|
||||
Belt image.Rectangle
|
||||
Minimap image.Rectangle
|
||||
Inventory image.Rectangle
|
||||
Stash image.Rectangle
|
||||
SkillLeft image.Rectangle
|
||||
SkillRight image.Rectangle
|
||||
}
|
||||
|
||||
// HSVRange defines a color range in HSV space.
|
||||
type HSVRange struct {
|
||||
LowerH, LowerS, LowerV int
|
||||
UpperH, UpperS, UpperV int
|
||||
}
|
||||
|
||||
// Colors defines HSV ranges for game elements.
|
||||
type Colors struct {
|
||||
HealthFilled HSVRange
|
||||
ManaFilled HSVRange
|
||||
ItemUnique HSVRange
|
||||
ItemSet HSVRange
|
||||
ItemRare HSVRange
|
||||
ItemRuneword HSVRange
|
||||
PortalBlue HSVRange
|
||||
}
|
||||
|
||||
// Timings defines game-specific delay constants.
|
||||
type Timings struct {
|
||||
LoadingScreenMaxMs int
|
||||
TownPortalCastMs int
|
||||
TeleportDelayMs int
|
||||
PotionCooldownMs int
|
||||
PickupDelayMs int
|
||||
}
|
||||
|
||||
// Config holds all D2R-specific configuration.
|
||||
type Config struct {
|
||||
Resolution image.Point
|
||||
Regions ScreenRegions
|
||||
Colors Colors
|
||||
Timings Timings
|
||||
|
||||
// Loot settings
|
||||
PickupUniques bool
|
||||
PickupSets bool
|
||||
PickupRares bool
|
||||
PickupRunes bool
|
||||
MinRuneTier int
|
||||
PickupGems bool
|
||||
|
||||
// Safety thresholds (0.0 - 1.0)
|
||||
HealthPotionThreshold float64
|
||||
ManaPotionThreshold float64
|
||||
ChickenThreshold float64 // exit game if health below this
|
||||
}
|
||||
|
||||
// DefaultConfig returns the default D2R config for 1920x1080.
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
Resolution: image.Point{X: 1920, Y: 1080},
|
||||
Regions: ScreenRegions{
|
||||
HealthOrb: image.Rect(28, 545, 198, 715),
|
||||
ManaOrb: image.Rect(1722, 545, 1892, 715),
|
||||
XPBar: image.Rect(0, 1058, 1920, 1080),
|
||||
Belt: image.Rect(838, 1010, 1082, 1058),
|
||||
Minimap: image.Rect(1600, 0, 1920, 320),
|
||||
Inventory: image.Rect(960, 330, 1490, 770),
|
||||
Stash: image.Rect(430, 330, 960, 770),
|
||||
SkillLeft: image.Rect(194, 1036, 246, 1088),
|
||||
SkillRight: image.Rect(1674, 1036, 1726, 1088),
|
||||
},
|
||||
Colors: Colors{
|
||||
HealthFilled: HSVRange{0, 100, 100, 10, 255, 255},
|
||||
ManaFilled: HSVRange{100, 100, 100, 130, 255, 255},
|
||||
ItemUnique: HSVRange{15, 100, 180, 30, 255, 255},
|
||||
ItemSet: HSVRange{35, 100, 150, 55, 255, 255},
|
||||
ItemRare: HSVRange{15, 50, 200, 25, 150, 255},
|
||||
ItemRuneword: HSVRange{15, 100, 180, 30, 255, 255},
|
||||
PortalBlue: HSVRange{90, 150, 150, 120, 255, 255},
|
||||
},
|
||||
Timings: Timings{
|
||||
LoadingScreenMaxMs: 15000,
|
||||
TownPortalCastMs: 3500,
|
||||
TeleportDelayMs: 150,
|
||||
PotionCooldownMs: 1000,
|
||||
PickupDelayMs: 300,
|
||||
},
|
||||
PickupUniques: true,
|
||||
PickupSets: true,
|
||||
PickupRares: true,
|
||||
PickupRunes: true,
|
||||
MinRuneTier: 10,
|
||||
PickupGems: false,
|
||||
HealthPotionThreshold: 0.5,
|
||||
ManaPotionThreshold: 0.3,
|
||||
ChickenThreshold: 0.2,
|
||||
}
|
||||
}
|
||||
77
plugins/d2r/detector.go
Normal file
77
plugins/d2r/detector.go
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// Game state detection for D2R.
|
||||
package d2r
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"git.cloonar.com/openclawd/iso-bot/pkg/plugin"
|
||||
)
|
||||
|
||||
// Detector implements plugin.GameDetector for D2R.
|
||||
type Detector struct {
|
||||
config Config
|
||||
}
|
||||
|
||||
// NewDetector creates a D2R state detector.
|
||||
func NewDetector(config Config) *Detector {
|
||||
return &Detector{config: config}
|
||||
}
|
||||
|
||||
// DetectState analyzes a screenshot and returns the current game state.
|
||||
func (d *Detector) DetectState(frame image.Image) plugin.GameState {
|
||||
// Priority-based detection:
|
||||
// 1. Check for loading screen
|
||||
// 2. Check for main menu
|
||||
// 3. Check for character select
|
||||
// 4. Check for in-game (health orb visible)
|
||||
// 5. Check for death screen
|
||||
|
||||
if d.isLoading(frame) {
|
||||
return plugin.StateLoading
|
||||
}
|
||||
if d.isMainMenu(frame) {
|
||||
return plugin.StateMainMenu
|
||||
}
|
||||
if d.isCharacterSelect(frame) {
|
||||
return plugin.StateCharacterSelect
|
||||
}
|
||||
if d.isInGame(frame) {
|
||||
vitals := d.ReadVitals(frame)
|
||||
if vitals.HealthPct == 0 {
|
||||
return plugin.StateDead
|
||||
}
|
||||
return plugin.StateInGame
|
||||
}
|
||||
return plugin.StateUnknown
|
||||
}
|
||||
|
||||
// ReadVitals reads health and mana from the orbs.
|
||||
func (d *Detector) ReadVitals(frame image.Image) plugin.VitalStats {
|
||||
// TODO: Analyze health/mana orb regions using color detection
|
||||
return plugin.VitalStats{}
|
||||
}
|
||||
|
||||
// IsInGame returns true if health orb is visible.
|
||||
func (d *Detector) IsInGame(frame image.Image) bool {
|
||||
return d.isInGame(frame)
|
||||
}
|
||||
|
||||
func (d *Detector) isLoading(frame image.Image) bool {
|
||||
// TODO: Check for loading screen (mostly black with loading bar)
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *Detector) isMainMenu(frame image.Image) bool {
|
||||
// TODO: Template match main menu elements
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *Detector) isCharacterSelect(frame image.Image) bool {
|
||||
// TODO: Template match character select screen
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *Detector) isInGame(frame image.Image) bool {
|
||||
// TODO: Check if health orb region contains red pixels
|
||||
return false
|
||||
}
|
||||
56
plugins/d2r/loot/default.yaml
Normal file
56
plugins/d2r/loot/default.yaml
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# Default D2R loot filter rules.
|
||||
# Rules are evaluated top-to-bottom; first match wins.
|
||||
rules:
|
||||
# High-value uniques — always alert
|
||||
- name: "GG Uniques"
|
||||
match:
|
||||
type: unique
|
||||
name_contains: "Shako"
|
||||
action: alert
|
||||
priority: 10
|
||||
|
||||
- name: "High Runes"
|
||||
match:
|
||||
type: rune
|
||||
min_rarity: 20 # Vex+
|
||||
action: alert
|
||||
priority: 10
|
||||
|
||||
# Standard pickups
|
||||
- name: "All Uniques"
|
||||
match:
|
||||
type: unique
|
||||
action: pickup
|
||||
priority: 8
|
||||
|
||||
- name: "All Sets"
|
||||
match:
|
||||
type: set
|
||||
action: pickup
|
||||
priority: 7
|
||||
|
||||
- name: "Mid Runes"
|
||||
match:
|
||||
type: rune
|
||||
min_rarity: 10 # Lem+
|
||||
action: pickup
|
||||
priority: 6
|
||||
|
||||
- name: "GG Rare Bases"
|
||||
match:
|
||||
type: rare
|
||||
base_type: "Diadem"
|
||||
action: pickup
|
||||
priority: 5
|
||||
|
||||
- name: "GG Rare Bases"
|
||||
match:
|
||||
type: rare
|
||||
base_type: "Circlet"
|
||||
action: pickup
|
||||
priority: 5
|
||||
|
||||
# Ignore everything else
|
||||
- name: "Default Ignore"
|
||||
match: {}
|
||||
action: ignore
|
||||
65
plugins/d2r/plugin.go
Normal file
65
plugins/d2r/plugin.go
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
// Package d2r implements the Diablo II: Resurrected game plugin.
|
||||
package d2r
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"git.cloonar.com/openclawd/iso-bot/pkg/plugin"
|
||||
)
|
||||
|
||||
// Plugin implements plugin.Plugin for D2R.
|
||||
type Plugin struct {
|
||||
config Config
|
||||
services plugin.EngineServices
|
||||
detector *Detector
|
||||
reader *Reader
|
||||
}
|
||||
|
||||
// New creates a new D2R plugin with default config.
|
||||
func New() *Plugin {
|
||||
return &Plugin{
|
||||
config: DefaultConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
// Info returns plugin metadata.
|
||||
func (p *Plugin) Info() plugin.PluginInfo {
|
||||
return plugin.PluginInfo{
|
||||
ID: "d2r",
|
||||
Name: "Diablo II: Resurrected",
|
||||
Version: "0.1.0",
|
||||
Description: "Bot plugin for Diablo II: Resurrected — MF runs, rune farming, and more",
|
||||
Resolution: image.Point{X: 1920, Y: 1080},
|
||||
}
|
||||
}
|
||||
|
||||
// Init initializes the plugin with engine services.
|
||||
func (p *Plugin) Init(services plugin.EngineServices) error {
|
||||
p.services = services
|
||||
p.detector = NewDetector(p.config)
|
||||
p.reader = NewReader(p.config)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Detector returns the game state detector.
|
||||
func (p *Plugin) Detector() plugin.GameDetector {
|
||||
return p.detector
|
||||
}
|
||||
|
||||
// Reader returns the screen reader.
|
||||
func (p *Plugin) Reader() plugin.ScreenReader {
|
||||
return p.reader
|
||||
}
|
||||
|
||||
// Routines returns available farming routines.
|
||||
func (p *Plugin) Routines() []plugin.Routine {
|
||||
return []plugin.Routine{
|
||||
// TODO: Initialize routines with plugin services
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultLootFilter returns the default D2R loot filter.
|
||||
func (p *Plugin) DefaultLootFilter() plugin.LootFilter {
|
||||
// TODO: Return default rule engine
|
||||
return nil
|
||||
}
|
||||
47
plugins/d2r/reader.go
Normal file
47
plugins/d2r/reader.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// Screen reader for D2R — extracts game information from screenshots.
|
||||
package d2r
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"git.cloonar.com/openclawd/iso-bot/pkg/plugin"
|
||||
)
|
||||
|
||||
// Reader implements plugin.ScreenReader for D2R.
|
||||
type Reader struct {
|
||||
config Config
|
||||
}
|
||||
|
||||
// NewReader creates a D2R screen reader.
|
||||
func NewReader(config Config) *Reader {
|
||||
return &Reader{config: config}
|
||||
}
|
||||
|
||||
// FindItems detects item labels on the ground.
|
||||
func (r *Reader) FindItems(frame image.Image) []plugin.DetectedItem {
|
||||
// TODO: Detect colored item text labels
|
||||
// - Gold text = unique
|
||||
// - Green text = set
|
||||
// - Yellow text = rare
|
||||
// - Orange text = runeword / crafted
|
||||
// - White/grey = normal/magic
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindPortal locates a town portal on screen.
|
||||
func (r *Reader) FindPortal(frame image.Image) (image.Point, bool) {
|
||||
// TODO: Detect blue portal glow
|
||||
return image.Point{}, false
|
||||
}
|
||||
|
||||
// FindEnemies detects enemy positions.
|
||||
func (r *Reader) FindEnemies(frame image.Image) []image.Point {
|
||||
// TODO: Enemy health bar detection
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadText extracts text from a screen region.
|
||||
func (r *Reader) ReadText(frame image.Image, region image.Rectangle) string {
|
||||
// TODO: OCR on the given region
|
||||
return ""
|
||||
}
|
||||
135
plugins/d2r/routines/mephisto.go
Normal file
135
plugins/d2r/routines/mephisto.go
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
// Mephisto farming routine for D2R.
|
||||
//
|
||||
// Classic MF run: Create game → WP to Durance 2 → Teleport to Durance 3 →
|
||||
// Moat trick Mephisto → Loot → TP to town → Stash → Exit → Repeat
|
||||
package mephisto
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"git.cloonar.com/openclawd/iso-bot/pkg/plugin"
|
||||
)
|
||||
|
||||
// Phase represents the current phase of a Mephisto run.
|
||||
type Phase string
|
||||
|
||||
const (
|
||||
PhaseCreateGame Phase = "create_game"
|
||||
PhaseTeleport Phase = "teleport_to_durance"
|
||||
PhaseFindBoss Phase = "find_mephisto"
|
||||
PhaseKill Phase = "kill"
|
||||
PhaseLoot Phase = "loot"
|
||||
PhaseTownPortal Phase = "town_portal"
|
||||
PhaseStash Phase = "stash"
|
||||
PhaseExitGame Phase = "exit_game"
|
||||
)
|
||||
|
||||
// Routine implements plugin.Routine for Mephisto runs.
|
||||
type Routine struct {
|
||||
mu sync.RWMutex
|
||||
services plugin.EngineServices
|
||||
phase Phase
|
||||
runCount int
|
||||
}
|
||||
|
||||
// New creates a Mephisto routine.
|
||||
func New(services plugin.EngineServices) *Routine {
|
||||
return &Routine{services: services}
|
||||
}
|
||||
|
||||
// Name returns the routine name.
|
||||
func (r *Routine) Name() string { return "Mephisto" }
|
||||
|
||||
// Phase returns current phase for status display.
|
||||
func (r *Routine) Phase() string {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return string(r.phase)
|
||||
}
|
||||
|
||||
// Run executes one Mephisto run.
|
||||
func (r *Routine) Run(ctx context.Context) error {
|
||||
phases := []struct {
|
||||
phase Phase
|
||||
handler func(ctx context.Context) error
|
||||
}{
|
||||
{PhaseCreateGame, r.createGame},
|
||||
{PhaseTeleport, r.teleportToDurance},
|
||||
{PhaseFindBoss, r.findMephisto},
|
||||
{PhaseKill, r.killMephisto},
|
||||
{PhaseLoot, r.lootItems},
|
||||
{PhaseTownPortal, r.townPortal},
|
||||
{PhaseStash, r.stashItems},
|
||||
{PhaseExitGame, r.exitGame},
|
||||
}
|
||||
|
||||
for _, p := range phases {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
r.phase = p.phase
|
||||
r.mu.Unlock()
|
||||
|
||||
r.services.Log("info", fmt.Sprintf("Phase: %s", p.phase))
|
||||
|
||||
if err := p.handler(ctx); err != nil {
|
||||
return fmt.Errorf("phase %s failed: %w", p.phase, err)
|
||||
}
|
||||
|
||||
r.services.Wait()
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
r.runCount++
|
||||
r.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Routine) createGame(ctx context.Context) error {
|
||||
// TODO: Navigate lobby → create game with randomized name
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Routine) teleportToDurance(ctx context.Context) error {
|
||||
// TODO: Open WP → Durance of Hate 2 → teleport to level 3 entrance
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Routine) findMephisto(ctx context.Context) error {
|
||||
// TODO: Teleport around Durance 3 to find Mephisto
|
||||
// Position for moat trick (safe spot across the moat)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Routine) killMephisto(ctx context.Context) error {
|
||||
// TODO: Cast offensive spells from moat trick position
|
||||
// Monitor health, use potions if needed
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Routine) lootItems(ctx context.Context) error {
|
||||
// TODO: Teleport to body, detect items, pick up per loot filter
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Routine) townPortal(ctx context.Context) error {
|
||||
// TODO: Cast TP, click portal to go to town
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Routine) stashItems(ctx context.Context) error {
|
||||
// TODO: If inventory has items worth stashing, open stash and transfer
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Routine) exitGame(ctx context.Context) error {
|
||||
// TODO: ESC → Save & Exit
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue