iso-bot/plugins/d2r/detector.go

183 lines
5.2 KiB
Go

// 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_slice")
manaRegion := d.services.Region("mana_slice")
var healthPct, manaPct float64
// Read health percentage from red-filled pixels in health slice
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.ReadOrbSlicePercentage(frame, healthRegion, healthColor)
}
// Read mana percentage from blue-filled pixels in mana slice
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.ReadOrbSlicePercentage(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)
}