Initial commit: Forge 1.20.1 Ellie companion mod

- EllieEntity with GeckoLib animations, sleep AI, pathfinding with crouching
- Dialog system with conditions and effects
- Relationship system with milestones
- OpenDoor and bed occupation pathfinding
- 15 animations: idle1/2/3, sleep, walkingsimple, shiftwalking/shiftidle, etc.
This commit is contained in:
2026-06-09 21:18:04 +03:00
commit f5d318f02e
48 changed files with 21932 additions and 0 deletions
@@ -0,0 +1,85 @@
package me.sashegdev.fabled_hearts;
import com.mojang.logging.LogUtils;
import me.sashegdev.fabled_hearts.dialog.DialogLoader;
import me.sashegdev.fabled_hearts.dialog.DialogManager;
import me.sashegdev.fabled_hearts.entity.ellie.EllieEntity;
import me.sashegdev.fabled_hearts.entity.ellie.EllieRenderer;
import me.sashegdev.fabled_hearts.network.ModNetworking;
import me.sashegdev.fabled_hearts.registry.ModEntities;
import me.sashegdev.fabled_hearts.registry.ModItems;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.EntityRenderersEvent;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.AddReloadListenerEvent;
import net.minecraftforge.event.entity.EntityAttributeCreationEvent;
import net.minecraftforge.event.level.BlockEvent;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import org.slf4j.Logger;
import software.bernie.geckolib.GeckoLib;
@Mod(Main.MODID)
public class Main {
public static final String MODID = "fabled_hearts";
public static final Logger LOGGER = LogUtils.getLogger();
public Main() {
IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus();
GeckoLib.initialize();
new DialogManager();
ModEntities.register(modEventBus);
ModItems.register(modEventBus);
modEventBus.addListener(this::commonSetup);
modEventBus.register(CommonHandler.class);
modEventBus.register(ClientHandler.class);
MinecraftForge.EVENT_BUS.register(this);
MinecraftForge.EVENT_BUS.register(new ForgeHandler());
}
private void commonSetup(FMLCommonSetupEvent event) {
event.enqueueWork(ModNetworking::register);
}
@Mod.EventBusSubscriber(modid = MODID, bus = Mod.EventBusSubscriber.Bus.MOD)
public static class CommonHandler {
@SubscribeEvent
public static void registerAttributes(EntityAttributeCreationEvent event) {
event.put(ModEntities.ELLIE.get(), EllieEntity.createAttributes().build());
}
}
@Mod.EventBusSubscriber(modid = MODID, bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT)
public static class ClientHandler {
@SubscribeEvent
public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) {
event.registerEntityRenderer(ModEntities.ELLIE.get(), EllieRenderer::new);
}
}
public static class ForgeHandler {
@SubscribeEvent
public void onAddReloadListeners(AddReloadListenerEvent event) {
event.addListener(new DialogLoader());
}
@SubscribeEvent
public void onBlockBreak(BlockEvent.BreakEvent event) {
if (!event.getLevel().isClientSide()) {
var pos = event.getPos();
event.getLevel().getEntitiesOfClass(
EllieEntity.class,
new net.minecraft.world.phys.AABB(pos).inflate(2),
e -> e.isSleeping() && pos.equals(e.getBedPos())
).forEach(EllieEntity::wakeUp);
}
}
}
}
@@ -0,0 +1,17 @@
package me.sashegdev.fabled_hearts.ai;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.goal.*;
import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal;
import net.minecraft.world.entity.player.Player;
public class BasicAI {
public static void addBasicGoals(PathfinderMob mob, GoalSelector selector, int start) {
int p = start;
selector.addGoal(p++, new FloatGoal(mob));
selector.addGoal(p++, new LookAtPlayerGoal(mob, Player.class, 8.0f));
selector.addGoal(p++, new RandomLookAroundGoal(mob));
selector.addGoal(p++, new WaterAvoidingRandomStrollGoal(mob, 0.8));
selector.addGoal(p++, new OpenDoorGoal(mob, true));
}
}
@@ -0,0 +1,143 @@
package me.sashegdev.fabled_hearts.ai;
import me.sashegdev.fabled_hearts.entity.ellie.EllieEntity;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.level.block.DoorBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec3;
import java.util.EnumSet;
public class EllieSleepGoal extends Goal {
private final EllieEntity ellie;
private final int searchRadius;
private BlockPos bedPos;
private int sleepTimer;
private boolean claimed;
private static final int MAX_SLEEP_TICKS = 6000;
private static final double REACH_DIST_SQR = 1.8 * 1.8;
public EllieSleepGoal(EllieEntity ellie, int searchRadius) {
this.ellie = ellie;
this.searchRadius = searchRadius;
this.setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
@Override
public boolean canUse() {
if (!ellie.level().isNight() && !ellie.isTired()) return false;
if (ellie.isSleeping()) return false;
if (ellie.hurtTime > 0) return false;
bedPos = findNearestBed();
return bedPos != null;
}
@Override
public void start() {
claimed = false;
if (bedPos != null) {
if (ellie.isSpaceLow(bedPos)) {
ellie.setPose(net.minecraft.world.entity.Pose.CROUCHING);
}
ellie.getNavigation().moveTo(bedPos.getX() + 0.5, bedPos.getY(), bedPos.getZ() + 0.5, 0.6);
}
}
@Override
public void tick() {
if (bedPos == null) return;
// Open doors in path
tryOpenDoor();
double distSqr = ellie.distanceToSqr(Vec3.atCenterOf(bedPos));
if (distSqr < REACH_DIST_SQR) {
ellie.getNavigation().stop();
if (!claimed) {
claimed = ellie.occupyBed(bedPos);
}
if (claimed) {
if (!ellie.isSleeping()) {
ellie.setBedSleepPos(bedPos);
}
sleepTimer++;
}
} else {
if (ellie.getNavigation().isDone()) {
ellie.getNavigation().moveTo(bedPos.getX() + 0.5, bedPos.getY(), bedPos.getZ() + 0.5, 0.6);
}
}
}
private void tryOpenDoor() {
BlockPos inFront = ellie.blockPosition().relative(ellie.getDirection());
for (int i = 0; i < 3; i++) {
BlockPos checkPos = ellie.blockPosition().relative(ellie.getDirection(), i);
BlockState state = ellie.level().getBlockState(checkPos);
if (state.getBlock() instanceof DoorBlock) {
if (!state.getValue(DoorBlock.OPEN)) {
ellie.level().setBlock(checkPos, state.setValue(DoorBlock.OPEN, true), 3);
}
return;
}
}
}
@Override
public void stop() {
if (ellie.isSleeping()) {
ellie.wakeUp();
}
sleepTimer = 0;
bedPos = null;
claimed = false;
}
@Override
public boolean canContinueToUse() {
if (ellie.hurtTime > 0) return false;
if (ellie.isSleeping()) {
if (sleepTimer >= MAX_SLEEP_TICKS) return false;
if (bedPos != null && !ellie.level().getBlockState(bedPos).isBed(ellie.level(), bedPos, null)) {
return false;
}
return true;
}
if (!ellie.level().isNight() && !ellie.isTired()) return false;
return true;
}
private BlockPos findNearestBed() {
BlockPos entityPos = ellie.blockPosition();
BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();
BlockPos nearest = null;
double nearestDist = Double.MAX_VALUE;
for (int x = -searchRadius; x <= searchRadius; x++) {
for (int z = -searchRadius; z <= searchRadius; z++) {
for (int y = -2; y <= 2; y++) {
mutable.set(entityPos.getX() + x, entityPos.getY() + y, entityPos.getZ() + z);
if (isFreeBed(mutable)) {
double dist = entityPos.distSqr(mutable);
if (dist < nearestDist) {
nearestDist = dist;
nearest = mutable.immutable();
}
}
}
}
}
return nearest;
}
private boolean isFreeBed(BlockPos pos) {
BlockState state = ellie.level().getBlockState(pos);
if (!state.isBed(ellie.level(), pos, null)) return false;
if (state.hasProperty(DoorBlock.OPEN) && state.getValue(DoorBlock.OPEN)) return false;
return !state.getValue(net.minecraft.world.level.block.BedBlock.OCCUPIED);
}
}
@@ -0,0 +1,60 @@
package me.sashegdev.fabled_hearts.ai;
import me.sashegdev.fabled_hearts.entity.ellie.EllieEntity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.ai.goal.Goal;
import java.util.EnumSet;
public class IdleAnimationGoal extends Goal {
private final EllieEntity ellie;
private int cooldown;
private int animLength;
public IdleAnimationGoal(EllieEntity ellie) {
this.ellie = ellie;
this.setFlags(EnumSet.of(Flag.LOOK));
}
@Override
public boolean canUse() {
if (cooldown > 0) {
cooldown--;
return false;
}
if (ellie.isSleeping()) return false;
if (ellie.isUnderLowCeiling()) return false;
if (ellie.isMoving()) return false;
if (ellie.getRandom().nextInt(200) != 0) return false;
return true;
}
@Override
public void start() {
int animId = ellie.getRandom().nextInt(3) + 1;
animLength = switch (animId) {
case 1 -> 141;
case 2 -> 231;
case 3 -> 164;
default -> 100;
};
ellie.triggerRandomIdle(animId);
ellie.getNavigation().stop();
}
@Override
public void tick() {
animLength--;
}
@Override
public void stop() {
ellie.clearRandomIdle();
cooldown = 100;
}
@Override
public boolean canContinueToUse() {
return animLength > 0 && !ellie.isMoving() && !ellie.isUnderLowCeiling();
}
}
@@ -0,0 +1,8 @@
package me.sashegdev.fabled_hearts.api.dialog;
import me.sashegdev.fabled_hearts.entity.ellie.EllieEntity;
import net.minecraft.world.entity.player.Player;
public interface IDialogCondition {
boolean test(EllieEntity ellie, Player player);
}
@@ -0,0 +1,8 @@
package me.sashegdev.fabled_hearts.api.dialog;
import me.sashegdev.fabled_hearts.entity.ellie.EllieEntity;
import net.minecraft.world.entity.player.Player;
public interface IDialogEffect {
void apply(EllieEntity ellie, Player player);
}
@@ -0,0 +1,14 @@
package me.sashegdev.fabled_hearts.api.girl;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.EntityType;
import software.bernie.geckolib.model.GeoModel;
public interface IGirlType {
String getName();
EntityType<?> getEntityType();
ResourceLocation getModelLocation();
ResourceLocation getTextureLocation();
ResourceLocation getAnimationLocation();
GeoModel<?> createModel();
}
@@ -0,0 +1,14 @@
package me.sashegdev.fabled_hearts.api.girl;
import me.sashegdev.fabled_hearts.entity.ellie.EllieEntity;
import net.minecraft.world.entity.player.Player;
import java.util.List;
public interface INSFWAction {
String getId();
String getAnimationName();
boolean canPerform(EllieEntity ellie, Player player);
void perform(EllieEntity ellie, Player player);
List<INSFWAction> getFollowUps();
}
@@ -0,0 +1,27 @@
package me.sashegdev.fabled_hearts.api.girl;
public interface IRelationship {
float getPoints();
float getMaxPoints();
void addPoints(float amount);
Milestone getCurrentMilestone();
enum Milestone {
STRANGER(0),
ACQUAINTANCE(50),
FRIEND(75),
LOVE(100);
final float threshold;
Milestone(float t) { this.threshold = t; }
public float getThreshold() { return threshold; }
public static Milestone fromPoints(float points) {
Milestone result = STRANGER;
for (var m : values()) {
if (points >= m.threshold) result = m;
}
return result;
}
}
}
@@ -0,0 +1,35 @@
package me.sashegdev.fabled_hearts.data;
import net.minecraft.world.entity.player.Player;
public enum RelationshipMilestones {
STRANGER(0, "idleins"),
ACQUAINTANCE(50, "idle_happy"),
FRIEND(75, "idle_flirty"),
LOVE(100, "idle_intimate");
private final float threshold;
private final String dialogAnimation;
RelationshipMilestones(float threshold, String dialogAnimation) {
this.threshold = threshold;
this.dialogAnimation = dialogAnimation;
}
public float getThreshold() { return threshold; }
public String getDialogAnimation() { return dialogAnimation; }
public static RelationshipMilestones getMilestone(float relationship) {
RelationshipMilestones current = STRANGER;
for (var milestone : values()) {
if (relationship >= milestone.threshold) {
current = milestone;
}
}
return current;
}
public static boolean hasReached(float relationship, RelationshipMilestones milestone) {
return relationship >= milestone.threshold;
}
}
@@ -0,0 +1,56 @@
package me.sashegdev.fabled_hearts.data;
import me.sashegdev.fabled_hearts.Main;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.saveddata.SavedData;
import org.jetbrains.annotations.Nullable;
import java.util.UUID;
public class WorldData extends SavedData {
private static final String DATA_NAME = Main.MODID + "_world_data";
private UUID ellieUUID;
private int ellieId;
public WorldData() {}
public static WorldData get(ServerLevel level) {
return level.getDataStorage().computeIfAbsent(WorldData::load, WorldData::new, DATA_NAME);
}
public static WorldData load(CompoundTag tag) {
WorldData data = new WorldData();
if (tag.hasUUID("EllieUUID")) {
data.ellieUUID = tag.getUUID("EllieUUID");
}
data.ellieId = tag.getInt("EllieId");
return data;
}
@Override
public CompoundTag save(CompoundTag tag) {
if (ellieUUID != null) {
tag.putUUID("EllieUUID", ellieUUID);
}
tag.putInt("EllieId", ellieId);
return tag;
}
public boolean hasEllie() { return ellieUUID != null; }
@Nullable
public UUID getEllieUUID() { return ellieUUID; }
public void setEllieUUID(UUID uuid) {
this.ellieUUID = uuid;
setDirty();
}
public void removeEllie() {
this.ellieUUID = null;
setDirty();
}
}
@@ -0,0 +1,125 @@
package me.sashegdev.fabled_hearts.dialog;
import com.google.gson.*;
import me.sashegdev.fabled_hearts.Main;
import me.sashegdev.fabled_hearts.api.dialog.IDialogCondition;
import me.sashegdev.fabled_hearts.api.dialog.IDialogEffect;
import me.sashegdev.fabled_hearts.dialog.conditions.*;
import me.sashegdev.fabled_hearts.dialog.effects.*;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.server.packs.resources.SimplePreparableReloadListener;
import net.minecraft.util.profiling.ProfilerFiller;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.*;
import java.util.stream.Collectors;
public class DialogLoader extends SimplePreparableReloadListener<Map<String, DialogNode>> {
private static final Gson GSON = new GsonBuilder().create();
private static final Map<String, IDialogCondition> CONDITION_REGISTRY = new HashMap<>();
private static final Map<String, IDialogEffect> EFFECT_REGISTRY = new HashMap<>();
private static DialogLoader INSTANCE;
private Map<String, DialogNode> dialogues = new HashMap<>();
public DialogLoader() {
INSTANCE = this;
}
public static DialogLoader get() { return INSTANCE; }
public static void registerCondition(String id, IDialogCondition condition) {
CONDITION_REGISTRY.put(id, condition);
}
public static void registerEffect(String id, IDialogEffect effect) {
EFFECT_REGISTRY.put(id, effect);
}
static {
registerCondition("relationship_min", new RelationshipCondition.Min());
registerCondition("relationship_max", new RelationshipCondition.Max());
registerCondition("is_tired", new FatigueCondition());
registerCondition("is_night", new TimeCondition());
registerCondition("first_dialog", new FirstDialogCondition());
registerEffect("add_relationship", new AddRelationshipEffect());
}
@Override
protected Map<String, DialogNode> prepare(ResourceManager manager, ProfilerFiller profiler) {
Map<String, DialogNode> result = new HashMap<>();
try {
var resourceOpt = manager.getResource(new ResourceLocation(Main.MODID, "dialogues/dialogues.json"));
if (resourceOpt.isEmpty()) return result;
try (var reader = new BufferedReader(new InputStreamReader(resourceOpt.get().open()))) {
JsonObject json = GSON.fromJson(reader, JsonObject.class);
JsonArray nodes = json.getAsJsonArray("dialogues");
for (var elem : nodes) {
DialogNode node = parseNode(elem.getAsJsonObject());
result.put(node.getId(), node);
}
}
} catch (Exception e) {
Main.LOGGER.error("Failed to load dialogues", e);
}
return result;
}
@Override
protected void apply(Map<String, DialogNode> loaded, ResourceManager manager, ProfilerFiller profiler) {
this.dialogues = loaded;
Main.LOGGER.info("Loaded {} dialogues", loaded.size());
}
private DialogNode parseNode(JsonObject obj) {
String id = obj.get("id").getAsString();
String text = obj.get("text").getAsString();
String animation = obj.has("animation") ? obj.get("animation").getAsString() : "idleins";
List<DialogNode.DialogChoice> choices = new ArrayList<>();
if (obj.has("choices")) {
JsonArray choicesArr = obj.getAsJsonArray("choices");
for (var choiceElem : choicesArr) {
choices.add(parseChoice(choiceElem.getAsJsonObject()));
}
}
return new DialogNode(id, text, animation, choices);
}
private DialogNode.DialogChoice parseChoice(JsonObject obj) {
String text = obj.get("text").getAsString();
String next = obj.get("next").getAsString();
List<IDialogCondition> conditions = new ArrayList<>();
if (obj.has("conditions")) {
for (var cond : obj.getAsJsonArray("conditions")) {
JsonObject condObj = cond.getAsJsonObject();
String type = condObj.get("type").getAsString();
IDialogCondition condition = CONDITION_REGISTRY.get(type);
if (condition instanceof IConditionWithJson configurable) {
configurable.configure(condObj);
}
if (condition != null) conditions.add(condition);
}
}
List<IDialogEffect> effects = new ArrayList<>();
if (obj.has("effects")) {
for (var eff : obj.getAsJsonArray("effects")) {
JsonObject effObj = eff.getAsJsonObject();
String type = effObj.get("type").getAsString();
IDialogEffect effect = EFFECT_REGISTRY.get(type);
if (effect instanceof IEffectWithJson configurable) {
configurable.configure(effObj);
}
if (effect != null) effects.add(effect);
}
}
return new DialogNode.DialogChoice(text, next, conditions, effects);
}
public DialogNode getNode(String id) { return dialogues.get(id); }
public Collection<DialogNode> getAllNodes() { return dialogues.values(); }
}
@@ -0,0 +1,85 @@
package me.sashegdev.fabled_hearts.dialog;
import me.sashegdev.fabled_hearts.api.dialog.IDialogEffect;
import me.sashegdev.fabled_hearts.entity.ellie.EllieEntity;
import me.sashegdev.fabled_hearts.network.DialogNodePacket;
import me.sashegdev.fabled_hearts.network.ModNetworking;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.network.PacketDistributor;
import java.util.*;
public class DialogManager {
private static DialogManager INSTANCE;
private final Map<UUID, Map<Integer, String>> playerDialogState = new HashMap<>();
public DialogManager() {
INSTANCE = this;
}
public static DialogManager get() { return INSTANCE; }
public void startDialog(EllieEntity ellie, Player player) {
String startNode = getStartNode(ellie, player);
if (startNode == null) return;
playerDialogState.computeIfAbsent(player.getUUID(), k -> new HashMap<>())
.put(ellie.getId(), startNode);
sendNode(ellie, player, startNode);
}
public void makeChoice(EllieEntity ellie, Player player, int choiceIndex) {
var state = playerDialogState.get(player.getUUID());
if (state == null) return;
String currentId = state.get(ellie.getId());
if (currentId == null) return;
DialogNode node = DialogLoader.get().getNode(currentId);
if (node == null || choiceIndex < 0 || choiceIndex >= node.getChoices().size()) return;
var choice = node.getChoices().get(choiceIndex);
for (var condition : choice.getConditions()) {
if (!condition.test(ellie, player)) return;
}
for (var effect : choice.getEffects()) {
effect.apply(ellie, player);
}
String nextId = choice.getNextNodeId();
if (nextId.equals("__end__")) {
state.remove(ellie.getId());
return;
}
state.put(ellie.getId(), nextId);
sendNode(ellie, player, nextId);
}
private void sendNode(EllieEntity ellie, Player player, String nodeId) {
DialogNode node = DialogLoader.get().getNode(nodeId);
if (node == null) return;
ModNetworking.CHANNEL.send(
PacketDistributor.PLAYER.with(() -> (net.minecraft.server.level.ServerPlayer) player),
new DialogNodePacket(ellie.getId(), node)
);
}
private String getStartNode(EllieEntity ellie, Player player) {
for (DialogNode node : DialogLoader.get().getAllNodes()) {
boolean allMatch = true;
boolean hasConditions = false;
for (var choice : node.getChoices()) {
for (var cond : choice.getConditions()) {
hasConditions = true;
if (!cond.test(ellie, player)) {
allMatch = false;
break;
}
}
if (!allMatch) break;
}
if (hasConditions && allMatch) return node.getId();
}
return "greeting";
}
}
@@ -0,0 +1,46 @@
package me.sashegdev.fabled_hearts.dialog;
import me.sashegdev.fabled_hearts.api.dialog.IDialogCondition;
import me.sashegdev.fabled_hearts.api.dialog.IDialogEffect;
import java.util.List;
public class DialogNode {
private final String id;
private final String text;
private final String animation;
private final List<DialogChoice> choices;
public DialogNode(String id, String text, String animation, List<DialogChoice> choices) {
this.id = id;
this.text = text;
this.animation = animation;
this.choices = choices;
}
public String getId() { return id; }
public String getText() { return text; }
public String getAnimation() { return animation; }
public List<DialogChoice> getChoices() { return choices; }
public static class DialogChoice {
private final String text;
private final String nextNodeId;
private final List<IDialogCondition> conditions;
private final List<IDialogEffect> effects;
public DialogChoice(String text, String nextNodeId,
List<IDialogCondition> conditions,
List<IDialogEffect> effects) {
this.text = text;
this.nextNodeId = nextNodeId;
this.conditions = conditions;
this.effects = effects;
}
public String getText() { return text; }
public String getNextNodeId() { return nextNodeId; }
public List<IDialogCondition> getConditions() { return conditions; }
public List<IDialogEffect> getEffects() { return effects; }
}
}
@@ -0,0 +1,70 @@
package me.sashegdev.fabled_hearts.dialog;
import me.sashegdev.fabled_hearts.Main;
import me.sashegdev.fabled_hearts.entity.ellie.EllieEntity;
import me.sashegdev.fabled_hearts.network.DialogChoicePacket;
import me.sashegdev.fabled_hearts.network.ModNetworking;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import java.util.List;
public class DialogScreen extends Screen {
private static final ResourceLocation BG = new ResourceLocation(Main.MODID, "textures/gui/dialog_bg.png");
private final EllieEntity ellie;
private final String dialogText;
private final List<String> choices;
private int textAnimTicks;
public DialogScreen(EllieEntity ellie, String dialogText, List<String> choices) {
super(Component.literal("Dialog"));
this.ellie = ellie;
this.dialogText = dialogText;
this.choices = choices;
}
@Override
protected void init() {
int centerX = width / 2;
int baseY = height - 100;
for (int i = 0; i < choices.size(); i++) {
final int idx = i;
int y = baseY + i * 25;
addRenderableWidget(Button.builder(
Component.literal("§e▸ " + choices.get(i)),
btn -> selectChoice(idx)
).bounds(centerX - 150, y, 300, 20).build());
}
}
private void selectChoice(int index) {
ModNetworking.CHANNEL.sendToServer(new DialogChoicePacket(ellie.getId(), index));
Minecraft.getInstance().setScreen(null);
}
@Override
public void render(GuiGraphics gui, int mouseX, int mouseY, float partialTick) {
renderBackground(gui);
super.render(gui, mouseX, mouseY, partialTick);
int centerX = width / 2;
textAnimTicks++;
int charsToShow = Math.min(textAnimTicks / 2, dialogText.length());
String displayed = dialogText.substring(0, charsToShow);
int textY = 30;
gui.drawString(font, "§l§dEllie", centerX - 150, textY, 0xFFFFFF);
gui.drawWordWrap(font, Component.literal(displayed), centerX - 150, textY + 20, 300, 0xEEEEEE);
}
@Override
public boolean isPauseScreen() {
return false;
}
}
@@ -0,0 +1,20 @@
package me.sashegdev.fabled_hearts.dialog.conditions;
import com.google.gson.JsonObject;
import me.sashegdev.fabled_hearts.api.dialog.IDialogCondition;
import me.sashegdev.fabled_hearts.entity.ellie.EllieEntity;
import net.minecraft.world.entity.player.Player;
public class FatigueCondition implements IDialogCondition, IConditionWithJson {
private int maxTicks = 48000;
@Override
public void configure(JsonObject obj) {
if (obj.has("max_ticks")) this.maxTicks = obj.get("max_ticks").getAsInt();
}
@Override
public boolean test(EllieEntity ellie, Player player) {
return ellie.getTicksWithoutSleep() <= maxTicks;
}
}
@@ -0,0 +1,26 @@
package me.sashegdev.fabled_hearts.dialog.conditions;
import com.google.gson.JsonObject;
import me.sashegdev.fabled_hearts.api.dialog.IDialogCondition;
import me.sashegdev.fabled_hearts.entity.ellie.EllieEntity;
import net.minecraft.world.entity.player.Player;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
public class FirstDialogCondition implements IDialogCondition, IConditionWithJson {
private static final Set<UUID> talkedTo = new HashSet<>();
@Override
public void configure(JsonObject obj) {}
@Override
public boolean test(EllieEntity ellie, Player player) {
return !talkedTo.contains(player.getUUID());
}
public static void markTalked(Player player) {
talkedTo.add(player.getUUID());
}
}
@@ -0,0 +1,7 @@
package me.sashegdev.fabled_hearts.dialog.conditions;
import com.google.gson.JsonObject;
public interface IConditionWithJson {
void configure(JsonObject obj);
}
@@ -0,0 +1,37 @@
package me.sashegdev.fabled_hearts.dialog.conditions;
import com.google.gson.JsonObject;
import me.sashegdev.fabled_hearts.api.dialog.IDialogCondition;
import me.sashegdev.fabled_hearts.entity.ellie.EllieEntity;
import net.minecraft.world.entity.player.Player;
public class RelationshipCondition {
public static class Min implements IDialogCondition, IConditionWithJson {
private float min = 0;
@Override
public void configure(JsonObject obj) {
if (obj.has("value")) this.min = obj.get("value").getAsFloat();
}
@Override
public boolean test(EllieEntity ellie, Player player) {
return ellie.getRelationshipPoints() >= min;
}
}
public static class Max implements IDialogCondition, IConditionWithJson {
private float max = 100;
@Override
public void configure(JsonObject obj) {
if (obj.has("value")) this.max = obj.get("value").getAsFloat();
}
@Override
public boolean test(EllieEntity ellie, Player player) {
return ellie.getRelationshipPoints() <= max;
}
}
}
@@ -0,0 +1,20 @@
package me.sashegdev.fabled_hearts.dialog.conditions;
import com.google.gson.JsonObject;
import me.sashegdev.fabled_hearts.api.dialog.IDialogCondition;
import me.sashegdev.fabled_hearts.entity.ellie.EllieEntity;
import net.minecraft.world.entity.player.Player;
public class TimeCondition implements IDialogCondition, IConditionWithJson {
private boolean night = false;
@Override
public void configure(JsonObject obj) {
if (obj.has("night")) this.night = obj.get("night").getAsBoolean();
}
@Override
public boolean test(EllieEntity ellie, Player player) {
return ellie.level().isNight() == night;
}
}
@@ -0,0 +1,22 @@
package me.sashegdev.fabled_hearts.dialog.effects;
import com.google.gson.JsonObject;
import me.sashegdev.fabled_hearts.api.dialog.IDialogEffect;
import me.sashegdev.fabled_hearts.entity.ellie.EllieEntity;
import net.minecraft.world.entity.player.Player;
public class AddRelationshipEffect implements IDialogEffect, IEffectWithJson {
private float amount = 5;
private boolean markFirstDialog = false;
@Override
public void configure(JsonObject obj) {
if (obj.has("value")) this.amount = obj.get("value").getAsFloat();
if (obj.has("mark_first_dialog")) this.markFirstDialog = obj.get("mark_first_dialog").getAsBoolean();
}
@Override
public void apply(EllieEntity ellie, Player player) {
ellie.addRelationshipPoints(amount);
}
}
@@ -0,0 +1,7 @@
package me.sashegdev.fabled_hearts.dialog.effects;
import com.google.gson.JsonObject;
public interface IEffectWithJson {
void configure(JsonObject obj);
}
@@ -0,0 +1,430 @@
package me.sashegdev.fabled_hearts.entity.ellie;
import me.sashegdev.fabled_hearts.ai.BasicAI;
import me.sashegdev.fabled_hearts.ai.EllieSleepGoal;
import me.sashegdev.fabled_hearts.ai.IdleAnimationGoal;
import me.sashegdev.fabled_hearts.network.ModNetworking;
import me.sashegdev.fabled_hearts.network.OpenDialogPacket;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.syncher.EntityDataAccessor;
import net.minecraft.network.syncher.EntityDataSerializers;
import net.minecraft.network.syncher.SynchedEntityData;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.*;
import net.minecraft.world.entity.ai.attributes.AttributeSupplier;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.ai.navigation.GroundPathNavigation;
import net.minecraft.world.entity.animal.Animal;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.BedBlock;
import net.minecraft.world.level.block.DoorBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BedPart;
import net.minecraft.server.level.ServerLevel;
import software.bernie.geckolib.animatable.GeoEntity;
import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache;
import software.bernie.geckolib.core.animation.AnimatableManager;
import software.bernie.geckolib.core.animation.AnimationController;
import software.bernie.geckolib.core.animation.AnimationState;
import software.bernie.geckolib.core.animation.RawAnimation;
import software.bernie.geckolib.core.object.PlayState;
import software.bernie.geckolib.util.GeckoLibUtil;
public class EllieEntity extends Animal implements GeoEntity {
private static final EntityDataAccessor<Boolean> DATA_TIRED =
SynchedEntityData.defineId(EllieEntity.class, EntityDataSerializers.BOOLEAN);
private static final EntityDataAccessor<Boolean> DATA_SLEEPING =
SynchedEntityData.defineId(EllieEntity.class, EntityDataSerializers.BOOLEAN);
private static final EntityDataAccessor<Boolean> DATA_IN_RAIN =
SynchedEntityData.defineId(EllieEntity.class, EntityDataSerializers.BOOLEAN);
private static final EntityDataAccessor<Boolean> DATA_UNDER_LOW_CEILING =
SynchedEntityData.defineId(EllieEntity.class, EntityDataSerializers.BOOLEAN);
private static final EntityDataAccessor<Integer> DATA_IDLE_ANIM =
SynchedEntityData.defineId(EllieEntity.class, EntityDataSerializers.INT);
private static final EntityDataAccessor<Boolean> DATA_EATING =
SynchedEntityData.defineId(EllieEntity.class, EntityDataSerializers.BOOLEAN);
private static final EntityDataAccessor<Integer> DATA_FALL_TICKS =
SynchedEntityData.defineId(EllieEntity.class, EntityDataSerializers.INT);
private static final EntityDataAccessor<Boolean> DATA_MOVING =
SynchedEntityData.defineId(EllieEntity.class, EntityDataSerializers.BOOLEAN);
private final AnimatableInstanceCache cache = GeckoLibUtil.createInstanceCache(this);
private int ticksWithoutSleep;
private float relationshipPoints;
private BlockPos bedPos;
private double lastX, lastZ;
private int moveDetectCooldown;
private BlockPos lastOpenedDoor;
private int doorCloseTimer;
private int crouchTimer;
public EllieEntity(EntityType<? extends Animal> type, Level level) {
super(type, level);
((GroundPathNavigation) this.getNavigation()).setCanOpenDoors(true);
this.setPersistenceRequired();
}
public static AttributeSupplier.Builder createAttributes() {
return Animal.createMobAttributes()
.add(Attributes.MAX_HEALTH, 40.0)
.add(Attributes.MOVEMENT_SPEED, 0.23)
.add(Attributes.FOLLOW_RANGE, 16.0);
}
@Override
protected void defineSynchedData() {
super.defineSynchedData();
this.entityData.define(DATA_TIRED, false);
this.entityData.define(DATA_SLEEPING, false);
this.entityData.define(DATA_IN_RAIN, false);
this.entityData.define(DATA_UNDER_LOW_CEILING, false);
this.entityData.define(DATA_IDLE_ANIM, 0);
this.entityData.define(DATA_EATING, false);
this.entityData.define(DATA_FALL_TICKS, 0);
this.entityData.define(DATA_MOVING, false);
}
@Override
protected void registerGoals() {
BasicAI.addBasicGoals(this, this.goalSelector, 2);
this.goalSelector.addGoal(1, new EllieSleepGoal(this, 20));
this.goalSelector.addGoal(1, new IdleAnimationGoal(this));
}
@Override
public void aiStep() {
super.aiStep();
tickMovementDetection();
tickDoors();
if (!level().isClientSide) {
tickServer();
}
}
private void tickMovementDetection() {
if (moveDetectCooldown-- <= 0) {
moveDetectCooldown = 2;
double dx = getX() - lastX;
double dz = getZ() - lastZ;
lastX = getX();
lastZ = getZ();
}
}
private void tickDoors() {
if (lastOpenedDoor != null) {
BlockState state = level().getBlockState(lastOpenedDoor);
if (state.getBlock() instanceof DoorBlock && state.getValue(DoorBlock.OPEN)) {
double dist = distanceToSqr(lastOpenedDoor.getX() + 0.5, lastOpenedDoor.getY(), lastOpenedDoor.getZ() + 0.5);
if (dist > 0.5 * 0.5) {
doorCloseTimer++;
if (doorCloseTimer > 10) {
level().setBlock(lastOpenedDoor, state.setValue(DoorBlock.OPEN, false), 3);
lastOpenedDoor = null;
doorCloseTimer = 0;
}
} else {
doorCloseTimer = 0;
}
} else {
lastOpenedDoor = null;
doorCloseTimer = 0;
}
}
}
public boolean isMoving() {
double dx = getX() - lastX;
double dz = getZ() - lastZ;
boolean posChanged = dx * dx + dz * dz > 1.0e-8;
boolean navBusy = navigation.isInProgress() && !navigation.isDone();
boolean hasVelocity = getDeltaMovement().horizontalDistanceSqr() > 0.001;
return posChanged || navBusy || hasVelocity;
}
private void tickServer() {
boolean isNight = level().isNight();
boolean raining = level().isRainingAt(blockPosition());
boolean lowCeiling = checkLowCeiling();
boolean tired = ticksWithoutSleep > 48000;
boolean sleeping = entityData.get(DATA_SLEEPING);
boolean falling = !onGround() && getDeltaMovement().y < -0.1;
boolean moving = isMoving();
entityData.set(DATA_MOVING, moving);
entityData.set(DATA_IN_RAIN, raining);
entityData.set(DATA_UNDER_LOW_CEILING, lowCeiling);
entityData.set(DATA_TIRED, tired);
if (falling) {
entityData.set(DATA_FALL_TICKS, entityData.get(DATA_FALL_TICKS) + 1);
} else {
entityData.set(DATA_FALL_TICKS, 0);
}
boolean wasCrouching = getPose() == Pose.CROUCHING;
if (wasCrouching) {
crouchTimer = Math.max(0, crouchTimer - 1);
}
if (sleeping) {
setPose(Pose.SLEEPING);
crouchTimer = 0;
} else if (lowCeiling) {
if (onGround() || wasCrouching) {
setPose(Pose.CROUCHING);
crouchTimer = 20;
}
} else if (wasCrouching && crouchTimer > 0) {
setPose(Pose.CROUCHING);
} else {
if (getPose() != Pose.STANDING) {
setPose(Pose.STANDING);
}
}
if (!sleeping) {
if (isNight) {
ticksWithoutSleep++;
}
} else {
ticksWithoutSleep = Math.max(0, ticksWithoutSleep - 10);
}
}
private boolean checkLowCeiling() {
if (isSpaceLow(blockPosition())) return true;
for (Direction dir : Direction.Plane.HORIZONTAL) {
for (int i = 2; i <= 4; i++) {
if (isSpaceLow(blockPosition().relative(dir, i))) return true;
}
}
if (navigation.isInProgress()) {
var path = navigation.getPath();
if (path != null) {
for (int i = 0; i < path.getNodeCount(); i++) {
BlockPos nodePos = path.getNodePos(i);
double dist = distanceToSqr(
nodePos.getX() + 0.5, nodePos.getY(), nodePos.getZ() + 0.5);
if (dist < 3 * 3 && isSpaceLow(nodePos)) {
return true;
}
}
}
}
return false;
}
public boolean isSpaceLow(BlockPos pos) {
return level().getBlockState(pos.above(1)).blocksMotion();
}
@Override
public EntityDimensions getDimensions(Pose pose) {
if (pose == Pose.SLEEPING) return EntityDimensions.fixed(0.4f, 0.3f);
if (pose == Pose.CROUCHING) return EntityDimensions.fixed(0.6f, 1.5f);
return super.getDimensions(pose);
}
public boolean occupyBed(BlockPos pos) {
BlockState state = level().getBlockState(pos);
if (state.getBlock() instanceof BedBlock) {
if (state.getValue(BedBlock.OCCUPIED)) return false;
BlockPos foot = state.getValue(BedBlock.PART) == BedPart.HEAD
? pos.relative(state.getValue(BedBlock.FACING).getOpposite())
: pos;
level().setBlock(foot, state.setValue(BedBlock.OCCUPIED, true), 3);
BlockPos head = foot.relative(state.getValue(BedBlock.FACING));
BlockState headState = level().getBlockState(head);
if (headState.getBlock() instanceof BedBlock) {
level().setBlock(head, headState.setValue(BedBlock.OCCUPIED, true), 3);
}
bedPos = foot;
return true;
}
return false;
}
public void releaseBed() {
if (bedPos != null) {
BlockState footState = level().getBlockState(bedPos);
if (footState.getBlock() instanceof BedBlock) {
Direction facing = footState.getValue(BedBlock.FACING);
for (BlockPos bp : new BlockPos[]{bedPos, bedPos.relative(facing)}) {
BlockState s = level().getBlockState(bp);
if (s.getBlock() instanceof BedBlock) {
level().setBlock(bp, s.setValue(BedBlock.OCCUPIED, false), 3);
}
}
}
bedPos = null;
}
}
public boolean isBedAt(BlockPos pos) {
return level().getBlockState(pos).isBed(level(), pos, null);
}
public void setBedSleepPos(BlockPos pos) {
BlockState state = level().getBlockState(pos);
if (!(state.getBlock() instanceof BedBlock)) return;
Direction facing = state.getValue(BedBlock.FACING);
BlockPos footPos = state.getValue(BedBlock.PART) == BedPart.HEAD
? pos.relative(facing.getOpposite())
: pos;
float yRot = facing.toYRot();
setPos(footPos.getX() + 0.5, footPos.getY() + 0.5625, footPos.getZ() + 0.5);
setYRot(yRot);
yRotO = yRot;
setYHeadRot(yRot);
setSleeping(true);
setPose(Pose.SLEEPING);
}
public void wakeUp() {
releaseBed();
setSleeping(false);
setPose(Pose.STANDING);
navigation.stop();
setDeltaMovement(getDeltaMovement().add(0, 0.15, 0));
}
public void openDoorInFront() {
for (int i = 0; i < 3; i++) {
BlockPos checkPos = blockPosition().relative(getDirection(), 2 + i);
BlockState state = level().getBlockState(checkPos);
if (state.getBlock() instanceof DoorBlock) {
if (!state.getValue(DoorBlock.OPEN)) {
level().setBlock(checkPos, state.setValue(DoorBlock.OPEN, true), 3);
lastOpenedDoor = checkPos;
doorCloseTimer = 0;
}
return;
}
}
}
@Override
public void registerControllers(AnimatableManager.ControllerRegistrar controllers) {
controllers.add(new AnimationController<>(this, "main", 4, this::animationHandler));
}
private <E extends GeoEntity> PlayState animationHandler(AnimationState<E> state) {
int idleAnim = entityData.get(DATA_IDLE_ANIM);
if (idleAnim > 0) {
String animName = switch (idleAnim) {
case 1 -> "idle1";
case 2 -> "idle2";
case 3 -> "idle3";
default -> null;
};
if (animName != null) {
return state.setAndContinue(RawAnimation.begin().thenPlay(animName));
}
}
RawAnimation anim = resolveAnimationRaw();
return state.setAndContinue(anim);
}
public void triggerRandomIdle(int id) {
if (id >= 1 && id <= 3) {
entityData.set(DATA_IDLE_ANIM, id);
}
}
public void clearRandomIdle() {
entityData.set(DATA_IDLE_ANIM, 0);
}
private RawAnimation resolveAnimationRaw() {
boolean tired = entityData.get(DATA_TIRED);
boolean sleeping = entityData.get(DATA_SLEEPING);
boolean inRain = entityData.get(DATA_IN_RAIN);
boolean lowCeiling = entityData.get(DATA_UNDER_LOW_CEILING);
boolean moving = entityData.get(DATA_MOVING);
boolean eating = entityData.get(DATA_EATING);
int fallTicks = entityData.get(DATA_FALL_TICKS);
if (sleeping) return RawAnimation.begin().thenLoop("sleep");
if (fallTicks > 5) return RawAnimation.begin().thenPlay("fall");
if (eating) return RawAnimation.begin().thenPlay("eat1");
if (tired) return moving
? RawAnimation.begin().thenLoop("walkingsleepy")
: RawAnimation.begin().thenLoop("idlesleepy");
if (inRain) return moving
? RawAnimation.begin().thenLoop("walkingrain")
: RawAnimation.begin().thenLoop("idleconrain");
if (lowCeiling) return moving
? RawAnimation.begin().thenLoop("shiftwalking")
: RawAnimation.begin().thenLoop("shiftidle");
if (moving) return RawAnimation.begin().thenLoop("walkingsimple");
return RawAnimation.begin().thenLoop("idleins");
}
@Override
public AgeableMob getBreedOffspring(ServerLevel level, AgeableMob other) {
return null;
}
@Override
public InteractionResult mobInteract(Player player, InteractionHand hand) {
if (player.isShiftKeyDown()) return InteractionResult.PASS;
if (entityData.get(DATA_SLEEPING)) {
if (!level().isClientSide) wakeUp();
return InteractionResult.SUCCESS;
}
if (entityData.get(DATA_TIRED)) {
return InteractionResult.SUCCESS;
}
if (this.level().isClientSide) {
ModNetworking.CHANNEL.sendToServer(new OpenDialogPacket(this.getId()));
return InteractionResult.SUCCESS;
}
return InteractionResult.SUCCESS;
}
@Override
public void addAdditionalSaveData(CompoundTag tag) {
super.addAdditionalSaveData(tag);
tag.putInt("TicksWithoutSleep", ticksWithoutSleep);
tag.putFloat("Relationship", relationshipPoints);
if (bedPos != null) tag.putLong("BedPos", bedPos.asLong());
}
@Override
public void readAdditionalSaveData(CompoundTag tag) {
super.readAdditionalSaveData(tag);
ticksWithoutSleep = tag.getInt("TicksWithoutSleep");
relationshipPoints = tag.getFloat("Relationship");
if (tag.contains("BedPos")) bedPos = BlockPos.of(tag.getLong("BedPos"));
}
@Override
public AnimatableInstanceCache getAnimatableInstanceCache() { return cache; }
public void setSleeping(boolean s) { entityData.set(DATA_SLEEPING, s); }
public boolean isSleeping() { return entityData.get(DATA_SLEEPING); }
public boolean isTired() { return entityData.get(DATA_TIRED); }
public boolean isInRain() { return entityData.get(DATA_IN_RAIN); }
public boolean isUnderLowCeiling() { return entityData.get(DATA_UNDER_LOW_CEILING); }
public BlockPos getBedPos() { return bedPos; }
public float getRelationshipPoints() { return relationshipPoints; }
public void addRelationshipPoints(float amount) {
this.relationshipPoints = Math.min(100, Math.max(0, this.relationshipPoints + amount));
}
public int getTicksWithoutSleep() { return ticksWithoutSleep; }
}
@@ -0,0 +1,26 @@
package me.sashegdev.fabled_hearts.entity.ellie;
import me.sashegdev.fabled_hearts.Main;
import net.minecraft.resources.ResourceLocation;
import software.bernie.geckolib.model.GeoModel;
public class EllieModel extends GeoModel<EllieEntity> {
private static final ResourceLocation MODEL = new ResourceLocation(Main.MODID, "geo/ellie.geo.json");
private static final ResourceLocation TEXTURE = new ResourceLocation(Main.MODID, "textures/entity/ellie.png");
private static final ResourceLocation ANIMATIONS = new ResourceLocation(Main.MODID, "animations/ellie.animation.json");
@Override
public ResourceLocation getModelResource(EllieEntity object) {
return MODEL;
}
@Override
public ResourceLocation getTextureResource(EllieEntity object) {
return TEXTURE;
}
@Override
public ResourceLocation getAnimationResource(EllieEntity animatable) {
return ANIMATIONS;
}
}
@@ -0,0 +1,38 @@
package me.sashegdev.fabled_hearts.entity.ellie;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.entity.EntityRendererProvider;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.Pose;
import org.jetbrains.annotations.Nullable;
import software.bernie.geckolib.renderer.GeoEntityRenderer;
public class EllieRenderer extends GeoEntityRenderer<EllieEntity> {
public EllieRenderer(EntityRendererProvider.Context renderManager) {
super(renderManager, new EllieModel());
this.shadowRadius = 0.5f;
}
@Override
public RenderType getRenderType(EllieEntity animatable, ResourceLocation texture,
@Nullable MultiBufferSource bufferSource, float partialTick) {
return RenderType.entityTranslucent(texture);
}
@Override
public void render(EllieEntity entity, float entityYaw, float partialTick, PoseStack poseStack,
MultiBufferSource bufferSource, int packedLight) {
if (entity.getPose() == Pose.SLEEPING) {
poseStack.pushPose();
poseStack.translate(0, 0.5625f, 0);
poseStack.mulPose(Axis.XP.rotationDegrees(-90));
super.render(entity, entityYaw, partialTick, poseStack, bufferSource, packedLight);
poseStack.popPose();
} else {
super.render(entity, entityYaw, partialTick, poseStack, bufferSource, packedLight);
}
}
}
@@ -0,0 +1,50 @@
package me.sashegdev.fabled_hearts.entity.ellie;
import me.sashegdev.fabled_hearts.api.girl.INSFWAction;
import me.sashegdev.fabled_hearts.data.RelationshipMilestones;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Player;
import java.util.ArrayList;
import java.util.List;
public class NSFWHandler {
private final EllieEntity ellie;
private boolean inNSFWMode;
private String currentPoseAnimation;
public NSFWHandler(EllieEntity ellie) {
this.ellie = ellie;
}
public boolean canStartNSFW(Player player) {
return RelationshipMilestones.hasReached(ellie.getRelationshipPoints(), RelationshipMilestones.LOVE)
&& !ellie.isTired()
&& !ellie.isSleeping();
}
public void startOral(Player player) {
if (!canStartNSFW(player)) {
player.sendSystemMessage(Component.literal("§cEllie не готова к этому..."));
return;
}
inNSFWMode = true;
currentPoseAnimation = "oral";
player.sendSystemMessage(Component.literal("§5Ellie..."));
}
public void choosePose(Player player, String poseAnimation) {
if (!inNSFWMode) return;
currentPoseAnimation = poseAnimation;
player.sendSystemMessage(Component.literal("§5Смена позы..."));
}
public void stopNSFW(Player player) {
inNSFWMode = false;
currentPoseAnimation = null;
player.sendSystemMessage(Component.literal("§7Всё закончилось..."));
}
public boolean isInNSFWMode() { return inNSFWMode; }
public String getCurrentPoseAnimation() { return currentPoseAnimation; }
}
@@ -0,0 +1,51 @@
package me.sashegdev.fabled_hearts.menu;
import me.sashegdev.fabled_hearts.Main;
import me.sashegdev.fabled_hearts.data.WorldData;
import me.sashegdev.fabled_hearts.registry.ModEntities;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
public class EllieSpawnItem extends Item {
public EllieSpawnItem() {
super(new Item.Properties().stacksTo(1));
}
@Override
public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand hand) {
ItemStack stack = player.getItemInHand(hand);
if (!level.isClientSide) {
ServerLevel serverLevel = (ServerLevel) level;
WorldData data = WorldData.get(serverLevel);
if (data.hasEllie()) {
Entity existing = serverLevel.getEntity(data.getEllieUUID());
if (existing != null) {
player.sendSystemMessage(Component.literal("§eEllie уже здесь!"));
return InteractionResultHolder.success(stack);
} else {
data.removeEllie();
}
}
var entity = ModEntities.ELLIE.get().create(serverLevel);
if (entity != null) {
entity.setPos(player.getX(), player.getY(), player.getZ());
serverLevel.addFreshEntity(entity);
data.setEllieUUID(entity.getUUID());
player.sendSystemMessage(Component.literal("§aEllie появилась!"));
}
}
return InteractionResultHolder.success(stack);
}
}
@@ -0,0 +1,37 @@
package me.sashegdev.fabled_hearts.network;
import me.sashegdev.fabled_hearts.dialog.DialogManager;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.network.NetworkEvent;
import java.util.function.Supplier;
public class DialogChoicePacket {
private final int entityId;
private final int choiceIndex;
public DialogChoicePacket(int entityId, int choiceIndex) {
this.entityId = entityId;
this.choiceIndex = choiceIndex;
}
public void encode(FriendlyByteBuf buf) {
buf.writeInt(entityId);
buf.writeInt(choiceIndex);
}
public static DialogChoicePacket decode(FriendlyByteBuf buf) {
return new DialogChoicePacket(buf.readInt(), buf.readInt());
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx.get().enqueueWork(() -> {
Player player = ctx.get().getSender();
if (player != null && player.level().getEntity(entityId) instanceof me.sashegdev.fabled_hearts.entity.ellie.EllieEntity ellie) {
DialogManager.get().makeChoice(ellie, player, choiceIndex);
}
});
ctx.get().setPacketHandled(true);
}
}
@@ -0,0 +1,68 @@
package me.sashegdev.fabled_hearts.network;
import me.sashegdev.fabled_hearts.dialog.DialogNode;
import net.minecraft.client.Minecraft;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.network.NetworkEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
public class DialogNodePacket {
private final int entityId;
private final String text;
private final String animation;
private final List<String> choiceTexts;
public DialogNodePacket(int entityId, DialogNode node) {
this.entityId = entityId;
this.text = node.getText();
this.animation = node.getAnimation();
this.choiceTexts = new ArrayList<>();
for (var choice : node.getChoices()) {
choiceTexts.add(choice.getText());
}
}
public DialogNodePacket(int entityId, String text, String animation, List<String> choiceTexts) {
this.entityId = entityId;
this.text = text;
this.animation = animation;
this.choiceTexts = choiceTexts;
}
public void encode(FriendlyByteBuf buf) {
buf.writeInt(entityId);
buf.writeUtf(text);
buf.writeUtf(animation);
buf.writeInt(choiceTexts.size());
for (var choice : choiceTexts) buf.writeUtf(choice);
}
public static DialogNodePacket decode(FriendlyByteBuf buf) {
int entityId = buf.readInt();
String text = buf.readUtf();
String animation = buf.readUtf();
int size = buf.readInt();
List<String> choices = new ArrayList<>();
for (int i = 0; i < size; i++) choices.add(buf.readUtf());
return new DialogNodePacket(entityId, text, animation, choices);
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx.get().enqueueWork(() -> {
var minecraft = Minecraft.getInstance();
var level = minecraft.level;
if (level != null && level.getEntity(entityId) instanceof me.sashegdev.fabled_hearts.entity.ellie.EllieEntity ellie) {
minecraft.setScreen(new me.sashegdev.fabled_hearts.dialog.DialogScreen(ellie, text, choiceTexts));
}
});
ctx.get().setPacketHandled(true);
}
public int getEntityId() { return entityId; }
public String getText() { return text; }
public String getAnimation() { return animation; }
public List<String> getChoiceTexts() { return choiceTexts; }
}
@@ -0,0 +1,29 @@
package me.sashegdev.fabled_hearts.network;
import me.sashegdev.fabled_hearts.Main;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.network.NetworkRegistry;
import net.minecraftforge.network.simple.SimpleChannel;
public class ModNetworking {
private static final String PROTOCOL = "1";
public static final SimpleChannel CHANNEL = NetworkRegistry.newSimpleChannel(
new ResourceLocation(Main.MODID, "main"),
() -> PROTOCOL,
PROTOCOL::equals,
PROTOCOL::equals
);
private static int id = 0;
public static void register() {
CHANNEL.registerMessage(id++, OpenDialogPacket.class,
OpenDialogPacket::encode, OpenDialogPacket::decode, OpenDialogPacket::handle);
CHANNEL.registerMessage(id++, DialogChoicePacket.class,
DialogChoicePacket::encode, DialogChoicePacket::decode, DialogChoicePacket::handle);
CHANNEL.registerMessage(id++, DialogNodePacket.class,
DialogNodePacket::encode, DialogNodePacket::decode, DialogNodePacket::handle);
CHANNEL.registerMessage(id++, RelationshipSyncPacket.class,
RelationshipSyncPacket::encode, RelationshipSyncPacket::decode, RelationshipSyncPacket::handle);
}
}
@@ -0,0 +1,34 @@
package me.sashegdev.fabled_hearts.network;
import me.sashegdev.fabled_hearts.dialog.DialogManager;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.network.NetworkEvent;
import java.util.function.Supplier;
public class OpenDialogPacket {
private final int entityId;
public OpenDialogPacket(int entityId) {
this.entityId = entityId;
}
public void encode(FriendlyByteBuf buf) {
buf.writeInt(entityId);
}
public static OpenDialogPacket decode(FriendlyByteBuf buf) {
return new OpenDialogPacket(buf.readInt());
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx.get().enqueueWork(() -> {
Player player = ctx.get().getSender();
if (player != null && player.level().getEntity(entityId) instanceof me.sashegdev.fabled_hearts.entity.ellie.EllieEntity ellie) {
DialogManager.get().startDialog(ellie, player);
}
});
ctx.get().setPacketHandled(true);
}
}
@@ -0,0 +1,36 @@
package me.sashegdev.fabled_hearts.network;
import net.minecraft.client.Minecraft;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.network.NetworkEvent;
import java.util.function.Supplier;
public class RelationshipSyncPacket {
private final int entityId;
private final float relationship;
public RelationshipSyncPacket(int entityId, float relationship) {
this.entityId = entityId;
this.relationship = relationship;
}
public void encode(FriendlyByteBuf buf) {
buf.writeInt(entityId);
buf.writeFloat(relationship);
}
public static RelationshipSyncPacket decode(FriendlyByteBuf buf) {
return new RelationshipSyncPacket(buf.readInt(), buf.readFloat());
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx.get().enqueueWork(() -> {
var level = Minecraft.getInstance().level;
if (level != null && level.getEntity(entityId) instanceof me.sashegdev.fabled_hearts.entity.ellie.EllieEntity ellie) {
ellie.addRelationshipPoints(relationship - ellie.getRelationshipPoints());
}
});
ctx.get().setPacketHandled(true);
}
}
@@ -0,0 +1,25 @@
package me.sashegdev.fabled_hearts.registry;
import me.sashegdev.fabled_hearts.Main;
import me.sashegdev.fabled_hearts.entity.ellie.EllieEntity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.MobCategory;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.registries.RegistryObject;
public class ModEntities {
public static final DeferredRegister<EntityType<?>> ENTITIES =
DeferredRegister.create(ForgeRegistries.ENTITY_TYPES, Main.MODID);
public static final RegistryObject<EntityType<EllieEntity>> ELLIE =
ENTITIES.register("ellie", () -> EntityType.Builder.of(EllieEntity::new, MobCategory.CREATURE)
.sized(0.9f, 2.5f)
.clientTrackingRange(64)
.build("ellie"));
public static void register(IEventBus bus) {
ENTITIES.register(bus);
}
}
@@ -0,0 +1,34 @@
package me.sashegdev.fabled_hearts.registry;
import me.sashegdev.fabled_hearts.Main;
import me.sashegdev.fabled_hearts.menu.EllieSpawnItem;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.CreativeModeTab;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.registries.RegistryObject;
public class ModItems {
public static final DeferredRegister<Item> ITEMS =
DeferredRegister.create(ForgeRegistries.ITEMS, Main.MODID);
public static final DeferredRegister<CreativeModeTab> TABS =
DeferredRegister.create(Registries.CREATIVE_MODE_TAB, Main.MODID);
public static final RegistryObject<Item> ELLIE_SPAWN = ITEMS.register("ellie_spawn", EllieSpawnItem::new);
public static final RegistryObject<CreativeModeTab> FABLED_TAB = TABS.register("fabled_hearts",
() -> CreativeModeTab.builder()
.icon(() -> new ItemStack(ELLIE_SPAWN.get()))
.title(Component.literal("Fabled Hearts"))
.displayItems((params, output) -> output.accept(ELLIE_SPAWN.get()))
.build());
public static void register(IEventBus bus) {
ITEMS.register(bus);
TABS.register(bus);
}
}