forked from mirrors/cobblemon
Compare commits
104
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea99b339ea | ||
|
|
5b23b0cfbe | ||
|
|
778ceff183 | ||
|
|
f32050c8b5 | ||
|
|
3522bac72a | ||
|
|
ecf037666f | ||
|
|
618d062ca9 | ||
|
|
49841b40ec | ||
|
|
87141e1137 | ||
|
|
1f962d664f | ||
|
|
09797f25b1 | ||
|
|
3911413c69 | ||
|
|
12f3e7b78b | ||
|
|
fdb37ba5e8 | ||
|
|
7a99845d0f | ||
|
|
247052ee67 | ||
|
|
29f6524c49 | ||
|
|
1e617a684b | ||
|
|
c9a0352a7c | ||
|
|
d157536e1e | ||
|
|
fb90ccb86e | ||
|
|
c112f9615c | ||
|
|
fc882190fe | ||
|
|
15d7cd815d | ||
|
|
fefdfd877b | ||
|
|
35fb170225 | ||
|
|
d2e405131f | ||
|
|
b6ec0205ef | ||
|
|
e57a42e755 | ||
|
|
5119617f00 | ||
|
|
6e84efb53c | ||
|
|
de3191b768 | ||
|
|
76ec4b4d5a | ||
|
|
25702ade61 | ||
|
|
c6da88d0b2 | ||
|
|
b22a31909d | ||
|
|
cbbbacc215 | ||
|
|
d22d81209d | ||
|
|
8571d78894 | ||
|
|
65c5e95ddf | ||
|
|
0fae95aabc | ||
|
|
bbfd864b33 | ||
|
|
b107d76762 | ||
|
|
8d4f10349c | ||
|
|
f36787e67f | ||
|
|
fc198c599e | ||
|
|
26f753880c | ||
|
|
a9ebb0b6f2 | ||
|
|
a7158ee57a | ||
|
|
06c3fd3f09 | ||
|
|
4e196e8a29 | ||
|
|
871f33f442 | ||
|
|
21c1f912a7 | ||
|
|
776fb5413f | ||
|
|
f645fd9b60 | ||
|
|
ac161ae5b4 | ||
|
|
fc3542e3eb | ||
|
|
88932c8c41 | ||
|
|
411f98d0ae | ||
|
|
02a1929ede | ||
|
|
ff90059eec | ||
|
|
4a30bb3b04 | ||
|
|
fac8c5f510 | ||
|
|
2d077c607a | ||
|
|
f75e3d11da | ||
|
|
f46968f878 | ||
|
|
3439aeff1e | ||
|
|
2ada7e0b68 | ||
|
|
7f6197ab8b | ||
|
|
e197287775 | ||
|
|
be019c79df | ||
|
|
73fe851c58 | ||
|
|
8544744530 | ||
|
|
32d7bfcef3 | ||
|
|
27263cd057 | ||
|
|
55f0962205 | ||
|
|
e753c75c31 | ||
|
|
4e96222a4d | ||
|
|
4ee90d4cfb | ||
|
|
540cd612c1 | ||
|
|
4b8196685a | ||
|
|
08c2a0d07b | ||
|
|
1f68fdac8c | ||
|
|
e8d4f0fcf4 | ||
|
|
5d522bd608 | ||
|
|
3894a99bc4 | ||
|
|
9036a2f317 | ||
|
|
c75ab28002 | ||
|
|
62a8320068 | ||
|
|
f21b38f66a | ||
|
|
596765bb52 | ||
|
|
297aafba45 | ||
|
|
a0a682e224 | ||
|
|
d161b11892 | ||
|
|
d70bb243b2 | ||
|
|
301a3efb7c | ||
|
|
05b7cbe20a | ||
|
|
48cbbc1bef | ||
|
|
f8f7c28274 | ||
|
|
0ebb53888a | ||
|
|
c0071c1525 | ||
|
|
71f7b3b3d8 | ||
|
|
bd6d7b7149 | ||
|
|
63de8fe44a |
+2
-1
@@ -12,7 +12,8 @@ out
|
||||
*.ipr
|
||||
*.iws
|
||||
*.iml
|
||||
.idea
|
||||
.idea/*
|
||||
!.idea/icon.png
|
||||
|
||||
# gradle
|
||||
build
|
||||
|
||||
Generated
BIN
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
@@ -12,6 +12,14 @@
|
||||
- SpawningContext is now SpawnablePosition
|
||||
- WorldSlice is SpawningZone
|
||||
- SpawningProspector is now SpawningZoneGenerator
|
||||
- Majorly refactored the hierarchy of Spawner
|
||||
- The base Spawner interface provides more functions to allow single-point and area spawning given appropriate inputs.
|
||||
- TickingSpawner is removed in favour of outside code handling ticking logic.
|
||||
- AreaSpawner is removed.
|
||||
- BasicSpawner is the first implementation of Spawner which can be used for any purpose.
|
||||
- PlayerSpawners are now mixin'd into ServerPlayer and ticked from ServerPlayer#tick.
|
||||
- A hierarchy diagram can be found at `./docs/spawner-hierarchy.png` in the mod repository.
|
||||
- The SpawnerManager class has been removed as its functionality is all now handled elsewhere.
|
||||
- Renamed things in Spawn Rules to go with the other renames:
|
||||
- contextSelector is now spawnablePositionSelector
|
||||
- contextSelector is now spawnablePositionSelector
|
||||
- context is now spawnable_position
|
||||
+20
-2
@@ -54,6 +54,8 @@
|
||||
- `PokemonRecallEvent`
|
||||
- `TradeEvent`
|
||||
- Added a new gamerule, 'healerHealsPC', when set to true a successful use of a healer will also heal all of the Pokemon in that player's PC.
|
||||
- Mooshtanks will switch between their red and brown variant when hit by lightning
|
||||
- Smeargle spawns with differing tail colour depending on its [Characteristic](https://bulbapedia.bulbagarden.net/wiki/Characteristic) stat
|
||||
|
||||
### Pokémon Added
|
||||
|
||||
@@ -474,7 +476,7 @@
|
||||
- Updated `HatchEggEvent.Post` to include the Pokemon that hatched.
|
||||
- Roseli Berry Trees now naturally generate in their preferred biomes.
|
||||
- Refactored dynamic lights compat to be more future proof. The dynamic light support is only tested with LambDynamicLights, on NeoForged use that mod in combination with Sinytra Connector
|
||||
- Shulker Boxes and Traveler's Backpacks can no longer be held by Pokémon. Thanks Monocle ;) You could re-enable this with a datapack but you'd be crazy.
|
||||
- Shulker Boxes and Traveler's Backpacks AND Packed Up backpacks can no longer be held by Pokémon. Thanks Monocle ;) You could re-enable this with our datapack (but you'd be crazy).
|
||||
- Moomoo Milk now clears Pokémon stat changes when used in battle.
|
||||
|
||||
### Fixes
|
||||
@@ -599,10 +601,25 @@
|
||||
- Added a `hoverText` option to PartySelectCallback, to display a tooltip on hovering over a Pokémon in the selection screen.
|
||||
- `PokemonEntity` instances spawned into the world now appropriately finalize the spawn for mod compatibility.
|
||||
- Added PokedexManager.obtain as a replacement for .catch which is not a friendly function name in Java.
|
||||
|
||||
- Added `Pokemon#hyperTrainIV()` and `IVs#setHyperTrainedIV(Stat, Int)`
|
||||
- `ElementalType` now implements `ShowdownIdentifiable` to ensure the communcation with showdown stays consistent (also in regards to TeraTypes)
|
||||
- Pokemon no longer have a change observable
|
||||
- Pokémon can now have a behaviour changing the value of a species feature on lightning hit:
|
||||
```JSON
|
||||
{
|
||||
"behaviour": {
|
||||
"lightningHit": {
|
||||
"rotateFeatures": [
|
||||
{
|
||||
"key": "mooshtank",
|
||||
"chain": ["red", "brown"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
This will change the value of `mooshtank` from `red` to `brown` and vice versa when a lightning strikes this Pokémon. Only chains of which a value is the current value will be considered, so multiple entries for the same key with different chains can be added.
|
||||
|
||||
### MoLang & Datapacks
|
||||
- The following usages for item predicates can now use item conditions like advancements do, you can learn about them in the [Minecraft wiki](https://minecraft.wiki/w/Advancement_definition#minecraft:filled_bucket)
|
||||
@@ -709,6 +726,7 @@
|
||||
- Fixed `cobblemon:reel_in_pokemon` criteria not working when used together with a `baitId`
|
||||
- Also changed the default from `cobblemon:empty_bait` to `any`
|
||||
- The previous default is still available by using the above as baitId
|
||||
- Added support for species-specific move action effects, using the format `{move_id}_{species}.json`.
|
||||
|
||||
### Particles
|
||||
Added new/updated particles for the following moves:
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common;
|
||||
|
||||
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner;
|
||||
|
||||
/**
|
||||
* Used for mixin shenanigans. ServerPlayers now store their Cobblemon spawner
|
||||
* inside, so this interface helps execute that.
|
||||
*
|
||||
* @author Hiroku
|
||||
* @since October 27th, 2025
|
||||
*/
|
||||
public interface PlayerSpawnerAccessor {
|
||||
PlayerSpawner getPlayerSpawner();
|
||||
void setPlayerSpawner(PlayerSpawner spawner);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.mixin;
|
||||
|
||||
import com.cobblemon.mod.common.OrientationControllable;
|
||||
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;
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.mixin;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.util.ByIdMap;
|
||||
import net.minecraft.util.StringRepresentable;
|
||||
import net.minecraft.world.item.component.FireworkExplosion;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Mutable;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.gen.Invoker;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.IntFunction;
|
||||
|
||||
@Mixin(FireworkExplosion.Shape.class)
|
||||
public abstract class FireworkExplosionShapeMixin {
|
||||
|
||||
@Shadow
|
||||
@Final
|
||||
@Mutable
|
||||
private static FireworkExplosion.Shape[] $VALUES;
|
||||
|
||||
|
||||
@Shadow
|
||||
@Final
|
||||
@Mutable
|
||||
private static IntFunction<FireworkExplosion.Shape> BY_ID;
|
||||
|
||||
@Shadow
|
||||
@Final
|
||||
@Mutable
|
||||
public static StreamCodec<ByteBuf, FireworkExplosion.Shape> STREAM_CODEC;
|
||||
|
||||
@Shadow
|
||||
@Final
|
||||
@Mutable
|
||||
public static Codec<FireworkExplosion.Shape> CODEC;
|
||||
|
||||
@Invoker("<init>")
|
||||
private static FireworkExplosion.Shape cobblemon$createFireworkExplosionShape(String enumName, int internalId, int id, String name) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
@Inject(
|
||||
method = "<clinit>",
|
||||
at = @At("TAIL")
|
||||
)
|
||||
private static void cobblemon$addFireworkExplosionShape(CallbackInfo ci) {
|
||||
ArrayList<FireworkExplosion.Shape> types = new ArrayList<>(List.of($VALUES));
|
||||
|
||||
types.add(cobblemon$createFireworkExplosionShape("POKEBALL", $VALUES.length, 5, "pokeball"));
|
||||
|
||||
$VALUES = types.toArray(FireworkExplosion.Shape[]::new);
|
||||
|
||||
BY_ID = ByIdMap.continuous(FireworkExplosion.Shape::getId, FireworkExplosion.Shape.values(), ByIdMap.OutOfBoundsStrategy.ZERO);
|
||||
STREAM_CODEC = ByteBufCodecs.idMapper(BY_ID, FireworkExplosion.Shape::getId);
|
||||
CODEC = StringRepresentable.fromValues(FireworkExplosion.Shape::values);
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.mixin;
|
||||
|
||||
import com.llamalad7.mixinextras.sugar.Local;
|
||||
import it.unimi.dsi.fastutil.ints.IntList;
|
||||
import net.minecraft.client.particle.FireworkParticles;
|
||||
import net.minecraft.world.item.component.FireworkExplosion;
|
||||
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.callback.CallbackInfo;
|
||||
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
|
||||
|
||||
@Mixin(FireworkParticles.Starter.class)
|
||||
public abstract class FireworkParticlesMixin {
|
||||
|
||||
@Shadow
|
||||
protected abstract void createParticleShape(double speed, double[][] coords, IntList colors, IntList fadeColors, boolean trail, boolean twinkle, boolean isCreeper);
|
||||
|
||||
@Inject(
|
||||
method = "tick()V",
|
||||
at = @At(
|
||||
value = "INVOKE",
|
||||
target = "Lnet/minecraft/client/particle/Particle;setColor(FFF)V",
|
||||
shift = At.Shift.BEFORE,
|
||||
by = 2
|
||||
)
|
||||
)
|
||||
private void cobblemon$createPokeballParticleShape(
|
||||
CallbackInfo ci,
|
||||
@Local FireworkExplosion fireworkExplosion2,
|
||||
@Local(ordinal = 0) int bl3,
|
||||
@Local(ordinal = 0) int bl4,
|
||||
@Local(ordinal = 0) IntList intList,
|
||||
@Local(ordinal = 0) IntList intList2
|
||||
) {
|
||||
if (fireworkExplosion2.shape().getSerializedName().equals("pokeball")) {
|
||||
final double[][] POKEBALL_PARTICLE_COORDS = new double[][]{
|
||||
{0, 0.5},
|
||||
{0.05, 0.497494},
|
||||
{0.1, 0.489898},
|
||||
{0.15, 0.47697},
|
||||
{0.2, 0.458258},
|
||||
{0.25, 0.433013},
|
||||
{0.3, 0.4},
|
||||
{0.35, 0.357071},
|
||||
{0.4, 0.3},
|
||||
{0.45, 0.217945},
|
||||
{0.4975, 0.049937},
|
||||
{0.086606, 0.049994},
|
||||
{0.05, 0.086603},
|
||||
{0, 0.1},
|
||||
{0.05, 0.049994},
|
||||
|
||||
{0.05, -0.049994},
|
||||
{0, -0.1},
|
||||
{0.05, -0.086603},
|
||||
{0.086606, -0.049994},
|
||||
{0.0, -0.049937},
|
||||
{0.4975, -0.049937},
|
||||
{0.45, -0.217945},
|
||||
{0.4, -0.3},
|
||||
{0.35, -0.357071},
|
||||
{0.3, -0.4},
|
||||
{0.25, -0.433013},
|
||||
{0.2, -0.458258},
|
||||
{0.15, -0.47697},
|
||||
{0.1, -0.489898},
|
||||
{0.05, -0.497494},
|
||||
{0, -0.5},
|
||||
};
|
||||
|
||||
boolean b3 = bl3 == 1;
|
||||
boolean b4 = bl4 == 1;
|
||||
|
||||
this.createParticleShape(0.5, POKEBALL_PARTICLE_COORDS, intList, intList2, b3, b4, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.mixin;
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonItems;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.component.FireworkExplosion;
|
||||
import net.minecraft.world.item.crafting.FireworkStarRecipe;
|
||||
import net.minecraft.world.item.crafting.Ingredient;
|
||||
import net.minecraft.world.level.ItemLike;
|
||||
import org.spongepowered.asm.mixin.*;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
@Mixin(FireworkStarRecipe.class)
|
||||
public class FireworkStarRecipeMixin {
|
||||
|
||||
@Mutable
|
||||
@Shadow
|
||||
@Final
|
||||
private static Map<Item, FireworkExplosion.Shape> SHAPE_BY_ITEM;
|
||||
|
||||
@Mutable
|
||||
@Shadow
|
||||
@Final
|
||||
private static Ingredient SHAPE_INGREDIENT;
|
||||
|
||||
@Inject(method = "<clinit>", at = @At("TAIL"))
|
||||
private static void cobblemon$addCustomShape(CallbackInfo ci) {
|
||||
ItemStack[] existing = SHAPE_INGREDIENT.getItems();
|
||||
ItemStack[] extended = new ItemStack[existing.length + 1];
|
||||
System.arraycopy(existing, 0, extended, 0, existing.length);
|
||||
|
||||
extended[existing.length] = new ItemStack(CobblemonItems.POKE_BALL);
|
||||
|
||||
SHAPE_INGREDIENT = Ingredient.of(Arrays.stream(extended)
|
||||
.map(ItemStack::getItem)
|
||||
.toArray(ItemLike[]::new));
|
||||
SHAPE_BY_ITEM.put(CobblemonItems.POKE_BALL, FireworkExplosion.Shape.byId(5));
|
||||
}
|
||||
}
|
||||
@@ -8,18 +8,23 @@
|
||||
|
||||
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;
|
||||
@@ -32,6 +37,7 @@ 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 {
|
||||
@@ -99,7 +105,7 @@ public abstract class LivingEntityMixin extends Entity implements OrientationCon
|
||||
|
||||
@Inject(method = "tick", at = @At("HEAD"))
|
||||
private void cobblemon$updateRenderOrientation(CallbackInfo ci) {
|
||||
this.cobblemon$orientationController.tick();
|
||||
//this.cobblemon$orientationController.tick();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -116,4 +122,31 @@ 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,6 +19,7 @@ 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;
|
||||
@@ -47,6 +48,7 @@ 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;
|
||||
@@ -63,7 +65,7 @@ import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Mixin(Player.class)
|
||||
public abstract class PlayerMixin extends LivingEntity implements ScannableEntity, OrientationControllable, PlayerDuck {
|
||||
public abstract class PlayerMixin extends LivingEntity implements ScannableEntity, OrientationControllable, PlayerDuck, RidePassenger {
|
||||
|
||||
@Unique
|
||||
private Vector3f cobblemon$driverInput;
|
||||
@@ -91,6 +93,12 @@ 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_);
|
||||
}
|
||||
@@ -117,11 +125,6 @@ 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) {
|
||||
@@ -307,4 +310,34 @@ 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,33 +8,70 @@
|
||||
|
||||
package com.cobblemon.mod.common.mixin;
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon;
|
||||
import com.cobblemon.mod.common.OrientationControllable;
|
||||
import com.cobblemon.mod.common.PlayerSpawnerAccessor;
|
||||
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner;
|
||||
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawnerFactory;
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
|
||||
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules;
|
||||
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
|
||||
import com.llamalad7.mixinextras.sugar.Local;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import 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 PlayerSpawnerAccessor {
|
||||
@Shadow
|
||||
public abstract ServerLevel serverLevel();
|
||||
|
||||
@Unique
|
||||
public PlayerSpawner cobblemon$spawner;
|
||||
|
||||
@Override
|
||||
public PlayerSpawner getPlayerSpawner() {
|
||||
ServerPlayer player = (ServerPlayer)(Object)this;
|
||||
if (cobblemon$spawner == null) {
|
||||
cobblemon$spawner = PlayerSpawnerFactory.INSTANCE.create(player);
|
||||
}
|
||||
return cobblemon$spawner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlayerSpawner(PlayerSpawner spawner) {
|
||||
this.cobblemon$spawner = spawner;
|
||||
}
|
||||
|
||||
@Inject(method = "tick", at = @At("TAIL"))
|
||||
private void cobblemon$tickSpawner(CallbackInfo ci) {
|
||||
if (!Cobblemon.config.getEnableSpawning()) {
|
||||
return;
|
||||
} else if (!serverLevel().getGameRules().getBoolean(CobblemonGameRules.DO_POKEMON_SPAWNING)) {
|
||||
return;
|
||||
}
|
||||
getPlayerSpawner().tick();
|
||||
}
|
||||
|
||||
@Inject(method = "rideTick", at = @At("HEAD"))
|
||||
private void cobblemon$updateOrientationControllerRideTick(CallbackInfo ci) {
|
||||
if (!(this instanceof OrientationControllable controllable)) return;
|
||||
var shouldUseCustomOrientation = cobblemon$shouldUseCustomOrientation((ServerPlayer)(Object)this);
|
||||
controllable.getOrientationController().setActive(shouldUseCustomOrientation);
|
||||
ServerPlayer player = (ServerPlayer)(Object)this;
|
||||
Entity vehicle = player.getVehicle();
|
||||
if (!(vehicle instanceof OrientationControllable controllableVehicle)) return;
|
||||
var shouldUseCustomOrientation = cobblemon$shouldUseCustomOrientation(vehicle);
|
||||
controllableVehicle.getOrientationController().setActive(shouldUseCustomOrientation);
|
||||
}
|
||||
|
||||
@Unique
|
||||
private boolean cobblemon$shouldUseCustomOrientation(ServerPlayer player) {
|
||||
var playerVehicle = player.getVehicle();
|
||||
if (playerVehicle == null) return false;
|
||||
if (!(playerVehicle instanceof PokemonEntity pokemonEntity)) return false;
|
||||
private boolean cobblemon$shouldUseCustomOrientation(Entity entity) {
|
||||
if (entity == null) return false;
|
||||
if (!(entity instanceof PokemonEntity pokemonEntity)) return false;
|
||||
return pokemonEntity.ifRidingAvailableSupply(false, (behaviour, settings, state) -> {
|
||||
return behaviour.shouldRoll(settings, state, pokemonEntity);
|
||||
});
|
||||
@@ -42,8 +79,9 @@ public abstract class ServerPlayerMixin {
|
||||
|
||||
@Inject(method = "stopRiding", at = @At("HEAD"))
|
||||
public void cobblemon$resetOrientationOnDismount(CallbackInfo ci) {
|
||||
if (!(this instanceof OrientationControllable controllable)) return;
|
||||
controllable.getOrientationController().setActive(false);
|
||||
ServerPlayer player = (ServerPlayer)(Object)this;
|
||||
if (!(player.getVehicle() instanceof OrientationControllable controllableVehicle)) return;
|
||||
controllableVehicle.getOrientationController().setActive(false);
|
||||
}
|
||||
|
||||
//TODO: Switch to sending out on load instead of preventing saving
|
||||
@@ -51,5 +89,4 @@ public abstract class ServerPlayerMixin {
|
||||
private boolean cobblemon$cancelSavingPokemonMounts(boolean original, @Local(ordinal=0) Entity entity) {
|
||||
return original && !(entity instanceof PokemonEntity);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-1
@@ -34,7 +34,8 @@ public class AbstractClientPlayerMixin {
|
||||
} else {
|
||||
// Only modify fov through riding if the pokedex is not open
|
||||
LocalPlayer player = (LocalPlayer) (Object) this;
|
||||
if (player != null && player.isPassenger() && player.getVehicle() instanceof PokemonEntity) {
|
||||
// Only modify fov for the driver and not the passengers
|
||||
if (player != null && player.isPassenger() && player.getVehicle() instanceof PokemonEntity && player.getVehicle().getControllingPassenger() == player) {
|
||||
PokemonEntity ride = (PokemonEntity) player.getVehicle();
|
||||
|
||||
// Return custom fov mult for riding
|
||||
|
||||
@@ -14,12 +14,15 @@ 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;
|
||||
@@ -48,45 +51,44 @@ public abstract class CameraMixin {
|
||||
|
||||
@Shadow protected abstract void setPosition(Vec3 pos);
|
||||
|
||||
@Unique private float returnTimer = 0;
|
||||
@Unique private float rollAngleStart = 0;
|
||||
@Unique Minecraft minecraft = Minecraft.getInstance();
|
||||
@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;
|
||||
|
||||
@Inject(method = "setRotation", at = @At("HEAD"), cancellable = true)
|
||||
|
||||
public void cobblemon$setRotation(float f, float g, CallbackInfo ci) {
|
||||
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();
|
||||
// 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;
|
||||
|
||||
this.returnTimer = 0;
|
||||
this.rollAngleStart = controller.getRoll();
|
||||
ci.cancel();
|
||||
// Don't assume the camera to be attached to an entity. Ponder Scenes from create et.al. for example aren't.
|
||||
if (this.entity == null) return;
|
||||
var vehicle = this.entity.getVehicle();
|
||||
if (!(vehicle instanceof OrientationControllable controllableVehicle)) return;
|
||||
var instance = (Camera)(Object)this;
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
@WrapOperation(method = "setup", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;setPosition(DDD)V"))
|
||||
@@ -119,20 +121,128 @@ public abstract class CameraMixin {
|
||||
|
||||
@Unique
|
||||
private void cobblemon$applyRotation(){
|
||||
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()) {
|
||||
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 = 5.0f; // Some smoothing constant k
|
||||
var smoothingFactor = lerpRateMod * cobblemon$frameTime * k;
|
||||
var newYaw = currAngs.y() + (Math.toRadians(degDiff) * smoothingFactor);
|
||||
|
||||
newRotation.set(new Quaternionf().rotateYXZ((float)newYaw, eulerAngs.x(), 0.0f));
|
||||
}
|
||||
|
||||
var rotationOffset = 0;
|
||||
if (Minecraft.getInstance().options.getCameraType().isMirrored()) {
|
||||
newRotation.rotateY((float)Math.toRadians(180));
|
||||
rotationOffset = 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);
|
||||
this.xRot = controller.getPitch();
|
||||
this.yRot = controller.getYaw();
|
||||
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 + rotationOffset);
|
||||
driver.setYRot(180.0f - (driverEulerAngs.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);
|
||||
}
|
||||
|
||||
@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,9 +19,11 @@ 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;
|
||||
@@ -29,12 +31,10 @@ 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;
|
||||
@@ -55,6 +55,10 @@ public abstract class LocalPlayerMixin extends AbstractClientPlayer implements O
|
||||
|
||||
@Unique Matrix3f cobblemon$lastOrientation;
|
||||
|
||||
@Unique float cobblemon$lastRideXRot;
|
||||
@Unique float cobblemon$lastRideYRot;
|
||||
@Unique Vec3 cobblemon$lastRideEyePos;
|
||||
|
||||
@Unique boolean cobblemon$isDoubleJumping = false;
|
||||
|
||||
@Shadow
|
||||
@@ -72,30 +76,35 @@ public abstract class LocalPlayerMixin extends AbstractClientPlayer implements O
|
||||
super(clientLevel, gameProfile);
|
||||
}
|
||||
|
||||
// @Inject(method = "sendPosition", at = @At("TAIL"))
|
||||
// private void cobblemon$updateRotationMatrix(CallbackInfo ci) {
|
||||
// if (!(this instanceof OrientationControllable controllable)) return;
|
||||
// var controller = controllable.getOrientationController();
|
||||
// if (!controller.isActive() || controller.getOrientation() == cobblemon$lastOrientation) return;
|
||||
// cobblemon$lastOrientation = controller.getOrientation() != null ? new Matrix3f(controller.getOrientation()) : null;
|
||||
// CobblemonNetwork.INSTANCE.sendToServer(new ServerboundUpdateOrientationPacket(controller.getOrientation()));
|
||||
// }
|
||||
|
||||
@Inject(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/multiplayer/ClientPacketListener;send(Lnet/minecraft/network/protocol/Packet;)V"))
|
||||
private void cobblemon$updateRotationMatrixPassenger(CallbackInfo ci) {
|
||||
if (!(this 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()));
|
||||
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()));
|
||||
}
|
||||
|
||||
@Inject(method = "rideTick", at = @At("HEAD"))
|
||||
private void cobblemon$updateOrientationControllerRideTick(CallbackInfo ci) {
|
||||
if (Minecraft.getInstance().player != (Object)this) return;
|
||||
if (!(this instanceof OrientationControllable controllable)) return;
|
||||
var driver = (LocalPlayer)(Object)this;
|
||||
var vehicle = driver.getVehicle();
|
||||
if (!(this.getVehicle() instanceof OrientationControllable controllableVehicle)) return;
|
||||
var shouldUseCustomOrientation = cobblemon$shouldUseCustomOrientation((LocalPlayer)(Object)this);
|
||||
controllable.getOrientationController().setActive(shouldUseCustomOrientation);
|
||||
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);
|
||||
}
|
||||
|
||||
@Inject(method = "rideTick", at = @At("HEAD"))
|
||||
@@ -115,6 +124,40 @@ 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();
|
||||
|
||||
// Check for a change in the values before sending them.
|
||||
if (cobblemon$lastRideXRot != rideXRot ||
|
||||
cobblemon$lastRideYRot != rideYRot ||
|
||||
cobblemon$lastRideEyePos != rideEyePos
|
||||
) {
|
||||
|
||||
// Update 'last' values
|
||||
cobblemon$lastRideXRot = rideXRot;
|
||||
cobblemon$lastRideYRot = rideYRot;
|
||||
cobblemon$lastRideEyePos = rideEyePos;
|
||||
|
||||
CobblemonNetwork.INSTANCE.sendToServer(new ServerboundUpdateRiderRotationPacket(rideXRot, rideYRot, rideEyePos));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Unique
|
||||
private boolean cobblemon$shouldUseCustomOrientation(LocalPlayer player) {
|
||||
var playerVehicle = player.getVehicle();
|
||||
@@ -129,8 +172,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 controllable)) return;
|
||||
controllable.getOrientationController().setActive(false);
|
||||
if (!(this instanceof OrientationControllable controllableVehicle)) return;
|
||||
controllableVehicle.getOrientationController().setActive(false);
|
||||
}
|
||||
|
||||
@Inject(method = "getJumpRidingScale", at = @At("HEAD"), cancellable = true)
|
||||
|
||||
@@ -10,13 +10,16 @@ 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;
|
||||
@@ -32,6 +35,9 @@ import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
import static com.google.common.primitives.Floats.min;
|
||||
import static net.minecraft.util.Mth.lerp;
|
||||
|
||||
@Mixin(MouseHandler.class)
|
||||
public abstract class MouseHandlerMixin {
|
||||
@Shadow private double accumulatedScrollY;
|
||||
@@ -117,8 +123,6 @@ public abstract class MouseHandlerMixin {
|
||||
@Local(argsOnly = true) double d,
|
||||
@Local(argsOnly = true) double movementTime
|
||||
) {
|
||||
boolean returnValue = true;
|
||||
|
||||
PokedexUsageContext usageContext = CobblemonClient.INSTANCE.getPokedexUsageContext();
|
||||
if (usageContext.getScanningGuiOpen()) {
|
||||
this.smoothTurnY.reset();
|
||||
@@ -126,34 +130,78 @@ 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 = Mth.lerp(usageContext.getFovMultiplier(), spyglassSensitivity, lookSensitivity);
|
||||
var sensitivity = lerp(usageContext.getFovMultiplier(), spyglassSensitivity, lookSensitivity);
|
||||
var yRotationFlip = this.minecraft.options.invertYMouse().get() ? -1 : 1;
|
||||
player.turn(this.accumulatedDX * sensitivity, (this.accumulatedDY * sensitivity * yRotationFlip));
|
||||
returnValue = false;
|
||||
}
|
||||
|
||||
var vehicle = player.getVehicle();
|
||||
if (!(vehicle instanceof PokemonEntity)) return true;
|
||||
|
||||
// Clamp player rotation if riding and the vehicle demands it
|
||||
if (player.isPassenger() && player.getVehicle() instanceof PokemonEntity vehicle) {
|
||||
vehicle.clampPassengerRotation(player);
|
||||
if (player.isPassenger() && vehicle instanceof PokemonEntity pokeVehicle) {
|
||||
pokeVehicle.clampPassengerRotation(player);
|
||||
}
|
||||
|
||||
if (!(player instanceof OrientationControllable controllable)) return true;
|
||||
|
||||
if (!controllable.getOrientationController().isActive()) {
|
||||
// If the ride is not orientation controllable then return and perform the normal turnPlayer logic
|
||||
if (!(vehicle instanceof OrientationControllable controllableVehicle) || !controllableVehicle.getOrientationController().isActive()) {
|
||||
xMouseSmoother.reset();
|
||||
yMouseSmoother.reset();
|
||||
pitchSmoother.reset();
|
||||
rollSmoother.reset();
|
||||
yawSmoother.reset();
|
||||
return returnValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Send mouse input to be interpreted into rotation
|
||||
// deltas by the ride controller
|
||||
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
|
||||
Vec3 angVecMouse = cobblemon$getRideMouseRotation(cursorDeltaX, cursorDeltaY, movementTime);
|
||||
|
||||
// Perform Rotation using mouse influenced rotation deltas.
|
||||
controllable.getOrientationController().rotate(
|
||||
vehicleController.rotate(
|
||||
(float) angVecMouse.x,
|
||||
(float) angVecMouse.y,
|
||||
(float) angVecMouse.z
|
||||
@@ -163,17 +211,16 @@ 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);
|
||||
controllable.getOrientationController().rotate((float) yaw, (float) pitch, (float) roll);
|
||||
vehicleController.rotate((float) yaw, (float) pitch, (float) roll);
|
||||
}
|
||||
// Otherwise simply apply the smoothing
|
||||
else {
|
||||
controllable.getOrientationController().rotate((float) (angRot.x * 10 * d), (float) (angRot.y * 10 * d), (float) (angRot.z * 10 * d));
|
||||
vehicleController.rotate((float) (angRot.x * 10 * d), (float) (angRot.y * 10 * d), (float) (angRot.z * 10 * d));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -186,10 +233,11 @@ public abstract class MouseHandlerMixin {
|
||||
@Inject(method = "handleAccumulatedMovement", at = @At("TAIL"))
|
||||
private void cobblemon$maintainMovementWhenInScreens(CallbackInfo ci) {
|
||||
if (minecraft.player == null) return;
|
||||
if (!(minecraft.player instanceof OrientationControllable controllable)) return;
|
||||
if (!controllable.getOrientationController().isActive()) return;
|
||||
if (!(minecraft.player.getVehicle() instanceof OrientationControllable vehicleControllable)) return;
|
||||
if (!(vehicleControllable.getOrientationController() instanceof OrientationController vehicleController)) return;
|
||||
if (vehicleController.getOrientation() == null) return;
|
||||
if (minecraft.isPaused()) return;
|
||||
if (isMouseGrabbed()) return;
|
||||
if (this.isMouseGrabbed()) return; // return if turnPlayer() has already run this pass
|
||||
|
||||
this.turnPlayer(cobblemon$timeDelta);
|
||||
}
|
||||
@@ -251,6 +299,20 @@ 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,20 +8,24 @@
|
||||
|
||||
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 {
|
||||
@@ -57,5 +61,46 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ import com.cobblemon.mod.common.net.messages.client.settings.ServerSettingsPacke
|
||||
import com.cobblemon.mod.common.permission.LaxPermissionValidator
|
||||
import com.cobblemon.mod.common.platform.events.PlatformEvents
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import com.cobblemon.mod.common.pokemon.aspects.CHARACTERISTIC_RAINBOW_ASPECT
|
||||
import com.cobblemon.mod.common.pokemon.aspects.COSMETIC_SLOT_ASPECT
|
||||
import com.cobblemon.mod.common.pokemon.aspects.GENDER_ASPECT
|
||||
import com.cobblemon.mod.common.pokemon.aspects.SHINY_ASPECT
|
||||
@@ -293,6 +294,7 @@ object Cobblemon {
|
||||
SHINY_ASPECT.register()
|
||||
GENDER_ASPECT.register()
|
||||
COSMETIC_SLOT_ASPECT.register()
|
||||
CHARACTERISTIC_RAINBOW_ASPECT.register()
|
||||
|
||||
SpeciesFeatures.types["choice"] = ChoiceSpeciesFeatureProvider::class.java
|
||||
SpeciesFeatures.types["flag"] = FlagSpeciesFeatureProvider::class.java
|
||||
|
||||
@@ -33,7 +33,7 @@ import com.cobblemon.mod.common.client.net.gui.PokedexUIPacketHandler
|
||||
import com.cobblemon.mod.common.client.net.gui.SummaryUIPacketHandler
|
||||
import com.cobblemon.mod.common.client.net.npc.CloseNPCEditorHandler
|
||||
import com.cobblemon.mod.common.client.net.npc.OpenNPCEditorHandler
|
||||
import com.cobblemon.mod.common.client.net.orientation.S2CUpdateOrientationHandler
|
||||
import com.cobblemon.mod.common.client.net.orientation.ClientboundUpdateOrientationHandler
|
||||
import com.cobblemon.mod.common.client.net.pasture.ClosePastureHandler
|
||||
import com.cobblemon.mod.common.client.net.pasture.OpenPastureHandler
|
||||
import com.cobblemon.mod.common.client.net.pasture.PokemonPasturedHandler
|
||||
@@ -179,6 +179,7 @@ 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
|
||||
@@ -216,7 +217,7 @@ import com.cobblemon.mod.common.net.serverhandling.dialogue.EscapeDialogueHandle
|
||||
import com.cobblemon.mod.common.net.serverhandling.dialogue.InputToDialogueHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.evolution.AcceptEvolutionHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.npc.SaveNPCHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.orientation.OrientationPacketHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.orientation.ServerboundUpdateOrientationHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.pasture.PasturePokemonHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.pasture.SetPastureConflictHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.pasture.UnpastureAllPokemonHandler
|
||||
@@ -231,6 +232,7 @@ 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
|
||||
@@ -442,7 +444,7 @@ object CobblemonNetwork {
|
||||
list.add(PacketRegisterInfo(UpdatePastureConflictFlagPacket.ID, UpdatePastureConflictFlagPacket::decode, UpdatePastureConflictFlagHandler))
|
||||
|
||||
// Orientation
|
||||
list.add(PacketRegisterInfo(ClientboundUpdateOrientationPacket.ID, ClientboundUpdateOrientationPacket::decode, S2CUpdateOrientationHandler))
|
||||
list.add(PacketRegisterInfo(ClientboundUpdateOrientationPacket.ID, ClientboundUpdateOrientationPacket::decode, ClientboundUpdateOrientationHandler))
|
||||
|
||||
// Behaviours
|
||||
list.add(PacketRegisterInfo(PlayPosableAnimationPacket.ID, PlayPosableAnimationPacket::decode, PlayPosableAnimationHandler))
|
||||
@@ -577,13 +579,14 @@ object CobblemonNetwork {
|
||||
list.add(PacketRegisterInfo(SaveNPCPacket.ID, SaveNPCPacket::decode, SaveNPCHandler))
|
||||
|
||||
// Riding packet(s)
|
||||
list.add(PacketRegisterInfo(ServerboundUpdateOrientationPacket.ID, ServerboundUpdateOrientationPacket::decode, OrientationPacketHandler))
|
||||
list.add(PacketRegisterInfo(ServerboundUpdateOrientationPacket.ID, ServerboundUpdateOrientationPacket::decode, ServerboundUpdateOrientationHandler))
|
||||
list.add(PacketRegisterInfo(ServerboundUpdateRidingStatePacket.ID, ServerboundUpdateRidingStatePacket::decode, ServerboundUpdateRidingStateHandler))
|
||||
list.add(PacketRegisterInfo(ServerboundUpdateRidingStatsPacket.ID, ServerboundUpdateRidingStatsPacket::decode, ServerboundUpdateRidingStatsHandler))
|
||||
list.add(PacketRegisterInfo(ServerboundUpdateRidingStatRangePacket.ID, ServerboundUpdateRidingStatRangePacket::decode, ServerboundUpdateRidingStatRangeHandler))
|
||||
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,6 +26,7 @@ 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
|
||||
@@ -63,6 +64,7 @@ object CobblemonRideSettings : DataRegistry {
|
||||
var boat = BoatSettings()
|
||||
var burst = BurstSettings()
|
||||
var dolphin = DolphinSettings()
|
||||
var submarine = SubmarineSettings()
|
||||
|
||||
override fun sync(player: ServerPlayer) {
|
||||
player.sendPacket(
|
||||
@@ -78,7 +80,8 @@ object CobblemonRideSettings : DataRegistry {
|
||||
vehicle = vehicle,
|
||||
boat = boat,
|
||||
burst = burst,
|
||||
dolphin = dolphin
|
||||
dolphin = dolphin,
|
||||
submarine = submarine
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -96,6 +99,7 @@ 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 {
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ class MoveToOwnerTaskConfig : SingleTaskConfig {
|
||||
override fun createTask(
|
||||
entity: LivingEntity,
|
||||
behaviourConfigurationContext: BehaviourConfigurationContext
|
||||
): BehaviorControl<in LivingEntity>? {
|
||||
): BehaviorControl<in LivingEntity> {
|
||||
behaviourConfigurationContext.addMemories(
|
||||
MemoryModuleType.WALK_TARGET,
|
||||
MemoryModuleType.LOOK_TARGET
|
||||
|
||||
+1
-1
@@ -110,7 +110,7 @@ class PlaceHoneyInSaccLeavesTaskConfig : SingleTaskConfig {
|
||||
override fun stop(level: ServerLevel, entity: LivingEntity, gameTime: Long) {
|
||||
if (successfulPollinationTicks > REQUIRED_SUCCESSFUL_POLLINATION_TICKS) {
|
||||
val blockPos = entity.brain.getMemorySafely(CobblemonMemories.NEARBY_SACC_LEAVES).orElse(null)
|
||||
blockPos.let {
|
||||
blockPos?.let {
|
||||
val blockState = level.getBlockState(blockPos)
|
||||
if (blockState.block == CobblemonBlocks.SACCHARINE_LEAVES) {
|
||||
val saccAge = blockState.getValue( SaccharineLeafBlock.AGE)
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.berry.spawncondition
|
||||
|
||||
import com.cobblemon.mod.common.api.berry.Berry
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import net.minecraft.core.Holder
|
||||
import net.minecraft.tags.TagKey
|
||||
import net.minecraft.util.RandomSource
|
||||
import net.minecraft.world.level.biome.Biome
|
||||
|
||||
class SpecificBiomeCondition : BerrySpawnCondition {
|
||||
lateinit var biome: TagKey<Biome>
|
||||
val minGroveSize: Int = 1
|
||||
val maxGroveSize: Int = 1
|
||||
|
||||
override fun canSpawn(berry: Berry, biome: Holder<Biome>) = biome.`is`(this.biome)
|
||||
override fun getGroveSize(random: RandomSource) = random.nextIntBetweenInclusive(minGroveSize, maxGroveSize)
|
||||
|
||||
companion object {
|
||||
val ID = cobblemonResource("specific_biome")
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ interface PokemonSideDelegate : EntitySideDelegate<PokemonEntity> {
|
||||
fun handleStatus(status: Byte) {}
|
||||
fun spawnShinyParticle(player: Player) {}
|
||||
fun spawnAspectParticle() {}
|
||||
fun applyRenderRotation(partialTick: Float) {}
|
||||
fun positionRider(passenger: Entity, positionUpdater: MoveFunction)
|
||||
fun updateAge(age: Int) {}
|
||||
}
|
||||
|
||||
@@ -17,18 +17,11 @@ object SpawnBaitUtils {
|
||||
return grouped.map { (key, groupEffects) ->
|
||||
val (type, subcategory) = key
|
||||
|
||||
val count = groupEffects.size
|
||||
val totalChance = groupEffects.sumOf { it.chance }
|
||||
val totalValue = groupEffects.sumOf { it.value }
|
||||
|
||||
val multiplier = when (count) {
|
||||
2 -> 0.8
|
||||
3 -> 0.6
|
||||
else -> 1.0
|
||||
}
|
||||
|
||||
val adjustedChance = (totalChance * multiplier).coerceAtMost(1.0)
|
||||
val adjustedValue = ceil(totalValue * multiplier)
|
||||
val adjustedChance = totalChance.coerceAtMost(1.0)
|
||||
val adjustedValue = ceil(totalValue)
|
||||
|
||||
SpawnBait.Effect(type, subcategory, adjustedChance, adjustedValue)
|
||||
}
|
||||
|
||||
@@ -53,8 +53,7 @@ open class MoveTemplate(
|
||||
val pp: Int,
|
||||
val priority: Int,
|
||||
val critRatio: Double,
|
||||
val effectChances: Array<Double>,
|
||||
val actionEffect: ActionEffectTimeline?
|
||||
val effectChances: Array<Double>
|
||||
) {
|
||||
val struct: MoStruct by lazy {
|
||||
QueryStruct(hashMapOf())
|
||||
@@ -87,8 +86,7 @@ open class MoveTemplate(
|
||||
pp = 5,
|
||||
priority = 0,
|
||||
critRatio = 0.0,
|
||||
effectChances = emptyArray(),
|
||||
actionEffect = null
|
||||
effectChances = emptyArray()
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -92,18 +92,7 @@ object Moves : DataRegistry {
|
||||
effectChances += secondaryMember.get("chance").asDouble
|
||||
}
|
||||
}
|
||||
val actionEffect = ActionEffects.actionEffects[id.asIdentifierDefaultingNamespace()]
|
||||
?: run {
|
||||
ActionEffects.actionEffects["generic_move".asIdentifierDefaultingNamespace()]
|
||||
// if (damageCategory == DamageCategories.STATUS) {
|
||||
// ActionEffects.actionEffects[cobblemonResource("status")]
|
||||
// } else {
|
||||
// val type = elementalType.name.lowercase()
|
||||
// val category = damageCategory.name.lowercase()
|
||||
// ActionEffects.actionEffects["${category}_$type".asIdentifierDefaultingNamespace()]
|
||||
// }
|
||||
}
|
||||
val move = MoveTemplate(id, num, elementalType, damageCategory, power, target, accuracy, pp, priority, critRatio, effectChances.toTypedArray(), actionEffect)
|
||||
val move = MoveTemplate(id, num, elementalType, damageCategory, power, target, accuracy, pp, priority, critRatio, effectChances.toTypedArray())
|
||||
this.register(move)
|
||||
} catch (e: Exception) {
|
||||
Cobblemon.LOGGER.error("Caught exception trying to resolve the move '{}'", id, e)
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.cobblemon.mod.common.api.molang.ExpressionLike
|
||||
import com.cobblemon.mod.common.api.moves.animations.keyframes.*
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stat
|
||||
import com.cobblemon.mod.common.api.reactive.SimpleObservable
|
||||
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
|
||||
import com.cobblemon.mod.common.pokemon.adapters.CobblemonStatTypeAdapter
|
||||
import com.cobblemon.mod.common.util.adapters.*
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
@@ -91,5 +92,11 @@ object ActionEffects : JsonDataRegistry<ActionEffectTimeline> {
|
||||
observable.emit(this)
|
||||
}
|
||||
|
||||
fun getEffectWithBattleContext(id: ResourceLocation, pokemon: BattlePokemon): ActionEffectTimeline? {
|
||||
val species = pokemon.entity?.exposedSpecies ?: pokemon.effectedPokemon.species
|
||||
val contextedEffect = actionEffects[ResourceLocation.fromNamespaceAndPath(id.namespace, id.path + "_" + species)]
|
||||
return contextedEffect ?: actionEffects[id]
|
||||
}
|
||||
|
||||
override fun sync(player: ServerPlayer) {}
|
||||
}
|
||||
+96
-12
@@ -10,13 +10,17 @@ package com.cobblemon.mod.common.api.orientation
|
||||
|
||||
import com.cobblemon.mod.common.util.math.geometry.toDegrees
|
||||
import com.cobblemon.mod.common.util.math.geometry.toRadians
|
||||
import com.mojang.blaze3d.Blaze3D
|
||||
import net.minecraft.util.Mth
|
||||
import net.minecraft.world.entity.LivingEntity
|
||||
import org.joml.AxisAngle4f
|
||||
import org.joml.Matrix3f
|
||||
import org.joml.Quaternionf
|
||||
import org.joml.Vector3f
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.asin
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.sign
|
||||
|
||||
open class OrientationController(val entity: LivingEntity) {
|
||||
@@ -35,6 +39,11 @@ open class OrientationController(val entity: LivingEntity) {
|
||||
private var renderOrientationO: Matrix3f? = null
|
||||
private var renderOrientation: Matrix3f? = null
|
||||
|
||||
|
||||
private var angularVelocity: Vector3f = Vector3f(0.0f, 0.0f, 0.0f)
|
||||
private var lastHandledRotationTime = Double.MIN_VALUE
|
||||
private var frameTime = 0.0
|
||||
|
||||
/** Adding this simply because it irritates me seeing `getActive` when most of this is Java. */
|
||||
fun isActive() = active
|
||||
|
||||
@@ -63,7 +72,7 @@ open class OrientationController(val entity: LivingEntity) {
|
||||
}
|
||||
|
||||
fun getRenderOrientation(delta: Float): Quaternionf {
|
||||
val old = renderOrientationO ?: renderOrientation ?: orientation
|
||||
val old = renderOrientationO ?: renderOrientation ?: orientation ?: Matrix3f()
|
||||
val new = renderOrientation ?: old
|
||||
val oldQuat = Quaternionf().setFromUnnormalized(old)
|
||||
val newQuat = Quaternionf().setFromUnnormalized(new)
|
||||
@@ -72,18 +81,93 @@ open class OrientationController(val entity: LivingEntity) {
|
||||
}
|
||||
|
||||
fun tick() {
|
||||
if (!active || orientation == null) return
|
||||
renderOrientationO = renderOrientation
|
||||
val current = orientation
|
||||
val renderMatrix = this.renderOrientation ?: current
|
||||
val renderQuat = Quaternionf().setFromUnnormalized(renderMatrix)
|
||||
val targetQuat = Quaternionf().setFromUnnormalized(current)
|
||||
val dampingFactor = 0.66f // We can change this factor for faster transitions
|
||||
renderQuat.slerp(targetQuat, dampingFactor)
|
||||
// val d = Blaze3D.getTime();
|
||||
// this.frameTime =
|
||||
// if (this.lastHandledRotationTime != Double.MIN_VALUE) d - this.lastHandledRotationTime else 0.0;
|
||||
// this.lastHandledRotationTime = d;
|
||||
//
|
||||
// if (!active || orientation == null) return
|
||||
//
|
||||
// // Find the desired angular velocity
|
||||
// // lerp the current ang vel towards the desired at a set rate (does this need to take into account frametime?
|
||||
// val current = orientation
|
||||
// val renderMatrix = this.renderOrientation ?: Matrix3f()
|
||||
// val renderQuat = Quaternionf().setFromUnnormalized(renderMatrix)
|
||||
// val targetQuat = Quaternionf().setFromUnnormalized(current)
|
||||
//
|
||||
// // Max rate of angular change in radians per second
|
||||
// val maxAngVel = Math.toRadians(180.0).toFloat()
|
||||
//
|
||||
// // Find the difference between the current render orientation and the target orientation
|
||||
// val qDiff: Quaternionf = targetQuat.mul(renderQuat.invert())
|
||||
//
|
||||
// // Construct the target rate of change from the difference quat
|
||||
// val axAng = qDiff.get(AxisAngle4f())
|
||||
// val capdRate = axAng.angle.coerceIn(-maxAngVel, maxAngVel)
|
||||
// val targetAngVel = Vector3f(
|
||||
// axAng.x,
|
||||
// axAng.y,
|
||||
// axAng.z
|
||||
// ).mul(capdRate)
|
||||
//
|
||||
// // lerp the current angular velocity towards the target
|
||||
// val k = 10.0f // Some smoothing constant k
|
||||
// val smoothingFactor = Math.min(1.0f, this.frameTime.toFloat() * k)
|
||||
// angularVelocity = angularVelocity.lerp(targetAngVel, smoothingFactor)
|
||||
//
|
||||
// // Use the angular velocity to slerp the renderQuat toward the targetQuat
|
||||
// val frameDisplacement =
|
||||
// angularVelocity.mul(this.frameTime.toFloat()) // Angular displacement happening on this frame
|
||||
//
|
||||
// val fAng = frameDisplacement.length() // Angle of displacement for this given pass/frame
|
||||
// if (fAng > 0.0001f) { // Ensure that the angle is significant enough that its worth normalizing and carrying out rotation
|
||||
// val fAx = frameDisplacement.normalize()
|
||||
// // Construct a rotation quat from the
|
||||
// val rotQuat = Quaternionf().fromAxisAngleRad(fAx, fAng)
|
||||
//
|
||||
// // Set the new orientation post frame stepped rotation.
|
||||
// renderOrientation = Matrix3f().set(renderQuat.mul(rotQuat))
|
||||
// }
|
||||
//
|
||||
//// this.renderOrientation = orientation
|
||||
//
|
||||
// // apply the new current angular velocity to the current render orientation
|
||||
// // construct a quat out of the angular velocity
|
||||
// // slerp the quat with a value of frametime
|
||||
|
||||
val newRenderOrientation = Matrix3f()
|
||||
renderQuat.get(newRenderOrientation)
|
||||
renderOrientation = newRenderOrientation
|
||||
val d = Blaze3D.getTime();
|
||||
this.frameTime = if (this.lastHandledRotationTime != Double.MIN_VALUE) d - this.lastHandledRotationTime else 0.0;
|
||||
this.lastHandledRotationTime = d;
|
||||
|
||||
if (!active || orientation == null) return
|
||||
|
||||
val current = orientation
|
||||
val renderMatrix = this.renderOrientation ?: Matrix3f()
|
||||
|
||||
// Create mutable copies for manipulation
|
||||
val renderQuat = Quaternionf().setFromUnnormalized(renderMatrix)
|
||||
var targetQuat = Quaternionf().setFromUnnormalized(current)
|
||||
|
||||
// --- Shortest Path Check (Still mandatory for Slerp) ---
|
||||
if (renderQuat.dot(targetQuat) < 0) {
|
||||
targetQuat = targetQuat.invert()
|
||||
}
|
||||
// --------------------------------------------------------
|
||||
|
||||
// K is the speed (rad/s) we want to close the gap at.
|
||||
val k_rate = 10.0f // E.g., target 10 units/second closing rate.
|
||||
|
||||
// Calculate the frame-rate independent alpha (t) for Slerp.
|
||||
// This makes the smoothing constant 'k_rate' a rate of change, not an arbitrary alpha value.
|
||||
val slerpAlpha = 1.0f - Math.exp(-(k_rate * this.frameTime)).toFloat()
|
||||
|
||||
// Use Slerp to interpolate the renderQuat towards the targetQuat
|
||||
val newRenderQuat = renderQuat.slerp(targetQuat, slerpAlpha, Quaternionf())
|
||||
|
||||
// Set the new orientation
|
||||
renderOrientation = Matrix3f().set(newRenderQuat)
|
||||
|
||||
// Angular velocity is now redundant for rendering, but could be used for particle effects, etc.
|
||||
}
|
||||
|
||||
val forwardVector: Vector3f
|
||||
|
||||
@@ -170,11 +170,8 @@ object Natures {
|
||||
* Utility method to get a nature by string
|
||||
* @return a nature type or null
|
||||
*/
|
||||
fun getNature(identifier: String): Nature? {
|
||||
val nature = getNature(cobblemonResource(identifier))
|
||||
if(nature != null) return nature
|
||||
return getNature(ResourceLocation.parse(identifier))
|
||||
}
|
||||
fun getNature(identifier: String) = getNature(cobblemonResource(identifier))
|
||||
?: getNature(ResourceLocation.parse(identifier))
|
||||
|
||||
/**
|
||||
* Helper function for a random Nature
|
||||
|
||||
@@ -806,8 +806,8 @@ open class PokemonProperties {
|
||||
return PokemonProperties().loadFromJSON(this.saveToJSON())
|
||||
}
|
||||
|
||||
// If the config value is at least 1, then do 1/x and use that as the property chance
|
||||
private fun Float.checkRate(): Boolean = if (this >= 1) (Random.Default.nextFloat() < 1 / this) else Random.Default.nextFloat() < this
|
||||
// If the config value is at least 0, then do 1/x and use that as the property chance
|
||||
private fun Float.checkRate(): Boolean = this > 0 && (Random.nextFloat() < 1 / this)
|
||||
|
||||
/**
|
||||
* Attempts to find an ability by ID and resolve if it should be forced or if it's legal for the given form.
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ package com.cobblemon.mod.common.api.pokemon.aspect
|
||||
import com.cobblemon.mod.common.CobblemonBlocks
|
||||
import com.cobblemon.mod.common.api.ai.config.task.PollinateFlowerTaskConfig
|
||||
import com.cobblemon.mod.common.api.spawning.influence.SaccharineLogSlatheredInfluence
|
||||
import com.cobblemon.mod.common.api.spawning.prospecting.LurePokeSnackDetector
|
||||
import com.cobblemon.mod.common.block.entity.PokeSnackBlockEntity
|
||||
import com.cobblemon.mod.common.client.entity.PokemonClientDelegate
|
||||
import net.minecraft.core.particles.BlockParticleOption
|
||||
import net.minecraft.core.particles.ParticleOptions
|
||||
@@ -23,7 +23,7 @@ import net.minecraft.resources.ResourceLocation
|
||||
*/
|
||||
val aspectParticleMap: Map<String, ParticleData> = mapOf(
|
||||
SaccharineLogSlatheredInfluence.HONEY_DRENCHED_ASPECT to ParticleData.MinecraftParticle(ParticleTypes.FALLING_HONEY, 0.075, 1),
|
||||
LurePokeSnackDetector.POKE_SNACK_CRUMBED_ASPECT to ParticleData.MinecraftParticle(
|
||||
PokeSnackBlockEntity.POKE_SNACK_CRUMBED_ASPECT to ParticleData.MinecraftParticle(
|
||||
BlockParticleOption(
|
||||
ParticleTypes.BLOCK,
|
||||
CobblemonBlocks.POKE_SNACK.defaultBlockState()
|
||||
|
||||
+12
@@ -103,6 +103,18 @@ 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
|
||||
|
||||
+2
@@ -16,6 +16,7 @@ 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 {
|
||||
@@ -34,6 +35,7 @@ object RidingBehaviours {
|
||||
register(MinekartBehaviour.KEY, MinekartBehaviour())
|
||||
register(HoverBehaviour.KEY, HoverBehaviour())
|
||||
register(RocketBehaviour.KEY, RocketBehaviour())
|
||||
register(SubmarineBehaviour.KEY, SubmarineBehaviour())
|
||||
register(CompositeBehaviour.KEY, CompositeBehaviour())
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ class RidingController(
|
||||
val newKey = behaviourSettings.key
|
||||
for (passenger in entity.passengers) {
|
||||
if (passenger is Player && passenger !is ServerPlayer) {
|
||||
MountedCameraTypeHandler.handleTransition(previousKey ?: continue, newKey)
|
||||
MountedCameraTypeHandler.handleTransition(passenger, previousKey ?: continue, newKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+85
-137
@@ -34,6 +34,7 @@ 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> {
|
||||
@@ -88,7 +89,8 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
|
||||
vehicle: PokemonEntity,
|
||||
driver: Player,
|
||||
) {
|
||||
val controller = (driver as OrientationControllable).orientationController
|
||||
// Also checks stamina
|
||||
val controller = (vehicle 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()))))
|
||||
@@ -109,6 +111,10 @@ 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"
|
||||
@@ -135,7 +141,7 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
|
||||
vehicle: PokemonEntity,
|
||||
driver: LivingEntity
|
||||
): Vec2 {
|
||||
return Vec2(driver.xRot * 0.5f, driver.yRot)
|
||||
return Vec2.ZERO//Vec2(driver.xRot * 0.5f, driver.yRot)
|
||||
}
|
||||
|
||||
override fun velocity(
|
||||
@@ -145,29 +151,26 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
|
||||
driver: Player,
|
||||
input: Vec3
|
||||
): Vec3 {
|
||||
var upForce = 0.0
|
||||
var forwardForce = 0.0
|
||||
val controller = (vehicle as? OrientationControllable)?.orientationController
|
||||
if (controller == null || controller.orientation == null) return Vec3.ZERO
|
||||
|
||||
//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
|
||||
|
||||
val velocity = Vec3(state.rideVelocity.get().x , upForce + extraDownwardForce, forwardForce)
|
||||
return velocity
|
||||
// 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
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -206,7 +209,7 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
|
||||
activeInput = true
|
||||
}
|
||||
|
||||
val controller = (driver as? OrientationControllable)?.orientationController
|
||||
val controller = (vehicle as? OrientationControllable)?.orientationController
|
||||
if (controller != null) {
|
||||
//Base glide speed change on current pitch of the ride.
|
||||
glideSpeedChange = sin(Math.toRadians(controller.pitch.toDouble()))
|
||||
@@ -282,15 +285,13 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
|
||||
driver: Player,
|
||||
deltaTime: Double
|
||||
): Vec3 {
|
||||
if (Cobblemon.config.disableRoll) return Vec3.ZERO
|
||||
if (driver !is OrientationControllable) return Vec3.ZERO
|
||||
val controller = (driver as OrientationControllable).orientationController
|
||||
if (vehicle !is OrientationControllable) return Vec3.ZERO
|
||||
val controller = (vehicle 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())) //* 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))
|
||||
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)
|
||||
|
||||
// Dampen yaw when upside down
|
||||
val yawDampen = (1 - abs(min(cos(controller.roll.toRadians()),0.0f)).pow(2))
|
||||
@@ -299,10 +300,13 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
|
||||
// Correct orientation when at low speeds
|
||||
correctOrientation(settings, state, vehicle, controller, deltaTime)
|
||||
|
||||
controller.applyGlobalPitch(state.currPitchCorrectionForce.get().toRadians())
|
||||
val pCorr = (state.currPitchCorrectionForce.get() * deltaTime).toFloat()
|
||||
val rCorr = (state.currRollCorrectionForce.get() * deltaTime).toFloat()
|
||||
controller.applyGlobalPitch(pCorr)
|
||||
controller.rotateRoll(rCorr)
|
||||
|
||||
//yaw, pitch, roll
|
||||
return Vec3(0.0, 0.0, state.currRollCorrectionForce.get())
|
||||
return Vec3(0.0, 0.0, 0.0)
|
||||
}
|
||||
|
||||
private fun correctOrientation(
|
||||
@@ -313,42 +317,48 @@ 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!!)
|
||||
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()
|
||||
// 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
|
||||
|
||||
// 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)
|
||||
)
|
||||
// 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 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
|
||||
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
|
||||
|
||||
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())
|
||||
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()
|
||||
|
||||
// Ensure the 'steering' correction force doesn't exceed our maxForce
|
||||
if (abs(steeredPitchForce) > maxPitchForce) {
|
||||
@@ -398,80 +408,18 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
|
||||
state.noInputTimePitch.set(0.0)
|
||||
}
|
||||
|
||||
return when {
|
||||
Cobblemon.config.disableRoll -> noRollRotation(
|
||||
settings,
|
||||
state,
|
||||
vehicle,
|
||||
driver,
|
||||
mouseY,
|
||||
mouseX,
|
||||
mouseYSmoother,
|
||||
mouseXSmoother,
|
||||
sensitivity,
|
||||
deltaTime
|
||||
return rollRotation(
|
||||
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(
|
||||
@@ -486,21 +434,23 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
|
||||
sensitivity: Double,
|
||||
deltaTime: Double
|
||||
): Vec3 {
|
||||
if (driver !is OrientationControllable) return Vec3.ZERO
|
||||
val controller = (driver as OrientationControllable).orientationController
|
||||
if (vehicle !is OrientationControllable) return Vec3.ZERO
|
||||
val controller = (vehicle 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
|
||||
val mouseXc = (mouseX).coerceIn(-45.0, 45.0) * mouseInputMult * rollDegreeDebuff
|
||||
// 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
|
||||
@@ -508,8 +458,6 @@ 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:
|
||||
|
||||
+1
-1
@@ -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().z) > 0.2
|
||||
return@PoseOption abs(state.rideVelocity.get().length()) > 0.01
|
||||
})
|
||||
|
||||
override fun isActive(
|
||||
|
||||
+4
-113
@@ -161,7 +161,7 @@ class JetBehaviour : RidingBehaviour<JetSettings, JetState> {
|
||||
driver: Player,
|
||||
input: Vec3
|
||||
): Vec3 {
|
||||
val controller = (driver as? OrientationControllable)?.orientationController
|
||||
val controller = (vehicle as? OrientationControllable)?.orientationController
|
||||
if (controller == null || controller.orientation == null) return Vec3.ZERO
|
||||
|
||||
//Calculate ride space velocity
|
||||
@@ -265,8 +265,6 @@ 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)
|
||||
@@ -298,107 +296,8 @@ class JetBehaviour : RidingBehaviour<JetSettings, JetState> {
|
||||
sensitivity: Double,
|
||||
deltaTime: Double
|
||||
): Vec3 {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
if (vehicle !is OrientationControllable) return Vec3.ZERO
|
||||
|
||||
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(
|
||||
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
|
||||
//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
|
||||
@@ -412,14 +311,6 @@ 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
|
||||
@@ -430,10 +321,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.02))
|
||||
state.currMouseXForce.set(lerp(state.currMouseXForce.get(), 0.0, 0.08))
|
||||
}
|
||||
if (mouseY == 0.0) {
|
||||
state.currMouseYForce.set(lerp(state.currMouseYForce.get(), 0.0, 0.005))
|
||||
state.currMouseYForce.set(lerp(state.currMouseYForce.get(), 0.0, 0.02))
|
||||
}
|
||||
|
||||
//yaw, pitch, roll
|
||||
|
||||
+147
-177
@@ -20,6 +20,7 @@ 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
|
||||
@@ -47,6 +48,7 @@ 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 {
|
||||
@@ -62,7 +64,7 @@ class DolphinBehaviour : RidingBehaviour<DolphinSettings, DolphinState> {
|
||||
}
|
||||
|
||||
val poseProvider = PoseProvider<DolphinSettings, DolphinState>(PoseType.FLOAT)
|
||||
.with(PoseOption(PoseType.SWIM) { _, _, entity -> entity.deltaMovement.length() > 0.1 })
|
||||
.with(PoseOption(PoseType.SWIM) { _, state, entity -> abs(state.rideVelocity.get().length()) > 0.05 })
|
||||
|
||||
override fun isActive(settings: DolphinSettings, state: DolphinState, vehicle: PokemonEntity): Boolean {
|
||||
return vehicle.isInWater || !vehicle.onGround()
|
||||
@@ -151,37 +153,46 @@ class DolphinBehaviour : RidingBehaviour<DolphinSettings, DolphinState> {
|
||||
driver: Player,
|
||||
input: Vec3
|
||||
): Vec3 {
|
||||
if (driver !is OrientationControllable) return Vec3.ZERO
|
||||
val controller = (driver as OrientationControllable).orientationController
|
||||
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 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)
|
||||
{
|
||||
val currVel = state.lastVelocity.get()
|
||||
val breachBoost = 1.5
|
||||
var prevVel = state.lastVelocity.get()
|
||||
val breachBoost = 0.3
|
||||
state.clearMouseSmoothers.set(false)
|
||||
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()) {
|
||||
state.lastVelocity.set(Vec3(currVel.x, currVel.y + breachBoost*sin(-controller.pitch.toRadians()).coerceIn(0.0f,0.5f), currVel.z))
|
||||
prevVel = Vec3(prevVel.x, prevVel.y + breachBoost*(sin(-controller.pitch.toRadians()).coerceIn(0.0f,0.5f)*2.0).pow(2), prevVel.z)
|
||||
}
|
||||
state.hasBreached.set(true)
|
||||
} else {
|
||||
val gravity = (9.8 / ( 20.0)) * 0.2 * 0.4
|
||||
val terminalVel = 2.0
|
||||
state.lastVelocity.set(Vec3(currVel.x, max(-terminalVel, currVel.y - gravity), currVel.z)) //.yRot(vehicle.yRot.toRadians()))
|
||||
prevVel = Vec3(prevVel.x, max(-terminalVel, prevVel.y - gravity), prevVel.z)
|
||||
}
|
||||
return state.lastVelocity.get()
|
||||
|
||||
state.lastVelocity.set(prevVel)
|
||||
return state.lastVelocity.get().yRot(vehicle.yRot.toRadians())
|
||||
} else {
|
||||
state.lastVelocity.set(vehicle.deltaMovement.yRot(vehicle.yRot.toRadians()))
|
||||
if(state.hasBreached.get()) {
|
||||
state.clearMouseSmoothers.set(true)
|
||||
}
|
||||
state.hasBreached.set(false)
|
||||
currVel = Vec3(currVel.x, 0.0, currVel.z) // Reset y velocity to account for y vel persistance edgecase
|
||||
}
|
||||
|
||||
/*********************************************
|
||||
@@ -190,32 +201,30 @@ class DolphinBehaviour : RidingBehaviour<DolphinSettings, DolphinState> {
|
||||
if (vehicle.horizontalCollision || vehicle.verticalCollision) {
|
||||
// not actually friction
|
||||
val frictionLimit = 0.1
|
||||
val postColSpeed = min(max(vehicle.deltaMovement.length(), frictionLimit), state.speed.get())
|
||||
val postColSpeed = min(max(state.rideVelocity.get().length(), frictionLimit), state.speed.get())
|
||||
state.speed.set(postColSpeed)
|
||||
}
|
||||
|
||||
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))
|
||||
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 vertInput = when {
|
||||
driver.jumping && state.stamina.get() != 0.0f -> 1.0
|
||||
driver.isShiftKeyDown -> -1.0
|
||||
else -> 0.0
|
||||
} * strafeFactor
|
||||
state.rideVelocity.set(Vec3(currVel.x, currVel.y, newZVel))
|
||||
|
||||
var newVelocity = Vec3(f, vertInput,-state.speed.get())
|
||||
// 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
|
||||
)
|
||||
|
||||
// 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)
|
||||
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())
|
||||
}
|
||||
|
||||
override fun angRollVel(
|
||||
@@ -225,9 +234,8 @@ class DolphinBehaviour : RidingBehaviour<DolphinSettings, DolphinState> {
|
||||
driver: Player,
|
||||
deltaTime: Double
|
||||
): Vec3 {
|
||||
if (Cobblemon.config.disableRoll) return Vec3.ZERO
|
||||
if (driver !is OrientationControllable) return Vec3.ZERO
|
||||
val controller = (driver as OrientationControllable).orientationController
|
||||
if (vehicle !is OrientationControllable) return Vec3.ZERO
|
||||
val controller = (vehicle as OrientationControllable).orientationController
|
||||
|
||||
val currSpeed = vehicle.deltaMovement.length()
|
||||
|
||||
@@ -235,7 +243,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()))) * sqrt(RidingBehaviour.scaleToRange(currSpeed, 0.0, topSpeed))
|
||||
val trueYawDelt = yawDeltaDeg * abs(cos(Math.toRadians(controller.pitch.toDouble()))*2).coerceIn(-1.0, 1.0) * sqrt(RidingBehaviour.scaleToRange(currSpeed, 0.0, topSpeed))
|
||||
|
||||
// Dampen yaw when upside down
|
||||
val yawDampen = (1 - abs(min(cos(controller.roll.toRadians()),0.0f)))
|
||||
@@ -256,45 +264,62 @@ class DolphinBehaviour : RidingBehaviour<DolphinSettings, DolphinState> {
|
||||
driver: Player,
|
||||
deltaTime: Double
|
||||
) {
|
||||
// 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!!)
|
||||
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
|
||||
|
||||
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()
|
||||
|
||||
// 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)
|
||||
// Stop roll and pitch correction when breached
|
||||
if (state.hasBreached.get()) {
|
||||
state.currRollCorrectionForce.set(
|
||||
state.currRollCorrectionForce.get() + steeredRollForce
|
||||
lerp(state.currRollCorrectionForce.get(), 0.0, deltaTime * 0.98)
|
||||
)
|
||||
} else {
|
||||
state.currRollCorrectionForce.set(
|
||||
//reduce rollCorrection gradually
|
||||
lerp(state.currRollCorrectionForce.get(), 0.0, deltaTime * 0.96)
|
||||
state.currPitchCorrectionForce.set(
|
||||
lerp(state.currPitchCorrectionForce.get(), 0.0, deltaTime * 0.98)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
//Calculate correcting pitch force.
|
||||
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)
|
||||
// Calculate correcting roll force.
|
||||
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
|
||||
|
||||
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
|
||||
// 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 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
|
||||
@@ -333,130 +358,72 @@ class DolphinBehaviour : RidingBehaviour<DolphinSettings, DolphinState> {
|
||||
} else {
|
||||
state.noInputTime.set(0.0)
|
||||
}
|
||||
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(
|
||||
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
|
||||
if (vehicle !is OrientationControllable) return Vec3.ZERO
|
||||
val controller = (vehicle as OrientationControllable).orientationController
|
||||
val topSpeed = vehicle.runtime.resolveDouble(settings.speedExpr ?: globalDolphin.speedExpr!!) / 20.0
|
||||
val boostingHandleDebuff = if (state.boosting.get()) 0.2 else 1.0
|
||||
val boostingHandleDebuff = if (state.boosting.get()) 0.5 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 = 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()
|
||||
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
|
||||
|
||||
// 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
|
||||
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())
|
||||
|
||||
// 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())) * p * hypot(controller.forwardVector.x.toDouble(), controller.forwardVector.z.toDouble()).toFloat())
|
||||
// localPitch *= if (currSpeed > hoverSpeed) RidingBehaviour.scaleToRange(currSpeed, hoverSpeed, topSpeed)
|
||||
// else 0.0
|
||||
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
|
||||
}
|
||||
|
||||
// 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(localYaw, localPitch, rollForce)
|
||||
return Vec3(localYawForce, localPitch, rollForce)
|
||||
}
|
||||
|
||||
override fun canJump(
|
||||
@@ -640,6 +607,7 @@ 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)
|
||||
@@ -661,6 +629,7 @@ 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 {
|
||||
@@ -678,6 +647,7 @@ 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 {
|
||||
|
||||
+647
@@ -0,0 +1,647 @@
|
||||
/*
|
||||
* 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 -> abs(state.rideVelocity.get().z) > 0.05 && !state.onSurface.get()})
|
||||
.with(PoseOption(PoseType.STAND) { _, state, entity -> abs(state.rideVelocity.get().z) <= 0.05 && state.onSurface.get()})
|
||||
.with(PoseOption(PoseType.STAND) { _, state, entity -> abs(state.rideVelocity.get().z) > 0.05 && state.onSurface.get()})
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
val hitboxBreachPercentage = vehicle.runtime.resolveDouble(settings.hitboxBreachPercentage ?: globalSubmarine.hitboxBreachPercentage!!)
|
||||
|
||||
// Grab the position of the vehicle that when out of the water is considered breached
|
||||
val vehicleBreachPoint = vehicle.position().add(0.0, vehicle.bbHeight * hitboxBreachPercentage, 0.0)
|
||||
|
||||
// If the block above the center is not fluid but this one is then move the center
|
||||
val centerBlockPos = BlockPos.containing(vehicleBreachPoint)
|
||||
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
|
||||
|
||||
// Percentage (0.0->1.0) of ride needing to be breached out of the water to be considered surfaced.
|
||||
var hitboxBreachPercentage: Expression? = null
|
||||
private set
|
||||
|
||||
// Tope strafe speed in Bl/s
|
||||
var jumpExpr: Expression? = null
|
||||
// Yaw rate in degrees per second
|
||||
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(hitboxBreachPercentage)
|
||||
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()
|
||||
hitboxBreachPercentage = 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)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -74,7 +74,7 @@ class RidingAnimationData(val ride: PokemonEntity)
|
||||
}
|
||||
if( r ) {
|
||||
//TODO: should this return?
|
||||
val controller = (ride.firstPassenger as? OrientationControllable)?.orientationController ?: return
|
||||
val controller = (ride 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)
|
||||
}
|
||||
}
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.spawning
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner
|
||||
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawnerFactory
|
||||
import com.cobblemon.mod.common.platform.events.PlatformEvents
|
||||
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules
|
||||
import net.minecraft.server.level.ServerLevel
|
||||
import java.util.UUID
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
|
||||
/**
|
||||
* The [SpawnerManager] that handles Cobblemon's world spawner, which is made up
|
||||
* of [PlayerSpawner] instances. This manager listens for login and logout and
|
||||
* cleans up where relevant.
|
||||
*
|
||||
* @author Hiroku
|
||||
* @since February 14th, 2022
|
||||
*/
|
||||
object CobblemonWorldSpawnerManager : SpawnerManager() {
|
||||
|
||||
var spawnersForPlayers = mutableMapOf<UUID, PlayerSpawner>()
|
||||
|
||||
init {
|
||||
PlatformEvents.SERVER_PLAYER_LOGIN.subscribe { this.onPlayerLogin(it.player) }
|
||||
PlatformEvents.SERVER_PLAYER_LOGOUT.subscribe { this.onPlayerLogout(it.player) }
|
||||
PlatformEvents.CHANGE_DIMENSION.subscribe { this.onDimensionChange(it.player, it.origin, it.destination) }
|
||||
}
|
||||
|
||||
fun onPlayerLogin(player: ServerPlayer) {
|
||||
// Disables spawning
|
||||
if (!Cobblemon.config.enableSpawning || player.level().gameRules.getBoolean(CobblemonGameRules.DO_POKEMON_SPAWNING) == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
registerPlayerSpawner(player)
|
||||
}
|
||||
|
||||
fun onPlayerLogout(player: ServerPlayer) {
|
||||
unregisterPlayerSpawner(player)
|
||||
}
|
||||
|
||||
fun onDimensionChange(player: ServerPlayer, origin: ServerLevel?, destination: ServerLevel?) {
|
||||
fun getSpawnRule(level: ServerLevel?) = level?.gameRules?.getBoolean(CobblemonGameRules.DO_POKEMON_SPAWNING)
|
||||
|
||||
if (!Cobblemon.config.enableSpawning || getSpawnRule(origin) == getSpawnRule(destination)) {
|
||||
return
|
||||
}
|
||||
|
||||
val spawnerAction = if (destination?.gameRules?.getBoolean(CobblemonGameRules.DO_POKEMON_SPAWNING) == false) {
|
||||
::unregisterPlayerSpawner
|
||||
} else {
|
||||
::registerPlayerSpawner
|
||||
}
|
||||
spawnerAction(player)
|
||||
}
|
||||
|
||||
private fun unregisterPlayerSpawner(player: ServerPlayer) {
|
||||
spawnersForPlayers.remove(player.uuid)?.let { unregisterSpawner(it) }
|
||||
}
|
||||
|
||||
private fun registerPlayerSpawner(player: ServerPlayer) {
|
||||
PlayerSpawnerFactory.create(this, player).also {
|
||||
spawnersForPlayers[player.uuid] = it
|
||||
registerSpawner(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,16 +17,21 @@ import net.minecraft.world.entity.EntityType
|
||||
open class SpawnCause(
|
||||
val spawner: Spawner,
|
||||
entity: Entity? = null
|
||||
): SpawningInfluence {
|
||||
) : SpawningInfluence {
|
||||
val entityWorldId = entity?.level()?.dimension()
|
||||
val entityId = entity?.id
|
||||
val entityUUID = entity?.uuid
|
||||
val entityType = entity?.type
|
||||
|
||||
val entity: Entity?
|
||||
get() = if (entityType == EntityType.PLAYER) {
|
||||
server()?.playerList?.getPlayer(entityUUID!!)
|
||||
} else {
|
||||
server()?.getLevel(entityWorldId!!)?.getEntity(entityId!!)
|
||||
}
|
||||
get() =
|
||||
if (entityType == EntityType.PLAYER) {
|
||||
entityUUID?.let {
|
||||
server()?.playerList?.getPlayer(it)
|
||||
}
|
||||
} else if (entityWorldId != null && entityId != null) {
|
||||
server()?.getLevel(entityWorldId)?.getEntity(entityId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.spawning
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
|
||||
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner
|
||||
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
|
||||
import com.cobblemon.mod.common.api.spawning.spawner.TickingSpawner
|
||||
import com.cobblemon.mod.common.util.server
|
||||
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules.DO_POKEMON_SPAWNING
|
||||
|
||||
/**
|
||||
* A manager of various spawners. This is a class in which you should register
|
||||
* ticking spawners so that they can be automatically ticked by the server.
|
||||
* This also supports registering [SpawningInfluence]s to apply to each spawner
|
||||
* that is registered.
|
||||
*
|
||||
* @author Hiroku
|
||||
* @since February 5th, 2022
|
||||
*/
|
||||
open class SpawnerManager {
|
||||
val spawners = mutableListOf<Spawner>()
|
||||
get() {
|
||||
if (server()?.isSameThread == false) {
|
||||
Cobblemon.LOGGER.error("Illegal access to spawners list from non-server thread!")
|
||||
Exception().printStackTrace()
|
||||
}
|
||||
return field
|
||||
}
|
||||
val influences = mutableListOf<SpawningInfluence>()
|
||||
get() {
|
||||
if (server()?.isSameThread == false) {
|
||||
Cobblemon.LOGGER.error("Illegal access to influences from non-server thread!")
|
||||
Exception().printStackTrace()
|
||||
}
|
||||
return field
|
||||
}
|
||||
inline fun <reified T : Spawner> getSpawnersOfType() = spawners.filterIsInstance<T>()
|
||||
open fun getSpawnerByName(name: String) = spawners.find { it.name == name }
|
||||
|
||||
open fun registerSpawner(spawner: Spawner) {
|
||||
spawners.add(spawner)
|
||||
if (spawner !is TickingSpawner) {
|
||||
spawner.influences.addAll(influences)
|
||||
}
|
||||
}
|
||||
|
||||
open fun unregisterSpawner(spawner: Spawner) {
|
||||
spawners.remove(spawner)
|
||||
if (spawner !is TickingSpawner) {
|
||||
spawner.influences.removeAll(influences)
|
||||
}
|
||||
}
|
||||
|
||||
open fun onConfigReload() {
|
||||
spawners.filterIsInstance<PlayerSpawner>().forEach {
|
||||
it.ticksBetweenSpawns = Cobblemon.config.ticksBetweenSpawnAttempts
|
||||
}
|
||||
}
|
||||
|
||||
open fun onServerStarted() {
|
||||
spawners.clear()
|
||||
}
|
||||
|
||||
open fun onServerTick() {
|
||||
// Disables spawning
|
||||
if (!Cobblemon.config.enableSpawning) {
|
||||
return
|
||||
}
|
||||
influences.removeIf { it.isExpired() }
|
||||
getSpawnersOfType<TickingSpawner>().forEach {
|
||||
if (it.getCauseEntity()?.level()?.gameRules?.getBoolean(DO_POKEMON_SPAWNING) == false) {
|
||||
return@forEach
|
||||
}
|
||||
it.tick()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,15 +33,15 @@ class TimeRange : IntRanges {
|
||||
"any" to TimeRange(0..23999),
|
||||
"day" to TimeRange(23460..23999, 0..12541),
|
||||
"night" to TimeRange(12542..23459),
|
||||
"noon" to TimeRange(5000..6999),
|
||||
"midnight" to TimeRange(17000..18999),
|
||||
"dawn" to TimeRange(22300..23999, 0..999),
|
||||
"dusk" to TimeRange(11834..13701),
|
||||
"twilight" to TimeRange(11834..13701, 22300..23999, 0..166),
|
||||
"morning" to TimeRange(23000..23999, 0..4999),
|
||||
"noon" to TimeRange(5000..6999),
|
||||
"afternoon" to TimeRange(7000..12999),
|
||||
"predawn" to TimeRange(19000..22999),
|
||||
"evening" to TimeRange(13000..16999)
|
||||
"evening" to TimeRange(13000..16999),
|
||||
"midnight" to TimeRange(17000..18999),
|
||||
"predawn" to TimeRange(19000..22999),
|
||||
"dawn" to TimeRange(22300..23999, 0..166),
|
||||
"dusk" to TimeRange(11834..13701),
|
||||
"twilight" to TimeRange(11834..13701, 22300..23999, 0..166)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
+2
-3
@@ -12,16 +12,15 @@ import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.Cobblemon.LOGGER
|
||||
import com.cobblemon.mod.common.api.events.CobblemonEvents
|
||||
import com.cobblemon.mod.common.api.events.fishing.BobberSpawnPokemonEvent
|
||||
import com.cobblemon.mod.common.api.fishing.SpawnBaitEffects
|
||||
import com.cobblemon.mod.common.api.fishing.SpawnBait
|
||||
import com.cobblemon.mod.common.api.fishing.SpawnBaitEffects
|
||||
import com.cobblemon.mod.common.api.pokemon.Natures
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stats
|
||||
import com.cobblemon.mod.common.api.spawning.SpawnBucket
|
||||
import com.cobblemon.mod.common.api.spawning.SpawnCause
|
||||
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
|
||||
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
|
||||
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
|
||||
import com.cobblemon.mod.common.api.spawning.influence.SpawnBaitInfluence
|
||||
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
|
||||
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.item.interactive.PokerodItem
|
||||
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.spawning.fishing
|
||||
|
||||
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
|
||||
import com.cobblemon.mod.common.api.spawning.position.FishingSpawnablePosition
|
||||
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
|
||||
import com.cobblemon.mod.common.api.spawning.spawner.TriggerSpawner
|
||||
import net.minecraft.server.level.ServerLevel
|
||||
import net.minecraft.core.BlockPos
|
||||
|
||||
/**
|
||||
* A spawner that takes in a [FishingSpawnCause] and spawns things.
|
||||
*
|
||||
* @author Hiroku
|
||||
* @since February 3rd, 2024
|
||||
*/
|
||||
open class FishingSpawner(
|
||||
override val name: String = "fishing",
|
||||
pool: SpawnPool = CobblemonSpawnPools.WORLD_SPAWN_POOL
|
||||
) : TriggerSpawner<FishingSpawnCause>(name, pool) {
|
||||
override fun parseSpawnablePosition(cause: FishingSpawnCause, world: ServerLevel, pos: BlockPos): FishingSpawnablePosition? {
|
||||
// Maybe confirm that it's water we're fishing in?
|
||||
val spawnablePosition = FishingSpawnablePosition(
|
||||
cause = cause,
|
||||
world = world,
|
||||
pos = pos,
|
||||
influences = influences
|
||||
)
|
||||
return spawnablePosition
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.spawning.influence
|
||||
|
||||
import com.cobblemon.mod.common.api.spawning.SpawnBucket
|
||||
|
||||
/**
|
||||
* A [SpawningInfluence] that multiplies bucket weights by the provided values
|
||||
* and then normalizes the resulting weights so that their total adds up to 100.
|
||||
*/
|
||||
class BucketMultiplyingInfluence(
|
||||
val multipliers: Map<String, Float>,
|
||||
) : SpawningInfluence {
|
||||
override fun affectBucketWeights(bucketWeights: MutableMap<SpawnBucket, Float>) {
|
||||
val updates = mutableMapOf<SpawnBucket, Float>()
|
||||
for ((bucket, weight) in bucketWeights) {
|
||||
val multiplier = multipliers[bucket.name]
|
||||
if (multiplier != null) {
|
||||
updates[bucket] = weight * multiplier
|
||||
}
|
||||
}
|
||||
|
||||
for ((bucket, newWeight) in updates) {
|
||||
bucketWeights[bucket] = newWeight
|
||||
}
|
||||
|
||||
normalizeBucketWeights(bucketWeights)
|
||||
}
|
||||
}
|
||||
+1
-6
@@ -44,11 +44,6 @@ open class BucketNormalizingInfluence(
|
||||
bucketWeights[it] = weight.toDouble().pow(1 / normalizationFactor).toFloat()
|
||||
}
|
||||
|
||||
val sum = bucketWeights.values.sum()
|
||||
val to100 = 100F / sum
|
||||
bucketWeights.keys.toList().forEach {
|
||||
val weight = bucketWeights[it] ?: return@forEach
|
||||
bucketWeights[it] = weight * to100
|
||||
}
|
||||
normalizeBucketWeights(bucketWeights)
|
||||
}
|
||||
}
|
||||
+5
@@ -9,6 +9,7 @@
|
||||
package com.cobblemon.mod.common.api.spawning.influence
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonBlocks
|
||||
import com.cobblemon.mod.common.api.Priority
|
||||
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
|
||||
import com.cobblemon.mod.common.api.spawning.fishing.FishingSpawnCause
|
||||
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
|
||||
@@ -43,6 +44,10 @@ class SaccharineLogSlatheredInfluence(val pos: BlockPos? = null) : SpawningInflu
|
||||
|
||||
override fun affectSpawn(action: SpawnAction<*>, entity: Entity) {
|
||||
if (entity is PokemonEntity) {
|
||||
// avoid double application or applying to a pokemon that already has a hidden ability
|
||||
if (entity.pokemon.forcedAspects.contains(HONEY_DRENCHED_ASPECT)
|
||||
|| entity.pokemon.ability.priority !== Priority.LOWEST) return
|
||||
|
||||
if (Math.random() <= HIDDEN_ABILITY_CHANCE) {
|
||||
FishingSpawnCause.alterHAAttempt(entity)
|
||||
}
|
||||
|
||||
+9
@@ -40,6 +40,15 @@ interface SpawningInfluence {
|
||||
fun affectSpawn(action: SpawnAction<*>, entity: Entity) {}
|
||||
/** Applies some influence over the weight of spawn buckets. */
|
||||
fun affectBucketWeights(bucketWeights: MutableMap<SpawnBucket, Float>) {}
|
||||
/** Normalizes all bucket weights to add up to 100 */
|
||||
fun normalizeBucketWeights(bucketWeights: MutableMap<SpawnBucket, Float>) {
|
||||
val sum = bucketWeights.values.sum()
|
||||
val to100 = 100F / sum
|
||||
bucketWeights.keys.toList().forEach {
|
||||
val weight = bucketWeights[it] ?: return@forEach
|
||||
bucketWeights[it] = weight * to100
|
||||
}
|
||||
}
|
||||
/** Filters positions that could be usable for a spawnable position calculator */
|
||||
fun isAllowedPosition(world: ServerLevel, pos: BlockPos, spawnablePositionCalculator: SpawnablePositionCalculator<*, *>): Boolean = true
|
||||
/**
|
||||
|
||||
+3
-5
@@ -9,16 +9,15 @@
|
||||
package com.cobblemon.mod.common.api.spawning.influence.detector
|
||||
|
||||
import com.cobblemon.mod.common.api.spawning.SpawningZone
|
||||
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
|
||||
import com.cobblemon.mod.common.api.spawning.influence.SpawningZoneInfluence
|
||||
import com.cobblemon.mod.common.api.spawning.prospecting.IncenseSweetDetector
|
||||
import com.cobblemon.mod.common.api.spawning.prospecting.SaccharineLogSlatheredDetector
|
||||
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
|
||||
import com.cobblemon.mod.common.api.spawning.spawner.SpawningZoneInput
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.server.level.ServerLevel
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
|
||||
import com.cobblemon.mod.common.api.spawning.prospecting.IncenseSweetDetector
|
||||
import com.cobblemon.mod.common.api.spawning.prospecting.LurePokeSnackDetector
|
||||
import com.cobblemon.mod.common.api.spawning.prospecting.SaccharineLogSlatheredDetector
|
||||
|
||||
/**
|
||||
* Prospects for [SpawningZoneInfluence] in a world at a given position. Occurs as part of a
|
||||
@@ -31,7 +30,6 @@ interface SpawningInfluenceDetector {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
val detectors = mutableSetOf<SpawningInfluenceDetector>(
|
||||
LurePokeSnackDetector,
|
||||
SaccharineLogSlatheredDetector,
|
||||
IncenseSweetDetector
|
||||
)
|
||||
|
||||
+7
-5
@@ -14,13 +14,15 @@ import net.minecraft.server.level.ServerLevel
|
||||
import net.minecraft.core.BlockPos
|
||||
|
||||
/**
|
||||
* A spawnable position relating to triggered spawning. Uses a single-point structure cache which is
|
||||
* the only contribution of this implementation.
|
||||
* A spawnable position relating to single-point spawning. Uses a single-point structure cache which is
|
||||
* the only contribution of this implementation really. By contrast, the area spawnable position provides
|
||||
* a structure cache that's shared across the entire zone that was used for spawning, which is great for
|
||||
* performance. That kind of optimization shouldn't be necessary for single-point spawning.
|
||||
*
|
||||
* @author Hiroku
|
||||
* @since February 3rd, 2024
|
||||
*/
|
||||
open class TriggerSpawnablePosition(
|
||||
open class BasicSpawnablePosition(
|
||||
override val cause: SpawnCause,
|
||||
override val world: ServerLevel,
|
||||
override val position: BlockPos,
|
||||
@@ -29,6 +31,6 @@ open class TriggerSpawnablePosition(
|
||||
override val canSeeSky: Boolean,
|
||||
override val influences: MutableList<SpawningInfluence>
|
||||
) : SpawnablePosition() {
|
||||
val triggerStructureCache = StructureChunkCache()
|
||||
override fun getStructureCache(pos: BlockPos) = triggerStructureCache
|
||||
val singlePointStructureCache = StructureChunkCache()
|
||||
override fun getStructureCache(pos: BlockPos) = singlePointStructureCache
|
||||
}
|
||||
+1
-1
@@ -28,7 +28,7 @@ class FishingSpawnablePosition(
|
||||
world: ServerLevel,
|
||||
pos: BlockPos,
|
||||
influences: MutableList<SpawningInfluence>
|
||||
) : TriggerSpawnablePosition(
|
||||
) : BasicSpawnablePosition(
|
||||
cause = cause,
|
||||
world = world,
|
||||
position = pos,
|
||||
|
||||
+1
@@ -36,6 +36,7 @@ interface SpawnablePositionCalculator<I : SpawnablePositionInput, O : SpawnableP
|
||||
val isLavaCondition: (BlockState) -> Boolean = { it.fluidState.`is`(FluidTags.LAVA) && it.fluidState.isSource }
|
||||
|
||||
private val calculators = PrioritizedList<SpawnablePositionCalculator<*, *>>()
|
||||
|
||||
val prioritizedAreaCalculators: List<AreaSpawnablePositionCalculator<*>>
|
||||
get() = calculators.filterIsInstance<AreaSpawnablePositionCalculator<*>>()
|
||||
|
||||
|
||||
-189
@@ -1,189 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.spawning.prospecting
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonPoiTypes
|
||||
import com.cobblemon.mod.common.api.fishing.SpawnBait
|
||||
import com.cobblemon.mod.common.api.spawning.influence.BucketNormalizingInfluence
|
||||
import com.cobblemon.mod.common.api.spawning.influence.SpatialSpawningZoneInfluence
|
||||
import com.cobblemon.mod.common.api.spawning.influence.SpawnBaitInfluence
|
||||
import com.cobblemon.mod.common.api.spawning.influence.SpawningZoneInfluence
|
||||
import com.cobblemon.mod.common.api.spawning.influence.UnconditionalSpawningZoneInfluence
|
||||
import com.cobblemon.mod.common.api.spawning.influence.detector.SpawningInfluenceDetector
|
||||
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
|
||||
import com.cobblemon.mod.common.api.spawning.spawner.SpawningZoneInput
|
||||
import com.cobblemon.mod.common.block.PokeSnackBlock
|
||||
import com.cobblemon.mod.common.block.entity.PokeSnackBlockEntity
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.net.messages.client.effect.PokeSnackBlockParticlesPacket
|
||||
import com.cobblemon.mod.common.util.math.pow
|
||||
import com.cobblemon.mod.common.util.toBlockPos
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.sqrt
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.Direction
|
||||
import net.minecraft.core.Holder
|
||||
import net.minecraft.server.level.ServerLevel
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiManager
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiType
|
||||
import net.minecraft.world.level.block.Blocks
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import net.minecraft.world.phys.Vec3
|
||||
import kotlin.math.abs
|
||||
|
||||
object LurePokeSnackDetector : SpawningInfluenceDetector {
|
||||
|
||||
@JvmField
|
||||
val RANGE: Int = 48
|
||||
|
||||
@JvmField
|
||||
val POKE_SNACK_CRUMBED_ASPECT = "poke_snack_crumbed"
|
||||
|
||||
override fun detectFromInput(spawner: Spawner, input: SpawningZoneInput): MutableList<SpawningZoneInfluence> {
|
||||
val world = input.world
|
||||
val listOfInfluences = mutableListOf<SpawningZoneInfluence>()
|
||||
|
||||
val searchRange = RANGE + ceil(sqrt(((input.length pow 2) + (input.width pow 2)).toDouble())).toInt()
|
||||
val lurePokeSnackPositions = world.poiManager.findAll(
|
||||
{ holder: Holder<PoiType> -> holder.`is`(CobblemonPoiTypes.POKE_SNACK_KEY) },
|
||||
{ true },
|
||||
input.getCenter().toBlockPos(),
|
||||
searchRange,
|
||||
PoiManager.Occupancy.ANY
|
||||
).toList()
|
||||
|
||||
var highestLureTier = 0
|
||||
|
||||
for (lurePokeSnackPos in lurePokeSnackPositions) {
|
||||
val blockState = world.getBlockState(lurePokeSnackPos)
|
||||
if (blockState.block !is PokeSnackBlock) continue
|
||||
|
||||
val blockEntity = world.getBlockEntity(lurePokeSnackPos) as? PokeSnackBlockEntity ?: continue
|
||||
val baitEffects = blockEntity.getBaitEffects()
|
||||
|
||||
highestLureTier = maxOf(highestLureTier, baitEffects.filter { it.type == SpawnBait.Effects.RARITY_BUCKET }.maxOfOrNull { it.value }?.toInt() ?: 0)
|
||||
|
||||
listOfInfluences.add(
|
||||
SpatialSpawningZoneInfluence(
|
||||
pos = lurePokeSnackPos,
|
||||
radius = RANGE.toFloat(),
|
||||
influence = SpawnBaitInfluence(
|
||||
effects = baitEffects,
|
||||
onUsed = { usedAmount, entity ->
|
||||
entity?.let {
|
||||
// Prevent triggering twice on same entity
|
||||
if (usedAmount % 2 == 0) {
|
||||
tryBite(world, lurePokeSnackPos)
|
||||
moveAndApplyAspect(world, lurePokeSnackPos, entity)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (highestLureTier > 0) {
|
||||
listOfInfluences.add(UnconditionalSpawningZoneInfluence(influence = BucketNormalizingInfluence(tier = highestLureTier)))
|
||||
}
|
||||
|
||||
return listOfInfluences
|
||||
}
|
||||
|
||||
private fun tryBite(level: ServerLevel, blockPos: BlockPos) {
|
||||
if (level.isLoaded(blockPos)) {
|
||||
val blockState = level.getBlockState(blockPos)
|
||||
val block = blockState.block as? PokeSnackBlock ?: return
|
||||
|
||||
block.eat(level, blockPos, blockState, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun moveAndApplyAspect(level: ServerLevel, blockPos: BlockPos, entity: PokemonEntity?) {
|
||||
entity?.let {
|
||||
entity.pokemon.forcedAspects += POKE_SNACK_CRUMBED_ASPECT
|
||||
attemptSafeMove(level, blockPos, entity)
|
||||
}
|
||||
}
|
||||
|
||||
fun attemptSafeMove(level: ServerLevel, startPos: BlockPos, entity: PokemonEntity) {
|
||||
val inWater = entity.isInWater
|
||||
val maxDistance = 10 // maximum search radius
|
||||
val maxPositions = 10 // maximum number of positions at the closest distance
|
||||
|
||||
var foundPositions: List<BlockPos> = emptyList()
|
||||
|
||||
// Search layer by layer, starting from the closest
|
||||
for (distance in 1..maxDistance) {
|
||||
val positionsAtThisDistance = mutableListOf<BlockPos>()
|
||||
|
||||
for (dx in -distance..distance) {
|
||||
for (dy in -distance..distance) {
|
||||
for (dz in -distance..distance) {
|
||||
// Consider only positions on the surface of the cube at current distance
|
||||
if (listOf(dx, dy, dz).count { abs(it) != 0 } == distance) {
|
||||
val candidatePos = startPos.offset(dx, dy, dz)
|
||||
if (!candidatePos.equals(startPos.relative(Direction.UP))) {
|
||||
// Check collision
|
||||
val currentBox = entity.boundingBox.move(
|
||||
candidatePos.x - entity.x,
|
||||
candidatePos.y - entity.y,
|
||||
candidatePos.z - entity.z
|
||||
)
|
||||
|
||||
if (level.noCollision(entity, currentBox)) {
|
||||
val blockBelowPos = candidatePos.below()
|
||||
val blockState = level.getBlockState(blockBelowPos)
|
||||
|
||||
val isWater = blockState.block == Blocks.WATER
|
||||
if (inWater) {
|
||||
if (isWater) {
|
||||
positionsAtThisDistance.add(candidatePos)
|
||||
}
|
||||
} else {
|
||||
if (!blockState.isAir) {
|
||||
positionsAtThisDistance.add(candidatePos)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (positionsAtThisDistance.isNotEmpty()) {
|
||||
// Limit to 10 nearest positions (all at this distance)
|
||||
foundPositions = if (positionsAtThisDistance.size > maxPositions) {
|
||||
positionsAtThisDistance.shuffled().take(maxPositions)
|
||||
} else {
|
||||
positionsAtThisDistance
|
||||
}
|
||||
break // Found the nearest layer with safe positions, stop expanding
|
||||
}
|
||||
}
|
||||
|
||||
if (foundPositions.isNotEmpty()) {
|
||||
val chosenPos = foundPositions.random()
|
||||
val centerX = chosenPos.x + 0.5
|
||||
val centerZ = chosenPos.z + 0.5
|
||||
entity.moveTo(Vec3(centerX, chosenPos.y.toDouble(), centerZ))
|
||||
|
||||
PokeSnackBlockParticlesPacket(startPos, chosenPos).sendToPlayersAround(
|
||||
startPos.x.toDouble(),
|
||||
startPos.y.toDouble(),
|
||||
startPos.z.toDouble(),
|
||||
64.0,
|
||||
level.dimension()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun detectFromBlock(level: ServerLevel, pos: BlockPos, blockState: BlockState) = emptyList<SpawningZoneInfluence>()
|
||||
}
|
||||
+7
-2
@@ -24,6 +24,11 @@ import com.cobblemon.mod.common.api.spawning.spawner.Spawner
|
||||
* @since January 31st, 2022
|
||||
*/
|
||||
interface SpawningSelector<T : SpawnSelectionData> {
|
||||
companion object {
|
||||
@JvmField
|
||||
val DEFAULT = FlatSpawnablePositionWeightedSelector()
|
||||
}
|
||||
|
||||
fun getSelectionData(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>): T
|
||||
|
||||
fun selectSpawnAction(
|
||||
@@ -32,12 +37,12 @@ interface SpawningSelector<T : SpawnSelectionData> {
|
||||
selectionData: T
|
||||
): SpawnAction<*>?
|
||||
|
||||
fun select(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>, max: Int): List<SpawnAction<*>> {
|
||||
fun select(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>, maxSpawns: Int): List<SpawnAction<*>> {
|
||||
val selectionData = getSelectionData(spawner, bucket, spawnablePositions)
|
||||
|
||||
val spawnActions = selectionData.spawnActions
|
||||
|
||||
while (spawnActions.size < max) {
|
||||
while (spawnActions.size < maxSpawns) {
|
||||
val spawnAction = selectSpawnAction(
|
||||
spawner = spawner,
|
||||
bucket = bucket,
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.spawning.spawner
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.api.spawning.SpawnCause
|
||||
import com.cobblemon.mod.common.api.spawning.SpawnerManager
|
||||
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePositionResolver
|
||||
import com.cobblemon.mod.common.api.spawning.position.calculators.AreaSpawnablePositionCalculator
|
||||
import com.cobblemon.mod.common.api.spawning.position.calculators.SpawnablePositionCalculator.Companion.prioritizedAreaCalculators
|
||||
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
|
||||
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
|
||||
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.util.isBoxLoaded
|
||||
import com.cobblemon.mod.common.util.squeezeWithinBounds
|
||||
import com.cobblemon.mod.common.util.toVec3f
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.SectionPos
|
||||
import net.minecraft.world.level.ChunkPos
|
||||
import net.minecraft.world.level.Level
|
||||
import net.minecraft.world.level.chunk.ChunkAccess
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus
|
||||
import net.minecraft.world.level.pathfinder.PathComputationType
|
||||
import net.minecraft.world.phys.AABB
|
||||
import net.minecraft.world.phys.Vec3
|
||||
|
||||
/**
|
||||
* A type of [TickingSpawner] that operates within some area. When this spawner type
|
||||
* is told to do a spawning action, the subclass can provide a [SpawningZoneInput] to use.
|
||||
* If a non-null value is returned, the [spawningZoneGenerator] and [spawnablePositionResolver] will be used to
|
||||
* select a spawn and action it.
|
||||
*
|
||||
* Subclasses must implement the function retrieving what area to do the spawning in,
|
||||
* but otherwise this class is feature-complete.
|
||||
*
|
||||
* @author Hiroku
|
||||
* @since February 5th, 2022
|
||||
*/
|
||||
abstract class AreaSpawner(
|
||||
name: String,
|
||||
spawns: SpawnPool,
|
||||
manager: SpawnerManager
|
||||
) : TickingSpawner(name, spawns, manager) {
|
||||
abstract fun getZoneInput(cause: SpawnCause): SpawningZoneInput?
|
||||
|
||||
companion object {
|
||||
const val CHUNK_REACH = 3
|
||||
}
|
||||
|
||||
var spawningZoneGenerator: SpawningZoneGenerator = Cobblemon.spawningZoneGenerator
|
||||
var spawnablePositionResolver: AreaSpawnablePositionResolver = Cobblemon.areaSpawnablePositionResolver
|
||||
var spawnablePositionCalculators: List<AreaSpawnablePositionCalculator<*>> = prioritizedAreaCalculators
|
||||
|
||||
var spawnsPerPass: Int? = null
|
||||
|
||||
override fun run(cause: SpawnCause): List<SpawnAction<*>> {
|
||||
val areaInput = getZoneInput(cause)
|
||||
val constrainedArea = if (areaInput != null) constrainArea(areaInput) else null
|
||||
if (constrainedArea != null) {
|
||||
|
||||
val areaBox = AABB.ofSize(
|
||||
Vec3(constrainedArea.getCenter().toVec3f()),
|
||||
CHUNK_REACH * 16.0 * 2,
|
||||
1000.0,
|
||||
CHUNK_REACH * 16.0 * 2
|
||||
)
|
||||
|
||||
if (!constrainedArea.world.isBoxLoaded(areaBox)) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val numberNearby = constrainedArea.world.getEntitiesOfClass(
|
||||
PokemonEntity::class.java,
|
||||
areaBox,
|
||||
PokemonEntity::countsTowardsSpawnCap
|
||||
).size
|
||||
|
||||
val chunksCovered = CHUNK_REACH * CHUNK_REACH
|
||||
if (numberNearby.toFloat() / chunksCovered >= Cobblemon.config.pokemonPerChunk) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
//val prospectStart = System.currentTimeMillis()
|
||||
val zone = spawningZoneGenerator.generate(this, constrainedArea)
|
||||
//val prospectEnd = System.currentTimeMillis()
|
||||
val spawnablePositions = spawnablePositionResolver.resolve(this, spawnablePositionCalculators, zone)
|
||||
|
||||
val influences = this.getAllInfluences() + zone.unconditionalInfluences
|
||||
|
||||
val bucket = chooseBucket(cause, influences)
|
||||
//val resolveEnd = System.currentTimeMillis()
|
||||
//val prospectDuration = prospectEnd - prospectStart
|
||||
//val resolveDuration = resolveEnd - prospectEnd
|
||||
//println("Prospecting took: $prospectDuration ms. Resolution took: $resolveDuration ms")
|
||||
// Takes about 3ms on my laptop to prospect, similar to spawnable position resolve - not very good, needs some thought
|
||||
return getSpawningSelector().select(spawner = this, bucket = bucket, spawnablePositions = spawnablePositions, max = spawnsPerPass ?: Cobblemon.config.maximumSpawnsPerPass)
|
||||
}
|
||||
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
fun isValidStartPoint(world: Level, chunk: ChunkAccess, startPos: BlockPos.MutableBlockPos): Boolean {
|
||||
val y = startPos.y
|
||||
if (!world.isLoaded(startPos) || !world.isLoaded(startPos.setY(y + 1))) {
|
||||
return false
|
||||
}
|
||||
|
||||
val mid = chunk.getBlockState(startPos.setY(y))
|
||||
val above = chunk.getBlockState(startPos.setY(y + 1))
|
||||
|
||||
// Above must be non-solid
|
||||
if (!above.isPathfindable(PathComputationType.AIR)) {
|
||||
return false
|
||||
}
|
||||
// Position must be non-air
|
||||
if (mid.isAir) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun constrainArea(area: SpawningZoneInput): SpawningZoneInput? {
|
||||
val basePos = BlockPos.MutableBlockPos(area.baseX, area.baseY, area.baseZ)
|
||||
val originalY = area.baseY
|
||||
|
||||
val (chunkX, chunkZ) = Pair(SectionPos.blockToSectionCoord(area.baseX), SectionPos.blockToSectionCoord(area.baseZ))
|
||||
|
||||
// if the chunk isn't loaded, we don't want to go further & we don't want the getChunk function below to load/create the chunk.
|
||||
if (!area.world.areEntitiesLoaded(ChunkPos.asLong(chunkX, chunkZ))) return null
|
||||
|
||||
val chunk = area.world.getChunk(chunkX, chunkZ, ChunkStatus.FULL) ?: return null
|
||||
|
||||
var valid = isValidStartPoint(area.world, chunk, basePos)
|
||||
|
||||
if (!valid) {
|
||||
var offset = 1
|
||||
do {
|
||||
if (isValidStartPoint(area.world, chunk, basePos.setY(originalY + offset))) {
|
||||
valid = true
|
||||
basePos.y = originalY + offset
|
||||
break
|
||||
} else if (isValidStartPoint(area.world, chunk, basePos.setY(originalY - offset))) {
|
||||
valid = true
|
||||
basePos.y = originalY - offset
|
||||
break
|
||||
}
|
||||
offset++
|
||||
} while (offset <= Cobblemon.config.maxVerticalCorrectionBlocks)
|
||||
}
|
||||
|
||||
if (valid) {
|
||||
val min = area.world.squeezeWithinBounds(basePos)
|
||||
val max = area.world.squeezeWithinBounds(basePos.move(area.length, area.height, area.width))
|
||||
if (area.world.isLoaded(min) && area.world.isLoaded(max) &&
|
||||
min.x < max.x && min.y < max.y && min.z < max.z
|
||||
) {
|
||||
return SpawningZoneInput(
|
||||
cause = area.cause,
|
||||
world = area.world,
|
||||
baseX = min.x,
|
||||
baseY = min.y,
|
||||
baseZ = min.z,
|
||||
length = max.x - min.x,
|
||||
height = max.y - min.y,
|
||||
width = max.z - min.z
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.spawning.spawner
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
|
||||
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
|
||||
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
|
||||
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePositionResolver
|
||||
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
|
||||
|
||||
/**
|
||||
* A basic spawner implementation that uses Cobblemon's default algorithms for zone generation,
|
||||
* position resolving, and spawn selection.
|
||||
*
|
||||
* @author Hiroku
|
||||
* @since October 27th, 2025
|
||||
*/
|
||||
open class BasicSpawner(
|
||||
override val name: String,
|
||||
override var spawnPool: SpawnPool,
|
||||
override var maxPokemonPerChunk: Float = Cobblemon.config.pokemonPerChunk
|
||||
) : Spawner {
|
||||
override var selector: SpawningSelector<*> = SpawningSelector.DEFAULT
|
||||
override var generator: SpawningZoneGenerator = Cobblemon.spawningZoneGenerator
|
||||
override var resolver: AreaSpawnablePositionResolver = Cobblemon.areaSpawnablePositionResolver
|
||||
override val influences = mutableListOf<SpawningInfluence>()
|
||||
}
|
||||
+14
-10
@@ -8,33 +8,37 @@
|
||||
|
||||
package com.cobblemon.mod.common.api.spawning.spawner
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.api.spawning.SpawnCause
|
||||
import com.cobblemon.mod.common.api.spawning.SpawnerManager
|
||||
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
|
||||
import net.minecraft.server.level.ServerLevel
|
||||
import net.minecraft.core.BlockPos
|
||||
|
||||
/**
|
||||
* A spawner that works within a fixed area, and ticks on its own. The location
|
||||
* and size of the area is necessary for construction, but after that this spawner
|
||||
* can be registered in a [SpawnerManager] and left to its own devices.
|
||||
* A spawner that works within a fixed area. Instances provide the center point and the radii and then the spawner
|
||||
* can be told to [run] and it will spawn Pokémon within that area.
|
||||
*
|
||||
* @author Hiroku
|
||||
* @since February 5th, 2022
|
||||
*/
|
||||
open class FixedAreaSpawner(
|
||||
name: String,
|
||||
spawns: SpawnPool,
|
||||
manager: SpawnerManager,
|
||||
spawnPool: SpawnPool,
|
||||
val world: ServerLevel,
|
||||
val position: BlockPos,
|
||||
val horizontalRadius: Int,
|
||||
val verticalRadius: Int,
|
||||
override var ticksBetweenSpawns: Float = 20F
|
||||
) : AreaSpawner(name, spawns, manager) {
|
||||
override fun getZoneInput(cause: SpawnCause): SpawningZoneInput? {
|
||||
val basePos = position.offset(-horizontalRadius, -verticalRadius, -horizontalRadius)
|
||||
maxPokemonPerChunk: Float = Cobblemon.config.pokemonPerChunk
|
||||
) : BasicSpawner(name, spawnPool, maxPokemonPerChunk) {
|
||||
fun run(cause: SpawnCause, maxSpawns: Int? = null): List<Any> {
|
||||
return runForArea(
|
||||
zoneInput = getZoneInput(cause),
|
||||
maxSpawns = maxSpawns
|
||||
)
|
||||
}
|
||||
|
||||
fun getZoneInput(cause: SpawnCause): SpawningZoneInput {
|
||||
val basePos = position.offset(-horizontalRadius, -verticalRadius, -horizontalRadius)
|
||||
return SpawningZoneInput(
|
||||
cause = cause,
|
||||
world = world,
|
||||
|
||||
+28
-5
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common.api.spawning.spawner
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon.config
|
||||
import com.cobblemon.mod.common.api.spawning.SpawnCause
|
||||
import com.cobblemon.mod.common.api.spawning.SpawnerManager
|
||||
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
|
||||
import com.cobblemon.mod.common.util.getPlayer
|
||||
import com.cobblemon.mod.common.util.nextBetween
|
||||
@@ -31,11 +30,16 @@ import kotlin.random.Random
|
||||
* @author Hiroku
|
||||
* @since February 14th, 2022
|
||||
*/
|
||||
class PlayerSpawner(player: ServerPlayer, spawns: SpawnPool, manager: SpawnerManager) : AreaSpawner(player.name.string, spawns, manager) {
|
||||
open class PlayerSpawner(
|
||||
player: ServerPlayer,
|
||||
spawnPool: SpawnPool
|
||||
) : BasicSpawner(
|
||||
name = player.name.string,
|
||||
spawnPool = spawnPool
|
||||
) {
|
||||
val uuid: UUID = player.uuid
|
||||
override var ticksBetweenSpawns = config.ticksBetweenSpawnAttempts
|
||||
override fun getCauseEntity() = uuid.getPlayer()
|
||||
override fun getZoneInput(cause: SpawnCause): SpawningZoneInput? {
|
||||
|
||||
fun getZoneInput(cause: SpawnCause): SpawningZoneInput? {
|
||||
val player = uuid.getPlayer() ?: return null
|
||||
val zoneDiameter = config.spawningZoneDiameter
|
||||
val zoneHeight = config.spawningZoneHeight
|
||||
@@ -53,6 +57,7 @@ class PlayerSpawner(player: ServerPlayer, spawns: SpawnPool, manager: SpawnerMan
|
||||
} else {
|
||||
thetatemp
|
||||
}
|
||||
|
||||
val x = center.x + r * cos(theta)
|
||||
val z = center.z + r * sin(theta)
|
||||
|
||||
@@ -67,4 +72,22 @@ class PlayerSpawner(player: ServerPlayer, spawns: SpawnPool, manager: SpawnerMan
|
||||
width = zoneDiameter
|
||||
)
|
||||
}
|
||||
|
||||
var active = true
|
||||
var ticksUntilNextSpawn = 100F
|
||||
var ticksBetweenSpawns: Float = config.ticksBetweenSpawnAttempts
|
||||
var tickTimerMultiplier = 1F
|
||||
|
||||
fun tick() {
|
||||
if (!active) {
|
||||
return
|
||||
}
|
||||
|
||||
ticksUntilNextSpawn -= tickTimerMultiplier
|
||||
if (ticksUntilNextSpawn <= 0) {
|
||||
val zoneInput = getZoneInput(cause = SpawnCause(spawner = this, entity = uuid.getPlayer()))
|
||||
zoneInput?.let(::runForArea)
|
||||
ticksUntilNextSpawn = ticksBetweenSpawns
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common.api.spawning.spawner
|
||||
|
||||
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
|
||||
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnRules
|
||||
import com.cobblemon.mod.common.api.spawning.SpawnerManager
|
||||
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
|
||||
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence
|
||||
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence.Companion.TYPICAL_VARIATION
|
||||
@@ -28,7 +27,7 @@ import net.minecraft.server.level.ServerPlayer
|
||||
* @since February 14th, 2022
|
||||
*/
|
||||
object PlayerSpawnerFactory {
|
||||
var spawns: SpawnPool by mutableLazy {
|
||||
var spawnPool: SpawnPool by mutableLazy {
|
||||
CobblemonSpawnPools.WORLD_SPAWN_POOL
|
||||
}
|
||||
|
||||
@@ -46,9 +45,9 @@ object PlayerSpawnerFactory {
|
||||
)
|
||||
})
|
||||
|
||||
fun create(spawnerManager: SpawnerManager, player: ServerPlayer): PlayerSpawner {
|
||||
fun create(player: ServerPlayer): PlayerSpawner {
|
||||
val influences = influenceBuilders.mapNotNull { it(player) }
|
||||
return PlayerSpawner(player, spawns, spawnerManager).also {
|
||||
return PlayerSpawner(player, spawnPool).also {
|
||||
it.influences.addAll(influences)
|
||||
it.influences.addAll(
|
||||
CobblemonSpawnRules.rules.values.filter(SpawnRule::enabled).flatMap(SpawnRule::components)
|
||||
|
||||
@@ -14,33 +14,67 @@ import com.cobblemon.mod.common.api.events.entity.SpawnBucketChosenEvent
|
||||
import com.cobblemon.mod.common.api.spawning.BestSpawner
|
||||
import com.cobblemon.mod.common.api.spawning.SpawnBucket
|
||||
import com.cobblemon.mod.common.api.spawning.SpawnCause
|
||||
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
|
||||
import com.cobblemon.mod.common.api.spawning.detail.EntitySpawnResult
|
||||
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
|
||||
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
|
||||
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
|
||||
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
|
||||
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePositionResolver
|
||||
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
|
||||
import com.cobblemon.mod.common.api.spawning.position.calculators.SpawnablePositionCalculator.Companion.prioritizedAreaCalculators
|
||||
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.util.isBoxLoaded
|
||||
import com.cobblemon.mod.common.util.squeezeWithinBounds
|
||||
import com.cobblemon.mod.common.util.toVec3f
|
||||
import com.cobblemon.mod.common.util.weightedSelection
|
||||
import kotlin.Any
|
||||
import kotlin.Boolean
|
||||
import kotlin.Float
|
||||
import kotlin.Int
|
||||
import kotlin.Pair
|
||||
import kotlin.String
|
||||
import kotlin.collections.plus
|
||||
import kotlin.math.max
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.SectionPos
|
||||
import net.minecraft.world.level.ChunkPos
|
||||
import net.minecraft.world.level.Level
|
||||
import net.minecraft.world.level.chunk.ChunkAccess
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus
|
||||
import net.minecraft.world.level.pathfinder.PathComputationType
|
||||
import net.minecraft.world.phys.AABB
|
||||
import net.minecraft.world.phys.Vec3
|
||||
|
||||
/**
|
||||
* Interface representing something that performs the action of spawning. Various functions
|
||||
* exist to streamline the process of using the [BestSpawner].
|
||||
*
|
||||
* The simplest implementation is a [BasicSpawner]
|
||||
*
|
||||
* @author Hiroku
|
||||
* @since January 24th, 2022
|
||||
*/
|
||||
interface Spawner {
|
||||
val name: String
|
||||
val influences: MutableList<SpawningInfluence>
|
||||
fun getSpawningSelector(): SpawningSelector<*>
|
||||
fun setSpawningSelector(selector: SpawningSelector<*>)
|
||||
fun getSpawnPool(): SpawnPool
|
||||
fun setSpawnPool(spawnPool: SpawnPool)
|
||||
var spawnPool: SpawnPool
|
||||
var selector: SpawningSelector<*>
|
||||
var generator: SpawningZoneGenerator
|
||||
var resolver: AreaSpawnablePositionResolver
|
||||
var maxPokemonPerChunk: Float
|
||||
|
||||
fun <R> afterSpawn(action: SpawnAction<R>, result: R) {}
|
||||
|
||||
companion object {
|
||||
/** The chunk range radius over which spawning will roughly keep the entity density under control. */
|
||||
const val ENTITY_LIMIT_CHUNK_RANGE = 3
|
||||
}
|
||||
|
||||
fun getMatchingSpawns(bucket: SpawnBucket, spawnablePosition: SpawnablePosition): List<SpawnDetail> {
|
||||
val spawns = mutableListOf<SpawnDetail>()
|
||||
spawns.addAll(getSpawnPool().retrieve(bucket, spawnablePosition).filter { it.isSatisfiedBy(spawnablePosition) })
|
||||
spawns.addAll(spawnPool.retrieve(bucket, spawnablePosition).filter { it.isSatisfiedBy(spawnablePosition) })
|
||||
spawnablePosition.influences.forEach { influence ->
|
||||
val influencedSpawns = influence.injectSpawns(bucket, spawnablePosition)
|
||||
if (influencedSpawns != null) {
|
||||
@@ -50,6 +84,101 @@ interface Spawner {
|
||||
return spawns
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the spawner for a specific [SpawnablePosition] and calculates a [SpawnAction] that could spawn there.
|
||||
* This does not trigger the resulting spawn. You can trigger the spawn in the same action by running
|
||||
* [runForPosition] instead or manually complete the [SpawnAction] it returns using [SpawnAction.complete].
|
||||
*/
|
||||
fun calculateSpawnActionForPosition(
|
||||
cause: SpawnCause,
|
||||
spawnablePosition: SpawnablePosition,
|
||||
): SpawnAction<*>? {
|
||||
influences.removeIf { it.isExpired() }
|
||||
spawnablePosition.influences.addAll(influences)
|
||||
val bucket = chooseBucket(cause, spawnablePosition.influences)
|
||||
return selector.select(
|
||||
spawner = this,
|
||||
bucket = bucket,
|
||||
spawnablePositions = listOf(spawnablePosition),
|
||||
maxSpawns = 1
|
||||
).firstOrNull()
|
||||
}
|
||||
|
||||
fun calculateSpawnActionsForArea(
|
||||
zoneInput: SpawningZoneInput,
|
||||
maxSpawns: Int?
|
||||
): List<SpawnAction<*>> {
|
||||
val maxSpawns = maxSpawns ?: Cobblemon.config.maximumSpawnsPerPass
|
||||
influences.removeIf { it.isExpired() }
|
||||
|
||||
val constrainedArea = constrainArea(zoneInput)
|
||||
?: return emptyList()
|
||||
|
||||
val areaBox = AABB.ofSize(
|
||||
Vec3(constrainedArea.getCenter().toVec3f()),
|
||||
ENTITY_LIMIT_CHUNK_RANGE * 16.0 * 2,
|
||||
1000.0,
|
||||
ENTITY_LIMIT_CHUNK_RANGE * 16.0 * 2
|
||||
)
|
||||
|
||||
if (!constrainedArea.world.isBoxLoaded(areaBox)) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val numberNearby = constrainedArea.world.getEntitiesOfClass(
|
||||
PokemonEntity::class.java,
|
||||
areaBox,
|
||||
PokemonEntity::countsTowardsSpawnCap
|
||||
).size
|
||||
|
||||
val chunksCovered = ENTITY_LIMIT_CHUNK_RANGE * ENTITY_LIMIT_CHUNK_RANGE
|
||||
val maxPokemonPerChunk = max(Cobblemon.config.pokemonPerChunk, zoneInput.cause.spawner.maxPokemonPerChunk)
|
||||
if (numberNearby.toFloat() / chunksCovered >= maxPokemonPerChunk) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val zone = generator.generate(this, constrainedArea)
|
||||
val spawnablePositions = resolver.resolve(this, prioritizedAreaCalculators, zone)
|
||||
val influences = influences + zone.unconditionalInfluences
|
||||
val bucket = chooseBucket(zoneInput.cause, influences)
|
||||
|
||||
return selector.select(
|
||||
spawner = this,
|
||||
bucket = bucket,
|
||||
spawnablePositions = spawnablePositions,
|
||||
maxSpawns = maxSpawns
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the spawner at the given [SpawnablePosition] and returns whatever the calculated [SpawnAction] produces,
|
||||
* if any spawn occurred. For most implementations of [SpawnAction], the resulting type is
|
||||
* [EntitySpawnResult]. Technically you can make spawns for anything though.
|
||||
*/
|
||||
fun runForPosition(
|
||||
cause: SpawnCause,
|
||||
spawnablePosition: SpawnablePosition
|
||||
): Any? {
|
||||
val action = calculateSpawnActionForPosition(
|
||||
cause = cause,
|
||||
spawnablePosition = spawnablePosition
|
||||
) ?: return null
|
||||
|
||||
return action.complete()
|
||||
}
|
||||
|
||||
fun runForArea(
|
||||
zoneInput: SpawningZoneInput,
|
||||
maxSpawns: Int? = null
|
||||
): List<Any> {
|
||||
val spawnActions = calculateSpawnActionsForArea(zoneInput = zoneInput, maxSpawns = maxSpawns)
|
||||
val results = mutableListOf<Any>()
|
||||
for (spawnAction in spawnActions) {
|
||||
spawnAction.complete()?.let(results::add)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
fun copyInfluences() = influences.filter { !it.isExpired() }.toMutableList()
|
||||
|
||||
fun chooseBucket(cause: SpawnCause, influences: List<SpawningInfluence>): SpawnBucket {
|
||||
@@ -66,4 +195,77 @@ interface Spawner {
|
||||
CobblemonEvents.SPAWN_BUCKET_CHOSEN.post(event)
|
||||
return event.bucket
|
||||
}
|
||||
|
||||
fun isValidStartPoint(world: Level, chunk: ChunkAccess, startPos: BlockPos.MutableBlockPos): Boolean {
|
||||
val y = startPos.y
|
||||
if (!world.isLoaded(startPos) || !world.isLoaded(startPos.setY(y + 1))) {
|
||||
return false
|
||||
}
|
||||
|
||||
val mid = chunk.getBlockState(startPos.setY(y))
|
||||
val above = chunk.getBlockState(startPos.setY(y + 1))
|
||||
|
||||
// Above must be non-solid
|
||||
if (!above.isPathfindable(PathComputationType.AIR)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Position must be non-air
|
||||
if (mid.isAir) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun constrainArea(area: SpawningZoneInput): SpawningZoneInput? {
|
||||
val basePos = BlockPos.MutableBlockPos(area.baseX, area.baseY, area.baseZ)
|
||||
val originalY = area.baseY
|
||||
|
||||
val (chunkX, chunkZ) = Pair(SectionPos.blockToSectionCoord(area.baseX), SectionPos.blockToSectionCoord(area.baseZ))
|
||||
|
||||
// if the chunk isn't loaded, we don't want to go further & we don't want the getChunk function below to load/create the chunk.
|
||||
if (!area.world.areEntitiesLoaded(ChunkPos.asLong(chunkX, chunkZ))) return null
|
||||
|
||||
val chunk = area.world.getChunk(chunkX, chunkZ, ChunkStatus.FULL) ?: return null
|
||||
|
||||
var valid = isValidStartPoint(area.world, chunk, basePos)
|
||||
|
||||
if (!valid) {
|
||||
var offset = 1
|
||||
do {
|
||||
if (isValidStartPoint(area.world, chunk, basePos.setY(originalY + offset))) {
|
||||
valid = true
|
||||
basePos.y = originalY + offset
|
||||
break
|
||||
} else if (isValidStartPoint(area.world, chunk, basePos.setY(originalY - offset))) {
|
||||
valid = true
|
||||
basePos.y = originalY - offset
|
||||
break
|
||||
}
|
||||
offset++
|
||||
} while (offset <= Cobblemon.config.maxVerticalCorrectionBlocks)
|
||||
}
|
||||
|
||||
if (valid) {
|
||||
val min = area.world.squeezeWithinBounds(basePos)
|
||||
val max = area.world.squeezeWithinBounds(basePos.move(area.length, area.height, area.width))
|
||||
if (area.world.isLoaded(min) && area.world.isLoaded(max) &&
|
||||
min.x < max.x && min.y < max.y && min.z < max.z
|
||||
) {
|
||||
return SpawningZoneInput(
|
||||
cause = area.cause,
|
||||
world = area.world,
|
||||
baseX = min.x,
|
||||
baseY = min.y,
|
||||
baseZ = min.z,
|
||||
length = max.x - min.x,
|
||||
height = max.y - min.y,
|
||||
width = max.z - min.z
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.spawning.spawner
|
||||
|
||||
import com.cobblemon.mod.common.api.spawning.SpawnCause
|
||||
import com.cobblemon.mod.common.api.spawning.SpawnerManager
|
||||
import com.cobblemon.mod.common.api.spawning.detail.EntitySpawnResult
|
||||
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
|
||||
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
|
||||
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
|
||||
import com.cobblemon.mod.common.api.spawning.selection.FlatSpawnablePositionWeightedSelector
|
||||
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
|
||||
import net.minecraft.world.entity.Entity
|
||||
|
||||
/**
|
||||
* A spawner that regularly attempts spawning entities. It has timing utilities,
|
||||
* and subclasses must provide the logic for generating a spawn which is called
|
||||
* periodically by the server.
|
||||
*
|
||||
* @author Hiroku
|
||||
* @since February 5th, 2022
|
||||
*/
|
||||
abstract class TickingSpawner(
|
||||
override val name: String,
|
||||
var spawns: SpawnPool,
|
||||
val manager: SpawnerManager
|
||||
) : Spawner {
|
||||
private var selector: SpawningSelector<*> = FlatSpawnablePositionWeightedSelector()
|
||||
override val influences = mutableListOf<SpawningInfluence>()
|
||||
|
||||
override fun getSpawningSelector() = selector
|
||||
override fun setSpawningSelector(selector: SpawningSelector<*>) { this.selector = selector }
|
||||
override fun getSpawnPool() = spawns
|
||||
override fun setSpawnPool(spawnPool: SpawnPool) { spawns = spawnPool }
|
||||
|
||||
abstract fun run(cause: SpawnCause): List<SpawnAction<*>>
|
||||
|
||||
var active = true
|
||||
val spawnedEntities = mutableListOf<Entity>()
|
||||
|
||||
var lastSpawnTime = 0L
|
||||
var ticksUntilNextSpawn = 100F
|
||||
abstract var ticksBetweenSpawns: Float
|
||||
var tickTimerMultiplier = 1F
|
||||
|
||||
var removalCheckTicks = 0
|
||||
|
||||
open fun tick() {
|
||||
removalCheckTicks++
|
||||
influences.removeIf { it.isExpired() }
|
||||
if (removalCheckTicks == 60) {
|
||||
spawnedEntities.removeIf { it.isRemoved }
|
||||
removalCheckTicks = 0
|
||||
}
|
||||
|
||||
if (!active) {
|
||||
return
|
||||
}
|
||||
|
||||
ticksUntilNextSpawn -= tickTimerMultiplier
|
||||
if (ticksUntilNextSpawn <= 0) {
|
||||
val spawnActions = run(cause = SpawnCause(spawner = this, entity = getCauseEntity()))
|
||||
ticksUntilNextSpawn = ticksBetweenSpawns
|
||||
for (spawnAction in spawnActions) {
|
||||
spawnAction.complete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun <R> afterSpawn(action: SpawnAction<R>, result: R) {
|
||||
super.afterSpawn(action, result)
|
||||
if (result is EntitySpawnResult) {
|
||||
spawnedEntities.addAll(result.entities)
|
||||
}
|
||||
lastSpawnTime = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
open fun getCauseEntity(): Entity? = null
|
||||
|
||||
fun getAllInfluences() = this.influences + manager.influences
|
||||
|
||||
override fun copyInfluences() = this.getAllInfluences().toMutableList()
|
||||
}
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.spawning.spawner
|
||||
|
||||
import com.cobblemon.mod.common.api.spawning.SpawnCause
|
||||
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
|
||||
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
|
||||
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
|
||||
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
|
||||
import com.cobblemon.mod.common.api.spawning.selection.FlatSpawnablePositionWeightedSelector
|
||||
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.server.level.ServerLevel
|
||||
|
||||
/**
|
||||
* A type of spawner that occurs at a single point and provides its own means of generating a
|
||||
* spawnable position from a particular cause and position. The trigger spawner must be directly called to
|
||||
* spawn things (almost as if it was being triggered!!!) as opposed to being scheduled on a ticker.
|
||||
*
|
||||
* @author Hiroku
|
||||
* @since February 3rd, 2024
|
||||
*/
|
||||
abstract class TriggerSpawner<T : SpawnCause>(override val name: String, var spawns: SpawnPool) : Spawner {
|
||||
private var selector: SpawningSelector<*> = FlatSpawnablePositionWeightedSelector()
|
||||
override val influences = mutableListOf<SpawningInfluence>()
|
||||
|
||||
open fun getSpawnAction(
|
||||
cause: T,
|
||||
world: ServerLevel,
|
||||
pos: BlockPos,
|
||||
influences: List<SpawningInfluence> = emptyList()
|
||||
): SpawnAction<*>? {
|
||||
val spawnablePosition = parseSpawnablePosition(cause, world, pos) ?: return null
|
||||
spawnablePosition.influences.addAll(influences)
|
||||
val bucket = chooseBucket(cause, spawnablePosition.influences)
|
||||
return selector.select(this, bucket, listOf(spawnablePosition), max = 1).firstOrNull()
|
||||
}
|
||||
|
||||
/** Parses a spawnable position, if possible, from the given cause and position. */
|
||||
abstract fun parseSpawnablePosition(cause: T, world: ServerLevel, pos: BlockPos): SpawnablePosition?
|
||||
|
||||
override fun getSpawningSelector() = selector
|
||||
override fun setSpawningSelector(selector: SpawningSelector<*>) { this.selector = selector }
|
||||
override fun getSpawnPool() = spawns
|
||||
override fun setSpawnPool(spawnPool: SpawnPool) { spawns = spawnPool }
|
||||
}
|
||||
@@ -137,6 +137,9 @@ object CobblemonItemTags {
|
||||
@JvmField val WEARABLE_HAT_ITEMS = create("held/visibility/hat")
|
||||
@JvmField val HIDDEN_ITEMS = create("held/visibility/hidden")
|
||||
|
||||
//Empty Tag
|
||||
@JvmField val EMPTY = create("empty")
|
||||
|
||||
|
||||
private fun create(path: String) = TagKey.create(Registries.ITEM, cobblemonResource(path))
|
||||
|
||||
|
||||
+6
-1
@@ -20,6 +20,8 @@ import com.cobblemon.mod.common.api.molang.MoLangFunctions.addStandardFunctions
|
||||
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMostSpecificMoLangValue
|
||||
import com.cobblemon.mod.common.api.moves.Moves
|
||||
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
|
||||
import com.cobblemon.mod.common.api.moves.animations.ActionEffectTimeline
|
||||
import com.cobblemon.mod.common.api.moves.animations.ActionEffects
|
||||
import com.cobblemon.mod.common.api.moves.animations.TargetsProvider
|
||||
import com.cobblemon.mod.common.api.moves.animations.UsersProvider
|
||||
import com.cobblemon.mod.common.battles.ShowdownInterpreter
|
||||
@@ -31,6 +33,7 @@ import com.cobblemon.mod.common.battles.dispatch.UntilDispatch
|
||||
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
|
||||
import com.cobblemon.mod.common.pokemon.evolution.progress.UseMoveEvolutionProgress
|
||||
import com.cobblemon.mod.common.util.asArrayValue
|
||||
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
|
||||
import com.cobblemon.mod.common.util.battleLang
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import java.util.concurrent.CompletableFuture
|
||||
@@ -49,7 +52,7 @@ class MoveInstruction(
|
||||
) : InterpreterInstruction, CauserInstruction {
|
||||
val effect = message.effectAt(1) ?: Effect.pure("", "")
|
||||
val move = Moves.getByNameOrDummy(effect.id)
|
||||
val actionEffect = move.actionEffect
|
||||
val actionEffect: ActionEffectTimeline? by lazy { ActionEffects.getEffectWithBattleContext(cobblemonResource(move.name), userPokemon) }
|
||||
val spreadTargets = message.optionalArgument("spread")?.split(",") ?: emptyList()
|
||||
|
||||
var future = CompletableFuture.completedFuture(Unit)
|
||||
@@ -61,6 +64,8 @@ class MoveInstruction(
|
||||
override fun invoke(battle: PokemonBattle) {
|
||||
userPokemon = message.battlePokemon(0, battle)!!
|
||||
targetPokemon = message.battlePokemon(2, battle)
|
||||
|
||||
val actionEffect = actionEffect ?: ActionEffects.actionEffects["generic_move".asIdentifierDefaultingNamespace()]
|
||||
val targetPokemon = targetPokemon // So smart non-null casts can happen
|
||||
|
||||
val optionalEffect = message.effect()
|
||||
|
||||
@@ -18,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
|
||||
@@ -140,13 +141,14 @@ class PokeSnackBlock(settings: Properties, val isLure: Boolean): BaseEntityBlock
|
||||
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 {
|
||||
@@ -308,6 +310,7 @@ class PokeSnackBlock(settings: Properties, val isLure: Boolean): BaseEntityBlock
|
||||
|
||||
if (newBites > MAX_BITES) {
|
||||
dropCandle(level, pos, state, player)
|
||||
|
||||
level.removeBlock(pos, false)
|
||||
level.removeBlockEntity(pos)
|
||||
} else {
|
||||
@@ -366,8 +369,19 @@ class PokeSnackBlock(settings: Properties, val isLure: Boolean): BaseEntityBlock
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isRandomlyTicking(state: BlockState) = isLure
|
||||
|
||||
override fun randomTick(
|
||||
state: BlockState,
|
||||
level: ServerLevel,
|
||||
pos: BlockPos,
|
||||
random: RandomSource
|
||||
) {
|
||||
if (level.isClientSide) return
|
||||
(level.getBlockEntity(pos) as? PokeSnackBlockEntity)?.randomTick()
|
||||
}
|
||||
}
|
||||
|
||||
+27
-25
@@ -52,8 +52,8 @@ class CookingPotMenu : RecipeBookMenu<CraftingInput, CookingPotRecipeBase>, Cont
|
||||
constructor(containerId: Int, playerInventory: Inventory) : super(CobblemonMenuType.COOKING_POT, containerId) {
|
||||
this.playerInventory = playerInventory
|
||||
this.container = CookingPotContainer(this,
|
||||
CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH,
|
||||
CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
|
||||
CampfireBlockEntity.CRAFTING_GRID_WIDTH,
|
||||
CampfireBlockEntity.CRAFTING_GRID_WIDTH
|
||||
)
|
||||
this.resultContainer = ResultContainer()
|
||||
this.containerData = SimpleContainerData(4)
|
||||
@@ -82,25 +82,25 @@ class CookingPotMenu : RecipeBookMenu<CraftingInput, CookingPotRecipeBase>, Cont
|
||||
val resultSlotX = 128
|
||||
val resultSlotY = 55
|
||||
|
||||
addSlot(CookingPotResultSlot(this.container, CampfireBlockEntity.Companion.RESULT_SLOT, resultSlotX, resultSlotY))
|
||||
addSlot(CookingPotResultSlot(this.container, CampfireBlockEntity.RESULT_SLOT, resultSlotX, resultSlotY))
|
||||
|
||||
for ((index, slotIndex) in CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.withIndex()) {
|
||||
val i = index / CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
|
||||
val j = index % CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
|
||||
for ((index, slotIndex) in CampfireBlockEntity.CRAFTING_GRID_SLOTS.withIndex()) {
|
||||
val i = index / CampfireBlockEntity.CRAFTING_GRID_WIDTH
|
||||
val j = index % CampfireBlockEntity.CRAFTING_GRID_WIDTH
|
||||
addSlot(Slot(this.container, slotIndex, 33 + j * 18, 18 + i * 18))
|
||||
}
|
||||
|
||||
for ((index, slotIndex) in CampfireBlockEntity.Companion.SEASONING_SLOTS.withIndex()) {
|
||||
addSlot(SeasoningSlot(this.container, slotIndex, 110 + index * 18, 18))
|
||||
for ((index, slotIndex) in CampfireBlockEntity.SEASONING_SLOTS.withIndex()) {
|
||||
addSlot(SeasoningSlot(this, this.container, slotIndex, 110 + index * 18, 18))
|
||||
}
|
||||
|
||||
for ((index, _) in CampfireBlockEntity.Companion.PLAYER_INVENTORY_SLOTS.withIndex()) {
|
||||
val i = index / CampfireBlockEntity.Companion.PLAYER_INVENTORY_WIDTH
|
||||
val j = index % CampfireBlockEntity.Companion.PLAYER_INVENTORY_WIDTH
|
||||
for ((index, _) in CampfireBlockEntity.PLAYER_INVENTORY_SLOTS.withIndex()) {
|
||||
val i = index / CampfireBlockEntity.PLAYER_INVENTORY_WIDTH
|
||||
val j = index % CampfireBlockEntity.PLAYER_INVENTORY_WIDTH
|
||||
addSlot(Slot(playerInventory, index + 9, 8 + j * 18, 84 + i * 18))
|
||||
}
|
||||
|
||||
for ((index, _) in CampfireBlockEntity.Companion.PLAYER_HOTBAR_SLOTS.withIndex()) {
|
||||
for ((index, _) in CampfireBlockEntity.PLAYER_HOTBAR_SLOTS.withIndex()) {
|
||||
addSlot(Slot(playerInventory, index, 8 + index * 18, 142))
|
||||
}
|
||||
}
|
||||
@@ -173,29 +173,31 @@ class CookingPotMenu : RecipeBookMenu<CraftingInput, CookingPotRecipeBase>, Cont
|
||||
currentActiveRecipe = recipe
|
||||
if (recipe != null) {
|
||||
previewItem = recipe.value.assemble(craftInput, level.registryAccess())
|
||||
// Apply seasoning to the preview item
|
||||
recipe.value.applySeasoning(
|
||||
previewItem,
|
||||
container.items.subList(CampfireBlockEntity.SEASONING_SLOTS.first,
|
||||
CampfireBlockEntity.SEASONING_SLOTS.last + 1)
|
||||
.filterNotNull()
|
||||
.filter { !it.isEmpty })
|
||||
.filter { !it.isEmpty && it.`is`(recipe.value.seasoningTag) })
|
||||
|
||||
} else previewItem = ItemStack.EMPTY
|
||||
}
|
||||
|
||||
override fun getResultSlotIndex(): Int {
|
||||
return CampfireBlockEntity.Companion.RESULT_SLOT
|
||||
return CampfireBlockEntity.RESULT_SLOT
|
||||
}
|
||||
|
||||
override fun getGridWidth(): Int {
|
||||
return CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
|
||||
return CampfireBlockEntity.CRAFTING_GRID_WIDTH
|
||||
}
|
||||
|
||||
override fun getGridHeight(): Int {
|
||||
return CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
|
||||
return CampfireBlockEntity.CRAFTING_GRID_WIDTH
|
||||
}
|
||||
|
||||
override fun getSize(): Int {
|
||||
return CampfireBlockEntity.Companion.ITEMS_SIZE
|
||||
return CampfireBlockEntity.ITEMS_SIZE
|
||||
}
|
||||
|
||||
fun getBurnProgress(): Float {
|
||||
@@ -227,24 +229,24 @@ class CookingPotMenu : RecipeBookMenu<CraftingInput, CookingPotRecipeBase>, Cont
|
||||
val slotItemStack = slot.item
|
||||
itemStack = slotItemStack.copy()
|
||||
|
||||
if (index == CampfireBlockEntity.Companion.RESULT_SLOT) {
|
||||
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.PLAYER_INVENTORY_SLOTS.first, CampfireBlockEntity.Companion.PLAYER_HOTBAR_SLOTS.last + 1, false)) {
|
||||
if (index == CampfireBlockEntity.RESULT_SLOT) {
|
||||
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.PLAYER_INVENTORY_SLOTS.first, CampfireBlockEntity.PLAYER_HOTBAR_SLOTS.last + 1, false)) {
|
||||
return ItemStack.EMPTY
|
||||
}
|
||||
|
||||
slot.onQuickCraft(slotItemStack, itemStack);
|
||||
} else if (index in CampfireBlockEntity.Companion.PLAYER_INVENTORY_SLOTS || index in CampfireBlockEntity.Companion.PLAYER_HOTBAR_SLOTS) {
|
||||
} else if (index in CampfireBlockEntity.PLAYER_INVENTORY_SLOTS || index in CampfireBlockEntity.PLAYER_HOTBAR_SLOTS) {
|
||||
if (Seasonings.isSeasoning(slotItemStack)) {
|
||||
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.first, CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.last + 1, false) &&
|
||||
!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.SEASONING_SLOTS.first, CampfireBlockEntity.Companion.SEASONING_SLOTS.last + 1, false)
|
||||
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.CRAFTING_GRID_SLOTS.first, CampfireBlockEntity.CRAFTING_GRID_SLOTS.last + 1, false) &&
|
||||
!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.SEASONING_SLOTS.first, CampfireBlockEntity.SEASONING_SLOTS.last + 1, false)
|
||||
) {
|
||||
return ItemStack.EMPTY
|
||||
}
|
||||
} else if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.first, CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.last + 1, false)) {
|
||||
} else if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.CRAFTING_GRID_SLOTS.first, CampfireBlockEntity.CRAFTING_GRID_SLOTS.last + 1, false)) {
|
||||
return ItemStack.EMPTY
|
||||
}
|
||||
} else if (index in CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS || index in CampfireBlockEntity.Companion.SEASONING_SLOTS) {
|
||||
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.PLAYER_INVENTORY_SLOTS.first, CampfireBlockEntity.Companion.PLAYER_HOTBAR_SLOTS.last + 1, false)) {
|
||||
} else if (index in CampfireBlockEntity.CRAFTING_GRID_SLOTS || index in CampfireBlockEntity.SEASONING_SLOTS) {
|
||||
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.PLAYER_INVENTORY_SLOTS.first, CampfireBlockEntity.PLAYER_HOTBAR_SLOTS.last + 1, false)) {
|
||||
return ItemStack.EMPTY
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
|
||||
package com.cobblemon.mod.common.block.campfirepot
|
||||
|
||||
import com.cobblemon.mod.common.api.cooking.Seasonings
|
||||
import net.minecraft.world.Container
|
||||
import net.minecraft.world.inventory.Slot
|
||||
import net.minecraft.world.item.ItemStack
|
||||
|
||||
class SeasoningSlot(container: Container, slot: Int, x: Int, y: Int) : Slot(container, slot, x, y) {
|
||||
class SeasoningSlot(val menu: CookingPotMenu, container: Container, slot: Int, x: Int, y: Int) : Slot(container, slot, x, y) {
|
||||
override fun mayPlace(stack: ItemStack): Boolean {
|
||||
return Seasonings.isSeasoning(stack) && super.mayPlace(stack)
|
||||
val recipe = menu.currentActiveRecipe?.value() ?: return false
|
||||
return stack.`is`(recipe.seasoningTag) && super.mayPlace(stack)
|
||||
}
|
||||
}
|
||||
@@ -168,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
|
||||
@@ -314,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)
|
||||
|
||||
+150
-8
@@ -8,34 +8,121 @@
|
||||
|
||||
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 = 4
|
||||
const val SPAWNS_PER_BITE = 1
|
||||
const val RADIUS = 8
|
||||
const val RANDOM_TICKS_BETWEEN_SPAWNS = 4
|
||||
const val POKE_SNACK_CRUMBED_ASPECT = "poke_snack_crumbed"
|
||||
}
|
||||
|
||||
val spawner: FixedAreaSpawner by lazy {
|
||||
FixedAreaSpawner(
|
||||
name = "poke_snack_spawner_${level?.dimension()?.location()}}_$blockPos",
|
||||
spawnPool = CobblemonSpawnPools.WORLD_SPAWN_POOL,
|
||||
world = level as ServerLevel,
|
||||
position = blockPos,
|
||||
horizontalRadius = RADIUS,
|
||||
verticalRadius = RADIUS,
|
||||
maxPokemonPerChunk = Cobblemon.config.pokeSnackPokemonPerChunk
|
||||
).also {
|
||||
it.influences.add(this)
|
||||
|
||||
val baitEffects = getBaitEffects()
|
||||
|
||||
val stackedLureTier = baitEffects
|
||||
.filter { it.type == SpawnBait.Effects.RARITY_BUCKET }
|
||||
.sumOf { it.value }
|
||||
.toInt()
|
||||
|
||||
if (stackedLureTier > 0) {
|
||||
it.influences += BucketNormalizingInfluence(
|
||||
tier = stackedLureTier,
|
||||
gradient = 0.2F,
|
||||
firstTier = 1.2F
|
||||
)
|
||||
}
|
||||
|
||||
it.influences += BucketMultiplyingInfluence(
|
||||
mapOf(
|
||||
"uncommon" to 2.25f,
|
||||
"rare" to 5.5f,
|
||||
"ultra-rare" to 5.5f,
|
||||
)
|
||||
)
|
||||
|
||||
it.influences += SpawnBaitInfluence(effects = baitEffects)
|
||||
}
|
||||
}
|
||||
|
||||
// Only non-herd Pokémon can be spawned from a Poké Snack
|
||||
override fun affectSpawnable(detail: SpawnDetail, spawnablePosition: SpawnablePosition) = detail.type in SpawnDetail.pokemonTypes && detail !is PokemonHerdSpawnDetail
|
||||
|
||||
// When a Pokémon spawns, apply the crumbed aspect and play the eating effects. Eat part of the cake, too.
|
||||
override fun affectSpawn(action: SpawnAction<*>, entity: Entity) {
|
||||
if (entity is PokemonEntity) {
|
||||
entity.pokemon.forcedAspects += POKE_SNACK_CRUMBED_ASPECT
|
||||
val block = blockState.block as? PokeSnackBlock ?: return
|
||||
val level = level ?: return
|
||||
val entityPos = entity.blockPosition()
|
||||
|
||||
PokeSnackBlockParticlesPacket(blockPos, entityPos).sendToPlayersAround(
|
||||
blockPos.x.toDouble(),
|
||||
blockPos.y.toDouble(),
|
||||
blockPos.z.toDouble(),
|
||||
64.0,
|
||||
level.dimension(),
|
||||
)
|
||||
|
||||
block.eat(level, blockPos, blockState, action.spawnablePosition.cause.entity as? Player)
|
||||
}
|
||||
}
|
||||
|
||||
var placedBy: UUID? = null
|
||||
var amountSpawned: Int = 0
|
||||
var flavourComponent: FlavourComponent? = null
|
||||
var baitEffectsComponent: BaitEffectsComponent? = null
|
||||
var ingredientComponent: IngredientComponent? = null
|
||||
var randomTicksUntilNextSpawn: Float = getRandomTicksBetweenSpawns()
|
||||
|
||||
fun initializeFromItemStack(itemStack: ItemStack) {
|
||||
flavourComponent = itemStack.get(CobblemonItemComponents.FLAVOUR)
|
||||
@@ -43,9 +130,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +142,45 @@ 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)
|
||||
|
||||
@@ -79,7 +203,11 @@ 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) {
|
||||
@@ -113,6 +241,12 @@ 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) {
|
||||
@@ -146,5 +280,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,8 +56,11 @@ object MountedCameraTypeHandler {
|
||||
data.updatePerspective(key, currentCameraType)
|
||||
}
|
||||
|
||||
fun handleTransition(fromKey: ResourceLocation, toKey: ResourceLocation) {
|
||||
fun handleTransition(passenger: Entity, fromKey: ResourceLocation, toKey: ResourceLocation) {
|
||||
val mc = Minecraft.getInstance()
|
||||
if (passenger == mc.player) {
|
||||
return
|
||||
}
|
||||
val currentCameraType = mc.options.cameraType
|
||||
val data = ClientPersistedData.ridingPerspectives
|
||||
data.updatePerspective(fromKey, currentCameraType)
|
||||
|
||||
@@ -491,6 +491,12 @@ class PokemonClientDelegate : PosableState(), PokemonSideDelegate {
|
||||
cryAnimation = animation
|
||||
}
|
||||
|
||||
override fun applyRenderRotation(partialTick: Float) {
|
||||
currentEntity.ifRidingAvailable { behaviour, settings, state ->
|
||||
behaviour.applyRenderRotation(settings, state, currentEntity, partialTick)
|
||||
}
|
||||
}
|
||||
|
||||
fun getSeatLocator(passenger: Entity): String {
|
||||
val seatIndex = this.getEntity().occupiedSeats.indexOf(passenger)
|
||||
if (seatIndex == -1) throw IllegalArgumentException("Entity is not currently riding a seat")
|
||||
|
||||
@@ -11,26 +11,25 @@ package com.cobblemon.mod.common.client.gui
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.CobblemonItems
|
||||
import com.cobblemon.mod.common.api.gui.blitk
|
||||
import com.cobblemon.mod.common.api.gui.drawPosablePortrait
|
||||
import com.cobblemon.mod.common.api.text.darkGray
|
||||
import com.cobblemon.mod.common.api.text.red
|
||||
import com.cobblemon.mod.common.api.text.text
|
||||
import com.cobblemon.mod.common.client.CobblemonClient
|
||||
import com.cobblemon.mod.common.client.CobblemonResources
|
||||
import com.cobblemon.mod.common.client.gui.battle.BattleGUI
|
||||
import com.cobblemon.mod.common.client.gui.portrait.FullyAnimatedPortraitDrawer
|
||||
import com.cobblemon.mod.common.client.gui.portrait.SelectedAnimatedPortraitDrawer
|
||||
import com.cobblemon.mod.common.client.gui.portrait.InanimatePortraitDrawer
|
||||
import com.cobblemon.mod.common.client.gui.portrait.PortraitStyle
|
||||
import com.cobblemon.mod.common.client.gui.toast.CobblemonToast
|
||||
import com.cobblemon.mod.common.client.keybind.boundKey
|
||||
import com.cobblemon.mod.common.client.keybind.keybinds.HidePartyBinding
|
||||
import com.cobblemon.mod.common.client.keybind.keybinds.SummaryBinding
|
||||
import com.cobblemon.mod.common.client.render.drawScaledText
|
||||
import com.cobblemon.mod.common.client.render.getDepletableRedGreen
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
|
||||
import com.cobblemon.mod.common.client.render.renderScaledGuiItemIcon
|
||||
import com.cobblemon.mod.common.pokemon.Gender
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.lang
|
||||
import java.util.UUID
|
||||
import net.minecraft.client.DeltaTracker
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.gui.Gui
|
||||
@@ -58,12 +57,10 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
|
||||
private val genderIconMale = cobblemonResource("textures/gui/party/party_gender_male.png")
|
||||
private val genderIconFemale = cobblemonResource("textures/gui/party/party_gender_female.png")
|
||||
private val portraitBackground = cobblemonResource("textures/gui/party/party_slot_portrait_background.png")
|
||||
|
||||
private val inanimatePortraitDrawer = InanimatePortraitDrawer()
|
||||
private val selectedAnimatedPortraitDrawer = SelectedAnimatedPortraitDrawer()
|
||||
private val fullyAnimatedPortraitDrawer = FullyAnimatedPortraitDrawer()
|
||||
}
|
||||
|
||||
private val stateAtIndex = hashMapOf<Int, Pair<UUID, FloatingState>>()
|
||||
|
||||
private val screenExemptions: List<Class<out Screen>> = listOf(
|
||||
ChatScreen::class.java,
|
||||
BattleGUI::class.java
|
||||
@@ -88,6 +85,16 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
|
||||
attachedToast = false
|
||||
}
|
||||
|
||||
fun getPokemonState(index: Int, pokemonUUID: UUID): FloatingState {
|
||||
val state = this.stateAtIndex.getOrPut(index) { pokemonUUID to FloatingState() }
|
||||
if (state.first != pokemonUUID) {
|
||||
val newState = FloatingState()
|
||||
this.stateAtIndex[index] = pokemonUUID to newState
|
||||
return newState
|
||||
}
|
||||
return state.second
|
||||
}
|
||||
|
||||
override fun render(context: GuiGraphics, tickCounter: DeltaTracker) {
|
||||
val partialDeltaTicks = tickCounter.realtimeDeltaTicks
|
||||
|
||||
@@ -160,13 +167,20 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
|
||||
0.0
|
||||
)
|
||||
|
||||
val portraitDrawer = when (Cobblemon.config.partyPortraitAnimations) {
|
||||
PortraitStyle.NEVER_ANIMATE -> inanimatePortraitDrawer
|
||||
PortraitStyle.ANIMATE_SELECTED -> selectedAnimatedPortraitDrawer
|
||||
PortraitStyle.ALWAYS_ANIMATE -> fullyAnimatedPortraitDrawer
|
||||
val shouldAnimate = when (Cobblemon.config.partyPortraitAnimations) {
|
||||
PokemonGUIAnimationStyle.NEVER_ANIMATE -> false
|
||||
PokemonGUIAnimationStyle.ANIMATE_SELECTED -> selectedSlot == index
|
||||
PokemonGUIAnimationStyle.ALWAYS_ANIMATE -> true
|
||||
}
|
||||
|
||||
portraitDrawer.draw(pokemon, matrices, partialDeltaTicks, selectedSlot == index, index)
|
||||
drawPosablePortrait(
|
||||
identifier = pokemon.species.resourceIdentifier,
|
||||
matrixStack = matrices,
|
||||
doQuirks = false,
|
||||
partialTicks = if (shouldAnimate) partialDeltaTicks else 0F,
|
||||
contextScale = pokemon.form.baseScale,
|
||||
state = getPokemonState(index = index, pokemonUUID = pokemon.uuid).also { it.currentAspects = pokemon.aspects }
|
||||
)
|
||||
|
||||
matrices.popPose()
|
||||
context.disableScissor()
|
||||
|
||||
+4
-8
@@ -6,24 +6,20 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.gui.portrait
|
||||
package com.cobblemon.mod.common.client.gui
|
||||
|
||||
import com.mojang.serialization.Codec
|
||||
import net.minecraft.util.StringRepresentable
|
||||
|
||||
enum class PortraitStyle : StringRepresentable {
|
||||
enum class PokemonGUIAnimationStyle : StringRepresentable {
|
||||
|
||||
NEVER_ANIMATE,
|
||||
ANIMATE_SELECTED,
|
||||
ALWAYS_ANIMATE;
|
||||
|
||||
override fun getSerializedName(): String = this.name
|
||||
|
||||
override fun getSerializedName() = this.name
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
val CODEC: Codec<PortraitStyle> = StringRepresentable.fromEnum(PortraitStyle::values)
|
||||
|
||||
val CODEC: Codec<PokemonGUIAnimationStyle> = StringRepresentable.fromEnum(PokemonGUIAnimationStyle::values)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
package com.cobblemon.mod.common.client.gui.battle
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.api.gui.blitk
|
||||
import com.cobblemon.mod.common.api.gui.drawPosablePortrait
|
||||
import com.cobblemon.mod.common.api.pokedex.PokedexEntryProgress
|
||||
@@ -322,7 +323,7 @@ class BattleOverlay : Gui(Minecraft.getInstance()), Schedulable {
|
||||
reversed = reversed,
|
||||
doQuirks = false,
|
||||
state = state,
|
||||
partialTicks = partialTicks
|
||||
partialTicks = if (Cobblemon.config.animateBattleTiles) partialTicks else 0F
|
||||
)
|
||||
}
|
||||
matrixStack.popPose()
|
||||
|
||||
+1
-3
@@ -13,7 +13,6 @@ import com.cobblemon.mod.common.api.gui.blitk
|
||||
import com.cobblemon.mod.common.api.moves.MoveTemplate
|
||||
import com.cobblemon.mod.common.api.moves.Moves
|
||||
import com.cobblemon.mod.common.battles.*
|
||||
import com.cobblemon.mod.common.client.gui.battle.subscreen.BattleGimmickButton.GimmickTile
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.math.toRGB
|
||||
import com.mojang.blaze3d.vertex.PoseStack
|
||||
@@ -139,8 +138,7 @@ abstract class BattleGimmickButton(gimmick: ShowdownMoveset.Gimmick, val x: Floa
|
||||
pp = gimmickTemplate?.pp ?: moveTemplate.pp,
|
||||
priority = gimmickTemplate?.priority ?: moveTemplate.priority,
|
||||
critRatio = gimmickTemplate?.critRatio ?: moveTemplate.critRatio,
|
||||
effectChances = gimmickTemplate?.effectChances ?: moveTemplate.effectChances,
|
||||
actionEffect = null
|
||||
effectChances = gimmickTemplate?.effectChances ?: moveTemplate.effectChances
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -201,7 +201,7 @@ class CookingPotScreen(
|
||||
}
|
||||
|
||||
override fun renderSlot(guiGraphics: GuiGraphics, slot: Slot) {
|
||||
if (slot is SeasoningSlot && !menu.carried.isEmpty && !slot.mayPlace(menu.carried)) {
|
||||
if (slot is SeasoningSlot && ((!menu.carried.isEmpty && !slot.mayPlace(menu.carried)) || (slot.hasItem() && !slot.mayPlace(slot.item))) ) {
|
||||
val x = slot.x
|
||||
val y = slot.y
|
||||
guiGraphics.fill(x,y,x+16,y+16,822018048)
|
||||
|
||||
+1
-1
@@ -299,7 +299,7 @@ class DialogueScreen(var dialogueDTO: DialogueDTO) : Screen("gui.dialogue".asTra
|
||||
fun playGibberSpeak(delta: Float, text: String, gibber: DialogueGibberDTO) {
|
||||
timeElapsedSinceGibber += delta / 20
|
||||
|
||||
if (timeElapsedSinceGibber < gibber.interval) {
|
||||
if (timeElapsedSinceGibber < gibber.interval || gibberDone) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,12 @@
|
||||
|
||||
package com.cobblemon.mod.common.client.gui.pc
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.api.gui.blitk
|
||||
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
|
||||
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
|
||||
import com.cobblemon.mod.common.client.gui.pasture.PasturePCGUIConfiguration
|
||||
import com.cobblemon.mod.common.client.gui.PokemonGUIAnimationStyle
|
||||
import com.cobblemon.mod.common.client.render.drawScaledText
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
|
||||
import com.cobblemon.mod.common.client.render.renderScaledGuiItemIcon
|
||||
@@ -72,12 +74,19 @@ open class StorageSlot(
|
||||
matrices.translate(posX + (SIZE / 2.0), posY + 1.0, 0.0)
|
||||
matrices.scale(2.5F, 2.5F, 1F)
|
||||
|
||||
val animationConfig = if (this is PartyStorageSlot) Cobblemon.config.summaryProfileAnimations else Cobblemon.config.pcProfileAnimations
|
||||
val shouldAnimate = when (animationConfig) {
|
||||
PokemonGUIAnimationStyle.ALWAYS_ANIMATE -> true
|
||||
PokemonGUIAnimationStyle.NEVER_ANIMATE -> false
|
||||
PokemonGUIAnimationStyle.ANIMATE_SELECTED -> isHoveredOrFocused
|
||||
}
|
||||
|
||||
drawProfilePokemon(
|
||||
renderablePokemon = pokemon.asRenderablePokemon(),
|
||||
matrixStack = matrices,
|
||||
rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, 35F, 0F)),
|
||||
state = state,
|
||||
partialTicks = partialTicks,
|
||||
partialTicks = if (shouldAnimate) partialTicks else 0F,
|
||||
scale = 4.5F
|
||||
)
|
||||
matrices.popPose()
|
||||
|
||||
-63
@@ -1,63 +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.client.gui.portrait
|
||||
|
||||
import com.cobblemon.mod.common.api.gui.drawPosablePortrait
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import com.mojang.blaze3d.vertex.PoseStack
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Base logic for a [PortraitDrawer] that animates a portrait based on conditions.
|
||||
*/
|
||||
abstract class AnimatedPortraitDrawer : PortraitDrawer {
|
||||
|
||||
private val state = FloatingState()
|
||||
|
||||
private val stateAtIndex = hashMapOf<Int, Pair<UUID, FloatingState>>()
|
||||
|
||||
override fun draw(pokemon: Pokemon, poseStack: PoseStack, partialTicks: Float, isSelected: Boolean, index: Int) {
|
||||
drawPosablePortrait(
|
||||
identifier = pokemon.species.resourceIdentifier,
|
||||
matrixStack = poseStack,
|
||||
partialTicks = this.ticksFor(pokemon, isSelected, partialTicks),
|
||||
contextScale = pokemon.form.baseScale,
|
||||
state = this.stateFor(pokemon, isSelected, index)
|
||||
)
|
||||
}
|
||||
|
||||
protected abstract fun shouldAnimate(pokemon: Pokemon, isSelected: Boolean): Boolean
|
||||
|
||||
protected open fun ticksFor(pokemon: Pokemon, isSelected: Boolean, partialTicks: Float): Float {
|
||||
return if (this.shouldAnimate(pokemon, isSelected)) partialTicks else 0F
|
||||
}
|
||||
|
||||
protected open fun stateFor(pokemon: Pokemon, isSelected: Boolean, index: Int): FloatingState {
|
||||
if (!this.shouldAnimate(pokemon, isSelected)) {
|
||||
return this.state
|
||||
}
|
||||
val state = this.stateAtIndex.getOrPut(index) { pokemon.uuid to this.createState(pokemon) }
|
||||
if (state.first != pokemon.uuid) {
|
||||
val newState = this.createState(pokemon)
|
||||
this.stateAtIndex[index] = pokemon.uuid to newState
|
||||
return newState
|
||||
}
|
||||
return state.second
|
||||
}
|
||||
|
||||
protected open fun createState(pokemon: Pokemon): FloatingState {
|
||||
val state = FloatingState()
|
||||
state.currentAspects = pokemon.aspects
|
||||
return state
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
-25
@@ -1,25 +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.client.gui.portrait
|
||||
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import net.minecraft.client.GraphicsStatus
|
||||
|
||||
/**
|
||||
* Draws portraits if the Pokemon is not fainted.
|
||||
*
|
||||
* Meant for client with [GraphicsStatus.FABULOUS].
|
||||
*/
|
||||
class FullyAnimatedPortraitDrawer : AnimatedPortraitDrawer() {
|
||||
|
||||
override fun shouldAnimate(pokemon: Pokemon, isSelected: Boolean): Boolean {
|
||||
return !pokemon.isFainted()
|
||||
}
|
||||
|
||||
}
|
||||
-37
@@ -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.client.gui.portrait
|
||||
|
||||
import com.cobblemon.mod.common.api.gui.drawPosablePortrait
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import com.mojang.blaze3d.vertex.PoseStack
|
||||
import net.minecraft.client.GraphicsStatus
|
||||
|
||||
/**
|
||||
* The simplest implementation, the Pokemon is drawn staying still.
|
||||
*
|
||||
* Meant for client with [GraphicsStatus.FAST].
|
||||
*/
|
||||
class InanimatePortraitDrawer : PortraitDrawer {
|
||||
|
||||
private val state = FloatingState()
|
||||
|
||||
override fun draw(pokemon: Pokemon, poseStack: PoseStack, partialTicks: Float, isSelected: Boolean, index: Int) {
|
||||
state.currentAspects = pokemon.aspects
|
||||
drawPosablePortrait(
|
||||
identifier = pokemon.species.resourceIdentifier,
|
||||
matrixStack = poseStack,
|
||||
partialTicks = 0F,
|
||||
contextScale = pokemon.form.baseScale,
|
||||
state = this.state
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.client.gui.portrait
|
||||
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import com.mojang.blaze3d.vertex.PoseStack
|
||||
|
||||
/**
|
||||
* Responsible for drawing a Pokemon portrait.
|
||||
*
|
||||
* Keep in mind any scaling and handling of the [PoseStack] must be done on the implementations using this system.
|
||||
*/
|
||||
fun interface PortraitDrawer {
|
||||
|
||||
/**
|
||||
* Draws the portrait.
|
||||
*
|
||||
* @param pokemon The [Pokemon] being drawn.
|
||||
* @param poseStack The [PoseStack] being used.
|
||||
* @param partialTicks The partial ticks of the screen using this.
|
||||
* @param isSelected If the [pokemon] is considered in a selected state for example hovered or the current party member.
|
||||
* @param index The index of the [pokemon] in the screen using this.
|
||||
*/
|
||||
fun draw(
|
||||
pokemon: Pokemon,
|
||||
poseStack: PoseStack,
|
||||
partialTicks: Float,
|
||||
isSelected: Boolean,
|
||||
index: Int
|
||||
)
|
||||
|
||||
}
|
||||
-25
@@ -1,25 +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.client.gui.portrait
|
||||
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import net.minecraft.client.GraphicsStatus
|
||||
|
||||
/**
|
||||
* Draws portraits if the Pokemon is not fainted and selected.
|
||||
*
|
||||
* Meant for client with [GraphicsStatus.FANCY].
|
||||
*/
|
||||
class SelectedAnimatedPortraitDrawer : AnimatedPortraitDrawer() {
|
||||
|
||||
override fun shouldAnimate(pokemon: Pokemon, isSelected: Boolean): Boolean {
|
||||
return !pokemon.isFainted() && isSelected
|
||||
}
|
||||
|
||||
}
|
||||
+10
-1
@@ -8,9 +8,11 @@
|
||||
|
||||
package com.cobblemon.mod.common.client.gui.summary.widgets
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.CobblemonSounds
|
||||
import com.cobblemon.mod.common.api.gui.blitk
|
||||
import com.cobblemon.mod.common.client.CobblemonResources
|
||||
import com.cobblemon.mod.common.client.gui.PokemonGUIAnimationStyle
|
||||
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
|
||||
import com.cobblemon.mod.common.client.gui.summary.Summary
|
||||
import com.cobblemon.mod.common.client.render.drawScaledText
|
||||
@@ -154,13 +156,20 @@ class PartySlotWidget(
|
||||
matrices.pushPose()
|
||||
matrices.translate(x + (PORTRAIT_DIAMETER / 2.0), y - 3.0, 0.0)
|
||||
matrices.scale(2.5F, 2.5F, 1F)
|
||||
|
||||
val shouldAnimate = when (Cobblemon.config.summaryProfileAnimations) {
|
||||
PokemonGUIAnimationStyle.ALWAYS_ANIMATE -> true
|
||||
PokemonGUIAnimationStyle.NEVER_ANIMATE -> false
|
||||
PokemonGUIAnimationStyle.ANIMATE_SELECTED -> isHovered || isSelected
|
||||
}
|
||||
|
||||
drawProfilePokemon(
|
||||
species = slotPokemon.species.resourceIdentifier,
|
||||
matrixStack = matrices,
|
||||
rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, 35F, 0F)),
|
||||
state = state,
|
||||
scale = 4.5F,
|
||||
partialTicks = delta
|
||||
partialTicks = if (shouldAnimate) delta else 0F
|
||||
)
|
||||
matrices.popPose()
|
||||
|
||||
|
||||
+13
-12
@@ -78,14 +78,17 @@ object PartySendBinding : CobblemonBlockingKeyBinding(
|
||||
return
|
||||
}
|
||||
|
||||
if (CobblemonClient.storage.selectedSlot == -1) return
|
||||
if (Minecraft.getInstance().screen != null) return
|
||||
|
||||
val selectedPartyPokemon = CobblemonClient.storage.party.get(CobblemonClient.storage.selectedSlot) ?: return
|
||||
val selectedPartyPokemon = if (CobblemonClient.storage.selectedSlot >= 0) {
|
||||
CobblemonClient.storage.party.get(CobblemonClient.storage.selectedSlot)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (isRidingPokemon(player) && canAttemptDismount(player, selectedPartyPokemon)) {
|
||||
sendToServer(DismountPokemonPacket())
|
||||
}
|
||||
else if (!isRidingSelectedPokemon(player, selectedPartyPokemon)){
|
||||
} else if (selectedPartyPokemon != null && !isRidingSelectedPokemon(player, selectedPartyPokemon)){
|
||||
checkForTargetInteractions(player, selectedPartyPokemon)
|
||||
}
|
||||
}
|
||||
@@ -105,8 +108,7 @@ object PartySendBinding : CobblemonBlockingKeyBinding(
|
||||
collideBlock = ClipContext.Fluid.NONE)
|
||||
if (canSendOutPokemon(player, targetEntity)) {
|
||||
sendToServer(SendOutPokemonPacket(CobblemonClient.storage.selectedSlot))
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
processEntityTarget(player, selectedPartyPokemon, targetEntity)
|
||||
}
|
||||
}
|
||||
@@ -140,7 +142,7 @@ object PartySendBinding : CobblemonBlockingKeyBinding(
|
||||
}
|
||||
}
|
||||
|
||||
private fun canAttemptDismount(player: LocalPlayer, selectedPartyPokemon: Pokemon): Boolean {
|
||||
private fun canAttemptDismount(player: LocalPlayer, selectedPartyPokemon: Pokemon?): Boolean {
|
||||
if (player.vehicle !is PokemonEntity) return false
|
||||
val vehicle = player.vehicle as PokemonEntity
|
||||
if (player != vehicle.controllingPassenger) {
|
||||
@@ -148,11 +150,10 @@ object PartySendBinding : CobblemonBlockingKeyBinding(
|
||||
}
|
||||
val isAirRide = vehicle.ridingController?.context?.style == RidingStyle.AIR
|
||||
val hasLandRide = vehicle.rideProp.behaviours?.get(RidingStyle.LAND) != null
|
||||
if (isAirRide && hasLandRide) {
|
||||
return false
|
||||
}
|
||||
else {
|
||||
return vehicle.pokemon.uuid == selectedPartyPokemon.uuid
|
||||
return if (isAirRide && hasLandRide) {
|
||||
false
|
||||
} else {
|
||||
vehicle.pokemon.uuid == selectedPartyPokemon?.uuid
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -29,5 +29,6 @@ object RideSettingsSyncHandler : ClientNetworkPacketHandler<RideSettingsSyncPack
|
||||
CobblemonRideSettings.boat = packet.boat
|
||||
CobblemonRideSettings.burst = packet.burst
|
||||
CobblemonRideSettings.dolphin = packet.dolphin
|
||||
CobblemonRideSettings.submarine = packet.submarine
|
||||
}
|
||||
}
|
||||
+27
-8
@@ -10,22 +10,41 @@ 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> {
|
||||
object ClientboundUpdateOrientationHandler : ClientNetworkPacketHandler<ClientboundUpdateOrientationPacket> {
|
||||
override fun handle(packet: ClientboundUpdateOrientationPacket, client: Minecraft) {
|
||||
client.executeIfPossible {
|
||||
val level = client.level ?: return@executeIfPossible
|
||||
val entity = level.getEntity(packet.entityId)
|
||||
if (entity is OrientationControllable) {
|
||||
packet.active?.let {
|
||||
entity.orientationController.active = it
|
||||
if (!it) {
|
||||
entity.orientationController.reset()
|
||||
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()))
|
||||
}
|
||||
}
|
||||
|
||||
vehicle.orientationController.active = shouldUseCustomOrientation
|
||||
}
|
||||
entity.orientationController.updateOrientation { _ -> packet.orientation }
|
||||
vehicle.orientationController.updateOrientation { _ -> packet.orientation }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+65
-75
@@ -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.Matrix3f
|
||||
import org.joml.Quaternionf
|
||||
import org.joml.Vector3f
|
||||
|
||||
object MountedCameraRenderer
|
||||
{
|
||||
fun getCameraPosition(
|
||||
instance: Camera,
|
||||
entity: Entity,
|
||||
driver: Entity,
|
||||
vehicle: Entity,
|
||||
x: Double,
|
||||
y: Double,
|
||||
@@ -51,99 +51,86 @@ object MountedCameraRenderer
|
||||
Mth.lerp(instance.partialTickTime.toDouble(), vehicle.zOld, vehicle.z)
|
||||
)
|
||||
setup(vehicle, instance.partialTickTime)
|
||||
val rollable = entity as OrientationControllable
|
||||
val controller = rollable.getOrientationController()
|
||||
val rollable = vehicle as OrientationControllable
|
||||
val vehicleController = rollable.getOrientationController()
|
||||
|
||||
val model = getPoser(vehicle.pokemon.species.resourceIdentifier, FloatingState())
|
||||
|
||||
val locatorName = delegate.getSeatLocator(driver)
|
||||
val locator = delegate.locatorStates[locatorName] ?: return null
|
||||
|
||||
// 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]
|
||||
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)
|
||||
|
||||
if (locator == null) {
|
||||
return null
|
||||
}
|
||||
// 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.
|
||||
|
||||
val locatorOffset = Vec3(locator.matrix.getTranslation(Vector3f()))
|
||||
|
||||
// Get Eye height
|
||||
val currEyeHeight: Double = Mth.lerp(instance.partialTickTime.toDouble(), eyeHeight, eyeHeightOld)
|
||||
var offset = Vector3f(0f, (currEyeHeight - (entity.bbHeight / 2)).toFloat(), 0f)
|
||||
|
||||
// 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)
|
||||
)
|
||||
}
|
||||
|
||||
if (controller.isActive()) {
|
||||
offset = controller.getRenderOrientation(instance.partialTickTime).transform(offset)
|
||||
} else {
|
||||
offset = Axis.YP.rotationDegrees(180f - instance.yRot).transform(offset)
|
||||
}
|
||||
|
||||
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))
|
||||
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 {
|
||||
val xRot = (-1 * Math.toRadians(instance.xRot.toDouble())).toFloat()
|
||||
offset.add(
|
||||
getThirdPersonOffset(thirdPersonReverse, model.thirdPersonCameraOffsetNoViewBobbing, locatorName, shouldFlip)
|
||||
)
|
||||
}
|
||||
|
||||
// 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)
|
||||
// Grab the current camera rotation to use as the offset rotation.
|
||||
val offsetRot = instance.rotation()
|
||||
offset = offsetRot.transform(offset)
|
||||
eyeOffset = offsetRot.transform(eyeOffset)
|
||||
|
||||
// Get pivot from poser
|
||||
val pivotOffsets = model.thirdPersonPivotOffset
|
||||
val pokemonCenter = Vector3f(0f, entity.bbHeight/2, 0f)
|
||||
val eyeLocatorOffset = Vec3(locator.matrix.getTranslation(Vector3f()))
|
||||
val eyePos = eyeLocatorOffset.add(entityPos).toVector3f()
|
||||
|
||||
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)
|
||||
// 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)
|
||||
}
|
||||
|
||||
val pos = entityPos.toVector3f().add(pivot)
|
||||
val offsetDistance = offset.length()
|
||||
val offsetDirection = offset.mul(1 / offsetDistance)
|
||||
|
||||
// Get offset from poser
|
||||
val offset = getThirdPersonOffset(thirdPersonReverse, model.thirdPersonCameraOffsetNoViewBobbing, locatorName)
|
||||
val eyeOffsetDistance = eyeOffset.length()
|
||||
val eyeOffsetDirection = eyeOffset.mul(1 / eyeOffsetDistance)
|
||||
|
||||
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)
|
||||
val eyeMaxZoom: Float = getMaxZoom(eyeOffsetDistance, eyeOffsetDirection, eyePos, instance)
|
||||
|
||||
// Use getMaxZoom to calculate clipping
|
||||
val maxZoom: Float = getMaxZoom(offsetDistance, offsetDirection, pos, instance)
|
||||
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))
|
||||
}
|
||||
|
||||
return Vec3(offsetDirection.mul(maxZoom).add(pos))
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
|
||||
private fun getThirdPersonOffset(
|
||||
thirdPersonReverse: Boolean,
|
||||
cameraOffsets: Map<String, Vec3>,
|
||||
locatorName: String
|
||||
locatorName: String,
|
||||
shouldFlip: Boolean = true
|
||||
): Vector3f {
|
||||
val offset = if (thirdPersonReverse && cameraOffsets.containsKey(locatorName + "_reverse")) {
|
||||
cameraOffsets[locatorName + "_reverse"]!!.toVector3f()
|
||||
@@ -152,7 +139,10 @@ object MountedCameraRenderer
|
||||
} else {
|
||||
Vector3f(0f, 2f, 4f)
|
||||
}
|
||||
if (thirdPersonReverse) offset.z *= -1
|
||||
|
||||
// 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
|
||||
return offset
|
||||
}
|
||||
|
||||
|
||||
+10
@@ -8,6 +8,7 @@
|
||||
|
||||
package com.cobblemon.mod.common.client.render.entity
|
||||
|
||||
import com.cobblemon.mod.common.OrientationControllable
|
||||
import com.cobblemon.mod.common.api.riding.Rideable
|
||||
import com.cobblemon.mod.common.client.MountedPokemonAnimationRenderController
|
||||
import com.cobblemon.mod.common.client.entity.PokemonClientDelegate
|
||||
@@ -30,11 +31,20 @@ object MountedLivingEntityRenderer {
|
||||
partialTicks: Float,
|
||||
i: Float
|
||||
) {
|
||||
val vehicle = entity.vehicle
|
||||
if (entity.vehicle !is Rideable) return
|
||||
val matrix = stack.last().pose()
|
||||
val delegate = pokemon.delegate as PokemonClientDelegate
|
||||
val locator = delegate.locatorStates[delegate.getSeatLocator(entity)]
|
||||
|
||||
|
||||
if (vehicle is OrientationControllable) {
|
||||
val vehicleController = vehicle.orientationController
|
||||
if (vehicleController.isActive() && vehicleController.orientation != null) {
|
||||
vehicleController.tick()
|
||||
}
|
||||
}
|
||||
|
||||
//Positions entity
|
||||
if (locator != null) {
|
||||
MountedPokemonAnimationRenderController.setup(pokemon, partialTicks)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user