Compare commits

...
Author SHA1 Message Date
Nadja Reitzenstein 4716b8e4fd Gamerule test build 2025-07-24 11:48:39 +02:00
Nadja Reitzenstein 1ef62a5a73 Set Pokéballs to the current consensus behaviour 2025-07-23 17:07:12 +02:00
Nadja Reitzenstein 89f1f53664 Remove test-wise vibration detection code
Partially reverts commit fe0c6af51c1d5f8134126d68e0289903e0daf2c1.

# Conflicts:
#	common/src/main/kotlin/com/cobblemon/mod/common/entity/pokemon/PokemonEntity.kt
2025-07-23 17:07:12 +02:00
Nadja Reitzenstein 62a7e1498b Design Meeting stuff pt.2 2025-07-23 17:07:12 +02:00
Nadja Reitzenstein 89b913fab7 Design Meeting stuff 2025-07-23 17:07:12 +02:00
Nadja Reitzenstein d40317d9e0 Make build 2025-07-23 17:07:12 +02:00
Nadja von Reitzenstein Čerpnjak 91395461c8 Polish bouncy balls a bit more 2025-07-23 17:07:12 +02:00
Nadja Reitzenstein 505fb14b2a Update bouncy Pokéballs
This fixes a math error with the inaccuracy calculation, and adds a (preliminary) particle-based indicator about the inaccuracy of the throw.

This commit also removes the bouncy gamerule since current consensus is that the more accurate movement tracking is here to stay.
Bouncy balls can still be essentially removed by setting the max bounces gamerule to 0
2025-07-23 17:07:12 +02:00
Nadja Reitzenstein a8f2d15cea Bouncy Bouncy Balls 2025-07-23 17:07:11 +02:00
17 changed files with 495 additions and 121 deletions
@@ -34,9 +34,7 @@ public class AbstractClientPlayerMixin {
}
LocalPlayer player = (LocalPlayer) (Object) this;
if (player != null && player.isPassenger() && player.getVehicle() instanceof PokemonEntity) {
PokemonEntity ride = (PokemonEntity) player.getVehicle();
if (player != null && player.isPassenger() && player.getVehicle() instanceof PokemonEntity ride) {
//Return custom fov mult
cir.setReturnValue(ride.rideFovMult());
}
@@ -0,0 +1,21 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.invoker;
import net.minecraft.world.level.GameRules;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Invoker;
@Mixin(GameRules.IntegerValue.class)
public interface IntegerRuleInvoker {
@Invoker("create")
static GameRules.Type<GameRules.IntegerValue> cobblemon$create(int defaultValue) {
throw new UnsupportedOperationException();
}
}
@@ -0,0 +1,22 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.pokeball;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.projectile.Projectile;
import net.minecraft.world.level.Level;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(Projectile.class)
public abstract class ProjectileMixin extends Entity {
public ProjectileMixin(EntityType<? extends Projectile> type, Level world) {
super(type, world);
}
}
@@ -0,0 +1,38 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.pokeball;
import com.cobblemon.mod.common.entity.pokeball.EmptyPokeballMarker;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.projectile.ThrowableProjectile;
import net.minecraft.world.level.Level;
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(ThrowableProjectile.class)
public abstract class ThrowableProjectileMixin extends ProjectileMixin {
public ThrowableProjectileMixin(EntityType<? extends ThrowableProjectile> type, Level world) {
super(type, world);
}
@Inject(method = "tick", at = @At("HEAD"), cancellable = true)
private void cobblemon$tickPokeballs(CallbackInfo ci) {
/// [ThrowableProjectile.tick] does a lot of things we actively do *not* want it to do. However its
/// superclasses do a lot of stuff in tick() that we *need* them to do. Ergo we inject this tiny mixin which
/// will check in essence `this instanceof EmptyPokeballEntity` and then call the minimal super.tick() && return
/// marker.isBouncy() is required since we may not have bouncy balls all the time and if we don't then this
/// mixin will break stuff.
if (this instanceof EmptyPokeballMarker) {
super.tick();
ci.cancel();
}
}
}
@@ -1,35 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.pokeball;
import com.cobblemon.mod.common.entity.pokeball.WaterDragModifier;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.projectile.ThrowableProjectile;
import net.minecraft.world.level.Level;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
@Mixin(ThrowableProjectile.class)
public abstract class ThrownEntityMixin extends Entity {
public ThrownEntityMixin(EntityType<?> type, Level world) {
super(type, world);
}
@ModifyVariable(method = "tick", at = @At("STORE"), ordinal = 0)
private float cobblemon$waterDragModifier(float value) {
if (this.isInWater() && this instanceof WaterDragModifier) {
return ((WaterDragModifier) this).waterDrag();
}
return value;
}
}
@@ -47,7 +47,8 @@ object CobblemonEntities : PlatformRegistry<Registry<EntityType<*>>, ResourceKey
@JvmField
val EMPTY_POKEBALL: EntityType<EmptyPokeBallEntity> = this.create(
EMPTY_POKEBALL_KEY.path,
EntityType.Builder.of({ _, world -> EmptyPokeBallEntity(PokeBalls.POKE_BALL, world) }, MobCategory.MISC)
EntityType.Builder.of(::EmptyPokeBallEntity, MobCategory.MISC)
.sized(EmptyPokeBallEntity.DIMENSIONS.width, EmptyPokeBallEntity.DIMENSIONS.height)
.build(EMPTY_POKEBALL_KEY.toString())
)
@@ -170,7 +170,7 @@ object PokeBalls : JsonDataRegistry<PokeBall> {
createDefault("moon_ball", CatchRateModifiers.MOON_PHASES)
createDefault("sport_ball", MultiplierModifier(1.5F))
createDefault("net_ball", CatchRateModifiers.typeBoosting(3F, ElementalTypes.BUG, ElementalTypes.WATER))
createDefault("dive_ball", CatchRateModifiers.SUBMERGED_IN_WATER, waterDragValue = 0.99F)
createDefault("dive_ball", CatchRateModifiers.SUBMERGED_IN_WATER, waterDragValue = 0.99)
createDefault("nest_ball", CatchRateModifiers.NEST)
createDefault("repeat_ball", CatchRateModifiers.REPEAT)
createDefault("timer_ball", CatchRateModifiers.turnBased { turn -> (1F * turn * (1229F / 4096F)).coerceAtMost(4F) })
@@ -192,12 +192,12 @@ object PokeBalls : JsonDataRegistry<PokeBall> {
createDefault("ancient_ivory_ball", ancient = true)
createDefault("ancient_great_ball", MultiplierModifier(1.5F), ancient = true)
createDefault("ancient_ultra_ball", MultiplierModifier(2F), ancient = true)
createDefault("ancient_heavy_ball", throwPower = 0.75f, ancient = true)
createDefault("ancient_leaden_ball", throwPower = 0.75f, ancient = true)
createDefault("ancient_gigaton_ball", throwPower = 0.75f, ancient = true)
createDefault("ancient_feather_ball", throwPower = 2.5f, ancient = true)
createDefault("ancient_wing_ball", throwPower = 2.5f, ancient = true)
createDefault("ancient_jet_ball", throwPower = 2.5f, ancient = true)
createDefault("ancient_heavy_ball", throwPower = 0.75f, ancient = true, waterDragValue = 0.9)
createDefault("ancient_leaden_ball", throwPower = 0.75f, ancient = true, waterDragValue = 0.9)
createDefault("ancient_gigaton_ball", throwPower = 0.75f, ancient = true, waterDragValue = 0.9)
createDefault("ancient_feather_ball", throwPower = 2.5f, ancient = true, waterDragValue = 0.7)
createDefault("ancient_wing_ball", throwPower = 2.5f, ancient = true, waterDragValue = 0.7)
createDefault("ancient_jet_ball", throwPower = 2.5f, ancient = true, waterDragValue = 0.7)
createDefault("ancient_origin_ball", GuaranteedModifier(), ancient = true)
// Luxury ball effect, low priority as it must be triggered before soothe bell as of gen 4
CobblemonEvents.FRIENDSHIP_UPDATED.subscribe(priority = Priority.LOW) { event ->
@@ -233,15 +233,17 @@ object PokeBalls : JsonDataRegistry<PokeBall> {
name: String,
modifier: CatchRateModifier = MultiplierModifier(1F) { _, _ -> true },
effects: List<CaptureEffect> = emptyList(),
waterDragValue: Float = 0.8F,
waterDragValue: Double = 0.8,
model2d: ResourceLocation = cobblemonResource(name),
model3d: ResourceLocation = cobblemonResource("item/${name}_model"),
throwPower: Float = 1.25f,
ancient: Boolean = false
ancient: Boolean = false,
maxBounces: Int = 1,
bouncyness: Double = 0.4,
): PokeBall {
val identifier = cobblemonResource(name)
//val finalModifiers = if (appendUltraBeastPenalty) modifiers + listOf(LabelModifier(0.1F, true, CobblemonPokemonLabels.ULTRA_BEAST)) else modifiers
val pokeball = PokeBall(identifier, modifier, effects, waterDragValue, model2d, model3d, throwPower, ancient)
val pokeball = PokeBall(identifier, modifier, effects, waterDragValue, model2d, model3d, throwPower, ancient, maxBounces, bouncyness)
this.defaults[identifier] = pokeball
return pokeball
}
@@ -67,14 +67,18 @@ import net.minecraft.world.entity.*
import net.minecraft.world.entity.decoration.LeashFenceKnotEntity
import net.minecraft.world.entity.projectile.ThrowableItemProjectile
import net.minecraft.world.item.Item
import net.minecraft.world.level.ClipContext
import net.minecraft.world.level.ClipContext.Block
import net.minecraft.world.level.ClipContext.Fluid
import net.minecraft.world.level.Level
import net.minecraft.world.phys.BlockHitResult
import net.minecraft.world.phys.EntityHitResult
import net.minecraft.world.phys.HitResult
import net.minecraft.world.phys.Vec3
import net.minecraft.world.phys.*
import org.joml.Quaterniond
import org.joml.Vector3d
import java.util.concurrent.CompletableFuture
import java.util.function.Predicate
import kotlin.math.*
class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragModifier, Schedulable {
class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, EmptyPokeballMarker, Schedulable {
enum class CaptureState {
NOT,
HIT,
@@ -91,11 +95,84 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
val HIT_VELOCITY = SynchedEntityData.defineId(EmptyPokeBallEntity::class.java, Vec3DataSerializer)
val SHAKE = SynchedEntityData.defineId(EmptyPokeBallEntity::class.java, EntityDataSerializers.BOOLEAN)
val ASPECTS = SynchedEntityData.defineId(EmptyPokeBallEntity::class.java, StringSetDataSerializer)
val BOUNCES = SynchedEntityData.defineId(EmptyPokeBallEntity::class.java, EntityDataSerializers.INT)
// GAMERULE CONFIGURABLE
val MAX_BOUNCES = SynchedEntityData.defineId(EmptyPokeBallEntity::class.java, EntityDataSerializers.INT)
val BOUNCYNESS = SynchedEntityData.defineId(EmptyPokeBallEntity::class.java, EntityDataSerializers.INT)
val BOUNCE_DRAG = SynchedEntityData.defineId(EmptyPokeBallEntity::class.java, EntityDataSerializers.INT)
val WATER_SKIP_FACTOR = SynchedEntityData.defineId(EmptyPokeBallEntity::class.java, EntityDataSerializers.INT)
// END GAMERULE CONFIGURABLE
const val SECONDS_BETWEEN_SHAKES = 1.25F
const val SECONDS_BEFORE_SHAKE = 1F
val DIMENSIONS = EntityDimensions.fixed(0.4F, 0.4F)
/**
* Calculate an aim vector with a given miss effect.
*
* @param missMagnitudeRad How far away from the aimed at point the output vector is, in radians. A value of 0 will result in a perfectly accurate throw, a value of 2π will result in a throw going exactly backwards.
* @param missAngleRad The point on the circle spanned by [missMagnitudeRad] which the resulting miss vector will point at, given in radian. Since the miss vector needs to be rotated which direction a given number points at from the perspective of the player depends on their orientation
*/
fun getAimVector(playerAimX: Double, playerAimY: Double, playerAimZ: Double, missMagnitudeRad: Double, missAngleRad: Double): Vec3 {
// We want to sample a uniform vector out of the cone of potential misses. We can't just sample a rotation
// angle from [0, 360°] and a radius from [0, $inaccuracy] since at smaller radii the points would be much
// closer to each other and thus much more frequent. The mathematical trick we use comes from the fact that
// for a sphere all slices of equal height have an equal surface area. So we instead sample a value for a
// slice height and a rotation angle on the resulting circle, giving us uniformly distributed outputs.
// This means given a maximum miss angle θ, i.e. the maximum angle the adjusted vector will have from the
// player's aim we first calculate the possible range for our height: [cos(θ), 1]
// Given this range we then sample two values, z from [cos(θ), 1] and φ from [0, 2π] and calculate an offset
// vector given those values.
// Finally that offset vector is rotated to use the player aim as unit axis instead of [0, 0, 1], giving us
// our final miss vector to output.
val Z = cos(missMagnitudeRad)
val sqrtZ = sqrt(1.0 - Z.pow(2))
val miss = Vector3d(
sqrtZ * cos(missAngleRad),
sqrtZ * sin(missAngleRad),
Z
)
val rotAxis = Vector3d(0.0, 0.0, 1.0)
val rotAngle = acos(rotAxis.dot(playerAimX, playerAimY, playerAimZ))
rotAxis.cross(playerAimX, playerAimY, playerAimZ)
return Quaterniond()
.fromAxisAngleRad(rotAxis, rotAngle)
.transform(miss)
.let { Vec3(it.x, it.y, it.z) }
}
fun getEntityHitResult(
level: Level,
projectile: Entity,
startVec: Vec3,
endVec: Vec3,
boundingBox: AABB,
filter: Predicate<Entity>,
inflationAmount: Double
): EntityHitResult? {
var entity: Pair<Entity, Vec3>? = null
level.getEntities(projectile, boundingBox, filter).forEach {
val aABB = it.boundingBox.inflate(inflationAmount)
val optional = aABB.clip(startVec, endVec)
if (optional.isPresent) {
val vec = optional.orElseThrow()
val distanceToEntity = startVec.distanceToSqr(vec)
if (distanceToEntity < Double.MAX_VALUE) {
entity = it to vec
return@forEach
}
}
}
return entity?.let { (entity, location) -> EntityHitResult(entity, location) }
}
}
val dataTrackerEmitter = SimpleObservable<EntityDataAccessor<*>>()
@@ -109,6 +186,24 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
var aspects: Set<String>
get() = entityData.get(ASPECTS)
set(value) { entityData.set(ASPECTS, value) }
var bounces: Int
get() = entityData.get(BOUNCES)
set(value) { entityData.set(BOUNCES, value) }
// GAMERULE CONFIGURABLE
var maxBounces: Int
get() = entityData.get(MAX_BOUNCES)
set(value) { entityData.set(MAX_BOUNCES, value) }
var bouncynessPercent: Int
get() = entityData.get(BOUNCYNESS)
set(value) { entityData.set(BOUNCYNESS, value) }
var bounceDragPercent: Int
get() = entityData.get(BOUNCE_DRAG)
set(value) { entityData.set(BOUNCE_DRAG, value) }
var waterSkipFactor: Int
get() = entityData.get(WATER_SKIP_FACTOR)
set(value) { entityData.set(WATER_SKIP_FACTOR, value) }
// END GAMERULE CONFIGURABLE
override val delegate = if (level().isClientSide) {
EmptyPokeBallClientDelegate()
@@ -129,11 +224,20 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
builder.define(HIT_TARGET_POSITION, Vec3.ZERO)
builder.define(HIT_VELOCITY, Vec3.ZERO)
builder.define(SHAKE, false)
builder.define(BOUNCES, 0)
// GAMERULE CONFIGURABLE
builder.define(MAX_BOUNCES, 0)
builder.define(BOUNCYNESS, 0)
builder.define(BOUNCE_DRAG, 0)
builder.define(WATER_SKIP_FACTOR, 0)
// END GAMERULE CONFIGURABLE
}
override fun onSyncedDataUpdated(data: EntityDataAccessor<*>) {
super.onSyncedDataUpdated(data)
// See what I said about this in PokemonEntity
@Suppress("SENSELESS_COMPARISON")
if (delegate != null) {
delegate.onSyncedDataUpdated(data)
}
@@ -155,7 +259,7 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
addPosableFunctions(struct)
}
constructor(world: Level) : this(pokeBall = PokeBalls.POKE_BALL, world = world)
constructor(entityType: EntityType<out EmptyPokeBallEntity> = EMPTY_POKEBALL, world: Level) : this(pokeBall = PokeBalls.POKE_BALL, world = world, entityType = entityType)
constructor(
pokeBall: PokeBall,
world: Level,
@@ -175,20 +279,174 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
var pokeBall: PokeBall = PokeBalls.POKE_BALL
//fun getMaxBounces(): Int = pokeBall.maxBounces
override fun isPushedByFluid(): Boolean = false
override fun getMovementToShoot(x: Double, y: Double, z: Double, velocity: Float, inaccuracy: Float): Vec3? {
// We multiply the inaccuracy value by 0.02 to have rough equivalence to vanilla, meaning an inaccuracy of
// `1.0f` passed to getMovementToShoot will give you about as much of an inaccurate throw as a 1.0f would for a
// bow or snowball.
val missMagnitudeRad: Double = inaccuracy * 0.02 * this.random.nextDouble()
val missAngleRad: Double = this.random.nextDouble() * 2 * Math.PI
// Also make velocity inaccurate by a linear factor. This will mean it becomes much more impactful on weak throws.
val adjustedVelocity = velocity + 0.02 * (this.random.nextDouble() - this.random.nextDouble())
return getAimVector(x, y, z, missMagnitudeRad, missAngleRad).scale(adjustedVelocity)
}
fun moveWithCollision() {
/* ==============================
* Gamerule configurables
* ============================== */
// The game rule bouncyWaterSkipAngle sets the default angle below which balls will skip.
// Setting it to 90 will make all balls (minus dive ball) skip all the time, setting it to 0 will make no balls skip ever
val waterSkipDegree = if (pokeBall.waterDragValue >= 0.99) {
// 1 means never skip
1.0
} else {
// 1 = cos(0), 0 = cos(90)
cos(waterSkipFactor * (kotlin.math.PI / 180.0))
}
// bounce elasticity. Default is 60 which means a ball will bounce back with 60% of the impact momentum
val bouncyness = bouncynessPercent / 100.0
// Water skip elasticity. Things that skip well retain more of their vertical movement when skipping
// The default ball has a water drag of 0.8, an extremely skippy ball has one of 0.6. This mans that a normal
// ball applies 80% of the elasticity reduction on water bounces while a skippy ball applies 60%
val water_bouncyness = 1.0 - (pokeBall.waterDragValue * bouncyness)
// Ground bounce drag. Set to 0 to lose no speed on bounce other than air friction, set to 100 to lose all speed immediately.
val bounce_drag = 1.0 - (bounceDragPercent / 100.0)
// Water skip drag. Skippy balls apply 30% of the configured bounce drag on skip, normal ones 40%
val water_bounce_drag = 1.0 - (pokeBall.waterDragValue * bounceDragPercent / 200.0)
/* ==============================
* END Gamerule configurables END
* ============================== */
// Account for floating point inaccuracies, always.
val FLOAT_MARGIN = 0.01
val MIN_MOVEMENT = 0.1
val entityFilter = this::canHitEntity
val entityHitMargin = 0.3
val deltaMovementSqr = this.deltaMovement.lengthSqr()
// The entire movement budget we have this tick (done as squared because it's cheaper)
var remainingMovementSqr = this.deltaMovement.lengthSqr()
var cumulativeBounceDrag = Vec3(1.0, 1.0, 1.0)
var isInWaterSubtick = false
while (remainingMovementSqr > FLOAT_MARGIN) {
/* This while loop checks ball movement in segments:
* On the first run through moveRatio will be 1.0 and all isBounce* will be false, so movement will be
* this.deltaMovement. That movement is then checked for collisions. If no, we're immediately done here as hitResult.location will be set to the end point we wanted to go anyway.
* If yes we perform the bounce:
* - Set the isBounce* variables which are tracking which direction the ball should en up flying *after the whole tick*.
* - Reduce the cummulative drag (value: [0.0; 1.0]) depending on the nature of the bounce.
* Lastly, set our new position to the position after a bounce and reduce our movement budget according to the current segment
* If we have movement budget left (yes in case of a bounce, no in case of a miss), do more segmented collision checks.
*/
val moveRatio = remainingMovementSqr / deltaMovementSqr
val movement = this.deltaMovement
.multiply(cumulativeBounceDrag)
.scale(moveRatio)
val fluidFilter = if (this.isInWater || isInWaterSubtick) Fluid.NONE else Fluid.WATER
val hitResult = this.level().clip(ClipContext(this.position(), this.position().add(movement), Block.COLLIDER, fluidFilter, this))
val bounceSound = if (hitResult.type != HitResult.Type.MISS) {
val hitBlock = this.level().getBlockState(hitResult.blockPos)
// Normal vector of the surface we bounce off
val normal = hitResult.direction.normal
if (!hitBlock.fluidState.isEmpty) {
// We calculate the bounce angle using normalised vectors, so this will be a value
// between 0 (ball is moving parallel to the water) and 1 (ball is hitting the water straight on)
val bounceAngle = abs(movement.normalize().dot(Vec3(normal.x.toDouble(), normal.y.toDouble(), normal.z.toDouble())))
// The theory is that a ball that has no water drag (waterDragValue = 1.0) moves through water like
// air so it physically can't skip. A ball that moves very slow in water skips much easier.
// `pow(3)` serves as a good curve since it keeps the [0; 1] output range but most drag values will
// be much closer to 1 so should be easier to skip (e.g. 0.8^3 = 0.512 so about 45° or sharper angles will skip)
val targetAngle = 1.0 - waterSkipDegree
// At a flat enough angle (we're comparing against the normal so > is correct) balls bounce off of water.
// This does not happen if the ball has no remaining bounces so that a ball will never break on the
// water surface but instead submerge
if (bounceAngle < targetAngle && deltaMovementSqr > 0.4 && bounces < maxBounces) {
bounces++
cumulativeBounceDrag = cumulativeBounceDrag
.multiply(
if (normal.x != 0) { -water_bouncyness } else { water_bounce_drag },
if (normal.y != 0) { -water_bouncyness } else { water_bounce_drag },
if (normal.z != 0) { -water_bouncyness } else { water_bounce_drag },
)
} else {
isInWaterSubtick = true
}
} else {
bounces++
cumulativeBounceDrag = cumulativeBounceDrag
// The effect vector a bounce with this normal has. Note that *negative* bouncyness is taken here; if we
// bounce off e.g. the ground we were going down before (y is negative) but are going *up* after (y is positive),
// although with less upward speed than we had downward speed (bouncyness is less than 1.0)
.multiply(
if (normal.x != 0) { -bouncyness } else { bounce_drag },
if (normal.y != 0) { -bouncyness } else { bounce_drag },
if (normal.z != 0) { -bouncyness } else { bounce_drag },
)
}
if (captureState == CaptureState.NOT) {
hitBlock.soundType.hitSound
} else {
null
}
} else {
null
}
val entityHitResult = getEntityHitResult(this.level(), this, this.position(), hitResult.location, this.boundingBox.expandTowards(this.deltaMovement).inflate(1.0), entityFilter, entityHitMargin)
// The actual hit that happened
val effectiveHitResult = entityHitResult ?: hitResult
remainingMovementSqr = if (effectiveHitResult.type != HitResult.Type.MISS) {
remainingMovementSqr - this.position().distanceToSqr(effectiveHitResult.location)
} else {
0.0
}
val speedEstimateAfterBounce = deltaMovementSqr * cumulativeBounceDrag.lengthSqr()
if (bounces > maxBounces || effectiveHitResult.type == HitResult.Type.ENTITY || speedEstimateAfterBounce < MIN_MOVEMENT) {
val deflection = this.hitTargetOrDeflectSelf(effectiveHitResult)
val entity = (effectiveHitResult as? EntityHitResult)?.entity
deflection.deflect(this, entity, this.random)
} else {
bounceSound?.let { playSound(it, 0.5f, 1f) }
}
this.setPos(effectiveHitResult.location)
}
this.deltaMovement = this.deltaMovement.multiply(cumulativeBounceDrag)
}
override fun onHitBlock(hitResult: BlockHitResult) {
if (captureState == CaptureState.NOT) {
if (level().isServerSide()) {
super.onHitBlock(hitResult)
level().sendParticlesServer(ParticleTypes.CLOUD, hitResult.location, 2, hitResult.location.subtract(position()).normalize().scale(-0.1), 0.0)
level().playSoundServer(position(), SoundEvents.WOOD_PLACE, pitch = 2.5F)
discard()
val player = this.owner as? ServerPlayer
if (player?.isCreative == false) {
spawnAtLocation(defaultItem)
}
playSound(SoundEvents.WOOD_BREAK, 0.5f, 2.5f)
return drop()
}
} else {
setNoGravity(false)
isNoGravity = false
deltaMovement = Vec3.ZERO
}
}
@@ -279,9 +537,9 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
private fun drop() {
val owner = owner
discard()
val player = owner?.takeIf { it is ServerPlayer } as? ServerPlayer
if (player?.isCreative != true) spawnAtLocation(defaultItem)
return
if (owner is ServerPlayer && !owner.hasInfiniteMaterials()) {
spawnAtLocation(defaultItem)?.deltaMovement = deltaMovement
}
}
// Poké Balls don't save to the world.
@@ -289,13 +547,28 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
override fun tick() {
super.tick()
moveWithCollision()
// Do the things that [ThrowableProjectile.tick] does that we want but skipped because of our mixin
checkInsideBlocks()
updateRotation()
// We want water behaviour to be special so we don't do the this.isInWater() check the same way
val drag = if (this.isInWater) {
pokeBall.waterDragValue
} else {
0.99
}
this.deltaMovement = this.deltaMovement.scale(drag)
this.applyGravity()
delegate.tick(this)
if (level().isServerSide()) {
if (this.tickCount > 600 && this.capturingPokemon == null) {
this.remove(RemovalReason.DISCARDED)
drop()
}
if (owner == null || !owner!!.isAlive || (captureState != CaptureState.NOT && capturingPokemon?.isAlive != true)) {
@@ -549,6 +822,4 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
)
) as Packet<ClientGamePacketListener>
override fun waterDrag(): Float = this.pokeBall.waterDragValue
}
@@ -0,0 +1,14 @@
/*
* 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.entity.pokeball
/**
* Marker interface so our mixin can special case ThrowableProjectile.tick for the thrown Pokéball entity.
*/
interface EmptyPokeballMarker
@@ -1,23 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.entity.pokeball
/**
* A Cobblemon extension to make it possible to add water drag values to our Pokeballs without relying on the PersistentProjectileEntity implementation.
*/
interface WaterDragModifier {
/**
* Calculates and returns the value of the water drag.
*
* @return The value of the water drag on the entity.
*/
fun waterDrag(): Float
}
@@ -442,6 +442,7 @@ open class PokemonEntity(
override fun onSyncedDataUpdated(data: EntityDataAccessor<*>) {
super.onSyncedDataUpdated(data)
// "But it's imposs-" shut up nerd, it happens during super construction and that's before delegate is assigned by class construction
@Suppress("SENSELESS_COMPARISON")
if (delegate != null) {
delegate.onSyncedDataUpdated(data)
}
@@ -2402,7 +2403,7 @@ open class PokemonEntity(
/**
* If the Pokémon is following another Pokémon, checks the herd size using the leader. Otherwise check this Pokémon's
* herd count. Not necessarily strictly up to date but it should be good enough for typical purposes.
* herd count. Not necessarily strictly up to datreturn super.getListenableEvents()e but it should be good enough for typical purposes.
*/
fun getHerdSize(): Int {
val world = level() as? ServerLevel ?: return 0
@@ -10,17 +10,32 @@ package com.cobblemon.mod.common.item
import com.cobblemon.mod.common.api.pokeball.PokeBalls
import com.cobblemon.mod.common.entity.pokeball.EmptyPokeBallEntity
import com.cobblemon.mod.common.entity.pokeball.EmptyPokeBallEntity.Companion.BOUNCES
import com.cobblemon.mod.common.pokeball.PokeBall
import com.cobblemon.mod.common.util.isServerSide
import com.cobblemon.mod.common.util.math.geometry.toRadians
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
import com.cobblemon.mod.common.util.random
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules
import net.minecraft.client.multiplayer.ClientLevel
import net.minecraft.core.component.DataComponents
import net.minecraft.core.particles.DustParticleOptions
import net.minecraft.server.level.ServerPlayer
import net.minecraft.sounds.SoundEvents
import net.minecraft.stats.Stats
import net.minecraft.world.InteractionHand
import net.minecraft.world.InteractionResultHolder
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Rarity
import net.minecraft.world.item.UseAnim
import net.minecraft.world.level.Level
import net.minecraft.world.phys.Vec3
import org.joml.Vector3f
import kotlin.math.cos
import kotlin.math.max
import kotlin.math.min
import kotlin.text.toDouble
class PokeBallItem(
val pokeBall: PokeBall
@@ -36,14 +51,48 @@ class PokeBallItem(
override fun use(world: Level, player: Player, usedHand: InteractionHand): InteractionResultHolder<ItemStack> {
val itemStack = player.getItemInHand(usedHand)
if (world.isServerSide()) {
throwPokeBall(world, player as ServerPlayer)
}
itemStack.consume(1, player)
return InteractionResultHolder.sidedSuccess(itemStack, world.isClientSide)
player.startUsingItem(usedHand)
return InteractionResultHolder.consume(itemStack)
}
private fun throwPokeBall(world: Level, player: ServerPlayer) {
override fun releaseUsing(stack: ItemStack, level: Level, player: LivingEntity, remainingUseDuration: Int) {
if (player is ServerPlayer) {
val ticksHeld = getUseDuration(stack, player) - remainingUseDuration
val force = getPowerForTime(ticksHeld)
if (force > 0.1) {
val inaccuracy = getInaccuracyForTime(ticksHeld)
throwPokeBall(level, player, force, inaccuracy)
player.cooldowns.addCooldown(this, 10)
stack.consume(1, player)
player.awardStat(Stats.ITEM_USED.get(this));
}
} else {
super.releaseUsing(stack, level, player, remainingUseDuration)
}
}
fun getPowerForTime(charge: Int): Float {
var f = charge.toFloat() / 20.0f
f = (f * f + f * 2.0f) / 3.0f
if (f > 1.0f) {
f = 1.0f
}
return f
}
// Calculate the max inaccuracy (in radians over the entire screen space) for the given charge time.
fun getInaccuracyForTime(charge: Int): Float = 1.00f
// How long the item can be used for before resetting. Not the time it takes to fully charge up
override fun getUseDuration(stack: ItemStack, entity: LivingEntity): Int = 72000
override fun getUseAnimation(stack: ItemStack): UseAnim? = UseAnim.SPEAR
private fun throwPokeBall(world: Level, player: ServerPlayer, force: Float, inaccuracy: Float) {
val pokeBallEntity = EmptyPokeBallEntity(pokeBall, player.level(), player).apply {
val overhandFactor: Float = if (player.xRot < 0) {
5f * cos(player.xRot.toRadians())
@@ -51,8 +100,16 @@ class PokeBallItem(
5f
}
shootFromRotation(player, player.xRot - overhandFactor, player.yRot, 0.0f, pokeBall.throwPower, 1.0f)
setPos(position().add(deltaMovement.normalize().scale(1.0)))
// Query gamerules
if (world.isServerSide()) {
this.bouncynessPercent = world.gameRules.getInt(CobblemonGameRules.BOUNCY_BOUNCYNESS)
this.waterSkipFactor = world.gameRules.getInt(CobblemonGameRules.BOUNCY_WATER_SKIP_FACTOR)
this.maxBounces = world.gameRules.getInt(CobblemonGameRules.BOUNCY_BOUNCE_COUNT)
this.bounceDragPercent = world.gameRules.getInt(CobblemonGameRules.BOUNCY_BOUNCE_DRAG)
}
shootFromRotation(player, player.xRot - overhandFactor, player.yRot, 0.0f, pokeBall.throwPower * force, inaccuracy)
setPos(position().add(deltaMovement.normalize()))
owner = player
}
world.addFreshEntity(pokeBallEntity)
@@ -36,17 +36,21 @@ open class PokeBall(
val name: ResourceLocation,
val catchRateModifier: CatchRateModifier = CatchRateModifier.DUMMY,
val effects: List<CaptureEffect> = listOf(),
val waterDragValue: Float,
val waterDragValue: Double,
val model2d: ResourceLocation,
val model3d: ResourceLocation,
val throwPower: Float,
val ancient: Boolean
val ancient: Boolean,
val maxBounces: Int,
val bouncyness: Double,
) {
val struct = QueryStruct(hashMapOf())
.addFunction("name") { StringValue(name.toString()) }
.addFunction("water_drag_value") { DoubleValue(waterDragValue) }
.addFunction("throw_power") { DoubleValue(throwPower) }
.addFunction("is_ancient") { DoubleValue(ancient) }
.addFunction("max_bounces") { DoubleValue(maxBounces) }
.addFunction("bouncyness") { DoubleValue(bouncyness) }
// .addFunction("item") { } // requires a registry which is hard
// This gets attached during item registry
@@ -21,6 +21,7 @@ import org.joml.Vector3f
* @property values the values of the matrix, in the form of a two-dimensional immutable array
* @author landonjw
*/
@ConsistentCopyVisibility
data class TransformationMatrix internal constructor(val values: ImmutableArray<ImmutableArray<Float>>) {
operator fun get(row: Int): ImmutableArray<Float> = values[row]
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.world.gamerules
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.mixin.invoker.BooleanRuleInvoker
import com.cobblemon.mod.common.mixin.invoker.IntegerRuleInvoker
import net.minecraft.world.level.GameRules
object CobblemonGameRules {
@@ -25,4 +26,12 @@ object CobblemonGameRules {
@JvmField
val SHINY_STARTERS: GameRules.Key<GameRules.BooleanValue> = Cobblemon.implementation.registerGameRule("doShinyStarters", GameRules.Category.MISC, BooleanRuleInvoker.`cobblemon$create`(false))
@JvmField
val BOUNCY_BOUNCYNESS: GameRules.Key<GameRules.IntegerValue> = Cobblemon.implementation.registerGameRule("bouncyElasticity", GameRules.Category.MISC, IntegerRuleInvoker.`cobblemon$create`(60))
@JvmField
val BOUNCY_WATER_SKIP_FACTOR: GameRules.Key<GameRules.IntegerValue> = Cobblemon.implementation.registerGameRule("bouncyWaterSkipAngle", GameRules.Category.MISC, IntegerRuleInvoker.`cobblemon$create`(40))
@JvmField
val BOUNCY_BOUNCE_COUNT: GameRules.Key<GameRules.IntegerValue> = Cobblemon.implementation.registerGameRule("bouncyBounceCount", GameRules.Category.MISC, IntegerRuleInvoker.`cobblemon$create`(0))
@JvmField
val BOUNCY_BOUNCE_DRAG: GameRules.Key<GameRules.IntegerValue> = Cobblemon.implementation.registerGameRule("bouncyBounceDrag", GameRules.Category.MISC, IntegerRuleInvoker.`cobblemon$create`(40))
}
@@ -91,9 +91,11 @@
"invoker.BooleanRuleInvoker",
"invoker.DoorBlockInvoker",
"invoker.FireBlockInvoker",
"invoker.IntegerRuleInvoker",
"invoker.PressurePlateBlockInvoker",
"invoker.StairsBlockInvoker",
"invoker.TrapdoorBlockInvoker",
"pokeball.ThrownEntityMixin"
"pokeball.ProjectileMixin",
"pokeball.ThrowableProjectileMixin"
]
}
@@ -30,21 +30,12 @@ public class GenericPacketSplitterMixin {
@Inject(method = "encode(Lio/netty/channel/ChannelHandlerContext;Lnet/minecraft/network/protocol/Packet;Ljava/util/List;)V", at=@At("HEAD"), cancellable = true)
public void encode(ChannelHandlerContext channelHandlerContext, Packet packet, List<Object> list, CallbackInfo ci) {
if (packet instanceof ClientboundCustomPayloadPacket) {
ClientboundCustomPayloadPacket pac = (ClientboundCustomPayloadPacket) packet;
if (((ClientboundCustomPayloadPacket) packet).payload() instanceof UnsplittablePacket) {
list.add(packet);
ci.cancel();
return;
}
}
if (packet instanceof ServerboundCustomPayloadPacket) {
ServerboundCustomPayloadPacket pac = (ServerboundCustomPayloadPacket) packet;
if (((ServerboundCustomPayloadPacket) packet).payload() instanceof UnsplittablePacket) {
list.add(packet);
ci.cancel();
return;
}
if (packet instanceof ClientboundCustomPayloadPacket(UnsplittablePacket payload )) {
list.add(packet);
ci.cancel();
} else if (packet instanceof ServerboundCustomPayloadPacket(UnsplittablePacket payload)) {
list.add(packet);
ci.cancel();
}
}
}