582 lines
16 KiB
Lua
582 lines
16 KiB
Lua
-- TODO: Change to rp_mobs when ready
|
|
local S = minetest.get_translator("mobs")
|
|
|
|
-- If true, will write the task queues of mobs as their nametag
|
|
local TASK_DEBUG = true
|
|
|
|
-- Default gravity that affects the mobs
|
|
local GRAVITY = tonumber(minetest.settings:get("movement_gravity")) or 9.81
|
|
|
|
-- List of entity variables to store in staticdata
|
|
-- (so they are persisted when unloading)
|
|
local persisted_entity_vars = {}
|
|
|
|
-- Declare an entity variable name to be persisted on shutdown
|
|
-- (recommended only for internal rp_mobs use)
|
|
rp_mobs.add_persisted_entity_var = function(name)
|
|
for i=1, #persisted_entity_vars do
|
|
if persisted_entity_vars[i] == name then
|
|
return
|
|
end
|
|
end
|
|
table.insert(persisted_entity_vars, name)
|
|
end
|
|
-- Same as above, but for a list of variables
|
|
-- (recommended only for internal rp_mobs use)
|
|
rp_mobs.add_persisted_entity_vars = function(names)
|
|
for n=1, #names do
|
|
rp_mobs.add_persisted_entity_var(names[n])
|
|
end
|
|
end
|
|
rp_mobs.add_persisted_entity_var("_custom_state")
|
|
|
|
local microtask_to_string = function(microtask)
|
|
return "Microtask: "..(microtask.label or "<UNNAMED>")
|
|
end
|
|
local task_to_string = function(task)
|
|
local str = "* Task: "..(task.label or "<UNNAMED>")
|
|
local next_microtask = task.microTasks:iterator()
|
|
local microtask = next_microtask()
|
|
while microtask do
|
|
str = str .. "\n** " .. microtask_to_string(microtask)
|
|
microtask = next_microtask()
|
|
end
|
|
return str
|
|
end
|
|
local task_queue_to_string = function(task_queue)
|
|
local str = ""
|
|
local next_task = task_queue.tasks:iterator()
|
|
local task = next_task()
|
|
local first = true
|
|
while task do
|
|
if not first then
|
|
str = str .. "\n"
|
|
end
|
|
str = str .. task_to_string(task)
|
|
task = next_task()
|
|
first = false
|
|
end
|
|
return str
|
|
end
|
|
local set_task_queues_as_nametag = function(self)
|
|
local str = ""
|
|
local next_task_queue = self._task_queues:iterator()
|
|
local task_queue = next_task_queue()
|
|
local first = true
|
|
local num = 1
|
|
while task_queue do
|
|
if not first then
|
|
str = str .. "\n"
|
|
end
|
|
str = str .. "Task queue #" .. num .. ":\n"
|
|
num = num + 1
|
|
str = str .. task_queue_to_string(task_queue)
|
|
task_queue = next_task_queue()
|
|
first = false
|
|
end
|
|
self.object:set_properties({
|
|
nametag = str,
|
|
})
|
|
end
|
|
|
|
rp_mobs.registered_mobs = {}
|
|
|
|
rp_mobs.register_mob = function(mobname, def)
|
|
local mdef = table.copy(def)
|
|
mdef.entity_definition._cmi_is_mob = true
|
|
mdef.entity_definition._description = def.description
|
|
mdef.entity_definition._is_animal = def.is_animal
|
|
mdef.entity_definition._base_size = table.copy(def.entity_definition.visual_size or { x=1, y=1, z=1 })
|
|
mdef.entity_definition._base_selbox = table.copy(def.entity_definition.selectionbox or { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5, rotate = false })
|
|
mdef.entity_definition._base_colbox = table.copy(def.entity_definition.collisionbox or { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5})
|
|
mdef.entity_definition._default_sounds = table.copy(def.default_sounds or {})
|
|
mdef.entity_definition._animations = table.copy(def.animations or {})
|
|
mdef.entity_definition._current_animation = nil
|
|
mdef.entity_definition._dying = false
|
|
|
|
rp_mobs.registered_mobs[mobname] = mdef
|
|
|
|
minetest.register_entity(mobname, mdef.entity_definition)
|
|
end
|
|
|
|
rp_mobs.get_staticdata_default = function(self)
|
|
local staticdata_table = {}
|
|
for p=1, #persisted_entity_vars do
|
|
local pvar = persisted_entity_vars[p]
|
|
local pvalue = self[pvar]
|
|
staticdata_table[pvar] = pvalue
|
|
end
|
|
local staticdata = minetest.serialize(staticdata_table)
|
|
return staticdata
|
|
end
|
|
|
|
rp_mobs.restore_state = function(self, staticdata)
|
|
local staticdata_table = minetest.deserialize(staticdata)
|
|
if not staticdata_table then
|
|
-- Default for empty/invalid staticdata
|
|
self._custom_state = {}
|
|
self._temp_custom_state = {}
|
|
return
|
|
end
|
|
for k,v in pairs(staticdata_table) do
|
|
self[k] = v
|
|
end
|
|
|
|
-- Make small if a child
|
|
if self._child then
|
|
rp_mobs.set_mob_child_size(self)
|
|
end
|
|
|
|
-- Make sure the custom state vars are always tables
|
|
if not self._custom_state then
|
|
self._custom_state = {}
|
|
end
|
|
if not self._temp_custom_state then
|
|
self._temp_custom_state = {}
|
|
end
|
|
end
|
|
|
|
rp_mobs.is_alive = function(mob)
|
|
if not mob then
|
|
return false
|
|
elseif mob._dying then
|
|
return false
|
|
else
|
|
return true
|
|
end
|
|
end
|
|
|
|
rp_mobs.spawn_mob_drop = function(pos, item)
|
|
local obj = minetest.add_item(pos, item)
|
|
if obj then
|
|
obj:set_velocity({
|
|
x = math.random(-1, 1),
|
|
y = 5,
|
|
z = math.random(-1, 1)
|
|
})
|
|
end
|
|
return obj
|
|
end
|
|
|
|
rp_mobs.drop_death_items = function(self, pos)
|
|
if not pos then
|
|
pos = self.object:get_pos()
|
|
end
|
|
local mobdef = rp_mobs.registered_mobs[self.name]
|
|
if not mobdef then
|
|
error("[rp_mobs] rp_mobs.drop_death_items was called on something that is not a registered mob! name="..tostring(self.name))
|
|
end
|
|
if not self._child and mobdef.drops then
|
|
for d=1, #mobdef.drops do
|
|
rp_mobs.spawn_mob_drop(pos, mobdef.drops[d])
|
|
end
|
|
end
|
|
if self._child and mobdef.child_drops then
|
|
for d=1, #mobdef.child_drops do
|
|
rp_mobs.spawn_mob_drop(pos, mobdef.child_drops[d])
|
|
end
|
|
end
|
|
end
|
|
|
|
rp_mobs.check_and_trigger_hunter_achievement = function(self, killer)
|
|
-- Hunter achievement: If mob is a food-dropping animal, it counts.
|
|
local mobdef = rp_mobs.registered_mobs[self.name]
|
|
if not mobdef then
|
|
error("[rp_mobs] rp_mobs.check_and_trigger_hunter_achievement was called on something that is not a registered mob! name="..tostring(self.name))
|
|
end
|
|
local drops_food = false
|
|
for _,drop in ipairs(mobdef.drops) do
|
|
if minetest.get_item_group(drop, "food") ~= 0 then
|
|
drops_food = true
|
|
break
|
|
end
|
|
end
|
|
if drops_food and killer ~= nil and killer:is_player() and mobdef.entity_definition._is_animal then
|
|
achievements.trigger_achievement(killer, "hunter")
|
|
end
|
|
end
|
|
|
|
rp_mobs.on_death_default = function(self, killer)
|
|
rp_mobs.check_and_trigger_hunter_achievement(self, killer)
|
|
rp_mobs.drop_death_items(self)
|
|
rp_mobs.default_mob_sound(self, "death")
|
|
end
|
|
|
|
rp_mobs.on_punch_default = function(self, puncher, time_from_last_punch, tool_capabilities, dir, damage)
|
|
if damage >= 1 then
|
|
rp_mobs.default_mob_sound(self, "damage")
|
|
else
|
|
rp_mobs.default_mob_sound(self, "hit_no_damage")
|
|
end
|
|
end
|
|
|
|
rp_mobs.damage = function(self, damage, no_sound)
|
|
if damage <= 0 then
|
|
return false
|
|
end
|
|
local hp = self.object:get_hp()
|
|
hp = math.max(0, hp - damage)
|
|
self.object:set_hp(hp)
|
|
if hp <= 0 then
|
|
self._dying = true
|
|
return true
|
|
else
|
|
if not no_sound then
|
|
rp_mobs.default_mob_sound(self, "damage")
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
rp_mobs.heal = function(self, heal)
|
|
local hp = self.object:get_hp()
|
|
local hp_max = self.object:get_properties().hp_max
|
|
hp = math.min(hp_max, hp + heal)
|
|
self.object:set_hp(hp)
|
|
end
|
|
|
|
rp_mobs.init_physics = function(self)
|
|
self._mob_acceleration = vector.zero()
|
|
self._phys_acceleration = {}
|
|
self._mob_velocity = vector.zero()
|
|
self._phys_velocity = {}
|
|
|
|
self._phys_acceleration_changed = false
|
|
self._phys_velocity_changed = false
|
|
self._mob_acceleration_changed = false
|
|
self._mob_velocity_changed = false
|
|
end
|
|
|
|
rp_mobs.add_phys_acceleration = function(self, name, force)
|
|
for i=1, #self._phys_acceleration do
|
|
local entry = self._phys_acceleration[i]
|
|
if entry.name == name then
|
|
entry.vec = force
|
|
self._phys_acceleration_changed = true
|
|
return
|
|
end
|
|
end
|
|
table.insert(self._phys_acceleration, { name = name, vec = force })
|
|
self._phys_acceleration_changed = true
|
|
end
|
|
rp_mobs.remove_phys_acceleration = function(self, name)
|
|
for i=1, #self._phys_acceleration do
|
|
local entry = self._phys_acceleration[i]
|
|
if entry.name == name then
|
|
table.remove(self._phys_acceleration, i)
|
|
self._phys_acceleration_changed = true
|
|
return
|
|
end
|
|
end
|
|
end
|
|
rp_mobs.add_phys_velocity = function(self, name, vel)
|
|
for i=1, #self._phys_velocity do
|
|
local entry = self._phys_velocity[i]
|
|
if entry.name == name then
|
|
entry.vec = vel
|
|
self._phys_velocity_changed = true
|
|
return
|
|
end
|
|
end
|
|
table.insert(self._phys_velocity, { name = name, vec = vel})
|
|
self._phys_velocity_changed = true
|
|
end
|
|
rp_mobs.remove_phys_velocity = function(self, name)
|
|
for i=1, #self._phys_velocity do
|
|
local entry = self._phys_velocity[i]
|
|
if entry.name == name then
|
|
table.remove(self._phys_velocity, i)
|
|
self._phys_velocity_changed = true
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
rp_mobs.activate_gravity = function(self)
|
|
rp_mobs.add_phys_acceleration(self, "rp_mobs:gravity", {x=0, y=-GRAVITY, z=0})
|
|
end
|
|
rp_mobs.deactivate_gravity = function(self)
|
|
rp_mobs.remove_phys_acceleration(self, "rp_mobs:gravity")
|
|
end
|
|
|
|
rp_mobs.handle_physics = function(self)
|
|
if not self._cmi_is_mob then
|
|
local entname = self.name or "<UNKNOWN>"
|
|
minetest.log("error", "[rp_mobs] rp_mobs.handle_physics was called on '"..entname.."' which is not a registered mob!")
|
|
end
|
|
if not self._phys_acceleration then
|
|
local entname = self.name or "<UNKNOWN>"
|
|
minetest.log("error", "[rp_mobs] rp_mobs.handle_physics was called on '"..entname.."' with uninitialized physics variables!")
|
|
end
|
|
if not rp_mobs.is_alive(self) then
|
|
return
|
|
end
|
|
|
|
-- Apply physics change
|
|
if self._phys_acceleration_changed or self._mob_acceleration_changed then
|
|
local acceleration = vector.zero()
|
|
for i=1, #self._phys_acceleration do
|
|
local entry = self._phys_acceleration[i]
|
|
acceleration = vector.add(acceleration, entry.vec)
|
|
end
|
|
acceleration = vector.add(acceleration, self._mob_acceleration)
|
|
self.object:set_acceleration(acceleration)
|
|
|
|
self._phys_acceleration_changed = false
|
|
self._mob_acceleration_changed = false
|
|
end
|
|
if self._phys_velocity_changed or self._mob_velocity_changed then
|
|
local velocity = vector.zero()
|
|
for i=1, #self._phys_velocity do
|
|
local entry = self._phys_velocity[i]
|
|
velocity = vector.add(velocity, entry.vec)
|
|
end
|
|
velocity = vector.add(velocity, self._mob_velocity)
|
|
self.object:set_velocity(velocity)
|
|
|
|
self._phys_velocity_changed = false
|
|
self._mob_velocity_changed = false
|
|
end
|
|
end
|
|
|
|
rp_mobs.init_tasks = function(self)
|
|
self._task_queues = rp_mobs.DoublyLinkedList()
|
|
self._active_task_queue = nil
|
|
end
|
|
|
|
rp_mobs.create_task_queue = function(decider)
|
|
return {
|
|
tasks = rp_mobs.DoublyLinkedList(),
|
|
decider = decider,
|
|
}
|
|
end
|
|
|
|
rp_mobs.add_task_queue = function(self, task_queue)
|
|
self._task_queues:append(task_queue)
|
|
end
|
|
|
|
rp_mobs.add_task_to_task_queue = function(task_queue, task)
|
|
task_queue.tasks:append(task)
|
|
if task.generateMicroTasks then
|
|
task:generateMicroTasks()
|
|
end
|
|
end
|
|
|
|
rp_mobs.create_task = function(def)
|
|
local task
|
|
if def then
|
|
task = table.copy(def)
|
|
else
|
|
task = {}
|
|
end
|
|
task.microTasks = rp_mobs.DoublyLinkedList()
|
|
return task
|
|
end
|
|
|
|
rp_mobs.create_microtask = function(def)
|
|
local mtask
|
|
if def then
|
|
mtask = table.copy(def)
|
|
else
|
|
mtask = {}
|
|
end
|
|
mtask.statedata = {}
|
|
return mtask
|
|
end
|
|
|
|
rp_mobs.add_microtask_to_task = function(self, microtask, task)
|
|
return task.microTasks:append(microtask)
|
|
end
|
|
|
|
rp_mobs.handle_tasks = function(self, dtime)
|
|
if not self._task_queues then
|
|
minetest.log("error", "[rp_mobs] rp_mobs.handle_tasks called before tasks were initialized!")
|
|
return
|
|
end
|
|
if not rp_mobs.is_alive(self) then
|
|
return
|
|
end
|
|
|
|
-- Trivial case: No task queues, nothing to do
|
|
if self._task_queues:isEmpty() then
|
|
return
|
|
end
|
|
|
|
-- Select next task queue
|
|
if not self._active_task_queue_entry then
|
|
self._active_task_queue_entry = self._task_queues:getFirst()
|
|
else
|
|
local nexxt = self._active_task_queue_entry.nextEntry
|
|
if not nexxt then
|
|
nexxt = self._task_queues:getFirst()
|
|
end
|
|
self._active_task_queue_entry = nexxt
|
|
end
|
|
if not self._active_task_queue_entry then
|
|
return
|
|
end
|
|
|
|
local activeTaskQueue = self._active_task_queue_entry.data
|
|
|
|
-- Run decider if active task queue is empty
|
|
local activeTaskEntry
|
|
if activeTaskQueue.tasks:isEmpty() then
|
|
if activeTaskQueue.decider then
|
|
activeTaskQueue:decider(self)
|
|
end
|
|
end
|
|
activeTaskEntry = activeTaskQueue.tasks:getFirst()
|
|
|
|
if not activeTaskEntry then
|
|
-- No more microtasks: Set idle animation if it exists
|
|
if self._animations and self._animations.idle then
|
|
rp_mobs.set_animation(self, "idle")
|
|
end
|
|
if TASK_DEBUG then
|
|
set_task_queues_as_nametag(self)
|
|
end
|
|
return
|
|
end
|
|
|
|
-- Handle current task of active task queue
|
|
local activeTask = activeTaskEntry.data
|
|
|
|
local activeMicroTaskEntry = activeTask.microTasks:getFirst()
|
|
if not activeMicroTaskEntry then
|
|
activeTaskQueue.tasks:remove(activeTaskEntry)
|
|
if TASK_DEBUG then
|
|
set_task_queues_as_nametag(self)
|
|
end
|
|
return
|
|
end
|
|
|
|
-- Remove microtask if completed
|
|
local activeMicroTask = activeMicroTaskEntry.data
|
|
if not activeMicroTask.singlestep and activeMicroTask:is_finished(self) then
|
|
if activeMicroTask.on_end then
|
|
activeMicroTask:on_end(self)
|
|
end
|
|
activeTask.microTasks:remove(activeMicroTaskEntry)
|
|
if TASK_DEBUG then
|
|
set_task_queues_as_nametag(self)
|
|
end
|
|
return
|
|
end
|
|
|
|
-- Execute microtask
|
|
|
|
-- Call on_start and set microtask animation before the first step
|
|
if not activeMicroTask.has_started then
|
|
if activeMicroTask.start_animation then
|
|
rp_mobs.set_animation(self, activeMicroTask.start_animation)
|
|
end
|
|
if activeMicroTask.on_start then
|
|
activeMicroTask:on_start(self)
|
|
end
|
|
activeMicroTask.has_started = true
|
|
end
|
|
|
|
-- on_step: The main microtask logic goes here
|
|
activeMicroTask:on_step(self, dtime)
|
|
|
|
-- If singlestep is set, finish microtask after its first and only step
|
|
if activeMicroTask.singlestep then
|
|
if activeMicroTask.on_end then
|
|
activeMicroTask:on_end(self)
|
|
end
|
|
activeTask.microTasks:remove(activeMicroTaskEntry)
|
|
if TASK_DEBUG then
|
|
set_task_queues_as_nametag(self)
|
|
end
|
|
return
|
|
end
|
|
|
|
if TASK_DEBUG then
|
|
set_task_queues_as_nametag(self)
|
|
end
|
|
end
|
|
|
|
rp_mobs.register_mob_item = function(mobname, invimg, desc)
|
|
local place
|
|
if not desc then
|
|
desc = rp_mobs.registered_mobs[mobname].description
|
|
end
|
|
minetest.register_craftitem(mobname, {
|
|
description = desc,
|
|
inventory_image = invimg,
|
|
groups = { spawn_egg = 1 },
|
|
on_place = function(itemstack, placer, pointed_thing)
|
|
local handled, handled_itemstack = util.on_place_pointed_node_handler(itemstack, placer, pointed_thing)
|
|
if handled then
|
|
return handled_itemstack
|
|
end
|
|
if pointed_thing.type == "node" then
|
|
local pos = pointed_thing.above
|
|
local pname = placer:get_player_name()
|
|
if minetest.is_protected(pos, pname) and
|
|
not minetest.check_player_privs(placer, "protection_bypass") then
|
|
minetest.record_protection_violation(pos, pname)
|
|
return itemstack
|
|
end
|
|
|
|
pos.y = pos.y + 0.5
|
|
local mob = minetest.add_entity(pos, mobname)
|
|
local ent = mob:get_luaentity()
|
|
if ent.type ~= "monster" then
|
|
-- set owner
|
|
ent.owner = pname
|
|
ent.tamed = true
|
|
end
|
|
minetest.log("action", "[rp_mobs] "..pname.." spawns "..mobname.." at "..minetest.pos_to_string(pos, 1))
|
|
if not minetest.is_creative_enabled(pname) then
|
|
itemstack:take_item()
|
|
end
|
|
end
|
|
return itemstack
|
|
end,
|
|
})
|
|
end
|
|
|
|
function rp_mobs.mob_sound(self, sound, keep_pitch)
|
|
local pitch
|
|
if not keep_pitch then
|
|
if self._child then
|
|
pitch = 1.5
|
|
else
|
|
pitch = 1.0
|
|
end
|
|
pitch = pitch + 0.0025 * math.random(-10,10)
|
|
end
|
|
minetest.sound_play(sound, {
|
|
pitch = pitch,
|
|
object = self.object,
|
|
}, true)
|
|
end
|
|
|
|
function rp_mobs.default_mob_sound(self, default_sound, keep_pitch)
|
|
local sound = self._default_sounds[default_sound]
|
|
if sound then
|
|
rp_mobs.mob_sound(self, sound, keep_pitch)
|
|
end
|
|
end
|
|
|
|
function rp_mobs.default_hurt_sound(self, keep_pitch)
|
|
rp_mobs.default_mob_sound(self, "damage", keep_pitch)
|
|
end
|
|
|
|
function rp_mobs.set_animation(self, animation_name, animation_speed)
|
|
local anim = self._animations[animation_name]
|
|
if not anim then
|
|
minetest.log("error", "[rp_mobs] set_animation for mob '"..tostring(self.name).."' called with unknown animation_name: "..tostring(animation_name))
|
|
return
|
|
end
|
|
local anim_speed = animation_speed or anim.default_frame_speed
|
|
if self._current_animation ~= animation_name then
|
|
self._current_animation = animation_name
|
|
self._current_animation_speed = anim_speed
|
|
self.object:set_animation(anim.frame_range, anim_speed)
|
|
elseif self._current_animation_speed ~= anim_speed then
|
|
self.object:set_animation_frame_speed(anim_speed)
|
|
end
|
|
end
|