Occupations: Add schedule check function, allow enqueuing of schedule check.
Add simple test farmer occupation (WIP).
This commit is contained in:
commit
0f931d273c
388
occupations/occupations.lua
Normal file
388
occupations/occupations.lua
Normal file
@ -0,0 +1,388 @@
|
||||
-- Occupations/jobs functionality by Zorman2000
|
||||
-----------------------------------------------
|
||||
-- Occupations functionality
|
||||
-- NPCs need an occupation or job in order to simulate being alive.
|
||||
-- This functionality is built on top of the schedules functionality.
|
||||
-- Occupations are essentially specific schedules, that can have slight
|
||||
-- random variations to provide diversity and make specific occupations
|
||||
-- less predictable. Occupations are associated with textures, dialogues,
|
||||
-- specific initial items, type of building (and surroundings) where NPC
|
||||
-- lives, etc.
|
||||
-- Example of an occupation: farmer
|
||||
-- The farmer will have to live in a farm, or just beside a field.
|
||||
-- It will have the following schedule:
|
||||
-- 6AM - get out of bed, walk to home inside, goes to chest, retrieves
|
||||
-- seeds and wander
|
||||
-- 7AM - goes out to the field and randomly start harvesting and planting
|
||||
-- crops that are already fully grown
|
||||
-- 12PM - gets a random but moderate (5-15) amount of seeds and harvested
|
||||
-- - crops. Goes into the house, stores 1/4 of the amount in a chest,
|
||||
-- - gets all currency items it has, and sits into a bench
|
||||
-- 1PM - goes outside the house and becomes trader, sells the remaining
|
||||
-- - seeds and crops
|
||||
-- 6PM - goes inside the house. Stores all currency items it has, all
|
||||
-- - remainin seeds and crops, and sits on a bench
|
||||
-- 8PM - gets out of the bench, wanders inside home
|
||||
-- 10PM - goes to bed
|
||||
|
||||
-- Implementation:
|
||||
-- A function, npc.register_occupation(), will be provided to register an
|
||||
-- occupation that can be used to initialize NPCs. The format is the following:
|
||||
-- {
|
||||
-- dialogues = {
|
||||
-- type = "",
|
||||
-- -- The type can be "given", "mix" or "tags"
|
||||
-- data = {},
|
||||
-- -- Array of dialogue definitions. This will have dialogue
|
||||
-- -- if the type is either "mix" or "given"
|
||||
-- tags = {},
|
||||
-- -- Array of tags to search for. This will have tags
|
||||
-- -- if the type is either "mix" or "tags"
|
||||
--
|
||||
-- },
|
||||
-- textures = {},
|
||||
-- -- Textures are an array of textures, as usually given on
|
||||
-- -- an entity definition. If given, the NPC will be guaranteed
|
||||
-- -- to have one of the given textures. Also, ensure they have sex
|
||||
-- -- as well in the filename so they can be chosen appropriately.
|
||||
-- -- If left empty, it can spawn with any texture.
|
||||
-- building_types = {},
|
||||
-- -- An array of string where each string is the type of building
|
||||
-- -- where the NPC can spawn with this occupation.
|
||||
-- -- Example: building_type = {"farm", "house"}
|
||||
-- -- If left empty or nil, NPC can spawn in any building
|
||||
-- surrounding_building_types = {},
|
||||
-- -- An array of string where each string is the type of building
|
||||
-- -- that is an immediate neighbor of the NPC's home which can also
|
||||
-- -- be suitable for this occupation. Example, if NPC is farmer and
|
||||
-- -- spawns on house, then it has to be because there is a field
|
||||
-- -- nearby. If left empty or nil, surrounding buildings doesn't
|
||||
-- -- matter
|
||||
-- initial_inventory = {},
|
||||
-- -- An array of entries like the following:
|
||||
-- -- {name="", count=1} -- or
|
||||
-- -- {name="", random=true, min=1, max=10}
|
||||
-- -- This will initialize the inventory for the NPC with the given
|
||||
-- -- items and the specified count, or, a count between min and max
|
||||
-- -- when the entry contains random=true
|
||||
-- -- If left empty, it will initialize with random items.
|
||||
-- schedules_entries = {},
|
||||
-- -- This is a table of tables in the following format:
|
||||
-- -- {
|
||||
-- [1] = {[1] = action = npc.action.cmd.freeze, args={freeze=true}},
|
||||
-- [13] = {[1] = action = npc.action.cmd.freeze, args={freeze=false},
|
||||
-- [2] = action = npc.action.cmd.freeze, args={freeze=true}
|
||||
-- },
|
||||
-- [23] = {[1] = action=npc.action.cmd.freeze, args={freeze=false}}
|
||||
-- -- }
|
||||
-- -- The numbers, [1], [13] and [23] are the times when the entries
|
||||
-- -- corresponding to each are supposed to happen. The tables with
|
||||
-- -- [1], [1],[2] and [1] actions respectively are the entries that
|
||||
-- -- will happen at time 1, 13 and 23.
|
||||
-- }
|
||||
|
||||
-- Public API
|
||||
npc.occupations = {}
|
||||
|
||||
-- Private API
|
||||
local occupations = {}
|
||||
|
||||
-- This array contains all the registered occupations.
|
||||
-- The key is the name of the occupation.
|
||||
npc.occupations.registered_occupations = {}
|
||||
|
||||
-- This is the basic occupation definition, this is for all NPCs that
|
||||
-- don't have a specific occupation. It serves as an example.
|
||||
npc.occupations.basic_def = {
|
||||
-- Use random textures
|
||||
textures = {
|
||||
{"npc_male1.png"},
|
||||
{"npc_child_male1.png"}
|
||||
},
|
||||
-- Use random dialogues
|
||||
dialogues = {},
|
||||
-- Initialize inventory with random items
|
||||
initial_inventory = {},
|
||||
-- Initialize schedule
|
||||
schedules_entries = {
|
||||
-- Schedule entry for 7 in the morning
|
||||
[7] = {
|
||||
-- Get out of bed
|
||||
[1] = {task = npc.actions.cmd.USE_BED, args = {
|
||||
pos = npc.places.PLACE_TYPE.BED.PRIMARY,
|
||||
action = npc.actions.const.beds.GET_UP
|
||||
}
|
||||
},
|
||||
-- Walk to home inside
|
||||
[2] = {task = npc.actions.cmd.WALK_TO_POS, args = {
|
||||
end_pos = npc.places.PLACE_TYPE.OTHER.HOME_INSIDE,
|
||||
walkable = {}
|
||||
},
|
||||
chance = 75
|
||||
},
|
||||
-- Allow mobs_redo wandering
|
||||
[3] = {action = npc.actions.cmd.FREEZE, args = {freeze = false}}
|
||||
},
|
||||
-- Schedule entry for 7 in the morning
|
||||
[8] = {
|
||||
-- Walk to outside of home
|
||||
[1] = {task = npc.actions.cmd.WALK_TO_POS, args = {
|
||||
end_pos = npc.places.PLACE_TYPE.OTHER.HOME_OUTSIDE,
|
||||
walkable = {}
|
||||
},
|
||||
chance = 75
|
||||
},
|
||||
-- Allow mobs_redo wandering
|
||||
[2] = {action = npc.actions.cmd.FREEZE, args = {freeze = false}}
|
||||
},
|
||||
-- Schedule entry for 12 midday
|
||||
[12] = {
|
||||
-- Walk to a sittable node
|
||||
[1] = {task = npc.actions.cmd.WALK_TO_POS, args = {
|
||||
end_pos = {place_type=npc.places.PLACE_TYPE.SITTABLE.PRIMARY, use_access_node=true},
|
||||
walkable = {"cottages:bench"}
|
||||
},
|
||||
chance = 75
|
||||
},
|
||||
-- Sit on the node
|
||||
[2] = {task = npc.actions.cmd.USE_SITTABLE, args = {
|
||||
pos = npc.places.PLACE_TYPE.SITTABLE.PRIMARY,
|
||||
action = npc.actions.const.sittable.SIT
|
||||
},
|
||||
depends = {1}
|
||||
},
|
||||
-- Stay put into place
|
||||
[3] = {action = npc.actions.cmd.SET_INTERVAL, args = {
|
||||
freeze = true,
|
||||
interval = 35
|
||||
},
|
||||
depends = {2}
|
||||
},
|
||||
[4] = {action = npc.actions.cmd.SET_INTERVAL, args = {
|
||||
freeze = true,
|
||||
interval = npc.actions.default_interval
|
||||
},
|
||||
depends = {3}
|
||||
},
|
||||
-- Get up from sit
|
||||
[5] = {action = npc.actions.cmd.USE_SITTABLE, args = {
|
||||
pos = npc.places.PLACE_TYPE.SITTABLE.PRIMARY,
|
||||
action = npc.actions.const.sittable.GET_UP
|
||||
},
|
||||
depends = {4}
|
||||
}
|
||||
},
|
||||
-- Schedule entry for 1 in the afternoon
|
||||
[13] = {
|
||||
-- Give NPC money to buy from player
|
||||
[1] = {property = npc.schedule_properties.put_multiple_items, args = {
|
||||
itemlist = {
|
||||
{name="default:iron_lump", random=true, min=2, max=4}
|
||||
}
|
||||
},
|
||||
chance = 75
|
||||
},
|
||||
-- Change trader status to "trader"
|
||||
[2] = {property = npc.schedule_properties.trader_status, args = {
|
||||
status = npc.trade.TRADER
|
||||
},
|
||||
chance = 75
|
||||
},
|
||||
[3] = {property = npc.schedule_properties.can_receive_gifts, args = {
|
||||
can_receive_gifts = false
|
||||
},
|
||||
depends = {1}
|
||||
},
|
||||
-- Allow mobs_redo wandering
|
||||
[4] = {action = npc.actions.cmd.FREEZE, args = {freeze = false}}
|
||||
},
|
||||
-- Schedule entry for 6 in the evening
|
||||
[18] = {
|
||||
-- Change trader status to "none"
|
||||
[1] = {property = npc.schedule_properties.trader_status, args = {
|
||||
status = npc.trade.NONE
|
||||
}
|
||||
},
|
||||
-- Enable gift receiving again
|
||||
[2] = {property = npc.schedule_properties.can_receive_gifts, args = {
|
||||
can_receive_gifts = true
|
||||
}
|
||||
},
|
||||
-- Get inside home
|
||||
[3] = {task = npc.actions.cmd.WALK_TO_POS, args = {
|
||||
end_pos = npc.places.PLACE_TYPE.OTHER.HOME_INSIDE,
|
||||
walkable = {}
|
||||
}
|
||||
},
|
||||
-- Allow mobs_redo wandering
|
||||
[4] = {action = npc.actions.cmd.FREEZE, args = {freeze = false}}
|
||||
},
|
||||
-- Schedule entry for 10 in the evening
|
||||
[22] = {
|
||||
[1] = {task = npc.actions.cmd.WALK_TO_POS, args = {
|
||||
end_pos = {place_type=npc.places.PLACE_TYPE.BED.PRIMARY, use_access_node=true},
|
||||
walkable = {}
|
||||
}
|
||||
},
|
||||
-- Use bed
|
||||
[2] = {task = npc.actions.cmd.USE_BED, args = {
|
||||
pos = npc.places.PLACE_TYPE.BED.PRIMARY,
|
||||
action = npc.actions.const.beds.LAY
|
||||
}
|
||||
},
|
||||
-- Stay put on bed
|
||||
[3] = {action = npc.actions.cmd.FREEZE, args = {freeze = true}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-- This function registers an occupation
|
||||
function npc.occupations.register_occupation(name, def)
|
||||
npc.occupations.registered_occupations[name] = def
|
||||
end
|
||||
|
||||
-- This function scans all registered occupations and filter them by
|
||||
-- building type and surrounding building type, returning an array
|
||||
-- of occupation names (strings)
|
||||
function npc.occupations.get_for_building(building_type, surrounding_building_types)
|
||||
local result = {}
|
||||
for name,def in pairs(npc.registered_occupations) do
|
||||
-- Check for empty or nil building types, in that case, any building
|
||||
if def.building_types == nil or def.building_types == {} then
|
||||
-- Empty building types, add to result
|
||||
table.insert(result, name)
|
||||
else
|
||||
-- Check if building type is contained in the def's building types
|
||||
if npc.utils.array_contains(def.building_types, building_type) then
|
||||
-- Check for empty or nil surrounding building types
|
||||
if def.surrounding_building_types == nil
|
||||
or def.surrounding_building_types == {} then
|
||||
-- Add this occupation
|
||||
table.insert(result, name)
|
||||
else
|
||||
-- Check if surround type is included in the def
|
||||
if npc.utils.array_is_subset_of_array(def.surrounding_building_types,
|
||||
surrounding_building_types) then
|
||||
-- Add this occupation
|
||||
table.insert(result, name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- This function will initialize entities values related to
|
||||
-- the occupation: textures, dialogues, inventory items and
|
||||
-- will set schedules accordingly.
|
||||
function npc.occupations.initialize_occupation_values(self, occupation_name)
|
||||
-- Get occupation definition
|
||||
local def = npc.occupations.registered_occupations[occupation_name]
|
||||
|
||||
if not def then
|
||||
npc.log("WARNING", "No definition found for occupation name: "..dump(occupation_name))
|
||||
return
|
||||
end
|
||||
|
||||
npc.log("INFO", "Overriding NPC values using occupation '"..dump(occupation_name).."' values")
|
||||
|
||||
-- Initialize textures, else it will leave the current textures
|
||||
minetest.log("Texture entries: "..dump(table.getn(def.textures)))
|
||||
if def.textures and table.getn(def.textures) > 0 then
|
||||
self.selected_texture =
|
||||
npc.get_random_texture_from_array(self.sex, self.age, def.textures)
|
||||
-- Set texture if it found for sex and age
|
||||
minetest.log("No textures found for sex "..dump(self.sex).." and age "..dump(self.age))
|
||||
if self.selected_texture then
|
||||
-- Set texture and base texture
|
||||
self.textures = self.selected_texture
|
||||
self.base_texture = self.selected_texture
|
||||
-- Refresh entity
|
||||
self.object:set_properties(self)
|
||||
end
|
||||
end
|
||||
|
||||
-- Initialize inventory
|
||||
if def.initial_inventory and table.getn(def.initial_inventory) > 0 then
|
||||
for i = 1, #def.initial_inventory do
|
||||
local item = def.initial_inventory[i]
|
||||
-- Check if item count is randomized
|
||||
if item.random and item.min and item.max then
|
||||
npc.add_item_to_inventory(self, item.name, math.random(item.min, item.max))
|
||||
else
|
||||
-- Add item with the given count
|
||||
npc.add_item_to_inventory(self, item.name, item.count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Initialize dialogues
|
||||
if def.dialogues and table.getn(def.dialogues) > 0 then
|
||||
local dialogue_keys = {}
|
||||
-- Check which type of dialogues we have
|
||||
if def.dialogues.type == "given" then
|
||||
-- We have been given the dialogues, so def.dialogues.data contains
|
||||
-- an array of dialogues
|
||||
for _, dialogue in def.dialogues.data do
|
||||
-- Add to the dialogue tags the "occupation name"
|
||||
table.insert(dialogue.tags, occupation_name)
|
||||
-- Register dialogue
|
||||
local key = npc.dialogue.register_dialogue(dialogue)
|
||||
-- Add key to set of dialogue keys
|
||||
table.insert(dialogue_keys, key)
|
||||
end
|
||||
elseif def.dialogues.type == "mix" then
|
||||
-- We have been given the dialogues, so def.dialogues.data contains
|
||||
-- an array of dialogues and def.dialogues.tags contains an array of
|
||||
-- tags that we will use to search
|
||||
-- Register dialogues
|
||||
for _, dialogue in def.dialogues.data do
|
||||
-- Add to the dialogue tags the "occupation name"
|
||||
table.insert(dialogue.tags, occupation_name)
|
||||
-- Register dialogue
|
||||
local key = npc.dialogue.register_dialogue(dialogue)
|
||||
-- Add key to set of dialogue keys
|
||||
table.insert(dialogue_keys, key)
|
||||
end
|
||||
-- Find dialogues using tags
|
||||
local dialogues = npc.search_dialogue_by_tags(def.dialogues.tags, true)
|
||||
-- Add keys to set of dialogue keys
|
||||
for _, key in npc.utils.get_map_keys(dialogues) do
|
||||
table.insert(dialogue_keys, key)
|
||||
end
|
||||
elseif def.dialogues.type == "tags" then
|
||||
-- We need to find the dialogues from tags. def.dialogues.tags contains
|
||||
-- an array of tags that we will use to search.
|
||||
local dialogues = npc.search_dialogue_by_tags(def.dialogues.tags, true)
|
||||
-- Add keys to set of dialogue keys
|
||||
dialogue_keys = npc.utils.get_map_keys(dialogues)
|
||||
end
|
||||
-- Add dialogues to NPC
|
||||
-- Check if there is a max of dialogues to be added
|
||||
local max_dialogue_count = npc.dialogue.MAX_DIALOGUES
|
||||
if def.dialogues.max_count and def.max_dialogue_count > 0 then
|
||||
max_dialogue_count = def.dialogues.max_count
|
||||
end
|
||||
-- Add dialogues to the normal dialogues for NPC
|
||||
self.dialogues.normal = {}
|
||||
for i = 1, max_dialogue_count do
|
||||
self.dialogues.normal[i] = dialogue_keys[i]
|
||||
end
|
||||
end
|
||||
|
||||
-- Initialize schedule entries
|
||||
if def.schedules_entries and table.getn(npc.utils.get_map_keys(def.schedules_entries)) > 0 then
|
||||
-- Create schedule in NPC
|
||||
npc.create_schedule(self, npc.schedule_types.generic, 0)
|
||||
-- Traverse schedules
|
||||
for time, entries in pairs(def.schedules_entries) do
|
||||
-- Add schedule entry for each time
|
||||
npc.add_schedule_entry(self, npc.schedule_types.generic, 0, time, nil, entries)
|
||||
end
|
||||
end
|
||||
|
||||
npc.log("INFO", "Successfully initialized NPC with occupation values")
|
||||
|
||||
end
|
82
random_data/occupations_data.lua
Normal file
82
random_data/occupations_data.lua
Normal file
@ -0,0 +1,82 @@
|
||||
-- Occupations definitions
|
||||
|
||||
-- Register default occupation
|
||||
npc.occupations.register_occupation("default_basic", npc.occupations.basic_def)
|
||||
|
||||
-- Test farmer
|
||||
npc.occupations.register_occupation("test_farmer", {
|
||||
dialogues = {},
|
||||
textures = {},
|
||||
initial_inventory = {},
|
||||
workplace = {
|
||||
search_nodes = {"cottages:gate_open","cottages:gate_closed"},
|
||||
search_type = "orthogonal",
|
||||
surrounding_nodes =
|
||||
{
|
||||
bottom = {
|
||||
nodes = {"farming:soil", "farming:wet_soil", "default:dirt", "default:dirt_with_grass"},
|
||||
criteria = "any"
|
||||
}
|
||||
sides = {
|
||||
nodes = {"doors:wooden_fence"}
|
||||
criteria = "exact"
|
||||
}
|
||||
}
|
||||
}
|
||||
schedule_entries = {
|
||||
[7] = {
|
||||
[1] =
|
||||
{
|
||||
task = npc.actions.cmd.WALK_TO_POS,
|
||||
args = {
|
||||
end_pos = npc.places.PLACE_TYPE.OTHER.HOME_OUTSIDE,
|
||||
walkable = {}
|
||||
}
|
||||
},
|
||||
[2] =
|
||||
{
|
||||
check = true,
|
||||
range = 2,
|
||||
random_execution_times = true
|
||||
min_count = 10,
|
||||
max_count = 12,
|
||||
nodes = {"farming:cotton_3"},
|
||||
actions =
|
||||
{
|
||||
-- Actions for grown cotton - harvest and replant
|
||||
["farming:cotton_3"] =
|
||||
{
|
||||
[1] =
|
||||
{
|
||||
action = npc.actions.cmd.WALK_STEP,
|
||||
},
|
||||
[2] =
|
||||
{
|
||||
action = npc.actions.cmd.DIG,
|
||||
},
|
||||
[3] =
|
||||
{
|
||||
action = npc.actions.cmd.PLACE,
|
||||
args =
|
||||
{
|
||||
node = "farming:cotton_1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
none_actions =
|
||||
{
|
||||
-- Walk a single step in a random direction
|
||||
[1] = {
|
||||
action = npc.actions.cmd.WALK_STEP,
|
||||
args =
|
||||
{
|
||||
dir = "random"
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
Loading…
Reference in New Issue
Block a user