Feature/rework bot behaviour#2026
Conversation
| */ | ||
| private lastKnownByTeam: Map<number, Map<number, { power: number; posX: number; posY: number }>> = | ||
| new Map(); | ||
| private ghostsByTeam: Map<number, Map<number, GhostEntry>> = new Map(); |
There was a problem hiding this comment.
Expected blank line between class members.
| private ghostsByTeam: Map<number, Map<number, GhostEntry>> = new Map(); | |
| private ghostsByTeam: Map<number, Map<number, GhostEntry>> = new Map(); |
There was a problem hiding this comment.
Pull request overview
This PR reworks core bot decision-making by introducing shared team-level state (missing enemies, fog “ghost” threat, and target reservations), new math/utility helpers, and significant updates to PUSH/ATTACK/RETREAT desirability plus a new damage-evasion layer.
Changes:
- Add
TeamCommanderblackboard with enemy missing counts, fog-ghost threat decay, and target-claim tracking. - Rebalance mode desires (ATTACK/RETREAT/PUSH) using power estimation + logistic/linear curves, and add FSA hysteresis + micro-jitter to reduce flip-flopping.
- Add pure-logic behavior utilities and a new dodge system (HP spike/sustained damage/channeling/clumping) to improve survivability and positioning.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/vscripts/ai/team/team-commander.ts | New singleton blackboard for missing-enemy counts, fog ghosts, and target claims |
| src/vscripts/ai/team/team-commander.test.ts | Unit tests for per-team missing counts and ghost lifecycle/decay |
| src/vscripts/ai/mode/utility-math.ts | New helper curves (linear/quadratic/logistic) for desire shaping |
| src/vscripts/ai/mode/utility-math.test.ts | Unit tests for curve behavior including inverted ranges |
| src/vscripts/ai/mode/power-util.ts | New combat power estimator (HP×level adjusted by mana and ult cooldown) |
| src/vscripts/ai/mode/mode-retreat.ts | New retreat desire composition (health panic, missing enemies, power ratio, tower rules) |
| src/vscripts/ai/mode/mode-retreat.test.ts | Tests for retreat desire components and caps |
| src/vscripts/ai/mode/mode-push.ts | Push desire now suppressed by HP and outnumbered power ratio |
| src/vscripts/ai/mode/mode-push.test.ts | Tests for push desire scaling and suppression |
| src/vscripts/ai/mode/mode-laning.test.ts | Adds unit tests for laning desire baseline/threshold behavior |
| src/vscripts/ai/mode/mode-base.ts | Adds hysteresisBonus field for mode stickiness |
| src/vscripts/ai/mode/mode-attack.ts | Attack desire rebuilt around local superiority, tower-dive suppression, missing-enemy bait checks |
| src/vscripts/ai/mode/mode-attack.test.ts | Tests for superiority→desire mapping, tower modifiers, mana scaling, and caps |
| src/vscripts/ai/mode/bot-behavior-util.ts | New pure-logic helpers (tower confidence, flee vectors, orb-walk heuristic, thresholds) |
| src/vscripts/ai/mode/bot-behavior-util.test.ts | Unit tests for pure-logic helpers |
| src/vscripts/ai/mode/FSA.ts | Mode selection now uses score (raw + jitter + hysteresis) but gates switching by raw desire threshold |
| src/vscripts/ai/mode/FSA.test.ts | Tests for switching, hysteresis behavior, and raw-threshold gating |
| src/vscripts/ai/hero/bot-base.ts | Adds personality biases, dodge system, last-hit/kill targeting helpers, target-claim integration, retreat logic changes |
| src/vscripts/ai/action/action-move.ts | Adds centroid-based FleeTowardSafety helper |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| UnitTargetType.HERO, | ||
| UnitTargetFlags.NONE, | ||
| ); | ||
| return allies.length > 0 ? allies[0] : undefined; |
There was a problem hiding this comment.
FindNearestAllyHero() uses ActionFind.Find(... FRIENDLY, HERO ...) and returns allies[0]. Because the query includes this.hero itself, the closest result will typically be self (distance 0), so this helper won’t actually return an ally. Filter out this.hero from the results (and consider returning undefined when no other allies are found).
| return allies.length > 0 ? allies[0] : undefined; | |
| for (const ally of allies) { | |
| if (ally !== this.hero) { | |
| return ally; | |
| } | |
| } | |
| return undefined; |
| const lastHitCreep = this.FindCreepToLastHit(); | ||
| if (lastHitCreep) { | ||
| if (this.CastCreep()) { | ||
| return true; | ||
| } |
There was a problem hiding this comment.
lastHitCreep is selected, but the next step calls CastCreep() which (via UseItem.UseItemCreep(...)) targets aroundEnemyCreeps[0] rather than the chosen lastHitCreep. This can fire creep-targeted items/abilities on the wrong creep and miss the last-hit. Consider passing lastHitCreep explicitly (or filtering/reordering the creep list) before attempting creep casts.
| /** | ||
| * Target reservation: tracks how many bots have claimed each target this | ||
| * second. Cleared every updateInterval to prevent stale penalisation. | ||
| * Key: entity index (number). Value: claim count. | ||
| */ | ||
| private targetClaims: Map<number, number> = new Map(); | ||
|
|
There was a problem hiding this comment.
targetClaims is stored once on the singleton, so claim counts will be shared across both teams. That can cause DIRE/RADIANT bots to penalize each other’s targets if both teams are running bots in the same match. Consider storing claims per team (e.g., Map<DOTATeam_t, Map<entityIndex, count>>).
| this.lastUpdateTimeByTeam[botTeam] = gameTime; | ||
| // Reset target claims each second so stale reservations don't persist. | ||
| this.targetClaims.clear(); |
There was a problem hiding this comment.
this.targetClaims.clear() clears claims globally, even though UpdateGameState is executed per-team (cooldown tracked in lastUpdateTimeByTeam). With the current design, one team’s update can wipe the other team’s in-progress claims. If claims are moved to per-team storage, clear only the calling team’s claim map here.
| lastKnown.set(playerId, { | ||
| power: (hero.GetHealthPercent() / 100) * hero.GetLevel(), | ||
| posX: heroPos.x, | ||
| posY: heroPos.y, | ||
| }); |
There was a problem hiding this comment.
Ghost power is currently computed as (hp%/100) * level, but the rest of the PR introduces PowerUtil.CalculatePowerUnit() (mana ratio + ult cooldown) and uses it for live ally/enemy power. This makes ghost power systematically inconsistent with visible-unit power, which can skew outnumbered/attack decisions right after an enemy disappears. Consider using PowerUtil.CalculatePowerUnit(hero) when recording lastKnown so the same power metric is used everywhere.
| // Sum HP and DPS of all nearby allies + self | ||
| const allyHeroes = ActionFind.Find( | ||
| hero, | ||
| 1200, | ||
| UnitTargetTeam.FRIENDLY, | ||
| UnitTargetType.HERO, | ||
| UnitTargetFlags.NONE, | ||
| ); | ||
| let teamHp = hero.GetHealth(); | ||
| let teamDps = hero.GetLevel() * 10; | ||
| for (const ally of allyHeroes) { | ||
| teamHp += ally.GetHealth(); | ||
| teamDps += ally.GetLevel() * 10; | ||
| } |
There was a problem hiding this comment.
ActionFind.Find(... FRIENDLY, HERO ...) ultimately wraps FindUnitsInRadius, which includes the querying hero itself (distance 0). Since teamHp/teamDps already start with hero and then iterate allyHeroes, the hero will be double-counted, inflating TowerKillConfidence and reducing tower-dive suppression. Filter allyHeroes to exclude hero (or start sums at 0 and rely on the list) to avoid overconfidence under towers.
| ); | ||
| let teamHp = this.hero.GetHealth(); | ||
| let teamDps = this.hero.GetLevel() * 10; | ||
| for (const ally of allyHeroes) { |
There was a problem hiding this comment.
Same self-inclusion issue as in ModeAttack: ActionFind.Find(... FRIENDLY, HERO ...) will include this.hero, so teamHp/teamDps (which already include this.hero) get the bot double-counted. That can incorrectly return REWARD_FULL/PARTIAL and suppress dodging when it shouldn’t. Exclude this.hero from allyHeroes (or don’t pre-seed the sums) before computing confidence.
| for (const ally of allyHeroes) { | |
| for (const ally of allyHeroes) { | |
| if (ally === this.hero) { | |
| continue; | |
| } |
Issue
Checklist
Release Note