mirror of
https://gitlab.com/cable-mc/cobblemon.git
synced 2026-10-26 09:06:26 -04:00
Compare commits
62
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa7a90ad02 | ||
|
|
5d1f0804d5 | ||
|
|
5b01754a82 | ||
|
|
6630ee942c | ||
|
|
88932c8c41 | ||
|
|
411f98d0ae | ||
|
|
02a1929ede | ||
|
|
ff90059eec | ||
|
|
4a30bb3b04 | ||
|
|
fac8c5f510 | ||
|
|
2d077c607a | ||
|
|
f46968f878 | ||
|
|
b8f73f277d | ||
|
|
6df2b996e2 | ||
|
|
ddc4bd358d | ||
|
|
e197287775 | ||
|
|
be019c79df | ||
|
|
73fe851c58 | ||
|
|
8544744530 | ||
|
|
32d7bfcef3 | ||
|
|
27263cd057 | ||
|
|
55f0962205 | ||
|
|
40c5df46ce | ||
|
|
e753c75c31 | ||
|
|
4e96222a4d | ||
|
|
4ee90d4cfb | ||
|
|
540cd612c1 | ||
|
|
1b1d22fdf3 | ||
|
|
1ee1001774 | ||
|
|
533272ef09 | ||
|
|
91300e4d03 | ||
|
|
fc135faf57 | ||
|
|
cdd72c664e | ||
|
|
5f025d0f68 | ||
|
|
6af2c807ab | ||
|
|
c45c90c6c7 | ||
|
|
1ec97c4c90 | ||
|
|
f0bcd38ac7 | ||
|
|
f4ba446c79 | ||
|
|
60f98ebfce | ||
|
|
36a93bda5a | ||
|
|
8d60b07c54 | ||
|
|
5f9cc86044 | ||
|
|
a6b7e07cf5 | ||
|
|
0ba6215b8d | ||
|
|
e2f6fae689 | ||
|
|
296aa01d0f | ||
|
|
12ab3f2d07 | ||
|
|
5a7a374bc9 | ||
|
|
ceece45ad4 | ||
|
|
b344ad66c5 | ||
|
|
dbc7af4ad4 | ||
|
|
5f5bf01cb5 | ||
|
|
3f03afc7eb | ||
|
|
93011f084b | ||
|
|
7c90a8425e | ||
|
|
17c71efb17 | ||
|
|
a5b6a954fd | ||
|
|
3b7bd4d810 | ||
|
|
81278e2d16 | ||
|
|
3af66e0028 | ||
|
|
75ead2f665 |
+2
-1
@@ -12,7 +12,8 @@ out
|
||||
*.ipr
|
||||
*.iws
|
||||
*.iml
|
||||
.idea
|
||||
.idea/*
|
||||
!.idea/icon.png
|
||||
|
||||
# gradle
|
||||
build
|
||||
|
||||
Generated
BIN
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
@@ -12,6 +12,14 @@
|
||||
- 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
|
||||
@@ -421,6 +421,8 @@
|
||||
- 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.
|
||||
@@ -473,6 +475,7 @@
|
||||
- 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
|
||||
@@ -557,6 +560,8 @@
|
||||
- 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.
|
||||
@@ -701,6 +706,9 @@
|
||||
- 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:
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
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,48 +55,6 @@ 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,20 +8,51 @@
|
||||
|
||||
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 {
|
||||
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();
|
||||
}
|
||||
|
||||
@Inject(method = "rideTick", at = @At("HEAD"))
|
||||
private void cobblemon$updateOrientationControllerRideTick(CallbackInfo ci) {
|
||||
@@ -51,5 +82,4 @@ public abstract class ServerPlayerMixin {
|
||||
private boolean cobblemon$cancelSavingPokemonMounts(boolean original, @Local(ordinal=0) Entity entity) {
|
||||
return original && !(entity instanceof PokemonEntity);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,15 +9,21 @@
|
||||
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
|
||||
@@ -42,4 +48,16 @@ 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", CobblemonItem(Item.Properties().craftRemainder(Items.GLASS_BOTTLE).stacksTo((16))))
|
||||
val MOOMOO_MILK = create("moomoo_milk", MoomooMilk(Item.Properties().craftRemainder(Items.GLASS_BOTTLE).stacksTo((16))))
|
||||
|
||||
@JvmField
|
||||
val POTION = create("potion", PotionItem(PotionType.POTION))
|
||||
|
||||
@@ -26,8 +26,6 @@ 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
|
||||
|
||||
+5
-4
@@ -22,18 +22,19 @@ class ReelInPokemonContext(val species : ResourceLocation, val baitId: ResourceL
|
||||
class ReelInPokemonCriterionCondition(
|
||||
playerCtx: Optional<ContextAwarePredicate>,
|
||||
val species: String,
|
||||
val baitId: String
|
||||
val baitId: String // also allows "cobblemon:empty_bait" to indicate that no bait is allowed for this criterion to match
|
||||
) : 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", "empty_bait").forGetter { it.baitId }
|
||||
).apply(it, { playerCtx, pokemonId, baitId -> ReelInPokemonCriterionCondition(playerCtx, pokemonId, baitId.ifEmpty { "empty_bait" }) }) }
|
||||
Codec.STRING.optionalFieldOf("baitId", "any").forGetter { it.baitId }
|
||||
).apply(it, { playerCtx, pokemonId, baitId -> ReelInPokemonCriterionCondition(playerCtx, pokemonId, baitId) }) }
|
||||
}
|
||||
|
||||
override fun matches(player: ServerPlayer, context: ReelInPokemonContext): Boolean {
|
||||
return (context.species == this.species.asIdentifierDefaultingNamespace() || this.species == "any") && (context.baitId == this.baitId.asIdentifierDefaultingNamespace() || this.baitId == "empty_bait")
|
||||
return (context.species == this.species.asIdentifierDefaultingNamespace() || this.species == "any")
|
||||
&& (context.baitId == this.baitId.asIdentifierDefaultingNamespace() || this.baitId == "any")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -11,7 +11,6 @@ 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
|
||||
|
||||
@@ -23,6 +22,13 @@ 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))
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -23,7 +23,7 @@ import net.minecraft.world.entity.schedule.Activity
|
||||
|
||||
class ActivityChangeTaskConfig : SingleTaskConfig {
|
||||
val activity: Activity = Activity.IDLE
|
||||
val condition: ExpressionLike = "false".asExpressionLike()
|
||||
val condition: ExpressionLike = "true".asExpressionLike()
|
||||
|
||||
override fun getVariables(entity: LivingEntity, behaviourConfigurationContext: BehaviourConfigurationContext): List<MoLangConfigVariable> = emptyList()
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ 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
|
||||
@@ -40,7 +39,6 @@ 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
|
||||
@@ -57,7 +55,6 @@ 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
|
||||
@@ -137,7 +134,6 @@ 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
|
||||
@@ -145,7 +141,6 @@ 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
|
||||
@@ -160,9 +155,6 @@ 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
|
||||
@@ -1185,16 +1177,28 @@ object MoLangFunctions {
|
||||
return@put DoubleValue.ONE
|
||||
}
|
||||
map.put("has_memory_value") { params ->
|
||||
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) })
|
||||
for (param in params.params) {
|
||||
if (!entity.hasMemoryFromString(param.asString())) {
|
||||
return@put DoubleValue.ZERO
|
||||
}
|
||||
}
|
||||
return@put DoubleValue.ONE
|
||||
}
|
||||
map.put("lacks_memory_value") { params ->
|
||||
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) })
|
||||
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
|
||||
}
|
||||
map.put("can_see") { params ->
|
||||
val target = params.get<MoValue>(0)
|
||||
@@ -1252,15 +1256,6 @@ 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) {
|
||||
@@ -1905,6 +1900,9 @@ 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 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
|
||||
// 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)
|
||||
|
||||
/**
|
||||
* Attempts to find an ability by ID and resolve if it should be forced or if it's legal for the given form.
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ 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.api.spawning.prospecting.LurePokeSnackDetector
|
||||
import com.cobblemon.mod.common.block.entity.PokeSnackBlockEntity
|
||||
import com.cobblemon.mod.common.client.entity.PokemonClientDelegate
|
||||
import net.minecraft.core.particles.BlockParticleOption
|
||||
import net.minecraft.core.particles.ParticleOptions
|
||||
@@ -23,7 +23,7 @@ import net.minecraft.resources.ResourceLocation
|
||||
*/
|
||||
val aspectParticleMap: Map<String, ParticleData> = mapOf(
|
||||
SaccharineLogSlatheredInfluence.HONEY_DRENCHED_ASPECT to ParticleData.MinecraftParticle(ParticleTypes.FALLING_HONEY, 0.075, 1),
|
||||
LurePokeSnackDetector.POKE_SNACK_CRUMBED_ASPECT to ParticleData.MinecraftParticle(
|
||||
PokeSnackBlockEntity.POKE_SNACK_CRUMBED_ASPECT to ParticleData.MinecraftParticle(
|
||||
BlockParticleOption(
|
||||
ParticleTypes.BLOCK,
|
||||
CobblemonBlocks.POKE_SNACK.defaultBlockState()
|
||||
|
||||
+2
@@ -603,6 +603,7 @@ class DolphinSettings : RidingBehaviourSettings {
|
||||
buffer.writeNullableExpression(canJump)
|
||||
buffer.writeNullableExpression(reverseDriveFactor)
|
||||
buffer.writeNullableExpression(strafeFactor)
|
||||
buffer.writeNullableExpression(timeToRollCorrect)
|
||||
buffer.writeNullableExpression(jumpExpr)
|
||||
buffer.writeNullableExpression(handlingExpr)
|
||||
buffer.writeNullableExpression(horizontalPitchExpr)
|
||||
@@ -619,6 +620,7 @@ 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,18 +18,11 @@ 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
|
||||
@@ -39,16 +32,21 @@ 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.AreaSpawner
|
||||
import com.cobblemon.mod.common.api.spawning.spawner.BasicSpawner
|
||||
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
|
||||
@@ -66,14 +64,13 @@ 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 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
|
||||
* 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
|
||||
* that area is unmoving then it is a [FixedAreaSpawner] whereas if it is actively following the player it is a
|
||||
* [PlayerSpawner].
|
||||
* [PlayerSpawner]. You can also use a [BasicSpawner] for more general-purpose spawning without any issue.
|
||||
*
|
||||
* Spawning is coordinated and ticked using a [SpawnerManager], and all the current managers are accessible from
|
||||
* [BestSpawner.spawnerManagers].
|
||||
* 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.
|
||||
*
|
||||
* 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
|
||||
@@ -87,9 +84,9 @@ import net.minecraft.server.MinecraftServer
|
||||
*/
|
||||
object BestSpawner {
|
||||
var config = BestSpawnerConfig()
|
||||
val spawnerManagers = mutableListOf<SpawnerManager>(CobblemonWorldSpawnerManager)
|
||||
|
||||
lateinit var defaultPokemonDespawner: Despawner<PokemonEntity>
|
||||
lateinit var fishingSpawner: FishingSpawner
|
||||
lateinit var fishingSpawner: BasicSpawner
|
||||
|
||||
fun init() {
|
||||
LOGGER.info("Starting the Best Spawner...")
|
||||
@@ -137,12 +134,13 @@ object BestSpawner {
|
||||
|
||||
fun reloadConfig() {
|
||||
loadConfig()
|
||||
spawnerManagers.forEach(SpawnerManager::onConfigReload)
|
||||
}
|
||||
|
||||
fun onServerStarted(server: MinecraftServer) {
|
||||
CobblemonSpawnPools.onServerLoad(server)
|
||||
spawnerManagers.forEach(SpawnerManager::onServerStarted)
|
||||
fishingSpawner = FishingSpawner()
|
||||
fishingSpawner = BasicSpawner(
|
||||
name = "fishing",
|
||||
spawnPool = CobblemonSpawnPools.WORLD_SPAWN_POOL
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,6 @@ object CobblemonSpawnPools {
|
||||
|
||||
fun onServerLoad(server: MinecraftServer) {
|
||||
Cobblemon.LOGGER.info("Optimizing spawn pools...")
|
||||
WORLD_SPAWN_POOL.forEach { it.onServerLoad(server) }
|
||||
WORLD_SPAWN_POOL.precalculate()
|
||||
WORLD_SPAWN_POOL.onServerLoad(server)
|
||||
}
|
||||
}
|
||||
-76
@@ -1,76 +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
|
||||
|
||||
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,16 +17,21 @@ 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) {
|
||||
server()?.playerList?.getPlayer(entityUUID!!)
|
||||
} else {
|
||||
server()?.getLevel(entityWorldId!!)?.getEntity(entityId!!)
|
||||
}
|
||||
get() =
|
||||
if (entityType == EntityType.PLAYER) {
|
||||
entityUUID?.let {
|
||||
server()?.playerList?.getPlayer(it)
|
||||
}
|
||||
} else if (entityWorldId != null && entityId != null) {
|
||||
server()?.getLevel(entityWorldId)?.getEntity(entityId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -1,85 +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
|
||||
|
||||
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,9 +39,13 @@ 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,6 +24,7 @@ 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
|
||||
|
||||
@@ -72,6 +73,13 @@ 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.
|
||||
|
||||
-38
@@ -1,38 +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.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
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
+1
-6
@@ -44,11 +44,6 @@ open class BucketNormalizingInfluence(
|
||||
bucketWeights[it] = weight.toDouble().pow(1 / normalizationFactor).toFloat()
|
||||
}
|
||||
|
||||
val sum = bucketWeights.values.sum()
|
||||
val to100 = 100F / sum
|
||||
bucketWeights.keys.toList().forEach {
|
||||
val weight = bucketWeights[it] ?: return@forEach
|
||||
bucketWeights[it] = weight * to100
|
||||
}
|
||||
normalizeBucketWeights(bucketWeights)
|
||||
}
|
||||
}
|
||||
+9
@@ -40,6 +40,15 @@ 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
|
||||
/**
|
||||
|
||||
+3
-5
@@ -9,16 +9,15 @@
|
||||
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
|
||||
@@ -31,7 +30,6 @@ interface SpawningInfluenceDetector {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
val detectors = mutableSetOf<SpawningInfluenceDetector>(
|
||||
LurePokeSnackDetector,
|
||||
SaccharineLogSlatheredDetector,
|
||||
IncenseSweetDetector
|
||||
)
|
||||
|
||||
+7
-5
@@ -14,13 +14,15 @@ import net.minecraft.server.level.ServerLevel
|
||||
import net.minecraft.core.BlockPos
|
||||
|
||||
/**
|
||||
* A spawnable position relating to triggered spawning. Uses a single-point structure cache which is
|
||||
* the only contribution of this implementation.
|
||||
* 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.
|
||||
*
|
||||
* @author Hiroku
|
||||
* @since February 3rd, 2024
|
||||
*/
|
||||
open class TriggerSpawnablePosition(
|
||||
open class BasicSpawnablePosition(
|
||||
override val cause: SpawnCause,
|
||||
override val world: ServerLevel,
|
||||
override val position: BlockPos,
|
||||
@@ -29,6 +31,6 @@ open class TriggerSpawnablePosition(
|
||||
override val canSeeSky: Boolean,
|
||||
override val influences: MutableList<SpawningInfluence>
|
||||
) : SpawnablePosition() {
|
||||
val triggerStructureCache = StructureChunkCache()
|
||||
override fun getStructureCache(pos: BlockPos) = triggerStructureCache
|
||||
val singlePointStructureCache = StructureChunkCache()
|
||||
override fun getStructureCache(pos: BlockPos) = singlePointStructureCache
|
||||
}
|
||||
+1
-1
@@ -28,7 +28,7 @@ class FishingSpawnablePosition(
|
||||
world: ServerLevel,
|
||||
pos: BlockPos,
|
||||
influences: MutableList<SpawningInfluence>
|
||||
) : TriggerSpawnablePosition(
|
||||
) : BasicSpawnablePosition(
|
||||
cause = cause,
|
||||
world = world,
|
||||
position = pos,
|
||||
|
||||
+1
@@ -36,6 +36,7 @@ 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<*>>()
|
||||
|
||||
|
||||
-189
@@ -1,189 +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.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>()
|
||||
}
|
||||
+7
-2
@@ -24,6 +24,11 @@ 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(
|
||||
@@ -32,12 +37,12 @@ interface SpawningSelector<T : SpawnSelectionData> {
|
||||
selectionData: T
|
||||
): SpawnAction<*>?
|
||||
|
||||
fun select(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>, max: Int): List<SpawnAction<*>> {
|
||||
fun select(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>, maxSpawns: Int): List<SpawnAction<*>> {
|
||||
val selectionData = getSelectionData(spawner, bucket, spawnablePositions)
|
||||
|
||||
val spawnActions = selectionData.spawnActions
|
||||
|
||||
while (spawnActions.size < max) {
|
||||
while (spawnActions.size < maxSpawns) {
|
||||
val spawnAction = selectSpawnAction(
|
||||
spawner = spawner,
|
||||
bucket = bucket,
|
||||
|
||||
@@ -1,180 +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.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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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>()
|
||||
}
|
||||
+14
-10
@@ -8,33 +8,37 @@
|
||||
|
||||
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, 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.
|
||||
* 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.
|
||||
*
|
||||
* @author Hiroku
|
||||
* @since February 5th, 2022
|
||||
*/
|
||||
open class FixedAreaSpawner(
|
||||
name: String,
|
||||
spawns: SpawnPool,
|
||||
manager: SpawnerManager,
|
||||
spawnPool: SpawnPool,
|
||||
val world: ServerLevel,
|
||||
val position: BlockPos,
|
||||
val horizontalRadius: Int,
|
||||
val verticalRadius: Int,
|
||||
override var ticksBetweenSpawns: Float = 20F
|
||||
) : AreaSpawner(name, spawns, manager) {
|
||||
override fun getZoneInput(cause: SpawnCause): SpawningZoneInput? {
|
||||
val basePos = position.offset(-horizontalRadius, -verticalRadius, -horizontalRadius)
|
||||
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 {
|
||||
val basePos = position.offset(-horizontalRadius, -verticalRadius, -horizontalRadius)
|
||||
return SpawningZoneInput(
|
||||
cause = cause,
|
||||
world = world,
|
||||
|
||||
+28
-5
@@ -10,7 +10,6 @@ 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
|
||||
@@ -31,11 +30,16 @@ import kotlin.random.Random
|
||||
* @author Hiroku
|
||||
* @since February 14th, 2022
|
||||
*/
|
||||
class PlayerSpawner(player: ServerPlayer, spawns: SpawnPool, manager: SpawnerManager) : AreaSpawner(player.name.string, spawns, manager) {
|
||||
class PlayerSpawner(
|
||||
player: ServerPlayer,
|
||||
spawnPool: SpawnPool
|
||||
) : BasicSpawner(
|
||||
name = player.name.string,
|
||||
spawnPool = spawnPool
|
||||
) {
|
||||
val uuid: UUID = player.uuid
|
||||
override var ticksBetweenSpawns = config.ticksBetweenSpawnAttempts
|
||||
override fun getCauseEntity() = uuid.getPlayer()
|
||||
override fun getZoneInput(cause: SpawnCause): SpawningZoneInput? {
|
||||
|
||||
fun getZoneInput(cause: SpawnCause): SpawningZoneInput? {
|
||||
val player = uuid.getPlayer() ?: return null
|
||||
val zoneDiameter = config.spawningZoneDiameter
|
||||
val zoneHeight = config.spawningZoneHeight
|
||||
@@ -53,6 +57,7 @@ class PlayerSpawner(player: ServerPlayer, spawns: SpawnPool, manager: SpawnerMan
|
||||
} else {
|
||||
thetatemp
|
||||
}
|
||||
|
||||
val x = center.x + r * cos(theta)
|
||||
val z = center.z + r * sin(theta)
|
||||
|
||||
@@ -67,4 +72,22 @@ class PlayerSpawner(player: ServerPlayer, spawns: SpawnPool, manager: SpawnerMan
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -10,7 +10,6 @@ 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
|
||||
@@ -28,7 +27,7 @@ import net.minecraft.server.level.ServerPlayer
|
||||
* @since February 14th, 2022
|
||||
*/
|
||||
object PlayerSpawnerFactory {
|
||||
var spawns: SpawnPool by mutableLazy {
|
||||
var spawnPool: SpawnPool by mutableLazy {
|
||||
CobblemonSpawnPools.WORLD_SPAWN_POOL
|
||||
}
|
||||
|
||||
@@ -46,9 +45,9 @@ object PlayerSpawnerFactory {
|
||||
)
|
||||
})
|
||||
|
||||
fun create(spawnerManager: SpawnerManager, player: ServerPlayer): PlayerSpawner {
|
||||
fun create(player: ServerPlayer): PlayerSpawner {
|
||||
val influences = influenceBuilders.mapNotNull { it(player) }
|
||||
return PlayerSpawner(player, spawns, spawnerManager).also {
|
||||
return PlayerSpawner(player, spawnPool).also {
|
||||
it.influences.addAll(influences)
|
||||
it.influences.addAll(
|
||||
CobblemonSpawnRules.rules.values.filter(SpawnRule::enabled).flatMap(SpawnRule::components)
|
||||
|
||||
@@ -14,33 +14,67 @@ 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>
|
||||
fun getSpawningSelector(): SpawningSelector<*>
|
||||
fun setSpawningSelector(selector: SpawningSelector<*>)
|
||||
fun getSpawnPool(): SpawnPool
|
||||
fun setSpawnPool(spawnPool: SpawnPool)
|
||||
var spawnPool: SpawnPool
|
||||
var selector: SpawningSelector<*>
|
||||
var generator: SpawningZoneGenerator
|
||||
var resolver: AreaSpawnablePositionResolver
|
||||
var maxPokemonPerChunk: Float
|
||||
|
||||
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(getSpawnPool().retrieve(bucket, spawnablePosition).filter { it.isSatisfiedBy(spawnablePosition) })
|
||||
spawns.addAll(spawnPool.retrieve(bucket, spawnablePosition).filter { it.isSatisfiedBy(spawnablePosition) })
|
||||
spawnablePosition.influences.forEach { influence ->
|
||||
val influencedSpawns = influence.injectSpawns(bucket, spawnablePosition)
|
||||
if (influencedSpawns != null) {
|
||||
@@ -50,6 +84,101 @@ 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 {
|
||||
@@ -66,4 +195,77 @@ 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
|
||||
}
|
||||
}
|
||||
-89
@@ -1,89 +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.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()
|
||||
}
|
||||
-52
@@ -1,52 +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.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,6 +137,9 @@ 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,6 +18,7 @@ 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
|
||||
@@ -51,12 +52,13 @@ 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 {
|
||||
val MAX_BITES = 8
|
||||
val CAKE_HEIGHT = 0.4375
|
||||
const val MAX_BITES = 8
|
||||
const val CAKE_HEIGHT = 0.4375
|
||||
|
||||
val CANDLE_PARTICLE_POSITION = Vec3(0.5, CAKE_HEIGHT + 0.5, 0.5)
|
||||
|
||||
@@ -135,16 +137,18 @@ 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
|
||||
blockEntity?.initializeFromItemStack(stack)
|
||||
val blockEntity = level.getBlockEntity(pos) as? PokeSnackBlockEntity ?: return
|
||||
blockEntity.initializeFromItemStack(stack)
|
||||
blockEntity.placedBy = placer?.uuid
|
||||
}
|
||||
|
||||
override fun getCloneItemStack(level: LevelReader, pos: BlockPos, state: BlockState): ItemStack {
|
||||
@@ -294,15 +298,28 @@ 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
|
||||
if (bites < MAX_BITES) {
|
||||
level.setBlock(pos, state.setValue(BITES, bites + 1) as BlockState, UPDATE_ALL)
|
||||
} else {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
@@ -352,8 +369,19 @@ 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()
|
||||
}
|
||||
}
|
||||
|
||||
+27
-25
@@ -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.Companion.CRAFTING_GRID_WIDTH,
|
||||
CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
|
||||
CampfireBlockEntity.CRAFTING_GRID_WIDTH,
|
||||
CampfireBlockEntity.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.Companion.RESULT_SLOT, resultSlotX, resultSlotY))
|
||||
addSlot(CookingPotResultSlot(this.container, CampfireBlockEntity.RESULT_SLOT, resultSlotX, resultSlotY))
|
||||
|
||||
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
|
||||
for ((index, slotIndex) in CampfireBlockEntity.CRAFTING_GRID_SLOTS.withIndex()) {
|
||||
val i = index / CampfireBlockEntity.CRAFTING_GRID_WIDTH
|
||||
val j = index % CampfireBlockEntity.CRAFTING_GRID_WIDTH
|
||||
addSlot(Slot(this.container, slotIndex, 33 + j * 18, 18 + i * 18))
|
||||
}
|
||||
|
||||
for ((index, slotIndex) in CampfireBlockEntity.Companion.SEASONING_SLOTS.withIndex()) {
|
||||
addSlot(SeasoningSlot(this.container, slotIndex, 110 + index * 18, 18))
|
||||
for ((index, slotIndex) in CampfireBlockEntity.SEASONING_SLOTS.withIndex()) {
|
||||
addSlot(SeasoningSlot(this, this.container, slotIndex, 110 + index * 18, 18))
|
||||
}
|
||||
|
||||
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
|
||||
for ((index, _) in CampfireBlockEntity.PLAYER_INVENTORY_SLOTS.withIndex()) {
|
||||
val i = index / CampfireBlockEntity.PLAYER_INVENTORY_WIDTH
|
||||
val j = index % CampfireBlockEntity.PLAYER_INVENTORY_WIDTH
|
||||
addSlot(Slot(playerInventory, index + 9, 8 + j * 18, 84 + i * 18))
|
||||
}
|
||||
|
||||
for ((index, _) in CampfireBlockEntity.Companion.PLAYER_HOTBAR_SLOTS.withIndex()) {
|
||||
for ((index, _) in CampfireBlockEntity.PLAYER_HOTBAR_SLOTS.withIndex()) {
|
||||
addSlot(Slot(playerInventory, index, 8 + index * 18, 142))
|
||||
}
|
||||
}
|
||||
@@ -173,29 +173,31 @@ 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 })
|
||||
.filter { !it.isEmpty && it.`is`(recipe.value.seasoningTag) })
|
||||
|
||||
} else previewItem = ItemStack.EMPTY
|
||||
}
|
||||
|
||||
override fun getResultSlotIndex(): Int {
|
||||
return CampfireBlockEntity.Companion.RESULT_SLOT
|
||||
return CampfireBlockEntity.RESULT_SLOT
|
||||
}
|
||||
|
||||
override fun getGridWidth(): Int {
|
||||
return CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
|
||||
return CampfireBlockEntity.CRAFTING_GRID_WIDTH
|
||||
}
|
||||
|
||||
override fun getGridHeight(): Int {
|
||||
return CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
|
||||
return CampfireBlockEntity.CRAFTING_GRID_WIDTH
|
||||
}
|
||||
|
||||
override fun getSize(): Int {
|
||||
return CampfireBlockEntity.Companion.ITEMS_SIZE
|
||||
return CampfireBlockEntity.ITEMS_SIZE
|
||||
}
|
||||
|
||||
fun getBurnProgress(): Float {
|
||||
@@ -227,24 +229,24 @@ class CookingPotMenu : RecipeBookMenu<CraftingInput, CookingPotRecipeBase>, Cont
|
||||
val slotItemStack = slot.item
|
||||
itemStack = slotItemStack.copy()
|
||||
|
||||
if (index == CampfireBlockEntity.Companion.RESULT_SLOT) {
|
||||
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.PLAYER_INVENTORY_SLOTS.first, CampfireBlockEntity.Companion.PLAYER_HOTBAR_SLOTS.last + 1, false)) {
|
||||
if (index == CampfireBlockEntity.RESULT_SLOT) {
|
||||
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.PLAYER_INVENTORY_SLOTS.first, CampfireBlockEntity.PLAYER_HOTBAR_SLOTS.last + 1, false)) {
|
||||
return ItemStack.EMPTY
|
||||
}
|
||||
|
||||
slot.onQuickCraft(slotItemStack, itemStack);
|
||||
} else if (index in CampfireBlockEntity.Companion.PLAYER_INVENTORY_SLOTS || index in CampfireBlockEntity.Companion.PLAYER_HOTBAR_SLOTS) {
|
||||
} else if (index in CampfireBlockEntity.PLAYER_INVENTORY_SLOTS || index in CampfireBlockEntity.PLAYER_HOTBAR_SLOTS) {
|
||||
if (Seasonings.isSeasoning(slotItemStack)) {
|
||||
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)
|
||||
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)
|
||||
) {
|
||||
return ItemStack.EMPTY
|
||||
}
|
||||
} else if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.first, CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.last + 1, false)) {
|
||||
} else if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.CRAFTING_GRID_SLOTS.first, CampfireBlockEntity.CRAFTING_GRID_SLOTS.last + 1, false)) {
|
||||
return ItemStack.EMPTY
|
||||
}
|
||||
} 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)) {
|
||||
} 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)) {
|
||||
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(container: Container, slot: Int, x: Int, y: Int) : Slot(container, slot, x, y) {
|
||||
class SeasoningSlot(val menu: CookingPotMenu, container: Container, slot: Int, x: Int, y: Int) : Slot(container, slot, x, y) {
|
||||
override fun mayPlace(stack: ItemStack): Boolean {
|
||||
return Seasonings.isSeasoning(stack) && super.mayPlace(stack)
|
||||
val recipe = menu.currentActiveRecipe?.value() ?: return false
|
||||
return stack.`is`(recipe.seasoningTag) && super.mayPlace(stack)
|
||||
}
|
||||
}
|
||||
@@ -134,6 +134,8 @@ class CampfireBlockEntity(pos: BlockPos, state: BlockState) : BaseContainerBlock
|
||||
|
||||
campfireBlockEntity.particleCooldown = 20
|
||||
}
|
||||
|
||||
campfireBlockEntity.time++
|
||||
}
|
||||
|
||||
fun serverTick(level: Level, pos: BlockPos, state: BlockState, campfireBlockEntity: CampfireBlockEntity) {
|
||||
@@ -166,7 +168,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())
|
||||
recipe.applySeasoning(cookedItem, campfireBlockEntity.getSeasonings().filter { it.`is`(recipe.seasoningTag) })
|
||||
|
||||
if (!campfireBlockEntity.blockState.getValue(CampfireBlock.LID)) {
|
||||
campfireBlockEntity.cookingProgress = 0
|
||||
@@ -237,6 +239,7 @@ 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 {
|
||||
@@ -311,7 +314,7 @@ class CampfireBlockEntity(pos: BlockPos, state: BlockState) : BaseContainerBlock
|
||||
consumeItem(i)
|
||||
}
|
||||
for (i in SEASONING_SLOTS.first..SEASONING_SLOTS.last) {
|
||||
if (recipe.seasoningProcessors.any { it.consumesItem(getItem(i)) }) consumeItem(i)
|
||||
if (getItem(i).`is`(recipe.seasoningTag) && recipe.seasoningProcessors.any { it.consumesItem(getItem(i)) }) consumeItem(i)
|
||||
}
|
||||
|
||||
val direction = state.getValue(CampfireBlock.ITEM_DIRECTION)
|
||||
|
||||
+162
-7
@@ -8,29 +8,118 @@
|
||||
|
||||
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.*
|
||||
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.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) {
|
||||
open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) :
|
||||
TintBlockEntity(CobblemonBlockEntities.POKE_SNACK, pos, state),
|
||||
SpawningInfluence {
|
||||
|
||||
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)
|
||||
@@ -38,9 +127,7 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) : TintBlockEnt
|
||||
if (isLure()) baitEffectsComponent = itemStack.get(CobblemonItemComponents.BAIT_EFFECTS)
|
||||
|
||||
flavourComponent?.let {
|
||||
getColourMixFromFlavours(it.getDominantFlavours())?.let {
|
||||
setTint(it)
|
||||
}
|
||||
getColourMixFromFlavours(it.getDominantFlavours())?.let(::setTint)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,14 +139,60 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) : TintBlockEnt
|
||||
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
|
||||
}
|
||||
|
||||
@@ -67,12 +200,18 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) : TintBlockEnt
|
||||
* 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)
|
||||
@@ -99,11 +238,19 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) : TintBlockEnt
|
||||
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))
|
||||
@@ -130,5 +277,13 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) : TintBlockEnt
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -201,7 +201,7 @@ class CookingPotScreen(
|
||||
}
|
||||
|
||||
override fun renderSlot(guiGraphics: GuiGraphics, slot: Slot) {
|
||||
if (slot is SeasoningSlot && !menu.carried.isEmpty && !slot.mayPlace(menu.carried)) {
|
||||
if (slot is SeasoningSlot && ((!menu.carried.isEmpty && !slot.mayPlace(menu.carried)) || (slot.hasItem() && !slot.mayPlace(slot.item))) ) {
|
||||
val x = slot.x
|
||||
val y = slot.y
|
||||
guiGraphics.fill(x,y,x+16,y+16,822018048)
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ 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,
|
||||
|
||||
+270
-61
@@ -11,7 +11,14 @@ 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
|
||||
@@ -21,10 +28,13 @@ 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
|
||||
|
||||
@@ -53,7 +63,7 @@ class VaryingRenderableResolver(
|
||||
}
|
||||
|
||||
fun getResolvedTexture(state: PosableState): ResourceLocation {
|
||||
return getVariationValue(state) { texture }?.invoke(state)
|
||||
return getVariationValue(state) { texture }?.getTexture(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.")
|
||||
}
|
||||
|
||||
@@ -180,6 +190,13 @@ 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].
|
||||
@@ -187,12 +204,17 @@ class ModelAssetVariation(
|
||||
* @author Hiroku
|
||||
* @since February 6th, 2023
|
||||
*/
|
||||
fun interface ModelTextureSupplier {
|
||||
operator fun invoke(state: PosableState): ResourceLocation
|
||||
interface ModelTextureSupplier {
|
||||
operator fun invoke(state: PosableState): RenderStateShard.EmptyTextureStateShard
|
||||
fun getTexture(state: PosableState): ResourceLocation
|
||||
}
|
||||
|
||||
class StaticModelTextureSupplier(val texture: ResourceLocation): ModelTextureSupplier {
|
||||
override fun invoke(state: PosableState): ResourceLocation {
|
||||
override fun invoke(state: PosableState): RenderStateShard.EmptyTextureStateShard {
|
||||
return RenderStateShard.TextureStateShard(texture, false, false)
|
||||
}
|
||||
|
||||
override fun getTexture(state: PosableState): ResourceLocation {
|
||||
return texture
|
||||
}
|
||||
}
|
||||
@@ -203,80 +225,196 @@ class AnimatedModelTextureSupplier(
|
||||
val frames: List<ResourceLocation>,
|
||||
val interpolation: Boolean
|
||||
): ModelTextureSupplier {
|
||||
override fun invoke(state: PosableState): ResourceLocation {
|
||||
val frameIndex = floor(state.animationSeconds * fps).toInt()
|
||||
return if (frameIndex >= frames.size && !loop) {
|
||||
frames.last()
|
||||
} else {
|
||||
frames[frameIndex % frames.size]
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fun interpolatedTexture(state: PosableState): DynamicTexture? {
|
||||
override fun getTexture(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
|
||||
|
||||
val frameIndex = floor(state.animationSeconds * fps).toInt()
|
||||
try {
|
||||
if (frameIndex >= frames.size && !loop) {
|
||||
return DynamicTexture(NativeImage.read(resourceManager.getResourceOrThrow(frames.last()).open()))
|
||||
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)
|
||||
|
||||
} 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)
|
||||
}
|
||||
} catch (e : Exception) {
|
||||
return null
|
||||
}
|
||||
val o = frameIndex % frames.size
|
||||
val i = state.animationSeconds * fps - frameIndex
|
||||
|
||||
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 {
|
||||
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 texture1 : NativeImage
|
||||
val texture2 : NativeImage
|
||||
var texture2 : NativeImage? = null
|
||||
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
var newRed : Int
|
||||
var newGreen : Int
|
||||
var newBlue : Int
|
||||
var newAlpha : 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 == 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
|
||||
}
|
||||
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())
|
||||
}
|
||||
val newTexture = DynamicTexture(texture1)
|
||||
newTexture.setFilter(false, false)
|
||||
return newTexture
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
return NativeImage.read(resourceManager.getResourceOrThrow(textures[keyframe1.value.textureSlot]).open())
|
||||
}
|
||||
}
|
||||
|
||||
class VariableModelTextureSupplier : ModelTextureSupplier {
|
||||
override fun invoke(state: PosableState): ResourceLocation {
|
||||
override fun invoke(state: PosableState): RenderStateShard.EmptyTextureStateShard {
|
||||
return RenderStateShard.TextureStateShard(getTexture(state), false, false)
|
||||
}
|
||||
|
||||
override fun getTexture(state: PosableState): ResourceLocation {
|
||||
return state.runtime.environment.variable.map["texture"]?.asString()?.asIdentifierDefaultingNamespace()
|
||||
?: cobblemonResource("textures/npcs/default.png")
|
||||
}
|
||||
@@ -295,4 +433,75 @@ 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))
|
||||
}
|
||||
+1
-1
@@ -125,7 +125,7 @@ class CampfireBlockEntityRenderer(ctx: BlockEntityRendererProvider.Context) : Bl
|
||||
) {
|
||||
val seasonings = blockEntity.getSeasonings()
|
||||
|
||||
val gameTime = (blockEntity.level?.gameTime ?: 0) + tickDelta
|
||||
val gameTime = blockEntity.time + tickDelta
|
||||
val rotationAngle = (gameTime * ROTATION_SPEED) % 360
|
||||
|
||||
seasonings.forEachIndexed { index, seasoning ->
|
||||
|
||||
+35
@@ -16,6 +16,7 @@ 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
|
||||
@@ -177,4 +178,38 @@ 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)
|
||||
}
|
||||
}
|
||||
+23
-23
@@ -19,11 +19,13 @@ 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
|
||||
@@ -276,7 +278,9 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
|
||||
onTransitionedInto: (PosableState) -> Unit = {},
|
||||
animations: Array<PoseAnimation> = emptyArray(),
|
||||
transformedParts: Array<ModelPartTransformation> = emptyArray(),
|
||||
quirks: Array<ModelQuirk<*>> = 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
|
||||
): Pose {
|
||||
return Pose(
|
||||
poseType.name,
|
||||
@@ -288,7 +292,9 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
|
||||
namedAnimations,
|
||||
animations,
|
||||
transformedParts,
|
||||
quirks
|
||||
quirks,
|
||||
texturePhaseIn,
|
||||
texturePhaseOut
|
||||
).also {
|
||||
poses[poseType.name] = it
|
||||
}
|
||||
@@ -304,7 +310,9 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
|
||||
onTransitionedInto: (PosableState) -> Unit = {},
|
||||
animations: Array<PoseAnimation> = emptyArray(),
|
||||
transformedParts: Array<ModelPartTransformation> = emptyArray(),
|
||||
quirks: Array<ModelQuirk<*>> = 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
|
||||
): Pose {
|
||||
return Pose(
|
||||
poseName,
|
||||
@@ -316,7 +324,9 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
|
||||
namedAnimations,
|
||||
animations,
|
||||
transformedParts,
|
||||
quirks
|
||||
quirks,
|
||||
texturePhaseIn,
|
||||
texturePhaseOut
|
||||
).also {
|
||||
poses[poseName] = it
|
||||
}
|
||||
@@ -332,7 +342,9 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
|
||||
onTransitionedInto: (PosableState) -> Unit = {},
|
||||
animations: Array<PoseAnimation> = emptyArray(),
|
||||
transformedParts: Array<ModelPartTransformation> = emptyArray(),
|
||||
quirks: Array<ModelQuirk<*>> = 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
|
||||
): Pose {
|
||||
return Pose(
|
||||
poseName,
|
||||
@@ -344,7 +356,9 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
|
||||
namedAnimations,
|
||||
animations,
|
||||
transformedParts,
|
||||
quirks
|
||||
quirks,
|
||||
texturePhaseIn,
|
||||
texturePhaseOut
|
||||
).also {
|
||||
poses[poseName] = it
|
||||
}
|
||||
@@ -412,13 +426,6 @@ 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,
|
||||
@@ -453,16 +460,9 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
|
||||
val provider = bufferProvider
|
||||
if (provider != null) {
|
||||
for (layer in currentLayers) {
|
||||
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)
|
||||
}
|
||||
(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)
|
||||
val consumer = provider.getBuffer(renderLayer)
|
||||
val tint = layer.tint
|
||||
val tintRed = (tint.x * r2 * 255).toInt()
|
||||
|
||||
+3
-1
@@ -90,7 +90,9 @@ class PoseAdapter(
|
||||
animations = pose.idleAnimations,
|
||||
transformedParts = pose.transformedParts,
|
||||
quirks = pose.quirks.toTypedArray(),
|
||||
namedAnimations = pose.animations
|
||||
namedAnimations = pose.animations,
|
||||
texturePhaseIn = pose.texturePhaseIn,
|
||||
texturePhaseOut = pose.texturePhaseOut
|
||||
).also {
|
||||
it.transitions.putAll(
|
||||
pose.transitions
|
||||
|
||||
+13
-1
@@ -132,7 +132,9 @@ data class BedrockAnimation(
|
||||
val shouldLoop: Boolean,
|
||||
val animationLength: Double,
|
||||
val effects: List<BedrockEffectKeyframe>,
|
||||
val boneTimelines: Map<String, BedrockBoneTimeline>
|
||||
val boneTimelines: Map<String, BedrockBoneTimeline>,
|
||||
//This is custom data added by our custom BB plugin
|
||||
val cobblemonTextures: MutableMap<String, TreeMap<Double, CobblemonBedrockTextureKeyframe>>
|
||||
) {
|
||||
fun checkForErrors() {
|
||||
boneTimelines.forEach { (_, timeline) ->
|
||||
@@ -396,4 +398,14 @@ 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
|
||||
}
|
||||
+22
-1
@@ -20,6 +20,7 @@ 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.
|
||||
@@ -89,7 +90,27 @@ object BedrockAnimationAdapter : JsonDeserializer<BedrockAnimation> {
|
||||
)
|
||||
}
|
||||
|
||||
return BedrockAnimation(shouldLoop, animationLength, effects, boneTimelines)
|
||||
//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)
|
||||
}
|
||||
else {
|
||||
throw IllegalStateException("animation json could not be parsed")
|
||||
|
||||
+5
-1
@@ -13,6 +13,7 @@ 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
|
||||
@@ -39,7 +40,10 @@ class Pose(
|
||||
val namedAnimations: MutableMap<String, ExpressionLike> = mutableMapOf(),
|
||||
val animations: Array<PoseAnimation>,
|
||||
val transformedParts: Array<ModelPartTransformation>,
|
||||
val quirks: Array<ModelQuirk<*>>
|
||||
val quirks: Array<ModelQuirk<*>>,
|
||||
/** These are very specific to the animated texture**/
|
||||
val texturePhaseIn: Pair<TextureInterpolationType, MutableList<Double>>,
|
||||
val texturePhaseOut: Pair<TextureInterpolationType, MutableList<Double>>?,
|
||||
) {
|
||||
fun isSuitable(state: PosableState) = condition?.invoke(state) ?: true
|
||||
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
|
||||
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
|
||||
@@ -24,6 +25,7 @@ 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
|
||||
@@ -56,11 +58,11 @@ object CheckSpawnsCommand {
|
||||
return 0
|
||||
}
|
||||
|
||||
val spawner = CobblemonWorldSpawnerManager.spawnersForPlayers[player.uuid] ?: return Command.SINGLE_SUCCESS
|
||||
val spawner = player.spawner
|
||||
val bucket = SpawnBucketArgumentType.getSpawnBucket(context, name = "bucket")
|
||||
val cause = SpawnCause(spawner, player)
|
||||
|
||||
val slice = spawner.spawningZoneGenerator.generate(
|
||||
val slice = Cobblemon.spawningZoneGenerator.generate(
|
||||
spawner = spawner,
|
||||
input = SpawningZoneInput(
|
||||
cause = cause,
|
||||
@@ -74,9 +76,9 @@ object CheckSpawnsCommand {
|
||||
)
|
||||
)
|
||||
|
||||
val contexts = spawner.spawnablePositionResolver.resolve(spawner, spawner.spawnablePositionCalculators, slice)
|
||||
val contexts = Cobblemon.areaSpawnablePositionResolver.resolve(spawner, prioritizedAreaCalculators, slice)
|
||||
|
||||
val spawnProbabilities = spawner.getSpawningSelector().getProbabilities(spawner, bucket, contexts)
|
||||
val spawnProbabilities = spawner.selector.getProbabilities(spawner, bucket, contexts)
|
||||
|
||||
val spawnNames = mutableMapOf<String, MutableComponent>()
|
||||
val namedProbabilities = mutableMapOf<MutableComponent, Float>()
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
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
|
||||
@@ -19,6 +18,7 @@ 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 selection spawn region has no possible [AreaSpawnablePosition]. For example if
|
||||
* This command can fail if the randomly selected spawn region has no possible [AreaSpawnablePosition]. For example if
|
||||
* you are flying in the air
|
||||
*/
|
||||
object SpawnPokemonFromPool {
|
||||
@@ -54,47 +54,26 @@ object SpawnPokemonFromPool {
|
||||
|
||||
private fun execute(context: CommandContext<CommandSourceStack>, amount: Int): Int {
|
||||
val player = context.source.playerOrException
|
||||
val spawner = CobblemonWorldSpawnerManager.spawnersForPlayers.getValue(player.uuid)
|
||||
val spawner = player.spawner
|
||||
|
||||
var spawnsTriggered = 0
|
||||
|
||||
// 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()) {
|
||||
repeat(times = amount) {
|
||||
val spawnCause = SpawnCause(spawner = spawner, entity = player)
|
||||
val results = spawner.runForArea(spawner.getZoneInput(spawnCause) ?: return@repeat, maxSpawns = 1)
|
||||
if (results.isEmpty()) {
|
||||
player.sendSystemMessage(UNABLE_TO_SPAWN.red())
|
||||
continue
|
||||
return@repeat
|
||||
}
|
||||
|
||||
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 {
|
||||
results.forEach {
|
||||
if (it is EntitySpawnResult) {
|
||||
for (entity in it.entities) {
|
||||
player.sendSystemMessage(commandLang("spawnpokemonfrompool.success", entity.effectiveName()).green())
|
||||
spawnsTriggered++
|
||||
player.sendSystemMessage(commandLang("spawnpokemonfrompool", entity.effectiveName()).green())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spawnAction.complete()
|
||||
spawnsTriggered++
|
||||
}
|
||||
|
||||
return spawnsTriggered
|
||||
|
||||
@@ -155,6 +155,9 @@ class CobblemonConfig {
|
||||
@CobblemonConfigField(Category.Spawning, lang = "pokemon_per_chunk", SERVER)
|
||||
var pokemonPerChunk = 1F
|
||||
|
||||
@CobblemonConfigField(Category.Spawning, lang = "poke_snack_pokemon_per_chunk", SERVER)
|
||||
var pokeSnackPokemonPerChunk = 2F
|
||||
|
||||
@CobblemonConfigField(Category.PassiveStatus, lang = "passive_statuses", SERVER)
|
||||
var passiveStatuses = mutableMapOf(
|
||||
Statuses.POISON.configEntry(),
|
||||
@@ -273,7 +276,7 @@ class CobblemonConfig {
|
||||
var summaryPokemonFollowCursor = true
|
||||
|
||||
@CobblemonConfigField(Category.Interface, lang = "party_portrait_animations", CLIENT)
|
||||
var partyPortraitAnimations = PortraitStyle.NEVER_ANIMATE
|
||||
var partyPortraitAnimations = PortraitStyle.ANIMATE_SELECTED
|
||||
|
||||
@CobblemonConfigField(Category.Riding, lang = "third_person_view_bobbing", CLIENT)
|
||||
var thirdPersonViewBobbing = true
|
||||
|
||||
@@ -11,16 +11,17 @@ package com.cobblemon.mod.common.entity.ai
|
||||
import com.cobblemon.mod.common.CobblemonMemories
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
|
||||
import com.cobblemon.mod.common.util.toBlockPos
|
||||
import com.google.common.collect.ImmutableMap
|
||||
import java.util.UUID
|
||||
import kotlin.math.pow
|
||||
import net.minecraft.server.level.ServerLevel
|
||||
import net.minecraft.world.entity.MoverType
|
||||
import net.minecraft.util.Mth
|
||||
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.ai.memory.WalkTarget
|
||||
import net.minecraft.world.phys.Vec3
|
||||
import java.util.*
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
class FollowHerdLeaderTask : Behavior<PokemonEntity>(
|
||||
ImmutableMap.of(
|
||||
@@ -34,7 +35,7 @@ class FollowHerdLeaderTask : Behavior<PokemonEntity>(
|
||||
var tooFar = 8F
|
||||
var closeEnough = 4F
|
||||
/** Chance per tick of following the herd leader. */
|
||||
val chance = 1 / 20F
|
||||
val chance = 1 / 60F
|
||||
|
||||
override fun checkExtraStartConditions(level: ServerLevel, owner: PokemonEntity): Boolean {
|
||||
leader = level.getEntity(owner.brain.getMemory(CobblemonMemories.HERD_LEADER).map(UUID::fromString).orElse(null) ?: return false) as? PokemonEntity
|
||||
@@ -50,16 +51,19 @@ class FollowHerdLeaderTask : Behavior<PokemonEntity>(
|
||||
|
||||
override fun tick(level: ServerLevel, entity: PokemonEntity, gameTime: Long) {
|
||||
val leader = leader ?: return
|
||||
|
||||
if (entity.brain.hasMemoryValue(MemoryModuleType.WALK_TARGET)) {
|
||||
return // We're busy goin' n' shit
|
||||
}
|
||||
|
||||
val moveControl = entity.moveControl as? PokemonMoveControl ?: return
|
||||
val leaderMoveControl = leader.moveControl as? PokemonMoveControl ?: return
|
||||
|
||||
if (leader.distanceTo(entity) > tooFar) {
|
||||
entity.brain.setMemory(
|
||||
MemoryModuleType.WALK_TARGET,
|
||||
CobblemonWalkTarget(
|
||||
pos = leader.blockPosition(),
|
||||
pos = leader.position().add(getRandomOffset(entity, level)).toBlockPos(),
|
||||
speedModifier = 0.4F,
|
||||
completionRange = closeEnough.toInt()
|
||||
)
|
||||
@@ -69,10 +73,11 @@ class FollowHerdLeaderTask : Behavior<PokemonEntity>(
|
||||
entity.brain.eraseMemory(CobblemonMemories.HERD_LEADER) // We can't go where this guy's going
|
||||
return
|
||||
}
|
||||
|
||||
entity.brain.setMemory(
|
||||
MemoryModuleType.WALK_TARGET,
|
||||
CobblemonWalkTarget(
|
||||
pos = leader.blockPosition(),
|
||||
pos = leader.position().add(getRandomOffset(entity, level)).toBlockPos(),
|
||||
speedModifier = 0.3F,
|
||||
completionRange = closeEnough.toInt()
|
||||
)
|
||||
@@ -83,7 +88,7 @@ class FollowHerdLeaderTask : Behavior<PokemonEntity>(
|
||||
entity.brain.setMemory(
|
||||
MemoryModuleType.WALK_TARGET,
|
||||
CobblemonWalkTarget(
|
||||
pos = leader.blockPosition(),
|
||||
pos = leader.position().add(getRandomOffset(entity, level)).toBlockPos(),
|
||||
speedModifier = 0.4F,
|
||||
completionRange = 1
|
||||
)
|
||||
@@ -105,14 +110,15 @@ class FollowHerdLeaderTask : Behavior<PokemonEntity>(
|
||||
// Go to the leader's walk target, roughly.
|
||||
val walkTarget = leader.brain.getMemory(MemoryModuleType.WALK_TARGET).orElse(null) ?: return
|
||||
|
||||
if (entity.distanceToSqr(walkTarget.target.currentPosition()) < leader.distanceToSqr(walkTarget.target.currentPosition())) {
|
||||
return // We're already closer to the walk target than the leader is, so no need to move, if anything we should get out of the way
|
||||
// We're already closer to the walk target than the leader is, so no need to move, if anything we should get out of the way
|
||||
if (entity.distanceToSqr(walkTarget.target.currentPosition()) + closeEnough < leader.distanceToSqr(walkTarget.target.currentPosition())) {
|
||||
return
|
||||
}
|
||||
|
||||
entity.brain.setMemory(
|
||||
MemoryModuleType.WALK_TARGET,
|
||||
CobblemonWalkTarget(
|
||||
pos = walkTarget.target.currentBlockPosition(),
|
||||
pos = walkTarget.target.currentPosition().add(getRandomOffset(entity, level)).toBlockPos(),
|
||||
speedModifier = walkTarget.speedModifier,
|
||||
nodeTypeFilter = (walkTarget as? CobblemonWalkTarget)?.nodeTypeFilter ?: { true },
|
||||
completionRange = closeEnough.toInt()
|
||||
@@ -120,4 +126,23 @@ class FollowHerdLeaderTask : Behavior<PokemonEntity>(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getRandomOffset(entity: PokemonEntity, level: ServerLevel): Vec3 {
|
||||
val leader = leader ?: return Vec3.ZERO
|
||||
|
||||
val minRange = closeEnough.toDouble()
|
||||
val maxRange = (tooFar - closeEnough / 2).toDouble()
|
||||
val randomDistance = Mth.nextDouble(level.random, minRange, maxRange)
|
||||
val randomAngle = Mth.nextDouble(level.random, 0.0, 2 * Math.PI)
|
||||
|
||||
var offset = Vec3(cos(randomAngle) * randomDistance, 0.0, sin(randomAngle) * randomDistance)
|
||||
|
||||
if ((entity.isInLiquid && leader.isInLiquid) || (entity.isFlying() && leader.isFlying())) {
|
||||
val verticalRange = randomDistance * 0.5
|
||||
val randomY = Mth.nextDouble(level.random, -verticalRange, verticalRange)
|
||||
offset = offset.add(0.0, randomY, 0.0)
|
||||
}
|
||||
|
||||
return offset
|
||||
}
|
||||
}
|
||||
+14
-5
@@ -26,6 +26,7 @@ import com.cobblemon.mod.common.api.spawning.fishing.FishingSpawnCause
|
||||
import com.cobblemon.mod.common.api.spawning.influence.BucketNormalizingInfluence
|
||||
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence
|
||||
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence.Companion.TYPICAL_VARIATION
|
||||
import com.cobblemon.mod.common.api.spawning.position.FishingSpawnablePosition
|
||||
import com.cobblemon.mod.common.api.text.red
|
||||
import com.cobblemon.mod.common.client.sound.EntitySoundTracker
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
@@ -771,11 +772,16 @@ class PokeRodFishingBobberEntity(type: EntityType<out PokeRodFishingBobberEntity
|
||||
lureLevel = lureLevel,
|
||||
)
|
||||
|
||||
val result = spawner.getSpawnAction(
|
||||
val spawnablePosition = FishingSpawnablePosition(
|
||||
cause = spawnCause,
|
||||
world = level() as ServerLevel,
|
||||
pos = position().toBlockPos(),
|
||||
influences = listOf(PlayerLevelRangeInfluence(player, TYPICAL_VARIATION), bucketInfluence)
|
||||
influences = mutableListOf(PlayerLevelRangeInfluence(player, TYPICAL_VARIATION), bucketInfluence)
|
||||
)
|
||||
|
||||
val result = spawner.calculateSpawnActionForPosition(
|
||||
cause = spawnCause,
|
||||
spawnablePosition = spawnablePosition
|
||||
)
|
||||
|
||||
this.plannedSpawnAction = result
|
||||
@@ -811,8 +817,11 @@ class PokeRodFishingBobberEntity(type: EntityType<out PokeRodFishingBobberEntity
|
||||
// create accessory splash particle when you fish something up
|
||||
particleEntityHandler(this, ResourceLocation.fromNamespaceAndPath("cobblemon", "accessory_fish_splash"))
|
||||
|
||||
var baitId = PokerodItem.getBaitStackOnRod(this.bobberBait).takeUnless { it.isEmpty }?.itemHolder?.unwrapKey()
|
||||
?.orElse(null)?.location() ?: "empty_bait".asIdentifierDefaultingNamespace()
|
||||
val baitId =
|
||||
if (bobberBait.isEmpty)
|
||||
"empty_bait".asIdentifierDefaultingNamespace()
|
||||
else
|
||||
bobberBait.itemHolder.unwrapKey().map { it.location() }.orElse("empty_bait".asIdentifierDefaultingNamespace())
|
||||
val pokemonId = spawnedPokemon.pokemon.species.resourceIdentifier
|
||||
CobblemonCriteria.REEL_IN_POKEMON.trigger(player, ReelInPokemonContext(pokemonId, baitId))
|
||||
|
||||
@@ -850,7 +859,7 @@ class PokeRodFishingBobberEntity(type: EntityType<out PokeRodFishingBobberEntity
|
||||
BobberSpawnPokemonEvent.Post(
|
||||
this,
|
||||
spawnAction,
|
||||
rodItemStack,
|
||||
bobberBait,
|
||||
entity
|
||||
)
|
||||
)
|
||||
|
||||
@@ -120,6 +120,7 @@ import com.cobblemon.mod.common.util.math.geometry.toRadians
|
||||
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules
|
||||
import com.mojang.serialization.Codec
|
||||
import com.mojang.serialization.Dynamic
|
||||
import net.minecraft.client.Minecraft
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.CompletableFuture
|
||||
@@ -2407,8 +2408,12 @@ open class PokemonEntity(
|
||||
|
||||
fun rideFovMult(): Float {
|
||||
val driver = this.controllingPassenger as? Player ?: return 1.0f
|
||||
val fovEffectScale = Minecraft.getInstance().options.fovEffectScale().get().toFloat()
|
||||
|
||||
return ifRidingAvailableSupply(fallback = 1.0f) { behaviour, settings, state ->
|
||||
behaviour.rideFovMultiplier(settings, state, this, driver)
|
||||
val rideFov = behaviour.rideFovMultiplier(settings, state, this, driver)
|
||||
|
||||
1f + (rideFov - 1f) * fovEffectScale
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -45,10 +45,12 @@ class DrowsySensor : Sensor<PokemonEntity>(20) {
|
||||
entity.brain.eraseMemory(CobblemonMemories.POKEMON_DROWSY)
|
||||
}
|
||||
} else {
|
||||
if (!isDrowsy && shouldBeDrowsy) {
|
||||
if (!isDrowsy && shouldBeDrowsy && entity.random.nextFloat() < rest.drowsyChance) {
|
||||
entity.brain.setMemory(CobblemonMemories.POKEMON_DROWSY, true)
|
||||
} else if (isDrowsy && !shouldBeDrowsy) {
|
||||
entity.brain.eraseMemory(CobblemonMemories.POKEMON_DROWSY)
|
||||
} else if (isDrowsy && entity.random.nextFloat() < rest.rouseChance) {
|
||||
entity.brain.eraseMemory(CobblemonMemories.POKEMON_DROWSY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
package com.cobblemon.mod.common.events
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.battles.BattleRegistry
|
||||
import com.cobblemon.mod.common.util.party
|
||||
import net.minecraft.server.MinecraftServer
|
||||
@@ -17,7 +16,6 @@ object ServerTickHandler {
|
||||
private var secondsTick = 0
|
||||
|
||||
fun onTick(server: MinecraftServer) {
|
||||
Cobblemon.bestSpawner.spawnerManagers.forEach { it.onServerTick() }
|
||||
BattleRegistry.tick()
|
||||
|
||||
secondsTick++
|
||||
|
||||
+1
@@ -85,6 +85,7 @@ class CampfirePotRecipeCategory(registration: IRecipeCategoryRegistration) : IRe
|
||||
if (recipe.seasoningProcessors.isNotEmpty()) {
|
||||
val processedSeasonings = EXAMPLE_SEASONINGS.filter { item ->
|
||||
var valid = true
|
||||
if (!item.`is`(recipe.seasoningTag)) return@filter false
|
||||
recipe.seasoningProcessors.forEach {
|
||||
if (!it.consumesItem(item)) {
|
||||
valid = false
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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.item
|
||||
|
||||
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
|
||||
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
|
||||
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
|
||||
import com.cobblemon.mod.common.item.battle.BagItem
|
||||
import com.cobblemon.mod.common.item.battle.SimpleBagItemLike
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.item.Items
|
||||
|
||||
class MoomooMilk(properties: Properties) : CobblemonItem(properties), SimpleBagItemLike {
|
||||
override val bagItem = object : BagItem {
|
||||
override val itemName = "item.cobblemon.moomoo_milk"
|
||||
override val returnItem = Items.GLASS_BOTTLE
|
||||
|
||||
override fun canUse(stack: ItemStack, battle: PokemonBattle, target: BattlePokemon): Boolean {
|
||||
return target.health > 0
|
||||
}
|
||||
|
||||
override fun getShowdownInput(actor: BattleActor, battlePokemon: BattlePokemon, data: String?): String {
|
||||
return "clear_boost"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,10 @@ interface BagItemLike {
|
||||
|
||||
battlePokemon.actor.forceChoose(BagItemActionResponse(bagItem, battlePokemon))
|
||||
val stackName = BuiltInRegistries.ITEM.getKey(stack.item)
|
||||
stack.consume(1, player)
|
||||
if (!player.hasInfiniteMaterials()) {
|
||||
stack.shrink(1)
|
||||
battlePokemon.actor.itemsUsed.add(bagItem)
|
||||
}
|
||||
CobblemonCriteria.POKEMON_INTERACT.trigger(player, PokemonInteractContext(battlePokemon.effectedPokemon.species.resourceIdentifier, stackName))
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -10,15 +10,23 @@ package com.cobblemon.mod.common.item.crafting
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonRecipeSerializers
|
||||
import com.cobblemon.mod.common.CobblemonRecipeTypes
|
||||
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
|
||||
import com.cobblemon.mod.common.util.codec.CodecUtils.createByStringCodec
|
||||
import com.cobblemon.mod.common.util.readIdentifier
|
||||
import com.cobblemon.mod.common.util.readString
|
||||
import com.cobblemon.mod.common.util.writeIdentifier
|
||||
import com.cobblemon.mod.common.util.writeString
|
||||
import com.mojang.serialization.Codec
|
||||
import com.mojang.serialization.MapCodec
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder
|
||||
import net.minecraft.core.NonNullList
|
||||
import net.minecraft.core.registries.Registries
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf
|
||||
import net.minecraft.network.codec.StreamCodec
|
||||
import net.minecraft.resources.ResourceKey
|
||||
import net.minecraft.tags.TagEntry
|
||||
import net.minecraft.tags.TagKey
|
||||
import net.minecraft.world.item.Item
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.item.crafting.CraftingInput
|
||||
import net.minecraft.world.item.crafting.Ingredient
|
||||
@@ -31,6 +39,7 @@ class CookingPotRecipe(
|
||||
override val result: ItemStack,
|
||||
override val groupName: String,
|
||||
override val category: CookingPotBookCategory,
|
||||
override val seasoningTag: TagKey<Item>,
|
||||
override val seasoningProcessors: List<SeasoningProcessor>
|
||||
) : CookingPotRecipeBase {
|
||||
override fun getType() = CobblemonRecipeTypes.COOKING_POT_COOKING
|
||||
@@ -56,7 +65,8 @@ class CookingPotRecipe(
|
||||
ShapedRecipePattern.MAP_CODEC.forGetter { recipe -> recipe.pattern },
|
||||
ItemStack.STRICT_CODEC.fieldOf("result").forGetter { recipe -> recipe.result },
|
||||
Codec.STRING.optionalFieldOf("group", "").forGetter { recipe -> recipe.group },
|
||||
CookingPotBookCategory.Companion.CODEC.fieldOf("category").orElse(CookingPotBookCategory.MISC).forGetter { recipe -> recipe.category },
|
||||
CookingPotBookCategory.CODEC.fieldOf("category").orElse(CookingPotBookCategory.MISC).forGetter { recipe -> recipe.category },
|
||||
TagKey.codec(Registries.ITEM).fieldOf("seasoningTag").orElse(CobblemonItemTags.EMPTY).forGetter { recipe -> recipe.seasoningTag },
|
||||
createByStringCodec<SeasoningProcessor>(
|
||||
{ SeasoningProcessor.processors[it] },
|
||||
{ it.type },
|
||||
@@ -70,18 +80,20 @@ class CookingPotRecipe(
|
||||
private fun fromNetwork(buffer: RegistryFriendlyByteBuf): CookingPotRecipe {
|
||||
val group = buffer.readUtf()
|
||||
val category = buffer.readEnum(CookingPotBookCategory::class.java)
|
||||
val seasoningTag = TagKey.create(Registries.ITEM, buffer.readIdentifier())
|
||||
val pattern = ShapedRecipePattern.STREAM_CODEC.decode(buffer)
|
||||
val result = ItemStack.STREAM_CODEC.decode(buffer)
|
||||
val seasoningProcessors = buffer.readList {
|
||||
val type = buffer.readString()
|
||||
SeasoningProcessor.processors[type] ?: error("Unknown seasoning processor: $type")
|
||||
}
|
||||
return CookingPotRecipe(pattern, result, group, category, seasoningProcessors)
|
||||
return CookingPotRecipe(pattern, result, group, category, seasoningTag, seasoningProcessors)
|
||||
}
|
||||
|
||||
private fun toNetwork(buffer: RegistryFriendlyByteBuf, recipe: CookingPotRecipe) {
|
||||
buffer.writeUtf(recipe.groupName)
|
||||
buffer.writeEnum(recipe.category)
|
||||
buffer.writeIdentifier(recipe.seasoningTag.location)
|
||||
ShapedRecipePattern.STREAM_CODEC.encode(buffer, recipe.pattern)
|
||||
ItemStack.STREAM_CODEC.encode(buffer, recipe.result)
|
||||
buffer.writeCollection(recipe.seasoningProcessors) { _, it ->
|
||||
|
||||
@@ -10,6 +10,8 @@ package com.cobblemon.mod.common.item.crafting
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonItems
|
||||
import net.minecraft.core.HolderLookup
|
||||
import net.minecraft.tags.TagKey
|
||||
import net.minecraft.world.item.Item
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.item.crafting.CraftingInput
|
||||
import net.minecraft.world.item.crafting.Recipe
|
||||
@@ -19,6 +21,7 @@ interface CookingPotRecipeBase : Recipe<CraftingInput> {
|
||||
val result: ItemStack
|
||||
val groupName: String
|
||||
val category: CookingPotBookCategory
|
||||
val seasoningTag: TagKey<Item>
|
||||
val seasoningProcessors: List<SeasoningProcessor>
|
||||
|
||||
override fun getGroup() = groupName
|
||||
|
||||
+11
-1
@@ -10,16 +10,22 @@ package com.cobblemon.mod.common.item.crafting
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonRecipeSerializers
|
||||
import com.cobblemon.mod.common.CobblemonRecipeTypes
|
||||
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
|
||||
import com.cobblemon.mod.common.util.codec.CodecUtils.createByStringCodec
|
||||
import com.cobblemon.mod.common.util.readIdentifier
|
||||
import com.cobblemon.mod.common.util.readString
|
||||
import com.cobblemon.mod.common.util.writeIdentifier
|
||||
import com.cobblemon.mod.common.util.writeString
|
||||
import com.mojang.serialization.Codec
|
||||
import com.mojang.serialization.DataResult
|
||||
import com.mojang.serialization.MapCodec
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder
|
||||
import net.minecraft.core.NonNullList
|
||||
import net.minecraft.core.registries.Registries
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf
|
||||
import net.minecraft.network.codec.StreamCodec
|
||||
import net.minecraft.tags.TagKey
|
||||
import net.minecraft.world.item.Item
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.item.crafting.*
|
||||
import net.minecraft.world.level.Level
|
||||
@@ -29,6 +35,7 @@ class CookingPotShapelessRecipe(
|
||||
override val category: CookingPotBookCategory,
|
||||
override val result: ItemStack,
|
||||
private val ingredients: NonNullList<Ingredient>,
|
||||
override val seasoningTag: TagKey<Item>,
|
||||
override val seasoningProcessors: List<SeasoningProcessor>
|
||||
) : Recipe<CraftingInput>, CookingPotRecipeBase {
|
||||
override fun getType(): RecipeType<CookingPotShapelessRecipe> = CobblemonRecipeTypes.COOKING_POT_SHAPELESS
|
||||
@@ -68,6 +75,7 @@ class CookingPotShapelessRecipe(
|
||||
else -> DataResult.success(NonNullList.of(Ingredient.EMPTY, *ingredients))
|
||||
}
|
||||
}, { DataResult.success(it) }).forGetter { it.ingredients },
|
||||
TagKey.codec(Registries.ITEM).fieldOf("seasoningTag").orElse(CobblemonItemTags.EMPTY).forGetter { recipe -> recipe.seasoningTag },
|
||||
createByStringCodec<SeasoningProcessor>(
|
||||
{ SeasoningProcessor.processors[it] },
|
||||
{ it.type },
|
||||
@@ -82,6 +90,7 @@ class CookingPotShapelessRecipe(
|
||||
private fun fromNetwork(buffer: RegistryFriendlyByteBuf): CookingPotShapelessRecipe {
|
||||
val group = buffer.readUtf()
|
||||
val category = buffer.readEnum(CookingPotBookCategory::class.java)
|
||||
val seasoningTag = TagKey.create(Registries.ITEM, buffer.readIdentifier())
|
||||
val size = buffer.readVarInt()
|
||||
val ingredients = NonNullList.withSize(size, Ingredient.EMPTY)
|
||||
ingredients.replaceAll { Ingredient.CONTENTS_STREAM_CODEC.decode(buffer) }
|
||||
@@ -90,12 +99,13 @@ class CookingPotShapelessRecipe(
|
||||
val type = buffer.readString()
|
||||
SeasoningProcessor.processors[type] ?: error("Unknown seasoning processor: $type")
|
||||
}
|
||||
return CookingPotShapelessRecipe(group, category, result, ingredients, seasoningProcessors)
|
||||
return CookingPotShapelessRecipe(group, category, result, ingredients, seasoningTag, seasoningProcessors)
|
||||
}
|
||||
|
||||
private fun toNetwork(buffer: RegistryFriendlyByteBuf, recipe: CookingPotShapelessRecipe) {
|
||||
buffer.writeUtf(recipe.group)
|
||||
buffer.writeEnum(recipe.category)
|
||||
buffer.writeIdentifier(recipe.seasoningTag.location)
|
||||
buffer.writeVarInt(recipe.ingredients.size)
|
||||
recipe.ingredients.forEach { ingredient ->
|
||||
Ingredient.CONTENTS_STREAM_CODEC.encode(buffer, ingredient)
|
||||
|
||||
@@ -12,7 +12,7 @@ import com.bedrockk.molang.runtime.value.DoubleValue
|
||||
import com.cobblemon.mod.common.api.molang.ObjectValue
|
||||
|
||||
class CombatBehaviour {
|
||||
var willDefendSelf = true
|
||||
var willDefendSelf = false
|
||||
var willFlee = true
|
||||
var willDefendOwner = false
|
||||
var fightsMelee = true
|
||||
|
||||
@@ -33,6 +33,8 @@ class RestBehaviour {
|
||||
val depth = SleepDepth.normal
|
||||
val canSeeSky: Boolean? = null
|
||||
val skyLight: IntRange? = null
|
||||
val drowsyChance: Float = 1 / 60F // Once per minute on average. Drowsy sensor runs once per second.
|
||||
val rouseChance: Float = 1 / 120F // Once every two minutes on average. Drowsy sensor runs once per second.
|
||||
|
||||
val willSleepOnBed = false
|
||||
|
||||
|
||||
+3
-2
@@ -130,8 +130,9 @@ override fun handleStartInstruction(pokemon: BattlePokemon, battle: PokemonBattl
|
||||
if (this.takeItemEffect.contains(effectId) && this.giveItemEffect.contains(effectId) && !consumeHeldItems) {
|
||||
return
|
||||
}
|
||||
// Prevent duping
|
||||
if (battle.isPvP && this.takeItemEffect.contains(effectId) || this.giveItemEffect.contains(effectId)) {
|
||||
// TODO: This is strictly a temporary fix to prevent duping, we should determine a proper solution where we
|
||||
// can allow physical item swapping while covering the proper use cases here.
|
||||
if (this.takeItemEffect.contains(effectId) || this.giveItemEffect.contains(effectId)) {
|
||||
return
|
||||
}
|
||||
// Block item swapping in PVP until we have a rule
|
||||
|
||||
@@ -151,10 +151,15 @@ object DataKeys {
|
||||
const val HEALER_MACHINE_INFINITE = "MachineInfinite"
|
||||
|
||||
// Cooking
|
||||
const val AMOUNT_SPAWNED = "AmountSpawned"
|
||||
const val FLAVOUR = "Flavour"
|
||||
const val BAIT_EFFECTS = "BaitEffects"
|
||||
const val INGREDIENTS = "Ingredients"
|
||||
|
||||
const val PLACED_BY = "PlacedBy"
|
||||
|
||||
const val TICKS_UNTIL_NEXT_SPAWN = "TicksUntilNextSpawn"
|
||||
|
||||
// Pokémon Item
|
||||
const val POKEMON_ITEM_SPECIES = "species"
|
||||
const val POKEMON_ITEM_ASPECTS = "aspects"
|
||||
|
||||
@@ -17,14 +17,20 @@ import kotlin.math.floor
|
||||
import kotlin.math.min
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.Direction
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.core.registries.Registries
|
||||
import net.minecraft.nbt.NbtOps
|
||||
import net.minecraft.nbt.Tag
|
||||
import net.minecraft.network.syncher.EntityDataAccessor
|
||||
import net.minecraft.network.syncher.SynchedEntityData
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.tags.TagKey
|
||||
import net.minecraft.util.ExtraCodecs
|
||||
import net.minecraft.world.entity.Entity
|
||||
import net.minecraft.world.entity.LivingEntity
|
||||
import net.minecraft.world.entity.ai.memory.MemoryModuleType
|
||||
import net.minecraft.world.entity.ai.memory.MemoryStatus
|
||||
import net.minecraft.world.entity.schedule.Activity
|
||||
import net.minecraft.world.level.Level
|
||||
import net.minecraft.world.phys.AABB
|
||||
import net.minecraft.world.phys.Vec3
|
||||
@@ -40,8 +46,45 @@ fun Entity.makeEmptyBrainDynamic() = Dynamic(
|
||||
|
||||
fun Entity.effectiveName() = this.displayName ?: this.name
|
||||
|
||||
fun LivingEntity.hasMemory(memoryIdentifier: ResourceLocation): Boolean {
|
||||
val registry = BuiltInRegistries.MEMORY_MODULE_TYPE
|
||||
val memoryType = registry.get(memoryIdentifier)
|
||||
return brain.checkMemory(memoryType, MemoryStatus.VALUE_PRESENT)
|
||||
}
|
||||
|
||||
fun LivingEntity.hasMemory(tagKey: TagKey<MemoryModuleType<*>>): Boolean {
|
||||
val registry = BuiltInRegistries.MEMORY_MODULE_TYPE
|
||||
val memoryTypesInTag = registry.getTag(tagKey).orElse(null) ?: return false
|
||||
return memoryTypesInTag.any { brain.checkMemory(it.value(), MemoryStatus.VALUE_PRESENT) }
|
||||
}
|
||||
|
||||
fun LivingEntity.hasMemoryFromString(memory: String): Boolean {
|
||||
val key = memory.asIdentifierOrTag(Registries.MEMORY_MODULE_TYPE)
|
||||
return key.map(
|
||||
{ hasMemory(memoryIdentifier = it) },
|
||||
{ hasMemory(tagKey = it) }
|
||||
)
|
||||
}
|
||||
|
||||
fun LivingEntity.isDoingActivity(activityIdentifier: ResourceLocation): Boolean {
|
||||
val registry = BuiltInRegistries.ACTIVITY
|
||||
val activity = registry.get(activityIdentifier) ?: return false
|
||||
return brain.isActive(activity)
|
||||
}
|
||||
|
||||
fun LivingEntity.isDoingActivity(tagKey: TagKey<Activity>): Boolean {
|
||||
val registry = BuiltInRegistries.ACTIVITY
|
||||
val activitiesInTag = registry.getTag(tagKey).orElse(null) ?: return false
|
||||
return activitiesInTag.any { brain.isActive(it.value()) }
|
||||
}
|
||||
|
||||
fun LivingEntity.isDoingActivityFromString(activity: String): Boolean {
|
||||
val key = activity.asIdentifierOrTag(Registries.ACTIVITY)
|
||||
return key.map(
|
||||
{ isDoingActivity(activityIdentifier = it) },
|
||||
{ isDoingActivity(tagKey = it) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun getPosScore(level: Level, entity: Entity, box: AABB, pos: BlockPos): Int {
|
||||
// position the hitbox in the xz center of the block
|
||||
|
||||
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.util
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.CobblemonSounds
|
||||
import com.cobblemon.mod.common.PlayerSpawnerGetter
|
||||
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
|
||||
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
|
||||
import com.cobblemon.mod.common.api.dialogue.ActiveDialogue
|
||||
@@ -17,6 +18,7 @@ import com.cobblemon.mod.common.api.dialogue.Dialogue
|
||||
import com.cobblemon.mod.common.api.dialogue.DialogueManager
|
||||
import com.cobblemon.mod.common.api.reactive.Observable.Companion.filter
|
||||
import com.cobblemon.mod.common.api.reactive.Observable.Companion.takeFirst
|
||||
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner
|
||||
import com.cobblemon.mod.common.battles.BattleRegistry
|
||||
import com.cobblemon.mod.common.battles.TeamManager
|
||||
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.RequestPCBoxWallpapersPacket
|
||||
@@ -98,6 +100,8 @@ fun ServerPlayer.didSleep(): Boolean {
|
||||
}
|
||||
|
||||
fun ServerPlayer.isInBattle() = BattleRegistry.getBattleByParticipatingPlayer(this) != null
|
||||
val ServerPlayer.spawner: PlayerSpawner
|
||||
get() = (this as PlayerSpawnerGetter).getPlayerSpawner()
|
||||
fun ServerPlayer.getBattleState(): Pair<PokemonBattle, BattleActor>? {
|
||||
val battle = BattleRegistry.getBattleByParticipatingPlayer(this)
|
||||
if (battle != null) {
|
||||
|
||||
@@ -10,6 +10,11 @@ package com.cobblemon.mod.common.util
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
|
||||
import com.mojang.datafixers.util.Either
|
||||
import net.minecraft.core.Registry
|
||||
import net.minecraft.resources.ResourceKey
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.tags.TagKey
|
||||
|
||||
const val QUOTE = '"'
|
||||
|
||||
@@ -62,6 +67,15 @@ fun String.splitMap(delimiter: String, assigner: String) : MutableList<Pair<Stri
|
||||
return result
|
||||
}
|
||||
|
||||
fun <T, U : Registry<T>> String.asIdentifierOrTag(resourceKey: ResourceKey<U>): Either<ResourceLocation, TagKey<T>> {
|
||||
return if (this.startsWith("#")) {
|
||||
val tagLocation = ResourceLocation.parse(this.substring(1))
|
||||
Either.right(TagKey.create<T>(resourceKey, tagLocation))
|
||||
} else {
|
||||
Either.left(ResourceLocation.parse(this))
|
||||
}
|
||||
}
|
||||
|
||||
fun String.isLaterVersion(otherVersion: String): Boolean {
|
||||
if (this === otherVersion) {
|
||||
return false
|
||||
|
||||
+9
-4
@@ -8,10 +8,8 @@
|
||||
|
||||
package com.cobblemon.mod.common.util.adapters
|
||||
|
||||
import com.cobblemon.mod.common.client.render.AnimatedModelTextureSupplier
|
||||
import com.cobblemon.mod.common.client.render.ModelTextureSupplier
|
||||
import com.cobblemon.mod.common.client.render.StaticModelTextureSupplier
|
||||
import com.cobblemon.mod.common.client.render.VariableModelTextureSupplier
|
||||
import com.cobblemon.mod.common.client.render.*
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
@@ -47,6 +45,13 @@ object ModelTextureSupplierAdapter : JsonDeserializer<ModelTextureSupplier> {
|
||||
frames = frames,
|
||||
interpolation = interpolation
|
||||
)
|
||||
} else if (json.isJsonArray) {
|
||||
val jsonArray = json as JsonArray
|
||||
val textures = mutableListOf<ResourceLocation>()
|
||||
jsonArray.forEach {
|
||||
textures.add(ResourceLocation.parse(it.asString))
|
||||
}
|
||||
return KeyframableTextureSupplier(textures)
|
||||
} else {
|
||||
throw IllegalArgumentException("Invalid JSON provided for model texture, it was of type ${json::class.simpleName} instead of a String or Object.")
|
||||
}
|
||||
|
||||
+2
-2
@@ -86,8 +86,8 @@ internal data class ClientPokemonP3(
|
||||
Optional.ofNullable(pokemon.heldItemVisible),
|
||||
Optional.ofNullable(pokemon.cosmeticItem.takeIf { !it.isEmpty }),
|
||||
Optional.ofNullable(pokemon.activeMark?.identifier),
|
||||
pokemon.marks.map { it.identifier }.toSet(),
|
||||
pokemon.potentialMarks.map { it.identifier }.toSet(),
|
||||
pokemon.marks.toList().map { it.identifier }.toSet(),
|
||||
pokemon.potentialMarks.toList().map { it.identifier }.toSet(),
|
||||
pokemon.markings,
|
||||
pokemon.getRideBoosts().mapKeys { it.key.name },
|
||||
pokemon.currentFullness,
|
||||
|
||||
@@ -632,8 +632,9 @@
|
||||
"item.cobblemon.dire_hit": "Dire Hit",
|
||||
"item.cobblemon.dire_hit.tooltip": "Temporarily boosts a Pokémon's critical hit ratio when used in battle",
|
||||
"item.cobblemon.guard_spec": "Guard Spec",
|
||||
"item.cobblemon.moomoo_milk": "Moomoo Milk",
|
||||
"item.cobblemon.guard_spec.tooltip": "Temporarily prevents stat reduction when used in battle",
|
||||
"item.cobblemon.moomoo_milk": "Moomoo Milk",
|
||||
"item.cobblemon.moomoo_milk.tooltip": "Clears all stat changes of the target Pokémon when used in battle.",
|
||||
|
||||
"item.cobblemon.full_heal": "Full Heal",
|
||||
"item.cobblemon.full_heal.tooltip": "Fully heals any status problems of a Pokémon",
|
||||
@@ -2201,9 +2202,9 @@
|
||||
"item.cobblemon.babiri_berry": "Babiri Berry",
|
||||
"item.cobblemon.babiri_berry.tooltip": "Halves damage taken from a supereffective Steel-type attack",
|
||||
"item.cobblemon.belue_berry": "Belue Berry",
|
||||
"item.cobblemon.belue_berry.tooltip": "Cannot be eaten by the holder",
|
||||
"item.cobblemon.belue_berry.tooltip": "Cannot be eaten during battles",
|
||||
"item.cobblemon.bluk_berry": "Bluk Berry",
|
||||
"item.cobblemon.bluk_berry.tooltip": "Cannot be eaten by the holder",
|
||||
"item.cobblemon.bluk_berry.tooltip": "Cannot be eaten during battles",
|
||||
"item.cobblemon.charti_berry": "Charti Berry",
|
||||
"item.cobblemon.charti_berry.tooltip": "Halves damage taken from a supereffective Rock-type attack",
|
||||
"item.cobblemon.cheri_berry": "Cheri Berry",
|
||||
@@ -2219,11 +2220,11 @@
|
||||
"item.cobblemon.colbur_berry": "Colbur Berry",
|
||||
"item.cobblemon.colbur_berry.tooltip": "Halves damage taken from a supereffective Dark-type attack",
|
||||
"item.cobblemon.cornn_berry": "Cornn Berry",
|
||||
"item.cobblemon.cornn_berry.tooltip": "Cannot be eaten by the holder",
|
||||
"item.cobblemon.cornn_berry.tooltip": "Cannot be eaten during battles",
|
||||
"item.cobblemon.custap_berry": "Custap Berry",
|
||||
"item.cobblemon.custap_berry.tooltip": "Holder moves first in its priority bracket when at 1/4 max HP or less",
|
||||
"item.cobblemon.durin_berry": "Durin Berry",
|
||||
"item.cobblemon.durin_berry.tooltip": "Cannot be eaten by the holder",
|
||||
"item.cobblemon.durin_berry.tooltip": "Cannot be eaten during battles",
|
||||
"item.cobblemon.enigma_berry": "Enigma Berry",
|
||||
"item.cobblemon.enigma_berry.tooltip": "Restores 1/4 max HP after holder is hit by a supereffective move",
|
||||
"item.cobblemon.figy_berry": "Figy Berry",
|
||||
@@ -2231,15 +2232,15 @@
|
||||
"item.cobblemon.ganlon_berry": "Ganlon Berry",
|
||||
"item.cobblemon.ganlon_berry.tooltip": "Raises holder's Defense by 1 stage when at 1/4 max HP or less",
|
||||
"item.cobblemon.grepa_berry": "Grepa Berry",
|
||||
"item.cobblemon.grepa_berry.tooltip_1": "Lowers the pokémon's Special Defense EVs by 10, while raising friendship",
|
||||
"item.cobblemon.grepa_berry.tooltip_2": "Cannot be eaten by the holder",
|
||||
"item.cobblemon.grepa_berry.tooltip_1": "Lowers the Pokémon's Special Defense EVs by 10, while raising friendship",
|
||||
"item.cobblemon.grepa_berry.tooltip_2": "Cannot be eaten during battles",
|
||||
"item.cobblemon.haban_berry": "Haban Berry",
|
||||
"item.cobblemon.haban_berry.tooltip": "Halves damage taken from a supereffective Dragon-type attack",
|
||||
"item.cobblemon.hondew_berry": "Hondew Berry",
|
||||
"item.cobblemon.hondew_berry.tooltip_1": "Lowers the pokémon's Special Attack EVs by 10, while raising friendship",
|
||||
"item.cobblemon.hondew_berry.tooltip_2": "Cannot be eaten by the holder",
|
||||
"item.cobblemon.hondew_berry.tooltip_1": "Lowers the Pokémon's Special Attack EVs by 10, while raising friendship",
|
||||
"item.cobblemon.hondew_berry.tooltip_2": "Cannot be eaten during battles",
|
||||
"item.cobblemon.hopo_berry": "Hopo Berry",
|
||||
"item.cobblemon.hopo_berry.tooltip": "Cannot be eaten by the holder",
|
||||
"item.cobblemon.hopo_berry.tooltip": "Restores 10 PP to selected move when fed to a Pokémon",
|
||||
"item.cobblemon.iapapa_berry": "Iapapa Berry",
|
||||
"item.cobblemon.iapapa_berry.tooltip": "Restores 1/3 max HP at 1/4 max HP or less; confuses if -Def Nature",
|
||||
"item.cobblemon.jaboca_berry": "Jaboca Berry",
|
||||
@@ -2251,8 +2252,8 @@
|
||||
"item.cobblemon.kee_berry": "Kee Berry",
|
||||
"item.cobblemon.kee_berry.tooltip": "Raises holder's Defense by 1 stage after it is hit by a physical attack",
|
||||
"item.cobblemon.kelpsy_berry": "Kelpsy Berry",
|
||||
"item.cobblemon.kelpsy_berry.tooltip_1": "Lowers the pokémon's Attack EVs by 10, while raising friendship",
|
||||
"item.cobblemon.kelpsy_berry.tooltip_2": "Cannot be eaten by the holder",
|
||||
"item.cobblemon.kelpsy_berry.tooltip_1": "Lowers the Pokémon's Attack EVs by 10, while raising friendship",
|
||||
"item.cobblemon.kelpsy_berry.tooltip_2": "Cannot be eaten during battles",
|
||||
"item.cobblemon.lansat_berry": "Lansat Berry",
|
||||
"item.cobblemon.lansat_berry.tooltip": "Holder gains the Focus Energy effect when at 1/4 max HP or less",
|
||||
"item.cobblemon.leppa_berry": "Leppa Berry",
|
||||
@@ -2264,23 +2265,23 @@
|
||||
"item.cobblemon.mago_berry": "Mago Berry",
|
||||
"item.cobblemon.mago_berry.tooltip": "Restores 1/3 max HP at 1/4 max HP or less; confuses if -Spe Nature",
|
||||
"item.cobblemon.magost_berry": "Magost Berry",
|
||||
"item.cobblemon.magost_berry.tooltip": "Cannot be eaten by the holder",
|
||||
"item.cobblemon.magost_berry.tooltip": "Cannot be eaten during battles",
|
||||
"item.cobblemon.maranga_berry": "Maranga Berry",
|
||||
"item.cobblemon.maranga_berry.tooltip": "Raises holder's Sp. Def by 1 stage after it is hit by a special attack",
|
||||
"item.cobblemon.micle_berry": "Micle Berry",
|
||||
"item.cobblemon.micle_berry.tooltip": "Holder's next move has 1.2x accuracy when at 1/4 max HP or less",
|
||||
"item.cobblemon.nanab_berry": "Nanab Berry",
|
||||
"item.cobblemon.nanab_berry.tooltip": "Cannot be eaten by the holder",
|
||||
"item.cobblemon.nanab_berry.tooltip": "Cannot be eaten during battles",
|
||||
"item.cobblemon.touga_berry": "Touga Berry",
|
||||
"item.cobblemon.touga_berry.tooltip": "Holder is cured if it is confused",
|
||||
"item.cobblemon.nomel_berry": "Nomel Berry",
|
||||
"item.cobblemon.nomel_berry.tooltip": "Cannot be eaten by the holder",
|
||||
"item.cobblemon.nomel_berry.tooltip": "Cannot be eaten during battles",
|
||||
"item.cobblemon.occa_berry": "Occa Berry",
|
||||
"item.cobblemon.occa_berry.tooltip": "Halves damage taken from a supereffective Fire-type attack",
|
||||
"item.cobblemon.oran_berry": "Oran Berry",
|
||||
"item.cobblemon.oran_berry.tooltip": "Restores 10 HP when at 1/2 max HP or less",
|
||||
"item.cobblemon.pamtre_berry": "Pamtre Berry",
|
||||
"item.cobblemon.pamtre_berry.tooltip": "Cannot be eaten by the holder",
|
||||
"item.cobblemon.pamtre_berry.tooltip": "Cannot be eaten during battles",
|
||||
"item.cobblemon.passho_berry": "Passho Berry",
|
||||
"item.cobblemon.passho_berry.tooltip": "Halves damage taken from a supereffective Water-type attack",
|
||||
"item.cobblemon.payapa_berry": "Payapa Berry",
|
||||
@@ -2292,19 +2293,19 @@
|
||||
"item.cobblemon.petaya_berry": "Petaya Berry",
|
||||
"item.cobblemon.petaya_berry.tooltip": "Raises holder's Sp. Atk by 1 stage when at 1/4 max HP or less",
|
||||
"item.cobblemon.pinap_berry": "Pinap Berry",
|
||||
"item.cobblemon.pinap_berry.tooltip": "Cannot be eaten by the holder",
|
||||
"item.cobblemon.pinap_berry.tooltip": "Cannot be eaten during battles",
|
||||
"item.cobblemon.pomeg_berry": "Pomeg Berry",
|
||||
"item.cobblemon.pomeg_berry.tooltip_1": "Lowers the pokémon's HP EVs by 10, while raising friendship",
|
||||
"item.cobblemon.pomeg_berry.tooltip_2": "Cannot be eaten by the holder",
|
||||
"item.cobblemon.pomeg_berry.tooltip_1": "Lowers the Pokémon's HP EVs by 10, while raising friendship",
|
||||
"item.cobblemon.pomeg_berry.tooltip_2": "Cannot be eaten during battles",
|
||||
"item.cobblemon.qualot_berry": "Qualot Berry",
|
||||
"item.cobblemon.qualot_berry.tooltip_1": "Lowers the pokémon's Defense EVs by 10, while raising friendship",
|
||||
"item.cobblemon.qualot_berry.tooltip_2": "Cannot be eaten by the holder",
|
||||
"item.cobblemon.qualot_berry.tooltip_1": "Lowers the Pokémon's Defense EVs by 10, while raising friendship",
|
||||
"item.cobblemon.qualot_berry.tooltip_2": "Cannot be eaten during battles",
|
||||
"item.cobblemon.rabuta_berry": "Rabuta Berry",
|
||||
"item.cobblemon.rabuta_berry.tooltip": "Cannot be eaten by the holder",
|
||||
"item.cobblemon.rabuta_berry.tooltip": "Cannot be eaten during battles",
|
||||
"item.cobblemon.rawst_berry": "Rawst Berry",
|
||||
"item.cobblemon.rawst_berry.tooltip": "Holder is cured if it is burned",
|
||||
"item.cobblemon.razz_berry": "Razz Berry",
|
||||
"item.cobblemon.razz_berry.tooltip": "Cannot be eaten by the holder",
|
||||
"item.cobblemon.razz_berry.tooltip": "Cannot be eaten during battles",
|
||||
"item.cobblemon.rindo_berry": "Rindo Berry",
|
||||
"item.cobblemon.rindo_berry.tooltip": "Halves damage taken from a supereffective Grass-type attack",
|
||||
"item.cobblemon.roseli_berry": "Roseli Berry",
|
||||
@@ -2318,20 +2319,20 @@
|
||||
"item.cobblemon.sitrus_berry": "Sitrus Berry",
|
||||
"item.cobblemon.sitrus_berry.tooltip": "Restores 1/4 max HP when at 1/2 max HP or less",
|
||||
"item.cobblemon.spelon_berry": "Spelon Berry",
|
||||
"item.cobblemon.spelon_berry.tooltip": "Cannot be eaten by the holder",
|
||||
"item.cobblemon.spelon_berry.tooltip": "Cannot be eaten during battles",
|
||||
"item.cobblemon.starf_berry": "Starf Berry",
|
||||
"item.cobblemon.starf_berry.tooltip": "Raises a random stat by 2 when at 1/4 max HP or less",
|
||||
"item.cobblemon.tamato_berry": "Tamato Berry",
|
||||
"item.cobblemon.tamato_berry.tooltip_1": "Lowers the pokémon's Speed EVs by 10, while raising friendship",
|
||||
"item.cobblemon.tamato_berry.tooltip_2": "Cannot be eaten by the holder",
|
||||
"item.cobblemon.tamato_berry.tooltip_1": "Lowers the Pokémon's Speed EVs by 10, while raising friendship",
|
||||
"item.cobblemon.tamato_berry.tooltip_2": "Cannot be eaten during battles",
|
||||
"item.cobblemon.tanga_berry": "Tanga Berry",
|
||||
"item.cobblemon.tanga_berry.tooltip": "Halves damage taken from a supereffective Bug-type attack",
|
||||
"item.cobblemon.wacan_berry": "Wacan Berry",
|
||||
"item.cobblemon.wacan_berry.tooltip": "Halves damage taken from a supereffective Electric-type attack",
|
||||
"item.cobblemon.watmel_berry": "Watmel Berry",
|
||||
"item.cobblemon.watmel_berry.tooltip": "Cannot be eaten by the holder",
|
||||
"item.cobblemon.watmel_berry.tooltip": "Cannot be eaten during battles",
|
||||
"item.cobblemon.wepear_berry": "Wepear Berry",
|
||||
"item.cobblemon.wepear_berry.tooltip": "Cannot be eaten by the holder",
|
||||
"item.cobblemon.wepear_berry.tooltip": "Cannot be eaten during battles",
|
||||
"item.cobblemon.wiki_berry": "Wiki Berry",
|
||||
"item.cobblemon.wiki_berry.tooltip": "Restores 1/3 max HP at 1/4 max HP or less; confuses if -SpA Nature",
|
||||
"item.cobblemon.yache_berry": "Yache Berry",
|
||||
@@ -2479,6 +2480,8 @@
|
||||
"cobblemon.config.ui.experience_multiplier.tooltip": "Additional Exp. multiplier for Exp. calculation.",
|
||||
"cobblemon.config.ui.pokemon_per_chunk": "Pokémon per Chunk",
|
||||
"cobblemon.config.ui.pokemon_per_chunk.tooltip": "The amount of Pokémon allowed within a Chunk when spawning.",
|
||||
"cobblemon.config.ui.poke_snack_pokemon_per_chunk": "Poké Snack Pokémon per Chunk",
|
||||
"cobblemon.config.ui.poke_snack_pokemon_per_chunk.tooltip": "The amount of Pokémon allowed within a Chunk when spawning via Poké Snacks.",
|
||||
"cobblemon.config.ui.passive_statuses": "Passive Statuses",
|
||||
"cobblemon.config.ui.passive_statuses.tooltip": "Range of seconds before clearing the status effect. The value is selected randomly from the range each time an effect is applied.",
|
||||
"cobblemon.config.ui.infinite_healer_charge": "Infinite Healer Charge",
|
||||
@@ -3443,10 +3446,19 @@
|
||||
"tag.item.cobblemon.held.power_weight": "Power Weights",
|
||||
"tag.item.cobblemon.held.terrain_seeds": "Terrain Seeds",
|
||||
"tag.item.cobblemon.herbs": "Herbs",
|
||||
"tag.item.cobblemon.mint_leaf": "Mint Leaves",
|
||||
"tag.item.cobblemon.mint_leaves": "Mint Leaves",
|
||||
"tag.item.cobblemon.mints": "Mints",
|
||||
"tag.item.cobblemon.mint_seeds": "Mint Seeds",
|
||||
"tag.item.cobblemon.mutated_berries": "Mutation Berries",
|
||||
"tag.item.cobblemon.berries.mutation_result": "Mutation Berries",
|
||||
"tag.item.cobblemon.berries.damage_reduction": "Damage Reducing Berries",
|
||||
"tag.item.cobblemon.berries.damaging": "Retaliation Damage Berries",
|
||||
"tag.item.cobblemon.berries.friendship": "Friendship Raising Berries",
|
||||
"tag.item.cobblemon.berries.hp_recovery": "Health Recovery Berries",
|
||||
"tag.item.cobblemon.berries.nature_recovery": "Strong Recovery Berries",
|
||||
"tag.item.cobblemon.berries.non_battle": "Non-Held Item Berries",
|
||||
"tag.item.cobblemon.berries.pp_recovery": "PP Recovery Berries",
|
||||
"tag.item.cobblemon.berries.stat_buff": "Battle Berries",
|
||||
"tag.item.cobblemon.berries.status_recovery": "Status Recovery Berries",
|
||||
"tag.item.cobblemon.plants": "Plants",
|
||||
"tag.item.cobblemon.seeds": "Seeds",
|
||||
"tag.item.cobblemon.boats": "Boats",
|
||||
@@ -3525,7 +3537,12 @@
|
||||
"tag.item.cobblemon.small_flowers": "Small Flowers",
|
||||
"tag.item.cobblemon.held.blacklisted_items_to_hold": "Blacklisted Held Items",
|
||||
"tag.item.cobblemon.held.whitelisted_items_to_hold": "Whitelisted Held Items",
|
||||
"tag.item.cobblemon.poke_puff_berries": "Poké Puff Berries",
|
||||
"tag.item.cobblemon.recipe_filters.poke_puff_seasoner": "Poké Puff Berries",
|
||||
"tag.item.cobblemon.recipe_filters.bait_seasoning": "Baits",
|
||||
"tag.item.cobblemon.recipe_filters.effect_seasoning": "Effect Seasoning Items",
|
||||
"tag.item.cobblemon.recipe_filters.nutrition_seasoning": "Nutrition Seasoning Items",
|
||||
"tag.item.cobblemon.recipe_filters.flavour_seasoning": "Flavour Seasoning Items",
|
||||
"tag.item.cobblemon.recipe_filters.colour_seasoning": "Colour Seasoning Items",
|
||||
"tag.item.cobblemon.candies": "Candies",
|
||||
"tag.item.cobblemon.experience_candies": "Experience Candies",
|
||||
"tag.item.cobblemon.iv_candies": "Hyper Training Candies",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"replace": false,
|
||||
"values": [
|
||||
"#cobblemon:mint_leaf"
|
||||
"#cobblemon:mint_leaves"
|
||||
]
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
"conditions": {
|
||||
"items": [
|
||||
{
|
||||
"items": "#cobblemon:mutated_berries"
|
||||
"items": "#cobblemon:berries/mutation_result"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
{
|
||||
use(battle, pokemon, itemId, data) {
|
||||
const boosts = {};
|
||||
|
||||
for (boost in pokemon.boosts) {
|
||||
boosts[boost] = 0;
|
||||
}
|
||||
|
||||
pokemon.setBoost(boosts);
|
||||
|
||||
battle.add('-clearboost', pokemon, '[from] bagitem: ' + itemId);
|
||||
}
|
||||
}
|
||||
+1
@@ -12,5 +12,6 @@
|
||||
"result": {
|
||||
"id": "cobblemon:antidote"
|
||||
},
|
||||
"seasoningTag": "cobblemon:empty",
|
||||
"seasoningProcessors": []
|
||||
}
|
||||
+1
@@ -12,5 +12,6 @@
|
||||
"result": {
|
||||
"id": "cobblemon:awakening"
|
||||
},
|
||||
"seasoningTag": "cobblemon:empty",
|
||||
"seasoningProcessors": []
|
||||
}
|
||||
+1
@@ -28,5 +28,6 @@
|
||||
"id": "minecraft:beetroot_soup",
|
||||
"count": 1
|
||||
},
|
||||
"seasoningTag": "cobblemon:empty",
|
||||
"seasoningProcessors": []
|
||||
}
|
||||
@@ -16,5 +16,6 @@
|
||||
"result": {
|
||||
"id": "cobblemon:berry_sweet"
|
||||
},
|
||||
"seasoningTag": "cobblemon:empty",
|
||||
"seasoningProcessors": []
|
||||
}
|
||||
@@ -18,5 +18,6 @@
|
||||
"result": {
|
||||
"id": "cobblemon:big_malasada"
|
||||
},
|
||||
"seasoningTag": "cobblemon:empty",
|
||||
"seasoningProcessors": []
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
"result": {
|
||||
"id": "cobblemon:aprijuice_black"
|
||||
},
|
||||
"seasoningTag": "cobblemon:recipe_filters/flavour_seasoning",
|
||||
"seasoningProcessors": [
|
||||
"flavour"
|
||||
]
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"result": {
|
||||
"id": "cobblemon:aprijuice_blue"
|
||||
},
|
||||
"seasoningTag": "cobblemon:recipe_filters/flavour_seasoning",
|
||||
"seasoningProcessors": [
|
||||
"flavour"
|
||||
]
|
||||
|
||||
@@ -21,5 +21,6 @@
|
||||
"result": {
|
||||
"id": "cobblemon:brittle_candy"
|
||||
},
|
||||
"seasoningTag": "cobblemon:empty",
|
||||
"seasoningProcessors": []
|
||||
}
|
||||
@@ -22,5 +22,6 @@
|
||||
"count": 3,
|
||||
"id": "cobblemon:brittle_candy"
|
||||
},
|
||||
"seasoningTag": "cobblemon:empty",
|
||||
"seasoningProcessors": []
|
||||
}
|
||||
+1
@@ -12,5 +12,6 @@
|
||||
"result": {
|
||||
"id": "cobblemon:burn_heal"
|
||||
},
|
||||
"seasoningTag": "cobblemon:empty",
|
||||
"seasoningProcessors": []
|
||||
}
|
||||
@@ -23,5 +23,6 @@
|
||||
"result": {
|
||||
"id": "minecraft:cake"
|
||||
},
|
||||
"seasoningTag": "cobblemon:empty",
|
||||
"seasoningProcessors": []
|
||||
}
|
||||
+1
@@ -28,5 +28,6 @@
|
||||
"result": {
|
||||
"id": "cobblemon:calcium"
|
||||
},
|
||||
"seasoningTag": "cobblemon:empty",
|
||||
"seasoningProcessors": []
|
||||
}
|
||||
@@ -21,5 +21,6 @@
|
||||
"id": "cobblemon:candied_apple",
|
||||
"count": 1
|
||||
},
|
||||
"seasoningTag": "cobblemon:empty",
|
||||
"seasoningProcessors": []
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user