starlit  Check-in [abe3882d1c]

Overview
Comment:initial commit
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: abe3882d1c8cf5dbf62af7dd295134ebc8a50fa7472c214f15fe58092d50ad08
User & Date: lexi on 2024-03-29 20:50:28
Other Links: manifest | tags
Context
2024-03-29
22:43
fix batteries, silence debug noise check-in: 6469428393 user: lexi tags: trunk
20:50
initial commit check-in: abe3882d1c user: lexi tags: trunk
18:29
initial empty check-in check-in: 10ecdc14d9 user: lexi tags: trunk
Changes

Added dev.ct version [3b1604bc1d].







































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# starsoul development
this file contains information meant for those who wish to develop for Starsoul or build the game from trunk. do NOT add any story information, particularly spoilers; those go in src/lore.ct.

## tooling
starsoul uses the following software in the development process:
* [*csound] to generate sound effects
* [*GNU make] to automate build tasks
* [*lua] to automate configure tasks

## building
to run a trunk version of Starsoul, you'll need to install the above tools and run `make` from the base directory. this will:
* run lua scripts to generate necessary makefiles
* generate the game sound effects and install them in mods/starsoul/sounds

## policy
* copyright of all submitted code must be reassigned to the maintainer.
* all code is to be indented with tabs and aligned with spaces; formatting is otherwise up to whoever is responsible for maintaining that code
* use [`camelCase], not [`snake_case] and CERTAINLY not [`SCREAMING_SNAKE_CASE]
* sounds effects should be contributed in the form of csound files; avoid adding audio files to the repository except for foley effects

Added game.conf version [d6b8d1d1d1].











>
>
>
>
>
1
2
3
4
5
title = Starsoul
author = velartrill
description = High-tech survival on a hostile alien world
allowed_mapgens = v7
disabled_settings = !enable_damage, creative_mode

Added mods/starsoul-building/init.lua version [a42549c83d].















































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local lib = starsoul.mod.lib
local B = {}
starsoul.mod.building = B

B.path = {}
-- this maps stage IDs to tables of the following form
--[[ {
	part = {
		['starsoul_building:pipe'] = 'myMod:stage3';
	};
	tool = {
		['starsoul:scredriver'] = 'myMod:otherThing_stage1';
		['starsoul:saw'] = function(node, tool)
			minetest.replace_node(node, {name='myMod:stage1'})
			minetest.drop_item(node, 'starsoul_building:pipe')
		end;
		['myMod:laserWrench'] = {
			allow = function(node, tool) ... end;
			handle = function(node, tool) ... end;
		};
	};
} ]]
-- it should only be written by special accessor functions!

B.stage = lib.registry.mk 'starsoul_building:stage'
-- a stage consists of a list of pieces and maps from possible materials
-- / tool usages to succeeding stages in the build tree. note that all
-- used pieces must be defined before a stage is defined currently, due
-- to a lack of cross-registry dependency mechanisms. i will hopefully
-- improve vtlib to handle this condition eventually.
--[[
	starsoul.mod.building.stage.link(id, {
		pieces = {
			'starsoul_building:foundation';
			'starsoul_building:insulation'; -- offset             ofsFac
			{'starsoul_building:pipe',      vector.new(-.5, 0, 0), 0};--
			{'starsoul_building:pipe',      vector.new(-.5, 0, 0), 1};
			'starsoul_building:insulation';
			'starsoul_building:panel';
		};
	})
]]

B.piece = lib.registry.mk 'starsoul_building:piece'
-- a piece is used to produce stage definitions, by means of appending
-- nodeboxes with appropriate offsets. it also lists the recoverable
-- materials which can be obtained by destroying a stage containing
-- this piece using nano. part IDs should correspond with piece IDs
-- where possible
--[[
	starsoul.mod.building.piece.link(id, {
		tex = 'myMod_part.png';
		height = 0.1; -- used for auto-offset
		fab = {
			element = {iron=10};
		};	
		shape = {
			type = "fixed";
			fixed = { ... };
		};
	})
]]

B.part = lib.registry.mk 'starsoul_building:part'
-- a part is implemented as a special craftitem with the proper callbacks
-- to index the registries and place/replace noes by reference to the
-- build tree.
--[[
	starsoul.mod.building.part.link(id, {
		name = ''; -- display name
		desc = ''; -- display desc
		img = ''; -- display image
	})
]]

B.stage.foreach('starsoul:stageGen', {}, function(id, e)
	local box = {type = 'fixed', fixed = {}}
	local tex = {}
	local ofs = vector.new(0,0,0)
	for idx, p in ipairs(e.pieces) do
		local ho, pieceID, pos
		if type(p) == 'string' then
			pieceID, pos, ho = p, vector.zero(), 1.0
		else
			pieceID, pos, ho = pc[1],pc[2],pc[3]
		end
		local pc = B.piece.db[pieceID]
		pos = pos + ofs
		if ho ~= 0.0 then
			ofs = vector.offset(ofs, 0, pc.height)
		end
		local sh = lib.node.boxwarped(pc.shape, function(b)
			-- { -x, -y, -z;
			--   +x, +y, +z }
			b[1] = b[1] + ofs.x  b[4] = b[4] + ofs.x
			b[2] = b[2] + ofs.y  b[5] = b[5] + ofs.y
			b[3] = b[3] + ofs.z  b[6] = b[6] + ofs.z
		end)
		table.insert(box, sh)
		if type(pc.tex) == 'string' then
			table.insert(tex, pc.tex)
		else
			for i,t in ipairs(pc.tex) do
				table.insert(tex, t or '')
			end
		end
	end
	minetest.register_node(id, {
		description = 'Construction';
		drawtype = 'nodebox';
		paramtype  = 'light';
		paramtype2 = e.stateful or 'none';
		textures = tex;
		node_box = box;
		group = { stage = 1 };
		_starsoul = {
			stage = id;
		};
	})
end)

function B.pathLink(from, kind, what, to)
	if not B.path[from] then
		B.path[from] = {part={}, tool={}}
	end
	local k = B.path[from][kind]
	assert(k[what] == nil)
	k[what] = to
end

function B.pathFind(from, kind, what)
	if not B.path[from] then return nil end
	return B.path[from][kind][what]
end

Added mods/starsoul-building/mod.conf version [41dba78d03].







>
>
>
1
2
3
name = starsoul_building
depends = starsoul_electronics, starsoul
description = implements construction elements

Added mods/starsoul-electronics/init.lua version [e59f2eda8d].

















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local lib = starsoul.mod.lib

local E = {}
starsoul.mod.electronics = E

---------------------
-- item registries --
---------------------

-- a dynamo is any item that produces power and can be slotted into a power
-- source slot. this includes batteries, but also things like radiothermal
-- dynamos.
starsoul.item.dynamo = lib.registry.mk 'starsoul_electronics:dynamo'

-- batteries hold a charge of power (measured in kJ). how much they can hold
-- (and how much power they can discharge?) depends on their quality
starsoul.item.battery = lib.registry.mk 'starsoul_electronics:battery'

-- a battery has the properties:
--  class
--	 |- capacity (J ): amount of energy the battery can hold
--	 |- dischargeRate  (W ): rate at which battery can supply power/be charged
--	 |- decay   (J/J): rate at which the battery capacity degrades while
--	                   discharging. decay=0 batteries require no maintenance;
--	                   decay=1 batteries are effectively disposable
--	 |- leak    (fac): charging inefficiency. depends on the energy storage
--	                   technology. when N J are drawn from a power source,
--	                   only (N*leak) J actually make it into the battery.
--	                   leak=0 is a supercapacitor, leak=1 is /dev/null
--	 |- size      (m): each suit has a limit to how big of a battery it can take
--	instance
--	 |- degrade (mJ): how much the battery has degraded. instance max charge is
--	 |                determined by $capacity - @degrade
--	 |- %wear ÷ 2¹⁶ : used as a factor to determine battery charge

-- chips are standardized data storage hardware that can contain a certain amount
-- of software. in addition to their flash storage, they also provide a given amount
-- of working memory and processor power. processor power speeds up operations like
-- crafting, while programs require a certain amount of memory.
-- chips have a variable number of program slots and a single bootloader slot
--
starsoul.item.chip = lib.registry.mk 'starsoul_electronics:chip'

-- software is of one of the following types:
--   schematic: program for your matter compiler that enables crafting a given item.
--       output: the result
--   driver: inserted into a Core to control attached hardware
--     suitPower: provides suit functionality like nanoshredding or healing
--                passive powers are iterated on suit application/configuration and upon fst-tick
--   cost: what the software needs to run. some fields are fab-specific
--		   energy: for fab, total energy cost of process in joules
--               for suitPassive, added suit power consumption in watts
starsoul.item.sw = lib.registry.mk 'starsoul_electronics:sw'
-- chip    = lib.color(0, 0, .3);

E.schematicGroups = lib.registry.mk 'starsoul_electronics:schematicGroups'
E.schematicGroupMembers = {}
E.schematicGroups.foreach('starsoul_electronics:ensure-memlist', {}, function(id,g)
	E.schematicGroupMembers[id] = {}
end)
function E.schematicGroupLink(group, item)
	table.insert(E.schematicGroupMembers[group], item)
end

E.schematicGroups.link('starsoul_electronics:chip', {
	title = 'Chip', icon = 'starsoul-item-chip.png';
	description = 'Standardized data storage and compute modules';
})

E.schematicGroups.link('starsoul_electronics:battery', {
	title = 'Battery', icon = 'starsoul-item-battery.png';
	description = 'Portable power storage cells are essential to all aspects of survival';
})

E.schematicGroups.link('starsoul_electronics:decayCell', {
	title = 'Decay Cell', icon = 'starsoul-item-decaycell.png';
	description = "Radioisotope generators can pack much more power into a smaller amount of space than conventional battery, but they can't be recharged, dump power and heat whether they're in use or not, and their power yield drop towards zero over their usable lifetime.";
})


-------------------------
-- batteries & dynamos --
-------------------------

E.battery = {}
local function accessor(ty, fn)
	return function(stack, ...)
		local function fail()
			error(string.format('object %q is not a %s', stack:get_name(), ty))
		end

		if not stack or stack:is_empty() then fail() end

		if minetest.get_item_group(stack:get_name(), ty) == 0 then fail() end

		return fn(stack,
		          stack:get_definition()._starsoul[ty],
		          stack:get_meta(), ...)
	end
end

-- return a wear level that won't destroy the item
local function safeWear(fac) return math.min(math.max(fac,0),1) * 0xFFFE end
local function safeWearToFac(w) return w/0xFFFE end

-- E.battery.capacity(bat) --> charge (J)
E.battery.capacity = accessor('battery', function(stack, batClass, meta)
	local dmg = meta:get_int('starsoul_electronics:battery_degrade') -- µJ/μW
	local dmg_J = dmg / 1000
	return (batClass.capacity - dmg_J)
end)

-- E.battery.charge(bat) --> charge (J)
E.battery.charge = accessor('battery', function(stack, batClass, meta)
	local fac = 1 - safeWearToFac(stack:get_wear())
	return E.battery.capacity(stack) * fac
end)

-- E.battery.dischargeRate(bat) --> dischargeRate (W)
E.battery.dischargeRate = accessor('battery', function(stack, batClass, meta)
	local dmg = meta:get_int('starsoul_electronics:battery_degrade') -- µJ/μW
	local dmg_W = dmg / 1000
	return batClass.dischargeRate - dmg_W
end);


-- E.battery.drawCurrent(bat, power, time, test) --> supply (J), wasteHeat (J)
--       bat   = battery stack
--     power J = joules of energy user wishes to consume
--      time s = the amount of time available for this transaction
--    supply J = how much power was actually provided in $time seconds
-- wasteHeat J = how heat is generated in the process
--      test   = if true, the battery is not actually modified
E.battery.drawCurrent = accessor('battery', function(s, bc, m, power, time, test)
	local ch = E.battery.charge(s)
	local maxPower = math.min(E.battery.dischargeRate(s)*time, power, ch)
	ch = ch - maxPower

	if not test then
		local degrade = m:get_int 'starsoul_electronics:battery_degrade' or 0
		degrade = degrade + maxPower * bc.decay
		-- for each joule of power drawn, capacity degrades by `decay` J
		-- this should ordinarily be on the order of mJ or smaller
		m:set_int('starsoul_electronics:battery_degrade', degrade)
		s:set_wear(safeWear(1 - (ch / E.battery.capacity(s))))
	end

	return maxPower, 0 -- FIXME specify waste heat
end)

-- E.battery.recharge(bat, power, time) --> draw (J)
--     bat   = battery stack
--   power J = joules of energy user wishes to charge the battery with
--    time s = the amount of time available for this transaction
--    draw J = how much power was actually drawn in $time seconds
E.battery.recharge = accessor('battery', function(s, bc, m, power, time)
	local ch = E.battery.charge(s)
	local cap = E.battery.capacity(s)
	local maxPower = math.min(E.battery.dischargeRate(s)*time, power)
	local total = math.min(ch + maxPower, cap)
	s:set_wear(safeWear(1 - (total/cap)))
	return maxPower, 0 -- FIXME
end)

E.battery.setCharge = accessor('battery', function(s, bc, m, newPower)
	local cap = E.battery.capacity(s)
	local power = math.min(cap, newPower)
	s:set_wear(safeWear(1 - (power/cap)))
end)

E.dynamo = { kind = {} }

E.dynamo.drawCurrent = accessor('dynamo', function(s,c,m, power, time, test)
	return c.vtable.drawCurrent(s, power, time, test)
end)
E.dynamo.totalPower    = accessor('dynamo', function(s,c,m) return c.vtable.totalPower(s) end)
E.dynamo.dischargeRate = accessor('dynamo', function(s,c,m) return c.vtable.dischargeRate   (s) end)
E.dynamo.initialPower  = accessor('dynamo', function(s,c,m) return c.vtable.initialPower(s) end)
E.dynamo.wasteHeat     = accessor('dynamo', function(s,c,m) return c.vtable.wasteHeat(s) end)
-- baseline waste heat, produced whether or not power is being drawn. for batteries this is 0, but for
-- radiothermal generators it may be high

E.dynamo.kind.battery = {
	drawCurrent = E.battery.drawCurrent;
	totalPower = E.battery.charge;
	initialPower = E.battery.capacity;
	dischargeRate = E.battery.dischargeRate;
	wasteHeat = function() return 0 end;
};

starsoul.item.battery.foreach('starsoul_electronics:battery-gen', {}, function(id, def)
	minetest.register_tool(id, {
		short_description = def.name;
		groups = { battery = 1; dynamo = 1; electronic = 1; };
		inventory_image = def.img or 'starsoul-item-battery.png';
		description = starsoul.ui.tooltip {
			title = def.name;
			desc = def.desc;
			color = lib.color(0,.2,1);
			props = {
				{ title = 'Optimal Capacity', affinity = 'info';
					desc = lib.math.si('J', def.capacity) };
				{ title = 'Discharge Rate', affinity = 'info';
					desc = lib.math.si('W', def.dischargeRate) };
				{ title = 'Charge Efficiency', affinity = 'info';
					desc = string.format('%s%%', (1-def.leak) * 100) };
				{ title = 'Size', affinity = 'info';
					desc = lib.math.si('m', def.fab.size.print) };
			};
		};
		_starsoul = {
			event = {
				create = function(st, how)
					if not how.gift then -- cheap hack to make starting batteries fully charged
						E.battery.setCharge(st, 0)
					end
				end;
			};
			fab = def.fab;
			dynamo = {
				vtable = E.dynamo.kind.battery;
			};
			battery = def;
		};
	})
end)


-- to use the power functions, consider the following situation. you have
-- a high-tier battery charger that can draw 100kW. (for simplicity, assume
-- it supports only one battery). if you install a low-tier battery, and
-- the charging callback is called every five seconds, you might use
-- a `recharge` call that looks like
--
--   starsoul.mod.electronics.battery.recharge(bat, 5 * 100*1e4, 5)
--
-- this would offer the battery 500kJ over five seconds. the battery will
-- determine how much power it can actually make use of in 5 five seconds,
-- and then return that amount.
--
-- always remember to save the battery back to its inventory slot after
-- modifying its ItemStack with one of these functions!


-- battery types
-- supercapacitor: low capacity, no degrade, high dischargeRate, no leak
-- chemical: high capacity, high degrade, mid dischargeRate, low leak

-- battery tiers
-- makeshift: cheap, weak, low quality
-- imperial ("da red wunz go fasta"): powerful, low quality
-- commune ("snooty sophisticates"): limited power, high quality, expensive
-- usukwinya ("value engineering"): high power, mid quality, affordable
-- eluthrai ("uncompromising"): high power, high quality, wildly expensive
-- firstborn ("god-tier"): exceptional

local batteryTiers = {
	makeshift = {
		name = 'Makeshift'; capacity = .5, decay = 3, leak = 2, dischargeRate = 1,
		fab = starsoul.type.fab {
			metal = {copper=10};
		};
		desc = "Every attosecond this electrical abomination doesn't explode in your face is but the unearned grace of the Wild Gods.";
		complexity = 1;
		sw = {rarity = 1};
	};
	imperial  = {
		name = 'Imperial'; capacity = 2, decay = 2, leak = 2, dischargeRate = 2; 
		fab = starsoul.type.fab {
			metal = {copper=15, iron = 20};
			size = { print = 0.1 };
		};
		desc = "The Empire's native technology is a lumbering titan: bulky, inefficient, unreliable, ugly, and awesomely powerful. Their batteries are no exception, with raw capacity and throughput that exceed even Usukinwya designs.";
		drm = 1;
		complexity = 2;
		sw = {rarity = 2};
	};
	commune   = {
		name = 'Commune'; capacity = 1, decay = .5, leak = .2, dischargeRate = 1; 
		fab = starsoul.type.fab {
			metal = {vanadium=50, steel=10};
			size = { print = 0.05 };
		};
		desc = "The Commune's proprietary battery designs prioritize reliability, compactness, and maintenance concerns above raw throughput, with an elegance of engineering and design that would make a Su'ikuri cry.";
		complexity = 5;
		sw = {rarity = 3};
	};
	usukwinya = {
		name = 'Usukwinya'; capacity = 2, decay = 1, leak = 1, dischargeRate = 1.5,
		fab = starsoul.type.fab {
			metal = {vanadium=30, argon=10};
			size = { print = 0.07 };
		};
		desc = "A race of consummate value engineers, the Usukwinya have spent thousands of years refining their tech to be as cheap to build as possible, without compromising much on quality. The Tradebirds drive an infamously hard bargain, but their batteries are more than worth their meagre cost.";
		drm = 2;
		sw = {rarity = 10};
		complexity = 15;
	};
	eluthrai  = {
		name = 'Eluthrai'; capacity = 3, decay = .4, leak = .1, dischargeRate = 1.5,
		fab = starsoul.type.fab {
			metal = {beryllium=20, platinum=20, technetium = 1, cinderstone = 10 };
			size = { print = 0.03 };
		};
		desc = "The uncompromising Eluthrai are never satisfied until every quantifiable characteristic of their tech is maximally optimised down to the picoscale. Their batteries are some of the best in the Reach, and unquestionably the most expensive -- especially for those lesser races trying to copy the designs without the benefit of the sublime autofabricator ecosystem of the Eluthrai themselves.";
		complexity = 200;
		sw = {rarity = 0}; -- you think you're gonna buy eluthran schematics on SuperDiscountNanoWare.space??
	};
	firstborn = {
		name = 'Firstborn'; capacity = 5, decay = 0.1, leak = 0, dischargeRate = 3;
		fab = starsoul.type.fab {
			metal = {neodymium=20, xenon=150, technetium=5, sunsteel = 10 };
			crystal = {astrite = 1};
			size = { print = 0.05 };
		};
		desc = "Firstborn engineering seamlessly merges psionic effects with a mastery of the physical universe unattained by even the greatest of the living Starsouls. Their batteries reach levels of performance that strongly imply Quantum Gravity Theory -- and several major holy books -- need to be rewritten. From the ground up.";
		complexity = 1000;
		sw = {rarity = 0}; -- lol no
	};
}

local batterySizes = {
	small = {name = 'Small', capacity = .5, dischargeRate =  .5, complexity = 1, matMult = .5, fab = starsoul.type.fab {size={print=0.1}}};
	mid   = {                capacity =  1, dischargeRate =   1, complexity = 1, matMult = 1, fab = starsoul.type.fab {size={print=0.3}}};
	large = {name = 'Large', capacity =  2, dischargeRate = 1.5, complexity = 1, matMult = 1.5, fab = starsoul.type.fab {size={print=0.5}}};
	huge  = {name = 'Huge',  capacity =  3, dischargeRate =   2, complexity = 1, matMult = 2, fab = starsoul.type.fab {size={print=0.8}}};
}

local batteryTypes = {
	supercapacitor = {
		name = 'Supercapacitor';
		desc = 'Room-temperature superconductors make for very reliable, high-dischargeRate, but low-capacity batteries.';
		fab = starsoul.type.fab {
			metal = { enodium = 5 };
			size = {print=0.8};
		};
		sw = {
			cost = {
				cycles = 5e9; -- 5 bil cycles
				ram = 10e9; -- 10GB
			};
			pgmSize = 2e9; -- 2GB
			rarity = 5;
		};
		capacity = 50e3, dischargeRate = 1000;
		leak = 0, decay = 1e-6;

		complexity = 3;
	};
	chemical = {
		name = 'Chemical';
		desc = '';
		fab = starsoul.type.fab {
			element = { lithium = 3};
			metal = {iron = 5};
			size = {print=1.0};
		};
		sw = {
			cost = {
				cycles = 1e9; -- 1 bil cycles
				ram = 2e9; -- 2GB
			};
			pgmSize = 512e6; -- 512MB
			rarity = 2;
		};
		capacity = 200e3, dischargeRate = 200;
		leak = 0.2, decay = 1e-2;
		complexity = 1;
	};
	carbon = {
		name = 'Carbon';
		desc = 'Carbon nanotubes form the basis of many important metamaterials, chief among them power-polymer.';
		capacity = 1;
		fab = starsoul.type.fab {
			element = { carbon = 40 };
			size = {print=0.5};
		};
		sw = {
			cost = {
				cycles = 50e9; -- 50 bil cycles
				ram = 64e9; -- 64GB
			};
			pgmSize = 1e9; -- 1GB
			rarity = 10;
		};
		capacity = 100e3, dischargeRate = 500;
		leak = 0.1, decay = 1e-3;
		complexity = 10;
	};
	hybrid = {
		name = 'Hybrid';
		desc = '';
		capacity = 1;
		fab = starsoul.type.fab {
			element = {
				lithium = 3;
			};
			metal = {
				iron = 5;
			};
			size = {print=1.5};
		};
		sw = {
			cost = {
				cycles = 65e9; -- 65 bil cycles
				ram = 96e9; -- 96GB
			};
			pgmSize = 5e9; -- 5GB
			rarity = 15;
		};
		capacity = 300e3, dischargeRate = 350;
		leak = 0.3, decay = 1e-5;
		complexity = 30;
	};
}

local function elemath(dest, src, mult)
	dest = dest or {}
	for k,v in pairs(src) do
		if not dest[k] then dest[k] = 0 end
		dest[k] = dest[k] + v*mult
	end
	return dest
end

for bTypeName, bType in pairs(batteryTypes) do
for bTierName, bTier in pairs(batteryTiers) do
for bSizeName, bSize in pairs(batterySizes) do
	-- elemath(elementCost, bType.fab.element or {}, bSize.matMult)
	-- elemath(elementCost, bTier.fab.element or {}, bSize.matMult)
	-- elemath(metalCost, bType.fab.metal or {}, bSize.matMult)
	-- elemath(metalCost, bTier.fab.metal or {}, bSize.matMult)
	local fab = bType.fab + bTier.fab + bSize.fab + starsoul.type.fab {
		element = {copper = 10, silicon = 5};
	}
	local baseID = string.format('battery_%s_%s_%s',
		bTypeName, bTierName, bSizeName)
	local id = 'starsoul_electronics:'..baseID
	local name = string.format('%s %s Battery', bTier.name, bType.name)
	if bSize.name then name = bSize.name .. ' ' .. name end
	local function batStat(s)
		if s == 'size' then
			--return bType.fab[s] * (bTier.fab[s] or 1) * (bSize.fab[s] or 1)
			return fab.size and fab.size.print or 1
		else
			return bType[s] * (bTier[s] or 1) * (bSize[s] or 1)
		end
	end

	local swID = 'starsoul_electronics:schematic_'..baseID
	fab.reverseEngineer = {
		complexity = bTier.complexity * bSize.complexity * bType.complexity;
		sw = swID;
	}
	fab.flag = {print=true}

	starsoul.item.battery.link(id, {
		name = name;
		desc = table.concat({
			bType.desc or '';
			bTier.desc or '';
			bSize.desc or '';
		}, ' ');

		fab = fab;

		capacity = batStat 'capacity';
		dischargeRate  = batStat 'dischargeRate';
		leak     = batStat 'leak';
		decay    = batStat 'decay';
	})

	local rare
	if bType.sw.rarity == 0 or bTier.sw.rarity == 0 then
		-- rarity is measured such that the player has a 1/r
		-- chance of finding a given item, or if r=0, no chance
		-- whatsoever (the sw must be obtained e.g. by reverse-
		-- engineering alien tech)
		rare = 0
	else
		rare = bType.sw.rarity + bTier.sw.rarity
	end

	starsoul.item.sw.link(swID, {
		kind = 'schematic';
		name = name .. ' Schematic';
		output = id;
		size = bType.sw.pgmSize;
		cost = bType.sw.cost;
		rarity = rare;
	})

	E.schematicGroupLink('starsoul_electronics:battery', swID)

end end end


-----------
-- chips --
-----------

E.sw = {}
function E.sw.findSchematicFor(item)
	local id = ItemStack(item):get_name()
	print(id)
	local fm = minetest.registered_items[id]._starsoul
	if not (fm and fm.fab and fm.fab.reverseEngineer) then return nil end
	local id = fm.fab.reverseEngineer.sw
	return id, starsoul.item.sw.db[id]
end

E.chip = { file = {} }
do local T,G = lib.marshal.t, lib.marshal.g
	-- love too reinvent unions from first principles
	E.chip.data = G.struct {
		label = T.str;
		uuid = T.u64;
		files = G.array(16, G.class(G.struct {
			kind = G.enum {
				'sw'; -- a piece of installed software
				'note'; -- a user-readable text file
				'research'; -- saved RE progress
				'genome'; -- for use with plant biosequencer?
				'blob'; -- opaque binary blob, so 3d-pty mods can use the
                    -- file mechanism to store arbirary data.
			};
			drm = T.u8; -- inhibit copying
			name = T.str;
			body = T.text;
		}, function(file) -- enc
			local b = E.chip.file[file.kind].enc(file.body)
			return {
				kind = file.kind;
				drm = file.drm;
				name = file.name;
				body = b;
			}
		end, function(file) -- dec
			local f, ns = E.chip.file[file.kind].dec(file.body)
			file.body = f
			return file, ns
		end));
		bootSlot = T.u8; -- indexes into files; 0 = no bootloader
	}
	E.chip.file.sw = G.struct {
		pgmId = T.str;
		conf = G.array(16, G.struct {
			key = T.str, value = T.str;
		});
	}
	E.chip.file.note = G.struct {
		author = T.str;
		entries = G.array(16, G.struct {
			title = T.str;
			body = T.str;
		});
	}
	E.chip.file.research = G.struct {
		itemId = T.str;
		progress = T.clamp;
	}
	E.chip.file.blob = G.struct {
		kind = T.str; -- MT ID that identifies a blob file type belonging to an external mod
		size = T.u8; -- this must be manually reported since we don't know how to evaluate it
		data = T.text;
	}
	function E.chip.fileSize(file)
		-- boy howdy
		if file.kind == 'blob' then
			return file.body.size
		elseif file.kind == 'note' then
			local sz = 0x10 + #file.body.author
			for _, e in pairs(file.body.entries) do
				sz = sz + #e.title + #e.body + 0x10 -- header overhead
			end
			return sz
		elseif file.kind == 'research' then
			local re = assert(minetest.registered_items[file.body.itemId]._starsoul.fab.reverseEngineer)
			return starsoul.item.sw.db[re.sw].size * file.body.progress
		elseif file.kind == 'sw' then
			return starsoul.item.sw.db[file.body.pgmId].size
		elseif file.kind == 'genome' then
			return 0 -- TODO
		end
	end
	local metaKey = 'starsoul_electronics:chip'
	function E.chip.read(chip)
		local m = chip:get_meta()
		local blob = m:get_string(metaKey)
		if blob and blob ~= '' then
			return E.chip.data.dec(lib.str.meta_dearmor(blob))
		else -- prepare to format the chip
			return {
				label = '';
				bootSlot = 0;
				uuid = math.floor(math.random(0,2^32));
				files = {};
			}
		end
	end
	function E.chip.write(chip, data)
		local m = chip:get_meta()
		m:set_string(metaKey, lib.str.meta_armor(E.chip.data.enc(data)))
		E.chip.update(chip)
	end
	function E.chip.fileOpen(chip, inode, fn)
		local c = E.chip.read(chip)
		if fn(c.files[inode]) then
			E.chip.write(chip, c)
			return true
		end
		return false
	end
	function E.chip.fileWrite(chip, inode, file)
		local c = E.chip.read(chip)
		c.files[inode] = file
		E.chip.write(chip, c)
	end
	function E.chip.usedSpace(chip, d)
		d = d or E.chip.read(chip)
		local sz = 0
		for _, f in pairs(d.files) do
			sz = sz + E.chip.fileSize(f)
		end
		return sz
	end
	function E.chip.freeSpace(chip, d)
		local used = E.chip.usedSpace(chip,d)
		local max = assert(chip:get_definition()._starsoul.chip.flash)
		return max - used
	end
	function E.chip.install(chip, file)
		-- remember to write out the itemstack after using this function!
		local d = E.chip.read(chip)
		if E.chip.freeSpace(chip, d) - E.chip.fileSize(file) >= 0 then
			table.insert(d.files, file)
			E.chip.write(chip, d)
			return true
		else
			return false
		end
	end
end

function E.chip.files(ch)
	local m = ch:get_meta()
	if not m:contains 'starsoul_electronics:chip' then
		return nil
	end
	local data = E.chip.read(ch)
	local f = 0
	return function()
		f = f + 1
		return data.files[f], f
	end
end

function E.chip.describe(ch, defOnly)
	local def, data if defOnly then
		def, data = ch, {}
	else
		def = ch:get_definition()
		local m = ch:get_meta()
		if m:contains 'starsoul_electronics:chip' then
			data = E.chip.read(ch)
		else
			data = {}
			defOnly = true
		end
		def = assert(def._starsoul.chip)
	end
	local props = {
		{title = 'Clock Rate', affinity = 'info';
		 desc  = lib.math.si('Hz', def.clockRate)};
		{title = 'RAM', affinity = 'info';
		 desc  = lib.math.si('B', def.ram)};
	}
	if not defOnly then
		table.insert(props, {
			title = 'Free Storage', affinity = 'info';
			 desc = lib.math.si('B', E.chip.freeSpace(ch, data)) .. ' / '
			     .. lib.math.si('B', def.flash);
		})
		local swAffMap = {
			schematic = 'schematic';
			suitPower = 'ability';
			driver = 'driver';
		}
		for i, e in ipairs(data.files) do
			local aff = 'neutral'
			local name = e.name
			local disabled = false
			if e.kind == 'sw' then
				for _,cf in pairs(e.body.conf) do
					if cf.key == 'disable' and cf.value == 'yes' then
						disabled = true
						break
					end
				end
				local sw = starsoul.item.sw.db[e.body.pgmId]
				aff = swAffMap[sw.kind] or 'good'
				if name == '' then name = sw.name end
			end
			name = name or '<???>'
			table.insert(props, disabled and {
				title = name;
				affinity = aff;
				desc = '<off>';
			} or {
				--title = name;
				affinity = aff;
				desc = name;
			})
		end
	else
		table.insert(props, {
			title = 'Flash Storage', affinity = 'info';
			 desc = lib.math.si('B', def.flash);
		 })
	end
	return starsoul.ui.tooltip {
		title = data.label and data.label~='' and string.format('<%s>', data.label) or def.name;
		color = lib.color(.6,.6,.6);
		desc = def.desc;
		props = props;
	};
end

function E.chip.update(chip)
	chip:get_meta():set_string('description', E.chip.describe(chip))
end

starsoul.item.chip.foreach('starsoul_electronics:chip-gen', {}, function(id, def)
	minetest.register_craftitem(id, {
		short_description = def.name;
		description = E.chip.describe(def, true);
		inventory_image = def.img or 'starsoul-item-chip.png';
		groups = {chip = 1};
		_starsoul = {
			fab = def.fab;
			chip = def;
		};
	})
end)

-- in case other mods want to define their own tiers
E.chip.tiers = lib.registry.mk 'starsoul_electronics:chipTiers'
E.chip.tiers.meld {
	-- GP chips
	tiny    = {name = 'Tiny Chip', clockRate = 512e3, flash = 4096, ram = 1024, powerEfficiency = 1e9, size = 1};
	small   = {name = 'Small Chip', clockRate = 128e6, flash = 512e6, ram = 512e6, powerEfficiency = 1e8, size = 3};
	med     = {name = 'Chip', clockRate = 1e9, flash = 4e9, ram = 4e9, powerEfficiency = 1e7, size = 6};
	large   = {name = 'Large Chip', clockRate = 2e9, flash = 8e9, ram = 8e9, powerEfficiency = 1e6, size = 8};
	-- specialized chips
	compute = {name = 'Compute Chip', clockRate = 4e9, flash = 24e6, ram = 64e9, powerEfficiency = 1e8, size = 4};
	data    = {name = 'Data Chip', clockRate = 128e3, flash = 2e12, ram = 32e3, powerEfficiency = 1e5, size = 4};
	lp      = {name = 'Low-Power Chip', clockRate = 128e6, flash = 64e6, ram = 1e9, powerEfficiency = 1e10, size = 4};
	carbon  = {name = 'Carbon Chip', clockRate = 64e6, flash = 32e6, ram = 2e6, powerEfficiency = 2e9, size = 2, circ='carbon'};
}

E.chip.tiers.foreach('starsoul_electronics:genChips', {}, function(id, t)
	id = t.id or string.format('%s:chip_%s', minetest.get_current_modname(), id)
	local circMat = t.circ or 'silicon';
	starsoul.item.chip.link(id, {
		name = t.name;
		clockRate = t.clockRate;
		flash = t.flash;
		ram = t.ram;
		powerEfficiency = t.powerEfficiency; -- cycles per joule
		fab = {
			flag = {
				silicompile = true;
			};
			time = {
				silicompile = t.size * 24*60;
			};
			cost = {
				energy = 50e3 + t.size * 15e2;
			};
			element = {
				[circMat] = 50 * t.size;
				copper = 30;
				gold = 15;
			};
		};
	})
end)

function E.chip.findBest(test, ...)
	local chip, bestFitness
	for id, c in pairs(starsoul.item.chip.db) do
		local fit, fitness = test(c, ...)
		if fit and (bestFitness == nil or fitness > bestFitness) then
			chip, bestFitness = id, fitness
		end
	end
	return chip, starsoul.item.chip.db[chip], bestFitness
end

function E.chip.findForStorage(sz)
	return E.chip.findBest(function(c)
		return c.flash >= sz, -math.abs(c.flash - sz)
	end)
end

function E.chip.sumCompute(chips)
	local c = {
		cycles = 0;
		ram = 0;
		flashFree = 0;
		powerEfficiency = 0;
	}
	local n = 0
	for _, e in pairs(chips) do
		n = n + 1
		if not e:is_empty() then
			local ch = e:get_definition()._starsoul.chip
			c.cycles = c.cycles + ch.clockRate
			c.ram = c.ram + ch.clockRate
			c.flashFree = c.flashFree + E.chip.freeSpace(e)
			c.powerEfficiency = c.powerEfficiency + ch.powerEfficiency
		end
	end
	if n > 0 then c.powerEfficiency = c.powerEfficiency / n end
	return c
end

E.chip.fileHandle = lib.class {
	__name = 'starsoul_electronics:chip.fileHandle';
	construct = function(chip, inode) -- stack, int --> fd
		return { chip = chip, inode = inode }
	end;
	__index = {
		read = function(self)
			local dat = E.chip.read(self.chip)
			return dat.files[self.inode]
		end;
		write = function(self,data)
			print('writing', self.chip, self.inode)
			return E.chip.fileWrite(self.chip, self.inode, data)
		end;
		erase = function(self)
			local dat = E.chip.read(self.chip)
			table.remove(dat.files, self.inode)
			E.chip.write(self.chip, dat)
			self.inode = nil
		end;
		open = function(self,fn)
			return E.chip.fileOpen(self.chip, self.inode, fn)
		end;
	};
}

function E.chip.usableSoftware(chips,pgm)
	local comp = E.chip.sumCompute(chips)
	local r = {}
	local unusable = {}
	local sw if pgm then
		if type(pgm) == 'string' then
			pgm = {starsoul.item.sw.db[pgm]}
		end
		sw = pgm
	else
		sw = {}
		for i, e in ipairs(chips) do
			if (not e:is_empty())
			   and minetest.get_item_group(e:get_name(), 'chip') ~= 0
			then
				for fl, inode in E.chip.files(e) do
					if fl.kind == 'sw' then
						local s = starsoul.item.sw.db[fl.body.pgmId]
						table.insert(sw, {
							sw = s, chip = e, chipSlot = i;
							file = fl, inode = inode;
						})
					end
				end
			end
		end
	end

	for _, s in pairs(sw) do
		if s.sw.cost.ram <= comp.ram then
			table.insert(r, {
				sw = s.sw;
				chip = s.chip, chipSlot = s.chipSlot;
				file = s.file;
				fd = E.chip.fileHandle(s.chip, s.inode);
				speed = s.sw.cost.cycles / comp.cycles;
				powerCost = s.sw.cost.cycles / comp.powerEfficiency;
				comp = comp;
			})
		else
			table.insert(unusable, {
				sw = s.sw;
				chip = s.chip;
				ramNeeded = s.sw.cost.ram - comp.ram;
			})
		end
	end
	return r, unusable
end

starsoul.include 'sw'

Added mods/starsoul-electronics/mod.conf version [8bb64074c2].







>
>
>
1
2
3
name = starsoul_electronics
description = basic electronic components and logic
depends = starsoul

Added mods/starsoul-electronics/sw.lua version [3b2a0cfbed].



































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
-- [ʞ] sw.lua
--  ~ lexi hale <lexi@hale.su>
--  🄯 EUPL v1.2
--  ? 

-------------------------------
-- basic suit nano abilities --
-------------------------------
local function shredder(prop)
	local function getItemsForFab(fab)
		local elt
		if fab then
			elt = fab:elementalize()
		else
			elt = {}
		end
		local items = {}
		if elt.element then
			for k,v in pairs(elt.element) do
				local st = ItemStack {
					name = starsoul.world.material.element.db[k].form.element;
					count = v;
				}
				table.insert(items, st)
			end
		end
		return items
	end

	return function(user, ctx)
		local function cleanup()
			user.action.prog.shred = nil
			if user.action.sfx.shred then
				minetest.sound_fade(user.action.sfx.shred, 1, 0)
				user.action.sfx.shred = nil
			end
			if user.action.fx.shred then
				user.action.fx.shred.abort()
			end
		end

		if user.action.tgt.type ~= 'node' then return end
		local what = user.action.tgt.under
		if what == nil or user.entity:get_pos():distance(what) > prop.range then
			cleanup()
			return false
		end
		local shredTime = 1.0
		local soundPitch = 1.0 -- TODO
		local pdraw = prop.powerDraw or 0

		local node = minetest.get_node(what)
		local nd = minetest.registered_nodes[node.name]
		local elt, fab, vary
		if nd._starsoul then
			fab = nd._starsoul.recover or nd._starsoul.fab
			vary = nd._starsoul.recover_vary
		end
		if fab then
			if fab.flag then
				if fab.flag.unshreddable then
					cleanup()
					return false
					-- TODO error beep
				end
			end
			shredTime = fab.time and fab.time.shred or shredTime -- FIXME
			if fab.cost and fab.cost.shredPower then
				pdraw = pdraw * fab.cost.shredPower
			end
		end
		local maxW = user:getSuit():maxPowerUse()
		if maxW < pdraw then
			shredTime = shredTime * (pdraw/maxW)
			pdraw = maxW
		end
		if ctx.how.state == 'prog' then
			local pdx = pdraw * ctx.how.delta
			local p = user:suitDrawCurrent(pdx, ctx.how.delta, {kind='nano',label='Shredder'}, pdx)
			if p < pdx then
				cleanup()
				return false
			elseif not user.action.prog.shred then
				cleanup() -- kill danglers
				-- begin
				user.action.prog.shred = 0
				user.action.sfx.shred = minetest.sound_play('starsoul-nano-shred', {
					object = user.entity;
					max_hear_distance = prop.range*2;
					loop = true;
					pitch = soundPitch;
				})
				user.action.fx.shred = starsoul.fx.nano.shred(user, what, prop, shredTime, node)
			else
				user.action.prog.shred = user.action.prog.shred + ctx.how.delta or 0
			end
			--print('shred progress: ', user.action.prog.shred)
			if user.action.prog.shred >= shredTime then
				if minetest.dig_node(what) then
					--print('shred complete')
					user:suitSound 'starsoul-success'
					if fab then
						local vf = fab
						if vary then
							local rng = (starsoul.world.seedbank+0xa891f62)[minetest.hash_node_position(what)]
							vf = vf + vary(rng, {})
						end
						local items = getItemsForFab(vf)
						for i, it in ipairs(items) do user:give(it) end
					end
				else
					user:suitSound 'starsoul-error'
				end
				cleanup()
			end
		elseif ctx.how.state == 'halt' then
			cleanup()
		end
		return true
	end
end

starsoul.item.sw.link('starsoul_electronics:shred', {
	name = 'NanoShred';
	kind = 'suitPower', powerKind = 'active';
	desc = 'An open-source program used in its various forks and iterations all across human-inhabited space and beyond. Rumored to contain fragments of code stolen from the nanoware of the Greater Races by an elusive infoterrorist.';
	size = 500e3;
	cost = {
		cycles = 100e6;
		ram = 500e6;
	};
	run = shredder{range=2, powerDraw=200};
})

starsoul.item.sw.link('starsoul_electronics:compile_commune', {
	name = 'Compile Matter';
	kind = 'suitPower', powerKind = 'direct';
	desc = "A basic suit matter compiler program, rather slow but ruthlessly optimized for power- and memory-efficiency by some of the Commune's most fanatic coders.";
	size = 700e3;
	cost = {
		cycles = 300e6;
		ram = 2e9;
	};
	ui = 'starsoul:compile-matter-component';
	run = function(user, ctx)
	end;
})

starsoul.item.sw.link('starsoul_electronics:compile_block_commune', {
	name = 'Compile Block';
	kind = 'suitPower', powerKind = 'active';
	desc = "An advanced suit matter compiler program, capable of printing complete devices and structure parts directly into the world.";
	size = 5e6;
	cost = {
		cycles = 700e6;
		ram = 4e9;
	};
	ui = 'starsoul:compile-matter-block';
	run = function(user, ctx)
	end;
})

do local J = starsoul.store.compilerJob
	starsoul.item.sw.link('starsoul_electronics:driver_compiler_commune', {
		name = 'Matter Compiler';
		kind = 'driver';
		desc = "A driver for a standalone matter compiler, suitable for building larger components than your suit alone can handle.";
		size = 850e3;
		cost = {
			cycles = 400e6;
			ram = 2e9;
		};
		ui = 'starsoul:device-compile-matter-component';
		run = function(user, ctx)
		end;
		bgProc = function(user, ctx, interval, runState)
			if runState.flags.compiled == true then return false end
			-- only so many nanides to go around
			runState.flags.compiled = true
			local time = minetest.get_gametime()
			local cyclesLeft = ctx.comp.cycles * interval

			for id, e in ipairs(ctx.file.body.conf) do
				if e.key == 'job' then
					local t = J.dec(e.value)
					local remove = false
					local r = starsoul.item.sw.db[t.schematic]
					if not r then -- bad schematic
						remove = true
					else
						local ccost = ctx.sw.cost.cycles + r.cost.cycles
						local tcost = ccost / cyclesLeft
						t.progress = t.progress + (1/tcost)*interval
						cyclesLeft = cyclesLeft - ccost*interval
						if t.progress >= 1 then
							-- complete
							remove = true
							local i = starsoul.item.mk(r.output, {
								how = 'print';
								user = user; -- for suit
								compiler = {
									node = ctx.compiler; -- for device
									sw = ctx.sw;
									install = ctx.fd;
								};
								schematic = r;
							})
							ctx.giveItem(i)
						end
					end
					if remove then
						table.remove(ctx.file.body.conf, id)
					else
						e.value = J.enc(t)
					end
					if not cyclesLeft > 0 then break end
				end
			end
			ctx.saveConf()
		end;
	})
end

local function pasv_heal(effect, energy, lvl, pgmId)
	return function(user, ctx, interval, runState)
		if runState.flags.healed == true then return false end
		-- competing nanosurgical programs?? VERY bad idea
		runState.flags.healed = true

		local amt, f = user:effectiveStat 'health'
		local st = user:getSuit():powerState()
		if (st == 'on' and f < lvl) or (st == 'powerSave' and f < math.min(lvl,0.25)) then
			local maxPower = energy*interval
			local p = user:suitDrawCurrent(maxPower, interval, {
				id = 'heal';
				src = 'suitPower';
				pgmId = pgmId;
				healAmount = effect;
			})
			if p > 0 then
				local heal = (p/maxPower) * ctx.speed * effect*interval
				--user:statDelta('health', math.max(1, heal))
				starsoul.fx.nano.heal(user, {{player=user.entity}}, heal, 1)
				return true
			end
		end
		return false -- program did not run
	end;
end

starsoul.item.sw.link('starsoul_electronics:nanomed', {
	name = 'NanoMed';
	kind = 'suitPower', powerKind = 'passive';
	desc = 'Repair of the body is a Commune specialty, and their environment suits all come equipped with highly sophisticated nanomedicine suites, able to repair even the most grievous of wounds given sufficient energy input and time.';
	size = 2e9;
	cost = {
		cycles = 400e6;
		ram = 3e9;
	};
	run = pasv_heal(2, 20, 1);
})

starsoul.item.sw.link('starsoul_electronics:autodoc_deluxe', {
	name = 'AutoDoc Deluxe';
	kind = 'suitPower', powerKind = 'passive';
	desc = "A flagship offering of the Excellence Unyielding nanoware division, AutoDoc Deluxe has been the top-rated nanocare package in the Celestial Shores Province for six centuries and counting. Every chip includes our comprehensive database of illnesses, prosyn schematics, and organ repair techniques, with free over-the-ether updates guaranteed for ten solariads from date of purchase! When professional medical care just isn't an option, 9/10 doctors recommend Excellence Unyielding AutoDoc Deluxe! The remaining doctor was bribed by our competitors.";
	size = 1e9;
	cost = {
		cycles = 700e6;
		ram = 1e9;
	};
	run = pasv_heal(4, 50, .7);
})

Added mods/starsoul-material/elements.lua version [8655f86d81].



















































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local lib = starsoul.mod.lib
local W = starsoul.world
local M = W.material

M.element.meld {
	hydrogen = {
		name = 'hydrogen', sym = 'H', n = 1;
		gas = true;
		color = lib.color(1,0.8,.3);
	};
	beryllium = {
		name = 'Beryllium', sym = 'Be', n = 4;
		metal = true; -- rare emerald-stuff
		color = lib.color(0.2,1,0.2);
	};
	oxygen = {
		name = 'oxygen', sym = 'O', n = 8;
		gas = true;
		color = lib.color(.2,1,.2);
	};
	carbon = {
		name = 'carbon', sym = 'C', n = 6;
		color = lib.color(.7,.2,.1);
	};
	silicon = {
		name = 'silicon', sym = 'Si', n = 14;
		metal = true; -- can be forged into an ingot
		color = lib.color(.6,.6,.4);
	};
	potassium = {
		name = 'potassium', sym = 'K', n = 19;
		-- potassium is technically a metal but it's so soft
		-- it can be easily nanoworked without high temps, so
		-- ingots make no sense
		color = lib.color(1,.8,0.1);
	};
	calcium = {
		name = 'calcium', sym = 'Ca', n = 20;
		metal = true;
		color = lib.color(1,1,0.7);
	};
	aluminum = {
		name = 'aluminum', sym = 'Al', n = 13;
		metal = true;
		color = lib.color(0.9,.95,1);
	};
	iron = {
		name = 'iron', sym = 'Fe', n = 26;
		metal = true;
		color = lib.color(.3,.3,.3);
	};
	copper = {
		name = 'copper', sym = 'Cu', n = 29;
		metal = true;
		color = lib.color(.8,.4,.1);
	};
	lithium = {
		name = 'lithium', sym = 'Li', n = 3;
		-- i think lithium is considered a metal but we don't mark it as
		-- one here because making a 'lithium ingot' is insane (even possible?)
		color = lib.color(1,0.8,.3);
	};
	titanium = {
		name = 'titanium', sym = 'Ti', n = 22;
		metal = true;
		color = lib.color(.7,.7,.7);
	};
	vanadium = {
		name = 'vanadium', sym = 'V', n = 23;
		metal = true;
		color = lib.color(.3,0.5,.3);
	};
	xenon = {
		name = 'xenon', sym = 'Xe', n = 54;
		gas = true;
		color = lib.color(.5,.1,1);
	};
	argon = {
		name = 'argon', sym = 'Ar', n = 18;
		gas = true;
		color = lib.color(0,0.1,.9);
	};
	osmium = {
		name = 'osmium', sym = 'Os', n = 76;
		metal = true;
		color = lib.color(.8,.1,1);
	};
	iridium = {
		name = 'iridium', sym = 'Ir', n = 77;
		metal = true;
		color = lib.color(.8,0,.5);
	};
	technetium = {
		name = 'technetium', sym = 'Tc', n = 43;
		desc = 'Prized by the higher Powers for subtle interactions that elude mere human scholars, technetium is of particular use in nuclear nanobatteries.';
		metal = true;
		color = lib.color(.2,0.2,1);
	};
	uranium = {
		name = 'uranium', sym = 'U', n = 92;
		desc = 'A weak but relatively plentiful nuclear fuel.';
		metal = true;
		color = lib.color(.2,.7,0);
	};
	thorium = {
		name = 'thorium', sym = 'Th', n = 90;
		desc = 'A frighteningly powerful nuclear fuel.';
		metal = true;
		color = lib.color(.7,.3,.1);
	};
	silver = {
		name = 'silver', sym = 'Ag', n = 47;
		metal = true;
		color = lib.color(.7,.7,.8);
	};
	gold = {
		name = 'gold', sym = 'Au', n = 79;
		metal = true;
		color = lib.color(1,.8,0);
	};
}

Added mods/starsoul-material/init.lua version [4473abdf0e].













































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
local lib = starsoul.mod.lib
local M = {
	canisterSizes = lib.registry.mk 'starsoul_material:canister-size';
}
M.canisterSizes.foreach('starsoul_material:canister_link', {}, function(id, sz)
	starsoul.item.canister.link(minetest.get_current_modname() .. ':canister_' .. id, {
		name = sz.name;
		slots = sz.slots;
		vol = 0.1; -- too big for suit?
		desc = sz.desc;
	})
end)
M.canisterSizes.meld {
	tiny = {name = 'Tiny Canister', slots = 1, vol = 0.05};
	small = {name = 'Small Canister', slots = 3, vol = 0.2};
	mid = {name = 'Canister', slots = 5, vol = 0.5};
	large = {name = 'Large Canister', slots = 10, vol = 1.0};
	storage = {name = 'Storage Canister', slots = 50, vol = 5.0};
}

starsoul.include 'elements'

Added mods/starsoul-material/mod.conf version [4c6f9cbdac].







>
>
>
1
2
3
name = starsoul_material
description = defines the raw materials and alloys used in printing
depends = starsoul

Added mods/starsoul-scenario/init.lua version [dcc87d6f8a].

































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local lib = starsoul.mod.lib
local scenario = starsoul.world.scenario

local function makeChip(label, schem, sw)
	local E = starsoul.mod.electronics
	local files = {}
	local sz = 0
	for _, e in ipairs(schem) do
		local p = E.sw.findSchematicFor(e[1])
		if p then
			local file = {
				kind = 'sw', name = '', drm = e[2];
				body = {pgmId = p};
			}
			table.insert(files, file)
			sz = sz + E.chip.fileSize(file)
		end
	end
	for _, e in ipairs(sw) do
		local file = {
			kind = 'sw', name = '', drm = e[2];
			body = {pgmId = e[1]};
		}
		table.insert(files, file)
		sz = sz + E.chip.fileSize(file)
	end
	local chip = ItemStack(assert(E.chip.findBest(function(c)
		return c.flash >= sz, c.ram + c.clockRate
	end)))
	local r = E.chip.read(chip)
	r.label = label
	r.files = files
	E.chip.write(chip, r)
	return chip
end

local chipLibrary = {
	compendium = makeChip('The Gentleman Adventurer\'s Compleat Wilderness Compendium', {
		{'starsoul_electronics:battery_chemical_imperial_small', 0};
	}, {
		{'starsoul_electronics:shred', 0};
		--{'starsoul_electronics:compile_empire', 0};
		{'starsoul_electronics:autodoc_deluxe', 1};
		--{'starsoul_electronics:driver_compiler_empire', 0};
	});
	survivalware = makeChip('Emergency Survivalware', {
		{'starsoul_electronics:battery_chemical_commune_small', 0};
	}, {
		{'starsoul_electronics:shred', 0};
		{'starsoul_electronics:compile_commune', 0};
		{'starsoul_electronics:nanomed', 0};
		{'starsoul_electronics:driver_compiler_commune', 0};
	});
	misfortune = makeChip("Sold1er0fMisf0rtune TOP Schematic Crackz REPACK", {
		{'starsoul_electronics:battery_chemical_usukwinya_mid', 0};
		{'starsoul_electronics:battery_hybrid_imperial_small', 0};
		-- ammunition
	}, {});
}


table.insert(scenario, {
	id = 'starsoul_scenario:imperialExpat';
	name = 'Imperial Expat';
	desc = "Hoping to escape a miserable life deep in the grinding gears of the capitalist machine for the bracing freedom of the frontier, you sought entry as a colonist to the new Commune world of Thousand Petal. Fate -- which is to say, terrorists -- intervened, and you wound up stranded on Farthest Shadow with little more than the nanosuit on your back, ship blown to tatters and your soul thoroughly mauled by the explosion of some twisted alien artifact -- which SOMEONE neglected to inform you your ride would be carrying.\nAt least you got some nifty psionic powers out of this whole clusterfuck. Hopefully they're safe to use.";

	species = 'human';
	speciesVariant = 'female';
	soul = {
		externalChannel = true; -- able to touch other souls in the spiritual realm
		physicalChannel = true; -- able to extend influence into physical realm
		damage = 1;
	};
	social = {
		empire = 'workingClass';
		commune = 'metic';
	};

	startingItems = {
		suit = ItemStack('starsoul_suit:suit_survival_commune');
		suitBatteries = {ItemStack('starsoul_electronics:battery_carbon_commune_small')};
		suitChips = {
			chipLibrary.survivalware;
			-- you didn't notice it earlier, but your Commune environment suit
			-- came with this chip already plugged in. it's apparently true
			-- what they say: the Commune is always prepared for everything.
			-- E V E R Y T H I N G.
		};
		suitGuns = {};
		suitAmmo = {};
		suitCans = {
			ItemStack('starsoul_material:canister_small');
		};
		carry = {
			chipLibrary.compendium;
			-- you bought this on a whim before you left the Empire, and
			-- just happened to still have it on your person when everything
			-- went straight to the Wild Gods' privy
		};
	};
})

table.insert(scenario, {
	id = 'starsoul_scenario:gentlemanAdventurer';
	--       Othar Tryggvasson,
	name = 'Gentleman Adventurer';
	desc = "Tired of the same-old-same-old, sick of your idiot contemporaries, exasperated with the shallow soul-rotting luxury of life as landless lordling, and earnestly eager to enrage your father, you resolved to see the Reach in all her splendor. Deftly evading the usual tourist traps, you finagled your way into the confidence of the Commune ambassador with a few modest infusions of Father's money -- now *that* should pop his monocle -- and secured yourself a seat on a ride to their brand-new colony at Thousand Petal. How exciting -- a genuine frontier outing!";

	species = 'human';
	speciesVariant = 'male';
	soul = {
		externalChannel = true;
		physicalChannel = true;
		damage = 1;
	};
	social = {
		empire = 'lord';
	};

	startingItems = {
		suit = 'starsoul_suit:suit_survival_imperial';
		suitBatteries = {ItemStack('starsoul_electronics:battery_supercapacitor_imperial_mid')};
		suitChips = {
			chipLibrary.compendium;
			-- Mother, bless her soul, simply insisted on buying you this as a parting
			-- gift. "it's dangerous out there for a young man," she proclaimed as
			-- if she had profound firsthand experience of the matter. mindful of the
			-- husband she endures, you suffered to humor her, and made a big show of
			-- installing it your brand-new nanosuit before you fled the family seat.
		};
		suitGuns = {};
		suitAmmo = {};
		suitCans = {
			ItemStack('starsoul_material:canister_mid');
		};
		carry = {};
	};
})

table.insert(scenario, {
	-- you start out with strong combat abilities but weak engineering,
	-- and will have to scavenge wrecks to find basic crafting gear
	id = 'starsoul_scenario:terroristTagalong';
	name = 'Terrorist Tagalong';
	desc = "It turns out there's a *reason* Crown jobs pay so well.";
	species = 'human';
	speciesVariant = 'female';
	social = {
		empire = 'lowlife';
		commune = 'mostWanted';
		underworldConnections = true;
	};
	soul = {
		externalChannel = true;
		physicalChannel = true;
		damage = 2; -- closer to the blast
	};
	startingItems = {
		suit = 'starsoul_suit:suit_combat_imperial';
		suitBatteries = {
			ItemStack('starsoul_electronics:battery_supercapacitor_imperial_small');
			ItemStack('starsoul_electronics:battery_chemical_imperial_large');
		};
		suitGuns = {};
		suitAmmo = {};
		carry = {};
	};
	suitChips = {chipLibrary.misfortune};
})

table.insert(scenario, {
	id = 'starsoul_scenario:tradebirdBodyguard';
	name = 'Tradebird Bodyguard';
	desc = "You've never understood why astropaths of all people *insist* on bodyguards. This one could probably make hash of a good-sized human batallion, if her feathers were sufficiently ruffled. Perhaps it's a status thing. Whatever the case, it's easy money.\nAt least, it was supposed to be.'";
	species = 'usukwinya';
	speciesVariant = 'male';
	soul = {
		damage = 0; -- Inyukiriku and her entourage fled the ship when she sensed something serious was about to go down. lucky: the humans only survived because their souls were closed off to the Physical. less luckily, the explosion knocked your escape pod off course and the damn astropath was too busy saving her own skin to come after you
		externalChannel = true; -- usukwinya are already psionic
		physicalChannel = true; -- usukwinya are Starsouls
	};
	startingItems = {
		suit = 'starsoul_suit:suit_combat_usukwinya';
		suitBatteries = {
			ItemStack('starsoul_electronics:battery_hybrid_usukwinya_mid');
		};
		suitGuns = {};
		suitChips = {};
		suitAmmo = {};
		carry = {};
	};
})

Added mods/starsoul-scenario/mod.conf version [cb3b4db72f].









>
>
>
>
1
2
3
4
name = starsoul_scenario
description = built-in scenarios for Starsoul
depends = starsoul, starsoul_suit, starsoul_electronics, starsoul_building, starsoul_material
# be sure to add any mods from which you list new starting items!

Added mods/starsoul-secrets/init.lua version [f98f7f7b59].





















































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
----------------------------------------------------
------------- CONTROLLED INFORMATION  --------------
----------------------------------------------------
-- THE INFORMATION CONTAINED IN THIS DOCUMENT IS  --
-- SUBJECT TO RESTRAINT OF TRANSMISSION PER THE   --
-- TERMS OF THE COMMUNE CHARTER INFOSECURITY      --
-- PROVISION. IF YOU ARE NOT AUTHORIZED UNDER THE --
-- AEGIS OF THE APPROPRITE CONTROLLING AUTHORITY, --
-- CLOSE THIS DOCUMENT IMMEDIATELY AND REPORT THE --
-- SECURITY BREACH TO YOUR DESIGNATED INFORMATION --
-- HYGIENE OVERSEER OR FACE CORRECTIVE DISCIPLINE --
----------------------------------------------------

local lib = starsoul.mod.lib
local sec = {}
starsoul.mod.secrets = sec

sec.index = lib.registry.mk 'starsoul_secrets:secret'

--[==[

a secret is a piece of information that is made available
for review once certain conditions are met. despite the name,
it doesn't necessarily have to be secret -- it could include
e.g. journal entries about a character's background. a secret
is defined in the following manner:

{
	title = the string that appears in the UI
	stages = {
		{
			prereqs = {
				{kind = 'fact', id = 'starsoul:terroristEmployer'}
				{kind = 'item', id = 'starsoul_electronic:firstbornDoomBong'}
				{kind = 'background', id = 'starsoul:terroristTagalong'}
			}
			body = {
				'the firstborn smonked hella weed';
			};
			-- body can also be a function(user,secret)
		}
   }
}

TODO would it be useful to impl horn clauses and a general fact database?
     is that level of flexibility meaningful? or are simply flags better

a secret can be a single piece of information predicated
on a fact, in which case the secret and fact should share
the same ID. the ID should be as non-indicative as possible
to avoid spoilers for devs of unrelated code.

a secret can also be manually unlocked e.g. by using an item

]==]--

function sec.prereqCheck(user, pr)
end

Added mods/starsoul-secrets/mod.conf version [26c2999a4f].









>
>
>
>
1
2
3
4
name = starsoul_secrets
title = starsoul secrets
description = TS//NOFORN
depends = starsoul

Added mods/starsoul-suit/init.lua version [d4fabd1162].









































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local lib = starsoul.mod.lib
local fab = starsoul.type.fab

local facDescs = {
	commune = {
		survival = {
			suit = 'A light, simple, bare-bones environment suit that will provide heating, cooling, and nanide support to a stranded cosmonaut';
			cc = 'The Survival Suit uses compact thermoelectrics to keep the wearer perfectly comfortable in extremes of heat or cold. It makes up for its heavy power usage with effective insulation that substantially reduces any need for climate control.';
		};
		engineer = {
			suit = 'A lightweight environment suit designed for indoor work, the Commune\'s Engineer Suit boasts advanced nanotech capable of constructing objects in place.';
			cc = 'The Engineer Suit is designed for indoor work. Consequently, it features only a low-power thermoelectric cooler meant to keep its wearer comfortable during strenuous work.';
		};
		combat = {
			suit = 'A military-grade suit with the latest Commune technology. Designed for maximum force multiplication, the suit has dual weapon hardpoints and supports a gargantuan power reserve. Its nanotech systems are specialized for tearing through obstacles, but can also be used to manufacturer ammunition in a pinch.';
			cc = 'This Combat Suit uses electrothermal cooling to keep an active soldier comfortable and effective, as well as conventional heating coils to enable operation in hostile atmospheres.';
		};
	};

}


starsoul.world.tier.foreach('starsoul:suit-gen', {}, function(tid, t)
	local function hasTech(tech)
		return starsoul.world.tier.tech(tid, tech)
	end
	if not hasTech 'suit' then return end
	-- TODO tier customization
	--
	local function fabsum(f)
		return starsoul.world.tier.fabsum(tid, 'suit')
	end
	local function fabReq(sz, days)
		local tierMatBase = (
			(fabsum 'electric' * 4) +
			(fabsum 'basis' + fabsum 'suit') * sz
		)
		local b = tierMatBase + fab {
			-- universal suit requirements
			time = { print    = 60*60*24 * days };
			size = { printBay = sz              };
		}
		b.flag = lib.tbl.set('print');
		return b
	end
	local function facDesc(s, t)
		local default = 'A protective nanosuit' -- FIXME
		if not facDescs[tid] then return default end
		if not facDescs[tid][s] then return default end
		if not facDescs[tid][s][t] then return default end
		return facDescs[tid][s][t]
	end
	starsoul.item.suit.link('starsoul_suit:suit_survival_' .. tid, {
		name = t.name .. ' Survival Suit';
		desc = facDesc('survival','suit');
		fab = fabReq(1, 2.2) + fab { };
		tex = {
			plate = {
				id = 'starsoul-suit-survival-plate';
				tint = lib.color {hue = 210, sat = .5, lum = .5};
			};
			lining = {
				id = 'starsoul-suit-survival-lining';
				tint = lib.color {hue = 180, sat = .2, lum = .7};
			};
		};
		tints = {'suit_plate', 'suit_lining'};
		temp = {
			desc = facDesc('survival','cc');
			maxHeat = 0.7; -- can produce a half-degree Δ per second
			maxCool = 0.5;
			heatPower = 50; -- 50W
			coolPower = 50/t.efficiency;
			insulation = 0.5; -- prevent half of heat loss
		};
		protection = {
			rad = 0.7; -- blocks 70% of ionizing radiation
		};
		slots = {
			canisters = 1;
			batteries = math.ceil(math.max(1, t.power/2));
			chips = 3;
			guns = 0;
			ammo = 0;
		};
		nano = {
			compileSpeed = 0.1 * t.efficiency;
			shredSpeed = 0.1 * t.power;
			fabSizeLimit = 0.6; -- 60cm
		};
	})

	starsoul.item.suit.link('starsoul_suit:suit_engineer_' .. tid, {
		name = t.name .. ' Engineer Suit';
		desc = facDesc('engineer','suit');
		tex = {
			plate = {
				id = 'starsoul-suit-survival-plate';
				tint = lib.color {hue = 0, sat = .5, lum = .7};
			};
		};
		tints = {'suit_plate', 'suit_lining'};
		fab = fabReq(.8, 7) + fab { };
		temp = {
			desc = facDesc('engineer','cc');
			maxHeat = 0;
			maxCool = 0.2;
			heatPower = 0;
			coolPower = 10 / t.efficiency;
			insulation = 0.1; -- no lining
		};
		slots = {
			canisters = 2;
			batteries = 2;
			chips = 6;
			guns = 0;
			ammo = 0;
		};
		compat = {
			maxBatterySize = 0.10 * t.power; -- 10cm
		};
		protection = {
			rad = 0.1; -- blocks 10% of ionizing radiation
		};
		nano = {
			compileSpeed = 1 * t.efficiency;
			shredSpeed = 0.7 * t.power;
			fabSizeLimit = 1.5; -- 1.5m (enables node compilation)
		};
	})

	if hasTech 'suitCombat' then
		starsoul.item.suit.link('starsoul_suit:suit_combat_' .. tid, {
			name = t.name .. ' Combat Suit';
			desc = facDesc('combat','suit');
			fab = fabReq(1.5, 14) + fab {
				metal = {iridium = 1e3};
			};
			tex = {
				plate = {
					id = 'starsoul-suit-survival-plate';
					tint = lib.color {hue = 0, sat = 0, lum = 0};
				};
				lining = {
					id = 'starsoul-suit-survival-lining';
					tint = lib.color {hue = 180, sat = .5, lum = .3};
				};
			};
			tints = {'suit_plate', 'suit_lining'};
			slots = {
				canisters = 1;
				batteries = math.ceil(math.max(3, 8*(t.power/2)));
				chips = 5;
				guns = 2;
				ammo = 1;
			};
			compat = {
				maxBatterySize = 0.10 * t.power; -- 10cm
			};
			temp = {
				desc = facDesc('combat','cc');
				maxHeat = 0.3; 
				maxCool = 0.6;
				heatPower = 20 / t.efficiency; 
				coolPower = 40 / t.efficiency;
				insulation = 0.2; 
			};
			protection = {
				rad = 0.9; -- blocks 90% of ionizing radiation
			};
			nano = {
				compileSpeed = 0.05;
				shredSpeed = 2 * t.power;
				fabSizeLimit = 0.3; -- 30cm
			};
		})
	end
end)


Added mods/starsoul-suit/mod.conf version [dbff224e42].







>
>
>
1
2
3
name = starsoul_suit
description = defines the environment suits available in starsoul
depends = starsoul, starsoul_electronics

Added mods/starsoul/container.lua version [72968ffd62].























































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
-- a container item defines a 'container' structure listing its
-- inventories and their properties. a container object is created
-- in order to interact with a container
local lib = starsoul.mod.lib
starsoul.item.container = lib.class {
	__name = 'starsoul:container';
	construct = function(stack, inv, def)
		local T,G = lib.marshal.t, lib.marshal.g
		local cdef = stack:get_definition()._starsoul.container;
		local sd = {}
		for k,v in pairs(cdef.list) do
			sd[k] = {
				key  = v.key;
				type = T.inventoryList;
			}
		end
		return {
			stack = stack, inv = inv, pdef = def, cdef = cdef;
			store = lib.marshal.metaStore(sd)(stack);
		}
	end;
	__index = {
		slot = function(self, id)
			return string.format("%s_%s", self.pdef.pfx, id)
		end;
		clear = function(self) -- initialize or empty the metadata
			self:update(function()
				for k,v in pairs(self.cdef.list) do
					if v.sz > 0 then
						self.store.write(k, {})
					end
				end
			end)
		end;
		list = function(self, k) return self.store.read(k) end;
		read = function(self)
			local lst = {}
			for k,v in pairs(self.cdef.list) do
				if v.sz > 0 then lst[k] = self:list(k) end
			end
			return lst
		end;
		pull = function(self) -- align the inventories with the metadata
			for k,v in pairs(self.cdef.list) do
				if v.sz > 0 then
					local stacks = self:list(k)
					local sid    = self:slot(k)
					self.inv:set_size(sid, v.sz)
					self.inv:set_list(sid, stacks)
				end
			end
		end;
		update = function(self, fn)
			local old = ItemStack(self.stack)
			if fn then fn() end
			if self.cdef.handle then
				self.cdef.handle(self.stack, old)
			end
		end;
		push = function(self) -- align the metadata with the inventories
			self:update(function()
				for k,v in pairs(self.cdef.list) do
					if v.sz > 0 then
						local sid = self:slot(k)
						local lst = self.inv:get_list(sid)
						self.store.write(k, lst)
					end
				end
			end)
		end;
		drop = function(self) -- remove the inventories from the node/entity
			for k,v in pairs(self.cdef.list) do
				local sid = self:slot(k)
				self.inv:set_size(sid, 0)
			end
		end;
		slotAccepts = function(self, lst, slot, stack)
		end;
	};
}

function starsoul.item.container.dropPrefix(inv, pfx)
	local lists = inv:get_lists()
	for k,v in pairs(lists) do
		if #k > #pfx then
			if string.sub(k, 1, #pfx + 1) == pfx .. '_' then
				inv:set_size(k, 0)
			end
		end
	end
end

Added mods/starsoul/effect.lua version [2bec98314f].





































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
-- ported from sorcery/spell.lua, hence the lingering refs to "magic"
--
-- this file is used to track active effects, for the purposes of metamagic
-- like disjunction. a "effect" is a table consisting of several properties:
-- a "disjoin" function that, if present, is called when the effect is
-- abnormally interrupted, a "terminate" function that calls when the effect
-- completes, a "duration" property specifying how long the effect lasts in
-- seconds, and a "timeline" table that maps floats to functions called at 
-- specific points during the function's activity. it can also have a
-- 'delay' property that specifies how long to wait until the effect sequence
-- starts; the effect is however still vulnerable to disjunction during this
-- period. there can also be a sounds table that maps timepoints to sounds
-- the same way timeline does. each value should be a table of form {sound,
-- where}. the `where` field may contain one of 'pos', 'caster', 'subjects', or
-- a vector specifying a position in the world, and indicate where the sound
-- should be played. by default 'caster' and 'subjects' sounds will be attached
-- to the objects they reference; 'attach=false' can be added to prevent this.
-- by default sounds will be faded out quickly when disjunction occurs; this
-- can be controlled by the fade parameter.
--
-- effects can have various other properties, for instance 'disjunction', which
-- when true prevents other effects from being cast in its radius while it is
-- still in effect. disjunction is absolute; there is no way to overwhelm it.
--
-- the effect also needs at least one of "anchor", "subjects", or "caster".
--  * an anchor is a position that, in combination with 'range', specifies the area
--    where a effect is in effect; this is used for determining whether it
--    is affected by a disjunction that incorporates part of that position
--  * subjects is an array of individuals affected by the effect. when
--    disjunction is cast on one of them, they will be removed from the
--    table. each entry should have at least a 'player' field; they can
--    also contain any other data useful to the effect. if a subject has
--    a 'disjoin' field it must be a function called when they are removed
--    from the list of effect targets.
--  * caster is the individual who cast the effect, if any. a disjunction
--    against their person will totally disrupt the effect.
local log = starsoul.logger 'effect'
local lib = starsoul.mod.lib

-- FIXME saving object refs is iffy, find a better alternative
starsoul.effect = {
	active = {}
}

local get_effect_positions = function(effect)
	local effectpos
	if effect.anchor then
		effectpos = {effect.anchor}
	elseif effect.attach then
		if effect.attach == 'caster' then
			effectpos = {effect.caster:get_pos()}
		elseif effect.attach == 'subjects' or effect.attach == 'both' then
			if effect.attach == 'both' then
				effectpos = {effect.caster:get_pos()}
			else effectpos = {} end
			for _,s in pairs(effect.subjects) do
				effectpos[#effectpos+1] = s.player:get_pos()
			end
		else effectpos = {effect.attach:get_pos()} end
	else assert(false) end
	return effectpos
end

local ineffectrange = function(effect,pos,range)
	local effectpos = get_effect_positions(effect)

	for _,p in pairs(effectpos) do
		if vector.equals(pos,p) or
			(range       and lib.math.vdcomp(range,      pos,p)<=1) or
			(effect.range and lib.math.vdcomp(effect.range,p,pos)<=1) then
			return true
		end
	end
	return false
end

starsoul.effect.probe = function(pos,range)
	-- this should be called before any effects are performed.
	-- other mods can overlay their own functions to e.g. protect areas
	-- from effects
	local result = {}

	-- first we need to check if any active injunctions are in effect
	-- injunctions are registered as effects with a 'disjunction = true'
	-- property
	for id,effect in pairs(starsoul.effect.active) do
		if not (effect.disjunction and (effect.anchor or effect.attach)) then goto skip end
		if ineffectrange(effect,pos,range) then
			result.disjunction = true
			break
		end
	::skip::end
	
	-- at some point we might also check to see if certain anti-effect
	-- blocks are nearby or suchlike. there could also be regions where
	-- perhaps certain kinds of effect are unusually empowered or weak
	return result
end
starsoul.effect.disjoin = function(d)
	local effects,targets = {},{}
	if d.effect then effects = {{v=d.effect}}
	elseif d.target then targets = {d.target}
	elseif d.pos then -- find effects anchored here and people in range
		for id,effect in pairs(starsoul.effect.active) do
			if not effect.anchor then goto skip end -- this intentionally excludes attached effects
			if ineffectrange(effect,d.pos,d.range) then
				effects[#effects+1] = {v=effect,i=id}
			end
		::skip::end
		local ppl = minetest.get_objects_inside_radius(d.pos,d.range)
		if #targets == 0 then targets = ppl else
			for _,p in pairs(ppl) do targets[#targets+1] = p end
		end
	end

	-- iterate over targets to remove from any effect's influence
	for _,t in pairs(targets) do
		for id,effect in pairs(starsoul.effect.active) do
			if effect.caster == t then effects[#effects+1] = {v=effect,i=id} else
				for si, sub in pairs(effect.subjects) do
					if sub.player == t then
						if sub.disjoin then sub:disjoin(effect) end
						effect.release_subject(si)
						break
					end
				end
			end
		end
	end

	-- effects to disjoin entirely
	for _,s in pairs(effects) do local effect = s.v
		if effect.disjoin then effect:disjoin() end
		effect.abort()
		if s.i then starsoul.effect.active[s.i] = nil else
			for k,v in pairs(starsoul.effect.active) do
				if v == effect then starsoul.effect.active[k] = nil break end
			end
		end
	end
end

starsoul.effect.ensorcelled = function(player,effect)
	if type(player) == 'string' then player = minetest.get_player_by_name(player) end
	for _,s in pairs(starsoul.effect.active) do
		if effect and (s.name ~= effect) then goto skip end
		for _,sub in pairs(s.subjects) do
			if sub.player == player then return s end
		end
	::skip::end
	return false
end

starsoul.effect.each = function(player,effect)
	local idx = 0
	return function()
		repeat idx = idx + 1
			local sp = starsoul.effect.active[idx]
			if sp == nil then return nil end
			if effect == nil or sp.name == effect then
				for _,sub in pairs(sp.subjects) do
					if sub.player == player then return sp end
				end
			end
		until idx >= #starsoul.effect.active
	end
end

-- when a new effect is created, we analyze it and make the appropriate calls
-- to minetest.after to queue up the events. each job returned needs to be
-- saved in 'jobs' so they can be canceled if the effect is disjoined. no polling
-- necessary :D

starsoul.effect.cast = function(proto)
	local s = table.copy(proto)
	s.jobs = s.jobs or {} s.vfx = s.vfx or {} s.sfx = s.sfx or {}
	s.impacts = s.impacts or {} s.subjects = s.subjects or {}
	s.delay = s.delay or 0
	s.visual = function(subj, def)
		s.vfx[#s.vfx + 1] = {
			handle = minetest.add_particlespawner(def);
			subject = subj;
		}
	end
	s.visual_caster = function(def) -- convenience function
		local d = table.copy(def)
		d.attached = s.caster
		s.visual(nil, d)
	end
	s.visual_subjects = function(def)
		for _,sub in pairs(s.subjects) do
			local d = table.copy(def)
			d.attached = sub.player
			s.visual(sub, d)
		end
	end
	s.affect = function(i)
		local etbl = {}
		for _,sub in pairs(s.subjects) do
			-- local eff = late.new_effect(sub.player, i)
			-- starsoul will not be using late
			local rec = {
				effect = eff;
				subject = sub;
			}
			s.impacts[#s.impacts+1] = rec
			etbl[#etbl+1] = rec
		end
		return etbl
	end
	s.abort = function()
		for _,j in ipairs(s.jobs) do j:cancel() end
		for _,v in ipairs(s.vfx) do minetest.delete_particlespawner(v.handle) end
		for _,i in ipairs(s.sfx) do s.silence(i) end
		for _,i in ipairs(s.impacts) do i.effect:stop() end
	end
	s.release_subject = function(si)
		local t = s.subjects[si]
		for _,f in pairs(s.sfx)     do if f.subject == t then s.silence(f) end end
		for _,f in pairs(s.impacts) do if f.subject == t then f.effect:stop() end end
		for _,f in pairs(s.vfx) do
			if f.subject == t then minetest.delete_particlespawner(f.handle) end
		end
		s.subjects[si] = nil
	end
	local interpret_timespec = function(when)
		if when == nil then return 0 end
		local t if type(when) == 'number' then
			t = s.duration * when
		else
			t = (s.duration * (when.whence or 0)) + (when.secs or 0)
		end
		if t then return math.min(s.duration,math.max(0,t)) end

		log.err('invalid timespec ' .. dump(when))
		return 0
	end
	s.queue = function(when,fn)
		local elapsed = s.starttime and minetest.get_server_uptime() - s.starttime or 0
		local timepast = interpret_timespec(when)
		if not timepast then timepast = 0 end
		local timeleft = s.duration - timepast
		local howlong = (s.delay + timepast) - elapsed
		if howlong < 0 then
			log.err('cannot time-travel! queue() called with `when` specifying timepoint that has already passed')
			howlong = 0
		end
		s.jobs[#s.jobs+1] = minetest.after(howlong, function()
			-- this is somewhat awkward. since we're using a non-polling approach, we
			-- need to find a way to account for a caster or subject walking into an
			-- existing antimagic field, or someone with an existing antimagic aura
			-- walking into range of the anchor. so every time a effect effect would
			-- take place, we first check to see if it's in range of something nasty
			if not s.disjunction and -- avoid self-disjunction
				((s.caster and starsoul.effect.probe(s.caster:get_pos()).disjunction) or
				 (s.anchor and starsoul.effect.probe(s.anchor,s.range).disjunction)) then
				starsoul.effect.disjoin{effect=s}
			else
				if not s.disjunction then for _,sub in pairs(s.subjects) do
					local sp = sub.player:get_pos()
					if starsoul.effect.probe(sp).disjunction then
						starsoul.effect.disjoin{pos=sp}
					end
				end end
				-- effect still exists and we've removed any subjects who have been
				-- affected by a disjunction effect, it's now time to actually perform
				-- the queued-up action
				fn(s,timepast,timeleft)
			end
		end)
	end
	s.play_now = function(spec)
		local specs, stbl = {}, {}
		local addobj = function(obj,sub)
			if spec.attach == false then specs[#specs+1] = {
				spec = { pos = obj:get_pos() };
				obj = obj, subject = sub;
			} else specs[#specs+1] = {
				spec = { object = obj };
				obj = obj, subject = sub;
			} end
		end

		if spec.where == 'caster' then addobj(s.caster)
		elseif spec.where == 'subjects' then
			for _,sub in pairs(s.subjects) do addobj(sub.player,sub) end
		elseif spec.where == 'pos' then specs[#specs+1] = { spec = {pos = s.anchor} }
		else specs[#specs+1] = { spec = {pos = spec.where} } end

		for _,sp in pairs(specs) do
			sp.spec.gain = sp.spec.gain or spec.gain
			local so = {
				handle = minetest.sound_play(spec.sound, sp.spec, spec.ephemeral);
				ctl = spec;
				-- object = sp.obj;
				subject = sp.subject;
			}
			stbl[#stbl+1] = so
			s.sfx[#s.sfx+1] = so
		end
		return stbl
	end
	s.play = function(when,spec)
		s.queue(when, function()
			local snds = s.play_now(spec)
			if spec.stop then
				s.queue(spec.stop, function()
					for _,snd in pairs(snds) do s.silence(snd) end
				end)
			end
		end)
	end
	s.silence = function(sound)
		if sound.ctl.fade == 0 then minetest.sound_stop(sound.handle)
		else minetest.sound_fade(sound.handle,sound.ctl.fade or 1,0) end
	end
	local startqueued, termqueued = false, false
	local myid = #starsoul.effect.active+1
	s.cancel = function()
		s.abort()
		starsoul.effect.active[myid] = nil
	end
	local perform_disjunction_calls = function()
		local positions = get_effect_positions(s)
		for _,p in pairs(positions) do
			starsoul.effect.disjoin{pos = p, range = s.range}
		end
	end
	if s.timeline then
		for when_raw,what in pairs(s.timeline) do
			local when = interpret_timespec(when_raw)
			if s.delay == 0 and when == 0 then
				startqueued = true
				if s.disjunction then perform_disjunction_calls() end
				what(s,0,s.duration)
			elseif when_raw == 1 or when >= s.duration then -- avoid race conditions
				if not termqueued then
					termqueued = true
					s.queue(1,function(s,...)
						what(s,...)
						if s.terminate then s:terminate() end
						starsoul.effect.active[myid] = nil
					end)
				else
					log.warn('multiple final timeline events not possible, ignoring')
				end
			elseif when == 0 and s.disjunction then
				startqueued = true
				s.queue(when_raw,function(...)
					perform_disjunction_calls()
					what(...)
				end)
			else s.queue(when_raw,what) end
		end
	end
	if s.intervals then
		for _,int in pairs(s.intervals) do
			local timeleft = s.duration - interpret_timespec(int.after)
			local iteration, itercount = 0, timeleft / int.period
			local function iterate(lastreturn)
				iteration = iteration + 1
				local nr = int.fn {
					effect = s;
					iteration = iteration;
					iterationcount = itercount;
					timeleft = timeleft;
					timeelapsed = s.duration - timeleft;
					lastreturn = lastreturn;
				}
				if nr ~= false and iteration < itercount then
					s.jobs[#s.jobs+1] = minetest.after(int.period,
						function() iterate(nr) end)
				end
			end
			if int.after
				then s.queue(int.after, iterate)
				else s.queue({whence=0, secs=s.period}, iterate)
			end
		end
	end
	if s.disjunction and not startqueued then
		if s.delay == 0 then perform_disjunction_calls() else
			s.queue(0, function() perform_disjunction_calls() end)
		end
	end
	if s.sounds then
		for when,what in pairs(s.sounds) do s.play(when,what) end
	end
	starsoul.effect.active[myid] = s
	if not termqueued then
		s.jobs[#s.jobs+1] = minetest.after(s.delay + s.duration, function()
			if s.terminate then s:terminate() end
			starsoul.effect.active[myid] = nil
		end)
	end
	s.starttime = minetest.get_server_uptime()
	return s
end

minetest.register_on_dieplayer(function(player)
	starsoul.effect.disjoin{target=player}
end)

Added mods/starsoul/element.lua version [52033ae372].





















































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local lib = starsoul.mod.lib
local W = starsoul.world
local M = W.material

M.element.foreach('starsoul:sort', {}, function(id, m)
	if m.metal then
		M.metal.link(id, {
			name = m.name;
			composition = starsoul.type.fab{element = {[id] = 1}};
			color = m.color;
			-- n.b. this is a RATIO: it will be appropriately multiplied
			-- for the object in question; e.g a normal chunk will be
			-- 100 $element, an ingot will be 1000 $element
		})
	elseif m.gas then
		M.gas.link(id, {
			name = m.name;
			composition = starsoul.type.fab{element = {[id] = 1}};
		})
	elseif m.liquid then
		M.liquid.link(id, {
			name = m.name;
			composition = starsoul.type.fab{element = {[id] = 1}};
		})
	end
end)

local F = string.format

local function mkEltIndicator(composition)
	local indicator = ''
	local idx = 0
	local ccount = 0
	for _ in pairs(composition) do
		ccount = ccount + 1
	end
	local indsz,indpad = 28,4
	local ofs = math.min(11, (indsz-indpad)/ccount)
	for id, amt in pairs(composition) do
		idx = idx + 1
		indicator = indicator .. F(
			':%s,3=starsoul-element-%s.png',
			(indsz-indpad) - (idx*ofs), id
		)
	end
	indicator = lib.image(indicator)
	return function(s)
		return string.format('(%s^[resize:%sx%s)^[combine:%sx%s%s',
			s,
			indsz, indsz,
			indsz, indsz,
			indicator);
	end
end

M.element.foreach('starsoul:gen-forms', {}, function(id, m)
	local eltID = F('%s:element_%s', minetest.get_current_modname(), id)
	local eltName = F('Elemental %s', lib.str.capitalize(m.name))
	local tt = function(t, d, g)
		return starsoul.ui.tooltip {
			title = t, desc = d;
			color = lib.color(0.1,0.2,0.1);
			props = {
				{title = 'Mass', desc = lib.math.si('g', g), affinity='info'}
			}
		}
	end
	local comp = {[id] = 1}
	local iblit = mkEltIndicator(comp)
	m.form = m.form or {}
	m.form.element = eltID

	local powder = F('starsoul-element-%s-powder.png', id);
	minetest.register_craftitem(eltID, {
		short_description = eltName;
		description = tt(eltName, F('Elemental %s kept in suspension by a nanide storage system, ready to be worked by a cold matter compiler', m.name), 1);
		inventory_image = iblit(powder);
		wield_image = powder;
		stack_max = 1000; -- 1kg
		groups = {element = 1, powder = 1, specialInventory = 1};
		_starsoul = {
			mass = 1;
			material = {
				kind = 'element';
				element = id;
			};
			fab = starsoul.type.fab {
				element = comp;
			};
		};
	});
end)


M.metal.foreach('starsoul:gen-forms', {}, function(id, m)
	local baseID = F('%s:metal_%s_', minetest.get_current_modname(), id)
	local brickID, ingotID = baseID .. 'brick', baseID .. 'ingot'
	local brickName, ingotName =
		F('%s Brick', lib.str.capitalize(m.name)),
		F('%s Ingot', lib.str.capitalize(m.name))
	m.form = m.form or {}
	m.form.brick = brickID
	m.form.ingot = ingotID
	local tt = function(t, d, g)
		return starsoul.ui.tooltip {
			title = t, desc = d;
			color = lib.color(0.1,0.1,0.1);
			props = {
				{title = 'Mass', desc = lib.math.si('g', g), affinity='info'}
			}
		}
	end
	local mcomp = m.composition:elementalize().element
	local function comp(n)
		local t = {}
		for id, amt in pairs(mcomp) do
			t[id] = amt * n
		end
		return t
	end
	local iblit = mkEltIndicator(mcomp)
	local function img(s)
		return iblit(s:colorize(m.color):render())
	end

	minetest.register_craftitem(brickID, {
		short_description = brickName;
		description = tt(brickName, F('A solid brick of %s, ready to be worked by a matter compiler', m.name), 100);
		inventory_image = img(lib.image 'starsoul-item-brick.png');
		wield_image = lib.image 'starsoul-item-brick.png':colorize(m.color):render();
		stack_max = 10;
		groups = {metal = 1, ingot = 1};
		_starsoul = {
			mass = 100;
			material = {
				kind = 'metal';
				metal = id;
			};
			fab = starsoul.type.fab {
				flag = {smelt= true};
				element = comp(1e2);
			};
		};
	});

	minetest.register_craftitem(ingotID, {
		short_description = ingotName;
		description = tt(ingotName, F('A solid ingot of %s, ready to be worked by a large matter compiler', m.name), 1e3);
		inventory_image = img(lib.image('starsoul-item-ingot.png'));
		wield_image = lib.image 'starsoul-item-ingot.png':colorize(m.color):render();
		groups = {metal = 1, ingot = 1};
		stack_max = 5;
		_starsoul = {
			mass = 1e3;
			material = {
				kind = 'metal';
				metal = id;
			};
			fab = starsoul.type.fab {
				flag = {smelt= true};
				element = comp(1e3);
			};
		};
	});


end)

local function canisterDesc(stack, def)
	def = def or stack:get_definition()._starsoul.canister
	local props = {
		{title = 'Charge Slots', affinity = 'info', desc = tostring(def.slots)};
	};
	if stack then
		local inv = starsoul.item.container(stack)
		for i,e in ipairs(inv:list 'elem') do
			local comp = e:get_definition()._starsoul.fab
			table.insert(props, {
				title = comp:formula();
				desc = lib.math.si('g', e:get_count());
				affinity = 'good';
			})
		end
		-- TODO list masses
	end
	return starsoul.ui.tooltip {
		title = def.name, desc = def.desc or 'A canister that can store a charge of elemental powder, gas, or liquid';
		color = lib.color(0.2,0.1,0.1);
		props = props;
	};	
end

starsoul.item.canister = lib.registry.mk 'starsoul:canister';
starsoul.item.canister.foreach('starsoul:item-gen', {}, function(id, c)
	minetest.register_craftitem(id, {
		short_description = c.name;
		description = canisterDesc(nil, c);
		inventory_image = c.image or 'starsoul-item-element-canister.png';
		groups = {canister = 1};
		stack_max = 1;
		_starsoul = {
			canister = c;
			container = {
				handle = function(stack, oldstack)
					stack:get_meta():set_string('description', canisterDesc(stack))
					return stack
				end;
				list = {
					elem = {
						key = 'starsoul:canister_elem';
						accept = 'powder';
						sz = c.slots;
					};
				};
			};
		};
	})
end)

Added mods/starsoul/fab.lua version [d8c093fcd3].















































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
-- [ʞ] fab.lua
--  ~ lexi hale <lexi@hale.su>
--  🄯 EUPL1.2
--  ? fabrication spec class
--    a type.fab supports two operators:
--
--    + used for compounding recipes. that is,
--			a+b = compose a new spec from the spec parts a and b.
--      this is used e.g. for creating tier-based
--      fabspecs.
--
--    * used for determining quantities. that is,
--			f*x = spec to make x instances of f
--
--    new fab fields must be defined in starsoul.type.fab.opClass.
--    this maps a name to fn(a,b,n) -> quant, where a is the first
--    argument, b is a compounding amount, and n is a quantity of
--    items to produce. fields that are unnamed will be underwritten

local function fQuant(a,b,n) return ((a or 0)+(b or 0))*n end
local function fFac  (a,b,n)
	if a == nil and b == nil then return nil end
	local f if a == nil or b == nil then
		f = a or b
	else
		f = (a or 1)*(b or 1)
	end
	return f*n
end
local function fReq  (a,b,n) return a or b         end
local function fFlag (a,b,n) return a and b        end
local function fSize (a,b,n) return math.max(a,b)  end
local opClass = {
	-- fabrication eligibility will be determined by which kinds
	-- of input a particular fabricator can introduce. e.g. a
	-- printer with a  but no cache can only print items whose
	-- recipe only names elements as ingredients

	-- ingredients
	element    = fQuant; -- (g)
	gas        = fQuant; -- ()
	liquid     = fQuant; -- (l)
	crystal    = fQuant; -- (g)
	item       = fQuant; -- n
	metal      = fQuant; -- (g)
	metalIngot = fQuant; -- (g)
	-- factors
	cost = fFac; -- units vary
	time = fFac; -- (s)
		-- print: base printing time
	size = fSize;
		-- printBay: size of the printer bay necessary to produce the item
	req  = fReq;
	flag = fFlag; -- means that can be used to produce the item & misc flags
		-- print: allow production with a printer
		-- smelt: allow production with a smelter
	-- all else defaults to underwrite
}

local F = string.format
local strClass = {
	element = function(x, n)
		local el = starsoul.world.material.element[x]
		return lib.math.si('g', n) .. ' ' .. (el.sym or el.name)
	end;
	metal = function(x, n)
		local met = starsoul.world.material.metal[x]
		return lib.math.si('g', n) .. ' ' .. met.name
	end;
	liquid = function(x, n)
		local liq = starsoul.world.material.liquid[x]
		return lib.math.si('L', n) .. ' ' .. liq.name
	end;
	gas = function(x, n)
		local gas = starsoul.world.material.gas[x]
		return lib.math.si('g', n) .. ' ' .. gas.name
	end;
	item = function(x, n)
		local i = minetest.registered_items[x]
		return tostring(n) .. 'x ' .. i.short_description
	end;
}

local order = {
	'element', 'metal', 'liquid', 'gas', 'item'
}

local lib = starsoul.mod.lib
local fab fab = lib.class {
	__name = 'starsoul:fab';
	
	opClass = opClass;
	strClass = strClass;
	order = order;
	construct = function(q) return q end;
	__index = {
		elementalize = function(self)
			local e = fab {element = self.element or {}}
			for _, kind in pairs {'metal', 'gas', 'liquid'} do
				for m,mass in pairs(self[kind] or {}) do
					local mc = starsoul.world.material[kind][m].composition
					e = e + mc:elementalize()*mass
				end
			end
			return e
		end;

		elementSeq = function(self)
			local el = {}
			local em = self.element
			local s = 0
			local eldb = starsoul.world.material.element.db
			for k in pairs(em) do table.insert(el, k) s=s+eldb[k].n end
			table.sort(el, function(a,b)
				return eldb[a].n > eldb[b].n
			end)
			return el, em, s
		end;

		formula = function(self)
			print('make formula', dump(self))
			local ts,f=0
			if self.element then
				f = {}
				local el, em, s = self:elementSeq()
				local eldb = starsoul.world.material.element.db
				for i, e in ipairs(el) do
					local sym, n = eldb[e].sym, em[e]
					if n > 0 then
						table.insert(f, string.format("%s%s",
							sym, n>1 and lib.str.nIdx(n) or ''))
					end
				end
				f = table.concat(f)
				ts = ts + s
			end

			local sub = {}
			for _, w in pairs {'metal', 'gas', 'liquid'} do
				if self[w] then
					local mdb = starsoul.world.material[w].db
					for k, amt in pairs(self[w]) do
						local mf, s = mdb[k].composition:formula()
						if amt > 0 then table.insert(sub, {
							f = string.format("(%s)%s",mf,
								lib.str.nIdx(amt));
							s = s;
						}) end
						ts = ts + s*amt
					end
				end
			end
			table.sort(sub, function(a,b) return a.s > b.s end)
			local fml = {}
			for i, v in ipairs(sub) do fml[i] = v.f end
			if f then table.insert(fml, f) end
			fml = table.concat(fml, ' + ')

			return fml, ts
		end;
	};

	__tostring = function(self)
		local t = {}
		for i,o in ipairs(order) do
			if self[o] then
				for mat,amt in pairs(self[o]) do
					if amt > 0 then
						table.insert(t, strClass[o](mat, amt))
					end
				end
			end
		end
		return table.concat(t, ", ")
	end;


	__add = function(a,b)
		local new = fab {}
		for cat, vals in pairs(a) do
			new[cat] = lib.tbl.copy(vals)
		end
		for cat, vals in pairs(b) do
			if not new[cat] then
				new[cat] = lib.tbl.copy(vals)
			else
				local f = opClass[cat]
				for k,v in pairs(vals) do
					local n = f(new[cat][k], v, 1)
					new[cat][k] = n > 0 and n or nil
				end
			end
		end
		return new
	end;

	__mul = function(x,n)
		local new = fab {}
		for cat, vals in pairs(x) do
			new[cat] = {}
			local f = opClass[cat]
			for k,v in pairs(vals) do
				local num = f(v,nil,n)
				new[cat][k] = num > 0 and num or nil
			end
		end
		return new
	end;

	__div = function(x,n)
		return x * (1/n)
	end;
}

starsoul.type.fab = fab

Added mods/starsoul/fx/nano.lua version [4c580d7de8].









































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local lib = starsoul.mod.lib
local E = starsoul.effect
local N = {}
starsoul.fx.nano = N
local nanopool= {
	{
		name = 'starsoul-fx-nano-spark-small.png';
		scale_tween = {0,.5, style = 'pulse', rep = 3};
	};
	{
		name = 'starsoul-fx-nano-spark-small.png';
		scale_tween = {0,1, style = 'pulse', rep = 2};
	};
	{
		name = 'starsoul-fx-nano-spark-big.png';
		scale_tween = {0,1, style = 'pulse'};
	};
}

function N.heal(user, targets, amt, dur)
	local amthealed = {}
	local f = E.cast {
		caster = user.entity;
		subjects = targets;
		duration = dur;
		intervals = {
			{
				after = 0;
				period = 4;
				fn = function(c)
					for i,v in pairs(c.effect.subjects) do
						local u = starsoul.activeUsers[v.player:get_player_name()]
						if u then
							local heal = math.max(amt/4, 1)
							amthealed[u] = amthealed[u] or 0
							if amthealed[u] < amt then
								amthealed[u] = amthealed[u] + heal
								u:statDelta('health', heal)
							end
						end
					end
				end;
			}
		}
	}

	local casterIsTarget = false
	for _, sub in pairs(f.subjects) do
		if sub.player == user.entity then
			casterIsTarget = true
		end
		f.visual(sub, {
			amount = 50;
			time = dur;
			glow = 14;
			jitter = 0.01;
			attached = user.entity;
			vel = { min = -0.1, max = 0.1; };
			pos = {
				min = vector.new(0,0.2,0);
				max = vector.new(0,1.2,0);
			};
			radius  = { min = 0.2; max = 0.6; bias = -1; };
			exptime = {min=0.5,max=2};
			attract = {
				kind = 'line';
				strength = {min = 0.5, max = 2};
				origin = 0;
				direction = vector.new(0,1,0);
				origin_attached = sub.player;
				direction_attached = sub.player;
			};

			texpool = nanopool;
		})
	end
	if not casterIsTarget then
		-- f.visual_caster { }
	end
	f.play(0.3, {
		where = 'subjects';
		sound = 'starsoul-nano-heal';
		ephemeral = true;
		spec = {gain = 0.3};
	})

	return f
end

function N.shred(user, pos, prop, time, node)
	local f = E.cast {
		caster = user.entity;
		subjects = {};
		duration = time;
	}
	local sp,sv = user:lookupSpecies()
	local eh = sv.eyeHeight or sp.eyeHeight
	f.visual_caster {
		amount = 200 * time;
		pos =  vector.new(0.12,eh - 0.1,0);
		radius = 0.2;
		time = time - (time/3);
		glow = 14;
		jitter = 0.1;
		size = {min = 0.2, max = 0.5};
		exptime = {min=0.5,max=1};
		vel_tween = {
			0;
			{ min = -0.4, max = 0.4; };
			style = 'pulse', rep = time * 2; 
		};
		attract = {
			kind = 'point';
			origin = pos;
			radius = 0.5;
			strength = {min=.3,max=2};
		};
		texpool = nanopool;
	};
	f.queue(0.05, function(s, timepast, timeleft)
		f.visual(nil, {
			amount = timeleft * 40;
			time = timeleft;
			pos = pos;
			size_tween = {
				0, {min = 0.5, max = 2};
			};
			vel = {
				min = vector.new(-1.2,0.5,-1.2);
				max = vector.new(1.2,3.5,1.2);
			};
			acc = vector.new(0,-starsoul.world.planet.gravity,0);
			node = node;
		})
	end);
	f.queue(0.9, function(s, timepast, timeleft)
		f.visual(nil, {
			amount = 200;
			time = timeleft;
			pos = pos;
			size = {min = 0.1, max = 0.3};
			vel = {
				min = vector.new(-2,0.5,-2);
				max = vector.new(2,4,2);
			};
			acc = vector.new(0,-starsoul.world.planet.gravity,0);
			node = node;
		})
	end);
	f.queue(0.3, function(s, timepast, timeleft)
		local function v(fn)
			local def = {
				amount = timeleft * 100;
				pos = pos;
				time = timeleft;
				radius = 0.5;
				jitter = {min = 0.0, max = 0.2};
				size = {min = 0.2, max = 0.5};
				exptime = {min = 0.5, max = 1};
				attract = {
					kind = 'point';
					strength = {min=0.3, max = 1};
					origin = vector.new(0,eh-0.1,0);
					radius = 0.5;
					origin_attached = user.entity;
				};
			}
			fn(def)
			f.visual(nil, def)
		end
		v(function(t) t.texpool = nanopool t.glow = 14 end)
		v(function(t)
			t.node = node
			t.amount = timeleft * 20
			t.size = {min = 0.1, max = 0.3};
		end)
	end)
	return f

end

Added mods/starsoul/init.lua version [aca0a214d9].























































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
-- [ʞ] starsoul/init.lua
--  ~ lexi hale <lexi@hale.su>
--  ? basic setup, game rules, terrain
--  © EUPL v1.2

local T = minetest.get_translator 'starsoul'

-- TODO enforce latest engine version

local mod = {
	-- subordinate mods register here
	lib = vtlib;
		-- vtlib should be accessed as starsoul.mod.lib by starsoul modules for the sake of proper encapsulation. vtlib should simply be a provider, not a hardcoded dependency
}
local lib = mod.lib


starsoul = {
	ident = minetest.get_current_modname();
	mod = mod;
	translator = T;

	constant = {
		light = { --minetest units
			dim = 3;
			lamp = 7;
			bright = 10;
			brightest = 14; -- only sun and growlights
		};
		heat = { -- celsius
			freezing = 0;
			safe = 4;
			overheat = 32;
			boiling = 100;
		};
		rad = {
		};
	};

	activeUsers = {
		-- map of username -> user object
	};
	activeUI = {
		-- map of username -> UI context
	};
	liveUI = {
		-- cached subset of activeUI containing those UIs needing live updates
	};

	interface = lib.registry.mk 'starsoul:interface';
	item = {
	};

	region = {
		radiator = {
			store = AreaStore();
			emitters = {}
		};
	};

	-- standardized effects
	fx = {};

	type = {};
	world = {
		defaultScenario = 'starsoul_scenario:imperialExpat';
		seedbank = lib.math.seedbank(minetest.get_mapgen_setting 'seed');
		mineral = lib.registry.mk 'starsoul:mineral';
		material = { -- raw materials
			element = lib.registry.mk 'starsoul:element';
			-- elements are automatically sorted into the following categories
			-- if they match. however, it's possible to have a metal/gas/liquid
			-- that *isn't* a pure element, so these need separate registries
			-- for alloys and mixtures like steel and water
			metal   = lib.registry.mk 'starsoul:metal';
			gas     = lib.registry.mk 'starsoul:gas';
			liquid  = lib.registry.mk 'starsoul:liquid';
		};
		ecology = {
			plants = lib.registry.mk 'starsoul:plants';
			trees = lib.registry.mk 'starsoul:trees';
			biomes = lib.registry.mk 'starsoul:biome';
		};
		climate = {};
		scenario = {};
		planet = {
			gravity = 7.44;
			orbit = 189; -- 1 year is 189 days
			revolve = 20; -- 1 day is 20 irl minutes
		};
		fact = lib.registry.mk 'starsoul:fact';
		time = {
			calendar = {
				empire  = {
					name = 'Imperial Regnal Calendar';
					year = function(t, long)
						local reigns = {
							-- if anyone actually makes it to his Honor & Glory Unfailing Persigan I i will be
							-- exceptionally flattered
							{4, 'Emperor', 'Atavarka', 'the Bold'}; -- died at war
							{9, 'Emperor', 'Vatikserka', 'the Unconquered'}; -- died at war
							{22, 'Emperor', 'Rusifend', 'the Wise'}; -- poisoned at diplomacy
							{61, 'Empress', 'Tafseshendi', 'the Great'}; -- died of an 'insurrection of the innards' after a celebrated reign
							{291, 'Emperor', 'Treptebaska', 'the Unwise'}; -- murdered by his wife in short order
							{292, 'Empress', 'Vilintalti', 'the Impious'}; -- removed by the praetorian elite
							{298, 'Emperor', 'Radavan', 'the Reckless'}; -- died at war
							{316, 'Emperor', 'Suldibrand', 'the Forsaken of Men'}; -- fucked around. found out.
							{320, 'Emperor', 'Persigan', 'the Deathless'};
						}
						local year, r = math.floor(t / 414)
						for i=1, #reigns do if reigns[i+1][1] < year then r = reigns[i+1] end end
						local reignBegin, title, name, epithet = lib.tbl.unpack(r)
						local ry = 1 + (year - reignBegin)
						return long and string.format('Year %s of the Reign of HH&GU %s %s %s',
							ry, title, name, epithet) or string.format('Y. %s %s', name, ry)
					end;
					time = function(t, long)
						local bellsInDay, candleSpansInBell = 5, 7
						local bell = bellsInDay*t
						local cspan = (bellsInDay*candleSpansInBell*t) % candleSpansInBell
						return string.format(long and 'Bell %s, Candlespan %s' or '%sb %sc', math.floor(bell), math.floor(cspan))
					end;
				};
				commune = {
					name = 'People\'s Calendar';
					date = function(t, long)
						local year = math.floor(t / 256) + 314
						return string.format(long and 'Foundation %s' or 'F:%s', year)
					end;
					time = function(t, long)
						local hoursInDay, minutesInHour = 16, 16
						local hour = hoursInDay*t
						local min = (hoursInDay*minutesInHour*t) % minutesInHour

						local dawn     = 0.24*hoursInDay
						local noon     = 0.5*hoursInDay
						local dusk     = 0.76*hoursInDay
						local midnight = 1.0*hoursInDay

						local tl, str
						if hour < dawn then
							tl = dawn - hour
							str = long and 'dawn' or 'D'
						elseif hour < noon then
							tl = noon - hour
							str = long and 'noon' or 'N'
						elseif hour < dusk then
							tl = dusk - hour
							str = long and 'dusk' or 'd'
						elseif hour < midnight then
							tl = midnight - hour
							str = long and 'midnight' or 'M'
						end
						return long
							and string.format('%s hours, %s minutes to %s',
							    math.floor(tl), math.floor(minutesInHour - min), str)
							or  string.format('%s.%sH.%sM', str, math.floor(tl),
							    math.floor(minutesInHour - min))
					end;
				};
			};
		};
	};

	jobs = {};
}

starsoul.cfgDir = minetest.get_worldpath() .. '/' .. starsoul.ident

local logger = function(module)
	local function argjoin(arg, nxt, ...)
		if arg and not nxt then return tostring(arg) end
		if not arg then return "(nil)" end
		return tostring(arg) .. ' ' .. argjoin(nxt, ...)
	end
	local lg = {}
	local setup = function(fn, lvl)
		lvl = lvl or fn
		local function emit(...)
			local call = (fn == 'fatal') and error
				or function(str) minetest.log(lvl, str) end
			if module
				then call(string.format('[%s :: %s] %s',starsoul.ident,module,argjoin(...)))
				else call(string.format('[%s] %s',starsoul.ident,argjoin(...)))
			end
		end
		lg[fn       ] = function(...) emit(...)                end
		lg[fn .. 'f'] = function(...) emit(string.format(...)) end -- convenience fn
	end
	setup('info')
	setup('warn','warning')
	setup('err','error')
	setup('act','action')
	setup('fatal')
	return lg
end

starsoul.logger = logger

local log = logger()

function starsoul.evaluate(name, ...)
	local path = minetest.get_modpath(minetest.get_current_modname())
	local filename = string.format('%s/%s', path, name)
	log.info('loading', filename)
	local chunk, err = loadfile(filename, filename)
	if not chunk then error(err) end
	return chunk(...)
end

function starsoul.include(name, ...) -- semantic variant used for loading modules
	return starsoul.evaluate(name..'.lua', ...)
end

minetest.register_lbm {
	label = 'build radiator index';
	name = 'starsoul:loadradiatorboxes';
	nodenames = {'group:radiator'};
	run_at_every_load = true;
	action = function(pos, node, dt)
		local R = starsoul.region
		local phash = minetest.hash_node_position(pos)
		if R.radiator.sources[phash] then return end -- already loaded

		local def = minetest.registered_nodes[node.name]
		local cl = def._starsoul.radiator
		local min,max = cl.maxEffectArea(pos)
		local id = R.radiator.store:insert_area(min,max, minetest.pos_to_string(pos))
		R.radiator.sources[phash] = id
	end;
	-- NOTE: temp emitter nodes are responsible for decaching themselves in their on_destruct cb
}

function starsoul.startJob(id, interval, job)
	local lastRun
	local function start()
		starsoul.jobs[id] = minetest.after(interval, function()
			local t = minetest.get_gametime()
			local d = lastRun and t - lastRun or nil
			lastRun = t
			local continue = job(d, interval)
			if continue == true or continue == nil then
				start()
			elseif continue ~= false then
				interval = continue
				start()
			end
		end)
	end
	start()
end

starsoul.include 'stats'
starsoul.include 'world'
starsoul.include 'fab'
starsoul.include 'tiers'
starsoul.include 'species'

starsoul.include 'store'

starsoul.include 'ui'
starsoul.include 'item'
starsoul.include 'container'
starsoul.include 'user'
starsoul.include 'effect'

starsoul.include 'fx/nano'

starsoul.include 'element'

starsoul.include 'terrain'
starsoul.include 'interfaces'
starsoul.include 'suit'

minetest.settings:set('movement_gravity', starsoul.world.planet.gravity) -- ??? seriously???

---------------
-- callbacks --
---------------
-- here we connect our types up to the minetest API

local function userCB(fn)
	return function(luser, ...)
		local name = luser:get_player_name()
		local user = starsoul.activeUsers[name]
		return fn(user, ...)
	end
end

minetest.register_on_joinplayer(function(luser, lastLogin)
	-- TODO check that necessary CSMs are installed
	local user = starsoul.type.user(luser)

	if lastLogin == nil then
		user:onSignup()
	end
	user:onJoin()

	starsoul.activeUsers[user.name] = user
end)

minetest.register_on_leaveplayer(function(luser)
	starsoul.activeUsers[luser:get_player_name()]:onPart()
end)

minetest.register_on_player_receive_fields(function(luser, formid, fields)
	local name = luser:get_player_name()
	local user = starsoul.activeUsers[name]
	if not user then return false end
	if formid == '' then -- main menu
		return starsoul.ui.userMenuDispatch(user,fields)
	end
	local ui = starsoul.interface.db[formid]
	local state = starsoul.activeUI[name] or {}
	if formid == '__builtin:help_cmds' 
	or formid == '__builtin:help_privs' 
		then return false end
	assert(state.form == formid) -- sanity check
	user:onRespond(ui, state, fields)
	if fields.quit then
		starsoul.activeUI[name] = nil
	end
	return true
end)

minetest.register_on_respawnplayer(userCB(function(user)
	return user:onRespawn()
end))

minetest.register_on_dieplayer(userCB(function(user, reason)
	return user:onDie(reason)
end))

minetest.register_on_punchnode(function(pos,node,puncher,point)
	local user = starsoul.activeUsers[puncher:get_player_name()]
	local oldTgt = user.action.tgt
	user.action.tgt = point
	if bit.band(user.action.bits, 0x80)==0 then
		user.action.bits = bit.bor(user.action.bits, 0x80)
		--user:trigger('primary', {state = 'init'})
	else
		user:trigger('retarget', {oldTgt = oldTgt})
	end
end)

local function pointChanged(a,b)
	return a.type ~= b.type
		or a.type == 'node'   and vector.new(a.under) ~= vector.new(b.under)
		or a.type == 'object' and a.ref ~= b.ref 
end
local function triggerPower(_, luser, point)
	local user = starsoul.activeUsers[luser:get_player_name()]
	local oldTgt = user.action.tgt
	user.action.tgt = point
	if bit.band(user.action.bits, 0x100)==0 then
		user.action.bits = bit.bor(user.action.bits, 0x100)
		--return user:trigger('secondary', {state = 'prog', delta = 0})
	elseif pointChanged(oldTgt, point) then
		user:trigger('retarget', {oldTgt = oldTgt})
	end
end
-- sigh
core.noneitemdef_default.on_place = function(...)
	if not triggerPower(...) then
		minetest.item_place(...)
	end
end
core.noneitemdef_default.on_use           = function(...) triggerPower(...) end
core.noneitemdef_default.on_secondary_use = function(...) triggerPower(...) end

minetest.register_on_player_inventory_action(function(luser, act, inv, p)
	local name = luser:get_player_name()
	local user = starsoul.activeUsers[name]
	-- allow UIs to update on UI changes
	local state = starsoul.activeUI[name]
	if state then
		local ui = starsoul.interface.db[state.form]
		ui:cb('onMoveItem', user, act, inv, p)
	end
end)

minetest.register_on_player_hpchange(function(luser, delta, cause)
	local user = starsoul.activeUsers[luser:get_player_name()]
	if cause.type == 'fall' then
		delta = user:damageModifier('bluntForceTrauma', (delta * 50))
		-- justification: a short fall can do around
		-- five points of damage, which is nearly 50%
		-- of the default hp_max. since we crank up
		-- hp by a factor of 50~40, damage should be
		-- cranked by similarly
	end
	return delta
end, true)

function minetest.handle_node_drops(pos, drops, digger)
	local function jitter(pos)
		local function r(x) return x+math.random(-0.2, 0.2) end
		return vector.new(
			r(pos.x),
			r(pos.y),
			r(pos.z)
		)
	end
	for i, it in ipairs(drops) do
		minetest.add_item(jitter(pos), it)
	end
end


-- TODO timer iterates live UI

Added mods/starsoul/interfaces.lua version [b552296eeb].



















































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local lib = starsoul.mod.lib

function starsoul.ui.setupForUser(user)
	local function cmode(mode)
		if user.actMode == mode then return {hue = 150, sat = 0, lum = .3} end
	end
	user.entity:set_inventory_formspec(starsoul.ui.build {
		kind = 'vert', mode = 'sw';
		padding = .5, spacing = 0.1;
		{kind = 'hztl';
			{kind = 'contact', w=1.5,h=1.5, id = 'mode_nano',
				img='starsoul-ui-icon-nano.png', close=true, color = cmode'nano'};
			{kind = 'contact', w=1.5,h=1.5, id = 'mode_weapon',
				img='starsoul-ui-icon-weapon.png', close=true, color = cmode'weapon'};
			{kind = 'contact', w=1.5,h=1.5, id = 'mode_psi',
				img='starsoul-ui-icon-psi.png', close=true, color = cmode'psi'};
		};
		{kind = 'hztl';
			{kind = 'contact', w=1.5,h=1.5, id = 'open_elements',
				img='starsoul-ui-icon-element.png'};
			{kind = 'contact', w=1.5,h=1.5, id = 'open_suit',
				img='starsoul-item-suit.png^[hsl:200:-.7:0'};
			{kind = 'contact', w=1.5,h=1.5, id = 'open_psi',
				img='starsoul-ui-icon-psi-cfg.png'};
			{kind = 'contact', w=1.5,h=1.5, id = 'open_body',
				img='starsoul-ui-icon-self.png'};
		};
		{kind = 'list';
			target = 'current_player', inv = 'main';
			w = 6, h = 1, spacing = 0.1;
		};
	})
end

function starsoul.ui.userMenuDispatch(user, fields)
	local function setSuitMode(mode)
		if user.actMode == mode then
			user:actModeSet 'off'
		else
			user:actModeSet(mode)
		end
	end

	local modes = { nano = true, psi = false, weapon = true }
	for e,s in pairs(modes) do
		if fields['mode_' .. e] then
			if s and (user:naked() or user:getSuit():powerState() == 'off') then
				user:suitSound 'starsoul-error'
			else
				setSuitMode(e)
			end
			return true
		end
	end

	if fields.open_elements then
		user:openUI('starsoul:user-menu', 'compiler')
		return true
	elseif fields.open_psi then
		user:openUI('starsoul:user-menu', 'psi')
		return true
	elseif fields.open_suit then
		if not user:naked() then
			user:openUI('starsoul:user-menu', 'suit')
		end
		return true
	elseif fields.open_body then
		user:openUI('starsoul:user-menu', 'body')
	end
	return false
end

local function listWrap(n, max)
	local h = math.ceil(n / max)
	local w = math.min(max, n)
	return w, h
end

local function wrapMenu(w, h, rh, max, l)
	local root = {kind = 'vert', w=w, h=h}
	local bar
	local function flush()
		if bar and bar[1] then table.insert(root, bar) end
		bar = {kind = 'hztl'}
	end
	flush()

	for _, i in ipairs(l) do
		local bw = w/max
		if i.cfg then w = w - rh end

		table.insert(bar, {
			kind = 'button', close = i.close;
			color = i.color;
			fg = i.fg;
			label = i.label;
			icon = i.img;
			id = i.id;
			w = bw, h = rh;
		})
		if i.cfg then 
			table.insert(bar, {
				kind = 'button';
				color = i.color;
				fg = i.fg;
				label = "CFG";
				icon = i.img;
				id = i.id .. '_cfg';
				w = rh, h = rh;
			})
		end

		if bar[max] then flush() end
	end
	flush()
	
	return root
end

local function abilityMenu(a)
	-- select primary/secondary abilities or activate ritual abilities
	local p = {kind = 'vert'}
	for _, o in ipairs(a.order) do
		local m = a.menu[o]
		table.insert(p, {kind='label', text=m.label, w=a.w, h = .5})
		table.insert(p, wrapMenu(a.w, a.h, 1.2, 2, m.opts))
	end
	return p
end

local function pptrMatch(a,b)
	if a == nil or b == nil then return false end
	return a.chipID == b.chipID and a.pgmIndex == b.pgmIndex
end

starsoul.interface.install(starsoul.type.ui {
	id = 'starsoul:user-menu';
	pages = {
		compiler = {
			setupState = function(state, user)
				-- nanotech/suit software menu
				local chips = user.entity:get_inventory():get_list 'starsoul_suit_chips' -- FIXME need better subinv api
				local sw = starsoul.mod.electronics.chip.usableSoftware(chips)
				state.suitSW = {}
				local dedup = {}
				for i, r in ipairs(sw) do if
					r.sw.kind      == 'suitPower'
				then
					if not dedup[r.sw] then
						dedup[r.sw] = true
						table.insert(state.suitSW, r)
					end
				end end
			end;
			handle = function(state, user, act)
				if user:getSuit():powerState() == 'off' then return false end
				local pgm, cfg
				for k in next, act do
					local id, mode = k:match('^suit_pgm_([0-9]+)_(.*)$')
					if id then
						id = tonumber(id)
						if state.suitSW[id] then
							pgm = state.suitSW[id]
							cfg = mode == '_cfg'
							break
						end
					end
				end
				if not pgm then return false end -- HAX

				-- kind=active programs must be assigned to a command slot
				-- kind=direct programs must open their UI
				-- kind=passive programs must toggle on and off
				if pgm.sw.powerKind == 'active' then
					if cfg then
						user:openUI(pgm.sw.ui, 'index', {
							context = 'suit';
							program = pgm;
						})
						return false
					end
					local ptr = {chipID = starsoul.mod.electronics.chip.read(pgm.chip).uuid, pgmIndex = pgm.fd.inode}
					local pnan = user.power.nano
					if pnan.primary == nil then
						pnan.primary = ptr
					elseif pptrMatch(ptr, pnan.primary) then
						pnan.primary = nil
					elseif pptrMatch(ptr, pnan.secondary) then
						pnan.secondary = nil
					else
						pnan.secondary = ptr
					end
					user:suitSound 'starsoul-configure'
				elseif pgm.sw.powerKind == 'direct' then
					local ctx = {
						context = 'suit';
						program = pgm;
					}
					if pgm.sw.ui then
						user:openUI(pgm.sw.ui, 'index', ctx)
						return false
					else
						pgm.sw.run(user, ctx)
					end
				elseif pgm.sw.powerKind == 'passive' then
					if cfg then
						user:openUI(pgm.sw.ui, 'index', {
							context = 'suit';
							program = pgm;
						})
						return false
					end

					local addDisableRec = true
					for i, e in ipairs(pgm.file.body.conf) do
						if e.key == 'disable' and e.value == 'yes' then
							addDisableRec = false
							table.remove(pgm.file.body.conf, i)
							break
						elseif e.key == 'disable' and e.value == 'no' then
							e.value = 'yes'
							addDisableRec = false
							break
						end
					end
					if addDisableRec then
						table.insert(pgm.file.body.conf, {key='disable',value='yes'})
					end
					-- update the chip *wince*
					pgm.fd:write(pgm.file)
					user.entity:get_inventory():set_stack('starsoul_suit_chips',
					pgm.chipSlot, pgm.chip)
					user:reconfigureSuit()
					user:suitSound('starsoul-configure')

				end
				return true, true
			end;
			render = function(state, user)
				local suit = user:getSuit()
				local swm
				if user:getSuit():powerState() ~= 'off' then
					swm = {
						w = 8, h = 3;
						order = {'active','ritual','pasv'};
						menu = {
							active = {
								label = 'Nanoware';
								opts = {};
							};
							ritual = {
								label = 'Programs';
								opts = {};
							};
							pasv = {
								label = 'Passive';
								opts = {};
							};
						};
					}
					for id, r in pairs(state.suitSW) do
						local color = {hue=300,sat=0,lum=0}
						local fg = nil
						local close = nil
						local tbl, cfg if r.sw.powerKind == 'active' then
							tbl = swm.menu.active.opts
							if r.sw.ui then cfg = true end
							local pnan = user.power.nano
							if pnan then
								local ptr = {chipID = starsoul.mod.electronics.chip.read(r.chip).uuid, pgmIndex = r.fd.inode}
								if pptrMatch(ptr, pnan.primary) then
									color.lum = 1
								elseif pptrMatch(ptr, pnan.secondary) then
									color.lum = 0.8
								end
							end
						elseif r.sw.powerKind == 'direct' then
							tbl = swm.menu.ritual.opts
							if not r.sw.ui then
								close = true
							end
						elseif r.sw.powerKind == 'passive' then
							tbl = swm.menu.pasv.opts
							if r.sw.ui then cfg = true end
							for i, e in ipairs(r.file.body.conf) do
								if e.key == 'disable' and e.value == 'yes' then
									color.lum = -.2
									fg = lib.color {hue=color.hue,sat=0.7,lum=0.7}
									break
								end
							end
						end
						if tbl then table.insert(tbl, {
							color = color, fg = fg;
							label = r.sw.label or r.sw.name;
							id = string.format('suit_pgm_%s_', id);
							cfg = cfg, close = close;
						}) end
					end
				end
				local menu = { kind = 'vert', mode = 'sw', padding = 0.5 }
				if swm then table.insert(menu, abilityMenu(swm)) end

				local inv = user.entity:get_inventory()
				local cans = inv:get_list 'starsoul_suit_canisters'
				if cans and next(cans) then for i, st in ipairs(cans) do
					local id = string.format('starsoul_canister_%u_elem', i)
					local esz = inv:get_size(id)
					if esz > 0 then
						local eltW, eltH = listWrap(esz, 5)
						table.insert(menu, {kind = 'hztl',
							{kind = 'img', desc='Elements', img = 'starsoul-ui-icon-element.png', w=1,h=1};
							{kind = 'list', target = 'current_player', inv = id,
								listContent = 'element', w = eltW, h = eltH, spacing = 0.1};
						})
					end
				end end

				if #menu == 0 then
					table.insert(menu, {
						kind = 'img';
						img = 'starsoul-ui-alert.png';
						w=2, h=2;
					})
					menu.padding = 1;
				end
				return starsoul.ui.build(menu)
			end;
		};
		compilerListRecipes = {
		};
		psi = {
			render = function(state, user)
				return starsoul.ui.build {
					kind = 'vert', mode = 'sw';
					padding = 0.5;
				}
			end;
		};
		body = {
			render = function(state, user)
				local barh = .75
				local tb = {
					kind = 'vert', mode = 'sw';
					padding = 0.5, 
					{kind = 'hztl', padding = 0.25;
						{kind = 'label', text = 'Name', w = 2, h = barh};
						{kind = 'label', text = user.persona.name, w = 4, h = barh}};
				}
				local statBars = {'hunger', 'thirst', 'fatigue', 'morale'}
				for idx, id in ipairs(statBars) do
					local s = starsoul.world.stats[id]
					local amt, sv = user:effectiveStat(id)
					local min, max = starsoul.world.species.statRange(user.persona.species, user.persona.speciesVariant, id)
					local st = string.format('%s / %s', s.desc(amt, true), s.desc(max))
					table.insert(tb, {kind = 'hztl', padding = 0.25;
						{kind = 'label', w=2, h=barh, text = s.name};
						{kind = 'hbar',  w=4, h=barh, fac = sv, text = st, color=s.color};
					})
				end
				local abilities = {
					{id = 'abl_sprint', label = 'Sprint', img = 'starsoul-ui-icon-ability-sprint.png'};
				}
				table.insert(tb, wrapMenu(6.25,4, 1,2, abilities))
				return starsoul.ui.build(tb)
			end;
		};
		suit = {
			render = function(state, user)
				local suit = user:getSuit()
				local suitDef = suit:def()
				local chipW, chipH = listWrap(suitDef.slots.chips, 5)
				local batW, batH = listWrap(suitDef.slots.batteries, 5)
				local canW, canH = listWrap(suitDef.slots.canisters, 5)
				local suitMode = suit:powerState()
				local function modeColor(mode)
					if mode == suitMode then return {hue = 180, sat = 0, lum = .5} end
				end
				return starsoul.ui.build {
					kind = 'vert', mode = 'sw';
					padding = 0.5, spacing = 0.1;
					{kind = 'hztl',
						{kind = 'img', desc='Batteries', img = 'starsoul-item-battery.png', w=1,h=1};
						{kind = 'list', target = 'current_player', inv = 'starsoul_suit_bat',
							listContent = 'power', w = batW, h = batH, spacing = 0.1};
					};
					{kind = 'hztl',
						{kind = 'img', desc='Chips', img = 'starsoul-item-chip.png', w=1,h=1};
						{kind = 'list', target = 'current_player', inv = 'starsoul_suit_chips',
							listContent = 'chip', w = chipW, h = chipH, spacing = 0.1};
					};
					{kind = 'hztl',
						{kind = 'img', desc='Canisters', img = 'starsoul-item-element-canister.png', w=1,h=1};
						{kind = 'list', target = 'current_player', inv = 'starsoul_suit_canisters',
							listContent = nil, w = canW, h = canH, spacing = 0.1};
					};
					{kind = 'hztl';
						{kind = 'img', w=1,h=1, item = suit.item:get_name(),
							desc = suit.item:get_definition().short_description};
						{kind = 'button', w=1.5,h=1, id = 'powerMode_off', label = 'Off';
							color=modeColor'off'};
						{kind = 'button', w=2.5,h=1, id = 'powerMode_save', label = 'Power Save';
							color=modeColor'powerSave'};
						{kind = 'button', w=1.5,h=1, id = 'powerMode_on', label = 'On'; 
							color=modeColor'on'};
					};
					{kind = 'list', target = 'current_player', inv = 'main', w = 6, h = 1, spacing = 0.1};
				}
			end;
			handle = function(state, user, q)
				local suitMode
				if     q.powerMode_off  then suitMode = 'off'
				elseif q.powerMode_save then suitMode = 'powerSave'
				elseif q.powerMode_on   then suitMode = 'on' end
				if suitMode then
					user:suitPowerStateSet(suitMode)
					return true
				end
			end;
		};
	};
})

starsoul.interface.install(starsoul.type.ui {
	id = 'starsoul:compile-matter-component';
	pages = {
		index = {
			setupState = function(state, user, ctx)
				if ctx.context == 'suit' then
				end
				state.pgm = ctx.program
			end;
			render = function(state, user)
				return starsoul.ui.build {
					kind = 'vert', padding = 0.5; w = 5, h = 5, mode = 'sw';
					{kind = 'label', w = 4, h = 1, text = 'hello'};
				}
			end;
		};
	};
})

Added mods/starsoul/item.lua version [597f09c699].



































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
local lib = starsoul.mod.lib
local I = starsoul.item

function I.mk(item, context)
	local st = ItemStack(item)
	local md = st:get_definition()._starsoul
	local ctx = context or {}
	if md and md.event then
		md.event.create(st, ctx)
	end
	if context.how == 'print' then
		if context.schematic and context.schematic.setup then
			context.schematic.setup(st, ctx)
		end
	end
	return st
end

Added mods/starsoul/mod.conf version [7e3c22e1be].









>
>
>
>
1
2
3
4
name = starsoul
author = velartrill
description = world logic and UI
depends = vtlib

Added mods/starsoul/species.lua version [9c138bbb33].













































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local lib = starsoul.mod.lib

local paramTypes do local T,G = lib.marshal.t, lib.marshal.g
	paramTypes = {
		tone = G.struct {
			hue = T.angle;
			sat = T.clamp;
			lum = T.clamp;
		};
		str = T.str;
		num = T.decimal;
	}
end

-- constants
local animationFrameRate = 60

local species = {
	human = {
		name = 'Human';
		desc = 'The weeds of the galactic flowerbed. Humans are one of the Lesser Races, excluded from the ranks of the Greatest Races by souls that lack, in normal circumstances, external psionic channels. Their mastery of the universe cut unexpectedly short, forever locked out of FTL travel, short-lived without augments, and alternately pitied or scorned by the lowest of the low, humans flourish nonetheless due to a capacity for adaptation unmatched among the Thinking Few, terrifyingly rapid reproductive cycles -- and a keen facility for bribery. While the lack of human psions remains a sensitive topic, humans (unlike the bitter and emotional Kruthandi) are practical enough to hire the talent they cannot possess, and have even built a small number of symbiotic civilizations with the more indulging of the Powers. In a galaxy where nearly all sophont life is specialized to a fault, humans have found the unique niche of occupying no particular niche.';
		scale = 1.0;
		params = {
			{'eyeColor',  'Eye Color',  'tone', {hue=327, sat=0, lum=0}};
			{'hairColor', 'Hair Color', 'tone', {hue=100, sat=0, lum=0}};
			{'skinTone',  'Skin Tone',  'tone', {hue=  0, sat=0, lum=0}};
		};
		tempRange = {
			comfort    = {18.3, 23.8}; -- needed for full stamina regen
			survivable = {5,    33}; -- anything below/above will cause progressively more damage
		};
		variants = {
			female = {
				name = 'Human Female';
				mesh = 'starsoul-body-female.x';
				eyeHeight = 1.4;
				texture = function(t, adorn)
					local skin = lib.image 'starsoul-body-skin.png' : shift(t.skinTone)
					local eye  = lib.image 'starsoul-body-eye.png'  : shift(t.eyeColor)
					local hair = lib.image 'starsoul-body-hair.png' : shift(t.hairColor)

					local invis = lib.image '[fill:1x1:0,0:#00000000'
					local plate = adorn.suit and adorn.suit.plate or invis
					local lining = adorn.suit and adorn.suit.lining or invis

					return {lining, plate, skin, skin, eye, hair}
				end;
				stats = {
					psiRegen = 1.3;
					psiPower = 1.2;
					psi = 1.2;
					hunger = .8; -- women have smaller stomachs
					thirst = .8;
					staminaRegen = 1.0;
					morale = 0.8; -- you are not She-Bear Grylls
				};
				traits = {
					health = 400;
					lungCapacity = .6;
					irradiation = 0.8; -- you are smaller, so it takes less rads to kill ya
					sturdiness = 0; -- women are more fragile and thus susceptible to blunt force trauma
					metabolism = 1800; --Cal
					painTolerance = 0.4;
				};
			};
			male = {
				name = 'Human Male';
				eyeHeight = 1.6;
				stats = {
					psiRegen = 1.0;
					psiPower = 1.0;
					psi = 1.0;
					hunger = 1.0;
					staminaRegen = .7; -- men are strong but have inferior endurance
				};
				traits = {
					health = 500;
					painTolerance = 1.0;
					lungCapacity = 1.0;
					sturdiness = 0.3;
					metabolism = 2200; --Cal
				};
			};
		};
		traits = {};
	};
}

starsoul.world.species = {
	index = species;
	paramTypes = paramTypes;
}

function starsoul.world.species.mkDefaultParamsTable(pSpecies, pVariant)
	local sp = species[pSpecies]
	local var = sp.variants[pVariant]
	local vpd = var.defaults or {}
	local tbl = {}
	for _, p in pairs(sp.params) do
		local name, desc, ty, dflt = lib.tbl.unpack(p)
		tbl[name] = vpd[name] or dflt
	end
	return tbl
end


function starsoul.world.species.mkPersonaFor(pSpecies, pVariant)
	return {
		species = pSpecies;
		speciesVariant = pVariant;
		bodyParams = starsoul.world.species.paramsFromTable(pSpecies,
			starsoul.world.species.mkDefaultParamsTable(pSpecies, pVariant)
		);
		statDeltas = {};
	}
end

local function spLookup(pSpecies, pVariant)
	local sp = species[pSpecies]
	local var = sp.variants[pVariant or next(sp.variants)]
	return sp, var
end
starsoul.world.species.lookup = spLookup

function starsoul.world.species.statRange(pSpecies, pVariant, pStat)
	local sp,spv = spLookup(pSpecies, pVariant)
	local min, max, base
	if pStat == 'health' then
		min,max = 0, spv.traits.health
	elseif pStat == 'breath' then
		min,max = 0, 65535
	else
		local spfac = spv.stats[pStat]
		local basis = starsoul.world.stats[pStat]
		min,max = basis.min, basis.max

		if spfac then
			min = min * spfac
			max = max * spfac
		end

		base = basis.base
		if base == true then
			base = max
		elseif base == false then
			base = min
		end

	end
	return min, max, base
end

-- set the necessary properties and create a persona for a newspawned entity
function starsoul.world.species.birth(pSpecies, pVariant, entity, circumstances)
	circumstances = circumstances or {}
	local sp,var = spLookup(pSpecies, pVariant)

	local function pct(st, p)
		local min, max = starsoul.world.species.statRange(pSpecies, pVariant, st)
		local delta = max - min
		return min + delta*p
	end
	local ps = starsoul.world.species.mkPersonaFor(pSpecies,pVariant)
	local startingHP = pct('health', 1.0)
	if circumstances.injured    then startingHP = pct('health', circumstances.injured) end
	if circumstances.psiCharged then ps.statDeltas.psi = pct('psi', circumstances.psiCharged) end
	ps.statDeltas.warmth = 20 -- don't instantly start dying of frostbite

	entity:set_properties{hp_max = var.traits.health or sp.traits.health}
	entity:set_hp(startingHP, 'initial hp')
	return ps
end

function starsoul.world.species.paramsFromTable(pSpecies, tbl)
	local lst = {}
	local sp = species[pSpecies]
	for i, par in pairs(sp.params) do
		local name,desc,ty,dflt = lib.tbl.unpack(par)
		if tbl[name] then
			table.insert(lst, {id=name, value=paramTypes[ty].enc(tbl[name])})
		end
	end
	return lst
end
function starsoul.world.species.paramsToTable(pSpecies, lst)
	local tymap = {}
	local sp = species[pSpecies]
	for i, par in pairs(sp.params) do
		local name,desc,ty,dflt = lib.tbl.unpack(par)
		tymap[name] = paramTypes[ty]
	end

	local tbl = {}
	for _, e in pairs(lst) do
		tbl[e.id] = tymap[e.id].dec(e.value)
	end
	return tbl
end

for speciesName, sp in pairs(species) do
	for varName, var in pairs(sp.variants) do
		if var.mesh then
			var.animations = starsoul.evaluate(string.format('models/%s.nla', var.mesh)).skel.action
		end
	end
end


function starsoul.world.species.updateTextures(ent, persona, adornment)
	local s,v = spLookup(persona.species, persona.speciesVariant)
	local paramTable = starsoul.world.species.paramsToTable(persona.species, persona.bodyParams)
	local texs = {}
	for i, t in ipairs(v.texture(paramTable, adornment)) do
		texs[i] = t:render()
	end
	ent:set_properties { textures = texs }
end

function starsoul.world.species.setupEntity(ent, persona)
	local s,v = spLookup(persona.species, persona.speciesVariant)
	local _, maxHealth = starsoul.world.species.statRange(persona.species, persona.speciesVariant, 'health')
	ent:set_properties {
		visual = 'mesh';
		mesh = v.mesh;
		stepheight = .51;
		eye_height = v.eyeHeight;
		collisionbox = { -- FIXME
			-0.3, 0.0, -0.3;
			0.3, 1.5,  0.3;
		};
		visual_size = vector.new(10,10,10) * s.scale;

		hp_max = maxHealth;
	}
	local function P(v)
		if v then return {x=v[1],y=v[2]} end
		return {x=0,y=0}
	end
	ent:set_local_animation(
		P(v.animations.idle),
		P(v.animations.run),
		P(v.animations.work),
		P(v.animations.runWork),
		animationFrameRate)

end

Added mods/starsoul/stats.lua version [39aabe1578].



































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local lib = starsoul.mod.lib

local function U(unit, prec, fixed)
	if fixed then
		return function(amt, excludeUnit)
			if excludeUnit then return tostring(amt/prec) end
			return string.format("%s %s", amt/prec, unit)
		end
	else
		return function(amt, excludeUnit)
			if excludeUnit then return tostring(amt/prec) end
			return lib.math.si(unit, amt/prec)
		end
	end
end

local function C(h, s, l)
	return lib.color {hue = h, sat = s or 1, lum = l or .7}
end
starsoul.world.stats = {
	psi        = {min = 0, max = 500, base = 0, desc = U('ψ', 10), color = C(320), name = 'Numina'};
	-- numina is measured in daψ
	warmth     = {min = -1000, max = 1000, base = 0, desc = U('°C', 10, true), color = C(5), name = 'Warmth'};
	-- warmth in measured in °C×10
	fatigue    = {min = 0, max = 76 * 60, base = 0, desc = U('hr', 60, true), color = C(288,.3,.5), name = 'Fatigue'};
	-- fatigue is measured in minutes one needs to sleep to cure it
	stamina    = {min = 0, max = 20 * 100, base = true, desc = U('m', 100), color = C(88), name = 'Stamina'};
	-- stamina is measured in how many 10th-nodes (== cm) one can sprint
	hunger     = {min = 0, max = 20000, base = 0, desc = U('Cal', 1), color = C(43,.5,.4), name = 'Hunger'};
	-- hunger is measured in calories one must consume to cure it
	thirst     = {min = 0, max = 1600, base = 0, desc = U('l', 100), color = C(217, .25,.4), name = 'Thirst'};
	-- thirst is measured in centiliters of H²O required to cure it
	morale     = {min = 0, max = 24 * 60 * 10, base = true, desc = U('hr', 60, true), color = C(0,0,.8), name = 'Morale'};
	-- morale is measured in minutes. e.g. at base rate morale degrades by
	-- 60 points every hour. morale can last up to 10 days
	irradiation = {min = 0, max = 20000, base = 0, desc = U('Gy', 1000), color = C(141,1,.5), name = 'Irradiation'};
	-- irrad is measured is milligreys
	-- 1Gy counters natural healing
	-- ~3Gy counters basic nanomedicine
	-- 5Gy causes death within two weeks without nanomedicine
	-- radiation speeds up psi regen
	-- morale drain doubles with each 2Gy
	illness    = {min = 0, max = 1000, base = 0, desc = U('%', 10, true), color = C(71,.4,.25), name = 'Illness'};
	-- as illness increases, maximum stamina and health gain a corresponding limit
	-- illness is increased by certain conditions, and decreases on its own as your
	-- body heals when those conditions wear off. some drugs can lower accumulated illness
	-- but illness-causing conditions require specific cures
	-- illness also causes thirst and fatigue to increase proportionately
}

Added mods/starsoul/store.lua version [efa19f35ab].











































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
-- [ʞ] store.lua
--  ~ lexi hale <lexi@hale.su>
--  © EUPLv1.2
--  ? defines serialization datatypes that don't belong to
--    any individual class

local lib = starsoul.mod.lib
local T,G = lib.marshal.t, lib.marshal.g
starsoul.store = {} -- the serialization equivalent of .type

-------------
-- persona --
------------- -----------------------------------------------
-- a Persona is a structure that defines the nature of     --
-- an (N)PC and how it interacts with the Starsoul-managed --
-- portion of the game world -- things like name, species, --
-- stat values, physical characteristics, and so forth     --

local statStructFields = {}
for k,v in pairs(starsoul.world.stats) do
	statStructFields[k] = v.srzType or (
		(v.base == true or v.base > 0) and T.s16 or T.u16
	)
end

starsoul.store.compilerJob = G.struct {
	schematic = T.str;
	progress = T.clamp;
}

starsoul.store.persona = G.struct {
	name = T.str;
	species = T.str;
	speciesVariant = T.str;
	background = T.str;
	bodyParams = G.array(8, G.struct {id = T.str, value = T.str}); --variant

	statDeltas = G.struct(statStructFields);

	facts = G.array(32, G.array(8, T.str));
	-- facts stores information the player has discovered and narrative choices
	-- she has made.
	-- parametric facts are encoded as horn clauses
	-- non-parametric facts are encoded as {'fact-mod:fact-id'}
}

starsoul.store.suitMeta = lib.marshal.metaStore {
	batteries = {key = 'starsoul:suit_slots_bat', type = T.inventoryList};
	chips = {key = 'starsoul:suit_slots_chips', type = T.inventoryList};
	elements = {key = 'starsoul:suit_slots_elem', type = T.inventoryList};
	guns = {key = 'starsoul:suit_slots_gun', type = T.inventoryList};
	ammo = {key = 'starsoul:suit_slots_ammo', type = T.inventoryList};
}

Added mods/starsoul/suit.lua version [5d51eaa4b3].















































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local lib = starsoul.mod.lib

local suitStore = starsoul.store.suitMeta
starsoul.item.suit = lib.registry.mk 'starsoul:suits';

-- note that this cannot be persisted as a reference to a particular suit in the world
local function suitContainer(stack, inv)
	return starsoul.item.container(stack, inv, {
		pfx = 'starsoul_suit'
	})
end
starsoul.type.suit = lib.class {
	name = 'starsoul:suit';
	construct = function(stack)
		return {
			item = stack;
			inv = suitStore(stack);
		}
	end;
	__index = {
		powerState = function(self)
			local s = self.item
			if not s then return nil end
			local m = s:get_meta():get_int('starsoul:power_mode')
			if m == 1 then return 'on'
			elseif m == 2 then return 'powerSave'
			else return 'off' end
		end;
		powerStateSet = function(self, state)
			local s = self.item
			if not s then return nil end
			local m
			if state == 'on' then m = 1 -- TODO check power level
			elseif state == 'powerSave' then m = 2
			else m = 0 end
			if self:powerLeft() <= 0 then m = 0 end
			s:get_meta():set_int('starsoul:power_mode', m)
		end;
		powerLeft = function(self)
			local batteries = self.inv.read 'batteries'
			local power = 0
			for idx, slot in pairs(batteries) do
				power = power + starsoul.mod.electronics.dynamo.totalPower(slot)
			end
			return power
		end;
		powerCapacity = function(self)
			local batteries = self.inv.read 'batteries'
			local power = 0
			for idx, slot in pairs(batteries) do
				power = power + starsoul.mod.electronics.dynamo.initialPower(slot)
			end
			return power
		end;
		maxPowerUse = function(self)
			local batteries = self.inv.read 'batteries'
			local w = 0
			for idx, slot in pairs(batteries) do
				w = w + starsoul.mod.electronics.dynamo.dischargeRate(slot)
			end
			return w
		end;
		onReconfigure = function(self, inv)
			-- apply any changes to item metadata and export any subinventories
			-- to the provided invref, as they may have changed
			local sc = starsoul.item.container(self.item, inv, {pfx = 'starsoul_suit'})
			sc:push()
			self:pullCanisters(inv)
		end;
		onItemMove = function(self, user, list, act, what)
			-- called when the suit inventory is changed
			if act == 'put' then
				if list == 'starsoul_suit_bat' then
					user:suitSound('starsoul-suit-battery-in')
				elseif list == 'starsoul_suit_chips' then
					user:suitSound('starsoul-suit-chip-in')
				elseif list == 'starsoul_suit_canisters' then
					user:suitSound('starsoul-insert-snap')
				end
			elseif act == 'take' then
				if list == 'starsoul_suit_bat' then
					user:suitSound('starsoul-insert-snap')
				elseif list == 'starsoul_suit_chips' then
					--user:suitSound('starsoul-suit-chip-out')
				elseif list == 'starsoul_suit_canisters' then
					user:suitSound('starsoul-insert-snap')
				end
			end
		end;
		def = function(self)
			return self.item:get_definition()._starsoul.suit
		end;
		pullCanisters = function(self, inv)
			starsoul.item.container.dropPrefix(inv, 'starsoul_canister')
			self:forCanisters(inv, function(sc) sc:pull() end)
		end;
		pushCanisters = function(self, inv, st, i)
			self:forCanisters(inv, function(sc)
				sc:push()
				return true
			end)
		end;
		forCanisters = function(self, inv, fn)
			local cans = inv:get_list 'starsoul_suit_canisters'
			if cans and next(cans) then for i, st in ipairs(cans) do
				if not st:is_empty() then
					local pfx = 'starsoul_canister_' .. tostring(i)
					local sc = starsoul.item.container(st, inv, {pfx = pfx})
					if fn(sc, st, i, pfx) then
						inv:set_stack('starsoul_suit_canisters', i, st)
					end
				end
			end end
		end;
		establishInventories = function(self, obj)
			local inv = obj:get_inventory()
			local ct = suitContainer(self.item, inv)
			ct:pull()
			self:pullCanisters(inv)

			--[[
			local def = self:def()
			local sst = suitStore(self.item)
			local function readList(listName, prop)
				inv:set_size(listName, def.slots[prop])
				if def.slots[prop] > 0 then
					local lst = sst.read(prop)
					inv:set_list(listName, lst)
				end
			end
			readList('starsoul_suit_chips', 'chips')
			readList('starsoul_suit_bat',   'batteries')
			readList('starsoul_suit_guns',  'guns')
			readList('starsoul_suit_elem',  'elements')
			readList('starsoul_suit_ammo',  'ammo')
			]]
		end;
	};
}

-- TODO find a better place for this!
starsoul.type.suit.purgeInventories = function(obj)
	local inv = obj:get_inventory()
	starsoul.item.container.dropPrefix(inv, 'starsoul_suit')
	starsoul.item.container.dropPrefix(inv, 'starsoul_canister')
	--[[inv:set_size('starsoul_suit_bat', 0)
	inv:set_size('starsoul_suit_guns', 0)
	inv:set_size('starsoul_suit_chips', 0)
	inv:set_size('starsoul_suit_ammo', 0)
	inv:set_size('starsoul_suit_elem', 0)
	]]
end

starsoul.item.suit.foreach('starsoul:suit-gen', {}, function(id, def)
	local icon = lib.image(def.img or 'starsoul-item-suit.png')

	local iconColor = def.iconColor
	if not iconColor then
		iconColor = (def.tex and def.tex.plate and def.tex.plate.tint)
			or def.defaultColor
		iconColor = iconColor:to_hsl()
		iconColor.lum = 0
	end

	if iconColor then icon = icon:shift(iconColor) end

	if not def.adorn then
		function def.adorn(a, item, persona)
			local function imageFor(pfx)
				return lib.image(string.format("%s-%s-%s.png", pfx, persona.species, persona.speciesVariant))
			end
			if not def.tex then return end
			a.suit = {}
			for name, t in pairs(def.tex) do
				local img = imageFor(t.id)
				local color

				local cstr = item:get_meta():get_string('starsoul:tint_suit_' .. name)
				if cstr and cstr ~= '' then
					color = lib.color.unmarshal(cstr)
				elseif t.tint then
					color = t.tint or def.defaultColor
				end

				if color then
					local hsl = color:to_hsl()
					local adjusted = {
						hue = hsl.hue;
						sat = hsl.sat * 2 - 1;
						lum = hsl.lum * 2 - 1;
					}
					img = img:shift(adjusted)
				end

				a.suit[name] = img
			end
		end
	end

	minetest.register_tool(id, {
		short_description = def.name;
		description = starsoul.ui.tooltip {
			title = def.name;
			desc = def.desc;
			color = lib.color(.1, .7, 1);
		};
		groups = {
			suit = 1;
			inv = 1; -- has inventories
			batteryPowered = 1; -- has a battery inv 
			programmable = 1; -- has a chip inv
		};
		on_use = function(st, luser, pointed)
			local user = starsoul.activeUsers[luser:get_player_name()]
			if not user then return end
			-- have mercy on users who've lost their suits and wound
			-- up naked and dying of exposure
			if user:naked() then
				local ss = st:take_item(1)
				user:setSuit(starsoul.type.suit(ss))
				user:suitSound('starsoul-suit-don')
				return st
			end
		end;
		inventory_image = icon:render();
		_starsoul = {
			container = {
				workbench = {
					order = {'batteries','chips','guns','ammo'}
				};
				list = {
					bat = {
						key = 'starsoul:suit_slots_bat';
						accept = 'dynamo';
						sz = def.slots.batteries;
					};
					chips = {
						key = 'starsoul:suit_slots_chips';
						accept = 'chip';
						sz = def.slots.chips;
					};
					canisters = {
						key = 'starsoul:suit_slots_canisters';
						accept = 'canister';
						sz = def.slots.canisters;
					};
					guns = {
						key = 'starsoul:suit_slots_gun';
						accept = 'weapon';
						workbench = {
							label = 'Weapon';
							icon = 'starsoul-ui-icon-gun';
							color = lib.color(1,0,0);
						};
						sz = def.slots.guns;
					};
					ammo = {
						key = 'starsoul:suit_slots_ammo';
						accept = 'ammo';
						workbench = {
							label = 'Ammunition';
							color = lib.color(1,.5,0);
							easySlots = true; -- all slots accessible on the go
						};
						sz = def.slots.ammo;
					};
				};
			};
			event = {
				create = function(st,how)
					local s = suitStore(st)
					-- make sure there's a defined powerstate
					starsoul.type.suit(st):powerStateSet 'off'
					suitContainer(st):clear()
					--[[ populate meta tables
					s.write('batteries', {})
					s.write('guns', {})
					s.write('ammo', {})
					s.write('elements', {})
					s.write('chips', {})]]
				end;
			};
			suit = def;
		};
	});
end)

local slotProps = {
	starsoul_cfg = {
		itemClass = 'inv';
	};
	starsoul_suit_bat = {
		suitSlot = true;
		powerLock = true;
		itemClass = 'dynamo';
	};
	starsoul_suit_chips = {
		suitSlot = true;
		powerLock = true;
		itemClass = 'chip';
	};
	starsoul_suit_guns = {
		suitSlot = true;
		maintenanceNode = '';
		itemClass = 'suitWeapon';
	};
	starsoul_suit_ammo = {
		suitSlot = true;
		maintenanceNode = '';
		itemClass = 'suitAmmo';
	};
	starsoul_suit_canisters = {
		suitSlot = true;
		itemClass = 'canister';
	};
}

minetest.register_allow_player_inventory_action(function(luser, act, inv, p)
	local user = starsoul.activeUsers[luser:get_player_name()]
	local function grp(i,g)
		return minetest.get_item_group(i:get_name(), g) ~= 0
	end
	local function checkBaseRestrictions(list)
		local restrictions = slotProps[list]
		if not restrictions then return nil, true end
		if restrictions.suitSlot then
			if user:naked() then return restrictions, false end
		end
		if restrictions.powerLock then
			if user:getSuit():powerState() ~= 'off' then return restrictions, false end
		end
		return restrictions, true
	end
	local function itemFits(item, list)
		local rst, ok = checkBaseRestrictions(list)
		if not ok then return false end
		if rst == nil then return true end

		if rst.itemClass and not grp(item, rst.itemClass) then
			return false
		end
		if rst.maintenanceNode then return false end
		-- FIXME figure out best way to identify when the player is using a maintenance node

		if grp(item, 'specialInventory') then
			if grp(item, 'powder') and list ~= 'starsoul_suit_elem' then return false end
			-- FIXME handle containers
			if grp(item, 'psi') and list ~= 'starsoul_psi' then return false end
		end

		return true
	end
	local function itemCanLeave(item, list)
		local rst, ok = checkBaseRestrictions(list)
		if not ok then return false end
		if rst == nil then return true end

		if minetest.get_item_group(item:get_name(), 'specialInventory') then

		end

		if rst.maintenanceNode then return false end
		return true
	end

	if act == 'move' then
		local item = inv:get_stack(p.from_list, p.from_index)
		if not (itemFits(item, p.to_list) and itemCanLeave(item, p.from_list)) then
			return 0
		end
	elseif act == 'put' then
		if not itemFits(p.stack, p.listname) then return 0 end
	elseif act == 'take' then
		if not itemCanLeave(p.stack, p.listname) then return 0 end
	end
	return true
end)

minetest.register_on_player_inventory_action(function(luser, act, inv, p)
	local user = starsoul.activeUsers[luser:get_player_name()]
	local function slotChange(slot,a,item)
		local s = slotProps[slot]
		if slot == 'starsoul_suit' then
			user:updateSuit()
			if user:naked() then
				starsoul.type.suit.purgeInventories(user.entity)
				user.power.nano = {}
			end
		elseif s and s.suitSlot then
			local s = user:getSuit()
			s:onItemMove(user, slot, a, item)
			s:onReconfigure(user.entity:get_inventory())
			user:setSuit(s)
		else return end
		user:updateHUD()
	end

	if act == 'put' or act == 'take' then
		local item = p.stack
		slotChange(p.listname, act, item)
	elseif act == 'move' then
		local item = inv:get_stack(p.to_list, p.to_index)
		slotChange(p.from_list, 'take', item)
		slotChange(p.to_list, 'put', item)
	end
end)

local suitInterval = 2.0
starsoul.startJob('starsoul:suit-software', suitInterval, function(delta)
	local runState = {
		pgmsRun = {};
		flags = {};
	}
	for id, u in pairs(starsoul.activeUsers) do
		if not u:naked() then
			local reconfSuit = false
			local inv = u.entity:get_inventory()
			local chips = inv:get_list('starsoul_suit_chips')
			local suitprog = starsoul.mod.electronics.chip.usableSoftware(chips)
			for _, prop in pairs(suitprog) do
				local s = prop.sw
				if s.kind == 'suitPower' and (s.powerKind == 'passive' or s.bgProc) and (not runState.pgmsRun[s]) then
					local conf = prop.file.body.conf
					local enabled = true
					for _, e in ipairs(conf) do
						if e.key == 'disable'  and e.value == 'yes' then
							enabled = false
							break
						end
					end
					local fn if s.powerKind == 'passive'
						then fn = s.run
						else fn = s.bgProc
					end
					function prop.saveConf(cfg) cfg = cfg or conf
						prop.fd:write(cfg)
						inv:set_stack('starsoul_suit_chips', prop.chipSlot, prop.fd.chip)
						reconfSuit = true
					end
					function prop.giveItem(st)
						u:thrustUpon(st)
					end
					
					if enabled and fn(u, prop, suitInterval, runState) then
						runState.pgmsRun[s] = true
					end
				end
			end
			if reconfSuit then
				u:reconfigureSuit()
			end
		end
	end
end)

Added mods/starsoul/terrain.lua version [cc82288cf4].







































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local T = starsoul.translator
local lib = starsoul.mod.lib

starsoul.terrain = {}
local soilSounds = {}
local grassSounds = {}

minetest.register_node('starsoul:soil', {
	description = T 'Soil';
	tiles = {'default_dirt.png'};
	groups = {dirt = 1};
	drop = '';
	sounds = soilSounds;
	_starsoul = {
		onDestroy = function() end;
		kind = 'block';
		elements = {};
	};
})


minetest.register_node('starsoul:sand', {
	description = T 'Sand';
	tiles = {'default_sand.png'};
	groups = {dirt = 1};
	drop = '';
	sounds = soilSounds;
	_starsoul = {
		kind = 'block';
		fab = starsoul.type.fab { element = { silicon = 25 } };
	};
})
minetest.register_craftitem('starsoul:soil_clump', {
	short_description = T 'Soil';
	description = starsoul.ui.tooltip {
		title = T 'Soil';
		desc = 'A handful of nutrient-packed soil, suitable for growing plants';
		color = lib.color(0.3,0.2,0.1);
	};
	inventory_image = 'starsoul-item-soil.png';
	groups = {soil = 1};
	_starsoul = {
		fab = starsoul.type.fab { element = { carbon = 12 / 4 } };
	};
})

function starsoul.terrain.createGrass(def)
	local function grassfst(i)
		local nextNode = def.name
		if i >= 0 then
			nextNode = nextNode .. '_walk_' .. tostring(i)
		end
		return {
			onWalk = function(pos)
				minetest.set_node_at(pos, def.name .. '_walk_2');
			end;
			onDecay = function(pos,delta)
				minetest.set_node_at(pos, nextNode);
			end;
			onDestroy = function(pos) end;
			fab = def.fab;
			recover = def.recover;
			recover_vary = def.recover_vary;
		};
	end
	local drop = {
		max_items = 4;
		items = {
			{
				items = {'starsoul:soil'}, rarity = 2;
				tool_groups = { 'shovel', 'trowel' };
			};
		};
	}
	minetest.register_node(def.name, {
		description = T 'Greengraze';
		tiles = {
			def.img .. '.png';
			'default_dirt.png';
			{
				name = 'default_dirt.png^' .. def.img ..'_side.png';
				tileable_vertical = false;
			};
		};
		groups = {grass = 1, sub_walk = 1};
		drop = '';
		sounds = grassSounds;
		_starsoul = grassfst(2);
	})
	for i=2,0,-1 do
		local opacity = tostring((i/2.0) * 255)

		minetest.register_node(def.name, {
			description = def.desc;
			tiles = {
				def.img .. '.png^(default_footprint.png^[opacity:'..opacity..')';
				'default_dirt.png';
				{
					name = 'default_dirt.png^' .. def.img ..'_side.png';
					tileable_vertical = false;
				};
			};
			groups = {grass = 1, sub_walk = 1, sub_decay = 5};
			drop = '';
			_starsoul = grassfst(i-1);
			sounds = grassSounds;
		})
	end
end


starsoul.terrain.createGrass {
	name = 'starsoul:greengraze';
	desc = T 'Greengraze';
	img = 'default_grass';
	fab = starsoul.type.fab {
		element = {
			carbon = 12;
		};
		time = {
			shred = 2.5;
		};
	};
}

for _, w in pairs {false,true} do
	minetest.register_node('starsoul:liquid_water' .. (w and '_flowing' or ''), {
		description = T 'Water';
		drawtype = 'liquid';
		waving = 3;
		tiles = {
			{
				name = "default_water_source_animated.png";
				backface_culling = false;
				animation = {
					type = "vertical_frames";
					aspect_w = 16;
					aspect_h = 16;
					length = 2.0;
				};
			};
			{
				name = "default_water_source_animated.png";
				backface_culling = true;
				animation = {
					type = "vertical_frames";
					aspect_w = 16;
					aspect_h = 16;
					length = 2.0;
				};
			};
		};
		use_texture_alpha = 'blend';
		paramtype = 'light';
		walkable = false, pointable = false, diggable = false, buildable_to = true;
		is_ground_content = false;
		drop = '';
		drowning = 1;
		liquidtype = w and 'flowing' or 'source';
		liquid_alternative_flowing = 'starsoul:liquid_water_flowing';
		liquid_alternative_source = 'starsoul:liquid_water';
		liquid_viscosity = 1;
		liquid_renewable = true;
		liquid_range = 2;
		drowning = 40;
		post_effect_color = {a=103, r=10, g=40, b=70};
		groups = {water = 3, liquid = 3};
	});
end


starsoul.world.mineral.foreach('starsoul:mineral_generate', {}, function(name,m)
	local node = string.format('starsoul:mineral_%s', name)
	local grp = {mineral = 1}
	minetest.register_node(node, {
		description = m.desc;
		tiles = m.tiles or 
				(m.tone and {
					string.format('default_stone.png^[colorizehsl:%s:%s:%s',
						m.tone.hue, m.tone.sat, m.tone.lum)
				}) or {'default_stone.png'};
		groups = grp;
		drop = m.rocks or '';
		_starsoul = {
			kind = 'block';
			elements = m.elements;
			fab = m.fab;
			recover = m.recover;
			recover_vary = m.recover_vary;
		};
	})
	if not m.excludeOre then
		local seed = 0
		grp.ore = 1
		for i = 1, #m.name do
			seed = seed*50 + string.byte(name, i)
		end
		minetest.register_ore {
			ore = node;
			ore_type = m.dist.kind;
			wherein = {m.dist.among};
			clust_scarcity = m.dist.rare;
			y_max = m.dist.height[1], y_min = m.dist.height[2];
			noise_params = m.dist.noise or {
				offset = 28;
				scale = 16;
				spread = vector.new(128,128,128);
				seed = seed;
				octaves = 1;
			};
		}
	end
end)

starsoul.world.mineral.link('feldspar', {
	desc = T 'Feldspar';
	excludeOre = true;
	recover = starsoul.type.fab {
		time = {
			shred = 3;
		};
		cost = {
			shredPower = 3;
		};
	};
	recover_vary = function(rng, ctx)
		print('vary!', rng:int(), rng:int(0,10))
		return starsoul.type.fab {
			element = {
				aluminum  = rng:int(0,4);
				potassium = rng:int(0,2);
				calcium   = rng:int(0,2);
			}
		};
	end;
})

-- map generation

minetest.register_alias('mapgen_stone', 'starsoul:mineral_feldspar')
minetest.register_alias('mapgen_water_source', 'starsoul:liquid_water')
minetest.register_alias('mapgen_river_water_source', 'starsoul:liquid_water')

Added mods/starsoul/tiers.lua version [97d9d01bfc].



















































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local lib = starsoul.mod.lib

starsoul.world.tier = lib.registry.mk 'starsoul:tier'
local T = starsoul.world.tier
local fab = starsoul.type.fab

function starsoul.world.tier.fabsum(name, ty)
	local dest = fab {}
	local t = starsoul.world.tier.db[name]
	assert(t, 'reference to nonexisting tier '..name)
	if t.super then
		dest = dest+starsoul.world.tier.fabsum(t.super, ty)*(t.cost or 1)
	end
	if t.fabclasses and t.fabclasses[ty] then
		dest = dest + t.fabclasses[ty]
	end
	return dest
end

function starsoul.world.tier.tech(name, tech)
	local t = starsoul.world.tier.db[name]
	if t.techs and t.techs[tech] ~= nil then return t.techs[tech] end
	if t.super then return starsoul.world.tier.tech(t.super, tech) end
	return false
end

T.meld {
	base = {
		fabclass = {
			electric = fab {metal={copper = 10}};
			suit = fab {element={carbon = 1e3}};
			psi = fab {metal={numinium = 1}};
			bio = fab {element={carbon = 1}};
		};

	}; -- properties that apply to all tiers
	------------------
	-- tier classes --
	------------------

	lesser = {
		name = 'Lesser', adj = 'Lesser';
		super = 'base';
		fabclasses = {
			basis = fab {
				metal = {aluminum=4};
			};
		};
	};
	greater = {
		name = 'Greater', adj = 'Greater';
		super = 'base';
		fabclasses = {
			basis = fab {
				metal = {vanadium=2};
			};
		};
	};
	starsoul = {
		name = 'Starsoul', adj = 'Starsoul';
		super = 'base';
		fabclasses = {
			basis = fab {
				metal = {osmiridium=1};
			};
		};
	};
	forevanished = {
		name = 'Forevanished One', adj = 'Forevanished';
		super = 'base';
		fabclasses = {
			basis = fab {
				metal = {elusium=1};
			};
		};
	};

	------------------
	-- Lesser Races --
	------------------

	makeshift = { -- regular trash
		name = 'Makeshift', adj = 'Makeshift';
		super = 'lesser';
		techs = {tool = true, prim = true, electric = true};
		power = 0.5;
		efficiency = 0.3;
		reliability = 0.2;
		cost = 0.3;
		fabclasses = { -- characteristic materials
			basis = fab { -- fallback
				metal = {iron=3};
			};
		};
	};

	imperial = { --powerful trash
		name = 'Imperial', adj = 'Imperial';
		super = 'lesser';
		techs = {tool = true, electric = true, electronic = true, suit = true, combatSuit = true, weapon = true, hover='ion'};
		power = 2.0;
		efficiency = 0.5;
		reliability = 0.5;
		cost = 1.0;
		fabclasses = {
			basis = fab {
				metal = {steel=2};
			};
		};
	};

	commune = { --reliability
		name = 'Commune', adj = 'Commune';
		super = 'lesser';
		techs = {tool = true, electric = true, electronic = true, suit = true, combatSuit = true, weapon = true, gravitic = true, hover='grav'};
		power = 1.0;
		efficiency = 2.0;
		reliability = 3.0;
		cost = 1.5;
		fabclasses = {
			basis = fab {
				metal = {titanium=1};
				time = {print = 1.2}; -- commune stuff is intricate
			};
		};
	};

	-------------------
	-- Greater Races --
	-------------------


	----------------
	-- Starsouled --
	----------------

	suIkuri = { --super-tier
		name = 'Su\'ikuri', adj = "Su'ikuruk";
		super = 'starsoul';
		techs = {psi = true, prim = true, bioSuit = true, psiSuit = true};
		power = 1.5;
		efficiency = 1.0;
		reliability = 3.0;
		cost = 2.0;
		fabclasses = {
			psi = fab {
				metal = {numinium = 2.0};
				crystal = {beryllium = 1.0};
			};
			bio = fab {
				crystal = {beryllium = 1.0};
			};
		};
	};

	usukwinya = { --value for 'money'; no weapons; no hovertech (they are birds)
		-- NOTA BENE: the ususkwinya *do* have weapons of their own; however,
		-- they are extremely restricted and never made available except to a
		-- very select number of that species. consequently, usuk players
		-- of a certain scenario may have usuk starting weapons, but these must
		-- be manually encoded to avoid injecting them into the overall crafting
		-- /loot system. because there are so few of these weapons in existence,
		-- all so tightly controlled, the odds of the weapons or plans winding
		-- up on Farthest Shadow are basically zero unless you bring them yourself
		name = 'Usukwinya', adj = 'Usuk';
		super = 'starsoul';
		techs = lib.tbl.set('tool', 'electric', 'electronic', 'suit', 'gravitic');
		power = 2.0;
		efficiency = 2.0;
		reliability = 2.0;
		cost = 0.5;
		fabclasses = {
			basis = fab {
				crystal = {aluminum = 5}; -- ruby
			};
		};
	};

	eluthrai = { --super-tier
		name = 'Eluthrai', adj = 'Eluthran';
		super = 'starsoul';
		techs = {tool = true, electric = true, electronic = true, weapon = true, gravitic = true, gravweapon = true, suit = true, combatSuit = true, hover = 'grav'};
		power = 4.0;
		efficiency = 4.0;
		reliability = 4.0;
		cost = 4.0;
		fabclasses = {
			basis = fab {
				crystal = {carbon = 5}; -- diamond
			};
			special	= fab {
				metal = {technetium=1, cinderstone=1}
			};
		};
	};

	-----------------------
	-- Forevanished Ones --
	-----------------------

	firstborn = { --god-tier
		name = 'Firstborn', adj = 'Firstborn';
		super = 'forevanished';
		techs = {tool = true, electric = true, electronic = true, suit = true, psi = true, combatSuit = true, weapon = true, gravitic = true, gravweapon = true};
		power = 10.0;
		efficiency = 5.0;
		reliability = 3.0;
		cost = 10.0;
		fabclasses = {
			basis = fab {
				metal = {technetium=2, neodymium=3, sunsteel=1};
				crystal = {astrite=1};
			};
		};
	};

	forevanisher = { --godslayer-tier
		name = 'Forevanisher', adj = 'Forevanisher';
		super = 'forevanished';
		techs = {tool = true, electric = true, electronic = true, suit = true, psi = true, combatSuit = true, weapon = true, gravitic = true, gravweapon = true};
		power = 20.0;
		efficiency = 1.0;
		reliability = 2.0;
		cost = 100.0;
		fabclasses = {
			basis = fab {
				metal = {};
				crystal = {};
			};
		};
	};

}

Added mods/starsoul/ui.lua version [e13ae59c08].



















































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local lib = starsoul.mod.lib

starsoul.ui = {}

starsoul.type.ui = lib.class {
	name = 'starsoul:ui';
	__index = {
		action = function(self, user, state, fields)
			local pg = self.pages[state.page or 'index']
			if not pg then return end
			if pg.handle then
				local redraw, reset = pg.handle(state, user, fields)
				if reset then pg.setupState(state,user) end
				if redraw then self:show(user) end
			end
			if fields.quit then self:cb('onClose', user) end
		end;
		cb = function(self, name, user, ...)
			local state = self:begin(user)
			if self[name] then self[name](state, user, ...) end
			local pcb = self.pages[state.page][name] 
			if pcb then pcb(state, user, ...) end
		end;
		begin = function(self, user, page, ...)
			local state = starsoul.activeUI[user.name]
			if state and state.form ~= self.id then
				state = nil
				starsoul.activeUI[user.name] = nil
			end
			local created = state == nil

			if not state then
				state = {
					page = page or 'index';
					form = self.id;
				}
				starsoul.activeUI[user.name] = state
				self:cb('setupState', user, ...)
			elseif page ~= nil and state.page ~= page then
				state.page = page
				local psetup = self.pages[state.page].setupState
				if psetup then psetup(state,user, ...) end
			end
			return state, created
		end;
		render = function(self, state, user)
			return self.pages[state.page].render(state, user)
		end;
		show = function(self, user)
			local state = self:begin(user)
			minetest.show_formspec(user.name, self.id,self:render(state, user))
		end;
		open = function(self, user, page, ...)
			user:suitSound 'starsoul-nav'
			self:begin(user, page, ...)
			self:show(user)
		end;
		close = function(self, user)
			local state = starsoul.activeUI[user.name]
			if state and state.form == self.id then
				self:cb('onClose', user)
				starsoul.activeUI[user.name] = nil
				minetest.close_formspec(user.name, self.id)
			end
		end;
	};
	construct = function(p)
		if not p.id then error('UI missing id') end
		p.pages = p.pages or {}
		return p
	end;
}

function starsoul.interface.install(ui)
	starsoul.interface.link(ui.id, ui)
end

function starsoul.ui.build(def, parent)
	local clr = def.color
	if clr and lib.color.id(clr) then
		clr = clr:to_hsl_o()
	end
	local state = {
		x = (def.x or 0);
		y = (def.y or 0);
		w = def.w or 0, h = def.h or 0;
		fixed = def.fixed or false;
		spacing = def.spacing or 0;
		padding = def.padding or 0;
		align = def.align or (parent and parent.align) or 'left';
		lines = {};
		mode = def.mode or (parent and parent.mode or nil); -- hw or sw
		gen = (parent and parent.gen or 0) + 1;
		fg = def.fg or (parent and parent.fg);
		color = clr or (parent and parent.color) or {
			hue = 260, sat = 0, lum = 0
		};
	}
	local lines = state.lines
	local cmod = string.format('^[hsl:%s:%s:%s',
		state.color.hue, state.color.sat*0xff, state.color.lum*0xff)

	local E = minetest.formspec_escape
	if state.padding/2 > state.x then state.x = state.padding/2 end
	if state.padding/2 > state.y then state.y = state.padding/2 end

	local function btnColorDef(sel)
		local function climg(state,img)
			local selstr
			if sel == nil then
				selstr = string.format(
					'button%s,' ..
					'button_exit%s,' ..
					'image_button%s,' ..
					'item_image_button%s',
					state, state, state, state)
			else
				selstr = E(sel) .. state
			end

			return string.format('%s[%s;' ..
					'bgimg=%s;'               ..
					'bgimg_middle=16;'        ..
					'content_offset=0,0'      ..
				']', sel and 'style' or 'style_type',
				selstr, E(img..'^[resize:48x48'..cmod))
		end

		return climg('',         'starsoul-ui-button-sw.png')       ..
		       climg(':hovered', 'starsoul-ui-button-sw-hover.png') ..
		       climg(':pressed', 'starsoul-ui-button-sw-press.png')
	end
	local function widget(...)
		table.insert(lines, string.format(...))
	end
	if def.kind == 'vert' then
		for _, w in ipairs(def) do
			local src, st = starsoul.ui.build(w, state)
			widget('container[%s,%s]%scontainer_end[]', state.x, state.y, src)
			state.y=state.y + state.spacing + st.h
			state.w = math.max(state.w, st.w)
		end
		state.w = state.w + state.padding
		state.h = state.y + state.padding/2
	elseif def.kind == 'hztl' then
		for _, w in ipairs(def) do
			local src, st = starsoul.ui.build(w, state)
			widget('container[%s,%s]%scontainer_end[]', state.x, state.y, src)
			-- TODO alignments
			state.x=state.x + state.spacing + st.w
			state.h = math.max(state.h, st.h)
		end
		state.h = state.h + state.padding
		state.w = state.x + state.padding/2
	elseif def.kind == 'list' then
		local slotTypes = {
			plain = {hue = 200, sat = -.1, lum = 0};
			element = {hue = 20, sat = -.3, lum = 0};
			chip = {hue = 0, sat = -1, lum = 0};
			psi = {hue = 300, sat = 0, lum = 0};
			power = {hue = 50, sat = 0, lum = .2};
		}
		local img
		if state.mode == 'hw' then
			img = lib.image('starsoul-ui-slot-physical.png');
		else
			img = lib.image('starsoul-ui-slot.png'):shift(slotTypes[def.listContent or 'plain']);
		end
		local spac = state.spacing
		widget('style_type[list;spacing=%s,%s]',spac,spac)
		assert(def.w and def.h, 'ui-lists require a fixed size')
		for lx = 0, def.w-1 do
		for ly = 0, def.h-1 do
			local ox, oy = state.x + lx*(1+spac), state.y + ly*(1+spac)
			table.insert(lines, string.format('image[%s,%s;1.1,1.1;%s]', ox-0.05,oy-0.05, img:render()))
		end end
		table.insert(lines, string.format('listcolors[#00000000;#ffffff10]')) -- FIXME
		table.insert(lines, string.format('list[%s;%s;%s,%s;%s,%s;%s]',
			E(def.target), E(def.inv),
			state.x, state.y,
			def.w,   def.h,
			def.idx))
		local sm = 1
		state.w = def.w * sm + (spac * (def.w - 1))
		state.h = def.h * sm + (spac * (def.h - 1))
	elseif def.kind == 'contact' then
		if def.color then table.insert(lines, btnColorDef(def.id)) end
		widget('image_button%s[%s,%s;%s,%s;%s;%s;%s]',
			def.close and '_exit' or '',
			state.x, state.y, def.w, def.h,
			E(def.img), E(def.id), E(def.label or ''))
	elseif def.kind == 'button' then
		if def.color then table.insert(lines, btnColorDef(def.id)) end
		local label = E(def.label or '')
		if state.fg then label = lib.color(state.fg):fmt(label) end
		widget('button%s[%s,%s;%s,%s;%s;%s]',
			def.close and '_exit' or '',
			state.x, state.y, def.w, def.h,
			E(def.id), label)
	elseif def.kind == 'img' then
		widget('%s[%s,%s;%s,%s;%s]',
			def.item and 'item_image' or 'image',
			state.x, state.y, def.w, def.h, E(def.item or def.img))
	elseif def.kind == 'label' then
		local txt = E(def.text)
		if state.fg then txt = lib.color(state.fg):fmt(txt) end
		widget('label[%s,%s;%s]',
			state.x, state.y + def.h*.5, txt)
	elseif def.kind == 'text' then
		-- TODO paragraph formatter
		widget('hypertext[%s,%s;%s,%s;%s;%s]',
			state.x, state.y, def.w, def.h, E(def.id), E(def.text))
	elseif def.kind == 'hbar' or def.kind == 'vbar' then -- TODO fancy image bars
		local cl = lib.color(state.color)
		local fg = state.fg or cl:readable(.8,1)
		local wfac, hfac = 1,1
		local clamp = math.min(math.max(def.fac, 0), 1)
		if def.kind == 'hbar'
			then wfac = wfac * clamp
			else hfac = hfac * clamp
		end
		local x,y, w,h = state.x, state.y, def.w, def.h
		widget('box[%s,%s;%s,%s;%s]',
			x,y, w,h, cl:brighten(0.2):hex())
		widget('box[%s,%s;%s,%s;%s]',
			x, y + (h*(1-hfac)), w * wfac, h * hfac, cl:hex())
		if def.text then
			widget('hypertext[%s,%s;%s,%s;;%s]',
				state.x, state.y, def.w, def.h,
				string.format('<global halign=center valign=middle color=%s>%s', fg:hex(), E(def.text)))
		end
	end

	if def.desc then
		widget('tooltip[%s,%s;%s,%s;%s]',
			state.x, state.y, def.w, def.h, E(def.desc))
	end

	local originX = (parent and parent.x or 0)
	local originY = (parent and parent.y or 0)
	local l = table.concat(lines)
	-- if state.fixed and (state.w < state.x or state.h < state.y) then
	-- 	l = string.format('scroll_container[%s,%s;%s,%s;scroll_%s;%s]%sscroll_container_end[]',
	-- 		(parent and parent.x or 0), (parent and parent.y or 0),
	-- 		state.w, state.h, state.gen,
	-- 		(state.x > state.w) and 'horizontal' or 'vertical', l)
	-- end
	

	if def.mode or def.container then
		if def.mode then
			l = string.format('background9[%s,%s;%s,%s;%s;false;64]',
					originX, originY, state.w, state.h,
					E(string.format('starsoul-ui-bg-%s.png%s^[resize:128x128',
						(def.mode == 'sw') and 'digital'
											or 'panel', cmod))) .. l
		end
		if parent == nil or state.color ~= parent.color then
			l = btnColorDef() .. l
		end
	end
	if not parent then
		return string.format('formspec_version[6]size[%s,%s]%s', state.w, state.h, l), state
	else
		return l, state
	end
end

starsoul.ui.tooltip = lib.ui.tooltipper {
	colors = {
		-- generic notes
		neutral = lib.color(.5,.5,.5);
		good    = lib.color(.2,1,.2);
		bad     = lib.color(1,.2,.2);
		info    = lib.color(.4,.4,1);
		-- chip notes
		schemaic = lib.color(.2,.7,1);
		ability  = lib.color(.7,.2,1);
		driver   = lib.color(1,.7,.2);
	};
}

Added mods/starsoul/user.lua version [5326333d8a].



















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
-- [ʞ] user.lua
--  ~ lexi hale <lexi@hale.su>
--  © EUPL v1.2
--  ? defines the starsoul.type.user class, which is
--    the main interface between the game world and the
--    client. it provides for initial signup and join,
--    managing the HUD, skinning the player model,
--    effecting weather changes, etc.

local lib = starsoul.mod.lib

local function hudAdjustBacklight(img)
	local night = math.abs(minetest.get_timeofday() - .5) * 2
	local opacity = night*0.8
	return img:fade(opacity)
end

local userStore = lib.marshal.metaStore {
	persona = {
		key  = 'starsoul:persona';
		type = starsoul.store.persona;
	};
}

local suitStore = starsoul.store.suitMeta

starsoul.type.user = lib.class {
	name = 'starsoul:user';
	construct = function(ident)
		local name, luser
		if type(ident) == 'string' then
			name = ident
			luser = minetest.get_player_by_name(name)
		else
			luser = ident
			name = luser:get_player_name()
		end
		return {
			entity = luser;
			name = name;
			hud = {
				elt = {};
			};
			tree = {};
			action = {
				bits = 0; -- for control deltas
				prog = {}; -- for recording action progress on a node; reset on refocus
				tgt = {type='nothing'};
				sfx = {};
				fx = {};
			};
			actMode = 'off';
			power = {
				nano = {primary = nil, secondary = nil};
				weapon = {primary = nil, secondary = nil};
				psi = {primary = nil, secondary = nil};
				maneuver = nil;
			};
			pref = {
				calendar = 'commune';
			};
		}
	end;
	__index = {
		pullPersona = function(self)
			-- if later records are added in public updates, extend this function to merge them
			-- into one object
			local s = userStore(self.entity)
			self.persona = s.read 'persona'
		end;
		pushPersona = function(self)
			local s = userStore(self.entity)
			s.write('persona', self.persona)
		end;
		uiColor = function(self) return lib.color {hue=238,sat=.5,lum=.5} end;
		statDelta = function(self, stat, d, cause, abs)
			local dt = self.persona.statDeltas
			local base
			if abs then
				local min, max
				min, max, base = self:statRange(stat)
				if     d == true  then d = max
				elseif d == false then d = min end
			end
			if stat == 'health' then
				self.entity:set_hp(abs and d or (self.entity:get_hp() + d), cause)
			elseif stat == 'breath' then
				self.entity:set_breath(abs and d or (self.entity:get_breath() + d))
			else
				if abs then
					dt[stat] = d - base
				else
					dt[stat] = dt[stat] + d
				end
				self:pushPersona()
			end
			self:updateHUD()
			-- TODO trigger relevant animations?
		end;
		lookupSpecies = function(self)
			return starsoul.world.species.lookup(self.persona.species, self.persona.speciesVariant)
		end;
		phenoTrait = function(self, trait)
			local s,v = self:lookupSpecies()
			return v.traits[trait] or s.traits[trait] or 0
		end;
		statRange = function(self, stat) --> min, max, base
			return starsoul.world.species.statRange(
				self.persona.species, self.persona.speciesVariant, stat)
		end;
		effectiveStat = function(self, stat)
			local val
			local min, max, base = self:statRange(stat)

			if stat == 'health' then
				val = self.entity:get_hp()
			elseif stat == 'breath' then
				val = self.entity:get_breath()
			else
				val = base + self.persona.statDeltas[stat] or 0
			end

			local d = max - min
			return val, (val - min) / d
		end;
		damageModifier = function(self, kind, amt)
			if kind == 'bluntForceTrauma' then
				local std = self:phenoTrait 'sturdiness'
				if std < 0 then
					amt = amt / 1+std
				else
					amt = amt * 1-std
				end
			end
			return amt
		end;
		attachImage = function(self, def)
			local user = self.entity
			local img = {}
			img.id = user:hud_add {
				type = 'image';
				text = def.tex;
				scale = def.scale;
				alignment = def.align;
				position = def.pos;
				offset = def.ofs;
				z_index = def.z;
			}
			if def.update then
				img.update = function()
					def.update(user, function(prop, val)
						user:hud_change(img.id, prop, val)
					end, def)
				end
			end
			return img
		end;
		attachMeter = function(self, def)
			local luser = self.entity
			local m = {}
			local w = def.size or 80
			local szf = w / 80
			local h = szf * 260
			m.meter = luser:hud_add {
				type = 'image';
				scale = {x = szf, y = szf};
				alignment = def.align;
				position = def.pos;
				offset = def.ofs;
				z_index = def.z or 0;
			}
			local cx = def.ofs.x + (w/2)*def.align.x
			local cy = def.ofs.y + (h/2)*def.align.y
			local oy = cy + h/2 - 42
			-- this is so fucking fragile holy fuck
			m.readout = luser:hud_add {
				type = 'text';
				scale = {x = w, y = h};
				size = szf;
				style = 4;
				position = def.pos;
				alignment = {x=0,0};
				offset = {x = cx, y = oy};
				z_index = (def.z or 0)+1;
				number = 0xffffff;
			}
			m.destroy = function()
				luser:hud_remove(m.meter)
				luser:hud_remove(m.readout)
			end
			m.update = function()
				local v,txt,color,txtcolor = def.measure(luser,def)
				v = math.max(0, math.min(1, v))
				local n = math.floor(v*16) + 1
				local img = hudAdjustBacklight(lib.image('starsoul-ui-meter.png'))
					:colorize(color or def.color)
				if def.flipX then
					img = img:transform 'FX'
				end
				img = img:render()
				img = img .. '^[verticalframe:17:' .. tostring(17 - n)
				luser:hud_change(m.meter, 'text', img)
				if txt then
					luser:hud_change(m.readout, 'text', txt)
				end
				if txtcolor then
					luser:hud_change(m.readout, 'number', txtcolor:hex())
				end
			end
			return m
		end;
		attachTextBox = function(self, def)
			local luser = self.entity
			local box = {}
			box.id = luser:hud_add {
				type = 'text';
				text = '';
				alignment = def.align;
				number = def.color and def.color:int24() or 0xFFffFF;
				scale = def.bound;
				size = {x = def.size, y=0};
				style = def.style;
				position = def.pos;
				offset = def.ofs;
			}
			box.update = function()
				local text, color = def.text(self, box, def)
				luser:hud_change(box.id, 'text', text)
				if color then
					luser:hud_change(box.id, 'number', color:int24())
				end
			end
			return box
		end;
		attachStatBar = function(self, def)
			local luser = self.entity
			local bar = {}
			local img = lib.image 'starsoul-ui-bar.png'
			local colorized = img
			if type(def.color) ~= 'function' then
				colorized = colorized:shift(def.color)
			end

			bar.id = luser:hud_add {
				type = 'statbar';
				position = def.pos;
				offset = def.ofs;
				name = def.name;
				text = colorized:render();
				text2 = img:tint{hue=0, sat=-1, lum = -0.5}:fade(0.5):render();
				number = def.size;
				item = def.size;
				direction = def.dir;
				alignment = def.align;
				size = {x=4,y=24};
			}
			bar.update = function()
				local sv, sf = def.stat(self, bar, def)
				luser:hud_change(bar.id, 'number', def.size * sf)
				if type(def.color) == 'function' then
					local clr = def.color(sv, luser, sv, sf)
					luser:hud_change(bar.id, 'text', img:tint(clr):render())
				end
			end
			return bar, {x=3 * def.size, y=16} -- x*2??? what
		end;
		createHUD = function(self)
			local function basicStat(statName)
				return function(user, bar)
					return self:effectiveStat(statName)
				end
			end
			local function batteryLookup(user)
				local max = user:suitPowerCapacity()
				if max == 0 then return 0, 0 end
				local ch = user:suitCharge()
				return (ch/max)*100, ch/max
			end
			local function C(h,s,l) return {hue=h,sat=s,lum=l} end
			local hbofs = (1+self.entity:hud_get_hotbar_itemcount()) * 25
			local bpad = 8
			self.hud.elt.health = self:attachStatBar {
				name = 'health', stat = basicStat 'health';
				color = C(340,0,.3), size = 100;
				pos = {x=0.5, y=1}, ofs = {x = -hbofs, y=-48 - bpad};
				dir = 1;
				align = {x=-1, y=-1};
			}
			self.hud.elt.stamina = self:attachStatBar {
				name = 'stamina', stat = basicStat 'stamina';
				color = C(60,0,.2), size = 100;
				pos = {x=0.5, y=1}, ofs = {x = -hbofs, y=-24 - bpad};
				dir = 1;
				align = {x=-1, y=-1};
			}
			self.hud.elt.bat = self:attachStatBar {
				name = 'battery', stat = batteryLookup;
				color = C(190,0,.2), size = 100;
				pos = {x=0.5, y=1}, ofs = {x = hbofs - 4, y=-48 - bpad};
				dir = 0;
				align = {x=1, y=-1};
			}
			self.hud.elt.psi = self:attachStatBar {
				name = 'psi', stat = basicStat 'psi';
				color = C(320,0,.2), size = 100;
				pos = {x=0.5, y=1}, ofs = {x = hbofs - 4, y=-24 - bpad};
				dir = 0;
				align = {x=1, y=-1};
			}
			self.hud.elt.time = self:attachTextBox {
				name = 'time';
				align = {x=0, y=1};
				pos = {x=0.5, y=1};
				ofs = {x=0,y=-95};
				text = function(user)
					local cal = starsoul.world.time.calendar[user.pref.calendar]
					return cal.time(minetest.get_timeofday())
				end;
			}
			self.hud.elt.temp = self:attachMeter {
				name = 'temp';
				align = {x=1, y=-1};
				pos = {x=0, y=1};
				ofs = {x=20, y=-20};
				measure = function(user)
					local warm = self:effectiveStat 'warmth'
					local n, color if warm < 0 then
						n = math.min(100, -warm)
						color = lib.color(0.1,0.3,1):lerp(lib.color(0.7, 1, 1), math.min(1, n/50))
					else
						n = math.min(100,  warm)
						color = lib.color(0.1,0.3,1):lerp(lib.color(1, 0, 0), math.min(1, n/50))
					end
					local txt = string.format("%s°", math.floor(warm))
					return (n/50), txt, color
				end;
			}
			self.hud.elt.geiger = self:attachMeter {
				name = 'geiger';
				align = {x=-1, y=-1};
				pos = {x=1, y=1};
				ofs = {x=-20, y=-20};
				flipX = true;
				measure = function(user)
					local hot = self:effectiveStat 'irradiation'
					local color = self:uiColor():lerp(lib.color(0.3, 1, 0), math.min(1, hot/5))
					local txt = string.format("%sGy", math.floor(hot))
					return (hot/5), txt, color
				end;
			}
			self.hud.elt.crosshair = self:attachImage {
				name = 'crosshair ';
				tex = '';
				pos = {x=.5, y=.5};
				scale = {x=1,y=1};
				ofs = {x=0, y=0};
				align = {x=0, y=0};
				update = function(user, set)
					local imgs = {
						off = '';
						nano = 'starsoul-ui-crosshair-nano.png';
						psi = 'starsoul-ui-crosshair-psi.png';
						weapon = 'starsoul-ui-crosshair-weapon.png';
					}
					set('text', imgs[self.actMode] or imgs.off)
				end;
			};
			local hudCenterBG = lib.image 'starsoul-ui-hud-bg.png':colorize(self:uiColor())
			self.hud.elt.bg = self:attachImage {
				name = 'hudBg';
				tex = hudCenterBG:render();
				pos = {x=.5, y=1};
				scale = {x=1,y=1};
				ofs = {x=0, y=0};
				align = {x=0, y=-1};
				z = -1;
				update = function(user, set)
					set('text', hudAdjustBacklight(hudCenterBG):render())
				end;
			};
		end;
		onModeChange = function(self, oldMode, silent)
			self.hud.elt.crosshair.update()
			if not silent then
				local sfxt = {
					off = 'starsoul-mode-off';
					nano = 'starsoul-mode-nano';
					psi = 'starsoul-mode-psi';
					weapon = 'starsoul-mode-weapon';
				}
				local sfx = self.actMode and sfxt[self.actMode] or sfxt.off
				self:suitSound(sfx)
			end
		end;
		actModeSet = function(self, mode, silent)
			if not mode then mode = 'off' end
			local oldMode = self.actMode
			self.actMode = mode
			self:onModeChange(oldMode, silent)
			if mode ~= oldMode then
				starsoul.ui.setupForUser(self)
			end
		end;
		deleteHUD = function(self)
			for name, e in pairs(self.hud.elt) do
				self:hud_delete(e.id)
			end
		end;
		updateHUD = function(self)
			for name, e in pairs(self.hud.elt) do
				if e.update then e.update() end
			end
		end;
		clientInfo = function(self)
			return minetest.get_player_information(self.name)
		end;
		onSignup = function(self)
			local meta = self.entity:get_meta()
			local inv = self.entity:get_inventory()
			-- the sizes indicated here are MAXIMA. limitations on e.g. the number of elements that may be carried are defined by your suit and enforced through callbacks and UI generation code, not inventory size
			inv:set_size('main', 6) -- carried items and tools. main hotbar.

			inv:set_size('starsoul_suit', 1) -- your environment suit (change at wardrobe)
			inv:set_size('starsoul_cfg', 1) -- the item you're reconfiguring / container you're accessing

			local scenario
			for _, e in pairs(starsoul.world.scenario) do
				if e.id == starsoul.world.defaultScenario then
					scenario = e break
				end
			end assert(scenario)
			self.persona = starsoul.world.species.birth(scenario.species, scenario.speciesVariant, self.entity)
			self.persona.name = self.entity:get_player_name() -- a reasonable default
			self.persona.background = starsoul.world.defaultScenario
			self:pushPersona()

			local gifts = scenario.startingItems
			local inv = self.entity:get_inventory()
			inv:set_stack('starsoul_suit', 1, starsoul.item.mk(gifts.suit, self, {gift=true}))
			self:getSuit():establishInventories(self.entity)

			local function giveGifts(name, list)
				if inv:get_size(name) > 0 then
					for i, e in ipairs(list) do
						inv:add_item(name, starsoul.item.mk(e, self, {gift=true}))
					end
				end
			end

			giveGifts('starsoul_suit_bat', gifts.suitBatteries)
			giveGifts('starsoul_suit_chips', gifts.suitChips)
			giveGifts('starsoul_suit_guns', gifts.suitGuns)
			giveGifts('starsoul_suit_ammo', gifts.suitAmmo)
			giveGifts('starsoul_suit_canisters', gifts.suitCans)

			giveGifts('main', gifts.carry)

			self:reconfigureSuit()

			-- i feel like there has to be a better way
			local cx = math.random(-500,500)
			local startPoint
			repeat local temp = -100
				local cz = math.random(-500,500)
				local cy = minetest.get_spawn_level(cx, cz)
				if cy then
					startPoint = vector.new(cx,cy,cz)
					temp = starsoul.world.climate.eval(startPoint,.5,.5).surfaceTemp
				end
				if cx > 10000 then break end -- avoid infiniloop in pathological conditions
			until temp > -2
			self.entity:set_pos(startPoint)
			meta:set_string('starsoul_spawn', startPoint:to_string())
		end;
		onDie = function(self, reason)
			local inv = self.entity:get_inventory()
			local where = self.entity:get_pos()
			local function dropInv(lst)
				local l = inv:get_list(lst)
				for i, o in ipairs(l) do
					if o and not o:is_empty() then
						minetest.item_drop(o, self.entity, where)
					end
				end
				inv:set_list(lst, {})
			end
			dropInv 'main'
			dropInv 'starsoul_suit'
			self:statDelta('psi',     0, 'death', true)
			self:statDelta('hunger',  0, 'death', true)
			self:statDelta('thirst',  0, 'death', true)
			self:statDelta('fatigue', 0, 'death', true)
			self:statDelta('stamina', 0, 'death', true)
			self:updateSuit()
		end;
		onRespawn = function(self)
			local meta = self.entity:get_meta()
			self.entity:set_pos(vector.from_string(meta:get_string'starsoul_spawn'))
			self:updateSuit()
			return true
		end;
		onJoin = function(self)
			local me = self.entity
			local meta = me:get_meta()
			self:pullPersona()

			-- formspec_version and real_coordinates are apparently just
			-- completely ignored here
			me:set_formspec_prepend [[
				bgcolor[#00000000;true]
				style_type[button,button_exit,image_button,item_image_button;border=false]
				style_type[button;bgimg=starsoul-ui-button-hw.png;bgimg_middle=8;content_offset=0,-2]
				style_type[button:hovered;bgimg=starsoul-ui-button-hw-hover.png;bgimg_middle=8]
				style_type[button:pressed;bgimg=starsoul-ui-button-hw-press.png;bgimg_middle=8;content_offset=0,1]
			]]
			local hotbarSlots = me:get_inventory():get_size 'main';
-- 			local slotTex = 'starsoul-ui-slot.png'
-- 			local hbimg = string.format('[combine:%sx128', 128 * hotbarSlots)
-- 			for i = 0, hotbarSlots-1 do
-- 				hbimg = hbimg .. string.format(':%s,0=%s', 128 * i, slotTex)
-- 			end
			--me:hud_set_hotbar_image(lib.image(hbimg):colorize(self:uiColor()):fade(.36):render())
-- 			me:hud_set_hotbar_selected_image(lib.image(slotTex):colorize(self:uiColor()):render())
			me:hud_set_hotbar_image('[fill:1x24:0,0:' .. self:uiColor():fade(.1):hex())
			me:hud_set_hotbar_selected_image(
				'[fill:1x24,0,0:' .. self:uiColor():fade(.4):hex() .. '^[fill:1x1:0,23:#ffFFffff'
			)
			me:hud_set_hotbar_itemcount(hotbarSlots)
			me:hud_set_flags {
				hotbar = true;
				healthbar = false;
				breathbar = false;
				basic_debug = false;
				crosshair = false;
			}
			-- disable builtin crafting
			local inv = me:get_inventory()
				inv:set_size('craftpreview', 0)
				inv:set_size('craftresult', 0)
				inv:set_size('craft', 0)

			me:set_stars {
				day_opacity = 0.7;
			}
			me:set_sky {
				sky_color = {
					  day_sky = '#a7c2cd',   day_horizon = '#ddeeff';
					 dawn_sky = '#003964',  dawn_horizon = '#87ebff';
					night_sky = '#000000', night_horizon = '#000E29';
					fog_sun_tint = '#72e4ff';
					fog_moon_tint = '#2983d0';
					fog_tint_type = 'custom';
				};
				fog = { -- not respected??
					-- TODO make this seasonal & vary with weather
					fog_distance = 40;
					fog_start = 0.3;
				};
			}
			me:set_sun {
				texture = 'starsoul-sun.png';
				sunrise = 'sunrisebg.png^[hsl:180:1:.7';
				tonemap = 'sun_tonemap.png^[hsl:180:1:.7';
				scale = 0.8;
			}
			me:set_lighting {
				shadows = {
					intensity = .5;
				};
				exposure = {
					luminance_max = 3.0;
					speed_dark_bright = 0.5;
					speed_bright_dark = 1.0;
				};
				volumetric_light = {
					strength = 0.3;
				};
			}
			me:set_eye_offset(nil, vector.new(3,-.2,10))
			-- TODO set_clouds speed in accordance with wind
			starsoul.world.species.setupEntity(me, self.persona)
			starsoul.ui.setupForUser(self)
			self:createHUD()
			self:updateSuit()
		end;
		suitStack = function(self)
			return self.entity:get_inventory():get_stack('starsoul_suit', 1)
		end;
		suitSound = function(self, sfx)
			-- trigger a sound effect from the player's suit computer
			minetest.sound_play(sfx, {object=self.entity, max_hear_distance=4}, true)
		end;
		suitPowerStateSet = function(self, state, silent)
			-- necessary to enable reacting to power state changes
			-- e.g. to play sound effects, display warnings
			local os
			self:forSuit(function(s)
				os=s:powerState()
				s:powerStateSet(state)
			end)
			if state == 'off' then
				if self.actMode == 'nano' or self.actMode == 'weapon' then
					self:actModeSet('off', silent)
				end
			end
			if not silent and os ~= state then
				local sfx
				if state == 'off' then
					sfx = 'starsoul-power-down'
				elseif os == 'off' then
					sfx = 'starsoul-power-up'
				elseif state == 'powerSave' or os == 'powerSave' then
					sfx = 'starsoul-configure'
				end
				if sfx then self:suitSound(sfx) end
			end
		end;
		species = function(self)
			return starsoul.world.species.index[self.persona.species]
		end;
		updateBody = function(self)
			local adornment = {}
			local suitStack = self:suitStack()
			if suitStack and not suitStack:is_empty() then
				local suit = suitStack:get_definition()._starsoul.suit
				suit.adorn(adornment, suitStack, self.persona)
			end
			starsoul.world.species.updateTextures(self.entity, self.persona, adornment)
		end;
		updateSuit = function(self)
			self:updateBody()
			local inv = self.entity:get_inventory()
			local sst = suitStore(self:suitStack())
			if self:naked() then
				starsoul.type.suit.purgeInventories(self.entity)
				if self.actMode == 'nano' or self.actMode == 'weapon' then
					self:actModeSet 'off'
				end
			else
				local suit = self:getSuit()
				suit:establishInventories(self.entity)

				if self:suitCharge() <= 0 then
					self:suitPowerStateSet 'off'
				end
			end
			self:updateHUD()
		end;
		reconfigureSuit = function(self)
			-- and here's where things get ugly
			-- you can't have an inventory inside another item. to hack around this,
			-- we use the player as the location of the suit inventories, and whenever
			-- there's a change in the content of these inventories, this function is
			-- called to serialize those inventories out to the suit stack
			if self:naked() then return end
			local suit = self:getSuit()
			suit:onReconfigure(self.entity:get_inventory())
			self:setSuit(suit)

			-- reconfiguring the suit can affect player abilities: e.g. removing
			-- / inserting a chip with a minimap program
		end;
		getSuit = function(self)
			local st = self:suitStack()
			if st:is_empty() then return nil end
			return starsoul.type.suit(st)
		end;
		setSuit = function(self, suit)
			self.entity:get_inventory():set_stack('starsoul_suit', 1, suit.item)
		end;
		changeSuit = function(self, ...)
			self:setSuit(...)
			self:updateSuit()
		end;
		forSuit = function(self, fn)
			local s = self:getSuit()
			if fn(s) ~= false then
				self:setSuit(s)
			end
		end;
		suitPowerCapacity = function(self) -- TODO optimize
			if self:naked() then return 0 end
			return self:getSuit():powerCapacity()
		end;
		suitCharge = function(self) -- TODO optimize
			if self:naked() then return 0 end
			return self:getSuit():powerLeft()
		end;
		suitDrawCurrent = function(self, power, time, whatFor, min)
			if self:naked() then return 0,0 end
			local inv = self.entity:get_inventory()
			local bl = inv:get_list('starsoul_suit_bat')
			local supply = 0
			local wasteHeat = 0 --TODO handle internally
			for slot, ps in ipairs(bl) do
				if not ps:is_empty() then
					local p, h = starsoul.mod.electronics.dynamo.drawCurrent(ps, power - supply, time)
					supply = supply + p
					wasteHeat = wasteHeat + h
					if power-supply <= 0 then break end
				end
			end
			if min and supply < min then return 0,0 end
			inv:set_list('starsoul_suit_bat', bl)
			self:reconfigureSuit()
			if whatFor then
				-- TODO display power use icon
			end
			return supply, wasteHeat
		end;
		naked = function(self)
			return self:suitStack():is_empty()
		end;
		onPart = function(self)
			starsoul.liveUI     [self.name] = nil
			starsoul.activeUI   [self.name] = nil
			starsoul.activeUsers[self.name] = nil
		end;
		openUI = function(self, id, page, ...)
			local ui = assert(starsoul.interface.db[id])
			ui:open(self, page, ...)
		end;
		onRespond = function(self, ui, state, resp)
			ui:action(self, state, resp)
		end;

		updateWeather = function(self)
		end;

		canInteract = function(self, with)
			return true; -- TODO
		end;

		trigger = function(self, which, how)
			--print('trigger', which, dump(how))
			local p
			local wld = self.entity:get_wielded_item()
			if which == 'maneuver' then
				p = self.power.maneuver
			elseif which == 'retarget' then
				self.action.prog = {}
			elseif wld and not wld:is_empty() then
				local wdef = wld:get_definition()
				if wdef._starsoul and wdef._starsoul.tool then
					p = {tool = wdef._starsoul.tool}
				end
			elseif self.actMode ~= 'off' then
				p = self.power[self.actMode][which]
			end
			if p == nil then return false end
			local ctx, run = {
				how = how;
			}
			if p.chipID then
				local inv = self.entity:get_inventory()
				local chips = inv:get_list 'starsoul_suit_chips'
				for chSlot, ch in pairs(chips) do
					if ch and not ch:is_empty() then
						local d = starsoul.mod.electronics.chip.read(ch)
						if d.uuid == p.chipID then
							local pgm = assert(d.files[p.pgmIndex], 'file missing for ability')
							ctx.file = starsoul.mod.electronics.chip.fileHandle(ch, p.pgmIndex)
							ctx.saveChip = function()
								inv:set_slot('starsoul_suit_chips', chSlot, ch)
							end
							local sw = starsoul.item.sw.db[pgm.body.pgmId]
							run = assert(sw.run, 'missing run() for active software ability ' .. pgm.body.pgmId)
							break
						end
					end
				end
			else
				error('bad ability pointer ' .. dump(p))
			end
			if run then
				run(self, ctx)
				return true
			end
			return false
		end;
		give = function(self, item)
			local inv = self.entity:get_inventory()
			local function is(grp)
				return minetest.get_item_group(item:get_name(), grp) ~= 0
			end
			-- TODO notif popups
			if is 'specialInventory' then
				if is 'powder' then
					if self:naked() then return item end
					local cans = inv:get_list 'starsoul_suit_canisters'
					if cans and next(cans) then for i, st in ipairs(cans) do
						local lst = string.format('starsoul_canister_%u_elem', i)
						item = inv:add_item(lst, item)
						if item:is_empty() then break end
					end end
					self:forSuit(function(x) x:pushCanisters(inv) end)
				end
				return item
			else
				return inv:add_item('main', item)
			end
		end;
		thrustUpon = function(self, item)
			local r = self:give(st)
			if not r:is_empty() then
				return minetest.add_item(self.entity:get_pos(), r)
			end
		end;
	};
}

local biointerval = 3.0
starsoul.startJob('starsoul:bio', biointerval, function(delta)
	for id, u in pairs(starsoul.activeUsers) do

	end
end)

local cbit = {
	up   = 0x001;
	down = 0x002;
	left = 0x004;
	right= 0x008;
	jump = 0x010;
	manv = 0x020;
	snk  = 0x040;
	dig  = 0x080;
	put  = 0x100;
	zoom = 0x200;
}
-- this is the painful part
minetest.register_globalstep(function(delta)
	local doNothing,mustInit,mustHalt = 0,1,2
	for id, user in pairs(starsoul.activeUsers) do
		local ent = user.entity
		local bits = ent:get_player_control_bits()

		local function what(b)
			if bit.band(bits, b) ~= 0 and bit.band(user.action.bits, b) == 0 then
				return mustInit
			elseif bit.band(bits, b) == 0 and bit.band(user.action.bits, b) ~= 0 then
				return mustHalt
			else return doNothing end
		end
		local skipBits = 0
		if user.action.bits ~= bits then
			local mPrimary = what(cbit.dig)
			local mSecondary = what(cbit.put)
			if mPrimary == mustInit then -- ENGINE-BUG
				user.action.tgt = {type='nothing'}
				user.action.prog = {}
			elseif mPrimary == mustHalt then
				user:trigger('primary', {state='halt'})
			end
			if mSecondary == mustHalt then
				user:trigger('secondary', {state='halt'})
			end
		end
		--bits = bit.band(bits, bit.bnot(skipBits))
		if bit.band(bits, cbit.dig)~=0 then
			user:trigger('primary', {state='prog', delta=delta})
		end
		if bit.band(bits, cbit.put)~=0 then
			user:trigger('secondary', {state='prog', delta=delta})
		end
		user.action.bits = bits
		-- ENGINE-BUG: dig and put are not handled equally in the
		-- engine. it is possible for the put bit to get stuck on
		-- if the key is hammered while the player is not moving.
		-- the bit will release as soon as the player looks or turns
		-- nonetheless this is obnoxious
	end
end)

Added mods/starsoul/world.lua version [20212b373d].











































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local lib = starsoul.mod.lib
local world = starsoul.world

function world.date()
	local days = minetest.get_day_count()
	local year = math.floor(days / world.planet.orbit);
	local day = days % world.planet.orbit;
	return {
		year = year, day = day;
		season = day / world.planet.orbit;
	}
end
local lerp = lib.math.lerp

local function gradient(grad, pos)
	local n = #grad
	if n == 1 then return grad[1] end
	local op = pos*(n-1)
	local idx = math.floor(op)
	local t = op-idx
	return lerp(t, grad[1 + idx], grad[2 + idx])
end

local altitudeCooling = 10 / 100

-- this function provides the basis for temperature calculation,
-- which is performed by adding this value to the ambient temperature,
-- determined by querying nearby group:heatSource items in accordance
-- with the inverse-square law
function world.climate.eval(pos, tod, season)
	local data = minetest.get_biome_data(pos)
	local biome = world.ecology.biomes.db[minetest.get_biome_name(data.biome)]
	local heat, humid = data.heat, data.humidity
	tod = tod or minetest.get_timeofday()
	heat = lerp(math.abs(tod - 0.5)*2, heat, heat + biome.nightTempDelta)

	local td = world.date()
	heat = heat + gradient(biome.seasonalTemp, season or td.season)
	if pos.y > 0 then
		heat = heat - pos.y*altitudeCooling 
	end

	return {
		surfaceTemp = heat;
		waterTemp = heat + biome.waterTempDelta;
		surfaceHumid = humid;
	}
end

local vdsq = lib.math.vdsq
function world.climate.temp(pos) --> irradiance at pos in W
	local cl = world.climate.eval(pos)
	local radCenters = starsoul.region.radiator.store:get_areas_for_pos(pos, false, true)
	local irradiance = 0
	for _,e in pairs(radCenters) do
		local rpos = minetest.string_to_pos(e.data)
		local rdef = assert(minetest.registered_nodes[assert(minetest.get_node(rpos)).name])
		local rc = rdef._starsoul.radiator
		local r_max = rc.radius(rpos)

		local dist_sq = vdsq(rpos,pos)
		if dist_sq <= r_max^2 then
			-- cheap bad way
			-- if minetest.line_of_sight(rpos,pos) then
			--
			-- expensive way
			local obstruct = 0
			local ray = Raycast(rpos, pos, true, true)
			for p in ray do
				if p.type == 'node' then obstruct = obstruct + 1 end
			end

			if obstruct < 4 then
				local power, customFalloff = rc.radiate(rpos, pos)
				-- okay this isn't the real inverse square law but i 
				-- couldn't figure out a better way to simplify the
				-- model without checking an ENORMOUS number of nodes
				-- maybe someone else who isn't completely
				-- mathtarded can do better.
				if not customFalloff then
					power = power * (1 - (dist_sq / ((r_max+1)^2)))
				end
				power = power * (1 - (obstruct/5))
				irradiance = irradiance + power
			end
		end
	end
	return irradiance + cl.surfaceTemp
end

world.ecology.biomes.foreach('starsoul:biome-gen', {}, function(id, b)
	b.def.name = id
	minetest.register_biome(b.def)
end)

world.ecology.biomes.link('starsoul:steppe', {
	nightTempDelta = -30;
	waterTempDelta = 0;
	--               W    Sp   Su    Au   W
	seasonalTemp = {-50, -10, 5, 5, -20, -50};
	def = {
		node_top      = 'starsoul:greengraze', depth_top = 1;
		node_filler   = 'starsoul:soil',    depth_filler = 4;
		node_riverbed = 'starsoul:sand',  depth_riverbed = 4;
		y_min = 0;
		y_max = 512;
		heat_point = 10;
		humidity_point = 30;
	};
})
	
world.ecology.biomes.link('starsoul:ocean', {
	nightTempDelta = -35;
	waterTempDelta = 5;
	seasonalTemp = {0}; -- no seasonal variance
	def = {
		y_max = 3;
		y_min = -512;
		heat_point = 15;
		humidity_point = 50;
		node_top    = 'starsoul:sand', depth_top    = 1;
		node_filler = 'starsoul:sand', depth_filler = 3;
	};
})

local toward = lib.math.toward
local hfinterval = 1.5
starsoul.startJob('starsoul:heatflow', hfinterval, function(delta)

	-- our base thermal conductivity (κ) is measured in °C/°C/s. say the
	-- player is in -30°C weather, and has an internal temperature of
	-- 10°C. then:
	--   κ  = .1°C/C/s (which is apparently 100mHz)
	--   Tₚ =  10°C
	--   Tₑ = -30°C
	--   d  = Tₑ − Tₚ = -40°C
	--   ΔT = κ×d = -.4°C/s
	-- our final change in temperature is computed as tΔC where t is time
	local kappa = .05
	for name,user in pairs(starsoul.activeUsers) do
		local tr = user:species().tempRange
		local t = starsoul.world.climate.temp(user.entity:get_pos())
		local insul = 0
		local naked = user:naked()
		local suitDef
		if not naked then
			suitDef = user:suitStack():get_definition()
			insul = suitDef._starsoul.suit.temp.insulation
		end

		local warm = user:effectiveStat 'warmth'
		local tSafeMin, tSafeMax = tr.survivable[1], tr.survivable[2]
		local tComfMin, tComfMax = tr.comfort[1], tr.comfort[2]

		local tDelta = (kappa * (1-insul)) * (t - warm) * hfinterval
		local tgt = warm + tDelta

		-- old logic: we move the user towards the exterior temperature, modulated
		-- by her suit insulation.
		--local tgt = toward(warm, t, hfinterval * thermalConductivity * (1 - insul))

		if not naked then
			local suit = user:getSuit()
			local suitPower = suit:powerState()
			local suitPowerLeft = suit:powerLeft()
			if suitPower ~= 'off' then
				local coilPower = 1.0
				local st = suitDef._starsoul.suit.temp
				if suitPower == 'powerSave' and (tgt >= tSafeMin and tgt <= tSafeMax) then coilPower = 0.5 end
				if tgt < tComfMin and st.maxHeat > 0 then
					local availPower = user:suitDrawCurrent(st.heatPower*coilPower, hfinterval)
					tgt = tgt + (availPower / st.heatPower) * st.maxHeat * coilPower * hfinterval
				end
				if tgt > tComfMax and st.maxCool > 0 then
					local availPower = user:suitDrawCurrent(st.coolPower*coilPower, hfinterval)
					tgt = tgt - (availPower / st.coolPower) * st.maxCool * coilPower * hfinterval
				end
			end
		end

		user:statDelta('warmth', tgt - warm) -- dopey but w/e

		warm = tgt -- for the sake of readable code

		if warm < tSafeMin or warm > tSafeMax then
			local dv
			if warm < tSafeMin then
				dv = math.abs(warm - tSafeMin)
			else
				dv = math.abs(warm - tSafeMax)
			end
			-- for every degree of difference you suffer 2 points of damage/s
			local dmg = math.ceil(dv * 2)
			user:statDelta('health', -dmg)
		end
	end
end)

Added mods/vtlib/class.lua version [97eb733bda].































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
return function(meta)
	local class = {
		id = function(instance)
			return getmetatable(instance) == meta
		end;
		
		merge = function(dest, src)
			for k,v in pairs(src) do
				dest[k] = v
			end
			if meta.clone then meta.clone(dest) end
		end;
	}
	class.clone = function(instance)
		local new = {}
		class.merge(new, instance)
		setmetatable(new, meta)
		return new
	end
	class.change = function(orig, delta)
		local new = class.clone(orig)
		class.merge(new, delta)
		return new
	end
	class.mk = function(...)
		local new
		if #{...} == 1 then
			if class.id((...)) then
				-- default copy constructor
				return class.clone((...))
			elseif meta.cast then
				if type((...)) == 'table' then
					for from, conv in pairs(meta.cast) do
						if from.id and from.id((...)) then
							new = conv((...))
							goto setup
						end
					end
				else
					local conv = meta.cast[type((...))]
					if conv then
						new = conv((...))
						goto setup
					else assert(false) end
				end
			end
		end
		if meta.construct then
			new = meta.construct(...)
		else -- if there is no constructor, this 'class' is simply a bundle
		     -- of behaviors for an arbitrary struct
			new = (...)
		end

		::setup::
			setmetatable(new, meta)
			return new
	end
	setmetatable(class, { __call = function(self, ...)
		return self.mk(...)
	end })
	return class
end

Added mods/vtlib/color.lua version [d9e5a5527c].









































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local lib = ...

local color
local function warp(f)
	return function(self, ...)
		local n = color(self)
		f(n, ...)
		return n
	end;
end

local function clip(v)
	return math.max(0,math.min(255,v))
end
local colorStruct do local T,G = lib.marshal.t, lib.marshal.g
	colorStruct = G.struct {
		red = T.u8;
		green = T.u8;
		blue = T.u8;
		alpha = T.u8;
	}
end

local function from_hsl(hsl, alpha)
	-- Based on the algorithm in Computer Graphics: Principles and Practice, by
	-- James D. Foley et. al., 2nd ed., p. 596
	-- Degree version, though radian is more natural, I don't want to translate it yet
	local h = hsl.hue
	local s = hsl.sat
	local l = hsl.lum
	local value = function(n1, n2, hue)
		if hue > 360 then
			hue = hue - 360
		elseif hue < 0 then
			hue = hue + 360
		end
		if hue < 60 then
			return n1 + (n2 - n1) * hue/60
		elseif hue < 180 then
			return n2
		elseif hue < 240 then
			return n1 + (n2 - n1) * (240 - hue)/60
		else
			return n1
		end
	end
	local m2
	if l < 0.5 then
		m2 = l * (1 + s)
	else
		m2 = l + s - l * s
	end
	local m1 = 2 * l - m2
	if s == 0 then
		-- Achromatic, there is no hue
		-- In book this errors if hue is not undefined, but we set hue to 0 in this case, not nil or something, so
		return color(l, l, l, alpha)
	else
		-- Chromatic case, so there is a hue
		return color(
			clip(value(m1, m2, h + 120)),
			clip(value(m1, m2, h)),
			clip(value(m1, m2, h - 120)),
			alpha
		)
	end
end

color = lib.class {
	__tostring = function(self)
		local hex = function(val)
			return string.format('%02X',math.max(0,math.min(0xff,math.floor(0xff*val))))
		end
		local str = '#' ..
			hex(self.red) ..
			hex(self.green) ..
			hex(self.blue)
		if self.alpha and self.alpha < 1.0 then str = str .. hex(self.alpha) end
		return str
	end;

	__add = function(self, other)
		local sfac = (self.alpha or 1.0) / 1.0
		local ofac = (other.alpha or 1.0) / 1.0
		if self.alpha == other.alpha then
			sfac = 1 ofac = 1
		end

		local sr, sg, sb = other.red * ofac, other.blue * ofac, other.green * ofac
		local nr, ng, nb =  self.red * sfac,  self.blue * sfac,  self.green * sfac
		local saturate = function(a,b)
			return math.max(0, math.min(1.0, a+b))
		end
		local alpha = nil
		if self.alpha and other.alpha then
			alpha = saturate(self.alpha or 1.0, other.alpha or 1.0)
		end
		return color(
			saturate(sr, nr),
			saturate(sg, ng),
			saturate(sb, nb),
			alpha
		)
	end;
	
	cast = {
		number = function(n) return {
			red = n; green = n; blue = n;
		} end;
		table = function(t) return {
			red = t[1]; green = t[2]; blue = t[3]; alpha = t[4];
		} end;
	};

	__index = {
		hex = function(self) return
			getmetatable(self).__tostring(self)
		end;

		int24 = function(self)
			return bit.bor(
				bit.lshift(math.floor(0xff*self.red),   16),
				bit.lshift(math.floor(0xff*self.green),  8),
				           math.floor(0xff*self.blue))
		end;

		int = function(self)
			return bit.bor(bit.lshift(self:int24(), 8), math.floor(0xff*(self.alpha or 1.0)))
		end;

		fmt = function(self, text) return
			minetest.colorize(self:hex(), text)
		end;

		bg = function(self, text) return
			text .. minetest.get_background_escape_sequence(self:hex())
		end;

		lum = function(self) return
			(self.red + self.green + self.blue) / 3
		end;

		pair = function(self) --> bg, fg
			if self:to_hsl().lum > 0.5 then -- dark on light
				return self:brighten(1.2), self:brighten(0.1)
			else -- light on dark
				return self:brighten(0.6), self:readable(.9, 1.0)
			end
		end;

		marshal = function(self)
			local raw = colorStruct.enc {
				red   = math.floor(self.red   * 0xff);
				green = math.floor(self.green * 0xff);
				blue  = math.floor(self.blue  * 0xff);
				alpha = math.floor(self.alpha * 0xff);
			}
			return lib.str.meta_armor(raw)
		end;
		to_hsl_o = function(self)
			local s = self:to_hsl()
			return {
				hue = s.hue;
				sat = s.sat*2-1;
				lum = s.lum*2-1;
			}
		end;

		to_hsl = function(self)
			-- THIS DOESN'T WORK. color(hsl):to_hsl() ~= hsl.
			-- has ugly implications for light control

			-- Based on the algorithm in Computer Graphics: Principles and Practice, by
			-- James D. Foley et. al., 2nd ed., p. 595
			-- We need the rgb between 0 and 1
			local r = self.red
			local g = self.green
			local b = self.blue
			local max = math.max(r, g, b)
			local min = math.min(r, g, b)
			local luminosity = (max + min)/2
			local hue = 0
			local saturation = 0
			if max == min then
				-- Achromatic case, because r=g=b
				saturation = 0
				hue = 0 -- Undefined, so just replace w/ 0 for usability
			else
				-- Chromatic case
				if luminosity <= 0.5 then
					saturation = (max - min)/(max + min)
				else
					saturation = (max - min)/(2 - max - min)
				end
				-- Next calculate the hue
				local delta = max - min
				if r == max then
					hue = (g - b)/delta
				elseif g == max then
					hue = 2 + (b - r)/delta
				else -- blue must be max, so no point in checking
					hue = 4 + (r - g)/delta
				end
				hue = hue * 60 -- degrees
				--hue = hue * (math.pi / 3) -- for hue in radians instead of degrees
				if hue < 0 then
					hue = hue + 2 * math.pi
				end
			end
			-- print("r"..self.red.."g"..self.green.."b"..self.blue.." is h"..hue.."s"..saturation.."l"..luminosity)
			--local temp = from_hsl({hue=hue,sat=saturation,lum=luminosity},self.alpha)
			-- print("back is r"..temp.red.."g"..temp.green.."b"..temp.blue)
			if hue < 0 then
				hue = 360 + hue
			end
			return { hue = hue, sat = saturation, lum = luminosity, alpha = self.alpha }
		end;

		readable = function(self, target, minalpha)
			target = target or 0.6
			local hsl = self:to_hsl()
			hsl.lum = target
			local worstHue = 230
			local nearness = math.abs(worstHue - hsl.hue)
			if nearness <= 70 then
				local boost = 1.0 - (nearness / 70)
				hsl.lum = math.min(1, hsl.lum * (1 + (boost*0.4)))
			end

			return from_hsl(hsl, math.max(self.alpha or 1.0, minalpha))
		end;

		fade = warp(function(new, fac)
			new.alpha = math.min(1.0, (new.alpha or 1.0) * fac)
		end);

		warp = warp(function(new, fn) fn(new) end);

		lerp = warp(function(self, new, fac) -- uses rgb color space
			local function lerp(t, a, b) return (1-t)*a + t*b end
			self.red = lerp(fac, self.red,   new.red)
			self.green = lerp(fac, self.green, new.green)
			self.blue = lerp(fac, self.blue,  new.blue)
			if self.alpha ~= nil or new.alpha ~= nil then
				if new.alpha == nil and fac >= 1.0 then
					self.alpha = nil
				elseif self.alpha == nil and fac <= 0.0 then
					self.alpha = nil
				else
					self.alpha = lerp(fac, self.alpha or 1.0,  new.alpha or 1.0)
				end
			end
		end);

		brighten = function(self, fac)
			-- Use HSL to brighten
			-- To HSL
			local hsl = self:to_hsl()
			-- Do the calculation, clamp to 0-1 instead of the clamp fn
			hsl.lum = math.min(math.max(hsl.lum * fac, 0), 1)
			-- Turn back into RGB color
			-- local t = from_hsl(hsl, self.alpha)
			-- print("darker is r"..hsl.red.."g"..hsl.green.."b"..hsl.blue)
			-- print("brighten is r"..t.red.."g"..t.green.."b"..t.blue)
			return from_hsl(hsl, self.alpha)
		end;

		darken = warp(function(new, fac)
			-- TODO: is there any point to this being different than brighten? Probably especially not now.
			new.red = clip(new.red - (new.red * fac))
			new.blue = clip(new.blue - (new.blue * fac))
			new.green = clip(new.green - (new.green * fac))
		end);
	};

	construct = function(r,g,b,a)
		local new = {}
		if g == nil then
			if type(r) == 'string' then
				assert(false) -- TODO parse color string
			elseif type(r) == 'table' then
				if r.hue then
					return from_hsl(r, r.alpha or g)
				elseif r.r and r.g and r.b then
					new.red = r.r
					new.green = r.g
					new.blue = r.b
					new.alpha = r.a
				else
					new.red = r[1]
					new.green = r[2]
					new.blue = r[3]
					new.alpha = r[4]
				end
			else assert(false) end
		else
			if color.id(r) then
				new.red = r.red
				new.green = r.green
				new.blue = r.blue
				new.alpha = g
			else
				new.red = r
				new.green = g
				new.blue = b
				new.alpha = a
			end
		end
		return new
	end
}

function color.unmarshal(str)
	local raw = lib.str.meta_dearmor(raw)
	local o = colorStruct.dec(raw)
	return color(
		o.red   / 0xff,
		o.green / 0xff,
		o.blue  / 0xff,
		o.alpha / 0xff
	)
end;

return color

Added mods/vtlib/dbg.lua version [3d3bef9b5d].











































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local lib = ...
local dbg = {
	aloud = false;
}

local lastmod, lastarea

function dbg.debugger(area)
	local depth = 0
	local d = {}
	function d.enter() depth = depth+1 end
	function d.exit() depth = depth-1 end
	local mod = minetest.get_current_modname()
	if dbg.aloud then
		function d.report(fmt, ...)
			local where = debug.getinfo(2)
			local caller = debug.getinfo(3)
			if mod and (lastmod ~= mod or lastarea ~= area) then
				local ms = mod or ''
				if area then ms = ms .. '.' .. area end
				print(string.format('\27[1mmodule \27[31m%s\27[m\n%s', ms, string.rep('-', #ms + 7)))
				lastmod, lastarea = mod, area
			end
			local callsource = string.format('%s:%s', caller.name, caller.currentline)
			if caller.source ~= where.source then
				callsource = callsource .. caller.source
			end
			print(
				string.rep('  ', depth) ..
				string.format(
					'> \27[1m%s:%s\27[m ← \27[34m%s\27[m :: %s',
					where.name, where.currentline,
					callsource,
					string.format(fmt, ...)
				)
			)
		end
		function d.wrap(fn)
			return function(...)
				d.enter()
				local ret = {fn(...)}
				d.exit()
				return (table.unpack or unpack)(ret)
			end
		end
	else
		function d.report() end
		function d.wrap(fn) return fn end
	end
	return d
end

return dbg

Added mods/vtlib/image.lua version [479480cc25].













































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local lib = ...

local image
image = lib.class {

	__concat   = function(self,with) return self:blit(with) end;
	__tostring = function(self)      return self:render()   end;
	__index = {
		render = function(self)
			local str = ''
			local bracket = false
			if self.combine then
				str = string.format('[combine:%sx%s', self.w, self.h)
				for _,i in pairs(self.atop) do
					str = str .. string.format(':%s,%s=(%s)', i.at.x, i.at.y, i.img:render())
				end
			else
				for _,i in pairs(self.atop) do
					str = '(' .. i.img:render() .. ')^' .. str
				end
				if str ~= '' then
					str = str .. '('
					bracket = true
				end
				str = str .. self.string
				end
			for _,e in pairs(self.fx) do
				str = str .. '^[' .. e
				-- be sure to escape ones that take arguments
				-- correctly!
			end
			if bracket then str = str .. ')' end
			return str
		end;

		-- must be used to mark an image before
		-- the second form of blit can be used
		compound = function(self, w, h)
			return image.change(self, {
				combine = true;
				w = w, h = h;
			})
		end;

		blit = function(self, img, at)
			assert((not at) or self.combine)
			if img then return image.change(self, {
				atop = lib.tbl.append(self.atop, {{img=img, at=at}})
			}) else return self end
		end;

		multiply = function(self, color)
			return image.change(self, {
				fx = lib.tbl.append(self.fx, {'multiply:' .. tostring(color)})
			})
		end;

		paint = function(self, color, ratio)
			return image.change(self, {
				fx = lib.tbl.append(self.fx, {'colorize:' .. tostring(color) .. ':' .. ratio})
			})
		end;

		tint = function(self, color)
			if not color.hue then
				if not lib.color.id(color) then
					color = lib.color(color)
				end
				color = color:to_hsl()
			end
			return image.change(self, {
				fx = lib.tbl.append(self.fx, {
					string.format('colorizehsl:%s:%s:%s',
						color.hue, color.sat*100, color.lum*100)
				})
			})
		end;

		shift = function(self, color)
			if color.hue == nil or color.sat == nil or color.lum == nil then
				if not lib.color.id(color) then
					color = lib.color(color)
				end
				color = color:to_hsl()
			end
			return image.change(self, {
				fx = lib.tbl.append(self.fx, {
					string.format('hsl:%s:%s:%s',
						color.hue, color.sat*100, color.lum*100)
				})
			})
		end;

		rehue = function(self, hue)
			return self.shift{hue=hue, sat=0, lum=0}
		end;

		colorize = function(self, color)
			local hsl
			if color.hue then
				hsl = color
			else
				hsl = color:to_hsl()
			end
			return self:shift {
				hue = hsl.hue;
				sat = hsl.sat * 2 - 1;
				lum = hsl.lum * 2 - 1;
			}
		end;

		fade = function(self, fac)
			return image.change(self, {
				fx = lib.tbl.append(self.fx, {'opacity:' .. (255 - 255*fac)})
			})
		end;

		transform = function(self, kind)
			return image.change(self, {
				fx = lib.tbl.append(self.fx, {'transform' .. tostring(kind)})
			})
		end;

		glow = function(self,color) return self:blit(self:multiply(color)) end;
	};

	construct = function(file, w, h) return {
		string = file;
		atop = {};
		fx = {};
		combine = w and h and true or nil;
	} end;
}
return image

Added mods/vtlib/init.lua version [d31a921c62].















































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local ident = minetest.get_current_modname()
local path = minetest.get_modpath(ident)

local lib = {}
_G[ident] = lib

local function
component(name)
	local p = string.format('%s/%s.lua', path, name)
	print('[vtlib] loading component ' .. p)
	local chunk, err = loadfile(p)
	if chunk == nil then error(err) end
	lib[name] = chunk(lib, ident, path)
end

component 'dbg'

-- primitive manip
component 'tbl'
component 'class'
component 'math'
component 'str'

-- reading and writing data formats
component 'marshal'

-- classes
component 'color'
component 'image'
component 'ui'
component 'tree'

-- organization
component 'registry'

-- game object manip
component 'item'
component 'node'
component 'obj'

Added mods/vtlib/item.lua version [29827ac7a0].





















































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local fn = {}
local lib = ...

fn.match = function(a,b,exact)
	if exact == nil then exact = true end
	if exact then return ItemStack(a):to_string() == ItemStack(b):to_string() else
		a,b = ItemStack(a), ItemStack(b)
		if a:get_name()	~= b:get_name() then return false, b end
		if a:get_count() <= b:get_count() then
			return true, ItemStack {
				name = a:get_name();
				count = b:get_count() - a:get_count();
			}
		end
	end
end

-- it is extremely unfortunate this function needs to exist.
-- minetest needs to export its matching capabilities already
fn.groupmatch = function(identity,item,exact)
	if exact == nil then exact = true end
	local count
	if type(identity) == 'table' then
		count = identity.count
		identity = identity.name
	else
		if lib.str.beginswith(identity, 'group:') then
			identity,count = lib.tbl.split(identity,'%s+',true)
			if count
				then count = tonumber(count)
				else count = 1
			end
		else
			local is = ItemStack(identity)
			identity,count = is:get_name(), is:get_count()
		end
	end

	if lib.str.beginswith(identity, 'group:') then
		local stack = ItemStack(item)
		local groups = lib.str.explode(string.sub(identity,7), ',')
		for _,g in pairs(groups) do
			local rn,rv = lib.tbl.split(g,'=')
			local gv = minetest.get_item_group(stack:get_name(), rn)
			if rv then
				if gv ~= tonumber(rv) then return false, stack end
			else
				if (not gv) or gv == 0 then return false, stack end
			end
		end

		if stack:get_count() < count then return false, stack end

		if exact then
			if stack:get_count() ~= count then
				return false, stack end
			return true, ItemStack(nil)
		else
			stack:take_item(count)
			return true, stack
		end
	else return fn.match(identity,item,exact) end
end

-- local try = function(a,b,e)
-- 	print('::: match',e==false and 'inexact' or 'exact',dump(a))
-- 	print('\t against',dump(b))
-- 	local res, leftover = fn.groupmatch(a,b,e)
-- 	print('result',res)
-- 	if leftover and leftover:get_count() > 0 then
-- 		print('leftover items:',leftover:to_string())
-- 	end
-- 	print("\n")
-- end

function fn.getStorage(item, var)
	return lib.marshal.t.inventoryList.dec(
		lib.str.meta_dearmor(
			item:get_meta():get_string(var)
		)
	)
end

function fn.setStorage(item, var, lst)
	item:get_meta():set_string(var, lib.str.meta_armor(
		lib.marshal.t.inventoryList.enc(lst)
	))
end

return fn

Added mods/vtlib/marshal.lua version [ec9d9f2682].



























































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
-- [ʞ] marshal.lua
--  ~ lexi hale <lexi@hale.su>
--  © EUPLv1.2
--  ? a replacement for the shitty old Sorcery marshaller
--    this marshaller focuses on reliability and extensibility;
--    it is much less space-efficient than the (broken) Sorcery
--    marshaller or the slick parvan marshaller. i don't care anymore

local lib = ...
local m = {
	t = {};
	g = {};
}

local T,G = m.t, m.g

-- a type is an object with two functions 'enc' and 'dec'

----------------
--- utilities --
----------------


local debugger = lib.dbg.debugger 'marshal'
local report = debugger.report

function m.streamReader(blob)
	local idx = 1
	local blobLen = #blob
	local function advance(ct)
		if not ct then
			--report('advancing to end of %s-byte blob',  blobLen)
			idx = blobLen+1
		else
			--report('advancing %s bytes from %s of %s', ct, idx, blobLen)
			assert(idx+ct <= blobLen + 1)
			idx = idx + ct
		end
	end
	local function dataLeft() return idx <= blobLen end
	local function consume(ct)
		if ct == 0 then return '' end
		assert(dataLeft(), string.format('wanted %s bytes but no data left: %s/%s', ct, idx, blobLen))
		local str = string.sub(blob, idx,
		  ct and idx + ct - 1 or nil)
		advance(ct)
		return str
	end
	return {
		dataLeft = dataLeft;
		advance = advance;
		consume = consume;
		dec = function(t) -- parse a fixed-size type in the stream
			assert(t.sz, 'type ' .. t.name .. ' is variably-sized')
			report('parsing type %s from stream at %s/%s', t.name, idx, blobLen)
			return t.dec(consume(t.sz))
		end;
	}
end

function m.streamEncoder(sizeType)
	local encFrags = {}
	local encoder = {}
	function encoder.push(v, ...)
		assert(type(v) == 'string')
		if v ~= nil then
			table.insert(encFrags, v)
		end
		if select('#', ...) > 0 then encoder.push(...) end
	end
	function encoder.ppush(...) -- "pascal push"
		local sz = 0
		local function szi(e, ...)
			if e then sz = sz + #e end
			if select('#') > 0 then szi(...) end
		end
		szi(...)
		encoder.push(sizeType.enc(sz), ...)
	end
	function encoder.pspush(v, ...) -- "pascal struct push"
		if v~=nil then encoder.ppush(v) end
		if select('#', ...) > 0 then encoder.prpush(...) end
	end
	function encoder.peek()
		return table.concat(encFrags)
	end
	function encoder.pull()
		local s = encoder.peek()
		encFrags = {}
		return s
	end

	return encoder
end

function m.metaStore(map, prefix)
	report('generating metaStore for %s', dump(map))
	if prefix == true then prefix = minetest.get_current_modname() end
	local function keyFor(k)
		k = map[k].key
		if prefix then return prefix .. ':' .. k end
		return k
	end

	return function(obj)
		local m = obj:get_meta()
		local store = {}
		function store.write(key, val)
			report('store: setting %q(%q)=%s (mapping %s)', key, keyFor(key), dump(val), dump(map[key]))
			local armored = lib.str.meta_armor(map[key].type.enc(val))
			m:set_string(keyFor(key), armored)
			return store
		end
		function store.read(key)
			report('store: reading %q', key)
			local dearmored = lib.str.meta_dearmor(m:get_string(keyFor(key)))
			return map[key].type.dec(dearmored)
		end
		function store.erase(key)
			m:set_string(keyFor(key), '')
			return store
		end
		function store.over(key,fn)
			local n = fn(read(key))
			if n ~= nil then write(key,n) end
			return store
		end
		return store
	end
end


-------------------------------
-- generic type constructors --
-------------------------------

function G.int(bits,signed)
	local bytes = math.ceil(bits / 8)
	local max = 2 ^ bits
	local spoint = math.floor(max/2)
	return {
		sz = bytes;
		name = string.format("%sint<%s>",
			signed and 's' or 'u', bits
		);
		enc = function(obj)
			obj = obj or 0
			local val = math.abs(obj)
			local str = ''
			if signed then
				local max = math.floor(max / 2)
				if (obj > max) or (obj < (0-(max+1))) then
					return m.err.domain end
				if obj < 0 then val = val + spoint end
				-- e.g. for 8bit: 0x80 == -1; 0xFF = -128
			else
				if val > max then return m.err.domain end
			end
			for i=1,bytes do
				local n = math.fmod(val, 0x100)
				str = str .. string.char(n)
				val = math.floor(val / 0x100)
			end
			return str
		end;
		dec = function(str)
			local val = 0
			for i = 0, bytes-1 do
				local b = string.byte(str,bytes - i)
				val = (val * 0x100) + (b or 0)
			end
			if signed then
				if val > spoint then val = 0 - (val - spoint) end
			end
			return val
		end;
	}
end

local size = G.int(32, false)


function G.struct(...)
	-- struct record {
	--		uint< 8> keySz;
	--		uint<32> valSz;
	--		string[keySz] name;
	--		string[valSz] data;
	-- }
	-- struct struct {
	--		uint<32>         nRecords;
	--		record[nRecords] records;
	-- }
	local def, name
	if select('#', ...) >= 2 then
		name, def = ...
	else
		def = ...
	end
	name = 'struct' .. (name and ':' .. name or '');
	report('defining struct name=%q fields=%s', name, dump(def))
	return {
		name = name;
		enc = function(obj)
			local enc = m.streamEncoder()
			local n = 0
			for k,ty in pairs(def) do n=n+1
				local encoded = ty.enc(obj[k])
				enc.push(T.u8.enc(#k), size.enc(#encoded), k, encoded)
			end
			return size.enc(n) .. enc.peek()
		end;
		dec = debugger.wrap(function(blob)
			if blob == '' then
				-- struct is more likely to be used directly, as the top of a serialization
				-- tree, which means it is more likely to be exposed to ill-formed input.
				-- a particularly common case will be the empty string, returned when a
				-- get_string is performed on an empty key. i think the most sensible behavior
				-- here is to return nil, rather than just crashing
				return nil
			end
			local s = m.streamReader(blob)
			local obj = {}
			report('struct.dec: decoding type %s; reading string %s', name, dump(blob))
			local nRecords = s.dec(size)
			while s.dataLeft() and nRecords > 0 do
				report('%s records left', nRecords)
				local	ksz = s.dec(T.u8)
				local	vsz = s.dec(size)
				local k = s.consume(ksz)
				local v = s.consume(vsz)
				local ty = def[k]
				report('decoding field %s of type %s, %s bytes', k, ty.name, vsz)
				if not ty then
					report('warning: unfamiliar record %q found in struct', k)
				else
					obj[k] = ty.dec(v)
				end
				nRecords = nRecords - 1
			end
			if s.dataLeft() then
				report('warning: junk at end of struct %q',s.consume())
			end
			report('returning object %s', dump(obj))
			return obj
		end);
	}
end

function G.fixed(bits, base, prec, sign)
	local c = G.int(bits, sign)
	local mul = base ^ prec
	return {
		sz = c.sz;
		name = string.format("%sfixed<%s,%s,%s>",
			sign and 's' or 'u',
			bits, base, prec
		);
		enc = function(v)
			return c.enc(v)
		end;
		dec = function(s)
			local v = c.dec(s)
			return v / mul
		end;
	}
end

function G.range(min, max, bits)
	local d = max-min
	local precType = G.fixed(bits, d, 1, false)
	return {
		sz = precType.sz;
		name = string.format("range<%s,%s~%s>",
			bits, min, max
		);
		enc = function(v)
			return precType.enc((v - min) / d)
		end;
		dec = function(s)
			local v = precType.dec(s)
			return d*v + min
		end;
	}
end

T.str = {
	name = 'str';
	enc = function(s) return s end;
	dec = function(s) return s end;
}


function G.array(bitlen,t)
	local sz = G.int(bitlen,false)
	local name = string.format("array<%s,%s>",
		sz.name, t.name
	);
	return {
		name = name;
		enc = debugger.wrap(function(obj)
			local s = m.streamEncoder(size)
			report('encoding array of type %s', name)
			local nVals = (obj and #obj) or 0
			s.push(sz.enc(nVals))
			for i=1,nVals do
				report('encoding value %s: %s', i, dump(obj[i]))
				s.ppush(t.enc(obj[i]) or '')
			end
			report('returning blob %q', s.peek())
			return s.peek()
		end);
		dec = debugger.wrap(function(blob)
			local s = m.streamReader(blob)
			local ct = s.dec(sz)
			local obj = {}
			report('decoding array %s of size %s', name, ct)
			for i=1,ct do
				report('decoding elt [%s] of %s', i, ct)
				local eltsz = s.dec(size)
				obj[i] = t.dec(s.consume(eltsz))
			end
			if s.dataLeft() then
				print('warning: junk at end of array', dump(s.consume))
			end
			return obj
		end);
	}
end

function G.enum(values)
	local n = #values
	local bits = 8
	if     n > 65536 then bits = 32 -- don't think we really need any more
	elseif n > 256   then bits = 16 end
	local t = G.int(bits, false)
	local map = {}
	for k,v in pairs(values) do map[v] = k end
	return {
		name = string.format("enum<[%s]>", dump(values) );
		sz = t.sz;
		enc = function(v)
			local iv = map[v] or error('value ' .. v .. ' not allowed in enum')
			return t.enc(iv)
		end;
		dec = function(s)
			return values[t.dec(s)]
		end;
	}
end

function G.class(struct, extract, construct)
	return {
		sz = struct.sz;
		name = string.format("class<%s>",
			struct.name
		);
		enc = debugger.wrap(function(v)
			report('encoding class<%s>', struct.name)
			return struct.enc(extract(v))
		end);
		dec = debugger.wrap(function(s)
			report('decoding class<%s>', struct.name)
			return construct(s ~= '' and struct.dec(s) or nil)
				-- allow classes to handle empty metastrings after their own fashion
		end);
	}
end



-------------------------
-- common type aliases --
-------------------------

for _, sz in pairs{8,16,32,64} do
	T['u' .. tostring(sz)] = G.int(sz,false)
	T['s' .. tostring(sz)] = G.int(sz,true)
end


T.factor  = G.fixed(16, 10.0, 3, false);
T.decimal = G.fixed(32, 10.0, 5, true);
T.fixed   = G.fixed(32, 2.0, 16, true);
T.double  = G.fixed(64, 10.0, 10, true);
T.wide    = G.fixed(64, 2.0, 32, true);

T.angle     = G.range(   0, 360, 16);
T.turn      = G.range(-360, 360, 16);
T.clamp     = G.range(   0, 1.0, 16);
T.tinyClamp = G.range(   0, 1.0, 8);

-----------------------------------
-- abstractions over engine data --
-----------------------------------

T.inventoryList = G.class(
	G.array(16, G.struct ('inventoryItem', {
		index = T.u16;
		itemString = T.str;
	})),
	function(lst)
		report('encoding inventory list %s', dump(lst))
		local ary = {}
		for i, s in pairs(lst) do
			if not s:is_empty() then
				table.insert(ary, {index = i, itemString = s:to_string()})
			end
		end
		report('list structure: %s', dump(ary))
		return ary
	end,
	function(ary)
		report('decoding inventory list %s', dump(ary))
		if not ary then return {} end
		local tbl = {}
		for _, s in pairs(ary) do
			tbl[s.index] = ItemStack(s.itemString)
		end
		return tbl
	end
);

T.inventory = G.class(
	G.array(8, G.struct('inventory', {
		name = T.str;
		items = T.inventoryList;
	})),
	function (inv)
		if inv.get_lists then
			inv = inv:get_lists()
		end
		local lst = {}
		for name, items in pairs(inv) do table.insert(lst, {name=name,items=items}) end
		return lst
	end,
	function (tbl)
		if not tbl then return {} end
		local inv = {}
		for _, e in pairs(tbl) do
			inv[e.name] = e.items
		end
		return inv
	end
);

-------------------
-- legacy compat --
-------------------

-- strings are now fixed at 32-bit sizetype
T.text   = T.str
T.phrase = T.str
function G.blob() return T.str end

function m.transcoder(tbl)
	local ty = G.struct(tbl)
	return ty.enc, ty.dec
end

return m

Added mods/vtlib/math.lua version [28fc5b216f].









































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local lib = ...
local fn = {}

fn.vsep = function(vec) -- separate a vector into a direction + magnitude
	return vec:normalize(), vec:length()
end

-- minetest now only provides the version of this function that sqrts the result
-- which is pointlessly wasteful much of the time
fn.vdsq = function(a,b)
	local d = vector.subtract(v1,v2)
	return (d.x ^ 2) + (d.y ^ 2) + (d.z ^ 2)
end

fn.vdcomp = function(dist,v1,v2) -- compare the distance between two points
	-- (cheaper than calculating distance outright)
	local d if v2
		then d = vector.subtract(v1,v2)
		else d = v1
	end
	local dsq = (d.x ^ 2) + (d.y ^ 2) + (d.z ^ 2)
	return dsq / (dist^2)
	-- [0,1) == less then
	-- 1 == equal
	-- >1 == greater than
end

-- produce an SI expression for a quantity
fn.si = function(unit, val, full, uncommonScales)
	if val == 0 then return '0 ' .. unit end
	local scales = {
		{30, 'Q', 'quetta',true,  'q', 'quecto',true};
		{27, 'R', 'ronna', true,  'r', 'ronto', true};
		{24, 'Y', 'yotta', true,  'y', 'yocto', true};
		{21, 'Z', 'zetta', true,  'z', 'zepto', true};
		{18, 'E', 'exa',   true,  'a', 'atto',  true};
		{15, 'P', 'peta',  true,  'f', 'femto', true};
		{12, 'T', 'tera',  true,  'p', 'pico',  true};
		{9, 'G', 'giga',   true,  'n', 'nano',  true};
		{6, 'M', 'mega',   true,  'μ', 'micro', true};
		{3, 'k', 'kilo',   true,  'm', 'milli', true};
		{2, 'h', 'hecto',  false, 'c', 'centi', true};
		{1, 'da','deca',   false, 'd', 'deci',  false};
	}
	for i, s in ipairs(scales) do
		local amt, smaj, pmaj, cmaj,
		           smin, pmin, cmin = lib.tbl.unpack(s)

		if math.abs(val) > 1 then
			if uncommonScales or cmaj then
				local denom = 10^amt
				if math.abs(val) >= (10^(amt)) then
					return string.format("%s %s%s",
						val / denom, (full and pmaj or smaj), unit)
				end
			end
		elseif math.abs(val) < 1 then
			if uncommonScales or cmin then
				local denom = 10^-amt
				if math.abs(val) <= (10^-(amt-1)) then
					return string.format("%s %s%s",
						val / denom, (full and pmin or smin), unit)
				end
			end
		end
	end

	return string.format("%s %s", val, unit)
end

function fn.lerp(t, a, b) return (1-t)*a + t*b end

function fn.trim(fl, prec)
	local fac = 10^prec
	return math.floor(fl * fac) / fac
end

function fn.sign(v)
	if v > 0 then return 1
	elseif v < 0 then return -1
	else return 0 end
end

function fn.toward(from, to, mag)
	local dir = fn.sign(to - from)
	local step = mag * dir
	if (dir ==  1 and from+step < to)
	or (dir == -1 and from+step > to)
		then return from+step
		else return to
	end
end

fn.rng = lib.class {
	__name = 'rng';
	construct = function(seed)
		return {seed = seed, rng = PcgRandom(seed)}
	end;
	__index = {
		int = function(self,m,x)
			return self.rng:next(m,x)
		end;
		real = function(self,m,x)
			local i = self:int()
			local f = (i+bit.lshift(1,31)) / bit.lshift(1, 32)
			if m==nil then return f end
			if x==nil then x=m m=0  end
			return m + ((x-m) * f)
		end;
		fork = function(self)
			return fn.rng(self:int())
		end;
	};
	__add = function(self,n)
		return fn.rng(self.seed + n)
	end;
}

fn.seedbank = lib.class {
	__name = 'seedbank';
	construct = function(seed)
		return {seed = seed}
	end;
	__index = function(self, n)
		return fn.rng(PcgRandom(self.seed+n):next())
	end;
	__add = function(self, n)
		return fn.seedbank(self.seed + n)
	end;
}
-- function fn.vlerp
return fn

Added mods/vtlib/mod.conf version [8174a4aaf3].









>
>
>
>
1
2
3
4
name = vtlib
description = velartrill's utility library
author = velartrill
title = ʞlib

Added mods/vtlib/node.lua version [75e0d781d2].













































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local lib = ...
local V = vector.new
local ofs = {
	neighbors = {
		V( 0,  1,  0);
		V( 0, -1,  0);
		V( 1,  0,  0);
		V(-1,  0,  0);
		V( 0,  0,  1);
		V( 0,  0, -1);
	};
	corners = {
		V( 1,  0,  1);
		V(-1,  0,  1);
		V(-1,  0, -1);
		V( 1,  0, -1);
	};
	planecorners = {
		V( 1,  0,  1);
		V(-1,  0,  1);
		V(-1,  0, -1);
		V( 1,  0, -1);

		V( 1,  1,  0);
		V(-1,  1,  0);
		V(-1, -1,  0);
		V( 1, -1,  0);
	};
	cubecorners = {
		V( 1,  1,  1);
		V(-1,  1,  1);
		V(-1, -1,  1);
		V(-1, -1, -1);
		V( 1, -1, -1);
		V( 1,  1, -1);
		V( 1, -1,  1);
		V(-1,  1, -1);
	};
	nextto = {
		V( 1,  0,  0);
		V(-1,  0,  0);
		V( 0,  0,  1);
		V( 0,  0, -1);
	};
	cardinal = {
		V( 1, 0,  0);
		V(-1, 0,  0);
		V( 0, 0,  1);
		V( 0, 0, -1);
	};
}

ofs.adjoining = lib.tbl.append(lib.tbl.append(
	ofs.neighbors,ofs.planecorners),ofs.cubecorners)

local purge_container = function(only, pos,node,meta,user)
	local offset = function(pos,range)
		local r = function(min,max)
			return (math.random() * (max - min)) + min
		end
		return {
			x = pos.x + r(0 - range, range);
			y = pos.y;
			z = pos.z + r(0 - range, range);
		}
	end
	for name, inv in pairs(meta.inventory) do
		if only and not lib.tbl.has(only,name) then goto skip end
		for _, item in pairs(inv) do
			if not item:is_empty() then
				minetest.add_item(offset(pos,0.4), item)
			end
		end
	::skip::end
end;

local force = function(pos,preload_for)
	local n = minetest.get_node_or_nil(pos)
	if preload_for then lib.node.preload(pos,preload_for) end
	if n then return n end

	minetest.load_area(pos)
	return minetest.get_node(pos)
end;

local amass = function(startpoint,names,directions)
	if not directions then directions = ofs.neighbors end
	local check = function(n)
		return lib.tbl.has(names, n.name, function(check,against)
			return lib.item.groupmatch(against,check)
		end)-- match found
	end
	if type(names) == 'function' then check = names end
	local nodes, positions, checked = {},{},{}
	local checkedp = function(pos)
		for _,v in pairs(checked) do
			if vector.equals(pos,v) then return true end
		end
		return false
	end
	local i,stack = 1,{startpoint} repeat
		local pos = stack[i]
		local n = force(pos)
		if check(n, pos, nodes, positions) then
			-- record the find
			nodes[pos] = n.name
			if positions[n.name]
				then positions[n.name][#positions[n.name]+1] = pos
				else positions[n.name] = {pos}
			end

			-- check selected neighbors to see if any need scanning
			for _,d in pairs(directions) do
				local sum = vector.add(pos, d)
				if not checkedp(sum) then
					stack[#stack + 1] = sum
					checked[#checked+1] = sum
				end
			end
		end
		checked[#checked+1] = pos
		i = i + 1
	until i > #stack
	return nodes, positions
end;

local is_air = function(pos)
	local n = force(pos)
	if n.name == 'air' then return true end
	local d = minetest.registered_nodes[n.name]
	if not d then return false end
	return (d.walkable == false) and (d.drawtype == 'airlike' or d.buildable_to == true)
end;

local is_clear = function(pos)
	if not lib.node.is_air(pos) then return false end
	local ents = minetest.get_objects_inside_radius(pos,0.5)
	if #ents > 0 then return false end
	return true
end;

local function boxwarp(nb, mogrifier, par)
	if nb == nil then
		return
	elseif nb.type then
		if nb.type == 'fixed' or nb.type == 'leveled' then
			for i, b in ipairs(nb.fixed) do
				boxwarp(b, mogrifier, nb)
			end
		elseif nb.type == 'wallmounted' then
			boxwarp(nb.wall_top,    mogrifier, nb)
			boxwarp(nb.wall_bottom, mogrifier, nb)
			boxwarp(nb.wall_side,   mogrifier, nb)
		elseif nb.type == 'connected' then
			for _, state in pairs{'connect', 'disconnected'} do
				for _, dir in pairs{'top','bottom','front','left','back','right'} do
					boxwarp(nb[state ..'_'.. dir], mogrifier, nb)
				end
				boxwarp(nb.disconnected, mogrifier, nb)
				boxwarp(nb.disconnected_sides, mogrifier, nb)
			end
		elseif nb.type == 'regular' then
			nb.type = 'fixed'
			nb.fixed = {-.5, -.5, -.5; .5, .5, .5};
			boxwarp(nb.fixed, mogrifier, nb);
		end
	elseif nb[1] then
		mogrifier(nb, par)
	end
end

local function boxwarped(box, warp) --oof
	local c = lib.tbl.deepcopy(box)
	boxwarp(c, warp)
	return c
end

return {
	offsets = ofs;
	purge_container = function(...) return purge_container(nil, ...) end;
	purge_only = function(lst)
		return function(...)
			return purge_container(lst, ...)
		end
	end; 

	is_air = is_air;
	is_clear = is_clear;

	insert = function(item, slot, npos, user, inv)
		inv = inv or minetest.get_meta(npos):get_inventory()
		if inv:room_for_item(slot,item) then
			inv:add_item(slot,item)
		else repeat
			if user then
				local ui = user:get_inventory()
				if ui:room_for_item('main', item) then
					ui:add_item('main', item)
					break
				end
			end
			minetest.add_item(npos, item)
		until true end
	end;

	install_bed = function(bed, where, dir)
		local bottom = bed .. '_bottom'
		local top = bed .. '_top'
		local d
		if type(dir) == 'number' then
			d = dir
			dir = minetest.facedir_to_dir(d)
		else
			d = minetest.dir_to_facedir(dir)
		end
		if not is_clear(where) and is_clear(where - dir) then return false end
		minetest.set_node(where,       {name = top, param2 = d})
		minetest.set_node(where - dir, {name = bottom, param2 = d})
		return true
	end;

	get_arrival_point = function(pos)
		local try = function(p)
			local air = lib.node.is_clear
			if air(p) then
				if air(vector.offset(p,0,1,0))  then return p end
				if air(vector.offset(p,0,-1,0)) then return vector.offset(p,0,-1,0) end
			end
			return false
		end
		
		do local t = try(pos) if t then return t end end
		for _,o in pairs(ofs.neighbors) do
			local p = vector.add(pos, o)
			do local t = try(p) if t then return t end end
		end
	end;


	forneighbor = function(pos, n, fn)
		for _,p in pairs(n) do
			local sum = vector.add(pos, p)
			local n = minetest.get_node(sum)
			if n.name == 'ignore' then
				minetest.load_area(sum)
				n = minetest.get_node(sum)
			end
			if fn(sum, n) == false then break end
		end
	end;

	amass = amass;
	
	force = force;

	blockpos = function(pos)
		return {
			x = math.floor(pos.x / 16);
			y = math.floor(pos.y / 16);
			z = math.floor(pos.z / 16);
		}
	end;

	preload = function(pos, user)
		minetest.load_area(pos)
		user:send_mapblock(lib.node.blockpos(pos))
	end;

	discharger = function(pos)
		local below = force(vector.subtract(pos,{x=0,y=1,z=0}))
		if below.name == 'hopper:hopper'
		or below.name == 'hopper:hopper_side' then
			local hopper = minetest.get_meta(below):get_inventory()
			return function(i)
				if hopper:room_for_item('main',i) then
					return hopper:add_item('main',i), true
				end
				return i, false
			end
		else
			return function(i) return i, false end
		end
	end;

	autopreserve = function(id, tbl)
		tbl.drop = tbl.drop or {
			max_items = 1;
			items = {
				{ items = {id} };
			};
		}
		local next_apn = tbl.after_place_node
		tbl.after_place_node = function(...) local pos, who, stack = ...
			minetest.get_meta(pos):from_table(stack:get_meta():to_table())
			if next_apn then return next_apn(...) end
		end
		local next_pm = tbl.preserve_metadata
		tbl.preserve_metadata = function(...) local pos, node, meta, drops = ...
			drops[1]:get_meta():from_table({fields = meta})
			if next_pm then return next_pm(...) end
		end
		return tbl
	end;
	reg_autopreserve = function(id, tbl)
		minetest.register_node(id, lib.node.autopreserve(id, tbl))
	end;

	boxwarp = boxwarp;
	boxwarped = boxwarped;
}

Added mods/vtlib/obj.lua version [7fd77150af].































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
-- functions for working with entities inexplicably missing
-- from the game API

local fn = {}
local lib = ...

-- WARNING: INEFFICIENT AS FUCK
fn.identify = function(objref) --> objectid
	for _, o in pairs(minetest.get_connected_players()) do
		if objref == o then return o:get_player_name(), 'player' end
	end
	for id, le in pairs(minetest.luaentities) do
		if le.object == objref then return id, 'entity' end
	end
end

fn.handle = lib.class {
	__newindex = function(self,key,newval)
		local hnd if self.player
			then hnd = minetest.get_player_by_name(self._id)
			else hnd = minetest.luaentities[self._id]
		end
		if key == 'id' then
			if type(newval) == 'string' then
				local p = minetest.get_player_by_name(newval)
				if p then
					self._id = newval
					self.player = true
					return
				end
			end
			if minetest.luaentities[newval] then
				self._id = newval
				self.player = false
			else error('attempted to assign invalid ID to entity handle') end
		elseif key == 'obj' then
			local no, kind = fn.identify(newval)
			if no then
				self._id = no
				if kind == 'player'
					then self.player = true
					else self.player = false
				end
			else error('attempted to assign invalid ObjectRef to entity handle') end
		elseif key == 'stack' and self.kind == 'item' then
			hnd:set_item(newval)
		end
	end;
	__index = function(self,key)
		local hnd if self.player then
			hnd = minetest.get_player_by_name(self._id)
		else
			hnd = minetest.luaentities[self._id]
		end
		if key == 'online' then
			return hnd ~= nil
		elseif key == 'id' then
			if self.player then return nil
			else return self._id end
		elseif key == 'obj' then
			if self.player
				then return hnd
				else return hnd.object
			end
		elseif key == 'kind' then
			if     self.player                  then return 'player'
			elseif hnd.name == '__builtin:item' then return 'item'
			                                    else return 'object' end
		elseif key == 'name' then
			if self.player then return self._id
			elseif self.kind == 'item' then
				return ItemStack(hnd.itemstring):get_name()
			else return hnd.name end
		elseif key == 'stack' and self.kind == 'item' then
			return ItemStack(hnd.itemstring)
		elseif key == 'height' then
			if kind == 'item' then return 0.5
			elseif kind == 'player' then
				local eh = hnd.object:get_properties().eye_height
				return eh and (eh*1.2) or 1
			else
				local box = hnd.object:get_properties().collisionbox
				if box then
					local miny,maxy = box[2], box[5]
					return maxy-miny, miny
				else return 0 end
			end
		end
	end;
	construct = function(h)
		local kind, id
		if type(h) == 'string' and minetest.get_player_by_name(h) ~= nil then
			kind = 'player';
			id = h
		elseif minetest.luaentities[h] then
			kind = 'entity';
			id = h
		else id, kind = fn.identify(h) end

		if not id then
			error('attempted to construct object handle from invalid value')
		end

		return {
			player = kind == 'player';
			_id = id;
		}
	end;
}

return fn

Added mods/vtlib/registry.lua version [0cb9a501da].







































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local register = {}
local registry = {
	defercheck = function() return true end
	-- used to warn about deferments that have not been discharged by a certain threshold
}
registry.mk = function(name,db)
	local reg = {}
	if not db then -- auxiliary db, stored in registry itself
		reg.db = {}
		db = reg.db
	end
	local dat = {
		iters = {};
		state = {};
		defer = {};
	}
	reg.invoke = function(fnid,tgtid)
		local fno = dat.iters[fnid]
		if not fno then return false end

		local runid = fnid .. '@' ..tgtid
		if dat.state[runid] then return true end

		if fno.deps then for k,f in pairs(fno.deps) do
			if reg.invoke(f,tgtid) == false then return false end
		end end
		
		fno.fn(tgtid, db[tgtid])

		dat.state[runid] = true
		return true
	end
	reg.foreach = function(ident,deps,fn)
		dat.iters[ident] = {deps = deps, fn = fn}
		for k in pairs(db) do
			if reg.invoke(ident,k) == false then
				-- not all dependencies are available
				-- to run yet; must defer until then
				dat.defer[#dat.defer+1] = ident
				return false
			end
		end
		for i,dfn in pairs(dat.defer) do
			local deferred = dat.iters[dfn]
			for _,d in pairs(deferred.deps) do
				if not dat.iters[d] then goto skipdfmt end
			end
			-- a deferred function can now be run, do so
			table.remove(dat.defer,i)
			reg.foreach(dfn,deferred.deps,deferred.fn)
		::skipdfmt::end

		return true
	end
	reg.link = function(key,value)
		-- support simple arrays as well as kv stores
		if value == nil then
			value = key
			key = #db+1
		end
		db[key] = value
		for id in pairs(dat.iters) do 
			reg.invoke(id,key)
		end
		return value
	end
	reg.meld = function(tbl)
		for k,v in pairs(tbl) do reg.link(k,v) end
	end
	register[name] = reg

	local nextfn = registry.defercheck
	registry.defercheck = function()
		if #dat.defer ~= 0 then
			print('WARNING: ' .. tostring(#dat.defer) .. ' deferred iterator(s) have not yet been discharged for registry “' .. name .. '”')
			local log = print
			for i,v in pairs(dat.defer) do
				log('\t' .. tostring(i) .. ') ' .. v)
			end
			log('there is likely a missing dependency or dependency ordering problem. also make sure you have spelled the names of the iterator dependencies correctly')
			return false
		end
		nextfn()
	end
	return reg
end

return registry

--[[
if sorcery.DEBUG then
	local function dump(tbl,indent)
		indent = indent or 0
		local space = string.rep(' ',indent*4)
		for k,v in pairs(tbl) do
			if type(v) == 'table' then
				print(string.format('%s%s = {',space,k))
				dump(v,indent + 1)
				print(string.format('%s}', space))
			else
				print(string.format('%s%s = %q',space,k,tostring(v)))
			end
		end
	end 

	local metals = {
		oregonium = {desc = 'Oregonium'};
		nevadite = {desc = 'Nevadite'};
	}
	local myreg = sorcery.registry.mk('metals',metals)

	sorcery.register.metals.link('californium', {
		desc = "Californium";
	})

	sorcery.register.metals.foreach('sorcery:mkingot',{'sorcery:mkfrag'}, function(k,v)
		local ingot = 'sorcery:' .. k .. '_ingot'
		print('registered',ingot)
		v.parts = v.parts or {}
		v.parts.ingot = ingot;
	end)

	sorcery.registry.defercheck()

	sorcery.register.metals.link('washingtonium', {
		desc = "Washingtonium";
	})

	sorcery.register.metals.foreach('sorcery:mkfrag',{}, function(k,v)
		local fragment = 'sorcery:' .. k .. '_fragment'
		print('registered',fragment)
		v.parts = v.parts or {}
		v.parts.fragment = fragment;
	end)

	sorcery.register.metals.link('biloxite', {
		desc = "Biloxite";
	})
	
	dump(metals)
	if sorcery.registry.defercheck() then
		print('lingering deferments!')
	else
		print('all good!')
	end
end
]]

Added mods/vtlib/str.lua version [01c1839f00].





































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local sanitable = {
	from = {
		['\xfe'] = '\xf0';
		['\1'] = '\xf1';
		['\2'] = '\xf2';
		['\3'] = '\xf3';
		['\0'] = '\xf4'; -- NULs apparently can't be saved in sqlite,
						 -- or possibly just player metadata
	};

	to = {
		['\xf0'] = '\xfe';
		['\xf1'] = '\1';
		['\xf2'] = '\2';
		['\xf3'] = '\3';
		['\xf4'] = '\0';
	};
}

local utf8
if _G.minetest then
	if minetest.global_exists 'utf8' then
		utf8 = _G.utf8
	end
else
	utf8 = _G.utf8
end
if not utf8 then -- sigh
	utf8 = {}
	local bptns = {
		{0x80, 0x00};
		{0xE0, 0xC0};
		{0xF0, 0xE0};
		{0xF8, 0xF0};
	}
	local function bl(n)
		for i = 1,4 do
			local mask, ptn = bptns[i][1], bptns[i][2]
			if bit.band(mask,n) == ptn then
				return i, bit.bnot(mask)
			end
		end
		-- invalid codepoint
	end
	local function ub(bytes, ofs) ofs = ofs or 0
		local function B(n) return string.byte(bytes, n+ofs) end
		local eb, m1 = bl(B(1))
		if not eb then return -1 end
		local val = bit.band(B(1), m1)
		for i = 2,eb do
			val = bit.bor(bit.lshift(val, 6), bit.band(0x3F, B(i)))
		end
		return val
	end
	function utf8.codepoint(str, n) return ub(str, (n and n-1 or nil)) end
	local uMatchPtn = "()([^\x80-\xC1\xF5-\xFF][\x80-\xBF]*)"
	function utf8.codes(s)
		local nx = string.gmatch(s,uMatchPtn)
		return function()
			local pos,bytes = nx()
			if not pos then return nil end
			local by = ub(bytes)
			return pos, by
		end
	end
	function utf8.len(s)
		local i = 0
		for _ in utf8.codes(s) do i=i+1 end
		return i
	end
	function utf8.char(s, ...)
		local v
		if s <= 0x7F then
			v = string.char(s)
		elseif s <= 0x7FF then
			v = string.char(
				bit.bor(0xC0, bit.rshift(s,6)),
				bit.bor(0x80, bit.band(s, 0x3F))
			)
		elseif s <= 0xFFFF then
			v = string.char(
				bit.bor(0xE0,          bit.rshift(s,6*2)     ),
				bit.bor(0x80, bit.band(bit.rshift(s,6), 0x3F)),
				bit.bor(0x80, bit.band(           s,    0x3F))
			)
		elseif s <= 0x10FFFF then
			v = string.char(
				bit.bor(0xF0,          bit.rshift(s,6*3)       ),
				bit.bor(0x80, bit.band(bit.rshift(s,6*2), 0x3F)),
				bit.bor(0x80, bit.band(bit.rshift(s,6*1), 0x3F)),
				bit.bor(0x80, bit.band(           s,      0x3F))
			)
		else -- invalid byte
			v=""
		end
		if select('#', ...) > 0 then
			return v, utf8.char(...)
		else return v end
	end
end

local function nToStr(n, b, tbl)
	local str = {}
	local i = 0
	if n < 0 then
		n = -n
		str[1] = tbl['-']
		i = 1
	end
	if n == 0 then return tbl[0] else repeat i = i + 1
		local v = n%b
		n = math.floor(n / b)
		str[i] = assert(tbl[v])
	until n == 0 end
	return table.concat(str)
end

return {
	utf8 = utf8;

	capitalize = function(str)
		return string.upper(string.sub(str, 1,1)) .. string.sub(str, 2)
	end;

	beginswith = function(str,pfx)
		if #str < #pfx then return false end
		if string.sub(str,1,#pfx) == pfx then
			return true, string.sub(str,1 + #pfx)
		end
	end;

	endswith = function(str,sfx)
		if #str < #sfx then return false end
		if string.sub(str,#sfx) == sfx then
			return true, string.sub(str,1,#sfx)
		end
	end;

	explode = function(str,delim,pat) -- this is messy as fuck but it works so im keeping it
		local i = 1
		local tbl = {}
		if pat == nil then pat = false end
		repeat
			local ss = string.sub(str, i)
			local d
			if pat then
				local matches = {string.match(ss, '()' .. delim .. '()')}
				if #matches > 0 then
					local start,stop = matches[1], matches[#matches]
					d = start
					i = i + stop - 1
				end
			else
				local dl = string.len(delim)
				d = string.find(ss, delim, 1, not pat)
				if d then i = i + d + dl - 1 end
			end
			if not d then
				tbl[#tbl+1] = string.sub(ss,1,string.len(ss))
				break
			else
				tbl[#tbl+1] = string.sub(ss,1,d-1)
			end
		until i > string.len(str)
		return tbl
	end;

	rand = function(min,max)
		if not min then min = 16  end
		if not max then max = min end
		local str = ''
		local r_int   =            0x39 - 0x30
		local r_upper = r_int   + (0x5a - 0x41)
		local r_lower = r_upper + (0x7a - 0x61)
		for i = 1,math.random(max - min) + min do
			-- 0x30 -- 0x39
			-- 0x41 -- 0x5A
			-- 0x61 -- 0x71
			local codepoint = math.random(r_lower)
			if codepoint > r_upper then
				codepoint = (codepoint - r_upper) + 0x61
			elseif codepoint > r_int then
				codepoint = (codepoint - r_int) + 0x41
			else
				codepoint = codepoint + 0x30
			end
			str = str .. string.char(codepoint)
		end
		return str
	end;

	chop = function(str)
		if string.sub(str, 1,1) == ' ' then
			str = string.sub(str, 2)
		end
		if string.sub(str, #str,#str) == ' ' then
			str = string.sub(str, 1, #str - 1)
		end
		return str
	end;

	meta_armor = function(str,mark_struct)
		-- binary values stored in metadata need to be sanitized so
		-- they don't contain values that will disrupt parsing of the
		-- KV store, as minetest (stupidly) uses in-band signalling
		local sanitized = string.gsub(str, '.', function(char)
			if sanitable.from[char] then
				return '\xfe' .. sanitable.from[char]
			else return char end
		end)
		if sanitized ~= str and mark_struct then
			-- use different type code to mark struct headers for
			-- back-compat
			return string.gsub(sanitized,'^\xfe\xf0\x99','\xfe\x98')
		else return sanitized end
	end;
	meta_dearmor = function(str,cond)
		local dearmor = function(s)
			return string.gsub(s, '\xfe([\xf0\xf1\xf2\xf3\xf4])', function(char)
				return sanitable.to[char]
			end)
		end
		if cond then
			if string.sub(str,1,2) == '\xfe\x98' then
				return dearmor(string.gsub(str,'^\xfe\x98','\xfe\xf0\x99'))
			else return str end
		else return dearmor(str) end
	end;

	nExp = function(n)
		return nToStr(n, 10, {
			[0]='⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹';
			['-'] = '⁻',  ['('] = '⁽', [')'] = '⁾';
		})
	end;
	nIdx = function(n)
		return nToStr(n, 10, {
			[0]='₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉';
			['-'] = '₋', ['('] = '₍', [')'] = '₎';
		})
	end;
}

Added mods/vtlib/tbl.lua version [ed1f208dfe].







































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local lib = ...

local fn = {}

fn.shuffle = function(list)
	for i = #list, 2, -1 do
		local j = math.random(i)
		list[i], list[j] = list[j], list[i]
	end
	return list
end

fn.scramble = function(list)
	return fn.shuffle(table.copy(list))
end

fn.urnd = function(min,max)
	local r = {}
	for i=min,max do r[1 + (i - min)] = i end
	fn.shuffle(r)
	return r
end

fn.uniq = function(lst)
	local hash = {}
	local new = {}
	for i,v in ipairs(lst) do
		if not hash[v] then
			hash[v] = true
			new[#new+1] = v
		end
	end
	return new
end

fn.copy = function(t)
	local new = {}
	for i,v in pairs(t) do new[i] = v end
	setmetatable(new,getmetatable(t))
	return new
end

fn.deepcopy = table.copy or function(t)
	new = {}
	for k,v in pairs(t) do
		if type(v) == 'table' then
			new[k] = fn.deepcopy(v)
		else
			new[k] = v
		end
	end
	return new
end

fn.append = function(r1, r2)
	local new = fn.copy(r1)
	for i=1,#r2 do
		new[#new + 1] = r2[i]
	end
	return new
end

fn.merge = function(base,override)
	local new = fn.copy(base)
	for k,v in pairs(override) do
		new[k] = v
	end
	return new
end

fn.deepmerge = function(base,override,func)
	local new = {}
	local keys = fn.append(fn.keys(base),fn.keys(override))
	for _,k in pairs(keys) do
		if type(base[k]) == 'table' and
		   type(override[k]) == 'table' then
			new[k] = fn.deepmerge(base[k], override[k], func)
		elseif func and override[k] and base[k] then
			new[k] = func(base[k],override[k], k)
		elseif override[k] then
			new[k] = override[k]
		else
			new[k] = base[k]
		end
	end
	return new
end

fn.has = function(tbl,value,eqfn)
	for k,v in pairs(tbl) do
		if eqfn then
			if eqfn(v,value,tbl) then return true, k end
		else
			if value == v then return true, k end
		end
	end
	return false, nil
end

fn.keys = function(lst)
	local ks = {}
	for k,_ in pairs(lst) do
		ks[#ks + 1] = k
	end
	return ks
end

fn.pick = function(lst)
	local keys = fn.keys(lst)
	local k = keys[math.random(#keys)]
	return k, lst[k]
end

fn.unpack = table.unpack or unpack or function(tbl,i)
	i = i or 1
	if #tbl == i then return tbl[i] end
	return tbl[i], fn.unpack(tbl, i+1)
end

fn.split = function(...) return fn.unpack(lib.str.explode(...)) end

fn.each = function(tbl,f)
	local r = {}
	for k,v in pairs(tbl) do
		local v, c = f(v,k)
		r[#r+1] = v
		if c == false then break end
	end
	return r
end

fn.each_o = function(tbl,f)
	local keys = fn.keys(tbl)
	table.sort(keys)
	return fn.each(keys, function(k,i)
		return f(tbl[k],k,i)
	end)
end

fn.iter = function(tbl,fn)
	for i,v in ipairs(tbl) do fn(v, i) end
end

fn.map = function(tbl,fn)
	local new = {}
	for k,v in pairs(tbl) do
		local nv, nk = fn(v, k)
		new[nk or k] = nv
	end
	return new
end

fn.fold = function(tbl,fn,acc)
	if #tbl == 0 then
		fn.each_o(tbl, function(v)
			acc = fn(acc, v, k)
		end)
	else
		for i,v in ipairs(tbl) do
			acc = fn(acc,v,i)
		end
	end
	return acc
end

fn.walk = function(tbl,path)
	if type(path) == 'table' then
		for _,p in pairs(path) do
			if tbl == nil or tbl[p] == nil then return nil end
			tbl = tbl[p]
		end
	else
		tbl = tbl[path]
	end
	return tbl
end

fn.proto = function(tbl,proto)
	local meta = getmetatable(tbl)
	local nm = {__index = proto or tbl}
	if meta ~= nil then
		nm = table.copy(meta)
		nm.__index = proto
		nm.__metatable = meta
	end
	return setmetatable(tbl or {},nm)
end
fn.defaults = function(dft, tbl)
	tbl = tbl or {}
	local rp = {}
	for k,v in pairs(dft) do
		if tbl[k] == nil then rp[k] = v end
	end
	return fn.proto(rp, tbl)
end

fn.case = function(e, c)
	if type(c[e]) == 'function'
		then return (c[e])(e)
		else return c[e]
	end
end

fn.cond = function(exp, c)
	for i, v in ipairs(c) do
		if c[1](exp) then return c[2](exp) end
	end
end

fn.strmatch = function(tbl, str)
	if tbl == str then return true end
	if type(tbl) == 'string' then return false end
	return fn.has(tbl, str)
end

fn.select = function(tbl, prop, ...)
	local keycache
	local check if type(prop) == 'function' then
		check = prop
		keycache = ...
	else
		local val val, keycache = ...
		check = function(ent) return ent[prop] == val end
	end
	for k,v in pairs(tbl) do
		if (not keycache) or (not keycache[k]) then -- help avoid expensive selectors
			if check(v,k) then
				if keycache then keycache[k] = true end
				return v, k
			end
		end
	end
end

fn.setOrD = function(set, a, ...)
	set[a] = true
	if select('#', ...) == 0 then return set end
	fn.setOrD(set, ...)
end

fn.setAndD = function(set, ...)
	local t = {}
	local function iter(a, ...)
		if set[a] then t = true end
		if select('#', ...) == 0 then return end
		iter(...)
	end
	iter(...)
	return t
end

fn.set = function(...)
	local s = {}
	fn.setOrD(s, ...)
	return s
end


return fn

Added mods/vtlib/tree.lua version [accf8b0fa3].























































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local lib = ...
local tree = {}

local let let = lib.class {
	__index = {
		get = function(self, v)
			if self.bind[v] ~= nil then
				return self.bind[v]
			elseif self.import ~= nil then
				for i=#self.import, 1, -1 do
					local g = self.import[i]:get(v)
					if g then return g end
				end
			end
			if self.parent ~= nil then
				return self.parent:get(v)
			else return nil end
		end;
		put = function(self, v, val)
			self.bind[v] = val
		end;
		unify = function(self, v, val)
			local x = self:get(v)
			if     x == nil then self:put(v, val)
		                        return true
			elseif x == val then return true
			else                 return false end
		end;
		newBinds = function(self)
			return pairs(self.bind)
		end;
		binds = function(self)
			local k
			return function()
				k = next(self.bind, k)
				if k == nil then
					self = self.parent
					if not self then return nil end
					k = next(self.bind, k)
				end
				return k, self.bind[k]
			end
		end;
		compile = function(self, into)
			-- assemble the entire tree into a single unified list,
			-- suitable for returning from a completed query. uses
			-- tail recursion for optimal performance
			into = into or {}
			for k,v in pairs(self.bind) do
				into[k]=into[k] or v
			end
			if self.parent then -- branch node
				return self.parent:compile(into)
			else -- root node in let tree
				return into
			end
		end;
		branch = function(self, ref) return let(self,ref) end;
		import = function(i)
			if self.import == nil then self.import = {i}
			else table.insert(self.import, i) end
		end;
	};
	construct = function(parent, ref)
		return {
			bind = {};
			parent = parent;
			import = nil;
			ref = ref; -- used to store e.g. a position in a program
		}
	end;
}
tree.let = let

return tree

Added mods/vtlib/ui.lua version [121fdb8a0d].





































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local l = ...

return {
	form = l.class {
		name = 'form';
		__index = {
			nl = function(self, h)
				h = h or 0
				self.curs.x = 0
				self.curs.y = math.max(h, self.curs.y + self.curs.maxh)
				self.curs.maxh = 0
			end;
			attach = function(self, elt, x, y, w, h, ...)
				local content = ''
				if self.width - self.curs.x < w then self:nl() end
				for _, v in pairs{...} do
					content = content .. ';' .. minetest.formspec_escape(tostring(v))
				end
				self.src = self.src .. string.format('%s[%f,%f;%f,%f%s]', elt, x,y, w,h, content)
				if h > self.curs.maxh then self.curs.maxh = h end
			end;
			add = function(self, elt, w, h, ...)
				local ax, ay = self.curs.x, self.curs.y
				self:attach(elt, ax,ay, w,h, ...)
				self.curs.x = self.curs.x + w + self.pad
				if self.curs.x > self.width then self:nl() end
				return ax, ay
			end;
			render = function(self)
				return string.format("size[%f,%f]%s", self.width, self.curs.y + self.pad + self.curs.maxh, self.src)
			end;
		};
		__tostring = function(self) return self:render() end;
		construct = function()
			return {
				src = "";
				width = 8;
				pad = 0;
				curs = {x = 0, y = 0, maxh = 0};
			}
		end;
	};

	tooltipper = function(dui)
		-- takes a configuration table mapping affinities to colors.
		-- 'neutral' is the only required affinity
		return function(a)
			local color = a.color and a.color:readable(0.65, 1.0)
			if color == nil then color = l.color(136,158,177) end
			local str = a.title
			if a.desc then
				str = str .. '\n' .. color:fmt(minetest.wrap_text(a.desc,60))
			end
			if a.props then
				-- str = str .. '\n'
				for _,prop in pairs(a.props) do
					local c
					if prop.color and l.color.id(prop.color) then
						c = prop.color:readable(0.6, 1.0)
					elseif dui.colors[prop.affinity] then
						c = l.color(dui.colors[prop.affinity])
					else
						c = l.color(dui.colors.neutral)
					end

					str = str .. '\n ' .. c:fmt('* ')

					if prop.title then
						str = str .. c:brighten(1.2):fmt(prop.title) .. ': '
					end

					local lines = minetest.wrap_text(prop.desc, 55, true)
					str = str .. c:fmt(lines[1])
					for i=2,#lines do
						str = str .. '\n' .. string.rep(' ',5) .. c:fmt(lines[i])
					end
				end
			end
			return color:darken(0.8):bg(str)
		end;
	end;
}

Added mods/vtlib/vtlib.ct version [088cd62f90].

















>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
# vtlib
this is a standalone library of critical utility functions i, velartrill, would never want to write a game or mod without. vtlib can be renamed; it will register itself in a global matching its mod name. it is assumed you will be using it under the name of vtlib for the purposes of documentation

## primitive munging

### vtlib.math
### vtlib.str
### vtlib.tbl

Added settingtypes.txt version [ba8cba9cb8].













































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[starsoul]

[*genesis]
# scenario used for new games. this is a horrible hack used until i
# can figure out some way to get a proper start-game menu in
starsoulPlayerScenario (player scenario) enum imperialExpat imperialExpat,gentlemanAdventurer,terroristTagalong,tradebirdBodyguard

[*multiplayer]
# use the fatigue mechanic as an antipoopsocking measure, requiring
# players to go to bed (and presumably log out) in order to recover
# wakefulness. when disabled, an alternate fatigue mechanic is used
# where players slowly recover wakefulness during prolonged periods
# of rest (zero stamina consumption).
starsoulMultiplayersMustSleep (require sleep) bool true

# when sleep is enabled, require players to actually right-click on
# their bed before logging out in order to obtain sleep credit.
# otherwise, fatigue will decrease whenever players are simply
# logged out.
#
# Requires: starsoulMultiplayersMustSleep
starsoulMultiplayersNeedBed (sleep requires bed) bool true

Added src/lore.ct version [8a0d33ccae].









































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
# starsoul lore
! spoilers ahoy!

## Thinking Few
the Galaxy teems with life, but only one in a trillion of its creatures is fully sophont, with a soul and mentality of their own.

### Lesser Races
the majority of the Thinking Few are held in thrall to the Starsouled, their null psionic potential locking them out of the higher levels of civilizational power and attainment.

#### Humans
The weeds of the galactic flowerbed. Humans are one of the Lesser Races, excluded from the ranks of the Greater Races by souls that lack, in normal circumstances, external psionic channels. Their mastery of the universe cut unexpectedly short, forever locked out of FTL travel, short-lived without augments, and alternately pitied or scorned by the lowest of the low, humans flourish nonetheless due to a capacity for adaptation all but unmatched among the Thinking Few, terrifyingly rapid reproductive cycles -- and a keen facility for bribery. While the lack of human psions remains a sensitive topic, humans (unlike the bitter and emotional Kruthandi) are practical enough to hire the talent they cannot possess, and have even built a small number of symbiotic civilizations with the more indulging of the Powers. In a galaxy where nearly all sophont life is specialized to a fault, humans have found the unique niche of occupying no particular niche.

#### Kruthandi
The Kruthandi are a race of four-armed marsupialoids, with the rough body proportions of meerkats, though much larger.

Geographically, they occupy a small net of systems linked only through their home system. Unable to accept the reality that their lack of psionics doomed them to a subservient, sublight role, the Kruthandi indulged in a brief, petulant, and entirely futile war with a Su'ikuri state[^lizard-war], and then retreated to their home web to sulk. Access to their space is tightly controlled, and psionic races are absolutely barred from their worlds, even the mere Greater Races, tho this is hilariously unenforcible. In practice they are particularly vulnerable to psicrime – when you ban psionics, only criminals will have mind powers. The Kruthandi are generally viewed with pity and amusement as a pathetic basket case of a civilization, and engage in little intercourse with the broader galaxy.

	lizard-war: The Su'ikuri sovereign in question was of a decidedly philosophical bent and was commendably gentle dealing with the upstarts, seeming more bemused than angered by the attack. He took a few of their leaders as battle trophies and spanked the remainder out of his system. There were no casualties on either side.

However, the Kruthandi are an ancient and sophisticated civilization, and there is much more to their culture than mere wallowing in victimhood (though this certainly has a special place in the Kruthandi heart). Their hate-crush on the Starsouled Races has lead to an utter obsession with metric technology, and the Kruthandi have, by sheer brute force and fanatic tenacity, built a surprisingly sophisticated gravitics industry. While this makes little economic sense, it soothes the Kruthandic psyche.

#### Qir
a race of religious fanatics cleaving to no particular faith, Qir enthusiastically adopt and syncretize new religions as fast as Heaven can churn out prophets. their fanaticism seems to be a cultural evolution to compensate for exceptionally weak mememmune systems; unable to properly critique new ideas, the Qir need to outsource much of their reasoning to systems of sacred commandments, ideally those developed by species who know how to deal with memes. good old-fashioned natural selection does the rest.

### Greater Races
a handful of species have souls that are just barely capable of developing external psionic channels. the respected vassals of the Starsouled, they can touch the spirits of others, sending messages and reading thoughtforms -- or attacking with torturous sensations and ruinous emotion. however, their power does not extend to the physical universe; they rely entirely on their Starsouled masters for access to faster-than-light travel.

### Starsouled
the unquestioned lords of space and time, the Starsouled Races are those lucky few to have evolved cognitive architectures that allow a soul to reach its full development potential, progressing from merely cogitating about the universe to manipulating it directly with innate power. seemingly as a consequence of the necessary neural architecture, the Starsouled are all ferociously intelligent -- IQs below 150 are unheard of. however, they are marred by a proportional tendency toward mental instability and psychosis.

their civilizations are known as the Powers.

#### Su'ikuri
(sg. Su'ikutra, adj. Su'ikuruk)
a reptilian race of artists, aesthetes, hedonists, monks, and philosophers, the Su'ikuri are an idle, contemplative, and aristocratic people whose massive psionic sophistication numbers them among the Powers -- much to the annoyance of the Eluthrai. as any adult has the requisite level of finesse and raw power to tweak individual alleles throughout the whole body of a living organism, the Su'ikuri are a race of peerless organgineers. they eschew "dead" hylotechnology, and insist on using biotech wherever remotely practicable.

their 'spacecraft' are massive tree-like organisms housing whole ecosystems, propelled and protected against radiation by the psionic power of their crew. sometimes they are equipped with technology produced by a vassal race, but only when unavoidable. 

Su'ikuri generally use Lesser Races for manual labor, and Greater Races to overseer these laborers. whether these are paid and respected laborers or outright slaves depends entirely on the ethos of the local civilization.

Su'ikuruk society is strictly feudal, with a hierarchy based on psionic skill and wit. virtually all conflict is resolved with either a polite, prolonged philosophical debate (the Su'ikuruk version of a duel) or a brute psionic struggle -- the party overpowered by the greater psion is compelled to submit totally, and may achieve freedom only by strengthening their soul to the point of being able to overpower their former superior. even other members of other Starsouled Races can wind up enslaved this way.

Su'ikuri relations with the Eluthrai are, as a rule, extremely strained, and many small but high-energy wars have been fought between the Su'ikuruk Powers and the Corcordance.

a motivated and talented Su'ikutra can reach astropathic levels of psionic power with only a century of practice, something otherwise unheard of among Starsouls.

#### Usukwinya
(sg. Usukwinti, adj. Usuk)

the Usukwinya, known affectionately as the "Tradebirds", are a psionic avian race. their adults range in height from 1 to 1.3m, and 20-30kg in weight. they lack precise manipulatory appendages and are physically weak, forcing them to rely heavily on their psionics for everyday dasks. however, they remain fully capable of flight even without psionic assistance.

culturally, the Usukwinya are a mercantile race. they exert their power not through physical force, but through obscene wealth, garnered by selling their painstakingly [^drm value-engineered] technologies to the highest bidder. some rare few Usukwinya will also rent out their psionics, even to the Lesser Races (if they can afford their prodigious fees).
	drm: Usukwinya DRM is some of the most powerful in the Reach.

the enthusiastic capitalism of the Usukwinya is tempered by a hardwired loyalty drive so powerful that before First Contact they had no concept of contract law. they are also noteworthy for having never fought a war among themselves, and seem utterly unwilling to resort to force unless physically provoked. their governments all work diligently to maintain peace among the other races, and the somewhat absurd spectacle of a Starsouled diplomat gently negotiating with two hysterical Lesser ambassadors tends to crop up when two factions the Usukwinya have good relationships with threaten war on one another. (the Eluthrai find this patently ridiculous, and prefer to maintain peace with a judicious application of preventive violence. several Usukwinya-organized peace conferences have dissolved when the Eluthrai summarily shattered the offending governments without a note of forewarning.)

their willingness to trade with or work for anyone and everyone mean that the Usukwinya are the main reason the Lesser Races have any ability to travel beyond the Great Web. however, Usuk astropaths are very selective: they will not use their powers to help their employers to commit acts of aggression, no matter how much you offer to pay them. many human captains chafe under the restrictions of their Tradebird astropaths, but short of relativistic travel, they have no other way to escape the confines of the Web.

Usukwinya get along with everyone and make excellent diplomats, so long as they can restrain their urge to make a quick profit at the first available opportunity.

#### Eluthrai
(sg. Eluthra)

the greatest and most aloof of the living Starsouled races, the Eluthrai are a race of psionic warrior-poets. they are slim humanoids with subtly iridescent dark grey skin, lustrous white hair, red~violet eyes, and tapered, expressive ears. they are very few in number, with no more than ten thousand Eluthrai in the entire galaxy. it is popularly said, not without some reason, that the only reason the Eluthrai haven't conquered that entire galaxy is because they don't care to.

natural immortals with a very low reproduction rate, the Eluthrai all have an exceptionally long-term worldview that frequently confounds mortal morals. little about them is known for certain, and they interact with the non-Starsouled very rarely, usually to deliver some form of unforeseen intervention that they typically refuse to explain.

the Eluthrai see themselves as the masters of the Thinking Few, and spare no expense in ensuring they maintain their position. to them, the Great Web is a garden, a place  to be tended carefully and protected from the storms outside. their civilization is dedicated to combatting extra-Web threats -- in particular, guarding against the possible return of the Forevanishers. they have cultivated a strong & highly spiritual warrior ethos in consequence

within the Web itself, they mostly by clandestine means, using "Agents" selected from the Greater (and, occasionally, Lesser) Races to act on their behalf. in general they act directly only when overwhelming force is required, such as to exclude the Kuradoqshe, or to excise Suldibrand.

it is known that the Eluthrai are of great intelligence: a 200pt IQ makes you a laughable simpleton in their eyes. it is estimated that the average individual has an IQ of 290, close to the theoretical maximum where organized intelligence dissolves into a sea of blinding psychosis. consequently, they are very conservative and cautious of new ideas; their culture emphasises skepticism and avoiding rash action.

early Eluthran history was extremely warlike, and they could have easily devastated the whole of the Reach in their fanatical pursuit of competing ideologies. however, a philosophical tradition emerged from the rubble of a particularly ruinous exchange that offered the correct tools for neutering the more dangerous aspects of their intelligence -- after the centuries proved its value, the Philosophers exterminated all the remaining Eluthrai who had not adopted their practices. it was a coldblooded but rational act of genocide: an individual Eluthra is intelligent enough to bootstrap an industrial civilization from first principles with a few years of effort. an entire civilization of them, devoid of self-control? that wasn't merely a threat to the Philosophers; it was a threat to the Galaxy entire.

the Eluthrai have a single common language, Eluthric, which they use in interstellar discourse and in the sciences. however, the different far-flung colonies have their own individual tongues as well. Eluthric has the largest vocabulary of any known language, with over twenty million words. an Eluthra who hasn't learned at least a million of them by adolescence is deemed slow.

they have developed very slowly since the Philosophers came to power, but were already so advanced that nobody is expected to exceed them any time soon.

Eluthran civilization is united under the rule of the Philosopher-King, an enlightened despot with unrestricted power, in a complex web of fealty and patronage collectively named the Corcordance of the Eluthrai. while the First Philosopher died tens of thousands of years ago, he had the foresight to prepare a successor to take his place in case of his assassination or ill-fortune. in all those years, power has changed hands only three times. the current Philosopher-King has ruled for eight thousand years.

Eluthrai have two genders, and dramatic dimorphism. their women are much more intelligent than their men, and proportionately more prone to psychosis. traditionally most of their societies were matriarchal -- with the brains and psionic brawn to overpower the males, there was very little that could keep the Clan-Queens from exerting their will. the First Philosopher recognized however that the lesser intelligence of men was useful, due to their stabler psyches, and proposed patriarchy as part of his solution. this was made possible through a previously obscure psionic technique known as quelling -- with enough intimate exposure to the soul of another, it becomes possible to negate their psionics, even if that psion is stronger.

among the modern Concordant Eluthrai, a female's mate is expected to be capable of quelling her psionics. female Eluthrai generally cooperate with the practice; it is difficult to learn to quell someone who actively tries to stymie you. it is widely understood, however, that the female sex will only cooperate so long as their men rule wisely: in a celebrated case on a far-flung world where the men began to take too many liberties, the women carefully organized to overpower one another's mates and instituted a compensatory subjugation of the local males for a proportionate period, which the Philosopher-King himself agreed was the just and proper punishment.

Eluthran technology can be tidily summarized as "uncompromising." the Eluthrai demand excellence from their machines as much as one another, and will happily incur absurd expense to eliminate the smallest flaw. languishing for thousands of years of under such attentions, abetted by the most ferocious living intelligence to be found in the Reach, has created a technological ecosystem that is succeeded in its phenomenal capabilities only by its preposterous expense. an Eluthran computer requires about ten times the time and a hundred times the energy input to fabricate as does a conventional human computer, despite the vast gulf in manufacturing capabilities, but you can be [!damn] sure it'll still be working in ten million years' time.

they enjoy a post-scarcity economy that is the envy of even the other Starsouled.

very few of the Greater Races, and vanishingly few of the Lesser Races, have ever had the opportunity to visit an Eluthran world. they admit only their mysterious Agents and the occasional individual subjected to penal servitude for some great crime against the interests of "the Garden".

while even their females are not nearly the psionic match of the Su'ikuri, they are nonetheless vastly powerful. their psionics are not as seamlessly integrated into their nervous system as in Usuk neurology, and deploying their power is consequently more effortful, requiring some concentration and intent, but they can bring far more energy to bear. where the Usukwinya have finesse and the Su'ikuri have brute power, the Eluthrai have technique: they can do things with their psionics that the other races never would have imagined possible.

the Eluthrai put a great deal of effort into foremodeling the universe, seeking to predict future events and trends. their models are far from infallible, but reliable enough that some supersitious Lessers have come to believe that Eluthran psionics can be used to see the future. intelligence-gathering is in the modern era the prime industry of that exalted race, second only to warfare.

### Forevanished Ones

#### Forevanishers
a mysterious race or power thought to be responsible for exterminating a number of Forevanished Ones. no one knows for sure that they have themselves Vanished -- for all we know, they could be one of the contemporary Powers…

#### firstborn
the architects of the Great Web, the Firstborn were the first civilization of which traces remain within the Reach. their psionic and scientific mastery, developed over ten million years of energetic industry, reached levels even the greatest of the modern Starsouled Races cannot hope to equal. while their Continuum Bridges form the backbone of the Lesser civilizations, little else of their manufacture seems to have survived into the present era.

practically every trace of their existence that does remain is scored with weaponsfire.

the artifact which tore an external channel into the player's soul in the backstory is of Firstborn design and uncertain purpose. Commune scholars had hitherto ascertained only that it was a machine seemingly able to produce psionic effects -- something that [!should] have been a contradiction in terms.

! possible plot: the Firstborn devised a means to produce psionic effects with carefully cultured neurons embedded in a mechanical matrix. essentially creating slaved psionic AI dedicated to a single purpose. while these rudimentary consciousnesses, barely fit to called souls, did not suffer, some other race or perhaps a faction among the Firstborn seems to have taken exception to the practice of trapping souls in metal, outside the thread of reincarnation, and exterminated the civilization to prevent its heresy.
! if this was a real proper AAA game the player would face some epic choice to release the secret and free the Lesser Races (or a subset of them) from the dominion of the Starsouled, turning psionics into a mere commodity; keeping the secret but placing her power at the disposal of the Commune (or Empire, in return for elevation to the ranks of nobility); or joining the Eluthrai as an honorary citizen in recompense for keeping the secret. alas, i don't have a budget.

## psionics
the ability of the soul to extend its will beyond the confines of its substrate. this power is technically defined as the presence of one or more external psionic channels in the structure of the soul. such a channel allows the soul to direct excess numina into other souls or into the numon field of the physical universe.

the delicate interlink between soul and body relies on quantum phenomena, and only carbon-based life seems able to maintain such a link. silicon-based intelligence is at most a simulacrum of true thought.

### farspeakers
the most powerful psions among the Greater Races, the Farspeakers can extend their mentality across light-years of space, providing FTL comms. such long-range comms require a bonded psion on the receiving end, and cannot feasible reach much beyond a parsec; consequently, long-range FTL communication requires complex networks of bonding.

### astropaths
the greatest -- and rarest -- of all Starsouls, astropaths can, with technological assistance, manipulate and direct so much numina that they can perform metric conjunction across light-years of distance. a single astropath usually requires a week or more to recover from a fold and regain enough power to perform another, though the greatest Su'ikuri astropaths require only a few days.

## culture
the Galaxy is an intensely cosmopolitan place. while a few of the Lesser Races keep to themselves, most intermix to varying degrees. this is largely necessary due to the intense specialization of most races. there are some tasks a Qir cannot undertake without the help (however grudging) of a Kruthandi, and vice-versa. before humans burst onto the scene, rescued by the Qir from a state of savagery on a long-lost Webworld, most scientific advances and engineering breakthrough were a result of either Eluthran super-science or scholars of multiple races working together.

### faith
faith is at once fantastically diverse and largely uniform. certain ancient customs are universally acknowledged: even the barbarian humans remembered the Wild Gods (and had fallen so low as to begin worshipping them, in hope of appeasing them and forestalling their wrath). there are two broad "schools" of faith: one, by far the most common, is syncretic and idiosyncratic, with the believer mixing and matching customs, notions, and gods from wherever they might travel, sincerely if awkwardly celebrating alien festivities as their own, and generally Doing In Rome. the other is the religious school: the undertaking of a singular exclusive religio-philosophical system, and reserving one's devotion exclusively to that. the Religious, as opposed to the Faithful, are often scorned for denying the Gods of the Faith, though in truth most are henotheistic in their approach.

an important distinction is that between the Gods of the Faith and the Wild Gods. the former are the subjects of both organized and disorganized religion, worshipped and plied with incense and sacrifices. the Gods of the Faith are the civilized divinities, who first bestowed thought and reason upon the Thinking Few, and it was only by their grace that we rose from the dream of subsophonce. they are tutelary powers, often of a particular place or science, and they are deeply involved in the affairs and fates of Men. they are the Gods of the Harvest, of War and of Peace, of Computation and Cybernetics. they are beloved, oft invoked, and much celebrated.

the Wild Gods, by contrast, are the animin powers, the temperamental personifications of nature in its varied aspects. these are the Gods of the Storm, of the Sun, of Lust and Beauty, of Life Itself, of Gravity & the Weak Electric Force. they are the far greater of the divinities, powers incomprehensible even the Gods of the Faith, and their whim is utterly unpredictable. there is no point to the organized worship of them; indeed, one turns only to the Wild Gods in the lowest of desperation, undertaking great acts of sacrifice in the hope of attracting their attention and mercy for the briefest of moments in which to regroup. rather, they are objects of awe, inspiration, and terror. one builds shrines to the Gods of the Faith in hopes of attracting their blessing; one builds shrines to the Wild Gods to ward away their temper. they are invoked only in moments of rage, terror, passion, or sublime awe. to seek connection with them, as some rare few do -- mostly artists and weary, tortured souls -- is a spiritual endeavor, not a religious one, the undertaking of oblivion and dissolution, to lose oneself in the depths of power and passion, and dance among the storm. there is nothing in the universe so powerful, so dangerous, majestic, or beautiful as the Wild Gods: they are truly beyond all knowing.

it is of note that atheism is a singularly scorned and rare tendency. even the Eluthrai honor the Gods, even if the Lesser Races might be inclined to account them on the same level.

Added src/sem.ct version [5017b1c243].















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# starsoul semantics

## tool levels
some items have a particular level requirement to enable digging. in general, level 0 should be used for things that can be dug by hand
nanotech can dismantle anything up to level 2

* sediment
** 0: sand, dirt (diggable with hand)
* stone
** 0: loose rock (hands)
** 1: brittle minerals (pick)
** 2: hard minerals (jackhammer)
** 3: diamond
* metal
** 1: solid metal
** 2: solid titanium
** 3: solid osmiridium
** 4: solid diamond
** 5: unobtanium
* plant
** 0: leaves, twigs
** 1: branches

Added src/sfx/base.orc version [16d6dd4e05].











































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
; [ʞ] base.orc
;  ~ lexi hale <lexi@hale.su>
;  🄯 CC-NC-BY-SA 3.0
;  ? useful opcodes and defs

#ifndef BASE_INC
#define BASE_INC #1#

opcode fade, a, iiii
	isdur, isvol, ifin, ifout xin
	afade bpf linseg:a(0,isdur,1), 0,0, ifin,isvol, ifout,isvol, 1,0
	xout afade
endop

opcode pulse, a, ii
	idur,iwhen xin
	ap bpf linseg:a(0,idur,1), 0,0, iwhen,1, 1,0
	xout ap
endop

#end

Added src/sfx/conf.lua version [7fad9bea38].







































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
local function find(args)
	local cmd = string.format('find . -maxdepth 1 %s -printf "%%f\\0"', args)
	local ls = io.popen(cmd, 'r')
	local v = ls:read 'a'
	ls:close()
	local names = {}
	for n in v:gmatch '([^\0]+)' do
		table.insert(names, n)
	end
	return names
end

local polyfx = find '-name "*.n.csd"'
local fx = find '-name "*.csd" -not -name "*.n.csd"'


local function envarg(n, default)
	local v = os.getenv(n)
	if v then return tonumber(v) end
	return default
end

local baseSeed = envarg('starsoul_sfx_vary', 420691917)
local variants = envarg('starsoul_sfx_variants', 4)
local fmt = os.getenv 'starsoul_sfx_fmt' or 'wav'

local rules = {}
local all = {}
if fmt ~= 'ogg' then table.insert(rules,
	'out/starsoul-%.ogg: %.' .. fmt .. '\n' ..
		'\tffmpeg -y -i "$<" "$@"\n')
end
local function rule(out, file, seed)
	if fmt == 'ogg' then
		table.insert(rules, string.format(
			'out/starsoul-%s.ogg: %s.csd digital.orc physical.orc psi.orc dqual.inc\n' ..
			'\tcsound --omacro:seed=%d --format=ogg -o "$@" "$<"\n',
			out, file,
			seed
		))
	else
		table.insert(rules, string.format(
			'%s.%s: %s.csd digital.orc physical.orc psi.orc dqual.inc\n' ..
			'\tcsound --omacro:seed=%d --format=%s -o "$@" "$<"\n',
			out, fmt, file,
			seed, fmt
		))
	end
	table.insert(all, string.format('out/starsoul-%s.ogg', out))
end
for _, v in ipairs(polyfx) do
	local bn = v:match '^(.+).n.csd$'
	for i=1, variants do
		rule(bn .. '.' .. tostring(i), bn .. '.n', baseSeed + 4096*i)
	end
end
for _, v in ipairs(fx) do
	local bn = v:match '^(.+).csd$'
	rule(bn, bn, baseSeed)
end

local makefile = io.open('makefile', 'w')
makefile:write('all: ', table.concat(all, ' '), '\n')
makefile:write('clean:\n\trm ', '"'..table.concat(all, '" "') .. '"', '\n\n')

makefile:write(table.concat(rules, '\n'))
makefile:close()

Added src/sfx/configure.csd version [19a31ab4e8].







































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
; [ʞ] configure.csd
;  ~ lexi hale <lexi@hale.su>
;  🄯 CC-NC-BY-SA 3.0
;  ? sound effect for reconfiguring your suit,
;    e.g. turning on power save

<CsoundSynthesizer>

<CsInstruments>
#include "dqual.inc"
#include "digital.orc"
</CsInstruments>

<CsScore>
i"chirp"  0.00 0.10 0.20  20 1600
i"chirp"  +    0.10 0.20  20 1300
</CsScore>

</CsoundSynthesizer>

Added src/sfx/digital.orc version [62769fca6a].











































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
; [ʞ] digital.orc
;  ~ lexi hale <lexi@hale.su>
;  🄯 CC-NC-BY-SA 3.0
;  ? chirpy beepy blarpy noises for UI and computer
;    sound effects.

; conventions
; - all waveforms start with aw*
; useful idioms:
;  - current time factor: linseg(0,p3,1)
; std parameters
;   p2 = start time
;   p3 = duration
;   p4 = amp

#include "base.orc"

instr rumble
	aton jspline 1, 100,500
	;amp adsr 0.2,0.4,.9, 0.2
	kamp bpf linseg(0,p3,1), \
		0.0, 0.0, \
		0.1, 0.8, \
		0.5, 1.0, \
		0.7, 0.8, \
		1.0, 0.0

	aw poscil p4*kamp, 200 + 100*aton
	out aw
endin

instr spindown
	avol fade p3,p4,0.5,0.9
	kpr linseg 0, p3, 1.0
	; remap time so our spindown starts slowly but rapidly
	; speeds up as the effect progresses
	at bpf a(kpr), 0,0, 0.6,0.2, 1,1

	; noise component
	al = .3 + a(unirand:k(.7))
	af bpf at, 0, 700, .5, 400, 1,200
	aw poscil al*avol, af

	out aw
endin

instr blarp
	apr linseg 0,p3,1
	avol bpf apr, 0,0,  0.3,1,  1,0
	
	aff poscil 1.0, 10
	afr bpf aff, \
		0.0, 400, \
		0.2, 600, \
		0.5, 500, \
		0.6, 700, \
		0.9, 300, \
		1.0, 400
	aw poscil avol*p4, afr
	out aw
endin

instr chirp
	abeep poscil 1, p5
	abeep bpf abeep, \ ; apply chirp envelope
		0.0, 0.0, \
		0.4, 0.1, \
		0.6, 0.9, \
		1.0, 1.0
	aw poscil abeep*p4, p6
	out aw
endin

opcode warbulator, kkk, kkk
	kfreq, kfi, kfo xin

	kt1 init 0
	kt2 init 0
	kvol poscil 1, kfreq
	kvol bpf kvol, kfi,0, kfo,1
	if kvol == 0 then
		kt1 = unirand:k(1.0)
		kt2 = unirand:k(1.0)
	endif

	xout kvol, kt1, kt2
endop

instr warble
	afade fade p3,p4, 0.1,0.9

	kvol, kton, k_ warbulator p5, p6,p7
	kton bpf kton, 0,p8, 1,p9

	aw poscil afade * a(kvol), kton
	out aw
endin

instr warple
	afade fade p3,p4, 0.2,0.8
	ap pulse p3,.5
	kv, kwb, kwv warbulator p5, p6, p7

	atn  bpf ap, 0,p8, 1,p9
	ktno = kwb * p10
	av = a(kv) * bpf:a(a(kwv), 0,.2, 0.9,0.3, 1,1)
	aw poscil afade*av, atn+a(ktno)
	out aw

endin

instr wslope
	avol fade p3,p4,0.3,0.8

	afb bpf linseg:a(0,p3,1), 0,p7, 0.5,p8, 1,p9
	afw poscil 1.0, p5
	afw bpf afw, 0,0, 0.3,0.1, 0.7,0.8, 1,1
	afreq = afb - (afw*p6)

	aw poscil avol, afreq
	out aw
endin

instr winddown
	avol fade p3,p4,0.1,0.5

	afmf bpf linseg:a(0,p3,1), 0,100, 0.2,30, 1,1
	afm poscil 1.0, afmf

	afn bpf linseg:a(0,p3,1), 0,1200, 0.5,1000, 0,700

	aw poscil avol, afn - afm*400
	out aw
endin

instr blare
	avol fade p3,p4,0.03,0.9
	an poscil 1.0, 30
	an bpf an, 0,0, 0.9,0, 1,1
	ar bpf linseg:a(0,p3,1), 0,100, .5,300, 1,400
	aw poscil avol, 500 + (an*ar)
	out aw
endin

instr tune
	anois unirand 1
	aw poscil p4, anois * 1000
	out aw
endin

Added src/sfx/dqual.inc version [bc96869fb5].























>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
; [ʞ] dqual.inc [vim: ft=csound]
;  ~ lexi hale <lexi@hale.su>
;  🄯 public domain / CC0
;  ? default quality settings for Starsoul sfx when
;    not overridden in the makefile variables

sr     = 44100
ksmps  = 64
nchnls = 1
0dbfs  = 1
seed     $seed

Added src/sfx/error.csd version [e38089a4eb].







































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
; [ʞ] error.csd
;  ~ lexi hale <lexi@hale.su>
;  🄯 CC-NC-BY-SA 3.0
;  ? angry blarp fer when ya done a bad

<CsoundSynthesizer>

<CsInstruments>
#include "dqual.inc"
#include "digital.orc"
</CsInstruments>

<CsScore>
i"blare" 0.00 0.10 0.4
i"blare" 0.15 0.30 0.6
e 0.5
</CsScore>

</CsoundSynthesizer>

Added src/sfx/insert-snap.csd version [e8b0f78a06].



































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
; [ʞ] insert-snap.csd
;  ~ lexi hale <lexi@hale.su>
;  🄯 CC-NC-BY-SA 3.0
;  ? snappy noise for slappin a thing into a slot

<CsoundSynthesizer>

<CsInstruments>
#include "dqual.inc"
#include "physical.orc"
</CsInstruments>

<CsScore>
i"snap"   0.00 0.5 0.20
</CsScore>

</CsoundSynthesizer>

Added src/sfx/make.sh version [96e0bc70ee].















>
>
>
>
>
>
>
1
2
3
4
5
6
7
csfmt=wav
baseSeed=420691917
iters=${iters:-7}

for mc in *.n.csd; do

done

Added src/sfx/mode-nano.csd version [e372209146].













































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
; [ʞ] mode-nano.csd
;  ~ lexi hale <lexi@hale.su>
;  🄯 CC-NC-BY-SA 3.0
;  ? sound effect for activating your nanides

<CsoundSynthesizer>

<CsInstruments>
#include "dqual.inc"
#include "digital.orc"
</CsInstruments>

<CsScore>
; common mode switch header
i"chirp"  0.00 0.10 0.10  20 1400
i"chirp"  +    0.20 0.05  10 1100

; mode-specific score
i"wslope" 0.20 0.7 0.3  15 200  600 750 900
</CsScore>

</CsoundSynthesizer>

Added src/sfx/mode-off.csd version [aaccae581c].







































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
; [ʞ] mode-nano.csd
;  ~ lexi hale <lexi@hale.su>
;  🄯 CC-NC-BY-SA 3.0
;  ? sound effect for activating your nanides

<CsoundSynthesizer>

<CsInstruments>
#include "dqual.inc"
#include "digital.orc"
</CsInstruments>

<CsScore>
; inverse common mode switch header
i"chirp"  0.00 0.10 0.10  20 1100
i"chirp"  +    0.20 0.05  10 1400
</CsScore>

</CsoundSynthesizer>

Added src/sfx/mode-psi.n.csd version [a13b5dd954].

















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
; [ʞ] mode-psi.csd
;  ~ lexi hale <lexi@hale.su>
;  🄯 CC-NC-BY-SA 3.0
;  ? sound effect for activating your
;    t e l e p a t h i c  m i n d  p o w e r s

<CsoundSynthesizer>

<CsInstruments>
#include "dqual.inc"
#include "digital.orc"
#include "psi.orc"
</CsInstruments>

<CsScore>
; common mode switch header
i"chirp"  0.00 0.10 0.10  20 1400
i"chirp"  +    0.20 0.05  10 1100

; mode-specific score
i"wobble" 0.10 1.0 0.6
</CsScore>

</CsoundSynthesizer>

Added src/sfx/mode-weapon.csd version [c74b6f5c1f].















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
; [ʞ] mode-nano.csd
;  ~ lexi hale <lexi@hale.su>
;  🄯 CC-NC-BY-SA 3.0
;  ? sound effect for activating your weapons

<CsoundSynthesizer>

<CsInstruments>
#include "dqual.inc"
#include "digital.orc"
</CsInstruments>

<CsScore>
; common mode switch header
i"chirp"  0.00 0.10 0.10  20 1400
i"chirp"  +    0.20 0.05  10 1100

; mode-specific score
i"blare" 0.10 0.60 0.3
e 0.8
</CsScore>

</CsoundSynthesizer>

Added src/sfx/nano-heal.csd version [0096bc485f].





































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
; [ʞ] nano-heal.csd
;  ~ lexi hale <lexi@hale.su>
;  🄯 CC-NC-BY-SA 3.0
;  ? sound effect for nanosurgical healing

<CsoundSynthesizer>

<CsInstruments>
#include "dqual.inc"
#include "digital.orc"
</CsInstruments>

<CsScore>
i"wslope" 0.20 0.4 0.15  15 200  400 550 300
e 0.65
</CsScore>

</CsoundSynthesizer>

Added src/sfx/nano-shred.n.csd version [6d16032129].













































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
; [ʞ] nano-shred.csd
;  ~ lexi hale <lexi@hale.su>
;  🄯 CC-NC-BY-SA 3.0
;  ? sound effect for nanoindustrial shredding

<CsoundSynthesizer>

<CsInstruments>
#include "dqual.inc"
#include "digital.orc"
#include "physical.orc"
</CsInstruments>

<CsScore>
//i"keen"     0.00 2.50 0.30   1100 1400 20
i"keen"     0.00 2.50 0.60   900 1000 15
i"shred"    0.00 2.50 0.30
//i"warble"   0.00 2.50 0.03   20 .5 1  1000 2000
i"warple"   0.00 2.50 0.03  20 .6 .8  1000 1500 900
</CsScore>

</CsoundSynthesizer>

Added src/sfx/nav.csd version [56e08bff0b].





































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
; [ʞ] nav.csd
;  ~ lexi hale <lexi@hale.su>
;  🄯 CC-NC-BY-SA 3.0
;  ? navigating UI pages

<CsoundSynthesizer>

<CsInstruments>
#include "dqual.inc"
#include "digital.orc"
</CsInstruments>

<CsScore>
i"chirp"  0.00 0.13 0.10  20 5000
e 0.2
</CsScore>

</CsoundSynthesizer>

Added src/sfx/physical.orc version [44dda97b4d].







































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
; [ʞ] physical.orc
;  ~ lexi hale <lexi@hale.su>
;  🄯 CC-NC-BY-SA 3.0
;  ? rippy stampy slappy noises for meatspace shenanigans


#include "base.orc"

; for inserting items into slots
instr snap
	aw crunch p4, p3, 500, 0.3
	aws stix p4, p3, 200, 0.5
	out aw+aws
endin

instr slide
	anois unirand 1
	avol pulse p3, 0.3
	aw poscil p4*avol, anois * 300
	out aw*p4*avol
endin

;gisaw ftgen 0, 0, 16384, 10, 0, .2, 0, .4, 0, .6, 0, .8, 0, 1, 0, .8, 0, .6, 0, .4, 0,.2

instr zip
	kton bpf linseg:k(0,p3,1), 0,300, 0.7, 600, 1, 1000
	ktf unirand 1.0
	kton = kton - ktf * 350
	aw lfo 1.0, kton, 4
	avol fade p3,p4,0.3,0.9
	out aw*avol
endin

gisine ftgen 0, 0, 16384, 10, 1

instr vrrm
	avol pulse p3, 0.3
	kvib vibr 1, 100, gisine
	aw poscil p4*avol, 300 + (200*kvib)
	out aw
endin

instr flutter
	anois unirand 1
	avol pulse p3, 0.3
	aph phasor 20
	out anois*avol*p4*aph
endin

instr shred_b
	avol fade p3,p4, 0.03, 0.97
	ar rand 1.0
	abf poscil 10, 60
	ap poscil 1, abf
	af bpf ap, 0,p5, 0.5, p6, 1,p7
	av bpf ap, 0,.1, 0.2,1, 0.5,0.6, 1,.1
	aw reson ar, af, af/2
	out aw*avol*av * 0.01
endin

instr shred
	al subinstr "shred_b", p4/2, 200, 300, 1500
	ah subinstr "shred_b", p4/2, 600, 800, 1700
	ahh subinstr "shred_b", p4/2, 800, 1200, 1900
	avvh poscil 1, 10
	avvh bpf avvh, 0,0, 0.5,0, 1,1
	avh poscil 1, 10
	avl poscil 1, 5
	out (al*avl) + (ah*avh) 
endin

instr keen
	an unirand 1.0
	al bpf an, 0,1000, .5,p5, 1,2000
	ah bpf an, 0,1000, .5,p6, 1,2000

	ap poscil 1, p7
	at = (al * ap) + (ah * (1-ap))

	ak poscil p4*0.005, at

	out ak
endin

Added src/sfx/power-down.csd version [7acf6f1a91].









































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
; [ʞ] power-down.csd
;  ~ lexi hale <lexi@hale.su>
;  🄯 CC-NC-BY-SA 3.0
;  ? sound effect for turning off your suit

<CsoundSynthesizer>

<CsInstruments>
#include "dqual.inc"
#include "digital.orc"
</CsInstruments>

<CsScore>
i"chirp"    0.00  0.40  0.20 20 2200
i"chirp"    +     0.20  0.60 10 950
;i"blarp"    ^+.1  0.3   0.60
i"spindown"  0.1  1.00  0.50
</CsScore>

</CsoundSynthesizer>

Added src/sfx/power-up.n.csd version [8839815455].







































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
; [ʞ] power-up.n.csd
;  ~ lexi hale <lexi@hale.su>
;  🄯 CC-NC-BY-SA 3.0
;  ? sound effect for turning on your suit

<CsoundSynthesizer>

<CsInstruments>
#include "dqual.inc"
#include "digital.orc"
</CsInstruments>

<CsScore>
i"warble" 0.00 0.50 0.10  15 .4 .6  400 1200
i"chirp"  0.70 0.20 0.20  20 1500
i"chirp"  +    0.15 0.20  15 1800
</CsScore>

</CsoundSynthesizer>

Added src/sfx/psi.orc version [340e944d7f].



































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
; [ʞ] psi.orc
;  ~ lexi hale <lexi@hale.su>
;  🄯 CC-NC-BY-SA 3.0
;  ? warpy wacky woobly noises for psionics

instr wobble
	at linseg 0,p3,1
	avol bpf at,   \
		0.00, 0.0, \
		0.05, 1.0, \
		0.20, 0.1,
		1.00, 0.0
	aff jspline 1, 40,500
	af bpf aff, 0,400,1,900
	aw poscil avol*p4, af
	out aw
endin

Added src/sfx/success.csd version [9c04e07d3b].





































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
; [ʞ] success.csd
;  ~ lexi hale <lexi@hale.su>
;  🄯 CC-NC-BY-SA 3.0
;  ? glory hallelujah

<CsoundSynthesizer>

<CsInstruments>
#include "dqual.inc"
#include "digital.orc"
</CsInstruments>

<CsScore>
i"chirp"  0.00 0.15 0.05  20 1300
i"chirp"  +    0.25 0.10  20 1600
</CsScore>

</CsoundSynthesizer>

Added src/sfx/suit-battery-in.csd version [c7f6014130].















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
; [ʞ] suit-battery-in.csd
;  ~ lexi hale <lexi@hale.su>
;  🄯 CC-NC-BY-SA 3.0
;  ? sound effect for inserting a battery into your suit

<CsoundSynthesizer>

<CsInstruments>
#include "dqual.inc"
#include "digital.orc"
#include "physical.orc"
</CsInstruments>

<CsScore>
i"snap"   0.00 0.5 0.20
i"chirp"  0.30 0.20 0.03  23 1600
i"chirp"  ^+0.22 0.10 0.07  20 1900
i"chirp"  ^+0.12 0.10 0.10  20 1300
s
e 0.1
</CsScore>

</CsoundSynthesizer>

Added src/sfx/suit-chip-in.csd version [104ee8233e].





































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
; [ʞ] suit-chip-in.csd
;  ~ lexi hale <lexi@hale.su>
;  🄯 CC-NC-BY-SA 3.0
;  ? sound effect for inserting a chip into your suit

<CsoundSynthesizer>

<CsInstruments>
#include "dqual.inc"
#include "digital.orc"
</CsInstruments>

<CsScore>
i"chirp"  0.00 0.10 0.10  40 1600
i"chirp"  0.10 0.80 0.05  20 1400
</CsScore>

</CsoundSynthesizer>

Added src/sfx/suit-don.csd version [3c059ffaa8].







































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
; [ʞ] suit-don.csd
;  ~ lexi hale <lexi@hale.su>
;  🄯 CC-NC-BY-SA 3.0
;  ? strappy slippy zippy noise for pullin on yer spacesuit

<CsoundSynthesizer>

<CsInstruments>
#include "dqual.inc"
#include "physical.orc"
</CsInstruments>

<CsScore>
i"slide"   0.00 0.50 0.60
i"zip"     0.40 0.50 0.03
e 2
</CsScore>

</CsoundSynthesizer>

Added src/sfx/violence.orc version [4a53d43339].





















>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
; [ʞ] violence.orc
;  ~ lexi hale <lexi@hale.su>
;  🄯 CC-NC-BY-SA 3.0
;  ? sound effects for the many exciting
;    ways of doing violence to people

instr shot
	aw crunch p4, p3, 350, 0.95
	out aw
endin

Added starsoul.ct version [81ada79a74].



























































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
# starsoul
[*starsoul] is a sci-fi survival game. you play the survivor of a disaster in space, stranded on a just-barely-habitable world with nothing but your escape pod, your survival suit, and some handy new psionic powers that the accident seems to have unlocked in you. scavenge resources, search alien ruins, and build a base where you can survive indefinitely, but be careful: winter approaches, and your starting suit heater is already struggling to keep you alive in the comparatively warm days of "summer".

## story
about a month ago, you woke up to unexpected good news. your application to join the new Commune colony at Thousand Petal, submitted in a moment of utter existential weariness and almost in jest, was actually accepted. your skillset was a "perfect match" for the budding colony's needs, claimed the Population Control Authority, and you'd earned yourself a free trip to your new home -- on a swanky state transport, no less.

it took a few discreet threats and bribes from Commune diplomats, but after a week of wrangling with the surly Crown Service for Comings & Goings -- whose bureaucrats seemed outright [!offended] that you actually managed to find a way off that hateful rock -- you secured grudging clearance to depart. you celebrated by telling your slackjawed boss exactly what you thought of him in a meeting room packed with his fellow parasites -- and left Circumsolar Megascale with a new appreciation for the value of your labor, as they found themselves desperately scrabbling for a replacement on short notice.

you almost couldn't believe it when the Commune ship -- a sleek, solid piece of engineering whose graceful descent onto the landing pad seemed to sneer at the lurching rattletraps arrayed all around it -- actually showed up. in a daze you handed over your worldly possessions -- all three of them -- to a valet with impeccable manners, and climbed up out of the wagie nightmare into high orbit around your homeworld. the mercenary psion aboard, a preening Usukwinti with her very own luxury suite, tore a bleeding hole in the spacetime metric, and five hundred hopeful souls dove through towards fancied salvation. "sure," you thought to yourself as you slipped into your sleek new nanotech environment suit, itself worth more than the sum total of your earnings on Flame of Unyielding Purification, "life won't be easy -- but damn it, it'll [!mean] something out there."

a free life on the wild frontier with a nation of comrades to have your back, with the best tech humans can make, fresh, clean water that isn't laced with compliance serum, and -- best of all -- never having to worry about paying rent again. it was too good to be true, you mused.

clearly, the terrorists who blew up your ship agreed.

you're still not certain what happened. all you know for sure is that transport was carrying more than just people. in those last hectic moments, you caught a glimpse of something -- maybe machine, maybe artwork, and [!definitely] ancient beyond measure. you've seen artifacts before in museums, of course; in fact, thanks to a childhood fascination, you can still name all the Elder Races and the Forevanished Ones off the top of your head.

you have no [!idea] what that [!thing] was or who in the sublimated [!fuck] could possibly have made it.

but one thing is for certain: your ship wasn't the only thing it ripped open when it blew. because when you woke up in your tiny escape pod beyond the furthest edge of the Reach, circling Farthest Shadow in a suicide orbit, you discovered yourself transformed into something impossible. a contradiction in terms.

a human psionic.

for years beyond counting, the Starsouled species -- three of whom yet live and deign once every so often to notice the Lesser Races -- have held the galaxy in sway through their monopoly on psionic power. of all the Thinking Few, only they are free to wander the distant stars at whim, heedless of the lightspeed barrier. there are no mechanisms for FTL travel or reactionless drive without that innate power, and, they assured us, psionic channels are fixed in the soul. your species either has the power or it doesn't.

[!liars], all of them.

are there other survivors? have they been similarly changed? what was that artifact and who were those terrorists? important questions, all, but they pale in comparison with the most important one:

how the fuck are you going to survive the next 24 hours?

## engine
starsoul is developed against a bleeding-edge version of minetest. it definitely won't work with anything older than v5.7, and ideally you should build directly from master.

starsoul is best used with a patched version of minetest, though it is compatible with vanilla. the recommended patches are:

* [>p11143 11143] - fix third-person view orientation

	p11143: https://github.com/minetest/minetest/pull/11143.diff

### shadows
i was delighted to see dynamic shadows land in minetest, and i hope the implementation will eventually mature. however, as it stands, there are severe issues with shadows that make them essentially incompatible with complex meshes like the Starsouled player character meshes. for the sake of those who don't mind these glitches, Starsoul does enable shadows, but i unfortunately have to recommend that you disable them until the minetest devs get their act together on this feature.

## gameplay
starsoul is somewhat unusual in how it uses the minetest engine. it's a voxel game but not of the minecraft variety.

### controls
summon your Suit Interface by pressing the [*E] / [*Inventory] key. this will allow you to move items around in your inventory, but more importantly, it also allows you select or configure your Interaction Mode.

the top three buttons can be used to select (or deactivate) an Interaction Mode. an Interaction Mode can be configured by pressing the button immediately below. the active Interaction Mode controls the behavior of the mouse buttons when no item is selected in the hotbar.

the modes are:
 * [*Fabrication]: use your suit's onboard nanotech to directly manipulate matter in the world.
 ** [*Left Button] / [*Punch]: activate your primary nano program. by default this activates your nanoshredder, reducing the targeted object to monatomic powder and storing the resulting elements in your suit for use with the Matter Compiler
 ** [*Right Button] / [*Place]: activate your secondary nano program. by default, if your suit compiler can generate sufficiently large objects, creates a block of the configured type directly in the world without having to build it by hand
 * [*Psionics]: wield the awesome, if illicitly obtained, power of mind over matter
 ** [*Left Button] / [*Punch]: perform your selected Primary Power
 ** [*Right Button] / [*Place]: perform your selected Secondary Power
 * [*Weapon]: military-grade suits have built-in hardpoints for specialized weapon systems that draw directly on your suit battery for power (and in the most exotic cases, your psi reserve)
  ** [*Left Button] / [*Punch]: fire your primary weapon
  ** [*Right Button] / [*Place]: fire your offhand weapon / summon your shield

to use a tool, select it in the hotbar. even if an Interaction Mode is active, the tool will take priority. press [*Left Button] / [*Punch] to use the tool on a block; for instance, to break a stone with a jackhammer. to configure a tool or use its secondary functions, if any, press [*Right Button] / [*Place].

hold [*Aux1] to activate your selected Maneuver. by default this is Sprint, which will consume stamina to allow you to run much faster. certain suits offer the Flight ability, which allows slow, mid-range flight. you can also unlock the psionic ability Lift, which allows very rapid flight but consumes psi at a prodigious rate.

you can only have one Maneuver active at a time, whether this is a Psi Maneuver (consuming psi), a Suit Maneuver (consuming battery), or a Body Maneuver (consuming stamina). Maneuvers are activated in their respective panel.

### psionics
there are four types of psionic abilities: Manual, Maneuver, Ritual, and Contextual.

you can assign two Manual abilities at any given time and access them with the mouse buttons in Psionics mode.

you can select a Psi Maneuver in the Psionics panel and activate it by holding [*Aux1].

a Ritual is triggered directly from the psionics menu. as the name implies, these are complex, powerful abilities that require large amounts of Psi and time to meditate before they trigger, and any interruption will cancel the ability (though it will not restore any lost psi). the most famous Ritual is of course Conjoin Metric, which Starsouled astropaths use in conjunction with powerful amplifiers to perform long-distance FTL jumps -- but without centuries of dedication to the art, the best you can hope for if you manage to learn this storied power is to move yourself a few kilometers.

a Contextual ability is triggered in a specific situation, usually by interacting with a certain kind of object. Contextual abilities often require specialized equipment, to the point that many Starsouled practitioners maintain their own Psionics Lab.