561 lines
17 KiB
Lua
561 lines
17 KiB
Lua
|
|
dofile(minetest.get_modpath("adaptive_ai").."/kobold/kobold.lua")
|
|
|
|
local kobolds = adaptive_ai.kobolds
|
|
local stats = kobolds.stats
|
|
local helper = adaptive_ai.helper
|
|
local calc = adaptive_ai.calc
|
|
local population = adaptive_ai.population
|
|
|
|
local random = math.random
|
|
local floor = math.floor
|
|
|
|
local chat_log = adaptive_ai.chat_log
|
|
local ai_on_step = adaptive_ai.ai_on_step
|
|
|
|
local sort_nils_out = helper.sort_nils_out
|
|
local get_surface = helper.search_surface
|
|
local pop = helper.pop
|
|
local shuffle = helper.shuffle
|
|
local distance = calc.dist_euclidean
|
|
|
|
local kobold_from_lines = kobolds.from_lines
|
|
local kobold_score = kobolds.score
|
|
local kobold_decide = kobolds.decide
|
|
local kobold_create = kobolds.create
|
|
local kobold_eat = kobolds.eat
|
|
|
|
local get_from_census = population.get_from_census
|
|
local create_tribe = population.create_tribe
|
|
local add_to_tribe = population.add_to_tribe
|
|
local empty_population = population.empty
|
|
local get_couple = population.get_couple
|
|
local get_tribe = population.get_tribe
|
|
local popul_size = population.get_popul_size
|
|
|
|
local do_jump = helper.mobs.do_jump
|
|
local set_velocity = helper.mobs.set_velocity
|
|
local is_at_cliff = helper.mobs.is_at_cliff
|
|
local set_yaw = helper.mobs.set_yaw
|
|
local flight_check = helper.mobs.flight_check
|
|
local set_animation = helper.mobs.set_animation
|
|
local update_tag = helper.mobs.update_tag
|
|
|
|
local adjacent = helper.adjacent
|
|
local file_tag = kobolds.file_tag
|
|
local popul_path = population.path
|
|
local train_path = adaptive_ai.modpath.."/training/"
|
|
|
|
local pi = math.pi
|
|
local actions = kobolds.actions
|
|
local yaws = {
|
|
0, pi, 3*pi/2, pi/2, -- North, South, East, West
|
|
7*pi/4, 5*pi/4, pi/4, 3*pi/4 -- NE, SE, NW, SW
|
|
}
|
|
local food_nodes = {"adaptive_ai:moondrum_mushroom"}
|
|
local founders = nil
|
|
local min_founders = 4
|
|
local timer = 0
|
|
|
|
-- Out of Mobs_Redo
|
|
-- Equal to do_states(self, dtime) for self.state == "walk"
|
|
-- except without turning to random directions when safe
|
|
local function state_move(self, dtime)
|
|
local yaw = self.object:get_yaw() or 0
|
|
local s = self.object:get_pos()
|
|
local lp = nil
|
|
|
|
-- is there something I need to avoid?
|
|
if self.water_damage > 0
|
|
and self.lava_damage > 0 then
|
|
lp = minetest.find_node_near(s, 1, {"group:water", "group:lava"})
|
|
elseif self.water_damage > 0 then
|
|
lp = minetest.find_node_near(s, 1, {"group:water"})
|
|
elseif self.lava_damage > 0 then
|
|
lp = minetest.find_node_near(s, 1, {"group:lava"})
|
|
end
|
|
|
|
if lp then
|
|
-- if mob in water or lava then look for land
|
|
if (self.lava_damage
|
|
and minetest.registered_nodes[self.standing_in].groups.lava)
|
|
or (self.water_damage
|
|
and minetest.registered_nodes[self.standing_in].groups.water) then
|
|
|
|
lp = minetest.find_node_near(s, 5, {"group:soil", "group:stone",
|
|
"group:sand", node_ice, node_snowblock})
|
|
|
|
-- did we find land?
|
|
if lp then
|
|
local vec = {
|
|
x = lp.x - s.x,
|
|
z = lp.z - s.z
|
|
}
|
|
yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate
|
|
|
|
if lp.x > s.x then yaw = yaw + pi end
|
|
|
|
-- look towards land and jump/move in that direction
|
|
yaw = set_yaw(self, yaw, 6)
|
|
local jumped = do_jump(self)
|
|
if jumped then chat_log("Jumping while moving") end
|
|
set_velocity(self, self.walk_velocity)
|
|
else
|
|
yaw = yaw + random(-0.5, 0.5)
|
|
end
|
|
else
|
|
local vec = {
|
|
x = lp.x - s.x,
|
|
z = lp.z - s.z
|
|
}
|
|
|
|
yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate
|
|
|
|
if lp.x > s.x then yaw = yaw + pi end
|
|
end
|
|
|
|
yaw = set_yaw(self, yaw, 8)
|
|
end
|
|
|
|
-- stand for great fall in front
|
|
local temp_is_cliff = is_at_cliff(self)
|
|
|
|
if self.facing_fence == true
|
|
or temp_is_cliff then
|
|
set_velocity(self, 0)
|
|
self.state = "stand"
|
|
set_animation(self, "stand")
|
|
else
|
|
set_velocity(self, self.walk_velocity)
|
|
|
|
if flight_check(self)
|
|
and self.animation
|
|
and self.animation.fly_start
|
|
and self.animation.fly_end then
|
|
set_animation(self, "fly")
|
|
else
|
|
set_animation(self, "walk")
|
|
end
|
|
end
|
|
end
|
|
|
|
local function move_creature(self, yaw_id)
|
|
if self.state == "stand" or self.state == "walk" or self.state == "move" then
|
|
self.object:set_yaw(yaws[yaw_id])
|
|
set_velocity(self, self.walk_velocity)
|
|
self.state = "move"
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function load_founders()
|
|
local file = io.open(train_path..kobolds.trained_tag..".txt", "r")
|
|
local counter = 0
|
|
|
|
if file then
|
|
founders = {}
|
|
local reading, kobold = false, {}
|
|
for line in file:lines() do
|
|
counter = counter + 1
|
|
if not reading then
|
|
reading = line == file_tag[1]
|
|
if reading then
|
|
kobold = {}
|
|
table.insert(kobold,line)
|
|
end
|
|
else
|
|
reading = line ~= file_tag[2]
|
|
table.insert(kobold,line)
|
|
if not reading then
|
|
kobold = kobold_from_lines(kobold)
|
|
kobold = kobold_create(kobold)
|
|
table.insert(founders, kobold)
|
|
chat_log("Founder loaded: "..kobold.name)
|
|
end
|
|
end
|
|
end
|
|
file:close()
|
|
end
|
|
if #founders == 0 then
|
|
--error("Founders not found")
|
|
end
|
|
end
|
|
|
|
load_founders()
|
|
|
|
local function get_founders(n)
|
|
chat_log(#founders.." founders")
|
|
founders = founders or {}
|
|
local f
|
|
while #founders < min_founders do
|
|
f = {}
|
|
kobold_create(f)
|
|
table.insert(founders, f)
|
|
end
|
|
|
|
if #founders > min_founders then
|
|
n = n or min_founders
|
|
local tribe_founders = {}
|
|
shuffle(founders)
|
|
for i=1,n do
|
|
--chat_log("Shuffling founder "..founders[i].name)
|
|
table.insert(tribe_founders, founders[i])
|
|
end
|
|
|
|
return tribe_founders
|
|
else
|
|
return founders
|
|
end
|
|
end
|
|
|
|
local function load_kobold(self)
|
|
chat_log("Loading from census "..(self.nametag and "true" or "false"))
|
|
local old_self = get_from_census(self)
|
|
if old_self then
|
|
self.kobold = old_self
|
|
if not old_self.genes then error("No genes") end
|
|
chat_log(old_self.name.." loaded "..#old_self.genes.." genes")
|
|
self.timer = 0
|
|
else
|
|
self.kobold = nil
|
|
chat_log("Nil kobold")
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function set_kobold(self, kobold)
|
|
self.kobold = kobold
|
|
self.nametag = kobold.name
|
|
end
|
|
|
|
local function setup_kobold(self, mother, father)
|
|
if load_kobold(self) then
|
|
return
|
|
else
|
|
local kobold = {}
|
|
kobold_create(kobold, mother, father)
|
|
set_kobold(self, kobold)
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function place_kobold(p)
|
|
local ent = adaptive_ai:place_ai({
|
|
name = "adaptive_ai:kobold",
|
|
pos = p,
|
|
})
|
|
|
|
return ent
|
|
end
|
|
|
|
local function breed_kobold(self)
|
|
local mother = self.kobold
|
|
if not mother.tribe_id then
|
|
error("Breeding with no tribe.")
|
|
return
|
|
end
|
|
|
|
local tribe_id = mother.tribe_id
|
|
local tribe = population.get_tribe(tribe_id)
|
|
--chat_log(tribe and "Got tribe" or "Didn't get tribe")
|
|
if tribe then
|
|
local popul_size = popul_size(tribe_id)
|
|
|
|
if popul_size and popul_size < tribe.size * 2 then
|
|
local p = mother.pos
|
|
p = {x=(p.x), y=(p.y), z=(p.z)}
|
|
p.x = p.x + (random(0,1) - 0.5)*2
|
|
p.z = p.z + (random(0,1) - 0.5)*2
|
|
|
|
local father = get_couple(self.kobold)
|
|
local child = place_kobold(p)
|
|
setup_kobold(child, mother, father)
|
|
update_tag(child)
|
|
|
|
chat_log("Parents are "..mother.name..", "..father.name)
|
|
chat_log("Child is "..child.name)
|
|
|
|
child = child.kobold
|
|
child.tribe_id = tribe_id
|
|
population.add_to_tribe(tribe_id, child)
|
|
return true
|
|
else
|
|
chat_log("Popul size: "..(popul_size or "none").."\tTribe max: "..tribe.size * 2)
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
kobolds.breed_kobold = breed_kobold
|
|
|
|
local kobold_decisions = {
|
|
function(self, dtime) -- idle
|
|
set_velocity(self, 0)
|
|
self.state = "stand"
|
|
end,
|
|
function(self, dtime) -- wander
|
|
local n = random(1,#yaws)
|
|
local move = move_creature(self, n)
|
|
local kobold = self.kobold
|
|
if move then
|
|
kobold.hunger = kobold.hunger - 1
|
|
self.state = "walk"
|
|
end
|
|
end,
|
|
function(self, dtime) -- eat
|
|
local kobold = self.kobold
|
|
if kobold.food and #kobold.food > 0 then
|
|
local satiate = pop(kobold.food)
|
|
kobold_eat(kobold, satiate)
|
|
self.object:set_hp(kobold.hp)
|
|
end
|
|
end,
|
|
function(self, dtime) -- breed
|
|
local kobold = self.kobold
|
|
kobold.fitness = kobold.fitness + 2
|
|
kobold.hunger = kobold.hunger - 10
|
|
|
|
kobolds.breed_kobold(self)
|
|
end,
|
|
function(self, dtime) -- move North
|
|
local move = move_creature(self, 1)
|
|
--self.state = move and "move" or self.state
|
|
end,
|
|
function(self, dtime) -- move South
|
|
local move = move_creature(self, 2)
|
|
--self.state = move and "move" or self.state
|
|
end,
|
|
function(self, dtime) -- move East
|
|
local move = move_creature(self, 3)
|
|
--self.state = move and "move" or self.state
|
|
end,
|
|
function(self, dtime) -- move West
|
|
local move = move_creature(self, 4)
|
|
--self.state = move and "move" or self.state
|
|
end,
|
|
function(self, dtime) -- move NE
|
|
local move = move_creature(self, 5)
|
|
--self.state = move and "move" or self.state
|
|
end,
|
|
function(self, dtime) -- move SE
|
|
local move = move_creature(self, 6)
|
|
--self.state = move and "move" or self.state
|
|
end,
|
|
function(self, dtime) -- move NW
|
|
local move = move_creature(self, 7)
|
|
--self.state = move and "move" or self.state
|
|
end,
|
|
function(self, dtime) -- move SW
|
|
local move = move_creature(self, 8)
|
|
--self.state = move and "move" or self.state
|
|
end,
|
|
}
|
|
|
|
local function missing_food(pos)
|
|
if not pos then return true end
|
|
|
|
local still_there = false
|
|
for _,node in ipairs(food_nodes) do
|
|
--chat_log(minetest.get_node(pos) == node.name and "Food is still there" or "Food is no longer there")
|
|
still_there = still_there or minetest.get_node(pos) == node.name
|
|
end
|
|
--chat_log(not still_there and "Missing food" or "Not missing")
|
|
return not still_there
|
|
end
|
|
|
|
|
|
local manage_kobold = function(self, dtime)
|
|
local kobold = self.kobold
|
|
if kobold then
|
|
if kobold.delete or kobold.hp <= 0 then
|
|
self.object:remove()
|
|
return
|
|
end
|
|
kobold.hp = self.object:get_hp()
|
|
self.timer = self.timer + dtime
|
|
|
|
if kobold.hp > 0 then
|
|
local p = self.object:get_pos()
|
|
p = {x=floor(p.x+0.5), y=floor(p.y+0.5), z=floor(p.z+0.5)}
|
|
kobold.pos = p
|
|
|
|
local food_pos = minetest.find_node_near(self.object:get_pos(), self.reach, food_nodes)
|
|
|
|
if food_pos then
|
|
--chat_log("Pocketing food from "..food_pos.x..","..food_pos.z)
|
|
if kobold.closest_food
|
|
and kobold.closest_food.x == food_pos.x
|
|
and kobold.closest_food.z == food_pos.z then
|
|
kobold.closest_food = nil
|
|
end
|
|
table.insert(kobold.food, random(2,5))
|
|
minetest.set_node(food_pos, {name = "air"})
|
|
end
|
|
|
|
if missing_food(kobold.closest_food) then
|
|
kobold.closest_food = nil
|
|
end
|
|
|
|
food_pos = minetest.find_node_near(self.object:get_pos(), self.view_range, food_nodes)
|
|
if food_pos then
|
|
--chat_log("Smelling good food")
|
|
food_pos = {x=floor(food_pos.x+0.5),y=floor(food_pos.y+0.5),z=floor(food_pos.z+0.5)}
|
|
if food_pos and (not kobold.closest_food
|
|
or distance(p, food_pos) < distance(p, kobold.closest_food)) then
|
|
kobold.closest_food = food_pos
|
|
end
|
|
end
|
|
|
|
if not kobold.action or self.timer >= 0.5 then
|
|
self.timer = 0
|
|
--chat_log("Time to decide for "..kobold.name)
|
|
kobold.hunger = kobold.hunger - dtime
|
|
local neighbors = {}
|
|
|
|
local pos, pos2 = {}
|
|
for i=1,#adjacent do
|
|
pos.x = p.x + adjacent[i].x
|
|
pos.y = p.y
|
|
pos.z = p.z + adjacent[i].z
|
|
pos2 = get_surface(pos)
|
|
--chat_log(not pos2 and "Emergency" or "Height: "..pos2.y)
|
|
neighbors[i] = pos2 and {x=pos2.x,y=pos2.y-pos.y,z=pos2.z} or {x=pos.x, y=pos.y+self.kobold.climb + 1,z=pos.z}
|
|
end
|
|
|
|
--chat_log("Closest food: "..(kobold.closest_food and kobold.closest_food.x-p.x..","..kobold.closest_food.z-p.z.." from me" or "none"))
|
|
|
|
local decision, d_id = kobold_decide(kobold, neighbors)
|
|
|
|
if decision then
|
|
chat_log(kobold.name.." decides: "..actions[decision])
|
|
kobold.action = decision
|
|
kobold_decisions[decision](self, dtime)
|
|
else
|
|
if not kobold.action then
|
|
chat_log(kobold.name.." decides nothing -- error")
|
|
kobold.action = 1
|
|
kobold_decisions[1](self, dtime)
|
|
else
|
|
chat_log(kobold.name.." keeps decision: "..actions[kobold.action])
|
|
end
|
|
end
|
|
kobold.fitness = kobold.fitness + kobold_score(kobold)
|
|
end
|
|
|
|
if kobold.hunger < 0 then
|
|
kobold.hunger = 0
|
|
kobold.hp = self.object:get_hp() - 1
|
|
self.object:set_hp(kobold.hp)
|
|
end
|
|
end
|
|
else
|
|
chat_log("No kobold")
|
|
end
|
|
end
|
|
|
|
local function custom_on_step(self, dtime)
|
|
local old_state = self.state
|
|
|
|
manage_kobold(self, dtime)
|
|
--[[
|
|
if old_state ~= self.state then
|
|
chat_log("Current state: "..self.state)
|
|
end
|
|
--]]
|
|
|
|
if self.state == "move" then
|
|
state_move(self, dtime)
|
|
end
|
|
do_jump(self, dtime)
|
|
--ai_on_step(self, dtime)
|
|
return false
|
|
end
|
|
|
|
local function place_tribe(p)
|
|
local roots = {x=p.x, y=p.y-2, z=p.z}
|
|
local ents = {}
|
|
|
|
local popul = create_tribe(p, "adaptive_ai:kobold", get_founders(), kobolds.stats.view_range, kobolds.clone, 4)
|
|
local ent, pos, c
|
|
c = 0
|
|
|
|
for i,k in ipairs(popul) do
|
|
pos = get_surface(k.pos)
|
|
ent = place_kobold(pos)
|
|
set_kobold(ent, k)
|
|
--chat_log("Placing "..ent.kobold.name.."!")
|
|
ent.kobold.pos = pos
|
|
update_tag(ent)
|
|
end
|
|
|
|
chat_log("Placed tribe!")
|
|
minetest.set_node(roots, {name = "adaptive_ai:moondrum_roots"})
|
|
end
|
|
|
|
adaptive_ai:register_ai("adaptive_ai:kobold", {
|
|
stepheight = 0.8,
|
|
hp_min = kobolds.stats.hp_max,
|
|
hp_max = kobolds.stats.hp_max,
|
|
armor = 100,
|
|
walk_velocity = 1.5,
|
|
run_velocity = 2,
|
|
jump = true,
|
|
jump_height = kobolds.stats.jump,
|
|
view_range = kobolds.stats.view_range,
|
|
damage = 2,
|
|
knock_back = true,
|
|
fear_height = kobolds.stats.fear_height,
|
|
floats = 1,
|
|
reach = 1,
|
|
attack_type = "dogfight",
|
|
makes_footstep_sound = true,
|
|
visual = "cube",
|
|
visual_size = {x=0.8, y=1.6},
|
|
textures = {
|
|
"aa_imp_top.png",
|
|
"aa_imp_top.png",
|
|
"aa_imp_side.png",
|
|
"aa_imp_side.png",
|
|
"aa_imp_face.png",
|
|
"aa_imp_side.png",
|
|
},
|
|
collisionbox = {-0.5,-0.8,-0.5, 0.5,0.8,0.5},
|
|
walk_chance = 1,
|
|
|
|
actions = kobolds.actions,
|
|
action_managers = kobold_decisions,
|
|
on_spawn = load_kobold,
|
|
do_custom = custom_on_step,
|
|
|
|
asexual = false,
|
|
breed = kobold_create,
|
|
})
|
|
|
|
minetest.register_chatcommand("kobold", {
|
|
func = function(name, param)
|
|
local player = minetest.get_player_by_name(name)
|
|
local pos = player and player:getpos() or nil
|
|
if pos then
|
|
place_tribe(pos)
|
|
end
|
|
end
|
|
})
|
|
|
|
minetest.register_chatcommand("empty_camps", {
|
|
func = function(name, param)
|
|
empty_population()
|
|
end
|
|
})
|
|
|
|
minetest.register_chatcommand("founders", {
|
|
func = function(name, param)
|
|
local player = minetest.get_player_by_name(name)
|
|
|
|
for i,f in ipairs(founders) do
|
|
if player then
|
|
minetest.chat_send_player(name, "Founder: "..f.name)
|
|
end
|
|
end
|
|
end
|
|
})
|