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:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user