Add "Creatures MOB-Engine" (cme) mod.
parent
bdc8da3226
commit
385df02cc5
|
@ -22,6 +22,8 @@ The following mods are also included:
|
|||
* [stairsplus][] ([zlib](mods/buildings/stairsplus/LICENSE.txt))
|
||||
* crafting/
|
||||
* [craftguide][] ([GPL/WTFPL](mods/crafting/craftguide/LICENSE))
|
||||
* engines/
|
||||
* [creatures (Creatures MOB-Engine)][cme] ([zlib/CC-BY-SA](doc/modpacks/cme/README.txt))
|
||||
* farming/
|
||||
* [farming_plus][] ([WTFPL](mods/farming/farming_plus/README.txt))
|
||||
* lib/
|
||||
|
@ -63,6 +65,7 @@ The following mods are also included:
|
|||
[awards]: https://forum.minetest.net/viewtopic.php?t=4870
|
||||
[biome_lib]: https://forum.minetest.net/viewtopic.php?f=11&t=12999
|
||||
[character_creator]: https://forum.minetest.net/viewtopic.php?f=9&t=13138
|
||||
[cme]: https://forum.minetest.net/viewtopic.php?t=8638
|
||||
[craftguide]: https://forum.minetest.net/viewtopic.php?f=11&t=14088
|
||||
[farming_plus]: https://forum.minetest.net/viewtopic.php?t=2787
|
||||
[fort_spikes]: https://forum.minetest.net/viewtopic.php?t=14574
|
||||
|
|
|
@ -0,0 +1,202 @@
|
|||
Creatures MOB-Engine API
|
||||
------------------------
|
||||
|
||||
creatures.register_mob(#Creature definition)
|
||||
-registers a mob at MOB-Engine; returns true when sucessfull
|
||||
|
||||
creatures.rnd(chance_table)
|
||||
-returns a weighted random table element; chance_sum of table must be 1
|
||||
^ example: creatures.rnd({elem1 = {chance = 0.7}, {elem2 = {chance = 0.3}})
|
||||
|
||||
creatures.compare_pos(pos1, pos2)
|
||||
-returns true if pos1 == pos2
|
||||
|
||||
creatures.findTarget(search_obj, pos, radius, search_type, mob_name, xray, no_count)
|
||||
-returns table of found objects (as ObjectRef) and boolean player_near
|
||||
^ search_obj is searching object; can be nil
|
||||
^ pos is starting position for search radius
|
||||
^ radius for searching in blocks/node
|
||||
^ search_type that specifies returned object requirements
|
||||
^ "all" -- returns every object except dropped Items
|
||||
^ "hostile" -- returns every object(creature) that has hostile setting or is player
|
||||
^ ignores "mob_type" if specified
|
||||
^ "nonhostile" -- returns every object that is not hostile or player
|
||||
^ "player" -- returns all players
|
||||
^ "mates" -- returns all objects(creatures) that are of same kind
|
||||
^ requires "mob_type" specifies
|
||||
^ mob_type specifies creature that is ignored or searched, depending on search_type
|
||||
^ xray allows searching through blocks/nodes (default == false)
|
||||
^ no_count skips collecting loop and returns just the boolean player_near
|
||||
^ table is empty
|
||||
|
||||
creatures.dropItems(pos, drops)
|
||||
-drops items at position pos
|
||||
^ pos where to drop Items
|
||||
^ drops table in #ItemDrops format
|
||||
|
||||
|
||||
#ItemDrops
|
||||
----------
|
||||
{
|
||||
{
|
||||
<Itemname>, -- e.g. "default:wood"
|
||||
<amount>, -- either a <number> or table in format {min = <number>, max = <number>}; optional
|
||||
<rarity> -- "chance = <value>": <value> between 0.0 and 1.0
|
||||
},
|
||||
}
|
||||
|
||||
Example:
|
||||
Will drop with a chance of 30% 1 to 3 items of type "default:wood"
|
||||
and with a chance of 100% 2 items of type "default:stone"
|
||||
{
|
||||
{"default:wood", {min = 1, max = 3}, chance = 0.3},
|
||||
{"default:stone", 2}
|
||||
}
|
||||
|
||||
|
||||
#Creature definition
|
||||
--------------------
|
||||
{
|
||||
name = "", -- e.g. "creatures:sheep"
|
||||
stats = {
|
||||
hp = 1, -- 1 HP = "1/2 player heart"
|
||||
hostile = false, -- is mob hostile (required for mode "attack") <optional>
|
||||
lifetime = 300, -- after which time mob despawns, in seconds <optional>
|
||||
dies_when_tamed = false, -- stop despawn when tamed <optional>
|
||||
can_jump = 1, -- height in nodes <optional>
|
||||
can_swim = false, -- can mob swim or will it drown <optional>
|
||||
can_fly = false, -- allows to fly (requires mode "fly") and disable step sounds <optional>
|
||||
can_burn = false, -- takes damage of lava <optional>
|
||||
can_panic = false, -- runs fast around when hit (requires mode "walk") <optional>
|
||||
has_falldamage = false, -- deals damage if falling more than 3 blocks <optional>
|
||||
has_kockback = false, -- get knocked back when hit <optional>
|
||||
sneaky = false, -- disables step sounds <optional>
|
||||
light = {min, max}, -- which light level will burn creature (requires can_burn = true) <optional>
|
||||
},
|
||||
|
||||
modes = {
|
||||
idle = {chance = <part of 1.0>, duration = <time>, moving_speed = <number>, update_yaw = <yawtime>},
|
||||
^ chance -- number between 0.0 and 1.0 (!!NOTE: sum of all modes MUST be 1.0!!)
|
||||
^ if chance is 0 then mode is not chosen automatically
|
||||
^ duration -- time in seconds until the next mode is chosen (depending on chance)
|
||||
^ moving_speed -- moving speed(flying/walking) <optional>
|
||||
^ update_yaw -- timer in seconds until the looking dir is changed <optional>
|
||||
^ if moving_speed > 0 then the moving direction is also changed
|
||||
|
||||
-- special modes
|
||||
attack = {<same as above>}
|
||||
follow = {<same as above>, radius = <number>, timer = <time>, items = <table>},
|
||||
^ same as above -- all possible values like specified above
|
||||
^ radius -- search distance in blocks/nodes for player
|
||||
^ timer -- time in seconds between each check for player
|
||||
^ items -- table of items to make mob follow in format {<Itemname>, <Itemname>}; e.g. {"farming:wheat"}
|
||||
eat = {<same as above>, nodes = <table>},
|
||||
^ same as above -- all possible values like specified above
|
||||
^ items -- eatable nodes in format {<Itemname>, <Itemname>}; e.g. {"default:dirt_with_grass"}
|
||||
},
|
||||
|
||||
model = {
|
||||
mesh = "creatures_sheep.x", -- mesh name; see Minetest Documentation for supported filetypes
|
||||
textures = {"creatures_sheep.png"}, -- table of textures; see Minetest Documentation
|
||||
collisionbox = <NodeBox>, -- defines mesh collision box; see Minetest Documentation
|
||||
rotation = 0.0, -- sets rotation offset when moving
|
||||
backface_culling = false, -- set true to enable backface culling
|
||||
animations = { -- animation used if defined <optional>
|
||||
idle = {#AnimationDef}, -- see #AnimationDef
|
||||
... -- depends on modes (must correspond to be used);
|
||||
^ supported "special modes": eat, follow, attack, death, swim, panic
|
||||
},
|
||||
},
|
||||
|
||||
sounds = {
|
||||
on_damage = {#SoundDef}, -- see #SoundDef <optional>
|
||||
on_death = {#SoundDef}, -- see #SoundDef <optional>
|
||||
swim = {#SoundDef}, -- see #SoundDef <optional>
|
||||
random = { -- depends on mode <optional>
|
||||
idle = {#SoundDef}, -- <optional>
|
||||
... -- depends on modes (must correspond to be used); supports "special modes": eat, follow, attack
|
||||
},
|
||||
},
|
||||
|
||||
drops = {#ItemDrops}, -- see #ItemDrops definition <optional>
|
||||
^ can also be a function; receives "self" reference
|
||||
|
||||
combat = { -- specifies behavior of hostile mobs in "attack" mode
|
||||
attack_damage = 1, -- how much damage deals each hit
|
||||
attack_speed = 0.6, -- time in seconds between hits
|
||||
attack_radius = 1.1, -- distance in blocks mob can reach to hit
|
||||
|
||||
search_enemy = true, -- true to search enemies to attack
|
||||
search_timer = 2, -- time in seconds to search an enemy (only if none found yet)
|
||||
search_radius = 12, -- radius in blocks within enemies are searched
|
||||
search_type = "player", -- what enemy is being searched (see types at creatures.findTarget())
|
||||
}
|
||||
|
||||
spawning = { -- defines spawning in world <optional>
|
||||
abm_nodes = {
|
||||
spawn_on = {<table>}, -- on what nodes mob can spawn <optional>
|
||||
^ table -- nodes and groups in table format; e.g. {"group:stone", "default:stone"}
|
||||
neighbors = {}, -- what node should be neighbors to spawnnode <optional>
|
||||
^ can be nil or table as above; "air" is forced always as neighbor
|
||||
},
|
||||
abm_interval = <interval>, -- time in seconds until Minetest tries to find a node with set specs
|
||||
abm_chance = <chance>, -- chance is 1/<chance>
|
||||
max_number = <number>, -- maximum mobs of this kind per mapblock(16x16x16)
|
||||
number = <amount>, -- how many mobs are spawned if found suitable spawn position
|
||||
^ amount -- number or table {min = <value>, max = <value>}
|
||||
time_range = <range>, -- time range in time of day format (0-24000) <optional>
|
||||
^ range -- table {min = <value>, max = <value>}
|
||||
light = <range>, -- min and max lightvalue at spawn position <optional>
|
||||
^ range -- table {min = <value>, max = <value>}
|
||||
height_limit = <range>, -- min and max height (world Y coordinate) <optional>
|
||||
^ range -- table {min = <value>, max = <value>}
|
||||
|
||||
spawn_egg = { -- is set a spawn_egg is added to creative inventory <optional>
|
||||
description = <desc>, -- Item description as string
|
||||
texture = <name>, -- texture name as string
|
||||
},
|
||||
|
||||
spawner = { -- is set a spawner_node is added to creative inventory <optional>
|
||||
range = <number>, -- defines an area (in blocks/nodes) within mobs are spawned
|
||||
number = <number>, -- maxmimum number of mobs spawned in area defined via range
|
||||
description = <desc>, -- Item description as string <optional>
|
||||
light = <range>, -- min and max lightvalue at spawn position <optional>
|
||||
^ range -- table {min = <value>, max = <value>}
|
||||
}
|
||||
},
|
||||
|
||||
on_rightclick = func(self, clicker) -- called when mob is rightclicked
|
||||
^ prevents default action when returns boolean true
|
||||
|
||||
on_punch = func(self, puncher) -- called when mob is punched (puncher can be nil)
|
||||
^ prevents default action when returns boolean true
|
||||
|
||||
on_step = func(self, dtime) -- called each server step
|
||||
^ prevents default action when returns boolean true
|
||||
|
||||
on_activate = func(self, staticdata) -- called when mob (re-)actived
|
||||
^ Note: staticdata is deserialized by MOB-Engine (including costum values)
|
||||
|
||||
get_staticdata = func(self) -- called when mob is punched (puncher can be nil)
|
||||
^ must return a table to save mob data (serialization is done by MOB-Engine)
|
||||
^ e.g:
|
||||
return {
|
||||
costum_mob_data = self.my_value,
|
||||
}
|
||||
}
|
||||
|
||||
#AnimationDef {
|
||||
start = 0, -- animation start frame
|
||||
stop = 80, -- animation end frame
|
||||
speed = 15, -- animation speed
|
||||
loop = true, -- if false, animation if just played once <optional>
|
||||
duration = 1 -- only supported in "death"-Animation, sets time the animation needs until mob is removed <optional>
|
||||
}
|
||||
|
||||
#SoundDef {
|
||||
name = <name>, -- sound name as string; see Minetest documentation
|
||||
gain = 1.0, -- sound gain; see Minetest documentation
|
||||
distance = <number>, -- hear distance in blocks/nodes <optional>
|
||||
time_min = <time> -- minimum time in seconds between sounds (random only) <optional>
|
||||
time_max = <time> -- maximum time in seconds between sounds (random only) <optional>
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
Copyright (c) 2015-2016 BlockMen <blockmen2015@gmail.com>
|
||||
|
||||
|
||||
This software is provided 'as-is', without any express or implied warranty. In no
|
||||
event will the authors be held liable for any damages arising from the use of
|
||||
this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose, including
|
||||
commercial applications, and to alter it and redistribute it freely, subject to the
|
||||
following restrictions:
|
||||
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software in a
|
||||
product, an acknowledgment in the product documentation is required.
|
||||
|
||||
2. Altered source versions must be plainly marked as such, and must not
|
||||
be misrepresented as being the original software.
|
||||
|
||||
3. This notice may not be removed or altered from any source distribution.
|
|
@ -0,0 +1,36 @@
|
|||
Mod/Modpack Creatures
|
||||
=====================
|
||||
Copyright (c) 2015-2016 BlockMen <blockmen2015@gmail.com>
|
||||
|
||||
Version: 2.3.1
|
||||
|
||||
|
||||
A Mod(pack) for Minetest that provides a MOB-Engine and adds several creatures to the game.
|
||||
Currently included: Ghosts, Zombies, Sheep, Chicken and Oerrki.
|
||||
|
||||
|
||||
License:
|
||||
~~~~~~~~
|
||||
Code(if not stated differently):
|
||||
(c) Copyright 2015-2016 BlockMen; modified zlib-License
|
||||
see "LICENSE.txt" for details.
|
||||
|
||||
Media(if not stated differently):
|
||||
(c) Copyright (2014-2016) BlockMen; CC-BY-SA 3.0
|
||||
|
||||
see each MOB-Module for detailed informations.
|
||||
|
||||
|
||||
Github:
|
||||
~~~~~~~
|
||||
https://github.com/BlockMen/cme
|
||||
|
||||
|
||||
Forum:
|
||||
~~~~~~
|
||||
https://forum.minetest.net/viewtopic.php?id=8638
|
||||
|
||||
|
||||
Changelog:
|
||||
~~~~~~~~~~
|
||||
see Changelog.txt
|
|
@ -0,0 +1,40 @@
|
|||
Version 2.1
|
||||
-----------
|
||||
- Added death animations
|
||||
- Zombie spawners are now generated in dungeons (if they have more than 4 rooms)
|
||||
- Fixed bug that hostile mobs did sometimes not attack
|
||||
- Reduced sheep model size
|
||||
- Tweak codestyle a bit/ added missing documentation of combat values
|
||||
|
||||
Version 2.2
|
||||
-----------
|
||||
- Added chicken
|
||||
- Readded sheep spawner (by LNJ)
|
||||
- Fix possible crash (reported by bbaez)
|
||||
- Added descriptions to spawners
|
||||
- Added option to set custom panic and swim animations
|
||||
|
||||
Version 2.2.1
|
||||
-------------
|
||||
- Fixed crash caused by not existing node
|
||||
|
||||
Version 2.2.2
|
||||
-------------
|
||||
- Eggs can be thrown to spawn chicken (rare)
|
||||
- Chicken drop chicken meat and feather(s) on death
|
||||
- Fixed spawn eggs being endless in singleplayer
|
||||
- Fix searching for target if in panic mode
|
||||
|
||||
Version 2.3
|
||||
-----------
|
||||
- Added Oerrki
|
||||
- Added fried eggs
|
||||
- Fixed moveing facement being reset
|
||||
- Fixed chicken model
|
||||
- Fixed sneaky variable not working
|
||||
- Fixed feathers being eatable
|
||||
|
||||
Version 2.3.1
|
||||
-------------
|
||||
- Added colored sheep
|
||||
- Fixed Oerrki spawning times (spawns on night as intended)
|
|
@ -0,0 +1,148 @@
|
|||
--= Creatures MOB-Engine (cme) =--
|
||||
-- Copyright (c) 2015-2016 BlockMen <blockmen2015@gmail.com>
|
||||
--
|
||||
-- common.lua
|
||||
--
|
||||
-- This software is provided 'as-is', without any express or implied warranty. In no
|
||||
-- event will the authors be held liable for any damages arising from the use of
|
||||
-- this software.
|
||||
--
|
||||
-- Permission is granted to anyone to use this software for any purpose, including
|
||||
-- commercial applications, and to alter it and redistribute it freely, subject to the
|
||||
-- following restrictions:
|
||||
--
|
||||
-- 1. The origin of this software must not be misrepresented; you must not
|
||||
-- claim that you wrote the original software. If you use this software in a
|
||||
-- product, an acknowledgment in the product documentation is required.
|
||||
-- 2. Altered source versions must be plainly marked as such, and must not
|
||||
-- be misrepresented as being the original software.
|
||||
-- 3. This notice may not be removed or altered from any source distribution.
|
||||
--
|
||||
|
||||
|
||||
-- constants
|
||||
nullVec = {x = 0, y = 0, z = 0}
|
||||
DEGTORAD = math.pi / 180.0
|
||||
|
||||
-- common functions
|
||||
function creatures.rnd(table, errval)
|
||||
if not errval then
|
||||
errval = false
|
||||
end
|
||||
|
||||
local res = 1000000000
|
||||
local rn = math.random(0, res - 1)
|
||||
local retval = nil
|
||||
|
||||
local psum = 0
|
||||
for s,w in pairs(table) do
|
||||
psum = psum + ((tonumber(w) or w.chance or 0) * res)
|
||||
if psum > rn then
|
||||
retval = s
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return retval
|
||||
end
|
||||
|
||||
function throw_error(msg)
|
||||
core.log("error", "#Creatures: ERROR: " .. msg)
|
||||
end
|
||||
|
||||
function creatures.compare_pos(pos1, pos2)
|
||||
if not pos1 or not pos2 then
|
||||
return
|
||||
end
|
||||
if pos1.x == pos2.x and pos1.y == pos2.y and pos1.z == pos2.z then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function creatures.findTarget(search_obj, pos, radius, search_type, ignore_mob, xray, no_count)
|
||||
local player_near = false
|
||||
local mobs = {}
|
||||
for _,obj in ipairs(core.get_objects_inside_radius(pos, radius)) do
|
||||
if obj ~= search_obj then
|
||||
if xray or core.line_of_sight(pos, obj:getpos()) == true then
|
||||
local is_player = obj:is_player()
|
||||
if is_player then
|
||||
player_near = true
|
||||
if no_count == true then
|
||||
return {}, true
|
||||
end
|
||||
end
|
||||
local entity = obj:get_luaentity()
|
||||
local isItem = (entity and entity.name == "__builtin:item") or false
|
||||
local ignore = (entity and entity.mob_name == ignore_mob and search_type ~= "mates") or false
|
||||
|
||||
if search_type == "all" then
|
||||
if not isItem and not ignore then
|
||||
table.insert(mobs, obj)
|
||||
end
|
||||
elseif search_type == "hostile" then
|
||||
if not ignore and (entity and entity.hostile == true) or is_player then
|
||||
table.insert(mobs, obj)
|
||||
end
|
||||
elseif search_type == "nonhostile" then
|
||||
if entity and not entity.hostile and not isItem and not ignore then
|
||||
table.insert(mobs, obj)
|
||||
end
|
||||
elseif search_type == "player" then
|
||||
if is_player then
|
||||
table.insert(mobs, obj)
|
||||
end
|
||||
elseif search_type == "mate" then
|
||||
if not isItem and (entity and entity.mob_name == ignore_mob) then
|
||||
table.insert(mobs, obj)
|
||||
end
|
||||
end
|
||||
end
|
||||
end --for
|
||||
end
|
||||
|
||||
return mobs,player_near
|
||||
end
|
||||
|
||||
function creatures.dropItems(pos, drops)
|
||||
if not pos or not drops then
|
||||
return
|
||||
end
|
||||
|
||||
-- convert drops table
|
||||
local tab = {}
|
||||
for _,elem in pairs(drops) do
|
||||
local name = tostring(elem[1])
|
||||
local v = elem[2]
|
||||
local chance = elem.chance
|
||||
local amount = ""
|
||||
-- check if drops depending on defined chance
|
||||
if name and chance then
|
||||
local ct = {}
|
||||
ct[name] = chance
|
||||
ct["_fake"] = 1 - chance
|
||||
local res = creatures.rnd(ct)
|
||||
if res == "_fake" then
|
||||
name = nil
|
||||
end
|
||||
end
|
||||
-- get amount
|
||||
if name and v then
|
||||
if type(v) == "table" then
|
||||
amount = math.random(v.min or 1, v.max or 1) or 1
|
||||
elseif type(v) == "number" then
|
||||
amount = v
|
||||
end
|
||||
if amount > 0 then
|
||||
amount = " " .. amount
|
||||
end
|
||||
end
|
||||
if name then
|
||||
local obj = core.add_item(pos, name .. amount)
|
||||
if not obj then
|
||||
throw_error("Could not drop item '" .. name .. amount .. "'")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,2 @@
|
|||
default
|
||||
wool
|
|
@ -0,0 +1 @@
|
|||
A Mod(pack) for Minetest that provides a MOB-Engine and adds several creatures to the game.
|
|
@ -0,0 +1,674 @@
|
|||
--= Creatures MOB-Engine (cme) =--
|
||||
-- Copyright (c) 2015-2016 BlockMen <blockmen2015@gmail.com>
|
||||
--
|
||||
-- functions.lua
|
||||
--
|
||||
-- This software is provided 'as-is', without any express or implied warranty. In no
|
||||
-- event will the authors be held liable for any damages arising from the use of
|
||||
-- this software.
|
||||
--
|
||||
-- Permission is granted to anyone to use this software for any purpose, including
|
||||
-- commercial applications, and to alter it and redistribute it freely, subject to the
|
||||
-- following restrictions:
|
||||
--
|
||||
-- 1. The origin of this software must not be misrepresented; you must not
|
||||
-- claim that you wrote the original software. If you use this software in a
|
||||
-- product, an acknowledgment in the product documentation is required.
|
||||
-- 2. Altered source versions must be plainly marked as such, and must not
|
||||
-- be misrepresented as being the original software.
|
||||
-- 3. This notice may not be removed or altered from any source distribution.
|
||||
--
|
||||
|
||||
|
||||
-- Localizations
|
||||
local rnd = math.random
|
||||
|
||||
|
||||
local function knockback(selfOrObject, dir, old_dir, strengh)
|
||||
local object = selfOrObject
|
||||
if selfOrObject.mob_name then
|
||||
object = selfOrObject.object
|
||||
end
|
||||
local current_fmd = object:get_properties().automatic_face_movement_dir or 0
|
||||
object:set_properties({automatic_face_movement_dir = false})
|
||||
object:setvelocity(vector.add(old_dir, {x = dir.x * strengh, y = 3.5, z = dir.z * strengh}))
|
||||
old_dir.y = 0
|
||||
core.after(0.4, function()
|
||||
object:set_properties({automatic_face_movement_dir = current_fmd})
|
||||
object:setvelocity(old_dir)
|
||||
selfOrObject.falltimer = nil
|
||||
if selfOrObject.stunned == true then
|
||||
selfOrObject.stunned = false
|
||||
if selfOrObject.can_panic == true then
|
||||
selfOrObject.target = nil
|
||||
selfOrObject.mode = "_run"
|
||||
selfOrObject.modetimer = 0
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function on_hit(me)
|
||||
core.after(0.1, function()
|
||||
me:settexturemod("^[colorize:#c4000099")
|
||||
end)
|
||||
core.after(0.5, function()
|
||||
me:settexturemod("")
|
||||
end)
|
||||
end
|
||||
|
||||
local hasMoved = creatures.compare_pos
|
||||
|
||||
local function getDir(pos1, pos2)
|
||||
local retval
|
||||
if pos1 and pos2 then
|
||||
retval = {x = pos2.x - pos1.x, y = pos2.y - pos1.y, z = pos2.z - pos1.z}
|
||||
end
|
||||
return retval
|
||||
end
|
||||
|
||||
local function getDistance(vec, fly_offset)
|
||||
if not vec then
|
||||
return -1
|
||||
end
|
||||
if fly_offset then
|
||||
vec.y = vec.y + fly_offset
|
||||
end
|
||||
return math.sqrt((vec.x)^2 + (vec.y)^2 + (vec.z)^2)
|
||||
end
|
||||
|
||||
local findTarget = creatures.findTarget
|
||||
|
||||
local function update_animation(obj_ref, mode, anim_def)
|
||||
if anim_def and obj_ref then
|
||||
obj_ref:set_animation({x = anim_def.start, y = anim_def.stop}, anim_def.speed, 0, anim_def.loop)
|
||||
end
|
||||
end
|
||||
|
||||
local function update_velocity(obj_ref, dir, speed, add)
|
||||
local velo = obj_ref:getvelocity()
|
||||
if not dir.y then
|
||||
dir.y = velo.y/speed
|
||||
end
|
||||
local new_velo = {x = dir.x * speed, y = dir.y * speed or velo.y , z = dir.z * speed}
|
||||
if add then
|
||||
new_velo = vector.add(velo, new_velo)
|
||||
end
|
||||
obj_ref:setvelocity(new_velo)
|
||||
end
|
||||
|
||||
local function getYaw(dirOrYaw)
|
||||
local yaw = 360 * rnd()
|
||||
if dirOrYaw and type(dirOrYaw) == "table" then
|
||||
yaw = math.atan(dirOrYaw.z / dirOrYaw.x) + math.pi^2 - 2
|
||||
if dirOrYaw.x > 0 then
|
||||
yaw = yaw + math.pi
|
||||
end
|
||||
elseif dirOrYaw and type(dirOrYaw) == "number" then
|
||||
-- here could be a value based on given yaw
|
||||
end
|
||||
|
||||
return yaw
|
||||
end
|
||||
|
||||
local dropItems = creatures.dropItems
|
||||
|
||||
local function killMob(me, def)
|
||||
if not def then
|
||||
if me then
|
||||
me:remove()
|
||||
end
|
||||
end
|
||||
local pos = me:getpos()
|
||||
me:setvelocity(nullVec)
|
||||
me:set_properties({collisionbox = nullVec})
|
||||
me:set_hp(0)
|
||||
|
||||
if def.sounds and def.sounds.on_death then
|
||||
local death_snd = def.sounds.on_death
|
||||
core.sound_play(death_snd.name, {pos = pos, max_hear_distance = death_snd.distance or 5, gain = death_snd.gain or 1})
|
||||
end
|
||||
|
||||
if def.model.animations.death then
|
||||
local dur = def.model.animations.death.duration or 0.5
|
||||
update_animation(me, "death", def.model.animations["death"])
|
||||
core.after(dur, function()
|
||||
me:remove()
|
||||
end)
|
||||
else
|
||||
me:remove()
|
||||
end
|
||||
if def.drops then
|
||||
if type(def.drops) == "function" then
|
||||
def.drops(me:get_luaentity())
|
||||
else
|
||||
dropItems(pos, def.drops)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function limit(value, min, max)
|
||||
if value < min then
|
||||
return min
|
||||
end
|
||||
if value > max then
|
||||
return max
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
local function calcPunchDamage(obj, actual_interval, tool_caps)
|
||||
local damage = 0
|
||||
if not tool_caps or not actual_interval then
|
||||
return 0
|
||||
end
|
||||
local my_armor = obj:get_armor_groups() or {}
|
||||
for group,_ in pairs(tool_caps.damage_groups) do
|
||||
damage = damage + (tool_caps.damage_groups[group] or 0) * limit(actual_interval / tool_caps.full_punch_interval, 0.0, 1.0) * ((my_armor[group] or 0) / 100.0)
|
||||
end
|
||||
return damage or 0
|
||||
end
|
||||
|
||||
local function onDamage(self, hp)
|
||||
local me = self.object
|
||||
local def = core.registered_entities[self.mob_name]
|
||||
hp = hp or me:get_hp()
|
||||
|
||||
if hp <= 0 then
|
||||
self.stunned = true
|
||||
killMob(me, def)
|
||||
else
|
||||
on_hit(me) -- red flashing
|
||||
if def.sounds and def.sounds.on_damage then
|
||||
local dmg_snd = def.sounds.on_damage
|
||||
core.sound_play(dmg_snd.name, {pos = me:getpos(), max_hear_distance = dmg_snd.distance or 5, gain = dmg_snd.gain or 1})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function changeHP(self, value)
|
||||
local me = self.object
|
||||
local hp = me:get_hp()
|
||||
hp = hp + math.floor(value)
|
||||
me:set_hp(hp)
|
||||
if value < 0 then
|
||||
onDamage(self, hp)
|
||||
end
|
||||
end
|
||||
|
||||
local function checkWielded(wielded, itemList)
|
||||
for s,w in pairs(itemList) do
|
||||
if w == wielded then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local tool_uses = {0, 30, 110, 150, 280, 300, 500, 1000}
|
||||
local function addWearout(player, tool_def)
|
||||
if not core.setting_getbool("creative_mode") then
|
||||
local item = player:get_wielded_item()
|
||||
if tool_def and tool_def.damage_groups and tool_def.damage_groups.fleshy then
|
||||
local uses = tool_uses[tool_def.damage_groups.fleshy] or 0
|
||||
if uses > 0 then
|
||||
local wear = 65535/uses
|
||||
item:add_wear(wear)
|
||||
player:set_wielded_item(item)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function spawnParticles(...)
|
||||
end
|
||||
if core.setting_getbool("creatures_enable_particles") == true then
|
||||
spawnParticles = function(pos, velocity, texture_str)
|
||||
local vel = vector.multiply(velocity, 0.5)
|
||||
vel.y = 0
|
||||
core.add_particlespawner({
|
||||
amount = 8,
|
||||
time = 1,
|
||||
minpos = vector.add(pos, -0.7),
|
||||
maxpos = vector.add(pos, 0.7),
|
||||
minvel = vector.add(vel, {x = -0.1, y = -0.01, z = -0.1}),
|
||||
maxvel = vector.add(vel, {x = 0.1, y = 0, z = 0.1}),
|
||||
minacc = vector.new(),
|
||||
maxacc = vector.new(),
|
||||
minexptime = 0.8,
|
||||
maxexptime = 1,
|
||||
minsize = 1,
|
||||
maxsize = 2.5,
|
||||
texture = texture_str,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- --
|
||||
-- Default entity functions
|
||||
-- --
|
||||
|
||||
creatures.on_punch = function(self, puncher, time_from_last_punch, tool_capabilities, dir)
|
||||
if self.stunned == true then
|
||||
return
|
||||
end
|
||||
|
||||
local me = self.object
|
||||
local mypos = me:getpos()
|
||||
|
||||
changeHP(self, calcPunchDamage(me, time_from_last_punch, tool_capabilities) * -1)
|
||||
if puncher then
|
||||
if self.hostile then
|
||||
self.mode = "attack"
|
||||
self.target = puncher
|
||||
end
|
||||
if time_from_last_punch >= 0.45 and self.stunned == false then
|
||||
if self.has_kockback == true then
|
||||
local v = me:getvelocity()
|
||||
v.y = 0
|
||||
if not self.can_fly then
|
||||
me:setacceleration({x = 0, y = -15, z = 0})
|
||||
end
|
||||
knockback(self, dir, v, 5)
|
||||
self.stunned = true
|
||||
end
|
||||
|
||||
-- add wearout to weapons/tools
|
||||
addWearout(puncher, tool_capabilities)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
creatures.on_rightclick = function(self, clicker)
|
||||
end
|
||||
|
||||
creatures.on_step = function(self, dtime)
|
||||
-- first get the relevant specs; exit if we don't know anything (1-3ms)
|
||||
local def = core.registered_entities[self.mob_name]
|
||||
if not def then
|
||||
throw_error("Can't load creature-definition")
|
||||
return
|
||||
end
|
||||
|
||||
-- timer updates
|
||||
self.lifetimer = self.lifetimer + dtime
|
||||
self.modetimer = self.modetimer + dtime
|
||||
self.soundtimer = self.soundtimer + dtime
|
||||
self.yawtimer = self.yawtimer + dtime
|
||||
self.nodetimer = self.nodetimer + dtime
|
||||
self.followtimer = self.followtimer + dtime
|
||||
if self.envtimer then
|
||||
self.envtimer = self.envtimer + dtime
|
||||
end
|
||||
if self.falltimer then
|
||||
self.falltimer = self.falltimer + dtime
|
||||
end
|
||||
if self.searchtimer then
|
||||
self.searchtimer = self.searchtimer + dtime
|
||||
end
|
||||
if self.attacktimer then
|
||||
self.attacktimer = self.attacktimer + dtime
|
||||
end
|
||||
if self.swimtimer then
|
||||
self.swimtimer = self.swimtimer + dtime
|
||||
end
|
||||
|
||||
-- main
|
||||
if self.stunned == true then
|
||||
return
|
||||
end
|
||||
|
||||
if self.lifetimer > def.stats.lifetime and not (self.mode == "attack" and self.target) then
|
||||
self.lifetimer = 0
|
||||
if not self.tamed or (self.tamed and def.stats.dies_when_tamed) then
|
||||
killMob(self.object, def)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- localize some things
|
||||
local modes = def.modes
|
||||
local current_mode = self.mode
|
||||
local me = self.object
|
||||
local current_pos = me:getpos()
|
||||
current_pos.y = current_pos.y + 0.5
|
||||
local moved = hasMoved(current_pos, self.last_pos) or false
|
||||
local fallen = false
|
||||
|
||||
-- Update pos and current node if necessary
|
||||
if moved == true or not self.last_pos then
|
||||
-- for falldamage
|
||||
if self.has_falldamage and self.last_pos and not self.in_water then
|
||||
local dist = math.abs(current_pos.y - self.last_pos.y)
|
||||
if dist > 0 then
|
||||
self.fall_dist = self.fall_dist - dist
|
||||
if not self.falltimer then
|
||||
self.falltimer = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self.last_pos = current_pos
|
||||
if self.nodetimer > 0.2 then
|
||||
self.nodetimer = 0
|
||||
local current_node = core.get_node_or_nil(current_pos)
|
||||
self.last_node = current_node
|
||||
if def.stats.light then
|
||||
local wtime = core.get_timeofday()
|
||||
local llvl = core.get_node_light({x = current_pos.x, y = current_pos.y + 0.5, z = current_pos.z}) or 0
|
||||
self.last_llvl = llvl
|
||||
end
|
||||
end
|
||||
else
|
||||
if (modes[current_mode].moving_speed or 0) > 0 then
|
||||
update_velocity(me, nullVec, 0)
|
||||
if modes["idle"] and not (current_mode == "attack" or current_mode == "follow") then
|
||||
current_mode = "idle"
|
||||
self.modetimer = 0
|
||||
end
|
||||
end
|
||||
if self.fall_dist < 0 then
|
||||
fallen = true
|
||||
end
|
||||
end
|
||||
|
||||
if fallen then
|
||||
local falltime = tonumber(self.falltimer) or 0
|
||||
local dist = math.abs(self.fall_dist) or 0
|
||||
self.falltimer = 0
|
||||
self.fall_dist = 0
|
||||
fallen = false
|
||||
|
||||
local damage = 0
|
||||
if dist > 3 and not self.in_water and falltime/dist < 0.2 then
|
||||
damage = dist - 3
|
||||
end
|
||||
|
||||
-- damage by calced value
|
||||
if damage > 0 then
|
||||
changeHP(self, damage * -1)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- special mode handling
|
||||
-- check distance to target
|
||||
if self.target and self.followtimer > 0.6 then
|
||||
self.followtimer = 0
|
||||
local p2 = self.target:getpos()
|
||||
local dir = getDir(current_pos, p2)
|
||||
local offset
|
||||
if self.can_fly then
|
||||
offset = modes["fly"].target_offset
|
||||
end
|
||||
local dist = getDistance(dir, offset)
|
||||
local radius
|
||||
if self.hostile and def.combat then
|
||||
radius = def.combat.search_radius
|
||||
elseif modes["follow"] then
|
||||
radius = modes["follow"].radius
|
||||
end
|
||||
if dist == -1 or dist > (radius or 5) then
|
||||
self.target = nil
|
||||
current_mode = ""
|
||||
elseif dist > -1 and self.hostile and dist < def.combat.attack_radius then
|
||||
-- attack
|
||||
if self.attacktimer > def.combat.attack_speed then
|
||||
self.attacktimer = 0
|
||||
if core.line_of_sight(current_pos, p2) == true then
|
||||
self.target:punch(me, 1.0, {
|
||||
full_punch_interval = def.combat.attack_speed,
|
||||
damage_groups = {fleshy = def.combat.attack_damage}
|
||||
})
|
||||
end
|
||||
update_velocity(me, self.dir, 0)
|
||||
end
|
||||
else
|
||||
if current_mode == "attack" or current_mode == "follow" then
|
||||
self.dir = vector.normalize(dir)
|
||||
me:setyaw(getYaw(dir))
|
||||
if self.in_water then
|
||||
self.dir.y = me:getvelocity().y
|
||||
end
|
||||
update_velocity(me, self.dir, modes[current_mode].moving_speed or 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- search a target (1-2ms)
|
||||
if not self.target and ((self.hostile and def.combat.search_enemy) or modes["follow"]) and current_mode ~= "_run" then
|
||||
local timer
|
||||
if self.hostile then
|
||||
timer = def.combat.search_timer or 2
|
||||
elseif modes["follow"] then
|
||||
timer = modes["follow"].timer
|
||||
end
|
||||
if self.searchtimer > (timer or 4) then
|
||||
self.searchtimer = 0
|
||||
local targets = {}
|
||||
if self.hostile then
|
||||
targets = findTarget(me, current_pos, def.combat.search_radius, def.combat.search_type, def.combat.search_xray)
|
||||
else
|
||||
targets = findTarget(me, current_pos, modes["follow"].radius or 5, "player")
|
||||
end
|
||||
if #targets > 1 then
|
||||
self.target = targets[rnd(1, #targets)]
|
||||
elseif #targets == 1 then
|
||||
self.target = targets[1]
|
||||
end
|
||||
if self.target then
|
||||
if self.hostile and modes["attack"] then
|
||||
current_mode = "attack"
|
||||
else
|
||||
local name = self.target:get_wielded_item():get_name()
|
||||
if name and checkWielded(name, modes["follow"].items) == true then
|
||||
current_mode = "follow"
|
||||
self.modetimer = 0
|
||||
else
|
||||
self.target = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if current_mode == "eat" and not self.eat_node then
|
||||
local nodes = modes[current_mode].nodes
|
||||
local p = {x = current_pos.x, y = current_pos.y - 1, z = current_pos.z}
|
||||
local sn = core.get_node_or_nil(p)
|
||||
local eat_node
|
||||
for _,name in pairs(nodes) do
|
||||
if name == self.last_node.name then
|
||||
eat_node = current_pos
|
||||
break
|
||||
elseif sn and sn.name == name then
|
||||
eat_node = p
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not eat_node then
|
||||
current_mode = "idle"
|
||||
else
|
||||
self.eat_node = eat_node
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- further mode handling
|
||||
-- update mode
|
||||
if current_mode ~= "attack" and
|
||||
(current_mode == "" or self.modetimer > (modes[current_mode].duration or 4)) then
|
||||
self.modetimer = 0
|
||||
|
||||
local new_mode = creatures.rnd(modes) or "idle"
|
||||
if new_mode == "eat" and self.in_water == true then
|
||||
new_mode = "idle"
|
||||
end
|
||||
if current_mode == "follow" and rnd(1, 10) < 3 then
|
||||
new_mode = current_mode
|
||||
elseif current_mode == "follow" then
|
||||
-- "lock" searching a little bit
|
||||
self.searchtimer = rnd(5, 8) * -1
|
||||
self.target = nil
|
||||
end
|
||||
current_mode = new_mode
|
||||
|
||||
-- change eaten node when mode changes
|
||||
if self.eat_node then
|
||||
local n = core.get_node_or_nil(self.eat_node)
|
||||
local nnn = n.name
|
||||
local def = core.registered_nodes[n.name]
|
||||
local sounds
|
||||
if def then
|
||||
if def.drop and type(def.drop) == "string" then
|
||||
nnn = def.drop
|
||||
elseif not def.walkable then
|
||||
nnn = "air"
|
||||
end
|
||||
end
|
||||
if nnn and nnn ~= n.name and core.registered_nodes[nnn] then
|
||||
core.set_node(self.eat_node, {name = nnn})
|
||||
if not sounds then
|
||||
sounds = def.sounds
|
||||
end
|
||||
if sounds and sounds.dug then
|
||||
core.sound_play(sounds.dug, {pos = self.eat_node, max_hear_distance = 5, gain = 1})
|
||||
end
|
||||
end
|
||||
self.eat_node = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- mode has changes, do things
|
||||
if current_mode ~= self.last_mode then
|
||||
self.last_mode = current_mode
|
||||
|
||||
local moving_speed = modes[current_mode].moving_speed or 0
|
||||
if moving_speed > 0 then
|
||||
local yaw = (getYaw(me:getyaw()) + 90.0) * DEGTORAD
|
||||
me:setyaw(yaw + 4.73)
|
||||
self.dir = {x = math.cos(yaw), y = 0, z = math.sin(yaw)}
|
||||
if self.can_fly then
|
||||
if current_pos.y >= (modes["fly"].max_height or 50) and not self.target then
|
||||
self.dir.y = -0.5
|
||||
else
|
||||
self.dir.y = (rnd() - 0.5)
|
||||
end
|
||||
end
|
||||
|
||||
-- reduce speed in water
|
||||
if self.in_water == true then
|
||||
moving_speed = moving_speed * 0.7
|
||||
end
|
||||
else
|
||||
self.dir = nullVec
|
||||
end
|
||||
|
||||
update_velocity(me, self.dir, moving_speed)
|
||||
local anim_def = def.model.animations[current_mode]
|
||||
if self.in_water and def.model.animations["swim"] then
|
||||
anim_def = def.model.animations["swim"]
|
||||
end
|
||||
update_animation(me, current_mode, anim_def)
|
||||
end
|
||||
|
||||
-- update yaw
|
||||
if current_mode ~= "attack" and current_mode ~= "follow" and
|
||||
(modes[current_mode].update_yaw or 0) > 0 and
|
||||
self.yawtimer > (modes[current_mode].update_yaw or 4) then
|
||||
self.yawtimer = 0
|
||||
local mod = nil
|
||||
if current_mode == "_run" then
|
||||
mod = me:getyaw()
|
||||
end
|
||||
local yaw = (getYaw(mod) + 90.0) * DEGTORAD
|
||||
me:setyaw(yaw + 4.73)
|
||||
local moving_speed = modes[current_mode].moving_speed or 0
|
||||
if moving_speed > 0 then
|
||||
self.dir = {x = math.cos(yaw), y = nil, z = math.sin(yaw)}
|
||||
update_velocity(me, self.dir, moving_speed)
|
||||
end
|
||||
end
|
||||
|
||||
--swim
|
||||
if self.can_swim and self.swimtimer > 0.8 and self.last_node then
|
||||
self.swimtimer = 0
|
||||
local name = self.last_node.name
|
||||
if name then
|
||||
if name == "default:water_source" then
|
||||
self.air_cnt = 0
|
||||
local vel = me:getvelocity()
|
||||
update_velocity(me, {x = vel.x, y = 0.9, z = vel.z}, 1)
|
||||
me:setacceleration({x = 0, y = -1.2, z = 0})
|
||||
self.in_water = true
|
||||
-- play swimming sounds
|
||||
if def.sounds and def.sounds.swim then
|
||||
local swim_snd = def.sounds.swim
|
||||
core.sound_play(swim_snd.name, {pos = current_pos, gain = swim_snd.gain or 1, max_hear_distance = swim_snd.distance or 10})
|
||||
end
|
||||
spawnParticles(current_pos, vel, "bubble.png")
|
||||
else
|
||||
self.air_cnt = self.air_cnt + 1
|
||||
if self.in_water == true and self.air_cnt > 5 then
|
||||
self.in_water = false
|
||||
if not self.can_fly then
|
||||
me:setacceleration({x = 0, y = -15, z = 0})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Add damage when drowning or in lava
|
||||
if self.env_damage and self.envtimer > 1 and self.last_node then
|
||||
self.envtimer = 0
|
||||
local name = self.last_node.name
|
||||
if not self.can_swim and name == "default:water_source" then
|
||||
changeHP(self, -1)
|
||||
elseif self.can_burn then
|
||||
if name == "fire:basic_flame" or name == "default:lava_source" then
|
||||
changeHP(self, -4)
|
||||
end
|
||||
end
|
||||
|
||||
-- add damage when light is too bright or too dark
|
||||
local tod = core.get_timeofday() * 24000
|
||||
if self.last_llvl and self.can_burn and self.last_llvl > (def.stats.light.max or 15) and tod < 18000 then
|
||||
changeHP(self, -1)
|
||||
elseif self.last_llvl and self.last_llvl < (def.stats.light.min or 0) then
|
||||
changeHP(self, -2)
|
||||
end
|
||||
end
|
||||
|
||||
-- Random sounds
|
||||
if def.sounds and def.sounds.random[current_mode] then
|
||||
local rnd_sound = def.sounds.random[current_mode]
|
||||
if not self.snd_rnd_time then
|
||||
self.snd_rnd_time = rnd((rnd_sound.time_min or 5), (rnd_sound.time_max or 35))
|
||||
end
|
||||
if rnd_sound and self.soundtimer > self.snd_rnd_time + rnd() then
|
||||
self.soundtimer = 0
|
||||
self.snd_rnd_time = nil
|
||||
core.sound_play(rnd_sound.name, {pos = current_pos, gain = rnd_sound.gain or 1, max_hear_distance = rnd_sound.distance or 30})
|
||||
end
|
||||
end
|
||||
|
||||
self.mode = current_mode
|
||||
end
|
||||
|
||||
|
||||
creatures.get_staticdata = function(self)
|
||||
return {
|
||||
hp = self.object:get_hp(),
|
||||
mode = self.mode,
|
||||
tamed = self.tamed,
|
||||
modetimer = self.modetimer,
|
||||
lifetimer = self.lifetimer,
|
||||
soundtimer = self.soundtimer,
|
||||
fall_dist = self.fall_dist,
|
||||
in_water = self.in_water,
|
||||
}
|
||||
end
|
|
@ -0,0 +1,33 @@
|
|||
--= Creatures MOB-Engine (cme) =--
|
||||
-- Copyright (c) 2015-2016 BlockMen <blockmen2015@gmail.com>
|
||||
--
|
||||
-- init.lua
|
||||
--
|
||||
-- This software is provided 'as-is', without any express or implied warranty. In no
|
||||
-- event will the authors be held liable for any damages arising from the use of
|
||||
-- this software.
|
||||
--
|
||||
-- Permission is granted to anyone to use this software for any purpose, including
|
||||
-- commercial applications, and to alter it and redistribute it freely, subject to the
|
||||
-- following restrictions:
|
||||
--
|
||||
-- 1. The origin of this software must not be misrepresented; you must not
|
||||
-- claim that you wrote the original software. If you use this software in a
|
||||
-- product, an acknowledgment in the product documentation is required.
|
||||
-- 2. Altered source versions must be plainly marked as such, and must not
|
||||
-- be misrepresented as being the original software.
|
||||
-- 3. This notice may not be removed or altered from any source distribution.
|
||||
--
|
||||
|
||||
|
||||
creatures = {}
|
||||
|
||||
local modpath = core.get_modpath("creatures")
|
||||
|
||||
-- API and common functions
|
||||
dofile(modpath .."/common.lua")
|
||||
dofile(modpath .."/functions.lua")
|
||||
dofile(modpath .."/register.lua")
|
||||
|
||||
-- Common items
|
||||
dofile(modpath .."/items.lua")
|
|
@ -0,0 +1,39 @@
|
|||
--= Creatures MOB-Engine (cme) =--
|
||||
-- Copyright (c) 2015-2016 BlockMen <blockmen2015@gmail.com>
|
||||
--
|
||||
-- items.lua
|
||||
--
|
||||
-- This software is provided 'as-is', without any express or implied warranty. In no
|
||||
-- event will the authors be held liable for any damages arising from the use of
|
||||
-- this software.
|
||||
--
|
||||
-- Permission is granted to anyone to use this software for any purpose, including
|
||||
-- commercial applications, and to alter it and redistribute it freely, subject to the
|
||||
-- following restrictions:
|
||||
--
|
||||
-- 1. The origin of this software must not be misrepresented; you must not
|
||||
-- claim that you wrote the original software. If you use this software in a
|
||||
-- product, an acknowledgment in the product documentation is required.
|
||||
-- 2. Altered source versions must be plainly marked as such, and must not
|
||||
-- be misrepresented as being the original software.
|
||||
-- 3. This notice may not be removed or altered from any source distribution.
|
||||
--
|
||||
|
||||
|
||||
core.register_craftitem("creatures:flesh", {
|
||||
description = "Flesh",
|
||||
inventory_image = "creatures_flesh.png",
|
||||
on_use = core.item_eat(2),
|
||||
})
|
||||
|
||||
core.register_craftitem("creatures:meat", {
|
||||
description = "Cooked Meat",
|
||||
inventory_image = "creatures_meat.png",
|
||||
on_use = core.item_eat(4),
|
||||
})
|
||||
|
||||
core.register_craft({
|
||||
type = "cooking",
|
||||
output = "creatures:meat",
|
||||
recipe = "creatures:flesh",
|
||||
})
|
|
@ -0,0 +1,580 @@
|
|||
--= Creatures MOB-Engine (cme) =--
|
||||
-- Copyright (c) 2015-2016 BlockMen <blockmen2015@gmail.com>
|
||||
--
|
||||
-- register.lua
|
||||
--
|
||||
-- This software is provided 'as-is', without any express or implied warranty. In no
|
||||
-- event will the authors be held liable for any damages arising from the use of
|
||||
-- this software.
|
||||
--
|
||||
-- Permission is granted to anyone to use this software for any purpose, including
|
||||
-- commercial applications, and to alter it and redistribute it freely, subject to the
|
||||
-- following restrictions:
|
||||
--
|
||||
-- 1. The origin of this software must not be misrepresented; you must not
|
||||
-- claim that you wrote the original software. If you use this software in a
|
||||
-- product, an acknowledgment in the product documentation is required.
|
||||
-- 2. Altered source versions must be plainly marked as such, and must not
|
||||
-- be misrepresented as being the original software.
|
||||
-- 3. This notice may not be removed or altered from any source distribution.
|
||||
--
|
||||
|
||||
|
||||
local allow_hostile = core.setting_getbool("only_peaceful_mobs") ~= true
|
||||
|
||||
local function translate_def(def)
|
||||
local new_def = {
|
||||
physical = true,
|
||||
visual = "mesh",
|
||||
stepheight = 0.6, -- ensure we get over slabs/stairs
|
||||
automatic_face_movement_dir = def.model.rotation or 0.0,
|
||||
|
||||
mesh = def.model.mesh,
|
||||
textures = def.model.textures,
|
||||
collisionbox = def.model.collisionbox or {-0.5, -0.5, -0.5, 0.5, 0.5, 0.5},
|
||||
visual_size = def.model.scale or {x = 1, y = 1},
|
||||
backface_culling = def.model.backface_culling or false,
|
||||
collide_with_objects = def.model.collide_with_objects or true,
|
||||
makes_footstep_sound = true,
|
||||
|
||||
stats = def.stats,
|
||||
model = def.model,
|
||||
sounds = def.sounds,
|
||||
combat = def.combat,
|
||||
modes = {},
|
||||
drops = def.drops,
|
||||
}
|
||||
|
||||
-- Tanslate modes to better accessable format
|
||||
for mn,def in pairs(def.modes) do
|
||||
local name = tostring(mn)
|
||||
if name ~= "update_time" then
|
||||
new_def.modes[name] = def
|
||||
--if name == "attack" then new_def.modes[name].chance = 0 end
|
||||
end
|
||||
end
|
||||
-- insert special mode "_run" which is used when in panic
|
||||
if def.stats.can_panic then
|
||||
if def.modes.walk then
|
||||
local new = table.copy(new_def.modes["walk"])
|
||||
new.chance = 0
|
||||
new.duration = 3
|
||||
new.moving_speed = new.moving_speed * 2
|
||||
if def.modes.panic and def.modes.panic.moving_speed then
|
||||
new.moving_speed = def.modes.panic.moving_speed
|
||||
end
|
||||
new.update_yaw = 0.7
|
||||
new_def.modes["_run"] = new
|
||||
local new_anim = def.model.animations.panic
|
||||
if not new_anim then
|
||||
new_anim = table.copy(def.model.animations.walk)
|
||||
new_anim.speed = new_anim.speed * 2
|
||||
end
|
||||
new_def.model.animations._run = new_anim
|
||||
end
|
||||
end
|
||||
|
||||
if def.stats.can_jump and type(def.stats.can_jump) == "number" then
|
||||
if def.stats.can_jump > 0 then
|
||||
new_def.stepheight = def.stats.can_jump + 0.1
|
||||
end
|
||||
end
|
||||
|
||||
if def.stats.sneaky or def.stats.can_fly then
|
||||
new_def.makes_footstep_sound = false
|
||||
end
|
||||
|
||||
|
||||
new_def.get_staticdata = function(self)
|
||||
local main_tab = creatures.get_staticdata(self)
|
||||
-- is own staticdata function defined? If so, merge results
|
||||
if def.get_staticdata then
|
||||
local data = def.get_staticdata(self)
|
||||
if data and type(data) == "table" then
|
||||
for s,w in pairs(data) do
|
||||
main_tab[s] = w
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- return data serialized
|
||||
return core.serialize(main_tab)
|
||||
end
|
||||
|
||||
new_def.on_activate = function(self, staticdata)
|
||||
|
||||
-- Add everything we need as basis for the engine
|
||||
self.mob_name = def.name
|
||||
self.hp = def.stats.hp
|
||||
self.hostile = def.stats.hostile
|
||||
self.mode = ""
|
||||
self.stunned = false -- if knocked back or hit do nothing else
|
||||
|
||||
self.has_kockback = def.stats.has_kockback
|
||||
self.has_falldamage = def.stats.has_falldamage
|
||||
self.can_swim = def.stats.can_swim
|
||||
self.can_fly = def.stats.can_fly
|
||||
self.can_burn = def.stats.can_burn
|
||||
self.can_panic = def.stats.can_panic == true and def.modes.walk ~= nil
|
||||
--self.is_tamed = nil
|
||||
--self.target = nil
|
||||
self.dir = {x = 0, z = 0}
|
||||
|
||||
--self.last_pos = nil (was nullVec)
|
||||
--self.last_node = nil
|
||||
--self.last_llvl = 0
|
||||
self.fall_dist = 0
|
||||
self.air_cnt = 0
|
||||
|
||||
|
||||
-- Timers
|
||||
self.lifetimer = 0
|
||||
self.modetimer = math.random()--0
|
||||
self.soundtimer = math.random()
|
||||
self.nodetimer = 2 -- ensure we get the first step
|
||||
self.yawtimer = math.random() * 2--0
|
||||
self.followtimer = 0
|
||||
if self.can_swim then
|
||||
self.swimtimer = 2 -- ensure we get the first step
|
||||
-- self.in_water = nil
|
||||
end
|
||||
if self.hostile then
|
||||
self.attacktimer = 0
|
||||
end
|
||||
if self.hostile or def.modes.follow then
|
||||
self.searchtimer = 0
|
||||
end
|
||||
if self.can_burn or not def.stats.can_swim or self.has_falldamage then
|
||||
self.env_damage = true
|
||||
self.envtimer = 0
|
||||
end
|
||||
|
||||
-- Other things
|
||||
|
||||
|
||||
if staticdata then
|
||||
local tab = core.deserialize(staticdata)
|
||||
if tab and type(tab) == "table" then
|
||||
for s,w in pairs(tab) do
|
||||
self[tostring(s)] = w
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- check we got a valid mode
|
||||
if not new_def.modes[self.mode] or (new_def.modes[self.mode].chance or 0) <= 0 then
|
||||
self.mode = "idle"
|
||||
end
|
||||
|
||||
if not self.can_fly then
|
||||
if not self.in_water then
|
||||
self.object:setacceleration({x = 0, y = -15, z = 0})
|
||||
end
|
||||
end
|
||||
|
||||
-- check if falling and set velocity only 0 when not falling
|
||||
if self.fall_dist == 0 then
|
||||
self.object:setvelocity(nullVec)
|
||||
end
|
||||
|
||||
self.object:set_hp(self.hp)
|
||||
|
||||
if not core.setting_getbool("enable_damage") then
|
||||
self.hostile = false
|
||||
end
|
||||
|
||||
-- immortal is needed to disable clientside smokepuff shit
|
||||
self.object:set_armor_groups({fleshy = 100, immortal = 1})
|
||||
|
||||
-- call custom on_activate if defined
|
||||
if def.on_activate then
|
||||
def.on_activate(self, staticdata)
|
||||
end
|
||||
end
|
||||
|
||||
new_def.on_punch = function(self, puncher, time_from_last_punch, tool_capabilities, dir)
|
||||
if def.on_punch and def.on_punch(self, puncher, time_from_last_punch, tool_capabilities, dir) == true then
|
||||
return
|
||||
end
|
||||
|
||||
creatures.on_punch(self, puncher, time_from_last_punch, tool_capabilities, dir)
|
||||
end
|
||||
|
||||
new_def.on_rightclick = function(self, clicker)
|
||||
if def.on_rightclick and def.on_rightclick(self, clicker) == true then
|
||||
return
|
||||
end
|
||||
|
||||
creatures.on_rightclick(self, clicker)
|
||||
end
|
||||
|
||||
new_def.on_step = function(self, dtime)
|
||||
if def.on_step and def.on_step(self, dtime) == true then
|
||||
return
|
||||
end
|
||||
|
||||
creatures.on_step(self, dtime)
|
||||
end
|
||||
|
||||
return new_def
|
||||
end
|
||||
|
||||
function creatures.register_mob(def) -- returns true if sucessfull
|
||||
if not def or not def.name then
|
||||
throw_error("Can't register mob. No name or Definition given.")
|
||||
return false
|
||||
end
|
||||
|
||||
local mob_def = translate_def(def)
|
||||
|
||||
core.register_entity(":" .. def.name, mob_def)
|
||||
|
||||
-- register spawn
|
||||
if def.spawning and not (def.stats.hostile and not allow_hostile) then
|
||||
local spawn_def = def.spawning
|
||||
spawn_def.mob_name = def.name
|
||||
spawn_def.mob_size = def.model.collisionbox
|
||||
if creatures.register_spawn(spawn_def) ~= true then
|
||||
throw_error("Couldn't register spawning for '" .. def.name .. "'")
|
||||
end
|
||||
|
||||
if spawn_def.spawn_egg then
|
||||
local egg_def = def.spawning.spawn_egg
|
||||
egg_def.mob_name = def.name
|
||||
egg_def.box = def.model.collisionbox
|
||||
creatures.register_egg(egg_def)
|
||||
end
|
||||
|
||||
if spawn_def.spawner then
|
||||
local spawner_def = def.spawning.spawner
|
||||
spawner_def.mob_name = def.name
|
||||
spawner_def.range = spawner_def.range or 4
|
||||
spawner_def.number = spawner_def.number or 6
|
||||
spawner_def.model = def.model
|
||||
creatures.register_spawner(spawner_def)
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
local function inRange(min_max, value)
|
||||
if not value or not min_max or not min_max.min or not min_max.max then
|
||||
return false
|
||||
end
|
||||
if (value >= min_max.min and value <= min_max.max) then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function checkSpace(pos, height)
|
||||
for i = 0, height do
|
||||
local n = core.get_node_or_nil({x = pos.x, y = pos.y + i, z = pos.z})
|
||||
if not n or n.name ~= "air" then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local time_taker = 0
|
||||
local function step(tick)
|
||||
core.after(tick, step, tick)
|
||||
time_taker = time_taker + tick
|
||||
end
|
||||
step(0.5)
|
||||
|
||||
local function stopABMFlood()
|
||||
if time_taker == 0 then
|
||||
return true
|
||||
end
|
||||
time_taker = 0
|
||||
end
|
||||
|
||||
local function groupSpawn(pos, mob, group, nodes, range, max_loops)
|
||||
local cnt = 0
|
||||
local cnt2 = 0
|
||||
|
||||
local nodes = core.find_nodes_in_area({x = pos.x - range, y = pos.y - range, z = pos.z - range},
|
||||
{x = pos.x + range, y = pos.y, z = pos.z + range}, nodes)
|
||||
local number = #nodes - 1
|
||||
if max_loops and type(max_loops) == "number" then
|
||||
number = max_loops
|
||||
end
|
||||
while cnt < group and cnt2 < number do
|
||||
cnt2 = cnt2 + 1
|
||||
local p = nodes[math.random(1, number)]
|
||||
p.y = p.y + 1
|
||||
if checkSpace(p, mob.size) == true then
|
||||
cnt = cnt + 1
|
||||
core.add_entity(p, mob.name)
|
||||
end
|
||||
end
|
||||
if cnt < group then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function creatures.register_spawn(spawn_def)
|
||||
if not spawn_def or not spawn_def.abm_nodes then
|
||||
throw_error("No valid definition for given.")
|
||||
return false
|
||||
end
|
||||
|
||||
if not spawn_def.abm_nodes.neighbors then
|
||||
spawn_def.abm_nodes.neighbors = {}
|
||||
end
|
||||
table.insert(spawn_def.abm_nodes.neighbors, "air")
|
||||
|
||||
core.register_abm({
|
||||
nodenames = spawn_def.abm_nodes.spawn_on,
|
||||
neighbors = spawn_def.abm_nodes.neighbors,
|
||||
interval = spawn_def.abm_interval or 44,
|
||||
chance = spawn_def.abm_chance or 7000,
|
||||
catch_up = false,
|
||||
action = function(pos, node, active_object_count, active_object_count_wider)
|
||||
-- prevent abm-"feature"
|
||||
if stopABMFlood() == true then
|
||||
return
|
||||
end
|
||||
|
||||
-- time check
|
||||
local tod = core.get_timeofday() * 24000
|
||||
if spawn_def.time_range then
|
||||
local wanted_res = false
|
||||
local range = table.copy(spawn_def.time_range)
|
||||
if range.min > range.max and range.min <= tod then
|
||||
wanted_res = true
|
||||
end
|
||||
if inRange(range, tod) == wanted_res then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- position check
|
||||
if spawn_def.height_limit and not inRange(spawn_def.height_limit, pos.y) then
|
||||
return
|
||||
end
|
||||
|
||||
-- light check
|
||||
pos.y = pos.y + 1
|
||||
local llvl = core.get_node_light(pos)
|
||||
if spawn_def.light and not inRange(spawn_def.light, llvl) then
|
||||
return
|
||||
end
|
||||
-- creature count check
|
||||
local max
|
||||
if active_object_count_wider > (spawn_def.max_number or 1) then
|
||||
local mates_num = #creatures.findTarget(nil, pos, 16, "mate", spawn_def.mob_name, true)
|
||||
if (mates_num or 0) >= spawn_def.max_number then
|
||||
return
|
||||
else
|
||||
max = spawn_def.max_number - mates_num
|
||||
end
|
||||
end
|
||||
|
||||
-- ok everything seems fine, spawn creature
|
||||
local height_min = (spawn_def.mob_size[5] or 2) - (spawn_def.mob_size[2] or 0)
|
||||
height_min = math.ceil(height_min)
|
||||
|
||||
local number = 0
|
||||
if type(spawn_def.number) == "table" then
|
||||
number = math.random(spawn_def.number.min, spawn_def.number.max)
|
||||
else
|
||||
number = spawn_def.number or 1
|
||||
end
|
||||
|
||||
if max and number > max then
|
||||
number = max
|
||||
end
|
||||
|
||||
if number > 1 then
|
||||
groupSpawn(pos, {name = spawn_def.mob_name, size = height_min}, number, spawn_def.abm_nodes.spawn_on, 5)
|
||||
else
|
||||
-- space check
|
||||
if not checkSpace(pos, height_min) then
|
||||
return
|
||||
end
|
||||
core.add_entity(pos, spawn_def.mob_name)
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function eggSpawn(itemstack, placer, pointed_thing, egg_def)
|
||||
if pointed_thing.type == "node" then
|
||||
local pos = pointed_thing.above
|
||||
pos.y = pos.y + 0.5
|
||||
local height = (egg_def.box[5] or 2) - (egg_def.box[2] or 0)
|
||||
if checkSpace(pos, height) == true then
|
||||
core.add_entity(pos, egg_def.mob_name)
|
||||
if core.setting_getbool("creative_mode") ~= true then
|
||||
itemstack:take_item()
|
||||
end
|
||||
end
|
||||
return itemstack
|
||||
end
|
||||
end
|
||||
|
||||
function creatures.register_egg(egg_def)
|
||||
if not egg_def or not egg_def.mob_name or not egg_def.box then
|
||||
throw_error("Can't register Spawn-Egg. Not enough parameters given.")
|
||||
return false
|
||||
end
|
||||
|
||||
core.register_craftitem(":" .. egg_def.mob_name .. "_spawn_egg", {
|
||||
description = egg_def.description or egg_def.mob_name .. " spawn egg",
|
||||
inventory_image = egg_def.texture or "creatures_spawn_egg.png",
|
||||
liquids_pointable = false,
|
||||
on_place = function(itemstack, placer, pointed_thing)
|
||||
return eggSpawn(itemstack, placer, pointed_thing, egg_def)
|
||||
end,
|
||||
})
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
local function makeSpawnerEntiy(mob_name, model)
|
||||
core.register_entity(":" .. mob_name .. "_spawner_dummy", {
|
||||
hp_max = 1,
|
||||
physical = false,
|
||||
collide_with_objects = false,
|
||||
collisionbox = nullVec,
|
||||
visual = "mesh",
|
||||
visual_size = {x = 0.42, y = 0.42},
|
||||
mesh = model.mesh,
|
||||
textures = model.textures,
|
||||
makes_footstep_sound = false,
|
||||
automatic_rotate = math.pi * -2.9,
|
||||
mob_name = "_" .. mob_name .. "_dummy",
|
||||
|
||||
on_activate = function(self)
|
||||
self.timer = 0
|
||||
self.object:setvelocity(nullVec)
|
||||
self.object:setacceleration(nullVec)
|
||||
self.object:set_armor_groups({immortal = 1})
|
||||
--self.object:set_bone_position("Root", nullVec, {x=45,y=0,z=0})
|
||||
end,
|
||||
|
||||
on_step = function(self, dtime)
|
||||
self.timer = self.timer + dtime
|
||||
if self.timer > 30 then
|
||||
self.timer = 0
|
||||
local n = core.get_node_or_nil(self.object:getpos())
|
||||
if n and n.name and n.name ~= mob_name .. "_spawner" then
|
||||
self.object:remove()
|
||||
end
|
||||
end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
local function spawnerSpawn(pos, spawner_def)
|
||||
local mates = creatures.findTarget(nil, pos, spawner_def.range, "mate", spawner_def.mob_name, true) or {}
|
||||
if #mates >= spawner_def.number then
|
||||
return false
|
||||
end
|
||||
local number_max = spawner_def.number - #mates
|
||||
|
||||
local rh = math.floor(spawner_def.range/2)
|
||||
local area = {
|
||||
min = {x = pos.x - rh, y=pos.y - rh, z = pos.z - rh},
|
||||
max = {x = pos.x + rh, y=pos.y + rh - spawner_def.height - 1, z = pos.z + rh}
|
||||
}
|
||||
|
||||
local height = area.max.y - area.min.y
|
||||
local cnt = 0
|
||||
for i = 0, height do
|
||||
if cnt >= number_max then
|
||||
break
|
||||
end
|
||||
local p = {x = math.random(area.min.x, area.max.x), y = area.min.y + i, z = math.random(area.min.z, area.max.z)}
|
||||
local n = core.get_node_or_nil(p)
|
||||
if n and n.name then
|
||||
local walkable = core.registered_nodes[n.name].walkable or false
|
||||
p.y = p.y + 1
|
||||
if walkable and checkSpace(p, spawner_def.height) == true then
|
||||
local llvl = core.get_node_light(p)
|
||||
if not spawner_def.light or (spawner_def.light and inRange(spawner_def.light, llvl)) then
|
||||
cnt = cnt + 1
|
||||
core.add_entity(p, spawner_def.mob_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local spawner_timers = {}
|
||||
function creatures.register_spawner(spawner_def)
|
||||
if not spawner_def or not spawner_def.mob_name or not spawner_def.model then
|
||||
throw_error("Can't register Spawn-Egg. Not enough parameters given.")
|
||||
return false
|
||||
end
|
||||
|
||||
makeSpawnerEntiy(spawner_def.mob_name, spawner_def.model)
|
||||
|
||||
core.register_node(":" .. spawner_def.mob_name .. "_spawner", {
|
||||
description = spawner_def.description or spawner_def.mob_name .. " spawner",
|
||||
paramtype = "light",
|
||||
tiles = {"creatures_spawner.png"},
|
||||
is_ground_content = true,
|
||||
drawtype = "glasslike",
|
||||
groups = {cracky = 1, level = 1},
|
||||
drop = "",
|
||||
on_construct = function(pos)
|
||||
pos.y = pos.y - 0.3
|
||||
core.add_entity(pos, spawner_def.mob_name .. "_spawner_dummy")
|
||||
end,
|
||||
on_destruct = function(pos)
|
||||
for _,obj in ipairs(core.get_objects_inside_radius(pos, 1)) do
|
||||
local entity = obj:get_luaentity()
|
||||
if obj and entity and entity.mob_name == "_" .. spawner_def.mob_name .. "_dummy" then
|
||||
obj:remove()
|
||||
end
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
local box = spawner_def.model.collisionbox
|
||||
local height = (box[5] or 2) - (box[2] or 0)
|
||||
spawner_def.height = height
|
||||
|
||||
if spawner_def.player_range and type(spawner_def.player_range) == "number" then
|
||||
core.register_abm({
|
||||
nodenames = {spawner_def.mob_name .. "_spawner"},
|
||||
interval = 2,
|
||||
chance = 1,
|
||||
catch_up = false,
|
||||
action = function(pos)
|
||||
local id = core.pos_to_string(pos)
|
||||
if not spawner_timers[id] then
|
||||
spawner_timers[id] = os.time()
|
||||
end
|
||||
local time_from_last_call = os.time() - spawner_timers[id]
|
||||
local mobs,player_near = creatures.findTarget(nil, pos, spawner_def.player_range, "player", nil, true, true)
|
||||
if player_near == true and time_from_last_call > 10 and (math.random(1, 5) == 1 or (time_from_last_call ) > 27) then
|
||||
spawner_timers[id] = os.time()
|
||||
|
||||
spawnerSpawn(pos, spawner_def)
|
||||
end
|
||||
end
|
||||
})
|
||||
else
|
||||
core.register_abm({
|
||||
nodenames = {spawner_def.mob_name .. "_spawner"},
|
||||
interval = 10,
|
||||
chance = 3,
|
||||
action = function(pos)
|
||||
|
||||
spawnerSpawn(pos, spawner_def)
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
Binary file not shown.
After Width: | Height: | Size: 562 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 441 B |
Binary file not shown.
After Width: | Height: | Size: 478 B |
Binary file not shown.
After Width: | Height: | Size: 686 B |
Binary file not shown.
After Width: | Height: | Size: 453 B |
Loading…
Reference in New Issue