File: autofight.lua

package info (click to toggle)
crawl 2%3A0.34.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 100,188 kB
  • sloc: cpp: 363,709; ansic: 27,765; javascript: 9,516; python: 8,463; perl: 3,293; java: 3,132; xml: 2,380; makefile: 1,835; sh: 611; objc: 250; cs: 15; sed: 9; lisp: 3
file content (531 lines) | stat: -rw-r--r-- 16,322 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
---------------------------------------------------------------------------
-- autofight.lua:
-- One-key fighting.
--
-- To use these, it is *not* recommended to bind a key directly to the lua
-- functions, except for toggle_autothrow. Rather, bind a key to the associated
-- commands.
--
-- CMD_AUTOFIGHT (===hit_closest), tab on the default binding.
-- With autofight_throw = false: Attack with primary weapon, moving towards
--     enemy if needed. If the primary weapon is ranged, it will fire. Default.
-- With autofight_throw = true: Attack with primary weapon if within range.
--     Otherwise, fire quiver action if possible; only actions that do direct
--     damage will be considered. If no ranged attack is available, fall back on
--     movement. If a launcher is wielded, prioritize the launcher over the
--     quiver.
--
-- ===toggle_autothrow, not bound by default
-- toggle autofight_throw
--
-- CMD_AUTOFIRE (===hit_closest), shift-tab on the default binding
-- Fire the quiver action, including actions that don't do direct damage; never
-- moves.
--
-- CMD_AUTOFIGHT_NOMOVE (===hit_closest_nomove), not bound by default
-- Attack with primary weapon if within range, fire the quiver action if not;
-- never moves. Only fires quiver actions that do direct damage. If a launcher
-- is wielded, prioritize the launcher over the quiver.
--
-- This uses the very incomplete client monster and view bindings, and
-- is currently very primitive. Improvements welcome!
---------------------------------------------------------------------------

local ATT_HOSTILE = 0
local ATT_NEUTRAL = 1


AUTOFIGHT_STOP = 50
AUTOFIGHT_CAUGHT = false
AUTOFIGHT_THROW = false
AUTOFIGHT_THROW_NOMOVE = true
AUTOFIGHT_FIRE_STOP = false
AUTOFIGHT_WAIT = false
AUTOFIGHT_PROMPT_RANGE = true
AUTOMAGIC_ACTIVE = false
AUTOFIGHT_FORCE_FIRE = false

local function delta_to_cmd(dx, dy)
  local d2v = {
    [-1] = { [-1] = "CMD_MOVE_UP_LEFT",  [0] = "CMD_MOVE_LEFT",  [1] = "CMD_MOVE_DOWN_LEFT"},
    [0]  = { [-1] = "CMD_MOVE_UP",                               [1] = "CMD_MOVE_DOWN"},
    [1]  = { [-1] = "CMD_MOVE_UP_RIGHT", [0] = "CMD_MOVE_RIGHT", [1] = "CMD_MOVE_DOWN_RIGHT"},
  }
  return d2v[dx][dy]
end

local function target_delta_to_cmd(dx, dy)
  local d2v = {
    [-1] = { [-1] = "CMD_TARGET_UP_LEFT",  [0] = "CMD_TARGET_LEFT",  [1] = "CMD_TARGET_DOWN_LEFT"},
    [0]  = { [-1] = "CMD_TARGET_UP",                                 [1] = "CMD_TARGET_DOWN"},
    [1]  = { [-1] = "CMD_TARGET_UP_RIGHT", [0] = "CMD_TARGET_RIGHT", [1] = "CMD_TARGET_DOWN_RIGHT"},
  }
  return d2v[dx][dy]
end

local function sign(a)
  return a > 0 and 1 or a < 0 and -1 or 0
end

local function abs(a)
  return a * sign(a)
end

local function adjacent(dx, dy)
  return abs(dx) <= 1 and abs(dy) <= 1
end

local function vector_move(a, dx, dy)
  for i = 1,abs(dx) do
    a[#a+1] = target_delta_to_cmd(sign(dx), 0)
  end
  for i = 1,abs(dy) do
    a[#a+1] = target_delta_to_cmd(0, sign(dy))
  end
end

local function have_reaching()
  return you.reach_range() > 1
end

local function have_ranged()
  local wp = items.equipped_at("weapon")
  return wp and wp.is_ranged and not wp.is_melded
end

local function have_quiver_action(no_move)
    return (no_move and AUTOFIGHT_THROW_NOMOVE
            or not no_move and AUTOFIGHT_THROW)
        and you.quiver_valid(1) and you.quiver_enabled(1)
        -- TODO: armataur roll passes the following check, which may be
        -- counterintuitive for the nomove case.
        and you.quiver_allows_autofight()
        and (not you.quiver_uses_mp()
            or not AUTOMAGIC_FIGHT
            or not af_mp_is_low())
end

local function is_safe_square(dx, dy)
    if view.feature_at(dx, dy) == "trap_web" and not you.is_web_immune() then
        return false
    end
    return view.is_safe_square(dx, dy)
end

local function can_move_maybe(dx, dy)
  if view.feature_at(dx,dy) ~= "unseen" and view.is_safe_square(dx,dy) then
    local m = monster.get_monster_at(dx, dy)
    if not m or not m:is_firewood() then
      return true
    end
  end
  return false
end

local function can_move_now(dx, dy)
  local m = monster.get_monster_at(dx, dy)
  -- attitude > ATT_NEUTRAL should mean you can push past the monster
  return (is_safe_square(dx, dy) and (not m or m:attitude() > ATT_NEUTRAL))
end

local function choose_move_towards(ax, ay, bx, by, square_func)
  local los_radius = you.los()
  local move = nil
  local dx = bx - ax
  local dy = by - ay
  local function try_move(mx, my)
    if mx == 0 and my == 0 then
      return nil
    elseif abs(ax+mx) > los_radius or abs(ay+my) > los_radius then
      return nil
    elseif not view.cell_see_cell(ax + mx, ay + my, ax + dx, ay + dy) then
      -- we know that dx,dy is currently in view, so if there is a path at all,
      -- there must be a path where it remains in view.
      return nil
    elseif square_func(ax+mx, ay+my) then
      return {mx,my}
    else
      return nil
    end
  end
  if abs(dx) > abs(dy) then
    if abs(dy) == 1 then
      -- at distance one, there's no need to adjust y position
      move = try_move(sign(dx), 0)
    end
    -- first try diagonal. Not sure why? Also, sign(0)=0, so these two checks
    -- are equivalent in that case.
    if move == nil then move = try_move(sign(dx), sign(dy)) end
    if move == nil then move = try_move(sign(dx), 0) end
    if move == nil and abs(dx) > abs(dy)+1 then
      move = try_move(sign(dx), 1)
    end
    if move == nil and abs(dx) > abs(dy)+1 then
      move = try_move(sign(dx), -1)
    end
    if move == nil then move = try_move(0, sign(dy)) end
  elseif abs(dx) == abs(dy) then
    -- exact diagonal
    move = try_move(sign(dx), sign(dy))
    if move == nil then move = try_move(sign(dx), 0) end
    if move == nil then move = try_move(0, sign(dy)) end
  else
    -- symmetric case to first condition, i.e. abs(dy) > abs(dx)
    if abs(dx) == 1 then
      move = try_move(0, sign(dy))
    end
    if move == nil then move = try_move(sign(dx), sign(dy)) end
    if move == nil then move = try_move(0, sign(dy)) end
    if move == nil and abs(dy) > abs(dx)+1 then
      move = try_move(1, sign(dy))
    end
    if move == nil and abs(dy) > abs(dx)+1 then
      move = try_move(-1, sign(dy))
    end
    if move == nil then move = try_move(sign(dx), 0) end
  end
  return move
end

local function move_towards(dx, dy)
  local move = choose_move_towards(0, 0, dx, dy, can_move_now)
  if move == nil then
    crawl.mpr("Failed to move towards target.")
  elseif you.status("immotile") then
    if AUTOFIGHT_WAIT then
      crawl.do_commands({"CMD_WAIT"})
    else
      crawl.mpr("Failed to move towards target because you cannot move.")
    end
  else
      crawl.do_commands({delta_to_cmd(move[1],move[2])})
  end
end

local function will_tab(ax, ay, bx, by)
  local range = you.reach_range()
  if abs(bx-ax) <= range and abs(by-ay) <= range then
    return true
  end
  local move = choose_move_towards(ax, ay, bx, by, can_move_maybe)
  if move == nil then
    return false
  end
  return will_tab(ax+move[1], ay+move[2], bx, by)
end

-- attack types for get_monster_info return value
local AF_FAILS = -1
local AF_MOVES = 0    -- target not in range, can move towards it
local AF_REACHING = 1 -- target in range for a reaching attack + reaching available
local AF_MELEE = 2    -- target in melee range + melee attack available
local AF_FIRE = 3     -- target in fire range + ranged attack available

local function get_monster_info(dx,dy,no_move)
  m = monster.get_monster_at(dx,dy)
  name = m:name()
  if not m then
    return nil
  end
  info = {}
  info.distance = (abs(dx) > abs(dy)) and -abs(dx) or -abs(dy)
  if have_ranged() then
    info.attack_type = you.see_cell_no_trans(dx, dy) and AF_FIRE or AF_MOVES
  elseif not have_reaching() then
    info.attack_type = (-info.distance < 2) and AF_MELEE or AF_MOVES
  else
    local range = you.reach_range()
    -- Assume extended reach (i.e. Rift) gets smite targeting.
    local can_reach = range > 2 and you.see_cell_no_trans or view.can_reach
    if -info.distance > range then
      info.attack_type = AF_MOVES
    elseif -info.distance < 2 then
      info.attack_type = AF_MELEE
    else
      info.attack_type = can_reach(dx, dy) and AF_REACHING or AF_MOVES
    end
  end
  if info.attack_type == 0 and have_quiver_action(no_move) and you.see_cell_no_trans(dx, dy) then
    info.attack_type = AF_FIRE
  end
  if info.attack_type ~= AF_FIRE and AUTOFIGHT_FORCE_FIRE then
    -- firing can often be preempted by melee etc, but we have been called by
    -- CMD_AUTOFIRE, so force firing.
    -- TODO: refactor so that this is less hacky
    info.attack_type = you.see_cell_no_trans(dx,dy) and AF_FIRE or AF_FAILS
  end

  -- We can possibly move towards warded enemies, but not attack them while immune
  if m:is_damage_immune() and m:is("warding") then
    info.attack_type = AF_MOVES
  end

  if info.attack_type == AF_MOVES and not will_tab(0,0,dx,dy) then
    info.attack_type = AF_FAILS
  end
  info.can_attack = (info.attack_type > 0) and 1 or info.attack_type
  info.safe = m:is_safe() and -1 or 0
  info.constricting_you = m:is_constricting_you() and 1 or 0
  info.good_stab = you.has_good_stab() and m:stabbability() or 0
  info.poor_stab = (m:stabbability() >= 1) and 1 or 0
  info.injury = m:damage_level()
  info.threat = m:threat()
  info.orc_priest_wizard = (name == "orc priest" or name == "orc wizard") and 1 or 0
  info.bullseye_target = (info.attack_type == AF_FIRE and m:status("targeted by your dimensional bullseye")) and -1 or 0
  return info
end

local function compare_monster_info(m1, m2)
  flag_order = autofight_flag_order
  if flag_order == nil then
    flag_order = {"bullseye_target", "can_attack", "safe", "good_stab",
                  "distance", "constricting_you", "poor_stab", "injury",
                  "threat", "orc_priest_wizard"}
  end
  for i,flag in ipairs(flag_order) do
    if m1[flag] > m2[flag] then
      return true
    elseif m1[flag] < m2[flag] then
      return false
    end
  end
  return false
end

local function is_candidate_for_attack(x,y, no_move)
  m = monster.get_monster_at(x, y)
  --if m then crawl.mpr("Checking: (" .. x .. "," .. y .. ") " .. m:name()) end
  if not m then
    return false
  end
  if m:name() == "butterfly"
      or m:name() == "orb of destruction" then
    return false
  end
  if m:is_firewood() then
  --crawl.mpr("... is firewood.")
    if string.find(m:name(), "ballistomycete") then
      return true
    end
    return false
  end
  if m:is_damage_immune() and (no_move or not m:is("warding")) then
    return false
  end
  if m:attitude() == ATT_HOSTILE
      or m:attitude() == ATT_NEUTRAL and m:is("frenzied") then
    return true
  end
  return false
end

local function get_target(no_move)
  local los_radius = you.los()
  local x, y, bestx, besty, best_info, new_info
  bestx = 0
  besty = 0
  best_info = nil
  for x = -los_radius,los_radius do
    for y = -los_radius,los_radius do
      if is_candidate_for_attack(x, y, no_move) then
        new_info = get_monster_info(x, y, no_move)
        if (not best_info) or compare_monster_info(new_info, best_info) then
          bestx = x
          besty = y
          best_info = new_info
        end
      end
    end
  end
  return bestx, besty, best_info
end

local function attack_fire(x,y)
  if AUTOFIGHT_FORCE_FIRE or not have_ranged() then
    -- fire from quiver
    crawl.do_targeted_command("CMD_FIRE", x, y, AUTOFIGHT_FIRE_STOP)
  else
    -- fire a wielded launcher
    crawl.do_targeted_command("CMD_PRIMARY_ATTACK", x, y, AUTOFIGHT_FIRE_STOP)
  end
end

local function attack_reach(x,y)
  crawl.do_targeted_command("CMD_PRIMARY_ATTACK", x, y, true)
end

local function attack_melee(x,y)
  crawl.do_commands({delta_to_cmd(x, y)})
end

local function set_stop_level(key, value, mode)
  AUTOFIGHT_STOP = tonumber(value)
end

local function set_af_caught(key, value, mode)
  AUTOFIGHT_CAUGHT = string.lower(value) ~= "false"
end

local function set_af_throw(key, value, mode)
  AUTOFIGHT_THROW = string.lower(value) ~= "false"
end

local function set_af_throw_nomove(key, value, mode)
  AUTOFIGHT_THROW_NOMOVE = string.lower(value) ~= "false"
end

local function set_af_fire_stop(key, value, mode)
  AUTOFIGHT_FIRE_STOP = string.lower(value) ~= "false"
end

-- is there a better way to get lua options from c++ than defining a function?
function get_af_fire_stop()
  return AUTOFIGHT_FIRE_STOP
end

local function set_af_wait(key, value, mode)
    AUTOFIGHT_WAIT = string.lower(value) ~= "false"
end

local function set_af_prompt_range(key, value, mode)
    AUTOFIGHT_PROMPT_RANGE = string.lower(value) ~= "false"
end

function set_automagic(key, value, mode)
  AUTOMAGIC_ACTIVE = string.lower(value) ~= "false"
end

function af_hp_is_low()
  local hp, mhp = you.hp()
  return (100*hp <= AUTOFIGHT_STOP*mhp)
end

function af_mp_is_low()
  local mp, mmp
  if you.race() == "Djinni" then
    mp, mmp = you.hp()
  else
    mp, mmp = you.mp()
  end
  -- AUTOMAGIC_STOP is currently in automagic.lua
  return (100*mp <= AUTOMAGIC_STOP*mmp)
end

function autofight_check_preconditions(check_caught)
  local caught = you.caught()
  if af_hp_is_low() then
    crawl.mpr("You are too injured to fight recklessly!")
    return false
  elseif you.confused() then
    crawl.mpr("You are too confused!")
    return false
  elseif caught and check_caught and not AUTOFIGHT_CAUGHT then
    crawl.mpr("You are " .. caught .. "!")
    return false
  end
  return true
end

function attack(allow_movement, check_caught)
  local x, y, info = get_target(not allow_movement)
  if not autofight_check_preconditions(check_caught) then
    return
  end

  if check_caught and you.caught() then
    crawl.do_commands({delta_to_cmd(1, 0)}) -- Direction doesn't matter.
    return
  end

  if info == nil then
    if AUTOFIGHT_WAIT and not allow_movement then
      crawl.do_commands({"CMD_WAIT"})
    else
      crawl.mpr("No target in view!")
    end
  elseif info.attack_type == AF_FIRE then
    attack_fire(x,y)
  elseif info.attack_type == AF_MELEE then
    attack_melee(x,y)
  elseif info.attack_type == AF_REACHING then
    attack_reach(x,y)
  elseif info.attack_type == AF_FAILS then
    crawl.mpr("No reachable target in view!")
  elseif allow_movement then
    if not AUTOFIGHT_PROMPT_RANGE or you.quiver_valid(0) or crawl.weapon_check() then
      move_towards(x,y)
    end
  elseif AUTOFIGHT_WAIT then
    crawl.do_commands({"CMD_WAIT"})
  else
    crawl.mpr("No target in range!")
  end
end

function hit_closest()
  if AUTOMAGIC_ACTIVE and you.spell_table()[AUTOMAGIC_SPELL_SLOT] then
    mag_attack(true)
  else
    attack(true, true)
  end
end

function hit_closest_nomove()
  if AUTOMAGIC_ACTIVE and you.spell_table()[AUTOMAGIC_SPELL_SLOT] then
    mag_attack(false)
  else
    attack(false, true)
  end
end

function fire_closest()
  if AUTOMAGIC_ACTIVE and you.spell_table()[AUTOMAGIC_SPELL_SLOT] then
    mag_attack(false)
  else
    local old = AUTOFIGHT_FORCE_FIRE
    AUTOFIGHT_FORCE_FIRE = true
    attack(false, false)
    AUTOFIGHT_FORCE_FIRE = old
  end
end

function hit_nonmagic()
  attack(true, true)
end

function hit_nonmagic_nomove()
  attack(false, true)
end

function hit_magic()
  if you.spell_table()[AUTOMAGIC_SPELL_SLOT] then
    mag_attack(true)
  else
    crawl.mpr("No spell in slot " .. AUTOMAGIC_SPELL_SLOT .. "!")
  end
end

function hit_magic_nomove()
  if you.spell_table()[AUTOMAGIC_SPELL_SLOT] then
    mag_attack(false)
  else
    crawl.mpr("No spell in slot " .. AUTOMAGIC_SPELL_SLOT .. "!")
  end
end

function toggle_autothrow()
  AUTOFIGHT_THROW = not AUTOFIGHT_THROW
  crawl.mpr(AUTOFIGHT_THROW and "Enabling autothrow." or "Disabling autothrow.")
end

chk_lua_option.autofight_stop = set_stop_level
chk_lua_option.autofight_caught = set_af_caught
chk_lua_option.autofight_fires = set_af_throw
chk_lua_option.autofight_nomove_fires = set_af_throw_nomove
-- the following two options are here for backwards compatibility
chk_lua_option.autofight_throw = set_af_throw
chk_lua_option.autofight_throw_nomove = set_af_throw_nomove
chk_lua_option.autofight_fire_stop = set_af_fire_stop
chk_lua_option.autofight_wait = set_af_wait
chk_lua_option.autofight_prompt_range = set_af_prompt_range
chk_lua_option.automagic_enable = set_automagic