diff --git a/api.lua b/api.lua new file mode 100644 index 0000000..15ce981 --- /dev/null +++ b/api.lua @@ -0,0 +1,444 @@ +-------------- +-- Creatura -- +-------------- + +creatura.api = {} + +-- Math -- + +local pi = math.pi +local pi2 = pi * 2 +local abs = math.abs +local floor = math.floor +local random = math.random + +local sin = math.sin +local cos = math.cos +local atan2 = math.atan2 + +local function diff(a, b) -- Get difference between 2 angles + return math.atan2(math.sin(b - a), math.cos(b - a)) +end + +local function clamp(val, min, max) + if val < min then + val = min + elseif max < val then + val = max + end + return val +end + +local vec_dir = vector.direction +local vec_dist = vector.distance +local vec_multi = vector.multiply +local vec_sub = vector.subtract +local vec_add = vector.add + +local function vec_center(v) + return {x = floor(v.x + 0.5), y = floor(v.y + 0.5), z = floor(v.z + 0.5)} +end + +local function vec_raise(v, n) + if not v then return end + return {x = v.x, y = v.y + n, z = v.z} +end + +local function dist_2d(pos1, pos2) + local a = {x = pos1.x, y = 0, z = pos1.z} + local b = {x = pos2.x, y = 0, z = pos2.z} + return vec_dist(a, b) +end + +--------------- +-- Local API -- +--------------- + +local function indicate_damage(self) + self.object:set_texture_mod("^[colorize:#FF000040") + core.after(0.2, function() + if creatura.is_alive(self) then + self.object:set_texture_mod("") + end + end) +end + +local function get_node_height(pos) + local node = minetest.get_node(pos) + local def = minetest.registered_nodes[node.name] + if not def then return nil end + if def.walkable then + if def.drawtype == "nodebox" then + if def.node_box + and def.node_box.type == "fixed" then + if type(def.node_box.fixed[1]) == "number" then + return pos.y + node.node_box.fixed[5] + elseif type(node.node_box.fixed[1]) == "table" then + return pos.y + node.node_box.fixed[1][5] + else + return pos.y + 0.5 + end + elseif node.node_box + and node.node_box.type == 'leveled' then + return minetest.get_node_level(pos) / 64 - 0.5 + pos.y + else + return pos.y + 0.5 + end + else + return pos.y + 0.5 + end + else + return pos.y - 0.5 + end +end + +local function walkable(pos) + return minetest.registered_nodes[minetest.get_node(pos).name].walkable +end + +local function is_under_solid(pos) + local pos2 = vector.new(pos.x, pos.y + 1, pos.z) + return (walkable(pos2) or ((get_node_height(pos2) or 0) < 1.5)) +end + +local function is_node_walkable(name) + local def = minetest.registered_nodes[name] + return def and def.walkable +end + +local function is_value_in_table(tbl, val) + for _, v in pairs(tbl) do + if v == val then + return true + end + end + return false +end + +----------------------- +-- Utility Functions -- +----------------------- + +-- Movement Methods -- + +creatura.registered_movement_methods = {} + +function creatura.register_movement_method(name, func) + creatura.registered_movement_methods[name] = func +end + +-- Utility Behaviors -- + +creatura.registered_utilities = {} + +function creatura.register_utility(name, func) + creatura.registered_utilities[name] = func +end + +-- Sensors -- + +function creatura.is_pos_moveable(pos, width, height) + local pos1 = { + x = pos.x - (width + 0.2), + y = pos.y, + z = pos.z - (width + 0.2), + } + local pos2 = { + x = pos.x + (width + 0.2), + y = pos.y, + z = pos.z + (width + 0.2), + } + for x = pos1.x, pos2.x do + for z = pos1.z, pos2.z do + local pos3 = {x = x, y = (pos.y + height), z = z} + local pos4 = {x = pos3.x, y = pos.y, z = pos3.z} + local ray = minetest.raycast(pos3, pos4, false, false) + for pointed_thing in ray do + if pointed_thing.type == "node" then + return false + end + end + end + end + return true +end + +local moveable = creatura.is_pos_moveable + +function creatura.fast_ray_sight(pos1, pos2, water) + local ray = minetest.raycast(pos1, pos2, false, water or false) + for pointed_thing in ray do + if pointed_thing.type == "node" then + return false, vec_dist(pos1, pointed_thing.intersection_point), pointed_thing.ref + end + end + return true, vec_dist(pos1, pos2) +end + +local fast_ray_sight = creatura.fast_ray_sight + +function creatura.get_next_move(self, pos2) + local last_move = self._movement_data.last_move + local width = self.width + local height = self.height + local scan_width = width * 2 + local pos = self.object:get_pos() + pos.y = floor(pos.y + 0.5) + if last_move + and last_move.pos then + local last_call = minetest.get_position_from_hash(last_move.pos) + local last_move = minetest.get_position_from_hash(last_move.move) + if vector.equals(vec_center(last_call), vec_center(pos)) then + return last_move + end + end + local neighbors = { + vec_add(pos, {x = 1, y = 0, z = 0}), + vec_add(pos, {x = 1, y = 0, z = 1}), + vec_add(pos, {x = 0, y = 0, z = 1}), + vec_add(pos, {x = -1, y = 0, z = 1}), + vec_add(pos, {x = -1, y = 0, z = 0}), + vec_add(pos, {x = -1, y = 0, z = -1}), + vec_add(pos, {x = 0, y = 0, z = -1}), + vec_add(pos, {x = 1, y = 0, z = -1}) + } + local next + table.sort(neighbors, function(a, b) + return vec_dist(a, pos2) < vec_dist(b, pos2) + end) + for i = 1, #neighbors do + local neighbor = neighbors[i] + local can_move = fast_ray_sight({x = pos.x, y = neighbor.y, z = pos.z}, neighbor) + if vector.equals(neighbor, pos2) then + can_move = true + end + if not self:is_pos_safe(vec_raise(neighbor, 0.6)) then + can_move = false + end + if can_move + and not moveable(vec_raise(neighbor, 0.6), width, height) then + can_move = false + end + local dist = vec_dist(neighbor, pos2) + if can_move then + next = neighbor + break + end + end + if next then + self._movement_data.last_move = { + pos = minetest.hash_node_position(pos), + move = minetest.hash_node_position(next) + } + end + return next +end + +function creatura.get_next_move_3d(self, pos2) + local last_move = self._movement_data.last_move + local width = self.width + local height = self.height + local scan_width = width * 2 + local pos = self.object:get_pos() + pos.y = pos.y + 0.5 + if last_move + and last_move.pos then + local last_call = minetest.get_position_from_hash(last_move.pos) + local last_move = minetest.get_position_from_hash(last_move.move) + if vector.equals(vec_center(last_call), vec_center(pos)) then + return last_move + end + end + local neighbors = { + vec_add(pos, {x = scan_width, y = 0, z = 0}), + vec_add(pos, {x = scan_width, y = 0, z = scan_width}), + vec_add(pos, {x = 0, y = 0, z = scan_width}), + vec_add(pos, {x = -scan_width, y = 0, z = scan_width}), + vec_add(pos, {x = -scan_width, y = 0, z = 0}), + vec_add(pos, {x = -scan_width, y = 0, z = -scan_width}), + vec_add(pos, {x = 0, y = 0, z = -scan_width}), + vec_add(pos, {x = scan_width, y = 0, z = -scan_width}) + } + local next + table.sort(neighbors, function(a, b) + return vec_dist(a, pos2) < vec_dist(b, pos2) + end) + for i = 1, #neighbors do + local neighbor = neighbors[i] + local can_move = fast_ray_sight({x = pos.x, y = neighbor.y, z = pos.z}, neighbor) + if not moveable(vec_raise(neighbor, 0.6), width, height) then + can_move = false + end + if vector.equals(neighbor, pos2) then + can_move = true + end + local dist = vec_dist(neighbor, pos2) + if can_move then + next = neighbor + break + end + end + if next then + self._movement_data.last_move = { + pos = minetest.hash_node_position(pos), + move = minetest.hash_node_position(next) + } + end + return vec_raise(next, clamp((pos2.y - pos.y) + -0.6, -1, 1)) +end + +function creatura.sensor_floor(self, range, water) + local pos = self.object:get_pos() + local pos2 = vec_raise(pos, -range) + local _, dist, node = fast_ray_sight(pos, pos2, water or false) + return dist, node +end + +function creatura.sensor_ceil(self, range, water) + local pos = vec_raise(self.object:get_pos(), self.height) + local pos2 = vec_raise(pos, range) + local _, dist, node = fast_ray_sight(pos, pos2, water or false) + return dist, node +end + +-- Misc + +function creatura.is_valid(mob) + if not mob then return false end + if type(mob) == "table" then mob = mob.object end + if type(mob) == "userdata" then + if mob:is_player() then + if mob:get_look_horizontal() then return mob end + else + if mob:get_yaw() then return mob end + end + end + return false +end + +function creatura.is_alive(mob) + if not creatura.is_valid(mob) then + return false + end + if type(mob) == "table" then + return mob.hp > 0 + end + if mob:is_player() then + return mob:get_hp() > 0 + else + local ent = mob:get_luaentity() + return ent and ent.hp and ent.hp > 0 + end +end + +function creatura.get_nearby_player(self) + local objects = minetest.get_objects_inside_radius(self:get_center_pos(), self.tracking_range) + for _, object in ipairs(objects) do + if object:is_player() + and creatura.is_alive(object) then + return object + end + end +end + +function creatura.get_nearby_players(self) + local objects = minetest.get_objects_inside_radius(self:get_center_pos(), self.tracking_range) + local nearby = {} + for _, object in ipairs(objects) do + if object:is_player() + and creatura.is_alive(object) then + table.insert(nearby, object) + end + end + return nearby +end + +function creatura.get_nearby_entity(self, name) + local objects = minetest.get_objects_inside_radius(self:get_center_pos(), self.tracking_range) + for _, object in ipairs(objects) do + if creatura.is_alive(object) + and not object:is_player() + and object ~= self.object + and object:get_luaentity().name == name then + return object + end + end + return +end + +function creatura.get_nearby_entities(self, name) + local objects = minetest.get_objects_inside_radius(self:get_center_pos(), self.tracking_range) + local nearby = {} + for _, object in ipairs(objects) do + if creatura.is_alive(object) + and not object:is_player() + and object ~= self.object + and object:get_luaentity().name == name then + table.insert(nearby, object) + end + end + return nearby +end + +function creatura.get_node_def(pos) + local def = minetest.registered_nodes[minetest.get_node(pos).name] + return def +end + +-------------------- +-- Global Mob API -- +-------------------- + +-- Drops -- + +function creatura.drop_items(self) + if not self.drops then return end + for i = 1, #self.drops do + local drop_def = self.drops[i] + local name = drop_def.name + if not name then return end + local min_amount = drop_def.min or 1 + local max_amount = drop_def.max or 2 + local chance = drop_def.chance or 1 + local amount = random(min_amount, max_amount) + if random(chance) < 2 then + local pos = self.object:get_pos() + local item = minetest.add_item(pos, ItemStack(name .. " " .. amount)) + if item then + item:add_velocity({ + x = random(-2, 2), + y = 1.5, + z = random(-2, 2) + }) + end + end + end +end + +-- On Punch -- + +function creatura.basic_punch_func(self, puncher, time_from_last_punch, tool_capabilities, direction, damage) + if not puncher then return end + local tool = "" + if puncher:is_player() then + tool = puncher:get_wielded_item():get_name() + end + if (self.immune_to + and is_value_in_table(self.immune_to, tool)) then + return + end + local dir = vec_dir(puncher:get_pos(), self:get_center_pos()) + self:apply_knockback(dir) + self:hurt(tool_capabilities.damage_groups.fleshy or 2) + if random(4) < 2 then + self:play_sound("hurt") + end + if time_from_last_punch > 0.5 then + self:play_sound("hit") + end + indicate_damage(self) +end + +local path = minetest.get_modpath("creatura") + +dofile(path.."/mob_meta.lua") \ No newline at end of file diff --git a/boids.lua b/boids.lua new file mode 100644 index 0000000..e690c51 --- /dev/null +++ b/boids.lua @@ -0,0 +1,158 @@ +----------- +-- Boids -- +----------- + +local random = math.random + +local function average(tbl) + local sum = 0 + for _,v in pairs(tbl) do -- Get the sum of all numbers in t + sum = sum + v + end + return sum / #tbl +end + +local function average_angle(tbl) + local sum_sin, sum_cos = 0, 0 + for _, v in pairs(tbl) do + sum_sin = sum_sin + math.sin(v) + sum_cos = sum_cos + math.cos(v) + end + return math.atan2(sum_sin, sum_cos) +end + +local vec_dist = vector.distance +local vec_dir = vector.direction +local vec_len = vector.length +local vec_add = vector.add +local vec_multi = vector.multiply +local vec_normal = vector.normalize +local vec_divide = vector.divide +local function vec_raise(v, n) + return {x = v.x, y = v.y + n, z = v.z} +end + +local function get_average_pos(vectors) + local sum = {x = 0, y = 0, z = 0} + for _, vec in pairs(vectors) do sum = vec_add(sum, vec) end + return vec_divide(sum, #vectors) +end + +local function dist_2d(pos1, pos2) + local a = vector.new( + pos1.x, + 0, + pos1.z + ) + local b = vector.new( + pos2.x, + 0, + pos2.z + ) + return vec_dist(a, b) +end + +local yaw2dir = minetest.yaw_to_dir +local dir2yaw = minetest.dir_to_yaw + +-- Refresh Boid Leader -- + +local last_boid_refresh = minetest.get_us_time() + +-- Get Boid Members -- + +-- This function scans within +-- a set radius for potential +-- boid members, and assigns +-- a leader. A new leader +-- is only assigned every 12 +-- seconds or if a new mob +-- is in the boid. + +function creatura.get_boid_members(pos, radius, name) + local objects = minetest.get_objects_inside_radius(pos, radius) + if #objects < 2 then return {} end + local members = {} + local max_boid = minetest.registered_entities[name].max_boids or 7 + for i = 1, #objects do + if #members > max_boid then break end + local object = objects[i] + if object:get_luaentity() + and object:get_luaentity().name == name then + object:get_luaentity().boid_heading = math.rad(random(360)) + table.insert(members, object) + end + end + return members +end + +-- Calculate Boid angles and offsets. + +local function debugpart(pos, time, part) + minetest.add_particle({ + pos = pos, + expirationtime = time or 0.2, + size = 8, + glow = 16, + texture = part or "creatura_particle_red.png" + }) +end + +function creatura.get_boid_angle(self, boid, range) -- calculates boid angle based on seperation, alignment, and cohesion + local pos = self.object:get_pos() + local boids = boid or creatura.get_boid_members(pos, range or 4, self.name) + if #boids < 3 then return end + local yaw = self.object:get_yaw() + local lift = self.object:get_velocity().y + -- Add Boid data to tables + local closest_pos + local positions = {} + local angles = {} + local lifts = {} + for i = 1, #boids do + local boid = boids[i] + if boid:get_pos() then + local boid_pos = boid:get_pos() + local boid_yaw = boid:get_yaw() + table.insert(positions, boid_pos) + if boid ~= self.object then + table.insert(lifts, boid:get_velocity().y) + table.insert(angles, boid:get_yaw()) + if not closest_pos + or vec_dist(pos, boid_pos) < vec_dist(pos, closest_pos) then + closest_pos = boid_pos + end + end + end + end + if #positions < 3 then return end + local center = get_average_pos(positions) + local dir2closest = vec_dir(pos, closest_pos) + -- Calculate Parameters + local alignment = average_angle(angles) + center = vec_add(center, yaw2dir(alignment)) + local dir2center = vec_dir(pos, center) + local seperation = dir2yaw(vector.multiply(dir2center, -1)) + local cohesion = dir2yaw(dir2center) + local params = {alignment} + if self.boid_heading then + table.insert(params, yaw + self.boid_heading) + end + if dist_2d(pos, closest_pos) < (self.boid_seperation or self.width * 3) then -- seperation is causing north issue + table.insert(params, seperation) + elseif dist_2d(pos, center) > (#boids * 0.33) * (self.boid_seperation or self.width * 3) then + table.insert(params, cohesion) + end + -- Vertical Params + local vert_alignment = average(lifts) + local vert_seperation = (self.speed or 2) * -dir2closest.y + local vert_cohesion = (self.speed or 2) * dir2center.y + local vert_params = {vert_alignment} + if math.abs(pos.y - closest_pos.y) < (self.boid_seperation or self.width * 3) then + table.insert(vert_params, vert_seperation) + elseif math.abs(pos.y - closest_pos.y) > 1.5 * (self.boid_seperation or self.width * 3) then + table.insert(vert_params, vert_cohesion + (lift - vert_cohesion) * 0.1) + end + self.boid_heading = nil + return average_angle(params), average_angle(vert_params) +end \ No newline at end of file diff --git a/doc.txt b/doc.txt new file mode 100644 index 0000000..3bd2616 --- /dev/null +++ b/doc.txt @@ -0,0 +1,140 @@ + +Registration +------------ + +creatura.register_mob(name, mob definition) + +Mob Definition uses almost all entity definition params + +{ + max_health = 10 -- Maximum Health + damage = 0 -- Damage dealt by mob + speed = 4 -- Maximum Speed + tracking_range = 16 -- Maximum range for finding entities/blocks + despawn_after = 1500 -- Despawn after being active for this amount of time + + max_fall = 8 -- How far a mob can fall before taking damage (set to 0 to disable fall damage) + turn_rate = 7 -- Turn Rate in rad/s + bouyancy_multiplier = 1 -- Multiplier for bouyancy effects (set to 0 to disable bouyancy) + hydrodynamics_multiplier = 1 -- Multiplier for hydroynamic effects (set to 0 to disable hydrodynamics) + + hitbox = { -- Hitbox params (Uses custom registration to force get_pos() to always return bottom of box) + width = 0.5, (total width = width * 2. A width of 0.5 results in a box with a total width of 1) + height = 1 (total height of box) + } + + animations = { + anim = {range = {x = 1, y = 10}, speed = 30, frame_blend = 0.3, loop = true} + } + + drops = { + {name = (itemstring), min = 1, max = 3, chance = 1}, + } + follow = { + "farming:seed_wheat", + "farming:seed_cotton" + } + + utility_stack = { + -- Every second, all utilities in the stack are evaluated + -- Whichever utilitiy's get_score function returns the highest number will be executed + -- If multiple utilities have the same score, the one with the highest index is executed + [1] = { + `utility` -- name of utility to evaluate + `get_score` -- function (only accepts `self` as an arg) that returns a number + } + } + + activate_func = function(self, staticdata, dtime_s) -- called upon activation + step_func = function(self, dtime, moveresult) -- called every server step + death_func = function(self) -- called when mobs health drops to/below 0 +} + +Lua Entity Methods +------------------ + +`move(pos, method, speed, animation)` +- `pos`: position to move to +- `method`: method used to move to `pos` +- `speed`: multiplier for `speed` +- `animation`: animation to play while moving + +`halt()` +- stops movement + +`turn_to(yaw[, turn_rate])` +- `yaw`: yaw (in radians) to turn to +- `turn_rate`: turn rate in rad/s (default: 10) -- likely to be deprecated + +`set_gravity(gravity)` +- `gravity`: vertical acceleration rate + +`set_forward_velocity(speed)` +- `speed`: rate in m/s to travel forward at + +`set_vertical_velocity(speed)` +- `speed`: rate in m/s to travel vertically at + +`apply_knockback(dir, power)` +- `dir`: direction vector +- `power`: multiplier for dir + +`punch_target(target)` +- applies 'damage' to 'target' + +`hurt(damage)` +- `damage`: number to subtract from health (ignores armor) + +`heal(health)` +- `health`: number to add to health + +`get_center_pos()` +- returns position at center of hitbox + +`pos_in_box(pos[, size])` +- returns true if 'pos' is within hitbox +- `size`: width of box to check in (optional) + +`animate(anim)` +- sets animation to `anim` + +`set_texture(id, tbl)` +- `id`: table index +- `tbl`: table of textures + +`set_scale(x)` +- `x`: multiplier for base scale (0.5 sets scale to half, 2 sets scale to double) + +`fix_attached_scale(parent)` +- sets scale to appropriate value when attached to 'parent' +- `parent`: object + +`memorize(id, val)` +-- stores `val` to staticdata +- `id`: key for table +- `val`: value to store + +`forget(id)` +-- removes `id` from staticdata + +`recall(id)` +-- returns value of `id` from staticdata + +`timer(n)` +-- returns true avery `n` seconds + +`get_hitbox()` +-- returns current hitbox + +`get_height()` +-- returns current height + +`get_visual_size()` +-- returns current visual size + +`follow_wielded_item(player)` +-- returns itemstack, item name of `player`s wielded item if item is in 'follow' + +`get_target(target)` +-- returns if `target` is alive, if mob has a line of sight with `target`, `target`s position + diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..bbced09 --- /dev/null +++ b/init.lua @@ -0,0 +1,24 @@ +creatura = {} + +local path = minetest.get_modpath("creatura") + +dofile(path.."/pathfinder.lua") +dofile(path.."/api.lua") +dofile(path.."/methods.lua") + +-- Optional Files -- + +-- Optional files can be safely removed +-- by game developers who don't need the +-- extra features + +local function load_file(filepath, filename) + if io.open(filepath .. "/" .. filename, "r") then + dofile(filepath .. "/" .. filename) + else + minetest.log("action", "[Creatura] The file " .. filename .. " could not be loaded.") + end +end + +load_file(path, "boids.lua") +load_file(path, "spawning.lua") \ No newline at end of file diff --git a/methods.lua b/methods.lua new file mode 100644 index 0000000..c86f04c --- /dev/null +++ b/methods.lua @@ -0,0 +1,386 @@ +------------- +-- Methods -- +------------- + +local pi = math.pi +local pi2 = pi * 2 +local abs = math.abs +local floor = math.floor +local random = math.random +local rad = math.rad + +local function diff(a, b) -- Get difference between 2 angles + return math.atan2(math.sin(b - a), math.cos(b - a)) +end + +local function clamp(val, min, max) + if val < min then + val = min + elseif max < val then + val = max + end + return val +end + +local function vec_center(v) + return {x = floor(v.x + 0.5), y = floor(v.y + 0.5), z = floor(v.z + 0.5)} +end + +local function vec_raise(v, n) + return {x = v.x, y = v.y + n, z = v.z} +end + +local vec_dir = vector.direction +local vec_dist = vector.distance +local vec_multi = vector.multiply +local vec_add = vector.add +local yaw2dir = minetest.yaw_to_dir + +------------- +-- Actions -- +------------- + +-- Actions are more specific behaviors used +-- to compose a Utility. + +-- Walk + +function creatura.action_walk(self, pos2, timeout, method, speed_factor, anim) + local timer = timeout or 4 + local move_init = false + local function func(self) + if not pos2 + or (move_init + and not self._movement_data.goal) then return true end + local pos = self.object:get_pos() + timer = timer - self.dtime + if timer <= 0 + or self:pos_in_box({x = pos2.x, y = pos.y + 0.1, z = pos2.z}) then + self:halt() + return true + end + self:move(pos2, method or "creatura:neighbors", speed_factor or 0.5, anim) + move_init = true + end + self:set_action(func) +end + +function creatura.action_fly(self, pos2, timeout, method, speed_factor, anim) + local timer = timeout or 4 + local move_init = false + local function func(self) + if not pos2 + or (move_init + and not self._movement_data.goal) then return true end + local pos = self.object:get_pos() + timer = timer - self.dtime + if timer <= 0 + or self:pos_in_box(pos2) then + self:halt() + return true + end + self:move(pos2, method, speed_factor or 0.5, anim) + move_init = true + end + self:set_action(func) +end + +-- Idle + +function creatura.action_idle(self, time, anim) + local timer = time + local function func(self) + self:set_gravity(-9.8) + self:halt() + self:animate(anim or "stand") + timer = timer - self.dtime + if timer <= 0 then + return true + end + end + self:set_action(func) +end + +-- Rotate on Z axis in random direction until 90 degree angle is reached + +function creatura.action_fallover(self) + local zrot = 0 + local init = false + local dir = 1 + local function func(self) + if not init then + self:animate("stand") + if random(2) < 2 then + dir = -1 + end + init = true + end + local rot = self.object:get_rotation() + local goal = (pi * 0.5) * dir + local dif = abs(rot.z - goal) + zrot = rot.z + (dif * dir) * 0.15 + self.object:set_rotation({x = rot.x, y = rot.y, z = zrot}) + if (dir > 0 and zrot >= goal) + or (dir < 0 and zrot <= goal) then return true end + end + self:set_action(func) +end + +---------------------- +-- Movement Methods -- +---------------------- + +-- Pathfinding + +function get_line_of_sight(a, b) + local steps = floor(vector.distance(a, b)) + local line = {} + + for i = 0, steps do + local pos + + if steps > 0 then + pos = { + x = a.x + (b.x - a.x) * (i / steps), + y = a.y + (b.y - a.y) * (i / steps), + z = a.z + (b.z - a.z) * (i / steps) + } + else + pos = a + end + table.insert(line, pos) + end + + if #line < 1 then + return false + else + for i = 1, #line do + local node = minetest.get_node(line[i]) + if minetest.registered_nodes[node.name].walkable then + return false + end + end + end + return true +end + +local function movement_theta_pathfind(self, pos2, speed) + local pos = self.object:get_pos() + local goal = pos2 + self._path = self._path or {} + local temp_goal = self._movement_data.temp_goal + if not temp_goal + or self:pos_in_box({x = temp_goal.x, y = pos.y + self.height * 0.5, z = temp_goal.z}) then + self._movement_data.temp_goal = creatura.get_next_move(self, pos2) + temp_goal = self._movement_data.temp_goal + end + if #self._path < 1 then + self._path = creatura.find_theta_path(self, self.object:get_pos(), pos2, self.width, self.height, 500) or {} + else + temp_goal = self._path[2] or self._path[1] + if self:pos_in_box({x = temp_goal.x, y = pos.y + self.height * 0.5, z = temp_goal.z}) then + table.remove(self._path, 1) + end + end + goal.y = pos.y + 0.5 + local dir = vector.direction(self.object:get_pos(), pos2) + local tyaw = minetest.dir_to_yaw(dir) + local turn_rate = self.turn_rate or 10 + if temp_goal then + dir = vector.direction(self.object:get_pos(), temp_goal) + tyaw = minetest.dir_to_yaw(dir) + if #self._path < 1 + and not self:is_pos_safe(temp_goal) then + self:animate("walk") + self:set_forward_velocity(0) + self:halt() + return + end + end + self:turn_to(tyaw, turn_rate) + self:animate("walk") + self:set_gravity(-9.8) + self:set_forward_velocity(speed or 2) + if self:pos_in_box(goal) then + self:halt() + end +end + +creatura.register_movement_method("creatura:theta_pathfind", movement_theta_pathfind) + +local function movement_pathfind(self, pos2, speed) + local pos = self.object:get_pos() + local goal = pos2 + local temp_goal = self._movement_data.temp_goal + self._path = self._path or {} + if (not temp_goal + or self:pos_in_box({x = temp_goal.x, y = pos.y + self.height * 0.5, z = temp_goal.z})) + and #self._path < 1 then + self._movement_data.temp_goal = creatura.get_next_move(self, pos2) + temp_goal = self._movement_data.temp_goal + end + if #self._path < 2 then + self._path = creatura.find_path(self, self.object:get_pos(), pos2, self.width, self.height, 100) or {} + else + temp_goal = self._path[2] + if self:pos_in_box({x = temp_goal.x, y = pos.y + self.height * 0.5, z = temp_goal.z}) then + table.remove(self._path, 1) + end + end + goal.y = pos.y + 0.5 + local dir = vector.direction(self.object:get_pos(), pos2) + local tyaw = minetest.dir_to_yaw(dir) + local turn_rate = self.turn_rate or 10 + if temp_goal then + dir = vector.direction(self.object:get_pos(), temp_goal) + tyaw = minetest.dir_to_yaw(dir) + if #self._path < 2 + and not self:is_pos_safe(temp_goal) then + self:animate("walk") + self:set_forward_velocity(0) + self:halt() + return + end + end + self:turn_to(tyaw, turn_rate) + self:animate("walk") + self:set_gravity(-9.8) + self:set_forward_velocity(speed or 2) + if self:pos_in_box(pos2) then + self:halt() + end +end + +creatura.register_movement_method("creatura:pathfind", movement_pathfind) + +-- Obstacle Avoidance + +local function moveable(pos, width, height) + local pos1 = { + x = pos.x - (width + 0.2), + y = pos.y, + z = pos.z - (width + 0.2), + } + local pos2 = { + x = pos.x + (width + 0.2), + y = pos.y, + z = pos.z + (width + 0.2), + } + for z = pos1.z, pos2.z do + for x = pos1.x, pos2.x do + local pos3 = {x = x, y = (pos.y + height), z = z} + local pos4 = {x = x, y = pos.y, z = z} + local ray = minetest.raycast(pos3, pos4, false, false) + for pointed_thing in ray do + if pointed_thing.type == "node" then + return false + end + end + end + end + return true +end + +local function get_obstacle_avoidance(self, pos2) + local pos = self.object:get_pos() + local yaw = minetest.dir_to_yaw(vec_dir(pos, pos2)) + pos.y = pos.y + self.stepheight + local height = self.height + local width = self.width + local outset = vec_center(vec_add(pos, vec_multi(yaw2dir(yaw), width + 0.2))) + local pos2 + if not moveable(outset, width, height) then + yaw = self.object:get_yaw() + for i = 1, 89, 45 do + angle = rad(i) + dir = vec_multi(yaw2dir(yaw + angle), width + 0.2) + pos2 = vec_center(vec_add(pos, dir)) + if moveable(pos2, width, height) then + break + end + angle = -rad(i) + dir = vec_multi(yaw2dir(yaw + angle), width + 0.2) + pos2 = vec_center(vec_add(pos, dir)) + if moveable(pos2, width, height) then + break + end + end + end + return pos2 +end + +local function movement_obstacle_avoidance(self, pos2, speed) + local pos = self.object:get_pos() + local temp_goal = self._movement_data.temp_goal + if not temp_goal + or self:pos_in_box(temp_goal) then + self._movement_data.temp_goal = get_obstacle_avoidance(self, pos2) + temp_goal = self._movement_data.temp_goal + if temp_goal then + temp_goal.y = floor(pos.y + self.height * 0.5) + end + end + pos2.y = floor(pos2.y + 0.5) + local dir = vector.direction(pos, pos2) + local tyaw = minetest.dir_to_yaw(dir) + local turn_rate = self.turn_rate or 10 + if temp_goal then + dir = vector.direction(pos, temp_goal) + tyaw = minetest.dir_to_yaw(dir) + end + local turn_diff = abs(diff(self.object:get_yaw(), tyaw)) + self:turn_to(tyaw, turn_rate) + self:animate("walk") + self:set_gravity(-9.8) + self:set_forward_velocity(speed - clamp(turn_diff, 0, speed * 0.66)) + if self:pos_in_box({x = pos2.x, y = pos.y + 0.1, z = pos2.z}) + or (temp_goal + and not self:is_pos_safe(temp_goal)) then + self:halt() + end +end + +creatura.register_movement_method("creatura:obstacle_avoidance", movement_obstacle_avoidance) + +-- Neighbors + +local function movement_neighbors(self, pos2, speed) + local pos = self.object:get_pos() + local temp_goal = self._movement_data.temp_goal + local width = clamp(self.width, 0.5, 1.5) + if not temp_goal + or self:pos_in_box(temp_goal) then + self._movement_data.temp_goal = creatura.get_next_move(self, pos2) + temp_goal = self._movement_data.temp_goal + end + pos2.y = pos.y + self.height * 0.5 + local yaw = self.object:get_yaw() + local dir = vector.direction(self.object:get_pos(), pos2) + local tyaw = minetest.dir_to_yaw(dir) + local turn_rate = self.turn_rate or 10 + if temp_goal then + temp_goal.x = math.floor(temp_goal.x + 0.5) + temp_goal.z = math.floor(temp_goal.z + 0.5) + temp_goal.y = pos.y + self.height * 0.5 + dir = vector.direction(self.object:get_pos(), temp_goal) + tyaw = minetest.dir_to_yaw(dir) + if not self:is_pos_safe(temp_goal) then + self:set_forward_velocity(0) + self:halt() + return + end + end + local yaw_diff = abs(diff(yaw, tyaw)) + self:turn_to(tyaw, turn_rate) + self:set_gravity(-9.8) + if yaw_diff < pi then + self:animate("walk") + self:set_forward_velocity(speed) + end + if self:pos_in_box(pos2) then + self:halt() + end +end + +creatura.register_movement_method("creatura:neighbors", movement_neighbors) + diff --git a/mob_meta.lua b/mob_meta.lua new file mode 100644 index 0000000..3179e77 --- /dev/null +++ b/mob_meta.lua @@ -0,0 +1,1244 @@ +-------------- +-- Mob Meta -- +-------------- + +-- Math -- + +local pi = math.pi +local pi2 = pi * 2 +local abs = math.abs +local floor = math.floor +local random = math.random + +local sin = math.sin +local cos = math.cos +local atan2 = math.atan2 + +local function diff(a, b) -- Get difference between 2 angles + return math.atan2(math.sin(b - a), math.cos(b - a)) +end + +local function round(n, dec) + local x = 10^(dec or 0) + return math.floor(n * x + 0.5) / x +end + +local vec_dir = vector.direction +local vec_dist = vector.distance +local vec_multi = vector.multiply +local vec_sub = vector.subtract +local vec_add = vector.add +local vec_normal = vector.normalize + +local function vec_center(v) + return {x = floor(v.x + 0.5), y = floor(v.y + 0.5), z = floor(v.z + 0.5)} +end + +local function vec_raise(v, n) + return {x = v.x, y = v.y + n, z = v.z} +end + +local function vec_compress(v) + return {x = round(v.x, 2), y = round(v.y, 2), z = round(v.z, 2)} +end + +local function dist_2d(pos1, pos2) + local a = vector.new(pos1.x, 0, pos1.z) + local b = vector.new(pos2.x, 0, pos2.z) + return vec_dist(a, b) +end + +local function fast_ray_sight(pos1, pos2) + local ray = minetest.raycast(pos1, pos2, false, false) + for pointed_thing in ray do + if pointed_thing.type == "node" then + return false + end + end + return true +end + +local step_tick = 0.15 + +-- Local Utilities -- + +local function get_node_def(name) + return minetest.registered_nodes[name] +end + +local function get_ground_level(pos2, max_height) + local node = minetest.get_node(pos2) + local node_under = minetest.get_node({ + x = pos2.x, + y = pos2.y - 1, + z = pos2.z + }) + local height = 0 + local walkable = get_node_def(node_under).walkable and not get_node_def(node).walkable + if walkable then + return pos2 + elseif not walkable then + if not get_node_def(node_under).walkable then + while not get_node_def(node_under).walkable + and height < max_height do + pos2.y = pos2.y - 1 + node_under = minetest.get_node({ + x = pos2.x, + y = pos2.y - 1, + z = pos2.z + }) + height = height + 1 + end + else + while get_node_def(node).walkable + and height < max_height do + pos2.y = pos2.y + 1 + node = minetest.get_node(pos2) + height = height + 1 + end + end + return pos2 + end +end + +------------------------- +-- Physics/Vitals Tick -- +------------------------- + +minetest.register_globalstep(function(dtime) + if step_tick <= 0 then + step_tick = 0.15 + end + step_tick = step_tick - dtime +end) + +-- A metatable is used to avoid issues +-- With mobs performing functions outside +-- their own scope + +local mob = { + -- Stats + max_health = 20, + armor_groups = {fleshy = 100}, + damage = 2, + speed = 4, + tracking_range = 16, + despawn_after = 100, + -- Physics + max_fall = 3, + stepheight = 1.1, + hitbox = { + width = 0.5, + height = 1 + }, +} + +local mob_meta = {__index = mob} + +local function index_box_border(self) + local width = self.width + local pos = self.object:get_pos() + pos.y = pos.y + 0.5 + local pos1 = { + x = pos.x - (width + 0.7), + y = pos.y, + z = pos.z - (width + 0.7), + } + local pos2 = { + x = pos.x + (width + 0.7), + y = pos.y, + z = pos.z + (width + 0.7), + } + local border = {} + for z = pos1.z, pos2.z do + for x = pos1.x, pos2.x do + local vec = { + x = x, + y = pos.y, + z = z + } + if not self:pos_in_box(vec, width) then + table.insert(border, vec_sub(vec, pos)) + end + end + end + return border +end + +local function indicate_damage(self) + self.object:set_texture_mod("^[colorize:#FF000040") + core.after(0.2, function() + if creatura.is_alive(self) then + self.object:set_texture_mod("") + end + end) +end + +-- Set Movement Data + +function mob:move(pos, method, speed_factor, anim) + self._movement_data.goal = pos + self._movement_data.method = method + self._movement_data.last_neighbor = nil + self._movement_data.gravity = self._movement_data.gravity or -9.8 + self._movement_data.speed = (self.speed or 2) * (speed_factor or 1) + if anim then + self._movement_data.anim = anim + end +end + +-- Clear Movement Data + +function mob:halt() + self._movement_data = { + goal = nil, + method = nil, + last_neighbor = nil, + gravity = self._movement_data.gravity or -9.8, + speed = 0 + } + self._path_data = {} +end + +-- Turn to specified yaw + +function mob:turn_to(tyaw, rate) + self._tyaw = tyaw + local weight = rate or 10 + local yaw = self.object:get_yaw() + + yaw = yaw + pi + tyaw = (tyaw + pi) % pi2 + + local step = math.min(self.dtime * weight, abs(tyaw - yaw) % pi2) + + local dir = abs(tyaw - yaw) > pi and -1 or 1 + dir = tyaw > yaw and dir * 1 or dir * -1 + + local nyaw = (yaw + step * dir) % pi2 + self.object:set_yaw(nyaw - pi) + self.last_yaw = self.object:get_yaw() +end + +-- Set Gravity (default of -9.8) + +function mob:set_gravity(gravity) + self._movement_data.gravity = gravity or -9.8 +end + +-- Sets Velocity to desired speed in mobs current look direction + +function mob:set_forward_velocity(speed) + local speed = speed or self._movement_data.speed + local dir = minetest.yaw_to_dir(self.object:get_yaw()) + local vel = vec_multi(dir, speed) + vel.y = self.object:get_velocity().y + self.object:set_velocity(vel) +end + +-- Sets Velocity on y axis + +function mob:set_vertical_velocity(speed) + local vel = self.object:get_velocity() or {x = 0, y = 0, z = 0} + vel.y = speed + self.object:set_velocity(vel) +end + +-- Applies knockback in 'dir' + +function mob:apply_knockback(dir, power) + if not dir then return end + power = power or 6 + if not self.touching_ground then + power = power * 0.8 + end + local knockback = vec_multi(dir, power) + knockback.y = abs(power * 0.22) + self.object:add_velocity(knockback) +end + +-- Punch 'target' + +function mob:punch_target(target) -- + target:punch(self.object, 1.0, { + full_punch_interval = 1.0, + damage_groups = {fleshy = self.damage or 5}, + }) +end + +-- Apply damage to mob + +function mob:hurt(health) + if self.protected then return end + self.hp = self.hp - math.ceil(health) +end + +-- Add HP to mob + +function mob:heal(health) + if self.protected then return end + self.hp = self.hp + math.ceil(health) + if self.hp > self.max_health then + self.hp = self.max_health + end +end + +-- Return position at center of mobs hitbox + +function mob:get_center_pos() + return vec_raise(self.object:get_pos(), self.height * 0.5 or 0.5) +end + +-- Return true if position is within box + +function mob:pos_in_box(pos, size) + if not pos then return false end + local center = self:get_center_pos() + local width = size or self.width + local height = size or (self.height * 0.5) + if not size + and self.width < 0.5 then + width = 0.5 + end + local edge_a = { + x = center.x - width, + y = center.y - height, + z = center.z - width + } + local edge_b = { + x = center.x + width, + y = center.y + height, + z = center.z + width + } + local minp, maxp = vector.sort(edge_a, edge_b) + if pos.x >= minp.x + and pos.y >= minp.y + and pos.z >= minp.z + and pos.x <= maxp.x + and pos.y <= maxp.y + and pos.z <= maxp.z then + return true + end + return false +end + +-- Terrain Navigation -- + +function mob:get_wander_pos(min_range, max_range, max_vert) + local outset = random(min_range, max_range) + local move_dir = { + x = random(-10, 10) * 0.1, + z = 0, + y = random(-10, 10) * 0.1 + } + local pos2 = get_ground_level(vec_add(pos, vec_multi(vec_normal(move_dir), outset)), max_vert or min_range) + for i = 2, outset do + local out_pos = vec_add(pos, vec_multi(vec_normal(move_dir), i)) + if minetest.registered_nodes[minetest.get_node(out_pos).name].walkable + or not minetest.registered_nodes[minetest.get_node(vec_raise(pos2, -1)).name].walkable then + break + end + pos2 = out_pos + end + return pos2 +end + +function mob:get_wander_pos_3d(min_range, max_range) + local pos = self.object:get_pos() + local outset = random(min_range, max_range) + local move_dir = { + x = random(-10, 10) * 0.1, + z = random(-10, 10) * 0.1, + y = random(-10, 10) * 0.1 + } + local pos2 = vec_add(pos, vec_multi(vec_normal(move_dir), 1)) + local fail_safe = 0 + while fail_safe < 4 + and minetest.registered_nodes[minetest.get_node(pos2).name].walkable do + move_dir = { + x = random(-10, 10) * 0.1, + z = random(-10, 10) * 0.1, + y = random(-10, 10) * 0.1 + } + pos2 = vec_add(pos, vec_multi(vec_normal(move_dir), 1)) + end + for i = 2, outset do + local out_pos = vec_add(pos, vec_multi(vec_normal(move_dir), i)) + if minetest.registered_nodes[minetest.get_node(out_pos).name].walkable then + break + end + pos2 = out_pos + end + return pos2 +end + +function mob:is_pos_safe(pos) + local mob_pos = self.object:get_pos() + local node = minetest.get_node(pos) + if not node then return false end + if minetest.get_item_group(node.name, "igniter") > 0 + or minetest.registered_nodes[node.name].drawtype == "liquid" then return false end + local fall_safe = false + if self.max_fall ~= 0 then + for i = 1, self.max_fall or 3 do + local fall_pos = { + x = pos.x, + y = floor(mob_pos.y + 0.5) - i, + z = pos.z + } + if minetest.registered_nodes[minetest.get_node(fall_pos).name].walkable then + fall_safe = true + break + end + end + else + fall_safe = true + end + return fall_safe +end + +-- Set mobs animation (if specified animation isn't already playing) + +function mob:animate(animation) + if not animation + or not self.animations[animation] then return end + if not self._anim + or self._anim ~= animation then + local anim = self.animations[animation] + self.object:set_animation(anim.range, anim.speed, anim.frame_blend, anim.loop) + self._anim = animation + end +end + +-- Set texture to variable at 'id' index in 'tbl' or 'textures' + +function mob:set_texture(id, tbl) + local _table = self.textures + if tbl then + _table = tbl + end + if not _table + or not _table[id] then + return + end + self.object:set_properties({ + textures = {_table[id]} + }) + return _table[id] +end + +-- Set scale to base scale times 'x' and update bordering positions + +function mob:set_scale(x) + local def = minetest.registered_entities[self.name] + local scale = def.visual_size + local box = def.collisionbox + local new_box = {} + for k, v in ipairs(box) do + new_box[k] = v * x + end + self.object:set_properties({ + visual_size = { + x = scale.x * x, + y = scale.y * x + }, + collisionbox = new_box + }) + self._border = index_box_border(self) +end + +-- Fixes mob scale being changed when attached to a parent + +function mob:fix_attached_scale(parent) + local scale = self:get_visual_size() + local parent_size = parent:get_properties().visual_size + self.object:set_properties({ + visual_size = { + x = scale.x / parent_size.x, + y = scale.y / parent_size.y + }, + }) +end + +-- Add sets 'id' to 'val' in permanent data + +function mob:memorize(id, val) + self.perm_data[id] = val + return self.perm_data[id] +end + +-- Remove 'id' from permanent data + +function mob:forget(id) + self.perm_data[id] = nil +end + +-- Return value from 'id' in permanent data + +function mob:recall(id) + return self.perm_data[id] +end + +-- Return true on interval specified by 'n' + +function mob:timer(n) + local t1 = floor(self.active_time) + local t2 = floor(self.active_time + self.dtime) + if t2 > t1 and t2%n == 0 then return true end +end + +-- Play 'sound' from self.sounds + +function mob:play_sound(sound) + local spec = self.sounds and self.sounds[sound] + local parameters = {object = self.object} + + if type(spec) == "table" then + local name = spec.name + if spec.variations then + name = name .. "_" .. random(spec.variations) + end + local pitch = 1.0 + + pitch = pitch - (random(-10, 10) * 0.005) + + parameters.gain = spec.gain or 1 + parameters.max_hear_distance = spec.distance or 8 + parameters.fade = spec.fade or 1 + parameters.pitch = pitch + return minetest.sound_play(name, parameters) + end + return minetest.sound_play(spec, parameters) +end + +-- Return current collisionbox + +function mob:get_hitbox() + if not self.object:get_properties() then return self.collisionbox end + return self.object:get_properties().collisionbox +end + +-- Return height of current collisionbox + +function mob:get_height() + local hitbox = self:get_hitbox() + return hitbox[5] - hitbox[2] +end + +-- Return current visual size + +function mob:get_visual_size() + if not self.object:get_properties() then return end + return self.object:get_properties().visual_size +end + +local function is_group_in_table(tbl, name) + for _, v in pairs(tbl) do + if minetest.get_item_group(name, v:split(":")[2]) > 0 then + return true + end + end + return false +end + +function mob:follow_wielded_item(player) + if not player + or not self.follow then return end + local item = player:get_wielded_item() + local name = item:get_name() + if type(self.follow) == "string" + and (name == self.follow + or minetest.get_item_group(name, self.follow:split(":")[2]) > 0) then + return item, name + end + if type(self.follow) == "table" + and (is_value_in_table(self.follow, name) + or is_group_in_table(self.follow, name)) then + return item, name + end +end + +function mob:get_target(target) + local alive = creatura.is_alive(target) + if not alive then + return false, false, nil + end + if type(target) == "table" then + target = target.object + end + local pos = self:get_center_pos() + local tpos = target:get_pos() + tpos.y = floor(tpos.y + 0.5) + local line_of_sight = fast_ray_sight(pos, tpos) + return true, line_of_sight, tpos +end + +-- Actions + +function mob:set_action(func) + self._action = func +end + +function mob:get_action() + if type(self._action) ~= "table" then + return self._action + end + return nil +end + +function mob:clear_action() + self._action = {} +end + +function mob:force_behavior(func) + self._bh_tree_data = { + active_task = #self.bh_tree + 1, + func = func, + current_selection = 1 + } +end + +function mob:set_behavior(func) + self._bh_tree_data = { + active_task = #self.bh_tree + 1, + func = func, + current_selection = 1 + } +end + +function mob:clear_behavior(func) + self._bh_tree_data = { + active_task = 0, + func = nil, + current_selection = 0 + } +end + +function mob:set_utility(func) + self._utility_data.func = func +end + +function mob:get_utility() + return self._utility_data.utility +end + +function mob:initiate_utility(utility, ...) + local func = creatura.registered_utilities[utility] + if not func then return end + self._utility_data.utility = utility + self:clear_action() + func(...) +end + +function mob:set_utility_score(n) + self._utility_data.score = n or 0 +end + +-- Functions + +function mob:activate(staticdata, dtime) + self.width = self:get_hitbox()[4] or 0.5 + self.height = self:get_height() or 1 + self._tyaw = self.object:get_yaw() + self.last_yaw = self.object:get_yaw() + self.active_time = 0 + self.in_liquid = false + self.is_falling = false + self.touching_ground = false + + -- Backend Data (Should not be modified unless modder knows what they're doing) + self._movement_data = { + goal = nil, + method = nil, + last_neighbor = nil, + gravity = -9.8, + speed = 0 + } + self._path_data = {} + self._path = {} + self._task = {} + self._action = {} + + local pos = self.object:get_pos() + local node = minetest.get_node(pos) + + if node + and minetest.get_item_group(node.name, "liquid") > 0 then + self.in_liquid = node.name + end + + -- Staticdata + if staticdata then + local data = minetest.deserialize(staticdata) + if data then + for k, v in pairs(data) do + self[k] = v + end + end + end + + -- Initialize Stats and Visuals + if not self.textures then + local textures = self.object:get_properties().textures + if textures then self.textures = textures end + end + + if not self.perm_data then + if self.memory then + self.perm_data = self.memory + else + self.perm_data = {} + end + if #self.textures > 1 then self.texture_no = random(#self.textures) end + end + + if self:recall("despawn_after") ~= nil then + self.despawn_after = self:recall("despawn_after") + end + self._despawn = self:recall("_despawn") or false + + if self._despawn + and self.despawn_after then + self.object:remove() + return + end + + self._breath = self:recall("_breath") or (self.max_breath or 30) + self._border = index_box_border(self) + + if self.textures + and self.texture_no then + self:set_texture(self.texture_no, self.textures) + end + + self.max_health = self.max_health or 10 + self.hp = self.hp or self.max_health + + if type(self.armor_groups) ~= "table" then + self.armor_groups = {} + end + self.armor_groups.immortal = 1 + self.object:set_armor_groups(self.armor_groups) + + if self.timer + and type(self.timer) == "number" then -- fix crash for converted mobs_redo mobs + self.timer = function(self, n) + local t1 = floor(self.active_time) + local t2 = floor(self.active_time + self.dtime) + if t2 > t1 and t2%n == 0 then return true end + end + end + + if self.activate_func then + self:activate_func(self, staticdata, dtime) + end +end + +function mob:staticdata() + local data = {} + data.perm_data = self.perm_data + data.hp = self.hp or self.max_health + data.texture_no = self.texture_no or random(#self.textures) + return minetest.serialize(data) +end + + +local function average(t) + local sum = 0 + for _,v in pairs(t) do -- Get the sum of all numbers in t + sum = sum + v + end + return sum / #t +end + +function mob:on_step(dtime, moveresult) + --local us_time = minetest.get_us_time() + if not self.hp then return end + self.dtime = dtime or 0.09 + self.moveresult = moveresult or {} + self.touching_ground = false + if moveresult then + self.touching_ground = moveresult.touching_ground + end + if step_tick <= 0 then + -- Physics and Vitals + if self._physics then + self:_physics(moveresult) + end + if self._vitals then + self:_vitals() + end + -- Cached Geometry + self.width = self:get_hitbox()[4] or 0.5 + self.height = self:get_height() or 1 + end + self:_light_physics() + -- Movement Control + if self._move then + self:_move() + end + -- Execute Mobkit-like Logic + self:_execute_logic() + -- Execute Task Tree + if self.bh_tree + and self._execute_bh_tree then + self:_execute_bh_tree() + end + if self.utility_stack + and self._execute_utilities then + self:_execute_utilities() + end + -- Die + if self.hp <= 0 + and self.death_func then + self:death_func() + self:halt() + return + end + if self.step_func + and self.perm_data then + self:step_func(dtime, moveresult) + end + self.active_time = self.active_time + dtime + if self.despawn_after + and self.active_time >= self.despawn_after then + self._despawn = self:memorize("_despawn", true) + end +end + +function mob:on_deactivate() + self._task = {} + self._action = {} + if self.deactivate_func then + self:deactivate_func(self) + end +end + +---------------- +-- Object API -- +---------------- + +local fancy_step = false + +local step_type = minetest.settings:get("creatura_step_type") + +if step_type == "fancy" then + fancy_step = true +end + +-- Physics + +local function do_step(self) + if not fancy_step then return end + local pos = self.object:get_pos() + local vel = self.object:get_velocity() + if not self._step then + if self.touching_ground + and abs(vel.x + vel.z) > 0 then + local border = self._border + local yaw_offset = vec_add(pos, vec_multi(minetest.yaw_to_dir(self.object:get_yaw()), self.width + 0.7)) + table.sort(border, function(a, b) return vec_dist(vec_add(pos, a), yaw_offset) < vec_dist(vec_add(pos, b), yaw_offset) end) + local step_pos = vec_center(vec_add(pos, border[1])) + local halfway = vec_add(pos, vec_multi(vec_dir(pos, step_pos), 0.5)) + halfway.y = step_pos.y + if creatura.get_node_def(step_pos).walkable + and abs(diff(self.object:get_yaw(), minetest.dir_to_yaw(vec_dir(pos, step_pos)))) < 1.5 + and moveable(halfway, self.width, self.height) then + self._step = vec_center(step_pos) + end + end + else + local vel = self.object:get_velocity() + self.object:set_velocity(vector.new(vel.x, 7, vel.z)) + if self._step.y < pos.y - 0.5 then + self.object:set_velocity(vector.new(vel.x, 0.5, vel.z)) + self._step = nil + local step_pos = self.object:get_pos() + local dir = minetest.yaw_to_dir(self.object:get_yaw()) + step_pos = vec_add(step_pos, vec_multi(dir, 0.1)) + self.object:set_pos(step_pos) + end + end +end + +local function collision_detection(self) + if not creatura.is_alive(self) then return end + local pos = self.object:get_pos() + local width = self.width + 0.25 + local objects = minetest.get_objects_in_area(vec_sub(pos, width), vec_add(pos, width)) + if #objects < 2 then return end + local col_no = 0 + for i = 2, #objects do + local object = objects[i] + if creatura.is_alive(object) + and (not object:get_attach() + or object:get_attach() ~= self.object) then + if i > 5 then break end + local pos2 = object:get_pos() + local dir = vec_dir(pos, pos2) + dir.y = 0 + if dir.x == 0 and dir.z == 0 then + dir = vector.new(random(-1, 1) * random(), 0, + random(-1, 1) * random()) + end + local velocity = vec_multi(dir, 1.1) + local vel1 = vec_multi(velocity, -1) + local vel2 = velocity + self.object:add_velocity(vel1) + object:add_velocity(vel2) + end + end +end + +local function water_physics(self) + -- Props + local gravity = self._movement_data.gravity + local height = self.height + -- Vectors + local floor_pos = self.object:get_pos() + floor_pos.y = floor_pos.y + 0.01 + local surface_pos = floor_pos + local floor_node = minetest.get_node(floor_pos) + local surface_node = minetest.get_node(surface_pos) + if minetest.get_item_group(floor_node.name, "liquid") < 1 then + self.object:set_acceleration({ + x = 0, + y = gravity, + z = 0 + }) + if self.in_liquid then + self.in_liquid = false + end + return + end + self.in_liquid = floor_node.name + -- Get submergence (Not the most accurate, but reduces lag) + for i = 1, math.ceil(height * 3) do + local step_pos = { + x = floor_pos.x, + y = floor_pos.y + 0.5 * i, + z = floor_pos.z + } + if minetest.get_item_group(minetest.get_node(step_pos).name, "liquid") > 0 then + surface_pos = step_pos + else + break + end + end + -- Apply Physics + local submergence = surface_pos.y - floor_pos.y + local vel = self.object:get_velocity() + local bouyancy = self.bouyancy_multiplier or 1 + self.object:set_acceleration({ + x = 0, + y = (submergence - vel.y * abs(vel.y) * 0.4) * bouyancy, + z = 0 + }) + local hydrodynamics = self.hydrodynamics_multiplier or 0.7 + local vel_y = vel.y + if self.bouyancy_multiplier == 0 then -- if bouyancy is disabled drag will be applied to keep awuatic mobs from drifting + vel_y = vel.y * hydrodynamics + end + self.object:set_velocity({ + x = vel.x * hydrodynamics, + y = vel_y, + z = vel.z * hydrodynamics + }) +end + +function mob:_physics(moveresult) + if not self.object then return end + water_physics(self) + -- Step up nodes + do_step(self, moveresult) + -- Object collision + collision_detection(self) + if not self.in_liquid + and not self.touching_ground then + self.is_falling = true + else + self.is_falling = false + end + if not self.in_liquid + and self._movement_data.gravity ~= 0 then + local vel = self.object:get_velocity() + if self.touching_ground then + local nvel = vector.multiply(vel, 0.2) + if nvel.x < 0.2 + and nvel.z < 0.2 then + nvel.x = 0 + nvel.z = 0 + end + nvel.y = vel.y + self.object:set_velocity(nvel) + else + local nvel = vector.multiply(vel, 0.1) + if nvel.x < 0.2 + and nvel.z < 0.2 then + nvel.x = 0 + nvel.z = 0 + end + nvel.y = vel.y + self.object:set_velocity(nvel) + end + end +end + +function mob:_light_physics() -- physics that are lightweight enough to be called each step +end + +-- Movement Control + +function mob:_move() + if not self.object then return end + local data = self._movement_data + local speed = data.speed + if data.goal then + local pos = data.goal + local method = data.method + local anim = data.anim + if creatura.registered_movement_methods[method] then + local func = creatura.registered_movement_methods[method] + func(self, pos, speed, anim) + end + end +end + +-- Execute Tasks and Actions + +function mob:_execute_logic() + if not self.object then return end + local task = self._task + if #self._task > 0 then + local func = self._task[#self._task].func + if func(self) then + self._task[#self._task] = nil + self:clear_action() + return + end + end + local action = self._action + if type(action) ~= "table" then + local func = action + if func(self) then + self:clear_action() + end + end +end + +function mob:_execute_bh_tree(immediate) + if not self._bh_tree_data then + self._bh_tree_data = { + active_task = 0, + func = nil, + current_selection = 0 + } + end + if (self:timer(self.task_timer or 1) + or immediate + or self._bh_tree_data.func == nil) then + for i = #self.bh_tree, 1, -1 do + local active_task_no = self._bh_tree_data.active_task + if i >= active_task_no then + local selector = self.bh_tree[i].selector + local selection, args = selector(self) + if selection + and (args + or selection < 1) then + if selection < 1 + and i == active_task_no then + self._bh_tree_data = { + active_task = 0, + func = nil, + current_selection = 0 + } + self:clear_action() + elseif selection > 0 + and ((i > active_task_no + or selection ~= self._bh_tree_data.current_selection) + or active_task_no == 0) then + self._bh_tree_data.active_task = i + self._bh_tree_data.current_selection = selection + self:clear_action() + local func = self.bh_tree[i][selection] + if not self.object:get_pos() then return end + func(unpack(args)) + break + end + end + end + end + end + if self._bh_tree_data.func then + local func = self._bh_tree_data.func + if func(self) then + self._bh_tree_data = { + active_task = 0, + func = nil, + current_selection = 0 + } + self:clear_action() + end + end +end + +local function tbl_equals(tbl1, tbl2) + local match = true + for k, v in pairs(tbl1) do + if not tbl2[k] + and tbl2[k] ~= v then + match = false + break + end + end + return match +end + +function mob:_execute_utilities() + local is_alive = self.hp > 0 + if not self._utility_data then + self._utility_data = { + utility = nil, + func = nil, + score = 0 + } + end + local loop_data = { + utility = nil, + func = nil, + score = 0 + } + if (self:timer(self.task_timer or 1) + or not self._utility_data.func) + and is_alive then + for i = 1, #self.utility_stack do + local utility = self.utility_stack[i].utility + local get_score = self.utility_stack[i].get_score + local score, args = get_score(self) + if score > 0 + and score >= self._utility_data.score + and score >= loop_data.score then + loop_data = { + utility = utility, + score = score, + args = args + } + end + end + end + if loop_data.utility + and loop_data.args then + local no_data = not self._utility_data.utility and not self._utility_data.args + local new_util = self._utility_data.utility ~= loop_data.utility or not tbl_equals(self._utility_data.args, loop_data.args) + if no_data + or new_util then -- if utilities are different or utilities are the same and args are different set new data + self._utility_data = loop_data + end + end + if self._utility_data.utility then + if not self._utility_data.func then + self:initiate_utility(self._utility_data.utility, unpack(self._utility_data.args)) + end + local func = self._utility_data.func + if not func then return end + if func(self) then + self._utility_data = { + utility = nil, + func = nil, + score = 0 + } + self:clear_action() + end + end +end + +-- Vitals + +function mob:_vitals() + local stand_pos = self.object:get_pos() + local fall_start = self._fall_start + if self.is_falling + and not fall_start + and self.max_fall > 0 then + self._fall_start = stand_pos.y + elseif fall_start + and self.max_fall > 0 then + if self.touching_ground + and not self.in_liquid then + local damage = fall_start - stand_pos.y + if damage < (self.max_fall or 3) then + self._fall_start = nil + return + end + local resist = self.fall_resistance or 0 + self:hurt(damage - (damage * (resist * 0.1))) + indicate_damage(self) + if random(4) < 2 then + self:play_sound("hurt") + end + self._fall_start = nil + elseif self.in_liquid then + self._fall_start = nil + end + end + if self:timer(1) then + local head_pos = vec_raise(stand_pos, self.height) + local head_def = minetest.registered_nodes[minetest.get_node(head_pos).name] + if head_def.drawtype == "liquid" + and minetest.get_item_group(minetest.get_node(head_pos), "water") > 0 then + if self._breath <= 0 then + self:hurt(1) + indicate_damage(self) + if random(4) < 2 then + self:play_sound("hurt") + end + else + self._breath = self._breath - 1 + self:memorize("_breath", self._breath) + end + end + local stand_def = minetest.registered_nodes[minetest.get_node(stand_pos).name] + if minetest.get_item_group(minetest.get_node(stand_pos), "fire") > 0 + and stand_def.damage_per_second then + local damage = stand_def.damage_per_second + local resist = self.fire_resistance or 0 + self:hurt(damage - (damage * (resist * 0.1))) + indicate_damage(self) + if random(4) < 2 then + self:play_sound("hurt") + end + end + end + if self:timer(5) then + local objects = minetest.get_objects_inside_radius(stand_pos, 0.2) + if #objects > 10 then + indicate_damage(self) + self.hp = self:memorize("hp", -1) + self:death_func() + end + end +end + +function creatura.register_mob(name, def) + local box_width = def.hitbox and def.hitbox.width or 0.5 + local box_height = def.hitbox and def.hitbox.height or 1 + local hitbox = {-box_width, 0, -box_width, box_width, box_height, box_width} + + def.physical = def.physical or true + def.collide_with_objects = def.collide_with_objects or false + def.visual = "mesh" + def.makes_footstep_sound = def.makes_footstep_sound or false + def.static_save = true + def.collisionbox = hitbox + def._creatura_mob = true + + def.on_activate = function(self, staticdata, dtime) + return self:activate(staticdata, dtime) + end + + def.get_staticdata = function(self) + return self:staticdata(self) + end + + minetest.register_entity(name, setmetatable(def, mob_meta)) +end \ No newline at end of file diff --git a/mod.conf b/mod.conf new file mode 100644 index 0000000..e126b90 --- /dev/null +++ b/mod.conf @@ -0,0 +1,2 @@ +name = creatura +description = A performant, semi-modular mob API \ No newline at end of file diff --git a/pathfinder.lua b/pathfinder.lua new file mode 100644 index 0000000..4fddf62 --- /dev/null +++ b/pathfinder.lua @@ -0,0 +1,610 @@ +----------------- +-- Pathfinding -- +----------------- + +local a_star_alloted_time = tonumber(minetest.settings:get("creatura_a_star_alloted_time")) or 500 +local theta_star_alloted_time = tonumber(minetest.settings:get("creatura_theta_star_alloted_time")) or 700 + +local floor = math.floor +local abs = math.abs + +local function is_node_walkable(name) + local def = minetest.registered_nodes[name] + return def and def.walkable +end + +local function is_node_liquid(name) + local def = minetest.registered_nodes[name] + return def and def.drawtype == "liquid" +end + +local function moveable(pos, width, height) + local pos1 = { + x = pos.x - (width + 0.2), + y = pos.y, + z = pos.z - (width + 0.2), + } + local pos2 = { + x = pos.x + (width + 0.2), + y = pos.y, + z = pos.z + (width + 0.2), + } + for z = pos1.z, pos2.z do + for x = pos1.x, pos2.x do + local pos3 = {x = x, y = pos.y + height, z = z} + local pos4 = {x = x, y = pos.y, z = z} + local ray = minetest.raycast(pos3, pos4, false, false) + for pointed_thing in ray do + if pointed_thing.type == "node" then + return false + end + end + end + end + return true +end + +local function get_ground_level(pos2, max_height) + local node = minetest.get_node(pos2) + local node_under = minetest.get_node({ + x = pos2.x, + y = pos2.y - 1, + z = pos2.z + }) + local height = 0 + local walkable = is_node_walkable(node_under.name) and not is_node_walkable(node.name) + if walkable then + return pos2 + elseif not walkable then + if not is_node_walkable(node_under.name) then + while not is_node_walkable(node_under.name) + and height < max_height do + pos2.y = pos2.y - 1 + node_under = minetest.get_node({ + x = pos2.x, + y = pos2.y - 1, + z = pos2.z + }) + height = height + 1 + end + else + while is_node_walkable(node.name) + and height < max_height do + pos2.y = pos2.y + 1 + node = minetest.get_node(pos2) + height = height + 1 + end + end + return pos2 + end +end + +local function get_distance(start_pos, end_pos) + local distX = abs(start_pos.x - end_pos.x) + local distZ = abs(start_pos.z - end_pos.z) + + if distX > distZ then + return 14 * distZ + 10 * (distX - distZ) + else + return 14 * distX + 10 * (distZ - distX) + end +end + +local function get_distance_to_neighbor(start_pos, end_pos) + local distX = abs(start_pos.x - end_pos.x) + local distY = abs(start_pos.y - end_pos.y) + local distZ = abs(start_pos.z - end_pos.z) + + if distX > distZ then + return (14 * distZ + 10 * (distX - distZ)) * (distY + 1) + else + return (14 * distX + 10 * (distZ - distX)) * (distY + 1) + end +end + +local function is_on_ground(pos) + local ground = { + x = pos.x, + y = pos.y - 1, + z = pos.z + } + if is_node_walkable(minetest.get_node(ground).name) then + return true + end + return false +end + +local function vec_raise(v, n) + return {x = v.x, y = v.y + n, z = v.z} +end + +-- Find a path from start to goal + +function creatura.find_path(self, start, goal, obj_width, obj_height, max_open, climb, fly, swim) + climb = climb or false + fly = fly or false + swim = swim or false + + start = self._path_data.start or start + + self._path_data.start = start + + local path_neighbors = { + {x = 1, y = 0, z = 0}, + {x = 1, y = 0, z = 1}, + {x = 0, y = 0, z = 1}, + {x = -1, y = 0, z = 1}, + {x = -1, y = 0, z = 0}, + {x = -1, y = 0, z = -1}, + {x = 0, y = 0, z = -1}, + {x = 1, y = 0, z = -1} + } + + if climb then + table.insert(path_neighbors, {x = 0, y = 1, z = 0}) + end + + if fly + or swim then + path_neighbors = { + -- Central + {x = 1, y = 0, z = 0}, + {x = 0, y = 0, z = 1}, + {x = -1, y = 0, z = 0}, + {x = 0, y = 0, z = -1}, + -- Directly Up or Down + {x = 0, y = 1, z = 0}, + {x = 0, y = -1, z = 0} + } + end + + local function get_neighbors(pos, width, height, tbl, open, closed) + local result = {} + for i = 1, #tbl do + local neighbor = vector.add(pos, tbl[i]) + if neighbor.y == pos.y + and not fly + and not swim then + neighbor = get_ground_level(neighbor, 1) + end + local can_move = get_line_of_sight({x = pos.x, y = neighbor.y, z = pos.z}, neighbor) + if swim then + can_move = true + end + if not moveable(vec_raise(neighbor, -0.49), width, height) then + can_move = false + if neighbor.y == pos.y + and moveable(vec_raise(neighbor, 0.51), width, height) then + neighbor = vec_raise(neighbor, 1) + can_move = true + end + end + if vector.equals(neighbor, goal) then + can_move = true + end + if open[minetest.hash_node_position(neighbor)] + or closed[minetest.hash_node_position(neighbor)] then + can_move = false + end + if can_move + and ((is_on_ground(neighbor) + or (fly or swim)) + or (neighbor.x == pos.x + and neighbor.z == pos.z + and climb)) + and (not swim + or is_node_liquid(minetest.get_node(neighbor).name)) then + table.insert(result, neighbor) + end + end + return result + end + + local function find_path(start, goal) + local us_time = minetest.get_us_time() + + start = { + x = floor(start.x + 0.5), + y = floor(start.y + 0.5), + z = floor(start.z + 0.5) + } + + goal = { + x = floor(goal.x + 0.5), + y = floor(goal.y + 0.5), + z = floor(goal.z + 0.5) + } + + if goal.x == start.x + and goal.z == start.z then -- No path can be found + return nil + end + + local openSet = self._path_data.open or {} + + local closedSet = self._path_data.closed or {} + + local start_index = minetest.hash_node_position(start) + + openSet[start_index] = { + pos = start, + parent = nil, + gScore = 0, + fScore = get_distance(start, goal) + } + + local count = self._path_data.count or 1 + + while count > 0 do + if minetest.get_us_time() - us_time > a_star_alloted_time then + self._path_data = { + start = start, + open = openSet, + closed = closedSet, + count = count + } + return + end + -- Initialize ID and data + local current_id + local current + + -- Get an initial id in open set + for i, v in pairs(openSet) do + current_id = i + current = v + break + end + + -- Find lowest f cost + for i, v in pairs(openSet) do + if v.fScore < current.fScore then + current_id = i + current = v + end + end + + -- Add lowest fScore to closedSet and remove from openSet + openSet[current_id] = nil + closedSet[current_id] = current + + self._path_data.open = openSet + self._path_data.closedSet = closedSet + + -- Reconstruct path if end is reached + if ((is_on_ground(goal) + or fly) + and current_id == minetest.hash_node_position(goal)) + or (not fly + and not is_on_ground(goal) + and goal.x == current.pos.x + and goal.z == current.pos.z) then + local path = {} + local fail_safe = 0 + for k, v in pairs(closedSet) do + fail_safe = fail_safe + 1 + end + repeat + if not closedSet[current_id] then return end + table.insert(path, closedSet[current_id].pos) + current_id = closedSet[current_id].parent + until current_id == start_index or #path >= fail_safe + if not closedSet[current_id] then self._path_data = {} return nil end + table.insert(path, closedSet[current_id].pos) + local reverse_path = {} + repeat table.insert(reverse_path, table.remove(path)) until #path == 0 + self._path_data = {} + return reverse_path + end + + count = count - 1 + + local adjacent = get_neighbors(current.pos, obj_width, obj_height, path_neighbors, openSet, closedSet) + + -- Go through neighboring nodes + for i = 1, #adjacent do + local neighbor = { + pos = adjacent[i], + parent = current_id, + gScore = 0, + fScore = 0 + } + temp_gScore = current.gScore + get_distance_to_neighbor(current.pos, neighbor.pos) + local new_gScore = 0 + if openSet[minetest.hash_node_position(neighbor.pos)] then + new_gScore = openSet[minetest.hash_node_position(neighbor.pos)].gScore + end + if (temp_gScore < new_gScore + or not openSet[minetest.hash_node_position(neighbor.pos)]) + and not closedSet[minetest.hash_node_position(neighbor.pos)] then + if not openSet[minetest.hash_node_position(neighbor.pos)] then + count = count + 1 + end + local hCost = get_distance_to_neighbor(neighbor.pos, goal) + neighbor.gScore = temp_gScore + neighbor.fScore = temp_gScore + hCost + openSet[minetest.hash_node_position(neighbor.pos)] = neighbor + end + end + if count > (max_open or 100) then + self._path_data = {} + return + end + end + self._path_data = {} + return nil + end + return find_path(start, goal) +end + + +------------ +-- Theta* -- +------------ + +function get_line_of_sight(a, b) + local steps = floor(vector.distance(a, b)) + local line = {} + + for i = 0, steps do + local pos + + if steps > 0 then + pos = { + x = a.x + (b.x - a.x) * (i / steps), + y = a.y + (b.y - a.y) * (i / steps), + z = a.z + (b.z - a.z) * (i / steps) + } + else + pos = a + end + table.insert(line, pos) + end + + if #line < 1 then + return false + else + for i = 1, #line do + local node = minetest.get_node(line[i]) + if minetest.registered_nodes[node.name].walkable then + return false + end + end + end + return true +end + +function creatura.find_theta_path(self, start, goal, obj_width, obj_height, max_open, climb, fly, swim) + climb = climb or false + fly = fly or false + swim = swim or false + + start = self._path_data.start or start + + self._path_data.start = start + + local path_neighbors = { + {x = 1, y = 0, z = 0}, + {x = 0, y = 0, z = 1}, + {x = -1, y = 0, z = 0}, + {x = 0, y = 0, z = -1}, + } + + if climb then + table.insert(path_neighbors, {x = 0, y = 1, z = 0}) + end + + if fly + or swim then + path_neighbors = { + -- Central + {x = 1, y = 0, z = 0}, + {x = 0, y = 0, z = 1}, + {x = -1, y = 0, z = 0}, + {x = 0, y = 0, z = -1}, + -- Directly Up or Down + {x = 0, y = 1, z = 0}, + {x = 0, y = -1, z = 0} + } + end + + local function get_neighbors(pos, width, height, tbl, open, closed) + local result = {} + for i = 1, #tbl do + local neighbor = vector.add(pos, tbl[i]) + if neighbor.y == pos.y + and not fly + and not swim then + neighbor = get_ground_level(neighbor, 1) + end + local can_move = get_line_of_sight({x = pos.x, y = neighbor.y, z = pos.z}, neighbor) + if swim then + can_move = true + end + if not moveable(vec_raise(neighbor, -0.49), width, height) then + can_move = false + if neighbor.y == pos.y + and moveable(vec_raise(neighbor, 0.51), width, height) then + neighbor = vec_raise(neighbor, 1) + can_move = true + end + end + if vector.equals(neighbor, goal) then + can_move = true + end + if open[minetest.hash_node_position(neighbor)] + or closed[minetest.hash_node_position(neighbor)] then + can_move = false + end + if can_move + and ((is_on_ground(neighbor) + or (fly or swim)) + or (neighbor.x == pos.x + and neighbor.z == pos.z + and climb)) + and (not swim + or is_node_liquid(minetest.get_node(neighbor).name)) then + table.insert(result, neighbor) + end + end + return result + end + + local function find_path(start, goal) + local us_time = minetest.get_us_time() + + start = { + x = floor(start.x + 0.5), + y = floor(start.y + 0.5), + z = floor(start.z + 0.5) + } + + goal = { + x = floor(goal.x + 0.5), + y = floor(goal.y + 0.5), + z = floor(goal.z + 0.5) + } + + if goal.x == start.x + and goal.z == start.z then -- No path can be found + return nil + end + + local openSet = self._path_data.open or {} + + local closedSet = self._path_data.closed or {} + + local start_index = minetest.hash_node_position(start) + + openSet[start_index] = { + pos = start, + parent = nil, + gScore = 0, + fScore = get_distance(start, goal) + } + + local count = self._path_data.count or 1 + + while count > 0 do + if minetest.get_us_time() - us_time > theta_star_alloted_time then + self._path_data = { + start = start, + open = openSet, + closed = closedSet, + count = count + } + return + end + + -- Initialize ID and data + local current_id + local current + + -- Get an initial id in open set + for i, v in pairs(openSet) do + current_id = i + current = v + break + end + + -- Find lowest f cost + for i, v in pairs(openSet) do + if v.fScore < current.fScore then + current_id = i + current = v + end + end + + -- Add lowest fScore to closedSet and remove from openSet + openSet[current_id] = nil + closedSet[current_id] = current + + -- Reconstruct path if end is reached + if (is_on_ground(goal) + and current_id == minetest.hash_node_position(goal)) + or (not is_on_ground(goal) + and goal.x == current.pos.x + and goal.z == current.pos.z) then + local path = {} + local fail_safe = 0 + for k, v in pairs(closedSet) do + fail_safe = fail_safe + 1 + end + repeat + if not closedSet[current_id] then return end + table.insert(path, closedSet[current_id].pos) + current_id = closedSet[current_id].parent + until current_id == start_index or #path >= fail_safe + if not closedSet[current_id] then self._path_data = {} return nil end + table.insert(path, closedSet[current_id].pos) + local reverse_path = {} + repeat table.insert(reverse_path, table.remove(path)) until #path == 0 + self._path_data = {} + return reverse_path + end + + count = count - 1 + + local adjacent = get_neighbors(current.pos, obj_width, obj_height, path_neighbors, openSet, closedSet) + + -- Go through neighboring nodes + for i = 1, #adjacent do + local neighbor = { + pos = adjacent[i], + parent = current_id, + gScore = 0, + fScore = 0 + } + if not openSet[minetest.hash_node_position(neighbor.pos)] + and not closedSet[minetest.hash_node_position(neighbor.pos)] then + local current_parent = closedSet[current.parent] or closedSet[start_index] + if not current_parent then + current_parent = openSet[current.parent] or openSet[start_index] + end + if current_parent + and get_line_of_sight(current_parent.pos, neighbor.pos) then + local temp_gScore = current_parent.gScore + get_distance_to_neighbor(current_parent.pos, neighbor.pos) + local new_gScore = 999 + if openSet[minetest.hash_node_position(neighbor.pos)] then + new_gScore = openSet[minetest.hash_node_position(neighbor.pos)].gScore + end + if temp_gScore < new_gScore then + local hCost = get_distance_to_neighbor(neighbor.pos, goal) + neighbor.gScore = temp_gScore + neighbor.fScore = temp_gScore + hCost + neighbor.parent = minetest.hash_node_position(current_parent.pos) + if openSet[minetest.hash_node_position(neighbor.pos)] then + openSet[minetest.hash_node_position(neighbor.pos)] = nil + end + openSet[minetest.hash_node_position(neighbor.pos)] = neighbor + count = count + 1 + end + else + local temp_gScore = current.gScore + get_distance_to_neighbor(current_parent.pos, neighbor.pos) + local new_gScore = 999 + if openSet[minetest.hash_node_position(neighbor.pos)] then + new_gScore = openSet[minetest.hash_node_position(neighbor.pos)].gScore + end + if temp_gScore < new_gScore then + local hCost = get_distance_to_neighbor(neighbor.pos, goal) + neighbor.gScore = temp_gScore + neighbor.fScore = temp_gScore + hCost + if openSet[minetest.hash_node_position(neighbor.pos)] then + openSet[minetest.hash_node_position(neighbor.pos)] = nil + end + openSet[minetest.hash_node_position(neighbor.pos)] = neighbor + count = count + 1 + end + end + end + end + if count > (max_open or 100) then + self._path_data = {} + return + end + end + self._path_data = {} + return nil + end + return find_path(start, goal) +end \ No newline at end of file diff --git a/settingtypes.txt b/settingtypes.txt new file mode 100644 index 0000000..9ee0d3d --- /dev/null +++ b/settingtypes.txt @@ -0,0 +1,17 @@ +# How mobs step up nodes. +# +# - Simple means mobs use Minetests builtin stepping. +# - Fancy means mobs will step up nodes with a quick hop but can cause lag. +creatura_step_type (Step Type) enum simple simple,fancy + +# How often (in seconds) the spawn ABM is called +creatura_spawn_interval (Spawn ABM Interval) float 10 + +# Time (in seconds) between spawn steps +creatura_spawn_step (Spawn Step Interval) float 15 + +# Allotted time (in μs) per step for A* pathfinding (lower means less lag but slower pathfinding) +creatura_a_star_alloted_time (A* Pathfinding Alloted time per step) float 500 + +# Allotted time (in μs) per step for Theta* pathfinding (lower means less lag but slower pathfinding) +creatura_theta_star_alloted_time (Theta* Pathfinding Alloted time per step) float 700 \ No newline at end of file diff --git a/sounds/creatura_hit_1.ogg b/sounds/creatura_hit_1.ogg new file mode 100644 index 0000000..b41e505 Binary files /dev/null and b/sounds/creatura_hit_1.ogg differ diff --git a/sounds/creatura_hit_2.ogg b/sounds/creatura_hit_2.ogg new file mode 100644 index 0000000..89192b1 Binary files /dev/null and b/sounds/creatura_hit_2.ogg differ diff --git a/sounds/creatura_hit_3.ogg b/sounds/creatura_hit_3.ogg new file mode 100644 index 0000000..df31c8d Binary files /dev/null and b/sounds/creatura_hit_3.ogg differ diff --git a/spawning.lua b/spawning.lua new file mode 100644 index 0000000..20c6cb0 --- /dev/null +++ b/spawning.lua @@ -0,0 +1,265 @@ +-------------- +-- Spawning -- +-------------- + +creatura.registered_mob_spawns = {} + +local walkable_nodes = {} + +minetest.register_on_mods_loaded(function() + for name in pairs(minetest.registered_nodes) do + if name ~= "air" and name ~= "ignore" then + if minetest.registered_nodes[name].walkable then + table.insert(walkable_nodes, name) + end + end + end +end) + +-- Math -- + +local random = math.random + +local function vec_raise(v, n) + return {x = v.x, y = v.y + n, z = v.z} +end + +-- Registration -- + +local function format_name(str) + if str then + if str:match(":") then str = str:split(":")[2] end + return (string.gsub(" " .. str, "%W%l", string.upper):sub(2):gsub("_", " ")) + end +end + +function creatura.register_spawn_egg(name, col1, col2, inventory_image) + if col1 and col2 then + local base = "(creatura_spawning_crystal.png^[multiply:#" .. col1 .. ")" + local spots = "(creatura_spawning_crystal_overlay.png^[multiply:#" .. col2 .. ")" + inventory_image = base .. "^" .. spots + end + local mod_name = name:split(":")[1] + local mob_name = name:split(":")[2] + minetest.register_craftitem(mod_name .. ":spawn_" .. mob_name, { + description = "Spawn " .. format_name(name), + inventory_image = inventory_image, + stack_max = 99, + on_place = function(itemstack, _, pointed_thing) + local mobdef = minetest.registered_entities[name] + local spawn_offset = math.abs(mobdef.collisionbox[2]) + local pos = minetest.get_pointed_thing_position(pointed_thing, true) + pos.y = (pos.y - 0.49) + spawn_offset + local object = minetest.add_entity(pos, name) + if object then + object:set_yaw(math.random(1, 6)) + object:get_luaentity().last_yaw = object:get_yaw() + end + if not creative then + itemstack:take_item() + return itemstack + end + end + }) +end + +function creatura.register_mob_spawn(name, def) + local spawn = { + chance = def.chance or 5, + min_radius = def.min_height or nil, + max_radius = def.max_radius or nil, + min_height = def.min_height or 0, + max_height = def.max_height or 128, + min_light = def.min_light or 6, + max_light = def.max_light or 15, + min_group = def.min_group or 1, + max_group = def.max_group or 4, + nodes = def.nodes or nil, + biomes = def.biomes or nil, + spawn_cluster = def.spawn_cluster or false, + spawn_in_nodes = def.spawn_in_nodes or false, + send_debug = def.send_debug or false + } + creatura.registered_mob_spawns[name] = spawn +end + + +-- Utility Functions -- + +function is_value_in_table(tbl, val) + for _, v in pairs(tbl) do + if v == val then + return true + end + end + return false +end + +function get_biome_name(pos) + if not pos then return end + return minetest.get_biome_name(minetest.get_biome_data(pos).biome) +end + +function get_ground_level(pos) + local node = minetest.get_node(pos) + local node_def = minetest.registered_nodes[node.name] + local height = 0 + while node_def.walkable + and height < 4 do + height = height + 1 + node = minetest.get_node(vec_raise(pos, height)) + node_def = minetest.registered_nodes[node.name] + end + return vec_raise(pos, height) +end + +local function get_spawnable_mobs(pos) + local biome = get_biome_name(pos) + if not biome then biome = "_nil" end + local spawnable = {} + for k, v in pairs(creatura.registered_mob_spawns) do + if not v.biomes + or is_value_in_table(v.biomes, biome) then + table.insert(spawnable, k) + end + end + return spawnable +end + +-- Spawning Function -- + +local spawn_queue = {} + +local min_spawn_radius = 16 + +local min_spawn_radius = 64 + +function execute_spawns(player) + if not player:get_pos() then return end + local pos = player:get_pos() + local spawnable_mobs = get_spawnable_mobs(pos) + if spawnable_mobs + and #spawnable_mobs > 0 then + local mob = spawnable_mobs[random(#spawnable_mobs)] + local spawn = creatura.registered_mob_spawns[mob] + if not spawn + or random(spawn.chance) > 1 then return end + local spawn_pos_center = { + x = pos.x + random(-min_spawn_radius, spawn.min_radius or min_spawn_radius), + y = pos.y, + z = pos.z + random(-min_spawn_radius, spawn.max_radius or min_spawn_radius) + } + local index_func + if spawn.spawn_in_nodes then + index_func = minetest.find_nodes_in_area + else + index_func = minetest.find_nodes_in_area_under_air + end + local spawn_on = spawn.nodes or walkable_nodes + if type(spawn_on) == "string" then + spawn_on = {spawn_on} + end + local spawn_y_array = index_func(vec_raise(spawn_pos_center, -8), vec_raise(spawn_pos_center, 8), spawn_on) + if spawn_y_array[1] then + local spawn_pos = spawn_y_array[1] + + if spawn_pos.y > spawn.max_height + or spawn_pos.y < spawn.min_height then + return + end + + local light = minetest.get_node_light(spawn_pos) or 7 + + if light > spawn.max_light + or light < spawn.min_light then + return + end + + local group_size = random(spawn.min_group, spawn.max_group) + + if spawn.spawn_cluster then + minetest.add_node(spawn_pos, {name = "creatura:spawn_node"}) + local meta = minetest.get_meta(spawn_pos) + meta:set_string("mob", mob) + meta:set_int("cluster", group_size) + else + for _ = 1, group_size do + spawn_pos = { + x = spawn_pos.x + random(-3, 3), + y = spawn_pos.y, + z = spawn_pos.z + random(-3, 3) + } + spawn_pos = get_ground_level(spawn_pos) + minetest.add_node(spawn_pos, {name = "creatura:spawn_node"}) + local meta = minetest.get_meta(spawn_pos) + meta:set_string("mob", mob) + end + end + if spawn.send_debug then + minetest.chat_send_all(mob .. " spawned at " .. minetest.pos_to_string(spawn_pos)) + end + end + end +end + +local spawn_step = tonumber(minetest.settings:get("creatura_spawn_step")) or 15 + +local spawn_tick = 0 + +minetest.register_globalstep(function(dtime) + spawn_tick = spawn_tick - dtime + if spawn_tick <= 0 then + for _, player in ipairs(minetest.get_connected_players()) do + execute_spawns(player) + end + spawn_tick = spawn_step + end +end) + +-- Node -- + +minetest.register_node("creatura:spawn_node", { + drawtype = "airlike", + groups = {not_in_creative_inventory = 1} +}) + +local spawn_interval = tonumber(minetest.settings:get("creatura_spawn_interval")) or 10 + +minetest.register_abm({ + label = "Creatura Spawning", + nodenames = {"creatura:spawn_node"}, + interval = spawn_interval, + chance = 1, + action = function(pos) + local meta = minetest.get_meta(pos) + local name = meta:get_string("mob") + local amount = meta:get_int("cluster") + if amount > 0 then + for i = 1, amount do + minetest.add_entity(pos, name) + end + else + minetest.add_entity(pos, name) + end + minetest.remove_node(pos) + end, +}) + +minetest.register_lbm({ + name = "creatura:spawning", + nodenames = {"creatura:spawn_node"}, + run_at_every_load = true, + action = function(pos) + local meta = minetest.get_meta(pos) + local name = meta:get_string("mob") + local amount = meta:get_int("cluster") + if amount > 0 then + for i = 1, amount do + minetest.add_entity(pos, name) + end + else + minetest.add_entity(pos, name) + end + minetest.remove_node(pos) + end, +}) \ No newline at end of file diff --git a/textures/creatura_particle_green.png b/textures/creatura_particle_green.png new file mode 100644 index 0000000..2693cc3 Binary files /dev/null and b/textures/creatura_particle_green.png differ diff --git a/textures/creatura_particle_red.png b/textures/creatura_particle_red.png new file mode 100644 index 0000000..019d9a2 Binary files /dev/null and b/textures/creatura_particle_red.png differ diff --git a/textures/creatura_smoke_particle.png b/textures/creatura_smoke_particle.png new file mode 100644 index 0000000..4b77207 Binary files /dev/null and b/textures/creatura_smoke_particle.png differ diff --git a/textures/creatura_spawning_crystal.png b/textures/creatura_spawning_crystal.png new file mode 100644 index 0000000..bceaee3 Binary files /dev/null and b/textures/creatura_spawning_crystal.png differ diff --git a/textures/creatura_spawning_crystal_overlay.png b/textures/creatura_spawning_crystal_overlay.png new file mode 100644 index 0000000..439e444 Binary files /dev/null and b/textures/creatura_spawning_crystal_overlay.png differ