Skip to content

Commit

Permalink
Advancement Rework (#145)
Browse files Browse the repository at this point in the history
* root advancement

* find outpost advancement

* adjust the defeat frostologer advancement

* fix brushing not triggering animation

* increase brushing durability damage to match armadillo brushing

* add brush polar bear advancement

* rework fur armor advancements slightly

* increase freezing for root advancement

* better advancement title

* location warmth loot condition

* warm by light advancement

* make craft fur armour a child of brush polar bear

* sun lichen enchantment

* remove warm sun lichens tag

* update translation keys for add log to campfire advancement

* custom sun lichen discharge advancement criterion

* translate fur armour item tag

* split the trim advancement in two

* frozen by frost wand advancement

* fix wrong criteria

* add trigger for frost wand freezing

* add freeze 3 creepers advancement

* slightly decrease frost spell effect range

* consistent advancement field ordering

* add more xp to advancement rewards

* reduce xp reward for freeze creepers

* unit test location warmth loot condition

* gametest weird criterion logic

* suppress the unused warning
  • Loading branch information
TheDeathlyCow authored Jan 5, 2025
1 parent b845b24 commit bf915ea
Show file tree
Hide file tree
Showing 41 changed files with 987 additions and 178 deletions.
Binary file not shown.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ dependencies {
}

testImplementation "net.fabricmc:fabric-loader-junit:${project.loader_version}"
testImplementation "org.junit.jupiter:junit-jupiter"
testImplementation "org.mockito:mockito-core:3.+"

//// Bring depends into testmod ////
Expand All @@ -95,6 +96,8 @@ dependencies {

testmodImplementation project("${it.name}:").sourceSets.testmod.output
}

testmodImplementation "org.mockito:mockito-core:3.+"
}
}

Expand Down
Empty file added logs/latest.log
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ public void onInitialize() {
FLootConditionTypes.initialize();
FFeatures.initialize();
FPlacedFeatures.initialize();
FCriteria.initialize();

ServerLivingEntityEvents.AFTER_DAMAGE.register(FrostWandRootComponent::afterDamage);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.github.thedeathlycow.frostiful.Frostiful;
import com.github.thedeathlycow.frostiful.config.FrostifulConfig;
import com.github.thedeathlycow.frostiful.registry.FBlocks;
import com.github.thedeathlycow.frostiful.registry.FCriteria;
import com.github.thedeathlycow.frostiful.registry.FSoundEvents;
import com.github.thedeathlycow.frostiful.registry.tag.FItemTags;
import com.github.thedeathlycow.thermoo.api.temperature.HeatingModes;
Expand All @@ -16,6 +17,7 @@
import net.minecraft.item.ItemPlacementContext;
import net.minecraft.item.ItemStack;
import net.minecraft.particle.ParticleTypes;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.sound.SoundCategory;
import net.minecraft.util.math.BlockPos;
Expand Down Expand Up @@ -55,10 +57,10 @@ public void onEntityCollision(BlockState state, World world, BlockPos pos, Entit

FrostifulConfig config = Frostiful.getConfig();

// only add heat if cold, but always damage
int heatToDischarge = config.freezingConfig.getSunLichenHeatPerLevel() * this.heatLevel;
// only add heatToDischarge if cold, but always damage
if (livingEntity.thermoo$isCold()) {
int heat = config.freezingConfig.getSunLichenHeatPerLevel() * this.heatLevel;
livingEntity.thermoo$addTemperature(heat, HeatingModes.ACTIVE);
livingEntity.thermoo$addTemperature(heatToDischarge, HeatingModes.ACTIVE);

// reset temperature if temp change overheated
if (livingEntity.thermoo$isWarm()) {
Expand All @@ -72,6 +74,9 @@ public void onEntityCollision(BlockState state, World world, BlockPos pos, Entit
}

entity.damage(world.getDamageSources().hotFloor(), 1);
if (livingEntity instanceof ServerPlayerEntity player) {
FCriteria.SUN_LICHEN_DISCHARGE.trigger(player, heatToDischarge);
}
createFireParticles(world, pos);

BlockState coldSunLichenState = FBlocks.COLD_SUN_LICHEN.getStateWithProperties(state);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@

import com.github.thedeathlycow.frostiful.registry.FComponents;
import com.github.thedeathlycow.frostiful.util.FLootHelper;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.passive.AnimalEntity;
import net.minecraft.entity.passive.ArmadilloEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.loot.LootTable;
import net.minecraft.registry.RegistryKey;
import net.minecraft.sound.SoundCategory;
import net.minecraft.sound.SoundEvents;
import net.minecraft.util.ActionResult;
import net.minecraft.util.Hand;
import net.minecraft.world.World;
import net.minecraft.world.event.GameEvent;

public interface FBrushable {

Expand All @@ -20,6 +28,30 @@ public interface FBrushable {

boolean frostiful$wasBrushed();

/**
* @param animal The mob that was interacted with
* @param player The player who interacted with the mob
* @param hand The hand they used to interact
* @param base Original action result for the interaction
*/
static ActionResult interact(MobEntity animal, PlayerEntity player, Hand hand, ActionResult base) {
if (base == ActionResult.FAIL) {
return ActionResult.FAIL;
}

ItemStack heldItem = player.getStackInHand(hand);
if (heldItem.isOf(Items.BRUSH) && animal instanceof FBrushable brushable && brushable.frostiful$isBrushable()) {
brushable.frostiful$brush(player, SoundCategory.PLAYERS);
animal.emitGameEvent(GameEvent.SHEAR, player);

if (!animal.getWorld().isClient) {
heldItem.damage(16, player, LivingEntity.getSlotForHand(hand));
}
}

return ActionResult.success(animal.getWorld().isClient);
}

static void brushEntity(
AnimalEntity animalEntity,
SoundCategory shearedSoundCategory,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
package com.github.thedeathlycow.frostiful.entity;

import com.github.thedeathlycow.frostiful.registry.FComponents;
import com.github.thedeathlycow.frostiful.registry.FCriteria;
import com.github.thedeathlycow.frostiful.registry.FEntityTypes;
import com.github.thedeathlycow.frostiful.registry.FSoundEvents;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityType;
import net.minecraft.entity.LivingEntity;
import net.minecraft.particle.ParticleTypes;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.sound.SoundCategory;
import net.minecraft.sound.SoundEvents;
import net.minecraft.util.math.Box;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.World;

import java.util.ArrayList;
import java.util.List;

public class FrostSpellEntity extends SpellEntity {

private static final double EFFECT_CLOUD_SIZE = 4.0;
private static final double EFFECT_CLOUD_SIZE = 3.0;

public FrostSpellEntity(World world, LivingEntity owner, Vec3d velocity) {
super(FEntityTypes.FROST_SPELL, world, owner, velocity);
Expand All @@ -41,12 +44,19 @@ protected void applyEffectCloud() {

Box box = this.getBoundingBox().expand(EFFECT_CLOUD_SIZE, EFFECT_CLOUD_SIZE, EFFECT_CLOUD_SIZE);
List<LivingEntity> targets = world.getNonSpectatingEntities(LivingEntity.class, box);
List<LivingEntity> targetsFrozen = new ArrayList<>();
for (var target : targets) {
Entity owner = this.getOwner();
if (owner == null || !target.getUuid().equals(owner.getUuid())) {
this.applySingleTargetEffect(target);
boolean isTargetable = owner == null || !target.getUuid().equals(owner.getUuid());
if (isTargetable && this.applySingleTargetEffect(target)) {
targetsFrozen.add(target);
}
}

if (!targetsFrozen.isEmpty() && this.getOwner() instanceof ServerPlayerEntity serverPlayer) {
FCriteria.FROZEN_BY_FROST_WAND.trigger(serverPlayer, targetsFrozen);
}

world.playSound(
null,
this.getX(), this.getY(), this.getZ(),
Expand All @@ -68,8 +78,7 @@ protected void applyEffectCloud() {
this.discard();
}

@Override
protected void applySingleTargetEffect(Entity target) {
protected boolean applySingleTargetEffect(Entity target) {
World world = target.getWorld();
if (!world.isClient) {
if (FComponents.FROST_WAND_ROOT_COMPONENT.get(target).tryRootFromFrostWand(this.getOwner())) {
Expand All @@ -79,7 +88,9 @@ protected void applySingleTargetEffect(Entity target) {
FSoundEvents.ENTITY_FROST_SPELL_FREEZE, SoundCategory.AMBIENT,
1.0f, 1.0f
);
return true;
}
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ protected SpellEntity(EntityType<? extends SpellEntity> entityType, World world)

protected abstract void applyEffectCloud();

protected abstract void applySingleTargetEffect(Entity target);

public void tick() {
super.tick();

Expand All @@ -59,7 +57,6 @@ public void tick() {
public void onEntityHit(EntityHitResult hitResult) {
super.onEntityHit(hitResult);
if (!getWorld().isClient && this.isAlive()) {
this.applySingleTargetEffect(hitResult.getEntity());
this.applyEffectCloud();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.github.thedeathlycow.frostiful.entity.advancement;

import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.advancement.criterion.AbstractCriterion;
import net.minecraft.entity.Entity;
import net.minecraft.entity.LivingEntity;
import net.minecraft.loot.context.LootContext;
import net.minecraft.predicate.NumberRange;
import net.minecraft.predicate.entity.EntityPredicate;
import net.minecraft.predicate.entity.LootContextPredicate;
import net.minecraft.predicate.entity.LootContextPredicateValidator;
import net.minecraft.server.network.ServerPlayerEntity;

import java.util.*;

public class FrozenByFrostWandCriterion extends AbstractCriterion<FrozenByFrostWandCriterion.Conditions> {

@Override
public Codec<Conditions> getConditionsCodec() {
return Conditions.CODEC;
}

public void trigger(ServerPlayerEntity player, Collection<LivingEntity> frozenEntities) {
List<LootContext> victimContexts = new ArrayList<>(frozenEntities.size());

for (LivingEntity frozenEntity : frozenEntities) {
victimContexts.add(EntityPredicate.createAdvancementEntityLootContext(player, frozenEntity));
}

this.trigger(player, conditions -> conditions.matches(victimContexts));
}

public record Conditions(
Optional<LootContextPredicate> player,
List<LootContextPredicate> victims,
NumberRange.IntRange entitiesFrozen
) implements AbstractCriterion.Conditions {
public static final Codec<Conditions> CODEC = RecordCodecBuilder.create(
instance -> instance.group(
EntityPredicate.LOOT_CONTEXT_PREDICATE_CODEC
.optionalFieldOf("player")
.forGetter(Conditions::player),
EntityPredicate.LOOT_CONTEXT_PREDICATE_CODEC
.listOf()
.optionalFieldOf("victims", List.of())
.forGetter(Conditions::victims),
NumberRange.IntRange.CODEC
.optionalFieldOf("entities_frozen", NumberRange.IntRange.ANY)
.forGetter(Conditions::entitiesFrozen)
)
.apply(instance, Conditions::new)
);

/**
*
* Implementation: Finds the first victim that matches each predicate. If the predicate matches no victims,
* returns false. Otherwise, checks the entities frozen count
*
* @param victims victims frozen
* @return returns true
*/
public boolean matches(Collection<LootContext> victims) {
if (!this.victims.isEmpty()) {
List<LootContext> unmatchedVictims = new ArrayList<>(victims);

for (LootContextPredicate predicate : this.victims) {
boolean matched = false;

Iterator<LootContext> iterator = unmatchedVictims.iterator();
while (iterator.hasNext()) {
LootContext lootContext = iterator.next();
if (predicate.test(lootContext)) {
iterator.remove();
matched = true;
break;
}
}

if (!matched) {
return false;
}
}
}
return this.entitiesFrozen.test(victims.size());
}

@Override
public void validate(LootContextPredicateValidator validator) {
AbstractCriterion.Conditions.super.validate(validator);
validator.validateEntityPredicates(this.victims, ".victims");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.github.thedeathlycow.frostiful.entity.advancement;

import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.advancement.criterion.AbstractCriterion;
import net.minecraft.predicate.NumberRange;
import net.minecraft.predicate.entity.EntityPredicate;
import net.minecraft.predicate.entity.LootContextPredicate;
import net.minecraft.server.network.ServerPlayerEntity;

import java.util.Optional;

public class SunLichenDischargeCriterion extends AbstractCriterion<SunLichenDischargeCriterion.Conditions> {

public void trigger(ServerPlayerEntity player, int temperatureImparted) {
this.trigger(player, conditions -> conditions.temperatureImparted.test(temperatureImparted));
}

@Override
public Codec<Conditions> getConditionsCodec() {
return Conditions.CODEC;
}

public record Conditions(
Optional<LootContextPredicate> player,
NumberRange.IntRange temperatureImparted
) implements AbstractCriterion.Conditions {
public static final Codec<Conditions> CODEC = RecordCodecBuilder.create(
instance -> instance.group(
EntityPredicate.LOOT_CONTEXT_PREDICATE_CODEC
.optionalFieldOf("player")
.forGetter(Conditions::player),
NumberRange.IntRange.CODEC
.fieldOf("temperature_imparted")
.orElse(NumberRange.IntRange.ANY)
.forGetter(Conditions::temperatureImparted)
).apply(instance, Conditions::new)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.github.thedeathlycow.frostiful.entity.loot;

import com.github.thedeathlycow.frostiful.registry.FLootConditionTypes;
import com.github.thedeathlycow.thermoo.api.predicate.TemperatureLootCondition;
import com.github.thedeathlycow.thermoo.api.temperature.EnvironmentController;
import com.github.thedeathlycow.thermoo.api.temperature.EnvironmentManager;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.loot.condition.LocationCheckLootCondition;
import net.minecraft.loot.condition.LootCondition;
import net.minecraft.loot.condition.LootConditionType;
import net.minecraft.loot.context.LootContext;
import net.minecraft.loot.context.LootContextParameters;
import net.minecraft.predicate.NumberRange;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;

import java.util.Objects;

public record LocationWarmthLootCondition(
NumberRange.IntRange value
) implements LootCondition {

public static final MapCodec<LocationWarmthLootCondition> CODEC = RecordCodecBuilder.mapCodec(
instance -> instance.group(
NumberRange.IntRange.CODEC
.fieldOf("value")
.orElse(NumberRange.IntRange.ANY)
.forGetter(LocationWarmthLootCondition::value)
).apply(instance, LocationWarmthLootCondition::new)
);

@Override
public LootConditionType getType() {
return FLootConditionTypes.LOCATION_WARMTH;
}

@Override
public boolean test(LootContext lootContext) {
World world = lootContext.getWorld();
BlockPos pos = BlockPos.ofFloored(Objects.requireNonNull(lootContext.get(LootContextParameters.ORIGIN)));

int areaWarmth = EnvironmentManager.INSTANCE.getController().getHeatAtLocation(world, pos);
return this.value.test(areaWarmth);
}

public static LootCondition.Builder builder(NumberRange.IntRange value) {
return () -> new LocationWarmthLootCondition(value);
}
}
Loading

0 comments on commit bf915ea

Please sign in to comment.