From 8ca6e42f108468c509a485f5d3d7c9d9447af543 Mon Sep 17 00:00:00 2001 From: ThePenguinMan96 Date: Sat, 23 May 2026 11:42:08 -0700 Subject: [PATCH] Druid Overhaul (#2392) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit druids Hello playerbots community!! After my pvp gear update, I was itching to get back to class strategies. I was testing raids, and noticed that druids... Well, they kinda sucked. They had glaring issues, would randomly die from bugs, and overall felt bad. Looking at issues discussed on github/discord, I decided to start working on a druid update. I started with the boomkin, and made a boomkin PR, but there were some technical issues with the storing of the eclipse mapping not clearing, and it was not good... I closed that PR, and went back to the drawing board, with one goal in mind: Make the druid class function as best as possible WHILST keeping the code consistent with what already exists. There is a TON of _yoink and twist_ (copy and paste with or without slight edits), and anything that is custom/new is discussed in the section below. I am very proud and excited to release this though - after 45 days of coding and testing, the druid finally feels good to have in the group. Disclaimer - this PR aims to address bugs, utility, and overall performance of Druids. It will not magically make them top DPS. I have done hours of testing, and druids can occasionally top the charts - but inconsistently. Boomkins are inconsistent due to Eclipse (until later gear phases), and Feral Cats are incredibly dependent on positioning, timing, combo points, energy, clearcasting procs... The stars have to align for things to go right for the Cat (It doesn't help that all movement, targeting, and actions are ran through the same engine, so cats really suffer in boss fights with scripted movement) But the changes I have made help those situations occur a fair bit more frequently. Let's dive in! ## Pull Request Description A druid overhaul across all four specs — new Cat stealth and CC systems, Eclipse tracking and rotation fixes for Balance, a Bear threat rotation rework, a Resto healing priority overhaul, and a restructured CC and AoE strategy architecture. --- ## Talent & Glyph Config - **Balance:** Moved points out of Improved Moonfire and into Nature's Reach for threat reduction and extended cast range. - **Bear:** Moved points out of King of the Jungle (15% enrage damage) and into Feral Instinct for increased Swipe damage and AoE threat. - **Cat:** Swapped the Glyph of Typhoon (useless for feral) for Glyph of Dash, which benefits the cat a ton in prowl. - **Resto:** Swapped Glyph of Rejuvenation for Glyph of Nourish (better HPS for difficult content), and Glyph of Typhoon (useless for resto) for Glyph of Dash. Moved one point from Nature's Bounty to Empowered Touch. --- ## Balance - **New skill - Typhoon:** Added Typhoon, triggered by "enemy within melee," using cone targeting logic ported from Cone of Cold. Typhoon is from the "balance pvp" spec only, and is housed in the shared AoE strategy. - **New skill - Cyclone:** Added Cyclone targeting the RTI CC-marked target (default moon). Incapacitates for 6 seconds with full damage immunity — cannot be broken by AoE. Priority 24.0f > Hibernate (23.0f) > Entangling Roots (22.0f). *(See CC Implementation in the code notes below.)* - **Bug fix - Eclipse cooldown tracking:** Eclipse procs referenced a cooldown not registered in the database, so boomkins immediately reverted to the wrong filler after an Eclipse buff fell off. Fixed with manual timestamp tracking. *(See Eclipse Cooldown Tracking in the code notes below.)* - **Bug fix — Starfall no longer pulls out-of-combat hostile enemies:** Previously fired on cooldown regardless of surroundings — its 36-yard radius (the largest AoE in the game) could silently pull entire unengaged packs. Now suppressed if any non-combat hostile NPC is within 40 yards. *(See Starfall Pull Safety in the code notes below.)* - **Filler changed to Starfire:** Starfire is now the filler (priority 5.4) over Wrath (5.3). Starfire has a 100% spellpower coefficient vs. Wrath's 12%, is more mana-efficient per point of damage, and has a 100% eclipse proc chance on crit vs. Wrath's 40%. In practice this significantly reduced mana consumption, especially before level 40 when Moonkin Form's mana-on-crit passive isn't available. - **Moonfire / Insect Swarm on Attacker rework:** Previously tied to the light AoE trigger (2+ enemies, 13.0f), causing low-level boomkins to multi-dot instead of casting fillers — mana-inefficient at low levels where targets rarely live long enough to tick the full DoT. Re-added as dedicated on-attacker triggers at lower priority than the fillers, so they fire only as a movement fallback. The triggers have been changed to override the TTL check (time to life), similar to the warlocks DoTs. - **Hurricane channel check rework:** Previously cancelled based on distance from the bot — enemies could leave the AoE but remain within 30 yards, keeping the channel alive. Now reads the Hurricane DynamicObject's actual radius and counts only attackers physically inside it. *(See Hurricane Channel Cancel in the code notes below.)* --- ## Cat - **Prowl (Stealth):** Implemented using the same logic as the existing Rogue stealth system. The bot enters Prowl when out of combat and a target is within range. Engagement distances are: - 30 yards baseline - −10 yards if the target already has a victim (engaged in combat) - −10 more yards if the target is also moving (minimum 10 yards) - +15 yards in Battlegrounds or Arenas - Enemy player targets take priority over grind/DPS targets when evaluating distance. - **Prowl openers:** The bot approaches the target in Prowl and opens based on approach angle and level: - **From behind:** Ravage (learned at level 32). Before Ravage is learned, Shred is used as the opener. - **From the front:** Pounce (stun + bleed, learned at level 36). Before Pounce is learned, Claw is used as the opener. - **New skill - Maim:** Added Maim as a 5 second stun-finisher at 5 combo points, but only against player targets. It will only fire when Rip and Savage Roar are already active. - **Innervate on healer:** Cats now cast Innervate when a healer drops below the low mana threshold (`AiPlayerbot.LowMana`, default 15%). *(See Healer Low Mana Framework in the code notes below for the shared value/trigger infrastructure backing this.)* - **Predator's Swiftness with CC spells:** Added a twotrigger pairing Predator's Swiftness (the instant-cast proc from finishing moves) with the existing CC triggers (Cyclone, Hibernate, Entangling Roots). Feral cats can now instant-cast CC the RTI CC-marked target (default moon) after a finisher using the Predator's Swiftness proc. - **Predator's Swiftness with Rebirth:** Added a twotrigger pairing Predator's Swiftness with the combat resurrection trigger. Cats can now use a Predator's Swiftness proc to instantly cast Rebirth on a dead party member. - **Bug fix - Autoattack no longer breaks prowl:** `MeleeAction::isUseful()` now returns false while the bot has the Prowl aura, preventing autoattack from breaking stealth before an opener spell fires. The code comment notes this pattern should be reused for a future Rogue autoattack in stealth fix. - **Bug fix - Non-prowl skills no longer break prowl:** `isUseful()` overrides were added to Feral Charge (Cat), Mangle (Cat), Swipe (Cat), Rake to return false while Prowl is active, preventing accidental prowl breaks before the opener fires. - **Bug fix - Clearcasting proc with energy spells:** Added dedicated `ClearcastingTrigger` / action pairings to ensure Clearcasting procs are consumed immediately. On single target, Shred is used; on AoE, Swipe (Cat) is used. This prevents the cat from using it's valuable clearcasting proc on a low energy spell (rake, feral charge - cat, cower, etc). This also fixes a bug where Clearcasting would sometimes linger for 4+ seconds without being used, as energy-based spells do not recognize the free cast and would not fire until their energy condition was met. - **Tiger's Fury rework:** TF previously fired on cooldown as a default action, meaning the bot would use it at full or near-full energy and gain no benefit from the energy it generates. The trigger now requires energy to be below 30 before firing, ensuring the bot recovers the full 60 energy granted by the King of the Jungle talent. - **Faerie Fire (Feral) rework for cat:** With Omen of Clarity, spams on cooldown to fish for Clearcasting procs. Without it, applies as a normal debuff and does not reapply while active. *(See Faerie Fire (Feral) Trigger in the code notes below.)* - **Feral Charge toggleable strategy:** Feral Charge (Cat) is now housed in a dedicated `feral charge` strategy, enabled by default for Cat druids. It can be disabled with `co -feral charge` and re-enabled with `co +feral charge`. Disabling this strategy can be useful for encounters where charging in would be unfavorable. - **Rake on Melee Attackers removed:** The Rake on Melee Attackers action was removed from the Cat AoE strategy. Applying Rake to multiple attackers and spreading out combo points produced far lower AoE DPS than simply continuing the single-target rotation. - **Antiquated Omen of Clarity framework removed:** `OmenOfClarityTrigger` and `CastOmenOfClarityAction` were not functional and have been removed. The `ClearcastingTrigger` appropriately tracks Omen of Clarity procs. I believe this was from TBC when Omen of Clarity was a spell. --- ## Bear - **Berserk tracking for Mangle:** A `berserk active` trigger fires Mangle (Bear) at priority 25.0f while Berserk is up. Previously, Mangle sat at 5.5f in the default actions while Swipe (Bear) sat at 25.0f on the light AoE trigger — meaning in any 2+ enemy encounter, Swipe would always win regardless of Berserk. Now Mangle (25.0f) sits above the AoE triggers (24.5f), so it takes priority during Berserk. It's pretty cool to see a Bear's dps on pull! - **Faerie Fire (Feral) rework:** Previously applied once and stopped. Now spams on cooldown for continuous threat generation (~3.5k threat per cast at level 80), and serves as a ranged soft-taunt fallback on the `lose aggro` trigger when Growl is on cooldown. *(See Faerie Fire (Feral) Trigger in the code notes below.)* - **Lacerate rework:** Previously, lacerate was a low priority default action and bears had no duration awareness. They would occasionally let 5 stacks of Lacerate fall off, resulting in pretty significant threat loss. Now, `LacerateTrigger` fires when the target has no Lacerate debuff, the stack count is below 5, or the remaining duration is ≤ 6 seconds. - **Demoralizing Roar on single target:** Previously, Demoralizing Roar only fired on the medium AoE trigger (3+ enemies, skipped on bosses). A dedicated trigger now applies it in any encounter, provided Vindication, Demoralizing Shout, or Curse of Weakness are not already present, as they don't stack with Demoralizing Roar. - **Bug fix - Rebirth on bears:** Bears no longer attempt to cast Rebirth in combat. The generic combat resurrection trigger was firing for all druid specs, causing bears to shift out of Dire Bear Form mid-fight to cast Rebirth — dropping their armor and HP while still holding aggro. Lots of sudden tank deaths... - **Feral Charge toggleable strategy:** Feral Charge (Bear) is now housed in a dedicated `feral charge` strategy, enabled by default for Bear druids. It can be disabled with `co -feral charge` and re-enabled with `co +feral charge`. Disabling this strategy can be useful for encounters where charging in would be unfavorable. --- ## Resto - **Blanketing strategy:** Added a `blanketing` strategy (enabled by default, `co -blanketing` to disable) that pre-HoTs group members with Wild Growth and Rejuvenation regardless of current health, prioritizing tanks → melee → ranged to maximize Revitalize uptime. *(See Blanketing Strategy in the code notes below.)* - **Nature's Swiftness → instant Healing Touch combo:** Nature's Swiftness was previously included in the boost strategy, where it would fire proactively on cooldown regardless of context. This wasted the proc on situations where it provided no benefit. It is now exclusively reactive — triggered at high priority (56.0f) when a party member hits critical health. A paired `nature's swiftness active` trigger then immediately fires Healing Touch (55.0f) on the lowest-health party member, consuming the proc as an instant-cast emergency heal. - **Lifebloom priority lowered (29.0f → 13.0f):** Lifebloom on the main tank is cast on Omen of Clarity procs. At 29.0f it previously outprioritised all low health reactive healing (21.4f), meaning a Clearcasting proc while a party member was at 25–44% HP would cause the bot to cast Lifebloom on the tank instead of Swiftmend or Nourish on the injured target. Lowered to 13.0f so it fires only when no reactive healing is queued. - **Healing spell priority order reworked:** All three reactive categories (critical, low, medium) now follow the same sequence: Swiftmend → Wild Growth → Nourish → Regrowth → Healing Touch. - **Tranquility toggleable strategy:** Tranquility is now housed in a dedicated `tranquility` strategy, enabled by default for Resto druids. It can be disabled with `co -tranquility`. In raids, Tranquility only heals the druid's own group — not the full raid — making it situationally poor during raid-wide damage or heavy movement phases. Disabling this strategy lets players suppress the cast on those encounters without affecting the rest of the healing rotation. --- ## CC & Strategy - **Boost strategy:** The boost strategy is assigned to all druid specs by default. It is spec-gated internally — Balance druids use it for Force of Nature (treants on cooldown), and Feral druids (both cat and bear) use it for Berserk. - **CC strategy enabled by default:** The CC strategy was previously not assigned to druids by default. It is now enabled for Balance and Feral Cat, with behavior gated by spec: - Balance receives the full RTI CC trigger set (Cyclone, Hibernate, Entangling Roots). - Feral cats only receive RTI CC triggers when Predator's Swiftness is active (see above). - **AoE strategy reorganized:** All AoE spells — Hurricane, Starfall, Typhoon, Swipe (Cat), and DoTs on attackers (Moonfire, Insect Swarm) — are now handled by the shared AoE strategy, consistent with how the rest of the playerbot project structures AoE logic. This does not affect Bear druids. - **Aquatic Form while submerged:** The non-combat strategy now shifts into Aquatic Form when the bot is fully submerged out of combat (`LIQUID_MAP_UNDER_WATER`). If the bot is in another shapeshift (Bear, Cat, Moonkin, Tree), it first shifts to caster form as a prerequisite before entering Aquatic Form. The trigger intentionally does not fire at the water surface (`LIQUID_MAP_IN_WATER`), or use the "swimming" trigger, because it caused the druid to loop caster form and aquatic form endlessly while surfaced. --- ## Code Consolidation & Refactoring - The Bear, Cat, Heal, and Caster strategy files have been renamed to Bear, Cat, Balance, and Resto respectively, to match the naming conventions used elsewhere in the project. - The Melee and Offheal strategy files have been deleted. The offheal healing logic is preserved in full — it now lives as an optional strategy (`CatOffhealStrategy`) inside `CatDruidStrategy`, sharing the same action node factories as the base cat strategy rather than duplicating them. - All action relevance values across the druid strategies have been converted from named constants (e.g. `ACTION_NORMAL`, `ACTION_HIGH + 4`) to explicit numerical floats (e.g. `10.0f`, `24.0f`). This makes priority ordering immediately visible in the source without needing to cross-reference the constant definitions. --- ## New Code & Project References ### Eclipse Cooldown Tracking (`DruidActions.cpp`) The previous implementation tracked the Eclipse cooldown using `EclipseSolarCooldownTrigger` and `EclipseLunarCooldownTrigger`, both of which extended `SpellCooldownTrigger` and called `bot->HasSpellCooldown(48517/48518)`. `SpellCooldownTrigger` is the standard project pattern for this — it works correctly for spells whose cooldowns are registered in the database. However, Eclipse (Solar) and Eclipse (Lunar) both have **Cooldown: n/a** in the DB. `HasSpellCooldown` always returned false, so boomkins never respected the cooldown and would revert to the wrong filler immediately after an Eclipse buff fell off. Since the cooldown can't be read from the DB, the fix tracks it manually. When `CastWrathAction::isUseful()` or `CastStarfireAction::isUseful()` detects that the corresponding Eclipse aura has become active, it records the current timestamp and suppresses the opposing filler for 30 seconds — the actual in-game cooldown duration. The timestamps are stored using `ManualSetValue`, the same pattern as `LastSpellCastTimeValue` (`src/Ai/Base/Value/LastSpellCastTimeValue.h`), which is already used throughout the project to record when spells were last cast. Two new value classes — `EclipseSolarProcTimeValue` and `EclipseLunarProcTimeValue` — are registered in a new `DruidValueContextInternal` factory inside `DruidAiObjectContext.cpp`, following the same factory pattern as `DruidTriggerFactoryInternal` and `DruidAiObjectContextInternal` in the same file. Because these values live in the per-bot `AiObjectContext`, they are automatically destroyed when the bot logs out — no manual cleanup needed, and no shared state between bots. --- ### Healer Low Mana Framework (`PartyMemberToHeal.h/.cpp`, `HealthTriggers.h/.cpp`, `ValueContext.h`, `TriggerContext.h`) `HealerLowMana` and `HealerLowManaTrigger` are added to the shared base framework rather than the druid-specific code. Currently used by the Cat Innervate trigger; designed so Mana Tide Totem, Hymn of Hope, and similar spells from other classes can hook into the same trigger without duplicating the group-scanning logic. The pair follows the same pattern as the existing `PartyMemberToHeal` / `PartyMemberLowHealthTrigger` — the project's standard design for "scan the group for the most in-need member, then trigger when that member crosses a threshold." **Value** (`HealerLowMana : PartyMemberValue`): `Calculate()` walks the group reference list, skips non-healers via the existing `IsHeal()` check, and uses `MinValueCalculator` to return the lowest-mana healer as a `Unit*`. Registered in `ValueContext.h` under the key `"healer low mana"`. **Trigger** (`HealerLowManaTrigger : Trigger`): `GetTargetName()` returns `"healer low mana"`, which the base `Trigger::GetTarget()` resolves against the value context — exactly how `PartyMemberLowHealthTrigger::GetTargetName()` returns `"party member to heal"`. `IsActive()` calls `GetTarget()` and checks `GetPowerPct(POWER_MANA) < sPlayerbotAIConfig.lowMana`. The trigger doesn't extend `HealthInRangeTrigger` because that class is specifically for health (it reads the `"health"` value). Mana requires a direct `GetPowerPct(POWER_MANA)` call, so a plain `Trigger` with a custom `IsActive()` is used instead. Both are registered in the global `ValueContext.h` and `TriggerContext.h` rather than a class-specific factory, consistent with how all other `PartyMemberValue` subclasses are registered in the project. --- ### Blanketing Strategy (`RestoDruidStrategy.h/.cpp`, `DruidActions.h/.cpp`, `DruidAiObjectContext.cpp`) **Strategy structure:** `DruidBlanketStrategy` is a standalone `Strategy` overlay, not embedded inside `RestoDruidStrategy`. This is the same pattern as `DruidTranquilityStrategy`, `DruidBoostStrategy`, and `DruidCcStrategy` — additive overlays that layer behavior on top of the base strategy and can be toggled independently via `co +/-blanketing`. **Triggers:** Both `"wild growth blanket"` and `"rejuvenation blanket"` are instantiated as `BuffOnPartyTrigger(ai, spellName)` — the project's existing class from `GenericTriggers.h` for party-wide buff maintenance, used throughout the codebase for things like Blessings and Mark of the Wild. `BuffOnPartyTrigger` extends `BuffTrigger` and fires when any party member is missing the named aura. No custom trigger class was needed. **Actions:** Both actions inherit from a shared `CastBlanketHotAction` base that itself extends `CastSpellAction`. Inheriting from `CastSpellAction` means `isPossible()` is handled for free — spell known, off cooldown, target reachable, resources available. The constructor sets `range = botAI->GetRange("heal")` to use the standard healing range. **`GetBlanketTarget(auraName)`:** The custom part of the implementation. Walks the group in three prioritized passes — tanks first, then melee non-tanks, then ranged — returning the first eligible member found. Eligible is defined as: alive, not a GM, within `spellDistance`, and `!botAI->HasAura(auraName, member, false, true)` (not already carrying the HoT). Returns nullptr if every member is already covered. **`isUseful()`:** On both actions simply returns `GetTarget() != nullptr` — fires as long as `GetBlanketTarget` finds someone without the HoT, and suppresses itself the moment all targets are covered. --- ### CC Implementation — Cyclone, Hibernate, Entangling Roots (`DruidTriggers.h/.cpp`, `DruidActions.h/.cpp`, `GenericDruidStrategy.cpp`) The druid CC spells use the project's strict RTI-only pattern rather than the fallback "best candidate" pattern used by other classes (e.g., Mage Polymorph). **`"rti cc target"` value:** A direct raid icon lookup. Reads the `"rti cc"` string value (default `"moon"`, configurable per-bot via `rti cc `), converts it to a raid icon index, and returns the live `Unit*` for that GUID. If no CC icon is set, it returns `nullptr`. There is no fallback to a best-candidate scan. **Triggers** (`CycloneTrigger`, `HibernateTrigger`, `EntanglingRootsTrigger`): All three extend `HasCcTargetTrigger` and override `IsActive()`. The first check is always `"rti cc target"` — if it returns `nullptr`, the trigger is immediately silent. If an icon is set, it checks `"cc target"` (with the spell name as a qualifier) to verify the RTI target matches and delegates to `HasCcTargetTrigger::IsActive()`, which handles the "don't re-cast while already CC'd" check. **Actions** (`CastCycloneCcAction`, `CastHibernateCcAction`, `CastEntanglingRootsCcAction`): All three extend `CastCrowdControlSpellAction` rather than plain `CastSpellAction`. The action names are `"cyclone on cc"`, `"hibernate on cc"`, `"entangling roots on cc"` — not the raw spell names. This matters because `CastSpellAction` stores its constructor argument as both the action name and the spell name, and `isPossible()` calls `CanCastSpell(spell, target)` using that string. Passing `"cyclone on cc"` to `CastSpellAction` would resolve to spell ID 0 and silently return false forever. `CastCrowdControlSpellAction` keeps the spell name separate from the action name, avoiding this. `GetTargetValue()` on all three returns `context->GetValue("rti cc target")` directly. **Form prerequisite:** The action nodes for `"cyclone on cc"` and `"hibernate on cc"` have `NextAction("caster form")` as a prerequisite, so the bot automatically shifts out of Bear, Cat, or Moonkin form before casting. Entangling Roots has the same prerequisite. **Priority order:** Cyclone (24.0f) > Hibernate (23.0f) > Entangling Roots (22.0f). Cyclone is preferred because it works on any target type and the target is immune to all damage and healing while cycloned — it cannot be broken by AoE. Hibernate is beast/dragonkin only. Entangling Roots can be broken by damage. **Feral Cat CC:** Wired through `TwoTrigger` pairings with `"predator's swiftness"` (see Cat section above). Because the Predator's Swiftness proc makes the spell instant-cast, no form shift is needed — the cat casts directly from Cat Form after a finisher. --- ### Ferocious Bite Execute (`DruidTriggers.h`, `DruidCatActions.h`, `CatDruidStrategy.cpp`) Two separate triggers fire the same `CastFerociousBiteAction`, which is a plain `CastMeleeSpellAction` with no custom logic — all the intelligence lives in the triggers. **`FerociousBiteTimeTrigger`** ("ferocious bite time", 22.5f) — the normal rotation path. Requires 5 combo points, Savage Roar active with >10 seconds remaining, and Rip active on the target with >10 seconds remaining. The duration checks prevent spending combo points on Ferocious Bite when either buff is about to fall off and needs to be refreshed first. **`FerociousBiteExecuteTrigger`** ("ferocious bite execute", 24.0f) — the execute window, higher priority than the time trigger. Requires only 1 combo point, and fires when the target is below **both** 25% HP and 20,000 absolute HP. The dual condition is the key design detail: the 25% threshold alone would trigger on a raid boss at 25% health — which could still be millions of HP remaining. The 20,000 HP cap ensures the execute behavior only activates when the target is genuinely close to death, at which point dumping even a partial combo point buildup into Ferocious Bite is better than continuing a normal builder-spender cycle. --- ### Faerie Fire (Feral) Trigger (`DruidTriggers.h`) A single `FaerieFireFeralTrigger` class handles both Bear and Cat with spec-branched behavior inside `IsActive()`. It extends `DebuffTrigger` — the project's standard class for debuff maintenance on the current target — but overrides `IsActive()` to produce three distinct behaviors depending on form and talent state: **Bear:** Bypasses `DebuffTrigger::IsActive()` entirely. Returns true whenever the target is alive and in world, regardless of whether the debuff is already present. Every cast generates immediate threat and damage, so there is no reason to wait for it to fall off before recasting. **Cat with Omen of Clarity (talent aura 16864):** Same bypass — spams on cooldown to fish for Clearcasting procs. Faerie Fire (Feral) has no energy cost, making it a free input that can proc Omen of Clarity on any hit. **Cat without Omen of Clarity:** Falls through to `DebuffTrigger::IsActive()` — the standard base class behavior, which checks: target alive and in world, debuff not already present (`!botAI->HasAura("faerie fire (feral)", target)`), and estimated remaining lifetime of the target is at least `needLifeTime` seconds (default 8.0f — no point applying a 30-second debuff to something about to die). Applied as a normal debuff; does not reapply while active. Both spam paths additionally guard against Prowl — `IsActive()` returns false while the bot has the Prowl aura to prevent casting from breaking stealth. **Strategy wiring:** - Bear: standard rotation slot at 17.0f, plus wired into the `"lose aggro"` trigger at 25.5f as a soft-taunt fallback when Growl is on cooldown. - Cat: low-priority filler at 5.0f. --- ### Starfall Pull Safety (`DruidActions.cpp`) Starfall's 36-yard AoE radius is the largest in the game. A single cast near an unengaged patrol or mob pack would silently pull everything in that area. The previous implementation fired on cooldown with no awareness of the surrounding area. `CastStarfallAction::isUseful()` now applies two guards before allowing the cast: **CC safety check** (standard project pattern): reads `"current cc target"` and `"aoe position"`; suppresses the cast if the CC'd target is within `aoeRadius` of the bot's AoE position. **Unengaged hostile NPC scan (custom)**: reads `"nearest hostile npcs"` (`NearestHostileNpcsValue`), which uses the project's standard `Acore::AnyUnitInObjectRangeCheck` + `Cell::VisitObjects` grid searcher at `sightDistance` (~50 yards). The value pre-filters via `AcceptUnit()`: non-players only, and `unit->IsHostileTo(bot)` must be true — this excludes neutral-faction trigger creatures, dummies, and invisible spawns that would otherwise appear in a raw range scan. The loop then applies four additional filters: - Skip null / dead / out-of-world units (standard guard). - Skip the current target — it is the reason we're in combat; its in-combat flag is already covered. - Skip `!bot->IsValidAttackTarget(unit)` — safety net for hostile-faction trigger creatures carrying `UNIT_FLAG_NON_ATTACKABLE` that `IsHostileTo` alone doesn't filter. - Skip units beyond 40 yards — Starfall's listed radius is 36; 40 adds a small buffer for patrols about to enter range. If any remaining unit is `!unit->IsInCombat()`, the cast is suppressed — that mob is unengaged and would be pulled. **Why `"nearest hostile npcs"` and not `"attackers"`:** `attackers` only contains units currently targeting the bot. We need to scan all hostile units in the area, not just those already aggro'd. --- ### Hurricane Channel Cancel (`DruidTriggers.h/.cpp`, `GenericDruidStrategy.cpp`) The previous cancel condition checked whether fewer than 3 enemies were within 30 yards of the bot. This is a poor proxy — enemies could scatter laterally but still sit within that radius, keeping the channel alive while none of them were taking damage. The replacement is `HurricaneChannelCheckTrigger`, which locates the actual Hurricane `DynamicObject` on the field and measures from it directly. **`IsActive()` logic:** 1. Checks `bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL)` — if the bot isn't channeling at all, returns false immediately. If it is channeling but the spell isn't a Hurricane rank, also returns false. This check is necessary because `CURRENT_CHANNELED_SPELL` is a slot, not a specific spell — the same cancel action is reused for other channeled spells in the codebase, so the trigger must verify it's specifically Hurricane before acting. 2. Iterates through `HURRICANE_SPELL_IDS` (all five ranks: 16914, 17401, 17402, 27012, 48467) calling `bot->GetDynObject(spellId)` until a non-null result is found. Hurricane places a `DynamicObject` on the field that the server uses as the actual AoE cylinder — each damage tick queries which units are inside it. The DynamicObject is keyed by spell ID, so the trigger must try each rank to find whichever one the bot currently has learned and placed. 3. Reads `dynObj->GetRadius()` — the actual radius stored on the DynamicObject itself rather than a hardcoded constant. This matches exactly what the server uses to calculate damage, so the trigger's cancel condition is spatially identical to the server's hit detection. 4. Walks the `"attackers"` GuidVector and counts how many live attackers are within `dynObj->GetRadius()` of the DynamicObject's position using `unit->GetDistance(dynObj->GetPosition()) <= radius`. 5. Returns `count < minEnemies` (default 3). The trigger fires — cancelling the channel — when fewer than 3 attackers are physically inside the Hurricane AoE. **Why `"attackers"` and not a full area scan:** Hurricane only deals damage to units that are attacking the bot (or in its threat list). Scanning all nearby hostile units would cause premature cancellation if non-aggro'd enemies happened to be standing outside the AoE. Attackers is the right scope. **Strategy wiring:** The trigger is paired with `NextAction("cancel channel", 22.0f)` in the AoE strategy for both Balance and Resto druids. The cancel priority (22.0f) sits below the Hurricane cast priority (23.0f), so if the medium AoE trigger re-activates on the same tick the cancel fires — meaning enemies came back into range — the new cast wins over the cancel. --- ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. Most new triggers are simple aura or cooldown checks. The heavier ones are the group scans (for the blanketing HoTs and the healer mana check), but these are identical in cost to group scans already running throughout the project (all of the party member health checks). The Starfall safety check is the only genuinely new scan — it looks for nearby hostile NPCs before allowing a cast, using the same grid search the project already uses elsewhere. That being said, it's loaded on the end of the trigger/action pairing - so in the StarfallNoCDTrigger, the bot has to already have learned starfall, already be in combat, and have Starfall off of cooldown and ready to use. The Hurricane cancel check only runs while the bot is actively channeling, so it's tightly gated. - Describe the **processing cost** when this logic executes across many bots. Negligible for almost everything in this PR. The vast majority of new logic is aura/buff/debuff lookups and cooldown checks that cost nothing at scale. The group scans for blanketing and healer mana follow the same pattern as existing party scans that already run on every healer bot every tick. No new unbounded operations, no shared state between bots. ## How to Test the Changes All druids perform a bit better now - I'd say test the branch out with the druids y'all currently use. JUST REMEMBER TO DO reset botAI or talents spec "x" again, since there have been some strategies changed!! The big one being the blanketing strategy for resto druids. They heal so much better now. Also being able to control when they pre-hot is really great. ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) Tested before an after with the same performance logs. I tested it with a 25 man group of only druids versus my normal 25 man group on several raid bosses - no difference in pmon. - Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) Druid bots currently have several bugs/issues with them. This doesn't exactly change the skills they were already using - just refines the scenarios in which they should be used. For example, a boomkin won't use starfall when there is a pack within range but not aggro'd. You can turn off feral charge for cat druids now, so they don't fly into a bosses aoe (locust swarm on anub, overload on iron council). Bear druids don't battle rez anymore. They just feel less clunky and heal/hold aggro better. - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) There are only 2 changes to files outside of the druid strategy, which is the healer low mana framework and the modification to autoattack not being used while in prowl. ## AI Assistance Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) AI was used heavily in the process to make this PR. First in the research necessary into how systems work, to the initial code implementation, to the testing results (explaining the outcome/why it sucks), to the fix, and then to the review of the code at the end. I will say that after I started researching how to use AI, use .md files for context, clearing sessions, I got a lot better results. I'll be the first to admit that it is 10 times easier to introduce a bug with AI than it is to solve one or implement something new. That is why every time it proposed a change, I asked it if the code was consistent with the project (Already present somewhere else) and if it wasn't, it was heavily scrutinized. It was written with Claude Code with Sonnet 4.6 (high), and peer reviewed by Github copilot. AI also made the description part of the PR, in which I modified myself. ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). **Wiki commands** @Dreathean While this PR does add strategies, they are all enabled by default: co +feral charge (feral druids, both cat and bear) - enabled by default, allows/prevents the use of feral charge co +tranquility (resto druids) - enabled by default, allows/prevents the use of tranquility co +blanketing (resto druids) - enabled by default, allows the druid to pre-hot with wild growth and rejuvenation But it would be worth a mention on the wiki - there are scenarios where having these strategies disabled would be beneficial. ## Notes for Reviewers @kadeshar @Celandriel @brighton-chi Thank you for taking the time to look this over. There is a lot of copied code, a bit of new code (which is explained in the code explanation part of it, but please still ask questions), and a lot of refactoring. Please remember to reset the bot strategies before/after you test this branch, due to the several changes (blanketing, feral charge strategies). Reset with reset botAI or "talents spec balance pve" for any testers out there that didn't know. If/When this PR goes to the master branch, it will need to be noted to the people this same thing about resetting strategies. --- conf/playerbots.conf.dist | 14 +- src/Ai/Base/Actions/GenericActions.cpp | 5 +- src/Ai/Base/Trigger/GenericTriggers.cpp | 5 + src/Ai/Base/Trigger/GenericTriggers.h | 11 + src/Ai/Base/Trigger/HealthTriggers.cpp | 9 + src/Ai/Base/Trigger/HealthTriggers.h | 9 + src/Ai/Base/Trigger/RtiTriggers.cpp | 16 + src/Ai/Base/Trigger/RtiTriggers.h | 9 + src/Ai/Base/TriggerContext.h | 4 + src/Ai/Base/Value/PartyMemberToHeal.cpp | 26 + src/Ai/Base/Value/PartyMemberToHeal.h | 9 + src/Ai/Base/ValueContext.h | 2 + src/Ai/Class/Druid/Action/DruidActions.cpp | 188 +++++++- src/Ai/Class/Druid/Action/DruidActions.h | 96 +++- src/Ai/Class/Druid/Action/DruidCatActions.h | 81 +++- src/Ai/Class/Druid/DruidAiObjectContext.cpp | 125 +++-- ...dStrategy.cpp => BalanceDruidStrategy.cpp} | 176 +++---- .../Druid/Strategy/BalanceDruidStrategy.h | 25 + ...ruidStrategy.cpp => BearDruidStrategy.cpp} | 116 ++--- ...ankDruidStrategy.h => BearDruidStrategy.h} | 8 +- .../Druid/Strategy/CasterDruidStrategy.h | 45 -- .../Druid/Strategy/CatDpsDruidStrategy.cpp | 314 ------------ .../Class/Druid/Strategy/CatDruidStrategy.cpp | 446 ++++++++++++++++++ ...tDpsDruidStrategy.h => CatDruidStrategy.h} | 18 +- .../Druid/Strategy/FeralDruidStrategy.cpp | 41 +- .../Class/Druid/Strategy/FeralDruidStrategy.h | 21 +- .../GenericDruidNonCombatStrategy.cpp | 29 +- .../Druid/Strategy/GenericDruidStrategy.cpp | 145 +++++- .../Druid/Strategy/GenericDruidStrategy.h | 9 + .../Druid/Strategy/HealDruidStrategy.cpp | 108 ----- .../Class/Druid/Strategy/HealDruidStrategy.h | 24 - .../Druid/Strategy/MeleeDruidStrategy.cpp | 32 -- .../Class/Druid/Strategy/MeleeDruidStrategy.h | 21 - .../Strategy/OffhealDruidCatStrategy.cpp | 307 ------------ .../Druid/Strategy/OffhealDruidCatStrategy.h | 27 -- .../Druid/Strategy/RestoDruidStrategy.cpp | 120 +++++ .../Class/Druid/Strategy/RestoDruidStrategy.h | 42 ++ src/Ai/Class/Druid/Trigger/DruidTriggers.cpp | 76 ++- src/Ai/Class/Druid/Trigger/DruidTriggers.h | 201 +++++++- .../Class/Warlock/Action/WarlockActions.cpp | 3 + src/Ai/Class/Warlock/Action/WarlockActions.h | 2 + .../Class/Warlock/Trigger/WarlockTriggers.cpp | 16 - .../Class/Warlock/Trigger/WarlockTriggers.h | 11 +- src/Bot/Factory/AiFactory.cpp | 15 +- 44 files changed, 1740 insertions(+), 1267 deletions(-) rename src/Ai/Class/Druid/Strategy/{CasterDruidStrategy.cpp => BalanceDruidStrategy.cpp} (52%) create mode 100644 src/Ai/Class/Druid/Strategy/BalanceDruidStrategy.h rename src/Ai/Class/Druid/Strategy/{BearTankDruidStrategy.cpp => BearDruidStrategy.cpp} (59%) rename src/Ai/Class/Druid/Strategy/{BearTankDruidStrategy.h => BearDruidStrategy.h} (75%) delete mode 100644 src/Ai/Class/Druid/Strategy/CasterDruidStrategy.h delete mode 100644 src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.cpp create mode 100644 src/Ai/Class/Druid/Strategy/CatDruidStrategy.cpp rename src/Ai/Class/Druid/Strategy/{CatDpsDruidStrategy.h => CatDruidStrategy.h} (59%) delete mode 100644 src/Ai/Class/Druid/Strategy/HealDruidStrategy.cpp delete mode 100644 src/Ai/Class/Druid/Strategy/HealDruidStrategy.h delete mode 100644 src/Ai/Class/Druid/Strategy/MeleeDruidStrategy.cpp delete mode 100644 src/Ai/Class/Druid/Strategy/MeleeDruidStrategy.h delete mode 100644 src/Ai/Class/Druid/Strategy/OffhealDruidCatStrategy.cpp delete mode 100644 src/Ai/Class/Druid/Strategy/OffhealDruidCatStrategy.h create mode 100644 src/Ai/Class/Druid/Strategy/RestoDruidStrategy.cpp create mode 100644 src/Ai/Class/Druid/Strategy/RestoDruidStrategy.h diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index c1d740774..6ea92b2d6 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -1786,18 +1786,18 @@ AiPlayerbot.PremadeSpecLink.9.5.80 = -2032003311302-05230015220331351005031051 AiPlayerbot.PremadeSpecName.11.0 = balance pve AiPlayerbot.PremadeSpecGlyph.11.0 = 40916,43331,40921,43335,44922,40919 -AiPlayerbot.PremadeSpecLink.11.0.60 = 5022203105331003213005301231 -AiPlayerbot.PremadeSpecLink.11.0.80 = 5032203105331303213305301231--205003012 +AiPlayerbot.PremadeSpecLink.11.0.60 = 5032003125031003213304301231 +AiPlayerbot.PremadeSpecLink.11.0.80 = 5032003125331303213305301231--205003012 AiPlayerbot.PremadeSpecName.11.1 = bear pve AiPlayerbot.PremadeSpecGlyph.11.1 = 40897,43331,46372,43335,43332,40899 -AiPlayerbot.PremadeSpecLink.11.1.60 = -500232130322110353100301310501 +AiPlayerbot.PremadeSpecLink.11.1.60 = -503232132322010303120300013501 AiPlayerbot.PremadeSpecLink.11.1.80 = -503232132322010353120303013511-20350001 AiPlayerbot.PremadeSpecName.11.2 = resto pve -AiPlayerbot.PremadeSpecGlyph.11.2 = 40913,43331,40906,43335,43674,45602 -AiPlayerbot.PremadeSpecLink.11.2.60 = --230033312031500531050113051 -AiPlayerbot.PremadeSpecLink.11.2.80 = 05320131003--230023312131500531050313051 +AiPlayerbot.PremadeSpecGlyph.11.2 = 40906,43331,45602,43335,43674,45603 +AiPlayerbot.PremadeSpecLink.11.2.60 = --230033312031502331050313031 +AiPlayerbot.PremadeSpecLink.11.2.80 = 05320031--230033312031502431053313051 AiPlayerbot.PremadeSpecName.11.3 = cat pve -AiPlayerbot.PremadeSpecGlyph.11.3 = 40902,43331,40901,43335,43674,45604 +AiPlayerbot.PremadeSpecGlyph.11.3 = 40902,43331,40901,43674,43335,45604 AiPlayerbot.PremadeSpecLink.11.3.60 = -552202032322010053100030310501 AiPlayerbot.PremadeSpecLink.11.3.80 = -553202032322010053120030310511-203503012 AiPlayerbot.PremadeSpecName.11.4 = balance pvp diff --git a/src/Ai/Base/Actions/GenericActions.cpp b/src/Ai/Base/Actions/GenericActions.cpp index a4873d86a..354cb456e 100644 --- a/src/Ai/Base/Actions/GenericActions.cpp +++ b/src/Ai/Base/Actions/GenericActions.cpp @@ -50,7 +50,10 @@ bool MeleeAction::isUseful() if (botAI->IsInVehicle() && !botAI->IsInVehicle(false, false, true)) return false; - return true; + // Do not start autoattack while prowled — let opener spells break stealth intentionally. + // Future rogue stealth implementation should use this instead: + // return !(botAI->HasAura("stealth", bot) || botAI->HasAura("prowl", bot)); + return !botAI->HasAura("prowl", bot); } bool TogglePetSpellAutoCastAction::Execute(Event /*event*/) diff --git a/src/Ai/Base/Trigger/GenericTriggers.cpp b/src/Ai/Base/Trigger/GenericTriggers.cpp index 27dd5999c..2035e1c29 100644 --- a/src/Ai/Base/Trigger/GenericTriggers.cpp +++ b/src/Ai/Base/Trigger/GenericTriggers.cpp @@ -34,6 +34,11 @@ bool MediumManaTrigger::IsActive() AI_VALUE2(uint8, "mana", "self target") < sPlayerbotAIConfig.mediumMana; } +bool LowEnergyTrigger::IsActive() +{ + return AI_VALUE2(uint8, "energy", "self target") < threshold; +} + bool NoPetTrigger::IsActive() { return (bot->GetMinionGUID().IsEmpty()) && (!AI_VALUE(Unit*, "pet target")) && (!bot->GetGuardianPet()) && diff --git a/src/Ai/Base/Trigger/GenericTriggers.h b/src/Ai/Base/Trigger/GenericTriggers.h index 7a44112f3..a3931c246 100644 --- a/src/Ai/Base/Trigger/GenericTriggers.h +++ b/src/Ai/Base/Trigger/GenericTriggers.h @@ -550,6 +550,17 @@ public: bool IsActive() override; }; +class LowEnergyTrigger : public Trigger +{ +public: + LowEnergyTrigger(PlayerbotAI* botAI, uint8 threshold = 30) : Trigger(botAI, "low energy"), threshold(threshold) {} + + bool IsActive() override; + +private: + uint8 threshold; +}; + BEGIN_TRIGGER(PanicTrigger, Trigger) // cppcheck-suppress unknownMacro std::string const getName() override { return "panic"; } END_TRIGGER() diff --git a/src/Ai/Base/Trigger/HealthTriggers.cpp b/src/Ai/Base/Trigger/HealthTriggers.cpp index a2b36962d..746198acf 100644 --- a/src/Ai/Base/Trigger/HealthTriggers.cpp +++ b/src/Ai/Base/Trigger/HealthTriggers.cpp @@ -22,6 +22,15 @@ bool DeadTrigger::IsActive() { return AI_VALUE2(bool, "dead", GetTargetName()); bool AoeHealTrigger::IsActive() { return AI_VALUE2(uint8, "aoe heal", type) >= count; } +bool HealerLowManaTrigger::IsActive() +{ + Unit* target = GetTarget(); + if (!target) + return false; + + return target->GetPowerPct(POWER_MANA) < sPlayerbotAIConfig.lowMana; +} + bool AoeInGroupTrigger::IsActive() { int32 member = botAI->GetNearGroupMemberCount(); diff --git a/src/Ai/Base/Trigger/HealthTriggers.h b/src/Ai/Base/Trigger/HealthTriggers.h index 0fbd403d7..14e68c7e3 100644 --- a/src/Ai/Base/Trigger/HealthTriggers.h +++ b/src/Ai/Base/Trigger/HealthTriggers.h @@ -143,6 +143,15 @@ public: TargetCriticalHealthTrigger(PlayerbotAI* botAI) : TargetLowHealthTrigger(botAI, 20) {} }; +class HealerLowManaTrigger : public Trigger +{ +public: + HealerLowManaTrigger(PlayerbotAI* botAI) : Trigger(botAI, "healer low mana") {} + + std::string const GetTargetName() override { return "healer low mana"; } + bool IsActive() override; +}; + class PartyMemberDeadTrigger : public Trigger { public: diff --git a/src/Ai/Base/Trigger/RtiTriggers.cpp b/src/Ai/Base/Trigger/RtiTriggers.cpp index c7dc737cf..ba934c5d7 100644 --- a/src/Ai/Base/Trigger/RtiTriggers.cpp +++ b/src/Ai/Base/Trigger/RtiTriggers.cpp @@ -18,3 +18,19 @@ bool NoRtiTrigger::IsActive() Unit* target = AI_VALUE(Unit*, "rti target"); return target == nullptr; } + +// Fires when the RTI CC target should be crowd controlled by this spell. +// Standard path: the target is already in the attackers list and "cc target" matches the RTI +// mark — delegates to HasCcTargetTrigger to confirm no one else is already CCing it. +bool RtiCcTrigger::IsActive() +{ + Unit* rtiCcTarget = AI_VALUE(Unit*, "rti cc target"); + if (!rtiCcTarget) + return false; + + Unit* ccTarget = AI_VALUE2(Unit*, "cc target", getName()); + if (ccTarget && ccTarget == rtiCcTarget) + return HasCcTargetTrigger::IsActive(); + + return botAI->CanCastSpell(getName(), rtiCcTarget); +} diff --git a/src/Ai/Base/Trigger/RtiTriggers.h b/src/Ai/Base/Trigger/RtiTriggers.h index a8ecab4d9..9ff128ec2 100644 --- a/src/Ai/Base/Trigger/RtiTriggers.h +++ b/src/Ai/Base/Trigger/RtiTriggers.h @@ -6,6 +6,7 @@ #ifndef _PLAYERBOT_RTITRIGGERS_H #define _PLAYERBOT_RTITRIGGERS_H +#include "GenericTriggers.h" #include "Trigger.h" class PlayerbotAI; @@ -18,4 +19,12 @@ public: bool IsActive() override; }; +class RtiCcTrigger : public HasCcTargetTrigger +{ +public: + RtiCcTrigger(PlayerbotAI* botAI, std::string const name) : HasCcTargetTrigger(botAI, name) {} + + bool IsActive() override; +}; + #endif diff --git a/src/Ai/Base/TriggerContext.h b/src/Ai/Base/TriggerContext.h index 54edbb017..d440e5b5f 100644 --- a/src/Ai/Base/TriggerContext.h +++ b/src/Ai/Base/TriggerContext.h @@ -51,6 +51,7 @@ public: creators["low mana"] = &TriggerContext::LowMana; creators["medium mana"] = &TriggerContext::MediumMana; + creators["low energy"] = &TriggerContext::LowEnergy; creators["high mana"] = &TriggerContext::HighMana; creators["almost full mana"] = &TriggerContext::AlmostFullMana; creators["enough mana"] = &TriggerContext::EnoughMana; @@ -59,6 +60,7 @@ public: creators["party member low health"] = &TriggerContext::PartyMemberLowHealth; creators["party member medium health"] = &TriggerContext::PartyMemberMediumHealth; creators["party member almost full health"] = &TriggerContext::PartyMemberAlmostFullHealth; + creators["healer low mana"] = &TriggerContext::HealerLowMana; creators["generic boost"] = &TriggerContext::generic_boost; creators["loss of control"] = &TriggerContext::loss_of_control; @@ -312,6 +314,7 @@ private: static Trigger* TargetCriticalHealth(PlayerbotAI* botAI) { return new TargetCriticalHealthTrigger(botAI); } static Trigger* LowMana(PlayerbotAI* botAI) { return new LowManaTrigger(botAI); } static Trigger* MediumMana(PlayerbotAI* botAI) { return new MediumManaTrigger(botAI); } + static Trigger* LowEnergy(PlayerbotAI* botAI) { return new LowEnergyTrigger(botAI); } static Trigger* HighMana(PlayerbotAI* botAI) { return new HighManaTrigger(botAI); } static Trigger* AlmostFullMana(PlayerbotAI* botAI) { return new AlmostFullManaTrigger(botAI); } static Trigger* EnoughMana(PlayerbotAI* botAI) { return new EnoughManaTrigger(botAI); } @@ -383,6 +386,7 @@ private: { return new PartyMemberCriticalHealthTrigger(botAI); } + static Trigger* HealerLowMana(PlayerbotAI* botAI) { return new HealerLowManaTrigger(botAI); } static Trigger* protect_party_member(PlayerbotAI* botAI) { return new ProtectPartyMemberTrigger(botAI); } static Trigger* no_pet(PlayerbotAI* botAI) { return new NoPetTrigger(botAI); } static Trigger* has_pet(PlayerbotAI* botAI) { return new HasPetTrigger(botAI); } diff --git a/src/Ai/Base/Value/PartyMemberToHeal.cpp b/src/Ai/Base/Value/PartyMemberToHeal.cpp index 9845bc119..80621775a 100644 --- a/src/Ai/Base/Value/PartyMemberToHeal.cpp +++ b/src/Ai/Base/Value/PartyMemberToHeal.cpp @@ -135,6 +135,32 @@ bool PartyMemberToHeal::Check(Unit* player) bot->GetDistance2d(player) < sPlayerbotAIConfig.healDistance * 2 && bot->IsWithinLOSInMap(player); } +Unit* HealerLowMana::Calculate() +{ + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + MinValueCalculator calc(100); + + for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) + { + Player* player = gref->GetSource(); + if (!player || player == bot) + continue; + if (player->IsGameMaster() || !player->IsAlive()) + continue; + if (!botAI->IsHeal(player)) + continue; + + float mana = player->GetPowerPct(POWER_MANA); + if (mana < calc.minValue) + calc.probe(mana, player); + } + + return (Unit*)calc.param; +} + Unit* PartyMemberToProtect::Calculate() { return nullptr; diff --git a/src/Ai/Base/Value/PartyMemberToHeal.h b/src/Ai/Base/Value/PartyMemberToHeal.h index f86035c09..6a255b724 100644 --- a/src/Ai/Base/Value/PartyMemberToHeal.h +++ b/src/Ai/Base/Value/PartyMemberToHeal.h @@ -37,4 +37,13 @@ protected: Unit* Calculate() override; }; +class HealerLowMana : public PartyMemberValue +{ +public: + HealerLowMana(PlayerbotAI* botAI) : PartyMemberValue(botAI, "healer low mana") {} + +protected: + Unit* Calculate() override; +}; + #endif diff --git a/src/Ai/Base/ValueContext.h b/src/Ai/Base/ValueContext.h index bac5fd835..4070da80b 100644 --- a/src/Ai/Base/ValueContext.h +++ b/src/Ai/Base/ValueContext.h @@ -132,6 +132,7 @@ public: creators["attacker without aura"] = &ValueContext::attacker_without_aura; creators["melee attacker without aura"] = &ValueContext::melee_attacker_without_aura; creators["party member to heal"] = &ValueContext::party_member_to_heal; + creators["healer low mana"] = &ValueContext::healer_low_mana; creators["party member to resurrect"] = &ValueContext::party_member_to_resurrect; creators["current target"] = &ValueContext::current_target; creators["self target"] = &ValueContext::self_target; @@ -451,6 +452,7 @@ private: return new MeleeAttackerWithoutAuraTargetValue(botAI); } static UntypedValue* party_member_to_heal(PlayerbotAI* botAI) { return new PartyMemberToHeal(botAI); } + static UntypedValue* healer_low_mana(PlayerbotAI* botAI) { return new HealerLowMana(botAI); } static UntypedValue* party_member_to_resurrect(PlayerbotAI* botAI) { return new PartyMemberToResurrect(botAI); } static UntypedValue* party_member_to_dispel(PlayerbotAI* botAI) { return new PartyMemberToDispel(botAI); } static UntypedValue* party_member_to_protect(PlayerbotAI* botAI) { return new PartyMemberToProtect(botAI); } diff --git a/src/Ai/Class/Druid/Action/DruidActions.cpp b/src/Ai/Class/Druid/Action/DruidActions.cpp index c01cc4342..e32a5ea04 100644 --- a/src/Ai/Class/Druid/Action/DruidActions.cpp +++ b/src/Ai/Class/Druid/Action/DruidActions.cpp @@ -11,6 +11,9 @@ #include "AoeValues.h" #include "TargetValue.h" +constexpr uint32 SPELL_ECLIPSE_SOLAR = 48517; +constexpr uint32 SPELL_ECLIPSE_LUNAR = 48518; + namespace { bool PrepareThornsTarget(PlayerbotAI* botAI, Unit* target) @@ -64,16 +67,89 @@ bool CastThornsOnMainTankAction::Execute(Event event) return PrepareThornsTarget(botAI, GetTarget()) && BuffOnMainTankAction::Execute(event); } -Value* CastEntanglingRootsCcAction::GetTargetValue() +bool CastWrathAction::isUseful() { - return context->GetValue("cc target", "entangling roots"); + time_t now = time(nullptr); + time_t solarTime = context->GetValue("eclipse solar proc time")->Get(); + time_t lunarTime = context->GetValue("eclipse lunar proc time")->Get(); + + // --- Update Solar Eclipse tracking --- + // Wrath is selected during Solar Eclipse (eclipse trigger at 20.0f), so we reliably see it here. + if (bot->HasAura(SPELL_ECLIPSE_SOLAR) && !solarTime) + context->GetValue("eclipse solar proc time")->Set(now); + // Lunar procced — Solar fishing window is over, new cycle begins + if (bot->HasAura(SPELL_ECLIPSE_LUNAR) && solarTime) + context->GetValue("eclipse solar proc time")->Set(0); + // 30 s cooldown window expired + if (solarTime && (now - solarTime) >= 30) + context->GetValue("eclipse solar proc time")->Set(0); + + // --- Update Lunar Eclipse tracking (belt-and-suspenders in case Starfire isn't evaluated) --- + if (bot->HasAura(SPELL_ECLIPSE_LUNAR) && !lunarTime) + context->GetValue("eclipse lunar proc time")->Set(now); + // Solar procced — Lunar fishing window is over + if (bot->HasAura(SPELL_ECLIPSE_SOLAR) && lunarTime) + context->GetValue("eclipse lunar proc time")->Set(0); + if (lunarTime && (now - lunarTime) >= 30) + context->GetValue("eclipse lunar proc time")->Set(0); + + // Block Wrath while in Lunar Eclipse / post-Lunar fishing window + if (context->GetValue("eclipse lunar proc time")->Get()) + return false; + + return CastSpellAction::isUseful(); } -bool CastEntanglingRootsCcAction::Execute(Event /*event*/) { return botAI->CastSpell("entangling roots", GetTarget()); } +bool CastStarfireAction::isUseful() +{ + time_t now = time(nullptr); + time_t solarTime = context->GetValue("eclipse solar proc time")->Get(); + time_t lunarTime = context->GetValue("eclipse lunar proc time")->Get(); -Value* CastHibernateCcAction::GetTargetValue() { return context->GetValue("cc target", "hibernate"); } + // --- Update Lunar Eclipse tracking --- + // Starfire is selected during Lunar Eclipse (eclipse trigger at 20.0f), so we reliably see it here. + if (bot->HasAura(SPELL_ECLIPSE_LUNAR) && !lunarTime) + context->GetValue("eclipse lunar proc time")->Set(now); + // Solar procced — Lunar fishing window is over, new cycle begins + if (bot->HasAura(SPELL_ECLIPSE_SOLAR) && lunarTime) + context->GetValue("eclipse lunar proc time")->Set(0); + // 30 s cooldown window expired + if (lunarTime && (now - lunarTime) >= 30) + context->GetValue("eclipse lunar proc time")->Set(0); + + // --- Update Solar Eclipse tracking (belt-and-suspenders in case Wrath isn't evaluated) --- + if (bot->HasAura(SPELL_ECLIPSE_SOLAR) && !solarTime) + context->GetValue("eclipse solar proc time")->Set(now); + // Lunar procced — Solar fishing window is over + if (bot->HasAura(SPELL_ECLIPSE_LUNAR) && solarTime) + context->GetValue("eclipse solar proc time")->Set(0); + if (solarTime && (now - solarTime) >= 30) + context->GetValue("eclipse solar proc time")->Set(0); + + // Block Starfire while in Solar Eclipse / post-Solar fishing window + if (context->GetValue("eclipse solar proc time")->Get()) + return false; + + return CastSpellAction::isUseful(); +} + +Value* CastEntanglingRootsCcAction::GetTargetValue() +{ + return context->GetValue("rti cc target"); +} + +Value* CastHibernateCcAction::GetTargetValue() { return context->GetValue("rti cc target"); } + +Value* CastCycloneCcAction::GetTargetValue() { return context->GetValue("rti cc target"); } + +bool CastTyphoonAction::isUseful() +{ + bool facingTarget = AI_VALUE2(bool, "facing", "current target"); + bool targetClose = ServerFacade::instance().IsDistanceLessOrEqualThan( + AI_VALUE2(float, "distance", GetTargetName()), 15.f); + return facingTarget && targetClose; +} -bool CastHibernateCcAction::Execute(Event /*event*/) { return botAI->CastSpell("hibernate", GetTarget()); } bool CastStarfallAction::isUseful() { if (!CastSpellAction::isUseful()) @@ -89,12 +165,26 @@ bool CastStarfallAction::isUseful() return false; } - // Avoid single-target usage on initial pull - uint8 aoeCount = *context->GetValue("aoe count"); - if (aoeCount < 2) + // Suppress if any unengaged hostile unit is within 40 yards — Starfall's 36-yard radius would pull them. + Unit* currentTarget = AI_VALUE(Unit*, "current target"); + GuidVector const& nearbyNpcs = AI_VALUE(GuidVector, "possible targets"); + for (ObjectGuid const& guid : nearbyNpcs) { - Unit* target = context->GetValue("current target")->Get(); - if (!target || (!botAI->HasAura("moonfire", target) && !botAI->HasAura("insect swarm", target))) + Unit* unit = botAI->GetUnit(guid); + // Standard null/world-state guard before touching the unit. + if (!unit || !unit->IsAlive() || !unit->IsInWorld()) + continue; + // Already our target — its in-combat flag covers it. + if (unit == currentTarget) + continue; + // Safety net for any hostile-faction trigger creature that carries NON_ATTACKABLE flags. + if (!bot->IsValidAttackTarget(unit)) + continue; + // Outside Starfall's actual radius; no pull risk. + if (ServerFacade::instance().GetDistance2d(bot, unit) > 40.0f) + continue; + // Unengaged mob within range — casting would pull it. + if (!unit->IsInCombat()) return false; } @@ -119,6 +209,24 @@ bool CastRebirthAction::isUseful() AI_VALUE2(float, "distance", GetTargetName()) <= sPlayerbotAIConfig.spellDistance; } +bool CastInnervateOnHealerAction::isPossible() +{ + Unit* target = GetTarget(); + if (!target || !target->IsInWorld()) + return false; + + if (botAI->HasAura("innervate", target)) + return false; + + uint32 spellId = AI_VALUE2(uint32, "spell id", "innervate"); + return spellId && !bot->HasSpellCooldown(spellId); +} + +std::vector CastInnervateOnHealerAction::getPrerequisites() +{ + return { NextAction("caster form") }; +} + Unit* CastRejuvenationOnNotFullAction::GetTarget() { Group* group = bot->GetGroup(); @@ -149,3 +257,63 @@ bool CastRejuvenationOnNotFullAction::isUseful() { return GetTarget(); } + +// --- Blanket HoT actions --- + +Unit* CastBlanketHotAction::GetBlanketTarget(std::string const& auraName) +{ + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + auto eligible = [&](Player* member) -> bool + { + return member && member->IsAlive() && + !member->IsGameMaster() && + bot->GetDistance2d(member) <= sPlayerbotAIConfig.spellDistance && + !botAI->HasAura(auraName, member, false, true); + }; + + Player* firstMelee = nullptr; + Player* firstRanged = nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!eligible(member)) + continue; + + if (PlayerbotAI::IsTank(member)) + return member; + else if (!firstMelee && PlayerbotAI::IsMelee(member) && !PlayerbotAI::IsTank(member)) + firstMelee = member; + else if (!firstRanged && PlayerbotAI::IsRanged(member)) + firstRanged = member; + + if (firstMelee && firstRanged) + break; + } + + if (firstMelee) return firstMelee; + return firstRanged; +} + +Unit* CastRejuvenationBlanketAction::GetTarget() +{ + return GetBlanketTarget("rejuvenation"); +} + +bool CastRejuvenationBlanketAction::isUseful() +{ + return GetTarget() != nullptr; +} + +Unit* CastWildGrowthBlanketAction::GetTarget() +{ + return GetBlanketTarget("wild growth"); +} + +bool CastWildGrowthBlanketAction::isUseful() +{ + return GetTarget() != nullptr; +} diff --git a/src/Ai/Class/Druid/Action/DruidActions.h b/src/Ai/Class/Druid/Action/DruidActions.h index 7e02a985f..e386ff05d 100644 --- a/src/Ai/Class/Druid/Action/DruidActions.h +++ b/src/Ai/Class/Druid/Action/DruidActions.h @@ -8,6 +8,7 @@ #include "GenericSpellActions.h" #include "SharedDefines.h" +#include "Value.h" class PlayerbotAI; class Unit; @@ -64,7 +65,7 @@ class CastHealingTouchOnPartyAction : public HealPartyMemberAction { public: CastHealingTouchOnPartyAction(PlayerbotAI* botAI) - : HealPartyMemberAction(botAI, "healing touch", 50.0f, HealingManaEfficiency::LOW) + : HealPartyMemberAction(botAI, "healing touch", 50.0f, HealingManaEfficiency::MEDIUM) { } }; @@ -142,16 +143,11 @@ public: bool isUseful() override; }; -class CastOmenOfClarityAction : public CastBuffSpellAction -{ -public: - CastOmenOfClarityAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "omen of clarity") {} -}; - class CastWrathAction : public CastSpellAction { public: CastWrathAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "wrath") {} + bool isUseful() override; }; class CastStarfallAction : public CastSpellAction @@ -169,6 +165,14 @@ public: ActionThreatType getThreatType() override { return ActionThreatType::Aoe; } }; +class CastTyphoonAction : public CastSpellAction +{ +public: + CastTyphoonAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "typhoon") {} + ActionThreatType getThreatType() override { return ActionThreatType::Aoe; } + bool isUseful() override; +}; + class CastMoonfireAction : public CastDebuffSpellAction { public: @@ -185,6 +189,7 @@ class CastStarfireAction : public CastSpellAction { public: CastStarfireAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "starfire") {} + bool isUseful() override; }; class CastEntanglingRootsAction : public CastSpellAction @@ -193,12 +198,11 @@ public: CastEntanglingRootsAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "entangling roots") {} }; -class CastEntanglingRootsCcAction : public CastSpellAction +class CastEntanglingRootsCcAction : public CastCrowdControlSpellAction { public: - CastEntanglingRootsCcAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "entangling roots on cc") {} + CastEntanglingRootsCcAction(PlayerbotAI* botAI) : CastCrowdControlSpellAction(botAI, "entangling roots") {} Value* GetTargetValue() override; - bool Execute(Event event) override; }; class CastHibernateAction : public CastSpellAction @@ -207,12 +211,18 @@ public: CastHibernateAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "hibernate") {} }; -class CastHibernateCcAction : public CastSpellAction +class CastHibernateCcAction : public CastCrowdControlSpellAction { public: - CastHibernateCcAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "hibernate on cc") {} + CastHibernateCcAction(PlayerbotAI* botAI) : CastCrowdControlSpellAction(botAI, "hibernate") {} + Value* GetTargetValue() override; +}; + +class CastCycloneCcAction : public CastCrowdControlSpellAction +{ +public: + CastCycloneCcAction(PlayerbotAI* botAI) : CastCrowdControlSpellAction(botAI, "cyclone") {} Value* GetTargetValue() override; - bool Execute(Event event) override; }; class CastNaturesGraspAction : public CastBuffSpellAction @@ -264,6 +274,16 @@ public: std::string const GetTargetName() override { return "self target"; } }; +class CastInnervateOnHealerAction : public CastSpellAction +{ +public: + CastInnervateOnHealerAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "innervate") {} + + std::string const GetTargetName() override { return "healer low mana"; } + bool isPossible() override; + std::vector getPrerequisites() override; +}; + class CastTranquilityAction : public CastAoeHealSpellAction { public: @@ -312,13 +332,15 @@ public: class CastInsectSwarmOnAttackerAction : public CastDebuffSpellOnAttackerAction { public: - CastInsectSwarmOnAttackerAction(PlayerbotAI* ai) : CastDebuffSpellOnAttackerAction(ai, "insect swarm") {} + CastInsectSwarmOnAttackerAction(PlayerbotAI* ai) : CastDebuffSpellOnAttackerAction(ai, "insect swarm", true, 0.0f) {} + bool isUseful() override { return CastAuraSpellAction::isUseful(); } }; class CastMoonfireOnAttackerAction : public CastDebuffSpellOnAttackerAction { public: - CastMoonfireOnAttackerAction(PlayerbotAI* ai) : CastDebuffSpellOnAttackerAction(ai, "moonfire") {} + CastMoonfireOnAttackerAction(PlayerbotAI* ai) : CastDebuffSpellOnAttackerAction(ai, "moonfire", true, 0.0f) {} + bool isUseful() override { return CastAuraSpellAction::isUseful(); } }; class CastEnrageAction : public CastBuffSpellAction @@ -344,4 +366,48 @@ public: CastForceOfNatureAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "force of nature") {} }; +// Base for blanket HoT actions. Provides GetBlanketTarget() as a member so +// subclasses can use AI_VALUE and the standard context machinery. +class CastBlanketHotAction : public CastSpellAction +{ +public: + CastBlanketHotAction(PlayerbotAI* ai, std::string const& spell) : CastSpellAction(ai, spell) + { + range = botAI->GetRange("heal"); + } + +protected: + Unit* GetBlanketTarget(std::string const& auraName); +}; + +class CastRejuvenationBlanketAction : public CastBlanketHotAction +{ +public: + CastRejuvenationBlanketAction(PlayerbotAI* ai) : CastBlanketHotAction(ai, "rejuvenation") {} + bool isUseful() override; + Unit* GetTarget() override; + std::string const getName() override { return "rejuvenation blanket"; } +}; + +class CastWildGrowthBlanketAction : public CastBlanketHotAction +{ +public: + CastWildGrowthBlanketAction(PlayerbotAI* ai) : CastBlanketHotAction(ai, "wild growth") {} + bool isUseful() override; + Unit* GetTarget() override; + std::string const getName() override { return "wild growth blanket"; } +}; + +class EclipseSolarProcTimeValue : public ManualSetValue +{ +public: + EclipseSolarProcTimeValue(PlayerbotAI* botAI) : ManualSetValue(botAI, 0) {} +}; + +class EclipseLunarProcTimeValue : public ManualSetValue +{ +public: + EclipseLunarProcTimeValue(PlayerbotAI* botAI) : ManualSetValue(botAI, 0) {} +}; + #endif diff --git a/src/Ai/Class/Druid/Action/DruidCatActions.h b/src/Ai/Class/Druid/Action/DruidCatActions.h index f6d8f2320..1fdd9aba7 100644 --- a/src/Ai/Class/Druid/Action/DruidCatActions.h +++ b/src/Ai/Class/Druid/Action/DruidCatActions.h @@ -9,12 +9,23 @@ #include "GenericSpellActions.h" #include "ReachTargetActions.h" +constexpr uint32 SPELL_POUNCE_RANK_1 = 9005; +constexpr uint32 SPELL_RAVAGE_RANK_1 = 6785; + class PlayerbotAI; class CastFeralChargeCatAction : public CastReachTargetSpellAction { public: CastFeralChargeCatAction(PlayerbotAI* botAI) : CastReachTargetSpellAction(botAI, "feral charge - cat", 1.5f) {} + + bool isUseful() override + { + if (botAI->HasAura("prowl", bot)) + return false; + + return CastReachTargetSpellAction::isUseful(); + } }; class CastCowerAction : public CastBuffSpellAction @@ -48,28 +59,47 @@ public: CastRakeAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "rake", true, 6.0f) {} }; -class CastRakeOnMeleeAttackersAction : public CastDebuffSpellOnMeleeAttackerAction -{ -public: - CastRakeOnMeleeAttackersAction(PlayerbotAI* botAI) : CastDebuffSpellOnMeleeAttackerAction(botAI, "rake", true, 6.0f) {} -}; - class CastClawAction : public CastMeleeSpellAction { public: CastClawAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "claw") {} + + bool isUseful() override + { + // Block Claw once Pounce is learned; Claw remains available as the stealth opener before then. + if (botAI->HasAura("prowl", bot) && bot->HasSpell(SPELL_POUNCE_RANK_1)) + return false; + + return CastMeleeSpellAction::isUseful(); + } }; class CastMangleCatAction : public CastMeleeSpellAction { public: CastMangleCatAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "mangle (cat)") {} + + bool isUseful() override + { + if (botAI->HasAura("prowl", bot)) + return false; + + return CastMeleeSpellAction::isUseful(); + } }; class CastSwipeCatAction : public CastMeleeSpellAction { public: CastSwipeCatAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "swipe (cat)") {} + + bool isUseful() override + { + if (botAI->HasAura("prowl", bot)) + return false; + + return CastMeleeSpellAction::isUseful(); + } }; class CastFerociousBiteAction : public CastMeleeSpellAction @@ -78,6 +108,21 @@ public: CastFerociousBiteAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "ferocious bite") {} }; +class CastMaimAction : public CastMeleeSpellAction +{ +public: + CastMaimAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "maim") {} + + bool isUseful() override + { + Unit* target = GetTarget(); + if (!target || !target->ToPlayer()) + return false; + + return CastMeleeSpellAction::isUseful(); + } +}; + class CastRipAction : public CastMeleeDebuffSpellAction { public: @@ -88,6 +133,14 @@ class CastShredAction : public CastMeleeSpellAction { public: CastShredAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "shred") {} + + bool isUseful() override + { + if (botAI->HasAura("prowl", bot) && bot->HasSpell(SPELL_RAVAGE_RANK_1)) + return false; + + return CastMeleeSpellAction::isUseful(); + } }; class CastProwlAction : public CastBuffSpellAction @@ -106,12 +159,28 @@ class CastRavageAction : public CastMeleeSpellAction { public: CastRavageAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "ravage") {} + + bool isUseful() override + { + if (!botAI->HasAura("prowl", bot)) + return false; + + return CastMeleeSpellAction::isUseful(); + } }; class CastPounceAction : public CastMeleeSpellAction { public: CastPounceAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "pounce") {} + + bool isUseful() override + { + if (!botAI->HasAura("prowl", bot)) + return false; + + return CastMeleeSpellAction::isUseful(); + } }; #endif diff --git a/src/Ai/Class/Druid/DruidAiObjectContext.cpp b/src/Ai/Class/Druid/DruidAiObjectContext.cpp index 4d74d1db3..9a4f243c0 100644 --- a/src/Ai/Class/Druid/DruidAiObjectContext.cpp +++ b/src/Ai/Class/Druid/DruidAiObjectContext.cpp @@ -5,9 +5,9 @@ #include "DruidAiObjectContext.h" -#include "BearTankDruidStrategy.h" -#include "CasterDruidStrategy.h" -#include "CatDpsDruidStrategy.h" +#include "BalanceDruidStrategy.h" +#include "BearDruidStrategy.h" +#include "CatDruidStrategy.h" #include "DruidActions.h" #include "DruidBearActions.h" #include "DruidCatActions.h" @@ -15,9 +15,7 @@ #include "DruidTriggers.h" #include "GenericDruidNonCombatStrategy.h" #include "GenericDruidStrategy.h" -#include "HealDruidStrategy.h" -#include "MeleeDruidStrategy.h" -#include "OffhealDruidCatStrategy.h" +#include "RestoDruidStrategy.h" #include "Playerbots.h" #include "DruidPullStrategy.h" @@ -28,30 +26,31 @@ public: { creators["nc"] = &DruidStrategyFactoryInternal::nc; creators["pull"] = &DruidStrategyFactoryInternal::pull; - creators["cat aoe"] = &DruidStrategyFactoryInternal::cat_aoe; - creators["caster aoe"] = &DruidStrategyFactoryInternal::caster_aoe; - creators["caster debuff"] = &DruidStrategyFactoryInternal::caster_debuff; - creators["dps debuff"] = &DruidStrategyFactoryInternal::caster_debuff; + creators["aoe"] = &DruidStrategyFactoryInternal::aoe; creators["cure"] = &DruidStrategyFactoryInternal::cure; - creators["melee"] = &DruidStrategyFactoryInternal::melee; creators["buff"] = &DruidStrategyFactoryInternal::buff; creators["boost"] = &DruidStrategyFactoryInternal::boost; creators["cc"] = &DruidStrategyFactoryInternal::cc; creators["healer dps"] = &DruidStrategyFactoryInternal::healer_dps; + creators["offheal"] = &DruidStrategyFactoryInternal::offheal; + creators["blanketing"] = &DruidStrategyFactoryInternal::blanketing; + creators["tranquility"] = &DruidStrategyFactoryInternal::tranquility; + creators["feral charge"] = &DruidStrategyFactoryInternal::feral_charge; } private: static Strategy* nc(PlayerbotAI* botAI) { return new GenericDruidNonCombatStrategy(botAI); } static Strategy* pull(PlayerbotAI* botAI) { return new DruidPullStrategy(botAI); } - static Strategy* cat_aoe(PlayerbotAI* botAI) { return new CatAoeDruidStrategy(botAI); } - static Strategy* caster_aoe(PlayerbotAI* botAI) { return new CasterDruidAoeStrategy(botAI); } - static Strategy* caster_debuff(PlayerbotAI* botAI) { return new CasterDruidDebuffStrategy(botAI); } + static Strategy* aoe(PlayerbotAI* botAI) { return new DruidAoeStrategy(botAI); } static Strategy* cure(PlayerbotAI* botAI) { return new DruidCureStrategy(botAI); } - static Strategy* melee(PlayerbotAI* botAI) { return new MeleeDruidStrategy(botAI); } static Strategy* buff(PlayerbotAI* botAI) { return new GenericDruidBuffStrategy(botAI); } static Strategy* boost(PlayerbotAI* botAI) { return new DruidBoostStrategy(botAI); } static Strategy* cc(PlayerbotAI* botAI) { return new DruidCcStrategy(botAI); } static Strategy* healer_dps(PlayerbotAI* botAI) { return new DruidHealerDpsStrategy(botAI); } + static Strategy* offheal(PlayerbotAI* botAI) { return new CatOffhealStrategy(botAI); } + static Strategy* blanketing(PlayerbotAI* botAI) { return new DruidBlanketStrategy(botAI); } + static Strategy* tranquility(PlayerbotAI* botAI) { return new DruidTranquilityStrategy(botAI); } + static Strategy* feral_charge(PlayerbotAI* botAI) { return new FeralChargeDruidStrategy(botAI); } }; class DruidDruidStrategyFactoryInternal : public NamedObjectContext @@ -62,18 +61,16 @@ public: creators["bear"] = &DruidDruidStrategyFactoryInternal::bear; creators["tank"] = &DruidDruidStrategyFactoryInternal::bear; creators["cat"] = &DruidDruidStrategyFactoryInternal::cat; - creators["caster"] = &DruidDruidStrategyFactoryInternal::caster; + creators["balance"] = &DruidDruidStrategyFactoryInternal::balance; creators["dps"] = &DruidDruidStrategyFactoryInternal::cat; - creators["heal"] = &DruidDruidStrategyFactoryInternal::heal; - creators["offheal"] = &DruidDruidStrategyFactoryInternal::offheal; + creators["resto"] = &DruidDruidStrategyFactoryInternal::heal; } private: - static Strategy* bear(PlayerbotAI* botAI) { return new BearTankDruidStrategy(botAI); } - static Strategy* cat(PlayerbotAI* botAI) { return new CatDpsDruidStrategy(botAI); } - static Strategy* caster(PlayerbotAI* botAI) { return new CasterDruidStrategy(botAI); } - static Strategy* heal(PlayerbotAI* botAI) { return new HealDruidStrategy(botAI); } - static Strategy* offheal(PlayerbotAI* botAI) { return new OffhealDruidCatStrategy(botAI); } + static Strategy* bear(PlayerbotAI* botAI) { return new BearDruidStrategy(botAI); } + static Strategy* cat(PlayerbotAI* botAI) { return new CatDruidStrategy(botAI); } + static Strategy* balance(PlayerbotAI* botAI) { return new BalanceDruidStrategy(botAI); } + static Strategy* heal(PlayerbotAI* botAI) { return new RestoDruidStrategy(botAI); } }; class DruidTriggerFactoryInternal : public NamedObjectContext @@ -81,7 +78,6 @@ class DruidTriggerFactoryInternal : public NamedObjectContext public: DruidTriggerFactoryInternal() { - creators["omen of clarity"] = &DruidTriggerFactoryInternal::omen_of_clarity; creators["clearcasting"] = &DruidTriggerFactoryInternal::clearcasting; creators["thorns"] = &DruidTriggerFactoryInternal::thorns; creators["thorns on party"] = &DruidTriggerFactoryInternal::thorns_on_party; @@ -90,10 +86,12 @@ public: creators["faerie fire (feral)"] = &DruidTriggerFactoryInternal::faerie_fire_feral; creators["faerie fire"] = &DruidTriggerFactoryInternal::faerie_fire; creators["insect swarm"] = &DruidTriggerFactoryInternal::insect_swarm; + creators["insect swarm on attacker"] = &DruidTriggerFactoryInternal::insect_swarm_on_attacker; creators["moonfire"] = &DruidTriggerFactoryInternal::moonfire; + creators["moonfire on attacker"] = &DruidTriggerFactoryInternal::moonfire_on_attacker; creators["nature's grasp"] = &DruidTriggerFactoryInternal::natures_grasp; - creators["tiger's fury"] = &DruidTriggerFactoryInternal::tigers_fury; creators["berserk"] = &DruidTriggerFactoryInternal::berserk; + creators["berserk active"] = &DruidTriggerFactoryInternal::berserk_active; creators["savage roar"] = &DruidTriggerFactoryInternal::savage_roar; creators["rake"] = &DruidTriggerFactoryInternal::rake; creators["mark of the wild"] = &DruidTriggerFactoryInternal::mark_of_the_wild; @@ -110,17 +108,34 @@ public: creators["eclipse (lunar)"] = &DruidTriggerFactoryInternal::eclipse_lunar; creators["bash on enemy healer"] = &DruidTriggerFactoryInternal::bash_on_enemy_healer; creators["nature's swiftness"] = &DruidTriggerFactoryInternal::natures_swiftness; + creators["nature's swiftness active"] = &DruidTriggerFactoryInternal::natures_swiftness_active; creators["party member remove curse"] = &DruidTriggerFactoryInternal::party_member_remove_curse; - creators["eclipse (solar) cooldown"] = &DruidTriggerFactoryInternal::eclipse_solar_cooldown; - creators["eclipse (lunar) cooldown"] = &DruidTriggerFactoryInternal::eclipse_lunar_cooldown; + creators["mangle (bear)"] = &DruidTriggerFactoryInternal::mangle_bear_trigger; + creators["lacerate"] = &DruidTriggerFactoryInternal::lacerate_trigger; + creators["demoralizing roar"] = &DruidTriggerFactoryInternal::demoralize_roar; creators["mangle (cat)"] = &DruidTriggerFactoryInternal::mangle_cat; creators["ferocious bite time"] = &DruidTriggerFactoryInternal::ferocious_bite_time; + creators["ferocious bite execute"] = &DruidTriggerFactoryInternal::ferocious_bite_execute; creators["hurricane channel check"] = &DruidTriggerFactoryInternal::hurricane_channel_check; creators["no healer dps strategy"] = &DruidTriggerFactoryInternal::no_healer_dps_strategy; + creators["starfall"] = &DruidTriggerFactoryInternal::starfall; + creators["force of nature"] = &DruidTriggerFactoryInternal::force_of_nature; + creators["cyclone"] = &DruidTriggerFactoryInternal::cyclone; + creators["predator's swiftness"] = &DruidTriggerFactoryInternal::predators_swiftness; + creators["predator's swiftness and cyclone"] = &DruidTriggerFactoryInternal::predators_swiftness_and_cyclone; + creators["predator's swiftness and hibernate"] = &DruidTriggerFactoryInternal::predators_swiftness_and_hibernate; + creators["predator's swiftness and entangling roots"] = &DruidTriggerFactoryInternal::predators_swiftness_and_entangling_roots; + creators["predator's swiftness and combat party member dead"] = &DruidTriggerFactoryInternal::predators_swiftness_and_combat_party_member_dead; + creators["clearcasting and medium aoe"] = &DruidTriggerFactoryInternal::clearcasting_and_medium_aoe; + creators["prowl"] = &DruidTriggerFactoryInternal::prowl_trigger; + creators["rejuvenation blanket"] = &DruidTriggerFactoryInternal::rejuvenation_blanket; + creators["wild growth blanket"] = &DruidTriggerFactoryInternal::wild_growth_blanket; + creators["aquatic form"] = &DruidTriggerFactoryInternal::aquatic_form; } private: static Trigger* natures_swiftness(PlayerbotAI* botAI) { return new NaturesSwiftnessTrigger(botAI); } + static Trigger* natures_swiftness_active(PlayerbotAI* botAI) { return new NaturesSwiftnessActiveTrigger(botAI); } static Trigger* clearcasting(PlayerbotAI* botAI) { return new ClearcastingTrigger(botAI); } static Trigger* eclipse_solar(PlayerbotAI* botAI) { return new EclipseSolarTrigger(botAI); } static Trigger* eclipse_lunar(PlayerbotAI* botAI) { return new EclipseLunarTrigger(botAI); } @@ -130,11 +145,13 @@ private: static Trigger* bash(PlayerbotAI* botAI) { return new BashInterruptSpellTrigger(botAI); } static Trigger* faerie_fire_feral(PlayerbotAI* botAI) { return new FaerieFireFeralTrigger(botAI); } static Trigger* insect_swarm(PlayerbotAI* botAI) { return new InsectSwarmTrigger(botAI); } + static Trigger* insect_swarm_on_attacker(PlayerbotAI* botAI) { return new InsectSwarmOnAttackerTrigger(botAI); } static Trigger* moonfire(PlayerbotAI* botAI) { return new MoonfireTrigger(botAI); } + static Trigger* moonfire_on_attacker(PlayerbotAI* botAI) { return new MoonfireOnAttackerTrigger(botAI); } static Trigger* faerie_fire(PlayerbotAI* botAI) { return new FaerieFireTrigger(botAI); } static Trigger* natures_grasp(PlayerbotAI* botAI) { return new NaturesGraspTrigger(botAI); } - static Trigger* tigers_fury(PlayerbotAI* botAI) { return new TigersFuryTrigger(botAI); } static Trigger* berserk(PlayerbotAI* botAI) { return new BerserkTrigger(botAI); } + static Trigger* berserk_active(PlayerbotAI* botAI) { return new BerserkActiveTrigger(botAI); } static Trigger* savage_roar(PlayerbotAI* botAI) { return new SavageRoarTrigger(botAI); } static Trigger* rake(PlayerbotAI* botAI) { return new RakeTrigger(botAI); } static Trigger* mark_of_the_wild(PlayerbotAI* botAI) { return new MarkOfTheWildTrigger(botAI); } @@ -148,14 +165,28 @@ private: static Trigger* cat_form(PlayerbotAI* botAI) { return new CatFormTrigger(botAI); } static Trigger* tree_form(PlayerbotAI* botAI) { return new TreeFormTrigger(botAI); } static Trigger* bash_on_enemy_healer(PlayerbotAI* botAI) { return new BashInterruptEnemyHealerSpellTrigger(botAI); } - static Trigger* omen_of_clarity(PlayerbotAI* botAI) { return new OmenOfClarityTrigger(botAI); } static Trigger* party_member_remove_curse(PlayerbotAI* ai) { return new DruidPartyMemberRemoveCurseTrigger(ai); } - static Trigger* eclipse_solar_cooldown(PlayerbotAI* ai) { return new EclipseSolarCooldownTrigger(ai); } - static Trigger* eclipse_lunar_cooldown(PlayerbotAI* ai) { return new EclipseLunarCooldownTrigger(ai); } + static Trigger* mangle_bear_trigger(PlayerbotAI* botAI) { return new MangleBearTrigger(botAI); } + static Trigger* lacerate_trigger(PlayerbotAI* botAI) { return new LacerateTrigger(botAI); } + static Trigger* demoralize_roar(PlayerbotAI* botAI) { return new DemoralizeRoarTrigger(botAI); } static Trigger* mangle_cat(PlayerbotAI* ai) { return new MangleCatTrigger(ai); } static Trigger* ferocious_bite_time(PlayerbotAI* ai) { return new FerociousBiteTimeTrigger(ai); } + static Trigger* ferocious_bite_execute(PlayerbotAI* ai) { return new FerociousBiteExecuteTrigger(ai); } static Trigger* hurricane_channel_check(PlayerbotAI* ai) { return new HurricaneChannelCheckTrigger(ai); } static Trigger* no_healer_dps_strategy(PlayerbotAI* ai) { return new NoHealerDpsStrategyTrigger(ai); } + static Trigger* starfall(PlayerbotAI* ai) { return new StarfallTrigger(ai); } + static Trigger* force_of_nature(PlayerbotAI* ai) { return new ForceOfNatureTrigger(ai); } + static Trigger* cyclone(PlayerbotAI* ai) { return new CycloneTrigger(ai); } + static Trigger* predators_swiftness(PlayerbotAI* ai) { return new PredatorsSwiftnessTrigger(ai); } + static Trigger* predators_swiftness_and_cyclone(PlayerbotAI* ai) { return new TwoTriggers(ai, "predator's swiftness", "cyclone"); } + static Trigger* predators_swiftness_and_hibernate(PlayerbotAI* ai) { return new TwoTriggers(ai, "predator's swiftness", "hibernate"); } + static Trigger* predators_swiftness_and_entangling_roots(PlayerbotAI* ai) { return new TwoTriggers(ai, "predator's swiftness", "entangling roots"); } + static Trigger* predators_swiftness_and_combat_party_member_dead(PlayerbotAI* ai) { return new TwoTriggers(ai, "predator's swiftness", "combat party member dead"); } + static Trigger* clearcasting_and_medium_aoe(PlayerbotAI* ai) { return new TwoTriggers(ai, "clearcasting", "medium aoe"); } + static Trigger* prowl_trigger(PlayerbotAI* ai) { return new ProwlTrigger(ai); } + static Trigger* rejuvenation_blanket(PlayerbotAI* ai) { return new BuffOnPartyTrigger(ai, "rejuvenation"); } + static Trigger* wild_growth_blanket(PlayerbotAI* ai) { return new BuffOnPartyTrigger(ai, "wild growth"); } + static Trigger* aquatic_form(PlayerbotAI* ai) { return new AquaticFormTrigger(ai); } }; class DruidAiObjectContextInternal : public NamedObjectContext @@ -193,8 +224,8 @@ public: creators["hibernate"] = &DruidAiObjectContextInternal::hibernate; creators["entangling roots"] = &DruidAiObjectContextInternal::entangling_roots; creators["entangling roots on cc"] = &DruidAiObjectContextInternal::entangling_roots_on_cc; - creators["hibernate"] = &DruidAiObjectContextInternal::hibernate; creators["hibernate on cc"] = &DruidAiObjectContextInternal::hibernate_on_cc; + creators["cyclone on cc"] = &DruidAiObjectContextInternal::cyclone_on_cc; creators["wrath"] = &DruidAiObjectContextInternal::wrath; creators["starfall"] = &DruidAiObjectContextInternal::starfall; creators["insect swarm"] = &DruidAiObjectContextInternal::insect_swarm; @@ -205,9 +236,9 @@ public: creators["mangle (cat)"] = &DruidAiObjectContextInternal::mangle_cat; creators["swipe (cat)"] = &DruidAiObjectContextInternal::swipe_cat; creators["rake"] = &DruidAiObjectContextInternal::rake; - creators["rake on attacker"] = &DruidAiObjectContextInternal::rake_on_attacker; creators["ferocious bite"] = &DruidAiObjectContextInternal::ferocious_bite; creators["rip"] = &DruidAiObjectContextInternal::rip; + creators["maim"] = &DruidAiObjectContextInternal::maim; creators["cower"] = &DruidAiObjectContextInternal::cower; creators["survival instincts"] = &DruidAiObjectContextInternal::survival_instincts; creators["frenzied regeneration"] = &DruidAiObjectContextInternal::frenzied_regeneration; @@ -237,9 +268,9 @@ public: creators["lacerate"] = &DruidAiObjectContextInternal::lacerate; creators["hurricane"] = &DruidAiObjectContextInternal::hurricane; creators["innervate"] = &DruidAiObjectContextInternal::innervate; + creators["innervate on healer"] = &DruidAiObjectContextInternal::innervate_on_healer; creators["tranquility"] = &DruidAiObjectContextInternal::tranquility; creators["bash on enemy healer"] = &DruidAiObjectContextInternal::bash_on_enemy_healer; - creators["omen of clarity"] = &DruidAiObjectContextInternal::omen_of_clarity; creators["nature's swiftness"] = &DruidAiObjectContextInternal::natures_swiftness; creators["prowl"] = &DruidAiObjectContextInternal::prowl; creators["dash"] = &DruidAiObjectContextInternal::dash; @@ -254,11 +285,13 @@ public: creators["moonfire on attacker"] = &DruidAiObjectContextInternal::moonfire_on_attacker; creators["enrage"] = &DruidAiObjectContextInternal::enrage; creators["force of nature"] = &DruidAiObjectContextInternal::force_of_nature; + creators["typhoon"] = &DruidAiObjectContextInternal::typhoon; + creators["rejuvenation blanket"] = &DruidAiObjectContextInternal::rejuvenation_blanket; + creators["wild growth blanket"] = &DruidAiObjectContextInternal::wild_growth_blanket; } private: static Action* natures_swiftness(PlayerbotAI* botAI) { return new CastNaturesSwiftnessAction(botAI); } - static Action* omen_of_clarity(PlayerbotAI* botAI) { return new CastOmenOfClarityAction(botAI); } static Action* tranquility(PlayerbotAI* botAI) { return new CastTranquilityAction(botAI); } static Action* feral_charge_bear(PlayerbotAI* botAI) { return new CastFeralChargeBearAction(botAI); } static Action* feral_charge_cat(PlayerbotAI* botAI) { return new CastFeralChargeCatAction(botAI); } @@ -291,6 +324,7 @@ private: static Action* entangling_roots(PlayerbotAI* botAI) { return new CastEntanglingRootsAction(botAI); } static Action* hibernate_on_cc(PlayerbotAI* botAI) { return new CastHibernateCcAction(botAI); } static Action* entangling_roots_on_cc(PlayerbotAI* botAI) { return new CastEntanglingRootsCcAction(botAI); } + static Action* cyclone_on_cc(PlayerbotAI* botAI) { return new CastCycloneCcAction(botAI); } static Action* wrath(PlayerbotAI* botAI) { return new CastWrathAction(botAI); } static Action* starfall(PlayerbotAI* botAI) { return new CastStarfallAction(botAI); } static Action* insect_swarm(PlayerbotAI* botAI) { return new CastInsectSwarmAction(botAI); } @@ -301,9 +335,9 @@ private: static Action* mangle_cat(PlayerbotAI* botAI) { return new CastMangleCatAction(botAI); } static Action* swipe_cat(PlayerbotAI* botAI) { return new CastSwipeCatAction(botAI); } static Action* rake(PlayerbotAI* botAI) { return new CastRakeAction(botAI); } - static Action* rake_on_attacker(PlayerbotAI* botAI) { return new CastRakeOnMeleeAttackersAction(botAI); } static Action* ferocious_bite(PlayerbotAI* botAI) { return new CastFerociousBiteAction(botAI); } static Action* rip(PlayerbotAI* botAI) { return new CastRipAction(botAI); } + static Action* maim(PlayerbotAI* botAI) { return new CastMaimAction(botAI); } static Action* cower(PlayerbotAI* botAI) { return new CastCowerAction(botAI); } static Action* survival_instincts(PlayerbotAI* botAI) { return new CastSurvivalInstinctsAction(botAI); } static Action* frenzied_regeneration(PlayerbotAI* botAI) { return new CastFrenziedRegenerationAction(botAI); } @@ -333,6 +367,7 @@ private: static Action* lacerate(PlayerbotAI* botAI) { return new CastLacerateAction(botAI); } static Action* hurricane(PlayerbotAI* botAI) { return new CastHurricaneAction(botAI); } static Action* innervate(PlayerbotAI* botAI) { return new CastInnervateAction(botAI); } + static Action* innervate_on_healer(PlayerbotAI* botAI) { return new CastInnervateOnHealerAction(botAI); } static Action* bash_on_enemy_healer(PlayerbotAI* botAI) { return new CastBashOnEnemyHealerAction(botAI); } static Action* ravage(PlayerbotAI* botAI) { return new CastRavageAction(botAI); } static Action* pounce(PlayerbotAI* botAI) { return new CastPounceAction(botAI); } @@ -347,6 +382,9 @@ private: static Action* moonfire_on_attacker(PlayerbotAI* ai) { return new CastMoonfireOnAttackerAction(ai); } static Action* enrage(PlayerbotAI* ai) { return new CastEnrageAction(ai); } static Action* force_of_nature(PlayerbotAI* ai) { return new CastForceOfNatureAction(ai); } + static Action* typhoon(PlayerbotAI* ai) { return new CastTyphoonAction(ai); } + static Action* rejuvenation_blanket(PlayerbotAI* ai) { return new CastRejuvenationBlanketAction(ai); } + static Action* wild_growth_blanket(PlayerbotAI* ai) { return new CastWildGrowthBlanketAction(ai); } }; SharedNamedObjectContextList DruidAiObjectContext::sharedStrategyContexts; @@ -386,7 +424,22 @@ void DruidAiObjectContext::BuildSharedTriggerContexts(SharedNamedObjectContextLi triggerContexts.Add(new DruidTriggerFactoryInternal()); } +class DruidValueContextInternal : public NamedObjectContext +{ +public: + DruidValueContextInternal() + { + creators["eclipse solar proc time"] = &DruidValueContextInternal::eclipse_solar_proc_time; + creators["eclipse lunar proc time"] = &DruidValueContextInternal::eclipse_lunar_proc_time; + } + +private: + static UntypedValue* eclipse_solar_proc_time(PlayerbotAI* botAI) { return new EclipseSolarProcTimeValue(botAI); } + static UntypedValue* eclipse_lunar_proc_time(PlayerbotAI* botAI) { return new EclipseLunarProcTimeValue(botAI); } +}; + void DruidAiObjectContext::BuildSharedValueContexts(SharedNamedObjectContextList& valueContexts) { AiObjectContext::BuildSharedValueContexts(valueContexts); + valueContexts.Add(new DruidValueContextInternal()); } diff --git a/src/Ai/Class/Druid/Strategy/CasterDruidStrategy.cpp b/src/Ai/Class/Druid/Strategy/BalanceDruidStrategy.cpp similarity index 52% rename from src/Ai/Class/Druid/Strategy/CasterDruidStrategy.cpp rename to src/Ai/Class/Druid/Strategy/BalanceDruidStrategy.cpp index a91f3b540..f3fbc2218 100644 --- a/src/Ai/Class/Druid/Strategy/CasterDruidStrategy.cpp +++ b/src/Ai/Class/Druid/Strategy/BalanceDruidStrategy.cpp @@ -3,15 +3,15 @@ * and/or modify it under version 3 of the License, or (at your option), any later version. */ -#include "CasterDruidStrategy.h" +#include "BalanceDruidStrategy.h" #include "AiObjectContext.h" #include "FeralDruidStrategy.h" -class CasterDruidStrategyActionNodeFactory : public NamedObjectFactory +class BalanceDruidStrategyActionNodeFactory : public NamedObjectFactory { public: - CasterDruidStrategyActionNodeFactory() + BalanceDruidStrategyActionNodeFactory() { creators["faerie fire"] = &faerie_fire; creators["hibernate"] = &hibernate; @@ -23,6 +23,10 @@ public: creators["moonfire"] = &moonfire; creators["starfire"] = &starfire; creators["moonkin form"] = &moonkin_form; + creators["typhoon"] = &typhoon; + creators["hurricane"] = &hurricane; + creators["force of nature"] = &force_of_nature; + creators["cyclone on cc"] = &cyclone_on_cc; } private: @@ -125,130 +129,76 @@ private: /*C*/ {} ); } + + static ActionNode* typhoon([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "typhoon", + /*P*/ { NextAction("moonkin form") }, + /*A*/ {}, + /*C*/ {} + ); + } + + static ActionNode* hurricane([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "hurricane", + /*P*/ { NextAction("moonkin form") }, + /*A*/ {}, + /*C*/ {} + ); + } + + static ActionNode* force_of_nature([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "force of nature", + /*P*/ { NextAction("moonkin form") }, + /*A*/ {}, + /*C*/ {} + ); + } + + static ActionNode* cyclone_on_cc([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "cyclone on cc", + /*P*/ { NextAction("moonkin form") }, + /*A*/ {}, + /*C*/ {} + ); + } }; -CasterDruidStrategy::CasterDruidStrategy(PlayerbotAI* botAI) : GenericDruidStrategy(botAI) +BalanceDruidStrategy::BalanceDruidStrategy(PlayerbotAI* botAI) : GenericDruidStrategy(botAI) { - actionNodeFactories.Add(new CasterDruidStrategyActionNodeFactory()); + actionNodeFactories.Add(new BalanceDruidStrategyActionNodeFactory()); actionNodeFactories.Add(new ShapeshiftDruidStrategyActionNodeFactory()); } -std::vector CasterDruidStrategy::getDefaultActions() +std::vector BalanceDruidStrategy::getDefaultActions() { return { - NextAction("starfall", ACTION_HIGH + 1.0f), - NextAction("force of nature", ACTION_DEFAULT + 1.0f), - NextAction("wrath", ACTION_DEFAULT + 0.1f), + NextAction("starfire", 5.4f), + NextAction("wrath", 5.3f), }; } -void CasterDruidStrategy::InitTriggers(std::vector& triggers) +void BalanceDruidStrategy::InitTriggers(std::vector& triggers) { GenericDruidStrategy::InitTriggers(triggers); - triggers.push_back( - new TriggerNode( - "eclipse (lunar) cooldown", - { - NextAction("starfire", ACTION_DEFAULT + 0.2f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "eclipse (solar) cooldown", - { - NextAction("wrath", ACTION_DEFAULT + 0.2f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "insect swarm", - { - NextAction("insect swarm", ACTION_NORMAL + 5) - } - ) - ); - triggers.push_back( - new TriggerNode( - "moonfire", - { - NextAction("moonfire", ACTION_NORMAL + 4) - } - ) - ); - triggers.push_back( - new TriggerNode( - "eclipse (solar)", - { - NextAction("wrath", ACTION_NORMAL + 6) - } - ) - ); - triggers.push_back( - new TriggerNode( - "eclipse (lunar)", - { - NextAction("starfire", ACTION_NORMAL + 6) - } - ) - ); - triggers.push_back( - new TriggerNode( - "medium mana", - { - NextAction("innervate", ACTION_HIGH + 9) - } - ) - ); - triggers.push_back( - new TriggerNode( - "enemy too close for spell", - { - NextAction("flee", ACTION_MOVE + 9) - } - ) - ); -} + // Debuffs and DoTs + triggers.push_back(new TriggerNode("faerie fire", { NextAction("faerie fire", 29.5f) })); + triggers.push_back(new TriggerNode("insect swarm", { NextAction("insect swarm", 18.0f) })); + triggers.push_back(new TriggerNode("moonfire", { NextAction("moonfire", 17.5f) })); -void CasterDruidAoeStrategy::InitTriggers(std::vector& triggers) -{ - triggers.push_back( - new TriggerNode( - "hurricane channel check", - { - NextAction("cancel channel", ACTION_HIGH + 2) - } - ) - ); - triggers.push_back( - new TriggerNode( - "medium aoe", - { - NextAction("hurricane", ACTION_HIGH + 1) - } - ) - ); - triggers.push_back( - new TriggerNode( - "light aoe", - { - NextAction("insect swarm on attacker", ACTION_NORMAL + 3), - NextAction("moonfire on attacker", ACTION_NORMAL + 3) - } - ) - ); -} + // Eclipse procs + triggers.push_back(new TriggerNode("eclipse (solar)", { NextAction("wrath", 20.0f) })); + triggers.push_back(new TriggerNode("eclipse (lunar)", { NextAction("starfire", 20.0f) })); -void CasterDruidDebuffStrategy::InitTriggers(std::vector& triggers) -{ - triggers.push_back( - new TriggerNode( - "faerie fire", - { - NextAction("faerie fire", ACTION_HIGH) - } - ) - ); + // Utility/Defensive + triggers.push_back(new TriggerNode("medium mana", { NextAction("innervate", 29.0f) })); + triggers.push_back(new TriggerNode("enemy too close for spell", { NextAction("flee", 39.0f) })); } diff --git a/src/Ai/Class/Druid/Strategy/BalanceDruidStrategy.h b/src/Ai/Class/Druid/Strategy/BalanceDruidStrategy.h new file mode 100644 index 000000000..d01db4901 --- /dev/null +++ b/src/Ai/Class/Druid/Strategy/BalanceDruidStrategy.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_BALANCEDRUIDSTRATEGY_H +#define _PLAYERBOT_BALANCEDRUIDSTRATEGY_H + +#include "GenericDruidStrategy.h" + +class PlayerbotAI; + +class BalanceDruidStrategy : public GenericDruidStrategy +{ +public: + BalanceDruidStrategy(PlayerbotAI* botAI); + +public: + void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "balance"; } + std::vector getDefaultActions() override; + uint32 GetType() const override { return STRATEGY_TYPE_COMBAT | STRATEGY_TYPE_DPS | STRATEGY_TYPE_RANGED; } +}; + +#endif diff --git a/src/Ai/Class/Druid/Strategy/BearTankDruidStrategy.cpp b/src/Ai/Class/Druid/Strategy/BearDruidStrategy.cpp similarity index 59% rename from src/Ai/Class/Druid/Strategy/BearTankDruidStrategy.cpp rename to src/Ai/Class/Druid/Strategy/BearDruidStrategy.cpp index 13af635c3..418f057cd 100644 --- a/src/Ai/Class/Druid/Strategy/BearTankDruidStrategy.cpp +++ b/src/Ai/Class/Druid/Strategy/BearDruidStrategy.cpp @@ -3,19 +3,17 @@ * and/or modify it under version 3 of the License, or (at your option), any later version. */ -#include "BearTankDruidStrategy.h" +#include "BearDruidStrategy.h" #include "Playerbots.h" -class BearTankDruidStrategyActionNodeFactory : public NamedObjectFactory +class BearDruidStrategyActionNodeFactory : public NamedObjectFactory { public: - BearTankDruidStrategyActionNodeFactory() + BearDruidStrategyActionNodeFactory() { - creators["melee"] = &melee; creators["feral charge - bear"] = &feral_charge_bear; creators["swipe (bear)"] = &swipe_bear; - creators["faerie fire (feral)"] = &faerie_fire_feral; creators["bear form"] = &bear_form; creators["dire bear form"] = &dire_bear_form; creators["mangle (bear)"] = &mangle_bear; @@ -28,16 +26,6 @@ public: } private: - static ActionNode* melee([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "melee", - /*P*/ { NextAction("feral charge - bear") }, - /*A*/ {}, - /*C*/ {} - ); - } - static ActionNode* feral_charge_bear([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode( @@ -58,16 +46,6 @@ private: ); } - static ActionNode* faerie_fire_feral([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "faerie fire (feral)", - /*P*/ { NextAction("feral charge - bear") }, - /*A*/ {}, - /*C*/ {} - ); - } - static ActionNode* bear_form([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode( @@ -159,99 +137,81 @@ private: } }; -BearTankDruidStrategy::BearTankDruidStrategy(PlayerbotAI* botAI) : FeralDruidStrategy(botAI) +BearDruidStrategy::BearDruidStrategy(PlayerbotAI* botAI) : FeralDruidStrategy(botAI) { - actionNodeFactories.Add(new BearTankDruidStrategyActionNodeFactory()); + actionNodeFactories.Add(new BearDruidStrategyActionNodeFactory()); } -std::vector BearTankDruidStrategy::getDefaultActions() +std::vector BearDruidStrategy::getDefaultActions() { return { - NextAction("mangle (bear)", ACTION_DEFAULT + 0.5f), - NextAction("faerie fire (feral)", ACTION_DEFAULT + 0.4f), - NextAction("lacerate", ACTION_DEFAULT + 0.3f), - NextAction("maul", ACTION_DEFAULT + 0.2f), - NextAction("enrage", ACTION_DEFAULT + 0.1f), - NextAction("melee", ACTION_DEFAULT) + NextAction("maul", 5.2f), + NextAction("enrage", 5.1f), + NextAction("melee", 5.0f) }; } -void BearTankDruidStrategy::InitTriggers(std::vector& triggers) +void BearDruidStrategy::InitTriggers(std::vector& triggers) { FeralDruidStrategy::InitTriggers(triggers); - triggers.push_back( - new TriggerNode( - "enemy out of melee", - { - NextAction("feral charge - bear", ACTION_NORMAL + 8) - } - ) - ); triggers.push_back( new TriggerNode( "bear form", - { - NextAction("dire bear form", ACTION_HIGH + 8) - } + { NextAction("dire bear form", 28.0f) } ) ); triggers.push_back( new TriggerNode( - "low health", - { - NextAction("frenzied regeneration", ACTION_HIGH + 7) - } - ) - ); - triggers.push_back( - new TriggerNode( - "faerie fire (feral)", - { - NextAction("faerie fire (feral)", ACTION_HIGH + 7) - } - ) - ); - triggers.push_back(new TriggerNode("high aoe", {NextAction("challenging roar", ACTION_HIGH + 8)})); - triggers.push_back( - new TriggerNode( - "lose aggro", - { - NextAction("growl", ACTION_HIGH + 8) - } + "medium health", + { NextAction("frenzied regeneration", 27.0f) } ) ); + triggers.push_back(new TriggerNode( + "mangle (bear)", { NextAction("mangle (bear)", 17.5f) } + )); + triggers.push_back(new TriggerNode( + "faerie fire (feral)", { NextAction("faerie fire (feral)", 17.0f) } + )); + triggers.push_back(new TriggerNode( + "lacerate", { NextAction("lacerate", 16.0f) } + )); + triggers.push_back(new TriggerNode( + "demoralizing roar", { NextAction("demoralizing roar", 15.5f) } + )); + triggers.push_back(new TriggerNode("high aoe", { NextAction("challenging roar", 26.5f) })); + triggers.push_back(new TriggerNode("lose aggro", + { + NextAction("growl", 26.0f), + NextAction("faerie fire (feral)", 25.5f) + } + )); + triggers.push_back(new TriggerNode("berserk active", { NextAction("mangle (bear)", 25.0f) })); triggers.push_back( new TriggerNode( "medium aoe", { - NextAction("demoralizing roar", ACTION_HIGH + 6), - NextAction("swipe (bear)", ACTION_HIGH + 6) + NextAction("demoralizing roar", 24.5f), + NextAction("swipe (bear)", 24.0f) } ) ); triggers.push_back( new TriggerNode( "light aoe", - { - NextAction("swipe (bear)", ACTION_HIGH + 5) - } + { NextAction("swipe (bear)", 24.0f) } ) ); triggers.push_back( new TriggerNode( "bash", - { - NextAction("bash", ACTION_INTERRUPT + 2) - } + { NextAction("bash", 42.0f) } ) ); triggers.push_back( new TriggerNode( "bash on enemy healer", - { - NextAction("bash on enemy healer", ACTION_INTERRUPT + 1) - } + { NextAction("bash on enemy healer", 41.0f) } ) ); } diff --git a/src/Ai/Class/Druid/Strategy/BearTankDruidStrategy.h b/src/Ai/Class/Druid/Strategy/BearDruidStrategy.h similarity index 75% rename from src/Ai/Class/Druid/Strategy/BearTankDruidStrategy.h rename to src/Ai/Class/Druid/Strategy/BearDruidStrategy.h index 2019bd0eb..1e47fd08a 100644 --- a/src/Ai/Class/Druid/Strategy/BearTankDruidStrategy.h +++ b/src/Ai/Class/Druid/Strategy/BearDruidStrategy.h @@ -3,17 +3,17 @@ * and/or modify it under version 3 of the License, or (at your option), any later version. */ -#ifndef _PLAYERBOT_BEARTANKDRUIDSTRATEGY_H -#define _PLAYERBOT_BEARTANKDRUIDSTRATEGY_H +#ifndef _PLAYERBOT_BEARDRUIDSTRATEGY_H +#define _PLAYERBOT_BEARDRUIDSTRATEGY_H #include "FeralDruidStrategy.h" class PlayerbotAI; -class BearTankDruidStrategy : public FeralDruidStrategy +class BearDruidStrategy : public FeralDruidStrategy { public: - BearTankDruidStrategy(PlayerbotAI* botAI); + BearDruidStrategy(PlayerbotAI* botAI); void InitTriggers(std::vector& triggers) override; std::string const getName() override { return "bear"; } diff --git a/src/Ai/Class/Druid/Strategy/CasterDruidStrategy.h b/src/Ai/Class/Druid/Strategy/CasterDruidStrategy.h deleted file mode 100644 index 45bc78dba..000000000 --- a/src/Ai/Class/Druid/Strategy/CasterDruidStrategy.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it - * and/or modify it under version 3 of the License, or (at your option), any later version. - */ - -#ifndef _PLAYERBOT_CASTERDRUIDSTRATEGY_H -#define _PLAYERBOT_CASTERDRUIDSTRATEGY_H - -#include "GenericDruidStrategy.h" - -class PlayerbotAI; - -class CasterDruidStrategy : public GenericDruidStrategy -{ -public: - CasterDruidStrategy(PlayerbotAI* botAI); - -public: - void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "caster"; } - std::vector getDefaultActions() override; - uint32 GetType() const override { return STRATEGY_TYPE_COMBAT | STRATEGY_TYPE_DPS | STRATEGY_TYPE_RANGED; } -}; - -class CasterDruidAoeStrategy : public CombatStrategy -{ -public: - CasterDruidAoeStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) {} - -public: - void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "caster aoe"; } -}; - -class CasterDruidDebuffStrategy : public CombatStrategy -{ -public: - CasterDruidDebuffStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) {} - -public: - void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "caster debuff"; } -}; - -#endif diff --git a/src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.cpp b/src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.cpp deleted file mode 100644 index f7a76b0fc..000000000 --- a/src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.cpp +++ /dev/null @@ -1,314 +0,0 @@ -/* - * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it - * and/or modify it under version 3 of the License, or (at your option), any later version. - */ - -#include "CatDpsDruidStrategy.h" - -#include "AiObjectContext.h" - -class CatDpsDruidStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - CatDpsDruidStrategyActionNodeFactory() - { - creators["faerie fire (feral)"] = &faerie_fire_feral; - creators["melee"] = &melee; - creators["feral charge - cat"] = &feral_charge_cat; - creators["cat form"] = &cat_form; - creators["claw"] = &claw; - creators["mangle (cat)"] = &mangle_cat; - creators["rake"] = &rake; - creators["ferocious bite"] = &ferocious_bite; - creators["rip"] = &rip; - creators["pounce"] = &pounce; - creators["ravage"] = &ravage; - } - -private: - static ActionNode* faerie_fire_feral([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "faerie fire (feral)", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* melee([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "melee", - /*P*/ { NextAction("feral charge - cat") }, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* feral_charge_cat([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "feral charge - cat", - /*P*/ {}, - /*A*/ { NextAction("reach melee") }, - /*C*/ {} - ); - } - - static ActionNode* cat_form([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "cat form", - /*P*/ { NextAction("caster form") }, - /*A*/ { NextAction("bear form") }, - /*C*/ {} - ); - } - - static ActionNode* claw([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "claw", - /*P*/ {}, - /*A*/ { NextAction("melee") }, - /*C*/ {} - ); - } - - static ActionNode* mangle_cat([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "mangle (cat)", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* rake([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "rake", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* ferocious_bite([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "ferocious bite", - /*P*/ {}, - /*A*/ { NextAction("rip") }, - /*C*/ {} - ); - } - - static ActionNode* rip([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "rip", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* pounce([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "pounce", - /*P*/ {}, - /*A*/ { NextAction("ravage") }, - /*C*/ {} - ); - } - - static ActionNode* ravage([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "ravage", - /*P*/ {}, - /*A*/ { NextAction("shred") }, - /*C*/ {} - ); - } -}; - -CatDpsDruidStrategy::CatDpsDruidStrategy(PlayerbotAI* botAI) : FeralDruidStrategy(botAI) -{ - actionNodeFactories.Add(new CatDpsDruidStrategyActionNodeFactory()); -} - -std::vector CatDpsDruidStrategy::getDefaultActions() -{ - return { - NextAction("tiger's fury", ACTION_DEFAULT + 0.1f) - }; -} - -void CatDpsDruidStrategy::InitTriggers(std::vector& triggers) -{ - FeralDruidStrategy::InitTriggers(triggers); - - // Default priority - triggers.push_back( - new TriggerNode( - "almost full energy available", - { - NextAction("shred", ACTION_DEFAULT + 0.4f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "combo points not full", - { - NextAction("shred", ACTION_DEFAULT + 0.4f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "almost full energy available", - { - NextAction("mangle (cat)", ACTION_DEFAULT + 0.3f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "combo points not full and high energy", - { - NextAction("mangle (cat)", ACTION_DEFAULT + 0.3f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "almost full energy available", - { - NextAction("claw", ACTION_DEFAULT + 0.2f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "combo points not full and high energy", - { - NextAction("claw", ACTION_DEFAULT + 0.2f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "faerie fire (feral)", - { - NextAction("faerie fire (feral)", ACTION_DEFAULT + 0.0f) - } - ) - ); - - // Main spell - triggers.push_back( - new TriggerNode( - "cat form", { - NextAction("cat form", ACTION_HIGH + 8) - } - ) - ); - triggers.push_back( - new TriggerNode( - "savage roar", { - NextAction("savage roar", ACTION_HIGH + 7) - } - ) - ); - triggers.push_back( - new TriggerNode( - "combo points 5 available", - { - NextAction("rip", ACTION_HIGH + 6) - } - ) - ); - triggers.push_back( - new TriggerNode( - "ferocious bite time", - { - NextAction("ferocious bite", ACTION_HIGH + 5) - } - ) - ); - triggers.push_back( - new TriggerNode( - "target with combo points almost dead", - { - NextAction("ferocious bite", ACTION_HIGH + 4) - } - ) - ); - triggers.push_back( - new TriggerNode( - "mangle (cat)", - { - NextAction("mangle (cat)", ACTION_HIGH + 3) - } - ) - ); - triggers.push_back( - new TriggerNode( - "rake", - { - NextAction("rake", ACTION_HIGH + 2) - } - ) - ); - triggers.push_back( - new TriggerNode( - "medium threat", - { - NextAction("cower", ACTION_HIGH + 1) - } - ) - ); - - // AOE - triggers.push_back( - new TriggerNode( - "medium aoe", - { - NextAction("swipe (cat)", ACTION_HIGH + 3) - } - ) - ); - triggers.push_back( - new TriggerNode( - "light aoe", - { - NextAction("rake on attacker", ACTION_HIGH + 2) - } - ) - ); - // Reach target - triggers.push_back( - new TriggerNode( - "enemy out of melee", - { - NextAction("feral charge - cat", ACTION_HIGH + 9) - } - ) - ); - triggers.push_back( - new TriggerNode( - "enemy out of melee", - { - NextAction("dash", ACTION_HIGH + 8) - } - ) - ); -} - -void CatAoeDruidStrategy::InitTriggers(std::vector& /*triggers*/) {} diff --git a/src/Ai/Class/Druid/Strategy/CatDruidStrategy.cpp b/src/Ai/Class/Druid/Strategy/CatDruidStrategy.cpp new file mode 100644 index 000000000..df6ba4a92 --- /dev/null +++ b/src/Ai/Class/Druid/Strategy/CatDruidStrategy.cpp @@ -0,0 +1,446 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#include "CatDruidStrategy.h" + +#include "AiObjectContext.h" + +class CatDruidStrategyActionNodeFactory : public NamedObjectFactory +{ +public: + CatDruidStrategyActionNodeFactory() + { + creators["faerie fire (feral)"] = &faerie_fire_feral; + creators["melee"] = &melee; + creators["feral charge - cat"] = &feral_charge_cat; + creators["cat form"] = &cat_form; + creators["claw"] = &claw; + creators["mangle (cat)"] = &mangle_cat; + creators["rake"] = &rake; + creators["ferocious bite"] = &ferocious_bite; + creators["rip"] = &rip; + creators["pounce"] = &pounce; + creators["ravage"] = &ravage; + creators["prowl"] = &prowl; + } + +private: + static ActionNode* faerie_fire_feral([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "faerie fire (feral)", + /*P*/ {}, + /*A*/ {}, + /*C*/ {} + ); + } + + static ActionNode* melee([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "melee", + /*P*/ {}, + /*A*/ {}, + /*C*/ {} + ); + } + + static ActionNode* feral_charge_cat([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "feral charge - cat", + /*P*/ {}, + /*A*/ { NextAction("reach melee") }, + /*C*/ {} + ); + } + + static ActionNode* cat_form([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "cat form", + /*P*/ { NextAction("caster form") }, + /*A*/ { NextAction("bear form") }, + /*C*/ {} + ); + } + + static ActionNode* claw([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "claw", + /*P*/ {}, + /*A*/ { NextAction("melee") }, + /*C*/ {} + ); + } + + static ActionNode* mangle_cat([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "mangle (cat)", + /*P*/ {}, + /*A*/ {}, + /*C*/ {} + ); + } + + static ActionNode* rake([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "rake", + /*P*/ {}, + /*A*/ {}, + /*C*/ {} + ); + } + + static ActionNode* ferocious_bite([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "ferocious bite", + /*P*/ {}, + /*A*/ {}, + /*C*/ {} + ); + } + + static ActionNode* rip([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "rip", + /*P*/ {}, + /*A*/ {}, + /*C*/ {} + ); + } + + static ActionNode* ravage([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "ravage", + /*P*/ {}, + /*A*/ { NextAction("pounce") }, + /*C*/ {} + ); + } + + static ActionNode* pounce([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "pounce", + /*P*/ {}, + /*A*/ { NextAction("shred") }, + /*C*/ {} + ); + } + + static ActionNode* prowl([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "prowl", + /*P*/ { NextAction("cat form") }, + /*A*/ {}, + /*C*/ {} + ); + } + +}; + +CatDruidStrategy::CatDruidStrategy(PlayerbotAI* botAI) : FeralDruidStrategy(botAI) +{ + actionNodeFactories.Add(new CatDruidStrategyActionNodeFactory()); +} + +std::vector CatDruidStrategy::getDefaultActions() +{ + return { + NextAction("melee", ACTION_DEFAULT) + }; +} + +void CatDruidStrategy::InitTriggers(std::vector& triggers) +{ + FeralDruidStrategy::InitTriggers(triggers); + + triggers.push_back( + new TriggerNode( + "healer low mana", { + NextAction("innervate on healer", 35.0f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "prowl", { + NextAction("prowl", 29.5f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "enemy out of melee", { + NextAction("dash", 28.0f) + } + ) + ); + + triggers.push_back( + new TriggerNode( + "cat form", { + NextAction("cat form", 28.0f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "low energy", { + NextAction("tiger's fury", 27.0f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "savage roar", { + NextAction("savage roar", 26.0f) + } + ) + ); + + triggers.push_back( + new TriggerNode( + "combo points 5 available", { + NextAction("rip", 23.5f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "combo points 5 available", { + NextAction("maim", 23.0f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "ferocious bite execute", { + NextAction("ferocious bite", 24.0f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "clearcasting", { + NextAction("shred", 24.5f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "ferocious bite time", { + NextAction("ferocious bite", 22.5f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "mangle (cat)", { + NextAction("mangle (cat)", 22.0f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "rake", { + NextAction("rake", 21.5f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "medium threat", { + NextAction("cower", 21.0f) + } + ) + ); + + triggers.push_back( + new TriggerNode( + "almost full energy available", { + NextAction("ravage", 5.6f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "combo points not full", { + NextAction("ravage", 5.6f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "almost full energy available", { + NextAction("pounce", 5.5f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "combo points not full", { + NextAction("pounce", 5.5f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "almost full energy available", { + NextAction("shred", 5.4f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "combo points not full", { + NextAction("shred", 5.4f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "almost full energy available", { + NextAction("mangle (cat)", 5.3f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "combo points not full and high energy", { + NextAction("mangle (cat)", 5.3f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "almost full energy available", { + NextAction("claw", 5.2f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "combo points not full and high energy", { + NextAction("claw", 5.2f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "faerie fire (feral)", { + NextAction("faerie fire (feral)", 5.0f) + } + ) + ); +} + +// ============================================================ +// CatOffhealStrategy +// Additive overlay — only the healing triggers. Designed to be +// stacked on top of "cat" so the bot stays in cat form for DPS +// but shifts out to heal when the party needs it. +// ============================================================ + +class CatOffhealStrategyActionNodeFactory : public NamedObjectFactory +{ +public: + CatOffhealStrategyActionNodeFactory() + { + creators["healing touch on party"] = &healing_touch_on_party; + creators["regrowth on party"] = ®rowth_on_party; + creators["rejuvenation on party"] = &rejuvenation_on_party; + } + +private: + // P: shift to caster form before casting C: shift back to cat form afterwards + static ActionNode* healing_touch_on_party([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "healing touch on party", + /*P*/ { NextAction("caster form") }, + /*A*/ {}, + /*C*/ { NextAction("cat form") } + ); + } + + static ActionNode* regrowth_on_party([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "regrowth on party", + /*P*/ { NextAction("caster form") }, + /*A*/ {}, + /*C*/ { NextAction("cat form") } + ); + } + + static ActionNode* rejuvenation_on_party([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "rejuvenation on party", + /*P*/ { NextAction("caster form") }, + /*A*/ {}, + /*C*/ { NextAction("cat form") } + ); + } +}; + +CatOffhealStrategy::CatOffhealStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) +{ + actionNodeFactories.Add(new CatOffhealStrategyActionNodeFactory()); +} + +void CatOffhealStrategy::InitTriggers(std::vector& triggers) +{ + triggers.push_back( + new TriggerNode( + "party member critical health", + { + NextAction("regrowth on party", 36.0f), + NextAction("healing touch on party", 35.0f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "party member low health", + { + NextAction("healing touch on party", 25.0f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "party member medium health", + { + NextAction("rejuvenation on party", 18.0f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "party member to heal out of spell range", + { + NextAction("reach party member to heal", 93.0f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "low mana", + { + NextAction("innervate", 24.0f) + } + ) + ); +} diff --git a/src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.h b/src/Ai/Class/Druid/Strategy/CatDruidStrategy.h similarity index 59% rename from src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.h rename to src/Ai/Class/Druid/Strategy/CatDruidStrategy.h index 312e94d0f..51d80cd77 100644 --- a/src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.h +++ b/src/Ai/Class/Druid/Strategy/CatDruidStrategy.h @@ -3,17 +3,17 @@ * and/or modify it under version 3 of the License, or (at your option), any later version. */ -#ifndef _PLAYERBOT_CATDPSDRUIDSTRATEGY_H -#define _PLAYERBOT_CATDPSDRUIDSTRATEGY_H +#ifndef _PLAYERBOT_CATDRUIDSTRATEGY_H +#define _PLAYERBOT_CATDRUIDSTRATEGY_H #include "FeralDruidStrategy.h" class PlayerbotAI; -class CatDpsDruidStrategy : public FeralDruidStrategy +class CatDruidStrategy : public FeralDruidStrategy { public: - CatDpsDruidStrategy(PlayerbotAI* botAI); + CatDruidStrategy(PlayerbotAI* botAI); public: void InitTriggers(std::vector& triggers) override; @@ -22,14 +22,16 @@ public: uint32 GetType() const override { return STRATEGY_TYPE_COMBAT | STRATEGY_TYPE_MELEE; } }; -class CatAoeDruidStrategy : public CombatStrategy +// Optional additive strategy. Layers emergency heals on top of the "cat" strategy. +// Enable : co +offheal +// Disable: co -offheal +class CatOffhealStrategy : public CombatStrategy { public: - CatAoeDruidStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) {} + CatOffhealStrategy(PlayerbotAI* botAI); -public: void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "cat aoe"; } + std::string const getName() override { return "offheal"; } }; #endif diff --git a/src/Ai/Class/Druid/Strategy/FeralDruidStrategy.cpp b/src/Ai/Class/Druid/Strategy/FeralDruidStrategy.cpp index 894c05bff..aec438890 100644 --- a/src/Ai/Class/Druid/Strategy/FeralDruidStrategy.cpp +++ b/src/Ai/Class/Druid/Strategy/FeralDruidStrategy.cpp @@ -14,12 +14,10 @@ public: { creators["survival instincts"] = &survival_instincts; creators["thorns"] = þs; - creators["omen of clarity"] = &omen_of_clarity; creators["cure poison"] = &cure_poison; creators["cure poison on party"] = &cure_poison_on_party; creators["abolish poison"] = &abolish_poison; creators["abolish poison on party"] = &abolish_poison_on_party; - creators["prowl"] = &prowl; } private: @@ -39,14 +37,6 @@ private: /*C*/ {}); } - static ActionNode* omen_of_clarity([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("omen of clarity", - /*P*/ { NextAction("caster form") }, - /*A*/ {}, - /*C*/ {}); - } - static ActionNode* cure_poison([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode("cure poison", @@ -79,13 +69,6 @@ private: /*C*/ {}); } - static ActionNode* prowl([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("prowl", - /*P*/ { NextAction("cat form") }, - /*A*/ {}, - /*C*/ {}); - } }; FeralDruidStrategy::FeralDruidStrategy(PlayerbotAI* botAI) : GenericDruidStrategy(botAI) @@ -99,15 +82,23 @@ void FeralDruidStrategy::InitTriggers(std::vector& triggers) GenericDruidStrategy::InitTriggers(triggers); triggers.push_back(new TriggerNode( - "enemy out of melee", { NextAction("reach melee", ACTION_HIGH + 1) })); + "enemy out of melee", { NextAction("reach melee", 21.0f) })); triggers.push_back(new TriggerNode( - "critical health", { NextAction("survival instincts", ACTION_EMERGENCY + 1) })); - triggers.push_back(new TriggerNode( - "omen of clarity", { NextAction("omen of clarity", ACTION_HIGH + 9) })); + "low health", { NextAction("survival instincts", 91.0f) })); triggers.push_back(new TriggerNode("player has flag", - { NextAction("dash", ACTION_EMERGENCY + 2) })); + { NextAction("dash", 92.0f) })); triggers.push_back(new TriggerNode("enemy flagcarrier near", - { NextAction("dash", ACTION_EMERGENCY + 2) })); - triggers.push_back( - new TriggerNode("berserk", { NextAction("berserk", ACTION_HIGH + 6) })); + { NextAction("dash", 92.0f) })); +} + +void FeralChargeDruidStrategy::InitTriggers(std::vector& triggers) +{ + Player* bot = botAI->GetBot(); + + if (bot->HasSpell(SPELL_CAT_FORM) && !bot->HasAura(AURA_THICK_HIDE)) + triggers.push_back(new TriggerNode( + "enemy out of melee", { NextAction("feral charge - cat", 29.0f) })); + else + triggers.push_back(new TriggerNode( + "enemy out of melee", { NextAction("feral charge - bear", 18.0f) })); } diff --git a/src/Ai/Class/Druid/Strategy/FeralDruidStrategy.h b/src/Ai/Class/Druid/Strategy/FeralDruidStrategy.h index abf01c694..ebff73de3 100644 --- a/src/Ai/Class/Druid/Strategy/FeralDruidStrategy.h +++ b/src/Ai/Class/Druid/Strategy/FeralDruidStrategy.h @@ -3,11 +3,14 @@ * and/or modify it under version 3 of the License, or (at your option), any later version. */ -#ifndef _PLAYERBOT_FERALRUIDSTRATEGY_H -#define _PLAYERBOT_FERALRUIDSTRATEGY_H +#ifndef _PLAYERBOT_FERALDRUIDSTRATEGY_H +#define _PLAYERBOT_FERALDRUIDSTRATEGY_H #include "GenericDruidStrategy.h" +constexpr uint32 SPELL_CAT_FORM = 768; +constexpr uint32 AURA_THICK_HIDE = 16931; + class PlayerbotAI; class ShapeshiftDruidStrategyActionNodeFactory : public NamedObjectFactory @@ -83,4 +86,18 @@ public: uint32 GetType() const override { return STRATEGY_TYPE_COMBAT | STRATEGY_TYPE_MELEE; } }; +// Optional strategy — enabled by default for cat and bear. +// Registers the "enemy out of melee" → Feral Charge trigger, spec-gated at +// init time so cats get Feral Charge (Cat) and bears get Feral Charge (Bear). +// Disable with: co -feral charge +// Re-enable with: co +feral charge +class FeralChargeDruidStrategy : public CombatStrategy +{ +public: + FeralChargeDruidStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) {} + + void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "feral charge"; } +}; + #endif diff --git a/src/Ai/Class/Druid/Strategy/GenericDruidNonCombatStrategy.cpp b/src/Ai/Class/Druid/Strategy/GenericDruidNonCombatStrategy.cpp index 7d1a5f7ca..0977540f6 100644 --- a/src/Ai/Class/Druid/Strategy/GenericDruidNonCombatStrategy.cpp +++ b/src/Ai/Class/Druid/Strategy/GenericDruidNonCombatStrategy.cpp @@ -17,12 +17,13 @@ public: creators["thorns on party"] = þs_on_party; creators["mark of the wild"] = &mark_of_the_wild; creators["mark of the wild on party"] = &mark_of_the_wild_on_party; - // creators["innervate"] = &innervate; creators["regrowth_on_party"] = ®rowth_on_party; creators["rejuvenation on party"] = &rejuvenation_on_party; creators["remove curse on party"] = &remove_curse_on_party; creators["abolish poison on party"] = &abolish_poison_on_party; creators["revive"] = &revive; + creators["prowl"] = &prowl; + creators["aquatic form"] = &aquatic_form; } private: @@ -92,6 +93,23 @@ private: /*A*/ {}, /*C*/ {}); } + + static ActionNode* prowl([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode("prowl", + /*P*/ { NextAction("cat form") }, + /*A*/ {}, + /*C*/ {}); + } + + static ActionNode* aquatic_form([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode("aquatic form", + /*P*/ { NextAction("caster form") }, + /*A*/ {}, + /*C*/ {}); + } + }; GenericDruidNonCombatStrategy::GenericDruidNonCombatStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI) @@ -165,11 +183,16 @@ void GenericDruidNonCombatStrategy::InitTriggers(std::vector& trig NextAction("remove curse on party", ACTION_DISPEL + 7), })); + triggers.push_back(new TriggerNode("aquatic form", { NextAction("aquatic form", 10.0f) })); + int specTab = AiFactory::GetPlayerSpecTab(botAI->GetBot()); - if (specTab == 0 || specTab == 2) // Balance or Restoration + if (specTab == DRUID_TAB_BALANCE || specTab == DRUID_TAB_RESTORATION) triggers.push_back(new TriggerNode("often", { NextAction("apply oil", 1.0f) })); - if (specTab == 1) // Feral + if (specTab == DRUID_TAB_FERAL) + { triggers.push_back(new TriggerNode("often", { NextAction("apply stone", 1.0f) })); + triggers.push_back(new TriggerNode("prowl", { NextAction("prowl", ACTION_INTERRUPT) })); + } } diff --git a/src/Ai/Class/Druid/Strategy/GenericDruidStrategy.cpp b/src/Ai/Class/Druid/Strategy/GenericDruidStrategy.cpp index 36b90a146..d5e7b9f40 100644 --- a/src/Ai/Class/Druid/Strategy/GenericDruidStrategy.cpp +++ b/src/Ai/Class/Druid/Strategy/GenericDruidStrategy.cpp @@ -5,6 +5,8 @@ #include "GenericDruidStrategy.h" +#include "AiFactory.h" +#include "FeralDruidStrategy.h" #include "Playerbots.h" class GenericDruidStrategyActionNodeFactory : public NamedObjectFactory @@ -20,6 +22,8 @@ public: creators["abolish poison on party"] = &abolish_poison_on_party; creators["rebirth"] = &rebirth; creators["entangling roots on cc"] = &entangling_roots_on_cc; + creators["cyclone on cc"] = &cyclone_on_cc; + creators["hibernate on cc"] = &hibernate_on_cc; creators["innervate"] = &innervate; } @@ -88,6 +92,22 @@ private: /*C*/ {}); } + static ActionNode* cyclone_on_cc([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode("cyclone on cc", + /*P*/ { NextAction("caster form") }, + /*A*/ {}, + /*C*/ {}); + } + + static ActionNode* hibernate_on_cc([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode("hibernate on cc", + /*P*/ { NextAction("caster form") }, + /*A*/ {}, + /*C*/ {}); + } + static ActionNode* innervate([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode("innervate", @@ -107,41 +127,95 @@ void GenericDruidStrategy::InitTriggers(std::vector& triggers) CombatStrategy::InitTriggers(triggers); triggers.push_back( - new TriggerNode("low health", { NextAction("barkskin", ACTION_HIGH + 7) })); + new TriggerNode("almost full health", { NextAction("barkskin", 40.0f) })); + + Player* bot = botAI->GetBot(); + int tab = AiFactory::GetPlayerSpecTab(bot); + + if (tab == DRUID_TAB_FERAL) + { + if (!bot->HasAura(16931) /*thick hide — bear spec*/) + { + triggers.push_back(new TriggerNode("predator's swiftness and combat party member dead", + { NextAction("rebirth", 29.0f) })); + triggers.push_back(new TriggerNode("combat party member dead", + { NextAction("rebirth", 28.5f) })); + } + } + else + { + triggers.push_back(new TriggerNode("combat party member dead", + { NextAction("rebirth", 29.0f) })); + } - triggers.push_back(new TriggerNode("combat party member dead", - { NextAction("rebirth", ACTION_HIGH + 9) })); triggers.push_back(new TriggerNode("being attacked", - { NextAction("nature's grasp", ACTION_HIGH + 1) })); - triggers.push_back(new TriggerNode("new pet", { NextAction("set pet stance", 60.0f) })); + { NextAction("nature's grasp", 39.0f) })); } void DruidCureStrategy::InitTriggers(std::vector& triggers) { triggers.push_back( new TriggerNode("party member cure poison", - { NextAction("abolish poison on party", ACTION_DISPEL + 1) })); + { NextAction("abolish poison on party", 51.0f) })); triggers.push_back( new TriggerNode("party member remove curse", - { NextAction("remove curse on party", ACTION_DISPEL + 7) })); + { NextAction("remove curse on party", 57.0f) })); } void DruidBoostStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode( - "nature's swiftness", { NextAction("nature's swiftness", ACTION_HIGH + 9) })); + Player* bot = botAI->GetBot(); + int tab = AiFactory::GetPlayerSpecTab(bot); + + if (tab == DRUID_TAB_BALANCE) + { + triggers.push_back(new TriggerNode("force of nature", { NextAction("force of nature", 29.0f) })); + triggers.push_back(new TriggerNode("new pet", { NextAction("set pet stance", 60.0f) })); + } + + if (tab == DRUID_TAB_FERAL) + { + triggers.push_back(new TriggerNode("berserk", { NextAction("berserk", 27.5f) })); + } } void DruidCcStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode( - "entangling roots", { NextAction("entangling roots on cc", ACTION_HIGH + 2) })); - triggers.push_back(new TriggerNode( - "entangling roots kite", { NextAction("entangling roots", ACTION_HIGH + 2) })); - triggers.push_back(new TriggerNode( - "hibernate", { NextAction("hibernate on cc", ACTION_HIGH + 3) })); + Player* bot = botAI->GetBot(); + int tab = AiFactory::GetPlayerSpecTab(bot); + + if (tab == DRUID_TAB_BALANCE || tab == DRUID_TAB_RESTORATION) + { + triggers.push_back(new TriggerNode( + "cyclone", { NextAction("cyclone on cc", 42.0f) })); + triggers.push_back(new TriggerNode( + "hibernate", { NextAction("hibernate on cc", 41.0f) })); + triggers.push_back(new TriggerNode( + "entangling roots", { NextAction("entangling roots on cc", 40.0f) })); + } + if (tab == DRUID_TAB_FERAL) + { + if (bot->HasSpell(SPELL_CAT_FORM) && !bot->HasAura(AURA_THICK_HIDE)) + { + triggers.push_back(new TriggerNode( + "predator's swiftness and cyclone", { NextAction("cyclone on cc", 42.0f) })); + triggers.push_back(new TriggerNode( + "predator's swiftness and hibernate", { NextAction("hibernate on cc", 41.0f) })); + triggers.push_back(new TriggerNode( + "predator's swiftness and entangling roots", { NextAction("entangling roots on cc", 40.0f) })); + } + else + { + triggers.push_back(new TriggerNode( + "cyclone", { NextAction("cyclone on cc", 42.0f) })); + triggers.push_back(new TriggerNode( + "hibernate", { NextAction("hibernate on cc", 41.0f) })); + triggers.push_back(new TriggerNode( + "entangling roots", { NextAction("entangling roots on cc", 40.0f) })); + } + } } void DruidHealerDpsStrategy::InitTriggers(std::vector& triggers) @@ -149,10 +223,39 @@ void DruidHealerDpsStrategy::InitTriggers(std::vector& triggers) triggers.push_back( new TriggerNode("healer should attack", { - NextAction("cancel tree form", ACTION_DEFAULT + 0.4f), - NextAction("moonfire", ACTION_DEFAULT + 0.3f), - NextAction("wrath", ACTION_DEFAULT + 0.2f), - NextAction("starfire", ACTION_DEFAULT + 0.1f), -})); - + NextAction("cancel tree form", 5.4f), + NextAction("moonfire", 5.3f), + NextAction("wrath", 5.2f), + NextAction("starfire", 5.1f), + })); +} + +void DruidAoeStrategy::InitTriggers(std::vector& triggers) +{ + Player* bot = botAI->GetBot(); + int tab = AiFactory::GetPlayerSpecTab(bot); + + if (tab == DRUID_TAB_BALANCE) + { + triggers.push_back(new TriggerNode("hurricane channel check", { NextAction("cancel channel", 22.0f) })); + triggers.push_back(new TriggerNode("starfall", { NextAction("starfall", 28.5f) })); + triggers.push_back(new TriggerNode("medium aoe", { NextAction("hurricane", 23.0f) })); + triggers.push_back(new TriggerNode("enemy within melee", { NextAction("typhoon", 40.0f) })); + triggers.push_back(new TriggerNode("insect swarm on attacker", { NextAction("insect swarm on attacker", 5.2f) })); + triggers.push_back(new TriggerNode("moonfire on attacker", { NextAction("moonfire on attacker", 5.1f) })); + } + + if (tab == DRUID_TAB_RESTORATION) + { + triggers.push_back(new TriggerNode("hurricane channel check", { NextAction("cancel channel", 22.0f) })); + triggers.push_back(new TriggerNode("medium aoe", { NextAction("hurricane", 23.0f) })); + triggers.push_back(new TriggerNode("insect swarm on attacker", { NextAction("insect swarm on attacker", 5.2f) })); + triggers.push_back(new TriggerNode("moonfire on attacker", { NextAction("moonfire on attacker", 5.1f) })); + } + + if (tab == DRUID_TAB_FERAL && bot->HasSpell(SPELL_CAT_FORM) && !bot->HasAura(AURA_THICK_HIDE)) + { + triggers.push_back(new TriggerNode("clearcasting and medium aoe", { NextAction("swipe (cat)", 25.5f) })); + triggers.push_back(new TriggerNode("medium aoe", { NextAction("swipe (cat)", 25.0f) })); + } } diff --git a/src/Ai/Class/Druid/Strategy/GenericDruidStrategy.h b/src/Ai/Class/Druid/Strategy/GenericDruidStrategy.h index 2f1c2e787..1cb88f82a 100644 --- a/src/Ai/Class/Druid/Strategy/GenericDruidStrategy.h +++ b/src/Ai/Class/Druid/Strategy/GenericDruidStrategy.h @@ -55,4 +55,13 @@ public: std::string const getName() override { return "healer dps"; } }; +class DruidAoeStrategy : public Strategy +{ +public: + DruidAoeStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "aoe"; } +}; + #endif diff --git a/src/Ai/Class/Druid/Strategy/HealDruidStrategy.cpp b/src/Ai/Class/Druid/Strategy/HealDruidStrategy.cpp deleted file mode 100644 index 0710e0fdc..000000000 --- a/src/Ai/Class/Druid/Strategy/HealDruidStrategy.cpp +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it - * and/or modify it under version 3 of the License, or (at your option), any later version. - */ - -#include "HealDruidStrategy.h" - -#include "Playerbots.h" - -class HealDruidStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - HealDruidStrategyActionNodeFactory() { - creators["nourish on party"] = &nourtish_on_party; - } - -private: - static ActionNode* nourtish_on_party([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("nourish on party", - /*P*/ {}, - /*A*/ { NextAction("healing touch on party") }, - /*C*/ {}); - } -}; - -HealDruidStrategy::HealDruidStrategy(PlayerbotAI* botAI) : GenericDruidStrategy(botAI) -{ - actionNodeFactories.Add(new HealDruidStrategyActionNodeFactory()); -} - -void HealDruidStrategy::InitTriggers(std::vector& triggers) -{ - GenericDruidStrategy::InitTriggers(triggers); - - // no healer dps strategy - triggers.push_back(new TriggerNode("no healer dps strategy", - { NextAction("tree form", ACTION_DEFAULT) })); - - triggers.push_back(new TriggerNode( - "party member to heal out of spell range", - { NextAction("reach party member to heal", ACTION_CRITICAL_HEAL + 9) })); - - // CRITICAL - triggers.push_back( - new TriggerNode("party member critical health", - { - NextAction("tree form", ACTION_CRITICAL_HEAL + 4.1f), - NextAction("swiftmend on party", ACTION_CRITICAL_HEAL + 4), - NextAction("regrowth on party", ACTION_CRITICAL_HEAL + 3), - NextAction("wild growth on party", ACTION_CRITICAL_HEAL + 2), - NextAction("nourish on party", ACTION_CRITICAL_HEAL + 1), - })); - - triggers.push_back( - new TriggerNode("party member critical health", - { NextAction("nature's swiftness", ACTION_CRITICAL_HEAL + 4) })); - - triggers.push_back(new TriggerNode("clearcasting", - { NextAction("lifebloom on main tank", ACTION_CRITICAL_HEAL - 1) })); - - triggers.push_back(new TriggerNode( - "group heal setting", - { - NextAction("tree form", ACTION_MEDIUM_HEAL + 2.3f), - NextAction("wild growth on party", ACTION_MEDIUM_HEAL + 2.2f), - NextAction("rejuvenation on not full", ACTION_MEDIUM_HEAL + 2.1f), - })); - - triggers.push_back( - new TriggerNode("medium group heal setting", - { - NextAction("tree form", ACTION_CRITICAL_HEAL + 0.6f), - NextAction("tranquility", ACTION_CRITICAL_HEAL + 0.5f) })); - - // LOW - triggers.push_back( - new TriggerNode("party member low health", - { NextAction("tree form", ACTION_MEDIUM_HEAL + 1.5f), - NextAction("wild growth on party", ACTION_MEDIUM_HEAL + 1.4f), - NextAction("regrowth on party", ACTION_MEDIUM_HEAL + 1.3f), - NextAction("swiftmend on party", ACTION_MEDIUM_HEAL + 1.2), - NextAction("nourish on party", ACTION_MEDIUM_HEAL + 1.1f), - })); - - // MEDIUM - triggers.push_back( - new TriggerNode("party member medium health", - { - NextAction("tree form", ACTION_MEDIUM_HEAL + 0.5f), - NextAction("wild growth on party", ACTION_MEDIUM_HEAL + 0.4f), - NextAction("rejuvenation on party", ACTION_MEDIUM_HEAL + 0.3f), - NextAction("regrowth on party", ACTION_MEDIUM_HEAL + 0.2f), - NextAction("nourish on party", ACTION_MEDIUM_HEAL + 0.1f) })); - - // almost full - triggers.push_back( - new TriggerNode("party member almost full health", - { NextAction("wild growth on party", ACTION_LIGHT_HEAL + 0.3f), - NextAction("rejuvenation on party", ACTION_LIGHT_HEAL + 0.2f), - NextAction("regrowth on party", ACTION_LIGHT_HEAL + 0.1f) })); - - triggers.push_back( - new TriggerNode("medium mana", { NextAction("innervate", ACTION_HIGH + 5) })); - - triggers.push_back(new TriggerNode("enemy too close for spell", - { NextAction("flee", ACTION_MOVE + 9) })); -} diff --git a/src/Ai/Class/Druid/Strategy/HealDruidStrategy.h b/src/Ai/Class/Druid/Strategy/HealDruidStrategy.h deleted file mode 100644 index 36ce40271..000000000 --- a/src/Ai/Class/Druid/Strategy/HealDruidStrategy.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it - * and/or modify it under version 3 of the License, or (at your option), any later version. - */ - -#ifndef _PLAYERBOT_HEALDRUIDSTRATEGY_H -#define _PLAYERBOT_HEALDRUIDSTRATEGY_H - -#include "GenericDruidStrategy.h" -#include "Strategy.h" - -class PlayerbotAI; - -class HealDruidStrategy : public GenericDruidStrategy -{ -public: - HealDruidStrategy(PlayerbotAI* botAI); - - void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "heal"; } - uint32 GetType() const override { return STRATEGY_TYPE_RANGED | STRATEGY_TYPE_HEAL; } -}; - -#endif diff --git a/src/Ai/Class/Druid/Strategy/MeleeDruidStrategy.cpp b/src/Ai/Class/Druid/Strategy/MeleeDruidStrategy.cpp deleted file mode 100644 index 5dc0f85d9..000000000 --- a/src/Ai/Class/Druid/Strategy/MeleeDruidStrategy.cpp +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it - * and/or modify it under version 3 of the License, or (at your option), any later version. - */ - -#include "MeleeDruidStrategy.h" - -#include "Playerbots.h" - -MeleeDruidStrategy::MeleeDruidStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) {} - -std::vector MeleeDruidStrategy::getDefaultActions() -{ - return { - NextAction("faerie fire", ACTION_DEFAULT + 0.1f), - NextAction("melee", ACTION_DEFAULT) - }; -} - -void MeleeDruidStrategy::InitTriggers(std::vector& triggers) -{ - triggers.push_back( - new TriggerNode( - "omen of clarity", - { - NextAction("omen of clarity", ACTION_HIGH + 9) - } - ) - ); - - CombatStrategy::InitTriggers(triggers); -} diff --git a/src/Ai/Class/Druid/Strategy/MeleeDruidStrategy.h b/src/Ai/Class/Druid/Strategy/MeleeDruidStrategy.h deleted file mode 100644 index 67bbbbfed..000000000 --- a/src/Ai/Class/Druid/Strategy/MeleeDruidStrategy.h +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it - * and/or modify it under version 3 of the License, or (at your option), any later version. - */ - -#ifndef _PLAYERBOT_MELEEDRUIDSTRATEGY_H -#define _PLAYERBOT_MELEEDRUIDSTRATEGY_H - -#include "CombatStrategy.h" - -class MeleeDruidStrategy : public CombatStrategy -{ -public: - MeleeDruidStrategy(PlayerbotAI* botAI); - - void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "melee"; } - std::vector getDefaultActions() override; -}; - -#endif diff --git a/src/Ai/Class/Druid/Strategy/OffhealDruidCatStrategy.cpp b/src/Ai/Class/Druid/Strategy/OffhealDruidCatStrategy.cpp deleted file mode 100644 index fb7893651..000000000 --- a/src/Ai/Class/Druid/Strategy/OffhealDruidCatStrategy.cpp +++ /dev/null @@ -1,307 +0,0 @@ -/* - * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it - * and/or modify it under version 3 of the License, or (at your option), any later version. - */ - - #include "OffhealDruidCatStrategy.h" - - #include "Playerbots.h" - #include "Strategy.h" - - class OffhealDruidCatStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - OffhealDruidCatStrategyActionNodeFactory() - { - creators["cat form"] = &cat_form; - creators["mangle (cat)"] = &mangle_cat; - creators["shred"] = &shred; - creators["rake"] = &rake; - creators["rip"] = &rip; - creators["ferocious bite"] = &ferocious_bite; - creators["savage roar"] = &savage_roar; - creators["faerie fire (feral)"] = &faerie_fire_feral; - creators["healing touch on party"] = &healing_touch_on_party; - creators["regrowth on party"] = ®rowth_on_party; - creators["rejuvenation on party"] = &rejuvenation_on_party; - } - -private: - static ActionNode* cat_form([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "cat form", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* mangle_cat([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "mangle (cat)", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* shred([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "shred", - /*P*/ {}, - /*A*/ { NextAction("claw") }, - /*C*/ {} - ); - } - - static ActionNode* rake([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "rake", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* rip([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "rip", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* ferocious_bite([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "ferocious bite", - /*P*/ {}, - /*A*/ { NextAction("rip") }, - /*C*/ {} - ); - } - - static ActionNode* savage_roar([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "savage roar", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* faerie_fire_feral([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "faerie fire (feral)", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* healing_touch_on_party([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "healing touch on party", - /*P*/ { NextAction("caster form") }, - /*A*/ {}, - /*C*/ { NextAction("cat form") } - ); - } - - static ActionNode* regrowth_on_party([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "regrowth on party", - /*P*/ { NextAction("caster form") }, - /*A*/ {}, - /*C*/ { NextAction("cat form") } - ); - } - - static ActionNode* rejuvenation_on_party([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "rejuvenation on party", - /*P*/ { NextAction("caster form") }, - /*A*/ {}, - /*C*/ { NextAction("cat form") } - ); - } -}; - -OffhealDruidCatStrategy::OffhealDruidCatStrategy(PlayerbotAI* botAI) : FeralDruidStrategy(botAI) -{ - actionNodeFactories.Add(new OffhealDruidCatStrategyActionNodeFactory()); -} - -std::vector OffhealDruidCatStrategy::getDefaultActions() -{ - return { - NextAction("mangle (cat)", ACTION_DEFAULT + 0.5f), - NextAction("shred", ACTION_DEFAULT + 0.4f), - NextAction("rake", ACTION_DEFAULT + 0.3f), - NextAction("melee", ACTION_DEFAULT), - NextAction("cat form", ACTION_DEFAULT - 0.1f) - }; -} - -void OffhealDruidCatStrategy::InitTriggers(std::vector& triggers) -{ - FeralDruidStrategy::InitTriggers(triggers); - - triggers.push_back( - new TriggerNode( - "cat form", - { - NextAction("cat form", ACTION_HIGH + 8) - } - ) - ); - triggers.push_back( - new TriggerNode( - "savage roar", - { - NextAction("savage roar", ACTION_HIGH + 7) - } - ) - ); - triggers.push_back( - new TriggerNode( - "combo points 5 available", - { - NextAction("rip", ACTION_HIGH + 6) - } - ) - ); - triggers.push_back( - new TriggerNode( - "ferocious bite time", - { - NextAction("ferocious bite", ACTION_HIGH + 5) - } - ) - ); - triggers.push_back( - new TriggerNode( - "target with combo points almost dead", - { - NextAction("ferocious bite", ACTION_HIGH + 4) - } - ) - ); - triggers.push_back( - new TriggerNode( - "mangle (cat)", - { - NextAction("mangle (cat)", ACTION_HIGH + 3) - } - ) - ); - triggers.push_back( - new TriggerNode( - "rake", - { - NextAction("rake", ACTION_HIGH + 2) - } - ) - ); - triggers.push_back( - new TriggerNode( - "almost full energy available", - { - NextAction("shred", ACTION_DEFAULT + 0.4f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "combo points not full", - { - NextAction("shred", ACTION_DEFAULT + 0.4f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "faerie fire (feral)", - { - NextAction("faerie fire (feral)", ACTION_NORMAL) - } - ) - ); - triggers.push_back( - new TriggerNode( - "enemy out of melee", - { - NextAction("feral charge - cat", ACTION_HIGH + 9), - NextAction("dash", ACTION_HIGH + 8) - } - ) - ); - triggers.push_back( - new TriggerNode( - "medium aoe", - { - NextAction("swipe (cat)", ACTION_HIGH + 3) - } - ) - ); - triggers.push_back( - new TriggerNode( - "tiger's fury", - { - NextAction("tiger's fury", ACTION_NORMAL + 1) - } - ) - ); - triggers.push_back( - new TriggerNode( - "party member critical health", - { - NextAction("regrowth on party", ACTION_CRITICAL_HEAL + 6), - NextAction("healing touch on party", ACTION_CRITICAL_HEAL + 5) - } - ) - ); - triggers.push_back( - new TriggerNode( - "party member low health", - { - NextAction("healing touch on party", ACTION_MEDIUM_HEAL + 5) - } - ) - ); - triggers.push_back( - new TriggerNode( - "party member medium health", - { - NextAction("rejuvenation on party", ACTION_LIGHT_HEAL + 8) - } - ) - ); - triggers.push_back( - new TriggerNode( - "party member to heal out of spell range", - { - NextAction("reach party member to heal", ACTION_EMERGENCY + 3) - } - ) - ); - triggers.push_back( - new TriggerNode( - "low mana", - { - NextAction("innervate", ACTION_HIGH + 4) - } - ) - ); -} diff --git a/src/Ai/Class/Druid/Strategy/OffhealDruidCatStrategy.h b/src/Ai/Class/Druid/Strategy/OffhealDruidCatStrategy.h deleted file mode 100644 index 83775ef98..000000000 --- a/src/Ai/Class/Druid/Strategy/OffhealDruidCatStrategy.h +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it - * and/or modify it under version 3 of the License, or (at your option), any later version. - */ - - #ifndef _PLAYERBOT_OFFHEALDRUIDCATSTRATEGY_H - #define _PLAYERBOT_OFFHEALDRUIDCATSTRATEGY_H - - #include "FeralDruidStrategy.h" - - class PlayerbotAI; - - class OffhealDruidCatStrategy : public FeralDruidStrategy - { - public: - OffhealDruidCatStrategy(PlayerbotAI* botAI); - - void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "offheal"; } - std::vector getDefaultActions() override; - uint32 GetType() const override - { - return STRATEGY_TYPE_COMBAT | STRATEGY_TYPE_DPS | STRATEGY_TYPE_HEAL | STRATEGY_TYPE_MELEE; - } - }; - - #endif diff --git a/src/Ai/Class/Druid/Strategy/RestoDruidStrategy.cpp b/src/Ai/Class/Druid/Strategy/RestoDruidStrategy.cpp new file mode 100644 index 000000000..5f086ee50 --- /dev/null +++ b/src/Ai/Class/Druid/Strategy/RestoDruidStrategy.cpp @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#include "RestoDruidStrategy.h" + +#include "Playerbots.h" + +class RestoDruidStrategyActionNodeFactory : public NamedObjectFactory +{ +public: + RestoDruidStrategyActionNodeFactory() { + creators["nourish on party"] = &nourish_on_party; + } + +private: + static ActionNode* nourish_on_party([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode("nourish on party", + /*P*/ {}, + /*A*/ {}, + /*C*/ {}); + } +}; + +RestoDruidStrategy::RestoDruidStrategy(PlayerbotAI* botAI) : GenericDruidStrategy(botAI) +{ + actionNodeFactories.Add(new RestoDruidStrategyActionNodeFactory()); +} + +void RestoDruidStrategy::InitTriggers(std::vector& triggers) +{ + GenericDruidStrategy::InitTriggers(triggers); + + triggers.push_back(new TriggerNode("no healer dps strategy", + { NextAction("tree form", 5.0f) })); + + triggers.push_back(new TriggerNode( + "party member to heal out of spell range", + { NextAction("reach party member to heal", 39.0f) })); + + triggers.push_back( + new TriggerNode("party member critical health", + { + NextAction("tree form", 34.1f), + NextAction("swiftmend on party", 34.0f), + NextAction("wild growth on party", 33.0f), + NextAction("nourish on party", 32.0f), + NextAction("regrowth on party", 31.0f), + NextAction("healing touch on party", 30.0f), + })); + + triggers.push_back( + new TriggerNode("party member critical health", + { NextAction("nature's swiftness", 58.0f) })); + + triggers.push_back(new TriggerNode( + "nature's swiftness active", + { NextAction("healing touch on party", 55.0f) })); + + triggers.push_back(new TriggerNode("clearcasting", + { NextAction("lifebloom on main tank", 13.0f) })); + + // LOW + triggers.push_back( + new TriggerNode("party member low health", + { + NextAction("tree form", 21.5f), + NextAction("swiftmend on party", 21.4f), + NextAction("wild growth on party", 21.3f), + NextAction("nourish on party", 21.2f), + NextAction("regrowth on party", 21.1f), + NextAction("healing touch on party", 21.0f), + })); + + // MEDIUM + triggers.push_back( + new TriggerNode("party member medium health", + { + NextAction("tree form", 20.5f), + NextAction("swiftmend on party", 20.4f), + NextAction("wild growth on party", 20.3f), + NextAction("nourish on party", 20.2f), + NextAction("regrowth on party", 20.1f), + NextAction("healing touch on party", 20.0f), + })); + + // ALMOST FULL + triggers.push_back( + new TriggerNode("party member almost full health", + { + NextAction("wild growth on party", 10.3f), + NextAction("rejuvenation on party", 10.2f), + NextAction("regrowth on party", 10.1f), + })); + + triggers.push_back( + new TriggerNode("medium mana", { NextAction("innervate", 25.0f) })); + + triggers.push_back(new TriggerNode("enemy too close for spell", + { NextAction("flee", 39.0f) })); +} + +void DruidTranquilityStrategy::InitTriggers(std::vector& triggers) +{ + triggers.push_back(new TriggerNode("medium group heal setting", + { NextAction("tree form", 30.6f), NextAction("tranquility", 30.5f) })); +} + +void DruidBlanketStrategy::InitTriggers(std::vector& triggers) +{ + triggers.push_back(new TriggerNode( + "wild growth blanket", + { NextAction("tree form", 8.1f), NextAction("wild growth blanket", 8.0f) })); + + triggers.push_back(new TriggerNode( + "rejuvenation blanket", + { NextAction("tree form", 6.1f), NextAction("rejuvenation blanket", 6.0f) })); +} diff --git a/src/Ai/Class/Druid/Strategy/RestoDruidStrategy.h b/src/Ai/Class/Druid/Strategy/RestoDruidStrategy.h new file mode 100644 index 000000000..afc3a93bc --- /dev/null +++ b/src/Ai/Class/Druid/Strategy/RestoDruidStrategy.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_RESTODRUIDSTRATEGY_H +#define _PLAYERBOT_RESTODRUIDSTRATEGY_H + +#include "GenericDruidStrategy.h" +#include "Strategy.h" + +class PlayerbotAI; + +class RestoDruidStrategy : public GenericDruidStrategy +{ +public: + RestoDruidStrategy(PlayerbotAI* botAI); + + void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "resto"; } + uint32 GetType() const override { return STRATEGY_TYPE_RANGED | STRATEGY_TYPE_HEAL; } +}; + +class DruidBlanketStrategy : public Strategy +{ +public: + DruidBlanketStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "blanketing"; } +}; + +class DruidTranquilityStrategy : public Strategy +{ +public: + DruidTranquilityStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "tranquility"; } +}; + +#endif diff --git a/src/Ai/Class/Druid/Trigger/DruidTriggers.cpp b/src/Ai/Class/Druid/Trigger/DruidTriggers.cpp index 6cf8552a3..5a8dbdfa4 100644 --- a/src/Ai/Class/Druid/Trigger/DruidTriggers.cpp +++ b/src/Ai/Class/Druid/Trigger/DruidTriggers.cpp @@ -4,8 +4,10 @@ */ #include "DruidTriggers.h" +#include "DynamicObject.h" #include "Player.h" #include "Playerbots.h" +#include "ServerFacade.h" bool MarkOfTheWildOnPartyTrigger::IsActive() { @@ -35,6 +37,45 @@ bool TreeFormTrigger::IsActive() { return !botAI->HasAura(33891, bot); } bool CatFormTrigger::IsActive() { return !botAI->HasAura("cat form", bot); } +bool AquaticFormTrigger::IsActive() +{ + return !bot->IsInCombat() && !botAI->HasAura("aquatic form", bot) && + bot->GetLiquidData().Status == LIQUID_MAP_UNDER_WATER; +} + +bool ProwlTrigger::IsActive() +{ + if (botAI->HasAura("prowl", bot) || bot->IsInCombat()) + return false; + + uint32 prowlId = botAI->GetAiObjectContext()->GetValue("spell id", "prowl")->Get(); + if (!prowlId || !bot->HasSpell(prowlId) || bot->HasSpellCooldown(prowlId)) + return false; + + float distance = 30.f; + + Unit* target = AI_VALUE(Unit*, "enemy player target"); + if (target && !target->IsInWorld()) + return false; + if (!target) + target = AI_VALUE(Unit*, "grind target"); + if (!target) + target = AI_VALUE(Unit*, "dps target"); + if (!target) + return false; + + if (target && target->GetVictim()) + distance -= 10; + if (target->isMoving() && target->GetVictim()) + distance -= 10; + if (bot->InBattleground()) + distance += 15; + if (bot->InArena()) + distance += 15; + + return target && ServerFacade::instance().GetDistance2d(bot, target) < distance; +} + const std::set HurricaneChannelCheckTrigger::HURRICANE_SPELL_IDS = { 16914, // Hurricane Rank 1 17401, // Hurricane Rank 2 @@ -45,19 +86,38 @@ const std::set HurricaneChannelCheckTrigger::HURRICANE_SPELL_IDS = { bool HurricaneChannelCheckTrigger::IsActive() { - Player* bot = botAI->GetBot(); - - // Check if the bot is channeling a spell if (Spell* spell = bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL)) { - // Only trigger if the spell being channeled is Hurricane - if (HURRICANE_SPELL_IDS.count(spell->m_spellInfo->Id)) + if (!HURRICANE_SPELL_IDS.count(spell->m_spellInfo->Id)) + return false; + + // Find this bot's own Hurricane DynamicObject + DynamicObject* dynObj = nullptr; + for (uint32 spellId : HURRICANE_SPELL_IDS) { - uint8 attackerCount = AI_VALUE(uint8, "attacker count"); - return attackerCount < minEnemies; + dynObj = bot->GetDynObject(spellId); + if (dynObj) + break; } + + if (!dynObj) + return false; + + // Count attackers actually inside the Hurricane AoE + float radius = dynObj->GetRadius(); + GuidVector attackers = AI_VALUE(GuidVector, "attackers"); + uint32 count = 0; + for (ObjectGuid const& guid : attackers) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive()) + continue; + if (unit->GetDistance(dynObj->GetPosition()) <= radius) + count++; + } + + return count < minEnemies; } - // Not channeling Hurricane return false; } diff --git a/src/Ai/Class/Druid/Trigger/DruidTriggers.h b/src/Ai/Class/Druid/Trigger/DruidTriggers.h index 01daaeba6..1f389c947 100644 --- a/src/Ai/Class/Druid/Trigger/DruidTriggers.h +++ b/src/Ai/Class/Druid/Trigger/DruidTriggers.h @@ -8,6 +8,7 @@ #include "CureTriggers.h" #include "GenericTriggers.h" +#include "RtiTriggers.h" #include "Player.h" #include "PlayerbotAI.h" #include "Playerbots.h" @@ -15,6 +16,8 @@ #include "Trigger.h" #include +constexpr uint32 AURA_OMEN_OF_CLARITY = 16864; + class PlayerbotAI; class MarkOfTheWildOnPartyTrigger : public BuffOnPartyTrigger @@ -55,22 +58,30 @@ public: bool IsActive() override; }; -class OmenOfClarityTrigger : public BuffTrigger -{ -public: - OmenOfClarityTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "omen of clarity") {} -}; - class ClearcastingTrigger : public HasAuraTrigger { public: ClearcastingTrigger(PlayerbotAI* botAI) : HasAuraTrigger(botAI, "clearcasting") {} }; +class PredatorsSwiftnessTrigger : public HasAuraTrigger +{ +public: + PredatorsSwiftnessTrigger(PlayerbotAI* botAI) : HasAuraTrigger(botAI, "predator's swiftness") {} +}; + +class NaturesSwiftnessActiveTrigger : public HasAuraTrigger +{ +public: + NaturesSwiftnessActiveTrigger(PlayerbotAI* botAI) : HasAuraTrigger(botAI, "nature's swiftness") {} + bool IsActive() override { return botAI->HasAura("nature's swiftness", bot); } +}; + class RakeTrigger : public DebuffTrigger { public: RakeTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "rake", 1, true) {} + bool IsActive() override { return !botAI->HasAura("prowl", bot) && DebuffTrigger::IsActive(); } }; class InsectSwarmTrigger : public DebuffTrigger @@ -79,12 +90,26 @@ public: InsectSwarmTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "insect swarm", 1, true) {} }; +class InsectSwarmOnAttackerTrigger : public DebuffOnAttackerTrigger +{ +public: + InsectSwarmOnAttackerTrigger(PlayerbotAI* botAI) : DebuffOnAttackerTrigger(botAI, "insect swarm", true) {} + bool IsActive() override { return BuffTrigger::IsActive(); } +}; + class MoonfireTrigger : public DebuffTrigger { public: MoonfireTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "moonfire", 1, true) {} }; +class MoonfireOnAttackerTrigger : public DebuffOnAttackerTrigger +{ +public: + MoonfireOnAttackerTrigger(PlayerbotAI* botAI) : DebuffOnAttackerTrigger(botAI, "moonfire", true) {} + bool IsActive() override { return BuffTrigger::IsActive(); } +}; + class FaerieFireTrigger : public DebuffTrigger { public: @@ -95,6 +120,35 @@ class FaerieFireFeralTrigger : public DebuffTrigger { public: FaerieFireFeralTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "faerie fire (feral)") {} + + bool IsActive() override + { + if (!bot->IsInCombat()) + return false; + + // Bear: every cast generates immediate threat/damage for free — spam it + if (botAI->HasAnyAuraOf(bot, "bear form", "dire bear form", nullptr)) + { + Unit* target = GetTarget(); + return target && target->IsAlive() && target->IsInWorld(); + } + + if (!botAI->HasAura("cat form", bot)) + return false; + + if (botAI->HasAura("prowl", bot)) + return false; + + // Cat with Omen of Clarity: spam to fish for Clearcasting procs + if (bot->HasAura(AURA_OMEN_OF_CLARITY)) + { + Unit* target = GetTarget(); + return target && target->IsAlive() && target->IsInWorld(); + } + + // Cat without Omen of Clarity: apply as a normal debuff, don't reapply + return DebuffTrigger::IsActive(); + } }; class BashInterruptSpellTrigger : public InterruptSpellTrigger @@ -103,18 +157,18 @@ public: BashInterruptSpellTrigger(PlayerbotAI* botAI) : InterruptSpellTrigger(botAI, "bash") {} }; -class TigersFuryTrigger : public BuffTrigger -{ -public: - TigersFuryTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "tiger's fury") {} -}; - class BerserkTrigger : public BoostTrigger { public: BerserkTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "berserk") {} }; +class BerserkActiveTrigger : public HasAuraTrigger +{ +public: + BerserkActiveTrigger(PlayerbotAI* botAI) : HasAuraTrigger(botAI, "berserk") {} +}; + class SavageRoarTrigger : public BuffTrigger { public: @@ -127,10 +181,10 @@ public: NaturesGraspTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "nature's grasp") {} }; -class EntanglingRootsTrigger : public HasCcTargetTrigger +class EntanglingRootsTrigger : public RtiCcTrigger { public: - EntanglingRootsTrigger(PlayerbotAI* botAI) : HasCcTargetTrigger(botAI, "entangling roots") {} + EntanglingRootsTrigger(PlayerbotAI* botAI) : RtiCcTrigger(botAI, "entangling roots") {} }; class EntanglingRootsKiteTrigger : public DebuffTrigger @@ -141,10 +195,16 @@ public: bool IsActive() override; }; -class HibernateTrigger : public HasCcTargetTrigger +class HibernateTrigger : public RtiCcTrigger { public: - HibernateTrigger(PlayerbotAI* botAI) : HasCcTargetTrigger(botAI, "hibernate") {} + HibernateTrigger(PlayerbotAI* botAI) : RtiCcTrigger(botAI, "hibernate") {} +}; + +class CycloneTrigger : public RtiCcTrigger +{ +public: + CycloneTrigger(PlayerbotAI* botAI) : RtiCcTrigger(botAI, "cyclone") {} }; class CurePoisonTrigger : public NeedCureTrigger @@ -185,6 +245,14 @@ public: bool IsActive() override; }; +class AquaticFormTrigger : public Trigger +{ +public: + AquaticFormTrigger(PlayerbotAI* botAI) : Trigger(botAI, "aquatic form") {} + + bool IsActive() override; +}; + class EclipseSolarTrigger : public HasAuraTrigger { public: @@ -218,18 +286,69 @@ public: } }; -class EclipseSolarCooldownTrigger : public SpellCooldownTrigger +class StarfallTrigger : public SpellNoCooldownTrigger { public: - EclipseSolarCooldownTrigger(PlayerbotAI* ai) : SpellCooldownTrigger(ai, "eclipse (solar)") {} - bool IsActive() override { return bot->HasSpellCooldown(48517); } + StarfallTrigger(PlayerbotAI* botAI) : SpellNoCooldownTrigger(botAI, "starfall") {} }; -class EclipseLunarCooldownTrigger : public SpellCooldownTrigger +class ForceOfNatureTrigger : public BoostTrigger { public: - EclipseLunarCooldownTrigger(PlayerbotAI* ai) : SpellCooldownTrigger(ai, "eclipse (lunar)") {} - bool IsActive() override { return bot->HasSpellCooldown(48518); } + ForceOfNatureTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "force of nature") {} +}; + +class MangleBearTrigger : public DebuffTrigger +{ +public: + MangleBearTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "mangle (bear)") {} + + bool IsActive() override + { + if (!bot->IsInCombat() || !botAI->HasAnyAuraOf(bot, "bear form", "dire bear form", nullptr)) + return false; + Unit* target = GetTarget(); + return target && target->IsAlive() && target->IsInWorld(); + } +}; + +class LacerateTrigger : public DebuffTrigger +{ +public: + LacerateTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "lacerate") {} + + bool IsActive() override + { + if (!bot->IsInCombat() || !botAI->HasAnyAuraOf(bot, "bear form", "dire bear form", nullptr)) + return false; + + Unit* target = GetTarget(); + if (!target || !target->IsAlive() || !target->IsInWorld()) + return false; + + Aura* lacerate = botAI->GetAura("lacerate", target, false, false); + if (!lacerate) + return true; + + if (lacerate->GetStackAmount() < 5) + return true; + + return lacerate->GetDuration() <= 6000; + } +}; + +class DemoralizeRoarTrigger : public DebuffTrigger +{ +public: + DemoralizeRoarTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "demoralizing roar") {} + + bool IsActive() override + { + return DebuffTrigger::IsActive() + && !botAI->HasAura("curse of weakness", GetTarget(), false, false) + && !botAI->HasAura("demoralizing shout", GetTarget(), false, false) + && !botAI->HasAura("vindication", GetTarget(), false, false); + } }; class MangleCatTrigger : public DebuffTrigger @@ -238,6 +357,8 @@ public: MangleCatTrigger(PlayerbotAI* ai) : DebuffTrigger(ai, "mangle (cat)", 1, false, 0.0f) {} bool IsActive() override { + if (botAI->HasAura("prowl", bot)) + return false; return DebuffTrigger::IsActive() && !botAI->HasAura("mangle (bear)", GetTarget(), false, false, -1, true) && !botAI->HasAura("trauma", GetTarget(), false, false, -1, true); } @@ -271,10 +392,36 @@ public: } }; +class FerociousBiteExecuteTrigger : public Trigger +{ +public: + FerociousBiteExecuteTrigger(PlayerbotAI* ai) : Trigger(ai, "ferocious bite execute") {} + bool IsActive() override + { + Unit* target = AI_VALUE(Unit*, "current target"); + if (!target || !target->IsAlive()) + return false; + + if (!AI_VALUE2(uint32, "spell id", "ferocious bite")) + return false; + + if (AI_VALUE2(uint8, "combo", "current target") < 1) + return false; + + if (target->GetHealthPct() >= 25.0f) + return false; + + if (target->GetHealth() >= 20000) + return false; + + return true; + } +}; + class HurricaneChannelCheckTrigger : public Trigger { public: - HurricaneChannelCheckTrigger(PlayerbotAI* botAI, uint32 minEnemies = 2) + HurricaneChannelCheckTrigger(PlayerbotAI* botAI, uint32 minEnemies = 3) : Trigger(botAI, "hurricane channel check"), minEnemies(minEnemies) { } @@ -297,4 +444,12 @@ public: } }; +class ProwlTrigger : public Trigger +{ +public: + ProwlTrigger(PlayerbotAI* botAI) : Trigger(botAI, "prowl") {} + + bool IsActive() override; +}; + #endif diff --git a/src/Ai/Class/Warlock/Action/WarlockActions.cpp b/src/Ai/Class/Warlock/Action/WarlockActions.cpp index 93798b702..5e23f1af5 100644 --- a/src/Ai/Class/Warlock/Action/WarlockActions.cpp +++ b/src/Ai/Class/Warlock/Action/WarlockActions.cpp @@ -27,6 +27,9 @@ bool CastDrainSoulAction::isUseful() { return AI_VALUE2(uint32, "item count", "s // Checks if the bot's health is above a certain threshold, and if so, allows casting Life Tap bool CastLifeTapAction::isUseful() { return AI_VALUE2(uint8, "health", "self target") > sPlayerbotAIConfig.lowHealth; } +Value* CastBanishOnCcAction::GetTargetValue() { return context->GetValue("rti cc target"); } +Value* CastFearOnCcAction::GetTargetValue() { return context->GetValue("rti cc target"); } + // Checks if the target marked with the moon icon can be banished bool CastBanishOnCcAction::isPossible() { diff --git a/src/Ai/Class/Warlock/Action/WarlockActions.h b/src/Ai/Class/Warlock/Action/WarlockActions.h index 09f346370..8fca0fdc4 100644 --- a/src/Ai/Class/Warlock/Action/WarlockActions.h +++ b/src/Ai/Class/Warlock/Action/WarlockActions.h @@ -170,6 +170,7 @@ class CastBanishOnCcAction : public CastCrowdControlSpellAction { public: CastBanishOnCcAction(PlayerbotAI* botAI) : CastCrowdControlSpellAction(botAI, "banish") {} + Value* GetTargetValue() override; bool isPossible() override; }; @@ -177,6 +178,7 @@ class CastFearOnCcAction : public CastCrowdControlSpellAction { public: CastFearOnCcAction(PlayerbotAI* botAI) : CastCrowdControlSpellAction(botAI, "fear") {} + Value* GetTargetValue() override; bool isPossible() override; }; diff --git a/src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp b/src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp index 1df531e7f..14e7a52e5 100644 --- a/src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp +++ b/src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp @@ -50,22 +50,6 @@ bool TooManySoulShardsTrigger::IsActive() { return GetSoulShardCount(botAI->GetB bool OutOfSoulstoneTrigger::IsActive() { return GetSoulstoneCount(botAI->GetBot()) == 0; } -// Checks if the target marked with the moon icon can be banished -bool BanishTrigger::IsActive() -{ - Unit* ccTarget = context->GetValue("cc target", "banish")->Get(); - Unit* moonTarget = context->GetValue("rti cc target")->Get(); - return ccTarget && moonTarget && ccTarget == moonTarget && HasCcTargetTrigger::IsActive(); -} - -// Checks if the target marked with the moon icon can be feared -bool FearTrigger::IsActive() -{ - Unit* ccTarget = context->GetValue("cc target", "fear")->Get(); - Unit* moonTarget = context->GetValue("rti cc target")->Get(); - return ccTarget && moonTarget && ccTarget == moonTarget && HasCcTargetTrigger::IsActive(); -} - bool DemonArmorTrigger::IsActive() { Unit* target = GetTarget(); diff --git a/src/Ai/Class/Warlock/Trigger/WarlockTriggers.h b/src/Ai/Class/Warlock/Trigger/WarlockTriggers.h index d66fe537e..15991e270 100644 --- a/src/Ai/Class/Warlock/Trigger/WarlockTriggers.h +++ b/src/Ai/Class/Warlock/Trigger/WarlockTriggers.h @@ -8,6 +8,7 @@ #include "GenericTriggers.h" #include "PlayerbotAI.h" +#include "RtiTriggers.h" #include "Playerbots.h" #include "CureTriggers.h" #include "Trigger.h" @@ -137,18 +138,16 @@ public: // CC and Pet Triggers -class BanishTrigger : public HasCcTargetTrigger +class BanishTrigger : public RtiCcTrigger { public: - BanishTrigger(PlayerbotAI* botAI) : HasCcTargetTrigger(botAI, "banish") {} - bool IsActive() override; + BanishTrigger(PlayerbotAI* botAI) : RtiCcTrigger(botAI, "banish") {} }; -class FearTrigger : public HasCcTargetTrigger +class FearTrigger : public RtiCcTrigger { public: - FearTrigger(PlayerbotAI* botAI) : HasCcTargetTrigger(botAI, "fear") {} - bool IsActive() override; + FearTrigger(PlayerbotAI* botAI) : RtiCcTrigger(botAI, "fear") {} }; class SpellLockInterruptSpellTrigger : public InterruptSpellTrigger diff --git a/src/Bot/Factory/AiFactory.cpp b/src/Bot/Factory/AiFactory.cpp index c95180538..f1fd2491f 100644 --- a/src/Bot/Factory/AiFactory.cpp +++ b/src/Bot/Factory/AiFactory.cpp @@ -338,15 +338,17 @@ void AiFactory::AddDefaultCombatStrategies(Player* player, PlayerbotAI* const fa break; case CLASS_DRUID: if (tab == DRUID_TAB_BALANCE) - engine->addStrategiesNoInit("caster", "cure", "caster aoe", "caster debuff", "dps assist", nullptr); + { + engine->addStrategiesNoInit("balance", "cure", "aoe", "cc", "dps assist", nullptr); + } else if (tab == DRUID_TAB_RESTORATION) - engine->addStrategiesNoInit("heal", "cure", "dps assist", nullptr); - else // if (tab == DRUID_TAB_FERAL) + engine->addStrategiesNoInit("resto", "cure", "dps assist", "blanketing", "tranquility", nullptr); + else { if (player->HasSpell(768) /*cat form*/ && !player->HasAura(16931) /*thick hide*/) - engine->addStrategiesNoInit("cat", "dps assist", nullptr); + engine->addStrategiesNoInit("cat", "aoe", "cc", "dps assist", "feral charge", nullptr); else - engine->addStrategiesNoInit("bear", "tank assist", "pull", "pull back", nullptr); + engine->addStrategiesNoInit("bear", "tank assist", "pull", "pull back", "feral charge", nullptr); } break; case CLASS_HUNTER: @@ -420,8 +422,7 @@ void AiFactory::AddDefaultCombatStrategies(Player* player, PlayerbotAI* const fa { if (tab == DRUID_TAB_RESTORATION) { - engine->addStrategiesNoInit("caster", "caster aoe", nullptr); - engine->addStrategy("caster debuff", false); + engine->addStrategiesNoInit("aoe", nullptr); } break; }