Compare commits

..
Author SHA1 Message Date
Nadja Reitzenstein 78dfb30759 Add a hacky riding debug system
Enable it with `/debugutils cobblemon:riding true`
2025-10-02 19:02:27 +02:00
895 changed files with 5078 additions and 20690 deletions
+1 -2
View File
@@ -12,8 +12,7 @@ out
*.ipr
*.iws
*.iml
.idea/*
!.idea/icon.png
.idea
# gradle
build
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

+1 -9
View File
@@ -12,14 +12,6 @@
- SpawningContext is now SpawnablePosition
- WorldSlice is SpawningZone
- SpawningProspector is now SpawningZoneGenerator
- Majorly refactored the hierarchy of Spawner
- The base Spawner interface provides more functions to allow single-point and area spawning given appropriate inputs.
- TickingSpawner is removed in favour of outside code handling ticking logic.
- AreaSpawner is removed.
- BasicSpawner is the first implementation of Spawner which can be used for any purpose.
- PlayerSpawners are now mixin'd into ServerPlayer and ticked from ServerPlayer#tick.
- A hierarchy diagram can be found at `./docs/spawner-hierarchy.png` in the mod repository.
- The SpawnerManager class has been removed as its functionality is all now handled elsewhere.
- Renamed things in Spawn Rules to go with the other renames:
- contextSelector is now spawnablePositionSelector
- contextSelector is now spawnablePositionSelector
- context is now spawnable_position
-15
View File
@@ -34,7 +34,6 @@
- Added Hyper Training items (IV Modification) as well as some additional candy items to do so (Health Candy, Sickly Candy)
- Added Galarica Nut Bushes
- Many Pokémon (mostly cats) are now feared by phantoms
- Combees are now capable of gathering nectar from flowers and delivering it to either Saccharine leaves or hives. (Hive interactions are restricted to wild Combees)
- Lightning is now affected by a Pokemon's ability/typing
- Pokémon with the ability Lightning Rod draw in lightning similar to a lightning rod block albeit with a lower priority and range, gain immunity to lightning damage, and receive a temporary damage buff.
- Pokémon with the ability Motor Drive are immune to lightning damage and receive a temporary speed buff when struck by lightning
@@ -294,7 +293,6 @@
- Tauros - Paldea Blaze
- Tauros - Paldea Combat
- Dewgong
- Exeggutor - Kanto
### Model updates for the following Pokémon
- Cleffa
@@ -405,9 +403,6 @@
- Revavroom
- Nickit
- Thievul
- Litwick
- Lampent
- Chandelure
### Changes
- Changed pokemon caught and seen count to update based on the current pokedex being looked
@@ -421,8 +416,6 @@
- Updated some item tags to better integrate behaviours between Cobblemon, Vanilla Minecraft, and other mods
- Removed Cooked Meat, Raw Meat, Protein Ingredients, and Zinc Ingredients, the first two are now using `c` namespace tags, the latter have better integrated use of tags within their recipes which removes need for custom tags.
- Added our seeds tag into `#c:seeds`, which is now made use of for the Miracle Seed recipe.
- Updated the `#cobblemon:berries` tag to use subtags located in `data/cobblemon/tags/item/berries`, such as `#cobblemon:berries/hp_recovery` for sorting purposes
- Moved the tag `#cobblemon:mutated_berries` to `#cobblemon:berries/mutation_result`
- Added herbs and snowballs to the consumable in PvE and Wild battle tags
- Tweaked the Natural Materials Vanilla file to fit with the changes to tags
- Substantially optimised spawning checks mainly by front-loading biome filtering.
@@ -474,8 +467,6 @@
- Updated `HatchEggEvent.Post` to include the Pokemon that hatched.
- Roseli Berry Trees now naturally generate in their preferred biomes.
- Refactored dynamic lights compat to be more future proof. The dynamic light support is only tested with LambDynamicLights, on NeoForged use that mod in combination with Sinytra Connector
- Shulker Boxes and Traveler's Backpacks can no longer be held by Pokémon. Thanks Monocle ;) You could re-enable this with a datapack but you'd be crazy.
- Moomoo Milk now clears Pokémon stat changes when used in battle.
### Fixes
- Fixed game crashing when removing national pokedex using datapacks
@@ -560,8 +551,6 @@
- Fixed singular Pokémon corruption causing entire storage corruption. Storages will now skip corrupted Pokémon and print an error to console.
- Fixed species additions not being able to properly mark a species as implemented.
- Fixed Pokémon item models not showing a glint when enchanted.
- Fixed some specific bag items not being dropped when used in battle.
- Fixed the "use all berry bait" achievement not being progressed
### Developer
- A finished battle now has winners and losers set inside of `PokemonBattle` instead of them always being empty.
@@ -706,9 +695,6 @@
- Mechanics
- Unlockable Wallpapers
- Starter Data
- Fixed `cobblemon:reel_in_pokemon` criteria not working when used together with a `baitId`
- Also changed the default from `cobblemon:empty_bait` to `any`
- The previous default is still available by using the above as baitId
### Particles
Added new/updated particles for the following moves:
@@ -1004,7 +990,6 @@ Added new/updated particles for the following moves:
- Parametric particle motion now works.
- Event-spawned particles now work.
- Particles can now have independent coordinate spaces.
- Fixed suffocation of Pokémon when a pokeball breaks near a wall
### Developer
- Updated the Pokédex data updated events to always include a `Pokemon` instance, and optionally a `DisguiseData` instance.
@@ -1,22 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common;
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner;
/**
* Used for mixin shenanigans. ServerPlayers now store their Cobblemon spawner
* inside, so this interface helps execute that.
*
* @author Hiroku
* @since October 27th, 2025
*/
public interface PlayerSpawnerGetter {
PlayerSpawner getPlayerSpawner();
}
@@ -1,100 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin;
import com.cobblemon.mod.common.CobblemonMemories;
import com.cobblemon.mod.common.api.ai.config.task.PathToBeeHiveTaskConfig;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.BeehiveBlock;
import net.minecraft.world.level.block.HorizontalDirectionalBlock;
import net.minecraft.world.level.block.entity.BeehiveBlockEntity;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(BeehiveBlockEntity.Occupant.class)
public abstract class BeeOccupantMixin {
@Inject(method = "createEntity", at = @At("RETURN"), cancellable = true)
private void cobblemon$createEntity(Level level, BlockPos pos, CallbackInfoReturnable<Entity> cir) {
// Needing to replicate all the post op stuff on the hive isn't great
if (cir.getReturnValue() == null) {
final BeehiveBlockEntity.Occupant occupant = (BeehiveBlockEntity.Occupant) (Object) this;
CompoundTag compoundTag = occupant.entityData().copyTag();
Entity entity = EntityType.loadEntityRecursive(compoundTag, level, (entityx) -> entityx);
if (entity instanceof PokemonEntity pokemonEntity) {
var state = level.getBlockState(pos);
var newPos = Vec3.ZERO;
var newYaw = 0f;
var brain = pokemonEntity.getBrain();
if (state.is(BlockTags.BEEHIVES)) {
// position in front of the entrance, face away from the block
var facing = state.getValue(HorizontalDirectionalBlock.FACING);
newPos = pos.relative(facing).getCenter();
newYaw = facing.toYRot();
brain.setMemory(CobblemonMemories.INSTANCE.getHIVE_LOCATION(), pos); // This needs to be set in the case that the hive was picked up and moved.
} else {
// Block has likely been destroyed
var center = pos.getCenter();
newPos = new Vec3(
center.x + level.random.nextFloat() * 0.6 - 0.3,
center.y + level.random.nextFloat() * 0.6 - 0.3,
center.z + level.random.nextFloat() * 0.6 - 0.3
);
newYaw = level.random.nextFloat() * 360F;
brain.eraseMemory(CobblemonMemories.INSTANCE.getHIVE_LOCATION());
}
entity.yRotO = newYaw;
entity.setPos(newPos);
// Do honey logic
var hasNectar = brain.getMemory(CobblemonMemories.INSTANCE.getHAS_NECTAR()).orElse(false);
if (hasNectar) {
// Remove nectar and reset got to hive cooldown
brain.eraseMemory(CobblemonMemories.INSTANCE.getHAS_NECTAR());
brain.setMemoryWithExpiry(CobblemonMemories.INSTANCE.getHIVE_COOLDOWN(), true, PathToBeeHiveTaskConfig.STAY_OUT_OF_HIVE_COOLDOWN);
// Increment honey level of the hive
if (state.is(BlockTags.BEEHIVES, (blockStateBase) -> blockStateBase.hasProperty(BeehiveBlock.HONEY_LEVEL))) {
int i = state.getValue(BeehiveBlock.HONEY_LEVEL);
if (i < 5) {
int j = level.random.nextInt(100) == 0 ? 2 : 1;
if (i + j > 5) {
--j;
}
level.setBlockAndUpdate(pos, state.setValue(BeehiveBlock.HONEY_LEVEL, i + j));
}
}
}
cir.setReturnValue(entity);
}
}
}
@Inject(method = "of", at = @At("HEAD"), cancellable = true)
private static void cobblemon$of(Entity entity, CallbackInfoReturnable<BeehiveBlockEntity.Occupant> cir) {
if (entity instanceof PokemonEntity pokemonEntity) {
CompoundTag compoundTag = new CompoundTag();
entity.save(compoundTag);
Boolean hasNectar = pokemonEntity.getBrain().getMemory(CobblemonMemories.INSTANCE.getHAS_NECTAR()).orElse(false);
cir.setReturnValue(new BeehiveBlockEntity.Occupant(CustomData.of(compoundTag), 0, hasNectar ? 2400 : 600));
cir.cancel();
}
}
}
@@ -1,102 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.cobblemon.mod.common.mixin.accessor.BeeDataAccessor;
import com.cobblemon.mod.common.mixin.accessor.BeehiveBlockEntityAccessor;
import com.cobblemon.mod.common.mixin.accessor.BlockEntityAccessor;
import com.google.common.collect.Lists;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.ai.memory.MemoryModuleType;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BeehiveBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.List;
@Mixin(BeehiveBlockEntity.class)
public abstract class BeehiveBlockEntityMixin {
// Accessor for the private 'stored' field in BeehiveBlockEntity
@Accessor("stored")
public abstract List<BeeDataAccessor> getStored();
@Inject(method = "emptyAllLivingFromHive", at = @At("HEAD"))
private void cobblemon$emptyAllLivingFromHive(@Nullable Player player, BlockState state, BeehiveBlockEntity.BeeReleaseStatus releaseStatus, CallbackInfo ci) {
if (player != null) {
final BeehiveBlockEntity blockEntity = (BeehiveBlockEntity) (Object) this;
// if the block is sedated or the player is too far away, no aggro should be applied
if (blockEntity.isSedated() || player.position().distanceToSqr(blockEntity.getBlockPos().getCenter()) > 16.0)
return;
// from this point need to access private members of the block entity
final BeehiveBlockEntityAccessor blockEntityAccessor = (BeehiveBlockEntityAccessor) this;
List<Object> storedList = blockEntityAccessor.getStored();
List<Entity> list = Lists.newArrayList();
storedList.removeIf((beeData) -> {
BeehiveBlockEntity.Occupant occupant = ((BeeDataAccessor) beeData).invokeToOccupant();
// Checking the for the tag allows us to make sure it's one of our pokemon before trying to rehydrate it into an entity
boolean isPokemon = false;
if (occupant.entityData().contains("id")) {
isPokemon = "cobblemon:pokemon".equals(occupant.entityData().getUnsafe().getString("id"));
}
if (isPokemon) {
return BeehiveBlockEntityAccessor.releaseOccupant(
((BlockEntityAccessor) blockEntityAccessor).getLevel(),
((BlockEntityAccessor) blockEntityAccessor).getWorldPosition(),
state,
occupant,
list,
releaseStatus,
blockEntityAccessor.savedFlowerPos());
} else {
return false;
}
}
);
for (Entity entity : list) {
if (entity instanceof PokemonEntity) {
((PokemonEntity) entity).getBrain().setMemory(MemoryModuleType.ATTACK_TARGET, player);
}
}
}
}
@Redirect(
method = "releaseOccupant",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/level/Level;addFreshEntity(Lnet/minecraft/world/entity/Entity;)Z"
)
)
private static boolean cobblemon$captureReleasedEntity(
Level level, Entity entity,
Level lvl, BlockPos pos, BlockState state,
BeehiveBlockEntity.Occupant occupant, @Nullable List<Entity> bees,
BeehiveBlockEntity.BeeReleaseStatus releaseStatus, @Nullable BlockPos flowerPos) {
if (bees != null && !bees.contains(entity)) {
// Avoid duplicates if vanilla also adds bees to the list on your version
bees.add(entity);
}
return level.addFreshEntity(entity);
}
}
@@ -7,7 +7,6 @@
*/
package com.cobblemon.mod.common.mixin;
import com.cobblemon.mod.common.duck.ChannelDuck;
import com.mojang.blaze3d.audio.Channel;
import org.lwjgl.openal.AL10;
@@ -55,6 +55,48 @@ public abstract class LivingEntityMixin extends Entity implements OrientationCon
}
}
@Override
public @NotNull HitResult pick(double hitDistance, float partialTicks, boolean hitFluids) {
Entity vehicle = this.getVehicle();
if (vehicle instanceof PokemonEntity pokemonEntity && pokemonEntity.getDelegate() instanceof PokemonClientDelegate delegate) {
int seatIndex = pokemonEntity.getPassengers().indexOf(this);
Seat seat = pokemonEntity.getSeats().get(seatIndex);
MatrixWrapper locator = delegate.getLocatorStates().get(seat.getLocator());
Vec3 locatorOffset = new Vec3(locator.getMatrix().getTranslation(new Vector3f()));
Vec3 eyePosition = cobblemon$getEyePosition(partialTicks, pokemonEntity, locatorOffset);
Vec3 viewVector = this.getViewVector(partialTicks);
Vec3 viewDistanceVector = eyePosition.add(viewVector.x * hitDistance, viewVector.y * hitDistance, viewVector.z * hitDistance);
return this.level()
.clip(
new ClipContext(
eyePosition, viewDistanceVector, ClipContext.Block.OUTLINE, hitFluids ? net.minecraft.world.level.ClipContext.Fluid.ANY : net.minecraft.world.level.ClipContext.Fluid.NONE, this
)
);
}
return super.pick(hitDistance, partialTicks, hitFluids);
}
@Unique
private Vec3 cobblemon$getEyePosition(float partialTicks, PokemonEntity pokemonEntity, Vec3 locatorOffset) {
OrientationController controller = this.getOrientationController();
float currEyeHeight = this.getEyeHeight();
Vec3 offset = locatorOffset.add(pokemonEntity.position());
if (controller.isActive() && controller.getOrientation() != null) {
Quaternionf orientation = controller.getRenderOrientation(partialTicks);
Vec3 rotatedEyeHeight = new Vec3(orientation.transform(new Vector3f(0f, currEyeHeight - (this.getBbHeight() / 2), 0f)));
offset.add(rotatedEyeHeight);
}
return offset;
}
@Inject(method = "tick", at = @At("HEAD"))
private void cobblemon$updateRenderOrientation(CallbackInfo ci) {
this.cobblemon$orientationController.tick();
@@ -8,51 +8,20 @@
package com.cobblemon.mod.common.mixin;
import com.cobblemon.mod.common.Cobblemon;
import com.cobblemon.mod.common.OrientationControllable;
import com.cobblemon.mod.common.PlayerSpawnerGetter;
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner;
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawnerFactory;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ServerPlayer.class)
public abstract class ServerPlayerMixin implements PlayerSpawnerGetter {
@Shadow
public abstract ServerLevel serverLevel();
@Unique
public PlayerSpawner cobblemon$spawner;
@Override
public PlayerSpawner getPlayerSpawner() {
ServerPlayer player = (ServerPlayer)(Object)this;
if (cobblemon$spawner == null) {
cobblemon$spawner = PlayerSpawnerFactory.INSTANCE.create(player);
}
return cobblemon$spawner;
}
@Inject(method = "tick", at = @At("TAIL"))
private void cobblemon$tickSpawner(CallbackInfo ci) {
if (!Cobblemon.config.getEnableSpawning()) {
return;
} else if (!serverLevel().getGameRules().getBoolean(CobblemonGameRules.DO_POKEMON_SPAWNING)) {
return;
}
getPlayerSpawner().tick();
}
public abstract class ServerPlayerMixin {
@Inject(method = "rideTick", at = @At("HEAD"))
private void cobblemon$updateOrientationControllerRideTick(CallbackInfo ci) {
@@ -82,4 +51,5 @@ public abstract class ServerPlayerMixin implements PlayerSpawnerGetter {
private boolean cobblemon$cancelSavingPokemonMounts(boolean original, @Local(ordinal=0) Entity entity) {
return original && !(entity instanceof PokemonEntity);
}
}
@@ -1,21 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.accessor;
import net.minecraft.world.level.block.entity.BeehiveBlockEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Invoker;
@Mixin(targets = "net.minecraft.world.level.block.entity.BeehiveBlockEntity$BeeData")
public interface BeeDataAccessor {
@Invoker("toOccupant")
BeehiveBlockEntity.Occupant invokeToOccupant();
}
@@ -1,40 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.accessor;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BeehiveBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import org.spongepowered.asm.mixin.gen.Invoker;
import java.util.List;
@Mixin(BeehiveBlockEntity.class)
public interface BeehiveBlockEntityAccessor {
@Accessor("stored")
List<Object> getStored();
@Accessor("savedFlowerPos")
BlockPos savedFlowerPos();
/**
* Access the private static method:
* private static void releaseOccupant(Level level, BlockPos pos, BeeData beeData, ...)
*/
@Invoker("releaseOccupant")
static boolean releaseOccupant(Level level, BlockPos pos, BlockState state, BeehiveBlockEntity.Occupant occupant, @Nullable List<Entity> storedInHives, BeehiveBlockEntity.BeeReleaseStatus releaseStatus, @Nullable BlockPos storedFlowerPos) {
throw new AssertionError(); // Mixin will overwrite
}
}
@@ -1,21 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.accessor;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(BlockEntity.class)
public interface BlockEntityAccessor {
@Accessor("level") Level getLevel();
@Accessor("worldPosition") BlockPos getWorldPosition();
}
@@ -9,21 +9,15 @@
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.api.scheduling.ClientTaskTracker;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.world.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.function.Predicate;
@Mixin(GameRenderer.class)
public class GameRendererMixin {
@Unique
@@ -48,16 +42,4 @@ public class GameRendererMixin {
lastTime = newTime;
}
@ModifyArg(
method = "pick(Lnet/minecraft/world/entity/Entity;DDF)Lnet/minecraft/world/phys/HitResult;",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/projectile/ProjectileUtil;getEntityHitResult(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/world/phys/AABB;Ljava/util/function/Predicate;D)Lnet/minecraft/world/phys/EntityHitResult;"
),
index = 4
)
private Predicate<Entity> filterPokemonVehicle(Predicate<Entity> original, @Local(ordinal = 0, argsOnly = true) Entity entity) {
return (entityX) -> !(entity.getVehicle() == entityX && entityX instanceof PokemonEntity) && original.test(entityX);
}
}
@@ -696,7 +696,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val SUPERB_REMEDY = compostableItem("superb_remedy", RemedyItem(RemedyItem.SUPERB), 0.85f)
@JvmField
val MOOMOO_MILK = create("moomoo_milk", MoomooMilk(Item.Properties().craftRemainder(Items.GLASS_BOTTLE).stacksTo((16))))
val MOOMOO_MILK = create("moomoo_milk", CobblemonItem(Item.Properties().craftRemainder(Items.GLASS_BOTTLE).stacksTo((16))))
@JvmField
val POTION = create("potion", PotionItem(PotionType.POTION))
@@ -15,12 +15,12 @@ import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import com.mojang.serialization.Codec
import com.mojang.serialization.codecs.ListCodec
import com.mojang.serialization.codecs.PrimitiveCodec
import net.minecraft.core.BlockPos
import net.minecraft.core.GlobalPos
import net.minecraft.core.UUIDUtil
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import java.util.*
import java.util.Optional
import java.util.UUID
import net.minecraft.core.BlockPos
import net.minecraft.world.entity.LivingEntity
object CobblemonMemories {
val memories = mutableMapOf<String, MemoryModuleType<*>>()
@@ -48,13 +48,10 @@ object CobblemonMemories {
val TIME_TRYING_TO_REACH_BERRY_BUSH = register<Int>("time_trying_to_reach_berry_bush")
val IS_CONSUMING_ITEM = register<Boolean>("is_consuming_item")
val RECENTLY_ATE_GRASS = register<Boolean>("recently_ate_grass")
val HIVE_LOCATION = register<BlockPos>("hive_location", BlockPos.CODEC)
val HIVE_BLACKLIST = register<List<BlockPos>>("hive_blacklist")
val HIVE_LOCATION = register<BlockPos>("hive_location")
val HIVE_COOLDOWN = register<Boolean>("hive_cooldown")
val NEARBY_FLOWER = register<BlockPos>("nearby_flower", BlockPos.CODEC)
val PATH_TO_NEARBY_FLOWER_COOLDOWN = register<Boolean>("path_to_nearby_flower_cooldown")
val HAS_NECTAR = register<Boolean>("has_nectar", PrimitiveCodec.BOOL)
val NEARBY_SACC_LEAVES = register<BlockPos>("nearby_sacc_leaves", BlockPos.CODEC)
val NEARBY_FLOWERS = register<List<BlockPos>>("nearby_flower")
val POLLINATED = register<Boolean>("pollinated", PrimitiveCodec.BOOL)
/** who am i following rn? */
val HERD_LEADER = register<String>("herd_leader", PrimitiveCodec.STRING)
/** how many are following me rn? */
@@ -26,6 +26,8 @@ object CobblemonPoiTypes: PlatformRegistry<Registry<PoiType>, ResourceKey<Regist
@JvmField
val NURSE_KEY = createKey("nurse", CobblemonBlocks.HEALING_MACHINE, 1, 1)
@JvmField
val POKE_SNACK_KEY = createKey("poke_snack", CobblemonBlocks.POKE_SNACK, 0, 1)
@JvmField
val SACCHARINE_LOG_SLATHERED_KEY = createKey("saccharine_log_slathered", CobblemonBlocks.SACCHARINE_LOG_SLATHERED, 0, 1)
// TODO after 1.7
// @JvmField
@@ -41,8 +41,6 @@ object CobblemonSensors {
val POKEMON_NEARBY_WANTED_ITEM = register("pokemon_nearby_wanted_item", ::PokemonItemSensor)
val NEARBY_SACC_LEAVES = register("nearby_sacc_leaves", ::SacLeavesSensor)
fun <E : Entity, U : Sensor<E>> register(id: String, supplier: Supplier<U>): SensorType<U> {
val sensor = SensorType(supplier)
sensors[id] = sensor
@@ -22,19 +22,18 @@ class ReelInPokemonContext(val species : ResourceLocation, val baitId: ResourceL
class ReelInPokemonCriterionCondition(
playerCtx: Optional<ContextAwarePredicate>,
val species: String,
val baitId: String // also allows "cobblemon:empty_bait" to indicate that no bait is allowed for this criterion to match
val baitId: String
) : SimpleCriterionCondition<ReelInPokemonContext>(playerCtx) {
companion object {
val CODEC: Codec<ReelInPokemonCriterionCondition> = RecordCodecBuilder.create { it.group(
EntityPredicate.ADVANCEMENT_CODEC.optionalFieldOf("player").forGetter { it.playerCtx },
Codec.STRING.optionalFieldOf("species", "any").forGetter { it.species },
Codec.STRING.optionalFieldOf("baitId", "any").forGetter { it.baitId }
).apply(it, { playerCtx, pokemonId, baitId -> ReelInPokemonCriterionCondition(playerCtx, pokemonId, baitId) }) }
Codec.STRING.optionalFieldOf("baitId", "empty_bait").forGetter { it.baitId }
).apply(it, { playerCtx, pokemonId, baitId -> ReelInPokemonCriterionCondition(playerCtx, pokemonId, baitId.ifEmpty { "empty_bait" }) }) }
}
override fun matches(player: ServerPlayer, context: ReelInPokemonContext): Boolean {
return (context.species == this.species.asIdentifierDefaultingNamespace() || this.species == "any")
&& (context.baitId == this.baitId.asIdentifierDefaultingNamespace() || this.baitId == "any")
return (context.species == this.species.asIdentifierDefaultingNamespace() || this.species == "any") && (context.baitId == this.baitId.asIdentifierDefaultingNamespace() || this.baitId == "empty_bait")
}
}
@@ -1,49 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.ai
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.behavior.Behavior
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.entity.ai.memory.MemoryStatus
import net.minecraft.world.entity.schedule.Activity
/**
* A [Behavior] subclass that represents tasks that check that the entity is still in one
* of the activities that this task is part of. If the entity is no longer doing any of those
* activities, the task will be interrupted and you can handle any cleanup in [onInterrupted].
*
* @author Hiroku
* @since October 17th, 2025
*/
abstract class ActivityAwareBehavior<E : LivingEntity> : Behavior<E> {
constructor(entryCondition: Map<MemoryModuleType<*>, MemoryStatus>) : super(entryCondition)
constructor(entryCondition: Map<MemoryModuleType<*>, MemoryStatus>, duration: Int) : super(entryCondition, duration)
constructor(entryCondition: Map<MemoryModuleType<*>, MemoryStatus>, minDuration: Int, maxDuration: Int) : super(entryCondition, minDuration, maxDuration)
val activities: MutableSet<Activity> = mutableSetOf()
fun isStillDoingActivity(entity: E): Boolean {
return activities.any(entity.brain::isActive)
}
override fun canStillUse(level: ServerLevel, entity: E, gameTime: Long) = isStillDoingActivity(entity)
open fun onInterrupted(level: ServerLevel, entity: E, gameTime: Long) {
// No-op
}
override fun stop(level: ServerLevel, entity: E, gameTime: Long) {
super.stop(level, entity, gameTime)
if (!isStillDoingActivity(entity)) {
onInterrupted(level, entity, gameTime)
}
}
}
@@ -11,6 +11,7 @@ package com.cobblemon.mod.common.api.ai
import com.google.common.collect.ImmutableList
import com.mojang.datafixers.util.Pair
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.Mob
import net.minecraft.world.entity.ai.behavior.BehaviorControl
import net.minecraft.world.entity.schedule.Activity
@@ -22,13 +23,6 @@ class ActivityConfigurationContext(val activity: Activity) {
}
fun apply(entity: LivingEntity) {
tasks.forEach {
val activityAwareBehavior = it.second as? ActivityAwareBehavior<*>
?: (it.second as? WrapperLivingEntityTask<*>)?.task as? ActivityAwareBehavior
?: return@forEach
activityAwareBehavior.activities += activity
}
entity.brain.addActivity(activity, ImmutableList.copyOf(tasks))
}
}
@@ -23,7 +23,7 @@ import net.minecraft.world.entity.schedule.Activity
class ActivityChangeTaskConfig : SingleTaskConfig {
val activity: Activity = Activity.IDLE
val condition: ExpressionLike = "true".asExpressionLike()
val condition: ExpressionLike = "false".asExpressionLike()
override fun getVariables(entity: LivingEntity, behaviourConfigurationContext: BehaviourConfigurationContext): List<MoLangConfigVariable> = emptyList()
@@ -17,7 +17,6 @@ import com.cobblemon.mod.common.api.ai.ExpressionOrEntityVariable
import com.cobblemon.mod.common.api.ai.asVariables
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.getMemorySafely
import com.cobblemon.mod.common.util.resolveString
import com.mojang.datafixers.util.Either
import net.minecraft.server.level.ServerLevel
@@ -63,7 +62,7 @@ class MemoryAspectTaskConfig : SingleTaskConfig {
override fun canStillUse(level: ServerLevel, entity: LivingEntity, gameTime: Long) = true
override fun tick(level: ServerLevel, owner: LivingEntity, gameTime: Long) {
val hasMemory = owner.brain.hasMemoryValue(memory)
val hasMemory = !owner.brain.hasMemoryValue(memory)
if (owner is PokemonEntity) {
if (!hasMemory && aspect in owner.pokemon.forcedAspects) {
owner.pokemon.forcedAspects -= aspect
@@ -8,31 +8,16 @@
package com.cobblemon.mod.common.api.ai.config.task
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.CobblemonSensors
import com.cobblemon.mod.common.api.ai.BehaviourConfigurationContext
import com.cobblemon.mod.common.api.ai.asVariables
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.tasks.PlaceHoneyInHiveTask
import com.cobblemon.mod.common.util.distanceTo
import com.cobblemon.mod.common.util.getMemorySafely
import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerLevel
import com.cobblemon.mod.common.entity.pokemon.ai.tasks.PathToBeeHiveTask
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.PathfinderMob
import net.minecraft.world.entity.ai.behavior.Behavior
import net.minecraft.world.entity.ai.behavior.BehaviorControl
import net.minecraft.world.entity.ai.behavior.BlockPosTracker
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.entity.ai.memory.MemoryStatus
import net.minecraft.world.entity.ai.memory.WalkTarget
import net.minecraft.world.phys.Vec3
class PathToBeeHiveTaskConfig : SingleTaskConfig {
companion object {
const val HONEY = "honey"
const val STAY_OUT_OF_HIVE_COOLDOWN = 400
}
val condition = booleanVariable(HONEY, "can_add_honey", true).asExpressible()
@@ -51,107 +36,6 @@ class PathToBeeHiveTaskConfig : SingleTaskConfig {
if (!checkCondition(behaviourConfigurationContext.runtime, condition)) {
return null
}
behaviourConfigurationContext.addSensors(CobblemonSensors.NEARBY_BEE_HIVE)
return object : Behavior<LivingEntity>(
mapOf(
MemoryModuleType.LOOK_TARGET to MemoryStatus.REGISTERED,
MemoryModuleType.WALK_TARGET to MemoryStatus.VALUE_ABSENT,
CobblemonMemories.HAS_NECTAR to MemoryStatus.REGISTERED,
CobblemonMemories.HIVE_LOCATION to MemoryStatus.VALUE_PRESENT,
CobblemonMemories.HIVE_COOLDOWN to MemoryStatus.VALUE_ABSENT,
CobblemonMemories.HIVE_BLACKLIST to MemoryStatus.REGISTERED,
),
400,
400
) {
val maxTicks = 400
var traveledTicks = 0
override fun checkExtraStartConditions(level: ServerLevel, owner: LivingEntity): Boolean {
return (owner is PathfinderMob && owner.isAlive && (owner is PokemonEntity && PlaceHoneyInHiveTask.wantsToEnterHive(owner))) && owner.brain.checkMemory(
MemoryModuleType.WALK_TARGET, MemoryStatus.VALUE_ABSENT)
}
override fun canStillUse(level: ServerLevel, entity: LivingEntity, gameTime: Long): Boolean {
if (maxTicks >= traveledTicks) return false
val pos = entity.brain.getMemory(CobblemonMemories.HIVE_LOCATION)
return !pos.isEmpty && entity.distanceTo(pos.get()) > 1.0
}
override fun start(level: ServerLevel, entity: LivingEntity, gameTime: Long) {
super.start(level, entity, gameTime)
val hiveLocation = entity.brain.getMemorySafely(CobblemonMemories.HIVE_LOCATION).orElse(null)
val world = entity.level()
val sidesByClosest = listOf(
hiveLocation.north(),
hiveLocation.south(),
hiveLocation.east(),
hiveLocation.west()
).sortedBy { it.distSqr(entity.blockPosition()) }
var openSide = BlockPos.ZERO
for (side in sidesByClosest) {
if (world.getBlockState(side).isAir) {
openSide = side
break
}
}
if (openSide == BlockPos.ZERO) {
if (world.getBlockState(hiveLocation.above()).isAir) {
// If the above position is also air, we can use it
openSide = hiveLocation.above() // Fallback to above if no open sides found
} else if (world.getBlockState(hiveLocation.below()).isAir) {
// If the below position is also air, we can use it
openSide = hiveLocation.below() // Fallback to below if no open sides found
} else {
// If no open sides or above/below positions are found, we cannot proceed
return
}
}
val targetVec = Vec3.atCenterOf(openSide)
// Set path target toward hive
if (pathfindDirectlyTowards(openSide, entity)) {
entity.brain.setMemory(MemoryModuleType.WALK_TARGET,WalkTarget(targetVec, 0.35F, 0))
entity.brain.setMemory(MemoryModuleType.LOOK_TARGET,BlockPosTracker(targetVec.add(0.0, entity.eyeHeight.toDouble(), 0.0)))
traveledTicks = 0
} else {
dropAndBlackListHive(entity)
}
}
override fun tick(level: ServerLevel, owner: LivingEntity, gameTime: Long) {
traveledTicks++
}
override fun stop(level: ServerLevel, entity: LivingEntity, gameTime: Long) {
if (maxTicks < traveledTicks) {
dropAndBlackListHive(entity)
entity.brain.setMemoryWithExpiry(CobblemonMemories.HIVE_COOLDOWN, true, 100)
}
}
fun dropAndBlackListHive(entity: LivingEntity) {
val hivePos = entity.brain.getMemorySafely(CobblemonMemories.HIVE_LOCATION).orElse(null)
if (hivePos == null) return
val blackList = entity.brain.getMemorySafely(CobblemonMemories.HIVE_BLACKLIST).orElse(emptyList()).toMutableList()
blackList.add(hivePos)
if (blackList.count() > 3) {
blackList.removeFirst()
}
entity.brain.eraseMemory(CobblemonMemories.HIVE_LOCATION)
entity.brain.setMemory(CobblemonMemories.HIVE_BLACKLIST, blackList)
}
private fun pathfindDirectlyTowards(pos: BlockPos, entity: LivingEntity): Boolean {
if (entity !is PathfinderMob) return false
val nav = entity.navigation
val path = nav. createPath(pos, 0)
return path != null && path.canReach()
}
}
return PathToBeeHiveTask.create()
}
}
@@ -8,6 +8,7 @@
package com.cobblemon.mod.common.api.ai.config.task
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.CobblemonSensors
import com.cobblemon.mod.common.api.ai.BehaviourConfigurationContext
@@ -40,14 +41,12 @@ class PathToFlowerTaskConfig : SingleTaskConfig {
return null
}
behaviourConfigurationContext.addMemories(
CobblemonMemories.NEARBY_FLOWER,
CobblemonMemories.PATH_TO_NEARBY_FLOWER_COOLDOWN,
CobblemonMemories.NEARBY_FLOWERS,
CobblemonMemories.HIVE_COOLDOWN,
CobblemonMemories.HIVE_LOCATION,
CobblemonMemories.HIVE_BLACKLIST,
CobblemonMemories.HAS_NECTAR,
CobblemonMemories.POLLINATED
)
behaviourConfigurationContext.addSensors(CobblemonSensors.NEARBY_FLOWER)
behaviourConfigurationContext.addSensors(CobblemonSensors.NEARBY_BEE_HIVE, CobblemonSensors.NEARBY_FLOWER)
return PathToFlowerTask.create()
}
}
@@ -1,49 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.ai.config.task
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.CobblemonSensors
import com.cobblemon.mod.common.api.ai.BehaviourConfigurationContext
import com.cobblemon.mod.common.api.ai.asVariables
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.tasks.PathToSacLeafTask
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.behavior.BehaviorControl
class PathToSaccLeavesTaskConfig : SingleTaskConfig {
companion object {
const val POLLINATE = "pollinate"
}
val condition = booleanVariable(POLLINATE, "can_pollinate", true).asExpressible()
override fun getVariables(entity: LivingEntity, behaviourConfigurationContext: BehaviourConfigurationContext) = listOf(
condition
).asVariables()
override fun createTask(
entity: LivingEntity,
behaviourConfigurationContext: BehaviourConfigurationContext
): BehaviorControl<in LivingEntity>? {
if (entity !is PokemonEntity) {
return null
}
if (!checkCondition(behaviourConfigurationContext.runtime, condition)) {
return null
}
behaviourConfigurationContext.addMemories(
CobblemonMemories.NEARBY_SACC_LEAVES,
CobblemonMemories.HAS_NECTAR
)
behaviourConfigurationContext.addSensors(CobblemonSensors.NEARBY_SACC_LEAVES, CobblemonSensors.NEARBY_FLOWER)
return PathToSacLeafTask.create()
}
}
@@ -8,8 +8,8 @@
package com.cobblemon.mod.common.api.ai.config.task
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.api.ai.BehaviourConfigurationContext
import com.cobblemon.mod.common.api.ai.WrapperLivingEntityTask
import com.cobblemon.mod.common.api.ai.asVariables
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.tasks.PlaceHoneyInHiveTask
@@ -37,9 +37,6 @@ class PlaceHoneyInHiveTaskConfig : SingleTaskConfig {
if (!checkCondition(behaviourConfigurationContext.runtime, condition)) {
return null
}
return WrapperLivingEntityTask(
PlaceHoneyInHiveTask.create(),
PokemonEntity::class.java
)
return PlaceHoneyInHiveTask.create()
}
}
@@ -1,130 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.ai.config.task
import com.cobblemon.mod.common.CobblemonActivities
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.api.ai.BehaviourConfigurationContext
import com.cobblemon.mod.common.api.ai.asVariables
import com.cobblemon.mod.common.block.SaccharineLeafBlock
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.getMemorySafely
import net.minecraft.server.level.ServerLevel
import net.minecraft.sounds.SoundEvents
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.behavior.Behavior
import net.minecraft.world.entity.ai.behavior.BehaviorControl
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.entity.ai.memory.MemoryStatus
import net.minecraft.world.phys.Vec3
class PlaceHoneyInSaccLeavesTaskConfig : SingleTaskConfig {
companion object {
const val POLLINATE = "pollinate"
const val MIN_DURATION : Int = 300
const val MAX_DURATION : Int = 400
const val REQUIRED_SUCCESSFUL_POLLINATION_TICKS = 250
}
val condition = booleanVariable(POLLINATE, "can_pollinate", true).asExpressible()
override fun getVariables(entity: LivingEntity, behaviourConfigurationContext: BehaviourConfigurationContext) = listOf(
condition
).asVariables()
override fun createTask(
entity: LivingEntity,
behaviourConfigurationContext: BehaviourConfigurationContext
): BehaviorControl<in LivingEntity>? {
if (entity !is PokemonEntity) {
return null
}
if (!checkCondition(behaviourConfigurationContext.runtime, condition)) {
return null
}
return object : Behavior<LivingEntity>(
mapOf(
CobblemonMemories.NEARBY_SACC_LEAVES to MemoryStatus.VALUE_PRESENT,
MemoryModuleType.WALK_TARGET to MemoryStatus.VALUE_ABSENT,
CobblemonMemories.HAS_NECTAR to MemoryStatus.VALUE_PRESENT,
),
MIN_DURATION,
MAX_DURATION
) {
var successfulPollinationTicks = 0
var lastSoundPlayedTick = 0
var hoverPos: Vec3? = null
override fun checkExtraStartConditions(level: ServerLevel, owner: LivingEntity): Boolean {
if (level.isNight || level.isRaining || !(entity.brain.getMemorySafely(CobblemonMemories.HAS_NECTAR).orElse(false))) return false
val optionalBlockPos = entity.brain.getMemorySafely(CobblemonMemories.NEARBY_SACC_LEAVES)
if (!optionalBlockPos.isPresent || level.getBlockState(optionalBlockPos.get()).block != CobblemonBlocks.SACCHARINE_LEAVES) return false
return Vec3.atCenterOf(optionalBlockPos.get()).distanceTo(entity.position()) <= 0.6
}
override fun canStillUse(level: ServerLevel, entity: LivingEntity, gameTime: Long): Boolean {
return checkExtraStartConditions(level, entity) && entity.brain.activeActivities.indexOf(
CobblemonActivities.POKEMON_POLLINATION) != -1
}
override fun start(level: ServerLevel, entity: LivingEntity, gameTime: Long) {
super.start(level, entity, gameTime)
hoverPos = null
}
override fun tick(level: ServerLevel, owner: LivingEntity, gameTime: Long) {
++successfulPollinationTicks
if (entity.random.nextFloat() < 0.05f && successfulPollinationTicks > this.lastSoundPlayedTick + 60) {
this.lastSoundPlayedTick = successfulPollinationTicks
entity.playSound(SoundEvents.BEEHIVE_DRIP, 1.0f, 1.0f)
}
val leavesPos = entity.brain.getMemorySafely(CobblemonMemories.NEARBY_SACC_LEAVES).orElse(null)
val bl = hoverPos == null || entity.position().distanceTo(hoverPos!!) <= 0.1
val bl3 = entity.random.nextInt(40) == 0
if (bl && bl3) {
// mimics the wriggling over a flower the vanilla bees do
if (hoverPos == null) {
hoverPos = leavesPos.bottomCenter.add(0.0, 0.4, 0.0)
}
if (leavesPos != null) {
hoverPos = leavesPos.bottomCenter.add(Vec3((entity.random.nextDouble() * 2.0 - 1.0) * (1.0/5.0), 0.3 + (entity.random.nextDouble() * 2.0 - 1.0) * (1.0/7.0), (entity.random.nextDouble() * 2.0 - 1.0) * (1.0/5.0)))
entity.getMoveControl()
.setWantedPosition(hoverPos!!.x(), hoverPos!!.y(), hoverPos!!.z(), 0.35)
entity.lookControl.setLookAt(leavesPos.bottomCenter.add(0.0,0.5,0.0))
}
} else if (!bl) {
entity.getMoveControl()
.setWantedPosition(hoverPos!!.x(), hoverPos!!.y(), hoverPos!!.z(), 0.35)
}
}
override fun stop(level: ServerLevel, entity: LivingEntity, gameTime: Long) {
if (successfulPollinationTicks > REQUIRED_SUCCESSFUL_POLLINATION_TICKS) {
val blockPos = entity.brain.getMemorySafely(CobblemonMemories.NEARBY_SACC_LEAVES).orElse(null)
blockPos.let {
val blockState = level.getBlockState(blockPos)
if (blockState.block == CobblemonBlocks.SACCHARINE_LEAVES) {
val saccAge = blockState.getValue( SaccharineLeafBlock.AGE)
if (saccAge < SaccharineLeafBlock.MAX_AGE) {
entity.brain.eraseMemory(CobblemonMemories.HAS_NECTAR)
val newBlockState = blockState.setValue(SaccharineLeafBlock.AGE, saccAge + 1)
level.setBlock(blockPos, newBlockState, 3)
}
}
}
}
successfulPollinationTicks = 0
entity.brain.eraseMemory(MemoryModuleType.WALK_TARGET)
}
}
}
}
@@ -8,14 +8,17 @@
package com.cobblemon.mod.common.api.ai.config.task
import com.cobblemon.mod.common.CobblemonActivities
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.api.ai.BehaviourConfigurationContext
import com.cobblemon.mod.common.api.ai.ExpressionOrEntityVariable
import com.cobblemon.mod.common.api.ai.asVariables
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.asExpression
import com.cobblemon.mod.common.util.getMemorySafely
import com.cobblemon.mod.common.util.sendParticlesServer
import com.mojang.datafixers.util.Either
import net.minecraft.core.particles.ParticleTypes
import net.minecraft.server.level.ServerLevel
import net.minecraft.sounds.SoundEvents
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.behavior.Behavior
import net.minecraft.world.entity.ai.behavior.BehaviorControl
@@ -26,11 +29,10 @@ import net.minecraft.world.phys.Vec3
class PollinateFlowerTaskConfig : SingleTaskConfig {
companion object {
const val POLLINATE = "pollinate"
const val PATH_TO_FLOWER_COOLDOWN_TICKS : Long = 200
const val HAS_NECTAR_ASPECT = "has_nectar"
}
val condition = booleanVariable(POLLINATE, "can_pollinate", true).asExpressible()
val durationTicks: ExpressionOrEntityVariable = Either.left("30".asExpression())
override fun getVariables(entity: LivingEntity, behaviourConfigurationContext: BehaviourConfigurationContext) = listOf(
condition
@@ -49,28 +51,22 @@ class PollinateFlowerTaskConfig : SingleTaskConfig {
return object : Behavior<LivingEntity>(
mapOf(
CobblemonMemories.NEARBY_FLOWER to MemoryStatus.VALUE_PRESENT,
CobblemonMemories.NEARBY_FLOWERS to MemoryStatus.VALUE_PRESENT,
MemoryModuleType.WALK_TARGET to MemoryStatus.VALUE_ABSENT,
CobblemonMemories.HAS_NECTAR to MemoryStatus.REGISTERED,
CobblemonMemories.POLLINATED to MemoryStatus.VALUE_ABSENT,
CobblemonMemories.HIVE_COOLDOWN to MemoryStatus.VALUE_ABSENT,
),
600,
625
15,
45
) {
val REQUIRED_SUCCESSFUL_POLLINATION_TICKS = 400
var successfulPollinationTicks = 0
var lastSoundPlayedTick = 0
var hoverPos: Vec3? = null
override fun checkExtraStartConditions(level: ServerLevel, owner: LivingEntity): Boolean {
if (level.isNight || level.isRaining || entity.brain.getMemorySafely(CobblemonMemories.HAS_NECTAR).orElse(false)) return false
val optionalBlockPos = entity.brain.getMemorySafely(CobblemonMemories.NEARBY_FLOWER)
return optionalBlockPos.isPresent && Vec3.atCenterOf(optionalBlockPos.get()).distanceTo(entity.position()) <= 1.0
return entity.brain.getMemorySafely(CobblemonMemories.NEARBY_FLOWERS).orElse(emptyList()).any {
Vec3.atCenterOf(it).distanceTo(entity.position()) <= 1.0
}
}
override fun canStillUse(level: ServerLevel, entity: LivingEntity, gameTime: Long): Boolean {
return checkExtraStartConditions(level, entity) && entity.brain.activeActivities.indexOf(
CobblemonActivities.POKEMON_POLLINATION) != -1
return checkExtraStartConditions(level, entity)
}
override fun start(level: ServerLevel, entity: LivingEntity, gameTime: Long) {
@@ -78,44 +74,20 @@ class PollinateFlowerTaskConfig : SingleTaskConfig {
}
override fun tick(level: ServerLevel, owner: LivingEntity, gameTime: Long) {
++successfulPollinationTicks
if (entity.random.nextFloat() < 0.05f && successfulPollinationTicks > this.lastSoundPlayedTick + 60) {
this.lastSoundPlayedTick = successfulPollinationTicks
entity.playSound(SoundEvents.BEE_POLLINATE, 1.0f, 1.0f)
}
val flowerPos = entity.brain.getMemorySafely(CobblemonMemories.NEARBY_FLOWER).orElse(null)
val bl = hoverPos == null || entity.position().distanceTo(hoverPos!!) <= 0.1
val bl3 = entity.random.nextInt(40) == 0
if (bl && bl3) {
// mimics the wriggling over a flower the vanilla bees do
if (hoverPos == null) {
hoverPos = flowerPos.bottomCenter.add(0.0, 0.4, 0.0)
}
if (flowerPos != null) {
hoverPos = flowerPos.bottomCenter.add(Vec3((entity.random.nextDouble() * 2.0 - 1.0) * (1.0/5.0), 0.3 + (entity.random.nextDouble() * 2.0 - 1.0) * (1.0/7.0), (entity.random.nextDouble() * 2.0 - 1.0) * (1.0/5.0)))
entity.getMoveControl()
.setWantedPosition(hoverPos!!.x(), hoverPos!!.y(), hoverPos!!.z(), 0.35)
entity.lookControl.setLookAt(flowerPos.bottomCenter.add(0.0,0.5,0.0))
}
} else if (!bl) {
entity.getMoveControl()
.setWantedPosition(hoverPos!!.x(), hoverPos!!.y(), hoverPos!!.z(), 0.35)
}
entity.lookControl.setLookAt(flowerPos.bottomCenter.add(0.0,0.5,0.0))
level.sendParticlesServer(
particleType = ParticleTypes.DRIPPING_HONEY,
position = owner.eyePosition,
particles = 3,
offset = Vec3.ZERO,
speed = 0.0
)
}
override fun stop(level: ServerLevel, entity: LivingEntity, gameTime: Long) {
if (successfulPollinationTicks > REQUIRED_SUCCESSFUL_POLLINATION_TICKS) {
entity.brain.setMemory(CobblemonMemories.HAS_NECTAR, true)
val flowerStillThere = canStillUse(level, entity, gameTime)
if (flowerStillThere) {
entity.brain.setMemory(CobblemonMemories.POLLINATED, true)
}
successfulPollinationTicks = 0
entity.brain.setMemoryWithExpiry(CobblemonMemories.PATH_TO_NEARBY_FLOWER_COOLDOWN, true, PATH_TO_FLOWER_COOLDOWN_TICKS)
}
}
}
@@ -97,9 +97,7 @@ interface TaskConfig {
cobblemonResource("go_to_land") to GoToLandTaskConfig::class.java,
cobblemonResource("path_to_hive") to PathToBeeHiveTaskConfig::class.java,
cobblemonResource("place_honey_in_hive") to PlaceHoneyInHiveTaskConfig::class.java,
cobblemonResource("place_honey_in_sacc_leaves") to PlaceHoneyInSaccLeavesTaskConfig::class.java,
cobblemonResource("path_to_flower") to PathToFlowerTaskConfig::class.java,
cobblemonResource("path_to_sacc_leaves") to PathToSaccLeavesTaskConfig::class.java,
cobblemonResource("pollinate_flower") to PollinateFlowerTaskConfig::class.java,
cobblemonResource("go_to_land") to GoToLandTaskConfig::class.java,
cobblemonResource("manage_flight_in_battle") to ManageFlightInBattleTaskConfig::class.java,
@@ -12,6 +12,7 @@ import com.bedrockk.molang.runtime.MoLangEnvironment
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.MoParams
import com.bedrockk.molang.runtime.struct.ArrayStruct
import com.bedrockk.molang.runtime.struct.MoStruct
import com.bedrockk.molang.runtime.struct.QueryStruct
import com.bedrockk.molang.runtime.struct.VariableStruct
import com.bedrockk.molang.runtime.value.DoubleValue
@@ -39,6 +40,7 @@ import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import com.cobblemon.mod.common.api.moves.animations.ActionEffects
import com.cobblemon.mod.common.api.moves.animations.NPCProvider
import com.cobblemon.mod.common.api.moves.animations.TargetsProvider
import com.cobblemon.mod.common.api.npc.NPCClass
import com.cobblemon.mod.common.api.npc.NPCClasses
import com.cobblemon.mod.common.api.npc.configuration.interaction.DialogueNPCInteractionConfiguration
import com.cobblemon.mod.common.api.npc.configuration.interaction.ScriptNPCInteractionConfiguration
@@ -55,6 +57,7 @@ import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
import com.cobblemon.mod.common.api.pokemon.experience.SidemodExperienceSource
import com.cobblemon.mod.common.api.pokemon.moves.LearnsetQuery
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.riding.Rideable
import com.cobblemon.mod.common.api.riding.stats.RidingStat
import com.cobblemon.mod.common.api.scheduling.ClientTaskTracker
import com.cobblemon.mod.common.api.scheduling.Schedulable
@@ -134,6 +137,7 @@ import net.minecraft.world.entity.LightningBolt
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.Mob
import net.minecraft.world.entity.PathfinderMob
import net.minecraft.world.entity.PlayerRideable
import net.minecraft.world.entity.TamableAnimal
import net.minecraft.world.entity.ai.behavior.BlockPosTracker
import net.minecraft.world.entity.ai.memory.MemoryModuleType
@@ -141,6 +145,7 @@ import net.minecraft.world.entity.ai.memory.MemoryStatus
import net.minecraft.world.entity.ai.memory.WalkTarget
import net.minecraft.world.entity.animal.Animal
import net.minecraft.world.entity.item.ItemEntity
import net.minecraft.world.entity.monster.Enemy
import net.minecraft.world.entity.monster.Monster
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
@@ -155,6 +160,9 @@ import net.minecraft.world.level.levelgen.Heightmap
import net.minecraft.world.level.pathfinder.PathType
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
import kotlin.text.get
import kotlin.text.toInt
import kotlin.toString
/**
* Holds a bunch of useful MoLang trickery that can be used or extended in API
@@ -1177,28 +1185,16 @@ object MoLangFunctions {
return@put DoubleValue.ONE
}
map.put("has_memory_value") { params ->
for (param in params.params) {
if (!entity.hasMemoryFromString(param.asString())) {
return@put DoubleValue.ZERO
}
}
return@put DoubleValue.ONE
val memoryTypes = params.params.map {
it.asString().asIdentifierDefaultingNamespace()
}.map(BuiltInRegistries.MEMORY_MODULE_TYPE::get)
return@put DoubleValue(memoryTypes.all { entity.brain.checkMemory(it, MemoryStatus.VALUE_PRESENT) })
}
map.put("lacks_memory_value") { params ->
for (param in params.params) {
if (entity.hasMemoryFromString(param.asString())) {
return@put DoubleValue.ZERO
}
}
return@put DoubleValue.ONE
}
map.put("is_doing_activity") { params ->
for (param in params.params) {
if (entity.isDoingActivityFromString(param.asString())) {
return@put DoubleValue.ONE
}
}
return@put DoubleValue.ZERO
val memoryTypes = params.params.map {
it.asString().asIdentifierDefaultingNamespace()
}.map(BuiltInRegistries.MEMORY_MODULE_TYPE::get)
return@put DoubleValue(memoryTypes.none { entity.brain.checkMemory(it, MemoryStatus.VALUE_PRESENT) })
}
map.put("can_see") { params ->
val target = params.get<MoValue>(0)
@@ -1256,6 +1252,15 @@ object MoLangFunctions {
map.put("in_battle") { DoubleValue(npc.isInBattle()) }
map.put("battles") { ArrayStruct(npc.battleIds.mapNotNull { BattleRegistry.getBattle(it)?.struct }.mapIndexed { index, value -> "$index" to value }.toMap()) }
map.put("stop_battles") { _ -> npc.battleIds.forEach { BattleRegistry.getBattle(it)?.stop() } }
map.put("is_doing_activity") { params ->
val identifiers = params.params.map { it.asString().asIdentifierDefaultingNamespace(namespace = "minecraft") }
val activities = identifiers.mapNotNull { identifier -> BuiltInRegistries.ACTIVITY.get(identifier) }
if (activities.isNotEmpty()) {
return@put DoubleValue(activities.any { activity -> npc.brain.isActive(activity) })
} else {
return@put DoubleValue.ZERO
}
}
map.put("run_script_on_client") { params ->
val world = npc.level()
if (world is ServerLevel) {
@@ -1900,9 +1905,6 @@ object MoLangFunctions {
map.put("is_holding_item") { DoubleValue(!pokemonEntity.entityData.get(PokemonEntity.SHOWN_HELD_ITEM).let {
it.isEmpty || it.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) || it.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS)
}) }
map.put("riding_style") {
StringValue(pokemonEntity.ifRidingAvailableSupply("") { behaviour, settings, state -> behaviour.getRidingStyle(settings, state).name })
}
map.put("is_wearing_hat") { DoubleValue(pokemonEntity.entityData.get(PokemonEntity.SHOWN_HELD_ITEM).`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS)) }
map.put("is_wearing_face") { DoubleValue(pokemonEntity.entityData.get(PokemonEntity.SHOWN_HELD_ITEM).`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS)) }
map.put("is_pastured") {
@@ -806,8 +806,8 @@ open class PokemonProperties {
return PokemonProperties().loadFromJSON(this.saveToJSON())
}
// If the config value is at least 0, then do 1/x and use that as the property chance
private fun Float.checkRate(): Boolean = this > 0 && (Random.nextFloat() < 1 / this)
// If the config value is at least 1, then do 1/x and use that as the property chance
private fun Float.checkRate(): Boolean = if (this >= 1) (Random.Default.nextFloat() < 1 / this) else Random.Default.nextFloat() < this
/**
* Attempts to find an ability by ID and resolve if it should be forced or if it's legal for the given form.
@@ -9,9 +9,8 @@
package com.cobblemon.mod.common.api.pokemon.aspect
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.api.ai.config.task.PollinateFlowerTaskConfig
import com.cobblemon.mod.common.api.spawning.influence.SaccharineLogSlatheredInfluence
import com.cobblemon.mod.common.block.entity.PokeSnackBlockEntity
import com.cobblemon.mod.common.api.spawning.prospecting.LurePokeSnackDetector
import com.cobblemon.mod.common.client.entity.PokemonClientDelegate
import net.minecraft.core.particles.BlockParticleOption
import net.minecraft.core.particles.ParticleOptions
@@ -23,12 +22,11 @@ import net.minecraft.resources.ResourceLocation
*/
val aspectParticleMap: Map<String, ParticleData> = mapOf(
SaccharineLogSlatheredInfluence.HONEY_DRENCHED_ASPECT to ParticleData.MinecraftParticle(ParticleTypes.FALLING_HONEY, 0.075, 1),
PokeSnackBlockEntity.POKE_SNACK_CRUMBED_ASPECT to ParticleData.MinecraftParticle(
LurePokeSnackDetector.POKE_SNACK_CRUMBED_ASPECT to ParticleData.MinecraftParticle(
BlockParticleOption(
ParticleTypes.BLOCK,
CobblemonBlocks.POKE_SNACK.defaultBlockState()
), 0.05, 3),
PollinateFlowerTaskConfig.HAS_NECTAR_ASPECT to ParticleData.MinecraftParticle(ParticleTypes.FALLING_NECTAR, 0.075, 1)
)
sealed class ParticleData {
@@ -30,6 +30,6 @@ interface RidingBehaviourSettings: Encodable, Decodable {
fun calculate(stat: RidingStat, boostAmount: Float): Float {
val range = stats[stat] ?: return 0F
return (range.first + boostAmount).coerceAtMost(range.endInclusive.toFloat())
return range.first + boostAmount
}
}
@@ -19,7 +19,6 @@ import net.minecraft.world.entity.LivingEntity
import kotlin.collections.get
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.player.Player
import kotlin.collections.get
/**
* Small wrapper around a RidingBehaviour to provide sane defaults in the event that the behaviour is not active.
@@ -110,22 +109,12 @@ class RidingController(
}
private fun canTransitionToLand(): Boolean {
val hasLandController = entity.pokemon.riding.behaviours?.get(RidingStyle.LAND) != null
if (hasLandController && (entity.isInLiquid || entity.isUnderWater)) return false
if (!hasLandController && (entity.isEyeInFluid(FluidTags.WATER) || entity.isEyeInFluid(FluidTags.LAVA))) return false
if (entity.isInLiquid || entity.isUnderWater) return false
return entity.onGround()
}
private fun canTransitionToLiquid(): Boolean {
// If it has a liquid controller to transition to and it isn't currently an air controller then transition on
// touching water. If not then check for eyes in water. This allows fliers to fly closely above water and
// for walkers that don't have a liquid controller to be able to walk in not deep water
return if (entity.pokemon.riding.behaviours?.get(RidingStyle.LIQUID) != null && context?.style != RidingStyle.AIR) {
entity.isInLiquid || entity.isUnderWater
} else {
entity.isEyeInFluid(FluidTags.WATER) || entity.isEyeInFluid(FluidTags.LAVA)
}
return entity.isInLiquid || entity.isUnderWater
}
private fun canTransitionToAir(driver: LivingEntity?): Boolean {
@@ -54,7 +54,18 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
get() = CobblemonRideSettings.bird
override fun isActive(settings: BirdSettings, state: BirdState, vehicle: PokemonEntity): Boolean {
return !(vehicle.isInWater || vehicle.isUnderWater)
return Shapes.create(vehicle.boundingBox).blockPositionsAsListRounded().any {
//Need to check other fluids
if (vehicle.isInWater || vehicle.isUnderWater) {
return@any false
}
//This might not actually work, depending on what the yPos actually is. yPos of the middle of the entity? the feet?
if (it.y.toDouble() == (vehicle.position().y)) {
val blockState = vehicle.level().getBlockState(it.below())
return@any !(!blockState.isAir && blockState.fluidState.isEmpty)
}
true
}
}
override fun pose(settings: BirdSettings, state: BirdState, vehicle: PokemonEntity): PoseType {
@@ -107,24 +118,16 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
driver: Player
) {
val stam = state.stamina.get()
val controller = (driver as OrientationControllable).orientationController
// Grab the boost time in seconds and convert to ticks. Then calculate the drain rate as inversely
// proportional to the number of ticks of boost thus making a full boost take x ticks
// in short: "Stamina drains slower at higher values and also replenishes slower"
val totalStam = vehicle.runtime.resolveDouble(settings.staminaExpr ?: globalBird.staminaExpr!!) * 20.0f
val horzMod = vehicle.runtime.resolveDouble(settings.stamDrainHorzMod ?: globalBird.stamDrainHorzMod!!)
val glideMod = vehicle.runtime.resolveFloat(settings.stamDrainPercRateGlide ?: globalBird.stamDrainPercRateGlide!!)
val stamDrainPerRateDive = vehicle.runtime.resolveDouble(settings.stamDrainPerRateDive ?: globalBird.stamDrainPerRateDive!!).coerceIn(0.0, 1.0) // Ensure its between 1 and 0 or things get weird
val stamDrainRate = (1.0f / totalStam).toFloat()
val stamDrainHorz = if(vehicle.deltaMovement.horizontalDistance() < 1e-6) horzMod else max(1.0 + ((horzMod - 1.0) * atan( vehicle.deltaMovement.y / vehicle.deltaMovement.horizontalDistance())), 1.0)
val diveAmount = sin(controller.pitch.toRadians()).coerceIn(0.0f, 1.0f)// from 0.0 -> 1.0 how steep is the current dive angle
val diveMod = ((1.0 - diveAmount) * ( 1.0 - stamDrainPerRateDive ) + stamDrainPerRateDive).toFloat() // smooth between 1.0 and the dive drain mod depending upon how dived the ride is
val stamDrainRate = (1.0f / totalStam).toFloat() * stamDrainHorz.toFloat()
val newStam = if (driver.deltaMovement.length() > 0.1) max(0.0f,stam - stamDrainRate) // Drain at normal fly rate
else if (state.gliding.get()) min(1.0f,stam - stamDrainRate * glideMod * diveMod) // Gliding drain logic
else max(0.0f,stam - stamDrainRate * 0.5f) // if hovering half the stam drain
val newStam = if (driver.zza > 0.0) max(0.0f,stam - stamDrainRate)
else if (!state.gliding.get()) max(0.0f,stam - stamDrainRate * 0.5f) // if hovering half the stam drain
else min(1.0f,stam + stamDrainRate * 0.2f)
state.stamina.set(newStam)
}
@@ -188,8 +191,8 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
newVelocity = newVelocity.normalize().scale(vehicle.deltaMovement.length())
}
// speed up and slow down based on input
if (driver.zza != 0.0f && state.stamina.get() > 0.0 && newVelocity.length() < glideTopSpeed) {
//speed up and slow down based on input
if (driver.zza != 0.0f && state.stamina.get() > 0.0) {
//make sure it can't exceed top speed
val forwardInput = when {
state.stamina.get() == 0.0f -> 0.0
@@ -344,6 +347,9 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
val maxPitchForce = maxPitchCorrectionRate.toFloat() / 40.0f
val pitchArrivalDeg = 45.0f
var desiredPitch = if (state.currUpsideDownForce.get() != 0.0) 90.0 else 0.0
// if (cos(controller.roll.toRadians()) < 0.0) {
// desiredPitch *= -1 // If not rolled over then flip the pitch force. There are better ways to do this :(
// }
if((state.rideVelocity.get().length() < 0.1 && controller.pitch < 0.0) || state.currUpsideDownForce.get() != 0.0) {
val arrivalInfluence = (min(pitchArrivalDeg,abs(Mth.wrapDegrees(desiredPitch.toFloat().angleDifference(controller.pitch)))) / pitchArrivalDeg)
@@ -504,13 +510,11 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
val currSpeed = state.rideVelocity.get().length()
val currHorzSpeed = state.rideVelocity.get().horizontalDistance()
val hoverSpeed = 0.1
val pitchDamping = abs(cos(controller.pitch.toRadians())).pow(2) // Helper value used when wanting to discard the influence of another value at steep pitches
// Yaw locally when at or below hoverSpeed but not when pitched up or rolled over
val globalYaw = xInput * (1 - sqrt(RidingBehaviour.scaleToRange(currHorzSpeed, hoverSpeed, topSpeed))).coerceIn(0.0, 1.0)
//xInput * 0.4 * cos(controller.roll.toRadians()).coerceIn(0.0f, 1.0f) * yawDampen
controller.applyGlobalYaw(globalYaw.toFloat())
val yawDampen = abs(cos(controller.pitch.toRadians())).pow(2) * abs(cos(controller.roll.toRadians())).pow(2)
val localYaw = if (currHorzSpeed < hoverSpeed) xInput * (1 - sqrt(RidingBehaviour.scaleToRange(currHorzSpeed, hoverSpeed, topSpeed))).coerceIn(0.0, 1.0)
else 0.0 //xInput * 0.4 * cos(controller.roll.toRadians()).coerceIn(0.0f, 1.0f) * yawDampen
// Pitch locally up or down depending upon a number of factors:
// - Reduce pitch substantially when rolled and in a steep yaw. This prevents the player from ignoring a slow handling stat
@@ -525,18 +529,14 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
// Apply a roll righting force when trying to pitch hard while rolled sideways. This nudges the player towards
// pitching globally
val pitchInfluencedRollCorrection = 0.0
val howRolledAmI = (( cos(controller.roll.toRadians()) - 1).coerceIn(-1.0f, 0.0f) * -2) //Coerce into how much roll you want to let in in the deadzone
val isInputtingTowardsRoll = if ( xInput.sign == controller.roll.sign.toDouble()) 1.0 else 0.0
val inputInRolledDir = if (isInputtingTowardsRoll == 1.0) 1.0 else -cos(controller.roll.toRadians()).coerceIn(-1.0f, 0.0f).toDouble()
val rollDampen = (1 - howRolledAmI.coerceIn(0.0f, 0.7f) * pitchDamping * inputInRolledDir)
val howRolledAmI = (( cos(controller.roll.toRadians()) - 1).coerceIn(-1.0f, 0.0f) * -2).coerceIn(0.0f, 0.7f) //Coerce into how much roll you want to let in in the deadzone
val inputInRolledDir = if (xInput.sign.toFloat() == controller.roll.sign) 1.0 else -cos(controller.roll.toRadians()).coerceIn(-1.0f, 0.0f).toDouble()
val rollDampen = (1 - howRolledAmI * abs(cos(controller.pitch.toRadians())).pow(2) * inputInRolledDir)
val rollForce = if (currHorzSpeed > hoverSpeed) xInput * sqrt(abs(RidingBehaviour.scaleToRange(currHorzSpeed, hoverSpeed, topSpeed)).coerceIn(0.0,1.0)) * rollDampen + pitchInfluencedRollCorrection
else 0.0
// When already rolled significantly part of the roll force should turn to local yaw force
val localYawForce = rollForce * abs(sin(controller.roll.toRadians())).pow(2) * pitchDamping * isInputtingTowardsRoll
//yaw, pitch, roll
return Vec3(localYawForce, localPitch, rollForce)
return Vec3(localYaw, localPitch, rollForce)
}
@@ -584,11 +584,11 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
driver: Player
): Float {
val topSpeed = vehicle.runtime.resolveDouble(settings.speedExpr ?: globalBird.speedExpr!!) / 20.0
val glideTopSpeed = topSpeed * vehicle.runtime.resolveDouble(settings.glideSpeedExpr ?: globalBird.glideSpeedExpr!!)
val currSpeed = state.rideVelocity.get().length()
val speedOverTopSpeed = max( 0.0, currSpeed - topSpeed)
val normalizedGlideSpeed = RidingBehaviour.scaleToRange(speedOverTopSpeed, 0.0, 0.3)
// Must I ensure that topspeed is greater than minimum?
val normalizedGlideSpeed = RidingBehaviour.scaleToRange(currSpeed, topSpeed, glideTopSpeed)
// Only ever want the fov change to be a max of 0.2 and for it to have non linear scaling.
return 1.0f + normalizedGlideSpeed.pow(2).toFloat() * 0.2f
@@ -692,22 +692,6 @@ class BirdSettings : RidingBehaviourSettings {
var glideSpeedExpr: Expression? = null
private set
// debuff to stamina drain (increases stamina drain?) depending on how horizontal you're looking (so just drain more when climbing in altitude)
// So for the base 1.5: If looking straight up and holding forward you drain at 150 percent the rate you would if
// you were horizontal.
var stamDrainHorzMod: Expression? = null
private set
// long ass variable name
// Percent of stamina drain during glide (percent of normal drain rate) So base is 25 percent. (change to 0 if you want no drain or negative if you want regen)
var stamDrainPercRateGlide: Expression? = null
private set
// Percent of stamina drain as a factor of how hard you're diving. So if its zero then at full dive you drain no stam.
// This stacks with `stamDrainPercRateGlide`
// This is coerced between 1 and 0 or things would get weird.
var stamDrainPerRateDive: Expression? = null
private set
var rideSounds: RideSoundSettingsList = RideSoundSettingsList()
override fun encode(buffer: RegistryFriendlyByteBuf) {
@@ -722,9 +706,6 @@ class BirdSettings : RidingBehaviourSettings {
buffer.writeNullableExpression(speedExpr)
buffer.writeNullableExpression(accelerationExpr)
buffer.writeNullableExpression(staminaExpr)
buffer.writeNullableExpression(stamDrainHorzMod)
buffer.writeNullableExpression(stamDrainPercRateGlide)
buffer.writeNullableExpression(stamDrainPerRateDive)
buffer.writeNullableExpression(timeToRollCorrect)
}
@@ -740,9 +721,6 @@ class BirdSettings : RidingBehaviourSettings {
speedExpr = buffer.readNullableExpression()
accelerationExpr = buffer.readNullableExpression()
staminaExpr = buffer.readNullableExpression()
stamDrainHorzMod = buffer.readNullableExpression()
stamDrainPercRateGlide = buffer.readNullableExpression()
stamDrainPerRateDive = buffer.readNullableExpression()
timeToRollCorrect = buffer.readNullableExpression()
}
}
@@ -103,12 +103,12 @@ class BoatBehaviour : RidingBehaviour<BoatSettings, BoatState> {
if (!state.isVehicleSprinting.get()) {
if (state.staminaBuffer.get() >= 20) {
val staminaIncrease = (1.0f / vehicle.runtime.resolveDouble(settings.stamReplenishTimeSeconds ?: globalBoat.stamReplenishTimeSeconds!!) ).toFloat() / 20.0f
state.stamina.set(min(1.0f, state.stamina.get() + staminaIncrease))
val staminaIncrease = vehicle.runtime.resolveDouble(settings.staminaIncreaseExpr ?: globalBoat.staminaIncreaseExpr!!)
state.stamina.set(min(1.0f, state.stamina.get() + 0.01f * staminaIncrease.toFloat()))
}
}
else if (abs(state.rideVelocity.get().z) > 0.1 && state.isVehicleSprinting.get()) {
consumeStamina(vehicle, driver, settings, state)
consumeStamina(vehicle, driver, settings, state, 0.01f)
}
state.staminaBuffer.set(state.staminaBuffer.get() + 1)
super.tick(settings, state, vehicle, driver, input)
@@ -219,12 +219,12 @@ class BoatBehaviour : RidingBehaviour<BoatSettings, BoatState> {
return state.stamina.get() != 0f
}
private fun consumeStamina(vehicle: PokemonEntity, driver: Player, settings: BoatSettings, state: BoatState) {
if (vehicle.runtime.resolveBoolean(settings.infiniteStamina ?: globalBoat.infiniteStamina!!)) {
private fun consumeStamina(vehicle: PokemonEntity, driver: Player, settings: BoatSettings, state: BoatState, drain: Float) {
val staminaModifier = vehicle.runtime.resolveFloat(settings.staminaDecreaseExpr ?: globalBoat.staminaDecreaseExpr!!)
if (staminaModifier == 0F) {
return
}
val stamDrainRate = (1.0f / vehicle.runtime.resolveDouble(settings.staminaExpr ?: globalBoat.staminaExpr!!)).toFloat() / 20.0f
state.stamina.set(max(0f, state.stamina.get() - stamDrainRate))
state.stamina.set(max(0f, state.stamina.get() - drain / staminaModifier))
if (state.stamina.get() == 0f) {
state.isVehicleSprinting.set(false)
driver.isSprinting = false
@@ -404,19 +404,16 @@ class BoatSettings : RidingBehaviourSettings {
override val stats = mutableMapOf<RidingStat, IntRange>()
var rideSounds: RideSoundSettingsList = RideSoundSettingsList()
var infiniteStamina: Expression? = null
private set
var terminalVelocity: Expression? = null
private set
var rotatePokemonHead: Expression? = null
private set
var stamReplenishTimeSeconds: Expression? = null
var staminaIncreaseExpr: Expression? = null
private set
var staminaExpr: Expression? = null
var staminaDecreaseExpr: Expression? = null
private set
var rotationSpeedModifierExpr: Expression? = null
@@ -441,11 +438,10 @@ class BoatSettings : RidingBehaviourSettings {
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeRidingStats(stats)
rideSounds.encode(buffer)
buffer.writeNullableExpression(infiniteStamina)
buffer.writeNullableExpression(terminalVelocity)
buffer.writeNullableExpression(rotatePokemonHead)
buffer.writeNullableExpression(stamReplenishTimeSeconds)
buffer.writeNullableExpression(staminaExpr)
buffer.writeNullableExpression(staminaIncreaseExpr)
buffer.writeNullableExpression(staminaDecreaseExpr)
buffer.writeNullableExpression(rotationSpeedModifierExpr)
buffer.writeNullableExpression(jumpStrengthExpr)
buffer.writeNullableExpression(speedExpr)
@@ -458,11 +454,10 @@ class BoatSettings : RidingBehaviourSettings {
override fun decode(buffer: RegistryFriendlyByteBuf) {
stats.putAll(buffer.readRidingStats())
rideSounds = RideSoundSettingsList.decode(buffer)
infiniteStamina = buffer.readNullableExpression()
terminalVelocity = buffer.readNullableExpression()
rotatePokemonHead = buffer.readNullableExpression()
stamReplenishTimeSeconds = buffer.readNullableExpression()
staminaExpr = buffer.readNullableExpression()
staminaIncreaseExpr = buffer.readNullableExpression()
staminaDecreaseExpr = buffer.readNullableExpression()
rotationSpeedModifierExpr = buffer.readNullableExpression()
jumpStrengthExpr = buffer.readNullableExpression()
speedExpr = buffer.readNullableExpression()
@@ -603,7 +603,6 @@ class DolphinSettings : RidingBehaviourSettings {
buffer.writeNullableExpression(canJump)
buffer.writeNullableExpression(reverseDriveFactor)
buffer.writeNullableExpression(strafeFactor)
buffer.writeNullableExpression(timeToRollCorrect)
buffer.writeNullableExpression(jumpExpr)
buffer.writeNullableExpression(handlingExpr)
buffer.writeNullableExpression(horizontalPitchExpr)
@@ -620,7 +619,6 @@ class DolphinSettings : RidingBehaviourSettings {
canJump = buffer.readNullableExpression()
reverseDriveFactor = buffer.readNullableExpression()
strafeFactor = buffer.readNullableExpression()
timeToRollCorrect = buffer.readNullableExpression()
jumpExpr = buffer.readNullableExpression()
handlingExpr = buffer.readNullableExpression()
horizontalPitchExpr = buffer.readNullableExpression()
@@ -18,11 +18,18 @@ import com.cobblemon.mod.common.api.spawning.condition.SeafloorSpawningCondition
import com.cobblemon.mod.common.api.spawning.condition.SpawningCondition
import com.cobblemon.mod.common.api.spawning.condition.SubmergedSpawningCondition
import com.cobblemon.mod.common.api.spawning.condition.SurfaceSpawningCondition
import com.cobblemon.mod.common.api.spawning.position.calculators.GroundedSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.LavafloorSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SeafloorSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SubmergedSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SurfaceSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.detail.NPCSpawnDetail
import com.cobblemon.mod.common.api.spawning.detail.PokemonHerdSpawnDetail
import com.cobblemon.mod.common.api.spawning.detail.PokemonSpawnDetail
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.api.spawning.fishing.FishingSpawner
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePositionResolver
import com.cobblemon.mod.common.api.spawning.position.FishingSpawnablePosition
@@ -32,21 +39,16 @@ import com.cobblemon.mod.common.api.spawning.position.SeafloorSpawnablePosition
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.position.SubmergedSpawnablePosition
import com.cobblemon.mod.common.api.spawning.position.SurfaceSpawnablePosition
import com.cobblemon.mod.common.api.spawning.position.calculators.GroundedSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.LavafloorSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SeafloorSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SubmergedSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SurfaceSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.preset.BasicSpawnDetailPreset
import com.cobblemon.mod.common.api.spawning.preset.BestSpawnerConfig
import com.cobblemon.mod.common.api.spawning.preset.PokemonSpawnDetailPreset
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
import com.cobblemon.mod.common.api.spawning.spawner.BasicSpawner
import com.cobblemon.mod.common.api.spawning.spawner.AreaSpawner
import com.cobblemon.mod.common.api.spawning.spawner.FixedAreaSpawner
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawnerFactory
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.api.spawning.spawner.TickingSpawner
import com.cobblemon.mod.common.entity.pokemon.CobblemonAgingDespawner
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import net.minecraft.server.MinecraftServer
@@ -64,13 +66,14 @@ import net.minecraft.server.MinecraftServer
* In the case of more specialized use, the creation of a [SpawnablePosition] that motivates most of the spawn
* process can be created manually, skipping the first two steps.
*
* An individually spawnable thing is defined as a [SpawnDetail]. A processor handling this process is a [Spawner].
* Some subclasses exist for more specialized cases. If you are spawning in a fixed area and the spawner controlling
* An individually spawnable entity is defined as a [SpawnDetail]. A processor handling this process is a [Spawner].
* Various subclasses exist for more specialized cases. A spawner that is constantly ticking and will spawn things
* without prompts is a [TickingSpawner], and one of those which occurs within a defined area is a [AreaSpawner]. If
* that area is unmoving then it is a [FixedAreaSpawner] whereas if it is actively following the player it is a
* [PlayerSpawner]. You can also use a [BasicSpawner] for more general-purpose spawning without any issue.
* [PlayerSpawner].
*
* PlayerSpawners are stored inside ServerPlayers using a mixin and tick at the end of a player's tick. The [PlayerSpawnerFactory]
* is used to build those. An extension function exists for getting the spawner for a player.
* Spawning is coordinated and ticked using a [SpawnerManager], and all the current managers are accessible from
* [BestSpawner.spawnerManagers].
*
* Spawners and spawnable positions are often put under the effects of [SpawningInfluence]s which can be used to make
* temporary or lasting changes to spawning for whatever component they are attached to (whether that is a spawner or a
@@ -84,9 +87,9 @@ import net.minecraft.server.MinecraftServer
*/
object BestSpawner {
var config = BestSpawnerConfig()
val spawnerManagers = mutableListOf<SpawnerManager>(CobblemonWorldSpawnerManager)
lateinit var defaultPokemonDespawner: Despawner<PokemonEntity>
lateinit var fishingSpawner: BasicSpawner
lateinit var fishingSpawner: FishingSpawner
fun init() {
LOGGER.info("Starting the Best Spawner...")
@@ -134,13 +137,12 @@ object BestSpawner {
fun reloadConfig() {
loadConfig()
spawnerManagers.forEach(SpawnerManager::onConfigReload)
}
fun onServerStarted(server: MinecraftServer) {
CobblemonSpawnPools.onServerLoad(server)
fishingSpawner = BasicSpawner(
name = "fishing",
spawnPool = CobblemonSpawnPools.WORLD_SPAWN_POOL
)
spawnerManagers.forEach(SpawnerManager::onServerStarted)
fishingSpawner = FishingSpawner()
}
}
@@ -40,6 +40,7 @@ object CobblemonSpawnPools {
fun onServerLoad(server: MinecraftServer) {
Cobblemon.LOGGER.info("Optimizing spawn pools...")
WORLD_SPAWN_POOL.onServerLoad(server)
WORLD_SPAWN_POOL.forEach { it.onServerLoad(server) }
WORLD_SPAWN_POOL.precalculate()
}
}
@@ -0,0 +1,76 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawnerFactory
import com.cobblemon.mod.common.platform.events.PlatformEvents
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules
import net.minecraft.server.level.ServerLevel
import java.util.UUID
import net.minecraft.server.level.ServerPlayer
/**
* The [SpawnerManager] that handles Cobblemon's world spawner, which is made up
* of [PlayerSpawner] instances. This manager listens for login and logout and
* cleans up where relevant.
*
* @author Hiroku
* @since February 14th, 2022
*/
object CobblemonWorldSpawnerManager : SpawnerManager() {
var spawnersForPlayers = mutableMapOf<UUID, PlayerSpawner>()
init {
PlatformEvents.SERVER_PLAYER_LOGIN.subscribe { this.onPlayerLogin(it.player) }
PlatformEvents.SERVER_PLAYER_LOGOUT.subscribe { this.onPlayerLogout(it.player) }
PlatformEvents.CHANGE_DIMENSION.subscribe { this.onDimensionChange(it.player, it.origin, it.destination) }
}
fun onPlayerLogin(player: ServerPlayer) {
// Disables spawning
if (!Cobblemon.config.enableSpawning || player.level().gameRules.getBoolean(CobblemonGameRules.DO_POKEMON_SPAWNING) == false) {
return;
}
registerPlayerSpawner(player)
}
fun onPlayerLogout(player: ServerPlayer) {
unregisterPlayerSpawner(player)
}
fun onDimensionChange(player: ServerPlayer, origin: ServerLevel?, destination: ServerLevel?) {
fun getSpawnRule(level: ServerLevel?) = level?.gameRules?.getBoolean(CobblemonGameRules.DO_POKEMON_SPAWNING)
if (!Cobblemon.config.enableSpawning || getSpawnRule(origin) == getSpawnRule(destination)) {
return
}
val spawnerAction = if (destination?.gameRules?.getBoolean(CobblemonGameRules.DO_POKEMON_SPAWNING) == false) {
::unregisterPlayerSpawner
} else {
::registerPlayerSpawner
}
spawnerAction(player)
}
private fun unregisterPlayerSpawner(player: ServerPlayer) {
spawnersForPlayers.remove(player.uuid)?.let { unregisterSpawner(it) }
}
private fun registerPlayerSpawner(player: ServerPlayer) {
PlayerSpawnerFactory.create(this, player).also {
spawnersForPlayers[player.uuid] = it
registerSpawner(it)
}
}
}
@@ -17,21 +17,16 @@ import net.minecraft.world.entity.EntityType
open class SpawnCause(
val spawner: Spawner,
entity: Entity? = null
) : SpawningInfluence {
): SpawningInfluence {
val entityWorldId = entity?.level()?.dimension()
val entityId = entity?.id
val entityUUID = entity?.uuid
val entityType = entity?.type
val entity: Entity?
get() =
if (entityType == EntityType.PLAYER) {
entityUUID?.let {
server()?.playerList?.getPlayer(it)
}
} else if (entityWorldId != null && entityId != null) {
server()?.getLevel(entityWorldId)?.getEntity(entityId)
} else {
null
}
get() = if (entityType == EntityType.PLAYER) {
server()?.playerList?.getPlayer(entityUUID!!)
} else {
server()?.getLevel(entityWorldId!!)?.getEntity(entityId!!)
}
}
@@ -0,0 +1,85 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.api.spawning.spawner.TickingSpawner
import com.cobblemon.mod.common.util.server
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules.DO_POKEMON_SPAWNING
/**
* A manager of various spawners. This is a class in which you should register
* ticking spawners so that they can be automatically ticked by the server.
* This also supports registering [SpawningInfluence]s to apply to each spawner
* that is registered.
*
* @author Hiroku
* @since February 5th, 2022
*/
open class SpawnerManager {
val spawners = mutableListOf<Spawner>()
get() {
if (server()?.isSameThread == false) {
Cobblemon.LOGGER.error("Illegal access to spawners list from non-server thread!")
Exception().printStackTrace()
}
return field
}
val influences = mutableListOf<SpawningInfluence>()
get() {
if (server()?.isSameThread == false) {
Cobblemon.LOGGER.error("Illegal access to influences from non-server thread!")
Exception().printStackTrace()
}
return field
}
inline fun <reified T : Spawner> getSpawnersOfType() = spawners.filterIsInstance<T>()
open fun getSpawnerByName(name: String) = spawners.find { it.name == name }
open fun registerSpawner(spawner: Spawner) {
spawners.add(spawner)
if (spawner !is TickingSpawner) {
spawner.influences.addAll(influences)
}
}
open fun unregisterSpawner(spawner: Spawner) {
spawners.remove(spawner)
if (spawner !is TickingSpawner) {
spawner.influences.removeAll(influences)
}
}
open fun onConfigReload() {
spawners.filterIsInstance<PlayerSpawner>().forEach {
it.ticksBetweenSpawns = Cobblemon.config.ticksBetweenSpawnAttempts
}
}
open fun onServerStarted() {
spawners.clear()
}
open fun onServerTick() {
// Disables spawning
if (!Cobblemon.config.enableSpawning) {
return
}
influences.removeIf { it.isExpired() }
getSpawnersOfType<TickingSpawner>().forEach {
if (it.getCauseEntity()?.level()?.gameRules?.getBoolean(DO_POKEMON_SPAWNING) == false) {
return@forEach
}
it.tick()
}
}
}
@@ -39,13 +39,9 @@ import net.minecraft.server.MinecraftServer
*/
abstract class SpawnDetail : ModDependant {
companion object {
val pokemonTypes = mutableListOf<String>()
val spawnDetailTypes = mutableMapOf<String, RegisteredSpawnDetail<*>>()
fun <T : SpawnDetail> registerSpawnType(name: String, detailClass: Class<T>) {
spawnDetailTypes[name] = RegisteredSpawnDetail(detailClass)
if (detailClass == PokemonSpawnDetail::class.java || detailClass == PokemonHerdSpawnDetail::class.java) {
pokemonTypes.add(name)
}
}
}
@@ -24,7 +24,6 @@ import com.cobblemon.mod.common.util.server
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
@@ -73,13 +72,6 @@ class SpawnPool(val name: String) : JsonDataRegistry<SpawnSet>, Iterable<SpawnDe
return this
}
fun onServerLoad(server: MinecraftServer) {
for (detail in details) {
detail.onServerLoad(server)
}
precalculate()
}
/**
* Precalculates spawns into hash mappings using the [precalculators] included
* in this pool as well as the range of spawnable positions mentioned in the pool.
@@ -0,0 +1,38 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.fishing
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.position.FishingSpawnablePosition
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.spawner.TriggerSpawner
import net.minecraft.server.level.ServerLevel
import net.minecraft.core.BlockPos
/**
* A spawner that takes in a [FishingSpawnCause] and spawns things.
*
* @author Hiroku
* @since February 3rd, 2024
*/
open class FishingSpawner(
override val name: String = "fishing",
pool: SpawnPool = CobblemonSpawnPools.WORLD_SPAWN_POOL
) : TriggerSpawner<FishingSpawnCause>(name, pool) {
override fun parseSpawnablePosition(cause: FishingSpawnCause, world: ServerLevel, pos: BlockPos): FishingSpawnablePosition? {
// Maybe confirm that it's water we're fishing in?
val spawnablePosition = FishingSpawnablePosition(
cause = cause,
world = world,
pos = pos,
influences = influences
)
return spawnablePosition
}
}
@@ -1,35 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.influence
import com.cobblemon.mod.common.api.spawning.SpawnBucket
/**
* A [SpawningInfluence] that multiplies bucket weights by the provided values
* and then normalizes the resulting weights so that their total adds up to 100.
*/
class BucketMultiplyingInfluence(
val multipliers: Map<String, Float>,
) : SpawningInfluence {
override fun affectBucketWeights(bucketWeights: MutableMap<SpawnBucket, Float>) {
val updates = mutableMapOf<SpawnBucket, Float>()
for ((bucket, weight) in bucketWeights) {
val multiplier = multipliers[bucket.name]
if (multiplier != null) {
updates[bucket] = weight * multiplier
}
}
for ((bucket, newWeight) in updates) {
bucketWeights[bucket] = newWeight
}
normalizeBucketWeights(bucketWeights)
}
}
@@ -44,6 +44,11 @@ open class BucketNormalizingInfluence(
bucketWeights[it] = weight.toDouble().pow(1 / normalizationFactor).toFloat()
}
normalizeBucketWeights(bucketWeights)
val sum = bucketWeights.values.sum()
val to100 = 100F / sum
bucketWeights.keys.toList().forEach {
val weight = bucketWeights[it] ?: return@forEach
bucketWeights[it] = weight * to100
}
}
}
@@ -40,15 +40,6 @@ interface SpawningInfluence {
fun affectSpawn(action: SpawnAction<*>, entity: Entity) {}
/** Applies some influence over the weight of spawn buckets. */
fun affectBucketWeights(bucketWeights: MutableMap<SpawnBucket, Float>) {}
/** Normalizes all bucket weights to add up to 100 */
fun normalizeBucketWeights(bucketWeights: MutableMap<SpawnBucket, Float>) {
val sum = bucketWeights.values.sum()
val to100 = 100F / sum
bucketWeights.keys.toList().forEach {
val weight = bucketWeights[it] ?: return@forEach
bucketWeights[it] = weight * to100
}
}
/** Filters positions that could be usable for a spawnable position calculator */
fun isAllowedPosition(world: ServerLevel, pos: BlockPos, spawnablePositionCalculator: SpawnablePositionCalculator<*, *>): Boolean = true
/**
@@ -9,15 +9,16 @@
package com.cobblemon.mod.common.api.spawning.influence.detector
import com.cobblemon.mod.common.api.spawning.SpawningZone
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
import com.cobblemon.mod.common.api.spawning.influence.SpawningZoneInfluence
import com.cobblemon.mod.common.api.spawning.prospecting.IncenseSweetDetector
import com.cobblemon.mod.common.api.spawning.prospecting.SaccharineLogSlatheredDetector
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.api.spawning.spawner.SpawningZoneInput
import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.level.block.state.BlockState
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
import com.cobblemon.mod.common.api.spawning.prospecting.IncenseSweetDetector
import com.cobblemon.mod.common.api.spawning.prospecting.LurePokeSnackDetector
import com.cobblemon.mod.common.api.spawning.prospecting.SaccharineLogSlatheredDetector
/**
* Prospects for [SpawningZoneInfluence] in a world at a given position. Occurs as part of a
@@ -30,6 +31,7 @@ interface SpawningInfluenceDetector {
companion object {
@JvmStatic
val detectors = mutableSetOf<SpawningInfluenceDetector>(
LurePokeSnackDetector,
SaccharineLogSlatheredDetector,
IncenseSweetDetector
)
@@ -28,7 +28,7 @@ class FishingSpawnablePosition(
world: ServerLevel,
pos: BlockPos,
influences: MutableList<SpawningInfluence>
) : BasicSpawnablePosition(
) : TriggerSpawnablePosition(
cause = cause,
world = world,
position = pos,
@@ -14,15 +14,13 @@ import net.minecraft.server.level.ServerLevel
import net.minecraft.core.BlockPos
/**
* A spawnable position relating to single-point spawning. Uses a single-point structure cache which is
* the only contribution of this implementation really. By contrast, the area spawnable position provides
* a structure cache that's shared across the entire zone that was used for spawning, which is great for
* performance. That kind of optimization shouldn't be necessary for single-point spawning.
* A spawnable position relating to triggered spawning. Uses a single-point structure cache which is
* the only contribution of this implementation.
*
* @author Hiroku
* @since February 3rd, 2024
*/
open class BasicSpawnablePosition(
open class TriggerSpawnablePosition(
override val cause: SpawnCause,
override val world: ServerLevel,
override val position: BlockPos,
@@ -31,6 +29,6 @@ open class BasicSpawnablePosition(
override val canSeeSky: Boolean,
override val influences: MutableList<SpawningInfluence>
) : SpawnablePosition() {
val singlePointStructureCache = StructureChunkCache()
override fun getStructureCache(pos: BlockPos) = singlePointStructureCache
val triggerStructureCache = StructureChunkCache()
override fun getStructureCache(pos: BlockPos) = triggerStructureCache
}
@@ -36,7 +36,6 @@ interface SpawnablePositionCalculator<I : SpawnablePositionInput, O : SpawnableP
val isLavaCondition: (BlockState) -> Boolean = { it.fluidState.`is`(FluidTags.LAVA) && it.fluidState.isSource }
private val calculators = PrioritizedList<SpawnablePositionCalculator<*, *>>()
val prioritizedAreaCalculators: List<AreaSpawnablePositionCalculator<*>>
get() = calculators.filterIsInstance<AreaSpawnablePositionCalculator<*>>()
@@ -0,0 +1,189 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.prospecting
import com.cobblemon.mod.common.CobblemonPoiTypes
import com.cobblemon.mod.common.api.fishing.SpawnBait
import com.cobblemon.mod.common.api.spawning.influence.BucketNormalizingInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpatialSpawningZoneInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawnBaitInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawningZoneInfluence
import com.cobblemon.mod.common.api.spawning.influence.UnconditionalSpawningZoneInfluence
import com.cobblemon.mod.common.api.spawning.influence.detector.SpawningInfluenceDetector
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.api.spawning.spawner.SpawningZoneInput
import com.cobblemon.mod.common.block.PokeSnackBlock
import com.cobblemon.mod.common.block.entity.PokeSnackBlockEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.effect.PokeSnackBlockParticlesPacket
import com.cobblemon.mod.common.util.math.pow
import com.cobblemon.mod.common.util.toBlockPos
import kotlin.math.ceil
import kotlin.math.sqrt
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.core.Holder
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.ai.village.poi.PoiManager
import net.minecraft.world.entity.ai.village.poi.PoiType
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.phys.Vec3
import kotlin.math.abs
object LurePokeSnackDetector : SpawningInfluenceDetector {
@JvmField
val RANGE: Int = 48
@JvmField
val POKE_SNACK_CRUMBED_ASPECT = "poke_snack_crumbed"
override fun detectFromInput(spawner: Spawner, input: SpawningZoneInput): MutableList<SpawningZoneInfluence> {
val world = input.world
val listOfInfluences = mutableListOf<SpawningZoneInfluence>()
val searchRange = RANGE + ceil(sqrt(((input.length pow 2) + (input.width pow 2)).toDouble())).toInt()
val lurePokeSnackPositions = world.poiManager.findAll(
{ holder: Holder<PoiType> -> holder.`is`(CobblemonPoiTypes.POKE_SNACK_KEY) },
{ true },
input.getCenter().toBlockPos(),
searchRange,
PoiManager.Occupancy.ANY
).toList()
var highestLureTier = 0
for (lurePokeSnackPos in lurePokeSnackPositions) {
val blockState = world.getBlockState(lurePokeSnackPos)
if (blockState.block !is PokeSnackBlock) continue
val blockEntity = world.getBlockEntity(lurePokeSnackPos) as? PokeSnackBlockEntity ?: continue
val baitEffects = blockEntity.getBaitEffects()
highestLureTier = maxOf(highestLureTier, baitEffects.filter { it.type == SpawnBait.Effects.RARITY_BUCKET }.maxOfOrNull { it.value }?.toInt() ?: 0)
listOfInfluences.add(
SpatialSpawningZoneInfluence(
pos = lurePokeSnackPos,
radius = RANGE.toFloat(),
influence = SpawnBaitInfluence(
effects = baitEffects,
onUsed = { usedAmount, entity ->
entity?.let {
// Prevent triggering twice on same entity
if (usedAmount % 2 == 0) {
tryBite(world, lurePokeSnackPos)
moveAndApplyAspect(world, lurePokeSnackPos, entity)
}
}
}
)
)
)
}
if (highestLureTier > 0) {
listOfInfluences.add(UnconditionalSpawningZoneInfluence(influence = BucketNormalizingInfluence(tier = highestLureTier)))
}
return listOfInfluences
}
private fun tryBite(level: ServerLevel, blockPos: BlockPos) {
if (level.isLoaded(blockPos)) {
val blockState = level.getBlockState(blockPos)
val block = blockState.block as? PokeSnackBlock ?: return
block.eat(level, blockPos, blockState, null)
}
}
private fun moveAndApplyAspect(level: ServerLevel, blockPos: BlockPos, entity: PokemonEntity?) {
entity?.let {
entity.pokemon.forcedAspects += POKE_SNACK_CRUMBED_ASPECT
attemptSafeMove(level, blockPos, entity)
}
}
fun attemptSafeMove(level: ServerLevel, startPos: BlockPos, entity: PokemonEntity) {
val inWater = entity.isInWater
val maxDistance = 10 // maximum search radius
val maxPositions = 10 // maximum number of positions at the closest distance
var foundPositions: List<BlockPos> = emptyList()
// Search layer by layer, starting from the closest
for (distance in 1..maxDistance) {
val positionsAtThisDistance = mutableListOf<BlockPos>()
for (dx in -distance..distance) {
for (dy in -distance..distance) {
for (dz in -distance..distance) {
// Consider only positions on the surface of the cube at current distance
if (listOf(dx, dy, dz).count { abs(it) != 0 } == distance) {
val candidatePos = startPos.offset(dx, dy, dz)
if (!candidatePos.equals(startPos.relative(Direction.UP))) {
// Check collision
val currentBox = entity.boundingBox.move(
candidatePos.x - entity.x,
candidatePos.y - entity.y,
candidatePos.z - entity.z
)
if (level.noCollision(entity, currentBox)) {
val blockBelowPos = candidatePos.below()
val blockState = level.getBlockState(blockBelowPos)
val isWater = blockState.block == Blocks.WATER
if (inWater) {
if (isWater) {
positionsAtThisDistance.add(candidatePos)
}
} else {
if (!blockState.isAir) {
positionsAtThisDistance.add(candidatePos)
}
}
}
}
}
}
}
}
if (positionsAtThisDistance.isNotEmpty()) {
// Limit to 10 nearest positions (all at this distance)
foundPositions = if (positionsAtThisDistance.size > maxPositions) {
positionsAtThisDistance.shuffled().take(maxPositions)
} else {
positionsAtThisDistance
}
break // Found the nearest layer with safe positions, stop expanding
}
}
if (foundPositions.isNotEmpty()) {
val chosenPos = foundPositions.random()
val centerX = chosenPos.x + 0.5
val centerZ = chosenPos.z + 0.5
entity.moveTo(Vec3(centerX, chosenPos.y.toDouble(), centerZ))
PokeSnackBlockParticlesPacket(startPos, chosenPos).sendToPlayersAround(
startPos.x.toDouble(),
startPos.y.toDouble(),
startPos.z.toDouble(),
64.0,
level.dimension()
)
}
}
override fun detectFromBlock(level: ServerLevel, pos: BlockPos, blockState: BlockState) = emptyList<SpawningZoneInfluence>()
}
@@ -24,11 +24,6 @@ import com.cobblemon.mod.common.api.spawning.spawner.Spawner
* @since January 31st, 2022
*/
interface SpawningSelector<T : SpawnSelectionData> {
companion object {
@JvmField
val DEFAULT = FlatSpawnablePositionWeightedSelector()
}
fun getSelectionData(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>): T
fun selectSpawnAction(
@@ -37,12 +32,12 @@ interface SpawningSelector<T : SpawnSelectionData> {
selectionData: T
): SpawnAction<*>?
fun select(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>, maxSpawns: Int): List<SpawnAction<*>> {
fun select(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>, max: Int): List<SpawnAction<*>> {
val selectionData = getSelectionData(spawner, bucket, spawnablePositions)
val spawnActions = selectionData.spawnActions
while (spawnActions.size < maxSpawns) {
while (spawnActions.size < max) {
val spawnAction = selectSpawnAction(
spawner = spawner,
bucket = bucket,
@@ -0,0 +1,180 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.SpawnerManager
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePositionResolver
import com.cobblemon.mod.common.api.spawning.position.calculators.AreaSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SpawnablePositionCalculator.Companion.prioritizedAreaCalculators
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.isBoxLoaded
import com.cobblemon.mod.common.util.squeezeWithinBounds
import com.cobblemon.mod.common.util.toVec3f
import net.minecraft.core.BlockPos
import net.minecraft.core.SectionPos
import net.minecraft.world.level.ChunkPos
import net.minecraft.world.level.Level
import net.minecraft.world.level.chunk.ChunkAccess
import net.minecraft.world.level.chunk.status.ChunkStatus
import net.minecraft.world.level.pathfinder.PathComputationType
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
/**
* A type of [TickingSpawner] that operates within some area. When this spawner type
* is told to do a spawning action, the subclass can provide a [SpawningZoneInput] to use.
* If a non-null value is returned, the [spawningZoneGenerator] and [spawnablePositionResolver] will be used to
* select a spawn and action it.
*
* Subclasses must implement the function retrieving what area to do the spawning in,
* but otherwise this class is feature-complete.
*
* @author Hiroku
* @since February 5th, 2022
*/
abstract class AreaSpawner(
name: String,
spawns: SpawnPool,
manager: SpawnerManager
) : TickingSpawner(name, spawns, manager) {
abstract fun getZoneInput(cause: SpawnCause): SpawningZoneInput?
companion object {
const val CHUNK_REACH = 3
}
var spawningZoneGenerator: SpawningZoneGenerator = Cobblemon.spawningZoneGenerator
var spawnablePositionResolver: AreaSpawnablePositionResolver = Cobblemon.areaSpawnablePositionResolver
var spawnablePositionCalculators: List<AreaSpawnablePositionCalculator<*>> = prioritizedAreaCalculators
var spawnsPerPass: Int? = null
override fun run(cause: SpawnCause): List<SpawnAction<*>> {
val areaInput = getZoneInput(cause)
val constrainedArea = if (areaInput != null) constrainArea(areaInput) else null
if (constrainedArea != null) {
val areaBox = AABB.ofSize(
Vec3(constrainedArea.getCenter().toVec3f()),
CHUNK_REACH * 16.0 * 2,
1000.0,
CHUNK_REACH * 16.0 * 2
)
if (!constrainedArea.world.isBoxLoaded(areaBox)) {
return emptyList()
}
val numberNearby = constrainedArea.world.getEntitiesOfClass(
PokemonEntity::class.java,
areaBox,
PokemonEntity::countsTowardsSpawnCap
).size
val chunksCovered = CHUNK_REACH * CHUNK_REACH
if (numberNearby.toFloat() / chunksCovered >= Cobblemon.config.pokemonPerChunk) {
return emptyList()
}
//val prospectStart = System.currentTimeMillis()
val zone = spawningZoneGenerator.generate(this, constrainedArea)
//val prospectEnd = System.currentTimeMillis()
val spawnablePositions = spawnablePositionResolver.resolve(this, spawnablePositionCalculators, zone)
val influences = this.getAllInfluences() + zone.unconditionalInfluences
val bucket = chooseBucket(cause, influences)
//val resolveEnd = System.currentTimeMillis()
//val prospectDuration = prospectEnd - prospectStart
//val resolveDuration = resolveEnd - prospectEnd
//println("Prospecting took: $prospectDuration ms. Resolution took: $resolveDuration ms")
// Takes about 3ms on my laptop to prospect, similar to spawnable position resolve - not very good, needs some thought
return getSpawningSelector().select(spawner = this, bucket = bucket, spawnablePositions = spawnablePositions, max = spawnsPerPass ?: Cobblemon.config.maximumSpawnsPerPass)
}
return emptyList()
}
fun isValidStartPoint(world: Level, chunk: ChunkAccess, startPos: BlockPos.MutableBlockPos): Boolean {
val y = startPos.y
if (!world.isLoaded(startPos) || !world.isLoaded(startPos.setY(y + 1))) {
return false
}
val mid = chunk.getBlockState(startPos.setY(y))
val above = chunk.getBlockState(startPos.setY(y + 1))
// Above must be non-solid
if (!above.isPathfindable(PathComputationType.AIR)) {
return false
}
// Position must be non-air
if (mid.isAir) {
return false
}
return true
}
fun constrainArea(area: SpawningZoneInput): SpawningZoneInput? {
val basePos = BlockPos.MutableBlockPos(area.baseX, area.baseY, area.baseZ)
val originalY = area.baseY
val (chunkX, chunkZ) = Pair(SectionPos.blockToSectionCoord(area.baseX), SectionPos.blockToSectionCoord(area.baseZ))
// if the chunk isn't loaded, we don't want to go further & we don't want the getChunk function below to load/create the chunk.
if (!area.world.areEntitiesLoaded(ChunkPos.asLong(chunkX, chunkZ))) return null
val chunk = area.world.getChunk(chunkX, chunkZ, ChunkStatus.FULL) ?: return null
var valid = isValidStartPoint(area.world, chunk, basePos)
if (!valid) {
var offset = 1
do {
if (isValidStartPoint(area.world, chunk, basePos.setY(originalY + offset))) {
valid = true
basePos.y = originalY + offset
break
} else if (isValidStartPoint(area.world, chunk, basePos.setY(originalY - offset))) {
valid = true
basePos.y = originalY - offset
break
}
offset++
} while (offset <= Cobblemon.config.maxVerticalCorrectionBlocks)
}
if (valid) {
val min = area.world.squeezeWithinBounds(basePos)
val max = area.world.squeezeWithinBounds(basePos.move(area.length, area.height, area.width))
if (area.world.isLoaded(min) && area.world.isLoaded(max) &&
min.x < max.x && min.y < max.y && min.z < max.z
) {
return SpawningZoneInput(
cause = area.cause,
world = area.world,
baseX = min.x,
baseY = min.y,
baseZ = min.z,
length = max.x - min.x,
height = max.y - min.y,
width = max.z - min.z
)
}
}
return null
}
}
@@ -1,34 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePositionResolver
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
/**
* A basic spawner implementation that uses Cobblemon's default algorithms for zone generation,
* position resolving, and spawn selection.
*
* @author Hiroku
* @since October 27th, 2025
*/
open class BasicSpawner(
override val name: String,
override var spawnPool: SpawnPool,
override var maxPokemonPerChunk: Float = Cobblemon.config.pokemonPerChunk
) : Spawner {
override var selector: SpawningSelector<*> = SpawningSelector.DEFAULT
override var generator: SpawningZoneGenerator = Cobblemon.spawningZoneGenerator
override var resolver: AreaSpawnablePositionResolver = Cobblemon.areaSpawnablePositionResolver
override val influences = mutableListOf<SpawningInfluence>()
}
@@ -8,37 +8,33 @@
package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.SpawnerManager
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import net.minecraft.server.level.ServerLevel
import net.minecraft.core.BlockPos
/**
* A spawner that works within a fixed area. Instances provide the center point and the radii and then the spawner
* can be told to [run] and it will spawn Pokémon within that area.
* A spawner that works within a fixed area, and ticks on its own. The location
* and size of the area is necessary for construction, but after that this spawner
* can be registered in a [SpawnerManager] and left to its own devices.
*
* @author Hiroku
* @since February 5th, 2022
*/
open class FixedAreaSpawner(
name: String,
spawnPool: SpawnPool,
spawns: SpawnPool,
manager: SpawnerManager,
val world: ServerLevel,
val position: BlockPos,
val horizontalRadius: Int,
val verticalRadius: Int,
maxPokemonPerChunk: Float = Cobblemon.config.pokemonPerChunk
) : BasicSpawner(name, spawnPool, maxPokemonPerChunk) {
fun run(cause: SpawnCause, maxSpawns: Int? = null): List<Any> {
return runForArea(
zoneInput = getZoneInput(cause),
maxSpawns = maxSpawns
)
}
fun getZoneInput(cause: SpawnCause): SpawningZoneInput {
override var ticksBetweenSpawns: Float = 20F
) : AreaSpawner(name, spawns, manager) {
override fun getZoneInput(cause: SpawnCause): SpawningZoneInput? {
val basePos = position.offset(-horizontalRadius, -verticalRadius, -horizontalRadius)
return SpawningZoneInput(
cause = cause,
world = world,
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.Cobblemon.config
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.SpawnerManager
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.util.getPlayer
import com.cobblemon.mod.common.util.nextBetween
@@ -30,16 +31,11 @@ import kotlin.random.Random
* @author Hiroku
* @since February 14th, 2022
*/
class PlayerSpawner(
player: ServerPlayer,
spawnPool: SpawnPool
) : BasicSpawner(
name = player.name.string,
spawnPool = spawnPool
) {
class PlayerSpawner(player: ServerPlayer, spawns: SpawnPool, manager: SpawnerManager) : AreaSpawner(player.name.string, spawns, manager) {
val uuid: UUID = player.uuid
fun getZoneInput(cause: SpawnCause): SpawningZoneInput? {
override var ticksBetweenSpawns = config.ticksBetweenSpawnAttempts
override fun getCauseEntity() = uuid.getPlayer()
override fun getZoneInput(cause: SpawnCause): SpawningZoneInput? {
val player = uuid.getPlayer() ?: return null
val zoneDiameter = config.spawningZoneDiameter
val zoneHeight = config.spawningZoneHeight
@@ -57,7 +53,6 @@ class PlayerSpawner(
} else {
thetatemp
}
val x = center.x + r * cos(theta)
val z = center.z + r * sin(theta)
@@ -72,22 +67,4 @@ class PlayerSpawner(
width = zoneDiameter
)
}
var active = true
var ticksUntilNextSpawn = 100F
var ticksBetweenSpawns: Float = config.ticksBetweenSpawnAttempts
var tickTimerMultiplier = 1F
fun tick() {
if (!active) {
return
}
ticksUntilNextSpawn -= tickTimerMultiplier
if (ticksUntilNextSpawn <= 0) {
val zoneInput = getZoneInput(cause = SpawnCause(spawner = this, entity = uuid.getPlayer()))
zoneInput?.let(::runForArea)
ticksUntilNextSpawn = ticksBetweenSpawns
}
}
}
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnRules
import com.cobblemon.mod.common.api.spawning.SpawnerManager
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence.Companion.TYPICAL_VARIATION
@@ -27,7 +28,7 @@ import net.minecraft.server.level.ServerPlayer
* @since February 14th, 2022
*/
object PlayerSpawnerFactory {
var spawnPool: SpawnPool by mutableLazy {
var spawns: SpawnPool by mutableLazy {
CobblemonSpawnPools.WORLD_SPAWN_POOL
}
@@ -45,9 +46,9 @@ object PlayerSpawnerFactory {
)
})
fun create(player: ServerPlayer): PlayerSpawner {
fun create(spawnerManager: SpawnerManager, player: ServerPlayer): PlayerSpawner {
val influences = influenceBuilders.mapNotNull { it(player) }
return PlayerSpawner(player, spawnPool).also {
return PlayerSpawner(player, spawns, spawnerManager).also {
it.influences.addAll(influences)
it.influences.addAll(
CobblemonSpawnRules.rules.values.filter(SpawnRule::enabled).flatMap(SpawnRule::components)
@@ -14,67 +14,33 @@ import com.cobblemon.mod.common.api.events.entity.SpawnBucketChosenEvent
import com.cobblemon.mod.common.api.spawning.BestSpawner
import com.cobblemon.mod.common.api.spawning.SpawnBucket
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
import com.cobblemon.mod.common.api.spawning.detail.EntitySpawnResult
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePositionResolver
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.position.calculators.SpawnablePositionCalculator.Companion.prioritizedAreaCalculators
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.isBoxLoaded
import com.cobblemon.mod.common.util.squeezeWithinBounds
import com.cobblemon.mod.common.util.toVec3f
import com.cobblemon.mod.common.util.weightedSelection
import kotlin.Any
import kotlin.Boolean
import kotlin.Float
import kotlin.Int
import kotlin.Pair
import kotlin.String
import kotlin.collections.plus
import kotlin.math.max
import net.minecraft.core.BlockPos
import net.minecraft.core.SectionPos
import net.minecraft.world.level.ChunkPos
import net.minecraft.world.level.Level
import net.minecraft.world.level.chunk.ChunkAccess
import net.minecraft.world.level.chunk.status.ChunkStatus
import net.minecraft.world.level.pathfinder.PathComputationType
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
/**
* Interface representing something that performs the action of spawning. Various functions
* exist to streamline the process of using the [BestSpawner].
*
* The simplest implementation is a [BasicSpawner]
*
* @author Hiroku
* @since January 24th, 2022
*/
interface Spawner {
val name: String
val influences: MutableList<SpawningInfluence>
var spawnPool: SpawnPool
var selector: SpawningSelector<*>
var generator: SpawningZoneGenerator
var resolver: AreaSpawnablePositionResolver
var maxPokemonPerChunk: Float
fun getSpawningSelector(): SpawningSelector<*>
fun setSpawningSelector(selector: SpawningSelector<*>)
fun getSpawnPool(): SpawnPool
fun setSpawnPool(spawnPool: SpawnPool)
fun <R> afterSpawn(action: SpawnAction<R>, result: R) {}
companion object {
/** The chunk range radius over which spawning will roughly keep the entity density under control. */
const val ENTITY_LIMIT_CHUNK_RANGE = 3
}
fun getMatchingSpawns(bucket: SpawnBucket, spawnablePosition: SpawnablePosition): List<SpawnDetail> {
val spawns = mutableListOf<SpawnDetail>()
spawns.addAll(spawnPool.retrieve(bucket, spawnablePosition).filter { it.isSatisfiedBy(spawnablePosition) })
spawns.addAll(getSpawnPool().retrieve(bucket, spawnablePosition).filter { it.isSatisfiedBy(spawnablePosition) })
spawnablePosition.influences.forEach { influence ->
val influencedSpawns = influence.injectSpawns(bucket, spawnablePosition)
if (influencedSpawns != null) {
@@ -84,101 +50,6 @@ interface Spawner {
return spawns
}
/**
* Runs the spawner for a specific [SpawnablePosition] and calculates a [SpawnAction] that could spawn there.
* This does not trigger the resulting spawn. You can trigger the spawn in the same action by running
* [runForPosition] instead or manually complete the [SpawnAction] it returns using [SpawnAction.complete].
*/
fun calculateSpawnActionForPosition(
cause: SpawnCause,
spawnablePosition: SpawnablePosition,
): SpawnAction<*>? {
influences.removeIf { it.isExpired() }
spawnablePosition.influences.addAll(influences)
val bucket = chooseBucket(cause, spawnablePosition.influences)
return selector.select(
spawner = this,
bucket = bucket,
spawnablePositions = listOf(spawnablePosition),
maxSpawns = 1
).firstOrNull()
}
fun calculateSpawnActionsForArea(
zoneInput: SpawningZoneInput,
maxSpawns: Int?
): List<SpawnAction<*>> {
val maxSpawns = maxSpawns ?: Cobblemon.config.maximumSpawnsPerPass
influences.removeIf { it.isExpired() }
val constrainedArea = constrainArea(zoneInput)
?: return emptyList()
val areaBox = AABB.ofSize(
Vec3(constrainedArea.getCenter().toVec3f()),
ENTITY_LIMIT_CHUNK_RANGE * 16.0 * 2,
1000.0,
ENTITY_LIMIT_CHUNK_RANGE * 16.0 * 2
)
if (!constrainedArea.world.isBoxLoaded(areaBox)) {
return emptyList()
}
val numberNearby = constrainedArea.world.getEntitiesOfClass(
PokemonEntity::class.java,
areaBox,
PokemonEntity::countsTowardsSpawnCap
).size
val chunksCovered = ENTITY_LIMIT_CHUNK_RANGE * ENTITY_LIMIT_CHUNK_RANGE
val maxPokemonPerChunk = max(Cobblemon.config.pokemonPerChunk, zoneInput.cause.spawner.maxPokemonPerChunk)
if (numberNearby.toFloat() / chunksCovered >= maxPokemonPerChunk) {
return emptyList()
}
val zone = generator.generate(this, constrainedArea)
val spawnablePositions = resolver.resolve(this, prioritizedAreaCalculators, zone)
val influences = influences + zone.unconditionalInfluences
val bucket = chooseBucket(zoneInput.cause, influences)
return selector.select(
spawner = this,
bucket = bucket,
spawnablePositions = spawnablePositions,
maxSpawns = maxSpawns
)
}
/**
* Runs the spawner at the given [SpawnablePosition] and returns whatever the calculated [SpawnAction] produces,
* if any spawn occurred. For most implementations of [SpawnAction], the resulting type is
* [EntitySpawnResult]. Technically you can make spawns for anything though.
*/
fun runForPosition(
cause: SpawnCause,
spawnablePosition: SpawnablePosition
): Any? {
val action = calculateSpawnActionForPosition(
cause = cause,
spawnablePosition = spawnablePosition
) ?: return null
return action.complete()
}
fun runForArea(
zoneInput: SpawningZoneInput,
maxSpawns: Int? = null
): List<Any> {
val spawnActions = calculateSpawnActionsForArea(zoneInput = zoneInput, maxSpawns = maxSpawns)
val results = mutableListOf<Any>()
for (spawnAction in spawnActions) {
spawnAction.complete()?.let(results::add)
}
return results
}
fun copyInfluences() = influences.filter { !it.isExpired() }.toMutableList()
fun chooseBucket(cause: SpawnCause, influences: List<SpawningInfluence>): SpawnBucket {
@@ -195,77 +66,4 @@ interface Spawner {
CobblemonEvents.SPAWN_BUCKET_CHOSEN.post(event)
return event.bucket
}
fun isValidStartPoint(world: Level, chunk: ChunkAccess, startPos: BlockPos.MutableBlockPos): Boolean {
val y = startPos.y
if (!world.isLoaded(startPos) || !world.isLoaded(startPos.setY(y + 1))) {
return false
}
val mid = chunk.getBlockState(startPos.setY(y))
val above = chunk.getBlockState(startPos.setY(y + 1))
// Above must be non-solid
if (!above.isPathfindable(PathComputationType.AIR)) {
return false
}
// Position must be non-air
if (mid.isAir) {
return false
}
return true
}
fun constrainArea(area: SpawningZoneInput): SpawningZoneInput? {
val basePos = BlockPos.MutableBlockPos(area.baseX, area.baseY, area.baseZ)
val originalY = area.baseY
val (chunkX, chunkZ) = Pair(SectionPos.blockToSectionCoord(area.baseX), SectionPos.blockToSectionCoord(area.baseZ))
// if the chunk isn't loaded, we don't want to go further & we don't want the getChunk function below to load/create the chunk.
if (!area.world.areEntitiesLoaded(ChunkPos.asLong(chunkX, chunkZ))) return null
val chunk = area.world.getChunk(chunkX, chunkZ, ChunkStatus.FULL) ?: return null
var valid = isValidStartPoint(area.world, chunk, basePos)
if (!valid) {
var offset = 1
do {
if (isValidStartPoint(area.world, chunk, basePos.setY(originalY + offset))) {
valid = true
basePos.y = originalY + offset
break
} else if (isValidStartPoint(area.world, chunk, basePos.setY(originalY - offset))) {
valid = true
basePos.y = originalY - offset
break
}
offset++
} while (offset <= Cobblemon.config.maxVerticalCorrectionBlocks)
}
if (valid) {
val min = area.world.squeezeWithinBounds(basePos)
val max = area.world.squeezeWithinBounds(basePos.move(area.length, area.height, area.width))
if (area.world.isLoaded(min) && area.world.isLoaded(max) &&
min.x < max.x && min.y < max.y && min.z < max.z
) {
return SpawningZoneInput(
cause = area.cause,
world = area.world,
baseX = min.x,
baseY = min.y,
baseZ = min.z,
length = max.x - min.x,
height = max.y - min.y,
width = max.z - min.z
)
}
}
return null
}
}
@@ -0,0 +1,89 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.SpawnerManager
import com.cobblemon.mod.common.api.spawning.detail.EntitySpawnResult
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.selection.FlatSpawnablePositionWeightedSelector
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
import net.minecraft.world.entity.Entity
/**
* A spawner that regularly attempts spawning entities. It has timing utilities,
* and subclasses must provide the logic for generating a spawn which is called
* periodically by the server.
*
* @author Hiroku
* @since February 5th, 2022
*/
abstract class TickingSpawner(
override val name: String,
var spawns: SpawnPool,
val manager: SpawnerManager
) : Spawner {
private var selector: SpawningSelector<*> = FlatSpawnablePositionWeightedSelector()
override val influences = mutableListOf<SpawningInfluence>()
override fun getSpawningSelector() = selector
override fun setSpawningSelector(selector: SpawningSelector<*>) { this.selector = selector }
override fun getSpawnPool() = spawns
override fun setSpawnPool(spawnPool: SpawnPool) { spawns = spawnPool }
abstract fun run(cause: SpawnCause): List<SpawnAction<*>>
var active = true
val spawnedEntities = mutableListOf<Entity>()
var lastSpawnTime = 0L
var ticksUntilNextSpawn = 100F
abstract var ticksBetweenSpawns: Float
var tickTimerMultiplier = 1F
var removalCheckTicks = 0
open fun tick() {
removalCheckTicks++
influences.removeIf { it.isExpired() }
if (removalCheckTicks == 60) {
spawnedEntities.removeIf { it.isRemoved }
removalCheckTicks = 0
}
if (!active) {
return
}
ticksUntilNextSpawn -= tickTimerMultiplier
if (ticksUntilNextSpawn <= 0) {
val spawnActions = run(cause = SpawnCause(spawner = this, entity = getCauseEntity()))
ticksUntilNextSpawn = ticksBetweenSpawns
for (spawnAction in spawnActions) {
spawnAction.complete()
}
}
}
override fun <R> afterSpawn(action: SpawnAction<R>, result: R) {
super.afterSpawn(action, result)
if (result is EntitySpawnResult) {
spawnedEntities.addAll(result.entities)
}
lastSpawnTime = System.currentTimeMillis()
}
open fun getCauseEntity(): Entity? = null
fun getAllInfluences() = this.influences + manager.influences
override fun copyInfluences() = this.getAllInfluences().toMutableList()
}
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.selection.FlatSpawnablePositionWeightedSelector
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerLevel
/**
* A type of spawner that occurs at a single point and provides its own means of generating a
* spawnable position from a particular cause and position. The trigger spawner must be directly called to
* spawn things (almost as if it was being triggered!!!) as opposed to being scheduled on a ticker.
*
* @author Hiroku
* @since February 3rd, 2024
*/
abstract class TriggerSpawner<T : SpawnCause>(override val name: String, var spawns: SpawnPool) : Spawner {
private var selector: SpawningSelector<*> = FlatSpawnablePositionWeightedSelector()
override val influences = mutableListOf<SpawningInfluence>()
open fun getSpawnAction(
cause: T,
world: ServerLevel,
pos: BlockPos,
influences: List<SpawningInfluence> = emptyList()
): SpawnAction<*>? {
val spawnablePosition = parseSpawnablePosition(cause, world, pos) ?: return null
spawnablePosition.influences.addAll(influences)
val bucket = chooseBucket(cause, spawnablePosition.influences)
return selector.select(this, bucket, listOf(spawnablePosition), max = 1).firstOrNull()
}
/** Parses a spawnable position, if possible, from the given cause and position. */
abstract fun parseSpawnablePosition(cause: T, world: ServerLevel, pos: BlockPos): SpawnablePosition?
override fun getSpawningSelector() = selector
override fun setSpawningSelector(selector: SpawningSelector<*>) { this.selector = selector }
override fun getSpawnPool() = spawns
override fun setSpawnPool(spawnPool: SpawnPool) { spawns = spawnPool }
}
@@ -137,9 +137,6 @@ object CobblemonItemTags {
@JvmField val WEARABLE_HAT_ITEMS = create("held/visibility/hat")
@JvmField val HIDDEN_ITEMS = create("held/visibility/hidden")
//Empty Tag
@JvmField val EMPTY = create("empty")
private fun create(path: String) = TagKey.create(Registries.ITEM, cobblemonResource(path))
@@ -18,7 +18,6 @@ import net.minecraft.core.BlockPos
import net.minecraft.core.particles.BlockParticleOption
import net.minecraft.core.particles.ColorParticleOption
import net.minecraft.core.particles.ParticleTypes
import net.minecraft.server.level.ServerLevel
import net.minecraft.sounds.SoundEvents
import net.minecraft.sounds.SoundSource
import net.minecraft.stats.Stats
@@ -52,13 +51,12 @@ import net.minecraft.world.phys.Vec3
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraft.world.phys.shapes.VoxelShape
import kotlin.math.floor
import kotlin.random.Random
class PokeSnackBlock(settings: Properties, val isLure: Boolean): BaseEntityBlock(settings) {
companion object {
const val MAX_BITES = 8
const val CAKE_HEIGHT = 0.4375
val MAX_BITES = 8
val CAKE_HEIGHT = 0.4375
val CANDLE_PARTICLE_POSITION = Vec3(0.5, CAKE_HEIGHT + 0.5, 0.5)
@@ -137,18 +135,16 @@ class PokeSnackBlock(settings: Properties, val isLure: Boolean): BaseEntityBlock
if (hasCandle(state)) {
return Shapes.or(SHAPES[state.getValue(BITES)], CANDLE_SHAPE)
}
return SHAPES[state.getValue(BITES)]
}
override fun getRenderShape(blockState: BlockState) = RenderShape.MODEL
override fun getRenderShape(blockState: BlockState?) = RenderShape.MODEL
override fun setPlacedBy(level: Level, pos: BlockPos, state: BlockState, placer: LivingEntity?, stack: ItemStack) {
super.setPlacedBy(level, pos, state, placer, stack)
val blockEntity = level.getBlockEntity(pos) as? PokeSnackBlockEntity ?: return
blockEntity.initializeFromItemStack(stack)
blockEntity.placedBy = placer?.uuid
val blockEntity = level.getBlockEntity(pos) as? PokeSnackBlockEntity
blockEntity?.initializeFromItemStack(stack)
}
override fun getCloneItemStack(level: LevelReader, pos: BlockPos, state: BlockState): ItemStack {
@@ -298,28 +294,15 @@ class PokeSnackBlock(settings: Properties, val isLure: Boolean): BaseEntityBlock
fun eat(level: Level, pos: BlockPos, state: BlockState, player: Player?) {
val bites = state.getValue(BITES) as Int
val newBites =
if (isLure) {
val pokeSnackBlockEntity = level.getBlockEntity(pos) as PokeSnackBlockEntity? ?: return
pokeSnackBlockEntity.amountSpawned += 1
floor(pokeSnackBlockEntity.amountSpawned / PokeSnackBlockEntity.SPAWNS_PER_BITE.toDouble()).toInt()
} else {
bites + 1
}
if (newBites > MAX_BITES) {
if (bites < MAX_BITES) {
level.setBlock(pos, state.setValue(BITES, bites + 1) as BlockState, UPDATE_ALL)
} else {
dropCandle(level, pos, state, player)
level.removeBlock(pos, false)
level.removeBlockEntity(pos)
} else {
level.setBlock(pos, state.setValue(BITES, newBites) as BlockState, UPDATE_ALL)
}
level.playSound(null, player?.blockPosition() ?: pos, SoundEvents.GENERIC_EAT, if (player != null) SoundSource.PLAYERS else SoundSource.NEUTRAL)
spawnEatParticles(level, pos)
player?.let {
level.gameEvent(it, GameEvent.BLOCK_DESTROY, pos)
}
@@ -369,19 +352,8 @@ class PokeSnackBlock(settings: Properties, val isLure: Boolean): BaseEntityBlock
0.0,
0.0,
0.0
)
}
}
override fun isRandomlyTicking(state: BlockState) = isLure
override fun randomTick(
state: BlockState,
level: ServerLevel,
pos: BlockPos,
random: RandomSource
) {
if (level.isClientSide) return
(level.getBlockEntity(pos) as? PokeSnackBlockEntity)?.randomTick()
}
}
@@ -8,8 +8,6 @@
package com.cobblemon.mod.common.block
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.entity.MoLangScriptingEntity
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.core.dispenser.DispenseItemBehavior
@@ -27,7 +25,6 @@ import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Items
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.Level
import net.minecraft.world.level.LevelAccessor
import net.minecraft.world.level.block.Block
@@ -43,9 +40,6 @@ import net.minecraft.world.level.material.FluidState
import net.minecraft.world.level.material.Fluids
import net.minecraft.world.level.pathfinder.PathComputationType
import net.minecraft.world.phys.BlockHitResult
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.EntityCollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraft.world.phys.shapes.VoxelShape
class SaccharineLeafBlock(settings: Properties) : LeavesBlock(settings) {
@@ -96,13 +90,7 @@ class SaccharineLeafBlock(settings: Properties) : LeavesBlock(settings) {
}
}
if (!added) Containers.dropItemStack(
level,
source.pos.x.toDouble(),
source.pos.y.toDouble(),
source.pos.z.toDouble(),
outputStack
)
if (!added) Containers.dropItemStack(level, source.pos.x.toDouble(), source.pos.y.toDouble(), source.pos.z.toDouble(), outputStack)
}
}
stack
@@ -120,27 +108,7 @@ class SaccharineLeafBlock(settings: Properties) : LeavesBlock(settings) {
)
}
override fun getCollisionShape(
state: BlockState,
level: BlockGetter,
pos: BlockPos,
context: CollisionContext
): VoxelShape? {
if (context is EntityCollisionContext) {
val entity = context.entity
if (entity != null
&& entity is MoLangScriptingEntity
&& entity.config.map.getOrDefault("can_path_through_sacc_leaves", DoubleValue.ZERO).asDouble() == 1.0
) {
return Shapes.empty()
}
}
return super.getCollisionShape(state, level, pos, context)
}
override fun isRandomlyTicking(state: BlockState): Boolean =
true //!state.getValue(PERSISTENT) && state.getValue(DISTANCE) > 6
override fun isRandomlyTicking(state: BlockState): Boolean = !state.getValue(PERSISTENT) && state.getValue(DISTANCE) > 6
override fun randomTick(state: BlockState, world: ServerLevel, pos: BlockPos, random: RandomSource) {
val currentAge = state.getValue(AGE)
@@ -155,8 +123,8 @@ class SaccharineLeafBlock(settings: Properties) : LeavesBlock(settings) {
} else if (belowState.block is SaccharineLeafBlock) {
val belowAge = belowState.getValue(AGE)
if (belowAge < MAX_AGE) {
world.setBlock(pos, changeAge(state, -1), UPDATE_CLIENTS)
world.setBlock(belowPos, changeAge(belowState, 1), UPDATE_CLIENTS)
changeAge(state, -1)
changeAge(belowState, 1)
}
break
} else {
@@ -249,15 +217,7 @@ class SaccharineLeafBlock(settings: Properties) : LeavesBlock(settings) {
override fun isPathfindable(state: BlockState, type: PathComputationType): Boolean = false
override fun useItemOn(
stack: ItemStack,
state: BlockState,
level: Level,
pos: BlockPos,
player: Player,
hand: InteractionHand,
hit: BlockHitResult
): ItemInteractionResult {
override fun useItemOn(stack: ItemStack, state: BlockState, level: Level, pos: BlockPos, player: Player, hand: InteractionHand, hit: BlockHitResult): ItemInteractionResult {
if (!state.getValue(WATERLOGGED)) {
val itemStack = player.getItemInHand(hand)
val isGlassBottle = itemStack.`is`(Items.GLASS_BOTTLE)
@@ -52,8 +52,8 @@ class CookingPotMenu : RecipeBookMenu<CraftingInput, CookingPotRecipeBase>, Cont
constructor(containerId: Int, playerInventory: Inventory) : super(CobblemonMenuType.COOKING_POT, containerId) {
this.playerInventory = playerInventory
this.container = CookingPotContainer(this,
CampfireBlockEntity.CRAFTING_GRID_WIDTH,
CampfireBlockEntity.CRAFTING_GRID_WIDTH
CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH,
CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
)
this.resultContainer = ResultContainer()
this.containerData = SimpleContainerData(4)
@@ -82,25 +82,25 @@ class CookingPotMenu : RecipeBookMenu<CraftingInput, CookingPotRecipeBase>, Cont
val resultSlotX = 128
val resultSlotY = 55
addSlot(CookingPotResultSlot(this.container, CampfireBlockEntity.RESULT_SLOT, resultSlotX, resultSlotY))
addSlot(CookingPotResultSlot(this.container, CampfireBlockEntity.Companion.RESULT_SLOT, resultSlotX, resultSlotY))
for ((index, slotIndex) in CampfireBlockEntity.CRAFTING_GRID_SLOTS.withIndex()) {
val i = index / CampfireBlockEntity.CRAFTING_GRID_WIDTH
val j = index % CampfireBlockEntity.CRAFTING_GRID_WIDTH
for ((index, slotIndex) in CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.withIndex()) {
val i = index / CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
val j = index % CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
addSlot(Slot(this.container, slotIndex, 33 + j * 18, 18 + i * 18))
}
for ((index, slotIndex) in CampfireBlockEntity.SEASONING_SLOTS.withIndex()) {
addSlot(SeasoningSlot(this, this.container, slotIndex, 110 + index * 18, 18))
for ((index, slotIndex) in CampfireBlockEntity.Companion.SEASONING_SLOTS.withIndex()) {
addSlot(SeasoningSlot(this.container, slotIndex, 110 + index * 18, 18))
}
for ((index, _) in CampfireBlockEntity.PLAYER_INVENTORY_SLOTS.withIndex()) {
val i = index / CampfireBlockEntity.PLAYER_INVENTORY_WIDTH
val j = index % CampfireBlockEntity.PLAYER_INVENTORY_WIDTH
for ((index, _) in CampfireBlockEntity.Companion.PLAYER_INVENTORY_SLOTS.withIndex()) {
val i = index / CampfireBlockEntity.Companion.PLAYER_INVENTORY_WIDTH
val j = index % CampfireBlockEntity.Companion.PLAYER_INVENTORY_WIDTH
addSlot(Slot(playerInventory, index + 9, 8 + j * 18, 84 + i * 18))
}
for ((index, _) in CampfireBlockEntity.PLAYER_HOTBAR_SLOTS.withIndex()) {
for ((index, _) in CampfireBlockEntity.Companion.PLAYER_HOTBAR_SLOTS.withIndex()) {
addSlot(Slot(playerInventory, index, 8 + index * 18, 142))
}
}
@@ -173,31 +173,29 @@ class CookingPotMenu : RecipeBookMenu<CraftingInput, CookingPotRecipeBase>, Cont
currentActiveRecipe = recipe
if (recipe != null) {
previewItem = recipe.value.assemble(craftInput, level.registryAccess())
// Apply seasoning to the preview item
recipe.value.applySeasoning(
previewItem,
container.items.subList(CampfireBlockEntity.SEASONING_SLOTS.first,
CampfireBlockEntity.SEASONING_SLOTS.last + 1)
.filterNotNull()
.filter { !it.isEmpty && it.`is`(recipe.value.seasoningTag) })
.filter { !it.isEmpty })
} else previewItem = ItemStack.EMPTY
}
override fun getResultSlotIndex(): Int {
return CampfireBlockEntity.RESULT_SLOT
return CampfireBlockEntity.Companion.RESULT_SLOT
}
override fun getGridWidth(): Int {
return CampfireBlockEntity.CRAFTING_GRID_WIDTH
return CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
}
override fun getGridHeight(): Int {
return CampfireBlockEntity.CRAFTING_GRID_WIDTH
return CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
}
override fun getSize(): Int {
return CampfireBlockEntity.ITEMS_SIZE
return CampfireBlockEntity.Companion.ITEMS_SIZE
}
fun getBurnProgress(): Float {
@@ -229,24 +227,24 @@ class CookingPotMenu : RecipeBookMenu<CraftingInput, CookingPotRecipeBase>, Cont
val slotItemStack = slot.item
itemStack = slotItemStack.copy()
if (index == CampfireBlockEntity.RESULT_SLOT) {
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.PLAYER_INVENTORY_SLOTS.first, CampfireBlockEntity.PLAYER_HOTBAR_SLOTS.last + 1, false)) {
if (index == CampfireBlockEntity.Companion.RESULT_SLOT) {
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.PLAYER_INVENTORY_SLOTS.first, CampfireBlockEntity.Companion.PLAYER_HOTBAR_SLOTS.last + 1, false)) {
return ItemStack.EMPTY
}
slot.onQuickCraft(slotItemStack, itemStack);
} else if (index in CampfireBlockEntity.PLAYER_INVENTORY_SLOTS || index in CampfireBlockEntity.PLAYER_HOTBAR_SLOTS) {
} else if (index in CampfireBlockEntity.Companion.PLAYER_INVENTORY_SLOTS || index in CampfireBlockEntity.Companion.PLAYER_HOTBAR_SLOTS) {
if (Seasonings.isSeasoning(slotItemStack)) {
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.CRAFTING_GRID_SLOTS.first, CampfireBlockEntity.CRAFTING_GRID_SLOTS.last + 1, false) &&
!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.SEASONING_SLOTS.first, CampfireBlockEntity.SEASONING_SLOTS.last + 1, false)
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.first, CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.last + 1, false) &&
!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.SEASONING_SLOTS.first, CampfireBlockEntity.Companion.SEASONING_SLOTS.last + 1, false)
) {
return ItemStack.EMPTY
}
} else if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.CRAFTING_GRID_SLOTS.first, CampfireBlockEntity.CRAFTING_GRID_SLOTS.last + 1, false)) {
} else if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.first, CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.last + 1, false)) {
return ItemStack.EMPTY
}
} else if (index in CampfireBlockEntity.CRAFTING_GRID_SLOTS || index in CampfireBlockEntity.SEASONING_SLOTS) {
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.PLAYER_INVENTORY_SLOTS.first, CampfireBlockEntity.PLAYER_HOTBAR_SLOTS.last + 1, false)) {
} else if (index in CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS || index in CampfireBlockEntity.Companion.SEASONING_SLOTS) {
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.PLAYER_INVENTORY_SLOTS.first, CampfireBlockEntity.Companion.PLAYER_HOTBAR_SLOTS.last + 1, false)) {
return ItemStack.EMPTY
}
}
@@ -8,13 +8,13 @@
package com.cobblemon.mod.common.block.campfirepot
import com.cobblemon.mod.common.api.cooking.Seasonings
import net.minecraft.world.Container
import net.minecraft.world.inventory.Slot
import net.minecraft.world.item.ItemStack
class SeasoningSlot(val menu: CookingPotMenu, container: Container, slot: Int, x: Int, y: Int) : Slot(container, slot, x, y) {
class SeasoningSlot(container: Container, slot: Int, x: Int, y: Int) : Slot(container, slot, x, y) {
override fun mayPlace(stack: ItemStack): Boolean {
val recipe = menu.currentActiveRecipe?.value() ?: return false
return stack.`is`(recipe.seasoningTag) && super.mayPlace(stack)
return Seasonings.isSeasoning(stack) && super.mayPlace(stack)
}
}
@@ -134,8 +134,6 @@ class CampfireBlockEntity(pos: BlockPos, state: BlockState) : BaseContainerBlock
campfireBlockEntity.particleCooldown = 20
}
campfireBlockEntity.time++
}
fun serverTick(level: Level, pos: BlockPos, state: BlockState, campfireBlockEntity: CampfireBlockEntity) {
@@ -168,7 +166,7 @@ class CampfireBlockEntity(pos: BlockPos, state: BlockState) : BaseContainerBlock
val cookedItem = recipe.assemble(craftingInput, level.registryAccess())
val resultSlotItem = campfireBlockEntity.getItem(0)
recipe.applySeasoning(cookedItem, campfireBlockEntity.getSeasonings().filter { it.`is`(recipe.seasoningTag) })
recipe.applySeasoning(cookedItem, campfireBlockEntity.getSeasonings())
if (!campfireBlockEntity.blockState.getValue(CampfireBlock.LID)) {
campfireBlockEntity.cookingProgress = 0
@@ -239,7 +237,6 @@ class CampfireBlockEntity(pos: BlockPos, state: BlockState) : BaseContainerBlock
private var particleCooldown: Int = 0
var brothColor: Int = BASE_BROTH_COLOR
var bubbleColor: Int = BASE_BROTH_BUBBLE_COLOR
var time: Int = 0
var dataAccess: ContainerData = object : ContainerData {
override fun get(index: Int): Int {
@@ -314,7 +311,7 @@ class CampfireBlockEntity(pos: BlockPos, state: BlockState) : BaseContainerBlock
consumeItem(i)
}
for (i in SEASONING_SLOTS.first..SEASONING_SLOTS.last) {
if (getItem(i).`is`(recipe.seasoningTag) && recipe.seasoningProcessors.any { it.consumesItem(getItem(i)) }) consumeItem(i)
if (recipe.seasoningProcessors.any { it.consumesItem(getItem(i)) }) consumeItem(i)
}
val direction = state.getValue(CampfireBlock.ITEM_DIRECTION)
@@ -8,118 +8,29 @@
package com.cobblemon.mod.common.block.entity
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonBlockEntities
import com.cobblemon.mod.common.CobblemonItemComponents
import com.cobblemon.mod.common.api.cooking.getColourMixFromFlavours
import com.cobblemon.mod.common.api.fishing.SpawnBait
import com.cobblemon.mod.common.api.fishing.SpawnBait.Effect
import com.cobblemon.mod.common.api.fishing.SpawnBaitEffects
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.detail.PokemonHerdSpawnDetail
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.api.spawning.influence.BucketMultiplyingInfluence
import com.cobblemon.mod.common.api.spawning.influence.BucketNormalizingInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawnBaitInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.spawner.FixedAreaSpawner
import com.cobblemon.mod.common.block.PokeSnackBlock
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.item.components.BaitEffectsComponent
import com.cobblemon.mod.common.item.components.FlavourComponent
import com.cobblemon.mod.common.item.components.IngredientComponent
import com.cobblemon.mod.common.net.messages.client.effect.PokeSnackBlockParticlesPacket
import com.cobblemon.mod.common.util.DataKeys
import java.util.UUID
import net.minecraft.core.BlockPos
import net.minecraft.core.HolderLookup
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.NbtOps
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.Entity
import net.minecraft.world.entity.player.Player
import net.minecraft.nbt.*
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.block.state.BlockState
import kotlin.collections.orEmpty
open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) :
TintBlockEntity(CobblemonBlockEntities.POKE_SNACK, pos, state),
SpawningInfluence {
open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) : TintBlockEntity(CobblemonBlockEntities.POKE_SNACK, pos, state) {
companion object {
const val SPAWNS_PER_BITE = 1
const val RADIUS = 8
const val RANDOM_TICKS_BETWEEN_SPAWNS = 4
const val POKE_SNACK_CRUMBED_ASPECT = "poke_snack_crumbed"
}
val spawner: FixedAreaSpawner by lazy {
FixedAreaSpawner(
name = "poke_snack_spawner_${level?.dimension()?.location()}}_$blockPos",
spawnPool = CobblemonSpawnPools.WORLD_SPAWN_POOL,
world = level as ServerLevel,
position = blockPos,
horizontalRadius = RADIUS,
verticalRadius = RADIUS,
maxPokemonPerChunk = Cobblemon.config.pokeSnackPokemonPerChunk
).also {
it.influences.add(this)
val baitEffects = getBaitEffects()
val highestLureTier = baitEffects
.filter { it.type == SpawnBait.Effects.RARITY_BUCKET }
.maxOfOrNull { it.value }
?.toInt()
?: 0
if (highestLureTier > 0) {
it.influences += BucketNormalizingInfluence(tier = highestLureTier)
}
it.influences += BucketMultiplyingInfluence(
mapOf(
"uncommon" to 2.25f,
"rare" to 5.5f,
"ultra-rare" to 5.5f,
)
)
it.influences += SpawnBaitInfluence(effects = baitEffects)
}
}
// Only non-herd Pokémon can be spawned from a Poké Snack
override fun affectSpawnable(detail: SpawnDetail, spawnablePosition: SpawnablePosition) = detail.type in SpawnDetail.pokemonTypes && detail !is PokemonHerdSpawnDetail
// When a Pokémon spawns, apply the crumbed aspect and play the eating effects. Eat part of the cake, too.
override fun affectSpawn(action: SpawnAction<*>, entity: Entity) {
if (entity is PokemonEntity) {
entity.pokemon.forcedAspects += POKE_SNACK_CRUMBED_ASPECT
val block = blockState.block as? PokeSnackBlock ?: return
val level = level ?: return
val entityPos = entity.blockPosition()
PokeSnackBlockParticlesPacket(blockPos, entityPos).sendToPlayersAround(
blockPos.x.toDouble(),
blockPos.y.toDouble(),
blockPos.z.toDouble(),
64.0,
level.dimension(),
)
block.eat(level, blockPos, blockState, action.spawnablePosition.cause.entity as? Player)
}
}
var placedBy: UUID? = null
var amountSpawned: Int = 0
var flavourComponent: FlavourComponent? = null
var baitEffectsComponent: BaitEffectsComponent? = null
var ingredientComponent: IngredientComponent? = null
var randomTicksUntilNextSpawn: Float = getRandomTicksBetweenSpawns()
fun initializeFromItemStack(itemStack: ItemStack) {
flavourComponent = itemStack.get(CobblemonItemComponents.FLAVOUR)
@@ -127,7 +38,9 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) :
if (isLure()) baitEffectsComponent = itemStack.get(CobblemonItemComponents.BAIT_EFFECTS)
flavourComponent?.let {
getColourMixFromFlavours(it.getDominantFlavours())?.let(::setTint)
getColourMixFromFlavours(it.getDominantFlavours())?.let {
setTint(it)
}
}
}
@@ -139,60 +52,14 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) :
return false
}
fun randomTick() {
randomTicksUntilNextSpawn--
if (randomTicksUntilNextSpawn <= 0) {
val nearestPlayer = level?.getNearestPlayer(
blockPos.x.toDouble(),
blockPos.y.toDouble(),
blockPos.z.toDouble(),
Cobblemon.config.maximumSpawningZoneDistanceFromPlayer.toDouble(),
false, // true here makes it so creative players aren't considered
)
// High simulation distances may cause the player to be null here, we don't want to spawn without a player nearby.
if (nearestPlayer != null) {
spawner.run(
cause = SpawnCause(spawner = spawner, entity = nearestPlayer),
maxSpawns = 1
)
}
randomTicksUntilNextSpawn = getRandomTicksBetweenSpawns()
}
}
fun getRandomTicksBetweenSpawns(): Float {
val biteTimeMultiplier = getBiteTimeMultiplier()
return (RANDOM_TICKS_BETWEEN_SPAWNS * biteTimeMultiplier).coerceAtLeast(1F)
}
fun getBiteTimeMultiplier(): Float {
val baitEffects = getBaitEffects()
val biteTimeEffects = baitEffects.filter { it.type == SpawnBait.Effects.BITE_TIME }
if (biteTimeEffects.isEmpty()) return 1F
val biteTimeEffect = biteTimeEffects.random()
if (Math.random() > biteTimeEffect.chance) {
return 1F
}
return 1F - biteTimeEffect.value.toFloat()
}
fun toItemStack(): ItemStack {
val stack = ItemStack(this.blockState.block)
if (isLure() && baitEffectsComponent != null) {
stack.set(CobblemonItemComponents.BAIT_EFFECTS, baitEffectsComponent)
}
if (flavourComponent != null) {
stack.set(CobblemonItemComponents.FLAVOUR, flavourComponent)
}
if (ingredientComponent != null) {
stack.set(CobblemonItemComponents.INGREDIENT, ingredientComponent)
}
return stack
}
@@ -200,18 +67,12 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) :
* Combine all the [SpawnBait.Effect] values from the [baitEffectsComponent] data.
*/
fun getBaitEffects(): List<Effect> {
return baitEffectsComponent
?.effects
?.mapNotNull(SpawnBaitEffects::getFromIdentifier)
?.flatMap { it.effects }
.orEmpty()
return baitEffectsComponent?.effects?.mapNotNull(SpawnBaitEffects::getFromIdentifier)?.flatMap { it.effects }.orEmpty()
}
override fun saveAdditional(tag: CompoundTag, registries: HolderLookup.Provider) {
super.saveAdditional(tag, registries)
tag.putInt(DataKeys.AMOUNT_SPAWNED, amountSpawned)
flavourComponent?.let { component ->
CobblemonItemComponents.FLAVOUR.codec()!!
.encodeStart(NbtOps.INSTANCE, component)
@@ -238,19 +99,11 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) :
tag.put(DataKeys.INGREDIENTS, encodedTag)
}
}
tag.putFloat(DataKeys.TICKS_UNTIL_NEXT_SPAWN, randomTicksUntilNextSpawn)
placedBy?.let {
tag.putUUID(DataKeys.PLACED_BY, it)
}
}
override fun loadAdditional(tag: CompoundTag, registries: HolderLookup.Provider) {
super.loadAdditional(tag, registries)
amountSpawned = tag.getInt(DataKeys.AMOUNT_SPAWNED)
if (tag.contains(DataKeys.FLAVOUR)) {
CobblemonItemComponents.FLAVOUR.codec()
?.parse(NbtOps.INSTANCE, tag.get(DataKeys.FLAVOUR))
@@ -277,13 +130,5 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) :
ingredientComponent = component
}
}
if (tag.contains(DataKeys.TICKS_UNTIL_NEXT_SPAWN)) {
randomTicksUntilNextSpawn = tag.getFloat(DataKeys.TICKS_UNTIL_NEXT_SPAWN)
}
if (tag.contains(DataKeys.PLACED_BY)) {
placedBy = tag.getUUID(DataKeys.PLACED_BY)
}
}
}
@@ -69,11 +69,14 @@ import com.cobblemon.mod.common.client.tooltips.RecipeSeasoningAbsorbtionTooltip
import com.cobblemon.mod.common.client.tooltips.SeasoningTooltipGenerator
import com.cobblemon.mod.common.client.tooltips.TooltipManager
import com.cobblemon.mod.common.client.trade.ClientTrade
import com.cobblemon.mod.common.debug.RidingDebugRenderer
import com.cobblemon.mod.common.entity.boat.CobblemonBoatType
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.platform.events.PlatformEvents
import com.cobblemon.mod.common.pokedex.scanner.PokedexUsageContext
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.isLookingAt
import io.github.flemmli97.debugutils.api.RegisterDebugRenderers
import net.minecraft.client.Minecraft
import net.minecraft.client.color.block.BlockColor
import net.minecraft.client.gui.GuiGraphics
@@ -193,6 +196,11 @@ object CobblemonClient {
}
ClientPlayerIcon.onTick()
}
val debugId = cobblemonResource("riding")
RegisterDebugRenderers.registerCustomDebugRenderer(debugId, RidingDebugRenderer)
RegisterDebugRenderers.registerServerToggle(debugId)
RegisterDebugRenderers.registerClientHandler(debugId) { RidingDebugRenderer.clientHandler(it) }
}
private fun registerTooltipManagers() {
@@ -45,7 +45,6 @@ class CobblemonConfigScreen(
val clonedConfig = Cobblemon.config.clone()
val defaultConfig = CobblemonConfig()
val hideServerConfigs = Minecraft.getInstance().currentServer != null && !Minecraft.getInstance().isLocalServer
private lateinit var layout: HeaderAndFooterLayout
private lateinit var searchEdit: EditBox
@@ -21,7 +21,6 @@ import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.config.Category
import com.cobblemon.mod.common.config.CobblemonConfig
import com.cobblemon.mod.common.config.CobblemonConfigField
import com.cobblemon.mod.common.config.CobblemonConfigField.CobblemonConfigSide.SERVER
import com.cobblemon.mod.common.util.asTranslated
import net.minecraft.SharedConstants
import net.minecraft.client.Minecraft
@@ -33,7 +32,6 @@ import net.minecraft.client.gui.narration.NarratableEntry
import kotlin.math.min
import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaField
@@ -64,11 +62,6 @@ class CobblemonConfigVariableList(
return@map null
}
val fieldAnnotation = property.javaField!!.getAnnotation(CobblemonConfigField::class.java)
if (parent.hideServerConfigs && fieldAnnotation.side == SERVER) {
return@map null
}
val entry: CobblemonConfigVariable? =
when (val classifier = property.returnType.classifier) {
String::class -> CobblemonConfigStringVariable(this, property as KMutableProperty1<out CobblemonConfig, String>)
@@ -201,7 +201,7 @@ class CookingPotScreen(
}
override fun renderSlot(guiGraphics: GuiGraphics, slot: Slot) {
if (slot is SeasoningSlot && ((!menu.carried.isEmpty && !slot.mayPlace(menu.carried)) || (slot.hasItem() && !slot.mayPlace(slot.item))) ) {
if (slot is SeasoningSlot && !menu.carried.isEmpty && !slot.mayPlace(menu.carried)) {
val x = slot.x
val y = slot.y
guiGraphics.fill(x,y,x+16,y+16,822018048)
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common.client.gui.debug.riding
import com.bedrockk.molang.Expression
import com.cobblemon.mod.common.CobblemonNetwork
import com.cobblemon.mod.common.CobblemonRideSettings
import com.cobblemon.mod.common.api.riding.RidingStyle
import com.cobblemon.mod.common.api.riding.behaviour.RidingBehaviourSettings
import com.cobblemon.mod.common.api.text.text
@@ -90,7 +89,7 @@ class RideSettingsEditorGUI(val parentScreen: Screen, val vehicle: PokemonEntity
for (variable in expressionVariables) {
val (name, value) = variable
val editBox = EditBox(this.minecraft!!.font, 130, currentY, 300, 20, name.text())
val editBox = EditBox(this.minecraft!!.font, 110, currentY, 300, 20, name.text())
editBox.setMaxLength(256)
editBox.value = value
@@ -159,10 +158,6 @@ class RideSettingsEditorGUI(val parentScreen: Screen, val vehicle: PokemonEntity
val clazz = settings.javaClass
val fields = clazz.declaredFields
val gsField = CobblemonRideSettings.javaClass.declaredFields.firstOrNull { it.type == clazz }
gsField?.isAccessible = true
val controllerGlobalSettings = gsField?.get(CobblemonRideSettings) as? RidingBehaviourSettings?
// Iterate through all expression fields and return the list of those that
// aer expressions
return fields.mapNotNull { field ->
@@ -170,18 +165,10 @@ class RideSettingsEditorGUI(val parentScreen: Screen, val vehicle: PokemonEntity
field.isAccessible = true // Make private fields accessible
val name = field.name
// Get the value, cast it, and call .getString() on it
if (field.get(settings) != null) {
val value = (field.get(settings) as Expression).getString()
val value = (field.get(settings) as Expression).getString()
// Create the pair and return it for the list
name to value
} else {
// If the settings field is null then check for a global setting
val value = getMatchingGlobalSettingsValue(settings, controllerGlobalSettings, name) ?: "novalue"
name to value
}
// Create the pair and return it for the list
name to value
} else {
// If it's not an Expression (they all honestly should be if being used by the
// behaviour in question) then ignore
@@ -189,13 +176,4 @@ class RideSettingsEditorGUI(val parentScreen: Screen, val vehicle: PokemonEntity
}
}
}
private fun getMatchingGlobalSettingsValue(settings: RidingBehaviourSettings, globalSettings: RidingBehaviourSettings?, fieldName: String): String? {
if (globalSettings == null) return null
val gssField = globalSettings.javaClass.declaredFields.firstOrNull {
it.name == fieldName
}
gssField?.isAccessible = true
return (gssField?.get(globalSettings) as? Expression)?.getString()
}
}
@@ -24,7 +24,6 @@ abstract class AnimatedPortraitDrawer : PortraitDrawer {
private val stateAtIndex = hashMapOf<Int, Pair<UUID, FloatingState>>()
override fun draw(pokemon: Pokemon, poseStack: PoseStack, partialTicks: Float, isSelected: Boolean, index: Int) {
state.currentAspects = pokemon.aspects
drawPosablePortrait(
identifier = pokemon.species.resourceIdentifier,
matrixStack = poseStack,
@@ -25,8 +25,6 @@ import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import com.cobblemon.mod.common.client.gui.summary.widgets.screens.stats.features.FriendshipFeatureRenderer
import com.cobblemon.mod.common.client.gui.summary.widgets.screens.stats.features.FullnessFeatureRenderer
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.pokemon.EVs
import com.cobblemon.mod.common.pokemon.IVs
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
@@ -76,7 +74,6 @@ class StatWidget(
private val statsBaseResource = cobblemonResource("textures/gui/summary/summary_stats_chart_base.png")
private val statsChartResource = cobblemonResource("textures/gui/summary/summary_stats_chart.png")
private val statsChartPentagonResource = cobblemonResource("textures/gui/summary/summary_stats_chart_pentagon.png")
private val statsChartPentagonHighlightResource = cobblemonResource("textures/gui/summary/summary_stats_chart_pentagon_highlight.png")
private val statsOtherBaseResource = cobblemonResource("textures/gui/summary/summary_stats_other_base.png")
private val statsOtherSidebarsResource = cobblemonResource("textures/gui/summary/summary_stats_other_sidebars.png")
private val statsOtherSidebarArrowLeft = cobblemonResource("textures/gui/summary/summary_stats_other_sidebar_arrow_left.png")
@@ -293,34 +290,33 @@ class StatWidget(
}
} else {
if (renderPentagonStats) {
blitk(
matrixStack = matrices,
texture = statsChartPentagonResource,
x= (x + 20.5) / SCALE,
y = (y + 22) / SCALE,
width = 186,
height = 176,
scale = SCALE
)
pokemon.form.riding.behaviours?.let {
val behaviours = it.entries.toList()
val selectedBehaviour = behaviours[rideBehaviourIndex]
val canSwitchStyle = behaviours.size > 1 && centerHovered(pMouseX, pMouseY)
blitk(
matrixStack = matrices,
texture = if (canSwitchStyle) statsChartPentagonHighlightResource else statsChartPentagonResource,
x= (x + 20.5) / SCALE,
y = (y + 22) / SCALE,
width = 186,
height = 176,
scale = SCALE
)
val pentagonColour = when (selectedBehaviour.key) {
RidingStyle.AIR -> {
if (canSwitchStyle)
if (behaviours.size > 1 && centerHovered(pMouseX, pMouseY))
Vector3f(70F/255F, 235F/225F, 195F/255F)
else Vector3f(40F/255F, 205F/255F, 165F/255F)
}
RidingStyle.LIQUID -> {
if (canSwitchStyle)
if (behaviours.size > 1 && centerHovered(pMouseX, pMouseY))
Vector3f(95F/255F, 165F/255F, 1F)
else Vector3f(65F/255F, 135F/255F, 1F)
}
else -> {
if (canSwitchStyle)
if (behaviours.size > 1 && centerHovered(pMouseX, pMouseY))
Vector3f(1F, 195F/255F, 30F/255F)
else Vector3f(1F, 165F/255F, 0F)
}
@@ -357,8 +353,8 @@ class StatWidget(
context = context,
labels = RidingStat.entries.toList().map { stat ->
if (statLabelsHovered(pentagonVerticesOffset, pMouseX, pMouseY))
"+${floor(pokemon.getRideBoost(stat) / pokemon.getMaxRideBoost(stat) * 100)}%".text()
else floor(pokemon.getRideStat(selectedBehaviour.key, stat)).toString().text()
"${floor((pokemon.getRideStat(selectedBehaviour.key, stat) / (selectedBehaviour.value.stats[stat]?.endInclusive ?: 1)) * 100)}%".text()
else floor(pokemon.getRideStat(selectedBehaviour.key, stat)).toString().text()
},
verticesOffset = pentagonVerticesOffset,
offsetY = 5.5
@@ -393,7 +389,7 @@ class StatWidget(
colour = Vector3f(216F/255, 100F/255, 1F)
)
EV -> drawStatPolygon(
statLabels.values.map { (pokemon.evs[it]?.toFloat() ?: 0F) / EVs.MAX_STAT_VALUE.toFloat() },
statLabels.values.map { (pokemon.evs[it]?.toFloat() ?: 0F) / 252F },
colour = Vector3f(1F, 1F, 100F/255F)
)
}
@@ -407,9 +403,9 @@ class StatWidget(
"${
if (pokemon.ivs.isHyperTrained(stat.value)) "${pokemon.ivs[stat.value]} (${pokemon.ivs.hyperTrainedIVs[stat.value]})"
else pokemon.ivs[stat.value].toString()
}/${IVs.MAX_VALUE}".text()
}/31".text()
}
EV -> "${pokemon.evs.getOrDefault(stat.value)}/${EVs.MAX_STAT_VALUE}".text()
EV -> "${pokemon.evs.getOrDefault(stat.value)}/252".text()
else -> stat.key
}
} else { stat.key }
@@ -494,14 +490,14 @@ class StatWidget(
STATS -> (if (stat == Stats.HP) "${pokemon.currentHealth} / ${pokemon.maxHealth}" else pokemon.getStat(stat).toString()).text()
IV -> {
(if (asPercent) {
"${floor((((if (pokemon.ivs.isHyperTrained(stat)) pokemon.ivs.hyperTrainedIVs[stat] else pokemon.ivs[stat]) ?: 0) / IVs.MAX_VALUE.toDouble()) * 100)}%"
"${floor((((if (pokemon.ivs.isHyperTrained(stat)) pokemon.ivs.hyperTrainedIVs[stat] else pokemon.ivs[stat]) ?: 0) / 31.0) * 100)}%"
} else {
if (pokemon.ivs.isHyperTrained(stat)) "${pokemon.ivs[stat]} (${pokemon.ivs.hyperTrainedIVs[stat]})"
else pokemon.ivs[stat].toString()
}).text()
}
EV -> {
(if (asPercent) "${floor((pokemon.evs.getOrDefault(stat) / EVs.MAX_STAT_VALUE.toDouble()) * 100)}%"
(if (asPercent) "${floor((pokemon.evs.getOrDefault(stat) / 252.0) * 100)}%"
else pokemon.evs.getOrDefault(stat).toString()).text()
}
else -> "0".text()
@@ -562,9 +558,7 @@ class StatWidget(
}
private fun centerHovered(pMouseX: Number, pMouseY: Number): Boolean {
val centerX = x + (WIDTH / 2)
val centerY = y + (HEIGHT / 2)
return (pMouseX.toInt() in (centerX - 30)..(centerX + 30)) && (pMouseY.toInt() in (centerY - 30)..(centerY + 30))
return (pMouseX.toInt() in (x + 44)..(x + 90)) && (pMouseY.toInt() in (y + 47)..(y + 93))
}
private fun statLabelsHovered(labelOffsets: List<Pair<Double, Double>>, mouseX: Int, mouseY: Int): Boolean {
@@ -9,12 +9,13 @@
package com.cobblemon.mod.common.client.net.data
import com.cobblemon.mod.common.CobblemonRideSettings
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.net.messages.client.data.RideSettingsSyncPacket
import net.minecraft.client.Minecraft
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
object RideSettingsSyncHandler : ClientNetworkPacketHandler<RideSettingsSyncPacket> {
override fun handle(packet: RideSettingsSyncPacket, client: Minecraft) {
object RideSettingsSyncHandler : ServerNetworkPacketHandler<RideSettingsSyncPacket> {
override fun handle(packet: RideSettingsSyncPacket, server: MinecraftServer, player: ServerPlayer) {
CobblemonRideSettings.bird = packet.bird
CobblemonRideSettings.glider = packet.glider
CobblemonRideSettings.helicopter = packet.helicopter
@@ -11,14 +11,7 @@ package com.cobblemon.mod.common.client.render
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.client.render.models.blockbench.PosableModel
import com.cobblemon.mod.common.client.render.models.blockbench.PosableState
import com.cobblemon.mod.common.client.render.models.blockbench.animation.PoseTransitionAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockActiveAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockPoseAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.TextureInterpolationType
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Bone
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
import com.cobblemon.mod.common.client.render.models.blockbench.repository.RenderContext
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
import com.cobblemon.mod.common.util.adapters.*
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
@@ -28,13 +21,10 @@ import com.cobblemon.mod.common.util.toRGBA
import com.google.gson.GsonBuilder
import com.google.gson.annotations.SerializedName
import com.mojang.blaze3d.platform.NativeImage
import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.RenderStateShard
import net.minecraft.client.renderer.texture.DynamicTexture
import kotlin.math.floor
import net.minecraft.resources.ResourceLocation
import org.joml.Vector2d
import org.joml.Vector3f
import org.joml.Vector4f
@@ -63,7 +53,7 @@ class VaryingRenderableResolver(
}
fun getResolvedTexture(state: PosableState): ResourceLocation {
return getVariationValue(state) { texture }?.getTexture(state)
return getVariationValue(state) { texture }?.invoke(state)
?: throw IllegalStateException("Unable to find a texture for $name with aspects ${state.currentAspects.joinToString()}. This shouldn't be possible if you've defined the fallback variation.")
}
@@ -190,13 +180,6 @@ class ModelAssetVariation(
}
}
/** Needed a custom one, so I can make it take in a dynamic texture instead of resource location */
class DynamicStateShard(texture: DynamicTexture) : RenderStateShard.EmptyTextureStateShard(Runnable {
RenderSystem.setShaderTexture(0, texture.id)
}, Runnable {
texture.close() // Cleanup
})
/**
* Given the animation seconds, returns a texture to use. Only implemented
* by [StaticModelTextureSupplier] and [AnimatedModelTextureSupplier].
@@ -204,17 +187,12 @@ class DynamicStateShard(texture: DynamicTexture) : RenderStateShard.EmptyTexture
* @author Hiroku
* @since February 6th, 2023
*/
interface ModelTextureSupplier {
operator fun invoke(state: PosableState): RenderStateShard.EmptyTextureStateShard
fun getTexture(state: PosableState): ResourceLocation
fun interface ModelTextureSupplier {
operator fun invoke(state: PosableState): ResourceLocation
}
class StaticModelTextureSupplier(val texture: ResourceLocation): ModelTextureSupplier {
override fun invoke(state: PosableState): RenderStateShard.EmptyTextureStateShard {
return RenderStateShard.TextureStateShard(texture, false, false)
}
override fun getTexture(state: PosableState): ResourceLocation {
override fun invoke(state: PosableState): ResourceLocation {
return texture
}
}
@@ -225,196 +203,80 @@ class AnimatedModelTextureSupplier(
val frames: List<ResourceLocation>,
val interpolation: Boolean
): ModelTextureSupplier {
override fun invoke(state: PosableState): RenderStateShard.EmptyTextureStateShard {
var frameIndex = state.animationSeconds * fps
if (frameIndex > frames.size - 1.0f && !loop) frameIndex = frames.size.toFloat() - 1.0f
val i = frameIndex % 1.0
if (interpolation && i != 0.0) {
val o = floor(frameIndex).toInt() % frames.size
val s = if (o == frames.size - 1) 0 else o + 1
val resourceManager = Minecraft.getInstance().resourceManager
val texture1 = NativeImage.read(resourceManager.getResourceOrThrow(frames[o]).open())
val texture2 = NativeImage.read(resourceManager.getResourceOrThrow(frames[s]).open())
val image = getInterpolatedTexture(texture1, texture2, i)
val newTexture = DynamicTexture(image)
newTexture.setFilter(false, false)
return DynamicStateShard(newTexture)
}
else {
val frame = floor(frameIndex).toInt() % frames.size
return RenderStateShard.TextureStateShard(frames[frame], false, false)
}
}
override fun getTexture(state: PosableState): ResourceLocation {
override fun invoke(state: PosableState): ResourceLocation {
val frameIndex = floor(state.animationSeconds * fps).toInt()
return frames[frameIndex]
}
}
class KeyframableTextureSupplier(val textures: MutableList<ResourceLocation>) : ModelTextureSupplier {
var layerName = ""
override fun invoke(state: PosableState): RenderStateShard.EmptyTextureStateShard {
val resourceManager = Minecraft.getInstance().resourceManager
if (state.primaryAnimation?.animation is PoseTransitionAnimation) {
val poseTransition = state.primaryAnimation!!.animation as PoseTransitionAnimation
val (animation1, startTime1) = getMainAnimFromPose(poseTransition.beforePose, state, true)
val (animation2, startTime2) = getMainAnimFromPose(poseTransition.afterPose, state, true)
val texture2 = if (animation2 != null) {
getImageFromAnimation(animation2, state.animationSeconds - startTime2)
} else {
val image = NativeImage.read(resourceManager.getResourceOrThrow(textures[0]).open())
getEmptyTexture(image.width, image.height)
}
val (interpType, cubicValues) = poseTransition.beforePose.texturePhaseOut
?: poseTransition.afterPose.texturePhaseIn
if (interpType == TextureInterpolationType.STEP) {
val newTexture = DynamicTexture(texture2)
newTexture.setFilter(false, false)
return DynamicStateShard(newTexture)
}
val texture1 = if (animation1 != null) {
getImageFromAnimation(animation1, state.animationSeconds - startTime1)
} else {
val image = NativeImage.read(resourceManager.getResourceOrThrow(textures[0]).open())
getEmptyTexture(image.width, image.height)
}
var i =
((state.animationSeconds - poseTransition.startTime) / (poseTransition.endTime - poseTransition.startTime)).toDouble() //Linear
if (interpType == TextureInterpolationType.BEZIER) { //Switch to Bezier
i = resolveCubicValues(cubicValues, i)
}
val newImage = getInterpolatedTexture(texture1, texture2, i)
val newTexture = DynamicTexture(newImage)
newTexture.setFilter(false, false)
return DynamicStateShard(newTexture)
return if (frameIndex >= frames.size && !loop) {
frames.last()
} else {
var animation = if ((state.primaryAnimation?.animation as? BedrockActiveAnimation)?.animation?.cobblemonTextures?.get(layerName) != null) {
(state.primaryAnimation?.animation as? BedrockActiveAnimation)?.animation
} else null
var startTime = 0F
if (animation == null) {
val data = getMainAnimFromPose(state.currentPose?.let(state.currentModel!!.poses::get), state)
animation = data.first
startTime = data.second
}
if (animation != null) {
val image = getImageFromAnimation(animation, state.animationSeconds - startTime)
val newTexture = DynamicTexture(image)
newTexture.setFilter(false, false)
return DynamicStateShard(newTexture)
}
frames[frameIndex % frames.size]
}
val image = NativeImage.read(resourceManager.getResourceOrThrow(textures[0]).open())
val newTexture = DynamicTexture(getEmptyTexture(image.width, image.height))
newTexture.setFilter(false, false)
return DynamicStateShard(newTexture)
}
override fun getTexture(state: PosableState): ResourceLocation {
return textures[0]
}
private fun getMainAnimFromPose(pose: Pose?, state: PosableState, transitionAnimation: Boolean = false): Pair<BedrockAnimation?, Float> {
var animation: BedrockAnimation? = null
var startTime = 0F
pose?.animations?.filterIsInstance<BedrockPoseAnimation>()?.filter{ transitionAnimation || state.shouldIdleRun(it, 0F) }?.forEach {
if (it.animation.cobblemonTextures[layerName] != null) {
animation = it.animation
}
}
if (state.primaryAnimation == null && state.currentModel!!.context.request(RenderContext.DO_QUIRKS) != false) {
pose?.quirks?.forEach { quirk ->
val quirkData = quirk.getOrCreateData(state)
quirkData.animations.filterIsInstance<BedrockActiveAnimation>().forEach {
if (it.animation.cobblemonTextures[layerName] != null) {
startTime = it.startedSeconds
animation = it.animation
}
}
}
}
return Pair(animation, startTime)
}
private fun getImageFromAnimation(animation: BedrockAnimation, animationSeconds: Float): NativeImage {
fun interpolatedTexture(state: PosableState): DynamicTexture? {
val resourceManager = Minecraft.getInstance().resourceManager
val keyframeMap = animation.cobblemonTextures[layerName]
if (keyframeMap.isNullOrEmpty()) {
val image1 = NativeImage.read(resourceManager.getResourceOrThrow(textures[0]).open())
return getEmptyTexture(image1.width, image1.height)
}
var adjustedTime = animationSeconds
if (animation.shouldLoop) adjustedTime %= animation.animationLength.toFloat()
val keyIndex = keyframeMap.keys.indexOfLast { it <= adjustedTime }
val keyframe1 = when(keyIndex) {
-1 -> keyframeMap.firstEntry()
else -> keyframeMap.entries.toMutableList()[keyIndex]
}
val keyframe2 = when {
keyIndex + 1 >= keyframeMap.size -> keyframeMap.lastEntry()
else -> keyframeMap.entries.toMutableList()[keyIndex+1]
val frameIndex = floor(state.animationSeconds * fps).toInt()
try {
if (frameIndex >= frames.size && !loop) {
return DynamicTexture(NativeImage.read(resourceManager.getResourceOrThrow(frames.last()).open()))
}
} catch (e : Exception) {
return null
}
val o = frameIndex % frames.size
val i = state.animationSeconds * fps - frameIndex
val texture1 : NativeImage
var texture2 : NativeImage? = null
val texture2 : NativeImage
if (keyframe1.value.textureSlot == -1 && (keyframe2.value.textureSlot == -1 || keyframe1.value.interpolationType == TextureInterpolationType.STEP)) { // If both -1 we just return empty texture or if Step
val image1 = NativeImage.read(resourceManager.getResourceOrThrow(textures[0]).open())
return getEmptyTexture(image1.width, image1.height)
try { //Try getting images of the frames so we can use them to make a new one
texture1 = NativeImage.read(resourceManager.getResourceOrThrow(frames[o]).open())
val s = if (o == frames.size - 1) 0 else o + 1
texture2 = NativeImage.read(resourceManager.getResourceOrThrow(frames[s]).open())
} catch (e : Exception) {
return null
}
var inverseInterp = false
if (keyframe1.value.textureSlot == -1) {
texture1 = NativeImage.read(resourceManager.getResourceOrThrow(textures[keyframe2.value.textureSlot]).open())
inverseInterp = true
} else if (keyframe2.value.textureSlot == -1) {
texture1 = NativeImage.read(resourceManager.getResourceOrThrow(textures[keyframe1.value.textureSlot]).open())
} else {
texture1 = NativeImage.read(resourceManager.getResourceOrThrow(textures[keyframe1.value.textureSlot]).open())
texture2 = NativeImage.read(resourceManager.getResourceOrThrow(textures[keyframe2.value.textureSlot]).open())
}
for (x in 0..<texture1.width) {
for (y in 0..<texture1.height) {
val color1 = texture1.getPixelRGBA(x,y).toRGBA()
val color2 = texture2.getPixelRGBA(x,y).toRGBA()
val i = (adjustedTime - keyframe1.key.toFloat()) / (keyframe2.key - keyframe1.key)
if (keyframe1.value.textureSlot != keyframe2.value.textureSlot && keyframe1.value.interpolationType == TextureInterpolationType.LINEAR) {
var interp = i
if (inverseInterp) interp = 1 - i
return getInterpolatedTexture(texture1, texture2, interp)
} else if (keyframe1.value.textureSlot != keyframe2.value.textureSlot && keyframe1.value.interpolationType == TextureInterpolationType.BEZIER) {
var cubicInterp = resolveCubicValues(keyframe1.value.cubicLerpFunctionValues, i)
if (inverseInterp) cubicInterp = 1 - cubicInterp
return getInterpolatedTexture(texture1, texture2, cubicInterp)
}
var newRed : Int
var newGreen : Int
var newBlue : Int
var newAlpha : Int
return NativeImage.read(resourceManager.getResourceOrThrow(textures[keyframe1.value.textureSlot]).open())
//Usually full transparent pixel is black, so this makes it copy the other texture colours to look better, however considering if we should allow to disable this feature
if (color1.w == 0F && color2.w == 1F) {
newRed = floor(color2.x * 255).toInt()
newGreen = floor(color2.y * 255).toInt()
newBlue = floor(color2.z * 255).toInt()
} else if (color1.w == 1F && color2.w == 0F) {
newRed = floor(color1.x * 255).toInt()
newGreen = floor(color1.y * 255).toInt()
newBlue = floor(color1.z * 255).toInt()
} else {
newRed = floor((color1.x + (color2.x - color1.x) * i) * 255).toInt()
newGreen = floor((color1.y + (color2.y - color1.y) * i) * 255).toInt()
newBlue = floor((color1.z + (color2.z - color1.z) * i) * 255).toInt()
}
newAlpha = floor((color1.w + (color2.w - color1.w) * i) * 255).toInt()
val finalColor = (newAlpha shl 24) or (newRed shl 16) or (newGreen shl 8) or newBlue
texture1.setPixelRGBA(x,y,finalColor) //We aren't using texture 1 anymore so it's easier to override it
}
}
val newTexture = DynamicTexture(texture1)
newTexture.setFilter(false, false)
return newTexture
}
}
class VariableModelTextureSupplier : ModelTextureSupplier {
override fun invoke(state: PosableState): RenderStateShard.EmptyTextureStateShard {
return RenderStateShard.TextureStateShard(getTexture(state), false, false)
}
override fun getTexture(state: PosableState): ResourceLocation {
override fun invoke(state: PosableState): ResourceLocation {
return state.runtime.environment.variable.map["texture"]?.asString()?.asIdentifierDefaultingNamespace()
?: cobblemonResource("textures/npcs/default.png")
}
@@ -433,75 +295,4 @@ class ModelLayer {
enum class SpriteType {
PORTRAIT,
PROFILE
}
private fun getInterpolatedTexture(texture1: NativeImage, texture2: NativeImage?, i: Double): NativeImage {
for (x in 0..<texture1.width) {
for (y in 0..<texture1.height) {
val color1 = texture1.getPixelRGBA(x,y).toRGBA()
val color2 = texture2?.getPixelRGBA(x,y)?.toRGBA() ?: Vector4f(0F,0F,0F,0F)
var newRed : Int
var newGreen : Int
var newBlue : Int
//Usually full transparent pixel is black, so this makes it copy the other texture colours to look better, however considering if we should allow to disable this feature
if (color1.w == 0F && color2.w > 0F) {
newRed = floor(color2.x * 255).toInt()
newGreen = floor(color2.y * 255).toInt()
newBlue = floor(color2.z * 255).toInt()
} else if (color1.w > 0F && color2.w == 0F) {
newRed = floor(color1.x * 255).toInt()
newGreen = floor(color1.y * 255).toInt()
newBlue = floor(color1.z * 255).toInt()
} else {
newRed = floor((color1.x + (color2.x - color1.x) * i) * 255).toInt()
newGreen = floor((color1.y + (color2.y - color1.y) * i) * 255).toInt()
newBlue = floor((color1.z + (color2.z - color1.z) * i) * 255).toInt()
}
val newAlpha = floor((color1.w + (color2.w - color1.w) * i) * 255).toInt()
val finalColor = (newAlpha shl 24) or (newRed shl 16) or (newGreen shl 8) or newBlue
texture1.setPixelRGBA(x,y,finalColor) //We aren't using texture 1 anymore so it's easier to override it
}
}
return texture1
}
private fun getEmptyTexture(width: Int, height: Int): NativeImage {
return NativeImage(width, height, false)
}
private fun resolveCubicValues(clampedCubicValues : MutableList<Double>, i: Double): Double {
for (value in 0..<clampedCubicValues.size) {
clampedCubicValues[value] = Math.clamp(clampedCubicValues[value],0.0,1.0)
}
val points = mutableListOf<Vector2d>()
for (o in 0..200) {
val t = o/200.0
val omt = (1.0-t)
val point = Vector2d()
point.x = 3.0 * omt * omt * t * clampedCubicValues[0] + 3.0 * omt * t * t * clampedCubicValues[2] + t * t * t
point.y = 3.0 * omt * omt * t * clampedCubicValues[1] + 3.0 * omt * t * t * clampedCubicValues[3] + t * t * t
points.add(point)
}
var beforePoint = Vector2d(0.0,0.0)
var afterPoint = Vector2d(1.0, 1.0)
for (point in points) {
if (point.x > beforePoint.x && point.x < i) {
beforePoint = point
}
if (point.x < afterPoint.x && point.x > i) {
afterPoint = point
}
}
return beforePoint.y + (afterPoint.y - beforePoint.y) * ((i - beforePoint.x) / (afterPoint.x - beforePoint.x))
}
@@ -125,7 +125,7 @@ class CampfireBlockEntityRenderer(ctx: BlockEntityRendererProvider.Context) : Bl
) {
val seasonings = blockEntity.getSeasonings()
val gameTime = blockEntity.time + tickDelta
val gameTime = (blockEntity.level?.gameTime ?: 0) + tickDelta
val rotationAngle = (gameTime * ROTATION_SPEED) % 360
seasonings.forEachIndexed { index, seasoning ->
@@ -16,7 +16,6 @@ import com.cobblemon.mod.common.client.ClientMoLangFunctions.setupClient
import com.cobblemon.mod.common.client.render.models.blockbench.animation.ActiveAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.animation.PoseAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.animation.SingleBoneLookAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.TextureInterpolationType
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
import com.cobblemon.mod.common.client.render.models.blockbench.quirk.SimpleQuirk
@@ -178,38 +177,4 @@ class JsonPose(model: PosableModel, json: JsonObject) {
val transitions = json.get("transitions")?.takeIf { it is JsonObject }?.asJsonObject?.entrySet()?.map { (key, value) ->
JsonPoseTransition(key, value.asString.asExpressionLike())
} ?: emptyList()
val texturePhaseIn = json.get("texturePhaseIn")?.takeIf { it is JsonObject }?.asJsonObject?.let { theObject ->
val interpolationType = when(theObject.get("interpolation")?.asString) {
"step" -> TextureInterpolationType.STEP
"linear" -> TextureInterpolationType.LINEAR
"bezier" -> TextureInterpolationType.BEZIER
else -> TextureInterpolationType.STEP
}
val cubicValues = (theObject.get("cubicValues")?.asString ?: "0.0, 0.0, 1.0, 1.0").let { values ->
val list = mutableListOf<Double>()
values.split(",").forEach {
list.add(it.toDouble())
}
list
}
Pair(interpolationType, cubicValues)
} ?: Pair(TextureInterpolationType.STEP, mutableListOf(0.0, 0.0, 1.0, 1.0))
val texturePhaseOut = json.get("texturePhaseOut")?.takeIf { it is JsonObject }?.asJsonObject?.let { theObject ->
val interpolationType = when(theObject.get("interpolation")?.asString) {
"step" -> TextureInterpolationType.STEP
"linear" -> TextureInterpolationType.LINEAR
"bezier" -> TextureInterpolationType.BEZIER
else -> TextureInterpolationType.STEP
}
val cubicValues = (theObject.get("cubicValues")?.asString ?: "0.0, 0.0, 1.0, 1.0").let { values ->
val list = mutableListOf<Double>()
values.split(",").forEach {
list.add(it.toDouble())
}
list
}
Pair(interpolationType, cubicValues)
}
}
@@ -19,13 +19,11 @@ import com.cobblemon.mod.common.client.ClientMoLangFunctions.animationFunctions
import com.cobblemon.mod.common.client.ClientMoLangFunctions.setupClient
import com.cobblemon.mod.common.client.entity.PokemonClientDelegate
import com.cobblemon.mod.common.client.render.AnimatedModelTextureSupplier
import com.cobblemon.mod.common.client.render.KeyframableTextureSupplier
import com.cobblemon.mod.common.client.render.ModelLayer
import com.cobblemon.mod.common.client.render.models.blockbench.animation.*
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockActiveAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockAnimationRepository
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockPoseAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.TextureInterpolationType
import com.cobblemon.mod.common.client.render.models.blockbench.frame.ModelFrame
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.CryProvider
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Bone
@@ -278,9 +276,7 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
onTransitionedInto: (PosableState) -> Unit = {},
animations: Array<PoseAnimation> = emptyArray(),
transformedParts: Array<ModelPartTransformation> = emptyArray(),
quirks: Array<ModelQuirk<*>> = emptyArray(),
texturePhaseIn: Pair<TextureInterpolationType, MutableList<Double>> = Pair(TextureInterpolationType.STEP, mutableListOf(0.0, 0.0, 1.0, 1.0)),
texturePhaseOut: Pair<TextureInterpolationType, MutableList<Double>>? = null
quirks: Array<ModelQuirk<*>> = emptyArray()
): Pose {
return Pose(
poseType.name,
@@ -292,9 +288,7 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
namedAnimations,
animations,
transformedParts,
quirks,
texturePhaseIn,
texturePhaseOut
quirks
).also {
poses[poseType.name] = it
}
@@ -310,9 +304,7 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
onTransitionedInto: (PosableState) -> Unit = {},
animations: Array<PoseAnimation> = emptyArray(),
transformedParts: Array<ModelPartTransformation> = emptyArray(),
quirks: Array<ModelQuirk<*>> = emptyArray(),
texturePhaseIn: Pair<TextureInterpolationType, MutableList<Double>> = Pair(TextureInterpolationType.STEP, mutableListOf(0.0, 0.0, 1.0, 1.0)),
texturePhaseOut: Pair<TextureInterpolationType, MutableList<Double>>? = null
quirks: Array<ModelQuirk<*>> = emptyArray()
): Pose {
return Pose(
poseName,
@@ -324,9 +316,7 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
namedAnimations,
animations,
transformedParts,
quirks,
texturePhaseIn,
texturePhaseOut
quirks
).also {
poses[poseName] = it
}
@@ -342,9 +332,7 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
onTransitionedInto: (PosableState) -> Unit = {},
animations: Array<PoseAnimation> = emptyArray(),
transformedParts: Array<ModelPartTransformation> = emptyArray(),
quirks: Array<ModelQuirk<*>> = emptyArray(),
texturePhaseIn: Pair<TextureInterpolationType, MutableList<Double>> = Pair(TextureInterpolationType.STEP, mutableListOf(0.0, 0.0, 1.0, 1.0)),
texturePhaseOut: Pair<TextureInterpolationType, MutableList<Double>>? = null
quirks: Array<ModelQuirk<*>> = emptyArray()
): Pose {
return Pose(
poseName,
@@ -356,9 +344,7 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
namedAnimations,
animations,
transformedParts,
quirks,
texturePhaseIn,
texturePhaseOut
quirks
).also {
poses[poseName] = it
}
@@ -426,6 +412,13 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
fun registerRelevantPart(pairing: Pair<String, ModelPart>) = registerRelevantPart(pairing.first, pairing.second)
/** Needed a custom one, so I can make it take in a dynamic texture instead of resource location */
class DynamicStateShard(texture: DynamicTexture) : RenderStateShard.EmptyTextureStateShard(Runnable {
RenderSystem.setShaderTexture(0, texture.id)
}, Runnable {
texture.close() // Cleanup
})
/** Renders the model. Assumes rotations have been set. Will simply render the base model and then any extra layers. */
fun render(
context: RenderContext,
@@ -460,9 +453,16 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
val provider = bufferProvider
if (provider != null) {
for (layer in currentLayers) {
(layer.texture as? KeyframableTextureSupplier)?.layerName = layer.name
val texture = layer.texture?.invoke(currentState ?: FloatingState()) ?: continue
val renderLayer = makeLayer(texture, layer.emissive, layer.translucent, layer.translucent_cull)
var renderLayer : RenderType
if (layer.texture is AnimatedModelTextureSupplier && layer.texture.interpolation) {
//Handle Interpolation
val texture = layer.texture.interpolatedTexture(currentState ?: FloatingState()) ?: continue
renderLayer = makeLayer(DynamicStateShard(texture), layer.emissive, layer.translucent, layer.translucent_cull)
}
else {
val texture = layer.texture?.invoke(currentState ?: FloatingState()) ?: continue
renderLayer = getLayer(texture, layer.emissive, layer.translucent, layer.translucent_cull)
}
val consumer = provider.getBuffer(renderLayer)
val tint = layer.tint
val tintRed = (tint.x * r2 * 255).toInt()
@@ -90,9 +90,7 @@ class PoseAdapter(
animations = pose.idleAnimations,
transformedParts = pose.transformedParts,
quirks = pose.quirks.toTypedArray(),
namedAnimations = pose.animations,
texturePhaseIn = pose.texturePhaseIn,
texturePhaseOut = pose.texturePhaseOut
namedAnimations = pose.animations
).also {
it.transitions.putAll(
pose.transitions
@@ -132,9 +132,7 @@ data class BedrockAnimation(
val shouldLoop: Boolean,
val animationLength: Double,
val effects: List<BedrockEffectKeyframe>,
val boneTimelines: Map<String, BedrockBoneTimeline>,
//This is custom data added by our custom BB plugin
val cobblemonTextures: MutableMap<String, TreeMap<Double, CobblemonBedrockTextureKeyframe>>
val boneTimelines: Map<String, BedrockBoneTimeline>
) {
fun checkForErrors() {
boneTimelines.forEach { (_, timeline) ->
@@ -398,14 +396,4 @@ enum class InterpolationType {
enum class Transformation {
POSITION, ROTATION, SCALE
}
data class CobblemonBedrockTextureKeyframe(
val textureSlot: Int,
val cubicLerpFunctionValues: MutableList<Double>,
val interpolationType: TextureInterpolationType
)
enum class TextureInterpolationType {
STEP, LINEAR, BEZIER
}
@@ -20,7 +20,6 @@ import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import java.lang.reflect.Type
import java.util.TreeMap
/**
* Gson adapter for converting bedrock/blockbench json data into a friendlier object model.
@@ -90,27 +89,7 @@ object BedrockAnimationAdapter : JsonDeserializer<BedrockAnimation> {
)
}
//Custom Data reading added by our custom BB plugin
val cobblemonTextures = mutableMapOf<String, TreeMap<Double, CobblemonBedrockTextureKeyframe>>()
json["cobblemon_texture"]?.asJsonObject?.entrySet()?.forEach { (texture, timeline) ->
val textureTimeline = TreeMap<Double, CobblemonBedrockTextureKeyframe>()
timeline.asJsonObject.entrySet().forEach { (frame, values) ->
val lerpFunctionValues = mutableListOf<Double>()
values.asJsonObject["cobblemon_cubic_lerp_function"].asString.split(",").forEach {
lerpFunctionValues.add(it.toDouble())
}
val interpolation = when(values.asJsonObject["cobblemon_interpolation"].asString) {
"step" -> TextureInterpolationType.STEP
"linear" -> TextureInterpolationType.LINEAR
"bezier" -> TextureInterpolationType.BEZIER
else -> TextureInterpolationType.STEP
}
textureTimeline[frame.toDouble()] = CobblemonBedrockTextureKeyframe(values.asJsonObject["cobblemon_texture_slot"].asInt, lerpFunctionValues, interpolation)
}
cobblemonTextures[texture] = textureTimeline
}
return BedrockAnimation(shouldLoop, animationLength, effects, boneTimelines, cobblemonTextures)
return BedrockAnimation(shouldLoop, animationLength, effects, boneTimelines)
}
else {
throw IllegalStateException("animation json could not be parsed")
@@ -0,0 +1,67 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render.models.blockbench.pokemon.gen5
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BimanualFrame
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BipedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.CryProvider
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPosableModel
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
import com.cobblemon.mod.common.entity.PoseType
import net.minecraft.client.model.geom.ModelPart
import net.minecraft.world.phys.Vec3
class BeheeyemModel (root: ModelPart) : PokemonPosableModel(root), HeadedFrame, BipedFrame, BimanualFrame {
override val rootPart = root.registerChildWithAllChildren("beheeyem")
override val head = getPart("head")
override val rightArm = getPart("arm_right")
override val leftArm = getPart("arm_left")
override val rightLeg = getPart("leg_right")
override val leftLeg = getPart("leg_left")
override var portraitScale = 3.0F
override var portraitTranslation = Vec3(-0.34, -0.8, 0.0)
override var profileScale = 0.8F
override var profileTranslation = Vec3(0.0, 0.6, 0.0)
lateinit var sleep: Pose
lateinit var standing: Pose
lateinit var walk: Pose
override val cryAnimation = CryProvider { bedrockStateful("beheeyem", "cry") }
override fun registerPoses() {
sleep = registerPose(
poseType = PoseType.SLEEP,
animations = arrayOf(bedrock("beheeyem", "sleep"))
)
standing = registerPose(
poseName = "standing",
poseTypes = PoseType.STATIONARY_POSES + PoseType.UI_POSES,
transformTicks = 10,
animations = arrayOf(
singleBoneLook(),
bedrock("beheeyem", "ground_idle")
)
)
walk = registerPose(
poseName = "walk",
poseTypes = PoseType.MOVING_POSES,
transformTicks = 5,
animations = arrayOf(
singleBoneLook(),
bedrock("beheeyem", "ground_walk")
)
)
}
}
@@ -0,0 +1,94 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render.models.blockbench.pokemon.gen5
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.CryProvider
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPosableModel
import com.cobblemon.mod.common.client.render.models.blockbench.pose.CobblemonPose
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.util.isBattling
import net.minecraft.client.model.geom.ModelPart
import net.minecraft.world.phys.Vec3
class ChandelureModel (root: ModelPart) : PokemonPosableModel(root), HeadedFrame {
override val rootPart = root.registerChildWithAllChildren("chandelure")
override val head = getPart("head")
override var portraitScale = 1.85F
override var portraitTranslation = Vec3(-0.3, -0.19, 0.0)
override var profileScale = 0.81F
override var profileTranslation = Vec3(0.0, 0.68, 0.0)
lateinit var standing: CobblemonPose
lateinit var walk: CobblemonPose
lateinit var hover: CobblemonPose
lateinit var flying: CobblemonPose
lateinit var battle_idle: CobblemonPose
lateinit var sleep: CobblemonPose
override val cryAnimation = CryProvider { bedrockStateful("chandelure", "cry") }
override fun registerPoses() {
val blink = quirk { bedrockStateful("chandelure", "blink") }
standing = registerPose(
poseName = "standing",
poseTypes = PoseType.STATIONARY_POSES + PoseType.UI_POSES - PoseType.HOVER,
quirks = arrayOf(blink),
condition = { !it.isBattling },
animations = arrayOf(
bedrock("chandelure", "ground_idle")
)
)
walk = registerPose(
poseName = "walk",
poseTypes = PoseType.MOVING_POSES - PoseType.FLY,
quirks = arrayOf(blink),
animations = arrayOf(
bedrock("chandelure", "ground_walk")
)
)
hover = registerPose(
poseName = "hover",
poseType = PoseType.HOVER,
quirks = arrayOf(blink),
animations = arrayOf(
bedrock("chandelure", "air_idle")
)
)
flying = registerPose(
poseName = "flying",
poseType = PoseType.FLY,
quirks = arrayOf(blink),
animations = arrayOf(
bedrock("chandelure", "air_fly")
)
)
battle_idle = registerPose(
poseName = "battle_idle",
poseTypes = PoseType.STATIONARY_POSES,
quirks = arrayOf(blink),
condition = { it.isBattling },
animations = arrayOf(
bedrock("chandelure", "battle_idle")
)
)
}
// override fun getFaintAnimation(
// pokemonEntity: PokemonEntity,
// state: PosableState<PokemonEntity>
// ) = if (state.isPosedIn(standing, walk)) bedrockStateful("chandelure", "faint") else null
}
@@ -0,0 +1,113 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render.models.blockbench.pokemon.gen5
import com.cobblemon.mod.common.client.render.models.blockbench.PosableState
import com.cobblemon.mod.common.client.render.models.blockbench.createTransformation
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BimanualFrame
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BipedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.CryProvider
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPosableModel
import com.cobblemon.mod.common.client.render.models.blockbench.pose.ModelPartTransformation
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.util.isBattling
import net.minecraft.client.model.geom.ModelPart
import net.minecraft.world.phys.Vec3
class ElgyemModel (root: ModelPart) : PokemonPosableModel(root), HeadedFrame, BipedFrame, BimanualFrame {
override val rootPart = root.registerChildWithAllChildren("elgyem")
override val head = getPart("head")
override val rightArm = getPart("arm_right")
override val leftArm = getPart("arm_left")
override val rightLeg = getPart("leg_right")
override val leftLeg = getPart("leg_left")
override var portraitScale = 2.5F
override var portraitTranslation = Vec3(-0.2, -0.8, 0.0)
override var profileScale = 0.85F
override var profileTranslation = Vec3(0.0, 0.5, 0.0)
lateinit var sleep: Pose
lateinit var standing: Pose
lateinit var walk: Pose
lateinit var battleidle: Pose
lateinit var shoulderLeft: Pose
lateinit var shoulderRight: Pose
override val cryAnimation = CryProvider { bedrockStateful("elgyem", "cry") }
val shoulderOffset = 5.6
override fun registerPoses() {
sleep = registerPose(
poseType = PoseType.SLEEP,
animations = arrayOf(bedrock("elgyem", "sleep"))
)
standing = registerPose(
poseName = "standing",
poseTypes = PoseType.STATIONARY_POSES + PoseType.UI_POSES,
transformTicks = 10,
condition = { !it.isBattling },
animations = arrayOf(
singleBoneLook(),
bedrock("elgyem", "ground_idle")
)
)
walk = registerPose(
poseName = "walk",
poseTypes = PoseType.MOVING_POSES,
transformTicks = 5,
animations = arrayOf(
singleBoneLook(),
bedrock("elgyem", "ground_walk")
)
)
battleidle = registerPose(
poseName = "battle_idle",
poseTypes = PoseType.STATIONARY_POSES,
transformTicks = 10,
condition = { it.isBattling },
animations = arrayOf(
singleBoneLook(),
bedrock("elgyem", "battle_idle")
)
)
shoulderLeft = registerPose(
poseType = PoseType.SHOULDER_LEFT,
//quirks = arrayOf(blink),
animations = arrayOf(
singleBoneLook(),
bedrock("elgyem", "ground_idle")
),
transformedParts = arrayOf(
rootPart.createTransformation().addPosition(ModelPartTransformation.X_AXIS, shoulderOffset)
)
)
shoulderRight = registerPose(
poseType = PoseType.SHOULDER_RIGHT,
//quirks = arrayOf(blink),
animations = arrayOf(
singleBoneLook(),
bedrock("elgyem", "ground_idle")
),
transformedParts = arrayOf(
rootPart.createTransformation().addPosition(ModelPartTransformation.X_AXIS, -shoulderOffset)
)
)
}
override fun getFaintAnimation(state: PosableState) = if (state.isPosedIn(standing, walk, battleidle, sleep)) bedrockStateful("elgyem", "faint") else null
}
@@ -0,0 +1,102 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render.models.blockbench.pokemon.gen5
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPosableModel
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.CryProvider
import com.cobblemon.mod.common.client.render.models.blockbench.pose.CobblemonPose
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.util.isBattling
import net.minecraft.client.model.geom.ModelPart
import net.minecraft.world.phys.Vec3
class LampentModel (root: ModelPart) : PokemonPosableModel(root), HeadedFrame {
override val rootPart = root.registerChildWithAllChildren("lampent")
override val head = getPart("head")
override var portraitScale = 2.2F
override var portraitTranslation = Vec3(-0.19, -0.17, 0.0)
override var profileScale = 0.8F
override var profileTranslation = Vec3(0.0, 0.7, 0.0)
lateinit var standing: CobblemonPose
lateinit var walk: CobblemonPose
lateinit var hover: CobblemonPose
lateinit var flying: CobblemonPose
lateinit var battle_idle: CobblemonPose
lateinit var sleep: CobblemonPose
override val cryAnimation = CryProvider { bedrockStateful("lampent", "cry") }
override fun registerPoses() {
val blink = quirk { bedrockStateful("lampent", "blink") }
sleep = registerPose(
poseName = "sleep",
poseType = PoseType.SLEEP,
animations = arrayOf(
bedrock("lampent", "sleep")
)
)
standing = registerPose(
poseName = "standing",
poseTypes = PoseType.STATIONARY_POSES + PoseType.UI_POSES - PoseType.HOVER,
quirks = arrayOf(blink),
condition = { !it.isBattling },
animations = arrayOf(
bedrock("lampent", "ground_idle")
)
)
walk = registerPose(
poseName = "walk",
poseTypes = PoseType.MOVING_POSES - PoseType.FLY,
quirks = arrayOf(blink),
animations = arrayOf(
bedrock("lampent", "ground_walk")
)
)
hover = registerPose(
poseName = "hover",
poseType = PoseType.HOVER,
quirks = arrayOf(blink),
animations = arrayOf(
bedrock("lampent", "air_idle")
)
)
flying = registerPose(
poseName = "flying",
poseType = PoseType.FLY,
quirks = arrayOf(blink),
animations = arrayOf(
bedrock("lampent", "air_fly")
)
)
battle_idle = registerPose(
poseName = "battle_idle",
poseTypes = PoseType.STATIONARY_POSES,
quirks = arrayOf(blink),
condition = { it.isBattling },
animations = arrayOf(
bedrock("lampent", "battle_idle")
)
)
}
// override fun getFaintAnimation(
// pokemonEntity: PokemonEntity,
// state: PosableState<PokemonEntity>
// ) = if (state.isPosedIn(standing, walk)) bedrockStateful("lampent", "faint") else null
}
@@ -0,0 +1,110 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render.models.blockbench.pokemon.gen5
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPosableModel
import com.cobblemon.mod.common.client.render.models.blockbench.createTransformation
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.CryProvider
import com.cobblemon.mod.common.client.render.models.blockbench.pose.CobblemonPose
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.util.isBattling
import net.minecraft.client.model.geom.ModelPart
import net.minecraft.world.phys.Vec3
class LitwickModel (root: ModelPart) : PokemonPosableModel(root), HeadedFrame {
override val rootPart = root.registerChildWithAllChildren("litwick")
override val head = getPart("head")
override var portraitScale = 2.33F
override var portraitTranslation = Vec3(0.01, -1.9, 0.0)
override var profileScale = 0.86F
override var profileTranslation = Vec3(0.0, 0.44, 0.0)
lateinit var standing: CobblemonPose
lateinit var walk: CobblemonPose
lateinit var sleep: CobblemonPose
lateinit var battle_idle: CobblemonPose
lateinit var shoulderLeft: CobblemonPose
lateinit var shoulderRight: CobblemonPose
override val cryAnimation = CryProvider { bedrockStateful("litwick", "cry") }
val shoulderOffset = 5.5
val wax = getPart("wax_trail")
override fun registerPoses() {
val blink = quirk { bedrockStateful("litwick", "blink") }
sleep = registerPose(
poseName = "sleep",
poseType = PoseType.SLEEP,
animations = arrayOf(
bedrock("litwick", "sleep")
)
)
standing = registerPose(
poseName = "standing",
poseTypes = PoseType.STATIONARY_POSES + PoseType.UI_POSES,
condition = { !it.isBattling },
quirks = arrayOf(blink),
animations = arrayOf(
bedrock("litwick", "ground_idle")
)
)
walk = registerPose(
poseName = "walk",
poseTypes = PoseType.MOVING_POSES,
quirks = arrayOf(blink),
animations = arrayOf(
bedrock("litwick", "ground_walk")
)
)
battle_idle = registerPose(
poseName = "battle_idle",
poseTypes = PoseType.STATIONARY_POSES,
quirks = arrayOf(blink),
condition = { it.isBattling },
animations = arrayOf(
bedrock("litwick", "battle_idle")
)
)
shoulderLeft = registerPose(
poseType = PoseType.SHOULDER_LEFT,
quirks = arrayOf(blink),
animations = arrayOf(
bedrock("litwick", "ground_idle")
),
transformedParts = arrayOf(
wax.createTransformation().withVisibility(visibility = false)
)
)
shoulderRight = registerPose(
poseType = PoseType.SHOULDER_RIGHT,
quirks = arrayOf(blink),
animations = arrayOf(
bedrock("litwick", "ground_idle")
),
transformedParts = arrayOf(
wax.createTransformation().withVisibility(visibility = false)
)
)
}
// override fun getFaintAnimation(
// pokemonEntity: PokemonEntity,
// state: PosableState<PokemonEntity>
// ) = if (state.isPosedIn(standing, walk)) bedrockStateful("litwick", "faint") else null
}
@@ -13,7 +13,6 @@ import com.cobblemon.mod.common.client.render.models.blockbench.PosableModel
import com.cobblemon.mod.common.client.render.models.blockbench.PosableState
import com.cobblemon.mod.common.client.render.models.blockbench.animation.ActiveAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.animation.PoseAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.TextureInterpolationType
import com.cobblemon.mod.common.client.render.models.blockbench.quirk.ModelQuirk
import com.cobblemon.mod.common.client.render.models.blockbench.repository.RenderContext
import com.cobblemon.mod.common.entity.PoseType
@@ -40,10 +39,7 @@ class Pose(
val namedAnimations: MutableMap<String, ExpressionLike> = mutableMapOf(),
val animations: Array<PoseAnimation>,
val transformedParts: Array<ModelPartTransformation>,
val quirks: Array<ModelQuirk<*>>,
/** These are very specific to the animated texture**/
val texturePhaseIn: Pair<TextureInterpolationType, MutableList<Double>>,
val texturePhaseOut: Pair<TextureInterpolationType, MutableList<Double>>?,
val quirks: Array<ModelQuirk<*>>
) {
fun isSuitable(state: PosableState) = condition?.invoke(state) ?: true
@@ -361,6 +361,8 @@ object VaryingModelRepository {
inbuilt("chespin", :: ChespinModel)
inbuilt("quilladin", :: QuilladinModel)
inbuilt("chesnaught", :: ChesnaughtModel)
inbuilt("elgyem", :: ElgyemModel)
inbuilt("beheeyem", :: BeheeyemModel)
inbuilt("pineco", :: PinecoModel)
inbuilt("forretress", :: ForretressModel)
inbuilt("lotad", :: LotadModel)
@@ -552,6 +554,9 @@ object VaryingModelRepository {
inbuilt("gurdurr", ::GurdurrModel)
inbuilt("conkeldurr", ::ConkeldurrModel)
inbuilt("clodsire", ::ClodsireModel)
inbuilt("litwick", ::LitwickModel)
inbuilt("lampent", ::LampentModel)
inbuilt("chandelure", ::ChandelureModel)
inbuilt("gimmighoulchest", ::GimmighoulChestModel)
inbuilt("drifloon", ::DrifloonModel)
inbuilt("lileep", ::LileepModel)
@@ -23,6 +23,9 @@ object CobblemonTooltipGenerator : TooltipGenerator() {
val resultLines = mutableListOf<Component>()
if (stack.item.builtInRegistryHolder().unwrapKey().isPresent && stack.item.builtInRegistryHolder().unwrapKey().get().location().namespace == Cobblemon.MODID) {
if (stack.get(DataComponents.HIDE_ADDITIONAL_TOOLTIP) != null) {
return null
}
val language = Language.getInstance()
val key = this.baseLangKeyForItem(stack)
if (language.has(key)) {
@@ -31,6 +31,9 @@ object FishingBaitTooltipGenerator : TooltipGenerator() {
private val fishingBaitItemClass by lazy { lang("item_class.fishing_bait").blue() }
override fun generateCategoryTooltip(stack: ItemStack, lines: MutableList<Component>): MutableList<Component>? {
if (stack.get(DataComponents.HIDE_ADDITIONAL_TOOLTIP) != null) {
return null
}
if (!SpawnBaitEffects.isFishingBait(stack)) {
return null
}
@@ -38,6 +41,9 @@ object FishingBaitTooltipGenerator : TooltipGenerator() {
}
override fun generateAdditionalTooltip(stack: ItemStack, lines: MutableList<Component>): MutableList<Component>? {
if (stack.get(DataComponents.HIDE_ADDITIONAL_TOOLTIP) != null) {
return null
}
val resultLines = mutableListOf<Component>()
if (SpawnBaitEffects.isFishingBait(stack)) resultLines.add(this.fishingBaitItemClass)
@@ -23,6 +23,9 @@ import net.minecraft.world.item.ItemStack
object FishingRodTooltipGenerator : TooltipGenerator() {
override fun generateTooltip(stack: ItemStack, lines: MutableList<Component>): MutableList<Component>? {
if (stack.get(DataComponents.HIDE_ADDITIONAL_TOOLTIP) != null) {
return null
}
val resultLines = mutableListOf<Component>()
val rod = (stack.item as? PokerodItem)?.pokeRodId?.let { PokeRods.getPokeRod(it) } ?: return null
@@ -61,6 +61,10 @@ object SeasoningTooltipGenerator : TooltipGenerator() {
}
override fun generateAdditionalTooltip(stack: ItemStack, lines: MutableList<Component>): MutableList<Component>? {
if (stack.get(DataComponents.HIDE_ADDITIONAL_TOOLTIP) != null) {
return null
}
val isSeasoningIngredient = Seasonings.isSeasoning(stack)
val flavors = stack.get(CobblemonItemComponents.FLAVOUR)?.flavours
val food = stack.get(CobblemonItemComponents.FOOD)
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common.client.tooltips
import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.util.lang
import net.minecraft.core.component.DataComponents
import net.minecraft.network.chat.Component
import net.minecraft.world.item.ItemStack
@@ -23,12 +22,6 @@ object TooltipManager {
}
fun generateTooltips(stack: ItemStack, lines: MutableList<Component>, hasShiftDown: Boolean) {
// This will need to be changed in 1.21.5 as 25w04a changed the following:
// "Removed the hide_tooltip and hide_additional_tooltip components in favor of the new tooltip_display component."
if (stack.get(DataComponents.HIDE_ADDITIONAL_TOOLTIP) != null) {
return
}
val standardLines = tooltipGenerators.flatMap {
val innerLines = mutableListOf<Component>()
val regularTooltip = it.generateTooltip(stack, lines)
@@ -8,11 +8,10 @@
package com.cobblemon.mod.common.command
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.Cobblemon.config
import com.cobblemon.mod.common.api.permission.CobblemonPermissions
import com.cobblemon.mod.common.api.spawning.CobblemonWorldSpawnerManager
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.position.calculators.SpawnablePositionCalculator.Companion.prioritizedAreaCalculators
import com.cobblemon.mod.common.api.spawning.spawner.SpawningZoneInput
import com.cobblemon.mod.common.api.text.add
import com.cobblemon.mod.common.api.text.green
@@ -25,7 +24,6 @@ import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.command.argument.SpawnBucketArgumentType
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.permission
import com.cobblemon.mod.common.util.spawner
import com.mojang.brigadier.Command
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.context.CommandContext
@@ -58,11 +56,11 @@ object CheckSpawnsCommand {
return 0
}
val spawner = player.spawner
val spawner = CobblemonWorldSpawnerManager.spawnersForPlayers[player.uuid] ?: return Command.SINGLE_SUCCESS
val bucket = SpawnBucketArgumentType.getSpawnBucket(context, name = "bucket")
val cause = SpawnCause(spawner, player)
val slice = Cobblemon.spawningZoneGenerator.generate(
val slice = spawner.spawningZoneGenerator.generate(
spawner = spawner,
input = SpawningZoneInput(
cause = cause,
@@ -76,9 +74,9 @@ object CheckSpawnsCommand {
)
)
val contexts = Cobblemon.areaSpawnablePositionResolver.resolve(spawner, prioritizedAreaCalculators, slice)
val contexts = spawner.spawnablePositionResolver.resolve(spawner, spawner.spawnablePositionCalculators, slice)
val spawnProbabilities = spawner.selector.getProbabilities(spawner, bucket, contexts)
val spawnProbabilities = spawner.getSpawningSelector().getProbabilities(spawner, bucket, contexts)
val spawnNames = mutableMapOf<String, MutableComponent>()
val namedProbabilities = mutableMapOf<MutableComponent, Float>()
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common.command
import com.cobblemon.mod.common.api.permission.CobblemonPermissions
import com.cobblemon.mod.common.api.spawning.CobblemonWorldSpawnerManager
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.detail.EntitySpawnResult
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePosition
@@ -18,7 +19,6 @@ import com.cobblemon.mod.common.util.alias
import com.cobblemon.mod.common.util.commandLang
import com.cobblemon.mod.common.util.effectiveName
import com.cobblemon.mod.common.util.permission
import com.cobblemon.mod.common.util.spawner
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.arguments.IntegerArgumentType
import com.mojang.brigadier.context.CommandContext
@@ -31,7 +31,7 @@ import net.minecraft.commands.Commands.literal
*
* `/spawnpokemonfrompool [amount]` or the alias `/forcespawn [amount]`
*
* This command can fail if the randomly selected spawn region has no possible [AreaSpawnablePosition]. For example if
* This command can fail if the randomly selection spawn region has no possible [AreaSpawnablePosition]. For example if
* you are flying in the air
*/
object SpawnPokemonFromPool {
@@ -54,26 +54,47 @@ object SpawnPokemonFromPool {
private fun execute(context: CommandContext<CommandSourceStack>, amount: Int): Int {
val player = context.source.playerOrException
val spawner = player.spawner
val spawner = CobblemonWorldSpawnerManager.spawnersForPlayers.getValue(player.uuid)
var spawnsTriggered = 0
repeat(times = amount) {
val spawnCause = SpawnCause(spawner = spawner, entity = player)
val results = spawner.runForArea(spawner.getZoneInput(spawnCause) ?: return@repeat, maxSpawns = 1)
if (results.isEmpty()) {
// This could instead directly use a [Spawner] method if refactored, as it is currently it has
// entity counting coupled to the generation of an entity to spawn. Might be a good future change?
for (i in 1..amount) {
val spawnCause = SpawnCause(spawner = spawner, entity = spawner.getCauseEntity())
val zoneInput = spawner.getZoneInput(spawnCause) ?: continue
val zone = spawner.spawningZoneGenerator.generate(spawner, zoneInput)
val contexts = spawner.spawnablePositionResolver.resolve(spawner, spawner.spawnablePositionCalculators, zone)
val influences = spawner.getAllInfluences() + zone.unconditionalInfluences
// This has a chance to fail, if you get a spawning zone that has no associated contexts.
// but as it was selected at random by the Spawning Zone Generator, it could just be a miss which
// means two attempts to spawn in the same location can have differing results (which is expected for
// randomness).
if (contexts.isEmpty()) {
player.sendSystemMessage(UNABLE_TO_SPAWN.red())
return@repeat
continue
}
results.forEach {
val bucket = spawner.chooseBucket(spawnCause, influences)
val spawnAction = spawner.getSpawningSelector().select(spawner, bucket, contexts, max = 1).firstOrNull() // one at a time
if (spawnAction == null) {
player.sendSystemMessage(UNABLE_TO_SPAWN.red())
continue
}
spawnAction.future.thenApply {
if (it is EntitySpawnResult) {
for (entity in it.entities) {
spawnsTriggered++
player.sendSystemMessage(commandLang("spawnpokemonfrompool", entity.effectiveName()).green())
player.sendSystemMessage(commandLang("spawnpokemonfrompool.success", entity.effectiveName()).green())
}
}
}
spawnAction.complete()
spawnsTriggered++
}
return spawnsTriggered
@@ -12,8 +12,6 @@ import com.cobblemon.mod.common.api.drop.ItemDropMethod
import com.cobblemon.mod.common.api.pokeball.catching.calculators.CaptureCalculator
import com.cobblemon.mod.common.api.pokemon.status.Statuses
import com.cobblemon.mod.common.client.gui.portrait.PortraitStyle
import com.cobblemon.mod.common.config.CobblemonConfigField.CobblemonConfigSide.CLIENT
import com.cobblemon.mod.common.config.CobblemonConfigField.CobblemonConfigSide.SERVER
import com.cobblemon.mod.common.config.constraint.IntConstraint
import com.cobblemon.mod.common.pokeball.catching.calculators.CobblemonCaptureCalculator
import com.cobblemon.mod.common.util.adapters.CaptureCalculatorAdapter
@@ -21,9 +19,9 @@ import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
import com.cobblemon.mod.common.util.adapters.IntRangeAdapter
import com.google.gson.GsonBuilder
import com.google.gson.annotations.SerializedName
import net.minecraft.resources.ResourceLocation
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
import net.minecraft.resources.ResourceLocation
class CobblemonConfig {
companion object {
@@ -39,126 +37,123 @@ class CobblemonConfig {
var lastSavedVersion: String = "0.0.1"
@CobblemonConfigField(Category.Pokemon, lang = "max_pokemon_level", SERVER)
@CobblemonConfigField(Category.Pokemon, lang = "max_pokemon_level")
@IntConstraint(min = 1, max = 1000)
var maxPokemonLevel = 100
@CobblemonConfigField(Category.Pokemon, lang = "max_pokemon_friendship", SERVER)
@CobblemonConfigField(Category.Pokemon, lang = "max_pokemon_friendship")
@IntConstraint(min = 0, max = 1000)
var maxPokemonFriendship = 255
@CobblemonConfigField(Category.Pokemon, lang = "announce_drop_items", SERVER)
@CobblemonConfigField(Category.Pokemon, lang = "announce_drop_items")
var announceDropItems = true
@CobblemonConfigField(Category.Pokemon, lang = "default_drop_item_method", SERVER)
@CobblemonConfigField(Category.Pokemon, lang = "default_drop_item_method")
var defaultDropItemMethod = ItemDropMethod.ON_ENTITY
@CobblemonConfigField(Category.Pokemon, lang = "drops_after_death_animation", SERVER)
@CobblemonConfigField(Category.Pokemon, lang = "drops_after_death_animation")
var dropAfterDeathAnimation = false
@CobblemonConfigField(Category.Pokemon, lang = "ambient_pokemon_cry_ticks", SERVER)
@CobblemonConfigField(Category.Pokemon, lang = "ambient_pokemon_cry_ticks")
@LastChangedVersion("1.4.0")
var ambientPokemonCryTicks = 1080
@CobblemonConfigField(Category.Storage, lang = "default_box_count", SERVER)
@CobblemonConfigField(Category.Storage, lang = "default_box_count")
@IntConstraint(min = 1, max = 1000)
@LastChangedVersion("1.7.0")
var defaultBoxCount = 40
@CobblemonConfigField(Category.Storage, lang = "pokemon_save_interval_seconds", SERVER)
@CobblemonConfigField(Category.Storage, lang = "pokemon_save_interval_seconds")
@IntConstraint(min = 1, max = 120)
var pokemonSaveIntervalSeconds = 30
@CobblemonConfigField(Category.Storage, lang = "storage_format", SERVER)
@CobblemonConfigField(Category.Storage, lang = "storage_format")
var storageFormat = "nbt"
@CobblemonConfigField(Category.Storage, lang = "prevent_complete_party_deposit", SERVER)
@CobblemonConfigField(Category.Storage, lang = "prevent_complete_party_deposit")
var preventCompletePartyDeposit = false
@CobblemonConfigField(Category.Storage, lang = "mongo_db_connection_string", SERVER)
@CobblemonConfigField(Category.Storage, lang = "mongo_db_connection_string")
var mongoDBConnectionString = "mongodb://localhost:27017"
@CobblemonConfigField(Category.Storage, lang = "mongo_db_database_name", SERVER)
@CobblemonConfigField(Category.Storage, lang = "mongo_db_database_name")
var mongoDBDatabaseName = "cobblemon"
@CobblemonConfigField(Category.Spawning, lang = "max_vertical_correction_blocks", SERVER)
@CobblemonConfigField(Category.Spawning, lang = "max_vertical_correction_blocks")
@IntConstraint(min = 1, max = 200)
var maxVerticalCorrectionBlocks = 64
@CobblemonConfigField(Category.Spawning, lang = "minimum_level_range_max", SERVER)
@CobblemonConfigField(Category.Spawning, lang = "minimum_level_range_max")
@IntConstraint(min = 1, max = 1000)
var minimumLevelRangeMax = 10
@CobblemonConfigField(Category.Spawning, lang = "enable_spawning", SERVER)
@CobblemonConfigField(Category.Spawning, lang = "enable_spawning")
var enableSpawning = true
@CobblemonConfigField(Category.Spawning, lang = "minimum_distance_between_entities", SERVER)
@CobblemonConfigField(Category.Spawning, lang = "minimum_distance_between_entities")
var minimumDistanceBetweenEntities = 8.0
@CobblemonConfigField(Category.Spawning, lang = "max_nearby_blocks_horizontal_range", SERVER)
@CobblemonConfigField(Category.Spawning, lang = "max_nearby_blocks_horizontal_range")
var maxNearbyBlocksHorizontalRange = 4
@CobblemonConfigField(Category.Spawning, lang = "max_nearby_blocks_vertical_range", SERVER)
@CobblemonConfigField(Category.Spawning, lang = "max_nearby_blocks_vertical_range")
var maxNearbyBlocksVerticalRange = 2
@CobblemonConfigField(Category.Spawning, lang = "max_vertical_space", SERVER)
@CobblemonConfigField(Category.Spawning, lang = "max_vertical_space")
var maxVerticalSpace = 8
@CobblemonConfigField(Category.Spawning, lang = "spawning_zone_diameter", SERVER)
@CobblemonConfigField(Category.Spawning, lang = "spawning_zone_diameter")
@SerializedName("spawningZoneDiameter", alternate = ["worldSliceDiameter"])
var spawningZoneDiameter = 8
@CobblemonConfigField(Category.Spawning, lang = "spawning_zone_height", SERVER)
@CobblemonConfigField(Category.Spawning, lang = "spawning_zone_height")
@SerializedName("spawningZoneHeight", alternate = ["worldSliceHeight"])
var spawningZoneHeight = 16
@CobblemonConfigField(Category.Spawning, lang = "ticks_between_spawn_attempts", SERVER)
@CobblemonConfigField(Category.Spawning, lang = "ticks_between_spawn_attempts")
var ticksBetweenSpawnAttempts = 20F
@CobblemonConfigField(Category.Spawning, lang = "minimum_spawning_zone_distance_from_player", SERVER)
@CobblemonConfigField(Category.Spawning, lang = "minimum_spawning_zone_distance_from_player")
@SerializedName("minimumSpawningZoneDistanceFromPlayer", alternate = ["minimumSliceDistanceFromPlayer"])
var minimumSpawningZoneDistanceFromPlayer = 16F
@CobblemonConfigField(Category.Spawning, lang = "maximum_spawning_zone_distance_from_player", SERVER)
@CobblemonConfigField(Category.Spawning, lang = "maximum_spawning_zone_distance_from_player")
@SerializedName("maximumSpawningZoneDistanceFromPlayer", alternate = ["maximumSliceDistanceFromPlayer"])
var maximumSpawningZoneDistanceFromPlayer = 16 * 4F
@CobblemonConfigField(Category.Spawning, lang = "maximum_spawns_per_pass", SERVER)
@CobblemonConfigField(Category.Spawning, lang = "maximum_spawns_per_pass")
var maximumSpawnsPerPass = 8
@CobblemonConfigField(Category.Spawning, lang = "export_spawn_config", SERVER)
@CobblemonConfigField(Category.Spawning, lang = "export_spawn_config")
var exportSpawnConfig = false
@CobblemonConfigField(Category.Spawning, lang = "save_pokemon_to_world", SERVER)
@CobblemonConfigField(Category.Spawning, lang = "save_pokemon_to_world")
var savePokemonToWorld = true
@CobblemonConfigField(Category.Starter, lang = "export_starter_config", SERVER)
@CobblemonConfigField(Category.Starter, lang = "export_starter_config")
var exportStarterConfig = false
@CobblemonConfigField(Category.Battles, lang = "auto_update_showdown", SERVER)
@CobblemonConfigField(Category.Battles, lang = "auto_update_showdown")
var autoUpdateShowdown = true
@CobblemonConfigField(Category.Battles, lang = "default_flee_distance", SERVER)
@CobblemonConfigField(Category.Battles, lang = "default_flee_distance")
var defaultFleeDistance = 16F * 2
@CobblemonConfigField(Category.Battles, lang = "allow_experience_from_pvp", SERVER)
@CobblemonConfigField(Category.Battles, lang = "allow_experience_from_pvp")
var allowExperienceFromPvP = true
@CobblemonConfigField(Category.Battles, lang = "experience_share_multiplier", SERVER)
@CobblemonConfigField(Category.Battles, lang = "experience_share_multiplier")
var experienceShareMultiplier = .5
@CobblemonConfigField(Category.Battles, lang = "lucky_egg_multiplier", SERVER)
@CobblemonConfigField(Category.Battles, lang = "lucky_egg_multiplier")
var luckyEggMultiplier = 1.5
@CobblemonConfigField(Category.Battles, lang = "allow_spectating", SERVER)
@CobblemonConfigField(Category.Battles, lang = "allow_spectating")
var allowSpectating = true
@CobblemonConfigField(Category.Pokemon, lang = "experience_multiplier", SERVER)
@CobblemonConfigField(Category.Pokemon, lang = "experience_multiplier")
var experienceMultiplier = 2F
@CobblemonConfigField(Category.Spawning, lang = "pokemon_per_chunk", SERVER)
@CobblemonConfigField(Category.Spawning, lang = "pokemon_per_chunk")
var pokemonPerChunk = 1F
@CobblemonConfigField(Category.Spawning, lang = "poke_snack_pokemon_per_chunk", SERVER)
var pokeSnackPokemonPerChunk = 2F
@CobblemonConfigField(Category.PassiveStatus, lang = "passive_statuses", SERVER)
@CobblemonConfigField(Category.PassiveStatus, lang = "passive_statuses")
var passiveStatuses = mutableMapOf(
Statuses.POISON.configEntry(),
Statuses.POISON_BADLY.configEntry(),
@@ -168,165 +163,165 @@ class CobblemonConfig {
Statuses.BURN.configEntry()
)
@CobblemonConfigField(Category.Healing, lang = "infinite_healer_charge", SERVER)
@CobblemonConfigField(Category.Healing, lang = "infinite_healer_charge")
var infiniteHealerCharge = false
@CobblemonConfigField(Category.Healing, lang = "max_healer_charge", SERVER)
@CobblemonConfigField(Category.Healing, lang = "max_healer_charge")
var maxHealerCharge = 6.0f
@CobblemonConfigField(Category.Healing, lang = "seconds_to_charge_healing_machine", SERVER)
@CobblemonConfigField(Category.Healing, lang = "seconds_to_charge_healing_machine")
var secondsToChargeHealingMachine = 900.0
@CobblemonConfigField(Category.Healing, lang = "default_faint_timer", SERVER)
@CobblemonConfigField(Category.Healing, lang = "default_faint_timer")
var defaultFaintTimer = 300
@CobblemonConfigField(Category.Healing, lang = "faint_awaken_health_percent", SERVER)
@CobblemonConfigField(Category.Healing, lang = "faint_awaken_health_percent")
var faintAwakenHealthPercent = 0.2f
@CobblemonConfigField(Category.Healing, lang = "heal_percent", SERVER)
@CobblemonConfigField(Category.Healing, lang = "heal_percent")
var healPercent = 0.05
@CobblemonConfigField(Category.Healing, lang = "heal_timer", SERVER)
@CobblemonConfigField(Category.Healing, lang = "heal_timer")
var healTimer = 60
@CobblemonConfigField(Category.Spawning, lang = "base_apricorn_tree_generation_chance", SERVER)
@CobblemonConfigField(Category.Spawning, lang = "base_apricorn_tree_generation_chance")
var baseApricornTreeGenerationChance = 0.1F
@CobblemonConfigField(Category.Pokemon, lang = "display_entity_level_label", CLIENT)
@CobblemonConfigField(Category.Pokemon, lang = "display_entity_level_label")
var displayEntityLevelLabel = true
@CobblemonConfigField(Category.Pokemon, lang = "display_entity_name_label", CLIENT)
@CobblemonConfigField(Category.Pokemon, lang = "display_entity_name_label")
var displayEntityNameLabel = true
@CobblemonConfigField(Category.Pokemon, lang = "display_name_for_unknown_pokemon", CLIENT)
@CobblemonConfigField(Category.Pokemon, lang = "display_name_for_unknown_pokemon")
var displayNameForUnknownPokemon = false
@CobblemonConfigField(Category.Pokemon, lang = "display_entity_labels_when_crouching_only", CLIENT)
@CobblemonConfigField(Category.Pokemon, lang = "display_entity_labels_when_crouching_only")
var displayEntityLabelsWhenCrouchingOnly = false
@CobblemonConfigField(Category.Spawning, lang = "shiny_rate", SERVER)
@CobblemonConfigField(Category.Spawning, lang = "shiny_rate")
var shinyRate = 8192F
@CobblemonConfigField(Category.Pokemon, lang = "shiny_notice_particles_distance", CLIENT)
@CobblemonConfigField(Category.Pokemon, lang = "shiny_notice_particles_distance")
var shinyNoticeParticlesDistance = 24F
@CobblemonConfigField(Category.Pokemon, lang = "capture_calculator", SERVER)
@CobblemonConfigField(Category.Pokemon, lang = "capture_calculator")
var captureCalculator: CaptureCalculator = CobblemonCaptureCalculator
@CobblemonConfigField(Category.Pokemon, lang = "player_damage_pokemon", SERVER)
@CobblemonConfigField(Category.Pokemon, lang = "player_damage_pokemon")
var playerDamagePokemon = true
@CobblemonConfigField(Category.World, lang = "apple_leftovers_chance", SERVER)
@CobblemonConfigField(Category.World, lang = "apple_leftovers_chance")
var appleLeftoversChance = 0.025
@CobblemonConfigField(Category.World, lang = "max_roots_in_area", SERVER)
@CobblemonConfigField(Category.World, lang = "max_roots_in_area")
var maxRootsInArea = 5
@CobblemonConfigField(Category.World, lang = "big_root_propagation_chance", SERVER)
@CobblemonConfigField(Category.World, lang = "big_root_propagation_chance")
var bigRootPropagationChance = 0.1
@CobblemonConfigField(Category.World, lang = "energy_root_chance", SERVER)
@CobblemonConfigField(Category.World, lang = "energy_root_chance")
var energyRootChance = 0.25
@CobblemonConfigField(Category.Pokemon, lang = "max_dynamax_level", SERVER)
@CobblemonConfigField(Category.Pokemon, lang = "max_dynamax_level")
@IntConstraint(min = 0, max = 10)
var maxDynamaxLevel = 10
@CobblemonConfigField(Category.Spawning, lang = "tera_type_rate", SERVER)
@CobblemonConfigField(Category.Spawning, lang = "tera_type_rate")
var teraTypeRate = 20F
@CobblemonConfigField(Category.World, lang = "default_pastured_pokemon_limit", SERVER)
@CobblemonConfigField(Category.World, lang = "default_pastured_pokemon_limit")
var defaultPasturedPokemonLimit = 16
@CobblemonConfigField(Category.World, lang = "pasture_block_update_ticks", SERVER)
@CobblemonConfigField(Category.World, lang = "pasture_block_update_ticks")
var pastureBlockUpdateTicks = 40
@CobblemonConfigField(Category.World, lang = "pasture_max_wander_distance", SERVER)
@CobblemonConfigField(Category.World, lang = "pasture_max_wander_distance")
var pastureMaxWanderDistance = 64
@CobblemonConfigField(Category.World, lang = "pasture_max_per_chunk", SERVER)
@CobblemonConfigField(Category.World, lang = "pasture_max_per_chunk")
var pastureMaxPerChunk = 4F
@CobblemonConfigField(Category.World, lang = "max_inserted_fossil_items", SERVER)
@CobblemonConfigField(Category.World, lang = "max_inserted_fossil_items")
var maxInsertedFossilItems = 2
@CobblemonConfigField(Category.Battles, lang = "walking_in_battle_animations", CLIENT)
@CobblemonConfigField(Category.Battles, lang = "walking_in_battle_animations")
var walkingInBattleAnimations = false
@CobblemonConfigField(Category.Battles, lang = "battle_wild_max_distance", SERVER)
@CobblemonConfigField(Category.Battles, lang = "battle_wild_max_distance")
var battleWildMaxDistance = 12F
@CobblemonConfigField(Category.World, lang = "trade_max_distance", SERVER)
@CobblemonConfigField(Category.World, lang = "trade_max_distance")
var tradeMaxDistance = 12F
@CobblemonConfigField(Category.Battles, lang = "battle_pvp_max_distance", SERVER)
@CobblemonConfigField(Category.Battles, lang = "battle_pvp_max_distance")
@SerializedName("battlePvPMaxDistance", alternate = ["BattlePvPMaxDistance"])
var battlePvPMaxDistance = 32F
@CobblemonConfigField(Category.Battles, lang = "battle_spectate_max_distance", SERVER)
@CobblemonConfigField(Category.Battles, lang = "battle_spectate_max_distance")
var battleSpectateMaxDistance = 64F
@CobblemonConfigField(Category.Pokedex, lang = "max_pokedex_scanning_detection_range", SERVER)
@CobblemonConfigField(Category.Pokedex, lang = "max_pokedex_scanning_detection_range")
var maxPokedexScanningDetectionRange = 10.0
@CobblemonConfigField(Category.Pokedex, lang = "hide_unimplemented_pokemon_in_the_pokedex", CLIENT)
@CobblemonConfigField(Category.Pokedex, lang = "hide_unimplemented_pokemon_in_the_pokedex")
var hideUnimplementedPokemonInThePokedex = false
@CobblemonConfigField(Category.Interface, lang = "summary_pokemon_follow_cursor", CLIENT)
@CobblemonConfigField(Category.Interface, lang = "summary_pokemon_follow_cursor")
var summaryPokemonFollowCursor = true
@CobblemonConfigField(Category.Interface, lang = "party_portrait_animations", CLIENT)
var partyPortraitAnimations = PortraitStyle.ANIMATE_SELECTED
@CobblemonConfigField(Category.Interface, lang = "party_portrait_animations")
var partyPortraitAnimations = PortraitStyle.NEVER_ANIMATE
@CobblemonConfigField(Category.Riding, lang = "third_person_view_bobbing", CLIENT)
@CobblemonConfigField(Category.Riding, lang = "third_person_view_bobbing")
var thirdPersonViewBobbing = true
@CobblemonConfigField(Category.Riding, lang = "invert_roll", CLIENT)
@CobblemonConfigField(Category.Riding, lang = "invert_roll")
var invertRoll = false
@CobblemonConfigField(Category.Riding, lang = "invert_pitch", CLIENT)
@CobblemonConfigField(Category.Riding, lang = "invert_pitch")
var invertPitch = false
@CobblemonConfigField(Category.Riding, lang = "invert_yaw", CLIENT)
@CobblemonConfigField(Category.Riding, lang = "invert_yaw")
var invertYaw = false
@CobblemonConfigField(Category.Riding, lang = "x_axis_sensitivity", CLIENT)
@CobblemonConfigField(Category.Riding, lang = "x_axis_sensitivity")
var xAxisSensitivity = 1.0
@CobblemonConfigField(Category.Riding, lang = "y_axis_sensitivity", CLIENT)
@CobblemonConfigField(Category.Riding, lang = "y_axis_sensitivity")
var yAxisSensitivity = 1.0
@CobblemonConfigField(Category.Riding, lang = "swap_x_and_y_axes", CLIENT)
@CobblemonConfigField(Category.Riding, lang = "swap_x_and_y_axes")
var swapXAndYAxes = false
@CobblemonConfigField(Category.Riding, lang = "automatic_righting_delay", CLIENT)
@CobblemonConfigField(Category.Riding, lang = "automatic_righting_delay")
var rightingDelay = -1.0
@CobblemonConfigField(Category.Riding, lang = "disable_roll", CLIENT)
@CobblemonConfigField(Category.Riding, lang = "disable_roll")
var disableRoll = false
@CobblemonConfigField(Category.Riding, lang = "display_controls_duration_seconds", CLIENT)
@CobblemonConfigField(Category.Riding, lang = "display_controls_duration_seconds")
var displayControlSeconds = 3
@CobblemonConfigField(Category.Riding, lang = "infinite_ride_stamina", SERVER)
@CobblemonConfigField(Category.Riding, lang = "infinite_ride_stamina")
var infiniteRideStamina = false
@CobblemonConfigField(Category.Riding, lang = "remember_riding_camera", CLIENT)
@CobblemonConfigField(Category.Riding, lang = "remember_riding_camera")
var rememberRidingCamera = true
@CobblemonConfigField(Category.Debug, lang = "enable_debug_keys", CLIENT)
@CobblemonConfigField(Category.Debug, lang = "enable_debug_keys")
var enableDebugKeys = true
@CobblemonConfigField(Category.Spawning, lang = "despawner_near_distance", SERVER)
@CobblemonConfigField(Category.Spawning, lang = "despawner_near_distance")
var despawnerNearDistance = 32f
@CobblemonConfigField(Category.Spawning, lang = "despawner_far_distance", SERVER)
@CobblemonConfigField(Category.Spawning, lang = "despawner_far_distance")
var despawnerFarDistance = 96f
@CobblemonConfigField(Category.Spawning, lang = "despawner_min_age_ticks", SERVER)
@CobblemonConfigField(Category.Spawning, lang = "despawner_min_age_ticks")
var despawnerMinAgeTicks = 600
@CobblemonConfigField(Category.Spawning, lang = "despawner_max_age_ticks", SERVER)
@CobblemonConfigField(Category.Spawning, lang = "despawner_max_age_ticks")
var despawnerMaxAgeTicks = 3600
fun clone(): CobblemonConfig {

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