-
Notifications
You must be signed in to change notification settings - Fork 5
/
npc.lua
executable file
·2353 lines (2181 loc) · 80.3 KB
/
npc.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-- Advanced NPC by Zorman2000
-- Based on original NPC by Tenplus1
local S = mobs.intllib
npc = {}
-- Constants
npc.FEMALE = "female"
npc.MALE = "male"
npc.age = {
adult = "adult",
child = "child"
}
npc.INVENTORY_ITEM_MAX_STACK = 99
npc.ANIMATION_STAND_START = 0
npc.ANIMATION_STAND_END = 79
npc.ANIMATION_SIT_START = 81
npc.ANIMATION_SIT_END = 160
npc.ANIMATION_LAY_START = 162
npc.ANIMATION_LAY_END = 166
npc.ANIMATION_WALK_START = 168
npc.ANIMATION_WALK_END = 187
npc.ANIMATION_MINE_START = 189
npc.ANIMATION_MINE_END =198
npc.direction = {
north = 0,
east = 1,
south = 2,
west = 3,
north_east = 4,
north_west = 5,
south_east = 6,
south_west = 7
}
npc.action_state = {
none = 0,
executing = 1,
interrupted = 2
}
npc.log_level = {
INFO = true,
WARNING = true,
ERROR = true,
DEBUG = false,
DEBUG_ACTION = false,
DEBUG_SCHEDULE = false,
EXECUTION = false
}
npc.texture_check = {
timer = 0,
interval = 2
}
---------------------------------------------------------------------------------------
-- General functions
---------------------------------------------------------------------------------------
-- Logging
function npc.log(level, message)
if npc.log_level[level] then
minetest.log("[advanced_npc] "..level..": "..message)
end
end
-- NPC chat
function npc.chat(npc_name, player_name, message)
minetest.chat_send_player(player_name, npc_name..": "..message)
end
-- Simple wrapper over minetest.add_particle()
-- Copied from mobs_redo/api.lua
function npc.effect(pos, amount, texture, min_size, max_size, radius, gravity, glow)
radius = radius or 2
min_size = min_size or 0.5
max_size = max_size or 1
gravity = gravity or -10
glow = glow or 0
minetest.add_particlespawner({
amount = amount,
time = 0.25,
minpos = pos,
maxpos = pos,
minvel = {x = -radius, y = -radius, z = -radius},
maxvel = {x = radius, y = radius, z = radius},
minacc = {x = 0, y = gravity, z = 0},
maxacc = {x = 0, y = gravity, z = 0},
minexptime = 0.1,
maxexptime = 1,
minsize = min_size,
maxsize = max_size,
texture = texture,
glow = glow,
})
end
-- Gets name of player or NPC
function npc.get_entity_name(entity)
if entity:is_player() then
return entity:get_player_name()
else
return entity:get_luaentity().name
end
end
-- Returns the item "wielded" by player or NPC
-- TODO: Implement NPC
function npc.get_entity_wielded_item(entity)
if entity:is_player() then
return entity:get_wielded_item()
end
end
---------------------------------------------------------------------------------------
-- Spawning functions
---------------------------------------------------------------------------------------
-- These functions are used at spawn time to determine several
-- random attributes for the NPC in case they are not already
-- defined. On a later phase, pre-defining many of the NPC values
-- will be allowed.
local function get_random_name(gender, tags)
local search_tags = {gender}
if tags then
search_tags = { gender, unpack(tags) }
end
local names = npc.info.get_names(search_tags, "all_match")
if next(names) ~= nil then
local i = math.random(#names)
return names[i]
else
-- Return a default name if no name was found
return "Anonymous"
end
end
local function initialize_inventory()
return {
[1] = "", [2] = "", [3] = "", [4] = "",
[5] = "", [6] = "", [7] = "", [8] = "",
[9] = "", [10] = "", [11] = "", [12] = "",
[13] = "", [14] = "", [15] = "", [16] = "",
}
end
-- This function checks for "female" text on the texture name
local function is_female_texture(textures)
for i = 1, #textures do
if string.find(textures[i], "female") ~= nil then
return true
end
end
return false
end
function npc.assign_gender_from_texture(self)
if is_female_texture(self.base_texture) then
return npc.FEMALE
else
return npc.MALE
end
end
local function get_random_texture(gender, age)
local textures = npc.info.get_textures({gender, age}, "all_match")
if next(textures) ~= nil then
local i = math.random(#textures)
return {textures[i]}
else
return {"default_"..gender..".png"}
end
-- local textures = {}
-- local filtered_textures = {}
-- -- Find textures by gender and age
-- if age == npc.age.adult then
-- --minetest.log("Registered: "..dump(minetest.registered_entities["advanced_npc:npc"]))
-- textures = minetest.registered_entities["advanced_npc:npc"].texture_list
-- elseif age == npc.age.child then
-- textures = minetest.registered_entities["advanced_npc:npc"].child_texture
-- end
--
-- for i = 1, #textures do
-- local current_texture = textures[i][1]
-- if (gender == npc.MALE
-- and string.find(current_texture, gender)
-- and not string.find(current_texture, npc.FEMALE))
-- or (gender == npc.FEMALE
-- and string.find(current_texture, gender)) then
-- table.insert(filtered_textures, current_texture)
-- end
-- end
--
-- -- Check if filtered textures is empty
-- if filtered_textures == {} then
-- return textures[1][1]
-- end
--
-- return filtered_textures[math.random(1,#filtered_textures)]
end
--function npc.get_random_texture_from_array(age, gender, textures)
-- local filtered_textures = {}
--
-- for i = 1, #textures do
-- local current_texture = textures[i]
-- -- Filter by age
-- if (gender == npc.MALE
-- and string.find(current_texture, gender)
-- and not string.find(current_texture, npc.FEMALE)
-- and ((age == npc.age.adult
-- and not string.find(current_texture, npc.age.child))
-- or (age == npc.age.child
-- and string.find(current_texture, npc.age.child))
-- )
-- )
-- or (gender == npc.FEMALE
-- and string.find(current_texture, gender)
-- and ((age == npc.age.adult
-- and not string.find(current_texture, npc.age.child))
-- or (age == npc.age.child
-- and string.find(current_texture, npc.age.child))
-- )
-- ) then
-- table.insert(filtered_textures, current_texture)
-- end
-- end
--
-- -- Check if there are no textures
-- if #filtered_textures == 0 then
-- -- Return whole array for re-evaluation
-- npc.log("DEBUG", "No textures found, returning original array")
-- return textures
-- end
--
-- return filtered_textures[math.random(1, #filtered_textures)]
--end
-- Choose whether NPC can have relationships. Only 30% of NPCs
-- cannot have relationships
local function can_have_relationships(is_child)
-- Children can't have relationships
if is_child then
return false
end
local chance = math.random(1,10)
return chance > 3
end
-- Choose a maximum of two items that the NPC will have at spawn time
-- These items are chosen from the favorite items list.
local function choose_spawn_items(self)
local number_of_items_to_add = math.random(1, 2)
-- local number_of_items = #npc.FAVORITE_ITEMS[self.gender].phase1
--
-- for i = 1, number_of_items_to_add do
-- npc.add_item_to_inventory(
-- self,
-- npc.FAVORITE_ITEMS[self.gender].phase1[math.random(1, number_of_items)].item,
-- math.random(1,5)
-- )
-- end
-- Add currency to the items spawned with. Will add 5-10 tier 3
-- currency items
local currency_item_count = math.random(5, 10)
npc.add_item_to_inventory(self, npc.trade.prices.get_currency_itemstring("tier3"), currency_item_count)
-- For test
--npc.add_item_to_inventory(self, "default:tree", 10)
--npc.add_item_to_inventory(self, "default:cobble", 10)
--npc.add_item_to_inventory(self, "default:diamond", 2)
--npc.add_item_to_inventory(self, "default:mese_crystal", 2)
--npc.add_item_to_inventory(self, "flowers:rose", 2)
--npc.add_item_to_inventory(self, "advanced_npc:marriage_ring", 2)
--npc.add_item_to_inventory(self, "flowers:geranium", 2)
--npc.add_item_to_inventory(self, "mobs:meat", 2)
--npc.add_item_to_inventory(self, "mobs:leather", 2)
--npc.add_item_to_inventory(self, "default:sword_stone", 2)
--npc.add_item_to_inventory(self, "default:shovel_stone", 2)
--npc.add_item_to_inventory(self, "default:axe_stone", 2)
--minetest.log("Initial inventory: "..dump(self.inventory))
end
-- Spawn function. Initializes all variables that the
-- NPC will have and choose random, starting values
function npc.initialize(entity, pos, is_lua_entity, npc_stats, npc_info)
npc.log("INFO", "Initializing NPC at "..minetest.pos_to_string(pos))
-- Get variables
local ent = entity
if not is_lua_entity then
ent = entity:get_luaentity()
end
local occupation_name
if npc_info then
occupation_name = npc_info.occupation_name
end
-- Avoid NPC to be removed by mobs_redo API
ent.remove_ok = false
-- Flag that enables/disables right-click interaction - good for moments where NPC
-- can't be disturbed
ent.enable_rightclick_interaction = true
-- Determine gender and age
-- If there's no previous NPC data, gender and age will be randomly chosen.
-- - Sex: Female or male will have each 50% of spawning
-- - Age: 90% chance of spawning adults, 10% chance of spawning children.
-- If there is previous data then:
-- - Sex: The unbalanced gender will get a 75% chance of spawning
-- - Example: If there's one male, then female will have 75% spawn chance.
-- - If there's male and female, then each have 50% spawn chance.
-- - Age: For each two adults, the chance of spawning a child next will be 50%
-- If there's a child for two adults, the chance of spawning a child goes to
-- 40% and keeps decreasing unless two adults have no child.
-- Use NPC stats if provided
if npc_stats then
-- Default chances
local male_s, male_e = 0, 50
local female_s, female_e = 51, 100
local adult_s, adult_e = 0, 85
local child_s, child_e = 86, 100
-- Determine gender probabilities
if npc_stats[npc.FEMALE].total > npc_stats[npc.MALE].total then
male_e = 75
female_s, female_e = 76, 100
elseif npc_stats[npc.FEMALE].total < npc_stats[npc.MALE].total then
male_e = 25
female_s, female_e = 26, 100
end
-- Determine age probabilities
if npc_stats["adult_total"] >= 2 then
if npc_stats["adult_total"] % 2 == 0
and (npc_stats["adult_total"] / 2 > npc_stats["child_total"]) then
child_s,child_e = 26, 100
adult_e = 25
else
child_s, child_e = 61, 100
adult_e = 60
end
end
-- Get gender and age based on the probabilities
local gender_chance = math.random(1, 100)
local age_chance = math.random(1, 100)
local selected_gender = ""
local selected_age = ""
-- Select gender
if male_s <= gender_chance and gender_chance <= male_e then
selected_gender = npc.MALE
elseif female_s <= gender_chance and gender_chance <= female_e then
selected_gender = npc.FEMALE
end
-- Set gender for NPC
ent.gender = selected_gender
-- Select age
if adult_s <= age_chance and age_chance <= adult_e then
selected_age = npc.age.adult
elseif child_s <= age_chance and age_chance <= child_e then
selected_age = npc.age.child
ent.visual_size = {
x = 0.65,
y = 0.65
}
ent.collisionbox = {-0.10,-0.50,-0.10, 0.10,0.40,0.10}
ent.is_child = true
-- For mobs_redo
ent.child = true
end
-- Store the selected age
ent.age = selected_age
-- Set texture accordingly
local selected_texture = get_random_texture(selected_gender, selected_age)
--minetest.log("Selected texture: "..dump(selected_texture))
-- Store selected texture due to the need to restore it later
ent.selected_texture = selected_texture
-- Set texture and base texture
ent.textures = {selected_texture}
ent.base_texture = {selected_texture}
elseif npc_info then
-- Attempt to assign gender from npc_info
if npc_info.gender then
ent.gender = npc_info.gender
else
local gender_chance = math.random(1,2)
ent.gender = npc.FEMALE
if gender_chance == 1 then
ent.gender = npc.MALE
end
end
-- Attempt to assign age from npc_info
if npc_info.age then
ent.age = npc_info.age
else
ent.age = npc.age.adult
end
else
-- Randomly choose gender, and spawn as adult
local gender_chance = math.random(1,2)
ent.gender = npc.FEMALE
if gender_chance == 1 then
ent.gender = npc.MALE
end
ent.age = npc.age.adult
end
-- Initialize all gift data
ent.gift_data = {
-- Choose favorite items. Choose phase1 per default
favorite_items = npc.relationships.select_random_favorite_items(ent.gender, "phase1"),
-- Choose disliked items. Choose phase1 per default
disliked_items = npc.relationships.select_random_disliked_items(ent.gender),
-- Enable/disable gift item hints dialogue lines
enable_gift_items_hints = true
}
-- Flag that determines if NPC can have a relationship
ent.can_have_relationship = can_have_relationships(ent.is_child)
--ent.infotext = "Interested in relationships: "..dump(ent.can_have_relationship)
-- Flag to determine if NPC can receive gifts
ent.can_receive_gifts = ent.can_have_relationship
-- Initialize relationships object
ent.relationships = {}
-- Determines if NPC is married or not
ent.is_married_to = nil
-- Initialize dialogues
ent.dialogues = npc.dialogue.select_random_dialogues_for_npc(ent, "phase1")
-- Declare NPC inventory
ent.inventory = initialize_inventory()
-- Choose items to spawn with
choose_spawn_items(ent)
-- Flags: generic booleans or functions that help drive functionality
ent.flags = {}
-- Declare trade data
ent.trader_data = {
-- Type of trader
trader_status = npc.trade.get_random_trade_status(),
-- Current buy offers
buy_offers = {},
-- Current sell offers
sell_offers = {},
-- Items to buy change timer
change_offers_timer = 0,
-- Items to buy change timer interval
change_offers_timer_interval = 60,
-- Trading list: a list of item names the trader is expected to trade in.
-- It is mostly related to its occupation.
-- If empty, the NPC will revert to casual trading
-- If not, it will try to sell those that it have, and buy the ones it not.
trade_list = {},
-- Custom trade allows to specify more than one payment
-- and a custom prompt (instead of the usual buy or sell prompts)
custom_trades = {}
}
-- To model and control behavior of a NPC, advanced_npc follows an OS model
-- where it allows developers to create processes. These processes executes
-- programs, or a group of instructions that together make the NPC do something,
-- e.g. follow a player, use a furnace, etc. The model is:
-- - Each process has:
-- - An `execution context`, which is memory to store variables
-- - An `instruction queue`, which is a queue with the program instructions
-- to execute
-- - A `state`, whether the process is running or is paused
-- - Processes can specify whether they allow interruptions or not. They also
-- can opt to handle the interruption with a callback. The possible
-- interruptions are:
-- - Punch interruption
-- - Rightclick interruption
-- - Schedule interruption
-- - Only one process can run at a time. If another process is executed,
-- the currently running process is paused, and restored when the other ends.
-- - Processes can be enqueued, so once the executing process finishes, the
-- next one in the queue can be started.
-- - One process, called the `state` process, will run by default when no
-- processes are executing.
ent.execution = {
process_id = 0,
-- Queue of processes
process_queue = {},
-- State process
state_process = {},
-- Whether state process was changed or not
state_process_changed = false,
-- Whether to enable process execution or not
enable = true,
-- Interval to run process queue scheduler
scheduler_interval = 1,
-- Timer for next scheduler interval
scheduler_timer = 0,
-- Monitor environment executes timers and registered callbacks
monitor = {
timer = {},
callback = {
to_execute = {}
},
enabled = true
}
}
-- NPC permanent storage for data
ent.data = {}
-- State date
ent.npc_state = {
-- This table defines the types of interaction the NPC is performing
interaction = {
dialogues = {
is_in_dialogue = false,
in_dialogue_with = "",
in_dialogue_with_name = ""
},
yaw_before_interaction = 0
},
punch = {
last_punch_time = 0,
},
movement = {
is_idle = false,
is_sitting = false,
is_laying = false,
walking = {
is_walking = false,
path = {},
target_pos = {},
}
},
following = {
is_following = false,
following_obj = "",
following_obj_name = ""
}
}
-- This flag is checked on every step. If it is true, the rest of
-- Mobs Redo API is not executed
ent.freeze = nil
-- This map will hold all the places for the NPC
-- Map entries should be like: "bed" = {x=1, y=1, z=1}
ent.places_map = {}
-- Schedule data
ent.schedules = {
-- Flag to enable or disable the schedules functionality
enabled = true,
-- Lock for when executing a schedule
lock = -1,
-- Queue of programs in schedule to be enqueued
-- Used to calculate dependencies
dependency_queue = {},
-- An array of schedules, meant to be one per day at some point
-- when calendars are implemented. Allows for only 7 schedules,
-- one for each day of the week
generic = {},
-- An array of schedules, meant to be for specific dates in the
-- year. Can contain as many as possible. The keys will be strings
-- in the format MM:DD
date_based = {},
-- The following holds the check parameters provided by the
-- current schedule
current_check_params = {}
}
-- If occupation name given, override properties with
-- occupation values and initialize schedules
if occupation_name and occupation_name ~= "" and ent.age == npc.age.adult then
-- Set occupation name
ent.occupation_name = occupation_name
-- Override relevant values
npc.occupations.initialize_occupation_values(ent, occupation_name)
end
-- Nametag is initialized to blank
ent.nametag = ""
-- Set name
if npc_info and npc_info.name then
if npc_info.name.value then
ent.npc_name = npc_info.name.value
elseif npc_info.name.tags then
ent.npc_name = get_random_name(ent.gender, npc_info.name.tags)
else
ent.npc_name = get_random_name(ent.gender)
end
else
ent.npc_name = get_random_name(ent.gender)
end
-- Set ID
ent.npc_id = tostring(math.random(1000, 9999))..":"..ent.npc_name
-- Generate trade offers
npc.trade.generate_trade_offers_by_status(ent)
-- Set initialized flag on
ent.initialized = true
--npc.log("WARNING", "Spawned entity: "..dump(ent))
npc.log("INFO", "Successfully initialized NPC with name "..dump(ent.npc_name)
..", gender: "..ent.gender..", is child: "..dump(ent.is_child)
..", texture: "..dump(ent.textures))
-- Refreshes entity
ent.object:set_properties(ent)
end
---------------------------------------------------------------------------------------
-- Trading functions
---------------------------------------------------------------------------------------
function npc.generate_trade_list_from_inventory(self)
local list = {}
for i = 1, #self.inventory do
list[npc.get_item_name(self.inventory[i])] = {}
end
self.trader_data.trade_list = list
end
function npc.set_trading_status(self, status)
-- Stop, if any, the casual offer regeneration timer
npc.monitor.timer.stop(self, "advanced_npc:trade:casual_offer_regeneration")
--minetest.log("Trader_data: "..dump(self.trader_data))
-- Set status
self.trader_data.trader_status = status
-- Check if status is casual
if status == npc.trade.CASUAL then
-- Register timer for changing casual trade offers
local timer_reg_success = npc.monitor.timer.register(self, "advanced_npc:trade:casual_offer_regeneration", 60,
function(self)
-- Re-select casual trade offers
npc.trade.generate_trade_offers_by_status(self)
end)
if timer_reg_success == false then
-- Activate timer instead
npc.monitor.timer.start(self, "advanced_npc:trade:casual_offer_regeneration")
end
end
-- Re-generate trade offers
npc.trade.generate_trade_offers_by_status(self)
end
---------------------------------------------------------------------------------------
-- Inventory functions
---------------------------------------------------------------------------------------
-- NPCs inventories are restrained to 16 slots.
-- Each slot can hold one item up to 99 count.
-- Utility function to get item name from a string
function npc.get_item_name(item_string)
return ItemStack(item_string):get_name()
end
-- Utility function to get item count from a string
function npc.get_item_count(item_string)
return ItemStack(item_string):get_count()
end
-- Add an item to inventory. Returns true if add successful
-- These function can be used to give items to other NPCs
-- given that the "self" variable can be any NPC
function npc.add_item_to_inventory(self, item_name, count)
-- Check if NPC already has item
local existing_item = npc.inventory_contains(self, item_name)
if existing_item ~= nil and existing_item.item_string ~= nil then
-- NPC already has item. Get count and see
local existing_count = npc.get_item_count(existing_item.item_string)
if (existing_count + count) < npc.INVENTORY_ITEM_MAX_STACK then
-- Set item here
self.inventory[existing_item.slot] =
npc.get_item_name(existing_item.item_string).." "..tostring(existing_count + count)
return true
else
--Find next free slot
for i = 1, #self.inventory do
if self.inventory[i] == "" then
-- Found slot, set item
self.inventory[i] =
item_name.." "..tostring((existing_count + count) - npc.INVENTORY_ITEM_MAX_STACK)
return true
end
end
-- No free slot found
return false
end
else
-- Find a free slot
for i = 1, #self.inventory do
if self.inventory[i] == "" then
-- Found slot, set item
self.inventory[i] = item_name.." "..tostring(count)
return true
end
end
-- No empty slot found
return false
end
end
-- Same add method but with itemstring for convenience
function npc.add_item_to_inventory_itemstring(self, item_string)
local item_name = npc.get_item_name(item_string)
local item_count = npc.get_item_count(item_string)
npc.add_item_to_inventory(self, item_name, item_count)
end
-- Checks if an item is contained in the inventory. Returns
-- the item string or nil if not found
function npc.inventory_contains(self, item_name)
for key,value in pairs(self.inventory) do
if value ~= "" and string.find(value, item_name) then
return {slot=key, item_string=value}
end
end
-- Item not found
return nil
end
-- Removes the item from an NPC inventory and returns the item
-- with its count (as a string, e.g. "default:apple 2"). Returns
-- nil if unable to get the item.
function npc.take_item_from_inventory(self, item_name, count)
local existing_item = npc.inventory_contains(self, item_name)
if existing_item ~= nil then
-- Found item
local existing_count = npc.get_item_count(existing_item.item_string)
local new_count = existing_count
if existing_count - count < 0 then
-- Remove item first
self.inventory[existing_item.slot] = ""
-- TODO: Support for retrieving from next stack. Too complicated
-- and honestly might be unecessary.
return item_name.." "..tostring(new_count)
else
new_count = existing_count - count
if new_count == 0 then
self.inventory[existing_item.slot] = ""
else
self.inventory[existing_item.slot] = item_name.." "..new_count
end
return item_name.." "..tostring(count)
end
else
-- Not able to take item because not found
return nil
end
end
-- Same take method but with itemstring for convenience
function npc.take_item_from_inventory_itemstring(self, item_string)
local item_name = npc.get_item_name(item_string)
local item_count = npc.get_item_count(item_string)
npc.take_item_from_inventory(self, item_name, item_count)
end
---------------------------------------------------------------------------------------
-- Flag functionality
---------------------------------------------------------------------------------------
-- TODO: Consider removing them as they are pretty simple and straight forward.
-- Generic variables or function that help drive some functionality for the NPC.
function npc.add_flag(self, flag_name, value)
self.flags[flag_name] = value
end
function npc.update_flag(self, flag_name, value)
self.flags[flag_name] = value
end
function npc.get_flag(self, flag_name)
return self.flags[flag_name]
end
---------------------------------------------------------------------------------------
-- Dialogue functionality
---------------------------------------------------------------------------------------
function npc.start_dialogue(self, clicker, show_married_dialogue)
-- Call dialogue function as normal
npc.dialogue.start_dialogue(self, clicker, show_married_dialogue)
-- Check and update relationship if needed
npc.relationships.dialogue_relationship_update(self, clicker)
end
---------------------------------------------------------------------------------------
-- State functionality
---------------------------------------------------------------------------------------
-- All the self.npc_state variables are used to track the state of the NPC, and
-- if necessary, restore it back in case of changes. The following functions allow
-- to set different aspects of the state.
function npc.set_movement_state(self, args)
self.npc_state.movement.is_idle = args.is_idle or false
self.npc_state.movement.is_sitting = args.is_sitting or false
self.npc_state.movement.is_laying = args.is_laying or false
self.npc_state.movement.walking.is_walking = args.is_walking or false
end
---------------------------------------------------------------------------------------
-- Execution API
---------------------------------------------------------------------------------------
-- Methods for:
-- - Enqueue a program
-- - Set a program as the `state` process
-- - Execute next process in queue
-- - Pause/restore current process
-- - Process scheduling
-- - Get the current process data
-- - Create, read, write and update variables in current process
-- - Enqueue and execute instructions for the current process
-- Global namespace
npc.exec = {
var = {},
proc = {
instr = {}
}
}
-- Private namespace
local _exec = {
proc = {}
}
-- Process states
npc.exec.proc.state = {
INACTIVE = "inactive",
RUNNING = "running",
EXECUTING = "executing",
PAUSED = "paused",
WAITING_USER_INPUT = "waiting_user_input",
READY = "ready"
}
npc.exec.proc.instr.state = {
INACTIVE = "inactive",
EXECUTING = "executing",
INTERRUPTED = "interrupted"
}
-- This function sets the interrupt options as given from the `interrupt_options`
-- table. This table can have the following values:
-- - allow_punch, boolean
-- - allow_rightclick, boolean
-- - allow_schedule, boolean
function npc.exec.create_interrupt_options(interrupt_options)
local interrupt_options = interrupt_options or {}
if next(interrupt_options) ~= nil then
local allow_punch = interrupt_options.allow_punch
local allow_rightclick = interrupt_options.allow_rightclick
local allow_schedule = interrupt_options.allow_schedule
-- Set defaults
if allow_punch == nil then allow_punch = true end
if allow_rightclick == nil then allow_rightclick = true end
if allow_schedule == nil then allow_schedule = true end
return {
allow_punch = allow_punch,
allow_rightclick = allow_rightclick,
allow_schedule = allow_schedule
}
else
return {
allow_punch = true,
allow_rightclick = true,
allow_schedule = true
}
end
end
function _exec.get_new_process_id(self)
self.execution.process_id = self.execution.process_id + 1
if self.execution.process_id > 10000 then
self.execution.process_id = 0
end
return self.execution.process_id
end
function _exec.create_process_entry(program_name, arguments, interrupt_options, is_state_program, process_id)
return {
id = process_id,
program_name = program_name,
arguments = arguments,
state = npc.exec.proc.state.INACTIVE,
execution_context = {
data = {},
instr_interval = 1,
instr_timer = 0
},
instruction_queue = {},
current_instruction = {
entry = {},
state = npc.exec.proc.instr.state.INACTIVE,
pos = {}
},
interrupt_options = npc.exec.create_interrupt_options(interrupt_options),
interrupted_process = {},
is_state_process = is_state_program
}
end
-- This function creates a process for the given program, and
-- places it into the process queue.
function npc.exec.enqueue_program(self, program_name, arguments, interrupt_options, is_state_program)
if is_state_program == nil then
is_state_program = false
end
if is_state_program == true then
npc.exec.set_state_program(self, program_name, arguments, interrupt_options)
-- Enqueue state process
self.execution.process_queue[#self.execution.process_queue + 1] = self.execution.state_process
else
-- Enqueue process
self.execution.process_queue[#self.execution.process_queue + 1] =
_exec.create_process_entry(program_name, arguments, interrupt_options, is_state_program, _exec.get_new_process_id(self))
end
end
-- This function creates a state process. The state process will execute
-- everytime there's no other process executing
function npc.exec.set_state_program(self, program_name, arguments, interrupt_options)
-- Disable monitor - give a chance to this state process to do what it has to do
self.execution.monitor.enabled = false
-- This flag signals the state process was changed and scheduler needs to consume
self.execution.state_process_changed = true
self.execution.state_process = {
program_name = program_name,
arguments = arguments,
state = npc.exec.proc.state.INACTIVE,
execution_context = {
data = {},
instr_interval = 1,
instr_timer = 0
},
instruction_queue = {},
current_instruction = {
entry = {},
state = npc.exec.proc.instr.state.INACTIVE,
pos = {}
},
interrupt_options = npc.exec.create_interrupt_options(interrupt_options),
is_state_process = true,
state_process_id = os.time()
}
end
-- Convenience function that returns first process in the queue
function npc.exec.get_current_process(self)
local result = self.execution.process_queue[1]
if result then
if next(result) == 0 then
return nil
end
end
return result
end
-- This function always execute the process at the start of the process
-- queue. When a process is stopped (because its instruction queue is empty
-- or because the process itself stops), the entry is removed from the
-- process queue, and thus the next process to execute will be the first one
-- in the queue.
function npc.exec.execute_process(self)
local current_process = self.execution.process_queue[1]
-- Execute current process
if current_process then
-- Restore scheduler interval
self.execution.scheduler_interval = 1
if not current_process.is_state_process then
npc.log("EXECUTION", "NPC "..dump(self.npc_name).." is executing: "..dump(current_process.program_name))
end
current_process.state = npc.exec.proc.state.EXECUTING
npc.programs.execute(self, current_process.program_name, current_process.arguments)
current_process.state = npc.exec.proc.state.RUNNING
-- Re-enable monitor
if current_process.is_state_process then