From a21443a69b2059220f155c2bea399017e30319ce Mon Sep 17 00:00:00 2001
From: Naruni <Naruni@crazynomad.org>
Date: Wed, 11 Sep 2013 19:58:38 -0700
Subject: [PATCH] This patch adds 3 options to init.txt:

Automagic_enabled (Default: disabled)
When the player presses tab (or whatever is bound to hit_closest), if automagic
is enabled and there is a spell in slot "a" automagic will attempt to attack by
spell.

Set automagic_stop (Default: 30)
If the player's magic points are below this number (percentage of total), this
prevents casting. What happens is dependent on behavior setting.

Set automagic_fight (Default: false)
If automagic cannot attack by magic, a message will be printed (not enough magic
or magic below threshold) by default. If this is true, a melee attack will be
called.

There are measures to catch any unintended keypresses being sent. Out of magic
and no target are handled. I can't think of any other situations that could
cause the sendkeys command to actually happen under false situations.

Added some more error handling and specified the public lua function.

Fixed which keys can be assigned.

Added the ability to toggle in game by using am_set_spell macro. Removed
net/caught check. Moved hp threshold to hit_closest function.

Changed AUTOMAGIC_ENABLE to AUTOMAGIC_ACTIVE for clarity.

Put some description in options guide, and tweaked allow move attacks.

Fix the range calculation

Now is range^2+1
---
 crawl-ref/docs/options_guide.txt        |  15 ++
 crawl-ref/settings/init.txt             |   3 +
 crawl-ref/source/dat/clua/autofight.lua |  22 ++-
 crawl-ref/source/dat/clua/automagic.lua | 275 ++++++++++++++++++++++++++++++++
 crawl-ref/source/initfile.cc            |   1 +
 5 files changed, 312 insertions(+), 4 deletions(-)
 create mode 100644 crawl-ref/source/dat/clua/automagic.lua

diff --git a/crawl-ref/docs/options_guide.txt b/crawl-ref/docs/options_guide.txt
index ddd8310..19c7210 100644
--- a/crawl-ref/docs/options_guide.txt
+++ b/crawl-ref/docs/options_guide.txt
@@ -1258,6 +1258,21 @@ autofight_caught = false
         If true, autofight will attempt to escape webs or nets that are
         holding you, even if no enemies are in view.
 
+automagic_enable = false
+	If true, the autofight function will resort to casting an assigned
+	spell at a target. Default spell slot is a. Can be toggled in game
+	by assigning am_set_spell to a macro.
+
+automagic_fight = false
+	Setting this to true causes melee combat when trying to cast a spell
+	that you either do not have enough magic points for, or are below
+	the set threshold. If false, a message will appear and no further
+	action is taken.
+
+automagic_stop = 30
+	Similar to autofight_stop, if magic points are below this number
+	(percentage of total), casting is prevented. What happens depends
+	on automagic_fight setting.
 
 3-i     Messages and Display Enhancements.
 ------------------------------------------
diff --git a/crawl-ref/settings/init.txt b/crawl-ref/settings/init.txt
index 6cfbf5f..524fcc9 100644
--- a/crawl-ref/settings/init.txt
+++ b/crawl-ref/settings/init.txt
@@ -194,6 +194,9 @@
 # sort_menus = inv: true : equipped, freshness, charged
 
 # autofight_stop = 30
+# automagic_enable = false
+# automagic_stop = 0
+# automagic_fight = false
 
 ##### 3-i       Messages and Display Enhancements #####
 #
diff --git a/crawl-ref/source/dat/clua/autofight.lua b/crawl-ref/source/dat/clua/autofight.lua
index 293b56e..8875f10 100644
--- a/crawl-ref/source/dat/clua/autofight.lua
+++ b/crawl-ref/source/dat/clua/autofight.lua
@@ -19,6 +19,7 @@ AUTOFIGHT_CAUGHT = false
 AUTOFIGHT_THROW = false
 AUTOFIGHT_THROW_NOMOVE = true
 AUTOFIGHT_FIRE_STOP = false
+AUTOMAGIC_ACTIVE = false
 
 local function delta_to_vi(dx, dy)
   local d2v = {
@@ -247,6 +248,10 @@ local function set_af_fire_stop(key, value, mode)
   AUTOFIGHT_FIRE_STOP = string.lower(value) ~= "false"
 end
 
+function set_automagic(key, value, mode)
+  AUTOMAGIC_ACTIVE = string.lower(value) ~= "false"
+end
+
 local function hp_is_low()
   local hp, mhp = you.hp()
   return (100*hp <= AUTOFIGHT_STOP*mhp)
@@ -263,9 +268,6 @@ function attack(allow_movement)
     else
       crawl.mpr("You are " .. caught .. "!")
     end
-  elseif caught then
-  elseif hp_is_low() then
-    crawl.mpr("You are too injured to fight recklessly!")
   elseif info == nil then
     crawl.mpr("No target in view!")
   elseif info.attack_type == 3 then
@@ -286,10 +288,21 @@ function attack(allow_movement)
 end
 
 function hit_closest()
-  attack(true)
+  if hp_is_low() then
+    crawl.mpr("You are too injured to fight recklessly!")
+  elseif AUTOMAGIC_ACTIVE and you.spell_table()[AUTOMAGIC_SPELL_SLOT] then
+    mag_attack(true)
+  else
+    attack(true)
+  end
 end
 
 function hit_adjacent()
+  if hp_is_low() then
+    crawl.mpr("You are too injured to fight recklessly!")
+  elseif AUTOMAGIC_ACTIVE and you.spell_table()[AUTOMAGIC_SPELL_SLOT] then
+    mag_attack(false)
+  else
   attack(false)
 end
 
@@ -303,3 +316,4 @@ chk_lua_option.autofight_caught = set_af_caught
 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.automagic_enable = set_automagic
diff --git a/crawl-ref/source/dat/clua/automagic.lua b/crawl-ref/source/dat/clua/automagic.lua
new file mode 100644
index 0000000..b3e635f
--- /dev/null
+++ b/crawl-ref/source/dat/clua/automagic.lua
@@ -0,0 +1,275 @@
+---------------------------------------------------------------------------
+-- automagic.lua:
+-- One-key casting.
+--
+--
+-- This uses the "very incomplete client monster and view bindings" from
+-- autofight.lua, and "is currently very primitive."
+--
+-- The behavior with target selection is kind of quirky, especially with
+-- short range spells and dangerous monsters out of range.
+---------------------------------------------------------------------------
+
+local ATT_HOSTILE = 0
+local ATT_NEUTRAL = 1
+
+AUTOMAGIC_SPELL_SLOT = "a"
+AUTOMAGIC_STOP = 0
+AUTOMAGIC_FIGHT = false
+
+local function delta_to_vi(dx, dy)
+  local d2v = {
+    [-1] = { [-1] = 'y', [0] = 'h', [1] = 'b'},
+    [0]  = { [-1] = 'k',            [1] = 'j'},
+    [1]  = { [-1] = 'u', [0] = 'l', [1] = 'n'},
+  }
+  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(dx, dy)
+  local str = ''
+  for i = 1,abs(dx) do
+    str = str .. delta_to_vi(sign(dx), 0)
+  end
+  for i = 1,abs(dy) do
+    str = str .. delta_to_vi(0, sign(dy))
+  end
+  return str
+end
+
+local function try_move(dx, dy)
+  m = monster.get_monster_at(dx, dy)
+  -- attitude > ATT_NEUTRAL should mean you can push past the monster
+  if view.is_safe_square(dx, dy) and (not m or m:attitude() > ATT_NEUTRAL) then
+    return delta_to_vi(dx, dy)
+  else
+    return nil
+  end
+end
+
+local function move_towards(dx, dy)
+  local move = nil
+  if abs(dx) > abs(dy) then
+    if abs(dy) == 1 then
+      move = try_move(sign(dx), 0)
+    end
+    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
+    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
+    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
+  if move == nil then
+    crawl.mpr("Failed to move towards target.")
+  else
+    crawl.process_keys(move)
+  end
+end
+
+local function get_monster_info(dx,dy,no_move)
+  m = monster.get_monster_at(dx,dy)
+  if not m then
+    return nil
+  end
+
+  info = {}
+
+  -- Decide what to do for target's range by circleLOS range
+  if (dx^2 + dy^2 <= spells.range(you.spell_table()[AUTOMAGIC_SPELL_SLOT])^2+1) then
+    -- In range
+    info.attack_type = 1
+  else
+    -- Out of range
+    info.attack_type = 2
+  end
+  return info
+end
+
+local function is_candidate_for_attack(x,y)
+  m = monster.get_monster_at(x, y)
+  --if m then crawl.mpr("Checking: (" .. x .. "," .. y .. ") " .. m:name()) end
+  if not m or m:attitude() ~= ATT_HOSTILE 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
+  return true
+end
+
+local function get_target(no_move)
+  local x, y, bestx, besty, best_info, new_info
+  bestx = 0
+  besty = 0
+  best_info = nil
+  for x = -8,8 do
+    for y = -8,8 do
+      if is_candidate_for_attack(x, y) then
+        new_info = get_monster_info(x, y, no_move)
+        if (not best_info) then
+          bestx = x
+          besty = y
+          best_info = new_info
+        end
+      end
+    end
+  end
+  return bestx, besty, best_info
+end
+
+local function spell_attack(x,y)
+  -- There has already been a valid target check to have gotten this far.
+  -- Required magic points have also been checked.
+  -- Spells that could hurt you (clouds, fireball) will still trigger "are you
+  -- sure" so that safeguard is not bypassed.
+  move = 'z' .. AUTOMAGIC_SPELL_SLOT .. 'r' .. vector_move(x, y) .. '.'
+  crawl.process_keys(move)
+end
+
+local function set_stop_level(key, value, mode)
+  AUTOMAGIC_STOP = tonumber(value)
+end
+
+local function set_fight_behavior(key, value, mode)
+  AUTOMAGIC_FIGHT = string.lower(value) ~= "false"
+end
+
+local function getkey()
+  local key
+  while true do
+    key = crawl.getch()
+    if key == 13 then
+      return "null"
+    end
+    if key == 27 then
+      return "escape"
+    end
+    -- Similar check to libutil.h isaalpha(int c) for valid key
+    if (key > 96 and key < 122) or (key > 64 and key < 91) then
+      local c = string.char(key)
+      return c
+    else
+      return "invalid"
+    end
+  end
+end
+
+local function mp_is_low()
+  local mp, mmp = you.mp()
+  return (100*mp <= AUTOMAGIC_STOP*mmp)
+end
+
+function mag_attack(allow_movement)
+  local x, y, info = get_target(not allow_movement)
+  if info == nil then
+    crawl.mpr("No target in view!")
+  elseif you.confused() then
+    crawl.mpr("You are too confused!")
+  elseif spells.mana_cost(you.spell_table()[AUTOMAGIC_SPELL_SLOT]) > you.mp() then
+    -- If you want to resort to melee, set AUTOMAGIC_FIGHT to true in rc
+    -- First check for enough magic points, then check if below threshold
+    if AUTOMAGIC_FIGHT then
+      attack(true)
+    else
+      crawl.mpr("You don't have enough magic to cast " ..
+          you.spell_table()[AUTOMAGIC_SPELL_SLOT] .. "!")
+    end
+  elseif mp_is_low() then
+    if AUTOMAGIC_FIGHT then
+      attack(true)
+    else
+      crawl.mpr("You are too depleted to cast spells recklessly!")
+    end
+  elseif info.attack_type == 2 then
+    move_towards(x,y)
+  elseif info.attack_type == 1 then
+    spell_attack(x,y)
+  elseif allow_movement then
+    move_towards(x,y)
+  else
+    crawl.mpr("No target in range!")
+  end
+end
+
+-- Set this as a macro to change which spell is cast, in game!
+function am_set_spell()
+  crawl.mpr("Which spell slot to assign automagic? (Enter to disable, Esc to cancel)", "prompt")
+  local slot = getkey()
+  crawl.mesclr()
+
+  if slot == "escape" then
+    crawl.mpr("Cancelled.")
+    return false
+
+  elseif slot == "null" then
+    crawl.mpr("Deactivated automagic.")
+    crawl.setopt("automagic_enable = false")
+    return false
+
+  elseif slot == "invalid" then
+    crawl.mpr("Invalid spell slot.")
+    return false
+
+  elseif not you.spell_table()[slot] then
+    crawl.mpr("No spell in slot " .. slot .. ".")
+    return false
+
+  else
+    message = ""
+    -- All conditional checks completed, continue to assign a slot now. If automagic
+    -- had been disabled, enable it now.
+    if AUTOMAGIC_ACTIVE == false then
+      crawl.setopt("automagic_enable = true")
+      message = " enabled,"
+    end
+    AUTOMAGIC_SPELL_SLOT = slot
+    crawl.mpr("Automagic" .. message .. " will cast spell in slot " .. slot .. " (" ..
+        you.spell_table()[AUTOMAGIC_SPELL_SLOT] .. ")" .. ".")
+    return false
+
+  end
+end
+
+chk_lua_option.automagic_stop = set_stop_level
+chk_lua_option.automagic_fight = set_fight_behavior
diff --git a/crawl-ref/source/initfile.cc b/crawl-ref/source/initfile.cc
index f8f2982..20c681c 100644
--- a/crawl-ref/source/initfile.cc
+++ b/crawl-ref/source/initfile.cc
@@ -1416,6 +1416,7 @@ static const char* lua_builtins[] =
     "clua/gearset.lua",
     "clua/trapwalk.lua",
     "clua/autofight.lua",
+    "clua/automagic.lua",
     "clua/kills.lua",
 };
 
-- 
1.8.4

