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 }