diff --git a/src/api b/src/api index 2438b2110..db7173c4e 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 2438b2110e74bfd49125cf74254d98c8b18e2331 +Subproject commit db7173c4ec6b01ffb4ab23fad130164c5815f7dc diff --git a/src/main/java/noppes/npcs/NPCSpawning.java b/src/main/java/noppes/npcs/NPCSpawning.java index 61f9d6ccb..9e9699915 100644 --- a/src/main/java/noppes/npcs/NPCSpawning.java +++ b/src/main/java/noppes/npcs/NPCSpawning.java @@ -28,8 +28,10 @@ import noppes.npcs.scripted.event.CustomNPCsEvent; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Random; import java.util.Set; @@ -37,6 +39,10 @@ public class NPCSpawning { private static Set eligibleChunksForSpawning = Sets.newHashSet(); private static final byte CHUNK_SPAWN_RADIUS = 7; private static final int SPAWN_ATTEMPTS_PER_CHUNK = 3; + private static final String NATURAL_SPAWN_ID_TAG = "CNPCNaturalSpawnId"; + private static final String NATURAL_SPAWN_NAME_TAG = "CNPCNaturalSpawnName"; + private static final String NATURAL_SPAWN_TIME_TAG = "CNPCNaturalSpawnTime"; + private static final Map SPAWN_COOLDOWNS = new HashMap(); private static boolean animalSpawn; private static boolean monsterSpawn; @@ -67,6 +73,7 @@ public class NPCSpawning { if (npcCount >= cap) { return; } + HashMap spawnEntryCounts = getNaturalSpawnEntryCounts(world); ArrayList tmp = new ArrayList(eligibleChunksForSpawning); Collections.shuffle(tmp); Iterator iterator = tmp.iterator(); @@ -95,10 +102,10 @@ public class NPCSpawning { String name = world.getBiomeGenForCoords(x, z).biomeName; SpawnData data = SpawnController.Instance.getRandomSpawnData(name, world.provider.dimensionId); - if (data == null || y < data.spawnHeightMin || y > data.spawnHeightMax || !canCreatureTypeSpawnAtLocation(data, world, x, y, z) || world.getClosestPlayer(x, y, z, 24.0D) != null) + if (data == null) continue; - if (spawnData(data, world, x, y, z)) { + if (trySpawnWithEntryConfig(data, world, x, y, z, world.rand, spawnEntryCounts)) { npcCount++; } } @@ -141,12 +148,108 @@ public class NPCSpawning { return chunkSet.size(); } + private static HashMap getNaturalSpawnEntryCounts(World world) { + HashMap counts = new HashMap(); + List list = world.loadedEntityList; + for (Entity entity : list) { + if (entity == null || entity.isDead) { + continue; + } + NBTTagCompound entityData = entity.getEntityData(); + if (!entityData.hasKey(NATURAL_SPAWN_ID_TAG)) { + continue; + } + int spawnId = entityData.getInteger(NATURAL_SPAWN_ID_TAG); + Integer count = counts.get(spawnId); + counts.put(spawnId, count == null ? 1 : count + 1); + } + return counts; + } + + private static long getSpawnCooldownKey(World world, int spawnId) { + return ((long) world.provider.dimensionId << 32) ^ (spawnId & 0xffffffffL); + } + + private static boolean isSpawnOnCooldown(World world, SpawnData data) { + if (data.cooldownTicks <= 0 || data.id < 0) { + return false; + } + long key = getSpawnCooldownKey(world, data.id); + Long lastSpawnTick = SPAWN_COOLDOWNS.get(key); + if (lastSpawnTick == null) { + return false; + } + long tickDelta = world.getWorldInfo().getWorldTotalTime() - lastSpawnTick; + return tickDelta < data.cooldownTicks; + } + + private static void markSpawnCooldown(World world, SpawnData data) { + if (data.cooldownTicks <= 0 || data.id < 0) { + return; + } + long key = getSpawnCooldownKey(world, data.id); + SPAWN_COOLDOWNS.put(key, world.getWorldInfo().getWorldTotalTime()); + } + + private static boolean isSpawnEntryAtCapacity(SpawnData data, Map spawnEntryCounts) { + if (data.maxAlive <= 0 || data.id < 0) { + return false; + } + Integer currentCount = spawnEntryCounts.get(data.id); + return currentCount != null && currentCount >= data.maxAlive; + } + + private static void incrementSpawnEntryCount(SpawnData data, Map spawnEntryCounts) { + if (data.id < 0) { + return; + } + Integer currentCount = spawnEntryCounts.get(data.id); + spawnEntryCounts.put(data.id, currentCount == null ? 1 : currentCount + 1); + } + + private static boolean hasNearbyPlayer(SpawnData data, World world, int x, int y, int z) { + if (data.playerMinDistance <= 0) { + return false; + } + return world.getClosestPlayer(x, y, z, data.playerMinDistance) != null; + } + + private static boolean trySpawnWithEntryConfig(SpawnData data, World world, int x, int y, int z, Random rand, Map spawnEntryCounts) { + if (y < data.spawnHeightMin || y > data.spawnHeightMax) { + return false; + } + if (isSpawnEntryAtCapacity(data, spawnEntryCounts) || isSpawnOnCooldown(world, data)) { + return false; + } + + int attempts = Math.max(1, data.attemptsPerCycle); + for (int attempt = 0; attempt < attempts; attempt++) { + int attemptX = x; + int attemptZ = z; + if (attempt > 0) { + attemptX += rand.nextInt(5) - rand.nextInt(5); + attemptZ += rand.nextInt(5) - rand.nextInt(5); + } + + if (!canCreatureTypeSpawnAtLocation(data, world, attemptX, y, attemptZ) || hasNearbyPlayer(data, world, attemptX, y, attemptZ)) { + continue; + } + if (spawnData(data, world, attemptX, y, attemptZ)) { + incrementSpawnEntryCount(data, spawnEntryCounts); + markSpawnCooldown(world, data); + return true; + } + } + return false; + } + public static void performWorldGenSpawning(World world, int x, int z, Random rand) { int cap = getCap(world); int npcCount = countNPCs(world); if (npcCount >= cap) { return; } + HashMap spawnEntryCounts = getNaturalSpawnEntryCounts(world); BiomeGenBase biome = world.getBiomeGenForCoords(x + 8, z + 8); while (rand.nextFloat() < biome.getSpawningChance()) { if (npcCount >= cap) { @@ -174,13 +277,13 @@ public class NPCSpawning { l2 = l2 + (int) ((data.spawnHeightMax - l2) * Math.random()); } - if (!canCreatureTypeSpawnAtLocation(data, world, j1, l2, k1)) { + if (!trySpawnWithEntryConfig(data, world, j1, l2, k1, rand, spawnEntryCounts)) { j1 += rand.nextInt(5) - rand.nextInt(5); for (k1 += rand.nextInt(5) - rand.nextInt(5); j1 < x || j1 >= x + size || k1 < z || k1 >= z + size; k1 = i2 + rand.nextInt(5) - rand.nextInt(5)) { j1 = l1 + rand.nextInt(5) - rand.nextInt(5); } - } else if (spawnData(data, world, j1, l2, k1)) { + } else { npcCount++; break; } @@ -226,14 +329,22 @@ public class NPCSpawning { if (spawnEntity instanceof EntityCustomNpc) { EntityCustomNpc npc = (EntityCustomNpc) spawnEntity; npc.stats.spawnCycle = 3; - npc.stats.canDespawn = true; - npc.stats.playerSetCanDespawn = true; + if (data.despawnMode == SpawnData.DESPAWN_FORCE_PERSISTENT) { + npc.stats.canDespawn = false; + npc.stats.playerSetCanDespawn = false; + } else if (data.despawnMode == SpawnData.DESPAWN_FORCE_NATURAL) { + npc.stats.canDespawn = true; + npc.stats.playerSetCanDespawn = true; + } npc.ais.returnToStart = false; npc.ais.startPos = new int[]{x, y, z}; npc.updateAI = true; npc.updateClient = true; npc.getNavigator().clearPathEntity(); } + entityliving.getEntityData().setInteger(NATURAL_SPAWN_ID_TAG, data.id); + entityliving.getEntityData().setString(NATURAL_SPAWN_NAME_TAG, data.name); + entityliving.getEntityData().setLong(NATURAL_SPAWN_TIME_TAG, world.getWorldInfo().getWorldTotalTime()); spawnEntity.setLocationAndAngles(x + 0.5, y, z + 0.5, world.rand.nextFloat() * 360.0F, 0.0F); } catch (Exception exception) { exception.printStackTrace(); diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java b/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java index 29c33352b..caabe3d11 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java @@ -5,6 +5,7 @@ import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcButtonYesNo; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.GuiScrollWindow; import noppes.npcs.client.gui.util.ITextfieldListener; import noppes.npcs.client.gui.util.SubGuiInterface; import noppes.npcs.controllers.data.SpawnData; @@ -22,30 +23,90 @@ public class SubGuiSpawningOptions extends SubGuiInterface implements ITextfield public void initGui() { super.initGui(); - int y = guiTop - 15; + int scrollX = guiLeft + 4; + int scrollY = guiTop + 4; + int scrollW = xSize - 8; + int scrollH = ySize - 34; - addButton(new GuiNpcButtonYesNo(10, guiLeft + 120, y += 22, data.animalSpawning)); - addLabel(new GuiNpcLabel(10, "spawning.animalSpawning", guiLeft + 4, y + 5)); + GuiScrollWindow sw = new GuiScrollWindow(this, scrollX, scrollY, scrollW, scrollH, 0); + sw.drawDefaultBackground = false; + addScrollableGui(0, sw); - addButton(new GuiNpcButtonYesNo(11, guiLeft + 120, y += 22, data.monsterSpawning)); - addLabel(new GuiNpcLabel(11, "spawning.monsterSpawning", guiLeft + 4, y + 5)); + int labelX = 4; + int controlX = 132; + int y = 6; - addButton(new GuiNpcButtonYesNo(12, guiLeft + 120, y += 22, data.liquidSpawning)); - addLabel(new GuiNpcLabel(12, "spawning.liquidSpawning", guiLeft + 4, y + 5)); + sw.addButton(new GuiNpcButtonYesNo(10, controlX, y, data.animalSpawning)); + sw.addLabel(new GuiNpcLabel(10, "spawning.animalSpawning", labelX, y + 5)); + sw.getButton(10).setHoverText("tooltip.naturalspawns.options.animal"); + y += 22; - addButton(new GuiNpcButtonYesNo(13, guiLeft + 120, y += 22, data.airSpawning)); - addLabel(new GuiNpcLabel(13, "spawning.airSpawning", guiLeft + 4, y + 5)); + sw.addButton(new GuiNpcButtonYesNo(11, controlX, y, data.monsterSpawning)); + sw.addLabel(new GuiNpcLabel(11, "spawning.monsterSpawning", labelX, y + 5)); + sw.getButton(11).setHoverText("tooltip.naturalspawns.options.monster"); + y += 22; - addTextField(new GuiNpcTextField(14, this, guiLeft + 120, y += 22, 40, 20, "" + data.spawnHeightMin)); - addLabel(new GuiNpcLabel(14, "spawning.minHeight", guiLeft + 4, y + 5)); - addTextField(new GuiNpcTextField(15, this, guiLeft + 120, y += 22, 40, 20, "" + data.spawnHeightMax)); - addLabel(new GuiNpcLabel(15, "spawning.maxHeight", guiLeft + 4, y + 5)); - getTextField(14).integersOnly = true; - getTextField(14).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - getTextField(15).integersOnly = true; - getTextField(15).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); + sw.addButton(new GuiNpcButtonYesNo(12, controlX, y, data.liquidSpawning)); + sw.addLabel(new GuiNpcLabel(12, "spawning.liquidSpawning", labelX, y + 5)); + sw.getButton(12).setHoverText("tooltip.naturalspawns.options.liquid"); + y += 22; - addButton(new GuiNpcButton(100, guiLeft + 154, guiTop + 192, 60, 20, "gui.done")); + sw.addButton(new GuiNpcButtonYesNo(13, controlX, y, data.airSpawning)); + sw.addLabel(new GuiNpcLabel(13, "spawning.airSpawning", labelX, y + 5)); + sw.getButton(13).setHoverText("tooltip.naturalspawns.options.air"); + y += 22; + + sw.addTextField(new GuiNpcTextField(14, this, controlX, y, 60, 20, "" + data.spawnHeightMin)); + sw.addLabel(new GuiNpcLabel(14, "spawning.minHeight", labelX, y + 5)); + sw.getTextField(14).integersOnly = true; + sw.getTextField(14).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); + sw.getTextField(14).setHoverText("tooltip.naturalspawns.options.minHeight"); + y += 22; + + sw.addTextField(new GuiNpcTextField(15, this, controlX, y, 60, 20, "" + data.spawnHeightMax)); + sw.addLabel(new GuiNpcLabel(15, "spawning.maxHeight", labelX, y + 5)); + sw.getTextField(15).integersOnly = true; + sw.getTextField(15).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); + sw.getTextField(15).setHoverText("tooltip.naturalspawns.options.maxHeight"); + y += 22; + + sw.addTextField(new GuiNpcTextField(16, this, controlX, y, 60, 20, "" + data.maxAlive)); + sw.addLabel(new GuiNpcLabel(16, "spawning.maxAlive", labelX, y + 5)); + sw.getTextField(16).integersOnly = true; + sw.getTextField(16).setMinMaxDefault(0, Integer.MAX_VALUE, 0); + sw.getTextField(16).setHoverText("tooltip.naturalspawns.options.maxAlive"); + y += 22; + + sw.addTextField(new GuiNpcTextField(17, this, controlX, y, 60, 20, "" + data.cooldownTicks)); + sw.addLabel(new GuiNpcLabel(17, "spawning.cooldownTicks", labelX, y + 5)); + sw.getTextField(17).integersOnly = true; + sw.getTextField(17).setMinMaxDefault(0, Integer.MAX_VALUE, 0); + sw.getTextField(17).setHoverText("tooltip.naturalspawns.options.cooldownTicks"); + y += 22; + + sw.addTextField(new GuiNpcTextField(18, this, controlX, y, 60, 20, "" + data.attemptsPerCycle)); + sw.addLabel(new GuiNpcLabel(18, "spawning.attemptsPerCycle", labelX, y + 5)); + sw.getTextField(18).integersOnly = true; + sw.getTextField(18).setMinMaxDefault(1, Integer.MAX_VALUE, 1); + sw.getTextField(18).setHoverText("tooltip.naturalspawns.options.attemptsPerCycle"); + y += 22; + + sw.addTextField(new GuiNpcTextField(19, this, controlX, y, 60, 20, "" + data.playerMinDistance)); + sw.addLabel(new GuiNpcLabel(19, "spawning.playerMinDistance", labelX, y + 5)); + sw.getTextField(19).integersOnly = true; + sw.getTextField(19).setMinMaxDefault(0, Integer.MAX_VALUE, 24); + sw.getTextField(19).setHoverText("tooltip.naturalspawns.options.playerMinDistance"); + y += 22; + + sw.addButton(new GuiNpcButton(20, controlX, y, 74, 20, + new String[]{"spawning.despawn.forceNatural", "spawning.despawn.preserve", "spawning.despawn.forcePersistent"}, data.despawnMode)); + sw.addLabel(new GuiNpcLabel(20, "spawning.despawnMode", labelX, y + 5)); + sw.getButton(20).setHoverText("tooltip.naturalspawns.options.despawnMode"); + y += 22; + + sw.maxScrollY = Math.max(0, y - sw.clipHeight + 8); + + addButton(new GuiNpcButton(100, guiLeft + xSize - 64, guiTop + ySize - 24, 60, 20, "gui.done")); } @Override @@ -64,6 +125,9 @@ public class SubGuiSpawningOptions extends SubGuiInterface implements ITextfield if (button.id == 13) { data.airSpawning = button.getValue() == 1; } + if (button.id == 20) { + data.setDespawnMode(button.getValue()); + } if (button.id == 100) { close(); } @@ -77,5 +141,17 @@ public class SubGuiSpawningOptions extends SubGuiInterface implements ITextfield if (textfield.id == 15) { data.spawnHeightMax = textfield.getInteger(); } + if (textfield.id == 16) { + data.setMaxAlive(textfield.getInteger()); + } + if (textfield.id == 17) { + data.setCooldownTicks(textfield.getInteger()); + } + if (textfield.id == 18) { + data.setAttemptsPerCycle(textfield.getInteger()); + } + if (textfield.id == 19) { + data.setPlayerMinDistance(textfield.getInteger()); + } } } diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java index ecdec2990..9a0fa727d 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java @@ -62,6 +62,12 @@ public class GuiNpcNaturalSpawns extends GuiNPCInterface2 implements IGuiData, I this.addButton(new GuiNpcButton(1, guiLeft + 358, guiTop + 38, 58, 20, "gui.add")); this.addButton(new GuiNpcButton(2, guiLeft + 358, guiTop + 61, 58, 20, "gui.remove")); + getButton(1).setHoverText("tooltip.naturalspawns.main.addProfile"); + getButton(2).setHoverText("tooltip.naturalspawns.main.removeProfile"); + + if (getTextField(55) != null) { + getTextField(55).setHoverText("tooltip.naturalspawns.main.search"); + } if (this.spawn.id >= 0) showSpawn(); @@ -74,15 +80,18 @@ public class GuiNpcNaturalSpawns extends GuiNPCInterface2 implements IGuiData, I int y = guiTop + 30; addLabel(new GuiNpcLabel(3, "spawning.biomes", guiLeft + 4, y)); addButton(new GuiNpcButton(3, guiLeft + 40, y - 5, 50, 20, "selectServer.edit")); + getButton(3).setHoverText("tooltip.naturalspawns.main.biomes"); addLabel(new GuiNpcLabel(11, StatCollector.translateToLocal("Dimensions"), guiLeft + 95, y)); addButton(new GuiNpcButton(11, guiLeft + 150, y - 5, 50, 20, "selectServer.edit")); + getButton(11).setHoverText("tooltip.naturalspawns.main.dimensions"); addLabel(new GuiNpcLabel(4, "spawning.options", guiLeft + 4, y += 22)); addButton(new GuiNpcButton(4, guiLeft + 90, y - 5, 50, 20, "selectServer.edit")); + getButton(4).setHoverText("tooltip.naturalspawns.main.options"); addSlider(new GuiNpcSlider(this, 5, guiLeft + 4, y += 17, 180, 20, (float) this.spawn.itemWeight / 100)); - addExtra(new GuiHoverText(1, "spawning.naturalInfo", guiLeft + 188, y + 5)); + addExtra(new GuiHoverText(1, "spawning.naturalInfoPerformance", guiLeft + 188, y + 5)); this.spawnEntryScroll.guiLeft = guiLeft + 4; this.spawnEntryScroll.guiTop = y + 40; @@ -96,13 +105,17 @@ public class GuiNpcNaturalSpawns extends GuiNPCInterface2 implements IGuiData, I addScroll(this.spawnEntryScroll); addButton(new GuiNpcButton(21, guiLeft + 6, y += 20, 20, 20, "+")); + getButton(21).setHoverText("tooltip.naturalspawns.main.addEntry"); if (this.spawnEntryScroll.hasSelected()) { int selected = Integer.parseInt(this.spawnEntryScroll.getSelected()); addButton(new GuiNpcButton(22, guiLeft + 32, y, 20, 20, "-")); + getButton(22).setHoverText("tooltip.naturalspawns.main.removeEntry"); GuiNpcTextField num = new GuiNpcTextField(25, this, guiLeft + 60, y += 10, 30, 20, "" + selected); num.integersOnly = true; addTextField(num); + getTextField(25).setHoverText("tooltip.naturalspawns.main.slot"); addButton(new GuiNpcButton(26, guiLeft + 92, y, 100, 20, this.getTitle(this.spawn.spawnCompounds.get(selected)))); + getButton(26).setHoverText("tooltip.naturalspawns.main.selectEntry"); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java b/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java index 4ebafd54f..2c0d51a18 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java @@ -82,7 +82,8 @@ public class GuiNpcButton extends GuiButton { } public void setHoverText(String text) { - this.hoverableText = StatCollector.translateToLocal(text); + String translated = StatCollector.translateToLocal(text); + this.hoverableText = translated == null ? "" : translated.replace("\\n", "\n"); } public int getValue() { @@ -201,13 +202,30 @@ public class GuiNpcButton extends GuiButton { Minecraft mc = Minecraft.getMinecraft(); String displayString = StatCollector.translateToLocal(hoverableText); GL11.glColor4f(1, 1, 1, 1); - List lines = TextSplitter.splitText(displayString, 30); + List lines = splitHoverText(displayString, 30); drawHoveringText(lines, i, j, mc); GL11.glDisable(GL11.GL_LIGHTING); GL11.glPopMatrix(); } } + private List splitHoverText(String text, int maxLineLength) { + List lines = new ArrayList(); + if (text == null || text.isEmpty()) + return lines; + + String normalized = text.replace("\\n", "\n"); + String[] explicitLines = normalized.split("\\r?\\n", -1); + for (String line : explicitLines) { + if (line.isEmpty()) { + lines.add(""); + continue; + } + lines.addAll(TextSplitter.splitText(line, maxLineLength)); + } + return lines; + } + protected void drawHoveringText(List textLines, int x, int y, Minecraft mc) { if (mc.fontRenderer == null || textLines.isEmpty()) return; diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java b/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java index 1d82deb61..71b5ed611 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java @@ -12,6 +12,7 @@ import net.minecraft.util.StatCollector; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL12; +import java.util.ArrayList; import java.util.List; public class GuiNpcTextField extends GuiTextField { @@ -304,7 +305,8 @@ public class GuiNpcTextField extends GuiTextField { } public void setHoverText(String text) { - this.hoverableText = StatCollector.translateToLocal(text); + String translated = StatCollector.translateToLocal(text); + this.hoverableText = translated == null ? "" : translated.replace("\\n", "\n"); } public boolean hasHoverText() { @@ -317,10 +319,14 @@ public class GuiNpcTextField extends GuiTextField { } public void drawHover(int mouseX, int mouseY, boolean hasSubGui) { + drawHover(mouseX, mouseY, mouseX, mouseY, hasSubGui); + } + + public void drawHover(int hitMouseX, int hitMouseY, int tooltipMouseX, int tooltipMouseY, boolean hasSubGui) { if (hasSubGui || !enabled || hoverableText.isEmpty()) return; - boolean isHovered = isMouseOver(mouseX, mouseY); + boolean isHovered = isMouseOver(hitMouseX, hitMouseY); if (!isHovered) { wasHovered = false; hoverCount = 0; @@ -338,13 +344,30 @@ public class GuiNpcTextField extends GuiTextField { GL11.glPushMatrix(); Minecraft mc = Minecraft.getMinecraft(); GL11.glColor4f(1, 1, 1, 1); - List lines = TextSplitter.splitText(hoverableText, 30); - drawHoveringText(lines, mouseX, mouseY, mc); + List lines = splitHoverText(hoverableText, 30); + drawHoveringText(lines, tooltipMouseX, tooltipMouseY, mc); GL11.glDisable(GL11.GL_LIGHTING); GL11.glPopMatrix(); } } + private List splitHoverText(String text, int maxLineLength) { + List lines = new ArrayList(); + if (text == null || text.isEmpty()) + return lines; + + String normalized = text.replace("\\n", "\n"); + String[] explicitLines = normalized.split("\\r?\\n", -1); + for (String line : explicitLines) { + if (line.isEmpty()) { + lines.add(""); + continue; + } + lines.addAll(TextSplitter.splitText(line, maxLineLength)); + } + return lines; + } + protected void drawHoveringText(List textLines, int x, int y, Minecraft mc) { if (mc.fontRenderer == null || textLines.isEmpty()) return; diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java b/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java index ab6e612d4..0df1c8547 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java @@ -300,9 +300,14 @@ public class GuiScrollWindow extends GuiScreen implements ITextfieldListener, IC button.drawHover(mouseX, mouseY, subGui); } } - // Label hover: convert screen mouse to scroll-local coords for hit testing int localMouseX = mouseX - xPos; int localMouseY = (int) (mouseY - yPos + scrollY); + for (GuiNpcTextField textField : textfields.values()) { + if (textField.hasHoverText()) { + textField.drawHover(localMouseX, localMouseY, mouseX, mouseY, subGui); + } + } + // Label hover: convert screen mouse to scroll-local coords for hit testing for (GuiNpcLabel label : labels.values()) { if (!label.hoverableText.isEmpty()) { label.drawHover(localMouseX, localMouseY, mouseX, mouseY, subGui, fontRendererObj); diff --git a/src/main/java/noppes/npcs/controllers/data/SpawnData.java b/src/main/java/noppes/npcs/controllers/data/SpawnData.java index f82d9b225..164f451da 100644 --- a/src/main/java/noppes/npcs/controllers/data/SpawnData.java +++ b/src/main/java/noppes/npcs/controllers/data/SpawnData.java @@ -21,6 +21,10 @@ import java.util.Map; import java.util.Set; public class SpawnData extends WeightedRandom.Item implements INaturalSpawn { + public static final int DESPAWN_FORCE_NATURAL = INaturalSpawn.DESPAWN_FORCE_NATURAL; + public static final int DESPAWN_PRESERVE_TEMPLATE = INaturalSpawn.DESPAWN_PRESERVE_TEMPLATE; + public static final int DESPAWN_FORCE_PERSISTENT = INaturalSpawn.DESPAWN_FORCE_PERSISTENT; + public List biomes = new ArrayList<>(); public HashSet dimensions = new HashSet<>(); public int id = -1; @@ -35,6 +39,11 @@ public class SpawnData extends WeightedRandom.Item implements INaturalSpawn { public int spawnHeightMin; public int spawnHeightMax = 100; + public int maxAlive = 0; + public int cooldownTicks = 0; + public int attemptsPerCycle = 1; + public int playerMinDistance = 24; + public int despawnMode = DESPAWN_FORCE_NATURAL; public SpawnData() { super(10); @@ -74,6 +83,19 @@ public class SpawnData extends WeightedRandom.Item implements INaturalSpawn { } else { spawnHeightMax = compound.getInteger("HeightMax"); } + + maxAlive = compound.hasKey("MaxAlive") ? Math.max(0, compound.getInteger("MaxAlive")) : 0; + cooldownTicks = compound.hasKey("CooldownTicks") ? Math.max(0, compound.getInteger("CooldownTicks")) : 0; + attemptsPerCycle = compound.hasKey("AttemptsPerCycle") ? Math.max(1, compound.getInteger("AttemptsPerCycle")) : 1; + playerMinDistance = compound.hasKey("PlayerMinDistance") ? Math.max(0, compound.getInteger("PlayerMinDistance")) : 24; + if (compound.hasKey("DespawnMode")) { + despawnMode = compound.getInteger("DespawnMode"); + } else { + despawnMode = DESPAWN_FORCE_NATURAL; + } + if (despawnMode < DESPAWN_FORCE_NATURAL || despawnMode > DESPAWN_FORCE_PERSISTENT) { + despawnMode = DESPAWN_FORCE_NATURAL; + } } public NBTTagCompound writeNBT(NBTTagCompound compound) { @@ -98,6 +120,11 @@ public class SpawnData extends WeightedRandom.Item implements INaturalSpawn { compound.setInteger("HeightMin", spawnHeightMin); compound.setInteger("HeightMax", spawnHeightMax); + compound.setInteger("MaxAlive", maxAlive); + compound.setInteger("CooldownTicks", cooldownTicks); + compound.setInteger("AttemptsPerCycle", attemptsPerCycle); + compound.setInteger("PlayerMinDistance", playerMinDistance); + compound.setInteger("DespawnMode", despawnMode); return compound; } @@ -204,4 +231,46 @@ public class SpawnData extends WeightedRandom.Item implements INaturalSpawn { public void setBiomes(String[] biomes) { this.biomes = new ArrayList<>(Arrays.asList(biomes)); } + + public void setMaxAlive(int maxAlive) { + this.maxAlive = Math.max(0, maxAlive); + } + + public int getMaxAlive() { + return this.maxAlive; + } + + public void setCooldownTicks(int ticks) { + this.cooldownTicks = Math.max(0, ticks); + } + + public int getCooldownTicks() { + return this.cooldownTicks; + } + + public void setAttemptsPerCycle(int attempts) { + this.attemptsPerCycle = Math.max(1, attempts); + } + + public int getAttemptsPerCycle() { + return this.attemptsPerCycle; + } + + public void setPlayerMinDistance(int distance) { + this.playerMinDistance = Math.max(0, distance); + } + + public int getPlayerMinDistance() { + return this.playerMinDistance; + } + + public void setDespawnMode(int mode) { + if (mode < DESPAWN_FORCE_NATURAL || mode > DESPAWN_FORCE_PERSISTENT) + mode = DESPAWN_FORCE_NATURAL; + this.despawnMode = mode; + } + + public int getDespawnMode() { + return this.despawnMode; + } } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 35d0ac83d..ee44acef2 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -425,7 +425,37 @@ spawning.liquidSpawning=Spawns in Liquid spawning.airSpawning=Spawns in Air spawning.minHeight=Minimum Height spawning.maxHeight=Maximum Height +spawning.maxAlive=Max Alive (0 = Unlimited) +spawning.cooldownTicks=Cooldown (Ticks) +spawning.attemptsPerCycle=Attempts Per Cycle +spawning.playerMinDistance=Min Player Distance +spawning.despawnMode=Despawn Mode +spawning.despawn.forceNatural=Force Natural +spawning.despawn.preserve=Preserve Template +spawning.despawn.forcePersistent=Force Persistent spawning.naturalInfo=Add at least one spawn entry and configure spawn options for the NPC to spawn naturally +spawning.naturalInfoPerformance=Natural Spawn Setup\nBest performance baseline:\n- maxAlive: 20-60\n- cooldownTicks: 200-600\n- attemptsPerCycle: 1-2\n- playerMinDistance: 32-48\n- despawnMode: Force Natural +tooltip.naturalspawns.main.addProfile=Create a new natural spawn profile.\nBest performance: Keep profile count focused by biome/theme. +tooltip.naturalspawns.main.removeProfile=Delete the selected natural spawn profile.\nBest performance: Remove unused profiles to reduce selection overhead. +tooltip.naturalspawns.main.search=Filter spawn profiles by name.\nTip: Use biome/type prefixes for faster maintenance. +tooltip.naturalspawns.main.biomes=Choose biomes this profile can spawn in.\nBest performance: Use only needed biomes. +tooltip.naturalspawns.main.dimensions=Choose dimensions this profile can spawn in.\nBest performance: Limit to required dimensions. +tooltip.naturalspawns.main.options=Open advanced spawn controls.\nBest performance: Configure cap, cooldown, attempts, and despawn mode. +tooltip.naturalspawns.main.addEntry=Add an entry slot to this profile.\nBest performance: Keep only active entries. +tooltip.naturalspawns.main.removeEntry=Remove selected entry slot.\nBest performance: Remove empty/duplicate entries. +tooltip.naturalspawns.main.slot=Entry slot index.\nTip: Keep numbering organized for easier debugging. +tooltip.naturalspawns.main.selectEntry=Select which clone/NPC this slot spawns.\nBest performance: Avoid very heavy scripted clones in high-frequency profiles. +tooltip.naturalspawns.options.animal=Uses animal-style spawn checks for this entry.\nBest performance: Enable only for passive/ground mobs. +tooltip.naturalspawns.options.monster=Uses monster-style spawn checks for this entry.\nBest performance: Enable only for hostile/night mobs. +tooltip.naturalspawns.options.liquid=Allows water/liquid spawn validation.\nBest performance: Keep disabled unless required. +tooltip.naturalspawns.options.air=Allows spawn attempts in open air blocks.\nBest performance: Keep disabled unless required. +tooltip.naturalspawns.options.minHeight=Minimum Y for this entry.\nBest performance: Use tight vertical ranges to reduce failed attempts. +tooltip.naturalspawns.options.maxHeight=Maximum Y for this entry.\nBest performance: Use tight vertical ranges to reduce failed attempts. +tooltip.naturalspawns.options.maxAlive=Per-entry alive cap (loaded entities).\n0 = unlimited (not recommended).\nBest performance: 20-60. +tooltip.naturalspawns.options.cooldownTicks=Ticks to wait after a successful spawn for this entry.\nBest performance: 200-600. +tooltip.naturalspawns.options.attemptsPerCycle=How many positions this entry tries each spawn cycle.\nBest performance: 1-2. +tooltip.naturalspawns.options.playerMinDistance=No-spawn radius from nearest player for this entry.\nBest performance: 32-48 to reduce pop-in and retests. +tooltip.naturalspawns.options.despawnMode=How spawned CNPC despawn flags are applied.\nBest performance: Force Natural.\nPreserve/Force Persistent can increase long-term entity load. role.name=Role role.none=No Role