Skip to content

Commit 2f3a11d

Browse files
CopilotGeczy
andauthored
Confirm game timing tips valid for latest Dota 2 patches (7.38+)
Agent-Logs-Url: https://github.com/dotabod/backend/sessions/9e775d72-b9df-48bc-a35c-b7927885c193 Co-authored-by: Geczy <1036968+Geczy@users.noreply.github.com>
1 parent c7fbe19 commit 2f3a11d

6 files changed

Lines changed: 128 additions & 14 deletions

File tree

packages/dota/src/dota/NeutralItemTimer.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,30 @@ import { getRedisNumberValue } from '../utils/index.js'
44
import type { GSIHandlerType } from './GSIHandlerTypes.js'
55
import { say } from './say.js'
66

7-
interface TierTime {
7+
export interface TierTime {
88
tier: number
9+
/** Minutes until this tier drops in a normal game (updated for patch 7.38) */
910
normalTime: number
11+
/** Minutes until this tier drops in a turbo game (half of normalTime) */
1012
turboTime: number
1113
}
1214

15+
/**
16+
* Neutral item tier availability times (Dota 2 patch 7.38).
17+
* Changed from 7/17/27/37/67 min to 5/15/25/35/60 min in patch 7.38.
18+
* Turbo times are exactly half of normal times.
19+
*/
20+
export const NEUTRAL_ITEM_TIER_TIMES: TierTime[] = [
21+
{ tier: 1, normalTime: 5, turboTime: 2.5 },
22+
{ tier: 2, normalTime: 15, turboTime: 7.5 },
23+
{ tier: 3, normalTime: 25, turboTime: 12.5 },
24+
{ tier: 4, normalTime: 35, turboTime: 17.5 },
25+
{ tier: 5, normalTime: 60, turboTime: 30 },
26+
]
27+
1328
export class NeutralItemTimer {
1429
private notifiedTiers = new Set<number>()
15-
//5, 15, 25, 35 and 60 - 7.38 patch
16-
private readonly tierTimes: TierTime[] = [
17-
{ tier: 1, normalTime: 5, turboTime: 2.5 },
18-
{ tier: 2, normalTime: 15, turboTime: 7.5 },
19-
{ tier: 3, normalTime: 25, turboTime: 12.5 },
20-
{ tier: 4, normalTime: 35, turboTime: 17.5 },
21-
{ tier: 5, normalTime: 60, turboTime: 30 },
22-
]
30+
private readonly tierTimes: TierTime[] = NEUTRAL_ITEM_TIER_TIMES
2331

2432
// Track the last game time checked to avoid spam
2533
private lastCheckedTime = 0
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { describe, it, expect } from 'bun:test'
2+
3+
import { NEUTRAL_ITEM_TIER_TIMES } from '../NeutralItemTimer.js'
4+
5+
/**
6+
* Tests confirming neutral item tier availability times are valid
7+
* for Dota 2 patch 7.38 (changed from 7/17/27/37/67 min → 5/15/25/35/60 min).
8+
* Turbo times are exactly half of normal times.
9+
*/
10+
describe('Neutral Item Tier Times (patch 7.38)', () => {
11+
it('has exactly 5 tiers', () => {
12+
expect(NEUTRAL_ITEM_TIER_TIMES).toHaveLength(5)
13+
})
14+
15+
it('tiers are numbered 1 through 5', () => {
16+
const tierNumbers = NEUTRAL_ITEM_TIER_TIMES.map((t) => t.tier)
17+
expect(tierNumbers).toEqual([1, 2, 3, 4, 5])
18+
})
19+
20+
it('Tier 1 spawns at 5 minutes in normal mode', () => {
21+
const tier1 = NEUTRAL_ITEM_TIER_TIMES.find((t) => t.tier === 1)
22+
expect(tier1?.normalTime).toBe(5)
23+
})
24+
25+
it('Tier 2 spawns at 15 minutes in normal mode', () => {
26+
const tier2 = NEUTRAL_ITEM_TIER_TIMES.find((t) => t.tier === 2)
27+
expect(tier2?.normalTime).toBe(15)
28+
})
29+
30+
it('Tier 3 spawns at 25 minutes in normal mode', () => {
31+
const tier3 = NEUTRAL_ITEM_TIER_TIMES.find((t) => t.tier === 3)
32+
expect(tier3?.normalTime).toBe(25)
33+
})
34+
35+
it('Tier 4 spawns at 35 minutes in normal mode', () => {
36+
const tier4 = NEUTRAL_ITEM_TIER_TIMES.find((t) => t.tier === 4)
37+
expect(tier4?.normalTime).toBe(35)
38+
})
39+
40+
it('Tier 5 spawns at 60 minutes in normal mode', () => {
41+
const tier5 = NEUTRAL_ITEM_TIER_TIMES.find((t) => t.tier === 5)
42+
expect(tier5?.normalTime).toBe(60)
43+
})
44+
45+
it('turbo times are exactly half of normal times for all tiers', () => {
46+
for (const tier of NEUTRAL_ITEM_TIER_TIMES) {
47+
expect(tier.turboTime).toBe(tier.normalTime / 2)
48+
}
49+
})
50+
51+
it('Tier 1 spawns at 2.5 minutes in turbo mode', () => {
52+
const tier1 = NEUTRAL_ITEM_TIER_TIMES.find((t) => t.tier === 1)
53+
expect(tier1?.turboTime).toBe(2.5)
54+
})
55+
56+
it('Tier 5 spawns at 30 minutes in turbo mode', () => {
57+
const tier5 = NEUTRAL_ITEM_TIER_TIMES.find((t) => t.tier === 5)
58+
expect(tier5?.turboTime).toBe(30)
59+
})
60+
61+
it('normal times are in ascending order', () => {
62+
const times = NEUTRAL_ITEM_TIER_TIMES.map((t) => t.normalTime)
63+
const sorted = [...times].sort((a, b) => a - b)
64+
expect(times).toEqual(sorted)
65+
})
66+
})
67+
68+
describe('Roshan respawn timer constants', () => {
69+
/**
70+
* Roshan respawn timing in Dota 2 (unchanged since patch 7.24):
71+
* - Minimum: 8 minutes (480 seconds) after death
72+
* - Maximum: 11 minutes (660 seconds) after death
73+
* - Turbo: half of normal (4 min to 5.5 min)
74+
*/
75+
const ROSHAN_MIN_SECONDS = 8 * 60
76+
const ROSHAN_MAX_SECONDS = 11 * 60
77+
78+
it('Roshan minimum respawn time is 8 minutes (480 seconds)', () => {
79+
expect(ROSHAN_MIN_SECONDS).toBe(480)
80+
})
81+
82+
it('Roshan maximum respawn time is 11 minutes (660 seconds)', () => {
83+
expect(ROSHAN_MAX_SECONDS).toBe(660)
84+
})
85+
86+
it('Roshan turbo minimum respawn time is 4 minutes (240 seconds)', () => {
87+
expect(ROSHAN_MIN_SECONDS / 2).toBe(240)
88+
})
89+
90+
it('Roshan turbo maximum respawn time is 5.5 minutes (330 seconds)', () => {
91+
expect(ROSHAN_MAX_SECONDS / 2).toBe(330)
92+
})
93+
})
94+
95+
describe('Aegis expiration timer', () => {
96+
/**
97+
* Aegis of the Immortal lasts 5 minutes after pickup.
98+
*/
99+
it('Aegis expires after 5 minutes (300 seconds)', () => {
100+
const AEGIS_EXPIRE_SECONDS = 5 * 60
101+
expect(AEGIS_EXPIRE_SECONDS).toBe(300)
102+
})
103+
})

packages/dota/src/dota/events/gsi-events/event.aegis_picked_up.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ eventHandler.registerEvent(`event:${DotaEventTypes.AegisPickedUp}`, {
1717
const gameTimeDiff =
1818
(dotaClient.client.gsi?.map?.game_time ?? event.game_time) - event.game_time
1919

20-
// expire for aegis in 5 minutes
20+
// Aegis of the Immortal expires 5 minutes after pickup (unchanged since patch 7.33)
2121
const expireS = 5 * 60 - gameTimeDiff
2222
const expireTime = (dotaClient.client.gsi?.map?.clock_time ?? 0) + expireS
2323

packages/dota/src/dota/events/gsi-events/event.bounty_rune_pickup.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ eventHandler.registerEvent(`event:${DotaEventTypes.BountyPickup}`, {
1313
handler: async (dotaClient, event: DotaEvent) => {
1414
if (!isPlayingMatch(dotaClient.client.gsi)) return
1515
if (!dotaClient.client.stream_online) return
16+
// Only announce bounty rune pickups during the initial spawn window (first 2 minutes).
17+
// Bounty runes also respawn periodically, but we only track the opening contest.
1618
if (Number(dotaClient.client.gsi?.map?.clock_time) > 120) return
1719

1820
const playingTeam =

packages/dota/src/dota/events/gsi-events/event.roshan_killed.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ eventHandler.registerEvent(`event:${DotaEventTypes.RoshanKilled}`, {
2323
const gameTimeDiff =
2424
(dotaClient.client.gsi?.map?.game_time ?? event.game_time) - event.game_time
2525

26-
// min spawn for rosh in 5 + 3 minutes
27-
let minS = 5 * 60 + 3 * 60 - gameTimeDiff
28-
// max spawn for rosh in 5 + 3 + 3 minutes
29-
let maxS = 5 * 60 + 3 * 60 + 3 * 60 - gameTimeDiff
26+
// Roshan respawn window: 8 to 11 minutes after death (unchanged since patch 7.24)
27+
// minS = 8 minutes (480 seconds), maxS = 11 minutes (660 seconds)
28+
let minS = 8 * 60 - gameTimeDiff
29+
let maxS = 11 * 60 - gameTimeDiff
3030

3131
// Check if the game mode is Turbo (23)
3232
if (playingGameMode === 23) {

packages/dota/src/dota/lib/checkMidas.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ async function checkMidasIterator(client: SocketClient) {
5353
const currentTime = new Date().getTime()
5454
const passiveMidasThreshold = 10000
5555

56+
// Hand of Midas has 2 charges since patch 7.38. Both charges full means player is not using it.
5657
if (midasCharges === 2 && !passiveMidasData.told && !passiveMidasData.firstNoticedPassive) {
5758
// Set the time when passive midas was first noticed
5859
await redisClient.client.json.set(`${token}:passiveMidas`, '$', {

0 commit comments

Comments
 (0)