Files
autobrew/alchemy/alchemy.go
2026-01-02 05:00:07 +01:00

444 lines
9.0 KiB
Go

package alchemy
import (
"autobrew/action"
"autobrew/container"
"autobrew/ingredient"
"autobrew/recipe"
"autobrew/utils"
"bytes"
"fmt"
"image"
"image/png"
"os"
"path/filepath"
"slices"
"strings"
"time"
"github.com/MaxHalford/halfgone"
"github.com/bendahl/uinput"
"github.com/otiai10/gosseract/v2"
"github.com/vova616/screenshot"
"github.com/yassinebenaid/godump"
)
type Alchemy struct {
keyboard uinput.Keyboard
mouse uinput.Mouse
searchRect image.Rectangle
save bool
}
func NewAlchemy(mouse uinput.Mouse, keyboard uinput.Keyboard, searchRect image.Rectangle, save bool) *Alchemy {
return &Alchemy{
mouse: mouse,
keyboard: keyboard,
searchRect: searchRect,
save: save,
}
}
func (a Alchemy) findString(word string) (out gosseract.BoundingBox, err error) {
img, err := screenshot.CaptureRect(a.searchRect)
utils.Check(err)
gray := halfgone.ImageToGray(img)
gray = halfgone.ThresholdDitherer{Threshold: 85}.Apply(gray)
buf := new(bytes.Buffer)
err = png.Encode(buf, gray)
utils.Check(err)
client := gosseract.NewClient()
defer client.Close()
client.SetImageFromBytes(buf.Bytes()) // use from screen frame
if a.save {
path1 := filepath.Join(".", "output.png")
err = os.WriteFile(path1, buf.Bytes(), 0644)
utils.Check(err)
}
bounding_boxes, err := client.GetBoundingBoxes(2)
utils.Check(err)
index := slices.IndexFunc(bounding_boxes, func(e gosseract.BoundingBox) bool {
a := strings.ToLower(e.Word)
b := strings.ToLower(word)
return strings.Contains(a, b)
})
if index < 0 {
text, err := client.Text()
utils.Check(err)
// godump.Dump(bounding_boxes)
godump.Dump(text)
return out, fmt.Errorf("%s not found\n", word)
}
out = bounding_boxes[index]
return
}
func (a Alchemy) sleep(msec time.Duration) {
time.Sleep(time.Duration(msec * time.Millisecond))
}
func (a Alchemy) use() {
fmt.Println("Pressing E")
a.keyboard.KeyPress(uinput.KeyE)
a.sleep(200)
}
func (a Alchemy) hold() {
fmt.Println("Holding E")
a.keyboard.KeyDown(18)
a.sleep(1000)
a.keyboard.KeyUp(18)
a.sleep(200)
}
func (a Alchemy) lookAt(str string) {
a.mouse.Move(-1200, -550)
a.sleep(200)
// fmt.Printf("Looking at %s\n", str)
switch strings.ToLower(str) {
case "water":
a.mouse.Move(0, 150)
case "wine":
a.mouse.Move(100, 150)
case "oil":
a.mouse.Move(200, 150)
case "spirits":
a.mouse.Move(300, 150)
case "phial":
a.mouse.Move(105, 320)
a.sleep(2500) // make sure we're actually looking at it
case "still":
a.mouse.Move(200, 460)
case "dish":
a.mouse.Move(600, 600)
case "mortar":
a.mouse.Move(850, 500)
case "cauldron":
a.mouse.Move(530, 300)
case "topshelf_0":
a.mouse.Move(1000, 150)
case "topshelf_1":
a.mouse.Move(900, 150)
case "topshelf_2":
a.mouse.Move(760, 150)
case "bottomshelf_0":
a.mouse.Move(980, 350)
case "bottomshelf_1":
a.mouse.Move(900, 350)
case "bottomshelf_2":
a.mouse.Move(780, 350)
}
a.sleep(100)
}
func (a Alchemy) OpenInventory() {
fmt.Println("Opening Inventory")
a.keyboard.KeyPress(uinput.KeyI)
a.sleep(400)
}
func (a Alchemy) SelectIngredient(str ingredient.Ingredient) {
rect, err := a.findString(str.Name())
utils.Check(err)
x := rect.Box.Min.X + (rect.Box.Bounds().Size().X / 2)
y := rect.Box.Min.Y + (rect.Box.Bounds().Size().Y / 2)
a.mouse.Move(-2560, -1440)
a.sleep(100)
// fmt.Printf("Selecting %s at %dx%d\n", str.Name(), x, y)
a.mouse.Move(int32(x), int32(y))
a.sleep(50)
a.mouse.LeftClick()
a.mouse.LeftClick()
a.sleep(50)
}
func (a Alchemy) CloseInventory() {
fmt.Println("Closing Inventory")
a.keyboard.KeyPress(uinput.KeyEsc)
a.sleep(300)
}
func (a Alchemy) ClosePopup() {
fmt.Println("Closing Popup")
a.keyboard.KeyPress(uinput.KeyEsc)
a.sleep(300)
}
func (a Alchemy) PourBase(i ingredient.Ingredient) {
a.lookAt(i.Name())
a.sleep(100)
a.use()
a.sleep(7980)
}
func (a Alchemy) GrabIngredient(i ingredient.Ingredient, shelf []ingredient.Ingredient) {
// fmt.Printf("Looking for %s\n", i.Name())
bottomShelf := utils.Filter(shelf, func(i ingredient.Ingredient) bool {
return i.BottomShelf()
})
topShelf := utils.Filter(shelf, func(i ingredient.Ingredient) bool {
return !i.BottomShelf()
})
topIndex := slices.Index(topShelf[:], i)
if topIndex > -1 {
// fmt.Printf("Found %s on the top shelf at index %d\n", i.Name(), topIndex)
a.sleep(1000)
switch topIndex {
case 0:
a.lookAt("topshelf_0")
case 1:
a.lookAt("topshelf_1")
case 2:
a.lookAt("topshelf_2")
}
} else {
bottomIndex := slices.Index(bottomShelf[:], i)
if bottomIndex == -1 {
return
}
// fmt.Printf("Found %s on the bottom shelf at index %d\n", i.Name(), bottomIndex)
switch bottomIndex {
case 0:
a.lookAt("bottomshelf_0")
case 1:
a.lookAt("bottomshelf_1")
case 2:
a.lookAt("bottomshelf_2")
}
}
a.sleep(500)
a.use()
a.sleep(5000)
}
func (a Alchemy) DropIn(c container.Container) {
switch c {
case container.Cauldron:
a.lookAt("cauldron")
case container.Dish:
a.lookAt("dish")
case container.Mortar:
a.lookAt("mortar")
default:
panic(fmt.Sprintf("unexpected container.Container: %#v", c))
}
a.sleep(100)
a.use()
a.sleep(5500)
}
func (a Alchemy) Grind() {
a.lookAt("mortar")
a.use()
a.sleep(10000)
}
func (a Alchemy) Boil(turns int) {
a.lookAt("cauldron")
a.sleep(100)
a.keyboard.KeyPress(uinput.KeyX)
a.sleep(5000)
for range turns {
// fmt.Printf("Boiling for %d turns\n", turns-i)
a.sleep(9000)
}
a.keyboard.KeyPress(uinput.KeyX)
a.sleep(5000)
}
func (a Alchemy) BoilBellows(turns int) {
// var duration time.Duration = 10
a.lookAt("cauldron")
a.sleep(100)
a.keyboard.KeyPress(uinput.KeyX)
a.sleep(5000) // 4+1 seconds to lower the cauldron and one for pre-boil
for range turns {
// fmt.Printf("Bellow Boiling for %d turns\n", turns-i)
for range 18 * 2 {
a.keyboard.KeyPress(uinput.KeyQ) // tap q every 500ms
a.sleep(250) // boil time, 18*500 makes 9000 again
}
}
a.sleep(2000)
a.keyboard.KeyPress(uinput.KeyX)
a.sleep(5000) // 5 seconds to pull it back up and stop animating
}
func (a Alchemy) PreparePhial() {
a.lookAt("phial")
a.use()
a.sleep(2000)
}
func (a Alchemy) Distill() {
a.lookAt("still")
a.hold()
a.sleep(16000)
}
func (a Alchemy) PourOut() {
a.lookAt("cauldron")
a.hold()
a.sleep(8000)
}
func (a Alchemy) GrindOut() {
a.lookAt("mortar")
a.hold()
a.sleep(15000)
}
func (a Alchemy) innerBrew(r recipe.Recipe) {
for _, step := range r.Steps {
switch step.Action {
case action.Prepare:
ing := utils.DedupeSlice(step.Ingredients)
base := utils.Filter(ing, func(i ingredient.Ingredient) bool {
return i.IngredientType == ingredient.Base
})
if len(base) == 0 {
panic("Recipe is missing a base")
}
a.OpenInventory()
for _, i := range ing {
if i.IngredientType != ingredient.Base {
// r.Ingredients = append(r.Ingredients, i)
a.SelectIngredient(i)
}
}
a.CloseInventory()
a.PourBase(base[0])
case action.AddToCauldron:
for _, i := range step.Ingredients {
a.GrabIngredient(i, r.Ingredients)
a.DropIn(container.Cauldron)
}
case action.Boil:
for _, i := range step.Ingredients {
a.GrabIngredient(i, r.Ingredients)
a.DropIn(container.Cauldron)
}
a.Boil(step.Turns)
case action.BoilBellows:
for _, i := range step.Ingredients {
a.GrabIngredient(i, r.Ingredients)
a.DropIn(container.Cauldron)
}
a.BoilBellows(step.Turns)
case action.GrindIngredients:
for _, i := range step.Ingredients {
a.GrabIngredient(i, r.Ingredients)
a.DropIn(container.Mortar)
}
a.Grind()
a.DropIn(container.Cauldron)
// finishers
case action.PourPhial:
a.PreparePhial()
a.PourOut()
case action.Distill:
a.PreparePhial()
a.Distill()
case action.GrindPotion:
a.GrindOut()
default:
panic(fmt.Sprintf("unexpected main.StepAction: %#v", step.Action))
}
}
}
func (a Alchemy) Brew(book map[string][]recipe.Step, name string, amount int) {
r := recipe.Recipe{
Steps: book[name],
}
r.LoadIngredients()
ingredients := make(map[string]int)
for _, i := range r.Ingredients {
ingredients[i.Name()]++
}
formatted := make([]string, 0)
for k, v := range ingredients {
formatted = append(formatted, fmt.Sprintf("%dx %s", v, k))
}
fmt.Printf("%s\n\nIngredients:\n\t%s\n\n", name, strings.Join(formatted, "\n\t"))
a.sleep(1000)
totalStartTime := time.Now()
if amount == -1 {
i := 0
for {
fmt.Printf("Start brewing %s in 3\n", name)
a.sleep(1000)
fmt.Printf("Start brewing %s in 2\n", name)
a.sleep(1000)
fmt.Printf("Start brewing %s in 1\n", name)
a.sleep(1000)
startTime := time.Now()
a.innerBrew(r)
fmt.Printf("Batch %d took %s, total %s\n", i+1, time.Since(startTime), time.Since(totalStartTime))
a.ClosePopup()
i++
}
}
for i := range amount {
fmt.Printf("Start brewing %s in 3\n", name)
a.sleep(1000)
fmt.Printf("Start brewing %s in 2\n", name)
a.sleep(1000)
fmt.Printf("Start brewing %s in 1\n", name)
a.sleep(1000)
startTime := time.Now()
a.innerBrew(r)
fmt.Printf("Batch %d took %s, total %s\n", i+1, time.Since(startTime), time.Since(totalStartTime))
a.ClosePopup()
}
}