tactical.lua
14.5 KB
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
-- ToME - Tales of Maj'Eyal
-- Copyright (C) 2009, 2010, 2011 Nicolas Casalini
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <http://www.gnu.org/licenses/>.
--
-- Nicolas Casalini "DarkGod"
-- darkgod@te4.org
--local print = function() end
-- Internal functions
local checkLOS = function(sx, sy, tx, ty)
what = what or "block_sight"
local l = line.new(sx, sy, tx, ty)
local lx, ly = l()
while lx and ly do
if game.level.map:checkAllEntities(lx, ly, what) then break end
lx, ly = l()
end
-- Ok if we are at the end reset lx and ly for the next code
if not lx and not ly then lx, ly = x, y end
if lx == x and ly == y then return true, lx, ly end
return false, lx, ly
end
local canFleeDmapKeepLos = function(self)
if self.ai_target.actor then
local act = self.ai_target.actor
local c = act:distanceMap(self.x, self.y)
if not c then return end
local dir
for i = 1, 9 do
local sx, sy = util.coordAddDir(self.x, self.y, i)
-- Check LOS first
if checkLOS(sx, sy, act.x, act.y) then
local cd = act:distanceMap(sx, sy)
-- print("looking for dmap", dir, i, "::", c, cd)
if not cd or (c and (cd < c and self:canMove(sx, sy))) then c = cd; dir = i end
end
end
if dir then
return true, self.x+dir_to_coord[dir][1], self.y+dir_to_coord[dir][2]
else
return false
end
end
end
newAI("use_tactical", function(self)
-- Find available talents
print("============================== TACTICAL AI", self.name)
local avail = {}
local ok = false
local target_dist = self.ai_target.actor and math.floor(core.fov.distance(self.x, self.y, self.ai_target.actor.x, self.ai_target.actor.y))
local hate = self.ai_target.actor and (self:reactionToward(self.ai_target.actor) < 0)
local has_los = self.ai_target.actor and self:hasLOS(self.ai_target.actor.x, self.ai_target.actor.y)
local self_compassion = (self.ai_state.self_compassion == false and 0) or self.ai_state.self_compassion or 5
local ally_compassion = (self.ai_state.ally_compassion == false and 0) or self.ai_state.ally_compassion or 1
for tid, lvl in pairs(self.talents) do
local t = self:getTalentFromId(tid)
local t_avail = false
print(self.name, self.uid, "tactical ai talents testing", t.name, tid)
if t.tactical then
local tg = self:getTalentTarget(t)
local default_tg = {type=util.getval(t.direct_hit, self, t) and "hit" or "bolt"}
-- Only assume range... some talents may no require LOS, etc
local within_range = target_dist and target_dist <= (self:getTalentRange(t) + self:getTalentRadius(t))
if t.mode == "activated" and not t.no_npc_use and
not self:isTalentCoolingDown(t) and self:preUseTalent(t, true, true) and
(not self:getTalentRequiresTarget(t) or within_range)
then
t_avail = true
elseif t.mode == "sustained" and not t.no_npc_use and not self:isTalentCoolingDown(t) and
not self:isTalentActive(t.id) and
self:preUseTalent(t, true, true)
then
t_avail = true
end
if t_avail then
-- Project the talent if possible, counting foes and allies hit
local foes_hit = {}
local allies_hit = {}
local self_hit = {}
local typ = engine.Target:getType(tg or default_tg)
if tg or self:getTalentRequiresTarget(t) then
local target_actor = self.ai_target.actor or self
self:project(typ, target_actor.x, target_actor.y, function(px, py)
local act = game.level.map(px, py, engine.Map.ACTOR)
if act and not act.dead then
if self:reactionToward(act) < 0 then
print("[DEBUG] hit a foe!")
foes_hit[#foes_hit+1] = act
elseif (typ.selffire) and (act == self) then
print("[DEBUG] hit self!")
self_hit[#self_hit+1] = act
elseif typ.friendlyfire then
print("[DEBUG] hit an ally!")
allies_hit[#allies_hit+1] = act
end
end
end)
end
-- Evaluate the tactical weights and weight functions
for tact, val in pairs(t.tactical) do
if type(val) == "function" then val = val(self, t, self.ai_target.actor) end
-- Handle damage_types and resistances
local nb_foes_hit, nb_allies_hit, nb_self_hit = 0, 0, 0
if type(val) == "table" then
for damtype, damweight in pairs(val) do
local pen = 0
if self.resists_pen then pen = (self.resists_pen.all or 0) + (self.resists_pen[damtype] or 0) end
for i, act in ipairs(foes_hit) do
local res = math.min((act.resists.all or 0) + (act.resists[damtype] or 0), (act.resists_cap.all or 0) + (act.resists_cap[damtype] or 0))
res = res * (100 - pen) / 100
nb_foes_hit = nb_foes_hit + damweight * (100 - res) / 100
end
for i, act in ipairs(self_hit) do
local res = math.min((act.resists.all or 0) + (act.resists[damtype] or 0), (act.resists_cap.all or 0) + (act.resists_cap[damtype] or 0))
res = res * (100 - pen) / 100
nb_self_hit = nb_self_hit + damweight * (100 - res) / 100 * (type(typ.selffire) == "number" and typ.selffire / 100 or 1)
end
for i, act in ipairs(allies_hit) do
local res = math.min((act.resists.all or 0) + (act.resists[damtype] or 0), (act.resists_cap.all or 0) + (act.resists_cap[damtype] or 0))
res = res * (100 - pen) / 100
nb_allies_hit = nb_allies_hit + damweight * (100 - res) / 100 * (type(typ.friendlyfire) == "number" and typ.friendlyfire / 100 or 1)
end
end
val = 1
-- Or assume no resistances
else
nb_foes_hit = #foes_hit
nb_self_hit = #self_hit * (type(typ.selffire) == "number" and typ.selffire / 100 or 1)
nb_allies_hit = #allies_hit * (type(typ.friendlyfire) == "number" and typ.friendlyfire / 100 or 1)
end
-- Use the player set ai_talents weights
val = val * (self.ai_talents and self.ai_talents[t.id] or 1) * (1 + lvl / 5)
-- Update the weight by the dummy projection data
-- Also force scaling if the talent requires a target (stand-in for canProject)
if self:getTalentRequiresTarget(t) or nb_foes_hit > 0 or nb_allies_hit > 0 or nb_self_hit > 0 then
val = val * (nb_foes_hit - ally_compassion * nb_allies_hit - self_compassion * nb_self_hit)
end
-- Only take values greater than 0... allows the ai_talents to turn talents off
if val > 0 then
if not avail[tact] then avail[tact] = {} end
-- Save the tactic, if the talent is instant it gets a huge bonus
-- Note the addition of a less than one random value, this means the sorting will randomly shift equal values
val = ((t.no_energy==true) and val * 10 or val) + rng.float(0, 0.9)
avail[tact][#avail[tact]+1] = {val=val, tid=tid, nb_foes_hit=nb_foes_hit, nb_allies_hit=nb_allies_hit, nb_self_hit=nb_self_hit}
print(self.name, self.uid, "tactical ai talents can use", t.name, tid, tact, "weight", avail[tact][#avail[tact]].val)
ok = true
end
end
end
end
end
if ok then
local want = {}
local need_heal = 0
local life = 100 * self.life / self.max_life
if life < 20 then need_heal = need_heal + 10 * self_compassion / 5
elseif life < 30 then need_heal = need_heal + 8 * self_compassion / 5
elseif life < 40 then need_heal = need_heal + 5 * self_compassion / 5
elseif life < 60 then need_heal = need_heal + 4 * self_compassion / 5
elseif life < 80 then need_heal = need_heal + 3 * self_compassion / 5
end
-- Need healing
if avail.heal then
want.heal = need_heal
end
-- Need mana
if avail.mana then
want.mana = 0
local mana = 100 * self.mana / self.max_mana
if mana < 20 then want.mana = want.mana + 4
elseif mana < 30 then want.mana = want.mana + 3
elseif mana < 40 then want.mana = want.mana + 2
elseif mana < 60 then want.mana = want.mana + 2
elseif mana < 80 then want.mana = want.mana + 1
elseif mana < 100 then want.mana = want.mana + 0.5
end
end
-- Need stamina
if avail.stamina then
want.stamina = 0
local stamina = 100 * self.stamina / self.max_stamina
if stamina < 20 then want.stamina = want.stamina + 4
elseif stamina < 30 then want.stamina = want.stamina + 3
elseif stamina < 40 then want.stamina = want.stamina + 2
elseif stamina < 60 then want.stamina = want.stamina + 2
elseif stamina < 80 then want.stamina = want.stamina + 1
elseif stamina < 100 then want.stamina = want.stamina + 0.5
end
end
-- Need vim
if avail.vim then
want.vim = 0
local vim = 100 * self.vim / self.max_vim
if vim < 20 then want.vim = want.vim + 4
elseif vim < 30 then want.vim = want.vim + 3
elseif vim < 40 then want.vim = want.vim + 2
elseif vim < 60 then want.vim = want.vim + 2
elseif vim < 80 then want.vim = want.vim + 1
elseif vim < 100 then want.vim = want.vim + 0.5
end
end
-- Need to reduce equilibrium
if avail.equilibrium then
want.equilibrium = 0
local _, failure_chance = self:equilibriumChance()
if failure_chance > 10 then want.equilibrium = want.equilibrium + 0.5
elseif failure_chance > 20 then want.equilibrium = want.equilibrium + 1
elseif failure_chance > 50 then want.equilibrium = want.equilibrium + 2
elseif failure_chance > 60 then want.equilibrium = want.equilibrium + 4
elseif failure_chance > 80 then want.equilibrium = want.equilibrium + 6
end
end
-- Need to reduce paradox
if avail.paradox then
want.paradox = 0
local _, failure_chance = self:paradoxFailChance()
if failure_chance > 10 then want.paradox = want.paradox + 0.5
elseif failure_chance > 20 then want.paradox = want.paradox + 1
elseif failure_chance > 50 then want.paradox = want.paradox + 2
elseif failure_chance > 60 then want.paradox = want.paradox + 4
elseif failure_chance > 80 then want.paradox = want.paradox + 6
end
end
-- Summoner needs protection
if avail.protect and self.summoner then
want.protect = 0
local life = 100 * self.summoner.life / self.summoner.max_life
if life < 20 then want.protect = want.protect + 10 * ally_compassion
elseif life < 30 then want.protect = want.protect + 8 * ally_compassion
elseif life < 40 then want.protect = want.protect + 5 * ally_compassion
elseif life < 60 then want.protect = want.protect + 4 * ally_compassion
elseif life < 80 then want.protect = want.protect + 3 * ally_compassion
end
end
-- Need closing-in
if avail.closein and target_dist and target_dist > 2 and self.ai_tactic.closein then
want.closein = 1 + target_dist / 2
end
-- Need escaping... allow escaping even if there isn't a talent so we can flee
if target_dist and (avail.escape or canFleeDmapKeepLos(self)) then
want.escape = need_heal / 2
if self.ai_tactic.safe_range and target_dist < self.ai_tactic.safe_range then want.escape = want.escape + self.ai_tactic.safe_range / 2 end
end
-- Surrounded
local nb_foes_seen = 0
local nb_allies_seen = 0
local arr = self.fov.actors_dist
local act
local sqsense = 2 * 2
for i = 1, #arr do
act = self.fov.actors_dist[i]
if act and not act.dead and self.fov.actors[act] and self.fov.actors[act].sqdist <= sqsense then
if self:reactionToward(act) < 0 then
nb_foes_seen = nb_foes_seen + 1
else
nb_allies_seen = nb_allies_seen + 1
end
end
end
if avail.surrounded then
want.surrounded = nb_foes_seen
end
-- Need defence
if avail.defend and need_heal and nb_foes_seen > 0 then
table.sort(avail.defend, function(a,b) return a.val > b.val end)
want.defend = 1 + need_heal / 2 + nb_foes_seen * 0.5
end
-- Attacks
if avail.attack and self.ai_target.actor then
-- Use the foe/ally ratio from the best attack talent
table.sort(avail.attack, function(a,b) return a.val > b.val end)
want.attack = avail.attack[1].val
end
if avail.disable and self.ai_target.actor then
-- Use the foe/ally ratio from the best disable talent
table.sort(avail.disable, function(a,b) return a.val > b.val end)
want.disable = (want.attack or 0) + avail.disable[1].val
end
if avail.attackarea and self.ai_target.actor then
-- Use the foe/ally ratio from the best attackarea talent
table.sort(avail.attackarea, function(a,b) return a.val > b.val end)
want.attackarea = avail.attackarea[1].val
end
-- Need buffs
if avail.buff and want.attack and want.attack > 0 then
want.buff = math.max(0.01, want.attack + 0.5)
end
print("Tactical ai report for", self.name)
local res = {}
for k, v in pairs(want) do
if v > 0 then
-- Randomize and multiply by the ai_tactic weights (if any)
v = (v + v + rng.float(0, 0.9)) * (self.ai_tactic[k] or 1)
if v > 0 then
print(" * "..k, v, "(mult)", self.ai_tactic[k])
res[#res+1] = {k,v}
end
end
end
if #res == 0 then return end
table.sort(res, function(a,b) return a[2] > b[2] end)
local selected_talents = avail[res[1][1]]
if selected_talents then
table.sort(selected_talents, function(a,b) return a.val > b.val end)
local tid = selected_talents[1].tid
print("Tactical choice:", res[1][1], tid)
self:useTalent(tid)
return true
else
return nil, res[1][1]
end
end
end)
newAI("tactical", function(self)
local targeted = self:runAI(self.ai_state.ai_target or "target_simple")
-- Keep your distance
local special_move = false
if self.ai_tactic.safe_range and self.ai_target.actor and self:hasLOS(self.ai_target.actor.x, self.ai_target.actor.y) then
local target_dist = math.floor(core.fov.distance(self.x, self.y, self.ai_target.actor.x, self.ai_target.actor.y))
if self.ai_tactic.safe_range == target_dist then
special_move = "none"
elseif self.ai_tactic.safe_range > target_dist then
special_move = "flee_dmap_keep_los"
end
end
local used_talent, want
-- One in "talent_in" chance of using a talent
if (not self.ai_state.no_talents or self.ai_state.no_talents == 0) and rng.chance(self.ai_state.talent_in or 2) then
used_talent, want = self:runAI("use_tactical")
end
if want == "escape" then
special_move = "flee_dmap_keep_los"
end
if targeted and not self.energy.used then
if special_move then
return self:runAI(special_move)
else
return self:runAI(self.ai_state.ai_move or "move_simple")
end
end
return false
end)
newAI("flee_dmap_keep_los", function(self)
local can_flee, fx, fy = canFleeDmapKeepLos(self)
if can_flee then
return self:move(fx, fy)
end
end)