2023-11-30 21:05:39 +01:00

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