@vibemancer/core 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/chunk-W7HWJFQ4.js +10599 -0
- package/dist/chunk-W7HWJFQ4.js.map +1 -0
- package/dist/index-browser.d.ts +1300 -1051
- package/dist/index-browser.js +55 -17
- package/dist/index.d.ts +53 -3
- package/dist/index.js +200 -54
- package/dist/index.js.map +1 -1
- package/package.json +21 -15
- package/src/bots/Hero.ts +867 -0
- package/src/bots/berserker/01_Stormchaser.ts +162 -120
- package/src/bots/berserker/02_Stormcaller.ts +160 -116
- package/src/bots/berserker/03_Stormforger.ts +162 -129
- package/src/bots/caster/01_Flamecaller.ts +126 -84
- package/src/bots/caster/02_Pyromancer.ts +148 -101
- package/src/bots/caster/03_Infernalist.ts +180 -160
- package/src/bots/defensive/01_Turtle.ts +48 -44
- package/src/bots/defensive/02_Sentinel.ts +57 -53
- package/src/bots/defensive/03_Golem.ts +85 -97
- package/src/bots/duelist/01_Battlemage.ts +157 -122
- package/src/bots/duelist/02_Warmage.ts +166 -121
- package/src/bots/duelist/03_Archmage.ts +198 -178
- package/src/bots/homing/01_Bonemancer.ts +20 -32
- package/src/bots/homing/02_Lich.ts +145 -93
- package/src/bots/homing/03_Archlich.ts +129 -60
- package/src/bots/kiter/01_Spellspinner.ts +145 -107
- package/src/bots/kiter/02_Spellweaver.ts +153 -105
- package/src/bots/kiter/03_Spellbinder.ts +168 -123
- package/src/bots/melee/01_Shadowblade.ts +131 -75
- package/src/bots/melee/02_Nightblade.ts +174 -130
- package/src/bots/melee/03_Voidblade.ts +266 -168
- package/src/bots/registry.ts +44 -37
- package/src/bots/shared.ts +185 -25
- package/src/bots/sniper/01_Spellshot.ts +151 -98
- package/src/bots/sniper/02_Spelltracer.ts +173 -128
- package/src/bots/sniper/03_Spellseeker.ts +177 -152
- package/src/bots/standalone/Critter.ts +46 -52
- package/src/bots/standalone/Doombringer.ts +36 -40
- package/src/bots/standalone/Hogger.ts +120 -89
- package/src/bots/standalone/Rookie.ts +21 -23
- package/src/bots/standalone/TargetDummy.ts +5 -6
- package/src/bots/test/cheater.ts +91 -85
- package/src/bots/test/crasher.ts +26 -28
- package/src/engine/bundle-fight.ts +242 -0
- package/src/engine/hooks-runtime.ts +58 -18
- package/src/engine/manual-match.ts +33 -25
- package/src/engine/missile-templates.ts +25 -43
- package/src/engine/optimizer.ts +7 -7
- package/src/engine/params-runtime.ts +3 -3
- package/src/engine/physics.ts +33 -23
- package/src/engine/sandbox-browser.ts +8 -7
- package/src/engine/sandbox-compile.ts +1 -1
- package/src/engine/sandbox-harness.ts +299 -367
- package/src/engine/sandbox.ts +3 -3
- package/src/engine/simulation.ts +291 -76
- package/src/engine/spells.ts +9 -12
- package/src/engine-version.ts +1 -1
- package/src/hooks/action-builders.ts +76 -21
- package/src/hooks/index.ts +17 -9
- package/src/hooks/state-hooks.ts +61 -25
- package/src/hooks/threat-analysis.ts +44 -20
- package/src/hooks/types.ts +62 -5
- package/src/index.ts +2 -0
- package/src/rules.ts +209 -63
- package/src/stats.ts +2 -2
- package/src/testing.ts +6 -30
- package/src/trace.ts +63 -3
- package/src/types.ts +127 -14
- package/src/utils/angles.ts +1 -0
- package/src/utils/combat.ts +98 -6
- package/src/utils/spatial.ts +3 -3
- package/dist/chunk-74FFJCFF.js +0 -9140
- package/dist/chunk-74FFJCFF.js.map +0 -1
- package/src/hooks/bot-wrapper.ts +0 -84
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type {FinalAction} from '../../hooks/index.js';
|
|
2
|
+
import {useStatus, useBlinkCooldown, useRandom, usePosition, move as moveAction, missile, shield, blink, cancel, getMissileContext, turnToward, flyStraight} from '../../hooks/index.js';
|
|
3
|
+
import {avoidLava} from '../shared.js';
|
|
4
|
+
import {ARENA_MIN, ARENA_MAX} from '../../rules.js';
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* Bot: Critter
|
|
@@ -19,71 +21,63 @@ import {ARENA_WIDTH, ARENA_HEIGHT} from '../../rules.js';
|
|
|
19
21
|
* "Hogger" bot could be an elite critter: same chaotic spirit but actually dangerous
|
|
20
22
|
* (like the famous WoW elite that wipes unprepared lowbies).
|
|
21
23
|
*/
|
|
22
|
-
export
|
|
24
|
+
export function Critter(): FinalAction
|
|
23
25
|
{
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
y: random() * 200 - 100, // -100 to 100
|
|
29
|
-
};
|
|
26
|
+
const status = useStatus();
|
|
27
|
+
const blinkCooldown = useBlinkCooldown();
|
|
28
|
+
const random = useRandom();
|
|
29
|
+
const position = usePosition();
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
const rawMove = {x: random() * 2 - 1, y: random() * 2 - 1};
|
|
32
|
+
const safe = avoidLava(position, rawMove);
|
|
33
|
+
const mx = safe.x;
|
|
34
|
+
const my = safe.y;
|
|
35
|
+
|
|
36
|
+
if (status === 'idle' && random() > 0.95)
|
|
32
37
|
{
|
|
33
38
|
const spellRoll = random();
|
|
34
|
-
if (spellRoll > 0.6)
|
|
39
|
+
if (spellRoll > 0.6)
|
|
35
40
|
{
|
|
36
|
-
return
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
y: missileRandom() * ARENA_HEIGHT,
|
|
53
|
-
}
|
|
54
|
-
: undefined,
|
|
55
|
-
}),
|
|
56
|
-
direction: random() * 360,
|
|
41
|
+
return missile(
|
|
42
|
+
{
|
|
43
|
+
damage: Math.floor(random() * 20) + 5,
|
|
44
|
+
speed: random() * 5 + 2,
|
|
45
|
+
turnRate: random() * 3,
|
|
46
|
+
duration: Math.floor(random() * 200) + 100,
|
|
47
|
+
},
|
|
48
|
+
// Missile AI gets its own random from engine - use it for random targeting
|
|
49
|
+
() =>
|
|
50
|
+
{
|
|
51
|
+
const ctx = getMissileContext();
|
|
52
|
+
if (ctx.random() > 0.5)
|
|
53
|
+
{
|
|
54
|
+
return turnToward(ARENA_MIN + ctx.random() * (ARENA_MAX - ARENA_MIN), ARENA_MIN + ctx.random() * (ARENA_MAX - ARENA_MIN));
|
|
55
|
+
}
|
|
56
|
+
return flyStraight();
|
|
57
57
|
},
|
|
58
|
-
|
|
58
|
+
random() * 360,
|
|
59
|
+
).move(mx, my);
|
|
59
60
|
}
|
|
60
|
-
else if (spellRoll > 0.3)
|
|
61
|
+
else if (spellRoll > 0.3)
|
|
61
62
|
{
|
|
62
|
-
return
|
|
63
|
-
move,
|
|
64
|
-
startCast: {spell: 'shield'},
|
|
65
|
-
};
|
|
63
|
+
return shield().move(mx, my);
|
|
66
64
|
}
|
|
67
|
-
else if (
|
|
65
|
+
else if (blinkCooldown === 0)
|
|
68
66
|
{
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
startCast: {
|
|
72
|
-
spell: 'blink',
|
|
73
|
-
target: {x: random() * ARENA_WIDTH, y: random() * ARENA_HEIGHT},
|
|
74
|
-
},
|
|
75
|
-
};
|
|
67
|
+
const margin = 50;
|
|
68
|
+
return blink(ARENA_MIN + margin + random() * (ARENA_MAX - ARENA_MIN - margin * 2), ARENA_MIN + margin + random() * (ARENA_MAX - ARENA_MIN - margin * 2));
|
|
76
69
|
}
|
|
77
70
|
}
|
|
78
71
|
|
|
79
72
|
if (
|
|
80
|
-
(
|
|
73
|
+
(status === 'casting' || status === 'channeling') &&
|
|
81
74
|
random() > 0.98
|
|
82
|
-
)
|
|
75
|
+
)
|
|
83
76
|
{
|
|
84
|
-
return
|
|
77
|
+
return cancel().move(mx, my);
|
|
85
78
|
}
|
|
86
79
|
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
80
|
+
// Critter doesn't know where anything is — just move randomly
|
|
81
|
+
// Note: aimDirection override is not supported by hooks API, so we just move randomly
|
|
82
|
+
return moveAction(mx, my);
|
|
83
|
+
}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type {FinalAction, MissileFunction} from '../../hooks/index.js';
|
|
2
|
+
import {usePosition, useEnemy, useStatus, useBlinkCooldown, useThreats, idle, missile, shield, blink, cancel, getMissileContext, turnToward, flyStraight} from '../../hooks/index.js';
|
|
2
3
|
import {angleTo} from '../../utils/angles.js';
|
|
3
4
|
import {distanceTo} from '../../utils/distance.js';
|
|
4
5
|
import {fitMissileToBudget} from '../../utils/combat.js';
|
|
5
6
|
import {
|
|
6
|
-
getThreats,
|
|
7
7
|
getMostUrgentThreat,
|
|
8
8
|
getBlinkToCenter,
|
|
9
9
|
} from '../shared.js';
|
|
10
10
|
|
|
11
|
-
const HomingAI:
|
|
11
|
+
const HomingAI: MissileFunction = () =>
|
|
12
12
|
{
|
|
13
|
-
const
|
|
14
|
-
|
|
13
|
+
const ctx = getMissileContext();
|
|
14
|
+
const enemy = ctx.worldState.enemies[0];
|
|
15
|
+
return enemy ? turnToward(enemy.position.x, enemy.position.y) : flyStraight();
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -24,68 +25,63 @@ const HomingAI: MissileAIFunction = ({worldState}) =>
|
|
|
24
25
|
*
|
|
25
26
|
* STANDALONE — no tier progression. Benchmark/test bot.
|
|
26
27
|
*/
|
|
27
|
-
export
|
|
28
|
+
export function Doombringer(): FinalAction
|
|
28
29
|
{
|
|
29
|
-
const
|
|
30
|
-
|
|
30
|
+
const position = usePosition();
|
|
31
|
+
const enemy = useEnemy();
|
|
32
|
+
const status = useStatus();
|
|
33
|
+
const blinkCooldown = useBlinkCooldown();
|
|
34
|
+
const threats = useThreats();
|
|
35
|
+
|
|
36
|
+
if (enemy.health <= 0)
|
|
31
37
|
{
|
|
32
|
-
return
|
|
38
|
+
return idle();
|
|
33
39
|
}
|
|
34
40
|
|
|
35
|
-
const distance = distanceTo(
|
|
36
|
-
const threats = getThreats(state);
|
|
41
|
+
const distance = distanceTo(position, enemy.position);
|
|
37
42
|
const urgentThreat = getMostUrgentThreat(threats);
|
|
38
43
|
|
|
39
44
|
// === DEFENSE: Handle channeling ===
|
|
40
|
-
if (
|
|
45
|
+
if (status === 'channeling')
|
|
41
46
|
{
|
|
42
|
-
if (!urgentThreat || urgentThreat.ticksToImpact > 150)
|
|
47
|
+
if (!urgentThreat || urgentThreat.ticksToImpact > 150)
|
|
43
48
|
{
|
|
44
|
-
return
|
|
49
|
+
return cancel();
|
|
45
50
|
}
|
|
46
|
-
return
|
|
51
|
+
return idle();
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
// === DEFENSE: Shield undodgeable threats ===
|
|
50
55
|
if (
|
|
51
|
-
|
|
56
|
+
status === 'idle' &&
|
|
52
57
|
urgentThreat &&
|
|
53
58
|
!urgentThreat.bestDodgeDirection
|
|
54
|
-
)
|
|
59
|
+
)
|
|
55
60
|
{
|
|
56
|
-
if (urgentThreat.canBlockInTime && urgentThreat.ticksToStartShield <= 3)
|
|
61
|
+
if (urgentThreat.canBlockInTime && urgentThreat.ticksToStartShield <= 3)
|
|
57
62
|
{
|
|
58
|
-
return
|
|
59
|
-
move: {x: 0, y: 0},
|
|
60
|
-
startCast: {spell: 'shield'},
|
|
61
|
-
};
|
|
63
|
+
return shield();
|
|
62
64
|
}
|
|
63
|
-
else if (!urgentThreat.canBlockInTime &&
|
|
65
|
+
else if (!urgentThreat.canBlockInTime && blinkCooldown === 0)
|
|
64
66
|
{
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
|
|
68
|
-
};
|
|
67
|
+
const t = getBlinkToCenter(position);
|
|
68
|
+
return blink(t.x, t.y);
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
// === OFFENSE: Fire max-damage homing missile (no damage cap) ===
|
|
73
|
-
if (
|
|
73
|
+
if (status === 'idle')
|
|
74
74
|
{
|
|
75
75
|
const config = fitMissileToBudget(Infinity, distance, {minTurnRate: 0.5});
|
|
76
|
-
if (config)
|
|
76
|
+
if (config)
|
|
77
77
|
{
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
missileAI: HomingAI,
|
|
84
|
-
direction: angleTo(state.position, enemy.position),
|
|
85
|
-
},
|
|
86
|
-
};
|
|
78
|
+
return missile(
|
|
79
|
+
config,
|
|
80
|
+
HomingAI,
|
|
81
|
+
angleTo(position, enemy.position),
|
|
82
|
+
).move(0, 0);
|
|
87
83
|
}
|
|
88
84
|
}
|
|
89
85
|
|
|
90
|
-
return
|
|
91
|
-
}
|
|
86
|
+
return idle();
|
|
87
|
+
}
|
|
@@ -1,28 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type {FinalAction, MissileFunction} from '../../hooks/index.js';
|
|
2
|
+
import {usePosition, useEnemy, useStatus, useBlinkCooldown, useRandom, useThreats, idle, move as moveAction, missile, shield, blink, cancel, getMissileContext, turnToward, flyStraight} from '../../hooks/index.js';
|
|
3
3
|
import {distanceTo} from '../../utils/distance.js';
|
|
4
|
-
import {
|
|
4
|
+
import {angleTo} from '../../utils/angles.js';
|
|
5
|
+
import {ARENA_MIN, ARENA_MAX} from '../../rules.js';
|
|
5
6
|
import {
|
|
6
|
-
getThreats,
|
|
7
7
|
getMostUrgentThreat,
|
|
8
8
|
getDodgeDirectionForMovement,
|
|
9
9
|
getBlinkToCenter,
|
|
10
|
+
avoidLava,
|
|
10
11
|
} from '../shared.js';
|
|
12
|
+
import {useParam} from '../../engine/params-runtime.js';
|
|
11
13
|
const WALL_BUFFER = 60;
|
|
12
|
-
const WALL_PUSH =
|
|
14
|
+
const WALL_PUSH = 0.3; // How hard to push away from walls
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* Homing AI that sometimes forgets what it's doing.
|
|
16
18
|
*/
|
|
17
|
-
const ChaoticHoming:
|
|
19
|
+
const ChaoticHoming: MissileFunction = () =>
|
|
18
20
|
{
|
|
19
|
-
const
|
|
21
|
+
const ctx = getMissileContext();
|
|
22
|
+
const enemy = ctx.worldState.enemies[0];
|
|
20
23
|
// 80% chance to actually track, 20% chance to just fly straight
|
|
21
|
-
if (random() < 0.8)
|
|
24
|
+
if (ctx.random() < 0.8)
|
|
22
25
|
{
|
|
23
|
-
return
|
|
26
|
+
return enemy ? turnToward(enemy.position.x, enemy.position.y) : flyStraight();
|
|
24
27
|
}
|
|
25
|
-
return
|
|
28
|
+
return flyStraight();
|
|
26
29
|
};
|
|
27
30
|
|
|
28
31
|
/**
|
|
@@ -45,140 +48,173 @@ const ChaoticHoming: MissileAIFunction = ({worldState, random}) =>
|
|
|
45
48
|
*
|
|
46
49
|
* STANDALONE — no tier progression. There's only one Hogger.
|
|
47
50
|
*/
|
|
48
|
-
export
|
|
51
|
+
export function Hogger(): FinalAction
|
|
49
52
|
{
|
|
50
|
-
const
|
|
51
|
-
|
|
53
|
+
const position = usePosition();
|
|
54
|
+
const enemy = useEnemy();
|
|
55
|
+
const status = useStatus();
|
|
56
|
+
const blinkCooldown = useBlinkCooldown();
|
|
57
|
+
const random = useRandom();
|
|
58
|
+
const threats = useThreats();
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
const centerBias = useParam('centerBias', 0.71, {
|
|
62
|
+
range: 0.4,
|
|
63
|
+
min: 0,
|
|
64
|
+
max: 0.8,
|
|
65
|
+
steps: 5,
|
|
66
|
+
});
|
|
67
|
+
const maxBlinkDistance = useParam('maxBlinkDistance', 224, {
|
|
68
|
+
range: 100,
|
|
69
|
+
min: 50,
|
|
70
|
+
max: 300,
|
|
71
|
+
steps: 7,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
function capBlinkDistance(target: {x: number; y: number}): {x: number; y: number}
|
|
52
75
|
{
|
|
53
|
-
|
|
76
|
+
const dx = target.x - position.x;
|
|
77
|
+
const dy = target.y - position.y;
|
|
78
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
79
|
+
if (dist <= maxBlinkDistance) return target;
|
|
80
|
+
const scale = maxBlinkDistance / dist;
|
|
81
|
+
return {x: position.x + dx * scale, y: position.y + dy * scale};
|
|
54
82
|
}
|
|
55
83
|
|
|
56
|
-
|
|
57
|
-
|
|
84
|
+
if (enemy.health <= 0)
|
|
85
|
+
{
|
|
86
|
+
return idle();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const distance = distanceTo(position, enemy.position);
|
|
58
90
|
const urgentThreat = getMostUrgentThreat(threats);
|
|
59
91
|
|
|
60
92
|
// Erratic movement: mostly toward/away from enemy with random strafe
|
|
61
|
-
const deltaX = enemy.position.x -
|
|
62
|
-
const deltaY = enemy.position.y -
|
|
93
|
+
const deltaX = enemy.position.x - position.x;
|
|
94
|
+
const deltaY = enemy.position.y - position.y;
|
|
63
95
|
const normalizedDistance = distance > 0 ? distance : 1;
|
|
64
96
|
|
|
65
97
|
// Dodge if there's a real threat
|
|
66
|
-
const dodgeDirection = getDodgeDirectionForMovement(threats,
|
|
98
|
+
const dodgeDirection = getDodgeDirectionForMovement(threats, position);
|
|
67
99
|
|
|
68
100
|
let moveX: number;
|
|
69
101
|
let moveY: number;
|
|
70
102
|
|
|
71
|
-
if (dodgeDirection)
|
|
103
|
+
if (dodgeDirection)
|
|
72
104
|
{
|
|
73
105
|
// Actually dodge (Hogger isn't stupid)
|
|
74
|
-
moveX = dodgeDirection.x
|
|
75
|
-
moveY = dodgeDirection.y
|
|
106
|
+
moveX = dodgeDirection.x;
|
|
107
|
+
moveY = dodgeDirection.y;
|
|
76
108
|
}
|
|
77
|
-
else
|
|
109
|
+
else
|
|
78
110
|
{
|
|
79
111
|
// Random strafe component
|
|
80
112
|
const perpX = -deltaY / normalizedDistance;
|
|
81
113
|
const perpY = deltaX / normalizedDistance;
|
|
82
|
-
const strafeIntensity = random() *
|
|
114
|
+
const strafeIntensity = random() * 0.8 - 0.4; // -0.4 to 0.4
|
|
83
115
|
|
|
84
116
|
// Approach/retreat: prefer medium range (250-400), but sometimes just wander
|
|
85
|
-
let approachIntensity
|
|
86
|
-
if (distance > 400)
|
|
117
|
+
let approachIntensity: number;
|
|
118
|
+
if (distance > 400)
|
|
87
119
|
{
|
|
88
|
-
approachIntensity =
|
|
120
|
+
approachIntensity = 0.4 + random() * 0.3; // approach
|
|
89
121
|
}
|
|
90
|
-
else if (distance < 200)
|
|
122
|
+
else if (distance < 200)
|
|
91
123
|
{
|
|
92
|
-
approachIntensity = -(
|
|
124
|
+
approachIntensity = -(0.3 + random() * 0.3); // retreat
|
|
93
125
|
}
|
|
94
|
-
else
|
|
126
|
+
else
|
|
95
127
|
{
|
|
96
|
-
approachIntensity = random() *
|
|
128
|
+
approachIntensity = random() * 0.6 - 0.3; // random walk
|
|
97
129
|
}
|
|
98
130
|
|
|
131
|
+
// Approach the center-side of the enemy so knockback pushes toward center, not lava.
|
|
132
|
+
let approachX = deltaX / normalizedDistance;
|
|
133
|
+
let approachY = deltaY / normalizedDistance;
|
|
134
|
+
if (approachIntensity > 0)
|
|
135
|
+
{
|
|
136
|
+
const center = (ARENA_MIN + ARENA_MAX) / 2;
|
|
137
|
+
const toCenterX = center - enemy.position.x;
|
|
138
|
+
const toCenterY = center - enemy.position.y;
|
|
139
|
+
const toCenterDist = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY) || 1;
|
|
140
|
+
const adX = approachX + (toCenterX / toCenterDist) * centerBias;
|
|
141
|
+
const adY = approachY + (toCenterY / toCenterDist) * centerBias;
|
|
142
|
+
const adMag = Math.sqrt(adX * adX + adY * adY) || 1;
|
|
143
|
+
approachX = adX / adMag;
|
|
144
|
+
approachY = adY / adMag;
|
|
145
|
+
}
|
|
99
146
|
moveX =
|
|
100
147
|
perpX * strafeIntensity +
|
|
101
|
-
|
|
148
|
+
approachX * approachIntensity;
|
|
102
149
|
moveY =
|
|
103
150
|
perpY * strafeIntensity +
|
|
104
|
-
|
|
151
|
+
approachY * approachIntensity;
|
|
105
152
|
|
|
106
153
|
// Wall avoidance (Hogger doesn't like corners)
|
|
107
|
-
if (
|
|
108
|
-
if (
|
|
109
|
-
if (
|
|
110
|
-
if (
|
|
154
|
+
if (position.x < ARENA_MIN + WALL_BUFFER) moveX += WALL_PUSH;
|
|
155
|
+
if (position.x > ARENA_MAX - WALL_BUFFER) moveX -= WALL_PUSH;
|
|
156
|
+
if (position.y < ARENA_MIN + WALL_BUFFER) moveY += WALL_PUSH;
|
|
157
|
+
if (position.y > ARENA_MAX - WALL_BUFFER) moveY -= WALL_PUSH;
|
|
111
158
|
}
|
|
112
159
|
|
|
113
160
|
const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
|
|
114
|
-
if (magnitude >
|
|
161
|
+
if (magnitude > 1)
|
|
115
162
|
{
|
|
116
|
-
moveX =
|
|
117
|
-
moveY =
|
|
163
|
+
moveX = moveX / magnitude;
|
|
164
|
+
moveY = moveY / magnitude;
|
|
118
165
|
}
|
|
119
166
|
|
|
120
167
|
const move = {x: moveX, y: moveY};
|
|
168
|
+
const safeMove = avoidLava(position, move);
|
|
121
169
|
|
|
122
170
|
// === DEFENSE: Shield if in real danger ===
|
|
123
|
-
if (
|
|
171
|
+
if (status === 'channeling')
|
|
124
172
|
{
|
|
125
|
-
if (!urgentThreat || urgentThreat.ticksToImpact > 120)
|
|
173
|
+
if (!urgentThreat || urgentThreat.ticksToImpact > 120)
|
|
126
174
|
{
|
|
127
|
-
return
|
|
175
|
+
return cancel().move(safeMove.x, safeMove.y);
|
|
128
176
|
}
|
|
129
|
-
return
|
|
177
|
+
return idle();
|
|
130
178
|
}
|
|
131
179
|
|
|
132
180
|
if (
|
|
133
|
-
|
|
181
|
+
status === 'idle' &&
|
|
134
182
|
urgentThreat &&
|
|
135
183
|
!urgentThreat.bestDodgeDirection
|
|
136
|
-
)
|
|
184
|
+
)
|
|
137
185
|
{
|
|
138
|
-
if (urgentThreat.canBlockInTime && urgentThreat.ticksToStartShield <= 3)
|
|
186
|
+
if (urgentThreat.canBlockInTime && urgentThreat.ticksToStartShield <= 3)
|
|
139
187
|
{
|
|
140
|
-
return
|
|
141
|
-
move: {x: 0, y: 0},
|
|
142
|
-
startCast: {spell: 'shield'},
|
|
143
|
-
};
|
|
188
|
+
return shield();
|
|
144
189
|
}
|
|
145
|
-
else if (!urgentThreat.canBlockInTime &&
|
|
190
|
+
else if (!urgentThreat.canBlockInTime && blinkCooldown === 0)
|
|
146
191
|
{
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
|
|
150
|
-
};
|
|
192
|
+
const blinkTarget = capBlinkDistance(getBlinkToCenter(position));
|
|
193
|
+
return blink(blinkTarget.x, blinkTarget.y);
|
|
151
194
|
}
|
|
152
195
|
}
|
|
153
196
|
|
|
154
197
|
// === OFFENSE: Random blink (sometimes offensive, sometimes random) ===
|
|
155
|
-
if (
|
|
198
|
+
if (status === 'idle' && blinkCooldown === 0 && random() < 0.03)
|
|
156
199
|
{
|
|
157
200
|
// 60% chance blink toward enemy, 40% chance random blink
|
|
158
|
-
if (random() < 0.6 && distance > 300)
|
|
201
|
+
if (random() < 0.6 && distance > 300)
|
|
159
202
|
{
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
startCast: {spell: 'blink', target: enemy.position},
|
|
163
|
-
};
|
|
203
|
+
const capped = capBlinkDistance(enemy.position);
|
|
204
|
+
return blink(capped.x, capped.y);
|
|
164
205
|
}
|
|
165
|
-
else
|
|
206
|
+
else
|
|
166
207
|
{
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
x: 100 + random() * 600,
|
|
173
|
-
y: 100 + random() * 600,
|
|
174
|
-
},
|
|
175
|
-
},
|
|
176
|
-
};
|
|
208
|
+
const capped = capBlinkDistance({
|
|
209
|
+
x: 100 + random() * 600,
|
|
210
|
+
y: 100 + random() * 600,
|
|
211
|
+
});
|
|
212
|
+
return blink(capped.x, capped.y);
|
|
177
213
|
}
|
|
178
214
|
}
|
|
179
215
|
|
|
180
216
|
// === OFFENSE: Chaotic missiles ===
|
|
181
|
-
if (
|
|
217
|
+
if (status === 'idle' && distance < 500)
|
|
182
218
|
{
|
|
183
219
|
// Random missile config each time
|
|
184
220
|
const roll = random();
|
|
@@ -187,7 +223,7 @@ export const Hogger: WizardFunction = ({state, random}) =>
|
|
|
187
223
|
let turnRate: number;
|
|
188
224
|
let duration: number;
|
|
189
225
|
|
|
190
|
-
if (roll < 0.3)
|
|
226
|
+
if (roll < 0.3)
|
|
191
227
|
{
|
|
192
228
|
// Fast snipe
|
|
193
229
|
damage = 8 + Math.floor(random() * 5); // 8-12
|
|
@@ -195,7 +231,7 @@ export const Hogger: WizardFunction = ({state, random}) =>
|
|
|
195
231
|
turnRate = 0;
|
|
196
232
|
duration = 80 + Math.floor(random() * 40); // 80-120
|
|
197
233
|
}
|
|
198
|
-
else if (roll < 0.7)
|
|
234
|
+
else if (roll < 0.7)
|
|
199
235
|
{
|
|
200
236
|
// Homing tracker
|
|
201
237
|
damage = 7 + Math.floor(random() * 5); // 7-11
|
|
@@ -203,7 +239,7 @@ export const Hogger: WizardFunction = ({state, random}) =>
|
|
|
203
239
|
turnRate = 1 + random() * 2; // 1-3
|
|
204
240
|
duration = 150 + Math.floor(random() * 100); // 150-250
|
|
205
241
|
}
|
|
206
|
-
else
|
|
242
|
+
else
|
|
207
243
|
{
|
|
208
244
|
// Big slow bomb
|
|
209
245
|
damage = 12 + Math.floor(random() * 4); // 12-15
|
|
@@ -212,17 +248,12 @@ export const Hogger: WizardFunction = ({state, random}) =>
|
|
|
212
248
|
duration = 200 + Math.floor(random() * 100); // 200-300
|
|
213
249
|
}
|
|
214
250
|
|
|
215
|
-
return
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
missileAI: ChaoticHoming,
|
|
221
|
-
direction:
|
|
222
|
-
angleTo(state.position, enemy.position) + (random() * 20 - 10), // ±10° aim wobble
|
|
223
|
-
},
|
|
224
|
-
};
|
|
251
|
+
return missile(
|
|
252
|
+
{damage, speed, turnRate, duration},
|
|
253
|
+
ChaoticHoming,
|
|
254
|
+
angleTo(position, enemy.position) + (random() * 20 - 10), // ±10° aim wobble
|
|
255
|
+
).move(safeMove.x, safeMove.y);
|
|
225
256
|
}
|
|
226
257
|
|
|
227
|
-
return
|
|
228
|
-
}
|
|
258
|
+
return moveAction(safeMove.x, safeMove.y);
|
|
259
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type {FinalAction} from '../../hooks/index.js';
|
|
2
|
+
import {usePosition, useEnemy, useStatus, idle, missile, flyStraight} from '../../hooks/index.js';
|
|
2
3
|
import {angleTo} from '../../utils/angles.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -18,33 +19,30 @@ import {angleTo} from '../../utils/angles.js';
|
|
|
18
19
|
* STANDALONE — no tier progression. Rookies either learn to play a real class
|
|
19
20
|
* or quit. This bot represents the rock-bottom of "at least it shoots."
|
|
20
21
|
*/
|
|
21
|
-
export
|
|
22
|
+
export function Rookie(): FinalAction
|
|
22
23
|
{
|
|
23
|
-
const
|
|
24
|
-
|
|
24
|
+
const position = usePosition();
|
|
25
|
+
const enemy = useEnemy();
|
|
26
|
+
const status = useStatus();
|
|
27
|
+
|
|
28
|
+
if (enemy.health <= 0)
|
|
25
29
|
{
|
|
26
|
-
return
|
|
30
|
+
return idle();
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
if (
|
|
33
|
+
if (status === 'idle')
|
|
30
34
|
{
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
speed: 5,
|
|
38
|
-
turnRate: 0,
|
|
39
|
-
duration: 200,
|
|
40
|
-
},
|
|
41
|
-
missileAI: () => ({}),
|
|
42
|
-
direction: angleTo(state.position, enemy.position),
|
|
35
|
+
return missile(
|
|
36
|
+
{
|
|
37
|
+
damage: 10,
|
|
38
|
+
speed: 5,
|
|
39
|
+
turnRate: 0,
|
|
40
|
+
duration: 200,
|
|
43
41
|
},
|
|
44
|
-
|
|
42
|
+
() => flyStraight(),
|
|
43
|
+
angleTo(position, enemy.position),
|
|
44
|
+
).move(0, 0);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
return
|
|
48
|
-
|
|
49
|
-
};
|
|
50
|
-
};
|
|
47
|
+
return idle();
|
|
48
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type {FinalAction} from '../../hooks/index.js';
|
|
2
|
+
import {idle} from '../../hooks/index.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Bot: TargetDummy
|
|
@@ -13,9 +14,7 @@ import {WizardFunction} from '../../types.js';
|
|
|
13
14
|
*
|
|
14
15
|
* STANDALONE — no tier progression. It's a test fixture, not a combatant.
|
|
15
16
|
*/
|
|
16
|
-
export
|
|
17
|
+
export function TargetDummy(): FinalAction
|
|
17
18
|
{
|
|
18
|
-
return
|
|
19
|
-
|
|
20
|
-
};
|
|
21
|
-
};
|
|
19
|
+
return idle();
|
|
20
|
+
}
|