Add NPC framework

master
rubenwardy 2016-08-24 14:21:08 +01:00
parent 3034fc0876
commit 00220a6bc8
478 changed files with 22486 additions and 0 deletions

1
mods/npcf/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
npcf.conf

286
mods/npcf/README.md Normal file
View File

@ -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.

192
mods/npcf/chatcommands.lua Normal file
View File

@ -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,
})

3
mods/npcf/depends.txt Normal file
View File

@ -0,0 +1,3 @@
default
instabuild?
skins?

10
mods/npcf/init.lua Normal file
View File

@ -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")

11262
mods/npcf/models/npcf_deco.x Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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

569
mods/npcf/npcf.lua Normal file
View File

@ -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)

View File

@ -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,
})

224
mods/npcf/npcs/deco_npc.lua Normal file
View File

@ -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,
})

View File

@ -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,
})

View File

@ -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,
})

View File

@ -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,
})

File diff suppressed because one or more lines are too long

BIN
mods/npcf/textures/W_-.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 B

BIN
mods/npcf/textures/W_0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 B

BIN
mods/npcf/textures/W_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 B

BIN
mods/npcf/textures/W_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 B

BIN
mods/npcf/textures/W_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

BIN
mods/npcf/textures/W_4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 B

BIN
mods/npcf/textures/W_5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

BIN
mods/npcf/textures/W_6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 B

BIN
mods/npcf/textures/W_7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 B

BIN
mods/npcf/textures/W_8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

BIN
mods/npcf/textures/W_9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 B

BIN
mods/npcf/textures/W_UA.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 B

BIN
mods/npcf/textures/W_UB.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

BIN
mods/npcf/textures/W_UC.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

BIN
mods/npcf/textures/W_UD.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 B

BIN
mods/npcf/textures/W_UE.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 B

BIN
mods/npcf/textures/W_UF.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 B

BIN
mods/npcf/textures/W_UG.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

BIN
mods/npcf/textures/W_UH.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 B

BIN
mods/npcf/textures/W_UI.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 B

BIN
mods/npcf/textures/W_UJ.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 B

BIN
mods/npcf/textures/W_UK.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 B

BIN
mods/npcf/textures/W_UL.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 B

BIN
mods/npcf/textures/W_UM.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

BIN
mods/npcf/textures/W_UN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

BIN
mods/npcf/textures/W_UO.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

BIN
mods/npcf/textures/W_UP.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 B

BIN
mods/npcf/textures/W_UQ.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 B

BIN
mods/npcf/textures/W_UR.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 B

BIN
mods/npcf/textures/W_US.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 B

BIN
mods/npcf/textures/W_UT.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 B

BIN
mods/npcf/textures/W_UU.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 B

BIN
mods/npcf/textures/W_UV.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 B

BIN
mods/npcf/textures/W_UW.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 B

BIN
mods/npcf/textures/W_UX.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 B

BIN
mods/npcf/textures/W_UY.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 B

BIN
mods/npcf/textures/W_UZ.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 B

BIN
mods/npcf/textures/W__.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 B

BIN
mods/npcf/textures/W_a.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 B

BIN
mods/npcf/textures/W_b.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 B

BIN
mods/npcf/textures/W_c.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

BIN
mods/npcf/textures/W_d.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 B

BIN
mods/npcf/textures/W_e.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 B

BIN
mods/npcf/textures/W_f.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

BIN
mods/npcf/textures/W_g.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 B

BIN
mods/npcf/textures/W_h.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 B

BIN
mods/npcf/textures/W_i.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 B

BIN
mods/npcf/textures/W_j.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 B

BIN
mods/npcf/textures/W_k.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 B

BIN
mods/npcf/textures/W_l.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 B

BIN
mods/npcf/textures/W_m.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 B

BIN
mods/npcf/textures/W_n.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 B

BIN
mods/npcf/textures/W_o.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 B

BIN
mods/npcf/textures/W_p.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

BIN
mods/npcf/textures/W_q.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

BIN
mods/npcf/textures/W_r.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

BIN
mods/npcf/textures/W_s.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 B

BIN
mods/npcf/textures/W_t.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 B

BIN
mods/npcf/textures/W_u.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 B

BIN
mods/npcf/textures/W_v.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 B

BIN
mods/npcf/textures/W_w.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 B

BIN
mods/npcf/textures/W_x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

BIN
mods/npcf/textures/W_y.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 B

BIN
mods/npcf/textures/W_z.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

View File

0
mods/textcolors/init.lua Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 B

Some files were not shown because too many files have changed in this diff Show More