diff --git a/mcpacker/format/datapack/placedfeature/__init__.py b/mcpacker/format/datapack/placedfeature/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mcpacker/format/datapack/placedfeature/crop.py b/mcpacker/format/datapack/placedfeature/crop.py new file mode 100644 index 0000000..d120e0b --- /dev/null +++ b/mcpacker/format/datapack/placedfeature/crop.py @@ -0,0 +1,90 @@ +from mcpacker.format.json import JsonBlob +from mcpacker.model.flora.patch import Patch +from mcpacker.model.flora.plantspawn import PlantSpawn +from mcpacker.model.modpack import ModPack + +import mcpacker.model.flora.immersion as IM + + +# Class ############################################################################################ + +class CropPlacedFeature: + + def __init__(self, pack:ModPack, spawn:PlantSpawn): + self.pack = pack + self.spawn = spawn + + def asJsonBlob(self) -> JsonBlob: + return { + "feature": f"{self.pack.name}:crop/{self.spawn.plant.name}", + "placement": self._buildPlacements(), + } + + @property + def patch(self) -> Patch: + if not isinstance(self.spawn.plant, Patch): raise ValueError() + return self.spawn.plant + + # Private Methods ########################################################## + + def _buildPlacements(self) -> list[JsonBlob]: + livesIn = "minecraft:air" + if self.patch.immersion == IM.SUBMERGED: + livesIn = "minecraft:water" + + placements:list[JsonBlob] = [ + { + "type": "minecraft:block_predicate_filter", + "predicate": [ + { + "type": "minecraft:matching_blocks", + "blocks": livesIn, + }, { + "type": "minecraft:matching_blocks", + "offset": [0, -1, 0], + "blocks": list(str(s) for s in self.patch.substrates), + }, + ] + } + ] + + if self.patch.immersion == IM.ADJACENT: + placements.append(self._buildWaterAdjacentPlacement()) + + return placements + + def _buildWaterAdjacentPlacement(self) -> JsonBlob: + return { + "type": "minecraft:any_of", + "predicates": [ + { + "type": "minecraft:matching_fluids", + "fluids": [ + "minecraft:water", + "minecraft:flowing_water" + ], + "offset": [ 1, -1, 0 ] + }, { + "type": "minecraft:matching_fluids", + "fluids": [ + "minecraft:water", + "minecraft:flowing_water" + ], + "offset": [ -1, -1, 0 ] + }, { + "type": "minecraft:matching_fluids", + "fluids": [ + "minecraft:water", + "minecraft:flowing_water" + ], + "offset": [ 0, -1, 1 ] + }, { + "type": "minecraft:matching_fluids", + "fluids": [ + "minecraft:water", + "minecraft:flowing_water" + ], + "offset": [ 0, -1, -1 ] + } + ] + } diff --git a/mcpacker/format/datapack/placedfeature/crop_test.py b/mcpacker/format/datapack/placedfeature/crop_test.py new file mode 100644 index 0000000..2cac6f6 --- /dev/null +++ b/mcpacker/format/datapack/placedfeature/crop_test.py @@ -0,0 +1,245 @@ +from mcpacker.format.datapack.placedfeature.crop import CropPlacedFeature +from mcpacker.format.json import JsonBlob +from mcpacker.model.ecology.biomefilter import BiomeFilter as BF +from mcpacker.model.ecotype import Ecotype +from mcpacker.model.flora.patch import Patch +from mcpacker.model.flora.plantspawn import PlantSpawn +from mcpacker.model.flora.tree import Tree +from mcpacker.model.habitat import Habitat +from mcpacker.model.modpack import ModPack +from mcpacker.write.json import dumps +from pytest import fixture + +import mcpacker.model.altitude as AL +import mcpacker.model.ecology.flora as FL +import mcpacker.model.ecology.heat as HE +import mcpacker.model.ecology.humidity as HU +import mcpacker.model.ecology.soil as SO +import mcpacker.model.ecology.water as WA +import mcpacker.model.flora.canopyshape as CA +import mcpacker.model.flora.density as DE +import mcpacker.model.flora.immersion as IM +import mcpacker.model.flora.trunkshape as TR +import mcpacker.model.scarcity as SC +import textwrap + + +# Fixtures ######################################################################################### + +@fixture(name="pack") +def createModPack(): + yield ModPack("test") + +@fixture(name="farmCropBlob") +def createFarmCropBlob(pack:ModPack): + yield CropPlacedFeature( + pack = pack, + spawn = PlantSpawn( + name = "potato", + habitat = Habitat( + altitude = AL.span(AL.UPLANDS, AL.HILLS), + biomeFilters = BF([FL.FOREST, HE.BOREAL, HU.DAMP, SO.ACIDIC]), + ), + ecotype = Ecotype( + scarcity = SC.UNCOMMON, + ), + plant = Patch( + name = "potato", + blocks = "farmersdelight:wild_potatoes", + density = DE.THIN, + radius = 4, + ), + ), + ).asJsonBlob() + +@fixture(name="waterAdjacentCropBlob") +def createWaterAdjacentBlob(pack:ModPack): + yield CropPlacedFeature( + pack = pack, + spawn = PlantSpawn( + name = "sugarcane", + habitat = Habitat( + altitude = AL.DUNES, + biomeFilters = BF([(WA.RIVER, WA.SWAMP)]), + ), + ecotype = Ecotype( + scarcity = SC.COMMON, + ), + plant = Patch( + name = "sugarcane", + blocks = "minecraft:sugarcane", + density = DE.CARPET, + immersion = IM.ADJACENT, + radius = 3, + ), + ), + ).asJsonBlob() + +@fixture(name="submergedCropBlob") +def createSubmergedCropBlob(pack:ModPack): + yield CropPlacedFeature( + pack = pack, + spawn = PlantSpawn( + name = "rice", + habitat = Habitat( + altitude = AL.SURFACE, + biomeFilters = BF([(WA.RIVER, WA.SWAMP)]), + ), + ecotype = Ecotype( + scarcity = SC.UNCOMMON, + ), + plant = Patch( + name = "rice", + blocks = "farmersdelight:wild_rice", + density = DE.PACKED, + immersion = IM.SUBMERGED, + radius = 6, + substrates = [ "minecraft:sand", "minecraft:dirt" ], + ), + ), + ).asJsonBlob() + + +# Tests ############################################################################################ + +def test_farmCrop(farmCropBlob:JsonBlob): + assert dumps(farmCropBlob) == textwrap.dedent(""" + { + "feature": "test:crop/potato", + "placement": [ + { + "type": "minecraft:block_predicate_filter", + "predicate": [ + { + "type": "minecraft:matching_blocks", + "blocks": "minecraft:air" + }, + { + "type": "minecraft:matching_blocks", + "offset": [ + 0, + -1, + 0 + ], + "blocks": [ + "#minecraft:dirt" + ] + } + ] + } + ] + } + """).strip() + +def test_submergedCrop(submergedCropBlob:JsonBlob): + assert dumps(submergedCropBlob) == textwrap.dedent(""" + { + "feature": "test:crop/rice", + "placement": [ + { + "type": "minecraft:block_predicate_filter", + "predicate": [ + { + "type": "minecraft:matching_blocks", + "blocks": "minecraft:water" + }, + { + "type": "minecraft:matching_blocks", + "offset": [ + 0, + -1, + 0 + ], + "blocks": [ + "minecraft:sand", + "minecraft:dirt" + ] + } + ] + } + ] + } + """).strip() + +def test_waterAdjacentCrop(waterAdjacentCropBlob:JsonBlob): + assert dumps(waterAdjacentCropBlob) == textwrap.dedent(""" + { + "feature": "test:crop/sugarcane", + "placement": [ + { + "type": "minecraft:block_predicate_filter", + "predicate": [ + { + "type": "minecraft:matching_blocks", + "blocks": "minecraft:air" + }, + { + "type": "minecraft:matching_blocks", + "offset": [ + 0, + -1, + 0 + ], + "blocks": [ + "#minecraft:dirt" + ] + } + ] + }, + { + "type": "minecraft:any_of", + "predicates": [ + { + "type": "minecraft:matching_fluids", + "fluids": [ + "minecraft:water", + "minecraft:flowing_water" + ], + "offset": [ + 1, + -1, + 0 + ] + }, + { + "type": "minecraft:matching_fluids", + "fluids": [ + "minecraft:water", + "minecraft:flowing_water" + ], + "offset": [ + -1, + -1, + 0 + ] + }, + { + "type": "minecraft:matching_fluids", + "fluids": [ + "minecraft:water", + "minecraft:flowing_water" + ], + "offset": [ + 0, + -1, + 1 + ] + }, + { + "type": "minecraft:matching_fluids", + "fluids": [ + "minecraft:water", + "minecraft:flowing_water" + ], + "offset": [ + 0, + -1, + -1 + ] + } + ] + } + ] + } + """).strip() + diff --git a/mcpacker/format/datapack/placedfeature/patch.py b/mcpacker/format/datapack/placedfeature/patch.py new file mode 100644 index 0000000..3523860 --- /dev/null +++ b/mcpacker/format/datapack/placedfeature/patch.py @@ -0,0 +1,77 @@ +from mcpacker.model.altitude import Altitude +from mcpacker.format.json import JsonBlob +from mcpacker.model.flora.patch import Patch +from mcpacker.model.flora.plantspawn import PlantSpawn +from mcpacker.model.modpack import ModPack + +import mcpacker.model.altitude as AL +import mcpacker.model.scarcity as SC + + +# Class ############################################################################################ + +class PatchPlacedFeature: + + def __init__(self, pack:ModPack, spawn:PlantSpawn, altitude:Altitude): + self.altitude = altitude + self.pack = pack + self.spawn = spawn + + def asJsonBlob(self) -> JsonBlob|None: + fullRange = self.spawn.habitat.altitude + band = AL.intersect(self.altitude, fullRange) + if not band: return None + + kind = "tree" + if isinstance(self.spawn.plant, Patch): + kind = "patch" + + return { + "feature": f"{self.pack.name}:{kind}/{self.spawn.plant.name}", + "placement": [ + { + "type": "mincraft:rarity_filter", + "chance": self._getRarityChance(), + }, { + "type": "minecraft:in_square", + }, { + "type": "minecraft:biome", + }, { + "type": "minecraft:height_range", + "height": { + "type": "minecraft:constant", + "value": { + "absolute": band.bottom, + }, + }, + }, { + "type": "minecraft:block_predicate_filter", + "predicate": { + "type": "minecraft:matching_blocks", + "blocks": "minecraft:air", + }, + }, { + "type": "minecraft:environment_scan", + "direction_of_search": "up", + "max_steps": min(32, band.top - band.bottom), + "target_condition": { + "type": "minecraft:matching_blocks", + "blocks": "minecraft:air", + }, + }, + ], + } + + + # Private Methods ########################################################## + + def _getRarityChance(self) -> int: + return { + SC.ABSENT: 100000, + SC.RARE: 64, + SC.UNUSUAL: 32, + SC.SPARSE: 16, + SC.UNCOMMON: 8, + SC.COMMON: 3, + SC.CARPET: 2 + }[self.spawn.ecotype.scarcity] diff --git a/mcpacker/format/datapack/placedfeature/patch_test.py b/mcpacker/format/datapack/placedfeature/patch_test.py new file mode 100644 index 0000000..ee3e8f3 --- /dev/null +++ b/mcpacker/format/datapack/placedfeature/patch_test.py @@ -0,0 +1,197 @@ +from mcpacker.format.datapack.placedfeature.patch import PatchPlacedFeature +from mcpacker.format.json import JsonBlob +from mcpacker.model.ecology.biomefilter import BiomeFilter as BF +from mcpacker.model.ecotype import Ecotype +from mcpacker.model.flora.patch import Patch +from mcpacker.model.flora.plantspawn import PlantSpawn +from mcpacker.model.flora.tree import Tree +from mcpacker.model.habitat import Habitat +from mcpacker.model.modpack import ModPack +from mcpacker.write.json import dumps +from pytest import fixture + +import mcpacker.model.altitude as AL +import mcpacker.model.ecology.flora as FL +import mcpacker.model.ecology.heat as HE +import mcpacker.model.ecology.humidity as HU +import mcpacker.model.ecology.soil as SO +import mcpacker.model.ecology.water as WA +import mcpacker.model.flora.canopyshape as CA +import mcpacker.model.flora.density as DE +import mcpacker.model.flora.immersion as IM +import mcpacker.model.flora.trunkshape as TR +import mcpacker.model.scarcity as SC +import textwrap + + +# Fixtures ######################################################################################### + +@fixture(name="pack") +def createModPack(): + yield ModPack("test") + +@fixture(name="farmPatchBlob") +def createFarmPatchBlob(pack:ModPack): + yield PatchPlacedFeature( + altitude = AL.UPLANDS, + pack = pack, + spawn = PlantSpawn( + name = "potato", + habitat = Habitat( + altitude = AL.span(AL.UPLANDS, AL.HILLS), + biomeFilters = BF([FL.FOREST, HE.BOREAL, HU.DAMP, SO.ACIDIC]), + ), + ecotype = Ecotype( + scarcity = SC.UNCOMMON, + ), + plant = Patch( + name = "potato", + blocks = "farmersdelight:wild_potatoes", + density = DE.THIN, + radius = 4, + ), + ), + ).asJsonBlob() + +@fixture(name="waterAdjacentBlob") +def createWaterAdjacentBlob(pack:ModPack): + yield PatchPlacedFeature( + altitude = AL.DUNES, + pack = pack, + spawn = PlantSpawn( + name = "sugarcane", + habitat = Habitat( + altitude = AL.DUNES, + biomeFilters = BF([(WA.RIVER, WA.SWAMP)]), + ), + ecotype = Ecotype( + scarcity = SC.COMMON, + ), + plant = Patch( + name = "sugarcane", + blocks = "minecraft:sugarcane", + density = DE.CARPET, + immersion = IM.ADJACENT, + radius = 3, + ), + ), + ).asJsonBlob() + +@fixture(name="treeBlob") +def createTreeBlob(pack:ModPack): + yield PatchPlacedFeature( + altitude = AL.LOWLANDS, + pack = pack, + spawn = PlantSpawn( + name = "apple", + habitat = Habitat( + altitude = AL.LOWLANDS, + biomeFilters = BF([(FL.FOREST, FL.CLEARING), HE.TEMPERATE, SO.LOAMY]), + ), + ecotype = Ecotype( + scarcity = SC.SPARSE, + ), + plant = Tree( + name = "apple", + foliage = "fruitsdelight:apple_leaves", + log = "minecraft:oak_log", + trunkShape = TR.STRAIGHT, + trunkHeightMin = 3, + trunkHeightMax = 5, + canopyShape = CA.BLOB, + canopyHeight = 3, + canopyRadius = 2 + ) + ) + ).asJsonBlob() + + +# Tests ############################################################################################ + +def test_farmPatch(farmPatchBlob:JsonBlob): + assert dumps(farmPatchBlob) == textwrap.dedent(""" + { + "feature": "test:patch/potato", + "placement": [ + { + "type": "mincraft:rarity_filter", + "chance": 8 + }, + { + "type": "minecraft:in_square" + }, + { + "type": "minecraft:biome" + }, + { + "type": "minecraft:height_range", + "height": { + "type": "minecraft:constant", + "value": { + "absolute": 92 + } + } + }, + { + "type": "minecraft:block_predicate_filter", + "predicate": { + "type": "minecraft:matching_blocks", + "blocks": "minecraft:air" + } + }, + { + "type": "minecraft:environment_scan", + "direction_of_search": "up", + "max_steps": 32, + "target_condition": { + "type": "minecraft:matching_blocks", + "blocks": "minecraft:air" + } + } + ] + } + """).strip() + +def test_tree(treeBlob:JsonBlob): + assert dumps(treeBlob) == textwrap.dedent(""" + { + "feature": "test:tree/apple", + "placement": [ + { + "type": "mincraft:rarity_filter", + "chance": 16 + }, + { + "type": "minecraft:in_square" + }, + { + "type": "minecraft:biome" + }, + { + "type": "minecraft:height_range", + "height": { + "type": "minecraft:constant", + "value": { + "absolute": 70 + } + } + }, + { + "type": "minecraft:block_predicate_filter", + "predicate": { + "type": "minecraft:matching_blocks", + "blocks": "minecraft:air" + } + }, + { + "type": "minecraft:environment_scan", + "direction_of_search": "up", + "max_steps": 22, + "target_condition": { + "type": "minecraft:matching_blocks", + "blocks": "minecraft:air" + } + } + ] + } + """).strip() diff --git a/mcpacker/model/altitude.py b/mcpacker/model/altitude.py index 34fe8b9..971bf57 100644 --- a/mcpacker/model/altitude.py +++ b/mcpacker/model/altitude.py @@ -70,7 +70,22 @@ ALL = OVERGROUND + UNDERGROUND + UNDERWATER # Helper Functions ################################################################################# -def span(*altitudes:Altitude): +def intersect(*altitudes:Altitude) -> Altitude|None: + if not altitudes: return None + + bottom = altitudes[0].bottom + top = altitudes[0].top + + for index in range(1, len(altitudes)): + bottom = max(bottom, altitudes[index].bottom) + top = min(top, altitudes[index].top) + + if bottom >= top: + return None + + return Altitude(altitudes[0].name, bottom, top) + +def span(*altitudes:Altitude) -> Altitude: if not altitudes: return ANYWHERE result = Altitude("", ANYWHERE.top, ANYWHERE.bottom) diff --git a/mcpacker/write/resourcepack/blockstatewriter.py b/mcpacker/write/resourcepack/blockstatewriter.py index 41edc02..70f80a5 100644 --- a/mcpacker/write/resourcepack/blockstatewriter.py +++ b/mcpacker/write/resourcepack/blockstatewriter.py @@ -5,9 +5,4 @@ from mcpacker.write.writer import Writer class BlockStateWriter(Writer): def doWrite(self): - for mod in self.pack.resourcePack.mods: - for blockState in mod.blockStates: - path = self.locator.blockStates() # filename? - # TODO: not implemented - - + pass