Add NPC framework
|
@ -0,0 +1 @@
|
|||
npcf.conf
|
|
@ -0,0 +1,286 @@
|
|||
NPC Framework for Minetest 0.4.8
|
||||
--------------------------------
|
||||
This mod adds some, hopefully useful, non-player characters to the minetest game.
|
||||
The mod also provides a framework for others to create and manage their own custom NPCs.
|
||||
|
||||
Features currently include overhead name tags, formspec handling, ownership and management
|
||||
and file based backups.
|
||||
|
||||
These NPC's are not craftable although by default will be available in the creative menu.
|
||||
Servers would be encouraged to override this default and allocate the NPCs on /give basis.
|
||||
|
||||
### Info NPC
|
||||
|
||||
The Info NPC is a simple information serving character. You could think of them as a
|
||||
human book providing information about a particular server location, or whatever else you like.
|
||||
Supports multiple pages of text. 12 lines per page, ~50 chars per line.
|
||||
|
||||
### Deco NPC
|
||||
|
||||
A purely decorative NPC, can be set to roam freely and/or follow random players it encounters.
|
||||
|
||||
### Guard NPC
|
||||
|
||||
Protect yourself and your property against other players and mobs. Features 3d weapon and armor.
|
||||
Can be left to guard a certain area or set to follow their owner.
|
||||
|
||||
### Trader NPC
|
||||
|
||||
Provides a quantity based exchange system. The owner can set a given number of exchanges.
|
||||
This would likely be best used in conjunction with one of the physical currency mods.
|
||||
|
||||
Buy [default:mese] Qty [1] - Sell [default:gold_ingot] Qty [10]
|
||||
Buy [default:gold_ingot] Qty [20] - Sell [default:mese] Qty [1]
|
||||
|
||||
Note that the NPC's owner cannot trade with their own NPC, that would be rather pointless anyway.
|
||||
|
||||
### Builder NPC
|
||||
|
||||
Not really much point to this atm other than it's really fun to watch. By default, it be can only
|
||||
build a basic hut, however this is compatible (or so it seems) with all the schematics provided by
|
||||
Dan Duncombe's instabuild mod. These should be automatically available for selection if you have
|
||||
the instabuild mod installed.
|
||||
|
||||
Server Commands
|
||||
---------------
|
||||
|
||||
Server commands are issued as follows.
|
||||
|
||||
/npcf <command> [npc_name] [args]
|
||||
|
||||
### list
|
||||
|
||||
List all registered NPCs and display online status.
|
||||
|
||||
### clearobjects
|
||||
|
||||
Clear all loaded NPCs. (requires server priv)
|
||||
|
||||
### loadobjects
|
||||
|
||||
Reload all currently unloaded NPCs. (requires server priv)
|
||||
|
||||
### getpos npc_name
|
||||
|
||||
Display the position of the named NPC.
|
||||
|
||||
### reload npc_name
|
||||
|
||||
Reload an unloaded NPC. (requires ownership or server priv)
|
||||
|
||||
### save npc_name
|
||||
|
||||
Save current NPC state to file. (requires ownership or server priv)
|
||||
|
||||
### clear npc_name
|
||||
|
||||
Clear (unload) named NPC. (requires ownership or server priv)
|
||||
|
||||
### delete npc_name
|
||||
|
||||
Permanently unload and delete named NPC. (requires server priv)
|
||||
|
||||
### load npc_name pos | here
|
||||
|
||||
Loads the NPC at the specified postion. (requires ownership or server priv)
|
||||
|
||||
/npcf load npc_name 0, 1.5, 0
|
||||
|
||||
Use 'here' to load the NPC at the player's current position.
|
||||
|
||||
/npcf load npc_name here
|
||||
|
||||
### setpos npc_name pos | here
|
||||
|
||||
Set named NPC location. (x, y, z)
|
||||
|
||||
/npcf setpos npc_name 0, 1.5, 0
|
||||
|
||||
Use 'here' to load the NPC at the player's current position.
|
||||
|
||||
### setskin npc_name skin_filename | random
|
||||
|
||||
Set the skin texture of the named NPC. (requires server priv)
|
||||
|
||||
/npcf setskin npc_name character.png
|
||||
|
||||
If you have Zeg9's skins mod installed you can select a random texture from said mod.
|
||||
|
||||
/npcf setskin npc_name random
|
||||
|
||||
|
||||
API Reference
|
||||
-------------
|
||||
Use the global npcf api to create your own NPC.
|
||||
|
||||
npcf:register_npc("my_mod:my_cool_npc" ,{
|
||||
description = "My Cool NPC",
|
||||
})
|
||||
|
||||
This is a minimal example, see the NPCs included for more elaborate usage examples.
|
||||
|
||||
Properties
|
||||
----------
|
||||
Additional properties included by the framework. (defaults)
|
||||
|
||||
on_registration = function(self, pos, sender),
|
||||
on_construct = function(self),
|
||||
on_receive_fields = function(self, fields, sender),
|
||||
animation = {
|
||||
stand_START = 0,
|
||||
stand_END = 79,
|
||||
sit_START = 81,
|
||||
sit_END = 160,
|
||||
lay_START = 162,
|
||||
lay_END = 166,
|
||||
walk_START = 168,
|
||||
walk_END = 187,
|
||||
mine_START = 189,
|
||||
mine_END = 198,
|
||||
walk_mine_START = 200,
|
||||
walk_mine_END = 219,
|
||||
},
|
||||
animation_speed = 30,
|
||||
armor_groups = {immortal=1},
|
||||
inventory_image = "npcf_info_inv.png",
|
||||
show_nametag = true,
|
||||
nametag_color = "white", --textcolors mod adds red, blue, green, cyan, yellow and magenta
|
||||
metadata = {},
|
||||
var = {},
|
||||
timer = 0,
|
||||
|
||||
Special Properties
|
||||
------------------
|
||||
Properties used internally by the framework.
|
||||
|
||||
properties = {textures = def.textures},
|
||||
npcf_id = "npc",
|
||||
npc_name = nil,
|
||||
owner = nil,
|
||||
origin = {},
|
||||
|
||||
These should be considered read-only, with the exception of origin
|
||||
where it may be desireable update the statically saved position.
|
||||
|
||||
self.origin.pos = self.object:getpos()
|
||||
self.origin.yaw = self.object:getyaw()
|
||||
|
||||
Callbacks
|
||||
---------
|
||||
Additional callbacks provided by the framework.
|
||||
|
||||
### on_construct = function(self)
|
||||
This is called before the slightly delayed inbuilt on_activate callback.
|
||||
Please note that the self.npc_name, self.owner and self.origin properties
|
||||
may not be available or nil at the time of registration.
|
||||
|
||||
### on_receive_fields = function(self, fields, sender)
|
||||
Called when a button is pressed in the NPC's formspec. text fields, dropdown,
|
||||
list and checkbox selections are automatically stored in the self.metadata table.
|
||||
|
||||
### on_registration = function(self, pos, sender)
|
||||
Only ever called once upon successful NPC registration using a spawner.
|
||||
Currently not used anywhere and may be removed from future version.
|
||||
|
||||
|
||||
npcf
|
||||
----
|
||||
The global NPC framework namespace.
|
||||
|
||||
### Global Constants
|
||||
|
||||
NPCF_ANIM_STAND = 1
|
||||
NPCF_ANIM_SIT = 2
|
||||
NPCF_ANIM_LAY = 3
|
||||
NPCF_ANIM_WALK = 4
|
||||
NPCF_ANIM_WALK_MINE = 5
|
||||
NPCF_ANIM_MINE = 6
|
||||
NPCF_ANIM_RUN = 7
|
||||
NPCF_SHOW_IN_CREATIVE = true
|
||||
NPCF_SHOW_NAMETAGS = true
|
||||
NPCF_BUILDER_REQ_MATERIALS = false
|
||||
NPCF_DECO_FREE_ROAMING = true
|
||||
NPCF_GUARD_ATTACK_PLAYERS = true
|
||||
NPCF_DUPLICATE_REMOVAL_TIME = 10
|
||||
|
||||
All of the above can be overridden by including a npcf.conf file in the npcf directory.
|
||||
See: npcf.conf.example
|
||||
|
||||
### npcf:register_npc(name, def)
|
||||
|
||||
Register a non-player character. Used as a replacement for minetest.register_entity, it includes
|
||||
all the callbacks and properties (at the time of writing) available there. The only exceptions
|
||||
are the get_staticdata callback (used internally) and you are currently not able to create arbitrary
|
||||
property variables, instead the framework provides 'metadata' and 'var' tables for those purposes.
|
||||
The metadata table is persistent following a reload and automatically stores submitted form data.
|
||||
The var table should be used for non-persistent data storage only. Note that self.timer is
|
||||
automatically incremented by the framework but should be reset externally.
|
||||
|
||||
### npcf:spawn(pos, name, def)
|
||||
|
||||
Spawns and registers a NPC entity at the specified position. Returns a minetest ObjectRef on success.
|
||||
|
||||
local pos = player:getpos()
|
||||
local yaw = player:get_look_yaw()
|
||||
local player_name = player:get_player_name()
|
||||
local luaentity = npcf:spawn(pos, "npcf:guard_npc", {
|
||||
owner = player_name,
|
||||
npc_name = "Sam",
|
||||
origin = {pos=pos, yaw=yaw}, --optional
|
||||
})
|
||||
|
||||
Note that the on_registration callback will not be issued when spawning NPC's this way.
|
||||
|
||||
### npcf:clear(npc_name)
|
||||
|
||||
Clear (unload) named NPC.
|
||||
|
||||
### npcf:load(npc_name, pos)
|
||||
|
||||
Loads the NPC at the specified postion. If pos is nil then the NPC is loaded at the last saved origin.
|
||||
|
||||
### npcf:save(luaentity)
|
||||
|
||||
Save current NPC state to file.
|
||||
|
||||
on_receive_fields = function(self, fields, sender)
|
||||
if fields.save then
|
||||
npcf:save(self)
|
||||
end
|
||||
end,
|
||||
|
||||
### npcf:set_animation(luaentity, state)
|
||||
|
||||
Sets the NPC's animation state.
|
||||
|
||||
on_activate = function(self, staticdata, dtime_s)
|
||||
npcf:set_animation(self, NPCF_ANIM_STAND)
|
||||
end,
|
||||
|
||||
### npcf:get_index()
|
||||
|
||||
Returns a table of all registered NPCs. (loaded or unloaded)
|
||||
|
||||
{[npc_name] = owner_name, ... }
|
||||
|
||||
### npcf:get_luaentity(npc_name)
|
||||
|
||||
Returns a minetest ObjectRef of the NPC entity.
|
||||
|
||||
### npcf:get_face_direction(v1, v2)
|
||||
|
||||
Helper routine used internally and by some of the example NPCs.
|
||||
Returns a yaw value in radians for position v1 facing position v2.
|
||||
|
||||
### npcf:get_walk_velocity(speed, y, yaw)
|
||||
|
||||
Returns a velocity vector for the given speed, y velocity and yaw.
|
||||
|
||||
### npcf:show_formspec(player_name, npc_name, formspec)
|
||||
|
||||
Shows a formspec, similar to minetest.show_formspec() but with the npc_name included.
|
||||
Submitted data can then be captured in the NPC's own 'on_receive_fields' callback.
|
||||
|
||||
Note that form text fields, dropdown, list and checkbox selections are automatically
|
||||
stored in the NPC's metadata table. Image/Button clicks, however, are not.
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
minetest.register_chatcommand("npcf", {
|
||||
params = "<cmd> [npc_name] [args]",
|
||||
description = "NPC Management",
|
||||
func = function(name, param)
|
||||
local index = npcf:get_index()
|
||||
local admin = minetest.check_player_privs(name, {server=true})
|
||||
local cmd, npc_name, args = string.match(param, "^([^ ]+) (.-) (.+)$")
|
||||
if cmd and npc_name and args then
|
||||
if cmd == "setpos" then
|
||||
if admin or name == index[npc_name] then
|
||||
local pos = minetest.string_to_pos(args)
|
||||
if args == "here" then
|
||||
local player = minetest.get_player_by_name(name)
|
||||
if player then
|
||||
pos = player:getpos()
|
||||
end
|
||||
end
|
||||
if pos then
|
||||
pos.y = pos.y + 1
|
||||
local luaentity = npcf:get_luaentity(npc_name)
|
||||
if luaentity then
|
||||
if admin or luaentity.owner == name then
|
||||
luaentity.object:setpos(pos)
|
||||
luaentity.origin.pos = pos
|
||||
npcf:save(luaentity)
|
||||
pos = minetest.pos_to_string(pos)
|
||||
minetest.log("action", name.." moves NPC "..npc_name.." to "..pos)
|
||||
end
|
||||
end
|
||||
else
|
||||
minetest.chat_send_player(name, "Invalid position "..args)
|
||||
end
|
||||
end
|
||||
elseif cmd == "load" then
|
||||
if admin or name == index[npc_name] then
|
||||
local pos = minetest.string_to_pos(args)
|
||||
if args == "here" then
|
||||
local player = minetest.get_player_by_name(name)
|
||||
if player then
|
||||
pos = player:getpos()
|
||||
end
|
||||
end
|
||||
if pos then
|
||||
pos.y = pos.y + 1
|
||||
if npcf:load(npc_name, pos) then
|
||||
minetest.after(1, function()
|
||||
local luaentity = npcf:get_luaentity(npc_name)
|
||||
if luaentity then
|
||||
npcf:save(luaentity)
|
||||
pos = minetest.pos_to_string(pos)
|
||||
minetest.log("action", name.." loads NPC "..npc_name.." at "..pos)
|
||||
else
|
||||
minetest.chat_send_player(name, "Unable to load "..npc_name)
|
||||
end
|
||||
end)
|
||||
end
|
||||
else
|
||||
minetest.chat_send_player(name, "Invalid position "..args)
|
||||
end
|
||||
end
|
||||
elseif cmd == "tell" then
|
||||
if admin or name == index[npc_name] then
|
||||
local luaentity = npcf:get_luaentity(npc_name)
|
||||
if luaentity and luaentity.on_tell then
|
||||
luaentity.on_tell(luaentity, name, args)
|
||||
end
|
||||
else
|
||||
minetest.chat_send_player(name, "You don't have permission to tell "..npc_name.." things")
|
||||
end
|
||||
elseif cmd == "setskin" then
|
||||
if admin or name == index[npc_name] then
|
||||
if args == "random" then
|
||||
local textures = {}
|
||||
if minetest.get_modpath("skins") then
|
||||
for _,skin in ipairs(skins.list) do
|
||||
if string.match(skin, "^character_") then
|
||||
table.insert(textures, skin..".png")
|
||||
end
|
||||
end
|
||||
args = textures[math.random(1, #textures)]
|
||||
else
|
||||
minetest.chat_send_player(name, "Skins mod not found!")
|
||||
return
|
||||
end
|
||||
end
|
||||
local luaentity = npcf:get_luaentity(npc_name)
|
||||
if luaentity then
|
||||
luaentity.properties.textures[1] = args
|
||||
luaentity.object:set_properties(luaentity.properties)
|
||||
end
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
cmd, npc_name = string.match(param, "([^ ]+) (.+)")
|
||||
if cmd and npc_name then
|
||||
if cmd == "delete" and admin then
|
||||
npcf:clear(npc_name)
|
||||
local input = io.open(NPCF_DATADIR.."/"..npc_name..".npc", "r")
|
||||
if input then
|
||||
io.close(input)
|
||||
os.remove(NPCF_DATADIR.."/"..npc_name..".npc")
|
||||
end
|
||||
if index[npc_name] then
|
||||
index[npc_name] = nil
|
||||
local output = io.open(NPCF_DATADIR.."/index.txt", 'w')
|
||||
if output then
|
||||
output:write(minetest.serialize(index))
|
||||
io.close(output)
|
||||
end
|
||||
minetest.log("action", name.." deletes NPC "..npc_name)
|
||||
end
|
||||
elseif cmd == "clear" then
|
||||
if admin or name == index[npc_name] then
|
||||
npcf:clear(npc_name)
|
||||
minetest.log("action", name.." clears NPC "..npc_name)
|
||||
end
|
||||
elseif cmd == "reload" then
|
||||
if admin or name == index[npc_name] then
|
||||
if npcf:load(npc_name, nil) then
|
||||
minetest.log("action", name.." reloads NPC "..npc_name)
|
||||
else
|
||||
minetest.chat_send_player(name, "Unable to reload "..npc_name)
|
||||
end
|
||||
end
|
||||
elseif cmd == "save" then
|
||||
if admin or name == index[npc_name] then
|
||||
local saved = false
|
||||
local luaentity = npcf:get_luaentity(npc_name)
|
||||
if luaentity then
|
||||
if npcf:save(luaentity) then
|
||||
saved = true
|
||||
minetest.chat_send_player(name, npc_name.." has been saved")
|
||||
minetest.log("action", name.." saves NPC "..npc_name)
|
||||
end
|
||||
end
|
||||
if saved == false then
|
||||
minetest.chat_send_player(name, "Unable to save "..npc_name)
|
||||
end
|
||||
end
|
||||
elseif cmd == "getpos" then
|
||||
local located = false
|
||||
local luaentity = npcf:get_luaentity(npc_name)
|
||||
if luaentity then
|
||||
local pos = luaentity.object:getpos()
|
||||
if pos then
|
||||
pos.x = math.floor(pos.x * 10) * 0.1
|
||||
pos.y = math.floor(pos.y * 10) * 0.1 - 1
|
||||
pos.z = math.floor(pos.z * 10) * 0.1
|
||||
local msg = npc_name.." located at "..minetest.pos_to_string(pos)
|
||||
minetest.chat_send_player(name, msg)
|
||||
located = true
|
||||
end
|
||||
end
|
||||
if located == false then
|
||||
minetest.chat_send_player(name, "Unable to locate "..npc_name)
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
cmd = string.match(param, "([^ ]+)")
|
||||
if cmd then
|
||||
if cmd == "list" then
|
||||
local npclist = {}
|
||||
local index = npcf:get_index()
|
||||
if index then
|
||||
for npc_name,_ in pairs(index) do
|
||||
table.insert(npclist, npc_name)
|
||||
end
|
||||
end
|
||||
local msg = "None"
|
||||
if #npclist > 0 then
|
||||
msg = table.concat(npclist, ", ")
|
||||
end
|
||||
minetest.chat_send_player(name, "NPC List: "..msg)
|
||||
elseif cmd == "clearobjects" and admin then
|
||||
minetest.log("action", name.." clears all NPC objects")
|
||||
for _,ref in pairs(minetest.luaentities) do
|
||||
if ref.object and ref.npcf_id then
|
||||
ref.object:remove()
|
||||
end
|
||||
end
|
||||
elseif cmd == "loadobjects" and admin then
|
||||
minetest.log("action", name.." reloads all NPC objects")
|
||||
for npc_name,_ in pairs(index) do
|
||||
npcf:load(npc_name, nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
default
|
||||
instabuild?
|
||||
skins?
|
|
@ -0,0 +1,10 @@
|
|||
NPCF_MODPATH = minetest.get_modpath(minetest.get_current_modname())
|
||||
NPCF_DATADIR = minetest.get_worldpath().."/npc_data"
|
||||
dofile(NPCF_MODPATH.."/npcf.lua")
|
||||
dofile(NPCF_MODPATH.."/chatcommands.lua")
|
||||
|
||||
-- dofile(NPCF_MODPATH.."/npcs/info_npc.lua")
|
||||
-- dofile(NPCF_MODPATH.."/npcs/deco_npc.lua")
|
||||
-- dofile(NPCF_MODPATH.."/npcs/guard_npc.lua")
|
||||
-- dofile(NPCF_MODPATH.."/npcs/trade_npc.lua")
|
||||
-- dofile(NPCF_MODPATH.."/npcs/builder_npc.lua")
|
|
@ -0,0 +1,19 @@
|
|||
-- NPC Framework config example.
|
||||
|
||||
-- Deafult Settings:
|
||||
|
||||
-- Show NPC spawners in creative menu.
|
||||
NPCF_SHOW_IN_CREATIVE = true
|
||||
|
||||
-- Show NPC Nametags.
|
||||
NPCF_SHOW_NAMETAGS = true
|
||||
|
||||
-- Make Builder NPC require all building materials.
|
||||
NPCF_BUILDER_REQ_MATERIALS = false
|
||||
|
||||
-- Allow Deco NPC to roam freely away from players.
|
||||
NPCF_DECO_FREE_ROAMING = true
|
||||
|
||||
-- Allow Guard NPC to attack players.
|
||||
NPCF_GUARD_ATTACK_PLAYERS = true
|
||||
|
|
@ -0,0 +1,569 @@
|
|||
npcf = {}
|
||||
NPCF_ANIM_STAND = 1
|
||||
NPCF_ANIM_SIT = 2
|
||||
NPCF_ANIM_LAY = 3
|
||||
NPCF_ANIM_WALK = 4
|
||||
NPCF_ANIM_WALK_MINE = 5
|
||||
NPCF_ANIM_MINE = 6
|
||||
NPCF_SHOW_IN_CREATIVE = true
|
||||
NPCF_SHOW_NAMETAGS = true
|
||||
NPCF_BUILDER_REQ_MATERIALS = false
|
||||
NPCF_DECO_FREE_ROAMING = true
|
||||
NPCF_GUARD_ATTACK_PLAYERS = true
|
||||
|
||||
local input = io.open(NPCF_MODPATH.."/npcf.conf", "r")
|
||||
if input then
|
||||
dofile(NPCF_MODPATH.."/npcf.conf")
|
||||
input:close()
|
||||
input = nil
|
||||
end
|
||||
local timer = 0
|
||||
local index = {}
|
||||
input = io.open(NPCF_DATADIR.."/index.txt", "r")
|
||||
if input then
|
||||
index = minetest.deserialize(input:read('*all'))
|
||||
io.close(input)
|
||||
else
|
||||
os.execute("mkdir \""..NPCF_DATADIR.."\"")
|
||||
local output = io.open(NPCF_DATADIR.."/index.txt", 'w')
|
||||
if output then
|
||||
output:write(minetest.serialize(index))
|
||||
io.close(output)
|
||||
end
|
||||
end
|
||||
local default_npc = {
|
||||
hp_max = 1,
|
||||
physical = true,
|
||||
weight = 5,
|
||||
collisionbox = {-0.35,-1.0,-0.35, 0.35,0.8,0.35},
|
||||
visual = "mesh",
|
||||
visual_size = {x=1, y=1},
|
||||
mesh = "character.x",
|
||||
textures = {"character.png"},
|
||||
colors = {},
|
||||
is_visible = true,
|
||||
makes_footstep_sound = true,
|
||||
automatic_rotate = false,
|
||||
stepheight = 0,
|
||||
automatic_face_movement_dir = false,
|
||||
armor_groups = {immortal=1},
|
||||
animation = {
|
||||
stand_START = 0,
|
||||
stand_END = 79,
|
||||
sit_START = 81,
|
||||
sit_END = 160,
|
||||
lay_START = 162,
|
||||
lay_END = 166,
|
||||
walk_START = 168,
|
||||
walk_END = 187,
|
||||
mine_START = 189,
|
||||
mine_END = 198,
|
||||
walk_mine_START = 200,
|
||||
walk_mine_END = 219,
|
||||
},
|
||||
animation_speed = 30,
|
||||
decription = "Default NPC",
|
||||
inventory_image = "npcf_inv_top.png",
|
||||
show_nametag = true,
|
||||
nametag_color = "white",
|
||||
metadata = {},
|
||||
var = {},
|
||||
}
|
||||
local nametag = {
|
||||
npcf_id = "nametag",
|
||||
physical = false,
|
||||
collisionbox = {x=0, y=0, z=0},
|
||||
visual = "sprite",
|
||||
textures = {"npcf_tag_bg.png"},
|
||||
visual_size = {x=0.72, y=0.12, z=0.72},
|
||||
npc_name = nil,
|
||||
on_activate = function(self, staticdata, dtime_s)
|
||||
if staticdata == "expired" then
|
||||
self.object:remove()
|
||||
end
|
||||
end,
|
||||
get_staticdata = function(self)
|
||||
return "expired"
|
||||
end,
|
||||
}
|
||||
local form_reg = "size[8,3]"
|
||||
.."label[0,0;NPC Name, max 12 characters (A-Za-z0-9_-)]"
|
||||
.."field[0.5,1.5;7.5,0.5;name;Name;]"
|
||||
.."button_exit[5,2.5;2,0.5;cancel;Cancel]"
|
||||
.."button_exit[7,2.5;1,0.5;submit;Ok]"
|
||||
|
||||
local function get_valid_player_name(player)
|
||||
if player then
|
||||
if player:is_player() then
|
||||
local player_name = player:get_player_name()
|
||||
if minetest.get_player_by_name(player_name) then
|
||||
return player_name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function get_valid_npc_name(npc_name)
|
||||
if npc_name then
|
||||
return npc_name:len() <= 12 and npc_name:match("^[A-Za-z0-9%_%-]+$")
|
||||
end
|
||||
end
|
||||
|
||||
local function get_valid_entity(luaentity)
|
||||
if luaentity then
|
||||
if luaentity.name and luaentity.owner and luaentity.origin then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function add_nametag(parent)
|
||||
if parent.npc_name then
|
||||
local pos = parent.object:getpos()
|
||||
local tag = minetest.add_entity(pos, "npcf:nametag")
|
||||
if tag then
|
||||
local color = "W"
|
||||
if minetest.get_modpath("textcolors") then
|
||||
local c = string.upper(parent.nametag_color:sub(1,1))
|
||||
if string.match("RGBCYMW", c) then
|
||||
color = c
|
||||
end
|
||||
end
|
||||
local texture = "npcf_tag_bg.png"
|
||||
local x = math.floor(66 - ((parent.npc_name:len() * 11) / 2))
|
||||
local i = 0
|
||||
parent.npc_name:gsub(".", function(char)
|
||||
if char:byte() > 64 and char:byte() < 91 then
|
||||
char = "U"..char
|
||||
end
|
||||
texture = texture.."^[combine:84x14:"..(x+i)..",0="..color.."_"..char..".png"
|
||||
i = i + 11
|
||||
end)
|
||||
tag:set_attach(parent.object, "", {x=0,y=9,z=0}, {x=0,y=0,z=0})
|
||||
tag = tag:get_luaentity()
|
||||
tag.npc_name = parent.npc_name
|
||||
tag.object:set_properties({textures={texture}})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function npcf:register_npc(name, def)
|
||||
for k,v in pairs(default_npc) do
|
||||
if not def[k] then
|
||||
def[k] = v
|
||||
end
|
||||
end
|
||||
minetest.register_entity(name, {
|
||||
hp_max = def.hp_max,
|
||||
physical = def.physical,
|
||||
weight = def.weight,
|
||||
collisionbox = def.collisionbox,
|
||||
visual = def.visual,
|
||||
visual_size = def.visual_size,
|
||||
mesh = def.mesh,
|
||||
textures = def.textures,
|
||||
colors = def.colors,
|
||||
is_visible = def.is_visible,
|
||||
makes_footstep_sound = def.makes_footstep_sound,
|
||||
automatic_rotate = def.automatic_rotate,
|
||||
stepheight = def.stepheight,
|
||||
automatic_face_movement_dir = def.automatic_face_movement_dir,
|
||||
armor_groups = def.armor_groups,
|
||||
on_receive_fields = def.on_receive_fields,
|
||||
animation = def.animation,
|
||||
animation_speed = def.animation_speed,
|
||||
decription = def.description,
|
||||
show_nametag = def.show_nametag,
|
||||
nametag_color = def.nametag_color,
|
||||
metadata = def.metadata,
|
||||
properties = {textures = def.textures},
|
||||
var = def.var,
|
||||
npcf_id = "npc",
|
||||
npc_name = nil,
|
||||
owner = nil,
|
||||
origin = {},
|
||||
timer = 0,
|
||||
state = NPCF_ANIM_STAND,
|
||||
on_activate = function(self, staticdata, dtime_s)
|
||||
self.object:set_armor_groups(self.armor_groups)
|
||||
if staticdata then
|
||||
local npc = minetest.deserialize(staticdata)
|
||||
if npc then
|
||||
if npc.npc_name and npc.owner and npc.origin then
|
||||
self.npc_name = npc.npc_name
|
||||
self.owner = npc.owner
|
||||
self.origin = npc.origin
|
||||
if npc.origin.pos then
|
||||
self.object:setpos(npc.origin.pos)
|
||||
end
|
||||
else
|
||||
self.object:remove()
|
||||
minetest.log("action", "Removed unknown npc")
|
||||
return
|
||||
end
|
||||
if npc.metadata then
|
||||
self.metadata = npc.metadata
|
||||
end
|
||||
if npc.properties then
|
||||
self.properties = npc.properties
|
||||
self.object:set_properties(npc.properties)
|
||||
end
|
||||
if NPCF_SHOW_NAMETAGS == true and self.show_nametag == true then
|
||||
add_nametag(self)
|
||||
end
|
||||
end
|
||||
end
|
||||
local x = self.animation.stand_START
|
||||
local y = self.animation.stand_END
|
||||
local speed = self.animation_speed
|
||||
if x and y then
|
||||
self.object:set_animation({x=x, y=y}, speed)
|
||||
end
|
||||
if type(def.on_construct) == "function" then
|
||||
def.on_construct(self)
|
||||
end
|
||||
minetest.after(0.5, function()
|
||||
if get_valid_entity(self) then
|
||||
if type(def.on_activate) == "function" then
|
||||
def.on_activate(self, staticdata, dtime_s)
|
||||
end
|
||||
else
|
||||
self.object:remove()
|
||||
end
|
||||
end)
|
||||
end,
|
||||
on_rightclick = function(self, clicker)
|
||||
if get_valid_entity(self) then
|
||||
local player_name = get_valid_player_name(clicker)
|
||||
if player_name then
|
||||
local ctrl = clicker:get_player_control()
|
||||
if ctrl.sneak then
|
||||
local yaw = npcf:get_face_direction(self.object:getpos(), clicker:getpos())
|
||||
self.object:setyaw(yaw)
|
||||
self.origin.yaw = yaw
|
||||
return
|
||||
end
|
||||
minetest.chat_send_player(player_name, self.npc_name)
|
||||
if type(def.on_rightclick) == "function" then
|
||||
def.on_rightclick(self, clicker)
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
on_punch = function(self, hitter)
|
||||
if get_valid_entity(self) then
|
||||
if hitter:is_player() then
|
||||
local player_name = get_valid_player_name(hitter)
|
||||
if player_name == self.owner then
|
||||
local ctrl = hitter:get_player_control()
|
||||
if ctrl.sneak then
|
||||
local yaw = hitter:get_look_yaw() - math.pi * 0.5
|
||||
local v = npcf:get_walk_velocity(0.1,0, yaw)
|
||||
local pos = self.object:getpos()
|
||||
pos = vector.add(v, pos)
|
||||
self.object:setpos(pos)
|
||||
self.origin.pos = pos
|
||||
end
|
||||
end
|
||||
end
|
||||
if type(def.on_punch) == "function" then
|
||||
def.on_punch(self, hitter)
|
||||
end
|
||||
end
|
||||
end,
|
||||
on_step = function(self, dtime)
|
||||
self.timer = self.timer + dtime
|
||||
if type(def.on_step) == "function" and get_valid_entity(self) then
|
||||
def.on_step(self, dtime)
|
||||
end
|
||||
end,
|
||||
on_tell = function(self, sender, message)
|
||||
if type(def.on_tell) == "function" and get_valid_entity(self) then
|
||||
local player = minetest.get_player_by_name(sender)
|
||||
local senderpos
|
||||
if player then
|
||||
senderpos = player:getpos()
|
||||
else
|
||||
senderpos = {0,0,0}
|
||||
end
|
||||
def.on_tell(self, sender, senderpos, message)
|
||||
end
|
||||
end,
|
||||
get_staticdata = function(self)
|
||||
local npc_data = {
|
||||
name = self.name,
|
||||
npc_name = self.npc_name,
|
||||
owner = self.owner,
|
||||
origin = self.origin,
|
||||
pos = self.object:getpos(),
|
||||
properties = self.properties,
|
||||
metadata = self.metadata,
|
||||
}
|
||||
return minetest.serialize(npc_data)
|
||||
end,
|
||||
})
|
||||
local groups = {falling_node=1}
|
||||
if NPCF_SHOW_IN_CREATIVE == false then
|
||||
groups.not_in_creative_inventory=1
|
||||
end
|
||||
minetest.register_node(name.."_spawner", {
|
||||
description = def.description,
|
||||
inventory_image = minetest.inventorycube("npcf_inv_top.png", def.inventory_image, def.inventory_image),
|
||||
tiles = {"npcf_inv_top.png", def.inventory_image, def.inventory_image},
|
||||
paramtype2 = "facedir",
|
||||
groups = groups,
|
||||
sounds = default.node_sound_defaults(),
|
||||
on_construct = function(pos)
|
||||
local meta = minetest.get_meta(pos)
|
||||
meta:set_string("formspec", form_reg)
|
||||
meta:set_string("infotext", def.description.." spawner")
|
||||
end,
|
||||
after_place_node = function(pos, placer, itemstack)
|
||||
local meta = minetest.get_meta(pos)
|
||||
meta:set_string("owner", placer:get_player_name())
|
||||
itemstack:take_item()
|
||||
return itemstack
|
||||
end,
|
||||
on_punch = function(pos, node, puncher)
|
||||
local meta = minetest.get_meta(pos)
|
||||
local owner = meta:get_string("owner")
|
||||
local player_name = puncher:get_player_name()
|
||||
local admin = minetest.check_player_privs(player_name, {server=true})
|
||||
if admin or player_name == owner then
|
||||
minetest.remove_node(pos)
|
||||
if player_name == owner then
|
||||
puncher:get_inventory():add_item("main", node)
|
||||
end
|
||||
end
|
||||
end,
|
||||
on_receive_fields = function(pos, formname, fields, sender)
|
||||
if fields.cancel then
|
||||
return
|
||||
end
|
||||
local meta = minetest.get_meta(pos)
|
||||
local owner = meta:get_string("owner")
|
||||
local player_name = sender:get_player_name()
|
||||
if player_name == owner then
|
||||
if get_valid_npc_name(fields.name) then
|
||||
if index[fields.name] then
|
||||
minetest.chat_send_player(player_name, "Error: Name Already Taken!")
|
||||
return
|
||||
end
|
||||
else
|
||||
minetest.chat_send_player(player_name, "Error: Invalid NPC Name!")
|
||||
return
|
||||
end
|
||||
minetest.remove_node(pos)
|
||||
local npc_pos = {x=pos.x, y=pos.y + 0.5, z=pos.z}
|
||||
local yaw = sender:get_look_yaw() + math.pi * 0.5
|
||||
local luaentity = npcf:spawn(npc_pos, name, {
|
||||
owner = player_name,
|
||||
npc_name = fields.name,
|
||||
origin = {pos=npc_pos, yaw=yaw}
|
||||
})
|
||||
if luaentity and type(def.on_registration) == "function" then
|
||||
def.on_registration(luaentity, pos, sender)
|
||||
end
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
function npcf:spawn(pos, name, def)
|
||||
if pos and name and def.npc_name and def.owner then
|
||||
if get_valid_npc_name(def.npc_name) and index[def.npc_name] == nil then
|
||||
local entity = minetest.add_entity(pos, name)
|
||||
if entity then
|
||||
local luaentity = entity:get_luaentity()
|
||||
if luaentity then
|
||||
index[def.npc_name] = def.owner
|
||||
luaentity.owner = def.owner
|
||||
luaentity.npc_name = def.npc_name
|
||||
luaentity.origin = def.origin or {pos=pos, yaw=0}
|
||||
if def.origin then
|
||||
luaentity.object:setyaw(luaentity.origin.yaw)
|
||||
end
|
||||
if npcf:save(luaentity) then
|
||||
local output = io.open(NPCF_DATADIR.."/index.txt", 'w')
|
||||
if output then
|
||||
output:write(minetest.serialize(index))
|
||||
io.close(output)
|
||||
if NPCF_SHOW_NAMETAGS == true and luaentity.show_nametag == true then
|
||||
add_nametag(luaentity)
|
||||
end
|
||||
return luaentity
|
||||
else
|
||||
minetest.log("error", "Failed to add "..def.npc_name.." to NPC index")
|
||||
end
|
||||
else
|
||||
minetest.log("error", "Failed to save NPC "..def.npc_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function npcf:clear(npc_name)
|
||||
if get_valid_npc_name(npc_name) then
|
||||
for _,ref in pairs(minetest.luaentities) do
|
||||
if ref.object and ref.npc_name == npc_name then
|
||||
ref.object:remove()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function npcf:load(npc_name, pos)
|
||||
if get_valid_npc_name(npc_name) then
|
||||
npcf:clear(npc_name)
|
||||
local input = io.open(NPCF_DATADIR.."/"..npc_name..".npc", "r")
|
||||
if input then
|
||||
local data = minetest.deserialize(input:read('*all'))
|
||||
io.close(input)
|
||||
if data then
|
||||
if pos and data.origin then
|
||||
data.origin.pos = pos
|
||||
end
|
||||
if data.origin.pos then
|
||||
local npc = minetest.add_entity(data.origin.pos, data.name)
|
||||
if npc then
|
||||
local luaentity = npc:get_luaentity()
|
||||
if luaentity then
|
||||
luaentity.owner = data.owner
|
||||
luaentity.npc_name = npc_name
|
||||
luaentity.origin = data.origin
|
||||
luaentity.animation = data.animation
|
||||
luaentity.metadata = data.metadata
|
||||
luaentity.object:setyaw(data.origin.yaw)
|
||||
luaentity.properties = data.properties
|
||||
luaentity.object:set_properties(data.properties)
|
||||
if NPCF_SHOW_NAMETAGS == true and luaentity.show_nametag == true then
|
||||
add_nametag(luaentity)
|
||||
end
|
||||
return 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
minetest.log("error", "Failed to load "..npc_name)
|
||||
return
|
||||
end
|
||||
minetest.log("error", "Attempt to load invalid NPC")
|
||||
end
|
||||
|
||||
function npcf:save(luaentity)
|
||||
if get_valid_entity(luaentity) then
|
||||
local npc = {
|
||||
name = luaentity.name,
|
||||
owner = luaentity.owner,
|
||||
origin = luaentity.origin,
|
||||
animation = luaentity.animation,
|
||||
metadata = luaentity.metadata,
|
||||
properties = luaentity.properties,
|
||||
}
|
||||
local npc_name = luaentity.npc_name
|
||||
local output = io.open(NPCF_DATADIR.."/"..npc_name..".npc", 'w')
|
||||
if output then
|
||||
output:write(minetest.serialize(npc))
|
||||
io.close(output)
|
||||
return 1
|
||||
end
|
||||
minetest.log("error", "Failed to save NPC "..npc_name)
|
||||
return
|
||||
end
|
||||
minetest.log("error", "Attempt to save invalid NPC")
|
||||
end
|
||||
|
||||
function npcf:set_animation(luaentity, state)
|
||||
if get_valid_entity(luaentity) and state then
|
||||
if state ~= luaentity.state then
|
||||
local speed = luaentity.animation_speed
|
||||
local anim = luaentity.animation
|
||||
if speed and anim then
|
||||
if state == NPCF_ANIM_STAND and anim.stand_START and anim.stand_END then
|
||||
luaentity.object:set_animation({x=anim.stand_START, y=anim.stand_END}, speed)
|
||||
elseif state == NPCF_ANIM_SIT and anim.sit_START and anim.sit_END then
|
||||
luaentity.object:set_animation({x=anim.sit_START, y=anim.sit_END}, speed)
|
||||
elseif state == NPCF_ANIM_LAY and anim.lay_START and anim.lay_END then
|
||||
luaentity.object:set_animation({x=anim.lay_START, y=anim.lay_END}, speed)
|
||||
elseif state == NPCF_ANIM_WALK and anim.walk_START and anim.walk_END then
|
||||
luaentity.object:set_animation({x=anim.walk_START, y=anim.walk_END}, speed)
|
||||
elseif state == NPCF_ANIM_WALK_MINE and anim.walk_mine_START and anim.walk_mine_END then
|
||||
luaentity.object:set_animation({x=anim.walk_mine_START, y=anim.walk_mine_END}, speed)
|
||||
elseif state == NPCF_ANIM_MINE and anim.mine_START and anim.mine_END then
|
||||
luaentity.object:set_animation({x=anim.mine_START, y=anim.mine_END}, speed)
|
||||
end
|
||||
luaentity.state = state
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function npcf:get_index()
|
||||
return index
|
||||
end
|
||||
|
||||
function npcf:get_luaentity(npc_name)
|
||||
if get_valid_npc_name(npc_name) then
|
||||
for _,ref in pairs(minetest.luaentities) do
|
||||
if ref.object then
|
||||
if ref.npcf_id == "npc" and ref.npc_name == npc_name then
|
||||
return ref.object:get_luaentity()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function npcf:get_face_direction(v1, v2)
|
||||
if v1 and v2 then
|
||||
if v1.x and v2.x and v1.z and v2.z then
|
||||
dx = v1.x - v2.x
|
||||
dz = v2.z - v1.z
|
||||
return math.atan2(dx, dz)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function npcf:get_walk_velocity(speed, y, yaw)
|
||||
if speed and y and yaw then
|
||||
if speed > 0 then
|
||||
yaw = yaw + math.pi * 0.5
|
||||
local x = math.cos(yaw) * speed
|
||||
local z = math.sin(yaw) * speed
|
||||
return {x=x, y=y, z=z}
|
||||
end
|
||||
return {x=0, y=y, z=0}
|
||||
end
|
||||
end
|
||||
|
||||
function npcf:show_formspec(player_name, npc_name, formspec)
|
||||
if player_name and npc_name and formspec then
|
||||
minetest.show_formspec(player_name, "npcf_"..npc_name, formspec)
|
||||
end
|
||||
end
|
||||
|
||||
minetest.register_on_player_receive_fields(function(player, formname, fields)
|
||||
if formname and not fields.quit then
|
||||
local npc_name = formname:gsub("npcf_", "")
|
||||
if npc_name ~= formname then
|
||||
local luaentity = npcf:get_luaentity(npc_name)
|
||||
if luaentity then
|
||||
for k,v in pairs(fields) do
|
||||
if k ~= "" then
|
||||
v = string.gsub(v, "^CHG:", "")
|
||||
luaentity.metadata[k] = v
|
||||
end
|
||||
end
|
||||
if type(luaentity.on_receive_fields) == "function" then
|
||||
luaentity.on_receive_fields(luaentity, fields, player)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
minetest.register_entity("npcf:nametag", nametag)
|
||||
|
|
@ -0,0 +1,309 @@
|
|||
local MAX_SPEED = 5
|
||||
local MAX_POS = 1000
|
||||
local DEFAULT_NODE = {name="air"}
|
||||
local SCHEMS = {"basic_hut.we"}
|
||||
local MODPATH = minetest.get_modpath("instabuild")
|
||||
if MODPATH then
|
||||
for _,v in ipairs({"factory.we", "large_warehouse.we", "small_farm.we", "tall_tower.we",
|
||||
"large_farm.we", "mansion.we", "small_house.we", "large_house.we", "modern_house.we",
|
||||
"small_hut.we", "large_hut.we", "short_tower.we", "small_warehouse.we"}) do
|
||||
table.insert(SCHEMS, v)
|
||||
end
|
||||
end
|
||||
|
||||
local function reset_build(self)
|
||||
self.var.nodedata = {}
|
||||
self.var.nodelist = {}
|
||||
self.metadata.index = nil
|
||||
self.metadata.schematic = nil
|
||||
self.metadata.build_pos = nil
|
||||
self.metadata.building = false
|
||||
end
|
||||
|
||||
local function get_registered_nodename(name)
|
||||
if string.find(name, "^doors") then
|
||||
name = name:gsub("_[tb]_[12]", "")
|
||||
elseif string.find(name, "^stairs") then
|
||||
name = name:gsub("upside_down", "")
|
||||
elseif string.find(name, "^farming") then
|
||||
name = name:gsub("_%d", "")
|
||||
end
|
||||
return name
|
||||
end
|
||||
|
||||
local function load_schematic(self, filename)
|
||||
local input = nil
|
||||
if MODPATH then
|
||||
input = io.open(MODPATH.."/models/"..filename, "r")
|
||||
end
|
||||
if not input then
|
||||
input = io.open(NPCF_MODPATH.."/schems/"..filename, "r")
|
||||
end
|
||||
if input then
|
||||
local data = minetest.deserialize(input:read('*all'))
|
||||
io.close(input)
|
||||
table.sort(data, function(a,b)
|
||||
if a.y == b.y then
|
||||
if a.z == b.z then
|
||||
return a.x > b.x
|
||||
end
|
||||
return a.z > b.z
|
||||
end
|
||||
return a.y > b.y
|
||||
end)
|
||||
local sorted = {}
|
||||
local pos = {x=0, y=0, z=0}
|
||||
while #data > 0 do
|
||||
local index = 1
|
||||
local min_pos = {x=MAX_POS, y=MAX_POS, z=MAX_POS}
|
||||
for i,v in ipairs(data) do
|
||||
if v.y < min_pos.y or vector.distance(pos, v) < vector.distance(pos, min_pos) then
|
||||
min_pos = v
|
||||
index = i
|
||||
end
|
||||
end
|
||||
local node = data[index]
|
||||
table.insert(sorted, node)
|
||||
table.remove(data, index)
|
||||
pos = {x=node.x, y=node.y, z=node.z}
|
||||
end
|
||||
self.var.nodedata = {}
|
||||
self.var.nodelist = {}
|
||||
for i,v in ipairs(sorted) do
|
||||
if v.name and v.param1 and v.param2 and v.x and v.y and v.z then
|
||||
local node = {name=v.name, param1=v.param1, param2=v.param2}
|
||||
local pos = vector.add(self.metadata.build_pos, {x=v.x, y=v.y, z=v.z})
|
||||
local name = get_registered_nodename(v.name)
|
||||
if minetest.registered_items[name] then
|
||||
self.metadata.inventory[name] = self.metadata.inventory[name] or 0
|
||||
self.var.nodelist[name] = self.var.nodelist[name] or 0
|
||||
self.var.nodelist[name] = self.var.nodelist[name] + 1
|
||||
else
|
||||
node = DEFAULT_NODE
|
||||
end
|
||||
self.var.nodedata[i] = {pos=pos, node=node}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function show_build_form(self, player_name)
|
||||
local nodelist = {}
|
||||
for k,v in pairs(self.var.nodelist) do
|
||||
if string.find(k, "^doors") then
|
||||
v = v * 0.5
|
||||
end
|
||||
if self.metadata.inventory[k] then
|
||||
v = v - self.metadata.inventory[k]
|
||||
end
|
||||
if v < 0 then
|
||||
v = 0
|
||||
end
|
||||
if minetest.registered_items[k].description ~= "" then
|
||||
k = minetest.registered_items[k].description
|
||||
end
|
||||
table.insert(nodelist, k.." ("..v..")")
|
||||
end
|
||||
local materials = table.concat(nodelist, ",") or ""
|
||||
local title = self.metadata.schematic:gsub("%.we","")
|
||||
local button_build = "button_exit[5.0,1.0;3.0,0.5;build_start;Begin Build]"
|
||||
if self.metadata.index then
|
||||
button_build = "button_exit[5.0,1.0;3.0,0.5;build_resume;Resume Build]"
|
||||
end
|
||||
local formspec = "size[8,9]"
|
||||
.."label[3.0,0.0;Project: "..title.."]"
|
||||
.."textlist[0.0,1.0;4.0,3.5;inv_sel;"..materials..";"..self.var.selected..";]"
|
||||
.."list[current_player;main;0.0,5.0;8.0,4.0;]"
|
||||
..button_build
|
||||
if NPCF_BUILDER_REQ_MATERIALS == true then
|
||||
formspec = formspec.."list[detached:npcf_"..self.npc_name..";input;6.0,3.5;1,1;]"
|
||||
end
|
||||
if self.owner == player_name then
|
||||
formspec = formspec.."button_exit[5.0,2.0;3.0,0.5;build_cancel;Cancel Build]"
|
||||
end
|
||||
npcf:show_formspec(player_name, self.npc_name, formspec)
|
||||
end
|
||||
|
||||
local function get_speed(distance)
|
||||
local speed = distance * 0.5
|
||||
if speed > MAX_SPEED then
|
||||
speed = MAX_SPEED
|
||||
end
|
||||
return speed
|
||||
end
|
||||
|
||||
npcf:register_npc("npcf:builder_npc" ,{
|
||||
description = "Builder NPC",
|
||||
textures = {"npcf_skin_builder.png"},
|
||||
nametag_color = "green",
|
||||
metadata = {
|
||||
schematic = nil,
|
||||
inventory = {},
|
||||
index = nil,
|
||||
build_pos = nil,
|
||||
building = false,
|
||||
},
|
||||
var = {
|
||||
selected = "",
|
||||
nodelist = {},
|
||||
nodedata = {},
|
||||
last_pos = {},
|
||||
},
|
||||
stepheight = 1,
|
||||
inventory_image = "npcf_inv_builder_npc.png",
|
||||
on_construct = function(self)
|
||||
self.metadata.building = false
|
||||
self.object:setvelocity({x=0, y=0, z=0})
|
||||
self.object:setacceleration({x=0, y=-10, z=0})
|
||||
if self.metadata.schematic and self.metadata.build_pos then
|
||||
load_schematic(self, self.metadata.schematic)
|
||||
end
|
||||
end,
|
||||
on_activate = function(self, staticdata, dtime_s)
|
||||
local inv = minetest.create_detached_inventory("npcf_"..self.npc_name, {
|
||||
on_put = function(inv, listname, index, stack, player)
|
||||
local player_name = player:get_player_name()
|
||||
local item = stack:get_name()
|
||||
if player_name and self.metadata.inventory[item] then
|
||||
self.metadata.inventory[item] = self.metadata.inventory[item] + stack:get_count()
|
||||
inv:remove_item("input", stack)
|
||||
show_build_form(self, player_name)
|
||||
end
|
||||
end,
|
||||
})
|
||||
inv:set_size("input", 1)
|
||||
end,
|
||||
on_rightclick = function(self, clicker)
|
||||
local player_name = clicker:get_player_name()
|
||||
if self.owner == player_name then
|
||||
if not self.metadata.schematic then
|
||||
local schemlist = table.concat(SCHEMS, ",") or ""
|
||||
local formspec = "size[6,5]"
|
||||
.."textlist[0.0,0.0;5.0,4.0;schemlist;"..schemlist..";;]"
|
||||
.."button_exit[5.0,4.5;1.0,0.5;;Ok]"
|
||||
npcf:show_formspec(player_name, self.npc_name, formspec)
|
||||
return
|
||||
elseif self.metadata.building == true then
|
||||
self.metadata.building = false
|
||||
return
|
||||
end
|
||||
end
|
||||
if self.metadata.schematic and self.metadata.building == false then
|
||||
show_build_form(self, player_name)
|
||||
end
|
||||
end,
|
||||
on_step = function(self, dtime)
|
||||
if self.timer > 1 then
|
||||
self.timer = 0
|
||||
if not self.owner then
|
||||
return
|
||||
end
|
||||
local pos = self.object:getpos()
|
||||
local yaw = self.object:getyaw()
|
||||
local state = NPCF_ANIM_STAND
|
||||
local speed = 0
|
||||
if self.metadata.building == true then
|
||||
local nodedata = self.var.nodedata[self.metadata.index]
|
||||
pos.y = math.floor(pos.y)
|
||||
local acceleration = {x=0, y=-10, z=0}
|
||||
if pos.y < nodedata.pos.y then
|
||||
if self.object:getacceleration().y > 0 and self.var.last_pos.y == pos.y then
|
||||
self.object:setpos({x=pos.x, y=nodedata.pos.y + 1.5, z=pos.z})
|
||||
acceleration = {x=0, y=0, z=0}
|
||||
else
|
||||
acceleration = {x=0, y=0.1, z=0}
|
||||
end
|
||||
end
|
||||
self.var.last_pos = pos
|
||||
self.object:setacceleration(acceleration)
|
||||
yaw = npcf:get_face_direction(pos, nodedata.pos)
|
||||
local distance = vector.distance(pos, nodedata.pos)
|
||||
if distance < 4 then
|
||||
if minetest.registered_items[nodedata.node.name].sounds then
|
||||
local soundspec = minetest.registered_items[nodedata.node.name].sounds.place
|
||||
if soundspec then
|
||||
soundspec.pos = pos
|
||||
minetest.sound_play(soundspec.name, soundspec)
|
||||
end
|
||||
end
|
||||
minetest.add_node(nodedata.pos, nodedata.node)
|
||||
local door_top = string.find(nodedata.node.name, "^doors+_t_[12]$")
|
||||
if NPCF_BUILDER_REQ_MATERIALS == true and not door_top then
|
||||
local name = get_registered_nodename(nodedata.node.name)
|
||||
if self.metadata.inventory[name] > 0 then
|
||||
self.metadata.inventory[name] = self.metadata.inventory[name] - 1
|
||||
self.var.selected = ""
|
||||
else
|
||||
self.metadata.building = false
|
||||
state = NPCF_ANIM_STAND
|
||||
speed = 0
|
||||
local i = 0
|
||||
for k,v in pairs(self.var.nodelist) do
|
||||
i = i + 1
|
||||
if k == name then
|
||||
self.var.selected = i
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
self.metadata.index = self.metadata.index + 1
|
||||
if self.metadata.index > #self.var.nodedata then
|
||||
reset_build(self)
|
||||
end
|
||||
state = NPCF_ANIM_WALK_MINE
|
||||
speed = 1
|
||||
else
|
||||
state = NPCF_ANIM_WALK
|
||||
speed = get_speed(distance)
|
||||
end
|
||||
elseif vector.equals(pos, self.origin.pos) == false then
|
||||
self.object:setacceleration({x=0, y=-10, z=0})
|
||||
yaw = npcf:get_face_direction(pos, self.origin.pos)
|
||||
local distance = vector.distance(pos, self.origin.pos)
|
||||
if distance > 1 then
|
||||
speed = get_speed(distance)
|
||||
state = NPCF_ANIM_WALK
|
||||
else
|
||||
self.object:setpos(self.origin.pos)
|
||||
yaw = self.origin.yaw
|
||||
end
|
||||
end
|
||||
self.object:setvelocity(npcf:get_walk_velocity(speed, self.object:getvelocity().y, yaw))
|
||||
self.object:setyaw(yaw)
|
||||
npcf:set_animation(self, state)
|
||||
end
|
||||
end,
|
||||
on_receive_fields = function(self, fields, sender)
|
||||
local player_name = sender:get_player_name()
|
||||
if self.owner == player_name then
|
||||
if fields.schemlist then
|
||||
local id = tonumber(string.match(fields.schemlist, "%d+"))
|
||||
if id then
|
||||
if SCHEMS[id] then
|
||||
local pos = {
|
||||
x=math.ceil(self.origin.pos.x) + 1,
|
||||
y=math.floor(self.origin.pos.y),
|
||||
z=math.ceil(self.origin.pos.z) + 1
|
||||
}
|
||||
self.metadata.schematic = SCHEMS[id]
|
||||
self.metadata.build_pos = pos
|
||||
load_schematic(self, self.metadata.schematic)
|
||||
end
|
||||
end
|
||||
elseif fields.build_cancel then
|
||||
reset_build(self)
|
||||
end
|
||||
end
|
||||
if fields.build_start then
|
||||
for i,v in ipairs(self.var.nodedata) do
|
||||
minetest.remove_node(v.pos)
|
||||
end
|
||||
self.metadata.index = 1
|
||||
self.metadata.building = true
|
||||
elseif fields.build_resume then
|
||||
self.metadata.building = true
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
local WALKING_SPEED = 1
|
||||
local RUNNING_SPEED = 2.5
|
||||
local FOLLOW_RADIUS_MIN = 5
|
||||
local FOLLOW_RADIUS_MAX = 30
|
||||
local AVOIDED_NODES = {
|
||||
"ignore",
|
||||
"default:water_source",
|
||||
"default:water_flowing",
|
||||
"default:lava_source",
|
||||
"default:lava_flowing",
|
||||
}
|
||||
local ANIMATION = {
|
||||
["Stand"] = {id=1, state=NPCF_ANIM_STAND},
|
||||
["Sit"] = {id=2, state=NPCF_ANIM_SIT},
|
||||
["Lay"] = {id=3, state=NPCF_ANIM_LAY},
|
||||
["Mine"] = {id=4, state=NPCF_ANIM_MINE},
|
||||
}
|
||||
|
||||
local function get_target_player(self)
|
||||
local target_player = nil
|
||||
local min_dist = FOLLOW_RADIUS_MAX
|
||||
for _,player in ipairs(minetest.get_connected_players()) do
|
||||
if player then
|
||||
local pos = player:getpos()
|
||||
if pos then
|
||||
local dist = vector.distance(pos, pos)
|
||||
if dist < min_dist then
|
||||
target_player = player
|
||||
min_dist = dist
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return target_player
|
||||
end
|
||||
|
||||
npcf:register_npc("npcf:deco_npc" ,{
|
||||
description = "Decorative NPC",
|
||||
mesh = "npcf_deco.x",
|
||||
textures = {"npcf_skin_deco.png"},
|
||||
nametag_color = "magenta",
|
||||
animation_speed = 12,
|
||||
animation = {
|
||||
stand_START = 0,
|
||||
stand_END = 79,
|
||||
sit_START = 81,
|
||||
sit_END = 160,
|
||||
lay_START = 162,
|
||||
lay_END = 166,
|
||||
walk_START = 168,
|
||||
walk_END = 187,
|
||||
mine_START = 189,
|
||||
mine_END = 198,
|
||||
run_START = 221,
|
||||
run_END = 240,
|
||||
},
|
||||
metadata = {
|
||||
free_roaming = "false",
|
||||
follow_players = "false",
|
||||
anim_stop = "Stand",
|
||||
},
|
||||
var = {
|
||||
speed = 1,
|
||||
avoid_dir = 1,
|
||||
last_pos = {x=0,y=0,z=0},
|
||||
target = nil,
|
||||
},
|
||||
stepheight = 1,
|
||||
inventory_image = "npcf_inv_deco_npc.png",
|
||||
on_construct = function(self)
|
||||
self.object:setvelocity({x=0, y=0, z=0})
|
||||
self.object:setacceleration({x=0, y=-10, z=0})
|
||||
npcf:set_animation(self, ANIMATION[self.metadata.anim_stop].state)
|
||||
end,
|
||||
on_activate = function(self, staticdata, dtime_s)
|
||||
|
||||
-- Deal with legacy errors where these fields sometimes had
|
||||
-- invalid values...
|
||||
if self.metadata.follow_players == true then
|
||||
self.metadata.follow_players = "true"
|
||||
elseif self.metadata.follow_players == false then
|
||||
self.metadata.follow_players = "false"
|
||||
end
|
||||
if self.metadata.free_roaming == true then
|
||||
self.metadata.free_roaming = "true"
|
||||
elseif self.metadata.free_roaming == false then
|
||||
self.metadata.free_roaming = "false"
|
||||
end
|
||||
|
||||
if self.metadata.follow_players == "true" then
|
||||
self.var.target = get_target_player(self)
|
||||
end
|
||||
end,
|
||||
on_rightclick = function(self, clicker)
|
||||
local player_name = clicker:get_player_name()
|
||||
local message = "Hello, my name is "..self.npc_name
|
||||
if self.metadata.message then
|
||||
message = minetest.formspec_escape(self.metadata.message)
|
||||
end
|
||||
local formspec
|
||||
if player_name == self.owner then
|
||||
local selected_id = ANIMATION[self.metadata.anim_stop].id or ""
|
||||
formspec = "size[8,4.0]"
|
||||
.."field[0.5,1.0;7.5,0.5;message;Message;"..message.."]"
|
||||
.."label[0.5,1.8;Stationary Animation\\:]"
|
||||
.."dropdown[4.0,1.8;3.5;anim_stop;Stand,Sit,Lay,Mine;"..selected_id.."]"
|
||||
.."checkbox[0.5,2.7;follow_players;Follow Players;"..self.metadata.follow_players.."]"
|
||||
.."button_exit[7.0,3.5;1.0,0.5;;Ok]"
|
||||
if NPCF_DECO_FREE_ROAMING == true then
|
||||
formspec = formspec.."checkbox[3.5,2.7;free_roaming;Wander Map;"..self.metadata.free_roaming.."]"
|
||||
end
|
||||
else
|
||||
formspec = "size[8,4]"
|
||||
.."label[0,0;"..message.."]"
|
||||
end
|
||||
self.var.speed = 0
|
||||
npcf:show_formspec(player_name, self.npc_name, formspec)
|
||||
end,
|
||||
on_step = function(self, dtime)
|
||||
if self.timer > 1 then
|
||||
self.timer = 0
|
||||
local speed = 0
|
||||
local pos = self.object:getpos()
|
||||
local yaw = self.object:getyaw()
|
||||
local turn = pos.x == self.var.last_pos.x and pos.z == self.var.last_pos.z
|
||||
local acceleration = {x=0, y=-10, z=0}
|
||||
local velocity = self.object:getvelocity()
|
||||
local roaming = NPCF_DECO_FREE_ROAMING == true and self.metadata.free_roaming == "true"
|
||||
if roaming == true or self.metadata.follow_players == "true" then
|
||||
speed = self.var.speed
|
||||
if math.random(10) == 1 then
|
||||
if speed == 0 or speed == RUNNING_SPEED then
|
||||
speed = WALKING_SPEED
|
||||
elseif math.random(5) == 1 then
|
||||
speed = RUNNING_SPEED
|
||||
elseif math.random(5) == 1 then
|
||||
speed = 0
|
||||
end
|
||||
elseif math.random(30) == 1 then
|
||||
self.var.avoid_dir = self.var.avoid_dir * -1
|
||||
end
|
||||
if self.metadata.follow_players == "true" then
|
||||
local valid_target = false
|
||||
if self.var.target then
|
||||
local target = self.var.target:getpos()
|
||||
if target then
|
||||
valid_target = true
|
||||
yaw = npcf:get_face_direction(pos, target)
|
||||
if vector.distance(pos, target) < FOLLOW_RADIUS_MIN then
|
||||
speed = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
if math.random(10) == 1 or valid_target == false then
|
||||
self.var.target = get_target_player(self)
|
||||
end
|
||||
end
|
||||
if speed ~= 0 then
|
||||
local node_pos = vector.add(npcf:get_walk_velocity(5, 0, yaw), pos)
|
||||
node_pos = vector.round(node_pos)
|
||||
local air_content = 0
|
||||
for i = 1,5 do
|
||||
local test_pos = {x=node_pos.x, y=node_pos.y-i, z=node_pos.z}
|
||||
local node = minetest.get_node(test_pos)
|
||||
if node.name == "air" then
|
||||
air_content = air_content + 1
|
||||
end
|
||||
for _,v in ipairs(AVOIDED_NODES) do
|
||||
if node.name == v then
|
||||
turn = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if turn == false then
|
||||
local objects = minetest.get_objects_inside_radius(node_pos, 2)
|
||||
if #objects > 0 then
|
||||
turn = true
|
||||
end
|
||||
end
|
||||
if turn == true or air_content == 5 then
|
||||
yaw = yaw + math.pi * 0.5 * self.var.avoid_dir
|
||||
speed = WALKING_SPEED
|
||||
elseif pos.x == self.var.last_pos.x or pos.z == self.var.last_pos.z then
|
||||
yaw = yaw + math.pi * 0.25 * self.var.avoid_dir
|
||||
end
|
||||
if roaming == true then
|
||||
if math.random(4) == 1 then
|
||||
yaw = yaw + (math.random(3) - 2) * 0.25
|
||||
end
|
||||
elseif self.var.target == nil then
|
||||
speed = 0
|
||||
end
|
||||
end
|
||||
self.var.speed = speed
|
||||
self.object:setyaw(yaw)
|
||||
end
|
||||
self.var.last_pos = pos
|
||||
if speed == 0 then
|
||||
npcf:set_animation(self, ANIMATION[self.metadata.anim_stop].state)
|
||||
elseif speed == RUNNING_SPEED then
|
||||
self.object:set_animation({x=self.animation.run_START, y=self.animation.run_END}, 20)
|
||||
self.state = 0
|
||||
else
|
||||
npcf:set_animation(self, NPCF_ANIM_WALK)
|
||||
end
|
||||
local node = minetest.get_node(pos)
|
||||
if string.find(node.name, "^default:water") then
|
||||
acceleration = {x=0, y=-4, z=0}
|
||||
velocity = {x=0, y=3, z=0}
|
||||
elseif minetest.find_node_near(pos, 2, {"group:water"}) then
|
||||
acceleration = {x=0, y=-1, z=0}
|
||||
end
|
||||
self.object:setvelocity(npcf:get_walk_velocity(speed, velocity.y, yaw))
|
||||
self.object:setacceleration(acceleration)
|
||||
end
|
||||
end,
|
||||
on_receive_fields = function(self, fields, sender)
|
||||
if fields.free_roaming or fields.follow_players then
|
||||
self.var.speed = WALKING_SPEED
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
local TARGET_RADIUS = 10
|
||||
local MAX_SPEED = 5
|
||||
|
||||
local function get_wield_image(item)
|
||||
local wield_image = "npcf_trans.png"
|
||||
if minetest.registered_items[item] then
|
||||
if minetest.registered_items[item].inventory_image ~= "" then
|
||||
wield_image = minetest.registered_items[item].inventory_image
|
||||
end
|
||||
end
|
||||
return wield_image
|
||||
end
|
||||
|
||||
local function get_speed(distance)
|
||||
local speed = distance * 0.5
|
||||
if speed > MAX_SPEED then
|
||||
speed = MAX_SPEED
|
||||
end
|
||||
return speed
|
||||
end
|
||||
|
||||
local function get_name_in_list(list, name)
|
||||
list = list:gsub(",", " ")
|
||||
for k,v in ipairs(list:split(" ")) do
|
||||
if k ~= "" and v == name then
|
||||
return name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function get_armor_texture(self)
|
||||
if self.metadata.show_armor == "true" then
|
||||
return "npcf_skin_armor.png"
|
||||
end
|
||||
return "npcf_trans.png"
|
||||
end
|
||||
|
||||
npcf:register_npc("npcf:guard_npc" ,{
|
||||
description = "Guard NPC",
|
||||
mesh = "npcf_guard.x",
|
||||
textures = {"character.png", "npcf_skin_armor.png", "npcf_trans.png"},
|
||||
nametag_color = "red",
|
||||
inventory_image = "npcf_inv_guard_npc.png",
|
||||
stepheight = 1,
|
||||
metadata = {
|
||||
wielditem = "default:sword_steel",
|
||||
blacklist = "mobs:oerkki mobs:dungeon_master",
|
||||
whitelist = "",
|
||||
attack_players = "false",
|
||||
follow_owner = "false",
|
||||
show_armor = "true",
|
||||
},
|
||||
on_activate = function(self, staticdata, dtime_s)
|
||||
self.object:setvelocity({x=0, y=0, z=0})
|
||||
self.object:setacceleration({x=0, y=-10, z=0})
|
||||
local wield_image = get_wield_image(self.metadata.wielditem)
|
||||
local textures = {self.properties.textures[1], get_armor_texture(self), wield_image}
|
||||
self.object:set_properties({textures = textures})
|
||||
end,
|
||||
on_rightclick = function(self, clicker)
|
||||
local player_name = clicker:get_player_name()
|
||||
if player_name == self.owner then
|
||||
local blacklist = minetest.formspec_escape(self.metadata.blacklist)
|
||||
local whitelist = minetest.formspec_escape(self.metadata.whitelist)
|
||||
local formspec = "size[8,6.5]"
|
||||
.."field[0.5,1.0;3.5,0.5;wielditem;Weapon;"..self.metadata.wielditem.."]"
|
||||
.."checkbox[4.0,0.5;show_armor;Show 3D Armor;"..self.metadata.show_armor.."]"
|
||||
.."field[0.5,2.5;7.5.0,0.5;blacklist;Blacklist (Mob Entities);"..blacklist.."]"
|
||||
.."field[0.5,4.0;7.5.0,0.5;whitelist;Whitelist (Player Names);"..whitelist.."]"
|
||||
.."checkbox[0.5,4.5;follow_owner;Follow;"..self.metadata.follow_owner.."]"
|
||||
.."button[0.0,6.0;2.0,0.5;origin;Set Origin]"
|
||||
.."button_exit[7.0,6.0;1.0,0.5;;Ok]"
|
||||
if NPCF_GUARD_ATTACK_PLAYERS == true then
|
||||
formspec = formspec.."checkbox[4.0,4.5;attack_players;Attack Players;"
|
||||
..self.metadata.attack_players.."]"
|
||||
end
|
||||
npcf:show_formspec(player_name, self.npc_name, formspec)
|
||||
end
|
||||
end,
|
||||
on_step = function(self, dtime)
|
||||
if self.timer > 1 then
|
||||
self.timer = 0
|
||||
local pos = self.object:getpos()
|
||||
local yaw = self.object:getyaw()
|
||||
local state = NPCF_ANIM_STAND
|
||||
local target = {object=nil, distance=0}
|
||||
local min_dist = 1000
|
||||
local speed = 0
|
||||
local acceleration = {x=0, y=-10, z=0}
|
||||
local velocity = self.object:getvelocity()
|
||||
local attacking = false
|
||||
for _,object in ipairs(minetest.get_objects_inside_radius(pos, TARGET_RADIUS)) do
|
||||
local to_target = false
|
||||
if object:is_player() then
|
||||
if NPCF_GUARD_ATTACK_PLAYERS == true and self.metadata.attack_players == "true" then
|
||||
local player_name = object:get_player_name()
|
||||
if player_name ~= self.owner then
|
||||
if not get_name_in_list(self.metadata.whitelist, player_name) then
|
||||
to_target = true
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
local luaentity = object:get_luaentity()
|
||||
if luaentity then
|
||||
if luaentity.name then
|
||||
if get_name_in_list(self.metadata.blacklist, luaentity.name) then
|
||||
to_target = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if to_target == true then
|
||||
local op = object:getpos()
|
||||
local dv = vector.subtract(pos, op)
|
||||
local dy = math.abs(dv.y - 1)
|
||||
if dy < math.abs(dv.x) or dy < math.abs(dv.z) then
|
||||
local dist = math.floor(vector.distance(pos, op))
|
||||
if dist < min_dist then
|
||||
target.object = object
|
||||
target.distance = dist
|
||||
min_dist = dist
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if target.object then
|
||||
yaw = npcf:get_face_direction(pos, target.object:getpos())
|
||||
if target.distance < 3 then
|
||||
attacking = true
|
||||
state = NPCF_ANIM_MINE
|
||||
end
|
||||
if target.distance > 2 then
|
||||
speed = get_speed(target.distance) * 1.1
|
||||
state = NPCF_ANIM_WALK
|
||||
if attacking == true then
|
||||
state = NPCF_ANIM_WALK_MINE
|
||||
end
|
||||
end
|
||||
if attacking == true then
|
||||
local tool_caps = {full_punch_interval=1.0, damage_groups={fleshy=1}}
|
||||
local item = self.metadata.wielditem
|
||||
if item ~= "" and minetest.registered_items[item] then
|
||||
if minetest.registered_items[item].tool_capabilities then
|
||||
tool_caps = minetest.registered_items[item].tool_capabilities
|
||||
end
|
||||
end
|
||||
target.object:punch(self.object, self.var.timer, tool_caps)
|
||||
end
|
||||
elseif self.metadata.follow_owner == "true" then
|
||||
local player = minetest.get_player_by_name(self.owner)
|
||||
if player then
|
||||
yaw = npcf:get_face_direction(pos, player:getpos())
|
||||
local p = player:getpos()
|
||||
local distance = vector.distance(pos, {x=p.x, y=pos.y, z=p.z})
|
||||
if distance > 3 then
|
||||
speed = get_speed(distance)
|
||||
state = NPCF_ANIM_WALK
|
||||
end
|
||||
end
|
||||
elseif vector.equals(pos, self.origin.pos) == false then
|
||||
yaw = npcf:get_face_direction(pos, self.origin.pos)
|
||||
local distance = vector.distance(pos, self.origin.pos)
|
||||
if distance > 1 then
|
||||
speed = get_speed(distance)
|
||||
state = NPCF_ANIM_WALK
|
||||
else
|
||||
self.object:setpos(self.origin.pos)
|
||||
yaw = self.origin.yaw
|
||||
end
|
||||
end
|
||||
local node = minetest.get_node(pos)
|
||||
if string.find(node.name, "^default:water") then
|
||||
acceleration = {x=0, y=-4, z=0}
|
||||
velocity = {x=0, y=3, z=0}
|
||||
elseif minetest.find_node_near(pos, 2, {"group:water"}) then
|
||||
acceleration = {x=0, y=-1, z=0}
|
||||
end
|
||||
self.object:setvelocity(npcf:get_walk_velocity(speed, velocity.y, yaw))
|
||||
self.object:setacceleration(acceleration)
|
||||
self.object:setyaw(yaw)
|
||||
npcf:set_animation(self, state)
|
||||
end
|
||||
end,
|
||||
on_receive_fields = function(self, fields, sender)
|
||||
if self.owner == sender:get_player_name() then
|
||||
if fields.wielditem then
|
||||
local wield_image = get_wield_image(fields.wielditem)
|
||||
local textures = {self.properties.textures[1], get_armor_texture(self), wield_image}
|
||||
self.object:set_properties({textures = textures})
|
||||
end
|
||||
if fields.origin then
|
||||
self.origin.pos = self.object:getpos()
|
||||
self.origin.yaw = self.object:getyaw()
|
||||
end
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
local function get_formspec(text, page)
|
||||
local lines = text:split("\n")
|
||||
local start = (page - 1) * 12
|
||||
local eof = false
|
||||
local formspec = "size[8,6.5]"
|
||||
for i = 1, 12 do
|
||||
if lines[start + i] then
|
||||
formspec = formspec.."label[0.5,"..(i*0.4)..";"..lines[start+i].."]"
|
||||
else
|
||||
eof = true
|
||||
end
|
||||
end
|
||||
if page > 1 then
|
||||
formspec = formspec.."button[0.0,6.0;1,0.5;page_"..(page-1)..";<<]"
|
||||
end
|
||||
if eof == false then
|
||||
formspec = formspec.."button[7.0,6.0;1,0.5;page_"..(page+1)..";>>]"
|
||||
end
|
||||
formspec = formspec.."button_exit[3.0,6.0;2,0.5;;Exit]"
|
||||
return formspec
|
||||
end
|
||||
|
||||
npcf:register_npc("npcf:info_npc" ,{
|
||||
description = "Information NPC",
|
||||
textures = {"npcf_skin_info.png"},
|
||||
nametag_color = "cyan",
|
||||
metadata = {
|
||||
infotext = "Infotext."
|
||||
},
|
||||
inventory_image = "npcf_inv_info_npc.png",
|
||||
on_rightclick = function(self, clicker)
|
||||
local player_name = clicker:get_player_name()
|
||||
local infotext = minetest.formspec_escape(self.metadata.infotext)
|
||||
local formspec = get_formspec(infotext, 1)
|
||||
if player_name == self.owner then
|
||||
formspec = "size[8,6]"
|
||||
.."textarea[0.5,0.5;7.5,5.0;infotext;Infotext;"..infotext.."]"
|
||||
.."button[0.0,5.5;2.0,0.5;page_1;View]"
|
||||
.."button_exit[7.0,5.5;1.0,0.5;;Ok]"
|
||||
end
|
||||
npcf:show_formspec(player_name, self.npc_name, formspec)
|
||||
end,
|
||||
on_receive_fields = function(self, fields, sender)
|
||||
for k,_ in pairs(fields) do
|
||||
page = k:gsub("page_", "")
|
||||
if page ~= k then
|
||||
local formspec = get_formspec(self.metadata.infotext, tonumber(page))
|
||||
npcf:show_formspec(sender:get_player_name(), self.npc_name, formspec)
|
||||
break
|
||||
end
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
local function show_formspec(self, player_name, selected_id)
|
||||
local inv = minetest.get_inventory({type="detached", name="npcf_"..self.npc_name})
|
||||
if not inv then
|
||||
return
|
||||
end
|
||||
local trades = {}
|
||||
for i,v in ipairs(self.metadata.trades) do
|
||||
local avail = self.metadata.inventory[v.item_sell] or 0
|
||||
local item_buy = minetest.registered_items[v.item_buy].description or v.item_buy
|
||||
local item_sell = minetest.registered_items[v.item_sell].description or v.item_sell
|
||||
if item_buy and item_sell then
|
||||
local str = item_buy.." ("..v.qty_buy..") - "..item_sell.." ("..v.qty_sell..") ["..avail.."]"
|
||||
trades[i] = minetest.formspec_escape(str)
|
||||
end
|
||||
end
|
||||
local tradelist = table.concat(trades, ",") or ""
|
||||
local select = ""
|
||||
local input = inv:get_stack("input", 1)
|
||||
local output = inv:get_stack("output", 1)
|
||||
output:clear()
|
||||
if selected_id == nil and input:get_count() > 0 then
|
||||
for i,v in ipairs(self.metadata.trades) do
|
||||
if v.item_buy == input:get_name() then
|
||||
selected_id = i
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if selected_id then
|
||||
local trade = self.metadata.trades[selected_id]
|
||||
if trade then
|
||||
if self.owner == player_name then
|
||||
local qty_sell = self.metadata.inventory[trade.item_sell] or 0
|
||||
local qty_buy = self.metadata.inventory[trade.item_buy] or 0
|
||||
output = ItemStack(trade.item_sell.." "..qty_sell)
|
||||
input = ItemStack(trade.item_buy.." "..qty_buy)
|
||||
inv:set_stack("input", 1, input)
|
||||
elseif input:get_name() == trade.item_buy then
|
||||
local avail = self.metadata.inventory[trade.item_sell] or 0
|
||||
local req = math.floor(input:get_count() / trade.qty_buy) * trade.qty_sell
|
||||
if req > avail then
|
||||
req = avail
|
||||
end
|
||||
output = ItemStack(trade.item_sell.." "..req)
|
||||
end
|
||||
select = selected_id
|
||||
end
|
||||
end
|
||||
inv:set_stack("output", 1, output)
|
||||
local formspec = "size[8,10]"
|
||||
.."list[detached:npcf_"..self.npc_name..";input;0.0,3.7;1,1;]"
|
||||
.."list[detached:npcf_"..self.npc_name..";output;7.0,3.7;1,1;]"
|
||||
.."list[current_player;main;0.0,5.0;8.0,4.0;]"
|
||||
if self.owner == player_name then
|
||||
formspec = formspec
|
||||
.."textlist[0.0,0.0;5.7,3.5;inv_select;"..tradelist..";"..select..";]"
|
||||
.."field[0.3,9.7;2.0,0.5;item_buy;Item Buy;]"
|
||||
.."field[2.3,9.7;1.0,0.5;qty_buy;Qty;]"
|
||||
.."field[3.3,9.7;2.0,0.5;item_sell;Item Sell;]"
|
||||
.."field[5.3,9.7;1.0,0.5;qty_sell;Qty;]"
|
||||
.."button[6.0,9.4;2.0,0.5;trade_add;Add Trade]"
|
||||
.."list[detached:npcf_"..self.npc_name..";stock;3.5,3.7;1,1;]"
|
||||
if select ~= "" then
|
||||
formspec = formspec.."button[6.0,0.0;2.0,0.5;trade_delete_"..select..";Del Trade]"
|
||||
end
|
||||
else
|
||||
formspec = formspec
|
||||
.."textlist[0.0,0.0;7.5,3.5;inv_select;"..tradelist..";"..select..";]"
|
||||
.."button_exit[3.0,4.0;2.0,0.5;trade_accept;Accept]"
|
||||
end
|
||||
npcf:show_formspec(player_name, self.npc_name, formspec)
|
||||
end
|
||||
|
||||
local function get_field_qty(str)
|
||||
if str then
|
||||
local qty = math.floor(tonumber(str) or 1)
|
||||
if qty > 0 then
|
||||
return qty
|
||||
end
|
||||
end
|
||||
return 1
|
||||
end
|
||||
|
||||
local function is_valid_item(item)
|
||||
if item then
|
||||
if item ~= "" then
|
||||
return minetest.registered_items[item]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
npcf:register_npc("npcf:trade_npc" ,{
|
||||
description = "Trader NPC",
|
||||
mesh = "npcf_deco.x",
|
||||
textures = {"npcf_skin_trader.png"},
|
||||
nametag_color = "yellow",
|
||||
metadata = {
|
||||
trades = {},
|
||||
inventory = {},
|
||||
},
|
||||
inventory_image = "npcf_inv_trader_npc.png",
|
||||
on_activate = function(self, staticdata, dtime_s)
|
||||
local inv = minetest.create_detached_inventory("npcf_"..self.npc_name, {
|
||||
on_put = function(inv, listname, index, stack, player)
|
||||
local player_name = player:get_player_name()
|
||||
if listname == "stock" and self.owner == player_name then
|
||||
local item = stack:get_name()
|
||||
if self.metadata.inventory[item] then
|
||||
self.metadata.inventory[item] = self.metadata.inventory[item] + stack:get_count()
|
||||
inv:remove_item("stock", stack)
|
||||
end
|
||||
end
|
||||
show_formspec(self, player_name, nil)
|
||||
end,
|
||||
on_take = function(inv, listname, index, stack, player)
|
||||
if self.owner == player:get_player_name() and (listname == "input" or listname == "output") then
|
||||
local item = stack:get_name()
|
||||
if self.metadata.inventory[item] then
|
||||
self.metadata.inventory[item] = self.metadata.inventory[item] - stack:get_count()
|
||||
end
|
||||
end
|
||||
show_formspec(self, player:get_player_name(), nil)
|
||||
end,
|
||||
allow_put = function(inv, listname, index, stack, player)
|
||||
if listname == "stock" or (listname == "input" and self.owner ~= player:get_player_name()) then
|
||||
return stack:get_count()
|
||||
end
|
||||
return 0
|
||||
end,
|
||||
allow_take = function(inv, listname, index, stack, player)
|
||||
if listname == "input" or self.owner == player:get_player_name() then
|
||||
return stack:get_count()
|
||||
end
|
||||
return 0
|
||||
end,
|
||||
})
|
||||
inv:set_size("input", 1)
|
||||
inv:set_size("output", 1)
|
||||
inv:set_size("stock", 1)
|
||||
end,
|
||||
on_rightclick = function(self, clicker)
|
||||
show_formspec(self, clicker:get_player_name(), nil)
|
||||
end,
|
||||
on_receive_fields = function(self, fields, sender)
|
||||
local player_name = sender:get_player_name()
|
||||
if fields.trade_add and self.owner == player_name then
|
||||
if is_valid_item(fields.item_buy) and is_valid_item(fields.item_sell) then
|
||||
local trade = {}
|
||||
trade["item_buy"] = fields.item_buy
|
||||
trade["qty_buy"] = get_field_qty(fields.qty_buy)
|
||||
trade["item_sell"] = fields.item_sell
|
||||
trade["qty_sell"] = get_field_qty(fields.qty_sell)
|
||||
table.insert(self.metadata.trades, trade)
|
||||
self.metadata.inventory[fields.item_buy] = self.metadata.inventory[fields.item_buy] or 0
|
||||
self.metadata.inventory[fields.item_sell] = self.metadata.inventory[fields.item_sell] or 0
|
||||
show_formspec(self, player_name, nil)
|
||||
else
|
||||
minetest.chat_send_player(player_name, "Error: Invalid Item Name!")
|
||||
end
|
||||
elseif fields.inv_select then
|
||||
local id = tonumber(string.match(fields.inv_select, "%d+"))
|
||||
if id then
|
||||
show_formspec(self, player_name, id)
|
||||
end
|
||||
elseif fields.trade_accept then
|
||||
local inv = minetest.get_inventory({type="detached", name="npcf_"..self.npc_name})
|
||||
local input = inv:get_stack("input", 1)
|
||||
local output = inv:get_stack("output", 1)
|
||||
local item_buy = input:get_name()
|
||||
local item_sell = output:get_name()
|
||||
local qty_buy = input:get_count()
|
||||
local qty_sell = output:get_count()
|
||||
local max = output:get_stack_max()
|
||||
if qty_buy > 0 and qty_sell > 0 and max > 0 then
|
||||
self.metadata.inventory[item_buy] = self.metadata.inventory[item_buy] + qty_buy
|
||||
self.metadata.inventory[item_sell] = self.metadata.inventory[item_sell] - qty_sell
|
||||
while qty_sell > max do
|
||||
sender:get_inventory():add_item("main", item_sell.." "..max)
|
||||
qty_sell = qty_sell - max
|
||||
end
|
||||
if qty_sell > 0 then
|
||||
sender:get_inventory():add_item("main", item_sell.." "..qty_sell)
|
||||
end
|
||||
input:clear()
|
||||
inv:set_stack("input", 1, input)
|
||||
end
|
||||
elseif self.owner == player_name then
|
||||
for k,_ in pairs(fields) do
|
||||
selected_id = k:gsub("trade_delete_", "")
|
||||
if selected_id ~= k and self.metadata.trades[tonumber(selected_id)] then
|
||||
local inv = minetest.get_inventory({type="detached", name="npcf_"..self.npc_name})
|
||||
inv:set_stack("input", 1, ItemStack(""))
|
||||
table.remove(self.metadata.trades, selected_id)
|
||||
show_formspec(self, player_name, nil)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
After Width: | Height: | Size: 78 B |
After Width: | Height: | Size: 107 B |
After Width: | Height: | Size: 93 B |
After Width: | Height: | Size: 101 B |
After Width: | Height: | Size: 105 B |
After Width: | Height: | Size: 110 B |
After Width: | Height: | Size: 103 B |
After Width: | Height: | Size: 114 B |
After Width: | Height: | Size: 87 B |
After Width: | Height: | Size: 105 B |
After Width: | Height: | Size: 111 B |
After Width: | Height: | Size: 109 B |
After Width: | Height: | Size: 95 B |
After Width: | Height: | Size: 108 B |
After Width: | Height: | Size: 99 B |
After Width: | Height: | Size: 89 B |
After Width: | Height: | Size: 88 B |
After Width: | Height: | Size: 108 B |
After Width: | Height: | Size: 87 B |
After Width: | Height: | Size: 88 B |
After Width: | Height: | Size: 94 B |
After Width: | Height: | Size: 118 B |
After Width: | Height: | Size: 83 B |
After Width: | Height: | Size: 105 B |
After Width: | Height: | Size: 113 B |
After Width: | Height: | Size: 100 B |
After Width: | Height: | Size: 97 B |
After Width: | Height: | Size: 106 B |
After Width: | Height: | Size: 109 B |
After Width: | Height: | Size: 110 B |
After Width: | Height: | Size: 85 B |
After Width: | Height: | Size: 91 B |
After Width: | Height: | Size: 109 B |
After Width: | Height: | Size: 110 B |
After Width: | Height: | Size: 104 B |
After Width: | Height: | Size: 102 B |
After Width: | Height: | Size: 86 B |
After Width: | Height: | Size: 78 B |
After Width: | Height: | Size: 107 B |
After Width: | Height: | Size: 99 B |
After Width: | Height: | Size: 100 B |
After Width: | Height: | Size: 99 B |
After Width: | Height: | Size: 104 B |
After Width: | Height: | Size: 90 B |
After Width: | Height: | Size: 107 B |
After Width: | Height: | Size: 94 B |
After Width: | Height: | Size: 91 B |
After Width: | Height: | Size: 93 B |
After Width: | Height: | Size: 111 B |
After Width: | Height: | Size: 86 B |
After Width: | Height: | Size: 93 B |
After Width: | Height: | Size: 92 B |
After Width: | Height: | Size: 93 B |
After Width: | Height: | Size: 100 B |
After Width: | Height: | Size: 100 B |
After Width: | Height: | Size: 90 B |
After Width: | Height: | Size: 102 B |
After Width: | Height: | Size: 93 B |
After Width: | Height: | Size: 92 B |
After Width: | Height: | Size: 102 B |
After Width: | Height: | Size: 111 B |
After Width: | Height: | Size: 108 B |
After Width: | Height: | Size: 119 B |
After Width: | Height: | Size: 88 B |
After Width: | Height: | Size: 678 B |
After Width: | Height: | Size: 360 B |
After Width: | Height: | Size: 316 B |
After Width: | Height: | Size: 562 B |
After Width: | Height: | Size: 217 B |
After Width: | Height: | Size: 755 B |
After Width: | Height: | Size: 973 B |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 162 B |
After Width: | Height: | Size: 146 B |
After Width: | Height: | Size: 85 B |
After Width: | Height: | Size: 129 B |
After Width: | Height: | Size: 116 B |
After Width: | Height: | Size: 131 B |
After Width: | Height: | Size: 129 B |
After Width: | Height: | Size: 128 B |