Compare commits

...

88 Commits

Author SHA1 Message Date
sys4-fr 97da2dc191 Fix crash when npc close a door
Comment debug messages
2018-03-27 23:29:04 +02:00
sys4-fr dfaa904ba8 Avoid crash when npc don't find alternative place to go 2018-03-25 21:22:19 +02:00
sys4-fr 166db4b5e5 Fix crash 2018-03-25 15:46:58 +02:00
BrunoMine 2dc2400b38 Api doc update (#31)
* Update api.md

* Add schedules
* Add occupation
* Update places map
* Fix `step` and `initialize`

* Add schedule commands

* Add WALK_TO_POS task

* orthographic correction
2017-12-20 14:40:36 -05:00
BrunoMine c7fad7d6c6 Initiating documentation (#25)
* Add API

* Add Actions and Tasks documentation

* Convert to .MD docs

* Add links

* Fix links

* Remove some terms
2017-11-06 08:57:59 -05:00
Hector Franqui 5f9afae5d9 NPC: Support backward compatibility. 2017-10-19 21:51:16 -04:00
Hector Franqui ec0392096f NPC: After interacting with NPC, original yaw is restored. 2017-10-19 21:47:35 -04:00
Hector Franqui 205bdf6eb1 NPC: Use `after_activate()` callback to fix animation issue. 2017-10-18 15:09:00 -04:00
Hector Franqui 5bdc3c3c29 NPC: Fix animation state not persistent on interaction/game restart.
Action lock/unlock doesn't perform lock if movement state is sitting or laying.
Right-click interaction can now be disabled in freeze action command.
Upon NPC activation, animation is restored.
2017-10-18 09:05:08 -04:00
Hector Franqui 4a4dc724cd NPC: Fix issues with female and male skins.
Fix naming issues with male skins.
Improve coloring on male skins.
2017-10-16 18:48:02 -04:00
Hector Franqui d95c8504ec Places: Add truly owned nodes. NPCs shouldn't override ownership
anymore.
Spawner: Add command "/restore_area" to clear up node ownership & usage in manually spawned places.
"/restore_plotmarkers" now clean up node ownership & usage in mg_villages.
2017-10-07 11:28:38 -04:00
Hector Franqui 8a5e80e2cb Places: Allow finding alternative nodes when nodes are being used.
Add owned and used properties to each owned/shared node.
Actions: Allow to find alternative sittable/bed/furnace node if currently
being used.
Default occupation: Add alternative sitting finding.
NPC: Log cleanup.
2017-10-05 18:53:46 -04:00
Hector Franqui d92b729e19 Fixed bugs related to door positioning, opening/closing, and finding
positions in general.
2017-09-23 10:43:51 -04:00
Hector Franqui 43c69ffee4 Spawner: Add search radius and height to manual spawn egg. 2017-09-21 20:03:01 -04:00
Hector Franqui 164e09bed5 Farmer: Add trading list.
Add 6 cotton seeds as initial inventory.
2017-09-21 18:55:29 -04:00
Hector Franqui 5a79b9a119 Trade: Fix name for stone hoe. 2017-09-21 18:55:02 -04:00
Hector Franqui d55ffd06fc Schedules: Allow to set trade list as a schedule property.
Trade: Trading list now supports setting maximum sell count, buy count and how much to keep (in case of sell).
Add price for empty bucket.
2017-09-21 18:53:43 -04:00
Hector Franqui c94edbc649 Log cleanup. 2017-09-21 13:45:34 -04:00
Hector Franqui cb218fc9f9 Actions: Fix bug with not finding walkable positions around bed. 2017-09-21 13:45:09 -04:00
Hector Franqui c37c4dd868 Default occupation: Perform casual trading during afternoon, none during
the rest of the day.
2017-09-21 09:02:41 -04:00
Hector Franqui cf77ab5299 Farmer: Add other schedule entries for commonplace tasks.
Priest: Fixed missing dialogue issue.
Miner: Fixed indentation.
2017-09-18 01:13:20 -04:00
Hector Franqui 175d07476d Remove workspace.xml 2017-09-18 00:33:24 -04:00
NewbProgrammer101 ee38dfe46b Update init.lua 2017-09-17 12:42:55 -04:00
NewbProgrammer101 7f9fedba9d Update default_miner.lua 2017-09-17 12:42:41 -04:00
NewbProgrammer101 5fed6e3a90 Update default_miner.lua 2017-09-17 12:42:24 -04:00
NewbProgrammer101 c3d5a995a6 Create default_miner.lua 2017-09-17 12:41:30 -04:00
NewbProgrammer101 a527bcd746 Update default_farmer.lua 2017-09-17 12:40:40 -04:00
NewbProgrammer101 e1a7efe9ab Update default_priest.lua 2017-09-17 12:31:31 -04:00
Hector Franqui 2a979ef1fe Actions: Add sounds to dig/place actions. 2017-09-17 12:11:31 -04:00
Hector Franqui 580792284d Spawner: Fix undefined node 2017-09-15 18:29:38 -04:00
Hector Franqui 561b11f8fe Spawner: Fix typo 2017-09-15 18:17:19 -04:00
Hector Franqui db415dee97 Occupations: Change building type format to be more flexible.
Spawner: Huge rework on logic to determine occupations. Warning! Completely incomprehensible code.
Schedule: Improve schedule check enqueue and execution.
Optimize walk_to_pos if used on distances < 3
Huge amounts of log cleanup.
Places: Improve adding shared places.
Improve orthogonal node scanning by using walkables and not one single node.
2017-09-15 14:51:37 -04:00
Hector Franqui b02a78de6d Default Farmer (WIP):
Improve schedule entries, schedule check.
Change to new building type format.
2017-09-15 14:47:04 -04:00
Hector Franqui 8cf1e932bc Default Priest:
Change building type definitions to new format.
2017-09-15 14:46:08 -04:00
Hector Franqui bcd6327a13 Merge branch 'master' of https://github.com/hkzorman/advanced_npc 2017-09-15 14:44:45 -04:00
Hector Franqui 866b25c63c Pathfinder: Fix indentation.
Improve path decorator to avoid jumping NPCs (in Minetest 0.5-dev)
2017-09-13 09:09:08 -04:00
Hector Franqui 70fd62825c Actions: Fix bugs in dig and place actions.
Fix bug in get_pos_argument() function.
Optimize walk_to_pos task and improve usage of access node.
Improve get_direction() by using vector.direction() and vector.round()
2017-09-13 09:06:58 -04:00
Hector Franqui 770125fae9 Log cleanup. 2017-09-12 09:09:15 -04:00
Hector Franqui 70c7a3c96f Actions: Add mine animation to dig/place actions.
Fix bug enabling wandering after dig action.
Improved "random" dir functionality for walk_step to avoid getting NPC stuck.
General formatting fixes.
2017-09-12 09:08:44 -04:00
NewbProgrammer101 245c2c32c2 My first pull request. (#15)
* Delete occupations_data.lua
* Update default_priest.lua
* Update init.lua
2017-09-08 08:31:19 -04:00
Hector Franqui ce47958144 Actions: Fix bugs and improve dig and place action commands. 2017-09-07 08:51:50 -04:00
Hector Franqui 2a0b0aa538 Occupations: Refactor all occupation defs into separate Lua files.
Add WIP priest occupation.
Add WIP (very WIP) farmer occupation.
2017-09-05 22:47:03 -04:00
Hector Franqui 6c3988a731 Occupation: Add ability to set some of the NPCs properties.
Currently, you can set:
 - Trader status
 - Whether to show or hide gift items hints
NPC: Add enable/disable gift item hints flag
Schedules: Allow to set the enable/disable gift item hints flag
Dialogues: Add ability to choose from normal dialogues if hint dialogues are disabled.
2017-09-05 19:02:50 -04:00
Hector Franqui 8e5d6d03f4 Spawner: Fix issue when "npcs" table is not present in restore_plotmarkers. 2017-09-01 18:27:17 -04:00
Hector Franqui fe5a155177 Fix bug with dialogues being cleared for NPCs on occupation init. 2017-09-01 18:15:53 -04:00
Hector Franqui cd8e3c09cb Priest occupation (WIP) 2017-09-01 14:15:23 -04:00
Hector Franqui 48eb8078b3 Schedules: Add support to change flags in schedules. 2017-09-01 14:15:01 -04:00
Hector Franqui 939181284b Spawner: Add workplaces properly to the usable nodes. 2017-09-01 14:14:35 -04:00
Hector Franqui 40ac2a55a1 Actions: Fix bug and add support for using access node. 2017-09-01 14:13:13 -04:00
Hector Franqui ba6348663b Dialogues: Fix bug when not having custom trade offers. 2017-08-31 17:59:46 -05:00
Hector Franqui c9eb3b0ef4 Spawner: Workplaces are properly assigned to NPC.
If a workplace is assigned to a NPC, it is not used by any other NPC.
"/restore_plotmarkers" command also restores the workplace so it can
be used by other NPCs.
Occupations: Dialogues are registered on occupation registration.
2017-08-31 09:11:43 -04:00
Hector Franqui 706a5cf188 Add occupation name to entity properties. 2017-08-29 18:16:48 -05:00
Hector Franqui e85a8161c3 Change names to match convention. 2017-08-29 18:12:21 -05:00
Hector Franqui 337f8c46b7 Small bug fixes and log cleanups. 2017-08-29 14:55:38 -04:00
Hector Franqui 4ec8ad7f7f Add priest texture (WIP) 2017-08-29 14:55:21 -04:00
Hector Franqui df56e44bbd Add workplaces support.
Places:
Workplaces are now usable nodes.
Plotmarkers around a building can be scanned and their information stored into current plotmarker.
Slightly optimize plotmarker search.
Occupations:
Add test "priest" occupation definition.
Fix lots of bugs in initialization code.
Spawner:
Add ability to determine occupation name out of surrounding workplaces.
Assign occupation to spawned NPC using simple algorithm.
Others:
Reduce log noise a bit.
Fix some warnings.
2017-08-29 14:54:57 -04:00
Hector Franqui 59bb430e62 Spawner: Nearby plotmarkers are now scanned for all mg_villages:plotmarkers.
Places: Usable nodes now include workplaces
Small bug fixes.
Relationships: Small bugfix with gift item response.
Occupations: Add work node descriptions.
Experimental "priest" occupation.
2017-08-28 15:43:57 -04:00
Hector Franqui 0ddb30c0f9 Finish implementation to find nearby plotmarkers. 2017-08-27 14:46:48 -04:00
Hector Franqui 19c5ca1d0c Add plotmarker search to adapt_mg_villages_plotmarker (WIP)
Small code cleanup.
2017-08-27 12:50:53 -04:00
Hector Franqui d347b6fad5 Finish implementation of npc.places.find_plotmarkers.
Small code refactor.
2017-08-27 12:32:04 -04:00
Hector Franqui ceae61f553 Add gitignore to IntelliJ folder 2017-08-27 11:41:35 -04:00
Hector Franqui a4fd06d1c6 Add nearby plotmarkers search 2017-08-27 11:28:40 -04:00
Hector Franqui ad9032ec5b Add scan functions and node definitions for workplaces. (WIP) 2017-08-25 19:48:34 -04:00
Hector Franqui 11e871a932 Log cleanups. 2017-08-25 09:33:25 -04:00
Hector Franqui 4c102a70a4 Spawner: Large code refactor to remove dependency on plotmarkers.
Most spawner functions can now be called without giving a plotmarker.
Move scanning functions to places.lua.
Places: Cleanup and add more area-scanning functions.
Schedules: Bugfix where schedules weren't being executed due to wrong
"end" order in the do_custom() function.
Data: Moved random data to "data" folder.
Textures: Add 14 male textures and 10 female textures.
Occupations: Small tweaks to "default_basic" occupation.
2017-08-25 09:31:45 -04:00
Hector Franqui 698d247aba Fix misc bugs 2017-08-11 15:41:50 -04:00
Hector Franqui 5a93800e77 Occupations: Add schedule check function, allow enqueuing of schedule check 2017-08-11 12:03:45 -04:00
Hector Franqui 0f931d273c Occupations: Add schedule check function, allow enqueuing of schedule check.
Add simple test farmer occupation (WIP).
2017-08-11 12:01:44 -04:00
Hector Kio 4c0e2b574a Temporary fix for male textures issue. 2017-08-11 10:44:57 -04:00
Hector Franqui 3edc959d3a Dialogues: Finish implementation of registered dialogues.
Ported all trade dialogues to use new dialogue system.
2017-07-19 14:00:29 -04:00
Hector Franqui b5dc9926cd Add 5 additional male textures.
Rename child textures to be more representative.
2017-07-13 19:05:39 -04:00
Hector Franqui bff013bc44 Utilities: Add simple utility code to work with arrays and Lua tables 2017-07-13 19:04:00 -04:00
Hector Franqui 5eceb09cdb Random data: Separate random data in different files for better classification. Add registrations as well. 2017-07-13 19:02:40 -04:00
Hector Franqui e79fb91ff3 Dialogues: Re-write dialogues to allow Minetest's like registration system instead of the actual table lookup system. In general, dialogues now are registered in similar fashion as nodes, entities and others are registered in Minetest.
Relationships: Re-write to the way favorite/disliked items are defined, now allow Minetest's like registration as well.
Random data: Random data now includes registrations for gift items and dialogues.
2017-07-13 19:01:28 -04:00
Hector Franqui bf935fd091 Dialogues: Fix bug with NPCs being frozen if 'Esc' button was pressed to exit dialogue 2017-06-29 19:01:03 -04:00
Hector Franqui 6141af11aa Relationships: Increase gift timer and relationship decrease timer to more usual values.
Fixed small bug on giving unliked items.
2017-06-27 18:39:13 -04:00
Hector Franqui e70888c3e5 Fix bug with 'ignore' nodes when some map areas are not fully loaded but NPC try to perform actions on them. 2017-06-27 18:38:28 -04:00
Hector Franqui cc56446206 NPC: (WIP) Schedule properties support adding one or multiple items to NPC inventory, and support taking one item from NPC inventory.
Attempt to fix child textures issue (2).
2017-06-21 21:07:36 -04:00
Hector Franqui 7110c49b42 Spawner: Avoid multiple spawning by not replacing the mg_villages:plotmarker, but just override its definition.
NPC: (WIP) Support change properties on schedules.
2017-06-21 07:16:11 -04:00
Hector Franqui fd4cec0d63 Spawner: Assign sits, furnaces and storage nodes to spawned NPCs.
Small basic schedule change: NPCs now sit from 12-1 on whatever sit they 'own'.
Fixed /restore_plotmarkers not clearing all metadata.
NPC: Attempted to add a fix for the children growing on their own (due to mobs_redo).
2017-06-19 21:03:24 -04:00
Hector Franqui 3df43ab580 Spawner: Assign sits, furnaces and storage nodes to spawned NPCs.
Small basic schedule change: NPCs now sit from 12-1 on whatever sit they 'own'.
Fixed /restore_plotmarkers not clearing all metadata.
NPC: Attempted to add a fix for the children growing on their own (due to mobs_redo).
2017-06-19 20:54:26 -04:00
Hector Franqui c19ea70242 Spawner: Add clearing of 'replaced' string to /restore_plotmarkers 2017-06-18 11:19:33 -04:00
Hector Franqui a3b428fe14 Fix README.txt and restore correct spawning frequency. 2017-06-17 13:21:24 -04:00
Hector Franqui 4814c16ba0 Remove debug.txt 2017-06-17 13:00:54 -04:00
Hector Franqui 2530918fe9 Fix various bugs 2017-06-17 12:55:50 -04:00
Hector Franqui fb549e7f93 Pathfinder: Drop jumper.lua's pathfinder in favor of MarkBu's pathfinder with slight modifications, as the later one supports 3D paths.
Clean up old pathfinder code.
Add path decoration to MarkBu pathfinder's generated paths.
Change actions.lua and places.lua to use the new pathfinder.
Properly modify README.md.
2017-06-17 11:47:51 -04:00
Hector Franqui e1bf931064 Relationships: change texture of smoke to use default's mod texture 2017-06-17 11:45:57 -04:00
Hector Franqui 3e006ac828 Add proper description.txt, depends.txt 2017-06-17 11:15:40 -04:00
66 changed files with 8052 additions and 415806 deletions

0
.idea/.gitignore vendored Normal file
View File

9
.idea/advanced_npc.iml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/advanced_npc.iml" filepath="$PROJECT_DIR$/.idea/advanced_npc.iml" />
</modules>
</component>
</project>

6
.idea/preferred-vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PreferredVcsStorage">
<preferredVcsName>ApexVCS</preferredVcsName>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -4,25 +4,40 @@ advanced_npc
Introduction Introduction
------------ ------------
Advanced NPC is a mod for Minetest, based on mobs_redo. Advanced NPC is a mod for Minetest using mobs_redo API.
The goal of this mod is to be able to have live villages in Minetest. These NPCs are highly inspired by the typical NPCs of Harvest Moon games. The general idea is that on almost all buildings of a village there are NPCs that are kind of intelligent: they have daily tasks they perform, can speak to players, can trade with the player, can use their own items (chests and furnaces for example), know where to go around their house and village, can be lumbers, miners or any other Minetest-suitable profession and can ultimately engage into relationships with the player. And while basically only players are mentioned here, the ultimate goal is that they can do all of this also among themselves, so that villages are alive and evolving by themselves, without player intervention. The goal of this mod is to be able to have live villages in Minetest. These NPCs are highly inspired by the typical NPCs of _Harvest Moon_ games. The general idea is that on almost all buildings of a village there are NPCs that are kind of intelligent: they have daily tasks they perform, can speak to players, can trade with the player, can use their own items (chests and furnaces for example), know where to go around their house and village, can be lumbers, miners or any other Minetest-suitable profession and can ultimately engage into relationships with the player. And while basically only players are mentioned here, the ultimate goal is that they can do all of this also among themselves, so that villages are alive and evolving by themselves, without player intervention.
Installation Installation
------------ ------------
__NOTE__: Advanced NPC is still under development. While the mod is largely stable, it lacks one of the most important pieces: spawning. Currently, NPCs will spawn on stone (default:stone) and the mg_villages' plotmarkers (mg_villages:plotmarker). The spawning is not controlled, so you will have several of them walking around. This is not how it is planned and is just for testing purposes. In the future, only a handful of NPCs should spawn at village house's plotmarker and they will know their way around the house and have specific jobs. __NOTE__: Advanced NPC is still under development. While the mod is largely stable, it lacks one of the most important pieces: spawning. Currently, NPCs can be spawned using eggs (found in creative inventory as 'NPC') and by themselves on villages of the [mg_villages mod](https://forum.minetest.net/viewtopic.php?t=13589). NPCs will spawn automatically on mg_villages villages and over time will populate the entire village. If something goes wrong, you can reset the village by:
- Clearing all objects (in chat, type /clearobjects quick)
- Restore original plotmarkers (in chat, type /restore_plotmarkers radius)
- The radius can be any number, but it is recommended you use a not so large number. 200 is suitable. So stand in the middle of the village and then run that command.
This will actually restore the village and will slowly make NPCs spawn again. Currently there's no way to disable NPCs spawning on village, except by going to `spawner.lua` and commenting out all of `minetest.register_abm()` code.
Download the mod [here](https://github.com/hkzorman/advanced_npc/archive/master.zip) (link always pointing to latest version) __Download__ the mod [here](https://github.com/hkzorman/advanced_npc/archive/master.zip) (link always pointing to latest version)
For this mod to work correctly, you also need to install the [mobs_redo](https://github.com/tenplus1/mobs_redo) mod. After installation, make sure you enable it in your world. For this mod to work correctly, you also need to install the [mobs_redo](https://github.com/tenplus1/mobs_redo) mod. After installation, make sure you enable it in your world.
License License
------- -------
__advanced_npc__ is Copyright (C) 2016-2017 Hector Franqui (zorman2000), licensed under the GPLv3 license. See `license.txt` for details. __advanced_npc__ is Copyright (C) 2016-2017 Hector Franqui (zorman2000), licensed under the GPLv3 license. See `license.txt` for details.
The `jumper.lua` file contains code based on the [Jumper library](https://github.com/Yonaba/Jumper), which is Copyright (c) 2012-2013 Roland Yonaba, licensed under MIT license. See `actions/jumper.lua` for details. The `pathfinder.lua` file contains code slighlty modified from the [pathfinder mod](https://github.com/MarkuBu/pathfinder) by MarkBu, which is licensed as WTFPL. See `actions/pathfinder.lua` for details.
Current NPC textures are from mobs_redo mod.
The following textures are by Zorman2000:
- marriage_ring.png - CC BY-SA
Documentation and API
---------------------
This mod requires a good user manual, and also is planned to have an extensive API, properly documented. Unfortunately, these still aren't ready. A very very very WIP manual can be found in the [wiki](https://github.com/hkzorman/advanced_npc/wiki/Concept%3A-Dialogues)
Roadmap Roadmap

View File

@ -13,6 +13,51 @@
npc.actions = {} npc.actions = {}
npc.actions.default_interval = 1
npc.actions.dir_data = {
-- North
[0] = {
yaw = 0,
vel = {x=0, y=0, z=1}
},
-- East
[1] = {
yaw = (3 * math.pi) / 2,
vel = {x=1, y=0, z=0}
},
-- South
[2] = {
yaw = math.pi,
vel = {x=0, y=0, z=-1}
},
-- West
[3] = {
yaw = math.pi / 2,
vel = {x=-1, y=0, z=0}
},
-- North east
[4] = {
yaw = (7 * math.pi) / 4,
vel = {x=1, y=0, z=1}
},
-- North west
[5] = {
yaw = math.pi / 4,
vel = {x=-1, y=0, z=1}
},
-- South east
[6] = {
yaw = (5 * math.pi) / 4,
vel = {x=1, y=0, z=-1}
},
-- South west
[7] = {
yaw = (3 * math.pi) / 4,
vel = {x=-1, y=0, z=-1}
}
}
-- Describes actions with doors or openable nodes -- Describes actions with doors or openable nodes
npc.actions.const = { npc.actions.const = {
doors = { doors = {
@ -50,7 +95,9 @@ npc.actions.cmd = {
USE_FURNACE = 11, USE_FURNACE = 11,
USE_BED = 12, USE_BED = 12,
USE_SITTABLE = 13, USE_SITTABLE = 13,
WALK_TO_POS = 14 WALK_TO_POS = 14,
DIG = 15,
PLACE = 16
} }
--npc.actions.one_nps_speed = 0.98 --npc.actions.one_nps_speed = 0.98
@ -60,6 +107,11 @@ npc.actions.one_nps_speed = 1
npc.actions.one_half_nps_speed = 1.5 npc.actions.one_half_nps_speed = 1.5
npc.actions.two_nps_speed = 2 npc.actions.two_nps_speed = 2
npc.actions.take_from_inventory = "take_from_inventory"
npc.actions.take_from_inventory_forced = "take_from_inventory_forced"
npc.actions.force_place = "force_place"
--------------
-- Executor -- -- Executor --
-------------- --------------
-- Function references aren't reliable in Minetest entities. Objects get serialized -- Function references aren't reliable in Minetest entities. Objects get serialized
@ -115,6 +167,12 @@ function npc.actions.execute(self, command, args)
-- Call walk to position task -- Call walk to position task
--minetest.log("Self: "..dump(self)..", Command: "..dump(command)..", args: "..dump(args)) --minetest.log("Self: "..dump(self)..", Command: "..dump(command)..", args: "..dump(args))
return npc.actions.walk_to_pos(self, args) return npc.actions.walk_to_pos(self, args)
elseif command == npc.actions.cmd.DIG then
-- Call dig node action
return npc.actions.dig(self, args)
elseif command == npc.actions.cmd.PLACE then
-- Call place node action
return npc.actions.place(self, args)
end end
end end
@ -141,16 +199,188 @@ end
-- and the NPC is allowed to roam freely. -- and the NPC is allowed to roam freely.
function npc.actions.freeze(self, args) function npc.actions.freeze(self, args)
local freeze_mobs_api = args.freeze local freeze_mobs_api = args.freeze
--minetest.log("Received: "..dump(freeze_mobs_api)) local disable_rightclick = args.disable_rightclick
--minetest.log("Returning: "..dump(not(freeze_mobs_api))) if disable_rightclick ~= nil then
--npc.log("INFO", "Enabling interactions for NPC "..self.npc_name..": "..dump(not(disable_rightclick)))
self.enable_rightclick_interaction = not(disable_rightclick)
end
-- minetest.log("Received: "..dump(freeze_mobs_api))
-- minetest.log("Returning: "..dump(not(freeze_mobs_api)))
return not(freeze_mobs_api) return not(freeze_mobs_api)
end end
-- This action digs the node at the given position
-- If 'add_to_inventory' is true, it will put the digged node in the NPC
-- inventory.
-- Returns true if dig is successful, otherwise false
function npc.actions.dig(self, args)
local pos = args.pos
local add_to_inventory = args.add_to_inventory
local bypass_protection = args.bypass_protection
local play_sound = args.play_sound or true
local node = minetest.get_node_or_nil(pos)
if node then
-- Set mine animation
self.object:set_animation({
x = npc.ANIMATION_MINE_START,
y = npc.ANIMATION_MINE_END},
self.animation.speed_normal, 0)
-- Play dig sound
if play_sound == true then
minetest.sound_play(
minetest.registered_nodes[node.name].sounds.dug,
{
max_hear_distance = 10,
object = self.object
}
)
end
-- Check if protection not enforced
if not bypass_protection then
-- Try to dig node
if minetest.dig_node(pos) then
-- Add to inventory the node drops
if add_to_inventory then
-- Get node drop
local drop = minetest.registered_nodes[node.name].drop
local drop_itemname = node.name
if drop and drop.items then
local random_item = drop.items[math.random(1, #drop.items)]
if random_item then
drop_itemname = random_item.items[1]
end
end
-- Add to NPC inventory
npc.add_item_to_inventory(self, drop_itemname, 1)
end
--return true
return
end
else
-- Add to inventory
if add_to_inventory then
-- Get node drop
local drop = minetest.registered_nodes[node.name].drop
local drop_itemname = node.name
if drop and drop.items then
local random_item = drop.items[math.random(1, #drop.items)]
if random_item then
drop_itemname = random_item.items[1]
end
end
-- Add to NPC inventory
npc.add_item_to_inventory(self, drop_itemname, 1)
end
-- Dig node
minetest.set_node(pos, {name="air"})
end
end
--return false
end
-- This action places a given node at the given position
-- There are three ways to source the node:
-- 1. take_from_inventory: takes node from inventory. If not in inventory,
-- node isn't placed.
-- 2. take_from_inventory_forced: takes node from inventory. If not in
-- inventory, node will be placed anyways.
-- 3. force_place: places node regardless of inventory - will not touch
-- the NPCs inventory
function npc.actions.place(self, args)
local pos = args.pos
local node = args.node
local source = args.source
local bypass_protection = args.bypass_protection
local play_sound = args.play_sound or true
local node_at_pos = minetest.get_node_or_nil(pos)
-- Check if position is empty or has a node that can be built to
if node_at_pos and
(node_at_pos.name == "air" or minetest.registered_nodes[node_at_pos.name].buildable_to == true) then
-- Check protection
if (not bypass_protection and not minetest.is_protected(pos, self.npc_name))
or bypass_protection == true then
-- Take from inventory if necessary
local place_item = false
if source == npc.actions.take_from_inventory then
if npc.take_item_from_inventory(self, node, 1) then
place_item = true
end
elseif source == npc.actions.take_from_inventory_forced then
npc.take_item_from_inventory(self, node, 1)
place_item = true
elseif source == npc.actions.force_place then
place_item = true
end
-- Place node
if place_item == true then
-- Set mine animation
self.object:set_animation({
x = npc.ANIMATION_MINE_START,
y = npc.ANIMATION_MINE_END},
self.animation.speed_normal, 0)
-- Place node
minetest.set_node(pos, {name=node})
-- Play place sound
if play_sound == true then
minetest.sound_play(
minetest.registered_nodes[node].sounds.place,
{
max_hear_distance = 10,
object = self.object
}
)
end
end
end
end
end
-- This function allows to move into directions that are walkable. It
-- avoids fences and allows to move on plants.
-- This will make for nice wanderings, making the NPC move smartly instead
-- of just getting stuck at places
local function random_dir_helper(start_pos, speed, dir_start, dir_end)
-- Limit the number of tries - otherwise it could become an infinite loop
for i = 1, 8 do
local dir = math.random(dir_start, dir_end)
local vel = vector.multiply(npc.actions.dir_data[dir].vel, speed)
local pos = vector.add(start_pos, vel)
local node = minetest.get_node(pos)
if node then
if node.name == "air"
-- Any walkable node except fences
or (minetest.registered_nodes[node.name].walkable == true
and minetest.registered_nodes[node.name].groups.fence ~= 1)
-- Farming plants
or minetest.registered_nodes[node.name].groups.plant == 1 then
return dir
end
end
end
-- Return -1 signaling that no good direction could be found
return -1
end
-- This action is to rotate to mob to a specifc direction. Currently, the code -- This action is to rotate to mob to a specifc direction. Currently, the code
-- contains also for diagonals, but remaining in the orthogonal domain is preferrable. -- contains also for diagonals, but remaining in the orthogonal domain is preferrable.
function npc.actions.rotate(self, args) function npc.actions.rotate(self, args)
local dir = args.dir local dir = args.dir
local yaw = 0 local yaw = args.yaw or 0
local start_pos = args.start_pos
local end_pos = args.end_pos
-- Calculate dir if positions are given
if start_pos and end_pos and not dir then
dir = npc.actions.get_direction(start_pos, end_pos)
end
-- Only yaw was given
if yaw and not dir and not start_pos and not end_pos then
self.object:setyaw(yaw)
return
end
self.rotate = 0 self.rotate = 0
if dir == npc.direction.north then if dir == npc.direction.north then
yaw = 0 yaw = 0
@ -177,20 +407,24 @@ end
-- true if it can move on that direction, and false if there is an obstacle -- true if it can move on that direction, and false if there is an obstacle
function npc.actions.walk_step(self, args) function npc.actions.walk_step(self, args)
local dir = args.dir local dir = args.dir
local step_into_air_only = args.step_into_air_only
local speed = args.speed local speed = args.speed
local target_pos = args.target_pos local target_pos = args.target_pos
local start_pos = args.start_pos
local vel = {} local vel = {}
-- Set default node per seconds -- Set default node per seconds
if speed == nil then if speed == nil then
speed = npc.actions.one_nps_speed speed = npc.actions.one_nps_speed
end end
-- If there is a target position to reach, set it
if target_pos ~= nil then
self.actions.walking.target_pos = target_pos
end
-- Set is_walking = true -- Check if dir should be random
self.actions.walking.is_walking = true if dir == "random_all" or dir == "random" then
dir = random_dir_helper(start_pos, speed, 0, 7)
end
if dir == "random_orthogonal" then
dir = random_dir_helper(start_pos, speed, 0, 3)
end
if dir == npc.direction.north then if dir == npc.direction.north then
vel = {x=0, y=0, z=speed} vel = {x=0, y=0, z=speed}
@ -207,45 +441,56 @@ function npc.actions.walk_step(self, args)
elseif dir == npc.direction.west then elseif dir == npc.direction.west then
vel = {x=-speed, y=0, z=0} vel = {x=-speed, y=0, z=0}
elseif dir == npc.direction.north_west then elseif dir == npc.direction.north_west then
vel = {x=-speed, y=0, z=speed} vel = {x=-speed, y=0, z=speed }
else
-- No direction provided or NPC is trapped, don't move NPC
vel = {x=0, y=0, z=0}
end end
-- If there is a target position to reach, set it and set walking to true
if target_pos ~= nil then
self.actions.walking.target_pos = target_pos
-- Set is_walking = true
self.actions.walking.is_walking = true
end
-- Rotate NPC -- Rotate NPC
npc.actions.rotate(self, {dir=dir}) npc.actions.rotate(self, {dir=dir})
-- Set velocity so that NPC walks -- Set velocity so that NPC walks
self.object:setvelocity(vel) self.object:setvelocity(vel)
-- Set walk animation -- Set walk animation
self.object:set_animation({ self.object:set_animation({
x = npc.ANIMATION_WALK_START, x = npc.ANIMATION_WALK_START,
y = npc.ANIMATION_WALK_END}, y = npc.ANIMATION_WALK_END},
self.animation.speed_normal, 0) self.animation.speed_normal, 0)
end end
-- This action makes the NPC stand and remain like that -- This action makes the NPC stand and remain like that
function npc.actions.stand(self, args) function npc.actions.stand(self, args)
local pos = args.pos local pos = args.pos
local dir = args.dir local dir = args.dir
-- Set is_walking = true -- Set is_walking = false
self.actions.walking.is_walking = false self.actions.walking.is_walking = false
-- Stop NPC -- Stop NPC
self.object:setvelocity({x=0, y=0, z=0}) self.object:setvelocity({x=0, y=0, z=0})
-- If position given, set to that position -- If position given, set to that position
if pos ~= nil then if pos ~= nil then
self.object:moveto(pos) self.object:moveto(pos)
end end
-- If dir given, set to that dir -- If dir given, set to that dir
if dir ~= nil then if dir ~= nil then
npc.actions.rotate(self, {dir=dir}) npc.actions.rotate(self, {dir=dir})
end end
-- Set stand animation -- Set stand animation
self.object:set_animation({ self.object:set_animation({
x = npc.ANIMATION_STAND_START, x = npc.ANIMATION_STAND_START,
y = npc.ANIMATION_STAND_END}, y = npc.ANIMATION_STAND_END},
self.animation.speed_normal, 0) self.animation.speed_normal, 0)
end end
-- This action makes the NPC sit on the node where it is -- This action makes the NPC sit on the node where it is
function npc.actions.sit(self, args) function npc.actions.sit(self, args)
local pos = args.pos local pos = args.pos
local dir = args.dir local dir = args.dir
-- Stop NPC -- Stop NPC
self.object:setvelocity({x=0, y=0, z=0}) self.object:setvelocity({x=0, y=0, z=0})
@ -259,9 +504,9 @@ function npc.actions.sit(self, args)
end end
-- Set sit animation -- Set sit animation
self.object:set_animation({ self.object:set_animation({
x = npc.ANIMATION_SIT_START, x = npc.ANIMATION_SIT_START,
y = npc.ANIMATION_SIT_END}, y = npc.ANIMATION_SIT_END},
self.animation.speed_normal, 0) self.animation.speed_normal, 0)
end end
-- This action makes the NPC lay on the node where it is -- This action makes the NPC lay on the node where it is
@ -275,9 +520,9 @@ function npc.actions.lay(self, args)
end end
-- Set sit animation -- Set sit animation
self.object:set_animation({ self.object:set_animation({
x = npc.ANIMATION_LAY_START, x = npc.ANIMATION_LAY_START,
y = npc.ANIMATION_LAY_END}, y = npc.ANIMATION_LAY_END},
self.animation.speed_normal, 0) self.animation.speed_normal, 0)
end end
-- Inventory functions for players and for nodes -- Inventory functions for players and for nodes
@ -309,7 +554,7 @@ function npc.actions.put_item_on_external_inventory(self, args)
end end
-- Add items to external inventory -- Add items to external inventory
inv:add_item(inv_list, item) inv:add_item(inv_list, item)
-- If this is a furnace, start furnace timer -- If this is a furnace, start furnace timer
if is_furnace == true then if is_furnace == true then
minetest.get_node_timer(pos):start(1.0) minetest.get_node_timer(pos):start(1.0)
@ -368,11 +613,18 @@ end
-- TODO: Refactor this function so that it uses a table to check -- TODO: Refactor this function so that it uses a table to check
-- for doors instead of having separate logic for each door type -- for doors instead of having separate logic for each door type
function npc.actions.get_openable_node_state(node, npc_dir) function npc.actions.get_openable_node_state(node, pos, npc_dir)
--minetest.log("Node name: "..dump(node.name)) --minetest.log("Node name: "..dump(node.name))
local state = npc.actions.const.doors.state.CLOSED local state = npc.actions.const.doors.state.CLOSED
-- Check for default doors and gates -- Check for MTG doors and gates
local a_i1, a_i2 = string.find(node.name, "_a") local mtg_door_closed = false
if minetest.get_item_group(node.name, "door") > 0 then
local back_pos = vector.add(pos, minetest.facedir_to_dir(node.param2))
local back_node = minetest.get_node(back_pos)
if back_node.name == "air" or minetest.registered_nodes[back_node.name].walkable == false then
mtg_door_closed = true
end
end
-- Check for cottages gates -- Check for cottages gates
local open_i1, open_i2 = string.find(node.name, "_close") local open_i1, open_i2 = string.find(node.name, "_close")
-- Check for cottages half door -- Check for cottages half door
@ -380,7 +632,7 @@ function npc.actions.get_openable_node_state(node, npc_dir)
if node.name == "cottages:half_door" then if node.name == "cottages:half_door" then
half_door_is_closed = (node.param2 + 2) % 4 == npc_dir half_door_is_closed = (node.param2 + 2) % 4 == npc_dir
end end
if a_i1 == nil and open_i1 == nil and not half_door_is_closed then if mtg_door_closed == false and open_i1 == nil and half_door_is_closed == false then
state = npc.actions.const.doors.state.OPEN state = npc.actions.const.doors.state.OPEN
end end
--minetest.log("Door state: "..dump(state)) --minetest.log("Door state: "..dump(state))
@ -395,9 +647,9 @@ function npc.actions.use_openable(self, args)
local action = args.action local action = args.action
local dir = args.dir local dir = args.dir
local node = minetest.get_node(pos) local node = minetest.get_node(pos)
local state = npc.actions.get_openable_node_state(node, dir) local state = npc.actions.get_openable_node_state(node, pos, dir)
local clicker = self.object local clicker = self.object.npc_name
if action ~= state then if action ~= state then
minetest.registered_nodes[node.name].on_rightclick(pos, node, clicker, nil, nil) minetest.registered_nodes[node.name].on_rightclick(pos, node, clicker, nil, nil)
end end
@ -411,7 +663,7 @@ end
-- walking from one place to another, operating a furnace, storing or taking -- walking from one place to another, operating a furnace, storing or taking
-- items from a chest, are provided here. -- items from a chest, are provided here.
local function get_pos_argument(self, pos) local function get_pos_argument(self, pos, use_access_node)
--minetest.log("Type of pos: "..dump(type(pos))) --minetest.log("Type of pos: "..dump(type(pos)))
-- Check which type of position argument we received -- Check which type of position argument we received
if type(pos) == "table" then if type(pos) == "table" then
@ -422,25 +674,48 @@ local function get_pos_argument(self, pos)
return pos return pos
elseif pos.place_type ~= nil then elseif pos.place_type ~= nil then
-- Received table in the following format: -- Received table in the following format:
-- {place_type = "", index = 1, use_access_node = false} -- {
-- place_category = "",
-- place_type = "",
-- index = 1,
-- use_access_node = false|true,
-- try_alternative_if_used = true|false
-- }
local index = pos.index or 1 local index = pos.index or 1
local use_access_node = pos.use_access_node or false local use_access_node = pos.use_access_node or false
local try_alternative_if_used = pos.try_alternative_if_used or false
local places = npc.places.get_by_type(self, pos.place_type) local places = npc.places.get_by_type(self, pos.place_type)
--minetest.log("Place type: "..dump(pos.place_type))
--minetest.log("Places: "..dump(places))
-- Check index is valid on the places map -- Check index is valid on the places map
if #places >= index then if #places >= index then
local place = places[index]
-- Check if place is used, and if it is, find alternative if required
if try_alternative_if_used == true then
place = npc.places.find_unused_place(self, pos.place_category, pos.place_type, place)
--minetest.log("Mark as used? "..dump(pos.mark_target_as_used))
if pos.mark_target_as_used == true then
--minetest.log("Marking as used: "..minetest.pos_to_string(place.pos))
npc.places.mark_place_used(place.pos, npc.places.USE_STATE.USED)
end
npc.places.add_shared_accessible_place(
self, {owner="", node_pos=place.pos}, npc.places.PLACE_TYPE.CALCULATED.TARGET, true, {})
end
-- Check if access node is desired -- Check if access node is desired
if use_access_node then if use_access_node == true then
-- Return actual node pos -- Return actual node pos
return places[index].access_node return place.access_node, place.pos
else else
-- Return node pos that allows access to node -- Return node pos that allows access to node
return places[index].pos return place.pos
end end
end end
end end
elseif type(pos) == "string" then elseif type(pos) == "string" then
-- Received name of place, so we are going to look for the actual pos -- Received name of place, so we are going to look for the actual pos
local places_pos = npc.places.get_by_type(self, pos) local places_pos = npc.places.get_by_type(self, pos, false)
-- Return nil if no position found -- Return nil if no position found
if places_pos == nil or #places_pos == 0 then if places_pos == nil or #places_pos == 0 then
return nil return nil
@ -450,13 +725,21 @@ local function get_pos_argument(self, pos)
-- Check all places, return owned if existent, else return the first one -- Check all places, return owned if existent, else return the first one
for i = 1, #places_pos do for i = 1, #places_pos do
if places_pos[i].status == "owned" then if places_pos[i].status == "owned" then
return places_pos[i].pos if use_access_node == true then
return places_pos[i].access_node, places_pos[i].pos
else
return places_pos[i].pos
end
end end
end end
end end
-- Return the first position only if it couldn't find an owned -- Return the first position only if it couldn't find an owned
-- place, or if it there is only oneg -- place, or if it there is only one
return places_pos[1].pos if use_access_node == true then
return places_pos[1].access_node, places_pos[1].pos
else
return places_pos[1].pos
end
end end
end end
@ -468,24 +751,25 @@ end
function npc.actions.use_furnace(self, args) function npc.actions.use_furnace(self, args)
local pos = get_pos_argument(self, args.pos) local pos = get_pos_argument(self, args.pos)
if pos == nil then if pos == nil then
npc.log("WARNING", "Got nil position in 'use_furnace' using args.pos: "..dump(args.pos)) -- npc.log("WARNING", "Got nil position in 'use_furnace' using args.pos: "..dump(args.pos))
return return
end end
local enable_usage_marking = args.enable_usage_marking or true
local item = args.item local item = args.item
local freeze = args.freeze local freeze = args.freeze
-- Define which items are usable as fuels. The NPC -- Define which items are usable as fuels. The NPC
-- will mainly use this as fuels to avoid getting useful -- will mainly use this as fuels to avoid getting useful
-- items (such as coal lumps) for burning -- items (such as coal lumps) for burning
local fuels = {"default:leaves", local fuels = {"default:leaves",
"default:pine_needles", "default:pine_needles",
"default:tree", "default:tree",
"default:acacia_tree", "default:acacia_tree",
"default:aspen_tree", "default:aspen_tree",
"default:jungletree", "default:jungletree",
"default:pine_tree", "default:pine_tree",
"default:coalblock", "default:coalblock",
"farming:straw"} "farming:straw"}
-- Check if NPC has item to cook -- Check if NPC has item to cook
local src_item = npc.inventory_contains(self, npc.get_item_name(item)) local src_item = npc.inventory_contains(self, npc.get_item_name(item))
@ -496,23 +780,23 @@ function npc.actions.use_furnace(self, args)
-- Check if NPC has a fuel item -- Check if NPC has a fuel item
for i = 1,9 do for i = 1,9 do
local fuel_item = npc.inventory_contains(self, fuels[i]) local fuel_item = npc.inventory_contains(self, fuels[i])
if fuel_item ~= nil then if fuel_item ~= nil then
-- Get fuel item's burn time -- Get fuel item's burn time
local fuel_time = local fuel_time =
minetest.get_craft_result({method="fuel", width=1, items={ItemStack(fuel_item.item_string)}}).time minetest.get_craft_result({method="fuel", width=1, items={ItemStack(fuel_item.item_string)}}).time
local total_fuel_time = fuel_time * npc.get_item_count(fuel_item.item_string) local total_fuel_time = fuel_time * npc.get_item_count(fuel_item.item_string)
npc.log("DEBUG", "Fuel time: "..dump(fuel_time)) --npc.log("DEBUG", "Fuel time: "..dump(fuel_time))
-- Get item to cook's cooking time -- Get item to cook's cooking time
local cook_result = local cook_result =
minetest.get_craft_result({method="cooking", width=1, items={ItemStack(src_item.item_string)}}) minetest.get_craft_result({method="cooking", width=1, items={ItemStack(src_item.item_string)}})
local total_cook_time = cook_result.time * npc.get_item_count(item) local total_cook_time = cook_result.time * npc.get_item_count(item)
npc.log("DEBUG", "Cook: "..dump(cook_result)) --npc.log("DEBUG", "Cook: "..dump(cook_result))
npc.log("DEBUG", "Total cook time: "..total_cook_time -- npc.log("DEBUG", "Total cook time: "..total_cook_time
..", total fuel burn time: "..dump(total_fuel_time)) -- ..", total fuel burn time: "..dump(total_fuel_time))
-- Check if there is enough fuel to cook all items -- Check if there is enough fuel to cook all items
if total_cook_time > total_fuel_time then if total_cook_time > total_fuel_time then
@ -525,6 +809,12 @@ function npc.actions.use_furnace(self, args)
return cook_result.time - fuel_time return cook_result.time - fuel_time
end end
-- Set furnace as used if flag is enabled
if enable_usage_marking then
-- Set place as used
npc.places.mark_place_used(pos, npc.places.USE_STATE.USED)
end
-- Calculate how much fuel is needed -- Calculate how much fuel is needed
local fuel_amount = total_cook_time / fuel_time local fuel_amount = total_cook_time / fuel_time
if fuel_amount < 1 then if fuel_amount < 1 then
@ -535,26 +825,26 @@ function npc.actions.use_furnace(self, args)
-- Put this item on the fuel inventory list of the furnace -- Put this item on the fuel inventory list of the furnace
local args = { local args = {
player = nil, player = nil,
pos = pos, pos = pos,
inv_list = "fuel", inv_list = "fuel",
item_name = npc.get_item_name(fuel_item.item_string), item_name = npc.get_item_name(fuel_item.item_string),
count = fuel_amount count = fuel_amount
} }
npc.add_action(self, npc.actions.cmd.PUT_ITEM, args) npc.add_action(self, npc.actions.cmd.PUT_ITEM, args)
-- Put the item that we want to cook on the furnace -- Put the item that we want to cook on the furnace
args = { args = {
player = nil, player = nil,
pos = pos, pos = pos,
inv_list = "src", inv_list = "src",
item_name = npc.get_item_name(src_item.item_string), item_name = npc.get_item_name(src_item.item_string),
count = npc.get_item_count(item), count = npc.get_item_count(item),
is_furnace = true is_furnace = true
} }
npc.add_action(self, npc.actions.cmd.PUT_ITEM, args) npc.add_action(self, npc.actions.cmd.PUT_ITEM, args)
-- Now, set NPC to wait until furnace is done. -- Now, set NPC to wait until furnace is done.
npc.log("DEBUG", "Setting wait action for "..dump(total_cook_time)) -- npc.log("DEBUG", "Setting wait action for "..dump(total_cook_time))
npc.add_action(self, npc.actions.cmd.SET_INTERVAL, {interval=total_cook_time, freeze=freeze}) npc.add_action(self, npc.actions.cmd.SET_INTERVAL, {interval=total_cook_time, freeze=freeze})
-- Reset timer -- Reset timer
@ -563,24 +853,30 @@ function npc.actions.use_furnace(self, args)
-- If freeze is false, then we will have to find the way back to the furnace -- If freeze is false, then we will have to find the way back to the furnace
-- once cooking is done. -- once cooking is done.
if freeze == false then if freeze == false then
npc.log("DEBUG", "Adding walk to position to wandering: "..dump(pos)) -- npc.log("DEBUG", "Adding walk to position to wandering: "..dump(pos))
npc.add_task(self, npc.actions.cmd.WALK_TO_POS, {end_pos=pos, walkable={}}) npc.add_task(self, npc.actions.cmd.WALK_TO_POS, {end_pos=pos, walkable={}})
end end
-- Take cooked items back -- Take cooked items back
args = { args = {
player = nil, player = nil,
pos = pos, pos = pos,
inv_list = "dst", inv_list = "dst",
item_name = cook_result.item:get_name(), item_name = cook_result.item:get_name(),
count = npc.get_item_count(item), count = npc.get_item_count(item),
is_furnace = false is_furnace = false
} }
npc.log("DEBUG", "Taking item back: "..minetest.pos_to_string(pos)) npc.log("DEBUG", "Taking item back: "..minetest.pos_to_string(pos))
npc.add_action(self, npc.actions.cmd.TAKE_ITEM, args) npc.add_action(self, npc.actions.cmd.TAKE_ITEM, args)
npc.log("DEBUG", "Inventory: "..dump(self.inventory)) -- npc.log("DEBUG", "Inventory: "..dump(self.inventory))
-- Set furnace as unused if flag is enabled
if enable_usage_marking then
-- Set place as used
npc.places.mark_place_used(pos, npc.places.USE_STATE.NOT_USED)
end
return true return true
end end
@ -594,25 +890,39 @@ end
function npc.actions.use_bed(self, args) function npc.actions.use_bed(self, args)
local pos = get_pos_argument(self, args.pos) local pos = get_pos_argument(self, args.pos)
if pos == nil then if pos == nil then
npc.log("WARNING", "Got nil position in 'use_bed' using args.pos: "..dump(args.pos)) -- npc.log("WARNING", "Got nil position in 'use_bed' using args.pos: "..dump(args.pos))
return return
end end
local action = args.action local action = args.action
local enable_usage_marking = args.enable_usage_marking or true
local node = minetest.get_node(pos) local node = minetest.get_node(pos)
minetest.log(dump(node)) --minetest.log(dump(node))
local dir = minetest.facedir_to_dir(node.param2) local dir = minetest.facedir_to_dir(node.param2)
if action == npc.actions.const.beds.LAY then if action == npc.actions.const.beds.LAY then
-- Get position -- Get position
-- Error here due to ignore. Need to come up with better solution
if node.name == "ignore" then
return
end
local bed_pos = npc.actions.nodes.beds[node.name].get_lay_pos(pos, dir) local bed_pos = npc.actions.nodes.beds[node.name].get_lay_pos(pos, dir)
-- Sit down on bed, rotate to correct direction -- Sit down on bed, rotate to correct direction
npc.add_action(self, npc.actions.cmd.SIT, {pos=bed_pos, dir=(node.param2 + 2) % 4}) npc.add_action(self, npc.actions.cmd.SIT, {pos=bed_pos, dir=(node.param2 + 2) % 4})
-- Lay down -- Lay down
npc.add_action(self, npc.actions.cmd.LAY, {}) npc.add_action(self, npc.actions.cmd.LAY, {})
if enable_usage_marking then
-- Set place as used
npc.places.mark_place_used(pos, npc.places.USE_STATE.USED)
end
self.actions.move_state.is_laying = true
else else
-- Calculate position to get up -- Calculate position to get up
-- Error here due to ignore. Need to come up with better solution
if node.name == "ignore" then
return
end
local bed_pos_y = npc.actions.nodes.beds[node.name].get_lay_pos(pos, dir).y local bed_pos_y = npc.actions.nodes.beds[node.name].get_lay_pos(pos, dir).y
local bed_pos = {x = pos.x, y = bed_pos_y, z = pos.z} local bed_pos = {x = pos.x, y = bed_pos_y, z = pos.z}
-- Sit up -- Sit up
npc.add_action(self, npc.actions.cmd.SIT, {pos=bed_pos}) npc.add_action(self, npc.actions.cmd.SIT, {pos=bed_pos})
-- Initialize direction: Default is front of bottom of bed -- Initialize direction: Default is front of bottom of bed
@ -623,29 +933,38 @@ function npc.actions.use_bed(self, args)
if npc.actions.nodes.beds[node.name].type == "mat" then if npc.actions.nodes.beds[node.name].type == "mat" then
y_adjustment = 0 y_adjustment = 0
end end
local pos_out_of_bed = pos
local empty_nodes = npc.places.find_node_orthogonally(bed_pos, {"air", "cottages:bench"}, y_adjustment) local empty_nodes = npc.places.find_node_orthogonally(bed_pos, {"air", "cottages:bench"}, y_adjustment)
if empty_nodes ~= nil then if empty_nodes ~= nil and #empty_nodes > 0 then
-- Get direction to the empty node -- Get direction to the empty node
dir = npc.actions.get_direction(bed_pos, empty_nodes[1].pos) dir = npc.actions.get_direction(bed_pos, empty_nodes[1].pos)
end
-- Calculate position to get out of bed -- Calculate position to get out of bed
local pos_out_of_bed = pos_out_of_bed =
{x=empty_nodes[1].pos.x, y=empty_nodes[1].pos.y + 1, z=empty_nodes[1].pos.z} {x=empty_nodes[1].pos.x, y=empty_nodes[1].pos.y + 1, z=empty_nodes[1].pos.z}
-- Account for benches if they are present to avoid standing over them -- Account for benches if they are present to avoid standing over them
if empty_nodes[1].name == "cottages:bench" then if empty_nodes[1].name == "cottages:bench" then
pos_out_of_bed = {x=empty_nodes[1].pos.x, y=empty_nodes[1].pos.y + 1, z=empty_nodes[1].pos.z} pos_out_of_bed = {x=empty_nodes[1].pos.x, y=empty_nodes[1].pos.y + 1, z=empty_nodes[1].pos.z}
if empty_nodes[1].param2 == 0 then if empty_nodes[1].param2 == 0 then
pos_out_of_bed.z = pos_out_of_bed.z - 0.3 pos_out_of_bed.z = pos_out_of_bed.z - 0.3
elseif empty_nodes[1].param2 == 1 then elseif empty_nodes[1].param2 == 1 then
pos_out_of_bed.x = pos_out_of_bed.x - 0.3 pos_out_of_bed.x = pos_out_of_bed.x - 0.3
elseif empty_nodes[1].param2 == 2 then elseif empty_nodes[1].param2 == 2 then
pos_out_of_bed.z = pos_out_of_bed.z + 0.3 pos_out_of_bed.z = pos_out_of_bed.z + 0.3
elseif empty_nodes[1].param2 == 3 then elseif empty_nodes[1].param2 == 3 then
pos_out_of_bed.x = pos_out_of_bed.x + 0.3 pos_out_of_bed.x = pos_out_of_bed.x + 0.3
end
end end
end end
-- Stand out of bed -- Stand out of bed
npc.add_action(self, npc.actions.cmd.STAND, {pos=pos_out_of_bed, dir=dir}) npc.add_action(self, npc.actions.cmd.STAND, {pos=pos_out_of_bed, dir=dir})
if enable_usage_marking then
-- Set place as unused
npc.places.mark_place_used(pos, npc.places.USE_STATE.NOT_USED)
end
self.actions.move_state.is_laying = false
end end
end end
@ -654,18 +973,32 @@ end
function npc.actions.use_sittable(self, args) function npc.actions.use_sittable(self, args)
local pos = get_pos_argument(self, args.pos) local pos = get_pos_argument(self, args.pos)
if pos == nil then if pos == nil then
npc.log("WARNING", "Got nil position in 'use_sittable' using args.pos: "..dump(args.pos)) -- npc.log("WARNING", "Got nil position in 'use_sittable' using args.pos: "..dump(args.pos))
return return
end end
local action = args.action local action = args.action
local enable_usage_marking = args.enable_usage_marking or true
local node = minetest.get_node(pos) local node = minetest.get_node(pos)
if action == npc.actions.const.sittable.SIT then if action == npc.actions.const.sittable.SIT then
-- Calculate position depending on bench -- Calculate position depending on bench
-- Error here due to ignore. Need to come up with better solution
if node.name == "ignore" then
return
end
local sit_pos = npc.actions.nodes.sittable[node.name].get_sit_pos(pos, node.param2) local sit_pos = npc.actions.nodes.sittable[node.name].get_sit_pos(pos, node.param2)
-- Sit down on bench/chair/stairs -- Sit down on bench/chair/stairs
npc.add_action(self, npc.actions.cmd.SIT, {pos=sit_pos, dir=(node.param2 + 2) % 4}) npc.add_action(self, npc.actions.cmd.SIT, {pos=sit_pos, dir=(node.param2 + 2) % 4})
if enable_usage_marking then
-- Set place as used
npc.places.mark_place_used(pos, npc.places.USE_STATE.USED)
end
self.actions.move_state.is_sitting = true
else else
if self.actions.move_state.is_sitting == false then
npc.log("DEBUG_ACTION", "NPC "..self.npc_name.." attempted to get up from sit when it is not sitting.")
return
end
-- Find empty areas around chair -- Find empty areas around chair
local dir = node.param2 + 2 % 4 local dir = node.param2 + 2 % 4
-- Default it to the current position in case it can't find empty -- Default it to the current position in case it can't find empty
@ -684,13 +1017,20 @@ function npc.actions.use_sittable(self, args)
end end
-- Stand -- Stand
npc.add_action(self, npc.actions.cmd.STAND, {pos=pos_out_of_sittable, dir=dir}) npc.add_action(self, npc.actions.cmd.STAND, {pos=pos_out_of_sittable, dir=dir})
minetest.log("Setting sittable at "..minetest.pos_to_string(pos).." as not used")
if enable_usage_marking then
-- Set place as unused
npc.places.mark_place_used(pos, npc.places.USE_STATE.NOT_USED)
end
self.actions.move_state.is_sitting = false
end end
end end
-- This function returns the direction enum -- This function returns the direction enum
-- for the moving from v1 to v2 -- for the moving from v1 to v2
function npc.actions.get_direction(v1, v2) function npc.actions.get_direction(v1, v2)
local dir = vector.subtract(v2, v1) local vector_dir = vector.direction(v1, v2)
local dir = vector.round(vector_dir)
if dir.x ~= 0 and dir.z ~= 0 then if dir.x ~= 0 and dir.z ~= 0 then
if dir.x > 0 and dir.z > 0 then if dir.x > 0 and dir.z > 0 then
@ -724,10 +1064,11 @@ end
-- going to be considered walkable for the algorithm to find a -- going to be considered walkable for the algorithm to find a
-- path. -- path.
function npc.actions.walk_to_pos(self, args) function npc.actions.walk_to_pos(self, args)
-- Get arguments for this task -- Get arguments for this task
local end_pos = get_pos_argument(self, args.end_pos) local use_access_node = args.use_access_node or true
local end_pos, node_pos = get_pos_argument(self, args.end_pos, use_access_node)
if end_pos == nil then if end_pos == nil then
npc.log("WARNING", "Got nil position in 'walk_to_pos' using args.pos: "..dump(args.end_pos)) -- npc.log("WARNING", "Got nil position in 'walk_to_pos' using args.pos: "..dump(args.end_pos))
return return
end end
local enforce_move = args.enforce_move or true local enforce_move = args.enforce_move or true
@ -735,31 +1076,31 @@ function npc.actions.walk_to_pos(self, args)
-- Round start_pos to make sure it can find start and end -- Round start_pos to make sure it can find start and end
local start_pos = vector.round(self.object:getpos()) local start_pos = vector.round(self.object:getpos())
-- Use y of end_pos (this can only be done assuming flat terrain)
--start_pos.y = self.object:getpos().y
npc.log("DEBUG", "walk_to_pos: Start pos: "..minetest.pos_to_string(start_pos)) npc.log("DEBUG", "walk_to_pos: Start pos: "..minetest.pos_to_string(start_pos))
npc.log("DEBUG", "walk_to_pos: End pos: "..minetest.pos_to_string(end_pos)) npc.log("DEBUG", "walk_to_pos: End pos: "..minetest.pos_to_string(end_pos))
-- Check if start_pos and end_pos are the same
if vector.equals(start_pos, end_pos) == true then
-- Check if it was using access node, if it was, add action to
-- rotate NPC into that direction
if use_access_node == true then
local dir = npc.actions.get_direction(end_pos, node_pos)
npc.add_action(self, npc.actions.cmd.STAND, {dir = dir})
end
npc.log("WARNING", "walk_to_pos Found start_pos == end_pos")
return
end
-- Set walkable nodes to empty if the parameter hasn't been used -- Set walkable nodes to empty if the parameter hasn't been used
if walkable_nodes == nil then if walkable_nodes == nil then
walkable_nodes = {} walkable_nodes = {}
end end
-- Find path -- Find path
--local path = pathfinder.find_path(start_pos, end_pos, 20, walkable_nodes) local path = npc.pathfinder.find_path(start_pos, end_pos, self, true)
local path = pathfinder.find_path(start_pos, end_pos, self)
if path ~= nil and #path > 1 then if path ~= nil and #path > 1 then
-- Get details from path nodes
-- This might get moved to proper place, pathfinder.lua code
local path_detail = {}
for i = 1, #path do
local node = minetest.get_node(path[i])
table.insert(path_detail, {pos=path[i], type=npc.pathfinder.is_good_node(node, {})})
end
path = path_detail
npc.log("DEBUG", "Detailed path: "..dump(path))
npc.log("INFO", "walk_to_pos Found path to node: "..minetest.pos_to_string(end_pos)) npc.log("INFO", "walk_to_pos Found path to node: "..minetest.pos_to_string(end_pos))
-- Store path -- Store path
self.actions.walking.path = path self.actions.walking.path = path
@ -784,6 +1125,11 @@ function npc.actions.walk_to_pos(self, args)
-- Add the last step -- Add the last step
npc.add_action(self, npc.actions.cmd.WALK_STEP, {dir = dir, speed = speed, target_pos = path[i+1].pos}) npc.add_action(self, npc.actions.cmd.WALK_STEP, {dir = dir, speed = speed, target_pos = path[i+1].pos})
-- Add stand animation at end -- Add stand animation at end
if use_access_node == true then
dir = npc.actions.get_direction(end_pos, node_pos)
end
-- minetest.log("Dir: "..dump(dir))
-- Change dir if using access_node
npc.add_action(self, npc.actions.cmd.STAND, {dir = dir}) npc.add_action(self, npc.actions.cmd.STAND, {dir = dir})
break break
end end
@ -793,13 +1139,15 @@ function npc.actions.walk_to_pos(self, args)
if path[i+1].type == npc.pathfinder.node_types.openable then if path[i+1].type == npc.pathfinder.node_types.openable then
-- Check if door is already open -- Check if door is already open
local node = minetest.get_node(path[i+1].pos) local node = minetest.get_node(path[i+1].pos)
if npc.actions.get_openable_node_state(node, dir) == npc.actions.const.doors.state.CLOSED then if npc.actions.get_openable_node_state(node, path[i+1].pos, dir) == npc.actions.const.doors.state.CLOSED then
--minetest.log("Opening action to open door") --minetest.log("Opening action to open door")
-- Stop to open door, this avoids misplaced movements later on -- Stop to open door, this avoids misplaced movements later on
npc.add_action(self, npc.actions.cmd.STAND, {dir=dir}) npc.add_action(self, npc.actions.cmd.STAND, {dir=dir})
-- Open door -- Open door
npc.add_action(self, npc.actions.cmd.USE_OPENABLE, {pos=path[i+1].pos, dir=dir, action=npc.actions.const.doors.action.OPEN}) npc.add_action(self, npc.actions.cmd.USE_OPENABLE, {pos=path[i+1].pos, dir=dir, action=npc.actions.const.doors.action.OPEN})
door_opened = true
else
door_opened = true door_opened = true
end end
@ -824,9 +1172,10 @@ function npc.actions.walk_to_pos(self, args)
-- Add extra walk step to ensure that one is standing at other side of openable node -- Add extra walk step to ensure that one is standing at other side of openable node
-- npc.add_action(self, npc.actions.cmd.WALK_STEP, {dir = dir, speed = speed, target_pos = path[i+2].pos}) -- npc.add_action(self, npc.actions.cmd.WALK_STEP, {dir = dir, speed = speed, target_pos = path[i+2].pos})
-- Stop to close the door -- Stop to close the door
npc.add_action(self, npc.actions.cmd.STAND, {dir=(dir + 2) % 4 })--, pos=pos_on_close}) --npc.add_action(self, npc.actions.cmd.STAND, {dir=(dir + 2) % 4 })--, pos=pos_on_close})
npc.add_action(self, npc.actions.cmd.STAND, {dir=dir })--, pos=pos_on_close})
-- Close door -- Close door
npc.add_action(self, npc.actions.cmd.USE_OPENABLE, {pos=path[i+1].pos, action=npc.actions.const.doors.action.CLOSE}) npc.add_action(self, npc.actions.cmd.USE_OPENABLE, {pos=path[i+1].pos, dir=dir, action=npc.actions.const.doors.action.CLOSE})
door_opened = false door_opened = false
end end
@ -844,6 +1193,6 @@ function npc.actions.walk_to_pos(self, args)
if enforce_move then if enforce_move then
-- Move to end pos -- Move to end pos
self.object:moveto({x=end_pos.x, y=end_pos.y+1, z=end_pos.z}) self.object:moveto({x=end_pos.x, y=end_pos.y+1, z=end_pos.z})
end end
end end
end end

File diff suppressed because it is too large Load Diff

View File

@ -1,246 +1,395 @@
-- Pathfinding code by Zorman2000 -- Pathfinding code by MarkBu, original can be found here:
-- https://github.com/MarkuBu/pathfinder
--
-- Modifications by Zorman2000
-- This version is slightly modified to use another "walkable" function,
-- plus add a "decorating" path function which allows to know the type
-- of nodes in the path.
--------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------
-- Pathfinding functionality -- Pathfinding functionality
--------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------
-- This class contains functions that allows to map the 3D map of Minetest into
-- a 2D array (basically by ignoring the y coordinate for the moment being) in order
-- to use the A* pathfinding algorithm to find the shortest path from one node to
-- another. The A* algorithm implementation is in the 'jumper.lua' file which a
-- reduced and slightly modified version of the Jumper library, by Roland Yonaba
-- (https://github.com/Yonaba/Jumper).
-- Mapping algorithm: transforms a Minetest map surface to a 2d grid.
npc.pathfinder = {} npc.pathfinder = {}
local pathfinder = {} local pathfinder = {}
npc.pathfinder.node_types = { npc.pathfinder.node_types = {
start = 0, start = 0,
goal = 1, goal = 1,
walkable = 2, walkable = 2,
openable = 3, openable = 3,
non_walkable = 4 non_walkable = 4
} }
npc.pathfinder.nodes = { npc.pathfinder.nodes = {
openable_prefix = { openable_prefix = {
"doors:", "doors:",
"cottages:gate", "cottages:gate",
"cottages:half_door" "cottages:half_door"
} }
} }
-- This function is used to determine if a node is walkable
-- or openable, in which case is good to use when finding a path
function pathfinder.is_good_node(node, exceptions)
--local function is_good_node(node, exceptions)
-- Is openable is to support doors, fence gates and other
-- doors from other mods. Currently, default doors, gates
-- and cottages doors are supported.
local is_openable = false
for _,node_prefix in pairs(npc.pathfinder.nodes.openable_prefix) do
local start_i,end_i = string.find(node.name, node_prefix)
if start_i ~= nil then
is_openable = true
break
end
end
if node ~= nil and node.name ~= nil and not minetest.registered_nodes[node.name].walkable then
return npc.pathfinder.node_types.walkable
elseif is_openable then
return npc.pathfinder.node_types.openable
else
for i = 1, #exceptions do
if node.name == exceptions[i] then
return npc.pathfinder.node_types.walkable
end
end
return npc.pathfinder.node_types.non_walkable
end
end
-- This function uses the mapping functions and the A* algorithm implementation function pathfinder.get_decorated_path(path)
-- of the Jumper library to find a path from start_pos to end_pos. The range is -- Get details from path nodes
-- an extra amount of nodes to search in both the x and z coordinates. local path_detail = {}
function npc.pathfinder.find_path(start_pos, end_pos, range, walkable_nodes) for i = 1, #path do
-- Check that start and end position are not the same local node = minetest.get_node(path[i])
if start_pos.x == end_pos.x and start_pos.z == end_pos.z then table.insert(path_detail, {pos={x=path[i].x, y=path[i].y-0.5, z=path[i].z},
return nil type=pathfinder.is_good_node(node, {})})
end end
-- Set walkable nodes to empty if parameter wasn't used
if walkable_nodes == nil then
walkable_nodes = {}
end
-- Map the Minetest area to a 2D array
local map = pathfinder.create_map(start_pos, end_pos, range, walkable_nodes)
-- Find start and end positions
local pos = pathfinder.find_start_and_end_pos(map)
-- Normalize the map
local normalized_map = pathfinder.normalize_map(map)
-- Create pathfinder object
local grid_object = Grid(normalized_map)
-- Define what is a walkable node
local walkable = 0
-- Pathfinder object using A* algorithm --npc.log("DEBUG", "Detailed path: "..dump(path_detail))
local finder = Pathfinder(grid_object, "ASTAR", walkable) return path_detail
-- Set orthogonal mode meaning it will not move in diagonal directions end
finder:setMode("ORTHOGONAL")
-- Calculates the path, and its length function npc.pathfinder.find_path(start_pos, end_pos, entity, decorate_path)
local path = finder:getPath(pos.start_pos.x, pos.start_pos.z, pos.end_pos.x, pos.end_pos.z) local path = pathfinder.find_path(start_pos, end_pos, entity)
if path then
if decorate_path then
path = pathfinder.get_decorated_path(path)
end
else
npc.log("ERROR", "Couldn't find path from "..minetest.pos_to_string(start_pos)
.." to "..minetest.pos_to_string(end_pos))
end
return path
end
--minetest.log("Found path: "..dump(path)) -- From this point onwards is MarkBu's original pathfinder code,
-- Pretty-printing the results -- except for the "walkable" function, which is modified by Zorman2000
if path then -- to include doors and other "walkable" nodes.
return pathfinder.get_path(map, path:nodes()) -- The version here is exactly this:
end -- https://github.com/MarkuBu/pathfinder/commit/ca0b433bf5efde5da545b11b2691fa7f7e53dc30
--[[
minetest.get_content_id(name)
minetest.registered_nodes
minetest.get_name_from_content_id(id)
local ivm = a:index(pos.x, pos.y, pos.z)
local ivm = a:indexp(pos)
minetest.hash_node_position({x=,y=,z=})
minetest.get_position_from_hash(hash)
start_index, target_index, current_index
^ Hash of position
current_value
^ {int:hCost, int:gCost, int:fCost, hash:parent, vect:pos}
]]--
local openSet = {}
local closedSet = {}
local function get_distance(start_pos, end_pos)
local distX = math.abs(start_pos.x - end_pos.x)
local distZ = math.abs(start_pos.z - end_pos.z)
if distX > distZ then
return 14 * distZ + 10 * (distX - distZ)
else
return 14 * distX + 10 * (distZ - distX)
end
end
local function get_distance_to_neighbor(start_pos, end_pos)
local distX = math.abs(start_pos.x - end_pos.x)
local distY = math.abs(start_pos.y - end_pos.y)
local distZ = math.abs(start_pos.z - end_pos.z)
if distX > distZ then
return (14 * distZ + 10 * (distX - distZ)) * (distY + 1)
else
return (14 * distX + 10 * (distZ - distX)) * (distY + 1)
end
end end
-- This function is used to determine if a node is walkable -- This function is used to determine if a node is walkable
-- or openable, in which case is good to use when finding a path -- or openable, in which case is good to use when finding a path
local function walkable(node, exceptions)
function npc.pathfinder.is_good_node(node, exceptions) local exceptions = exceptions or {}
--local function is_good_node(node, exceptions) -- Is openable is to support doors, fence gates and other
-- Is openable is to support doors, fence gates and other -- doors from other mods. Currently, default doors, gates
-- doors from other mods. Currently, default doors, gates -- and cottages doors are supported.
-- and cottages doors are supported. --minetest.log("Is good node: "..dump(node))
--minetest.log("Is good node: "..dump(node)) local is_openable = false
local is_openable = false for _,node_prefix in pairs(npc.pathfinder.nodes.openable_prefix) do
for _,node_prefix in pairs(npc.pathfinder.nodes.openable_prefix) do local start_i,end_i = string.find(node.name, node_prefix)
--for _,node_prefix in pairs(pathfinder.nodes.openable_prefix) do if start_i ~= nil then
local start_i,end_i = string.find(node.name, node_prefix) is_openable = true
if start_i ~= nil then break
is_openable = true end
break end
end -- Detect mg_villages ceilings usage of thin wood nodeboxes
end -- TODO: Improve
if node ~= nil and node.name ~= nil and not minetest.registered_nodes[node.name].walkable then local is_mg_villages_ceiling = false
--return pathfinder.node_types.walkable if node.name == "cottages:wood_flat" then
return npc.pathfinder.node_types.walkable is_mg_villages_ceiling = true
elseif is_openable then end
return npc.pathfinder.node_types.openable if node ~= nil
--return pathfinder.node_types.openable and node.name ~= nil
else and node.name ~= "ignore"
for i = 1, #exceptions do and minetest.registered_nodes[node.name]
if node.name == exceptions[i] then and not minetest.registered_nodes[node.name].walkable then
return npc.pathfinder.node_types.walkable return false
--return pathfinder.node_types.walkable elseif is_openable then
end return false
end elseif is_mg_villages_ceiling then
return npc.pathfinder.node_types.non_walkable return false
--return pathfinder.node_types.non_walkable else
end for i = 1, #exceptions do
end if node.name == exceptions[i] then
return false
function pathfinder.create_map(start_pos, end_pos, extra_range, walkables) end
end
minetest.log("Start pos: "..minetest.pos_to_string(start_pos)) return true
minetest.log("End pos: "..minetest.pos_to_string(end_pos))
-- Calculate all signs to ensure:
-- 1. Correct area calculation
-- 2. Iterate in the correct direction
local start_x_sign = (start_pos.x - end_pos.x) / math.abs(start_pos.x - end_pos.x)
local start_z_sign = (start_pos.z - end_pos.z) / math.abs(start_pos.z - end_pos.z)
local end_x_sign = (end_pos.x - start_pos.x) / math.abs(end_pos.x - start_pos.x)
local end_z_sign = (end_pos.z - start_pos.z) / math.abs(end_pos.z - start_pos.z)
-- Correct the signs if they are nan
if math.abs(start_pos.x - end_pos.x) == 0 then
start_x_sign = -1
end_x_sign = 1
end
if math.abs(start_pos.z - end_pos.z) == 0 then
start_z_sign = -1
end_z_sign = 1
end
-- Get starting and ending positions, adding the extra nodes to the area
local pos1 = {x=start_pos.x + (extra_range * start_x_sign), y = start_pos.y - 1, z=start_pos.z + (extra_range * start_z_sign)}
local pos2 = {x=end_pos.x + (extra_range * end_x_sign), y = end_pos.y, z=end_pos.z + (extra_range * end_z_sign)}
local grid = {}
-- Loop through the area and classify nodes
for z = 1, math.abs(pos1.z - pos2.z) do
local current_row = {}
for x = 1, math.abs(pos1.x - pos2.x) do
-- Calculate current position
local current_pos = {x=pos1.x + (x*end_x_sign), y=pos1.y, z=pos1.z + (z*end_z_sign)}
-- Check if this is the starting position
if current_pos.x == start_pos.x and current_pos.z == start_pos.z then
-- Is start position
table.insert(current_row, {pos=current_pos, type=pathfinder.node_types.start})
elseif current_pos.x == end_pos.x and current_pos.z == end_pos.z then
-- Is ending position or goal position
table.insert(current_row, {pos=current_pos, type=pathfinder.node_types.goal})
else
-- Check if node is walkable
local node = minetest.get_node(current_pos)
-- Check node has air above it
local node_above = minetest.get_node({x=current_pos.x, y=current_pos.y+1, z=current_pos.z})
if node.name == "air" then
-- Check if node above is air
if node.name == "air" then
-- If air do no more checks
table.insert(current_row, {pos=current_pos, type=pathfinder.node_types.walkable})
end
else
-- Check if it is of a walkable or openable type
table.insert(current_row, {pos=current_pos, type=is_good_node(node, walkables)})
end
end
end
-- Insert the converted row into the grid
table.insert(grid, current_row)
end end
return grid
end end
-- Utility function to print the created map to the console. local function check_clearance(cpos, x, z, height)
-- Used for debug. for i = 1, height do
local function print_map(map) local n_name = minetest.get_node({x = cpos.x + x, y = cpos.y + i, z = cpos.z + z}).name
for z,row in pairs(map) do local c_name = minetest.get_node({x = cpos.x, y = cpos.y + i, z = cpos.z}).name
local row_string = "[" --~ print(i, n_name, c_name)
for x,node in pairs(row) do if walkable(n_name) or walkable(c_name) then
if node.type == 2 then return false
row_string = row_string.."- " end
else end
row_string = row_string..node.type.." " return true
end
-- Use the following if the coordinates are also needed
--row_string = row_string..node.type..": {"..node.pos.x..", "..node.pos.y..", "..node.pos.z.."}, "
end
row_string = row_string.."]"
print(row_string)
end
end end
local function get_neighbor_ground_level(pos, jump_height, fall_height)
-- This function find the starting and ending points in the local node = minetest.get_node(pos)
-- map representation, and returns the coordinates in the map local height = 0
-- for the pathfinding algorithm to use if walkable(node) then
function pathfinder.find_start_and_end_pos(map) repeat
-- This is for debug height = height + 1
--print_map(map) if height > jump_height then
local result = {} return nil
for z,row in pairs(map) do end
for x,node in pairs(row) do pos.y = pos.y + 1
if node.type == pathfinder.node_types.start then node = minetest.get_node(pos)
--minetest.log("Start node: "..dump(node)) until not walkable(node)
result["start_pos"] = {x=x, z=z} return pos
elseif node.type == pathfinder.node_types.goal then else
--minetest.log("End node: "..dump(node)) repeat
result["end_pos"] = {x=x, z=z} height = height + 1
end if height > fall_height then
end return nil
end end
--minetest.log("Found start and end positions: ("..result.start_pos.)..", "..minetest.pos_to_string(result.end_pos)) pos.y = pos.y - 1
return result node = minetest.get_node(pos)
until walkable(node)
return {x = pos.x, y = pos.y + 1, z = pos.z}
end
end end
-- This function transforms the grid into binary values function pathfinder.find_path(pos, endpos, entity)
-- (0 walkable, 1 non-walkable) for the pathfinding algorithm. local start_index = minetest.hash_node_position(pos)
function pathfinder.normalize_map(map) local target_index = minetest.hash_node_position(endpos)
local result = {} local count = 1
for _,row in pairs(map) do
local result_row = {}
for _,node in pairs(row) do
if node.type ~= pathfinder.node_types.non_walkable then
table.insert(result_row, 0)
else
table.insert(result_row, 1)
end
end
table.insert(result, result_row)
end
return result
end
-- This function returns an array of tables with two parameters: type and pos. openSet = {}
-- The position parameter is the actual coordinate on the Minetest map. The closedSet = {}
-- type is the type of the node at the coordinate defined as pathfinder.node_types.
function pathfinder.get_path(map, path_nodes) local h_start = get_distance(pos, endpos)
local result = {} openSet[start_index] = {hCost = h_start, gCost = 0, fCost = h_start, parent = nil, pos = pos}
for node, count in path_nodes do
table.insert(result, map[node:getY()][node:getX()]) -- Entity values
-- For debug local entity_height = math.ceil(entity.collisionbox[5] - entity.collisionbox[2])
--minetest.log("Node: "..dump(map[node:getY()][node:getX()])) local entity_fear_height = entity.fear_height or 2
--print(('Step: %d - x: %d - y: %d'):format(count, node:getX(), node:getY())) local entity_jump_height = entity.jump_height or 1
end
return result repeat
local current_index
local current_values
-- Get one index as reference from openSet
for i, v in pairs(openSet) do
current_index = i
current_values = v
break
end
-- Search for lowest fCost
for i, v in pairs(openSet) do
if v.fCost < openSet[current_index].fCost or v.fCost == current_values.fCost and v.hCost < current_values.hCost then
current_index = i
current_values = v
end
end
openSet[current_index] = nil
closedSet[current_index] = current_values
count = count - 1
if current_index == target_index then
-- print("Success")
local path = {}
local reverse_path = {}
repeat
if not closedSet[current_index] then
return
end
table.insert(path, closedSet[current_index].pos)
current_index = closedSet[current_index].parent
if #path > 100 then
-- print("path to long")
return
end
until start_index == current_index
repeat
table.insert(reverse_path, table.remove(path))
until #path == 0
-- print("path lenght: "..#reverse_path)
return reverse_path
end
local current_pos = current_values.pos
local neighbors = {}
local neighbors_index = 1
for z = -1, 1 do
for x = -1, 1 do
local neighbor_pos = {x = current_pos.x + x, y = current_pos.y, z = current_pos.z + z}
local neighbor = minetest.get_node(neighbor_pos)
local neighbor_ground_level = get_neighbor_ground_level(neighbor_pos, entity_jump_height, entity_fear_height)
local neighbor_clearance = false
if neighbor_ground_level then
-- print(neighbor_ground_level.y - current_pos.y)
--minetest.set_node(neighbor_ground_level, {name = "default:dry_shrub"})
local node_above_head = minetest.get_node(
{x = current_pos.x, y = current_pos.y + entity_height, z = current_pos.z})
if neighbor_ground_level.y - current_pos.y > 0 and not walkable(node_above_head) then
local height = -1
repeat
height = height + 1
local node = minetest.get_node(
{x = neighbor_ground_level.x,
y = neighbor_ground_level.y + height,
z = neighbor_ground_level.z})
until walkable(node) or height > entity_height
if height >= entity_height then
neighbor_clearance = true
end
elseif neighbor_ground_level.y - current_pos.y > 0 and walkable(node_above_head) then
neighbors[neighbors_index] = {
hash = nil,
pos = nil,
clear = nil,
walkable = nil,
}
else
local height = -1
repeat
height = height + 1
local node = minetest.get_node(
{x = neighbor_ground_level.x,
y = current_pos.y + height,
z = neighbor_ground_level.z})
until walkable(node) or height > entity_height
if height >= entity_height then
neighbor_clearance = true
end
end
neighbors[neighbors_index] = {
hash = minetest.hash_node_position(neighbor_ground_level),
pos = neighbor_ground_level,
clear = neighbor_clearance,
walkable = walkable(neighbor),
}
else
neighbors[neighbors_index] = {
hash = nil,
pos = nil,
clear = nil,
walkable = nil,
}
end
neighbors_index = neighbors_index + 1
end
end
for id, neighbor in pairs(neighbors) do
-- don't cut corners
local cut_corner = false
if id == 1 then
if not neighbors[id + 1].clear or not neighbors[id + 3].clear
or neighbors[id + 1].walkable or neighbors[id + 3].walkable then
cut_corner = true
end
elseif id == 3 then
if not neighbors[id - 1].clear or not neighbors[id + 3].clear
or neighbors[id - 1].walkable or neighbors[id + 3].walkable then
cut_corner = true
end
elseif id == 7 then
if not neighbors[id + 1].clear or not neighbors[id - 3].clear
or neighbors[id + 1].walkable or neighbors[id - 3].walkable then
cut_corner = true
end
elseif id == 9 then
if not neighbors[id - 1].clear or not neighbors[id - 3].clear
or neighbors[id - 1].walkable or neighbors[id - 3].walkable then
cut_corner = true
end
end
if neighbor.hash ~= current_index and not closedSet[neighbor.hash] and neighbor.clear and not cut_corner then
local move_cost_to_neighbor = current_values.gCost + get_distance_to_neighbor(current_values.pos, neighbor.pos)
local gCost = 0
if openSet[neighbor.hash] then
gCost = openSet[neighbor.hash].gCost
end
if move_cost_to_neighbor < gCost or not openSet[neighbor.hash] then
if not openSet[neighbor.hash] then
count = count + 1
end
local hCost = get_distance(neighbor.pos, endpos)
openSet[neighbor.hash] = {
gCost = move_cost_to_neighbor,
hCost = hCost,
fCost = move_cost_to_neighbor + hCost,
parent = current_index,
pos = neighbor.pos
}
end
end
end
if count > 100 then
-- print("fail")
return
end
until count < 1
-- print("count < 1")
return {pos}
end end

File diff suppressed because it is too large Load Diff

127
data/dialogues_data.lua Normal file
View File

@ -0,0 +1,127 @@
-- Phase 1 dialogues, unisex
npc.dialogue.register_dialogue({
text = "Hello there!",
tags = {"unisex", "phase1"}
})
npc.dialogue.register_dialogue({
text = "How are you doing?",
tags = {"unisex", "phase1"}
})
npc.dialogue.register_dialogue({
text = "Just living another day...",
tags = {"unisex", "phase1"}
})
-- Phase 1 dialogues, female
npc.dialogue.register_dialogue({
text = "Is there any woman in this area more beautiful than I am?",
tags = {"female", "phase1"}
})
npc.dialogue.register_dialogue({
text = "Hello! Have you been to the sea?",
tags = {"female", "phase1"},
responses = {
[1] = {
text = "No, never before",
action_type = "function",
action = function(self, player)
minetest.chat_send_player(player:get_player_name(), "Oh, never? How come! You should."..
"\nHere, take this. It will guide you to the sea...")
end
},
[2] = {
text = "Yes, sure",
action_type = "dialogue",
action = {
text = "It's so beautiful, and big, and large, and infinite, and..."
}
},
[3] = {
text = "Of course! And to all the seas in the world!",
action_type = "dialogue",
action = {
text = "Awww you are no fun then! Go on then know-it-all!"
}
}
}
})
npc.dialogue.register_dialogue({
text = "Hello there, could you help me?",
tags = {"phase1", "female"},
flag = {name="received_money_help", value=false},
responses = {
[1] = {
text = "Yes, how can I help?",
action_type = "dialogue",
action = {
text = "Could you please give me 3 "..npc.trade.prices.currency.tier3.name.."?",
responses = {
[1] = {
text = "Yes, ok, here",
action_type = "function",
action = function(self, player)
-- Take item
if npc.actions.execute(self, npc.actions.cmd.TAKE_ITEM, {
player=player:get_player_name(),
pos=nil,
inv_list="main",
item_name=npc.trade.prices.currency.tier3.string,
count=3
}) then
-- Send message
npc.chat(self.npc_name, player:get_player_name(), "Thank you, thank you so much!")
-- Set flag
npc.add_flag(self, "received_money_help", true)
-- Add chat line
--table.insert(self.dialogues.normal, npc.data.DIALOGUES.female["phase1"][8])
else
npc.chat(self.npc_name, player:get_player_name(), "Looks like you don't have that amount of money...")
end
end
},
[2] = {
text = "No, I'm sorry",
action_type = "dialogue",
action = {
text = "Oh..."
}
}
}
}
},
[2] = {
text = "No, I'm sorry, can't now",
action_type = "function",
action = function(self, player)
npc.chat(self.npc_name, player:get_player_name(), "Oh, ok...")
end
}
}
})
npc.dialogue.register_dialogue({
text = "Thank you so much for your help, thank you!",
flag = {name="received_money_help", value=true},
tags = {"phase1", "female"}
})
-- Phase 1 dialogues, male
npc.dialogue.register_dialogue({
text = "Hunting is the best pasttime!",
tags = {"male", "phase1"}
})
npc.dialogue.register_dialogue({
text = "I hope my wheat grows well this harvest.",
tags = {"male", "default_farmer"}
})

239
data/gift_items_data.lua Normal file
View File

@ -0,0 +1,239 @@
------------------------------------------------------------------------------
-- Gift Items data definitions
------------------------------------------------------------------------------
------------------------------------------------------------------------------
-- PHASE 1
------------------------------------------------------------------------------
npc.relationships.register_favorite_item("default:apple", "phase1", "female", {
responses = {"Hey, I really wanted an apple, thank you!"},
hints = {"I could really do with an apple..."}
})
npc.relationships.register_favorite_item("farming:bread", "phase1", "female", {
responses = {"Thanks, you didn't have to, but thanks..."},
hints = {"Some fresh bread would be good!"}
})
npc.relationships.register_favorite_item("farming:seed_cotton", "phase1", "female", {
responses = {"Thank you, I will plant this really soon"},
hints = {"I would like to have some cotton plants around"}
})
npc.relationships.register_favorite_item("farming:seed_wheat", "phase1", "female", {
responses = {"Thank you! These seeds will make a good wheat plant!"},
hints = {"I've been thinking I should get wheat seeds"}
})
npc.relationships.register_favorite_item("flowers:rose", "phase1", "female", {
responses = {"Thanks..."},
hints = {"Red roses make a nice gift!"}
})
npc.relationships.register_favorite_item("flowers:geranium", "phase1", "female", {
responses = {"Oh, for me? Thank you!"},
hints = {"Blue geraniums are so beautiful"}
})
npc.relationships.register_favorite_item("default:clay_lump", "phase1", "female", {
responses = {"Thanks! Now, what can I do with this..."},
hints = {"If I had some clay lump, I may do some pottery"}
})
npc.relationships.register_favorite_item("mobs:meat_raw", "phase1", "female", {
responses = {"This will be great for tonight! Thanks"},
hints = {"A good dinner always have meat"}
})
npc.relationships.register_favorite_item("mobs:leather", "phase1", "female", {
responses = {"Thank you! I needed this!"},
hints = {"If only I could get some leather"}
})
npc.relationships.register_favorite_item("default:sapling", "phase1", "female", {
responses = {"Now I can plant that tree..."},
hints = {"I really would like an apple tree close by."}
})
npc.relationships.register_favorite_item("farming:cotton", "phase2", "female", {
responses = {"This is going to be very helpful, thank you!"},
hints = {"If I just had some cotton lying around..."}
})
npc.relationships.register_favorite_item("wool:white", "phase2", "female", {
responses = {"Thanks, you didn't have to, but thanks..."},
hints = {"Have you seen a sheep? I wish I had some white wool..."}
})
npc.relationships.register_favorite_item("default:apple", "phase3", "female", {
responses = {"Hey, I really wanted an apple, thank you!"},
hints = {"I could really do with an apple..."}
})
npc.relationships.register_favorite_item("farming:bread", "phase3", "female", {
responses = {"Thanks, you didn't have to, but thanks..."},
hints = {"Some fresh bread would be good!"}
})
npc.relationships.register_favorite_item("default:apple", "phase4", "female", {
responses = {"Hey, I really wanted an apple, thank you!"},
hints = {"I could really do with an apple..."}
})
npc.relationships.register_favorite_item("farming:bread", "phase4", "female", {
responses = {"Thanks, you didn't have to, but thanks..."},
hints = {"Some fresh bread would be good!"}
})
npc.relationships.register_favorite_item("default:apple", "phase5", "female", {
responses = {"Hey, I really wanted an apple, thank you!"},
hints = {"I could really do with an apple..."}
})
npc.relationships.register_favorite_item("farming:bread", "phase5", "female", {
responses = {"Thanks, you didn't have to, but thanks..."},
hints = {"Some fresh bread would be good!"}
})
npc.relationships.register_favorite_item("default:apple", "phase6", "female", {
responses = {"Hey, I really wanted an apple, thank you!"},
hints = {"I could really do with an apple..."}
})
npc.relationships.register_favorite_item("farming:bread", "phase6", "female", {
responses = {"Thanks, you didn't have to, but thanks..."},
hints = {"Some fresh bread would be good!"}
})
-- Male
npc.relationships.register_favorite_item("default:apple", "phase1", "male", {
responses = {"Good apple, thank you!"},
hints = {"I could do with an apple..."}
})
npc.relationships.register_favorite_item("farming:bread", "phase1", "male", {
responses = {"Thank you! I was hungry."},
hints = {"Some fresh bread would be good!"}
})
npc.relationships.register_favorite_item("farming:seed_cotton", "phase1", "male", {
responses = {"Thank you, I will plant this soon"},
hints = {"I would like to have some cotton plants around."}
})
npc.relationships.register_favorite_item("farming:seed_wheat", "phase1", "male", {
responses = {"Thank you! These seeds will make a good wheat plant!"},
hints = {"I've been thinking I should get wheat seeds."}
})
npc.relationships.register_favorite_item("default:wood", "phase1", "male", {
responses = {"Thanks, I needed this."},
hints = {"Some wood without having to cut a tree would be good.}"}
})
npc.relationships.register_favorite_item("default:tree", "phase1", "male", {
responses = {"Excellent to get that furnace going!"},
hints = {"I'm looking for some logs"}
})
npc.relationships.register_favorite_item("default:clay_lump", "phase1", "male", {
responses = {"Thanks! Now, what can I do with this..."},
hints = {"Now, some clay would be good."}
})
npc.relationships.register_favorite_item("mobs:meat_raw", "phase1", "male", {
responses = {"This makes a great meal. Thank you"},
hints = {"Meat is always great"},
})
npc.relationships.register_favorite_item("mobs:leather", "phase1", "male", {
responses = {"Time to tan some leathers!"},
hints = {"I have been needing leather these days."}
})
npc.relationships.register_favorite_item("default:sapling", "phase1", "male", {
responses = {"Thanks, I will plant this right now"},
hints = {"I really would like an apple tree close by."}
})
npc.relationships.register_favorite_item("default:apple", "phase2", "male", {
responses = {"Good apple, thank you!"},
hints = {"I could do with an apple..."}
})
npc.relationships.register_favorite_item("farming:bread", "phase2", "male", {
responses = {"Thank you! I was hungry."},
hints = {"Some fresh bread would be good!"}
})
npc.relationships.register_favorite_item("default:apple", "phase3", "male", {
responses = {"Good apple, thank you!"},
hints = {"I could do with an apple..."}
})
npc.relationships.register_favorite_item("farming:bread", "phase3", "male", {
responses = {"Thank you! I was hungry."},
hints = {"Some fresh bread would be good!"}
})
npc.relationships.register_favorite_item("default:apple", "phase4", "male", {
responses = {"Good apple, thank you!"},
hints = {"I could do with an apple..."}
})
npc.relationships.register_favorite_item("farming:bread", "phase4", "male", {
responses = {"Thank you! I was hungry."},
hints = {"Some fresh bread would be good!"}
})
npc.relationships.register_favorite_item("default:apple", "phase5", "male", {
responses = {"Good apple, thank you!"},
hints = {"I could do with an apple..."}
})
npc.relationships.register_favorite_item("farming:bread", "phase5", "male", {
responses = {"Thank you! I was hungry."},
hints = {"Some fresh bread would be good!"}
})
npc.relationships.register_favorite_item("default:apple", "phase6", "male", {
responses = {"Good apple, thank you!"},
hints = {"I could do with an apple..."}
})
npc.relationships.register_favorite_item("farming:bread", "phase6", "male", {
responses = {"Thank you! I was hungry."},
hints = {"Some fresh bread would be good!"}
})
-- Disliked items
-- Female
npc.relationships.register_disliked_item("default:stone", "female", {
responses = {"A stone, oh... why do you give this to me?"},
hints = {"Why would someone want a stone?"}
})
npc.relationships.register_disliked_item("default:cobble", "female", {
responses = {"Cobblestone? No, no, why?"},
hints = {"Anything worst than stone is cobblestone."}
})
-- Male
npc.relationships.register_disliked_item("default:stone", "male", {
responses = {"Good apple, thank you!"},
hints = {"I could do with an apple..."}
})
npc.relationships.register_disliked_item("default:cobble", "male", {
responses = {"Cobblestone!? Wow, you sure think a lot before giving a gift..."},
hints = {"If I really hate something, that's cobblestone!"}
})
--npc.log("DEBUG", "Registered gift items: "..dump(npc.relationships.gift_items))
--npc.log("DEBUG", "Registered dialogues: "..dump(npc.dialogue.registered_dialogues))
npc.log("INFO", "Registered gift items count: "..dump(#npc.relationships.gift_items))
npc.log("INFO", "Registered dialogues count: "..dump(#npc.dialogue.registered_dialogues))

0
data/names_data.lua Normal file
View File

View File

@ -0,0 +1,166 @@
----------------------------------------------------
-- Default occupation for Advanced NPC
-- By Zorman2000
----------------------------------------------------
-- The default "occupation" gives some schedule entries to the NPCs
-- which don't have any occupation. The rest is left as randomly
-- initialized.
local basic_def = {
-- Use random textures
textures = {},
-- Use random dialogues
dialogues = {},
-- Initialize inventory with random items
initial_inventory = {},
-- Initialize schedule
schedules_entries = {
-- Schedule entry for 7 in the morning
[7] = {
-- Change trader status to "none"
[1] = {
property = npc.schedule_properties.trader_status,
args = {
status = npc.trade.NONE
}
},
-- 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, disable_rightclick = false}}
},
-- Schedule entry for 8 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_category=npc.places.PLACE_TYPE.CATEGORIES.SITTABLE,
place_type=npc.places.PLACE_TYPE.SITTABLE.PRIMARY,
use_access_node=true,
try_alternative_if_used=true,
mark_target_as_used = true
},
walkable = {"cottages:bench"}
},
chance = 75
},
-- Sit on the node
[2] = {task = npc.actions.cmd.USE_SITTABLE,
args = {
pos = npc.places.PLACE_TYPE.CALCULATED.TARGET,
action = npc.actions.const.sittable.SIT
},
depends = {1}
},
-- Stay put into place
[3] = {
action = npc.actions.cmd.FREEZE, args = {freeze = true},
depends = {2}
}
},
-- Schedule entry for 1 in the afternoon
[13] = {
-- Get up from sit
[1] = {
action = npc.actions.cmd.USE_SITTABLE, args = {
pos = npc.places.PLACE_TYPE.CALCULATED.TARGET,
action = npc.actions.const.sittable.GET_UP
},
},
-- Give NPC money to buy from player
[2] = {
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 "casual trader"
[3] = {
property = npc.schedule_properties.trader_status,
args = {
status = npc.trade.CASUAL
},
chance = 75
},
[4] = {
property = npc.schedule_properties.can_receive_gifts,
args = {
can_receive_gifts = false
},
depends = {1}
},
-- Allow mobs_redo wandering
[5] = {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, disable_rightclick = true}}
}
}
}
-- Register default occupation
npc.occupations.register_occupation(npc.occupations.basic_name, basic_def)

View File

@ -0,0 +1,558 @@
----------------------------------------------------
-- Test farmer occupation for Advanced NPC
-- By Zorman2000
----------------------------------------------------
-- This farmer implementation is still WIP. It is supposed to spawn
-- on buildings that have plots or there are fields nearby. Also, it
-- work on its crops during the morning, and sell some of them on the
-- afternoon.
local farming_plants = {
"farming:cotton_1",
"farming:cotton_2",
"farming:cotton_3",
"farming:cotton_4",
"farming:cotton_5",
"farming:cotton_6",
"farming:cotton_7",
"farming:cotton_8",
"farming:wheat_1",
"farming:wheat_2",
"farming:wheat_3",
"farming:wheat_4",
"farming:wheat_5",
"farming:wheat_6",
"farming:wheat_7",
"farming:wheat_8"
}
local farmer_def = {
dialogues = {},
textures = {},
building_types = {
"farm_tiny", "farm_full"
},
surrounding_building_types = {
{type="field", origin_building_types={"hut", "house", "lumberjack"}}
},
walkable_nodes = farming_plants,
initial_inventory = {
{name="farming:seed_cotton", count=6}
},
schedules_entries = {
[6] = {
-- 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}}
},
[7] = {
[1] =
{
task = npc.actions.cmd.WALK_TO_POS,
args = {
end_pos = npc.places.PLACE_TYPE.WORKPLACE.PRIMARY,
walkable = {}
}
},
[2] =
{
check = true,
range = 2,
random_execution_times = true,
min_count = 20,
max_count = 25,
nodes = farming_plants,
prefer_last_acted_upon_node = true,
walkable_nodes = farming_plants,
actions =
{
-- Actions for cotton - harvest and replant
["farming:cotton_1"] =
{
[1] =
{
task = npc.actions.cmd.WALK_TO_POS,
args = {
end_pos = npc.places.PLACE_TYPE.SCHEDULE.TARGET,
walkable = farming_plants
}
},
[2] =
{
action = npc.actions.cmd.DIG,
args = {
bypass_protection = true
}
},
[3] =
{
action = npc.actions.cmd.STAND,
args = {}
},
[4] =
{
action = npc.actions.cmd.PLACE,
args =
{
node = "farming:cotton_2",
bypass_protection = true
}
},
[5] =
{
action = npc.actions.cmd.STAND,
args = {}
}
},
["farming:cotton_2"] =
{
[1] =
{
task = npc.actions.cmd.WALK_TO_POS,
args = {
end_pos = npc.places.PLACE_TYPE.SCHEDULE.TARGET,
walkable = farming_plants
}
},
[2] =
{
action = npc.actions.cmd.DIG,
args = {
bypass_protection = true
}
},
[3] =
{
action = npc.actions.cmd.STAND,
args = {}
},
[4] =
{
action = npc.actions.cmd.PLACE,
args =
{
node = "farming:cotton_3",
bypass_protection = true
}
},
[5] =
{
action = npc.actions.cmd.STAND,
args = {}
}
},
["farming:cotton_3"] =
{
[1] =
{
task = npc.actions.cmd.WALK_TO_POS,
args = {
end_pos = npc.places.PLACE_TYPE.SCHEDULE.TARGET,
walkable = farming_plants
}
},
[2] =
{
action = npc.actions.cmd.DIG,
args = {
bypass_protection = true
}
},
[3] =
{
action = npc.actions.cmd.STAND,
args = {}
},
[4] =
{
action = npc.actions.cmd.PLACE,
args =
{
node = "farming:cotton_4",
bypass_protection = true
}
},
[5] =
{
action = npc.actions.cmd.STAND,
args = {}
}
},
["farming:cotton_4"] =
{
[1] =
{
task = npc.actions.cmd.WALK_TO_POS,
args = {
end_pos = npc.places.PLACE_TYPE.SCHEDULE.TARGET,
walkable = farming_plants
}
},
[2] =
{
action = npc.actions.cmd.DIG,
args = {
bypass_protection = true
}
},
[3] =
{
action = npc.actions.cmd.STAND,
args = {}
},
[4] =
{
action = npc.actions.cmd.PLACE,
args =
{
node = "farming:cotton_5",
bypass_protection = true
}
},
[5] =
{
action = npc.actions.cmd.STAND,
args = {}
}
},
["farming:cotton_5"] =
{
[1] =
{
task = npc.actions.cmd.WALK_TO_POS,
args = {
end_pos = npc.places.PLACE_TYPE.SCHEDULE.TARGET,
walkable = farming_plants
}
},
[2] =
{
action = npc.actions.cmd.DIG,
args = {
bypass_protection = true
}
},
[3] =
{
action = npc.actions.cmd.STAND,
args = {}
},
[4] =
{
action = npc.actions.cmd.PLACE,
args =
{
node = "farming:cotton_6",
bypass_protection = true
}
},
[5] =
{
action = npc.actions.cmd.STAND,
args = {}
}
},
["farming:cotton_6"] =
{
[1] =
{
task = npc.actions.cmd.WALK_TO_POS,
args = {
end_pos = npc.places.PLACE_TYPE.SCHEDULE.TARGET,
walkable = farming_plants
}
},
[2] =
{
action = npc.actions.cmd.DIG,
args = {
bypass_protection = true
}
},
[3] =
{
action = npc.actions.cmd.STAND,
args = {}
},
[4] =
{
action = npc.actions.cmd.PLACE,
args =
{
node = "farming:cotton_7",
bypass_protection = true
}
},
[5] =
{
action = npc.actions.cmd.STAND,
args = {}
}
},
["farming:cotton_7"] =
{
[1] =
{
task = npc.actions.cmd.WALK_TO_POS,
args = {
end_pos = npc.places.PLACE_TYPE.SCHEDULE.TARGET,
walkable = farming_plants
}
},
[2] =
{
action = npc.actions.cmd.DIG,
args = {
bypass_protection = true
}
},
[3] =
{
action = npc.actions.cmd.STAND,
args = {}
},
[4] =
{
action = npc.actions.cmd.PLACE,
args =
{
node = "farming:cotton_8",
bypass_protection = true
}
},
[5] =
{
action = npc.actions.cmd.STAND,
args = {}
}
},
["farming:cotton_8"] =
{
[1] =
{
task = npc.actions.cmd.WALK_TO_POS,
args = {
end_pos = npc.places.PLACE_TYPE.SCHEDULE.TARGET,
walkable = farming_plants
}
},
[2] =
{
action = npc.actions.cmd.DIG,
args = {
bypass_protection = true
}
},
[3] =
{
action = npc.actions.cmd.STAND,
args = {}
},
[4] =
{
action = npc.actions.cmd.PLACE,
args =
{
node = "farming:cotton_1",
bypass_protection = true
}
},
[5] =
{
action = npc.actions.cmd.STAND,
args = {}
}
},
["farming:wheat_8"] =
{
[1] =
{
task = npc.actions.cmd.WALK_TO_POS,
args = {
end_pos = npc.places.PLACE_TYPE.SCHEDULE.TARGET,
walkable = farming_plants
}
},
[2] =
{
action = npc.actions.cmd.DIG,
args = {
bypass_protection = true
}
},
[3] =
{
action = npc.actions.cmd.STAND,
args = {}
},
[4] =
{
action = npc.actions.cmd.PLACE,
args =
{
node = "farming:wheat_1",
bypass_protection = true
}
},
[5] =
{
action = npc.actions.cmd.STAND,
args = {}
}
}
},
none_actions =
{
-- Walk a single step in a random direction
[1] = {
action = npc.actions.cmd.WALK_STEP,
args =
{
dir = "random_orthogonal"
}
},
[2] = {
action = npc.actions.cmd.STAND,
args = {}
}
}
}
},
[13] = {
-- 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}
}
},
[14] = {
-- 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 = 50
},
-- Set trade list - what NPC will buy and what NPC will sell
[2] = {
property = npc.schedule_properties.set_trade_list, args = {
items = {
[1] = {name="farming:seed_cotton", sell=5, keep=5},
[2] = {name="farming:cotton", sell=10},
[3] = {name="bucket:bucket_empty", buy=2},
[4] = {name="farming:hoe_stone", buy=2}
}
}
},
-- Change trader status to "trader"
[3] = {
property = npc.schedule_properties.trader_status, args = {
status = npc.trade.TRADER
},
chance = 90
},
[4] = {
property = npc.schedule_properties.can_receive_gifts, args = {
can_receive_gifts = false
},
depends = {1}
},
-- Allow mobs_redo wandering
[5] = {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.BED.PRIMARY,
walkable = {}
}
},
-- Allow mobs_redo wandering
[4] = {action = npc.actions.cmd.FREEZE, args = {freeze = false}}
},
[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} }
}
}
}
-- Register occupation
npc.occupations.register_occupation("default_farmer", farmer_def)

View File

@ -0,0 +1,51 @@
-- WIP miner by NewbProgrammer101 or roboto
local miner_def = {
dialogues = {},
textures = {"miner.png"},
initial_inventory = {
{name="default:pick_steel", chance=1},
{name="default:shovel_bronze", chance=1}
},
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 = 3,
random_execution_times = true,
min_count = 20,
max_count = 99,
nodes = {"default:dirt", "default:dirt_with_grass", "default:sand", "default:desert_sand", "default:silver_sand", "default:gravel", "default:clay", "default:snow", "default:snowblock"},
actions = {
["default:dirt"] = {
[1] = {
action = npc.actions.cmd.WALK_STEP
},
[2] = {
action = npc.actions.cmd.DIG
}
}
}
},
none_actions = {
[1] = {
action = npc.actions.cmd.WALK_STEP,
args = {
dir = "random"
}
}
}
}
}
}
-- Occupation registration
npc.occupations.register_occupation("default_miner", miner_def)

View File

@ -0,0 +1,215 @@
----------------------------------------------------
-- Basic priest occupation for Advanced NPC (WIP)
-- By Zorman2000
----------------------------------------------------
-- The basic priest occupation is given to NPCs that spawn on houses
-- surrounding churchs. While on the church, the priest provides
-- universal wisdom and advice, and also heals the player a limited number of times.
-- DISCLAIMER: The "teachings" in this file come from a compilation of 15 principles shared
-- among religions around the world. Zorman2000 and other contributors are not
-- necessarily aligned with the principles and morals in these teachings, nor affiliated
-- to religions that promote them.
local priest_def = {
dialogues = {
type = "given",
max_count = 5,
data = {
{
text = "Blessings be upon you, my child!",
tags = {"unisex"}
},
{
text = "The temple will always open the doors to everyone.",
flag = {name="on_church", value=true},
tags = {"unisex"}
},
{
text = "Following the teachings is the path to a good life.",
tags = {"unisex"}
},
{
text = "Thanks for coming to greet me, I hope you have a blessed day! ",
flag = {name="on_church", value=false},
tags = {"unisex"}
},
{
text = "Welcome to the temple, how can I help you today?",
flag = {name="on_church", value=true},
tags = {"unisex"},
responses =
{
[1] = {
text = "I'm injured. Can you heal me?",
action_type = "function",
action = function(self, player)
local heal_count = self.flags["heal_count"]
if heal_count then
-- Increase heal count
self.flags["heal_count"] = self.flags["heal_count"] + 1
else
self.flags["heal_count"] = 1
heal_count = 1
end
-- Check if heal count is achieved
if heal_count > 5 then
npc.chat(self.npc_name, player:get_player_name(), "I cannot heal you anymore, "
.."my child.\nTo mortals like you and me, the power of the Creator is\n"
.." limited. Only though learning the teachings we are able to understand more"
.."...\nBe safe my child.")
else
npc.chat(self.npc_name, player:get_player_name(),
"Receive the blessings of the Creator!")
npc.effect(self.object:getpos(), 20, "default_coral_skeleton.png", 0.1, 0.3, 3, 10)
-- Heal one heart
player:set_hp(player:get_hp() + 2)
end
end
},
[2] = {
text = "What are your teachings?",
action_type = "function",
action = function(self, player)
local teachings = {
[1] = "Do unto others what you would have them do unto you",
[2] = "Honor your Father and Mother. Knowing them is the key to knowing ourselves",
[3] = "Sincerity is the way to heaven,\nand to think how to be sincere is the way of the man",
[4] = "Generosity, charity and kindness will open an individual to an unbounded reservoir of riches",
[5] = "Even as the scent dwells within the flower, so God within thine own heart forever abides",
[6] = "Acts of faith, prayer and meditation provide us with the strength that allows love for our fellow man to become an abiding force. Love is unifying.",
[7] = "Peacemakers are blessed.\nPeace is the natural result of individuals and nations living in close kinship",
[8] = "You reap what you sow.\nEven if it is a mystery, we are all ruled by this inevitable law of nature",
[9] = "The blessings of life are deeper than what can be appreciated by the senses",
[10] = "Do no harm, as we are part of the whole, and shouldn't perceive others as foreign or separate from ownself",
[11] = "The most beautiful thing a man can do is to forgive wrong",
[12] = "Judge not, lest ye be judged. Mankind is nothing but a great family and we all spring from common source",
[13] = "Anger clouds the mind in the very moments that clarity and objectivity are needed most.",
[14] = "Nature, Being, The Absolute, Creator... whatever name man chooses, there is but one force in the universe. All people and things are of one essence",
[15] = "Study the words, no doubt, but look behind them to the thought they indicate;\nhaving fond it, throw the words away. Live the spirit of them",
[16] = "The wise store up choice food and olive oil, \nbut fools gulp theirs down.",
[17] = "An inheritance claimed too soon \nwill not be blessed at the end.",
[18] = "Young men give glory in their strength, \nbut old men are honored for their gray hair.",
[19] = "Humility is the fear of the Creator, or whatever name man chooses; \nits wages are riches and honor in life.",
[20] = "Listen, my child, and be wise, \nand set your heart on the right path.",
[21] = "Do not speak to fools, \nfor they will scorn your prudent words.",
[22] = "The schemes of folly are sin, \nand people detest a mocker.",
[23] = "An honest answer is like a kiss on the lips.",
[24] = "Do not envy the wicked, \ndo not desire their company; \nfor their hearts plot violence, \nand their lips talk about making trouble.",
[25] = "Do not fret because of evildoers, for the evildoer has no future hope.",
[26] = "It is to one's honor to avoid strife, \nbut every fool is quick to quarrel",
[27] = "Kill reverence, and you've killed the hero in man.",
[28] = "Kill man's sense of value, kill his capacity to recognize greatness or to achieve it, \nand you've killed his will to live.",
[29] = "The true hater of man, expects nothing from him and is indiscriminate to his works.",
[30] = "Love is a tool for capturing the souls of men. Pretend to love, and he will accept you. \nLove is reverence, and worship, and glory, and the upward glance. Not a bandage for dirty sores. \nBut he doesnt know it. Those who speak of love most promiscuously are the ones whove never felt it. \nThey make some sort of feeble stew out of sympathy, compassion, contempt and general indifference, and they call it love. \nOnce youve felt what it means to love, the total passion for the total height—youre incapable of anything less.",
[31] = "If you learn how to rule one single mans soul, you can get the rest of mankind. \nIts the soul, not whips or swords or fire or guns. \nThats why the Caesars, the Attilas, the Napoleons were fools and did not last. \nThe soul, is that which cant be ruled. It must be broken. \nDrive a wedge in, get your fingers on it—and the man is yours.",
[32] = "Great and wise men cant be ruled."
}
npc.chat(self.npc_name, player:get_player_name(), teachings[math.random(1, #teachings)]
..". \nThese are the teachings of our Creator.")
end
}
}
}
}
},
textures = {
"npc_male_priest.png"
},
initial_inventory = {
{name="farming:bread", count=1}
},
properties = {
initial_trader_status = npc.trade.NONE,
enable_gift_items_hints = false,
can_receive_gifts = false
},
building_types = {},
surrounding_building_types = {
{type="church", origin_building_types={"hut", "house", "farm_tiny", "lumberjack"}}
},
schedules_entries = {
[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,
chance = 95,
args = {
end_pos = npc.places.PLACE_TYPE.OTHER.HOME_INSIDE,
walkable = {}
}
},
-- Allow mobs_redo wandering
[3] = {action = npc.actions.cmd.FREEZE, args = {freeze = false}}
},
[8] = {
-- Walk to workplace
[1] =
{
task = npc.actions.cmd.WALK_TO_POS,
args = {
end_pos = npc.places.PLACE_TYPE.WORKPLACE.PRIMARY,
walkable = {},
use_access_node = true
}
},
[2] =
{
property = npc.schedule_properties.flag,
args = {
action = "set",
flag_name = "on_church",
flag_value = true
}
}
},
[17] = {
[1] =
{
property = npc.schedule_properties.flag,
args = {
action = "set",
flag_name = "on_church",
flag_value = false
}
},
[2] =
{
task = npc.actions.cmd.WALK_TO_POS,
args = {
end_pos = npc.places.PLACE_TYPE.OTHER.HOME_INSIDE,
walkable = {}
}
}
},
[21] = {
[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}}
}
}
}
-- Register occupation
npc.occupations.register_occupation("default_priest", priest_def)

411240
debug.txt

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
default default
mobs mobs
intllib? mg_villages?
pathfinder intllib?

View File

@ -1 +1 @@
Adds simple NPC and Trader. Adds NPCs which are smart, have homes, can talk, trade and even establish friendships and more with you!

View File

@ -1,20 +1,6 @@
-------------------------------------------------------------------------------------
-- NPC dialogue code by Zorman2000 -- NPC dialogue code by Zorman2000
-- Dialogue definitions: -------------------------------------------------------------------------------------
-- TODO: Complete
-- {
-- text: "",
-- ^ The "spoken" dialogue line
-- flag:
-- ^ If the flag with the specified name has the specified value
-- then this dialogue is valid
-- {
-- name: ""
-- ^ Name of the flag
-- value:
-- ^ Expected value of the flag. A flag can be a function. In such a case, it is
-- expected the function will return this value.
-- }
-- }
npc.dialogue = {} npc.dialogue = {}
@ -25,10 +11,10 @@ npc.dialogue.MIN_DIALOGUES = 2
npc.dialogue.MAX_DIALOGUES = 4 npc.dialogue.MAX_DIALOGUES = 4
npc.dialogue.dialogue_type = { npc.dialogue.dialogue_type = {
married = 1, married = 1,
casual_trade = 2, casual_trade = 2,
dedicated_trade = 3, dedicated_trade = 3,
custom_trade = 4 custom_trade = 4
} }
-- This table contains the answers of dialogue boxes -- This table contains the answers of dialogue boxes
@ -37,19 +23,220 @@ npc.dialogue.dialogue_results = {
yes_no_dialogue = {} yes_no_dialogue = {}
} }
--------------------------------------------------------------------------------------- npc.dialogue.tags = {
UNISEX = "unisex",
MALE = "male",
FEMALE = "female",
-- Relationship based tags - these are one-to-one with the
-- phase names.
DEFAULT_MARRIED_DIALOGUE = "default_married_dialogue",
PHASE_1 = "phase1",
PHASE_2 = "phase2",
PHASE_3 = "phase3",
PHASE_4 = "phase4",
PHASE_5 = "phase5",
GIFT_ITEM_HINT = "gift_item_hint",
GIFT_ITEM_RESPONSE = "gift_item_response",
GIFT_ITEM_LIKED = "gift_item_liked",
GIFT_ITEM_UNLIKED = "gift_item_unliked",
-- Trade-related tags
DEFAULT_CASUAL_TRADE = "default_casual_trade_dialogue",
DEFAULT_DEDICATED_TRADE = "default_dedicated_trade_dialogue",
DEFAULT_BUY_OFFER = "buy_offer",
DEFAULT_SELL_OFFER = "sell_offer",
-- Occupation-based tags - these are one-to-one with the
-- default occupation names
BASIC = "basic", -- Dialogues related to the basic occupation should
-- use this. As basic occupation is generic, any occupation
-- should be able to use these dialogues.
DEFAULT_FARMER = "default_farmer",
DEFAULT_COOKER = "default_cooker"
}
-- This table will contain all the registered dialogues for NPCs
npc.dialogue.registered_dialogues = {}
npc.dialogue.cache_keys = {
CASUAL_BUY_DIALOGUE = {key="CASUAL_BUY_DIALOGUE", tags={npc.dialogue.tags.DEFAULT_CASUAL_TRADE, npc.dialogue.tags.DEFAULT_BUY_OFFER}},
CASUAL_SELL_DIALOGUE = {key="CASUAL_SELL_DIALOGUE", tags={npc.dialogue.tags.DEFAULT_CASUAL_TRADE, npc.dialogue.tags.DEFAULT_SELL_OFFER}},
DEDICATED_TRADER_DIALOGUE = {key="DEDICATED_TRADER_DIALOGUE", tags={npc.dialogue.tags.DEFAULT_DEDICATED_TRADE}},
MARRIED_DIALOGUE = {key="MARRIED_DIALOGUE", tags={npc.dialogue.tags.DEFAULT_MARRIED_DIALOGUE}},
}
npc.dialogue.cache = {}
--------------------------------------------------------------------------------------
-- Dialogue registration functions
-- All dialogues will be registered by providing a definition.
-- A unique key will be assigned to them. The dialogue definition is the following:
-- {
-- text: "",
-- ^ The "spoken" dialogue line
-- flag:
-- ^ If the flag with the specified name has the specified value
-- then this dialogue is valid
-- {
-- name: ""
-- ^ Name of the flag
-- value:
-- ^ Expected value of the flag. A flag can be a function. In such a case, it is
-- expected the function will return this value.
-- },
-- tags = {
-- -- Tags are an array of string that allow to classify dialogues
-- -- A dialogue can have as many tags as desired and can take any form.
-- -- However, for consistency, some predefined tags can be found at
-- -- npc.dialogue.tags.
-- -- Example:
-- "phase1",
-- "any"
-- }
-- responses = {
-- -- Array of responses the player can choose. A response can be of
-- -- two types: as [1] or as [2] (see example below)
-- [1] = {
-- text = "Yes",
-- -- Text displayed to the player
-- action_type = "dialogue",
-- -- Type of action that happens when the player chooses this response.
-- -- can be "dialogue" or "function". This example shows "dialogue"
-- action = {
-- text = "It's so beautiful, and big, and large, and infinite, and..."
-- },
-- },
-- -- A table containing a dialogue. This means you can include not only
-- -- text but also flag and responses as well. Dialogues are recursive.
-- [2] = {
-- text = "No",
-- action_type = "function",
-- action = function(self, player)
-- -- A function will have access to self, which is the NPC
-- -- and the player, which is the player ObjectRef. You can
-- -- pretty much do anything here. The example here is very simple,
-- -- just sending a chat message. But you can add items to players
-- -- or to NPCs and so on.
-- minetest.chat_send_player(player:get_player_name(), "Oh, ok...")
-- end,
-- },
-- }
-- }
--------------------------------------------------------------------------------------
-- This function sets a unique response ID (made of <depth>:<response index>) to
-- each response that features a function. This is to be able to locate the
-- function easily later
local function set_response_ids_recursively(dialogue, depth, dialogue_id)
-- Base case: dialogue object with no responses and no responses below it
if dialogue.responses == nil
and (dialogue.action_type == "dialogue" and dialogue.action.responses == nil) then
return
elseif dialogue.responses ~= nil then
-- Assign a response ID to each response
local response_id_prefix = tostring(depth)..":"
for key,value in ipairs(dialogue.responses) do
if value.action_type == "function" then
value.response_id = response_id_prefix..key
value.dialogue_id = dialogue_id
else
-- We have a dialogue action type. Need to check if dialogue has further responses
if value.action.responses ~= nil then
set_response_ids_recursively(value.action, depth + 1, dialogue_id)
end
end
end
end
end
-- The register dialogue function will just receive the definition as
-- explained above. The unique key will be the index it gets into the
-- array when inserted.
function npc.dialogue.register_dialogue(def)
-- If def has not tags then apply the default ones
if not def.tags then
def.tags = {npc.dialogue.tags.UNISEX, npc.dialogue.tags.PHASE_1}
end
local dialogue_id = table.getn(npc.dialogue.registered_dialogues) + 1
-- Set the response IDs - required for dialogue objects that
-- form trees of dialogues
set_response_ids_recursively(def, 0, dialogue_id)
def.key = dialogue_id
-- Insert dialogue into table
table.insert(npc.dialogue.registered_dialogues, def)
return dialogue_id
end
-- This function returns a table of dialogues that meet the given
-- tags array. The keys in the table are the keys in
-- npc.dialogue.registered_dialogues, therefore you can use them to
--retrieve specific dialogues. However, it should be stored by the NPC.
function npc.dialogue.search_dialogue_by_tags(tags, find_all)
--minetest.log("Tags being searched: "..dump(tags))
local result = {}
for key, def in pairs(npc.dialogue.registered_dialogues) do
-- Check if def.tags have any of the provided tags
local tags_found = 0
--minetest.log("Tags on dialogue def: "..dump(def.tags))
for i = 1, #tags do
if npc.utils.array_contains(def.tags, tags[i]) then
tags_found = tags_found + 1
end
end
--minetest.log("Tags found: "..dump(tags_found))
-- Check if we found all tags
if find_all then
if tags_found == #tags then
-- Add result
result[key] = def
end
elseif not find_all then
if tags_found == #tags or tags_found == #def.tags then
-- Add result
result[key] = def
end
end
end
return result
end
function npc.dialogue.get_cached_dialogue_key(_cache_key, tags)
local cache_key = _cache_key
if type(_cache_key) == "table" then
cache_key = _cache_key.key
tags = _cache_key.tags
end
local key = npc.dialogue.cache[cache_key]
-- Check if key isn't cached
if not key then
-- Search for the dialogue
local dialogues = npc.dialogue.search_dialogue_by_tags(tags, true)
key = npc.utils.get_map_keys(dialogues)[1]
-- Populate cache
npc.dialogue.cache[cache_key] = key
-- Return key
return key
else
-- Return the cached key
return key
end
end
--------------------------------------------------------------------------------------
-- Dialogue box definitions -- Dialogue box definitions
-- The dialogue boxes are used for the player to interact with the -- The dialogue boxes are used for the player to interact with the
-- NPC in dialogues. -- NPC in dialogues.
--------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------
-- Creates and shows a multi-option dialogue based on the number of responses -- Creates and shows a multi-option dialogue based on the number of responses
-- that the dialogue object contains -- that the dialogue object contains
function npc.dialogue.show_options_dialogue(self, function npc.dialogue.show_options_dialogue(self,
dialogue, dialogue_key,
dismiss_option_label, dialogue,
player_name) dismiss_option_label,
local responses = dialogue.responses player_name)
local options_length = table.getn(responses) + 1 local responses = dialogue.responses
local options_length = table.getn(responses) + 1
local formspec_height = (options_length * 0.7) + 0.4 local formspec_height = (options_length * 0.7) + 0.4
local formspec = "size[7,"..tostring(formspec_height).."]" local formspec = "size[7,"..tostring(formspec_height).."]"
@ -59,18 +246,20 @@ function npc.dialogue.show_options_dialogue(self,
y = (0.75 * i) y = (0.75 * i)
end end
formspec = formspec.."button_exit[0.5," formspec = formspec.."button_exit[0.5,"
..(y - 0.5)..";6,0.5;opt"..tostring(i)..";"..responses[i].text.."]" ..(y - 0.5)..";6,0.5;opt"..tostring(i)..";"..responses[i].text.."]"
end end
formspec = formspec.."button_exit[0.5," formspec = formspec.."button_exit[0.5,"
..(formspec_height - 0.7)..";6,0.5;exit;"..dismiss_option_label.."]" ..(formspec_height - 0.7)..";6,0.5;exit;"..dismiss_option_label.."]"
-- Create entry on options_dialogue table -- Create entry on options_dialogue table
npc.dialogue.dialogue_results.options_dialogue[player_name] = { npc.dialogue.dialogue_results.options_dialogue[player_name] = {
npc = self, npc = self,
is_married_dialogue = (dialogue.dialogue_type == npc.dialogue.dialogue_type.married), dialogue = dialogue,
is_casual_trade_dialogue = (dialogue.dialogue_type == npc.dialogue.dialogue_type.casual_trade), dialogue_key = dialogue_key,
is_dedicated_trade_dialogue = (dialogue.dialogue_type == npc.dialogue.dialogue_type.dedicated_trade), is_married_dialogue =
is_custom_trade_dialogue = (dialogue.dialogue_type == npc.dialogue.dialogue_type.custom_trade), (dialogue.dialogue_type == npc.dialogue.dialogue_type.married),
is_custom_trade_dialogue =
(dialogue.dialogue_type == npc.dialogue.dialogue_type.custom_trade),
casual_trade_type = dialogue.casual_trade_type, casual_trade_type = dialogue.casual_trade_type,
options = responses options = responses
} }
@ -80,23 +269,23 @@ end
-- This function is used for showing a yes/no dialogue formspec -- This function is used for showing a yes/no dialogue formspec
function npc.dialogue.show_yes_no_dialogue(self, function npc.dialogue.show_yes_no_dialogue(self,
prompt, prompt,
positive_answer_label, positive_answer_label,
positive_callback, positive_callback,
negative_answer_label, negative_answer_label,
negative_callback, negative_callback,
player_name) player_name)
npc.lock_actions(self) npc.lock_actions(self)
local formspec = "size[7,3]".. local formspec = "size[7,3]"..
"label[0.5,0.1;"..prompt.."]".. "label[0.5,0.1;"..prompt.."]"..
"button_exit[0.5,1.15;6,0.5;yes_option;"..positive_answer_label.."]".. "button_exit[0.5,1.15;6,0.5;yes_option;"..positive_answer_label.."]"..
"button_exit[0.5,1.95;6,0.5;no_option;"..negative_answer_label.."]" "button_exit[0.5,1.95;6,0.5;no_option;"..negative_answer_label.."]"
-- Create entry into responses table -- Create entry into responses table
npc.dialogue.dialogue_results.yes_no_dialogue[player_name] = { npc.dialogue.dialogue_results.yes_no_dialogue[player_name] = {
npc = self, npc = self,
yes_callback = positive_callback, yes_callback = positive_callback,
no_callback = negative_callback no_callback = negative_callback
} }
@ -104,102 +293,97 @@ function npc.dialogue.show_yes_no_dialogue(self,
minetest.show_formspec(player_name, "advanced_npc:yes_no", formspec) minetest.show_formspec(player_name, "advanced_npc:yes_no", formspec)
end end
--------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------
-- Dialogue methods -- Dialogue methods
--------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------
-- This function sets a unique response ID (made of <depth>:<response index>) to
-- each response that features a function. This is to be able to locate the
-- function easily later
local function set_response_ids_recursively(dialogue, depth, dialogue_id)
-- Base case: dialogue object with no responses and no r,esponses below it
if dialogue.responses == nil
and (dialogue.action_type == "dialogue" and dialogue.action.responses == nil) then
return
elseif dialogue.responses ~= nil then
-- Assign a response ID to each response
local response_id_prefix = tostring(depth)..":"
for key,value in ipairs(dialogue.responses) do
if value.action_type == "function" then
value.response_id = response_id_prefix..key
value.dialogue_id = dialogue_id
else
-- We have a dialogue action type. Need to check if dialogue has further responses
if value.action.responses ~= nil then
set_response_ids_recursively(value.action, depth + 1, dialogue_id)
end
end
end
end
end
-- Select random dialogue objects for an NPC based on sex -- Select random dialogue objects for an NPC based on sex
-- and the relationship phase with player -- and the relationship phase with player
function npc.dialogue.select_random_dialogues_for_npc(sex, phase, favorite_items, disliked_items) function npc.dialogue.select_random_dialogues_for_npc(self, phase)
local result = { local result = {
normal = {}, normal = {},
hints = {} hints = {}
} }
local dialogues = npc.data.DIALOGUES.female local phase_tag = "phase1"
if sex == npc.MALE then if phase then
dialogues = npc.data.DIALOGUES.male phase_tag = phase
end end
dialogues = dialogues[phase]
local search_tags = {
"unisex",
self.sex,
phase_tag,
self.occupation
}
local dialogues = npc.dialogue.search_dialogue_by_tags(search_tags)
local keys = npc.utils.get_map_keys(dialogues)
-- Determine how many dialogue lines the NPC will have -- Determine how many dialogue lines the NPC will have
local number_of_dialogues = math.random(npc.dialogue.MIN_DIALOGUES, npc.dialogue.MAX_DIALOGUES) local number_of_dialogues = math.random(npc.dialogue.MIN_DIALOGUES, npc.dialogue.MAX_DIALOGUES)
for i = 1,number_of_dialogues do for i = 1, number_of_dialogues do
local dialogue_id = math.random(1, #dialogues) local key_id = math.random(1, #keys)
result.normal[i] = dialogues[dialogue_id] result.normal[i] = keys[key_id]
--npc.log("DEBUG", "Adding dialogue: "..dump(dialogues[keys[key_id]]))
set_response_ids_recursively(result.normal[i], 0, dialogue_id)
end end
-- Add item hints. -- Add item hints.
-- Favorite items
for i = 1, 2 do for i = 1, 2 do
result.hints[i] = {} local hints = npc.relationships.get_dialogues_for_gift_item(
result.hints[i].text = self.gift_data.favorite_items["fav"..tostring(i)],
npc.relationships.get_hint_for_favorite_item(favorite_items["fav"..tostring(i)], sex, phase) npc.dialogue.tags.GIFT_ITEM_HINT,
npc.dialogue.tags.GIFT_ITEM_LIKED,
self.sex,
phase_tag)
for key, value in pairs(hints) do
result.hints[i] = key
end
end end
-- Disliked items
for i = 3, 4 do for i = 3, 4 do
result.hints[i] = {} local hints = npc.relationships.get_dialogues_for_gift_item(
result.hints[i].text = self.gift_data.disliked_items["dis"..tostring(i-2)],
npc.relationships.get_hint_for_disliked_item(disliked_items["dis"..tostring(i-2)], sex) npc.dialogue.tags.GIFT_ITEM_HINT,
npc.dialogue.tags.GIFT_ITEM_UNLIKED,
self.sex)
for key, value in pairs(hints) do
result.hints[i] = key
end
end end
--npc.log("DEBUG", "Dialogue results:"..dump(result))
return result return result
end end
-- This function creates a multi-option dialogue from the custom trades that the -- This function creates a multi-option dialogue from the custom trades that the
-- NPC have. -- NPC have.
function npc.dialogue.create_custom_trade_options(self, player) function npc.dialogue.create_custom_trade_options(self, player)
-- Create the action for each option -- Create the action for each option
local actions = {} local actions = {}
for i = 1, #self.trader_data.custom_trades do for i = 1, #self.trader_data.custom_trades do
table.insert(actions, function() npc.trade.show_custom_trade_offer(self, player, self.trader_data.custom_trades[i]) end) table.insert(actions,
end function()
-- Default text to be shown for dialogue prompt npc.trade.show_custom_trade_offer(self, player, self.trader_data.custom_trades[i])
local text = npc.trade.CUSTOM_TRADES_PROMPT_TEXT end)
-- Get the options from each custom trade entry end
local options = {} -- Default text to be shown for dialogue prompt
if #self.trader_data.custom_trades == 1 then local text = npc.trade.CUSTOM_TRADES_PROMPT_TEXT
table.insert(options, self.trader_data.custom_trades[1].button_prompt) -- Get the options from each custom trade entry
text = self.trader_data.custom_trades[1].option_prompt local options = {}
else if #self.trader_data.custom_trades == 1 then
for i = 1, #self.trader_data.custom_trades do table.insert(options, self.trader_data.custom_trades[1].button_prompt)
table.insert(options, self.trader_data.custom_trades[i].button_prompt) text = self.trader_data.custom_trades[1].option_prompt
end else
end for i = 1, #self.trader_data.custom_trades do
-- Create dialogue object table.insert(options, self.trader_data.custom_trades[i].button_prompt)
local dialogue = npc.dialogue.create_option_dialogue(text, options, actions) end
dialogue.dialogue_type = npc.dialogue.dialogue_type.custom_trade end
-- Create dialogue object
local dialogue = npc.dialogue.create_option_dialogue(text, options, actions)
dialogue.dialogue_type = npc.dialogue.dialogue_type.custom_trade
return dialogue return dialogue
end end
-- This function will choose randomly a dialogue from the NPC data -- This function will choose randomly a dialogue from the NPC data
@ -210,127 +394,147 @@ function npc.dialogue.start_dialogue(self, player, show_married_dialogue)
-- Construct dialogue for marriage -- Construct dialogue for marriage
if npc.relationships.get_relationship_phase(self, player:get_player_name()) == "phase6" if npc.relationships.get_relationship_phase(self, player:get_player_name()) == "phase6"
and show_married_dialogue == true then and show_married_dialogue == true then
dialogue = npc.relationships.MARRIED_NPC_DIALOGUE dialogue = npc.relationships.MARRIED_NPC_DIALOGUE
npc.dialogue.process_dialogue(self, dialogue, player:get_player_name()) npc.dialogue.process_dialogue(self, dialogue, player:get_player_name())
return return
end end
-- Show options dialogue for dedicated trader -- Show options dialogue for dedicated trader
if self.trader_data.trader_status == npc.trade.TRADER then if self.trader_data.trader_status == npc.trade.TRADER then
dialogue = npc.trade.DEDICATED_TRADER_PROMPT dialogue = npc.dialogue.get_cached_dialogue_key(npc.dialogue.cache_keys.DEDICATED_TRADER_DIALOGUE)
npc.dialogue.process_dialogue(self, dialogue, player:get_player_name()) npc.dialogue.process_dialogue(self, dialogue, player:get_player_name())
return return
end end
local chance = math.random(1, 100) local chance = math.random(1, 100)
minetest.log("Chance: "..dump(chance)) --minetest.log("Chance: "..dump(chance))
if chance < 30 then if chance < 30 then
-- If NPC is a casual trader, show a sell or buy dialogue 30% of the time, depending -- Show trading options for casual traders
-- on the state of the casual trader. -- If NPC has custom trading options, these will be
if self.trader_data.trader_status == npc.trade.NONE then -- shown as well with equal chance as the casual
-- Show custom trade options if available -- buy/sell options
if table.getn(self.trader_data.custom_trades) > 0 then if self.trader_data.trader_status == npc.trade.NONE then
-- Show custom trade options -- Show custom trade options if available
dialogue = npc.dialogue.create_custom_trade_options(self, player) if table.getn(self.trader_data.custom_trades) > 0 then
end -- Show custom trade options
elseif self.trader_data.trader_status == npc.trade.CASUAL then dialogue = npc.dialogue.create_custom_trade_options(self, player)
local max_trade_chance = 2 else
if table.getn(self.trader_data.custom_trades) > 0 then -- If not available, choose normal dialogue
max_trade_chance = 3 dialogue = self.dialogues.normal[math.random(1, #self.dialogues.normal)]
end end
-- Show buy/sell with 50% chance each elseif self.trader_data.trader_status == npc.trade.CASUAL then
local trade_chance = math.random(1, max_trade_chance) local max_trade_chance = 2
if trade_chance == 1 then if table.getn(self.trader_data.custom_trades) > 0 then
-- Show casual buy dialogue max_trade_chance = 3
dialogue = npc.trade.CASUAL_TRADE_BUY_DIALOGUE end
elseif trade_chance == 2 then -- Show buy/sell with 50% chance each
-- Show casual sell dialogue local trade_chance = math.random(1, max_trade_chance)
dialogue = npc.trade.CASUAL_TRADE_SELL_DIALOGUE if trade_chance == 1 then
elseif trade_chance == 3 then -- Show casual buy dialogue
-- Show custom trade options dialogue = npc.dialogue.get_cached_dialogue_key(npc.dialogue.cache_keys.CASUAL_BUY_DIALOGUE)
dialogue = npc.dialogue.create_custom_trade_options(self, player) elseif trade_chance == 2 then
end -- Show casual sell dialogue
end dialogue = npc.dialogue.get_cached_dialogue_key(npc.dialogue.cache_keys.CASUAL_SELL_DIALOGUE)
elseif trade_chance == 3 then
-- Show custom trade options
dialogue = npc.dialogue.create_custom_trade_options(self, player)
end
end
elseif chance >= 30 and chance < 90 then elseif chance >= 30 and chance < 90 then
-- Choose a random dialogue from the common ones -- Choose a random dialogue from the common ones
dialogue = self.dialogues.normal[math.random(1, #self.dialogues.normal)] dialogue = self.dialogues.normal[math.random(1, #self.dialogues.normal)]
elseif chance >= 90 then elseif chance >= 90 then
-- Choose a random dialogue line from the favorite/disliked item hints -- Check if gift items hints are enabled
dialogue = self.dialogues.hints[math.random(1, 4)] --minetest.log("Self gift data enable: "..dump(self.gift_data.enable_gift_items_hints))
if self.gift_data.enable_gift_items_hints then
-- Choose a random dialogue line from the favorite/disliked item hints
dialogue = self.dialogues.hints[math.random(1, 4)]
else
-- Choose a random dialogue from the common ones
dialogue = self.dialogues.normal[math.random(1, #self.dialogues.normal)]
end
end end
local dialogue_result = npc.dialogue.process_dialogue(self, dialogue, player:get_player_name()) local dialogue_result = npc.dialogue.process_dialogue(self, dialogue, player:get_player_name())
if dialogue_result == false then if dialogue_result == false then
-- Try to find another dialogue line -- Try to find another dialogue line
npc.dialogue.start_dialogue(self, player, show_married_dialogue) npc.dialogue.start_dialogue(self, player, show_married_dialogue)
end end
end end
-- This function processes a dialogue object and performs -- This function processes a dialogue object and performs
-- actions depending on what is defined in the object -- actions depending on what is defined in the object
function npc.dialogue.process_dialogue(self, dialogue, player_name) function npc.dialogue.process_dialogue(self, dialogue, player_name)
-- Freeze NPC actions
npc.lock_actions(self)
-- Freeze NPC actions local dialogue_key = -1
npc.lock_actions(self)
-- Check if this dialogue has a flag definition if type(dialogue) ~= "table" then
if dialogue.flag then dialogue_key = dialogue
-- Check if the NPC has this flag dialogue = npc.dialogue.registered_dialogues[dialogue]
local flag_value = npc.get_flag(self, dialogue.flag.name) --minetest.log("Found dialogue: "..dump(dialogue))
if flag_value ~= nil then end
-- Check if value of the flag is equal to the expected value
if flag_value ~= dialogue.flag.value then -- Check if this dialogue has a flag definition
-- Do not process this dialogue if dialogue.flag then
return false -- Check if the NPC has this flag
end local flag_value = npc.get_flag(self, dialogue.flag.name)
else if flag_value ~= nil then
-- Check if value of the flag is equal to the expected value
if (type(dialogue.flag.value) == "boolean" and dialogue.flag.value ~= false) if flag_value ~= dialogue.flag.value then
or (type(dialogue.flag.value) == "number" and dialogue.flag.value > 0) then -- Do not process this dialogue
-- Do not process this dialogue return false
return false end
end else
end
end if (type(dialogue.flag.value) == "boolean" and dialogue.flag.value ~= false)
or (type(dialogue.flag.value) == "number" and dialogue.flag.value > 0) then
-- Do not process this dialogue
return false
end
end
end
-- Send dialogue line -- Send dialogue line
if dialogue.text then if dialogue.text then
npc.chat(self.npc_name, player_name, dialogue.text) npc.chat(self.npc_name, player_name, dialogue.text)
end end
-- Check if dialogue has responses. If it doesn't, unlock the actions -- Check if dialogue has responses. If it doesn't, unlock the actions
-- queue and reset actions timer.' -- queue and reset actions timer.'
if not dialogue.responses then if not dialogue.responses then
npc.unlock_actions(self) npc.unlock_actions(self)
end end
-- Check if there are responses, then show multi-option dialogue if there are -- Check if there are responses, then show multi-option dialogue if there are
if dialogue.responses then if dialogue.responses then
npc.dialogue.show_options_dialogue( npc.dialogue.show_options_dialogue(
self, self,
dialogue_key,
dialogue, dialogue,
npc.dialogue.NEGATIVE_ANSWER_LABEL, npc.dialogue.NEGATIVE_ANSWER_LABEL,
player_name player_name
) )
end end
-- Dialogue object processed successfully -- Dialogue object processed successfully
return true return true
end end
function npc.dialogue.create_option_dialogue(prompt, options, actions) function npc.dialogue.create_option_dialogue(prompt, options, actions)
local result = {} local result = {}
result.text = prompt result.text = prompt
result.responses = {} result.responses = {}
for i = 1, #options do for i = 1, #options do
table.insert(result.responses, {text = options[i], action_type="function", action=actions[i]}) table.insert(result.responses, {text = options[i], action_type="function", action=actions[i]})
end end
return result return result
end end
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
-- Functions for rotating NPC to look at player -- Functions for rotating NPC to look at player
-- (taken from the mobs_redo API) -- (taken from the mobs_redo API)
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
local atan = function(x) local atan = function(x)
@ -346,7 +550,7 @@ function npc.dialogue.rotate_npc_to_player(self)
local objs = minetest.get_objects_inside_radius(s, 4) local objs = minetest.get_objects_inside_radius(s, 4)
local lp = nil local lp = nil
local yaw = 0 local yaw = 0
for n = 1, #objs do for n = 1, #objs do
if objs[n]:is_player() then if objs[n]:is_player() then
lp = objs[n]:getpos() lp = objs[n]:getpos()
@ -374,38 +578,38 @@ end
--------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------
-- This function locates a response object that has function on the dialogue tree. -- This function locates a response object that has function on the dialogue tree.
local function get_response_object_by_id_recursive(dialogue, current_depth, response_id) local function get_response_object_by_id_recursive(dialogue, current_depth, response_id)
if dialogue.responses == nil if dialogue.responses == nil
and (dialogue.action_type == "dialogue" and dialoge.action.responses == nil) then and (dialogue.action_type == "dialogue" and dialoge.action.responses == nil) then
return nil return nil
elseif dialogue.responses ~= nil then elseif dialogue.responses ~= nil then
-- Get current depth and response ID -- Get current depth and response ID
local d_i1, d_i2 = string.find(response_id, ":") local d_i1, d_i2 = string.find(response_id, ":")
minetest.log("N1: "..dump(string.sub(response_id, 0, d_i1))..", N2: "..dump(string.sub(response_id, 1, d_i1-1))) --minetest.log("N1: "..dump(string.sub(response_id, 0, d_i1))..", N2: "..dump(string.sub(response_id, 1, d_i1-1)))
local depth = tonumber(string.sub(response_id, 0, d_i1-1)) local depth = tonumber(string.sub(response_id, 0, d_i1-1))
local id = tonumber(string.sub(response_id, d_i2 + 1)) local id = tonumber(string.sub(response_id, d_i2 + 1))
minetest.log("Depth: "..dump(depth)..", id: "..dump(id)) --minetest.log("Depth: "..dump(depth)..", id: "..dump(id))
-- Check each response -- Check each response
for key,value in ipairs(dialogue.responses) do for key,value in ipairs(dialogue.responses) do
minetest.log("Key: "..dump(key)..", value: "..dump(value)..", comp1: "..dump(current_depth == depth)) --minetest.log("Key: "..dump(key)..", value: "..dump(value)..", comp1: "..dump(current_depth == depth))
if value.action_type == "function" then if value.action_type == "function" then
-- Check if we are on correct response and correct depth -- Check if we are on correct response and correct depth
if current_depth == depth then if current_depth == depth then
if key == id then if key == id then
return value return value
end end
end end
else else
minetest.log("Entering again...") --minetest.log("Entering again...")
-- We have a dialogue action type. Need to check if dialogue has further responses -- We have a dialogue action type. Need to check if dialogue has further responses
if value.action.responses ~= nil then if value.action.responses ~= nil then
local response = get_response_object_by_id_recursive(value.action, current_depth + 1, response_id) local response = get_response_object_by_id_recursive(value.action, current_depth + 1, response_id)
if response ~= nil then if response ~= nil then
return response return response
end end
end end
end end
end end
end end
end end
-- Handler for dialogue formspec -- Handler for dialogue formspec
@ -418,8 +622,8 @@ minetest.register_on_player_receive_fields(function (player, formname, fields)
if fields then if fields then
local player_response = npc.dialogue.dialogue_results.yes_no_dialogue[player_name] local player_response = npc.dialogue.dialogue_results.yes_no_dialogue[player_name]
-- Unlock queue, reset action timer and unfreeze NPC. -- Unlock queue, reset action timer and unfreeze NPC.
npc.unlock_actions(player_response.npc) npc.unlock_actions(player_response.npc)
if fields.yes_option then if fields.yes_option then
player_response.yes_callback() player_response.yes_callback()
@ -437,76 +641,46 @@ minetest.register_on_player_receive_fields(function (player, formname, fields)
-- Get player response -- Get player response
local player_response = npc.dialogue.dialogue_results.options_dialogue[player_name] local player_response = npc.dialogue.dialogue_results.options_dialogue[player_name]
-- Check if the player hit the negative option -- Check if the player hit the negative option or esc button
if fields["exit"] then if fields["exit"] or fields["quit"] == "true" then
-- Unlock queue, reset action timer and unfreeze NPC. -- Unlock queue, reset action timer and unfreeze NPC.
npc.unlock_actions(player_response.npc) npc.unlock_actions(player_response.npc)
end end
for i = 1, #player_response.options do for i = 1, #player_response.options do
local button_label = "opt"..tostring(i) local button_label = "opt"..tostring(i)
if fields[button_label] then if fields[button_label] then
if player_response.options[i].action_type == "dialogue" then if player_response.options[i].action_type == "dialogue" then
-- Process dialogue object -- Process dialogue object
npc.dialogue.process_dialogue(player_response.npc, npc.dialogue.process_dialogue(player_response.npc,
player_response.options[i].action, player_response.options[i].action,
player_name) player_name)
elseif player_response.options[i].action_type == "function" then elseif player_response.options[i].action_type == "function" then
-- Execute function - get it directly from definition -- Execute function - get it directly from definition
-- Find NPC relationship phase with player -- Find NPC relationship phase with player
local phase = local phase =
npc.relationships.get_relationship_phase(player_response.npc, player_name) npc.relationships.get_relationship_phase(player_response.npc, player_name)
-- Check if NPC is married and the married NPC dialogue should be shown -- Check if NPC is married and the married NPC dialogue should be shown
if phase == "phase6" and player_response.is_married_dialogue == true then if phase == "phase6" and player_response.is_married_dialogue == true then
-- Get the function definitions from the married dialogue -- Get the function definitions from the married dialogue
npc.relationships.MARRIED_NPC_DIALOGUE npc.relationships.MARRIED_NPC_DIALOGUE
.responses[player_response.options[i].response_id] .responses[player_response.options[i].response_id]
.action(player_response.npc, player) .action(player_response.npc, player)
elseif player_response.is_custom_trade_dialogue == true then
elseif player_response.is_casual_trade_dialogue == true then -- Functions for a custom trade should be available from the same dialogue
-- Check if trade is casual buy or sell -- object as they are created on demand
if player_response.casual_trade_type == npc.trade.OFFER_BUY then --minetest.log("Player response: "..dump(player_response.options[i]))
-- Get functions from casual buy dialogue player_response.options[i].action(player_response.npc, player)
npc.trade.CASUAL_TRADE_BUY_DIALOGUE
.responses[player_response.options[i].response_id]
.action(player_response.npc, player)
elseif player_response.casual_trade_type == npc.trade.OFFER_SELL == true then
-- Get functions from casual sell dialogue
npc.trade.CASUAL_TRADE_SELL_DIALOGUE
.responses[player_response.options[i].response_id]
.action(player_response.npc, player)
end
return
elseif player_response.is_dedicated_trade_dialogue == true then
-- Get the functions for a dedicated trader prompt
npc.trade.DEDICATED_TRADER_PROMPT
.responses[player_response.options[i].response_id]
.action(player_response.npc, player)
return
elseif player_response.is_custom_trade_dialogue == true then
-- Functions for a custom trade should be available from the same dialogue
-- object as it is created in memory
minetest.log("Player response: "..dump(player_response.options[i]))
player_response.options[i].action(player_response.npc, player)
else else
-- Get dialogues for sex and phase -- Get dialogue from registered dialogues
local dialogues = npc.data.DIALOGUES[player_response.npc.sex][phase] local dialogue = npc.dialogue.registered_dialogues[player_response.options[i].dialogue_id]
local response = get_response_object_by_id_recursive(dialogue, 0, player_response.options[i].response_id)
minetest.log("Object: "..dump(dialogues[player_response.options[i].dialogue_id]))
local response = get_response_object_by_id_recursive(dialogues[player_response.options[i].dialogue_id], 0, player_response.options[i].response_id)
minetest.log("Found: "..dump(response))
-- Execute function
response.action(player_response.npc, player)
-- Execute function -- Execute function
-- dialogues[player_response.options[i].dialogue_id] response.action(player_response.npc, player)
-- .responses[player_response.options[i].response_id]
-- .action(player_response.npc, player)
-- Unlock queue, reset action timer and unfreeze NPC.
npc.unlock_actions(player_response.npc)
-- Unlock queue, reset action timer and unfreeze NPC.
npc.unlock_actions(player_response.npc)
end end
end end
return return

78
doc/actions_and_tasks.md Normal file
View File

@ -0,0 +1,78 @@
Actions and Tasks
Advanced_NPC Alpha-2 (DEV)
==========================
IMPORTANT: In this documentation is only the explanation of the particular operation of each predefined
action and task. Read reference documentation for details about API operation at [api.md](api.md).
Action (`add_action`)
---------------------
#### `SET_INTERVAL`
Set the interval at which the `action` are executed.
{
interval = 1, -- A decimal number, in seconds (default is 1 second)
freeze = false, -- if true, mobs_redo API will not execute until interval is set
}
#### `FREEZE`
This action allows to stop/execute mobs_redo API.
This is good for stopping the NPC from fighting, wandering, etc.
{
freeze = false, -- Boolean, if true, mobs_redo API will not execute.
}
Tasks (`add_task`)
------------------
#### `USE_BED`
Sequence of actions that allows the NPC to use a bed.
{
pos = {x=0,y=0,z=0}, --[[
^ Position of bed to be used.
^ Can be a coordinate x,y,z.
^ Can be a place name of the NPC place map.
Example: "bed_primary" ]]
action = action, --[[
^ Whether to get up or lay on bed
^ Defined in npc.actions.const.beds.action
^ Available options:
* npc.actions.const.beds.LAY : lay
* npc.actions.const.beds.GET_UP : get up
}
#### `WALK_TO_POS`
NPC will walk to the given position. This task uses the pathfinder to calculate the nodes
in the path that the NPC will walk through, then enqueues walk_step actions, combined with
correct directional rotations and opening/closing of doors on the path.
{
end_pos = {x=0,y=0,z=0}, --[[
^ Destination position to reach.
^ Can be a coordinate x,y,z.
^ Can be a place name of the NPC place map.
The position must be walkable for the npc to stop in,
or in the access position of the place.
Example: "home_inside" ]]
walkable = {}, --[[
^ An array of node names to consider as walkable nodes
for finding the path to the destination. ]]
use_access_node = true, --[[
^ Boolean, if true, when using places, it will find path
to the "accessible" node (empty or walkable node around
the target node) instead of to the target node.
^ Default is true. ]]
enforce_move = true, --[[
^ Boolean, if true and no path is found from the NPC's
position to the end_pos, the NPC will be teleported
to the destination (or, if use_access_node == true it will
teleport to the access position)
^ Default is true. ]]
}

424
doc/api.md Normal file
View File

@ -0,0 +1,424 @@
Advanced_NPC API Reference Alpha-2 (DEV)
=========================================
* More information at <https://github.com/hkzorman/advanced_npc/wiki>
IMPORTANT: This WIP & unfinished file contains the definitions of current advanced_npc functions
(Some documentation is lacking, so please bear in mind that this WIP file is just to enhance it)
Introduction
------------
You can consult this document for help on API of behaviors for the NPCs.
The goal is to be able to have NPCs that have the same functionality as normal players.
The NPCs make Sokomine's mg_villages in Minetest alive although they can
be manually spawned outside the village and work as good as new.
Here is some information about the API methods and systems.
* npc.lua also uses methods and functions from the dependency: mobs_redo <https://github.com/tenplus1/mobs_redo>
Initialize NPC
--------------
The API works with some variables into Lua Entity that represent a NPC,
then you should initialize the Lua Entity before that it really assume
a controled behavior.
### Methods
* `npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name)` : Initialize a NPC
The simplest way to start a mob (of mobs_redo API) is by using the `on_spawn` function
Note: currently this call is unduly repeated (mobs_redo problem), so you should check if npc has already been initialized.
on_spawn = function(self)
if self.initialized == nil then
npc.initialize(self, self.object:getpos(), true)
self.tamed = false
end
end
Or after add in the world
local obj = minetest.add_entity({x=0, y=10, z=0}, "mobs:sheep", {naked = true})
local luaentity = get_luaentity(obj)
npc.initialize(luaentity, luaentity.object:getpos(), true)
luaentity.tamed = false
NPC Steps
---------
The API works with NPC steps, then `on_step` callback need run the
`npc.on_step(luaentity)`. This function process the NPC actions
and return the freeze state, which is used for stop mobs_redo behavior.
Example:
on_step = function(self, dtime)
npc.step(self, dtime)
end
Mobs of Mobs_Redo API uses `do_custom` function instead of `on_step` callback
and it needs return the freeze state to stop mobs_redo behavior.
Here is a recommended code.
do_custom = function(self, dtime)
-- Here is my "do_custom" code
-- Process the NPC action and return freeze state
return npc.step(self, dtime)
end
Actions and Tasks Queue
-----------------------
Actions are "atomic" executable actions the NPC can perform. Tasks are
sequences of actions that are common enough to be supported by default.
Each action or task is wrapped on a Lua table which tells the action/task
to be executed and the arguments to be used. However, this is encapsulated
to the user in the following two methods for a NPCs:
### Methods
* `npc.add_action(luaentity, action, {action definition})`: Add action into NPC actions queue
* `npc.add_task(luaentity, task, {task definition})`: Add task into NPC actions queue
For both of the above, `action`/`task` is a constant defined in
`npc.actions.cmd`, and `{task/action definition}` is a Lua table specific arguments
to each `action`/`task`.
Example
npc.add_task(self, npc.actions.cmd.USE_BED, {
pos = {x=0,y=0,z=0},
action = npc.actions.const.beds.LAY
})
npc.add_action(self, npc.actions.cmd.SET_INTERVAL, {
interval = 10,
freeze = true,
})
npc.add_task(self, npc.actions.cmd.USE_BED, {
pos = {x=0,y=0,z=0},
action = npc.actions.const.beds.GET_UP
})
See more in [actions_and_tasks.md](actions_and_tasks.md) documentation.
Schedules
---------
The interesting part of Advanced NPC is its ability to simulate realistic
behavior in NPCs. Realistic behavior is defined simply as being able to
perform tasks at a certain time of the day, like usually people do. This
allow the NPC to go to bed, sleep, get up from it, sit in benches, etc.
All of this is simulated through a structured code using action and tasks.
The implementation resembles a rough OS process scheduling algorithm where
only one process is allowed at a time. The processes or tasks are held in
a queue, where they are executed one at a time in queue fashion.
Interruptions are allowed, and the interrupted action is re-started once
the interruption is finished.
### Schedule commands
Schedule commands are an array of actions and tasks that the NPC.
There are 4 possible commands:
* action
```
{
action = action, -- Is a constant defined in `npc.actions.cmd`
args = {} -- action arguments
}
```
* task
```
{
task = task, -- Is a constant defined in `npc.actions.cmd`
args = {} -- task arguments
}
```
* Property change
```
{
???
}
```
* Schedule query/check
```
   {
schedule query/check definition
   }
```
### Schedule time
Only integer value 0 until 23
* 0: 0/24000 - 999
* 1: 1000 - 1999
* 2: 2000 - 2999
* ...
* 22: 22000 - 22999
* 23: 23000 - 23999
### Schedule Type
* "generic" : Returns nil if there are already seven schedules, one for each
day of the week or if the schedule attempting to add already exists.
The date parameter is the day of the week it represents as follows:
Note: Currently only one schedule is supported, for day 0
1: Monday
2: Tuesday
3: Wednesday
4: Thursday
5: Friday
6: Saturday
7: Sunday
* "date_based" : The date parameter should be a string of the format "MM:DD".
If it already exists, function retuns nil
### Methods
* `npc.create_schedule(luaentity, schedule_type, day)` : Create a schedule for a NPC
* `npc.delete_schedule(luaentity, schedule_type, date)` : Delete a schedule for a NPC
* `npc.add_schedule_entry(luaentity, schedule_type, date, time, check, commands)` : Add a schedule entry for a time
* `npc.get_schedule_entry(luaentity, schedule_type, date, time)` : Get a schedule entry
* `npc.update_schedule_entry(luaentity, schedule_type, date, time, check, commands)` : Update a schedule entry
### Examples
-- Schedule entry for 7 in the morning
npc.add_schedule_entry(self, "generic", 0, 7, nil, {
-- Get out of bed
[1] = {
task = npc.actions.cmd.USE_BED,
args = {
pos = "bed_primary",
action = npc.actions.const.beds.GET_UP
}
},
-- Allow mobs_redo wandering
[2] = {
action = npc.actions.cmd.FREEZE,
args = {
freeze = false
}
}
})
Occupations
-----------
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.
### Methods
* `npc.occupations.register_occupation(occupation_name, {occupation definition})` : Register an occupation
* `npc.occupations.initialize_occupation_values(luaentity, occupation_name)` : Initialize an occupation for a NPC
Places Map
----------
Places map define which NPCs can access which places.
Places are separated into different types.
### Place types
Current place types
* `bed_primary` : the bed of a NPC
* `sit_primary`
* `sit_shared`
* `furnace_primary`
* `furnace_shared`
* `storage_primary`
* `storage_shared`
* `home_entrance_door`
* `schedule_target_pos` : used in the schedule actions
* `calculated_target_pos`
* `workplace_primary`
* `workplace_tool`
* `home_plotmarker`
* `home_inside`
* `home_outside`
### Methods
* `npc.places.add_owned(luaentity, place_name, place_type, pos, access_pos)` : Add owned place.
`luaentity` npc owner.
`place_name` a specific place name.
`place_type` place typing.
`pos` is a position of a node to be owned.
`access_pos` is the coordinate where npc must be to initiate the access.
Place is added for the NPC.
* `npc.places.add_shared(luaentity, place_name, place_type, pos, access_node)` : Add shared place
Dialogues
---------
Dialogs can be registered to be spoken by NPCs.
### Tags
The flags or marks of the dialogue text. Tags can be used for ....
* "unisex" : Both male and female NPCs can say the defined text.
* "phase1" : NPCs in phase 1 of a relationship can say the defined text.
### Methods
* `set_response_ids_recursively()` : A local function that assigns unique
key IDs to dialogue responses.
* `npc.dialogue.register_dialogue({dialogue definition})` : Defines and
registers dialogues.
* `npc.dialogue.search_dialogue_by_tags({search_tags})` : A method returning
a table of dialogues if called.
Definition tables
-----------------
### Occupation definition (`register_occupation`)
{
dialogues = {
enable_gift_item_dialogues = true, --[[
^ This flag enables/disables gift item dialogues.
^ If not set, it defaults to true. ]]
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. ]]
walkable_nodes = {}, -- Walkable nodes
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. ]]
workplace_nodes = {}, --[[
^ An array of string where each string is a node the NPC works with.
^ These are useful for assigning workplaces and work work nodes. ]]
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. ]]
initial_trader_status = "", --[[
^ String that specifies initial trader value.
^ Valid values are: "casual", "trader", "none" ]]
schedules_entries = {},
^ This is a table of tables in the following format:
{
[<time number>] = {
[<command number>] = {
command
}
}
}
^ Example:
{
[1] = {
[1] = schedule command
},
[13] = {
[1] = schedule command,
[2] = schedule command
},
[23] = {
[1] = schedule command
}
}
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. ]]
}
### Dialogue definition (`register_dialogue`)
{
text = "Hello.", --[[
^ The dialogue text itself.
^ It must be included in the method.]]
tags = {"tag1", "tag2"} --[[
^ The flags or marks of the dialogue text.
^ The object can be excluded. ]]
}
### Schedule query/check definition (schedule command)
{
check = true, -- Indicates that this is a schedule query/check
range = 2, -- Range of checked area in blocks.
count = 20, -- How many checks will be performed.
random_execution_times = true, --[[
^ Randomizes the number of checks that will be performed.
^ min_count and max_count is required ]]
min_count = 20, -- minimum of checks
max_count = 25, -- maximum of checks
nodes = {"itemstring1", "itemstring2"}, --[[
^ Nodes to be found for the actions.
^ When a node is found, it is add in the npc place map
with the place name "schedule_target_pos"
prefer_last_acted_upon_node = true, -- If prefer to act on nodes already acted upon
walkable_nodes = {"itemstring1", "itemstring2"}, -- Walkable nodes
actions = { --[[
^ Table where index is a itemstring of the node to be found,
and value is an array of actions and tasks to be performed
when found the node. ]]
["itemstring1"] = {
[1] = action or task in schedule command format,
[2] = action or task in schedule command format,
[3] = action or task in schedule command format
},
["itemstring2"] = {
[1] = action or task in schedule command format,
[2] = action or task in schedule command format
}
},
}
Examples:
Syntax example 1:
npc.dialogue.register_dialogue({
text = "Hello.", -- "Hello." will be said by the NPC upon rightclick and displayed in the messages section.
tags = {"unisex", "phase1"} -- The flags that define the conditions of who and what can say the text.
})
Syntax example 2:
npc.dialogue.register_dialogue({
text = "Hello again."
-- The tags object is excluded, meaning that any NPC can say "Hello again." upon rightclick under no condition.
})

View File

@ -24,6 +24,7 @@ end
mobs.intllib = S mobs.intllib = S
dofile(path .. "/npc.lua") dofile(path .. "/npc.lua")
dofile(path .. "/utils.lua")
dofile(path .. "/spawner.lua") dofile(path .. "/spawner.lua")
dofile(path .. "/relationships.lua") dofile(path .. "/relationships.lua")
dofile(path .. "/dialogue.lua") dofile(path .. "/dialogue.lua")
@ -32,8 +33,16 @@ dofile(path .. "/trade/prices.lua")
dofile(path .. "/actions/actions.lua") dofile(path .. "/actions/actions.lua")
dofile(path .. "/actions/places.lua") dofile(path .. "/actions/places.lua")
dofile(path .. "/actions/pathfinder.lua") dofile(path .. "/actions/pathfinder.lua")
dofile(path .. "/actions/jumper.lua")
dofile(path .. "/actions/node_registry.lua") dofile(path .. "/actions/node_registry.lua")
dofile(path .. "/occupations/occupations.lua")
-- Load random data definitions
dofile(path .. "/random_data.lua") dofile(path .. "/random_data.lua")
dofile(path .. "/data/dialogues_data.lua")
dofile(path .. "/data/gift_items_data.lua")
dofile(path .. "/data/names_data.lua")
dofile(path .. "/data/occupations/default.lua")
dofile(path .. "/data/occupations/default_farmer.lua")
dofile(path .. "/data/occupations/default_priest.lua")
dofile(path .. "/data/occupations/default_miner.lua")
print (S("[Mod] Advanced NPC loaded")) print (S("[Mod] Advanced NPC loaded"))

2149
npc.lua

File diff suppressed because it is too large Load Diff

468
occupations/occupations.lua Normal file
View File

@ -0,0 +1,468 @@
-- 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 = {
-- enable_gift_item_dialogues = true,
-- -- This flag enables/disables gift item dialogues.
-- -- If not set, it defaults to true.
-- 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
-- workplace_nodes = {},
-- -- An array of string where each string is a node the NPC
-- -- works with. These are useful for assigning workplaces and work
-- -- work nodes.
-- 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.
-- initial_trader_status = "",
-- -- String that specifies initial trader value. Valid values are:
-- -- "casual", "trader", "none"
-- 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 = {}
-- Basic occupation name
npc.occupations.basic_name = "default_basic"
-- 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 = {},
-- 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)
-- Register all dialogues per definition
local dialogue_keys = {}
if def.dialogues then
-- 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 pairs(def.dialogues.data) do
-- Add to the dialogue tags the "occupation name"
table.insert(dialogue.tags, name)
-- Register dialogue
--npc.log("INFO", "Registering dialogue for occupation "..dump(name)..": "..dump(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. Currently only registering will be performed.
-- Register dialogues
for _, dialogue in pairs(def.dialogues.data) do
-- Add to the dialogue tags the "occupation name"
table.insert(dialogue.tags, name)
-- Register dialogue
local key = npc.dialogue.register_dialogue(dialogue)
-- Add key to set of dialogue keys
table.insert(dialogue_keys, key)
end
end
end
-- Save into the definition the dialogue keys
def.dialogues["keys"] = dialogue_keys
-- Save the definition
npc.occupations.registered_occupations[name] = def
--npc.log("INFO", "Successfully registered occupation with name: "..dump(name))
end
-- This function scans all registered occupations and filter them by
-- building type and surrounding building type, returning an array
-- of occupation names (strings)
-- BEWARE! Below this lines lies ugly, incomprehensible code!
function npc.occupations.get_for_building(building_type, surrounding_building_types)
local result = {}
for name,def in pairs(npc.occupations.registered_occupations) do
-- Check for empty or nil building types, in that case, any building
if def.building_types == nil or def.building_types == {}
and def.surrounding_building_types == nil or def.surrounding_building_types == {} then
--minetest.log("Empty")
-- Empty building types, add to result
table.insert(result, name)
elseif def.building_types ~= nil and #def.building_types > 0 then
-- Check if building type is contained in the def's building types
if npc.utils.array_contains(def.building_types, building_type) then
table.insert(result, name)
end
end
-- Check for empty or nil surrounding building types
if def.surrounding_building_types ~= nil
and #def.surrounding_building_types > 0 then
-- -- Add this occupation
-- --table.insert(result, name)
-- else
-- Surrounding buildings is not empty, loop though them and compare
-- to the given ones
for i = 1, #surrounding_building_types do
for j = 1, #def.surrounding_building_types do
-- Check if the definition's surrounding building type is the same
-- as the given one
if def.surrounding_building_types[j].type
== surrounding_building_types[i].type then
-- Check if the origin buildings contain the expected type
if npc.utils.array_contains(def.surrounding_building_types[j].origin_building_types,
surrounding_building_types[i].origin_building_type) then
-- Add this occupation
table.insert(result, name)
end
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
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
-- If an array was returned, select a random texture from it
if type(self.selected_texture) == "table" then
local selected_texture = self.selected_texture[math.random(1, #self.selected_texture)]
self.selected_texture = selected_texture
end
-- Set texture and base texture
self.textures = {self.selected_texture}
self.base_texture = {self.selected_texture }
-- Assign sex based on texture
self.sex = npc.assign_sex_from_texture(self)
-- Refresh entity
self.object:set_properties(self)
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 then
-- Check for gift item dialogues enable
if def.dialogues.disable_gift_item_dialogues then
self.dialogues.hints = {}
end
local dialogue_keys = {}
-- Check which type of dialogues we have
if def.dialogues.type == "given" and def.dialogues.keys then
-- We have been given the dialogues, so def.dialogues.data contains
-- an array of dialogues. These dialogues were registered, therefore we need
-- just the keys
for i = 1, #def.dialogues.keys do
table.insert(dialogue_keys, def.dialogues.keys[i])
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
if def.dialogues.keys then
-- Add the registered dialogues
for i = 1, #def.dialogues.keys do
table.insert(dialogue_keys, def.dialogues.keys[i])
end
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 pairs(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.dialogues.max_count > 0 then
max_dialogue_count = def.dialogues.max_count
end
-- Add dialogues to the normal dialogues for NPC
if #dialogue_keys > 0 then
self.dialogues.normal = {}
for i = 1, math.min(max_dialogue_count, #dialogue_keys) do
self.dialogues.normal[i] = dialogue_keys[i]
end
end
end
-- Initialize properties
--minetest.log("def.properties: "..dump(def.properties))
if def.properties then
-- Initialize trader status
if def.properties.initial_trader_status then
self.trader_data.trader_status = def.properties.initial_trader_status
end
-- Enable/disable gift items hints
if def.properties.enable_gift_items_hints ~= nil then
self.gift_data.enable_gift_items_hints = def.properties.enable_gift_items_hints
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

View File

@ -374,7 +374,7 @@ npc.data.FIRST_NAMES = {
"Arianne", "Arianne",
"Lizzy", "Lizzy",
"Amy", "Amy",
"Chole", "Chloe",
"Alisson" "Alisson"
}, },
male = { male = {
@ -396,8 +396,7 @@ npc.FAVORITE_ITEMS = {
female = {}, female = {},
male = {} male = {}
} }
-- Define items by phase
-- Female
npc.FAVORITE_ITEMS.female["phase1"] = { npc.FAVORITE_ITEMS.female["phase1"] = {
{item = "default:apple", {item = "default:apple",
response = "Hey, I really wanted an apple, thank you!", response = "Hey, I really wanted an apple, thank you!",
@ -452,7 +451,7 @@ npc.FAVORITE_ITEMS.female["phase4"] = {
hint = "I could really do with an apple..."}, hint = "I could really do with an apple..."},
{item = "farming:bread", {item = "farming:bread",
response = "Thanks, you didn't have to, but thanks...", response = "Thanks, you didn't have to, but thanks...",
hint = "SOme fresh bread would be good!"} hint = "Some fresh bread would be good!"}
} }
npc.FAVORITE_ITEMS.female["phase5"] = { npc.FAVORITE_ITEMS.female["phase5"] = {
{item = "default:apple", {item = "default:apple",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

3
spawner_marker.lua Normal file
View File

@ -0,0 +1,3 @@
-- Spawner markers
-- Specialized functionality to allow players do NPC spawning
-- on their own custom buildings.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 901 B

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
textures/npc_child_male1.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
textures/npc_female10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
textures/npc_female11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
textures/npc_female2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
textures/npc_female3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
textures/npc_female4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
textures/npc_female5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
textures/npc_female6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
textures/npc_female7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
textures/npc_female8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
textures/npc_female9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
textures/npc_male10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
textures/npc_male11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
textures/npc_male12.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
textures/npc_male13.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
textures/npc_male14.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
textures/npc_male2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
textures/npc_male3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
textures/npc_male4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
textures/npc_male5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
textures/npc_male6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
textures/npc_male7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
textures/npc_male8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
textures/npc_male9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 B

View File

@ -35,8 +35,9 @@ npc.trade.prices.table["mobs:leather"] = {tier = npc.trade.prices.curre
npc.trade.prices.table["default:sword_stone"] = {tier = npc.trade.prices.currency.tier3.string, count = 6} npc.trade.prices.table["default:sword_stone"] = {tier = npc.trade.prices.currency.tier3.string, count = 6}
npc.trade.prices.table["default:shovel_stone"] = {tier = npc.trade.prices.currency.tier3.string, count = 6} npc.trade.prices.table["default:shovel_stone"] = {tier = npc.trade.prices.currency.tier3.string, count = 6}
npc.trade.prices.table["default:axe_stone"] = {tier = npc.trade.prices.currency.tier3.string, count = 6} npc.trade.prices.table["default:axe_stone"] = {tier = npc.trade.prices.currency.tier3.string, count = 6}
npc.trade.prices.table["default:hoe_stone"] = {tier = npc.trade.prices.currency.tier3.string, count = 6} npc.trade.prices.table["farming:hoe_stone"] = {tier = npc.trade.prices.currency.tier3.string, count = 6}
npc.trade.prices.table["default:pick_stone"] = {tier = npc.trade.prices.currency.tier3.string, count = 7} npc.trade.prices.table["default:pick_stone"] = {tier = npc.trade.prices.currency.tier3.string, count = 7}
npc.trade.prices.table["bucket:bucket_empty"] = {tier = npc.trade.prices.currency.tier3.string, count = 10}
npc.trade.prices.table["farming:cotton"] = {tier = npc.trade.prices.currency.tier3.string, count = 15} npc.trade.prices.table["farming:cotton"] = {tier = npc.trade.prices.currency.tier3.string, count = 15}
npc.trade.prices.table["farming:bread"] = {tier = npc.trade.prices.currency.tier3.string, count = 20} npc.trade.prices.table["farming:bread"] = {tier = npc.trade.prices.currency.tier3.string, count = 20}

File diff suppressed because it is too large Load Diff

97
utils.lua Normal file
View File

@ -0,0 +1,97 @@
-- Basic utilities to work with table operations in Lua, and specific querying
-- By Zorman2000
npc.utils = {}
function npc.utils.split(inputstr, sep)
if sep == nil then
sep = "%s"
end
local t={}
local i=1
for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
t[i] = str
i = i + 1
end
return t
end
function npc.utils.array_contains(array, item)
--minetest.log("Array: "..dump(array))
--minetest.log("Item being searched: "..dump(item))
for i = 1, #array do
--minetest.log("Equals? "..dump(array[i] == item))
if array[i] == item then
return true
end
end
return false
end
function npc.utils.array_is_subset_of_array(set, subset)
local match_count = 0
for j = 1, #subset do
for k = 1, #set do
if subset[j] == set[k] then
match_count = match_count + 1
end
end
end
-- Check match count
return match_count == #subset
end
function npc.utils.get_map_keys(map)
local result = {}
for key, _ in pairs(map) do
table.insert(result, key)
end
return result
end
function npc.utils.get_map_values(map)
local result = {}
for _, value in pairs(map) do
table.insert(result, value)
end
return result
end
-- This function searches for a node given the conditions specified in the
-- query object, starting from the given start_pos and up to a certain, specified
-- range.
-- Query object:
-- search_type: determines the direction to search nodes.
-- Valid values are: orthogonal, cross, cube
-- - orthogonal search means only nodes which are parallel to the search node's faces
-- will be considered. This limits the search to only 6 nodes.
-- - cross search will look at the same nodes as orthogonal, plus will also
-- check nodes diagonal to the node four horizontal nodes. This search looks at 14 nodes
-- - cube search means to look every node surrounding the node, including all diagonals.
-- This search looks at 26 nodes.
-- search_nodes: array of nodes to search for
-- surrounding_nodes: object specifying which neighbor nodes are to be expected and
-- at which locations. Valid keys are:
-- - North (+Z dir)
-- - East (+x dir)
-- - South (-Z dir)
-- - West (-X dir)
-- - Top (+Y dir)
-- - Bottom (-Y dir)
-- Example: ["bottom"] = {nodes={"default:dirt"}, criteria="all"}
-- Each object will contain nodes, and criteria for acceptance.
-- Criteria values are:
-- - any: true as long as one of the nodes on this side is one of the specified
-- in "nodes"
-- - all: true when the set of neighboring nodes on this side contain one or many of
-- the specified "nodes"
-- - all-exact: true when the set of neighboring nodes on this side contain all nodes
-- specified in "nodes"
-- - shape: true when the set of neighboring nodes on this side contains nodes in
-- the exact given shape. If so, nodes will not be an array, but a 2d array
-- of three rows and three columns, with the specific shape. Notice that
-- the nodes on the side can vary depending on the search type (orthogonal,
-- cross, cube)
function npc.utils.search_node(query, start_pos, range)
end