From c251f07ae5844c3318b2ee68a5325a6d53ee7e96 Mon Sep 17 00:00:00 2001 From: ElCeejo <40281901+ElCeejo@users.noreply.github.com> Date: Thu, 10 Feb 2022 16:32:53 -0800 Subject: [PATCH] Initial Commit --- api.lua | 444 ++++++ boids.lua | 158 +++ doc.txt | 140 ++ init.lua | 24 + methods.lua | 386 +++++ mob_meta.lua | 1244 +++++++++++++++++ mod.conf | 2 + pathfinder.lua | 610 ++++++++ settingtypes.txt | 17 + sounds/creatura_hit_1.ogg | Bin 0 -> 12297 bytes sounds/creatura_hit_2.ogg | Bin 0 -> 9875 bytes sounds/creatura_hit_3.ogg | Bin 0 -> 12968 bytes spawning.lua | 265 ++++ textures/creatura_particle_green.png | Bin 0 -> 8373 bytes textures/creatura_particle_red.png | Bin 0 -> 8702 bytes textures/creatura_smoke_particle.png | Bin 0 -> 5648 bytes textures/creatura_spawning_crystal.png | Bin 0 -> 5208 bytes .../creatura_spawning_crystal_overlay.png | Bin 0 -> 5173 bytes 18 files changed, 3290 insertions(+) create mode 100644 api.lua create mode 100644 boids.lua create mode 100644 doc.txt create mode 100644 init.lua create mode 100644 methods.lua create mode 100644 mob_meta.lua create mode 100644 mod.conf create mode 100644 pathfinder.lua create mode 100644 settingtypes.txt create mode 100644 sounds/creatura_hit_1.ogg create mode 100644 sounds/creatura_hit_2.ogg create mode 100644 sounds/creatura_hit_3.ogg create mode 100644 spawning.lua create mode 100644 textures/creatura_particle_green.png create mode 100644 textures/creatura_particle_red.png create mode 100644 textures/creatura_smoke_particle.png create mode 100644 textures/creatura_spawning_crystal.png create mode 100644 textures/creatura_spawning_crystal_overlay.png 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 0000000000000000000000000000000000000000..b41e505a4a48e7959658db325fc7c4c49f9f394b GIT binary patch literal 12297 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 B>=^(6 literal 0 HcmV?d00001 diff --git a/sounds/creatura_hit_2.ogg b/sounds/creatura_hit_2.ogg new file mode 100644 index 0000000000000000000000000000000000000000..89192b152a5c43d3de26425aca1fae9bc99e9dee GIT binary patch literal 9875 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 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 0000000000000000000000000000000000000000..2693cc35e1a13cbb196f10951f5c9330f551d554 GIT binary patch literal 8373 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 literal 0 HcmV?d00001 diff --git a/textures/creatura_particle_red.png b/textures/creatura_particle_red.png new file mode 100644 index 0000000000000000000000000000000000000000..019d9a2b24c8a7aacf3431bfd8fca78b253f89fa GIT binary patch literal 8702 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 W=XOcGk~e!0U02gkqg?Gm#Qy+{HIvu? literal 0 HcmV?d00001 diff --git a/textures/creatura_smoke_particle.png b/textures/creatura_smoke_particle.png new file mode 100644 index 0000000000000000000000000000000000000000..4b77207f51f2d42db08bd2d54ed410dee0c9e80a GIT binary patch literal 5648 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 literal 0 HcmV?d00001 diff --git a/textures/creatura_spawning_crystal.png b/textures/creatura_spawning_crystal.png new file mode 100644 index 0000000000000000000000000000000000000000..bceaee3199b7f3f1f392e63bae2e7a92a846f6b1 GIT binary patch literal 5208 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 literal 0 HcmV?d00001 diff --git a/textures/creatura_spawning_crystal_overlay.png b/textures/creatura_spawning_crystal_overlay.png new file mode 100644 index 0000000000000000000000000000000000000000..439e4440c93d59b252dad08e2011c04acd3504d2 GIT binary patch literal 5173 zcmeHKX;f3!77nvA6$iiytuZQ6^=6(WQb>SA8-@e~N4UAU2_Yj%AOTTP8~{-fs0t3H zJgK(M6;Xj8MI4a&wD3*>BCgjzuJ`)StaWqmJ$vu(?C(4K?BwJ` zhKIR1k9WpkFm3`qPXz8BmTQC~_@3~rAI4zpcBV(i8AY%OtJ7;`3Kfbqrs+^DYF5ZF z81wIsVi%R%nC7}v?zzaez+>f|gk>imciqC*INdm&GnH~R{1wyBah_LLa9DmrPk+Bp zmlOU)J$_I5R^<8dVE1B|RUI4mKIFVS)O#;0zP_+^5no`inxDf)t1Gs5O%k~{um0fhk5EYyH4d#^F zXIqxe=TUAtUw^Urasjtz?xCw0JwEfxzi^%J=#Uhh7}?f1+jF90mug(`^z4!!CZ8$Z zYjz@^Qc-`aU2GRW!}GawMaJpi>b4Wrdy8%zA2p*G6S}odliFYQqYL%Sj5OR=2Nq7m z?_8Zr^fMe;>p9}DJl^o5=ca{%{+cKaIgV7c_b0_J*sByW<{J?#tMdEL$<+ zOMl0tLqFEW)Yh%}_u>3VZb(ShccC<4$x&X;#H75-?E$ZIa~#C$WSeI!5EKMo8CzK1 zAqt;j@bcYIf>dtTHu5R;kN2P4W>)oi^R`j+om#es6O}(t+~i!Zn7n z(=zDj&Dpy*h&sPkZfr^KtlF|d7oMq2xZl9O-`?eIh&WN@ z(U;3u{8N>nr6!?C*x3qqL0Uqb^4X1V(vxR$4L(h~((7Ji1x2hD7sn^naDE8f(e3u! z?KxT+^?Y>mdHj+GW5X|dr_T9PSJZlJ$~8TF_o>q5_`y}dPc)n9v9lI5r&jGx$0bKO z|7rg2@QwnXvfU4RdiawTj#*Ii@zvaMHKD4c?j>IL&ZXv7MDO2!Y|{;O=czAV-_p5s zlmva1ca2H7yjkw1fo2lcX1)BhFZ;o59kwdaMMaSXJn_9}|=U%VJ> zSesP}E;mxZ<-XaeV1I63x{1R-^||lqc=4Q=Y3`~1?fGA`=XpTw{xLIFuD&uivAM3} z3L#O#7cKO4_;qF0gmp1a-ttACEOZF!wrjX}QE)kA_Um|2;;7qo$$_mcOK$#Xw@SAD zTiGT1(xlCs1bz(}aER;4vX5*=WMlCpaRlnE@Ai%F?S9gOKXc^7rN3B?MP=tOU-1Jj z7*F9APu#*}^u?uddop0gr5KPt?V7MFebyG;@t_tnUc z`VZZXD4gRL>zU{K?6*wkb5FAR>mF+^UioF$wz0D@E4**?jdNI8{rO(_Mq6WL3)|-P zrP?pAJ<(EE6h~a~`2(7n?q#8E(U>)w6@4+s zj=h)_J|WXL<*3ol*Vh(7&o6#vHfc@T{+){6UORn|e)qY#xwk+^?Nxxz87KULjcC<) zSgMtvc(YmuIx7Yf5NOuHh!QnoC8%7X;ou%u*Wj=UDF+uz6B30wE}E#|r|VI1dU!OF zu0&W;T;MF{05cl^s8J(~HLFz`1KZ5OS#jCm*)mMPVXZDkB?lKLjKp%adK62=Q}IM7 z#H>gq<7PQy1N2fETf_?;gaB_GT%ysaV-pA_lL>F4;I(==fy8352t+c0Ooo65WJuE( zVKbyLOtL@>VDL}_qF3mQ3atif!GtB+6e9#R{g>~}nk3fWt+-pS2kvCzs}XE4Z87JV2*gkAxv5>^e* z$^sY3O33>0N4WxjZ|wgyfsBD)Tr19Tl6H+DMThnnnYp|=_D%a ztPsMvj6VK3Gz3 z#ub0FTuh0CL8MV75R1ViLR1n;f?yg51U#$v({RZ5pa zOqvXVXb7D|q|sqIp#E;U{|h}9M5IL%$!rRfO(DIZ7eKJItT&4pK=`la9&i}eLf*>TEcOW=n<$nMx^`PJY literal 0 HcmV?d00001