Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package fr.hugman.mubble.world.power_up;

import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import fr.hugman.mubble.world.power_up.PowerUpProperties.ChargeCounting;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;

/**
* How many charges a power-up hands out, and how spent ones come back.
*
* @param counting how spent charges come back
* @param max how many charges the power-up holds at once
* @param interval the tick count the counting runs on, when it needs one
*/
public record PowerUpCharges(ChargeCounting counting, int max, int interval) {
/**
* One charge per entity currently out, with no limit on how many that can be.
*/
public static final PowerUpCharges DEFAULT = fromActiveEntities(Integer.MAX_VALUE);

public static final Codec<PowerUpCharges> CODEC = RecordCodecBuilder.create(instance -> instance.group(
ChargeCounting.CODEC.fieldOf("counting").forGetter(PowerUpCharges::counting),
Codec.INT.optionalFieldOf("max", Integer.MAX_VALUE).forGetter(PowerUpCharges::max),
Codec.INT.optionalFieldOf("interval", 0).forGetter(PowerUpCharges::interval)
).apply(instance, PowerUpCharges::new));

public static final StreamCodec<RegistryFriendlyByteBuf, PowerUpCharges> STREAM_CODEC = StreamCodec.composite(
ChargeCounting.STREAM_CODEC, PowerUpCharges::counting,
ByteBufCodecs.INT, PowerUpCharges::max,
ByteBufCodecs.INT, PowerUpCharges::interval,
PowerUpCharges::new
);

/**
* Charges that are never counted, so the power-up can always be used.
*/
public static PowerUpCharges none() {
return new PowerUpCharges(ChargeCounting.NONE, Integer.MAX_VALUE, 0);
}

/**
* One charge per entity the power-up currently has out; a charge comes back once its entity is gone.
*/
public static PowerUpCharges fromActiveEntities(int max) {
return new PowerUpCharges(ChargeCounting.FROM_ACTIVE_ENTITIES, max, 0);
}

/**
* Charges that never come back, so the power-up runs out for good.
*/
public static PowerUpCharges onlyDecrease(int max) {
return new PowerUpCharges(ChargeCounting.ONLY_DECREASE, max, 0);
}

/**
* One charge back {@code cooldown} ticks after the last use.
*/
public static PowerUpCharges cooldownRecharge(int max, int cooldown) {
return new PowerUpCharges(ChargeCounting.COOLDOWN_RECHARGE, max, cooldown);
}

/**
* One charge back every {@code interval} ticks, for as long as charges are missing.
*/
public static PowerUpCharges timedRecharge(int max, int interval) {
return new PowerUpCharges(ChargeCounting.TIMED_RECHARGE, max, interval);
}

/**
* All {@code max} charges at once, {@code window} ticks after the use that opened the window.
* <p>
* The power-up is used in bursts: a first use opens a window of {@code window} ticks in which up to
* {@code max} uses fit, then everything comes back and the next use opens a fresh window.
*/
public static PowerUpCharges burst(int max, int window) {
return new PowerUpCharges(ChargeCounting.BURST_RECHARGE, max, window);
}

public PowerUpProperties createProperties() {
return new PowerUpProperties(this.counting, this.max, this.interval);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public final class PowerUpProperties {

public ChargeCounting chargeCounting;
public int maxCharges;
public int interval;

private int cooldown;
private int chargeCount;
Expand All @@ -31,6 +32,7 @@ public final class PowerUpProperties {
public static final Codec<PowerUpProperties> CODEC = RecordCodecBuilder.create(instance -> instance.group(
ChargeCounting.CODEC.fieldOf("charge_counting").forGetter(p -> p.chargeCounting),
Codec.INT.fieldOf("max_charges").forGetter(p -> p.maxCharges),
Codec.INT.optionalFieldOf("interval", 0).forGetter(p -> p.interval),
Codec.INT.fieldOf("cooldown").forGetter(p -> p.cooldown),
Codec.INT.fieldOf("charge_count").forGetter(p -> p.chargeCount),
Codec.list(UUIDUtil.CODEC).fieldOf("charge_entities").forGetter(p -> p.chargeEntities)
Expand All @@ -39,6 +41,7 @@ public final class PowerUpProperties {
public static final StreamCodec<RegistryFriendlyByteBuf, PowerUpProperties> STREAM_CODEC = StreamCodec.composite(
ChargeCounting.STREAM_CODEC, p -> p.chargeCounting,
ByteBufCodecs.INT, p -> p.maxCharges,
ByteBufCodecs.INT, p -> p.interval,
ByteBufCodecs.INT, p -> p.cooldown,
ByteBufCodecs.INT, p -> p.chargeCount,
UUIDUtil.STREAM_CODEC.apply(ByteBufCodecs.list()), p -> p.chargeEntities,
Expand All @@ -50,32 +53,52 @@ public PowerUpProperties(
ChargeCounting chargeCounting,
int maxCharges
) {
this(chargeCounting, maxCharges, 0, maxCharges, new ArrayList<>());
this(chargeCounting, maxCharges, 0, 0, maxCharges, new ArrayList<>());
}

public PowerUpProperties(
ChargeCounting chargeCounting,
int maxCharges,
int interval
) {
this(chargeCounting, maxCharges, interval, 0, maxCharges, new ArrayList<>());
}

public PowerUpProperties(
ChargeCounting chargeCounting,
int maxCharges,
int interval,
int cooldown,
int chargeCount,
List<UUID> chargeEntities
) {
this.chargeCounting = chargeCounting;
this.maxCharges = maxCharges;
this.interval = interval;
this.cooldown = cooldown;
this.chargeCount = chargeCount;
this.chargeEntities = new ArrayList<>(chargeEntities);
}

public void setCooldown(int cooldown) {
this.cooldown = cooldown;
this.dirty = true;
private void setCooldown(int cooldown) {
if (this.cooldown != cooldown) {
this.cooldown = cooldown;
this.dirty = true;
}
}

public int getChargeCount() {
return this.chargeCount;
}

private void setChargeCount(int chargeCount) {
int clamped = Math.clamp(chargeCount, 0, this.maxCharges);
if (this.chargeCount != clamped) {
this.chargeCount = clamped;
this.dirty = true;
}
}

public boolean checkDirty() {
if (this.dirty) {
this.dirty = false;
Expand All @@ -88,22 +111,54 @@ public boolean isAtMax() {
return this.chargeCount >= this.maxCharges && this.cooldown == 0;
}

public void addEntity(UUID uuid) {
/**
* Spends one charge.
*/
public void useCharge() {
// A cooldown recharge pushes the next charge a full interval away on every use, while a burst window
// only opens on the first use: the ones that follow run out the window that is already going.
boolean startsCountdown = this.chargeCounting == ChargeCounting.COOLDOWN_RECHARGE
|| (this.chargeCounting == ChargeCounting.BURST_RECHARGE && this.cooldown <= 0);
if (startsCountdown && this.interval > 0) {
this.setCooldown(this.interval);
}
this.setChargeCount(this.chargeCount - 1);
}

/**
* Ties a charge to the lifetime of an entity. Only has an effect in {@link ChargeCounting#FROM_ACTIVE_ENTITIES},
* where the charge comes back once the entity is gone.
*/
public void trackEntity(UUID uuid) {
if (this.chargeCounting != ChargeCounting.FROM_ACTIVE_ENTITIES) {
return;
}
this.chargeEntities.add(uuid);
this.chargeCount--;
this.dirty = true;
}

public void tick() {
if (this.chargeCounting == ChargeCounting.FROM_ACTIVE_ENTITIES) {
this.chargeCount = this.maxCharges - this.chargeEntities.size();
this.setChargeCount(this.maxCharges - this.chargeEntities.size());
}
// A timed recharge always keeps a countdown running while charges are missing.
if (this.chargeCounting == ChargeCounting.TIMED_RECHARGE && this.interval > 0 && this.cooldown <= 0 && this.chargeCount < this.maxCharges) {
this.cooldown = this.interval;
}
if (this.cooldown > 0) {
this.cooldown--;
if (this.cooldown == 0 && this.chargeCounting == ChargeCounting.COOLDOWN_RECHARGE) {
this.chargeCount++;
if (this.cooldown == 0) {
// The cooldown hitting zero is observable through isAtMax(), so it has to be synced.
// Only that transition is: syncing every single tick of the countdown would be pure spam.
this.dirty = true;
if (this.chargeCounting == ChargeCounting.COOLDOWN_RECHARGE || this.chargeCounting == ChargeCounting.TIMED_RECHARGE) {
this.setChargeCount(this.chargeCount + 1);
}
// A burst gives every charge back at once, so the next use starts from a full window.
if (this.chargeCounting == ChargeCounting.BURST_RECHARGE) {
this.setChargeCount(this.maxCharges);
}
}
this.dirty = true;
}
}

Expand Down Expand Up @@ -133,7 +188,9 @@ public enum ChargeCounting implements StringRepresentable {
NONE(0, "none"),
FROM_ACTIVE_ENTITIES(1, "from_active_entities"), // based on the number of active entities tied to the power-up trigger
ONLY_DECREASE(2, "only_decrease"), // never increases once the power-up is triggered
COOLDOWN_RECHARGE(3, "cooldown_recharge"); // charges up once the cooldown is over
COOLDOWN_RECHARGE(3, "cooldown_recharge"), // charges up once the cooldown is over
TIMED_RECHARGE(4, "timed_recharge"), // charges up one at a time at a fixed interval
BURST_RECHARGE(5, "burst_recharge"); // charges up all at once, once the window opened by the first use is over

public static final IntFunction<ChargeCounting> BY_ID = ByIdMap.continuous(ChargeCounting::ordinal, values(), ByIdMap.OutOfBoundsStrategy.WRAP);
public static final Codec<ChargeCounting> CODEC = StringRepresentable.fromEnum(ChargeCounting::values);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.Optional;
import java.util.function.Consumer;

import fr.hugman.mubble.world.power_up.PowerUpCharges;
import fr.hugman.mubble.world.power_up.PowerUpProperties;
import net.minecraft.ChatFormatting;
import net.minecraft.core.Holder;
Expand All @@ -31,30 +32,26 @@
import net.minecraft.world.item.component.TooltipProvider;
import net.minecraft.world.phys.Vec3;

//TODO: cooldown is not yet implemented
public record ShootProjectilePowerUpAction(
EntityType<?> projectile,
Holder<SoundEvent> sound,
Optional<Holder<SoundEvent>> sound,
float speed,
Optional<Integer> maxProjectiles,
Optional<Integer> cooldown
PowerUpCharges charges
//TODO: add shooting algorithm
//TODO: add projectile NBT
) implements PowerUpAction, TooltipProvider {
public static final MapCodec<ShootProjectilePowerUpAction> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
BuiltInRegistries.ENTITY_TYPE.byNameCodec().fieldOf("projectile").forGetter(ShootProjectilePowerUpAction::projectile),
SoundEvent.CODEC.fieldOf("sound").forGetter(ShootProjectilePowerUpAction::sound),
SoundEvent.CODEC.optionalFieldOf("sound").forGetter(ShootProjectilePowerUpAction::sound),
Codec.FLOAT.optionalFieldOf("speed", 1.5F).forGetter(ShootProjectilePowerUpAction::speed),
Codec.INT.optionalFieldOf("max_projectiles").forGetter(ShootProjectilePowerUpAction::maxProjectiles),
Codec.INT.optionalFieldOf("cooldown").forGetter(ShootProjectilePowerUpAction::cooldown)
PowerUpCharges.CODEC.optionalFieldOf("charges", PowerUpCharges.DEFAULT).forGetter(ShootProjectilePowerUpAction::charges)
).apply(instance, ShootProjectilePowerUpAction::new));

public static final StreamCodec<RegistryFriendlyByteBuf, ShootProjectilePowerUpAction> STREAM_CODEC = StreamCodec.composite(
ByteBufCodecs.registry(Registries.ENTITY_TYPE), (ShootProjectilePowerUpAction::projectile),
SoundEvent.STREAM_CODEC, (ShootProjectilePowerUpAction::sound),
ByteBufCodecs.optional(SoundEvent.STREAM_CODEC), (ShootProjectilePowerUpAction::sound),
ByteBufCodecs.FLOAT, (ShootProjectilePowerUpAction::speed),
ByteBufCodecs.optional(ByteBufCodecs.INT), (ShootProjectilePowerUpAction::maxProjectiles),
ByteBufCodecs.optional(ByteBufCodecs.INT), (ShootProjectilePowerUpAction::cooldown),
PowerUpCharges.STREAM_CODEC, (ShootProjectilePowerUpAction::charges),
ShootProjectilePowerUpAction::new
);

Expand All @@ -70,7 +67,7 @@ public boolean canBeRefilled() {

@Override
public PowerUpProperties setUpProperties() {
return new PowerUpProperties(PowerUpProperties.ChargeCounting.FROM_ACTIVE_ENTITIES, maxProjectiles.orElse(Integer.MAX_VALUE));
return this.charges.createProperties();
}

@Override
Expand Down Expand Up @@ -103,19 +100,21 @@ public InteractionResult trigger(Player player) {
return InteractionResult.SUCCESS;
}
else {
level.playSound(null, player.getX(), player.getY(), player.getZ(), this.sound, SoundSource.NEUTRAL, 0.5F, 1.0F);
this.sound.ifPresent(s -> level.playSound(null, player.getX(), player.getY(), player.getZ(), s, SoundSource.NEUTRAL, 0.5F, 1.0F));
var entity = this.projectile.create(level, EntitySpawnReason.TRIGGERED);
if (null == entity) {
return InteractionResult.FAIL;
}
if (entity instanceof Projectile projectileEntity) {
projectileEntity.setOwner(player);
}
entity.setPos(player.getX(), player.getEyeY() - 0.1F, player.getZ());
// setPos places the bottom of the bounding box, so the projectile has to be lowered by half its
// height to actually come out centered on the eye line.
entity.setPos(player.getX(), player.getEyeY() - 0.1F - entity.getBbHeight() / 2.0F, player.getZ());
setVelocity(entity, player, player.getXRot(), player.getYRot(), 0.0F, this.speed, 1.0F);
level.addFreshEntity(entity);
properties.addEntity(entity.getUUID());
properties.setCooldown(cooldown.orElse(0));
properties.useCharge();
properties.trackEntity(entity.getUUID());
}
return InteractionResult.SUCCESS;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package fr.hugman.mubble.super_mario.client.mixin;

import fr.hugman.mubble.super_mario.world.entity.projectile.Bubble;
import net.minecraft.client.renderer.entity.HumanoidMobRenderer;
import net.minecraft.world.entity.LivingEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;

@Mixin(HumanoidMobRenderer.class)
public class HumanoidMobRendererMixin {
/**
* {@link net.minecraft.client.model.HumanoidModel} folds a humanoid's legs into a sitting pose as soon as it
* rides anything. An entity floating inside a bubble should keep standing.
*/
@Redirect(
method = "extractHumanoidRenderState(Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/client/renderer/entity/state/HumanoidRenderState;FLnet/minecraft/client/renderer/item/ItemModelResolver;)V",
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/LivingEntity;isPassenger()Z")
)
private static boolean super_mario$standUpInsideBubbles(LivingEntity entity) {
return entity.isPassenger() && !(entity.getVehicle() instanceof Bubble);
}
}
Loading
Loading