Initial sanctuary sources
This commit is contained in:
+24
@@ -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/
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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.
|
||||
|
@@ -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.
|
||||
@@ -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 |
@@ -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
|
||||
+253
@@ -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" "$@"
|
||||
Vendored
+93
@@ -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
@@ -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)));
|
||||
}
|
||||
}
|
||||
+22
@@ -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() {
|
||||
}
|
||||
}
|
||||
+93
@@ -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));
|
||||
}
|
||||
}
|
||||
+142
@@ -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;
|
||||
}
|
||||
}
|
||||
+46
@@ -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);
|
||||
}
|
||||
}
|
||||
+132
@@ -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) {
|
||||
}
|
||||
}
|
||||
+59
@@ -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));
|
||||
}
|
||||
}
|
||||
+11
@@ -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();
|
||||
}
|
||||
}
|
||||
+199
@@ -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
|
||||
}
|
||||
+166
@@ -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;
|
||||
}
|
||||
}
|
||||
+33
@@ -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;
|
||||
}
|
||||
}
|
||||
+63
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -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;
|
||||
}
|
||||
}
|
||||
+29
@@ -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);
|
||||
}
|
||||
}
|
||||
+24
@@ -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;
|
||||
}
|
||||
}
|
||||
+17
@@ -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;
|
||||
}
|
||||
}
|
||||
+24
@@ -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;
|
||||
}
|
||||
}
|
||||
+24
@@ -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) {
|
||||
}
|
||||
}
|
||||
+123
@@ -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) {
|
||||
}
|
||||
}
|
||||
+75
@@ -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);
|
||||
}
|
||||
}
|
||||
+115
@@ -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
Reference in New Issue
Block a user