Show the pick path in the hover card

Biome title, pick tree with noise bands.
This commit is contained in:
piexel committed 2026-10-21 21:32:03 +02:00
1 parent 97612385e5
commit 86ef09e768
7 files changed
+522 -225

No files matched your search

@@ -1103,7 +1103,7 @@ final class MapCanvas extends JPanel {
double worldZ = screenToWorldZ(point.y); double worldZ = screenToWorldZ(point.y);
controller.submitProbe( controller.submitProbe(
frame, frame,
() -> MapHoverProbe.sample(probeEngine, worldX, worldZ, previewedRegion(), currentType.drawsPickNoise()), () -> MapHoverProbe.sample(probeEngine, worldX, worldZ, previewedRegion(), hoverExplainsBiomePick()),
details -> { details -> {
QuickHover quick = quickHover; QuickHover quick = quickHover;
if (quick != null && quick.worldX() == details.worldX() && quick.worldZ() == details.worldZ()) { if (quick != null && quick.worldX() == details.worldX() && quick.worldZ() == details.worldZ()) {
@@ -1114,6 +1114,11 @@ final class MapCanvas extends JPanel {
); );
} }
// These views' hover card names the surface biome, so the whole way to it is worth looking up.
private boolean hoverExplainsBiomePick() {
return currentType.drawsPickNoise() || currentType == RenderType.HEIGHT;
}
private String previewedRegion() { private String previewedRegion() {
return currentType == RenderType.REGION_PREVIEW ? previewRegionKey : null; return currentType == RenderType.REGION_PREVIEW ? previewRegionKey : null;
} }
@@ -18,258 +18,466 @@
package com.volmit.iris.core.gui; package com.volmit.iris.core.gui;
import com.volmit.iris.core.gui.MapHoverProbe.PickStep;
import com.volmit.iris.core.gui.NoisePickSpread.Band;
import com.volmit.iris.core.gui.components.MapLabels; import com.volmit.iris.core.gui.components.MapLabels;
import com.volmit.iris.core.gui.components.RenderType; import com.volmit.iris.core.gui.components.RenderType;
import com.volmit.iris.core.loader.IrisRegistrant; import com.volmit.iris.core.loader.IrisRegistrant;
import com.volmit.iris.engine.HybridBiomeSelector;
import com.volmit.iris.engine.object.IrisBiome; import com.volmit.iris.engine.object.IrisBiome;
import java.awt.Color; import java.awt.Color;
import java.awt.Font; import java.awt.Font;
import java.awt.FontMetrics; import java.awt.FontMetrics;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.RoundRectangle2D;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale;
/** /**
* The card that follows the cursor: what the drawn map shows there at once, and the rest once probed, in titled * The card that follows the cursor, laid out like IntelliJ's documentation popup: the biome as the title, the steps
* sections of labelled values. * that picked it as a small tree, and with Shift the terrain, location and pack files below.
*/ */
record MapHoverCard(String title, String subtitle, List<Section> sections, String footer) { record MapHoverCard(Header header, String subtitle, String pathTitle, List<PickStep> path, boolean showsBands,
List<Group> groups, List<Hint> hints) {
private static final int MINIMUM_CONTENT_WIDTH = 260;
private static final int PADDING = 12; private static final int PADDING = 12;
private static final int SUBTITLE_GAP = 2; private static final int HEADER_TOP = 10;
private static final int SECTION_GAP = 10; private static final int HEADER_ICON_SIZE = 14;
private static final int SECTION_TITLE_GAP = 4; private static final int NODE_ICON_SIZE = 12;
private static final int ICON_GAP = 8;
private static final int KIND_GAP = 16;
private static final int GROUP_GAP = 8;
private static final int GROUP_TITLE_GAP = 4;
private static final int NODE_HEIGHT = 24;
private static final int NODE_INDENT = 18;
private static final int CHIP_GAP = 16;
private static final int CHIP_PADDING_X = 5;
private static final int CHIP_PADDING_Y = 1;
private static final int BAND_HEIGHT = 4;
private static final int BAND_GAP = 6;
private static final int MARKER_WIDTH = 2;
private static final int MARKER_HEIGHT = 10;
private static final int COLUMN_GAP = 14; private static final int COLUMN_GAP = 14;
private static final int ROW_GAP = 2; private static final int ROW_GAP = 3;
private static final int BOTTOM_PADDING = 10;
private static final int FOOTER_PADDING_Y = 7;
private static final int KEY_PADDING_X = 4;
private static final int KEY_GAP = 4;
private static final int HINT_GAP = 14;
private static final Color ICON_EDGE = new ThemeColor(new Color(0, 0, 0, 38), new Color(255, 255, 255, 38));
private static final Color MARKER_EDGE = new Color(0, 0, 0, 200);
/** /**
* @param subject the biome, or region in the region view, the drawn tile shows; null while that tile is drawing * @param subject the biome, or region in the region view, the drawn tile shows; null while that tile is drawing
* @param region the region the drawn tile shows
* @param details the probed facts about the spot, or null until they arrive * @param details the probed facts about the spot, or null until they arrive
* @param detailed whether Shift is held, which adds the pack files * @param detailed whether Shift is held, which trades the noise bands for terrain, location and pack files
*/ */
static MapHoverCard of(MapLabels.Label subject, MapLabels.Label region, MapHoverProbe.Details details, static MapHoverCard of(MapLabels.Label subject, MapLabels.Label region, MapHoverProbe.Details details,
RenderType type, boolean detailed, double worldX, double worldZ) { RenderType type, boolean detailed, double worldX, double worldZ) {
List<Section> sections = new ArrayList<>(); List<PickStep> path = details == null ? List.of() : details.pickPath();
if (details != null) { IrisBiome biome = cardBiome(subject, details, type);
sections.add(terrainSection(details, type)); List<Group> groups = new ArrayList<>();
addPickSection(sections, details);
}
sections.add(locationSection(worldX, worldZ, detailed));
if (detailed) { if (detailed) {
addFilesSection(sections, subject, region); addTerrainGroup(groups, details, type);
groups.add(locationGroup(worldX, worldZ));
addFilesGroup(groups, biome, details != null ? details.region() : region == null ? null : region.subject());
} }
return new MapHoverCard(title(subject, details), subtitle(region, details, type), sections, return new MapHoverCard(header(subject, biome, type, path), subtitle(details), pathTitle(path), path, !detailed,
detailed ? null : "Shift for pack files · H highlights"); groups, hints(type, detailed));
} }
/** /**
* Draws the card with its top left corner as close to the given point as fits inside the bounds. * Draws the card with its top left corner as close to the given point as fits inside the bounds.
*/ */
void paint(Graphics2D canvas, int anchorX, int anchorY, int boundsWidth, int boundsHeight) { void paint(Graphics2D canvas, int anchorX, int anchorY, int boundsWidth, int boundsHeight) {
Graphics2D card = (Graphics2D) canvas.create();
card.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
card.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
int padding = StudioTheme.scale(PADDING); int padding = StudioTheme.scale(PADDING);
int labelWidth = labelColumnWidth(canvas); int contentWidth = contentWidth(card);
int width = Math.max(headerWidth(canvas), labelWidth + StudioTheme.scale(COLUMN_GAP) + valueColumnWidth(canvas)) + padding * 2; int width = contentWidth + padding * 2;
int height = contentHeight(canvas) + padding * 2; int footerHeight = footerHeight(card);
int height = StudioTheme.scale(HEADER_TOP) + headerHeight(card) + pathHeight(card) + groupsHeight(card)
+ StudioTheme.scale(BOTTOM_PADDING) + footerHeight;
int x = Math.max(padding, Math.min(boundsWidth - width - padding, anchorX)); int x = Math.max(padding, Math.min(boundsWidth - width - padding, anchorX));
int y = Math.max(padding, Math.min(boundsHeight - height - padding, anchorY)); int y = Math.max(padding, Math.min(boundsHeight - height - padding, anchorY));
StudioOverlay.drawCard(canvas, x, y, width, height); StudioOverlay.drawCard(card, x, y, width, height, footerHeight);
int left = x + padding; int left = x + padding;
int top = y + padding; int top = y + StudioTheme.scale(HEADER_TOP);
top = drawLine(canvas, title, StudioTheme.boldFont(), StudioTheme.TEXT, left, top); top = drawHeader(card, left, top, contentWidth);
if (subtitle != null) { top = drawPath(card, left, top, contentWidth);
top = drawLine(canvas, subtitle, StudioTheme.smallFont(), StudioTheme.SECONDARY_TEXT, left, drawGroups(card, left, top);
top + StudioTheme.scale(SUBTITLE_GAP)); drawFooter(card, left, y + height - footerHeight);
} card.dispose();
for (Section section : sections) {
top += StudioTheme.scale(SECTION_GAP);
canvas.setColor(StudioTheme.OVERLAY_BORDER);
canvas.fillRect(left, top, width - padding * 2, 1);
top += StudioTheme.scale(SECTION_GAP) / 2;
top = drawLine(canvas, section.title().toUpperCase(Locale.ROOT), StudioTheme.sectionFont(), StudioTheme.MUTED_TEXT,
left, top) + StudioTheme.scale(SECTION_TITLE_GAP);
for (Row row : section.rows()) {
drawLine(canvas, row.label(), StudioTheme.font(), StudioTheme.SECONDARY_TEXT, left, top);
top = drawLine(canvas, row.value(), StudioTheme.font(), StudioTheme.TEXT,
left + labelWidth + StudioTheme.scale(COLUMN_GAP), top) + StudioTheme.scale(ROW_GAP);
}
}
if (footer != null) {
drawLine(canvas, footer, StudioTheme.smallFont(), StudioTheme.MUTED_TEXT, left, top + StudioTheme.scale(SECTION_GAP));
}
} }
private int labelColumnWidth(Graphics2D canvas) { private int contentWidth(Graphics2D canvas) {
FontMetrics metrics = canvas.getFontMetrics(StudioTheme.font()); int width = Math.max(headerWidth(canvas), pathWidth(canvas));
return sections.stream().flatMap(section -> section.rows().stream()) width = Math.max(width, groupsWidth(canvas));
.mapToInt(row -> metrics.stringWidth(row.label())).max().orElse(0); width = Math.max(width, footerWidth(canvas));
return Math.max(StudioTheme.scale(MINIMUM_CONTENT_WIDTH), width);
} }
private int valueColumnWidth(Graphics2D canvas) { // Header: the biome's colour, its name in bold with its kind on the right, and the ground's height below.
FontMetrics metrics = canvas.getFontMetrics(StudioTheme.font()); private int headerTextOffset() {
return sections.stream().flatMap(section -> section.rows().stream()) return header.color() == null ? 0 : StudioTheme.scale(HEADER_ICON_SIZE + ICON_GAP);
.mapToInt(row -> metrics.stringWidth(row.value())).max().orElse(0);
} }
private int headerWidth(Graphics2D canvas) { private int headerWidth(Graphics2D canvas) {
int width = canvas.getFontMetrics(StudioTheme.boldFont()).stringWidth(title); int width = headerTextOffset() + canvas.getFontMetrics(StudioTheme.boldFont()).stringWidth(header.name());
FontMetrics small = canvas.getFontMetrics(StudioTheme.smallFont()); if (header.kind() != null) {
if (subtitle != null) { width += StudioTheme.scale(KIND_GAP) + canvas.getFontMetrics(StudioTheme.smallFont()).stringWidth(header.kind());
width = Math.max(width, small.stringWidth(subtitle));
} }
if (footer != null) { if (subtitle != null) {
width = Math.max(width, small.stringWidth(footer)); width = Math.max(width, headerTextOffset() + canvas.getFontMetrics(StudioTheme.smallFont()).stringWidth(subtitle));
} }
return width; return width;
} }
private int contentHeight(Graphics2D canvas) { private int headerHeight(Graphics2D canvas) {
int small = canvas.getFontMetrics(StudioTheme.smallFont()).getHeight();
int row = canvas.getFontMetrics(StudioTheme.font()).getHeight() + StudioTheme.scale(ROW_GAP);
int sectionTitle = canvas.getFontMetrics(StudioTheme.sectionFont()).getHeight() + StudioTheme.scale(SECTION_TITLE_GAP);
int height = canvas.getFontMetrics(StudioTheme.boldFont()).getHeight(); int height = canvas.getFontMetrics(StudioTheme.boldFont()).getHeight();
if (subtitle != null) { if (subtitle != null) {
height += StudioTheme.scale(SUBTITLE_GAP) + small; height += canvas.getFontMetrics(StudioTheme.smallFont()).getHeight();
}
for (Section section : sections) {
height += StudioTheme.scale(SECTION_GAP) + StudioTheme.scale(SECTION_GAP) / 2 + sectionTitle + section.rows().size() * row;
}
if (footer != null) {
height += StudioTheme.scale(SECTION_GAP) + small;
} }
return height; return height;
} }
private static int drawLine(Graphics2D canvas, String text, Font font, Color color, int left, int top) { private int drawHeader(Graphics2D canvas, int left, int top, int contentWidth) {
canvas.setFont(font); FontMetrics bold = canvas.getFontMetrics(StudioTheme.boldFont());
canvas.setColor(color); if (header.color() != null) {
FontMetrics metrics = canvas.getFontMetrics(); int iconSize = StudioTheme.scale(HEADER_ICON_SIZE);
canvas.drawString(text, left, top + metrics.getAscent()); drawIcon(canvas, header.color(), left, top + (bold.getHeight() - iconSize) / 2, iconSize);
return top + metrics.getHeight(); }
int textLeft = left + headerTextOffset();
int baseline = top + bold.getAscent();
drawText(canvas, header.name(), StudioTheme.boldFont(), StudioTheme.TEXT, textLeft, baseline);
if (header.kind() != null) {
FontMetrics small = canvas.getFontMetrics(StudioTheme.smallFont());
drawText(canvas, header.kind(), StudioTheme.smallFont(), StudioTheme.MUTED_TEXT,
left + contentWidth - small.stringWidth(header.kind()), baseline);
}
top += bold.getHeight();
if (subtitle != null) {
FontMetrics small = canvas.getFontMetrics(StudioTheme.smallFont());
drawText(canvas, subtitle, StudioTheme.smallFont(), StudioTheme.SECONDARY_TEXT, textLeft, top + small.getAscent());
top += small.getHeight();
}
return top;
} }
private static String title(MapLabels.Label subject, MapHoverProbe.Details details) { // Path: one indented row per step, its value as a chip on the right and, without Shift, the noise bands below it.
if (subject != null) { private int pathWidth(Graphics2D canvas) {
return subject.name(); int width = 0;
for (int level = 0; level < path.size(); level++) {
PickStep step = path.get(level);
int row = nodeTextLeft(level) + canvas.getFontMetrics(StudioTheme.font()).stringWidth(step.picked().name() + " ")
+ canvas.getFontMetrics(StudioTheme.smallFont()).stringWidth(step.reason());
if (step.value() != null) {
row += StudioTheme.scale(CHIP_GAP) + chipWidth(canvas, step.value());
}
width = Math.max(width, row);
} }
if (details != null && details.surfaceBiome() != null) { return width;
return details.surfaceBiome().getName();
}
return "Drawing…";
} }
// The region view's title already is the region, so only the kind of ground is left to say. private int pathHeight(Graphics2D canvas) {
private static String subtitle(MapLabels.Label region, MapHoverProbe.Details details, RenderType type) { if (path.isEmpty()) {
List<String> parts = new ArrayList<>(); return 0;
if (type != RenderType.REGION) { }
String regionName = region != null ? region.name() : details != null ? details.region().getName() : null; int height = groupTitleHeight(canvas);
if (regionName != null) { for (PickStep step : path) {
parts.add(regionName + " region"); height += nodeHeight(step);
}
return height;
}
private int nodeHeight(PickStep step) {
int height = StudioTheme.scale(NODE_HEIGHT);
return showsBands && step.showsBands() ? height + StudioTheme.scale(BAND_HEIGHT + BAND_GAP) : height;
}
private static int nodeTextLeft(int level) {
return StudioTheme.scale(level * NODE_INDENT + NODE_ICON_SIZE + ICON_GAP);
}
private int drawPath(Graphics2D canvas, int left, int top, int contentWidth) {
if (path.isEmpty()) {
return top;
}
top = drawGroupTitle(canvas, pathTitle, left, top);
for (int level = 0; level < path.size(); level++) {
PickStep step = path.get(level);
drawNode(canvas, step, left, top, level, contentWidth);
if (showsBands && step.showsBands()) {
int bandsLeft = left + nodeTextLeft(level);
drawBands(canvas, step, bandsLeft, top + StudioTheme.scale(NODE_HEIGHT), left + contentWidth - bandsLeft);
}
top += nodeHeight(step);
}
return top;
}
private void drawNode(Graphics2D canvas, PickStep step, int left, int top, int level, int contentWidth) {
int rowHeight = StudioTheme.scale(NODE_HEIGHT);
int iconSize = StudioTheme.scale(NODE_ICON_SIZE);
drawIcon(canvas, step.picked().color(), left + StudioTheme.scale(level * NODE_INDENT), top + (rowHeight - iconSize) / 2,
iconSize);
FontMetrics regular = canvas.getFontMetrics(StudioTheme.font());
int baseline = top + (rowHeight - regular.getHeight()) / 2 + regular.getAscent();
int textLeft = left + nodeTextLeft(level);
String name = step.picked().name() + " ";
drawText(canvas, name, StudioTheme.font(), StudioTheme.TEXT, textLeft, baseline);
drawText(canvas, step.reason(), StudioTheme.smallFont(), StudioTheme.MUTED_TEXT, textLeft + regular.stringWidth(name),
baseline);
if (step.value() != null) {
drawChip(canvas, step.value(), left + contentWidth - chipWidth(canvas, step.value()), top, rowHeight);
}
}
// A strip with each option's band of noise in its colour, and a marker where this spot's noise falls.
private static void drawBands(Graphics2D canvas, PickStep step, int left, int top, int width) {
int height = StudioTheme.scale(BAND_HEIGHT);
Shape clip = canvas.getClip();
canvas.clip(new RoundRectangle2D.Double(left, top, width, height, height, height));
for (Band band : step.bands()) {
int from = left + (int) Math.round(band.fromNoise() * width);
int to = left + (int) Math.round(band.toNoise() * width);
canvas.setColor(new Color(band.option().color()));
canvas.fillRect(from, top, Math.max(1, to - from), height);
}
canvas.setClip(clip);
int markerWidth = StudioTheme.scale(MARKER_WIDTH);
int markerHeight = StudioTheme.scale(MARKER_HEIGHT);
int markerX = left + (int) Math.round(Math.clamp(step.noise(), 0D, 1D) * (width - markerWidth));
int markerY = top + (height - markerHeight) / 2;
canvas.setColor(MARKER_EDGE);
canvas.fillRect(markerX - 1, markerY - 1, markerWidth + 2, markerHeight + 2);
canvas.setColor(Color.WHITE);
canvas.fillRect(markerX, markerY, markerWidth, markerHeight);
}
private static Font chipFont() {
return StudioTheme.monospacedFont().deriveFont(StudioTheme.smallFont().getSize2D());
}
private static int chipWidth(Graphics2D canvas, String value) {
return canvas.getFontMetrics(chipFont()).stringWidth(value) + StudioTheme.scale(CHIP_PADDING_X) * 2;
}
// Styled like an inlay hint in IntelliJ's editor.
private static void drawChip(Graphics2D canvas, String value, int left, int rowTop, int rowHeight) {
FontMetrics metrics = canvas.getFontMetrics(chipFont());
int height = metrics.getHeight() + StudioTheme.scale(CHIP_PADDING_Y) * 2;
int top = rowTop + (rowHeight - height) / 2;
canvas.setColor(StudioTheme.HOVER_BACKGROUND);
int arc = StudioTheme.scale(8);
canvas.fillRoundRect(left, top, chipWidth(canvas, value), height, arc, arc);
drawText(canvas, value, chipFont(), StudioTheme.SECONDARY_TEXT, left + StudioTheme.scale(CHIP_PADDING_X),
top + StudioTheme.scale(CHIP_PADDING_Y) + metrics.getAscent());
}
private static void drawIcon(Graphics2D canvas, int rgb, int left, int top, int size) {
int arc = Math.max(2, size / 2);
canvas.setColor(new Color(rgb));
canvas.fillRoundRect(left, top, size, size, arc, arc);
canvas.setColor(ICON_EDGE);
canvas.drawRoundRect(left, top, size - 1, size - 1, arc, arc);
}
// Groups: sentence-case titles over rows of muted labels and values, all labels in one column.
private int labelColumnWidth(Graphics2D canvas) {
FontMetrics metrics = canvas.getFontMetrics(StudioTheme.font());
return groups.stream().flatMap(group -> group.rows().stream())
.mapToInt(row -> metrics.stringWidth(row.label())).max().orElse(0);
}
private int groupsWidth(Graphics2D canvas) {
FontMetrics metrics = canvas.getFontMetrics(StudioTheme.font());
int valueWidth = groups.stream().flatMap(group -> group.rows().stream())
.mapToInt(row -> metrics.stringWidth(row.value())).max().orElse(0);
return labelColumnWidth(canvas) + StudioTheme.scale(COLUMN_GAP) + valueWidth;
}
private int groupsHeight(Graphics2D canvas) {
int rowHeight = canvas.getFontMetrics(StudioTheme.font()).getHeight() + StudioTheme.scale(ROW_GAP);
return groups.stream().mapToInt(group -> groupTitleHeight(canvas) + group.rows().size() * rowHeight).sum();
}
private void drawGroups(Graphics2D canvas, int left, int top) {
FontMetrics regular = canvas.getFontMetrics(StudioTheme.font());
int valueLeft = left + labelColumnWidth(canvas) + StudioTheme.scale(COLUMN_GAP);
for (Group group : groups) {
top = drawGroupTitle(canvas, group.title(), left, top);
for (Row row : group.rows()) {
int baseline = top + regular.getAscent();
drawText(canvas, row.label(), StudioTheme.font(), StudioTheme.SECONDARY_TEXT, left, baseline);
drawText(canvas, row.value(), StudioTheme.font(), row.link() ? StudioTheme.LINK : StudioTheme.TEXT, valueLeft,
baseline);
top += regular.getHeight() + StudioTheme.scale(ROW_GAP);
} }
} }
if (details != null && details.terrainKind() != null) {
parts.add(details.terrainKind());
}
return parts.isEmpty() ? null : String.join(" · ", parts);
} }
private static Section terrainSection(MapHoverProbe.Details details, RenderType type) { private static int groupTitleHeight(Graphics2D canvas) {
return StudioTheme.scale(GROUP_GAP) + canvas.getFontMetrics(StudioTheme.smallFont()).getHeight()
+ StudioTheme.scale(GROUP_TITLE_GAP);
}
private static int drawGroupTitle(Graphics2D canvas, String title, int left, int top) {
FontMetrics small = canvas.getFontMetrics(StudioTheme.smallFont());
top += StudioTheme.scale(GROUP_GAP);
drawText(canvas, title, StudioTheme.smallFont(), StudioTheme.MUTED_TEXT, left, top + small.getAscent());
return top + small.getHeight() + StudioTheme.scale(GROUP_TITLE_GAP);
}
// Footer: the shortcuts as key caps, each followed by what it does.
private int footerWidth(Graphics2D canvas) {
FontMetrics small = canvas.getFontMetrics(StudioTheme.smallFont());
int width = 0;
for (Hint hint : hints) {
width += keyWidth(small, hint.key()) + StudioTheme.scale(KEY_GAP) + small.stringWidth(hint.action());
}
return width + StudioTheme.scale(HINT_GAP) * Math.max(0, hints.size() - 1);
}
private static int footerHeight(Graphics2D canvas) {
return canvas.getFontMetrics(StudioTheme.smallFont()).getHeight() + StudioTheme.scale(FOOTER_PADDING_Y) * 2;
}
private static int keyWidth(FontMetrics small, String key) {
return small.stringWidth(key) + StudioTheme.scale(KEY_PADDING_X) * 2;
}
private void drawFooter(Graphics2D canvas, int left, int top) {
FontMetrics small = canvas.getFontMetrics(StudioTheme.smallFont());
int textTop = top + StudioTheme.scale(FOOTER_PADDING_Y);
int baseline = textTop + small.getAscent();
int arc = StudioTheme.scale(6);
for (Hint hint : hints) {
int keyWidth = keyWidth(small, hint.key());
canvas.setColor(StudioTheme.HOVER_BACKGROUND);
canvas.fillRoundRect(left, textTop, keyWidth, small.getHeight(), arc, arc);
drawText(canvas, hint.key(), StudioTheme.smallFont(), StudioTheme.SECONDARY_TEXT,
left + StudioTheme.scale(KEY_PADDING_X), baseline);
left += keyWidth + StudioTheme.scale(KEY_GAP);
drawText(canvas, hint.action(), StudioTheme.smallFont(), StudioTheme.MUTED_TEXT, left, baseline);
left += small.stringWidth(hint.action()) + StudioTheme.scale(HINT_GAP);
}
}
private static void drawText(Graphics2D canvas, String text, Font font, Color color, int left, int baseline) {
canvas.setFont(font);
canvas.setColor(color);
canvas.drawString(text, left, baseline);
}
// The card is about a biome; the region view's tiles only know the region until the probe finds the biome.
private static IrisBiome cardBiome(MapLabels.Label subject, MapHoverProbe.Details details, RenderType type) {
if (subject != null && subject.subject() instanceof IrisBiome biome) {
return biome;
}
if (details == null) {
return null;
}
return type == RenderType.CAVE_LAND ? details.caveBiome() : details.surfaceBiome();
}
private static Header header(MapLabels.Label subject, IrisBiome biome, RenderType type, List<PickStep> path) {
if (biome != null) {
return new Header(biome.getName(), colorOf(biome, path), type == RenderType.CAVE_LAND ? "Cave biome" : "Biome");
}
if (subject != null) {
return new Header(subject.name(), null, subject.kind() == MapLabels.Kind.REGION ? "Region" : "Biome");
}
return new Header("Drawing…", null, null);
}
private static Integer colorOf(IrisBiome biome, List<PickStep> path) {
return path.stream()
.filter(step -> step.picked().key().equals(biome.getLoadKey()))
.reduce((first, last) -> last)
.map(step -> step.picked().color())
.orElse(null);
}
private static String subtitle(MapHoverProbe.Details details) {
if (details == null) {
return null;
}
return Math.round(details.height()) + " · " + heightAboveSea(details.height() - details.fluidHeight()) + " · slope "
+ Math.round(details.slopeDegrees()) + "°";
}
private static String pathTitle(List<PickStep> path) {
return path.size() > 1 ? "Picked by" : "Region";
}
private static void addTerrainGroup(List<Group> groups, MapHoverProbe.Details details, RenderType type) {
if (details == null) {
return;
}
List<Row> rows = new ArrayList<>(); List<Row> rows = new ArrayList<>();
long height = Math.round(details.height());
rows.add(new Row("Height", height + " · " + heightAboveSea(details.height() - details.fluidHeight())));
rows.add(new Row("Slope", Math.round(details.slopeDegrees()) + "°"));
IrisBiome surface = details.surfaceBiome(); IrisBiome surface = details.surfaceBiome();
IrisBiome cave = details.caveBiome(); IrisBiome cave = details.caveBiome();
if (type == RenderType.CAVE_LAND && surface != null) { if (type == RenderType.CAVE_LAND && surface != null) {
rows.add(new Row("Surface biome", surface.getName())); rows.add(new Row("Surface biome", surface.getName(), false));
} else if (type != RenderType.CAVE_LAND && cave != null && cave != surface) { } else if (type != RenderType.CAVE_LAND && cave != null && cave != surface) {
rows.add(new Row("Cave biome", cave.getName())); rows.add(new Row("Cave biome", cave.getName(), false));
} }
if (details.vanillaBiome() != null) { if (details.vanillaBiome() != null) {
rows.add(new Row("Minecraft", details.vanillaBiome().replace('_', ' '))); rows.add(new Row("Minecraft", details.vanillaBiome().replace('_', ' '), false));
}
return new Section("Terrain", rows);
}
private static void addPickSection(List<Section> sections, MapHoverProbe.Details details) {
List<Row> rows = new ArrayList<>();
MapHoverProbe.PickNoise noise = details.pickNoise();
if (noise != null && noise.regionNoise() != null) {
rows.add(new Row("Region noise", noiseText(noise.regionNoise())));
}
if (noise != null && noise.biomeType() != null) {
String label = capitalize(noise.biomeType().name().toLowerCase(Locale.ROOT)) + " noise";
rows.add(new Row(label, biomeNoiseText(noise, details.surfaceBiome())));
}
if (details.placement() != null) {
rows.add(new Row("Height", placementText(details.placement())));
} }
if (!rows.isEmpty()) { if (!rows.isEmpty()) {
sections.add(new Section("Picked by", rows)); groups.add(new Group("Terrain", rows));
} }
} }
private static String biomeNoiseText(MapHoverProbe.PickNoise noise, IrisBiome surface) { private static Group locationGroup(double worldX, double worldZ) {
String value = noiseText(noise.biomeNoise());
IrisBiome picked = noise.noisePick();
if (picked == null) {
return value;
}
if (surface == null || picked.getLoadKey().equals(surface.getLoadKey())) {
return value + " → " + picked.getName();
}
return value + " → " + picked.getName() + ", child " + surface.getName() + " took over";
}
// How hybrid terrain chose the base biome; a child biome or a coast can still replace it afterwards.
private static String placementText(HybridBiomeSelector.Choice choice) {
String height = heightAboveSea(choice.placementHeight()) + " · ";
if (choice.fallback()) {
return height + "no range covers it, closest wins";
}
if (choice.rivalCount() == 0) {
return height + "only biome in range";
}
return height + "beat " + choice.rivalCount() + (choice.rivalCount() == 1 ? " other" : " others") + " in range";
}
private static Section locationSection(double worldX, double worldZ, boolean detailed) {
int blockX = MapCanvas.floorWorldCoordinate(worldX); int blockX = MapCanvas.floorWorldCoordinate(worldX);
int blockZ = MapCanvas.floorWorldCoordinate(worldZ); int blockZ = MapCanvas.floorWorldCoordinate(worldZ);
List<Row> rows = new ArrayList<>(); return new Group("Location", List.of(
rows.add(new Row("Block", blockX + ", " + blockZ)); new Row("Block", blockX + ", " + blockZ, false),
rows.add(new Row("Chunk", (blockX >> 4) + ", " + (blockZ >> 4))); new Row("Chunk", (blockX >> 4) + ", " + (blockZ >> 4), false),
if (detailed) { new Row("Region file", "r." + (blockX >> 9) + "." + (blockZ >> 9) + ".mca", false)));
rows.add(new Row("Region file", "r." + (blockX >> 9) + "." + (blockZ >> 9) + ".mca"));
}
return new Section("Location", rows);
} }
private static void addFilesSection(List<Section> sections, MapLabels.Label subject, MapLabels.Label region) { private static void addFilesGroup(List<Group> groups, IrisBiome biome, Object region) {
List<Row> rows = new ArrayList<>(); List<Row> rows = new ArrayList<>();
addPackFile(rows, subject); if (biome != null) {
if (subject != null && subject.kind() != MapLabels.Kind.REGION) { rows.add(new Row("Biome", packPath(biome), true));
addPackFile(rows, region); }
if (region instanceof IrisRegistrant registrant) {
rows.add(new Row("Region", packPath(registrant), true));
} }
if (!rows.isEmpty()) { if (!rows.isEmpty()) {
sections.add(new Section("Pack files", rows)); groups.add(new Group("Files", rows));
} }
} }
// The key is the path under the type's folder, so folder, key and extension give the file within the pack. // The key is the path under the type's folder, so folder, key and extension give the file within the pack.
private static void addPackFile(List<Row> rows, MapLabels.Label label) { private static String packPath(IrisRegistrant registrant) {
if (label == null || !(label.subject() instanceof IrisRegistrant registrant)) {
return;
}
String kind = label.kind() == MapLabels.Kind.REGION ? "Region" : "Biome";
File file = registrant.getLoadFile(); File file = registrant.getLoadFile();
String fileName = file == null ? "" : file.getName(); String fileName = file == null ? "" : file.getName();
int extensionStart = fileName.lastIndexOf('.'); int extensionStart = fileName.lastIndexOf('.');
String extension = extensionStart < 0 ? "" : fileName.substring(extensionStart); String extension = extensionStart < 0 ? "" : fileName.substring(extensionStart);
rows.add(new Row(kind, registrant.getFolderName() + "/" + label.key() + extension)); return registrant.getFolderName() + "/" + registrant.getLoadKey() + extension;
}
private static List<Hint> hints(RenderType type, boolean detailed) {
List<Hint> hints = new ArrayList<>();
if (!detailed) {
hints.add(new Hint("Shift", "More"));
}
hints.add(new Hint("H", "Highlight"));
if (type != RenderType.CONTINENT) {
hints.add(new Hint("Alt+Click", "Open file"));
}
return hints;
} }
private static String heightAboveSea(double height) { private static String heightAboveSea(double height) {
@@ -277,17 +485,19 @@ record MapHoverCard(String title, String subtitle, List<Section> sections, Strin
return rounded < 0 ? -rounded + " below sea" : rounded + " above sea"; return rounded < 0 ? -rounded + " below sea" : rounded + " above sea";
} }
private static String noiseText(double noise) { /**
return String.format(Locale.ROOT, "%.3f", noise); * @param color the biome's map colour, or null when no step of the path tells it
* @param kind what the title names, like "Biome", or null while drawing
*/
record Header(String name, Integer color, String kind) {
} }
private static String capitalize(String text) { record Group(String title, List<Row> rows) {
return Character.toUpperCase(text.charAt(0)) + text.substring(1);
} }
record Section(String title, List<Row> rows) { record Row(String label, String value, boolean link) {
} }
record Row(String label, String value) { record Hint(String key, String action) {
} }
} }
@@ -18,6 +18,8 @@
package com.volmit.iris.core.gui; package com.volmit.iris.core.gui;
import com.volmit.iris.core.gui.NoisePickSpread.Band;
import com.volmit.iris.core.gui.NoisePickSpread.Option;
import com.volmit.iris.engine.HybridBiomeSelector; import com.volmit.iris.engine.HybridBiomeSelector;
import com.volmit.iris.engine.IrisComplex; import com.volmit.iris.engine.IrisComplex;
import com.volmit.iris.engine.framework.Engine; import com.volmit.iris.engine.framework.Engine;
@@ -27,6 +29,8 @@ import com.volmit.iris.engine.object.IrisDimension;
import com.volmit.iris.engine.object.IrisRegion; import com.volmit.iris.engine.object.IrisRegion;
import org.bukkit.block.Biome; import org.bukkit.block.Biome;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale; import java.util.Locale;
/** /**
@@ -41,61 +45,108 @@ final class MapHoverProbe {
/** /**
* @param previewRegionKey the region the region preview treats every spot as part of, or null for the real world * @param previewRegionKey the region the region preview treats every spot as part of, or null for the real world
* @param explainPicks also look up the noise values that picked the region and biome, for the region view * @param explainBiomePick whether the card shows the surface biome, so the whole way to it is worth looking up;
* otherwise only how the region was picked
*/ */
static Details sample(Engine engine, double worldX, double worldZ, String previewRegionKey, boolean explainPicks) { static Details sample(Engine engine, double worldX, double worldZ, String previewRegionKey, boolean explainBiomePick) {
IrisComplex complex = engine.getComplex(); IrisComplex complex = engine.getComplex();
IrisRegion previewRegion = previewRegionKey == null ? null : engine.getData().getRegionLoader().load(previewRegionKey); IrisRegion previewRegion = previewRegionKey == null ? null : engine.getData().getRegionLoader().load(previewRegionKey);
IrisRegion region = previewRegion != null ? previewRegion : complex.getRegionStream().get(worldX, worldZ); IrisRegion region = previewRegion != null ? previewRegion : complex.getRegionStream().get(worldX, worldZ);
IrisBiome surfaceBiome = previewRegion != null IrisComplex.BiomePick surfacePick = complex.surfacePickAsIfIn(region, worldX, worldZ);
? complex.surfaceBiomeAsIfIn(previewRegion, worldX, worldZ)
: complex.getBaseBiomeStream().get(worldX, worldZ);
IrisBiome caveBiome = complex.getCaveBiomeStream().get(worldX, worldZ);
InferredType terrainKind = complex.getBridgeStream().get(worldX, worldZ);
Biome vanillaBiome = complex.getTrueBiomeDerivativeStream().get(worldX, worldZ); Biome vanillaBiome = complex.getTrueBiomeDerivativeStream().get(worldX, worldZ);
double height = complex.getHeightStream().getDouble(worldX, worldZ);
double slope = complex.getSlopeStream().getDouble(worldX, worldZ); double slope = complex.getSlopeStream().getDouble(worldX, worldZ);
IrisDimension dimension = engine.getDimension(); IrisDimension dimension = engine.getDimension();
return new Details( return new Details(
worldX, worldX,
worldZ, worldZ,
surfaceBiome, surfacePick.biome(),
caveBiome, complex.getCaveBiomeStream().get(worldX, worldZ),
region, region,
terrainKindName(surfaceBiome, terrainKind),
vanillaBiome == null ? null : vanillaBiome.getKey().getKey(), vanillaBiome == null ? null : vanillaBiome.getKey().getKey(),
height, complex.getHeightStream().getDouble(worldX, worldZ),
dimension == null ? 0D : dimension.getFluidHeight(), dimension == null ? 0D : dimension.getFluidHeight(),
Math.toDegrees(Math.atan(slope / SLOPE_STREAM_RANGE_BLOCKS)), Math.toDegrees(Math.atan(slope / SLOPE_STREAM_RANGE_BLOCKS)),
complex.explainHybridChoice(region, worldX, worldZ), pickPath(engine, region, previewRegion != null, explainBiomePick ? surfacePick : null, worldX, worldZ)
explainPicks ? pickNoise(complex, region, previewRegion, worldX, worldZ) : null
); );
} }
// HYBRID terrain picks biomes by height, which the placement line already tells, so only the region noise is left. // The region preview chose the region itself; the real map picks it by the region noise.
// The preview shows surface biomes, which the height can move to another biome list; the real map shows base biomes. private static List<PickStep> pickPath(Engine engine, IrisRegion region, boolean previewed,
private static PickNoise pickNoise(IrisComplex complex, IrisRegion region, IrisRegion previewRegion, double x, double z) { IrisComplex.BiomePick surfacePick, double x, double z) {
Double regionNoise = previewRegion == null ? complex.getRegionStyleStream().get(x, z) : null; List<PickStep> path = new ArrayList<>();
if (complex.isHybridTerrain() || region == null) { path.add(previewed ? previewedRegionStep(engine, region) : regionStep(engine, region, x, z));
return new PickNoise(regionNoise, null, Double.NaN, null); if (surfacePick == null || surfacePick.type() == null || engine.getComplex().getFocusBiome() != null) {
return path;
} }
InferredType biomeType = previewRegion != null PickStep biomeStep = engine.getComplex().isHybridTerrain()
? complex.surfacePickAsIfIn(region, x, z).type() ? heightStep(engine, region, surfacePick.type(), x, z)
: complex.getBridgeStream().get(x, z); : noiseStep(engine, region, surfacePick.type(), x, z);
return new PickNoise(regionNoise, biomeType, complex.biomePickNoise(region, biomeType, x, z), if (biomeStep == null) {
complex.biomePickedByNoise(region, biomeType, x, z)); return path;
}
path.add(biomeStep);
if (!biomeStep.picked().key().equals(surfacePick.biome().getLoadKey())) {
path.add(new PickStep(PickMeasuring.biomeOption(engine, surfacePick.biome()), "child", null, List.of(), Double.NaN));
}
return path;
} }
// A shore biome sits on land the continent stream calls land or sea, so the biome's own type says it best. private static PickStep regionStep(Engine engine, IrisRegion region, double x, double z) {
private static String terrainKindName(IrisBiome surfaceBiome, InferredType continentKind) { IrisComplex complex = engine.getComplex();
InferredType kind = surfaceBiome != null && surfaceBiome.getInferredType() != null double noise = complex.getRegionStyleStream().get(x, z);
? surfaceBiome.getInferredType() List<Option> regions = complex.noisePickedRegions().stream()
: continentKind; .map(candidate -> PickMeasuring.regionOption(engine, candidate))
if (kind == null) { .toList();
return new PickStep(PickMeasuring.regionOption(engine, region), "region", noiseText(noise),
NoisePickSpread.splitNoise(regions, complex.isLegacyRarity()), noise);
}
private static PickStep previewedRegionStep(Engine engine, IrisRegion region) {
return new PickStep(PickMeasuring.regionOption(engine, region), "previewed", null, List.of(), Double.NaN);
}
private static PickStep noiseStep(Engine engine, IrisRegion region, InferredType type, double x, double z) {
IrisComplex complex = engine.getComplex();
double noise = complex.biomePickNoise(region, type, x, z);
List<Option> biomes = complex.noisePickedBiomes(region, type).stream()
.map(biome -> PickMeasuring.biomeOption(engine, biome))
.toList();
IrisBiome picked = complex.biomePickedByNoise(region, type, x, z);
return new PickStep(PickMeasuring.biomeOption(engine, picked), typeName(type), noiseText(noise),
NoisePickSpread.splitNoise(biomes, complex.isLegacyRarity()), noise);
}
// HYBRID terrain picks by the height a spot is placed at, and only competes where biome ranges share that height.
private static PickStep heightStep(Engine engine, IrisRegion region, InferredType type, double x, double z) {
HybridBiomeSelector.Choice choice = engine.getComplex().explainHybridChoice(region, type, x, z);
if (choice == null) {
return null; return null;
} }
String name = kind.name().toLowerCase(Locale.ROOT); return new PickStep(PickMeasuring.biomeOption(engine, choice.biome()), typeName(type) + ", " + competitionText(choice),
return Character.toUpperCase(name.charAt(0)) + name.substring(1); signedHeightText(choice.placementHeight()), List.of(), Double.NaN);
}
private static String competitionText(HybridBiomeSelector.Choice choice) {
if (choice.fallback()) {
return "nearest range";
}
if (choice.rivalCount() == 0) {
return "only fit";
}
return "beat " + choice.rivalCount();
}
private static String typeName(InferredType type) {
return type.name().toLowerCase(Locale.ROOT);
}
private static String noiseText(double noise) {
return String.format(Locale.ROOT, "%.3f", noise);
}
private static String signedHeightText(double heightAboveSea) {
long rounded = Math.round(heightAboveSea);
return rounded < 0 ? "−" + -rounded : "+" + rounded;
} }
record Details( record Details(
@@ -104,21 +155,24 @@ final class MapHoverProbe {
IrisBiome surfaceBiome, IrisBiome surfaceBiome,
IrisBiome caveBiome, IrisBiome caveBiome,
IrisRegion region, IrisRegion region,
String terrainKind,
String vanillaBiome, String vanillaBiome,
double height, double height,
double fluidHeight, double fluidHeight,
double slopeDegrees, double slopeDegrees,
HybridBiomeSelector.Choice placement, List<PickStep> pickPath
PickNoise pickNoise
) { ) {
} }
/** /**
* @param regionNoise the region noise value, or null when the region preview chose the region * One step on the way to the spot's biome, starting with its region.
* @param biomeType the biome list the biome noise picked from, or null when no noise picked the biome *
* @param noisePick the biome the noise picked, which a child biome can still replace * @param reason what picked it: its kind, the biome list the noise picked from, or "child"
* @param value the noise value or placement height that picked it, or null
* @param bands the noise bands of all the options the noise picked from, empty when no noise picked it
*/ */
record PickNoise(Double regionNoise, InferredType biomeType, double biomeNoise, IrisBiome noisePick) { record PickStep(Option picked, String reason, String value, List<Band> bands, double noise) {
boolean showsBands() {
return !bands.isEmpty();
}
} }
} }
@@ -48,7 +48,14 @@ final class NoisePickSpread {
* @param legacyRarity whether the dimension splits the noise the legacy way * @param legacyRarity whether the dimension splits the noise the legacy way
*/ */
static NoisePickSpread of(String title, List<Option> options, boolean legacyRarity) { static NoisePickSpread of(String title, List<Option> options, boolean legacyRarity) {
return new NoisePickSpread(title, legacyRarity ? legacyBands(options) : bands(options)); return new NoisePickSpread(title, splitNoise(options, legacyRarity));
}
/**
* The band of noise each option gets, in the order the noise meets them.
*/
static List<Band> splitNoise(List<Option> options, boolean legacyRarity) {
return legacyRarity ? legacyBands(options) : bands(options);
} }
void add(double noise) { void add(double noise) {
@@ -58,6 +58,11 @@ final class PickMeasuring {
return new Option(region.getLoadKey(), region.getName(), regionColor(engine, region), region.getRarity()); return new Option(region.getLoadKey(), region.getName(), regionColor(engine, region), region.getRarity());
} }
static Option biomeOption(Engine engine, IrisBiome biome) {
return new Option(biome.getLoadKey(), biome.getName(), biome.getColor(engine, RenderType.BIOME).getRGB() & 0xFFFFFF,
biome.getRarity());
}
static int regionColor(Engine engine, IrisRegion region) { static int regionColor(Engine engine, IrisRegion region) {
return region.getColor(engine.getComplex(), RenderType.REGION).getRGB() & 0xFFFFFF; return region.getColor(engine.getComplex(), RenderType.REGION).getRGB() & 0xFFFFFF;
} }
@@ -161,11 +166,6 @@ final class PickMeasuring {
return Character.toUpperCase(name.charAt(0)) + name.substring(1); return Character.toUpperCase(name.charAt(0)) + name.substring(1);
} }
private static Option biomeOption(Engine engine, IrisBiome biome) {
return new Option(biome.getLoadKey(), biome.getName(), biome.getColor(engine, RenderType.BIOME).getRGB() & 0xFFFFFF,
biome.getRarity());
}
@FunctionalInterface @FunctionalInterface
interface SampleVisitor { interface SampleVisitor {
void visit(double x, double z); void visit(double x, double z);
@@ -18,26 +18,47 @@
package com.volmit.iris.core.gui; package com.volmit.iris.core.gui;
import java.awt.Color;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.RenderingHints; import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.RoundRectangle2D; import java.awt.geom.RoundRectangle2D;
/** /**
* Draws the floating cards (legends, hover details, hints) on top of a map, styled like IntelliJ's popups. * Draws the floating cards (legends, hover details, hints) on top of a map, styled like IntelliJ's popups.
*/ */
final class StudioOverlay { final class StudioOverlay {
private static final Color FOOTER_BACKGROUND = new ThemeColor(0xF7F8FA, 0x27292C);
private StudioOverlay() { private StudioOverlay() {
} }
static void drawCard(Graphics2D canvas, int x, int y, int width, int height) { static void drawCard(Graphics2D canvas, int x, int y, int width, int height) {
Object antialiasing = canvas.getRenderingHint(RenderingHints.KEY_ANTIALIASING); drawCard(canvas, x, y, width, height, 0);
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); }
RoundRectangle2D card = new RoundRectangle2D.Double(x + 0.5D, y + 0.5D, width - 1D, height - 1D,
/**
* Draws a card whose bottom {@code footerHeight} pixels are a darker bar, like the shortcut bar of IntelliJ's popups.
*/
static void drawCard(Graphics2D canvas, int x, int y, int width, int height, int footerHeight) {
Graphics2D card = (Graphics2D) canvas.create();
card.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
RoundRectangle2D outline = new RoundRectangle2D.Double(x + 0.5D, y + 0.5D, width - 1D, height - 1D,
StudioTheme.scale(StudioTheme.OVERLAY_ARC), StudioTheme.scale(StudioTheme.OVERLAY_ARC)); StudioTheme.scale(StudioTheme.OVERLAY_ARC), StudioTheme.scale(StudioTheme.OVERLAY_ARC));
canvas.setColor(StudioTheme.OVERLAY_BACKGROUND); card.setColor(StudioTheme.OVERLAY_BACKGROUND);
canvas.fill(card); card.fill(outline);
canvas.setColor(StudioTheme.OVERLAY_BORDER); if (footerHeight > 0) {
canvas.draw(card); int footerTop = y + height - footerHeight;
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing); Shape cardClip = card.getClip();
card.clip(outline);
card.setColor(FOOTER_BACKGROUND);
card.fillRect(x, footerTop, width, footerHeight);
card.setColor(StudioTheme.OVERLAY_BORDER);
card.fillRect(x, footerTop, width, 1);
card.setClip(cardClip);
}
card.setColor(StudioTheme.OVERLAY_BORDER);
card.draw(outline);
card.dispose();
} }
} }
@@ -328,14 +328,14 @@ public class IrisComplex implements DataProvider {
} }
/** /**
* Why HYBRID terrain gives a spot its base biome when the spot belongs to the given region; null for BIOME terrain. * Why HYBRID terrain picks a biome from the region's biomes of this type at a spot; null for BIOME terrain.
*/ */
public HybridBiomeSelector.Choice explainHybridChoice(IrisRegion region, double x, double z) { public HybridBiomeSelector.Choice explainHybridChoice(IrisRegion region, InferredType type, double x, double z) {
if (!hybridTerrain || focusBiome != null) { if (!hybridTerrain || focusBiome != null) {
return null; return null;
} }
return chooseHybridBiome(region, bridgeStream.get(x, z), x, z); return chooseHybridBiome(region, type, x, z);
} }
private IrisBiome biomeInRegion(IrisRegion region, InferredType type, double x, double z) { private IrisBiome biomeInRegion(IrisRegion region, InferredType type, double x, double z) {