Compare commits

..
Author SHA1 Message Date
jackowes da73fd5616 Velocity based motion sick cam (WIP) 2025-11-03 00:45:38 -06:00
jackowes 729485f540 - Make stamina regen rate a setting and consistent no matter the stamina pool (currently 5 seconds)
- Sync passenger Rotation info with the server.
- Fix arrow spawning on rollable rides (still needs fixed for non rolling rides)
2025-10-31 13:20:37 -05:00
jackowes 586fe259ad Fixed passenger support. 2025-10-31 01:17:52 -05:00
jackowes 49cc86331f A few subamrine tweaks 2025-10-31 00:45:48 -05:00
jackowes b923bcad50 Add per axis mouse input handling to whether the input is used for driver rotation or vehicle rotation. Add better checks for submarine surface calculation. Add smooth transition from underwater to surface breaching. WIP still. 2025-10-29 22:57:05 -05:00
jackowes b49cad021e Fix player xRot and yRot values. Fixes the view angle issues when on a rollable ride. Began fixing eyeheight and projectile spawning locations. 2025-10-29 15:34:14 -05:00
Carmendar 78c733e1c1 Updated Dolphin and Submarine expressions. 2025-10-29 03:07:47 -05:00
jackowes 2825e13e7a - Make it so only forward movement gives the swim posetype.
- increase the downward angle needed to submerge by inputting fowards (could change to only crouch)
- Add rolling into turns for more fluidity.
- Tie in jump stat to strafing speed.
- temp fix for passenger seat rotation on blastoise.
- Cleanup unused submarine settings.
2025-10-26 21:42:41 -05:00
jackowes bcfd4cad49 - Submarine breaching logic refined
- Submarine breathe drain through stamina implemented
- supplied a hook to the controllers so that rotations may be set using partialticks alongside rendering
- Temporarily adjusted blastoises seating rotations
- Added beginnings of leaning animations through submarine.
2025-10-26 11:57:41 -05:00
jackowes 037dd79a79 Got most of submarine done. 2025-10-26 00:30:16 -05:00
jackowes fc3da8a476 - Fix dolphin stationary orientation righting by damping it with pitch.
- Fix yaw axis flip when stationary by performing a blend between local roll and local yaw instead of globalyaw.
2025-10-25 12:42:50 -05:00
jackowes 46eac875e7 - make pitch smoothing... smoother
- no orientation correction when breached
- Increase breaching angle range.
- Yaw was backwards when upside down and breached. Fixed that.
- Allow for more rotation momentum gain during breaching. (better flips)
- zero mouse smoothers on entering water so as not to carry over yaw momentum from breach into roll momentum.
2025-10-24 17:33:30 -05:00
jackowes 8725f5c8b9 Fixed:
- sustained downward force after beaching and then reentering the water
- Crash on a server when mounting dolphin
- Fixed passenger fov jitter (I think this is where a lot of the weirdness for the passenger was actually coming from)
2025-10-24 16:19:00 -05:00
jackowes f2c4ebef7c - Fix angle sweetspot on banking to be wider and easier to hit. 2025-10-24 01:32:38 -05:00
jackowes d228da9eb5 - Constant adjusted roll correction on bird.
- Trend dolphin roll towards zero when rolling to keep it upright more naturally
- Yaw when breached instead of roll
- Adjust roll correction completely on dolphin
- Make sprint debuffs more forgiving.
- Fixed swim animation not playing
- Fix hover animation not playing
- Improve bird roll handling to keep more upright
- Fix breaching so it doesn't air strafe
2025-10-24 01:14:32 -05:00
jackowes f30209baf5 Make out of water jump not strafe you 2025-10-23 00:18:10 -05:00
jackowes 70d316b990 Start of dolphin fixes and changes 2025-10-22 22:03:41 -05:00
jackowes b3c56488d8 Fix the menued mouse handling I broke. Improve orientation correction and also make it rely onu frametimes. 2025-10-20 20:24:21 -05:00
jackowes 1b06085772 Try and consolidate something then realize I am dumb. But some cleanup and fixes to the new orientation use when in reverse nonetheless. 2025-10-18 16:33:02 -05:00
jackowes 7e9a9c6dd9 Merge branch 'main' into orientation-refactor 2025-10-18 13:36:18 -05:00
jackowes eb38284e63 Fix mistake in passenger rolling transition. 2025-10-18 11:39:11 -05:00
jackowes 54095d535a Fix passenger transition to and from riding and active rolling mount. 2025-10-18 11:21:31 -05:00
jackowes 3944aaefd0 reorganize 2025-10-15 23:57:56 -05:00
jackowes 3487617e1d Smooth transitions to and from rollables.
TODO:
- passengers need to align their x and yrots properly as they mount a ride and as the ride transtitions to and from a rollable controller.
- Clean up current functionality
2025-10-11 21:04:54 -05:00
jackowes 9c7aba7481 Have I made it worse? Idk, tune in next time to probably not find out either.
When the controller is not active do not allow changes from the base rotation function to be applied in the ridetick. Still so jank right now but that might have everything to do with how the camera is reset in MountedCameraRenderer
2025-10-07 21:49:57 -05:00
jackowes e4e0846a24 Smooth out transition to rollables. Transition from rollables to non rollables is broken. 2025-09-28 15:27:12 -05:00
jackowes 7d70af44ba WIP multi passenger rolling support (it works but will likely need a better smoothing function eventually)
Still to do:
- zero driver rotx and roty when mounting.
- Fix the odd camera transition when going from rollable to non rollable (kick out of rollable camera logic earlier probably)
- position logic for freelook camera is wrong in reverse 3rd person.
2025-09-27 23:59:39 -05:00
jackowes 7c8fcdcd48 Freelook support (position logic for 3rd person no view bob still needs fixed 2025-09-27 11:54:23 -05:00
jackowes 67684ff9e8 Merge branch 'main' into orientation-refactor
# Conflicts:
#	common/src/main/kotlin/com/cobblemon/mod/common/api/riding/behaviour/types/air/BirdBehaviour.kt
2025-09-24 21:32:45 -05:00
jackowes 7144aa8dce split orientations 2025-09-24 21:31:14 -05:00
jackowes 6801035c1d messing around with some temp changes 2025-09-23 20:18:22 -05:00
463 changed files with 2301 additions and 13819 deletions

No files matched your search

+1 -2
View File
@@ -12,8 +12,7 @@ out
*.ipr
*.iws
*.iml
.idea/*
!.idea/icon.png
.idea
# gradle
build
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

+1 -9
View File
@@ -12,14 +12,6 @@
- SpawningContext is now SpawnablePosition
- WorldSlice is SpawningZone
- SpawningProspector is now SpawningZoneGenerator
- Majorly refactored the hierarchy of Spawner
- The base Spawner interface provides more functions to allow single-point and area spawning given appropriate inputs.
- TickingSpawner is removed in favour of outside code handling ticking logic.
- AreaSpawner is removed.
- BasicSpawner is the first implementation of Spawner which can be used for any purpose.
- PlayerSpawners are now mixin'd into ServerPlayer and ticked from ServerPlayer#tick.
- A hierarchy diagram can be found at `./docs/spawner-hierarchy.png` in the mod repository.
- The SpawnerManager class has been removed as its functionality is all now handled elsewhere.
- Renamed things in Spawn Rules to go with the other renames:
- contextSelector is now spawnablePositionSelector
- contextSelector is now spawnablePositionSelector
- context is now spawnable_position
+2 -28
View File
@@ -54,8 +54,6 @@
- `PokemonRecallEvent`
- `TradeEvent`
- Added a new gamerule, 'healerHealsPC', when set to true a successful use of a healer will also heal all of the Pokemon in that player's PC.
- Mooshtanks will switch between their red and brown variant when hit by lightning
- Smeargle spawns with differing tail colour depending on its [Characteristic](https://bulbapedia.bulbagarden.net/wiki/Characteristic) stat
### Pokémon Added
@@ -423,8 +421,6 @@
- Updated some item tags to better integrate behaviours between Cobblemon, Vanilla Minecraft, and other mods
- Removed Cooked Meat, Raw Meat, Protein Ingredients, and Zinc Ingredients, the first two are now using `c` namespace tags, the latter have better integrated use of tags within their recipes which removes need for custom tags.
- Added our seeds tag into `#c:seeds`, which is now made use of for the Miracle Seed recipe.
- Updated the `#cobblemon:berries` tag to use subtags located in `data/cobblemon/tags/item/berries`, such as `#cobblemon:berries/hp_recovery` for sorting purposes
- Moved the tag `#cobblemon:mutated_berries` to `#cobblemon:berries/mutation_result`
- Added herbs and snowballs to the consumable in PvE and Wild battle tags
- Tweaked the Natural Materials Vanilla file to fit with the changes to tags
- Substantially optimised spawning checks mainly by front-loading biome filtering.
@@ -476,8 +472,7 @@
- Updated `HatchEggEvent.Post` to include the Pokemon that hatched.
- Roseli Berry Trees now naturally generate in their preferred biomes.
- Refactored dynamic lights compat to be more future proof. The dynamic light support is only tested with LambDynamicLights, on NeoForged use that mod in combination with Sinytra Connector
- Shulker Boxes and Traveler's Backpacks AND Packed Up backpacks can no longer be held by Pokémon. Thanks Monocle ;) You could re-enable this with our datapack (but you'd be crazy).
- Moomoo Milk now clears Pokémon stat changes when used in battle.
- 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.
### Fixes
- Fixed game crashing when removing national pokedex using datapacks
@@ -562,8 +557,6 @@
- Fixed singular Pokémon corruption causing entire storage corruption. Storages will now skip corrupted Pokémon and print an error to console.
- Fixed species additions not being able to properly mark a species as implemented.
- Fixed Pokémon item models not showing a glint when enchanted.
- Fixed some specific bag items not being dropped when used in battle.
- Fixed the "use all berry bait" achievement not being progressed
### Developer
- A finished battle now has winners and losers set inside of `PokemonBattle` instead of them always being empty.
@@ -601,25 +594,10 @@
- Added a `hoverText` option to PartySelectCallback, to display a tooltip on hovering over a Pokémon in the selection screen.
- `PokemonEntity` instances spawned into the world now appropriately finalize the spawn for mod compatibility.
- Added PokedexManager.obtain as a replacement for .catch which is not a friendly function name in Java.
- Added `Pokemon#hyperTrainIV()` and `IVs#setHyperTrainedIV(Stat, Int)`
- `ElementalType` now implements `ShowdownIdentifiable` to ensure the communcation with showdown stays consistent (also in regards to TeraTypes)
- Pokemon no longer have a change observable
- Pokémon can now have a behaviour changing the value of a species feature on lightning hit:
```JSON
{
"behaviour": {
"lightningHit": {
"rotateFeatures": [
{
"key": "mooshtank",
"chain": ["red", "brown"]
}
]
}
}
}
```
This will change the value of `mooshtank` from `red` to `brown` and vice versa when a lightning strikes this Pokémon. Only chains of which a value is the current value will be considered, so multiple entries for the same key with different chains can be added.
### MoLang & Datapacks
- The following usages for item predicates can now use item conditions like advancements do, you can learn about them in the [Minecraft wiki](https://minecraft.wiki/w/Advancement_definition#minecraft:filled_bucket)
@@ -723,10 +701,6 @@
- Mechanics
- Unlockable Wallpapers
- Starter Data
- Fixed `cobblemon:reel_in_pokemon` criteria not working when used together with a `baitId`
- Also changed the default from `cobblemon:empty_bait` to `any`
- The previous default is still available by using the above as baitId
- Added support for species-specific move action effects, using the format `{move_id}_{species}.json`.
### Particles
Added new/updated particles for the following moves:
@@ -1,23 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common;
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner;
/**
* Used for mixin shenanigans. ServerPlayers now store their Cobblemon spawner
* inside, so this interface helps execute that.
*
* @author Hiroku
* @since October 27th, 2025
*/
public interface PlayerSpawnerAccessor {
PlayerSpawner getPlayerSpawner();
void setPlayerSpawner(PlayerSpawner spawner);
}
@@ -1,11 +1,3 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin;
import com.cobblemon.mod.common.OrientationControllable;
@@ -7,7 +7,6 @@
*/
package com.cobblemon.mod.common.mixin;
import com.cobblemon.mod.common.duck.ChannelDuck;
import com.mojang.blaze3d.audio.Channel;
import org.lwjgl.openal.AL10;
@@ -53,6 +53,12 @@ public abstract class LivingEntityMixin extends Entity implements OrientationCon
return cobblemon$orientationController;
}
@Inject(method = "baseTick", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/multiplayer/ClientPacketListener;send(Lnet/minecraft/network/protocol/Packet;)V"))
private void cobblemon$updateRotationMatrix(CallbackInfo ci) {
if (!cobblemon$orientationController.isActive() || cobblemon$orientationController.getOrientation() == null) return;
CobblemonNetwork.INSTANCE.sendToServer(new ServerboundUpdateOrientationPacket(this.getId(), cobblemon$orientationController.getOrientation()));
}
@Inject(method = "onEffectRemoved", at = @At(value = "TAIL"))
private void cobblemon$onEffectRemoved(MobEffectInstance effect, CallbackInfo ci) {
final LivingEntity entity = (LivingEntity) (Object) this;
@@ -105,7 +111,7 @@ public abstract class LivingEntityMixin extends Entity implements OrientationCon
@Inject(method = "tick", at = @At("HEAD"))
private void cobblemon$updateRenderOrientation(CallbackInfo ci) {
//this.cobblemon$orientationController.tick();
this.cobblemon$orientationController.tick();
}
@Override
@@ -8,56 +8,21 @@
package com.cobblemon.mod.common.mixin;
import com.cobblemon.mod.common.Cobblemon;
import com.cobblemon.mod.common.OrientationControllable;
import com.cobblemon.mod.common.PlayerSpawnerAccessor;
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 net.minecraft.world.entity.player.Player;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ServerPlayer.class)
public abstract class ServerPlayerMixin implements PlayerSpawnerAccessor {
@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;
}
@Override
public void setPlayerSpawner(PlayerSpawner spawner) {
this.cobblemon$spawner = spawner;
}
@Inject(method = "tick", at = @At("TAIL"))
private void cobblemon$tickSpawner(CallbackInfo ci) {
if (!Cobblemon.config.getEnableSpawning()) {
return;
} else if (!serverLevel().getGameRules().getBoolean(CobblemonGameRules.DO_POKEMON_SPAWNING)) {
return;
}
getPlayerSpawner().tick();
}
public abstract class ServerPlayerMixin {
@Inject(method = "rideTick", at = @At("HEAD"))
private void cobblemon$updateOrientationControllerRideTick(CallbackInfo ci) {
@@ -89,4 +54,5 @@ public abstract class ServerPlayerMixin implements PlayerSpawnerAccessor {
private boolean cobblemon$cancelSavingPokemonMounts(boolean original, @Local(ordinal=0) Entity entity) {
return original && !(entity instanceof PokemonEntity);
}
}
@@ -36,6 +36,9 @@ import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import static net.minecraft.util.Mth.abs;
import static net.minecraft.util.Mth.atan2;
@Mixin(Camera.class)
public abstract class CameraMixin {
@Shadow private Entity entity;
@@ -62,14 +65,13 @@ public abstract class CameraMixin {
@Unique private double cobblemon$frameTime = 0.0;
@Inject(method = "setRotation", at = @At("HEAD"), cancellable = true)
public void cobblemon$setRotation(float f, float g, CallbackInfo ci) {
// Calculate the current frametime
double d = Blaze3D.getTime();
this.cobblemon$frameTime = (this.cobblemon$lastHandledRotationTime != Double.MIN_VALUE) ? d - this.cobblemon$lastHandledRotationTime : 0.0;
this.cobblemon$lastHandledRotationTime = d;
// Don't assume the camera to be attached to an entity. Ponder Scenes from create et.al. for example aren't.
if (this.entity == null) return;
var vehicle = this.entity.getVehicle();
if (!(vehicle instanceof OrientationControllable controllableVehicle)) return;
var instance = (Camera)(Object)this;
@@ -161,17 +163,17 @@ public abstract class CameraMixin {
var lerpRateMod = Mth.cos((float)Math.toRadians(cutoffPitch));
// Apply smoothing from the cameras current yaw to the yaw of the velocity vector of the ride.
var k = 5.0f; // Some smoothing constant k
var k = 2.0f; // Some smoothing constant k
var smoothingFactor = lerpRateMod * cobblemon$frameTime * k;
var newYaw = currAngs.y() + (Math.toRadians(degDiff) * smoothingFactor);
newRotation.set(new Quaternionf().rotateYXZ((float)newYaw, eulerAngs.x(), 0.0f));
}
var rotationOffset = 0;
var k = 0;
if (Minecraft.getInstance().options.getCameraType().isMirrored()) {
newRotation.rotateY((float)Math.toRadians(180));
rotationOffset = 180;
k = 180;
}
// Get the drivers local look angle in relation to the ride and apply it.
@@ -192,7 +194,7 @@ public abstract class CameraMixin {
this.yRot = eulerAngs.y() * Mth.RAD_TO_DEG;
// Set the drivers rotations to match
var driverEulerAngs = new Quaternionf(this.rotation).getEulerAnglesYXZ(new Vector3f());
driver.setXRot(-driverEulerAngs.x() * Mth.RAD_TO_DEG + rotationOffset);
driver.setXRot(-driverEulerAngs.x() * Mth.RAD_TO_DEG + k);
driver.setYRot(180.0f - (driverEulerAngs.y() * Mth.RAD_TO_DEG));
OrientationController.Companion.getFORWARDS().rotate(this.rotation, this.forwards);
@@ -9,21 +9,15 @@
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.api.scheduling.ClientTaskTracker;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.world.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.function.Predicate;
@Mixin(GameRenderer.class)
public class GameRendererMixin {
@Unique
@@ -48,16 +42,4 @@ public class GameRendererMixin {
lastTime = newTime;
}
@ModifyArg(
method = "pick(Lnet/minecraft/world/entity/Entity;DDF)Lnet/minecraft/world/phys/HitResult;",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/projectile/ProjectileUtil;getEntityHitResult(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/world/phys/AABB;Ljava/util/function/Predicate;D)Lnet/minecraft/world/phys/EntityHitResult;"
),
index = 4
)
private Predicate<Entity> filterPokemonVehicle(Predicate<Entity> original, @Local(ordinal = 0, argsOnly = true) Entity entity) {
return (entityX) -> !(entity.getVehicle() == entityX && entityX instanceof PokemonEntity) && original.test(entityX);
}
}
@@ -55,10 +55,6 @@ public abstract class LocalPlayerMixin extends AbstractClientPlayer implements O
@Unique Matrix3f cobblemon$lastOrientation;
@Unique float cobblemon$lastRideXRot;
@Unique float cobblemon$lastRideYRot;
@Unique Vec3 cobblemon$lastRideEyePos;
@Unique boolean cobblemon$isDoubleJumping = false;
@Shadow
@@ -76,6 +72,15 @@ public abstract class LocalPlayerMixin extends AbstractClientPlayer implements O
super(clientLevel, gameProfile);
}
// @Inject(method = "sendPosition", at = @At("TAIL"))
// private void cobblemon$updateRotationMatrix(CallbackInfo ci) {
// if (!(this instanceof OrientationControllable controllable)) return;
// var controller = controllable.getOrientationController();
// if (!controller.isActive() || controller.getOrientation() == cobblemon$lastOrientation) return;
// cobblemon$lastOrientation = controller.getOrientation() != null ? new Matrix3f(controller.getOrientation()) : null;
// CobblemonNetwork.INSTANCE.sendToServer(new ServerboundUpdateOrientationPacket(controller.getOrientation()));
// }
@Inject(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/multiplayer/ClientPacketListener;send(Lnet/minecraft/network/protocol/Packet;)V"))
private void cobblemon$updateRotationMatrixPassenger(CallbackInfo ci) {
if (!(this.getVehicle() instanceof OrientationControllable controllableVehicle)) return;
@@ -141,21 +146,8 @@ public abstract class LocalPlayerMixin extends AbstractClientPlayer implements O
var rideYRot = playerRotater.cobblemon$getRideYRot();
var rideEyePos = playerRotater.cobblemon$getRideEyePos();
// Check for a change in the values before sending them.
if (cobblemon$lastRideXRot != rideXRot ||
cobblemon$lastRideYRot != rideYRot ||
cobblemon$lastRideEyePos != rideEyePos
) {
// Update 'last' values
cobblemon$lastRideXRot = rideXRot;
cobblemon$lastRideYRot = rideYRot;
cobblemon$lastRideEyePos = rideEyePos;
CobblemonNetwork.INSTANCE.sendToServer(new ServerboundUpdateRiderRotationPacket(rideXRot, rideYRot, rideEyePos));
}
// Send new info to server. Currently not checking to see if this has change substantially
CobblemonNetwork.INSTANCE.sendToServer(new ServerboundUpdateRiderRotationPacket(rideXRot, rideYRot, rideEyePos));
}
@Unique
@@ -123,6 +123,8 @@ public abstract class MouseHandlerMixin {
@Local(argsOnly = true) double d,
@Local(argsOnly = true) double movementTime
) {
boolean returnValue = true;
PokedexUsageContext usageContext = CobblemonClient.INSTANCE.getPokedexUsageContext();
if (usageContext.getScanningGuiOpen()) {
this.smoothTurnY.reset();
@@ -133,6 +135,7 @@ public abstract class MouseHandlerMixin {
var sensitivity = lerp(usageContext.getFovMultiplier(), spyglassSensitivity, lookSensitivity);
var yRotationFlip = this.minecraft.options.invertYMouse().get() ? -1 : 1;
player.turn(this.accumulatedDX * sensitivity, (this.accumulatedDY * sensitivity * yRotationFlip));
returnValue = false;
}
var vehicle = player.getVehicle();
@@ -150,7 +153,7 @@ public abstract class MouseHandlerMixin {
pitchSmoother.reset();
rollSmoother.reset();
yawSmoother.reset();
return true;
return returnValue;
}
var vehicleController = controllableVehicle.getOrientationController();
@@ -83,7 +83,6 @@ import com.cobblemon.mod.common.net.messages.client.settings.ServerSettingsPacke
import com.cobblemon.mod.common.permission.LaxPermissionValidator
import com.cobblemon.mod.common.platform.events.PlatformEvents
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.aspects.CHARACTERISTIC_RAINBOW_ASPECT
import com.cobblemon.mod.common.pokemon.aspects.COSMETIC_SLOT_ASPECT
import com.cobblemon.mod.common.pokemon.aspects.GENDER_ASPECT
import com.cobblemon.mod.common.pokemon.aspects.SHINY_ASPECT
@@ -294,7 +293,6 @@ object Cobblemon {
SHINY_ASPECT.register()
GENDER_ASPECT.register()
COSMETIC_SLOT_ASPECT.register()
CHARACTERISTIC_RAINBOW_ASPECT.register()
SpeciesFeatures.types["choice"] = ChoiceSpeciesFeatureProvider::class.java
SpeciesFeatures.types["flag"] = FlagSpeciesFeatureProvider::class.java
@@ -696,7 +696,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val SUPERB_REMEDY = compostableItem("superb_remedy", RemedyItem(RemedyItem.SUPERB), 0.85f)
@JvmField
val MOOMOO_MILK = create("moomoo_milk", MoomooMilk(Item.Properties().craftRemainder(Items.GLASS_BOTTLE).stacksTo((16))))
val MOOMOO_MILK = create("moomoo_milk", CobblemonItem(Item.Properties().craftRemainder(Items.GLASS_BOTTLE).stacksTo((16))))
@JvmField
val POTION = create("potion", PotionItem(PotionType.POTION))
@@ -33,7 +33,7 @@ import com.cobblemon.mod.common.client.net.gui.PokedexUIPacketHandler
import com.cobblemon.mod.common.client.net.gui.SummaryUIPacketHandler
import com.cobblemon.mod.common.client.net.npc.CloseNPCEditorHandler
import com.cobblemon.mod.common.client.net.npc.OpenNPCEditorHandler
import com.cobblemon.mod.common.client.net.orientation.ClientboundUpdateOrientationHandler
import com.cobblemon.mod.common.client.net.orientation.S2CUpdateOrientationHandler
import com.cobblemon.mod.common.client.net.pasture.ClosePastureHandler
import com.cobblemon.mod.common.client.net.pasture.OpenPastureHandler
import com.cobblemon.mod.common.client.net.pasture.PokemonPasturedHandler
@@ -217,7 +217,7 @@ import com.cobblemon.mod.common.net.serverhandling.dialogue.EscapeDialogueHandle
import com.cobblemon.mod.common.net.serverhandling.dialogue.InputToDialogueHandler
import com.cobblemon.mod.common.net.serverhandling.evolution.AcceptEvolutionHandler
import com.cobblemon.mod.common.net.serverhandling.npc.SaveNPCHandler
import com.cobblemon.mod.common.net.serverhandling.orientation.ServerboundUpdateOrientationHandler
import com.cobblemon.mod.common.net.serverhandling.orientation.OrientationPacketHandler
import com.cobblemon.mod.common.net.serverhandling.pasture.PasturePokemonHandler
import com.cobblemon.mod.common.net.serverhandling.pasture.SetPastureConflictHandler
import com.cobblemon.mod.common.net.serverhandling.pasture.UnpastureAllPokemonHandler
@@ -444,7 +444,7 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(UpdatePastureConflictFlagPacket.ID, UpdatePastureConflictFlagPacket::decode, UpdatePastureConflictFlagHandler))
// Orientation
list.add(PacketRegisterInfo(ClientboundUpdateOrientationPacket.ID, ClientboundUpdateOrientationPacket::decode, ClientboundUpdateOrientationHandler))
list.add(PacketRegisterInfo(ClientboundUpdateOrientationPacket.ID, ClientboundUpdateOrientationPacket::decode, S2CUpdateOrientationHandler))
// Behaviours
list.add(PacketRegisterInfo(PlayPosableAnimationPacket.ID, PlayPosableAnimationPacket::decode, PlayPosableAnimationHandler))
@@ -579,7 +579,7 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(SaveNPCPacket.ID, SaveNPCPacket::decode, SaveNPCHandler))
// Riding packet(s)
list.add(PacketRegisterInfo(ServerboundUpdateOrientationPacket.ID, ServerboundUpdateOrientationPacket::decode, ServerboundUpdateOrientationHandler))
list.add(PacketRegisterInfo(ServerboundUpdateOrientationPacket.ID, ServerboundUpdateOrientationPacket::decode, OrientationPacketHandler))
list.add(PacketRegisterInfo(ServerboundUpdateRidingStatePacket.ID, ServerboundUpdateRidingStatePacket::decode, ServerboundUpdateRidingStateHandler))
list.add(PacketRegisterInfo(ServerboundUpdateRidingStatsPacket.ID, ServerboundUpdateRidingStatsPacket::decode, ServerboundUpdateRidingStatsHandler))
list.add(PacketRegisterInfo(ServerboundUpdateRidingStatRangePacket.ID, ServerboundUpdateRidingStatRangePacket::decode, ServerboundUpdateRidingStatRangeHandler))
@@ -26,6 +26,8 @@ object CobblemonPoiTypes: PlatformRegistry<Registry<PoiType>, ResourceKey<Regist
@JvmField
val NURSE_KEY = createKey("nurse", CobblemonBlocks.HEALING_MACHINE, 1, 1)
@JvmField
val POKE_SNACK_KEY = createKey("poke_snack", CobblemonBlocks.POKE_SNACK, 0, 1)
@JvmField
val SACCHARINE_LOG_SLATHERED_KEY = createKey("saccharine_log_slathered", CobblemonBlocks.SACCHARINE_LOG_SLATHERED, 0, 1)
// TODO after 1.7
// @JvmField
@@ -22,19 +22,18 @@ class ReelInPokemonContext(val species : ResourceLocation, val baitId: ResourceL
class ReelInPokemonCriterionCondition(
playerCtx: Optional<ContextAwarePredicate>,
val species: String,
val baitId: String // also allows "cobblemon:empty_bait" to indicate that no bait is allowed for this criterion to match
val baitId: String
) : SimpleCriterionCondition<ReelInPokemonContext>(playerCtx) {
companion object {
val CODEC: Codec<ReelInPokemonCriterionCondition> = RecordCodecBuilder.create { it.group(
EntityPredicate.ADVANCEMENT_CODEC.optionalFieldOf("player").forGetter { it.playerCtx },
Codec.STRING.optionalFieldOf("species", "any").forGetter { it.species },
Codec.STRING.optionalFieldOf("baitId", "any").forGetter { it.baitId }
).apply(it, { playerCtx, pokemonId, baitId -> ReelInPokemonCriterionCondition(playerCtx, pokemonId, baitId) }) }
Codec.STRING.optionalFieldOf("baitId", "empty_bait").forGetter { it.baitId }
).apply(it, { playerCtx, pokemonId, baitId -> ReelInPokemonCriterionCondition(playerCtx, pokemonId, baitId.ifEmpty { "empty_bait" }) }) }
}
override fun matches(player: ServerPlayer, context: ReelInPokemonContext): Boolean {
return (context.species == this.species.asIdentifierDefaultingNamespace() || this.species == "any")
&& (context.baitId == this.baitId.asIdentifierDefaultingNamespace() || this.baitId == "any")
return (context.species == this.species.asIdentifierDefaultingNamespace() || this.species == "any") && (context.baitId == this.baitId.asIdentifierDefaultingNamespace() || this.baitId == "empty_bait")
}
}
@@ -1,49 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.ai
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.behavior.Behavior
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.entity.ai.memory.MemoryStatus
import net.minecraft.world.entity.schedule.Activity
/**
* A [Behavior] subclass that represents tasks that check that the entity is still in one
* of the activities that this task is part of. If the entity is no longer doing any of those
* activities, the task will be interrupted and you can handle any cleanup in [onInterrupted].
*
* @author Hiroku
* @since October 17th, 2025
*/
abstract class ActivityAwareBehavior<E : LivingEntity> : Behavior<E> {
constructor(entryCondition: Map<MemoryModuleType<*>, MemoryStatus>) : super(entryCondition)
constructor(entryCondition: Map<MemoryModuleType<*>, MemoryStatus>, duration: Int) : super(entryCondition, duration)
constructor(entryCondition: Map<MemoryModuleType<*>, MemoryStatus>, minDuration: Int, maxDuration: Int) : super(entryCondition, minDuration, maxDuration)
val activities: MutableSet<Activity> = mutableSetOf()
fun isStillDoingActivity(entity: E): Boolean {
return activities.any(entity.brain::isActive)
}
override fun canStillUse(level: ServerLevel, entity: E, gameTime: Long) = isStillDoingActivity(entity)
open fun onInterrupted(level: ServerLevel, entity: E, gameTime: Long) {
// No-op
}
override fun stop(level: ServerLevel, entity: E, gameTime: Long) {
super.stop(level, entity, gameTime)
if (!isStillDoingActivity(entity)) {
onInterrupted(level, entity, gameTime)
}
}
}
@@ -11,6 +11,7 @@ package com.cobblemon.mod.common.api.ai
import com.google.common.collect.ImmutableList
import com.mojang.datafixers.util.Pair
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.Mob
import net.minecraft.world.entity.ai.behavior.BehaviorControl
import net.minecraft.world.entity.schedule.Activity
@@ -22,13 +23,6 @@ class ActivityConfigurationContext(val activity: Activity) {
}
fun apply(entity: LivingEntity) {
tasks.forEach {
val activityAwareBehavior = it.second as? ActivityAwareBehavior<*>
?: (it.second as? WrapperLivingEntityTask<*>)?.task as? ActivityAwareBehavior
?: return@forEach
activityAwareBehavior.activities += activity
}
entity.brain.addActivity(activity, ImmutableList.copyOf(tasks))
}
}
@@ -23,7 +23,7 @@ import net.minecraft.world.entity.schedule.Activity
class ActivityChangeTaskConfig : SingleTaskConfig {
val activity: Activity = Activity.IDLE
val condition: ExpressionLike = "true".asExpressionLike()
val condition: ExpressionLike = "false".asExpressionLike()
override fun getVariables(entity: LivingEntity, behaviourConfigurationContext: BehaviourConfigurationContext): List<MoLangConfigVariable> = emptyList()
@@ -38,7 +38,7 @@ class MoveToOwnerTaskConfig : SingleTaskConfig {
override fun createTask(
entity: LivingEntity,
behaviourConfigurationContext: BehaviourConfigurationContext
): BehaviorControl<in LivingEntity> {
): BehaviorControl<in LivingEntity>? {
behaviourConfigurationContext.addMemories(
MemoryModuleType.WALK_TARGET,
MemoryModuleType.LOOK_TARGET
@@ -110,7 +110,7 @@ class PlaceHoneyInSaccLeavesTaskConfig : SingleTaskConfig {
override fun stop(level: ServerLevel, entity: LivingEntity, gameTime: Long) {
if (successfulPollinationTicks > REQUIRED_SUCCESSFUL_POLLINATION_TICKS) {
val blockPos = entity.brain.getMemorySafely(CobblemonMemories.NEARBY_SACC_LEAVES).orElse(null)
blockPos?.let {
blockPos.let {
val blockState = level.getBlockState(blockPos)
if (blockState.block == CobblemonBlocks.SACCHARINE_LEAVES) {
val saccAge = blockState.getValue( SaccharineLeafBlock.AGE)
@@ -1,29 +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.berry.spawncondition
import com.cobblemon.mod.common.api.berry.Berry
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.core.Holder
import net.minecraft.tags.TagKey
import net.minecraft.util.RandomSource
import net.minecraft.world.level.biome.Biome
class SpecificBiomeCondition : BerrySpawnCondition {
lateinit var biome: TagKey<Biome>
val minGroveSize: Int = 1
val maxGroveSize: Int = 1
override fun canSpawn(berry: Berry, biome: Holder<Biome>) = biome.`is`(this.biome)
override fun getGroveSize(random: RandomSource) = random.nextIntBetweenInclusive(minGroveSize, maxGroveSize)
companion object {
val ID = cobblemonResource("specific_biome")
}
}
@@ -22,7 +22,6 @@ interface PokemonSideDelegate : EntitySideDelegate<PokemonEntity> {
fun handleStatus(status: Byte) {}
fun spawnShinyParticle(player: Player) {}
fun spawnAspectParticle() {}
fun applyRenderRotation(partialTick: Float) {}
fun positionRider(passenger: Entity, positionUpdater: MoveFunction)
fun updateAge(age: Int) {}
}
@@ -17,11 +17,18 @@ object SpawnBaitUtils {
return grouped.map { (key, groupEffects) ->
val (type, subcategory) = key
val count = groupEffects.size
val totalChance = groupEffects.sumOf { it.chance }
val totalValue = groupEffects.sumOf { it.value }
val adjustedChance = totalChance.coerceAtMost(1.0)
val adjustedValue = ceil(totalValue)
val multiplier = when (count) {
2 -> 0.8
3 -> 0.6
else -> 1.0
}
val adjustedChance = (totalChance * multiplier).coerceAtMost(1.0)
val adjustedValue = ceil(totalValue * multiplier)
SpawnBait.Effect(type, subcategory, adjustedChance, adjustedValue)
}
@@ -12,6 +12,7 @@ import com.bedrockk.molang.runtime.MoLangEnvironment
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.MoParams
import com.bedrockk.molang.runtime.struct.ArrayStruct
import com.bedrockk.molang.runtime.struct.MoStruct
import com.bedrockk.molang.runtime.struct.QueryStruct
import com.bedrockk.molang.runtime.struct.VariableStruct
import com.bedrockk.molang.runtime.value.DoubleValue
@@ -39,6 +40,7 @@ import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import com.cobblemon.mod.common.api.moves.animations.ActionEffects
import com.cobblemon.mod.common.api.moves.animations.NPCProvider
import com.cobblemon.mod.common.api.moves.animations.TargetsProvider
import com.cobblemon.mod.common.api.npc.NPCClass
import com.cobblemon.mod.common.api.npc.NPCClasses
import com.cobblemon.mod.common.api.npc.configuration.interaction.DialogueNPCInteractionConfiguration
import com.cobblemon.mod.common.api.npc.configuration.interaction.ScriptNPCInteractionConfiguration
@@ -55,6 +57,7 @@ import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
import com.cobblemon.mod.common.api.pokemon.experience.SidemodExperienceSource
import com.cobblemon.mod.common.api.pokemon.moves.LearnsetQuery
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.riding.Rideable
import com.cobblemon.mod.common.api.riding.stats.RidingStat
import com.cobblemon.mod.common.api.scheduling.ClientTaskTracker
import com.cobblemon.mod.common.api.scheduling.Schedulable
@@ -134,6 +137,7 @@ import net.minecraft.world.entity.LightningBolt
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.Mob
import net.minecraft.world.entity.PathfinderMob
import net.minecraft.world.entity.PlayerRideable
import net.minecraft.world.entity.TamableAnimal
import net.minecraft.world.entity.ai.behavior.BlockPosTracker
import net.minecraft.world.entity.ai.memory.MemoryModuleType
@@ -141,6 +145,7 @@ import net.minecraft.world.entity.ai.memory.MemoryStatus
import net.minecraft.world.entity.ai.memory.WalkTarget
import net.minecraft.world.entity.animal.Animal
import net.minecraft.world.entity.item.ItemEntity
import net.minecraft.world.entity.monster.Enemy
import net.minecraft.world.entity.monster.Monster
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
@@ -155,6 +160,9 @@ import net.minecraft.world.level.levelgen.Heightmap
import net.minecraft.world.level.pathfinder.PathType
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
import kotlin.text.get
import kotlin.text.toInt
import kotlin.toString
/**
* Holds a bunch of useful MoLang trickery that can be used or extended in API
@@ -1177,28 +1185,16 @@ object MoLangFunctions {
return@put DoubleValue.ONE
}
map.put("has_memory_value") { params ->
for (param in params.params) {
if (!entity.hasMemoryFromString(param.asString())) {
return@put DoubleValue.ZERO
}
}
return@put DoubleValue.ONE
val memoryTypes = params.params.map {
it.asString().asIdentifierDefaultingNamespace()
}.map(BuiltInRegistries.MEMORY_MODULE_TYPE::get)
return@put DoubleValue(memoryTypes.all { entity.brain.checkMemory(it, MemoryStatus.VALUE_PRESENT) })
}
map.put("lacks_memory_value") { params ->
for (param in params.params) {
if (entity.hasMemoryFromString(param.asString())) {
return@put DoubleValue.ZERO
}
}
return@put DoubleValue.ONE
}
map.put("is_doing_activity") { params ->
for (param in params.params) {
if (entity.isDoingActivityFromString(param.asString())) {
return@put DoubleValue.ONE
}
}
return@put DoubleValue.ZERO
val memoryTypes = params.params.map {
it.asString().asIdentifierDefaultingNamespace()
}.map(BuiltInRegistries.MEMORY_MODULE_TYPE::get)
return@put DoubleValue(memoryTypes.none { entity.brain.checkMemory(it, MemoryStatus.VALUE_PRESENT) })
}
map.put("can_see") { params ->
val target = params.get<MoValue>(0)
@@ -1256,6 +1252,15 @@ object MoLangFunctions {
map.put("in_battle") { DoubleValue(npc.isInBattle()) }
map.put("battles") { ArrayStruct(npc.battleIds.mapNotNull { BattleRegistry.getBattle(it)?.struct }.mapIndexed { index, value -> "$index" to value }.toMap()) }
map.put("stop_battles") { _ -> npc.battleIds.forEach { BattleRegistry.getBattle(it)?.stop() } }
map.put("is_doing_activity") { params ->
val identifiers = params.params.map { it.asString().asIdentifierDefaultingNamespace(namespace = "minecraft") }
val activities = identifiers.mapNotNull { identifier -> BuiltInRegistries.ACTIVITY.get(identifier) }
if (activities.isNotEmpty()) {
return@put DoubleValue(activities.any { activity -> npc.brain.isActive(activity) })
} else {
return@put DoubleValue.ZERO
}
}
map.put("run_script_on_client") { params ->
val world = npc.level()
if (world is ServerLevel) {
@@ -1900,9 +1905,6 @@ object MoLangFunctions {
map.put("is_holding_item") { DoubleValue(!pokemonEntity.entityData.get(PokemonEntity.SHOWN_HELD_ITEM).let {
it.isEmpty || it.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) || it.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS)
}) }
map.put("riding_style") {
StringValue(pokemonEntity.ifRidingAvailableSupply("") { behaviour, settings, state -> behaviour.getRidingStyle(settings, state).name })
}
map.put("is_wearing_hat") { DoubleValue(pokemonEntity.entityData.get(PokemonEntity.SHOWN_HELD_ITEM).`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS)) }
map.put("is_wearing_face") { DoubleValue(pokemonEntity.entityData.get(PokemonEntity.SHOWN_HELD_ITEM).`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS)) }
map.put("is_pastured") {
@@ -53,7 +53,8 @@ open class MoveTemplate(
val pp: Int,
val priority: Int,
val critRatio: Double,
val effectChances: Array<Double>
val effectChances: Array<Double>,
val actionEffect: ActionEffectTimeline?
) {
val struct: MoStruct by lazy {
QueryStruct(hashMapOf())
@@ -86,7 +87,8 @@ open class MoveTemplate(
pp = 5,
priority = 0,
critRatio = 0.0,
effectChances = emptyArray()
effectChances = emptyArray(),
actionEffect = null
)
companion object {
@@ -92,7 +92,18 @@ object Moves : DataRegistry {
effectChances += secondaryMember.get("chance").asDouble
}
}
val move = MoveTemplate(id, num, elementalType, damageCategory, power, target, accuracy, pp, priority, critRatio, effectChances.toTypedArray())
val actionEffect = ActionEffects.actionEffects[id.asIdentifierDefaultingNamespace()]
?: run {
ActionEffects.actionEffects["generic_move".asIdentifierDefaultingNamespace()]
// if (damageCategory == DamageCategories.STATUS) {
// ActionEffects.actionEffects[cobblemonResource("status")]
// } else {
// val type = elementalType.name.lowercase()
// val category = damageCategory.name.lowercase()
// ActionEffects.actionEffects["${category}_$type".asIdentifierDefaultingNamespace()]
// }
}
val move = MoveTemplate(id, num, elementalType, damageCategory, power, target, accuracy, pp, priority, critRatio, effectChances.toTypedArray(), actionEffect)
this.register(move)
} catch (e: Exception) {
Cobblemon.LOGGER.error("Caught exception trying to resolve the move '{}'", id, e)
@@ -14,7 +14,6 @@ import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.moves.animations.keyframes.*
import com.cobblemon.mod.common.api.pokemon.stats.Stat
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.pokemon.adapters.CobblemonStatTypeAdapter
import com.cobblemon.mod.common.util.adapters.*
import com.cobblemon.mod.common.util.cobblemonResource
@@ -92,11 +91,5 @@ object ActionEffects : JsonDataRegistry<ActionEffectTimeline> {
observable.emit(this)
}
fun getEffectWithBattleContext(id: ResourceLocation, pokemon: BattlePokemon): ActionEffectTimeline? {
val species = pokemon.entity?.exposedSpecies ?: pokemon.effectedPokemon.species
val contextedEffect = actionEffects[ResourceLocation.fromNamespaceAndPath(id.namespace, id.path + "_" + species)]
return contextedEffect ?: actionEffects[id]
}
override fun sync(player: ServerPlayer) {}
}
@@ -10,17 +10,13 @@ package com.cobblemon.mod.common.api.orientation
import com.cobblemon.mod.common.util.math.geometry.toDegrees
import com.cobblemon.mod.common.util.math.geometry.toRadians
import com.mojang.blaze3d.Blaze3D
import net.minecraft.util.Mth
import net.minecraft.world.entity.LivingEntity
import org.joml.AxisAngle4f
import org.joml.Matrix3f
import org.joml.Quaternionf
import org.joml.Vector3f
import kotlin.math.abs
import kotlin.math.asin
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sign
open class OrientationController(val entity: LivingEntity) {
@@ -39,11 +35,6 @@ open class OrientationController(val entity: LivingEntity) {
private var renderOrientationO: Matrix3f? = null
private var renderOrientation: Matrix3f? = null
private var angularVelocity: Vector3f = Vector3f(0.0f, 0.0f, 0.0f)
private var lastHandledRotationTime = Double.MIN_VALUE
private var frameTime = 0.0
/** Adding this simply because it irritates me seeing `getActive` when most of this is Java. */
fun isActive() = active
@@ -72,7 +63,7 @@ open class OrientationController(val entity: LivingEntity) {
}
fun getRenderOrientation(delta: Float): Quaternionf {
val old = renderOrientationO ?: renderOrientation ?: orientation ?: Matrix3f()
val old = renderOrientationO ?: renderOrientation ?: orientation
val new = renderOrientation ?: old
val oldQuat = Quaternionf().setFromUnnormalized(old)
val newQuat = Quaternionf().setFromUnnormalized(new)
@@ -81,93 +72,18 @@ open class OrientationController(val entity: LivingEntity) {
}
fun tick() {
// val d = Blaze3D.getTime();
// this.frameTime =
// if (this.lastHandledRotationTime != Double.MIN_VALUE) d - this.lastHandledRotationTime else 0.0;
// this.lastHandledRotationTime = d;
//
// if (!active || orientation == null) return
//
// // Find the desired angular velocity
// // lerp the current ang vel towards the desired at a set rate (does this need to take into account frametime?
// val current = orientation
// val renderMatrix = this.renderOrientation ?: Matrix3f()
// val renderQuat = Quaternionf().setFromUnnormalized(renderMatrix)
// val targetQuat = Quaternionf().setFromUnnormalized(current)
//
// // Max rate of angular change in radians per second
// val maxAngVel = Math.toRadians(180.0).toFloat()
//
// // Find the difference between the current render orientation and the target orientation
// val qDiff: Quaternionf = targetQuat.mul(renderQuat.invert())
//
// // Construct the target rate of change from the difference quat
// val axAng = qDiff.get(AxisAngle4f())
// val capdRate = axAng.angle.coerceIn(-maxAngVel, maxAngVel)
// val targetAngVel = Vector3f(
// axAng.x,
// axAng.y,
// axAng.z
// ).mul(capdRate)
//
// // lerp the current angular velocity towards the target
// val k = 10.0f // Some smoothing constant k
// val smoothingFactor = Math.min(1.0f, this.frameTime.toFloat() * k)
// angularVelocity = angularVelocity.lerp(targetAngVel, smoothingFactor)
//
// // Use the angular velocity to slerp the renderQuat toward the targetQuat
// val frameDisplacement =
// angularVelocity.mul(this.frameTime.toFloat()) // Angular displacement happening on this frame
//
// val fAng = frameDisplacement.length() // Angle of displacement for this given pass/frame
// if (fAng > 0.0001f) { // Ensure that the angle is significant enough that its worth normalizing and carrying out rotation
// val fAx = frameDisplacement.normalize()
// // Construct a rotation quat from the
// val rotQuat = Quaternionf().fromAxisAngleRad(fAx, fAng)
//
// // Set the new orientation post frame stepped rotation.
// renderOrientation = Matrix3f().set(renderQuat.mul(rotQuat))
// }
//
//// this.renderOrientation = orientation
//
// // apply the new current angular velocity to the current render orientation
// // construct a quat out of the angular velocity
// // slerp the quat with a value of frametime
val d = Blaze3D.getTime();
this.frameTime = if (this.lastHandledRotationTime != Double.MIN_VALUE) d - this.lastHandledRotationTime else 0.0;
this.lastHandledRotationTime = d;
if (!active || orientation == null) return
renderOrientationO = renderOrientation
val current = orientation
val renderMatrix = this.renderOrientation ?: Matrix3f()
// Create mutable copies for manipulation
val renderMatrix = this.renderOrientation ?: current
val renderQuat = Quaternionf().setFromUnnormalized(renderMatrix)
var targetQuat = Quaternionf().setFromUnnormalized(current)
val targetQuat = Quaternionf().setFromUnnormalized(current)
val dampingFactor = 0.66f // We can change this factor for faster transitions
renderQuat.slerp(targetQuat, dampingFactor)
// --- Shortest Path Check (Still mandatory for Slerp) ---
if (renderQuat.dot(targetQuat) < 0) {
targetQuat = targetQuat.invert()
}
// --------------------------------------------------------
// K is the speed (rad/s) we want to close the gap at.
val k_rate = 10.0f // E.g., target 10 units/second closing rate.
// Calculate the frame-rate independent alpha (t) for Slerp.
// This makes the smoothing constant 'k_rate' a rate of change, not an arbitrary alpha value.
val slerpAlpha = 1.0f - Math.exp(-(k_rate * this.frameTime)).toFloat()
// Use Slerp to interpolate the renderQuat towards the targetQuat
val newRenderQuat = renderQuat.slerp(targetQuat, slerpAlpha, Quaternionf())
// Set the new orientation
renderOrientation = Matrix3f().set(newRenderQuat)
// Angular velocity is now redundant for rendering, but could be used for particle effects, etc.
val newRenderOrientation = Matrix3f()
renderQuat.get(newRenderOrientation)
renderOrientation = newRenderOrientation
}
val forwardVector: Vector3f
@@ -170,8 +170,11 @@ object Natures {
* Utility method to get a nature by string
* @return a nature type or null
*/
fun getNature(identifier: String) = getNature(cobblemonResource(identifier))
?: getNature(ResourceLocation.parse(identifier))
fun getNature(identifier: String): Nature? {
val nature = getNature(cobblemonResource(identifier))
if(nature != null) return nature
return getNature(ResourceLocation.parse(identifier))
}
/**
* Helper function for a random Nature
@@ -806,8 +806,8 @@ open class PokemonProperties {
return PokemonProperties().loadFromJSON(this.saveToJSON())
}
// If the config value is at least 0, then do 1/x and use that as the property chance
private fun Float.checkRate(): Boolean = this > 0 && (Random.nextFloat() < 1 / this)
// If the config value is at least 1, then do 1/x and use that as the property chance
private fun Float.checkRate(): Boolean = if (this >= 1) (Random.Default.nextFloat() < 1 / this) else Random.Default.nextFloat() < this
/**
* Attempts to find an ability by ID and resolve if it should be forced or if it's legal for the given form.
@@ -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.block.entity.PokeSnackBlockEntity
import com.cobblemon.mod.common.api.spawning.prospecting.LurePokeSnackDetector
import com.cobblemon.mod.common.client.entity.PokemonClientDelegate
import net.minecraft.core.particles.BlockParticleOption
import net.minecraft.core.particles.ParticleOptions
@@ -23,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),
PokeSnackBlockEntity.POKE_SNACK_CRUMBED_ASPECT to ParticleData.MinecraftParticle(
LurePokeSnackDetector.POKE_SNACK_CRUMBED_ASPECT to ParticleData.MinecraftParticle(
BlockParticleOption(
ParticleTypes.BLOCK,
CobblemonBlocks.POKE_SNACK.defaultBlockState()
@@ -59,7 +59,7 @@ class RidingController(
val newKey = behaviourSettings.key
for (passenger in entity.passengers) {
if (passenger is Player && passenger !is ServerPlayer) {
MountedCameraTypeHandler.handleTransition(passenger, previousKey ?: continue, newKey)
MountedCameraTypeHandler.handleTransition(previousKey ?: continue, newKey)
}
}
@@ -295,6 +295,32 @@ class JetBehaviour : RidingBehaviour<JetSettings, JetState> {
mouseXSmoother: SmoothDouble,
sensitivity: Double,
deltaTime: Double
): Vec3 {
return rollRotation(
settings,
state,
vehicle,
driver,
mouseY,
mouseX,
mouseYSmoother,
mouseXSmoother,
sensitivity,
deltaTime
)
}
fun rollRotation(
settings: JetSettings,
state: JetState,
vehicle: PokemonEntity,
driver: Player,
mouseY: Double,
mouseX: Double,
mouseYSmoother: SmoothDouble,
mouseXSmoother: SmoothDouble,
sensitivity: Double,
deltaTime: Double
): Vec3 {
if (vehicle !is OrientationControllable) return Vec3.ZERO
@@ -358,7 +358,32 @@ class DolphinBehaviour : RidingBehaviour<DolphinSettings, DolphinState> {
} else {
state.noInputTime.set(0.0)
}
return rollRotation(
settings,
state,
vehicle,
driver,
mouseY,
mouseX,
mouseYSmoother,
mouseXSmoother,
sensitivity,
deltaTime
)
}
fun rollRotation(
settings: DolphinSettings,
state: DolphinState,
vehicle: PokemonEntity,
driver: Player,
mouseY: Double,
mouseX: Double,
mouseYSmoother: SmoothDouble,
mouseXSmoother: SmoothDouble,
sensitivity: Double,
deltaTime: Double
): Vec3 {
if (vehicle !is OrientationControllable) return Vec3.ZERO
val controller = (vehicle as OrientationControllable).orientationController
val topSpeed = vehicle.runtime.resolveDouble(settings.speedExpr ?: globalDolphin.speedExpr!!) / 20.0
@@ -57,9 +57,9 @@ class SubmarineBehaviour : RidingBehaviour<SubmarineSettings, SubmarineState> {
}
val poseProvider = PoseProvider<SubmarineSettings, SubmarineState>(PoseType.FLOAT)
.with(PoseOption(PoseType.SWIM) { _, state, entity -> abs(state.rideVelocity.get().z) > 0.05 && !state.onSurface.get()})
.with(PoseOption(PoseType.STAND) { _, state, entity -> abs(state.rideVelocity.get().z) <= 0.05 && state.onSurface.get()})
.with(PoseOption(PoseType.STAND) { _, state, entity -> abs(state.rideVelocity.get().z) > 0.05 && state.onSurface.get()})
.with(PoseOption(PoseType.SWIM) { _, state, entity -> state.rideVelocity.get().z > 0.05 && !state.onSurface.get()})
.with(PoseOption(PoseType.STAND) { _, state, entity -> state.rideVelocity.get().z <= 0.05 && state.onSurface.get()})
.with(PoseOption(PoseType.STAND) { _, state, entity -> state.rideVelocity.get().z > 0.05 && state.onSurface.get()})
override fun isActive(settings: SubmarineSettings, state: SubmarineState, vehicle: PokemonEntity): Boolean {
return vehicle.isInWater || !vehicle.onGround()
@@ -123,13 +123,15 @@ class SubmarineBehaviour : RidingBehaviour<SubmarineSettings, SubmarineState> {
return
}
val hitboxBreachPercentage = vehicle.runtime.resolveDouble(settings.hitboxBreachPercentage ?: globalSubmarine.hitboxBreachPercentage!!)
// if it is then check half the bbheight above and below the center point.
// Grab the position of the vehicle that when out of the water is considered breached
val vehicleBreachPoint = vehicle.position().add(0.0, vehicle.bbHeight * hitboxBreachPercentage, 0.0)
// If the point above is air and the point below is water then you know to trend towards on surface.
// Grab the center position of the vehicle (vehicle.position.add(bbheight/2)
val vehicleCenter = vehicle.position().add(0.0, vehicle.bbHeight/2.0, 0.0)
// If the block above the center is not fluid but this one is then move the center
val centerBlockPos = BlockPos.containing(vehicleBreachPoint)
val centerBlockPos = BlockPos.containing(vehicleCenter)
val centerInWater = !vehicle.level().getFluidState(centerBlockPos).isEmpty
val abovePos = centerBlockPos.above()
@@ -559,10 +561,6 @@ class SubmarineSettings : RidingBehaviourSettings {
var staminaRegenTime: Expression? = null
private set
// Percentage (0.0->1.0) of ride needing to be breached out of the water to be considered surfaced.
var hitboxBreachPercentage: Expression? = null
private set
// Tope strafe speed in Bl/s
var jumpExpr: Expression? = null
// Yaw rate in degrees per second
@@ -581,7 +579,6 @@ class SubmarineSettings : RidingBehaviourSettings {
rideSounds.encode(buffer)
buffer.writeNullableExpression(infiniteStamina)
buffer.writeNullableExpression(staminaRegenTime)
buffer.writeNullableExpression(hitboxBreachPercentage)
buffer.writeNullableExpression(jumpExpr)
buffer.writeNullableExpression(handlingExpr)
buffer.writeNullableExpression(speedExpr)
@@ -594,7 +591,6 @@ class SubmarineSettings : RidingBehaviourSettings {
rideSounds = RideSoundSettingsList.decode(buffer)
infiniteStamina = buffer.readNullableExpression()
staminaRegenTime = buffer.readNullableExpression()
hitboxBreachPercentage = buffer.readNullableExpression()
jumpExpr = buffer.readNullableExpression()
handlingExpr = buffer.readNullableExpression()
speedExpr = buffer.readNullableExpression()
@@ -18,11 +18,18 @@ import com.cobblemon.mod.common.api.spawning.condition.SeafloorSpawningCondition
import com.cobblemon.mod.common.api.spawning.condition.SpawningCondition
import com.cobblemon.mod.common.api.spawning.condition.SubmergedSpawningCondition
import com.cobblemon.mod.common.api.spawning.condition.SurfaceSpawningCondition
import com.cobblemon.mod.common.api.spawning.position.calculators.GroundedSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.LavafloorSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SeafloorSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SubmergedSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SurfaceSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.detail.NPCSpawnDetail
import com.cobblemon.mod.common.api.spawning.detail.PokemonHerdSpawnDetail
import com.cobblemon.mod.common.api.spawning.detail.PokemonSpawnDetail
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.api.spawning.fishing.FishingSpawner
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePositionResolver
import com.cobblemon.mod.common.api.spawning.position.FishingSpawnablePosition
@@ -32,21 +39,16 @@ import com.cobblemon.mod.common.api.spawning.position.SeafloorSpawnablePosition
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.position.SubmergedSpawnablePosition
import com.cobblemon.mod.common.api.spawning.position.SurfaceSpawnablePosition
import com.cobblemon.mod.common.api.spawning.position.calculators.GroundedSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.LavafloorSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SeafloorSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SubmergedSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SurfaceSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.preset.BasicSpawnDetailPreset
import com.cobblemon.mod.common.api.spawning.preset.BestSpawnerConfig
import com.cobblemon.mod.common.api.spawning.preset.PokemonSpawnDetailPreset
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
import com.cobblemon.mod.common.api.spawning.spawner.BasicSpawner
import com.cobblemon.mod.common.api.spawning.spawner.AreaSpawner
import com.cobblemon.mod.common.api.spawning.spawner.FixedAreaSpawner
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawnerFactory
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.api.spawning.spawner.TickingSpawner
import com.cobblemon.mod.common.entity.pokemon.CobblemonAgingDespawner
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import net.minecraft.server.MinecraftServer
@@ -64,13 +66,14 @@ import net.minecraft.server.MinecraftServer
* In the case of more specialized use, the creation of a [SpawnablePosition] that motivates most of the spawn
* process can be created manually, skipping the first two steps.
*
* An individually spawnable thing is defined as a [SpawnDetail]. A processor handling this process is a [Spawner].
* Some subclasses exist for more specialized cases. If you are spawning in a fixed area and the spawner controlling
* An individually spawnable entity is defined as a [SpawnDetail]. A processor handling this process is a [Spawner].
* Various subclasses exist for more specialized cases. A spawner that is constantly ticking and will spawn things
* without prompts is a [TickingSpawner], and one of those which occurs within a defined area is a [AreaSpawner]. If
* that area is unmoving then it is a [FixedAreaSpawner] whereas if it is actively following the player it is a
* [PlayerSpawner]. You can also use a [BasicSpawner] for more general-purpose spawning without any issue.
* [PlayerSpawner].
*
* PlayerSpawners are stored inside ServerPlayers using a mixin and tick at the end of a player's tick. The [PlayerSpawnerFactory]
* is used to build those. An extension function exists for getting the spawner for a player.
* Spawning is coordinated and ticked using a [SpawnerManager], and all the current managers are accessible from
* [BestSpawner.spawnerManagers].
*
* Spawners and spawnable positions are often put under the effects of [SpawningInfluence]s which can be used to make
* temporary or lasting changes to spawning for whatever component they are attached to (whether that is a spawner or a
@@ -84,9 +87,9 @@ import net.minecraft.server.MinecraftServer
*/
object BestSpawner {
var config = BestSpawnerConfig()
val spawnerManagers = mutableListOf<SpawnerManager>(CobblemonWorldSpawnerManager)
lateinit var defaultPokemonDespawner: Despawner<PokemonEntity>
lateinit var fishingSpawner: BasicSpawner
lateinit var fishingSpawner: FishingSpawner
fun init() {
LOGGER.info("Starting the Best Spawner...")
@@ -134,13 +137,12 @@ object BestSpawner {
fun reloadConfig() {
loadConfig()
spawnerManagers.forEach(SpawnerManager::onConfigReload)
}
fun onServerStarted(server: MinecraftServer) {
CobblemonSpawnPools.onServerLoad(server)
fishingSpawner = BasicSpawner(
name = "fishing",
spawnPool = CobblemonSpawnPools.WORLD_SPAWN_POOL
)
spawnerManagers.forEach(SpawnerManager::onServerStarted)
fishingSpawner = FishingSpawner()
}
}
@@ -40,6 +40,7 @@ object CobblemonSpawnPools {
fun onServerLoad(server: MinecraftServer) {
Cobblemon.LOGGER.info("Optimizing spawn pools...")
WORLD_SPAWN_POOL.onServerLoad(server)
WORLD_SPAWN_POOL.forEach { it.onServerLoad(server) }
WORLD_SPAWN_POOL.precalculate()
}
}
@@ -0,0 +1,76 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawnerFactory
import com.cobblemon.mod.common.platform.events.PlatformEvents
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules
import net.minecraft.server.level.ServerLevel
import java.util.UUID
import net.minecraft.server.level.ServerPlayer
/**
* The [SpawnerManager] that handles Cobblemon's world spawner, which is made up
* of [PlayerSpawner] instances. This manager listens for login and logout and
* cleans up where relevant.
*
* @author Hiroku
* @since February 14th, 2022
*/
object CobblemonWorldSpawnerManager : SpawnerManager() {
var spawnersForPlayers = mutableMapOf<UUID, PlayerSpawner>()
init {
PlatformEvents.SERVER_PLAYER_LOGIN.subscribe { this.onPlayerLogin(it.player) }
PlatformEvents.SERVER_PLAYER_LOGOUT.subscribe { this.onPlayerLogout(it.player) }
PlatformEvents.CHANGE_DIMENSION.subscribe { this.onDimensionChange(it.player, it.origin, it.destination) }
}
fun onPlayerLogin(player: ServerPlayer) {
// Disables spawning
if (!Cobblemon.config.enableSpawning || player.level().gameRules.getBoolean(CobblemonGameRules.DO_POKEMON_SPAWNING) == false) {
return;
}
registerPlayerSpawner(player)
}
fun onPlayerLogout(player: ServerPlayer) {
unregisterPlayerSpawner(player)
}
fun onDimensionChange(player: ServerPlayer, origin: ServerLevel?, destination: ServerLevel?) {
fun getSpawnRule(level: ServerLevel?) = level?.gameRules?.getBoolean(CobblemonGameRules.DO_POKEMON_SPAWNING)
if (!Cobblemon.config.enableSpawning || getSpawnRule(origin) == getSpawnRule(destination)) {
return
}
val spawnerAction = if (destination?.gameRules?.getBoolean(CobblemonGameRules.DO_POKEMON_SPAWNING) == false) {
::unregisterPlayerSpawner
} else {
::registerPlayerSpawner
}
spawnerAction(player)
}
private fun unregisterPlayerSpawner(player: ServerPlayer) {
spawnersForPlayers.remove(player.uuid)?.let { unregisterSpawner(it) }
}
private fun registerPlayerSpawner(player: ServerPlayer) {
PlayerSpawnerFactory.create(this, player).also {
spawnersForPlayers[player.uuid] = it
registerSpawner(it)
}
}
}
@@ -17,21 +17,16 @@ import net.minecraft.world.entity.EntityType
open class SpawnCause(
val spawner: Spawner,
entity: Entity? = null
) : SpawningInfluence {
): SpawningInfluence {
val entityWorldId = entity?.level()?.dimension()
val entityId = entity?.id
val entityUUID = entity?.uuid
val entityType = entity?.type
val entity: Entity?
get() =
if (entityType == EntityType.PLAYER) {
entityUUID?.let {
server()?.playerList?.getPlayer(it)
}
} else if (entityWorldId != null && entityId != null) {
server()?.getLevel(entityWorldId)?.getEntity(entityId)
} else {
null
}
get() = if (entityType == EntityType.PLAYER) {
server()?.playerList?.getPlayer(entityUUID!!)
} else {
server()?.getLevel(entityWorldId!!)?.getEntity(entityId!!)
}
}
@@ -0,0 +1,85 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.api.spawning.spawner.TickingSpawner
import com.cobblemon.mod.common.util.server
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules.DO_POKEMON_SPAWNING
/**
* A manager of various spawners. This is a class in which you should register
* ticking spawners so that they can be automatically ticked by the server.
* This also supports registering [SpawningInfluence]s to apply to each spawner
* that is registered.
*
* @author Hiroku
* @since February 5th, 2022
*/
open class SpawnerManager {
val spawners = mutableListOf<Spawner>()
get() {
if (server()?.isSameThread == false) {
Cobblemon.LOGGER.error("Illegal access to spawners list from non-server thread!")
Exception().printStackTrace()
}
return field
}
val influences = mutableListOf<SpawningInfluence>()
get() {
if (server()?.isSameThread == false) {
Cobblemon.LOGGER.error("Illegal access to influences from non-server thread!")
Exception().printStackTrace()
}
return field
}
inline fun <reified T : Spawner> getSpawnersOfType() = spawners.filterIsInstance<T>()
open fun getSpawnerByName(name: String) = spawners.find { it.name == name }
open fun registerSpawner(spawner: Spawner) {
spawners.add(spawner)
if (spawner !is TickingSpawner) {
spawner.influences.addAll(influences)
}
}
open fun unregisterSpawner(spawner: Spawner) {
spawners.remove(spawner)
if (spawner !is TickingSpawner) {
spawner.influences.removeAll(influences)
}
}
open fun onConfigReload() {
spawners.filterIsInstance<PlayerSpawner>().forEach {
it.ticksBetweenSpawns = Cobblemon.config.ticksBetweenSpawnAttempts
}
}
open fun onServerStarted() {
spawners.clear()
}
open fun onServerTick() {
// Disables spawning
if (!Cobblemon.config.enableSpawning) {
return
}
influences.removeIf { it.isExpired() }
getSpawnersOfType<TickingSpawner>().forEach {
if (it.getCauseEntity()?.level()?.gameRules?.getBoolean(DO_POKEMON_SPAWNING) == false) {
return@forEach
}
it.tick()
}
}
}
@@ -33,15 +33,15 @@ class TimeRange : IntRanges {
"any" to TimeRange(0..23999),
"day" to TimeRange(23460..23999, 0..12541),
"night" to TimeRange(12542..23459),
"morning" to TimeRange(23000..23999, 0..4999),
"noon" to TimeRange(5000..6999),
"afternoon" to TimeRange(7000..12999),
"evening" to TimeRange(13000..16999),
"midnight" to TimeRange(17000..18999),
"predawn" to TimeRange(19000..22999),
"dawn" to TimeRange(22300..23999, 0..166),
"dawn" to TimeRange(22300..23999, 0..999),
"dusk" to TimeRange(11834..13701),
"twilight" to TimeRange(11834..13701, 22300..23999, 0..166)
"twilight" to TimeRange(11834..13701, 22300..23999, 0..166),
"morning" to TimeRange(23000..23999, 0..4999),
"afternoon" to TimeRange(7000..12999),
"predawn" to TimeRange(19000..22999),
"evening" to TimeRange(13000..16999)
)
}
@@ -39,13 +39,9 @@ import net.minecraft.server.MinecraftServer
*/
abstract class SpawnDetail : ModDependant {
companion object {
val pokemonTypes = mutableListOf<String>()
val spawnDetailTypes = mutableMapOf<String, RegisteredSpawnDetail<*>>()
fun <T : SpawnDetail> registerSpawnType(name: String, detailClass: Class<T>) {
spawnDetailTypes[name] = RegisteredSpawnDetail(detailClass)
if (detailClass == PokemonSpawnDetail::class.java || detailClass == PokemonHerdSpawnDetail::class.java) {
pokemonTypes.add(name)
}
}
}
@@ -24,7 +24,6 @@ import com.cobblemon.mod.common.util.server
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
@@ -73,13 +72,6 @@ class SpawnPool(val name: String) : JsonDataRegistry<SpawnSet>, Iterable<SpawnDe
return this
}
fun onServerLoad(server: MinecraftServer) {
for (detail in details) {
detail.onServerLoad(server)
}
precalculate()
}
/**
* Precalculates spawns into hash mappings using the [precalculators] included
* in this pool as well as the range of spawnable positions mentioned in the pool.
@@ -12,15 +12,16 @@ import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.fishing.BobberSpawnPokemonEvent
import com.cobblemon.mod.common.api.fishing.SpawnBait
import com.cobblemon.mod.common.api.fishing.SpawnBaitEffects
import com.cobblemon.mod.common.api.fishing.SpawnBait
import com.cobblemon.mod.common.api.pokemon.Natures
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.spawning.SpawnBucket
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
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.SpawnBaitInfluence
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.item.interactive.PokerodItem
@@ -0,0 +1,38 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.fishing
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.position.FishingSpawnablePosition
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.spawner.TriggerSpawner
import net.minecraft.server.level.ServerLevel
import net.minecraft.core.BlockPos
/**
* A spawner that takes in a [FishingSpawnCause] and spawns things.
*
* @author Hiroku
* @since February 3rd, 2024
*/
open class FishingSpawner(
override val name: String = "fishing",
pool: SpawnPool = CobblemonSpawnPools.WORLD_SPAWN_POOL
) : TriggerSpawner<FishingSpawnCause>(name, pool) {
override fun parseSpawnablePosition(cause: FishingSpawnCause, world: ServerLevel, pos: BlockPos): FishingSpawnablePosition? {
// Maybe confirm that it's water we're fishing in?
val spawnablePosition = FishingSpawnablePosition(
cause = cause,
world = world,
pos = pos,
influences = influences
)
return spawnablePosition
}
}
@@ -1,35 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.influence
import com.cobblemon.mod.common.api.spawning.SpawnBucket
/**
* A [SpawningInfluence] that multiplies bucket weights by the provided values
* and then normalizes the resulting weights so that their total adds up to 100.
*/
class BucketMultiplyingInfluence(
val multipliers: Map<String, Float>,
) : SpawningInfluence {
override fun affectBucketWeights(bucketWeights: MutableMap<SpawnBucket, Float>) {
val updates = mutableMapOf<SpawnBucket, Float>()
for ((bucket, weight) in bucketWeights) {
val multiplier = multipliers[bucket.name]
if (multiplier != null) {
updates[bucket] = weight * multiplier
}
}
for ((bucket, newWeight) in updates) {
bucketWeights[bucket] = newWeight
}
normalizeBucketWeights(bucketWeights)
}
}
@@ -44,6 +44,11 @@ open class BucketNormalizingInfluence(
bucketWeights[it] = weight.toDouble().pow(1 / normalizationFactor).toFloat()
}
normalizeBucketWeights(bucketWeights)
val sum = bucketWeights.values.sum()
val to100 = 100F / sum
bucketWeights.keys.toList().forEach {
val weight = bucketWeights[it] ?: return@forEach
bucketWeights[it] = weight * to100
}
}
}
@@ -9,7 +9,6 @@
package com.cobblemon.mod.common.api.spawning.influence
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.api.Priority
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.fishing.FishingSpawnCause
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
@@ -44,10 +43,6 @@ class SaccharineLogSlatheredInfluence(val pos: BlockPos? = null) : SpawningInflu
override fun affectSpawn(action: SpawnAction<*>, entity: Entity) {
if (entity is PokemonEntity) {
// avoid double application or applying to a pokemon that already has a hidden ability
if (entity.pokemon.forcedAspects.contains(HONEY_DRENCHED_ASPECT)
|| entity.pokemon.ability.priority !== Priority.LOWEST) return
if (Math.random() <= HIDDEN_ABILITY_CHANCE) {
FishingSpawnCause.alterHAAttempt(entity)
}
@@ -40,15 +40,6 @@ interface SpawningInfluence {
fun affectSpawn(action: SpawnAction<*>, entity: Entity) {}
/** Applies some influence over the weight of spawn buckets. */
fun affectBucketWeights(bucketWeights: MutableMap<SpawnBucket, Float>) {}
/** Normalizes all bucket weights to add up to 100 */
fun normalizeBucketWeights(bucketWeights: MutableMap<SpawnBucket, Float>) {
val sum = bucketWeights.values.sum()
val to100 = 100F / sum
bucketWeights.keys.toList().forEach {
val weight = bucketWeights[it] ?: return@forEach
bucketWeights[it] = weight * to100
}
}
/** Filters positions that could be usable for a spawnable position calculator */
fun isAllowedPosition(world: ServerLevel, pos: BlockPos, spawnablePositionCalculator: SpawnablePositionCalculator<*, *>): Boolean = true
/**
@@ -9,15 +9,16 @@
package com.cobblemon.mod.common.api.spawning.influence.detector
import com.cobblemon.mod.common.api.spawning.SpawningZone
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
import com.cobblemon.mod.common.api.spawning.influence.SpawningZoneInfluence
import com.cobblemon.mod.common.api.spawning.prospecting.IncenseSweetDetector
import com.cobblemon.mod.common.api.spawning.prospecting.SaccharineLogSlatheredDetector
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.api.spawning.spawner.SpawningZoneInput
import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.level.block.state.BlockState
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
import com.cobblemon.mod.common.api.spawning.prospecting.IncenseSweetDetector
import com.cobblemon.mod.common.api.spawning.prospecting.LurePokeSnackDetector
import com.cobblemon.mod.common.api.spawning.prospecting.SaccharineLogSlatheredDetector
/**
* Prospects for [SpawningZoneInfluence] in a world at a given position. Occurs as part of a
@@ -30,6 +31,7 @@ interface SpawningInfluenceDetector {
companion object {
@JvmStatic
val detectors = mutableSetOf<SpawningInfluenceDetector>(
LurePokeSnackDetector,
SaccharineLogSlatheredDetector,
IncenseSweetDetector
)
@@ -28,7 +28,7 @@ class FishingSpawnablePosition(
world: ServerLevel,
pos: BlockPos,
influences: MutableList<SpawningInfluence>
) : BasicSpawnablePosition(
) : TriggerSpawnablePosition(
cause = cause,
world = world,
position = pos,
@@ -14,15 +14,13 @@ import net.minecraft.server.level.ServerLevel
import net.minecraft.core.BlockPos
/**
* A spawnable position relating to single-point spawning. Uses a single-point structure cache which is
* the only contribution of this implementation really. By contrast, the area spawnable position provides
* a structure cache that's shared across the entire zone that was used for spawning, which is great for
* performance. That kind of optimization shouldn't be necessary for single-point spawning.
* A spawnable position relating to triggered spawning. Uses a single-point structure cache which is
* the only contribution of this implementation.
*
* @author Hiroku
* @since February 3rd, 2024
*/
open class BasicSpawnablePosition(
open class TriggerSpawnablePosition(
override val cause: SpawnCause,
override val world: ServerLevel,
override val position: BlockPos,
@@ -31,6 +29,6 @@ open class BasicSpawnablePosition(
override val canSeeSky: Boolean,
override val influences: MutableList<SpawningInfluence>
) : SpawnablePosition() {
val singlePointStructureCache = StructureChunkCache()
override fun getStructureCache(pos: BlockPos) = singlePointStructureCache
val triggerStructureCache = StructureChunkCache()
override fun getStructureCache(pos: BlockPos) = triggerStructureCache
}
@@ -36,7 +36,6 @@ interface SpawnablePositionCalculator<I : SpawnablePositionInput, O : SpawnableP
val isLavaCondition: (BlockState) -> Boolean = { it.fluidState.`is`(FluidTags.LAVA) && it.fluidState.isSource }
private val calculators = PrioritizedList<SpawnablePositionCalculator<*, *>>()
val prioritizedAreaCalculators: List<AreaSpawnablePositionCalculator<*>>
get() = calculators.filterIsInstance<AreaSpawnablePositionCalculator<*>>()
@@ -0,0 +1,189 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.prospecting
import com.cobblemon.mod.common.CobblemonPoiTypes
import com.cobblemon.mod.common.api.fishing.SpawnBait
import com.cobblemon.mod.common.api.spawning.influence.BucketNormalizingInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpatialSpawningZoneInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawnBaitInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawningZoneInfluence
import com.cobblemon.mod.common.api.spawning.influence.UnconditionalSpawningZoneInfluence
import com.cobblemon.mod.common.api.spawning.influence.detector.SpawningInfluenceDetector
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.api.spawning.spawner.SpawningZoneInput
import com.cobblemon.mod.common.block.PokeSnackBlock
import com.cobblemon.mod.common.block.entity.PokeSnackBlockEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.effect.PokeSnackBlockParticlesPacket
import com.cobblemon.mod.common.util.math.pow
import com.cobblemon.mod.common.util.toBlockPos
import kotlin.math.ceil
import kotlin.math.sqrt
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.core.Holder
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.ai.village.poi.PoiManager
import net.minecraft.world.entity.ai.village.poi.PoiType
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.phys.Vec3
import kotlin.math.abs
object LurePokeSnackDetector : SpawningInfluenceDetector {
@JvmField
val RANGE: Int = 48
@JvmField
val POKE_SNACK_CRUMBED_ASPECT = "poke_snack_crumbed"
override fun detectFromInput(spawner: Spawner, input: SpawningZoneInput): MutableList<SpawningZoneInfluence> {
val world = input.world
val listOfInfluences = mutableListOf<SpawningZoneInfluence>()
val searchRange = RANGE + ceil(sqrt(((input.length pow 2) + (input.width pow 2)).toDouble())).toInt()
val lurePokeSnackPositions = world.poiManager.findAll(
{ holder: Holder<PoiType> -> holder.`is`(CobblemonPoiTypes.POKE_SNACK_KEY) },
{ true },
input.getCenter().toBlockPos(),
searchRange,
PoiManager.Occupancy.ANY
).toList()
var highestLureTier = 0
for (lurePokeSnackPos in lurePokeSnackPositions) {
val blockState = world.getBlockState(lurePokeSnackPos)
if (blockState.block !is PokeSnackBlock) continue
val blockEntity = world.getBlockEntity(lurePokeSnackPos) as? PokeSnackBlockEntity ?: continue
val baitEffects = blockEntity.getBaitEffects()
highestLureTier = maxOf(highestLureTier, baitEffects.filter { it.type == SpawnBait.Effects.RARITY_BUCKET }.maxOfOrNull { it.value }?.toInt() ?: 0)
listOfInfluences.add(
SpatialSpawningZoneInfluence(
pos = lurePokeSnackPos,
radius = RANGE.toFloat(),
influence = SpawnBaitInfluence(
effects = baitEffects,
onUsed = { usedAmount, entity ->
entity?.let {
// Prevent triggering twice on same entity
if (usedAmount % 2 == 0) {
tryBite(world, lurePokeSnackPos)
moveAndApplyAspect(world, lurePokeSnackPos, entity)
}
}
}
)
)
)
}
if (highestLureTier > 0) {
listOfInfluences.add(UnconditionalSpawningZoneInfluence(influence = BucketNormalizingInfluence(tier = highestLureTier)))
}
return listOfInfluences
}
private fun tryBite(level: ServerLevel, blockPos: BlockPos) {
if (level.isLoaded(blockPos)) {
val blockState = level.getBlockState(blockPos)
val block = blockState.block as? PokeSnackBlock ?: return
block.eat(level, blockPos, blockState, null)
}
}
private fun moveAndApplyAspect(level: ServerLevel, blockPos: BlockPos, entity: PokemonEntity?) {
entity?.let {
entity.pokemon.forcedAspects += POKE_SNACK_CRUMBED_ASPECT
attemptSafeMove(level, blockPos, entity)
}
}
fun attemptSafeMove(level: ServerLevel, startPos: BlockPos, entity: PokemonEntity) {
val inWater = entity.isInWater
val maxDistance = 10 // maximum search radius
val maxPositions = 10 // maximum number of positions at the closest distance
var foundPositions: List<BlockPos> = emptyList()
// Search layer by layer, starting from the closest
for (distance in 1..maxDistance) {
val positionsAtThisDistance = mutableListOf<BlockPos>()
for (dx in -distance..distance) {
for (dy in -distance..distance) {
for (dz in -distance..distance) {
// Consider only positions on the surface of the cube at current distance
if (listOf(dx, dy, dz).count { abs(it) != 0 } == distance) {
val candidatePos = startPos.offset(dx, dy, dz)
if (!candidatePos.equals(startPos.relative(Direction.UP))) {
// Check collision
val currentBox = entity.boundingBox.move(
candidatePos.x - entity.x,
candidatePos.y - entity.y,
candidatePos.z - entity.z
)
if (level.noCollision(entity, currentBox)) {
val blockBelowPos = candidatePos.below()
val blockState = level.getBlockState(blockBelowPos)
val isWater = blockState.block == Blocks.WATER
if (inWater) {
if (isWater) {
positionsAtThisDistance.add(candidatePos)
}
} else {
if (!blockState.isAir) {
positionsAtThisDistance.add(candidatePos)
}
}
}
}
}
}
}
}
if (positionsAtThisDistance.isNotEmpty()) {
// Limit to 10 nearest positions (all at this distance)
foundPositions = if (positionsAtThisDistance.size > maxPositions) {
positionsAtThisDistance.shuffled().take(maxPositions)
} else {
positionsAtThisDistance
}
break // Found the nearest layer with safe positions, stop expanding
}
}
if (foundPositions.isNotEmpty()) {
val chosenPos = foundPositions.random()
val centerX = chosenPos.x + 0.5
val centerZ = chosenPos.z + 0.5
entity.moveTo(Vec3(centerX, chosenPos.y.toDouble(), centerZ))
PokeSnackBlockParticlesPacket(startPos, chosenPos).sendToPlayersAround(
startPos.x.toDouble(),
startPos.y.toDouble(),
startPos.z.toDouble(),
64.0,
level.dimension()
)
}
}
override fun detectFromBlock(level: ServerLevel, pos: BlockPos, blockState: BlockState) = emptyList<SpawningZoneInfluence>()
}
@@ -24,11 +24,6 @@ import com.cobblemon.mod.common.api.spawning.spawner.Spawner
* @since January 31st, 2022
*/
interface SpawningSelector<T : SpawnSelectionData> {
companion object {
@JvmField
val DEFAULT = FlatSpawnablePositionWeightedSelector()
}
fun getSelectionData(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>): T
fun selectSpawnAction(
@@ -37,12 +32,12 @@ interface SpawningSelector<T : SpawnSelectionData> {
selectionData: T
): SpawnAction<*>?
fun select(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>, maxSpawns: Int): List<SpawnAction<*>> {
fun select(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>, max: Int): List<SpawnAction<*>> {
val selectionData = getSelectionData(spawner, bucket, spawnablePositions)
val spawnActions = selectionData.spawnActions
while (spawnActions.size < maxSpawns) {
while (spawnActions.size < max) {
val spawnAction = selectSpawnAction(
spawner = spawner,
bucket = bucket,
@@ -0,0 +1,180 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.SpawnerManager
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePositionResolver
import com.cobblemon.mod.common.api.spawning.position.calculators.AreaSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SpawnablePositionCalculator.Companion.prioritizedAreaCalculators
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.isBoxLoaded
import com.cobblemon.mod.common.util.squeezeWithinBounds
import com.cobblemon.mod.common.util.toVec3f
import net.minecraft.core.BlockPos
import net.minecraft.core.SectionPos
import net.minecraft.world.level.ChunkPos
import net.minecraft.world.level.Level
import net.minecraft.world.level.chunk.ChunkAccess
import net.minecraft.world.level.chunk.status.ChunkStatus
import net.minecraft.world.level.pathfinder.PathComputationType
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
/**
* A type of [TickingSpawner] that operates within some area. When this spawner type
* is told to do a spawning action, the subclass can provide a [SpawningZoneInput] to use.
* If a non-null value is returned, the [spawningZoneGenerator] and [spawnablePositionResolver] will be used to
* select a spawn and action it.
*
* Subclasses must implement the function retrieving what area to do the spawning in,
* but otherwise this class is feature-complete.
*
* @author Hiroku
* @since February 5th, 2022
*/
abstract class AreaSpawner(
name: String,
spawns: SpawnPool,
manager: SpawnerManager
) : TickingSpawner(name, spawns, manager) {
abstract fun getZoneInput(cause: SpawnCause): SpawningZoneInput?
companion object {
const val CHUNK_REACH = 3
}
var spawningZoneGenerator: SpawningZoneGenerator = Cobblemon.spawningZoneGenerator
var spawnablePositionResolver: AreaSpawnablePositionResolver = Cobblemon.areaSpawnablePositionResolver
var spawnablePositionCalculators: List<AreaSpawnablePositionCalculator<*>> = prioritizedAreaCalculators
var spawnsPerPass: Int? = null
override fun run(cause: SpawnCause): List<SpawnAction<*>> {
val areaInput = getZoneInput(cause)
val constrainedArea = if (areaInput != null) constrainArea(areaInput) else null
if (constrainedArea != null) {
val areaBox = AABB.ofSize(
Vec3(constrainedArea.getCenter().toVec3f()),
CHUNK_REACH * 16.0 * 2,
1000.0,
CHUNK_REACH * 16.0 * 2
)
if (!constrainedArea.world.isBoxLoaded(areaBox)) {
return emptyList()
}
val numberNearby = constrainedArea.world.getEntitiesOfClass(
PokemonEntity::class.java,
areaBox,
PokemonEntity::countsTowardsSpawnCap
).size
val chunksCovered = CHUNK_REACH * CHUNK_REACH
if (numberNearby.toFloat() / chunksCovered >= Cobblemon.config.pokemonPerChunk) {
return emptyList()
}
//val prospectStart = System.currentTimeMillis()
val zone = spawningZoneGenerator.generate(this, constrainedArea)
//val prospectEnd = System.currentTimeMillis()
val spawnablePositions = spawnablePositionResolver.resolve(this, spawnablePositionCalculators, zone)
val influences = this.getAllInfluences() + zone.unconditionalInfluences
val bucket = chooseBucket(cause, influences)
//val resolveEnd = System.currentTimeMillis()
//val prospectDuration = prospectEnd - prospectStart
//val resolveDuration = resolveEnd - prospectEnd
//println("Prospecting took: $prospectDuration ms. Resolution took: $resolveDuration ms")
// Takes about 3ms on my laptop to prospect, similar to spawnable position resolve - not very good, needs some thought
return getSpawningSelector().select(spawner = this, bucket = bucket, spawnablePositions = spawnablePositions, max = spawnsPerPass ?: Cobblemon.config.maximumSpawnsPerPass)
}
return emptyList()
}
fun isValidStartPoint(world: Level, chunk: ChunkAccess, startPos: BlockPos.MutableBlockPos): Boolean {
val y = startPos.y
if (!world.isLoaded(startPos) || !world.isLoaded(startPos.setY(y + 1))) {
return false
}
val mid = chunk.getBlockState(startPos.setY(y))
val above = chunk.getBlockState(startPos.setY(y + 1))
// Above must be non-solid
if (!above.isPathfindable(PathComputationType.AIR)) {
return false
}
// Position must be non-air
if (mid.isAir) {
return false
}
return true
}
fun constrainArea(area: SpawningZoneInput): SpawningZoneInput? {
val basePos = BlockPos.MutableBlockPos(area.baseX, area.baseY, area.baseZ)
val originalY = area.baseY
val (chunkX, chunkZ) = Pair(SectionPos.blockToSectionCoord(area.baseX), SectionPos.blockToSectionCoord(area.baseZ))
// if the chunk isn't loaded, we don't want to go further & we don't want the getChunk function below to load/create the chunk.
if (!area.world.areEntitiesLoaded(ChunkPos.asLong(chunkX, chunkZ))) return null
val chunk = area.world.getChunk(chunkX, chunkZ, ChunkStatus.FULL) ?: return null
var valid = isValidStartPoint(area.world, chunk, basePos)
if (!valid) {
var offset = 1
do {
if (isValidStartPoint(area.world, chunk, basePos.setY(originalY + offset))) {
valid = true
basePos.y = originalY + offset
break
} else if (isValidStartPoint(area.world, chunk, basePos.setY(originalY - offset))) {
valid = true
basePos.y = originalY - offset
break
}
offset++
} while (offset <= Cobblemon.config.maxVerticalCorrectionBlocks)
}
if (valid) {
val min = area.world.squeezeWithinBounds(basePos)
val max = area.world.squeezeWithinBounds(basePos.move(area.length, area.height, area.width))
if (area.world.isLoaded(min) && area.world.isLoaded(max) &&
min.x < max.x && min.y < max.y && min.z < max.z
) {
return SpawningZoneInput(
cause = area.cause,
world = area.world,
baseX = min.x,
baseY = min.y,
baseZ = min.z,
length = max.x - min.x,
height = max.y - min.y,
width = max.z - min.z
)
}
}
return null
}
}
@@ -1,34 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePositionResolver
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
/**
* A basic spawner implementation that uses Cobblemon's default algorithms for zone generation,
* position resolving, and spawn selection.
*
* @author Hiroku
* @since October 27th, 2025
*/
open class BasicSpawner(
override val name: String,
override var spawnPool: SpawnPool,
override var maxPokemonPerChunk: Float = Cobblemon.config.pokemonPerChunk
) : Spawner {
override var selector: SpawningSelector<*> = SpawningSelector.DEFAULT
override var generator: SpawningZoneGenerator = Cobblemon.spawningZoneGenerator
override var resolver: AreaSpawnablePositionResolver = Cobblemon.areaSpawnablePositionResolver
override val influences = mutableListOf<SpawningInfluence>()
}
@@ -8,37 +8,33 @@
package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.SpawnerManager
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import net.minecraft.server.level.ServerLevel
import net.minecraft.core.BlockPos
/**
* A spawner that works within a fixed area. Instances provide the center point and the radii and then the spawner
* can be told to [run] and it will spawn Pokémon within that area.
* A spawner that works within a fixed area, and ticks on its own. The location
* and size of the area is necessary for construction, but after that this spawner
* can be registered in a [SpawnerManager] and left to its own devices.
*
* @author Hiroku
* @since February 5th, 2022
*/
open class FixedAreaSpawner(
name: String,
spawnPool: SpawnPool,
spawns: SpawnPool,
manager: SpawnerManager,
val world: ServerLevel,
val position: BlockPos,
val horizontalRadius: Int,
val verticalRadius: Int,
maxPokemonPerChunk: Float = Cobblemon.config.pokemonPerChunk
) : BasicSpawner(name, spawnPool, maxPokemonPerChunk) {
fun run(cause: SpawnCause, maxSpawns: Int? = null): List<Any> {
return runForArea(
zoneInput = getZoneInput(cause),
maxSpawns = maxSpawns
)
}
fun getZoneInput(cause: SpawnCause): SpawningZoneInput {
override var ticksBetweenSpawns: Float = 20F
) : AreaSpawner(name, spawns, manager) {
override fun getZoneInput(cause: SpawnCause): SpawningZoneInput? {
val basePos = position.offset(-horizontalRadius, -verticalRadius, -horizontalRadius)
return SpawningZoneInput(
cause = cause,
world = world,
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.Cobblemon.config
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.SpawnerManager
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.util.getPlayer
import com.cobblemon.mod.common.util.nextBetween
@@ -30,16 +31,11 @@ import kotlin.random.Random
* @author Hiroku
* @since February 14th, 2022
*/
open class PlayerSpawner(
player: ServerPlayer,
spawnPool: SpawnPool
) : BasicSpawner(
name = player.name.string,
spawnPool = spawnPool
) {
class PlayerSpawner(player: ServerPlayer, spawns: SpawnPool, manager: SpawnerManager) : AreaSpawner(player.name.string, spawns, manager) {
val uuid: UUID = player.uuid
fun getZoneInput(cause: SpawnCause): SpawningZoneInput? {
override var ticksBetweenSpawns = config.ticksBetweenSpawnAttempts
override fun getCauseEntity() = uuid.getPlayer()
override fun getZoneInput(cause: SpawnCause): SpawningZoneInput? {
val player = uuid.getPlayer() ?: return null
val zoneDiameter = config.spawningZoneDiameter
val zoneHeight = config.spawningZoneHeight
@@ -57,7 +53,6 @@ open class PlayerSpawner(
} else {
thetatemp
}
val x = center.x + r * cos(theta)
val z = center.z + r * sin(theta)
@@ -72,22 +67,4 @@ open class PlayerSpawner(
width = zoneDiameter
)
}
var active = true
var ticksUntilNextSpawn = 100F
var ticksBetweenSpawns: Float = config.ticksBetweenSpawnAttempts
var tickTimerMultiplier = 1F
fun tick() {
if (!active) {
return
}
ticksUntilNextSpawn -= tickTimerMultiplier
if (ticksUntilNextSpawn <= 0) {
val zoneInput = getZoneInput(cause = SpawnCause(spawner = this, entity = uuid.getPlayer()))
zoneInput?.let(::runForArea)
ticksUntilNextSpawn = ticksBetweenSpawns
}
}
}
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnRules
import com.cobblemon.mod.common.api.spawning.SpawnerManager
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence.Companion.TYPICAL_VARIATION
@@ -27,7 +28,7 @@ import net.minecraft.server.level.ServerPlayer
* @since February 14th, 2022
*/
object PlayerSpawnerFactory {
var spawnPool: SpawnPool by mutableLazy {
var spawns: SpawnPool by mutableLazy {
CobblemonSpawnPools.WORLD_SPAWN_POOL
}
@@ -45,9 +46,9 @@ object PlayerSpawnerFactory {
)
})
fun create(player: ServerPlayer): PlayerSpawner {
fun create(spawnerManager: SpawnerManager, player: ServerPlayer): PlayerSpawner {
val influences = influenceBuilders.mapNotNull { it(player) }
return PlayerSpawner(player, spawnPool).also {
return PlayerSpawner(player, spawns, spawnerManager).also {
it.influences.addAll(influences)
it.influences.addAll(
CobblemonSpawnRules.rules.values.filter(SpawnRule::enabled).flatMap(SpawnRule::components)
@@ -14,67 +14,33 @@ import com.cobblemon.mod.common.api.events.entity.SpawnBucketChosenEvent
import com.cobblemon.mod.common.api.spawning.BestSpawner
import com.cobblemon.mod.common.api.spawning.SpawnBucket
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
import com.cobblemon.mod.common.api.spawning.detail.EntitySpawnResult
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePositionResolver
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.position.calculators.SpawnablePositionCalculator.Companion.prioritizedAreaCalculators
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.isBoxLoaded
import com.cobblemon.mod.common.util.squeezeWithinBounds
import com.cobblemon.mod.common.util.toVec3f
import com.cobblemon.mod.common.util.weightedSelection
import kotlin.Any
import kotlin.Boolean
import kotlin.Float
import kotlin.Int
import kotlin.Pair
import kotlin.String
import kotlin.collections.plus
import kotlin.math.max
import net.minecraft.core.BlockPos
import net.minecraft.core.SectionPos
import net.minecraft.world.level.ChunkPos
import net.minecraft.world.level.Level
import net.minecraft.world.level.chunk.ChunkAccess
import net.minecraft.world.level.chunk.status.ChunkStatus
import net.minecraft.world.level.pathfinder.PathComputationType
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
/**
* Interface representing something that performs the action of spawning. Various functions
* exist to streamline the process of using the [BestSpawner].
*
* The simplest implementation is a [BasicSpawner]
*
* @author Hiroku
* @since January 24th, 2022
*/
interface Spawner {
val name: String
val influences: MutableList<SpawningInfluence>
var spawnPool: SpawnPool
var selector: SpawningSelector<*>
var generator: SpawningZoneGenerator
var resolver: AreaSpawnablePositionResolver
var maxPokemonPerChunk: Float
fun getSpawningSelector(): SpawningSelector<*>
fun setSpawningSelector(selector: SpawningSelector<*>)
fun getSpawnPool(): SpawnPool
fun setSpawnPool(spawnPool: SpawnPool)
fun <R> afterSpawn(action: SpawnAction<R>, result: R) {}
companion object {
/** The chunk range radius over which spawning will roughly keep the entity density under control. */
const val ENTITY_LIMIT_CHUNK_RANGE = 3
}
fun getMatchingSpawns(bucket: SpawnBucket, spawnablePosition: SpawnablePosition): List<SpawnDetail> {
val spawns = mutableListOf<SpawnDetail>()
spawns.addAll(spawnPool.retrieve(bucket, spawnablePosition).filter { it.isSatisfiedBy(spawnablePosition) })
spawns.addAll(getSpawnPool().retrieve(bucket, spawnablePosition).filter { it.isSatisfiedBy(spawnablePosition) })
spawnablePosition.influences.forEach { influence ->
val influencedSpawns = influence.injectSpawns(bucket, spawnablePosition)
if (influencedSpawns != null) {
@@ -84,101 +50,6 @@ interface Spawner {
return spawns
}
/**
* Runs the spawner for a specific [SpawnablePosition] and calculates a [SpawnAction] that could spawn there.
* This does not trigger the resulting spawn. You can trigger the spawn in the same action by running
* [runForPosition] instead or manually complete the [SpawnAction] it returns using [SpawnAction.complete].
*/
fun calculateSpawnActionForPosition(
cause: SpawnCause,
spawnablePosition: SpawnablePosition,
): SpawnAction<*>? {
influences.removeIf { it.isExpired() }
spawnablePosition.influences.addAll(influences)
val bucket = chooseBucket(cause, spawnablePosition.influences)
return selector.select(
spawner = this,
bucket = bucket,
spawnablePositions = listOf(spawnablePosition),
maxSpawns = 1
).firstOrNull()
}
fun calculateSpawnActionsForArea(
zoneInput: SpawningZoneInput,
maxSpawns: Int?
): List<SpawnAction<*>> {
val maxSpawns = maxSpawns ?: Cobblemon.config.maximumSpawnsPerPass
influences.removeIf { it.isExpired() }
val constrainedArea = constrainArea(zoneInput)
?: return emptyList()
val areaBox = AABB.ofSize(
Vec3(constrainedArea.getCenter().toVec3f()),
ENTITY_LIMIT_CHUNK_RANGE * 16.0 * 2,
1000.0,
ENTITY_LIMIT_CHUNK_RANGE * 16.0 * 2
)
if (!constrainedArea.world.isBoxLoaded(areaBox)) {
return emptyList()
}
val numberNearby = constrainedArea.world.getEntitiesOfClass(
PokemonEntity::class.java,
areaBox,
PokemonEntity::countsTowardsSpawnCap
).size
val chunksCovered = ENTITY_LIMIT_CHUNK_RANGE * ENTITY_LIMIT_CHUNK_RANGE
val maxPokemonPerChunk = max(Cobblemon.config.pokemonPerChunk, zoneInput.cause.spawner.maxPokemonPerChunk)
if (numberNearby.toFloat() / chunksCovered >= maxPokemonPerChunk) {
return emptyList()
}
val zone = generator.generate(this, constrainedArea)
val spawnablePositions = resolver.resolve(this, prioritizedAreaCalculators, zone)
val influences = influences + zone.unconditionalInfluences
val bucket = chooseBucket(zoneInput.cause, influences)
return selector.select(
spawner = this,
bucket = bucket,
spawnablePositions = spawnablePositions,
maxSpawns = maxSpawns
)
}
/**
* Runs the spawner at the given [SpawnablePosition] and returns whatever the calculated [SpawnAction] produces,
* if any spawn occurred. For most implementations of [SpawnAction], the resulting type is
* [EntitySpawnResult]. Technically you can make spawns for anything though.
*/
fun runForPosition(
cause: SpawnCause,
spawnablePosition: SpawnablePosition
): Any? {
val action = calculateSpawnActionForPosition(
cause = cause,
spawnablePosition = spawnablePosition
) ?: return null
return action.complete()
}
fun runForArea(
zoneInput: SpawningZoneInput,
maxSpawns: Int? = null
): List<Any> {
val spawnActions = calculateSpawnActionsForArea(zoneInput = zoneInput, maxSpawns = maxSpawns)
val results = mutableListOf<Any>()
for (spawnAction in spawnActions) {
spawnAction.complete()?.let(results::add)
}
return results
}
fun copyInfluences() = influences.filter { !it.isExpired() }.toMutableList()
fun chooseBucket(cause: SpawnCause, influences: List<SpawningInfluence>): SpawnBucket {
@@ -195,77 +66,4 @@ interface Spawner {
CobblemonEvents.SPAWN_BUCKET_CHOSEN.post(event)
return event.bucket
}
fun isValidStartPoint(world: Level, chunk: ChunkAccess, startPos: BlockPos.MutableBlockPos): Boolean {
val y = startPos.y
if (!world.isLoaded(startPos) || !world.isLoaded(startPos.setY(y + 1))) {
return false
}
val mid = chunk.getBlockState(startPos.setY(y))
val above = chunk.getBlockState(startPos.setY(y + 1))
// Above must be non-solid
if (!above.isPathfindable(PathComputationType.AIR)) {
return false
}
// Position must be non-air
if (mid.isAir) {
return false
}
return true
}
fun constrainArea(area: SpawningZoneInput): SpawningZoneInput? {
val basePos = BlockPos.MutableBlockPos(area.baseX, area.baseY, area.baseZ)
val originalY = area.baseY
val (chunkX, chunkZ) = Pair(SectionPos.blockToSectionCoord(area.baseX), SectionPos.blockToSectionCoord(area.baseZ))
// if the chunk isn't loaded, we don't want to go further & we don't want the getChunk function below to load/create the chunk.
if (!area.world.areEntitiesLoaded(ChunkPos.asLong(chunkX, chunkZ))) return null
val chunk = area.world.getChunk(chunkX, chunkZ, ChunkStatus.FULL) ?: return null
var valid = isValidStartPoint(area.world, chunk, basePos)
if (!valid) {
var offset = 1
do {
if (isValidStartPoint(area.world, chunk, basePos.setY(originalY + offset))) {
valid = true
basePos.y = originalY + offset
break
} else if (isValidStartPoint(area.world, chunk, basePos.setY(originalY - offset))) {
valid = true
basePos.y = originalY - offset
break
}
offset++
} while (offset <= Cobblemon.config.maxVerticalCorrectionBlocks)
}
if (valid) {
val min = area.world.squeezeWithinBounds(basePos)
val max = area.world.squeezeWithinBounds(basePos.move(area.length, area.height, area.width))
if (area.world.isLoaded(min) && area.world.isLoaded(max) &&
min.x < max.x && min.y < max.y && min.z < max.z
) {
return SpawningZoneInput(
cause = area.cause,
world = area.world,
baseX = min.x,
baseY = min.y,
baseZ = min.z,
length = max.x - min.x,
height = max.y - min.y,
width = max.z - min.z
)
}
}
return null
}
}
@@ -0,0 +1,89 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.SpawnerManager
import com.cobblemon.mod.common.api.spawning.detail.EntitySpawnResult
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.selection.FlatSpawnablePositionWeightedSelector
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
import net.minecraft.world.entity.Entity
/**
* A spawner that regularly attempts spawning entities. It has timing utilities,
* and subclasses must provide the logic for generating a spawn which is called
* periodically by the server.
*
* @author Hiroku
* @since February 5th, 2022
*/
abstract class TickingSpawner(
override val name: String,
var spawns: SpawnPool,
val manager: SpawnerManager
) : Spawner {
private var selector: SpawningSelector<*> = FlatSpawnablePositionWeightedSelector()
override val influences = mutableListOf<SpawningInfluence>()
override fun getSpawningSelector() = selector
override fun setSpawningSelector(selector: SpawningSelector<*>) { this.selector = selector }
override fun getSpawnPool() = spawns
override fun setSpawnPool(spawnPool: SpawnPool) { spawns = spawnPool }
abstract fun run(cause: SpawnCause): List<SpawnAction<*>>
var active = true
val spawnedEntities = mutableListOf<Entity>()
var lastSpawnTime = 0L
var ticksUntilNextSpawn = 100F
abstract var ticksBetweenSpawns: Float
var tickTimerMultiplier = 1F
var removalCheckTicks = 0
open fun tick() {
removalCheckTicks++
influences.removeIf { it.isExpired() }
if (removalCheckTicks == 60) {
spawnedEntities.removeIf { it.isRemoved }
removalCheckTicks = 0
}
if (!active) {
return
}
ticksUntilNextSpawn -= tickTimerMultiplier
if (ticksUntilNextSpawn <= 0) {
val spawnActions = run(cause = SpawnCause(spawner = this, entity = getCauseEntity()))
ticksUntilNextSpawn = ticksBetweenSpawns
for (spawnAction in spawnActions) {
spawnAction.complete()
}
}
}
override fun <R> afterSpawn(action: SpawnAction<R>, result: R) {
super.afterSpawn(action, result)
if (result is EntitySpawnResult) {
spawnedEntities.addAll(result.entities)
}
lastSpawnTime = System.currentTimeMillis()
}
open fun getCauseEntity(): Entity? = null
fun getAllInfluences() = this.influences + manager.influences
override fun copyInfluences() = this.getAllInfluences().toMutableList()
}
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.selection.FlatSpawnablePositionWeightedSelector
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerLevel
/**
* A type of spawner that occurs at a single point and provides its own means of generating a
* spawnable position from a particular cause and position. The trigger spawner must be directly called to
* spawn things (almost as if it was being triggered!!!) as opposed to being scheduled on a ticker.
*
* @author Hiroku
* @since February 3rd, 2024
*/
abstract class TriggerSpawner<T : SpawnCause>(override val name: String, var spawns: SpawnPool) : Spawner {
private var selector: SpawningSelector<*> = FlatSpawnablePositionWeightedSelector()
override val influences = mutableListOf<SpawningInfluence>()
open fun getSpawnAction(
cause: T,
world: ServerLevel,
pos: BlockPos,
influences: List<SpawningInfluence> = emptyList()
): SpawnAction<*>? {
val spawnablePosition = parseSpawnablePosition(cause, world, pos) ?: return null
spawnablePosition.influences.addAll(influences)
val bucket = chooseBucket(cause, spawnablePosition.influences)
return selector.select(this, bucket, listOf(spawnablePosition), max = 1).firstOrNull()
}
/** Parses a spawnable position, if possible, from the given cause and position. */
abstract fun parseSpawnablePosition(cause: T, world: ServerLevel, pos: BlockPos): SpawnablePosition?
override fun getSpawningSelector() = selector
override fun setSpawningSelector(selector: SpawningSelector<*>) { this.selector = selector }
override fun getSpawnPool() = spawns
override fun setSpawnPool(spawnPool: SpawnPool) { spawns = spawnPool }
}
@@ -137,9 +137,6 @@ object CobblemonItemTags {
@JvmField val WEARABLE_HAT_ITEMS = create("held/visibility/hat")
@JvmField val HIDDEN_ITEMS = create("held/visibility/hidden")
//Empty Tag
@JvmField val EMPTY = create("empty")
private fun create(path: String) = TagKey.create(Registries.ITEM, cobblemonResource(path))
@@ -20,8 +20,6 @@ import com.cobblemon.mod.common.api.molang.MoLangFunctions.addStandardFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMostSpecificMoLangValue
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import com.cobblemon.mod.common.api.moves.animations.ActionEffectTimeline
import com.cobblemon.mod.common.api.moves.animations.ActionEffects
import com.cobblemon.mod.common.api.moves.animations.TargetsProvider
import com.cobblemon.mod.common.api.moves.animations.UsersProvider
import com.cobblemon.mod.common.battles.ShowdownInterpreter
@@ -33,7 +31,6 @@ import com.cobblemon.mod.common.battles.dispatch.UntilDispatch
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.pokemon.evolution.progress.UseMoveEvolutionProgress
import com.cobblemon.mod.common.util.asArrayValue
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.cobblemonResource
import java.util.concurrent.CompletableFuture
@@ -52,7 +49,7 @@ class MoveInstruction(
) : InterpreterInstruction, CauserInstruction {
val effect = message.effectAt(1) ?: Effect.pure("", "")
val move = Moves.getByNameOrDummy(effect.id)
val actionEffect: ActionEffectTimeline? by lazy { ActionEffects.getEffectWithBattleContext(cobblemonResource(move.name), userPokemon) }
val actionEffect = move.actionEffect
val spreadTargets = message.optionalArgument("spread")?.split(",") ?: emptyList()
var future = CompletableFuture.completedFuture(Unit)
@@ -64,8 +61,6 @@ class MoveInstruction(
override fun invoke(battle: PokemonBattle) {
userPokemon = message.battlePokemon(0, battle)!!
targetPokemon = message.battlePokemon(2, battle)
val actionEffect = actionEffect ?: ActionEffects.actionEffects["generic_move".asIdentifierDefaultingNamespace()]
val targetPokemon = targetPokemon // So smart non-null casts can happen
val optionalEffect = message.effect()
@@ -18,7 +18,6 @@ import net.minecraft.core.BlockPos
import net.minecraft.core.particles.BlockParticleOption
import net.minecraft.core.particles.ColorParticleOption
import net.minecraft.core.particles.ParticleTypes
import net.minecraft.server.level.ServerLevel
import net.minecraft.sounds.SoundEvents
import net.minecraft.sounds.SoundSource
import net.minecraft.stats.Stats
@@ -52,13 +51,12 @@ import net.minecraft.world.phys.Vec3
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraft.world.phys.shapes.VoxelShape
import kotlin.math.floor
import kotlin.random.Random
class PokeSnackBlock(settings: Properties, val isLure: Boolean): BaseEntityBlock(settings) {
companion object {
const val MAX_BITES = 8
const val CAKE_HEIGHT = 0.4375
val MAX_BITES = 8
val CAKE_HEIGHT = 0.4375
val CANDLE_PARTICLE_POSITION = Vec3(0.5, CAKE_HEIGHT + 0.5, 0.5)
@@ -137,18 +135,16 @@ class PokeSnackBlock(settings: Properties, val isLure: Boolean): BaseEntityBlock
if (hasCandle(state)) {
return Shapes.or(SHAPES[state.getValue(BITES)], CANDLE_SHAPE)
}
return SHAPES[state.getValue(BITES)]
}
override fun getRenderShape(blockState: BlockState) = RenderShape.MODEL
override fun getRenderShape(blockState: BlockState?) = RenderShape.MODEL
override fun setPlacedBy(level: Level, pos: BlockPos, state: BlockState, placer: LivingEntity?, stack: ItemStack) {
super.setPlacedBy(level, pos, state, placer, stack)
val blockEntity = level.getBlockEntity(pos) as? PokeSnackBlockEntity ?: return
blockEntity.initializeFromItemStack(stack)
blockEntity.placedBy = placer?.uuid
val blockEntity = level.getBlockEntity(pos) as? PokeSnackBlockEntity
blockEntity?.initializeFromItemStack(stack)
}
override fun getCloneItemStack(level: LevelReader, pos: BlockPos, state: BlockState): ItemStack {
@@ -298,28 +294,15 @@ class PokeSnackBlock(settings: Properties, val isLure: Boolean): BaseEntityBlock
fun eat(level: Level, pos: BlockPos, state: BlockState, player: Player?) {
val bites = state.getValue(BITES) as Int
val newBites =
if (isLure) {
val pokeSnackBlockEntity = level.getBlockEntity(pos) as PokeSnackBlockEntity? ?: return
pokeSnackBlockEntity.amountSpawned += 1
floor(pokeSnackBlockEntity.amountSpawned / PokeSnackBlockEntity.SPAWNS_PER_BITE.toDouble()).toInt()
} else {
bites + 1
}
if (newBites > MAX_BITES) {
if (bites < MAX_BITES) {
level.setBlock(pos, state.setValue(BITES, bites + 1) as BlockState, UPDATE_ALL)
} else {
dropCandle(level, pos, state, player)
level.removeBlock(pos, false)
level.removeBlockEntity(pos)
} else {
level.setBlock(pos, state.setValue(BITES, newBites) as BlockState, UPDATE_ALL)
}
level.playSound(null, player?.blockPosition() ?: pos, SoundEvents.GENERIC_EAT, if (player != null) SoundSource.PLAYERS else SoundSource.NEUTRAL)
spawnEatParticles(level, pos)
player?.let {
level.gameEvent(it, GameEvent.BLOCK_DESTROY, pos)
}
@@ -369,19 +352,8 @@ class PokeSnackBlock(settings: Properties, val isLure: Boolean): BaseEntityBlock
0.0,
0.0,
0.0
)
}
}
override fun isRandomlyTicking(state: BlockState) = isLure
override fun randomTick(
state: BlockState,
level: ServerLevel,
pos: BlockPos,
random: RandomSource
) {
if (level.isClientSide) return
(level.getBlockEntity(pos) as? PokeSnackBlockEntity)?.randomTick()
}
}
@@ -52,8 +52,8 @@ class CookingPotMenu : RecipeBookMenu<CraftingInput, CookingPotRecipeBase>, Cont
constructor(containerId: Int, playerInventory: Inventory) : super(CobblemonMenuType.COOKING_POT, containerId) {
this.playerInventory = playerInventory
this.container = CookingPotContainer(this,
CampfireBlockEntity.CRAFTING_GRID_WIDTH,
CampfireBlockEntity.CRAFTING_GRID_WIDTH
CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH,
CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
)
this.resultContainer = ResultContainer()
this.containerData = SimpleContainerData(4)
@@ -82,25 +82,25 @@ class CookingPotMenu : RecipeBookMenu<CraftingInput, CookingPotRecipeBase>, Cont
val resultSlotX = 128
val resultSlotY = 55
addSlot(CookingPotResultSlot(this.container, CampfireBlockEntity.RESULT_SLOT, resultSlotX, resultSlotY))
addSlot(CookingPotResultSlot(this.container, CampfireBlockEntity.Companion.RESULT_SLOT, resultSlotX, resultSlotY))
for ((index, slotIndex) in CampfireBlockEntity.CRAFTING_GRID_SLOTS.withIndex()) {
val i = index / CampfireBlockEntity.CRAFTING_GRID_WIDTH
val j = index % CampfireBlockEntity.CRAFTING_GRID_WIDTH
for ((index, slotIndex) in CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.withIndex()) {
val i = index / CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
val j = index % CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
addSlot(Slot(this.container, slotIndex, 33 + j * 18, 18 + i * 18))
}
for ((index, slotIndex) in CampfireBlockEntity.SEASONING_SLOTS.withIndex()) {
addSlot(SeasoningSlot(this, this.container, slotIndex, 110 + index * 18, 18))
for ((index, slotIndex) in CampfireBlockEntity.Companion.SEASONING_SLOTS.withIndex()) {
addSlot(SeasoningSlot(this.container, slotIndex, 110 + index * 18, 18))
}
for ((index, _) in CampfireBlockEntity.PLAYER_INVENTORY_SLOTS.withIndex()) {
val i = index / CampfireBlockEntity.PLAYER_INVENTORY_WIDTH
val j = index % CampfireBlockEntity.PLAYER_INVENTORY_WIDTH
for ((index, _) in CampfireBlockEntity.Companion.PLAYER_INVENTORY_SLOTS.withIndex()) {
val i = index / CampfireBlockEntity.Companion.PLAYER_INVENTORY_WIDTH
val j = index % CampfireBlockEntity.Companion.PLAYER_INVENTORY_WIDTH
addSlot(Slot(playerInventory, index + 9, 8 + j * 18, 84 + i * 18))
}
for ((index, _) in CampfireBlockEntity.PLAYER_HOTBAR_SLOTS.withIndex()) {
for ((index, _) in CampfireBlockEntity.Companion.PLAYER_HOTBAR_SLOTS.withIndex()) {
addSlot(Slot(playerInventory, index, 8 + index * 18, 142))
}
}
@@ -173,31 +173,29 @@ class CookingPotMenu : RecipeBookMenu<CraftingInput, CookingPotRecipeBase>, Cont
currentActiveRecipe = recipe
if (recipe != null) {
previewItem = recipe.value.assemble(craftInput, level.registryAccess())
// Apply seasoning to the preview item
recipe.value.applySeasoning(
previewItem,
container.items.subList(CampfireBlockEntity.SEASONING_SLOTS.first,
CampfireBlockEntity.SEASONING_SLOTS.last + 1)
.filterNotNull()
.filter { !it.isEmpty && it.`is`(recipe.value.seasoningTag) })
.filter { !it.isEmpty })
} else previewItem = ItemStack.EMPTY
}
override fun getResultSlotIndex(): Int {
return CampfireBlockEntity.RESULT_SLOT
return CampfireBlockEntity.Companion.RESULT_SLOT
}
override fun getGridWidth(): Int {
return CampfireBlockEntity.CRAFTING_GRID_WIDTH
return CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
}
override fun getGridHeight(): Int {
return CampfireBlockEntity.CRAFTING_GRID_WIDTH
return CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
}
override fun getSize(): Int {
return CampfireBlockEntity.ITEMS_SIZE
return CampfireBlockEntity.Companion.ITEMS_SIZE
}
fun getBurnProgress(): Float {
@@ -229,24 +227,24 @@ class CookingPotMenu : RecipeBookMenu<CraftingInput, CookingPotRecipeBase>, Cont
val slotItemStack = slot.item
itemStack = slotItemStack.copy()
if (index == CampfireBlockEntity.RESULT_SLOT) {
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.PLAYER_INVENTORY_SLOTS.first, CampfireBlockEntity.PLAYER_HOTBAR_SLOTS.last + 1, false)) {
if (index == CampfireBlockEntity.Companion.RESULT_SLOT) {
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.PLAYER_INVENTORY_SLOTS.first, CampfireBlockEntity.Companion.PLAYER_HOTBAR_SLOTS.last + 1, false)) {
return ItemStack.EMPTY
}
slot.onQuickCraft(slotItemStack, itemStack);
} else if (index in CampfireBlockEntity.PLAYER_INVENTORY_SLOTS || index in CampfireBlockEntity.PLAYER_HOTBAR_SLOTS) {
} else if (index in CampfireBlockEntity.Companion.PLAYER_INVENTORY_SLOTS || index in CampfireBlockEntity.Companion.PLAYER_HOTBAR_SLOTS) {
if (Seasonings.isSeasoning(slotItemStack)) {
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.CRAFTING_GRID_SLOTS.first, CampfireBlockEntity.CRAFTING_GRID_SLOTS.last + 1, false) &&
!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.SEASONING_SLOTS.first, CampfireBlockEntity.SEASONING_SLOTS.last + 1, false)
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.first, CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.last + 1, false) &&
!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.SEASONING_SLOTS.first, CampfireBlockEntity.Companion.SEASONING_SLOTS.last + 1, false)
) {
return ItemStack.EMPTY
}
} else if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.CRAFTING_GRID_SLOTS.first, CampfireBlockEntity.CRAFTING_GRID_SLOTS.last + 1, false)) {
} else if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.first, CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.last + 1, false)) {
return ItemStack.EMPTY
}
} else if (index in CampfireBlockEntity.CRAFTING_GRID_SLOTS || index in CampfireBlockEntity.SEASONING_SLOTS) {
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.PLAYER_INVENTORY_SLOTS.first, CampfireBlockEntity.PLAYER_HOTBAR_SLOTS.last + 1, false)) {
} else if (index in CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS || index in CampfireBlockEntity.Companion.SEASONING_SLOTS) {
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.PLAYER_INVENTORY_SLOTS.first, CampfireBlockEntity.Companion.PLAYER_HOTBAR_SLOTS.last + 1, false)) {
return ItemStack.EMPTY
}
}
@@ -8,13 +8,13 @@
package com.cobblemon.mod.common.block.campfirepot
import com.cobblemon.mod.common.api.cooking.Seasonings
import net.minecraft.world.Container
import net.minecraft.world.inventory.Slot
import net.minecraft.world.item.ItemStack
class SeasoningSlot(val menu: CookingPotMenu, container: Container, slot: Int, x: Int, y: Int) : Slot(container, slot, x, y) {
class SeasoningSlot(container: Container, slot: Int, x: Int, y: Int) : Slot(container, slot, x, y) {
override fun mayPlace(stack: ItemStack): Boolean {
val recipe = menu.currentActiveRecipe?.value() ?: return false
return stack.`is`(recipe.seasoningTag) && super.mayPlace(stack)
return Seasonings.isSeasoning(stack) && super.mayPlace(stack)
}
}
@@ -134,8 +134,6 @@ class CampfireBlockEntity(pos: BlockPos, state: BlockState) : BaseContainerBlock
campfireBlockEntity.particleCooldown = 20
}
campfireBlockEntity.time++
}
fun serverTick(level: Level, pos: BlockPos, state: BlockState, campfireBlockEntity: CampfireBlockEntity) {
@@ -168,7 +166,7 @@ class CampfireBlockEntity(pos: BlockPos, state: BlockState) : BaseContainerBlock
val cookedItem = recipe.assemble(craftingInput, level.registryAccess())
val resultSlotItem = campfireBlockEntity.getItem(0)
recipe.applySeasoning(cookedItem, campfireBlockEntity.getSeasonings().filter { it.`is`(recipe.seasoningTag) })
recipe.applySeasoning(cookedItem, campfireBlockEntity.getSeasonings())
if (!campfireBlockEntity.blockState.getValue(CampfireBlock.LID)) {
campfireBlockEntity.cookingProgress = 0
@@ -239,7 +237,6 @@ class CampfireBlockEntity(pos: BlockPos, state: BlockState) : BaseContainerBlock
private var particleCooldown: Int = 0
var brothColor: Int = BASE_BROTH_COLOR
var bubbleColor: Int = BASE_BROTH_BUBBLE_COLOR
var time: Int = 0
var dataAccess: ContainerData = object : ContainerData {
override fun get(index: Int): Int {
@@ -314,7 +311,7 @@ class CampfireBlockEntity(pos: BlockPos, state: BlockState) : BaseContainerBlock
consumeItem(i)
}
for (i in SEASONING_SLOTS.first..SEASONING_SLOTS.last) {
if (getItem(i).`is`(recipe.seasoningTag) && recipe.seasoningProcessors.any { it.consumesItem(getItem(i)) }) consumeItem(i)
if (recipe.seasoningProcessors.any { it.consumesItem(getItem(i)) }) consumeItem(i)
}
val direction = state.getValue(CampfireBlock.ITEM_DIRECTION)
@@ -8,121 +8,29 @@
package com.cobblemon.mod.common.block.entity
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonBlockEntities
import com.cobblemon.mod.common.CobblemonItemComponents
import com.cobblemon.mod.common.api.cooking.getColourMixFromFlavours
import com.cobblemon.mod.common.api.fishing.SpawnBait
import com.cobblemon.mod.common.api.fishing.SpawnBait.Effect
import com.cobblemon.mod.common.api.fishing.SpawnBaitEffects
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.detail.PokemonHerdSpawnDetail
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.api.spawning.influence.BucketMultiplyingInfluence
import com.cobblemon.mod.common.api.spawning.influence.BucketNormalizingInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawnBaitInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.spawner.FixedAreaSpawner
import com.cobblemon.mod.common.block.PokeSnackBlock
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.item.components.BaitEffectsComponent
import com.cobblemon.mod.common.item.components.FlavourComponent
import com.cobblemon.mod.common.item.components.IngredientComponent
import com.cobblemon.mod.common.net.messages.client.effect.PokeSnackBlockParticlesPacket
import com.cobblemon.mod.common.util.DataKeys
import java.util.UUID
import net.minecraft.core.BlockPos
import net.minecraft.core.HolderLookup
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.NbtOps
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.Entity
import net.minecraft.world.entity.player.Player
import net.minecraft.nbt.*
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.block.state.BlockState
import kotlin.collections.orEmpty
open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) :
TintBlockEntity(CobblemonBlockEntities.POKE_SNACK, pos, state),
SpawningInfluence {
open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) : TintBlockEntity(CobblemonBlockEntities.POKE_SNACK, pos, state) {
companion object {
const val SPAWNS_PER_BITE = 1
const val RADIUS = 8
const val RANDOM_TICKS_BETWEEN_SPAWNS = 4
const val POKE_SNACK_CRUMBED_ASPECT = "poke_snack_crumbed"
}
val spawner: FixedAreaSpawner by lazy {
FixedAreaSpawner(
name = "poke_snack_spawner_${level?.dimension()?.location()}}_$blockPos",
spawnPool = CobblemonSpawnPools.WORLD_SPAWN_POOL,
world = level as ServerLevel,
position = blockPos,
horizontalRadius = RADIUS,
verticalRadius = RADIUS,
maxPokemonPerChunk = Cobblemon.config.pokeSnackPokemonPerChunk
).also {
it.influences.add(this)
val baitEffects = getBaitEffects()
val stackedLureTier = baitEffects
.filter { it.type == SpawnBait.Effects.RARITY_BUCKET }
.sumOf { it.value }
.toInt()
if (stackedLureTier > 0) {
it.influences += BucketNormalizingInfluence(
tier = stackedLureTier,
gradient = 0.2F,
firstTier = 1.2F
)
}
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)
@@ -130,7 +38,9 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) :
if (isLure()) baitEffectsComponent = itemStack.get(CobblemonItemComponents.BAIT_EFFECTS)
flavourComponent?.let {
getColourMixFromFlavours(it.getDominantFlavours())?.let(::setTint)
getColourMixFromFlavours(it.getDominantFlavours())?.let {
setTint(it)
}
}
}
@@ -142,60 +52,14 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) :
return false
}
fun randomTick() {
randomTicksUntilNextSpawn--
if (randomTicksUntilNextSpawn <= 0) {
val nearestPlayer = level?.getNearestPlayer(
blockPos.x.toDouble(),
blockPos.y.toDouble(),
blockPos.z.toDouble(),
Cobblemon.config.maximumSpawningZoneDistanceFromPlayer.toDouble(),
false, // true here makes it so creative players aren't considered
)
// High simulation distances may cause the player to be null here, we don't want to spawn without a player nearby.
if (nearestPlayer != null) {
spawner.run(
cause = SpawnCause(spawner = spawner, entity = nearestPlayer),
maxSpawns = 1
)
}
randomTicksUntilNextSpawn = getRandomTicksBetweenSpawns()
}
}
fun getRandomTicksBetweenSpawns(): Float {
val biteTimeMultiplier = getBiteTimeMultiplier()
return (RANDOM_TICKS_BETWEEN_SPAWNS * biteTimeMultiplier).coerceAtLeast(1F)
}
fun getBiteTimeMultiplier(): Float {
val baitEffects = getBaitEffects()
val biteTimeEffects = baitEffects.filter { it.type == SpawnBait.Effects.BITE_TIME }
if (biteTimeEffects.isEmpty()) return 1F
val biteTimeEffect = biteTimeEffects.random()
if (Math.random() > biteTimeEffect.chance) {
return 1F
}
return 1F - biteTimeEffect.value.toFloat()
}
fun toItemStack(): ItemStack {
val stack = ItemStack(this.blockState.block)
if (isLure() && baitEffectsComponent != null) {
stack.set(CobblemonItemComponents.BAIT_EFFECTS, baitEffectsComponent)
}
if (flavourComponent != null) {
stack.set(CobblemonItemComponents.FLAVOUR, flavourComponent)
}
if (ingredientComponent != null) {
stack.set(CobblemonItemComponents.INGREDIENT, ingredientComponent)
}
return stack
}
@@ -203,18 +67,12 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) :
* Combine all the [SpawnBait.Effect] values from the [baitEffectsComponent] data.
*/
fun getBaitEffects(): List<Effect> {
return baitEffectsComponent
?.effects
?.mapNotNull(SpawnBaitEffects::getFromIdentifier)
?.flatMap { it.effects }
.orEmpty()
return baitEffectsComponent?.effects?.mapNotNull(SpawnBaitEffects::getFromIdentifier)?.flatMap { it.effects }.orEmpty()
}
override fun saveAdditional(tag: CompoundTag, registries: HolderLookup.Provider) {
super.saveAdditional(tag, registries)
tag.putInt(DataKeys.AMOUNT_SPAWNED, amountSpawned)
flavourComponent?.let { component ->
CobblemonItemComponents.FLAVOUR.codec()!!
.encodeStart(NbtOps.INSTANCE, component)
@@ -241,19 +99,11 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) :
tag.put(DataKeys.INGREDIENTS, encodedTag)
}
}
tag.putFloat(DataKeys.TICKS_UNTIL_NEXT_SPAWN, randomTicksUntilNextSpawn)
placedBy?.let {
tag.putUUID(DataKeys.PLACED_BY, it)
}
}
override fun loadAdditional(tag: CompoundTag, registries: HolderLookup.Provider) {
super.loadAdditional(tag, registries)
amountSpawned = tag.getInt(DataKeys.AMOUNT_SPAWNED)
if (tag.contains(DataKeys.FLAVOUR)) {
CobblemonItemComponents.FLAVOUR.codec()
?.parse(NbtOps.INSTANCE, tag.get(DataKeys.FLAVOUR))
@@ -280,13 +130,5 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) :
ingredientComponent = component
}
}
if (tag.contains(DataKeys.TICKS_UNTIL_NEXT_SPAWN)) {
randomTicksUntilNextSpawn = tag.getFloat(DataKeys.TICKS_UNTIL_NEXT_SPAWN)
}
if (tag.contains(DataKeys.PLACED_BY)) {
placedBy = tag.getUUID(DataKeys.PLACED_BY)
}
}
}
@@ -56,11 +56,8 @@ object MountedCameraTypeHandler {
data.updatePerspective(key, currentCameraType)
}
fun handleTransition(passenger: Entity, fromKey: ResourceLocation, toKey: ResourceLocation) {
fun handleTransition(fromKey: ResourceLocation, toKey: ResourceLocation) {
val mc = Minecraft.getInstance()
if (passenger == mc.player) {
return
}
val currentCameraType = mc.options.cameraType
val data = ClientPersistedData.ridingPerspectives
data.updatePerspective(fromKey, currentCameraType)
@@ -491,12 +491,6 @@ class PokemonClientDelegate : PosableState(), PokemonSideDelegate {
cryAnimation = animation
}
override fun applyRenderRotation(partialTick: Float) {
currentEntity.ifRidingAvailable { behaviour, settings, state ->
behaviour.applyRenderRotation(settings, state, currentEntity, partialTick)
}
}
fun getSeatLocator(passenger: Entity): String {
val seatIndex = this.getEntity().occupiedSeats.indexOf(passenger)
if (seatIndex == -1) throw IllegalArgumentException("Entity is not currently riding a seat")
@@ -11,25 +11,26 @@ package com.cobblemon.mod.common.client.gui
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonItems
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.gui.drawPosablePortrait
import com.cobblemon.mod.common.api.text.darkGray
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.battle.BattleGUI
import com.cobblemon.mod.common.client.gui.portrait.FullyAnimatedPortraitDrawer
import com.cobblemon.mod.common.client.gui.portrait.SelectedAnimatedPortraitDrawer
import com.cobblemon.mod.common.client.gui.portrait.InanimatePortraitDrawer
import com.cobblemon.mod.common.client.gui.portrait.PortraitStyle
import com.cobblemon.mod.common.client.gui.toast.CobblemonToast
import com.cobblemon.mod.common.client.keybind.boundKey
import com.cobblemon.mod.common.client.keybind.keybinds.HidePartyBinding
import com.cobblemon.mod.common.client.keybind.keybinds.SummaryBinding
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.getDepletableRedGreen
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.client.render.renderScaledGuiItemIcon
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import java.util.UUID
import net.minecraft.client.DeltaTracker
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.Gui
@@ -57,9 +58,11 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
private val genderIconMale = cobblemonResource("textures/gui/party/party_gender_male.png")
private val genderIconFemale = cobblemonResource("textures/gui/party/party_gender_female.png")
private val portraitBackground = cobblemonResource("textures/gui/party/party_slot_portrait_background.png")
}
private val stateAtIndex = hashMapOf<Int, Pair<UUID, FloatingState>>()
private val inanimatePortraitDrawer = InanimatePortraitDrawer()
private val selectedAnimatedPortraitDrawer = SelectedAnimatedPortraitDrawer()
private val fullyAnimatedPortraitDrawer = FullyAnimatedPortraitDrawer()
}
private val screenExemptions: List<Class<out Screen>> = listOf(
ChatScreen::class.java,
@@ -85,16 +88,6 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
attachedToast = false
}
fun getPokemonState(index: Int, pokemonUUID: UUID): FloatingState {
val state = this.stateAtIndex.getOrPut(index) { pokemonUUID to FloatingState() }
if (state.first != pokemonUUID) {
val newState = FloatingState()
this.stateAtIndex[index] = pokemonUUID to newState
return newState
}
return state.second
}
override fun render(context: GuiGraphics, tickCounter: DeltaTracker) {
val partialDeltaTicks = tickCounter.realtimeDeltaTicks
@@ -167,20 +160,13 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
0.0
)
val shouldAnimate = when (Cobblemon.config.partyPortraitAnimations) {
PokemonGUIAnimationStyle.NEVER_ANIMATE -> false
PokemonGUIAnimationStyle.ANIMATE_SELECTED -> selectedSlot == index
PokemonGUIAnimationStyle.ALWAYS_ANIMATE -> true
val portraitDrawer = when (Cobblemon.config.partyPortraitAnimations) {
PortraitStyle.NEVER_ANIMATE -> inanimatePortraitDrawer
PortraitStyle.ANIMATE_SELECTED -> selectedAnimatedPortraitDrawer
PortraitStyle.ALWAYS_ANIMATE -> fullyAnimatedPortraitDrawer
}
drawPosablePortrait(
identifier = pokemon.species.resourceIdentifier,
matrixStack = matrices,
doQuirks = false,
partialTicks = if (shouldAnimate) partialDeltaTicks else 0F,
contextScale = pokemon.form.baseScale,
state = getPokemonState(index = index, pokemonUUID = pokemon.uuid).also { it.currentAspects = pokemon.aspects }
)
portraitDrawer.draw(pokemon, matrices, partialDeltaTicks, selectedSlot == index, index)
matrices.popPose()
context.disableScissor()
@@ -8,7 +8,6 @@
package com.cobblemon.mod.common.client.gui.battle
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.gui.drawPosablePortrait
import com.cobblemon.mod.common.api.pokedex.PokedexEntryProgress
@@ -323,7 +322,7 @@ class BattleOverlay : Gui(Minecraft.getInstance()), Schedulable {
reversed = reversed,
doQuirks = false,
state = state,
partialTicks = if (Cobblemon.config.animateBattleTiles) partialTicks else 0F
partialTicks = partialTicks
)
}
matrixStack.popPose()
@@ -13,6 +13,7 @@ import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.battles.*
import com.cobblemon.mod.common.client.gui.battle.subscreen.BattleGimmickButton.GimmickTile
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.math.toRGB
import com.mojang.blaze3d.vertex.PoseStack
@@ -138,7 +139,8 @@ abstract class BattleGimmickButton(gimmick: ShowdownMoveset.Gimmick, val x: Floa
pp = gimmickTemplate?.pp ?: moveTemplate.pp,
priority = gimmickTemplate?.priority ?: moveTemplate.priority,
critRatio = gimmickTemplate?.critRatio ?: moveTemplate.critRatio,
effectChances = gimmickTemplate?.effectChances ?: moveTemplate.effectChances
effectChances = gimmickTemplate?.effectChances ?: moveTemplate.effectChances,
actionEffect = null
)
}
}
@@ -201,7 +201,7 @@ class CookingPotScreen(
}
override fun renderSlot(guiGraphics: GuiGraphics, slot: Slot) {
if (slot is SeasoningSlot && ((!menu.carried.isEmpty && !slot.mayPlace(menu.carried)) || (slot.hasItem() && !slot.mayPlace(slot.item))) ) {
if (slot is SeasoningSlot && !menu.carried.isEmpty && !slot.mayPlace(menu.carried)) {
val x = slot.x
val y = slot.y
guiGraphics.fill(x,y,x+16,y+16,822018048)
@@ -299,7 +299,7 @@ class DialogueScreen(var dialogueDTO: DialogueDTO) : Screen("gui.dialogue".asTra
fun playGibberSpeak(delta: Float, text: String, gibber: DialogueGibberDTO) {
timeElapsedSinceGibber += delta / 20
if (timeElapsedSinceGibber < gibber.interval || gibberDone) {
if (timeElapsedSinceGibber < gibber.interval) {
return
}
@@ -8,12 +8,10 @@
package com.cobblemon.mod.common.client.gui.pc
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
import com.cobblemon.mod.common.client.gui.pasture.PasturePCGUIConfiguration
import com.cobblemon.mod.common.client.gui.PokemonGUIAnimationStyle
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.client.render.renderScaledGuiItemIcon
@@ -74,19 +72,12 @@ open class StorageSlot(
matrices.translate(posX + (SIZE / 2.0), posY + 1.0, 0.0)
matrices.scale(2.5F, 2.5F, 1F)
val animationConfig = if (this is PartyStorageSlot) Cobblemon.config.summaryProfileAnimations else Cobblemon.config.pcProfileAnimations
val shouldAnimate = when (animationConfig) {
PokemonGUIAnimationStyle.ALWAYS_ANIMATE -> true
PokemonGUIAnimationStyle.NEVER_ANIMATE -> false
PokemonGUIAnimationStyle.ANIMATE_SELECTED -> isHoveredOrFocused
}
drawProfilePokemon(
renderablePokemon = pokemon.asRenderablePokemon(),
matrixStack = matrices,
rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, 35F, 0F)),
state = state,
partialTicks = if (shouldAnimate) partialTicks else 0F,
partialTicks = partialTicks,
scale = 4.5F
)
matrices.popPose()
@@ -0,0 +1,63 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.portrait
import com.cobblemon.mod.common.api.gui.drawPosablePortrait
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.pokemon.Pokemon
import com.mojang.blaze3d.vertex.PoseStack
import java.util.UUID
/**
* Base logic for a [PortraitDrawer] that animates a portrait based on conditions.
*/
abstract class AnimatedPortraitDrawer : PortraitDrawer {
private val state = FloatingState()
private val stateAtIndex = hashMapOf<Int, Pair<UUID, FloatingState>>()
override fun draw(pokemon: Pokemon, poseStack: PoseStack, partialTicks: Float, isSelected: Boolean, index: Int) {
drawPosablePortrait(
identifier = pokemon.species.resourceIdentifier,
matrixStack = poseStack,
partialTicks = this.ticksFor(pokemon, isSelected, partialTicks),
contextScale = pokemon.form.baseScale,
state = this.stateFor(pokemon, isSelected, index)
)
}
protected abstract fun shouldAnimate(pokemon: Pokemon, isSelected: Boolean): Boolean
protected open fun ticksFor(pokemon: Pokemon, isSelected: Boolean, partialTicks: Float): Float {
return if (this.shouldAnimate(pokemon, isSelected)) partialTicks else 0F
}
protected open fun stateFor(pokemon: Pokemon, isSelected: Boolean, index: Int): FloatingState {
if (!this.shouldAnimate(pokemon, isSelected)) {
return this.state
}
val state = this.stateAtIndex.getOrPut(index) { pokemon.uuid to this.createState(pokemon) }
if (state.first != pokemon.uuid) {
val newState = this.createState(pokemon)
this.stateAtIndex[index] = pokemon.uuid to newState
return newState
}
return state.second
}
protected open fun createState(pokemon: Pokemon): FloatingState {
val state = FloatingState()
state.currentAspects = pokemon.aspects
return state
}
}
@@ -0,0 +1,25 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.portrait
import com.cobblemon.mod.common.pokemon.Pokemon
import net.minecraft.client.GraphicsStatus
/**
* Draws portraits if the Pokemon is not fainted.
*
* Meant for client with [GraphicsStatus.FABULOUS].
*/
class FullyAnimatedPortraitDrawer : AnimatedPortraitDrawer() {
override fun shouldAnimate(pokemon: Pokemon, isSelected: Boolean): Boolean {
return !pokemon.isFainted()
}
}
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.portrait
import com.cobblemon.mod.common.api.gui.drawPosablePortrait
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.pokemon.Pokemon
import com.mojang.blaze3d.vertex.PoseStack
import net.minecraft.client.GraphicsStatus
/**
* The simplest implementation, the Pokemon is drawn staying still.
*
* Meant for client with [GraphicsStatus.FAST].
*/
class InanimatePortraitDrawer : PortraitDrawer {
private val state = 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,
partialTicks = 0F,
contextScale = pokemon.form.baseScale,
state = this.state
)
}
}
@@ -0,0 +1,38 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.portrait
import com.cobblemon.mod.common.pokemon.Pokemon
import com.mojang.blaze3d.vertex.PoseStack
/**
* Responsible for drawing a Pokemon portrait.
*
* Keep in mind any scaling and handling of the [PoseStack] must be done on the implementations using this system.
*/
fun interface PortraitDrawer {
/**
* Draws the portrait.
*
* @param pokemon The [Pokemon] being drawn.
* @param poseStack The [PoseStack] being used.
* @param partialTicks The partial ticks of the screen using this.
* @param isSelected If the [pokemon] is considered in a selected state for example hovered or the current party member.
* @param index The index of the [pokemon] in the screen using this.
*/
fun draw(
pokemon: Pokemon,
poseStack: PoseStack,
partialTicks: Float,
isSelected: Boolean,
index: Int
)
}
@@ -6,20 +6,24 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui
package com.cobblemon.mod.common.client.gui.portrait
import com.mojang.serialization.Codec
import net.minecraft.util.StringRepresentable
enum class PokemonGUIAnimationStyle : StringRepresentable {
enum class PortraitStyle : StringRepresentable {
NEVER_ANIMATE,
ANIMATE_SELECTED,
ALWAYS_ANIMATE;
override fun getSerializedName() = this.name
override fun getSerializedName(): String = this.name
companion object {
@JvmStatic
val CODEC: Codec<PokemonGUIAnimationStyle> = StringRepresentable.fromEnum(PokemonGUIAnimationStyle::values)
val CODEC: Codec<PortraitStyle> = StringRepresentable.fromEnum(PortraitStyle::values)
}
}
@@ -0,0 +1,25 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.portrait
import com.cobblemon.mod.common.pokemon.Pokemon
import net.minecraft.client.GraphicsStatus
/**
* Draws portraits if the Pokemon is not fainted and selected.
*
* Meant for client with [GraphicsStatus.FANCY].
*/
class SelectedAnimatedPortraitDrawer : AnimatedPortraitDrawer() {
override fun shouldAnimate(pokemon: Pokemon, isSelected: Boolean): Boolean {
return !pokemon.isFainted() && isSelected
}
}
@@ -8,11 +8,9 @@
package com.cobblemon.mod.common.client.gui.summary.widgets
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.PokemonGUIAnimationStyle
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
import com.cobblemon.mod.common.client.gui.summary.Summary
import com.cobblemon.mod.common.client.render.drawScaledText
@@ -156,20 +154,13 @@ class PartySlotWidget(
matrices.pushPose()
matrices.translate(x + (PORTRAIT_DIAMETER / 2.0), y - 3.0, 0.0)
matrices.scale(2.5F, 2.5F, 1F)
val shouldAnimate = when (Cobblemon.config.summaryProfileAnimations) {
PokemonGUIAnimationStyle.ALWAYS_ANIMATE -> true
PokemonGUIAnimationStyle.NEVER_ANIMATE -> false
PokemonGUIAnimationStyle.ANIMATE_SELECTED -> isHovered || isSelected
}
drawProfilePokemon(
species = slotPokemon.species.resourceIdentifier,
matrixStack = matrices,
rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, 35F, 0F)),
state = state,
scale = 4.5F,
partialTicks = if (shouldAnimate) delta else 0F
partialTicks = delta
)
matrices.popPose()
@@ -78,17 +78,14 @@ object PartySendBinding : CobblemonBlockingKeyBinding(
return
}
if (CobblemonClient.storage.selectedSlot == -1) return
if (Minecraft.getInstance().screen != null) return
val selectedPartyPokemon = if (CobblemonClient.storage.selectedSlot >= 0) {
CobblemonClient.storage.party.get(CobblemonClient.storage.selectedSlot)
} else {
null
}
val selectedPartyPokemon = CobblemonClient.storage.party.get(CobblemonClient.storage.selectedSlot) ?: return
if (isRidingPokemon(player) && canAttemptDismount(player, selectedPartyPokemon)) {
sendToServer(DismountPokemonPacket())
} else if (selectedPartyPokemon != null && !isRidingSelectedPokemon(player, selectedPartyPokemon)){
}
else if (!isRidingSelectedPokemon(player, selectedPartyPokemon)){
checkForTargetInteractions(player, selectedPartyPokemon)
}
}
@@ -108,7 +105,8 @@ object PartySendBinding : CobblemonBlockingKeyBinding(
collideBlock = ClipContext.Fluid.NONE)
if (canSendOutPokemon(player, targetEntity)) {
sendToServer(SendOutPokemonPacket(CobblemonClient.storage.selectedSlot))
} else {
}
else {
processEntityTarget(player, selectedPartyPokemon, targetEntity)
}
}
@@ -142,7 +140,7 @@ object PartySendBinding : CobblemonBlockingKeyBinding(
}
}
private fun canAttemptDismount(player: LocalPlayer, selectedPartyPokemon: Pokemon?): Boolean {
private fun canAttemptDismount(player: LocalPlayer, selectedPartyPokemon: Pokemon): Boolean {
if (player.vehicle !is PokemonEntity) return false
val vehicle = player.vehicle as PokemonEntity
if (player != vehicle.controllingPassenger) {
@@ -150,10 +148,11 @@ object PartySendBinding : CobblemonBlockingKeyBinding(
}
val isAirRide = vehicle.ridingController?.context?.style == RidingStyle.AIR
val hasLandRide = vehicle.rideProp.behaviours?.get(RidingStyle.LAND) != null
return if (isAirRide && hasLandRide) {
false
} else {
vehicle.pokemon.uuid == selectedPartyPokemon?.uuid
if (isAirRide && hasLandRide) {
return false
}
else {
return vehicle.pokemon.uuid == selectedPartyPokemon.uuid
}
}
@@ -15,7 +15,7 @@ import com.cobblemon.mod.common.net.messages.client.orientation.ClientboundUpdat
import net.minecraft.client.Minecraft
import net.minecraft.util.Mth
object ClientboundUpdateOrientationHandler : ClientNetworkPacketHandler<ClientboundUpdateOrientationPacket> {
object S2CUpdateOrientationHandler : ClientNetworkPacketHandler<ClientboundUpdateOrientationPacket> {
override fun handle(packet: ClientboundUpdateOrientationPacket, client: Minecraft) {
client.executeIfPossible {
val level = client.level ?: return@executeIfPossible
@@ -125,7 +125,7 @@ class CampfireBlockEntityRenderer(ctx: BlockEntityRendererProvider.Context) : Bl
) {
val seasonings = blockEntity.getSeasonings()
val gameTime = blockEntity.time + tickDelta
val gameTime = (blockEntity.level?.gameTime ?: 0) + tickDelta
val rotationAngle = (gameTime * ROTATION_SPEED) % 360
seasonings.forEachIndexed { index, seasoning ->
@@ -8,7 +8,6 @@
package com.cobblemon.mod.common.client.render.entity
import com.cobblemon.mod.common.OrientationControllable
import com.cobblemon.mod.common.api.riding.Rideable
import com.cobblemon.mod.common.client.MountedPokemonAnimationRenderController
import com.cobblemon.mod.common.client.entity.PokemonClientDelegate
@@ -31,20 +30,11 @@ object MountedLivingEntityRenderer {
partialTicks: Float,
i: Float
) {
val vehicle = entity.vehicle
if (entity.vehicle !is Rideable) return
val matrix = stack.last().pose()
val delegate = pokemon.delegate as PokemonClientDelegate
val locator = delegate.locatorStates[delegate.getSeatLocator(entity)]
if (vehicle is OrientationControllable) {
val vehicleController = vehicle.orientationController
if (vehicleController.isActive() && vehicleController.orientation != null) {
vehicleController.tick()
}
}
//Positions entity
if (locator != null) {
MountedPokemonAnimationRenderController.setup(pokemon, partialTicks)
@@ -187,7 +187,7 @@ class PokemonRenderer(
if (!DISABLE_ROLLING_DEBUG && controller.active) {
// Allow the ride controller to modify its rotations using partialTick
entity.delegate.applyRenderRotation(partialTicks)
entity.applyRenderRotation(partialTicks)
val matrix = poseMatrix.last().pose()
val yaw = Mth.rotLerp(partialTicks, entity.yBodyRotO, entity.yBodyRot)
@@ -8,11 +8,10 @@
package com.cobblemon.mod.common.command
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.Cobblemon.config
import com.cobblemon.mod.common.api.permission.CobblemonPermissions
import com.cobblemon.mod.common.api.spawning.CobblemonWorldSpawnerManager
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.position.calculators.SpawnablePositionCalculator.Companion.prioritizedAreaCalculators
import com.cobblemon.mod.common.api.spawning.spawner.SpawningZoneInput
import com.cobblemon.mod.common.api.text.add
import com.cobblemon.mod.common.api.text.green
@@ -25,7 +24,6 @@ import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.command.argument.SpawnBucketArgumentType
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.permission
import com.cobblemon.mod.common.util.spawner
import com.mojang.brigadier.Command
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.context.CommandContext
@@ -58,11 +56,11 @@ object CheckSpawnsCommand {
return 0
}
val spawner = player.spawner
val spawner = CobblemonWorldSpawnerManager.spawnersForPlayers[player.uuid] ?: return Command.SINGLE_SUCCESS
val bucket = SpawnBucketArgumentType.getSpawnBucket(context, name = "bucket")
val cause = SpawnCause(spawner, player)
val slice = Cobblemon.spawningZoneGenerator.generate(
val slice = spawner.spawningZoneGenerator.generate(
spawner = spawner,
input = SpawningZoneInput(
cause = cause,
@@ -76,9 +74,9 @@ object CheckSpawnsCommand {
)
)
val contexts = Cobblemon.areaSpawnablePositionResolver.resolve(spawner, prioritizedAreaCalculators, slice)
val contexts = spawner.spawnablePositionResolver.resolve(spawner, spawner.spawnablePositionCalculators, slice)
val spawnProbabilities = spawner.selector.getProbabilities(spawner, bucket, contexts)
val spawnProbabilities = spawner.getSpawningSelector().getProbabilities(spawner, bucket, contexts)
val spawnNames = mutableMapOf<String, MutableComponent>()
val namedProbabilities = mutableMapOf<MutableComponent, Float>()
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common.command
import com.cobblemon.mod.common.api.permission.CobblemonPermissions
import com.cobblemon.mod.common.api.spawning.CobblemonWorldSpawnerManager
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.detail.EntitySpawnResult
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePosition
@@ -18,7 +19,6 @@ import com.cobblemon.mod.common.util.alias
import com.cobblemon.mod.common.util.commandLang
import com.cobblemon.mod.common.util.effectiveName
import com.cobblemon.mod.common.util.permission
import com.cobblemon.mod.common.util.spawner
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.arguments.IntegerArgumentType
import com.mojang.brigadier.context.CommandContext
@@ -31,7 +31,7 @@ import net.minecraft.commands.Commands.literal
*
* `/spawnpokemonfrompool [amount]` or the alias `/forcespawn [amount]`
*
* This command can fail if the randomly selected spawn region has no possible [AreaSpawnablePosition]. For example if
* This command can fail if the randomly selection spawn region has no possible [AreaSpawnablePosition]. For example if
* you are flying in the air
*/
object SpawnPokemonFromPool {
@@ -54,26 +54,47 @@ object SpawnPokemonFromPool {
private fun execute(context: CommandContext<CommandSourceStack>, amount: Int): Int {
val player = context.source.playerOrException
val spawner = player.spawner
val spawner = CobblemonWorldSpawnerManager.spawnersForPlayers.getValue(player.uuid)
var spawnsTriggered = 0
repeat(times = amount) {
val spawnCause = SpawnCause(spawner = spawner, entity = player)
val results = spawner.runForArea(spawner.getZoneInput(spawnCause) ?: return@repeat, maxSpawns = 1)
if (results.isEmpty()) {
// This could instead directly use a [Spawner] method if refactored, as it is currently it has
// entity counting coupled to the generation of an entity to spawn. Might be a good future change?
for (i in 1..amount) {
val spawnCause = SpawnCause(spawner = spawner, entity = spawner.getCauseEntity())
val zoneInput = spawner.getZoneInput(spawnCause) ?: continue
val zone = spawner.spawningZoneGenerator.generate(spawner, zoneInput)
val contexts = spawner.spawnablePositionResolver.resolve(spawner, spawner.spawnablePositionCalculators, zone)
val influences = spawner.getAllInfluences() + zone.unconditionalInfluences
// This has a chance to fail, if you get a spawning zone that has no associated contexts.
// but as it was selected at random by the Spawning Zone Generator, it could just be a miss which
// means two attempts to spawn in the same location can have differing results (which is expected for
// randomness).
if (contexts.isEmpty()) {
player.sendSystemMessage(UNABLE_TO_SPAWN.red())
return@repeat
continue
}
results.forEach {
val bucket = spawner.chooseBucket(spawnCause, influences)
val spawnAction = spawner.getSpawningSelector().select(spawner, bucket, contexts, max = 1).firstOrNull() // one at a time
if (spawnAction == null) {
player.sendSystemMessage(UNABLE_TO_SPAWN.red())
continue
}
spawnAction.future.thenApply {
if (it is EntitySpawnResult) {
for (entity in it.entities) {
spawnsTriggered++
player.sendSystemMessage(commandLang("spawnpokemonfrompool", entity.effectiveName()).green())
player.sendSystemMessage(commandLang("spawnpokemonfrompool.success", entity.effectiveName()).green())
}
}
}
spawnAction.complete()
spawnsTriggered++
}
return spawnsTriggered
@@ -11,7 +11,7 @@ package com.cobblemon.mod.common.config
import com.cobblemon.mod.common.api.drop.ItemDropMethod
import com.cobblemon.mod.common.api.pokeball.catching.calculators.CaptureCalculator
import com.cobblemon.mod.common.api.pokemon.status.Statuses
import com.cobblemon.mod.common.client.gui.PokemonGUIAnimationStyle
import com.cobblemon.mod.common.client.gui.portrait.PortraitStyle
import com.cobblemon.mod.common.config.CobblemonConfigField.CobblemonConfigSide.CLIENT
import com.cobblemon.mod.common.config.CobblemonConfigField.CobblemonConfigSide.SERVER
import com.cobblemon.mod.common.config.constraint.IntConstraint
@@ -155,9 +155,6 @@ 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(),
@@ -276,16 +273,7 @@ class CobblemonConfig {
var summaryPokemonFollowCursor = true
@CobblemonConfigField(Category.Interface, lang = "party_portrait_animations", CLIENT)
var partyPortraitAnimations = PokemonGUIAnimationStyle.NEVER_ANIMATE
@CobblemonConfigField(Category.Interface, lang = "pc_profile_animations", CLIENT)
var pcProfileAnimations = PokemonGUIAnimationStyle.ANIMATE_SELECTED
@CobblemonConfigField(Category.Interface, lang = "summary_profile_animations", CLIENT)
var summaryProfileAnimations = PokemonGUIAnimationStyle.ANIMATE_SELECTED
@CobblemonConfigField(Category.Interface, lang = "animate_battle_tiles", CLIENT)
var animateBattleTiles = false
var partyPortraitAnimations = PortraitStyle.NEVER_ANIMATE
@CobblemonConfigField(Category.Riding, lang = "third_person_view_bobbing", CLIENT)
var thirdPersonViewBobbing = true
@@ -11,17 +11,16 @@ 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.util.Mth
import net.minecraft.world.entity.MoverType
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(
@@ -35,7 +34,7 @@ class FollowHerdLeaderTask : Behavior<PokemonEntity>(
var tooFar = 8F
var closeEnough = 4F
/** Chance per tick of following the herd leader. */
val chance = 1 / 60F
val chance = 1 / 20F
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
@@ -51,19 +50,16 @@ 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.position().add(getRandomOffset(entity, level)).toBlockPos(),
pos = leader.blockPosition(),
speedModifier = 0.4F,
completionRange = closeEnough.toInt()
)
@@ -73,11 +69,10 @@ 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.position().add(getRandomOffset(entity, level)).toBlockPos(),
pos = leader.blockPosition(),
speedModifier = 0.3F,
completionRange = closeEnough.toInt()
)
@@ -88,7 +83,7 @@ class FollowHerdLeaderTask : Behavior<PokemonEntity>(
entity.brain.setMemory(
MemoryModuleType.WALK_TARGET,
CobblemonWalkTarget(
pos = leader.position().add(getRandomOffset(entity, level)).toBlockPos(),
pos = leader.blockPosition(),
speedModifier = 0.4F,
completionRange = 1
)
@@ -110,15 +105,14 @@ class FollowHerdLeaderTask : Behavior<PokemonEntity>(
// Go to the leader's walk target, roughly.
val walkTarget = leader.brain.getMemory(MemoryModuleType.WALK_TARGET).orElse(null) ?: 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
if (entity.distanceToSqr(walkTarget.target.currentPosition()) + closeEnough < leader.distanceToSqr(walkTarget.target.currentPosition())) {
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
}
entity.brain.setMemory(
MemoryModuleType.WALK_TARGET,
CobblemonWalkTarget(
pos = walkTarget.target.currentPosition().add(getRandomOffset(entity, level)).toBlockPos(),
pos = walkTarget.target.currentBlockPosition(),
speedModifier = walkTarget.speedModifier,
nodeTypeFilter = (walkTarget as? CobblemonWalkTarget)?.nodeTypeFilter ?: { true },
completionRange = closeEnough.toInt()
@@ -126,23 +120,4 @@ 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
}
}
@@ -26,7 +26,6 @@ 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
@@ -373,11 +372,11 @@ class PokeRodFishingBobberEntity(type: EntityType<out PokeRodFishingBobberEntity
if (Mth.nextInt(random, 0, 100) < getPokemonSpawnChance(rodStack)) {
val player = playerOwner as? ServerPlayer ?: return
this.typeCaught = TypeCaught.POKEMON
val stackedLureTier = SpawnBaitEffects.getEffectsFromItemStack(bobberBait)
val lureBoost = SpawnBaitEffects.getEffectsFromItemStack(bobberBait)
.filter { it.type == SpawnBait.Effects.RARITY_BUCKET }
.sumOf { it.value }
.toInt()
planSpawn(player, rodStack, stackedLureTier)
.maxOfOrNull { it.value.toInt() }
?: 0
planSpawn(player, rodStack, lureBoost)
val selectedWeight = plannedSpawnAction?.bucket?.weight ?: 50F
val reactionMinMax = calculateMinMaxCountdown(selectedWeight)
@@ -772,16 +771,11 @@ class PokeRodFishingBobberEntity(type: EntityType<out PokeRodFishingBobberEntity
lureLevel = lureLevel,
)
val spawnablePosition = FishingSpawnablePosition(
val result = spawner.getSpawnAction(
cause = spawnCause,
world = level() as ServerLevel,
pos = position().toBlockPos(),
influences = mutableListOf(PlayerLevelRangeInfluence(player, TYPICAL_VARIATION), bucketInfluence)
)
val result = spawner.calculateSpawnActionForPosition(
cause = spawnCause,
spawnablePosition = spawnablePosition
influences = listOf(PlayerLevelRangeInfluence(player, TYPICAL_VARIATION), bucketInfluence)
)
this.plannedSpawnAction = result
@@ -817,11 +811,8 @@ class PokeRodFishingBobberEntity(type: EntityType<out PokeRodFishingBobberEntity
// create accessory splash particle when you fish something up
particleEntityHandler(this, ResourceLocation.fromNamespaceAndPath("cobblemon", "accessory_fish_splash"))
val baitId =
if (bobberBait.isEmpty)
"empty_bait".asIdentifierDefaultingNamespace()
else
bobberBait.itemHolder.unwrapKey().map { it.location() }.orElse("empty_bait".asIdentifierDefaultingNamespace())
var baitId = PokerodItem.getBaitStackOnRod(this.bobberBait).takeUnless { it.isEmpty }?.itemHolder?.unwrapKey()
?.orElse(null)?.location() ?: "empty_bait".asIdentifierDefaultingNamespace()
val pokemonId = spawnedPokemon.pokemon.species.resourceIdentifier
CobblemonCriteria.REEL_IN_POKEMON.trigger(player, ReelInPokemonContext(pokemonId, baitId))
@@ -859,7 +850,7 @@ class PokeRodFishingBobberEntity(type: EntityType<out PokeRodFishingBobberEntity
BobberSpawnPokemonEvent.Post(
this,
spawnAction,
bobberBait,
rodItemStack,
entity
)
)
@@ -121,7 +121,6 @@ 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
@@ -301,8 +300,6 @@ open class PokemonEntity(
get() = entityData.get(SHOWN_HELD_ITEM)
set(value) = entityData.set(SHOWN_HELD_ITEM, value)
var lastLightningBoltUUID: UUID? = null
var drops: DropTable? = null
var tethering: PokemonPastureBlockEntity.Tethering? = null
@@ -557,55 +554,22 @@ open class PokemonEntity(
}
override fun thunderHit(level: ServerLevel, lightning: LightningBolt) {
// Ground types shouldn't take lightning damage
val isTypeImmune = ElementalTypes.GROUND in pokemon.types
// Deals with special cases in which Pokemon should either be immune or buffed by lightning strikes.
val isAbilityImmune = when (pokemon.ability.name) {
when (pokemon.ability.name) {
"lightningrod" -> {
this.addEffect(MobEffectInstance(MobEffects.DAMAGE_BOOST, 1200, 1))
true
}
"motordrive" -> {
this.addEffect(MobEffectInstance(MobEffects.MOVEMENT_SPEED, 1200, 0))
true
}
"voltabsorb" -> {
this.addEffect(MobEffectInstance(MobEffects.HEAL, 1, 1))
true
}
else -> false
}
// Lightning hits entities multiple times, if this Pokémon changed a feature because of this lighting strike it should be immune to all remaining hits of this specific lightning strike as well.
var rotated = this.lastLightningBoltUUID == lightning.uuid
if (!rotated && pokemon.form.behaviour.lightningHit.isSpecial()) {
for (rotateFeature in pokemon.form.behaviour.lightningHit.rotateFeatures) {
val feature: StringSpeciesFeature = pokemon.getFeature(rotateFeature.key) ?: continue
val index = rotateFeature.chain.indexOf(feature.value)
// index is -1 if the element wasn't found, i.e. here if the feature's value is not part of the chain
if (index < 0) continue
val next = index.inc()
val nextIndex = if (next < rotateFeature.chain.size) next else 0
feature.value = rotateFeature.chain[nextIndex]
pokemon.markFeatureDirty(feature)
rotated = true
}
if (rotated) {
this.lastLightningBoltUUID = lightning.uuid
this.playSound(SoundEvents.MOOSHROOM_CONVERT, 2.0F, 1.0F)
pokemon.updateAspects()
}
}
if (!isTypeImmune && !isAbilityImmune && !rotated) {
super.thunderHit(level, lightning)
// Ground types shouldn't take lightning damage
else -> if (this.pokemon.types.none { it == ElementalTypes.GROUND }) super.thunderHit(level, lightning)
}
}
override fun tick() {
/* Addresses watchdog hanging that is completely bloody inexplicable. */
yBodyRot = Mth.wrapDegrees(yBodyRot)
@@ -626,7 +590,8 @@ open class PokemonEntity(
if (passengers.isNotEmpty() && level().isClientSide) {
rideSoundManager.tick()
ridingAnimationData.update()
} else if (!passengers.isNotEmpty() && level().isClientSide) {
} else if (!passengers.isNotEmpty() && level().isClientSide)
{
rideSoundManager.stop()
}
@@ -2115,7 +2080,7 @@ open class PokemonEntity(
private fun createSidedPokemon(): Pokemon = Pokemon().apply { isClient = this@PokemonEntity.level().isClientSide }
override fun canRide(entity: Entity): Boolean {
return platform == PlatformType.NONE && super.canRide(entity) && seats.isNotEmpty()
return platform == PlatformType.NONE && super.canRide(entity)
}
// Takes in a requested stat type with a base minimum and base maximum and returns the interpolated
@@ -2131,10 +2096,6 @@ open class PokemonEntity(
return max(statVal, 1e-6)
}
override fun couldAcceptPassenger(): Boolean {
return seats.isNotEmpty() && super.couldAcceptPassenger()
}
fun getRawRideStat(stat: RidingStat, style: RidingStyle): Double {
if (rideStatOverrides[style] != null && rideStatOverrides[style]!![stat] != null) {
return rideStatOverrides[style]!![stat]!!
@@ -2360,6 +2321,12 @@ open class PokemonEntity(
return loc.compareTo(resourceLocation) == 0
}
fun applyRenderRotation(partialTicks: Float) {
ifRidingAvailable { behaviour, settings, state ->
behaviour.applyRenderRotation(settings, state, this, partialTicks)
}
}
var jumpInputStrength: Int = 0 // move this
override fun onPlayerJump(strength: Int) {
// See if this controls the hot bar element
@@ -2457,12 +2424,8 @@ 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 ->
val rideFov = behaviour.rideFovMultiplier(settings, state, this, driver)
1f + (rideFov - 1f) * fovEffectScale
behaviour.rideFovMultiplier(settings, state, this, driver)
}
}
@@ -45,12 +45,10 @@ class DrowsySensor : Sensor<PokemonEntity>(20) {
entity.brain.eraseMemory(CobblemonMemories.POKEMON_DROWSY)
}
} else {
if (!isDrowsy && shouldBeDrowsy && entity.random.nextFloat() < rest.drowsyChance) {
if (!isDrowsy && shouldBeDrowsy) {
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)
}
}
}
Loaded 100 of 463 files, more files were not shown because too many files have changed in this diff. Show more