Forked from
tome / Tales of MajEyal
527 commits behind the upstream repository.
-
DarkGod authored
New Wanderer class. It's akin to adventurer but more chaotic! Instead of choosing talent trees you get a random set as you levelup! The set is determined by a seed that you can share with others to have friendly random competitions, or just share cool combos ;)
DarkGod authoredNew Wanderer class. It's akin to adventurer but more chaotic! Instead of choosing talent trees you get a random set as you levelup! The set is determined by a seed that you can share with others to have friendly random competitions, or just share cool combos ;)
talents.lua 12.62 KiB
-- ToME - Tales of Maj'Eyal
-- Copyright (C) 2009 - 2019 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 tacticals = {}
local Entity = require "engine.Entity"
local Tiles = require "engine.Tiles"
local Astar = require "engine.Astar"
---convert TACTIC labels in talent tactical tables to lower case
function Talents.aiLowerTacticals(tactical)
if type(tactical) ~= "table" then return tactical end
local tacts = {}
for tact, val in pairs(tactical) do
tact = tact:lower()
tacts[tact] = val
if tact == "self" then
tacts[tact]=Talents.aiLowerTacticals(val)
else
tacticals[tact] = true
end
end
return tacts
end
local oldNewTalentType = Talents.newTalentType
Talents.newTalentType = function(self, t)
-- That's some trikery ... try to guess where the talent tree comes from, base game or addon
local i = 1
while true do
local info = debug.getinfo(i, "S")
if not info then break end
if info.source then
local _, _, addon = info.source:find("/data%-([^/]+)/")
if addon then t.source = addon
elseif info.source:find("/data/talents") then t.source = "@vanilla@"
end
end
i = i + 1
end
return oldNewTalentType(self, t)
end
local oldNewTalent = Talents.newTalent
Talents.newTalent = function(self, t)
local tt = engine.interface.ActorTalents.talents_types_def[t.type[1]]
assert(tt, "No talent category "..tostring(t.type[1]).." for talent "..t.name)
if tt.generic then t.generic = true end
if tt.no_silence then t.no_silence = true end
if tt.is_spell then t.is_spell = true end
if tt.is_mind then t.is_mind = true end
if tt.is_nature then t.is_nature = true end
if tt.is_antimagic then t.is_antimagic = true end
if tt.is_unarmed then t.is_unarmed = true end
if tt.is_necromancy then t.is_necromancy = true end
if tt.autolearn_mindslayer then t.autolearn_mindslayer = true end
if tt.speed and not t.speed then t.speed = tt.speed end
if t.tactical then t.tactical = Talents.aiLowerTacticals(t.tactical) end
if t.tactical_imp then t.tactical_imp = Talents.aiLowerTacticals(t.tactical_imp) end -- DEBUGGING transitional
if not t.image then
t.image = "talents/"..(t.short_name or t.name):lower():gsub("[^a-z0-9_]", "_")..".png"
end
if fs.exists(Tiles.baseImageFile(t.image)) then t.display_entity = Entity.new{image=t.image, is_talent=true}
else t.display_entity = Entity.new{image="talents/default.png", is_talent=true}
end
if t.is_class_evolution then
t.short_name = (t.short_name or t.name):upper():gsub("[ ']", "_")
t.name = ("#LIGHT_STEEL_BLUE#%s (Class Evolution)"):tformat(_t(t.name, "talent name"))
end
if t.is_race_evolution then
t.short_name = (t.short_name or t.name):upper():gsub("[ ']", "_")
t.name = ("#SANDY_BROWN#%s (Race Evolution)"):tformat(_t(t.name, "talent name"))
end
-- Generate easier, reverse parameters, calls for methods
for k, e in pairsclone(t) do if type(e) == "function" and type(k) == "string" then
t["_"..k] = function(t, self, ...) return e(self, t, ...) end
end end
return oldNewTalent(self, t)
end
damDesc = function(self, type, dam)
if self:attr("dazed") then
dam = dam * 0.5
end
if self:attr("stunned") then
dam = dam * 0.5
end
if self:attr("invisible_damage_penalty") then
dam = dam * util.bound(1 - (self.invisible_damage_penalty / (self.invisible_damage_penalty_divisor or 1)), 0, 1)
end
if self:attr("numbed") then
dam = dam - dam * self:attr("numbed") / 100
end
if self:attr("generic_damage_penalty") then
dam = dam - dam * math.min(100, self:attr("generic_damage_penalty")) / 100
end
-- Increases damage
if self.inc_damage then
if _G.type(type) == "string" then
local dt = DamageType:get(type)
if dt.damdesc_split then
if _G.type(dt.damdesc_split) == "function" then
type = dt.damdesc_split(self, type, dam)
else
type = dt.damdesc_split
end
end
end
if _G.type(type) == "table" then
local basedam = dam
for _, ds in ipairs(type) do
local inc = self:combatGetDamageIncrease(ds[1])
dam = dam + (basedam * inc / 100) * ds[2]
end
else
local inc = self:combatGetDamageIncrease(type)
dam = dam + (dam * inc / 100)
end
end
return dam
end
Talents.is_a_type = {
is_spell = _t"a spell",
is_mind = _t"a mind power",
is_nature = _t"a nature gift",
is_antimagic = _t"an antimagic ability",
is_summon = _t"a summon power",
is_necromancy = _t"necromancy",
use_only_arcane = _t"usable during Aether Avatar",
}
Talents.damDesc = damDesc
Talents.main_env = getfenv(1)
Talents.ai_tactics_list = tacticals
-- Summoning AI helper functions
--- Generate targeting parameters for summoning talents
-- Only used by the (tactical) AI to test tactical parameters vs. aitarget when evaluating talents
function SummonTarget(self, t)
return {type="bolt", nowarning=true, pass_terrain = true, friendlyblock=false, nolock=true, talent=t}
end
-- returns grid coords in which to place a summon (if self.__talent_running is set, i.e. t.action is being executed)
-- or the exact coordinates of an NPC's target
-- allows the (tactical) AI to test the talent against aitarget properly
function onAIGetTargetSummon(self, t)
local aitarget = self.ai_target.actor
if aitarget and self:reactionToward(aitarget) < 0 then
if self.__talent_running then -- get coords in which to place the summon
--print("[onAIGetTargetSummon] placing summon", t.id) -- debugging
return t.aiSummonGrid(self, t)
else -- getting aitarget info
--print("[onAIGetTargetSummon] getting aitarget info", t.id) -- debugging
return aitarget.x, aitarget.y, aitarget
end
end
end
--- find a path between summoner and target (limit to range 25, effective)
function summonPath(self)
local aitarget = self.ai_target.actor
self.turn_procs.summoning = self.turn_procs.summoning or {}
if aitarget and self:reactionToward(aitarget) < 0 and core.fov.distance(self.x, self.y, aitarget.x, aitarget.y) <= 25 then
if config.settings.log_detail_ai > 1 then print(("[summonPath] calculating new summoning path [%d]%s at (%s, %s) to [%d]%s at (%s, %s)"):format( self.uid, self.name, self.x, self.y, aitarget.uid, aitarget.name, aitarget.x, aitarget.y)) end
local tx, ty = self:aiSeeTargetPos(aitarget)
local ast = Astar.new(game.level.map, self)
local path = ast:calc(tx, ty, self.x, self.y, nil, nil,
function(gx, gy) -- path around other actors
if gx == self.x and gy == self.y then return true end
return not game.level.map(gx, gy, engine.Map.ACTOR)
end
)
if path then
local range = 10
path[0] = {x=tx, y=ty} -- place origin
local stoptgtLOS, stopselfLOS = false, false
for i = 0, #path do
local node = path[i]
if not stoptgtLOS then
node.tgtLOS = self:hasLOS(node.x, node.y, "block_move", range, tx, ty)
if node.tgtLOS then path.nearTgtLOS = i else stoptgtLOS = true end
end
if not stopselfLOS then
node.selfLOS = self:hasLOS(node.x, node.y, "block_move", range)
if node.selfLOS and not path.farSelfLOS then path.farSelfLOS = i stopselfLOS = true end
end
end
self.turn_procs.summoning.path = path
--print("new summoning path:") table.print(path)
return path
else
self.turn_procs.summoning.path = false
end
else
self.turn_procs.summoning.path = false
end
if aitarget and config.settings.log_detail_ai > 1 and self.turn_procs.summoning.path == false then print(("[summonPath] no summoning path [%d]%s at (%s, %s) to [%d]%s at (%s, %s)"):format(self.uid, self.name, self.x, self.y, aitarget.uid, aitarget.name, aitarget.x, aitarget.y)) end
return self.turn_procs.summoning.path
end
-- find a reasonable spot for a MELEE summon to be placed
-- attempts to place the summon adjacent to the target
-- returns x, y coordinates or false if no spot can be found
function aiSummonGridMelee(self, t)
if self.turn_procs.summoning and self.turn_procs.summoning[t.id] then return self.turn_procs.summoning[t.id][1], self.turn_procs.summoning[t.id][2] end
local path = self.turn_procs.summoning and self.turn_procs.summoning.path
if path == nil then path = summonPath(self) end
--print("[aiSummonGridMelee] path:", path) table.print(path)
if path then
local range, max_dist = self:getTalentRange(t), 1
local node = math.max(path.farSelfLOS, #path - range)
if config.settings.log_detail_ai > 2 then print(("[aiSummonGridMelee] found melee summon node %d at (%d, %d) max_dist=%s"):format(node, path[node].x, path[node].y, max_dist)) end
if node > max_dist then return false end
local tg = self:getTalentTarget(t) or {type="bolt", nowarning=true, nolock=true, talent=t}
tg.range, pass_terrain = range, false
local ok, nx, ny
ok, ok, ok, nx, ny = self:canProject(tg, path[node].x, path[node].y)
--print("Projection result:", ok, nx, ny)
local x, y = util.findFreeGrid(nx, ny, 1, true, {[Map.ACTOR]=true})
if config.settings.log_detail_ai > 1 then print("[aiSummonGridMelee] Find Free Grid result:", x, y) end
if not (x and y) or core.fov.distance(path[0].x, path[0].y, x, y) > max_dist then return false end
self.turn_procs.summoning[t.id] = {nx, ny}
return nx, ny
end
return false
end
-- find a reasonable spot for a RANGED summon to be placed
-- attempts to place the summon within or close to LOS of the target but slightly off axis so it doesn't block the summoner's attacks
-- returns x, y coordinates or false if no spot can be found
function aiSummonGridRanged(self, t)
if self.turn_procs.summoning and self.turn_procs.summoning[t.id] then return self.turn_procs.summoning[t.id][1], self.turn_procs.summoning[t.id][2] end
local path = self.turn_procs.summoning and self.turn_procs.summoning.path
if path == nil then path = summonPath(self) end
if path then
local range, max_dist = self:getTalentRange(t), math.min(5, t.on_arrival and self:isTalentActive(self.T_MASTER_SUMMONER) and self:knowTalent(self.T_GRAND_ARRIVAL) and self:callTalent(self.T_GRAND_ARRIVAL, "radius") or 5)
local node = math.max(path.farSelfLOS, #path - range) -- closest possible node to target
if node > max_dist then return false end
node = math.min(max_dist, math.max(path.nearTgtLOS, path.farSelfLOS))
local nx, ny = path[node].x, path[node].y
local tgtDir = util.getDir(path[0].x, path[0].y, self.x, self.y)
if config.settings.log_detail_ai > 2 then print(("[aiSummonGridRanged] found ranged summon node %d at (%d, %d), dir=%s"):format(node, nx, ny, tgtDir)) end
-- if possible place the summon to the side of the LOS path to the target
local sides=util.dirSides(tgtDir, self.x, self.y)
local allow_grids = {}, sx, sy
local tg = self:getTalentTarget(t) or {type="bolt", nowarning=true, nolock=true, talent=t}
tg.range = range
for s, dir in pairs(sides) do
sx, sy = util.dirToCoord(dir, nx, ny)
sx, sy = sx + nx, sy + ny
if not game.level.map:checkAllEntities(sx, sy, "block_move") and self:hasLOS(sx, sy, "block_move", max_dist, path[0].x, path[0].y) and self:canProject(tg, sx, sy) then
allow_grids[#allow_grids+1] = {sx, sy}
end
end
--table.print(allow_grids, "_ag_")
local grid = rng.table(allow_grids)
if grid then nx, ny = grid[1], grid[2] end
self.turn_procs.summoning[t.id] = {nx, ny}
if config.settings.log_detail_ai > 1 then print(("[aiSummonGridRanged] picked summon grid at (%d, %d)"):format(nx, ny)) end
return nx, ny
end
return false
end
-- Checks that there is a reasonable spot to place a summon
function aiSummonPreUse(self, t, silent, fake)
if self.ai_target.actor and self:reactionToward(self.ai_target.actor) < 0 then
if not self.ai_state.target_last_seen or game.turn - (self.ai_state.target_last_seen.GCknown_turn or 0) <=50 then
return t.aiSummonGrid(self, t)
end
end
end
-- Archery range talents
Talents.main_env.archery_range = require("mod.class.interface.Archery").archery_range
load("/data/talents/misc/misc.lua")
load("/data/talents/techniques/techniques.lua")
load("/data/talents/cunning/cunning.lua")
load("/data/talents/spells/spells.lua")
load("/data/talents/gifts/gifts.lua")
load("/data/talents/celestial/celestial.lua")
load("/data/talents/corruptions/corruptions.lua")
load("/data/talents/undeads/undeads.lua")
load("/data/talents/cursed/cursed.lua")
load("/data/talents/chronomancy/chronomancer.lua")
load("/data/talents/psionic/psionic.lua")
load("/data/talents/uber/uber.lua")