1 /*
2  * Copyright (c) 2017-2019 sel-project
3  *
4  * Permission is hereby granted, free of charge, to any person obtaining a copy
5  * of this software and associated documentation files (the "Software"), to deal
6  * in the Software without restriction, including without limitation the rights
7  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8  * copies of the Software, and to permit persons to whom the Software is
9  * furnished to do so, subject to the following conditions:
10  *
11  * The above copyright notice and this permission notice shall be included in all
12  * copies or substantial portions of the Software.
13  *
14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20  * SOFTWARE.
21  *
22  */
23 /**
24  * Copyright: Copyright (c) 2017-2019 sel-project
25  * License: MIT
26  * Authors: Kripth
27  * Source: $(HTTP github.com/sel-project/selery/source/selery/block/solid.d, selery/block/solid.d)
28  */
29 module selery.block.solid;
30 
31 import std.algorithm : canFind, min;
32 import std.conv : to;
33 import std.math : ceil;
34 import std.random : Random, uniform, uniform01, randomShuffle;
35 
36 import selery.about : block_t, item_t, tick_t;
37 import selery.block.block : Update, Remove, Block;
38 import selery.block.blocks : Blocks;
39 import selery.enchantment : Enchantments;
40 import selery.entity.entity : Entity;
41 import selery.entity.projectile : FallingBlock;
42 import selery.item.item : Item;
43 import selery.item.items : Items;
44 import selery.item.slot : Slot;
45 import selery.item.tool : Tools;
46 import selery.math.vector : BlockPosition;
47 import selery.player.player : Player;
48 import selery.world.world : World;
49 
50 static import sul.blocks;
51 
52 enum Facing : ubyte {
53 
54 	upDown = 0 << 4,
55 	eastWest = 1 << 4,
56 	northSouth = 2 << 4,
57 	bark = 3 << 4,
58 
59 	y = upDown,
60 	x = eastWest,
61 	z = northSouth,
62 
63 	south = 0,
64 	west = 1,
65 	north = 2,
66 	east = 3,
67 
68 }
69 
70 /+class MineableBlock(sul.blocks.Block sb, MiningTool miningTool, Drop[] cdrops, Experience exp=Experience.init) : SimpleBlock!(sb) {
71 
72 	static if(cdrops.length) {
73 
74 		private template DropsTuple(Drop[] a, E...) {
75 			static if(a.length) {
76 				alias DropsTuple = DropsTuple!(a[1..$], E, a[0]);
77 			} else {
78 				alias DropsTuple = E;
79 			}
80 		}
81 
82 		// use a typetuple for the drops
83 		alias dropsTuple = DropsTuple!(cdrops[1..$], cdrops[0]);
84 		
85 		public override Slot[] drops(World world, Player player, Item item) {
86 			static if(miningTool.material != Tools.all) {
87 				// validate the tool
88 				if(item is null || item.toolType != miningTool.type || item.toolMaterial != miningTool.material) return [];
89 			}
90 			ubyte[item_t] ret;
91 			foreach(drop ; dropsTuple) {
92 				static if(drop.silkTouch != 0) {
93 					if(item !is null && Enchantments.silkTouch in item) return [Slot(world.items.get(drop.silkTouch))];
94 				} else static if(drop.item != 0) {
95 					static if(drop.max <= drop.min) {
96 						ubyte amount = cast(ubyte)drop.min;
97 					} else static if(drop.min <= 0) {
98 						immutable a = world.random.range(drop.min, drop.max);
99 						if(a <= 0) return [];
100 						ubyte amount = cast(ubyte)a;
101 					} else {
102 						ubyte amount = cast(ubyte)world.random.range(drop.min, drop.max);
103 					}
104 					static if(drop.fortune !is null) {
105 						if(item.hasEnchantment(Enchantments.fortune)) drop.fortune(amount, item.getEnchantmentLevel(Enchantments.fortune), world.random);
106 					}
107 					ret[drop.item] += amount;
108 				}
109 			}
110 			Slot[] slots;
111 			foreach(item, amount; ret) {
112 				auto func = world.items.getConstructor(item);
113 				if(func !is null) {
114 					//TODO do not drop more than 3/4 items (group them)
115 					foreach(i ; 0..amount) {
116 						slots ~= Slot(func(0), 1);
117 					}
118 				}
119 			}
120 			return slots;
121 		}
122 
123 	}
124 
125 	public override uint xp(World world, Player player, Item item) {
126 		static if(miningTool.material != Tools.all) {
127 			// validate the tool
128 			if(item is null || item.toolType != miningTool.type || item.toolMaterial != miningTool.material) return 0;
129 		}
130 		static if(cdrops.length) {
131 			foreach(drop ; dropsTuple) {
132 				static if(drop.silkTouch != 0) {
133 					// do not drop experience when mined with silk touch
134 					if(item !is null && Enchantments.silkTouch in item) return 0;
135 				}
136 			}
137 		}
138 		static if(exp.max <= exp.min) {
139 			uint amount = exp.min;
140 		} else {
141 			uint amount = world.random.range(exp.min, exp.max);
142 		}
143 		static if(exp.fortune) {
144 			auto fortune = Enchantments.fortune in item;
145 			if(fortune) amount += min((*fortune).level, 3) * exp.fortune;
146 		}
147 		return amount;
148 	}
149 
150 	public override tick_t miningTime(Player player, Item item) {
151 		static if(miningTool.type & Tools.sword) {
152 			if(item !is null && item.toolType == Tools.sword) return cast(tick_t)ceil(sb.hardness * 20);
153 		}
154 		double time = sb.hardness; // from seconds to ticks
155 		static if(miningTool.material == Tools.all) {
156 			time *= 1.5;
157 		} else {
158 			time *= item !is null && item.toolMaterial >= miningTool.material ? 1.5 : 5;
159 		}
160 		static if([Tools.pickaxe, Tools.axe, Tools.shovel].canFind(miningTool.type & 7)) {
161 			if(item.toolType == (miningTool.type & 7)) {
162 				final switch(item.toolMaterial) {
163 					case Tools.wood:
164 						time /= 2;
165 						break;
166 					case Tools.stone:
167 						time /= 4;
168 						break;
169 					case Tools.iron:
170 						time /= 6;
171 						break;
172 					case Tools.diamond:
173 						time /= 8;
174 						break;
175 					case Tools.gold:
176 						time /= 12;
177 						break;
178 				}
179 			}
180 		}
181 		return cast(tick_t)ceil(time * 20);
182 	}
183 	
184 }+/
185 
186 struct MiningTool {
187 
188 	ubyte type = Tools.none;
189 	ubyte material = Tools.all;
190 
191 	public this(ubyte type, ubyte material) {
192 		this.type = type;
193 		this.material = material;
194 	}
195 
196 	public this(bool required, ubyte type, ubyte material) {
197 		this(type, required ? material : Tools.all);
198 	}
199 
200 }
201 
202 struct Drop {
203 
204 	public item_t item;
205 	public int min;
206 	public int max;
207 
208 	public item_t silkTouch;
209 
210 	void function(ref ubyte, ubyte, ref Random) fortune = null;
211 
212 	static void plusOne(ref ubyte amount, ubyte level, ref Random random) {
213 		amount += level > 3 ? 3 : level;
214 	}
215 
216 }
217 
218 struct Experience {
219 
220 	uint min, max, fortune;
221 
222 }
223 
224 class MineableBlock : Block {
225 
226 	private const MiningTool _miningTool;
227 	private const Drop[] _drops;
228 	private const Experience _exp;
229 
230 	private bool delegate(Item) validateTool;
231 
232 	public this(sul.blocks.Block data, inout MiningTool miningTool, inout Drop[] drops, inout Experience exp=Experience.init) {
233 		super(data);
234 		this._miningTool = miningTool;
235 		this._drops = drops;
236 		this._exp = exp;
237 		if(miningTool.material == Tools.none || drops.length == 0) {
238 			this.validateTool = &validateToolNo;
239 		} else {
240 			this.validateTool = &validateToolYes;
241 		}
242 	}
243 
244 	public this(sul.blocks.Block data, inout MiningTool miningTool, inout Drop drop, inout Experience exp=Experience.init) {
245 		this(data, miningTool, [drop], exp);
246 	}
247 
248 	public override Slot[] drops(World world, Player player, Item item) {
249 		if(this.validateTool(item)) {
250 			bool silkTouch = item !is null && Enchantments.silkTouch in item; //TODO only calculate if needed
251 			ubyte[item_t] ret;
252 			foreach(drop ; this._drops) {
253 				if(drop.silkTouch && silkTouch) {
254 					ret[drop.silkTouch] = 1;
255 				} else if(drop.item) {
256 					if(drop.max) {
257 						auto res = uniform!"[]"(drop.min, drop.max, world.random);
258 						if(res > 0) ret[drop.item] = cast(ubyte)res;
259 					} else {
260 						ret[drop.item] = cast(ubyte)drop.min;
261 					}
262 				}
263 			}
264 			if(ret.length) {
265 				Slot[] slots;
266 				foreach(item, amount; ret) {
267 					auto func = world.items.getConstructor(item);
268 					if(func !is null) {
269 						//TODO do not drop more than 3/4 items (group them)
270 						foreach(i ; 0..amount) {
271 							slots ~= Slot(func(0), 1);
272 						}
273 					}
274 				}
275 				return slots;
276 			}
277 		}
278 		return [];
279 	}
280 
281 	//TODO exp
282 
283 	//TODO mining time
284 
285 	// functions
286 
287 	private bool validateToolNo(Item item) {
288 		return true;
289 	}
290 
291 	private bool validateToolYes(Item item) {
292 		return item !is null && item.toolType == this._miningTool.type && item.toolMaterial >= this._miningTool.material;
293 	}
294 
295 }
296 
297 class StoneBlock : Block {
298 
299 	private immutable item_t drop, silk_touch;
300 
301 	public this(sul.blocks.Block data, item_t item, item_t silkTouch=0) {
302 		super(data);
303 		this.drop = item;
304 		this.silk_touch = silkTouch;
305 	}
306 
307 	public override Slot[] drops(World world, Player player, Item item) {
308 		if(item !is null) {
309 			if(Enchantments.silkTouch in item) {
310 				return [Slot(world.items.get(this.silk_touch))]; //TODO may be 0
311 			} else if(item.toolType == Tools.pickaxe) {
312 				return [Slot(world.items.get(this.drop))];
313 			}
314 		}
315 		return [];
316 	}
317 
318 	//TODO mining time
319 
320 }
321 
322 class RedstoneOreBlock(bool lit) : MineableBlock {
323 
324 	private block_t change;
325 
326 	public this(sul.blocks.Block data, block_t change) {
327 		super(data, MiningTool(true, Tools.pickaxe, Tools.iron), Drop(Items.redstoneDust, 4, 5, Items.redstoneOre), Experience(1, 5, 1));
328 		this.change = change;
329 	}
330 
331 	//TODO +1 with fortune
332 
333 	static if(lit) {
334 
335 		public final override pure nothrow @property @safe @nogc bool doRandomTick() {
336 			return true;
337 		}
338 
339 		public final override void onRandomTick(World world, BlockPosition position) {
340 			world[position] = this.change;
341 		}
342 
343 	} else {
344 
345 		public override void onEntityStep(Entity entity, BlockPosition position, float fallDistance) {
346 			entity.world[position] = this.change;
347 		}
348 
349 		public override bool onInteract(Player player, Item item, BlockPosition position, ubyte face) {
350 			player.world[position] = this.change;
351 			return false;
352 		}
353 
354 	}
355 	
356 }
357 
358 class SpreadingBlock : MineableBlock {
359 
360 	private block_t[] spreadTo;
361 
362 	private BlockPosition[] positions;
363 
364 	public this(sul.blocks.Block data, MiningTool miningTool, Drop[] drops, block_t[] spreadTo, uint r_x, uint r_z, uint r_y_down, uint r_y_up) {
365 		super(data, miningTool, drops);
366 		this.spreadTo = spreadTo;
367 		// instantiate positions here instead of instatiate them every time onRandomTick is called
368 		BlockPosition[] positions;
369 		foreach(int x ; r_x..r_x+1) {
370 			foreach(int y ; r_y_down..r_y_up+1) {
371 				foreach(int z ; r_z..r_z+1) {
372 					this.positions ~= BlockPosition(x, y, z);
373 				}
374 			}
375 		}
376 	}
377 
378 	public final override pure nothrow @property @safe @nogc bool doRandomTick() {
379 		return true;
380 	}
381 
382 	public override void onRandomTick(World world, BlockPosition position) {
383 		// spread
384 		randomShuffle(this.positions, world.random);
385 		foreach(check ; this.positions) {
386 			BlockPosition target = position + check;
387 			Block b = world[target];
388 			if(b == spreadTo) {
389 				auto sup = world[target + [0, 1, 0]];
390 				if(!sup.hasBoundingBox) {
391 					world[target] = this.id;
392 					break;
393 				}
394 			}
395 		}
396 	}
397 
398 }
399 
400 class SuffocatingSpreadingBlock : SpreadingBlock {
401 
402 	private block_t suffocation;
403 
404 	public this(sul.blocks.Block data, MiningTool miningTool, Drop[] drops, block_t[] spreadTo, uint r_x, uint r_z, uint r_y_down, uint r_y_up, block_t suffocation) {
405 		super(data, miningTool, drops, spreadTo, r_x, r_z, r_y_down, r_y_up);
406 	}
407 
408 	public override void onRandomTick(World world, BlockPosition position) {
409 		auto up = world[position + [0, 1, 0]];
410 		if(up.opacity == 15 || up.fluid) {
411 			world[position] = this.suffocation;
412 			return;
413 		}
414 		super.onRandomTick(world, position);
415 	}
416 
417 }
418 
419 class SaplingBlock : MineableBlock {
420 
421 	private block_t[4] logs, leaves;
422 
423 	public this(sul.blocks.Block data, size_t drop, block_t[] logs, block_t[4] leaves) {
424 		super(data, MiningTool.init, Drop(drop, 1));
425 		this.logs = logs;
426 		this.leaves = leaves;
427 	}
428 
429 	//TODO grow with bone meal
430 
431 }
432 
433 class GravityBlock : MineableBlock {
434 
435 	public this(sul.blocks.Block data, MiningTool miningTool, Drop[] drops) {
436 		super(data, miningTool, drops);
437 	}
438 
439 	public this(sul.blocks.Block data, MiningTool miningTool, Drop drop) {
440 		this(data, miningTool, [drop]);
441 	}
442 
443 	public override void onUpdated(World world, BlockPosition position, Update update) {
444 		if(!world[position].solid) {
445 			world[position] = Blocks.air;
446 			world.spawn!FallingBlock(this, position);
447 		}
448 	}
449 
450 }
451 
452 final class GravelBlock : GravityBlock {
453 
454 	public this(sul.blocks.Block data) {
455 		super(data, MiningTool.init, []);
456 	}
457 
458 	public override Slot[] drops(World world, Player player, Item item) {
459 		Slot[] impl(float prob) {
460 			return [Slot(world.items.get(uniform01(world.random) <= prob ? Items.flint : Items.gravel), 1)];
461 		}
462 		if(item !is null) {
463 			auto fortune = Enchantments.fortune in item;
464 			if(fortune && Enchantments.silkTouch !in item) {
465 				switch((*fortune).level) {
466 					case 1: return impl(.14f);
467 					case 2: return impl(.25f);
468 					default: return [Slot(world.items.get(Items.flint), 1)];
469 				}
470 			}
471 		}
472 		return impl(.1f);
473 	}
474 
475 }
476 
477 class WoodBlock : MineableBlock {
478 
479 	public this(sul.blocks.Block data, item_t drop) {
480 		super(data, MiningTool(false, Tools.axe, Tools.wood), Drop(drop, 1));
481 	}
482 
483 }
484 
485 class LeavesBlock(bool decayable, bool dropApples) : Block {
486 
487 	private enum ubyte[4] bigSaplingDrop = [20, 16, 12, 10];
488 	private enum ubyte[4] smallSaplingDrop = [40, 36, 32, 24];
489 	private enum ubyte[4] appleDrop = [200, 180, 160, 120];
490 
491 	private immutable item_t drop, sapling;
492 
493 	private ubyte[4] saplingDrop;
494 
495 	public this(sul.blocks.Block data, item_t drop, item_t sapling, bool smallDrop) {
496 		super(data);
497 		this.drop = drop;
498 		this.sapling = sapling;
499 		if(smallDrop) {
500 			this.saplingDrop = smallSaplingDrop;
501 		} else {
502 			this.saplingDrop = bigSaplingDrop;
503 		}
504 	}
505 
506 	public override Slot[] drops(World world, Player player, Item item) {
507 		if(item !is null && item == Items.shears) {
508 			return [Slot(world.items.get(this.drop), 1)];
509 		} else if(player !is null) {
510 			size_t lvl = 0;
511 			if(item !is null) {
512 				auto fortune = Enchantments.fortune in item;
513 				if(fortune) lvl = min(3, (*fortune).level);
514 			}
515 			return this.decayDrop(world, this.saplingDrop[lvl], appleDrop[lvl]);
516 		} else {
517 			return [];
518 		}
519 	}
520 
521 	private Slot[] decayDrop(World world, ubyte sp, ubyte ap) {
522 		Slot[] ret;
523 		if(uniform(0, sp, world.random) == 0) {
524 			ret ~= Slot(world.items.get(this.sapling), 1);
525 		}
526 		static if(dropApples) {
527 			if(uniform(0, ap, world.random) == 0) {
528 				ret ~= Slot(world.items.get(Items.apple), 1);
529 			}
530 		}
531 		return ret;
532 	}
533 
534 	static if(decayable) {
535 
536 		public final override pure nothrow @property @safe @nogc bool doRandomTick() {
537 			return true;
538 		}
539 
540 		public override void onRandomTick(World world, BlockPosition position) {
541 			//TODO check decay
542 		}
543 
544 	}
545 
546 }
547 
548 class AbsorbingBlock : MineableBlock {
549 
550 	public this(sul.blocks.Block data, item_t drop, block_t wet, block_t[] absorb, size_t maxDistance, size_t maxBlocks) {
551 		super(data, MiningTool.init, Drop(drop, 1));
552 	}
553 
554 	public override void onUpdated(World world, BlockPosition position, Update update) {
555 		// also called when the block is placed
556 		//TODO try to absorb water
557 	}
558 
559 }
560 
561 class FlowerBlock : MineableBlock {
562 
563 	public this(sul.blocks.Block data, item_t drop) {
564 		super(data, MiningTool.init, Drop(drop, 1));
565 	}
566 
567 }
568 
569 class DoublePlantBlock : Block {
570 
571 	private int y;
572 	private block_t other;
573 	private item_t drop;
574 	private ubyte count;
575 
576 	public this(sul.blocks.Block data, bool top, block_t other, item_t drop, ubyte count=1) {
577 		super(data);
578 		this.count = count;
579 		this.y = top ? -1 : 1;
580 		this.other = other;
581 		this.drop = drop;
582 		this.count = count;
583 	}
584 
585 	public override Slot[] drops(World world, Player player, Item item) {
586 		return [Slot(world.items.get(this.drop), this.count)];
587 	}
588 
589 	public override void onUpdated(World world, BlockPosition position, Update update) {
590 		auto pos = position + [0, this.y, 0];
591 		if(world[pos] != this.other) {
592 			world[pos] = Blocks.air;
593 		}
594 	}
595 
596 }
597 
598 class GrassDoublePlantBlock : DoublePlantBlock {
599 
600 	public this(sul.blocks.Block data, bool top, block_t other, item_t drop) {
601 		super(data, top, other, drop, 2);
602 	}
603 
604 	public override Slot[] drops(World world, Player player, Item item) {
605 		if(item !is null && item.toolType == Tools.shears) return super.drops(world, player, item);
606 		else return [];
607 	}
608 
609 }
610 
611 class PlantBlock : Block {
612 
613 	private item_t shears;
614 	private Drop hand;
615 
616 	public this(sul.blocks.Block data, item_t shears, Drop hand) {
617 		super(data);
618 		this.shears = shears;
619 		this.hand = hand;
620 	}
621 
622 	public override Slot[] drops(World world, Player player, Item item) {
623 		Slot[] ret;
624 		if(item !is null && item.toolType == Tools.shears) {
625 			ret ~= Slot(world.items.get(this.shears), 1);
626 		} else {
627 			ubyte count = cast(ubyte)uniform!"[]"(this.hand.min, this.hand.max, world.random);
628 			if(count) {
629 				auto f = world.items.getConstructor(this.hand.item);
630 				if(f !is null) {
631 					foreach(i ; 0..count) {
632 						ret ~= Slot(f(0), 1);
633 					}
634 				}
635 			}
636 		}
637 		return ret;
638 	}
639 
640 }
641 
642 class StairsBlock : MineableBlock {
643 
644 	public this(sul.blocks.Block data, ubyte facing, bool upsideDown, MiningTool miningTool, item_t drop) {
645 		super(data, miningTool, Drop(drop, 1));
646 	}
647 
648 	//TODO
649 
650 }
651 
652 class CakeBlock : Block {
653 
654 	private block_t next;
655 
656 	public this(sul.blocks.Block data, block_t next) {
657 		super(data);
658 		this.next = next;
659 	}
660 
661 	public override bool onInteract(Player player, Item item, BlockPosition position, ubyte face) {
662 		player.hunger = player.hunger + 2;
663 		player.saturate(.4f);
664 		player.world[position] = this.next;
665 		return true;
666 	}
667 
668 	public override tick_t miningTime(Player player, Item item) {
669 		return 15;
670 	}
671 
672 }
673 
674 class MonsterEggBlock : Block {
675 
676 	private block_t disguise;
677 
678 	private bool silkTouch = false;
679 
680 	public this(sul.blocks.Block data, block_t disguise) {
681 		super(data);
682 		this.disguise = disguise;
683 	}
684 
685 	public override Slot[] drops(World world, Player player, Item item) {
686 		this.silkTouch = item !is null && Enchantments.silkTouch in item;
687 		if(this.silkTouch) {
688 			return [Slot(world.items.get(this.disguise), 1)];
689 		} else {
690 			return [];
691 		}
692 	}
693 
694 	//TODO mining time
695 
696 	public override void onRemoved(World world, BlockPosition position, Remove type) {
697 		if(type == Remove.broken && !this.silkTouch || type == Remove.exploded) {
698 			//world.spawn!Silverfish(position.entityPosition + .5);
699 			//TODO spawn silverfish
700 		}
701 	}
702 
703 }
704 
705 class InactiveEndPortalBlock : Block {
706 
707 	private block_t active;
708 	public immutable ubyte direction;
709 
710 	public this(sul.blocks.Block data, block_t active, ubyte direction) {
711 		super(data);
712 		this.active = active;
713 		this.direction = direction;
714 	}
715 
716 	public override bool onInteract(Player player, Item item, BlockPosition position, ubyte face) {
717 		if(!player.inventory.held.empty && player.inventory.held.item == Items.eyeOfEnder) {
718 			player.inventory.held = player.inventory.held.count == 1 ? Slot(null) : Slot(player.inventory.held.item, cast(ubyte)(player.inventory.held.count - 1));
719 			player.world[position] = this.active;
720 			//TODO check for portal creation (or check when active is placed)
721 			return true;
722 		} else {
723 			return false;
724 		}
725 	}
726 
727 }