Add placed feature foramtters for plants

This commit is contained in:
Andrew Miner
2025-11-02 17:45:44 -07:00
parent ec26afda0b
commit 4b743a79b3
7 changed files with 626 additions and 7 deletions
@@ -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 ]
}
]
}
@@ -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()
@@ -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]
@@ -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()
+16 -1
View File
@@ -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)
@@ -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