hectorgimenez / koolo

Diablo II Resurrected bot written in Go
MIT License
85 stars 56 forks source link

Run statistics are back, improved nova/foh, bosses act correctly detected, pause button fixed, moveto improved, area correction +++ #513

Closed elobo91 closed 1 month ago

elobo91 commented 1 month ago

Some info on Areacorrection : Travincal and Pit are both AreaAware ( pit as an exemple on how to use it) . Travincal will return to adjacent level (travincal) if accidently entered durance 1 . if fails moving back then it will try from town making a portal, using waypoint then returning to last recorded movetocoords position ( wich is council position). The start from town in travincal works with any character except for berserk_barb because of his iskillingcouncil . By returning to town berserk_barb returns that all council are dead so it proceed with horking phase. i didnt have time to look into it but thats only him. work flawless if using adjacentlevel method for him . So basicly you can pause bot with a character while killing council, waypoint anywhere in game then resume bot. it will return exactly where it was and resume killing. I did an exemple for pit. It keep a trace of visited areas and also its progress( like clearlevel1 done, clearlevel 2 done) . so if you pause bot while its clearing pit lvl 1 and waypoint anywhere in game. it will portal town, use waypoint then move back sequentially through visitedareas then resume at the lastaction area( run progress) like pit lvl 1. Exemple if pit lvl 1 was done then you waypoint to another area and resume it will skip the clearing of lvl 1 and move directly to pit lvl 2 and resume clearing there.

At first i wanted areaCorrection to be in action package but i had multiple import cycle not allowed. This is why i let the function in bot.go . i moved the run interface to another package due to import cycle also.

elobo91 commented 1 month ago

step to static has been removed from the kill(monstername) function. Original func (s NovaSorceress) KillIzual() error { m, := s.data.Monsters.FindOne(npc.Izual, data.MonsterTypeNone) = step.SecondaryAttack(skill.StaticField, m.UnitID, 7, step.Distance(8, 13)) s.killMonster(npc.Izual, data.MonsterTypeNone) s.killMonster(npc.Izual, data.MonsterTypeNone) s.killMonster(npc.Izual, data.MonsterTypeNone) s.killMonster(npc.Izual, data.MonsterTypeNone) s.killMonster(npc.Izual, data.MonsterTypeNone) s.killMonster(npc.Izual, data.MonsterTypeNone)

return s.killMonster(npc.Izual, data.MonsterTypeNone)

} New way: func (s NovaSorceress) KillIzual() error { return s.killBossWithStatic(npc.Izual, data.MonsterTypeUnique) } Andariel, Meptistos, Summoner, duriel, Diablo, Baal .. Arent MonsterTypeNone . They are MonsterTypeUnique because they always have same stats without modifiers/auras. Thats the difference with superunique like nihlatak/pindle/countess/boss seals etc . So first we correctly identify the monster type so we dont have to trick the bot to attack it by calling killmethod multiple time. MonsterTypeNone are plain monsters.. Then i created func (s NovaSorceress) killBossWithStatic(bossID npc.ID, monsterType data.MonsterType) wich static boss until % hp defined in settings is reached then proceed with novas. is better then forcing it to cast 7 static before attacking.

The StaticBossHp% is configurable in character settings, i only applied it to nova until you review it( should reuse code for all sorceress). Since its class specific the option only show up when Nova_Sorceress is selected . It has validation depending on the selected difficulty in settings. Hell lowest static hp is 50% , nightmare 33% , normal 1% . the validation prevent user from configuring it unproperly. leaving it at 100% wont static bosses . That doesnt change the behaviour to static normal monsters ( wich also been improved).

todo: Maybe having a second configurable option for static ( other monsters) because now it statics monsters above 60% ( was like that before).

let me know what do you think of the concept .

elobo91 commented 1 month ago

added an option to context to disablepickupitems at any moment. For the moment it is used for Andariel , mephistos and barb while he kills council. Bot is picking up items as they drop . is a good practice but at same time can cause unwanted behaviour. Giving an exemple : when killing andariel, sometimes monsters around her dies before her and they drop many potions or items. doesnt really matter but the bot will stop fighting her to loot those items. It means bot will get massive hit by all monsters still around and by andariel. By locking the itempickup until andariel is dead and then unlocking it . it prevent this.

Todo : add it to nihlatak and possibly to council, shenk (not only barb) . also think about a way on how this could be used in runs like cowlevel to prevent character from looting useless items when there is stack of monsters surrounding her. I think setting a priority for things like High runes ( loot it as it drop) for other things disable item pickup while clearing, enable after its safe.

elobo91 commented 1 month ago

Sometimes bot iddle in a zone at any moment. This is caused by shouldbepickedup, pickupitems, getitemstopickup. It idle for up to 15 seconds . When pressing E bot resume so I tried to add

// Check if character is standing idle outside town if ctx.Data.PlayerUnit.Mode == mode.StandingOutsideTown { idleCounter++ if idleCounter >= maxIdleCount { ctx.Logger.Debug("Character idle for too long, forcing movement") ctx.HID.PressKeyBinding(ctx.Data.KeyBindings.ForceMove) idleCounter = 0 continue } } else { idleCounter = 0 }

but then i realised it wasnt only happening in pickupitems but at 3 different places. I applied the anti-idle to all 3 but that made the pickupitems delayed. Have to find a better way to call the anti-idle . ctx.Data.PlayerUnit.Mode == mode.StandingOutsideTown is the key._

elobo91 commented 1 month ago

Berserker Specific options. Now FindItemSwitch option only appears when berserk_barb is the current profile. Also added an option to skippotionspickupduringtravincal . is waste of time because it will refill next game anyway. This can be reused for any class specific options ( only show up when that profile is selected). Nova is an exemple.

elobo91 commented 1 month ago

FoH is now having 2 different attack sequences ( well 2 and a half). For boss will cast 1 FOH followed by 3 holy bolt (repeat till target is dead).

For general monsters, it will first look for imunity then check as the attacksequence is ongoing if its no longer immune due to State.Conviction applied on monsters. Depending if still imune will cast holybolt or FOH.

Todo : make him a little bit safer, it moves to close to monsters.

elobo91 commented 1 month ago

move.go had a slight modification for movetoarea that involve interacting with entrance. it would fail 1/2 time.

if lvl.IsEntrance {
    maxAttempts := 3
    for attempt := 0; attempt < maxAttempts; attempt++ {
        // Add a short delay before each attempt
        utils.Sleep(200)

        err := step.InteractEntrance(dst)
        if err == nil {
            // Successful interaction, exit the loop
            break
        }

        if attempt == maxAttempts-1 {
            // If this was the last attempt, return the error
            return fmt.Errorf("failed to interact with entrance after %d attempts: %w", maxAttempts, err)
        }

    }

    // Add a short delay after successful interaction
    utils.Sleep(200)
}

now have a retry mechanism. solving the issue

elobo91 commented 1 month ago

For run statistics i tried to keep like it was before as much as i could but that wasnt working at all. run statistics were not updating.

now we have func (s *SinglePlayerSupervisor) updateRunStats(runName string, startTime, endTime time.Time, reason event.FinishReason) { event.Send(event.RunStatsUpdated(event.Text(s.name, fmt.Sprintf("Updated stats for run: %s", runName)), runName, startTime, endTime, reason)) } calling a new event in events.go .

i added to bot.go in Low priority loop to update run statistics otherwize no statistics other then gamefinished.

var runFinishReason event.FinishReason if err != nil { switch { case errors.Is(err, health.ErrChicken): runFinishReason = event.FinishedChicken case errors.Is(err, health.ErrMercChicken): runFinishReason = event.FinishedMercChicken case errors.Is(err, health.ErrDied): runFinishReason = event.FinishedDied default: runFinishReason = event.FinishedError } } else { runFinishReason = event.FinishedOK }

        event.Send(event.RunFinished(event.Text(b.ctx.Name, fmt.Sprintf("Finished run: %s", r.Name())), r.Name(), runFinishReason))