Initial sanctuary sources

This commit is contained in:
cnstiout
2026-07-04 20:27:08 +02:00
commit c796a20321
1582 changed files with 253264 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
.DS_Store
**/.DS_Store
# Gradle and build outputs
.gradle/
build/
run/
logs/
out/
*.class
*.log
# Pack/export artifacts kept outside Git
/sanctuary.zip
/Gameplay/blocodex/blocodex-java.zip
# Local IDE/editor files
.idea/
.vscode/
*.iml
# Local toolchain caches
.jdk/
.codex-cache/
+34
View File
@@ -0,0 +1,34 @@
# Gradle
.gradle/
build/
out/
classes/
# Eclipse
*.launch
# IntelliJ IDEA
.idea/
*.iml
*.ipr
*.iws
# VS Code
.settings/
.vscode/
bin/
.classpath
.project
# macOS
*.DS_Store
# Fabric
run/
# Java
.jdk/
hs_err_*.log
replay_*.log
*.hprof
*.jfr
+74
View File
@@ -0,0 +1,74 @@
plugins {
id "net.fabricmc.fabric-loom" version "${loom_version}"
id "maven-publish"
}
version = project.mod_version
group = project.maven_group
repositories {
mavenCentral()
}
loom {
splitEnvironmentSourceSets()
mods {
"canaplia" {
sourceSet sourceSets.main
sourceSet sourceSets.client
}
}
}
dependencies {
minecraft "com.mojang:minecraft:${project.minecraft_version}"
implementation "net.fabricmc:fabric-loader:${project.loader_version}"
implementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}"
testImplementation "org.junit.jupiter:junit-jupiter:5.11.4"
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
}
processResources {
def version = project.version
inputs.property "version", version
filesMatching("fabric.mod.json") {
expand "version": version
}
}
tasks.withType(JavaCompile).configureEach {
it.options.release = 25
}
tasks.withType(Test).configureEach {
useJUnitPlatform()
}
java {
withSourcesJar()
sourceCompatibility = JavaVersion.VERSION_25
targetCompatibility = JavaVersion.VERSION_25
}
jar {
def projectName = project.name
inputs.property "projectName", projectName
from("../../license.txt") {
rename { "${it}_${projectName}" }
}
}
publishing {
publications {
create("mavenJava", MavenPublication) {
from components.java
}
}
repositories {
}
}
@@ -0,0 +1,25 @@
# Canaplia 2.0 Foundations
This document tracks the architectural groundwork introduced in `2.0.0`.
## Goals
- Prepare variant-specific smoke effects without changing item ids later.
- Prepare resin evolution from shared-behavior resin blocks toward future unique resin variants.
- Keep current gameplay behavior stable until the next prompts add new content.
## What changed
- `RolledJointItem` is now aware of its `HempVariant`.
- `CanapliaSmokeEffects` now resolves profiles from `SmokeSource + HempVariant`.
- `HempBudCompostingState` now remembers which hemp variant was inserted into each tracked composter.
- `HempBudComposterInteraction` now accepts any hemp bud variant and blocks mixed-variant composting in a single composter.
- `CanapliaResinCatalog` now maps each bud variant to its matching resin block and burn duration.
## Current shared behavior
- All hemp variants still use the same smoke effect set for now.
- All resin block variants still use the same smoke behavior for now.
- All resin block variants still use the same burn duration for now.
## Next prompt entry points
- Add unique smoke profiles in `CanapliaSmokeEffects#createVariantAwareProfile`.
- Add unique burn durations in `CanapliaResinCatalog#resolve`.
- Add unique resin-side effects if they need to diverge from the hemp source variant.
@@ -0,0 +1,18 @@
source_block,source_family,biome_focus,possible_variants,base_seed_drop_chance,extra_conditions,codex_notes
minecraft:short_grass,surface_flora,plains; forest; meadow; cherry_grove,common; tender; olive,0.06,Starter source buffed for survival playability,Main survival starter source
minecraft:tall_grass,surface_flora,plains; meadow; savanna; flower_forest,common; tender; olive,0.1,Higher chance than short grass,Best early-game exploration source
minecraft:short_dry_grass,dry_surface_flora,savanna; desert; badlands,olive,0.06,Fabric 1.21.11 dry-grass starter source,Primary dry-biome starter source
minecraft:tall_dry_grass,dry_surface_flora,savanna; desert; badlands,olive,0.1,Higher chance than short dry grass,Best early-game dry starter source
minecraft:fern,cold_or_shaded_flora,taiga; dark_forest; snowy_taiga; grove,dark; ashen,0.07,Pick by biome temperature / canopy,Useful for cold and shade strains
minecraft:large_fern,cold_or_shaded_flora,snowy_taiga; grove; old_growth_taiga,ashen; dark,0.1,Slightly better than normal fern,Good for cold-biome hunting
minecraft:dead_bush,dry_flora,badlands; eroded_badlands; wooded_badlands; desert,amber; olive,0.1,Amber should dominate in badlands,Avoid dirt-breaking loot; keep it botanical
minecraft:moss_carpet,humid_flora,lush_caves,moss,0.07,Humidity source,Pairs well with cave exploration
minecraft:hanging_roots,humid_flora,lush_caves; mangrove_swamp,moss,0.1,Humidity source,Good spelunker / wet-biome source
minecraft:cave_vines,humid_flora,lush_caves,moss,0.06,Humidity source,Optional extra source if desired
minecraft:crimson_roots,nether_native,crimson_forest; nether_wastes,ember,0.02,Nether only,Forced ember result
minecraft:warped_roots,nether_native,warped_forest; basalt_deltas,ember,0.02,Nether only,Forced ember result
minecraft:nether_sprouts,nether_native,soul_sand_valley; basalt_deltas,ember,0.015,Nether only,Forced ember result
minecraft:weeping_vines,nether_native,crimson_forest,ember,0.01,Nether only,Forced ember result
minecraft:twisting_vines,nether_native,warped_forest; soul_sand_valley,ember,0.01,Nether only,Forced ember result
minecraft:chorus_plant,end_native,end_highlands; end_midlands; small_end_islands; end_barrens,indigo,0.015,End only,Forced indigo result
minecraft:chorus_flower,end_native,end_highlands; end_midlands; small_end_islands; end_barrens,indigo,0.02,End only,Forced indigo result
1 source_block source_family biome_focus possible_variants base_seed_drop_chance extra_conditions codex_notes
2 minecraft:short_grass surface_flora plains; forest; meadow; cherry_grove common; tender; olive 0.06 Starter source buffed for survival playability Main survival starter source
3 minecraft:tall_grass surface_flora plains; meadow; savanna; flower_forest common; tender; olive 0.1 Higher chance than short grass Best early-game exploration source
4 minecraft:short_dry_grass dry_surface_flora savanna; desert; badlands olive 0.06 Fabric 1.21.11 dry-grass starter source Primary dry-biome starter source
5 minecraft:tall_dry_grass dry_surface_flora savanna; desert; badlands olive 0.1 Higher chance than short dry grass Best early-game dry starter source
6 minecraft:fern cold_or_shaded_flora taiga; dark_forest; snowy_taiga; grove dark; ashen 0.07 Pick by biome temperature / canopy Useful for cold and shade strains
7 minecraft:large_fern cold_or_shaded_flora snowy_taiga; grove; old_growth_taiga ashen; dark 0.1 Slightly better than normal fern Good for cold-biome hunting
8 minecraft:dead_bush dry_flora badlands; eroded_badlands; wooded_badlands; desert amber; olive 0.1 Amber should dominate in badlands Avoid dirt-breaking loot; keep it botanical
9 minecraft:moss_carpet humid_flora lush_caves moss 0.07 Humidity source Pairs well with cave exploration
10 minecraft:hanging_roots humid_flora lush_caves; mangrove_swamp moss 0.1 Humidity source Good spelunker / wet-biome source
11 minecraft:cave_vines humid_flora lush_caves moss 0.06 Humidity source Optional extra source if desired
12 minecraft:crimson_roots nether_native crimson_forest; nether_wastes ember 0.02 Nether only Forced ember result
13 minecraft:warped_roots nether_native warped_forest; basalt_deltas ember 0.02 Nether only Forced ember result
14 minecraft:nether_sprouts nether_native soul_sand_valley; basalt_deltas ember 0.015 Nether only Forced ember result
15 minecraft:weeping_vines nether_native crimson_forest ember 0.01 Nether only Forced ember result
16 minecraft:twisting_vines nether_native warped_forest; soul_sand_valley ember 0.01 Nether only Forced ember result
17 minecraft:chorus_plant end_native end_highlands; end_midlands; small_end_islands; end_barrens indigo 0.015 End only Forced indigo result
18 minecraft:chorus_flower end_native end_highlands; end_midlands; small_end_islands; end_barrens indigo 0.02 End only Forced indigo result
@@ -0,0 +1,7 @@
result_variant,parent_a,parent_b,rule_type,environment_requirement,harvest_requirement,result_seed_chance,codex_notes
gold,tender,olive,hybrid,warm biome; sky visible; daytime,Both parents mature and adjacent (radius 1),0.12,Bright dry hybrid
purple,dark,moss,hybrid,humid area; low light,Both parents mature and adjacent (radius 1),0.08,Shadow-humidity hybrid
pink,tender,purple,hybrid,flower biome or greenhouse; dawn or dusk,Both parents mature and adjacent (radius 1),0.1,Floral color hybrid
coral,amber,moss,hybrid,warm biome; farmland touching water,Both parents mature and adjacent (radius 1),0.1,Warm coastal hybrid
bluish,ashen,tender,hybrid,cold biome or snow/ice nearby,Both parents mature and adjacent (radius 1),0.09,Cold color hybrid
varieg,any,different_any,mutation,humid biome; especially lush_caves,Two distinct mature variants adjacent (radius 1),0.06,Collector mutation
1 result_variant parent_a parent_b rule_type environment_requirement harvest_requirement result_seed_chance codex_notes
2 gold tender olive hybrid warm biome; sky visible; daytime Both parents mature and adjacent (radius 1) 0.12 Bright dry hybrid
3 purple dark moss hybrid humid area; low light Both parents mature and adjacent (radius 1) 0.08 Shadow-humidity hybrid
4 pink tender purple hybrid flower biome or greenhouse; dawn or dusk Both parents mature and adjacent (radius 1) 0.1 Floral color hybrid
5 coral amber moss hybrid warm biome; farmland touching water Both parents mature and adjacent (radius 1) 0.1 Warm coastal hybrid
6 bluish ashen tender hybrid cold biome or snow/ice nearby Both parents mature and adjacent (radius 1) 0.09 Cold color hybrid
7 varieg any different_any mutation humid biome; especially lush_caves Two distinct mature variants adjacent (radius 1) 0.06 Collector mutation
@@ -0,0 +1,4 @@
variant_key,rule_type,source,condition,result_chance,codex_notes
lunar,special_harvest,ashen or bluish,Harvest mature parent at night with sky visible in a cold biome,0.06,Night-only special roll; no full moon requirement
ember,dimension_lock,nether_native_plants,Break eligible Nether flora in any Nether biome,1.0,Forced result in Nether source table; never obtainable elsewhere
indigo,dimension_lock,chorus_plant or chorus_flower,Break eligible End flora in any End biome,1.0,Forced result in End source table; never obtainable elsewhere
1 variant_key rule_type source condition result_chance codex_notes
2 lunar special_harvest ashen or bluish Harvest mature parent at night with sky visible in a cold biome 0.06 Night-only special roll; no full moon requirement
3 ember dimension_lock nether_native_plants Break eligible Nether flora in any Nether biome 1.0 Forced result in Nether source table; never obtainable elsewhere
4 indigo dimension_lock chorus_plant or chorus_flower Break eligible End flora in any End biome 1.0 Forced result in End source table; never obtainable elsewhere
@@ -0,0 +1,496 @@
{
"variants_catalog": [
{
"variant_key": "common",
"variant_name_fr": "Chanvre commun",
"family": "wild_base",
"obtainable_in_survival": "yes",
"first_obtention": "Discover seeds by breaking short grass or tall grass",
"source_blocks_or_plants": "minecraft:short_grass; minecraft:tall_grass",
"biome_focus": "plains; forest; riverbanks",
"breeding_recipe": "",
"special_conditions": "No special condition",
"dimension_lock": "",
"codex_notes": "Main starter line. Use as fallback green strain."
},
{
"variant_key": "tender",
"variant_name_fr": "Chanvre tendre",
"family": "wild_base",
"obtainable_in_survival": "yes",
"first_obtention": "Discover seeds by breaking short grass or tall grass",
"source_blocks_or_plants": "minecraft:short_grass; minecraft:tall_grass",
"biome_focus": "meadow; flower_forest; cherry_grove",
"breeding_recipe": "",
"special_conditions": "Best as a bright / floral base strain",
"dimension_lock": "",
"codex_notes": "Soft green line used in several hybrid recipes."
},
{
"variant_key": "olive",
"variant_name_fr": "Chanvre olive",
"family": "wild_base",
"obtainable_in_survival": "yes",
"first_obtention": "Discover seeds by breaking grass or dry grass in warm biomes",
"source_blocks_or_plants": "minecraft:tall_grass; minecraft:short_grass; minecraft:tall_dry_grass; minecraft:short_dry_grass",
"biome_focus": "savanna; windswept_savanna; desert_edge",
"breeding_recipe": "",
"special_conditions": "Warm / dry overworld discovery line",
"dimension_lock": "",
"codex_notes": "Dry-climate green line."
},
{
"variant_key": "dark",
"variant_name_fr": "Chanvre sombre",
"family": "wild_base",
"obtainable_in_survival": "yes",
"first_obtention": "Discover seeds by breaking ferns or shaded grass",
"source_blocks_or_plants": "minecraft:fern; minecraft:short_grass",
"biome_focus": "dark_forest; old_growth_taiga; pale_garden",
"breeding_recipe": "",
"special_conditions": "Better in low light / canopy-heavy biomes",
"dimension_lock": "",
"codex_notes": "Shade line; key parent for purple."
},
{
"variant_key": "ashen",
"variant_name_fr": "Chanvre cendré",
"family": "wild_base",
"obtainable_in_survival": "yes",
"first_obtention": "Discover seeds by breaking ferns in cold biomes",
"source_blocks_or_plants": "minecraft:fern; minecraft:large_fern",
"biome_focus": "snowy_taiga; grove; snowy_plains",
"breeding_recipe": "",
"special_conditions": "Cold-climate base line",
"dimension_lock": "",
"codex_notes": "Cold green line; key parent for bluish and lunar."
},
{
"variant_key": "moss",
"variant_name_fr": "Chanvre mousse",
"family": "wild_base",
"obtainable_in_survival": "yes",
"first_obtention": "Discover seeds from humid cave / swamp flora",
"source_blocks_or_plants": "minecraft:moss_carpet; minecraft:hanging_roots; minecraft:cave_vines",
"biome_focus": "lush_caves; swamp; mangrove_swamp",
"breeding_recipe": "",
"special_conditions": "Humidity-heavy discovery line",
"dimension_lock": "",
"codex_notes": "Humid green line; key parent for purple, coral, varieg."
},
{
"variant_key": "amber",
"variant_name_fr": "Chanvre ambré",
"family": "wild_rare",
"obtainable_in_survival": "yes",
"first_obtention": "Rare seed from dead bushes",
"source_blocks_or_plants": "minecraft:dead_bush",
"biome_focus": "badlands; eroded_badlands; wooded_badlands",
"breeding_recipe": "",
"special_conditions": "Rare wild discovery in hot mineral biomes",
"dimension_lock": "",
"codex_notes": "Dry rare line; parent for coral."
},
{
"variant_key": "gold",
"variant_name_fr": "Chanvre doré",
"family": "hybrid",
"obtainable_in_survival": "yes",
"first_obtention": "Obtain as hybrid seed",
"source_blocks_or_plants": "",
"biome_focus": "warm_biomes; bright_surface_fields",
"breeding_recipe": "tender + olive",
"special_conditions": "Mature parents adjacent (radius 1); harvest by day with sky visible",
"dimension_lock": "",
"codex_notes": "Colorful hybrid from bright and dry green parents."
},
{
"variant_key": "pink",
"variant_name_fr": "Chanvre rosé",
"family": "hybrid",
"obtainable_in_survival": "yes",
"first_obtention": "Obtain as hybrid seed",
"source_blocks_or_plants": "",
"biome_focus": "flower_forest; meadow; cherry_grove",
"breeding_recipe": "tender + purple",
"special_conditions": "Mature parents adjacent (radius 1); harvest at dawn or dusk",
"dimension_lock": "",
"codex_notes": "Stylized floral hybrid."
},
{
"variant_key": "coral",
"variant_name_fr": "Chanvre corail",
"family": "hybrid",
"obtainable_in_survival": "yes",
"first_obtention": "Obtain as hybrid seed",
"source_blocks_or_plants": "",
"biome_focus": "mangrove_edge; beach; warm_coast",
"breeding_recipe": "amber + moss",
"special_conditions": "Mature parents adjacent (radius 1); at least 1 water block touching farmland",
"dimension_lock": "",
"codex_notes": "Warm wet hybrid."
},
{
"variant_key": "purple",
"variant_name_fr": "Chanvre pourpre",
"family": "hybrid",
"obtainable_in_survival": "yes",
"first_obtention": "Obtain as hybrid seed",
"source_blocks_or_plants": "",
"biome_focus": "humid_shaded_fields; swamp; dark_forest",
"breeding_recipe": "dark + moss",
"special_conditions": "Mature parents adjacent (radius 1); low light and humidity",
"dimension_lock": "",
"codex_notes": "Shadow-humidity hybrid."
},
{
"variant_key": "varieg",
"variant_name_fr": "Chanvre panaché",
"family": "hybrid",
"obtainable_in_survival": "yes",
"first_obtention": "Obtain as mutation seed",
"source_blocks_or_plants": "",
"biome_focus": "lush_caves; jungle; swamp_greenhouse",
"breeding_recipe": "any_two_distinct_variants",
"special_conditions": "Two distinct mature parents adjacent (radius 1); humid biome",
"dimension_lock": "",
"codex_notes": "Mutation-style collector variant."
},
{
"variant_key": "bluish",
"variant_name_fr": "Chanvre bleuté",
"family": "hybrid",
"obtainable_in_survival": "yes",
"first_obtention": "Obtain as hybrid seed",
"source_blocks_or_plants": "",
"biome_focus": "cold_fields; grove; ice_edge",
"breeding_recipe": "ashen + tender",
"special_conditions": "Mature parents adjacent (radius 1); cold biome or snow/ice nearby",
"dimension_lock": "",
"codex_notes": "Cold hybrid, moved out of wild discovery to keep survival logic coherent."
},
{
"variant_key": "lunar",
"variant_name_fr": "Chanvre lunaire",
"family": "circumstantial",
"obtainable_in_survival": "yes",
"first_obtention": "Special night harvest",
"source_blocks_or_plants": "",
"biome_focus": "cold_open_sky_biomes",
"breeding_recipe": "ashen or bluish -> lunar seed roll",
"special_conditions": "Harvest mature ashen or bluish plant at night, with sky visible, in a cold biome",
"dimension_lock": "",
"codex_notes": "Not found directly; obtained through timed harvest."
},
{
"variant_key": "ember",
"variant_name_fr": "Chanvre braise",
"family": "dimensional",
"obtainable_in_survival": "yes",
"first_obtention": "Break native Nether flora",
"source_blocks_or_plants": "minecraft:crimson_roots; minecraft:warped_roots; minecraft:nether_sprouts; minecraft:weeping_vines; minecraft:twisting_vines",
"biome_focus": "all_nether_biomes",
"breeding_recipe": "",
"special_conditions": "Nether-only. Never drops outside the Nether.",
"dimension_lock": "nether",
"codex_notes": "Dimension identity strain."
},
{
"variant_key": "indigo",
"variant_name_fr": "Chanvre indigo",
"family": "dimensional",
"obtainable_in_survival": "yes",
"first_obtention": "Break chorus plants or chorus flowers",
"source_blocks_or_plants": "minecraft:chorus_plant; minecraft:chorus_flower",
"biome_focus": "all_end_biomes",
"breeding_recipe": "",
"special_conditions": "End-only. Never drops outside the End.",
"dimension_lock": "end",
"codex_notes": "Dimension identity strain."
}
],
"discovery_sources": [
{
"source_block": "minecraft:short_grass",
"source_family": "surface_flora",
"biome_focus": "plains; forest; meadow; cherry_grove",
"possible_variants": [
"common",
"tender",
"olive"
],
"base_seed_drop_chance": 0.06,
"extra_conditions": "Starter source buffed for survival playability",
"codex_notes": "Main survival starter source"
},
{
"source_block": "minecraft:tall_grass",
"source_family": "surface_flora",
"biome_focus": "plains; meadow; savanna; flower_forest",
"possible_variants": [
"common",
"tender",
"olive"
],
"base_seed_drop_chance": 0.1,
"extra_conditions": "Higher chance than short grass",
"codex_notes": "Best early-game exploration source"
},
{
"source_block": "minecraft:short_dry_grass",
"source_family": "dry_surface_flora",
"biome_focus": "savanna; desert; badlands",
"possible_variants": [
"olive"
],
"base_seed_drop_chance": 0.06,
"extra_conditions": "Fabric 1.21.11 dry-grass starter source",
"codex_notes": "Primary dry-biome starter source"
},
{
"source_block": "minecraft:tall_dry_grass",
"source_family": "dry_surface_flora",
"biome_focus": "savanna; desert; badlands",
"possible_variants": [
"olive"
],
"base_seed_drop_chance": 0.1,
"extra_conditions": "Higher chance than short dry grass",
"codex_notes": "Best early-game dry starter source"
},
{
"source_block": "minecraft:fern",
"source_family": "cold_or_shaded_flora",
"biome_focus": "taiga; dark_forest; snowy_taiga; grove",
"possible_variants": [
"dark",
"ashen"
],
"base_seed_drop_chance": 0.07,
"extra_conditions": "Pick by biome temperature / canopy",
"codex_notes": "Useful for cold and shade strains"
},
{
"source_block": "minecraft:large_fern",
"source_family": "cold_or_shaded_flora",
"biome_focus": "snowy_taiga; grove; old_growth_taiga",
"possible_variants": [
"ashen",
"dark"
],
"base_seed_drop_chance": 0.1,
"extra_conditions": "Slightly better than normal fern",
"codex_notes": "Good for cold-biome hunting"
},
{
"source_block": "minecraft:dead_bush",
"source_family": "dry_flora",
"biome_focus": "badlands; eroded_badlands; wooded_badlands; desert",
"possible_variants": [
"amber",
"olive"
],
"base_seed_drop_chance": 0.1,
"extra_conditions": "Amber should dominate in badlands",
"codex_notes": "Avoid dirt-breaking loot; keep it botanical"
},
{
"source_block": "minecraft:moss_carpet",
"source_family": "humid_flora",
"biome_focus": "lush_caves",
"possible_variants": [
"moss"
],
"base_seed_drop_chance": 0.07,
"extra_conditions": "Humidity source",
"codex_notes": "Pairs well with cave exploration"
},
{
"source_block": "minecraft:hanging_roots",
"source_family": "humid_flora",
"biome_focus": "lush_caves; mangrove_swamp",
"possible_variants": [
"moss"
],
"base_seed_drop_chance": 0.1,
"extra_conditions": "Humidity source",
"codex_notes": "Good spelunker / wet-biome source"
},
{
"source_block": "minecraft:cave_vines",
"source_family": "humid_flora",
"biome_focus": "lush_caves",
"possible_variants": [
"moss"
],
"base_seed_drop_chance": 0.06,
"extra_conditions": "Humidity source",
"codex_notes": "Optional extra source if desired"
},
{
"source_block": "minecraft:crimson_roots",
"source_family": "nether_native",
"biome_focus": "crimson_forest; nether_wastes",
"possible_variants": [
"ember"
],
"base_seed_drop_chance": 0.02,
"extra_conditions": "Nether only",
"codex_notes": "Forced ember result"
},
{
"source_block": "minecraft:warped_roots",
"source_family": "nether_native",
"biome_focus": "warped_forest; basalt_deltas",
"possible_variants": [
"ember"
],
"base_seed_drop_chance": 0.02,
"extra_conditions": "Nether only",
"codex_notes": "Forced ember result"
},
{
"source_block": "minecraft:nether_sprouts",
"source_family": "nether_native",
"biome_focus": "soul_sand_valley; basalt_deltas",
"possible_variants": [
"ember"
],
"base_seed_drop_chance": 0.015,
"extra_conditions": "Nether only",
"codex_notes": "Forced ember result"
},
{
"source_block": "minecraft:weeping_vines",
"source_family": "nether_native",
"biome_focus": "crimson_forest",
"possible_variants": [
"ember"
],
"base_seed_drop_chance": 0.01,
"extra_conditions": "Nether only",
"codex_notes": "Forced ember result"
},
{
"source_block": "minecraft:twisting_vines",
"source_family": "nether_native",
"biome_focus": "warped_forest; soul_sand_valley",
"possible_variants": [
"ember"
],
"base_seed_drop_chance": 0.01,
"extra_conditions": "Nether only",
"codex_notes": "Forced ember result"
},
{
"source_block": "minecraft:chorus_plant",
"source_family": "end_native",
"biome_focus": "end_highlands; end_midlands; small_end_islands; end_barrens",
"possible_variants": [
"indigo"
],
"base_seed_drop_chance": 0.015,
"extra_conditions": "End only",
"codex_notes": "Forced indigo result"
},
{
"source_block": "minecraft:chorus_flower",
"source_family": "end_native",
"biome_focus": "end_highlands; end_midlands; small_end_islands; end_barrens",
"possible_variants": [
"indigo"
],
"base_seed_drop_chance": 0.02,
"extra_conditions": "End only",
"codex_notes": "Forced indigo result"
}
],
"hybrid_rules": [
{
"result_variant": "gold",
"parent_a": "tender",
"parent_b": "olive",
"rule_type": "hybrid",
"environment_requirement": "warm biome; sky visible; daytime",
"harvest_requirement": "Both parents mature and adjacent (radius 1)",
"result_seed_chance": 0.12,
"codex_notes": "Bright dry hybrid"
},
{
"result_variant": "purple",
"parent_a": "dark",
"parent_b": "moss",
"rule_type": "hybrid",
"environment_requirement": "humid area; low light",
"harvest_requirement": "Both parents mature and adjacent (radius 1)",
"result_seed_chance": 0.08,
"codex_notes": "Shadow-humidity hybrid"
},
{
"result_variant": "pink",
"parent_a": "tender",
"parent_b": "purple",
"rule_type": "hybrid",
"environment_requirement": "flower biome or greenhouse; dawn or dusk",
"harvest_requirement": "Both parents mature and adjacent (radius 1)",
"result_seed_chance": 0.1,
"codex_notes": "Floral color hybrid"
},
{
"result_variant": "coral",
"parent_a": "amber",
"parent_b": "moss",
"rule_type": "hybrid",
"environment_requirement": "warm biome; farmland touching water",
"harvest_requirement": "Both parents mature and adjacent (radius 1)",
"result_seed_chance": 0.1,
"codex_notes": "Warm coastal hybrid"
},
{
"result_variant": "bluish",
"parent_a": "ashen",
"parent_b": "tender",
"rule_type": "hybrid",
"environment_requirement": "cold biome or snow/ice nearby",
"harvest_requirement": "Both parents mature and adjacent (radius 1)",
"result_seed_chance": 0.09,
"codex_notes": "Cold color hybrid"
},
{
"result_variant": "varieg",
"parent_a": "any",
"parent_b": "different_any",
"rule_type": "mutation",
"environment_requirement": "humid biome; especially lush_caves",
"harvest_requirement": "Two distinct mature variants adjacent (radius 1)",
"result_seed_chance": 0.06,
"codex_notes": "Collector mutation"
}
],
"special_rules": [
{
"variant_key": "lunar",
"rule_type": "special_harvest",
"source": "ashen or bluish",
"condition": "Harvest mature parent at night with sky visible in a cold biome",
"result_chance": 0.06,
"codex_notes": "Night-only special roll; no full moon requirement"
},
{
"variant_key": "ember",
"rule_type": "dimension_lock",
"source": "nether_native_plants",
"condition": "Break eligible Nether flora in any Nether biome",
"result_chance": 1.0,
"codex_notes": "Forced result in Nether source table; never obtainable elsewhere"
},
{
"variant_key": "indigo",
"rule_type": "dimension_lock",
"source": "chorus_plant or chorus_flower",
"condition": "Break eligible End flora in any End biome",
"result_chance": 1.0,
"codex_notes": "Forced result in End source table; never obtainable elsewhere"
}
]
}
@@ -0,0 +1,29 @@
# Canaplia survival obtainment tables for Codex
These files describe how already-implemented hemp crops can be obtained in survival.
## Design summary
- The crop blocks and item variants already exist in the mod.
- The missing part is the survival obtainment loop.
- Wild green variants are discovered from biome flora.
- Colorful variants mostly come from breeding / mutation.
- `lunar` is a timed special harvest.
- `ember` is Nether-only.
- `indigo` is End-only.
## Files
- `canaplia_codex_variants_catalog.csv`: one row per variant
- `canaplia_codex_discovery_sources.csv`: world blocks / plants that can drop seeds
- `canaplia_codex_hybrid_rules.csv`: breeding and mutation outputs
- `canaplia_codex_special_rules.csv`: night / Nether / End rules
- `canaplia_codex_survival_obtainment.xlsx`: workbook version of the same data
## Intended Codex usage
Use these files as the survival design source-of-truth for:
- loot/drop tables for biome flora
- breeding logic between mature adjacent crops
- special timed harvest rules
- dimension-exclusive obtainment rules
## Internal key alignment
- Runtime enum keys must match the mod item/block ids: use `pink` instead of `rosy`, and `varieg` instead of `variegated`.
@@ -0,0 +1,17 @@
variant_key,variant_name_fr,family,obtainable_in_survival,first_obtention,source_blocks_or_plants,biome_focus,breeding_recipe,special_conditions,dimension_lock,codex_notes
common,Chanvre commun,wild_base,yes,Discover seeds by breaking short grass or tall grass,minecraft:short_grass; minecraft:tall_grass,plains; forest; riverbanks,,No special condition,,Main starter line. Use as fallback green strain.
tender,Chanvre tendre,wild_base,yes,Discover seeds by breaking short grass or tall grass,minecraft:short_grass; minecraft:tall_grass,meadow; flower_forest; cherry_grove,,Best as a bright / floral base strain,,Soft green line used in several hybrid recipes.
olive,Chanvre olive,wild_base,yes,Discover seeds by breaking grass or dry grass in warm biomes,minecraft:tall_grass; minecraft:short_grass; minecraft:tall_dry_grass; minecraft:short_dry_grass,savanna; windswept_savanna; desert_edge,,Warm / dry overworld discovery line,,Dry-climate green line.
dark,Chanvre sombre,wild_base,yes,Discover seeds by breaking ferns or shaded grass,minecraft:fern; minecraft:short_grass,dark_forest; old_growth_taiga; pale_garden,,Better in low light / canopy-heavy biomes,,Shade line; key parent for purple.
ashen,Chanvre cendré,wild_base,yes,Discover seeds by breaking ferns in cold biomes,minecraft:fern; minecraft:large_fern,snowy_taiga; grove; snowy_plains,,Cold-climate base line,,Cold green line; key parent for bluish and lunar.
moss,Chanvre mousse,wild_base,yes,Discover seeds from humid cave / swamp flora,minecraft:moss_carpet; minecraft:hanging_roots; minecraft:cave_vines,lush_caves; swamp; mangrove_swamp,,Humidity-heavy discovery line,,"Humid green line; key parent for purple, coral, varieg."
amber,Chanvre ambré,wild_rare,yes,Rare seed from dead bushes,minecraft:dead_bush,badlands; eroded_badlands; wooded_badlands,,Rare wild discovery in hot mineral biomes,,Dry rare line; parent for coral.
gold,Chanvre doré,hybrid,yes,Obtain as hybrid seed,,warm_biomes; bright_surface_fields,tender + olive,Mature parents adjacent (radius 1); harvest by day with sky visible,,Colorful hybrid from bright and dry green parents.
pink,Chanvre rosé,hybrid,yes,Obtain as hybrid seed,,flower_forest; meadow; cherry_grove,tender + purple,Mature parents adjacent (radius 1); harvest at dawn or dusk,,Stylized floral hybrid.
coral,Chanvre corail,hybrid,yes,Obtain as hybrid seed,,mangrove_edge; beach; warm_coast,amber + moss,Mature parents adjacent (radius 1); at least 1 water block touching farmland,,Warm wet hybrid.
purple,Chanvre pourpre,hybrid,yes,Obtain as hybrid seed,,humid_shaded_fields; swamp; dark_forest,dark + moss,Mature parents adjacent (radius 1); low light and humidity,,Shadow-humidity hybrid.
varieg,Chanvre panaché,hybrid,yes,Obtain as mutation seed,,lush_caves; jungle; swamp_greenhouse,any_two_distinct_variants,Two distinct mature parents adjacent (radius 1); humid biome,,Mutation-style collector variant.
bluish,Chanvre bleuté,hybrid,yes,Obtain as hybrid seed,,cold_fields; grove; ice_edge,ashen + tender,Mature parents adjacent (radius 1); cold biome or snow/ice nearby,,"Cold hybrid, moved out of wild discovery to keep survival logic coherent."
lunar,Chanvre lunaire,circumstantial,yes,Special night harvest,,cold_open_sky_biomes,ashen or bluish -> lunar seed roll,"Harvest mature ashen or bluish plant at night, with sky visible, in a cold biome",,Not found directly; obtained through timed harvest.
ember,Chanvre braise,dimensional,yes,Break native Nether flora,minecraft:crimson_roots; minecraft:warped_roots; minecraft:nether_sprouts; minecraft:weeping_vines; minecraft:twisting_vines,all_nether_biomes,,Nether-only. Never drops outside the Nether.,nether,Dimension identity strain.
indigo,Chanvre indigo,dimensional,yes,Break chorus plants or chorus flowers,minecraft:chorus_plant; minecraft:chorus_flower,all_end_biomes,,End-only. Never drops outside the End.,end,Dimension identity strain.
1 variant_key variant_name_fr family obtainable_in_survival first_obtention source_blocks_or_plants biome_focus breeding_recipe special_conditions dimension_lock codex_notes
2 common Chanvre commun wild_base yes Discover seeds by breaking short grass or tall grass minecraft:short_grass; minecraft:tall_grass plains; forest; riverbanks No special condition Main starter line. Use as fallback green strain.
3 tender Chanvre tendre wild_base yes Discover seeds by breaking short grass or tall grass minecraft:short_grass; minecraft:tall_grass meadow; flower_forest; cherry_grove Best as a bright / floral base strain Soft green line used in several hybrid recipes.
4 olive Chanvre olive wild_base yes Discover seeds by breaking grass or dry grass in warm biomes minecraft:tall_grass; minecraft:short_grass; minecraft:tall_dry_grass; minecraft:short_dry_grass savanna; windswept_savanna; desert_edge Warm / dry overworld discovery line Dry-climate green line.
5 dark Chanvre sombre wild_base yes Discover seeds by breaking ferns or shaded grass minecraft:fern; minecraft:short_grass dark_forest; old_growth_taiga; pale_garden Better in low light / canopy-heavy biomes Shade line; key parent for purple.
6 ashen Chanvre cendré wild_base yes Discover seeds by breaking ferns in cold biomes minecraft:fern; minecraft:large_fern snowy_taiga; grove; snowy_plains Cold-climate base line Cold green line; key parent for bluish and lunar.
7 moss Chanvre mousse wild_base yes Discover seeds from humid cave / swamp flora minecraft:moss_carpet; minecraft:hanging_roots; minecraft:cave_vines lush_caves; swamp; mangrove_swamp Humidity-heavy discovery line Humid green line; key parent for purple, coral, varieg.
8 amber Chanvre ambré wild_rare yes Rare seed from dead bushes minecraft:dead_bush badlands; eroded_badlands; wooded_badlands Rare wild discovery in hot mineral biomes Dry rare line; parent for coral.
9 gold Chanvre doré hybrid yes Obtain as hybrid seed warm_biomes; bright_surface_fields tender + olive Mature parents adjacent (radius 1); harvest by day with sky visible Colorful hybrid from bright and dry green parents.
10 pink Chanvre rosé hybrid yes Obtain as hybrid seed flower_forest; meadow; cherry_grove tender + purple Mature parents adjacent (radius 1); harvest at dawn or dusk Stylized floral hybrid.
11 coral Chanvre corail hybrid yes Obtain as hybrid seed mangrove_edge; beach; warm_coast amber + moss Mature parents adjacent (radius 1); at least 1 water block touching farmland Warm wet hybrid.
12 purple Chanvre pourpre hybrid yes Obtain as hybrid seed humid_shaded_fields; swamp; dark_forest dark + moss Mature parents adjacent (radius 1); low light and humidity Shadow-humidity hybrid.
13 varieg Chanvre panaché hybrid yes Obtain as mutation seed lush_caves; jungle; swamp_greenhouse any_two_distinct_variants Two distinct mature parents adjacent (radius 1); humid biome Mutation-style collector variant.
14 bluish Chanvre bleuté hybrid yes Obtain as hybrid seed cold_fields; grove; ice_edge ashen + tender Mature parents adjacent (radius 1); cold biome or snow/ice nearby Cold hybrid, moved out of wild discovery to keep survival logic coherent.
15 lunar Chanvre lunaire circumstantial yes Special night harvest cold_open_sky_biomes ashen or bluish -> lunar seed roll Harvest mature ashen or bluish plant at night, with sky visible, in a cold biome Not found directly; obtained through timed harvest.
16 ember Chanvre braise dimensional yes Break native Nether flora minecraft:crimson_roots; minecraft:warped_roots; minecraft:nether_sprouts; minecraft:weeping_vines; minecraft:twisting_vines all_nether_biomes Nether-only. Never drops outside the Nether. nether Dimension identity strain.
17 indigo Chanvre indigo dimensional yes Break chorus plants or chorus flowers minecraft:chorus_plant; minecraft:chorus_flower all_end_biomes End-only. Never drops outside the End. end Dimension identity strain.
+160
View File
@@ -0,0 +1,160 @@
# Hemp Effect System
## Goal
Each hemp variety applies:
1. a main gameplay profile,
2. a subtle visual profile during the normal trip,
3. a possible bad trip with stronger visual disturbance.
Normal trip must stay readable and non-invasive.
Bad trip is the only state allowed to become truly disturbing.
---
## Core rules
### Consumption flow
When the player consumes a hemp item:
1. Resolve the consumed variety from its `id`.
2. Apply all `main_effects` immediately.
3. Enable the `normal_visual_profile`.
4. Roll one bad trip chance using `bad_trip_chance`.
5. If the roll succeeds, schedule the bad trip after a random delay in `bad_trip_delay_sec`.
6. When the delay expires, apply `bad_trip_effects` and enable `bad_trip_visual_profile`.
7. When the main duration ends, clear normal visuals and gameplay effects.
8. When the bad trip duration ends, clear bad trip visuals and gameplay effects.
---
## Design constraints
### Normal trip
Normal trip should:
- remain comfortable for 2 to 3 minutes,
- never make HUD unreadable,
- never make combat or building frustrating,
- use only light visual treatment.
Allowed normal visual changes:
- subtle tint,
- mild saturation shift,
- mild contrast shift,
- very light vignette,
- very light ghosting.
Forbidden during normal trip:
- strong blur,
- strong wobble,
- aggressive FOV change,
- strong double vision,
- strong screen shake.
### Bad trip
Bad trip should:
- feel clearly worse,
- remain shorter than the main trip,
- include nausea and hunger,
- be visually more unstable.
Allowed bad trip visual changes:
- stronger tint,
- stronger vignette,
- stronger saturation shift,
- light image wobble,
- light afterimage,
- light brightness pulsing.
---
## Stacking rules
Current stacking model:
- Consuming the same variety refreshes that variety's duration.
- Consuming a different variety keeps the previous gameplay effects active and adds the new variety on top.
- Main gameplay effects from several varieties can coexist at the same time.
- A new manual consumption rerolls the bad trip entirely from the latest variety.
- Passive resin aura refresh keeps extending the same variety without rerolling the bad trip every tick.
- Only one normal hemp visual profile can be active at a time: the latest active variety drives visuals.
- Only one bad trip state can be active at a time, and a reroll replaces the previous pending or active bad trip.
---
## Effect conventions
### Main effect duration
Main trip duration should stay between 140 and 180 seconds.
### Bad trip duration
Bad trip duration should stay between 25 and 40 seconds.
### Bad trip base gameplay package
All bad trips contain at least:
- Nausea
- Hunger
Rare and very rare varieties may add:
- Weakness
- Slowness
- short Blindness
Use these sparingly.
---
## Data format
Each variety entry contains:
- `id`: stable machine id
- `name_fr`: display name
- `rarity`: common, uncommon, rare, very_rare
- `color_family`: readable color identity
- `main_duration_sec`: main trip duration
- `main_effects`: list of vanilla effects
- `normal_visual_profile`: subtle visual profile
- `bad_trip_chance`: float from 0.0 to 1.0
- `bad_trip_delay_sec`: min/max delay before bad trip starts
- `bad_trip_duration_sec`: bad trip duration
- `bad_trip_effects`: gameplay penalty package
- `bad_trip_visual_profile`: stronger visual profile
---
## Visual profile semantics
### Intensity scale
Use a 0.0 to 1.0 scale:
- 0.05 to 0.15 = subtle
- 0.20 to 0.35 = noticeable
- 0.40+ = invasive
Normal trip should usually stay in 0.08 to 0.14.
Bad trip may go to 0.25 to 0.50 depending on rarity.
### Suggested shader parameters
A visual profile can drive:
- `tint_strength`
- `saturation_delta`
- `contrast_delta`
- `vignette_strength`
- `ghosting_strength`
- `wobble_strength`
- `pulse_strength`
Not every profile needs every parameter.
---
## Implementation note
The system should be data-driven:
- all plant tuning lives in JSON,
- gameplay code reads the JSON,
- visuals are picked by profile name or inline values,
- item classes should not hardcode effect logic.
This allows easy balancing without rewriting gameplay code.
+601
View File
@@ -0,0 +1,601 @@
{
"version": 1,
"bad_trip_base_rules": {
"applies_nausea": true,
"applies_hunger": true,
"normal_trip_visual_intensity_target": [0.08, 0.14],
"bad_trip_visual_intensity_target": [0.25, 0.50]
},
"varieties": [
{
"id": "chanvre_commun",
"name_fr": "Chanvre commun",
"rarity": "common",
"color_family": "natural_green",
"main_duration_sec": 160,
"main_effects": [
{ "effect": "minecraft:resistance", "amplifier": 0, "duration_sec": 160 },
{ "effect": "minecraft:slowness", "amplifier": 0, "duration_sec": 160 },
{ "effect": "minecraft:haste", "amplifier": 0, "duration_sec": 160 },
{ "effect": "minecraft:strength", "amplifier": 0, "duration_sec": 160 }
],
"normal_visual_profile": {
"palette": "soft_green",
"intensity": 0.09,
"tint_strength": 0.05,
"saturation_delta": 0.03,
"contrast_delta": 0.02,
"vignette_strength": 0.03
},
"bad_trip_chance": 0.10,
"bad_trip_delay_sec": { "min": 18, "max": 32 },
"bad_trip_duration_sec": 28,
"bad_trip_effects": [
{ "effect": "minecraft:nausea", "amplifier": 0, "duration_sec": 28 },
{ "effect": "minecraft:hunger", "amplifier": 0, "duration_sec": 32 }
],
"bad_trip_visual_profile": {
"palette": "dirty_green",
"intensity": 0.26,
"tint_strength": 0.10,
"saturation_delta": -0.05,
"contrast_delta": 0.04,
"vignette_strength": 0.10,
"ghosting_strength": 0.06
}
},
{
"id": "chanvre_tendre",
"name_fr": "Chanvre tendre",
"rarity": "common",
"color_family": "fresh_light_green",
"main_duration_sec": 170,
"main_effects": [
{ "effect": "minecraft:speed", "amplifier": 0, "duration_sec": 170 },
{ "effect": "minecraft:jump_boost", "amplifier": 0, "duration_sec": 170 },
{ "effect": "minecraft:weakness", "amplifier": 0, "duration_sec": 170 }
],
"normal_visual_profile": {
"palette": "fresh_green",
"intensity": 0.08,
"tint_strength": 0.04,
"saturation_delta": 0.04,
"contrast_delta": 0.01,
"vignette_strength": 0.02
},
"bad_trip_chance": 0.09,
"bad_trip_delay_sec": { "min": 20, "max": 35 },
"bad_trip_duration_sec": 26,
"bad_trip_effects": [
{ "effect": "minecraft:nausea", "amplifier": 0, "duration_sec": 26 },
{ "effect": "minecraft:hunger", "amplifier": 0, "duration_sec": 30 }
],
"bad_trip_visual_profile": {
"palette": "sour_green",
"intensity": 0.24,
"tint_strength": 0.09,
"saturation_delta": -0.04,
"vignette_strength": 0.08,
"ghosting_strength": 0.05
}
},
{
"id": "chanvre_olive",
"name_fr": "Chanvre olive",
"rarity": "common",
"color_family": "olive_green",
"main_duration_sec": 155,
"main_effects": [
{ "effect": "minecraft:haste", "amplifier": 0, "duration_sec": 155 },
{ "effect": "minecraft:luck", "amplifier": 0, "duration_sec": 155 },
{ "effect": "minecraft:hunger", "amplifier": 0, "duration_sec": 155 }
],
"normal_visual_profile": {
"palette": "olive_matte",
"intensity": 0.09,
"tint_strength": 0.05,
"saturation_delta": -0.01,
"contrast_delta": 0.02,
"vignette_strength": 0.03
},
"bad_trip_chance": 0.12,
"bad_trip_delay_sec": { "min": 16, "max": 30 },
"bad_trip_duration_sec": 30,
"bad_trip_effects": [
{ "effect": "minecraft:nausea", "amplifier": 0, "duration_sec": 30 },
{ "effect": "minecraft:hunger", "amplifier": 1, "duration_sec": 34 }
],
"bad_trip_visual_profile": {
"palette": "mustard_sick",
"intensity": 0.28,
"tint_strength": 0.11,
"saturation_delta": -0.08,
"vignette_strength": 0.10,
"ghosting_strength": 0.06
}
},
{
"id": "chanvre_sombre",
"name_fr": "Chanvre sombre",
"rarity": "common",
"color_family": "deep_forest_green",
"main_duration_sec": 165,
"main_effects": [
{ "effect": "minecraft:night_vision", "amplifier": 0, "duration_sec": 165 },
{ "effect": "minecraft:strength", "amplifier": 0, "duration_sec": 165 },
{ "effect": "minecraft:slowness", "amplifier": 0, "duration_sec": 165 }
],
"normal_visual_profile": {
"palette": "deep_green",
"intensity": 0.10,
"tint_strength": 0.06,
"saturation_delta": -0.01,
"contrast_delta": 0.03,
"vignette_strength": 0.04
},
"bad_trip_chance": 0.12,
"bad_trip_delay_sec": { "min": 18, "max": 34 },
"bad_trip_duration_sec": 30,
"bad_trip_effects": [
{ "effect": "minecraft:nausea", "amplifier": 0, "duration_sec": 30 },
{ "effect": "minecraft:hunger", "amplifier": 0, "duration_sec": 34 }
],
"bad_trip_visual_profile": {
"palette": "swamp_dark",
"intensity": 0.29,
"tint_strength": 0.12,
"saturation_delta": -0.06,
"contrast_delta": 0.05,
"vignette_strength": 0.12,
"ghosting_strength": 0.05
}
},
{
"id": "chanvre_cendre",
"name_fr": "Chanvre cendré",
"rarity": "common",
"color_family": "ashy_green",
"main_duration_sec": 150,
"main_effects": [
{ "effect": "minecraft:slow_falling", "amplifier": 0, "duration_sec": 150 },
{ "effect": "minecraft:weakness", "amplifier": 0, "duration_sec": 150 }
],
"normal_visual_profile": {
"palette": "ash_green",
"intensity": 0.09,
"tint_strength": 0.04,
"saturation_delta": -0.05,
"contrast_delta": -0.01,
"vignette_strength": 0.03,
"ghosting_strength": 0.03
},
"bad_trip_chance": 0.14,
"bad_trip_delay_sec": { "min": 14, "max": 28 },
"bad_trip_duration_sec": 32,
"bad_trip_effects": [
{ "effect": "minecraft:nausea", "amplifier": 0, "duration_sec": 32 },
{ "effect": "minecraft:hunger", "amplifier": 1, "duration_sec": 34 }
],
"bad_trip_visual_profile": {
"palette": "dusty_gray_green",
"intensity": 0.30,
"tint_strength": 0.10,
"saturation_delta": -0.12,
"contrast_delta": -0.03,
"vignette_strength": 0.11,
"ghosting_strength": 0.08,
"wobble_strength": 0.04
}
},
{
"id": "chanvre_mousse",
"name_fr": "Chanvre mousse",
"rarity": "common",
"color_family": "moss_green",
"main_duration_sec": 175,
"main_effects": [
{ "effect": "minecraft:water_breathing", "amplifier": 0, "duration_sec": 175 },
{ "effect": "minecraft:regeneration", "amplifier": 0, "duration_sec": 175 },
{ "effect": "minecraft:slowness", "amplifier": 0, "duration_sec": 175 }
],
"normal_visual_profile": {
"palette": "wet_moss",
"intensity": 0.09,
"tint_strength": 0.05,
"saturation_delta": 0.01,
"contrast_delta": -0.01,
"vignette_strength": 0.03
},
"bad_trip_chance": 0.11,
"bad_trip_delay_sec": { "min": 20, "max": 36 },
"bad_trip_duration_sec": 28,
"bad_trip_effects": [
{ "effect": "minecraft:nausea", "amplifier": 0, "duration_sec": 28 },
{ "effect": "minecraft:hunger", "amplifier": 0, "duration_sec": 32 }
],
"bad_trip_visual_profile": {
"palette": "bog_green",
"intensity": 0.27,
"tint_strength": 0.10,
"saturation_delta": -0.04,
"contrast_delta": -0.02,
"vignette_strength": 0.09,
"ghosting_strength": 0.06
}
},
{
"id": "chanvre_dore",
"name_fr": "Chanvre doré",
"rarity": "uncommon",
"color_family": "golden_green",
"main_duration_sec": 150,
"main_effects": [
{ "effect": "minecraft:absorption", "amplifier": 0, "duration_sec": 150 },
{ "effect": "minecraft:haste", "amplifier": 1, "duration_sec": 150 },
{ "effect": "minecraft:speed", "amplifier": 0, "duration_sec": 150 }
],
"normal_visual_profile": {
"palette": "soft_gold",
"intensity": 0.11,
"tint_strength": 0.06,
"saturation_delta": 0.05,
"contrast_delta": 0.03,
"vignette_strength": 0.03
},
"bad_trip_chance": 0.15,
"bad_trip_delay_sec": { "min": 15, "max": 28 },
"bad_trip_duration_sec": 30,
"bad_trip_effects": [
{ "effect": "minecraft:nausea", "amplifier": 0, "duration_sec": 30 },
{ "effect": "minecraft:hunger", "amplifier": 1, "duration_sec": 34 }
],
"bad_trip_visual_profile": {
"palette": "toxic_yellow",
"intensity": 0.32,
"tint_strength": 0.13,
"saturation_delta": 0.03,
"contrast_delta": 0.06,
"vignette_strength": 0.11,
"pulse_strength": 0.05
}
},
{
"id": "chanvre_ambre",
"name_fr": "Chanvre ambré",
"rarity": "uncommon",
"color_family": "amber_yellow_brown",
"main_duration_sec": 155,
"main_effects": [
{ "effect": "minecraft:fire_resistance", "amplifier": 0, "duration_sec": 155 },
{ "effect": "minecraft:strength", "amplifier": 0, "duration_sec": 155 },
{ "effect": "minecraft:hunger", "amplifier": 0, "duration_sec": 155 }
],
"normal_visual_profile": {
"palette": "warm_amber",
"intensity": 0.11,
"tint_strength": 0.06,
"saturation_delta": 0.03,
"contrast_delta": 0.03,
"vignette_strength": 0.03
},
"bad_trip_chance": 0.16,
"bad_trip_delay_sec": { "min": 14, "max": 28 },
"bad_trip_duration_sec": 32,
"bad_trip_effects": [
{ "effect": "minecraft:nausea", "amplifier": 0, "duration_sec": 32 },
{ "effect": "minecraft:hunger", "amplifier": 1, "duration_sec": 34 }
],
"bad_trip_visual_profile": {
"palette": "burnt_amber",
"intensity": 0.33,
"tint_strength": 0.14,
"saturation_delta": 0.01,
"contrast_delta": 0.05,
"vignette_strength": 0.12,
"pulse_strength": 0.06
}
},
{
"id": "chanvre_bleute",
"name_fr": "Chanvre bleuté",
"rarity": "uncommon",
"color_family": "cool_blue_green",
"main_duration_sec": 165,
"main_effects": [
{ "effect": "minecraft:night_vision", "amplifier": 0, "duration_sec": 165 },
{ "effect": "minecraft:water_breathing", "amplifier": 0, "duration_sec": 165 },
{ "effect": "minecraft:speed", "amplifier": 0, "duration_sec": 165 }
],
"normal_visual_profile": {
"palette": "cool_blue",
"intensity": 0.10,
"tint_strength": 0.05,
"saturation_delta": 0.01,
"contrast_delta": 0.02,
"vignette_strength": 0.02
},
"bad_trip_chance": 0.14,
"bad_trip_delay_sec": { "min": 18, "max": 30 },
"bad_trip_duration_sec": 29,
"bad_trip_effects": [
{ "effect": "minecraft:nausea", "amplifier": 0, "duration_sec": 29 },
{ "effect": "minecraft:hunger", "amplifier": 0, "duration_sec": 33 }
],
"bad_trip_visual_profile": {
"palette": "cold_cyan",
"intensity": 0.30,
"tint_strength": 0.12,
"saturation_delta": -0.02,
"contrast_delta": 0.05,
"vignette_strength": 0.10,
"ghosting_strength": 0.07
}
},
{
"id": "chanvre_rose",
"name_fr": "Chanvre rosé",
"rarity": "uncommon",
"color_family": "soft_plant_pink",
"main_duration_sec": 175,
"main_effects": [
{ "effect": "minecraft:regeneration", "amplifier": 0, "duration_sec": 175 },
{ "effect": "minecraft:slowness", "amplifier": 0, "duration_sec": 175 }
],
"normal_visual_profile": {
"palette": "soft_pink",
"intensity": 0.09,
"tint_strength": 0.04,
"saturation_delta": 0.03,
"contrast_delta": 0.01,
"vignette_strength": 0.02
},
"bad_trip_chance": 0.15,
"bad_trip_delay_sec": { "min": 20, "max": 35 },
"bad_trip_duration_sec": 30,
"bad_trip_effects": [
{ "effect": "minecraft:nausea", "amplifier": 0, "duration_sec": 30 },
{ "effect": "minecraft:hunger", "amplifier": 1, "duration_sec": 34 }
],
"bad_trip_visual_profile": {
"palette": "sickly_pink",
"intensity": 0.31,
"tint_strength": 0.12,
"saturation_delta": -0.01,
"vignette_strength": 0.10,
"ghosting_strength": 0.06
}
},
{
"id": "chanvre_corail",
"name_fr": "Chanvre corail",
"rarity": "uncommon",
"color_family": "warm_coral",
"main_duration_sec": 150,
"main_effects": [
{ "effect": "minecraft:dolphins_grace", "amplifier": 0, "duration_sec": 150 },
{ "effect": "minecraft:water_breathing", "amplifier": 0, "duration_sec": 150 },
{ "effect": "minecraft:jump_boost", "amplifier": 0, "duration_sec": 150 }
],
"normal_visual_profile": {
"palette": "coral_warm",
"intensity": 0.11,
"tint_strength": 0.05,
"saturation_delta": 0.04,
"contrast_delta": 0.02,
"vignette_strength": 0.02
},
"bad_trip_chance": 0.17,
"bad_trip_delay_sec": { "min": 14, "max": 26 },
"bad_trip_duration_sec": 31,
"bad_trip_effects": [
{ "effect": "minecraft:nausea", "amplifier": 0, "duration_sec": 31 },
{ "effect": "minecraft:hunger", "amplifier": 1, "duration_sec": 35 }
],
"bad_trip_visual_profile": {
"palette": "burnt_coral",
"intensity": 0.33,
"tint_strength": 0.13,
"saturation_delta": 0.00,
"contrast_delta": 0.05,
"vignette_strength": 0.11,
"ghosting_strength": 0.06,
"pulse_strength": 0.05
}
},
{
"id": "chanvre_pourpre",
"name_fr": "Chanvre pourpre",
"rarity": "rare",
"color_family": "deep_red_violet",
"main_duration_sec": 145,
"main_effects": [
{ "effect": "minecraft:strength", "amplifier": 1, "duration_sec": 145 },
{ "effect": "minecraft:resistance", "amplifier": 0, "duration_sec": 145 }
],
"normal_visual_profile": {
"palette": "mystic_purple",
"intensity": 0.12,
"tint_strength": 0.06,
"saturation_delta": 0.05,
"contrast_delta": 0.03,
"vignette_strength": 0.03
},
"bad_trip_chance": 0.22,
"bad_trip_delay_sec": { "min": 12, "max": 24 },
"bad_trip_duration_sec": 34,
"bad_trip_effects": [
{ "effect": "minecraft:nausea", "amplifier": 1, "duration_sec": 34 },
{ "effect": "minecraft:hunger", "amplifier": 1, "duration_sec": 36 },
{ "effect": "minecraft:weakness", "amplifier": 0, "duration_sec": 24 }
],
"bad_trip_visual_profile": {
"palette": "corrupted_magenta",
"intensity": 0.40,
"tint_strength": 0.17,
"saturation_delta": 0.08,
"contrast_delta": 0.07,
"vignette_strength": 0.14,
"ghosting_strength": 0.10,
"wobble_strength": 0.05
}
},
{
"id": "chanvre_indigo",
"name_fr": "Chanvre indigo",
"rarity": "rare",
"color_family": "dark_blue_violet",
"main_duration_sec": 145,
"main_effects": [
{ "effect": "minecraft:invisibility", "amplifier": 0, "duration_sec": 145 },
{ "effect": "minecraft:night_vision", "amplifier": 0, "duration_sec": 145 },
{ "effect": "minecraft:weakness", "amplifier": 0, "duration_sec": 145 }
],
"normal_visual_profile": {
"palette": "deep_indigo",
"intensity": 0.12,
"tint_strength": 0.06,
"saturation_delta": -0.01,
"contrast_delta": 0.04,
"vignette_strength": 0.04
},
"bad_trip_chance": 0.21,
"bad_trip_delay_sec": { "min": 12, "max": 25 },
"bad_trip_duration_sec": 34,
"bad_trip_effects": [
{ "effect": "minecraft:nausea", "amplifier": 1, "duration_sec": 34 },
{ "effect": "minecraft:hunger", "amplifier": 1, "duration_sec": 36 },
{ "effect": "minecraft:weakness", "amplifier": 0, "duration_sec": 24 }
],
"bad_trip_visual_profile": {
"palette": "void_indigo",
"intensity": 0.39,
"tint_strength": 0.16,
"saturation_delta": -0.05,
"contrast_delta": 0.08,
"vignette_strength": 0.15,
"ghosting_strength": 0.11,
"wobble_strength": 0.04
}
},
{
"id": "chanvre_panache",
"name_fr": "Chanvre panaché",
"rarity": "rare",
"color_family": "mixed_pale_green",
"main_duration_sec": 160,
"main_effects_mode": "random_pick",
"main_effects_pool": [
{ "effect": "minecraft:speed", "amplifier": 0, "duration_sec": 160 },
{ "effect": "minecraft:haste", "amplifier": 0, "duration_sec": 160 },
{ "effect": "minecraft:regeneration", "amplifier": 0, "duration_sec": 160 },
{ "effect": "minecraft:night_vision", "amplifier": 0, "duration_sec": 160 }
],
"main_effects_pick_count": 2,
"normal_visual_profile": {
"palette": "pale_mixed_green",
"intensity": 0.11,
"tint_strength": 0.05,
"saturation_delta": 0.01,
"contrast_delta": 0.02,
"vignette_strength": 0.03,
"ghosting_strength": 0.03
},
"bad_trip_chance": 0.20,
"bad_trip_delay_sec": { "min": 13, "max": 27 },
"bad_trip_duration_sec": 33,
"bad_trip_effects": [
{ "effect": "minecraft:nausea", "amplifier": 1, "duration_sec": 33 },
{ "effect": "minecraft:hunger", "amplifier": 1, "duration_sec": 36 }
],
"bad_trip_visual_profile": {
"palette": "unstable_mixed",
"intensity": 0.36,
"tint_strength": 0.14,
"saturation_delta": -0.02,
"contrast_delta": 0.04,
"vignette_strength": 0.12,
"ghosting_strength": 0.10,
"wobble_strength": 0.05
}
},
{
"id": "chanvre_lunaire",
"name_fr": "Chanvre lunaire",
"rarity": "very_rare",
"color_family": "pale_silver_green",
"main_duration_sec": 150,
"main_effects": [
{ "effect": "minecraft:jump_boost", "amplifier": 1, "duration_sec": 150 },
{ "effect": "minecraft:slow_falling", "amplifier": 0, "duration_sec": 150 },
{ "effect": "minecraft:night_vision", "amplifier": 0, "duration_sec": 150 }
],
"normal_visual_profile": {
"palette": "moon_pale",
"intensity": 0.13,
"tint_strength": 0.06,
"saturation_delta": -0.03,
"contrast_delta": 0.01,
"vignette_strength": 0.03,
"ghosting_strength": 0.03
},
"bad_trip_chance": 0.27,
"bad_trip_delay_sec": { "min": 10, "max": 22 },
"bad_trip_duration_sec": 36,
"bad_trip_effects": [
{ "effect": "minecraft:nausea", "amplifier": 1, "duration_sec": 36 },
{ "effect": "minecraft:hunger", "amplifier": 1, "duration_sec": 38 },
{ "effect": "minecraft:blindness", "amplifier": 0, "duration_sec": 4 }
],
"bad_trip_visual_profile": {
"palette": "overexposed_moon",
"intensity": 0.46,
"tint_strength": 0.18,
"saturation_delta": -0.10,
"contrast_delta": -0.02,
"vignette_strength": 0.13,
"ghosting_strength": 0.12,
"wobble_strength": 0.06,
"pulse_strength": 0.06
}
},
{
"id": "chanvre_braise",
"name_fr": "Chanvre braise",
"rarity": "very_rare",
"color_family": "dark_red_ember",
"main_duration_sec": 145,
"main_effects": [
{ "effect": "minecraft:fire_resistance", "amplifier": 0, "duration_sec": 145 },
{ "effect": "minecraft:strength", "amplifier": 1, "duration_sec": 145 },
{ "effect": "minecraft:haste", "amplifier": 1, "duration_sec": 145 }
],
"normal_visual_profile": {
"palette": "ember_warm",
"intensity": 0.13,
"tint_strength": 0.07,
"saturation_delta": 0.04,
"contrast_delta": 0.04,
"vignette_strength": 0.03
},
"bad_trip_chance": 0.30,
"bad_trip_delay_sec": { "min": 10, "max": 20 },
"bad_trip_duration_sec": 38,
"bad_trip_effects": [
{ "effect": "minecraft:nausea", "amplifier": 1, "duration_sec": 38 },
{ "effect": "minecraft:hunger", "amplifier": 2, "duration_sec": 40 },
{ "effect": "minecraft:weakness", "amplifier": 0, "duration_sec": 20 }
],
"bad_trip_visual_profile": {
"palette": "burning_red",
"intensity": 0.50,
"tint_strength": 0.20,
"saturation_delta": 0.06,
"contrast_delta": 0.08,
"vignette_strength": 0.16,
"ghosting_strength": 0.09,
"wobble_strength": 0.05,
"pulse_strength": 0.08
}
}
]
}
@@ -0,0 +1,162 @@
{
"layout": {
"tile_size": [
16,
16
],
"columns": 16,
"rows": 1,
"column_order": [
"common",
"tender",
"olive",
"dark",
"ashen",
"moss",
"gold",
"amber",
"bluish",
"pink",
"coral",
"purple",
"indigo",
"varieg",
"lunar",
"ember"
],
"row_order": [
"resin_block"
],
"note": "Raw sheet has no margins. Size: 256x16."
},
"variants": [
{
"key": "common",
"name": "Résine commune",
"rarity": "commun",
"h": 36,
"s": 1.0,
"v": 1.0
},
{
"key": "tender",
"name": "Résine tendre",
"rarity": "commun",
"h": 44,
"s": 0.88,
"v": 1.1
},
{
"key": "olive",
"name": "Résine olive",
"rarity": "commun",
"h": 78,
"s": 0.82,
"v": 0.96
},
{
"key": "dark",
"name": "Résine sombre",
"rarity": "commun",
"h": 28,
"s": 1.0,
"v": 0.72
},
{
"key": "ashen",
"name": "Résine cendrée",
"rarity": "commun",
"h": 34,
"s": 0.34,
"v": 0.92
},
{
"key": "moss",
"name": "Résine mousse",
"rarity": "commun",
"h": 112,
"s": 0.72,
"v": 0.82
},
{
"key": "gold",
"name": "Résine dorée",
"rarity": "peu commun",
"h": 52,
"s": 0.96,
"v": 1.12
},
{
"key": "amber",
"name": "Résine ambrée",
"rarity": "peu commun",
"h": 22,
"s": 1.0,
"v": 1.04
},
{
"key": "bluish",
"name": "Résine bleutée",
"rarity": "peu commun",
"h": 190,
"s": 0.7,
"v": 0.98
},
{
"key": "pink",
"name": "Résine rosée",
"rarity": "peu commun",
"h": 340,
"s": 0.62,
"v": 1.08
},
{
"key": "coral",
"name": "Résine corail",
"rarity": "peu commun",
"h": 10,
"s": 0.82,
"v": 1.1
},
{
"key": "purple",
"name": "Résine pourpre",
"rarity": "rare",
"h": 296,
"s": 0.82,
"v": 0.84
},
{
"key": "indigo",
"name": "Résine indigo",
"rarity": "rare",
"h": 246,
"s": 0.82,
"v": 0.84
},
{
"key": "varieg",
"name": "Résine panachée",
"rarity": "rare",
"h": 88,
"s": 0.88,
"v": 1.0
},
{
"key": "lunar",
"name": "Résine lunaire",
"rarity": "très rare",
"h": 72,
"s": 0.12,
"v": 1.22
},
{
"key": "ember",
"name": "Résine braise",
"rarity": "très rare",
"h": 4,
"s": 0.96,
"v": 0.92
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

+21
View File
@@ -0,0 +1,21 @@
# Done to increase the memory available to Gradle.
org.gradle.jvmargs=-Xmx1G
org.gradle.parallel=true
# IntelliJ IDEA is not yet fully compatible with configuration cache:
# https://github.com/FabricMC/fabric-loom/issues/1349
org.gradle.configuration-cache=false
# Fabric properties
# Check current versions on https://fabricmc.net/develop
minecraft_version=26.1.2
loader_version=0.19.2
loom_version=1.16-SNAPSHOT
# Mod properties
mod_version=2.0.0
maven_group=io.github.koka
archives_base_name=canaplia
# Dependencies
fabric_api_version=0.148.0+26.1.2
Binary file not shown.
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+253
View File
@@ -0,0 +1,253 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
if [ -z "$JAVA_HOME" ] && [ -x "$APP_HOME/.jdk/current/bin/java" ] ; then
JAVA_HOME=$APP_HOME/.jdk/current
export JAVA_HOME
fi
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
+93
View File
@@ -0,0 +1,93 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
File diff suppressed because one or more lines are too long
+12
View File
@@ -0,0 +1,12 @@
pluginManagement {
repositories {
maven {
name = "Fabric"
url = "https://maven.fabricmc.net/"
}
mavenCentral()
gradlePluginPortal()
}
}
rootProject.name = "canaplia"
@@ -0,0 +1,25 @@
package io.github.koka.canaplia;
import io.github.koka.canaplia.effect.HempVisualStateStore;
import io.github.koka.canaplia.effect.network.HempBadTripPayload;
import io.github.koka.canaplia.effect.network.HempVisualClearPayload;
import io.github.koka.canaplia.effect.network.HempVisualSyncPayload;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
public final class CanapliaClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
ClientPlayNetworking.registerGlobalReceiver(HempVisualSyncPayload.ID, (payload, context) ->
HempVisualStateStore.applyNormal(payload.profile(), payload.remainingTicks())
);
ClientPlayNetworking.registerGlobalReceiver(HempBadTripPayload.ID, (payload, context) ->
HempVisualStateStore.applyBadTrip(payload.profile(), payload.remainingTicks())
);
ClientPlayNetworking.registerGlobalReceiver(HempVisualClearPayload.ID, (payload, context) -> HempVisualStateStore.clear());
ClientTickEvents.END_CLIENT_TICK.register(client -> HempVisualStateStore.tick());
ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> HempVisualStateStore.clear());
}
}
@@ -0,0 +1,35 @@
package io.github.koka.canaplia;
import io.github.koka.canaplia.block.CanapliaBlocks;
import io.github.koka.canaplia.block.entity.CanapliaBlockEntityTypes;
import io.github.koka.canaplia.compost.HempBudComposterInteraction;
import io.github.koka.canaplia.effect.HempEffectCatalog;
import io.github.koka.canaplia.effect.HempEffectController;
import io.github.koka.canaplia.item.CanapliaItems;
import io.github.koka.canaplia.survival.CanapliaSurvivalObtainment;
import net.fabricmc.api.ModInitializer;
import net.minecraft.resources.Identifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class CanapliaMod implements ModInitializer {
public static final String MOD_ID = "canaplia";
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
public static Identifier id(String path) {
return Identifier.fromNamespaceAndPath(MOD_ID, path);
}
@Override
public void onInitialize() {
HempEffectCatalog.getDefault();
CanapliaBlocks.register();
CanapliaBlockEntityTypes.register();
CanapliaItems.register();
HempEffectController.register();
CanapliaSurvivalObtainment.register();
HempBudComposterInteraction.register();
LOGGER.info("Initialized {}", MOD_ID);
}
}
@@ -0,0 +1,61 @@
package io.github.koka.canaplia.block;
import io.github.koka.canaplia.CanapliaMod;
import io.github.koka.canaplia.hemp.HempVariant;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.PushReaction;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.core.Registry;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.block.SoundType;
public final class CanapliaBlocks {
public static final ResourceKey<Block> HEMP_CROP_KEY = ResourceKey.create(Registries.BLOCK, CanapliaMod.id("hemp_crop"));
public static final ResourceKey<Block> HEMP_TOP_KEY = ResourceKey.create(Registries.BLOCK, CanapliaMod.id("hemp_top"));
public static final ResourceKey<Block> RESIN_BLOCK_KEY = ResourceKey.create(Registries.BLOCK, CanapliaMod.id("resin_block"));
public static final HempCropBlock HEMP_CROP = new HempCropBlock(
BlockBehaviour.Properties.of()
.noCollision()
.noOcclusion()
.randomTicks()
.instabreak()
.sound(SoundType.CROP)
.pushReaction(PushReaction.DESTROY)
.setId(HEMP_CROP_KEY)
);
public static final HempTopBlock HEMP_TOP = new HempTopBlock(
BlockBehaviour.Properties.of()
.noCollision()
.noOcclusion()
.instabreak()
.sound(SoundType.CROP)
.pushReaction(PushReaction.DESTROY)
.setId(HEMP_TOP_KEY)
);
public static final ResinBlock RESIN_BLOCK = new ResinBlock(
BlockBehaviour.Properties.of()
.strength(0.8F)
.sound(SoundType.MUD)
.lightLevel(state -> state.getValue(ResinBlock.LIT) ? 10 : 0)
.setId(RESIN_BLOCK_KEY)
);
private CanapliaBlocks() {
}
public static void register() {
Registry.register(BuiltInRegistries.BLOCK, HEMP_CROP_KEY, HEMP_CROP);
Registry.register(BuiltInRegistries.BLOCK, HEMP_TOP_KEY, HEMP_TOP);
Registry.register(BuiltInRegistries.BLOCK, RESIN_BLOCK_KEY, RESIN_BLOCK);
}
public static BlockState resinState(HempVariant variant) {
return RESIN_BLOCK.defaultBlockState().setValue(ResinBlock.VARIANT, variant).setValue(ResinBlock.LIT, false);
}
}
@@ -0,0 +1,165 @@
package io.github.koka.canaplia.block;
import com.mojang.serialization.MapCodec;
import io.github.koka.canaplia.hemp.HempVariant;
import io.github.koka.canaplia.item.CanapliaItems;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.CropBlock;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.level.pathfinder.PathComputationType;
import net.minecraft.world.level.ItemLike;
import net.minecraft.world.item.ItemStack;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.BooleanProperty;
import net.minecraft.world.level.block.state.properties.EnumProperty;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.util.RandomSource;
import net.minecraft.world.phys.shapes.VoxelShape;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.ScheduledTickAccess;
public final class HempCropBlock extends CropBlock {
public static final MapCodec<HempCropBlock> CODEC = simpleCodec(HempCropBlock::new);
public static final BooleanProperty TALL = BooleanProperty.create("tall");
public static final EnumProperty<HempVariant> VARIANT = EnumProperty.create("variant", HempVariant.class);
private static final VoxelShape[] AGE_TO_SHAPE = Block.boxes(
8,
age -> Block.box(0.0, 0.0, 0.0, 16.0, Math.min(16.0, 2.0 + age * 2.0), 16.0)
);
public HempCropBlock(BlockBehaviour.Properties settings) {
super(settings);
this.registerDefaultState(this.defaultBlockState().setValue(AGE, 0).setValue(TALL, false).setValue(VARIANT, HempVariant.COMMON));
}
@Override
public MapCodec<HempCropBlock> codec() {
return CODEC;
}
@Override
protected VoxelShape getShape(BlockState state, BlockGetter world, BlockPos pos, CollisionContext context) {
return AGE_TO_SHAPE[this.getAge(state)];
}
@Override
protected boolean isRandomlyTicking(BlockState state) {
return HempCropLogic.hasPendingGrowth(this.getAge(state), this.getMaxAge(), state.getValue(TALL));
}
@Override
protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
if (!hasSufficientLight(world, pos)) {
return;
}
int age = this.getAge(state);
if (age < this.getMaxAge()) {
if (random.nextBoolean()) {
return;
}
float moisture = getGrowthSpeed(this, world, pos);
if (random.nextInt((int) (25.0F / moisture) + 1) == 0) {
world.setBlock(pos, state.setValue(AGE, age + 1), Block.UPDATE_ALL);
}
return;
}
if (!state.getValue(TALL) && random.nextBoolean()) {
this.tryGrowTall(world, pos, state);
}
}
@Override
public boolean isValidBonemealTarget(LevelReader world, BlockPos pos, BlockState state) {
return this.getAge(state) < this.getMaxAge() || this.canGrowTall(world, pos);
}
@Override
public boolean isBonemealSuccess(Level world, RandomSource random, BlockPos pos, BlockState state) {
return this.getAge(state) < this.getMaxAge() || this.canGrowTall(world, pos);
}
@Override
public void performBonemeal(ServerLevel world, RandomSource random, BlockPos pos, BlockState state) {
int age = this.getAge(state);
if (age < this.getMaxAge()) {
int grownAge = HempCropLogic.bonemealAge(age, this.getMaxAge(), this.getBonemealAgeIncrease(world));
world.setBlock(pos, state.setValue(AGE, grownAge), Block.UPDATE_ALL);
return;
}
if (!state.getValue(TALL)) {
this.tryGrowTall(world, pos, state);
}
}
@Override
protected ItemLike getBaseSeedId() {
return CanapliaItems.HEMP_SEEDS;
}
@Override
protected ItemStack getCloneItemStack(net.minecraft.world.level.LevelReader world, BlockPos pos, BlockState state, boolean includeData) {
return new ItemStack(CanapliaItems.getSeeds(state.getValue(VARIANT)));
}
@Override
protected BlockState updateShape(
BlockState state,
LevelReader world,
ScheduledTickAccess tickView,
BlockPos pos,
Direction direction,
BlockPos neighborPos,
BlockState neighborState,
RandomSource random
) {
if (!state.canSurvive(world, pos)) {
return Blocks.AIR.defaultBlockState();
}
if (
direction == Direction.UP
&& state.getValue(TALL)
&& (!neighborState.is(CanapliaBlocks.HEMP_TOP) || neighborState.getValue(HempTopBlock.VARIANT) != state.getValue(VARIANT))
) {
return state.setValue(TALL, false);
}
return super.updateShape(state, world, tickView, pos, direction, neighborPos, neighborState, random);
}
@Override
protected boolean isPathfindable(BlockState state, PathComputationType type) {
return false;
}
@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
super.createBlockStateDefinition(builder);
builder.add(TALL, VARIANT);
}
private boolean canGrowTall(LevelReader world, BlockPos pos) {
BlockState aboveState = world.getBlockState(pos.above());
return aboveState.isAir() || aboveState.canBeReplaced();
}
private void tryGrowTall(Level world, BlockPos pos, BlockState state) {
if (!this.canGrowTall(world, pos)) {
return;
}
world.setBlock(pos, state.setValue(TALL, true), Block.UPDATE_ALL);
world.setBlock(pos.above(), CanapliaBlocks.HEMP_TOP.defaultBlockState().setValue(HempTopBlock.VARIANT, state.getValue(VARIANT)), Block.UPDATE_ALL);
}
}
@@ -0,0 +1,18 @@
package io.github.koka.canaplia.block;
final class HempCropLogic {
private HempCropLogic() {
}
static int bonemealAge(int age, int maxAge, int growthAmount) {
return Math.min(maxAge, age + growthAmount);
}
static boolean hasPendingGrowth(int age, int maxAge, boolean tall) {
return age < maxAge || !tall;
}
static boolean shouldDropBud(int age, int maxAge, boolean tall) {
return age >= maxAge && tall;
}
}
@@ -0,0 +1,85 @@
package io.github.koka.canaplia.block;
import com.mojang.serialization.MapCodec;
import io.github.koka.canaplia.hemp.HempVariant;
import io.github.koka.canaplia.item.CanapliaItems;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.VegetationBlock;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.EnumProperty;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.util.RandomSource;
import net.minecraft.world.phys.shapes.VoxelShape;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.ScheduledTickAccess;
public final class HempTopBlock extends VegetationBlock {
public static final MapCodec<HempTopBlock> CODEC = simpleCodec(HempTopBlock::new);
public static final EnumProperty<HempVariant> VARIANT = HempCropBlock.VARIANT;
private static final VoxelShape SHAPE = Block.box(0.0, 0.0, 0.0, 16.0, 16.0, 16.0);
public HempTopBlock(BlockBehaviour.Properties settings) {
super(settings);
this.registerDefaultState(this.defaultBlockState().setValue(VARIANT, HempVariant.COMMON));
}
@Override
public MapCodec<HempTopBlock> codec() {
return CODEC;
}
@Override
protected VoxelShape getShape(BlockState state, BlockGetter world, BlockPos pos, CollisionContext context) {
return SHAPE;
}
@Override
protected ItemStack getCloneItemStack(LevelReader world, BlockPos pos, BlockState state, boolean includeData) {
return new ItemStack(CanapliaItems.getSeeds(state.getValue(VARIANT)));
}
@Override
protected boolean mayPlaceOn(BlockState floor, BlockGetter world, BlockPos pos) {
return floor.is(CanapliaBlocks.HEMP_CROP) && floor.getValue(HempCropBlock.TALL);
}
@Override
protected BlockState updateShape(
BlockState state,
LevelReader world,
ScheduledTickAccess tickView,
BlockPos pos,
net.minecraft.core.Direction direction,
BlockPos neighborPos,
BlockState neighborState,
RandomSource random
) {
if (
direction == Direction.DOWN
&& (!neighborState.is(CanapliaBlocks.HEMP_CROP)
|| !neighborState.getValue(HempCropBlock.TALL)
|| neighborState.getValue(HempCropBlock.VARIANT) != state.getValue(VARIANT))
) {
return Blocks.AIR.defaultBlockState();
}
if (!state.canSurvive(world, pos)) {
return Blocks.AIR.defaultBlockState();
}
return super.updateShape(state, world, tickView, pos, direction, neighborPos, neighborState, random);
}
@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
super.createBlockStateDefinition(builder);
builder.add(VARIANT);
}
}
@@ -0,0 +1,182 @@
package io.github.koka.canaplia.block;
import com.mojang.serialization.MapCodec;
import io.github.koka.canaplia.block.entity.CanapliaBlockEntityTypes;
import io.github.koka.canaplia.block.entity.ResinBlockEntity;
import io.github.koka.canaplia.hemp.HempVariant;
import io.github.koka.canaplia.item.CanapliaItems;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.RenderShape;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.BaseEntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityTicker;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.entity.projectile.Projectile;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundSource;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.BooleanProperty;
import net.minecraft.world.level.block.state.properties.EnumProperty;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.stats.Stats;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.core.BlockPos;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.gameevent.GameEvent;
public final class ResinBlock extends BaseEntityBlock {
public static final MapCodec<ResinBlock> CODEC = simpleCodec(ResinBlock::new);
public static final BooleanProperty LIT = BlockStateProperties.LIT;
public static final EnumProperty<HempVariant> VARIANT = EnumProperty.create("variant", HempVariant.class);
public ResinBlock(BlockBehaviour.Properties settings) {
super(settings);
this.registerDefaultState(this.defaultBlockState().setValue(LIT, false).setValue(VARIANT, HempVariant.COMMON));
}
@Override
public MapCodec<ResinBlock> codec() {
return CODEC;
}
@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
builder.add(LIT, VARIANT);
}
@Override
protected RenderShape getRenderShape(BlockState state) {
return RenderShape.MODEL;
}
@Override
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new ResinBlockEntity(pos, state);
}
@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level world, BlockState state, BlockEntityType<T> type) {
return world.isClientSide() ? null : createTickerHelper(type, CanapliaBlockEntityTypes.RESIN_BLOCK, ResinBlockEntity::tickServer);
}
@Override
protected ItemStack getCloneItemStack(LevelReader world, BlockPos pos, BlockState state, boolean includeData) {
return new ItemStack(CanapliaItems.getResinBlock(state.getValue(VARIANT)));
}
@Override
protected InteractionResult useItemOn(
ItemStack stack,
BlockState state,
Level world,
BlockPos pos,
Player player,
InteractionHand hand,
BlockHitResult hitResult
) {
if (state.getValue(LIT) || !isIgnitionItem(stack)) {
return InteractionResult.TRY_WITH_EMPTY_HAND;
}
if (world.isClientSide()) {
return InteractionResult.SUCCESS;
}
igniteWithItem((ServerLevel) world, pos, state, player, hand);
return InteractionResult.SUCCESS_SERVER;
}
@Override
protected void onProjectileHit(Level world, BlockState state, BlockHitResult hitResult, Projectile projectile) {
if (!(world instanceof ServerLevel serverWorld) || state.getValue(LIT) || !projectile.isOnFire()) {
return;
}
BlockPos pos = hitResult.getBlockPos();
if (!projectile.mayInteract(serverWorld, pos)) {
return;
}
ignite(serverWorld, pos, state, SoundEvents.FIRECHARGE_USE, 0.9F);
}
@Override
public void animateTick(BlockState state, Level world, BlockPos pos, RandomSource random) {
if (!state.getValue(LIT)) {
return;
}
if (random.nextInt(10) == 0) {
world.playLocalSound(
pos.getX() + 0.5,
pos.getY() + 0.5,
pos.getZ() + 0.5,
SoundEvents.CAMPFIRE_CRACKLE,
SoundSource.BLOCKS,
0.35F + random.nextFloat() * 0.3F,
0.6F + random.nextFloat() * 0.4F,
false
);
}
spawnSmokeParticle(world, pos, random);
if (random.nextBoolean()) {
spawnSmokeParticle(world, pos, random);
}
}
private static void spawnSmokeParticle(Level world, BlockPos pos, RandomSource random) {
world.addParticle(
ParticleTypes.CAMPFIRE_COSY_SMOKE,
pos.getX() + 0.5D,
pos.getY() + 1.0D,
pos.getZ() + 0.5D,
random.nextDouble() * 0.01D,
random.nextDouble() * 0.02D,
random.nextDouble() * 0.01D
);
}
private static boolean isIgnitionItem(ItemStack stack) {
return stack.is(Items.FLINT_AND_STEEL) || stack.is(Items.FIRE_CHARGE);
}
private static void igniteWithItem(ServerLevel world, BlockPos pos, BlockState state, Player player, InteractionHand hand) {
ItemStack stack = player.getItemInHand(hand);
if (stack.is(Items.FLINT_AND_STEEL)) {
if (player instanceof ServerPlayer serverPlayer) {
stack.hurtAndBreak(1, world, serverPlayer, item -> {});
}
player.awardStat(Stats.ITEM_USED.get(Items.FLINT_AND_STEEL));
ignite(world, pos, state, SoundEvents.FLINTANDSTEEL_USE, 1.0F);
return;
}
stack.consume(1, player);
player.awardStat(Stats.ITEM_USED.get(Items.FIRE_CHARGE));
ignite(world, pos, state, SoundEvents.FIRECHARGE_USE, 0.9F);
}
private static void ignite(ServerLevel world, BlockPos pos, BlockState state, SoundEvent sound, float pitch) {
world.setBlock(pos, state.setValue(LIT, true), Block.UPDATE_ALL);
if (world.getBlockEntity(pos) instanceof ResinBlockEntity blockEntity) {
blockEntity.light(state.getValue(VARIANT));
}
world.playSound(null, pos, sound, SoundSource.BLOCKS, 1.0F, pitch + world.getRandom().nextFloat() * 0.2F);
world.gameEvent(GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(world.getBlockState(pos)));
}
}
@@ -0,0 +1,22 @@
package io.github.koka.canaplia.block.entity;
import io.github.koka.canaplia.CanapliaMod;
import io.github.koka.canaplia.block.CanapliaBlocks;
import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityTypeBuilder;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.Registry;
public final class CanapliaBlockEntityTypes {
public static final BlockEntityType<ResinBlockEntity> RESIN_BLOCK = Registry.register(
BuiltInRegistries.BLOCK_ENTITY_TYPE,
CanapliaMod.id("resin_block"),
FabricBlockEntityTypeBuilder.create(ResinBlockEntity::new, CanapliaBlocks.RESIN_BLOCK).build()
);
private CanapliaBlockEntityTypes() {
}
public static void register() {
}
}
@@ -0,0 +1,93 @@
package io.github.koka.canaplia.block.entity;
import io.github.koka.canaplia.block.ResinBlock;
import io.github.koka.canaplia.effect.HempEffectController;
import io.github.koka.canaplia.hemp.HempVariant;
import io.github.koka.canaplia.resin.CanapliaResinCatalog;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundSource;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.world.level.storage.ValueInput;
import net.minecraft.world.level.storage.ValueOutput;
import net.minecraft.core.BlockPos;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.gameevent.GameEvent;
public final class ResinBlockEntity extends BlockEntity {
private static final int EFFECT_INTERVAL_TICKS = 20;
private int remainingBurnTicks;
public ResinBlockEntity(BlockPos pos, BlockState state) {
super(CanapliaBlockEntityTypes.RESIN_BLOCK, pos, state);
}
public int getRemainingBurnTicks() {
return this.remainingBurnTicks;
}
public void setRemainingBurnTicks(int remainingBurnTicks) {
this.remainingBurnTicks = Math.max(0, remainingBurnTicks);
this.setChanged();
}
public void light(HempVariant variant) {
this.remainingBurnTicks = CanapliaResinCatalog.resolve(variant).burnTicks();
this.setChanged();
}
@Override
protected void loadAdditional(ValueInput view) {
super.loadAdditional(view);
this.remainingBurnTicks = Math.max(0, view.getIntOr("remaining_burn_ticks", 0));
}
@Override
protected void saveAdditional(ValueOutput view) {
super.saveAdditional(view);
view.putInt("remaining_burn_ticks", this.remainingBurnTicks);
}
public static void tickServer(Level world, BlockPos pos, BlockState state, ResinBlockEntity blockEntity) {
if (!(world instanceof ServerLevel serverWorld) || !state.getValue(ResinBlock.LIT)) {
return;
}
if (blockEntity.remainingBurnTicks <= 0) {
extinguishAndConsume(serverWorld, pos, state);
return;
}
if (blockEntity.remainingBurnTicks % EFFECT_INTERVAL_TICKS == 0) {
applyAura(serverWorld, pos, state.getValue(ResinBlock.VARIANT));
}
blockEntity.remainingBurnTicks--;
blockEntity.setChanged();
if (blockEntity.remainingBurnTicks <= 0) {
extinguishAndConsume(serverWorld, pos, state);
}
}
private static void applyAura(ServerLevel world, BlockPos pos, HempVariant variant) {
AABB effectBox = new AABB(pos.getX() - 1.0, pos.getY(), pos.getZ() - 1.0, pos.getX() + 2.0, pos.getY() + 4.0, pos.getZ() + 2.0);
for (ServerPlayer player : world.getPlayers(serverPlayer -> effectBox.intersects(serverPlayer.getBoundingBox()))) {
HempEffectController.refreshResinAura(player, variant);
}
}
private static void extinguishAndConsume(ServerLevel world, BlockPos pos, BlockState state) {
world.playSound(null, pos, SoundEvents.FIRE_EXTINGUISH, SoundSource.BLOCKS, 0.8F, 1.0F);
world.sendParticles(ParticleTypes.SMOKE, pos.getX() + 0.5, pos.getY() + 0.85, pos.getZ() + 0.5, 20, 0.25, 0.15, 0.25, 0.01);
world.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
world.gameEvent(GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(state));
}
}
@@ -0,0 +1,142 @@
package io.github.koka.canaplia.compost;
import io.github.koka.canaplia.hemp.HempVariant;
import io.github.koka.canaplia.item.CanapliaItems;
import io.github.koka.canaplia.resin.CanapliaResinCatalog;
import java.util.Optional;
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.ComposterBlock;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundSource;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.stats.Stats;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.gameevent.GameEvent;
public final class HempBudComposterInteraction {
private static final int COMPOSTER_FILL_EVENT = 1500;
private HempBudComposterInteraction() {
}
public static void register() {
UseBlockCallback.EVENT.register(HempBudComposterInteraction::onUseBlock);
}
private static InteractionResult onUseBlock(Player player, Level world, InteractionHand hand, BlockHitResult hitResult) {
if (player.isSpectator()) {
return InteractionResult.PASS;
}
BlockPos pos = hitResult.getBlockPos();
BlockState state = world.getBlockState(pos);
if (!state.is(Blocks.COMPOSTER)) {
return InteractionResult.PASS;
}
ItemStack stack = player.getItemInHand(hand);
Optional<HempVariant> budVariant = CanapliaItems.getBudVariant(stack);
if (world.isClientSide()) {
if (budVariant.isPresent() && state.getValue(ComposterBlock.LEVEL) < 8) {
return InteractionResult.SUCCESS;
}
if (stack.isEmpty() && state.getValue(ComposterBlock.LEVEL) == 8) {
return InteractionResult.SUCCESS;
}
return InteractionResult.PASS;
}
ServerLevel serverWorld = (ServerLevel) world;
HempBudCompostingState trackedComposters = HempBudCompostingState.get(serverWorld);
trackedComposters.invalidateIfMismatched(pos, state);
int budCount = trackedComposters.getBudCount(pos);
if (budCount > 0) {
if (stack.isEmpty()) {
if (trackedComposters.isReady(pos)) {
return extractResinBlock(player, serverWorld, pos, state, trackedComposters);
}
return InteractionResult.PASS;
}
if (budVariant.isEmpty() || trackedComposters.isReady(pos)) {
return InteractionResult.SUCCESS_SERVER;
}
if (!trackedComposters.canAccept(pos, budVariant.get())) {
return InteractionResult.SUCCESS_SERVER;
}
return insertBud(player, hand, serverWorld, pos, state, trackedComposters, budVariant.get());
}
if (budVariant.isEmpty() || !HempBudCompostingLogic.canStart(state)) {
return InteractionResult.PASS;
}
return insertBud(player, hand, serverWorld, pos, state, trackedComposters, budVariant.get());
}
private static InteractionResult insertBud(
Player player,
InteractionHand hand,
ServerLevel world,
BlockPos pos,
BlockState state,
HempBudCompostingState trackedComposters,
HempVariant budVariant
) {
ItemStack stack = player.getItemInHand(hand);
int budCount = trackedComposters.incrementBudCount(pos, budVariant);
int level = HempBudCompostingLogic.levelForBudCount(budCount);
if (state.getValue(ComposterBlock.LEVEL) != level) {
world.setBlock(pos, state.setValue(ComposterBlock.LEVEL, level), Block.UPDATE_ALL);
}
stack.consume(1, player);
player.awardStat(Stats.ITEM_USED.get(CanapliaItems.getBud(budVariant)));
world.levelEvent(COMPOSTER_FILL_EVENT, pos, 1);
world.gameEvent(player, GameEvent.BLOCK_CHANGE, pos);
if (HempBudCompostingLogic.isReady(budCount)) {
world.playSound(null, pos, SoundEvents.COMPOSTER_READY, SoundSource.BLOCKS, 1.0F, 1.0F);
}
return InteractionResult.SUCCESS_SERVER;
}
private static InteractionResult extractResinBlock(
Player player,
ServerLevel world,
BlockPos pos,
BlockState state,
HempBudCompostingState trackedComposters
) {
HempVariant sourceVariant = trackedComposters.getSourceVariant(pos).orElse(HempVariant.COMMON);
trackedComposters.clear(pos);
world.setBlock(pos, state.setValue(ComposterBlock.LEVEL, 0), Block.UPDATE_ALL);
ItemStack reward = CanapliaResinCatalog.createReward(sourceVariant);
if (!player.addItem(reward)) {
player.drop(reward, false);
}
world.playSound(null, pos, SoundEvents.COMPOSTER_EMPTY, SoundSource.BLOCKS, 1.0F, 1.0F);
world.gameEvent(player, GameEvent.BLOCK_CHANGE, pos);
return InteractionResult.SUCCESS_SERVER;
}
}
@@ -0,0 +1,46 @@
package io.github.koka.canaplia.compost;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.ComposterBlock;
public final class HempBudCompostingLogic {
public static final int REQUIRED_BUDS = 10;
private HempBudCompostingLogic() {
}
public static boolean canStart(BlockState state) {
boolean isComposter = state.is(Blocks.COMPOSTER);
return canStart(isComposter, isComposter ? state.getValue(ComposterBlock.LEVEL) : -1);
}
public static boolean canStart(boolean isComposter, int level) {
return isComposter && level == 0;
}
public static boolean isReady(int budCount) {
return budCount == REQUIRED_BUDS;
}
public static int levelForBudCount(int budCount) {
if (budCount < 1 || budCount > REQUIRED_BUDS) {
throw new IllegalArgumentException("Bud count must be between 1 and " + REQUIRED_BUDS);
}
if (budCount == REQUIRED_BUDS) {
return 8;
}
return (budCount * 7 + REQUIRED_BUDS - 1) / REQUIRED_BUDS;
}
public static boolean matchesTrackedState(int budCount, BlockState state) {
boolean isComposter = state.is(Blocks.COMPOSTER);
return matchesTrackedState(budCount, isComposter, isComposter ? state.getValue(ComposterBlock.LEVEL) : -1);
}
public static boolean matchesTrackedState(int budCount, boolean isComposter, int level) {
return isComposter && level == levelForBudCount(budCount);
}
}
@@ -0,0 +1,132 @@
package io.github.koka.canaplia.compost;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import io.github.koka.canaplia.hemp.HempVariant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.util.datafix.DataFixTypes;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.saveddata.SavedData;
import net.minecraft.world.level.saveddata.SavedDataType;
public final class HempBudCompostingState extends SavedData {
private static final Codec<HempVariant> HEMP_VARIANT_CODEC = Codec.STRING.xmap(HempBudCompostingState::variantFromKey, HempVariant::asString);
private static final Codec<TrackedComposterEntry> ENTRY_CODEC = RecordCodecBuilder.create(instance ->
instance.group(
Codec.LONG.fieldOf("pos").forGetter(TrackedComposterEntry::pos),
Codec.intRange(1, HempBudCompostingLogic.REQUIRED_BUDS).fieldOf("bud_count").forGetter(TrackedComposterEntry::budCount),
HEMP_VARIANT_CODEC.optionalFieldOf("source_variant", HempVariant.COMMON).forGetter(TrackedComposterEntry::sourceVariant)
).apply(instance, TrackedComposterEntry::new)
);
private static final Codec<HempBudCompostingState> CODEC = ENTRY_CODEC.listOf()
.xmap(HempBudCompostingState::fromEntries, HempBudCompostingState::asEntries);
public static final SavedDataType<HempBudCompostingState> TYPE = new SavedDataType<>(
io.github.koka.canaplia.CanapliaMod.id("hemp_bud_composters"),
HempBudCompostingState::new,
CODEC,
DataFixTypes.SAVED_DATA_COMMAND_STORAGE
);
private final Map<Long, TrackedComposterEntry> trackedComposters = new HashMap<>();
public static HempBudCompostingState get(ServerLevel world) {
return world.getDataStorage().computeIfAbsent(TYPE);
}
public HempBudCompostingState() {
}
private HempBudCompostingState(List<TrackedComposterEntry> entries) {
entries.forEach(entry -> this.trackedComposters.put(entry.pos(), entry));
}
public int getBudCount(BlockPos pos) {
TrackedComposterEntry entry = this.trackedComposters.get(pos.asLong());
return entry == null ? 0 : entry.budCount();
}
public boolean isTracked(BlockPos pos) {
return this.trackedComposters.containsKey(pos.asLong());
}
public Optional<HempVariant> getSourceVariant(BlockPos pos) {
TrackedComposterEntry entry = this.trackedComposters.get(pos.asLong());
return entry == null ? Optional.empty() : Optional.of(entry.sourceVariant());
}
public boolean canAccept(BlockPos pos, HempVariant variant) {
return this.getSourceVariant(pos).map(sourceVariant -> sourceVariant == variant).orElse(true);
}
public boolean isReady(BlockPos pos) {
return HempBudCompostingLogic.isReady(this.getBudCount(pos));
}
public int incrementBudCount(BlockPos pos, HempVariant sourceVariant) {
long key = pos.asLong();
TrackedComposterEntry entry = this.trackedComposters.get(key);
if (entry != null && entry.sourceVariant() != sourceVariant) {
throw new IllegalArgumentException("Tracked composter at " + pos + " already contains " + entry.sourceVariant().asString() + " buds.");
}
int budCount = Math.min(HempBudCompostingLogic.REQUIRED_BUDS, (entry == null ? 0 : entry.budCount()) + 1);
this.trackedComposters.put(key, new TrackedComposterEntry(key, budCount, sourceVariant));
this.setDirty();
return budCount;
}
public boolean invalidateIfMismatched(BlockPos pos, BlockState state) {
boolean isComposter = state.is(net.minecraft.world.level.block.Blocks.COMPOSTER);
return this.invalidateIfMismatched(pos, isComposter, isComposter ? state.getValue(net.minecraft.world.level.block.ComposterBlock.LEVEL) : -1);
}
public boolean invalidateIfMismatched(BlockPos pos, boolean isComposter, int level) {
int budCount = this.getBudCount(pos);
if (budCount == 0) {
return false;
}
if (HempBudCompostingLogic.matchesTrackedState(budCount, isComposter, level)) {
return false;
}
this.trackedComposters.remove(pos.asLong());
this.setDirty();
return true;
}
public void clear(BlockPos pos) {
if (this.trackedComposters.remove(pos.asLong()) != null) {
this.setDirty();
}
}
private static HempBudCompostingState fromEntries(List<TrackedComposterEntry> entries) {
return new HempBudCompostingState(entries);
}
private List<TrackedComposterEntry> asEntries() {
return this.trackedComposters.values().stream()
.toList();
}
private static HempVariant variantFromKey(String key) {
for (HempVariant variant : HempVariant.values()) {
if (variant.asString().equals(key)) {
return variant;
}
}
return HempVariant.COMMON;
}
private record TrackedComposterEntry(long pos, int budCount, HempVariant sourceVariant) {
}
}
@@ -0,0 +1,59 @@
package io.github.koka.canaplia.effect;
import io.github.koka.canaplia.hemp.HempVariant;
import java.util.List;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.effect.MobEffect;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.core.Holder;
public final class CanapliaSmokeEffects {
private CanapliaSmokeEffects() {
}
public static Holder<MobEffect> resolve(String effectId) {
return switch (effectId) {
case "minecraft:absorption" -> MobEffects.ABSORPTION;
case "minecraft:blindness" -> MobEffects.BLINDNESS;
case "minecraft:dolphins_grace" -> MobEffects.DOLPHINS_GRACE;
case "minecraft:fire_resistance" -> MobEffects.FIRE_RESISTANCE;
case "minecraft:haste" -> MobEffects.HASTE;
case "minecraft:hunger" -> MobEffects.HUNGER;
case "minecraft:invisibility" -> MobEffects.INVISIBILITY;
case "minecraft:jump_boost" -> MobEffects.JUMP_BOOST;
case "minecraft:luck" -> MobEffects.LUCK;
case "minecraft:nausea" -> MobEffects.NAUSEA;
case "minecraft:night_vision" -> MobEffects.NIGHT_VISION;
case "minecraft:regeneration" -> MobEffects.REGENERATION;
case "minecraft:resistance" -> MobEffects.RESISTANCE;
case "minecraft:slow_falling" -> MobEffects.SLOW_FALLING;
case "minecraft:slowness" -> MobEffects.SLOWNESS;
case "minecraft:speed" -> MobEffects.SPEED;
case "minecraft:strength" -> MobEffects.STRENGTH;
case "minecraft:water_breathing" -> MobEffects.WATER_BREATHING;
case "minecraft:weakness" -> MobEffects.WEAKNESS;
default -> throw new IllegalArgumentException("Unknown status effect: " + effectId + ".");
};
}
public static MobEffectInstance createEffect(HempEffectSpec effect) {
return new MobEffectInstance(resolve(effect.effectId()), effect.durationTicks(), effect.amplifier(), false, true, true);
}
public static List<MobEffectInstance> createEffects(List<HempEffectSpec> effects) {
return effects.stream().map(CanapliaSmokeEffects::createEffect).toList();
}
public static void applyEffects(LivingEntity entity, List<HempEffectSpec> effects) {
createEffects(effects).forEach(entity::addEffect);
}
public static HempResolvedEffectProfile jointProfile(HempVariant variant) {
return HempEffectCatalog.getDefault().resolve(HempEffectSource.JOINT, variant, net.minecraft.util.RandomSource.create(0L));
}
public static HempResolvedEffectProfile auraProfile(HempVariant variant) {
return HempEffectCatalog.getDefault().resolve(HempEffectSource.RESIN_AURA, variant, net.minecraft.util.RandomSource.create(0L));
}
}
@@ -0,0 +1,11 @@
package io.github.koka.canaplia.effect;
public record HempBadTripBaseRules(
boolean appliesNausea,
boolean appliesHunger,
float normalVisualIntensityMin,
float normalVisualIntensityMax,
float badTripVisualIntensityMin,
float badTripVisualIntensityMax
) {
}
@@ -0,0 +1,19 @@
package io.github.koka.canaplia.effect;
import net.minecraft.util.RandomSource;
public record HempDelayRange(int minTicks, int maxTicks) {
public HempDelayRange {
if (minTicks < 0 || maxTicks < minTicks) {
throw new IllegalArgumentException("Invalid delay range: " + minTicks + " -> " + maxTicks);
}
}
public int roll(RandomSource random) {
if (this.minTicks == this.maxTicks) {
return this.minTicks;
}
return this.minTicks + random.nextInt(this.maxTicks - this.minTicks + 1);
}
}
@@ -0,0 +1,381 @@
package io.github.koka.canaplia.effect;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import io.github.koka.canaplia.CanapliaMod;
import io.github.koka.canaplia.hemp.HempVariant;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.minecraft.util.RandomSource;
public final class HempEffectCatalog {
private static final String RESOURCE_PATH = "/data/canaplia/hemp_varieties.json";
private static final EnumMap<HempVariant, String> VARIANT_TO_JSON_ID = createVariantIdMap();
private static final Set<String> SUPPORTED_EFFECT_IDS = Set.of(
"minecraft:absorption",
"minecraft:blindness",
"minecraft:dolphins_grace",
"minecraft:fire_resistance",
"minecraft:haste",
"minecraft:hunger",
"minecraft:invisibility",
"minecraft:jump_boost",
"minecraft:luck",
"minecraft:nausea",
"minecraft:night_vision",
"minecraft:regeneration",
"minecraft:resistance",
"minecraft:slow_falling",
"minecraft:slowness",
"minecraft:speed",
"minecraft:strength",
"minecraft:water_breathing",
"minecraft:weakness"
);
private final int version;
private final HempBadTripBaseRules badTripBaseRules;
private final EnumMap<HempVariant, HempVarietyDefinition> definitions;
private final Map<String, HempVarietyDefinition> definitionsByJsonId;
private HempEffectCatalog(
int version,
HempBadTripBaseRules badTripBaseRules,
EnumMap<HempVariant, HempVarietyDefinition> definitions,
Map<String, HempVarietyDefinition> definitionsByJsonId
) {
this.version = version;
this.badTripBaseRules = badTripBaseRules;
this.definitions = definitions;
this.definitionsByJsonId = definitionsByJsonId;
}
public static HempEffectCatalog getDefault() {
return Holder.INSTANCE;
}
public static HempEffectCatalog load(Reader reader) {
try {
JsonObject root = JsonParser.parseReader(reader).getAsJsonObject();
int version = requiredInt(root, "version");
HempBadTripBaseRules badTripBaseRules = parseBadTripBaseRules(requiredObject(root, "bad_trip_base_rules"));
EnumMap<HempVariant, HempVarietyDefinition> definitions = new EnumMap<>(HempVariant.class);
Map<String, HempVarietyDefinition> definitionsByJsonId = new HashMap<>();
for (JsonElement element : requiredArray(root, "varieties")) {
HempVarietyDefinition definition = parseVarietyDefinition(requiredObject(element, "variety entry"), badTripBaseRules);
if (definitions.put(definition.variant(), definition) != null) {
throw new IllegalArgumentException("Duplicate variety mapping for " + definition.variant().asString() + ".");
}
if (definitionsByJsonId.put(definition.jsonId(), definition) != null) {
throw new IllegalArgumentException("Duplicate JSON id " + definition.jsonId() + ".");
}
}
validateCompleteness(definitions);
return new HempEffectCatalog(version, badTripBaseRules, definitions, definitionsByJsonId);
} catch (IllegalStateException | JsonParseException exception) {
throw new IllegalArgumentException("Invalid hemp effect catalog JSON.", exception);
}
}
public int version() {
return this.version;
}
public HempBadTripBaseRules badTripBaseRules() {
return this.badTripBaseRules;
}
public List<HempVarietyDefinition> varieties() {
return List.copyOf(this.definitions.values());
}
public HempVarietyDefinition definition(HempVariant variant) {
HempVarietyDefinition definition = this.definitions.get(variant);
if (definition == null) {
throw new IllegalArgumentException("No hemp effect definition for " + variant.asString() + ".");
}
return definition;
}
public HempResolvedEffectProfile resolve(HempEffectSource source, HempVariant variant, RandomSource random) {
return this.definition(variant).resolve(source, random);
}
private static HempBadTripBaseRules parseBadTripBaseRules(JsonObject object) {
float[] normalIntensityTarget = requiredFloatPair(object, "normal_trip_visual_intensity_target");
float[] badTripIntensityTarget = requiredFloatPair(object, "bad_trip_visual_intensity_target");
return new HempBadTripBaseRules(
requiredBoolean(object, "applies_nausea"),
requiredBoolean(object, "applies_hunger"),
normalIntensityTarget[0],
normalIntensityTarget[1],
badTripIntensityTarget[0],
badTripIntensityTarget[1]
);
}
private static HempVarietyDefinition parseVarietyDefinition(JsonObject object, HempBadTripBaseRules badTripBaseRules) {
String jsonId = requiredString(object, "id");
HempVariant variant = variantFromJsonId(jsonId);
HempMainEffectMode mainEffectMode = parseMainEffectMode(object);
int mainDurationTicks = secondsToTicks(requiredInt(object, "main_duration_sec"), "main_duration_sec");
List<HempEffectSpec> mainEffects = mainEffectMode == HempMainEffectMode.DIRECT
? parseEffects(requiredArray(object, "main_effects"), "main_effects")
: List.of();
List<HempEffectSpec> mainEffectsPool = mainEffectMode == HempMainEffectMode.RANDOM_PICK
? parseEffects(requiredArray(object, "main_effects_pool"), "main_effects_pool")
: List.of();
int mainEffectsPickCount = mainEffectMode == HempMainEffectMode.RANDOM_PICK ? requiredInt(object, "main_effects_pick_count") : 0;
if (mainEffectMode == HempMainEffectMode.RANDOM_PICK && (mainEffectsPickCount <= 0 || mainEffectsPickCount > mainEffectsPool.size())) {
throw new IllegalArgumentException(
"Invalid main_effects_pick_count for " + jsonId + ": " + mainEffectsPickCount + " over pool size " + mainEffectsPool.size() + "."
);
}
HempVarietyDefinition definition = new HempVarietyDefinition(
jsonId,
variant,
requiredString(object, "name_fr"),
requiredString(object, "rarity"),
requiredString(object, "color_family"),
mainDurationTicks,
mainEffectMode,
mainEffects,
mainEffectsPool,
mainEffectsPickCount,
parseVisualProfile(requiredObject(object, "normal_visual_profile")),
requiredChance(object, "bad_trip_chance"),
parseDelayRange(requiredObject(object, "bad_trip_delay_sec")),
secondsToTicks(requiredInt(object, "bad_trip_duration_sec"), "bad_trip_duration_sec"),
parseEffects(requiredArray(object, "bad_trip_effects"), "bad_trip_effects"),
parseVisualProfile(requiredObject(object, "bad_trip_visual_profile"))
);
validateBadTripBaseEffects(definition, badTripBaseRules);
return definition;
}
private static HempMainEffectMode parseMainEffectMode(JsonObject object) {
if (!object.has("main_effects_mode")) {
return HempMainEffectMode.DIRECT;
}
String rawMode = requiredString(object, "main_effects_mode");
return switch (rawMode) {
case "random_pick" -> HempMainEffectMode.RANDOM_PICK;
case "direct" -> HempMainEffectMode.DIRECT;
default -> throw new IllegalArgumentException("Unsupported main_effects_mode: " + rawMode + ".");
};
}
private static List<HempEffectSpec> parseEffects(JsonArray array, String fieldName) {
java.util.ArrayList<HempEffectSpec> effects = new java.util.ArrayList<>(array.size());
for (JsonElement element : array) {
effects.add(parseEffect(requiredObject(element, fieldName + " entry")));
}
return List.copyOf(effects);
}
private static HempEffectSpec parseEffect(JsonObject object) {
String effectId = requiredString(object, "effect");
validateEffectExists(effectId);
return new HempEffectSpec(effectId, requiredInt(object, "amplifier"), secondsToTicks(requiredInt(object, "duration_sec"), "duration_sec"));
}
private static HempVisualProfile parseVisualProfile(JsonObject object) {
return new HempVisualProfile(
requiredString(object, "palette"),
requiredFloat(object, "intensity"),
optionalFloat(object, "tint_strength"),
optionalFloat(object, "saturation_delta"),
optionalFloat(object, "contrast_delta"),
optionalFloat(object, "vignette_strength"),
optionalFloat(object, "ghosting_strength"),
optionalFloat(object, "wobble_strength"),
optionalFloat(object, "pulse_strength")
);
}
private static HempDelayRange parseDelayRange(JsonObject object) {
return new HempDelayRange(
secondsToTicks(requiredInt(object, "min"), "bad_trip_delay_sec.min"),
secondsToTicks(requiredInt(object, "max"), "bad_trip_delay_sec.max")
);
}
private static void validateCompleteness(EnumMap<HempVariant, HempVarietyDefinition> definitions) {
for (HempVariant variant : HempVariant.values()) {
if (!definitions.containsKey(variant)) {
throw new IllegalArgumentException("Missing hemp effect definition for " + variant.asString() + ".");
}
}
}
private static void validateBadTripBaseEffects(HempVarietyDefinition definition, HempBadTripBaseRules badTripBaseRules) {
List<String> badTripEffects = definition.badTripEffects().stream().map(HempEffectSpec::effectId).toList();
if (badTripBaseRules.appliesNausea() && !badTripEffects.contains("minecraft:nausea")) {
throw new IllegalArgumentException(definition.jsonId() + " must include minecraft:nausea in bad_trip_effects.");
}
if (badTripBaseRules.appliesHunger() && !badTripEffects.contains("minecraft:hunger")) {
throw new IllegalArgumentException(definition.jsonId() + " must include minecraft:hunger in bad_trip_effects.");
}
}
private static HempVariant variantFromJsonId(String jsonId) {
return VARIANT_TO_JSON_ID.entrySet()
.stream()
.filter(entry -> entry.getValue().equals(jsonId))
.map(Map.Entry::getKey)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("No runtime hemp variant mapping for JSON id " + jsonId + "."));
}
private static void validateEffectExists(String effectId) {
if (!SUPPORTED_EFFECT_IDS.contains(effectId)) {
throw new IllegalArgumentException("Unknown status effect in hemp catalog: " + effectId + ".");
}
}
private static JsonObject requiredObject(JsonObject object, String fieldName) {
if (!object.has(fieldName)) {
throw new IllegalArgumentException("Missing object field " + fieldName + ".");
}
return object.getAsJsonObject(fieldName);
}
private static JsonObject requiredObject(JsonElement element, String label) {
if (!element.isJsonObject()) {
throw new IllegalArgumentException("Expected object for " + label + ".");
}
return element.getAsJsonObject();
}
private static JsonArray requiredArray(JsonObject object, String fieldName) {
if (!object.has(fieldName)) {
throw new IllegalArgumentException("Missing array field " + fieldName + ".");
}
return object.getAsJsonArray(fieldName);
}
private static String requiredString(JsonObject object, String fieldName) {
if (!object.has(fieldName)) {
throw new IllegalArgumentException("Missing string field " + fieldName + ".");
}
return object.get(fieldName).getAsString();
}
private static int requiredInt(JsonObject object, String fieldName) {
if (!object.has(fieldName)) {
throw new IllegalArgumentException("Missing int field " + fieldName + ".");
}
return object.get(fieldName).getAsInt();
}
private static boolean requiredBoolean(JsonObject object, String fieldName) {
if (!object.has(fieldName)) {
throw new IllegalArgumentException("Missing boolean field " + fieldName + ".");
}
return object.get(fieldName).getAsBoolean();
}
private static float requiredFloat(JsonObject object, String fieldName) {
if (!object.has(fieldName)) {
throw new IllegalArgumentException("Missing float field " + fieldName + ".");
}
return object.get(fieldName).getAsFloat();
}
private static float optionalFloat(JsonObject object, String fieldName) {
return object.has(fieldName) ? object.get(fieldName).getAsFloat() : 0.0F;
}
private static float requiredChance(JsonObject object, String fieldName) {
float chance = requiredFloat(object, fieldName);
if (chance < 0.0F || chance > 1.0F) {
throw new IllegalArgumentException("Chance field " + fieldName + " must stay between 0.0 and 1.0.");
}
return chance;
}
private static float[] requiredFloatPair(JsonObject object, String fieldName) {
JsonArray array = requiredArray(object, fieldName);
if (array.size() != 2) {
throw new IllegalArgumentException("Field " + fieldName + " must contain exactly 2 values.");
}
return new float[] {array.get(0).getAsFloat(), array.get(1).getAsFloat()};
}
private static int secondsToTicks(int seconds, String fieldName) {
if (seconds <= 0) {
throw new IllegalArgumentException("Field " + fieldName + " must be positive.");
}
return seconds * 20;
}
private static EnumMap<HempVariant, String> createVariantIdMap() {
EnumMap<HempVariant, String> map = new EnumMap<>(HempVariant.class);
map.put(HempVariant.COMMON, "chanvre_commun");
map.put(HempVariant.TENDER, "chanvre_tendre");
map.put(HempVariant.OLIVE, "chanvre_olive");
map.put(HempVariant.DARK, "chanvre_sombre");
map.put(HempVariant.ASHEN, "chanvre_cendre");
map.put(HempVariant.MOSS, "chanvre_mousse");
map.put(HempVariant.GOLD, "chanvre_dore");
map.put(HempVariant.AMBER, "chanvre_ambre");
map.put(HempVariant.BLUISH, "chanvre_bleute");
map.put(HempVariant.PINK, "chanvre_rose");
map.put(HempVariant.CORAL, "chanvre_corail");
map.put(HempVariant.PURPLE, "chanvre_pourpre");
map.put(HempVariant.INDIGO, "chanvre_indigo");
map.put(HempVariant.VARIEG, "chanvre_panache");
map.put(HempVariant.LUNAR, "chanvre_lunaire");
map.put(HempVariant.EMBER, "chanvre_braise");
return map;
}
private static HempEffectCatalog loadBundled() {
InputStream stream = HempEffectCatalog.class.getResourceAsStream(RESOURCE_PATH);
if (stream == null) {
throw new IllegalStateException("Missing bundled hemp effect catalog at " + RESOURCE_PATH + ".");
}
try (Reader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) {
return load(reader);
} catch (IOException exception) {
throw new IllegalStateException("Failed to read bundled hemp effect catalog.", exception);
}
}
private static final class Holder {
private static final HempEffectCatalog INSTANCE = loadBundled();
}
}
@@ -0,0 +1,199 @@
package io.github.koka.canaplia.effect;
import io.github.koka.canaplia.effect.network.CanapliaEffectPayloads;
import io.github.koka.canaplia.hemp.HempVariant;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.effect.MobEffect;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.core.Holder;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
public final class HempEffectController {
private static final HempEffectCatalog CATALOG = HempEffectCatalog.getDefault();
private static final Map<UUID, HempPlayerEffectState> ACTIVE_STATES = new HashMap<>();
private HempEffectController() {
}
public static void register() {
CanapliaEffectPayloads.register();
ServerTickEvents.END_SERVER_TICK.register(HempEffectController::tickServer);
}
public static void consumeJoint(ServerPlayer player, HempVariant variant) {
startOrRefresh(player, HempEffectSource.JOINT, variant);
}
public static void refreshResinAura(ServerPlayer player, HempVariant variant) {
startOrRefresh(player, HempEffectSource.RESIN_AURA, variant);
}
public static void applyImmediateEffects(LivingEntity entity, HempVariant variant) {
HempResolvedEffectProfile profile = CATALOG.resolve(HempEffectSource.JOINT, variant, net.minecraft.util.RandomSource.create(0L));
CanapliaSmokeEffects.applyEffects(entity, profile.mainEffects());
}
static HempPlayerEffectState getActiveState(UUID playerId) {
return ACTIVE_STATES.get(playerId);
}
static void clearForTests() {
ACTIVE_STATES.clear();
}
private static void startOrRefresh(ServerPlayer player, HempEffectSource source, HempVariant variant) {
long currentTick = player.level().getGameTime();
HempPlayerEffectState state = ACTIVE_STATES.computeIfAbsent(player.getUUID(), unused -> new HempPlayerEffectState());
state.pruneExpiredSessions(currentTick);
HempEffectSession existingSession = state.session(variant);
HempResolvedEffectProfile profile = existingSession != null ? existingSession.profile() : CATALOG.resolve(source, variant, player.getRandom());
boolean badTripWasActive = state.hasBadTripStarted();
state.captureBaseline(player, profile.allEffectIds());
state.upsertSession(source, profile, currentTick);
boolean rerollBadTrip = source == HempEffectSource.JOINT || existingSession == null;
if (rerollBadTrip) {
state.rerollBadTrip(profile, currentTick, player.getRandom());
} else {
state.refreshActiveBadTrip(variant, currentTick);
}
reapplyManagedEffects(player, state, currentTick);
syncVisualState(player, state, currentTick, badTripWasActive && rerollBadTrip);
}
private static void tickServer(MinecraftServer server) {
Iterator<Map.Entry<UUID, HempPlayerEffectState>> iterator = ACTIVE_STATES.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<UUID, HempPlayerEffectState> entry = iterator.next();
ServerPlayer player = server.getPlayerList().getPlayer(entry.getKey());
if (player == null) {
iterator.remove();
continue;
}
if (!player.isAlive()) {
clearState(player, entry.getValue(), true);
iterator.remove();
continue;
}
long currentTick = player.level().getGameTime();
HempPlayerEffectState state = entry.getValue();
boolean removedExpiredSessions = state.pruneExpiredSessions(currentTick);
boolean expiredBadTrip = state.expireBadTripIfNeeded(currentTick);
boolean startedBadTrip = false;
if (state.shouldStartBadTrip(currentTick)) {
state.startBadTrip(currentTick);
startedBadTrip = true;
}
if (removedExpiredSessions || expiredBadTrip || startedBadTrip) {
reapplyManagedEffects(player, state, currentTick);
}
if (state.isEmpty()) {
clearState(player, state, true);
iterator.remove();
continue;
}
if (removedExpiredSessions || expiredBadTrip || startedBadTrip) {
syncVisualState(player, state, currentTick, expiredBadTrip);
}
}
}
private static void reapplyManagedEffects(ServerPlayer player, HempPlayerEffectState state, long currentTick) {
Map<Holder<MobEffect>, MobEffectInstance> targetEffects = new HashMap<>();
for (HempEffectSession session : state.sessions()) {
mergeEffects(targetEffects, session.activeMainEffects(currentTick));
}
mergeEffects(targetEffects, state.activeBadTripEffects(currentTick));
Set<Holder<MobEffect>> managedTypes = new HashSet<>(state.baselineEffects().keySet());
managedTypes.addAll(targetEffects.keySet());
for (Holder<MobEffect> effectType : managedTypes) {
player.removeEffect(effectType);
}
targetEffects.values().forEach(effect -> player.addEffect(new MobEffectInstance(effect)));
for (Map.Entry<Holder<MobEffect>, MobEffectInstance> baselineEntry : state.baselineEffects().entrySet()) {
if (!targetEffects.containsKey(baselineEntry.getKey())) {
player.addEffect(new MobEffectInstance(baselineEntry.getValue()));
}
}
}
private static void mergeEffects(Map<Holder<MobEffect>, MobEffectInstance> targetEffects, List<HempEffectSpec> effects) {
for (HempEffectSpec effect : effects) {
Holder<MobEffect> effectType = CanapliaSmokeEffects.resolve(effect.effectId());
MobEffectInstance candidate = CanapliaSmokeEffects.createEffect(effect);
MobEffectInstance existing = targetEffects.get(effectType);
if (existing == null || shouldReplace(existing, candidate)) {
targetEffects.put(effectType, candidate);
}
}
}
private static boolean shouldReplace(MobEffectInstance existing, MobEffectInstance candidate) {
if (candidate.getAmplifier() != existing.getAmplifier()) {
return candidate.getAmplifier() > existing.getAmplifier();
}
return candidate.getDuration() > existing.getDuration();
}
private static void syncVisualState(ServerPlayer player, HempPlayerEffectState state, long currentTick, boolean clearFirst) {
HempEffectSession visualSession = state.visualSession();
if (clearFirst || visualSession == null) {
CanapliaEffectPayloads.sendClear(player);
}
if (visualSession != null) {
CanapliaEffectPayloads.sendNormalProfile(player, visualSession.profile().normalVisualProfile(), visualSession.remainingMainTicks(currentTick));
}
if (state.hasBadTripStarted() && state.badTripProfile() != null) {
CanapliaEffectPayloads.sendBadTripProfile(player, state.badTripProfile().badTripVisualProfile(), state.remainingBadTripTicks(currentTick));
}
}
private static void clearState(ServerPlayer player, HempPlayerEffectState state, boolean syncClient) {
Set<Holder<MobEffect>> managedTypes = new HashSet<>(state.baselineEffects().keySet());
for (HempEffectSession session : state.sessions()) {
for (String effectId : session.profile().allEffectIds()) {
managedTypes.add(CanapliaSmokeEffects.resolve(effectId));
}
}
if (state.badTripProfile() != null) {
for (String effectId : state.badTripProfile().allEffectIds()) {
managedTypes.add(CanapliaSmokeEffects.resolve(effectId));
}
}
for (Holder<MobEffect> effectType : managedTypes) {
player.removeEffect(effectType);
}
state.baselineEffects().values().forEach(effect -> player.addEffect(new MobEffectInstance(effect)));
if (syncClient) {
CanapliaEffectPayloads.sendClear(player);
}
}
}
@@ -0,0 +1,62 @@
package io.github.koka.canaplia.effect;
import java.util.ArrayList;
import java.util.List;
final class HempEffectSession {
private HempEffectSource source;
private final HempResolvedEffectProfile profile;
private long mainEndTick;
private long lastTouchedTick;
private HempEffectSession(HempEffectSource source, HempResolvedEffectProfile profile, long mainEndTick, long lastTouchedTick) {
this.source = source;
this.profile = profile;
this.mainEndTick = mainEndTick;
this.lastTouchedTick = lastTouchedTick;
}
static HempEffectSession start(HempEffectSource source, HempResolvedEffectProfile profile, long currentTick) {
return new HempEffectSession(source, profile, currentTick + profile.mainDurationTicks(), currentTick);
}
HempEffectSource source() {
return this.source;
}
HempResolvedEffectProfile profile() {
return this.profile;
}
void refresh(HempEffectSource source, long currentTick) {
this.source = source;
this.mainEndTick = currentTick + this.profile.mainDurationTicks();
this.lastTouchedTick = currentTick;
}
boolean isExpired(long currentTick) {
return currentTick >= this.mainEndTick;
}
int remainingMainTicks(long currentTick) {
return (int) Math.max(0L, this.mainEndTick - currentTick);
}
long lastTouchedTick() {
return this.lastTouchedTick;
}
List<HempEffectSpec> activeMainEffects(long currentTick) {
int remainingTicks = this.remainingMainTicks(currentTick);
if (remainingTicks <= 0) {
return List.of();
}
List<HempEffectSpec> effects = new ArrayList<>(this.profile.mainEffects().size());
for (HempEffectSpec effect : this.profile.mainEffects()) {
effects.add(effect.withDurationTicks(Math.min(effect.durationTicks(), remainingTicks)));
}
return List.copyOf(effects);
}
}
@@ -0,0 +1,6 @@
package io.github.koka.canaplia.effect;
public enum HempEffectSource {
JOINT,
RESIN_AURA
}
@@ -0,0 +1,21 @@
package io.github.koka.canaplia.effect;
public record HempEffectSpec(String effectId, int amplifier, int durationTicks) {
public HempEffectSpec {
if (effectId == null || effectId.isBlank()) {
throw new IllegalArgumentException("Effect id must not be blank.");
}
if (amplifier < 0) {
throw new IllegalArgumentException("Amplifier must not be negative for " + effectId + ".");
}
if (durationTicks <= 0) {
throw new IllegalArgumentException("Duration must be positive for " + effectId + ".");
}
}
public HempEffectSpec withDurationTicks(int updatedDurationTicks) {
return new HempEffectSpec(this.effectId, this.amplifier, updatedDurationTicks);
}
}
@@ -0,0 +1,6 @@
package io.github.koka.canaplia.effect;
public enum HempMainEffectMode {
DIRECT,
RANDOM_PICK
}
@@ -0,0 +1,166 @@
package io.github.koka.canaplia.effect;
import io.github.koka.canaplia.hemp.HempVariant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.minecraft.world.effect.MobEffect;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.core.Holder;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.RandomSource;
final class HempPlayerEffectState {
private final EnumMap<HempVariant, HempEffectSession> sessions = new EnumMap<>(HempVariant.class);
private final Map<Holder<MobEffect>, MobEffectInstance> baselineEffects = new HashMap<>();
private HempResolvedEffectProfile badTripProfile;
private Long badTripStartTick;
private Long badTripEndTick;
private boolean badTripStarted;
HempEffectSession session(HempVariant variant) {
return this.sessions.get(variant);
}
Collection<HempEffectSession> sessions() {
return this.sessions.values();
}
Map<Holder<MobEffect>, MobEffectInstance> baselineEffects() {
return this.baselineEffects;
}
HempResolvedEffectProfile badTripProfile() {
return this.badTripProfile;
}
HempEffectSession upsertSession(HempEffectSource source, HempResolvedEffectProfile profile, long currentTick) {
HempEffectSession session = this.sessions.get(profile.variant());
if (session != null) {
session.refresh(source, currentTick);
return session;
}
HempEffectSession newSession = HempEffectSession.start(source, profile, currentTick);
this.sessions.put(profile.variant(), newSession);
return newSession;
}
void captureBaseline(ServerPlayer player, Set<String> effectIds) {
for (String effectId : effectIds) {
Holder<MobEffect> effectType = CanapliaSmokeEffects.resolve(effectId);
if (this.baselineEffects.containsKey(effectType)) {
continue;
}
MobEffectInstance activeEffect = player.getEffect(effectType);
if (activeEffect != null) {
this.baselineEffects.put(effectType, new MobEffectInstance(activeEffect));
}
}
}
boolean pruneExpiredSessions(long currentTick) {
boolean removed = false;
Iterator<Map.Entry<HempVariant, HempEffectSession>> iterator = this.sessions.entrySet().iterator();
while (iterator.hasNext()) {
if (iterator.next().getValue().isExpired(currentTick)) {
iterator.remove();
removed = true;
}
}
return removed;
}
HempEffectSession visualSession() {
return this.sessions.values().stream().max(Comparator.comparingLong(HempEffectSession::lastTouchedTick)).orElse(null);
}
void rerollBadTrip(HempResolvedEffectProfile profile, long currentTick, RandomSource random) {
this.badTripProfile = null;
this.badTripStartTick = null;
this.badTripEndTick = null;
this.badTripStarted = false;
if (random.nextFloat() >= profile.badTripChance()) {
return;
}
this.badTripProfile = profile;
this.badTripStartTick = currentTick + profile.badTripDelay().roll(random);
}
void refreshActiveBadTrip(HempVariant variant, long currentTick) {
if (!this.badTripStarted || this.badTripProfile == null || this.badTripProfile.variant() != variant || this.badTripEndTick == null) {
return;
}
this.badTripEndTick = Math.max(this.badTripEndTick, currentTick + this.badTripProfile.badTripDurationTicks());
}
boolean shouldStartBadTrip(long currentTick) {
return !this.badTripStarted && this.badTripStartTick != null && currentTick >= this.badTripStartTick;
}
void startBadTrip(long currentTick) {
if (this.badTripProfile == null) {
return;
}
this.badTripStarted = true;
this.badTripEndTick = currentTick + this.badTripProfile.badTripDurationTicks();
}
boolean expireBadTripIfNeeded(long currentTick) {
if (!this.badTripStarted || this.badTripEndTick == null || currentTick < this.badTripEndTick) {
return false;
}
this.badTripProfile = null;
this.badTripStartTick = null;
this.badTripEndTick = null;
this.badTripStarted = false;
return true;
}
boolean hasBadTripStarted() {
return this.badTripStarted;
}
int remainingBadTripTicks(long currentTick) {
if (!this.badTripStarted || this.badTripEndTick == null) {
return 0;
}
return (int) Math.max(0L, this.badTripEndTick - currentTick);
}
List<HempEffectSpec> activeBadTripEffects(long currentTick) {
if (!this.badTripStarted || this.badTripProfile == null) {
return List.of();
}
int remainingTicks = this.remainingBadTripTicks(currentTick);
if (remainingTicks <= 0) {
return List.of();
}
List<HempEffectSpec> effects = new ArrayList<>(this.badTripProfile.badTripEffects().size());
for (HempEffectSpec effect : this.badTripProfile.badTripEffects()) {
effects.add(effect.withDurationTicks(Math.min(effect.durationTicks(), remainingTicks)));
}
return List.copyOf(effects);
}
boolean isEmpty() {
return this.sessions.isEmpty() && this.badTripProfile == null;
}
}
@@ -0,0 +1,33 @@
package io.github.koka.canaplia.effect;
import io.github.koka.canaplia.hemp.HempVariant;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
public record HempResolvedEffectProfile(
HempEffectSource source,
HempVariant variant,
String jsonId,
String nameFr,
int mainDurationTicks,
List<HempEffectSpec> mainEffects,
HempVisualProfile normalVisualProfile,
float badTripChance,
HempDelayRange badTripDelay,
int badTripDurationTicks,
List<HempEffectSpec> badTripEffects,
HempVisualProfile badTripVisualProfile
) {
public HempResolvedEffectProfile {
mainEffects = List.copyOf(mainEffects);
badTripEffects = List.copyOf(badTripEffects);
}
public Set<String> allEffectIds() {
Set<String> effectIds = new LinkedHashSet<>();
this.mainEffects.stream().map(HempEffectSpec::effectId).forEach(effectIds::add);
this.badTripEffects.stream().map(HempEffectSpec::effectId).forEach(effectIds::add);
return effectIds;
}
}
@@ -0,0 +1,63 @@
package io.github.koka.canaplia.effect;
import io.github.koka.canaplia.hemp.HempVariant;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.util.RandomSource;
public record HempVarietyDefinition(
String jsonId,
HempVariant variant,
String nameFr,
String rarity,
String colorFamily,
int mainDurationTicks,
HempMainEffectMode mainEffectMode,
List<HempEffectSpec> mainEffects,
List<HempEffectSpec> mainEffectsPool,
int mainEffectsPickCount,
HempVisualProfile normalVisualProfile,
float badTripChance,
HempDelayRange badTripDelay,
int badTripDurationTicks,
List<HempEffectSpec> badTripEffects,
HempVisualProfile badTripVisualProfile
) {
public HempVarietyDefinition {
mainEffects = List.copyOf(mainEffects);
mainEffectsPool = List.copyOf(mainEffectsPool);
badTripEffects = List.copyOf(badTripEffects);
}
public HempResolvedEffectProfile resolve(HempEffectSource source, RandomSource random) {
return new HempResolvedEffectProfile(
source,
this.variant,
this.jsonId,
this.nameFr,
this.mainDurationTicks,
this.resolveMainEffects(random),
this.normalVisualProfile,
this.badTripChance,
this.badTripDelay,
this.badTripDurationTicks,
this.badTripEffects,
this.badTripVisualProfile
);
}
private List<HempEffectSpec> resolveMainEffects(RandomSource random) {
if (this.mainEffectMode == HempMainEffectMode.DIRECT) {
return this.mainEffects;
}
List<HempEffectSpec> pool = new ArrayList<>(this.mainEffectsPool);
List<HempEffectSpec> picked = new ArrayList<>(this.mainEffectsPickCount);
for (int pick = 0; pick < this.mainEffectsPickCount; pick++) {
picked.add(pool.remove(random.nextInt(pool.size())));
}
return List.copyOf(picked);
}
}
@@ -0,0 +1,45 @@
package io.github.koka.canaplia.effect;
import io.netty.buffer.ByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.codec.ByteBufCodecs;
public record HempVisualProfile(
String palette,
float intensity,
float tintStrength,
float saturationDelta,
float contrastDelta,
float vignetteStrength,
float ghostingStrength,
float wobbleStrength,
float pulseStrength
) {
public static final StreamCodec<ByteBuf, HempVisualProfile> PACKET_CODEC = StreamCodec.composite(
ByteBufCodecs.STRING_UTF8,
HempVisualProfile::palette,
ByteBufCodecs.FLOAT,
HempVisualProfile::intensity,
ByteBufCodecs.FLOAT,
HempVisualProfile::tintStrength,
ByteBufCodecs.FLOAT,
HempVisualProfile::saturationDelta,
ByteBufCodecs.FLOAT,
HempVisualProfile::contrastDelta,
ByteBufCodecs.FLOAT,
HempVisualProfile::vignetteStrength,
ByteBufCodecs.FLOAT,
HempVisualProfile::ghostingStrength,
ByteBufCodecs.FLOAT,
HempVisualProfile::wobbleStrength,
ByteBufCodecs.FLOAT,
HempVisualProfile::pulseStrength,
HempVisualProfile::new
);
public HempVisualProfile {
if (palette == null || palette.isBlank()) {
throw new IllegalArgumentException("Visual palette must not be blank.");
}
}
}
@@ -0,0 +1,54 @@
package io.github.koka.canaplia.effect;
public final class HempVisualStateStore {
private static HempVisualProfile normalProfile;
private static HempVisualProfile badTripProfile;
private static int normalTicksRemaining;
private static int badTripTicksRemaining;
private HempVisualStateStore() {
}
public static void applyNormal(HempVisualProfile profile, int remainingTicks) {
normalProfile = profile;
normalTicksRemaining = Math.max(0, remainingTicks);
}
public static void applyBadTrip(HempVisualProfile profile, int remainingTicks) {
badTripProfile = profile;
badTripTicksRemaining = Math.max(0, remainingTicks);
}
public static HempVisualProfile normalProfile() {
return normalProfile;
}
public static HempVisualProfile badTripProfile() {
return badTripProfile;
}
public static int normalTicksRemaining() {
return normalTicksRemaining;
}
public static int badTripTicksRemaining() {
return badTripTicksRemaining;
}
public static void tick() {
if (normalTicksRemaining > 0 && --normalTicksRemaining == 0) {
normalProfile = null;
}
if (badTripTicksRemaining > 0 && --badTripTicksRemaining == 0) {
badTripProfile = null;
}
}
public static void clear() {
normalProfile = null;
badTripProfile = null;
normalTicksRemaining = 0;
badTripTicksRemaining = 0;
}
}
@@ -0,0 +1,29 @@
package io.github.koka.canaplia.effect.network;
import io.github.koka.canaplia.effect.HempVisualProfile;
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.server.level.ServerPlayer;
public final class CanapliaEffectPayloads {
private CanapliaEffectPayloads() {
}
public static void register() {
PayloadTypeRegistry.clientboundPlay().register(HempVisualSyncPayload.ID, HempVisualSyncPayload.CODEC);
PayloadTypeRegistry.clientboundPlay().register(HempBadTripPayload.ID, HempBadTripPayload.CODEC);
PayloadTypeRegistry.clientboundPlay().register(HempVisualClearPayload.ID, HempVisualClearPayload.CODEC);
}
public static void sendNormalProfile(ServerPlayer player, HempVisualProfile profile, int remainingTicks) {
ServerPlayNetworking.send(player, new HempVisualSyncPayload(profile, remainingTicks));
}
public static void sendBadTripProfile(ServerPlayer player, HempVisualProfile profile, int remainingTicks) {
ServerPlayNetworking.send(player, new HempBadTripPayload(profile, remainingTicks));
}
public static void sendClear(ServerPlayer player) {
ServerPlayNetworking.send(player, HempVisualClearPayload.INSTANCE);
}
}
@@ -0,0 +1,24 @@
package io.github.koka.canaplia.effect.network;
import io.github.koka.canaplia.CanapliaMod;
import io.github.koka.canaplia.effect.HempVisualProfile;
import io.netty.buffer.ByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
public record HempBadTripPayload(HempVisualProfile profile, int remainingTicks) implements CustomPacketPayload {
public static final Type<HempBadTripPayload> ID = new Type<>(CanapliaMod.id("hemp_bad_trip"));
public static final StreamCodec<ByteBuf, HempBadTripPayload> CODEC = StreamCodec.composite(
HempVisualProfile.PACKET_CODEC,
HempBadTripPayload::profile,
ByteBufCodecs.VAR_INT,
HempBadTripPayload::remainingTicks,
HempBadTripPayload::new
);
@Override
public Type<? extends CustomPacketPayload> type() {
return ID;
}
}
@@ -0,0 +1,17 @@
package io.github.koka.canaplia.effect.network;
import io.github.koka.canaplia.CanapliaMod;
import io.netty.buffer.ByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
public record HempVisualClearPayload() implements CustomPacketPayload {
public static final HempVisualClearPayload INSTANCE = new HempVisualClearPayload();
public static final Type<HempVisualClearPayload> ID = new Type<>(CanapliaMod.id("hemp_visual_clear"));
public static final StreamCodec<ByteBuf, HempVisualClearPayload> CODEC = StreamCodec.unit(INSTANCE);
@Override
public Type<? extends CustomPacketPayload> type() {
return ID;
}
}
@@ -0,0 +1,24 @@
package io.github.koka.canaplia.effect.network;
import io.github.koka.canaplia.CanapliaMod;
import io.github.koka.canaplia.effect.HempVisualProfile;
import io.netty.buffer.ByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
public record HempVisualSyncPayload(HempVisualProfile profile, int remainingTicks) implements CustomPacketPayload {
public static final Type<HempVisualSyncPayload> ID = new Type<>(CanapliaMod.id("hemp_visual_sync"));
public static final StreamCodec<ByteBuf, HempVisualSyncPayload> CODEC = StreamCodec.composite(
HempVisualProfile.PACKET_CODEC,
HempVisualSyncPayload::profile,
ByteBufCodecs.VAR_INT,
HempVisualSyncPayload::remainingTicks,
HempVisualSyncPayload::new
);
@Override
public Type<? extends CustomPacketPayload> type() {
return ID;
}
}
@@ -0,0 +1,6 @@
package io.github.koka.canaplia.gametest;
public final class CanapliaGameTests {
private CanapliaGameTests() {
}
}
@@ -0,0 +1,49 @@
package io.github.koka.canaplia.hemp;
import net.minecraft.util.StringRepresentable;
public enum HempVariant implements StringRepresentable {
COMMON("common"),
TENDER("tender"),
OLIVE("olive"),
DARK("dark"),
ASHEN("ashen"),
MOSS("moss"),
GOLD("gold"),
AMBER("amber"),
BLUISH("bluish"),
PINK("pink"),
CORAL("coral"),
PURPLE("purple"),
INDIGO("indigo"),
VARIEG("varieg"),
LUNAR("lunar"),
EMBER("ember");
private final String key;
HempVariant(String key) {
this.key = key;
}
public boolean isCommon() {
return this == COMMON;
}
public String itemPath(String basePath) {
return this.isCommon() ? basePath : this.key + "_" + basePath;
}
public String modelSuffix() {
return this.isCommon() ? "" : "_" + this.key;
}
@Override
public String getSerializedName() {
return this.key;
}
public String asString() {
return this.getSerializedName();
}
}
@@ -0,0 +1,200 @@
package io.github.koka.canaplia.item;
import io.github.koka.canaplia.CanapliaMod;
import io.github.koka.canaplia.block.CanapliaBlocks;
import io.github.koka.canaplia.hemp.HempVariant;
import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
import net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.CreativeModeTab;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.core.Registry;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
public final class CanapliaItems {
private static final ResourceKey<CreativeModeTab> NATURAL_BLOCKS_TAB = vanillaTab("natural_blocks");
private static final ResourceKey<CreativeModeTab> INGREDIENTS_TAB = vanillaTab("ingredients");
private static final ResourceKey<CreativeModeTab> FOOD_AND_DRINKS_TAB = vanillaTab("food_and_drinks");
private static final ResourceKey<CreativeModeTab> BUILDING_BLOCKS_TAB = vanillaTab("building_blocks");
private static final EnumMap<HempVariant, Item> SEEDS_BY_VARIANT = registerSeeds();
private static final EnumMap<HempVariant, Item> BUDS_BY_VARIANT = registerBuds();
private static final EnumMap<HempVariant, Item> JOINTS_BY_VARIANT = registerJoints();
private static final EnumMap<HempVariant, Item> RESIN_BLOCKS_BY_VARIANT = registerResinBlocks();
public static final Item HEMP_SEEDS = SEEDS_BY_VARIANT.get(HempVariant.COMMON);
public static final Item HEMP_BUD = BUDS_BY_VARIANT.get(HempVariant.COMMON);
public static final Item ROLLED_JOINT = JOINTS_BY_VARIANT.get(HempVariant.COMMON);
public static final Item RESIN_BLOCK = RESIN_BLOCKS_BY_VARIANT.get(HempVariant.COMMON);
private CanapliaItems() {
}
public static Item getSeeds(HempVariant variant) {
return SEEDS_BY_VARIANT.get(variant);
}
public static Item getBud(HempVariant variant) {
return BUDS_BY_VARIANT.get(variant);
}
public static Item getJoint(HempVariant variant) {
return JOINTS_BY_VARIANT.get(variant);
}
public static Item getResinBlock(HempVariant variant) {
return RESIN_BLOCKS_BY_VARIANT.get(variant);
}
public static Optional<HempVariant> getBudVariant(ItemStack stack) {
return resolveVariant(stack.getItem(), BUDS_BY_VARIANT);
}
public static Optional<HempVariant> getJointVariant(ItemStack stack) {
return resolveVariant(stack.getItem(), JOINTS_BY_VARIANT);
}
public static Optional<HempVariant> getResinBlockVariant(ItemStack stack) {
return resolveVariant(stack.getItem(), RESIN_BLOCKS_BY_VARIANT);
}
public static void register() {
CreativeModeTabEvents.modifyOutputEvent(NATURAL_BLOCKS_TAB).register(entries -> {
Item previous = Items.WHEAT_SEEDS;
for (HempVariant variant : HempVariant.values()) {
Item seed = getSeeds(variant);
entries.insertAfter(previous, seed);
previous = seed;
}
for (HempVariant variant : HempVariant.values()) {
Item bud = getBud(variant);
entries.insertAfter(previous, bud);
previous = bud;
}
for (HempVariant variant : HempVariant.values()) {
Item resinBlock = getResinBlock(variant);
entries.insertAfter(previous, resinBlock);
previous = resinBlock;
}
});
CreativeModeTabEvents.modifyOutputEvent(INGREDIENTS_TAB).register(entries -> {
Item previous = Items.WHEAT_SEEDS;
for (HempVariant variant : HempVariant.values()) {
Item seed = getSeeds(variant);
entries.insertAfter(previous, seed);
previous = seed;
}
for (HempVariant variant : HempVariant.values()) {
Item bud = getBud(variant);
entries.insertAfter(previous, bud);
previous = bud;
}
for (HempVariant variant : HempVariant.values()) {
Item resinBlock = getResinBlock(variant);
entries.insertAfter(previous, resinBlock);
previous = resinBlock;
}
});
CreativeModeTabEvents.modifyOutputEvent(FOOD_AND_DRINKS_TAB).register(entries -> {
Item previous = Items.POTION;
for (HempVariant variant : HempVariant.values()) {
Item joint = getJoint(variant);
entries.insertAfter(previous, joint);
previous = joint;
}
});
CreativeModeTabEvents.modifyOutputEvent(BUILDING_BLOCKS_TAB).register(entries -> {
Item previous = Items.MUD;
for (HempVariant variant : HempVariant.values()) {
Item resinBlock = getResinBlock(variant);
entries.insertAfter(previous, resinBlock);
previous = resinBlock;
}
});
}
private static Item register(ResourceKey<Item> key, Item item) {
return Registry.register(BuiltInRegistries.ITEM, key, item);
}
private static ResourceKey<CreativeModeTab> vanillaTab(String path) {
return ResourceKey.create(Registries.CREATIVE_MODE_TAB, Identifier.withDefaultNamespace(path));
}
private static EnumMap<HempVariant, Item> registerSeeds() {
EnumMap<HempVariant, Item> items = new EnumMap<>(HempVariant.class);
for (HempVariant variant : HempVariant.values()) {
ResourceKey<Item> key = ResourceKey.create(Registries.ITEM, CanapliaMod.id(variant.itemPath("hemp_seeds")));
items.put(
variant,
register(
key,
new HempSeedItem(
variant,
CanapliaBlocks.HEMP_CROP,
new Item.Properties()
.setId(key)
.useItemDescriptionPrefix()
)
)
);
}
return items;
}
private static EnumMap<HempVariant, Item> registerBuds() {
EnumMap<HempVariant, Item> items = new EnumMap<>(HempVariant.class);
for (HempVariant variant : HempVariant.values()) {
ResourceKey<Item> key = ResourceKey.create(Registries.ITEM, CanapliaMod.id(variant.itemPath("hemp_bud")));
items.put(variant, register(key, new Item(new Item.Properties().setId(key))));
}
return items;
}
private static EnumMap<HempVariant, Item> registerJoints() {
EnumMap<HempVariant, Item> items = new EnumMap<>(HempVariant.class);
for (HempVariant variant : HempVariant.values()) {
ResourceKey<Item> key = ResourceKey.create(Registries.ITEM, CanapliaMod.id(variant.itemPath("rolled_joint")));
items.put(variant, register(key, new RolledJointItem(variant, new Item.Properties().stacksTo(16).setId(key))));
}
return items;
}
private static EnumMap<HempVariant, Item> registerResinBlocks() {
EnumMap<HempVariant, Item> items = new EnumMap<>(HempVariant.class);
for (HempVariant variant : HempVariant.values()) {
ResourceKey<Item> key = ResourceKey.create(Registries.ITEM, CanapliaMod.id(variant.itemPath("resin_block")));
items.put(
variant,
register(
key,
new ResinBlockItem(
variant,
CanapliaBlocks.RESIN_BLOCK,
new Item.Properties()
.setId(key)
.useItemDescriptionPrefix()
)
)
);
}
return items;
}
private static Optional<HempVariant> resolveVariant(Item item, EnumMap<HempVariant, Item> itemsByVariant) {
for (Map.Entry<HempVariant, Item> entry : itemsByVariant.entrySet()) {
if (entry.getValue() == item) {
return Optional.of(entry.getKey());
}
}
return Optional.empty();
}
}
@@ -0,0 +1,28 @@
package io.github.koka.canaplia.item;
import io.github.koka.canaplia.block.HempCropBlock;
import io.github.koka.canaplia.hemp.HempVariant;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.context.BlockPlaceContext;
public final class HempSeedItem extends BlockItem {
private final HempVariant variant;
public HempSeedItem(HempVariant variant, Block block, Item.Properties settings) {
super(block, settings);
this.variant = variant;
}
public HempVariant getVariant() {
return this.variant;
}
@Override
protected BlockState getPlacementState(BlockPlaceContext context) {
BlockState state = super.getPlacementState(context);
return state == null ? null : state.setValue(HempCropBlock.VARIANT, this.variant).setValue(HempCropBlock.TALL, false);
}
}
@@ -0,0 +1,28 @@
package io.github.koka.canaplia.item;
import io.github.koka.canaplia.block.ResinBlock;
import io.github.koka.canaplia.hemp.HempVariant;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.context.BlockPlaceContext;
public final class ResinBlockItem extends BlockItem {
private final HempVariant variant;
public ResinBlockItem(HempVariant variant, Block block, Item.Properties settings) {
super(block, settings);
this.variant = variant;
}
public HempVariant getVariant() {
return this.variant;
}
@Override
protected BlockState getPlacementState(BlockPlaceContext context) {
BlockState state = super.getPlacementState(context);
return state == null ? null : state.setValue(ResinBlock.VARIANT, this.variant).setValue(ResinBlock.LIT, false);
}
}
@@ -0,0 +1,85 @@
package io.github.koka.canaplia.item;
import io.github.koka.canaplia.effect.HempEffectController;
import io.github.koka.canaplia.hemp.HempVariant;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.ItemUtils;
import net.minecraft.world.item.ItemUseAnimation;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundSource;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.level.Level;
public final class RolledJointItem extends Item {
private final HempVariant variant;
public RolledJointItem(HempVariant variant, Item.Properties settings) {
super(settings);
this.variant = variant;
}
public HempVariant getVariant() {
return this.variant;
}
@Override
public InteractionResult use(Level world, Player user, InteractionHand hand) {
return ItemUtils.startUsingInstantly(world, user, hand);
}
@Override
public ItemStack finishUsingItem(ItemStack stack, Level world, LivingEntity user) {
if (!world.isClientSide()) {
if (user instanceof ServerPlayer serverPlayer) {
HempEffectController.consumeJoint(serverPlayer, this.variant);
} else {
HempEffectController.applyImmediateEffects(user, this.variant);
}
world.playSound(
null,
user.getX(),
user.getY(),
user.getZ(),
SoundEvents.SMOKER_SMOKE,
SoundSource.PLAYERS,
0.35F,
0.9F + world.getRandom().nextFloat() * 0.2F
);
if (world instanceof ServerLevel serverWorld) {
serverWorld.sendParticles(
ParticleTypes.SMOKE,
user.getX(),
user.getY() + user.getBbHeight() * 0.6D,
user.getZ(),
8,
0.12D,
0.12D,
0.12D,
0.01D
);
}
}
stack.consume(1, user);
return stack;
}
@Override
public ItemUseAnimation getUseAnimation(ItemStack stack) {
return ItemUseAnimation.DRINK;
}
@Override
public int getUseDuration(ItemStack stack, LivingEntity user) {
return 32;
}
}
@@ -0,0 +1,24 @@
package io.github.koka.canaplia.resin;
import io.github.koka.canaplia.hemp.HempVariant;
import io.github.koka.canaplia.item.CanapliaItems;
import net.minecraft.world.item.ItemStack;
public final class CanapliaResinCatalog {
public static final int DEFAULT_BURN_TICKS = 90 * 20;
private CanapliaResinCatalog() {
}
public static ResinProfile resolve(HempVariant sourceVariant) {
return new ResinProfile(sourceVariant, sourceVariant, DEFAULT_BURN_TICKS);
}
public static ItemStack createReward(HempVariant sourceVariant) {
ResinProfile profile = resolve(sourceVariant);
return new ItemStack(CanapliaItems.getResinBlock(profile.blockVariant()));
}
public record ResinProfile(HempVariant sourceVariant, HempVariant blockVariant, int burnTicks) {
}
}
@@ -0,0 +1,123 @@
package io.github.koka.canaplia.survival;
import io.github.koka.canaplia.block.CanapliaBlocks;
import io.github.koka.canaplia.block.HempCropBlock;
import io.github.koka.canaplia.block.HempTopBlock;
import io.github.koka.canaplia.hemp.HempVariant;
import io.github.koka.canaplia.item.CanapliaItems;
import java.util.EnumSet;
import java.util.Optional;
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.CropBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
public final class CanapliaSurvivalObtainment {
private CanapliaSurvivalObtainment() {
}
public static void register() {
PlayerBlockBreakEvents.AFTER.register(CanapliaSurvivalObtainment::afterBlockBreak);
}
private static void afterBlockBreak(Level world, Player player, BlockPos pos, BlockState state, BlockEntity blockEntity) {
if (!(world instanceof ServerLevel serverWorld)) {
return;
}
if (player.isCreative()) {
return;
}
dropDiscoverySeed(serverWorld, pos, state);
dropHarvestSeeds(serverWorld, pos, state);
}
private static void dropDiscoverySeed(ServerLevel world, BlockPos pos, BlockState state) {
Block block = state.getBlock();
float chance = HempDiscoveryLogic.getBaseChance(block);
if (chance <= 0.0F || world.getRandom().nextFloat() >= chance) {
return;
}
Optional<HempVariant> variant = HempDiscoveryLogic.resolveVariant(block, HempEnvironment.probe(world, pos));
if (variant.isEmpty()) {
return;
}
Block.popResource(world, pos, new ItemStack(CanapliaItems.getSeeds(variant.get())));
}
private static void dropHarvestSeeds(ServerLevel world, BlockPos pos, BlockState state) {
Optional<HarvestedPlant> harvestedPlant = resolveHarvestedPlant(world, pos, state);
if (harvestedPlant.isEmpty()) {
return;
}
HarvestedPlant harvest = harvestedPlant.get();
EnumSet<HempVariant> adjacentVariants = collectAdjacentHarvestReadyVariants(world, harvest.basePos());
HempEnvironment environment = HempEnvironment.probe(world, harvest.basePos());
for (HempBreedingLogic.SeedRoll roll : HempBreedingLogic.getHarvestSeedRolls(harvest.variant(), adjacentVariants, environment)) {
if (world.getRandom().nextFloat() < roll.chance()) {
Block.popResource(world, pos, new ItemStack(CanapliaItems.getSeeds(roll.variant())));
}
}
}
private static Optional<HarvestedPlant> resolveHarvestedPlant(ServerLevel world, BlockPos pos, BlockState state) {
if (isHarvestReadyCrop(state)) {
return Optional.of(new HarvestedPlant(pos, state.getValue(HempCropBlock.VARIANT)));
}
if (!state.is(CanapliaBlocks.HEMP_TOP)) {
return Optional.empty();
}
BlockPos basePos = pos.below();
BlockState baseState = world.getBlockState(basePos);
if (
baseState.is(CanapliaBlocks.HEMP_CROP)
&& baseState.getValue(CropBlock.AGE) >= CanapliaBlocks.HEMP_CROP.getMaxAge()
&& baseState.getValue(HempCropBlock.VARIANT) == state.getValue(HempTopBlock.VARIANT)
) {
return Optional.of(new HarvestedPlant(basePos, state.getValue(HempTopBlock.VARIANT)));
}
return Optional.empty();
}
private static EnumSet<HempVariant> collectAdjacentHarvestReadyVariants(ServerLevel world, BlockPos basePos) {
EnumSet<HempVariant> variants = EnumSet.noneOf(HempVariant.class);
for (int dx = -1; dx <= 1; dx++) {
for (int dz = -1; dz <= 1; dz++) {
if (dx == 0 && dz == 0) {
continue;
}
BlockState neighborState = world.getBlockState(basePos.offset(dx, 0, dz));
if (isHarvestReadyCrop(neighborState)) {
variants.add(neighborState.getValue(HempCropBlock.VARIANT));
}
}
}
return variants;
}
private static boolean isHarvestReadyCrop(BlockState state) {
return state.is(CanapliaBlocks.HEMP_CROP)
&& state.getValue(CropBlock.AGE) >= CanapliaBlocks.HEMP_CROP.getMaxAge()
&& state.getValue(HempCropBlock.TALL);
}
private record HarvestedPlant(BlockPos basePos, HempVariant variant) {
}
}
@@ -0,0 +1,75 @@
package io.github.koka.canaplia.survival;
import io.github.koka.canaplia.hemp.HempVariant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
final class HempBreedingLogic {
record SeedRoll(HempVariant variant, float chance) {
}
private HempBreedingLogic() {
}
static List<SeedRoll> getHarvestSeedRolls(HempVariant harvestedVariant, Collection<HempVariant> adjacentVariants, HempEnvironment environment) {
ArrayList<SeedRoll> rolls = new ArrayList<>(4);
if (
(harvestedVariant == HempVariant.ASHEN || harvestedVariant == HempVariant.BLUISH)
&& environment.night()
&& environment.skyVisible()
&& environment.coldBiome()
) {
rolls.add(new SeedRoll(HempVariant.LUNAR, 0.06F));
}
if (matchesPair(harvestedVariant, adjacentVariants, HempVariant.TENDER, HempVariant.OLIVE) && environment.warmBiome() && environment.day() && environment.skyVisible()) {
rolls.add(new SeedRoll(HempVariant.GOLD, 0.12F));
}
if (matchesPair(harvestedVariant, adjacentVariants, HempVariant.DARK, HempVariant.MOSS) && environment.humidArea() && environment.lowLight()) {
rolls.add(new SeedRoll(HempVariant.PURPLE, 0.08F));
}
if (
matchesPair(harvestedVariant, adjacentVariants, HempVariant.TENDER, HempVariant.PURPLE)
&& environment.dawnOrDusk()
&& (
environment.floweringArea()
|| environment.biomeContains("meadow")
|| environment.biomeContains("flower_forest")
|| environment.biomeContains("cherry_grove")
)
) {
rolls.add(new SeedRoll(HempVariant.PINK, 0.10F));
}
if (matchesPair(harvestedVariant, adjacentVariants, HempVariant.AMBER, HempVariant.MOSS) && environment.warmBiome() && environment.waterTouchingFarmland()) {
rolls.add(new SeedRoll(HempVariant.CORAL, 0.10F));
}
if (
matchesPair(harvestedVariant, adjacentVariants, HempVariant.ASHEN, HempVariant.TENDER)
&& (environment.coldBiome() || environment.snowOrIceNearby())
) {
rolls.add(new SeedRoll(HempVariant.BLUISH, 0.09F));
}
if (adjacentVariants.stream().anyMatch(variant -> variant != harvestedVariant) && environment.humidArea()) {
rolls.add(new SeedRoll(HempVariant.VARIEG, 0.06F));
}
return List.copyOf(rolls);
}
private static boolean matchesPair(
HempVariant harvestedVariant,
Collection<HempVariant> adjacentVariants,
HempVariant parentA,
HempVariant parentB
) {
return harvestedVariant == parentA && adjacentVariants.contains(parentB)
|| harvestedVariant == parentB && adjacentVariants.contains(parentA);
}
}
@@ -0,0 +1,115 @@
package io.github.koka.canaplia.survival;
import io.github.koka.canaplia.hemp.HempVariant;
import java.util.Optional;
import net.minecraft.world.level.block.Block;
import net.minecraft.core.registries.BuiltInRegistries;
final class HempDiscoveryLogic {
private HempDiscoveryLogic() {
}
static float getBaseChance(Block block) {
return getBaseChance(BuiltInRegistries.BLOCK.getKey(block).toString());
}
static float getBaseChance(String blockId) {
return switch (blockId) {
case "minecraft:short_grass" -> 0.060F;
case "minecraft:tall_grass" -> 0.100F;
case "minecraft:short_dry_grass" -> 0.060F;
case "minecraft:tall_dry_grass" -> 0.100F;
case "minecraft:fern" -> 0.070F;
case "minecraft:large_fern" -> 0.100F;
case "minecraft:dead_bush" -> 0.100F;
case "minecraft:moss_carpet" -> 0.070F;
case "minecraft:hanging_roots" -> 0.100F;
case "minecraft:cave_vines", "minecraft:cave_vines_plant" -> 0.060F;
case "minecraft:crimson_roots", "minecraft:warped_roots" -> 0.020F;
case "minecraft:nether_sprouts", "minecraft:chorus_plant" -> 0.015F;
case "minecraft:weeping_vines", "minecraft:weeping_vines_plant", "minecraft:twisting_vines", "minecraft:twisting_vines_plant" -> 0.010F;
case "minecraft:chorus_flower" -> 0.020F;
default -> 0.0F;
};
}
static Optional<HempVariant> resolveVariant(Block block, HempEnvironment environment) {
return resolveVariant(BuiltInRegistries.BLOCK.getKey(block).toString(), environment);
}
static Optional<HempVariant> resolveVariant(String blockId, HempEnvironment environment) {
if (blockId.equals("minecraft:short_dry_grass") || blockId.equals("minecraft:tall_dry_grass")) {
return Optional.of(HempVariant.OLIVE);
}
if (blockId.equals("minecraft:short_grass") || blockId.equals("minecraft:tall_grass")) {
if (
environment.dryArea()
|| environment.biomeContains("savanna")
|| environment.biomeContains("desert")
|| environment.biomeContains("badlands")
) {
return Optional.of(HempVariant.OLIVE);
}
if (
environment.floweringArea()
|| environment.biomeContains("meadow")
|| environment.biomeContains("flower_forest")
|| environment.biomeContains("cherry_grove")
) {
return Optional.of(HempVariant.TENDER);
}
return Optional.of(HempVariant.COMMON);
}
if (blockId.equals("minecraft:fern") || blockId.equals("minecraft:large_fern")) {
if (
environment.coldBiome()
|| environment.snowOrIceNearby()
|| environment.biomeContains("snowy")
|| environment.biomeContains("grove")
) {
return Optional.of(HempVariant.ASHEN);
}
return Optional.of(HempVariant.DARK);
}
if (blockId.equals("minecraft:dead_bush")) {
if (environment.dryArea() || environment.biomeContains("badlands") || environment.biomeContains("desert")) {
return Optional.of(HempVariant.AMBER);
}
return Optional.of(HempVariant.OLIVE);
}
if (
blockId.equals("minecraft:moss_carpet")
|| blockId.equals("minecraft:hanging_roots")
|| blockId.equals("minecraft:cave_vines")
|| blockId.equals("minecraft:cave_vines_plant")
) {
return Optional.of(HempVariant.MOSS);
}
if (
blockId.equals("minecraft:crimson_roots")
|| blockId.equals("minecraft:warped_roots")
|| blockId.equals("minecraft:nether_sprouts")
|| blockId.equals("minecraft:weeping_vines")
|| blockId.equals("minecraft:weeping_vines_plant")
|| blockId.equals("minecraft:twisting_vines")
|| blockId.equals("minecraft:twisting_vines_plant")
) {
return Optional.of(HempVariant.EMBER);
}
if (blockId.equals("minecraft:chorus_plant") || blockId.equals("minecraft:chorus_flower")) {
return Optional.of(HempVariant.INDIGO);
}
return Optional.empty();
}
}
@@ -0,0 +1,118 @@
package io.github.koka.canaplia.survival;
import java.util.function.Predicate;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.material.Fluids;
import net.minecraft.tags.BlockTags;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.biome.Biome;
record HempEnvironment(
String biomePath,
boolean warmBiome,
boolean coldBiome,
boolean humidArea,
boolean dryArea,
boolean lowLight,
boolean floweringArea,
boolean snowOrIceNearby,
boolean waterTouchingFarmland,
boolean skyVisible,
boolean day,
boolean night,
boolean dawnOrDusk
) {
static HempEnvironment probe(ServerLevel world, BlockPos pos) {
Biome biome = world.getBiome(pos).value();
String biomePath = world.getBiome(pos)
.unwrapKey()
.map(key -> key.identifier().getPath())
.orElse("");
float temperature = biome.getBaseTemperature();
boolean coldBiome = biome.shouldSnow(world, pos) || temperature <= 0.30F;
boolean warmBiome = !coldBiome && temperature >= 0.90F;
boolean humidArea = world.dimension() != Level.NETHER
&& (
biome.hasPrecipitation()
|| biomePath.contains("lush_caves")
|| biomePath.contains("swamp")
|| biomePath.contains("mangrove")
|| hasNearbyWater(world, pos, 2)
);
long timeOfDay = world.getLevelData().getGameTime() % 24000L;
return new HempEnvironment(
biomePath,
warmBiome,
coldBiome,
humidArea,
warmBiome && !humidArea,
world.getMaxLocalRawBrightness(pos) <= 7,
hasNearbyFlowers(world, pos, 2),
hasNearbySnowOrIce(world, pos, 2),
hasWaterTouchingFarmland(world, pos.below()),
world.canSeeSkyFromBelowWater(pos),
timeOfDay < 12000L,
timeOfDay >= 13000L && timeOfDay < 23000L,
timeOfDay <= 1500L || timeOfDay >= 23000L || timeOfDay >= 11500L && timeOfDay <= 13000L
);
}
boolean biomeContains(String needle) {
return this.biomePath.contains(needle);
}
private static boolean hasNearbyFlowers(ServerLevel world, BlockPos center, int radius) {
return hasNearbyBlock(world, center, radius, state -> state.is(BlockTags.FLOWERS));
}
private static boolean hasNearbySnowOrIce(ServerLevel world, BlockPos center, int radius) {
return hasNearbyBlock(
world,
center,
radius,
state -> state.is(BlockTags.ICE) || state.is(Blocks.SNOW) || state.is(Blocks.SNOW_BLOCK) || state.is(Blocks.POWDER_SNOW)
);
}
private static boolean hasNearbyWater(ServerLevel world, BlockPos center, int radius) {
return hasNearbyBlock(world, center, radius, state -> state.getFluidState().getType().isSame(Fluids.WATER));
}
private static boolean hasWaterTouchingFarmland(ServerLevel world, BlockPos farmlandPos) {
if (!world.getBlockState(farmlandPos).is(Blocks.FARMLAND)) {
return false;
}
for (int dx = -1; dx <= 1; dx++) {
for (int dz = -1; dz <= 1; dz++) {
if (Math.abs(dx) + Math.abs(dz) != 1) {
continue;
}
if (world.getBlockState(farmlandPos.offset(dx, 0, dz)).getFluidState().getType().isSame(Fluids.WATER)) {
return true;
}
}
}
return false;
}
private static boolean hasNearbyBlock(ServerLevel world, BlockPos center, int radius, Predicate<BlockState> predicate) {
for (int dx = -radius; dx <= radius; dx++) {
for (int dy = -1; dy <= 1; dy++) {
for (int dz = -radius; dz <= radius; dz++) {
if (predicate.test(world.getBlockState(center.offset(dx, dy, dz)))) {
return true;
}
}
}
}
return false;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,52 @@
{
"variants": {
"variant=common": {
"model": "canaplia:block/hemp_tall_top"
},
"variant=tender": {
"model": "canaplia:block/hemp_tall_top_tender"
},
"variant=olive": {
"model": "canaplia:block/hemp_tall_top_olive"
},
"variant=dark": {
"model": "canaplia:block/hemp_tall_top_dark"
},
"variant=ashen": {
"model": "canaplia:block/hemp_tall_top_ashen"
},
"variant=moss": {
"model": "canaplia:block/hemp_tall_top_moss"
},
"variant=gold": {
"model": "canaplia:block/hemp_tall_top_gold"
},
"variant=amber": {
"model": "canaplia:block/hemp_tall_top_amber"
},
"variant=bluish": {
"model": "canaplia:block/hemp_tall_top_bluish"
},
"variant=pink": {
"model": "canaplia:block/hemp_tall_top_pink"
},
"variant=coral": {
"model": "canaplia:block/hemp_tall_top_coral"
},
"variant=purple": {
"model": "canaplia:block/hemp_tall_top_purple"
},
"variant=indigo": {
"model": "canaplia:block/hemp_tall_top_indigo"
},
"variant=varieg": {
"model": "canaplia:block/hemp_tall_top_varieg"
},
"variant=lunar": {
"model": "canaplia:block/hemp_tall_top_lunar"
},
"variant=ember": {
"model": "canaplia:block/hemp_tall_top_ember"
}
}
}
@@ -0,0 +1,100 @@
{
"variants": {
"lit=false,variant=common": {
"model": "canaplia:block/resin_block"
},
"lit=true,variant=common": {
"model": "canaplia:block/resin_block"
},
"lit=false,variant=tender": {
"model": "canaplia:block/tender_resin_block"
},
"lit=true,variant=tender": {
"model": "canaplia:block/tender_resin_block"
},
"lit=false,variant=olive": {
"model": "canaplia:block/olive_resin_block"
},
"lit=true,variant=olive": {
"model": "canaplia:block/olive_resin_block"
},
"lit=false,variant=dark": {
"model": "canaplia:block/dark_resin_block"
},
"lit=true,variant=dark": {
"model": "canaplia:block/dark_resin_block"
},
"lit=false,variant=ashen": {
"model": "canaplia:block/ashen_resin_block"
},
"lit=true,variant=ashen": {
"model": "canaplia:block/ashen_resin_block"
},
"lit=false,variant=moss": {
"model": "canaplia:block/moss_resin_block"
},
"lit=true,variant=moss": {
"model": "canaplia:block/moss_resin_block"
},
"lit=false,variant=gold": {
"model": "canaplia:block/gold_resin_block"
},
"lit=true,variant=gold": {
"model": "canaplia:block/gold_resin_block"
},
"lit=false,variant=amber": {
"model": "canaplia:block/amber_resin_block"
},
"lit=true,variant=amber": {
"model": "canaplia:block/amber_resin_block"
},
"lit=false,variant=bluish": {
"model": "canaplia:block/bluish_resin_block"
},
"lit=true,variant=bluish": {
"model": "canaplia:block/bluish_resin_block"
},
"lit=false,variant=pink": {
"model": "canaplia:block/pink_resin_block"
},
"lit=true,variant=pink": {
"model": "canaplia:block/pink_resin_block"
},
"lit=false,variant=coral": {
"model": "canaplia:block/coral_resin_block"
},
"lit=true,variant=coral": {
"model": "canaplia:block/coral_resin_block"
},
"lit=false,variant=purple": {
"model": "canaplia:block/purple_resin_block"
},
"lit=true,variant=purple": {
"model": "canaplia:block/purple_resin_block"
},
"lit=false,variant=indigo": {
"model": "canaplia:block/indigo_resin_block"
},
"lit=true,variant=indigo": {
"model": "canaplia:block/indigo_resin_block"
},
"lit=false,variant=varieg": {
"model": "canaplia:block/varieg_resin_block"
},
"lit=true,variant=varieg": {
"model": "canaplia:block/varieg_resin_block"
},
"lit=false,variant=lunar": {
"model": "canaplia:block/lunar_resin_block"
},
"lit=true,variant=lunar": {
"model": "canaplia:block/lunar_resin_block"
},
"lit=false,variant=ember": {
"model": "canaplia:block/ember_resin_block"
},
"lit=true,variant=ember": {
"model": "canaplia:block/ember_resin_block"
}
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/amber_hemp_bud"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/amber_hemp_seeds"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/amber_resin_block"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/amber_rolled_joint"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/ashen_hemp_bud"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/ashen_hemp_seeds"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/ashen_resin_block"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/ashen_rolled_joint"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/bluish_hemp_bud"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/bluish_hemp_seeds"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/bluish_resin_block"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/bluish_rolled_joint"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/coral_hemp_bud"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/coral_hemp_seeds"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/coral_resin_block"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/coral_rolled_joint"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/dark_hemp_bud"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/dark_hemp_seeds"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/dark_resin_block"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/dark_rolled_joint"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/ember_hemp_bud"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/ember_hemp_seeds"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/ember_resin_block"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/ember_rolled_joint"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/gold_hemp_bud"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/gold_hemp_seeds"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/gold_resin_block"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/gold_rolled_joint"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/hemp_bud"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/hemp_seeds"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/indigo_hemp_bud"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/indigo_hemp_seeds"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/indigo_resin_block"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/indigo_rolled_joint"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "canaplia:item/lunar_hemp_bud"
}
}

Some files were not shown because too many files have changed in this diff Show More