Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added CancelOnInterrupt option when the KeyAction.Interrupt condition is met #649

Merged
merged 1 commit into from
Dec 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Core/ClassConfig/ActionMask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ public static class ActionMask
public const int AfterCastWaitCombat = 1 << 14;
public const int AfterCastWaitGCD = 1 << 15;
public const int AfterCastAuraExpected = 1 << 16;
public const int CancelOnInterrupt = 1 << 17;
}
7 changes: 7 additions & 0 deletions Core/ClassConfig/KeyAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,13 @@ public bool AfterCastAuraExpected
get => features[ActionMask.AfterCastAuraExpected];
set => features[ActionMask.AfterCastAuraExpected] = value;
}

public bool CancelOnInterrupt
{
get => features[ActionMask.CancelOnInterrupt];
set => features[ActionMask.CancelOnInterrupt] = value;
}

public int AfterCastStepBack { get; set; }

public string InCombat { get; set; } = "false";
Expand Down
8 changes: 8 additions & 0 deletions Core/GoalsComponent/CastingHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,14 @@ public bool Cast(KeyAction item, Func<bool> interrupt)
{
if (result != CastResult.UIError)
{
if (result == CastResult.TokenInterrupted &&
item.CancelOnInterrupt)
{
input.PressESC();
wait.Fixed(playerReader.NetworkLatency);
wait.Update();
}

LogFailedDueReason(logger, item.Name, result.ToStringF());
return false;
}
Expand Down
4 changes: 4 additions & 0 deletions Core/Input/ConfigurableInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ public void PressDismount()

public void PressFollowTarget() => PressRandom(FollowTarget);

public void PressESC()
{
input.PressRandom(ConsoleKey.Escape, InputDuration.VeryFastPress);
}

#region Logging

Expand Down
38 changes: 37 additions & 1 deletion Core/Requirement/RequirementFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ public RequirementFactory(IServiceProvider sp, ClassConfiguration classConfig)
{ "Spell", CreateSpell },
{ "Talent", CreateTalent },
{ "Trigger:", CreateTrigger },
{ "Usable:", CreateUsable }
{ "Usable:", CreateUsable },
{ "CanRun:", CreateCanRun }
};
this.requirementMap = requirementMap.ToFrozenDictionary();

Expand Down Expand Up @@ -780,6 +781,17 @@ string s() =>
};
}

private Requirement CreateActionCanRun(KeyAction item)
{
bool f() => item.CanRun();
string s() => $"CanRun:{item.Name}";
return new Requirement
{
HasRequirement = f,
LogMessage = s
};
}

private Requirement CreateActionCurrent(KeyAction item,
ActionBarBits<ICurrentAction> currentAction)
{
Expand Down Expand Up @@ -1103,6 +1115,30 @@ private Requirement CreateUsable(ReadOnlySpan<char> requirement)
$"related named '{name}' {nameof(Core.KeyAction)} not found!");
}

private Requirement CreateCanRun(ReadOnlySpan<char> requirement)
{
// 'CanRun:_KeyAction_Name_'
int sep = requirement.IndexOf(SEP1);
ReadOnlySpan<char> name = requirement[(sep + 1)..].Trim();

List<(string _, KeyActions)> groups = classConfig.GetByType<KeyActions>();

foreach ((string _, KeyActions keyActions) in groups)
{
foreach (KeyAction keyAction in keyActions.Sequence)
{
if (name.SequenceEqual(keyAction.Name))
{
return CreateActionCanRun(keyAction);
}
}
}

throw new InvalidOperationException($"'{requirement}' " +
$"related named '{name}' {nameof(Core.KeyAction)} not found!");
}


private Requirement CreateGreaterThen(ReadOnlySpan<char> requirement)
{
return CreateArithmetic(greaterThen, requirement, intVariables);
Expand Down
98 changes: 98 additions & 0 deletions Json/class/Shaman_60_elemental_interrupt.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
{
"ClassName": "Shaman",
"Mode": "AttendedGrind",
"Loot": true,
"Skin": false,
"PathFilename": "_pack\\50-60\\Azshara\\50 - 53 Southridge Beach (mermaids).json",
"PathThereAndBack": false,
"PathReduceSteps": true,
"NPCMaxLevels_Below": 20,
"NPCMaxLevels_Above": 2,
"Mount": {
"Key": "N0"
},
"IntVariables":{
"Earth_Shock_MIN_HP%": 25,
"Healing_Wave_MIN_HP%": 20,
"Drink_MIN_MANA%": 20,
"Food_MIN_HP%": 20
},
"Pull": {
"Sequence": [
{
"Name": "Chain Lightning",
"Key": "2",
"HasCastBar": true,
"Requirement": "Clearcasting"
},
{
"Name": "Lightning Bolt",
"Key": "1",
"HasCastBar": true,
"Requirement": "!Clearcasting",
"Interrupt": "Clearcasting",
"AfterCastWaitCastbar": true,
"CancelOnInterrupt": true
}
]
},
"Combat": {
"Sequence": [
{
"Name": "Chain Lightning",
"Key": "2",
"HasCastBar": true,
"Requirement": "Clearcasting"
},
{
"Name": "Earth Shock",
"Key": "3",
"Requirements": [
"SpellInRange:1",
"TargetAlive && TargetHealth% < Earth_Shock_MIN_HP%"
]
},
{
"Name": "Lightning Bolt",
"Key": "1",
"HasCastBar": true,
"AfterCastWaitCastbar": true,
"Requirement": "!Clearcasting",
"Interrupt": "Clearcasting || CanRun:Earth Shock",
"CancelOnInterrupt": true
}
]
},
"Adhoc": {
"Sequence": [
{
"Name": "Lightning Shield",
"Key": "7",
"Requirement": "!Lightning Shield"
},
{
"Cost": 3,
"Name": "Healing Wave",
"HasCastBar": true,
"Key": "F1",
"Requirements": [
"Health% < Healing_Wave_MIN_HP%"
]
}
]
},
"Parallel": {
"Sequence": [
{
"Name": "Drink",
"Key": "-",
"Requirement": "Mana% < Drink_MIN_MANA%"
},
{
"Name": "Food",
"Key": "=",
"Requirement": "Health% < Food_MIN_HP%"
}
]
}
}
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,7 @@ Can specify conditions with [Requirement(s)](#requirement) in order to create a
| `"Requirements"` | List of [Requirement](#requirement) | `false` |
| `"Interrupt"` | Single [Requirement](#requirement) | `false` |
| `"Interrupts"` | List of [Requirement](#requirement) | `false` |
| `"CancelOnInterrupt"` | If the [Interrupt](#interrupt-requirement) [Requirement](#requirement) has met, shall **Cancel** current castbar spellcast (sending ESC) | `false` |
| `"ResetOnNewTarget"` | Reset the Cooldown if the target changes | `false` |
| `"Log"` | Related events should appear in the logs | `true` |
| --- | Before keypress cast, ... | --- |
Expand Down Expand Up @@ -1624,6 +1625,25 @@ e.g.
"FBuff_Mount": 132239
},
```
---
### **CanRun requirements**

Formula: `CanRun:_KeyAction_Name_`

Where the `_KeyAction_Name_` is one of the [KeyAction.Name](#keyaction).

It is suitable for [Interrupt Requirement](#interrupt-requirement), for such scenario:
* The player already casting a `Castbar` based spell which has a long cast time like 2-3 seconds
* However meanwhile a higher priority action can be used, which would be beneficial.
* Like: A mob is in low hp, so an instant cast spell such as `Earth Shock`, `Fireblast` would execute the enemy.

e.g.
```json
"Requirement": "CanRun:Frost Shock"
"Requirement": "CanRun:Fire Blast"
"Requirement": "!CanRun:Heartstone"
```

---
### **Trigger requirements**

Expand Down Expand Up @@ -2038,6 +2058,36 @@ This **500**ms duration is the reload animation time, while the player has to st
}
```
---

Execute low hp enemy instead of awaitning the currently casted spell.

The key takeaway here is that the `Earth Shock` spell has higher priority.

```json
"Combat": {
"Sequence": [
//...
{
"Name": "Earth Shock",
"Key": "3",
"Requirements": [
"SpellInRange:1",
"TargetAlive && TargetHealth% < 20"
]
},
{
"Name": "Lightning Bolt",
"Key": "1",
"HasCastBar": true,
"AfterCastWaitCastbar": true,
"Requirement": "!Clearcasting",
"Interrupt": "Clearcasting || CanRun:Earth Shock",
"CancelOnInterrupt": true
}
]
}
```
---
# Modes

The default mode for the bot is to grind, but there are other modes. The mode is set in the root of the class file.
Expand Down