Compare commits

..
Author SHA1 Message Date
Gertimoshka aa7a90ad02 Finalization Of The System 2025-11-19 23:06:19 +02:00
Gertimoshka 5d1f0804d5 fixup on how colour copying for transparent pixels done 2025-11-02 19:56:09 +02:00
Gertimoshka 5b01754a82 Having -1 Texture Slot will now provide a transparent texture 2025-10-31 20:59:23 +02:00
Gertimoshka 6630ee942c Merge branch 'main' into keyframable-textures 2025-10-31 15:16:56 +02:00
Hiroku 88932c8c41 Merge branch 'poke-snack-spawner' into 'main'
Makes Poké Snacks act as spawners

See merge request cable-mc/cobblemon!1915
2025-10-31 12:10:36 +00:00
lionandHiroku 411f98d0ae Makes Poké Snacks act as spawners 2025-10-31 12:10:36 +00:00
Polymeta 02a1929ede Merge branch 'fix-pick-block' into 'main'
Fix picking blocks while riding mounts

Closes #1698

See merge request cable-mc/cobblemon!1922
2025-10-31 12:00:14 +00:00
JordanandPolymeta ff90059eec Fix picking blocks while riding mounts 2025-10-31 12:00:13 +00:00
Polymeta 4a30bb3b04 Merge branch 'seasoning-filters' into 'main'
Cooking Pot Recipes now have filtered

See merge request cable-mc/cobblemon!1919
2025-10-29 07:45:59 +00:00
Polymeta fac8c5f510 Merge branch 'party-overlay-config-fix' into 'main'
Party overlay config fix

See merge request cable-mc/cobblemon!1921
2025-10-28 14:41:22 +00:00
lionandPolymeta 2d077c607a Party overlay config fix 2025-10-28 14:41:21 +00:00
Gertimoshka f46968f878 Remove Unnecesary Comments 2025-10-28 07:20:54 +02:00
Polymeta b8f73f277d Fix PokeRodFishingBobberEntity baitId detection for ReelInPokemonCriterionCondition
Fixes #1744

Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-10-25 21:40:17 +01:00
Polymeta 6df2b996e2 Merge branch 'smarter-scalable-brains' into 'main'
New tools for brain activity management

See merge request cable-mc/cobblemon!1916
2025-10-25 19:39:15 +00:00
Polymeta ddc4bd358d Merge branch 'niche-tags' into 'main'
Berry Tag Updates

See merge request cable-mc/cobblemon!1918
2025-10-25 19:34:59 +00:00
Gertimoshka e197287775 Fixed preview item still using seasoning items even if its invalid by tag 2025-10-25 22:04:47 +03:00
dhi_holo be019c79df Update poke_cake.json
Sadly, Poke Cakes do not get tinted by non-flavour colours, so despite my attempt to allow it, it just doesn't work
2025-10-25 20:45:47 +10:00
dhi_holo 73fe851c58 Merge branch 'seasoning-filters' of https://gitlab.com/cable-mc/cobblemon into seasoning-filters 2025-10-25 20:20:09 +10:00
dhi_holo 8544744530 Tags in Recipes 2025-10-25 20:19:37 +10:00
Gertimoshka 32d7bfcef3 Recipes no longer need to define seasoning tag and will assume it as cobblemon:empty 2025-10-25 12:53:25 +03:00
Gertimoshka 27263cd057 JEI is now filtering seasoning slots additionally based berries 2025-10-25 12:22:08 +03:00
Gertimoshka 55f0962205 Recipes will now not consume seasoning items that are not in seasoning filter of recipe and slots will highlight red if seasoning item does not apply to current recipe 2025-10-25 12:02:22 +03:00
dhi_holo 40c5df46ce Berry Tag Updates
Reformatted how the tag is handled to be a bit cleaner, and allow for more specific berry querying via searching, mod compat, etc
2025-10-25 17:08:06 +10:00
dhi_holo e753c75c31 Merge branch 'seasoning-filters' of https://gitlab.com/cable-mc/cobblemon into seasoning-filters 2025-10-25 15:52:35 +10:00
dhi_holo 4e96222a4d Tags
Renamed a tag and fixed errors it caused, as well as adding the current 6 Filter tags requested for system upgrades
2025-10-25 15:52:09 +10:00
Paul 4ee90d4cfb Removed unneeded comment 2025-10-25 15:22:29 +10:00
Paul 540cd612c1 Added seasoning tag filter to cooking pot recipes 2025-10-25 14:22:47 +10:00
Hiroku 1b1d22fdf3 Merge branch 'social-distancing' into 'main'
Adds random offset to follow herd leader task

See merge request cable-mc/cobblemon!1917
2025-10-25 01:49:48 +00:00
lionandHiroku 1ee1001774 Adds random offset to follow herd leader task 2025-10-25 01:49:48 +00:00
Paul 533272ef09 u got a loicense for that error? 2025-10-24 20:27:31 +10:00
Landon 91300e4d03 Merge branch '1.7-spawns' into 'main'
Some JSON tweaks

See merge request cable-mc/cobblemon!1913
2025-10-21 23:38:34 +00:00
Polymeta fc135faf57 revert pokemon number range 2025-10-21 22:48:59 +00:00
Polymeta cdd72c664e fix some typos
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-10-21 23:23:43 +01:00
Polymeta 5f025d0f68 Merge branch 'moomoo-milk' into 'main'
Adds clear stat changes effect to Moomoo Milk

See merge request cable-mc/cobblemon!1911
2025-10-21 21:50:59 +00:00
lionandPolymeta 6af2c807ab Adds clear stat changes effect to Moomoo Milk 2025-10-21 21:50:58 +00:00
Polymeta c45c90c6c7 Merge branch 'main' into 1.7-spawns
# Conflicts:
#	common/src/main/resources/data/cobblemon/spawn_pool_world/0027_sandshrew.json
#	common/src/main/resources/data/cobblemon/spawn_pool_world/0028_sandslash.json
#	common/src/main/resources/data/cobblemon/spawn_pool_world/0079_slowpoke.json
#	common/src/main/resources/data/cobblemon/spawn_pool_world/0080_slowbro.json
#	common/src/main/resources/data/cobblemon/spawn_pool_world/0194_wooper.json
#	common/src/main/resources/data/cobblemon/spawn_pool_world/0199_slowking.json
#	common/src/main/resources/data/cobblemon/spawn_pool_world/0688_binacle.json
#	common/src/main/resources/data/cobblemon/spawn_pool_world/0689_barbaracle.json
#	common/src/main/resources/data/cobblemon/spawn_pool_world/0751_dewpider.json
#	common/src/main/resources/data/cobblemon/spawn_pool_world/0752_araquanid.json
#	common/src/main/resources/data/cobblemon/spawn_pool_world/0845_cramorant.json
#	common/src/main/resources/data/cobblemon/tags/worldgen/biome/is_overworld.json
#	utilityscripts/cobblemon_spawn_csv_to_json.py
2025-10-21 22:38:33 +01:00
Polymeta 1ec97c4c90 Merge branch 'riding-style-molang' into 'main'
Add molang function to get the current riding style of a pokemon

See merge request cable-mc/cobblemon!1912
2025-10-21 06:42:54 +00:00
Landon Wills f0bcd38ac7 Add molang function to get the current riding style of a pokemon 2025-10-21 01:55:19 -04:00
Test f4ba446c79 Merge branch 'restoration-tank-fixes' into 'main'
Small fixes

Closes #1735 and #1741

See merge request cable-mc/cobblemon!1908
2025-10-21 03:49:09 +00:00
Guilherme Pereira 60f98ebfce refactor poke snack 2025-10-21 00:04:20 -03:00
Guilherme Pereira 36a93bda5a refactor poke snack 2025-10-20 03:10:09 -03:00
Paul 8d60b07c54 Merge remote-tracking branch 'origin/main' 2025-10-20 14:45:26 +10:00
Paul 5f9cc86044 Added rouseChance and drowsyChance to RestBehaviour so pokemon don't insta sleep in the wild and may spontaneously wake up 2025-10-20 14:45:06 +10:00
Landon a6b7e07cf5 Merge branch 'held-item-fix' into 'main'
Remove pvp check for held item logic

See merge request cable-mc/cobblemon!1907
2025-10-20 03:18:36 +00:00
Landon Wills 0ba6215b8d Elaborate on comment 2025-10-19 23:05:16 -04:00
Guilherme Pereira e2f6fae689 change willDefendSelf combat behaviour default to false 2025-10-19 23:36:52 -03:00
Guilherme Pereira 296aa01d0f remove println 2025-10-19 18:41:38 -03:00
Guilherme Pereira 12ab3f2d07 fix dolphin crash on servers 2025-10-19 18:18:31 -03:00
Guilherme Pereira 5a7a374bc9 Merge branch 'main' of gitlab.com:cable-mc/cobblemon into restoration-tank-fixes 2025-10-19 17:35:50 -03:00
Guilherme Pereira ceece45ad4 make poke snack spawn 4 mons per bite 2025-10-19 17:24:01 -03:00
Landon Wills b344ad66c5 Remove pvp check 2025-10-19 14:44:08 -04:00
Paul dbc7af4ad4 Don't need to override the start conditions function for activity aware behavior 2025-10-18 21:40:43 +10:00
Paul 5f5bf01cb5 Added ActivityAwareBehavior and tagging for memories and activities 2025-10-18 21:30:47 +10:00
Guilherme Pereira 3f03afc7eb smooth campfire pot spinning animation 2025-10-18 06:53:20 -03:00
Guilherme Pereira 93011f084b make riding fov effects proportional to minecraft fov effect scale setting 2025-10-15 19:23:51 -03:00
Guilherme Pereira 7c90a8425e fix restoration tank concurrent modification crash #1735 2025-10-15 15:01:41 -03:00
Veraxiel 17c71efb17 Added spawns for 1.7 Pokémon. Updated spawns. Updated spawn scraper. 2025-09-08 02:04:58 -04:00
Veraxiel a5b6a954fd Added "Mansion Dining" preset tag. Updated is_overworld tag. 2025-09-07 20:42:53 -04:00
Gertimoshka 3b7bd4d810 Keyframable Textures now come in 2 flavors of interpolation: Linear and Bezier 2025-06-26 15:38:12 +03:00
Gertimoshka 81278e2d16 Frist partial implementation of keyframable textures 2025-06-19 09:19:22 +03:00
Gertimoshka 3af66e0028 Remade some code to make implementation of the texture rendering on pokemon to interface neatly with new methods of texture generation like interpolated textures rather than being a side on slap 2025-06-10 20:34:40 +03:00
Gertimoshka 75ead2f665 The beginning :3 of making a great tool for animators/textures 2025-06-07 12:13:07 +03:00
418 changed files with 12431 additions and 3238 deletions
+2 -1
View File
@@ -12,7 +12,8 @@ out
*.ipr
*.iws
*.iml
.idea
.idea/*
!.idea/icon.png
# gradle
build
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

+9 -1
View File
@@ -12,6 +12,14 @@
- SpawningContext is now SpawnablePosition
- WorldSlice is SpawningZone
- SpawningProspector is now SpawningZoneGenerator
- Majorly refactored the hierarchy of Spawner
- The base Spawner interface provides more functions to allow single-point and area spawning given appropriate inputs.
- TickingSpawner is removed in favour of outside code handling ticking logic.
- AreaSpawner is removed.
- BasicSpawner is the first implementation of Spawner which can be used for any purpose.
- PlayerSpawners are now mixin'd into ServerPlayer and ticked from ServerPlayer#tick.
- A hierarchy diagram can be found at `./docs/spawner-hierarchy.png` in the mod repository.
- The SpawnerManager class has been removed as its functionality is all now handled elsewhere.
- Renamed things in Spawn Rules to go with the other renames:
- contextSelector is now spawnablePositionSelector
- contextSelector is now spawnablePositionSelector
- context is now spawnable_position
+8
View File
@@ -421,6 +421,8 @@
- Updated some item tags to better integrate behaviours between Cobblemon, Vanilla Minecraft, and other mods
- Removed Cooked Meat, Raw Meat, Protein Ingredients, and Zinc Ingredients, the first two are now using `c` namespace tags, the latter have better integrated use of tags within their recipes which removes need for custom tags.
- Added our seeds tag into `#c:seeds`, which is now made use of for the Miracle Seed recipe.
- Updated the `#cobblemon:berries` tag to use subtags located in `data/cobblemon/tags/item/berries`, such as `#cobblemon:berries/hp_recovery` for sorting purposes
- Moved the tag `#cobblemon:mutated_berries` to `#cobblemon:berries/mutation_result`
- Added herbs and snowballs to the consumable in PvE and Wild battle tags
- Tweaked the Natural Materials Vanilla file to fit with the changes to tags
- Substantially optimised spawning checks mainly by front-loading biome filtering.
@@ -473,6 +475,7 @@
- Roseli Berry Trees now naturally generate in their preferred biomes.
- Refactored dynamic lights compat to be more future proof. The dynamic light support is only tested with LambDynamicLights, on NeoForged use that mod in combination with Sinytra Connector
- Shulker Boxes and Traveler's Backpacks can no longer be held by Pokémon. Thanks Monocle ;) You could re-enable this with a datapack but you'd be crazy.
- Moomoo Milk now clears Pokémon stat changes when used in battle.
### Fixes
- Fixed game crashing when removing national pokedex using datapacks
@@ -557,6 +560,8 @@
- Fixed singular Pokémon corruption causing entire storage corruption. Storages will now skip corrupted Pokémon and print an error to console.
- Fixed species additions not being able to properly mark a species as implemented.
- Fixed Pokémon item models not showing a glint when enchanted.
- Fixed some specific bag items not being dropped when used in battle.
- Fixed the "use all berry bait" achievement not being progressed
### Developer
- A finished battle now has winners and losers set inside of `PokemonBattle` instead of them always being empty.
@@ -701,6 +706,9 @@
- Mechanics
- Unlockable Wallpapers
- Starter Data
- Fixed `cobblemon:reel_in_pokemon` criteria not working when used together with a `baitId`
- Also changed the default from `cobblemon:empty_bait` to `any`
- The previous default is still available by using the above as baitId
### Particles
Added new/updated particles for the following moves:
@@ -0,0 +1,22 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common;
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner;
/**
* Used for mixin shenanigans. ServerPlayers now store their Cobblemon spawner
* inside, so this interface helps execute that.
*
* @author Hiroku
* @since October 27th, 2025
*/
public interface PlayerSpawnerGetter {
PlayerSpawner getPlayerSpawner();
}
@@ -1,20 +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.duck;
import net.minecraft.world.phys.Vec3;
public interface RidePassenger {
void cobblemon$setRideXRot(float rideXRot);
float cobblemon$getRideXRot();
void cobblemon$setRideYRot(float rideYRot);
float cobblemon$getRideYRot();
void cobblemon$setRideEyePos(Vec3 rideEyePos);
Vec3 cobblemon$getRideEyePos();
}
@@ -1,42 +0,0 @@
package com.cobblemon.mod.common.mixin;
import com.cobblemon.mod.common.OrientationControllable;
import com.cobblemon.mod.common.duck.RidePassenger;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.projectile.AbstractArrow;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(AbstractArrow.class)
public abstract class AbstractArrowMixin {
// Target the constructor that takes a LivingEntity owner
@Inject(
method = "<init>(Lnet/minecraft/world/entity/EntityType;Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/world/level/Level;Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/item/ItemStack;)V",
at = @At("TAIL") // Inject at the very end of the constructor
)
private void cobblemon$adjustSpawnPosition(
EntityType<? extends AbstractArrow> entityType,
LivingEntity owner,
Level level,
ItemStack pickupItemStack,
@Nullable ItemStack firedFromWeapon,
CallbackInfo ci
) {
AbstractArrow arrow = (AbstractArrow)(Object)this;
if(!(owner.getVehicle() instanceof OrientationControllable vehicle)) return;
var vehicleController = vehicle.getOrientationController();
// If this is a rolling ride passenger then use the rider eye position to set the arrow spawn position.
if (owner instanceof RidePassenger ridePassenger && vehicleController != null && vehicleController.getActive()) {
var eyePos = ridePassenger.cobblemon$getRideEyePos().add(0.0, -2.0, 0.0);
arrow.setPos(eyePos);
}
}
}
@@ -7,6 +7,7 @@
*/
package com.cobblemon.mod.common.mixin;
import com.cobblemon.mod.common.duck.ChannelDuck;
import com.mojang.blaze3d.audio.Channel;
import org.lwjgl.openal.AL10;
@@ -8,23 +8,18 @@
package com.cobblemon.mod.common.mixin;
import com.cobblemon.mod.common.CobblemonNetwork;
import com.cobblemon.mod.common.OrientationControllable;
import com.cobblemon.mod.common.api.orientation.OrientationController;
import com.cobblemon.mod.common.api.pokemon.effect.ShoulderEffectRegistry;
import com.cobblemon.mod.common.api.riding.RidingStyle;
import com.cobblemon.mod.common.api.riding.Seat;
import com.cobblemon.mod.common.api.riding.behaviour.types.liquid.SubmarineBehaviour;
import com.cobblemon.mod.common.client.entity.PokemonClientDelegate;
import com.cobblemon.mod.common.client.render.MatrixWrapper;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.cobblemon.mod.common.net.messages.server.orientation.ServerboundUpdateOrientationPacket;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.ClipContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.HitResult;
@@ -37,7 +32,6 @@ 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;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(LivingEntity.class)
public abstract class LivingEntityMixin extends Entity implements OrientationControllable {
@@ -53,12 +47,6 @@ 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;
@@ -67,48 +55,6 @@ public abstract class LivingEntityMixin extends Entity implements OrientationCon
}
}
@Override
public @NotNull HitResult pick(double hitDistance, float partialTicks, boolean hitFluids) {
Entity vehicle = this.getVehicle();
if (vehicle instanceof PokemonEntity pokemonEntity && pokemonEntity.getDelegate() instanceof PokemonClientDelegate delegate) {
int seatIndex = pokemonEntity.getPassengers().indexOf(this);
Seat seat = pokemonEntity.getSeats().get(seatIndex);
MatrixWrapper locator = delegate.getLocatorStates().get(seat.getLocator());
Vec3 locatorOffset = new Vec3(locator.getMatrix().getTranslation(new Vector3f()));
Vec3 eyePosition = cobblemon$getEyePosition(partialTicks, pokemonEntity, locatorOffset);
Vec3 viewVector = this.getViewVector(partialTicks);
Vec3 viewDistanceVector = eyePosition.add(viewVector.x * hitDistance, viewVector.y * hitDistance, viewVector.z * hitDistance);
return this.level()
.clip(
new ClipContext(
eyePosition, viewDistanceVector, ClipContext.Block.OUTLINE, hitFluids ? net.minecraft.world.level.ClipContext.Fluid.ANY : net.minecraft.world.level.ClipContext.Fluid.NONE, this
)
);
}
return super.pick(hitDistance, partialTicks, hitFluids);
}
@Unique
private Vec3 cobblemon$getEyePosition(float partialTicks, PokemonEntity pokemonEntity, Vec3 locatorOffset) {
OrientationController controller = this.getOrientationController();
float currEyeHeight = this.getEyeHeight();
Vec3 offset = locatorOffset.add(pokemonEntity.position());
if (controller.isActive() && controller.getOrientation() != null) {
Quaternionf orientation = controller.getRenderOrientation(partialTicks);
Vec3 rotatedEyeHeight = new Vec3(orientation.transform(new Vector3f(0f, currEyeHeight - (this.getBbHeight() / 2), 0f)));
offset.add(rotatedEyeHeight);
}
return offset;
}
@Inject(method = "tick", at = @At("HEAD"))
private void cobblemon$updateRenderOrientation(CallbackInfo ci) {
this.cobblemon$orientationController.tick();
@@ -128,31 +74,4 @@ public abstract class LivingEntityMixin extends Entity implements OrientationCon
}
}
@Inject(method = "decreaseAirSupply", at = @At("HEAD"), cancellable = true)
private void cobblemon$conditionalWaterBreathing(
int currentAir,
CallbackInfoReturnable<Integer> cir
) {
var entity = (LivingEntity) (Object) this;
if (!(entity instanceof Player player)) return;
if (!(player.getVehicle() instanceof PokemonEntity vehicle)) return;
var ridingController = vehicle.getRidingController();
if (ridingController == null) return;
var activeContext = ridingController.getContext();
if (activeContext == null) return;
// If the rider is currently on a submarine and the ride still has stamina.
var stamina = activeContext.getState().getStamina().get();
if (
activeContext.getStyle() == RidingStyle.LIQUID &&
activeContext.getBehaviour().equals(SubmarineBehaviour.Companion.getKEY()) &&
stamina > 0.0f
) {
cir.setReturnValue(currentAir);
cir.cancel();
}
}
}
@@ -19,7 +19,6 @@ import com.cobblemon.mod.common.api.storage.party.PartyStore;
import com.cobblemon.mod.common.api.storage.party.PlayerPartyStore;
import com.cobblemon.mod.common.api.tags.CobblemonItemTags;
import com.cobblemon.mod.common.duck.PlayerDuck;
import com.cobblemon.mod.common.duck.RidePassenger;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.cobblemon.mod.common.pokedex.scanner.PokedexEntityData;
import com.cobblemon.mod.common.pokedex.scanner.ScannableEntity;
@@ -48,7 +47,6 @@ import net.minecraft.world.entity.player.Player;
import net.minecraft.world.food.FoodProperties;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.Nullable;
import org.joml.Vector3f;
import org.objectweb.asm.Opcodes;
@@ -65,7 +63,7 @@ import java.util.*;
import java.util.stream.Collectors;
@Mixin(Player.class)
public abstract class PlayerMixin extends LivingEntity implements ScannableEntity, OrientationControllable, PlayerDuck, RidePassenger {
public abstract class PlayerMixin extends LivingEntity implements ScannableEntity, OrientationControllable, PlayerDuck {
@Unique
private Vector3f cobblemon$driverInput;
@@ -93,12 +91,6 @@ public abstract class PlayerMixin extends LivingEntity implements ScannableEntit
@Shadow protected int jumpTriggerTime;
@Unique private final OrientationController cobblemon$orientationController = new OrientationController(this);
@Unique private float cobblemon$rideXRot = 0.0f;
@Unique private float cobblemon$rideYRot = 0.0f;
@Unique private Vec3 cobblemon$rideEyePos = Vec3.ZERO;
protected PlayerMixin(EntityType<? extends LivingEntity> p_20966_, Level p_20967_) {
super(p_20966_, p_20967_);
}
@@ -125,6 +117,11 @@ public abstract class PlayerMixin extends LivingEntity implements ScannableEntit
}
}
@Inject(method = "tick", at = @At("HEAD"))
private void cobblemon$updateRenderOrientation(CallbackInfo ci) {
this.cobblemon$orientationController.tick();
}
@Override
public EntityDimensions getDefaultDimensions(Pose pose) {
if (this.getVehicle() instanceof PokemonEntity) {
@@ -310,34 +307,4 @@ public abstract class PlayerMixin extends LivingEntity implements ScannableEntit
return cobblemon$lastSentDriverInput;
}
@Override
public float cobblemon$getRideXRot() {
return this.cobblemon$rideXRot;
}
@Override
public void cobblemon$setRideXRot(float rideXRot) {
this.cobblemon$rideXRot = rideXRot;
}
@Override
public float cobblemon$getRideYRot() {
return this.cobblemon$rideYRot;
}
@Override
public void cobblemon$setRideYRot(float rideYRot) {
this.cobblemon$rideYRot = rideYRot;
}
@Override
public Vec3 cobblemon$getRideEyePos() {
return this.cobblemon$rideEyePos;
}
@Override
public void cobblemon$setRideEyePos(Vec3 rideEyePos) {
this.cobblemon$rideEyePos = rideEyePos;
}
}
@@ -8,35 +8,64 @@
package com.cobblemon.mod.common.mixin;
import com.cobblemon.mod.common.Cobblemon;
import com.cobblemon.mod.common.OrientationControllable;
import com.cobblemon.mod.common.PlayerSpawnerGetter;
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner;
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawnerFactory;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import 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 {
public abstract class ServerPlayerMixin implements PlayerSpawnerGetter {
@Shadow
public abstract ServerLevel serverLevel();
@Unique
public PlayerSpawner cobblemon$spawner;
@Override
public PlayerSpawner getPlayerSpawner() {
ServerPlayer player = (ServerPlayer)(Object)this;
if (cobblemon$spawner == null) {
cobblemon$spawner = PlayerSpawnerFactory.INSTANCE.create(player);
}
return cobblemon$spawner;
}
@Inject(method = "tick", at = @At("TAIL"))
private void cobblemon$tickSpawner(CallbackInfo ci) {
if (!Cobblemon.config.getEnableSpawning()) {
return;
} else if (!serverLevel().getGameRules().getBoolean(CobblemonGameRules.DO_POKEMON_SPAWNING)) {
return;
}
getPlayerSpawner().tick();
}
@Inject(method = "rideTick", at = @At("HEAD"))
private void cobblemon$updateOrientationControllerRideTick(CallbackInfo ci) {
ServerPlayer player = (ServerPlayer)(Object)this;
Entity vehicle = player.getVehicle();
if (!(vehicle instanceof OrientationControllable controllableVehicle)) return;
var shouldUseCustomOrientation = cobblemon$shouldUseCustomOrientation(vehicle);
controllableVehicle.getOrientationController().setActive(shouldUseCustomOrientation);
if (!(this instanceof OrientationControllable controllable)) return;
var shouldUseCustomOrientation = cobblemon$shouldUseCustomOrientation((ServerPlayer)(Object)this);
controllable.getOrientationController().setActive(shouldUseCustomOrientation);
}
@Unique
private boolean cobblemon$shouldUseCustomOrientation(Entity entity) {
if (entity == null) return false;
if (!(entity instanceof PokemonEntity pokemonEntity)) return false;
private boolean cobblemon$shouldUseCustomOrientation(ServerPlayer player) {
var playerVehicle = player.getVehicle();
if (playerVehicle == null) return false;
if (!(playerVehicle instanceof PokemonEntity pokemonEntity)) return false;
return pokemonEntity.ifRidingAvailableSupply(false, (behaviour, settings, state) -> {
return behaviour.shouldRoll(settings, state, pokemonEntity);
});
@@ -44,9 +73,8 @@ public abstract class ServerPlayerMixin {
@Inject(method = "stopRiding", at = @At("HEAD"))
public void cobblemon$resetOrientationOnDismount(CallbackInfo ci) {
ServerPlayer player = (ServerPlayer)(Object)this;
if (!(player.getVehicle() instanceof OrientationControllable controllableVehicle)) return;
controllableVehicle.getOrientationController().setActive(false);
if (!(this instanceof OrientationControllable controllable)) return;
controllable.getOrientationController().setActive(false);
}
//TODO: Switch to sending out on load instead of preventing saving
@@ -54,5 +82,4 @@ public abstract class ServerPlayerMixin {
private boolean cobblemon$cancelSavingPokemonMounts(boolean original, @Local(ordinal=0) Entity entity) {
return original && !(entity instanceof PokemonEntity);
}
}
@@ -34,8 +34,7 @@ public class AbstractClientPlayerMixin {
} else {
// Only modify fov through riding if the pokedex is not open
LocalPlayer player = (LocalPlayer) (Object) this;
// Only modify fov for the driver and not the passengers
if (player != null && player.isPassenger() && player.getVehicle() instanceof PokemonEntity && player.getVehicle().getControllingPassenger() == player) {
if (player != null && player.isPassenger() && player.getVehicle() instanceof PokemonEntity) {
PokemonEntity ride = (PokemonEntity) player.getVehicle();
// Return custom fov mult for riding
@@ -14,15 +14,12 @@ import com.cobblemon.mod.common.api.orientation.OrientationController;
import com.cobblemon.mod.common.api.riding.Rideable;
import com.cobblemon.mod.common.client.MountedPokemonAnimationRenderController;
import com.cobblemon.mod.common.client.render.camera.MountedCameraRenderer;
import com.cobblemon.mod.common.duck.RidePassenger;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import com.mojang.blaze3d.Blaze3D;
import net.minecraft.client.Camera;
import net.minecraft.client.Minecraft;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.phys.Vec3;
import org.joml.Matrix3f;
@@ -36,9 +33,6 @@ 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;
@@ -54,43 +48,45 @@ public abstract class CameraMixin {
@Shadow protected abstract void setPosition(Vec3 pos);
@Shadow private float partialTickTime;
// Used to help in camera rotation rendering when transitioning
// from rolled to unrolled.
@Unique private float cobblemon$returnTimer = 0;
@Unique private float cobblemon$rollAngleStart = 0;
@Unique private double cobblemon$lastHandledRotationTime = Double.MIN_VALUE;
@Unique private double cobblemon$frameTime = 0.0;
@Unique private float returnTimer = 0;
@Unique private float rollAngleStart = 0;
@Unique Minecraft minecraft = Minecraft.getInstance();
@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;
var vehicle = this.entity.getVehicle();
if (!(vehicle instanceof OrientationControllable controllableVehicle)) return;
var instance = (Camera)(Object)this;
// If the current vehicle has no orientation then return and perform
// vanilla camera rotations
var vehicleController = controllableVehicle.getOrientationController();
if (vehicleController.getOrientation() == null) return;
// If the controller has been deactivated but the orientation isn't null yet
// then perform transition from rolled ride/camera to vanilla
if (!vehicleController.isActive() && vehicleController.getOrientation() != null) {
cobblemon$applyTransitionRotation(vehicleController, ci);
} else { // Otherwise perform cobblemon specific camera rotation and set transition helper variables
cobblemon$applyRotation();
this.cobblemon$returnTimer = 0;
this.cobblemon$rollAngleStart = instance.rotation().getEulerAnglesYXZ(new Vector3f()).z();
ci.cancel();
if (!(this.entity instanceof OrientationControllable controllable)) return;
var controller = controllable.getOrientationController();
if (!controller.isActive() && controller.getOrientation() != null) {
if(this.returnTimer < 1) {
//Rotation is taken from entity since we no longer handle mouse ourselves
//Stops a period of time when you can't input anything.
controller.getOrientation().set(
new Matrix3f()
.rotateY((float) Math.toRadians(180 - this.entity.getYRot()))
.rotateX((float) Math.toRadians(-this.entity.getXRot()))
);
if(rollAngleStart == 0){
this.returnTimer = 1;
controller.reset();
return;
}
controller.getOrientation().rotateZ((float) Math.toRadians(-rollAngleStart*(1-returnTimer)));
cobblemon$applyRotation();
this.returnTimer += .05F;
ci.cancel();
} else {
this.returnTimer = 1;
controller.reset();
}
return;
}
if (controller.getOrientation() == null) return;
cobblemon$applyRotation();
this.returnTimer = 0;
this.rollAngleStart = controller.getRoll();
ci.cancel();
}
@WrapOperation(method = "setup", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;setPosition(DDD)V"))
@@ -123,128 +119,20 @@ public abstract class CameraMixin {
@Unique
private void cobblemon$applyRotation(){
var driver = this.entity;
if (!(driver.getVehicle() instanceof OrientationControllable controllableVehicle) ||
controllableVehicle.getOrientationController().getOrientation() == null ||
!controllableVehicle.getOrientationController().getActive()
) return;
// Grab the driver and vehicle orientation controllers.
var vehicleController = controllableVehicle.getOrientationController();
var isDriving = driver.getControlledVehicle() != null;
// Grab the ride orientation
var newRotation = new Quaternionf();
// If the player is driving then use the current orientation since the driver sets it.
// If not then use the smoothed render orientation for passengers otherwise the vehicle rotation will be jerky
if (isDriving) {
newRotation = vehicleController.getOrientation().normal(new Matrix3f()).getNormalizedRotation(new Quaternionf());
} else {
newRotation = vehicleController.getRenderOrientation(this.partialTickTime);
}
// Unroll the current rotation if no roll is requested
if (Cobblemon.config.getDisableRoll()) {
var eulerAngs = newRotation.getEulerAnglesYXZ(new Vector3f());
var currAngs = this.rotation.getEulerAnglesYXZ(new Vector3f());
var vehicle = (PokemonEntity)driver.getVehicle();
var vel = vehicle.getRidingAnimationData().getVelocitySpring().getInterpolated(this.partialTickTime, 0);
// lerp the new rotation towards this y rot at a rate proportional to the horizontal movement speed?
var yaw = Mth.wrapDegrees(Math.toDegrees(Mth.atan2(vel.x(), vel.z())) + 180.0f);
var degDiff = Mth.degreesDifference((float)Math.toDegrees(currAngs.y()), (float)yaw);
var pitchDeadzoneDeg = 15.0; // x degrees of deadzone about +-90 degrees of pitch.
var pitchDeg = vehicleController.getPitch();
// ConsiderPitch to be already 90 if within the deadzone around it.
var cutoffPitch = Math.min(1.0,(Mth.abs(pitchDeg) / (90.0f - pitchDeadzoneDeg))) * 90.0f * Math.signum(pitchDeg);
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 = 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 k = 0;
if (Minecraft.getInstance().options.getCameraType().isMirrored()) {
if (!(this.entity instanceof OrientationControllable controllable) || controllable.getOrientationController().getOrientation() == null) return;
var controller = controllable.getOrientationController();
var newRotation = controller.getOrientation().normal(new Matrix3f()).getNormalizedRotation(new Quaternionf());
if (this.minecraft.options.getCameraType().isMirrored()) {
newRotation.rotateY((float)Math.toRadians(180));
k = 180;
}
// Get the drivers local look angle in relation to the ride and apply it.
if (vehicleController.getActive()) {
var vehicleMatrix = new Matrix3f().set(newRotation);
var driverMatrix = new Matrix3f();// Base the driver
var playerRotater = (RidePassenger)driver;
newRotation = vehicleMatrix.mul(
driverMatrix.rotateYXZ(
-Mth.DEG_TO_RAD * (playerRotater.cobblemon$getRideYRot()),
-Mth.DEG_TO_RAD * playerRotater.cobblemon$getRideXRot(),
0.0f)
).normal(new Matrix3f()).getNormalizedRotation(new Quaternionf());
}
this.rotation.set(newRotation);
var eulerAngs = newRotation.getEulerAnglesXYZ(new Vector3f());
this.xRot = eulerAngs.x() * Mth.RAD_TO_DEG;
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 + k);
driver.setYRot(180.0f - (driverEulerAngs.y() * Mth.RAD_TO_DEG));
this.xRot = controller.getPitch();
this.yRot = controller.getYaw();
OrientationController.Companion.getFORWARDS().rotate(this.rotation, this.forwards);
OrientationController.Companion.getUP().rotate(this.rotation, this.up);
OrientationController.Companion.getLEFT().rotate(this.rotation, this.left);
}
@Unique
private void cobblemon$applyTransitionRotation(
OrientationController vehicleController,
CallbackInfo ci
) {
// If the transition has just started then
if (this.cobblemon$returnTimer == 0.0) {
MountedCameraRenderer.INSTANCE.resetDriverRotations((Camera)(Object)this, this.entity);
}
if(this.cobblemon$returnTimer < 1) {
//Rotation is taken from entity since we no longer handle mouse ourselves
//Stops a period of time when you can't input anything.
float interpolatedRoll = Mth.lerp(this.cobblemon$returnTimer, this.cobblemon$rollAngleStart, 0.0f);
float pitch = (float) Math.toRadians(-this.entity.getXRot());
float yaw = (float) Math.toRadians(180 - this.entity.getYRot());
Quaternionf interRot = new Quaternionf();
interRot.rotationYXZ(yaw, pitch, interpolatedRoll);
this.rotation.set(interRot);
var eulerAngs = interRot.getEulerAnglesXYZ(new Vector3f());
this.xRot = eulerAngs.x() * Mth.RAD_TO_DEG;
this.yRot = eulerAngs.y() * Mth.RAD_TO_DEG;
OrientationController.Companion.getFORWARDS().rotate(this.rotation, this.forwards);
OrientationController.Companion.getUP().rotate(this.rotation, this.up);
OrientationController.Companion.getLEFT().rotate(this.rotation, this.left);
if(cobblemon$rollAngleStart == 0){
this.cobblemon$returnTimer = 1;
vehicleController.reset();
MountedCameraRenderer.INSTANCE.resetDriverRotations((Camera)(Object)this, this.entity);
return;
}
this.cobblemon$returnTimer += (partialTickTime * (1.0f/20.0f));
ci.cancel();
} else {
this.cobblemon$returnTimer = 1;
vehicleController.reset();
MountedCameraRenderer.INSTANCE.resetDriverRotations((Camera)(Object)this, this.entity);
}
}
@WrapOperation(method = "setup", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;move(FFF)V", ordinal = 0))
public void stopMoveIfRolling(Camera instance, float zoom, float dy, float dx, Operation<Void> original) {
Entity entity = instance.getEntity();
@@ -9,15 +9,21 @@
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.api.scheduling.ClientTaskTracker;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.world.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.function.Predicate;
@Mixin(GameRenderer.class)
public class GameRendererMixin {
@Unique
@@ -42,4 +48,16 @@ public class GameRendererMixin {
lastTime = newTime;
}
@ModifyArg(
method = "pick(Lnet/minecraft/world/entity/Entity;DDF)Lnet/minecraft/world/phys/HitResult;",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/projectile/ProjectileUtil;getEntityHitResult(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/world/phys/AABB;Ljava/util/function/Predicate;D)Lnet/minecraft/world/phys/EntityHitResult;"
),
index = 4
)
private Predicate<Entity> filterPokemonVehicle(Predicate<Entity> original, @Local(ordinal = 0, argsOnly = true) Entity entity) {
return (entityX) -> !(entity.getVehicle() == entityX && entityX instanceof PokemonEntity) && original.test(entityX);
}
}
@@ -19,11 +19,9 @@ import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState;
import com.cobblemon.mod.common.client.render.models.blockbench.PosableModel;
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository;
import com.cobblemon.mod.common.duck.PlayerDuck;
import com.cobblemon.mod.common.duck.RidePassenger;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.cobblemon.mod.common.net.messages.server.orientation.ServerboundUpdateOrientationPacket;
import com.cobblemon.mod.common.net.messages.server.riding.ServerboundUpdateDriverInputPacket;
import com.cobblemon.mod.common.net.messages.server.riding.ServerboundUpdateRiderRotationPacket;
import com.mojang.authlib.GameProfile;
import net.minecraft.client.CameraType;
import net.minecraft.client.Minecraft;
@@ -31,10 +29,12 @@ import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.client.player.Input;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.ClipContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.NotNull;
@@ -83,33 +83,19 @@ public abstract class LocalPlayerMixin extends AbstractClientPlayer implements O
@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;
var vehicleController = controllableVehicle.getOrientationController();
if (!vehicleController.isActive() || vehicleController.getOrientation() == cobblemon$lastOrientation) return;
cobblemon$lastOrientation = vehicleController.getOrientation() != null ? new Matrix3f(vehicleController.getOrientation()) : null;
CobblemonNetwork.INSTANCE.sendToServer(new ServerboundUpdateOrientationPacket(this.getVehicle().getId(), vehicleController.getOrientation()));
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 = "rideTick", at = @At("HEAD"))
private void cobblemon$updateOrientationControllerRideTick(CallbackInfo ci) {
if (Minecraft.getInstance().player != (Object)this) return;
var driver = (LocalPlayer)(Object)this;
var vehicle = driver.getVehicle();
if (!(this.getVehicle() instanceof OrientationControllable controllableVehicle)) return;
if (!(this instanceof OrientationControllable controllable)) return;
var shouldUseCustomOrientation = cobblemon$shouldUseCustomOrientation((LocalPlayer)(Object)this);
var vehicleController = controllableVehicle.getOrientationController();
// If the player has just switched to riding a custom orientation ride then set their
// x and y rots local to the vehicle rots(if the controller wasn't active and will now be active)
// This ensures that on transition your camera stays in the same spot
if (!vehicleController.isActive() && shouldUseCustomOrientation) {
// Set local to the vehicle x and yrot so
var playerRotater = (RidePassenger)driver;
playerRotater.cobblemon$setRideXRot(Mth.wrapDegrees(driver.getXRot() - vehicle.getXRot()));
playerRotater.cobblemon$setRideYRot(Mth.wrapDegrees(driver.getYRot() - vehicle.getYRot()));
}
vehicleController.setActive(shouldUseCustomOrientation);
controllable.getOrientationController().setActive(shouldUseCustomOrientation);
}
@Inject(method = "rideTick", at = @At("HEAD"))
@@ -129,27 +115,6 @@ public abstract class LocalPlayerMixin extends AbstractClientPlayer implements O
CobblemonNetwork.INSTANCE.sendToServer(new ServerboundUpdateDriverInputPacket(driverInput));
}
@Inject(method = "rideTick", at = @At("HEAD"))
private void cobblemon$updateRiderRotationsRideTick(CallbackInfo ci) {
if (Minecraft.getInstance().player != (Object)this) return;
if (!(this.getVehicle() instanceof PokemonEntity pokemonEntity)) return;
// Only send rotation updates if the player is riding a vehicle that is currently rollable
if (!(this.getVehicle() instanceof OrientationControllable controllableVehicle)) return;
if (!controllableVehicle.getOrientationController().getActive()) return;
var passenger = (LocalPlayer)(Object)this;
var playerRotater = (RidePassenger)passenger;
// Gather Rider Rotation info
var rideXRot = playerRotater.cobblemon$getRideXRot();
var rideYRot = playerRotater.cobblemon$getRideYRot();
var rideEyePos = playerRotater.cobblemon$getRideEyePos();
// Send new info to server. Currently not checking to see if this has change substantially
CobblemonNetwork.INSTANCE.sendToServer(new ServerboundUpdateRiderRotationPacket(rideXRot, rideYRot, rideEyePos));
}
@Unique
private boolean cobblemon$shouldUseCustomOrientation(LocalPlayer player) {
var playerVehicle = player.getVehicle();
@@ -164,8 +129,8 @@ public abstract class LocalPlayerMixin extends AbstractClientPlayer implements O
public void stopRiding() {
super.stopRiding();
if (Minecraft.getInstance().player != (Object)this) return;
if (!(this instanceof OrientationControllable controllableVehicle)) return;
controllableVehicle.getOrientationController().setActive(false);
if (!(this instanceof OrientationControllable controllable)) return;
controllable.getOrientationController().setActive(false);
}
@Inject(method = "getJumpRidingScale", at = @At("HEAD"), cancellable = true)
@@ -10,16 +10,13 @@ package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.Cobblemon;
import com.cobblemon.mod.common.OrientationControllable;
import com.cobblemon.mod.common.api.orientation.OrientationController;
import com.cobblemon.mod.common.client.CobblemonClient;
import com.cobblemon.mod.common.client.keybind.keybinds.PartySendBinding;
import com.cobblemon.mod.common.duck.RidePassenger;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.cobblemon.mod.common.pokedex.scanner.PokedexUsageContext;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import com.llamalad7.mixinextras.sugar.Local;
import com.mojang.blaze3d.Blaze3D;
import kotlin.Pair;
import net.minecraft.client.Minecraft;
import net.minecraft.client.MouseHandler;
import net.minecraft.client.player.LocalPlayer;
@@ -35,9 +32,6 @@ import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import static com.google.common.primitives.Floats.min;
import static net.minecraft.util.Mth.lerp;
@Mixin(MouseHandler.class)
public abstract class MouseHandlerMixin {
@Shadow private double accumulatedScrollY;
@@ -132,22 +126,20 @@ public abstract class MouseHandlerMixin {
var defaultSensitivity = this.minecraft.options.sensitivity().get() * 0.6 + 0.2;
var spyglassSensitivity = Math.pow(defaultSensitivity, 3);
var lookSensitivity = spyglassSensitivity * 8.0;
var sensitivity = lerp(usageContext.getFovMultiplier(), spyglassSensitivity, lookSensitivity);
var sensitivity = Mth.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();
if (!(vehicle instanceof PokemonEntity)) return true;
// Clamp player rotation if riding and the vehicle demands it
if (player.isPassenger() && vehicle instanceof PokemonEntity pokeVehicle) {
pokeVehicle.clampPassengerRotation(player);
if (player.isPassenger() && player.getVehicle() instanceof PokemonEntity vehicle) {
vehicle.clampPassengerRotation(player);
}
// If the ride is not orientation controllable then return and perform the normal turnPlayer logic
if (!(vehicle instanceof OrientationControllable controllableVehicle) || !controllableVehicle.getOrientationController().isActive()) {
if (!(player instanceof OrientationControllable controllable)) return true;
if (!controllable.getOrientationController().isActive()) {
xMouseSmoother.reset();
yMouseSmoother.reset();
pitchSmoother.reset();
@@ -156,55 +148,12 @@ public abstract class MouseHandlerMixin {
return returnValue;
}
var vehicleController = controllableVehicle.getOrientationController();
var lookaroundButtonPressed = Minecraft.getInstance().mouseHandler.isMiddlePressed();
Pair<Boolean, Boolean> mouseShouldModifyDriver = ((PokemonEntity) vehicle).ifRidingAvailableSupply(new Pair(false, false), (behaviour, settings, state) -> {
return behaviour.mouseModifiesDriverRotation(
settings,
state,
(PokemonEntity) vehicle
);
});
//-------------------------------------------------------------------------
// If looking around then modify the players orientation instead and zero the mouse deltas so the mouse doesn't
// affect the vehicle orientation when looking around.
// If the client is a passenger it will modify these rotations and return
//-------------------------------------------------------------------------
if (lookaroundButtonPressed || player.getControlledVehicle() == null) {
// Modify driver rotations
cobblemon$setDriverRotations(player, cursorDeltaX, cursorDeltaY);
// If the player/passenger is not the driver then return and ignore all vehicle rotation logic
if (player.getControlledVehicle() == null) return false;
// Set to zero so that vehicle rotations don't also get affected by movement.
cursorDeltaX = 0.0;
cursorDeltaY = 0.0;
} else if (mouseShouldModifyDriver.getFirst()) { // If not freelooking then check modification per mouse input dimension
cobblemon$setDriverRotations(player, cursorDeltaX, 0.0);
cursorDeltaX = 0.0;
} else if (mouseShouldModifyDriver.getSecond()) {
cobblemon$setDriverRotations(player, 0.0, cursorDeltaY);
cursorDeltaY = 0.0;
}
//-------------------------------------------------------------------------
// Smoothly return driver rotations to zero when needed
//-------------------------------------------------------------------------
var playerRotater = (RidePassenger)player;
if( !lookaroundButtonPressed && !mouseShouldModifyDriver.getFirst()){
playerRotater.cobblemon$setRideYRot(lerp(min(5.0f * (float)movementTime, 1.0f), Mth.wrapDegrees(playerRotater.cobblemon$getRideYRot()), 0.0f));
}
if( !lookaroundButtonPressed && !mouseShouldModifyDriver.getSecond()) {
playerRotater.cobblemon$setRideXRot(lerp(min(5.0f * (float)movementTime, 1.0f), Mth.wrapDegrees(playerRotater.cobblemon$getRideXRot()), 0.0f));
}
// Send mouse input to be interpreted into rotation deltas by the ride controller
// Send mouse input to be interpreted into rotation
// deltas by the ride controller
Vec3 angVecMouse = cobblemon$getRideMouseRotation(cursorDeltaX, cursorDeltaY, movementTime);
// Perform Rotation using mouse influenced rotation deltas.
vehicleController.rotate(
controllable.getOrientationController().rotate(
(float) angVecMouse.x,
(float) angVecMouse.y,
(float) angVecMouse.z
@@ -214,16 +163,17 @@ public abstract class MouseHandlerMixin {
var angRot = cobblemon$getAngularVelocity(movementTime);
// Apply smoothing if requested by the controller.
// This Might be best if done by the controller itself?
if(cobblemon$shouldUseAngVelSmoothing()) {
var yaw = yawSmoother.getNewDeltaValue(angRot.x * 0.5f, d);
var pitch = pitchSmoother.getNewDeltaValue(angRot.y * 0.5f, d);
var roll = rollSmoother.getNewDeltaValue(angRot.z * 0.5f, d);
vehicleController.rotate((float) yaw, (float) pitch, (float) roll);
controllable.getOrientationController().rotate((float) yaw, (float) pitch, (float) roll);
}
// Otherwise simply apply the smoothing
else {
vehicleController.rotate((float) (angRot.x * 10 * d), (float) (angRot.y * 10 * d), (float) (angRot.z * 10 * d));
controllable.getOrientationController().rotate((float) (angRot.x * 10 * d), (float) (angRot.y * 10 * d), (float) (angRot.z * 10 * d));
}
return false;
}
@@ -236,11 +186,10 @@ public abstract class MouseHandlerMixin {
@Inject(method = "handleAccumulatedMovement", at = @At("TAIL"))
private void cobblemon$maintainMovementWhenInScreens(CallbackInfo ci) {
if (minecraft.player == null) return;
if (!(minecraft.player.getVehicle() instanceof OrientationControllable vehicleControllable)) return;
if (!(vehicleControllable.getOrientationController() instanceof OrientationController vehicleController)) return;
if (vehicleController.getOrientation() == null) return;
if (!(minecraft.player instanceof OrientationControllable controllable)) return;
if (!controllable.getOrientationController().isActive()) return;
if (minecraft.isPaused()) return;
if (this.isMouseGrabbed()) return; // return if turnPlayer() has already run this pass
if (isMouseGrabbed()) return;
this.turnPlayer(cobblemon$timeDelta);
}
@@ -302,20 +251,6 @@ public abstract class MouseHandlerMixin {
return Math.pow(sensitivity, 3);
}
@Unique
private void cobblemon$setDriverRotations(LocalPlayer player, double cursorDeltaX, double cursorDeltaY) {
// the vanilla turn() method redone so that it uses rideRotations.
float f = (float)cursorDeltaY * 0.15F; // Note: turn() uses xRot for pitch (up/down)
float g = (float)cursorDeltaX * 0.15F; // Note: turn() uses yRot for yaw (left/right)
var playerRotater = (RidePassenger)player;
playerRotater.cobblemon$setRideXRot(playerRotater.cobblemon$getRideXRot() + f);
playerRotater.cobblemon$setRideYRot(playerRotater.cobblemon$getRideYRot() + g);
playerRotater.cobblemon$setRideXRot(Mth.clamp(playerRotater.cobblemon$getRideXRot(), -90.0F, 90.0F));
if (player.getVehicle() != null) {
player.getVehicle().onPassengerTurned(player);
}
}
@Unique
private boolean cobblemon$shouldUseAngVelSmoothing() {
var player = minecraft.player;
@@ -8,24 +8,20 @@
package com.cobblemon.mod.common.mixin.entity;
import com.cobblemon.mod.common.OrientationControllable;
import com.cobblemon.mod.common.duck.RidePassenger;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(Entity.class)
public abstract class EntityMixin {
@@ -61,46 +57,5 @@ public abstract class EntityMixin {
return original.call(entity);
}
@Inject(
method = "getEyePosition(F)Lnet/minecraft/world/phys/Vec3;",
at = @At("HEAD"),
cancellable = true
)
private void cobblemon$modifyEyePosition_partial(float partialTicks, CallbackInfoReturnable<Vec3> cir) {
var entity = (Entity)(Object)this;
if (!(entity instanceof Player player)) return;
if (!(player instanceof RidePassenger ridePassenger)) return;
if (!(player.getVehicle() instanceof OrientationControllable vehicle)) return;
var vehicleController = vehicle.getOrientationController();
if (vehicleController == null) return;
// If Orientation is currently being controlled then use the stored eye position from camera positioning.
if (vehicleController.getActive() && vehicleController.getOrientation() != null) {
// TODO: Determine if it needs partialtick involved.
Vec3 customEyePos = ridePassenger.cobblemon$getRideEyePos();
cir.setReturnValue(customEyePos);
}
}
@Inject(
method = "getEyePosition()Lnet/minecraft/world/phys/Vec3;",
at = @At("HEAD"),
cancellable = true
)
private void cobblemon$modifyEyePosition_noPartial(CallbackInfoReturnable<Vec3> cir) {
var entity = (Entity)(Object)this;
if (!(entity instanceof Player player)) return;
if (!(player instanceof RidePassenger ridePassenger)) return;
if (!(player.getVehicle() instanceof OrientationControllable vehicle)) return;
var vehicleController = vehicle.getOrientationController();
if (vehicleController == null) return;
// If Orientation is currently being controlled then use the stored eye position from camera positioning.
if (vehicleController.getActive() && vehicleController.getOrientation() != null) {
// TODO: Determine if it needs partialtick involved.
Vec3 customEyePos = ridePassenger.cobblemon$getRideEyePos();
cir.setReturnValue(customEyePos);
}
}
}
@@ -696,7 +696,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val SUPERB_REMEDY = compostableItem("superb_remedy", RemedyItem(RemedyItem.SUPERB), 0.85f)
@JvmField
val MOOMOO_MILK = create("moomoo_milk", CobblemonItem(Item.Properties().craftRemainder(Items.GLASS_BOTTLE).stacksTo((16))))
val MOOMOO_MILK = create("moomoo_milk", MoomooMilk(Item.Properties().craftRemainder(Items.GLASS_BOTTLE).stacksTo((16))))
@JvmField
val POTION = create("potion", PotionItem(PotionType.POTION))
@@ -179,7 +179,6 @@ import com.cobblemon.mod.common.net.messages.server.pokemon.update.SetNicknamePa
import com.cobblemon.mod.common.net.messages.server.pokemon.update.evolution.AcceptEvolutionPacket
import com.cobblemon.mod.common.net.messages.server.riding.DismountPokemonPacket
import com.cobblemon.mod.common.net.messages.server.riding.ServerboundUpdateDriverInputPacket
import com.cobblemon.mod.common.net.messages.server.riding.ServerboundUpdateRiderRotationPacket
import com.cobblemon.mod.common.net.messages.server.starter.RequestStarterScreenPacket
import com.cobblemon.mod.common.net.messages.server.storage.SwapPCPartyPokemonPacket
import com.cobblemon.mod.common.net.messages.server.storage.party.MovePartyPokemonPacket
@@ -232,7 +231,6 @@ import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetMarkingsHan
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetNicknameHandler
import com.cobblemon.mod.common.net.serverhandling.riding.DismountPokemonPacketHandler
import com.cobblemon.mod.common.net.serverhandling.riding.DriverInputPacketHandler
import com.cobblemon.mod.common.net.serverhandling.riding.ServerboundUpdateRiderRotationHandler
import com.cobblemon.mod.common.net.serverhandling.starter.RequestStarterScreenHandler
import com.cobblemon.mod.common.net.serverhandling.starter.SelectStarterPacketHandler
import com.cobblemon.mod.common.net.serverhandling.storage.BenchMoveHandler
@@ -586,7 +584,6 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(ServerboundUpdateRidingSettingsPacket.ID, ServerboundUpdateRidingSettingsPacket::decode, ServerboundUpdateRidingSettingsHandler))
list.add(PacketRegisterInfo(DismountPokemonPacket.ID, DismountPokemonPacket::decode, DismountPokemonPacketHandler))
list.add(PacketRegisterInfo(ServerboundUpdateDriverInputPacket.ID, ServerboundUpdateDriverInputPacket::decode, DriverInputPacketHandler))
list.add(PacketRegisterInfo(ServerboundUpdateRiderRotationPacket.ID, ServerboundUpdateRiderRotationPacket::decode, ServerboundUpdateRiderRotationHandler))
// Cooking
list.add(PacketRegisterInfo(ToggleCookingPotLidPacket.ID, ToggleCookingPotLidPacket::decode, ToggleCookingPotLidHandler))
@@ -26,8 +26,6 @@ object CobblemonPoiTypes: PlatformRegistry<Registry<PoiType>, ResourceKey<Regist
@JvmField
val NURSE_KEY = createKey("nurse", CobblemonBlocks.HEALING_MACHINE, 1, 1)
@JvmField
val POKE_SNACK_KEY = createKey("poke_snack", CobblemonBlocks.POKE_SNACK, 0, 1)
@JvmField
val SACCHARINE_LOG_SLATHERED_KEY = createKey("saccharine_log_slathered", CobblemonBlocks.SACCHARINE_LOG_SLATHERED, 0, 1)
// TODO after 1.7
// @JvmField
@@ -26,7 +26,6 @@ import com.cobblemon.mod.common.api.riding.behaviour.types.land.VehicleSettings
import com.cobblemon.mod.common.api.riding.behaviour.types.liquid.BoatSettings
import com.cobblemon.mod.common.api.riding.behaviour.types.liquid.BurstSettings
import com.cobblemon.mod.common.api.riding.behaviour.types.liquid.DolphinSettings
import com.cobblemon.mod.common.api.riding.behaviour.types.liquid.SubmarineSettings
import com.cobblemon.mod.common.net.messages.client.data.RideSettingsSyncPacket
import com.cobblemon.mod.common.util.adapters.ExpressionAdapter
import com.cobblemon.mod.common.util.adapters.ExpressionLikeAdapter
@@ -64,7 +63,6 @@ object CobblemonRideSettings : DataRegistry {
var boat = BoatSettings()
var burst = BurstSettings()
var dolphin = DolphinSettings()
var submarine = SubmarineSettings()
override fun sync(player: ServerPlayer) {
player.sendPacket(
@@ -80,8 +78,7 @@ object CobblemonRideSettings : DataRegistry {
vehicle = vehicle,
boat = boat,
burst = burst,
dolphin = dolphin,
submarine = submarine
dolphin = dolphin
)
)
}
@@ -99,7 +96,6 @@ object CobblemonRideSettings : DataRegistry {
boat = loadStyle(manager, "boat", BoatSettings::class.java)
burst = loadStyle(manager, "burst", BurstSettings::class.java)
dolphin = loadStyle(manager, "dolphin", DolphinSettings::class.java)
submarine = loadStyle(manager, "submarine", SubmarineSettings::class.java)
}
private fun <T : RidingBehaviourSettings> loadStyle(manager: ResourceManager, name: String, clazz: Class<T>): T {
@@ -22,18 +22,19 @@ class ReelInPokemonContext(val species : ResourceLocation, val baitId: ResourceL
class ReelInPokemonCriterionCondition(
playerCtx: Optional<ContextAwarePredicate>,
val species: String,
val baitId: String
val baitId: String // also allows "cobblemon:empty_bait" to indicate that no bait is allowed for this criterion to match
) : SimpleCriterionCondition<ReelInPokemonContext>(playerCtx) {
companion object {
val CODEC: Codec<ReelInPokemonCriterionCondition> = RecordCodecBuilder.create { it.group(
EntityPredicate.ADVANCEMENT_CODEC.optionalFieldOf("player").forGetter { it.playerCtx },
Codec.STRING.optionalFieldOf("species", "any").forGetter { it.species },
Codec.STRING.optionalFieldOf("baitId", "empty_bait").forGetter { it.baitId }
).apply(it, { playerCtx, pokemonId, baitId -> ReelInPokemonCriterionCondition(playerCtx, pokemonId, baitId.ifEmpty { "empty_bait" }) }) }
Codec.STRING.optionalFieldOf("baitId", "any").forGetter { it.baitId }
).apply(it, { playerCtx, pokemonId, baitId -> ReelInPokemonCriterionCondition(playerCtx, pokemonId, baitId) }) }
}
override fun matches(player: ServerPlayer, context: ReelInPokemonContext): Boolean {
return (context.species == this.species.asIdentifierDefaultingNamespace() || this.species == "any") && (context.baitId == this.baitId.asIdentifierDefaultingNamespace() || this.baitId == "empty_bait")
return (context.species == this.species.asIdentifierDefaultingNamespace() || this.species == "any")
&& (context.baitId == this.baitId.asIdentifierDefaultingNamespace() || this.baitId == "any")
}
}
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.ai
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.behavior.Behavior
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.entity.ai.memory.MemoryStatus
import net.minecraft.world.entity.schedule.Activity
/**
* A [Behavior] subclass that represents tasks that check that the entity is still in one
* of the activities that this task is part of. If the entity is no longer doing any of those
* activities, the task will be interrupted and you can handle any cleanup in [onInterrupted].
*
* @author Hiroku
* @since October 17th, 2025
*/
abstract class ActivityAwareBehavior<E : LivingEntity> : Behavior<E> {
constructor(entryCondition: Map<MemoryModuleType<*>, MemoryStatus>) : super(entryCondition)
constructor(entryCondition: Map<MemoryModuleType<*>, MemoryStatus>, duration: Int) : super(entryCondition, duration)
constructor(entryCondition: Map<MemoryModuleType<*>, MemoryStatus>, minDuration: Int, maxDuration: Int) : super(entryCondition, minDuration, maxDuration)
val activities: MutableSet<Activity> = mutableSetOf()
fun isStillDoingActivity(entity: E): Boolean {
return activities.any(entity.brain::isActive)
}
override fun canStillUse(level: ServerLevel, entity: E, gameTime: Long) = isStillDoingActivity(entity)
open fun onInterrupted(level: ServerLevel, entity: E, gameTime: Long) {
// No-op
}
override fun stop(level: ServerLevel, entity: E, gameTime: Long) {
super.stop(level, entity, gameTime)
if (!isStillDoingActivity(entity)) {
onInterrupted(level, entity, gameTime)
}
}
}
@@ -11,7 +11,6 @@ package com.cobblemon.mod.common.api.ai
import com.google.common.collect.ImmutableList
import com.mojang.datafixers.util.Pair
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.Mob
import net.minecraft.world.entity.ai.behavior.BehaviorControl
import net.minecraft.world.entity.schedule.Activity
@@ -23,6 +22,13 @@ class ActivityConfigurationContext(val activity: Activity) {
}
fun apply(entity: LivingEntity) {
tasks.forEach {
val activityAwareBehavior = it.second as? ActivityAwareBehavior<*>
?: (it.second as? WrapperLivingEntityTask<*>)?.task as? ActivityAwareBehavior
?: return@forEach
activityAwareBehavior.activities += activity
}
entity.brain.addActivity(activity, ImmutableList.copyOf(tasks))
}
}
@@ -23,7 +23,7 @@ import net.minecraft.world.entity.schedule.Activity
class ActivityChangeTaskConfig : SingleTaskConfig {
val activity: Activity = Activity.IDLE
val condition: ExpressionLike = "false".asExpressionLike()
val condition: ExpressionLike = "true".asExpressionLike()
override fun getVariables(entity: LivingEntity, behaviourConfigurationContext: BehaviourConfigurationContext): List<MoLangConfigVariable> = emptyList()
@@ -12,7 +12,6 @@ import com.bedrockk.molang.runtime.MoLangEnvironment
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.MoParams
import com.bedrockk.molang.runtime.struct.ArrayStruct
import com.bedrockk.molang.runtime.struct.MoStruct
import com.bedrockk.molang.runtime.struct.QueryStruct
import com.bedrockk.molang.runtime.struct.VariableStruct
import com.bedrockk.molang.runtime.value.DoubleValue
@@ -40,7 +39,6 @@ import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import com.cobblemon.mod.common.api.moves.animations.ActionEffects
import com.cobblemon.mod.common.api.moves.animations.NPCProvider
import com.cobblemon.mod.common.api.moves.animations.TargetsProvider
import com.cobblemon.mod.common.api.npc.NPCClass
import com.cobblemon.mod.common.api.npc.NPCClasses
import com.cobblemon.mod.common.api.npc.configuration.interaction.DialogueNPCInteractionConfiguration
import com.cobblemon.mod.common.api.npc.configuration.interaction.ScriptNPCInteractionConfiguration
@@ -57,7 +55,6 @@ import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
import com.cobblemon.mod.common.api.pokemon.experience.SidemodExperienceSource
import com.cobblemon.mod.common.api.pokemon.moves.LearnsetQuery
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.riding.Rideable
import com.cobblemon.mod.common.api.riding.stats.RidingStat
import com.cobblemon.mod.common.api.scheduling.ClientTaskTracker
import com.cobblemon.mod.common.api.scheduling.Schedulable
@@ -137,7 +134,6 @@ import net.minecraft.world.entity.LightningBolt
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.Mob
import net.minecraft.world.entity.PathfinderMob
import net.minecraft.world.entity.PlayerRideable
import net.minecraft.world.entity.TamableAnimal
import net.minecraft.world.entity.ai.behavior.BlockPosTracker
import net.minecraft.world.entity.ai.memory.MemoryModuleType
@@ -145,7 +141,6 @@ import net.minecraft.world.entity.ai.memory.MemoryStatus
import net.minecraft.world.entity.ai.memory.WalkTarget
import net.minecraft.world.entity.animal.Animal
import net.minecraft.world.entity.item.ItemEntity
import net.minecraft.world.entity.monster.Enemy
import net.minecraft.world.entity.monster.Monster
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
@@ -160,9 +155,6 @@ import net.minecraft.world.level.levelgen.Heightmap
import net.minecraft.world.level.pathfinder.PathType
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
import kotlin.text.get
import kotlin.text.toInt
import kotlin.toString
/**
* Holds a bunch of useful MoLang trickery that can be used or extended in API
@@ -1185,16 +1177,28 @@ object MoLangFunctions {
return@put DoubleValue.ONE
}
map.put("has_memory_value") { params ->
val memoryTypes = params.params.map {
it.asString().asIdentifierDefaultingNamespace()
}.map(BuiltInRegistries.MEMORY_MODULE_TYPE::get)
return@put DoubleValue(memoryTypes.all { entity.brain.checkMemory(it, MemoryStatus.VALUE_PRESENT) })
for (param in params.params) {
if (!entity.hasMemoryFromString(param.asString())) {
return@put DoubleValue.ZERO
}
}
return@put DoubleValue.ONE
}
map.put("lacks_memory_value") { params ->
val memoryTypes = params.params.map {
it.asString().asIdentifierDefaultingNamespace()
}.map(BuiltInRegistries.MEMORY_MODULE_TYPE::get)
return@put DoubleValue(memoryTypes.none { entity.brain.checkMemory(it, MemoryStatus.VALUE_PRESENT) })
for (param in params.params) {
if (entity.hasMemoryFromString(param.asString())) {
return@put DoubleValue.ZERO
}
}
return@put DoubleValue.ONE
}
map.put("is_doing_activity") { params ->
for (param in params.params) {
if (entity.isDoingActivityFromString(param.asString())) {
return@put DoubleValue.ONE
}
}
return@put DoubleValue.ZERO
}
map.put("can_see") { params ->
val target = params.get<MoValue>(0)
@@ -1252,15 +1256,6 @@ object MoLangFunctions {
map.put("in_battle") { DoubleValue(npc.isInBattle()) }
map.put("battles") { ArrayStruct(npc.battleIds.mapNotNull { BattleRegistry.getBattle(it)?.struct }.mapIndexed { index, value -> "$index" to value }.toMap()) }
map.put("stop_battles") { _ -> npc.battleIds.forEach { BattleRegistry.getBattle(it)?.stop() } }
map.put("is_doing_activity") { params ->
val identifiers = params.params.map { it.asString().asIdentifierDefaultingNamespace(namespace = "minecraft") }
val activities = identifiers.mapNotNull { identifier -> BuiltInRegistries.ACTIVITY.get(identifier) }
if (activities.isNotEmpty()) {
return@put DoubleValue(activities.any { activity -> npc.brain.isActive(activity) })
} else {
return@put DoubleValue.ZERO
}
}
map.put("run_script_on_client") { params ->
val world = npc.level()
if (world is ServerLevel) {
@@ -1905,6 +1900,9 @@ object MoLangFunctions {
map.put("is_holding_item") { DoubleValue(!pokemonEntity.entityData.get(PokemonEntity.SHOWN_HELD_ITEM).let {
it.isEmpty || it.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) || it.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS)
}) }
map.put("riding_style") {
StringValue(pokemonEntity.ifRidingAvailableSupply("") { behaviour, settings, state -> behaviour.getRidingStyle(settings, state).name })
}
map.put("is_wearing_hat") { DoubleValue(pokemonEntity.entityData.get(PokemonEntity.SHOWN_HELD_ITEM).`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS)) }
map.put("is_wearing_face") { DoubleValue(pokemonEntity.entityData.get(PokemonEntity.SHOWN_HELD_ITEM).`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS)) }
map.put("is_pastured") {
@@ -806,8 +806,8 @@ open class PokemonProperties {
return PokemonProperties().loadFromJSON(this.saveToJSON())
}
// If the config value is at least 1, then do 1/x and use that as the property chance
private fun Float.checkRate(): Boolean = if (this >= 1) (Random.Default.nextFloat() < 1 / this) else Random.Default.nextFloat() < this
// If the config value is at least 0, then do 1/x and use that as the property chance
private fun Float.checkRate(): Boolean = this > 0 && (Random.nextFloat() < 1 / this)
/**
* Attempts to find an ability by ID and resolve if it should be forced or if it's legal for the given form.
@@ -11,7 +11,7 @@ package com.cobblemon.mod.common.api.pokemon.aspect
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.api.ai.config.task.PollinateFlowerTaskConfig
import com.cobblemon.mod.common.api.spawning.influence.SaccharineLogSlatheredInfluence
import com.cobblemon.mod.common.api.spawning.prospecting.LurePokeSnackDetector
import com.cobblemon.mod.common.block.entity.PokeSnackBlockEntity
import com.cobblemon.mod.common.client.entity.PokemonClientDelegate
import net.minecraft.core.particles.BlockParticleOption
import net.minecraft.core.particles.ParticleOptions
@@ -23,7 +23,7 @@ import net.minecraft.resources.ResourceLocation
*/
val aspectParticleMap: Map<String, ParticleData> = mapOf(
SaccharineLogSlatheredInfluence.HONEY_DRENCHED_ASPECT to ParticleData.MinecraftParticle(ParticleTypes.FALLING_HONEY, 0.075, 1),
LurePokeSnackDetector.POKE_SNACK_CRUMBED_ASPECT to ParticleData.MinecraftParticle(
PokeSnackBlockEntity.POKE_SNACK_CRUMBED_ASPECT to ParticleData.MinecraftParticle(
BlockParticleOption(
ParticleTypes.BLOCK,
CobblemonBlocks.POKE_SNACK.defaultBlockState()
@@ -103,18 +103,6 @@ interface RidingBehaviour<Settings : RidingBehaviourSettings, State : RidingBeha
*/
fun damageOnCollision(settings: Settings, state: State, vehicle: PokemonEntity, impactVec: Vec3): Boolean = false
/**
* Called in [com.cobblemon.mod.common.client.render.pokemon.PokemonRenderer] to allow the controller to set rotations
* through code using partialTicks and right before rendering.
* //TODO: This needs to be further refined. Maybe only the orientationController needs affected? Maybe the rotations set here don't need to be applied to the controller/camera
*/
fun applyRenderRotation(settings: Settings, state: State, vehicle: PokemonEntity, partialTicks: Float) {}
/**
* Determines if the pair of mouse inputs Pair(mouseX, mouseY) get used to modify the driverRotations or the vehicle rotations themselves.
* Driver rotations are the rotations used by passengers or a driver that is freelooking. This does not override the freelook button.
*/
fun mouseModifiesDriverRotation(settings: Settings, state: State, vehicle: PokemonEntity): Pair<Boolean, Boolean> = Pair(false, false)
/**
* Internal helpers to help behaviour specific calculations
@@ -16,7 +16,6 @@ import com.cobblemon.mod.common.api.riding.behaviour.types.land.MinekartBehaviou
import com.cobblemon.mod.common.api.riding.behaviour.types.liquid.BoatBehaviour
import com.cobblemon.mod.common.api.riding.behaviour.types.liquid.BurstBehaviour
import com.cobblemon.mod.common.api.riding.behaviour.types.liquid.DolphinBehaviour
import com.cobblemon.mod.common.api.riding.behaviour.types.liquid.SubmarineBehaviour
import net.minecraft.resources.ResourceLocation
object RidingBehaviours {
@@ -35,7 +34,6 @@ object RidingBehaviours {
register(MinekartBehaviour.KEY, MinekartBehaviour())
register(HoverBehaviour.KEY, HoverBehaviour())
register(RocketBehaviour.KEY, RocketBehaviour())
register(SubmarineBehaviour.KEY, SubmarineBehaviour())
register(CompositeBehaviour.KEY, CompositeBehaviour())
}
@@ -34,7 +34,6 @@ import net.minecraft.world.entity.player.Player
import net.minecraft.world.phys.Vec2
import net.minecraft.world.phys.Vec3
import net.minecraft.world.phys.shapes.Shapes
import org.joml.Vector3f
import kotlin.math.*
class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
@@ -89,8 +88,7 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
vehicle: PokemonEntity,
driver: Player,
) {
// Also checks stamina
val controller = (vehicle as OrientationControllable).orientationController
val controller = (driver as OrientationControllable).orientationController
if (cos(controller.roll.toRadians()) < 0.0) {
val upsideDownRate = 1.0 / (2.0 * 20.0) // 2 seconds to max out upsideDown force
state.currUpsideDownForce.set( min(1.0, state.currUpsideDownForce.get() + upsideDownRate * abs(cos(controller.roll.toRadians()))))
@@ -111,10 +109,6 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
val stam = state.stamina.get()
val controller = (driver as OrientationControllable).orientationController
if (vehicle.runtime.resolveBoolean(settings.infiniteStamina ?: globalBird.infiniteStamina!!)) {
return
}
// Grab the boost time in seconds and convert to ticks. Then calculate the drain rate as inversely
// proportional to the number of ticks of boost thus making a full boost take x ticks
// in short: "Stamina drains slower at higher values and also replenishes slower"
@@ -141,7 +135,7 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
vehicle: PokemonEntity,
driver: LivingEntity
): Vec2 {
return Vec2.ZERO//Vec2(driver.xRot * 0.5f, driver.yRot)
return Vec2(driver.xRot * 0.5f, driver.yRot)
}
override fun velocity(
@@ -151,26 +145,29 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
driver: Player,
input: Vec3
): Vec3 {
val controller = (vehicle as? OrientationControllable)?.orientationController
if (controller == null || controller.orientation == null) return Vec3.ZERO
var upForce = 0.0
var forwardForce = 0.0
//Perform ride velocity update
calculateRideSpaceVel(settings, state, vehicle, driver)
//Translate ride space velocity to world space velocity.
val controller = (driver as? OrientationControllable)?.orientationController
if (controller != null) {
//Need to deadzone this when straight up or down
upForce += -1.0 * sin(Math.toRadians(controller.pitch.toDouble())) * state.rideVelocity.get().z
forwardForce += cos(Math.toRadians(controller.pitch.toDouble())) * state.rideVelocity.get().z
upForce += cos(Math.toRadians(controller.pitch.toDouble())) * state.rideVelocity.get().y
forwardForce += sin(Math.toRadians(controller.pitch.toDouble())) * state.rideVelocity.get().y
}
// The downward force used to encourage players to stop flying upside down.
val extraDownwardForce = state.currUpsideDownForce.get().pow(2) * -0.3 // 6 blocks a second downward
// Convert the local velocity vector into a world vector
val localVelVec = Vector3f(
state.rideVelocity.get().x.toFloat(),
(state.rideVelocity.get().y).toFloat(),
state.rideVelocity.get().z.toFloat() * -1.0f // Flip the z axis to make this left handed
)
// Unrotate preemptively as this vector gets rotatee later down the line in MC logic.
var worldVelVec = localVelVec.mul(controller.orientation).toVec3d().yRot(vehicle.yRot.toRadians())
worldVelVec = worldVelVec.add(0.0, extraDownwardForce, 0.0) // Add the stamina depletion force to bring the ride down.
return worldVelVec
val velocity = Vec3(state.rideVelocity.get().x , upForce + extraDownwardForce, forwardForce)
return velocity
}
/*
@@ -209,7 +206,7 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
activeInput = true
}
val controller = (vehicle as? OrientationControllable)?.orientationController
val controller = (driver as? OrientationControllable)?.orientationController
if (controller != null) {
//Base glide speed change on current pitch of the ride.
glideSpeedChange = sin(Math.toRadians(controller.pitch.toDouble()))
@@ -285,13 +282,15 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
driver: Player,
deltaTime: Double
): Vec3 {
if (vehicle !is OrientationControllable) return Vec3.ZERO
val controller = (vehicle as OrientationControllable).orientationController
if (Cobblemon.config.disableRoll) return Vec3.ZERO
if (driver !is OrientationControllable) return Vec3.ZERO
val controller = (driver as OrientationControllable).orientationController
val handling = vehicle.runtime.resolveDouble(settings.handlingExpr ?: globalBird.handlingExpr!!)
val topSpeed = vehicle.runtime.resolveDouble(settings.speedExpr ?: globalBird.speedExpr!!) / 20.0
val yawDeltaDeg = deltaTime * handling * sin(Math.toRadians(controller.roll.toDouble()))
val trueYawDelt = yawDeltaDeg * abs(cos(Math.toRadians(controller.pitch.toDouble()))*2).coerceIn(-1.0, 1.0)
val yawDeltaDeg = deltaTime * handling * sin(Math.toRadians(controller.roll.toDouble())) //* sin(Math.toRadians(controller.roll.toDouble())).sign
val trueYawDelt = yawDeltaDeg * abs(cos(Math.toRadians(controller.pitch.toDouble())))// * sqrt(RidingBehaviour.scaleToRange(state.rideVelocity.get().length(), 0.0, topSpeed))
// Dampen yaw when upside down
val yawDampen = (1 - abs(min(cos(controller.roll.toRadians()),0.0f)).pow(2))
@@ -300,13 +299,10 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
// Correct orientation when at low speeds
correctOrientation(settings, state, vehicle, controller, deltaTime)
val pCorr = (state.currPitchCorrectionForce.get() * deltaTime).toFloat()
val rCorr = (state.currRollCorrectionForce.get() * deltaTime).toFloat()
controller.applyGlobalPitch(pCorr)
controller.rotateRoll(rCorr)
controller.applyGlobalPitch(state.currPitchCorrectionForce.get().toRadians())
//yaw, pitch, roll
return Vec3(0.0, 0.0, 0.0)
return Vec3(0.0, 0.0, state.currRollCorrectionForce.get())
}
private fun correctOrientation(
@@ -317,48 +313,42 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
deltaTime: Double
) {
// Calculate correcting roll force.
// Mult by cos(pitch) to ensure that it doesn't roll correct at when diving or climbing
val rollCorrectionTimer = vehicle.runtime.resolveDouble(settings.timeToRollCorrect ?: globalBird.timeToRollCorrect!!)
// If the ride is moving then have the correction rate be an equal proportion of:
// - how rolled are you
// - how long has it been since last mouse input (higher is stronger correction)
// Also the correction is proportional as a whole to how NOT pitched are you so you don't spin endlessly when looking up or down
val howRolledAmI = sqrt(abs((cos(controller.roll.toRadians()) - 1.0f) * 0.5f)) // sqrt so it trends towards one pretty quickly
val maxRollCorrectionRate =
if (state.rideVelocity.get().length() < 0.2) 45.0f
else 45.0f * cos(controller.pitch.toRadians()).pow(2) * (
howRolledAmI*1.0f +
(state.noInputTimeRoll.get() - rollCorrectionTimer).coerceIn(0.0, 1.0).pow(2).toFloat()*0.5f
)
val maxRollForce = maxRollCorrectionRate / 40.0f
val rollArrivalDeg = 35.0f
val desiredRoll = 0.0f
val maxRollCorrectionRate = if (state.rideVelocity.get().length() < 0.1) 15.0f else 8.0f * (cos(controller.pitch.toRadians())).pow(2) * ((state.noInputTimeRoll.get() - rollCorrectionTimer)).coerceIn(0.0, 1.0).toFloat()
val maxRollForce = maxRollCorrectionRate / 40.0
val rollArrivalDeg = 90.0
if(state.rideVelocity.get().length() < 0.1 || state.noInputTimeRoll.get() > rollCorrectionTimer) {
val arrivalInfluence = (min(rollArrivalDeg,abs(controller.roll).toDouble()) / rollArrivalDeg)
val desiredRollForce = (0 - controller.roll).sign * maxRollCorrectionRate * arrivalInfluence
var steeredRollForce = desiredRollForce - state.currRollCorrectionForce.get()
// Calculate the signed error (how far and in what direction)
val rollError = Mth.wrapDegrees(desiredRoll.angleDifference(controller.roll))
// Use the absolute error to calculate the magnitude of the influence
val influenceMagnitude = (min(rollArrivalDeg, abs(rollError)) / rollArrivalDeg)
val desiredRollForce = maxRollCorrectionRate * influenceMagnitude * -rollError.sign
var steeredRollForce = desiredRollForce - state.currRollCorrectionForce.get()
// Ensure the 'steering' correction force doesn't exceed our maxForce
if (abs(steeredRollForce) > maxRollForce) {
steeredRollForce = steeredRollForce.sign * maxRollForce
// Ensure the 'steering' correction force doesn't exceed our maxForce
if (abs(steeredRollForce) > maxRollForce) {
steeredRollForce = steeredRollForce.sign * maxRollForce
}
// Apply the 'steered' force (its really not a steering force I need a better name)
state.currRollCorrectionForce.set(
state.currRollCorrectionForce.get() + steeredRollForce
)
} else {
state.currRollCorrectionForce.set(
//reduce rollCorrection gradually
lerp(state.currRollCorrectionForce.get(), 0.0, deltaTime * 0.96)
)
}
// Apply the 'steered' force (its really not a steering force I need a better name)
state.currRollCorrectionForce.set(
state.currRollCorrectionForce.get() + steeredRollForce
)
//Calculate correcting pitch force.
val maxPitchCorrectionRate = if (state.currUpsideDownForce.get() != 0.0) 15.0f * state.currUpsideDownForce.get().toFloat() else 30.0f
val maxPitchForce = maxPitchCorrectionRate / 40.0f
val pitchArrivalDeg = 35.0f
val desiredPitch = if (state.currUpsideDownForce.get() != 0.0) 90.0f else 0.0f
val maxPitchCorrectionRate = if (state.currUpsideDownForce.get() != 0.0) 8f * state.currUpsideDownForce.get() else 15f
val maxPitchForce = maxPitchCorrectionRate.toFloat() / 40.0f
val pitchArrivalDeg = 45.0f
var desiredPitch = if (state.currUpsideDownForce.get() != 0.0) 90.0 else 0.0
if((state.rideVelocity.get().length() < 0.1) || state.currUpsideDownForce.get() != 0.0) {
val pitchErrror = Mth.wrapDegrees(desiredPitch.angleDifference(controller.pitch))
val arrivalInfluence = (min(pitchArrivalDeg,abs(pitchErrror)) / pitchArrivalDeg)
val desiredPitchForce = maxPitchCorrectionRate * arrivalInfluence * -pitchErrror.sign
var steeredPitchForce = desiredPitchForce - state.currPitchCorrectionForce.get()
if((state.rideVelocity.get().length() < 0.1 && controller.pitch < 0.0) || state.currUpsideDownForce.get() != 0.0) {
val arrivalInfluence = (min(pitchArrivalDeg,abs(Mth.wrapDegrees(desiredPitch.toFloat().angleDifference(controller.pitch)))) / pitchArrivalDeg)
val desiredPitchForce = maxPitchCorrectionRate.toFloat() * arrivalInfluence
var steeredPitchForce = (desiredPitchForce - state.currPitchCorrectionForce.get())
// Ensure the 'steering' correction force doesn't exceed our maxForce
if (abs(steeredPitchForce) > maxPitchForce) {
@@ -408,18 +398,80 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
state.noInputTimePitch.set(0.0)
}
return rollRotation(
settings,
state,
vehicle,
driver,
mouseY,
mouseX,
mouseYSmoother,
mouseXSmoother,
sensitivity,
deltaTime
return when {
Cobblemon.config.disableRoll -> noRollRotation(
settings,
state,
vehicle,
driver,
mouseY,
mouseX,
mouseYSmoother,
mouseXSmoother,
sensitivity,
deltaTime
)
else -> rollRotation(
settings,
state,
vehicle,
driver,
mouseY,
mouseX,
mouseYSmoother,
mouseXSmoother,
sensitivity,
deltaTime
)
}
}
fun noRollRotation(
settings: BirdSettings,
state: BirdState,
vehicle: PokemonEntity,
driver: Player,
mouseY: Double,
mouseX: Double,
mouseYSmoother: SmoothDouble,
mouseXSmoother: SmoothDouble,
sensitivity: Double,
deltaTime: Double
): Vec3 {
if (driver !is OrientationControllable) return Vec3.ZERO
val controller = (driver as OrientationControllable).orientationController
// Set roll to zero if transitioning to noroll config
controller.rotateRoll(controller.roll * -1.0f)
val handling = vehicle.runtime.resolveDouble(settings.handlingExpr ?: globalBird.handlingExpr!!)
val mouseInputMult = vehicle.runtime.resolveDouble(settings.mouseInputMult ?: globalBird.mouseInputMult!!)
//Smooth out mouse input.
val smoothingSpeed = 4.0
val mouseXc = ((mouseX).coerceIn(-60.0, 60.0) / 60.0) * mouseInputMult
val mouseYc = ((mouseY).coerceIn(-60.0, 60.0) / 60.0) * mouseInputMult
val xInput = mouseXSmoother.getNewDeltaValue(mouseXc * 0.1, deltaTime * smoothingSpeed);
val yInput = mouseYSmoother.getNewDeltaValue(mouseYc * 0.1, deltaTime * smoothingSpeed);
//Give the ability to yaw with x mouse input when at low speeds.
val yawForce = xInput * handling //* ( 1.0 - min(sqrt(RidingBehaviour.scaleToRange(state.rideVelocity.get().length(), 0.0, topSpeed)), 0.5))
//Apply yaw globally as we don't want roll or pitch changes due to local yaw when looking up or down.
controller.applyGlobalYaw(yawForce.toFloat() * abs(cos(controller.pitch.toRadians())).pow(2))
var pitchRot = yInput * handling
// Pitch up globally
if (abs(controller.pitch + pitchRot) >= 89.5 ) {
pitchRot = 0.0
mouseYSmoother.reset()
} else {
controller.applyGlobalPitch(pitchRot.toFloat() * -1.0f)
}
//yaw, pitch, roll
return Vec3.ZERO
}
fun rollRotation(
@@ -434,23 +486,21 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
sensitivity: Double,
deltaTime: Double
): Vec3 {
if (vehicle !is OrientationControllable) return Vec3.ZERO
val controller = (vehicle as OrientationControllable).orientationController
if (driver !is OrientationControllable) return Vec3.ZERO
val controller = (driver as OrientationControllable).orientationController
val topSpeed = vehicle.runtime.resolveDouble(settings.speedExpr ?: globalBird.speedExpr!!) / 20.0
val mouseInputMult = vehicle.runtime.resolveDouble(settings.mouseInputMult ?: globalBird.mouseInputMult!!)
// Applied to mouseX to prevent turning upside down when trying to roll hard.
val rollDegreeDebuff = if(mouseX.sign.toFloat() == controller.roll.sign) 1 - 0.9f*sqrt(abs(sin(controller.roll.toRadians())))*sqrt(abs(cos(controller.pitch))) else 1.0f
//Smooth out mouse input.
val smoothingSpeed = 2.0
val mouseXc = (mouseX).coerceIn(-45.0, 45.0) * mouseInputMult * rollDegreeDebuff
val mouseXc = (mouseX).coerceIn(-45.0, 45.0) * mouseInputMult
// When upside down apply a pitching force to nudge the player towards a dive. This reduces the 'weirdness'
// of being upside down
val rolledOverPitchCorrection = -8*sqrt(abs(min(cos(controller.roll.toRadians()),0.0f))) * (cos(controller.pitch.toRadians())).pow(2)
val mouseYc = ((mouseY).coerceIn(-45.0, 45.0) * mouseInputMult) + rolledOverPitchCorrection
val xInput = mouseXSmoother.getNewDeltaValue(mouseXc * 0.1, deltaTime * smoothingSpeed)
val yInput = mouseYSmoother.getNewDeltaValue(mouseYc * 0.1, deltaTime * smoothingSpeed)
val xInput = mouseXSmoother.getNewDeltaValue(mouseXc * 0.1, deltaTime * smoothingSpeed);
val yInput = mouseYSmoother.getNewDeltaValue(mouseYc * 0.1, deltaTime * smoothingSpeed);
val currSpeed = state.rideVelocity.get().length()
val currHorzSpeed = state.rideVelocity.get().horizontalDistance()
val hoverSpeed = 0.1
@@ -458,6 +508,8 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
// Yaw locally when at or below hoverSpeed but not when pitched up or rolled over
val globalYaw = xInput * (1 - sqrt(RidingBehaviour.scaleToRange(currHorzSpeed, hoverSpeed, topSpeed))).coerceIn(0.0, 1.0)
//xInput * 0.4 * cos(controller.roll.toRadians()).coerceIn(0.0f, 1.0f) * yawDampen
controller.applyGlobalYaw(globalYaw.toFloat())
// Pitch locally up or down depending upon a number of factors:
@@ -51,7 +51,7 @@ class HoverBehaviour : RidingBehaviour<HoverSettings, HoverState> {
val poseProvider = PoseProvider<HoverSettings, HoverState>(PoseType.STAND)
.with(PoseOption(PoseType.WALK) { _, state, _ ->
return@PoseOption abs(state.rideVelocity.get().length()) > 0.01
return@PoseOption abs(state.rideVelocity.get().z) > 0.2
})
override fun isActive(
@@ -161,7 +161,7 @@ class JetBehaviour : RidingBehaviour<JetSettings, JetState> {
driver: Player,
input: Vec3
): Vec3 {
val controller = (vehicle as? OrientationControllable)?.orientationController
val controller = (driver as? OrientationControllable)?.orientationController
if (controller == null || controller.orientation == null) return Vec3.ZERO
//Calculate ride space velocity
@@ -265,6 +265,8 @@ class JetBehaviour : RidingBehaviour<JetSettings, JetState> {
driver: Player,
deltaTime: Double
): Vec3 {
//don't yaw this way if
if (Cobblemon.config.disableRoll) return Vec3.ZERO
//Cap at a rate of 5fps so frame skips dont lead to huge jumps
val cappedDeltaTime = min(deltaTime, 0.2)
@@ -296,7 +298,8 @@ class JetBehaviour : RidingBehaviour<JetSettings, JetState> {
sensitivity: Double,
deltaTime: Double
): Vec3 {
return rollRotation(
return when {
Cobblemon.config.disableRoll -> noRollRotation(
settings,
state,
vehicle,
@@ -307,7 +310,78 @@ class JetBehaviour : RidingBehaviour<JetSettings, JetState> {
mouseXSmoother,
sensitivity,
deltaTime
)
)
else -> rollRotation(
settings,
state,
vehicle,
driver,
mouseY,
mouseX,
mouseYSmoother,
mouseXSmoother,
sensitivity,
deltaTime
)
}
}
fun noRollRotation(
settings: JetSettings,
state: JetState,
vehicle: PokemonEntity,
driver: Player,
mouseY: Double,
mouseX: Double,
mouseYSmoother: SmoothDouble,
mouseXSmoother: SmoothDouble,
sensitivity: Double,
deltaTime: Double
): Vec3 {
if (driver !is OrientationControllable) return Vec3.ZERO
val controller = (driver as OrientationControllable).orientationController
// Set roll to zero if transitioning to noroll config
controller.rotateRoll(controller.roll * -1.0f)
//Cap at a rate of 5fps so frame skips dont lead to huge jumps
val cappedDeltaTime = min(deltaTime, 0.2)
// Accumulate the mouse input
state.currMouseXForce.set((state.currMouseXForce.get() + (0.0015 * mouseX)).coerceIn(-1.0, 1.0))
state.currMouseYForce.set((state.currMouseYForce.get() + (0.0015 * mouseY)).coerceIn(-1.0, 1.0))
//Get handling in degrees per second
val handlingDebuff = if(state.stamina.get() == 0.0f) 0.5 else 1.0
var handling = vehicle.runtime.resolveDouble(settings.handlingExpr ?: globalJet.handlingExpr!!) * handlingDebuff
//convert it to delta time
handling *= (cappedDeltaTime)
var pitchRot = handling * state.currMouseYForce.get()
// Yaw
val yawRot = handling * 0.5 * state.currMouseXForce.get()
controller.applyGlobalYaw(yawRot.toFloat())
if (abs(controller.pitch + pitchRot) >= 89.5 ) {
pitchRot = 0.0
state.currMouseYForce.set(0.0)
mouseYSmoother.reset()
} else {
controller.applyGlobalPitch(pitchRot.toFloat() * -1.0f)
}
// Have accumulated input begin decay when no input detected
if (abs(mouseX) == 0.0) {
// Have decay on roll be much stronger.
state.currMouseXForce.set(lerp(state.currMouseXForce.get(), 0.0, 0.02))
}
if (mouseY == 0.0) {
state.currMouseYForce.set(lerp(state.currMouseYForce.get(), 0.0, 0.005))
}
//yaw, pitch, roll
return Vec3.ZERO
}
fun rollRotation(
@@ -322,8 +396,9 @@ class JetBehaviour : RidingBehaviour<JetSettings, JetState> {
sensitivity: Double,
deltaTime: Double
): Vec3 {
if (vehicle !is OrientationControllable) return Vec3.ZERO
if (driver !is OrientationControllable) return Vec3.ZERO
//TODO: figure out a cleaner solution to this issue of large jumps when skipping frames or lagging
//Cap at a rate of 5fps so frame skips dont lead to huge jumps
val cappedDeltaTime = min(deltaTime, 0.2)
// Accumulate the mouse input
@@ -337,6 +412,14 @@ class JetBehaviour : RidingBehaviour<JetSettings, JetState> {
//convert it to delta time
handling *= (cappedDeltaTime)
val poke = driver.vehicle as? PokemonEntity
//TODO: reevaluate if deadzones are needed and if they are still causing issues.
//create deadzones for the constant input values.
//val xInput = remapWithDeadzone(state.currMouseXForce, 0.025, 1.0)
//val yInput = remapWithDeadzone(state.currMouseYForce, 0.025, 1.0)
val pitchRot = handling * state.currMouseYForce.get()
// Roll
@@ -347,10 +430,10 @@ class JetBehaviour : RidingBehaviour<JetSettings, JetState> {
// Have accumulated input begin decay when no input detected
if (abs(mouseX) == 0.0) {
// Have decay on roll be much stronger.
state.currMouseXForce.set(lerp(state.currMouseXForce.get(), 0.0, 0.08))
state.currMouseXForce.set(lerp(state.currMouseXForce.get(), 0.0, 0.02))
}
if (mouseY == 0.0) {
state.currMouseYForce.set(lerp(state.currMouseYForce.get(), 0.0, 0.02))
state.currMouseYForce.set(lerp(state.currMouseYForce.get(), 0.0, 0.005))
}
//yaw, pitch, roll
@@ -20,7 +20,6 @@ import com.cobblemon.mod.common.api.riding.behaviour.RidingBehaviourSettings
import com.cobblemon.mod.common.api.riding.behaviour.RidingBehaviourState
import com.cobblemon.mod.common.api.riding.behaviour.Side
import com.cobblemon.mod.common.api.riding.behaviour.ridingState
import com.cobblemon.mod.common.api.riding.behaviour.types.air.angleDifference
import com.cobblemon.mod.common.api.riding.posing.PoseOption
import com.cobblemon.mod.common.api.riding.posing.PoseProvider
import com.cobblemon.mod.common.api.riding.sound.RideSoundSettingsList
@@ -48,7 +47,6 @@ import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.player.Player
import net.minecraft.world.phys.Vec2
import net.minecraft.world.phys.Vec3
import org.joml.Vector3f
class DolphinBehaviour : RidingBehaviour<DolphinSettings, DolphinState> {
companion object {
@@ -64,7 +62,7 @@ class DolphinBehaviour : RidingBehaviour<DolphinSettings, DolphinState> {
}
val poseProvider = PoseProvider<DolphinSettings, DolphinState>(PoseType.FLOAT)
.with(PoseOption(PoseType.SWIM) { _, state, entity -> abs(state.rideVelocity.get().length()) > 0.05 })
.with(PoseOption(PoseType.SWIM) { _, _, entity -> entity.deltaMovement.length() > 0.1 })
override fun isActive(settings: DolphinSettings, state: DolphinState, vehicle: PokemonEntity): Boolean {
return vehicle.isInWater || !vehicle.onGround()
@@ -153,46 +151,37 @@ class DolphinBehaviour : RidingBehaviour<DolphinSettings, DolphinState> {
driver: Player,
input: Vec3
): Vec3 {
if (vehicle !is OrientationControllable) return Vec3.ZERO
val controller = (vehicle as OrientationControllable).orientationController
val orientation = controller.orientation ?: return Vec3.ZERO
if (driver !is OrientationControllable) return Vec3.ZERO
val controller = (driver as OrientationControllable).orientationController
val runtime = vehicle.runtime
val boostMod = if (state.boosting.get()) runtime.resolveDouble(settings.jumpExpr ?: globalDolphin.jumpExpr!!) else 1.0
val topSpeed = (vehicle.runtime.resolveDouble(settings.speedExpr ?: globalDolphin.speedExpr!!) / 20.0) * boostMod
val accel = (topSpeed / (vehicle.runtime.resolveDouble(settings.accelerationExpr ?: globalDolphin.accelerationExpr!!) * 20.0)) * boostMod
val strafeFactor = runtime.resolveDouble(settings.strafeFactor ?: globalDolphin.strafeFactor!!)
val reverseDriveFactor = runtime.resolveDouble(settings.reverseDriveFactor ?: globalDolphin.reverseDriveFactor!!)
var currVel = state.rideVelocity.get()
/*********************************************
* Breaching logic
*********************************************/
if (!vehicle.isInWater && !vehicle.isUnderWater)
{
var prevVel = state.lastVelocity.get()
val breachBoost = 0.3
state.clearMouseSmoothers.set(false)
val currVel = state.lastVelocity.get()
val breachBoost = 1.5
if (!state.hasBreached.get()) {
// Apply an upwards breaching force if boosting. This will enhance jumping out of water to feel more fun
// Apply an upwards breaching force if boosting. This will enhance jumping out of water to feel more
// fun
if (state.boosting.get()) {
prevVel = Vec3(prevVel.x, prevVel.y + breachBoost*(sin(-controller.pitch.toRadians()).coerceIn(0.0f,0.5f)*2.0).pow(2), prevVel.z)
state.lastVelocity.set(Vec3(currVel.x, currVel.y + breachBoost*sin(-controller.pitch.toRadians()).coerceIn(0.0f,0.5f), currVel.z))
}
state.hasBreached.set(true)
} else {
val gravity = (9.8 / ( 20.0)) * 0.2 * 0.4
val terminalVel = 2.0
prevVel = Vec3(prevVel.x, max(-terminalVel, prevVel.y - gravity), prevVel.z)
state.lastVelocity.set(Vec3(currVel.x, max(-terminalVel, currVel.y - gravity), currVel.z)) //.yRot(vehicle.yRot.toRadians()))
}
state.lastVelocity.set(prevVel)
return state.lastVelocity.get().yRot(vehicle.yRot.toRadians())
return state.lastVelocity.get()
} else {
if(state.hasBreached.get()) {
state.clearMouseSmoothers.set(true)
}
state.lastVelocity.set(vehicle.deltaMovement.yRot(vehicle.yRot.toRadians()))
state.hasBreached.set(false)
currVel = Vec3(currVel.x, 0.0, currVel.z) // Reset y velocity to account for y vel persistance edgecase
}
/*********************************************
@@ -201,30 +190,32 @@ class DolphinBehaviour : RidingBehaviour<DolphinSettings, DolphinState> {
if (vehicle.horizontalCollision || vehicle.verticalCollision) {
// not actually friction
val frictionLimit = 0.1
val postColSpeed = min(max(state.rideVelocity.get().length(), frictionLimit), state.speed.get())
val postColSpeed = min(max(vehicle.deltaMovement.length(), frictionLimit), state.speed.get())
state.speed.set(postColSpeed)
}
val currZVel = state.rideVelocity.get().z
val newZVel = when {
driver.zza > 0.0 -> min(topSpeed, currZVel + accel)
driver.zza < 0.0 -> max( -topSpeed * reverseDriveFactor, currZVel - accel)
else -> lerp(currZVel, 0.0, 0.05)
val currSpeed = state.speed.get()
val strafeFactor = runtime.resolveDouble(settings.strafeFactor ?: globalDolphin.strafeFactor!!)
val reverseDriveFactor = runtime.resolveDouble(settings.reverseDriveFactor ?: globalDolphin.reverseDriveFactor!!)
val f = -driver.xxa * strafeFactor
when {
driver.zza > 0.0 -> state.speed.set(min(topSpeed, currSpeed + accel))
driver.zza < 0.0 -> state.speed.set(max( -topSpeed * reverseDriveFactor, currSpeed - accel))
else -> state.speed.set( lerp(currSpeed, 0.0, 0.05))
}
state.rideVelocity.set(Vec3(currVel.x, currVel.y, newZVel))
val vertInput = when {
driver.jumping && state.stamina.get() != 0.0f -> 1.0
driver.isShiftKeyDown -> -1.0
else -> 0.0
} * strafeFactor
// Convert the local velocity vector into a world vector
val localVelVec = Vector3f(
state.rideVelocity.get().x.toFloat(),
(state.rideVelocity.get().y).toFloat(),
state.rideVelocity.get().z.toFloat() * -1.0f // Flip the z axis to make this left handed
)
var newVelocity = Vec3(f, vertInput,-state.speed.get())
val worldVelVec = localVelVec.mul(orientation).toVec3d()
state.lastVelocity.set(worldVelVec) // Set last velocity to be used during breaching
// Unrotate preemptively as this vector gets rotated later down the line in MC logic.
return worldVelVec.yRot(vehicle.yRot.toRadians())
// Align to the ride
newVelocity = controller.orientation?.transform(newVelocity.toVector3f())?.toVec3d() ?: Vec3.ZERO
newVelocity = newVelocity.yRot(vehicle.yRot.toRadians())
val desiredVec = newVelocity.subtract(vehicle.deltaMovement)
return vehicle.deltaMovement.add(desiredVec)
}
override fun angRollVel(
@@ -234,8 +225,9 @@ class DolphinBehaviour : RidingBehaviour<DolphinSettings, DolphinState> {
driver: Player,
deltaTime: Double
): Vec3 {
if (vehicle !is OrientationControllable) return Vec3.ZERO
val controller = (vehicle as OrientationControllable).orientationController
if (Cobblemon.config.disableRoll) return Vec3.ZERO
if (driver !is OrientationControllable) return Vec3.ZERO
val controller = (driver as OrientationControllable).orientationController
val currSpeed = vehicle.deltaMovement.length()
@@ -243,7 +235,7 @@ class DolphinBehaviour : RidingBehaviour<DolphinSettings, DolphinState> {
val topSpeed = vehicle.runtime.resolveDouble(settings.speedExpr ?: globalDolphin.speedExpr!!) / 20.0
val yawDeltaDeg = deltaTime * handling * sin(Math.toRadians(controller.roll.toDouble())).pow(2) * sin(Math.toRadians(controller.roll.toDouble())).sign
val trueYawDelt = yawDeltaDeg * abs(cos(Math.toRadians(controller.pitch.toDouble()))*2).coerceIn(-1.0, 1.0) * sqrt(RidingBehaviour.scaleToRange(currSpeed, 0.0, topSpeed))
val trueYawDelt = yawDeltaDeg * abs(cos(Math.toRadians(controller.pitch.toDouble()))) * sqrt(RidingBehaviour.scaleToRange(currSpeed, 0.0, topSpeed))
// Dampen yaw when upside down
val yawDampen = (1 - abs(min(cos(controller.roll.toRadians()),0.0f)))
@@ -264,62 +256,45 @@ class DolphinBehaviour : RidingBehaviour<DolphinSettings, DolphinState> {
driver: Player,
deltaTime: Double
) {
// Stop roll and pitch correction when breached
if (state.hasBreached.get()) {
state.currRollCorrectionForce.set(
lerp(state.currRollCorrectionForce.get(), 0.0, deltaTime * 0.98)
)
state.currPitchCorrectionForce.set(
lerp(state.currPitchCorrectionForce.get(), 0.0, deltaTime * 0.98)
)
return
}
// Calculate correcting roll force.
// Mult by cos(pitch) to ensure that it doesn't roll correct at when diving or climbing
val currSpeed = vehicle.deltaMovement.length()
val rollCorrectionTimer = vehicle.runtime.resolveDouble(settings.timeToRollCorrect ?: globalDolphin.timeToRollCorrect!!)
// If the ride is moving then have the correction rate be an equal proportion of:
// - how rolled are you
// - how long has it been since last mouse input (higher is stronger correction)
// Also the correction is proportional as a whole to how NOT pitched are you so you don't spin endlessly when looking up or down
val howRolledAmI = sqrt(abs((cos(controller.roll.toRadians()) - 1.0f) * 0.5f)) // sqrt so it trends towards one pretty quickly
val maxRollCorrectionRate =
if (state.rideVelocity.get().length() < 0.2) 10.0f * abs(cos(controller.pitch.toRadians()))
else 10.0f * cos(controller.pitch.toRadians()).pow(2) * (
howRolledAmI*1.0f +
(state.noInputTime.get() - rollCorrectionTimer).coerceIn(0.0, 1.0).pow(2).toFloat()*0.5f
)
val maxRollForce = maxRollCorrectionRate / 40.0f
val rollArrivalDeg = 60.0f
val desiredRoll = 0.0f
val maxRollCorrectionRate = if (!(vehicle.isInWater || vehicle.isUnderWater)) 15.0f
else 10.0f * (cos(controller.pitch.toRadians())).pow(2) * ((state.noInputTime.get() - rollCorrectionTimer) * 2).coerceIn(0.0, 1.0).toFloat()
val maxRollForce = maxRollCorrectionRate / 40.0
val rollArrivalDeg = 90.0
// Calculate the signed error (how far and in what direction)
val rollError = Mth.wrapDegrees(desiredRoll.angleDifference(controller.roll))
if((state.speed.get() < 0.1 && driver.zza.toDouble() == 0.0) || state.noInputTime.get() > rollCorrectionTimer || !(vehicle.isInWater || vehicle.isUnderWater)) {
val arrivalInfluence = (min(rollArrivalDeg,abs(controller.roll).toDouble()) / rollArrivalDeg)
val desiredRollForce = (0 - controller.roll).sign * maxRollCorrectionRate * arrivalInfluence
var steeredRollForce = desiredRollForce - state.currRollCorrectionForce.get()
// Use the absolute error to calculate the magnitude of the influence
val influenceMagnitude = (min(rollArrivalDeg, abs(rollError)) / rollArrivalDeg)
val desiredRollForce = maxRollCorrectionRate * influenceMagnitude * -rollError.sign
var steeredRollForce = desiredRollForce - state.currRollCorrectionForce.get()
// Ensure the 'steering' correction force doesn't exceed our maxForce
if (abs(steeredRollForce) > maxRollForce) {
steeredRollForce = steeredRollForce.sign * maxRollForce
}
// Ensure the 'steering' correction force doesn't exceed our maxForce
if (abs(steeredRollForce) > maxRollForce) {
steeredRollForce = steeredRollForce.sign * maxRollForce
// Apply the 'steered' force (its really not a steering force I need a better name)
state.currRollCorrectionForce.set(
state.currRollCorrectionForce.get() + steeredRollForce
)
} else {
state.currRollCorrectionForce.set(
//reduce rollCorrection gradually
lerp(state.currRollCorrectionForce.get(), 0.0, deltaTime * 0.96)
)
}
// Apply the 'steered' force (its really not a steering force I need a better name)
state.currRollCorrectionForce.set(
state.currRollCorrectionForce.get() + steeredRollForce
)
//Calculate correcting pitch force.
val maxPitchCorrectionRate = 5.0f * (state.noInputTime.get() - rollCorrectionTimer).coerceIn(0.0, 1.0).pow(2).toFloat()*0.5f
val maxPitchForce = maxPitchCorrectionRate / 40.0f
val pitchArrivalDeg = 45.0f
val desiredPitch = 0.0f
val maxPitchCorrectionRate = if (!(vehicle.isInWater || vehicle.isUnderWater)) 15.0 else 5.0f * ((state.noInputTime.get() - rollCorrectionTimer) * 2).coerceIn(0.0, 1.0).toFloat() * (1 - (currSpeed * 0.5).coerceIn(0.0, 1.0))
val maxPitchForce = maxPitchCorrectionRate / 40.0
val pitchArrivalDeg = 45.0
val targetPitch = if ((vehicle.isInWater || vehicle.isUnderWater)) 0.0f else (-asin(vehicle.deltaMovement.y / vehicle.deltaMovement.length()).toDegrees()).coerceIn(-45.0f, 45.0f)
if(state.rideVelocity.get().length() < 0.1 && state.noInputTime.get() > rollCorrectionTimer) {
val pitchErrror = Mth.wrapDegrees(desiredPitch.angleDifference(controller.pitch))
val arrivalInfluence = (min(pitchArrivalDeg,abs(pitchErrror)) / pitchArrivalDeg)
val desiredPitchForce = maxPitchCorrectionRate * arrivalInfluence * -pitchErrror.sign
if((state.noInputTime.get() > rollCorrectionTimer && driver.zza.toDouble() == 0.0) || !(vehicle.isInWater || vehicle.isUnderWater)) {
val arrivalInfluence = min(rollArrivalDeg, abs(Mth.wrapDegrees(targetPitch - controller.pitch.toDouble()))) / pitchArrivalDeg
val desiredPitchForce = (targetPitch - controller.pitch).sign * maxPitchCorrectionRate * arrivalInfluence
var steeredPitchForce = desiredPitchForce - state.currPitchCorrectionForce.get()
// Ensure the 'steering' correction force doesn't exceed our maxForce
@@ -358,18 +333,81 @@ class DolphinBehaviour : RidingBehaviour<DolphinSettings, DolphinState> {
} else {
state.noInputTime.set(0.0)
}
return rollRotation(
settings,
state,
vehicle,
driver,
mouseY,
mouseX,
mouseYSmoother,
mouseXSmoother,
sensitivity,
deltaTime
return when {
Cobblemon.config.disableRoll -> noRollRotation(
settings,
state,
vehicle,
driver,
mouseY,
mouseX,
mouseYSmoother,
mouseXSmoother,
sensitivity,
deltaTime
)
else -> rollRotation(
settings,
state,
vehicle,
driver,
mouseY,
mouseX,
mouseYSmoother,
mouseXSmoother,
sensitivity,
deltaTime
)
}
}
fun noRollRotation(
settings: DolphinSettings,
state: DolphinState,
vehicle: PokemonEntity,
driver: Player,
mouseY: Double,
mouseX: Double,
mouseYSmoother: SmoothDouble,
mouseXSmoother: SmoothDouble,
sensitivity: Double,
deltaTime: Double
): Vec3 {
if (driver !is OrientationControllable) return Vec3.ZERO
val controller = (driver as OrientationControllable).orientationController
// Set roll to zero if transitioning to noroll config
controller.rotateRoll(controller.roll * -1.0f)
val boostingHandleDebuff = if (state.boosting.get()) 0.2 else 1.0
val handling = vehicle.runtime.resolveDouble(settings.handlingExpr ?: globalDolphin.handlingExpr!!) * boostingHandleDebuff
val mouseInputMult = vehicle.runtime.resolveDouble(settings.mouseInputMult ?: globalDolphin.mouseInputMult!!)
//Smooth out mouse input.
val smoothingSpeed = 4.0
val mouseXc = ((mouseX).coerceIn(-60.0, 60.0) / 60.0) * mouseInputMult
val mouseYc = ((mouseY).coerceIn(-60.0, 60.0) / 60.0) * mouseInputMult
val xInput = mouseXSmoother.getNewDeltaValue(mouseXc * 0.1, deltaTime * smoothingSpeed);
val yInput = mouseYSmoother.getNewDeltaValue(mouseYc * 0.1, deltaTime * smoothingSpeed);
//Give the ability to yaw with x mouse input when at low speeds.
val yawForce = xInput * handling
//Apply yaw globally as we don't want roll or pitch changes due to local yaw when looking up or down.
controller.applyGlobalYaw(yawForce.toFloat() * abs(cos(controller.pitch.toRadians())).pow(2))
var pitchRot = yInput * handling
// Pitch up globally
if (abs(controller.pitch + pitchRot) >= 89.5 ) {
pitchRot = 0.0
mouseYSmoother.reset()
} else {
controller.applyGlobalPitch(pitchRot.toFloat() * -1.0f)
}
//yaw, pitch, roll
return Vec3.ZERO
}
fun rollRotation(
@@ -384,71 +422,41 @@ class DolphinBehaviour : RidingBehaviour<DolphinSettings, DolphinState> {
sensitivity: Double,
deltaTime: Double
): Vec3 {
if (vehicle !is OrientationControllable) return Vec3.ZERO
val controller = (vehicle as OrientationControllable).orientationController
if (driver !is OrientationControllable) return Vec3.ZERO
val controller = (driver as OrientationControllable).orientationController
val topSpeed = vehicle.runtime.resolveDouble(settings.speedExpr ?: globalDolphin.speedExpr!!) / 20.0
val boostingHandleDebuff = if (state.boosting.get()) 0.5 else 1.0
val boostingHandleDebuff = if (state.boosting.get()) 0.2 else 1.0
val mouseInputMult = vehicle.runtime.resolveDouble(settings.mouseInputMult ?: globalDolphin.mouseInputMult!!) * boostingHandleDebuff
// Applied to mouseX to prevent turning upside down when trying to roll hard.
val rollDegreeDebuff = if(mouseX.sign.toFloat() == controller.roll.sign) 1 - 0.9f*sqrt(abs(sin(controller.roll.toRadians())))*sqrt(abs(cos(controller.pitch))) else 1.0f
//Smooth out mouse input.
val smoothingSpeed = if(state.hasBreached.get()) 1.0f else 2.0f
// When upside down apply a pitching force to nudge the player towards a dive. This reduces the 'weirdness'
// of being upside down
val rolledOverPitchCorrection = -8*sqrt(abs(min(cos(controller.roll.toRadians()),0.0f))) * (cos(controller.pitch.toRadians())).pow(2)
val mouseXc = (mouseX).coerceIn(-45.0, 45.0) * mouseInputMult * rollDegreeDebuff
val mouseYc = (mouseY).coerceIn(-45.0, 45.0) * mouseInputMult + rolledOverPitchCorrection
val mouseModForce = if (state.hasBreached.get()) 0.15 else 0.1 // Used to effect more spin momentum during breaching
val smoothingSpeed = 4.0
val mouseXc = (mouseX).coerceIn(-45.0, 45.0) * mouseInputMult
val mouseYc = (mouseY).coerceIn(-45.0, 45.0) * mouseInputMult
val xInput = mouseXSmoother.getNewDeltaValue(mouseXc * 0.1, deltaTime * smoothingSpeed);
val yInput = mouseYSmoother.getNewDeltaValue(mouseYc * 0.1, deltaTime * smoothingSpeed);
val currSpeed = vehicle.deltaMovement.length()
if (state.clearMouseSmoothers.get()) {
mouseXSmoother.reset()
mouseYSmoother.reset()
state.clearMouseSmoothers.set(false)
}
val xInput = mouseXSmoother.getNewDeltaValue(mouseXc * mouseModForce, deltaTime * smoothingSpeed);
val yInput = mouseYSmoother.getNewDeltaValue(mouseYc * mouseModForce, deltaTime * smoothingSpeed);
val currSpeed = state.rideVelocity.get().length()
val currHorzSpeed = state.rideVelocity.get().horizontalDistance()
val hoverSpeed = 0.2
val pitchDamping = abs(cos(controller.pitch.toRadians())).pow(2) // Helper value used when wanting to discard the influence of another value at steep pitches
// Yaw locally when at or below hoverSpeed but not when pitched up or rolled over
val globalYaw = xInput * (1 - sqrt(RidingBehaviour.scaleToRange(currSpeed, 0.0, hoverSpeed))).coerceIn(0.0, 1.0)
controller.rotateYaw(pitchDamping*globalYaw.toFloat())
controller.rotateRoll((1-pitchDamping)*globalYaw.toFloat())
// Yaw locally
// Dampen yaw when rolled or pitched substantially
val yawDampen = abs(cos(controller.pitch.toRadians())).pow(2) * abs(cos(controller.roll.toRadians())).pow(2)
val localYaw = xInput * (1 - (RidingBehaviour.scaleToRange(currSpeed, 0.0, topSpeed))).coerceIn(0.4, 1.0) * yawDampen
// Pitch locally up or down depending upon a number of factors:
// - Reduce pitch substantially when rolled and in a steep yaw. This prevents the player from ignoring a slow handling stat
// - Do not pitch at all when at or below hover speed
// - pitch faster at higher speeds
val p = 1.0 - vehicle.runtime.resolveDouble(settings.horizontalPitchExpr ?: globalDolphin.horizontalPitchExpr!!)
val localPitch = yInput * (1 - abs(sin(controller.roll.toRadians())).pow(2) * p * hypot(controller.forwardVector.x.toDouble(), controller.forwardVector.z.toDouble()).toFloat())
var localYawForce = 0.0
var rollForce = 0.0
// If it has breached then turn the yaw into roll
if (state.hasBreached.get()) {
localYawForce = xInput
} else {
// Roll less when slow and not at all when at hoverSpeed or lower.
// Apply a roll righting force when trying to pitch hard while rolled sideways. This nudges the player towards
// pitching globally
val pitchInfluencedRollCorrection = 0.0
val howRolledAmI = (( cos(controller.roll.toRadians()) - 1).coerceIn(-1.0f, 0.0f) * -2) //Coerce into how much roll you want to let in in the deadzone
val isInputtingTowardsRoll = if ( xInput.sign == controller.roll.sign.toDouble()) 1.0 else 0.0
val inputInRolledDir = if (isInputtingTowardsRoll == 1.0) 1.0 else -cos(controller.roll.toRadians()).coerceIn(-1.0f, 0.0f).toDouble()
val rollDampen = (1 - howRolledAmI.coerceIn(0.0f, 0.5f) * pitchDamping * inputInRolledDir)
rollForce = xInput * sqrt(abs(RidingBehaviour.scaleToRange(currHorzSpeed, 0.0, topSpeed)).coerceIn(0.0,1.0)) * rollDampen + pitchInfluencedRollCorrection
// When already rolled significantly part of the roll force should turn to local yaw force
localYawForce = rollForce * abs(sin(controller.roll.toRadians())).pow(2) * pitchDamping * isInputtingTowardsRoll
}
val localPitch = yInput * (1 - abs(sin(controller.roll.toRadians())) * p * hypot(controller.forwardVector.x.toDouble(), controller.forwardVector.z.toDouble()).toFloat())
// localPitch *= if (currSpeed > hoverSpeed) RidingBehaviour.scaleToRange(currSpeed, hoverSpeed, topSpeed)
// else 0.0
// Roll less when slow and not at all when at hoverSpeed or lower.
// Apply a roll righting force when trying to pitch hard while rolled sideways. This nudges the player towards
// pitching globally
val rollForce = xInput * ((RidingBehaviour.scaleToRange(currSpeed, 0.0, topSpeed))).coerceIn(0.2, 1.0) //+ pitchInfluencedRollCorrection
//yaw, pitch, roll
return Vec3(localYawForce, localPitch, rollForce)
return Vec3(localYaw, localPitch, rollForce)
}
override fun canJump(
@@ -632,7 +640,6 @@ class DolphinState : RidingBehaviourState() {
var boostIsToggleable = ridingState(false, Side.CLIENT)
var speed = ridingState(0.0, Side.CLIENT)
var hasBreached = ridingState(false, Side.CLIENT)
var clearMouseSmoothers = ridingState(false, Side.CLIENT)
override fun encode(buffer: FriendlyByteBuf) {
super.encode(buffer)
@@ -654,7 +661,6 @@ class DolphinState : RidingBehaviourState() {
boostIsToggleable.set(true, forced = true)
speed.set(0.0, forced = true)
hasBreached.set(false, forced = true)
clearMouseSmoothers.set(false, forced = true)
}
override fun toString(): String {
@@ -672,7 +678,6 @@ class DolphinState : RidingBehaviourState() {
it.boostIsToggleable.set(this.boosting.get(), forced = true)
it.speed.set(this.speed.get(), forced = true)
it.hasBreached.set(this.hasBreached.get(), forced = true)
it.clearMouseSmoothers.set(this.clearMouseSmoothers.get(), forced = true)
}
override fun shouldSync(previous: RidingBehaviourState): Boolean {
@@ -1,643 +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.riding.behaviour.types.liquid
import com.bedrockk.molang.Expression
import com.bedrockk.molang.runtime.MoLangMath.lerp
import com.cobblemon.mod.common.CobblemonRideSettings
import com.cobblemon.mod.common.OrientationControllable
import com.cobblemon.mod.common.api.orientation.OrientationController
import com.cobblemon.mod.common.api.riding.RidingStyle
import com.cobblemon.mod.common.api.riding.behaviour.*
import com.cobblemon.mod.common.api.riding.posing.PoseOption
import com.cobblemon.mod.common.api.riding.posing.PoseProvider
import com.cobblemon.mod.common.api.riding.sound.RideSoundSettingsList
import com.cobblemon.mod.common.api.riding.stats.RidingStat
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.math.geometry.toRadians
import com.cobblemon.mod.common.util.readNullableExpression
import com.cobblemon.mod.common.util.readRidingStats
import com.cobblemon.mod.common.util.resolveBoolean
import com.cobblemon.mod.common.util.resolveDouble
import com.cobblemon.mod.common.util.toVec3d
import com.cobblemon.mod.common.util.writeNullableExpression
import com.cobblemon.mod.common.util.writeRidingStats
import net.minecraft.core.BlockPos
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
import net.minecraft.tags.FluidTags
import net.minecraft.util.SmoothDouble
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.player.Player
import net.minecraft.world.phys.Vec2
import net.minecraft.world.phys.Vec3
import org.joml.Vector3f
import kotlin.math.*
class SubmarineBehaviour : RidingBehaviour<SubmarineSettings, SubmarineState> {
companion object {
val KEY = cobblemonResource("liquid/submarine")
}
override val key = KEY
val globalSubmarine: SubmarineSettings
get() = CobblemonRideSettings.submarine
override fun getRidingStyle(settings: SubmarineSettings, state: SubmarineState): RidingStyle {
return RidingStyle.LIQUID
}
val poseProvider = PoseProvider<SubmarineSettings, SubmarineState>(PoseType.FLOAT)
.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()
}
override fun pose(settings: SubmarineSettings, state: SubmarineState, vehicle: PokemonEntity): PoseType {
return poseProvider.select(settings, state, vehicle)
}
override fun speed(settings: SubmarineSettings, state: SubmarineState, vehicle: PokemonEntity, driver: Player): Float {
return state.speed.get().toFloat()
}
override fun tick(
settings: SubmarineSettings,
state: SubmarineState,
vehicle: PokemonEntity,
driver: Player,
input: Vec3
) {
if(vehicle.level().isClientSide) {
tickStamina(settings, state, vehicle)
surfaceCheck(settings, state, vehicle, driver)
}
}
fun tickStamina(
settings: SubmarineSettings,
state: SubmarineState,
vehicle: PokemonEntity
) {
val stam = state.stamina.get()
if (vehicle.runtime.resolveBoolean(settings.infiniteStamina ?: globalSubmarine.infiniteStamina!!)) {
return
}
// Grab the boost time in seconds and convert to ticks. Then calculate the drain rate as inversely
// proportional to the number of ticks of boost thus making a full boost take x ticks
// in short: "Stamina drains slower at higher values and also replenishes slower"
val breathTime = vehicle.runtime.resolveDouble(settings.staminaExpr ?: globalSubmarine.staminaExpr!!) * 20.0f
val stamRegenRate = 1.0f / (vehicle.runtime.resolveDouble(settings.staminaRegenTime ?: globalSubmarine.staminaRegenTime!!).toFloat() * 20.0f)
val stamDrainRate = (1.0f / breathTime).toFloat()
val driver = vehicle.controllingPassenger
val newStam = if (driver?.isEyeInFluid(FluidTags.WATER) ?: false) max(0.0f,stam - stamDrainRate)
else min(1.0f,stam + stamRegenRate)
state.stamina.set(newStam)
}
private fun surfaceCheck(
settings: SubmarineSettings,
state: SubmarineState,
vehicle: PokemonEntity,
driver: Player,
) {
// determine if the vehicle is in water.
// if it isn't then set surface flag to false and return
if (!vehicle.isInWater) {
state.onSurface.set(false)
return
}
// if it is then check half the bbheight above and below the center point.
// 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(vehicleCenter)
val centerInWater = !vehicle.level().getFluidState(centerBlockPos).isEmpty
val abovePos = centerBlockPos.above()
val aboveInWater = !vehicle.level().getFluidState(abovePos).isEmpty
if (!centerInWater && !aboveInWater) {
state.onSurface.set(true)
} else {
state.onSurface.set(false)
}
//
// Function for if x mouse and y mouse "get through" to affecting the mounts rotation. Return a Pair<Boolean, Boolean>
}
override fun rotation(
settings: SubmarineSettings,
state: SubmarineState,
vehicle: PokemonEntity,
driver: LivingEntity
): Vec2 {
return Vec2(driver.xRot * 0.5f, driver.yRot)
}
override fun velocity(
settings: SubmarineSettings,
state: SubmarineState,
vehicle: PokemonEntity,
driver: Player,
input: Vec3
): Vec3 {
if (vehicle !is OrientationControllable) return Vec3.ZERO
val controller = (vehicle as OrientationControllable).orientationController
val orientation = controller.orientation ?: return Vec3.ZERO
val runtime = vehicle.runtime
val topSpeed = (vehicle.runtime.resolveDouble(settings.speedExpr ?: globalSubmarine.speedExpr!!) / 20.0)
val accel = (topSpeed / (vehicle.runtime.resolveDouble(settings.accelerationExpr ?: globalSubmarine.accelerationExpr!!) * 20.0))
val strafeSpeed = (runtime.resolveDouble(settings.jumpExpr ?: globalSubmarine.jumpExpr!!) / 20.0)
val strafeAccel = (strafeSpeed / (vehicle.runtime.resolveDouble(settings.accelerationExpr ?: globalSubmarine.accelerationExpr!!) * 20.0))
var currVel = state.rideVelocity.get()
/*********************************************
* Handle collisions
*********************************************/
if (vehicle.horizontalCollision || vehicle.verticalCollision) {
// not actually friction
val frictionLimit = 0.1
val postColSpeed = min(max(state.rideVelocity.get().length(), frictionLimit), state.speed.get())
state.speed.set(postColSpeed)
}
if (vehicle.verticalCollision) {
state.rideVelocity.set(Vec3(state.rideVelocity.get().x, 0.0, state.rideVelocity.get().z))
}
/*********************************************
* Calculate Forward Velocity
*********************************************/
val currZVel = state.rideVelocity.get().z
val newZVel = when {
driver.zza > 0.0 -> min(topSpeed, currZVel + accel)
driver.zza < 0.0 -> max( -strafeSpeed, currZVel - strafeAccel) //TODO: Might need to be applied in addition to the normal braking force?
else -> lerp(currZVel, 0.0, 0.05)
}
currVel = Vec3(currVel.x, currVel.y, newZVel)
/*********************************************
* Calculate up/down Velocity and gravity
*********************************************/
val vertTopSpeed = strafeSpeed
val currYVel = state.rideVelocity.get().y
val gravity = (9.8 / ( 20.0)) * 0.2 * 0.4
val terminalYVel = 2.0
val driverYInput = if(driver.jumping) 1.0 else if (driver.isShiftKeyDown) -1.0 else 0.0
val newYVel = when {
(!vehicle.isInWater) -> max(-terminalYVel, currYVel - gravity)
//(vehicle.isInWater && !vehicle.isUnderWater && currYVel > 0.001) -> lerp(currYVel, 0.0, 0.1) // Make sure that yVel is above a threshold before doing this otherwise it'll block submerging.
//driverYInput != 0.0 && currYVel > 0.001 && driverYInput.sign != currYVel.sign -> lerp(currYVel, 0.0, 0.05)
driverYInput > 0.0 -> min(vertTopSpeed, currYVel + strafeAccel)
driverYInput < 0.0 -> max(-vertTopSpeed, currYVel - strafeAccel)
else -> lerp(currYVel, 0.0, 0.05)
}
currVel = Vec3(currVel.x, newYVel, currVel.z)
/*********************************************
* Calculate left/right strafing Velocity
*********************************************/
val currXVel = state.rideVelocity.get().x
val horzTopSpeed = strafeSpeed
val newXVel = when {
driver.xxa > 0.0 -> min(horzTopSpeed, currXVel + strafeAccel)
driver.xxa < 0.0 -> max( -horzTopSpeed, currXVel - strafeAccel)
else -> lerp(currXVel, 0.0, 0.01)
}
currVel = Vec3(newXVel, currVel.y, currVel.z)
// Set the new velocity
state.rideVelocity.set(currVel)
/*********************************************
* Separate and apply velocities
*********************************************/
// If the driver and vehicles eyes are out of the water but the vehicle is still in it then move only horizontally or downwards as you have surfaced
if (state.onSurface.get()) {
val worldVelVec = Vec3(currVel.x, min(0.0, currVel.y), currVel.z)
return worldVelVec
} else {
val vertVec = Vec3(0.0, currVel.y, 0.0)
val horzVec = Vec3(currVel.x, 0.0, 0.0)
val localZVelVec = Vector3f(0.0f, 0.0f, currVel.z.toFloat() * -1.0f) // Flip the z axis to make this left handed
// Unrotate preemptively as this vector gets rotated later down the line in MC logic.
val worldVelVec = localZVelVec.mul(orientation).toVec3d().yRot(vehicle.yRot.toRadians())
// Add back the horizontal and vertical components.
return worldVelVec.add(vertVec).add(horzVec)
}
}
override fun angRollVel(
settings: SubmarineSettings,
state: SubmarineState,
vehicle: PokemonEntity,
driver: Player,
deltaTime: Double
): Vec3 {
return Vec3.ZERO
}
override fun applyRenderRotation(
settings: SubmarineSettings,
state: SubmarineState,
vehicle: PokemonEntity,
partialTicks: Float
) {
if (vehicle !is OrientationControllable) return
val controller = (vehicle as OrientationControllable).orientationController
var handling = vehicle.runtime.resolveDouble(settings.handlingExpr ?: globalSubmarine.handlingExpr!!)
val surfaceHandlingBoost = if(state.onSurface.get()) 3.0 else 1.0
handling *= surfaceHandlingBoost
val strafeSpeed = (vehicle.runtime.resolveDouble(settings.jumpExpr ?: globalSubmarine.jumpExpr!!) / 20.0)
// Grab the roll and see if its hit the limit yet. If it hasn't then trend it towards its desired roll
val maxRollDeg = if (state.onSurface.get()) 5 else 15
val maxPitchDeg = 15
var rollAmount = 0.0
var pitchAmount = 0.0
// Do horizontal speed based rolling
val maxHorzSpeed = strafeSpeed
val currHorzSpeed = state.rideVelocity.get().x
var desiredRoll = -(currHorzSpeed / maxHorzSpeed).coerceIn(-1.0, 1.0) * maxRollDeg
rollAmount += lerp(controller.roll.toDouble(), desiredRoll, partialTicks.toDouble()) - controller.roll
//val rotAmount //Maybe use some lerp?
controller.rotateRoll(rollAmount.toFloat())
// Do yawRate based rolling
val maxYawRate = handling
val currYawRate = vehicle.ridingAnimationData.rotDeltaSpring.getInterpolated(partialTicks.toDouble(), 2).y()
desiredRoll = -(currYawRate / maxYawRate).coerceIn(-1.0, 1.0) * maxRollDeg
rollAmount += desiredRoll - controller.roll
controller.rotateRoll(rollAmount.toFloat())
//TODO: why does this affect the actual controller but rolling doesn't??
// Do upwards movement based pitching
// val maxVertSpeed = 0.25 //TODO: CONNECT UP TO JUMP
// val currVertSpeed = state.rideVelocity.get().y
// var desiredPitch = -(currVertSpeed / maxVertSpeed) * maxPitchDeg
// pitchAmount += lerp(controller.pitch.toDouble(), desiredPitch, partialTicks.toDouble()) - controller.pitch
// controller.rotatePitch(pitchAmount.toFloat())
}
private fun correctOrientation(
settings: SubmarineSettings,
state: SubmarineState,
vehicle: PokemonEntity,
controller: OrientationController,
driver: Player,
deltaTime: Double
) {
// TODO: remove if not needed
// // Calculate correcting roll force.
// // If the ride is moving then have the correction rate be an equal proportion of:
// // - how rolled are you
// // - how long has it been since last mouse input (higher is stronger correction)
// // Also the correction is proportional as a whole to how NOT pitched are you so you don't spin endlessly when looking up or down
// val howRolledAmI = sqrt(abs((cos(controller.roll.toRadians()) - 1.0f) * 0.5f)) // sqrt so it trends towards one pretty quickly
// val maxRollCorrectionRate =
// if (state.rideVelocity.get().length() < 0.2) 10.0f * abs(cos(controller.pitch.toRadians()))
// else 10.0f * cos(controller.pitch.toRadians()).pow(2) * (
// howRolledAmI*1.0f +
// (state.noInputTime.get() - rollCorrectionTimer).coerceIn(0.0, 1.0).pow(2).toFloat()*0.5f
// )
// val maxRollForce = maxRollCorrectionRate / 40.0f
// val rollArrivalDeg = 60.0f
// val desiredRoll = 0.0f
//
// // Calculate the signed error (how far and in what direction)
// val rollError = Mth.wrapDegrees(desiredRoll.angleDifference(controller.roll))
//
// // Use the absolute error to calculate the magnitude of the influence
// val influenceMagnitude = (min(rollArrivalDeg, abs(rollError)) / rollArrivalDeg)
// val desiredRollForce = maxRollCorrectionRate * influenceMagnitude * -rollError.sign
// var steeredRollForce = desiredRollForce - state.currRollCorrectionForce.get()
//
// // Ensure the 'steering' correction force doesn't exceed our maxForce
// if (abs(steeredRollForce) > maxRollForce) {
// steeredRollForce = steeredRollForce.sign * maxRollForce
// }
//
// // Apply the 'steered' force (its really not a steering force I need a better name)
// state.currRollCorrectionForce.set(
// state.currRollCorrectionForce.get() + steeredRollForce
// )
//Calculate correcting pitch force.
// val rollCorrectionTimer = vehicle.runtime.resolveDouble(settings.timeToRollCorrect ?: globalSubmarine.timeToRollCorrect!!)
// val maxPitchCorrectionRate = 5.0f * (state.noInputTime.get() - rollCorrectionTimer).coerceIn(0.0, 1.0).pow(2).toFloat()*0.5f
// val maxPitchForce = maxPitchCorrectionRate / 40.0f
// val pitchArrivalDeg = 45.0f
// val desiredPitch = 0.0f
//
// if(state.rideVelocity.get().length() < 0.1 && state.noInputTime.get() > rollCorrectionTimer) {
// val pitchErrror = Mth.wrapDegrees(desiredPitch.angleDifference(controller.pitch))
// val arrivalInfluence = (min(pitchArrivalDeg,abs(pitchErrror)) / pitchArrivalDeg)
// val desiredPitchForce = maxPitchCorrectionRate * arrivalInfluence * -pitchErrror.sign
// var steeredPitchForce = desiredPitchForce - state.currPitchCorrectionForce.get()
//
// // Ensure the 'steering' correction force doesn't exceed our maxForce
// if (abs(steeredPitchForce) > maxPitchForce) {
// steeredPitchForce = steeredPitchForce.sign * maxPitchForce
// }
//
// // Apply the 'steered' force (its really not a steering force I need a better name)
// state.currPitchCorrectionForce.set(
// state.currPitchCorrectionForce.get() + steeredPitchForce
// )
// } else {
// state.currPitchCorrectionForce.set(
// //reduce rollCorrection gradually
// lerp(state.currPitchCorrectionForce.get(), 0.0, deltaTime * 0.98)
// )
// }
}
override fun rotationOnMouseXY(
settings: SubmarineSettings,
state: SubmarineState,
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
// Set roll to zero if transitioning to noroll config
controller.rotateRoll(controller.roll * -1.0f)
val handling = vehicle.runtime.resolveDouble(settings.handlingExpr ?: globalSubmarine.handlingExpr!!)
val surfaceHandlingBoost = if(state.onSurface.get()) 3.0 else 1.0
val effHandling = if(state.onSurface.get()) 1000 * deltaTime else handling * deltaTime
//Smooth out mouse input.
val smoothingSpeed = if(state.onSurface.get()) 10.0 else 2.0
val mouseXc = (mouseX * 0.1).coerceIn(-effHandling, effHandling)
val mouseYc = (mouseY * 0.1).coerceIn(-effHandling, effHandling)
val xInput = mouseXSmoother.getNewDeltaValue(mouseXc, deltaTime * smoothingSpeed);
val yInput = mouseYSmoother.getNewDeltaValue(mouseYc, deltaTime * smoothingSpeed);
// Give the ability to yaw with x mouse input when at low speeds.
val yawForce = xInput
// Apply yaw globally as we don't want roll or pitch changes due to local yaw when looking up or down.
controller.applyGlobalYaw(yawForce.toFloat())
var pitchRot = yInput
// Pitch up globally
val pitchLimit = 45.0
if (abs(controller.pitch + pitchRot) >= pitchLimit ) {
pitchRot = 0.0
mouseYSmoother.reset()
} else {
controller.applyGlobalPitch(pitchRot.toFloat())
}
// If on the surface then conditionally bring the pitch to 0 degrees.
if (state.onSurface.get()) {
//move back at max 30 degrees per second.
val maxCorrectionSpeed = 30.0
val arrivalDegreeWidth = 30.0
val currentPitch = controller.pitch
val correctionDegrees = (-currentPitch / arrivalDegreeWidth).coerceIn(-1.0, 1.0) * maxCorrectionSpeed * deltaTime
controller.applyGlobalPitch(correctionDegrees.toFloat())
// Compensate player ride rotations
// val playerRotater = driver as RideRotation
// playerRotater.`cobblemon$setRideXRot`(playerRotater.`cobblemon$getRideXRot`() - correctionDegrees.toFloat() )
}
//yaw, pitch, roll
return Vec3.ZERO
}
override fun canJump(
settings: SubmarineSettings,
state: SubmarineState,
vehicle: PokemonEntity,
driver: Player
): Boolean {
return false
}
override fun setRideBar(
settings: SubmarineSettings,
state: SubmarineState,
vehicle: PokemonEntity,
driver: Player
): Float {
return (state.stamina.get() / 1.0f)
}
override fun jumpForce(
settings: SubmarineSettings,
state: SubmarineState,
vehicle: PokemonEntity,
driver: Player,
jumpStrength: Int
): Vec3 {
return Vec3.ZERO
}
override fun gravity(
settings: SubmarineSettings,
state: SubmarineState,
vehicle: PokemonEntity,
regularGravity: Double
): Double {
return 0.0
}
override fun rideFovMultiplier(
settings: SubmarineSettings,
state: SubmarineState,
vehicle: PokemonEntity,
driver: Player
): Float {
return 1.0f
}
override fun useAngVelSmoothing(settings: SubmarineSettings, state: SubmarineState, vehicle: PokemonEntity): Boolean {
return false
}
override fun useRidingAltPose(
settings: SubmarineSettings,
state: SubmarineState,
vehicle: PokemonEntity,
driver: Player
): ResourceLocation {
return cobblemonResource("no_pose")
}
override fun inertia(settings: SubmarineSettings, state: SubmarineState, vehicle: PokemonEntity): Double {
return if (!vehicle.isUnderWater) 1.0 else 0.1
}
override fun shouldRoll(settings: SubmarineSettings, state: SubmarineState, vehicle: PokemonEntity): Boolean {
return true
}
override fun turnOffOnGround(settings: SubmarineSettings, state: SubmarineState, vehicle: PokemonEntity): Boolean {
return false
}
override fun dismountOnShift(settings: SubmarineSettings, state: SubmarineState, vehicle: PokemonEntity): Boolean {
return false
}
override fun mouseModifiesDriverRotation(settings: SubmarineSettings, state: SubmarineState, vehicle: PokemonEntity): Pair<Boolean, Boolean> {
return Pair(false, state.onSurface.get())
}
override fun shouldRotatePokemonHead(
settings: SubmarineSettings,
state: SubmarineState,
vehicle: PokemonEntity
): Boolean {
return false
}
override fun shouldRotateRiderHead(
settings: SubmarineSettings,
state: SubmarineState,
vehicle: PokemonEntity
): Boolean {
return false
}
override fun getRideSounds(
settings: SubmarineSettings,
state: SubmarineState,
vehicle: PokemonEntity
): RideSoundSettingsList {
return settings.rideSounds
}
override fun createDefaultState(settings: SubmarineSettings) = SubmarineState()
}
class SubmarineSettings : RidingBehaviourSettings {
override val key = SubmarineBehaviour.KEY
override val stats = mutableMapOf<RidingStat, IntRange>()
var infiniteStamina: Expression? = null
private set
var staminaRegenTime: Expression? = null
private set
// Tope strafe speed in Bl/s
var jumpExpr: Expression? = null
// Yaw rate in degrees per second
var handlingExpr: Expression? = null
// Top speed in Bl/s
var speedExpr: Expression? = null
// Seconds to get to top speed
var accelerationExpr: Expression? = null
// Seconds of air provided by mount
var staminaExpr: Expression? = null
var rideSounds: RideSoundSettingsList = RideSoundSettingsList()
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeRidingStats(stats)
rideSounds.encode(buffer)
buffer.writeNullableExpression(infiniteStamina)
buffer.writeNullableExpression(staminaRegenTime)
buffer.writeNullableExpression(jumpExpr)
buffer.writeNullableExpression(handlingExpr)
buffer.writeNullableExpression(speedExpr)
buffer.writeNullableExpression(accelerationExpr)
buffer.writeNullableExpression(staminaExpr)
}
override fun decode(buffer: RegistryFriendlyByteBuf) {
stats.putAll(buffer.readRidingStats())
rideSounds = RideSoundSettingsList.decode(buffer)
infiniteStamina = buffer.readNullableExpression()
staminaRegenTime = buffer.readNullableExpression()
jumpExpr = buffer.readNullableExpression()
handlingExpr = buffer.readNullableExpression()
speedExpr = buffer.readNullableExpression()
accelerationExpr = buffer.readNullableExpression()
staminaExpr = buffer.readNullableExpression()
}
}
class SubmarineState : RidingBehaviourState() {
var speed = ridingState(0.0, Side.CLIENT)
var currRollCorrectionForce = ridingState(0.0, Side.CLIENT)
var currPitchCorrectionForce = ridingState(0.0, Side.CLIENT)
var noInputTime = ridingState(0.0, Side.CLIENT)
var onSurface = ridingState(false, Side.BOTH)
override fun encode(buffer: FriendlyByteBuf) {
super.encode(buffer)
buffer.writeBoolean(onSurface.get())
}
override fun decode(buffer: FriendlyByteBuf) {
super.decode(buffer)
onSurface.set(buffer.readBoolean(), forced = true)
}
override fun reset() {
super.reset()
speed.set(0.0, forced = true)
currRollCorrectionForce.set(0.0, forced = true)
currPitchCorrectionForce.set(0.0, forced = true)
noInputTime.set(0.0, forced = true)
onSurface.set(false, forced = true)
}
override fun copy() = SubmarineState().also {
it.stamina.set(stamina.get(), forced = true)
it.speed.set(speed.get(), forced = true)
it.rideVelocity.set(rideVelocity.get(), forced = true)
it.currRollCorrectionForce.set(this.currRollCorrectionForce.get(), forced = true)
it.currPitchCorrectionForce.set(this.currPitchCorrectionForce.get(), forced = true)
it.noInputTime.set(this.noInputTime.get(), forced = true)
it.onSurface.set(this.onSurface.get(), forced = true)
}
override fun shouldSync(previous: RidingBehaviourState): Boolean {
if (previous !is SubmarineState) return false
if (previous.onSurface.get() != onSurface.get()) return true
return super.shouldSync(previous)
}
}
@@ -74,7 +74,7 @@ class RidingAnimationData(val ride: PokemonEntity)
}
if( r ) {
//TODO: should this return?
val controller = (ride as? OrientationControllable)?.orientationController ?: return
val controller = (ride.firstPassenger as? OrientationControllable)?.orientationController ?: return
val orientation = controller.orientation ?: Matrix3f()
val currPitch = controller.pitch
@@ -18,18 +18,11 @@ import com.cobblemon.mod.common.api.spawning.condition.SeafloorSpawningCondition
import com.cobblemon.mod.common.api.spawning.condition.SpawningCondition
import com.cobblemon.mod.common.api.spawning.condition.SubmergedSpawningCondition
import com.cobblemon.mod.common.api.spawning.condition.SurfaceSpawningCondition
import com.cobblemon.mod.common.api.spawning.position.calculators.GroundedSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.LavafloorSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SeafloorSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SubmergedSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SurfaceSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.detail.NPCSpawnDetail
import com.cobblemon.mod.common.api.spawning.detail.PokemonHerdSpawnDetail
import com.cobblemon.mod.common.api.spawning.detail.PokemonSpawnDetail
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.api.spawning.fishing.FishingSpawner
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePositionResolver
import com.cobblemon.mod.common.api.spawning.position.FishingSpawnablePosition
@@ -39,16 +32,21 @@ import com.cobblemon.mod.common.api.spawning.position.SeafloorSpawnablePosition
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.position.SubmergedSpawnablePosition
import com.cobblemon.mod.common.api.spawning.position.SurfaceSpawnablePosition
import com.cobblemon.mod.common.api.spawning.position.calculators.GroundedSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.LavafloorSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SeafloorSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SubmergedSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SurfaceSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.preset.BasicSpawnDetailPreset
import com.cobblemon.mod.common.api.spawning.preset.BestSpawnerConfig
import com.cobblemon.mod.common.api.spawning.preset.PokemonSpawnDetailPreset
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
import com.cobblemon.mod.common.api.spawning.spawner.AreaSpawner
import com.cobblemon.mod.common.api.spawning.spawner.BasicSpawner
import com.cobblemon.mod.common.api.spawning.spawner.FixedAreaSpawner
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawnerFactory
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.api.spawning.spawner.TickingSpawner
import com.cobblemon.mod.common.entity.pokemon.CobblemonAgingDespawner
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import net.minecraft.server.MinecraftServer
@@ -66,14 +64,13 @@ import net.minecraft.server.MinecraftServer
* In the case of more specialized use, the creation of a [SpawnablePosition] that motivates most of the spawn
* process can be created manually, skipping the first two steps.
*
* An individually spawnable entity is defined as a [SpawnDetail]. A processor handling this process is a [Spawner].
* Various subclasses exist for more specialized cases. A spawner that is constantly ticking and will spawn things
* without prompts is a [TickingSpawner], and one of those which occurs within a defined area is a [AreaSpawner]. If
* An individually spawnable thing is defined as a [SpawnDetail]. A processor handling this process is a [Spawner].
* Some subclasses exist for more specialized cases. If you are spawning in a fixed area and the spawner controlling
* that area is unmoving then it is a [FixedAreaSpawner] whereas if it is actively following the player it is a
* [PlayerSpawner].
* [PlayerSpawner]. You can also use a [BasicSpawner] for more general-purpose spawning without any issue.
*
* Spawning is coordinated and ticked using a [SpawnerManager], and all the current managers are accessible from
* [BestSpawner.spawnerManagers].
* PlayerSpawners are stored inside ServerPlayers using a mixin and tick at the end of a player's tick. The [PlayerSpawnerFactory]
* is used to build those. An extension function exists for getting the spawner for a player.
*
* Spawners and spawnable positions are often put under the effects of [SpawningInfluence]s which can be used to make
* temporary or lasting changes to spawning for whatever component they are attached to (whether that is a spawner or a
@@ -87,9 +84,9 @@ import net.minecraft.server.MinecraftServer
*/
object BestSpawner {
var config = BestSpawnerConfig()
val spawnerManagers = mutableListOf<SpawnerManager>(CobblemonWorldSpawnerManager)
lateinit var defaultPokemonDespawner: Despawner<PokemonEntity>
lateinit var fishingSpawner: FishingSpawner
lateinit var fishingSpawner: BasicSpawner
fun init() {
LOGGER.info("Starting the Best Spawner...")
@@ -137,12 +134,13 @@ object BestSpawner {
fun reloadConfig() {
loadConfig()
spawnerManagers.forEach(SpawnerManager::onConfigReload)
}
fun onServerStarted(server: MinecraftServer) {
CobblemonSpawnPools.onServerLoad(server)
spawnerManagers.forEach(SpawnerManager::onServerStarted)
fishingSpawner = FishingSpawner()
fishingSpawner = BasicSpawner(
name = "fishing",
spawnPool = CobblemonSpawnPools.WORLD_SPAWN_POOL
)
}
}
@@ -40,7 +40,6 @@ object CobblemonSpawnPools {
fun onServerLoad(server: MinecraftServer) {
Cobblemon.LOGGER.info("Optimizing spawn pools...")
WORLD_SPAWN_POOL.forEach { it.onServerLoad(server) }
WORLD_SPAWN_POOL.precalculate()
WORLD_SPAWN_POOL.onServerLoad(server)
}
}
@@ -1,76 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawnerFactory
import com.cobblemon.mod.common.platform.events.PlatformEvents
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules
import net.minecraft.server.level.ServerLevel
import java.util.UUID
import net.minecraft.server.level.ServerPlayer
/**
* The [SpawnerManager] that handles Cobblemon's world spawner, which is made up
* of [PlayerSpawner] instances. This manager listens for login and logout and
* cleans up where relevant.
*
* @author Hiroku
* @since February 14th, 2022
*/
object CobblemonWorldSpawnerManager : SpawnerManager() {
var spawnersForPlayers = mutableMapOf<UUID, PlayerSpawner>()
init {
PlatformEvents.SERVER_PLAYER_LOGIN.subscribe { this.onPlayerLogin(it.player) }
PlatformEvents.SERVER_PLAYER_LOGOUT.subscribe { this.onPlayerLogout(it.player) }
PlatformEvents.CHANGE_DIMENSION.subscribe { this.onDimensionChange(it.player, it.origin, it.destination) }
}
fun onPlayerLogin(player: ServerPlayer) {
// Disables spawning
if (!Cobblemon.config.enableSpawning || player.level().gameRules.getBoolean(CobblemonGameRules.DO_POKEMON_SPAWNING) == false) {
return;
}
registerPlayerSpawner(player)
}
fun onPlayerLogout(player: ServerPlayer) {
unregisterPlayerSpawner(player)
}
fun onDimensionChange(player: ServerPlayer, origin: ServerLevel?, destination: ServerLevel?) {
fun getSpawnRule(level: ServerLevel?) = level?.gameRules?.getBoolean(CobblemonGameRules.DO_POKEMON_SPAWNING)
if (!Cobblemon.config.enableSpawning || getSpawnRule(origin) == getSpawnRule(destination)) {
return
}
val spawnerAction = if (destination?.gameRules?.getBoolean(CobblemonGameRules.DO_POKEMON_SPAWNING) == false) {
::unregisterPlayerSpawner
} else {
::registerPlayerSpawner
}
spawnerAction(player)
}
private fun unregisterPlayerSpawner(player: ServerPlayer) {
spawnersForPlayers.remove(player.uuid)?.let { unregisterSpawner(it) }
}
private fun registerPlayerSpawner(player: ServerPlayer) {
PlayerSpawnerFactory.create(this, player).also {
spawnersForPlayers[player.uuid] = it
registerSpawner(it)
}
}
}
@@ -17,16 +17,21 @@ import net.minecraft.world.entity.EntityType
open class SpawnCause(
val spawner: Spawner,
entity: Entity? = null
): SpawningInfluence {
) : SpawningInfluence {
val entityWorldId = entity?.level()?.dimension()
val entityId = entity?.id
val entityUUID = entity?.uuid
val entityType = entity?.type
val entity: Entity?
get() = if (entityType == EntityType.PLAYER) {
server()?.playerList?.getPlayer(entityUUID!!)
} else {
server()?.getLevel(entityWorldId!!)?.getEntity(entityId!!)
}
get() =
if (entityType == EntityType.PLAYER) {
entityUUID?.let {
server()?.playerList?.getPlayer(it)
}
} else if (entityWorldId != null && entityId != null) {
server()?.getLevel(entityWorldId)?.getEntity(entityId)
} else {
null
}
}
@@ -1,85 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.api.spawning.spawner.TickingSpawner
import com.cobblemon.mod.common.util.server
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules.DO_POKEMON_SPAWNING
/**
* A manager of various spawners. This is a class in which you should register
* ticking spawners so that they can be automatically ticked by the server.
* This also supports registering [SpawningInfluence]s to apply to each spawner
* that is registered.
*
* @author Hiroku
* @since February 5th, 2022
*/
open class SpawnerManager {
val spawners = mutableListOf<Spawner>()
get() {
if (server()?.isSameThread == false) {
Cobblemon.LOGGER.error("Illegal access to spawners list from non-server thread!")
Exception().printStackTrace()
}
return field
}
val influences = mutableListOf<SpawningInfluence>()
get() {
if (server()?.isSameThread == false) {
Cobblemon.LOGGER.error("Illegal access to influences from non-server thread!")
Exception().printStackTrace()
}
return field
}
inline fun <reified T : Spawner> getSpawnersOfType() = spawners.filterIsInstance<T>()
open fun getSpawnerByName(name: String) = spawners.find { it.name == name }
open fun registerSpawner(spawner: Spawner) {
spawners.add(spawner)
if (spawner !is TickingSpawner) {
spawner.influences.addAll(influences)
}
}
open fun unregisterSpawner(spawner: Spawner) {
spawners.remove(spawner)
if (spawner !is TickingSpawner) {
spawner.influences.removeAll(influences)
}
}
open fun onConfigReload() {
spawners.filterIsInstance<PlayerSpawner>().forEach {
it.ticksBetweenSpawns = Cobblemon.config.ticksBetweenSpawnAttempts
}
}
open fun onServerStarted() {
spawners.clear()
}
open fun onServerTick() {
// Disables spawning
if (!Cobblemon.config.enableSpawning) {
return
}
influences.removeIf { it.isExpired() }
getSpawnersOfType<TickingSpawner>().forEach {
if (it.getCauseEntity()?.level()?.gameRules?.getBoolean(DO_POKEMON_SPAWNING) == false) {
return@forEach
}
it.tick()
}
}
}
@@ -39,9 +39,13 @@ import net.minecraft.server.MinecraftServer
*/
abstract class SpawnDetail : ModDependant {
companion object {
val pokemonTypes = mutableListOf<String>()
val spawnDetailTypes = mutableMapOf<String, RegisteredSpawnDetail<*>>()
fun <T : SpawnDetail> registerSpawnType(name: String, detailClass: Class<T>) {
spawnDetailTypes[name] = RegisteredSpawnDetail(detailClass)
if (detailClass == PokemonSpawnDetail::class.java || detailClass == PokemonHerdSpawnDetail::class.java) {
pokemonTypes.add(name)
}
}
}
@@ -24,6 +24,7 @@ import com.cobblemon.mod.common.util.server
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
@@ -72,6 +73,13 @@ class SpawnPool(val name: String) : JsonDataRegistry<SpawnSet>, Iterable<SpawnDe
return this
}
fun onServerLoad(server: MinecraftServer) {
for (detail in details) {
detail.onServerLoad(server)
}
precalculate()
}
/**
* Precalculates spawns into hash mappings using the [precalculators] included
* in this pool as well as the range of spawnable positions mentioned in the pool.
@@ -1,38 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.fishing
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.position.FishingSpawnablePosition
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.spawner.TriggerSpawner
import net.minecraft.server.level.ServerLevel
import net.minecraft.core.BlockPos
/**
* A spawner that takes in a [FishingSpawnCause] and spawns things.
*
* @author Hiroku
* @since February 3rd, 2024
*/
open class FishingSpawner(
override val name: String = "fishing",
pool: SpawnPool = CobblemonSpawnPools.WORLD_SPAWN_POOL
) : TriggerSpawner<FishingSpawnCause>(name, pool) {
override fun parseSpawnablePosition(cause: FishingSpawnCause, world: ServerLevel, pos: BlockPos): FishingSpawnablePosition? {
// Maybe confirm that it's water we're fishing in?
val spawnablePosition = FishingSpawnablePosition(
cause = cause,
world = world,
pos = pos,
influences = influences
)
return spawnablePosition
}
}
@@ -0,0 +1,35 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.influence
import com.cobblemon.mod.common.api.spawning.SpawnBucket
/**
* A [SpawningInfluence] that multiplies bucket weights by the provided values
* and then normalizes the resulting weights so that their total adds up to 100.
*/
class BucketMultiplyingInfluence(
val multipliers: Map<String, Float>,
) : SpawningInfluence {
override fun affectBucketWeights(bucketWeights: MutableMap<SpawnBucket, Float>) {
val updates = mutableMapOf<SpawnBucket, Float>()
for ((bucket, weight) in bucketWeights) {
val multiplier = multipliers[bucket.name]
if (multiplier != null) {
updates[bucket] = weight * multiplier
}
}
for ((bucket, newWeight) in updates) {
bucketWeights[bucket] = newWeight
}
normalizeBucketWeights(bucketWeights)
}
}
@@ -44,11 +44,6 @@ open class BucketNormalizingInfluence(
bucketWeights[it] = weight.toDouble().pow(1 / normalizationFactor).toFloat()
}
val sum = bucketWeights.values.sum()
val to100 = 100F / sum
bucketWeights.keys.toList().forEach {
val weight = bucketWeights[it] ?: return@forEach
bucketWeights[it] = weight * to100
}
normalizeBucketWeights(bucketWeights)
}
}
@@ -40,6 +40,15 @@ interface SpawningInfluence {
fun affectSpawn(action: SpawnAction<*>, entity: Entity) {}
/** Applies some influence over the weight of spawn buckets. */
fun affectBucketWeights(bucketWeights: MutableMap<SpawnBucket, Float>) {}
/** Normalizes all bucket weights to add up to 100 */
fun normalizeBucketWeights(bucketWeights: MutableMap<SpawnBucket, Float>) {
val sum = bucketWeights.values.sum()
val to100 = 100F / sum
bucketWeights.keys.toList().forEach {
val weight = bucketWeights[it] ?: return@forEach
bucketWeights[it] = weight * to100
}
}
/** Filters positions that could be usable for a spawnable position calculator */
fun isAllowedPosition(world: ServerLevel, pos: BlockPos, spawnablePositionCalculator: SpawnablePositionCalculator<*, *>): Boolean = true
/**
@@ -9,16 +9,15 @@
package com.cobblemon.mod.common.api.spawning.influence.detector
import com.cobblemon.mod.common.api.spawning.SpawningZone
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
import com.cobblemon.mod.common.api.spawning.influence.SpawningZoneInfluence
import com.cobblemon.mod.common.api.spawning.prospecting.IncenseSweetDetector
import com.cobblemon.mod.common.api.spawning.prospecting.SaccharineLogSlatheredDetector
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.api.spawning.spawner.SpawningZoneInput
import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.level.block.state.BlockState
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
import com.cobblemon.mod.common.api.spawning.prospecting.IncenseSweetDetector
import com.cobblemon.mod.common.api.spawning.prospecting.LurePokeSnackDetector
import com.cobblemon.mod.common.api.spawning.prospecting.SaccharineLogSlatheredDetector
/**
* Prospects for [SpawningZoneInfluence] in a world at a given position. Occurs as part of a
@@ -31,7 +30,6 @@ interface SpawningInfluenceDetector {
companion object {
@JvmStatic
val detectors = mutableSetOf<SpawningInfluenceDetector>(
LurePokeSnackDetector,
SaccharineLogSlatheredDetector,
IncenseSweetDetector
)
@@ -14,13 +14,15 @@ import net.minecraft.server.level.ServerLevel
import net.minecraft.core.BlockPos
/**
* A spawnable position relating to triggered spawning. Uses a single-point structure cache which is
* the only contribution of this implementation.
* A spawnable position relating to single-point spawning. Uses a single-point structure cache which is
* the only contribution of this implementation really. By contrast, the area spawnable position provides
* a structure cache that's shared across the entire zone that was used for spawning, which is great for
* performance. That kind of optimization shouldn't be necessary for single-point spawning.
*
* @author Hiroku
* @since February 3rd, 2024
*/
open class TriggerSpawnablePosition(
open class BasicSpawnablePosition(
override val cause: SpawnCause,
override val world: ServerLevel,
override val position: BlockPos,
@@ -29,6 +31,6 @@ open class TriggerSpawnablePosition(
override val canSeeSky: Boolean,
override val influences: MutableList<SpawningInfluence>
) : SpawnablePosition() {
val triggerStructureCache = StructureChunkCache()
override fun getStructureCache(pos: BlockPos) = triggerStructureCache
val singlePointStructureCache = StructureChunkCache()
override fun getStructureCache(pos: BlockPos) = singlePointStructureCache
}
@@ -28,7 +28,7 @@ class FishingSpawnablePosition(
world: ServerLevel,
pos: BlockPos,
influences: MutableList<SpawningInfluence>
) : TriggerSpawnablePosition(
) : BasicSpawnablePosition(
cause = cause,
world = world,
position = pos,
@@ -36,6 +36,7 @@ interface SpawnablePositionCalculator<I : SpawnablePositionInput, O : SpawnableP
val isLavaCondition: (BlockState) -> Boolean = { it.fluidState.`is`(FluidTags.LAVA) && it.fluidState.isSource }
private val calculators = PrioritizedList<SpawnablePositionCalculator<*, *>>()
val prioritizedAreaCalculators: List<AreaSpawnablePositionCalculator<*>>
get() = calculators.filterIsInstance<AreaSpawnablePositionCalculator<*>>()
@@ -1,189 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.prospecting
import com.cobblemon.mod.common.CobblemonPoiTypes
import com.cobblemon.mod.common.api.fishing.SpawnBait
import com.cobblemon.mod.common.api.spawning.influence.BucketNormalizingInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpatialSpawningZoneInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawnBaitInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawningZoneInfluence
import com.cobblemon.mod.common.api.spawning.influence.UnconditionalSpawningZoneInfluence
import com.cobblemon.mod.common.api.spawning.influence.detector.SpawningInfluenceDetector
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.api.spawning.spawner.SpawningZoneInput
import com.cobblemon.mod.common.block.PokeSnackBlock
import com.cobblemon.mod.common.block.entity.PokeSnackBlockEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.effect.PokeSnackBlockParticlesPacket
import com.cobblemon.mod.common.util.math.pow
import com.cobblemon.mod.common.util.toBlockPos
import kotlin.math.ceil
import kotlin.math.sqrt
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.core.Holder
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.ai.village.poi.PoiManager
import net.minecraft.world.entity.ai.village.poi.PoiType
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.phys.Vec3
import kotlin.math.abs
object LurePokeSnackDetector : SpawningInfluenceDetector {
@JvmField
val RANGE: Int = 48
@JvmField
val POKE_SNACK_CRUMBED_ASPECT = "poke_snack_crumbed"
override fun detectFromInput(spawner: Spawner, input: SpawningZoneInput): MutableList<SpawningZoneInfluence> {
val world = input.world
val listOfInfluences = mutableListOf<SpawningZoneInfluence>()
val searchRange = RANGE + ceil(sqrt(((input.length pow 2) + (input.width pow 2)).toDouble())).toInt()
val lurePokeSnackPositions = world.poiManager.findAll(
{ holder: Holder<PoiType> -> holder.`is`(CobblemonPoiTypes.POKE_SNACK_KEY) },
{ true },
input.getCenter().toBlockPos(),
searchRange,
PoiManager.Occupancy.ANY
).toList()
var highestLureTier = 0
for (lurePokeSnackPos in lurePokeSnackPositions) {
val blockState = world.getBlockState(lurePokeSnackPos)
if (blockState.block !is PokeSnackBlock) continue
val blockEntity = world.getBlockEntity(lurePokeSnackPos) as? PokeSnackBlockEntity ?: continue
val baitEffects = blockEntity.getBaitEffects()
highestLureTier = maxOf(highestLureTier, baitEffects.filter { it.type == SpawnBait.Effects.RARITY_BUCKET }.maxOfOrNull { it.value }?.toInt() ?: 0)
listOfInfluences.add(
SpatialSpawningZoneInfluence(
pos = lurePokeSnackPos,
radius = RANGE.toFloat(),
influence = SpawnBaitInfluence(
effects = baitEffects,
onUsed = { usedAmount, entity ->
entity?.let {
// Prevent triggering twice on same entity
if (usedAmount % 2 == 0) {
tryBite(world, lurePokeSnackPos)
moveAndApplyAspect(world, lurePokeSnackPos, entity)
}
}
}
)
)
)
}
if (highestLureTier > 0) {
listOfInfluences.add(UnconditionalSpawningZoneInfluence(influence = BucketNormalizingInfluence(tier = highestLureTier)))
}
return listOfInfluences
}
private fun tryBite(level: ServerLevel, blockPos: BlockPos) {
if (level.isLoaded(blockPos)) {
val blockState = level.getBlockState(blockPos)
val block = blockState.block as? PokeSnackBlock ?: return
block.eat(level, blockPos, blockState, null)
}
}
private fun moveAndApplyAspect(level: ServerLevel, blockPos: BlockPos, entity: PokemonEntity?) {
entity?.let {
entity.pokemon.forcedAspects += POKE_SNACK_CRUMBED_ASPECT
attemptSafeMove(level, blockPos, entity)
}
}
fun attemptSafeMove(level: ServerLevel, startPos: BlockPos, entity: PokemonEntity) {
val inWater = entity.isInWater
val maxDistance = 10 // maximum search radius
val maxPositions = 10 // maximum number of positions at the closest distance
var foundPositions: List<BlockPos> = emptyList()
// Search layer by layer, starting from the closest
for (distance in 1..maxDistance) {
val positionsAtThisDistance = mutableListOf<BlockPos>()
for (dx in -distance..distance) {
for (dy in -distance..distance) {
for (dz in -distance..distance) {
// Consider only positions on the surface of the cube at current distance
if (listOf(dx, dy, dz).count { abs(it) != 0 } == distance) {
val candidatePos = startPos.offset(dx, dy, dz)
if (!candidatePos.equals(startPos.relative(Direction.UP))) {
// Check collision
val currentBox = entity.boundingBox.move(
candidatePos.x - entity.x,
candidatePos.y - entity.y,
candidatePos.z - entity.z
)
if (level.noCollision(entity, currentBox)) {
val blockBelowPos = candidatePos.below()
val blockState = level.getBlockState(blockBelowPos)
val isWater = blockState.block == Blocks.WATER
if (inWater) {
if (isWater) {
positionsAtThisDistance.add(candidatePos)
}
} else {
if (!blockState.isAir) {
positionsAtThisDistance.add(candidatePos)
}
}
}
}
}
}
}
}
if (positionsAtThisDistance.isNotEmpty()) {
// Limit to 10 nearest positions (all at this distance)
foundPositions = if (positionsAtThisDistance.size > maxPositions) {
positionsAtThisDistance.shuffled().take(maxPositions)
} else {
positionsAtThisDistance
}
break // Found the nearest layer with safe positions, stop expanding
}
}
if (foundPositions.isNotEmpty()) {
val chosenPos = foundPositions.random()
val centerX = chosenPos.x + 0.5
val centerZ = chosenPos.z + 0.5
entity.moveTo(Vec3(centerX, chosenPos.y.toDouble(), centerZ))
PokeSnackBlockParticlesPacket(startPos, chosenPos).sendToPlayersAround(
startPos.x.toDouble(),
startPos.y.toDouble(),
startPos.z.toDouble(),
64.0,
level.dimension()
)
}
}
override fun detectFromBlock(level: ServerLevel, pos: BlockPos, blockState: BlockState) = emptyList<SpawningZoneInfluence>()
}
@@ -24,6 +24,11 @@ import com.cobblemon.mod.common.api.spawning.spawner.Spawner
* @since January 31st, 2022
*/
interface SpawningSelector<T : SpawnSelectionData> {
companion object {
@JvmField
val DEFAULT = FlatSpawnablePositionWeightedSelector()
}
fun getSelectionData(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>): T
fun selectSpawnAction(
@@ -32,12 +37,12 @@ interface SpawningSelector<T : SpawnSelectionData> {
selectionData: T
): SpawnAction<*>?
fun select(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>, max: Int): List<SpawnAction<*>> {
fun select(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>, maxSpawns: Int): List<SpawnAction<*>> {
val selectionData = getSelectionData(spawner, bucket, spawnablePositions)
val spawnActions = selectionData.spawnActions
while (spawnActions.size < max) {
while (spawnActions.size < maxSpawns) {
val spawnAction = selectSpawnAction(
spawner = spawner,
bucket = bucket,
@@ -1,180 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.SpawnerManager
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePositionResolver
import com.cobblemon.mod.common.api.spawning.position.calculators.AreaSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SpawnablePositionCalculator.Companion.prioritizedAreaCalculators
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.isBoxLoaded
import com.cobblemon.mod.common.util.squeezeWithinBounds
import com.cobblemon.mod.common.util.toVec3f
import net.minecraft.core.BlockPos
import net.minecraft.core.SectionPos
import net.minecraft.world.level.ChunkPos
import net.minecraft.world.level.Level
import net.minecraft.world.level.chunk.ChunkAccess
import net.minecraft.world.level.chunk.status.ChunkStatus
import net.minecraft.world.level.pathfinder.PathComputationType
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
/**
* A type of [TickingSpawner] that operates within some area. When this spawner type
* is told to do a spawning action, the subclass can provide a [SpawningZoneInput] to use.
* If a non-null value is returned, the [spawningZoneGenerator] and [spawnablePositionResolver] will be used to
* select a spawn and action it.
*
* Subclasses must implement the function retrieving what area to do the spawning in,
* but otherwise this class is feature-complete.
*
* @author Hiroku
* @since February 5th, 2022
*/
abstract class AreaSpawner(
name: String,
spawns: SpawnPool,
manager: SpawnerManager
) : TickingSpawner(name, spawns, manager) {
abstract fun getZoneInput(cause: SpawnCause): SpawningZoneInput?
companion object {
const val CHUNK_REACH = 3
}
var spawningZoneGenerator: SpawningZoneGenerator = Cobblemon.spawningZoneGenerator
var spawnablePositionResolver: AreaSpawnablePositionResolver = Cobblemon.areaSpawnablePositionResolver
var spawnablePositionCalculators: List<AreaSpawnablePositionCalculator<*>> = prioritizedAreaCalculators
var spawnsPerPass: Int? = null
override fun run(cause: SpawnCause): List<SpawnAction<*>> {
val areaInput = getZoneInput(cause)
val constrainedArea = if (areaInput != null) constrainArea(areaInput) else null
if (constrainedArea != null) {
val areaBox = AABB.ofSize(
Vec3(constrainedArea.getCenter().toVec3f()),
CHUNK_REACH * 16.0 * 2,
1000.0,
CHUNK_REACH * 16.0 * 2
)
if (!constrainedArea.world.isBoxLoaded(areaBox)) {
return emptyList()
}
val numberNearby = constrainedArea.world.getEntitiesOfClass(
PokemonEntity::class.java,
areaBox,
PokemonEntity::countsTowardsSpawnCap
).size
val chunksCovered = CHUNK_REACH * CHUNK_REACH
if (numberNearby.toFloat() / chunksCovered >= Cobblemon.config.pokemonPerChunk) {
return emptyList()
}
//val prospectStart = System.currentTimeMillis()
val zone = spawningZoneGenerator.generate(this, constrainedArea)
//val prospectEnd = System.currentTimeMillis()
val spawnablePositions = spawnablePositionResolver.resolve(this, spawnablePositionCalculators, zone)
val influences = this.getAllInfluences() + zone.unconditionalInfluences
val bucket = chooseBucket(cause, influences)
//val resolveEnd = System.currentTimeMillis()
//val prospectDuration = prospectEnd - prospectStart
//val resolveDuration = resolveEnd - prospectEnd
//println("Prospecting took: $prospectDuration ms. Resolution took: $resolveDuration ms")
// Takes about 3ms on my laptop to prospect, similar to spawnable position resolve - not very good, needs some thought
return getSpawningSelector().select(spawner = this, bucket = bucket, spawnablePositions = spawnablePositions, max = spawnsPerPass ?: Cobblemon.config.maximumSpawnsPerPass)
}
return emptyList()
}
fun isValidStartPoint(world: Level, chunk: ChunkAccess, startPos: BlockPos.MutableBlockPos): Boolean {
val y = startPos.y
if (!world.isLoaded(startPos) || !world.isLoaded(startPos.setY(y + 1))) {
return false
}
val mid = chunk.getBlockState(startPos.setY(y))
val above = chunk.getBlockState(startPos.setY(y + 1))
// Above must be non-solid
if (!above.isPathfindable(PathComputationType.AIR)) {
return false
}
// Position must be non-air
if (mid.isAir) {
return false
}
return true
}
fun constrainArea(area: SpawningZoneInput): SpawningZoneInput? {
val basePos = BlockPos.MutableBlockPos(area.baseX, area.baseY, area.baseZ)
val originalY = area.baseY
val (chunkX, chunkZ) = Pair(SectionPos.blockToSectionCoord(area.baseX), SectionPos.blockToSectionCoord(area.baseZ))
// if the chunk isn't loaded, we don't want to go further & we don't want the getChunk function below to load/create the chunk.
if (!area.world.areEntitiesLoaded(ChunkPos.asLong(chunkX, chunkZ))) return null
val chunk = area.world.getChunk(chunkX, chunkZ, ChunkStatus.FULL) ?: return null
var valid = isValidStartPoint(area.world, chunk, basePos)
if (!valid) {
var offset = 1
do {
if (isValidStartPoint(area.world, chunk, basePos.setY(originalY + offset))) {
valid = true
basePos.y = originalY + offset
break
} else if (isValidStartPoint(area.world, chunk, basePos.setY(originalY - offset))) {
valid = true
basePos.y = originalY - offset
break
}
offset++
} while (offset <= Cobblemon.config.maxVerticalCorrectionBlocks)
}
if (valid) {
val min = area.world.squeezeWithinBounds(basePos)
val max = area.world.squeezeWithinBounds(basePos.move(area.length, area.height, area.width))
if (area.world.isLoaded(min) && area.world.isLoaded(max) &&
min.x < max.x && min.y < max.y && min.z < max.z
) {
return SpawningZoneInput(
cause = area.cause,
world = area.world,
baseX = min.x,
baseY = min.y,
baseZ = min.z,
length = max.x - min.x,
height = max.y - min.y,
width = max.z - min.z
)
}
}
return null
}
}
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePositionResolver
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
/**
* A basic spawner implementation that uses Cobblemon's default algorithms for zone generation,
* position resolving, and spawn selection.
*
* @author Hiroku
* @since October 27th, 2025
*/
open class BasicSpawner(
override val name: String,
override var spawnPool: SpawnPool,
override var maxPokemonPerChunk: Float = Cobblemon.config.pokemonPerChunk
) : Spawner {
override var selector: SpawningSelector<*> = SpawningSelector.DEFAULT
override var generator: SpawningZoneGenerator = Cobblemon.spawningZoneGenerator
override var resolver: AreaSpawnablePositionResolver = Cobblemon.areaSpawnablePositionResolver
override val influences = mutableListOf<SpawningInfluence>()
}
@@ -8,33 +8,37 @@
package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.SpawnerManager
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import net.minecraft.server.level.ServerLevel
import net.minecraft.core.BlockPos
/**
* A spawner that works within a fixed area, and ticks on its own. The location
* and size of the area is necessary for construction, but after that this spawner
* can be registered in a [SpawnerManager] and left to its own devices.
* A spawner that works within a fixed area. Instances provide the center point and the radii and then the spawner
* can be told to [run] and it will spawn Pokémon within that area.
*
* @author Hiroku
* @since February 5th, 2022
*/
open class FixedAreaSpawner(
name: String,
spawns: SpawnPool,
manager: SpawnerManager,
spawnPool: SpawnPool,
val world: ServerLevel,
val position: BlockPos,
val horizontalRadius: Int,
val verticalRadius: Int,
override var ticksBetweenSpawns: Float = 20F
) : AreaSpawner(name, spawns, manager) {
override fun getZoneInput(cause: SpawnCause): SpawningZoneInput? {
val basePos = position.offset(-horizontalRadius, -verticalRadius, -horizontalRadius)
maxPokemonPerChunk: Float = Cobblemon.config.pokemonPerChunk
) : BasicSpawner(name, spawnPool, maxPokemonPerChunk) {
fun run(cause: SpawnCause, maxSpawns: Int? = null): List<Any> {
return runForArea(
zoneInput = getZoneInput(cause),
maxSpawns = maxSpawns
)
}
fun getZoneInput(cause: SpawnCause): SpawningZoneInput {
val basePos = position.offset(-horizontalRadius, -verticalRadius, -horizontalRadius)
return SpawningZoneInput(
cause = cause,
world = world,
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.Cobblemon.config
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.SpawnerManager
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.util.getPlayer
import com.cobblemon.mod.common.util.nextBetween
@@ -31,11 +30,16 @@ import kotlin.random.Random
* @author Hiroku
* @since February 14th, 2022
*/
class PlayerSpawner(player: ServerPlayer, spawns: SpawnPool, manager: SpawnerManager) : AreaSpawner(player.name.string, spawns, manager) {
class PlayerSpawner(
player: ServerPlayer,
spawnPool: SpawnPool
) : BasicSpawner(
name = player.name.string,
spawnPool = spawnPool
) {
val uuid: UUID = player.uuid
override var ticksBetweenSpawns = config.ticksBetweenSpawnAttempts
override fun getCauseEntity() = uuid.getPlayer()
override fun getZoneInput(cause: SpawnCause): SpawningZoneInput? {
fun getZoneInput(cause: SpawnCause): SpawningZoneInput? {
val player = uuid.getPlayer() ?: return null
val zoneDiameter = config.spawningZoneDiameter
val zoneHeight = config.spawningZoneHeight
@@ -53,6 +57,7 @@ class PlayerSpawner(player: ServerPlayer, spawns: SpawnPool, manager: SpawnerMan
} else {
thetatemp
}
val x = center.x + r * cos(theta)
val z = center.z + r * sin(theta)
@@ -67,4 +72,22 @@ class PlayerSpawner(player: ServerPlayer, spawns: SpawnPool, manager: SpawnerMan
width = zoneDiameter
)
}
var active = true
var ticksUntilNextSpawn = 100F
var ticksBetweenSpawns: Float = config.ticksBetweenSpawnAttempts
var tickTimerMultiplier = 1F
fun tick() {
if (!active) {
return
}
ticksUntilNextSpawn -= tickTimerMultiplier
if (ticksUntilNextSpawn <= 0) {
val zoneInput = getZoneInput(cause = SpawnCause(spawner = this, entity = uuid.getPlayer()))
zoneInput?.let(::runForArea)
ticksUntilNextSpawn = ticksBetweenSpawns
}
}
}
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnRules
import com.cobblemon.mod.common.api.spawning.SpawnerManager
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence.Companion.TYPICAL_VARIATION
@@ -28,7 +27,7 @@ import net.minecraft.server.level.ServerPlayer
* @since February 14th, 2022
*/
object PlayerSpawnerFactory {
var spawns: SpawnPool by mutableLazy {
var spawnPool: SpawnPool by mutableLazy {
CobblemonSpawnPools.WORLD_SPAWN_POOL
}
@@ -46,9 +45,9 @@ object PlayerSpawnerFactory {
)
})
fun create(spawnerManager: SpawnerManager, player: ServerPlayer): PlayerSpawner {
fun create(player: ServerPlayer): PlayerSpawner {
val influences = influenceBuilders.mapNotNull { it(player) }
return PlayerSpawner(player, spawns, spawnerManager).also {
return PlayerSpawner(player, spawnPool).also {
it.influences.addAll(influences)
it.influences.addAll(
CobblemonSpawnRules.rules.values.filter(SpawnRule::enabled).flatMap(SpawnRule::components)
@@ -14,33 +14,67 @@ import com.cobblemon.mod.common.api.events.entity.SpawnBucketChosenEvent
import com.cobblemon.mod.common.api.spawning.BestSpawner
import com.cobblemon.mod.common.api.spawning.SpawnBucket
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
import com.cobblemon.mod.common.api.spawning.detail.EntitySpawnResult
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePositionResolver
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.position.calculators.SpawnablePositionCalculator.Companion.prioritizedAreaCalculators
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.isBoxLoaded
import com.cobblemon.mod.common.util.squeezeWithinBounds
import com.cobblemon.mod.common.util.toVec3f
import com.cobblemon.mod.common.util.weightedSelection
import kotlin.Any
import kotlin.Boolean
import kotlin.Float
import kotlin.Int
import kotlin.Pair
import kotlin.String
import kotlin.collections.plus
import kotlin.math.max
import net.minecraft.core.BlockPos
import net.minecraft.core.SectionPos
import net.minecraft.world.level.ChunkPos
import net.minecraft.world.level.Level
import net.minecraft.world.level.chunk.ChunkAccess
import net.minecraft.world.level.chunk.status.ChunkStatus
import net.minecraft.world.level.pathfinder.PathComputationType
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
/**
* Interface representing something that performs the action of spawning. Various functions
* exist to streamline the process of using the [BestSpawner].
*
* The simplest implementation is a [BasicSpawner]
*
* @author Hiroku
* @since January 24th, 2022
*/
interface Spawner {
val name: String
val influences: MutableList<SpawningInfluence>
fun getSpawningSelector(): SpawningSelector<*>
fun setSpawningSelector(selector: SpawningSelector<*>)
fun getSpawnPool(): SpawnPool
fun setSpawnPool(spawnPool: SpawnPool)
var spawnPool: SpawnPool
var selector: SpawningSelector<*>
var generator: SpawningZoneGenerator
var resolver: AreaSpawnablePositionResolver
var maxPokemonPerChunk: Float
fun <R> afterSpawn(action: SpawnAction<R>, result: R) {}
companion object {
/** The chunk range radius over which spawning will roughly keep the entity density under control. */
const val ENTITY_LIMIT_CHUNK_RANGE = 3
}
fun getMatchingSpawns(bucket: SpawnBucket, spawnablePosition: SpawnablePosition): List<SpawnDetail> {
val spawns = mutableListOf<SpawnDetail>()
spawns.addAll(getSpawnPool().retrieve(bucket, spawnablePosition).filter { it.isSatisfiedBy(spawnablePosition) })
spawns.addAll(spawnPool.retrieve(bucket, spawnablePosition).filter { it.isSatisfiedBy(spawnablePosition) })
spawnablePosition.influences.forEach { influence ->
val influencedSpawns = influence.injectSpawns(bucket, spawnablePosition)
if (influencedSpawns != null) {
@@ -50,6 +84,101 @@ interface Spawner {
return spawns
}
/**
* Runs the spawner for a specific [SpawnablePosition] and calculates a [SpawnAction] that could spawn there.
* This does not trigger the resulting spawn. You can trigger the spawn in the same action by running
* [runForPosition] instead or manually complete the [SpawnAction] it returns using [SpawnAction.complete].
*/
fun calculateSpawnActionForPosition(
cause: SpawnCause,
spawnablePosition: SpawnablePosition,
): SpawnAction<*>? {
influences.removeIf { it.isExpired() }
spawnablePosition.influences.addAll(influences)
val bucket = chooseBucket(cause, spawnablePosition.influences)
return selector.select(
spawner = this,
bucket = bucket,
spawnablePositions = listOf(spawnablePosition),
maxSpawns = 1
).firstOrNull()
}
fun calculateSpawnActionsForArea(
zoneInput: SpawningZoneInput,
maxSpawns: Int?
): List<SpawnAction<*>> {
val maxSpawns = maxSpawns ?: Cobblemon.config.maximumSpawnsPerPass
influences.removeIf { it.isExpired() }
val constrainedArea = constrainArea(zoneInput)
?: return emptyList()
val areaBox = AABB.ofSize(
Vec3(constrainedArea.getCenter().toVec3f()),
ENTITY_LIMIT_CHUNK_RANGE * 16.0 * 2,
1000.0,
ENTITY_LIMIT_CHUNK_RANGE * 16.0 * 2
)
if (!constrainedArea.world.isBoxLoaded(areaBox)) {
return emptyList()
}
val numberNearby = constrainedArea.world.getEntitiesOfClass(
PokemonEntity::class.java,
areaBox,
PokemonEntity::countsTowardsSpawnCap
).size
val chunksCovered = ENTITY_LIMIT_CHUNK_RANGE * ENTITY_LIMIT_CHUNK_RANGE
val maxPokemonPerChunk = max(Cobblemon.config.pokemonPerChunk, zoneInput.cause.spawner.maxPokemonPerChunk)
if (numberNearby.toFloat() / chunksCovered >= maxPokemonPerChunk) {
return emptyList()
}
val zone = generator.generate(this, constrainedArea)
val spawnablePositions = resolver.resolve(this, prioritizedAreaCalculators, zone)
val influences = influences + zone.unconditionalInfluences
val bucket = chooseBucket(zoneInput.cause, influences)
return selector.select(
spawner = this,
bucket = bucket,
spawnablePositions = spawnablePositions,
maxSpawns = maxSpawns
)
}
/**
* Runs the spawner at the given [SpawnablePosition] and returns whatever the calculated [SpawnAction] produces,
* if any spawn occurred. For most implementations of [SpawnAction], the resulting type is
* [EntitySpawnResult]. Technically you can make spawns for anything though.
*/
fun runForPosition(
cause: SpawnCause,
spawnablePosition: SpawnablePosition
): Any? {
val action = calculateSpawnActionForPosition(
cause = cause,
spawnablePosition = spawnablePosition
) ?: return null
return action.complete()
}
fun runForArea(
zoneInput: SpawningZoneInput,
maxSpawns: Int? = null
): List<Any> {
val spawnActions = calculateSpawnActionsForArea(zoneInput = zoneInput, maxSpawns = maxSpawns)
val results = mutableListOf<Any>()
for (spawnAction in spawnActions) {
spawnAction.complete()?.let(results::add)
}
return results
}
fun copyInfluences() = influences.filter { !it.isExpired() }.toMutableList()
fun chooseBucket(cause: SpawnCause, influences: List<SpawningInfluence>): SpawnBucket {
@@ -66,4 +195,77 @@ interface Spawner {
CobblemonEvents.SPAWN_BUCKET_CHOSEN.post(event)
return event.bucket
}
fun isValidStartPoint(world: Level, chunk: ChunkAccess, startPos: BlockPos.MutableBlockPos): Boolean {
val y = startPos.y
if (!world.isLoaded(startPos) || !world.isLoaded(startPos.setY(y + 1))) {
return false
}
val mid = chunk.getBlockState(startPos.setY(y))
val above = chunk.getBlockState(startPos.setY(y + 1))
// Above must be non-solid
if (!above.isPathfindable(PathComputationType.AIR)) {
return false
}
// Position must be non-air
if (mid.isAir) {
return false
}
return true
}
fun constrainArea(area: SpawningZoneInput): SpawningZoneInput? {
val basePos = BlockPos.MutableBlockPos(area.baseX, area.baseY, area.baseZ)
val originalY = area.baseY
val (chunkX, chunkZ) = Pair(SectionPos.blockToSectionCoord(area.baseX), SectionPos.blockToSectionCoord(area.baseZ))
// if the chunk isn't loaded, we don't want to go further & we don't want the getChunk function below to load/create the chunk.
if (!area.world.areEntitiesLoaded(ChunkPos.asLong(chunkX, chunkZ))) return null
val chunk = area.world.getChunk(chunkX, chunkZ, ChunkStatus.FULL) ?: return null
var valid = isValidStartPoint(area.world, chunk, basePos)
if (!valid) {
var offset = 1
do {
if (isValidStartPoint(area.world, chunk, basePos.setY(originalY + offset))) {
valid = true
basePos.y = originalY + offset
break
} else if (isValidStartPoint(area.world, chunk, basePos.setY(originalY - offset))) {
valid = true
basePos.y = originalY - offset
break
}
offset++
} while (offset <= Cobblemon.config.maxVerticalCorrectionBlocks)
}
if (valid) {
val min = area.world.squeezeWithinBounds(basePos)
val max = area.world.squeezeWithinBounds(basePos.move(area.length, area.height, area.width))
if (area.world.isLoaded(min) && area.world.isLoaded(max) &&
min.x < max.x && min.y < max.y && min.z < max.z
) {
return SpawningZoneInput(
cause = area.cause,
world = area.world,
baseX = min.x,
baseY = min.y,
baseZ = min.z,
length = max.x - min.x,
height = max.y - min.y,
width = max.z - min.z
)
}
}
return null
}
}
@@ -1,89 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.SpawnerManager
import com.cobblemon.mod.common.api.spawning.detail.EntitySpawnResult
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.selection.FlatSpawnablePositionWeightedSelector
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
import net.minecraft.world.entity.Entity
/**
* A spawner that regularly attempts spawning entities. It has timing utilities,
* and subclasses must provide the logic for generating a spawn which is called
* periodically by the server.
*
* @author Hiroku
* @since February 5th, 2022
*/
abstract class TickingSpawner(
override val name: String,
var spawns: SpawnPool,
val manager: SpawnerManager
) : Spawner {
private var selector: SpawningSelector<*> = FlatSpawnablePositionWeightedSelector()
override val influences = mutableListOf<SpawningInfluence>()
override fun getSpawningSelector() = selector
override fun setSpawningSelector(selector: SpawningSelector<*>) { this.selector = selector }
override fun getSpawnPool() = spawns
override fun setSpawnPool(spawnPool: SpawnPool) { spawns = spawnPool }
abstract fun run(cause: SpawnCause): List<SpawnAction<*>>
var active = true
val spawnedEntities = mutableListOf<Entity>()
var lastSpawnTime = 0L
var ticksUntilNextSpawn = 100F
abstract var ticksBetweenSpawns: Float
var tickTimerMultiplier = 1F
var removalCheckTicks = 0
open fun tick() {
removalCheckTicks++
influences.removeIf { it.isExpired() }
if (removalCheckTicks == 60) {
spawnedEntities.removeIf { it.isRemoved }
removalCheckTicks = 0
}
if (!active) {
return
}
ticksUntilNextSpawn -= tickTimerMultiplier
if (ticksUntilNextSpawn <= 0) {
val spawnActions = run(cause = SpawnCause(spawner = this, entity = getCauseEntity()))
ticksUntilNextSpawn = ticksBetweenSpawns
for (spawnAction in spawnActions) {
spawnAction.complete()
}
}
}
override fun <R> afterSpawn(action: SpawnAction<R>, result: R) {
super.afterSpawn(action, result)
if (result is EntitySpawnResult) {
spawnedEntities.addAll(result.entities)
}
lastSpawnTime = System.currentTimeMillis()
}
open fun getCauseEntity(): Entity? = null
fun getAllInfluences() = this.influences + manager.influences
override fun copyInfluences() = this.getAllInfluences().toMutableList()
}
@@ -1,52 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.selection.FlatSpawnablePositionWeightedSelector
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerLevel
/**
* A type of spawner that occurs at a single point and provides its own means of generating a
* spawnable position from a particular cause and position. The trigger spawner must be directly called to
* spawn things (almost as if it was being triggered!!!) as opposed to being scheduled on a ticker.
*
* @author Hiroku
* @since February 3rd, 2024
*/
abstract class TriggerSpawner<T : SpawnCause>(override val name: String, var spawns: SpawnPool) : Spawner {
private var selector: SpawningSelector<*> = FlatSpawnablePositionWeightedSelector()
override val influences = mutableListOf<SpawningInfluence>()
open fun getSpawnAction(
cause: T,
world: ServerLevel,
pos: BlockPos,
influences: List<SpawningInfluence> = emptyList()
): SpawnAction<*>? {
val spawnablePosition = parseSpawnablePosition(cause, world, pos) ?: return null
spawnablePosition.influences.addAll(influences)
val bucket = chooseBucket(cause, spawnablePosition.influences)
return selector.select(this, bucket, listOf(spawnablePosition), max = 1).firstOrNull()
}
/** Parses a spawnable position, if possible, from the given cause and position. */
abstract fun parseSpawnablePosition(cause: T, world: ServerLevel, pos: BlockPos): SpawnablePosition?
override fun getSpawningSelector() = selector
override fun setSpawningSelector(selector: SpawningSelector<*>) { this.selector = selector }
override fun getSpawnPool() = spawns
override fun setSpawnPool(spawnPool: SpawnPool) { spawns = spawnPool }
}
@@ -137,6 +137,9 @@ object CobblemonItemTags {
@JvmField val WEARABLE_HAT_ITEMS = create("held/visibility/hat")
@JvmField val HIDDEN_ITEMS = create("held/visibility/hidden")
//Empty Tag
@JvmField val EMPTY = create("empty")
private fun create(path: String) = TagKey.create(Registries.ITEM, cobblemonResource(path))
@@ -18,6 +18,7 @@ import net.minecraft.core.BlockPos
import net.minecraft.core.particles.BlockParticleOption
import net.minecraft.core.particles.ColorParticleOption
import net.minecraft.core.particles.ParticleTypes
import net.minecraft.server.level.ServerLevel
import net.minecraft.sounds.SoundEvents
import net.minecraft.sounds.SoundSource
import net.minecraft.stats.Stats
@@ -51,12 +52,13 @@ import net.minecraft.world.phys.Vec3
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraft.world.phys.shapes.VoxelShape
import kotlin.math.floor
import kotlin.random.Random
class PokeSnackBlock(settings: Properties, val isLure: Boolean): BaseEntityBlock(settings) {
companion object {
val MAX_BITES = 8
val CAKE_HEIGHT = 0.4375
const val MAX_BITES = 8
const val CAKE_HEIGHT = 0.4375
val CANDLE_PARTICLE_POSITION = Vec3(0.5, CAKE_HEIGHT + 0.5, 0.5)
@@ -135,16 +137,18 @@ class PokeSnackBlock(settings: Properties, val isLure: Boolean): BaseEntityBlock
if (hasCandle(state)) {
return Shapes.or(SHAPES[state.getValue(BITES)], CANDLE_SHAPE)
}
return SHAPES[state.getValue(BITES)]
}
override fun getRenderShape(blockState: BlockState?) = RenderShape.MODEL
override fun getRenderShape(blockState: BlockState) = RenderShape.MODEL
override fun setPlacedBy(level: Level, pos: BlockPos, state: BlockState, placer: LivingEntity?, stack: ItemStack) {
super.setPlacedBy(level, pos, state, placer, stack)
val blockEntity = level.getBlockEntity(pos) as? PokeSnackBlockEntity
blockEntity?.initializeFromItemStack(stack)
val blockEntity = level.getBlockEntity(pos) as? PokeSnackBlockEntity ?: return
blockEntity.initializeFromItemStack(stack)
blockEntity.placedBy = placer?.uuid
}
override fun getCloneItemStack(level: LevelReader, pos: BlockPos, state: BlockState): ItemStack {
@@ -294,15 +298,28 @@ class PokeSnackBlock(settings: Properties, val isLure: Boolean): BaseEntityBlock
fun eat(level: Level, pos: BlockPos, state: BlockState, player: Player?) {
val bites = state.getValue(BITES) as Int
if (bites < MAX_BITES) {
level.setBlock(pos, state.setValue(BITES, bites + 1) as BlockState, UPDATE_ALL)
} else {
val newBites =
if (isLure) {
val pokeSnackBlockEntity = level.getBlockEntity(pos) as PokeSnackBlockEntity? ?: return
pokeSnackBlockEntity.amountSpawned += 1
floor(pokeSnackBlockEntity.amountSpawned / PokeSnackBlockEntity.SPAWNS_PER_BITE.toDouble()).toInt()
} else {
bites + 1
}
if (newBites > MAX_BITES) {
dropCandle(level, pos, state, player)
level.removeBlock(pos, false)
level.removeBlockEntity(pos)
} else {
level.setBlock(pos, state.setValue(BITES, newBites) as BlockState, UPDATE_ALL)
}
level.playSound(null, player?.blockPosition() ?: pos, SoundEvents.GENERIC_EAT, if (player != null) SoundSource.PLAYERS else SoundSource.NEUTRAL)
spawnEatParticles(level, pos)
player?.let {
level.gameEvent(it, GameEvent.BLOCK_DESTROY, pos)
}
@@ -352,8 +369,19 @@ class PokeSnackBlock(settings: Properties, val isLure: Boolean): BaseEntityBlock
0.0,
0.0,
0.0
)
}
}
override fun isRandomlyTicking(state: BlockState) = isLure
override fun randomTick(
state: BlockState,
level: ServerLevel,
pos: BlockPos,
random: RandomSource
) {
if (level.isClientSide) return
(level.getBlockEntity(pos) as? PokeSnackBlockEntity)?.randomTick()
}
}
@@ -52,8 +52,8 @@ class CookingPotMenu : RecipeBookMenu<CraftingInput, CookingPotRecipeBase>, Cont
constructor(containerId: Int, playerInventory: Inventory) : super(CobblemonMenuType.COOKING_POT, containerId) {
this.playerInventory = playerInventory
this.container = CookingPotContainer(this,
CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH,
CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
CampfireBlockEntity.CRAFTING_GRID_WIDTH,
CampfireBlockEntity.CRAFTING_GRID_WIDTH
)
this.resultContainer = ResultContainer()
this.containerData = SimpleContainerData(4)
@@ -82,25 +82,25 @@ class CookingPotMenu : RecipeBookMenu<CraftingInput, CookingPotRecipeBase>, Cont
val resultSlotX = 128
val resultSlotY = 55
addSlot(CookingPotResultSlot(this.container, CampfireBlockEntity.Companion.RESULT_SLOT, resultSlotX, resultSlotY))
addSlot(CookingPotResultSlot(this.container, CampfireBlockEntity.RESULT_SLOT, resultSlotX, resultSlotY))
for ((index, slotIndex) in CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.withIndex()) {
val i = index / CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
val j = index % CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
for ((index, slotIndex) in CampfireBlockEntity.CRAFTING_GRID_SLOTS.withIndex()) {
val i = index / CampfireBlockEntity.CRAFTING_GRID_WIDTH
val j = index % CampfireBlockEntity.CRAFTING_GRID_WIDTH
addSlot(Slot(this.container, slotIndex, 33 + j * 18, 18 + i * 18))
}
for ((index, slotIndex) in CampfireBlockEntity.Companion.SEASONING_SLOTS.withIndex()) {
addSlot(SeasoningSlot(this.container, slotIndex, 110 + index * 18, 18))
for ((index, slotIndex) in CampfireBlockEntity.SEASONING_SLOTS.withIndex()) {
addSlot(SeasoningSlot(this, this.container, slotIndex, 110 + index * 18, 18))
}
for ((index, _) in CampfireBlockEntity.Companion.PLAYER_INVENTORY_SLOTS.withIndex()) {
val i = index / CampfireBlockEntity.Companion.PLAYER_INVENTORY_WIDTH
val j = index % CampfireBlockEntity.Companion.PLAYER_INVENTORY_WIDTH
for ((index, _) in CampfireBlockEntity.PLAYER_INVENTORY_SLOTS.withIndex()) {
val i = index / CampfireBlockEntity.PLAYER_INVENTORY_WIDTH
val j = index % CampfireBlockEntity.PLAYER_INVENTORY_WIDTH
addSlot(Slot(playerInventory, index + 9, 8 + j * 18, 84 + i * 18))
}
for ((index, _) in CampfireBlockEntity.Companion.PLAYER_HOTBAR_SLOTS.withIndex()) {
for ((index, _) in CampfireBlockEntity.PLAYER_HOTBAR_SLOTS.withIndex()) {
addSlot(Slot(playerInventory, index, 8 + index * 18, 142))
}
}
@@ -173,29 +173,31 @@ class CookingPotMenu : RecipeBookMenu<CraftingInput, CookingPotRecipeBase>, Cont
currentActiveRecipe = recipe
if (recipe != null) {
previewItem = recipe.value.assemble(craftInput, level.registryAccess())
// Apply seasoning to the preview item
recipe.value.applySeasoning(
previewItem,
container.items.subList(CampfireBlockEntity.SEASONING_SLOTS.first,
CampfireBlockEntity.SEASONING_SLOTS.last + 1)
.filterNotNull()
.filter { !it.isEmpty })
.filter { !it.isEmpty && it.`is`(recipe.value.seasoningTag) })
} else previewItem = ItemStack.EMPTY
}
override fun getResultSlotIndex(): Int {
return CampfireBlockEntity.Companion.RESULT_SLOT
return CampfireBlockEntity.RESULT_SLOT
}
override fun getGridWidth(): Int {
return CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
return CampfireBlockEntity.CRAFTING_GRID_WIDTH
}
override fun getGridHeight(): Int {
return CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
return CampfireBlockEntity.CRAFTING_GRID_WIDTH
}
override fun getSize(): Int {
return CampfireBlockEntity.Companion.ITEMS_SIZE
return CampfireBlockEntity.ITEMS_SIZE
}
fun getBurnProgress(): Float {
@@ -227,24 +229,24 @@ class CookingPotMenu : RecipeBookMenu<CraftingInput, CookingPotRecipeBase>, Cont
val slotItemStack = slot.item
itemStack = slotItemStack.copy()
if (index == CampfireBlockEntity.Companion.RESULT_SLOT) {
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.PLAYER_INVENTORY_SLOTS.first, CampfireBlockEntity.Companion.PLAYER_HOTBAR_SLOTS.last + 1, false)) {
if (index == CampfireBlockEntity.RESULT_SLOT) {
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.PLAYER_INVENTORY_SLOTS.first, CampfireBlockEntity.PLAYER_HOTBAR_SLOTS.last + 1, false)) {
return ItemStack.EMPTY
}
slot.onQuickCraft(slotItemStack, itemStack);
} else if (index in CampfireBlockEntity.Companion.PLAYER_INVENTORY_SLOTS || index in CampfireBlockEntity.Companion.PLAYER_HOTBAR_SLOTS) {
} else if (index in CampfireBlockEntity.PLAYER_INVENTORY_SLOTS || index in CampfireBlockEntity.PLAYER_HOTBAR_SLOTS) {
if (Seasonings.isSeasoning(slotItemStack)) {
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.first, CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.last + 1, false) &&
!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.SEASONING_SLOTS.first, CampfireBlockEntity.Companion.SEASONING_SLOTS.last + 1, false)
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.CRAFTING_GRID_SLOTS.first, CampfireBlockEntity.CRAFTING_GRID_SLOTS.last + 1, false) &&
!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.SEASONING_SLOTS.first, CampfireBlockEntity.SEASONING_SLOTS.last + 1, false)
) {
return ItemStack.EMPTY
}
} else if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.first, CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.last + 1, false)) {
} else if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.CRAFTING_GRID_SLOTS.first, CampfireBlockEntity.CRAFTING_GRID_SLOTS.last + 1, false)) {
return ItemStack.EMPTY
}
} else if (index in CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS || index in CampfireBlockEntity.Companion.SEASONING_SLOTS) {
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.PLAYER_INVENTORY_SLOTS.first, CampfireBlockEntity.Companion.PLAYER_HOTBAR_SLOTS.last + 1, false)) {
} else if (index in CampfireBlockEntity.CRAFTING_GRID_SLOTS || index in CampfireBlockEntity.SEASONING_SLOTS) {
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.PLAYER_INVENTORY_SLOTS.first, CampfireBlockEntity.PLAYER_HOTBAR_SLOTS.last + 1, false)) {
return ItemStack.EMPTY
}
}
@@ -8,13 +8,13 @@
package com.cobblemon.mod.common.block.campfirepot
import com.cobblemon.mod.common.api.cooking.Seasonings
import net.minecraft.world.Container
import net.minecraft.world.inventory.Slot
import net.minecraft.world.item.ItemStack
class SeasoningSlot(container: Container, slot: Int, x: Int, y: Int) : Slot(container, slot, x, y) {
class SeasoningSlot(val menu: CookingPotMenu, container: Container, slot: Int, x: Int, y: Int) : Slot(container, slot, x, y) {
override fun mayPlace(stack: ItemStack): Boolean {
return Seasonings.isSeasoning(stack) && super.mayPlace(stack)
val recipe = menu.currentActiveRecipe?.value() ?: return false
return stack.`is`(recipe.seasoningTag) && super.mayPlace(stack)
}
}
@@ -134,6 +134,8 @@ class CampfireBlockEntity(pos: BlockPos, state: BlockState) : BaseContainerBlock
campfireBlockEntity.particleCooldown = 20
}
campfireBlockEntity.time++
}
fun serverTick(level: Level, pos: BlockPos, state: BlockState, campfireBlockEntity: CampfireBlockEntity) {
@@ -166,7 +168,7 @@ class CampfireBlockEntity(pos: BlockPos, state: BlockState) : BaseContainerBlock
val cookedItem = recipe.assemble(craftingInput, level.registryAccess())
val resultSlotItem = campfireBlockEntity.getItem(0)
recipe.applySeasoning(cookedItem, campfireBlockEntity.getSeasonings())
recipe.applySeasoning(cookedItem, campfireBlockEntity.getSeasonings().filter { it.`is`(recipe.seasoningTag) })
if (!campfireBlockEntity.blockState.getValue(CampfireBlock.LID)) {
campfireBlockEntity.cookingProgress = 0
@@ -237,6 +239,7 @@ class CampfireBlockEntity(pos: BlockPos, state: BlockState) : BaseContainerBlock
private var particleCooldown: Int = 0
var brothColor: Int = BASE_BROTH_COLOR
var bubbleColor: Int = BASE_BROTH_BUBBLE_COLOR
var time: Int = 0
var dataAccess: ContainerData = object : ContainerData {
override fun get(index: Int): Int {
@@ -311,7 +314,7 @@ class CampfireBlockEntity(pos: BlockPos, state: BlockState) : BaseContainerBlock
consumeItem(i)
}
for (i in SEASONING_SLOTS.first..SEASONING_SLOTS.last) {
if (recipe.seasoningProcessors.any { it.consumesItem(getItem(i)) }) consumeItem(i)
if (getItem(i).`is`(recipe.seasoningTag) && recipe.seasoningProcessors.any { it.consumesItem(getItem(i)) }) consumeItem(i)
}
val direction = state.getValue(CampfireBlock.ITEM_DIRECTION)
@@ -8,29 +8,118 @@
package com.cobblemon.mod.common.block.entity
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonBlockEntities
import com.cobblemon.mod.common.CobblemonItemComponents
import com.cobblemon.mod.common.api.cooking.getColourMixFromFlavours
import com.cobblemon.mod.common.api.fishing.SpawnBait
import com.cobblemon.mod.common.api.fishing.SpawnBait.Effect
import com.cobblemon.mod.common.api.fishing.SpawnBaitEffects
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.detail.PokemonHerdSpawnDetail
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.api.spawning.influence.BucketMultiplyingInfluence
import com.cobblemon.mod.common.api.spawning.influence.BucketNormalizingInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawnBaitInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.spawner.FixedAreaSpawner
import com.cobblemon.mod.common.block.PokeSnackBlock
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.item.components.BaitEffectsComponent
import com.cobblemon.mod.common.item.components.FlavourComponent
import com.cobblemon.mod.common.item.components.IngredientComponent
import com.cobblemon.mod.common.net.messages.client.effect.PokeSnackBlockParticlesPacket
import com.cobblemon.mod.common.util.DataKeys
import java.util.UUID
import net.minecraft.core.BlockPos
import net.minecraft.core.HolderLookup
import net.minecraft.nbt.*
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.NbtOps
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.Entity
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.block.state.BlockState
import kotlin.collections.orEmpty
open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) : TintBlockEntity(CobblemonBlockEntities.POKE_SNACK, pos, state) {
open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) :
TintBlockEntity(CobblemonBlockEntities.POKE_SNACK, pos, state),
SpawningInfluence {
companion object {
const val SPAWNS_PER_BITE = 1
const val RADIUS = 8
const val RANDOM_TICKS_BETWEEN_SPAWNS = 4
const val POKE_SNACK_CRUMBED_ASPECT = "poke_snack_crumbed"
}
val spawner: FixedAreaSpawner by lazy {
FixedAreaSpawner(
name = "poke_snack_spawner_${level?.dimension()?.location()}}_$blockPos",
spawnPool = CobblemonSpawnPools.WORLD_SPAWN_POOL,
world = level as ServerLevel,
position = blockPos,
horizontalRadius = RADIUS,
verticalRadius = RADIUS,
maxPokemonPerChunk = Cobblemon.config.pokeSnackPokemonPerChunk
).also {
it.influences.add(this)
val baitEffects = getBaitEffects()
val highestLureTier = baitEffects
.filter { it.type == SpawnBait.Effects.RARITY_BUCKET }
.maxOfOrNull { it.value }
?.toInt()
?: 0
if (highestLureTier > 0) {
it.influences += BucketNormalizingInfluence(tier = highestLureTier)
}
it.influences += BucketMultiplyingInfluence(
mapOf(
"uncommon" to 2.25f,
"rare" to 5.5f,
"ultra-rare" to 5.5f,
)
)
it.influences += SpawnBaitInfluence(effects = baitEffects)
}
}
// Only non-herd Pokémon can be spawned from a Poké Snack
override fun affectSpawnable(detail: SpawnDetail, spawnablePosition: SpawnablePosition) = detail.type in SpawnDetail.pokemonTypes && detail !is PokemonHerdSpawnDetail
// When a Pokémon spawns, apply the crumbed aspect and play the eating effects. Eat part of the cake, too.
override fun affectSpawn(action: SpawnAction<*>, entity: Entity) {
if (entity is PokemonEntity) {
entity.pokemon.forcedAspects += POKE_SNACK_CRUMBED_ASPECT
val block = blockState.block as? PokeSnackBlock ?: return
val level = level ?: return
val entityPos = entity.blockPosition()
PokeSnackBlockParticlesPacket(blockPos, entityPos).sendToPlayersAround(
blockPos.x.toDouble(),
blockPos.y.toDouble(),
blockPos.z.toDouble(),
64.0,
level.dimension(),
)
block.eat(level, blockPos, blockState, action.spawnablePosition.cause.entity as? Player)
}
}
var placedBy: UUID? = null
var amountSpawned: Int = 0
var flavourComponent: FlavourComponent? = null
var baitEffectsComponent: BaitEffectsComponent? = null
var ingredientComponent: IngredientComponent? = null
var randomTicksUntilNextSpawn: Float = getRandomTicksBetweenSpawns()
fun initializeFromItemStack(itemStack: ItemStack) {
flavourComponent = itemStack.get(CobblemonItemComponents.FLAVOUR)
@@ -38,9 +127,7 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) : TintBlockEnt
if (isLure()) baitEffectsComponent = itemStack.get(CobblemonItemComponents.BAIT_EFFECTS)
flavourComponent?.let {
getColourMixFromFlavours(it.getDominantFlavours())?.let {
setTint(it)
}
getColourMixFromFlavours(it.getDominantFlavours())?.let(::setTint)
}
}
@@ -52,14 +139,60 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) : TintBlockEnt
return false
}
fun randomTick() {
randomTicksUntilNextSpawn--
if (randomTicksUntilNextSpawn <= 0) {
val nearestPlayer = level?.getNearestPlayer(
blockPos.x.toDouble(),
blockPos.y.toDouble(),
blockPos.z.toDouble(),
Cobblemon.config.maximumSpawningZoneDistanceFromPlayer.toDouble(),
false, // true here makes it so creative players aren't considered
)
// High simulation distances may cause the player to be null here, we don't want to spawn without a player nearby.
if (nearestPlayer != null) {
spawner.run(
cause = SpawnCause(spawner = spawner, entity = nearestPlayer),
maxSpawns = 1
)
}
randomTicksUntilNextSpawn = getRandomTicksBetweenSpawns()
}
}
fun getRandomTicksBetweenSpawns(): Float {
val biteTimeMultiplier = getBiteTimeMultiplier()
return (RANDOM_TICKS_BETWEEN_SPAWNS * biteTimeMultiplier).coerceAtLeast(1F)
}
fun getBiteTimeMultiplier(): Float {
val baitEffects = getBaitEffects()
val biteTimeEffects = baitEffects.filter { it.type == SpawnBait.Effects.BITE_TIME }
if (biteTimeEffects.isEmpty()) return 1F
val biteTimeEffect = biteTimeEffects.random()
if (Math.random() > biteTimeEffect.chance) {
return 1F
}
return 1F - biteTimeEffect.value.toFloat()
}
fun toItemStack(): ItemStack {
val stack = ItemStack(this.blockState.block)
if (isLure() && baitEffectsComponent != null) {
stack.set(CobblemonItemComponents.BAIT_EFFECTS, baitEffectsComponent)
}
if (flavourComponent != null) {
stack.set(CobblemonItemComponents.FLAVOUR, flavourComponent)
}
if (ingredientComponent != null) {
stack.set(CobblemonItemComponents.INGREDIENT, ingredientComponent)
}
return stack
}
@@ -67,12 +200,18 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) : TintBlockEnt
* Combine all the [SpawnBait.Effect] values from the [baitEffectsComponent] data.
*/
fun getBaitEffects(): List<Effect> {
return baitEffectsComponent?.effects?.mapNotNull(SpawnBaitEffects::getFromIdentifier)?.flatMap { it.effects }.orEmpty()
return baitEffectsComponent
?.effects
?.mapNotNull(SpawnBaitEffects::getFromIdentifier)
?.flatMap { it.effects }
.orEmpty()
}
override fun saveAdditional(tag: CompoundTag, registries: HolderLookup.Provider) {
super.saveAdditional(tag, registries)
tag.putInt(DataKeys.AMOUNT_SPAWNED, amountSpawned)
flavourComponent?.let { component ->
CobblemonItemComponents.FLAVOUR.codec()!!
.encodeStart(NbtOps.INSTANCE, component)
@@ -99,11 +238,19 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) : TintBlockEnt
tag.put(DataKeys.INGREDIENTS, encodedTag)
}
}
tag.putFloat(DataKeys.TICKS_UNTIL_NEXT_SPAWN, randomTicksUntilNextSpawn)
placedBy?.let {
tag.putUUID(DataKeys.PLACED_BY, it)
}
}
override fun loadAdditional(tag: CompoundTag, registries: HolderLookup.Provider) {
super.loadAdditional(tag, registries)
amountSpawned = tag.getInt(DataKeys.AMOUNT_SPAWNED)
if (tag.contains(DataKeys.FLAVOUR)) {
CobblemonItemComponents.FLAVOUR.codec()
?.parse(NbtOps.INSTANCE, tag.get(DataKeys.FLAVOUR))
@@ -130,5 +277,13 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) : TintBlockEnt
ingredientComponent = component
}
}
if (tag.contains(DataKeys.TICKS_UNTIL_NEXT_SPAWN)) {
randomTicksUntilNextSpawn = tag.getFloat(DataKeys.TICKS_UNTIL_NEXT_SPAWN)
}
if (tag.contains(DataKeys.PLACED_BY)) {
placedBy = tag.getUUID(DataKeys.PLACED_BY)
}
}
}
@@ -201,7 +201,7 @@ class CookingPotScreen(
}
override fun renderSlot(guiGraphics: GuiGraphics, slot: Slot) {
if (slot is SeasoningSlot && !menu.carried.isEmpty && !slot.mayPlace(menu.carried)) {
if (slot is SeasoningSlot && ((!menu.carried.isEmpty && !slot.mayPlace(menu.carried)) || (slot.hasItem() && !slot.mayPlace(slot.item))) ) {
val x = slot.x
val y = slot.y
guiGraphics.fill(x,y,x+16,y+16,822018048)
@@ -24,6 +24,7 @@ abstract class AnimatedPortraitDrawer : PortraitDrawer {
private val stateAtIndex = hashMapOf<Int, Pair<UUID, FloatingState>>()
override fun draw(pokemon: Pokemon, poseStack: PoseStack, partialTicks: Float, isSelected: Boolean, index: Int) {
state.currentAspects = pokemon.aspects
drawPosablePortrait(
identifier = pokemon.species.resourceIdentifier,
matrixStack = poseStack,
@@ -29,6 +29,5 @@ object RideSettingsSyncHandler : ClientNetworkPacketHandler<RideSettingsSyncPack
CobblemonRideSettings.boat = packet.boat
CobblemonRideSettings.burst = packet.burst
CobblemonRideSettings.dolphin = packet.dolphin
CobblemonRideSettings.submarine = packet.submarine
}
}
@@ -10,41 +10,22 @@ package com.cobblemon.mod.common.client.net.orientation
import com.cobblemon.mod.common.OrientationControllable
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.duck.RidePassenger
import com.cobblemon.mod.common.net.messages.client.orientation.ClientboundUpdateOrientationPacket
import net.minecraft.client.Minecraft
import net.minecraft.util.Mth
object S2CUpdateOrientationHandler : ClientNetworkPacketHandler<ClientboundUpdateOrientationPacket> {
override fun handle(packet: ClientboundUpdateOrientationPacket, client: Minecraft) {
client.executeIfPossible {
val level = client.level ?: return@executeIfPossible
val player = client.player ?: return@executeIfPossible
val vehicle = level.getEntity(packet.entityId)
val vehicleController = if (vehicle is OrientationControllable) vehicle.orientationController else return@executeIfPossible
// If the client is also the driver of this entity then don't update it as the driver is the source
// of truth on vehicle orientation updates.
if (vehicle.controllingPassenger != player) {
packet.active?.let { shouldUseCustomOrientation ->
val playerIsPassenger = vehicle.passengers.contains(player)
// If the player has just switched to riding a custom orientation ride then set their
// ride x and y rots local to the vehicle rots(if the controller wasn't active and will now be active)
// This ensures that on transition your camera stays in the same spot
if (!vehicleController.isActive() && shouldUseCustomOrientation && playerIsPassenger) {
// Set local to the vehicle x and yrot
val playerRotater = player as RidePassenger?
if (playerRotater != null) {
playerRotater.`cobblemon$setRideXRot`(Mth.wrapDegrees(player.getXRot() - vehicle.getXRot()))
playerRotater.`cobblemon$setRideYRot`(Mth.wrapDegrees(player.getYRot() - vehicle.getYRot()))
}
val entity = level.getEntity(packet.entityId)
if (entity is OrientationControllable) {
packet.active?.let {
entity.orientationController.active = it
if (!it) {
entity.orientationController.reset()
}
vehicle.orientationController.active = shouldUseCustomOrientation
}
vehicle.orientationController.updateOrientation { _ -> packet.orientation }
entity.orientationController.updateOrientation { _ -> packet.orientation }
}
}
}
@@ -11,7 +11,14 @@ package com.cobblemon.mod.common.client.render
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.client.render.models.blockbench.PosableModel
import com.cobblemon.mod.common.client.render.models.blockbench.PosableState
import com.cobblemon.mod.common.client.render.models.blockbench.animation.PoseTransitionAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockActiveAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockPoseAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.TextureInterpolationType
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Bone
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
import com.cobblemon.mod.common.client.render.models.blockbench.repository.RenderContext
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
import com.cobblemon.mod.common.util.adapters.*
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
@@ -21,10 +28,13 @@ import com.cobblemon.mod.common.util.toRGBA
import com.google.gson.GsonBuilder
import com.google.gson.annotations.SerializedName
import com.mojang.blaze3d.platform.NativeImage
import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.RenderStateShard
import net.minecraft.client.renderer.texture.DynamicTexture
import kotlin.math.floor
import net.minecraft.resources.ResourceLocation
import org.joml.Vector2d
import org.joml.Vector3f
import org.joml.Vector4f
@@ -53,7 +63,7 @@ class VaryingRenderableResolver(
}
fun getResolvedTexture(state: PosableState): ResourceLocation {
return getVariationValue(state) { texture }?.invoke(state)
return getVariationValue(state) { texture }?.getTexture(state)
?: throw IllegalStateException("Unable to find a texture for $name with aspects ${state.currentAspects.joinToString()}. This shouldn't be possible if you've defined the fallback variation.")
}
@@ -180,6 +190,13 @@ class ModelAssetVariation(
}
}
/** Needed a custom one, so I can make it take in a dynamic texture instead of resource location */
class DynamicStateShard(texture: DynamicTexture) : RenderStateShard.EmptyTextureStateShard(Runnable {
RenderSystem.setShaderTexture(0, texture.id)
}, Runnable {
texture.close() // Cleanup
})
/**
* Given the animation seconds, returns a texture to use. Only implemented
* by [StaticModelTextureSupplier] and [AnimatedModelTextureSupplier].
@@ -187,12 +204,17 @@ class ModelAssetVariation(
* @author Hiroku
* @since February 6th, 2023
*/
fun interface ModelTextureSupplier {
operator fun invoke(state: PosableState): ResourceLocation
interface ModelTextureSupplier {
operator fun invoke(state: PosableState): RenderStateShard.EmptyTextureStateShard
fun getTexture(state: PosableState): ResourceLocation
}
class StaticModelTextureSupplier(val texture: ResourceLocation): ModelTextureSupplier {
override fun invoke(state: PosableState): ResourceLocation {
override fun invoke(state: PosableState): RenderStateShard.EmptyTextureStateShard {
return RenderStateShard.TextureStateShard(texture, false, false)
}
override fun getTexture(state: PosableState): ResourceLocation {
return texture
}
}
@@ -203,80 +225,196 @@ class AnimatedModelTextureSupplier(
val frames: List<ResourceLocation>,
val interpolation: Boolean
): ModelTextureSupplier {
override fun invoke(state: PosableState): ResourceLocation {
val frameIndex = floor(state.animationSeconds * fps).toInt()
return if (frameIndex >= frames.size && !loop) {
frames.last()
} else {
frames[frameIndex % frames.size]
override fun invoke(state: PosableState): RenderStateShard.EmptyTextureStateShard {
var frameIndex = state.animationSeconds * fps
if (frameIndex > frames.size - 1.0f && !loop) frameIndex = frames.size.toFloat() - 1.0f
val i = frameIndex % 1.0
if (interpolation && i != 0.0) {
val o = floor(frameIndex).toInt() % frames.size
val s = if (o == frames.size - 1) 0 else o + 1
val resourceManager = Minecraft.getInstance().resourceManager
val texture1 = NativeImage.read(resourceManager.getResourceOrThrow(frames[o]).open())
val texture2 = NativeImage.read(resourceManager.getResourceOrThrow(frames[s]).open())
val image = getInterpolatedTexture(texture1, texture2, i)
val newTexture = DynamicTexture(image)
newTexture.setFilter(false, false)
return DynamicStateShard(newTexture)
}
else {
val frame = floor(frameIndex).toInt() % frames.size
return RenderStateShard.TextureStateShard(frames[frame], false, false)
}
}
fun interpolatedTexture(state: PosableState): DynamicTexture? {
override fun getTexture(state: PosableState): ResourceLocation {
val frameIndex = floor(state.animationSeconds * fps).toInt()
return frames[frameIndex]
}
}
class KeyframableTextureSupplier(val textures: MutableList<ResourceLocation>) : ModelTextureSupplier {
var layerName = ""
override fun invoke(state: PosableState): RenderStateShard.EmptyTextureStateShard {
val resourceManager = Minecraft.getInstance().resourceManager
val frameIndex = floor(state.animationSeconds * fps).toInt()
try {
if (frameIndex >= frames.size && !loop) {
return DynamicTexture(NativeImage.read(resourceManager.getResourceOrThrow(frames.last()).open()))
if (state.primaryAnimation?.animation is PoseTransitionAnimation) {
val poseTransition = state.primaryAnimation!!.animation as PoseTransitionAnimation
val (animation1, startTime1) = getMainAnimFromPose(poseTransition.beforePose, state, true)
val (animation2, startTime2) = getMainAnimFromPose(poseTransition.afterPose, state, true)
val texture2 = if (animation2 != null) {
getImageFromAnimation(animation2, state.animationSeconds - startTime2)
} else {
val image = NativeImage.read(resourceManager.getResourceOrThrow(textures[0]).open())
getEmptyTexture(image.width, image.height)
}
val (interpType, cubicValues) = poseTransition.beforePose.texturePhaseOut
?: poseTransition.afterPose.texturePhaseIn
if (interpType == TextureInterpolationType.STEP) {
val newTexture = DynamicTexture(texture2)
newTexture.setFilter(false, false)
return DynamicStateShard(newTexture)
}
val texture1 = if (animation1 != null) {
getImageFromAnimation(animation1, state.animationSeconds - startTime1)
} else {
val image = NativeImage.read(resourceManager.getResourceOrThrow(textures[0]).open())
getEmptyTexture(image.width, image.height)
}
var i =
((state.animationSeconds - poseTransition.startTime) / (poseTransition.endTime - poseTransition.startTime)).toDouble() //Linear
if (interpType == TextureInterpolationType.BEZIER) { //Switch to Bezier
i = resolveCubicValues(cubicValues, i)
}
val newImage = getInterpolatedTexture(texture1, texture2, i)
val newTexture = DynamicTexture(newImage)
newTexture.setFilter(false, false)
return DynamicStateShard(newTexture)
} else {
var animation = if ((state.primaryAnimation?.animation as? BedrockActiveAnimation)?.animation?.cobblemonTextures?.get(layerName) != null) {
(state.primaryAnimation?.animation as? BedrockActiveAnimation)?.animation
} else null
var startTime = 0F
if (animation == null) {
val data = getMainAnimFromPose(state.currentPose?.let(state.currentModel!!.poses::get), state)
animation = data.first
startTime = data.second
}
if (animation != null) {
val image = getImageFromAnimation(animation, state.animationSeconds - startTime)
val newTexture = DynamicTexture(image)
newTexture.setFilter(false, false)
return DynamicStateShard(newTexture)
}
} catch (e : Exception) {
return null
}
val o = frameIndex % frames.size
val i = state.animationSeconds * fps - frameIndex
val image = NativeImage.read(resourceManager.getResourceOrThrow(textures[0]).open())
val newTexture = DynamicTexture(getEmptyTexture(image.width, image.height))
newTexture.setFilter(false, false)
return DynamicStateShard(newTexture)
}
override fun getTexture(state: PosableState): ResourceLocation {
return textures[0]
}
private fun getMainAnimFromPose(pose: Pose?, state: PosableState, transitionAnimation: Boolean = false): Pair<BedrockAnimation?, Float> {
var animation: BedrockAnimation? = null
var startTime = 0F
pose?.animations?.filterIsInstance<BedrockPoseAnimation>()?.filter{ transitionAnimation || state.shouldIdleRun(it, 0F) }?.forEach {
if (it.animation.cobblemonTextures[layerName] != null) {
animation = it.animation
}
}
if (state.primaryAnimation == null && state.currentModel!!.context.request(RenderContext.DO_QUIRKS) != false) {
pose?.quirks?.forEach { quirk ->
val quirkData = quirk.getOrCreateData(state)
quirkData.animations.filterIsInstance<BedrockActiveAnimation>().forEach {
if (it.animation.cobblemonTextures[layerName] != null) {
startTime = it.startedSeconds
animation = it.animation
}
}
}
}
return Pair(animation, startTime)
}
private fun getImageFromAnimation(animation: BedrockAnimation, animationSeconds: Float): NativeImage {
val resourceManager = Minecraft.getInstance().resourceManager
val keyframeMap = animation.cobblemonTextures[layerName]
if (keyframeMap.isNullOrEmpty()) {
val image1 = NativeImage.read(resourceManager.getResourceOrThrow(textures[0]).open())
return getEmptyTexture(image1.width, image1.height)
}
var adjustedTime = animationSeconds
if (animation.shouldLoop) adjustedTime %= animation.animationLength.toFloat()
val keyIndex = keyframeMap.keys.indexOfLast { it <= adjustedTime }
val keyframe1 = when(keyIndex) {
-1 -> keyframeMap.firstEntry()
else -> keyframeMap.entries.toMutableList()[keyIndex]
}
val keyframe2 = when {
keyIndex + 1 >= keyframeMap.size -> keyframeMap.lastEntry()
else -> keyframeMap.entries.toMutableList()[keyIndex+1]
}
val texture1 : NativeImage
val texture2 : NativeImage
var texture2 : NativeImage? = null
try { //Try getting images of the frames so we can use them to make a new one
texture1 = NativeImage.read(resourceManager.getResourceOrThrow(frames[o]).open())
val s = if (o == frames.size - 1) 0 else o + 1
texture2 = NativeImage.read(resourceManager.getResourceOrThrow(frames[s]).open())
} catch (e : Exception) {
return null
if (keyframe1.value.textureSlot == -1 && (keyframe2.value.textureSlot == -1 || keyframe1.value.interpolationType == TextureInterpolationType.STEP)) { // If both -1 we just return empty texture or if Step
val image1 = NativeImage.read(resourceManager.getResourceOrThrow(textures[0]).open())
return getEmptyTexture(image1.width, image1.height)
}
for (x in 0..<texture1.width) {
for (y in 0..<texture1.height) {
val color1 = texture1.getPixelRGBA(x,y).toRGBA()
val color2 = texture2.getPixelRGBA(x,y).toRGBA()
var newRed : Int
var newGreen : Int
var newBlue : Int
var newAlpha : Int
//Usually full transparent pixel is black, so this makes it copy the other texture colours to look better, however considering if we should allow to disable this feature
if (color1.w == 0F && color2.w == 1F) {
newRed = floor(color2.x * 255).toInt()
newGreen = floor(color2.y * 255).toInt()
newBlue = floor(color2.z * 255).toInt()
} else if (color1.w == 1F && color2.w == 0F) {
newRed = floor(color1.x * 255).toInt()
newGreen = floor(color1.y * 255).toInt()
newBlue = floor(color1.z * 255).toInt()
} else {
newRed = floor((color1.x + (color2.x - color1.x) * i) * 255).toInt()
newGreen = floor((color1.y + (color2.y - color1.y) * i) * 255).toInt()
newBlue = floor((color1.z + (color2.z - color1.z) * i) * 255).toInt()
}
newAlpha = floor((color1.w + (color2.w - color1.w) * i) * 255).toInt()
val finalColor = (newAlpha shl 24) or (newRed shl 16) or (newGreen shl 8) or newBlue
texture1.setPixelRGBA(x,y,finalColor) //We aren't using texture 1 anymore so it's easier to override it
}
var inverseInterp = false
if (keyframe1.value.textureSlot == -1) {
texture1 = NativeImage.read(resourceManager.getResourceOrThrow(textures[keyframe2.value.textureSlot]).open())
inverseInterp = true
} else if (keyframe2.value.textureSlot == -1) {
texture1 = NativeImage.read(resourceManager.getResourceOrThrow(textures[keyframe1.value.textureSlot]).open())
} else {
texture1 = NativeImage.read(resourceManager.getResourceOrThrow(textures[keyframe1.value.textureSlot]).open())
texture2 = NativeImage.read(resourceManager.getResourceOrThrow(textures[keyframe2.value.textureSlot]).open())
}
val newTexture = DynamicTexture(texture1)
newTexture.setFilter(false, false)
return newTexture
val i = (adjustedTime - keyframe1.key.toFloat()) / (keyframe2.key - keyframe1.key)
if (keyframe1.value.textureSlot != keyframe2.value.textureSlot && keyframe1.value.interpolationType == TextureInterpolationType.LINEAR) {
var interp = i
if (inverseInterp) interp = 1 - i
return getInterpolatedTexture(texture1, texture2, interp)
} else if (keyframe1.value.textureSlot != keyframe2.value.textureSlot && keyframe1.value.interpolationType == TextureInterpolationType.BEZIER) {
var cubicInterp = resolveCubicValues(keyframe1.value.cubicLerpFunctionValues, i)
if (inverseInterp) cubicInterp = 1 - cubicInterp
return getInterpolatedTexture(texture1, texture2, cubicInterp)
}
return NativeImage.read(resourceManager.getResourceOrThrow(textures[keyframe1.value.textureSlot]).open())
}
}
class VariableModelTextureSupplier : ModelTextureSupplier {
override fun invoke(state: PosableState): ResourceLocation {
override fun invoke(state: PosableState): RenderStateShard.EmptyTextureStateShard {
return RenderStateShard.TextureStateShard(getTexture(state), false, false)
}
override fun getTexture(state: PosableState): ResourceLocation {
return state.runtime.environment.variable.map["texture"]?.asString()?.asIdentifierDefaultingNamespace()
?: cobblemonResource("textures/npcs/default.png")
}
@@ -295,4 +433,75 @@ class ModelLayer {
enum class SpriteType {
PORTRAIT,
PROFILE
}
private fun getInterpolatedTexture(texture1: NativeImage, texture2: NativeImage?, i: Double): NativeImage {
for (x in 0..<texture1.width) {
for (y in 0..<texture1.height) {
val color1 = texture1.getPixelRGBA(x,y).toRGBA()
val color2 = texture2?.getPixelRGBA(x,y)?.toRGBA() ?: Vector4f(0F,0F,0F,0F)
var newRed : Int
var newGreen : Int
var newBlue : Int
//Usually full transparent pixel is black, so this makes it copy the other texture colours to look better, however considering if we should allow to disable this feature
if (color1.w == 0F && color2.w > 0F) {
newRed = floor(color2.x * 255).toInt()
newGreen = floor(color2.y * 255).toInt()
newBlue = floor(color2.z * 255).toInt()
} else if (color1.w > 0F && color2.w == 0F) {
newRed = floor(color1.x * 255).toInt()
newGreen = floor(color1.y * 255).toInt()
newBlue = floor(color1.z * 255).toInt()
} else {
newRed = floor((color1.x + (color2.x - color1.x) * i) * 255).toInt()
newGreen = floor((color1.y + (color2.y - color1.y) * i) * 255).toInt()
newBlue = floor((color1.z + (color2.z - color1.z) * i) * 255).toInt()
}
val newAlpha = floor((color1.w + (color2.w - color1.w) * i) * 255).toInt()
val finalColor = (newAlpha shl 24) or (newRed shl 16) or (newGreen shl 8) or newBlue
texture1.setPixelRGBA(x,y,finalColor) //We aren't using texture 1 anymore so it's easier to override it
}
}
return texture1
}
private fun getEmptyTexture(width: Int, height: Int): NativeImage {
return NativeImage(width, height, false)
}
private fun resolveCubicValues(clampedCubicValues : MutableList<Double>, i: Double): Double {
for (value in 0..<clampedCubicValues.size) {
clampedCubicValues[value] = Math.clamp(clampedCubicValues[value],0.0,1.0)
}
val points = mutableListOf<Vector2d>()
for (o in 0..200) {
val t = o/200.0
val omt = (1.0-t)
val point = Vector2d()
point.x = 3.0 * omt * omt * t * clampedCubicValues[0] + 3.0 * omt * t * t * clampedCubicValues[2] + t * t * t
point.y = 3.0 * omt * omt * t * clampedCubicValues[1] + 3.0 * omt * t * t * clampedCubicValues[3] + t * t * t
points.add(point)
}
var beforePoint = Vector2d(0.0,0.0)
var afterPoint = Vector2d(1.0, 1.0)
for (point in points) {
if (point.x > beforePoint.x && point.x < i) {
beforePoint = point
}
if (point.x < afterPoint.x && point.x > i) {
afterPoint = point
}
}
return beforePoint.y + (afterPoint.y - beforePoint.y) * ((i - beforePoint.x) / (afterPoint.x - beforePoint.x))
}
@@ -125,7 +125,7 @@ class CampfireBlockEntityRenderer(ctx: BlockEntityRendererProvider.Context) : Bl
) {
val seasonings = blockEntity.getSeasonings()
val gameTime = (blockEntity.level?.gameTime ?: 0) + tickDelta
val gameTime = blockEntity.time + tickDelta
val rotationAngle = (gameTime * ROTATION_SPEED) % 360
seasonings.forEachIndexed { index, seasoning ->
@@ -15,23 +15,23 @@ import com.cobblemon.mod.common.client.entity.PokemonClientDelegate
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.client.render.models.blockbench.PosableModel
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository.getPoser
import com.cobblemon.mod.common.duck.RidePassenger
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.mixin.accessor.CameraAccessor
import com.mojang.math.Axis
import net.minecraft.client.Camera
import net.minecraft.util.Mth
import net.minecraft.world.entity.Entity
import net.minecraft.world.level.ClipContext
import net.minecraft.world.phys.HitResult
import net.minecraft.world.phys.Vec3
import org.joml.Quaternionf
import org.joml.Matrix3f
import org.joml.Vector3f
object MountedCameraRenderer
{
fun getCameraPosition(
instance: Camera,
driver: Entity,
entity: Entity,
vehicle: Entity,
x: Double,
y: Double,
@@ -51,86 +51,99 @@ object MountedCameraRenderer
Mth.lerp(instance.partialTickTime.toDouble(), vehicle.zOld, vehicle.z)
)
setup(vehicle, instance.partialTickTime)
val rollable = vehicle as OrientationControllable
val vehicleController = rollable.getOrientationController()
val rollable = entity as OrientationControllable
val controller = rollable.getOrientationController()
val model = getPoser(vehicle.pokemon.species.resourceIdentifier, FloatingState())
val locatorName = delegate.getSeatLocator(driver)
val locator = delegate.locatorStates[locatorName] ?: return null
val currEyeHeight: Double = Mth.lerp(instance.partialTickTime.toDouble(), eyeHeight, eyeHeightOld)
var offset = Vector3f(0f, (currEyeHeight - (driver.bbHeight / 2)).toFloat(), 0f)
var eyeOffset = Vector3f(0f, (currEyeHeight - (driver.bbHeight / 2)).toFloat(), 0f)
// First person and view bobbing are handled the same way.
if (!instance.isDetached || Cobblemon.config.thirdPersonViewBobbing) {
val delegate = vehicle.delegate as PokemonClientDelegate
val locatorName = delegate.getSeatLocator(entity)
val locator = delegate.locatorStates[locatorName]
// Get additional offset from poser and add to the eyeHeight offset
val shouldFlip = !(vehicleController.active && vehicleController.orientation != null) // Do not flip the offset for 3rd person reverse unless we are using normal mc camera rotation.
eyeOffset.add(getFirstPersonOffset(model, locatorName))
if (!instance.isDetached) {
offset.add(
getFirstPersonOffset(model, locatorName)
)
} else if( Cobblemon.config.thirdPersonViewBobbing) {
offset.add(
getThirdPersonOffset(thirdPersonReverse, model.thirdPersonCameraOffset, locatorName, shouldFlip)
)
} else {
offset.add(
getThirdPersonOffset(thirdPersonReverse, model.thirdPersonCameraOffsetNoViewBobbing, locatorName, shouldFlip)
)
}
// Grab the current camera rotation to use as the offset rotation.
val offsetRot = instance.rotation()
offset = offsetRot.transform(offset)
eyeOffset = offsetRot.transform(eyeOffset)
val eyeLocatorOffset = Vec3(locator.matrix.getTranslation(Vector3f()))
val eyePos = eyeLocatorOffset.add(entityPos).toVector3f()
// Get the camera position. If 3rd person viewbobbing is enabled or the player is
// in first person then base the camera position off the seat locator offset
val pos = if (!instance.isDetached || Cobblemon.config.thirdPersonViewBobbing) {
val locatorOffset = Vec3(locator.matrix.getTranslation(Vector3f()))
locatorOffset.add(entityPos).toVector3f()
} else {
val pokemonCenter = Vector3f(0f, driver.bbHeight/2, 0f)
entityPos.toVector3f().add(pokemonCenter)
if (locator == null) {
return null
}
val offsetDistance = offset.length()
val offsetDirection = offset.mul(1 / offsetDistance)
val locatorOffset = Vec3(locator.matrix.getTranslation(Vector3f()))
val eyeOffsetDistance = eyeOffset.length()
val eyeOffsetDirection = eyeOffset.mul(1 / eyeOffsetDistance)
// Get Eye height
val currEyeHeight: Double = Mth.lerp(instance.partialTickTime.toDouble(), eyeHeight, eyeHeightOld)
var offset = Vector3f(0f, (currEyeHeight - (entity.bbHeight / 2)).toFloat(), 0f)
// Use getMaxZoom to calculate clipping
val maxZoom: Float = getMaxZoom(offsetDistance, offsetDirection, pos, instance)
val eyeMaxZoom: Float = getMaxZoom(eyeOffsetDistance, eyeOffsetDirection, eyePos, instance)
// Get additional offset from poser and add to the eyeHeight offset
if (!instance.isDetached) {
offset.add(
getFirstPersonOffset(model, locatorName)
)
} else {
offset.add(
getThirdPersonOffset(thirdPersonReverse, model.thirdPersonCameraOffset, locatorName)
)
}
val playerRotater = driver as RidePassenger
playerRotater.`cobblemon$setRideEyePos`(Vec3(eyeOffsetDirection.mul(eyeMaxZoom).add(eyePos)).add(0.0, 2.0, 0.0))
return Vec3(offsetDirection.mul(maxZoom).add(pos))
}
if (controller.isActive()) {
offset = controller.getRenderOrientation(instance.partialTickTime).transform(offset)
} else {
offset = Axis.YP.rotationDegrees(180f - instance.yRot).transform(offset)
}
fun resetDriverRotations(
instance: Camera,
driver: Entity,
) {
val eulerAngs = Quaternionf(instance.rotation()).getEulerAnglesYXZ(Vector3f())
val playerRotater = driver as RidePassenger
// Backticks because kotlin explodes otherwise due to syntax
playerRotater.`cobblemon$setRideXRot`(-eulerAngs.x() * Mth.RAD_TO_DEG)
playerRotater.`cobblemon$setRideYRot`(180.0f - (eulerAngs.y() * Mth.RAD_TO_DEG))
val pos = locatorOffset.add(entityPos).toVector3f()
val offsetDistance = offset.length()
val offsetDirection = offset.mul(1 / offsetDistance)
// Use getMaxZoom to calculate clipping
val maxZoom: Float = getMaxZoom(offsetDistance, offsetDirection, pos, instance)
return Vec3(offsetDirection.mul(maxZoom).add(pos))
} else {
val xRot = (-1 * Math.toRadians(instance.xRot.toDouble())).toFloat()
// get orientation based on OrientationController or normal rotations
val orientation =
if (controller.isActive() && controller.orientation != null)
controller.orientation!!
else
Matrix3f()
.rotateY(Math.toRadians((180f - instance.yRot).toDouble()).toFloat())
.rotateX(xRot)
// Get pivot from poser
val pivotOffsets = model.thirdPersonPivotOffset
val pokemonCenter = Vector3f(0f, entity.bbHeight/2, 0f)
var pivot = Vector3f(pokemonCenter)
val locatorName = delegate.getSeatLocator(entity)
if (pivotOffsets.containsKey(locatorName)) {
pivot = pivotOffsets[locatorName]!!.toVector3f()
pivot.sub(pokemonCenter)
orientation.transform(pivot)
pivot.add(pokemonCenter)
}
val pos = entityPos.toVector3f().add(pivot)
// Get offset from poser
val offset = getThirdPersonOffset(thirdPersonReverse, model.thirdPersonCameraOffsetNoViewBobbing, locatorName)
val rotatedOffset = orientation.transform(offset)
val offsetDistance = rotatedOffset.length()
val offsetDirection = rotatedOffset.mul(1 / offsetDistance)
// Use getMaxZoom to calculate clipping
val maxZoom: Float = getMaxZoom(offsetDistance, offsetDirection, pos, instance)
return Vec3(offsetDirection.mul(maxZoom).add(pos))
}
}
private fun getThirdPersonOffset(
thirdPersonReverse: Boolean,
cameraOffsets: Map<String, Vec3>,
locatorName: String,
shouldFlip: Boolean = true
locatorName: String
): Vector3f {
val offset = if (thirdPersonReverse && cameraOffsets.containsKey(locatorName + "_reverse")) {
cameraOffsets[locatorName + "_reverse"]!!.toVector3f()
@@ -139,10 +152,7 @@ object MountedCameraRenderer
} else {
Vector3f(0f, 2f, 4f)
}
// Don't need to account for this since orientation is derived from camera rotation and that z is already flipped.
// Flip only when not on a rollable.
if (thirdPersonReverse && shouldFlip) offset.z *= -1
if (thirdPersonReverse) offset.z *= -1
return offset
}
@@ -16,6 +16,7 @@ import com.cobblemon.mod.common.client.ClientMoLangFunctions.setupClient
import com.cobblemon.mod.common.client.render.models.blockbench.animation.ActiveAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.animation.PoseAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.animation.SingleBoneLookAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.TextureInterpolationType
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
import com.cobblemon.mod.common.client.render.models.blockbench.quirk.SimpleQuirk
@@ -177,4 +178,38 @@ class JsonPose(model: PosableModel, json: JsonObject) {
val transitions = json.get("transitions")?.takeIf { it is JsonObject }?.asJsonObject?.entrySet()?.map { (key, value) ->
JsonPoseTransition(key, value.asString.asExpressionLike())
} ?: emptyList()
val texturePhaseIn = json.get("texturePhaseIn")?.takeIf { it is JsonObject }?.asJsonObject?.let { theObject ->
val interpolationType = when(theObject.get("interpolation")?.asString) {
"step" -> TextureInterpolationType.STEP
"linear" -> TextureInterpolationType.LINEAR
"bezier" -> TextureInterpolationType.BEZIER
else -> TextureInterpolationType.STEP
}
val cubicValues = (theObject.get("cubicValues")?.asString ?: "0.0, 0.0, 1.0, 1.0").let { values ->
val list = mutableListOf<Double>()
values.split(",").forEach {
list.add(it.toDouble())
}
list
}
Pair(interpolationType, cubicValues)
} ?: Pair(TextureInterpolationType.STEP, mutableListOf(0.0, 0.0, 1.0, 1.0))
val texturePhaseOut = json.get("texturePhaseOut")?.takeIf { it is JsonObject }?.asJsonObject?.let { theObject ->
val interpolationType = when(theObject.get("interpolation")?.asString) {
"step" -> TextureInterpolationType.STEP
"linear" -> TextureInterpolationType.LINEAR
"bezier" -> TextureInterpolationType.BEZIER
else -> TextureInterpolationType.STEP
}
val cubicValues = (theObject.get("cubicValues")?.asString ?: "0.0, 0.0, 1.0, 1.0").let { values ->
val list = mutableListOf<Double>()
values.split(",").forEach {
list.add(it.toDouble())
}
list
}
Pair(interpolationType, cubicValues)
}
}
@@ -19,11 +19,13 @@ import com.cobblemon.mod.common.client.ClientMoLangFunctions.animationFunctions
import com.cobblemon.mod.common.client.ClientMoLangFunctions.setupClient
import com.cobblemon.mod.common.client.entity.PokemonClientDelegate
import com.cobblemon.mod.common.client.render.AnimatedModelTextureSupplier
import com.cobblemon.mod.common.client.render.KeyframableTextureSupplier
import com.cobblemon.mod.common.client.render.ModelLayer
import com.cobblemon.mod.common.client.render.models.blockbench.animation.*
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockActiveAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockAnimationRepository
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockPoseAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.TextureInterpolationType
import com.cobblemon.mod.common.client.render.models.blockbench.frame.ModelFrame
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.CryProvider
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Bone
@@ -276,7 +278,9 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
onTransitionedInto: (PosableState) -> Unit = {},
animations: Array<PoseAnimation> = emptyArray(),
transformedParts: Array<ModelPartTransformation> = emptyArray(),
quirks: Array<ModelQuirk<*>> = emptyArray()
quirks: Array<ModelQuirk<*>> = emptyArray(),
texturePhaseIn: Pair<TextureInterpolationType, MutableList<Double>> = Pair(TextureInterpolationType.STEP, mutableListOf(0.0, 0.0, 1.0, 1.0)),
texturePhaseOut: Pair<TextureInterpolationType, MutableList<Double>>? = null
): Pose {
return Pose(
poseType.name,
@@ -288,7 +292,9 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
namedAnimations,
animations,
transformedParts,
quirks
quirks,
texturePhaseIn,
texturePhaseOut
).also {
poses[poseType.name] = it
}
@@ -304,7 +310,9 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
onTransitionedInto: (PosableState) -> Unit = {},
animations: Array<PoseAnimation> = emptyArray(),
transformedParts: Array<ModelPartTransformation> = emptyArray(),
quirks: Array<ModelQuirk<*>> = emptyArray()
quirks: Array<ModelQuirk<*>> = emptyArray(),
texturePhaseIn: Pair<TextureInterpolationType, MutableList<Double>> = Pair(TextureInterpolationType.STEP, mutableListOf(0.0, 0.0, 1.0, 1.0)),
texturePhaseOut: Pair<TextureInterpolationType, MutableList<Double>>? = null
): Pose {
return Pose(
poseName,
@@ -316,7 +324,9 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
namedAnimations,
animations,
transformedParts,
quirks
quirks,
texturePhaseIn,
texturePhaseOut
).also {
poses[poseName] = it
}
@@ -332,7 +342,9 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
onTransitionedInto: (PosableState) -> Unit = {},
animations: Array<PoseAnimation> = emptyArray(),
transformedParts: Array<ModelPartTransformation> = emptyArray(),
quirks: Array<ModelQuirk<*>> = emptyArray()
quirks: Array<ModelQuirk<*>> = emptyArray(),
texturePhaseIn: Pair<TextureInterpolationType, MutableList<Double>> = Pair(TextureInterpolationType.STEP, mutableListOf(0.0, 0.0, 1.0, 1.0)),
texturePhaseOut: Pair<TextureInterpolationType, MutableList<Double>>? = null
): Pose {
return Pose(
poseName,
@@ -344,7 +356,9 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
namedAnimations,
animations,
transformedParts,
quirks
quirks,
texturePhaseIn,
texturePhaseOut
).also {
poses[poseName] = it
}
@@ -412,13 +426,6 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
fun registerRelevantPart(pairing: Pair<String, ModelPart>) = registerRelevantPart(pairing.first, pairing.second)
/** Needed a custom one, so I can make it take in a dynamic texture instead of resource location */
class DynamicStateShard(texture: DynamicTexture) : RenderStateShard.EmptyTextureStateShard(Runnable {
RenderSystem.setShaderTexture(0, texture.id)
}, Runnable {
texture.close() // Cleanup
})
/** Renders the model. Assumes rotations have been set. Will simply render the base model and then any extra layers. */
fun render(
context: RenderContext,
@@ -453,16 +460,9 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
val provider = bufferProvider
if (provider != null) {
for (layer in currentLayers) {
var renderLayer : RenderType
if (layer.texture is AnimatedModelTextureSupplier && layer.texture.interpolation) {
//Handle Interpolation
val texture = layer.texture.interpolatedTexture(currentState ?: FloatingState()) ?: continue
renderLayer = makeLayer(DynamicStateShard(texture), layer.emissive, layer.translucent, layer.translucent_cull)
}
else {
val texture = layer.texture?.invoke(currentState ?: FloatingState()) ?: continue
renderLayer = getLayer(texture, layer.emissive, layer.translucent, layer.translucent_cull)
}
(layer.texture as? KeyframableTextureSupplier)?.layerName = layer.name
val texture = layer.texture?.invoke(currentState ?: FloatingState()) ?: continue
val renderLayer = makeLayer(texture, layer.emissive, layer.translucent, layer.translucent_cull)
val consumer = provider.getBuffer(renderLayer)
val tint = layer.tint
val tintRed = (tint.x * r2 * 255).toInt()
@@ -747,8 +747,9 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
// If scale is 0 we start getting NaNs
scale.coerceAtLeast(0.01f)
if (entity.passengers.isNotEmpty() && entity.controllingPassenger is OrientationControllable
&& (entity as OrientationControllable).orientationController.active){
val controller = entity.orientationController
&& (entity.controllingPassenger as OrientationControllable).orientationController.active){
val controllingPassenger = entity.controllingPassenger as OrientationControllable
val controller = controllingPassenger.orientationController
val transformationMatrix = Matrix4f()
val center = Vector3f(0f, entity.bbHeight/2, 0f)
transformationMatrix.translate(center)
@@ -90,7 +90,9 @@ class PoseAdapter(
animations = pose.idleAnimations,
transformedParts = pose.transformedParts,
quirks = pose.quirks.toTypedArray(),
namedAnimations = pose.animations
namedAnimations = pose.animations,
texturePhaseIn = pose.texturePhaseIn,
texturePhaseOut = pose.texturePhaseOut
).also {
it.transitions.putAll(
pose.transitions
@@ -132,7 +132,9 @@ data class BedrockAnimation(
val shouldLoop: Boolean,
val animationLength: Double,
val effects: List<BedrockEffectKeyframe>,
val boneTimelines: Map<String, BedrockBoneTimeline>
val boneTimelines: Map<String, BedrockBoneTimeline>,
//This is custom data added by our custom BB plugin
val cobblemonTextures: MutableMap<String, TreeMap<Double, CobblemonBedrockTextureKeyframe>>
) {
fun checkForErrors() {
boneTimelines.forEach { (_, timeline) ->
@@ -396,4 +398,14 @@ enum class InterpolationType {
enum class Transformation {
POSITION, ROTATION, SCALE
}
data class CobblemonBedrockTextureKeyframe(
val textureSlot: Int,
val cubicLerpFunctionValues: MutableList<Double>,
val interpolationType: TextureInterpolationType
)
enum class TextureInterpolationType {
STEP, LINEAR, BEZIER
}
@@ -20,6 +20,7 @@ import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import java.lang.reflect.Type
import java.util.TreeMap
/**
* Gson adapter for converting bedrock/blockbench json data into a friendlier object model.
@@ -89,7 +90,27 @@ object BedrockAnimationAdapter : JsonDeserializer<BedrockAnimation> {
)
}
return BedrockAnimation(shouldLoop, animationLength, effects, boneTimelines)
//Custom Data reading added by our custom BB plugin
val cobblemonTextures = mutableMapOf<String, TreeMap<Double, CobblemonBedrockTextureKeyframe>>()
json["cobblemon_texture"]?.asJsonObject?.entrySet()?.forEach { (texture, timeline) ->
val textureTimeline = TreeMap<Double, CobblemonBedrockTextureKeyframe>()
timeline.asJsonObject.entrySet().forEach { (frame, values) ->
val lerpFunctionValues = mutableListOf<Double>()
values.asJsonObject["cobblemon_cubic_lerp_function"].asString.split(",").forEach {
lerpFunctionValues.add(it.toDouble())
}
val interpolation = when(values.asJsonObject["cobblemon_interpolation"].asString) {
"step" -> TextureInterpolationType.STEP
"linear" -> TextureInterpolationType.LINEAR
"bezier" -> TextureInterpolationType.BEZIER
else -> TextureInterpolationType.STEP
}
textureTimeline[frame.toDouble()] = CobblemonBedrockTextureKeyframe(values.asJsonObject["cobblemon_texture_slot"].asInt, lerpFunctionValues, interpolation)
}
cobblemonTextures[texture] = textureTimeline
}
return BedrockAnimation(shouldLoop, animationLength, effects, boneTimelines, cobblemonTextures)
}
else {
throw IllegalStateException("animation json could not be parsed")
@@ -13,6 +13,7 @@ import com.cobblemon.mod.common.client.render.models.blockbench.PosableModel
import com.cobblemon.mod.common.client.render.models.blockbench.PosableState
import com.cobblemon.mod.common.client.render.models.blockbench.animation.ActiveAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.animation.PoseAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.TextureInterpolationType
import com.cobblemon.mod.common.client.render.models.blockbench.quirk.ModelQuirk
import com.cobblemon.mod.common.client.render.models.blockbench.repository.RenderContext
import com.cobblemon.mod.common.entity.PoseType
@@ -39,7 +40,10 @@ class Pose(
val namedAnimations: MutableMap<String, ExpressionLike> = mutableMapOf(),
val animations: Array<PoseAnimation>,
val transformedParts: Array<ModelPartTransformation>,
val quirks: Array<ModelQuirk<*>>
val quirks: Array<ModelQuirk<*>>,
/** These are very specific to the animated texture**/
val texturePhaseIn: Pair<TextureInterpolationType, MutableList<Double>>,
val texturePhaseOut: Pair<TextureInterpolationType, MutableList<Double>>?,
) {
fun isSuitable(state: PosableState) = condition?.invoke(state) ?: true
@@ -42,9 +42,14 @@ import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.math.DoubleRange
import com.cobblemon.mod.common.util.math.geometry.toRadians
import com.cobblemon.mod.common.util.math.remap
import com.google.common.primitives.Floats
import com.mojang.blaze3d.vertex.PoseStack
import com.mojang.math.Axis
import kotlin.math.PI
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.sqrt
import net.minecraft.ChatFormatting
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.Font.DisplayMode
@@ -62,8 +67,11 @@ import net.minecraft.resources.ResourceLocation
import net.minecraft.util.Mth
import net.minecraft.world.entity.Entity
import net.minecraft.world.phys.Vec3
import org.joml.*
import kotlin.math.*
import org.joml.Math
import org.joml.Matrix4f
import org.joml.Quaternionf
import org.joml.Vector3f
import org.joml.Vector4f
class PokemonRenderer(
context: EntityRendererProvider.Context
@@ -181,14 +189,12 @@ class PokemonRenderer(
buffer: MultiBufferSource,
packedLight: Int
) {
val rollable = entity as? OrientationControllable ?: return
val driver = entity.firstPassenger ?: return
val rollable = driver as? OrientationControllable ?: return
val controller = rollable.orientationController
poseMatrix.pushPose()
if (!DISABLE_ROLLING_DEBUG && controller.active) {
// Allow the ride controller to modify its rotations using partialTick
entity.applyRenderRotation(partialTicks)
val matrix = poseMatrix.last().pose()
val yaw = Mth.rotLerp(partialTicks, entity.yBodyRotO, entity.yBodyRot)
val center = Vector3f(0f, entity.bbHeight/2, 0f)
@@ -8,10 +8,11 @@
package com.cobblemon.mod.common.command
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.Cobblemon.config
import com.cobblemon.mod.common.api.permission.CobblemonPermissions
import com.cobblemon.mod.common.api.spawning.CobblemonWorldSpawnerManager
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.position.calculators.SpawnablePositionCalculator.Companion.prioritizedAreaCalculators
import com.cobblemon.mod.common.api.spawning.spawner.SpawningZoneInput
import com.cobblemon.mod.common.api.text.add
import com.cobblemon.mod.common.api.text.green
@@ -24,6 +25,7 @@ import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.command.argument.SpawnBucketArgumentType
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.permission
import com.cobblemon.mod.common.util.spawner
import com.mojang.brigadier.Command
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.context.CommandContext
@@ -56,11 +58,11 @@ object CheckSpawnsCommand {
return 0
}
val spawner = CobblemonWorldSpawnerManager.spawnersForPlayers[player.uuid] ?: return Command.SINGLE_SUCCESS
val spawner = player.spawner
val bucket = SpawnBucketArgumentType.getSpawnBucket(context, name = "bucket")
val cause = SpawnCause(spawner, player)
val slice = spawner.spawningZoneGenerator.generate(
val slice = Cobblemon.spawningZoneGenerator.generate(
spawner = spawner,
input = SpawningZoneInput(
cause = cause,
@@ -74,9 +76,9 @@ object CheckSpawnsCommand {
)
)
val contexts = spawner.spawnablePositionResolver.resolve(spawner, spawner.spawnablePositionCalculators, slice)
val contexts = Cobblemon.areaSpawnablePositionResolver.resolve(spawner, prioritizedAreaCalculators, slice)
val spawnProbabilities = spawner.getSpawningSelector().getProbabilities(spawner, bucket, contexts)
val spawnProbabilities = spawner.selector.getProbabilities(spawner, bucket, contexts)
val spawnNames = mutableMapOf<String, MutableComponent>()
val namedProbabilities = mutableMapOf<MutableComponent, Float>()
@@ -9,7 +9,6 @@
package com.cobblemon.mod.common.command
import com.cobblemon.mod.common.api.permission.CobblemonPermissions
import com.cobblemon.mod.common.api.spawning.CobblemonWorldSpawnerManager
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.detail.EntitySpawnResult
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePosition
@@ -19,6 +18,7 @@ import com.cobblemon.mod.common.util.alias
import com.cobblemon.mod.common.util.commandLang
import com.cobblemon.mod.common.util.effectiveName
import com.cobblemon.mod.common.util.permission
import com.cobblemon.mod.common.util.spawner
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.arguments.IntegerArgumentType
import com.mojang.brigadier.context.CommandContext
@@ -31,7 +31,7 @@ import net.minecraft.commands.Commands.literal
*
* `/spawnpokemonfrompool [amount]` or the alias `/forcespawn [amount]`
*
* This command can fail if the randomly selection spawn region has no possible [AreaSpawnablePosition]. For example if
* This command can fail if the randomly selected spawn region has no possible [AreaSpawnablePosition]. For example if
* you are flying in the air
*/
object SpawnPokemonFromPool {
@@ -54,47 +54,26 @@ object SpawnPokemonFromPool {
private fun execute(context: CommandContext<CommandSourceStack>, amount: Int): Int {
val player = context.source.playerOrException
val spawner = CobblemonWorldSpawnerManager.spawnersForPlayers.getValue(player.uuid)
val spawner = player.spawner
var spawnsTriggered = 0
// This could instead directly use a [Spawner] method if refactored, as it is currently it has
// entity counting coupled to the generation of an entity to spawn. Might be a good future change?
for (i in 1..amount) {
val spawnCause = SpawnCause(spawner = spawner, entity = spawner.getCauseEntity())
val zoneInput = spawner.getZoneInput(spawnCause) ?: continue
val zone = spawner.spawningZoneGenerator.generate(spawner, zoneInput)
val contexts = spawner.spawnablePositionResolver.resolve(spawner, spawner.spawnablePositionCalculators, zone)
val influences = spawner.getAllInfluences() + zone.unconditionalInfluences
// This has a chance to fail, if you get a spawning zone that has no associated contexts.
// but as it was selected at random by the Spawning Zone Generator, it could just be a miss which
// means two attempts to spawn in the same location can have differing results (which is expected for
// randomness).
if (contexts.isEmpty()) {
repeat(times = amount) {
val spawnCause = SpawnCause(spawner = spawner, entity = player)
val results = spawner.runForArea(spawner.getZoneInput(spawnCause) ?: return@repeat, maxSpawns = 1)
if (results.isEmpty()) {
player.sendSystemMessage(UNABLE_TO_SPAWN.red())
continue
return@repeat
}
val bucket = spawner.chooseBucket(spawnCause, influences)
val spawnAction = spawner.getSpawningSelector().select(spawner, bucket, contexts, max = 1).firstOrNull() // one at a time
if (spawnAction == null) {
player.sendSystemMessage(UNABLE_TO_SPAWN.red())
continue
}
spawnAction.future.thenApply {
results.forEach {
if (it is EntitySpawnResult) {
for (entity in it.entities) {
player.sendSystemMessage(commandLang("spawnpokemonfrompool.success", entity.effectiveName()).green())
spawnsTriggered++
player.sendSystemMessage(commandLang("spawnpokemonfrompool", entity.effectiveName()).green())
}
}
}
spawnAction.complete()
spawnsTriggered++
}
return spawnsTriggered
@@ -155,6 +155,9 @@ class CobblemonConfig {
@CobblemonConfigField(Category.Spawning, lang = "pokemon_per_chunk", SERVER)
var pokemonPerChunk = 1F
@CobblemonConfigField(Category.Spawning, lang = "poke_snack_pokemon_per_chunk", SERVER)
var pokeSnackPokemonPerChunk = 2F
@CobblemonConfigField(Category.PassiveStatus, lang = "passive_statuses", SERVER)
var passiveStatuses = mutableMapOf(
Statuses.POISON.configEntry(),
@@ -273,7 +276,7 @@ class CobblemonConfig {
var summaryPokemonFollowCursor = true
@CobblemonConfigField(Category.Interface, lang = "party_portrait_animations", CLIENT)
var partyPortraitAnimations = PortraitStyle.NEVER_ANIMATE
var partyPortraitAnimations = PortraitStyle.ANIMATE_SELECTED
@CobblemonConfigField(Category.Riding, lang = "third_person_view_bobbing", CLIENT)
var thirdPersonViewBobbing = true
@@ -11,16 +11,17 @@ package com.cobblemon.mod.common.entity.ai
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
import com.cobblemon.mod.common.util.toBlockPos
import com.google.common.collect.ImmutableMap
import java.util.UUID
import kotlin.math.pow
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.MoverType
import net.minecraft.util.Mth
import net.minecraft.world.entity.ai.behavior.Behavior
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.entity.ai.memory.MemoryStatus
import net.minecraft.world.entity.ai.memory.WalkTarget
import net.minecraft.world.phys.Vec3
import java.util.*
import kotlin.math.cos
import kotlin.math.sin
class FollowHerdLeaderTask : Behavior<PokemonEntity>(
ImmutableMap.of(
@@ -34,7 +35,7 @@ class FollowHerdLeaderTask : Behavior<PokemonEntity>(
var tooFar = 8F
var closeEnough = 4F
/** Chance per tick of following the herd leader. */
val chance = 1 / 20F
val chance = 1 / 60F
override fun checkExtraStartConditions(level: ServerLevel, owner: PokemonEntity): Boolean {
leader = level.getEntity(owner.brain.getMemory(CobblemonMemories.HERD_LEADER).map(UUID::fromString).orElse(null) ?: return false) as? PokemonEntity
@@ -50,16 +51,19 @@ class FollowHerdLeaderTask : Behavior<PokemonEntity>(
override fun tick(level: ServerLevel, entity: PokemonEntity, gameTime: Long) {
val leader = leader ?: return
if (entity.brain.hasMemoryValue(MemoryModuleType.WALK_TARGET)) {
return // We're busy goin' n' shit
}
val moveControl = entity.moveControl as? PokemonMoveControl ?: return
val leaderMoveControl = leader.moveControl as? PokemonMoveControl ?: return
if (leader.distanceTo(entity) > tooFar) {
entity.brain.setMemory(
MemoryModuleType.WALK_TARGET,
CobblemonWalkTarget(
pos = leader.blockPosition(),
pos = leader.position().add(getRandomOffset(entity, level)).toBlockPos(),
speedModifier = 0.4F,
completionRange = closeEnough.toInt()
)
@@ -69,10 +73,11 @@ class FollowHerdLeaderTask : Behavior<PokemonEntity>(
entity.brain.eraseMemory(CobblemonMemories.HERD_LEADER) // We can't go where this guy's going
return
}
entity.brain.setMemory(
MemoryModuleType.WALK_TARGET,
CobblemonWalkTarget(
pos = leader.blockPosition(),
pos = leader.position().add(getRandomOffset(entity, level)).toBlockPos(),
speedModifier = 0.3F,
completionRange = closeEnough.toInt()
)
@@ -83,7 +88,7 @@ class FollowHerdLeaderTask : Behavior<PokemonEntity>(
entity.brain.setMemory(
MemoryModuleType.WALK_TARGET,
CobblemonWalkTarget(
pos = leader.blockPosition(),
pos = leader.position().add(getRandomOffset(entity, level)).toBlockPos(),
speedModifier = 0.4F,
completionRange = 1
)
@@ -105,14 +110,15 @@ class FollowHerdLeaderTask : Behavior<PokemonEntity>(
// Go to the leader's walk target, roughly.
val walkTarget = leader.brain.getMemory(MemoryModuleType.WALK_TARGET).orElse(null) ?: return
if (entity.distanceToSqr(walkTarget.target.currentPosition()) < leader.distanceToSqr(walkTarget.target.currentPosition())) {
return // We're already closer to the walk target than the leader is, so no need to move, if anything we should get out of the way
// We're already closer to the walk target than the leader is, so no need to move, if anything we should get out of the way
if (entity.distanceToSqr(walkTarget.target.currentPosition()) + closeEnough < leader.distanceToSqr(walkTarget.target.currentPosition())) {
return
}
entity.brain.setMemory(
MemoryModuleType.WALK_TARGET,
CobblemonWalkTarget(
pos = walkTarget.target.currentBlockPosition(),
pos = walkTarget.target.currentPosition().add(getRandomOffset(entity, level)).toBlockPos(),
speedModifier = walkTarget.speedModifier,
nodeTypeFilter = (walkTarget as? CobblemonWalkTarget)?.nodeTypeFilter ?: { true },
completionRange = closeEnough.toInt()
@@ -120,4 +126,23 @@ class FollowHerdLeaderTask : Behavior<PokemonEntity>(
)
}
}
fun getRandomOffset(entity: PokemonEntity, level: ServerLevel): Vec3 {
val leader = leader ?: return Vec3.ZERO
val minRange = closeEnough.toDouble()
val maxRange = (tooFar - closeEnough / 2).toDouble()
val randomDistance = Mth.nextDouble(level.random, minRange, maxRange)
val randomAngle = Mth.nextDouble(level.random, 0.0, 2 * Math.PI)
var offset = Vec3(cos(randomAngle) * randomDistance, 0.0, sin(randomAngle) * randomDistance)
if ((entity.isInLiquid && leader.isInLiquid) || (entity.isFlying() && leader.isFlying())) {
val verticalRange = randomDistance * 0.5
val randomY = Mth.nextDouble(level.random, -verticalRange, verticalRange)
offset = offset.add(0.0, randomY, 0.0)
}
return offset
}
}
@@ -26,6 +26,7 @@ import com.cobblemon.mod.common.api.spawning.fishing.FishingSpawnCause
import com.cobblemon.mod.common.api.spawning.influence.BucketNormalizingInfluence
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence.Companion.TYPICAL_VARIATION
import com.cobblemon.mod.common.api.spawning.position.FishingSpawnablePosition
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.client.sound.EntitySoundTracker
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
@@ -771,11 +772,16 @@ class PokeRodFishingBobberEntity(type: EntityType<out PokeRodFishingBobberEntity
lureLevel = lureLevel,
)
val result = spawner.getSpawnAction(
val spawnablePosition = FishingSpawnablePosition(
cause = spawnCause,
world = level() as ServerLevel,
pos = position().toBlockPos(),
influences = listOf(PlayerLevelRangeInfluence(player, TYPICAL_VARIATION), bucketInfluence)
influences = mutableListOf(PlayerLevelRangeInfluence(player, TYPICAL_VARIATION), bucketInfluence)
)
val result = spawner.calculateSpawnActionForPosition(
cause = spawnCause,
spawnablePosition = spawnablePosition
)
this.plannedSpawnAction = result
@@ -811,8 +817,11 @@ class PokeRodFishingBobberEntity(type: EntityType<out PokeRodFishingBobberEntity
// create accessory splash particle when you fish something up
particleEntityHandler(this, ResourceLocation.fromNamespaceAndPath("cobblemon", "accessory_fish_splash"))
var baitId = PokerodItem.getBaitStackOnRod(this.bobberBait).takeUnless { it.isEmpty }?.itemHolder?.unwrapKey()
?.orElse(null)?.location() ?: "empty_bait".asIdentifierDefaultingNamespace()
val baitId =
if (bobberBait.isEmpty)
"empty_bait".asIdentifierDefaultingNamespace()
else
bobberBait.itemHolder.unwrapKey().map { it.location() }.orElse("empty_bait".asIdentifierDefaultingNamespace())
val pokemonId = spawnedPokemon.pokemon.species.resourceIdentifier
CobblemonCriteria.REEL_IN_POKEMON.trigger(player, ReelInPokemonContext(pokemonId, baitId))
@@ -850,7 +859,7 @@ class PokeRodFishingBobberEntity(type: EntityType<out PokeRodFishingBobberEntity
BobberSpawnPokemonEvent.Post(
this,
spawnAction,
rodItemStack,
bobberBait,
entity
)
)
@@ -18,7 +18,6 @@ import com.cobblemon.mod.common.CobblemonItems
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.OrientationControllable
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.drop.DropTable
import com.cobblemon.mod.common.api.entity.Despawner
@@ -121,6 +120,7 @@ import com.cobblemon.mod.common.util.math.geometry.toRadians
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules
import com.mojang.serialization.Codec
import com.mojang.serialization.Dynamic
import net.minecraft.client.Minecraft
import java.util.Optional
import java.util.UUID
import java.util.concurrent.CompletableFuture
@@ -2188,19 +2188,9 @@ open class PokemonEntity(
}
this.yRotO = this.yRot
val rotation = behaviour.rotation(settings, state, this, driver)
if (this is OrientationControllable) {
val controller = this.orientationController
if (!controller.isActive()) {
val rotation = behaviour.rotation(settings, state, this, driver)
// TODO: Find a better solution than setting the vehicle xrot to zero.
// The problem is that nothing actually effects the vehicle/pokemon xrot so when it gets set to
// -45 degrees by a rollable ride controller it gets saved off and not modified until you try
// and takeoff again. And at that point you snap to -45 pitch in the orientationControllers matrix
setRot(rotation.y, 0.0f)
}
}
setRot(rotation.y, rotation.x)
this.yHeadRot = this.yRot
this.yBodyRot = this.yRot
this.passengers.filterIsInstance<LivingEntity>()
@@ -2321,12 +2311,6 @@ 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
@@ -2424,8 +2408,12 @@ open class PokemonEntity(
fun rideFovMult(): Float {
val driver = this.controllingPassenger as? Player ?: return 1.0f
val fovEffectScale = Minecraft.getInstance().options.fovEffectScale().get().toFloat()
return ifRidingAvailableSupply(fallback = 1.0f) { behaviour, settings, state ->
behaviour.rideFovMultiplier(settings, state, this, driver)
val rideFov = behaviour.rideFovMultiplier(settings, state, this, driver)
1f + (rideFov - 1f) * fovEffectScale
}
}
@@ -45,10 +45,12 @@ class DrowsySensor : Sensor<PokemonEntity>(20) {
entity.brain.eraseMemory(CobblemonMemories.POKEMON_DROWSY)
}
} else {
if (!isDrowsy && shouldBeDrowsy) {
if (!isDrowsy && shouldBeDrowsy && entity.random.nextFloat() < rest.drowsyChance) {
entity.brain.setMemory(CobblemonMemories.POKEMON_DROWSY, true)
} else if (isDrowsy && !shouldBeDrowsy) {
entity.brain.eraseMemory(CobblemonMemories.POKEMON_DROWSY)
} else if (isDrowsy && entity.random.nextFloat() < rest.rouseChance) {
entity.brain.eraseMemory(CobblemonMemories.POKEMON_DROWSY)
}
}
}
@@ -8,7 +8,6 @@
package com.cobblemon.mod.common.events
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.util.party
import net.minecraft.server.MinecraftServer
@@ -17,7 +16,6 @@ object ServerTickHandler {
private var secondsTick = 0
fun onTick(server: MinecraftServer) {
Cobblemon.bestSpawner.spawnerManagers.forEach { it.onServerTick() }
BattleRegistry.tick()
secondsTick++
@@ -85,6 +85,7 @@ class CampfirePotRecipeCategory(registration: IRecipeCategoryRegistration) : IRe
if (recipe.seasoningProcessors.isNotEmpty()) {
val processedSeasonings = EXAMPLE_SEASONINGS.filter { item ->
var valid = true
if (!item.`is`(recipe.seasoningTag)) return@filter false
recipe.seasoningProcessors.forEach {
if (!it.consumesItem(item)) {
valid = false
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.item
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.item.battle.BagItem
import com.cobblemon.mod.common.item.battle.SimpleBagItemLike
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Items
class MoomooMilk(properties: Properties) : CobblemonItem(properties), SimpleBagItemLike {
override val bagItem = object : BagItem {
override val itemName = "item.cobblemon.moomoo_milk"
override val returnItem = Items.GLASS_BOTTLE
override fun canUse(stack: ItemStack, battle: PokemonBattle, target: BattlePokemon): Boolean {
return target.health > 0
}
override fun getShowdownInput(actor: BattleActor, battlePokemon: BattlePokemon, data: String?): String {
return "clear_boost"
}
}
}
@@ -53,7 +53,10 @@ interface BagItemLike {
battlePokemon.actor.forceChoose(BagItemActionResponse(bagItem, battlePokemon))
val stackName = BuiltInRegistries.ITEM.getKey(stack.item)
stack.consume(1, player)
if (!player.hasInfiniteMaterials()) {
stack.shrink(1)
battlePokemon.actor.itemsUsed.add(bagItem)
}
CobblemonCriteria.POKEMON_INTERACT.trigger(player, PokemonInteractContext(battlePokemon.effectedPokemon.species.resourceIdentifier, stackName))
return true
}
@@ -10,15 +10,23 @@ package com.cobblemon.mod.common.item.crafting
import com.cobblemon.mod.common.CobblemonRecipeSerializers
import com.cobblemon.mod.common.CobblemonRecipeTypes
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.util.codec.CodecUtils.createByStringCodec
import com.cobblemon.mod.common.util.readIdentifier
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.writeIdentifier
import com.cobblemon.mod.common.util.writeString
import com.mojang.serialization.Codec
import com.mojang.serialization.MapCodec
import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.core.NonNullList
import net.minecraft.core.registries.Registries
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.network.codec.StreamCodec
import net.minecraft.resources.ResourceKey
import net.minecraft.tags.TagEntry
import net.minecraft.tags.TagKey
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.crafting.CraftingInput
import net.minecraft.world.item.crafting.Ingredient
@@ -31,6 +39,7 @@ class CookingPotRecipe(
override val result: ItemStack,
override val groupName: String,
override val category: CookingPotBookCategory,
override val seasoningTag: TagKey<Item>,
override val seasoningProcessors: List<SeasoningProcessor>
) : CookingPotRecipeBase {
override fun getType() = CobblemonRecipeTypes.COOKING_POT_COOKING
@@ -56,7 +65,8 @@ class CookingPotRecipe(
ShapedRecipePattern.MAP_CODEC.forGetter { recipe -> recipe.pattern },
ItemStack.STRICT_CODEC.fieldOf("result").forGetter { recipe -> recipe.result },
Codec.STRING.optionalFieldOf("group", "").forGetter { recipe -> recipe.group },
CookingPotBookCategory.Companion.CODEC.fieldOf("category").orElse(CookingPotBookCategory.MISC).forGetter { recipe -> recipe.category },
CookingPotBookCategory.CODEC.fieldOf("category").orElse(CookingPotBookCategory.MISC).forGetter { recipe -> recipe.category },
TagKey.codec(Registries.ITEM).fieldOf("seasoningTag").orElse(CobblemonItemTags.EMPTY).forGetter { recipe -> recipe.seasoningTag },
createByStringCodec<SeasoningProcessor>(
{ SeasoningProcessor.processors[it] },
{ it.type },
@@ -70,18 +80,20 @@ class CookingPotRecipe(
private fun fromNetwork(buffer: RegistryFriendlyByteBuf): CookingPotRecipe {
val group = buffer.readUtf()
val category = buffer.readEnum(CookingPotBookCategory::class.java)
val seasoningTag = TagKey.create(Registries.ITEM, buffer.readIdentifier())
val pattern = ShapedRecipePattern.STREAM_CODEC.decode(buffer)
val result = ItemStack.STREAM_CODEC.decode(buffer)
val seasoningProcessors = buffer.readList {
val type = buffer.readString()
SeasoningProcessor.processors[type] ?: error("Unknown seasoning processor: $type")
}
return CookingPotRecipe(pattern, result, group, category, seasoningProcessors)
return CookingPotRecipe(pattern, result, group, category, seasoningTag, seasoningProcessors)
}
private fun toNetwork(buffer: RegistryFriendlyByteBuf, recipe: CookingPotRecipe) {
buffer.writeUtf(recipe.groupName)
buffer.writeEnum(recipe.category)
buffer.writeIdentifier(recipe.seasoningTag.location)
ShapedRecipePattern.STREAM_CODEC.encode(buffer, recipe.pattern)
ItemStack.STREAM_CODEC.encode(buffer, recipe.result)
buffer.writeCollection(recipe.seasoningProcessors) { _, it ->
@@ -10,6 +10,8 @@ package com.cobblemon.mod.common.item.crafting
import com.cobblemon.mod.common.CobblemonItems
import net.minecraft.core.HolderLookup
import net.minecraft.tags.TagKey
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.crafting.CraftingInput
import net.minecraft.world.item.crafting.Recipe
@@ -19,6 +21,7 @@ interface CookingPotRecipeBase : Recipe<CraftingInput> {
val result: ItemStack
val groupName: String
val category: CookingPotBookCategory
val seasoningTag: TagKey<Item>
val seasoningProcessors: List<SeasoningProcessor>
override fun getGroup() = groupName
@@ -10,16 +10,22 @@ package com.cobblemon.mod.common.item.crafting
import com.cobblemon.mod.common.CobblemonRecipeSerializers
import com.cobblemon.mod.common.CobblemonRecipeTypes
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.util.codec.CodecUtils.createByStringCodec
import com.cobblemon.mod.common.util.readIdentifier
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.writeIdentifier
import com.cobblemon.mod.common.util.writeString
import com.mojang.serialization.Codec
import com.mojang.serialization.DataResult
import com.mojang.serialization.MapCodec
import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.core.NonNullList
import net.minecraft.core.registries.Registries
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.network.codec.StreamCodec
import net.minecraft.tags.TagKey
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.crafting.*
import net.minecraft.world.level.Level
@@ -29,6 +35,7 @@ class CookingPotShapelessRecipe(
override val category: CookingPotBookCategory,
override val result: ItemStack,
private val ingredients: NonNullList<Ingredient>,
override val seasoningTag: TagKey<Item>,
override val seasoningProcessors: List<SeasoningProcessor>
) : Recipe<CraftingInput>, CookingPotRecipeBase {
override fun getType(): RecipeType<CookingPotShapelessRecipe> = CobblemonRecipeTypes.COOKING_POT_SHAPELESS
@@ -68,6 +75,7 @@ class CookingPotShapelessRecipe(
else -> DataResult.success(NonNullList.of(Ingredient.EMPTY, *ingredients))
}
}, { DataResult.success(it) }).forGetter { it.ingredients },
TagKey.codec(Registries.ITEM).fieldOf("seasoningTag").orElse(CobblemonItemTags.EMPTY).forGetter { recipe -> recipe.seasoningTag },
createByStringCodec<SeasoningProcessor>(
{ SeasoningProcessor.processors[it] },
{ it.type },
@@ -82,6 +90,7 @@ class CookingPotShapelessRecipe(
private fun fromNetwork(buffer: RegistryFriendlyByteBuf): CookingPotShapelessRecipe {
val group = buffer.readUtf()
val category = buffer.readEnum(CookingPotBookCategory::class.java)
val seasoningTag = TagKey.create(Registries.ITEM, buffer.readIdentifier())
val size = buffer.readVarInt()
val ingredients = NonNullList.withSize(size, Ingredient.EMPTY)
ingredients.replaceAll { Ingredient.CONTENTS_STREAM_CODEC.decode(buffer) }
@@ -90,12 +99,13 @@ class CookingPotShapelessRecipe(
val type = buffer.readString()
SeasoningProcessor.processors[type] ?: error("Unknown seasoning processor: $type")
}
return CookingPotShapelessRecipe(group, category, result, ingredients, seasoningProcessors)
return CookingPotShapelessRecipe(group, category, result, ingredients, seasoningTag, seasoningProcessors)
}
private fun toNetwork(buffer: RegistryFriendlyByteBuf, recipe: CookingPotShapelessRecipe) {
buffer.writeUtf(recipe.group)
buffer.writeEnum(recipe.category)
buffer.writeIdentifier(recipe.seasoningTag.location)
buffer.writeVarInt(recipe.ingredients.size)
recipe.ingredients.forEach { ingredient ->
Ingredient.CONTENTS_STREAM_CODEC.encode(buffer, ingredient)
@@ -21,7 +21,6 @@ import com.cobblemon.mod.common.api.riding.behaviour.types.land.VehicleSettings
import com.cobblemon.mod.common.api.riding.behaviour.types.liquid.BoatSettings
import com.cobblemon.mod.common.api.riding.behaviour.types.liquid.BurstSettings
import com.cobblemon.mod.common.api.riding.behaviour.types.liquid.DolphinSettings
import com.cobblemon.mod.common.api.riding.behaviour.types.liquid.SubmarineSettings
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
@@ -40,8 +39,7 @@ class RideSettingsSyncPacket(
val vehicle: VehicleSettings,
val boat: BoatSettings,
val burst: BurstSettings,
val dolphin: DolphinSettings,
val submarine: SubmarineSettings
val dolphin: DolphinSettings
) : NetworkPacket<RideSettingsSyncPacket> {
override val id = ID
@@ -58,7 +56,6 @@ class RideSettingsSyncPacket(
boat.encode(buffer)
burst.encode(buffer)
dolphin.encode(buffer)
submarine.encode(buffer)
}
companion object {
@@ -76,8 +73,7 @@ class RideSettingsSyncPacket(
vehicle = VehicleSettings(),
boat = BoatSettings(),
burst = BurstSettings(),
dolphin = DolphinSettings(),
submarine = SubmarineSettings()
dolphin = DolphinSettings()
).apply {
bird.decode(buffer)
glider.decode(buffer)
@@ -91,7 +87,6 @@ class RideSettingsSyncPacket(
boat.decode(buffer)
burst.decode(buffer)
dolphin.decode(buffer)
submarine.decode(buffer)
}
}
}
@@ -15,13 +15,9 @@ import com.cobblemon.mod.common.util.writeMatrix3f
import net.minecraft.network.RegistryFriendlyByteBuf
import org.joml.Matrix3f
class ServerboundUpdateOrientationPacket internal constructor(
val entity: Int,
val orientation: Matrix3f?
) : NetworkPacket<ServerboundUpdateOrientationPacket> {
class ServerboundUpdateOrientationPacket internal constructor(val orientation: Matrix3f?) : NetworkPacket<ServerboundUpdateOrientationPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeInt(entity)
buffer.writeBoolean(orientation != null)
if (orientation != null) {
buffer.writeMatrix3f(orientation)
@@ -31,9 +27,8 @@ class ServerboundUpdateOrientationPacket internal constructor(
companion object {
val ID = cobblemonResource("c2s_update_orientation")
fun decode(buffer: RegistryFriendlyByteBuf): ServerboundUpdateOrientationPacket {
val entity = buffer.readInt()
val orientation = if (buffer.readBoolean()) buffer.readMatrix3f() else null
return ServerboundUpdateOrientationPacket(entity, orientation)
return ServerboundUpdateOrientationPacket(orientation)
}
}
}
@@ -1,37 +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.net.messages.server.riding
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.world.phys.Vec3
class ServerboundUpdateRiderRotationPacket internal constructor(
val riderXRot: Float,
val riderYRot: Float,
val rideEyePos: Vec3
) : NetworkPacket<ServerboundUpdateRiderRotationPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeFloat(riderXRot)
buffer.writeFloat(riderYRot)
buffer.writeVec3(rideEyePos)
}
companion object {
val ID = cobblemonResource("c2s_update_rider_rotation")
fun decode(buffer: RegistryFriendlyByteBuf): ServerboundUpdateRiderRotationPacket {
val riderXRot = buffer.readFloat()
val riderYRot = buffer.readFloat()
val rideEyePos = buffer.readVec3()
return ServerboundUpdateRiderRotationPacket(riderXRot, riderYRot, rideEyePos)
}
}
}
@@ -10,19 +10,15 @@ package com.cobblemon.mod.common.net.serverhandling.orientation
import com.cobblemon.mod.common.OrientationControllable
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.server.orientation.ServerboundUpdateOrientationPacket
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
import kotlin.math.atan2
object OrientationPacketHandler : ServerNetworkPacketHandler<ServerboundUpdateOrientationPacket> {
override fun handle(packet: ServerboundUpdateOrientationPacket, server: MinecraftServer, player: ServerPlayer) {
val entity = player.level().getEntity(packet.entity) ?: return
if (entity !is PokemonEntity) return
if (entity.controllingPassenger != player) return
if (entity !is OrientationControllable) return
entity.orientationController.updateOrientation { _ -> packet.orientation }
if (player is OrientationControllable) {
player.orientationController.updateOrientation { _ -> packet.orientation }
}
}
}
@@ -1,30 +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.net.serverhandling.riding
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.duck.RidePassenger
import com.cobblemon.mod.common.net.messages.server.riding.ServerboundUpdateRiderRotationPacket
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
object ServerboundUpdateRiderRotationHandler : ServerNetworkPacketHandler<ServerboundUpdateRiderRotationPacket> {
override fun handle(
packet: ServerboundUpdateRiderRotationPacket,
server: MinecraftServer,
player: ServerPlayer
) {
if (player is RidePassenger) {
player.`cobblemon$setRideXRot`(packet.riderXRot)
player.`cobblemon$setRideYRot`(packet.riderYRot)
player.`cobblemon$setRideEyePos`(packet.rideEyePos)
}
}
}

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