// Game state detection for D2R. package d2r import ( "image" "git.cloonar.com/openclawd/iso-bot/pkg/plugin" "git.cloonar.com/openclawd/iso-bot/pkg/engine/vision" ) // Detector implements plugin.GameDetector for D2R. type Detector struct { config Config services plugin.EngineServices vision *vision.Pipeline } // NewDetector creates a D2R state detector. func NewDetector(config Config, services plugin.EngineServices) *Detector { return &Detector{ config: config, services: services, vision: vision.NewPipeline(0.8), // 80% confidence threshold } } // 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 in-game first (health orb visible) // 2. Check for loading screen (but only if not in-game) // 3. Check for main menu // 4. Check for character select // 5. Check for death screen if d.isInGame(frame) { vitals := d.ReadVitals(frame) if vitals.HealthPct <= 0.01 { // Consider very low health as dead return plugin.StateDead } return plugin.StateInGame } if d.isLoading(frame) { return plugin.StateLoading } if d.isMainMenu(frame) { return plugin.StateMainMenu } if d.isCharacterSelect(frame) { return plugin.StateCharacterSelect } return plugin.StateUnknown } // ReadVitals reads health and mana from the orbs. func (d *Detector) ReadVitals(frame image.Image) plugin.VitalStats { healthRegion := d.services.Region("health_orb") manaRegion := d.services.Region("mana_orb") var healthPct, manaPct float64 // Read health percentage from red-filled pixels in health orb if !healthRegion.Empty() { healthColor := vision.ColorRange{ LowerH: d.config.Colors.HealthFilled.LowerH, UpperH: d.config.Colors.HealthFilled.UpperH, LowerS: d.config.Colors.HealthFilled.LowerS, UpperS: d.config.Colors.HealthFilled.UpperS, LowerV: d.config.Colors.HealthFilled.LowerV, UpperV: d.config.Colors.HealthFilled.UpperV, } healthPct = d.vision.ReadOrbPercentage(frame, healthRegion, healthColor) } // Read mana percentage from blue-filled pixels in mana orb if !manaRegion.Empty() { manaColor := vision.ColorRange{ LowerH: d.config.Colors.ManaFilled.LowerH, UpperH: d.config.Colors.ManaFilled.UpperH, LowerS: d.config.Colors.ManaFilled.LowerS, UpperS: d.config.Colors.ManaFilled.UpperS, LowerV: d.config.Colors.ManaFilled.LowerV, UpperV: d.config.Colors.ManaFilled.UpperV, } manaPct = d.vision.ReadOrbPercentage(frame, manaRegion, manaColor) } return plugin.VitalStats{ HealthPct: healthPct, ManaPct: manaPct, XPPct: 0.0, // TODO: Implement XP bar reading } } // 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 { // Check for loading screen by looking for mostly black screen (>90%) // AND ensuring no health orb colors are present in the health orb region bounds := frame.Bounds() totalPixels := 0 darkPixels := 0 // Sample every 10 pixels for performance step := 10 for y := bounds.Min.Y; y < bounds.Max.Y; y += step { for x := bounds.Min.X; x < bounds.Max.X; x += step { c := frame.At(x, y) r, g, b, _ := c.RGBA() // Convert to 8-bit r, g, b = r>>8, g>>8, b>>8 totalPixels++ // Consider pixel dark if all channels are below 30 if r < 30 && g < 30 && b < 30 { darkPixels++ } } } if totalPixels == 0 { return false } // Require more than 90% dark screen darkPercentage := float64(darkPixels) / float64(totalPixels) if darkPercentage <= 0.9 { return false } // Additional check: ensure no health colors in health orb region healthRegion := d.services.Region("health_orb") if !healthRegion.Empty() { healthColor := vision.ColorRange{ LowerH: d.config.Colors.HealthFilled.LowerH, UpperH: d.config.Colors.HealthFilled.UpperH, LowerS: d.config.Colors.HealthFilled.LowerS, UpperS: d.config.Colors.HealthFilled.UpperS, LowerV: d.config.Colors.HealthFilled.LowerV, UpperV: d.config.Colors.HealthFilled.UpperV, } // If health colors are detected in health orb region, it's not loading if d.vision.HasColorInRegion(frame, healthRegion, healthColor) { return false } } return true } func (d *Detector) isMainMenu(frame image.Image) bool { // TODO: Template match main menu elements or check for specific colors/text // For now, this is a placeholder return false } func (d *Detector) isCharacterSelect(frame image.Image) bool { // TODO: Template match character select screen // For now, this is a placeholder return false } func (d *Detector) isInGame(frame image.Image) bool { // Check if health orb region contains red pixels indicating health healthRegion := d.services.Region("health_orb") if healthRegion.Empty() { return false } healthColor := vision.ColorRange{ LowerH: d.config.Colors.HealthFilled.LowerH, UpperH: d.config.Colors.HealthFilled.UpperH, LowerS: d.config.Colors.HealthFilled.LowerS, UpperS: d.config.Colors.HealthFilled.UpperS, LowerV: d.config.Colors.HealthFilled.LowerV, UpperV: d.config.Colors.HealthFilled.UpperV, } return d.vision.HasColorInRegion(frame, healthRegion, healthColor) }